分享
 
 
 

C++面向对象特性实现机制的初步分析 Part3

王朝c/c++·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

Chapter 2 封装

2.1 封装的目的和意义

广义的说,封装是为了使这个世界以更加简单的方式呈现在我们的面前。买一台电冰箱,我不必要知道里面压缩机的运作过程,不必了解详细的制冷过程,我所要所的是,只是给它通电,通过调整一些旋钮来设置温度,剩下的事情,就不归我管了。

就面向对象程序设计来说,封装是为了隐藏实现的细节,使类的用户通过公开的接口来使用类的功能。在C++中,声明为public的成员函数,就是这样的接口,类的用户可以通过借口来安全的修改类的内部数据(设想,如果冰箱厂商让用户直接调节压缩机参数,那将是非常危险的)。对类内部方法的封装,可以增强代码的可复用性,用户是通过接口在访问类的功能的,类成员函数的实现细节发生变化,但只要结构不变,所有的使用类的客户代码都不需要修改。

很多人会认为C++中的class的封装效果,用C中的struct也可以实现。其实不然,看代码:

/////////////////////////////////////////////

//Source Code 2.1

#include<iostream.h>

struct s_Test

{

int (*p_add)(s_Test*);

int i;

};

int add(s_Test* p){return ++(p->i);}

class c_Test

{

public:

c_Test(){i=0;}

int add(){return ++i;}

void show(){cout<<i<<endl;}

private:

int i;

};

void main(void)

{

s_Test s;

s.i=0;

s.p_add=add;

s.p_add(&s);

cout<<s.i<<endl;

c_Test c;

c.add();

c.show();

}

//定义结构体 s_Test

//其中包含两个成员

//p_add为指向函数的指针

//i 为整形数据

//定义参数为指向结构体s_Test的指针的函数

//定义类c_Test

//构造函数对内部数据成员初始化

//成员函数add()完成对内部数据成员i的+1操作

//成员函数show()显示内部数据成员i的值

//内部数据成员

//创建结构体 s

//给s的数据成员i赋值

//初始化p_add指针,使其指向int add(s_Test* p)

//通过指向函数的指针来调用add(s_Test* p)

//输出i的值

//创建类c_Test的对象 c

//调用add()成员函数

//使用show()输出i的值

通过观察上述的代码,我们可以发现,struct可以将数据和操作“包装”在一起,但这种包装是松散和不安全的。首先,struct没有访问控制机制,任何代码都可以操作其内部数据i,其次,结构体内指针指向的函数与结构体本身没有必然的联系,任何代码都可以调用add(s_Test* p)函数。Struct只是一个框架,结构体中所有的成员对外来说都是可见和可访问的。再看Class,通过使用public和private关键字,类很好的对借口和内部数据进行的分离。对内部数据的访问,都要通过成员函数来实现,例如int i的初始化,改变i的值,先是输出等,都实现了封装。

我们可以得出这样的结论

a.封装的作用是提供接口

b.封装可以保护内部数据

c.封装可以代码的可复用性

2.2. 封装的实现机制

2.2.1 类成员函数的调用方法

在文章的第一部分中,我们探讨了C++ Class的内存格局,那么,类的封装和其内存格局之间,有着什么样的联系呢?成员函数对内部变量的访问,又是怎么实现的呢?

通过观察图1-2,我们知道,类的实例中只包括数据成员,类的成员函数是被排除在对象封装之外的。编译器通过“类名::函数名”的方式,区分每一个成员函数。这样的内存布局,使人认为class对成员函数的处理方法,跟上节程序中struct的函数指针像类似。我们先看看类对其成员函数的调用方法与struct中使用的函数指针在形式上的区别,看代码片断:

void main(void)

{

s_Test s;

s.i=0;

s.p_add=add;

s.p_add(&s);

cout<<s.i<<endl;

c_Test c;

c.add();

c.show();

}

//完整代码见上节Source Code 2.1

//通过函数指针调用add()时,我们把结构体指针传给了函数,//通过这个指针使add()函数访问并操作结构体中的数据

//调用类的成员函数,不需要传递指针

我们知道c_Test可以被创建很多份实例,但其成员函数在内存中只有一份,那么,这一份成员函数在没有参数传入的情况下,如何分辨这若干个c_Test的实例呢?调用类成员函数的时候,真的没有参数传入吗?让我们看看编译器针对c.add();语句生成的二进制代码,这样,任何隐藏的机制,都将一目了然。

Main函数中调用c.add()时,系统生成如下的代码,其中

00401050 lea ecx,[ebp-4]

00401053 call @ILT+5(c_Test::add) (0040100a)

call @ILT+5(c_Test::add) (0040100a) 将控制转向地址0040100a,这里是一条跳转指令

0040100A jmp c_Test::add (004010c0)

地址004010c0为函数c_Test::add的真实入口地址,我们再看一下004010c0处的代码

004010C0

004010C1

004010C3

004010C6

004010C7

004010C8

004010C9

004010CA

004010CD

004010D2

004010D7

004010D9

004010DA

004010DD

004010E0

004010E2

004010E5

004010E8

004010EA

004010ED

004010EF

004010F0

004010F1

004010F2

004010F4

004010F5

push

mov

sub

push

push

push

push

lea

mov

mov

rep stos

pop

mov

mov

mov

add

mov

mov

mov

mov

pop

pop

pop

mov

pop

ret

ebp

ebp,esp

esp,44h

ebx

esi

edi

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 [eax]

ecx,1

edx,dword ptr [ebp-4]

dword ptr [edx],ecx

eax,dword ptr [ebp-4]

eax,dword ptr [eax]

edi

esi

ebx

esp,ebp

ebp

上述代码为c_Test::adde的具体实现代码。

通过上述汇编代码的分析,我们可以发现add()函数有一个隐藏的参数,这是一个指向调用add()函数的那个类实例(即c这个对象的地址)的参数,因此,实际上编译器会将c.add();的调用转化为如下代码:

Test::add((Test*)&c);

int add(){return ++i;}函数实际上是

int add((Test*)&this)

{return ++((Test*)&this).i;}

这里,隐藏的参数(Test*)&this就是我们常说的this指针。我们会发现,这样的做法与使用结构体中指向函数的指针的方法相似(s.p_add(&s);),但C++中这一步骤是由编译器来实现的,通常用户不需要对this指针进行操作。这便是封装的好处,把复杂而且容易出错的部分交给编译器来实现。

2.2.2 封装的性能问题

封装就是将事物的内容和行为都隐藏在实现里,用户不需要知道其内部实现。但有得必有失,有时候我们无法保证封装的高效性。C++对封装的实现到底有没有性能上的损失呢?要是有的话,有多少呢?

请看下面的测试程序和相应的代码注释

//////////////////////////////////

//Source Code 2.2

//C++ class封装的性能

const double COUNT=1000000;

#include <iostream.h>

#include <time.h>

#include <sys/timeb.h>

void showtime()

class Test{

public:

Test(){i=0;}

int foo(){return 0;}

double i;

};

int foo(){return 0;}

double i=0;

void main(void){

double j=0;

Test t;

showtime();

for(j=0;j<COUNT;j++)i++;

showtime();

for(j=0;j<COUNT;j++)t.i++;

showtime();

for(j=0;j<COUNT;j++)foo();

showtime();

for(j=0;j<COUNT;j++)t.foo();

showtime();

i++;

t.i++;

foo();

t.foo();

}

//测试在英特尔赛扬800MHz CPU

//VC++ 6.0 ,Windows 2000环境下完成

//定义循环次数

//输出毫秒级精度的时间函数,实现代码略

//循环控制变量

//Test类的实例

//以下代码为性能测试,输出循环运行时间

//时间精确到毫秒级,i++和t.i++运行时间相同

//时间精确到毫秒级,foo()和t.foo()调用运行时间相同

//代码检验,输出对变量和函数访问的实际汇编代码

//0040128E fld qword ptr [i (0042f220)]

//00401294 fadd qword ptr [__real@8@3fff8000000000000000 (0042b050)]

//0040129A fstp qword ptr [i (0042f220)]

//004012A0 fld qword ptr [ebp-10h]

//004012A3 fadd qword ptr [__real@8@3fff8000000000000000 (0042b050)]

//004012A9 fstp qword ptr [ebp-10h]

//004012AC call @ILT+35(foo) (00401028)

//地址 00401028 处为一跳转指令,指向foo()函数的入口

//004012B1 lea ecx,[ebp-10h]

//004012B4 call @ILT+40(Test::foo) (0040102d)

//地址 0040102d 处为一跳转指令,指向Test::foo()函数的入口

从上面的结果我们可以知道,对类中封装了的数据和函数进行访问的时间与访问未封装的数据与函数相同。从上述反汇编出来的代码中我们可以发现,程序对封装数据和未封装数据的访问方式,在汇编代码的级别上是完全一样的。下面来看对函数的访问。

调用foo()生成的汇编代码为

004012AC call @ILT+35(foo) (00401028)

而调用类成员函数生成的代码为

004012B1 lea ecx,[ebp-10h]

004012B4 call @ILT+40(Test::foo) (0040102d)

这条指令引起了性能问题吗?前面进行的时间测试告诉我们这两个函数运行的时间是相同的,而且,针对一个函数调用过程,编译器会产生至少上百条汇编代码,因此,这里多出来的一条指令,在考虑性能问题的时候,完全可以忽略。那么,这条指令的作用是什么呢?在2.2.1 类成员函数的调用方法这一节中我分析了this指针的作用和实现,这条指令,便是起这个作用,具体的细节,请参考2.2.1节。

通过上面的分析,我们知道封装在普通成员变量和普通成员函数方面没有引起性能上的额外开销,参考C++ Class的内存格局一节,我们可以很容易的发现封装对静态成员也没有任何的性能开销,那么,虚函数呢?这的确是一个问题。

系统访问虚函数,实现要通过指针索引虚拟函数表,并且考虑到派生类对虚函数的改写,虚拟函数表实际上是在运行期才进行绑定的。关于虚函数的详细分析,将在多态一部分中展开。

根据权威测试,C++由封装而引起的性能损失相比同类的C语言,大概在5%以内。5%的性能损失,换来面向对象的编程,我想,任何程序员都是会做出明智的选择的。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有