类中无论有多少类的虚函数数,只会多占用一个虚表指针空间,对吗

虚函数表与虚表指针
普通函数的处理:一个特定的函数都会映射到特定的代码,无论时编译阶段还是连接阶段,编译器都能计算出这个函数的地址,调用即可。&虚函数的处理:被调用的函数不仅依据调用的特定函数,还依据调用的对象的种类。通常是由虚函数表(vtable)来实现的。
&& 虚函数表的结构:它是一个函数指针表,每一个表项都指向一个函数。任何一个包含至少一个虚函数的类都会有这样一张表。需要注意的是vtable只包含虚函数的指针,没有函数体。实现上是一个函数指针的数组。虚函数表既有继承性又有多态性。每个派生类的vtable继承了它各个基类的vtable,如果基类vtable中包含某一项,则其派生类的vtable中也将包含同样的一项,但是两项的值可能不同。如果派生类重载(override)了该项对应的虚函数,则派生类vtable的该项指向重载后的虚函数,没有重载的话,则沿用基类的值。&&&&每一个类只有唯一的一个vtable,不是每个对象都有一个vtable,恰恰是每个同一个类的对象都有一个虚函数指针,这个指针指向该类的vtable(当然,前提是这个类包含虚函数)。那么,每个对象只额外增加了一个指针的大小,一般说来是4字节。
&&&&&& 在类对象的内存布局中,首先是该类的vtable指针,然后才是对象数据。
请各位遵纪守法并注意语言文明下次自动登录
现在的位置:
& 综合 & 正文
虚函数,看看吧
说一个人后入为主,通常是说耳根子软,人家说什么就把原先的忘掉了,别人说向东走,他就向东走,一会儿有人说向西了,他立马赶回来。不过好处显而易见,这个人特别听话,如果你有时候不知道自己要干什么,或者不确定一会儿要干什么,那么就叫他在旁边等着,到你拿定主意的时候,再告诉他你的要求,让他照办。现实中这样的事情并不少见,秘书需要作记录、写报告、安排行程、联络客户等等,而这些工作都是在你的指示下去做的。如果编程的时候也有这么听话的代码就好了,当然这不是做梦,这完全可以实现。
求解方程时有一个未知数的概念,用一个可以为任意数的符号代表一个暂时不知道值的数,然后通过运算规则得到符合要求的值,用编程的角度来看,就是加上了一个间接层,我们暂时不处理数值,而是处理一个与数值有关的符号,如果变化的是数值也就是对应的属性,我们可以用成员变量来处理;如果变化的是运算规则呢?y=f(x),这个f(x)是可变的,要让我们的解决方案通用于各种可能的f(x),那就要提供一个间接层来连接不同的函数,在C++中用函数指针就可以办到。
指针的作用就是可以保存某种类型的对象在内存中的位置,改变指针的值就可以让他指向不同对象。(指针本身也是一个类型)
编译器是如何实现对虚函数在运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?这里把“标准的”方式简单介绍一下。
“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其建立一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个位置。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例(对象)的时候,编译器还会在每个实例的内存中增加一个vptr字段(虚函数表指针),该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写,如下面的例子:
class A{public:
virtual void foo() { cout && "A::foo() is called" &&}};
class B: public A{public:
virtual void foo() { cout && "B::foo() is called" &&}};
那么,在使用的时候,我们可以:
A * a = new B();a-&foo();
// 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
void bar(A * a){
// 被调用的是A::foo() 还是B::foo()?}因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无法确定这里被调用的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则B::foo()被调用。
编译时bar这个函数会被改写为:
void bar(A * a){
(a-&vptr[1])();}
因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。
虽然实际情况远非这么简单,但是基本原理大致如此。如类X的对象模型如下图所示:
  虚函数的多态性只能通过对象指针或对象的引用调用来实现,如下的调用:  
X* ptr = & X& ref =
ptr-&VirtualFunc();
ref.VirtualFunc();
  将被C++编译器转换为如下的形式。  
( *ptr-&vptr[2] )(ptr);
( *ptr-&vptr[2] )(&ref);
  其中的2表示VirtualFunc在类的虚函数表的第2个槽位。因此,在C++中用函数指针实现虚函数,编译器在类中隐藏了一个函数指针数组(因为成员函数可能有很多,叫做虚函数表)。显然这里增加了一个寻找函数位置的步骤,所以,使用虚函数将降低执行效率,需要在效率和灵活性之间的取舍。虚函数的调用相当于一个C的函数指针调用,其效率也并未降低多少。
我们用C++来看一下虚函数的效果
#include &iostream&
#include &stdlib.h&
class NormalA{
void DoSomething( void ){ cout && "This is class NormalA." && }
class NormalB : public NormalA{
void DoSomething( void ){ cout && "This is class NormalB." && }
class NormalC : public NormalB{
void DoSomething( void ){ cout && "This is class NormalC." && }
class VirtualA{
virtual void DoSomething( void ){ cout && "This is class VirtualA." && }
class VirtualB : public VirtualA{
void DoSomething( void ){ cout && "This is class VirtualB." && }
class VirtualC : public VirtualB{
void DoSomething( void ){ cout && "This is class VirtualC." && }
int main()
VirtualA *
na.DoSomething();
nb.DoSomething();
nc.DoSomething();
va.DoSomething();
vb.DoSomething();
vc.DoSomething();
pna-&DoSomething();
pna-&DoSomething();
pna-&DoSomething();
pva-&DoSomething();
pva-&DoSomething();
pva-&DoSomething();
cin.get();
This is class NormalA. //调用的是NormalA::DoSomething();
This is class NormalB. //调用的是NormalB::DoSomething();
This is class NormalC. //调用的是NormalC::DoSomething();
This is class VirtualA. //调用的是VirtualA::DoSomething();
This is class VirtualB. //调用的是VirtualB::DoSomething();
This is class VirtualC. //调用的是VirtualC::DoSomething();
This is class NormalA. //调用的是NormalA::DoSomething(); 没有虚函数表,NormalA *指定了类型
This is class NormalA. //调用的是NormalA::DoSomething(); 没有虚函数表,NormalA *指定了类型
This is class NormalA. //调用的是NormalA::DoSomething(); 没有虚函数表,NormalA *指定了类型
This is class VirtualA. //调用的是VirtualA::DoSomething(); 虚函数表指针指向的是VirtualA
This is class VirtualB. //调用的是VirtualB::DoSomething(); 虚函数表指针指向的是VirtualB
This is class VirtualC. //调用的是VirtualC::DoSomething(); 虚函数表指针指向的是VirtualC
&&&&推荐文章:
【上篇】【下篇】虚函数表指针,虚函数表详解
对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。 在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了 这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
听我扯了那么多,我可以感觉出来你现在可能比以前更加晕头转向了。 没关系,下面就是实际的例子,相信聪明的你一看就明白了。
假设我们有这样的一个类:
class Base {
virtual void f() { cout && &Base::f& && }
virtual void g() { cout && &Base::g& && }
virtual void h() { cout && &Base::h& && }
按照上面的说法,我们可以通过Base的实例来得到虚函数表。 下面是实际例程:
typedef void(*Fun)(void);
Fun pFun = NULL;
cout && &虚函数表地址:& && (int*)(&b) &&
cout && &虚函数表 — 第一个函数地址:& && (int*)*(int*)(&b) &&
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&b));
实际运行经果如下:(Windows XP+VS2003, Linux 2.6.22 + GCC 4.1.3)
虚函数表地址:0012FED4
虚函数表 — 第一个函数地址:
通过这个示例,我们可以看到,我们可以通过强行把&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,表示是最后一个虚函数表。
下面,我将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。
一般继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
对于实例:D 的虚函数表如下:
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证。
一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
以上内容来自:
1:虚表(虚函数表)是for类的
2:基类和派生类是各有各的表,也就是说他们的物理地址是分开的,基类和派生类的虚表的唯一关联是:当派生类没有实现基类虚函数的重载时,派生类会直接把自己表的该函数地址值写为基类的该函数地址值.
3:任何一个有虚表的类,在实例化时不允许其虚表内有项为空-&纯虚类不能初始化对象
4:带虚表的类在对象构造函数中,会把一个指针指向该类虚表地址,我在这给它起个名字叫
5:仅对于VC和BC两种编译器论,如果该类带有虚表,那么该类的对象的首地址就是虚表地址,也是this指针指向虚表
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1-&f(); //Derive::f()
b2-&f(); //Derive::f()
b3-&f(); //Derive::f()
b1-&g(); //Base1::g()
b2-&g(); //Base2::g()
b3-&g(); //Base3::g()
每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。
一、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:
Base1 *b1 = new Derive();
b1-&f1(); //编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,(但可以通过将b1强制转换成Derive*之后可以访问Derive类中的所有成员函数)所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)
二、访问non-public的虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
class Base {
virtual void f() { cout && &Base::f& && }
class Derive : public Base{
typedef void(*Fun)(void);
void main() {
Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
以下内容来自:
在进入主题之前先介绍一下联编的概念。联编就是将模块或者函数合并在一起生成可 执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址。按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。在编译阶段就将函数实现和函数调用关联起来称之为静态联编,静态联编在编译阶段就必须了解所有的函数或模块执行所需要检测的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型。反之在程序执行的时候才进行这种关联称之为动态联编,动态联编对成员函数的选择不是基于指针或者引用,而是基于对象类型,不同的对象类型将做出不同的编译结果。C语言中,所有的联编都是静态联编。C++中一般情况下联编也是静态联编,但是一旦涉及到多态性和虚函数就必须使用动态联编。&
多态性是面向对象的核心,它的最主要的思想就是可以采用多种形式的能力,通过一个用户名字或者用户接口完成不同的实现。通常多态性被简单的描述为&一个接口,多个实现。在C++里面具体的表现为通过基类指针访问派生类的函数和方法。&
下面我们看一个静态联编的例子,这种静态联编导致了我们不希望的结果。
1. #include &iostream.h&
2. class shape{
void draw(){cout&&&I am shape&&&}
void fun(){draw();}
7. class circle:public shape{
void draw(){cout&&&I am circle&&&}
11. main(){
oneshape.fun();
程序的输出结果我们希望是&I am circle&,但事实上却输出了&I am shape&的结果,造成这个结果的原因是静态联编。静态联编需要在编译时候就确定函数的实现,但事实上编译器在仅仅知道shape的地址时候无法获取正确的调用函数,它所知道的仅是shape::draw(),最终结果只能是draw操作束缚到shape类上。产生&I am shape&的结果就不足为奇了。
为了能够引起动态联编,我们只需要将需要动态联编的函数声明为虚函数即可。动态联编只对虚函数起作用。我们在通过基类而且只有通过基类访问派生类的时候,只要这个基类中直接的或者间接(从上上层继承)的包含虚函数,动态联编将自动唤醒。下面我们将上面的程序稍微改一下。
1. #include &iostream.h&
2. class shape{
virtual void draw(){cout&&&I am shape&&&}
void fun(){draw();}
7. class circle:public shape{
void draw(){cout&&&I am circle&&&}
11. main(){
fun(&oneshape);
程序执行得到了正确的结果&I am circle&。代码在VC6.0中执行。&
到目前为止我们不清楚动态联编的执行机制,但我们可以做个猜测。正如上面所说,对于函数的实际的对象类型不同,联编结果也应该不同。在静态联编中,执行的困难在于无法通过基类知道需要联编的子对象的确切类型。在1.cpp中shape的派生类既可能是circle,也可能是其余的rectangle或者square等等,到底应该静态联编哪一个呢。迷惑正在于此。动态联编在编译的时候应该也是不知道联编的确切对象类型的,(如果知道的话就成了静态联编了),因此它只能通过一定的机制,使得在执行时候能够找到和调用正确的函数体。可以想象,为了达到这个目的,一些相关信息应该封装在对象自身中。这些信息有点象身份证明,标识自己,这样在动态联编的时候,编译器可以根据这些标记找到相应的函数体,&不要跑,就是你了&。&
实际上的动态联编过程是什么样的呢。
二 对象类型信息
为了证明我们的猜想,我们用下面的一个程序进行测试,下面的程序将获取普通的类和包含虚函数的类的字节大小。程序代码如下。
1. #include &iostream.h&
2. class shape_novirtual{
draw(){cout&&&shape_novirtual::draw()&&&}
7. class shape_virtual1{
virtual void draw(){cout&&&shape_virtual::draw()&&&}
12. class shape_virtual2{
virtual void draw(){cout&&&shape_virtual2::draw()&&&}
virtual void draw1(){cout&&&shape_virtual2::draw1()&&&}
18. main(){
cout&&&sizeof(int)&&&sizeof(int)&&
cout&&&sizeof(class shape_novirtual):&&&sizeof(shape_novirtual)&&
cout&&&sizeof(void*):&&&sizeof(void*)&&
cout&&&sizeof(class shape_virtual):&&&sizeof(shape_virtual)&&
cout&&&sizeof(class shape_virtual2):&&&sizeof(shape_virtual2)&&
VC6.0中运行结果如下:
sizeof(int)4
sizeof(class shape_novirtual):4
sizeof(void*):4
sizeof(class shape_virtual1):8
sizeof(class shape_virtual2):8
Press any key to continue
从上面可以看出,没有虚函数的类shape_novirtual的大小为4,正好为int a的大小。而带有虚函数的类shape_virtual1和shape_virtual2的大小除了int a的大小还多出了4格个字节的大小,这个大小正好是void*指针的大小。到现在为止我们基本上可以说带有虚函数的对象自身确实插入了一些指针信息,而且这个指针信息并不随着虚函数的增加而增大。
如果我们将每个类的成员变量int a去掉,VC6.0运行结果就会变成下面的情况。
sizeof(int)4
sizeof(class shape_novirtual):1
sizeof(void*):4
sizeof(class shape_virtual1):4
sizeof(class shape_virtual2):4
Press any key to continue
上面的运行结果应该让人感到例外。既然size(int)为4,现在没有了这个成员变量,类shape_novirtual应该字节大小为0,但事实上C++编译器不允许对象为零长度。试想一个长度为0的对象在内存中怎么存放?怎么获取它的地址?为了避免这种情况,C++强制给这种类插入一个缺省成员,长度为1。如果有自定义的变量,变量将取代这个缺省成员。
三 虚函数表VTABLE
动态联编过程跟我们猜测的大致相同。编译器在执行过程中遇到virtual关键字的时候,将自动安装动态联编需要的机制,首先为这些包含virtual函数的类(注意不是类的实例)--即使是祖先类包含虚函数而本身没有--建立一张虚拟函数表VTABLE。在这些虚拟函数表中,编译器将依次按照函数声明次序放置类的特定虚函数的地址。同时在每个带有虚函数的类中放置一个称之为vpointer的指针,简称vptr,这个指针指向这个类的VTABLE。
关于虚拟函数表,有几点必须声明清楚:
1. 每一个类别只能有一个虚拟函数表,如果该类没有虚拟函数,则不存在虚拟函数表。
2. C++编译时候编译器会在含有虚函数的类中加上一个指向虚拟函数表的指针vptr。
3. 从一个类别诞生的每一个对象,将获取该类别中的vptr指针,这个指针同样指向类的VTABLE。
因此类、对象、VTABLE的层次结构可以用下图表示。其中X类和Y类的对象的指针 都指向了X,Y的虚拟函数表,同时X,Y类自身也包含了指向虚拟函数的指针。
为了方便问题说明,我们将2.cpp例子进行扩展,扩展程序如下。
#include &iostream.h &
16. class shape{
17. public:
18. virtual void draw(){cout&&&shape::draw()&&&}
19. virtual void area(){cout&&&shape::area()&&&}
20. void fun(){draw();area();}
22. class circle:public shape{
23. public:
24. void draw(){cout&&&circle::draw()&&&}
25. void adjust(){cout&&&circle::adjust()&&&}
27. main(){
29. oneshape.fun();
32. shape& baseshape=
33. baseshape.fun();
编译器在编译上面这段代码的时候将为这shape和circle两个对象分别建立一个VTABLE表,这些表依次填充派生类对象和基类对象中声明的所有的虚函数地址。如果派生类本身没有重新定义基类的虚函数,那么填充的就是基类的虚函数地址。这样一旦如果函数调用一个派生类不存在的方法时候能够自动调用基类方法。然后编译器在每个类中放置一个vptr,一般置于对象的起始位置,继而在对象的构造函数中将vptr初始化为本类的VTABLE的地址。整个结果布局如下。
&这张图有点问题
图一中的rectangle的VTABLE中的area() 和triangle的VTABLE的adjust()都是填充的基类的虚函数地址。 C++ 编译程序时候按下面的步骤进行工作:
①为各类建立虚拟函数表,如果没有虚函数则不建立。
②暂时不连接虚函数,而是将各个虚函数的地址放入虚拟函数表中。
③直接连接各静态函数。
这些工作做完之后,模块图如图二:
执行时候,诞生了oneshape和circleshape两个对象,oneshape对象的vptr指针指向shape的VTABLE,circleshape对象的vptr指针指向circleshape的VTABLE,在执行oneshape.fun()的时候,fun函数的this指针指向了oneshape对象,进入fun()之后程序继续执行this-&draw(),由于this指向oneshape对象,oneshape的vptr又指向shape类的VTABLE,这样就从VTABLE中得到需要绑定的函数的地址,并连接起来。同样,this-&
area()也经由oneshape对象而连接到相应的函数上,如图三。
现在我们执行baseshape.fun()函数。
shape& baseshape=baseshape.fun();函数进入fun函数之后,函数的this指针将指向basefun对象,另一方面basefun指向一个circleshape,因此this指针指向的实际上为circleshape对象,而circleshape的vptr指针指向circle类的虚拟函数表,这样编译器将从虚拟表中取出circle::draw()和circle::area()的地址,进行连接。因为circle本身没有重新定义area()方法,因此编译器使用shape的area()方法。如图四。
遵循上面的思路,基于基类的指针总能找到正确的子类对象的实现。但是象上面的 this-&draw是怎么编译的呢。
四 编译内幕
在上面的程序中,this指针不同,从而连接到不同的fun函数。那么C++如何编译这些指令呢。道理在于:所有的基类的派生类的虚拟函数表的顺序与基类的顺序是一样的,对于基类中不存在方法再按照声明次序进行排放。这样不管是shape还是circle或者从shape又继承出来的其余的类它们的虚拟函数表的第一项总是draw函数的地址,然后是area的地址。对于circle类,下面的才是adjust的地址。因此不管对于shape还是circle,this-&draw总是编译成
call this-&VTABLE[0]; this-&area()总是翻译成 call this-&VTABLE[1]; 程序到真正运行时候将会发现this的真正指向的对象,如果是shape,则调用shape-&VTABLE[0],如果是circle,则调用circle-&VTABLE[1]。
看过本文的人也看了:
我要留言技术领域:
取消收藏确定要取消收藏吗?
删除图谱提示你保存在该图谱下的知识内容也会被删除,建议你先将内容移到其他图谱中。你确定要删除知识图谱及其内容吗?
删除节点提示无法删除该知识节点,因该节点下仍保存有相关知识内容!
删除节点提示你确定要删除该知识节点吗?}

我要回帖

更多关于 基类析构函数是虚函数 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信