C++ 学习笔记----多态篇
(一)基本概念
1、入口地址:每个函数在内存中拥有一段存储空间,在内存的起始地址就是这个函数的入口地址;
2、编辑期绑定和运行期绑定:
编译期绑定:指的是程序运行哪一段代码,由编译器在编译阶段就确定了;
运行期绑定:一个函数的绑定发生在运行时刻;
我们也称运行期绑定的函数是多态的;
3、在C++中,只有满足一定的条件,才可能是多态的,这些条件是:
(1)必须有一个继承体系结构;
(2)继承体系结构中的一些类必须具有同名(相同函数签名)的virtual成员函数;
(3)至少有一个基类类型的指针或基类类型的引用。这个指针或引用可对virtual成员函数进行调用;
例如:
1 #include <iostream> 2 3 using namespace std; 4 5 class TradesPerson { 6 public : 7 virtual void sayHi() 8 { 9 cout<<"Just Hi ."<<endl; 10 } 11 }; 12 13 class Tinker : public TradesPerson { 14 public : 15 virtual void sayHi() 16 { 17 cout<<"Hi, I tinker ."<<endl; 18 } 19 }; 20 21 class Tailer : public TradesPerson { 22 public : 23 virtual void sayHi() 24 { 25 cout<<"Hi, I tailer ."<<endl; 26 } 27 }; 28 29 int main(int argc, char argv[]) 30 { 31 TradesPerson* p,*p1,*p2; 32 33 p = new TradesPerson; 34 p->sayHi(); //Just Hi . 35 delete p; 36 37 p1 = new Tinker; 38 p1->sayHi(); //Hi, I tinker . 39 delete p1; 40 41 p2 = new Tailer; 42 p2->sayHi(); 43 delete p2; //Hi, I tailer . 44 45 46 system("PAUSE"); 47 return 0; 48 }
在此代码中,派生类的virtual关键词不是必须的,若有和基类同签名的函数,基类中声明是virtual的,则派生类会自动将其变成虚函数,但加上关键字,方便检查,因此建议在派生类中加上virtual;若在外部定义函数,不用加关键字virtual,只要在声明时加上virtual即可;同样,虚函数也可从基类中继承;
4、运行期绑定和虚成员函数表:
在C++中使用vtable(虚成员函数表)来实现成员函数的运行期绑定;虚成员函数表,存在的用途是支持运行时查询,使得系统可以将某一函数名绑定到虚成员函数表的特定入口地址;使用虚成员函数表会影响效率,因为它需要额外的存储空间,对虚成员函数表进行查询也需要额外的时间;
5、构造函数与析构函数:
(1)构造函数不能使虚成员函数;
(2)析构函数可以使虚成员函数,在一定情况下,把析构函数设为虚函数是有必要的;
假设有一个基类A和它的一个继承类Z,定义了一个:A* ptr ; ptr = new Z(); delete ptr; 运行发现只有A的析构函数被调用,因为它们的析构函数不是虚函数,所以编译器实施的是静态绑定,编译器根据ptr的数据类型A*来决定调用哪一个析构函数;因此,仅调用了A的析构函数。若把A和Z的析构函数都设为虚函数,因为它们是运行期绑定,所以A和Z的析构函数都会被调用,这样,就不会发生内存泄露了。
6、注意一点:静态成员函数不能设置为虚成员函数;
(二)C++编译期多态机制:重载、覆盖和遮蔽
1、重载:具有相同的函数名,但函数签名不同,即“函数的参数的个数、类型和顺序不同”;成员函数和顶层函数都能构成重载;
2、覆盖:假定基类B有一个成员函数m,其派生类D也有一个具有相同函数签名的成员函数m,如果这个成员函数是虚函数,则任何通过指针或引用对m的调用都会激活运行期绑定;对于这种情况,我们说D::m覆盖了基类的成员函数B::m。如果成员函数不是虚函数,对m的任何调用都是编译期绑定。
3、遮蔽:假设基类B拥有一个非虚函数m,其派生类D也有一个成员函数m,我们说D::m遮蔽了基类B::m;如果它们具有不同的函数签名,这个情况就比较复杂了。
1 #include <iostream> 2 3 using namespace std; 4 5 class B { 6 public : 7 void m(int x) 8 { 9 cout<<x<<endl; 10 } 11 }; 12 13 class D : public B { 14 public : 15 void m() 16 { 17 cout<<"Hi"<<endl; 18 } 19 }; 20 21 22 int main(int argc, char argv[]) 23 { 24 D d1; 25 d1.m(); //OK 26 d1.m(20); //Error, 因为D的m遮蔽了B的m。 27 d1.B::m(20); //改成这样,OK; 28 29 system("PAUSE"); 30 return 0; 31 }
虚函数和非虚函数都有可能产生名字遮蔽,实际上一旦派生类的虚函数不能覆盖基类的虚函数,就会产生遮蔽;
像上述程序中,d1.B::m(20)虽然消除了编译错误,但不是好的编程风格,为了发挥多态性的作用,B::m和D::m应该具有相同的函数签名;
4、名字共享:
以下几种情况下,需要共享函数名:
(1)重载函数名的顶层函数;
(2)重载构造函数;
(3)重载成员函数;
(4)继承层次下的同名函数;等
(三)抽象基类:类必须至少有一个纯虚成员函数
1、综述:不能实例化抽象基类的对象,用来指明某些必须被派生类覆盖的虚函数,否则不能实例化对象;从这个意思上说,抽象基类实际上定义了一个公共接口;
只有虚函数才可以成为纯虚成员函数;
1 // =0 表示这个函数被定义为纯虚成员函数 2 class ABC { 3 public : 4 virtual void open() = 0; 5 //...... 6 };
(四)运行期类型识别(RTTI)
1、功能:
(1)在运行期对类型转换进行检查;
(2)在运行期确定对象的类型;
(3)扩展C++提供的RTTI;
2、dynamic_cast操作符:省略
3、static_cast操作符:省略
4、typeid操作符
功能:确定某个表达式的类型。头文件 #include <typeinfo>;
例如:float x; typeid(x) == typeid(float) //返回true
5、扩展RTTI
可通过从标准基类type_info派生出新类,对RTTI进行扩展。头文件:#include <typeinfo>
(五)强多态与弱多态
强多态是指:对覆盖的虚函数进行运行期绑定处理;
弱多态:顶层函数或成员函数的重载和类层次中非虚函数的函数名共享;
1 //////////////////////////////////////////////////////////////////////////////// 2 //C++学习笔记 3 ////////////////////////////////////////////////////////////////////////////////