飘云阁

 找回密码
 加入我们

QQ登录

只需一步,快速开始

查看: 1885|回复: 5

[C/C++] 类思想及其实现小结

[复制链接]

该用户从未签到

发表于 2010-1-20 11:41:54 | 显示全部楼层 |阅读模式
I  类的出现及其编译原则



1.1 模块化程序设计的内存模型

    类是对数据及其对相关操作的一个封装,其思想就是将我们所写的结构体及其操作结构体的方法封装到一起,因为我们所写的程序基本上就是在做一个数据处理的操作。将数据及其方法封装之后,会使我们的程序更具逻辑性、结构更清晰,同时封装也更能促进程序的分工合作。

    而类中的成员方法如何调用是让大家困惑的一个问题,这里我们站在编译的角度来思考可能就更能理解类的内部实现。

测试代码:

class Test
{
private:
        int m_a;
        int m_b;
public:
        Test(int a,int b):m_a(a),m_b(b){}
        virtual ~Test(){}
};

int main(int argc, char* argv[])
{
        Test tt(10,20);
        return 0;
}


    在结构体时代,其结构体数据操作的方法(函数)是与结构体数据在逻辑上分离的,即各个函数都是独立编译的,编译器在进行编译时,将每个函数都进行模块化编译。

001.JPG

    这里我们先用一个实例代码,说明一下普通函数的编译思想:
    编译器一定要将我们的函数进行模块化编译,模块化编译需要解决的就是解决参数及局部对象的处理,限于寄存器数量的有限性,于是CPU设计时引出了栈结构。
    实现上就是我们调用函数前,将参数从右向左依次压入堆栈,如上图中的PUSH 参数2,PUSH 参数1。当调用函数时顺便把call函数下一行代码的地址压入堆栈,就跟我们现在正在看书,然后电话响了,我们接完电话继续看书时得知道之前读到哪了,这里的压入下一行代码地址就是这个目的。
    进到函数之后,为了方便对参数的寻址,我们便使用了EBP这个寄存器来进行对参数的操作,在使用该寄存器之前,要先将EBP的数据进行保存,使用完了再对其恢复。所以有了一个PUSH EBP 的操作,然后MOV EBP,ESP之后,EBP就指向了当前的栈顶,此时[EBP+8]、[EBP+0C] …… 指向的就是我们压入的参数了。
    函数要想模块化,首先解决对了参数的处理,然后就是要解决局部变量的空间问题。这里的解决方案就是在编译时候,先统计好所有的局部变量所需的空间,然后再EBP上方的堆栈中申请空间(也就是我在C中所说的定义变量就是申请空间)。所以用[EBP-4]、[EBP-8] …… 就可以寻址到函数内定义的局部变量了。这样就解决了我们的模块化编译的问题。

   类思想引入之后,首先也必须要实现模块化编译,否则类就变成累赘了哈~~

   类的对象空间的大小为类内成员的大小(有虚方法存在时,前四个字节为指向虚表的指针),那就说明类对象根本就跟类方法在编译上是独立的,也就是说,我们在理解上就可以将类成员和类方法独立开,类方法就是一普通的函数,而在设计上将其封装聚合。而类方法的参数中,大家都知道默认带了一个隐藏的this指针,下面我们来揭开this指针的真面目。

我们以构造函数的调用为例:

20:       Test tt(10,20);
00401038   push        14h
0040103A   push        0Ah
0040103C   lea         ecx,[ebp-0Ch]      // 这里[EBP-C]就是main函数为局部变量所申请的空间(就是对象tt空间)
0040103F   call        @ILT+0(Test::Test) (00401005)  // 这里的this指针的这个参数没有压栈而是通过寄存器ECX传参

11:       Test(int a,int b:m_b(b),m_a(a){}
00401070   push        ebp
00401071   mov         ebp,esp
00401073   sub         esp,44h
…… ……
0040108A   mov         dword ptr [ebp-4],ecx  // [EBP-4]作为一个局部临时变量 保存this指针
0040108D   mov         eax,dword ptr [ebp-4]  // this指针保存到eax寄存器
00401090   mov         ecx,dword ptr [ebp+8]  // 参数 a
00401093   mov         dword ptr [eax+4],ecx  // 将参数 a 保存到 对象指针的第四个字节位置(前四个字节为虚表)
00401096   mov         edx,dword ptr [ebp-4]
00401099   mov         eax,dword ptr [ebp+0Ch]
0040109C   mov         dword ptr [edx+8],eax
// 设置虚表指针
0040109F   mov         ecx,dword ptr [ebp-4]
004010A2   mov         dword ptr [ecx],offset Test::`vftable' (0042210c)
004010A8   mov         eax,dword ptr [ebp-4]
…… ……
004010AE   mov         esp,ebp
004010B0   pop         ebp
004010B1   ret         8

    通过上边的代码,我们可以得出,类成员函数默认带一个this指针的参数,该参数通过ECX寄存器传参(以后再看到那个函数调用有ECX传参并在函数内为ECX寻址就可以得出该函数为类方法),用ECX这个类对象的基地址的相对偏移来对对象的成员进行操作。这样就可以实现模块化编译,无论哪个对象调用该函数,只需要将对象的地址送到ECX寄存器即可。编译器用这样的编译方案就实现了函数的模块化编译,所以说类思想是在逻辑上将成员及其方法进行封装,而在实现上仍然是模块化编译来实现。这也就印证了为何类对象的空间仅为该对象数据和的大小,而没有添加类似方法的指针等内容。

  通过上方的代码,我们还可以得出,在类的构造函数中,先对参数列表进行复制,然后填充虚表指针,最后才执行构造函数的指令。
构造函数()
{
  执行初始化类表
  填充虚表指针(若存在虚函数)
  执行函数代码
}

    我们只要弄清楚了函数如何实现模块化编译,如何进行传参及变量的赋值,就可以看到C源码而联想到其汇编代码,看到汇编反推出其C代码,这些是走向逆向的基础。




1.2 类在逻辑上封装,编译上独立

测试代码:
//-----------------------------------------------
struct Test1
{
public:
        int m_a;
        int m_b;
};

void SetData(Test1 * pObj,int a,int b)
{
        pObj->m_a = a;
        pObj->m_b = b;
}

//----------------------------------------------

class Test2
{
public:
        int m_a;
        int m_b;
        void SetData(int a,int b)
        {
                m_a = a;
                m_b = b;
        }
        void Show(){}
};
//-----------------------------------------------

int main()
{
        struct Test1 tt1;
        class  Test2 tt2;
        printf("%d %d",sizeof(tt1),sizeof(tt2));
        reutrn 0;
}


    运行结构都是8个字节,类机制并未因为包含了成员函数而使其体积变得臃肿,那当我们调用其成员函数时,编译器如何定位该成员函数的位置呢?又如何对成员数据进行操作,将所谓的 this 指针传入到参数中呢?那我们调用一下成员方法看一下其编译情况:

42:       tt2.SetData(10,20);
00401089   push        14h
0040108B   push        0Ah
0040108D   lea         ecx,[ebp-10h]                                // 将对象指针(地址)通过 ECX寄存器 传入函数
00401090   call        @ILT+15(Test2::Show) (00401014)        // 成员函数的调用是一个确定的地址,并非通过对象来获取

27:       void SetData(int a,int b)
28:       {
0040D740   push        ebp
0040D741   mov         ebp,esp
0040D743   sub         esp,44h
……
0040D75A   mov         dword ptr [ebp-4],ecx                        // this 指针,对成员数据的操作都通过(this指针 + 偏移)来操作
29:           m_a = a;
0040D75D   mov         eax,dword ptr [ebp-4]
0040D760   mov         ecx,dword ptr [ebp+8]
0040D763   mov         dword ptr [eax],ecx
30:           m_b = b;
0040D765   mov         edx,dword ptr [ebp-4]
0040D768   mov         eax,dword ptr [ebp+0Ch]
0040D76B   mov         dword ptr [edx+4],eax
31:       }
……
0040D771   mov         esp,ebp
0040D773   pop         ebp
0040D774   ret         8

43:       SetData(&tt1,10,20);
00401085   push        14h
00401087   push        0Ah
00401089   lea         eax,[ebp-8]
0040108C   push        eax                                                // 这里直接将结构体变量地址压入堆栈
0040108D   call        @ILT+0(SetData) (00401005)                // 函数的调用是一个确定的地址


    通过查看编译后的代码,我们可以得出:类方法的调用不需要通过类对象来寻址(这里不考虑虚表)。类方法的寻址跟普通函数一样,就是CALL一个固定的地址。其解决对类对象数据操作的方案就是将对象的指针通过ECX寄存器来传参,非成员函数是将参数压栈而已。
    这也印证了类对象的大小等于其类数据大小的总和。由此可以得,类思想是在逻辑上进行封装,在编译上独立,我们清楚了类方法如何处理类对象数据后,可以认为类方法跟普通函数没有什么两样。




1.3  new 和 delete 的运算符重载

    若类中没有定义构造函数,创建对象时编译器并不会为我们添加一个默认的构造函数,若类中未定义析构对象退出作用域时候也不会为我们添加一个析构函数来调用。因为此时的对象就相当于C语言中结构体所定义的变量,没必要去构造和析构。

    下面我们看一下当我们使用 new 和 delete 来处理类对象时,new 和 delete 在编译中均被做了运算符重载,其中调用了C语言的 malloc 和 free 函数,下面将其情况简单描述:

1. 若我们的类中存在构造函数,new 指令编译时,先调用 operator new 来申请空间,将返回值作为 this 指针来调用构造函数。
2. 若没写构造函数,将省去调用构造函数这一步。
3. 若类中未定义析构函数,执行 delete 语句仅是调用 operator delete 函数来调用 free 函数释放其空间。
4. 当 new class[] 创建对象时,将调用 eh vector constructor iterator 方法来生成 Vector 迭代器保存对象数组。
5. 若析构函数为虚函数,vector deleting destructor (向量删除函数)将作为虚表中的一项(析构函数并非添加进虚表)。此时我们无论调用delete 还是 delete[] 都将直接调用续表第一项进行处理,该函数内部有一个分析,若参数为1(参数为对象的个数),则直接调用析构,然后operator delete释放空间,若大于1,将调用 eh vector destructor iterator(向量删除迭代器),从后往前依次析构向量中所有的对象。在对空间进行释放。

   new class() 和 delete 及 new class[] 和 delete[] 都被该类进行运算符重载,前一种情况简单,我们重点分析一下第二种情况:

测试代码(析构函数非虚函数):

测试代码:

int main(int argc, char* argv[])
{
        Test * pObj = new Test();
        Test * pObj1 = new Test[3];
        delete pObj;
        delete[] pObj1;
        return 0;
}


new class[] 运算符的重载

004010BD   push        1Ch                     // 注意这里申请的空间为 sizeof(dword) + n*sizeof(class)
004010BF   call        operator new (004014c0)  // 先调用 operator new 申请空间

004010C7   .  8945 D4       MOV DWORD PTR SS:[EBP-2C],EAX             ;  003907e0 多申请了四个字节的空间
004010CA   .  C645 FC 02    MOV BYTE PTR SS:[EBP-4],2
004010CE   .  837D D4 00    CMP DWORD PTR SS:[EBP-2C],0
004010D2   .  74 2E         JE SHORT testnewd.00401102
004010D4   .  68 0A104000   PUSH testnewd.0040100A                    ;  析构
004010D9   .  68 19104000   PUSH testnewd.00401019                    ;  构造
004010DE   .  8B55 D4       MOV EDX,DWORD PTR SS:[EBP-2C]
004010E1   .  C702 03000000 MOV DWORD PTR DS:[EDX],3                  ;  这里把数量放在前四个字节
004010E7   .  6A 03         PUSH 3
004010E9   .  6A 08         PUSH 8
004010EB   .  8B45 D4       MOV EAX,DWORD PTR SS:[EBP-2C]             ;  所申请空间地址
004010EE   .  83C0 04       ADD EAX,4
004010F1   .  50            PUSH EAX                                  ;  push 申请空间+4的字节
004010F2   .  E8 29030000   CALL testnewd.??_L@YGXPAXIHP6EX0@Z1@Z

申请到的空间 前四个字节存放申请对象的个数

调用 XXXX 函数
0040146B   mov         ecx,dword ptr [ebp+8]
0040146E   call        dword ptr [ebp+14h]

再调用为对象循环调用构造的函数:

0040145A   mov         eax,dword ptr [ebp-1Ch]
0040145D   add         eax,1
00401460   mov         dword ptr [ebp-1Ch],eax
00401463   mov         ecx,dword ptr [ebp-1Ch]
00401466   cmp         ecx,dword ptr [ebp+10h]
00401469   jge         `eh vector constructor iterator'+5Ch (0040147c)
0040146B   mov         ecx,dword ptr [ebp+8]
0040146E   call        dword ptr [ebp+14h]
00401471   mov         edx,dword ptr [ebp+8]  // 这里调用构造的顺序是从前向后
00401474   add         edx,dword ptr [ebp+0Ch]
00401477   mov         dword ptr [ebp+8],edx
0040147A   jmp         `eh vector constructor iterator'+3Ah (0040145a)


delete[] 运算符的重载: Test::`vector deleting destructor'

析构 delete[]

0040121A   mov         dword ptr [ebp-4],ecx
0040121D   mov         eax,dword ptr [ebp+8]  // 申请对象的个数
00401220   and         eax,2
00401223   test        eax,eax
00401225   je          Test::`vector deleting destructor'+5Fh (0040125f)
00401227   push        offset @ILT+5(Test::~Test) (0040100a)
0040122C   mov         ecx,dword ptr [ebp-4]  // 空间的起始地址
0040122F   mov         edx,dword ptr [ecx-4]  // 申请对象的个数
00401232   push        edx
00401233   push        8
00401235   mov         eax,dword ptr [ebp-4]
00401238   push        eax
00401239   call        `eh vector destructor iterator' (004019f0)  // 从后向前循环调用所有对象的析构函数
0040123E   mov         ecx,dword ptr [ebp+8]
00401241   and         ecx,1
00401244   test        ecx,ecx
00401246   je          Test::`vector deleting destructor'+57h (00401257)
00401248   mov         edx,dword ptr [ebp-4]
0040124B   sub         edx,4
0040124E   push        edx
0040124F   call        operator delete (00401390)                    // 然后再去调用 operetor delete 去释放空间

`eh vector destructor iterator' (004019f0) 函数内部:

00401A30   mov         edx,dword ptr [ebp+10h]  // 取一共申请了多少个
00401A33   sub         edx,1
00401A36   mov         dword ptr [ebp+10h],edx
00401A39   cmp         dword ptr [ebp+10h],0
00401A3D   jl          `eh vector destructor iterator'+60h (00401a50)
00401A3F   mov         eax,dword ptr [ebp+8]     // 这里是从后往前析构
00401A42   sub         eax,dword ptr [ebp+0Ch]   // 一个对象的长度
00401A45   mov         dword ptr [ebp+8],eax
00401A48   mov         ecx,dword ptr [ebp+8]
00401A4B   call        dword ptr [ebp+14h]    // 开始调用析构
00401A4E   jmp         `eh vector destructor iterator'+40h (00401a30)

当析构函数为虚函数时,调用的方法将调用虚表中对应的函数指针,调用类似如下:

0040112F   mov         eax,dword ptr [ebp-34h]  // this 指针
00401132   mov         edx,dword ptr [eax]      // 虚表给 EDX
00401134   mov         ecx,dword ptr [ebp-34h]  // this 指针赋值给 ECX
00401137   call        dword ptr [edx]          // 调用虚表第一项

总结: 假设类中存在构造和析构函数,当new单个对象时,将先申请空间再调用构造函数;delete时先调用析构再调用释放空间;
当new多个对象时,先申请n*sizeof(class)+sizeof(dword)个空间,然后从第一个对象开始依次调用构造函数,delete[]时,从最后一个对象依次向前调用析构函数,最后再释放空间。

再分析一下两个关键函数:

eh vector constructor iterator 传参分析:

004012B4   push        offset @ILT+5(Test::~Test) (0040100a)
004012B9   push        offset @ILT+30(Test::Test) (00401023)
004012BE   mov         edx,dword ptr [ebp-2Ch]  // 将申请空间地址送EDX
004012C1   mov         dword ptr [edx],3        // 将申请对象个数保存到前四个字节
004012C7   push        3
004012C9   push        8
004012CB   mov         eax,dword ptr [ebp-2Ch]
004012CE   add         eax,4
004012D1   push        eax
004012D2   call        `eh vector constructor iterator' (00401700)

堆栈数据:
0012FED0   0039089C  // 所申请空间地址 + 4
0012FED4   00000008  // 一个对象的大小
0012FED8   00000003  // 对象个数
0012FEDC   00401023  // 对象构造函数
0012FEE0   0040100A  // 对象析构函数


eh vector destructor iterator 函数传参分析:

00401407   push        offset @ILT+5(Test::~Test) (0040100a)
0040140C   mov         ecx,dword ptr [ebp-4]
0040140F   mov         edx,dword ptr [ecx-4]
00401412   push        edx
00401413   push        8
00401415   mov         eax,dword ptr [ebp-4]
00401418   push        eax
00401419   call        `eh vector destructor iterator' (00401c90)

函数参数(堆空间地址,对象大小,对象个数,对象析构函数地址)



1.4 当参数为对象和返回值为对象时的分析及编译器对其的优化操作


测试代码:

void SetData(Test obj)
{
}

Test GetData()
{
        static Test obj;
        return obj;
}

int main(int argc, char* argv[])
{
        Test tt;
        SetData(tt);
        GetData();
        return 0;
}




情形一、当参数为对象时的构造和析构

21:       Test tt;
004010BD   lea         ecx,[ebp-18h]                        // tt对象的地址在 [EBP-18H]                       
004010C0   call        @ILT+30(Test::Test) (00401023)
004010C5   mov         dword ptr [ebp-4],0
22:       SetData(tt);
004010CC   sub         esp,0Ch                                // 在堆栈中申请新空间
004010CF   mov         ecx,esp                                   // 将新地址作为系统生成的构造函数的this指针
004010D1   mov         dword ptr [ebp-1Ch],esp
004010D4   lea         eax,[ebp-18h]                        // 将源数据以参数压入堆栈
004010D7   push        eax
004010D8   call        @ILT+0(Test::Test) (00401005)
004010DD   mov         dword ptr [ebp-24h],eax                // 把临时对象的this指针保存到[EBP-24]中       
// 注意:此时栈顶地址就是临时对象的地址 即this指针
004010E0   call        @ILT+20(SetData) (00401019)
004010E5   add         esp,0Ch

系统生成的构造函数:@ILT+0(Test::Test) (00401005)

00401159   pop         ecx
0040115A   mov         dword ptr [ebp-4],ecx             // 先将this指针放到局部变量[EBP-4]中
0040115D   mov         eax,dword ptr [ebp-4]                // 用eax寻址
00401160   mov         ecx,dword ptr [ebp+8]                // 取出参数
00401163   mov         edx,dword ptr [ecx+4]                // 跳过虚表
00401166   mov         dword ptr [eax+4],edx                // 跳过虚表向目标对象赋值,为this指针的对象赋值  
00401169   mov         eax,dword ptr [ebp-4]
0040116C   mov         ecx,dword ptr [ebp+8]
0040116F   mov         edx,dword ptr [ecx+8]
00401172   mov         dword ptr [eax+8],edx
00401175   mov         eax,dword ptr [ebp-4]
00401178   mov         dword ptr [eax],offset Test::`vftable' (0042501c)  // 为虚表赋值
0040117E   mov         eax,dword ptr [ebp-4]                // 将this指针保存到eax中

SetData中析构该对象:@ILT+20(SetData) (00401019)

00401068   lea         ecx,[ebp+8]                        // 上文中讲到为什么这里是临时对象的this指针
0040106B   call        @ILT+5(Test::~Test) (0040100a)

思考:这里显然是调用了一个拷贝构造函数,那为何编译器在我们没有定义构造和析构的情况下都懒的这两个函数,却好心在我们没有定义拷贝构造时为我们生成一个按位赋值的拷贝构造呢?并且当我们定义了拷贝构造后编译器将不再为我们生成该函数。
假设我们类成员中有一指针变量,拷贝构造时也按位进行了Copy,而出函数时临时对象进行了析构,释放了该指针指向的数据,而当这个类对象处作用域时在自动调用析构将会怎样呢?释放指针指向的空间时程序必定崩溃。

我们将所有的变量都思考为对象,即万物都为对象,我们看下方代码:
int i = 1;
int j = i;
第二句指令同样也可以写成 int j(i); // 该赋值就相当于就执行了一个拷贝构造
当普通变量做参数时,执行函数时需要压入堆栈,参数为对象时也不例外,只是该情况时编译器使用了一个小技巧来实现压栈操作,如上例中的:
004010CC   sub         esp,0Ch         // 在堆栈中为参数申请新空间



情况二:当函数返回值为类类型时的情况

29:       GetData();
0040128C   lea         eax,[ebp-24h]   // 用于接收函数返回值的空间 尽管我们并未接受返回值 编译器还是为其预留空间
0040128F   push        eax             // 将返回空间作为参数压入堆栈
00401290   call        @ILT+35(GetData) (00401028)
00401295   add         esp,4           // 自动平衡堆栈
00401298   lea         ecx,[ebp-24h]   // 用完返回值后 析构该临时对象
0040129B   call        @ILT+5(Test::~Test) (0040100a)


16:       return obj;
004010F3   push        offset __cfltcvt_tab+0AB8h (0042ae70) // static对象地址
004010F8   mov         ecx,dword ptr [ebp+8]                 // 压入的返回值指针
004010FB   call        @ILT+0(Test::Test) (00401005)         // 又调用了Copy构造


情况三: 编译器优化情况

class Student
{
        char * m_pName;
public:
        Student(char * str){ cout<<this<<"这里是构造函数!"<<endl; }
        Student(const Student& obj){ cout<<this<<"这里是拷贝构造!"<<endl; }
        ~Student(){ cout<<this<<"这里是析构函数!"<<endl; }
};

void Fn(Student& obj)
{
}

int main(int argc, char* argv[])
{
        Student& obj = Student("HaNi");
        Student st = Student("Jeney");
        Fn(Student("Hi"));
        return 0;
}

0x0012FF6C这里是构造函数!
0x0012FF68这里是构造函数!
0x0012FF64这里是构造函数!
0x0012FF64这里是析构函数!
0x0012FF68这里是析构函数!
0x0012FF6C这里是析构函数!
Press any key to continue

45:       Student& obj = Student("HaNi");
004012AD   push        offset string "HaNi" (00428028)
004012B2   lea         ecx,[ebp-14h]
004012B5   call        @ILT+45(Student::Student) (00401032)
004012BA   mov         dword ptr [ebp-4],0
004012C1   lea         eax,[ebp-14h]
004012C4   mov         dword ptr [ebp-10h],eax
46:       Student st = Student("Jeney");   // 这里不再生成无名对象再进行拷贝构造,而是直接优化掉
004012C7   push        offset string "Jeney" (00428020)
004012CC   lea         ecx,[ebp-18h]
004012CF   call        @ILT+45(Student::Student) (00401032)
004012D4   mov         byte ptr [ebp-4],1
47:       Fn(Student("Hi"));
004012D8   push        offset string "Hi" (00428020)
004012DD   lea         ecx,[ebp-1Ch]
004012E0   call        @ILT+45(Student::Student) (00401032)  // 自己先创建一个临时对象
004012E5   mov         dword ptr [ebp-24h],eax
004012E8   mov         ecx,dword ptr [ebp-24h]
004012EB   mov         dword ptr [ebp-28h],ecx
004012EE   mov         byte ptr [ebp-4],2
004012F2   mov         edx,dword ptr [ebp-28h]
004012F5   push        edx
004012F6   call        @ILT+70(Fn) (0040104b)
004012FB   add         esp,4
004012FE   mov         byte ptr [ebp-4],1
00401302   lea         ecx,[ebp-1Ch]
00401305   call        @ILT+40(Student::~Student) (0040102d)  // 出函数后自己再释放掉 函数对&作指针处理



1.5 const 相关总结

1. const 引发的成员函数重载

class Test
{
public:
        char * m_p;
public:
        void SetData(char * pstr);
        void SetData(char * pstr) const;
};

注:const 可修饰的函数只能是成员函数,即不可能出现静态方法用const修饰 static void Fun() const

2. const 常成员函数与 成员函数的区别

const 修饰的成员函数内的this指针类型为 const class * const this 指针,即this指针及对象数据均不能修改;
非const 的成员函数内this指针类型为 class * const this指针,即this指针无法修改。

3. const 修饰的变量

int i;
const int * p; <==> int const (* p) 即 指针 p 指向的数据不能通过 *p 来做修改;
int * const  p <==> 即 指针 p 为常量,使用时不可修改。

4. 类成员函数的参数及返回值 何时使用const做修饰

int SetData(int a){}

int main()
{
        int i = 1;
        SetData(i);
}

上例中函数参数为非指针类型,即便我们不希望函数内部修改参数值,但有必要将参数类型修改为 const int a 吗?在调用函数时,压入的参数已开辟了新的栈空间,即便我们修改参数数据,对源数据没有丝毫影响。但当参数为指针(或引用)时,我们就很有必要使用 const 对参数进行修饰来声明其禁止修改其数据了,函数的返回值亦如此。

5. const 于非 const 数据间的转换

class Test
{
public:
        char * m_pName;
        const char * GetData() const {}
        // 返回值为const 类型的函数多为 const 常成员函数
};

int main()
{
        Test tt;
        tt.GetData();  // 对象直接调用常成员函数是OK的
        char * pstr = tt.GetData();  // 这个是不OK的
}

在 调用 和 赋值中(传参或接受返回值)中:
非const类型 ==> 转化为const类型是OK的
但const类型 ==> 转为非const类型是不合理的



II 类的构造及析构顺序

III 类的拷贝构造
一切皆为构造

通过两种情况来写出拷贝构造为必须

IV 类间的对象相互包含情况
PYG19周年生日快乐!

该用户从未签到

发表于 2010-1-21 02:13:24 | 显示全部楼层
我们只要弄清楚了函数如何实现模块化编译,如何进行传参及变量的赋值,就可以看到C源码而联想到其汇编代码,看到汇编反推出其C代码,这些是走向逆向的基础。


总结的不错~~~
PYG19周年生日快乐!
  • TA的每日心情
    开心
    2022-11-16 14:28
  • 签到天数: 5 天

    [LV.2]偶尔看看I

    发表于 2010-1-21 11:07:17 | 显示全部楼层
    原来老秦在学习编译原理呀!膜拜一下先!
    PYG19周年生日快乐!

    该用户从未签到

     楼主| 发表于 2010-1-21 16:23:01 | 显示全部楼层
    通过汇编来推断其编译器的设计 和模块化程序设计中需要解决的一些问题 从而更好的去理解类的实现机理 /:QQ2
    PYG19周年生日快乐!
  • TA的每日心情
    无聊
    2017-4-18 16:56
  • 签到天数: 1 天

    [LV.1]初来乍到

    发表于 2010-1-24 02:03:21 | 显示全部楼层
    过来膜拜~
    PYG19周年生日快乐!
  • TA的每日心情
    开心
    2022-11-16 14:28
  • 签到天数: 5 天

    [LV.2]偶尔看看I

    发表于 2010-1-24 15:52:44 | 显示全部楼层
    红警一下,你就知道!哈哈!
    PYG19周年生日快乐!
    您需要登录后才可以回帖 登录 | 加入我们

    本版积分规则

    快速回复 返回顶部 返回列表