一,多态的理论推导
1.类型兼容性原则
在上一节的C++中的继承中介绍了什么是类型兼容性原则。所谓的类型兼容性原则是指子类公有继承自父类时,包含了父类的所有属性和方法,因此父类所能完成的功能,使用子类也可以替代完成,子类是一种特殊的父类。所以可以使用子类对象初始化父类对象,可以用父类指针指向子类对象,可以用父类引用来引用子类对象。
2.函数的重写
函数的发生在类的继承过程中,所谓的函数的重写是指在继承中,子类定义了与父类函数原型相同的函数,即定义了和父类中一样的函数。
3.类型兼容性原则遇上函数的重写
# include<iostream> using namespace std; /* 定义父类 */ class Parent { public: /* 定义print函数 */ void print() { cout << "Parent print()函数" << endl; } }; /* 定义子类继承自父类,并重写父类的print函数 */ class Child :public Parent { public: /* 重写父类的print函数 */ void print() { cout << "Child print()函数" << endl; } }; int main() { Child c; /* 调用子类对象的print函数,打印子类的print函数 */ c.print(); /* 通过使用作用域操作符调用父类的print函数,打印父类的print函数 */ c.Parent::print(); /* 当我们使用类型兼容性原则的时候,发现调用的函数是父类的print函数,这是符合编译器规则的 */ Parent p1 = c; p1.print(); Parent * p2 = &c; p2->print(); Parent& p3 = c; p3.print(); return 0; }
输出结果:
4.类型兼容性原则遇上函数的重写的总结
- 父类中被子类重写的函数依然存在于子类中,我们可以通过作用域操作符调用父类的函数。
- 子类重写父类的函数,调用子类对象时是调用的被重写的函数。
- 在C++编译期间,编译器不知道父类指针指向的是一个什么对象,编译器认为最安全的方法就是调用父类对象的函数。
5.静态联编和动态联编
- 所谓的联编就是指一个程序模块、代码之间互相关联的过程。
- 静态联编是程序的匹配、链接在编译阶段实现,也称为早期匹配,函数的重载使用的是静态联编。
- 动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定),switch和if语句是动态联编的典型例子。
6.类型兼容性原则和函数重写所带来的问题
- 首先编译器的这种做法是我们所不期望的。
- 我们需要的是根据实际的对象类型,来调用实际的函数。
- 如果父类指针(引用)指向(引用)的是父类对象,则调用父类对象的函数。
- 如果父类指针(引用)指向(引用)的是子类对象,则调用子类对象的函数。
7.针对上述问题的解决
针对上面的问题,C++提供了一套解决方案来实现上述我们的期望,通过使用virtual关键字来修饰被重写的函数后,即可以实现我们上述的问题。
8.多态的代码示例
# include<iostream> using namespace std; /* 定义父类 */ class Parent { public: /* 定义print函数,使用virtual关键字修饰 */ virtual void print() { cout << "Parent print()函数" << endl; } }; /* 定义子类继承自父类,并重写父类的print函数 */ class Child :public Parent { public: /* 重写父类的print函数 */ virtual void print() { cout << "Child print()函数" << endl; } }; int main() { Child c; /* 调用子类对象的print函数,打印子类的print函数 */ c.print(); /* 通过使用作用域操作符调用父类的print函数,打印父类的print函数 */ c.Parent::print(); /* 调用父类对象的函数发现当父类指针(引用)指向(引用)子类对象时,调用的是子类对象的函数,元素除外 */ Parent p1 = c; p1.print(); Parent * p2 = &c; p2->print(); Parent& p3 = c; p3.print(); return 0; }
输出结果:
9.多态案例的分析
- 使用virtual关键字的函数被重写后就会根据实际的对象类型指向对应的函数。
- 所谓的多态就是指同样的调用语句会有多种不同的表现状态。
- 父类指针指向子类对象和父类对象引用子类对象才可以实现所谓的多态,而父类元素被子类对象初始化是不展示多态特性的。
10.多态成立的条件
- 存在继承关系。
- 父类函数为virtual函数,并且子类重写父类的virtual函数。
- 存在父类的指针(引用)指向(引用)子类对象。
二,多态的原理探究
1.多态原理基础知识
- 当类中声明了虚函数(即virtual关键字修饰的函数)后,编译器会根据类生成一张虚函数表。
- 虚函数表是一张用来存储类中虚函数指针的表,虚函数表由编译器自动生成和维护。
- 当类中存在虚函数时,每个对象都会存在一个指向虚函数表的vptr指针。编译器会给父类对象,子类对象提前布局vptr指针,当调用对应的函数时,会根据vptr指针所指向的虚函数表来查找相应的函数并调用。
- vptr指针是指向虚函数表的指针,vptr指针一般作为类对象的第一个成员。
2.多态实现原理图示
3.多态原理说明
- 通过虚函数表的指针vptr在运行时调用相应的虚函数表中的函数,因此多态是动态联编。根据vptr寻找虚函数表是寻址操作,找到后再调用相应的函数。而普通的成员函数则是在函数编译时就已经确定了要调用的函数,因此在执行效率上虚函数要慢许多,所以出于效率的考虑,没有必要将类中的所有成员函数声明为虚函数。
- C++编译器在运行期间,不需要区分对象时子类对象还是父类对象,而只是 用相应的vptr指针来调用相应的函数而已,因此造成了这种虚假的多态现象。
4.vptr指针的存在证明
# include<iostream> using namespace std; class Test1 { public: /* 虚函数 */ virtual void test() { cout << "vptr指针的存在证明" << endl; } }; class Test2 { public: /* 普通成员函数 */ void test() { cout << "vptr指针的存在证明" << endl; } }; int main() { Test1 t1; Test2 t2; cout << "Test1 sizeof = " << sizeof(t1) << endl; cout << "Test1 sizeof = " << sizeof(t2) << endl; return 0; }
输出结果:
我们发现含有虚函数的类的对象包含了4个字节,说明存在一个vptr指针,因为指针大小即4个字节。
5.vptr指针的创建时机
vptr指针是在对象构造函数结束之后才创建的,然后指向虚函数表。
三,虚析构函数
1.虚析构函数的作用
当我们在开发父类的时候,通常会把父类的析构函数声明为虚函数,因为在继承中,当我们delete释放内存的时候,子类的对象的析构函数不会去执行,我们需要将父类的析构函数显式的声明为虚函数才会让子类的析构函数去调用执行。
2.案例演示
# include<iostream> using namespace std; class MyParent { public: MyParent() { cout << "MyParent构造函数" << endl; } /* 父类的析构函数一般声明为虚析构函数 */ virtual~MyParent() { cout << "MyParent析构函数" << endl; } }; class MyChild:public MyParent { public: MyChild() { cout << "MyChild构造函数" << endl; } ~MyChild() { cout << "MyChild析构函数" << endl; } }; int main() { /* 如果删除父类的虚析构函数,则子类的析构函数不会被调用执行 */ MyParent * p = new MyChild; delete p; return 0; }
四,函数的重载和重写的区别
1.函数的重载
- 必须在同一个类中。
- 子类无法重载父类的函数,父类的同名函数会被子类名称相同的函数覆盖,但不会发生重载。
- 重载是编译期间根据函数的参数个数和参数类型决定调用哪个函数。
2.函数的重写
- 函数的重写必须发生在继承中。
- 父类和子类的函数原型必须相同。
- 使用virtual关键字的父类函数才能成为函数的重写,否则叫做函数的重定义。
- 函数的重写调用时是在运行期间调用,属于动态联编,是根据实际对象的vptr指针所指向的虚函数表决定调用哪个函数。