C++多态和虚函数的实现原理

一、关于多态

多态是C++中的重要内容,多态的意思是可以使用父类指针指向子类,并通过这个指针调用子类的函数。

多态可以分为两种形式:

  1. 编译时多态,主要指泛型编程。
  2. 运行时多态,主要指虚函数和子类继承。

一般我们讨论的都是运行时多态,运行时多态的几个基本条件为:

  • 继承和虚函数。
  • 父类对象指向子类对象。

多态形成的原理就是vptr指针和vtable虚函数表,当类中有虚函数时,编译器会自动生成虚函数表vtable,这个表属于这个类,所有类实例共享。同时还成一个vptr指针指向这个虚函数表,执行调用的时候,先通过vptr指针找到自己的虚函数表,然后执行表中的函数。

二、证明vptr指针存在

多态形成的必要条件是虚函数和继承,当两者其中之一都不存在的时候,编译器不会形成多态,因此也不会生成vptr指针。只有两者同时存在的时候,编译器才会生成vptr指针。

以下代码通过类的大小来证明了vptr指针的存在:

#include <iostream>
using namespace std;

class A {
public:
    A() {};
    ~A() {};
    void print() { cout << "AAAAAAAAAA" << endl; };
};

class B : public A {
public:
    B() {};
    ~B() {};
    void print() { cout << "BBBBBBBBBB" << endl; };
};

int main() {
    cout << "sizeof(A): " << sizeof(A) << endl;
    cout << "sizeof(B): " << sizeof(B) << endl;
    return 0;
}

C++对空类会分配一个字节的内存,此时的输出A和B的大小都是1:

sizeof(A): 1
sizeof(B): 1

将A中的print()函数设置为虚函数后:

virtual void print() { cout << "AAAAAAAAAA" << endl; };

两个结构体的大小会变成8,因为添加虚函数后,类中会生成vptr指针,64位系统指针是四个字节,所以A和B的大小就变成了8。

三、vptr指针的工作原理

3.1 vptr和vtable

在创建一个含有虚函数的类时,系统会为每个类生成虚函数表,存放了当前对象所有的函数列表,而vptr指针就指向这个表的地址。子类继承父类后,也会生成一个属于自己的虚函数表和vptr指针,如果子类有重写父类的虚函数,虚函数表中的函数地址就是自己类中的成员函数。执行子类调用时,先通过vptr指针定位到对应的虚函数表,然后执行对应的函数。

以上是一个图形示例,类B继承了类A,类B重写了f1f2函数,它的函数表中f1f2都指向自己的成员函数。而f3并未继承,所以指向父类。因此执行子类函数时,f1f2都是用的自己的成员函数。

关于静态联编和动态联编

说到多态肯定免不了要提到动态联编,动态联编就是指程序在运行时才能决定的运行片段,例如ifswitch代码段,它们只有在运行时才能知道下一步是什么,走哪个代码代码段。多态也是如此,运行到了虚函数的时候才能决定执行哪个函数。而静态联编就是指程序在编译阶段就能决定的事情,就像main函数,编译后就固定从它开始执行。

3.2 vptr指针绑定的顺序

子类中vptr指针的值的绑定顺序:

  1. 运行父类构造函数时,先指向父类的虚函数表。
  2. 运行子类构造函数时,再把指针指向子类的虚函数表。

通过以下代码和GDB调试器可以验证这个观点:

#include<iostream>

using namespace std;

class A {
public:
    A() {
    };
    ~A() {};
    virtual void print() { cout << "AAAAAAAAAA" << endl; };
};

class B : public A {
public:
    B() {
    };
    ~B() {};
    void print() { cout << "BBBBBBBBBB" << endl; };
};

int main() {
    A a;
    B b;

    return 0;
}

带调试模式编译:

g++ main.cpp -g -o app -std=c++11

使用GDB调试执行,先在父类构造函数中添加断点:

子类构造函数中也添加断点:

输入run执行,执行到第一个断点,是正在构造a对象的时候,此时打印出A的vptr指针地址:

继续往下执行,下一个断点依旧是在类A的构造函数处,此时正在构造对象b,因为B继承了A,所以先执行A的构造函数。此时打印出b的vptr指针地址,可以看到b对象的vptr指针和a对象中的vptr指针地址一样:

再继续执行,走到类B的构造函数,此时对象b的vptr指针就被修改成了类B自己的虚函数表地址了:

过程图示:

第一步,执行父类构造函数时,指向父类的虚函数表。

第二步,执行子类自己的构造函数时,再指向子类的虚函数表。

最后修改:2019 年 12 月 29 日
如果觉得我的文章对你有用,请随意赞赏