Skip to content

Commit 5878e83

Browse files
feat: better float explanations (#107)
1 parent 99450d4 commit 5878e83

6 files changed

Lines changed: 164 additions & 64 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#include <stdio.h>
2+
#include <string.h>
3+
4+
/* 浮点数为什么是近似值?把 0.1f 的 32 个二进制位摊开看就明白了。 */
5+
int main(void) {
6+
/* 1) float 只有 32 位:1 符号 + 8 指数 + 23 尾数 */
7+
float f = 0.1f;
8+
unsigned int bits;
9+
memcpy(&bits, &f, sizeof(bits));
10+
11+
printf("0.1f 存进 float 的 32 个二进制位 (符号 | 指数 | 尾数):\n ");
12+
for (int i = 31; i >= 0; i--) {
13+
printf("%d", (bits >> i) & 1);
14+
if (i == 31 || i == 23)
15+
printf(" ");
16+
}
17+
printf("\n\n");
18+
19+
/* 尾数 1001100110011... 是无限循环,23 位放不下只能截断,所以不是 0.1 */
20+
printf("用不同精度打印它,都不是 0.1:\n");
21+
printf(" 9 位精度 : %.9f\n", f);
22+
printf(" 20 位精度: %.20f\n\n", f);
23+
24+
/* 2) 经典的 0.1 + 0.2 != 0.3 在 double 下才看得见 */
25+
double a = 0.1, b = 0.2, c = 0.3;
26+
printf("double: 0.1 + 0.2 == 0.3 ? %s\n", (a + b == c) ? "yes" : "no");
27+
printf(" a + b = %.20f\n", a + b);
28+
printf(" c = %.20f\n", c);
29+
30+
return 0;
31+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#include <stdio.h>
2+
#include <string.h>
3+
4+
/* Why are floating-point numbers approximations? Spread out the 32 bits of 0.1f and it becomes
5+
* clear. */
6+
int main(void) {
7+
/* 1) float has only 32 bits: 1 sign + 8 exponent + 23 mantissa */
8+
float f = 0.1f;
9+
unsigned int bits;
10+
memcpy(&bits, &f, sizeof(bits));
11+
12+
printf("32 bits of 0.1f in a float (sign | exponent | mantissa):\n ");
13+
for (int i = 31; i >= 0; i--) {
14+
printf("%d", (bits >> i) & 1);
15+
if (i == 31 || i == 23)
16+
printf(" ");
17+
}
18+
printf("\n\n");
19+
20+
/* The mantissa 1001100110011... repeats forever; 23 bits can't hold it, so it gets truncated.
21+
*/
22+
printf("Printed at different precisions — never exactly 0.1:\n");
23+
printf(" 9 digits : %.9f\n", f);
24+
printf(" 20 digits: %.20f\n\n", f);
25+
26+
/* 2) The classic 0.1 + 0.2 != 0.3 only shows up under double */
27+
double a = 0.1, b = 0.2, c = 0.3;
28+
printf("double: 0.1 + 0.2 == 0.3 ? %s\n", (a + b == c) ? "yes" : "no");
29+
printf(" a + b = %.20f\n", a + b);
30+
printf(" c = %.20f\n", c);
31+
32+
return 0;
33+
}

documents/en/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -69,55 +69,68 @@ long double ld = 3.14L; // 后缀 L 表示 long double
6969

7070
### Floating-point numbers are imprecise — this is not a bug
7171

72-
The most important concept to understand about floating-point numbers is this: **floating-point numbers are approximations, not exact values**. This is because computers use a finite number of binary bits to represent decimal fractions, just as you can only represent 1/3 using a finite number of decimal places — it will always be an approximation.
72+
To understand floating-point numbers, accept one fact first: **they are approximations, not exact values**. It feels counterintuitive — how can the `0.1` you store not equal `0.1`? The answer is hiding inside the binary structure of a floating-point number.
7373

74-
```c
75-
#include <stdio.h>
74+
Start with integers as a warm-up. An `int` has only 32 binary bits, so the numbers it can represent have a fixed ceiling and floor; go past them and you overflow. Yet there are infinitely many integers, and the computer just picks one finite segment to represent. Floating-point numbers face the exact same problem: there are infinitely many real numbers, many with infinitely many fractional digits, and a `float` still only gets 32 bits. How does it cover such a vast range with so few bits? The same way you would on paper — scientific notation.
7675

77-
int main(void)
78-
{
79-
float a = 0.1f;
80-
float b = 0.2f;
81-
if (a + b == 0.3f) {
82-
printf("equal\n");
83-
} else {
84-
printf("not equal: %.9f\n", a + b);
85-
}
86-
return 0;
87-
}
88-
```
76+
Decimal scientific notation writes `0.0123` as `1.23 × 10⁻²`, a mantissa times a power of 10. A computer does the same thing, only with base 2: `number = (±1) × mantissa × 2^exponent`. The 32 bits of a `float` are split into exactly these three parts:
8977

90-
Let's verify this by compiling and running the code:
78+
| 1 bit sign | 8 bits exponent | 23 bits mantissa |
79+
|---|---|---|
80+
| Positive or negative | How large or small the number can get (range) | How many significant digits you get (precision) |
9181

92-
```bash
93-
gcc -Wall -Wextra -std=c17 float_demo.c -o float_demo && ./float_demo
94-
```
82+
The exponent bits control magnitude, the mantissa bits control fine detail, and both are finite. The exponent lets you reach as high as `10³⁸` and as low as `10⁻⁴⁵`; but with only 23 mantissa bits, you get roughly 7 decimal digits of precision — anything beyond that cannot be stored.
9583

96-
**Output:**
84+
The real trouble lives in those 23 mantissa bits. The decimal `0.1`, written in binary, is `0.0001100110011...` — an infinitely repeating fraction. The mantissa field has only 23 bits, so it cannot hold the infinite repetition and must truncate. What gets stored is no longer `0.1`. The little program below prints out the raw 32 bits of `0.1f`; click "Try it yourself" to run it directly:
85+
86+
<OnlineCompilerDemo
87+
title="What 0.1 actually looks like inside a float"
88+
source-path="code/examples/vol1/c_float_representation_en.c"
89+
description="Print the 32 bits of 0.1f (sign | exponent | mantissa), then see why 0.1 + 0.2 does not equal 0.3 under double."
90+
allow-run
91+
run-compiler="cg132"
92+
run-options="-O2 -std=c17"
93+
/>
94+
95+
The key lines of output look like this:
9796

9897
```text
99-
not equal: 0.300000012
98+
32 bits of 0.1f in a float (sign | exponent | mantissa):
99+
0 01111011 10011001100110011001101
100+
101+
Printed at different precisions — never exactly 0.1:
102+
9 digits : 0.100000001
103+
20 digits: 0.10000000149011611938
104+
105+
double: 0.1 + 0.2 == 0.3 ? no
106+
a + b = 0.30000000000000004441
107+
c = 0.29999999999999998890
100108
```
101109

102-
See? `0.1 + 0.2` does not equal `0.3` in floating-point arithmetic. This is not a compiler bug; it is an inherent characteristic of the IEEE 754 floating-point standard. Therefore, **never use `==` to compare floating-point numbers**. The correct approach is to use a small epsilon value to determine "approximate equality":
110+
Look at the first line: the sign bit is `0` (positive), the exponent is `01111011`, and the mantissa is `10011001100110011001101` — that run of `1001 1001 1001...` is the infinite repetition truncated to 23 bits, with the final `1` left over from rounding. That is why `0.1f` prints as `0.10000000149011611938`, not `0.1`.
111+
112+
The `double` part is even clearer: `0.1 + 0.2` comes out to `0.30000000000000004441`, while `0.3` stored is `0.29999999999999998890`. Two numbers that were never equal to begin with, compared with `==`, will of course not be equal. There is no mystery here: a finite mantissa cannot hold an infinitely repeating fraction, so this is inevitable.
113+
114+
::: warning
115+
Never compare floating-point numbers with `==`. `0.1 + 0.2 != 0.3` is the norm in floating-point arithmetic, not a bug. Checking for "approximate equality" with an epsilon is the correct fix.
116+
:::
117+
118+
One related trap: under `float`, `0.1f + 0.2f` happens to equal `0.3f`. Don't conclude that `float` is somehow more "accurate" — the 23-bit mantissa is simply too coarse, so the error gets rounded away. Switch to the higher-precision `double` and the error has nowhere to hide. Always use this kind of approximate comparison for equality checks:
103119

104120
```c
105121
#include <math.h>
106122

107-
/// @brief 判断两个 float 是否近似相等
108-
/// @param a 第一个浮点数
109-
/// @param b 第二个浮点数
110-
/// @return 1 表示近似相等,0 表示不相等
123+
/// @brief Check whether two floats are approximately equal
124+
/// @param a the first float
125+
/// @param b the second float
126+
/// @return 1 if approximately equal, 0 otherwise
111127
int float_equal(float a, float b)
112128
{
113129
return fabsf(a - b) < 1e-6f;
114130
}
115131
```
116132
117-
> ⚠️ **Warning**
118-
> Never compare floating-point numbers using `==`. In floating-point arithmetic, `0.1 + 0.2 != 0.3` is the norm, not a bug. Using epsilon to check for approximate equality is the correct approach.
119-
120-
There is one more detail: when we write `float f = 0.1;`, the literal `0.1` is first treated as a `double` and then truncated to `float`—this might introduce additional precision differences. If we intend to use `float`, we should get into the habit of adding the `f` suffix.
133+
One more detail: when you write `float f = 0.1;`, the `0.1` is first treated as a `double` and then truncated to `float`, which introduces an extra precision difference. If you really mean to use `float`, get into the habit of adding the `f` suffix.
121134
122135
### Floating-Point in Embedded Systems
123136
@@ -311,9 +324,9 @@ Predict the output of the following code, then compile and run it to verify your
311324

312325
int main(void)
313326
{
314-
float a = 0.1f;
315-
float b = 0.2f;
316-
float c = 0.3f;
327+
double a = 0.1;
328+
double b = 0.2;
329+
double c = 0.3;
317330

318331
printf("a + b == c? %s\n", (a + b == c) ? "yes" : "no");
319332
printf("a + b = %.20f\n", a + b);

documents/en/vol1-fundamentals/c_tutorials/index.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Comprehensive C Language Tutorial
22

3-
If you are planning to learn or revisit C for whatever reason, and find the C crash course in the parent directory too fast-paced, consider this tutorial instead. It is more detailed and gradual than the crash course, and you can still pick and choose topics based on your interests.
3+
PS: This part of the tutorial is not aimed at complete beginners. It grew out of a set of C notes taken back when working in embedded development, by which point C was already well in hand. So if you actually need to learn C from scratch, head over to this repo instead:
4+
5+
> [GitHub: C Journey](https://github.com/Awesome-Embedded-Learning-Studio/C-Journey)
6+
> [Website: C Journey](https://awesome-embedded-learning-studio.github.io/C-Journey/)
7+
8+
The C tutorials here are better suited for people who once learned C but have forgotten what it looks like.
49

510
## Fundamentals
611

documents/vol1-fundamentals/c_tutorials/02B-float-char-const-cast.md

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -61,37 +61,53 @@ long double ld = 3.14L; // 后缀 L 表示 long double
6161

6262
### 浮点数是不精确的——这不是 bug
6363

64-
这是理解浮点数最重要的一个认知**浮点数是近似值,不是精确值**原因在于计算机用有限的二进制位来表示十进制小数,就像你用有限的小数位来表示 1/3 一样——永远只能近似
64+
理解浮点数,先认下一件事**它是近似值,不是精确值**这事反直觉——存进去的 `0.1` 凭什么就不等于 `0.1`?答案藏在浮点数内部的二进制结构里
6565

66-
```c
67-
#include <stdio.h>
66+
先用整数垫个底。一个 `int` 只有 32 个二进制位,能表示的数有上限也有下限,超过就溢出。可数学里的整数有无穷多个,计算机只取了其中一段。浮点数面对的是同一个问题:实数有无穷多个,不少还带无穷位小数,而 `float` 也只有 32 个位。它拿什么去盖住这么大的范围?靠的是和你纸上演算一样的办法——科学计数法。
6867

69-
int main(void)
70-
{
71-
float a = 0.1f;
72-
float b = 0.2f;
73-
if (a + b == 0.3f) {
74-
printf("equal\n");
75-
} else {
76-
printf("not equal: %.9f\n", a + b);
77-
}
78-
return 0;
79-
}
80-
```
68+
十进制科学计数法把 `0.0123` 写成 `1.23 × 10⁻²`,一个尾数乘以 10 的幂。计算机干的事一样,只是底换成 2:`小数 = (±1) × 尾数 × 2^指数``float` 的 32 个位就照这三块切:
8169

82-
来验证一下,编译运行:
70+
| 1 位符号 | 8 位指数 | 23 位尾数 |
71+
|---|---|---|
72+
| 正还是负 | 决定能表示多大、多小(范围) | 决定有多少位有效数字(精度) |
8373

84-
```bash
85-
gcc -Wall -Wextra -std=c17 float_demo.c -o float_demo && ./float_demo
86-
```
74+
指数位管量级,尾数位管精细度,两个都有限。指数让你能写到 `10³⁸` 那么大、也能小到 `10⁻⁴⁵`;可尾数只有 23 位,有效数字大概就 7 位十进制,再多存不下。
8775

88-
运行结果:
76+
真正的麻烦出在尾数那 23 格。十进制的 `0.1` 换成二进制是 `0.0001100110011...`,一个无限循环小数。尾数位只有 23 格,放不下无限循环,只能截断——存进去的早不是 `0.1` 了。下面这段小程序把 `0.1f` 的 32 个二进制位原样打印出来,点"动手试一试"就能直接跑:
77+
78+
<OnlineCompilerDemo
79+
title="0.1 在浮点数里到底长什么样"
80+
source-path="code/examples/vol1/c_float_representation.c"
81+
description="把 0.1f 的 32 个二进制位(符号|指数|尾数)打印出来,再看 double 下 0.1 + 0.2 为什么不等于 0.3。"
82+
allow-run
83+
run-compiler="cg132"
84+
run-options="-O2 -std=c17"
85+
/>
86+
87+
关键几行输出长这样:
8988

9089
```text
91-
not equal: 0.300000012
90+
0.1f 存进 float 的 32 个二进制位 (符号 | 指数 | 尾数):
91+
0 01111011 10011001100110011001101
92+
93+
用不同精度打印它,都不是 0.1:
94+
9 位精度 : 0.100000001
95+
20 位精度: 0.10000000149011611938
96+
97+
double: 0.1 + 0.2 == 0.3 ? no
98+
a + b = 0.30000000000000004441
99+
c = 0.29999999999999998890
92100
```
93101

94-
看到了吧——`0.1 + 0.2` 在浮点运算中并不等于 `0.3`。这不是编译器的 bug,是 IEEE 754 浮点标准的固有特性。所以,**永远不要用 `==` 比较浮点数**。正确的做法是用一个小的 epsilon 值来判断"近似相等":
102+
看第一行:符号位是 `0`(正数),指数 `01111011`,尾数 `10011001100110011001101`——那串 `1001 1001 1001...` 就是无限循环被截到 23 位的样子,末尾那个 `1` 是舍入进位留下的。所以 `0.1f` 打印出来是 `0.10000000149011611938`,不是 `0.1`
103+
104+
`double` 那段更直观:`0.1 + 0.2` 算出来是 `0.30000000000000004441`,而 `0.3` 存进去是 `0.29999999999999998890`,两个本来就不等的数拿 `==` 比,自然不相等。原因不神秘:有限位尾数存不下无限循环小数,必然如此。
105+
106+
::: warning
107+
永远不要用 `==` 比较浮点数。`0.1 + 0.2 != 0.3` 在浮点运算里是常态,不是 bug。用 epsilon 判断近似相等才是正解。
108+
:::
109+
110+
顺带一个坑:`float``0.1f + 0.2f` 恰好等于 `0.3f`。别因此觉得 `float` 更"准"——只是 23 位尾数精度太低,把误差一起舍没了。换成精度更高的 `double`,误差就藏不住。判等一律用下面这种近似比较:
95111

96112
```c
97113
#include <math.h>
@@ -106,10 +122,7 @@ int float_equal(float a, float b)
106122
}
107123
```
108124
109-
> ⚠️ **踩坑预警**
110-
> 永远不要用 `==` 比较浮点数。`0.1 + 0.2 != 0.3` 在浮点运算中是常态,不是 bug。用 epsilon 判断近似相等才是正确做法。
111-
112-
还有一个细节:写 `float f = 0.1;` 的时候,`0.1` 先被当作 `double` 处理,然后截断为 `float`——这可能会引入额外的精度差异。如果确定要用 `float`,养成加 `f` 后缀的习惯。
125+
还有一个细节:写 `float f = 0.1;` 时,`0.1` 先按 `double` 处理再截成 `float`,会引入额外的精度差异。确定用 `float` 就养成加 `f` 后缀的习惯。
113126
114127
### 嵌入式里的浮点
115128
@@ -303,9 +316,9 @@ C++ 在类型系统上做了大量的安全加固,很多改进直接瞄准了
303316

304317
int main(void)
305318
{
306-
float a = 0.1f;
307-
float b = 0.2f;
308-
float c = 0.3f;
319+
double a = 0.1;
320+
double b = 0.2;
321+
double c = 0.3;
309322

310323
printf("a + b == c? %s\n", (a + b == c) ? "yes" : "no");
311324
printf("a + b = %.20f\n", a + b);

documents/vol1-fundamentals/c_tutorials/index.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# C 语言系统教程
22

3-
如果你是因为一些原因打算学习或重修 C 语言,发现上一级目录的 C 语言速通教程节奏太紧,可以考虑这里的教程。它比速通教程更加详细,循序渐进,同样可以根据自己的喜好挑感兴趣的方向学习。
3+
PS: 这部分教程不是面向0基础的朋友的,本教程的原型是笔者曾今搞嵌入式的C语言笔记,当时写笔记的时候就已经掌握了C语言,所以如果存在C语言的学习需求,左转到这个仓库:
4+
5+
> [Github C语言的旅程](https://github.com/Awesome-Embedded-Learning-Studio/C-Journey)
6+
> [网站 C Journey](https://awesome-embedded-learning-studio.github.io/C-Journey/)
7+
8+
这里的C语言教程更偏向于曾学习过C但是忘记C长啥样的朋友看的。
49

510
## 基础篇
611

0 commit comments

Comments
 (0)