C++ 随笔-2,封装
封装概念很普遍,类似也可以称之为“包装”。人要包装,商品要包装,一切的一切都需要包装。为何会有如此需求呢?究其原因无非有二,其一是炫耀,其二是遮丑。经过包装后其价值会有不同程度的提高,这是因为通过包装展现出来的是其最有价值的一面。另一方面通过包装又可保证其内部具有一定的私密性,这是包装(或封装)的一个相当重要的特性。
现在来考察一下C和C++中的封装。在C中我们知道有数据结构struct类型,它就是某种程度上对数据的封装。但仔细看来这种封装只是将一堆相关的数据放在一起,从外面来看并没有多大新意,就内部而言又丝毫没有一点私密性。所以如此的包装既无从炫耀又无法遮丑。反过来分析一下C++中的类class类型,它是对“状态机”的封装(或抽象点说就是对“对象”的封装)。一方面,类似struct类型,class包含有一堆相关的数据(或称状态);而另一方面其又包含有一堆相关的“处理功能”,合起来正好就是一完美的状态机。这一点是值得大大地炫耀一下的。有了其光鲜的外表还不够,class类型还确保了其内部的私密性。这种私密性在语法层面上保证了软件系统的完整性和安全性,体现了软件工程学中的模块分割原则。
考察一个状态机的封装
我们知道一个状态机若用数学形式表达的话就是:
Y = Fy ( X, S ) S = Fs ( X, S )
其中Y为输出,X为输入,S为状态。
如果是有限状态机,则S可用有限离散量表示,即S0,S1,…,Sn。下面给出一个有限状态机的C++程序框架。注意,这只是对有限状态机数学形式的直接封装。
// 有关"状态机"的封装
enum status_type { S0, S1, S2, S3, S4,…,Sn }; class class_SM { protected: status_type status; virtual int Fy( int x ); virtual void Fs( int x ); public: class_SM( void ) { status = S0; } class_SM( status_type S ) { status = S; } int Do_it( int x ); ~class_SM() {} };
int class_SM :: Fy( int x ) { // return Fy( x, status )
return x; }
void class_SM :: Fs( int x ) { // status <- Fs( x, status ) }
int class_SM :: Do_it( int x ) { int result; result = Fy( x ); Fs( x ); return result; }
这个封装有壳,有头,有尾,还有躯干。下面就将其解剖一下:
首先是“壳”
class class_SM { };
由关键字class引入了一个类型class_SM,注意,在C++中同样可以由struct引入一个类型,这在C中是不可以的。
其次来看一下“头”和“尾”
class_SM( void ) { status = S0; } class_SM( status_type S ) { status = S; } ~class_SM() {}
在类中若用类名定义一个函数,则此函数具有特殊的地位,并给它一个称谓——构造函数。这个特殊的函数就是类的“头”,对象 (类的一个实现)的诞生就是由构造函数引领的。当进入对象所在的作用域时,系统首先分配空间,紧接着就自动调用“合适”的构造函数来初始化整个对象。在这里我们看到有两个构造函数,它们的形式差异就在于输入参数的不同,由不同参数形式来区分不同函数的调用是C++的另一个重要的特性,在此不作详叙。下面给出两种不同的对象定义:
class_SM SM; // 调用class_SM( void ) { status = S0; }
class_SM SM( S0 ); // 调用class_SM( status_type S ) { status = S; }
在前面可以看到有这样一条定义语句:
enum status_type { S0, S1, S2, S3, S4,…,Sn };
它是被放在类的定义“壳”外的,这是因为类的使用者有可能自己决定状态机的初始状态(即调用class_SM( status_type S ) { status = S; })。若没有这样的选项(即只有第一个构造函数class_SM( void ) { status = S0; } ),则就可以将定义enum status_type { S0, S1, S2, S3, S4,…,Sn }封在类的定义“壳”内,不对外开放。
看过了“头”再看“尾”,在class_SM类中有一个奇怪的函数,就是在类名前加上“~”符号的那个函数,此函数被称之为析构函数。虽然在此未定义任何具体的操作,但我们从它的调用位置就可以看出它的特殊性。析构函数是在对象消亡前被系统调用去处理相关后事的那个函数,所以它通常被调用的位置是在即将退出对象所在的作用域时。这样我们就清楚的知道,对于类的诞生和死亡,系统会自动地调用一些相关的函数去处理某些重要的事情,而这些事情的具体内容是可以由程序设计者来参与的。这就是类封装的一大特点,且具有相当高的价值。
另外对象还可以“动态”地诞生和消亡(使用对象指针和相关的系统操作),在此仅点到为止。
有了头和尾,当然不能没有躯干。在此我们所言的躯干就是下列一些成员:
status_type status; virtual int Fy( int x ); virtual void Fs( int x ); int Do_it( int x );
其中status是类型status_type的数据变量,称之为类class_SM的“成员变量”;而Fy、Fs、Do_it是函数,称之为类class_SM的“成员函数”。另外要说明的是,前面所提到的构造和析构函数同样是类class_SM的“成员函数”,不过由于这两类函数地位比较特殊,所以给他们另外起了两个特殊的称呼。一般的成员函数调用是由程序设计者自己显式指定的,并非由系统自动调用。
类封装的私密性
现在我们已经大致的了解了一个类的封装的基本结构和各部份粗略的功能特性。下面进一步看一下其中有点象标号的那些玩意儿:
pretected:
public:
private:
最后一个虽然未出现,但它却是类的缺省特性(未加说明就是private)。
上面所列的三个东西并非是标号,就单词本身它们是C++的预留关键字,在类中使用它们来说明其下面的成员(包括成员变量和成员函数等)具有相应的被使用权限(注意未加说明则权限就是private)。由此可见,类封装不仅把它的成员包装了起来,而且限定了使用它们的权限。缺省情况下,其特性为private(私密的),这样从类的外面就看不见它们,这非常类似于我们日常所说的私密性。私密性是C++中类封装的又一大特点,它使类这种新的类型更加符合软件工程学中的模块基本准则。
当然如果封装在类中的成员全都是私密的,这将是铁桶一个,毫无意义,就算阿拉伯妇女都会亮出她们那双美丽的眼睛。为了能把类的光辉炫耀出来,就必须把有使用价值的成员呈现给外界,而这就是有public(公用)来加以说明的。使用public将改变其下面所列成员的使用权限,使其能被外界存取或调用。这里还有一个定义使用权限的关键字protected,它与类的继承性有关,在此不阐述。
类的实例化和对象的使用
类被定义以后只是一堆代码(特殊情况下会伴随一些表格),必须将其实例化后才会作为“对象”实实在在的存在,随后被使用。在此需强调的是“类”不是“对象”,反过来“对象”也不是“类”,它们不是同层次的概念,这有点类似于“类型”和“变量”的关系。类是产生对象的模子,而对象则是由类这个模具生产出来的产品。所以一个类可以产生多个对象,这个过程就是类的实例化。
类被实例化后形成的实体——对象,从存储在内存中的形式上看类似于结构(struct),系统为每个对象的所有成员变量分配相应的空间。每个同类的对象共享它们的成员函数,换句话说就是,类中所定义的代码在实例化过程中并未被复制。
类的实例化过程除了分配空间外还有一件相当重要的事情就是调用构造函数,这是和一般变量或结构类型的实例化具有本质差异的地方。通过构造函数,对象在被使用之前自动对自己的状态进行初始化,所以对象在诞生之时就已作好了被使用的准备,而一般的变量或结构在引用前必须对其进行赋值。
作为类实例化的产物——对象,一旦诞生就可以被使用。具体来说,使用对象分两个方面,其一是对其成员变量的赋值或引用(虽然并不建议这样做),其二是调用其成员函数。关于成员函数的使用存在有几种异化的形式,如对象的“复制”和操作符的“重载”。在这种变异的情况下,对象仅以其名出现(如出现在表达式内),但实际上只是对它的某一特定的成员函数进行了调用。相关内容在此不作详表。
现在再回过头来看一下前面定义的状态机,其内有一成员函数int Do_it( int x )。它是唯一能被外界使用的成员(注意:构造和析构函数是对象诞生和消亡时由系统调用的)。沿用前面的实例化语句,相关对象的使用形式为:
Y = SM.Do_it( X );
从上面的调用语句可以看出,Do_it表面上使用了两个参数——X、Y。但实际上还有一个隐形的参数,就是指向对象SM数据存储区的指针变量——this。因此可以这么说,就是若成员函数一个参数也没定义的话,也存在着一个指针(this)作为函数的隐形参数。这就是类封装的“代价”,虽然并不是很大,但毕竟是要付出的。
现在来分析一下为何要有这样一个隐形参数——this,传给成员函数。就拿前面定义的另外一个成员函数Fs( int x )来看,形式上它只有一个参数x作为输入,但实际上它还要使用类的一个成员变量status。对于类class_SM的不同对象,成员变量status的地址是不同的。因此必须将相应对象的成员变量存储区地址传给成员函数,因为成员函数已经知道status的偏移量,所以只要将成员变量存储区的首地址this(这也是对象SM的地址)传给成员函数即可。 |
|