C 语言中的每个变量都有一个类型,在声明时选定,并在整个生命周期内保持不变。类型决定了三件事:变量占多少字节,这些字节如何解读,以及对它进行哪些操作才有意义。
C 语言的类型列表很短,复杂之处在于修饰符。
四种基础类型
char占一个字节。它用于单个字符('A')、字符串中的字节,有时也当作很小的整数使用。int是最常用的整数类型。循环计数器、大小、ID 都用它。float和double存放实数。double的精度大约是 float 的两倍。void在某种意义上是第四种类型,但它表示“没有值”:你不能声明void变量,只能把它用作函数返回类型、空参数列表或无类型指针。
注意 3.14f 末尾的 f 后缀。没有它,这个字面量就是 double,把它赋给 float 会悄悄地损失精度。有些编译器会对此发出警告。
修饰符:short、long、unsigned
基础整数类型可以搭配修饰符,用来改变大小或符号。
每种整数类型都需要自己的格式说明符:short 用 %hd,int 用 %d,long 用 %ld,long long 用 %lld,unsigned 用 %u。用错了是未定义行为,而不是舍入误差。
unsigned 去掉符号位,使正数范围扩大一倍。unsigned int 能存放 0 到约 43亿,而不是 -21亿到 +21亿。对于确实不可能为负的东西(大小、字节数、位模式),它是正确的选择;而对于任何可能被减到零以下的值,它就是一个陷阱。
int、short、long 和 long long 默认都是 signed,所以你几乎从不写它。唯一需要注意的是 char:普通的 char 是有符号还是无符号,由实现定义,所以当符号很重要时,请写 signed char 或 unsigned char。
大小:你实际得到的是什么
C 标准规定的是最小值,而不是确切的大小。在任何现代 64 位 Linux、macOS 或 Windows 机器上,你会看到这样的结果:
| 类型 | 常见大小 | 常见范围 |
|---|---|---|
char | 1 字节 | -128 至 127(或 0 至 255) |
short | 2 字节 | -32,768 至 32,767 |
int | 4 字节 | -2,147,483,648 至 2,147,483,647 |
long | 8 字节(Windows 上为 4) | 约 ±9.2×10¹⁸ |
long long | 8 字节 | 约 ±9.2×10¹⁸ |
float | 4 字节 | 约 7 位有效数字 |
double | 8 字节 | 约 15 位有效数字 |
long double | 16 字节(因平台而异) | 比 double 更多 |
最容易让人上当的是 long 这一行:它在 Linux 和 macOS 上是 8 字节,在 64 位 Windows 上是 4 字节。假定 long 能存放 64 位值的代码是不可移植的。当大小本身就是需求的一部分时,请使用 long long,或者 stdint.h 中的定宽类型(int32_t、uint64_t)。
sizeof:问编译器
永远不要猜测大小,去测量它:
sizeof 是运算符而不是函数,它在编译时求值。它的结果是 size_t 类型,用 %zu 打印。
sizeof(char) 保证恰好是 1,这就是 C 语言中“字节”的定义。其他所有类型的大小都以它为单位来衡量。
limits.h 和 float.h
你的编译器的确切范围都以具名常量的形式提供:
当你需要知道某个运算会不会溢出时,就和这些值比较。在计算 a + b 之前先检查 if (a > INT_MAX - b),就能在溢出发生之前发现它。这一点很重要,因为有符号溢出发生之后是无法检测的。
整数溢出
当值超出类型的范围时会发生什么,完全取决于它的符号。
无符号溢出是有定义的:值按 2^N 取模回绕。
第二种情况是 bug 的真正来源。像 for (unsigned i = n - 1; i >= 0; i--) 这样的循环永远不会结束,因为无符号值永远 >= 0。
有符号溢出是未定义行为。 不是“回绕”,而是未定义。编译器可以假定它永远不会发生,并据此进行优化,这意味着事后才写的溢出检查可能会被删掉:
int sum = a + b;
if (sum < a) { /* the compiler may remove this entirely */ }
要事先检查,借助这些范围常量:
if (b > 0 && a > INT_MAX - b) {
/* a + b would overflow - handle it */
}
浮点精度
float 和 double 以二进制存储数字,而大多数十进制小数都没有精确的二进制形式,就像 1/3 没有精确的十进制形式一样。
由此得出的规则是:永远不要用 == 比较浮点数。改为比较差的绝对值是否小于一个很小的容差。
也永远不要用浮点数表示金额。把分存成整数:用 long long 存放的分是精确的,用 double 存放的元却不是。
选择类型
一份涵盖大部分代码的简短决策清单:
- 整数:没有特别理由就用
int。它是 CPU 处理起来最高效的类型,所有算术规则也都围绕它建立。 - 超过 20亿的数:
long long,或stdint.h中的int64_t。 - 来自
sizeof或strlen的大小、长度、数组下标:size_t。它是无符号的,并保证足以表示任何对象的大小。 - 小数:
double。只有在大数组中需要节省一半内存,或者在没有双精度运算单元的嵌入式硬件上,才用float。 - 单个字符和原始字节:文本用
char,二进制数据用unsigned char。 - 真/假:
stdbool.h中的bool,参见 C 语言布尔值。 - 确切的位宽(文件格式、网络协议、硬件寄存器):
stdint.h中的uint8_t、int16_t、uint32_t等。
混合类型
当你在一个表达式中组合两种不同的类型时,C 语言会在做算术运算之前悄悄地转换它们。这通常有帮助,偶尔却是灾难:
第一个是整数除法:两个操作数都是 int,所以结果也是 int,小数部分被丢掉了。第三个更糟:把有符号值和无符号值比较时,有符号值会被转换成无符号值,于是 -1 变成了一个巨大的正数。
这些转换规则,以及如何用显式类型转换掌控它们,是 C 语言类型转换要讲的内容。
常见问题
C 语言有哪些基本数据类型?
四种基础类型:char 用于单个字符和字节,int 用于整数,float 和 double 用于小数。修饰符会改变它们的大小和符号(short、long、long long、signed 和 unsigned),从而组合出完整的类型集合。
C 语言中 int 占几个字节?
在现代桌面和服务器系统上几乎都是 4 字节(32 位),范围大约是 -21亿到 21亿。标准只保证至少 2 字节,16 位单片机上确实就是 2 字节。如果需要确切知道,请使用 sizeof(int)。
C 语言中 float 和 double 有什么区别?
float 占 4 字节,大约有 7 位有效十进制数字;double 占 8 字节,大约有 15 位。浮点字面量和数学函数默认使用 double,除非你要存放数百万个值或者面向嵌入式芯片,否则 double 才是正确的选择。
C 语言中 int 溢出会发生什么?
对于有符号 int,溢出是未定义行为:编译器可能让它回绕、饱和,或者干脆把检查代码优化掉。对于无符号 int,溢出有完全确定的定义:值按 2^N 取模回绕,所以 UINT_MAX + 1 等于 0。永远不要依赖有符号溢出。