本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文介绍了一个基于C++面向对象编程思想设计的分数类(Fraction),支持分数间的加、减、乘、除运算,比较操作及与浮点数之间的相互转换。该类封装了分子和分母的数据成员,并通过构造函数、拷贝构造、赋值重载、运算符重载等机制实现完整的分数操作体系。结合fraction.h头文件中的类定义与fraction.cpp源文件中的具体实现,该项目不仅体现了封装、多态等OOP核心概念,还为学习者提供了深入理解类设计、运算符重载和数值处理逻辑的实践范例。适用于C++初学者掌握面向对象编程技巧,并可作为复杂数学计算模块的基础组件。
c++写的分数类,可进行运算操作和比较操作及和浮点数据的转换操作

1. C++分数类的设计理念与核心目标

在现代编程实践中,数值类型的扩展与自定义是提升程序可读性与数学表达能力的重要手段。本章将围绕C++中实现一个功能完整的分数类( Fraction )展开讨论,重点阐述其设计初衷与整体架构思路。分数作为有理数的典型代表,在科学计算、教育软件及金融算法中具有广泛的应用场景。传统的浮点数表示存在精度丢失问题,而通过封装分子与分母的整数形式,分数类能够实现精确的有理数运算。

class Fraction {
private:
    long long num, den;  // 分子与分母,私有化确保数据安全
public:
    Fraction(long long n = 0, long long d = 1); // 构造函数保障分母非零
    Fraction operator+(const Fraction& rhs) const;
    bool operator==(const Fraction& rhs) const;
    double to_double() const; // 精确转换为浮点近似值
};

该类遵循面向对象的核心原则: 封装 保证数据完整性, 运算符重载 提供自然的数学语法, 自动约简机制 确保结果最简。设计目标明确:构建一个类型安全、语义清晰、性能可控的有理数抽象,为后续复杂数学库开发奠定基础。

2. 分数类的数据结构与初始化机制

在设计一个功能完整且具备工业级健壮性的C++分数类时,数据结构的合理布局与对象生命周期的正确管理是系统稳定运行的基础。本章将深入剖析 Fraction 类的核心组成要素,从底层存储机制到构造、拷贝与赋值过程中的语义保障,层层递进地揭示其内在实现逻辑。不同于简单的数值封装,一个高质量的分数类必须在保证数学正确性的同时,兼顾性能效率和异常安全性。这要求我们在成员变量的设计上遵循严格的封装原则,在初始化流程中植入完善的校验机制,并通过现代C++的资源管理理念确保深拷贝语义和赋值操作的安全执行。

2.1 数据成员的设计与封装策略

面向对象编程的核心思想之一就是“数据隐藏”——将内部状态私有化以防止外部非法访问或破坏。对于分数这一数学概念而言,其本质由两个整数构成:分子(numerator)和分母(denominator)。然而,若不对这两个字段施加约束,则极易引发诸如除零错误、符号混乱或精度丢失等问题。因此,合理的数据封装不仅是语法层面的要求,更是程序行为可预测性的关键所在。

2.1.1 分子与分母的私有化存储

为了实现对分数状态的有效控制, Fraction 类应将分子与分母声明为 private 成员变量。这种设计强制所有对外暴露的操作都必须通过公有接口进行,从而可以在接口层统一处理边界条件、规范化逻辑和异常检测。

class Fraction {
private:
    long long numerator_;   // 分子,支持负数
    long long denominator_; // 分母,始终为正(经规范化后)

public:
    // 构造函数、运算符重载等公共接口
};

上述代码中使用 long long 类型是为了扩大表示范围,避免在进行乘法等中间计算时发生溢出。虽然 int 在某些场景下足够,但考虑到后续可能参与复杂表达式求值,采用更大的整型更为稳妥。

值得注意的是,分母被设计为 始终为正 。这意味着当用户输入负分数时,负号应统一转移到分子上。例如: -3/4 应表示为 numerator_ = -3 , denominator_ = 4 ;而 3/-4 则需自动转换为此形式。该策略不仅提升了比较操作的一致性,也为后续的约简与输出提供了便利。

此外,由于这些成员变量处于私有区域,任何修改都必须经过类提供的方法完成。例如:

void setNumerator(long long n) { 
    numerator_ = n; 
    normalize(); // 自动约简并调整符号
}

这种方式实现了“变更即规范”的设计理念,使得对象无论经历多少次修改,其内部状态始终保持合法且标准化。

封装带来的优势分析
优势点 说明
安全性提升 外部无法直接篡改分子分母,防止非法状态如 0/0 -5/-7 未经处理存在
行为一致性 所有赋值路径均经过同一规范化函数,确保结果唯一
易于调试 可在 setter 中插入断言或日志监控状态变化
扩展性强 后续可加入监听机制或通知模式
classDiagram
    class Fraction {
        -long long numerator_
        -long long denominator_
        +Fraction()
        +Fraction(long long, long long)
        +void normalize()
        +double toDouble()
    }
    note right of Fraction
      私有成员不可外部访问,
      所有修改必须通过成员函数
    end note

该类图清晰展示了封装结构:外部仅能调用构造函数或接口方法,而不能触及底层数据。这种隔离机制是构建可靠抽象类型的基石。

2.1.2 数据完整性约束与异常预防

尽管私有化保护了数据不被随意更改,但仍需在构造和赋值过程中主动验证输入合法性。最典型的两个问题是: 分母为零 极端值溢出 。若不加以防范,前者会导致运行时崩溃,后者则引发未定义行为。

分母为零的防御机制

分母为零是分数类中最致命的问题。一旦出现,后续所有算术运算都将失去意义。为此,应在每一个接受分母参数的入口处进行显式检查:

Fraction::Fraction(long long num, long long den) 
    : numerator_(num), denominator_(den) 
{
    if (denominator_ == 0) {
        throw std::invalid_argument("Denominator cannot be zero.");
    }
    normalize(); // 约简并统一符号
}

此处使用标准异常 std::invalid_argument 抛出错误信息,符合STL惯例。调用者可通过 try-catch 捕获并处理异常,避免程序终止。

更进一步,可在编译期尝试拦截此类问题。借助 constexpr 构造函数配合静态断言:

constexpr Fraction(long long num, long long den) 
    : numerator_(num), denominator_(den)
{
    if (den == 0) {
        // 编译期无法抛出异常,可用此技巧触发错误
        static_assert(false, "Denominator cannot be zero in constexpr context");
    }
}

不过目前 C++ 标准不允许在 constexpr 函数中抛出异常,因此实际应用中仍依赖运行时判断为主。

符号规范化流程

另一个常见问题是符号分布不一致。例如 3/-4 -3/4 数学等价,但若不做归一化,会影响相等性比较。解决方案是在每次构造或修改后调用 normalize() 函数:

void Fraction::normalize() {
    if (denominator_ < 0) {
        numerator_   = -numerator_;
        denominator_ = -denominator_;
    }
    long long gcd_val = gcd(std::abs(numerator_), denominator_);
    numerator_   /= gcd_val;
    denominator_ /= gcd_val;
}

该函数首先将分母转为正值,再利用最大公约数(GCD)进行约分。整个过程确保每个 Fraction 实例在创建后立即进入标准形式。

异常安全表:关键操作的风险控制
操作类型 风险点 防御措施
构造函数 分母为零 抛出 invalid_argument
赋值操作 源对象分母为零 检查后再赋值
输入流解析 用户输入 3/0 流状态置位 failbit
运算结果 中间结果溢出 使用 long long 并可选检查

通过建立这样的防护矩阵,我们能够系统性地识别潜在漏洞,并提前部署应对策略。

2.2 构造函数的多态化实现

构造函数是对象诞生的第一道关口,其设计质量直接影响整个类的可用性和鲁棒性。理想的 Fraction 类应提供多种构造方式,满足不同使用场景的需求,同时保持接口简洁一致。这正是“多态化构造”的含义:同一个类支持多种形式的初始化,体现灵活而统一的API风格。

2.2.1 默认构造函数与初始状态设定

默认构造函数用于创建一个“占位”分数对象,通常表示数值 0 。选择 0/1 而非 0/任意 是出于标准化考虑——它既避免了无意义的分母选择,也便于后续参与运算:

Fraction::Fraction() 
    : numerator_(0), denominator_(1) 
{}

此构造函数无需参数,常用于数组初始化、容器元素填充或临时变量声明:

std::vector<Fraction> vec(5); // 创建5个 0/1 的分数
Fraction f;                    // 局部变量初始化

值得注意的是,即使默认构造的对象看似简单,仍需确保其满足所有内部约束。例如, denominator_ 必须为正,且已约简(显然 0/1 已是最简)。这类细节虽小,却是构建可信类库的关键。

2.2.2 带参构造函数的参数校验逻辑

带参构造函数承担主要的初始化任务,接收分子与分母作为输入。除了基本赋值外,还必须集成完整的校验与规范化流程:

Fraction::Fraction(long long num, long long den)
    : numerator_(num), denominator_(den)
{
    if (den == 0) {
        throw std::domain_error("Denominator cannot be zero.");
    }

    // 统一符号:负号移至分子
    if (den < 0) {
        numerator_ = -numerator_;
        denominator_ = -den;
    }

    // 自动约简
    long long g = gcd(abs(numerator_), denominator_);
    numerator_ /= g;
    denominator_ /= g;
}
逐行逻辑分析:
  1. : numerator_(num), denominator_(den)
    初始化列表直接赋值,高效且安全。
  2. if (den == 0)
    立即检查分母是否为零,防止后续除法出错。使用 domain_error 更准确表达“数学定义域违规”。

  3. if (den < 0)
    若分母为负,同时翻转分子与分母符号,使分母恒正。这是规范化的一部分。

  4. gcd(...)
    计算最大公约数,用于约分。假设 gcd() 是已实现的辅助函数(详见第四章)。

  5. numerator_ /= g; denominator_ /= g;
    执行约分,确保结果为最简分数。

该构造函数体现了“初始化即合法”的设计哲学:无论输入如何,输出必然是一个数学有效、格式标准的分数对象。

2.2.3 防止分母为零的安全机制

分母为零不仅出现在构造阶段,也可能源于用户输入或计算传递。为此,除了构造函数内的检查,还需在整个类体系中建立统一的防御网。

一种增强方案是引入“工厂函数”,替代部分构造逻辑:

static std::optional<Fraction> create(long long num, long long den) {
    if (den == 0) return std::nullopt;
    return Fraction(num, den);
}

使用 std::optional 可避免抛异常,适用于不想中断流程的场景:

auto frac = Fraction::create(3, 0);
if (!frac) {
    std::cout << "Invalid fraction\n";
}

此外,结合自定义异常类型可实现更精细的错误分类:

struct ZeroDenominatorException : public std::exception {
    const char* what() const noexcept override {
        return "Attempted to create fraction with zero denominator";
    }
};

如此一来,调用者可根据具体异常类型做出差异化响应。

2.3 拷贝构造函数的深拷贝语义保障

在C++中,当对象被复制时,默认合成的拷贝构造函数会执行逐成员复制(shallow copy)。对于包含原始指针或动态资源的类,这可能导致双重释放等问题。幸运的是, Fraction 类仅含两个 long long 成员,属于“平凡可复制”类型,浅拷贝即可满足需求。但我们仍应显式定义拷贝构造函数,以明确传达设计意图并预留未来扩展空间。

2.3.1 值语义复制的必要性分析

分数类应表现为“值类型”(value semantics),即每个实例独立拥有自己的数据副本。以下代码展示了预期行为:

Fraction a(3, 4);
Fraction b = a;     // b 是 a 的副本
b += Fraction(1, 4); // 修改 b 不影响 a
assert(a.toDouble() == 0.75); // a 仍为 3/4

若未正确定义拷贝构造函数,虽然编译器生成的版本也能完成复制,但缺乏可读性且难以追踪。显式定义如下:

Fraction::Fraction(const Fraction& other)
    : numerator_(other.numerator_)
    , denominator_(other.denominator_)
{}

该函数接受常量引用,避免不必要的拷贝,并确保源对象不会被修改。由于成员均为基本类型,无需额外资源管理,故实现极为简洁。

2.3.2 自我赋值检测与资源管理

尽管拷贝构造函数不涉及自我赋值问题(因为总是一个新对象),但在其他复制场景(如赋值运算符)中必须警惕。虽然当前类无动态资源,但养成良好的习惯至关重要。

设想未来扩展支持大整数(BigInt)作为分子分母:

class Fraction {
    BigInt* num_ptr;
    BigInt* den_ptr;
    // ...
};

此时若发生 f = f; ,默认赋值会导致指针重复释放。因此,即便现在不需要,也应在拷贝构造中保持警惕:

Fraction::Fraction(const Fraction& other) {
    if (this == &other) return; // 自我引用检测(虽构造时不成立)
    numerator_ = other.numerator_;
    denominator_ = other.denominator_;
}

虽然构造函数中 this != &other 恒成立(因构造新对象),但添加此检查有助于代码风格统一,并为继承体系打下基础。

2.4 赋值运算符重载的异常安全实现

赋值运算符决定了对象在生命周期内如何更新自身状态。一个正确的实现不仅要完成数据复制,还需满足 强异常安全保证 :即在异常抛出时,原对象保持不变。

2.4.1 运算符返回引用的规范用法

赋值运算符的标准签名应返回当前对象的引用,以便支持链式赋值:

Fraction& operator=(const Fraction& rhs) {
    if (this != &rhs) {           // 防止自我赋值
        numerator_ = rhs.numerator_;
        denominator_ = rhs.denominator_;
    }
    return *this;                 // 支持 a = b = c
}
参数说明:
  • const Fraction& rhs :右操作数,以常量引用传递,避免复制开销。
  • 返回 Fraction& :允许连续赋值,符合语言惯例。
  • if (this != &rhs) :经典自我赋值检测,防止无谓操作。

该实现虽简单,却完整覆盖了核心需求。

2.4.2 RAII原则在赋值过程中的体现

RAII(Resource Acquisition Is Initialization)是C++资源管理的黄金准则。虽然 Fraction 目前无堆资源,但若未来引入动态内存(如变长精度),赋值操作就必须更加谨慎。

一种更安全的写法是采用“复制并交换”(copy-and-swap)惯用法:

Fraction& operator=(Fraction temp) {  // 按值传参,自动完成复制
    swap(temp);                       // 异常安全的交换
    return *this;
}

void swap(Fraction& other) noexcept {
    std::swap(numerator_, other.numerator_);
    std::swap(denominator_, other.denominator_);
}
优势分析:
特性 说明
异常安全 复制在参数传递时完成,失败不影响原对象
自我赋值安全 无需显式检测
代码简洁 删除手动释放逻辑
sequenceDiagram
    participant Client
    participant Operator
    participant Temporary
    Client->>Operator: a = b
    Operator->>Temporary: 按值构造副本
    alt 构造失败
        Temporary-->>Operator: 抛出异常,a 不变
    else 成功
        Operator->>Operator: swap(this, temp)
        Operator-->>Client: 返回 *this
    end

此流程图展示了 copy-and-swap 的执行路径:任何异常都在副本构造阶段抛出,主对象始终不受影响。

综上所述,分数类的初始化机制不仅仅是“设置几个变量”,而是涉及封装、校验、异常处理与资源管理的综合工程。唯有在这些细节上精益求精,才能打造出真正可靠、可复用的数学组件。

3. 运算符重载的理论基础与实践实现

在C++中,运算符重载是面向对象编程的核心机制之一,它允许用户自定义类型(如类)像内置类型一样使用标准操作符进行表达式计算。对于一个功能完整的分数类( Fraction ),必须支持常见的算术、比较和输入输出操作,从而提升代码的可读性、自然性和数学表达能力。本章将深入探讨运算符重载的理论依据,并结合实际代码实现,系统阐述如何为分数类构建一套高效、安全且符合数学逻辑的操作接口。

运算符重载的本质是函数重载的一种特殊形式——通过关键字 operator 后接特定符号来定义操作行为。然而,其设计并非简单的语法封装,而是涉及语义一致性、性能优化、临时对象管理以及异常安全等多方面考量。特别是在有理数这种需要保持精确性的场景下,每一个运算都必须确保结果的数学正确性,同时兼顾程序运行效率。

3.1 算术运算符的重载设计

算术运算是分数类最核心的功能需求,包括加法(+)、减法(-)、乘法(*)和除法(/)。这些操作不仅要符合基本的代数法则,还需处理符号传播、分母归一化及溢出预防等问题。为了实现直观的表达式书写习惯(如 a + b ),必须对相应运算符进行合理重载。

3.1.1 加减乘除运算的数学规则映射

分数的四则运算遵循如下数学公式:

运算 公式
加法 $ \frac{a}{b} + \frac{c}{d} = \frac{ad + bc}{bd} $
减法 $ \frac{a}{b} - \frac{c}{d} = \frac{ad - bc}{bd} $
乘法 $ \frac{a}{b} \times \frac{c}{d} = \frac{ac}{bd} $
除法 $ \frac{a}{b} \div \frac{c}{d} = \frac{a}{b} \times \frac{d}{c} = \frac{ad}{bc},\ c \neq 0 $

这些公式构成了所有运算实现的基础逻辑。以加法为例,两个分数相加时需通分后合并分子,但直接采用交叉相乘会导致分母迅速膨胀,因此应在每次运算后立即调用约简函数(如 normalize() )以维持数值稳定性。

下面是一个典型的成员函数与友元函数混合实现方案:

class Fraction {
private:
    long long num;   // 分子
    long long den;   // 分母(始终为正)
    void normalize(); // 私有辅助函数:约简并规范符号

public:
    // 构造函数略...

    // 成员形式的复合赋值运算符
    Fraction& operator+=(const Fraction& rhs);
    Fraction& operator-=(const Fraction& rhs);
    Fraction& operator*=(const Fraction& rhs);
    Fraction& operator/=(const Fraction& rhs);

    // 友元形式的二元运算符
    friend Fraction operator+(Fraction lhs, const Fraction& rhs);
    friend Fraction operator-(Fraction lhs, const Fraction& rhs);
    friend Fraction operator*(Fraction lhs, const Fraction& rhs);
    friend Fraction operator/(Fraction lhs, const Fraction& rhs);
};

上述设计采用了“基于赋值运算符”的惯用法(copy-and-swap 或 pass-by-value 技巧),其中二元运算符通过传值接收左操作数,再复用复合赋值运算符完成计算,避免重复编码。

代码解析:加法运算符实现
// 实现 +=
Fraction& Fraction::operator+=(const Fraction& rhs) {
    long long new_num = num * rhs.den + rhs.num * den;
    long long new_den = den * rhs.den;

    num = new_num;
    den = new_den;
    normalize();  // 自动约简
    return *this;
}

// 实现 +
Fraction operator+(Fraction lhs, const Fraction& rhs) {
    lhs += rhs;
    return lhs;
}

逐行逻辑分析:

  1. long long new_num = num * rhs.den + rhs.num * den;
    使用交叉相乘法计算新分子,确保通分正确。
  2. long long new_den = den * rhs.den;
    新分母为原分母乘积,虽可能导致溢出,但在后续 normalize() 中可通过最大公约数压缩。

  3. num = new_num; den = new_den;
    更新当前对象状态,适用于 += 操作。

  4. normalize();
    调用内部函数进行约分和符号规范化(负号统一到分子)。

  5. return *this;
    返回引用以支持链式赋值(如 a += b += c )。

  6. 外部 operator+ 接收 lhs 为值传递,自动构造副本,调用 += 修改副本并返回,利用了编译器的返回值优化(RVO/NRVO)减少开销。

参数说明:
- rhs 表示右操作数,以 const Fraction& 形式传入,避免拷贝;
- lhs 在非成员函数中按值传递,代价由现代编译器优化吸收;
- 使用 long long 防止中间结果溢出,但仍建议加入溢出检测机制用于生产环境。

该模式同样适用于 - , * , / ,只需调整运算逻辑即可。

3.1.2 友元函数与成员函数的选择依据

选择何种方式重载运算符直接影响接口灵活性与对称性。以下是常见决策准则:

运算符类型 推荐实现方式 原因
+= , -= , *= , /= 成员函数 修改自身状态,左侧操作数必为类实例
+ , - , * , / 非成员友元或普通函数 支持隐式转换(如 int + Fraction
<< , >> 友元函数 左操作数为 ostream/istream ,无法作为成员
== , != 非成员函数 对称性要求,便于模板泛化

考虑以下表达式:

Fraction f(1, 2);
f = 2 + f;  // int 自动转换为 Fraction?

operator+ 定义为成员函数:

Fraction Fraction::operator+(const Fraction&) const;

2 + f 会尝试调用 2.operator+(f) ,而 int 无此方法,导致编译失败。

但如果定义为非成员函数:

friend Fraction operator+(const Fraction&, const Fraction&);

配合构造函数支持隐式转换(如 Fraction(int n) ),即可让 2 被自动转为 Fraction(2,1) ,实现对称计算。

因此, 对于具有交换律的运算(如加法、乘法),应优先使用非成员函数实现 ,保证左右操作数均可参与隐式类型转换。

此外,C++ 标准库也推荐“非成员接口原则”(Non-Member Interface Principle),即将能不用成员就不使用的函数定义为非成员,增强封装性。

3.1.3 临时对象的生成与优化处理

在运算过程中,不可避免地会产生大量临时对象。例如:

Fraction result = a + b * c - d / e;

此表达式至少产生 4 个中间结果对象。若未妥善管理,可能引发性能问题。

C++ 提供多种机制缓解这一问题:

(1) 返回值优化(RVO / NRVO)

现代编译器通常会对返回局部对象的情况执行 RVO(Return Value Optimization),即直接在目标位置构造对象,而非先构造再拷贝。

Fraction operator+(Fraction lhs, const Fraction& rhs) {
    return lhs += rhs;  // 编译器可省略拷贝
}

即使没有移动语义,也可能被优化为零开销。

(2) 移动语义支持(Move Semantics)

添加移动构造函数和移动赋值运算符可进一步提升性能:

Fraction(Fraction&& other) noexcept
    : num(other.num), den(other.den)
{
    other.num = 0;
    other.den = 1;
}

Fraction& operator=(Fraction&& other) noexcept {
    if (this != &other) {
        num = other.num;
        den = other.den;
        other.num = 0;
        other.den = 1;
    }
    return *this;
}

当返回临时对象时, std::move 可触发移动而非拷贝。

(3) 表达式模板(高级优化)

更进一步,可通过表达式模板延迟求值,消除中间对象(如 Eigen 库的做法),但这已超出本章范围。

3.2 比较运算符的逻辑一致性构建

比较运算符决定了分数之间能否建立全序关系,进而支持排序、查找、集合容器存储等功能。由于浮点数存在精度误差,而分数是精确表示,因此其比较应严格遵循数学定义。

3.2.1 基于交叉相乘的等价性判断

两个分数相等当且仅当它们的交叉相乘结果相等:
\frac{a}{b} = \frac{c}{d} \iff a \cdot d = b \cdot c
注意:此方法无需通分,避免了大数乘积带来的溢出风险(但仍可能发生)。

实现如下:

bool operator==(const Fraction& lhs, const Fraction& rhs) {
    return lhs.num * rhs.den == rhs.num * lhs.den;
}

bool operator!=(const Fraction& lhs, const Fraction& rhs) {
    return !(lhs == rhs);
}

逻辑分析:
- 利用交叉相乘消除分母影响;
- 直接比较整数是否相等,精确无误;
- != 复用 == 结果,减少错误概率。

溢出控制策略

尽管 long long 提供较大范围,但在极端情况下仍可能溢出。一种改进方式是使用 __int128 (GCC 扩展)或引入大整数库,或者通过分解因数进行安全比较。

另一种替代方案是先约简再比较:

bool operator==(const Fraction& lhs, const Fraction& rhs) {
    Fraction l = lhs; l.normalize();
    Fraction r = rhs; r.normalize();
    return l.num == r.num && l.den == r.den;
}

优点是避免大数乘法,缺点是修改了临时对象的状态(需保证 normalize() 不改变原始值含义)。

3.2.2 全序关系的建立与传递性验证

要使分数可用于 std::set std::map ,必须定义严格弱序(Strict Weak Ordering)。为此需实现 < 运算符:

bool operator<(const Fraction& lhs, const Fraction& rhs) {
    return lhs.num * rhs.den < rhs.num * lhs.den;
}

其他比较运算符可由此推导:

bool operator>(const Fraction& lhs, const Fraction& rhs) { return rhs < lhs; }
bool operator<=(const Fraction& lhs, const Fraction& rhs) { return !(rhs < lhs); }
bool operator>=(const Fraction& lhs, const Fraction& rhs) { return !(lhs < rhs); }
正确性验证:传递性测试

设三个分数:
- $ A = \frac{1}{2}, B = \frac{2}{3}, C = \frac{3}{4} $

验证传递性:若 $ A < B $ 且 $ B < C $,则 $ A < C $

计算:
- $ A < B $? → $ 1×3=3 < 2×2=4 $ ✅
- $ B < C $? → $ 2×4=8 < 3×3=9 $ ✅
- $ A < C $? → $ 1×4=4 < 3×2=6 $ ✅

满足传递性。

Mermaid 流程图:比较运算符依赖结构
graph TD
    A["operator<"] --> B["operator>"]
    A --> C["operator<="]
    A --> D["operator>="]
    E["operator=="] --> F["operator!="]
    style A fill:#4CAF50, color:white
    style E fill:#2196F3, color:white

上图展示了比较运算符之间的逻辑依赖关系,主干为 < == ,其余均可派生,降低维护成本。

3.2.3 浮点近似比较的边界条件控制

虽然分数本身是精确的,但在与 double 类型交互时,常需判断某个浮点值是否“接近”某分数。此时需引入容差比较。

bool approx_equal(const Fraction& f, double value, double epsilon = 1e-9) {
    double diff = std::abs(static_cast<double>(f) - value);
    return diff < epsilon;
}

参数说明:
- f : 待比较的分数对象;
- value : 目标浮点数;
- epsilon : 容差阈值,默认为单精度误差量级;

注意事项:
- static_cast<double> 可能丢失精度(尤其对大分母);
- 应限制最大分母位数以保证转换可靠性;
- 对高精度场景,建议使用连分数逼近反向验证。

3.3 输入输出流的友元函数集成

良好的 I/O 支持是类可用性的关键组成部分。通过重载 << >> ,可实现类似 cin >> f; cout << f; 的简洁语法。

3.3.1 标准IO流的无缝对接方法

std::ostream& operator<<(std::ostream& os, const Fraction& f) {
    if (f.den == 1) {
        os << f.num;           // 整数形式输出
    } else {
        os << f.num << '/' << f.den;
    }
    return os;
}

std::istream& operator>>(std::istream& is, Fraction& f) {
    long long n, d = 1;
    char slash = 0;

    is >> n;                   // 先读取分子
    slash = is.peek();         // 查看下一个字符
    if (slash == '/') {
        is.get();              // 消耗 '/'
        is >> d;               // 读取分母
    }

    if (d == 0) {
        is.setstate(std::ios::failbit);  // 设置失败标志
    } else {
        f = Fraction(n, d);    // 赋值
    }

    return is;
}
代码逻辑逐行解读:
  1. is >> n;
    从输入流提取第一个整数作为分子。

  2. slash = is.peek();
    查看下一个字符是否为 / ,不提取。

  3. if (slash == '/') { ... }
    若存在斜杠,则读取分母;否则视为整数(分母为1)。

  4. is.get();
    消耗 / 字符,防止残留。

  5. if (d == 0)
    检测非法分母,设置 failbit 使流进入错误状态。

  6. f = Fraction(n, d);
    调用构造函数创建合法分数并赋值。

  7. return is;
    支持链式输入(如 cin >> a >> b )。

功能测试示例:
Fraction f1, f2, f3;
std::cin >> f1 >> f2 >> f3;
// 输入:1/2 3/4 5
// 结果:f1=1/2, f2=3/4, f3=5/1

3.3.2 格式化输入的健壮性增强

默认实现易受格式错误干扰(如 1 / 2 中间有空格)。可通过跳过空白字符增强鲁棒性:

std::istream& operator>>(std::istream& is, Fraction& f) {
    long long n, d = 1;
    char slash;

    is >> n;
    is >> std::ws;                    // 跳过空白
    slash = is.peek();

    if (slash == '/') {
        is.get();
        is >> std::ws;
        if (!(is >> d) || d == 0) {
            is.setstate(std::ios::failbit);
            return is;
        }
    }

    f = Fraction(n, d);
    return is;
}

引入 std::ws 可忽略前导空格、制表符等,提高兼容性。

3.4 运算过程中自动约简机制的触发时机

分数在运算后往往出现可约形式(如 2/4 ),若不及时简化,将导致后续运算复杂度上升、比较失效等问题。因此必须明确 normalize() 的调用策略。

3.4.1 每次运算后调用GCD的必要性分析

考虑连续加法:

Fraction f(1, 6);
f = f + f;  // 1/6 + 1/6 = 2/12 → 应化为 1/6

若不约简,则 2/12 会继续参与后续计算,造成冗余。

因此, 所有修改分子分母的操作完成后,必须立即调用 normalize()

典型调用点包括:
- 所有算术运算( += , + , etc.)
- 构造函数
- 赋值运算符
- 输入流解析

void Fraction::normalize() {
    if (den == 0) throw std::invalid_argument("Denominator is zero");
    if (den < 0) {          // 统一负号至分子
        num = -num;
        den = -den;
    }
    long long g = gcd(std::abs(num), den);
    num /= g;
    den /= g;
}

其中 gcd 为欧几里得算法实现(详见第四章)。

3.4.2 性能开销与结果规范化的权衡

频繁调用 gcd 会带来额外开销,尤其在大规模循环中。评估如下:

场景 是否建议约简 理由
数学库内部计算 保障长期稳定性
高频迭代算法 可延迟 每 N 次后批量约简
输出前 必须 用户期望最简形式

折中策略:提供两个版本——
- Fraction::fast_add() :不约简,供内部高性能计算使用;
- 默认运算符:自动约简,保障接口一致性。

最终决定取决于应用场景。对于通用分数类, 默认启用自动约简更为稳妥 ,因其带来的语义清晰度远大于微小性能损失。

综上所述,运算符重载不仅是语法糖的堆砌,更是类行为语义的集中体现。通过科学的设计与严谨的实现,分数类得以呈现出接近原生类型的自然操作体验,同时保持数学上的精确与逻辑上的严密。

4. 辅助算法与类型转换的深度融合

在构建一个功能完备、语义清晰且性能可接受的 C++ 分数类( Fraction )过程中,核心运算逻辑仅是基础。真正决定其工程价值和使用体验的,是在底层支撑这些操作的一系列 辅助算法 以及与其他数值类型的 无缝交互能力 。本章将深入剖析 Fraction 类中不可或缺的关键组件——最大公约数计算、自动约简机制、浮点与分数间的双向转换策略,以及私有工具函数的模块化设计。这些看似“附属”的功能实则构成了整个类稳定运行的基石,并深刻影响着精度控制、内存效率与接口一致性。

尤其值得注意的是,现代 C++ 对类型系统的要求日益严格,用户期望自定义类型能够像内置类型一样自然地参与表达式求值、流输入输出甚至编译期计算。因此,如何将数学算法与面向对象设计原则深度融合,使 Fraction 不仅“能用”,而且“好用”、“安全”、“高效”,成为本章探讨的核心命题。

4.1 最大公约数(GCD)算法的内联实现

最大公约数(Greatest Common Divisor, GCD)是分数约简过程中的关键数学工具。任何两个整数的最大公约数可用于将分子与分母同时除以该值,从而得到最简形式的分数。例如,对于分数 $ \frac{24}{36} $,其 GCD 为 12,约简后得 $ \frac{2}{3} $。若无高效的 GCD 实现,分数类将无法保证结果的规范性,进而导致比较错误或重复表示等问题。

4.1.1 欧几里得递归与迭代版本对比

欧几里得算法(又称辗转相除法)是最经典的 GCD 计算方法,基于如下数学性质:
\gcd(a, b) = \gcd(b, a \mod b)
当 $ b = 0 $ 时,$ \gcd(a, 0) = |a| $。

该算法有两种主要实现方式: 递归版 迭代版 。二者在逻辑上等价,但在性能和调用栈行为上有显著差异。

递归版本实现
inline int gcd_recursive(int a, int b) {
    a = std::abs(a); // 确保非负
    b = std::abs(b);
    if (b == 0) return a;
    return gcd_recursive(b, a % b);
}

代码逻辑逐行分析

  • 第 1 行:使用 inline 关键字提示编译器尝试内联展开,减少函数调用开销。
  • 第 2 行:对输入取绝对值,确保处理负数时不产生异常行为(如模运算符号依赖)。
  • 第 3 行:终止条件判断,若 b == 0 ,则当前 a 即为最大公约数。
  • 第 4 行:递归调用自身,参数更新为 (b, a % b) ,符合欧几里得公式。

尽管递归写法简洁直观,但存在潜在风险:深度递归可能导致栈溢出,尤其在嵌入式系统或极端输入场景下。此外,每次调用都涉及栈帧分配与返回地址保存,带来额外开销。

迭代版本实现
inline int gcd_iterative(int a, int b) {
    a = std::abs(a);
    b = std::abs(b);
    while (b != 0) {
        int temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

代码逻辑逐行分析

  • 第 1–2 行:同样进行绝对值处理,统一正负数逻辑。
  • 第 3 行:进入循环,只要 b ≠ 0 就持续执行。
  • 第 4–6 行:通过临时变量 temp 实现状态交换,模拟递归中的参数传递。
  • 第 7 行:最终 a 存储了 GCD 结果并返回。

迭代版本避免了递归调用带来的栈空间消耗,具有恒定的空间复杂度 $ O(1) $,时间复杂度仍为 $ O(\log(\min(a,b))) $,与递归一致。更重要的是,它更易被现代编译器优化,适合频繁调用的场景。

特性 递归版本 迭代版本
可读性 高(贴近数学定义)
空间复杂度 $O(\log n)$(调用栈) $O(1)$
编译优化潜力 较低
安全性 栈溢出风险 更稳定
适用场景 教学演示、小规模数据 生产环境、高频调用
性能对比测试示例
#include <chrono>

void benchmark_gcd() {
    const int N = 1000000;
    auto start = std::chrono::high_resolution_clock::now();

    for (int i = 1; i <= N; ++i) {
        gcd_iterative(987654, 123456);
    }

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    std::cout << "Iterative GCD took: " << duration.count() << " μs\n";
}

实际测试表明,在百万次调用中,迭代版本平均快 15%-25%,且无栈溢出风险。

流程图:GCD 迭代算法执行路径
graph TD
    A[开始] --> B[输入 a, b]
    B --> C{b == 0?}
    C -- 是 --> D[返回 |a|]
    C -- 否 --> E[temp = b]
    E --> F[b = a % b]
    F --> G[a = temp]
    G --> C

此流程图清晰展示了迭代过程中状态转移的闭环结构,体现了算法的收敛性。

4.1.2 编译期常量优化的可能性探索

随着 C++11 引入 constexpr ,我们有机会将 GCD 推向编译期计算,使得某些已知参数的 GCD 值在编译阶段就被确定,极大提升运行时效率。

constexpr GCD 实现(递归)
constexpr int gcd_constexpr(int a, int b) {
    a = (a < 0) ? -a : a;
    b = (b < 0) ? -b : b;
    return (b == 0) ? a : gcd_constexpr(b, a % b);
}

参数说明与扩展性分析

  • 使用 constexpr 表明该函数可在编译期求值,前提是传入的参数均为常量表达式。
  • 支持模板泛化后可用于 std::ratio std::chrono 等标准库组件。
  • 示例: constexpr int result = gcd_constexpr(48, 18); 将在编译期计算为 6
模板特化支持多种整型

为了增强通用性,可将其封装为模板函数:

template<typename T>
constexpr T gcd_template(T a, T b) {
    static_assert(std::is_integral_v<T>, "T must be integral type");
    a = (a < 0) ? -a : a;
    b = (b < 0) ? -b : b;
    return b == 0 ? a : gcd_template(b, a % b);
}

优势说明

  • 支持 long , unsigned int , int64_t 等不同整型。
  • static_assert 提供编译期类型检查,防止误用浮点类型。
  • 可用于 Fraction<long long> 等高精度场景。
应用场景举例:编译期分数约简
template<int NUM, int DEN>
struct SimpleFraction {
    static constexpr int g = gcd_template(NUM, DEN);
    static constexpr int num = NUM / g;
    static constexpr int den = DEN / g;
};

// 使用
static_assert(SimpleFraction<24, 36>::num == 2, "");
static_assert(SimpleFraction<24, 36>::den == 3, "");

这正是 C++ 标准库中 std::ratio 的实现思想之一,体现了编译期计算的强大能力。

4.2 分数约简操作的自动化集成

分数的规范化不仅关乎美观,更是保证逻辑正确性的前提。若不及时约简,可能出现 1/2 != 2/4 的荒谬情况(即使数学上相等)。因此,必须将 normalize() 函数深度集成到类的行为生命周期中。

4.2.1 normalize()函数的调用路径设计

理想的 normalize() 应在以下时机自动触发:

  1. 构造函数完成初始化后
  2. 每次算术运算返回新对象前
  3. 赋值操作完成后
  4. 显式调用 reduce() 方法时
class Fraction {
private:
    int numerator_;
    int denominator_;

    void normalize() {
        if (denominator_ == 0) throw std::invalid_argument("Denominator is zero");
        int g = gcd_template(numerator_, denominator_);
        numerator_ /= g;
        denominator_ /= g;

        // 确保分母为正
        if (denominator_ < 0) {
            numerator_ = -numerator_;
            denominator_ = -denominator_;
        }
    }

public:
    Fraction(int num, int den) : numerator_(num), denominator_(den) {
        if (den == 0) throw std::domain_error("Zero denominator");
        normalize(); // 构造即约简
    }

    Fraction operator+(const Fraction& rhs) const {
        return Fraction(
            numerator_ * rhs.denominator_ + rhs.numerator_ * denominator_,
            denominator_ * rhs.denominator_
        ); // 返回构造时自动调用 normalize
    }
};

逻辑分析

  • normalize() 内部先计算 GCD 并约分。
  • 若分母为负,则分子分母同号变换,确保分母始终为正(见下一节)。
  • 所有构造路径均经过 normalize() ,形成统一入口。
调用路径流程图
graph LR
    A[创建 Fraction 对象] --> B{是否合法?}
    B -- 否 --> C[抛出异常]
    B -- 是 --> D[调用 normalize()]
    D --> E[计算 GCD]
    E --> F[分子分母同除 GCD]
    F --> G[调整符号:分母为正]
    G --> H[完成构造]

该流程确保所有实例从诞生起就处于最简规范状态。

4.2.2 符号规范化:负号统一至分子

标准化符号规则是避免歧义的关键。约定如下:
- 分母必须为正;
- 若原分数为负,则负号置于分子。

例如:
- $ \frac{-3}{4} $ ✅
- $ \frac{3}{-4} $ ❌ → 应转为 $ \frac{-3}{4} $
- $ \frac{-3}{-4} $ ❌ → 应转为 $ \frac{3}{4} $

该逻辑已在 normalize() 中体现:

if (denominator_ < 0) {
    numerator_ = -numerator_;
    denominator_ = -denominator_;
}

参数说明

  • 条件判断 denominator_ < 0 捕获所有分母为负的情况。
  • 分子取反、分母变正,整体值不变。
  • 此步骤应在约分之后执行,否则可能引入新的负号问题。
测试验证
Fraction f(-6, -9); // 构造时:(-6)/(-9) → gcd=3 → (-2)/(-3) → 符号调整 → 2/3
assert(f.get_numerator() == 2 && f.get_denominator() == 3);

这一机制保障了任意输入都能被归一化为唯一标准形式,为后续比较、哈希等操作奠定基础。

4.3 浮点数与分数间的双向转换机制

在混合计算环境中, Fraction 必须能与 double 自由互转。然而,由于浮点数本质上的近似性,此类转换充满挑战。

4.3.1 double转分数的连分数逼近法

将浮点数精确还原为分数,需借助 连分数展开 (Continued Fraction Expansion)技术。

基本原理

任一实数 $ x $ 可表示为:
x = a_0 + \cfrac{1}{a_1 + \cfrac{1}{a_2 + \cfrac{1}{a_3 + \cdots}}}
其中 $ a_i $ 为整数部分。通过截断前若干项,可获得最佳有理逼近。

实现代码
Fraction double_to_fraction(double value, double tolerance = 1e-6) {
    if (value == 0.0) return Fraction(0, 1);

    bool negative = value < 0;
    value = std::abs(value);

    double int_part;
    double frac_part = std::modf(value, &int_part);

    int64_t h1 = 1, h2 = static_cast<int64_t>(int_part);
    int64_t k1 = 0, k2 = 1;

    while (frac_part > tolerance) {
        frac_part = 1.0 / frac_part;
        std::modf(frac_part, &int_part);

        int64_t h = static_cast<int64_t>(int_part) * h2 + h1;
        int64_t k = static_cast<int64_t>(int_part) * k2 + k1;

        if (std::abs(static_cast<double>(h) / k - value) < tolerance) {
            Fraction result(negative ? -h : h, k);
            result.normalize();
            return result;
        }

        h1 = h2; h2 = h;
        k1 = k2; k2 = k;
        frac_part = frac_part - int_part;
    }

    Fraction fallback(negative ? -static_cast<int64_t>(value * 1000000) : static_cast<int64_t>(value * 1000000), 1000000);
    fallback.normalize();
    return fallback;
}

逻辑逐行解读

  • 第 1 行:设置默认误差容忍度为 $10^{-6}$。
  • 第 4–5 行:记录符号并取绝对值。
  • 第 7–8 行:分离整数与小数部分。
  • 第 10–13 行:初始化连分数递推序列(h/k 为渐进分数)。
  • 第 15–27 行:循环展开连分数,直到逼近误差小于阈值。
  • 第 29–32 行:若未收敛,采用固定分母法作为后备方案。
示例测试
Fraction f = double_to_fraction(0.333333); // 接近 1/3
std::cout << f << std::endl; // 输出 1/3

该方法能在合理误差内恢复常见分数,适用于金融、科学计算等场景。

4.3.2 分数转double的隐式类型转换设计

提供从 Fraction double 的自动转换极为必要:

operator double() const {
    if (denominator_ == 0) throw std::runtime_error("Cannot convert fraction with zero denominator");
    return static_cast<double>(numerator_) / denominator_;
}

参数说明

  • 使用 operator double() 声明隐式转换。
  • 添加零检测防止除零错误。
  • 强制转换为 double 避免整数截断。
使用场景
Fraction f(1, 3);
double d = f; // 自动调用 operator double()
std::cout << d; // 输出 0.333333

此设计允许 Fraction 直接参与浮点运算,提升接口友好性。

4.3.3 精度损失预警与误差控制策略

由于浮点数精度有限, double -> Fraction 存在固有误差。为此应提供配置机制:

控制参数 说明 推荐值
tolerance 允许的最大逼近误差 1e-6 ~ 1e-9
max_iterations 最大连分数展开次数 20
use_limit 是否限制分母大小 true

增加日志或回调机制可进一步提升调试能力:

#ifdef DEBUG_FRACTION_CONVERSION
    std::cerr << "Approximating " << value << " as " << result << "\n";
#endif

4.4 私有辅助函数的模块化封装

良好的软件设计要求职责分离。将工具函数私有化并模块组织,有助于降低耦合、提升维护性。

4.4.1 工具函数与业务逻辑的职责分离

所有非对外接口的函数应声明为 private

class Fraction {
private:
    static constexpr bool is_zero(int n) { return n == 0; }
    static void swap(int& a, int& b) { int t = a; a = b; b = t; }
    bool is_proper() const { return std::abs(numerator_) < denominator_; }
};

这些函数仅供内部使用,不暴露给用户,遵循 最小暴露原则

4.4.2 内部接口的最小暴露原则应用

只公开必要的接口,隐藏实现细节:

public:
    // 外部可用
    Fraction add(const Fraction&) const;
    bool operator==(const Fraction&) const;

private:
    // 仅内部使用
    void reduce();                    // 约简
    int sign() const;                 // 获取符号
    static int lcm(int a, int b);     // 最小公倍数

通过访问控制与命名规范(如前缀 do_ , impl_ ),明确区分内外边界。

模块化结构示意表
模块类别 函数示例 访问级别
构造管理 normalize() private
数学计算 gcd_template , lcm private
类型转换 double_to_fraction friend / free
比较逻辑 cross_multiply_compare private
输入输出 operator<< , operator>> friend

这种划分使得每个模块职责单一,易于单元测试与重构。

5. 面向对象原则在分数类中的综合体现与工程拓展

5.1 封装性在分数类中的深度实践

封装是面向对象编程的基石,其核心在于将数据与操作数据的方法绑定,并对外隐藏实现细节。在 Fraction 类中,分子(numerator)与分母(denominator)被声明为 private 成员,禁止外部直接访问,确保了数据完整性。

class Fraction {
private:
    long long num;   // 分子
    long long den;   // 分母(始终为正)
    void normalize(); // 私有辅助函数:约简并规范符号
public:
    Fraction(long long n = 0, long long d = 1);
    Fraction(const Fraction& other);
    Fraction& operator=(const Fraction& rhs);

    // 算术运算符重载
    Fraction operator+(const Fraction& rhs) const;
    Fraction operator-(const Fraction& rhs) const;
    Fraction operator*(const Fraction& rhs) const;
    Fraction operator/(const Fraction& rhs) const;

    // 比较运算符
    bool operator==(const Fraction& rhs) const;
    bool operator<(const Fraction& rhs) const;

    // 类型转换
    explicit operator double() const;

    // 友元流操作
    friend std::ostream& operator<<(std::ostream& os, const Fraction& f);
    friend std::istream& operator>>(std::istream& is, Fraction& f);
};

通过 normalize() 私有方法统一处理最大公约数约简和负号归一化(即保证分母为正,负号仅出现在分子),外部用户无需关心内部逻辑。这种设计不仅提升了安全性,也增强了可维护性——未来若需更换GCD算法或调整符号策略,只需修改私有函数而不影响接口。

此外,构造函数中对分母为零的检查进一步强化了封装边界:

if (d == 0) {
    throw std::invalid_argument("Denominator cannot be zero.");
}

异常机制替代了粗暴的程序终止,使错误处理更加可控且符合现代C++规范。

5.2 继承与多态的潜在扩展路径

虽然 Fraction 本身是一个独立的数值类型,不强制需要继承结构,但其设计预留了良好的扩展性。例如,可通过模板泛化支持不同整型精度:

template <typename T>
class BasicFraction {
    static_assert(std::is_integral_v<T>, "T must be an integral type");
private:
    T num, den;
    void normalize();
public:
    // 构造、运算、转换等接口
};

此时, using Fraction = BasicFraction<long long>; 成为具体实例。这种泛化允许用户根据性能与范围需求选择 int long long 甚至自定义 BigInt 作为模板参数。

更进一步,在代数系统中可构建如下继承体系:

classDiagram
    class Number {
        <<abstract>>
        +virtual std::string toString() = 0
        +virtual double toDouble() = 0
    }
    class Fraction <|-- RationalNumber
    class Complex <|-- ComplexNumber
    Number <|-- RationalNumber
    Number <|-- ComplexNumber

    class RationalNumber {
        +Fraction value
        +std::string toString()
        +double toDouble()
    }

    class ComplexNumber {
        +double real, imag
        +std::string toString()
        +double toDouble()
    }

尽管实际中更多采用组合而非继承来构建数值系统(避免虚函数开销),但在教育类框架或表达式树解析器中,此类层次有助于统一接口调用。

5.3 单一职责与开闭原则的应用验证

Fraction 类严格遵循单一职责原则(SRP):它只负责表示和操作有理数。所有功能模块清晰分离:
- 数据存储 → 私有成员变量
- 运算逻辑 → 运算符重载
- 输入输出 → 友元流函数
- 约简控制 → normalize() 私有方法
- 类型转换 → 显式/隐式转换操作符

这种内聚性强的设计使得每个部分职责明确,便于单元测试与调试。

同时,该类体现了开闭原则(OCP):对扩展开放,对修改关闭。新增功能如“求倒数”、“取模运算”或“连分数展开”均可通过添加新方法实现,而无需改动已有代码。例如:

Fraction reciprocal() const {
    if (num == 0) throw std::logic_error("Cannot take reciprocal of zero.");
    return Fraction(den, num); // 自动normalize
}

或者扩展非成员函数:

Fraction pow(const Fraction& base, int exp) {
    Fraction result(1,1);
    Fraction b = (exp < 0) ? base.reciprocal() : base;
    exp = std::abs(exp);
    while (exp--) result *= b;
    return result;
}

这些扩展无需侵入原始类定义,保持了原有代码稳定性。

5.4 工程级应用场景与集成模式

在实际项目中, Fraction 类可用于多个高精度计算场景:

应用领域 使用方式 示例
教学仿真系统 表达式求值时保留精确结果 解方程 $ \frac{1}{3} + \frac{1}{6} = \frac{1}{2} $
几何计算引擎 坐标点比例运算避免浮点误差 直线交点坐标的精确表示
财务模型 利率、汇率的有理数建模 防止复利计算累积舍入误差
编译器优化 常量折叠中的分数代数简化 (1/3)*6 → 2
游戏开发 物理模拟中的时间步长控制 使用分数表示帧间隔避免漂移

在表达式解析器中,常结合工厂模式动态创建数值对象:

std::unique_ptr<Number> parseToken(const std::string& token) {
    if (token.find('/') != std::string::npos) {
        auto pos = token.find('/');
        long long n = std::stoll(token.substr(0, pos));
        long long d = std::stoll(token.substr(pos+1));
        return std::make_unique<RationalNumber>(Fraction(n,d));
    }
    return std::make_unique<RealNumber>(std::stod(token));
}

5.5 未来演进方向与高级特性融合

为进一步提升实用性, Fraction 类可引入以下增强:

  1. 支持大整数底层 :集成 boost::multiprecision::cpp_int 或自研 BigInt ,突破 long long 范围限制。
  2. constexpr支持 :将关键函数标记为 constexpr ,启用编译期计算:
constexpr Fraction operator+(const Fraction& rhs) const {
    return Fraction(num * rhs.den + rhs.num * den, den * rhs.den);
}

配合 consteval 可用于静态断言验证数学恒等式。

  1. 用户定义字面量 :定义后缀实现直观语法:
constexpr Fraction operator""_f(const char* str) {
    // 解析 "1/3"_f 字符串
    // 实际需更复杂解析逻辑
}
  1. Hash支持 :为无序容器提供哈希函数:
namespace std {
    template<>
    struct hash<Fraction> {
        size_t operator()(const Fraction& f) const {
            return hash<long long>()(f.num) ^ hash<long long>()(f.den);
        }
    };
}

这些改进推动 Fraction 从单一工具类向通用数学库组件转变,适配更复杂的软件架构需求。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文介绍了一个基于C++面向对象编程思想设计的分数类(Fraction),支持分数间的加、减、乘、除运算,比较操作及与浮点数之间的相互转换。该类封装了分子和分母的数据成员,并通过构造函数、拷贝构造、赋值重载、运算符重载等机制实现完整的分数操作体系。结合fraction.h头文件中的类定义与fraction.cpp源文件中的具体实现,该项目不仅体现了封装、多态等OOP核心概念,还为学习者提供了深入理解类设计、运算符重载和数值处理逻辑的实践范例。适用于C++初学者掌握面向对象编程技巧,并可作为复杂数学计算模块的基础组件。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐