【转】http://www.cppblog.com/tiandejian/archive/2007/06/06/ec_22.html
第22条: 尽量将数据成员声明为私有的
好吧,直截了当的说,在这一条中:我们首先要分析为什么数据成员不应该是公有的,与此同时,继续分析为什么数据成员也不能是 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 值拥有只写级别访问权
};
很有必要将访问权管理得如此有条不紊,因为许多数据成员本应该被隐藏起来。并不是每个数据成员都需要一个赋值器和一个取值器。
还不是十分肯定?那么现在是时候使出杀手锏了:“封装”。如果你通过程序实现了对一个数据成员的访问,那么你就可以使用一次计算来代替这个数据成员,使用这一个类的人完全不会有所察觉。
请看下边的示例,假设你正在为一种自动装置编写一个应用程序,这一装置可以监视通过汽车的行驶速度,当一辆汽车通过时,这一应用程序就会计算出它的速度,然后将这一数值保存到一个小型数据库中,其中保存着曾通过所有车辆的速度数据:
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++ 中封装程度与代码的健壮程度(这段代码相关部分被修改时,抵御自身遭到破坏的能力)成正比。所以我们可以得出下面的结论:数据成员的封装程度与代码的健壮程度也是成正比的。代码遭到的破坏可能是:将某个数据成员从类中移除。(可能你期望使用一次计算来代替,就像上文中的 average 一样。)
请考虑这个问题:假设我们有一个 public 数据成员,然后我们把它删除了,那么将有多少的代码将遭到破坏呢?我们说,所有使用它的客户端代码。这将是一个。公有数据成员就是这样完全没有封装性的。但是继续考虑:我们有一个 protected 数据成员,然后我们把它删除了,此时将破坏多少代码?我们说,所有使用它的派生类,这同样是一个无法预知的巨大数字。由于在这两种情况下,如果数据成员被更改了,那么将会为客户端程序员带来无法估量的损失,因此可以说 protected 数据成员与 public 的一样没有封装性。这是违背直觉的,但是有经验的类实现者会告诉你,这是千真万确的。一旦你声明了一个 public 或 protected 的数据成员,然后客户端程序员开始使用它,你就很难再对这一数据成员做出修改。因为这样做会带来太多的代码重写、重新测试重新编写文档和重新翻译等等工作。按封装的理念来说,对于数据成员仅仅存在两个层次的访问权,那就是: private (可以提供封装性)和其它的一切(不提供封装性)。
铭记在心
l 要将数据成员声明为私有的。这样可以让客户端访问数据时拥有一致的语义,提供有条不紊的访问控制,强制类符合一致性,为类作者提供更高的灵活性。
l protected 并不会带来比 public 更高的封装性。