首页 > 程序开发 > 软件开发 > C++ >

Effective C++读书笔记(13)

2012-02-04

条款21:必须返回对象时,别妄想返回其referenceDon’t try to return a reference when youmust return an object一旦程序员抓住对象传值的效率隐忧,很多人就会一心一意根除传值的罪恶。他们不...

条款21:必须返回对象时,别妄想返回其reference

Don’t try to return a reference when youmust return an object

一旦程序员抓住对象传值的效率隐忧,很多人就会一心一意根除传值的罪恶。他们不屈不挠地追求传引用的纯度,但他们全都犯了一个致命的错误:他们开始传递并不存在的对象的引用。考虑一个用以表现有理数的类,包含一个函数计算两个有理数的乘积:

class Rational {
public:
Rational(int numerator = 0, int denominator = 1);

...

private:
int n, d; // 分子与分母

friend
const Rational operator*(const Rational& lhs, const Rational& rhs);
};

operator* 的这个版本以传值方式返回它的结果,需要付出对象的构造和析构成本。如果你能用返回一个引用来代替,就不需付出代价。但是,请记住一个引用仅仅是一个名字,一个实际存在的对象的名字。无论何时只要你看到一个引用的声明,应该立刻问自己它是什么东西的别名,因为它必定是某物的别名。以上述operator*为例,如果函数返回一个引用,它必然返回某个既有的而且包含两个对象相乘产物的Rational对象引用。

当然没有什么理由期望这样一个对象在调用operator*之前就存在。也就是说,如果你有

Rational a(1, 2); // a = 1/2

Rational b(3, 5); // b = 3/5

Rational c = a * b; // c should be 3/10

期望原本就存在一个值为3/10的有理数对象并不合理。如果operator*返回一个reference指向如此数值,它必须自己创建那个Rational对象。

函数创建新对象仅有两种方法:在栈或在堆上。如果定义一个local变量,就是在栈空间创建对象:

const Rational& operator*(constRational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
} //糟糕的代码!

这个函数返回一个指向result的引用,但是result是一个局部对象,在函数退出时被销毁了。因此这个operator*的版本不会返回指向一个Rational的引用,它返回指向一个过时的Rational,因为它已经被销毁了。任何调用者甚至只是对此函数的返回值做任何一点点运用,就立刻进入了未定义行为的领地。这是事实,任何返回一个指向局部变量引用(或指针)的函数都是错误的。

考虑一下在堆上构造一个对象并返回指向它的引用的可能性。基于堆的对象通过使用new创建:

const Rational& operator*(constRational& lhs, const Rational& rhs)
{
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
} //更糟的写法!

谁该队你用new创建出来的对象实施delete?

Rational w, x, y, z;

w = x * y * z; // 与operator*(operator*(x, y), z)相同

这里,在同一个语句中有两个operator*的调用,因此new被使用了两次,这两次都需要使用 delete来销毁。但是operator*的客户没有合理的办法进行那些调用,因为他们没有合理的办法取得隐藏在通过调用operator*返回的引用后面的指针。这绝对导致资源泄漏。

无论是在栈还是在堆上的方法,为了从operator*返回的每一个 result,我们都不得不容忍一次构造函数的调用,而我们最初的目标是避免这样的构造函数调用。我们可以继续考虑基于 operator*返回一个指向staticRational对象引用的实现,而这个static Rational对象定义在函数内部:

const Rational& operator*(constRational& lhs, const Rational& rhs)
{
static Rational result; // static对象,此函数返回其reference

result= ... ; // 将lhs乘以rhs,并将结果置于result内
return result;
} //又一堆烂代码!

bool operator==(const Rational& lhs,const Rational& rhs);

// 一个针对Rational所写的operator==

Rational a, b, c, d;

...

if ((a * b) == (c * d)) {当乘积相等时,做适当的相应动作;}

else {当乘积不等时,做适当的相应动作}

除了和所有使用static对象的设计一样可能引起的线程安全(thread-safety)的混乱,上面不管 a,b,c,d 的值是什么,表达式 ((a*b) == (c*d)) 总是等于 true!如果代码重写为功能完全等价的另一种形式,很容易了解出了什么意外:

if (operator==(operator*(a, b), operator*(c, d)))

在operator==被调用前,已有两个起作用的operator*调用,每一个都返回指向 operator*内部的staticRational对象的引用。两次operator*调用的确各自改变了staticRational对象值,但由于它们返回的都是reference,因此调用端看到的永远是static Rational对象的“现值”。

一个必须返回新对象的函数的正确方法就是让那个函数返回一个新对象。对于Rational的 operator*,这就意味着下面这些代码或在本质上与其等价的代码:

inline const Rational operator*(constRational& lhs, const Rational& rhs)
{return Rational(lhs.n * rhs.n, lhs.d * rhs.d);}

当然,你可能付出了构造和析构operator*的返回值的成本,但是从长远看,这只是为正确行为付出的很小代价。但万一代价很恐怖,你可以允许编译器施行最优化,用以改善出码的效率却不改变其可观察的行为。因此某些情况下operator*返回值的构造和析构可被安全的消除。如果编译器运用这一事实(它们也往往如此),程序将保持应有行为,而执行起来又比预期的更快。

总结:如果需要在返回一个引用和返回一个对象之间做决定,你的工作就是让那个选择能提供正确的行为。让你的编译器厂商去绞尽脑汁使那个选择成本尽可能地低廉。

· 绝不要返回一个local栈对象的指针或引用,绝不要返回一个被分配的堆对象的引用,如果存在需要一个以上这样的对象的可能性时,绝不要返回一个局部 static 对象的指针或引用。

条款22:将成员变量声明为private

Declare data members private

首先,我们将看看为什么数据成员不应该声明为 public;

然后,我们将看到所有反对public数据成员的理由同样适用于protected数据成员。

最后导出了数据成员应该是private的结论。

那么,为什么不应该声明public数据成员?以下有三大理由:

1.语法一致性: 如果数据成员不是public的,客户访问一个对象的唯一方法就是通过成员函数。如果在public接口中的每件东西都是函数,客户就不必绞尽脑汁试图记住当他们要访问一个类的成员时是否需要使用圆括号,他们只要使用就可以了,因为每件东西都是一个函数。

2.精确控制成员变量的处理:如果你让一个数据成员为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; // no access
int readOnly; // read-only access
int readWrite; // read-write access
int writeOnly; // write-only access
};

3.封装:如果你通过一个函数实现对数据成员的访问,你可以以后改以某个计算来替换这个数据成员,使用你的类的人不会有任何察觉。

例如,假设你正在写一个自动测速程序,当汽车通过,其速度便被计算并填入一个速度收集器内:

class SpeedDataCollection {
...
public:
void addValue(int speed); // 添加一笔新数据
double averageSoFar() const; // 返回平均速度
...
};

现在考虑成员函数averageSoFar的实现:办法之一是在类中用一个数据成员来实时变化迄今为止收集到的所有速度数据的平均值。无论何时averageSoFar被调用,它需返回那个数据成员的值。另一个方法是在每次调用averageSoFar时重新计算,通过分析集合中每一个数据值做成这些事情。

谁能说哪一个最好?在内存非常紧张的机器(如,一台嵌入式路边侦测设装置)上,或是一个很少需要平均值的应用程序中,每次都计算平均值可能是较好的解决方案;在一个频繁需要平均值的应用程序中,速度比较重要,且内存不成问题,保持一个实时变化的平均值更为可取。重点在于通过一个成员函数访问平均值(也就是说将它“封装”),你能替换这两个不同的实现(也包括其他你可能想到的)。

封装可能比它最初显现出来的更加重要。如果你对你的客户隐藏你的数据成员(也就是说,封装它们),你就能确保类的约束条件总能被维持,因为只有成员函数能影响它们。此外,你预留了日后变更实现的权利。如果你不隐藏你将很快发现,即使你拥有类的源代码,你改变任何一个public的东西的能力也是非常有限的,因为有太多的客户代码将被破坏。public意味着没有封装,没有封装意味着不可改变,尤其是被广泛使用的类。被广泛使用的类是最需要封装的,因为它们可以从一种更好的实现中得益。

l 切记声明数据成员为private。它为客户提供了访问数据的一致,细微划分的访问控制,允许约束条件获得保证,而且为类的作者提供了实现上的弹性。

为什么不应该声明protected数据成员?

反对protected数据成员的理由是类似的。关于语法一致性和细微划分之访问控制等理由显然也适用于protected数据,就连封装性上protected数据成员也不比public数据成员更好。

某些东西的封装性与“当其内容改变时可能造成的代码破坏量“成反比。所谓改变,也许是从类中移除它(就像上述的averageSoFar)。

假设我们有一个public数据成员,随后我们移除了它,所有使用了它的客户代码,其数量通常大得难以置信,因此public数据成员是完全未封装的。但是,假设我们有一个protected数据成员,随后我们移除了它。现在有多少代码会被破坏呢?所有使用了它的派生类,典型情况下,代码的数量还是大得难以置信,因此protected数据成员就像public数据成员一样没有封装。在这两种情况下,如果数据成员发生变化,被破坏的客户代码的数量都大得难以置信。一旦你声明一个数据成员为public或protected,而且客户开始使用它,就很难再改变与这个数据成员有关的任何事情。有太多的代码不得不被重写,重测试,重文档化,或重编译。从封装的观点来看,实际只有两个访问层次:private(提供了封装)与其他(没有提供封装)。

protected并不比 public的封装性强。



摘自 pandawuwyj的专栏

相关文章
最新文章
热点推荐