什么是虚函数
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”
虚函数表
虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
C++的编译器确保虚函数表的指针存在于对象实例中最前面的位置(确保虚函数的性能)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
class Base {
public:
virtual void f() { }
virtual void g() { }
virtual void h() { }
};
一般继承(无虚函数覆盖)
在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
一般继承(有虚函数覆盖)
子类
class Derive:public Base{
void f();
}
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
1、每个父类都有自己的虚表。
2、第一个父类是按照声明顺序来判断的
多重继承(有虚函数覆盖)
我们在子类中覆盖了父类的f()函数。
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了
虚表的结尾
windows下以NULL为结尾
Linux下如果是多重继承,则以1结尾,0代表没有后续的虚表了。
虚函数的一些问题
1、通过父类型的指针访问子类自己的虚函数
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。
2、 访问non-public的虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
虚函数的效率
虚函数需要一次间接的寻。而一般的函数可以在编译时定位到函数的地址,而虚函数(动态类型调用)是根据某个指针定位到函数在虚函数表的地址。当在动态执行时,会到该函数表中寻找函数,多增加了一个过程,效率自然比较低一点。(虚函数内部执行时间越短,虚函数查找时间占比就越高,相对来说性能损耗就越高)