第3章:数据和 C
数据类型决定了一个值怎样存储、能够表示多大的范围、能保留多少精度,以及应该怎样输入和输出。
本章可以沿着这条线理解:先看数据的性质,再选择类型;写清常量和变量,最后让输入、计算和输出的类型相互匹配。
1. 变量、常量与类型
1.1 常量与变量的区别
float length = 12.5f;
length = 18.0f;
这里的 length 是变量,可以在运行过程中获得不同的值。12.5f 和 18.0f 是直接写在代码中的常量。
现实中的某个量是否会变化,与代码中的值是不是常量,是两个问题。例如,市场价格会变化,但写进程序的 25.0 仍是一个固定的数值常量。
1.2 变量类型靠声明,常量类型看写法
| 写法 | 含义 |
|---|---|
int count = 12; |
count 是整型变量 |
float length = 12.5f; |
length 是 float 变量,右侧常量也是 float |
double length = 12.5; |
length 是 double 变量,右侧常量默认是 double |
char grade = 'B'; |
grade 是字符变量,右侧是普通字符常量 |
12、12.0 和 12.0f 在数学上数值相同,但在 C 中类型不同。
类型相同与数值相等不是一回事。 类型会影响运算、转换、存储和函数调用。
1.3 初始化只作用于明确写出的变量
int first, second = 10;
这只初始化了 second。对于函数内部的普通局部变量,不能认为 first 也等于 10,或者自动等于 0。
如果两个变量都需要初始值,应明确写出:
int first = 10;
int second = 10;
2. 位、字节与存储大小
2.1 三个容易混淆的单位
| 名称 | 含义 | 需要注意 |
|---|---|---|
| 位(bit) | 一个二进制位,可以表示 0 或 1 |
n 位共有 2^n 种组合 |
| 字节(byte) | C 中衡量对象大小的基本单位 | sizeof(char) 始终为 1 |
| 字(word) | 与机器体系结构有关的自然处理单位 | 不能直接据此推断所有 C 类型的大小 |
常见环境中一字节是 8 位,但 C 不把所有实现的一字节都限定为 8 位。<limits.h> 中的 CHAR_BIT 给出每字节的位数,至少为 8。
例如,在每字节 8 位、没有其他表示开销的无符号字节中,256 种组合可以表示 0 到 255。
2.2 sizeof 给出字节数
int count = 12;
printf("int: %zu bytes\n", sizeof(int));
printf("count: %zu bytes\n", sizeof count);
sizeof 是运算符,不是普通函数。操作数是类型名时需要括号;操作数是这里的变量表达式时,可以不加括号。
它的结果类型是 size_t,是一种无符号整数类型。C99/C11 中,输出这个结果应使用 %zu。
例如,sizeof(int) == 4 表示四个 C 字节;结合 CHAR_BIT == 8,才能进一步说它占用 32 位存储空间。
3. 整数类型:范围与符号
3.1 整数家族
| 常用写法 | 等价写法示例 | 主要含义 |
|---|---|---|
short |
short int、signed short int |
有符号短整型 |
int |
signed int |
基本有符号整型 |
long |
long int、signed long int |
有符号长整型 |
long long |
long long int |
C99 引入的有符号长长整型 |
unsigned int |
unsigned |
无符号基本整型 |
unsigned short |
unsigned short int |
无符号短整型 |
unsigned long |
unsigned long int |
无符号长整型 |
unsigned long long |
unsigned long long int |
无符号长长整型 |
有符号整数可以表示负数、零和正数。无符号整数表示零和正数,相应类型可用于表示更大的非负值。
3.2 标准保证与本机实际大小
对于这些整数类型,大小满足:
sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long)
相邻类型可以一样大。不要认为名称越长,就一定多占几个字节。
在本章涉及的 C99/C11 规则下,以下是必须至少覆盖的范围:
| 类型 | 至少应覆盖的取值范围 |
|---|---|
short、int |
-32767 到 32767 |
unsigned short、unsigned int |
0 到 65535 |
long |
-2147483647 到 2147483647 |
unsigned long |
0 到 4294967295 |
long long |
-9223372036854775807 到 9223372036854775807 |
unsigned long long |
0 到 18446744073709551615 |
这是最低保证,不是所有系统的实际范围。常见的 32 位有符号 int 通常能表示 -2147483648 到 2147483647。
64 位处理器也不意味着 int 必须是 64 位。需要实际范围时,查看 <limits.h> 中的 INT_MIN、INT_MAX、LONG_MAX、UINT_MAX 等宏。
3.3 怎样选择整数类型
- 普通且范围合适的整数计算,可以从
int开始。 - 需要更大范围时,根据数据上限选择
long或long long。 - 对无符号数据、位模式等有明确需求时,使用对应的无符号类型。
- 确实需要较小存储空间时,再考虑
short等类型,并确认本机大小。 - 对位宽有明确约束时,使用后面介绍的可移植整数类型。
选择类型时要考虑输入范围和中间计算结果。最终结果变量足够大,不代表中间运算就不会溢出。
4. 整型常量:进制与后缀
4.1 同一个值的不同写法
| 写法 | 进制 | 对应十进制值 |
|---|---|---|
32 |
十进制 | 32 |
040 |
八进制,前缀为 0 |
32 |
0x20、0X20 |
十六进制 | 32 |
这些写法改变的是源码中的表示方式。对于相同类型、相同数值,计算机不会因为写成八进制,就采用另一套整数存储方式。
前导零尤其需要留意:010 是十进制的 8,08 则不是合法的八进制整型常量。
4.2 后缀参与决定类型
| 后缀 | 例子 | 类型意图 |
|---|---|---|
| 无后缀 | 32 |
按进制、数值和候选类型确定 |
U |
32U |
使用无符号整数类型 |
L |
32L |
使用 long 层级的整数类型 |
LL |
32LL |
使用 long long 层级的整数类型 |
UL |
32UL |
使用无符号 long 层级类型 |
ULL |
32ULL |
使用无符号 long long 类型 |
能被 int 表示的小型无后缀常量通常就是 int。在 C99/C11 中,无后缀十进制常量依次尝试 int、long、long long;无后缀八进制和十六进制常量的候选序列还会穿插相应的无符号类型。
因此,不要简单认为“所有整数常量都是 int”。后缀也不是把数值强行塞进指定类型,仍须满足相应的可表示范围。
采用大写 L 比小写 l 更容易与数字 1 区分。
4.3 输出时选择进制
#include <stdio.h>
int main(void)
{
unsigned int value = 32U;
printf("decimal: %u\n", value);
printf("octal: %#o\n", value);
printf("hex: %#x\n", value);
return 0;
}
输出:
decimal: 32
octal: 040
hex: 0x20
%o、%x、%X 分别用于八进制、十六进制小写和十六进制大写显示。这里的值为 unsigned int。
# 请求相应的前缀形式;对于这里的非零值,%#o 显示前导 0,%#x 显示 0x。十六进制的零值不会因此显示 0x 前缀。
5. 整数溢出与类型转换
5.1 无符号整数按模运算
对于 unsigned int,运算结果按 UINT_MAX + 1 取模。下面这段行为是有定义的:
unsigned int value = UINT_MAX;
value = value + 1U;
printf("%u\n", value);
包含 <limits.h> 和 <stdio.h> 后,这段代码会输出 0。
从 0U 减去 1U 则会得到 UINT_MAX。所以,无符号类型并不会替你阻止“数量减成负数”这样的业务错误。
5.2 有符号整数溢出是未定义行为
如果 int 的加法结果超出了 int 的可表示范围,C99/C11 不保证它按某种固定方式回绕。
不能依赖“最大正数加一必定变成最小负数”,也不能假定程序一定会报错。即使某次运行看起来符合这种回绕,也不能据此编写后续逻辑。
5.3 计算类型也要足够大
long long product = 100000LL * 100000LL;
这使乘法本身按 long long 进行。若右侧写成两个普通 int 常量相乘,在常见的 32 位 int 环境中,会先发生有符号溢出,之后再赋给较大的变量也无法补救。
5.4 浮点数转整数会截去小数
int positive = 12.99;
int negative = -12.99;
结果分别为 12 和 -12。这里是向零截断,不是四舍五入。
这个结论以截断后的值能被目标整数类型表示为前提。不要把超出整数范围的浮点值转换也理解为安全的截断。
6. char:用整数编码表示字符
6.1 字符的底层仍是数值
char grade = 'B';
printf("character: %c, code: %d\n", grade, grade);
在使用 ASCII 编码的常见系统中,这会输出:
character: B, code: 66
%c 选择字符显示方式,%d 选择十进制整数显示方式。显示格式的改变,并没有改变变量本身保存的数据。
写 'B' 比直接写 66 更能表达字符含义,也能减少对特定字符编码的依赖。C 不要求所有实现都采用 ASCII。
6.2 四种相似写法含义不同
| 写法 | 含义 |
|---|---|
8 |
整数八 |
'8' |
数字字符,ASCII 编码值为 56 |
"8" |
字符串字面量,含字符和结尾的空字符 |
grade |
标识符,引用已经声明的实体 |
普通字符常量如 'B' 在 C 中的类型是 int,而 char grade 定义的是 char 对象。二者并不矛盾,初始化时会进行相应转换。
不要用 'AB' 这类多字符常量代替字符串;这类常量的值与实现有关,不能按普通单字符规则推断。
6.3 char 的符号性
char、signed char、unsigned char 是三个不同的类型。
- 普通
char的取值范围与有符号还是无符号字符类型一致,由实现决定。 signed char明确表示有符号字符整数类型。unsigned char明确表示无符号字符整数类型。
处理基本字符时通常使用 char。处理数值或原始字节时,应根据需求明确符号性。
一个 char 对象占一个 C 字节,但这不意味着一个 char 就能容纳任意语言中的完整文字字符。例如,UTF-8 中的一个汉字通常需要多个字节。
7. 浮点类型:范围与精度要分开看
7.1 三种实浮点类型
| 类型 | 精度方面的基本认识 | 常见实现情况 |
|---|---|---|
float |
至少提供 6 位十进制有效数字的精度保证 | 常见为 32 位,约 6~7 位有效数字 |
double |
至少提供 10 位十进制有效数字的精度保证 | 常见为 64 位,约 15~16 位有效数字 |
long double |
精度和范围至少覆盖 double |
可能更高,也可能与 double 相同 |
这里的“有效数字”不是“小数点后固定多少位”。例如,一个数的小数部分很短,也可能因为整数部分太长而超出精度。
应把两个限制分开:
- 范围:能表示多大或多接近零的数。
- 精度:在这个数量级上,能区分多细的数值变化。
浮点数能够覆盖很大的范围,不意味着它能精确表示范围内的每个整数或小数。
7.2 十进制浮点常量
| 写法 | 数学含义 |
|---|---|
12.5 |
十二点五 |
1.25e2 |
1.25 × 10²,即 125.0 |
1.25E-2 |
1.25 × 10⁻²,即 0.0125 |
.5 |
0.5 |
5. |
5.0 |
5e0 |
5.0 |
指数形式中的 e 或 E 表示十的幂,不是变量名。一个浮点常量内部不能插入空格。
7.3 浮点后缀
| 写法 | 类型 |
|---|---|
1.25 |
double |
1.25f、1.25F |
float |
1.25L |
long double |
例如,float x = 1.25; 的右侧先是 double 常量,再转换为 float;float x = 1.25f; 的右侧直接是 float。
把一个精度更高的值保存到精度更低的对象中,可能发生舍入。后缀应与计算需求一致。
7.4 十六进制浮点常量
C99 增加了十六进制浮点写法:
double value = 0x1.8p+2;
它的含义是:
0x1.8 = 1 + 8/16 = 1.5
p+2 = 乘以 2²
结果 = 6.0
前面的有效数使用十六进制,p 后面的指数按十进制书写,表示的是二的幂。不能把 p+2 理解为乘以十的平方。
8. 浮点误差、上溢与下溢
8.1 有限位数只能容纳有限种数值
常见浮点实现按二进制保存数值。某些简单的十进制小数,如 0.1,在二进制中需要无限展开,因此只能保存近似值。
另一方面,像 0.5、1.5、7.0 这样的数,在常见二进制浮点格式中可以精确表示。不能把所有浮点值都理解为“必定带误差”。
误差可能在常量转换、运算舍入或保存到较低精度类型时产生。
8.2 大数加小数,小变化可能消失
#include <stdio.h>
int main(void)
{
float large = 1.0e20f;
float increased = large + 1.0f;
float difference = increased - large;
printf("difference: %.1f\n", difference);
return 0;
}
在常见的二进制 float 实现中,输出为:
difference: 0.0
因为 large 所处数量级上的相邻可表示值,间距已经远大于 1。加一后保存的值仍可能与原值相同。
这说明数学上等价的计算变形,在有限精度浮点运算中未必得到相同结果。
8.3 几种特殊情况
| 情况 | 含义 | 应怎样理解 |
|---|---|---|
| 上溢 | 结果的绝对值过大,超出有限可表示范围 | 在支持相应 IEEE 浮点语义的环境中,可能得到无穷大 |
| 下溢 | 结果接近零,进入精度受限或无法表示的区域 | 可能成为次正规数,或进一步舍入为零 |
| 次正规数(subnormal) | 比最小正规正数更接近零的非零浮点值 | 精度通常随数量级降低而减少 |
| 无穷大 | 用于表示正无穷或负无穷的特殊值 | 常显示为 inf 等形式,具体行为依实现和环境而定 |
| NaN | 表示“不是一个数”的特殊值 | 可出现在某些无有效实数结果的运算中 |
不是每个 C 实现都以相同方式提供这些行为。不要把某个环境中看到的 inf 或 nan 输出,推广为所有环境都保证如此。
<float.h> 提供浮点能力信息,例如 FLT_MAX、DBL_MAX、FLT_DIG、DBL_DIG。
其中,FLT_MIN 表示最小的正规正 float,不是“最负的 float”,也不一定是最小的非零正值。
8.4 输出位数不会提升存储精度
%.2f 指定保留两位小数的显示格式。它会影响输出的舍入和外观,却不会把变量本身变成“精确到两位小数的数据类型”。
如果内部数值已经丢失精度,把输出改成更多小数位,也不能恢复丢失的信息。
9. printf 与 scanf 的类型匹配
9.1 输出传值,输入提供存储位置
float length = 0.0f;
scanf("%f", &length);
printf("%.2f\n", length);
printf 接收要输出的值;scanf 需要知道结果放在哪里。这里的 &length 取得变量地址,让 scanf 能修改该变量。
对本章这种简单标量变量的输入,不能漏写 &。后续学习数组和字符串输入时,还会遇到不同的写法。
9.2 常用格式对照
以下按变量的声明类型列出,scanf 一栏表示向相应变量读取数据时使用的转换说明:
| 变量类型 | printf 常用形式 |
scanf 对应形式 |
|---|---|---|
int |
%d |
%d,传入 int * |
unsigned int |
%u |
%u,传入 unsigned int * |
short |
%hd |
%hd,传入 short * |
unsigned short |
%hu |
%hu,传入 unsigned short * |
long |
%ld |
%ld,传入 long * |
unsigned long |
%lu |
%lu,传入 unsigned long * |
long long |
%lld |
%lld,传入 long long * |
unsigned long long |
%llu |
%llu,传入 unsigned long long * |
char,按字符处理 |
%c |
%c,传入 char * |
float |
%f、%e、%g、%a |
%f,传入 float * |
double |
%f、%e、%g、%a |
%lf,传入 double * |
long double |
%Lf、%Le、%Lg、%La |
%Lf,传入 long double * |
size_t |
%zu |
%zu,传入 size_t * |
%f 用普通小数形式输出,%e 用十进制指数形式,%a 用十六进制浮点形式;%g 根据数值和精度选择较合适的十进制形式。
9.3 为什么输出浮点数时不区分 float 与 double
传给 printf 可变参数部分的 float 会自动提升为 double,所以两者都可以使用 %f。
类似地,char 和 short 等较窄整数会发生整数提升;对于本章常见的基本字符与短整数示例,通常提升为 int。
但是,scanf 接收的是地址:float * 不会因为传参而变成 double *。因此:
float first = 0.0f;
double second = 0.0;
scanf("%f", &first);
scanf("%lf", &second);
printf("%f %f\n", first, second);
在 C99/C11 中,printf 的 %lf 与 %f 效果相同;scanf 的 %lf 与 %f 则要求不同类型的指针。不能把这两个函数的格式规则机械地互换。
9.4 转换说明不会替你改变实参类型
把一个浮点实参配上 %d,不会自动得到它的整数部分。格式字符串告诉函数如何解释实参,不能代替正确的类型转换。
对本章这些用法,应逐一核对转换说明的位置、数量和类型:
- 缺少所需实参,或者实参类型不匹配,可能导致未定义行为。
- 对
printf而言,多余实参会被求值,但在格式字符串用尽后被忽略;它们不等于缺少参数的情况。 - 输出结果“看起来正常”,不能证明类型匹配正确。
开启编译器的格式警告有助于发现问题,但程序员仍需理解接口要求。
9.5 读取字符时注意空白
scanf("%c", &ch) 读取下一个字符,不自动跳过空格或换行。
如果刚读完一个数字,输入中的换行可能还没有被消耗,接下来的 %c 就可能读到它。需要跳过前导空白再读一个字符时,可以使用:
scanf(" %c", &ch);
这里格式字符串开头的空格有作用:跳过输入中的前导空白。若任务需要读取空格本身,就不应采用这个写法。
10. 一个完整的交互示例
下面的程序读取以米为单位的长度,输出米和千米两种表示:
#include <stdio.h>
int main(void)
{
float meters = 0.0f;
printf("Length in meters: ");
fflush(stdout);
if (scanf("%f", &meters) != 1) {
printf("Invalid number.\n");
return 1;
}
double kilometers = meters / 1000.0;
printf("Meters: %.2f\n", meters);
printf("Kilometers: %.3f\n", kilometers);
return 0;
}
输入 1250 时,结果中的两行为:
Meters: 1250.00
Kilometers: 1.250
这个例子串起了几件事:
float meters保存输入数据,%f与它的地址匹配。scanf返回成功赋值的项目数;这里只期望读取一项。if用于在未成功读取时结束程序,具体分支语法后续再展开。1000.0是double常量,计算涉及相应的浮点转换。printf中的.2、.3控制显示位数。
示例的输入是目标类型可表示的普通数值;这里的返回值检查用于处理没有读到数值的情况,不是完整的输入范围校验。
11. 可移植整数类型
11.1 为什么还需要另一套名称
int 和 long 的实际宽度因实现而异。如果数据格式或计算需求明确规定了宽度,就不能只根据名称猜测。
C99 的 <stdint.h> 提供了一组整数类型别名:
| 类别 | 例子 | 含义 |
|---|---|---|
| 精确宽度 | int32_t、uint32_t |
恰好具有指定位宽;这些类型是可选的 |
| 最小宽度 | int_least32_t |
至少满足指定位宽,并采用符合要求的最小宽度类型 |
| 最快最小宽度 | int_fast32_t |
至少满足指定位宽,并按实现对速度的选择提供类型 |
| 最大宽度 | intmax_t、uintmax_t |
分别能够容纳实现支持的各有符号、无符号整数类型的值 |
int32_t 可能是 int 的别名,也可能对应另一个整数类型。因此,即使已经选好了类型,输出时也不应继续猜测该用 %d 还是 %ld。
11.2 用格式宏匹配可移植类型
在提供 int32_t 的实现中,可以这样写:
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
int main(void)
{
int32_t count = 32000;
printf("count: %" PRId32 "\n", count);
return 0;
}
输出:
count: 32000
PRId32 来自 <inttypes.h>,提供适合该类型的十进制输出格式片段。它本身不包含前面的 %。
C 会连接相邻字符串字面量。假设宏展开为 "d",这一格式相当于:
"count: %" "d" "\n"
最终得到 "count: %d\n"。输入也有对应的 SCN... 格式宏,不能把输出宏直接用于 scanf。
12. 布尔、复数与其他类型
12.1 _Bool 保存逻辑状态
C99 引入 _Bool,表示假和真,保存的值是 0 或 1:
_Bool empty = 0;
_Bool ready = 5;
printf("%d %d\n", empty, ready);
输出 0 1。把非零整数转换为 _Bool,结果为 1。
_Bool 是独立的整数类型,不是 unsigned int 的别名。逻辑上一个二进制位足以表达两种状态,也不意味着普通 _Bool 对象的 sizeof 是“八分之一个字节”。
在 C99/C11 中,还可以包含 <stdbool.h>,使用 bool、true 和 false 这些便利名称。
12.2 复数与虚数
复数有实部和虚部,可以分别基于三种实浮点类型组成:
float _Complexdouble _Complexlong double _Complex
<complex.h> 提供相关支持,例如 complex 和虚数单位 I 等名称。_Imaginary 相关虚数类型的支持是可选的;C11 中复数相关支持也可能是可选的,需要确认实现能力。
普通入门程序暂时不必使用这些类型,知道它们服务于复数计算即可。
12.3 基本类型之外
后续还会接触数组、指针、结构和联合等类型。C 没有一个名为 string 的内建基本类型,字符串会通过字符数组等方式表示和处理。
本章在 scanf 中使用 &变量,已经接触到了指向对象位置的指针值。
13. 转义序列与输出缓冲
13.1 常用转义序列
| 写法 | 含义 |
|---|---|
\n |
换行 |
\r |
回车,通常把输出位置移回当前行开头 |
\t |
水平制表,移到下一个制表位置 |
\b |
退格,通常使输出位置后退 |
\a |
警报,是否有声音或可见效果取决于环境 |
\f |
换页 |
\v |
垂直制表 |
\\ |
反斜杠 |
\' |
单引号 |
\" |
双引号 |
\? |
问号的转义写法 |
\0 |
数值为零的空字符 |
回车与换行不同,退格也不等于保证删除一个已经输出的字符。制表符不是固定数量的空格,显示位置取决于当前列和制表设置。
某些控制字符在终端、IDE 输出窗口、文件或打印设备中的效果不同,不能只凭一个界面的显示结果判断它的全部含义。
13.2 用数值编码写字符
字符常量可以使用八进制或十六进制转义:
char newline = '\n';
char octal_a = '\101';
char hex_a = '\x41';
后两种在 ASCII 环境中表示 'A'。八进制转义使用一到三位八进制数字;十六进制转义在 \x 后跟十六进制数字,并会继续读取后面连续的十六进制数字。
因此,不应认为十六进制转义一定只读取两位。需要隔开后续字符时,可借助相邻字符串连接等方式明确边界。
表达换行等已有标准转义的字符时,优先写 \n 这样的形式,比硬编码字符数值更清楚,也更便于移植。
13.3 输出可能先进入缓冲区
printf 通常向标准输出流写入数据,数据可能先存放在缓冲区,再交给终端或文件。
printf → 标准输出缓冲区 → 终端或其他输出目标
不同缓冲模式的刷新时机不同。例如,面向终端的标准输出常采用行缓冲,换行通常会促使其刷新;重定向到文件时,则不应假定每次换行都立即写出。
当提示文字没有换行、又希望用户输入前就看到它时,可以明确刷新:
printf("Enter a value: ");
fflush(stdout);
fflush(stdout) 用于刷新输出。不要把 fflush(stdin) 当成标准 C 中清理键盘输入的通用方法。
14. 查看当前环境的类型能力
下面的程序把“类型多大”和“能表示多少”分别打印出来:
#include <stdio.h>
#include <limits.h>
#include <float.h>
int main(void)
{
printf("bits per byte: %d\n", CHAR_BIT);
printf("char: %zu bytes\n", sizeof(char));
printf("short: %zu bytes\n", sizeof(short));
printf("int: %zu bytes\n", sizeof(int));
printf("long: %zu bytes\n", sizeof(long));
printf("long long: %zu bytes\n", sizeof(long long));
printf("float: %zu bytes\n", sizeof(float));
printf("double: %zu bytes\n", sizeof(double));
printf("long double: %zu bytes\n", sizeof(long double));
printf("_Bool: %zu bytes\n", sizeof(_Bool));
printf("int range: %d to %d\n", INT_MIN, INT_MAX);
printf("unsigned int max: %u\n", UINT_MAX);
printf("float decimal digits: %d\n", FLT_DIG);
printf("double decimal digits: %d\n", DBL_DIG);
printf("long double decimal digits: %d\n", LDBL_DIG);
return 0;
}
保存为 type_info.c,可以在已经提供 cc 的 macOS 或类 UNIX 命令行环境中运行:
cc -std=c11 -Wall -Wextra type_info.c -o type_info
./type_info
这些结果描述的是当前实现。移植到另一个环境后,应依照标准保证和程序需求重新判断,不能把某一次输出当作 C 类型的统一尺寸表。

