位运算
12 位运算
C语言是为描述系统设计的,因此它应该具有汇编语言所以完成的一些功能。C语言既有高级语言的特点,又具有低级语言的功能。因而具有广泛的用途和很强的生命力。
12.1 位运算符和位运算
运算符 含义
& 按位与
| 按位或
^ 按位异或
~ 取反
<< 左移
.>> 右移
说明:
(1)位运算符中除 ~ 外,均为二目运算符,即要求出侧各有一个运算量。
(2)运算早只能是整型或字符型的数据,不能为实型数据。
12.1.1 按位与运算符 &
参加运算的两个数制,按二进制进行 与运算。如果两个相应的二进位数为1,刚该位的结果为 1 否则为 0 即:
0 & 0 = 0;0 & 1 = 0;1 & 0 = 0;1& 1 = 1
例如:3 & 8 并不等于8,应该是按位与
3 = 00000011
5 = 00000101 &
00000001
因此 3 & 5 的值得 1, 如果参加 & 是负数(-3 & -5),则以补码形式表示为二进制数。然后按位进行 与 运算。
按拉与有一些特殊的用途:
(1)清零。如果想将一个单元清零,即使其全部二进位为 0,只要找一个二进制数,其中各个位符合以下条件:原来数中为 1 的位,新数中相应位为 0。然后使二者进行 & 运算,即可以达到清零目的。
(2)取一个数中某些指定位。如有一个整数 a (2个字节)想要其中的低字节。只需将 a 与(337)。按位与即可。
(3)要想将哪一个保留下来,就与一个数进行 & 运算,此数在该位位1,如有一个数 01010100,想把其中左面第3,4,5,7,8可以这样运算:
01010100
00111011 &
00010000
12.1.2 按位或运算符 |
两个相应的二进位中只要有一个为 1,该位的结果就为 1。
0|0=0; 0|1=1; 1|0=1; 1|1=1;
按位或运算常用来对一个数据的某些位定值为1,如 a 是一个整数(16位)有表达式 a & 0377,则低 8 位全置为 1。高 8 位保留原样。
12.1.3 异或运算符 ^
异或运算符 ^ 也称 XOR 运算符。它的规则是若参加运算的两个二进位同号,则结果为0,异号则为1。即 0^0=0; 0^1=1; 1^0=1;1^1=0;
下面举例说明 ^ 运算符的应用。
(1)使特定位翻转
假设有 01111010,想使其低4 位翻转,即 1 变为 0,0 变为 1,可以将它与 00001111进行 ^ 运算,即
01111010
00001111 ^
01110101
结果值的低 4 位正好是原数低4位的翻转。
(2)与 0 相 ^ 保留原值
如 012 ^ 00 = 012
00001010
00000000 ^
00001010
因为原数中的 1 与 0 进行 ^ 运算得 1,0 与 1 运算得 0,故保留原数。
(3)交换两个值,不用临时变量
假如 a = 3, b = 4。想将 a 和 b 的值互换,可以用以下赋值语句实现:
a = a ^ b;
b = b ^ a;
a = a ^ b;
a = 011
b = 100 //a = a ^ b;
a = 111//a = 7
b = 100 //b = b ^ a;
b = 011// b = 3
a = 111//a = a ^ b;
a = 100 // 4
12.1.4 取反运算符 ~
~是一个头单目运算符,用来对一个二进制按位取反,即将 0 变 1,1变 0。例如~25 是对八进制数 25 (即 00010101)按位取反。
00000000 00010101
11111111 11101010 ~
~运算符的优先级别比算术运算符,关系运算符,逻辑运算符和其它运算符都高,例如:~a & b,先进行 ~a 然后进行 & 运算。
12.1.5 左移运算符 <<
用来将一个数各二进位全部左移若干位。例如:
a = a << 2;
将 a 的二进制数左移 2 位,右补 0,若 a = 15,即二进制数 00001111,左移2位得到 00111100,即十进制数60.
高位左移后溢出,舍弃不起作用。
左移一位相当于该数乘以2。但些结论只适用于该数左移时被溢出舍弃的高位中不包含1 的情况。
左移比乘法运算快得多,有些C编译程序自动将乘2的运算用左移来实现。
12.1.6 右移运算符 >>
a >> 2 表示将 a 的各二进位右移 2 位。移到右端的低位被舍弃,对无符号数,高位补 0。如 a = 017 时:
a = 00001111 >> 2
00000011
右移一位相当于除以 2 ,右移 n 位相当于除于 2^n。
在右移时,需要注意符号位问题。对无符号数,右移时左边高位移入 0。对于有符号的值,如果原来符号位为 0 (该数为正),则左边也是移入 0,如果上例表示的那样,如果符号位原来为 1(该数为负),则左边移入的 0 还是 1 ,要取决于所用的计算机系统。移入 0 称为 逻辑右移,即简单右移。移入 1 称为 算术右移。
12.1.7 位运算赋值运算符
位运算符与赋值运算符可以组成复合赋值运算符。
如:&=, |=, >>|, <<=, ^=
12.1.8 不同长度的数据进行位运算
如果两个数据长度不同(例如 long 型和 int 型)进行位运算时(如 a & b 而 a 为 long型,b 为 int 型),系统会将二者按右端对齐。如果 b 为正数,则左侧 16 位补满 0。若 b 为负数,左端应补满 1。如果 b 为无符号整数型,则左侧补满 0。
12.2 位运算举例
取一个整数 a 从右端开始的 4~7 位
(1)先使 a 右移 4 位
a >> 4;
(2)设置一个低 4 位全为 1 ,其余全为 0 的数,可以用下面方法实现:
~(~0<<4)
~0 的全部二进制为 1 ,左移 4 位,这样右端低 4 位为 0。
(3)将上面二者进行 & 运算。
a >> 4 & ~(~0<<4)
#include <stdio.h>
void main()
{
unsigned a, b, c, d;
scanf("%o", &a);
b = a >> 4;
c = ~(~0<<4);
d = b & c;
}
循环移位
#include <stdio.h>
void main()
{
unsigned a, b, c;
int n;
scanf("a=%o, n=%d", &a, &b);
b = a << (16 - n);
c = a >> n;
c = c | b;
printf("%o\n%o", a, c);
}
12.3 位段
以前曾介绍过对内存中信息的存取,存取一般以字节为单位,实际上,有时存储一个信息不必用一个或多个字节,例如,真 或 假 用 0 或 1 ,只需 1 位即可,在计算机用于过程控制,参数检测或是数据通信领域时,控制信息往往只占一个字节中的一个或几个二进位,常常在一直字节中放几个信息。
向一个字节中的一个或几个二进拉赋值和改变它的值可以用以下两种方法:
(1)可以人为的地在两个字节 data 中设几个项。例如 a, b, c, d分别占 2 位,6 位,4位,4位。如果想将 c 的值改变为 12 (设原来为0)
a. 将数12 左移 4 位,使1100 成为右面起第 4 ~ 7位。
b. 将 data 与 12 << 4 进行按拉或,即可以使 c 的值变成 12。
(2)位段
C 语言允许在一个结构体中以位为单元来指定其成员所占内存长度,这种以位为单元的成员称为 位段 或称 位域,利用位段能够用较少的位数存储数据。例如:
struct packed_data
{
unsigned a : 2;
unsigned b : 6;
unsigned c : 4;
undigned d : 4;
int i;
}data;
其中 a, b, c, d 分别占2 位,6位,4位,4位,i 为整型。
也可以使各个位段不恰好占满一个字节。如:
struct packed_data
{
unsigned a : 2;
unsigned b : 3;
unsigned c : 4;
int i;
};
struct packed_data data;
其中 a, b, c 共占 9 位,占 1 个字节多,不到 2 个字节,它后面为 int 型。在 a, b, c 之后的 7 位空间闲置不用,i 从别一个字节开头起存放。
注意,在存储单元中位段的空间分配方向,因机器而异,在微机使用的 C系统中,一般是由右到左进行分配的,但用户可以不必过问这种细节。
对位段中的数据引用的方法。如:
data.a = 2;
data.b = 7;
data.c = 9;
注意位段允许的最大值范围如果写
data.a = 8;
就错了,因为 data.a 只占两个位,最大值为 3。在些情况下,自动取赋的数的低位。例如 8 的二进制形式为 1000 ,而 data.a 只有 2 位,取 1000 的低 2 位,故 data.a 得值 0。
关于位段的定义和引用说明:
(1)位段成员的类型必须指定为 unsigned int 类型。
(2)若某一位段要从另一个字开始存放。可以用以下形式定义
unsigned a : 1;
unsigned b : 2;
unsigned : 0;
unsigned c : 3;
本来 a, b, c 应连续存放在一个存储单元中,由于中了长度为 0 的位段,其作用是使下一个位段从下一下个存储单元开始存放,因此,现在只将 a, b 存储在一个存储中,c 另存放在下一个单元。
(3)一个位段必须存储在同一个存储单元中,不能跨两个单元,如果第一个单元空间不能容纳下一个位段,则该空间不用,而从下一个单元起存放该位段。
(4)可以定义无名字段。如:
unsigned a :1;
unsigned : 2;
unsigned b : 3;// 这两位空间不用
unsigned c : 4;
(5)位段的长度不能大于存储单元的长度,也不能定义位段数组。
(6)位段可以用整型格式输出。如:
printf("%d, %d, %d", data.a, data.b, data.c);
当然也可以用 %u, %o, %x, 等格式输出。
(7)位段可以在数据表达式中引用,它会被系统自动转换成整型数。如:
data.a + 5 / data.b
是合法的。
异或是一种基于二进制的位运算,用符号XOR或者 ^ 表示,其运算法则是对运算符两侧数的每一个二进制位,同值取0,异值取1。它与布尔运算的区别在于,当运算符两侧均为1时,布尔运算的结果为1,异或运算的结果为0。
简单理解就是不进位加法,如1+1=0,,0+0=0,1+0=1。
性质
1、交换律
2、结合律(即(a^b)^c == a^(b^c))
3、对于任何数x,都有x^x=0,x^0=x
4、自反性 A XOR B XOR B = A xor 0 = A
异或运算最常见于多项式除法,不过它最重要的性质还是自反性:A XOR B XOR B = A,即对给定的数A,用同样的运算因子(B)作两次异或运算后仍得到A本身。这是一个神奇的性质,利用这个性质,可以获得许多有趣的应用。 例如,所有的程序教科书都会向初学者指出,要交换两个变量的值,必须要引入一个中间变量。但如果使用异或,就可以节约一个变量的存储空间: 设有A,B两个变量,存储的值分别为a,b,则以下三行表达式将互换他们的值 表达式 (值) :
A=A XOR B (a XOR b)
B=B XOR A (b XOR a XOR b = a)
A=A XOR B (a XOR b XOR a = b)
类似地,该运算还可以应用在加密,数据传输,校验等等许多领域。
运用距离:
1-1000放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现
一次。每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空
间,能否设计一个算法实现?
解法一、显然已经有人提出了一个比较精彩的解法,将所有数加起来,减去1+2+…+1000的和。
这个算法已经足够完美了,相信出题者的标准答案也就是这个算法,唯一的问题是,如果数列过大,则可能会导致溢出。
解法二、异或就没有这个问题,并且性能更好。
将所有的数全部异或,得到的结果与1^2^3^…^1000的结果进行异或,得到的结果就是重复数。
但是这个算法虽然很简单,但证明起来并不是一件容易的事情。这与异或运算的几个特性有关系。
首先是异或运算满足交换律、结合律。
所以,1^2^…^n^…^n^…^1000,无论这两个n出现在什么位置,都可以转换成为1^2^…^1000^(n^n)的形式。
其次,对于任何数x,都有x^x=0,x^0=x。
所以1^2^…^n^…^n^…^1000 = 1^2^…^1000^(n^n)= 1^2^…^1000^0 = 1^2^…^1000(即序列中除了n的所有数的异或)。
令,1^2^…^1000(序列中不包含n)的结果为T
则1^2^…^1000(序列中包含n)的结果就是T^n。
T^(T^n)=n。
所以,将所有的数全部异或,得到的结果与1^2^3^…^1000的结果进行异或,得到的结果就是重复数。
当然有人会说,1+2+…+1000的结果有高斯定律可以快速计算,但实际上1^2^…^1000的结果也是有规律的,算法比高斯定律还该简单的多。
google面试题的变形:一个数组存放若干整数,一个数出现奇数次,其余数均出现偶数次,找出这个出现奇数次的数?
解法有很多,但是最好的和上面一样,就是把所有数异或,最后结构就是要找的,原理同上!!
奇数个异或是本身,偶数个是0;0^a=a;异或有交换律