C 箴言:必须返回对象时别返回引用

2008-02-23 05:40:40来源:互联网 阅读 ()

新老客户大回馈,云服务器低至5折

 一旦程式员抓住对象传值的效率隐忧,很多人就会成为狂热的圣战分子,誓要根除传值的罪恶,无论他隐藏多深。他们不屈不挠地追求传引用的纯度,但他们全都犯了一个致命的错误:他们开始传递并不存在的对象的引用。这可不是什么好事。

  考虑一个代表有理数的类,包含一个将两个有理数相乘的函数:

class Rational {
 public:
  Rational(int numerator = 0, // see Item 24 for why this
  int denominator = 1); // ctor isn’t declared explicit
  ...

 private:
  int n, d; // numerator and denominator

 friend:
  const Rational // see Item 3 for why the
  operator*(const Rational& lhs, // return type is const
  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

  似乎没有理由期望那里碰巧已存在一个值为十分之三的有理数。不是这样的,假如 operator* 返回这样一个数的引用,他必须自己创建那个数字对象。

  一个函数创建一个新对象仅有两种方法:在栈上或在堆上。栈上的生成物通过定义一个局部变量而生成。使用这个策略,您能够用这种方法试写 operator*:

const Rational& operator*(const Rational& lhs, // warning! bad code!
const Rational& rhs)
{
 Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
 return result;
}

  您能够立即否决这种方法,因为您的目标是避免调用构造函数,而 result 正像任何其他对象相同必须被构造。一个更严重的问题是这个函数返回一个引向 result 的引用,但是 result 是个局部对象,而局部对象在函数退出时被销毁。那么,这个 operator* 的版本不会返回引向一个 Rational 的引用——他返回引向一个前 Rational;一个曾的 Rational;一个空洞的、恶臭的、腐败的,从前是个 Rational 但永不再是的尸体的引用,因为他已被销毁了。任何调用者甚至于没有来得及匆匆看一眼这个函数的返回值就立即进入了未定义行为的领地。这是事实,任何返回一个引向局部变量的引用的函数都是错误的。(对于任何返回一个指向局部变量的指针的函数同样成立。)

  那么,让我们考虑一下在堆上构造一个对象并返回引向他的引用的可能性。基于堆的对象通过使用 new 而开始存在,所以您能够像这样写一个基于堆的 operator*:

const Rational& operator*(const Rational& lhs, // warning! more bad
const Rational& rhs) // code!
{
 Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
 return *result;
}

  哦,您还是必须要付出一个构造函数调用的成本,因为通过 new 分配的内存要通过调用一个适当的构造函数进行初始化,但是现在您有另一个问题:谁是删除您用 new 做出来的对象的合适人选?

  即使调用者尽职尽责且一心向善,他们也不太可能是用这样的方案来合理地预防泄漏:

Rational w, x, y, z;

w = x * y * z; // same as operator*(operator*(x, y), z)

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

  但是也许您注意到无论是在栈上的还是在堆上的方法,为了从 operator* 返回的每一个 result,我们都不得不容忍一次构造函数的调用。也许您想起我们最初的目标是避免这样的构造函数调用。也许您认为您知道一种方法能避免除一次以外几乎全部的构造函数调用。也许下面这个实现是您做过的,一个基于 operator* 返回一个引向 static Rational 对象的引用的实现,而这个 static Rational 对象定义在函数内部:

const Rational& operator*(const Rational& lhs, // warning! yet more
const Rational& rhs) // bad code!
{
 static Rational result; // static object to which a
 // reference will be returned

 result = ... ; // multiply lhs by rhs and put the
 // product inside result
 return result;
}

  就像任何使用了 static 对象的设计相同,这个也会立即引起我们的线程安全(thread-safety)的混乱,但那是他的比较明显的缺点。为了看到他的更深层的缺陷,考虑这个完全合理的客户代码:

bool operator==(const Rational& lhs, // an operator==
const Rational& rhs); // for Rationals

Rational a, b, c, d;

...
if ((a * b) == (c * d)) {
 do whatever’s appropriate when the products are equal;
} else {
 do whatever’s appropriate when they’re not;

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇: C 箴言:将数据成员声明为private

下一篇: C 箴言:用传引用给const取代传值