1、一个对象模型的内存布局
在C++中,对象模型的内存布局通常包含三个部分:虚函数表指针、成员变量和填充字节。
|------------------|
| vptr | <---- 虚函数表指针
|------------------|
| member1 | <---- 成员变量1
|------------------|
| member2 | <---- 成员变量2
|------------------|
| padding | <---- 填充字节
|------------------|
- 虚函数表指针是一个指向虚函数表的指针,虚函数表存储了类中虚函数的地址。这个指针通常是对象中的第一个成员,如果类没有定义任何虚函数,则不会有虚函数表指针。
- 成员变量是类定义中声明的变量,它们按照定义顺序存储在对象中。
- 填充字节是为了内存对齐而添加的额外字节,以便于硬件的访问和操作。填充字节通常是由编译器自动添加的,以使得对象的地址是某个特定大小的整数倍。
2、虚函数表的结构
虚函数表(virtual table)是一个由编译器自动生成的数据结构,用于实现C++中的多态机制。每个含有虚函数的类都有一个虚函数表,该表包含了该类的虚函数的地址。
子类继承父类的虚函数表的方式是复制一份。存在虚函数的类,都有自己的虚函数表,不与其他类共用。
对于一个含有虚函数的类,其虚函数表通常会被放置在该类的编译单元中的静态存储区域(如 .rodata 或 .data 段)。每个对象的vptr 通常被放置在对象的最开始位置,也就是该对象的地址。
下面是一个简单的虚函数表结构的示例:
+--------+
| vptr | --> 虚函数表地址
+--------+
| data |
+--------+
虚函数表:
+-----------------------+
| Animal::speak() |
+-----------------------+
| Animal::move() |
+-----------------------+
| Dog::speak() |
+-----------------------+
| Dog::move() |
+-----------------------+
| Dog::fetch() |
+-----------------------+
在上面的示例中,Animal 和 Dog 都有虚函数,它们各自有一个虚函数表。vptr 指针指向虚函数表的地址。Animal 的虚函数表包含 Animal::speak() 和 Animal::move() 两个函数的地址,Dog 的虚函数表包含 Dog::speak()、Dog::move() 和 Dog::fetch() 三个函数的地址。
3、如何实现多态?
通过上面的例子,可以很好地解释 C++ 中多态的原理。假设有如下的代码:
Animal* animalPtr = new Dog;
animalPtr->speak(); // 调用 Dog::speak()
animalPtr->move(); // 调用 Dog::move()
- 在上面的代码中,首先创建了一个 Dog 对象,并将其赋值给一个指向 Animal 的指针。由于 Animal 类中的函数 speak() 和 move() 是虚函数,因此它们被声明为虚函数,并放置在 Animal 类的虚函数表中。在 Dog 类中重新实现了这两个虚函数,并添加了一个新的虚函数 fetch(),这些函数的地址都被放置在 Dog 类的虚函数表中。
- 当执行 animalPtr->speak() 时,实际上是其类对象内存布局中 vptr 指针是指向 Dog 类的虚函数表,因此调用的实际上是 Dog::speak() 函数。
- 在 C++ 中,由于虚函数表的存在,通过基类指针或引用调用虚函数时,会根据实际对象的类型动态地绑定到该类型的虚函数,这就是多态的实现机制。在上面的示例中,由于 animalPtr 的实际类型是 Dog,因此调用的是 Dog 类中的虚函数,而不是 Animal 类中的虚函数。
需要注意的是,为了使函数能够动态绑定到正确的虚函数,每个类必须具有自己的虚函数表。如果没有虚函数,或者虚函数没有被重写,那么该类将没有虚函数表。此外,如果派生类中重新定义了基类中的虚函数,那么它必须使用 virtual 关键字进行声明,才能将其添加到虚函数表中,从而实现多态。