这一篇在干嘛?
程序就是”处理数据”的机器,而 C 给数据分了两大世家:整数(integer)与浮点数(floating-point)。这一章把 C 的类型全家族过一遍——int、short、long、char、float、double 各自多大、能装什么数、怎么写常量、怎么用 printf()/scanf() 读写。看完你就明白”为什么 7 和 7.00 在计算机里是两种东西”,还能写出第一个带交互的程序。
3.1 示例程序
先看一个交互程序:输入体重,算出”等重铑(rhodium)“值多少钱。它一口气引入了 float 变量、新式常量、%f 输出和 scanf() 输入。
程序清单 3.1 rhodium.c
/* rhodium.c -- your weight in rhodium */
#include <stdio.h>
int main(void)
{
float weight; /* 用户体重 */
float value; /* 等重铑的价值 */
printf("Are you worth your weight in rhodium?\n");
printf("Let's check it out.\n");
printf("Please enter your weight in pounds: ");
scanf("%f", &weight); /* 从键盘读入 */
/* 假设铑每盎司 770 美元;14.5833 把磅换算成金衡盎司 */
value = 770.0 * weight * 14.5833;
printf("Your weight in rhodium is worth $%.2f.\n", value);
return 0;
}Please enter your weight in pounds: 150
Your weight in rhodium is worth $1684371.12.
新知识点:float 能装带小数点的数;%f 是浮点数的格式说明符(format specifier),%.2f 表示小数点后留两位;scanf("%f", &weight) 从键盘读浮点数,变量名前的 &(取地址符)告诉 scanf() 值该放到哪里——第 4 章展开,先照着用。
错误(error)与警告(warning)不是一回事
少写分号会得到错误,程序无法编译;而 “conversion from ‘double’ to ‘float’, possible loss of data” 这类警告不阻止编译,只提醒”代码合法但可能不是你想要的”。看到警告别无视,要弄清原因。

3.2 数据:变量与常量
**常量(constant)**是程序运行前就定好、运行中不变的量(如 14.5833);**变量(variable)**运行中可被赋值、修改(如 weight)。
数据类型关键字
| 时期 | 关键字 |
|---|---|
| K&R(7 个) | int、long、short、unsigned、char、float、double |
| C90 新增 | signed、void |
| C99 新增 | _Bool、_Complex、_Imaginary |
按存储方式,这些类型分成两族:整数类型与浮点类型。
位、字节和字
**位(bit)**只能存 0 或 1;**字节(byte)**几乎都是 8 位,能表达 256 种比特组合;**字(word)**是给定计算机设计的自然存储单位(8 位机一个字 8 位,32 位机 32 位)。
整数与浮点数的本质区别
对人来说区别在”怎么写”,对计算机来说区别在”怎么存”:整数没有小数部分,直接按二进制存(7 存成 00000111);浮点数把**小数部分(fraction)和指数部分(exponent)**分开存——7.00 和 7 数值相同但存储方式完全不同,十进制类比是把 7.0 写成 0.7E1。


实际差别五条:整数无小数部分;浮点范围大得多;浮点运算(如大数相减)损失精度更多;任何区间的实数有无穷多个,浮点只能近似(7.0 可能存成 6.99999);浮点运算通常更慢(如今有专用浮点处理器,差距已缩小)。
3.3 基本数据类型
int 类型
int 是有符号整数(signed integer),标准保证最小范围 −32767~32767,实际大小通常等于机器字长(如今多为 32 位)。声明、赋值与初始化:
int erns;
int hogs, cows, goats; /* 一次声明多个 */
cows = 112; /* 赋值 */
int hogs = 21; /* 声明同时初始化 */
int dogs, cats = 94; /* 合法但糟糕:只有 cats 被初始化 */初始化与声明混写的陷阱
int dogs, cats = 94;里只有 cats 拿到 94,dogs 仍未初始化。快速阅读极易误读,建议两者别写在同一条声明里。
打印 int 值:%d 必须与值一一对应
程序清单 3.2 print1.c
/* print1.c -- displays some properties of printf() */
#include <stdio.h>
int main(void)
{
int ten = 10;
int two = 2;
printf("Doing it right: ");
printf("%d minus %d is %d\n", ten, 2, ten - two);
printf("Doing it wrong: ");
printf("%d minus %d is %d\n", ten); /* 少给了 2 个参数 */
return 0;
}Doing it right: 10 minus 2 is 8
Doing it wrong: 10 minus 10 is 2
printf() 参数个数不固定,编译器无法像普通函数那样检查——第二行多出来的 %d 把内存里碰巧躺着的垃圾值打了出来。格式说明符的个数、类型都必须与值一一对应,这是程序员的责任。
八进制与十六进制
进制只是书写方式,存储完全相同:十六进制前缀 0x(16 写作 0x10),八进制前缀 0(16 写作 020)。显示用 %o 打八进制、%x 打十六进制,加 # 可显示前缀。
程序清单 3.3 bases.c
/* bases.c -- prints 100 in decimal, octal, and hex */
#include <stdio.h>
int main(void)
{
int x = 100;
printf("dec = %d; octal = %o; hex = %x\n", x, x, x);
printf("dec = %d; octal = %#o; hex = %#x\n", x, x, x);
return 0;
}dec = 100; octal = 144; hex = 64
dec = 100; octal = 0144; hex = 0x64
其他整数类型:short、long、unsigned
| 类型 | 特点 |
|---|---|
| short int(short) | 可能比 int 省空间,有符号 |
| long int(long) | 可能比 int 装更大的数,有符号 |
| long long int(C99) | 可能比 long 更大,至少 64 位,有符号 |
| unsigned int(unsigned) | 只装非负数,16 位时范围 0~65535 |
C 只保证 short 不比 int 长、long 不比 int 短;常见配置是 long long 64 位、long 32 位、short 16 位、int 16 或 32 位。选择建议:计数用 unsigned;需要 32 位整数就用 long(哪怕当前机器 int 也是 32 位,移植到 16 位机仍正确);需要 64 位用 long long;省内存、存大数组用 short。
整数溢出(overflow)
/* toobig.c -- exceeds maximum int size on our system */
int i = 2147483647;
unsigned int j = 4294967295;
printf("%d %d %d\n", i, i+1, i+2);
printf("%u %u %u\n", j, j+1, j+2);2147483647 -2147483648 -2147483647
4294967295 0 1
unsigned 像汽车里程表:到最大值绕回 0;有符号 int 绕回 −2147483648。溢出不报错——C 对 unsigned 的绕回有明确规定,但对有符号类型的溢出行为未定义,需自己防范。
常量后缀与打印修饰符
常量后缀:L 表示 long(7L)、LL 表示 long long(3LL)、u/U 表示 unsigned(可组合 5ull)。推荐大写 L,小写 l 容易看成数字 1。打印修饰符:%u 打 unsigned、%ld 打 long、%lld 打 long long、%hd/%ho 打 short、%lu 打 unsigned long。
程序清单 3.4 print2.c
/* print2.c -- more printf() properties */
#include <stdio.h>
int main(void)
{
unsigned int un = 3000000000; /* 32 位 int 的系统 */
short end = 200; /* 16 位 short */
long big = 65537;
long long verybig = 12345678908642;
printf("un = %u and not %d\n", un, un);
printf("end = %hd and %d\n", end, end);
printf("big = %ld and not %hd\n", big, big);
printf("verybig = %lld and not %ld\n", verybig, verybig);
return 0;
}un = 3000000000 and not -1294967296
end = 200 and 200
big = 65537 and not 1
verybig = 12345678908642 and not 1942899938
三个要点:① %d 打 unsigned 3000000000 得负数——同一位模式按有无符号解读结果不同;② short 传参自动提升为 int,%hd 与 %d 打 200 一样;③ %hd 截取低 16 位:65537 低 16 位只剩末位 1,所以显示 1。
铁律:数量对应 + 类型对应
错误的说明符不会报错,只会产出”看起来像乱码”的结果——不同平台结果还不一样,极难排查。
char 类型:存字符其实是在存整数
计算机用数值编码表示字符,最常用的是 ASCII 码:65 代表 ‘A’。标准 ASCII 范围 0~127,char 通常 8 位,绰绰有余。
char grade = 'A'; /* 单引号:字符常量 */
char broiled;
broiled = 'T'; /* 正确 */
broiled = T; /* 错!编译器以为 T 是变量名 */
broiled = "T"; /* 错!双引号是字符串,第 4 章讲 */
char grade = 65; /* ASCII 系统可行,但风格差 */有趣的是,C 把字符常量当 int 处理:char grade = 'B'; 中 ‘B’ 先按 32 位存成 66,再截断成 8 位存进 grade。
单引号和双引号是两种东西
'x'是 char 类型的字符常量;"x"是字符串(char 数组),内容是 ‘x’ 和 ‘\0’ 两个字符。混用不一定报错,但语义完全不同。
非打印字符与转义序列(escape sequences)
表示蜂鸣、换行这类”看不见”的字符有三种方法:直接用 ASCII 码(char beep = 7;);转义序列(最推荐,好记且可移植);八进制/十六进制码(beep = '\007'; 可简写 ‘\7’,Ctrl+P 写作 '\x10')。
| 序列 | 含义 | 序列 | 含义 |
|---|---|---|---|
| \a | 警报(ANSI C) | \ | 反斜杠 \ |
| \b | 退格 | ’ | 单引号 ‘ |
| \f | 换页 | ” | 双引号 “ |
| \n | 换行 | ? | 问号 ? |
| \r | 回车 | \0oo | 八进制 ASCII 码 |
| \t | 水平制表符 | \xhh | 十六进制 ASCII 码 |
| \v | 垂直制表符 |
赋给 char 变量时转义序列要加单引号(char nerf = '\n';);出现在双引号字符串里则不加,如 printf("Gramps sez, \"a \\ is a backslash.\"\n");。
打印字符:%c 与 %d
程序清单 3.5 charcode.c
/* charcode.c -- displays code number for a character */
#include <stdio.h>
int main(void)
{
char ch;
printf("Please enter a character.\n");
scanf("%c", &ch); /* 用户输入一个字符 */
printf("The code for %c is %d.\n", ch, ch);
return 0;
}Please enter a character.
C
The code for C is 67.
同一个 ch,%c 打出字符 C,%d 打出编码 67。说明符决定数据如何”显示”,不决定如何”存储”。

有的编译器把 char 实现为有符号(−128~127),有的为无符号(0~255),可用 signed char / unsigned char 明确指定;用于字符时直接用普通 char 即可。
_Bool 与可移植类型:inttypes.h
C99 的 _Bool 只装 1(真)和 0(假)。int 在不同机器上大小可能不同,C99 在 inttypes.h 提供了精确定义的名字:精确宽度类型 int16_t 就是 16 位有符号整数、uint32_t 是 32 位无符号;最小宽度类型 int_least8_t 保证至少装下 8 位;最快类型 int_fast8_t;最大宽度类型 intmax_t/uintmax_t。打印时用头文件里的宏,如 PRId16 会替换成对应的说明符字符串:
int16_t me16 = 4593;
printf("me16 = %" PRId16 "\n", me16); /* 不必猜是 %hd 还是 %d */float、double 与 long double
浮点数类似科学计数法:2.75、3.16E7(3.16×10⁷)、2e−8。三种浮点类型:
| 类型 | 至少有效数字 | 典型存储 |
|---|---|---|
| float | 6 位 | 32 位(8 位指数 + 24 位尾数) |
| double | 10 位 | 64 位,精度更高、舍入误差更小 |
| long double | 不低于 double | 实现自定 |
声明示例:float planck = 6.63e-34;、double trouble;。浮点常量可以省小数点(2E5)或指数部分(19.28)但不能两者都省,可以省整数部分(.45E−6)或小数部分(3.E16),中间不能有空格。默认浮点常量是 double,后缀 f/F 变 float(2.3f),l/L 变 long double(54.3l)。
程序清单 3.6 showf_pt.c
/* showf_pt.c -- displays float value in two ways */
#include <stdio.h>
int main(void)
{
float abot = 32000.0;
double abet = 2.14e9;
long double dip = 5.32e-5;
printf("%f can be written %e\n", abot, abot);
printf("%f can be written %e\n", abet, abet);
printf("%f can be written %e\n", dip, dip);
return 0;
}32000.000000 can be written 3.200000e+04
2140000000.000000 can be written 2.140000e+09
0.000053 can be written 5.320000e-05
%f 十进制、%e 指数记数法;float 和 double 都用这两个说明符(float 传参自动转 double);long double 用 %Lf、%Le。
浮点数的三大坑:上溢、下溢与舍入误差
上溢(overflow):结果超出最大可表示值,赋给表示”无穷”的特殊值,printf() 显示 inf。下溢(underflow):最小浮点数再除以 2,指数已到下限,只能挪尾数补位、丢末位精度,这种值叫次正常(subnormal);除得够狠最后剩 0。还有特殊值 NaN(not-a-number):给 asin() 传大于 1 的值就会返回 NaN。**舍入误差(round-off error)**看这个著名例子:
/* floaterr.c -- demonstrates round-off error */
float a, b;
b = 2.0e20 + 1.0;
a = b - 2.0e20;
printf("%f \n", a);0.000000 ← 老版 gcc on Linux
-13584010575872.000000 ← Turbo C 1.5
4008175468544.000000 ← CodeWarrior 9.0, MSVC++ 7.1
2.0e20 加 1 相当于改第 21 位数字,而 float 只有六七位有效数字,根本”看不到”这次加法。换成 2.0e4 就没问题,因为第 5 位的变化在 float 精度内。
浮点数是"近似值"
别用 == 判断两个浮点数相等,也别指望 2.0e20 + 1.0 − 2.0e20 等于 1。金额计算等精度敏感场景优先考虑 double。
C99 另有 float _Complex 等三种复数类型与可选的 _Imaginary 类型,含 complex.h 后可用 complex、imaginary 和 √−1 的记号 I。
类型大小:sizeof 运算符
程序清单 3.7 typesize.c
/* typesize.c -- prints out type sizes */
#include <stdio.h>
int main(void)
{
/* C99 提供了 %zd 说明符来打印 sizeof 的结果 */
printf("Type int has a size of %zd bytes.\n", sizeof(int));
printf("Type char has a size of %zd bytes.\n", sizeof(char));
printf("Type long has a size of %zd bytes.\n", sizeof(long));
printf("Type double has a size of %zd bytes.\n", sizeof(double));
return 0;
}Type int has a size of 4 bytes.
Type char has a size of 1 bytes.
Type long has a size of 4 bytes.
Type double has a size of 8 bytes.
sizeof 以字节报告类型大小;char 必然是 1 字节,因为 C 就拿 char 定义”字节”。查类型取值范围可看 limits.h 和 float.h 头文件(第 4 章介绍)。
3.4 使用数据类型
开发程序的套路:想清楚需要哪些变量、各用什么类型(数值多半 int 或 float,字符 char);在函数开头声明,起见名知意的名字;初始化时让常量类型与变量类型匹配:
int apples = 3; /* 正确 */
int oranges = 3.0; /* 能编译,但坏习惯 */
int cost = 12.99; /* 小数部分被截断,cost 得 12 */
float pi = 3.1415926536; /* 超出 float 精度,后几位失真 */C 对类型不匹配比较宽容(自动转换),但转换可能丢数据:浮点转整数是**截断(truncation)**而非四舍五入。
3.5 参数与陷阱
传给函数的信息叫参数(argument):printf("%d cats ate %d cans\n", cats, cans) 有 3 个参数(格式串 + 2 个值)。printf()/scanf() 靠格式串里的说明符数量”预期”后面还有几个参数——数目、类型对不上时编译器通常不吭声,运行时输出垃圾。
程序清单 3.8 badcount.c
/* badcount.c -- incorrect argument counts */
#include <stdio.h>
int main(void)
{
int f = 4;
int g = 5;
float h = 5.0f;
printf("%d\n", f, g); /* 参数太多 */
printf("%d %d\n", f); /* 参数太少 */
printf("%d %f\n", h, g); /* 类型不匹配 */
return 0;
}作者试过的编译器没一个报错。用 %d 打印 float 得到的不是”最近的整数”,而是把浮点位模式硬按整数解读的乱码。程序输出异常时,第一件事就是检查说明符与参数是否匹配。
3.6 再来一个例子:转义序列
程序清单 3.9 escape.c
/* escape.c -- uses escape characters */
#include <stdio.h>
int main(void)
{
float salary;
printf("\aEnter your desired monthly salary:"); /* 1 */
printf(" $______\b\b\b\b\b\b\b"); /* 2 */
scanf("%f", &salary);
printf("\n\t$%.2f a month is $%.2f a year.", salary,
salary * 12.0); /* 3 */
printf("\rGee!\n"); /* 4 */
return 0;
}逐步拆解:① \a 响铃并打印提示语,行尾无 \n,光标停在冒号后;② 打印下划线后 7 个 \b 把光标退回 $ 后,等你在下划线位置输入工资;③ \n 换行、\t 跳到制表位,输出月薪与年薪(乘 12.0 保证按浮点算);④ \r 把光标移回行首,Gee! 覆盖在行首显示。
输出还有一个重要机制:printf() 的输出先进缓冲区(buffer),遇到缓冲区满、遇到换行符、或即将发生输入时才送到屏幕(刷新缓冲区/flushing)。上面程序里 scanf() 索要输入,会强制前面的提示先上屏;老编译器若不这么做,可在 scanf() 前手动加换行冲刷。
3.7 关键概念与小结
- C 类型众多,是为了让程序员按需选择”大小与符号”;整数与浮点是两种根本不同的存储方式——同一 32 位位模式,按 float 解读得 256.0,按 long 解读得 113246208。字符在内存里是数值编码,‘A’ 就是编码的符号化写法;
- 整数常量可写成十进制、八进制(前缀 0)、十六进制(前缀 0x):32、040、0x20 是同一个数;后缀 L、LL、u/U 决定常量类型;浮点常量默认 double,f、L 改类型;
- printf() 靠转换说明符决定显示形式,数量与类型必须与参数一一对应。
本章收获的两件趁手工具
一是”看类型就知道怎么打印”:int 用 %d、unsigned 用 %u、long 用 %ld、char 用 %c/%d、float/double 用 %f/%e、long double 用 %Lf;二是 sizeof——不确定类型多大时一行代码问出来,写可移植程序再也不用猜。
自测题(点开展开答案)
1. 7、‘7’、“7” 三者有什么区别? 7 是 int 类型的整数常量,值为数值 7;‘7’ 是 char 类型字符常量,存的是字符 ‘7’ 的 ASCII 码 52;“7” 是字符串常量,含 ‘7’ 和 ‘\0’ 两个字符。三者存储内容和类型都不同。
2. 为什么
printf("%d minus %d is %d\n", ten)会打印出奇怪的数字? 格式串里有 3 个 %d,但只给了 1 个参数。printf() 仍会去参数位置取 3 个值,后两个取到内存中的随机垃圾值。编译器对 printf() 的参数个数不做检查,这是程序员的责任。3. 0x10、020、16 三个常量在内存中存储是否相同? 完全相同。三者都是数值 16 的不同进制写法,最终都以二进制存储。进制前缀只影响书写,不影响存储。
4.
int cost = 12.99;之后 cost 的值是多少?为什么? cost 得 12。浮点值赋给 int 时 C 直接丢弃小数部分(截断)而非四舍五入。编译器可能给警告,但转换照常发生。5. 2.0e20 + 1.0 - 2.0e20 的结果为什么不是 1?换什么数就能得到正确答案? 2.0e20 有 21 位十进制数字,float 只有约 6 位有效数字,加 1 引起的第 21 位变化完全超出其表示能力,加法等于没加,结果是 0 或乱值。换成 2.0e4 + 1.0 - 2.0e4 就能得到 1,因为第 5 位的变化在 float 精度范围内。