C 语言第 2 章:数据类型——补码、字节与取值范围的完整对应
这一章是全课程最"硬"的一章,也是最能拉开差距的一章。数据类型不是语法装饰,它直接决定三件事:这块内存占几个字节、这串 0 和 1 怎么解释、能表示多大范围的数。学生写出的溢出、精度丢失、无符号比较反常,根子都在这里。学完这一章应该能做到:给定一个十进制负数,手写它的 8 位补码;给定一个类型,说出它在典型平台上的字节数与取值范围;看到 unsigned 与 int 混算时,预判结果会变成什么。
一、二进制与十进制的相互转换
十进制整数转二进制用"除 2 取余,逆序排列":不断除以 2,记下余数,直到商为 0,再把余数从下往上读。以 182 为例:182÷2 余 0,91÷2 余 1,45÷2 余 1,22÷2 余 0,11÷2 余 1,5÷2 余 1,2÷2 余 0,1÷2 余 1,商为 0 停止。余数逆序得 1011 0110。
二进制转十进制用"按权展开":从右往左第 k 位(从 0 计)的权是 2ᵏ。1011 0110 = 128 + 32 + 16 + 4 + 2 = 182。
小数部分不同:乘 2 取整数部分,顺序读。0.625×2 = 1.25 取 1,0.25×2 = 0.5 取 0,0.5×2 = 1.0 取 1,得 0.101。注意并非所有十进制小数都能用有限位二进制表示,0.1 就是无限循环——这是浮点数精度问题的根源。
进制之间的快捷换算:二进制转八进制每 3 位一组,转十六进制每 4 位一组,从右往左分组,不足补零。182 = 10110110 = 010 110 110 = 八进制 266 = 1011 0110 = 十六进制 B6。
| 十进制 | 二进制 | 八进制 | 十六进制 |
|---|---|---|---|
| 10 | 1010 | 12 | A |
| 15 | 1111 | 17 | F |
| 255 | 1111 1111 | 377 | FF |
| 182 | 1011 0110 | 266 | B6 |
C 源码里的写法:以 0 开头是八进制(0266),以 0x 开头是十六进制(0xB6),十进制不加前缀。C23 起支持 0b 前缀的二进制字面量。
二、原码、反码与补码
计算机用补码存整数。三种编码的规则:
- 原码:最高位是符号位(0 正 1 负),其余位是绝对值的二进制。8 位下 +5 = 0000 0101,−5 = 1000 0101。
- 反码:正数与原码相同;负数符号位不变、其余位按位取反。−5 的反码是 1111 1010。
- 补码:正数与原码相同;负数等于反码加 1。−5 的补码是 1111 1011,即 0xFB。
为什么用补码,三条理由:把减法统一成加法(a − b 写成 a + (−b) 的补码,加法器一套电路通吃);消除 −0(原码里 1000 0000 与 0000 0000 都表示 0,补码里只有一个 0);多表示一个最小负数。
8 位补码的取值范围是 −128 ~ +127,而不是对称的 −127~127。原因:0000 0000 ~ 0111 1111 表示 0~127,1000 0000 ~ 1111 1111 表示 −128 ~ −1,其中 1000 0000 这个"原码里的 −0"被征用去表示 −128。推广到 n 位:
n 位补码范围:−2^(n−1) ~ 2^(n−1) − 1
| 位数 | 范围 | 对应类型 |
|---|---|---|
| 8 | −128 ~ 127 | signed char |
| 16 | −32768 ~ 32767 | short |
| 32 | −2147483648 ~ 2147483647 | int |
| 64 | 约 ±9.22×10¹⁸ | long long |
求负数补码有个更快的口诀:从右往左找到第一个 1,这个 1 及其右边的 0 保持不变,左边的所有位取反。以 −5 为例,5 = 0000 0101,最右的 1 在末位,保留它,左边全部取反,得 1111 1011。
三、字节、地址与内存单元
内存按字节编址,每个字节有一个唯一地址。1 字节 = 8 位。容量换算按 1024 进制:1 KB = 1024 B,1 MB = 1024 KB,1 GB = 1024 MB。
一个 int 占 4 个字节,就有 4 个连续地址。变量名对应的是这 4 个字节中的低地址,用 &a 取到的就是这个低地址。多字节数据在内存中的排列顺序叫字节序:x86 是小端序(低位字节放低地址),网络传输用大端序。这一点在本章只作了解,到指针与文件读写时才会真正咬人。
sizeof 是运算符不是函数,在编译期求值,返回类型是 size_t(无符号整数)。所以 sizeof(int) 可以用在数组维度、宏里;也因为返回无符号,sizeof(a) - 1 在 a 为空时会得到一个巨大的正数而不是 −1。
四、常量、变量与赋值
常量有四类:字面常量(123、3.14、'A'、"abc")、const 修饰的常变量(有类型、占空间、不可改)、#define 宏常量(预处理阶段做文本替换,无类型)、枚举常量。
变量的三个属性是名字、类型、值;再往外还有存储类别(auto、static、register、extern)、作用域与生存期。
- 定义会分配存储空间,声明只是告诉编译器"有这么个东西"(
extern int a;)。同一个变量可以声明多次,只能定义一次。 - 命名规则:字母、数字、下划线组成,不能以数字开头,区分大小写,不能与关键字重名。
- 未初始化的局部变量是随机值(栈上的残留数据),不是 0。全局变量和 static 变量才默认初始化为 0。这是"累加器忘了清零"类 bug 的根源。
五、各类类型的存储与范围
| 类型 | 典型字节 | 取值范围 | printf 格式符 |
|---|---|---|---|
| char | 1 | −128 ~ 127(signed)或 0 ~ 255 | %c |
| short | 2 | −32768 ~ 32767 | %hd |
| int | 4 | −2147483648 ~ 2147483647 | %d |
| unsigned int | 4 | 0 ~ 4294967295 | %u |
| long long | 8 | 约 ±9.22×10¹⁸ | %lld |
| float | 4 | 约 ±3.4×10³⁸,6~7 位有效数字 | %f |
| double | 8 | 约 ±1.7×10³⁰⁸,15~16 位有效数字 | %lf |
浮点数按 IEEE 754 存储,分为符号位、阶码、尾数三段:float 是 1 + 8 + 23 位,double 是 1 + 11 + 52 位。因为尾数位数有限,很多十进制小数无法精确表示,所以 0.1 + 0.2 == 0.3 的结果是假。判断两个浮点数相等必须用容差:fabs(a - b) < 1e-6。
字符型 char 本质是 1 字节整数,存的是字符的 ASCII 码。'A' 的值是 65,'a' 是 97,'0' 是 48。所以 'A' + 1 得 66,ch - '0' 能把数字字符转成数值,ch - 'a' + 'A' 能完成小写转大写。'0' 与 0 完全不同,前者是 48。
类型转换分两种:自动转换按"低精度向高精度"的路线进行,char → short → int → unsigned → long → float → double;强制转换写成 (double)a,只改变本次运算的解释方式,不改变 a 本身的存储。
类型选择有三条判据:一般整数直接用 int——在 32/64 位平台上它就是最贴合机器字长的类型,用 short 反而可能因对齐补位而变慢;需要精确宽度时用 <stdint.h> 的定宽类型(int32_t、uint64_t),不要赌 int 到底几位;金额与小数值不要用浮点,二进制浮点无法精确表示 0.1 这类十进制小数,应该用"分"为单位的整数或定点数表示。
易错点
- 以为 char 只能存字符。它就是一个字节的整数,
char c = 65;与char c = 'A';等价。 - 忽略溢出。
int的最大值加 1 会变成最小负数(补码回绕),且不报错。 - 有符号与无符号混算。
-1 < 1u的结果是假,因为 −1 被转成无符号后是 4294967295。 - 用
==比较浮点数。这是精度问题的经典表现,必须用容差。 - 认为 8 位补码范围是 −127~127。−128 没有对应的原码和反码,只有补码能表示。
- 默认
sizeof(int) == 4。标准只保证short ≤ int ≤ long,具体宽度依平台而定,跨平台代码应使用<stdint.h>的int32_t这类定宽类型。
这一章在整门课里的位置
本章承接第 1 章"程序要编译成机器码"的前提,回答"机器码里的数据长什么样"。它向下为第 4 章的 printf/scanf 提供依据——每个格式符对应一种类型,类型对不上就会读错数据;向上为第 5、6 章的条件与循环提供基础——关系运算的结果是 int,循环计数器会溢出,浮点累加会累积误差。可以说后面每一章的 bug,一半能追到本章。
相关课程
这篇笔记涉及的主题,我录过对应的视频课程:
- C 语言程序设计 15 讲