C++虚函数原理,c虚函数详解你肯定懂了
C中虚函数的作用主要是实现多态的机制。关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以使父类的指针具有“多种形式”,是一种通用技术。所谓泛型技术,说白了就是试图用不可改变的代码来实现可改变的算法。比如:模板技术,RTTI技术,虚函数技术,要么尝试在编译时做一个解析,要么尝试在运行时做一个解析。
关于虚函数的使用,这里不做过多阐述。可以看看相关的C书籍。在这篇文章中,我只想从虚函数的实现机制给大家一个清晰的分析。
当然,网上也出现过一些相同的文章,但我总觉得这些文章不好读。有大段大段的代码,没有图片,没有详细的解释,没有对比,没有推论。不利于学习和阅读,所以这是我想写这篇文章的原因。也希望大家多提意见。
言归正传,我们一起进入虚函数的世界吧。
懂C的人应该知道,VirtualFunction是通过一个VirtualTable实现的。简称v表。在这个表中,需要一个类的虚函数的地址表。该表解决了继承性和覆盖性问题,保证了能真实反映实际功能。这样,在具有虚函数的类的实例中,这个表被分配在这个实例的内存中。所以当我们用父类的指针去操作子类的时候,这个虚函数表就变得重要了。像地图一样,它指出了实际应该调用的函数。
让我们来关注一下这个虚函数表。根据C的标准规范,编译器必须保证虚函数表的指针存在于对象实例的前端位置(这是为了保证虚函数的偏移量正确)。这意味着我们通过对象实例的地址得到这个虚函数表,然后就可以遍历其中的函数指针,调用相应的函数。
听了这么多,我能感觉到你现在可能比以前更迷茫了。没关系,下面是实际例子。相信聪明的你一眼就明白了。
假设我们有这样一个类:
类别库{
公共:
virtualvoidf(){ cout Base:f endl;}
virtualvoidg(){ cout Base:g endl;}
virtualvoidh(){ cout Base:h endl;}
};
根据上面的陈述,我们可以通过Base的实例得到虚函数表。下面是实际的例程:
typedefvoid(* Fun)(void);
Baseb
FunpFun=NULL
Cout 虚函数表地址:“(int *)(b)endl;
Cout 虚函数表—第一个函数地址:“(int *)*(int *)(b)endl;
//Invokethefirstvirtualfunction
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。在Ubuntu7.10 Linux2.6.22 GCC4.1.3下,如果该值为1,则表示有下一个虚函数表,如果该值为0,则表示是最后一个虚函数表。
接下来,我将分别解释虚函数表在“未覆盖”和“覆盖”时的外观。拥有一个不覆盖父类的虚函数是没有意义的。我之所以要讲没有覆盖的情况,主要是为了做一个对比。通过对比,我们可以更清楚的知道它的内部实现。
一般继承(没有虚函数覆盖)
接下来,我们来看看虚函数表在继承时是什么样子的。假设存在如下所示的继承关系:
请注意,在这个继承关系中,子类不会重载父类的任何函数。那么,在派生类的例子中,它的虚函数表如下:
比如:派生的;的虚函数表如下:
我们可以看到以下几点:
1)虚函数按照它们的声明顺序放在表中。
2)父类的虚函数先于子类的虚函数。
我相信聪明的你可以参考前面的程序写个程序验证一下。
一般继承(由虚函数覆盖)
覆盖父类的虚函数是显而易见的,否则虚函数就变得没有意义。现在,让我们来看看。如果子类中的虚函数重载了父类的虚函数,会是什么样子?假设我们有如下继承关系。
为了让大家看到继承的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。然后,对于派生类的实例,其虚函数表将如下所示:
从表中我们可以看出以下几点,
1)被覆盖的f()函数被放置在虚拟表中原始父虚函数的位置。
2)未涵盖的功能保持不变。
这样,我们可以看到,对于下面的程序,
base * b=new derive();
b-
b所指示的f()在内存虚函数表中的位置已经被derivative: f()函数的地址所取代,所以实际调用时,被调用的是derivative: f()。这实现了多态性。
多重继承(没有虚函数覆盖)
接下来,我们来看看多重继承中的情况,假设有下面这个类的继承关系。注意:子类不会覆盖父类的功能。
对于子类实例中的虚函数表,它看起来像这样:
我们可以看到:
1)每个父类都有自己的虚拟表。
2)子类的成员函数放在第一个父类的表中。(所谓第一个父类是根据声明顺序判断的)
这是为了解决不同父类型的指针指向同一个子类实例的问题,可以调用实际的函数。
多重继承(由虚函数覆盖)
我们再来看看,如果虚函数覆盖发生了。
在下图中,我们在子类中覆盖了父类的f()函数。
以下是子类实例中虚函数表的示意图:
我们可以看到f()在三个父类的虚函数表中的位置被子类的函数指针所取代。这样,我们可以指向任何静态类型的子类,并调用子类的f()。比如:
派生的;
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(){
派生的;
funp Fun=(Fun)*((int *)*(int *)(d)0);
pFun();
}
c语言是一种神奇的语言。对于程序员来说,我们似乎永远不知道这种语言在背后做什么。要熟悉这门语言,我们需要知道C里面的东西,知道C里面的危险的东西,否则就是搬起石头砸自己的脚的编程语言。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。