跳至内容
Topos Uranos

摘要

程序中的整数不是数学中的整数:它是一个有限的位序列,用 nn 个位恰好能容纳 2n2^n 个不同的值。这一课研究这种表示及其后果。它教你用二进制和十六进制写出一个整数,并用格式说明 {:b} 和 {:x} 加以核对;介绍 C++ 的整数类型、用 sizeof 测量的类型大小,以及用 std::numeric_limits 查询的取值范围;解释负数如何用二进制补码表示,以及取值范围为什么不对称;区分有符号整数和无符号整数,并研究运算超出取值范围时会发生什么:对无符号数,标准规定了结果,即模 2n2^n 运算;对有符号数,标准没有规定,程序失去一切保证。最后给出为每个数据选择类型的准则。

学习目标

  1. 在十进制、二进制和十六进制之间转换整数,并用格式说明 {:b} 和 {:x} 核对转换结果。
  2. 计算 nn 位整数在有符号和无符号时的取值范围,并用 std::numeric_limits 和 sizeof 加以核对。
  3. 解释二进制补码,并写出一个较小负数的八位表示。
  4. 区分有符号溢出(未定义行为)和无符号溢出(模 2n2^n 运算),并预测后者的结果。
  5. 根据数据可预见的取值范围选择合适的整数类型,在宽度重要时使用 <cstdint> 中的定宽类型。

进位制

变量与内存一课把变量介绍为一个有名字、类型和值的存储单元。然而,内存保存的不是十进制数字,而是位,每一位的值是 0 或 1,每八位组成一个字节(标准要求至少八位;现今所有系统都恰好使用八位)。因此,要知道一个整数变量能取哪些值,就必须知道如何只用两个数字写出一个数,而这正是二进制这种进位制所做的事。

在基数为 bb 的进位制中,每一位数字的值取决于它所在的位置:最后一位乘以 b0=1b^0 = 1,它前面的一位乘以 b1b^1,再前面的一位乘以 b2b^2,依此类推。在十进制中,20262026 表示 2⋅103+0⋅102+2⋅101+6⋅1002 \cdot 10^3 + 0 \cdot 10^2 + 2 \cdot 10^1 + 6 \cdot 10^0。在二进制中,数字只有 0 和 1,各位的权是 2 的幂:1,2,4,8,16,32,64,128,…1, 2, 4, 8, 16, 32, 64, 128, \ldots。由此得出从二进制化为十进制的规则:把有 1 的那些位置的权相加。

例题从二进制到十进制

图中读取八位的位模式 00101101。每个格子上方标着它的权;各个 1 从左到右逐个点亮,值随之累加它们的权。

证明

  1. 八个格子全部熄灭:值为 0。各位的权从右到左依次为 1,2,4,…,1281, 2, 4, \ldots, 128。

  2. 第一个 1 在权为 32 的位置:累计值为 32。

  3. 接着是权为 8 的位置:32+8=4032 + 8 = 40。

  4. 再接着是权为 4 的位置:40+4=4440 + 4 = 44。

  5. 最后一个在权为 1 的位置:44+1=4544 + 1 = 45。位模式 00101101 表示数 45。

  6. 把位四个一组分组,就得到十六进制数字:0010 是 2,1101 是 13,在十六进制中写作 d。因此,45 在十六进制中写作 2d。

反方向的路,从十进制到二进制,用连续除法来走:把这个数除以 2,记下余数(0 或 1),再对商重复这一步,直到商为 0;把余数从最后一个读到第一个,就是二进制数字。对于 45:45=2⋅22+145 = 2 \cdot 22 + 1,22=2⋅11+022 = 2 \cdot 11 + 0,11=2⋅5+111 = 2 \cdot 5 + 1,5=2⋅2+15 = 2 \cdot 2 + 1,2=2⋅1+02 = 2 \cdot 1 + 0,1=2⋅0+11 = 2 \cdot 0 + 1;把余数倒过来读,得到 101101,左边补上两个 0,就是图中的位模式。道理在于:每个余数都是被除的那个数的个位数字,而每个商都是那个数向右移动一位的结果。同样的方法适用于任何基数:对十六进制就除以 16,余数 10 到 15 用字母 a 到 f 表示。

基数 16,即十六进制,是为了方便而存在的:由于 16=2416 = 2^4,每个十六进制数字恰好对应四个位,一个字节用两个数字就能写出。一个 32 位的数用二进制要写三十二个数字,很难读;用十六进制只要八个,而且不必计算就能逐个数字地转换成二进制。所以程序员用十六进制阅读内存。进位制的数学基础在“代数与几何 I”课程中的“进位记数制”一课中研究。

C++ 允许直接用这些进位制书写整数:以 0b 开头的字面量是二进制的,以 0x 开头的是十六进制的。在任何字面量中都可以加一个撇号作为数字分隔符,编译器会忽略它,读者则会感激它:0b0010'1101,1'000'000。输出时,格式说明 {:b} 以二进制写出整数,{:x} 以小写字母的十六进制写出,{:X} 则用大写字母;符号 # 加上前缀,而前面带一个 0 的宽度会在左边补 0。

例题同一个数,三种写法
C++ 程序
#include <print>​int main(){    const int decimal{45};    const int binary{0b0010'1101};    const int hexadecimal{0x2d};    std::println("{} {} {}", decimal, binary, hexadecimal);    std::println("二进制:{:b}", decimal);    std::println("八位:{:08b}", decimal);    std::println("十六进制:{:x} {:X} {:#x}", decimal, decimal, decimal);}
输出
45 45 45二进制:101101八位:00101101十六进制:2d 2D 0x2d

三个变量保存的是同一个值,只是用三种进位制写出,输出的第一行显示了这一点:进位制是写法的性质,而不是数的性质。在这三种情况下,内存保存的都是同一个位模式。格式说明 {:b} 省略左边的 0;{:08b} 补足八位。