news 2026/9/10 1:27:42

C++高精度减法:从原理到实现,彻底解决大整数相减

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++高精度减法:从原理到实现,彻底解决大整数相减

1. 高精度减法到底在解决什么问题

1.1 当 int 和 long long 都不够用的时候

我先说一个很典型的使用场景,也是我最早接触高精度减法的契机。当时在写一个简单的表达式计算器,本来以为整数运算用long long就万事大吉了,结果用户输入了一个 20 位的数字做减法,程序直接溢出,输出变成了负数。那一刻才意识到,内置数据类型的天花板是真实存在的。

很多初学者会问:什么情况下能用到高精度减法?其实不只是在算法竞赛、OJ 做题,很多业务场景也会碰到。比如身份证号、银行卡号的某段数值处理,比如大文件分片后的偏移量计算,再比如区块链场景里的整数哈希比对,都会出现超过 64 位整数范围的数字。在 C++ 里,unsigned long long最大也只能存到18446744073709551615(约 1.8×10¹⁹),一旦数值超过这个范围,内置类型就无能为力了。

高精度减法是高精度运算系列里最基础、也最容易被忽视的一块。很多人学高精度直接从加法跳到乘法,减法只是草草看两眼,结果真到用的时候,在借位、负数、前导零这些细节上疯狂翻车。这篇就专门把减法拆开揉碎,从原理到实现再到踩坑,一次讲清楚。

1.2 高精度减法和加法、乘法的核心区别

高精度运算的本质,就是回到小学列竖式的方法,把大数字拆成一位一位的数组来模拟人工计算。这个思路对加减乘除都适用,但减法有自己的特殊性。

先说加减的区别。加法只需要从低位到高位逐位相加,超过 10 就进位,逻辑非常线性。减法却涉及到借位,而且可能出现结果前面一堆零的情况,处理完还要去掉前导零。更麻烦的是,减法结果可能是负数——这是加法和乘法理论都不需要额外操心的问题(乘法结果要么正要么零,加法同号也很少考虑负数场景)。一旦要考虑负数,就必须先判断被减数和减数谁大,然后决定计算结果的正负符号,并且在输出时把负号加上。

再一个区别是,减法的“每一步”操作其实比加法更简单,但控制流程更复杂。加法的每一位最多就是a[i] + b[i] + carry,而减法的每一位是a[i] - b[i] - borrow,一旦小于 0 就要向高位借 1。这里面的borrow状态会在整个遍历过程中传递,稍微不留神就会算错。我见过不少人写减法,代码看起来没问题,跑简单样例也对,但一到类似1000 - 999的用例就出问题,就是因为借位链和去除前导零的处理顺序不对。

所以在实现上,减法不能直接照搬加法的模板,需要单独设计一套流程。这篇文章里我会把每一步的思考过程都记录下来,包括为什么不先反转字符串、为什么要预先比较大小、借位状态怎么传播,这些才是高精度减法真正的难点所在。

2. 核心思路拆解:从字符串到数组的逆向思维

2.1 为什么用字符串存储大整数

在 C/C++ 里做高精度运算,第一步永远是解决“怎么存”的问题。int存不下、long long也存不下,那就只能用数组或者字符串。

常规做法是用字符串读入大整数,然后逆序存到int数组里。为什么逆序?因为减法和加法都是从低位开始运算的。如果正序存储,低位在数组末尾,高位在数组开头,计算到最高位之后可能还要进位/借位,数组前面就要不断插入元素,效率低而且代码复杂。逆序之后,数组下标 0 就是最低位,逐位运算时下标一路递增,非常自然。

还有一个细节:字符串读入后,下标 0 是最高位(字符形式)。如果要直接按正序操作,得先把字符串反转,或者从字符串从后往前遍历存入数组。我个人习惯是先反转字符串再存数组,这样后面逻辑清晰,不会在循环边界上绕来绕去。

2.2 比较大小:减法必须先解决符号问题

加法和乘法不需要提前比较两个数的大小——反正结果符号是确定的(同号为正,异号为负再单独处理)。但减法不同,a - bb - a的结果互为相反数。如果每次都硬着头皮用小的减大的,然后算完再补救,很容易出错。

常规做法是写一个比较函数compare(a, b),判断两个数字字符串(或两个数组)的大小关系:

  • 先比较长度。长度长的数字更大,因为位数多代表值更大(不考虑前导零的情况下)。
  • 长度相同时,从高位到低位逐位比较,找到第一个不同的数字,谁大谁就更大。
  • 如果所有位都相同,则两个数相等。

这个比较函数写完之后,减法主流程就分成两种情况:

  1. a >= b:直接算a - b,结果为正。
  2. a < b:算b - a,然后在结果前面加负号。

这样处理之后,后面的逐位减法就永远不需要考虑“减出负数”的问题——因为在a >= b的前提下,每一位在借位之后的结果一定是非负的。这个判断是整个高精度减法正确性的基石。

2.3 借位机制:逐位计算与状态传递

核心计算过程其实很简单,但细节很重要。假设两个数组ab都已经逆序存储,lalb分别是有效长度,我们保证a >= b,那么逐位减法可以这样描述:

对于第i位(从 0 开始):

diff = a[i] - borrow - b[i]

这里borrow是上一位借位状态(0 或 1)。如果diff < 0,说明不够减,需要向高位借 1,那么:

diff += 10 borrow = 1

否则:

borrow = 0

然后把diff存入结果数组。

这里有一个隐藏问题:b可能比a短。比如1000 - 999中,b是三位数,a是四位数,遍历到第 3 位时b[3]不存在,需要按 0 处理。所以循环条件不能两个数组一起走到头,而是应该遍历a的所有位,对b越界位置补 0。

借位状态的传播是整个算法的灵魂。最高位如果还需要借位,说明a < b,但我们在入口处已经保证了a >= b,所以不会发生这种情况。这也是为什么预先比较大小这么重要——它让借位逻辑变得非常简单。

2.4 去除前导零:小数减大数相关细节

减法结果容易出现前导零。最典型的例子:1000 - 999 = 1,如果直接按数组逐位打印,结果是0001,这是错的。所以计算完成后,必须把结果数组中高位的 0 全部去掉,直到只剩一位。

有一种特殊情况要单独注意:123 - 123 = 0。如果老老实实去掉前导零,最后数组里应该只剩一个0,不能是空数组。所以循环条件是:

while (len > 1 && result[len - 1] == 0) { len--; }

这个len > 1的边界条件很关键。没有它,遇到结果为 0 的情况,len会一路减到 0,后面打印的时候就傻眼了。

2.5 负数结果的处理顺序

再看a < b的情况。入口判断之后,我们交换ab的值,再执行同样的逐位减法,然后输出时在结果前面加一个负号。

这里有一个容易踩的坑:交换操作必须在比较之后、计算之前完成,否则比较判断就没意义了。而且,交换是在数组层面进行的,如果传入的是字符串,建议直接用swap(a_str, b_str)交换字符串,再重新转换为数组,这样更省心。

另外,输出负号的位置也要注意。结果是负数的场景,比如1 - 9999 = -9998,先输出-,再输出去掉前导零后的数字部分。不能输出成-09998,前导零处理必须放在负号拼接之前完成。

3. 完整实现:C++ 手写高精度减法的全过程

3.1 从字符串到数组:输入处理的标准姿势

先定义数据结构。我用vector<int>存储数字,每位存 0-9 的值。读入字符串后,将其逆序转换:

#include <iostream> #include <string> #include <vector> #include <algorithm> using namespace std; // 将字符串数字转为逆序数组,低位在前 vector<int> strToVec(const string& s) { vector<int> res; for (int i = s.size() - 1; i >= 0; --i) { res.push_back(s[i] - '0'); } return res; }

这里有一个细节值得说明:为什么要用vector<int>而不是vector<char>?虽然每个元素都是 0-9,但用int在后续运算中不需要每次做类型转换,代码更干净,性能影响也可以忽略不计。在算法竞赛中,很多选手习惯用vector<char>省内存,但个人建议新手先用int,搞清楚逻辑再优化不迟。

3.2 比较函数的实现与测试要点

比较函数可以基于字符串实现,也可以基于数组实现。如果后面还要反复用到(比如高精度除法里也要比较),建议直接做在数组上,因为数组逆序存储后,比较逻辑和字符串差不多,只是方向反了:字符串从下标 0 开始是高到低,数组从size()-1开始是高到低。

// 比较两个逆序数组,返回 true 表示 a >= b bool compareVec(const vector<int>& a, const vector<int>& b) { if (a.size() != b.size()) return a.size() > b.size(); for (int i = a.size() - 1; i >= 0; --i) { if (a[i] != b[i]) return a[i] > b[i]; } return true; // 完全相等 }

注意这里的等号处理。如果两个数完全相等,compareVec返回true,也就是说后续计算会走a - b的分支,结果为 0。这个结果没问题,但在主流程中一定不要漏掉a == b返回true的情况,否则会错误地走到负数分支,输出-0,这就是一个隐藏的逻辑 bug。

测试比较函数时,除了常规的123 > 45999 < 1000,一定要测123 == 1231000 > 999这类边界用例。很多初写高精度的人会在相等情况下栽跟头,因为长度相同、逐位比较到最后没有不同位,却返回了false

3.3 减法主函数:借位链与边界处理

接下来是核心的减法函数:

// 计算 result = a - b,调用前保证 a >= b vector<int> subVec(const vector<int>& a, const vector<int>& b) { vector<int> result; int borrow = 0; for (size_t i = 0; i < a.size(); ++i) { int bVal = (i < b.size()) ? b[i] : 0; int diff = a[i] - borrow - bVal; if (diff < 0) { diff += 10; borrow = 1; } else { borrow = 0; } result.push_back(diff); } // 去除前导零,保留至少一位 while (result.size() > 1 && result.back() == 0) { result.pop_back(); } return result; }

这里有几个要点逐一说明:

  1. 循环条件是i < a.size(),因为已经保证a >= b,所以a的长度一定不小于b,遍历a的所有位是安全的。
  2. bVal的补 0 逻辑放在了循环内部,当i超过b的长度时,按 0 处理。这比在外部把b填充到和a一样长更简洁,也省内存。
  3. borrow的状态在每一位计算后即时更新,不是累加,也不是提前算。减法借位不会跨多位,每位最多借 1,所以borrow只可能是 0 或 1。
  4. 去除前导零的循环写在函数内部,这样调用方不用每次单独处理。但要注意,这里没有处理a < b的情况,那个在主函数中完成。

3.4 整合输入输出:带符号、去前导零的完整代码

主函数负责判断符号和调用核心减法,输出部分单独封装:

int main() { string s1, s2; cin >> s1 >> s2; // 注意:这里为了方便,假设输入没有前导零;如果有,需要额外去除 vector<int> a = strToVec(s1); vector<int> b = strToVec(s2); bool negative = false; vector<int> result; if (compareVec(a, b)) { result = subVec(a, b); } else { negative = true; result = subVec(b, a); // 交换顺序,保证大的减小的 } if (negative) cout << '-'; for (int i = result.size() - 1; i >= 0; --i) { cout << result[i]; } cout << endl; return 0; }

这里把负号输出放在了最前面,然后从高位到低位打印结果数组。很多人容易在这里搞反——数组是逆序存储的,打印必须从size()-1倒着来。如果正序打印,123 - 45会输出873而不是78,因为在数组里低位在前,正序打印等于倒着读数字。

3.5 压位优化:快 10 倍的进阶写法

基础版本按 10 进制存储,每一位只存 0-9。对于大部分题目和场景来说,这个版本已经完全够用。但如果数据规模很大(比如 10 万位数字的减法),逐位计算的常数就比较大了。这时候可以用压位技巧,让数组的每一位存一个更大的基数。

常见做法是每 4 位数字压成一个int存储,即基数为 10000。这样数组长度直接缩小到原来的 1/4,循环次数也相应减少。减法逻辑几乎不用改,只需要把基数从 10 改成 10000,并且处理借位时改成diff < 0diff += BASE

const int BASE = 10000; vector<int> subVecPacked(const vector<int>& a, const vector<int>& b) { vector<int> result; int borrow = 0; for (size_t i = 0; i < a.size(); ++i) { int bVal = (i < b.size()) ? b[i] : 0; int diff = a[i] - borrow - bVal; if (diff < 0) { diff += BASE; borrow = 1; } else { borrow = 0; } result.push_back(diff); } while (result.size() > 1 && result.back() == 0) { result.pop_back(); } return result; }

压位后的输出需要额外注意:除了最高位那块之外,其他块输出时必须补前导零。比如BASE = 10000,结果为1234两块,输出应该是1234,不能把 234 直接打印成1234(如果块值是 234 而不是 0234,就是1234碰巧没问题;但如果块值是 34,就应该是10034而不是134)。这块细节比较多,单独写一个辅助函数处理。

4. 高精度减法在 Python、Java、JavaScript 里的不同打开方式

4.1 Python:自带大整数,但你可以模拟加法的思路

很多读者可能想:Python 的int本身就支持任意精度,为什么还要学高精度减法?

这是对的,在实际工程里我们确实不需要在 Python 里手写高精度。但在理解算法原理、应对某些不允许使用内置大整数的 OJ 题库,或者需要模拟底层计算逻辑的场景中,手写一遍仍然有意义。而且,掌握原理之后用 Python 去验证其他语言实现是否正确,非常方便——拿 Python 的int运算结果当基准测试用例,这是我在调试 C++ 高精度代码时最常用的手段。

Python 模拟高精度减法的代码和 C++ 思路完全一致:

def str_to_vec(s): return [int(ch) for ch in reversed(s)] def compare_vec(a, b): if len(a) != len(b): return len(a) > len(b) for i in range(len(a) - 1, -1, -1): if a[i] != b[i]: return a[i] > b[i] return True def sub_vec(a, b): result = [] borrow = 0 for i in range(len(a)): b_val = b[i] if i < len(b) else 0 diff = a[i] - borrow - b_val if diff < 0: diff += 10 borrow = 1 else: borrow = 0 result.append(diff) while len(result) > 1 and result[-1] == 0: result.pop() return result def big_sub(s1, s2): a = str_to_vec(s1) b = str_to_vec(s2) neg = False if compare_vec(a, b): result = sub_vec(a, b) else: neg = True result = sub_vec(b, a) res_str = ''.join(str(x) for x in reversed(result)) return '-' + res_str if neg else res_str

用 Python 模拟一遍,最大的好处是可以随时拿内置int来对拍验证:

import random for _ in range(10000): x = random.randint(-10**50, 10**50) y = random.randint(-10**50, 10**50) # 注意:这个模拟只处理非负整数,负数自行扩展

这也是我强烈建议的做法——学高精度一定要会写对拍器,靠随机数据批量验证,而不是盯着几个固定用例看半天。

4.2 Java:BigInteger 与手写对比

Java 里自带java.math.BigInteger,功能非常强大。日常开发中直接用BigInteger.subtract()即可,不需要自己写。

但在算法竞赛中,部分判题环境支持 Java 但不允许使用BigInteger(多见于一些教学型 OJ),这时候就得手写。Java 手写高精度减法的思路和 C++ 几乎一模一样,只是语法稍有不同:用ArrayList<Integer>int[]存储,注意数组扩容和长度管理。

public static int[] subArrays(int[] a, int[] b) { // 调用前保证 a >= b int[] result = new int[a.length]; int borrow = 0; for (int i = 0; i < a.length; i++) { int bVal = i < b.length ? b[i] : 0; int diff = a[i] - borrow - bVal; if (diff < 0) { diff += 10; borrow = 1; } else { borrow = 0; } result[i] = diff; } // 处理长度(可改为返回有效长度) return result; }

Java 里int[]长度固定,有效位数需要额外用一个变量记录,或者在返回后手动去除前导零。这些细节在实现时要特别留意。

4.3 JavaScript:Number 精度陷阱与 BigInt

JavaScript 的Number类型是双精度浮点,超过2^53 - 1(9007199254740991)就会出现精度丢失。所以在前端处理大数运算时,要么用BigInt,要么手写字符串模拟。

BigInt是 ES2020 引入的,可以直接处理任意大的整数:

const a = 123456789012345678901234567890n; const b = 98765432109876543210987654321n; const result = a - b; console.log(result.toString());

BigInt的减法很简单,但在某些极端场景下(比如需要兼容不支持BigInt的旧环境),手写字符串模拟仍然是一种兜底方案。JavaScript 手写高精度减法的循环思路和其他语言一致,只是注意parseIntString.fromCharCode的边界。

其实我在做前端项目时发现,很多情况下大数计算并不是性能瓶颈,而是容易在边界输入上出问题。比如用户输入一个超出Number.MAX_SAFE_INTEGER的数字,如果没有预先做字符串类型的判断,直接做Number(a) - Number(b),结果就是错的。所以前端处理大整数,第一原则是永远不要在Number上做大数运算。

5. 常见问题与调试实录:这些坑我替你先踩了

5.1 前导零问题:1000 - 999 = 0001 怎么修

这是高精度减法里出现频率最高的 bug。原因很简单:逐位计算结果数组里,高位确实有 0,但打印时没有去掉。

修复方法就是我在 3.3 节写的:

while (result.size() > 1 && result.back() == 0) { result.pop_back(); }

这里有一个容易忽略的前提:result.back()是最高位,因为数组是逆序存储的,最后 push 进去的元素就是最高位。如果你把数组正序存储,这个逻辑就要反着写。每一个高精度实现,都必须先搞清楚自己的数组方向,再去写去除前导零的逻辑。

5.2 借位传递错误:999 - 100 变成 809

很多人第一次写借位时会这样写:

diff = a[i] - b[i] - borrow; if (a[i] < b[i]) { diff += 10; borrow = 1; }

这个写法在borrow = 0时是对的,但一旦上一位借了位,当前位的判断就不对了。必须把borrow也纳入判断:

diff = a[i] - b[i] - borrow; if (diff < 0) { diff += 10; borrow = 1; }

判断条件应该基于diff是否为负,而不是a[i] < b[i]。因为即使a[i] >= b[i],如果上一位留下了borrow = 1,这一位依然可能减出负数,比如a[i] = 5b[i] = 4,但上一位借位 1,diff = 5 - 4 - 1 = 0,没问题;但如果a[i] = 4b[i] = 4borrow = 1diff = -1,必须借位。只看a[i] < b[i]就会漏掉这种情况。

5.3 负数边界:100 - 999 = -899 与 100 - 100 = 0

负数输出的逻辑看起来简单,但边界用例依然容易出错。

100 - 100 = 0的结果不是-0,这很重要。因为在比较函数里,相等返回true,所以会走a >= b的分支,不会进入负数逻辑,自然不会输出负号。所以只要你正确实现了compareVec对相等情况的处理,就不会出现-0的问题。如果你发现输出变成-0,大概率是compareVec的等号判断写反了。

100 - 999 = -899的正确输出是-899,不是-0899也不是-8990。按照我们的流程:先比较发现a < b,交换后计算999 - 100 = 899,去掉前导零,然后加负号输出。只要去除前导零在加负号之前完成,就不会出问题。

5.4 性能考量:什么时候需要压位

高精度减法的性能瓶颈主要在循环次数和内存访问。对于长度 100 位以内的数字,直接用 10 进制逐位计算完全没有问题。但如果长度到 10⁵ 甚至 10⁶ 级别,逐位循环的常数就很可观了。

这时候推荐压位存储,每 4 位压成一个int,循环次数减少到原来的 1/4。再配合快读快写,性能提升非常明显。我在处理一个 10 万位大数相减的测试用例时,基础版跑了大约 5 毫秒,压位后降到 1 毫秒左右,效果立竿见影。

另外还有一个小技巧:如果只做减法,而且两个数长度差距很大,可以先判断长度差,超过一定阈值时直接跳过逐位比较,减少不必要的计算。不过这种优化在减法里收益不大,真正需要的是在比较函数上优化——比较函数如果被频繁调用(比如在除法实现里),它的性能同样会影响整体表现。

5.5 对拍测试:高精度代码的正确性保障

高精度代码几乎不可能一次写对,尤其是减法这种边界条件极多的算法。我的建议是,写完不能只靠几个手写用例,一定要写对拍程序。

对拍思路很简单:

  1. 用 Python 或 Java 的内置大整数作为基准实现。
  2. 随机生成大量测试数据(覆盖不同长度、不同边界条件)。
  3. 将手写实现的结果和基准实现的结果做比对。
  4. 遇到不一致,缩小数据范围,定位是哪个环节出了问题。

以 C++ 和 Python 对拍为例,C++ 程序读入两个字符串,输出结果;Python 脚本生成随机大数,分别调用 C++ 程序和内置int运算,比对输出。

import random import subprocess for i in range(10000): a = random.randint(0, 10**50) b = random.randint(0, 10**50) sa, sb = str(a), str(b) # 调用 C++ 程序 p = subprocess.run(['./big_sub'], input=f'{sa}\n{sb}\n', capture_output=True, text=True) cpp_result = p.stdout.strip() # Python 基准 py_result = str(a - b) if cpp_result != py_result: print(f'Error: {sa} - {sb}') print(f'C++: {cpp_result}') print(f'Python: {py_result}') break else: print('All tests passed!')

对拍是验证高精度代码最有效的手段,没有之一。手写几个用例永远只能证明“这几个用例没问题”,对拍能在几分钟内帮你跑完几万个随机用例,覆盖面完全不是一个量级。

6. 从减法出发:高精度运算体系的构建

6.1 加法、乘法、除法的衔接关系

学会减法之后,高精度运算的其余部分就顺理成章了。加法、乘法、除法都是围绕“数组模拟竖式”这同一个核心思想展开的。

  • 加法和减法共用一个比较判断思路,减法里a < b的处理就能用到加法的逻辑。
  • 乘法的实现会用到加法和位移的概念,本质上是多次加法的整合。
  • 除法的实现最复杂,会用到减法(试商时反复减),也会用到比较函数。

所以很多人把高精度学习顺序定为“加减乘除”,这个顺序是合理的。减法作为承上启下的关键环节,学扎实了,后面就轻松不少。

6.2 如何用高精度减法实现浮点数处理

高精度不只在整数领域有应用,在处理定点小数时同样需要减法。思路是:把小数部分单独存放在另一个数组里,运算时按位对齐,借位可以跨整数部分和小数部分的边界。

举个例子,3.14 - 1.99按整数运算相当于314 - 199 = 115,然后根据小数位数还原为1.15。这个过程本质上就是用整数减法模拟小数减法。在实际的高精度小数库中,对齐小数位、处理借位、输出小数点的逻辑,都是在加减法基础上扩展出来的。

6.3 高精度减法在算法竞赛中的典型应用

在算法竞赛中,高精度减法很少单独出现,更多是作为其他算法的一个子步骤。典型场景有:

  1. 大整数阶乘、斐波那契数列计算(先乘后减)。
  2. 大整数取模运算的辅助步骤。
  3. 分数运算中分数约分(需要求最大公约数,辗转相除法里可能会涉及减法)。
  4. 大整数进制转换。

我在刷题时发现,很多题目看着是“数论题”,实际上核心是高精度运算。如果高精度减法实现得不够稳,整个题就跑不对,尤其是那种结果差一位就判错的题目,前导零或符号错误都会直接导致 WA。

6.4 扩展思路:从减法到模逆元、高精度分数的应用

如果你已经能够熟练实现高精度减法,下一步可以考虑更多扩展。比如结合高精度减法实现高精度最大公约数(辗转相除法),进而实现高精度分数运算。这类封装好的工具类,在需要精确计算(例如金融场景的利率计算)时非常有用。

我对高精度减法最深的体会是:它虽然简单,却是所有高精度运算的基础。但凡你想处理超出内置类型范围的数值计算,减法就是你绕不过去的坎。把这个坎迈过去,后面的路就顺了。

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

AI编程时代,程序员如何从编码者进化为价值定义者?

这两年&#xff0c;每隔一段时间就会有人把“AI编程会不会替代程序员”这个话题顶上热门。我也被问过很多次&#xff0c;问的人里有刚入行的新人、工作五六年的老同事&#xff0c;还有准备让孩子学编程的家长。说实话&#xff0c;我对“会不会被替代”这件事的焦虑已经过去了&a…

作者头像 李华
网站建设 2026/9/10 1:26:59

xhEditor集成PDF导入:高亮与注释还原完整方案

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

作者头像 李华
网站建设 2026/9/10 1:26:50

AI论文工具实测:7款软件全流程跑分与写作闭环选型指南

1. 毕业季实测&#xff1a;为什么我把市面上叫得上号的 AI 论文工具全跑了一遍 上个月学弟来找我时&#xff0c;我正在帮另一位朋友改硕士论文的致谢段落&#xff0c;改到第三版还是被答辩秘书挑毛病。他苦笑着说&#xff0c;现在连致谢这种固定套路的文字都写不顺&#xff0c;…

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

libcurl 自定义 DNS 服务器:CURLOPT_DNS_SERVERS 完整指南

libcurl 自定义 DNS 服务器&#xff1a;CURLOPT_DNS_SERVERS 完整指南 【免费下载链接】curl A command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQ…

作者头像 李华