Effective C++ 2e Item29
类和函数: 实现
C++是一种高度类型化的语言,所以,给出合适的类和模板的定义以及合适的函数声明是整个设计工作中最大的一部分。按理说,只要这部分做好了,类、模板以及函数的实现就不容易出问题。但是,往往人们还是会犯错。
犯错的原因有的是不小心违反了抽象的原则:让实现细节可以提取类和函数内部的数据。有的错误在于不清楚对象生命周期的长短。还有的错误起源于不合理的前期优化工作,特别是滥用inline关键字。最后一种情况是,有些实现策略会导致源文件间的相互联结问题,它可能在小规模范围内很合适,但在重建大系统时会带来难以接受的成本。
所有这些问题,以及与之类似的问题,都可以避免,只要你清楚该注意哪些方面。以下的条款就指明了应该特别注意的几种情况。
条款29: 避免返回内部数据的句柄
请看面向对象世界里发生的一幕:
对象A:亲爱的,永远别变心!
对象B:别担心,亲爱的,我是const。
然而,和现实生活中一样,A会怀疑,"能相信B吗?" 同样地,和现实生活中一样,答案取决于B的本性:其成员函数的组成结构。
假设B是一个const String对象:
class String {
public:
String(const char *value); // 具体实现参见条款11
~String(); // 构造函数的注解参见条款M5
operator char *() const; // 转换String -> char*;
// 参见条款M5
...
private:
char *data;
};
const String B("Hello World"); // B是一个const对象
既然B为const,最好的情况当然就是无论现在还是以后,B的值总是"Hello World"。这就寄希望于别的程序员能以合理的方式使用B了。特别是,千万别有什么人象下面这样残忍地将B强制转换掉const(参见条款21):
String& alsoB = // 使得alsoB成为B的另一个名字,
const_cast<String&>(B); // 但不具有const属性
然而,即使没有人做这种残忍的事,就能保证B永远不会改变吗?看看下面的情形:
char *str = B; // 调用B.operator char*()
strcpy(str, "Hi Mom"); // 修改str指向的值
B的值现在还是"Hello World"吗?或者,它是否已经变成了对母亲的问候语?答案完全取决于String::operator char*的实现。
下面是一个有欠考虑的实现,它导致了错误的结果。但是,它工作起来确实很高效,所以很多程序员才掉进它的错误陷阱之中:
// 一个执行很快但不正确的实现
inline String::operator char*() const
{ return data; }
这个函数的缺陷在于它返回了一个"句柄"(在本例中,是个指针),而这个句柄所指向的信息本来是应该隐藏在被调用函数所在的String对象的内部。这样,这个句柄就给了调用者自由访问data所指的私有数据的机会。换句话说,有了下面的语句:
char *str = B;
情况就会变成这样:
str------------------------->"Hello World