首页 > 代码库 > 解析虚函数表和虚继承
解析虚函数表和虚继承
之前大二在学C++的时候一直对虚函数和虚继承有些晕(其实好像就是对virtual这个关键字不太熟悉)现在又学习到了一些,对虚函数表和虚继承的机制有了一点更深入的了解。
关于虚函数以及虚继承的基础知识,我自己也总结了一下,点击浅谈C++多态和C++继承可查看,在继承的总结的时候,我没有总结关于虚继承的知识,而且在多态总结也没有设计到太多的虚函数的知识,我是想把这两块集中在一起讲下,也算是自己对virtual关键字有个比较深入的了解吧。(本文所有代码均在VS2013编译器win32下测试)
另外对于虚函数表尤其是后面的菱形继承等参考了陈皓老师的C++ 对象的内存布局。
虚继承
在谈虚继承前,我们先看这样一段代码:
class B
{
public:
int _b;
};
class C1 : public B
{
public:
int _c1;
};
class C2 : public B
{
public:
int _c2;
};
class D :public C1, public C2
{
public:
int _d;
};
int main()
{
D d;
//d._b = 10;错误,访问不明确
d.C1::_b = 10;//正确
d.C2::_b = 10;//正确
return 0;
}
为什么会出现这样的问题?
我们知道它们的继承层次如下图所示:
这种看似菱形的多继承会带来二义性:也就是说D中_b到底是从C1这条路继承而来的还是从C2这条路继承而来的?C++中为了避免这种访问不明确,从而引入了虚拟继承的机制。
虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如上述类D继承自类C1、C2,而类C1、C2都继承自类B,因此在类D中两次出现类B中的变量。为了节省内存空间,可以将C1、C2对B的继承定义为虚拟继承,而B就成了虚拟基类。实现代码如下:
class B
{
public:
int _b;
};
class C1 :virtual public B
{
public:
int _c1;
};
class C2 :virtual public B
{
public:
int _c2;
};
class D :public C1, public C2
{
public:
int _d;
};
int main()
{
D d;
d._d = 4;
return 0;
}
这样就可以达到我们的要求了,直接使用d._d访问到_d。然而虚继承到底是一种怎么样的实现机制?我们不妨在加不加virtual这两中情况下看下在内存中D d这个对象模型是怎么样的?
对于普通继承,我们通过VS2013的内存窗口可以看到:
先是C1类中的成员,再是C2类中的成员,最后是D类自己的成员,此时sizeof(D) = 20。而一旦加了虚继承了,变化就比较明显了,如下图:
最后再看几道有关的虚继承的题目:
对这四种情况分别求sizeof(a), sizeof(b)。结果是什么样的呢?我在VS2013的win32平台测试结果为:
第一种:4,12
第二种:4,4
第三种:8,16
第四种:8,8
这是为什么???首先我们看a类,我们知道每个存在虚函数的类都要有一个4字节的指针指向自己的虚函数表,再加上如果有数据,根据内存对齐机制,四种情况的类a所占的字节数应该是没有什么问题的。我们再看sizeof(B),我们先看普通继承,对于普通继承仅仅是在原来的基础对虚表指针指向的虚函数表进行改写,类B依旧只有一个虚表指针,再加上如果有数据,根据内存对齐机制,所以第二种和第四种情况下,sizeof(B)分别为4和8。然而对于虚拟继承,会增加了一个偏移指针,而且由于类B中新增了虚函数,所以它的一般对象模型为这样(具体为什么是这样本文菱形虚拟继承会讲):
根据图示,在第一种的情况下,由于没有对应的数据成员,所以大小为12个字节。在第三种情况下,子类有自己的数据成员,而基类没有,所以删去最后一项,大小就是16个字节了。这样子对于虚拟继承应该就没问题了吧。
虚函数
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。(参考文章)
通过以上这段话,我们知道动态多态(什么是动态多态,静态多态以及他们的差别?)主要是通过虚函数实现,而虚函数(Virtual Function)则是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。我们通过一些代码块来了解这个概念:
typedef void(*pFun)(void);//函数指针
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
void FunTest()
{
Base b;
cout << "虚函数表地址:" << (int*)(&b) << endl;
pFun* fun = (pFun*)*(int*)&b;
while (*fun) {
(*fun)();
fun++;
}
}
int main()
{
FunTest();
getchar();
return 0;
}
在VS2013编译器win32的测试结果为:
我们来看看虚函数表的地址0x00DF820里面存了什么?
通过上图我们知道,通过Base类实例化的对象b里面(有3个虚函数)有一个指向虚函数表的指针,也就是我们上面的0x00DF820,而在这个虚函数表中,分别存了3个虚函数的地址,我们通过函数指针fun可以访问到这些函数,因此就得到我们的输出结果了。通过sizeof(Base)=4也说明此时b对象仅仅存有一个指针,指向虚函数表。
所以就得到了我们的对象模型:
注意:在上面这个图中,虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“\0”一样,其标志了虚函数表的结束,也就是我们这里虚函数表的最后地址存的全是0。注意这个结束标志的值在不同的编译器下是不同的。
补充一点
如果基类定义了虚同名函数,那么派生类中的同名函数自动变成了虚函数,比如以下代码:
class C {
public:
virtual string toString()
{
return "class C";
}
};
class B : public C {
public:
/*virtual*/ string toString()
{
return "class B";
}
};
class A : public B {
public:
/*virtual*/ string toString()
{
return "class A";
}
};
有了这些知识我们再来看看虚函数的继承体系是怎么样的:
一般继承(无虚函数覆盖)
注意到:
1. 虚函数按照其声明顺序放于表中。
2. 父类的虚函数在子类的虚函数前面。
一般继承(有虚函数覆盖)
注意到:
1. 覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2. 没有被覆盖的函数依旧。
因此对于程序:
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
注意到:
1. 对于实例Derived d的对象,每个父类都有存有一个指针,指向对应的虚函数表。
2. 子类的成员函数被放到了第一个父类的表中。(第一个父类是按照声明顺序来判断的)
多重继承(有虚函数覆盖)
我们可以写一段代码对上图进行测试:
typedef void(*pFun)(void);
class Base1 {
public:
virtual void f() { cout << "Base1::f" << endl; }
virtual void g() { cout << "Base1::g" << endl; }
virtual void h() { cout << "Base1::h" << endl; }
};
class Base2 {
public:
virtual void f() { cout << "Base2::f" << endl; }
virtual void g() { cout << "Base2::g" << endl; }
virtual void h() { cout << "Base2::h" << endl; }
};
class Base3 {
public:
virtual void f() { cout << "Base3::f" << endl; }
virtual void g() { cout << "Base3::g" << endl; }
virtual void h() { cout << "Base3::h" << endl; }
};
class Derived : public Base1, public Base2, public Base3 {
public:
virtual void f() { cout << "Derived::f" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
};
void FunTest()
{
Derived d;
cout << sizeof(Derived) << endl;
//访问Base1虚函数表
pFun* fun = (pFun*)*((int*)&d + 0);
while (*fun) {
(*fun)();
fun++;
}
cout << endl;
//访问Base2虚函数表
fun = (pFun*)*((int*)&d + 1);
while (*fun) {
(*fun)();
fun++;
}
cout << endl;
//访问Base3虚函数表
fun = (pFun*)*((int*)&d + 2);
while (*fun) {
(*fun)();
fun++;
}
}
int main()
{
FunTest();
return 0;
}
最后显示结果为:
虚函数表总结
- Base虚表:Base类如果有虚函数的话,就按照虚函数出现的先后次序来填写续表
- Derived虚表:对于继承Base类的对象,首先按照Base类的虚表格式复制,如果有重写(覆盖)基类的虚函数,则在对应的位置修改,不改变次序。如果派生类中新增虚函数,则将这虚函数填写到第一个父类虚函数后面即可。
根据以上知识,再理解下面的对象模型就不难了:
单一的一般继承
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; }
};
对于Grandchildren gc这个对象,它的内存模型如下:
多重继承
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; }
};
对于Derive d这个对象,它的内存模型如下:
重复继承
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;}
};
对于D d这个对象,它的内存模型如下图:
菱形虚拟继承
在上面继承体系下,会出现这样的情况:
D d;
d.ib = 0; //二义性错误
d.B1::ib = 1; //正确
d.B2::ib = 2; //正确
为了避免这种不明确,C++引入了虚基类的概念。这也就是我们文章一开头讲的加virtual关键字的解决方法。
class B {……};
class B1 : virtual public B{……};
class B2: virtual public B{……};
class D : public B1, public B2{ …… };
在看菱形虚拟继承之前,我们先看一下简单的虚拟单继承是怎么样的,这样便于我们理解复杂一点的菱形虚拟继承,我们先看一组代码:
class A {
public:
int _a;
virtual void fun1() {}
};
class B : public virtual A {
public:
int _b;
//virtual void fun1() {}
//virtual void fun2() {}
};
int main()
{
B b;
b._a = 2;
b._b = 1;
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
getchar();
return 0;
}
在VS2013的测试结果为8和16,我们试着去掉//virtual void fun1() {}的注释,也就是
class B : public virtual A {
public:
int _b;
virtual void fun1() {}
//virtual void fun2() {}
};
此时测试结果仍为8和16,但是当我们去掉//virtual void fun2() {}的注释,也就是
class B : public virtual A {
public:
int _b;
virtual void fun1() {}
virtual void fun2() {}
};
测试结果为sizeof(A) = 8,sizeof(B) = 20。这是为什么???为了解决这个问题,我们有必要看看在这几种情况下的B对象模型,A类对象模型比较简单,我们知道虚函数必有一个指向虚表的指针,再加上A类对象本身有个int型数据加起来就是8。而对于B对象模型,我们可以简单分几种情况:
子类有覆盖(重写)且没有新增虚函数 and 子类没有覆盖(重写)且没有新增虚函数:这两种情况并没有太大差别,对于B对象模型都是下面这种:
唯一的区别就是基类A的虚表指针指向的虚表有没有被重写而已,因此在第一种和第二种情况下,sizeof(B) = 16。而对于有新增虚函数这种情况,对于B的对象模型则是这样的:
因为有重写基类的虚函数了,所以子类需要额外加一个虚表指针,这样sizeof(B) =20就不难理解了。有了这些知识,我们再看菱形虚拟继承就容易多了,首先对于菱形虚拟继承,它的继承层次图大概像下面这个样子:
为了便于分析,我们可以把这个图拆解下来,也就是说从B到B1,B2是两个单一的虚拟继承,而从B1,B2到则是多继承,这样一来,问题就变得简单多了。对于B到B1,B2两个单一的虚拟继承,根据前面讲的很容易得到B1,B2的对象模型:
接下来就是多继承,这样终于得到了我们D d的对象模型了:
解析虚函数表和虚继承