前言分数类Fraction是练习运算符重载的经典例子它需要保持「分母为正、分子分母互质」的不变量需要区分复合赋值与二元运算还需要处理分母为零、除以零、整数溢出这些边界。写一个能算出正确结果的版本不难写一个在异常和边界上都自洽的版本才真正能体现运算符重载的设计规则。常见误解有两个。第一把operator写成返回引用或者返回局部对象的引用——前者语义错误后者是悬垂引用。第二认为重载了就自动拥有或者反过来。C 不会从一个推导出另一个通常的做法是先实现再用它实现这样逻辑只写一遍。本文实现一个基于std::int64_t的分数类支持 - * /、复合赋值、比较和流输出并说明归一化、溢出和除零的处理方式。示例以 C17 为标准使用numeric中的std::gcd。一、不变量与归一化一个分数表示为num / den约定den ! 0。den 0符号统一放在分子上。gcd(|num|, den) 1即已经约分。num 0时den 1。只要构造和每次运算后都恢复这三条后续的比较、输出和相等判断都会简单很多。例如两个分数相等当且仅当分子和分母分别相等不需要做交叉相乘。归一化函数先处理符号再用std::gcd约分。std::gcd对负参数返回非负结果对(0, n)返回n因此0/5会被约成0/1。需要小心std::int64_t的最小值对INT64_MIN取相反数会溢出这是本实现没有完全防御的边界实际使用时应限制输入范围或改用更宽的类型。二、复合赋值与二元运算四个复合运算符的实现思路运算公式约分时机a/b c/d (a*d c*b) / (b*d)运算后-a/b - c/d (a*d - c*b) / (b*d)运算后*a/b * c/d (a*c) / (b*d)运算后/a/b / c/d (a*d) / (b*c)运算后且检查c ! 0先乘后约分会让中间结果变大增加溢出风险。可以在乘之前用gcd交叉约分但代码会更复杂。本文选择先算后约并在文档中明确「不保证大数不溢出」。二元运算符按值接收左操作数复用复合赋值// 适用于 C17friend Fraction operator(Fraction lhs, const Fraction rhs) {lhs rhs;return lhs;}这样写的好处是左值实参走拷贝右值实参走移动函数体内只维护一份逻辑返回值是值不会悬垂。不要写成Fraction operator(...)那会返回对局部对象的引用。三、完整实现// 适用于 C17#include cstdint#include numeric#include ostream#include stdexceptclass Fraction {public:Fraction() default;Fraction(std::int64_t numerator, std::int64_t denominator 1): num_(numerator), den_(denominator) {if (den_ 0) {throw std::invalid_argument(denominator must not be zero);}normalize();}std::int64_t numerator() const { return num_; }std::int64_t denominator() const { return den_; }Fraction operator(const Fraction rhs) {num_ num_ * rhs.den_ rhs.num_ * den_;den_ den_ * rhs.den_;normalize();return *this;}Fraction operator-(const Fraction rhs) {num_ num_ * rhs.den_ - rhs.num_ * den_;den_ den_ * rhs.den_;normalize();return *this;}Fraction operator*(const Fraction rhs) {num_ num_ * rhs.num_;den_ den_ * rhs.den_;normalize();return *this;}Fraction operator/(const Fraction rhs) {if (rhs.num_ 0) {throw std::domain_error(division by zero fraction);}num_ num_ * rhs.den_;den_ den_ * rhs.num_;normalize();return *this;}Fraction operator-() const {return Fraction(-num_, den_);}friend Fraction operator(Fraction lhs, const Fraction rhs) {lhs rhs;return lhs;}friend Fraction operator-(Fraction lhs, const Fraction rhs) {lhs - rhs;return lhs;}friend Fraction operator*(Fraction lhs, const Fraction rhs) {lhs * rhs;return lhs;}friend Fraction operator/(Fraction lhs, const Fraction rhs) {lhs / rhs;return lhs;}friend bool operator(const Fraction lhs, const Fraction rhs) {return lhs.num_ rhs.num_ lhs.den_ rhs.den_;}friend bool operator!(const Fraction lhs, const Fraction rhs) {return !(lhs rhs);}friend bool operator(const Fraction lhs, const Fraction rhs) {return lhs.num_ * rhs.den_ rhs.num_ * lhs.den_;}friend std::ostream operator(std::ostream os, const Fraction value) {if (value.den_ 1) {return os value.num_;}return os value.num_ / value.den_;}private:void normalize() {if (den_ 0) {num_ -num_;den_ -den_;}const std::int64_t divisor std::gcd(num_, den_);if (divisor 1) {num_ / divisor;den_ / divisor;}}std::int64_t num_ 0;std::int64_t den_ 1;};使用示例// 适用于 C17#include iostreamint main() {const Fraction half(1, 2);const Fraction third(1, 3);const Fraction sum half third; // 5/6const Fraction diff half - third; // 1/6const Fraction product half * third; // 1/6const Fraction quotient half / third; // 3/2std::cout sum diff product quotient \n;std::cout (sum Fraction(5, 6)) \n;return 0;}按标准语义sum为 5/6diff为 1/6product为 1/6quotient为 3/2相等比较为真。这里描述的是运算定义不是某次运行的输出。几个实现细节构造函数不是explicit因此half 1可以把1隐式转换成Fraction(1, 1)。这很方便但也意味着意外的隐式转换若不希望如此可把单参数构造标记为explicit并另写接受整数的重载。normalize只在divisor 1时约分。gcd(0, 1)为 1gcd(0, 5)为 5会把 0/5 约成 0/1。gcd(0, 0)为 0但分母为零的构造已被拒绝不会走到这里。operator用交叉相乘要求分母为正这一点由归一化保证。乘积仍可能溢出比较极大分数时需要更宽的类型或浮点近似加精确校验。operator直接比较成员依赖「所有对象都已归一化」这一不变量。如果将来允许未归一化的内部状态这个实现就会失效。四、溢出与异常安全std::int64_t的乘法溢出是未定义行为undefined behavior, UB不是「回绕成负数」这么简单。两个分母相乘、分子乘对方分母都可能超出范围。防御方式有三种在运算前用__int128GCC/Clang 扩展非标准计算再检查是否落在int64_t范围内。用任意精度整数库如 Boost.Multiprecision以官方文档为准。在接口层限制分子分母的范围并在运算前做检查。异常安全方面四个复合赋值都是「先算到成员再归一化」。如果归一化不抛异常、运算本身也不抛那么没有中间状态问题但一旦引入溢出检查并抛异常就应先算到局部变量成功后再写回成员避免对象处于半更新状态。常见坑点❌ 允许分母为 0。// 适用于 C17Fraction f(1, 0); // 后续除法、比较全部失去意义✅ 构造时检查并抛出异常。// 适用于 C17if (den_ 0) { throw std::invalid_argument(zero denominator); }❌ 不把负号统一到分子。// 适用于 C17Fraction f(1, -2); // 输出 1/-2比较时符号混乱✅ 归一化时把分母变正。// 适用于 C17if (den_ 0) { num_ -num_; den_ -den_; }❌ 忘记约分。// 适用于 C17Fraction f(2, 4); // 与 1/2 不相等若按成员比较✅ 每次构造和运算后都约分。// 适用于 C17const std::int64_t g std::gcd(num_, den_);num_ / g; den_ / g;❌operator返回引用。// 适用于 C17Fraction operator(const Fraction rhs); // 返回局部对象或 *this语义错误✅ 按值返回。// 适用于 C17friend Fraction operator(Fraction lhs, const Fraction rhs);❌ 除法不检查零分子。// 适用于 C17Fraction operator/(const Fraction rhs) {num_ num_ * rhs.den_;den_ den_ * rhs.num_; // rhs 为 0 时分母变成 0}✅ 先检查再运算。// 适用于 C17if (rhs.num_ 0) { throw std::domain_error(division by zero); }❌ 比较用作为排序谓词。// 适用于 C17return lhs.num_ * rhs.den_ rhs.num_ * lhs.den_; // 相等时也返回 true✅ 严格弱序用。// 适用于 C17return lhs.num_ * rhs.den_ rhs.num_ * lhs.den_;❌ 认为a / b会自动调用operator/。// 适用于 C17// 只定义 operator/却期望 a / b 可用✅ 成对提供复合赋值与二元运算。// 适用于 C17friend Fraction operator/(Fraction lhs, const Fraction rhs) {lhs / rhs;return lhs;}❌ 忽略交叉相乘的溢出。// 适用于 C17Fraction a(1, 1000000000), b(1, 999999999);bool less a b; // 乘积可能超出 int64_t✅ 使用更宽类型或任意精度库并在接口层限制范围。// 适用于 C17// 以 Boost.Multiprecision 或 __int128 扩展为例按平台文档使用总结主题做法不变量分母为正、互质、零表示为 0/1构造检查分母非零立即归一化复合赋值先算后约返回*this二元运算按值接收左操作数复用复合赋值比较归一化后可直接比较成员用交叉相乘输出分母为 1 时只输出整数部分风险乘法溢出、除零、INT64_MIN取反分数类的运算符重载没有太多语法技巧关键是先定义清楚「什么是一个合法的分数」再让每个运算符都维护这个不变量。把归一化和边界检查放在构造与复合赋值里二元运算符自然就短了。