15.0 位运算说明
位运算与其他一些底层运算在编写系统程序、加密程序、图像图形程序以及其他一些需要高执行速度或高效利用空间的程序时比较有用,但是由于某些位运算符与操作数是否有无符号有关,在编写普通的应用程序时并不建议使用。
注:本章内容基本都是复制了《C语言程序设计_现代方法(第2版)》的第20章-底层程序设计的相关内容。
15.1 移位运算
移位运算符可以向左或向右移动整数的二进制表示。C语言提供了两个移位运算符。
| 符号 | 含义 |
|---|---|
| << | 左移位 |
| >> | 右移位 |
运算符<<和>>的操作数可以是任意整数类型。对两个操作数都会进行整数提升,返回值的类型是左操作数提升后的类型。
i << j的值是将i中的位(的值)向左移动j位后的结果(的值)。向左移动一位的结果是:原最左端的位会移出,原最左端右边的各位都会左移一位,而最右端会补一个0。把i向左移动j位,可以理解为把i的值j次向左移动一位。
i >> j的值是将i中的位右移j位后的结果。向右移动一位的结果是:如果i是无符号数或非负值,需要在i的左端补0。如果i是负值,其结果是由实现定义的,一些实现会在左端补0,其他一些实现会保留符号位而补1。
为了可移植性,最好仅对无符号数进行移位运算。
下面演示对数19应用移位运算符的效果(简单起见,本章例子都使用16位的无符号短整型)
unsigned short i, j;
i = 19; // i的二进制是 0000000000010011 ,十进制是19
j = i << 1; // j的二进制是 0000000000100110 ,十进制是38
j = i << 2; // j的二进制是 0000000001001100 ,十进制是76
j = i >> 1; // j的二进制是 0000000000001001 ,十进制是9
j = i >> 2; // j的二进制是 0000000000000100 ,十进制是4
就像+运算符不会改变其操作数一样,两个移位运算符也都不会改变它的操作数(i的值不变)。如果需要修改i,需要把结果赋值给i,
i = 19; // i的二进制是 0000000000010011 ,十进制是19
i = i << 1; // i的二进制是 0000000000100110 ,十进制是38
i = i >> 1; // i的二进制是 0000000000010011 ,十进制是19
或者使用复合赋值运算符
i = 19; // i的二进制是 0000000000010011 ,十进制是19
i <<= 1; // i的二进制是 0000000000100110 ,十进制是38
i >>= 1; // i的二进制是 0000000000010011 ,十进制是19
其中,i <<= 1相当于i = i << 1,i >>= 1相当于i = i >> 1。
注意,移位运算符的优先级比算术运算符的优先级低,因此可能产生意料之外的结果。比如,i << 2 + 1等同于i << (2 + 1),而不是(i << 2) + 1。
15.2 按位求反、与、或、异或运算符
| 符号 | 含义 |
|---|---|
| ~ | 按位求反 |
| & | 按位与 |
| | | 按位或 |
| ^ | 按位异或 |
运算符~是一元运算符,对其操作数会进行整数提升。其他运算符都是二元运算符,对其操作数进行常用的算术转换。
运算符~、&、|和^对操作数的每一位执行布尔运算。~运算符会产生对操作数求反的结果,即将每一个0替换成1,将每一个1替换成0。运算符&对两个操作数对应的位执行逻辑与运算。运算符|和^相似,对两个操作数对应的位执行逻辑或运算。只是当两个操作数对应的位都是1时,^产生0。
下面的例子演示了这几个运算符,
unsigned short i, j, k;
i = 23; // i的二进制是0000000000010111 十进制是23
j = 53; // j的二进制是0000000000110101 十进制是53
k = ~i; // k的二进制是1111111111101000 十进制是65512
k = i & j; // k的二进制是0000000000010101 十进制是21
k = i | j; // k的二进制是0000000000110111 十进制是55
k = i ^ j; // k的二进制是0000000000100010 十进制是34
~是一元运算符,所以优先级很高,而移位运算符的优先级次之,介于加减(+、-)和关系运算符(<、<=、>、>=)之间,而&、^和|运算符的优先级低于移位运算符,&、^和|运算符的优先级由高到低,介于判等关系运算符(==、!=)和逻辑与&&运算符之间。除了~运算符的结合性是从右到左,其他位运算符的结合性都是从左到右。参考C语言的运算符的优先级和结合性
所以,下面的式子
if (status & 0x4000 != 0)...
的运算顺序是先计算0x4000 != 0(结果是1),接着判断status & 1是否非0,而不是判断status & 0x4000是否非0。
不要搞混&、^和|运算符和逻辑运算符&&、||,前3者是按位进行逻辑运算,而后两者是对变量整体看成逻辑真或者假进行逻辑运算。虽然有时他们的结果一样,但是他们绝不等同。比如,
unsigned short i, j, k;
i = 1;
j = 2;
k = i && j; // k是1
k = i & j; // k是0
| 逻辑运算 | 按位运算 | |
|---|---|---|
| 与 | && | & |
| 或 | || | | |
| 异或 | 无 | ^ |
注意,C语言有位的异或运算符,但是没有逻辑异或运算符,不过可以用其他逻辑运算符实现,
int xor(int a, int b){
return (a && !b) || (!a && b);
}
复合赋值运算符&=、^=、|=分别对应于位运算符&、^、|,
unsigned short i, j;
i = 23; // i的二进制是0000000000010111 十进制是23
j = 53; // j的二进制是0000000000110101 十进制是53
i &= j; // i的二进制是0000000000010101 十进制是21
i ^= j; // i的二进制是0000000000100000 十进制是32
i |= j; // i的二进制是0000000000110101 十进制是53
15.3 用位运算符访问位
在进行底层编程时,经常将信息存储在单个或一组位中,这样可以节省空间。比如,可能将一个像素的信息(颜色等)存储在一个字节中。使用位运算符,我们可以操作位中的数据。
假设i是16位的unsigned short变量,我们来看看如何对i进行最常用的单位运算。
1. 位的设置
假设我们需要设置i的第4位。(假定最高有效位为第15位,最低有效位是第0位。)设置第4位的方法是将i与常量0x0010(在第4位为1,其他位是0的16位整数常量,作为掩码)进行或|运算,
i = 0x0000; // i的二进制是0000000000000000
i |= 0x0010; // i的二进制是0000000000010000
更通用的做法是,如果需要设置位的位置值存储在变量j中,可以用左移运算符来构造掩码,
i |= 1 << j; // 设置第j位
如果j的值是3,1 << j是二进制的0000000000001000
2. 位的清除
要清除i的第4位(置为0),可以使用第4位为0,其余位为1的掩码,
i = 0x00ff; // i的二进制是0000000011111111
i &= ~0x0010; // i的二进制是0000000011101111
如果需要清除特定的位,假设为的位置存储在变量j中,
i &= ~(1 << j); // 清除第j位
3. 位的判断
要判断i的第4位是否被设置(为1),可以使用
if (i & 0x0010) ...
如果要判断第j位是否被设置,可以使用下面的语句,
if (i & (1 << j)) ... // 判断第j位是否被设置
由于移位运算符的优先级高于按位求与运算符,故小括号可以省略,
if (i & 1 << j) ... // 判断第j位是否被设置
为了使对于位的操作更容易,可以给它们起名字。例如,如果我们想要使一个数的第0、1和2位对应于相应的颜色蓝、绿和红。首先,定义分别代表这三个位的位置的名字:
#define BLUE 1 // 0000000000000001
#define GREEN 2 // 0000000000000010
#define RED 4 // 0000000000000100
设置、清除、判断GREEN位可以如下进行,
i |= GREEN; // 设置GREEN位
i &= ~GREEN; // 清除GREEN位
if (i & GREEN) ... // 判断GREEN位
同时设置、清除、判断几个位也类似,
i |= GREEN | RED; // 设置GREEN位和RED位
i &= ~(GREEN | RED) // 清除GREEN位和RED位
if (i & (GREEN | RED)) ... // 判断GREEN位或者RED位是否被设置
其中,if语句判断GREEN位或者RED位是否被设置。
如果要判断GREEN位和RED位是否都被设置,应该怎样写呢?
可以用逻辑运算符&&
if ((i & GREEN) && (i & RED)) ...
由于运算符&的优先级高于&&,故可以省略小括号,
if (i & GREEN && i & RED) ...
15.4 文件加密
对数据加密的一种最简单的方法是将每一个字符与一个秘钥进行异或(XOR)运算。假设秘钥是字符&。如果将它与字符z进行异或,我们会得到字符\(假定使用ASCII字符集)。具体计算如下:

要将消息解密,只需要用相同的算法。即只要对加密后的消息再次加密,即可得到原始的消息。例如,如果将字符&与字符\进行异或,就可以得到原来的字符z:

假设要对文件中的所有内容进行加密处理,秘钥是字符&,可以利用文件处理函数和异或运算进行。
下面的程序从原始文件(假设是文本文件)中依次读取每个字符,并将它加密,加密的字符存入新的文件中。由于我们还是希望加密后的文件是可读的,所以我们在加密过程中进行了判断,只要原字符或者加密后的字符有一个是不可打印的字符,就将原字符写入,否则,才写入加密后的字符。像换行符、制表符就不是可打印的字符,可以使用头文件ctype.h中的isprint函数来判断一个字符是否是可打印的字符。
#include <stdio.h>
#include <stdlib.h> // for exit(EXIT_FAILURE);
#include <ctype.h> // for isprint
#define KEY '&'
#define FILE_NAME_LEN 20
int main()
{
FILE *fp_src, *fp_dst;
char name_src[FILE_NAME_LEN + 1], name_dst[FILE_NAME_LEN + 1];
int ch, new_ch;
printf("请输入要加密的文件名:");
gets(name_src);
printf("请输入存放加密后信息的文件名:");
gets(name_dst);
if((fp_src = fopen(name_src, "r")) == NULL){
printf("不能打开文件:%s\n", name_src);
exit(EXIT_FAILURE);
}
if((fp_dst = fopen(name_dst, "w")) == NULL){
printf("不能打开文件:%s\n", name_dst);
fclose(fp_src);
exit(EXIT_FAILURE);
}
while((ch = fgetc(fp_src)) != EOF){
new_ch = ch ^ KEY;
if (isprint(ch) && isprint(new_ch)){
fputc(new_ch, fp_dst);
}else{
fputc(ch, fp_dst);
}
}
fclose(fp_src);
fclose(fp_dst);
printf("加密完成\n");
getchar();
return 0;
}
本文详细介绍了C语言中的位运算,包括移位运算、按位求反、与、或、异或运算符的使用,以及如何用位运算符访问和操作位。还探讨了位运算在文件加密中的应用。
4504

被折叠的 条评论
为什么被折叠?



