前言
使用过Java的同学应该特别熟悉多态,即耳熟能详的一句话:可以使用父类引用来指向子类对象,而实现起来非常容易,只需要重写父类的方法或者实现接口的方法即可:
java
//Java代码
class Parent {
void show() {
System.out.println("Inside Parent");
}
}
class Child extends Parent {
void show() {
System.out.println("Inside Child");
}
}
public class Main {
public static void main(String[] args) {
Parent obj = new Child();
obj.show(); // 调用的是子类的 show() 方法
}
}
在Java中,这里是使用父类Parent
的引用obj
,指向了子类对象,然后调用的是子类的show()
方法。
但是在C++中,如果按同样类似的写法,却得到的结果不一样:
C++
//C++代码
#include <iostream>
class Parent {
public:
void show() {
std::cout << "Inside Parent" << std::endl;
}
};
class Child : public Parent {
public:
void show() {
std::cout << "Inside Child" << std::endl;
}
};
int main() {
Parent* obj = new Child();
obj->show(); // 调用的是父类的 show() 方法
delete obj;
return 0;
}
在C++中,这里使用父类的指针,指向子类对象,但是最终调用的是父类的show()
方法。那如何实现像Java那种效果呢?就需要使用虚函数,本篇文章深入探究虚函数以及虚函数的原理--虚函数表(后面简称虚表)的相关知识。
正文
关于虚函数的使用,以及纯虚函数定义接口等,这些基础知识,暂时就不详细说明了,重点关注虚表。
虚表是属于类
虚表是函数指针类型的数组,指向该类定义的虚函数。
在C++中,只要一个类包含了一个虚函数,那么就有由一个虚表来维护。虚表是属于类的,而非属于某个对象,一个类只需要一个虚表,所以该类对象共用一个虚表。
当类B继承至类A,继承类也可以调用A的函数,如果A是一个包含虚表的基类,那么继承类B也拥有自己的虚表。
理解类的虚表非常重要,比如有代码:
C++
class A {
public:
A();
virtual void vfunc1() { qDebug() << "A vfunc1()"; }
virtual void vfunc2() { qDebug() << "A vfunc2()"; }
void func1() { qDebug() << "A func1()"; }
void func2() { qDebug() << "A func2()"; }
private:
int m_data1, m_data2;
};
那么类A的虚表如图:
虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针 。非虚函数的函数,调用不需要虚表,所以虚表中不会保存。虚表的创建在编译时期,即在编译时期,属于一个类的虚表就构造出来了。
对象的虚表指针
我们使用IDE调试模式,来看一下类A的对象包含哪些东西:
可以发现除了其数据成员外,还有一个__vptr
指针,这个指针是虚表指针,指向类的虚表 。为了指定对象的虚表,编译器在类中添加了__vptr
指针,专门用来指向虚表。
这里可以知道,同一个类的多个对象,其虚表指针所指向的虚表是一样的:
效果如图:
动态绑定
重点来了,C++是如何通过虚表实现多态的,即动态绑定,有如下3个类:
C++
class A {
public:
A();
virtual void vfunc1() { qDebug() << "A vfunc1()"; }
virtual void vfunc2() { qDebug() << "A vfunc2()"; }
void func1() { qDebug() << "A func1()"; }
void func2() { qDebug() << "A func2()"; }
private:
int m_data1, m_data2;
};
class B : public A {
public:
B();
virtual void vfunc1() { qDebug() << "B vfunc1()"; }
void func1() { qDebug() << "B func1()"; }
private:
int m_data3;
};
class C : public B {
public:
C();
virtual void vfunc2() { qDebug() << "c vfunc2()"; }
void func2() { qDebug() << "C func2()"; }
private:
int m_data1, m_data4;
};
记住:虚表是属于类的,上面代码的虚表通过IDE调试模式如下:
虚表关系如图:
我们来仔细分析:
-
类A包含2个虚函数,所以A的虚表元素是2个。
-
类B继承至A,但是重写了虚函数,对于重写的虚函数,会增加新的地址来保存新的函数指针 。所以对于类B的虚表,有一个是继承至A,即
A::vfunc2*()
,还有一个是新的函数,即B::vfunc1()
。 -
而对于C来说,它继承至B,但是重写了虚函数,所以会多出一个
C::vfunc2()
,那么另一个虚函数指针是执行类B的vfunc1()
还是类A的vfunc1()
呢?这里会指向
B::vfunc1()
,规律非常简单:对象的虚表指针用来指向类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数,所以C的虚表先是指向B的虚表,然后由于重写虚函数的原因,会修改其中一个指针的值。
基类指针指向子类对象
搞明白上面的继承关系后,多态就非常容易理解了,比如下面代码:
C++
B b;
A *p = &b;
p->vfunc1();
这里使用p来调用vfunc1()
,最终会调用类B中的虚函数,过程分析如下:
- 程序执行
p->vfunc1()
时,会发现p是一个指针,而且调用的是虚函数。 - 根据虚表指针
p->__vptr
来访问对象b对应的虚表 。虽然指针p是基类A的类型,但是__vptr
也是基类的一部分,是编译器加的,所以p->__ptr
可以访问对象的虚表。 - 在虚表中查找调用的函数 ,由于虚表是在编译时就确定,所以
p->vfunc1()
根据上面的图可知会调用B的虚函数 ,即重写的虚函数B::vfunc1()
。
这个过程就是动态绑定来实现的多态,非常容易理解。
总结
什么情况下会发生动态绑定呢?有如下几个条件:
- 通过指针调用函数。
- 指针upcast向上转型,比如继承类向基类的转换。
- 调用的是虚函数。