瞅瞅C++中的对象模型(下)
前言
上一篇讲了C++对象模型中的多重继承部分,这一篇该总结总结虚继承的部分了。
1.9 重复继承
有了上面的基础,很自然的就会引入重复继承的问题。其基本继承关系如下:
Derive会从Base1中和Base2继承两份b_data。 如果直接引入这个b_data数据成员的话会造成数据的二义性(编译器不知道你是想访问Base1::b_data还是Base2::b_data,),除非进行显式定义。如d.Base1::b_data 、或者d.Base2::bata 。
同样的,实验代码验证一下:
//Base类
class Base{
public:
Base(int val = 5):b_data(val){cout<<"constructor in base\n";}
virtual void f() {cout<<"Base::f\n";}
int b_data; //普通成员函数
};
//Base1类
class Base1 :public Base{
public:
Base1(int val = 10):b1_data(val){ cout<<"constructor in base1\n";} //构造函数
//以下三个是测试的虚函数
virtual void f() { cout << "virtual Base1::f" << endl; }
virtual void g() { cout << "virtual Base1::g" << endl; }
virtual void h1() { cout << "virtual Base1::h1" << endl; }
void show(){cout<<"Base1:: show"<<endl;} //普通的成员函数
int b1_data; //数据成员
};
//Base2类
class Base2 :public Base{
public:
Base2(int val = 20):b2_data(val){ cout<<"constructor in base2\n";} //构造函数
//以下三个是测试的虚函数
virtual void f() { cout << "virtual Base2::f" << endl; }
virtual void g() { cout << "virtual Base2::g" << endl; }
virtual void h2() { cout << "virtual Base2::h2" << endl; }
void show(){cout<<"Base2:: show"<<endl;} //普通的成员函数
int b2_data; //数据成员
};
//main 函数
typedef void(*Fun)();
int main()
{
Base1 b(100),b1(1000);
Derive d(200);
cout<<"the size of d(Derive):"<<sizeof(d)<<endl;
cout<<"\nduplicate member test\n";
// cout<<"show d.b_data:"<<d.b_data<<endl; //会引起数据二义性
cout<<"show d.Base1::b_data:"<<d.Base1::b_data<<endl;
int** pVtab = (int**)&d;
Fun pFun = NULL;
cout<<"\nvirtual function test.....\n";
cout << "[0] Base1:: vptr->" << endl;
pFun = (Fun)pVtab[0][0];
cout << " [0] ";
pFun();
pFun = (Fun)pVtab[0][1];
cout << " [1] ";
pFun();
pFun = (Fun)pVtab[0][2];
cout << " [2] ";
pFun();
pFun = (Fun)pVtab[0][3];
cout << " [3] ";
pFun();
pFun = (Fun)pVtab[0][4];
cout << " [4] ";
cout<<pFun<<endl;
cout << "[1] Base1.b_data = " << (int)pVtab[1] << endl; //继承自Base
cout << "[2] Base1.b1_data = " << (int)pVtab[2] << endl;
int s = sizeof(Base1)/4;
cout << "\n[" << s << "] Base2::_vptr->"<<endl;
pFun = (Fun)pVtab[s][0];
cout << " [0] ";
pFun();
pFun = (Fun)pVtab[s][1];
cout << " [1] ";
pFun();
pFun = (Fun)pVtab[s][2];
cout << " [2] ";
pFun();
pFun = (Fun)pVtab[s][3];
cout << " [3] ";
cout<<pFun<<endl;
cout << "["<< s+1 <<"] Base2.b_data = " << (int)pVtab[s+1] << endl;
cout << "["<< s+2 <<"] Base2.b2_data = " << (int)pVtab[s+2] << endl;
cout << "["<< s+3 <<"] Derive::d_data = " << (int)pVtab[s+3] << endl;
return 0;
}
实验结果如下所示:
如果有了前面的基础,这个应该很好理解。 在此就不多说了。
同样的,最后上个图帮助理解。
以上总结的重复继承也叫做菱形继承问题,为了解决重复继承的子类数据成员问题,C++中又有了一个叫做虚继承的概念。 这里先简单介绍单一的虚继承,再总结虚继承解决上面的菱形继承问题。
1.10 单一虚继承
所谓虚继承在形式上,就是在普通的继承语句中加一个virtual 关键字。但是就是这个关键字,改变了子类的内存布局。
做个实验瞧瞧!
实验代码中新的继承关系如下图所示:
实验代码如下:
//Base类
class Base{
public:
Base(int val = 5):b_data(val){cout<<"constructor in base\n";}
virtual void f() {cout<<"Base::f\n";}
virtual void m(){cout<<"Base::m\n";}
int b_data; //普通成员函数
};
//Base1类
class Base1 : virtual public Base{
public:
Base1(int val = 10):b1_data(val){ cout<<"constructor in base1\n";} //构造函数
//以下三个是测试的虚函数
virtual void f() override{ cout << "virtual Base1::f" << endl; }
virtual void g() { cout << "virtual Base1::g" << endl; }
virtual void h1() { cout << "virtual Base1::h1" << endl; }
void show(){cout<<"Base1:: show"<<endl;} //普通的成员函数
int b1_data; //数据成员
};
//main函数
#include<iostream>
#include<cstdio>
using namespace std;
//#include "Derive.h"
#include "base1.h"
//main 函数
typedef void(*Fun)();
int main()
{
//cout<<"sizeof(int*) :"<<sizeof(int**)<<endl;
Base1 b(100),b1(1000);
cout<<"the size of b(Base1):"<<sizeof(b)<<endl;
int** pVtab = (int**)&b;
Fun pFun = NULL;
cout<<"\nvirtual function test.....\n";
cout << "\n[0] Base1:: vptr->" << endl;
pFun = (Fun)pVtab[0][0];
cout << " [0] ";
pFun();
pFun = (Fun)pVtab[0][1];
cout << " [1] ";
pFun();
pFun = (Fun)pVtab[0][2];
cout << " [2] ";
pFun();
pFun = (Fun)pVtab[0][3];
cout << " [3] ";
cout<<pFun<<endl;
//pFun();
cout << "[1] Base1.b1_data = " << *((int*)(&b) + 1) << endl;
// cout << "[2] = " << *((int*)(&b) + 2) << endl;
// cout << "[3] = " << *((int*)(&b) + 3) << endl;
pVtab = (int**)((int*)(&b) + 2);
cout << "\n[" << 2<< "] Base::_vptr->"<<endl;
pFun = (Fun)pVtab[0][0];
cout << " [0] "<<"Base1::virtual thunk to Base1::f()";
// pFun(); //此函数不能直接这样执行,否则会报错。 pFun此处指向的是一段利用thunk机制的代码
pFun = (Fun)pVtab[0][1];
cout << " [1] ";
pFun();
pFun = (Fun)pVtab[0][2];
cout << " [2] ";
cout<<pFun<<endl;
return 0;
}
实验结果如下图所示。
同样的,我们画个逻辑图,呈现一下子。
关于上图,要做个说明:
(1)、图中蓝色圈圈表明了Base1原生的部分放在布局的前面,继承的Base虚表指针和数据成员放在后面.( 这个和非虚继承都不一样),按照【1】中的说法,对于虚继承来说,其内存被一般被分为两个部分,一个不变区域(如蓝色圈中的Base1部分)和一个共享区域(如蓝色圈中的Base)。 不变区域中的数据不管后继如何演化,总是拥有固定的offset,所以这一部分的数据可以直接存取。 而共享区域的数据,其位置会随着每次的派生类操作而有变化,所以他们只能被间接存取。
(2)、红色圈中的逻辑上应该是Base类中被重写的f(), 但是在虚继承具体实现的过程中为了保证多态需要进行this指针的动态调整,所以引出了chunk机制。(这里面的内容又够写一篇博客了,同时对于这一部分还是稍微有点乱,下次有机会在好好总结,在发出来吧)
(3)、绿色圈圈指示虚表中虚函数的末尾都是0,这一点和前面的非虚继承也不一样,至于为啥要这样,我暂且也说不出来 。 ^ _ ^ |||
1.11 虚继承解决菱形继承问题
好了,还剩最后一种情况,这是上面情况的升级版,也是重复继承的改进版。
其基本的继承图如1.9 章节所示 。
再贴一下吧。
验证的代码也和1.9 节的差不多。只不过把Base1 和Base2 继承自Base的类改成虚继承。
同时main函数要稍微修改一下 。 下面只贴上main函数调用代码。
#include<iostream>
#include<cstdio>
using namespace std;
#include "Derive.h"
typedef void(*Fun)();
int main()
{
Base1 b(100),b1(1000);
Derive d(200);
cout<<"the size of d(Derive):"<<sizeof(d)<<endl;
cout<<"\nduplicate member test\n";
cout<<"show d.b_data:"<<d.b_data<<endl; //采用虚继承后不会引起数据二义性
//cout<<"show d.Base1::b_data:"<<d.Base1::b_data<<endl;
int** pVtab = (int**)&d;
Fun pFun = NULL;
cout<<"\nvirtual function test.....\n";
cout << "[0] Base1:: vptr->" << endl;
pFun = (Fun)pVtab[0][0];
cout << " [0] ";
pFun();
pFun = (Fun)pVtab[0][1];
cout << " [1] ";
pFun();
pFun = (Fun)pVtab[0][2];
cout << " [2] ";
pFun();
pFun = (Fun)pVtab[0][3];
cout << " [3] ";
pFun();
pFun = (Fun)pVtab[0][4];
cout << " [4] ";
cout<<pFun<<endl;
cout << "[1] Base1.b1_data = " << (int)pVtab[1] << endl; //继承自Base
cout << "\n[" << 2 << "] Base2::_vptr->"<<endl;
pFun = (Fun)pVtab[2][0];
cout << " [0] ";
pFun();
pFun = (Fun)pVtab[2][1];
cout << " [1] ";
pFun();
pFun = (Fun)pVtab[2][2];
cout << " [2] ";
pFun();
pFun = (Fun)pVtab[2][3];
cout << " [3] ";
cout<<pFun<<endl;
cout << "["<< 3 <<"] Base2.b2_data = " << (int)pVtab[3] << endl;
cout << "["<< 4 <<"] Derive.d_data = " << (int)pVtab[4] << endl;
cout << "\n[" << 5 << "] Base2::_vptr->"<<endl;
pFun = (Fun)pVtab[5][0];
cout << " [0] "<<"Derive::virtual thunk to Base::f()\n";
//pFun();
pFun = (Fun)pVtab[5][1];
cout << " [1] ";
cout<<pFun<<endl;
cout << "["<< 6 <<"] Base.b_data = " << (int)pVtab[6] << endl;
return 0;
}
贴一下最终的结果图。
清晰一点的图如下:
解释一波:
(1)、使用了虚继承的菱形继承,在内存布局中先排布直接子类的成员(注意这里不包括从间接子类中继承下来的成员),然后原生的成员。以上都是不变的部分,最后才是上面所说的共享的可变的部分,也就是从Base类中继承而来的。 如上图中蓝色大括号中所示.
(2)、Derive会从直接子类(Base1和Base2)中继承虚函数表,并按照普通继承那样修改重写的虚函数。如上图中红色圈出来的部分。同时新增的虚函数会增加在第一个继承的虚函数表中。如上图中绿色圈出来的部分。
(3)、菱形继承的“顶端尖尖”(Base类), Derive中只会留有一份,放在内存模型的最后,同时也会继承其虚函数表,如果重写了了虚函数表中的虚函数(如上图中的Base::f()),那么 这里就会使用chunk 机制来动态定位最终的虚函数。 (和1.10节中说的类似)
二、总结
原来以为一篇博客就可以把这此要总结的内容都总结完的,每想到一总结起来就有点“没完没了”的感觉,才发现还有很多内容把握的不透彻,了解的不深入,边查边学、边学编想、边想边总结,零零散散花了一周多时间,才将将把内存模型的一部分总结完。 这还是要总结的一部分,还有总结过程中延伸的部分,如chunk机制的原理、虚继承多态的具体实现原理(上面顶多只是说了是什么,但是还没有研究为什么是这样?)等,发现自己搞的还不是很透彻,这个就留在后面有时间在研究总结吧。
最后对整个实验的做下说明:我在整个实验的过程中采用的是gcc -4.9.2 版本的编译器,可能有些结果和其他的编译器有些不同,这里要稍微注意一下。
参考
【1】、《深度探索C++对象模型》
【2】、C++ Data Member内存布局
【3】、C++ 对象的内存布局(上)
【4】、C++ 虚函数表解析
【5】、C++ 对象的内存布局(下)
【6】、C++幕后故事(四)-- 虚函数的魅力
【7】、图解C++虚函数
【8】、(好)C++ 多继承和虚继承的内存布局
【9】、C++中虚函数、虚继承内存模型