📌 更新(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 开头就被摘下来单独处理,打印前再挂回去)。

完整源码

四舍五入.c
1
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
// 英文教材P12作业1.3:打印double型任意实数
// 考虑了舍入误差(四舍五入)
// 只使用处理I/O的PrintDigit函数
#include<stdio.h>

void PrintReal(double N, int DecPlaces); // 打印实数N,保留其后DecPlaces位
double RoundUp( double N, int DecPlaces ); // 实现四舍五入的函数

int IntPart(double N); // 得到N的整数部分
double DecPart(double N); // 得到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); //输出实数value及其小数点后3位并四舍五入

printf("\n");

return 0;
}

void PrintReal(double N, int DecPlaces)// 打印实数N,保留其后DecPlaces位
{
int IntegerPart;
double FractionPart;

if( N < 0 ) //将实数N处理成正实数,输出前加“-”号
{
putchar('-');
N = -N;
}

N = RoundUp(N, DecPlaces); // 对实数N按要求实现四舍五入
IntegerPart = IntPart( N ); // 得到N的整数部分
FractionPart = DecPart( N ); // 得到N的小数部分

PrintOut(IntegerPart); // 打印N的整数部分
if(DecPlaces > 0)
putchar('.'); // 打印小数点
PrintFractionPart(FractionPart, DecPlaces);
}

double RoundUp( double N, int DecPlaces ) // 画龙点睛部分:四舍五入处理
{
int i;
double AmountToAdd = 0.5;

// 请在此添加代码,完成四舍五入
/********** Begin *********/
for(i = 0;i < DecPlaces; ++i) AmountToAdd *= 0.1;

/********** End **********/
return N + AmountToAdd;
}

int IntPart(double N) // 得到N的整数部分
{
return (int)N;
}

double DecPart(double N) // 得到N的小数部分
{
// 请在此添加代码,取得N的小数部分
/********** Begin *********/
return N - (int)N;

/********** End **********/
}

/*
打印小数部分
*/

void PrintFractionPart(double FractionPart, int DecPlaces) //如小数部分是0.123,保留3位小数
{
int i, Adigit;
printf("|%lf|",FractionPart);
for( i = 0; i < DecPlaces; i++ )
{
FractionPart *= 10; // 第一次循环:小数部分乘以10,0.123*10=1.23
Adigit = IntPart(FractionPart);// 第一次循环:取1.23的整数1
PrintDigit(Adigit); // 第一次循环:递归打印整数1

// 请在此添加代码,为参与下一次循环处理做好准备
/********** Begin *********/
FractionPart -= Adigit;

/********** End **********///
}
}

void PrintDigit (int N)
{
printf("%d ",N);
}

void PrintOut(int N) //教材P10例子
{
if(N>=10) /*1*/
PrintOut (N/10); /*2分离整数N的高位*/
PrintDigit (N%10); /*3打印单个位数*/
}

一道不起眼的填空作业,藏着浮点数最经典的一课:它从来不是精确的小数,只是无限逼近的演员——而 printf 就是它的化妆师。想看素颜,得自己一位一位往外抠。