说明:位操作和其他一些低级运算在编写系统程序(包括编译器和操作系统)、加密程序、图形程序以及其他一些需要高执行速度或高效地使用空间的程序时非常有用。
20.1 按位运算符
说明:C语言一共提供了6个按位运算符。
20.1.1 移位运算符
说明:移位运算符可以改变数的二进制形式,将它的位向左或向右移动。
优先级:低于算数运算符i<<2+1 <==> i<<(2+1)
操作数类型要求:可以是任意整型或字符型的
副作用:不存在(不会改变操作数本身)
|运算符|名称|示例|返回值|复合移位运算符|
|-|-|-|-|-|
|<<
|左位移|i<<j
|i
中的位左移j
位的结果(左端溢出,右端补0)|<<=
|
|>>
|右位移|i>>j
|i
中的位右移j
位的结果(右端溢出,左端补0或保存符号位而补1)|>>=
|
1 | unsigned int i, j; |
20.1.2 按位求反、按位与、按位亦或、按位或
优先级:
~
>&
>^
>|
,都比关系运算符
和判等运算符
低。
技巧:使用~
创建在位一级具备可移植性的程序(例子)
- ~0:所有位都为1的整数(否则就要最大的整数,但不同位的机器不同)
- ~0x001f:除了最后5位其它为都为1
|符号|名称|返回值|复合运算符|
|-|-|-|-|
|~
|按位求反|将每一个0替换成1,每一个1替换成0|~=
|
|&
|按位与|对两个操作数相应的位执行逻辑与运算|&=
|
|^
|按位亦或|对两个操作数相应的位执行逻辑或操作,都是1时产生0|^=
|
|\
|按位或|对两个操作数相应的位执行逻辑或操作,都是1时产生1|\=
|
1 | // 假设unsigned int类型的值占16位 |
20.1.3 用按位运算符访问位
说明:通过按位运算,可以提取或修改存储在少数几个位中的数据。
用途:比如,在编写图形程序时,可能会需要讲两个或更多的像素挤在一个字节中,从而降低空间复杂度和时间复杂度。
方式:构造“掩码”,通过按位复合运算修改位设置位(为1)
说明: 通过与“掩码”进行
按位或运算
设置某一位为1
掩码:除要设置的位外都为0(可以通过对整数1进行移位运算构造掩码)(0000000000001000
)。
相关按位运算:按位或运算
1 | i = 0x0000; |
将位清零
说明:通过与“掩码”进行
按位且运算
设置某一位为0
掩码:除要清零的位外都为1的掩码(可以通过移位运算
和按位非运算
)构造(1111111111110111
)。
相关按位运算:按位且运算
1 | i = 0x00ff; // 0000000011111111 |
检测位
说明:检测某一位是否被设置过(设置为1)。
掩码:除要设置的位外都为0(可以通过对整数1进行移位运算构造掩码)(1111111111110111
)。
相关按位运算:按位且运算
1 | // 设置需要的掩码(假设在16位机器上) |
1 | i |= BULE; // 设置BLUE bit(最后一位为1) |
20.1.4 用按位运算符访问位域
位域:连续的几个位
位域下标记法:最右边是最低位,记为0位修改位域
说明:不同于修改位,修改位域并不单纯的只是设置位或清除位,目标值中1和0可以并存,因此多了清除先清除位域的操作。
相关按位运算:按位与
(用来清除位域);按位或
(用来将新的位存入域)
1 | /*将二进制的值101存入变量i的第4-6位*/ |
1 | /*让上名的例子更加通用*/ |
获取位域
说明:获得指定位域上的值。
相关按位运算:按位与
技巧:当位域处在数的末尾区间时,获取值更加方便。如果要获取的位域不在末尾,可以先通过位移移至末尾。
1 | // 0000000000000111 (0x0007) |
1 | j = (i >> 4) & 0x0007; |
20.1.5 程序:XOR加密
说明:将每一个字符与一个密匙进行亦或(XOR)运算;要将信息解码,只需要再次加密,即可得到原来的字符。
注意:读或写包含控制字符的文件时会在一些操作系统中引发错误,应当避免。
1 | -------------加密--------------- |
1 | /** |
1 | $ ./xor < msg.txt |
20.2 结构中的位域
说明:C语言提供了可以在结构中声明存储在位域中的成员。
语法:位域的类型必需是int
、unsigned int
或signed int
。
1 | struct 结构名 { |
注意:使用
按位运算
可以达到同样的效果,而且可能更快些,当可读性不如结构中的位域
。
局限性:通常位域没有地址,因此C语言不允许将&运算符
或scanf函数
用于位域。
可移植性技巧:将所有的位域声明为unsigned int
或signed int
而不是int
,因为一些编译器将位域的最高位作为符号位,而其它一些编译器则不会。
1 | /* |
位域是如何存储的
存储单元:一个存储单元的大小是由实现定义的,通常是8位、16位或32位。
未命名为域:将无法被赋值和使用,但正常占据空间。经常用来作为成员间的填充,以保证其它位域存储在适当的位置。
长度为0的位域:告诉编译器将下一个位域放在一个存储单元的起始位置,即如果当前存储单元还有空间剩余,无论能否放下长度为0的成员的后面的成员,都会将其放在下一个存储单元中。
- 存储顺序:当编译器处理结构实例时,会将位域逐个存入存储单元(从左向右或从右向左)
- 位域型成员之间的间隙:位域之间没有间隙,直到剩下的空间不够放下一个位域(这时会跳到下一个存储单元继续存放,即
存在间隙
或跨存储单元存放,即没有间隙
)。
案例
说明:假设位域是
从右至左
存储,且当一个存储单元剩余的空间无法存储下一个位域成员时会跨存储单元存储
。这时DOS系统上编译常用的方式。
1 | // 声明结构实例 |
* | year | month | day |
---|---|---|---|
存储 | 0001000 | 1100 | 11100 |
大小(bit) | 7 | 4 | 5 |
区间 | 15-9 | 8-5 | 4-0 |
20.3 其他低级技术
20.3.1 定义依赖机器的类型
说明:可以将
char
作为一个字节(不一定存储字符)来使用。
1 | typedef unsigned char BYTE; |
20.3.2 用联合从多个视角看待数据
案例一:使用联合
结合结构体(位域成员)
实现文件日期和整数的转换。
说明:
- 获取:可以通过成员
i
以两个字节的形式获得日期的整数形式- 设置:可以通过成员
fd
以结构体的方式设置文件日期(以两个字节的方式存储)
文件日期定义
1 | /* |
封装一个方便读取和设置数据结构
1 | union int_date { |
实践
1 | /** |
案例二:模拟对寄存器的访问(Intel 80x86)
说明:需要对16位寄存器和8位寄存器进行访问,同时保留它们之间的关系。
寄存器:Intel 80x86处理器包涵4个16位的寄存器(AX(AH|AL)
、BX(BH|BL)
、CX(CH|CL)
、DX(DH|DL)
),其中每个16位寄存器都包含2个8位寄存器。
1 | typedef unsigned char BYTE; |
1 | // 按照8位寄存器的方式修改 |
20.3.3 将指针作为地址使用(将地址转换为指针使用)
说明:指针按照其自身的构造方式可以分为两类(以16位机器为例)
|分类|组成|大小(bit)|地址转换为指针|情景|
|-|-|-|-|
|近指针|偏移量||16|将整数强制转换为指针|在一些计算机中|
|远指针|段地址+偏移量|32|far(关键字,非标准c)
+MK_FP
(dos.h中的宏)|Intel CPU的实时模式(DOS使用的模式)|
1 | // 近指针 |
1 | // 远指针 |
20.3.4 程序:设置Num Lock 键
说明:在
IBM PC
机极其兼容机上,Num Lock
切换涌来确定数字键盘上的按键是作为数字键使用,还是作为移动光标的方向键。
原理:Num Lock
的状态保存在地址位地址段为40(16进制)、偏移量为17(16进制)的字节中。该字节的第5位(最低位为0位)用来控制Num Lock
的状态。
开启Num Lock
1 | /** |
关闭Num Lock
1 | /** |
20.3.5 volatile类型限定符
关键字:
volatile
说明:通常使用在用于指向易变内存空间的指针的声明中。用来防止编译器优化过程中错误地将易变内容缓存在了寄存器中,而不再读取内存中易变内容。
优化前的逻辑
1 | // 优化前的逻辑 |
优化后的逻辑
说明:注意到这个循环中既没有改变p,也没有改变p,因此对程序进行优化,使p只被读取一次。
1 | 在寄存器中存储*p; |