计算机系统基础实验要求实现一个 ftoa 函数,将 float 型变量转换为字符串,转换效果同 printf 函数
在这之前,写了 itoa 的实现,int 到字符串嘛,%%% 把数字取出来就好了嘛!
咋一看,ftoa 也简单,float 嘛,只有七八位精度,**// 搞一搞应该没啥问题。好做......
个屁
float 用 ieee754编码保存的,每个规格化数/非规格化数都是准确对应了一个十进制小数的!所以不能搞近似!
特别是实验了一下 printf ,居然也真的是全活不打折地输出的!
比如编码 0x7f7fffff 用 printf ,%f 输出,整数部分39位是一点没差;0x1 用 printf ,%f 输出,最低位是小数点后149位;
我想了半天是怎么实现的,最后猜测到:printf 里面难道还内置了高精度计算?
后来查阅了一些资料,似乎真的是?是篇英文的,不想看,反正就是确认了得写高精度!
但是不想写,再后来问了老师,确实是要写高精度!
那就写高精度咯!
因为是纯 C语言,没有类,没有成员函数这些概念,运算符重载没试,不知道有没有,估计是没有。写起来不习惯
先来 高精度加法、乘法
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define MAX_LEN 200
typedef struct bigint{
char val[MAX_LEN];
} bigint;
int reg[MAX_LEN];
bigint plus(bigint x, bigint y)
{
memset(reg,0,sizeof(reg));
bigint ans;
int xlen = strlen(x.val), ylen = strlen(y.val);
int len = (xlen > ylen)? xlen:ylen;
int i,j,k;
for(k = 0, i = xlen-1, j = ylen-1; (i >= 0) || (j >= 0); )
{
if(i >= 0)reg[k] += x.val[i] - '0';
if(j >= 0)reg[k] += y.val[j] - '0';
if(reg[k] >= 10){reg[k+1] = 1; reg[k] -= 10;}
k++; i--; j--;
}
if(reg[k] == 1)len++;
for(i = 0, j = len-1; j >= 0; i++, j--)ans.val[i] = reg[j] + '0';
ans.val[len] = '\0';
return ans;
}
bigint mul(bigint x, bigint y)
{
memset(reg,0,sizeof(reg));
bigint ans;
int xlen = strlen(x.val), ylen = strlen(y.val);
int len = xlen + ylen;
int i,j;
char a[MAX_LEN],b[MAX_LEN];
for(i = 0, j = xlen-1; j >= 0; i++,j--)a[i] = x.val[j] -'0';
for(i = 0, j = ylen-1; j >= 0; i++,j--)b[i] = y.val[j] -'0';
for(i = 0; i < xlen; i++)
for(j = 0; j < ylen; j++)
reg[i+j] += a[i]*b[j];
for(i = 0; i < len; i++){reg[i+1] += reg[i]/10; reg[i] %= 10;}
if(reg[len-1] == 0)len--;
for(i = 0, j = len-1; j >= 0; i++, j--)ans.val[i] = reg[j] + '0';
ans.val[len] = '\0';
return ans;
}
通过上面的实验可以估计,位串长度不会超过200位,于是把一个200位的字符串声明成了bigint的类型,来处理大整数计算
需要说明的是,这里是用的一个很朴素的方法来实现高精度计算的,只做实现,不谈效率,大佬们尽可使用 FFT 等等高阶方法
加法、乘法都是右对齐计算的,字符串天然下是左对齐的,所以需要进行一些处理
为了赋值的时候较为容易,还写了一个赋值函数,就是套了一个strncpy;为了把 int值赋给bigint,又把 itoa 贴了进来
因为会用到 2的幂次和 5的幂次,有求幂运算,就又搞了一个二分快速幂
void setval(bigint *x, char *ptr)
{
strncpy(x->val,ptr,200);
}
void cs_itoa(int num, char str[])
{
if(num == 0)
{
str[0] = '0'; str[1] = '\0';
return;
}
char a[16];
int i,j,len,flag;
flag = (num < 0);
if(flag) num = -num;
for(i = 0; num ; i++)
{
a[i] = (num % 10) + '0';
num /= 10;
}
if(flag)a[i++] = '-';
a[i] = '\0';
len = i;
for(i = len -1, j = 0; i >= 0 ; i--, j++)str[i] = a[j];
str[len] = '\0';
return;
}
bigint Montgemery(bigint a, int b)
{
bigint ans;
setval(&ans,"1");
while(b)
{
if(b&1) ans = mul(ans,a);
a = mul(a,a);
b >>= 1;
}
return ans;
}
下面才开始正题,ftoa 函数,把float转换为字符串
void cs_ftoa(float num, char str[], int digit )
先把符号码,阶码,尾数码取出来,完成这些需要位运算,我用到了一个union
union int_float
{
float f;
unsigned int i;
};
union int_float m;
m.f = num;
int e,frac,s;
bigint a,b;
int pointsh = 0; //小数点需左移位数
char tmp[MAX_LEN]; //保存纯数字
s = (m.i & 0x80000000)>>31;
e = (m.i & 0x7f800000)>>23;
frac = (m.i & 0x7fffff);
然后,把特殊值先行处理掉,inf、-inf 以及 nan
if(e == 255)
{
if(m.i == 0x7f800000)strncpy(str,"inf",3);
else if(m.i == 0xff800000)strncpy(str,"-inf",4);
else strncpy(str,"nan",3);
return;
}
先得出输出的数字串,并且得到小数点需移动的位数
先看非规格化数
if(e == 0)
{
pointsh = 149;
cs_itoa(frac, a.val);
setval(&b,"5");
b = Montgemery(b, 149);
a = mul(a,b);
strncpy(tmp,a.val,MAX_LEN);
}
再看规格化数
else
{
frac = frac | (1<<23);
e -= 150;
if(e >= 0) // frac*2^e
{
cs_itoa(frac, a.val);
setval(&b,"2");
b = Montgemery(b, e);
b = mul(b,a);
strncpy(tmp,b.val,MAX_LEN);
}
else // e<0, (frac*5^(-e))/(10^(-e)) -e 就是 小数点移位位数
{
e = -e;
pointsh = e;
cs_itoa(frac, a.val);
setval(&b,"5");
b = Montgemery(b, e);
a = mul(a,b);
strncpy(tmp,a.val,MAX_LEN);
}
}
首先补充尾数码的隐含位1,再把阶码 e -127 再 -23,这样原数值绝对值部分就变成了 frac*(2^e) 的形式了,当 e >= 0 时,结果一定为整数。
接下来确定正负号
int pos = 0, i;
int len = strlen(tmp);
if(s)str[pos++] = '-';
然后来做移位和舍入工作
对于结果是整数的,这个工作很好做
if(pointsh == 0){printf("%s\n",tmp); return;}
{
strncpy(str+pos,tmp, MAX_LEN);
pos += len;
if(digit > 0)str[pos++] = '.';
for(i = 1; i <= digit; i++)str[pos++] = '0';
str[pos] = '\0';
return;
}
如果数串长度小于等于左移位数,这意味着需要补充前导零。在这之前,需要根据保留位数把舍入先做了来
if(len <= psh )
{
if(digit == 0)
{
str[pos++] = '0';
str[pos] = '\0';
return;
}
strncpy(str+pos,"0.", 2);
pos += 2;
if(psh - len > digit) // digit + 1 位为前导0,输出全0
{
for(i = 1; i <= digit; i++)str[pos++] = '0';
strncpy(str+pos,tmp, len);
pos += len;
str[pos] = '\0';
return;
}
//去后导0
for(i = len-1;tmp[i] == '0';i--){tmp[i] = '\0';psh--;len--;}
if(digit >= psh) //前补0,后补0
{
for(i = len; i <= psh; i++)str[pos++] = '0';
str[pos] = '\0';
return;
}
// 最麻烦的部分,定位,进位,去除后导0
int pt = digit + len - psh;
if(tmp[pt] < '5')//舍尾;
{
tmp[pt] = '\0'; //舍尾
for(i = len; i <= psh; i++)str[pos++] = '0';
len = strlen(tmp);
strncpy(str+pos,tmp, len);
pos += len;
str[pos] = '\0';
return;
}
// 以下部分还没整清楚
if(tmp[pt] >= '5' && ( (psh > pt) || (tmp[pt-1]) ) )
for(i = len-1;tmp[i] == '0';i--)tmp[i] = '\0';
for(i = psh - len; i > 0; i--)putchar('0');
printf("%s\n",tmp);
return;
}
好麻烦,不想写了,睡觉!