news 2026/10/26 2:56:44

C++重载运算符实现分数加减乘除

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++重载运算符实现分数加减乘除

前言


分数类(Fraction)是练习运算符重载的经典例子:它需要保持「分母为正、分子分母互质」的不变量,需要区分复合赋值(+=)与二元运算(+),还需要处理分母为零、除以零、整数溢出这些边界。写一个能算出正确结果的版本不难,写一个在异常和边界上都自洽的版本,才真正能体现运算符重载的设计规则。


常见误解有两个。第一,把operator+写成返回引用,或者返回局部对象的引用——前者语义错误,后者是悬垂引用。第二,认为重载了+就自动拥有+=,或者反过来。C++ 不会从一个推导出另一个,通常的做法是先实现+=,再用它实现+,这样逻辑只写一遍。


本文实现一个基于std::int64_t的分数类,支持+ - * /、复合赋值、比较和流输出,并说明归一化、溢出和除零的处理方式。示例以 C++17 为标准,使用<numeric>中的std::gcd。


一、不变量与归一化


一个分数表示为num / den,约定:



  1. den != 0。

  2. den > 0;符号统一放在分子上。

  3. gcd(|num|, den) == 1,即已经约分。

  4. 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交叉约分,但代码会更复杂。本文选择先算后约,并在文档中明确「不保证大数不溢出」。


二元运算符按值接收左操作数,复用复合赋值:


// 适用于 C++17
friend Fraction operator+(Fraction lhs, const Fraction& rhs) {
lhs += rhs;
return lhs;
}

这样写的好处是:左值实参走拷贝,右值实参走移动;函数体内只维护一份逻辑;返回值是值,不会悬垂。不要写成Fraction& operator+(...),那会返回对局部对象的引用。


三、完整实现


// 适用于 C++17
#include <cstdint>
#include <numeric>
#include <ostream>
#include <stdexcept>

class 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;
};

使用示例:


// 适用于 C++17
#include <iostream>

int main() {
const Fraction half(1, 2);
const Fraction third(1, 3);

const Fraction sum = half + third; // 5/6
const Fraction diff = half - third; // 1/6
const Fraction product = half * third; // 1/6
const Fraction quotient = half / third; // 3/2

std::cout << sum << ' ' << diff << ' '
<< product << ' ' << quotient << '\n';
std::cout << (sum == Fraction(5, 6)) << '\n';
return 0;
}

按标准语义,sum为 5/6,diff为 1/6,product为 1/6,quotient为 3/2;相等比较为真。这里描述的是运算定义,不是某次运行的输出。


几个实现细节:



  • 构造函数不是explicit,因此half + 1可以把1隐式转换成Fraction(1, 1)。这很方便,但也意味着意外的隐式转换;若不希望如此,可把单参数构造标记为explicit,并另写接受整数的重载。

  • normalize只在divisor > 1时约分。gcd(0, 1)为 1;gcd(0, 5)为 5,会把 0/5 约成 0/1。gcd(0, 0)为 0,但分母为零的构造已被拒绝,不会走到这里。

  • operator<用交叉相乘,要求分母为正,这一点由归一化保证。乘积仍可能溢出,比较极大分数时需要更宽的类型或浮点近似加精确校验。

  • operator==直接比较成员,依赖「所有对象都已归一化」这一不变量。如果将来允许未归一化的内部状态,这个实现就会失效。


四、溢出与异常安全


std::int64_t的乘法溢出是未定义行为(undefined behavior, UB),不是「回绕成负数」这么简单。两个分母相乘、分子乘对方分母,都可能超出范围。防御方式有三种:



  1. 在运算前用__int128(GCC/Clang 扩展,非标准)计算,再检查是否落在int64_t范围内。

  2. 用任意精度整数库(如 Boost.Multiprecision),以官方文档为准。

  3. 在接口层限制分子分母的范围,并在运算前做检查。


异常安全方面,四个复合赋值都是「先算到成员,再归一化」。如果归一化不抛异常、运算本身也不抛,那么没有中间状态问题;但一旦引入溢出检查并抛异常,就应先算到局部变量,成功后再写回成员,避免对象处于半更新状态。


常见坑点



  1. ❌ 允许分母为 0。


// 适用于 C++17
Fraction f(1, 0); // 后续除法、比较全部失去意义

✅ 构造时检查并抛出异常。


// 适用于 C++17
if (den_ == 0) { throw std::invalid_argument("zero denominator"); }


  1. ❌ 不把负号统一到分子。


// 适用于 C++17
Fraction f(1, -2); // 输出 1/-2,比较时符号混乱

✅ 归一化时把分母变正。


// 适用于 C++17
if (den_ < 0) { num_ = -num_; den_ = -den_; }


  1. ❌ 忘记约分。


// 适用于 C++17
Fraction f(2, 4); // 与 1/2 不相等(若按成员比较)

✅ 每次构造和运算后都约分。


// 适用于 C++17
const std::int64_t g = std::gcd(num_, den_);
num_ /= g; den_ /= g;


  1. ❌operator+返回引用。


// 适用于 C++17
Fraction& operator+(const Fraction& rhs); // 返回局部对象或 *this,语义错误

✅ 按值返回。


// 适用于 C++17
friend Fraction operator+(Fraction lhs, const Fraction& rhs);


  1. ❌ 除法不检查零分子。


// 适用于 C++17
Fraction& operator/=(const Fraction& rhs) {
num_ = num_ * rhs.den_;
den_ = den_ * rhs.num_; // rhs 为 0 时分母变成 0
}

✅ 先检查再运算。


// 适用于 C++17
if (rhs.num_ == 0) { throw std::domain_error("division by zero"); }


  1. ❌ 比较用<=作为排序谓词。


// 适用于 C++17
return lhs.num_ * rhs.den_ <= rhs.num_ * lhs.den_; // 相等时也返回 true

✅ 严格弱序用<。


// 适用于 C++17
return lhs.num_ * rhs.den_ < rhs.num_ * lhs.den_;


  1. ❌ 认为a / b会自动调用operator/=。


// 适用于 C++17
// 只定义 operator/=,却期望 a / b 可用

✅ 成对提供复合赋值与二元运算。


// 适用于 C++17
friend Fraction operator/(Fraction lhs, const Fraction& rhs) {
lhs /= rhs;
return lhs;
}


  1. ❌ 忽略交叉相乘的溢出。


// 适用于 C++17
Fraction a(1, 1000000000), b(1, 999999999);
bool less = a < b; // 乘积可能超出 int64_t

✅ 使用更宽类型或任意精度库,并在接口层限制范围。


// 适用于 C++17
// 以 Boost.Multiprecision 或 __int128 扩展为例,按平台文档使用

总结




主题做法



不变量分母为正、互质、零表示为 0/1

构造检查分母非零,立即归一化

复合赋值先算后约,返回*this

二元运算按值接收左操作数,复用复合赋值

比较归一化后可直接比较成员;<用交叉相乘

输出分母为 1 时只输出整数部分

风险乘法溢出、除零、INT64_MIN取反



分数类的运算符重载没有太多语法技巧,关键是先定义清楚「什么是一个合法的分数」,再让每个运算符都维护这个不变量。把归一化和边界检查放在构造与复合赋值里,二元运算符自然就短了。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 2:56:38

STM32外部中断EXTI实战:从硬件链路到按键消抖与避坑指南

简介&#xff1a;这份资源面向STM32初学者与嵌入式入门开发者&#xff0c;围绕外部中断EXTI实现按键控制LED灯这一经典实验&#xff0c;帮助读者摆脱轮询方式带来的CPU资源浪费&#xff0c;建立中断响应式编程的基本思路。压缩包共185个文件&#xff0c;约4.75MB&#xff0c;以…

作者头像 李华
网站建设 2026/10/26 2:55:10

Java排课系统架构设计与教练培训场景实践

1. 教练培训场景下的排课系统核心痛点在传统教练培训行业中&#xff0c;排课管理一直是个令人头疼的问题。我见过太多培训机构还在用Excel表格手动排课&#xff0c;每次调整课表都要反复核对教练时间、学员分组和场地资源&#xff0c;稍有不慎就会出现双重预订或资源冲突。这种…

作者头像 李华
网站建设 2026/10/26 2:55:08

前端书架界面开发:从网格布局到性能优化实践

1. 项目概述这个实训项目中的"书架界面"功能实现&#xff0c;让我想起了刚入行时做的第一个图书管理系统。当时为了一个翻页动画效果折腾了整整三天&#xff0c;现在回头看那些踩过的坑都成了宝贵的经验。书架界面作为数字阅读产品的核心交互模块&#xff0c;远不止是…

作者头像 李华
网站建设 2026/10/26 2:54:39

PostgreSQL字段对齐优化:节省存储与提升性能

1. PostgreSQL表设计中的字段对齐规则解析在PostgreSQL数据库设计中&#xff0c;字段对齐&#xff08;Field Alignment&#xff09;是一个经常被忽视但极其重要的优化点。合理利用字段对齐规则&#xff0c;可以在不改变数据结构的前提下&#xff0c;显著减少表的存储空间占用。…

作者头像 李华
网站建设 2026/10/26 2:54:23

JavaScript书签工具实现网页文本高效提取

1. 项目概述&#xff1a;拖拽即用的网页文本提取工具这个不起眼的小书签工具解决了一个高频痛点——如何快速提取网页中的纯文本内容。作为一名经常需要收集网络资料的内容工作者&#xff0c;我受够了传统方式的低效&#xff1a;要么手动复制粘贴丢失格式&#xff0c;要么安装浏…

作者头像 李华
网站建设 2026/10/26 2:53:43

强化学习求解CVRP:端到端可复现方案(PyTorch+GNN+OR-Tools)

简介&#xff1a;本资源是一套面向人工智能与运筹优化方向学习者、研究生及毕业设计学生的强化学习实战方案&#xff0c;聚焦车辆路径问题&#xff08;VRP&#xff09;这一经典组合优化难题。内容完整复现论文《Reinforcement Learning for Solving the Vehicle Routing Proble…

作者头像 李华