OO的思想是一个飞跃,编程思想上的伟大的进步。C++开始出现和发展的时候,在1985年由AT&T发布了第一个商业的C++,被称作Cfront。Cfront并不是C++的编译器,而是从C++转成C后,再用C编译。关于C++的发展简史,可以参考Computer History Museum社区里的一篇文章The C++ Pages,并从那里得到Cfront第一个版本的源代码。
下面我们对C++和C完成相同功能的代码进行比较,仍然采用在VC中反汇编的方法。
首先来看下面这个C++的类和一个函数:
class CppClass { void cpp_func(void) { cout << "cpp class size is " << sizeof cc << endl; *(int*)&cc = 1; //cc.a = 1; cc.mytest(); |
CppClass有一个private成员,一个protected成员,和几个public成员。另外public mytest成员函数简单地令c = 'b'. 函数cpp_func先打印CppClass的size,接着对所有的成员变量赋值,然后再调用CppClass::mytest函数。
因为cc的a和b分别为private和public,所以直接赋值是不可以的,我们采用指针对内存地址直接修改的写法:
*(int*)&cc = 1;
*((float*)&cc + 1) = 1.0f;
我们如果把a和b声明为public,然后用
cc.a=1, cc.b = 1.0f;
这两种方法编译后的代码全都是:
mov dword ptr [cc],1
mov dword ptr [ebp-24h],3F800000h
这样我们有了第一个结论:private和public只在编译时起作用,编译后不起作用。这同时也说明了乱用指针(包括不适当的引用)是危险的。
我们在C语言中定义与CppClass类似的结构和函数:
typedef struct { void mytest(CStruct * cc) { void c_func(void) printf("c struct size is %d/n", sizeof cc); cc.a = 1, cc.b = 1.0f, cc.c = 'a'; mytest(&cc); |
extern "C" { int main(void) { |
extern "C"明确告诉编译器,这是个C连接,不希望进行名称转换,否则连接的时候无法找到c_func。执行的结果是:
cpp class size is 36 c struct size is 36 |
说明CppClass和CStruct的大小是一样的,我们在比较对成员变量赋值那几条语句的汇编代码,我们会发现是完全一致的:
00421DDD mov dword ptr [cc],1 00421DE4 mov dword ptr [ebp-24h],3F800000h 00421DEB mov byte ptr [ebp-20h],61h 00421DEF mov dword ptr [ebp-1Ch],2 00421DF6 mov dword ptr [ebp-18h],3 00421DFD mov dword ptr [ebp-14h],4 00421E04 mov dword ptr [ebp-10h],5 00421E0B mov dword ptr [ebp-0Ch],6 00421E12 mov dword ptr [ebp-8],7 |
甚至与我们使用指针自己计算地址赋值的语句都是完全一致的。查看内存中存放的数据:
01 00 00 00 00 00 80 3f 62 cc cc cc 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00 06 00 00 00 07 00 00 00 |
除了地址不一样,其他完全一样。
到此为止,我们得到了第二个结论:class和struct在内存中的存储是相同的。并且我们还有一个在效率方面的提示:在适当的编译优化选项下,使用->和.的效率是很高的,这与我们使用指针寻址赋值的效率是一致的,而代码的可读性却有极大的差别。我个人认为使用指针做类似*((float*)&cc + 1) = 1.0f的运算还不如汇编代码的可读性高。
我们继续比较CappClass::mytest和mytest的差别。为了不先看那写冗长的汇编代码,我把代码放在最后的附录中。
void c_func(void) { ...... mytest(&cc); lea eax,[cc] push eax call @ILT+1160(_mytest) (41948Dh) add esp,4 } void mytest(CStruct * cc) { push ebp mov ebp,esp sub esp,0C0h push ebx push esi push edi lea edi,[ebp-0C0h] mov ecx,30h mov eax,0CCCCCCCCh rep stos dword ptr [edi] cc->c = 'b'; mov eax,dword ptr [cc] mov byte ptr [eax+8],62h } pop edi pop esi pop ebx mov esp,ebp pop ebp ret |
首先我们看到,
1)在cpp调用的时候,简单地把cc的地址装到ecx寄存器中,因为这个函数并没有参数,所以不需要压栈,然后调用CppClass::test,其代码段的地址为0x4190C3h。
void cpp_func(void) { ....... cc.mytest(); lea ecx,[cc] call CppClass::mytest (4190C3h) } |
2)c调用时,把cc的地址装到eax寄存器中,并压栈,然后调用mytest,代码段的地址为0x41948Dh.
void c_func(void) { ...... mytest(&cc); lea eax,[cc] push eax call @ILT+1160(_mytest) (41948Dh) } |
3) 在执行函数中的第一行语句前,cpp调用比c调用多了三行:
push ecx ...... pop ecx mov dword ptr [ebp-8],ecx |
我们还记得,在cpp调用前,ecx里面装入了cc的地址,换句话说ecx积存器中装入的是class的this指针地址,并把this的地址保存在ebp-8这个地址中。
4)赋值语句,cpp_func中是
mov eax,dword ptr [this] mov byte ptr [eax+8],62h |
而c_func中的代码是是
mov eax,dword ptr [cc] mov byte ptr [eax+8],62h |
二者除了用this变量名(在此是汇编语言中的变量名,不具备C++函数中的意义)代替了cc变量名,其余完全相同。而我们刚才知道this的地址保存在ebp-8,再看c_func运行的时候ebp-8中存放的恰好就是cc的地址!
于是,我们有了结论三:
一个类的成员函数在编译后与一个C函数是相同的,他们都在代码段中,同时并没有private和protected访问限制
关于访问限制的证实,不在此仔细说明了。
在实际的工作中,有些工作还是需要把C++转成C,Cfront也有不断升级,一般的工作还是很少有这种时候。尽管如此,我们还是能够得到一些启发:
1. C++不完全是OO的,是在C和OO之间的一个产品,这也是为什么有时候会让我们比较感觉难以理解的原因之一。
2. 可以用C实现OO,方法就是声明一个结构,并把这个结构看成类,类的成员函数调用的时候,第一个参数(并不一定要在第一个,我建议如此)传递这个结构(类)的指针。这样,我们就可以用C实现OO了。事实上,很多项目都在这么做。
(待续)
附录:
cpp_func和c_func调用mytest的汇编代码
void cpp_func(void) { void c_func(void) { |