一篇写的比较好的博客 http://blog.csdn.net/haoel/article/details/3081328
这篇文章中主要想说以下几个问题
1 如何通过对象获得虚函数表中虚函数的地址
2 分几种情况讨论内存布局
1》单一继承
2》多重继承
3》重复继承
4》钻石虚拟继承(为了解决重复继承中出现问题而产生的虚拟继承)
1
虚函数主要是通过一张虚函数的地址表来实现的,简称V-Table,在有虚函数的实例中这个表被分配在这个实例的内存中,
当我们用一个父类指针来操作一个子类的时候,这张虚函数表就显的非常重要,告诉我们应该调用哪个函数。当然这么重要的表
的初始化是在对象的构造函数中,这样才能反映对象真实的意愿,指针才能调用到正确的函数。
c++编译器保证虚函数表指针存在于对象实例中最前面的位置。其他的编译器也有不同的安排
这样我们可以通过对象来获取虚函数表的地址,然后便利其中的函数指针。
class Base {
public:
int a;
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
void main()
{
Base b;
cout << "&b:" << &b << endl;
cout << "&b+1:" << &b+1 << endl;
cout << "(int*)(&b):" << (int*)(&b) << endl;
cout << "(int*)(&b)+1:" << (int*)(&b)+1 << endl;
cout <<"*(int*)(&b)"<<*(int*)(&b)<<endl;
cout <<"*(int*)(&b)+1"<<*(int*)(&b)+1<<endl;
cout << "(int*)*(int*)(&b)" << (int*)*(int*)(&b) << endl;
cout << "(int*)*(int*)(&b)+1" << (int*)*(int*)(&b)+1 << endl;
结果
&b对象地址
(int*)&b虚指针的地址(存放虚函数表的地址的指针)
*(int*)&b虚函数表的地址中存放的内容,及虚函数表的地址
(int*)*(int*)&b 第一个虚函数的地址
![](https://img-my.csdn.net/uploads/201206/13/1339577657_1849.png)
1 单一继承
程序就不写了,可以看那篇博客的
继承关系描述:
GrandChild 的内存布局:
结论:
1 虚函数在最前面
2 成员变量的根据其继承和声明顺序依次放在后面
3 在单一继承中,被overwrite的虚函数在虚函数表中得到了更新。函数的顺序按照继承声明的顺序依次存放。
2 多重继承
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。
3) 内存布局中,其父类布局依次按声明顺序排列。
4 ) 每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。3 重复继承
所谓重复继承,也就是某个基类被间接地重复继承了多次。
![](https://img-my.csdn.net/uploads/201206/13/1339578691_3282.png)
我们可以看见,最顶端的父类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++引入了虚基类的概念。
4 钻石型虚拟继承
图说明:
总结:
1 把B这个超类放在了最后,VC++有一个NULL分隔符把B和B1和B2的布局分开,图中忘记了画
2 顺序先是B1 ,然后是B2 ,接着是D ,而B这个超类的实例都放在最后的位置。
3 对于不同的编译器,函数处理的方式可能不同,有的将D::f这个函数,B,B1,B2这三个vptr中都有,而vc++的只将它放在
B的vptr中。编译器可以计算偏移量得出想要的结果