- ChungZH 的博客
位运算笔记
- 2022-8-25 20:42:45 @
欢迎访问我的博客:blog.chungzh.cn
基本概念
比特(bit,亦称二进制位)是指 1 位二进制的数码(0 或 1),是计算机中信息的最小单位。
字节(byte):一个字节由 8 位组成。
熟练地运用位运算,可以提高我们程序的时空效率。
计算机中的整数存储与运算
下面以 32 位二进制数,即 C++ 中的 int
和 unsigned int
类型为例。
原码、反码
简单介绍一下:
-
原码:最高位为符号位,正数为 ,负数为 ,其余所有位为十进制数的绝对值。
- 优点:对人类而言最直观。
- 缺点:无法将减法转换成加法运算。如:; 有两种表示方法 和 。
-
反码:最高位为符号位,正数为 ,负数为 。正数的反码等于本身,负数的反码除符号位外,各位取反。
- 优点:解决了减法运算的问题。
- 缺点: 有两种表示方法 和 ;减法算法规则较复杂,需要额外判断溢出。
补码
-
32 位无符号整数
unsigned int
: 直接把这 32 位编码 看作 32 位二进制数 。 -
32 位有符号整数
int
: 以最高位作为符号位, 表示非负数, 表示负数。对于最高位为 的每种编码 ,直接看做 32 位二进制数 。
同时,定义该编码按位取反后得到的编码
~C
表示的数值为 。32 位补码表示 unsigned int
int
可以发现,在补码下,每个数值都有唯一的表示方式,并且任意两个数值做加减法运算,都等价于在 32 位补码下做最高位不进位的二进制加减法运算。发生上/下溢出时,32 位无符号整数和有符号整数都相当于自动对 取模(回绕)。
这也解释了有符号整数算术上溢时会出现负数的现象。我们来对 int
的溢出做一个实验:
void print(int t) { cout << bitset<32>(t) << " " << t << endl; }
int main() {
int t = 2147483647;
print(t);
print(t + 1);
print((t + 1) * 2);
return 0;
}
输出:
01111111111111111111111111111111 2147483647
10000000000000000000000000000000 -2147483648
00000000000000000000000000000000 0
(t+1)*2
的结果本应是 ,依据最高位不进位原则,结果变成了 。
补码也被称为“二补数”(Two's complement)。反码也叫“一补数”(Ones' complement),直接把 (正数)的每一位取反表示负 。补码与反码在负数表示中,绝对值相差 。例如在上面的表格中,第一、四行是一对反码,第二、三行是一对反码。作为整数编码,补码比反码有很多优势。除了上面提到的“自然溢出取模”之外,补码重点解决了 的编码唯一性问题,能比反码多表示一个数。同时减少特殊判断,在电路设计中简单高效。
形式 | 加数 | 和 | |
---|---|---|---|
32 位补码 | |||
int | |||
unsigned int |
“反码加一”只是补码所具有的一个性质,不能被定义成补码。负数的补码,是能够和其相反数相加通过溢出从而使计算机内计算结果变为 的二进制码。这是补码设计的初衷,具体目标就是让 ,这利用原码是无法得到的。
所以对于一个 位的负数 ,有如下关系:
所以 的补码应该是 的二进制编码。
形式 | 加数 | 和 | |
---|---|---|---|
32 位补码 | |||
int | |||
unsigned int |
因为用二进制表示一个 int 需要写出 32 位,比较繁琐。而用十进制表示,又不容易明显地体现出补码的每一位,所以在程序设计中,常用十六进制来表示一个常数,这样只需要书写 8 个字符,每个字符()代表补码下的 4 个二进制位。C++ 的十六进制常数以 0x
开头,0x
本身只是声明了进制,0x
后面的部分对应具体的十六进制数值。例如:
32 位补码 | int(十进制) | int(十六进制) |
---|---|---|
上表中的 是一个很有用的数值,它有两个特性:
- 它的两倍不超过 ,即 int 能表示的最大正整数。
- 整数的每 8 位(每个字节)都是相同的。
我们常用的 memset(a, val, sizeof(a))
初始化一个 int 数组 时,把 填充到数组 的每个字节上,而一个 int 占用 4 个字节,所以用 memset 只能赋值出每 8 位都相同的 int。
所以,当我们想把一个数组中的数值初始化为正无穷时,为了避免加法上溢或者繁琐的判断,可以用 memset(a, 0x3f, sizeof(a))
给数组赋 的值。
移位运算
左移
在二进制表示下把数字同时向左移动,低位以 填充,高位越界后舍弃。
算术右移
在二进制补码表示下把数字同时向右移动,高位以符号位填充,低位越界后舍弃。
注意,整数除法在 C++ 中的实现是向零取整(舍弃小数部分),例如 ,。
逻辑右移
在二进制补码表示下把数字同时向右移动,高位以 填充,低位越界后舍弃。
无符号整数右移使用的是逻辑右移。对于有符号整数,在 C++ 20 前并没有规定使用算术右移还是逻辑右移(大多数平台上进行算术右移)。C++ 20 开始才规定使用算术右移。
参考资料
- 《算法竞赛进阶指南》
- 补码的计算方法 - Murphy - 知乎
- cppreference.com