笨鸟先飞学编程系列浅析C++的封装性.docx
- 文档编号:7576936
- 上传时间:2023-01-25
- 格式:DOCX
- 页数:16
- 大小:58.03KB
笨鸟先飞学编程系列浅析C++的封装性.docx
《笨鸟先飞学编程系列浅析C++的封装性.docx》由会员分享,可在线阅读,更多相关《笨鸟先飞学编程系列浅析C++的封装性.docx(16页珍藏版)》请在冰豆网上搜索。
笨鸟先飞学编程系列浅析C++的封装性
笨鸟先飞学编程系列-C++的封装性
C++的阶段,我想根据C++的一些特有的特性分别写一些专题,每个专题我都捎带讲一些语法,当然不会很多,我还是会像C语言那样,内存结构贯穿始终,有汇编就有真相……
本专题,我们讲述封装性。
封装性是C++的入门特性,要想学习C++语言,封装性是首先要掌握的。
下面我们进入正题:
一、类与对象
早在本系列第一节课(理解程序中的数据)的时候讲述过数据类型与数据的区别和联系当时得出的结论如下:
Ø数据类型规定了数据的大小和表现形式
Ø数据就是电脑中存放的数。
Ø每个数据都有自己的地址,而这些地址可以有变量来代替
Ø因此,数据通常存放于变量(或常量)中
这个结论在C++中仍然同样适用,类就是我们自己定义的复杂的数据类型,而对象则就是由类声明的变量。
下面我们进入纯语法层面。
1、类的定义方法
我相信,大家都还记得我在第一节课的时候讲述的结构体的课程,也相信大家没有忘记怎么定义一个结构体。
下面我给出类的定义方法:
classCExample//是不是很像定义一个结构体
{
private:
//权限控制,相关内容在下面的小节中详细讲述
intm_nFirstNum;//定义成员变量。
也叫属性
intm_nSecNum;
public:
intGetSum()const{returnm_nFirstNum}//成员函数
boolSetNum(intnFirst,intnSec)
{
m_nFirstNum=nFirst;
m_nSecNum=nSec;
returntrue;
}
CExample(){m_nFirstNum=0;m_nSecNum=0;}//构造函数
~CExample(){}//空析构
};
当然,上面这个类的定义是不是很像定义一个结构体?
只不过多了个private和public还有一些函数。
是的,C++里面,将结构体升级了,结构体里面可以有函数成员了,为了兼容,换了个关键字,当然,上面的这个class完全可以改成struct,一点问题都没有。
好奇的朋友会问:
如果函数体的语句太多,逻辑复杂了,函数很大,那这个类岂不是很难看,太臃肿了吧。
是的,为了方便类的组织,也为了协调项目工程中文件的组织。
上面的类还可以写成如下的形式:
//.h文件中写如下的声明部分
classCExample//是不是很像定义一个结构体
{
private:
//权限控制,防止外面直接操作这些变量,相关内容在下面的小节中详细讲述
intm_nFirstNum;//定义成员变量。
也叫属性
intm_nSecNum;
public:
intGetSum()const;//成员函数
boolSetNum(intnFirst,intnSec);
CExample();//构造函数
~CExample();//空析构
};
//.cpp文件中写如下的定义及实现部分
CExample:
:
CExample()
{
}
CExample:
:
~CExample()
{
}
intCExample:
:
GetFirstNum()const
{
returnm_nFirstNum;
}
intCExample:
:
GetSecNum()const
{
returnm_nSecNum;
}
boolCExample:
:
SetNum(intnFirst,intnSec)
{
m_nFirstNum=nFirst;
m_nSecNum=nSec;
returntrue;
}
intCExample:
:
GetSum()const
{
returnm_nFirstNum+m_nSecNum;
}
上面两种写法也是有区别的,第一种方法写的函数具有Inline函数的特性。
后一种则没有。
2、属性和方法的使用
C++中定义一个对象跟定义一个函数没有什么区别。
#include
#include"Example.h"
intmain(intargc,char*argv[])
{
CExampleobj_Exp;//定义一个对象
obj_Exp.SetNum(10,20);//调用一个方法
printf("%d+%d=%d\r\n",obj_Exp.GetFirstNum(),
obj_Exp.GetSecNum(),
obj_Exp.GetSum());
return0;
}
由此,我们就可以通过一个函数间接的来操作我们的变量,用户在给我们的变量赋值时,我们可以通过Set函数来对输入的内容作检测,在获取一个变量的内容时,我们可以通过一个函数来取得,这样都提高了程序安全性。
也许,有的朋友会说,如果我绕过你提供的函数,直接对_nFirstNum;和m_nSecNum;进行操作不是一样不安全么,是的,这就是为什么我在程序中加上了private的原因。
下面我们详细说明下这些关键字的含义。
private、public、protected三个关键字,是C++提供给并实现类封装的关键,它们用来说明类外的代码可以直接访问类内的什么哪些成员,哪些成员不可能被外面访问。
private:
说明,它后面所有的变量和函数,都不可能被类外访问,只能在类内被使用。
public:
说明,它后面的所有变量和函数可以被类外的代码所访问,没有任何限制。
protected:
说明,它后面的所有变量和函数,只能被自己或自己派生的类所使用。
不能被类外的代码使用。
这个我们将在C++继承性中详细讨论。
从程序设计的角度来讲,如果我们以类为单位编码的话,每个模块都是独立的我们只要关注与本类相关操作,比如人这个类,它一般情况下有两个眼睛、一个嘴巴等之类的属性,人可以使用工具,可以行走,可以跳跃等方法。
我们编写的所有的函数都是针对这些展开的。
而不用去关心谁要使用这个类。
因此,类/对象概念的加入,不单单是给编码方式做了改变,主要是设计思路的改变,程序模块化的改变等等。
3、关于常成员函数
相信我们讲过的const与inline相关的知识,大家一定都没有忘记,是的,你猜对了,现在我们要说的就是const的一个扩展用法。
当然,不用担心,这只是一个小小的扩展,不用担心混淆杂乱:
当我们的成员函数不允许修改我们类中的成员内容时,可以在函数的参数列表后加上一个const关键字。
以免以后不小心更改了我们的类中成员属性。
二、解析对象的内存结构
现在,我相信,如果习惯了我这种学习方式的朋友一定会很好奇,类定义对象的内存格式是怎样的,它是不是像一个普通变量那样,或者是不是像一个结构体变量那样在内存里连续的将各个成员组织到一起,它又是怎样将各个成员变量还有函数绑定到一起的?
变量和函数在一起它是怎么组织的?
本小节让我们来解决这些问题。
为节省篇幅,我们仍旧使用上面的代码。
我们用VC的调试器,调试这个代码:
注意看我们的变量监视区,我们定义的对象的内容跟结构体成员的内容格式差不多,(是按照定义的顺序连续存放的,这点跟普通的局部变量不一样,普通的局部变量在内存中的顺序与定义顺序相反)内存中只存放了成员变量,它并没有标出SetNum的位置,那它是怎么找到SetNum这个函数的呢?
根据我们先前调试C函数的经验,我们知道,函数的代码是被放在可执行文件的代码区段中的。
在这个代码中,也有调用SetNum的代码,我们详细的跟一下它的汇编代码:
10:
CExampleobj_Exp;
004011EDleaecx,[ebp-14h]
004011F0call@ILT+15(CExample:
:
CExample)(00401014)
004011F5movdwordptr[ebp-4],0
11:
obj_Exp.SetNum(10,20);
004011FCpush14h
004011FEpush0Ah
00401200leaecx,[ebp-14h]
00401203call@ILT+0(CExample:
:
SetNum)(00401005)
这段代码又给我们带来了新的问题,我们只用类定义了一个对象(变量),它自动的调用了一个函数,根据注释我们知道它调用的是构造函数。
我们跟进去看下:
11:
CExample:
:
CExample()
12:
{
00401050pushebp
00401051movebp,esp
00401053subesp,44h
00401056pushebx
00401057pushesi
00401058pushedi
00401059pushecx;保存寄存器环境
0040105Aleaedi,[ebp-44h]
0040105Dmovecx,11h
00401062moveax,0CCCCCCCCh
00401067repstosdwordptr[edi];将栈空间清为CC(Release编译就没有这部分代码了。
)
00401069popecx
0040106Amovdwordptr[ebp-4],ecx;将ECX中的内容给局部变量
13:
}
0040106Dmoveax,dwordptr[ebp-4];将ECX的内容返回
00401070popedi
00401071popesi
00401072popebx
00401073movesp,ebp
00401075popebp
00401076ret
这段代码,首次看还真看不出个所以然来,源码的构造函数中,我们什么都没写,是个空函数,而这里做的是返回ECX的值,可是这个函数也没有对ECX做什么特别的操作,而是直接使用进函数时ECX的值。
那只能说明在调用这个函数前,ECX发生了变化。
我们再回头看下调用构造函数的代码:
10:
CExampleobj_Exp;
004011EDleaecx,[ebp-14h]
004011F0call@ILT+15(CExample:
:
CExample)(00401014)
004011F5movdwordptr[ebp-4],0
哈哈,它是把我们obj_Exp对象的地址给了ECX,然后调用构造返回的,也就是说,构造的返回值是我们对象的首地址。
哎,迷糊了,真搞不懂这是在干什么。
先不管他,我们继续看怎么调用的SetNum这个函数吧:
11:
obj_Exp.SetNum(10,20);
004011FCpush14h;传递参数
004011FEpush0Ah
00401200leaecx,[ebp-14h];也有这句,还是把我们的对象首地址给ECX
00401203call@ILT+0(CExample:
:
SetNum)(00401005)
29:
boolCExample:
:
SetNum(intnFirst,intnSec)
30:
{
00401130pushebp
00401131movebp,esp
00401133subesp,44h
00401136pushebx
00401137pushesi
00401138pushedi
00401139pushecx
0040113Aleaedi,[ebp-44h]
0040113Dmovecx,11h
00401142moveax,0CCCCCCCCh
00401147repstosdwordptr[edi]
00401149popecx
0040114Amovdwordptr[ebp-4],ecx;备份一下我们的对象首地址
31:
m_nFirstNum=nFirst;
0040114Dmoveax,dwordptr[ebp-4];取出对象首地址
00401150movecx,dwordptr[ebp+8];取出nFirst参数
00401153movdwordptr[eax],ecx;给对象首地址指向的内容赋值为nFirst的内容
32:
m_nSecNum=nSec;
00401155moveax,dwordptr[ebp-4];取出对象首地址
00401158movecx,dwordptr[ebp+0Ch];取出nSec参数
0040115Bmovdwordptr[eax+4],ecx;给对象首地址+4指向的你内容赋值
returntrue;
0040115Emoval,1;返回1
34:
}
00401160popedi
00401161popesi
00401162popebx
00401163movesp,ebp
00401165popebp
00401166ret8
我简要的注释下来一下上面的代码。
通过分析上面的代码,我们可以得出这样的结论:
A、函数通过ecx传递了我们对象的首地址。
B、函数通过ecx传递的对象首地址定位对象的每个成员变量。
这样,很明显,ECX起到了传递参数的作用,这时ecx中的地址有个专业术语,叫做this指针。
OK,这就是一个新的知识点,我们成员函数的调用方式。
1、成员函数的调用方式:
__thiscall
记得在前面章节介绍函数时,讲过一些调用方式,但是没有提到过这种调用方式。
下面我做一个简要的总结:
A、参数也通过栈传递。
B、它用一个寄存器来传递this指针。
C、本条特性摘自《加密与解密》(第三版)非原文:
a)对于VC++中传参规则:
i.最左边两个不大于4字节的参数分别用ECX和EDX传参数.
ii.对于浮点数、远指针、__int64类型总是通过栈来传递的。
b)对于BC++|DELPHI中的传递规则:
i.最左边三个不大于DWORD的参数,依次使用EAX,ECX,EDX传递,其它多的参数依次通过PASCAL方式传递。
这样,函数的地址还是在代码区域,对象的内存中只存放数据成员,当我们要调用成员函数时,就通过一个寄存器将函数操作的对象的首地址(也就是this指针)传递过去就可以了,传递不同的对象指针,就操作不同的数据。
哈哈,太巧妙了。
2、浅谈构造与析构函数
OK,继续调试代码:
13:
printf("%d+%d=%d\r\n",obj_Exp.GetFirstNum(),
14:
obj_Exp.GetSecNum(),
15:
obj_Exp.GetSum());
00401208leaecx,[ebp-14h]
0040120Bcall@ILT+30(CExample:
:
GetSum)(00401023);调用GetSum函数
00401210pusheax
00401211leaecx,[ebp-14h]
00401214call@ILT+5(CExample:
:
GetSecNum)(0040100a)
00401219pusheax
0040121Aleaecx,[ebp-14h]
0040121Dcall@ILT+10(CExample:
:
GetFirstNum)(0040100f)
00401222pusheax
00401223pushoffsetstring"%d+%d=%d\r\n"(0042501c)
00401228callprintf(00401290)
0040122Daddesp,10h
16:
17:
return0;
00401230movdwordptr[ebp-18h],0
00401237movdwordptr[ebp-4],0FFFFFFFFh
0040123Eleaecx,[ebp-14h]
00401241call@ILT+20(CExample:
:
~CExample)(00401019);调用析构函数
00401246moveax,dwordptr[ebp-18h]
我们至始至终都没有调用过构造和析构函数。
但是,通过这次调我们知道,在创建一个对象(变量)的时候,我们的程序会自动的调用我们的构造函数,在要出对象作用域的时候,会自动的调用析构函数。
这样,我们很容易就能想象出,构造和析构的用途:
构造就做初始化对象的各个成员,申请空间等初始化工作。
析构就做一些释放申请的空间啊之类的清理工作。
就这样,C++将数据跟函数封装到了一起,这样我们每个类产生的对象都是一个独立的个体,它有一个自己的运作方式,几乎完全独立。
在我们使用它的时候,根本不需要它是怎么实现了,只要知道怎么使用即可。
三、浅谈类的静态成员
通过前面几节的学习,我们大概的能理解类的封装性及其运作过程,但是,如果我们继续深入的学习C++,我们很快就能发现一个问题:
我们上面说的所有的成员都是属于对象的,也就是说,我们必须先通过类来定义一个对象才可以操作。
但是有的时候,我们需要一些属于类的成员,比如:
人都有一个脑袋,这一个脑袋属于人类共有的特性。
不需要具体到哪一个人,我们都可以确定人只有一个脑袋。
放到类中也一样,比如我们需要知道当前这个类创建了几个对象的时候,我们不必在创建一个新的对象只需要使用类的相关函数或者直接访问类的某些属性就可以了,而这些函数或者变量,它肯定不可能属于某个对象,它应该属于这个类本身。
OK,下面就来体验一下静态带给我们的一些好处。
同样,我们将前面的代码添加点儿东西(见Exp02):
//.h文件中
public:
staticintm_nCount;//统计产生对象的
staticintprint(constchar*szFormat,...);//让我们的类有自己的输出函数
//.cpp文件中
intCExample:
:
m_nCount=0;//初始化静态成员变量
CExample:
:
CExample()
{
m_nCount++;//当创建一个对象的时候,这个变量加1
}
CExample:
:
~CExample()
{
if(m_nCount>0)
{
m_nCount--;//当对象销毁时,这个变量减1
}
}
/************************************************************************/
/*让我们的CExample可以打印自己的信息
/*支持多参,同printf用法相同
/************************************************************************/
intCExample:
:
print(constchar*szFormat,...)
{
if(!
szFormat)
{
return0;
}
va_listpArgs;
charszBuffer[256*15]={0};
va_start(pArgs,szFormat);
vsprintf(szBuffer,szFormat,pArgs);
va_end(pArgs);
printf(szBuffer);
returnstrlen(szFormat);
}
好,有了这些,我们可以编写如下的测试代码:
#include"stdafx.h"
#include
#include"Example.h"
intmain(intargc,char*argv[])
{
CExampleobj_Exp1;
CExample:
:
print("当前对象的数量为:
%d\r\n",CExample:
:
m_nCount);
if
(1)
{
CExampleobj_Exp2;//该对象属于if作用域,出了if,对象自动销毁
CExample:
:
print("当前对象的数量为:
%d\r\n",CExample:
:
m_nCount);
}
CExample:
:
print("当前对象的数量为:
%d\r\n",CExample:
:
m_nCount);
return0;
}
我想大家应该能想象出来运行的结果:
好,我们调试一下这段程序:
11:
CExample:
:
print("当前对象的数量为:
%d\r\n",CExample:
:
m_nCount);
004012ECmoveax,[CExample:
:
m_nCount(0042ae6c)];这明显告诉我们,静态就是全局
004012F1pusheax
004012F2pushoffsetstring"当前对象的数量为:
%d\r\n"
004012F7call@ILT+30(CExample:
:
print)(00401023);调用该静态函数没有传递this指针
004012FCaddesp,8
多了不用看了,通过这段代码,我们很明显就可以清楚,静态变量,不属于类对象,它存放于全局数据区,同全局变量在一个地方(更多关于静态变量的相关说明见我发的《static学习笔记》一文)。
静态函数,跟全局函数一样,它虽然在源码中书写与类内,但是它其实就是一个全局函数,不传递this指针,因此,在使用静态函数时需要知道,静态函数中不能调用其它普通的成员函数也不能引用普通的成员变量。
但是反过来,在其它的成员函数中可以调用静态函数也可以使用静态变量。
四、说一下初始化列表
现在让我们考虑一个问题:
倘若我们的类中有其它类成员、有引用成员、有常量成员时,应该怎么初始化它们呢?
是啊,如果我们在头文件中声明它们的时候,直接给它们初始化很明显是要报错的。
这时,我们的主角,初始化列表就上场了。
(见Exp03):
假如,我们有如下一个类:
classCBaseExp
{
public:
CBaseExp(char*pszStr)
{
intnLength=strlen(pszStr)+1;
if(1>=nLength)
{
return;
}
m_pszBuff=newchar[nLength];
strcpy(m_pszBuff,pszStr);
}
~CBaseExp(){};
private:
char*m_pszBuff;//需要临时分配堆空间
};
现在,我们在我们的CExample中增加如下几个成员:
public:
//为了描述方便,我直接使用public方式。
CBaseExpm_ObjBE;//这个对象需要初始化(用到初始化列表)
int&m_nFirst;//需要初始化(用到初始化列表)
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 笨鸟先飞 编程 系列 浅析 C+ 封装