着重推荐
本章内容非常不错,深入分析了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++面向对象后,还会继续分析锁、原子类等工具的实现原理。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。