问题提出
我们知道,在python中,使用内置的int数据类型即可完成大整数高精度运算。但是,C++和Java等语言内置的整数只能满足最高64位的计算。对此,我们希望在C++中实现一个大整数类,其具有一定的高精度计算能力。
目标
制作一个大整数类,实现高精度加、乘法运算等功能。为满足变长要求,采用动态内存申请方式管理内部数据。为简化难度,暂只考虑无符号情况。
功能点设计
首先,一个数字必须要有赋值能力。对此,我们设计如下功能:
- 使用 unsigned long long 为大整数对象赋值
- 大整数拷贝(使用另一大整数对象为该大整数对象赋值)
作为数,应该具有四则运算能力。为简化设计,暂只考虑加法和乘法计算。即,设计以下功能:
- 加法赋值
- 乘法赋值
为方便使用,还可设计以下功能:
- 判断是否为0
- 从输入缓冲区读入大整数
- 输出大整数
由于采用动态内存管理,应设计清理功能:
- 清理(释放内存等)
功能实现
首先,遵循面向对象程设思路,使用结构体描述大整数类。
结构体如下:
struct BigUInt {
char* num = NULL;
long long memSize = 0;
int firEmpty = 0;
void functions(...);
};
数据成员
我们先看数据成员。
注意,代码思路已经写在注释里,不再额外解释。具体请看代码注释。
特别注意:
以下代码对动态内存申请的处理非常不规范!
另一篇文章中有提供一种相对较好的处理方式,欢迎前往查看。
char* num = NULL;
使用 char 数组存储每一位数字。下标从0到n的元素分别存储个位到第n位。
此处应将 char 理解为占用内存空间为8位的整数(即:uint8),而不是字符。
当然,也可以用 unsigned char 或 short 等,直接用 int 也差不多,但用 int 会消耗更多内存。
内存空间动态申请,所以初始状态 num 为指向空位置的空指针,后续分配时其指向会不断改变。
long long memSize = 0;
记录目前已有的内存长度,在需要填入更多数字时判断要不要更多空间。
int firEmpty = 0;
第一个空的位,用来标记一个数字的结尾。
比如对于数字 123
,有 num[0] = 3
, num[1] = 2
, num[2] = 1
, 则第一个空位为3, 标记 firEmpty = 3
.
功能函数
接下来,开始书写功能函数。
使用 uint64 为大整数对象赋值
/**
* 使用一个无符号长整数给大整数对象赋值。
* 如果不懂什么是“对象”,就理解成“某个大整数”。
*/
void set(const unsigned long long x) {
// 如果传入数字是0,只需要把第一个空位标记为0,即可表示该大整数为0.
if (x == 0) {
firEmpty = 0;
return;
}
// ----------------
// 计算传入数字的长度。
int xLen = 0; // 传入数字的长度。
unsigned long long xt = x; // xt 表示对 x 的缓存。直接对 x 做改变数值的操作不礼貌,且后续还要重新用 x 的值。
while (xt > 0) {
xLen += 1;
xt /= 10;
}
// ----------------
// 当传入数字长度长于该大整数拥有的内存空间,
// 则需要申请使用更多的内存来存储数据。
if (xLen > memSize) {
// realloc 为动态内存申请。传入内存指针和需要的总内存量,返回新的内存地址指针。
// realloc 的返回值为 void* 但 num 的类型是 char*, 因此需进行类型转换。
// 直接用 (char*) 转换即可。讲究的话,可以查下 reintepret_cast<char*>().
num = (char*)realloc(num, xLen * sizeof(char));
// 当申请失败,realloc 会返回NULL.
// 工程中应对这种情况进行特殊处理(如 throw exception, 感兴趣可以查。
if (num == NULL) {
printf("BigUInt::set(uint64 x): Runtime Error: failed to reallocate *num!\n");
exit(0); // 此处处理方式为输出错误信息并退出程序。实际工程中应该不能这样处理。
}
memSize = xLen; // 申请成功后,记录目前拥有的内存大小。
}
xt = x;
int idx = 0; // idx 表示 下标 (index).
// 按位赋值。
while (xt > 0) {
num[idx] = xt % 10;
xt /= 10;
idx += 1;
}
firEmpty = idx; // 赋值完成,记录大整数的第一个空位置。
}
用另一个大整数给本大整数赋值
/**
* 用另一个大整数值给本大整数赋值。
*/
void set(BigUInt* x) {
// 至少要用的内存量与 x 的 firEmpty 值相同。判断当前大整数内存是否够用,并在不足时申请。
if (memSize < x->firEmpty) {
num = (char*)realloc(num, x->firEmpty * sizeof(char));
if (num == NULL) {
printf("BigUInt::set(BUI* x): Runtime Error: failed to reallocate *num!\n");
exit(0);
}
memSize = x->firEmpty;
}
firEmpty = x->firEmpty; // 复制 firEmpty 的值。
// 复制存储的每位数字内容。
for (int i = 0; i < firEmpty; i++) {
num[i] = x->num[i];
}
}
加法赋值
/**
* 加法。将 x 的值加到本大整数上。
*/
void add(BigUInt* x) {
if (x->isZero()) {
return; // 如果 x 是0, 则不执行任何操作。
}
// ----------------
// 计算 maxLen,表示计算结果最大位数。
// 两个数增加,结果不超过两个加数中更大的那个数的长度加一。
int maxLen = x->firEmpty + 1; // 假设 x 更长。
if (firEmpty + 1 > maxLen) {
maxLen = firEmpty + 1; // 如果本大整数更长,则最大位数是本大整数长度加一。
}
// 如果内存不够,就申请。
if (maxLen > memSize) {
num = (char*)realloc(num, maxLen * sizeof(char));
if (num == NULL) {
// failed to realloc
exit(0);
}
memSize = maxLen;
for (int i = firEmpty; i < memSize; i++) {
num[i] = 0;
}
}
int carry = 0; // carry 代表 进位。
int i;
for (i = 0; i < x->firEmpty; i++) {
if (i >= firEmpty) {
num[i] = 0;
}
num[i] += x->num[i] + carry;
carry = num[i] / 10;
num[i] %= 10;
}
while (carry != 0) {
if (i >= firEmpty) {
num[i] = 0;
}
num[i] += carry;
carry = num[i] / 10;
num[i] %= 10;
i += 1;
}
firEmpty = (firEmpty > i ? firEmpty : i); // 更新 firEmpty 数值。
/*
firEmpty > i ? firEmpty : i
相当于:
if (firEmpty > i) {
return firEmpty;
} else {
return i;
}
*/
}
乘法赋值
/**
* 大整数乘法。本题最难的地方。
* 自己写的时候,最好先思考好每一步做什么,把要做的事写到注释里,跟着自己的注释写代码。不然容易乱。
* 下方函数中的注释皆为开发时所写,可简单还原思考过程。此处不再额外添加教学注释。
*/
void multiply(BigUInt* x) {
// 对 0 做特殊处理,防干扰
if (x->isZero() || isZero()) {
firEmpty = 0;
return;
}
// 计算最大可能占用的长度
// 两数相乘,结果长度不超过原数字长度之和
int tarMemSize = x->firEmpty + firEmpty;
// 扩展空间
num = (char*)realloc(num, tarMemSize * sizeof(char));
if (num == NULL) {
printf("BigUInt::multiply: Runtime Error: failed to reallocate *num!\n");
exit(0);
}
memSize = tarMemSize;
int i, j; // 循环控制变量
// 被乘数对象缓存,并清空原对象
char* tmp = (char*)malloc(firEmpty * sizeof(char));
if (tmp == NULL) {
printf("BigUInt::multiply: Runtime Error: failed to allocate *tmp!\n");
while (true);//exit(0);
}
for (i = 0; i < firEmpty; i++) {
tmp[i] = num[i]; // 拷贝
}
for (i = 0; i < memSize; i++) {
num[i] = 0; // 完整清空被乘数数值数据
}
int carry; // 进位
int tarIdx; // 位相乘结果目标 index
for (i = 0; i < x->firEmpty; i++) { // i: 针对乘数每位循环
carry = 0; // 清除上一次进位记录
for (j = 0; j < firEmpty; j++) { // j: 针对被乘数每位循环
tarIdx = i + j;
num[tarIdx] += carry; // 补上进位
num[tarIdx] += tmp[j] * x->num[i];
carry = num[tarIdx] / 10; // 计算下一次进位
num[tarIdx] %= 10; // 去除个位以上的更高位
}
// 将 carry 添加到最高位。
num[tarIdx + 1] += carry;
}
firEmpty = (carry > 0 ? tarIdx + 2 : tarIdx + 1);
free(tmp); // 释放被乘数缓存数组
}
判断是否为0
/**
* 返回一个 bool, 表明该数字是否为0.
* 可以不要 inline.
* inline 是什么?不懂可以不要。
*/
inline bool isZero() {
return firEmpty == 0;
}
打印
注:本例中直接使用 stdout 将大整数内容输出。读者可以考虑研究使用重载 cout 的 << 运算符完成功能。
/**
* 使用 stdout (printf, putchar)输出本大整数。
*
* @参数 shouldAddDivider 是否要在输出结束后额外添加一个分隔字符。
* @参数 divider 如果需要添加分割字符,需要添加哪个字符。
*/
// 注:如上填写注释,将“参数”换为"param",即可被 visual studio, intellij idea 等比较聪明的编辑器识别。
// 此外,还可以添加 @return 说明返回值,@throw 说明可能出现的错误等。(throw 相关内容sj只要求听说,不要求会用)
void stdPrint(bool shouldAddDivider = false, char divider = ' ') {
if (isZero()) {
putchar('0'); // 如果大整数为0, 直接输出0结束.
}
else {
for (int i = firEmpty - 1; i >= 0; i--) {
putchar(num[i] + '0'); // 逐位输出。
}
}
if (shouldAddDivider) {
putchar(divider); // 末尾分割符。
}
}
清理
/**
* 清理。
*/
void cleanup() {
if (num != NULL) {
free(num); // 释放申请到的内存。
}
memSize = 0;
firEmpty = 0;
}
标准读入
从输入缓冲区,采用 stdin 模式读入大整数。
/**
* 使用 stdin (scanf, getchar) 读取该大整数的值。自动过滤前方非数字内容。
*/
void stdRead() {
char c = getchar();
while (c < '0' || c > '9') {
c = getchar();
}
// 上方三行代码过滤掉了前面的非数字内容,并使 c 为数字输入的第一位。
// 如,对于输入内容 "yqLyz123Larry", 执行到此处,c 的内容为'1'.
// ----------------
firEmpty = 0;
while (c >= '0' && c <= '9') {
if (memSize <= firEmpty) {
// 若内存不足,则增加 1KB
// 增加量太小,会导致频繁申请内存。实测频繁申请内存会导致交上去后无法满分。
num = (char*)realloc(num, (memSize + 1024) * sizeof(char));
if (num == NULL) {
printf("BigUInt::stdRead: Runtime Error: failed to reallocate *num!\n");
exit(0);
}
memSize += 1024;
}
num[firEmpty] = c - '0'; // 存储当前数字
firEmpty += 1;
c = getchar();
}
}
等等!有没有发现一些问题?
聪明的你一定发现,这种读入方式把大整数之后的一位字符也吃进去了!这样做是不太对的。读者可以对此进行改进。
此外,聪明的你一定还发现了一个问题:如此读入的数字,在大整数对象中存储时,是反的!因此,我们需要额外添加一个函数,用于反转大整数数据。
反转
/**
* 将大整数内容反转。如,将123变为321.
*/
void revert() {
for (int i = 0; i < firEmpty / 2; i++) {
std::swap(num[i], num[firEmpty - 1 - i]);
}
}
是不是十分简单!
但是,如果读入的大整数有前缀0,反转后这些0会存在高位,导致firEmpty的值不正确。是不是应该把那些0去掉?
去除前缀0
/**
* 去掉前缀 0. 例:将 00023 变为 23, 92 保持为 92.
*/
void shrink() {
while (firEmpty > 0 && num[firEmpty - 1] == 0) {
firEmpty -= 1;
}
}
同样非常简单!
如此,在读入结束前,调用反转和去0,即可完成读入的最后一步!
revert();
shrink();
至此,大整数类设计完毕。可以尽情使用啦!
展望
不难发现,这个大整数类尚存在很多问题,比如前述的动态内存申请处理不规范和读入时会额外读入一个不属于大整数的字符。此外,还存在一些问题:
- 不支持减法
- 不支持除法
- 不支持负数
- 不支持比较
- 单数字占用空间仍较大(每位数字元素可以存 -128~127 的值)
等。
关于符号,只需要加入一个数据成员bool isNegative
用于存储正负性即可。由于对0的判断使用函数isZero()
完成,isNegative
变量不需要考虑为0时符号的问题。
乘法执行时,只需要额外将两乘数的符号成员取异或即可。
加法执行时,需要对为负情况进行特别处理。该问题留给读者。
改进完加法,即可将减法看作取负之后进行加法。
对于除法,笔者暂时未思考如何实现,但认为同样可以通过模拟竖式计算完成。
以上便是使用 C++ 实现无符号大整数类的方法及一些展望,或许能在某些方面给你一些启发。