📌 更新(2026-09-01):归档这篇 2022 年的旧作业,顺手用 GCC 15.1.0 复测——结果一组测试数据当场翻车,挖出了下文的浮点罗生门现场。
我手里最老的存货之一:头上顶着”英文教材 P12 作业 1.3”的注释,从判题平台的 Begin/End 填空标记看,这是一道在线作业题——不用 printf 的格式化功能,自己把一个 double 逐位打印出来,还要处理四舍五入。
主框架是教材 P10 的递归打印函数 PrintOut——高位递归完了才打印当前位,个位数作递归出口,这是”递归打印整数”的经典范本。要填的空有三处:RoundUp 里凑出四舍五入的加数,DecPart 里取小数部分,PrintFractionPart 里为下一位循环做准备。填法本身不难,核心的 RoundUp 长这样:
1 2 3 4 5 6 7 8 9
| double RoundUp( double N, int DecPlaces ) { int i; double AmountToAdd = 0.5;
for(i = 0;i < DecPlaces; ++i) AmountToAdd *= 0.1;
return N + AmountToAdd; }
|
保留 N 位小数,就在原数上加 0.5 × 10⁻ᴺ:第三位够 5 就进到第二位,不够也只是把尾数抬高一点、截断时看不见。思路干净。但 2026 年复测的时候,一组测试数据当场翻车:
1 2 3 4 5 6 7 8
| 输入: 3.14159 2 3 .|0.146590|1 4
输入: 2.675 2 2 .|0.680000|6 7 ← 看这里
输入: -1.618 3 -1 .|0.618500|6 1 8
|
先解释输出格式:PrintDigit 打印的是 %d (带空格),所以最终结果长这样——3 . 1 4,中间那个 |0.146590| 是当年忘了删的调试打印,打印的是小数部分的值。
问题来了:输入 2.675 保留两位,|0.680000| 显示小数部分是 0.68,可后面逐位打出来的数字是 6 和 7——同一个变量,调试打印说它是 0.680000,数位提取说它是 0.67 开头。它自相矛盾了。
真相在比特里:2.675 在 double 里存的是 2.67499999…,加 0.005 之后是 2.67999999…。printf("%lf") 显示时会四舍五入到小数点后六位,好心地把 0.6799999… 显示成 0.680000;而逐位提取走的是 ×10 取整减掉 的老路,每一脚都踩在真实的比特上——6.7999 取整得 6,0.7999×10 得 7.999 取整得 7。显示层在粉饰,提取层在验尸,两边一对照,浮点数的底裤就露出来了。
顺便,2.675 这个数本身就是浮点界的著名陷阱:它连存储都存不准(存成 2.6749999…),所以”四舍五入到 2.68”在 IEEE 754 面前本来就是伪命题。作业题里撞见教科书级反例,运气不错。
另外两组输出是正常的:3.14159 保留两位得 3.14(第三位是 1,不进位),-1.618 保留三位得 -1.618(负号在 PrintReal 开头就被摘下来单独处理,打印前再挂回去)。
完整源码
四舍五入.c1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
|
#include<stdio.h> void PrintReal(double N, int DecPlaces); double RoundUp( double N, int DecPlaces );
int IntPart(double N); double DecPart(double N);
void PrintFractionPart(double FractionPart, int DecPlaces);
void PrintDigit (int N); void PrintOut(int N);
int main(void) { double RealValue,XiaoshuWeishu ;
printf("输入想显示的任意实数 :\n"); scanf("%lf",&RealValue);
printf("输入想显示的实数小数位数 :\n"); scanf("%lf",&XiaoshuWeishu);
printf("四舍五入后这个实数为:\n"); PrintReal(RealValue, XiaoshuWeishu);
printf("\n"); return 0; }
void PrintReal(double N, int DecPlaces) { int IntegerPart; double FractionPart; if( N < 0 ) { putchar('-'); N = -N; } N = RoundUp(N, DecPlaces); IntegerPart = IntPart( N ); FractionPart = DecPart( N ); PrintOut(IntegerPart); if(DecPlaces > 0) putchar('.'); PrintFractionPart(FractionPart, DecPlaces); } double RoundUp( double N, int DecPlaces ) { int i; double AmountToAdd = 0.5; for(i = 0;i < DecPlaces; ++i) AmountToAdd *= 0.1; return N + AmountToAdd; } int IntPart(double N) { return (int)N; } double DecPart(double N) { return N - (int)N; }
void PrintFractionPart(double FractionPart, int DecPlaces) { int i, Adigit; printf("|%lf|",FractionPart); for( i = 0; i < DecPlaces; i++ ) { FractionPart *= 10; Adigit = IntPart(FractionPart); PrintDigit(Adigit); FractionPart -= Adigit; } } void PrintDigit (int N) { printf("%d ",N); }
void PrintOut(int N) { if(N>=10) PrintOut (N/10); PrintDigit (N%10); }
|
一道不起眼的填空作业,藏着浮点数最经典的一课:它从来不是精确的小数,只是无限逼近的演员——而 printf 就是它的化妆师。想看素颜,得自己一位一位往外抠。