C++分数类设计实战:运算符重载与最大公约数约分
1. 为什么值得动手写一个分数运算类很多人看到“分数的四则运算”第一反应是这不就是小学数学吗有什么好写的。但真正动手用C实现过的人都知道这里面藏着不少坑——整数溢出、分母为零、约分效率、负数取模、运算符重载的返回值设计每一个都能让代码在边界测试时翻车。我带过几个刚入门的同学做这个练习十个人里有八个第一版跑不过1/3 1/6之外的用例问题基本都出在通分时的溢出和约分时的符号处理上。这篇文章面向的是已经学过C基础语法、想通过一个完整小项目把类设计、运算符重载、异常处理、最大公约数算法串起来的读者。哪怕你只是想把作业写扎实一点或者面试前找个练手项目这个题材都够用。我会从整体设计思路讲起把每一步的参数选择、算法取舍、边界处理都摊开说最后给出一份可以直接编译运行的完整实现以及我在调试过程中踩过的那些坑。核心关键词就三个分数类设计、运算符重载、最大公约数约分。把这三个吃透这个项目基本就稳了。2. 整体设计与方案选型2.1 用类封装还是用函数堆最朴素的做法是写四个函数add、sub、mul、div每个接收四个整数参数两个分数的分子分母返回四个整数。这样写能跑但有几个致命问题一是调用时参数顺序容易搞混add(a,b,c,d)到底是a/b c/d还是别的看代码的人得对着函数定义猜二是没法链式运算a b c得写成嵌套调用可读性极差三是没法统一处理约分和符号每个函数都得重复一遍约分逻辑。所以正确的做法是设计一个Fraction类用两个整型成员numerator分子和denominator分母表示一个分数然后把加减乘除都做成运算符重载。这样f1 f2写起来跟整数运算一模一样代码自解释性直接拉满。这也是C里“用类型系统表达意图”的典型思路——你定义了一个新类型就应该让它用起来像内置类型一样自然。2.2 分子分母用什么类型用int还是long long这是个必须提前想清楚的问题。假设你用int两个分母相乘做通分分母最大到 46340 左右就会溢出因为 46340² ≈ 2.1×10⁹接近 int 上限。也就是说只要用户输入1/50000 1/50000通分时50000 × 50000 2.5×10⁹直接溢出成负数结果就废了。我的选择是long long。64位有符号整数上限约 9.2×10¹⁸两个分母相乘只要各自不超过 3×10⁹ 就安全实际使用中分母很少超过几万余量非常充足。代价只是每个对象多占8字节对这个小项目来说完全可以接受。如果你要做竞赛级的高精度分数运算那得考虑大整数库但那是另一个话题了。2.3 约分策略构造时约还是运算后约有两种策略一是每次运算完立即约分二是构造对象时约分、运算时先不约。我选的是构造时约分 运算结果构造时自动约分。因为Fraction的构造函数里调用一次reduce()所有通过构造函数产生的对象包括运算返回的临时对象都是最简形式这样对象的不变式invariant就非常清晰任何时候分子分母都互质分母恒为正。这个设计的好处是你不需要在每个运算符函数里手动写约分代码只要保证返回值是通过构造函数创建的就行。坏处是构造函数的开销稍微大一点一次gcd计算但gcd是欧几里得算法对数级复杂度可以忽略。2.4 符号放在哪分子还是分母数学上分数-1/2和1/-2是等价的但代码里必须统一。我的约定是分母永远为正符号由分子承载。这样比较两个分数时不用先统一符号输出时也简单——直接输出分子、斜杠、分母即可负数自然显示成-1/2。实现上就是在构造函数里判断如果分母小于0分子分母同时取反。注意这里有个坑如果分母是LLONG_MIN取反会溢出不过实际场景中分母不会到那个量级可以不做特殊处理但心里要有数。3. 核心细节与实操要点3.1 最大公约数辗转相除还是更相减损约分的核心是求最大公约数GCD。两种经典算法辗转相除法欧几里得算法和更相减损术。辗转相除法的时间复杂度是 O(log min(a,b))更相减损术最坏情况是 O(max(a,b))比如求 gcd(1000000, 1) 要减一百万次。所以毫无疑问选辗转相除法。但辗转相除法有个细节要注意取模运算对负数不友好。C里-7 % 3结果是-1而不是2所以如果直接对负数取模递归可能出问题。我的做法是在gcd函数开头先把两个参数取绝对值保证全程正数运算。取绝对值也有溢出风险abs(LLONG_MIN)未定义但同样实际输入不会到那个量级。long long gcd(long long a, long long b) { a (a 0) ? -a : a; b (b 0) ? -b : b; while (b ! 0) { long long t a % b; a b; b t; } return a; }用迭代而不是递归避免栈溢出虽然对数级深度递归也不会爆栈但迭代更稳妥。3.2 构造函数里的约分与符号归一构造函数要做三件事检查分母是否为0、归一化符号、约分。顺序很重要——先检查分母为0否则后面除零崩溃再归一化符号保证gcd处理的是正数最后约分。Fraction(long long num, long long den) { if (den 0) { throw std::invalid_argument(分母不能为零); } if (den 0) { num -num; den -den; } long long g gcd(num, den); numerator num / g; denominator den / g; }这里有个细节如果num是0gcd(0, den) 返回 den约分后变成0/1这是正确的。如果 num 和 den 都是负数比如-2/-4先归一化符号变成2/4再约分成1/2也对。注意分母为0时抛异常而不是返回错误码是因为分数对象一旦构造出来就应该始终有效。用异常可以让调用方明确知道出了问题而不是拿到一个“坏对象”继续运算。3.3 运算符重载的返回值设计四个运算符都返回Fraction值而不是引用因为运算结果是新对象没有现成的对象可以返回引用。有人会想返回const Fraction来避免拷贝但那是错的——返回局部对象的引用是悬垂引用程序直接崩。参数用const Fraction传递避免不必要的拷贝。对于这么小的对象16字节拷贝开销其实可以忽略但养成传const引用的习惯没坏处。Fraction operator(const Fraction other) const { long long num numerator * other.denominator other.numerator * denominator; long long den denominator * other.denominator; return Fraction(num, den); }注意这里通分后的分子可能很大numerator * other.denominator最坏情况是 3×10⁹ × 3×10⁹超过 long long 上限。但实际使用中分子分母不会那么大如果你要防御性编程可以在乘法前检查是否溢出不过那会让代码复杂很多这个项目里我选择不做。3.4 除法为什么要单独处理除法a/b ÷ c/d a/b × d/c看起来就是把第二个分数取倒数再相乘。但有个致命问题如果c是0取倒数后分母为0构造函数会抛异常。这其实是正确的行为——除以零本来就该报错。但如果你在operator/里直接写return *this * Fraction(other.denominator, other.numerator);当other.numerator为0时Fraction(den, 0)会抛异常异常信息是“分母不能为零”用户可能一头雾水我明明做的是除法怎么跟我说分母为零更好的做法是在operator/里显式检查除数是否为零抛出更明确的异常信息Fraction operator/(const Fraction other) const { if (other.numerator 0) { throw std::invalid_argument(除数不能为零); } return *this * Fraction(other.denominator, other.numerator); }这样报错信息更贴合用户的操作意图。4. 完整实现与关键环节拆解4.1 类的完整定义把前面说的都串起来Fraction类的头文件大概长这样#include iostream #include stdexcept #include numeric class Fraction { private: long long numerator; long long denominator; static long long gcd(long long a, long long b) { a (a 0) ? -a : a; b (b 0) ? -b : b; while (b ! 0) { long long t a % b; a b; b t; } return a; } void reduce() { long long g gcd(numerator, denominator); numerator / g; denominator / g; } public: Fraction(long long num 0, long long den 1) { if (den 0) { throw std::invalid_argument(分母不能为零); } if (den 0) { num -num; den -den; } numerator num; denominator den; reduce(); } long long getNumerator() const { return numerator; } long long getDenominator() const { return denominator; } Fraction operator(const Fraction other) const { long long num numerator * other.denominator other.numerator * denominator; long long den denominator * other.denominator; return Fraction(num, den); } Fraction operator-(const Fraction other) const { long long num numerator * other.denominator - other.numerator * denominator; long long den denominator * other.denominator; return Fraction(num, den); } Fraction operator*(const Fraction other) const { return Fraction(numerator * other.numerator, denominator * other.denominator); } Fraction operator/(const Fraction other) const { if (other.numerator 0) { throw std::invalid_argument(除数不能为零); } return Fraction(numerator * other.denominator, denominator * other.numerator); } bool operator(const Fraction other) const { return numerator other.numerator denominator other.denominator; } bool operator(const Fraction other) const { return numerator * other.denominator other.numerator * denominator; } friend std::ostream operator(std::ostream os, const Fraction f) { if (f.denominator 1) { os f.numerator; } else { os f.numerator / f.denominator; } return os; } friend std::istream operator(std::istream is, Fraction f) { long long num, den; char slash; is num slash den; if (slash ! /) { is.setstate(std::ios::failbit); return is; } f Fraction(num, den); return is; } };4.2 输入输出重载的细节输出重载里我加了一个判断如果分母是1只输出分子。这样Fraction(4, 2)约分后是2/1输出就是2而不是2/1更符合人的阅读习惯。这个细节很多实现会忽略但用户体验差别很大。输入重载要求用户输入分子/分母格式比如3/4。如果用户输入3没有斜杠is num slash den会在读slash时失败slash保持原值未初始化所以我在读之前应该先初始化slash 0或者用is num然后is.get()读斜杠。更稳妥的写法是friend std::istream operator(std::istream is, Fraction f) { long long num, den; char slash 0; is num slash den; if (!is || slash ! /) { is.setstate(std::ios::failbit); return is; } f Fraction(num, den); return is; }这样即使输入格式错误流状态也会被正确设置调用方可以通过if (cin f)判断是否成功。4.3 比较运算符的必要性有人觉得分数运算只要加减乘除就够了比较运算符是多余的。但实际用起来和非常有用——比如你想验证1/2 1/3 5/6没有就得手动比较分子分母。而且在排序场景下必不可少比如把一组分数从小到大排列。的实现是交叉相乘比较a/b c/d等价于a*d c*b因为分母都为正。这里同样有溢出风险但和前面一样实际场景可以接受。4.4 主函数与测试用例写一个简单的主函数来验证各种情况int main() { Fraction a(1, 2); Fraction b(1, 3); std::cout a b a b std::endl; std::cout a - b a - b std::endl; std::cout a * b a * b std::endl; std::cout a / b a / b std::endl; Fraction c(-4, 6); std::cout 约分结果: c std::endl; try { Fraction d(1, 0); } catch (const std::exception e) { std::cout 异常捕获: e.what() std::endl; } try { Fraction e(1, 2); Fraction f(0, 1); std::cout e / f std::endl; } catch (const std::exception e) { std::cout 异常捕获: e.what() std::endl; } return 0; }预期输出1/2 1/3 5/6 1/2 - 1/3 1/6 1/2 * 1/3 1/6 1/2 / 1/3 3/2 约分结果: -2/3 异常捕获: 分母不能为零 异常捕获: 除数不能为零5. 常见问题与排查技巧实录5.1 结果符号不对最常见的症状是1/2 - 3/4算出来是1/4而不是-1/4。原因通常是构造函数里没有处理分母为负的情况或者gcd函数对负数取模出了问题。排查方法先单独测试Fraction(-1, 4)的输出如果输出1/-4或者-1/-4说明符号归一化没做对。检查构造函数里if (den 0)那个分支确保分子分母同时取反。5.2 约分不彻底比如2/4输出还是2/4。这通常是gcd函数写错了或者构造函数里忘了调用reduce()。排查方法单独测试gcd(2, 4)是否返回2。如果gcd返回1检查辗转相除的循环条件是不是写成了while (b 0)而不是while (b ! 0)前者在b为负数时会直接跳过循环。5.3 除以零没有报错如果1/2 / 0/1没有抛异常而是输出了某个奇怪的结果说明operator/里没有检查除数是否为零。注意检查的是other.numerator 0而不是other.denominator 0因为除数0/1的分子是0、分母是1分母不为零但整个分数值为零。5.4 大数运算溢出输入1/100000 1/100000结果变成负数这是通分时100000 * 100000 10¹⁰超过了int上限。解决办法就是把成员类型改成long long。如果你已经用了long long还溢出那说明输入的分母超过了 3×10⁹这种场景建议直接上大整数库或者限制输入范围。5.5 常见问题速查表症状可能原因排查方法解决方案结果符号错误分母为负未归一化测试Fraction(-1,4)输出构造函数中分母为负时分子分母同时取反约分不彻底gcd实现错误或未调用单独测试gcd(2,4)检查辗转相除循环条件确保构造函数调用reduce除以零未报错operator/未检查测试1/2 / 0/1在operator/中检查other.numerator是否为0大数溢出成员类型为int测试1/50000 1/50000改用long long输入格式错误崩溃未检查流状态输入3不带斜杠读入后检查slash是否为/设置failbit输出2/1而非2输出重载未处理分母为1测试Fraction(4,2)输出时判断分母是否为1实操心得调试分数类的时候不要一上来就测复杂的四则混合运算。先测单个分数的构造和输出确保Fraction(4, 6)输出2/3、Fraction(-4, 6)输出-2/3、Fraction(4, -6)也输出-2/3。这三个用例过了再测加减法最后测乘除和异常。由简到繁定位问题会快很多。6. 进一步扩展的方向这个基础版本跑通之后有几个方向可以继续打磨。一是加上、-、*、/这些复合赋值运算符让f1 f2这种写法也能用实现上就是*this *this other; return *this;。二是加上、、、!这些比较运算符其实有了和之后其他的都可以基于这两个推导出来。三是把分数和整数之间的运算也支持上比如f 1可以通过隐式转换构造函数Fraction(long long num 0, long long den 1)来实现因为整数可以隐式转成Fraction。还有一个比较实用的扩展是加上toDouble()方法返回double类型的近似值方便和浮点数比较或者输出。实现就是return static_castdouble(numerator) / denominator;注意要先转double再除否则整数除法会截断。我在实际写这个类的时候最大的体会是边界情况永远比正常情况多。正常路径1/2 1/3五分钟就能写对但分母为零、分子为零、负数、大数、输入格式错误这些边界花的时间是正常路径的好几倍。所以写完第一版之后一定要专门花时间构造边界用例去测而不是跑通一个正常用例就收工。这个习惯在写任何有数学运算的代码时都适用。

相关新闻

C#调用Cplex求解器实战:从环境配置到混合整数规划建模

C#调用Cplex求解器实战:从环境配置到混合整数规划建模

1. 为什么要在C#里折腾Cplex做过生产排程、物流调度或者资源分配这类项目的人,大概率都绕不开一个名字——Cplex。它在数学规划求解这个圈子里算是老牌选手了,线性规划、混合整数规划、二次规划都能扛,求解速度和稳定性在商业求解器里一直排在…

2026/10/12 17:26:47 阅读更多 →
大厂高薪offer怎么算怎么谈?校招总包背后的结构与避坑指南

大厂高薪offer怎么算怎么谈?校招总包背后的结构与避坑指南

最近朋友圈又被一份校招薪资表刷屏了。周围同行都在转,评论区清一色的感慨:不愧是那家厂,今年的薪资也很顶。我盯着那张表看了很久,比起数字本身,更让我在意的,是这几年互联网大厂薪酬设计背后透露出的逻辑…

2026/10/13 21:20:40 阅读更多 →
Python酒店评论情感分析:从TF-IDF到BERT的完整源码实战

Python酒店评论情感分析:从TF-IDF到BERT的完整源码实战

简介:这是一份面向计算机相关专业学生与项目实战学习者的酒店评论情感分析完整项目包,由导师指导并通过评审,评审分98分,适合用作毕业设计、课程设计或期末大作业参考。资源共23个文件,压缩包约4.36MB,以14…

2026/10/14 2:01:05 阅读更多 →

最新新闻

软考高项备考:每日5题训练法与解题技巧

软考高项备考:每日5题训练法与解题技巧

1. 项目背景与价值解析"3.13软考高项-每日5题"这个标题看似简单,却蕴含着备考者最迫切的需求。作为参加过多次软考高项(信息系统项目管理师)的过来人,我深知最后冲刺阶段每日保持题感的重要性。这个每日5题的训练模式&a…

2026/10/14 2:14:21 阅读更多 →
城域网SDN算法实战:路径计算、多径调度与故障收敛

城域网SDN算法实战:路径计算、多径调度与故障收敛

前两周在某地市的城域网机房做夜间割接,折腾的不是设备,而是把一张跑了快十年的老网平移进SDN控制面。这活儿做过的人都知道,真正让人熬夜到凌晨的从来不是交换机本身,而是控制器上那套网络工程算法。PPT上讲起来三页纸&#xff0…

2026/10/14 2:14:21 阅读更多 →
Bitnami Spark Helm Chart 版本演进全解:从 0.0.1 到 10.x 的功能、安全与部署实践

Bitnami Spark Helm Chart 版本演进全解:从 0.0.1 到 10.x 的功能、安全与部署实践

云原生容器编排 【免费下载链接】charts Bitnami Helm Charts 项目地址: https://gitcode.com/GitHub_Trending/charts30/charts 点击查看 免费下载 Apache Spark 是面向大规模数据处理、机器学习与实时流计算的高性能计算引擎,支持 Java、Python、Scal…

2026/10/14 2:14:21 阅读更多 →
智慧物流系统架构实战:微服务、Spring Boot与Redis缓存设计

智慧物流系统架构实战:微服务、Spring Boot与Redis缓存设计

1. 先看清业务:智慧物流的核心链路与Java技术栈1.1 智慧物流场景到底在解决什么问题很多人一看到“智慧物流”这个词,下意识觉得是无人车、无人机、自动分拣机器人这些硬件,实际上落到系统层面,真正核心的是订单流转和数据处理。一…

2026/10/14 2:14:20 阅读更多 →
物料主数据编号范围:从MMNR配置到NRIV扩位摸底ABAP报表

物料主数据编号范围:从MMNR配置到NRIV扩位摸底ABAP报表

简介:SAP后台配置中,物料主数据编号范围的维护是一项基础且关键的工作。这份资源以物料类型为主线,完整讲解号码组内部段与外部段的规则、物料类型分派逻辑及冲突处理办法,适合SAP顾问、物料管理关键用户及ERP后台配置学习者参考。…

2026/10/14 2:14:20 阅读更多 →
DeepSeek写教案怎么导出Word?完整步骤

DeepSeek写教案怎么导出Word?完整步骤

DeepSeek写教案怎么导出Word?完整步骤用 DeepSeek 写教案,最省事的做法是让它直接按 Markdown 输出,再整篇导出成 Word。教案里常见的教学目标、课时安排、板书设计、作业布置,在 Markdown 里就是标题层级和列表;一旦用…

2026/10/14 2:13:20 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →