logo资料库

浅析C++类的底层实现.doc

第1页 / 共12页
第2页 / 共12页
第3页 / 共12页
第4页 / 共12页
第5页 / 共12页
第6页 / 共12页
第7页 / 共12页
第8页 / 共12页
资料共12页,剩余部分请下载后查看
笨鸟先飞学编程系列-C++的封装性
一、类与对象
1、类的定义方法
2、属性和方法的使用
3、关于常成员函数
二、解析对象的内存结构
1、成员函数的调用方式: __thiscall
2、浅谈构造与析构函数
三、浅谈类的静态成员
四、说一下初始化列表
五、打破类封装性的棒槌 —— 友元
六、学习小结
笨鸟先飞学编程系列-C++的封装性 C++的阶段,我想根据 C++的一些特有的特性分别写一些专题,每个专题我都捎带讲一些语法,当然不会很多, 我还是会像 C 语言那样,内存结构贯穿始终,有汇编就有真相…… 本专题,我们讲述封装性。封装性是 C++的入门特性,要想学习 C++语言,封装性是首先要掌握的。下面我们进 入正题: 一、 类与对象 早在本系列第一节课(理解程序中的数据)的时候讲述过数据类型与数据的区别和联系当时得出的结论如下:  数据类型规定了数据的大小和表现形式  数据就是电脑中存放的数。  每个数据都有自己的地址,而这些地址可以有变量来代替  因此,数据通常存放于变量(或常量)中 这个结论在 C++中仍然同样适用,类就是我们自己定义的复杂的数据类型,而对象则就是由类声明的变量。 下面我们进入纯语法层面。 1、 类的定义方法 我相信,大家都还记得我在第一节课的时候讲述的结构体的课程,也相信大家没有忘记怎么定义一个结 构体。下面我给出类的定义方法: // 是不是很像定义一个结构体 class CExample { private: // 权限控制,相关内容在下面的小节中详细讲述 // 定义成员变量。也叫属性 int m_nFirstNum; int m_nSecNum; public: int bool SetNum(int nFirst, int nSec) { GetSum() const {return m_nFirstNum} // 成员函数 m_nFirstNum = nFirst; m_nSecNum = nSec ; return true; } CExample(){m_nFirstNum = 0; m_nSecNum = 0;} //构造函数 ~CExample(){} // 空析构 }; 当然,上面这个类的定义是不是很像定义一个结构体?只不过多了个 private 和 public 还有一些函数。是 的,C++里面,将结构体升级了,结构体里面可以有函数成员了,为了兼容,换了个关键字,当然,上面的这 个 class 完全可以改成 struct,一点问题都没有。 好奇的朋友会问:如果函数体的语句太多,逻辑复杂了,函数很大,那这个类岂不是很难看,太臃肿了吧。 是的,为了方便类的组织,也为了协调项目工程中文件的组织。上面的类还可以写成如下的形式:
// .h 文件中写如下的声明部分 class CExample { private: // 权限控制,防止外面直接操作这些变量,相关内容在下面的小节中详细讲述 // 是不是很像定义一个结构体 int m_nFirstNum; int m_nSecNum; // 定义成员变量。也叫属性 GetSum() const; public: int bool SetNum(int nFirst, int nSec); CExample(); ~CExample(); // 成员函数 //构造函数 // 空析构 }; // .cpp 文件中写如下的定义及实现部分 CExample::CExample() { } CExample::~CExample() { } int CExample::GetFirstNum() const { return m_nFirstNum; } int CExample::GetSecNum() const { return m_nSecNum; } bool CExample::SetNum(int nFirst, int nSec) { m_nFirstNum = nFirst; m_nSecNum = nSec ; return true; } int CExample::GetSum() const { return m_nFirstNum+m_nSecNum; } 上面两种写法也是有区别的,第一种方法写的函数具有 Inline 函数的特性。后一种则没有。 2、 属性和方法的使用 C++中定义一个对象跟定义一个函数没有什么区别。 #include #include "Example.h"
int main(int argc, char* argv[]) { CExample obj_Exp; // 定义一个对象 obj_Exp.SetNum(10, 20); // 调用一个方法 printf("%d+%d = %d\r\n", obj_Exp.GetFirstNum(), obj_Exp.GetSecNum(), obj_Exp.GetSum()); return 0; } 由此,我们就可以通过一个函数间接的来操作我们的变量,用户在给我们的变量赋值时,我们可以通过 Set 函数来对输入的内容作检测,在获取一个变量的内容时,我们可以通过一个函数来取得,这样都提高了程 序安全性。 也许,有的朋友会说,如果我绕过你提供的函数,直接对_nFirstNum;和 m_nSecNum;进行操作不是一样 不安全么,是的,这就是为什么我在程序中加上了 private 的原因。下面我们详细说明下这些关键字的含义。 private、public、protected 三个关键字,是 C++提供给并实现类封装的关键,它们用来说明类外的代码可 以直接访问类内的什么哪些成员,哪些成员不可能被外面访问。 private:说明,它后面所有的变量和函数,都不可能被类外访问,只能在类内被使用。 public:说明,它后面的所有变量和函数可以被类外的代码所访问,没有任何限制。 protected:说明,它后面的所有变量和函数,只能被自己或自己派生的类所使用。不能被类外的代码使用。 这个我们将在 C++继承性中详细讨论。 从程序设计的角度来讲,如果我们以类为单位编码的话,每个模块都是独立的我们只要关注与本类相关 操作,比如人这个类,它一般情况下有两个眼睛、一个嘴巴等之类的属性,人可以使用工具,可以行走,可 以跳跃等方法。我们编写的所有的函数都是针对这些展开的。而不用去关心谁要使用这个类。因此,类/对象 概念的加入,不单单是给编码方式做了改变,主要是设计思路的改变,程序模块化的改变等等。 3、 关于常成员函数 相信我们讲过的 const 与 inline 相关的知识,大家一定都没有忘记,是的,你猜对了,现在我们要说的就 是 const 的一个扩展用法。 当然,不用担心,这只是一个小小的扩展,不用担心混淆杂乱:当我们的成员函数不允许修改我们类中 的成员内容时,可以在函数的参数列表后加上一个 const 关键字。以免以后不小心更改了我们的类中成员属性。 二、 解析对象的内存结构 现在,我相信,如果习惯了我这种学习方式的朋友一定会很好奇,类定义对象的内存格式是怎样的,它 是不是像一个普通变量那样,或者是不是像一个结构体变量那样在内存里连续的将各个成员组织到一起,它 又是怎样将各个成员变量还有函数绑定到一起的?变量和函数在一起它是怎么组织的?本小节让我们来解决 这些问题。
为节省篇幅,我们仍旧使用上面的代码。我们用 VC 的调试器,调试这个代码: 注意看我们的变量监视区,我们定义的对象的内容跟结构体成员的内容格式差不多,(是按照定义的顺序 连续存放的,这点跟普通的局部变量不一样,普通的局部变量在内存中的顺序与定义顺序相反)内存中只存 放了成员变量,它并没有标出 SetNum 的位置,那它是怎么找到 SetNum 这个函数的呢? 根据我们先前调试 C 函数的经验,我们知道,函数的代码是被放在可执行文件的代码区段中的。在这个 CExample obj_Exp; dword ptr [ebp-4],0 ecx,[ebp-14h] @ILT+15(CExample::CExample) (00401014) 代码中,也有调用 SetNum 的代码,我们详细的跟一下它的汇编代码: 10: lea 004011ED 004011F0 call 004011F5 mov 11: 004011FC 004011FE 00401200 00401203 14h 0Ah ecx,[ebp-14h] @ILT+0(CExample::SetNum) (00401005) obj_Exp.SetNum(10, 20); push push lea call 这段代码又给我们带来了新的问题,我们只用类定义了一个对象(变量),它自动的调用了一个函数,根 CExample::CExample() { 据注释我们知道它调用的是构造函数。我们跟进去看下: 11: 12: push 00401050 00401051 mov sub 00401053 00401056 push push 00401057 push 00401058 00401059 push 0040105A lea 0040105D mov ebp ebp,esp esp,44h ebx esi edi ecx edi,[ebp-44h] ecx,11h ; 保存寄存器环境
} 00401062 mov rep stos 00401067 00401069 pop 0040106A mov 13: 0040106D mov pop 00401070 00401071 pop 00401072 pop 00401073 mov 00401075 pop ret 00401076 eax,0CCCCCCCCh dword ptr [edi] ; 将栈空间清为 CC(Release 编译就没有这部分代码了。) ; 将 ECX 中的内容给局部变量 ; 将 ECX 的内容返回 ecx dword ptr [ebp-4],ecx eax,dword ptr [ebp-4] edi esi ebx esp,ebp ebp 这段代码,首次看还真看不出个所以然来,源码的构造函数中,我们什么都没写,是个空函数,而这里 做的是返回 ECX 的值,可是这个函数也没有对 ECX 做什么特别的操作,而是直接使用进函数时 ECX 的值。 那只能说明在调用这个函数前,ECX 发生了变化。我们再回头看下调用构造函数的代码: 10: 004011ED lea 004011F0 call 004011F5 mov ecx,[ebp-14h] @ILT+15(CExample::CExample) (00401014) CExample obj_Exp; dword ptr [ebp-4],0 obj_Exp.SetNum(10, 20); ; 传递参数 哈哈,它是把我们 obj_Exp 对象的地址给了 ECX,然后调用构造返回的,也就是说,构造的返回值是我 们对象的首地址。哎,迷糊了,真搞不懂这是在干什么。先不管他,我们继续看怎么调用的 SetNum 这个函数 吧: 11: 004011FC 004011FE 00401200 00401203 29: 30: 14h 0Ah ecx,[ebp-14h] @ILT+0(CExample::SetNum) (00401005) bool CExample::SetNum(int nFirst, int nSec) { ; 也有这句,还是把我们的对象首地址给 ECX push push lea call push 00401130 00401131 mov sub 00401133 push 00401136 00401137 push push 00401138 ebp ebp,esp esp,44h ebx esi edi
00401139 push 0040113A lea 0040113D mov 00401142 mov rep stos 00401147 00401149 pop 0040114A mov 31: 0040114D mov 00401150 mov 00401153 mov 32: 00401155 mov 00401158 mov 0040115B mov m_nFirstNum = nFirst; m_nSecNum = nSec ; ecx edi,[ebp-44h] ecx,11h eax,0CCCCCCCCh dword ptr [edi] ecx dword ptr [ebp-4],ecx ; 备份一下我们的对象首地址 eax,dword ptr [ebp-4] ecx,dword ptr [ebp+8] dword ptr [eax],ecx ; 取出对象首地址 ; 取出 nFirst 参数 ; 给对象首地址指向的内容赋值为 nFirst 的内容 eax,dword ptr [ebp-4] ecx,dword ptr [ebp+0Ch]; 取出 nSec 参数 dword ptr [eax+4],ecx ; 取出对象首地址 ; 给对象首地址+4 指向的你内容赋值 return true; 0040115E mov al,1 ; 返回 1 } 34: pop 00401160 pop 00401161 pop 00401162 00401163 mov pop 00401165 00401166 ret edi esi ebx esp,ebp ebp 8 我简要的注释下来一下上面的代码。通过分析上面的代码,我们可以得出这样的结论: A、 函数通过 ecx 传递了我们对象的首地址。 B、函数通过 ecx 传递的对象首地址定位对象的每个成员变量。 这样,很明显,ECX 起到了传递参数的作用,这时 ecx 中的地址有个专业术语,叫做 this 指针。 OK,这就是一个新的知识点,我们成员函数的调用方式。 1、 成员函数的调用方式: __thiscall 记得在前面章节介绍函数时,讲过一些调用方式,但是没有提到过这种调用方式。下面我做一个简要的 总结: A、 参数也通过栈传递。 B、它用一个寄存器来传递 this 指针。 C、本条特性摘自《加密与解密》(第三版)非原文: a) 对于 VC++中传参规则: i. ii. 最左边两个不大于 4 字节的参数分别用 ECX 和 EDX 传参数. 对于浮点数、远指针、__int64 类型总是通过栈来传递的。 b) 对于 BC++|DELPHI 中的传递规则: i. 最左边三个不大于 DWORD 的参数,依次使用 EAX,ECX,EDX 传递,其它多的参数依次通 过 PASCAL 方式传递。 这样,函数的地址还是在代码区域,对象的内存中只存放数据成员,当我们要调用成员函数时,就通过 一个寄存器将函数操作的对象的首地址(也就是 this 指针)传递过去就可以了,传递不同的对象指针,就操 作不同的数据。哈哈,太巧妙了。
printf("%d+%d = %d\r\n", obj_Exp.GetFirstNum(), obj_Exp.GetSecNum(), obj_Exp.GetSum()); ; 调用 GetSum 函数 lea call push lea call push lea call push push call add ecx,[ebp-14h] @ILT+30(CExample::GetSum) (00401023) eax ecx,[ebp-14h] @ILT+5(CExample::GetSecNum) (0040100a) eax ecx,[ebp-14h] @ILT+10(CExample::GetFirstNum) (0040100f) eax offset string "%d+%d = %d\r\n" (0042501c) printf (00401290) 2、 浅谈构造与析构函数 OK,继续调试代码: 13: 14: 15: 00401208 0040120B 00401210 00401211 00401214 00401219 0040121A 0040121D 00401222 00401223 00401228 0040122D 16: 17: 00401230 mov 00401237 mov 0040123E lea 00401241 call 00401246 mov 我们至始至终都没有调用过构造和析构函数。但是,通过这次调我们知道,在创建一个对象(变量)的 dword ptr [ebp-18h],0 dword ptr [ebp-4],0FFFFFFFFh ecx,[ebp-14h] @ILT+20(CExample::~CExample) (00401019) eax,dword ptr [ebp-18h] esp,10h return 0; ; 调用析构函数 时候,我们的程序会自动的调用我们的构造函数,在要出对象作用域的时候,会自动的调用析构函数。 这样,我们很容易就能想象出,构造和析构的用途:构造就做初始化对象的各个成员,申请空间等初始 化工作。析构就做一些释放申请的空间啊之类的清理工作。 就这样,C++将数据跟函数封装到了一起,这样我们每个类产生的对象都是一个独立的个体,它有一个自己的 运作方式,几乎完全独立。在我们使用它的时候,根本不需要它是怎么实现了,只要知道怎么使用即可。 三、 浅谈类的静态成员 通过前面几节的学习,我们大概的能理解类的封装性及其运作过程,但是,如果我们继续深入的学习 C++, 我们很快就能发现一个问题:我们上面说的所有的成员都是属于对象的,也就是说,我们必须先通过类来定义一 个对象才可以操作。但是有的时候,我们需要一些属于类的成员,比如:人都有一个脑袋,这一个脑袋属于人类共 有的特性。不需要具体到哪一个人,我们都可以确定人只有一个脑袋。 放到类中也一样,比如我们需要知道当前这个类创建了几个对象的时候,我们不必在创建一个新的对象只需 要使用类的相关函数或者直接访问类的某些属性就可以了,而这些函数或者变量,它肯定不可能属于某个对象, 它应该属于这个类本身。 OK,下面就来体验一下静态带给我们的一些好处。同样,我们将前面的代码添加点儿东西(见 Exp02): // .h 文件中 public:
static int m_nCount; // 统计产生对象的 static int print(const char *szFormat, ...); // 让我们的类有自己的输出函数 // .cpp 文件中 int CExample::m_nCount = 0; // 初始化静态成员变量 CExample::CExample() { m_nCount++; // 当创建一个对象的时候,这个变量加 1 } CExample::~CExample() { if (m_nCount > 0) { m_nCount--; // 当对象销毁时,这个变量减 1 } } /************************************************************************/ /*让我们的 CExample 可以打印自己的信息 /*支持多参,同 printf 用法相同 /************************************************************************/ int CExample::print(const char *szFormat, ...) { if (!szFormat) { return 0; } pArgs; szBuffer[256 * 15] = {0}; va_list char va_start(pArgs, szFormat); vsprintf(szBuffer, szFormat, pArgs); va_end(pArgs); printf(szBuffer); return strlen(szFormat); } 好,有了这些,我们可以编写如下的测试代码: #include "stdafx.h" #include #include "Example.h" int main(int argc, char* argv[]) { CExample obj_Exp1; CExample::print("当前对象的数量为:%d\r\n", CExample::m_nCount); if (1)
分享到:
收藏