【文章标题】: 逆向C++之封装性 - 类和对象
【文章作者】: kanghtta
【作者邮箱】: kanghtta@hotmail.com
【作者主页】: kanghtta.cublog.cn
【下载地址】: 自己搜索下载
--------------------------------------------------------------------------------
【详细过程】
大家好,由于前段时间写了一部分逆向C++的内容,后来又因为考研和放假耽误了一段时间,希望大家谅解;好了,开始正题;
我们知道,面向对象的设计思想,近来成长的非常快;而c++是从C语言发展而来的;
这一节我们开始接触面向对象的第一个特性,封装性;首先,我们来回顾一下基本的概念;
学过java或者是任何一门面向对象语言的人对类和对象也许都不算陌生,而面向对象中其实应用最广泛的,莫过于继承
和多态性,而封装性,是学习后面的基础,我从高中开始,在学习上的座右铭: 基劳楼自高,水到渠自成;
好了废话太多,我们开始:
1:类和对象:
我们知道,类是对数据类型高度抽象的具体表现,它是一种类型,这种类型的特点就是将所描述的客观事物的不同类型
的数据和对它们的操作封装成为一个集合体;在数据结构种,类也称为数据类型;
数据类型:在高级语言编写的程序中,每个变量,常量和表达式都必须有一个它所属的的确定的数据类型,类型明显
或隐含地规定了在程序执行期间变量或表达式所有可能取值的范围,以及在这些值上允许的操作。因此,数据类型,是
一个值的集合和定义在这个集合上一组操作的总称。
类的分类: 类在语言中有原子类型也称为基本类型,如 int ,float,double,等常用的数据类型,这些类型在任何一门
语言中都是存在的,从内存的角度来看,它们的长度不同,而且表示方式也不同,int一般是整数,在计算机中一般用补码
表示,而float,double是浮点数表示,关于定点数和浮点数的的数制表述,大家请参考计算机组成原理;
而我们在这一章中主要做的就是用上面的基本数据类型来描述我们现实生活中需要处理的对象; 这在以前被称为
结构类型;由struct定义,有一定c++基础的人都知道,struct和class的作用是一样的,它们都可以作为定义类的关键字,
不同的是,它们定义的类,默认的访问权限不同; struct 默认为public,而class默认为private;至于什么是访问权限,
我会在下面解释;
什么是对象?
对象是类的实例化,比如人类就是一个抽象的类,而你或者我就是人类的一个实例化;类和对象的关系,
是抽象和具体的关系;
2;类的定义格式
类的定义格式,分为两部分;--- 说明部分和实现部分;
a:)说明部分用类说明该类中的若干成员,包括被说明的数据成员和成员函数;成员函数也称为方法,用来对数据成员进行
操作,数据成员的说明包含数据成员的名字和类型;成员函数可以在类体内只有说明,也可以在类体内对它定义(内联和外联);
b:)实现部分,主要用来对类体类只有说明而没有定义的函数进行定义; 而如果一个类的所有成员函数都定义在类体内,
该类就没有实现部分了;
简单点来说: 程序员在创建类的时候就创建了一种类型,提供了具有所定义的类的属性空间;对于复杂的对象,类能很方便
地在一个容器中把所需要用来描述对象的细节包装起来,这就是数据封装的意义;
下面我们来看一个例子:
我们一般将类定义在头文件中如下:
代码:
#include
using namespace std;
class CDate
{
public:
void SetDate(int y,int m,int d);
bool IsLeapYear();
void Print();
private:
int year,
month,
day;
};
void CDate::SetDate(int y,int m,int d)
{
year = y;
month = m;
day = d;
}
bool CDate::IsLeapYear()
{
return(year%4 == 0 && year % 100 !=0)||(year%400 == 0);
}
void CDate::Print()
{
cout<}
代码:
#include
#include "CDate.h"
using namespace std;
int main()
{
CDate d1,
d2,
&rd = d2;
d1.SetDate(2009,1,25);
d2.SetDate(2008,1,25);
d1.Print();
d2.Print();
rd.Print();
return 0;
}
访问权限:类体内的成员有3种不同的访问权限
1:public 公有成员不仅能被类体内的成员函数访问,还可被类体外的程序访问;公有成员提供了类与外界的接口;
通常将类中的成员全部和部分的定义为公有成员;
2:private 私有成员只有该类的成员函数或者是友员函数才可访问,类体外的程序不可访问,因此,私有成员是封装体
被隐藏的那一部分,通常将全部或者部分的数据成员定义为私有成员;私有成员在类体外不可见;
3:protected 保护成员在不同的条件下具有公有成员和私有成员的特性;对单个类来将,保护成员和私有成员没有什么
区别,该类的对象都不访问;如在程序中加入:
&rd = d2;
d1.day = 2;
编译程序: error:
E:\c++ 语言学习\date.cpp(10) : error C2248: 'day' : cannot access private member declared in class 'CDate'
单在继承的情况下,保护成员又不同于私有成员,基类中的保护成员可被派生类中的成员函数访问,而基类中私有成员不可被派生类访问;
保护成员又与公有成员不同,基类中公有成员在公有继承的情况下可被派生类的对象访问,而基类中的保护成员在任何继承的方式下也不能被
派生类的对象访问;
下面我们类看c++中比较重要的一个基础: 构造函数,和虚构函数;
创建对象时,系统将自动调用构造函数对所创建的对象进行初始化,也就是让对象的数据成员获取初值。对象的初值是通过
自动调用构造函数获取值。对象数据成员的值可以通过赋值,或者通过调用已有的成员函数来改变;
对象数据成员的值也就是对象的值,所以改变对象的值其实是改变对象中某一数据成员的值;我们知道,对象是类的一个实例化
,在给对象分配内存的时候,只分配该对象的数据成员所占用的空间,而成员函数是公用的,在每个对象中不包含成员函数
的空间;所以,在给对象初始化和赋值时,只考虑对对象的数据成员进行初始化和赋值就可以了;
我们通过OD来看一下:
在上面类的定义中,我们定义了三个数据成员和三个成员函数,
OD载日debug文件夹中的date.exe文件; 首先定位到main()函数入口,关于方法前面我已经介绍过了;
00421EFB |. E8 709E0000 call 0042BD70
00421F00 |. FF15 9CF14700 call dword ptr [<&KERNEL32.GetCommand>; [GetCommandLineA
00421F06 |. A3 24EF4700 mov dword ptr [47EF24], eax
00421F0B |. E8 409C0000 call 0042BB50
00421F10 |. A3 84D34700 mov dword ptr [47D384], eax
00421F15 |. E8 26970000 call 0042B640
00421F1A |. E8 D1950000 call 0042B4F0
00421F1F |. E8 DC400000 call 00426000
00421F24 |. 8B0D DCD34700 mov ecx, dword ptr [47D3DC]
00421F2A |. 890D E0D34700 mov dword ptr [47D3E0], ecx
00421F30 |. 8B15 DCD34700 mov edx, dword ptr [47D3DC]
00421F36 |. 52 push edx
00421F37 |. A1 D4D34700 mov eax, dword ptr [47D3D4]
00421F3C |. 50 push eax
00421F3D |. 8B0D D0D34700 mov ecx, dword ptr [47D3D0]
00421F43 |. 51 push ecx
00421F44 |. E8 ECF2FDFF call 00401235 //main() 函数入口
F7跟进,
00401580 /> \55 push ebp
00401581 |. 8BEC mov ebp, esp
00401583 |. 83EC 5C sub esp, 5C
00401586 |. 53 push ebx
00401587 |. 56 push esi
00401588 |. 57 push edi ;执行环境保存
00401589 |. 8D7D A4 lea edi, dword ptr [ebp-5C]
0040158C |. B9 17000000 mov ecx, 17
00401591 |. B8 CCCCCCCC mov eax, CCCCCCCC
00401596 |. F3:AB rep stos dword ptr es:[edi]
00401598 |. 8D45 E8 lea eax, dword ptr [ebp-18] ;
0040159B |. 8945 E4 mov dword ptr [ebp-1C], eax
0040159E |. 6A 19 push 19
004015A0 |. 6A 01 push 1
004015A2 |. 68 D9070000 push 7D9
004015A7 |. 8D4D F4 lea ecx, dword ptr [ebp-C]
004015AA |. E8 96FBFFFF call 00401145 ;d1.SetDate(2009,1,25);
004015AF |. 6A 19 push 19
004015B1 |. 6A 01 push 1
004015B3 |. 68 D8070000 push 7D8
004015B8 |. 8D4D E8 lea ecx, dword ptr [ebp-18]
004015BB |. E8 85FBFFFF call 00401145 ;d2.SetDate(2008,1,25);
004015C0 |. 8D4D F4 lea ecx, dword ptr [ebp-C]
004015C3 |. E8 A1FAFFFF call 00401069 ; d1.Print();
004015C8 |. 8D4D E8 lea ecx, dword ptr [ebp-18]
004015CB |. E8 99FAFFFF call 00401069 ; d2.Print();
004015D0 |. 8B4D E4 mov ecx, dword ptr [ebp-1C]
004015D3 |. E8 91FAFFFF call 00401069 ; rd.Print();
004015D8 |. 33C0 xor eax, eax
004015DA |. 5F pop edi
004015DB |. 5E pop esi
004015DC |. 5B pop ebx
004015DD |. 83C4 5C add esp, 5C
004015E0 |. 3BEC cmp ebp, esp
004015E2 |. E8 89F40100 call 00420A70
004015E7 |. 8BE5 mov esp, ebp
004015E9 |. 5D pop ebp
004015EA \. C3 retn
我们看下堆栈结构
0012FEE0 00000000
0012FEE4 00000000
0012FEE8 7FFDC000
0012FEEC CCCCCCCC ;rep stos dword ptr es:[edi]
0012FEF0 CCCCCCCC
0012FEF4 CCCCCCCC
0012FEF8 CCCCCCCC
0012FEFC CCCCCCCC
0012FF00 CCCCCCCC
0012FF04 CCCCCCCC
0012FF08 CCCCCCCC
0012FF0C CCCCCCCC
0012FF10 CCCCCCCC
0012FF14 CCCCCCCC
0012FF18 CCCCCCCC
0012FF1C CCCCCCCC
0012FF20 CCCCCCCC
0012FF24 CCCCCCCC
0012FF28 CCCCCCCC
0012FF2C CCCCCCCC ;rd引用
0012FF30 CCCCCCCC ;ebp-18 ;d1对象
0012FF34 CCCCCCCC ;ebp-14
0012FF38 CCCCCCCC ;ebp-10
0012FF3C CCCCCCCC ;ebp-c ;d1对象
0012FF40 CCCCCCCC
0012FF44 CCCCCCCC ;ebp-4
0012FF48 /0012FF88 ; ebp 的值
0012FF4C |00421F49 返回到 date.<模块入口点>+0E9 来自 date.00401235
我们一直F8,到4015D8时,我们来看看堆栈中的情况:
EBP-20 > CCCCCCCC
EBP-1C > 0012FF30 ;&rd
EBP-18 > 000007D8 ;d2对象
EBP-14 > 00000001
EBP-10 > 00000019
EBP-C > 000007D9 ; d1对象
EBP-8 > 00000001
EBP-4 > 00000019
EBP ==> >/0012FF88 ; 栈帧
我们先来看看对象的初始化:
由于对象的声明是在main函数的函数体内,所以对它的初始化是在栈中,我们说过,在对象的初始化时,只分配
数据成员的地址空间,而成员函数是公用的,我们来看下,在CDate类中,数据成员是3个int型的数据;占12个字节;
数据成员声明的语句和对应的反汇编语句如下:
CDate d1,
d2,
&rd = d2;
我们从上面一个的栈帧中可以看出,在初始化对象时,只是为对象分配了空间,如下:
0012FF2C CCCCCCCC ;rd引用
0012FF30 CCCCCCCC ;ebp-18 ;d1对象
0012FF34 CCCCCCCC ;ebp-14
0012FF38 CCCCCCCC ;ebp-10
0012FF3C CCCCCCCC ;ebp-c ;d1对象
0012FF40 CCCCCCCC
0012FF44 CCCCCCCC ;ebp-4
00401598 |. 8D45 E8 lea eax, dword ptr [ebp-18] ;d1
0040159B |. 8945 E4 mov dword ptr [ebp-1C], eax ;&rd
004015A7 |. 8D4D F4 lea ecx, dword ptr [ebp-C] ;d2
从上面可以看出,在c++产生对象时,上面的只是在栈中生成对象的方法;而在C++ 中
内存有,堆区,栈区,静态区等区分,所以这也只是一种方法;
我们在看看成员函数的调用:
004015A7 |. 8D4D F4 lea ecx, dword ptr [ebp-C]
004015AA |. E8 96FBFFFF call 00401145 ;d1.SetDate(2009,1,25);
004015AF |. 6A 19 push 19
004015B1 |. 6A 01 push 1
004015B3 |. 68 D8070000 push 7D8
004015B8 |. 8D4D E8 lea ecx, dword ptr [ebp-18]
004015BB |. E8 85FBFFFF call 00401145 ;d2.SetDate(2008,1,25);
004015C0 |. 8D4D F4 lea ecx, dword ptr [ebp-C]
004015C3 |. E8 A1FAFFFF call 00401069 ; d1.Print();
004015C8 |. 8D4D E8 lea ecx, dword ptr [ebp-18]
004015CB |. E8 99FAFFFF call 00401069 ; d2.Print();
004015D0 |. 8B4D E4 mov ecx, dword ptr [ebp-1C]
004015D3 |. E8 91FAFFFF call 00401069 ; rd.Print();
这就证明了我们上面那段话,在为对象分配内存时,只分配数据成员的空间,而成员函数公用;因为对类的相同成员函数调用
的地址都是相同的;
void SetDate(int y,int m,int d); 的地址是0040115;
void Print(); 的地址是00401069;
不论是产生什么类型的对象,(这里从存储角度来考察),c++都会产生构造函数的调用操作,在堆中或者是在栈中
生成对象时,C++都会在配置内存后立即产生一个隐含的构造函数调用,在程序代码中我们是看不出来的,而如果是产生
一个全局对象,由于对象是全局的,它的作用域在任何函数活动范围之外,显然更没有地方来安置这样一个构造函数了;
我们下面修改一下源程序来看下:
#include
using namespace std;
class CDate
{
public:
void SetDate(int y,int m,int d);
bool IsLeapYear();
void Print();
CDate();
private:
int year,
month,
day;
};
void CDate::SetDate(int y,int m,int d)
{
year = y;
month = m;
day = d;
}
bool CDate::IsLeapYear()
{
return(year%4 == 0 && year % 100 !=0)||(year%400 == 0);
}
void CDate::Print()
{
cout<
#include "CDate.h"
using namespace std;
CDate GlobalObject;
int main()
{
CDate d1; //栈对象
CDate *d2 = new CDate(); //堆对象
CDate &rd = d1; //引用对象
static CDate StaticObject; //静态全局对象
d1.SetDate(2009,1,25);
d2->SetDate(2008,1,25);
StaticObject.SetDate(2000,1,1);
GlobalObject.SetDate(1987,1,1);
GlobalObject.Print();
StaticObject.Print();
d1.Print();
d2->Print();
rd.Print();
return 0;
}
我们将这个程序编译执行后,用Od载入,你会发现得到的结果大同小异,但又有很多不同之处,重点你应该放在不同类型的对象
初始化上,这一点,由于时间关系,下一次在写; 大家又时间的自己揣摩下;