21.x — 第 21 章总结与测验

在本章中,我们探讨了与运算符重载、重载类型转换以及复制构造函数相关的主题。

总结

运算符重载是函数重载的一种变体,它允许你为你的类重载运算符。重载运算符时,运算符的意图应尽可能接近其原始意图。如果运算符应用于自定义类时的含义不明确或不直观,则应使用命名函数代替。

运算符可以作为普通函数、友元函数或成员函数进行重载。以下经验法则可以帮助你确定哪种形式最适合给定情况:

  • 如果你正在重载赋值运算符 (=)、下标运算符 ([])、函数调用运算符 (()) 或成员选择运算符 (->),则应将其作为成员函数进行重载。
  • 如果你正在重载一元运算符,则应将其作为成员函数进行重载。
  • 如果你正在重载修改其左操作数的二元运算符(例如 `operator+=`),如果可能,则应将其作为成员函数进行重载。
  • 如果你正在重载不修改其左操作数的二元运算符(例如 `operator+`),则应将其作为普通函数或友元函数进行重载。

类型转换可以重载以提供转换函数,这些函数可用于将你的类显式或隐式转换为另一种类型。

复制构造函数是一种特殊类型的构造函数,用于从同类型的另一个对象初始化对象。复制构造函数用于从同类型对象进行直接/统一初始化、复制初始化 (`Fraction f = Fraction(5,3)`),以及按值传递或返回参数时。

如果你不提供复制构造函数,编译器会为你创建一个。编译器提供的复制构造函数将使用逐成员初始化,这意味着副本的每个成员都从原始成员初始化。出于优化目的,复制构造函数可能会被省略,即使它有副作用,所以不要依赖你的复制构造函数实际执行。

构造函数默认被视为转换构造函数,这意味着编译器将使用它们将其他类型的对象隐式转换为你的类的对象。你可以通过在构造函数前面使用 `explicit` 关键字来避免这种情况。你还可以删除类中的函数,包括复制构造函数和重载的赋值运算符(如果需要)。如果调用已删除的函数,这将导致编译器错误。

赋值运算符可以重载,以允许赋值给你的类。如果你不提供重载的赋值运算符,编译器会为你创建一个。重载的赋值运算符应始终包含自赋值检查(除非它被自然处理,或者你正在使用复制和交换惯用法)。

新程序员经常混淆何时使用赋值运算符和复制构造函数,但它相当简单:

  • 如果必须在复制发生之前创建新对象,则使用复制构造函数(注意:这包括按值传递或返回对象)。
  • 如果无需在复制发生之前创建新对象,则使用赋值运算符。

默认情况下,编译器提供的复制构造函数和赋值运算符执行逐成员初始化或赋值,这是一种浅拷贝。如果你的类动态分配内存,这可能会导致问题,因为多个对象最终将指向相同的已分配内存。在这种情况下,你需要显式定义它们以进行深拷贝。更好的是,如果可以,避免自行管理内存,并使用标准库中的类。

小测验时间

问题 #1

  1. 假设 `Point` 是一个类,`point` 是该类的一个实例,以下运算符应使用普通/友元函数重载还是成员函数重载?

a) `point + point`

显示答案

b) `-point`

显示答案

c) `std::cout << point`

显示答案

d) `point = 5;`

显示答案

问题 #2

编写一个名为 `Average` 的类,它将跟踪传递给它的所有整数的平均值。使用两个成员:第一个成员应为 `std::int32_t` 类型,用于跟踪到目前为止所有数字的总和。第二个成员应跟踪到目前为止已看到的数字数量。你可以将它们相除以找到平均值。

a) 编写以下程序运行所需的所有函数

int main()
{
	Average avg{};
	std::cout << avg << '\n';
	
	avg += 4;
	std::cout << avg << '\n'; // 4 / 1 = 4
	
	avg += 8;
	std::cout << avg << '\n'; // (4 + 8) / 2 = 6

	avg += 24;
	std::cout << avg << '\n'; // (4 + 8 + 24) / 3 = 12

	avg += -10;
	std::cout << avg << '\n'; // (4 + 8 + 24 - 10) / 4 = 6.5

	(avg += 6) += 10; // 2 calls chained together
	std::cout << avg << '\n'; // (4 + 8 + 24 - 10 + 6 + 10) / 6 = 7

	Average copy{ avg };
	std::cout << copy << '\n';

	return 0;
}

并生成结果

0
4
6
12
6.5
7
7

显示答案

b) 你应该为这个类提供用户定义的复制构造函数或赋值运算符吗?

显示答案

c) 为什么要使用 `std::int32_t` 而不是 `int`?

显示答案

问题 #3

从头开始编写你自己的名为 `IntArray` 的整数数组类(不要使用 `std::array` 或 `std::vector`)。用户在创建数组时应传入数组的大小,并且数组应动态分配。使用断言语句来防止坏数据。创建使以下程序正常运行所需的任何构造函数或重载运算符

#include <iostream>

IntArray fillArray()
{
	IntArray a(5);

	a[0] = 5;
	a[1] = 8;
	a[2] = 2;
	a[3] = 3;
	a[4] = 6;

	return a;
}

int main()
{
	IntArray a{ fillArray() };

	std::cout << a << '\n';

	auto& ref{ a }; // we're using this reference to avoid compiler self-assignment errors
	a = ref;

	IntArray b(1);
	b = a;

	a[4] = 7;

	std::cout << b << '\n';

	return 0;
}

此程序应打印

5 8 2 3 6
5 8 2 3 6

显示答案

问题 #4

附加题:这有点棘手。

浮点数是带有小数的数字,其中小数部分后的位数是可变的。定点数是带有小数部分的数字,其中小数部分的位数是固定的。

在此测验中,我们将编写一个类来实现带有两位小数的定点数(例如 12.34、3.00 或 1278.99)。假设该类的范围应为 -32768.99 到 32767.99,小数部分应包含任意两位数字,我们不希望出现精度误差,并且我们希望节省空间。

> 步骤 #1

你认为我们应该使用哪种类型的成员变量来实现带有 2 位小数的定点数?(在继续下一个问题之前,请务必阅读答案)

显示答案

> 步骤 #2

编写一个名为 `FixedPoint2` 的类,它实现上一问题中推荐的解决方案。如果数字的非小数部分和/或小数部分是负数,则该数字应被视为负数。提供以下程序运行所需的重载运算符和构造函数。目前,不要担心小数部分超出范围(>99 或 <-99)。

#include <cassert>
#include <iostream>

int main()
{
	FixedPoint2 a{ 34, 56 };
	std::cout << a << '\n';
	std::cout << static_cast<double>(a) << '\n';
	assert(static_cast<double>(a) == 34.56);

	FixedPoint2 b{ -2, 8 };
	assert(static_cast<double>(b) == -2.08);

	FixedPoint2 c{ 2, -8 };
	assert(static_cast<double>(c) == -2.08);

	FixedPoint2 d{ -2, -8 };
	assert(static_cast<double>(d) == -2.08);

	FixedPoint2 e{ 0, -5 };
	assert(static_cast<double>(e) == -0.05);

	FixedPoint2 f{ 0, 10 };
	assert(static_cast<double>(f) == 0.1);
    
	return 0;
}

此程序应产生结果

34.56
34.56

提示:要输出你的数字,将其 `static_cast` 为 `double`。

显示答案

> 步骤 #3

现在让我们处理小数部分超出范围的情况。我们这里有两种合理的策略:

  • 限制小数部分(如果 >99,则设置为 99)。
  • 将溢出视为相关(如果 >99,则减去 100 并向基数加 1)。

在此练习中,我们将小数溢出视为相关,因为这在下一步中会很有用。

以下内容应运行

#include <cassert>
#include <iostream>

// You will need to make testDecimal a friend of FixedPoint2
// so the function can access the private members of FixedPoint2
bool testDecimal(const FixedPoint2 &fp)
{
    if (fp.m_base >= 0)
        return fp.m_decimal >= 0 && fp.m_decimal < 100;
    else
        return fp.m_decimal <= 0 && fp.m_decimal > -100;
}

int main()
{
	FixedPoint2 a{ 1, 104 };
	std::cout << a << '\n';
	std::cout << static_cast<double>(a) << '\n';
	assert(static_cast<double>(a) == 2.04);
	assert(testDecimal(a));

	FixedPoint2 b{ 1, -104 };
	assert(static_cast<double>(b) == -2.04);
	assert(testDecimal(b));

	FixedPoint2 c{ -1, 104 };
	assert(static_cast<double>(c) == -2.04);
	assert(testDecimal(c));

	FixedPoint2 d{ -1, -104 };
	assert(static_cast<double>(d) == -2.04);
	assert(testDecimal(d));

	return 0;
}

并产生输出

2.04
2.04

显示答案

> 第4步

现在添加一个接受 `double` 类型的构造函数。以下程序应运行

#include <cassert>
#include <iostream>

int main()
{
	FixedPoint2 a{ 0.01 };
	assert(static_cast<double>(a) == 0.01);

	FixedPoint2 b{ -0.01 };
	assert(static_cast<double>(b) == -0.01);

	FixedPoint2 c{ 1.9 }; // make sure we handle single digit decimal
	assert(static_cast<double>(c) == 1.9);
    
	FixedPoint2 d{ 5.01 }; // stored as 5.0099999... so we'll need to round this
	assert(static_cast<double>(d) == 5.01);

	FixedPoint2 e{ -5.01 }; // stored as -5.0099999... so we'll need to round this
	assert(static_cast<double>(e) == -5.01);

	// Handle case where the argument's decimal rounds to 100 (need to increase base by 1)
	FixedPoint2 f { 106.9978 }; // should be stored with base 107 and decimal 0
	assert(static_cast<double>(f) == 107.0);

	// Handle case where the argument's decimal rounds to -100 (need to decrease base by 1)
	FixedPoint2 g { -106.9978 }; // should be stored with base -107 and decimal 0
	assert(static_cast<double>(g) == -107.0);

	return 0;
}

建议:这会有点棘手。分三步完成。首先,解决 `double` 参数可以直接表示的情况(上述变量 `a` 到 `c`)。然后,更新你的代码以处理 `double` 参数存在舍入误差的情况(变量 `d` 和 `e`)。变量 `f` 和 `g` 应由我们在上一步中添加的溢出处理来处理。

对于所有情况:显示提示

对于变量 `a` 到 `c`:显示提示

对于变量 `d` 和 `e`:显示提示

显示答案

> 步骤 #5

重载 `operator==`、`operator>>`、`operator-`(一元)和 `operator+`(二元)。

以下程序应该运行

#include <cassert>
#include <iostream>

int main()
{
	assert(FixedPoint2{ 0.75 } == FixedPoint2{ 0.75 });    // Test equality true
	assert(!(FixedPoint2{ 0.75 } == FixedPoint2{ 0.76 })); // Test equality false
    
	// Test additional cases -- h/t to reader Sharjeel Safdar for these test cases
	assert(FixedPoint2{ 0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 1.98 });    // both positive, no decimal overflow
	assert(FixedPoint2{ 0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 2.25 });    // both positive, with decimal overflow
	assert(FixedPoint2{ -0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -1.98 }); // both negative, no decimal overflow
	assert(FixedPoint2{ -0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -2.25 }); // both negative, with decimal overflow
	assert(FixedPoint2{ 0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -0.48 });  // second negative, no decimal overflow
	assert(FixedPoint2{ 0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -0.75 });  // second negative, possible decimal overflow
	assert(FixedPoint2{ -0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 0.48 });   // first negative, no decimal overflow
	assert(FixedPoint2{ -0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 0.75 });   // first negative, possible decimal overflow
    
	FixedPoint2 a{ -0.48 };
	assert(static_cast<double>(a) == -0.48);
	assert(static_cast<double>(-a) == 0.48);

	std::cout << "Enter a number: "; // enter 5.678
	std::cin >> a;
	std::cout << "You entered: " << a << '\n';
	assert(static_cast<double>(a) == 5.68);
	
	return 0;
}

显示提示

显示提示

显示答案

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
521 评论
最新
最早 最多投票
内联反馈
查看所有评论