以前在书上C++primer和Effective C++看到类似的内容,编写了程序然后输出结果确实是如此,但就是不知道为什么会这样。再者当时对汇编也不是特殊感兴趣,虽然能看懂简单的指令,后来看了斯坦福大学的范式编程公开课,开始对汇编有兴趣了,加上自己对底层很感兴趣,于是有了….
那为什么会重载基类的虚函数会继承默认形参?
举个粟子[为了在一张图上,就紧凑了些]:
通过基类指针或引用指向派生类对象会引发虚机制,P指针类型静态决议,而实际类型动态决议,故p->foo()会被编译器转会为如下形式:(*p->vptr[1])(p),真正调用要到运行时。这里引用虚函数表的第1个索引,第0项是存储类的基础信息,typeinfo类型,通过typeid返回该类型信息;
p->foo()则调用的是CDerived中的foo,直观上输出的是i22j33,但事实并不是如此。咱们还是看看汇编代码怎么转化我们程序的:
182行调用operator new分配sizeof(CDerived),CDerived占用的字节数是一个指向虚函数表的指针[8字节],加上父类的数据成员和子类的数据成员,这里每个成员已经对齐了且总大小也对齐。
= 16个字节[64位],182~183行中,rax保存了this指针的地址,info r rax时this=0x602010,185行esi中为20,即u的值,_ZN8CDerivedC1Ei为CDerived的构造函数【CDerived()-->CBase(),虽然在CDerived中没有显示调用CBase构造函数代码,编译器会为我们扩展CDerived()在用户代码前插入CBase()代码】;
CDerived()构造函数:
402行rbp-8处存放了this= 0x00602010,rbp-c为u=20,405行调用CBase构造函数:
319行的rax存放了this= 0x00602010,查看下x/wx 0x602010,即头四个字节显示的是0x00400c80,这个是什么值呢?下篇博客会介绍[测试机是小端表示法];
320行,x/4wx 0x602010,是:
0x602010:0x00400e50 0x00000000 0x0000000a 0x00000000,也就是this地址的该对象的内容。
321~323行重新赋值了this地址的内容:
0x602010:0x00400e10 0x00000000 0x0000000a 0x00000014,由0x00400c80变为0x00400c60,具体是什么下篇详细介绍,
195~197行分别是:rax=this,rax=0x602010,rcx=0x400bf6,rcx就是虚函数foo的地址了,后三行是准备两个默认形参值和this参数[每一个类的非static成员函数都会被编译器name mangling为唯一的名称,故会隐含一个const this指针,不可修改这个this的值],198行调用foo:
这里也没什么好讲的,就调用_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc_ZNSolsEi打印结果,所以打印了i1j2,并不是i22j33,即默认形参继承了基类的。
而o.foo(),则不会引发虚机制,并不会被转化成(*o.vptr[1])(&o),直接在编译期间静态决议为:_ZN8CDerived3fooEii(&o),使用c++filt则显示的是:CDerived::foo(int, int),那么打印的是i22j33,对应的汇编代码如下:
201~204行直接在栈上构造对象,获得o对象地址,然后调用构造函数CDerived,作用同上;
206~210是为准备调用foo时的参数,这里没有引发虚机制,所以打印的是i22j33。
添加了delete p; p = NULL;语句后[反汇编和GDB查看有些数值变化了,和上面不对应]:
195~196行比较p是否为空,rbp-18处的是this,je[jump equal]是的话就跳转到400a0c处,不为空的话,197~200分别是rax=this=0x602010,(rax)= 0x00400d70 [虚函数表],(rax) + 0x10为虚函数表中第二个虚函数[不知为啥+0x10],x/wx 0x400d80即为0x400d80 <_ZTV8CDerived+32>:0x00400c0e,x/xw 0x00400c0e即为0x400c0e: 0xe5894855; 然后顺便查看下x/wx 0x400d70为0x400d70 <_ZTV8CDerived+16>: 0x00400b44;201~203是准备调用0x400c0e [虽然虚函数表第二个索引是派生类的析构函数,但真正调用是发生在0x400bd4处的,不是太明白]:
418行调用~CDerived():
399行时:0x602010:0x00400d70 0x00000000 0x0000000a 0x0000001,进入到400af0后:
309行后:0x602010:0x00400db0 0x00000000 0x0000000a 0x00000014
在调用析构函数时会设置相应虚函数表,这么做的原因是防止在虚析构函数中调用虚函数,为了正确作出决议,在这里设置好了虚函数指针;剩余的是跳转到400b1c,然后再跳转,最后operator
delete(void*);
205行把P赋值为0;
栈上的o对象也是一样的逻辑,在退出main前会自动调用其析构函数。
由于在对象构造期间,是由最下层的构造函数往上调用,故先构造基类实例,此时实例并不完整,很多都没有配置妥当,比如上面先后赋值了this的虚函数指针,这里只是单继承。所有调用虚函数都会被决议为当前的,否则会下降到下层使用了未初始化的成员变量导致未定义行为。析构函数调用先构造最低层的,所以任何上层引用下层的行为都未定义,所以会被决议为当前的。
这里只是简单的列出编译器是如何转化我们的程序的,可能在背后调整了用户代码,后期分析派生类实例赋值给基类实例,虚函数指针是如何正确赋值的,从汇编语言层面揭露用户代码怎么被转化的。
网友评论