1.02k likes | 1.14k Views
运行时多态性. 封装性 和 继承性 为软件设计提供了可靠的 安全性 和 良好的 结构层次 。在此基础上, 多态性 使软件中的对 象行为更加符合软件所模拟的客观世界中对象的 行为 多样性 ,为软件设计中的功能实现和任务调度提供了 灵活性 。 多态性 是指当 不同的对象 收到 相同的消息 时产生不 同的动作。 C++ 支持两种 多态性 : 编译时多态性 和 运行时多态性 。. Ø. 编译时的多态性:. 通过函数重载或运算符重载实现。. 重载的函数根据调用时给出的实参类型或个数,在. 程序编译时就可确定调用哪个函数。. Ø. 运行时的多态性:.
E N D
封装性和继承性为软件设计提供了可靠的安全性和封装性和继承性为软件设计提供了可靠的安全性和 良好的结构层次。在此基础上,多态性使软件中的对 象行为更加符合软件所模拟的客观世界中对象的行为 多样性,为软件设计中的功能实现和任务调度提供了 灵活性。 多态性是指当不同的对象收到相同的消息时产生不 同的动作。 C++ 支持两种多态性:编译时多态性和运行时多态性。
Ø 编译时的多态性: 通过函数重载或运算符重载实现。 重载的函数根据调用时给出的实参类型或个数,在 程序编译时就可确定调用哪个函数。 Ø 运行时的多态性: 在程序执行前,根据函数名和参 数无法确定应该调用哪个函数,必须在程序的执行 过程中,根据具体的执行情况来动态确定。它通过 类的继承关系和虚函数来实现,主要用来建立实用 的类层次体系结构、设计通用程序。
运行时多态性:又称为动态多态性,即确定消息的 多态响应的联编操作不是在编译链接过程完成的, 而是在程序运行中才能完成。这种联编称为动态联 编或后期联编。 实现的途径是:继承和虚函数。 只使用编译时多态性机制的程序设计只能称为基于 对象(Object Based)的程序设计,使用了运行时多 态性机制,才能称为是面向对象(Object Oriented) 的程序设计。
编译多态性的局限性 • 由于编译多态性是通过函数重载实现的,因此对象 • 行为的确定(函数的运行绑定)只能由编译器根据 • 程序源代码的安排在编译过程中实现。这样实现的 • 对象行为多样性虽然具有良好的可读性,但在控制 • 程序运行和对象行为多样性方面存在着局限性:
⑴ 对象行为的多样性必须是预先确定可知的,程序 运行的控制必须是预先规划固定的,因此只能解 决软件所模拟的客观世界中那些可以预先确定和 控制的事件的实现。 ⑵ 调用重载函数的不同版本(确定对象行为的多样 性)是依据参数(个数和类型)的差别实现的 (派生类对基类同名、同参数成员函数的覆盖除 外),即发送给不同对象的消息并不完全一致。 从这一点分析,编译多态性所实现的并非完备意 义上的多态性,即不同对象接收到同一消息(消 息名和参数完全一致),表现出不同的行为。
运行多态性要实现的目标 • 运行多态性要实现的目标是在保持编译多态性所能 • 实现的功能的基础上,克服编译多态性的局限性: • ⑴ 能根据程序运行中状态的变化,动态确定接收消 • 息的对象和对象行为的多样性,使得软件能够实 • 现所模拟的客观世界中那些无法预先确定事件的 • 发生和完成。 • ⑵ 完全相同的消息(消息名和参数完全一致)能够 • 触发(调用)不同类对象的特定行为,从而实现 • 完备意义的多态性,使得复杂的对象行为控制变 • 得更加简单、统一。
3 运行多态性的实现机制 • 使程序具有运行多态性的关键是能否在程序运行期 • 间,根据程序的运行状态,动态地修改程序的运行 • 控制指针。 • C++(包括 C)的编程要素 —— 指针变量为动态多 • 态性提供了基本实现机制,即程序能够在运行期间 • 通过修改指针变量的当前值,动态地确定程序的当 • 前操作和操作所施加的数据(对象)。指针变量分 • 为两种:
⑴ 数据指针变量 用于动态确定操作数据,即通过指针而不是使用 数据变量名访问数据变量。其定义格式为: 数据类型 *指针变量名; 例如:double *pt; 数据指针变量的值是数据空间的首地址,赋值合 法性依据是首地址所指示数据的类型。 例如: double d; int i; pt = &d; // 合法 pt = &i; // 非法
⑵ 函数指针变量 用于动态确定操作,即通过指针而不是使用函数 名实现函数调用。其定义格式为: 返回数据类型 (*指针变量名)(参数据类型, …); 例如:int (*fun)(int, int); 函数指针变量的值是函数的调用地址,赋值合法 性依据是调用地址所指示的函数原型(函数的返 回类型,参数个数、类型和顺序)。 例如: int max(int, int); void count(double); fun = max; // 合法 fun = count; // 非法 返回
2 实现运行多态性的方法 —— 虚函数 在面向对象程序设计中操作和操作所施加的数据是 绑定和封装在对象中的。所谓运行多态性,是在程序 运行期间,根据运行状态,动态确定接收同一消息的 对象,从而确定操作和操作所施加的数据,实现动态 地修改程序的运行控制。 响应同一消息要求被调用对象的成员函数原型(函 数名,返回类型,参数个数、类型和顺序)一致,这 是一般函数重载不能实现的。使用函数指针虽然能够 支持动态多态性,但在面向对象程序设计中直接使用 函数指针既烦琐又违背了面向对象的设计原则。因此 必须引入新的编程方法,这种新方法称为虚函数。
对象指针 • 实现运行多态性的第一步,是通过指针变量动态确 • 定接收同一消息的对象,这是首要条件。 • 1 一般对象的指针 • ⑴ 一般对象的指针与系统预定义数据的指针,在定 • 义和使用语法上都相同; • ⑵ 无派生关系的不同类对象指针之间不允许相互转 • 换,如果强制转换,可能会引起不可预期的结 • 果。例如:
#include <iostream.h> class class1 { int a,b; public: class1(int x, int y) { a =x; b = y; } void show() { cout << "my_base -----------\n"; cout << a << " " << b << endl; } };
class class2 { int c; public: class2(int x) { c = x; } void show() { cout << "my_class ----------\n"; cout << "c = " << c << endl; } };
void main() { class1 c1(50, 50), *ptr; class2 c2(30); ptr = &c1; ptr->show(); ptr = &c2; // 错误 ptr->show(); } 如果将代码 ptr = &c2;修改为 ptr = (class1*)&c2;则 会引起不可预测的执行结果,例如: my_base ----------- 50 50 my_base ----------- 30 6684136
具有派生层次的类对象指针 由于派生类是由基类继承定义的,即派生类的对象 中总是包含着基类对象部分,并在派生类对象的创建 过程中首先被创建。因此,基类对象和派生类对象的 首地址所指示的对象类型是向后兼容的,即允许基类 指针既可以指向基类对象,也可以指向派生类对象。
使用基类指针时要注意以下几个问题: ⑴ 基类指针可以指向该基类的公有派生类对象,但不 能自动指向该基类的私有派生类对象(除非使用强 制转换实现)。 例如: class base { ... }; class derive:base { ... }; void main() { base obj1, *ptr; derive obj2; ptr = &obj1; ptr = &obj2; // 不能自动指向其私有派生类对象,但可 // 强制转换,例如ptr = (base*)&obj2。 }
⑵ 不允许派生类指针指向该派生类的基类对象。 例如: class base { ... }; class derive:public base { ... }; void main() { base obj1; derive obj2,*ptr; ptr = &obj2; ptr = &obj1; // 派生类指针不能指向它的基类对象 }
⑶ 使用基类指针访问公有派生类对象时,只能自动访 问派生类对象中的基类对象部分。例如: class base { … public: void show1(); }; class derive:public base { ... public: void show2(); };
void main() { base obj1,*ptr; derive obj2; ptr = &obj1; ptr->show1(); ptr = &obj2; // 允许基类指针指向公有派生类对象 ptr->show1(); // 允许使用基类指针调用基类成员 ptr->show2(); // 不能使用基类指针调用派生类新增成员 } 如果将基类指针强制转换为派生类指针,则允许通 过基类指针访问派生类的新增成员(与使用派生类 对象名访问等效)。例如:((derive *)ptr)->show2();
为什麽要引入虚函数 通过前面的分析不难看出,面向对象的继承特性已 经为实现动态多态性提供了两个基本条件: ⑴ 在具有派生层次结构的各个类中定义同原型(函数 名,返回类型和参数个数、类型、顺序均一致)而 操作各异的成员函数为不同类对象响应同一消息实 现行为多样性提供了条件。 ⑵ 使用基类指针既可以访问基类对象又可以访问派生 类对象为动态确定接收同一消息的不同类对象提供 了条件。
但仅有上述两个条件仍然无法实现动态多态性,因但仅有上述两个条件仍然无法实现动态多态性,因 为要通过基类指针调用派生类的成员函数,必须将基 类指针强制转换为派生类指针。实现这样的强制转换 必须在编译时进行静态绑定,而不能实现运行时的动 态绑定。因此,必须引入一种新的编程机制,使得对 派生层次结构中各类的同原型成员函数的调用既能够 通过函数名编译绑定,又能够通过函数的调用地址运 行绑定。显然,实现运行绑定的最好途径是使用函数 指针。这种新的编程机制就是虚函数。
虚函数的定义及使用 1 虚函数的定义 ⑴ 虚函数是用来定义基类和公有派生类的同名且同 原型成员函数之间的多态关联关系的实现机制; ⑵虚函数必须在基类中首先定义,方法是在要定义 为虚函数的成员函数声明中冠以关键字virtual; ⑶虚函数一旦被定义,可以在一个或多个直接或间 接的公有派生类中重新定义; ⑷ 虚函数重新定义时,其函数名和函数原型,即函 数类型和参数(类型、个数、顺序),都必须与 基类中的虚函数完全一致。例如:
#include <iostream.h> class base { … public: virtual void who(){ cout << "base\n"; } // 定义虚函数 virtual void show() { cout << “properties of base: …\n"; } // 定义虚函数 }; class first:public base { … public: void who(){ cout << "the first derivation\n"; } // 重新定义虚函数 };
class second:public first { … public: void who() { cout << "the second derivation\n"; } // 重新定义虚函数 };
int main() { base obj1, *ptr; first obj2; second obj3; ptr = &obj1; ptr->who(); // 调用 base 类的 who() 版本 ptr->show(); // 调用 base 类的 show() 版本 ptr = &obj2; ptr->who(); // 调用 first 类的 who() 版本 ptr->show(); // 调用 base 类的 show() 版本
ptr = &obj3; ptr->who(); // 调用 second 类的 who() 版本 ptr->show(); // 调用 base 类的 show() 版本 return 0; } 执行结果: base properties of base: … the first derivation properties of base: … the second derivation properties of base: …
得到上述理想的动态多态性结果的原因是: ⑴ 定义了虚函数的基类增加了一个存放虚函数调用 地址函数指针表virtual table,该表的大小取决于 虚函数的个数,该表的首地址由基类的内部指针 vptr指示。 ⑵ 在派生层次中的各个派生类会从基类中直接或间 接继承虚函数指针表virtual table 和指针 vptr。如 果虚函数在派生类被重新定义,则 virtual table 中 的调用地址将被重新定义虚函数的调用地址替 换,否则仍然使用直接基类虚函数的调用地址。
⑶ 调用虚函数的运行绑定是由基类指针动态指向派 生层次中某个类的对象,通过该对象的内部指针 vptr所指示的 virtual table 中的相应调用地址实现 的。 下图示意了虚函数的定义和调用绑定关系:
… base type_info virtual table base ptr obj1:base … virtual table first type_info obj2:first … virtual table second type_info … obj3:second base::who() vptr base::show() first::who() vptr second::who() vptr
2 虚函数与重载函数的区别 ⑴ 虚函数的首次定义和重新定义实际上是在类派生 定义层次中的同名函数覆盖定义。因此,虚函数 的首次定义必须在基类中,重新定义只能在基类 的不同派生类中,而不能发生在同一类定义中, 也不能是全局函数。 而函数重载既可以发生在同一类定义中,也可以 是全局函数。
⑵函数重载必须具有相同的函数名和不同的参数⑵函数重载必须具有相同的函数名和不同的参数 (个数、类型或顺序)。而函数的返回类型不是 区别重载函数不同版本的标志,但如果两个函数 只是返回类型不同,则会导致二义性错误。 而虚函数重新定义必须保持与基类中的虚函数原 型(返回类型、函数名、参数类型以及个数和顺 序)完全一致,否则会产生两种情况: ① 如果仅返回类型不同,则会导致二义性错误。 ② 如果仅函数名相同,而函数的参数有所不同, 则系统会将该函数作为一般的重载函数处理, 从而失去虚函数特性。
虚函数与多态性 • 例 虚函数与多态性。 # include< iostream > using namespace std; class A{ protected: • int x; 问题 1 : 若将此处的 virtual 省去,程序运 public: 行结果如何? A( ){ x=1; } virtual void print( ) { cout <<"x="<<x<<' \ t'; } }; • 因派生类 B 的 print() 成员函数在参数类型、 参数个数及函数的返回值类型方面与基类 A class B:public A{ 的虚函数 print() 完全相同,故属于对基类 int y; A 虚函数 print() 的重定义,即使不用 public: virtual 修饰,也具有虚函数特性。 B( ){ y=2; } void print( ) { cout <<"y="<<y<<' \ n'; } };
• 编译时多态性: b 是派生类对象,对于 b.print() 调用,在编译时,根据对象名和优先规则即可确 定所调用的 print() 为派生类 B 中重定义的 print() , 与 print() 是否是虚函数无关。 int main(void) 程序运行结果: { A a,*pa; x=1 y=2 B b; x=1 y=2 a.print(); b. print() ; pa=&a; pa - >print(); pa=&b; pa - > print() ; “ ” • 问题 2 : 将 pa - >print(); 改为 return 0; “ ” pa - >A::print(); , 结果如何? } • 运行时多态性: 将派生类的对象 b 的指针赋给基类的指针变量 pa , “ ” 符合赋值兼容规则。执行 pa - >print(); , 因 print() 为基类中 的虚函数并在派生类中重定义,此时实际调用的是派生类中重定 义的虚函数 print() , 而不是基类中的虚函数 print() 。
• 关于运行时多态性与虚函数的说明: Ø 使用基类类型的指针变量 ( 或基类类型的引用 ) ,使 该指针指向派生类的对象 ( 或该引用是派生类的对象 的别名 ) ,并通过指针 ( 或引用 ) 调用指针 ( 或引用 ) 所 指对象的虚函数才能实现运行时的多态性。 Ø 当派生类未重定义基类的虚函数时,若调用该派生 类对象的虚函数,则调用其基类中的虚函数。 Ø 不能将构造函数定义为虚函数。但 通常把析构函数 定义为虚函数 ,以便通过运行时多态性,正确释放基类及其派生类申请的动态内存。 Ø 与一般成员函数相比,虚函数调用时的执行速度要 慢一些。原因:在每个派生类中均要保存相应虚函 数的入口地址来间接实现虚函数调用。
虚函数的特殊性 • 例 成员函数调用虚函数 # include< iostream > using namespace std; class A{ public: void f1(){ cout <<"A::f1 \ t"; f2() ; } virtual void f2() { cout <<"A::f2 \ t"; f3(); } void f3(){ cout <<"A::f3 \ n"; } }; class B:public A{ public: void f2() { cout <<"B::f2 \ t"; f3(); } void f3(){ cout <<"B::f3 \ n"; } }; int main(void){ B b; b.f1() ; return 0; }
程序运行结果: A::f1 B::f2 B::f3 • 问题: 在 main() 函数中,执行 b.f1() , 即调用基类 A 中的 f1(), 然后在 f1() 中又调用 f2() , 此时调用的是类 A 的 f2() 还是类 B 重 定义的 f2() ? • 分析: 类 A 的 f1() 的定义可用含 this 指针的等价形式表示为: void A::f1() { cout <<"A::f1 \ t"; this - > f2(); } Ø 此处 this 指针指向基类对象,其类型为: A* const this; // 即 this 指针是基类类型 Ø 此外, f2() 是基类 A 的虚函数又在派生类 B 中重定义,因此, “ ” 执行 this - >f2(); 语句 必然引发运行时多态性,即调用的 是 B::f2() , 而不是 A::f2() 。
3 多继承中的虚函数 使用虚函数实现动态多态性的关键在于: ⑴ 必须使用基类指针访问派生层次中某个类对象。 ⑵ 通过基类指针被运行绑定的成员函数必须在基类 中首次定义为虚函数。 由于多继承派生类有多个基类,所以用于动态访问 派生层次中各类对象的基类指针类型也不唯一的。 因此,在多继承派生情况下,使用虚函数实现动态 多态性就要比单继承复杂。例如:
#include <iostream.h> class a { public: virtual void f() { cout << "class a\n"; } // 虚函数 }; class b { public: void f() { cout << "class b\n"; } // 一般成员函数 }; class aa : public a, public b { public: void f() { cout << “class aa\n”; } // 重定义虚函数 };
main() { a obj1, *ptr1; b obj2, *ptr2; aa obj3; ptr1 = &obj1; // ptr1指向 a类对象 obj1 ptr1->f(); // 调用a类的f() 输出“a” ptr2= &obj2; // ptr2指向 b类对象 obj2 ptr2->f(); // 调用b类的f() 输出“b” ptr1 = &obj3; // ptr1指向 aa类对象 obj3 ptr1->f(); // 调用aa类的f()输出“aa” ptr2= &obj3; // ptr2指向 aa类的对象 obj3 ptr2->f(); // 调用b类的f()输出“b” return 0; }
在多继承派生情况下,建议使用虚基类技术简化动在多继承派生情况下,建议使用虚基类技术简化动 态多态性的实现复杂性。例如前面的程序可以改写 如下: #include <iostream.h> class a { public: virtual void f() { cout << "class a\n"; } // 虚函数 }; class a1: virtual public a { public: void f() { cout << "class a1\n"; } // 重新定义虚函数 };
class a2 : virtual public a { public: void f() { cout << "class a2\n"; } // 重新定义虚函数 }; class aa : public a1, public a2 { public: void f() { cout << "class aa\n"; } // 重新定义虚函数 };
main() { a *ptr; a1 *ptr1; a2 *ptr2; aa obj; ptr = &obj; // ptr指向 aa类对象 obj ptr->f(); // 调用 aa类的 f()输出“class aa” ptr1 = &obj; // ptr1指向 aa类对象 obj ptr1->f(); // 调用 aa类的 f()输出“class aa” ptr2= &obj; // ptr2指向 aa类对象 obj ptr2->f(); // 调用 aa类的 f()输出“class aa” return 0; }
4 基类构造函数调用虚函数 在派生类层次设计中,如果基类的构造函数中调用 了一个虚函数,并且该虚函数在派生类中定义了新 版本,则使用基类指针动态创建派生类对象时,只 能调用该虚函数的基类版本,而不会调用派生类版 本。这是因为在派生类对象创建过程中,调用基类 构造函数创建对象的基类部分时,虚函数指针表 virtual table还未初始化,无法实现动态链编。 显然,试图在对象构造过程中使用动态多态性实现 某种特定功能是行不通的,应该避免。例如:
A 继承自 基类的成员 B 派生类 自定义的成员 在构造函数中调用虚函数 • 例 程序运行结果: #include< iostream > A::f B::f using namespace std; • 结果分析: class A{ Ø 注意到,创建派生类的对象时, public: 先调用基类的构造函数初始化基 A(){ f() ; } 类成员,再调用派生类的构造函 virtual void f() 数初始化自定义成员。 { cout <<"A::f \ t"; } Ø 由于基类 A 的构造函数执行时,派 }; 生类 B 的构造函数尚未执行,所以, class B:public A{ 基类 A 的构造函数中所调用的虚函 public: 数 f() 只能是本类的 f , 而不可能 B(){ f() ; } 是派生类中中的 f() 。 void f() { cout <<"B::f \ t"; } }; b int main(void){ B b ; return 0; }
虚析构函数的重要性 • 例 虚析构函数的重要性 #include< iostream > B( ) using namespace std; { buf =new char[100]; cout <<"call B( ) \ n"; class A{ } public: ~B( ) A( ) { delete [] buf ; { cout <<"call A() \ n"; } cout <<"call ~B( ) \ n"; ~A( ) //L1 } { cout <<"call ~A() \ n"; } }; }; int main( ) { A *p=new B; //L2 class B:public A{ delete p; //L3 char* buf ; return 0; public: }
• 问题: main() 函数执行 L3 行语句时,因 p 是 程序运行结果: 基类类型指针,故仅调用基类的析构函数 call A() ~ A() , 而不调用派生类的析构函数 ~ B() 。 call B() 这样,派生类的动态对象申请分配的 100 字 call ~A() 节动态内存未被释放。 • 如何解决?有以下三种方法: 1. 将 L3 行的语句改为: delete (B*)p; 2. 或将 L2 行的语句改为: B *p=new B; 3. 或将 L1 行的语句改为: virtual ~A() 第三种方法将基类 A 的析构函数说明为虚函数,则其派生类 B 的 析构函数自动成为虚函数。这样,当 main() 函数执行 L3 行语句 时,因 p 指向一个派生类对象,故将调用派生类的析构函数 ~ B() 。 • 从使用的简便性上看,通常把析构函数说明为虚函数。
归纳虚函数的定义和使用应注意下几点: ⑴ 在基类中,使用virtual可以将其public或protected 部分的成员函数声明为虚函数。 ⑵派生类对基类中声明的虚函数进行重新定义时,关 键字 virtual可以忽略。但为了增强程序的可读性, 有时在派生类定义中,虚函数重定义时也使用关键 字 virtual。 ⑶虚函数被重新定义时,其函数原型与基类中的虚函 数原型必须完全相同。
⑷在一个派生类层次结构中定义了虚函数后,就允许 在执行过程中,动态改变基类指针所指向的该基类 派生的不同类对象,从而调用由虚函数确定的统一 接口的不同操作版本。显然,虚函数充分体现了面 向对象程序设计的动态多态性。 ⑸ 使用对象名和点运算符的方式也可以调用虚函数的 不同版本,但是这种调用是在编译时实现的静态联 编,没有充分利用虚函数的特性。因此,不需要通 过基类指针实现运行时多态性调用的成员函数不应 该定义为虚函数,避免不必要的内存开销。
⑹在派生类中,直接基类的虚函数指针表 virtual table 总是被自动继承。所以一个虚函数无论被公有继承 多少次,它仍然保持其虚函数的特性。因此,虚函 数特别适合定义一族类的统一接口。 ⑺虚函数必须是其所在类的成员函数,而不能是友元 函数,也不能是静态成员函数,因为虚函数调用要 靠特定的对象来决定应该动态绑定虚函数的哪个版 本。但是虚函数可以被声明为另一个类的友元成 员。