对于多继承类的理解,我一直感到疑惑。为什么要使用多继承呢?为什么不用组合呢?这个疑惑一会儿再来解答。今天先来探索一下多继承类下,虚函数表与虚函数表指针在内存布局的情况。
在运行任何测试代码之前,我先提出我目前的理解。如果先不考虑有菱形继承的情况。我们有一个电动车ElectricCar类,它继承了两个类:一个是Vehicle交通工具类(电动车是交通工具),一个是Electronic电子设备类(不得不说确实是电子设备)。它的内存布局我目前的理解长这样:
1 2 3 4 5 6 Vehicle的虚函数表指针 Vehicle的成员对象(int,string等) Electronic的虚函数表指针 Electronic的成员对象 ElectricCar的虚函数表指针 ElectricCar的成员对象
按照我目前的理解,为了实现多态(也就是可以用基类的指针指向派生类的对象)的话,好像必须要保留每一个基类对象的虚函数表指针。
口说无凭。来看看代码测试:
首先定义了三个基类,分别是交通工具类,电子设备类和导航类(工程里还是用组合好)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class Vehicle {public : virtual void vehicle_start () { cout << "Vehicle::vehicle_start()" << endl; } virtual void vehicle_stop () { cout << "Vehicle::vehicle_stop()" << endl; } virtual ~Vehicle () { cout << "Vehicle destructor" << endl; } }; class Electronic {public : virtual void electronic_powerOn () { cout << "Electronic::electronic_powerOn()" << endl; } virtual void electronic_powerOff () { cout << "Electronic::electronic_powerOff()" << endl; } virtual ~Electronic () { cout << "Electronic destructor" << endl; } }; class Navigable {public : virtual void navigable_setDestination () { cout << "Navigable::navigable_setDestination()" << endl; } virtual ~Navigable () { cout << "Navigable destructor" << endl; } };
又定义了派生类如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class ElectricCar : public Vehicle, public Electronic {public : virtual void vehicle_start () override { cout << "ElectricCar::vehicle_start() [静音启动]" << endl; } virtual void electronic_powerOn () override { cout << "ElectricCar::electronic_powerOn() [智能启动]" << endl; } virtual void electricCar_charge () { cout << "ElectricCar::electricCar_charge() [专用充电]" << endl; } virtual ~ElectricCar () { cout << "ElectricCar destructor" << endl; } }; class SmartElectricCar : public Vehicle, public Electronic, public Navigable {public : virtual void vehicle_start () override { cout << "SmartElectricCar::vehicle_start() [语音控制启动]" << endl; } virtual void electronic_powerOff () override { cout << "SmartElectricCar::electronic_powerOff() [延时关机]" << endl; } virtual void navigable_setDestination () override { cout << "SmartElectricCar::navigable_setDestination() [AI推荐路线]" << endl; } virtual void smartElectricCar_autoPilot () { cout << "SmartElectricCar::smartElectricCar_autoPilot() [自动驾驶]" << endl; } virtual ~SmartElectricCar () { cout << "SmartElectricCar destructor" << endl; } };
首先来测试不同类的大小
1 2 3 4 5 6 7 8 void testVTablePointers () { cout << "======= 测试不同类的对象大小 =======\n" ; cout << "sizeof(Vehicle): " << sizeof (Vehicle) << " bytes" << endl; cout << "sizeof(Electronic): " << sizeof (Electronic) << " bytes" << endl; cout << "sizeof(Navigable): " << sizeof (Navigable) << " bytes" << endl; cout << "sizeof(ElectricCar): " << sizeof (ElectricCar) << " bytes" << endl; cout << "sizeof(SmartElectricCar): " << sizeof (SmartElectricCar) << " bytes" << endl; }
测试结果:
1 2 3 4 5 6 7 ======= 测试不同类的对象大小 ======= sizeof(Vehicle): 8 bytes sizeof(Electronic): 8 bytes sizeof(Navigable): 8 bytes sizeof(ElectricCar): 16 bytes sizeof(SmartElectricCar): 24 bytes
在64位系统中,一个指针通常为8字节。从测试结果可以看出: ElectricCar有2个vptr,所以有16字节 SmartElectricCar有3个vptr,所以有24字节
接着看一看多继承下的指针转换
我一直认为,当一个基类指针指向派生类时,这个基类指针的地址就是派生类最头的那个虚函数表指针的地址。但这好像只适用于单继承。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void testPointerCasting () { cout << "\n======= 测试多继承下的指针转换 =======\n" ; ElectricCar electricCar; cout << "\n1. ElectricCar对象地址: " << &electricCar << endl; Vehicle* vehiclePtr = &electricCar; Electronic* electronicPtr = &electricCar; cout << "2. Vehicle* 指针值: " << vehiclePtr << endl; cout << "3. Electronic* 指针值: " << electronicPtr << endl; cout << "4. 两个指针的差值: " << reinterpret_cast <char *>(electronicPtr) - reinterpret_cast <char *>(vehiclePtr) << " 字节" << endl; }
测试结果:
1 2 3 4 5 6 7 8 9 10 ======= 测试多继承下的指针转换 ======= 1. ElectricCar对象地址: 000000CC36AFF868 2. Vehicle* 指针值: 000000CC36AFF868 3. Electronic* 指针值: 000000CC36AFF870 4. 两个指针的差值: 8 字节 ElectricCar destructor Electronic destructor Vehicle destructor
可以看出,和单继承不同。多继承时,不同的基类子对象在派生类中位置不同。Vehicle子对象在ElectricCar的开始位置而Electronic子对象在Vehicle子对象之后。正因如此,二者的指针地址并不一样,他们各自指向了自己的那个子对象位置的开头。相差的8字节便是一个虚函数表指针的大小。
现在可以认为,两个派生类的内存布局如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ElectricCar对象布局: [vptr_Vehicle ] → Vehicle虚函数表 [Vehicle 数据成员] [vptr_Electronic ] → Electronic虚函数表 [Electronic 数据成员] [ElectricCar 新增数据成员] SmartElectricCar对象布局: [vptr_Vehicle ] → Vehicle虚函数表 [Vehicle 数据成员] [vptr_Electronic ] → Electronic虚函数表 [Electronic 数据成员] [vptr_Navigable ] → Navigable虚函数表 [Navigable 数据成员] [SmartElectricCar 新增数据成员]
一个很重要的点:当通过基类指针调用虚函数时,编译器会自动调整this指针。
用一个直观的多继承的内存布局图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 地址 内容 大小 0x628C0FF328 ┌─────────────────────┐ │ Base1的vptr │ 8字节 0x628C0FF330 ├─────────────────────┤ │ Base1::b1_data=100 │ 4字节 ├─────────────────────┤ │ 填充字节 │ 4字节 0x628C0FF338 ├─────────────────────┤ │ Base2的vptr │ 8字节 0x628C0FF340 ├─────────────────────┤ │ Base2::b2_data=200 │ 4字节 ├─────────────────────┤ │ 填充字节 │ 4字节 0x628C0FF348 ├─────────────────────┤ │ Derived::d_data=300 │ 4字节 ├─────────────────────┤ │ 填充字节 │ 4字节 0x628C0FF350 └─────────────────────┘
Derived d; Base2* pb2 = &d; // pb2指向0x628C0FF338,而不是内存最头的0x628C0FF328
调用pb2->b2_func()时,主要的步骤如下:
通过pb2找到Base2的vptr
通过vptr找到虚函数表
调用正确的函数
函数内部的this指针就是pb2(指向Base2子对象)
虚函数表里到底有什么? 既然每个基类都有自己的虚函数表指针,那么这些虚函数表里到底存储了什么内容呢?
对于ElectricCar类,它有两个虚函数表:
Vehicle的虚函数表:存储Vehicle的虚函数地址
Electronic的虚函数表:存储Electronic的虚函数地址
但是,当ElectricCar重写了某些虚函数时,这些虚函数表里的内容会发生变化。让我写个测试来验证一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void testVTableContent () { cout << "\n======= 测试虚函数表内容 =======\n" ; ElectricCar electricCar; typedef void (*FuncPtr) () ; void ** vtable_vehicle = *(void ***)&electricCar; cout << "Vehicle虚函数表内容:" << endl; cout << " [0] vehicle_start: " << vtable_vehicle[0 ] << endl; cout << " [1] vehicle_stop: " << vtable_vehicle[1 ] << endl; cout << " [2] ~Vehicle: " << vtable_vehicle[2 ] << endl; void ** vtable_electronic = *(void ***)((char *)&electricCar + 8 ); cout << "\nElectronic虚函数表内容:" << endl; cout << " [0] electronic_powerOn: " << vtable_electronic[0 ] << endl; cout << " [1] electronic_powerOff: " << vtable_electronic[1 ] << endl; cout << " [2] ~Electronic: " << vtable_electronic[2 ] << endl; cout << "\n通过函数指针调用验证:" << endl; ((FuncPtr)vtable_vehicle[0 ])(); ((FuncPtr)vtable_electronic[0 ])(); }
测试结果会显示,Vehicle虚函数表中的vehicle_start指向的是ElectricCar::vehicle_start,而不是Vehicle::vehicle_start。这说明派生类重写的虚函数会替换基类虚函数表中的对应项。
但是,ElectricCar新增的虚函数electricCar_charge()会放在哪里呢?答案是:它会被添加到第一个基类(Vehicle)的虚函数表中。这是因为派生类新增的虚函数需要能够通过派生类指针调用,而第一个基类指针通常指向对象的起始位置。
多继承时,派生类自己的虚函数放在哪里? (引用自博客园某篇,忘复制地址了)
为什么派生类的虚函数是追加在第一张虚表的后面? 请看下面的一段汇编(没学过汇编,不献丑)结论: 派生类的虚函数是追加在第一张虚表的后面。当需要使用派生类的虚函数是,用第一张表的虚函数表指针指向派生类的虚函数即可。(个人观点)下面的汇编也应该是这样:1,找到虚函数表的起始地址,2.找到派生类的虚函数偏移,3.使用虚函数表指针指向派生类的虚函数。
1 2 3 4 5 6 7 8 9 10 11 12 Copy Highlighter-hljs deriveA *pda = &da 00A7A02E lea eax ,[ebp -28h ] 00A7A031 mov dword ptr [ebp -34h ],eax pda->print() 00A7A034 mov eax ,dword ptr [ebp -34h ] 00A7A037 mov edx ,dword ptr [eax ] 00A7A039 mov esi ,esp 00A7A03B mov ecx ,dword ptr [ebp -34h ] 00A7A03E mov eax ,dword ptr [edx +4 ] 00A7A041 call eax 00A7A043 cmp esi ,esp 00A7A045 call 00A714C4
构造函数和析构函数的调用顺序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Vehicle {public : int vehicle_id; Vehicle () : vehicle_id (1 ) { cout << "Vehicle constructor" << endl; } }; class Electronic {public : int electronic_id; Electronic () : electronic_id (2 ) { cout << "Electronic constructor" << endl; } }; void testConstructionOrder () { cout << "\n======= 测试构造和析构顺序 =======\n" ; cout << "创建ElectricCar对象:" << endl; ElectricCar ec; cout << "\n销毁ElectricCar对象:" << endl; }
测试结果:
1 2 3 4 5 6 7 8 9 10 ======= 测试构造和析构顺序 ======= 创建ElectricCar对象: Vehicle constructor Electronic constructor ElectricCar constructor 销毁ElectricCar对象: ElectricCar destructor Electronic destructor Vehicle destructor
可以看出:
构造顺序:按照继承列表的顺序,从左到右依次构造基类,最后构造派生类
析构顺序:与构造顺序完全相反,先析构派生类,然后从右到左析构基类
函数名冲突:二义性问题 多继承最容易遇到的问题就是函数名冲突。如果两个基类有同名的函数(即使参数不同),派生类在调用时就会产生二义性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Vehicle {public : void start () { cout << "Vehicle::start()" << endl; } }; class Electronic {public : void start () { cout << "Electronic::start()" << endl; } }; class ElectricCar : public Vehicle, public Electronic {public : void test () { Vehicle::start (); Electronic::start (); } };
解决方法有几种:
使用作用域解析符 :Vehicle::start() 或 Electronic::start()
在派生类中重写 :在派生类中定义一个start()函数,内部调用需要的基类版本
使用using声明 :using Vehicle::start; 将某个基类的函数引入派生类作用域
1 2 3 4 5 class ElectricCar : public Vehicle, public Electronic {public : using Vehicle::start; };
为什么要用多继承?为什么不用组合? 现在来解答我一开始的疑惑。
多继承的适用场景:
接口实现 :当一个类需要实现多个接口时,多继承是自然的选择。比如Java的接口、C++的纯虚基类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class IReadable {public : virtual void read () = 0 ; }; class IWritable {public : virtual void write () = 0 ; }; class File : public IReadable, public IWritable {public : void read () override { } void write () override { } };
混入(Mixin)模式 :提供可复用的功能片段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Serializable {public : virtual string serialize () = 0 ; }; class Loggable {public : virtual void log () = 0 ; }; class User : public Serializable, public Loggable { };
“是一个”关系的多重性 :当派生类确实是多个基类的特化时。
比如我们的ElectricCar,它确实”是一个”Vehicle,也”是一个”Electronic。这种情况下,多继承在语义上是合理的。
总结
多继承类有几个虚函数表? 答案:有几个基类(有虚函数的),就有几个虚函数表指针。
内存布局:每个基类子对象在派生类中都有独立的内存区域,包括自己的虚函数表指针。
指针转换:不同基类指针指向派生类对象时,地址可能不同,编译器会自动调整。
this指针调整:通过不同基类指针调用虚函数时,this指针会自动调整到正确的子对象位置。
构造/析构顺序:按继承列表顺序构造,逆序析构。
二义性处理:同名函数冲突需要用作用域解析符或using声明解决。
但这里没有提到多继承下虚析构的一些细节,后面再说吧。