第九章 多态性
多态性( polymorphism) 也是面向对象程序设计的标志
性特征。前面我们讨论过面向对象的两个重要特性 — 封装性和
继承性,这三大特性是相互关联的,封装性是基础,继承性是
关键,而多态性是补充 。
简单地说,在 C++语言中,同一程序的某个运算符或者某
个函数名在不同情况下具有不同的实现的现象,叫做多态性。
其实在 C语言中,我们已经接触过多态性的应用,对于
不同类型的数据,运算符, \”具有不同的运算含义,如果两个
操作数都是整数,那么, \”进行整数相除,结果也是整数,而
其中一个操作数是实数类型的,那么, \”进行的是数学上普通
的除法,因此,面对不同的处理对象,,\”运算符有不同意义。
C++支持两种形式的多态性,一种是 编译 时的多态性,也
称为 静态联编,是通过 重载 来实现的,第二种是 运行 时的多态
性,也称为 动态联编,是通过 继承和虚函数 来实现的。
第九章 多态性
编译时的多态性是指程序在编译前就可以确定的多态性,他是通过
重载机制 来实现,重载包括 函数重载 和 运算符重载 。函数重载是允许在同
一程序中一个函数名可以对应多个函数体的实现,形成函数名的多态性,
在使用中根据函数的参数具体确定调用哪个函数,而运算符重载允许根据
实际需要重新定义 C++语言已有运算符的功能。这种多态性体现出“一个
接口,多个方法”的机制。
运行多态性是指必须在运行中才可以确定的多态性,运行多态性 是通
过 继承和虚函数 来实现的。要产生运行时的多态性,首先要设计一个 类层
次,然后在某些类中使用 虚函数 。
在 C++
中有两
种多态

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

count<<func(?A?);
虽未声明函数原型 int func( char), 但函数调用 func
( ‘ A?) 并不会产生任何问题, 因为编译程序自动将字符 ‘ A?
转换为 double类型, 然后调用函数 int func( double) 。
隐式类型转换是由 C++编译程序自动完成的, 这种类型
转换是引起函数重载二义性的主要原因 。
9.1 函数重载
在重载函数中使用默认参数也可能造成二义性。
int fun(int a,int b=1,int c=2,int d= 3){…}
那么使用下列调用是合法的:
fun(5,6,7); // 第 4个参数默认,值为 3
fun(5,6); // 第 3,4个参数默认,值为 2,3
fun(5); // 第 2,3,4个参数默认,值为 1,2,3
而对下面两个函数,就会发生二义性
int fun(int a,int b= 0) { return a + b; }
int fun(int a) { return a + a; }
调用函数 fun(5);
9.2 运算符重载
C++语言除了函数重载之外,还允许程序员重新定义已有
的运算符,使其能按用户的要求完成一些特定的操作,这就是
所谓的运算符重载。例如,在 C++系统内,对运算符 +,-,*、
\ 这四种运算符已有规定,他们是整数和实数的四则运算符。
但如果需要对复数、分数或其他操作数作四则运算,那么就要
对 +,-,*,\重新进行定义。
运算符重载是对已有的运算符赋予多重含义,同一个运算
符作用于不同类型的数据导致不同类型的行为。 C++中预定义
的运算符的操作对象只能是基本数据类型,实际上,对于很多
用户自定义类型,也需要有类似的运算操作,这就提出了对运
算符进行重新定义,赋予已有符号以新功能的要求。
运算符重载的实质就是函数重载。在实现过程中,首先把
指定的运算表达式转化为对运算符函数的调用,运算对象转化
为运算符函数的实参,然后根据实参的类型来确定需要调用的
函数,这个过程是在编译过程中完成的。
9.2 运算符重载
例如, 运算符 +在系统中预先是按函数做如下定义:
int operator + (int,int) ;
double operator + (double,double);
如果我们可以实现两个复数 x和 y相加表示为 x+y,两个字符
串 s1和 s2相加表示为 s1+s2,那么整个程序更加符合逻辑和
自然, 所以, 运算符重载是 C++多态性的一个重要应用 。
运算符重载中应注意的几个问题:
( 1) 哪些运算符可以重载:
除了 *,.,::,sizeof(),?:其他运算符都可以重载
( 2) 运算符重载后遵循, 四个不变, 原则
优先级不变
结合性不变
操作数个数不变
语法结构不变
9.2 运算符重载
( 3) 运算符重载是针对新类型数据的实际需要, 对原有运算
符进行适当的改造 。 一般来讲, 重载的功能应当与原有功能相
类似, 不能改变原运算符的操作对象个数, 同时至少要有一个
操作对象是自定义类型 。
( 4) 运算符重载实际上是通过定义一个函数来实现的, 运算
符重载归根结底是函数的重载, 编译系统选择重载的运算符时
根据操作数的个数和类型, 这是跟函数重载的选择是类同的 。
运算符的重载形式有两种:
用户定义的重载运算符, 要求能访问运算对象的私有成员, 所
以分重载为类的成员函数和重载为类的友元函数, 格式如下:
<函数类型 > operator <运算符 >( <形参表 >)
{ <函数体 >;
}
friend <函数类型 > operator <运算符 >( <形参表 >)
{ <函数体 >;
}
9.2 运算符重载
其中, <函数类型 >指定了重载运算符的返回值类型,
operator是定义运算符重载函数的关键词, <运算符 >给定了
要重载的运算符名称, 是 C++中可重载的运算符, 形参表中给
出重载运算符所需要的参数和类型 。 对于运算符重载为友元函
数的情况, 还要在函数类型说明之前使用 friend关键词来说明 。
当运算符重载为类的成员函数时,函数的参数个数比原来
的运算数个数要少一个,即单目运算符重载时无参数,而 实际
参数就是该对象本身,而双目运算符重载时一个参数,而实际
上,右操作数是参数,左操作数就是该对象本身 ;当重载为类
的友元函数时,参数个数与原运算数的个数相同。
一般来讲,单目运算符最好重载为成员函数,而双目运算
符则最好重载为友元函数。运算符重载的主要优点就是允许改
变使用于系统内部的运算符的操作方式,以适应用户新定义类
型的类似运算。
9.2.1 运算符成员函数重载
下面以定义一个复数类,并重载 +,-,*,/ 运算符来
说明重载的过程。
#include <iostream.h> // 9-2-1.cpp
class Complex {
private:
double real,image; // 复数的实部和虚部
public:
Complex() { real = image = 0.0; }
Complex(double r) {real = r; image = 0.0; }
Complex(double r,double i)
{ real = r; image = i; };
Complex operator + (const Complex &c);
Complex operator - (const Complex &c);
Complex operator * (const Complex &c);
9.2.1 运算符成员函数重载
Complex operator / (const Complex &c);
Complex operator - ();
friend void print(const Complex &c);
};
inline Complex Complex::operator + (const Complex &c)
{ return Complex(real + c.real,image + c.image);
}
inline Complex Complex::operator - (const Complex &c)
{ return Complex(real - c.real,image - c.image);
}
inline Complex Complex::operator * (const Complex &c)
{ return Complex(real * c.real - image * c.image,
real * c.image + image * c.real);
}
9.2.1 运算符成员函数重载
inline Complex Complex::operator / (const Complex &c)
{ return Complex((real * c.real +image * c.image) /
(c.real * c.real + c.image * c.image),
(image * c.real + real * c.image) /
(c.real * c.real + c.image * c.image));
}
inline Complex Complex::operator - ()
{ return Complex( -real,-image);
}
Void print(const Complex &c)
{ if(c.image < 0)
cout <<c.real<<,-,<< (-c.image)<<,i”
else cout <<c.real<<,+,<<c.image<<,i”
cout << endl;
}
9.2.1 运算符成员函数重载
void main()
{ Complex c1(2.5),c2(3.6,-1.2),c3;
c3 = c1 + c2;
cout <<,c1+c2 =,; print(c3);
c3 = c1 - c2;
cout <<,c1-c2 =,; print(c3);
c3 = c1 * c2;
cout <<,c1*c2 =,; print(c3);
c3 = c1 / c2;
cout <<,c1/c2 =,; print(c3);
c3 = -c2;
cout <<, - c2 =,; print(c3);
}
9.2.1 运算符成员函数重载
说明:在上面例子中,定义复数类 Complex,并说明 5
个成员函数的重载函数,分别是 operator +,operator -
operator *,operator /,operator -(负数)。
在主函数中出现 c1+c2 的表达式,那么运算符 + 是被重载
为复数运算的加法运算符,他被系统解析为
c1.operator+(c2)
其中,c1是第一个操作数为 Complex类的对象,operator+
是该类成员函数名,c2是该类对象的第二个操作数。
同样,c1-c2 被理解为 c1.operator-(c2) ;
-c2 被理解为 c2.operator-
9.2.2 运算符重载的友元函数形式
友元函数的重载格式:
friend <函数类型 > operator <运算符 >( <形参表 >)
{ <函数体 >; }
#include <iostream.h> // 9-2-2.cpp
class Complex {
private:
double real,image; // 复数的实部和虚部
public:
Complex() { real = image = 0.0; }
Complex(double r) {real = r; image = 0.0; }
Complex(double r,double i)
{ real = r; image = i; };
friend Complex operator + (const Complex &c1,
const Complex &c2);
friend void print(const Complex &c);
};
9.2.2 运算符重载的友元函数形式
Complex operator - (const Complex &c1,
const Complex &c2)
{ return Complex(c1.real +c2.real,c1.image +c2.image);
}
void print(const Complex &c)
{ if(c.image < 0)
cout <<c.real<<,-,<< (-c.image)<<,i”
else cout <<c.real<<,+,<<c.image<<,i”
cout << endl;
}
void main()
{ Complex c1(2.5),c2(3.6,-1.2),c3;
c3 = c1 + c2;
cout <<,c1+c2 =,; print(c3);
}
在友元函数的重载形
式下:
c1+c2被理解为
operator+(c1,c2)
9.2.2 运算符重载的友元函数形式
两种重载形式的比较:
他们都可以完成运算符的重载,从表面上看,使用成员函数
形式重载双目运算符时仅用一个参数,而使用友元函数形式重载
双目运算符时要用两个参数。
一般情况下,单目运算符重载使用成员函数形式,而双目运
算符使用友元函数形式进行重载。如果运算符在操作数需要不同
类的对象或者是内部类型数据时,该运算符应用友元函数形式进
行重载。
如上面例子中,8.27 + c1
如果采用成员函数进行重载时:
8.27.operator+(c1) 这显然是错误的
而采用友元函数形式进行重载时:
operator+(8.27,c1) 是合法的,系统将 8.27
转换为 Complex类型,
再进行加法
9.2.3 赋值运算符重载
赋值运算符 = 的原有含义是将赋值号右边表达式的结果拷
贝给赋值号左边的变量, 在类对象的赋值中, 通过运算符 =
重载将赋值号右边对象的私有数据依次拷贝到赋值号左边对象
的私有数据中 。 在正常情况下, 系统会为每一个类自动生成一
个默认的完成上述功能的赋值运算符, 当然, 这种赋值只限于
由一个类类型说明的对象之间赋值 。
? 如果一个类包含指针成员,采用这种默认的按成员赋值,
那么当这些成员撤消后,内存的使用将变得不可靠。
? class Person {
? private:
? int no; char *name;
? public:
? Person(int n,char *np)
? { no = n;
name =new char[strlen(np)+1];
strcpy(name,np);
}
? };
Person a(8,“Wang”);
Person b;
b = a; // 这样缺省方式下,
对象 a和 b的 name数据地
址是一样的,如果 a析构后,
那么 b的 name所指向的空
间已经被释放,显然这是
不对的
9.2.3 赋值运算符重载
可以重载运算符, =”来解决这个问题 。 重载该运算符
的成员函数如下:
Person &operator = (Person &s)
{
no = s.no;
delete name;
name=new char[strlen(s.name)+1];
strcpy(name,s.name);
return *this;
}
把 s.name的内存内容
复制到 this.name的内
存中,而不是仅仅进行
指针复制,这样避免出
现上面所说的内存不可
靠现象。
9.2.4 类型转换运算符重载
类型转换运算符重载函数的格式如下:
operator <类型名 >( )
{
<函数体 >;
}
与以前的重载运算符函数不同的是,类型转换运算符重载
函数没有返回类型,因为 <类型名 >就代表了它的返回类型,
而且也没有任何参数。在调用过程中要带一个对象实参。
实际上, 类型转换运算符将对象转换成类型名规定的类型 。
转换时的形式就像强制转换一样 。 如果没有转换运算符定义,
直接用强制转换是不行的, 因为强制转换只能对标准数据类
型进行操作, 对类类型的操作是没有定义的 。
另外,转换运算符重载的缺点是无法定义其类对象运算
符操作的真正含义,因为只能进行相应对象成员数据和一般
数据变量的转换操作。
9.2.5 自增运算符 ++的重载
增量运算符 ++和减量运算符 -属于单目运算符,只有一个
数据,并且有前置和后置之分。它们一样可以重载。
class Clock {
private:
int h,m,s;
public:
clock(int vh=0,int vm=0,int vs=0) {
h = vh; m = vm; s = vs;
}
operator ++();
showtime() {
cout << h <<,:” << m <<,:” << s << endl;
return 0;
}
};
9.2.5 自增运算符 ++的重载
inline Clock::operator++() // 前置,后置:
{ s ++; // Clock::operator++(int)
if(s >= 60) {
s = s-60;
m ++;
if(m >= 60) {
m = m-60;
h ++;
h = h % 24;
}
}
cout <<,++ Clock:” ;
}
main()
{ Clock myclock(23,59,59);
cout <<,开始时间:” ;
myclock.showtime();
++ myclock;
myclock.showtime();
}
9.3 虚函数
在将虚函数之前, 我们先介绍派生类指针访问:
在 C++语言中, 可以用一个指向基类的指针指向其公有派
生类的对象, 但却不能用指向派生类的指针指向一个基类对象 。
但如果希望用基类指针访问其公有派生类的特定成员, 必须将
基类指针用显式类型转换为派生类指针 。 一个指向基类的指针
可用来指向从基类公有派生的任何对象, 这一事实非常重要,
它是 C++实现运行时多态的关键途径 。
指向基类和派生类的指针是相关的:
例如,class B:public A;
A *ap ; // 指向类型 A 的对象的指针
B *bp ; // 指向类型 B 的对象的指针
A A_obj ; // 类型 A 的对象
B B_obj ; // 类型 B 的对象
p = & A_obj ; // p 指向类型 A 的对象
p = & B_obj ; // p 指向类型 B 的对象, 它是 A 的派生类
9.3 虚函数
利用 p,可以通过 B_obj 访问所有从 A_obj 继承的元素,
但不能用 p访问 B_obj 自身特定的元素 ( 除非用了显式类型
转换 ) 。
例如:
A *ap ; // 指向类型 A 的对象的指针
p = & B_obj ; // p 指向类型 B 的对象,它是 A 的派生类
p->a_no; // 合法的,a_no 为类 A的成员
p->a_fun(); // 合法的,a_fun() 为 A的成员函数
p->b_fun(); // 非法的,b_fun() 为 B的成员函数
((B *)p)-> b_fun(); // 合法的,因为已经显式类型转换
一个指向基类的指针可以指向其公有派生类的任何对象
9.3 虚函数
C++支持两种形式的多态性:
第一种是 编译 时的多态性,也称为 静态联编 或 静态绑定,
是通过 重载 来实现的,特点是在编译阶段根据调用时所使用的
参数就确定所调用的函数,优点是速度快、效率高;
第二种是 运行 时的多态性,也称为 动态联编 或 动态绑定,
是通过 继承 和 虚函数 来实现的,特点是指在程序执行前,无法
根据函数名和参数来确定该调用哪一个函数,必须在程序执行
过程中,根据当时的 对象 来调用相应的函数实现动态地确定 。
优点是高灵活性, 抽象性和可扩充性 。 要产生运行时的多态性,
首先要设计一个 类层次, 然后在某些类中使用 虚函数 。 虚函数
是用 virtual修饰的某基类的 protected或 public成员函数, 该
函数可以在派生类中重新定义, 以形成不同的版本, 只有在程
序运行过程中, 依据基类 指针具体指向哪个类对象 或 引用哪个
类对象, 才调用相应的函数 。
下面先看一个不是虚函数的类层次结构 ( Point-Circle)
9.3 虚函数
#include <iostream.h> // 9-3-1.cpp
class Point {
private:
float mX,mY;
public:
Point() { }
Point(float x,float y) {mX=x; mY=y;}
float area()
{ cout <<,Point fun is call !”<< endl; return 0.0;}
};
const float Pi = 3.141593;
class Circle:public Point {
private:
float mRadius;
9.3 虚函数
public:
Circle(float x,float y,float r):Point(x,y)
{ mRadius = r; }
float area()
{ cout <<,Circle fun is call !”<< endl;
return Pi * mRadius * mRadius;
}
};
int main()
{ Point *pp; Circle c(10,10,6.4321);
pp = &c;
cout << pp->area() << endl;
return 0;
}
输出结果:
Point fun is call !
0
9.3 虚函数定义
在例子中我们可以看到,pp是基类指针,但指向派生类
Circle对象 c,由于没有使用虚函数,编译器对 area函数采用
静态联编,按照 pp的类型调用 Point类中的成员函数 area,那
么结果就是 0,如果我们将基类中的 area函数前面用 virtual将
之声明为虚函数,这时候,系统将对 area函数进行动态联编,
在程序的运行过程中,按照基类指针 pp实际指向的 Circle类,
调用 Circle中的 area函数,那么结果是 129.974
虚函数的定义:
虚函数是在基类中冠以关键字 virtual 的成员函数。它提供了
一种接口界面,虚函数可以在一个或多个派生类中被重定义。
virtual 类型 函数名(参数表);
关键字 virtual指明该成员函数为虚函数。 virtual仅用于类定
义中,如虚函数在类外定义,不可加 virtual。
当某一个类的一个类成员函数被定义为虚函数,则由该
类派生出来的所有派生类中,该函数始终保持虚函数的特征。
当在派生类中重新定义虚函数时,不必加关键字 virtual。
但重新定义时不仅要同名,而且它的参数表和返回类型全部与
基类中的虚函数一样,否则联编时出错。
9.3 虚函数注意问题
使用虚函数时应注意:
( 1) 在类层次体系中访问一个虚函数时, 应使用指向 基类类型的
指针 或 对基类类型的引用, 以满足运行时多态性的要求 。 当然也
可以像调用普通成员函数那样利用对象名来调用一个函数, 这时
候系统采用静态联编 。
( 2) 在派生类中重新定义虚函数时, 必须保证该函数的值和参数
与基类中的说明完全一致, 否则就属于重载 ( 参数不同 ) 或是一
个错误 ( 返回值不同 ) 。
( 3) 若在派生类中没有重新定义虚函数, 则该类的对象将使用其
基类中的虚函数代码 。
( 4)虚函数必须是类的一个成员函数,不能是友元,但它可以是
另一个类的友元。另外,虚函数不得是一个静态成员。
( 5) 析构函数可以是 virtual的虚函数, 但构造函数则不得是虚函
数 。 一般地讲, 若某类中定义有虚函数, 则其析构函数也应当说
明为虚函数 。 特别是在析构函数需要完成一些有意义的操作 ——
比如释放内存时, 尤其应当如此 。
( 6) 内联函数每个对象一个拷贝, 无映射关系, 不能作为虚函数
9.3 虚函数访问
虚函数的访问:
用基类指针访问,
Point *pp; Circle c;
……
pp = &c; pp->area(); // 动态联编
用基类引用访问,
Circle c; Point &pd = c;
……
pd.area(); // 动态联编
用对象名访问,
Circle c;
……
c.area(); // 静态联编
c.Point::area();
c.Circle::area();
9.3 虚函数访问
class base0 {
public:
void v() {
cout <<“base 0\n”;
}
};
class base1:public base0 {
public:
virtual void v() {
cout <<“base 1\n”;
}
};
class A1:public base1 {
public:
void v() {
cout <<“A 1\n”;
}
};
class A2:public A1 {
public:
void v() {
cout <<“A 2\n”;
}
};
class B1:private base1 {
public:
void v() {
cout <<“B 1\n”;
}
};
class B2:public B1 {
public:
void v() {
cout <<“B 2\n”;
}
};
9.3 虚函数访问
main()
{ base0 *pb;
A1 a1; pb = &a1; pb->v(); // base 0
A2 a2; pb = &a2; pb->v(); // base 0
B1 b1; pb = &b1; pb->v(); // 错误
B2 b2; pb = &a2; pb->v(); // 错误
base1 *pp;
pp = &a1; pb->v(); // A1
pp = &a2; pb->v(); // A2
}
9.3 虚函数访问
成员函数访问虚函数是采用动态联编:
class A {
public:
virtual void v1() {cout <<,v1 is called in A\n”; a1();}
void a1() {cout<<“a1 is called in A\n”; v2();}
virtual void v2() {cout <<,v2 is called in A\n”; }
};
class B:public A {
public:
void b1() {cout <<,b1 is called in B\n”; v2();}
void v2() {cout <<,v2 is called in B\n”;}
}
main()
{ A a; a.v1(); cout <<,OK !\n”;
B b; b.v1();
}
v1 is called in A
a1 is called in A
v2 is called in A
OK !
v1 is called in A
a1 is called in A
v2 is called in B
this->v2(); // v2函数的引用通过 this 指针来
实现的,而 this就是基类指针,所以动态联编
9.3 虚函数访问
构造函数 /析构函数调用虚函数是采用静态联编:
class Base {
public:
Base() {}
virtual void v1() {cout<<“V1 is called is base\n”;}
};
class A:public Base {
public:
A() {cout<<“call v1 is A()\n”; v1(); }
virtual void v1() {cout<<“v1 is called in A()\n”;}
}
class B:public A {
public:
B() {cout<<“call v1 is B()\n”;
A::v1(); }
void v1() {cout<<“v1 is called in B()\n”;}
}
定义 A a 时系统输出:
call v1 is A()
v1 is called in A()
定义 B b 时系统输出:
call v1 is A() // 基类构造函数
v1 is called in A()
call v1 is B() // 派生类构造
v1 is called in A()
9.3 虚函数和重载函数比较
虚函数与重载函数的比较:
( 1) 重载函数要求函数有相同的返回值类型和函数名称, 并
有不同的参数序列;而虚函数则要求这三项 ( 函数名, 返回
值类型和参数序列 ) 完全相同;
( 2) 重载函数可以是成员函数或友员函数, 而虚函数只能是
成员函数;
( 3) 重载函数的调用是以所传递参数序列的差别作为调用不
同函数的依据;虚函数是根据对象的不同去调用不同类的虚
函数;
( 4)虚函数在运行时表现出多态功能,这是 C++的精髓;
而重载函数则在编译时表现出多态性。
提问:
class A {
public:
virtual void fun1() {cout<<“A::fun1()\n”;}
void fun2() {cout<<“A::fun2()\n”;}
};
class B:public A {
public:
void fun1() {cout<<“B::fun1()\n”;}
void fun2() {cout<<“B::fun2()\n”;}
};
class C:public B {
public:
void fun1() {cout<<“C::fun1()\n”;}
void fun2() {cout<<“C::fun2()\n”;}
};
void main()
{ A *pa = new B;
B b;
C c;
b.fun1();
c.fun2();
pa->fun1();
pa->fun2();
pa=&c;
pa->fun1();
pa->fun2();
(*pa).A::fun1();
}
B::fun1()
C::fun2()
B::fun1()
A::fun2()
C::fun1()
A::fun2()
A::fun1()
9.4 纯虚函数和抽象类
在许多情况下,在基类中不能给出有意义的虚函数定义,
这时可以把它说明成 纯虚函数 ( pure virtual function),
纯虚函数 是指被标明为不具体实现的虚拟成员函数。它用于这
样的情况:定义一个基类时,会遇到无法定义基类中虚函数的
具体实现,其实现依赖于不同的派生类。定义纯虚函数的一般
格式为:
virtual 类型 函数名(参数表) =0;
纯虚函数是一个在基类中说明的虚函数,它在基类中没有
定义,要求任何派生类都定义自己的版本。纯虚函数为各派生
类提供一个公共界面。由于纯虚函数所在的类中没有它的定义,
在该类的构造函数和析构函数中不允许调用纯虚函数,否则会
导致程序运行错误。
含有纯虚函数的基类是不能用来定义对象的。纯虚函数没
有实现部分,不能产生对象,所以含有纯虚函数的类是 抽象类
( abstract class), 抽象类中的纯虚函数可能是在抽象类中
定义的,也可能是从它的抽象基类中继承下来且重定义的。
9.4 纯虚函数和抽象类
抽象类有一个重要特点, 即抽象类必须用作派生其他类
的基类, 而不能用于直接创建对象实例, 所以抽象类仅充当一
个统一的接口作用, 抽象类不能直接创建对象的原因是其中有
一个或多个函数没有定义, 但仍可使用指向抽象类的指针支持
运行时多态性 。
class Point {
private:
float mX,mY;
public:
Point(float x,float y)
{mX=x; mY=y;}
virtual float area() = 0;
};
这时,Point类中的 area()就是一个纯虚函数,Point就
是一个抽象类。
Point a; // 错误,因为 Point是抽象类,不能定义对象
Point *p; // 正确,可以定义指向抽象类的指针
人类
中国人 美国人 法国人
汉族 苗族 白族 回族
其中:人类就是抽象类
在大型软件项目开发时,需要进行需求分析、总体设计、
详细设计等阶段,抽象类主要在分析与设计阶段扮演重要角色,
在开发的初期尚无法确定一些行为的具体实现策略时,可以将
它们定义为纯虚函数,为后续开发留下扩充接口。
定义纯虚函数必须注意:
( 1)定义纯虚函数时,不能定义虚函数的实现部分。即使是
函数体为空也不可以,函数体为空就可以执行,只是什么也不
做就返回。而纯虚函数不能调用。
( 2),=0”表明程序员将不定义该函数,函数声明是为派生
类保留一个位置。,=0”本质上是将指向函数体的指针定为
NULL。
( 3)在派生类中必须有重新定义的纯虚函数的函数体,这样
的派生类才能用来定义对象。
9.4 纯虚函数和抽象类
9.4 纯虚函数和抽象类
例子:编写一个用于计算各类形状面积的总面积的 C++程序
由于该程序用于计算各类形状的总面积,各形状可能包括三
角形、矩形、圆等面积,因为事先我们不能确定程序所处理的具
体形状,所以我们可以先定义一个抽象的 Figure,不同形状的类
全部是由抽象类 Figure派生出来,再定义一个由指向 Figure的
指针组成的向量,使这些指针指向包含求不同形状面积的虚函数
的派生类的实际对象,利用统一的结构进行循环求和。
class Figure { // 9-4-1.cpp
public:
virtual float area() = 0; // 纯虚函数
};
class Circle:public Figure {
private:
float radius;
public:
Circle(float r) {radius = r;}
float area() {return radius*radius*Pi}
};
Figure
Triagle Circle
Rectangle
层次结构
9.4 纯虚函数和抽象类
class Triangle:public Figure {
private:
float high,wide;
public:
Triangle(float h,float w) {high = h; wide = w;}
float area() {return high * wide}
};
class Rectangle:public Triangle {
public:
Rectangle(float h,float w):Triangle(h,w) {}
float area() {return high * wide}
};
float total(Figure *pf[],int n)
{ float sum = 0;
for(int i=0; i < n; i ++)
sum += pf[i]->area();
return sum;
}
Total Area,338.156
void main()
{Figure *pf[4];
pf[0] = new Triangle(3.0,4.0);
pf[1] = new Rectangle(2.0,3.5);
pf[2] = new Rectangle(5.0,1.0);
pf[3] = new Circle(10.0);
cout <<,Total Area:”
<< total(pf,4) << endl; }