C_第15章 位操作

本文详细介绍了C语言中的位操作,包括二进制数、位和字节的概念,位运算符如按位与、按位或、按位异或、移位运算符的使用,并通过实例展示了位操作在位字段和数据存储中的应用。此外,还讨论了有符号整数的原码、补码和反码表示方法,以及位操作在数值表示和数据处理中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第15章 位操作

  • 运算符:~&|^<<>>&=|=^=>>=<<=
  • 二进制、十进制和十六进制计数法(复习)
  • 处理一个值中的位的两个C工具:位运算和位字段
  • 关键字:_Alignas_Alignof

15.1 二进制数、位和字节

计算机适用基底为 2 的数制系统。它用 2 的幂而不是 10 的幂。

eg: 二进制数 1101 可表示为:

1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0

15.1.1 二进制整数

1 字节(byte) 包含 8 位。

在这里插入图片描述

这里,12827 次幂,以此类推。该字节能表示的最大数字是把所有位都设置为 111111111。这个二进制数的值是:

128 + 64 + 32 + 16 + 8 + 4+ 2 + 1 = 255

而该字节最小的二进制数是 00000000,其值为 0

因此,1 字节可储存 0 ~ 255 范围内的数字,总共 256 个值。

或者,通过不同的方式解释 位组合(bit pattern),程序可以用 1 字节储存 -128 ~ +127 范围内的整数,总共还是 256 个值。

例如,通常 unsigned char1 字节表示的范围是 0 ~ 255,而 signed char1 字节表示的范围是 -128 ~ +127

15.1.2 有符号整数

原码、补码、反码:

原码

如何表示有符号整数取决于硬件,而不是C语言。
也许表示有符号数最简单的方式是用 1 位(如,高阶位)储存符号,只剩下 7 位表示数字本身(假设存储在 1 字节中)。

用这种**符号量(sign-magnitude)**表示法,
10000001 表示 -1
00000001 表示 1
因此,其表示范围是 -127 ~ +127

缺点:这种方法的缺点是有两个 0+0-0。这很容易混淆,而且用两个位组合来表示一个值也有些浪费。

补码

**二进制补码(two’s-complement)**方法避免了这个问题,是当今最常用的系统。

我们将以 1 字节为例,讨论这种方法。

  • 方法一:二进制补码用 1 字节中的后 7 位表示 0 ~ 127,高阶位设置为 0。目前,这种方法和符号量的方法相同。
  • 方法二:另外,如果高阶位是 1,表示的值为负。
  • 上述这两种方法的区别在于如何确定负值。

如何确定负值:从一个 9 位组合 100000000 (256 (2^8) 的二进制形式) 减去一个负数的位组合,结果是该负值的量。

举例:假设一个负值的位组合是 10000000
作为一个无符号字节,该组合为表示 128 (2^7);
作为一个有符号值,该组合表示负值(编码是7的位为1),而且值为 100000000 - 10000000,即10000000,值为 2^8 - 2^7 = 128

类似的,
10000001(129) 是 -127 (即 -(256 - 129) = -127),
11111111(255) 是 -1,(即 -(256 - 255) = -1)。

【总结】如何求补码?

第一步:先求该二进制数作为无符号字节时,该组合表示的值 x

第二步:用 - (2^8 - x) 即可求出当该二进制数作为有符号值时的负值。

反码

**二进制反码(one’s-complement)**方法通过反转位组合中的每一位形成一个附属。

例如:000000011,那么 11111110-1

缺点:这种方法也有一个 -0。该方法能表示 -127 ~ +127 之间的数。

15.1.3 二进制浮点数

十进制小数中,使用 10 的幂作为分母:

eg: 0.527 表示为:

5/10 + 2/100 + 7/1000

二进制小数中,使用 2 的幂作为分母:

eg: .101 表示为:

1/2 + 0/4 + 1/8,即 0.625

实际上,二进制表示法只能精确地表示多个 1/2 的幂的和。
因此,3/4 和 7/8 可以精确地表示为二进制小数,但是 1/3 和 2/5 却不能。

15.2 其他进制数

15.2.1 八进制

**八进制(octal)**是指八进制记数系统。基于 8 的幂,用 0~7表示数字。
例如:八进制数 451(在C中写作 0451)表示为:

4 * 8^2 + 5 * 8^1 + 1 * 8^0 = 297(十进制)

了解八进制的简单方法是,每个八进制位对应 3 个二进制位。
这种关系使得八进制与二进制之间的转换很容易。

例如:八进制数 0377 的二进制形式是 11111111

111 代替 0377 中的最后一个 7,再用 111 代替倒数第 27,最后用 011 代替 3,并社区第1位的 0

这表明比 0377 大的八进制要用多个字节表示。

这是八进制唯一不方便的地方:
一个 3 位的八进制数可能要用 9 位二进制数来表示。

表15.1 与八进制位等价的二进制位

八进制位等价的二进制位
0000
1001
2010
3011
4100
5101
6110
7111

15.2.2 十六进制

**十六进制(hexadecimal 或 hex)**是指十六进制记数系统。基于 16 的幂,用 0 ~ 15表示,0 ~ 9表示数字,而 10 ~ 15 用字母 A ~ F 或者小写 a ~ f 来表示。

例如:十六进制数 A3F(在C中写作 OxA3F 或者 0xa3f)表示为:

10 * 16^2 + 3 * 16^1 + 15 * 16^0 = 2623(十进制)

每个十六进制为都对应一个 4 位的二进制数(即 4 个二进制位),那么两个十六进制为恰好对应一个 8 位字节。第 1 个十六进制表示前 4 位,第 2 个十六进制位表示后 4 位。因此,十六进制很适合表示字节值。

表 15.2 列出了各进制之间的对应关系。
例如:十六进制值 0xC2 可转换为 11000010
相反,二进制值 11010101 可以看做是 1101 0101,课转换为 0xD5

表15.2 十进制、十六进制和等价的二进制

十进制十六进制等价二进制
000000
110001
220010
330011
440100
550101
660110
770111
881000
991001
10A1010
11B1011
12C1100
13D1101
14E1110
15F1111

15.3 C按位运算符

8 位二进制数,从左往后每位的编号为 7 ~ 0

15.3.1 按位逻辑运算符

4 个按位逻辑运算符都用于整型数据,包括 char
之所以叫做 **按位(bitwise)**运算,是因为这些操作都是针对每一个位进行,不影响它左右两边的位。

注意:不要把这些按位运算符与常规的逻辑运算符(&&||!)混淆,常规的逻辑运算符操作的是整个值。

1. 二进制反码或按位取反:~

一元运算符 ~1 变为 0,把 0 变为 1

~ (10011010)    // 表达式
(01100101)      // 结果值

例:假设 val 的类型是 unsigned char,已被赋值为 2
在二进制中,00000010 表示 2
那么,~val的值是 11111101,即 253

newval = ~val;
printf("%d", ~val);

// 把 val 的值改为 ~val:
val = ~val;
2. 按位与:&

二元运算符 & 通过逐位比较两个运算对象,生成一个新值。
对于每个位,只有两个运算对象中相应的位都为 1 时,结果才为 1

例:对下面的表达式求值

(10010011) & (00111101) // 表达式
// 由于两个运算对象中编号为 4 和 0 的位都为 1,得:
(00010001)              // 结果值
3. 按位或:|

对于每个位,如果两个运算对象中相应的位为 1,结果就是 1(只有有一个为真,则结果为真)。

(10010011) | (00111101) // 表达式
// 除了编号为6的位,其他位至少有一个位为 1
(10111111)              // 结果值
4. 按位异或:^

对于每个位,如果两个运算对象中相应的位一个为 1(但不是两个为 1),结果为 1。(只有一真一假时,结果才为 1)

(10010011) ^ (00111101) // 表达式
// 编号为 0 的位都是 1,所以结果为 0,得:

(10101110)              // 结果值

15.3.2 用法:掩码

按位与运算符常用于 掩码(mask)。所谓掩码指的是一些设置 (1)(0) 的位组合。

要明白其为掩码的原因,先来看通过 & 把一个量与掩码结合后发生什么情况。

例如,假设定义符号常量 MASK2 (即,二进制形式为 00000010),只有1号位是 1,其他位都是 0

flags = flags & MASK;
// 或者写成

flags &= MASK;

flags除1号位以外的所有位都设置为 0,因为使用按位与运算符(&),任何位与 0 组合都得 0

1号位的值不变(如果1号位是 1,那么 1&11;如果1号位是 0,那么 0&1 也得0)。这个过程叫做“使用掩码”,因为掩码中的 0 隐藏了 flags 中相应的位。

可以这样类比:把掩码中的 0 看作不透明,1 看作透明。
表达式 flags & MASK 相当于用掩码覆盖在 flags 的位组合上,只有 MASK1 的位才可见。

在这里插入图片描述

再举例:

ch &= 0xff; /*或者 ch &= 0377; */

前面介绍过 0xff 的二进制形式是 11111111,八进制形式是 0377
这个掩码保持 ch 中最后 8 位不变,其他位都设置为 0.
无论 ch 原来是 8 位、16 位或是其他更多位,最终的值都被修改为 18 位字节。
在该例中,掩码的宽度为 8 位。

15.3.3 用法:打开位(设置位)

有时,需要打开一个值中的特定位,同时保持其他位不变。

例如,一台IBM PC通过向端口发送值来控制硬件。为了打开内置扬声器,必须打开 1 号位,同时保持其他位不变。这种情况可以使用**按位或(|)**运算符。

以上一节的 flags 和 MASK (只有1号位为 1)为例。

flags = flags | MASK;
// 或者写成

flags |= MASK;

flags 的 1号位设置为 1,且其他位不变。

因为使用 | 运算符,

  • 使用 |,任何位与 0 组合,结果都为本身;
  • 使用 |, 任何位与 1 组合,结果都为 1

例如:

假设 flags00001111MASK10110110。下面的表达式:flags | MASK
即是:
(00001111) | (10110110) // 表达式

其结果为:(10111111) // 结果值

总结:根据 MASK 中为 1 的位,把 flags 中对应的位设置为 1,其他位不变。

  • MASK 中为 0 的位,flags 与其对应的位不变
  • MASK 中为 1 的位,flags 与其对应的位也为 1

15.3.4 用法:关闭位(清空位)

和打开特定的位类似,有时也需要在不影响其他位的情况下关闭指定的位。
假设要关闭变量 flags 的1号位。同样 MASK 只有1号位为 1(即,打开)。可以这样做:

flags = flags & ~MASK;
// 或者写成

flags &= ~MASK;

由于 MAKS 除1号位为 1 以外,其他位全为 0
所以 ~MASK 除1号位为 0 以外,其他位全为 1

  • 使用 &,任何位与 1 组合都得本身;
  • 使用 &,任何位与 0 组合都得 0
  • 所以无论1号位的初始值是什么,都将其设置为 0

例如,假设 flags 是 00001111,MASK 是 10110110。

下面的表达式: flags & ~MASK

即是:(00001111) & ~(10110110) // 表达式

其结果为:(00001001) // 结果值

MASK 中为 1 的位在结果中都被设置(清空)为 0
flags 中与 MASK0 的位相应的位在结果中都未改变。

15.3.5 用法:切换位

切换位置的是打开已关闭的位,或关闭已打开的位。

可以使用**按位异或(^)**运算符切换位。

  • 假设 b 是一个位(10),如果 b1,则 1^b0;如果 b0,则 1^b1
  • 另外,无论 b1 还是 0, 0^b 均为 b

例如,假设 flags 是 00001111, MASK 是 10110110。表达式:

flags ^ MASK
即是:
(00001111) ^ (10110110) // 表达式

其结果为:
10111001 // 结果值

flags: 00001111
MASK: 10110110
结果: 10111001

总结flags 中 与 MASK1 的位相对应的位都被切换了,MASK0 的位相对应的位不变。

15.3.6 用法:检查位的值

有时,需要检查某位的值。
例如:flags 中1号位是否被设置为 1?

我们必须覆盖 flags 中的其他位,只用 1 号位MASK 比较:

if ((flags & MASK) == MASK)
    puts("Wow!");

由于按位运算符的优先级比 == 低,所以必须在 flags & MASK 周围加上圆括号。

为了避免信息漏过边界,掩码至少要与覆盖的值宽度相同。

15.3.7 移位运算符

1. 左移: <<

左移运算符(<<),假设 stonk1,那么 stonk << 24,但是 stonk 本身不变,仍为 1

int stonk = 1;
int onkoo;
onkoo = stonk << 2; /* 把 4 赋给 onkoo */
stonk <<= 2;        /* 把 sotnk 的值改为 4 */
2. 右移: >>
int sweet = 16;
int ooosw;

ooosw = sweet >> 3; // ooosw = 2,sweet 的值仍然为 16
sweet >>= 3;        // sweet 的值为 2

【总结】位移运算
<<运算
a<<b 表示把a转为二进制后左移 b 位(在后面添加 b0)。例如 100 的二进制表示为 1100100,100 左移 2 位后(后面加 2 个零):1100100<<2 = 110010000 = 400,可以看出,a<<b 的值实际上就是 a 乘以 2 的 b 次方,因为在二进制数后面添加一个 0 就相当该数乘以 2,2 个零即 2 的 2 次方 等于 4。通常认为 a<<1 比 a*2 更快,因为前者是更底层一些的操作。因此程序中乘以 2 的操作尽量用左移一位来代替。

定义一些常量可能会用到 << 运算。你可以方便的用 1<<16 - 1 来表示 65535(unsingned int 最大值16位系统)。很多算法和数据结构要求数据模块必须是 2 的幂,此时就可以用<<来定义 MAX_N 等常量。

>>运算

<< 相似,a>>b 表示二进制右移 b 位(去掉末 b位),相当于 a 除以 2b 次方(取整)。我们经常用>>1 来代替 /2(div 2),比如二分查找、堆的插入操作等等。想办法用 >> 代替除法运算可以使程序的效率大大提高。最大公约数的二进制算法用除以 2 操作来代替慢的出奇的%(mod)运算,效率可以提高 60%。

3. 用法:移位运算符

移位运算符针对 2 的幂提供快速有效的乘法和除法:

number << n;    // number 乘以 2 的 n 次幂
number >> n;    // 如果 number 为非负,则用 number 除以 2 的 n 次幂

15.3.8 编程示例

15.3.9 另一个例子

15.4 位字段

15.4.1 位字段示例

15.4.2 位字段和按位运算符

15.5 对齐特性(C11)

15.6 关键概念

15.7 本章小结

15.8 复习题

15.9 编程练习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值