Skip to content

Latest commit

 

History

History
311 lines (240 loc) · 11.4 KB

File metadata and controls

311 lines (240 loc) · 11.4 KB

三十三、复制和初始化

完成这个阶段的最后一步是编写赋值操作符和改进构造器。原来 C++ 为您做了一些工作,但是您经常想要微调这些工作。让我们找出方法。

赋值运算符

到目前为止,所有的rational操作符都是自由函数。赋值运算符是不同的。C++ 标准要求它是一个成员函数。清单 33-1 显示了编写这个函数的一种方法。

struct rational
{
  rational(int num, int den)
  : numerator{num}, denominator{den}
  {
    reduce();
  }

  rational& operator=(rational const& rhs)
  {
    numerator = rhs.numerator;
    denominator = rhs.denominator;
    reduce();
    return *this;
  }
  int numerator;
  int denominator;
};

Listing 33-1.First Version of the Assignment Operator

有几点需要进一步解释。当您将运算符实现为自由函数时,每个操作数需要一个参数。因此,二元运算符需要一个双参数函数,而一元运算符需要一个单参数函数。成员函数则不同,因为对象本身就是一个操作数(总是左操作数),对象对所有成员函数都是隐式可用的;因此,您需要少一个参数。二元操作符需要一个参数(如清单 33-1 所示),一元操作符不需要参数(示例如下)。

赋值运算符的约定是返回对封闭类型的引用。要返回的值是对象本身。可以用表达式*this ( this是保留关键字)获取对象。

因为*this是对象本身,所以引用成员的另一种方式是使用点运算符(例如(*this).numerator),而不是不加修饰的numerator(*this).numerator的另一种写法是this->numerator。意思是一样的;备选语法主要是为了方便。对于这些简单的函数来说,编写this->并不是必须的,但这通常是个好主意。当你读取一个成员函数时,你很难区分成员和非成员,这是一个信号,你必须通过在所有成员名前使用this->来帮助读者。清单 33-2 显示了明确使用this->的赋值操作符。

rational& operator=(rational const& that)
{
  this->numerator = that.numerator;
  this->denominator = that.denominator;
  reduce();
  return *this;
}

Listing 33-2.Assignment Operator with Explicit Use of this->

右边的操作数可以是你想要的任何东西。例如,您可能想要优化一个整数到一个rational对象的赋值。赋值操作符与编译器的自动转换规则一起工作的方式是,编译器将这样的赋值(例如,r = 3)视为临时rational对象的隐式构造,随后是一个rational对象到另一个对象的赋值。

编写一个赋值操作符,它带有一个 int 参数。将您的解决方案与我的进行比较,如清单 33-3 所示。

rational& operator=(int num)
{
  this->numerator = num;
  this->denominator = 1; // no need to call reduce()
  return *this;
}

Listing 33-3.Assignment of an Integer to a rational

如果你没有写赋值操作符,编译器会为你写一个。在简单的rational类型的情况下,结果是编译器编写了一个与清单 32-2 中的完全一样的类型,所以实际上没有必要自己编写(除了教学目的)。当编译器为您编写代码时,读者很难知道实际定义了哪些函数。此外,更难记录隐式函数。所以 C++ 让你明确地声明你希望编译器为你提供一个特殊的函数,方法是在声明(不是定义)后面加上=default而不是函数体。

rational& operator=(rational const&) = default;

构造器

编译器还会自动编写一个构造器,特别是通过从另一个rational对象复制所有数据成员来构造一个rational对象的构造器。这被称为复制构造器。每当您通过值向函数传递一个rational参数时,编译器使用复制构造器将参数值复制到参数中。任何时候你定义一个rational变量并用另一个rational值初始化它,编译器通过调用复制构造器来构造变量。

与赋值操作符一样,编译器的默认实现正是我们自己编写的,所以没有必要编写复制构造器。与赋值运算符一样,您可以明确声明希望编译器提供其默认的复制构造器。

rational(rational const&) = default;

复制构造器的参数类型是引用。好好想想。当通过值传递参数时,编译器使用复制构造器,因此如果复制构造器使用通过值调用,程序将在第一次尝试复制对象时无限递归。所以复制构造器的参数必须是一个引用。几乎总是引用一个const对象。

如果你没有为一个类型写任何构造器,编译器也会创建一个不带参数的构造器,叫做默认构造器。当您定义自定义类型的变量并且没有为它提供初始值设定项时,编译器使用默认构造器。编译器对默认构造器的实现只是为每个数据成员调用默认构造器。如果数据成员具有内置类型,则该成员保持未初始化状态。换句话说,如果我们没有为rational编写任何构造器,任何rational变量都将是未初始化的,因此它的分子和分母将包含垃圾值。这很糟糕——非常糟糕。所有的操作者都假设rational对象已经被简化为正常形式。如果您向它们传递一个未初始化的rational对象,它们就会失败。解决方案很简单:不要让编译器编写它的默认构造器。相反,你写一个。

你所要做的就是写一个构造器。这将阻止编译器编写自己的默认构造器。(它仍然会编写自己的复制构造器。)

早期,我们为rational类型编写了一个构造器,但它不是默认的构造器。因此,您不能定义一个rational变量并不初始化它或者用空括号初始化它。(您可能在编写自己的测试程序时遇到过这个问题。)未初始化的数据是个坏主意,拥有默认构造器是个好主意。所以写一个默认的构造器来确保一个没有初始化器的rational变量仍然有一个定义良好的值。你应该使用什么值?我推荐零,这符合stringvector等类型的默认构造器的精神。 rational 写一个默认构造器,将值初始化为零

将你的解决方案与我的进行比较,我的解决方案在清单 33-4 中给出。

rational()
: rational{0, 1}
{}

Listing 33-4.Overloaded Constructors for rational

把这一切放在一起

在我们离开之前的rational式(只是暂时的;我们会回来),让我们把所有的碎片放在一起,这样你就可以看到你在过去的四次探索中完成了什么。清单 33-5 显示了rational和相关操作符的完整定义。

#include <cassert>
#include <cmath>
import <iostream>;
import <numeric>;
import <sstream>;
import test;

/// Represent a rational number (fraction) as a numerator and denominator.
struct rational
{
  rational()
  : rational{0}
  {/*empty*/}

  rational(int num)
  : numerator{num}, denominator{1}
  {/*empty*/}

  rational(int num, int den)
  : numerator{num}, denominator{den}
  {
    reduce();
  }

  rational(double r)
  : rational{static_cast<int>(r * 10000), 10000}

  {/*empty*/}

  rational& operator=(rational const& that)
  {
    this->numerator = that.numerator;
    this->denominator = that.denominator;
    return *this;
  }

  float as_float()
  {
    return static_cast<float>(numerator) / denominator;
  }

  double as_double()
  {
    return static_cast<double>(numerator) / denominator;
  }

  long double as_long_double()
  {
    return static_cast<long double>(numerator) / denominator;
  }

  /// Assign a numerator and a denominator, then reduce to normal form.
  void assign(int num, int den)
  {
    numerator = num;
    denominator = den;
    reduce();
  }

  /// Reduce the numerator and denominator by their GCD.
  void reduce()
  {
    assert(denominator != 0);
    if (denominator < 0)

    {
      denominator = -denominator;
      numerator = -numerator;
    }
    int div{std::gcd(numerator, denominator)};
    numerator = numerator / div;
    denominator = denominator / div;
  }

  int numerator;
  int denominator;
};

/// Absolute value of a rational number.
rational abs(rational const& r)
{
  return rational{std::abs(r.numerator), r.denominator};
}

/// Unary negation of a rational number.
rational operator-(rational const& r)

{
  return rational{-r.numerator, r.denominator};
}

/// Add rational numbers.
rational operator+(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator * rhs.denominator + rhs.numerator * lhs.denominator,
                  lhs.denominator * rhs.denominator};
}

/// Subtraction of rational numbers.
rational operator-(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator * rhs.denominator - rhs.numerator * lhs.denominator,
                  lhs.denominator * rhs.denominator};
}

/// Multiplication of rational numbers.
rational operator*(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator * rhs.numerator, lhs.denominator * rhs.denominator};
}

/// Division of rational numbers.
/// TODO: check for division-by-zero
rational operator/(rational const& lhs, rational const& rhs)
{
  return rational{lhs.numerator * rhs.denominator, lhs.denominator * rhs.numerator};
}

/// Compare two rational numbers for equality.
bool operator==(rational const& a, rational const& b)
{
  return a.numerator == b.numerator and a.denominator == b.denominator;
}

/// Compare two rational numbers for inequality.
inline bool operator!=(rational const& a, rational const& b)
{
  return not (a == b);
}

/// Compare two rational numbers for less-than.
bool operator<(rational const& a, rational const& b)
{
  return a.numerator * b.denominator < b.numerator * a.denominator;
}

/// Compare two rational numbers for less-than-or-equal.
inline bool operator<=(rational const& a, rational const& b)
{
  return not (b < a);
}
/// Compare two rational numbers for greater-than.
inline bool operator>(rational const& a, rational const& b)
{
  return b < a;
}

/// Compare two rational numbers for greater-than-or-equal.
inline bool operator>=(rational const& a, rational const& b)
{
  return not (b > a);
}

/// Read a rational number.
/// Format is @em integer @c / @em integer.
std::istream& operator>>(std::istream& in, rational& rat)
{
  int n{0}, d{0};
  char sep{'\0'};
  if (not (in >> n >> sep))
    // Error reading the numerator or the separator character.
    in.setstate(in.failbit);
  else if (sep != '/')
  {
    // Push sep back into the input stream, so the next input operation
    // will read it.
    in.unget();
    rat.assign(n, 1);
  }

  else if (in >> d)
    // Successfully read numerator, separator, and denominator.
    rat.assign(n, d);
  else
    // Error reading denominator.
    in.setstate(in.failbit);

  return in;
}

/// Write a rational numbers.
/// Format is @em numerator @c / @em denominator.
std::ostream& operator<<(std::ostream& out, rational const& rat)
{
  std::ostringstream tmp{};
  tmp << rat.numerator << '/' << rat.denominator;
  out << tmp.str();

  return out;
}

int main()
{
    TEST(rational{1} == rational{2,2});
    ... Add tests, lots of tests
}

Listing 33-5.Complete Definition of rational and Its Operators

我鼓励你向清单 33-5 中的程序添加测试,以测试rational类的所有最新特性。确保一切都按您预期的方式运行。然后将rational放在一边,进行下一次探索,更深入地了解编写定制类型的基础。