C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。
这里不再对虚函数的使用进行过多的描述,基类成员函数用virtual修饰时:
- 基类大小多4个字节(多一个指针,多出的大小和系统位数有关系),这4个字节是一个指针,指针的名字是_vfptr。这个指针 指向一个函数指针数组。数组中保存着所有虚函数的地址。C++的编译器应该是保证虚函数表的指针(_vfptr)存在于对象实例中最前面的位置(这是为了保证取到虚函数表的地址时有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表(这里和指针的大小以及对象的地址的大小有关系,32位系统中这两样都是4个字节),然后就可以遍历其中函数指针,并调用相应的函数。
- 派生类继承基类时,会继承基类的虚函数表,即会继承基类的函数指针数组里的元素。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
- 如果派生类有重写,那么重写后的函数地址会覆盖函数指针数组里面的函数地址
- 在调用函数时,回去虚函数表中找函数,自然而然就会调用覆盖后的函数。
注意,虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同,子类独有的虚函数放在后面。
下面是代码图一起讲解:
//基类
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
这个时候Base类对象b的虚函数表如下:
- 单继承的情况,并且子类没有重写了父类的虚方法
//派生类1,没有重写的情况
class Derive :public Base{
virtual void f1(){cout << "Derive ::f1" << endl;}
virtual void g1(){cout << "Derive ::g1" << endl;}
virtual void h1(){cout << "Derive ::h1" << endl;}
}
这个时候 Derive类对象d的虚函数表如下:
这里需要注意的是:虚函数按照其声明顺序放于表中;父类的虚函数在子类的虚函数前面。
- 单继承的情况,并且子类重写了父类的虚方法
//派生类2,有重写的情况
class Derive :public Base{
void f(){cout << "Derive ::f" << endl;}
virtual void f1(){cout << "Derive ::f1" << endl;}
virtual void g1(){cout << "Derive ::g1" << endl;}
virtual void h1(){cout << "Derive ::h1" << endl;}
}
这个时候 Derive类(注意这个类和上面的Derive类是并列的)对象d的虚函数表如下:
上面是单继承的情况,下面简单描述多集成的情况,了解了单继承的情况,多继承的情况一目了然。
- 假设有下面这样一个类的继承关系。注意:子类并没有重写父类的函数。
这个时候子类Derive类对象的虚函数表如下图:
这里需要注意的是:每个父类都有自己的虚表;子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
- 假设有下面这样一个类的继承关系。注意:子类并重写了父类的函数。
这个时候子类Derive类对象的虚函数表如下图:
这里需要注意:三个父类虚函数表中的f()的位置被替换成了子类的函数指针。
本文参考文章:https://blog.csdn.net/haoel/article/details/1948051/(注,本文所有的图片均来自参考博客)