之前一节讨论了C++语言中类在内存中分布模型,提到了C++语言编译器会自动为每一个拥有虚函数的类创建虚表和虚表指针,其中虚表指针指向虚表,而虚表则用于存放虚函数指针,如下图所示:

C++虚函数的内存分布
基类的内存模型
那么,若是拥有虚函数的类 A 作为基类,派生出其他类,这些派生类如何处理类 A 的虚表呢?换句话说,C++语言中的派生类是如何继承基类的虚函数的呢?为了便于讨论,先将类 A 的C++语言代码补充完整,请看:
class A {public: void foo1() { printf("A::prv_i1 addr: %p", &prv_i1); printf("A::prv_i2 addr: %p", &prv_i2); } void foo2() {} virtual void vfoo1() {} virtual void vfoo2() {} int pub_i1;private: int prv_i1; int prv_i2;};

类 A 的C++语言代码
在上述C++语言代码中,类 A 拥有两个常规函数,两个 int 型变量,此外它还有两个虚函数 vfoo1() 和 vfoo2(),因此编译器处理类 A 时,会自动为它分配一个虚表,用于存放 vfoo1() 和 vfoo2() 两个函数的地址。我们先定义一个 A 对象,并将相关成员的地址打印出来,相关的C++语言代码如下,请看:
...A a;print_addr(A, a);...
因为稍后还要分析基类 A 的派生类成员地址,所以为了方便,将打印地址的功能定义为 print_addr() 宏了,它的C++语言代码如下,请看:
#define print_addr(class, obj) printf(#obj" addr: %p", &obj); printf("sizeof "#obj" : %d", sizeof(obj)); printf(#class"::pub_i1 addr: %p", &obj.pub_i1); obj.foo1(); printf(#class"::foo1() addr: %p", (void *)&class::foo1); printf(#class"::foo2() addr: %p", (void *)&class::foo2); printf(#class"::vfoo1() addr: %p", (void *)&class::vfoo1); printf(#class"::vfoo2() addr: %p", (void *)&class::vfoo2)

print_addr()是一个宏
编译并执行,得到如下输出:
a addr: 0x7ffdcf3c8d50sizeof a : 24A::pub_i1 addr: 0x7ffdcf3c8d58A::prv_i1 addr: 0x7ffdcf3c8d5cA::prv_i2 addr: 0x7ffdcf3c8d60A::foo1() addr: 0x400b62A::foo2() addr: 0x400ba4A::vfoo1() addr: 0x400baeA::vfoo2() addr: 0x400bb8
在C++语言类的内存分布一节我们曾提到类的成员函数是独立存储的,只有成员变量和虚表指针(如果该类有虚函数的话)才会为类占据内存空间,因此对象 a 的 size 为 24,正是它的 3 个 int 型的成员变量与一个虚表指针占用的内存大小。
在我的机器上,int 型变量占用内存为 4 字节,指针占用内存大小为 8 字节。到这里可能有读者迷惑了,3个int型成员变量占用的内存为 12 字节,加上虚表指针的 8 字节,也才 20 字节,而 sizeof(a) 等于 24,多出的 4 字节从哪里来呢?请参考我之前关于内存对齐的文章。
内存对齐后,对象 a 的内存分布如下图所示:

对象 a 的内存分布
派生类的内存模型
类 A 作为基类,肯定可以被其他派生类继承的,下面是一段C++语言代码示例:
class B : public A {public: void foo1() { printf("B::prv_i1 addr: %p", &prv_i1); printf("B::prv_i2 addr: %p", &prv_i2); } void foo2() {} virtual void vfoo1() {} //void vfoo2() {}private: int prv_i1; int prv_i2;public: int pub_i1;};class C : public B {public: void foo1() { printf("C::prv_i1 addr: %p", &prv_i1); printf("C::prv_i2 addr: %p", &prv_i2); } void foo2() {} //void vfoo1() {} virtual void vfoo2() {}private: int prv_i1; int prv_i2;};

基类A及其派生类
上述C++语言代码定义了继承基类 A 的派生类 B,接着又以类 B 为基类定义了派生类 C。其中类 B 重写了由基类 A 继承而来的虚函数 vfoo1(),类 C 重写了由基类 B 继承而来的虚函数 vfoo2()。为了弄清派生类的内存模型,我们使用print_addr()宏将类A、B、C相关的地址打印出来:
A a;B b;C c;cout << endl;print_addr(A, a);cout << endl;print_addr(B, b);cout << endl;print_addr(C, c);cout << endl;
编译并执行这段C++语言代码,得到的输出如下:
a addr: 0x7ffe4eeacd50sizeof a : 24A::pub_i1 addr: 0x7ffe4eeacd58A::prv_i1 addr: 0x7ffe4eeacd5cA::prv_i2 addr: 0x7ffe4eeacd60A::foo1() addr: 0x400b9eA::foo2() addr: 0x400be0A::vfoo1() addr: 0x400beaA::vfoo2() addr: 0x400bf4b addr: 0x7ffe4eeacd70sizeof b : 32B::pub_i1 addr: 0x7ffe4eeacd8cB::prv_i1 addr: 0x7ffe4eeacd84B::prv_i2 addr: 0x7ffe4eeacd88B::foo1() addr: 0x400bfeB::foo2() addr: 0x400c40B::vfoo1() addr: 0x400c4aB::vfoo2() addr: 0x400bf4c addr: 0x7ffe4eeacd90sizeof c : 40C::pub_i1 addr: 0x7ffe4eeacdacC::prv_i1 addr: 0x7ffe4eeacdb0C::prv_i2 addr: 0x7ffe4eeacdb4C::foo1() addr: 0x400c54C::foo2() addr: 0x400c96C::vfoo1() addr: 0x400c4aC::vfoo2() addr: 0x400ca0
先关注类 B 的内存分布。注意到对象 b 的 size 为 32,这是类 B 继承基类 A 的结果。而基类 A 的 size 等于 24,类 B 仅多出 8 字节,问题来了:且不谈类 B 拥有自己的虚函数,可能拥有虚表指针,仅仅 3 个 int 型变量就要占用 12 字节内存,8 字节怎么放得下呢?
仔细考虑下,这个问题并不难回答。在分析基类 A 的内存分布时,提到了类 A 占据的内存中其实是有 4 个字节被填充的,这 4 个字节内存当然不会永远被白白浪费——在不违背内存对齐规则下,编译器会在“恰当的”时机使用这 4 个字节。在本例中,“恰当的时机”就是类 B 中的成员变量 prv_i1恰好为 4 字节,既然基类 A 可以被填充无意义的数据,自然也可以填充“有用的数据(prv_i1)”。这就解释了派生类 B 多出了 3 个 int 型成员变量,占用的内存却只比基类 A 多出 8 字节的原因。
仔细观察类 B 对象 b 的输出,应该能够发现对象 b 的地址与它的第一个成员变量(prv_i1)的地址偏移了 0x14 也就是 20 字节,在上一节我们已经知道对象的前 8 字节用于存储了虚表指针,接下来的 12 字节恰好存储了由基类 A 继承而来的三个 int 型变量,因此此时对象 b 占用的的内存模型如下图所示:

对象 b 的内存模型
对象 b 占用的内存空间为 32 字节一目了然了。再来分析对象 b 的成员函数,首先常规非虚函数就不必说了,它独立于对象 b 存储,不管类 B 实例化多少个对象,这些对象都共用同一地址处的成员函数,需要仔细考虑的是 b 的虚函数。
类 B 继承基类 A,并且重写了虚成员函数 vfoo1(),从对象 b 打印的地址可以看出,由基类 A 继承而来的 vfoo2() 函数地址与对象 a 中的 vfoo2() 函数地址是一样的,这说明对象 a 和 b 共用同一个虚函数 vfoo2(),同样的,对象 b 和对象 c 公用同一个虚函数 vfoo1()。
可以在 main() 函数中添加下面这段C++语言代码,分别将对象 a 和对象 b 的虚表中记录的函数指针打印出来:
... void **p = (void **)&b; void **vptr = (void **)*p; printf("b vptr[0]: %p", vptr[0]); printf("b vptr[1]: %p", vptr[1]); p = (void **)&a; vptr = (void **)*p; printf("a vptr[0]: %p", vptr[0]); printf("a vptr[1]: %p", vptr[1]);...
编译并执行修改后的C++语言代码,得到的输出如下,请看:
...A::vfoo1() addr: 0x400c7cA::vfoo2() addr: 0x400c86...B::vfoo1() addr: 0x400cdcB::vfoo2() addr: 0x400c86...b vptr[0]: 0x400cdcb vptr[1]: 0x400c86a vptr[0]: 0x400c7ca vptr[1]: 0x400c86
应注意 b vptr[1] 和 a vptr[1] 的值都是 0x400c86,该地址对应的函数为 vfoo2(),这进一步验证了前文的推测,事实上,派生类 B 及其基类 A 的实例化对象包括成员函数在内的内存模型如下图所示:

内存模型
该图清晰的描述了C++语言中的派生类是如何继承基类包括虚函数在内的成员的。弄懂了这张图,读者应该能够轻易的分析出派生类 C 的内存模型,这里就不再赘述了。
小结
本文主要在上一节的基础上,进一步分析了C++语言中带虚函数的基类与派生类的内存模型,值得注意的是C++语言中的对象和C语言的结构体有些相似,在追求效率时,也是会执行内存对齐操作的。另外,C++语言在处理虚函数的继承与派生时,的确有一些不同,例如自动分配虚表指针与虚表,共用同一个虚函数等,不过从本质上来看,虚函数又的确没有什么特殊的。