C++对象模型(9)-- 虚函数1

虚函数是指用 virtual 关键字修饰的类成员函数。其定义如下所示:

virtual void vir_print(){}

虚函数的作用是允许使用基类的指针来调用子类的函数,从而实现“多态”。含虚函数的类在编译时会产生一张虚函数表vtbl,里面保存了函数的调用地址。类对象通过虚函数表指针vptr找到虚函数表vtbl,通过vtbl找到对应函数的调用地址,然后调用执行。

1、虚函数表指针位置

一般地,虚函数表指针位于对象的开头位置。

我们可以写一段代码来测试一下:

class Base {
public:
    int t_i = 8;
    virtual void vir_print() {
        std::cout << "Base::vir_print()" << std::endl;
    }
};

int main()
{
    Base base;
    return 0;
}

我们用VS2019来调试上面的程序,把断点设在第12行的return 0处,运行后打开“快速监视”工具,在“表达式”窗口输入:&base。可以看到base对象的内存布局如下:

C++对象模型(9)-- 虚函数1_第1张图片

另外,我们也可以通过读取二进制的方式来验证base对象的内存模型。我们在main()函数中加入下面的代码:

std::cout << "sizeof(base) = " << sizeof(base) << std::endl;
int* pt_i = (int*)((char*)&base + 4);
std::cout << "t_i = " << *pt_i << std::endl;

程序的输出结果为:

sizeof(base) = 8

t_i = 8

所以对象的大小是8个字节,第5 - 8个字节是变量t_i,所以1-4个字节是虚函数表指针的。

C++对象模型(9)-- 虚函数1_第2张图片

2、虚函数的手工调用

为了加深对虚函数调用过程的认识,我们直接通过虚函数表指针来调用虚函数。

Base类的定义如下:

class Base {
public:
    int t_i = 8;
    virtual void vir_f() {
        std::cout << "Base::vir_f()" << std::endl;
    }

    virtual void vir_g() {
        std::cout << "Base::vir_g()" << std::endl;
    }

    virtual void vir_h() {
        std::cout << "Base::vir_h()" << std::endl;
    }
};

Base类对象的内存布局是下面这样的。

C++对象模型(9)-- 虚函数1_第3张图片

虚函数表和成员函数都是属于Base类的,虚函数表指针是属于base对象的,每个对象都会生成自己的虚函数表指针,但指针指向的虚函数表地址都相同。

main()函数的定义如下:

int main()
{
    Base base;
    long* pvptr = (long*)&base; //因为对象的首地址是虚函数表指针
    long* vptr = (long*)(*pvptr); //指针的值就是指向的虚函数表

    //定义函数指针,指向 void (void)函数
    typedef void (*Func)(void);

    //因为虚函数表是个数组,所以我们可以通过vptr[i]的方式来调用对应的虚函数
    Func vir_f = (Func)vptr[0];
    Func vir_g = (Func)vptr[1];
    Func vir_h = (Func)vptr[2];

    vir_f();
    vir_g();
    vir_h();
}

3、继承虚函数表分析

3.1 单继承

假设有Base, Derive两个类,其中Derive继承自Base类。代码如下所示:

class Base {
public:
    int b_i = 8;
    virtual void vir_f() {
        std::cout << "Base::vir_f()" << std::endl;
    }

    virtual void vir_g() {
        std::cout << "Base::vir_g()" << std::endl;
    }

    virtual void vir_h() {
        std::cout << "Base::vir_h()" << std::endl;
    }

    int d_i = 9;
};

class Derive :public Base {
public:
    int d_i = 9;
    void vir_f() {
        std::cout << "Derive::vir_f()" << std::endl;
    }

    void vir_g() {
        std::cout << "Derive::vir_g()" << std::endl;
    }

    void vir_h() {
        std::cout << "Derive::vir_h()" << std::endl;
    }
};

int main()
{
    Derive derive;

    return 0;
}

通过VS2019的“快速监视”窗口,我们可以看到derive对象的内存布局是这样的:

C++对象模型(9)-- 虚函数1_第4张图片

从图中我们可以看到derive对象的内存布局顺序从上到下是这样排列的:

虚函数表指针vptr、Base类的int变量b_i、Derive类的int变量d_i。

C++对象模型(9)-- 虚函数1_第5张图片

关于虚函数,有这么几个要点:

(1)只要在父类中是虚函数,子类中不用写virtual也依然是虚函数。

(2)如果父类中有虚函数,就算子类没有虚函数,子类也会创建自己的虚函数表。

(3)子类中的虚函数会覆盖父类中的同名虚函数。

(4)在把子类对象赋值给父类时,会用子类对象的值去初始化父类对象,但父类对象的虚函数表指针指向的依然是它自己的虚函数表。

关于第4点,我们可以通过下面的代码来验证:

Derive derive;
Base base = derive;
long* pvptr = (long*)&base; //因为对象的首地址是虚函数表指针
long* vptr = (long*)(*pvptr); //指针的值就是指向的虚函数表

//定义函数指针,指向 void (void)函数
typedef void (*Func)(void);

//因为虚函数表是个数组,所以我们可以通过vptr[i]的方式来调用对应的虚函数
Func vir_f = (Func)vptr[0];
Func vir_g = (Func)vptr[1];
Func vir_h = (Func)vptr[2];

vir_f();
vir_g();
vir_h();

输出结果如下:

Base::vir_f()

Base::vir_g()

Base::vir_h()

3.2 虚函数的动态绑定

前面说过:在把子类对象赋值给父类时,会用子类对象的值去初始化父类对象,但父类对象的虚函数表指针指向的依然是它自己的虚函数表。

Derive derive;
Base base = derive;
base.vir_f(); //输出Base::vir_f()

这里读者是否有疑问,这个base是用derive对象赋值的,应该调用Derive类的 vir_f()方法才对啊?

这里,必须说一下“静态绑定”、“动态绑定”这2个概念。

(1)静态绑定:绑定的是静态类型,所对应的函数或者属性依赖对象的静态类型,发生在编译期。

(2)动态绑定:绑定的是动态类型,所对应的函数或者属性依赖对象的动态类型,发生在运行期。

虚函数是动态绑定的,Base base = derive;只是把derive对象是值赋给了base对象,base对象已经确定了,没有动态绑定的动作,所以base.vir_f()只能调用自己的函数。要调用子类的虚函数,必须用类指针去动态调用。

Derive derive;
Base* pb = &derive; //输出Derive::vir_f()
pb->vir_f();

Base& b = derive; //输出Derive::vir_f()
b.vir_f();

3.3 多重继承

我们在前面例子的基础上,加入一个新类Base2:

class Base2 {
public:
    int b2_i = 10;
    virtual void vir_f2() {
        std::cout << "Base2::vir_f2()" << std::endl;
    }

    virtual void vir_g2() {
        std::cout << "Base2::vir_g2()" << std::endl;
    }

    virtual void vir_h2() {
        std::cout << "Base2::vir_h2()" << std::endl;
    }
};

然后让Derive类继承这个Base2类:

class Derive :public Base, public Base2 {......}

derive对象的内存布局如下:

C++对象模型(9)-- 虚函数1_第6张图片

对于多重继承下的虚函数,有几个要点:

(1)一个类如果继承自多个父类,且父类都有虚函数,则子类就会有多个虚函数表。

(2)类有几个虚函数表,其对象就会有对应的几个虚函数表指针。

(3)子类对象与第一个基类共用1个虚函数表指针。

C++对象模型(9)-- 虚函数1_第7张图片

你可能感兴趣的:(C++对象模型,c++,对象模型,继续,虚函数)