着重推荐

本章内容非常不错,深入分析了C++虚函数表、动态绑定、对象切片和静态声明的实现原理,原来这么复杂的C++语法,只是用了几句简单的汇编实现的!非常推荐读一读!

重写:

(overwrite)是指在子类中对父类中的虚函数重新实现、覆盖。即函数名和参数都一样,只是函数的实现体不一样。重写的父类函数必须要有virtual修饰,这样,当一个父类对象的指针或引用,指向子类时,调用该函数时,实际执行的是子类的函数,而不是父类的函数,这就是多态的体现。也就是说,我们不需要通过对象的声明类型去寻找对应的函数,即使声明对象的是父类,它依旧能调用子类中的重写函数。重写主要是为了支持多态,而多态又主要是为了让代码具有更好的可扩展性,即程序需要修改或增加功能时,只需改动或增加较少的代码。

下面是C++的重写代码:

#include<iostream>
/**
 * 继承:重写
*/

class Father
{
public:
    int a;
    virtual void add(){ // 不用virtual,子类只会函数隐藏,不会函数覆盖
        a = -1;
    }
};

class Son : public Father{
public:
    int s;
    void add() override{
        a = 99;
    }
};


int main(){
    Father father;
    father.add();

    Son son;
    son.add();
    
    Son s;
    Father f = s; // 发生对象切片,静态类型决定你调用哪个函数,父类对象调用父类的,子类对象调用子类的,因为只复制s中父类那部分数据到f,子类那部分直接丢弃
    f.add(); // 调用父类的add()

    Father &f1 = s; // 动态绑定,必须用引用或指针才会动态绑定,不会发生对象切片
    f1.add(); //调用子类add()
    // std::cout<<son.a << ", "<< father.a<< std::endl ;
    return 0;
}

从上述注释可以看出,必须要用指针或引用才能达到父类调用子类函数add()的目的,不用指针或引用,就会出现对象切片的问题,导致静态声明类型,决定了你调用哪个函数。

而重写的主要目的就是,不要让对象的声明类型去决定我调用哪个函数,而是让动态绑定的那个对象去决定我要调用哪个函数,也就是说,虽然我对象声明都是用Father类声明的,但是Father &f = son赋值时,用的是son赋值,此时son决定了f.add()会调用son里面的add()。

因此,必须要用指针或引用来达到动态绑定的目的,让静态声明类型无法决定我“能力”,这里的能力就是指能调用哪些函数。

那么,这么优秀的动态绑定功能,是如何用简简单单的汇编实现的呢?

下面看汇编(可先跳过汇编,先看下面分析,再回过头来看汇编的注释):

    .file    "3-object-class-inherit-2.cpp"
    .text
    .section    .rodata
    .type    _ZStL19piecewise_construct, @object
    .size    _ZStL19piecewise_construct, 1
_ZStL19piecewise_construct:
    .zero    1
    .local    _ZStL8__ioinit
    .comm    _ZStL8__ioinit,1,1
    .section    .text._ZN6Father3addEv,"axG",@progbits,_ZN6Father3addEv,comdat
    .align 2
    .weak    _ZN6Father3addEv
    .type    _ZN6Father3addEv, @function
_ZN6Father3addEv:
.LFB1522:
    .cfi_startproc
    endbr64
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp) # 传进来的似乎对象的起始地址
    movq    -8(%rbp), %rax
    movl    $-1, 8(%rax) # 对象起始地址的前8字节是虚函数表,rax+8才是变量a的地址
    nop
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1522:
    .size    _ZN6Father3addEv, .-_ZN6Father3addEv
    .section    .text._ZN3Son3addEv,"axG",@progbits,_ZN3Son3addEv,comdat
    .align 2
    .weak    _ZN3Son3addEv
    .type    _ZN3Son3addEv, @function
_ZN3Son3addEv:
.LFB1523:
    .cfi_startproc
    endbr64
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp)
    movq    -8(%rbp), %rax
    movl    $99, 8(%rax)
    nop
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1523:
    .size    _ZN3Son3addEv, .-_ZN3Son3addEv
    .section    .text._ZN6FatherC2Ev,"axG",@progbits,_ZN6FatherC5Ev,comdat
    .align 2
    .weak    _ZN6FatherC2Ev
    .type    _ZN6FatherC2Ev, @function
_ZN6FatherC2Ev:
.LFB1526:
    .cfi_startproc
    endbr64
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp)
    leaq    16+_ZTV6Father(%rip), %rdx # 虚函数标号+16地址作为虚函数表地址,此处是虚函数标号开始的第3个元素,即虚函数add的地址,将父类Father的虚函数表放到对象起始地址前8个字节
    movq    -8(%rbp), %rax
    movq    %rdx, (%rax)
    nop
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1526:
    .size    _ZN6FatherC2Ev, .-_ZN6FatherC2Ev
    .weak    _ZN6FatherC1Ev
    .set    _ZN6FatherC1Ev,_ZN6FatherC2Ev
    .section    .text._ZN3SonC2Ev,"axG",@progbits,_ZN3SonC5Ev,comdat
    .align 2
    .weak    _ZN3SonC2Ev
    .type    _ZN3SonC2Ev, @function
_ZN3SonC2Ev:
.LFB1529:
    .cfi_startproc
    endbr64
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movq    %rdi, -8(%rbp)
    movq    -8(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN6FatherC2Ev
    leaq    16+_ZTV3Son(%rip), %rdx # 虚函数标号+16地址作为虚函数表地址,此处是虚函数标号开始的第3个元素,即虚函数add的地址,将子类Son的虚函数表放到对象起始地址前8个字节
    movq    -8(%rbp), %rax
    movq    %rdx, (%rax)
    nop
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1529:
    .size    _ZN3SonC2Ev, .-_ZN3SonC2Ev
    .weak    _ZN3SonC1Ev
    .set    _ZN3SonC1Ev,_ZN3SonC2Ev
    .section    .text._ZN6FatherC2ERKS_,"axG",@progbits,_ZN6FatherC5ERKS_,comdat
    .align 2
    .weak    _ZN6FatherC2ERKS_
    .type    _ZN6FatherC2ERKS_, @function
_ZN6FatherC2ERKS_:
.LFB1532:
    .cfi_startproc
    endbr64
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp) # rbp-32 对象f的起始地址
    movq    %rsi, -16(%rbp) # rbp-48 对象s的起始地址

    leaq    16+_ZTV6Father(%rip), %rdx # 虚函数标号+16地址作为虚函数表地址,此处是虚函数标号开始的第3个元素,即虚函数add的地址,将父类Father的虚函数表放到对象起始地址前8个字节
    movq    -8(%rbp), %rax
    movq    %rdx, (%rax) # 这3行汇编代码将 Father 类对象f的 VTable 指针设置为 Father 类的 VTable,为了支持动态绑定
                         # 对象起始地址存放虚函数表的地址,8个字节

    movq    -16(%rbp), %rax 
    movl    8(%rax), %edx # 从这里可看出,对象起始地址前8个字节放的是虚函数表地址,紧接着放成员变量
    movq    -8(%rbp), %rax
    movl    %edx, 8(%rax)
    nop
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1532:
    .size    _ZN6FatherC2ERKS_, .-_ZN6FatherC2ERKS_
    .weak    _ZN6FatherC1ERKS_
    .set    _ZN6FatherC1ERKS_,_ZN6FatherC2ERKS_
    .text
    .globl    main
    .type    main, @function
main:
.LFB1524:
    .cfi_startproc
    endbr64
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $96, %rsp
    movq    %fs:40, %rax
    movq    %rax, -8(%rbp)
    xorl    %eax, %eax

    leaq    -80(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN6FatherC1Ev # 调用Father构造函数

    leaq    -80(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN6Father3addEv # father.add();调用父类add()

    leaq    -64(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN3SonC1Ev # 调用Son构造函数

    leaq    -64(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN3Son3addEv # son.add();

    leaq    -48(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN3SonC1Ev # Son s;调用Son构造函数

    leaq    -48(%rbp), %rdx # 对象s的起始地址
    leaq    -32(%rbp), %rax # 对象Father f中的起始地址
    movq    %rdx, %rsi
    movq    %rax, %rdi
    call    _ZN6FatherC1ERKS_ # 调用拷贝构造函数,给对象f设置Father类的虚函数表指针,并将Son s中的a的值复制到f中的a的内存里

    leaq    -32(%rbp), %rax 
    movq    %rax, %rdi
    call    _ZN6Father3addEv # f.add();调用父类add(),虽然传的是对象的起始地址,但前8字节是虚函数表,函数内部会将地址进行加8操作

    leaq    -48(%rbp), %rax # Son s的起始地址放到rax里
    movq    %rax, -88(%rbp) # Son s的起始地址放到 rbp-88指向的内存处
    movq    -88(%rbp), %rax # 将对象s的起始地址放到rax里
    movq    (%rax), %rax # 将s的前8个字节里的内容放到rax里,即把虚函数表地址放到rax里
    movq    (%rax), %rdx # 读取虚函数表的第1个元素,即第1个虚函数地址
    movq    -88(%rbp), %rax # 将对象s的起始地址放到rax里
    movq    %rax, %rdi # 参数传递,将对象s的起始地址放到rdi里给子函数用
    call    *%rdx # 间接调用了寄存器 %rdx 所存储的地址处的函数,即相当于调用了子类对象中重写的第一个虚函数


    movl    $0, %eax
    movq    -8(%rbp), %rcx
    xorq    %fs:40, %rcx
    je    .L8
    call    __stack_chk_fail@PLT
.L8:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1524:
    .size    main, .-main
    .weak    _ZTV3Son
    .section    .data.rel.ro.local._ZTV3Son,"awG",@progbits,_ZTV3Son,comdat
    .align 8
    .type    _ZTV3Son, @object
    .size    _ZTV3Son, 24
_ZTV3Son: # 子类Son的虚函数表,实际应用时,会将第3个元素的地址赋值给对象前8个字节
    .quad    0 
    .quad    _ZTI3Son # 类型信息(Type Information, TI)指针,指向一个名为 _ZTI3Son 的类型信息对象
    .quad    _ZN3Son3addEv # 虚函数 add 的地址
    .weak    _ZTV6Father
    .section    .data.rel.ro.local._ZTV6Father,"awG",@progbits,_ZTV6Father,comdat
    .align 8
    .type    _ZTV6Father, @object
    .size    _ZTV6Father, 24
_ZTV6Father: # 父类Father的虚函数表,实际应用时,会将第3个元素的地址赋值给对象前8个字节
    .quad    0
    .quad    _ZTI6Father
    .quad    _ZN6Father3addEv
    .weak    _ZTI3Son
    .section    .data.rel.ro._ZTI3Son,"awG",@progbits,_ZTI3Son,comdat
    .align 8
    .type    _ZTI3Son, @object
    .size    _ZTI3Son, 24
_ZTI3Son:
    .quad    _ZTVN10__cxxabiv120__si_class_type_infoE+16
    .quad    _ZTS3Son
    .quad    _ZTI6Father
    .weak    _ZTS3Son
    .section    .rodata._ZTS3Son,"aG",@progbits,_ZTS3Son,comdat
    .type    _ZTS3Son, @object
    .size    _ZTS3Son, 5
_ZTS3Son:
    .string    "3Son"
    .weak    _ZTI6Father
    .section    .data.rel.ro._ZTI6Father,"awG",@progbits,_ZTI6Father,comdat
    .align 8
    .type    _ZTI6Father, @object
    .size    _ZTI6Father, 16
_ZTI6Father:
    .quad    _ZTVN10__cxxabiv117__class_type_infoE+16
    .quad    _ZTS6Father
    .weak    _ZTS6Father
    .section    .rodata._ZTS6Father,"aG",@progbits,_ZTS6Father,comdat
    .align 8
    .type    _ZTS6Father, @object
    .size    _ZTS6Father, 8
_ZTS6Father:
    .string    "6Father"
    .text
    .type    _Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB2014:
    .cfi_startproc
    endbr64
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    cmpl    $1, -4(%rbp)
    jne    .L11
    cmpl    $65535, -8(%rbp)
    jne    .L11
    leaq    _ZStL8__ioinit(%rip), %rdi
    call    _ZNSt8ios_base4InitC1Ev@PLT
    leaq    __dso_handle(%rip), %rdx
    leaq    _ZStL8__ioinit(%rip), %rsi
    movq    _ZNSt8ios_base4InitD1Ev@GOTPCREL(%rip), %rax
    movq    %rax, %rdi
    call    __cxa_atexit@PLT
.L11:
    nop
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2014:
    .size    _Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii
    .type    _GLOBAL__sub_I_main, @function
_GLOBAL__sub_I_main:
.LFB2015:
    .cfi_startproc
    endbr64
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $65535, %esi
    movl    $1, %edi
    call    _Z41__static_initialization_and_destruction_0ii
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

经过分析、注释上述汇编代码,我终于搞清楚了C++动态绑定的实现原理和虚函数表的实现原理。

主要有如下几点:

虚函数表

每个类,都有一个自己的虚函数表,在上述汇编代码里,虚函数表的定义在子类虚函数表_ZTV3Son标号处和父类虚函数表_ZTV6Father标号处,虚函数表里面存储着每个类中,每个虚函数的地址,类似一个一维数组,一个元素为8字节(64位地址)。

对象的虚函数表的设置

每次实例化一个对象,比如Son s为例,汇编里都会先给对象s在栈上分配一段内存,然后调用Son的构造函数,在构造函数里会做一件非常重要的事,即把虚函数表的第3个元素的地址,放到对象s的起始地址开始的前8个字节处(8字节即64位,地址长度),而上述汇编中,虚函数表第3个元素的地址,实际上就是第1个有效虚函数的地址(看注释,第1个元素是无效的0,第2个元素是类型信息),此时,也就给这个对象,设置了它对应的虚函数表地址了。注意,对象的前8个字节都是存储虚函数表地址,后面才开始存成员变量,如果仔细从main()函数开始分析,可以分析出来!

对象切片和拷贝构造函数

 Son s;
 Father f = s; // 发生对象切片,静态类型决定你调用哪个函数,父类对象调用父类的,子类对象调用子类的,因为只复制s中父类那部分数据到f,子类那部分直接丢弃
 f.add(); // 调用的是父类add()函数

在上述C++代码里,有上面3句代码,最后f.add()实际调用的是父类add()函数,因为刚才在赋值时发生了对象切片的现象,把子类中的数据直接丢弃了,可是,为什么会这样,具体在汇编中是怎么发生的?

原来,分析上面的汇编代码得知,在执行Father f = s;时,实际上汇编先给f在main函数的栈帧里分配了内存(main函数的局部变量),然后调用了拷贝构造函数,在拷贝构造函数里,把对象f的前8个字节,设置为了Father类的虚函数表地址,所以说,当你调用f.add()时,肯定调用的就是父类的add()函数,因为虚函数表设置的就是Father的虚函数表!

动态绑定的实现原理

Father &f1 = s; // 动态绑定,必须用引用或指针才会动态绑定,不会发生对象切片
f1.add(); //调用子类add()

上面这2句c++代码,虽然f1的声明类型是Father,但我调用f1.add()时,执行的是子类的add(),而不是父类的add()。

于是我就产生了2个问题:

(1)为什么它和上面不一样,为什么它没有发生对象切片?
(2)当我把上面c++测试代码里面的virtual关键字和override删掉后,发现f1.add()调用的是父类的add()函数!动态绑定现象消失了!怎么又不一样了?为什么virtual能实现让Father声明的对象执行子类的函数,而删掉virtual关键字就不行了?怎么动态绑定现象就消失了?!

带着这2个问题,我们继续分析汇编,原来,就是下面这几句汇编“搞的鬼”,当你用virtual修饰父类函数时,当你用指向子类的父类引用去调用重写函数时,编译器会用子类的虚函数表去寻找重写函数的地址。如果你删掉virtual关键字,直接就没有虚函数表这回事了,编译器就直接让你调用父类的add()函数,可以说又发生对象切片现象了,直接把子类那部分数据又丢掉了!virtual关键字,决定了编译器怎么去编译你的代码!所以,想要实现动态绑定,必须是virtual+指针/引用。【注意,override并不是必须加的,但是加上可以防止你写错重写函数的参数名或参数列表,写错代码编译器会报错,所以最好加上】

下面是动态绑定的关键汇编代码:

    leaq    -48(%rbp), %rax # Son s的起始地址放到rax里
    movq    %rax, -88(%rbp) # Son s的起始地址放到 rbp-88指向的内存处
    movq    -88(%rbp), %rax # 将对象s的起始地址放到rax里
    movq    (%rax), %rax # 将s的前8个字节里的内容放到rax里,即把虚函数表地址放到rax里
    movq    (%rax), %rdx # 读取虚函数表的第1个元素,即第1个虚函数地址
    movq    -88(%rbp), %rax # 将对象s的起始地址放到rax里
    movq    %rax, %rdi # 参数传递,将对象s的起始地址放到rdi里给子函数用
    call    *%rdx # 间接调用了寄存器 %rdx 所存储的地址处的函数,即相当于调用了子类对象中重写的第一个虚函数

仔细阅读上面的注释,会发现,动态绑定,就是使用了子类的虚函数表去寻找重写函数!

总结

通过这一章的分析,我彻底了解了之前模模糊糊的虚函数表概念,并且对动态绑定、对象切片,静态类型声明等概念,有了更深入的了解!所以说,分析汇编还是很有必要的,能通过第一现场了解C++语法特性的实现原理,这件事我会继续做下去,继续分析更多的C++语法特性!后面分析完C++面向对象后,还会继续分析锁、原子类等工具的实现原理。


大白话的技术分享
4 声望0 粉丝

用最通俗易懂的大白话,总结自己学习的知识,顺带分享技术。