第3章 感受(一)——3.11. Hello object 封装版

白话C++

3.11. Hello object 封装版

小A做某事,成功了;小B做另一件事,失败了。分析其间的差别,无非两种可能:一是小A比小B牛,犯的错误少;二是小A做的事比小B做的事简单。

可见要做成一件事,第一要尽量让事情简单,第二尽量让人少犯错误。

除了个别世界观有问题的以外,多数编程语言都希望通过使用它,事情能变得简单些,程序员能变得强一些。

C++ 也如此。前面提到的“派生”、“多态”,这二者让C++语言可以更加逼真地表达现实问题。因此可以认为,“派生”及“多态”是“OO世界”中两样锋利的武 器,程序员拿着它们驰骋疆场,杀戮无数问题。显然,这是C++让事情变得简单的重要方法。紧接着,我们又要提到了“封装”,C++通过“封装”,允许将重 要数据的封闭起来,避免重要数据被错误地直接使用。 它是一项让程序员尽量少犯错误的重要方法。

 

轻松一刻〖轻松一刻〗:“封装”究竟封装了什么?

“封装”这项技术,表面上它保护的是代码中的数据,其实它是一件坚硬的盔甲,保护住的是程序员一颗容易犯错的脑袋瓜,还有程序员一颗脆弱的心。

 

3.11.1. 类型即封装

其实,当我们通过struct来定义一个新的数据类型时,就是一种“封装”。

struct Person
{

string name;
};

想像我们还想关注“人类”的年龄信息,那么我们可以Person类再添加一个成员数据。

struct Person
{

string name;
int
age;
};

看,和“人”有关的数据,被我们统一包装在Person这个类型信息中。假设我们要在程序中管理到2个人,那么使用Person这个类型,代码如下:

Person xiaA;
Person zhiLing;

2个人,对应定义两个对象,直观,轻松。如果不使用自定义类型来封装人的信息,代码该如何写呢?

string nameOfXiaA;
string nameOfZhiLing;
int
ageOfXiaoA;
int
ageOfZhiLing;

是 不是有一种所有数据被“拆散”得“支离破碎”的感觉?如果有,恭喜你,说明你具备程序员的气质;如果没有,也恭喜你,说明你拥有机器一般的超强能力的大 脑。 不过,当前Person只有两个信息,如果是10个呢?人脑一定会记不过来。所以,定义一个拥有复合信息的类型,本身就是一种封装。

不过,C++还有更强劲的一层封装,那就是,一个(复杂)数据类型,可以用来设置访问类成员的权限。

 

3.11.2. 私有、保护、公开

C++允许对自定义类型(struct和class)中的成员(数据、函数)及嵌套类型,指定三种访问权限:公开、保护、私有,对应的关键字是:public、protected、private。

    • 私有/private:只允许在该类型内部进行访问。
    • 保护/protected:允许在该类型,以及其派生类系内部进行访问。
    • 公开/public:允许在该类型及派生类系内部访问,也允许在类型外部进行访问。

为 什么有区分“公开”、“私有”数据呢?这其实也是对真实社会的一种对应。C++的访问规则是针对“类型”而言,为了直观,我们暂时先混淆一下“对象”与“ 类”的区别。比如“人类”有一个“胃”,“胃”数据是典型的“私有”数据。在人的内部,食管、直接连着胃,神经、肌肉控制着胃,但在人的外部,你会允许把 他人把一个苹果直接塞进你的胃吗?我们需要一个“吃东西”的成员函数,那么,“吃东西”就是一个public的成员。

 

qsyk〖轻松一刻〗:“胃”真的是“私有数据”吗?

那时——医生把长长的胃镜导管强行从我嘴里塞到我胃里,医生很兴奋地指着屏幕说:看,看,这就是你的贲门……我眼泪哗的下来了——就在那时,我突然意识到,胃应该是人类的私有数据,只不过在医生面前,它被暴力破解了。C++也一样,当你只为图一时方便,想把本应private的数据设置成public时,你应该去医院感受一下胃镜。

 

不谈胃了,医学我实在了解太少,还是谈社会学吧:Person有一个数据:name。我们来考虑一下,对于人类来说,“姓名”应该是“private”,还是“public”呢?

“当然是公开(public)的!”有些人略作思索,“名字就是要给别人叫的,如果是私有的,那人类取名做何用?”——这个回答似是而非。

如果“姓名”成为“人类”的公开数据,那就意味着在类就外部可以直接修改一个人的姓名。没错,我们其实一直在这么做的,复习一下曾经的代码:

int main()
{

003
Person xiaoA;
004
xiaoA.name = "Xiao A";
……
}

我们在main函数里——而不是在Person的内部——通过004行代码,直接修改了xiaoA的名字。认真地考虑现实生活中,最普遍的情况是:名字应该在出生时设定,然后一辈子都不再被修改。C++是如何实现此类逻辑呢?

除了“私有”与“公开”之外,C++还提供夹在二者之间的“保护”访问权限。采用“保护”访问权限的成员,允许在类内部,以及派生类内部访问,而不允许在其它范围内访问。

 

3.11.3. 语法

struct Person
{

public
:
/*
此处成员的访问权限为“公开”;
*/

protected
:
/*
引处成员的访问权限为“保护”
*/

private
:
/*
此处成员的访问权限为“私有”
*/

};

在类型定义中,加入访问权限关键字,并以“:”结束。其后所定义的成员,都采用该访问权限,直接到有新的访问权限关键字。

 

hint〖小提示〗:代码排版风格

1) 本书推荐把public等关键字与 struct 及一对花括号的起始列对齐。

2) 本书推荐以public、protected、private 的次序安排成员。

 

在本章之前,我们从未使用“publicprotectedprivate”三者之一,但我们的代码一直可以工作,这是因为对于“struct”,如果不提供访问权限关键字,则默认采用“public”修饰。也就是说:

struct Person
{

virtual
void Introduction();
string name;
};

等同于:

struct Person
{

public
:

virtual void
Introduction();
string name;
};

 

3.11.4. 应用

新建一个控制台应用项目,命名为“HelloOOEncapsulation”。复制上一小节“HelloOOVirtual” 项目main.cpp的最后代码内容,取代新项目的main.cpp的内容。如有必要,记得将main.cpp的文件编码改为“系统默认”。

首先将Person类型改造成“封装”版。

struct Person
{

008 public:
Person()
{

cout << "Wa~Wa~" << endl;
}


virtual
~Person()
{

cout << "Wu~Wu~" << endl;
}


virtual
void Introduction()
{

cout << "Hi, my name is " << name << "." << endl;
}

023
private:
std::string name;
};

区别就在于008行和023行分别添加的“公开”和“私有”的访问权限标志符。

按Ctrl + F9,编译代码……出错了。

编译出错

(图 28 有关访问权限出错的编译信息)

g++的编译信息在这方面提示得非常明了:24行的 name 成员是“私有”的,但是36行和85行的代码,都试图在Person类之外访问。

双击出错信息中具体某一行,会跳出对应行号的代码。

请先查看“24”行,现在,在派生类(Beauty)中访问基类(Person)的name成员,是非法的了。因为“私有”成员,只允许在当前类中访问,“保护”或“公开”的成员,才允许在派生类中访问。

再跳到“85”行,这回是在main函数内访问,现在,只有“公开”的成员才能满足了。可惜name成员已经被我们限定为Person的私有成员。

这两处的出错代码,还有一样不同,第24行代码要“读”姓名,而85行代码则试图修改姓名。

如何解决这两个问题?我们来为Person提供一个公开的成员函数,专门用来返回name。

struct Person
{

public
:
//此处略去构造函数与析构函数

019 string GetName()
020
{
021
return name;
022
}

virtual
void Introduction()

private
:
std::string name;
};

019~022行,我们为Person类,定义一个名为GetName的成员函数。该函数的返回结果类型为“string”类型,事实上它返回的内容是成员数据:name的值。

接下来,我们修改原036行,现041行处的代码,原来直接访问name,现在改为通过GetName()函数。

struct Beauty : public Person
{

virtual
~Beauty()


virtual
void Introduction()
{

041
cout << "大家好,我是美女: " << GetName() << ",请多多关照!" << endl;
}
};

再 次编译,编译出错消息少了一半。余的那一个,就是试图在main函数中修改一个新生儿的姓名的问题了。关于这个问题,有两种考虑方案。其一是“宽松型”, 即认为人出生后,可以先不要有名字,并且可以外部修改;其二是“严格型”,即认为人人出生后,就应及时起名,并且不允许外部修改。为了体现“封装”特性, 我们采用第二种。所以我们的做法是,将设置名字这个过程,移到Person的构造函数中去。

struct Person
{

public
:
Person()
{

cout << "Wa~Wa~" << endl;
cout << "为这个哇哇哭的人,起个名字吧:";
getline(cin, name);
}


//略去后面代码...
}

现 在,构造Person对象时,就会被要求输入姓名。构造Beauty的对象呢?由于派生类的构造函数会自动调用基类的默认构造函数,所以这个过程也将在美 女出生时起作用。如此,在main函数中输入姓名的代码就多余了,我们将它删除掉。另外,在调用自我介绍之前用于提示的那行代码,需要稍做修改。最终完整 代码如下:

#include <iostream>
#include <string>

using namespace
std;

struct
Person
{

public
:
Person()
{

cout << "Wa~Wa~" << endl;
cout << "为这个哇哇哭的人,起个名字吧:";
getline(cin, name);
}


virtual
~Person()
{

cout << "Wu~Wu~" << endl;
}


string GetName()
{

return
name;
}


virtual
void Introduction()
{

cout << "Hi, my name is " << name << "." << endl;
}

private
:
std::string name;
};


struct
Beauty : public Person
{

virtual
~Beauty()
{

cout << "wu~wu~人生似蚍蜉、似朝露;似秋天的草,似夏日的花……" << endl;
}


virtual
void Introduction()
{

cout << "大家好,我是美女: " << GetName() << ",请多多关照!" << endl;
}
};


int
main()
{

while
(true)
{

Person *someone;

cout << "请选择(1/2/3):" << endl
<<
"1----普通人" << endl
<<
"2----美人" << endl
<<
"3----退出" << endl;

int
sel = 0;
cin >> sel;

if
(cin.fail())
{

cin.clear();
}


cin.sync();

if
(3 == sel)
{

break
;
}


if
(1 == sel)
{

someone = new Person;
}

else if
(2 == sel)
{

someone = new Beauty;
}

else
//用户输入的,即不是1,也不是2,也不是3...
{
cout << "输入有误吧?请重新选择。" << endl;
continue
;
}


cout << someone->GetName() << "的自我介绍:" << endl;
someone->Introduction();

delete
someone;
}


return
0;
}

 

3.11.5. 常量成员函数

再次考虑一下“封装”,“封装”的目的之一是为了避免外界对类成员非必要的“访问”。而“访问数据”又可以分成两个操作:“读取数据”和“修改数据”(或者合称为“读写数据”)。

会有些数据,我们既不希望外部“修改”它,也不希望外部“读取”它——比如你的私房钱,你既不想让老婆知道,更不想它被没收。

会有些数据,我们允许外部“读取”它,但不允许外部“修改”它;——比如你们的新婚照,老同学来了,或许你会很开心地影集拿出来供大家“欣赏”,但若有不识相哥们想拿一张回去,并且准备将它PS一下,你一定会严辞拒绝。

至于,允许外部“修改”它,却不允许外部“读取”的它的情况比较少见。

相比两件事:“防止被读取”和“防止被修改”,显然“防止被修改”这件事更为重要。C++也在这方面提供技术保障,允许我们更加关注:“成员数据”是不是被修改了?技术之一就是“常量成员函数”。

请看以下修改过的代码:

struct Person
{

public
:
//……

string GetName()
{

name = "王二麻子";
return
name;
}

//……
}

天啊!成员函数GetName()的本意是读出成员数据name的值,但现在发生了什么?也许是程序员失恋了,也许是程序员喝醉了,也许是二者兼而有之,总之这哥们在代码直接修改了name于是造成了天下人只有一个名字。

C++认为,虽然成员函数,统统允许“访问”成员数据(因为它们同属一个类),但是仍有必要限定某些成员函数只允许“读取”成员数据,而不允许“修改”成员数据。这样的函数,就被称为“常量成员函数”。

定义常量成员函数,只需要函数体开始之前,加入const关键字。比如:

struct Person
{

public
:
//……

string GetName() const
{

name = "王二麻子";
return
name;
}


//……
}

 

zuoye〖课堂作业〗:感受“常量成员函数”

请打开项目,然后先在GetName()函数中添加将名字设置为“王二麻子”的代码,然后编译,你会发现编译成功了。然后再如上将GetName()改为常量版本,再次编译,这时编译将失败,请按F2查看编译出错信息,理解,并解决问题。

 

依据“常量成员数据”的定义,不难理解,一个常量成员数据如果要调用类中的其它成员数据,那么要求被调用的函数必须同样是“常量成员函数”。

白话C++
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南郁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值