C++ 多态原理
多态就是多种形态,C++的多态分为静态多态与动态多态。
动态多态就是通过继承重写基类的虚函数实现的多态,在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。
多态的总结:
(1) 用virtual关键字声明的函数叫做虚函数,虚函数肯定是类的成员函数。
(2) 存在虚函数的类都有一个虚函数表叫做虚表。当类中声明虚函数时,编译器会在类中生成一个虚函数表。
(3) 类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的
(4) 虚函数表是一个存储类成员函数指针的数据结构。
(5) 虚函数表是由编译器自动生成与维护的。
(6) virtual成员函数会被编译器放入虚函数表中。
(7) 当存在虚函数时,每个对象中都有一个指向虚函数的指针(C++编译器给父类对象,子类对象提前布局vptr指针),当进行test(parent *base)函数的时候,C++编译器不需要区分子类或者父类对象,只需要在base指针中,找到vptr指针即可)。
(8) vptr一般作为类对象的第一个成员。
为什么拷贝构造要使用引用?
1. 传递参数时避免拷贝产生的开销
在使用拷贝构造时,我们一般需要将源对象复制到新对象中。如果在拷贝构造中直接使用源对象的值,则会多次产生拷贝操作,导致不必要的开销。而使用引用作为参数,则可以节省这部分开销。
2. 避免出现无限递归的情况
如果在拷贝构造函数中使用传值方式,那么在执行时就会又次产生一个新的副本,从而又会调用拷贝构造函数。这样就会导致无限递归,进而使程序崩溃。而使用引用作为参数,则可以避免出现这种情况。
为什么调用普通函数比调用虚函数的效率高?
因为普通函数是静态联编的,而调用虚函数是动态联编的。
(1) 静态联编 :在编译的时候就确定了函数的地址,然后call就调用了。
(2) 动态联编 :首先需要取到对象的首地址,然后再解引用取到虚函数表的首地址后,再加上偏移量才能找到要调的虚函数,然后call调用。
明显动态联编要比静态联编做的操作多,肯定就费时间。
为什么要用虚函数表(存函数指针的数组)?
实现多态,父类对象的指针指向父类对象调用的是父类的虚函数,指向子类调用的是子类的虚函数。同一个类的多个对象的虚函数表是同一个,所以这样就可以节省空间,一个类自己的虚函数和继承的虚函数还有重写父类的虚函数都会存在自己的虚函数表。
为什么构造函数不能为虚函数?
C++对象在三个地方构建:(1)函数堆栈;(2)自由存储区,或称之为堆;(3)静态存储区。无论在那里构建,其过程都是两步:首先,分配一块内存;其次,调用构造函数。好,问题来了,如果构造函数是虚函数,那么就需要通过虚函数表 来调用,但此时面对一块 raw memeory,到哪里去找虚函数表呢?毕竟,虚函数表是在构造函数中才初始化的啊,而不是在其之前。因此构造函数不能为虚函数。
为什么C++默认的析构函数不是虚函数?
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
C++中析构函数的作用?
当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
注意:只能有一个析构函数,不能重载。
调用顺序?
构造函数:基类的构造函数 ----> 对象成员构造函数 ----> 派生类的构造函数
析构函数:派生类的析构函数 ----> 对象成员析构函数 ----> 基类析构函数
为什么要把基类的析构函数定义为虚函数?
为了防止内存泄露,如果不用virtual关键字,当基类指针指向派生类对象时,析构时只会析构基类,而不会析构派生类。
#include<iostream> #include<stdio.h> using namespace std; class A { //注意:内存对齐原则 public: int i; //4 Bytes virtual void func() {} //8 Bytes virtual void func2() {} //8 Bytes 与func函数共用8字节内存 }; class B: public A { int j; //4 Bytes void func() {} //普通函数不占内存空间 }; int main() { printf("A: %d, B: %d\n", sizeof(A), sizeof(B)); return 0; }
在 64位编译模式下,程序的运行结果是:
A: 16, B: 16
如果将程序中的 virtual 关键字去掉,输出结果变为:
A: 4, B: 8
虚函数表是编译器生成的,程序运行时被载入内存。一个类的虚函数表中列出了该类的全部虚函数地址。例如,在上面的程序中,类 A 对象的存储空间以及虚函数表(假定类 A 还有其他虚函数)如图 1 所示。
图1:类A对象的存储空间以及虚函数表
类 B 对象的存储空间以及虚函数表(假定类 B 还有其他虚函数)如图 2 所示。
假设 pa 的类型是 A*,则 pa->func() 这条语句的执行过程如下:
(1) 取出 pa 指针所指位置的前 8 个字节,即对象所属的类的虚函数表的地址。如果 pa 指向的是类 A 的对象,则这个地址就是类 A 的虚函数表的地址;如果 pa 指向的是类 B 的对象,则这个地址就是类 B 的虚函数表的地址。
(2) 根据虚函数表的地址找到虚函数表,在其中查找要调用的虚函数的地址。
(3) 根据找到的虚函数的地址调用虚函数。
由以上过程可以看出,只要是通过基类指针或基类引用调用虚函数的语句,就一定是多态的,也一定会执行上面的查表过程,哪怕这个虚函数仅在基类中有,在派生类中没有。