当前位置: 首页 > article >正文

大数运算:整数、小数的加减乘除与取余乘方(c++实现)

前言

本文源码见 【MyHeader】下的 Number 文件夹

c++ 中整型、浮点数等数据类型是由精度限制的,这就导致了使用 c++ 基本类型是无法正确进行大数运算的:
【例】

  • 代码:
    // test.cpp
    #include <iostream>
    #include <climits>
    
    using namespace std;
    
    int main()
    {
        long long num1 = LONG_LONG_MAX;
        long long num2 = LONG_LONG_MAX;
        cout << num1 << " + " << num2 << " = " 
        	 << num1 + num2 << endl;
        return 0;
    }
    
  • 输出:
    在这里插入图片描述

因此设计 Number 类来实现大数运算是本文的基本任务,本文将实现以下运算(包括整数、浮点数):

  • 加法:+
  • 减法:-
  • 乘法:*
  • 除法:/
  • 取余:%
  • 乘方:^

一、基本思路

基本四则运算加减乘除,相信通过手算大家都会,Number 类的实现思路就是 模拟手算

怎么存储大数呢?用基础类型(long long、double等),肯定不行,因为精度不够,那么可以采用数组、字符串等来存储大数的每一位数字,本文采用字符串。

class Number
{
public:
	/* ... */
private:
	std::string _val;
};

换句话说:

Number 的实现是建立在 实现字符串运算 的基础之上


二、API 设计

1. 构造函数

Number 类从定义上看,它就是一个数字,因此我们希望能用 long long、double 等类型初始化 Number:

Number num1{ 1000 };
Number num2{ 1.23 };

这对于在 c++ 允许的数据范围内的数字初始化,是有效的,但是对于更大的数是无效的,因此还需要有一个能初始化大数的构造函数,当然大数需要用字符串表示:

Number big_num{ "9999999999999999999999999" };

也就是说 Number 的构造函数如下:

class Number
{
public:
    Number(long long num)          :_val{ std::to_string(num) } { }
    Number(double num)             :_val{ std::to_string(num) } { }
    Number(const std::string& num) :_val{ num } { }
    ~Number() = default;
    
public:
	std::string str() const { return _val; }
	
private:
	std::string _val;
};

【修改】 浮点数精度问题

  1. c++ double 类型,当小数位数过多时,会出现精度问题:
if (3.1234567890123456 == 3.1234567890123457) {
	cout << "true" << endl;
}

上面的程序会输出: true

  1. 在 Number(double num) 中,使用到了 std::to_string(double x) 函数,有如下代码:
Number num{3.1415926};
cout << num.str();		

会输出:3.141593

上面两个问题涉及到浮点数精度问题,这是 c++ 内部实现 double 的问题,我们无法避免。
对于 Number 而言,显然我们希望 Number num{3.1415926} 就是 “3.1215926”,不应该是其他值,当然你可以自己设计一个算法能原样地将 double 转为 string,这显然仍然会遇到浮点数精度问题。

既然无法避免,那么就直接不要构造函数 Number(double num),也就是说,使用 Number 时,遇到浮点数必须使用 Number(const std::string& num) 进行初始化,这样就不必考虑浮点数精度问题:

class Number
{
public:
    Number(long long num)          :_val{ std::to_string(num) } { }
    Number(const std::string& num) :_val{ num } { }
    ~Number() = default;

private:
    std::string _val;
};

// 浮点数初始化 Number
Number num{ "3.1215926" };

然而这样的设计存在一个问题:

Number num{ 3.14 };		// 将调用 Number(long long num)

也就是说,想要更安全的 Number 类,Number(long long num) 也应该被删除。这样的 Number 类虽然初始化较为复杂(不管大数还是小数,每次都必须用字符串),但是换来了更为安全的转换(不必担心浮点数精度不够、超出整型数据范围等问题),这是可以接受的。

【修改版本】

class Number
{
public:
    Number()                       :_val{ "0" } { }
    Number(const std::string& num) :_val{ num } { }
    ~Number() = default;
    
private:
    std::string _val;
};

2. 重载运算符函数

c++ 中,类有一类函数是很好用的,那就是重载运算符函数,它运行我们重新定义运算符的功能。
以 加法运算为例,如果 Number 实现的加法功能是 add(const Number& other) 成员函数,那么使用加法:

Number num1, num2;
auto res = num1.add(num2);

但倘若在 Number 中重载了 + 运算符,那么使用加法:

Number num1, num2;
auto res = num1 + num2;

第二种对于类的使用者来说是比较友好的,因此 Number 重载运算符函数如下:

Number  operator+ (const Number& other);	// res = num1 + num2
Number& operator+=(const Number& other);	// num1 += num2
Number  operator- (const Number& other);
Number& operator-=(const Number& other);
Number  operator* (const Number& other);
Number& operator*=(const Number& other);
// @brief 整除
Number  operator/ (const Number& other);
// @brief 整除
Number& operator/=(const Number& other);
Number  operator% (const Number& other);
Number& operator%=(const Number& other);
Number  operator^ (Number other);
Number& operator^=(const Number& other);

Number& operator= (const std::string& number);
Number& operator= (const Number& number);

bool    operator==(const Number& other);
bool    operator!=(const Number& other);
bool    operator> (const Number& other);
bool    operator>=(const Number& other);
bool    operator< (const Number& other);
bool    operator<= (const Number& other);
  • operator/(const Number& other)
    1 / 3 = 0.333…,它是个无限循环小数,显然不可能表示完;因此对于除法需要特殊处理:保留指定的小数位数。使用这个函数显然不能指定保留小数位数的参数,此外,在 c++ 中,/ 是整除的含义,为了与 c++ 保持一致,Number 类的 / 运算符的含义也是整除,同时增加一个成员函数 div(…),此函数就是普通意义的除法,可指定保留的小数位数,它的设计在后续再详细说明。

  • Number& operator=(const std::string& number)
    赋值运算符,有了这个函数,这就能实现一下语句:

    Number num;
    num = "1";    // 使用较为方便
    

以上仅仅是它们的声明,具体实现在第三章讲解。

3. other

  • div(const Number& other, int precision, int flag)
    对于平常的除法而言,我们关心连个问题:(1) 保留小数的位数 precision;(2) 小数最后一位采用的保留方式 flag (四舍五入、截断等方式)
  • str()
    返回数字字符串
  • … …

【综上】 Number 完整定义如下:

class Number
{
public:
    using size_type = std::string::size_type;

public:
    static const int PRECISION = 6;

public:
    enum class DivFlag  /* 小数最后一个保留方式 */
    {
        round,          // 四舍五入
        truncate        // 截断
    };

public:
    Number()                       :_digit{ "0" } { }
    Number(const std::string& num) :_digit{ num } { }
    ~Number() = default;
    
public:
    Number  operator+ (const Number& other);
    Number& operator+=(const Number& other);
    Number  operator- (const Number& other);
    Number& operator-=(const Number& other);
    Number  operator* (const Number& other);
    Number& operator*=(const Number& other);
    // @brief 整除
    Number  operator/ (const Number& other);
    // @brief 整除
    Number& operator/=(const Number& other);
    Number  operator% (const Number& other);
    Number& operator%=(const Number& other);
    Number  operator^ (Number other);
    Number& operator^=(const Number& other);
    Number& operator= (const std::string& number);
    Number& operator= (const Number& number);
    bool    operator==(const Number& other);
    bool    operator!=(const Number& other);
    bool    operator> (const Number& other);
    bool    operator>=(const Number& other);
    bool    operator< (const Number& other);
    bool    operator<=(const Number& other);

public:
    /**
     * @brief 小数除法
     * @param other 除数
     * @param n 保留小数位数
     * @param flag 小数最后一个保留的方式
     */
    Number      div(const Number& other, int n = PRECISION, DivFlag flag = DivFlag::round);
    std::string str() const;
    
private:
    std::string _digit;     // 删除小数点后的数字 (数字部分)
    int         _point;     // 小数位数,则小数点位于 _digit[len - _point]
    bool        _signed;    // true: negative
};

看完定义,你会发现它的成员变量从原来的单个 _val 变成了三个,这样做的目的是什么呢?这是为了方便实现算术运算,因为 负数运算、小数运算 都能转为 正整数运算。用加法举个例子(假设已经实现了所有的正整数运算):

  • 负数运算:“- 2 + 1” 可以转为 “2 - 1” 再取相反数即可
  • 小数运算:“3 + 2.1” 可以转为 “30 + 21 = 51” 然后再结果上添加一位小数,即 “5.1”

也就是说,其他运算则是在 正整数运算 的基础上,进行 _point、_signed 的转换。

这里增加一个我自定义的术语:数字部分(_digit,表示删除小数点后剩余的数字
【例】

  • 3.14: _digit = 314,_point = 2,_signed = false
  • -3.0: _digit = 300,_point = 1,_signed = true

当然,由于成员变量的改变,那么构造函数的实现也要改变,即给定数字字符串,它能计算出 _digit、_point、_signed,这不在本文讨论范围内,读者可自行实现。

下文中一些自定义函数的实现也不给出,仅给出它的功能介绍


三、具体实现

在上面,我说过 “负数运算、小数运算 都能转为 正整数运算”,因此这里的主要任务是 实现正整数运算


1. 加法

1.1 正整数加法

基本思路:用代码模拟手算正整数加法
在这里插入图片描述
给定两个字符串,计算它们之间的加法,只需要将两字符串末尾对齐,然后每一位对应相加,同时记录进位即可:

std::string add_int(std::string num1, std::string num2)
{
    std::string res;
    // 记录当前位相加的结果: 
    // 1. 其个位 (cur % 10) 为 res 当前位的结果
	// 2. 其十位 (cur / 10) 为 当前位相加的进位
    int cur = 0;	
    int p1 = num1.size() - 1, p2 = num2.size() - 1;		// 末尾对齐
    while (p1 >= 0 || p2 >= 0 || cur > 0) {
        if(p1 >= 0) cur += num1[p1--] - '0';
        if(p2 >= 0) cur += num2[p2--] - '0';
        res = std::to_string(cur % 10) + res;	// 倒序
        cur = cur / 10;
    }
    return res;
}

在这里用到一个小技巧:数字字符 转 对应数字

  • ‘9’ - ‘0’ == 9
  • … …
  • ‘0’ - ‘0’ == 0

1.2 正小数加法

对于小数加法,手算的话需要将小数点对齐。前面说过:小数加法可转为正整数加法,在计算小数点位数即可:
在这里插入图片描述
观察上图:对齐小数点后,去掉小数点,对于小数位数较少的数字需要在末尾补 0,那么剩下的就是正整数加法,其结果的小数位数就是两个加数的最大小数位数。

/**
 * @brief 浮点数加法
 * @param num1 第一个浮点数的数据部分
 * @param p1   第一个浮点数的小数点位数
 * @param num2 第二个浮点数的数据部分
 * @param p2   第二个浮点数的小数点位数
 * @return 返回正常的小数 (包含小数点)
 */
inline std::string add_double(std::string num1, int p1, std::string num2, int p2)
{
    int p = std::max(p1, p2);
    int dif = abs(p1 - p2);
    if (dif != 0) {
        if (p1 > p2) num2 += std::string(dif, '0');
        else num1 += std::string(dif, '0');
    }
    std::string res = add_int(num1, num2);
    // to_double: 自定义函数,根据指定数据部分以及小数位数,返回小数的字符串形式
    // 例:to_double("100", 2) == "1.00" 
    res = to_double(res, p);	
    return res;
}

1.3 operator+

前面实现了正整数、正浮点数的加法,那么剩余的就是有负数参与的加法,这里分为四种情况:

  • 正 + 正:直接调用 add_int 即可
  • 正 + 负:相当于 “正 - 正”,调用 sub_int 即可 (后续实现 sub_int)
  • 负 + 正:相当于 “正 + 负”,同上述
  • 负 + 负:相当于 “- (正 + 正)”
Number operator+(const Number& other)
{
    if (!_signed) {
        if (!other._signed) return Number{add_double(_digit, _point, other._digit, other._point)};
        else return Number{sub_double(_digit, _point, other._digit, other._point)};
    }
    else {
        if (!other._signed) return Number{sub_double(other._digit, other._point, _digit, _point)};
        else return Number{"-" + add_double(_digit, _point, other._digit, other._point)};
    }
    return Number{};
}

2. 减法

2.1 正整数减法

减法与加法类似,需要将末尾对齐,每一位对应相减,同时记录借位即可。不过减法多了一点:需要比较两个数的大小。
在这里插入图片描述

std::string sub_int(std::string num1, std::string num2)
{
    bool is_signed = false;		// 记录结果的正负性
    // cmp_int:比较 num1 与 num2 的大小
    // 1. return  1:  num1 > num2
    // 2. return  0:  num1 = num2
    // 3. return -1:  num1 < num2
    if (cmp_int(num1, num2) == -1) {
        std::swap(num1, num2);
        is_signed = true;
    }
    std::string res;
    // 记录当前位相减的结果 (结果一定是一位数)
    // 1. cur < 0: 说明需要借位,结果为 cur + 10
    // 2. cur > 0: 不需要结尾,结果就是 cur
    int cur = 0;	
    bool is_carry = false;      // 是否借位
    int len1 = num1.size() - 1, len2 = num2.size() - 1;
    while (len1 >= 0 || len2 >= 0 || cur > 0) {
        if(len1 >= 0) {
            cur += num1[len1--] - '0' - is_carry;	// 第一个用 +,同时减去借位
            is_carry = false;
        }
        if(len2 >= 0) cur -= num2[len2--] - '0';	// 第二个用 -
        if (cur < 0) {
            cur += 10;
            is_carry = true;
        }
        res = std::to_string(cur) + res;	// 逆序
        cur = 0;
    }
    if (is_signed) res = "-" + res;
    return res;    
}

在上述代码中,is_carry 用于记录是否借位,将它转为 int 的话,要么是 0 要么是 1,你会发现这恰恰是减法借位的范围(向高位要么借1要么借0)

2.2 正小数减法

浮点数减法与浮点数加法思路一致:

std::string sub_double(std::string num1, int p1, std::string num2, int p2)
{
    int p = std::max(p1, p2);
    int dif = abs(p1 - p2);
    if (dif != 0) {
        if (p1 > p2) num2 += std::string(dif, '0');
        else num1 += std::string(dif, '0');
    }
    std::string res = sub_int(num1, num2);
    res = to_double(res, p);
    return res;
}

2.3 operator-

同加法,不再赘述

Number operator-(const Number& other)
{
    if (!_signed) {
        if (!other._signed) return Number{sub_double(_digit, _point, other._digit, other._point)};
        else return Number{add_double(_digit, _point, other._digit, other._point)};
    }
    else {
        if (!other._signed) return Number{"-" + add_double(_digit, _point, other._digit, other._point)};
        else return Number{sub_double(other._digit, other._point, _digit, _point)};
    }
    return Number{};
}

3. 乘法

3.1 正整数乘法

手算乘法,如下图左边是我们比较熟悉的方式,实际上在简化每一步的操作,那么就对应右边的式子:
在这里插入图片描述
对于 m 位数字与 n 位数字相乘,其结果最多有 m + n 位,对于上述例子的加法部分,当然可以用前面实现的字符串加法,但采用 int 数组来存储数字在再计算加法来模拟上述的过程效率更高些:

std::string mul_int(std::string num1, std::string num2)
{
    int m = num1.size(), n = num2.size();
    std::vector<int> res(m + n);
    for (int i = m - 1; i >= 0; i--) {
        for (int j = n - 1; j >= 0; j--) {
            int mul = (num1[i] - '0') * (num2[j] - '0');
            int p1 = i + j, p2 = i + j + 1;
            int sum = mul + res[p2];
            res[p2] = sum % 10;
            res[p1] += sum / 10;
        }
    }
    int i = 0;
    while (i < res.size() && res[i] == 0) i++;
    std::string str;
    for (; i < res.size(); i++) str.push_back(res[i] + '0');
    return str.empty() ? "0" : str;
}

此函数有优化的算法(Karatsuba、FFT),读者可自行查略资料

3.2 正小数乘法

对于两数相乘,其结果的小数位数为两乘数的小数位数之和:

std::string mul_double(std::string num1, int p1, std::string num2, int p2)
{
    std::string res = mul_int(num1, num2);
    res = to_double(res, p1 + p2);
    return res;
}

3.3 operator*

对于正负数乘法:符号相同为正,符号不同为负

Number operator*(const Number& other)
{
    if (!_signed) {
        if (!other._signed) return Number{mul_double(_digit, _point, other._digit, other._point)};
        else return Number{"-" + mul_double(_digit, _point, other._digit, other._point)};
    }
    else {
        if (!other._signed) return Number{"-" + mul_double(_digit, _point, other._digit, other._point)};
        else return Number{mul_double(_digit, _point, other._digit, other._point)};
    }
    return Number{};
}

4. 除法

4.1 正整数除法

对于除法来说,前面说过其参数应该有 保留小数位数 precision、最后一位小数保留方式 flag,因此函数声明应如下:

/**
 * @brief 计算 num1 / num2,保留 n 位小数 (如果能整除,无论 n 为几,都不保留小数)
 */
std::string div_int(std::string num1, std::string num2, int precision = PRECISION, DivFlag flag = DivFlag::round)

上面说明能整除的,div_int 的后两个参数将无效:

div_int(“4”, “2”) 应等于 “2” 而不是 “2.000000”

对于除法而言,其商分为两个部分:整数部分与小数部分,也就是说模拟手算除法分为两部分:

  • 计算整数部分
    对于 num1 / num2,整数部分恰恰是 num1 整除 num2,因此需要一个字符串整除函数 divide(…);同时,小数部分需要余数,故此函数还需返回余数。利用除法转减法的思想实现整除函数:

    /**
     * @brief 返回余数的除法 
     * @param num1 被除数,接收相除后的余数
     * @return 商
     */
    std::string divide(std::string& num1, std::string num2)
    {
        if (num2 == "0") throw NumberExcep{"The divisor cannot be 0"};
        std::string res = "0";
        while (cmp_int(num1, num2) != -1) {
            num1 = sub_int(num1, num2);
            add_int(res, "1");
        } 
        return res;
    }   
    

    这是最简单但是时间开销很大的版本,此函数有很大的优化空间,读者可自行查略资料

  • 计算小数部分
    无非就是用余数继续除以除数,不够除的乘以10继续除,直到余数为0或者小数位数达到指定位数

/**
 * @brief 计算 num1 / num2,保留 n 位小数 (如果能整除,无论 n 为几,都不保留小数)
 */
std::string div_int(std::string num1, std::string num2, int precision = PRECISION, DivFlag flag = DivFlag::round)
{
    std::string res;
    res = divide(num1, num2);
    if (num1 == "0") return res;
    // 小数部分
    for (int i = 0; i < precision; ++i) {
        if (num1 != "0") num1 += "0";
        res += divide(num1, num2);
    }
    // 最后一位保留方式
    switch (flag) {
        case DivFlag::round:
        {
            if (num1 != "0") num1 += "0";
            std::string last = divide(num1, num2);
            if (cmp_int(last, "5") == -1) break;
            res = add_double(res, precision, "1", precision);
            return res;
        }
        case DivFlag::truncate: break;
        default: THROW_SWITCH_DEFAULT_OUTDATED;
    }
    return to_double(res, precision);
}

4.2 正小数除法

对于小数,直接转为整数即可:
在这里插入图片描述

std::string div_double(std::string num1, int p1, std::string num2, int p2, int precision = PRECISION, DivFlag flag = DivFlag::round)
{
    int p = std::max(p1, p2);
    int dif = abs(p1 - p2);
    if (dif != 0) {
        if (p1 > p2) num2 += std::string(dif, '0');
        else num1 += std::string(dif, '0');
    }
    return div_int(num1, num2, precision, flag);
}

4.3 operator/

前面说过,这是整除,因此代码如下:

// @brief 整除
Number operator/(const Number& other)
{
    if (!_signed) {
        if (!other._signed) return Number{div_double(_digit, _point, other._digit, other._point, 0, DivFlag::truncate)};
        else return Number{"-" + div_double(_digit, _point, other._digit, other._point, 0, DivFlag::truncate)};
    }
    else {
        if (!other._signed) return Number{"-" + div_double(_digit, _point, other._digit, other._point, 0, DivFlag::truncate)};
        else return Number{div_double(_digit, _point, other._digit, other._point, 0, DivFlag::truncate)};
    }
    return Number{};
}

5. 取余

5.1 正数取余

取余,即计算余数。有如下式子:

num1 / num2 = s … m 即 num1 % num2 = m
【分析】现在相当于已知 num1 与 num2,求 m

  • 根据前式可知:num2 * s + m = num1,因此 m = num1 - num2 * s,现在仅 s 未知,s 是商,即 num1 整除 num2,因此 m 可求
std::string mod(std::string num1, int p1, std::string num2, int p2)
{
    std::string dif = div_double(num1, p1, num2, p2, 0, DivFlag::truncate);     // 整除
    std::string mul = mul_double(num2, p2, dif, 0);
    // cal_digit_point(n, p): 计算 n 的数据部分(结果赋值给 n) 与 小数位数(结果赋值给 p)
    // 例:n = "1.00" ⇒ n = "100", p == 2
    cal_digit_point(mul, p2);
    return sub_double(num1, p1, mul, p2);
}

5.2 operator%

当加入负数时,结果不变,只是正负性改变,满足以下规律:
对于 num1 % num2 = m,m 的正负性与 num1 一致

读者可自行推导

Number operator%(const Number& other)
{
    if (_signed) return Number{"-" + mod(_digit, _point, other._digit, other._point)};
    else return Number{mod(_digit, _point, other._digit, other._point)};
}

6. 乘方

对于乘方运算,为了方便要求指数必须为整数,那么使用快速幂算法可快速求出结果

Number  operator^ (Number other)
{
    bool is_signed = other._signed;   
    if (other._point != 0) throw NumberExcep{"Decimal exponents are not supported"};
    if (other == "0") return "1";
    if (*this == Number{ "0" }) return "0";
    Number res = "1";
    while (cmp_int(other._digit, "0") == 1) {
        if (other % "2" == "1") res *= (*this);
        other /= "2";
        *this *= (*this);
    }
    return (is_signed) ? Number{"1"}.div(res) : res;
}

http://www.kler.cn/a/523389.html

相关文章:

  • Solon Cloud Gateway 开发:Route 的过滤器与定制
  • 挂载mount
  • Ansible自动化运维实战--通过role远程部署nginx并配置(8/8)
  • jemalloc 5.3.0的tsd模块的源码分析
  • GD32的GD库开发
  • 相互作用感知的蛋白-小分子对接模型 - Interformer 评测
  • 我们需要有哪些知识体系,知识体系里面要有什么哪些内容?
  • 面试被问的一些问题汇总(持续更新)
  • Python帝王學集成-母稿
  • 【开源免费】基于Vue和SpringBoot的在线文档管理系统(附论文)
  • AIGC常见基础概念
  • DeepSeek R1学习
  • 27.日常算法
  • 【Leetcode 热题 100】152. 乘积最大子数组
  • 2025春晚临时直播源接口
  • Jellyfin的快速全文搜索代理JellySearch
  • iperf 测 TCP 和 UDP 网络吞吐量
  • 2025年数学建模美赛 A题分析(2)楼梯使用频率数学模型
  • t113 procd-init文件系统增加自己的程序文件
  • 7-Zip Mark-of-the-Web绕过漏洞复现(CVE-2025-0411)
  • 前端——js高级25.1.27
  • 20250128 大语言模型(Large Language Model, LLM)已成为自然语言处理(NLP)领域的重要突破
  • 脚本/编译安装nginx1.11.10
  • ArcGIS10.2 许可License点击始终启动无响应的解决办法及正常启动的前提
  • 使用 PyTorch 实现线性回归:从零开始的完整指南
  • Ubuntu 18.04安装Emacs 26.2问题解决