第6第 第第第第第第第
Jan 14, 2016
第 6 章 多态性与虚函数
主要内容6.1 多态性概述6.2 子类型6.3 虚函数6.4 纯虚函数和抽象类
6.1 多态性概述 所谓多态性就是不同对象收到相同的消息时,产生不同的动作。 直观地说,多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作,从而可以使用相同的调用方式来调用这些具有不同功能的同名函数。
6.1.1 多态的分类 C++ 中的多态性可以分为四类 :
参数多态(函数模板、类模板) 包含多态(虚函数) 重载多态(函数重载、运算符重载) 强制多态(强制类型转换)。 前面两种统称为通用多态,而后面两种统称为专用多态。
6.1.2 多态的实现 多态性的实现和联编这一概念有关。所谓联编就是把函数名与函数体的程序代码连接(联系)在一起的过程。 联编分成两大类:静态联编和动态联编。静态联编优点:调用速度快,效率高,但缺乏灵活性;动态联编优点:运行效率低,但增强了程序灵活性。 C++ 为了兼容 C 语言仍然是编译型的,采用静态联编。为了实现多态性,利用虚函数机制,可部分地采用动态联编。
6.1.2 多态的实现 多态从实现的角度来讲可以划分为两类 :编译时的多态和运行时的多态。 编译时的多态是通过静态联编来实现的。静态联编就是在编译阶段完成的联编。编译时多态性主要是通过函数重载和运算符重载实现的。 运行时的多态是用动态联编实现的。动态联编是运行阶段完成的联编。运行时多态性主要是通过虚函数来实现的。
6.2 子类型概念 子类型:有一个特定的类型 U ,当且仅当它至少提供了类型 X 的行为时,称类型 U
是类型 X 的子类型。 如果类 U 从类 X 公有继承,则类 U 将继承了 X 的行为,同时类 U 还可以根据需求增加新的行为。这意味着类 U 提供了类 X 的行为,根据定义,类 U 是类 X 的子类型。
6.2.1 子类型作用 类型适应:类型 U 的对象能被用于类型 X 的对象所能使用的场合,称为类型 U 适应类型 X 。 类型适应的四种情况:在使用 X 类型对象的场合,可以使用 U 类型对象;在使用 X* 指针或 X& 引用的场合,可以使用 U*指针或 U& 引用;在使用使用 const X* 常指针或 const X& 常引用的场合,可以使用 const U* 常指针或 const U& 常引用;
6.2.1 子类型作用 子类型作用:一个函数可被重用于处理 X 类型的对象和 X 的各个子类型的对象,而不必为处理这些子类型的对象去重载该函数,可以减轻程序人员编写程序代码的负担。
例 6.1 :分析下列程序的输出结果。
例 6.1 子类型静态束定实例 #include <iostream.h>class A{public: A() {a=0;} A(int i) {a=i;} void Print() {cout<<a<<endl;} int Geta() {return a;}private: int a;};class B:public A{public: B() {b=0;} B(int i,int j):A(i),b(j) {} void Print() {A::Print();cout<<b
<<endl;}
private:
int b;
};
void fun(A& d)
{ cout<<d.Geta()*10<<endl; }
void main()
{ B bb(9,5);
A aa(5);
aa=bb;
aa.Print();
A *pa=new A(8);
B *pb=new B(1,2);
pa=pb;
pa->Print();
fun(bb); }
6.3 虚函数 虚函数提供了一种更为灵活的多态性机制。虚函数允许函数调用与函数体之间的联系在运行时才建立,也就是在运行时才决定如何动作,即所谓的动态联编。
6.3.1 虚函数的引入
例 6.2 没有引入虚函数的实例 1 。#include<iostream.h>class A{public: void show(){ cout<<"A"; }};class B:public A {public: void show(){ cout<<"B"; }};main(){ A a,*pc; B b; pc=&a; pc->show(); pc=&b; pc->show(); return 0;}
例 6.3 没有引入虚函数的实例 2
#include<iostream.h>class Base{public:
Base(int x,int y){ a=x;
b=y; }void show(){cout<<"Base---------------\n"; cout<<a<<" "<<b<<endl; }
private:int a,b;
};class Derived:public Base{public: Derived(int x,int y,int z):Base(x,y) { c=z; }
void show()
{ cout<<"Derived-------------------\n";
cout<<"c="<<c<<endl; }
private:
int c;
};
void main()
{
Base mb(60,60),*pc;
Derived mc(10,20,30);
pc=&mb;
pc->show();
pc=&mc;
pc->show();
}
6.3.2 虚函数的作用和定义 1. 虚函数的作用 虚函数同派生类的结合可使 C++ 支持运行时的多态性,实现了在基类定义派生类所拥有的通用接口,而在派生类定义具体的实现方法,即常说的“同一接口,多种方法”,它帮助程序员处理越来越复杂的程序。
例 6.4 虚函数的作用。#include<iostream.h>class Base {public: Base(int x,int y) { a=x; b=y; } virtual void show() // 定义虚函数 show() { cout<<"Base----------\n"; cout<<a<<" "<<b<<endl;}
private: int a,b; };class Derived : public Base {public: Derived(int x,int y,int z):Base(x,y){c=z; } void show() // 重新定义虚函数 show() { cout<< "Derived---------\n"; cout<<c<<endl;}
private: int c;};
void main(){
Base mb(60,60),*pc; Derived mc(10,20,30); pc=&mb; pc->show(); // 调用基类 Base 的 show() 版本 pc=&mc; pc->show(); // 调用派生类 Derived 的 show() 版本
}程序运行结果如下 :Base---------60 60Derived--------30
2. 虚函数的定义定义虚函数的方法如下 :
virtual 函数类型 函数名 ( 形参表 )
{
// 函数体 }
说明:1. 派生类应该从它的基类公有派生。2. 必须首先在基类中定义虚函数。3. 派生类对基类中声明虚函数重新定义时,关键字 virtual 可
以不写。4. 一般通过基类指针访问虚函数时才能体现多态性。5. 一个虚函数无论被继承多少次,保持其虚函数特性。6. 虚函数必须是其所在类的成员函数,而不能是友元函数,也
不能是静态函数。7. 内联函数不能是虚函数。8. 构造函数不能是虚函数。9. 析构函数可以使虚函数,通常说明为虚函数。
例 6.5 虚函数的定义举例。#include<iostream.h>class Grandam { public: virtual void introduce_self() // 定义虚函数 introduce_self() { cout<<"I am grandam."<<endl; } };class Mother:public Grandam { public: void introduce_self() // 重新定义虚函数 introduce_self() { cout<<"I am mother."<<endl;}};class Daughter:public Mother { public: void introduce_self() // 重新定义虚函数 introduce_self() { cout<<"I am daughter."<<endl;}};
void main(){ Grandam *ptr; Grandam g; Mother m; Daughter d; ptr=&g; ptr->introduce_self();// 调用基类 Grandam 的 introduce_self(
) ptr=&m; ptr->introduce_self();// 调用派生类 Mother 的 introduce_self
() ptr=&d; ptr->introduce_self(); // 调用派生类 // Daughter 的 introduce_self()}
C ++规定,如果在派生类中,没有用 virtual 显式地给出虚函数声明,这时系统就会遵循以下规则来判断一个成员函数是不是虚函数:
( 1 )该函数与基类的虚函数有相同的名称;( 2 )该函数与基类的虚函数有相同的参数个数及相同
的对应参数类型;( 3 )该函数与基类的虚函数有相同的返回类型或者满
足赋值兼容性规则的指针、引用型的返回类型。
3. 成员函数调用虚函数(例 6.6 )#include <iostream.h>
class A
{ public:
virtual double funA(double x)
{ cout<<"funA of class A called."<<endl;
return x*x; }
double funB(double x)
{ return funA(x)/2; }
};
class B:public A
{ public:
virtual double funA(double x)
{ cout<<"funA of class B called."<<endl;
return 2*x*x; }
};
class C:public B
{ public:
virtual double funA(double x)
{ cout<<"funA of class C called."<<endl;
return 3*x*x;
}
};
void main()
{
C objc;
cout<<objc.funB(3)<<endl;
B objb;
cout<<objb.funB(3)<<endl;
}
4. 构造函数和析构函数内调用虚函数 构造函数中调用虚函数时,采用静态束定,即
构造函数调用的虚函数是自己类中实现的虚函数,如果自己类中没有实现这个虚函数,则调用基类中的虚函数,而不是任何派生类中实现的虚函数;
析构函数中调用虚函数同构造函数,即析构函数所调用的虚函数是自身类中的或者基类中实现的虚函数;
BA C
构造 (析构 )函数
一般成员函数
调用虚函数
构造函数和析构函数内调用虚函数(例 6.7 )
#include <iostream.h>
class A
{ public:
A(){ cout<<"default constructor of class A is called! \n";}
~A(){ cout<<"destructor of class A is called!\n";}
virtual void funA()
{ cout<<"funA of class A called."<<endl; }
};
class B:public A
{ public:
B(){ cout<<"default constructor of class B is called! \n";funA();}
~B(){ cout<<"destructor of class B is called!\n";funA();}
void funB() { funA(); }
};
class C:public B
{ public:
C(){ cout<<"default constructor
of class C is called! \n";}
~C(){ cout<<"destructor of class C is called!\n";}
virtual void funA()
{cout<<"funA of class C called.“
<<endl; }
};
void main()
{
cout<<"creating object...\n";
C objc;
cout<<"executing objc.funB()...\n";
objc.funB();
cout<<"exiting...\n";
}
5. 虚析构函数class B{ public: virtual ~B( ); // 虚析构函数}这样,用指针去释放对象时,基类指针指向哪个层次的对象,则调用的就是该类的析构函数,释放掉该对象。
注意:
1. 基类析构函数被声明为虚析构函数,则派生类中的析构函数会自动成为虚析构函数。2. 如果一个类拥有虚函数,则该类即使不需要虚析构函数,也应该为它提供一个。
注意:构造函数不能是虚函数
5. 虚析构函数(例 6.8 )
#include <iostream.h>
class A
{ public:
virtual ~A() {
cout<<"A's ~A()"<<endl; }
};
class B:public A
{ char *buf;
public:
B(int i) {
buf=new char[i]; }
virtual ~B()
{ delete [] buf;
cout<<"B's ~B()"<<endl;
}
};
void fun(A *a)
{
delete a;
}
void main()
{
A *a=new B(10) ;
fun(a);
}
上堂课总结1.普通成员函数调用虚函数(体现多态性)。 实质:基类指针指向普通成员函数(派生类自己或从基类
继承过来),而此成员函数又调用虚函数则调用派生类所指的虚函数。
2. 构造函数和析构函数调用虚函数(静态联编)。 实质:构造函数和析构函数是系统自动调用的,并不需要
使用基类指针调用,所以是静态的。3. 虚析构函数。 作用:保证使用基类类型的指针能够调用适当的析构函数
针对不同的对象进行清理。 实质:如果删除该指针,就会调用该指针指向的派生类的
析构函数。
6.3.4 虚函数与重载函数的关系 在一个派生类中重新定义基类的虚函数是函数重载的另
一种形式,但它不同于一般的函数重载。 ◆ 普通的函数重载时,其函数的参数或参数类型必须有所
不同,函数的返回类型也可以不同。 ◆ 当重载一个虚函数时,也就是说在派生类中重新定义虚
函数时,要求函数名、返回类型、参数个数、参数的类型和顺序与基类中的虚函数原型完全相同。
◆ 如果仅仅返回类型不同,其余均相同,系统会给出错误信息 ;
◆若仅仅函数名相同,而参数的个数、类型或顺序不同,系统将它作为普通的函数重载,这时将丢失虚函数的特性。
虚函数与重载函数关系(例 6_8 )
#include<iostream.h>
class Base
{
public:
virtual void func1();
virtual void func2();
virtual void func3();
void func4();
};
class Derived:public Base
{
public:
virtual void func1();
void func2(int x);
//char func3();
void func4();
};
void Base::func1()
{ cout<<"--Base func1--"<<endl;}
void Base::func2()
{ cout<<"--Base func2--"<<endl;}
void Base::func3()
{ cout<<"--Base func3--"<<endl;}
void Base::func4()
{ cout<<"--Base func4--"<<endl;}
void Derived::func1()
{ cout<<"--Dervied func1--"<<endl;}
void Derived::func2(int x)
{ cout<<"--Dervied func2--"<<endl;}
void Derived::func4()
{ cout<<"--Dervied func4--"<<endl;}
int main()
{ Base d1,*bp; Derived d2;bp=&d2;
bp->func1();bp->func2();bp->func4();
}
6.4 纯虚函数和抽象类
6.4.1 纯虚函数 纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义,但要求在它的派生类中必须定义自己的版本,或重新说明为纯虚函数。 纯虚函数的定义形式如下 :
virtual 函数类型 函数名 ( 参数表 )=0;
例 6.9 纯虚函数的使用。#include<iostream.h>class Circle {public: void setr(int x){ r=x; } virtual void show()=0; // 纯虚函数protected: int r;};class Area:public Circle{public: void show(){ cout<<"Area is "<<3.14*r*r<<endl;}}; // 重定义虚函数 show( )class Perimeter:public Circle{public: void show(){cout<<"Perimeter is "<<2*3.14*r<<endl;} }; // 重定义虚函数 show( )
void main(){ Circle *ptr; Area ob1; Perimeter ob2; ob1.setr(10); ob2.setr(10); ptr=&ob1; ptr->show(); ptr=&ob2; ptr->show();}
6.4.2 抽象类 如果一个类至少有一个纯虚函数,那么就称该类为抽象类。 抽象类只能作为其他类的基类来使用,不能建立抽象类对象,其纯虚函数的实现由派生类给出。
6.4.2 抽象类 ( 1 )抽象类只能作为其他类的基类来使用,不能建立类对象; ( 2 )不允许从具体类派生出抽象类; ( 3 )抽象类不能用作参数类型、函数返回类型或显式转换的类型; ( 4 )可以声明指向抽象类的指针或引用; ( 5 )派生类没有重定义纯虚函数,它仍然是一个抽象类; ( 6 )在抽象类可以定义普通成员函数或虚函数。
6.5 程序应用举例
例 6-10 应用抽象类,求圆、圆内接正方形和圆外切正方形的面积和周长 例 6-11 建立两种类型的表:队列与堆栈。虽然两个表的特性完全不同,但它们可用同一接口访问。