C++开发初级


896 浏览 5 years, 3 months

10.1 示例:为SpreadsheetCell实现加法

版权声明: 转载请注明出处 http://www.codingsoho.com/

示例:为SpreadsheetCell实现加法

在真正的面向对象方式中,SpreadsheetCell对象应该能够与其他SpreadsheetCell对象相加。将一个单元格与另一个单元格相加得到带有结果的第3个单元格,但是不会改变原始的单元格。

SpreadsheetCell相加的意义是单元格值的相加,字符串单元格被忽略。

首次尝试:add方法

可以像下面这样声明并定义SpreadsheetCell类的add()方法:

class SpreadsheetCell
{
 public:
  const SpreadsheetCell add(const SpreadsheetCell& cell) const;
};

代码取自 OperatorOverloading\AddFirstAttempt\SpreadsheetCell.h

这个方法将两个单元格相加,返回第三个新单元格,其值为前两个的和。这个方法声明为const,并且采用了const SpreadsheetCell的引用作为参数,原因是add()不改变任意一个原始单元格。因为您不希望用户改变返回值,所以它返回const SpreadsheetCell。这个值只是用来赋给其他对象的。add()是一个方法,因此从一个对象调用并传递另一个对象。下面是这个方法的实现:

const SpreadsheetCell SpreadsheetCell::add(const SpreadsheetCell& cell) const
{
  SpreadsheetCell newCell;
  newCell.set(mValue + cell.mValue); // call set to update mValue and mString
  return newCell;
}

代码取自 OperatorOverloading\AddFirstAttempt\SpreadsheetCell.cpp

注意,在方法的实现中创建了一个新的名为newCell的SpreadsheetCell,并返回了这个单元格的副本。您可能想返回这个单元格的引用,但是这样做代码将无法运行,因为add()方法结束时newCell超出了作用域,因此会被销毁。您返回的引用将会成为悬挂引用

可以这样使用add()方法:

  SpreadsheetCell myCell(4), anotherCell(5);
  SpreadsheetCell aThirdCell = myCell.add(anotherCell);

代码取自 OperatorOverloading\AddFirstAttempt\SpreadsheetCellTest.cpp

这样做可行,但是有一点笨拙。您可以做得更好。

第二次尝试:将运算符作为方法重载

用加号相加两个对象会比较方便,就像您相加两个int或者double那样,如下所示:

  SpreadsheetCell myCell(4), anotherCell(5);
  SpreadsheetCell aThirdCell = myCell + anotherCell;

代码取自 OperatorOverloading\AddSecondAttempt\SpreadsheetCellTest.cpp

C++允许您编写自己版本的加号以正确地处理您的类,称之为加运算符。为此可以编写一个名为operator+的方法,如下所示

class SpreadsheetCell
{
 public:
  const SpreadsheetCell operator+(const SpreadsheetCell& cell) const;
};

代码取自 OperatorOverloading\AddSecondAttempt\SpreadsheetCell.h

在operator以及加号之间可以使用空格。例如,可以用operator +替代operator+。这一点对于所有的运算符都成立。本处采用没有空格的格式。

方法的实现与add()方法的实现一样:

const SpreadsheetCell SpreadsheetCell::operator+(
    const SpreadsheetCell& cell) const
{
  SpreadsheetCell newCell;
  newCell.set(mValue + cell.mValue); // call set to update mValue and mString
  return newCell;
}

代码取自 OperatorOverloading\AddSecondAttempt\SpreadsheetCell.cpp

现在可以使用加号将两个单元格相加,就像前面做的那样。

这个语法需要花点功夫去适应。不要过于担心这个奇怪的方法名称operator+—这只是一个名称,就像foo或者add一样。理解此处实际发生的事情有助于理解其余的语法。当C++编译器分析一个程序并遇到运算符(例如,+、-、=或者<<)的时候,就会试着查找名称为operator+,operator-,operator=或者operator<<并且具有适当参数的函数或者方法。

例如,当编译器看到下面这行时,就会试着查找SpreadsheetCell类中是否有名称为operator+并将另一个SpreadsheetCell对象作为参数的方法,或者是否存在采用两个SpreadsheetCell对象做参数、名称为operator+的全局函数:

SpreadsheetCell aThirdCell = myCell + anotherCell;

注意,用作参数的对象类型并不一定要与编写operator+的类相同。您可以为SpreadsheetCell编写一个operator+,将Spreadsheet与SpreadsheetCell相加。对于这个程序而言这样做没有意义,但是编译器允许这样做。

此外还要注意,您可以任意指定operator+的返回值类型。运算符重载是函数重载的一种形式,函数重载对函数的返回类型并没有要求。

隐式转换

令人惊讶的是,一旦编写了前面所示的operator+,不仅可以将两个单元格相加,还可以将单元格与string,double或者int相加。

  SpreadsheetCell myCell(4), aThirdCell;
  string str = "hello";
  aThirdCell = myCell + str;
  aThirdCell = myCell + 5.6;
  aThirdCell = myCell + 4;

代码取自 OperatorOverloading\AddSecondAttempt\SpreadsheetCellTest.cpp

上面的代码之所以可以运行,是因为编译器会试着查找合适的operator+,而不是只关心指定了确切类型的那个operator+。为了与operator+匹配,编译器还试图查找合适的类型转换,构造函数会对有问题的类型进行适当的转换。

在前面的示例中,当编译器看到SpreadsheetCell试图与double相加时,发现了采用double做参数的SpreadsheetCell构造函数,就会构建一个临时的SpreadsheetCell对象传递给operator+。与此类似,当编译器看到试图将SpreadsheetCell与string相加的行时,会调用采用string做参数的SpreadsheetCell构造函数创建一个临时SpreadsheetCell对象传递给operator+。

隐式转换通常会带来方便。但在前面的示例中,将SpreadsheetCell与string相加并没有意义。可以使用explicit关键字标记构造函数,从而禁止将string隐式地转换为SpreadsheetCell:

class SpreadsheetCell
{
 public:
  SpreadsheetCell();
  SpreadsheetCell(double initialValue);
  SpreadsheetCell(const string& initialValue);
  explicit SpreadsheetCell(const string& initialValue);
};

代码取自 OperatorOverloading\AddSecondAttempt\SpreadsheetCell.h

explicit关键字只在类定义内使用,并且只适用于只有一个参数的构造函数。

由于必须创建临时对象,隐式地使用构造函数的效率不高。为了避免与double相加时隐式地使用构造函数,可以编写第二个operator+,如下所示:

const SpreadsheetCell SpreadsheetCell::operator+(double rhs) const
{
    return SpreadsheetCell(mValue + rhs);
}

代码取自 OperatorOverloading\AddSecondAttempt\SpreadsheetCell.cpp

此外还要注意,这段代码说明返回一个值并不需要创建一个变量。

第三次尝试:全局operator+

隐式转换允许使用operator+方法将SpreadsheetCell对象与int以及double相加。然而,这个符号不具有互换性,如下所示:

  aThirdCell = myCell + 4; // works fine
  aThirdCell = myCell + 5.6; // works fine

  // The following two lines don't compile
  // aThirdCell = 4 + myCell; // FAILS TO COMPILE!
  // aThirdCell = 5.6 + myCell; // FAILS TO COMPILE!

代码取自 OperatorOverloading\AddSecondAttempt\SpreadsheetCellTest.cpp

当SpreadsheetCell对象在运算符的左边时,隐式转换正常运行,但是在右边的时候无法运行。加法是可以互换的,因此这里存在错误。问题在于必须在SpreadsheetCell对象上调用operator+方法,对象必须在operator+的左边。这是C++语言定义的方式,因此使用operator+方法无法让上面的代码运行。

然而,如果用不局限于某个特定对象的全局operator+函数替换类内的operator+,上面的代码就可以运行,函数如下所示:

const SpreadsheetCell operator+(const SpreadsheetCell& lhs,
                const SpreadsheetCell& rhs)
{
  SpreadsheetCell newCell;
  newCell.set(lhs.mValue + rhs.mValue); // call set to update mValue and mString
  return newCell;
}

代码取自 OperatorOverloading\SpreadsheetCell.cpp

这样,下面的四个加法行都可以按照预期运行:

  aThirdCell = myCell + 4; // works fine
  aThirdCell = myCell + 5.6; // works fine
  aThirdCell = 4 + myCell; // works fine
  aThirdCell = 5.6 + myCell; // works fine

代码取自 OperatorOverloading\SpreadsheetCellTest.cpp

注意,全局oprator+的实现访问了SpreadsheeetCell对象的protected数据成员,因此必须是SpreadsheetCell类的友元:

class SpreadsheetCell
{
 public:
  friend const SpreadsheetCell operator+(const SpreadsheetCell& lhs,
                     const SpreadsheetCell& rhs);
};

代码取自 OperatorOverloading\SpreadsheetCell.h

您可能在考虑如果编写如下代码会发生什么:

aThirdCell = 4.5 + 5.5;

这段代码可以编译并运行,但是并没有调用您编写的Operator+。这段代码将普通的double 4.5以及5.5相加,得到了下面所示的中间状态语句:

aThirdCell = 10;

为了让这个赋值运行,在运算符右边应该是SpreadsheetCell对象。编译器找到用户定义的用double做参数的构造函数,然后用这个构造函数隐式地将double值转换为一个临时SpreadsheetCell对象,然后调用赋值运算符。