C 语言第 2 章:数据类型——补码、字节与取值范围的完整对应

写于 2026-11-01

这一章是全课程最"硬"的一章,也是最能拉开差距的一章。数据类型不是语法装饰,它直接决定三件事:这块内存占几个字节、这串 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 这类十进制小数,应该用"分"为单位的整数或定点数表示。

易错点

  1. 以为 char 只能存字符。它就是一个字节的整数,char c = 65; 与 char c = 'A'; 等价。
  2. 忽略溢出。int 的最大值加 1 会变成最小负数(补码回绕),且不报错。
  3. 有符号与无符号混算。-1 < 1u 的结果是假,因为 −1 被转成无符号后是 4294967295。
  4. 用 == 比较浮点数。这是精度问题的经典表现,必须用容差。
  5. 认为 8 位补码范围是 −127~127。−128 没有对应的原码和反码,只有补码能表示。
  6. 默认 sizeof(int) == 4。标准只保证 short ≤ int ≤ long,具体宽度依平台而定,跨平台代码应使用 <stdint.h> 的 int32_t 这类定宽类型。

这一章在整门课里的位置

本章承接第 1 章"程序要编译成机器码"的前提,回答"机器码里的数据长什么样"。它向下为第 4 章的 printf/scanf 提供依据——每个格式符对应一种类型,类型对不上就会读错数据;向上为第 5、6 章的条件与循环提供基础——关系运算的结果是 int,循环计数器会溢出,浮点累加会累积误差。可以说后面每一章的 bug,一半能追到本章。

本文是《C 语言程序设计》第 2 章的知识点摘要。完整板书与代码见 课程页;上一章 概述,下一章 算法基础。

相关课程

这篇笔记涉及的主题,我录过对应的视频课程: