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;
}
终端
$ 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.1 和 0.2 在二进制里是无限循环小数(就像十进制下 13=0.333... 存不准一样),存进有限的 double 里就有微小误差,两个带误差的数加起来误差还在,于是 0.30000000000000004 不等于 0.3 存出来的那个近似值。

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

char:它就是个小整数 ​

很多人把 char 当成“专门存字符”的类型,这是个善意的误会。char 本质上就是一个 1 字节的整数,它“能存字符”只是因为咱们约定用整数编号来代表字符(ASCII 里 A 是 65、a 是 97、0 是 48)。咱们验证一下:

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

咱们看到 'A' 用 %d 打出来就是 65。'A' + 1 是 66、正好是 '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!),咱们要 float 就得加后缀 F(3.14f)、L 是 long double。科学计数法 1.5e3 == 1500.0。

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

隐式转换:悄悄发生的截断和“整数除法” ​

最后这个,是新手重灾区。当不同类型的值一起运算、或从一个类型赋值给另一个类型时,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;
}
终端
$ 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.14159 变 3,而且这是“可能丢数据”的转换,开 -Wconversion 编译器会警告咱们。最经典的是 double avg = 5 / 2;,您以为“赋给 double,那得算成 2.5 吧”,错!52 的两个操作数都是 int,C 先做整数除法得 2,然后才把这个 2 提升成 2.0 赋给 avg,小数在您看到 double 之前就丢光了。而 5.02 就没事:因为 5.0 是 double,按 usual arithmetic conversions 整个表达式提升成浮点除法,得 2.5,这才是咱们想要的。

把第 3 章和这一章合起来,隐式转换的完整链条是:先做整型提升(char/short 升到 int)、再做 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 long、0x 是十六进制而 0 开头是八进制(010 == 8 这个坑过无数人)。浮点常量默认 double、F 后缀才是 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 章:整型提升(隐式转换链的前半段)
  • 开发环境·第 5 章:编译阶段看汇编(字符串字面量在 .rodata、改它段错误)

87bb5f2 · 87bb5f2 · 2026-09-21