C++到C的等价转换以及用C实现OO (1)

       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 {
private:
    int a;
protected:
    float b;
public:
    char c;
    int l, m, n, o, p, q;
    void mytest(void) { c = 'b'; }
};

void cpp_func(void) {
    CppClass cc;

    cout << "cpp class size is " << sizeof cc << endl;

    *(int*)&cc = 1;             //cc.a = 1;
    *((float*)&cc + 1) = 1.0f;  //cc.b = 1.0f; 
    cc.c = 'a', cc.l = 2, cc.m = 3, cc.n = 4;
    cc.o = 5, cc.p = 6, cc.q = 7;

    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 {
    int a;
    float b;
    char c;
    int l, m, n, o, p, q;
} CStruct;

void mytest(CStruct * cc) {
    cc->c = 'b';
}

void c_func(void)
{
    CStruct cc;

    printf("c struct size is %d/n", sizeof cc);

    cc.a = 1, cc.b = 1.0f, cc.c = 'a';
    cc.l = 2, cc.m = 3, cc.n = 4;
    cc.o = 5, cc.p = 6, cc.q = 7;

    mytest(&cc);
}

   CStruct定义了与CppClass相同的成员函数,mytest函数完成CppClass::mytest相同的功能,只是在调用的时候需要传递一个CStruct*的参数。c_func与cpp_func的功能完全相同,打印CStruct的结构大小,并对每个成员变量赋值,最后再调用mytest函数。
   在main函数中我们先调用cpp_func,然后调用c_func:

extern "C" {
    extern void c_func(void);
}

int main(void) {
    cpp_func();
    c_func();
    return (0):
}

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) {
     .......
    cc.mytest();
lea         ecx,[cc]
call        CppClass::mytest (4190C3h)
}

void CppClass::mytest(void) {
push        ebp 
mov         ebp,esp
sub         esp,0CCh
push        ebx 
push        esi 
push        edi 
push        ecx 
lea         edi,[ebp-0CCh]
mov         ecx,33h
mov         eax,0CCCCCCCCh
rep stos    dword ptr [edi]
pop         ecx 
mov         dword ptr [ebp-8],ecx 
   
c = 'b';
mov         eax,dword ptr [this]
mov         byte ptr [eax+8],62h 
}
pop         edi 
pop         esi 
pop         ebx 
mov         esp,ebp
pop         ebp 
ret

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

 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值