完成这个阶段的最后一步是编写赋值操作符和改进构造器。原来 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
变量仍然有一个定义良好的值。你应该使用什么值?我推荐零,这符合string
和vector
等类型的默认构造器的精神。为 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
放在一边,进行下一次探索,更深入地了解编写定制类型的基础。