C++高级特性:虚函数与多态的实现原理(十三)

1、虚函数表和虚函数表指针
  • 如果一个类存在virtual关键字函数或者继承的基类中存在virtual关键字的函数,那么该类的就会存在vptr和一个vtable

  • vptr虚函数表指针全称virtual table pointer、vtable是虚函数表virtual table的缩写。

    class A{
    public:
        ~A();
    };
    
    class B{
    public:
        virtual void func();
        virtual ~B();
    };
    
    class C: public B{
    
    };
    int main() {
        std::cout << "sizeof(A) = " << sizeof(A) << std::endl;          // sizeof(A) = 1
        std::cout << "sizeof(B) = " << sizeof(B) << std::endl;          // sizeof(B) = 8
        std::cout << "sizeof(C) = " << sizeof(C) << std::endl;          // sizeof(C) = 8
    
        std::cout << "Hello, World!" << std::endl;
        return 0;
    }
    
  • 空类的大小为1字节,而存在虚函数(虚析构函数或其他虚函数)那么就会为该类生成一个vptr虚函数表指针和一个vtable虚函数表

    • 32位机器:vptr大小4字节
    • 64为机器:vptr大小8字节
    • 这个大小其实就是虚函数表指针vptr的大小
2、vptr和vtable的生成与存储
2.1、vtable虚函数表的生成
  • vtable虚函数表是在编译阶段生成的,存放在代码区的.rodata常量存储区只读数据段中,其中存储所有虚函数的函数地址
  • 所有函数的具体实现方式都会在编译阶段生成机器指令存储在代码区的.text代码段,并且都有一个地址,虚函数表里会存储所有的虚函数的地址
  • 一个类最多只有一张虚函数表(存在.rodata段),一个类也只有一套函数体(存在.text段)
2.2、vptr虚函数指针的创建
  • vptr的创建是在new的阶段,当new一个对象进行初始化调用构造函数时会给vptr指针进行赋值让其指向“对应的”vtable虚函数表

  • 如果没有构造函数,编译器会生成默认的空构造函数

  • 继承状态下:

    • 首先调用基类的构造函数进行构造,把基类的vtable地址赋值给vptr

    • 其次调用本类的构造函数进行构造,如果本类存在vtable那么将本类的vtable地址赋值给vptr

    • 这其实就是动态多态的实现原理
      在这里插入图片描述

3、多态的实现原理
  • C++多态是有两种形式:
    • 静态多态:早绑定(静态绑定),在编译期间做
    • 动态多态:晚绑定(动态绑定),在运行期间做
3.1、静态多态原理

静态多态的实现手段主要通过函数重载技术手段来实现,它会在编译期间就确定

  • 函数重载:

    • 函数名相同、参数个数不一样
    • 函数名相同、参数类型不一样
  • 函数重载原理:通过函数名修饰来实现

    • 预编译:去除空格、注释等;把头文件当中的函数声明拷贝到源文件当中,避免编译过程中的词法、语法分析找不到函数定义
    • 编译:语法分析,同时进行符号汇总(函数名)
    • 汇编:生成函数名到函数地址的映射,方便后续通过函数名找到函数定义位置,从而执行函数
    • 链接:将多个文件中的符号表汇总合并,地址回填等
int sum(int a, int b)
{
    return a + b;
}

double sum(double a, double b)
{
    return a + b;
}

例如上面这段代码经过g++ -c main.cpp -o main.c编译过程后进行反汇编生成出来的代码和符号表如下

// 符号表
指令:objdump -t main.c> character_table.txt
0000000000000051 g     F .text	0000000000000014 _Z3sumii
0000000000000065 g     F .text	000000000000001a _Z3sumdd
    

// 反汇编指令
指令:objdump -DC main.c> decompiler_code.txt    
0000000000000051 <sum(int, int)>:
.....

0000000000000065 <sum(double, double)>:
....
  • 可以看到_Z3sumii、_Z3sumdd分别表示sum的两个重载类型:int int参数和double double参数,存放在.text段
  • 而反汇编指令也可以看到类似的情形。
3.2、动态多态原理
class A {
public:
    A() = default;
    virtual void func();
    virtual ~A();
};

class B: public A {
public:
    B() = default;
    virtual void func() override;
    virtual ~B();
};

void test2()
{
    A *a = new A();             // 基类自身
    A *b = new B();             // 多态
    a->func();                  // A::func()
    b->func();                  // B::func()
}
  • 对于这样一段代码,首先类A、类B的大小都是8字节(64位机器),其次每个类都有自己的vtable表

  • A* a = new A()时,a指针通过构造函数进行初始化,此时a内部的vptr指针会指向A的虚函数表vtable

  • A* b = new B()时,虽然表面类型是A,但是实际类型是B。

    • new B()构造时:首先调用父类A的构造函数,将vptr指向A类的vtable虚函数表
    • 接着调用自己的构造函数时,会将vptr的指向重新赋值指向自己的vtable虚函数表
  • 需要在拷贝构造和赋值时注意,进行深拷贝而不是浅拷贝。浅拷贝意味着两个对象的指针都共享同一个vptr,一个释放另外一个就会导致野指针。

  • 其次也可以通过手动获取vptr指针然后再获取地址表执行函数

    void test3()
    {
        typedef void (*Func)(void);
        A *a = new A();             // 基类自身
        A *b = new B();             // 多态
    
        long* vptr_a = (long *)*(long *)a;
        long* vptr_b = (long *)*(long *)b;
        Func f_a = (Func) vptr_a[0];
        Func f_b = (Func) vptr_b[0];
        f_a();                      // A::func()
        f_b();                      // B::func()
    }
    
    • 首先定义一个void Func(void )类型的函数指针
    • 将a、b对象强制转换为long类型的指针(8字节),该指针就是vptr,并且指向虚函数表的第一个
    • 然后进行解引用获取地址第一个函数的地址,最后执行。
  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值