你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

虚函数表指针与多态

2021/11/23 23:30:42

多态

多态就是多种形态,C++的多态分为静态多态与动态多态。

  • 静态多态就是编译器根据函数实参的类型判断出要调用哪个函数。比如函数重载和函数模板。
  • 动态多态依靠的是虚函数表和动态绑定机制,因为是在运行时根据对象的类型在虚函数表中寻找调用函数的地址来调用相应的函数,所以称为动态多态。

虚函数表何时建立,compile-time 还是 run-time?
在一个类的某个函数前加上virtual关键字,这个函数就变成了虚函数,当这个类中存在虚函数时,编译器会给这个类创建一个虚函数表,虚函数表里存放了这个类中所有虚函数的地址。
也就是说,虚函数表是在编译期间就已经生成了!

虚函数表与内存【虚函数表存放在.rodata段】

class A
{
public:
    A() {}
    ~A() {}
    virtual void func_one() {}
    virtual void func_two() {}
};

以上面的这个类A为例,类A里有两个虚函数func_one()、func_two()。编译完成后,源程序会生成可执行程序,可执行程序的格式如下,我们可以看到,类A的虚函数表里存放着各个虚函数的地址,每个地址对应着虚函数的代码实现。值得注意的是,类的非虚函数并不在虚函数表里。
在这里插入图片描述
然后程序开始运行了,程序运行时,上图的可执行程序的某些节会被装入内存,成为一个进程,过程如下图:
在这里插入图片描述
假设主函数里实例化了2个类A的对象,分别是a1、a2。运行时类A的内存是什么样的呢?

程序运行时的内存模型如下图,类A的实例a1、a2 各占一块堆区的内存,在实例a1、a2所占内存的最开始,是一个4字节的虚指针(32位的程序指针是4字节,64位程序指针是8字节),这个虚指针指向了类A的虚函数表。

在这里插入图片描述

一个类只会有一个虚函数表,类的所有对象是共享这个虚函数表的,在本例中,类A有一个虚函数表,图中绿色的那块,实例化了两个类A的对象a1、a2,对象a1、a2共享类A的虚函数表,就算再实例化出对象a3、a4、a5、a6、a7……,类A也只会有一个虚函数表,且这个虚函数表在可执行程序中存放在.rodata节,在进程的内存模型中存放在只读数据段。

类的这么多个对象如何共享类的这一个虚函数表呢?我们知道,运行时每个对象都会有自己的一块堆区内存(图中蓝色和黄色那块),如果类有虚函数,那么类的每个对象的内存中都会有一个指向虚表的虚指针,通过这个虚指针,这些对象就都能访问到类的虚表。这也是为什么不管类中有多少个虚函数,对象的内存中都只会多4个字节的原因,因为每个对象的内存中只是多了一个指向这个类的虚函数表的指针。

继承与虚函数表
如果基类中有虚函数,基类就会有一个虚函数表,派生类继承于这个基类,不管派生类中有没有虚函数,派生类中都会自动创建一个虚函数表, 虚函数表中存放基类所有虚函数的地址,也就是说派生类会继承基类的虚函数(其实基类的非虚函数、数据成员,派生类通通都会继承过来)。

来看个例子,基类A有虚函数func_one()、func_two(),派生类B、C没有虚函数,主函数中实例化了3个对象a、b、c。

class A		// 基类,有虚函数
{
public:
    A() {}
    virtual ~A() {}
    virtual void func_one() {}
    virtual void func_two() {}
};

class B:public A	// 派生类,没有虚函数
{
public:
    B() {}
    ~B() {}
};

class C:public A	// 派生类,没有虚函数
{
public:
    C() {}
    ~C() {}
};

int main() {		// 实例化三个对象
    A *a = new A();
    B *b = new B();
    C *c = new C();
    // 仅为举例,自己写代码时记得delete,否则会造成内存泄漏
}

进程运行的内存分配如下:
在这里插入图片描述
我们可以看到,尽管类B、类C没有自己的虚函数,但由于它们继承于类A,所以类B、类C也会各自有一个虚函数表,虚函数表中是类A的虚函数A::func_one()、A::func_two()的地址。

重写与虚函数表
如果我们在派生类中重写和基类的虚函数,那么在派生类的这个虚函数表里,重写的派生类虚函数就会覆盖基类的同名虚函数。

仍然举个例子,基类A有虚函数func_one()、func_two(),派生类B、C重写了虚函数func_two(),主函数中实例化了3个对象a、b、c。

class A		  // 基类,有虚函数
{
public:
    A() {}
    virtual ~A() {}
    virtual void func_one() {}
    virtual void func_two() {}
};

class B:public A
{
public:
    B() {}
    ~B() {}
    virtual void func_two() {}		// 派生类,重写了虚函数func_two()
};

class C:public A
{
public:
    C() {}
    ~C() {}
    virtual void func_two() {}	   // 派生类,重写了虚函数func_two()
};

int main() {		// 实例化三个对象
    A *a = new A();
    B *b = new B();
    C *c = new C();
    // 仅为举例,自己写代码时记得delete,否则会造成内存泄漏
}

我们可以看看下面这个横着的图:
在这里插入图片描述 我们在派生类B、C中写上和基类同名的虚函数func_two()的实现,这个操作叫做重写。重写func_two()后,派生类B和派生类C的虚函数表中,原本基类虚函数A::func_two()的地址会被覆盖成派生类虚函数B::func_two()的地址。

多态原理与虚函数表
用基类指针指向某个派生类对象,程序在运行时 ,会根据基类指针指向的对象类型,去查这个类的虚函数表,找到对应虚函数的地址并调用。

int main() {
    // 利用基类指针指向派生类对象,调用派生类对象的虚函数,实现多态
    A *p1 = new B();
    A *p2 = new C();
    p1->func_two();     // 调用的是B::func_two()
    p2->func_two();     // 调用的是C::func_two()
 	// 仅为举例,自己写代码时记得delete,否则会造成内存泄漏
}

现在基类指针p1指向的是类B,就会根据类B的虚指针找到类B的虚函数表,在虚函数表中查找func_two()的地址,因为在类B中重写了func_two(),因此在类B的虚函数表中A::func_two()的地址已经被B::func_two()的地址覆盖掉了,因此找到的这个虚函数地址是派生类B的虚函数地址,然后就可以去调用B::func_two()了。

同理,基类指针p2指向的是类C,就会去查类C的虚函数表,找到的就是C::func_two()的地址,调用的自然也就是C::func_two()。

通过基类指针指向的对象类型去调用不同类的同名虚函数,就实现了动态多态。

转载链接:
https://blog.csdn.net/weixin_43222324/article/details/109767675