原题: 如下代码,程序将输出什么结果?
1 #include <iostream> 2 3 using namespace std; 4 5 class Base{ 6 7 public: 8 9 Base(){ } 10 11 virtual int print(int a=10){ 12 13 cout<<"Base :"<<a<<endl; 14 15 return 0; 16 17 } 18 19 }; 20 21 class Derived:public Base{ 22 23 public: 24 25 Derived():Base(){ } 26 27 virtual int print(int a=20){ 28 29 cout<<"Derived :"<<a<<endl; return 0; 30 31 } 32 33 }; 34 35 int main(int argc, char* argv[]) 36 37 { 38 39 Base *p = new Derived(); 40 41 p->print(); 42 43 delete p; 44 45 return 0; 46 47 }
- A. Base :10
- B. Base :20
- C. Derived :10
- D. Derived :20
相信对虚函数机制有点了解的人更可能会选择D,因为答案很显然,虽然p是Base指针,但是p指向的是一个Derived对象,所以肯定调用的是Derived的print函数。(当然要是你知道正确答案,可以看看后面关于程序汇编的解释,要是你都会......绕道吧)
但是答案很出乎意料,不是D,更不可能是A,正确答案是C!是不是很神奇,因为怎么都不可能是这个答案啊!难不成是出题的人搞错了?我开始也深表怀疑,但是不论是在g++还是VC,这个程序的答案就是 “Derived :10”。
开始我很疑惑,然后开始调试,但是无论怎么调试每当执行 p->print()时,vc++都是直接将一个0ah(也就是10) 的值直接传入函数中。我甚至认为这当中有着更为隐蔽c++实现机制。为了能够搞明白这个问题,于是我就用cl.exe将这段代码翻译成汇编语言,为了简便起见,我将其中的头文件以及cout删去了。生成汇编代码的cl命令为“cl /FA test.cpp”。 由于生成的代码很长,所以我就截取其中的main函数的部分:(完整代码见文章末尾)
其它的代码就不用关注了,重点在于Line 22行,其中竟然是直接 push 10 ! 也就是说编译器翻译成汇编代码之后直接把一个常量 10 压栈了, (mov指令的注释是翻译后的代码,现在不需要关心-4[ebp]是什么):
; Line 22 push 10 ; 0000000aH mov ecx, DWORD PTR _p$[ebp] ; mov ecx,-4[ebp] mov edx, DWORD PTR [ecx] ; mov edx,[ecx] mov ecx, DWORD PTR _p$[ebp] ; mov ecx,-4[ebp] mov eax, DWORD PTR [edx] ; mov eax,[edx] call eax
接下来的四行mov指令无非是获得print函数的真正地址,然后调用该函数,总而言之,你什么动态调用都没用,编译器已经认定这个值就是10了,说明使用默认形参的函数实际上就是一个函数,当你用默认值的时候编译器就替你把这个常量压栈。
所以说答案很明确了,我们平时说什么多态其实就是程序在执行过程中可以根据指针指向的类的类型(该类的信息存在一个叫虚函数表(virtual table,vtbl)中)来动态决定到底调用哪一个函数,函数的参数是静态确定的,而当静态确定时,指针p正是Base类型,所以编译器自然用了Base中print函数的默认实参 a=10 了 。
因此不论你的Base中的形参名是a还是d,编译器总是会把这个形参的默认值提取作为常量交给被调用的函数。
实际上《C++ Primer》以及《Effective C++》都讨论过这个问题,这并不是说是c++的错误,而是一种权衡时间与空间上面效率的决定,所以《c++ primer》一书建议virtual函数不应该使用默认形参。
下面索性解释一遍mian函数的调用 (完整的_main函数汇编代码,附有解释)
_main PROC ;File d:\我的文档\visual studio 2010\projects\object_test\object_test\test.cpp ; Line 20 push ebp ;将ebp压栈,保护ebp mov ebp, esp ;将当前栈顶指针保存到ebp中,恢复时可以使用 mov esp,ebp 快速恢复栈顶指针,不用再计算大小了 ;ebp此时代表的是栈顶的地址,之后分配的形参都与该地址有一个固定的差值,自动变量便是分配在这段区域 sub esp, 16 ;将esp减了16位,也就是说分配了16字节区域存储自动变量 ;Line 21 push 4 ; ??2@YAPAXI@Z 是一个用于分配内存的函数,相当于malloc,压栈的4就是要求分配的空间的大小 call ??2@YAPAXI@Z ; operator new , new 也就是 ??2@YAPAXI@Z 函数 add esp, 4 ; 因为调用了 ??2@YAPAXI@Z ,该函数是 _cdecl方式,因此需要主动恢复栈顶指针位置 mov DWORD PTR $T2603[ebp], eax ; 即 mov -8[ebp],eax ,因为 new 返回的是堆区的地址,所以说 -8[ebp]就是返回的地址 cmp DWORD PTR $T2603[ebp], 0 ; 用于比较 返回的地址是不是0,因为new 分配内存失败会返回0 je SHORT $LN3@main ; 如果 分配地址失败就跳到 $LN3@main 处,见下 mov ecx, DWORD PTR $T2603[ebp] ; mov ecx,-8[ebp] 如果 分配成功就把返回的堆区地址交给ecx call ??0Derived@@QAE@XZ ; Derived::Derived 此时显然是在调用 Derived的构造函数,ecx就该地址 ; 实际上调用 类的非静态成员函数时都需要传入一个this指针,传递都是采用ecx寄存器传递 mov DWORD PTR tv72[ebp], eax ; mov -16[ebp],eax 将eax 赋给 -16[ebp], 查看上述代码可知eax是this指针 jmp SHORT $LN4@main ; 构造函数执行成功,因此 跳到 $LN4@main $LN3@main: mov DWORD PTR tv72[ebp], 0 ; 即 mov -16[ebp],0 此时内存分配失败,就 把 0 赋给 -16[ebp] $LN4@main: mov eax, DWORD PTR tv72[ebp] ; mov eax,-16[ebp] 也就是说 -16[ebp] 就是一个临时的this指针存放处 mov DWORD PTR _p$[ebp], eax ; mov -4[ebp],eax 这两行的目的是将-16[ebp]赋给-4[ebp],而-4[ebp]正是指针p ; ;Line 22 push 10 ; 0000000aH mov ecx, DWORD PTR _p$[ebp] ; mov ecx,-4[ebp] 将指针p的值传给ecx mov edx, DWORD PTR [ecx] ; mov edx,[ecx] 此时edx 即 指针p 指向的地址的值,该值实际上指向一个虚函数表 mov ecx, DWORD PTR _p$[ebp] ; mov ecx,-4[ebp] mov eax, DWORD PTR [edx] ; mov eax,[edx] [edx]即虚函数表的地址,所以eax正是虚函数表地址 call eax ; 调用 eax 所代表的地址,这一点涉及到虚函数实现机制 ; ;Line 23 mov ecx, DWORD PTR _p$[ebp] ; 根据下文可以推断的出 _p$[ebp] (-4[ebp])就是指针地址,所以ecx是指针的值 mov DWORD PTR $T2606[ebp], ecx ; 这里可能有些混乱,主要原因是类只有一个虚函数表,所以类的地址与虚函数表的地址是一样的 mov edx, DWORD PTR $T2606[ebp] ; push edx ; edx是指针p的值,即Derived对象在堆区的地址 call ??3@YAXPAX@Z ; operator delete , 该函数仅需要一个堆区的地址即可 add esp, 4 ; Line 24 xor eax, eax ; Line 25 mov esp, ebp ; 函数退出 pop ebp ret 0 _main ENDP