C++对象模型是C++语言中关于对象如何在内存中布局和如何工作的底层机制。理解C++对象模型对于编写高效、正确的C++代码非常重要。
基本概念
对象内存布局:C++对象在内存中的组织方式
虚函数机制:实现运行时多态的基础
继承模型:单继承、多继承和虚继承的实现
成员访问:成员变量和成员函数的访问机制
不含有有虚函数的对象模型
对于不包含虚函数的简单类:
内存布局:
成员变量按声明顺序排列
成员函数不占用对象空间(存储在代码区)
空类的大小:
如果一个类没有任何数据成员(包括静态成员),它的大小通常是 1字节(具体取决于编译器和内存对齐规则)。
带有虚函数的对象模型
当类包含虚函数时,编译器会添加一个虚函数表指针(vptr):
内存布局:
虚表指针(vptr)指向虚函数表(vtable)
然后是成员变量
虚函数表中存储着虚函数地址
继承模型
单继承
1、对一般继承而言,若子类重写(overwrite)了父类的虚函数,则子类虚函数将覆盖虚表中对应的父类虚函数(注意子类与父类拥有各自的一个虚函数表);
2、若子类并无overwrite父类虚函数,而是声明了自己新的虚函数,则该虚函数地址将扩充到虚函数表最后。
3、而对于虚继承,若子类overwrite父类虚函数,同样地将覆盖父类子物体中的虚函数表对应位置,而若子类声明了自己新的虚函数,则编译器将为子类增加一个新的虚表指针vptr,这与一般继承不同。
多继承
1、一般多继承 (非菱形继承)
如果继承于多个基类,且这个多个基类中有虚函数,那么 这个类 会有多个虚函数表。
这个类的对象会有多个虚函数表指针。
子类的虚函数被放在声明的第一个基类的虚函数表中。
overwrite时,所有基类的同名虚函数都被子类的同名虚函数覆盖。这样做就是为了解决不同的基类类型的指针指向同一个子类实例,而能够调用到实际的函数。
内存布局中,父类按照其声明顺序排列。
2、菱形继承
class A { int data; };
class B : public A { int b_data; };
class C :public A { int c_data; };
class D :public B, public c { int d_data; };
问题:D的对象会包含两份 A 的成员。通过D 访问A 的成员 data 会编译错误,需要通过 B::data 或 C::data 明确路径。
内存布局
相较于一般多继承,公共继承类在两个虚表中都有。
3.虚继承
class A { int data; };
class B : virtual public A { int b_data; };
class C : virtual public A { int c_data; };
class D : public B, public c { int d_data; };
最终派生类负责构造虚基类
注意:在虚继承的情况下,虚基类的构造由最底层的派生类直接负责,而不是由中间的基类来
构造过程:
1、先造虚基类A
由 D的构造函数直接调用 A的构造函数(B和C的造函数不再调用 A)
2、按照声明顺序构造:先 B,后 C
3、D的自身构造
解决菱形继承的问题。
this指针调整
在多继承中,当派生类指针转换为基类指针时,可能需要调整this指针的值:
Derived* d = new Derived;
Base2* b2 = d; // 可能需要调整指针值
对象构造与析构过程
分配内存
构造基类子对象
构造成员对象
执行构造函数体
析构顺序相反
运行时类型识别(RTTI)
通过type_info对象实现,通常与虚函数表存储在一起。
性能考虑
虚函数调用比普通函数调用多一次间接寻址
多重继承可能增加空间开销
虚继承会增加访问虚基类成员的开销
理解C++对象模型有助于编写更高效的代码,并更好地理解C++的底层机制。