好吧,以下是我们的计划:我们首先要分析为什么数据成员不应该是公有的,然后继续分析为什么数据成员也不能是protected的。然后就引出本条款的结论:数据成员必须是私有的。结论引出,计划完成。
那么,数据成员为什么不能是public的?
让我们从讨论语义一致性问题开始(另请参见条目18)。如果数据成员不是公有的,那么客户要想访问对象就只剩下成员函数一种方法。如果公有接口中所有的东西都是函数,那么客户在期望访问类成员时,由于一切都是函数,所以就可以任意使用,而不用担心是否需要使用括号。在整个过程中,这样做可以让你节省大量踌躇不定的时间。
但是也许你会发现,并没有强制规定来要求语义的一致性。那么你是否会发现:使用函数可以让你更精确地控制数据成员的访问权?如果把一个数据成员定义为public的,那么每个人对其都拥有“可读可写”的访问权,但是如果你使用函数来为数据成员赋值,或者获取数据成员的值,那么你可以将其实现为“禁止访问”、“只读”以及“可读可写”几种级别的访问权;嘿,如果需要,你甚至可以将其实现为“只写”的访问权:
class AccessLevels {
public:
...
int getReadOnly() const { return readOnly; }
void setReadWrite(int value) { readWrite = value; }
int getReadWrite() const { return readWrite; }
void setWriteOnly(int value) { writeOnly = value; }
private:
int noAccess; // 此int值禁止访问
int readOnly; // 此int值拥有只读级别访问权
int readWrite; // 此int值拥有可读可写级别访问权
int writeOnly; // 此int值拥有只写级别访问权
};
很有必要将访问权管理得如此有条不紊,因为许多数据成员本应该被隐藏起来。并不是每个数据成员都需要一个取值器(getter)和一个赋值器(setter)。
还不是十分肯定?那么现在是时候使出杀手锏了:“封装”。如果你通过程序实现了对一个数据成员的访问,那么你就可以使用一次计算来代替这个数据成员,使用这一个类的人完全不会有所察觉。
请看下边的示例,假设你正在为一种自动装置编写一个应用程序,这一装置可以监视通过汽车的行驶速度,当一辆汽车通过时,这一应用程序就会计算出它的速度,然后将这一数值保存到一个小型数据库中,其中保存着曾通过所有车辆的速度数据:
class SpeedDataCollection {
...
public:
void addValue(int speed); // 添加新的数据值
double averageSoFar() const; // 返回速度的平均值
...
};
现在请注意成员函数averageSoFar的具体实现问题。一种实现方法是:为类添加一个数据成员,让它保存速度的平均值,随数据库的改动更新这一成员的数值。当调用averageSoFar时,它仅仅返回这一数据成员的值。另一种做法是:在每次调用averageSoFar时都计算出这一平均值,此时需要检查数据库中所有的数据值。
因为第一种手段(保存即时更新的平均值)中,你需要为保存即时更新平均值、累计总和以及数据的个数这几种数据成员分配空间,因此这一方法使得SpeedDataCollection对象都变得更大一些。然而,averageSoFar却十分的高效。可以把它写成一个内联函数(参见条目30),所做的仅仅是返回这一即时更新的。相反地,在需要时进行计算,averageSoFar速度上会慢一些,但是SpeedDataCollection对象的体积更小。
二者孰优孰劣,谁又能断定呢?在一个内存较为局促的机器(比如嵌入式的公路设备)上,并且该应用程序不会频繁的调用平均值,那么实时计算的方案就更为优秀。相反地,在平均值需要频繁使用,速度是程序的关键,内存不是问题的情况下,则更应采用保存一个即时平均值的方案。最重要的一点是,在通过成员函数访问平均值时(也就是“封装”),你可以交替使用这两种实现方案(当然,你可能还会想到其它重要的问题),客户顶多要做的一件事就是重新编译一下代码。(即使编译所带来的不方便也可以排除。参见条目31中介绍的技术。)
将数据成员隐藏在函数式接口的背后可以使得任意种类的实现方法更加灵活多变。比如说,这样做可以非常容易地做到下面几件事情:在数据成员进行读写操作时告知其他对象,验证类的恒定性和函数运行前后的状态,在多线程系统下进行同步操作,等等。如果让Delphi或C#的程序员使用C++,他们会发现C++这一特性与这些语言中的“属性”很相像,只是C++中需要添加一对括号。
封装是C++的一个博大精深的特性。如果你对客户隐藏了数据成员的话(也就是将它们封装起来),你就可以确保类永远保持一致性,这是因为只有成员函数可以影响到数据成员,同时你也保留了在以后改变具体实现方法的权利。如果你不将这些方法隐藏起来,那么你很快就会发现,即使你拥有类的源代码,对公有接口的修改也是受到严格限制的,因为这样做会破坏许多客户端代码。公有就意味着未封装,同时从实用角度讲,未封装就意味着无法更改,较为广泛应用的类更甚之。然而广泛应用的类最需要使用封装,因为它们可以从“具体实现可以不断改良”这一点上获得最大程度的收益。
上面的分析对于protected数据成员也适用。尽管二者乍看上去有一定的区别,但实际上它们是完全一致的。在使用public数据成员时,我们分析了语意一致性问题和访问权条理性问题,这一分析过程对于使用protected数据同样适用。但还有一个问题——封装。protected数据成员不是比public的更具有封装性吗?从实用角度讲,你会得到一个令人吃惊的答案:不是。
条目23中将介绍这一问题:C++中封装程度与代码的健壮程度(这段代码相关部分被修改时抵御破坏的能力)成正比。因此,数据成员的封装程度与代码的健壮程度也是成正比的。比如,当一个数据成员从类中移除时(可能你期望使用一次计算来代替,就像上文中的averageSoFar一样),代码是否会遭到破坏,将取决于封装程度。
请考虑这个问题:假设我们有一个public数据成员,然后我们把它删除了,那么将有多少的代码将遭到破坏呢?我们说,所有使用它的客户端代码。这将是一个无法预知的巨大数字。公有数据成员就是这样完全没有封装性的。但是继续考虑:我们有一个protected数据成员,然后我们把它删除了,此时将破坏多少代码?我们说,所有使用它的派生类,这同样是一个无法预知的巨大数字。由于在这两种情况下,如果数据成员被更改了,那么将会为客户带来无法估量的损失,因此可以说protected数据成员与public的一样没有封装性。这是违背直觉的,但是有经验的库实现者会告诉你,这是千真万确的。一旦你声明了一个public或protected的数据成员,然后客户开始使用它,你就很难再对这一数据成员做出修改。因为这样做会带来太多的代码重写、重新测试重新编写文档和重新编译等等工作。按封装的理念来说,对于数据成员仅仅存在两个层次的访问权,那就是:private(可以提供封装性)和非private(不提供封装性)。
时刻牢记
l 要将数据成员声明为私有的。这样可以让客户端访问数据时拥有一致的语义,提供有序的访问控制,强制类保持一致性,为类作者提供更高的灵活性。
l protected并不会带来比public更高的封装性。