c++ 多态内存布局分析
前言
上节对分析了多态的基本概念, 本节从内存布局的角度再来分析一下多态吧。
单一继承的内存布局
最开始我们分析了虚指针, 虚表, 但都只停留在单继承中, 现在我们在来扩展的探讨多重继承的内存布局
#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <functional>
using std::cin; using std::cout; using std::string; using std::endl;
using type = int;
class Parent {
public:
virtual void fun1() { }
virtual void fun2() { }
virtual void fun3() { }
};
class Child : public Parent {
public:
virtual void fun1() { }
virtual void fun2_child() { }
virtual void fun3_child() { }
};
class GrandChild : public Child {
public:
virtual void fun1() { cout << "GrandChild::fun1()" << endl; }
virtual void fun2_child() { }
virtual void fun3_grandchild() { }
};
int main()
{
system("pause");
}
我们先来看一下Child
类的内存布局。从下面的图片可以看出来Child
继承了Parent
虚表中虚函数fun2
和fun3
, 但是将覆盖后新的虚函数fun1
替换了原来基类的fun1
了, 做完这些工作之后再将子类中声明的其他虚函数放在之后。
同样我们再来看GrandChild
的内存布局。 从下面我们可以发现相同的操作, GrandChild
先是将继承来的虚表复制过来, 再将修改过后的虚函数覆盖掉原来的虚函数位置, 最后又将新声明的虚函数插入在尾部。
明白了上面的例子之后我们再来分析多态又是怎么调用虚函数的。
下面实现了多态, 指针p
指向g
的首地址, 也就是g
的vptr, 但是在编译p->fun1()
时编译器只能通过计算来计算出fun1
函数在虚表中的偏移量为0, 即p->vptr[0]
, 但是需要确定vptr[0]
是GrandChild
类的函数, 函数Parent
类的函数必须要在运行时才能知道, 所以多态是在运行时才能绑定执行的函数体, 不过偏移在编译期就能就算, 运行时只需要绑定就行, 效率已经是很高。
int main()
{
Parent *p;
GrandChild g;
p = &g;
p->fun1(); // GrandChild::fun1()
system("pause");
}
多重继承的内存布局
多重继承的类的内存布局与单一继承的类的内存布局有些不一样了, 不一样在于后者只有一个虚指针, 而前者有多个虚指针。
#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <functional>
using std::cin; using std::cout; using std::string; using std::endl;
class Base1 {
public:
virtual void fun1() { }
virtual void fun2() { cout << Base1::fun2(); }
virtual void fun3() { }
};
class Base2 {
public:
virtual void fun1() { }
virtual void fun2() { }
virtual void fun3() { }
};
class Base3 {
public:
virtual void fun1() { }
virtual void fun2() { }
virtual void fun3() { }
};
class Derive : public Base1, public Base2, public Base3 {
public:
virtual void fun1() { cout << Derive::fun1(); }
virtual void D_fun() { }
};
int main()
{
system("pause");
}
从下面Derive
的内存分布可以验证前面说的多重继承不止一个虚指针。 分析之前要先对主vptr
和次vptr
解释一下, 第一个vptr
是主, 其他的vptr
都是次, 而所有的虚表信息都是由主vptr
决定的, 一般第一个继承的基类的虚表就是主vptr
。而每个虚表最开始有一个数字0, -8, -16, 简称为Thunk
; 这些数字是用来调整次vptr
的偏移量让其都通过偏移量最终指向主vptr
, 这里就不再展开细说了。
接下来再来分析多态, p
指针调用fun1
函数时调用的是Derive
类覆盖的虚函数, 而调用fun2
时, 调用的则是主vptr
中指向的Base1
中的虚函数。而p2
指向的是次vptr
, 所以能够正常的输出是Base2
中的虚函数。
int main()
{
Base1 *p;
Derive g;
p = &g;
p->fun1(); // Derive::fun1()
p->fun2(); // Base1::fun2()
Base2 *p2;
p->fun2(); // Base2::fun2()
system("pause");
}
关于直接调用fun2
函数就会出现二义性, 原因就是直接调用虚函数就在编译期就确定调用的是一个虚函数, 而多重继承有多个虚表, 多个虚表有重复的虚函数名就无法再编译时决定调用的是哪一个虚表中的虚函数了, 所以就有二义性的问题。
int main()
{
Derive g;
g.fun2(); // 二义性
system("pause");
}
总结
本节从内存布局的角度简单的分析了多态的动态绑定以及与虚表的关系, 这样应该对多态有了更深的印象就行。而本节也有一种菱形继承没有分析, 因为出处不是很大也就暂时省去了, 有兴趣的可以自己尝试分析。