Skip to content

浮点、字符、常量与隐式转换:0.1 + 0.2 为什么不等于 0.3

引言:整型之外,还有一堆坑等着你

前两章把整型翻了个底朝天。这一章看 C 里另外几样东西——浮点float/double)、字符char)、各种常量的写法,以及最要命的隐式类型转换。它们各自都有「反直觉」的角落:浮点天生算不准、char 其实是个整数、你写出来的常量字面量有它自己的类型规则,而隐式转换更是会在你眼皮底下悄悄截断数据、悄悄把整数除法塞给你。我们一个个真跑。

浮点:0.1+0.2 的名场面

先看浮点。C 的浮点类型主要是 float(单精度,4 字节)和 double(双精度,8 字节),更大的还有 long double。它们的大小和「有效数字位数」可以用 <float.h> 的宏看:

c
#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;
}
text
$ 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.30000000000000004——0.1 + 0.2 == 0.3 居然为假! 这不是 bug,是二进制浮点的天生缺陷:0.10.2 在二进制里是无限循环小数(就像十进制下 13=0.333... 存不准一样),存进有限的 double 里就有微小误差,两个带误差的数加起来误差还在,于是 0.30000000000000004 不等于 0.3 存出来的那个近似值。

这个坑的推论是铁律一条:永远别用 == 直接比两个浮点数。要比,就用「差的绝对值小于某个容差」——fabs(a - b) < 1e-9 这种写法。涉及钱的系统、物理计算里,直接 == 比浮点迟早出事(这事儿工业界吃过的大亏能写一本书)。另外,需要精确小数(比如财务)时,别用浮点,要么用整数(以「分」为单位算)、要么用专门的十进制库——浮点是用来算「近似值」的,不是用来算「精确账」的。

char:它就是个小整数

很多人把 char 当成「专门存字符」的类型,这是个善意的误会。char 本质上就是一个 1 字节的整数,它「能存字符」只是因为我们约定用整数编号来代表字符(ASCII 里 A65a97048)。验证一下:

c
#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;
}
text
$ gcc -std=c11 -Wall charint.c -o ci && ./ci
'A'       = 65   ← char 就是个小整数
'A' + 1   = 66 = 'B'
'a'-'A'   = 32   ← 大小写正好差 32

'A'%d 打出来就是 65'A' + 166、正好是 '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 的符号性」来给你添乱。

常量:你写的字面量,类型是有讲究的

你代码里直接写的 423.14'A'"hi" 这些「字面量」,每个都有自己的类型规则(§6.4.4),踩不准会出怪事。

整型常量:默认十进制,0x 开头是十六进制(0x10 == 16)、0 开头是八进制(010 == 8,这个坑过不少人——你想写「十进制的 10」,写成 010 它当成 8)。整型常量的类型会根据它的值的大小有无后缀自动挑:一个十进制常量默认是 int,值装不下 int 就升 long、再升 long long;后缀 U/L/UL/ULL 可以指定(100Uunsigned100Llong)。这个「按值挑类型」偶尔会咬人——比如 0xFFFFFFFFint 是 32 位的机器上装不下有符号 int,会被当成 unsigned int,于是 0xFFFFFFFF < 0 这种比较可能给你假(无符号怎么会小于 0)。

浮点常量:默认是 double3.14double,不是 float!),后缀 F 才是 float3.14f)、Llong double。科学计数法 1.5e3 == 1500.0。

字符常量:单引号 'A',类型是 int(注意,不是 char——C 里 'A' 的类型是 int,这又是个反直觉的点)。转义序列:\n(换行)、\t(制表)、\0(空字符,C 字符串结尾)、\\(反斜杠本身)、\'(单引号)等。字符串字面量用双引号 "hi",它的类型是「字符数组」、存在只读的 .rodata 段、末尾自动补一个 \0——这部分阶段 0 第 5 章已经讲过(尝试改字符串字面量会段错误),这里不重复。

隐式转换:悄悄发生的截断和「整数除法」

最后这个,是新手重灾区。当不同类型的值一起运算、或从一个类型赋值给另一个类型时,C 会自动做隐式转换(§6.3),而它常常不是你想要的方向:

c
#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;
}
text
$ 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,小数部分被直接截断(不是四舍五入!)3.141593,而且这是「可能丢数据」的转换,开 -Wconversion 编译器会警告你。第二,那个最经典的 double avg = 5 / 2;——你以为「赋给 double,那得算成 2.5 吧」,错!52 的两个操作数都是 int,C 先做整数除法2然后才把这个 2 提升成 2.0 赋给 avg。小数在你看到 double 之前就丢光了。第三,5.02:因为 5.0double,按 usual arithmetic conversions 整个表达式提升成浮点除法,得 2.5——这才是你想要的。

把第 3 章和这一章合起来,隐式转换的完整链条是:先做整型提升char/shortint)、再做 usual arithmetic conversions(两个操作数往「更宽/更精确」的类型对齐,int+double 都变 double)、赋值时再做一次「向左值类型」的转换(可能截断)。这一套自动转换能帮你写出简洁的代码,但也随时能给你埋雷——拿不准时,显式写转换(double)5 / 2)把意图写明,比让编译器替你猜强。

小结

浮点用二进制存,天生存不准 0.1 这种数,所以 0.1 + 0.2 != 0.3(真跑出 0.30000000000000004)——永远别用 == 比浮点,要用「差小于容差」比;要精确小数(财务)就别用浮点,用整数或十进制库。float 约 6 位精度、double 约 15 位。char 本质是 1 字节整数('A' == 65,大小写差 32,所以 'A'+32 == 'a'),但「普通 char」的符号性是实现定义的(x86 常是 signed、ARM 常是 unsigned),处理原始字节用 unsigned char 更稳。常量各有类型:整型默认 int、按值升 long/long long0x 是十六进制而 0 开头是八进制010 == 8 这个坑过无数人);浮点常量默认 doubleF 后缀才是 float;字符常量 'A' 的类型其实是 int。隐式转换是新手重灾区:int = double截断小数、52整数除法得 2(赋给 double 之前小数就丢了,要 5.02 才得 2.5),拿不准就显式写转换 (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、改它段错误)