第二十章  类/class (一) 封装

 

23.1 从“我吃饭”开始

23.2 从“结构”到“类”

23.3 类的成员数据与成员函数

  23.3.1 成员数据初始化的疑问

  23.3.2 成员函数的实现

23.4 封装

  23.4.1 私有成员/private member

  23.4.2 保护成员/protected member

  23.4.3 公有成员/public member

  23.4.4 “封装”的作用

23.5 作业

 

23.1 从“我吃饭”开始

 

我吃饭……

其中,“我”是一个变量,“我”的类型是“人类”;

“吃”是一个函数。

“饭”也是一个变量,它的类型是“食物”。这里用于做函数“吃”的参数。

 

“我吃饭”!这是一种面向对象的思想的表达。其中的对象是“我”。以我为中心。我因为是人类,所以具备“吃”这种能力。如果说“桌子吃饭”,那么编译器会报错,因为桌子属于家具类,而家具不提供“吃”的函数。

C++是一种具备面向对象能力的编程语言,所以,用C++来表达“我吃饭”这样一件事时,它的代码风格贴近这种人类的自然语言,即:我.吃(饭);“我”是一个对象,“吃”是“我”所属对象(人类)的一个函数,而“饭”是函数参数。

换成C语言,因为它不具备面向对象的设计思想,所以,它只能说成:“吃(我,饭)”。“吃”是函数,“我”和“饭”是两个参数。没有人规则一定要把“我”作为第一个参数,你尽可写成“吃(饭,我)”。二者比较,面向对象的最基本的好处或许您已经有所体会:自然,从而不容易出错。

 

23.2 从“结构”到“类”

 

上一章我们学习了结构(struct)。(强烈建议你暂时放下新课程,重温一下struct)。

结构让我们具备了把多种相同或不同的类型,组成一种新类型的能力。

比如上一章讲的“宝宝/BaoBao”这一结构,它的组合为:

 

struct BaoBao

{

    char xingMing[11]; //用字符数组,来存储姓名

    int shenGao; //身高,整型

    float tiZhong; //体重,实型

};

 

通过struct,我们通过将简单数据类型(int,float,bool……)或简单数据类型的数组、指针,组合成一个新的数据类型。由此,我们在用程序表达复杂的现实世界时,更接近了一步。但是别忘了,我们说过世界是由“数据”和“动作”组成的。光能定义出各种数据类型,还只是编程世界的一半。你可能会说,我们有函数啊,函数不是可以表达“动作”?

没错,比如说,宝宝肯定有“吃”的动作,所以我来声明一个“吃”的函数。为了直观,我们的函数命名为“Chi”。并且我们假充有一种数据类型叫“饭”,同样用拼音Fan表示,首字母大写,而小写的fan用来做形参。

 

void Chi(Fan fan);

 

乍一看,感觉这个函数这样声明也就对了。“吃饭”函数嘛,有“吃”又有“饭”……可仔细一想,谁吃饭啊?这个光有吃的动作和吃的东西,和我们前面的“宝宝”数据类型有何关系?所以,显然不够,需要再加一个参数,用于传一个“要吃饭”的宝宝进去。因此函数变成:

 

void Chi(BaoBao bb, Fan fan);

 

得,“吃我饭”的表达出来了。过程化的编程,其设计重在关心“过程”,比如:“如何吃”,但这个世界要求我们完整地关心“谁?如何吃?吃什么?”。事实上,同一样一个“吃”,宝宝吃的动作,和一个大男人吃的动作;或者,吃饭还是吃奶的动作?怕是完全不一样。如果真写,就不得不写很多版本的“吃”这一函数:

假设用DNR表大男人(晕,好像大女人也可以?):

void DNRChi(DNR dnr, PingGuo pg); //吃函数版本一:大男人吃苹果

void DNRChi(DNR dnr,Fan fan); //吃函数版本二:大男人吃饭

void BaoBaoChi(BaoBao bb, Nai nai); //吃函数版本三:宝宝吃奶

void BaoBaoChi(BaoBao bb, Fan fan); //吃函数版本四:宝宝吃饭

......

 

这样的函数还可以有很多。函数多或许并没有错,必竟所要表达事物本来就复杂。然而问题是我们如何去理解,区分,记忆这些函数呢?仅靠函数名和函数参数的不同吗?在超市付款时,看过收款员拉开过放钱的小抽屉吗?拉开一看,都是钱,但10元的5元,100元的及硬币分门别类地放好……聪明的你一定会提出:如果能把函数也归类就好了……这就有了“类”,英文称为:class。

 

请仔细看,下面示例的class定义里,加入了一个函数:

 

class BaoBao

{

    char xingMing[11]; //用字符数组,来存储姓名

    int shenGao; //身高,整型

    float tiZhong; //体重,实型

 

    void Chi(Fan fan); //加入“吃”的函数。  

};

 

这算是一个“突变”——我们一直说着的“动作”与“数据”从这里开始合二为一,表面看来或许不过如此:无非是在类的定义里,同时可以包括数据及函数。然而却由此开启了“面向对象”世界之门。如果你喜欢武侠,那你可以把它看成一门语言打通了任督二脉……

 

类的数据定义里,出现函数,那么,这个函数的声明它占用类的大小吗?

 

来看两个数据定义,前者是struct,后者是class。前者没有包括函数,后者包括一个函数。其余的数据定义完全一样。

 

struct SBaoBao

{

   char xingMing[11];

   int shenGao;

   float tiZhou;

};

class CBaoBao

{

   char xingMing[11];

   int shenGao;

   float tiZhou;

 

   void Chi(int a); //参数可不能用Fan了

};


   然后,我们来做个比较:

 

[略]

 

23.3 类的成员数据成员函数

 

成员?长这么大,肯定填写过什么“家庭成员”的样的表格。我们进行的数据定义或函数定义,它们之所以前面有没加个“成员”的修饰,是因为它们都没有一个家,哎,谁不想有个家呢?在C#和Java里,所有数据及函数都必须有个家才可以存在,而在C++里,允许数据或函数可以没有家(不属于某个类);也可以允许有个家(属于某个类)。

 

class CBaoBao

   //下面就是CBaoCBao类的成员数据:

   char xingMing[11];

   int shenGao;

   float tiZhou;

 

   //而这个是CBaoBao类的成员函数:

   void Chi(int a);

};

 

类的成员数据和成员函数,都称为类的成员(像是一句废话?)。

 

23.3.1 成员数据初始化的疑问

我们以前常有这样的代码:

 

int a = 100; //定义一个变量,同时给它赋值为100.

 

可是,你一定一定要明白了,当我们在定义一个类或一个结构时,我们只是在“组装”一个新的数据类型。而并没有实际定义一个变量,所以C++不允许在定义一个类的内部,对它的成员数据赋值。下面的代码是错误的:

 

//在定义一个类时,试图初始化它的成员数据!不行!

class CBaoBao

{

    int shenGao = 70; //不行!

    ...

};

那么,当我们定义某一个类的具体变量时,这个变量里的成员数据初始值是多少?理论上,它们将是随机的值,也就是不能预定的值。

 

... 

CBaoCBao baobao1;

...

 

定义完baobao1之后,bao1bao1里的shenGao是多少?不知道!

那么,如何给某个类变量的成员数据一个初始的值(即默认的值)?下章我们会讲到。

 

23.3.2 成员函数的实现

我们可以在类里头加了函数(声明了类的成员函数),比如在前面的BaoBao类加了函数:Chi(...);可是我们还没有实现这个函数呢。

 

一个类的成员函数一般在类的外部定义,但要注意它和普通函数定义时的区别。

 

//定义,或称为实现CBaoBao的成员函数:Chi:

void CBaoBao::Chi(int a) //和普通函数区别:类的成员函数定义时,必须加上类名和::

{

   //...和普通函数一样,这里是函数体

}//和普通函数一样,这里没有分号

 

类成员函数也可以直接在类的体内定义,但此时就不必写类名加::了。

 

class CBaoBao

   //下面就是CBaoCBao类的成员数据:

   char xingMing[11];

   int shenGao;

   float tiZhou;

 

   //而这个是CBaoBao类的成员函数:

   //并且直接定义:

   void Chi(int a) //没有分号

   {

      //函数体...

   } //没有分号了

};

 

不过,直接在类体内的定义的成员函数,将被默认当作inline(内联)函数。关于内联函数,大家可以找一找前面函数章节。

23.4

从有了“类”开始,C++的世界越来越有趣了。前面说类就像一个家,家里有成员(数据或函数),现在,我们还要讲“访问”类的成员……想像有个类叫“美女”。

 

class MeiNu //美女类!

{

   int XW;  //胸围

   int YW;  //我就不说了噢 :)

   int TW;  //我还是不说了噢 :))

};

 

我看到部分学员在想入非非了,这可不行。请打开CB,新建一个“控制台/Console”工程。然后把上面新建工程后默认出现的Unit1.cpp中的main()函数之上。(偷偷说一声,后面的章节里,我们学习C++的也可开始慢慢有在Windows下的工程了!因为我们学习类了嘛,任督二脉都打通了,当然得来点更有意思的……)

 

(在Unit1.cpp里加入MeiNu类)

 

然后……当然是定义一个美女了!我们就在main函数里定义了,我不贴图了,你们对着课程,自己往CB里添代码。

 

int main(int argc, char* argv[])

{

    MeiNu zhaoWei; //美女赵?
      

    return 0;

}

 

开始有些为难了,赵薇的三围是多少?甭说她了,一般地说,通常美女的三围是多少啊?上网查一下吧……

哈哈,终于找到了,不过是三版女朗乔丹的。郁闷中……88了赵薇。现在代码为:

...

MeiNu jordan; //now is 乔丹!

 

jordan.XW = 34;

jordan.YW = 24;

jordan.TW = 34;

...

 

按Ctrl+Shift+S,保存Unit1.cpp和工程,工程我就命令为MeiNuPrj.bpr了。

 

然后按Ctrl + F9.试图编译一下!可是,可是,编译好像说:对不起,乔丹的胸围无法访问……(如图)

(not accessible 就是 “无法接触到”,或“无法访问” )

 

我们想给乔丹设置一下三围,可是C++编译器竟义正辞严地拒绝了!这是怎么回事?

因为,类/class对它的成员(数据或函数),有“保护”机制。不允许“外人”随便访问到它的成员。这也就是传说中“面向对象”的三大基石之一“封装性”

 

[略]

23.4.1 私有成员/private member

[略]

 

从private:开始,后面本类的成员数据或函数,都将是私有的,除非我们又加了一个新的访问等级限制关键字。

23.4.2 保护成员/protected member

保护成员也不能在类的外部直接访问,但可以在该类的子类(或称为派生类)中访问。所谓子类或派生类,我们后面的章节才会讲到。大致的意思,先不妨认为是,你们家的东西,外人不能用,但你儿子或儿媳(他有自己的家)可以用……这是不合适的比喻。只是为了感性理解一下私有和保护的一种区别。

23.4.3 公有成员/public member

[略]

23.4.4 “封装”的作用

 

说着说着,这问题就来了。为什么要用private或protected来保护类的成员啊?大家都是公有的,都可以直接访问,多方便啊?这问题如果反过来问,就是面向对象三大基石之一“封装”有什么好处?

封装的好处,两点,并且两点相辅相成。

 

[略]

 

23.5

[略]