C++ 虚函数表,c++虚函数表原理
陈皓
http://blog.csdn.net/haoel
C中虚函数的作用主要是实现多态的机制。关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以使父类的指针具有“多种形式”,是一种通用技术。所谓泛型技术,说白了就是试图用不可改变的代码来实现可改变的算法。比如:模板技术,RTTI技术,虚函数技术,要么尝试在编译时做一个解析,要么尝试在运行时做一个解析。
关于虚函数的使用,这里不做过多阐述。可以看看相关的C书籍。在这篇文章中,我只想从虚函数的实现机制给大家一个清晰的分析。
当然,网上也出现过一些相同的文章,但我总觉得这些文章不好读。有大段大段的代码,没有图片,没有详细的解释,没有对比,没有推论。不利于学习和阅读,所以这是我想写这篇文章的原因。也希望大家多提意见。
言归正传,我们一起进入虚函数的世界吧。
懂C的人应该知道虚函数是通过一个虚表实现的。简称v表。在这个表中,需要一个类的虚函数的地址表。该表解决了继承性和覆盖性问题,保证了能真实反映实际功能。这样,在具有虚函数的类的实例中,这个表被分配在这个实例的内存中。所以当我们用父类的指针去操作子类的时候,这个虚函数表就变得重要了。像地图一样,它指出了实际应该调用的函数。
让我们来关注一下这个虚函数表。c编译器要保证虚函数表的指针存在于对象实例的前端位置(这是为了保证多重继承或者多重继承的情况下获取虚函数表的最高性能)。这意味着我们通过对象实例的地址得到这个虚函数表,然后就可以遍历其中的函数指针,调用相应的函数。
听了这么多,我能感觉到你现在可能比以前更迷茫了。没关系,下面是实际例子。相信聪明的你一眼就明白了。
假设我们有这样一个类:
类别库{
公共:
virtualvoidf(){ cout Base:f endl;}
virtualvoidg(){ cout Base:g endl;}
virtualvoidh(){ cout Base:h endl;}
};
根据上面的陈述,我们可以通过Base的实例得到虚函数表。下面是实际的例程:
typedefvoid(* Fun)(void);
基数b;
Fun pFun=NULL
Cout 虚函数表地址:“(int *)(b)endl;
Cout 虚函数表—第一个函数地址:“(int *)*(int *)(b)endl;
//调用第一个虚函数
pFun=(Fun)*((int *)*(int *)(b));
pFun();
实际运行结果如下:(Windows XP VS 2003,Linux 2.6.22 GCC 4.1.3)
虚拟表地址:0012FED4
虚函数表—第一个函数地址:0044F148
Base:f
通过这个例子我们可以看到,通过强制将B转换为int*可以得到虚函数表的地址,然后,通过再次寻址可以得到第一个虚函数的地址,即Base:f(),这个在上面的程序中已经得到了验证(强制将int *转换为函数指针)。通过这个例子我们可以知道,如果要调用Base:g()和Base:h(),其代码如下:
(Fun)*((int *)*(int *)(b)0);//Base:f()
(Fun)*((int *)*(int *)(b)1);//Base:g()
(Fun)*((int *)*(int *)(b)2);//Base:h()
这时候你应该明白了。什么?还是有点晕。是的,这段代码看起来太乱了。没问题,我来画张图解释一下。如下所示:
注意:上图中,我在虚函数表的末尾增加了一个额外的节点,是虚函数表的结束节点,就像字符串的终止符“/0”一样,标志着虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP VS2003下,该值为NULL。在Ubuntu
7.10 Linux 2.6.22 GCC 4.1.3,如果该值为1,则表示有下一个虚函数表,如果该值为0,则表示是最后一个虚函数表。
接下来,我将分别解释虚函数表在“未覆盖”和“覆盖”时的外观。拥有一个不覆盖父类的虚函数是没有意义的。我之所以要讲没有覆盖的情况,主要是为了做一个对比。通过对比,我们可以更清楚的知道它的内部实现。
一般继承(没有虚函数覆盖)
接下来,我们来看看虚函数表在继承时是什么样子的。假设存在如下所示的继承关系:
请注意,在这个继承关系中,子类不会重载父类的任何函数。那么,在派生类的例子中,它的虚函数表如下:
比如:derivedd的虚函数表如下:
我们可以看到以下几点:
1)虚函数按照它们的声明顺序放在表中。
2)父类的虚函数先于子类的虚函数。
我相信聪明的你可以参考前面的程序写个程序验证一下。
一般继承(由虚函数覆盖)
覆盖父类的虚函数是显而易见的,否则虚函数就变得没有意义。现在,让我们来看看。如果子类中的虚函数重载了父类的虚函数,会是什么样子?假设我们有如下继承关系。
为了让大家看到继承的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。然后,对于派生类的实例,其虚函数表将如下所示:
从表中我们可以看出以下几点,
1)被覆盖的f()函数被放置在虚拟表中原始父虚函数的位置。
2)未涵盖的功能保持不变。
这样,我们可以看到,对于下面的程序,
base * b=new derive();
b-
b所指示的f()在内存虚函数表中的位置已经被derivative: f()函数的地址所取代,所以实际调用时,被调用的是derivative: f()。这实现了多态性。
多重继承(没有虚函数覆盖)
接下来,我们来看看多重继承中的情况,假设有下面这个类的继承关系。注意:子类不会覆盖父类的功能。
对于子类实例中的虚函数表,它看起来像这样:
我们可以看到:
1)每个父类都有自己的虚拟表。
2)子类的成员函数放在第一个父类的表中。(所谓第一个父类是根据声明顺序判断的)
这是为了解决不同父类型的指针指向同一个子类实例的问题,可以调用实际的函数。
多重继承(由虚函数覆盖)
我们再来看看,如果虚函数覆盖发生了。
在下图中,我们在子类中覆盖了父类的f()函数。
以下是子类实例中虚函数表的示意图:
我们可以看到f()在三个父类的虚函数表中的位置被子类的函数指针所取代。这样,我们可以指向任何静态类型的子类,并调用子类的f()。比如:
推导d;
Base1 *b1=
Base2 *b2=
Base3 *b3=
b1- //Derive:f()
b2- //Derive:f()
b3- //Derive:f()
b1- //Base1:g()
b2- //Base2:g()
b3- //Base3:g()
每次写关于C的文章,总要批评C,这篇文章也不例外。通过上面的描述,相信大家对虚函数表有了更详细的了解。水能载舟,亦能覆舟。现在,让我们看看虚函数表能做什么坏事。
1.通过父类型的指针访问子类的虚函数。
我们知道,子类不重载父类的虚函数是一件没有意义的事情。因为多态性也是基于函数重载的。虽然我们在上图中可以看到Base1的虚表中有一个Derive的虚函数,但是我们不可能用下面的语句调用子类的自有虚函数:
base 1 * B1=new derive();
B1-f1();//编译错误
任何试图用父类的指针调用子类中不覆盖父类的成员函数的行为,都会被编译器视为非法,这样的程序根本无法编译。但是,在运行时,我们可以通过指针访问虚函数表来违反C语义。(对于这个尝试,我相信你可以通过阅读后面附录中的代码来做到)
第二,访问非公共的虚拟功能
另外,如果父类的虚函数是私有的或者是受保护的,但是这些非公有的虚函数也会存在于虚函数表中,那么我们也可以通过访问虚函数表来访问这些非公有的虚函数,这很容易做到。
比如:
类别库{
私人:
virtualvoidf(){ cout Base:f endl;}
};
classDerive :publicBase{
};
typedefvoid(* Fun)(void);
voidmain() {
推导d;
funp Fun=(Fun)*((int *)*(int *)(d)0);
pFun();
}
c语言是一种神奇的语言。对于程序员来说,我们似乎永远不知道这种语言在背后做什么。要熟悉这门语言,我们需要知道C里面的东西,知道C里面的危险的东西,否则就是搬起石头砸自己的脚的编程语言。
在文章捆绑之前先自我介绍一下。我从事软件研发十年了。目前我是软件开发的技术总监,技术方面主要是Unix/C/C。我更喜欢网络上的技术,比如分布式计算、网格计算、P2P、Ajax以及一切与互联网相关的东西。管理层擅长团队建设、技术趋势分析、项目管理。欢迎和我交流。我的MSN和邮箱是:haoel@hotmail.com。
附录1:在VC中查看虚函数表
我们可以在VC的IDE环境下展开调试状态下的类的实例,可以看到虚函数表(不是很全)
附录二:常规
下面是一个关于多重继承的虚函数表访问的例程:
#包括iostream
usingnamespacestd
classBase1 {
公共:
virtualvoidf(){ cout base 1:f endl;}
virtualvoidg(){ cout base 1:g endl;}
virtualvoidh(){ cout base 1:h endl;}
};
classBase2 {
公共:
virtualvoidf(){ cout base 2:f endl;}
virtualvoidg(){ cout base 2:g endl;}
virtualvoidh(){ cout base 2:h endl;}
};
classBase3 {
公共:
virtualvoidf(){ cout base 3:f endl;}
virtualvoidg(){ cout base 3:g endl;}
virtualvoidh(){ cout base 3:h endl;}
};
classDerive :publicBase1,publicBase2,publicBase3 {
公共:
virtualvoidf(){ cout Derive:f endl;}
virtualvoidg 1(){ cout Derive:G1 endl;}
};
//Base1的vtable
//pFun=(Fun)*((int *)*(int *)((int *)d 0)0);
pFun=(Fun)pVtab[0][0];
pFun();
//pFun=(Fun)*((int *)*(int *)((int *)d 0)1);
pFun=(Fun)pVtab[0][1];
pFun();
//pFun=(Fun)*((int *)*(int *)((int *)d 0)2);
pFun=(Fun)pVtab[0][2];
pFun();
//派生的vtable
//pFun=(Fun)*((int *)*(int *)((int *)d 0)3);
pFun=(Fun)pVtab[0][3];
pFun();
//虚拟表的尾部
pFun=(Fun)pVtab[0][4];
cout pFun endl
//Base2的vtable
//pFun=(Fun)*((int *)*(int *)((int *)d 1)0);
pFun=(Fun)pVtab[1][0];
pFun();
//pFun=(Fun)*((int *)*(int *)((int *)d 1)1);
pFun=(Fun)pVtab[1][1];
pFun();
pFun=(Fun)pVtab[1][2];
pFun();
//虚拟表的尾部
pFun=(Fun)pVtab[1][3];
cout pFun endl
//Base3的虚拟表
//pFun=(Fun)*((int *)*(int *)((int *)d 1)0);
pFun=(Fun)pVtab[2][0];
pFun();
//pFun=(Fun)*((int *)*(int *)((int *)d 1)1);
pFun=(Fun)pVtab[2][1];
pFun();
pFun=(Fun)pVtab[2][2];
pFun();
//虚拟表的尾部
pFun=(Fun)pVtab[2][3];
cout pFun endl
return0
}
(转载请注明作者和出处。未经允许不得用于商业目的)
更多文章请访问我的博客:http://blog.csdn.net/haoel
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。