80、实战:高精度计算(任意精度浮点数+牛顿迭代)

说实话,浮点数精度问题,是每个C++程序员迟早要面对的坎。

我记得刚入行那会儿,写一个金融计算模块,double类型算利息,结果对不上账。排查了半天,发现是0.1 + 0.2不等于0.3——经典问题。老板差点让我赔钱。从那以后,我对浮点数精度就特别敏感。

今天咱们要聊的,就是怎么在C++里实现任意精度浮点数,并且用牛顿迭代法做高精度开方运算。说白了,就是自己造一个能算几百位小数的轮子。

为什么需要任意精度?

double类型只有53位有效数字,大约15-17位十进制精度。够用吗?大部分场景够了。但有些场景不行:

  • 金融计算:一分钱都不能差,double的舍入误差会累积
  • 科学计算:某些迭代算法对精度敏感,低精度会导致不收敛
  • 密码学:大数运算需要精确到每一位
  • 教学演示:想看看牛顿迭代法到底能收敛到多少位

我个人的习惯是:能用double就别自己造轮子。但当你真的需要任意精度时,就得从底层开始设计。

核心思路:用整数模拟浮点数

任意精度浮点数的实现思路其实不复杂:

  1. 用一个大整数表示有效数字(比如用vector<uint32_t>存每一位)
  2. 用一个整数指数表示小数点位置
  3. 符号位单独处理

举个例子:3.141592653589793 可以表示为 3141592653589793 × 10^(-15)

这样,加减乘除就变成了大整数的运算,再加上指数对齐。嗯,听起来简单,但实现起来坑不少。

关键设计决策:我选择用十进制而不是二进制。为什么?因为十进制对人类更友好,输出调试方便,而且避免了二进制浮点数那种0.1无法精确表示的问题。代价是性能稍差——但既然要任意精度,性能本来就不是首要目标。

代码实现:高精度浮点数类

先看一个精简版实现。我把它叫做 BigFloat

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <cassert>

class BigFloat {
private:
    std::vector<int> digits;   // 数字位,每个元素0-9
    int exponent;              // 指数,表示小数点位置
    bool sign;                 // true为正,false为负

    // 去除前导零
    void trim() {
        while (digits.size() > 1 && digits.back() == 0) {
            digits.pop_back();
        }
        if (digits.size() == 1 && digits[0] == 0) {
            sign = true;  // 正零
            exponent = 0;
        }
    }

public:
    BigFloat() : digits({0}), exponent(0), sign(true) {}

    BigFloat(const std::string& s) {
        // 解析字符串,例如 "123.456" 或 "-0.00123"
        size_t pos = 0;
        sign = true;
        if (s[pos] == '-') { sign = false; pos++; }
        if (s[pos] == '+') { pos++; }

        std::string intPart, fracPart;
        bool hasDot = false;
        for (; pos < s.size(); pos++) {
            if (s[pos] == '.') {
                hasDot = true;
                continue;
            }
            if (!hasDot) intPart += s[pos];
            else fracPart += s[pos];
        }

        // 合并所有数字
        std::string all = intPart + fracPart;
        // 去除前导零
        size_t firstNonZero = all.find_first_not_of('0');
        if (firstNonZero == std::string::npos) {
            digits = {0};
            exponent = 0;
            sign = true;
            return;
        }
        all = all.substr(firstNonZero);

        digits.clear();
        for (char c : all) {
            digits.push_back(c - '0');
        }
        std::reverse(digits.begin(), digits.end());

        // 指数 = 小数部分长度
        exponent = -static_cast<int>(fracPart.size());
        // 如果有整数部分,调整指数
        if (!intPart.empty() && intPart != "0") {
            exponent += static_cast<int>(intPart.size() - firstNonZero);
        }
        // 如果整数部分全是0,需要额外处理
        if (intPart.find_first_not_of('0') == std::string::npos) {
            // 整数部分全是0,指数就是小数部分长度(负的)
            // 但要去掉小数部分的前导零
            size_t leadingZeros = 0;
            while (leadingZeros < fracPart.size() && fracPart[leadingZeros] == '0') {
                leadingZeros++;
            }
            exponent = -static_cast<int>(fracPart.size() - leadingZeros);
        }
    }

    std::string toString() const {
        if (digits.empty() || (digits.size() == 1 && digits[0] == 0)) {
            return "0";
        }
        std::string result;
        if (!sign) result += '-';

        // 从高位到低位
        std::string all;
        for (int i = static_cast<int>(digits.size()) - 1; i >= 0; i--) {
            all += static_cast<char>('0' + digits[i]);
        }

        int pointPos = exponent + static_cast<int>(digits.size());
        if (pointPos <= 0) {
            // 纯小数,如 0.00123
            result += "0.";
            for (int i = 0; i < -pointPos; i++) result += '0';
            result += all;
        } else if (pointPos >= static_cast<int>(all.size())) {
            // 纯整数,如 12300
            result += all;
            for (int i = 0; i < pointPos - static_cast<int>(all.size()); i++) result += '0';
        } else {
            // 混合,如 123.456
            result += all.substr(0, pointPos);
            result += '.';
            result += all.substr(pointPos);
        }
        return result;
    }

    // 加法(简化版,仅用于演示)
    BigFloat operator+(const BigFloat& other) const {
        // 实际实现需要对齐指数,逐位相加,处理进位
        // 这里省略完整实现,仅展示接口
        return BigFloat("0");
    }

    // 乘法(简化版)
    BigFloat operator*(const BigFloat& other) const {
        // 实际实现:大整数乘法 + 指数相加
        return BigFloat("0");
    }
};

我的经验:写这个类的时候,最容易出bug的地方是指数计算。特别是处理前导零和后置零时,指数很容易算错。我建议你写一个完整的单元测试,覆盖"0.00123"、"123.456"、"100"、"0"这些边界情况。

牛顿迭代法求平方根

有了BigFloat,我们就可以实现高精度开方了。牛顿迭代法的公式很简单:

x_{n+1} = (x_n + a / x_n) / 2

其中a是要求平方根的数,x_n是第n次迭代的近似值。

为什么用牛顿迭代?因为它收敛速度极快——每次迭代有效位数翻倍。你想想看,初始值有1位精度,迭代一次变2位,再迭代变4位、8位、16位……10次迭代就能到1024位精度。

BigFloat sqrtNewton(const BigFloat& a, int precision) {
    // 处理负数
    if (!a.sign) {
        throw std::runtime_error("不能对负数开平方");
    }

    // 初始猜测:用double算一个近似值
    double initVal = std::stod(a.toString());
    BigFloat x(std::to_string(initVal));

    // 迭代直到达到精度
    for (int iter = 0; iter < 20; iter++) {
        // x = (x + a / x) / 2
        BigFloat quotient = a / x;  // 需要实现除法
        BigFloat sum = x + quotient;
        x = sum / BigFloat("2");

        // 检查收敛情况(简化版)
        // 实际应该比较两次迭代的差值
        std::cout << "迭代 " << iter+1 << ": " << x.toString() << std::endl;
    }

    return x;
}

注意:上面的代码中,除法 a / xsum / 2 需要BigFloat实现除法运算。除法是BigFloat里最复杂的运算,通常用长除法或者牛顿迭代法求倒数来实现。我建议你先实现乘法,然后用牛顿迭代法求倒数,再乘以被除数——这样代码复用性更好。

核心知识体系

我把本章的知识结构整理成了一张图,方便你理解各个模块之间的关系:

高精度计算核心知识体系 BigFloat 类 大整数存储 指数管理 符号处理 加法 / 减法 乘法 除法(长除法) 牛顿迭代法求平方根 应用:金融计算 / 科学计算 / 密码学 / 教学演示

避坑指南

我曾经在实现BigFloat的除法时踩过一个坑,花了整整两天才找到bug。这里分享给你:

  • 指数溢出:当指数非常大或非常小时,int可能不够用。我建议用int64_t或者自己实现一个大整数指数。
  • 精度截断:每次运算后都要做精度截断,否则数字会无限膨胀。我习惯在每次运算后保留precision + 10位,最后再统一截断。
  • 除零检查:除法前一定要检查除数是否为零。BigFloat的零判断不能直接用== 0,要写一个isZero()方法。
  • 牛顿迭代的初始值:初始值越接近真实值,收敛越快。我通常先用double算一个近似值,再转成BigFloat开始迭代。

性能优化小技巧:如果你只需要几百位精度,可以用std::vector<uint32_t>存储,每个元素存9位十进制数(因为2^32 ≈ 4.3e9)。这样乘法可以用FFT加速,效率能提升一个数量级。

总结

高精度计算的核心就三件事:大整数存储指数管理牛顿迭代

大整数存储是地基,指数管理是框架,牛顿迭代是应用。三者缺一不可。

我建议你从最简单的十进制实现开始,跑通牛顿迭代求平方根,感受一下精度从double的15位扩展到100位、1000位的过程。那种掌控感,说实话,挺爽的。

等你把十进制版本跑通了,再考虑优化性能、改成二进制、加FFT乘法——那时候你已经是个高精度计算的老手了。


公众号:蓝海资料掘金营,微信deep3321