浮点、字符、常量与隐式转换:0.1 + 0.2 为什么不等于 0.3
引言:整型之外,还有一堆坑等着你
前两章把整型翻了个底朝天。这一章看 C 里另外几样东西——浮点(float/double)、字符(char)、各种常量的写法,以及最要命的隐式类型转换。它们各自都有「反直觉」的角落:浮点天生算不准、char 其实是个整数、你写出来的常量字面量有它自己的类型规则,而隐式转换更是会在你眼皮底下悄悄截断数据、悄悄把整数除法塞给你。我们一个个真跑。
浮点: 的名场面
先看浮点。C 的浮点类型主要是 float(单精度,4 字节)和 double(双精度,8 字节),更大的还有 long double。它们的大小和「有效数字位数」可以用 <float.h> 的宏看:
#include <float.h>
#include <stdio.h>
int main(void) {
float f = 0.1f;
double d = 0.1;
printf("sizeof(float) = %zu 精度 %d 位\n", sizeof(f), FLT_DIG);
printf("sizeof(double) = %zu 精度 %d 位\n", sizeof(d), DBL_DIG);
printf("0.1 + 0.2 = %.17f (double)\n", 0.1 + 0.2);
printf("0.1 + 0.2 == 0.3 ? %s\n", (0.1 + 0.2 == 0.3) ? "是" : "否(浮点不等!)");
return 0;
}$ gcc -std=c11 -Wall floatprec.c -o fp && ./fp
sizeof(float) = 4 精度 6 位
sizeof(double) = 8 精度 15 位
0.1 + 0.2 = 0.30000000000000004 (double)
0.1 + 0.2 == 0.3 ? 否(浮点不等!)float 只有约 6 位有效数字、double 约 15 位(FLT_DIG/DBL_DIG,§5.2.4.2.2)。最扎眼的是那个 0.1 + 0.2 == 0.3 居然为假! 这不是 bug,是二进制浮点的天生缺陷:double 里就有微小误差,两个带误差的数加起来误差还在,于是
这个坑的推论是铁律一条:永远别用 == 直接比两个浮点数。要比,就用「差的绝对值小于某个容差」——fabs(a - b) < 1e-9 这种写法。涉及钱的系统、物理计算里,直接 == 比浮点迟早出事(这事儿工业界吃过的大亏能写一本书)。另外,需要精确小数(比如财务)时,别用浮点,要么用整数(以「分」为单位算)、要么用专门的十进制库——浮点是用来算「近似值」的,不是用来算「精确账」的。
char:它就是个小整数
很多人把 char 当成「专门存字符」的类型,这是个善意的误会。char 本质上就是一个 1 字节的整数,它「能存字符」只是因为我们约定用整数编号来代表字符(ASCII 里 A 是 a 是 0 是
#include <stdio.h>
int main(void) {
char c = 'A';
printf("'A' = %d ← char 就是个小整数\n", c);
printf("'A' + 1 = %d = '%c'\n", c + 1, c + 1);
printf("'a'-'A' = %d ← 大小写正好差 32\n", 'a' - 'A');
return 0;
}$ gcc -std=c11 -Wall charint.c -o ci && ./ci
'A' = 65 ← char 就是个小整数
'A' + 1 = 66 = 'B'
'a'-'A' = 32 ← 大小写正好差 32'A' 用 %d 打出来就是 'A' + 1 是 'B';'a' - 'A' = 32,所以大小写转换可以靠「±32」来做('A' + 32 == 'a',这是 C 里手写大小写转换的常见手法)。正因为 char 是整数,它能参与第 3 章那些算术运算、整型提升('A' + 1 提升成 int 算)。
这里有个实现定义的坑要记一笔:「普通 char」到底是有符号还是无符号,标准没定死(§6.2.5 第 15 段),由实现决定——x86 上通常是 signed char(范围 −128..127)、ARM 上常常是 unsigned char(0..255)。所以你如果把一个大于 127 的字节塞进 char 再当 int 用,在有符号的机器上可能变成负数、无符号的机器上还是正数,行为不一样。处理原始字节(比如二进制数据、UTF-8)时,更稳妥的是显式用 unsigned char,别让「普通 char 的符号性」来给你添乱。
常量:你写的字面量,类型是有讲究的
你代码里直接写的 42、3.14、'A'、"hi" 这些「字面量」,每个都有自己的类型规则(§6.4.4),踩不准会出怪事。
整型常量:默认十进制,0x 开头是十六进制(0x10 == 16)、0 开头是八进制(010 == 8,这个坑过不少人——你想写「十进制的 10」,写成 010 它当成 8)。整型常量的类型会根据它的值的大小和有无后缀自动挑:一个十进制常量默认是 int,值装不下 int 就升 long、再升 long long;后缀 U/L/UL/ULL 可以指定(100U 是 unsigned、100L 是 long)。这个「按值挑类型」偶尔会咬人——比如 0xFFFFFFFF 在 int 是 32 位的机器上装不下有符号 int,会被当成 unsigned int,于是 0xFFFFFFFF < 0 这种比较可能给你假(无符号怎么会小于 0)。
浮点常量:默认是 double(3.14 是 double,不是 float!),后缀 F 才是 float(3.14f)、L 是 long double。科学计数法 1.5e3 == 1500.0。
字符常量:单引号 'A',类型是 int(注意,不是 char——C 里 'A' 的类型是 int,这又是个反直觉的点)。转义序列:\n(换行)、\t(制表)、\0(空字符,C 字符串结尾)、\\(反斜杠本身)、\'(单引号)等。字符串字面量用双引号 "hi",它的类型是「字符数组」、存在只读的 .rodata 段、末尾自动补一个 \0——这部分阶段 0 第 5 章已经讲过(尝试改字符串字面量会段错误),这里不重复。
隐式转换:悄悄发生的截断和「整数除法」
最后这个,是新手重灾区。当不同类型的值一起运算、或从一个类型赋值给另一个类型时,C 会自动做隐式转换(§6.3),而它常常不是你想要的方向:
#include <stdio.h>
int main(void) {
double pi = 3.14159;
int truncated = pi; /* double → int:直接截断小数 */
double avg = 5 / 2; /* 坑!5/2 是整数除法 = 2,再赋给 double */
double avg2 = 5.0 / 2; /* 有一个 double,整个提升成浮点除法 */
printf("pi = %f → int = %d\n", pi, truncated);
printf("5 / 2 = %f ← 整数除法,小数直接丢了!\n", avg);
printf("5.0 / 2 = %f\n", avg2);
return 0;
}$ gcc -std=c11 -Wall implicitcast.c -o ic && ./ic
pi = 3.141590 → int = 3
5 / 2 = 2.000000 ← 整数除法,小数直接丢了!
5.0 / 2 = 2.500000三个坑一次看全。第一,int truncated = pi;:把 double 赋给 int,小数部分被直接截断(不是四舍五入!)-Wconversion 编译器会警告你。第二,那个最经典的 double avg = 5 / 2;——你以为「赋给 double,那得算成 2.5 吧」,错!int,C 先做整数除法得 avg。小数在你看到 double 之前就丢光了。第三,double,按 usual arithmetic conversions 整个表达式提升成浮点除法,得
把第 3 章和这一章合起来,隐式转换的完整链条是:先做整型提升(char/short → int)、再做 usual arithmetic conversions(两个操作数往「更宽/更精确」的类型对齐,int+double 都变 double)、赋值时再做一次「向左值类型」的转换(可能截断)。这一套自动转换能帮你写出简洁的代码,但也随时能给你埋雷——拿不准时,显式写转换((double)5 / 2)把意图写明,比让编译器替你猜强。
小结
浮点用二进制存,天生存不准 0.1 这种数,所以 0.1 + 0.2 != 0.3(真跑出 == 比浮点,要用「差小于容差」比;要精确小数(财务)就别用浮点,用整数或十进制库。float 约 6 位精度、double 约 15 位。char 本质是 1 字节整数('A' == 65,大小写差 32,所以 'A'+32 == 'a'),但「普通 char」的符号性是实现定义的(x86 常是 signed、ARM 常是 unsigned),处理原始字节用 unsigned char 更稳。常量各有类型:整型默认 int、按值升 long/long long、0x 是十六进制而 0 开头是八进制(010 == 8 这个坑过无数人);浮点常量默认 double、F 后缀才是 float;字符常量 'A' 的类型其实是 int。隐式转换是新手重灾区:int = double 会截断小数、double 之前小数就丢了,要 (double)。这一章和第 3 章合起来,就是 C「自动转换」的全部规矩——后面写任何算术、比较,都绕不开它们。
参考资源
- ISO/IEC 9899:2011 §6.2.5(类型:浮点、
char的符号性)、§5.2.4.2.2(<float.h>:FLT_DIG/DBL_DIG)、§5.2.1(执行字符集,ASCII 编码)、§6.4.4(常量:整型/浮点/字符)、§6.3(隐式转换:整型提升 + usual arithmetic conversions + 赋值转换) - 第 3 章:整型提升(隐式转换链的前半段)
- 阶段 0 · 第 5 章:编译阶段看汇编(字符串字面量在
.rodata、改它段错误)