详解C++虚函数

版权声明:本文转载出处https://blog.csdn.net/linyt/article/details/51811314

介绍

本文尽量使用图来解释虚函数在类,继承,多继承各种场下的对象模型结构,以及虚函数实现多态绑定。

值得注意的是,不同编译器生成的对象结构和虚函数表稍为有一些不同,本文均采用gcc 5.3.0版本下的g++编译器作为研究对象。

普通类

object类的定义

class object {
    int a;
    int b;

public:
    object(): a(0), b(1) {}
    virtual void f() {}
};

上述代码中定义object类,定义两个int成员,分别是a和b,然后定义了虚函数f。

object对象内存结构

下图是定义两个object对象o1和o2的内存结构。

这里写图片描述

所有带虚函数类对象的首4字节为虚函数表(vtable)指针,object也是如此。object对象的第一个4字节为vtable指针,它指向object全局的虚函数表,每个类只需一个vtable表即可。o1和o2共享一个虚函数表。 
虚函数表的内容依次是:object::f(),object::g()

接下来8个字节是意想不到的东西,那就是object类的type_info对象,这由编译器生成的对象结构,它与C++库中的type_info内存部局完全相同。 
g++编译器将类的type_info对象信息放到了vtable表的尾部。

调用虚函数过程

下面图片描述了object指针调用虚函数的过程。 
这里写图片描述

具体过程可解释如下:

  1. 从o对象中找到它的虚函数表地址
  2. 根据g函数在虚函数表中的offset,该函数地址
  3. 根据函数地址进行调用

获取type_info对象

C++的RTTI机制本该属于别一个话题,不适合在虚函数中谈论。但在具体实现过程中,编译器将它和vtable合并到一起,所以还在有必要简单讨论RTTI机制。

由于type_info信息也是放到vtable里面,那可以认为typeid操作符是虚函数一部分,它在vtable也有一个offset.

下面是object对象获取它的type_info引用的过程。

这里写图片描述

type_info请查阅资料:https://msdn.microsoft.com/zh-cn/library/70ky2y6k

与其它虚函数调用类似,typeid返回的type_info对象就是vtable尾部的type_info对象。 

每个类只有一个type_inof对象,不能被修改,所以typeid操作符只能是返回const引用。

可以想象一下typeid(o).name()就是返回type_info对象是name成员指向的字符器串”6object”。

继承类

父类和子类定义

下面代码定义父类base和子类derive.

class base {
    int b;
public:
    virtual void f() {}
    virtual void g() {}
};

class derive: public base {
    int d;
public:
    virtual void g() {}
};

base对象和derive对象内存结构

这里写图片描述

derive子类重写了g()函数,所以它的vtable中的第二项为derive::g(),而f()函数没有重写,所以第一项仍然是base::f()函数。

多态的实现

我们经常看到这样的代码:

base *b = new derive();
b->g();

在b->g()调用过程中,调用的是derive::g()函数,而不是base::g(),是如何实现的呢?这其中的奥秘就是虚函数表中。详见下图。

这里写图片描述

b对象尽管是base*类型的,但它的地址跟new出来derive对象地址是同一个(后面多重继承例子中就不是这样子的了),所以在调用b->g()时,从vtable指向的虚函数中找第二项,它值为derive::g()函数的地址,所以最终调用的是derive::g()函数。

多重继承

多重继承是更复杂的一个场景,在多重继承的情况下,子类指针向基类指针转换时,它的地址是不一样的,所以编译必须生成一些额外代码来做地址转换。

多重继承类定义

class base1 {
    int b1;
public:
    virtual void f1() {}
    virtual void g1() {}
};

class base2 {
    int b2;
public:
    virtual void f2() {}
    virtual void g2() {}
};

class base3 {
    int b3;
public:
    virtual void f3() {}
    virtual void g3() {}
};

class derive: public base1, public base2, pbulic base3 {
    int d;
public:
    virtual void f1() {}
    virtual void f2() {}
    virtual void f3() {}
};

基类的对象内存结构

下图分别定义base1, base2, base3基类对象,不需过多解释。

这里写图片描述

派生类的对象内存结构

从这个图开始,我们开始要烧脑了。下图是derive对象d的内存结构:

这里写图片描述

derive对象内存结构有以下几个特点:

  1. base1, base2, base3这3个基类依次排列,后面才是derive类新增的d成员
  2. derive对象有3个虚函数表指针(请注意不是1个了,这里面大有戏法) 
    3.derive对象有前8字节,也是base1基类所在坑的位置;它的vtable指针指向的虚函数表,供derive类型使用,也供base1类型使用。对于base1类型,只使用前两项。而derive类型,则使用更多项
  3. derive类的虚函数表中:前两项的排列是与base1完全一样的,而后面的derive::f2(),derive::f3(),则是dervice类重载base2/base3虚函数的总列表。
  4. derive另外两个虚函数表,以base2坑的虚函数表为例,它有两项。第一项是non-virtual thunk to derive::f2(),第二项是base2::g2()。因为derive类没有重写g2函数,所以第二项填base2::g2()是乎合理解的。而non-virtul thunk to derive::f2()这项我们后面会解释。
  5. 其它的-8和-16数字,估计是其它语法场景下有用,目前没有看到,可以先跳过它们。
  6. 另外在整个derive的虚函数表中,出现两次derive类的type_info指针,先忽略它们吧。

派生类向基类转换的秘密

也许你知道,派生类对象转基类对象转换之后,这两者的地址都是一样的,而在多重继承里面,这个结论就不对了。

从上面看到,派生类是将基类依次排列而成。所以派生类对象指针向第一个基类指针转换时,两者地址是一样的;而第二个和第三个基类对象指针转换时,它的地址就不一样的。请看下图:

这里写图片描述

派生类调用非重写函数

以这两行代码为例 
derive *d = new derive(); 
d->g2();

显然,derive没有重写g2()函数,所以它调用的是base2类的虚函数。 
其实,不管derive是否有重写g2函数,都是通过base2的虚函数表找出来的。具体过程如下图所示:

这里写图片描述

由于g2函数是最早是由base2类定义的,所以d->g2()调用时,先从d对象中的base2虚函数表,查找g2偏移量(值为4)的表项,再调用。

但这里有个细节一定要注意的是,base2::g2函数的this指针是base2 *类型的,而这里的d是derive*类型的,需要先将derive *指针转换成base2*指针。这个转换完成之后,指针值就增加8字节了。

多重继承下的多态实现

这里详细分析

base2 *b2 = new derive();
b2->f2();

是如何实现从基类到派生类f2()函数的调用。

这里写图片描述

b2指针已指向了derive对象的base2部分,然后b2->f2()从base2-vtable对应的虚函数表的第一项,找到了non-virtual thunk to derive::f2(),然后调用。

咦,这里不应该是derive::f2()吗,那个non-virtual thunk to derive::f2()是什么鬼?

答案是和this指针强相关

derive::f2()函数的this指针肯定是derive*类型的,而这里的b2是base2*类型,不能直接调用。

non-virtual thunk to derive::f2()代码其实是两行汇编,它完成出b2指针从base*类型转换成derive*类型的功能,也即地址减去8。

小结

其实我想只用图表将C++虚函数全部表达出来,但当我画出来之后,发现很多细节不用文字稍作说明,不是很难明白。

其实这里说的C++虚函数原理跟你之前了解的应该是一致的,只是很难技术细节你没有想过而已,但不管理怎么样,我们一起学习吧。


#include <iostream>
#include <typeinfo>
using namespace std;

class Vehicle {
protected:
    int velocity;
public:
    virtual void drive() = 0;
    virtual void getCustomer() = 0;
};

class Car : public Vehicle {
protected:
   char *p;
public:
    void drive() {
        cout << "Car drive" << endl;
    }
    void getCustomer() {
        cout << "Car getCustomer" << endl;
    }
};

class Train: public Vehicle {
public:
    void drive() {
        cout << "Train drive" << endl;
    }
    void getCustomer() {
        cout << "Train getCustomer" << endl;
    }
    void fun() {

    }
};

void func_drive(Vehicle &vehivle) {  // 此处必须是基类对象的引用
    vehivle.drive();
}

void func_getCustomer(Vehicle &vehicle) {  // 此处必须是基类对象的引用
    vehicle.getCustomer();
}

int main() {
    Car car;
    Train train;
    func_drive(car);
    func_drive(train);
    func_getCustomer(car);
    func_getCustomer(train);
    cout << sizeof(int) << endl;
    cout << sizeof(char *) << endl;
    cout << sizeof(Car) << endl;
    cout << sizeof(Train) << endl;
    const type_info &info = typeid(Car);
    cout << info.name() << endl;
    const type_info &info_1 = typeid(Train);
    cout << info_1.name() << endl;
    return 0;
}

结果:

/home/azheng/CLionProjects/untitled/cmake-build-debug/untitled
Car drive
Train drive
Car getCustomer
Train getCustomer
4
8
24
16
3Car
5Train

Process finished with exit code 0

关于type_info().name()前面的数字的含义,暂时留着、

猜你喜欢

转载自blog.csdn.net/baidu_20351223/article/details/80218377