C++程序设计 湖南大学 杜四春、银红霞
第 13章 多态性与虚函数
13.1 普通成员函数重载
13.2 派生类指针
13.3 虚函数
13.4 纯虚函数与抽象类
C++程序设计 湖南大学 杜四春、银红霞
多态性是面向对象程序设计的重要特征之
一。所谓多态性是指当不同的对象收到相同的
消息时,产生不同的动作。 C++的多态性具体
体现在运行和编译两个方面,在程序运行时的
多态性通过继承和虚函数来体现,而在程序编
译时多态性体现在函数和运算符的重载上。
C++程序设计 湖南大学 杜四春、银红霞
13.1 普通成员函数重载
在 C++语言中, 只有在声明函数原型时形式参数
的个数或者对应位置的类型不同, 两个或更多的函数
就可以共用一个名字 。 这种在同一作用域中允许多个
函 数 使 用 同 一 函 数 名 的 措 施 被 称 为 重 载
( overloading) 。 函数重载是 C++程序获得多态性的
途径之一 。
13.1.1 函数重载的方法
函数重载要求编译器能够唯一地确定调用一个函
数时应执行哪个函数代码,既采用哪个函数实现。确定
函数实现时,要求从函数参数的个数和类型上来区分。
这就是说,进行函数重载时,要求同名函数在参数个
数上不同,或者参数类型不同。否则,将无法实现函
数重载。
C++程序设计 湖南大学 杜四春、银红霞
#include <iostream.h>
int square(int x)
{
return x*x;
}
doublesquare(double y)
{
return y*y;
}
main()
{
cout<<”Thesquare of integer 7 is”<<square(7)<<endl;
cout<<”The square of double 7.5 is”<<square(7.5)<<endl;
return 0;
}
例 13-1:给出以下程序的运行结果。
此程序的运行结果为:
The square of integer 7 is 49
The square of integer 7.5 is 56.25
C++程序设计 湖南大学 杜四春、银红霞
#include <iostream.h>
const double PI=3.1415;
doublelength(float r)
{
return 2*PI*r;
}
doublelength(float x,float y)
{
return 2*(x+y);
}
例 13-2:用重载函数实现求圆和矩形的周长。
void main()
{
float a,b,r;
cout<<”输入圆半径:, ;
cin>>r;
cout<<”圆周长:, <<length(r)<<endl;
cout<<”输入矩形长和宽:, ;
cin>>a>>b;
cout<<”矩形周长:, <<length(r)<<endl;
}
C++程序设计 湖南大学 杜四春、银红霞
13.1.2 函数重载的表示形式
普通成员函数重载可表达为两种形式:
1,在一个类说明中重载
例如,Show ( int,char ) ;
Show ( char *,float ) ;
2,基类的成员函数在派生类重载 。 有 3种编译区分
方法
( 1) 根据参数的特征加以区分
例如,Show ( int,char ) 与
Show ( char *,float )
不是同一函数, 编译能够区分
C++程序设计 湖南大学 杜四春、银红霞
( 2) 使用,,:,加以区分
例如,A,,Show ( )
有别于 B,,Show ( )
( 3) 根据类对象加以区分
例如,Aobj.Show ( )调用 A,,Show ( )
Bobj.Show ( )调用 B,,Show ( )
C++程序设计 湖南大学 杜四春、银红霞
13.1.3 函数重载的注意事项
在 C++语言中, 编译程序选择相应的重载函数版本
时函数返回值类型是不起作用的 。 不能仅靠函数的返
回值来区别重载函数, 必须从形式参数上区别开来 。
例如:
void print(int a);
void print(int a,int b);
int print(float a[]);
这三个函数是重载函数, 因为 C++编译程序可以从
形式参数上将它们区别开来 。
C++程序设计 湖南大学 杜四春、银红霞
但,int f(int a);
double f(int a);
这两个函数就不是重载函数,编译程序认为这是
对一个函数的重复说明,因为两个函数的形式参数个
数与相应位置的类型完全相同。
由 typedef定义的类型别名并没有真正创建一个新
的类型, 所以以下程序段:
typedef double money;
double calculate(double income);
money calculate(money income);
也是错误的函数重载。
C++程序设计 湖南大学 杜四春、银红霞
同样道理, 不同参数传递方式也无法区别重载函
数, 如:
void func(int value);
void func(int &value);
也不能作为重载函数 。
在程序中不可滥用函数重载,不适当的重载会降
低程序的可读性。 C++语言并没有提供任何约束限制
重载函数之间必须有关联,程序员可能用相同的名字
定义两个互不相关的函数。实际上函数重载暗示了一
种关联,不应该重载那些本质上有区别的函数,只有
当函数实现的语义非常相近时才应使用函数重载。
C++程序设计 湖南大学 杜四春、银红霞
13.1.4 函数重载的二义性
函数重载的二义性 ( ambiguity) 是指 C++语言的编
译程序无法在多个重载函数中选择正确的函数进行调
用 。 函数重载的二义性主要源于 C++语言的隐式类型
转换与默认参数 。
在函数调用时,编译程序将按以下规则选择重载
函数:如果函数调用的实际参数类型与一个重载函数
形式参数类型完全匹配,则选择调用该重载函数;如
果找不到与实际参数类型完全匹配的函数原型,但如
果将一个类型转换为更高级类型后能找到完全匹配的
函数原型,编译程序将选择调用该重载函数。所谓更
高级类型是指能处理的值域较大,如 int转换为 unsigned
int,unsigned int转换为 long,long转换为 unsigned float
等。
C++程序设计 湖南大学 杜四春、银红霞
例如,int func(double d);

count<<func(‘A’);
虽未声明函数原型 int func( char), 但函数调用
func( ‘ A’) 并不会产生任何问题, 因为编译程序自
动将字符 ‘ A’转换为 double类型, 然后调用函数 int
func( double) 。
隐式类型转换是由 C++编译程序自动完成的, 这种
类型转换是引起函数重载二义性的主要原因 。 在重载
函数中使用默认参数也可能造成二义性 。
C++还提供了一种更为灵活的多态性机制:虚函
数。虚函数允许函数调用与函数体的联系在运行时才
进行。当一般的类型对应于不同的类型变种时,这个
能力显得尤其重要。
C++程序设计 湖南大学 杜四春、银红霞
13.2 派生类指针
指向基类和派生类的指针是相关的 。
例如:
A * p ; // 指向类型 A 的对象的指针
A A_obj ; // 类型 A 的对象
B B_obj ; // 类型 B 的对象
p = & A_obj ; // p 指向类型 A 的对象
p = & B_obj ; // p 指向类型 B 的对象,
//它是 A 的派生类
利用 p,可以通过 B_obj 访问所有从 A_obj 继承的
元素,但不能用 p访问 B_obj 自身特定的元素 (除非
用了显式类型转换)。
C++程序设计 湖南大学 杜四春、银红霞
例 13-6
注意:可以用一个指向基类的指针指向其公有派
生类的对象。但却不能用指向派生类的指针指向一个
基类对象。希望用基类指针访问其公有派生类的特定
成员,必须将基类指针用显式类型转换为派生类指针。
例如(( B_class *) p) -> show_phone();一个指向
基类的指针可用来指向从基类公有派生的任何对象,
这一事实非常重要,它是 C++实现运行时多态的关键
途径。
C++程序设计 湖南大学 杜四春、银红霞
13.3 虚函数
13.3.1 虚函数的概念
虚函数是在基类中冠以关键字 virtual 的成员函数。
它提供了一种接口界面。虚函数可以在一个或多个派
生类中被重定义。
例 13-7,当 main()函数的语句 1~6随着指针 p所
指对象不同而调用不同版本的 who()函数时,就实现
了运行时的多态,这种机制的实现依赖于在在基类中
把成员函数 who()说明为虚函数。
C++程序设计 湖南大学 杜四春、银红霞
在 C++语言中, 是通过将一个函数定义成虚函数来
实现运行时的多态的 。 如果一个函数被定义为虚函数,
那么, 即使是使用指向基类对象的指针来调用该成员
函数, C++也能保证所调用的是正确的特定于实际对
象的成员函数 。
如果类 c1,c2… 由基类 base派生而来,base有一个
用 virtual修饰的公有或保护函数成员 f(),而在 c1,
c2… 中的一些类中重新定义了成员函数 f(),而对 f()
的调用都是通过级基类的对象或指针进行的,在程序
执行时才决定是调用 c1还是 c2或其他派生类中定义的 f
(),这样的函数 f()称为虚函数。
C++程序设计 湖南大学 杜四春、银红霞
一旦一个函数在基类中第一次声明时使用了 virtual
了关键字,那么,当派生类重载该成员函数时,无论
时否使用了 virtual 关键字,该成员函数都将被看作一
个虚函数,也就是说,虚函数的重载函数仍是虚函数。
注意:在派生类重定义虚函数时必须有相同的函
数原型,包括返回类型,函数名、参数个数、参数类
型的顺序必须相同。虚函数必须是类的成员函数。不
能为全局函数,也不能为静态函数。不能将友员说明
为虚函数,但虚函数可以是另一个类的友员。析构函
数可以是虚函数,但构造函数不能为虚函数。
C++程序设计 湖南大学 杜四春、银红霞
13.3.2 使用虚函数时应注意:
( 1) 在类体系中访问一个虚函数时, 应使用指向
基类类型的指针或对基类类型的引用, 以满足运行时
多态性的要求 。 当然也可以像调用普通成员函数那样
利用对象名来调用一个函数 。
( 2) 在派生类中重新定义虚函数时, 必须保证该
函数的值和参数与基类中的说明完全一致, 否则就属
于重载 ( 参数不同 ) 或是一个错误 ( 返回值不同 ) 。
( 3) 若在派生类中没有重新定义虚函数, 则该类
的对象将使用其基类中的虚函数代码 。
( 4)虚函数必须是类的一个成员函数,不能是友
元,但它可以是另一个类的友元。另外,虚函数不得
是一个静态成员。
C++程序设计 湖南大学 杜四春、银红霞
( 5) 析构函数可以是 virtual的虚函数, 但构造函
数则不得是虚函数 。 一般地将, 若某类中定义有虚函
数, 则其析构函数也应当说明为虚函数 。 特别是在析
构函数需要完成一些有意义的操作 —— 比如释放内存
时, 尤其应当如此 。
( 6)一个类的虚函数仅对派生类中重定义的函数
起作用,对其他函数没有影响。在基类中使用虚函数
保证了通过指向基类对象的指针调用基类的一个虚函
数时,C++系统对该调用进行动态绑定,而使用普通
函数则是静态绑定。
C++程序设计 湖南大学 杜四春、银红霞
13.3.3 虚函数与重载函数的比较
( 1) 重载函数要求函数有相同的返回值类型和函
数名称, 并有不同的参数序列;而虚函数则要求这三
项 ( 函数名, 返回值类型和参数序列 ) 完全相同;
( 2) 重载函数可以是成员函数或友员函数, 而虚
函数只能是成员函数;
( 3) 重载函数的调用是以所传递参数序列的差别
作为调用不同函数的依据;虚函数是根据对象的不同
去调用不同类的虚函数;
( 4)虚函数在运行时表现出多态功能,这是 C++
的精髓;而重载函数则在编译时表现出多态性。
C++程序设计 湖南大学 杜四春、银红霞
13.4 纯虚函数与抽象类
13.4.1 纯虚函数
在许多情况下, 在基类中不能给出有意义的虚函
数定义, 这时可以把它说明成纯虚函数, 把它的定义
留给派生类来做 。 定义纯虚函数的一般形式为:
class 类名 {
virtual 返回值类型 函数名 (参数表 )= 0;
};
纯虚函数是一个在基类中说明的虚函数,它在基
类中没有定义,要求任何派生类都定义自己的版本。
纯虚函数为各派生类提供一个公共界面。由于纯虚函
数所在的类中没有它的定义,在该类的构造函数和析
构函数中不允许调用纯虚函数,否则会导致程序运行
错误。但其他成员函数可以调用纯虚函。
C++程序设计 湖南大学 杜四春、银红霞
下面的代码在类 Creature中将虚函数 KindOf 声明
为纯虚函数:
class Creature
{
public:
virtual char *KindOf()=0;
};
char *Creature::KindOf()
{
return "Creature";
}
C++程序设计 湖南大学 杜四春、银红霞
使用下面的格式也是可以的:
class Creature
{
public:
virtual char *KindOf()=0
{
return "Creature";
}
};
C++程序设计 湖南大学 杜四春、银红霞
13.4.2 抽象类
如果一个类中至少有一个纯虚函数, 那么这个类
被成为抽象类 ( abstract class) 。 抽象类中不仅包括纯
虚函数, 也可包括虚函数 。 抽象类中的纯虚函数可能
是在抽象类中定义的, 也可能是从它的抽象基类中继
承下来且重定义的 。
抽象类有一个重要特点,即抽象类必须用作派生
其他类的基类,而不能用于直接创建对象实例。抽象
类不能直接创建对象的原因是其中有一个或多个函数
没有定义,但仍可使用指向抽象类的指针支持运行时
多态性。
C++程序设计 湖南大学 杜四春、银红霞
一个抽象类不可以用来创建对象, 这只能用来为
派生类提供了一个接口规范, 派生类中必须重载基类
中的纯虚函数, 否则它仍将被看作一个抽象类 。 如果
要直接调用抽象类中定义的纯虚函数, 必须使用完全
限定名, 如上面的示例, 要想直接调用抽象类 Creature
中定义的纯虚函数, 应该使用下面的格式:
cout<<pCreature->Creature::KindOf()<<endl;
上面的代码同时还给出了一种绕过虚函数机制的
方法,即使用带有作用域限定符的完全限定函数名。
C++程序设计 湖南大学 杜四春、银红霞
抽象类只能用作其他类的基类,抽象类不能建立
对象。抽象类不能用作函数参数类型、函数返回值类
型或显式转换的类型。可以声明抽象类的指针和引用。
而且,如果在抽象类的构造函数中调用了纯虚函数,
那么,其结果是不确定的。还有,由于抽象类的析构
函数可以被声明为纯虚函数,这时,应该至少提供该
析构函数的一个实现。一个很好的实现方式是的抽象
类中提供一个默认的析构函数,该析构函数保证至少
有析构函数的一个实现存在。如下面的例子所示:
C++程序设计 湖南大学 杜四春、银红霞
class classname
{
// 其他成员
public:
~classname()=0
{
// 在此添加析构函数的代码
}
};
C++程序设计 湖南大学 杜四春、银红霞
由于派生类的析构函数不可能和抽象类的析构函
数同名,因此,提供一个默认的析构函数的实现是完
全必要的。这也是纯虚析构函数和其他纯虚成员函数
的一个最大的不同之处。一般情况下,抽象类的析构
函数是有派生类的实现对象释放时由派生类的析构函
数隐含的所调用的。
抽象类的主要作用是取若干类的共同行为,形成
更清晰的概念层次。使用抽象类符合程序设计中的单
选原则( single choice principle)。
从基类继承来的纯虚函数,在派生类中仍是虚函
数。