转载自CoolShell: https://coolshell.cn/articles/12176.html
注:
1. 原文包含实验原理、实验代码与结论;本文只包含实验原理与结论,除去实验代码,目的是更为精简。
2. 本文对原文的行文结构和文字都有较大幅度的更改与删减。
3. 原文写作较早,采用的是32位机器做实验;本文没有更改。这一点对于结论没有影响;可能在某些细节处注意即可。
对象的影响因素
对内存布局的影响主要有:成员变量、虚函数、虚函数覆盖
常见的继承种类:
1. 单一继承
2. 单一的虚拟继承
3. 多重继承(继承多个类)
4. 重复的多重继承(继承的多个父类中有相同的祖父类)
5. 钻石型虚拟多重继承
实验原理
如何通过一个对象的地址来取得虚函数表的地址呢?
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
// Note: 32-bit machine
cout << "vtable address: " << (int*)(&b) << endl;
cout << "Address of the first virtual function:" << (int*)*(int*)(&b) << endl;
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
注:
如果是64位机器,将上面的int都改为long即可。
虚表地址是 (int*)(&b) 不难理解,因为虚表地址是放在对象内存空间的最前面的。
而第一个虚函数的地址,就是虚表地址处所存储的数字,因此,对虚表地址解引用(deference,即左边加一个星号)后,即第一个虚函数的地址;最左边再加一个(int*),是为了表示它是一个指针。
然后将这个指针强制转化为函数指针,就可以调用了。
由此也可以看出,Base中定义的第一个虚函数是一个无参无返回值的函数。
实验环境:
1. Windows XP + VC++2003
2. Cygwin + G++3.4.4
单一继承
class Parent {
public:
int iparent;
Parent ():iparent (10) {}
virtual void f() { cout << " Parent::f()" << endl; }
virtual void g() { cout << " Parent::g()" << endl; }
virtual void h() { cout << " Parent::h()" << endl; }
};
class Child : public Parent {
public:
int ichild;
Child():ichild(100) {}
virtual void f() { cout << "Child::f()" << endl; }
virtual void g_child() { cout << "Child::g_child()" << endl; }
virtual void h_child() { cout << "Child::h_child()" << endl; }
};
class GrandChild : public Child{
public:
int igrandchild;
GrandChild():igrandchild(1000) {}
virtual void f() { cout << "GrandChild::f()" << endl; }
virtual void g_child() { cout << "GrandChild::g_child()" << endl; }
virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
};
内存布局:
注:
上图中的 Child::h1() 应该是原作者的笔误,应为:Child::h_child()
总结:
1. 虚函数表在最前面的位置(注:即相对地址为0的位置)
2. 成员变量根据其继承和声明顺序依次放在后面
3. 在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新
多重继承
class Base1 {
public:
int ibase1;
Base1():ibase1(10) {}
virtual void f() { cout << "Base1::f()" << endl; }
virtual void g() { cout << "Base1::g()" << endl; }
virtual void h() { cout << "Base1::h()" << endl; }
};
class Base2 {
public:
int ibase2;
Base2():ibase2(20) {}
virtual void f() { cout << "Base2::f()" << endl; }
virtual void g() { cout << "Base2::g()" << endl; }
virtual void h() { cout << "Base2::h()" << endl; }
};
class Base3 {
public:
int ibase3;
Base3():ibase3(30) {}
virtual void f() { cout << "Base3::f()" << endl; }
virtual void g() { cout << "Base3::g()" << endl; }
virtual void h() { cout << "Base3::h()" << endl; }
};
class Derive : public Base1, public Base2, public Base3 {
public:
int iderive;
Derive():iderive(100) {}
virtual void f() { cout << "Derive::f()" << endl; }
virtual void g1() { cout << "Derive::g1()" << endl; }
};
内存布局如下:
注:
上图中的Base1和Base2的虚表里的NULL是vc++的实现,而g++3.4.4对应的则是1,表示后面还有一个虚表;而对Base3的虚表的末尾,则2种实现都是NULL.
总结:
1. 在子类的内存布局中,多重继承的每个父类都有自己的虚表;
2. 子类的成员函数被放到了第一个父类的虚表中;
3. 内存布局中,其父类布局依次按声明顺序排列;
4. 每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
重复继承
class B
{
public:
int ib;
char cb;
public:
B():ib(0),cb('B') {}
virtual void f() { cout << "B::f()" << endl;}
virtual void Bf() { cout << "B::Bf()" << endl;}
};
class B1 : public B
{
public:
int ib1;
char cb1;
public:
B1():ib1(11),cb1('1') {}
virtual void f() { cout << "B1::f()" << endl;}
virtual void f1() { cout << "B1::f1()" << endl;}
virtual void Bf1() { cout << "B1::Bf1()" << endl;}
};
class B2: public B
{
public:
int ib2;
char cb2;
public:
B2():ib2(12),cb2('2') {}
virtual void f() { cout << "B2::f()" << endl;}
virtual void f2() { cout << "B2::f2()" << endl;}
virtual void Bf2() { cout << "B2::Bf2()" << endl;}
};
class D : public B1, public B2
{
public:
int id;
char cd;
public:
D():id(100),cd('D') {}
virtual void f() { cout << "D::f()" << endl;}
virtual void f1() { cout << "D::f1()" << endl;}
virtual void f2() { cout << "D::f2()" << endl;}
virtual void Df() { cout << "D::Df()" << endl;}
};
内存布局如下:
我们可以看见,最顶端的父类B其成员变量存在于B1和B2中,并被D给继承下去了。而在D中,其有B1和B2的实例,于是B的成员在D的实例中存在两份,一份是B1继承而来的,另一份是B2继承而来的。所以,如果我们使用以下语句,则会产生二义性编译错误:
D d;
d.ib = 0; //二义性错误
d.B1::ib = 1; //正确
d.B2::ib = 2; //正确
注意,上面例程中的最后两条语句存取的是两个变量。虽然我们消除了二义性的编译错误,但B类在D中还是有两个实例,这种继承造成了数据的重复,我们叫这种继承为重复继承。重复的基类数据成员可能并不是我们想要的。所以,C++引入了虚基类的概念。
钻石型多重虚拟继承
上述的“重复继承”只需要把B1和B2继承B的语法中加上virtual 关键,就成了虚拟继承
上图和前面的“重复继承”中的类的内部数据和接口都是完全一样的,只是我们采用了虚拟继承:其省略后的源码如下所示:
class B {……};
class B1 : virtual public B{……};
class B2: virtual public B{……};
class D : public B1, public B2{ …… };
内存布局如下;
在上面的输出结果中,用不同的颜色做了一些标明。可以看到如下的几点:
1. 无论是GCC还是VC++,除了一些细节上的不同,其大体上的对象布局是一样的。也就是说,先是B1(黄色),然后是B2(绿色),接着是D(灰色),而B这个超类(青蓝色)的实例都放在最后的位置。
注:普通的多重继承中,虚表个数相当于继承的父类的个数;而这种钻石型虚拟继承中,还会多一个公共祖父类的虚表。
2. 关于虚函数表,尤其是第一个虚表,GCC和VC++有很重大的不一样。但仔细看下来,还是VC++的虚表比较清晰和有逻辑性。
3. VC++和GCC都把B这个超类放到了最后,而VC++有一个NULL分隔符把B和B1和B2的布局分开。GCC则没有。
4. GCC的内存布局中在B1和B2中则没有指向B的指针。这点可以理解,编译器可以通过计算B1和B2的size而得出B的偏移量。
注:
暂时先将精简后的文章刊出。后续可能会针对64位机器和更新的g++版本进行更新。
(完)