本章探讨了运算符重载、重载类型转换以及复制构造函数等相关主题。\
总结
运算符重载是函数重载的一种变体,允许为自定义类重载运算符。重载时应尽量保持其语义与原生运算符一致;若运算符对自定义类的含义不够直观,应改用具名函数。
运算符可作为普通函数、友元函数或成员函数重载。以下经验法则可帮助选择适当形式:
- 若重载赋值(=)、下标([])、函数调用(())或成员访问(->),应实现为成员函数。
- 若重载一元运算符,应实现为成员函数。
- 若重载会修改左操作数的二元运算符(如 operator+=),且可行时应实现为成员函数。
- 若重载不修改左操作数的二元运算符(如 operator+),应实现为普通函数或友元函数。
类型转换可被重载为转换函数,用于显式或隐式地将类转换为其他类型。
复制构造函数是一种特殊构造函数,用于以同类型对象初始化新对象,适用于直接/统一初始化、复制初始化(如 Fraction f = Fraction(5,3))以及按值传递或返回参数的情形。
若未手动提供复制构造函数,编译器会隐式生成一个。编译器版本采用按成员初始化,即用原对象的成员逐一初始化新对象。复制构造函数可能因优化而被省略,即使它有副作用,因此请勿依赖其一定执行。
构造函数默认被视为转换构造函数,编译器可用其将其他类型对象隐式转换为本类对象。可在构造函数前加 explicit 防止隐式转换;也可用 delete 删除包括复制构造函数、赋值运算符在内的任何函数,从而在调用时触发编译错误。
赋值运算符可被重载以支持类对象的赋值。若未手动提供,编译器将生成一个。重载的赋值运算符应始终包含自赋值检查(除非能自然处理或采用复制并交换惯用法)。
新手常混淆复制构造函数与赋值运算符的调用时机,实则简单:
- 若需先创建新对象才能完成复制,使用复制构造函数(注:按值传递或返回对象亦属此类)。
- 若无需创建新对象即可完成复制,使用赋值运算符。
默认情况下,编译器生成的复制构造函数与赋值运算符执行按成员初始化或赋值,即浅拷贝。若类动态分配内存,浅拷贝将导致多个对象指向同一块已分配内存,从而引发问题。此时需显式定义这些函数以执行深拷贝;更优做法是避免手动内存管理,改用标准库中的类。
测验
测验
问题 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; // 两次链式调用
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
)。用户创建时需提供数组大小,数组应动态分配。使用 assert
防止无效数据。编写任何必要的构造函数或重载运算符,使下列程序正确运行:
#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 }; // 用引用避免编译器自赋值报错
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
编写 FixedPoint2
类,实现上一步推荐的方案。若整数部分或小数部分任一(或两者)为负,则整体为负。提供重载运算符和构造函数,使下列程序运行:
#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 置 99);
- 进位(>99 减 100 并向整数部分加 1)。 本练习采用进位策略。
应能运行:
#include <cassert>
#include <iostream>
// 需将 testDecimal 设为 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 }; // 单数小数
assert(static_cast<double>(c) == 1.9);
FixedPoint2 d{ 5.01 }; // 存储为 5.009999...,需四舍五入
assert(static_cast<double>(d) == 5.01);
FixedPoint2 e{ -5.01 }; // 需四舍五入
assert(static_cast<double>(e) == -5.01);
// 小数部分四舍五入为 100(整数部分需加 1)
FixedPoint2 f{ 106.9978 }; // 应存为 base 107, decimal 0
assert(static_cast<double>(f) == 107.0);
// 小数部分四舍五入为 -100(整数部分需减 1)
FixedPoint2 g{ -106.9978 }; // 应存为 base -107, decimal 0
assert(static_cast<double>(g) == -107.0);
return 0;
}
建议:分三步完成。先实现可直接表示的 double
(a-c);再处理含舍入误差的情况(d-e);f 与 g 应借助上一步的进位逻辑处理。
步骤 5
重载 operator==
、operator>>
、一元 operator-
和二元 operator+
。
应能运行:
#include <cassert>
#include <iostream>
int main()
{
assert(FixedPoint2{ 0.75 } == FixedPoint2{ 0.75 }); // 相等为 true
assert(!(FixedPoint2{ 0.75 } == FixedPoint2{ 0.76 })); // 相等为 false
// 额外用例 —— 感谢读者 Sharjeel Safdar
assert(FixedPoint2{ 0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 1.98 }); // 两正,无小数溢出
assert(FixedPoint2{ 0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 2.25 }); // 两正,小数溢出
assert(FixedPoint2{ -0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -1.98 }); // 两负,无小数溢出
assert(FixedPoint2{ -0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -2.25 }); // 两负,小数溢出
assert(FixedPoint2{ 0.75 } + FixedPoint2{ -1.23 } == FixedPoint2{ -0.48 }); // 第二个为负,无小数溢出
assert(FixedPoint2{ 0.75 } + FixedPoint2{ -1.50 } == FixedPoint2{ -0.75 }); // 第二个为负,可能小数溢出
assert(FixedPoint2{ -0.75 } + FixedPoint2{ 1.23 } == FixedPoint2{ 0.48 }); // 第一个为负,无小数溢出
assert(FixedPoint2{ -0.75 } + FixedPoint2{ 1.50 } == FixedPoint2{ 0.75 }); // 第一个为负,可能小数溢出
FixedPoint2 a{ -0.48 };
assert(static_cast<double>(a) == -0.48);
assert(static_cast<double>(-a) == 0.48);
std::cout << "Enter a number: "; // 输入 5.678
std::cin >> a;
std::cout << "You entered: " << a << '\n';
assert(static_cast<double>(a) == 5.68);
return 0;
}
提示:
- 步骤 4 全部提示
- 步骤 5 全部提示
提示:
- 步骤 4 全部提示
- 步骤 5 全部提示