这篇文章继续分析C++虚函数表的内容,以及它的工作原理,即用户代码如何访问虚函数表的内容。
下面C++代码定义了一个类AAAA,main()函数new了一个对象,然后delete对象,我们按照调用顺序分析虚函数表的建立,关联等等操作。
#include <stdio.h>
#include <string>
class AAAA {
private:
long l;
public:
virtual void foo() {}
virtual ~AAAA() {}
};
int main(int argc, char * argv[]) {
AAAA * a = new AAAA();
delete a;
return 0;
}
从main()函数入口,主要有两条指令new一个AAAA对象,然后删除这个对象。
AAAA * a = new AAAA()
new指令生成的汇编指令如下:
movl $16, %edi
call _Znwm # operator new(unsigned long)
movq %rax, %rbx
movq %rbx, %rax
movq $0, (%rax) #set instance buffer to 0
movq $0, 8(%rax) # set instance buffer to 0
movq %rax, %rdi # move instance pointer to %rdi for calling
call _ZN4AAAAC1Ev # AAAA::AAAA()
主要有个三块功能,1. new一个16字节的内存,2. 内存初始化成0,3. 调用构造函数AAAA::AAAA(),即_ZN4AAAAC1Ev。
我们再看构造函数AAAA::AAAA()的代码:
_ZN4AAAAC1Ev: # AAAA::AAAA()
pushq %rbp
movq %rsp, %rbp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movq $_ZTV4AAAA+16, (%rax)
leave
ret
在C++源代码里面,我们并没有为AAAA定义自己的构造函数,所以这个函数是缺省的构造函数,主要功能就一句话,把$_ZTV4AAAA+16的值赋值到对象实例的前8个字节(movq $_ZTV4AAAA+16, (%rax))。
再来看_ZTV4AAAA+16是个什么内容:
_ZTV4AAAA: # vtable for AAAA
.quad 0
.quad _ZTI4AAAA # typeinfo for AAAA
.quad _ZN4AAAA3fooEv
.quad _ZN4AAAAD1Ev
.quad _ZN4AAAAD0Ev
_ZTS4AAAA: # typeinfo name for AAAA
.string "4AAAA"
_ZTI4AAAA: # typeinfo for AAAA
.quad _ZTVN10__cxxabiv117__class_type_infoE+16
.quad _ZTS4AAAA # typeinfo name for AAAA
上述代码都有编译器在翻译类AAAA的时候生成。我们看到_ZTV4AAAA是类AAAA的虚函数表地址,$_ZTV4AAAA+16指向的是虚函数AAAA:::foo()的地址;我们已经知道C++类对象内容的前八个字节是指向类虚函数表的指针,可是此时我们看到它并不是指向虚函数表首地址,而是指向首地址+16的一个偏移,为什么这样做呢?其实+16是第一个虚函数的地址,前面的16字节(+8字节指向类类型信息,+0我也不清楚其用处)保留属于C++类管理内部使用的,对用户而言可以隐藏,所以在使用者的角度看来,虚函数表就是按顺序从头开始排列的(+16偏移开始即可。
总结一句话,缺省构造函数就是把类的虚函数表地址写到类对象的前面8个字节地址。
下面我们看删除一个对象的函数
delete a;
delete指令生成汇编语言代码如下:
movq -24(%rbp), %rax
movq (%rax), %rax
addq $16, %rax
movq (%rax), %rdx
movq -24(%rbp), %rax
movq %rax, %rdi
call *%rdx
这段汇编代码的目的只有一个就是call到%rdx里面去,完成两件事,1.给%rdx找到正确的值,2.找到正确的函数参数。%rdx需要找到的值是析构函数的地址,参数当然是对象本身指针了。
从上述代码我们看到赋给%rdx的值是虚函数表+16的地址,看前面_ZTV4AAAA的定义,(+16)的地址就是指向第三个虚函数的地址,即_ZN4AAAAD0Ev
_ZN4AAAAD0Ev: # AAAA::~AAAA()
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN4AAAAD1Ev
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZdlPv
leave
ret
这个函数主要功能是调用另一个函数 _ZN4AAAAD1Ev
_ZN4AAAAD1Ev: # AAAA::~AAAA()
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movq $_ZTV4AAAA+16, (%rax)
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZdlPv
leave
ret
函数_ZN4AAAAD1Ev是用户定义的析构函数,因为没有具体功能;也不清楚它具体要干什么,只看到最后它调用了一个delete函数。
上述代码可能比较复杂啰嗦,但是我们清楚了一个重要概念,即每一个多态类实例对象的起始地址都是一个指向虚函数表的指针,所有类的虚函数都在这个表中占用一列;这个地址的前面一个指针指向类的类型信息定义,从而从一个对象指针我们就能查到其类类型定义;这也是typeid和dyanmic_cast能够工作的原理。
最后说明一点在类_ZTV4AAAA的虚函数表中定义有两个析构函数,不知道为什么。