C语言程序设计 第十二章 位运算
主要内容 12.1 位运算的概念 12.2 位运算 12.3 位运算举例 12.4 位段
12.1 位运算的概念 C语言是为描述系统而设计的,既具有高级语言的特点,又具有低级语言的功能,因而具有广泛的用途和很强的生命力。 在计算机用于检测和控制等诸多领域中要用到位运算的知识,因此读者应当学习和掌握本章的内容。 所谓位运算是指进行二进制位运算。例如,将一个存储单元中的各二进位左移或右移一位,两个数按位相加等。本章讲述C语言位运算的功能。
12.1 位运算的概念 计算机内数据的表示方法 计算机内存储器由许多称为字节的单元组成的,每个字节由8个二进制位(8bit)构成,每位的取值为0或1。最右端的位称为“最低位”,编号为0;最左端的位称为“最高位”,而且按从最低位到最高位顺序,依次编号。下图是一个字节数据和它各二进制位的编号。 计算机中数是用二进制来表示的,数的符号也是用二进制表示的。一般用最高位作为符号位,0表示正数,用1表示负数。 位号 7 6 5 4 3 2 1 各位数据 一个字节数据和它各位编号
12.1 位运算的概念 补码 正数表示形式:符号位为0,其余部分表示数的绝对值。例如,+9的补码是00001001
12.1 位运算的概念 负数表示形式: -9的绝对值按位取反后加1,符号位以1表示负数。由于计算机的字长不同,为方便描述,因此约定用一个字节表示一个整数。见下图:
12.1 位运算的概念 位运算及其运算符 所谓位运算是指,按二进制位进行的运算,即从具有0或1的运算对象出发,计算得出具有0或1的运算结果。C语言的位运算又分为按位操作和移位操作,位操作包括按位与、或、异或和求反运算;移位操作包括左移和右移操作
12.1 位运算的概念 说明 位运算的优先级:由高到低的顺序是:~ → << 、>> → & → | → ^; 位运算的运算对象只能是整型(int)或字符型(char)的数据; 位运算是对运算量的每一个二进制位分别进行;
12.2 位运算 按位与运算(&) 参加运算的两个数据,按二进位进行“与”运算。如果两个相应的二进位都为1,则该位的结果值为1,否则为0。即 0&0=0;0&1=0;1&0=0;1&1=1; 例如: 3&5 并不等于8,应该是按位与。 3 = 0 0 0 0 0 0 1 1 & 5 = 0 0 0 0 0 1 0 1 1 = 0 0 0 0 0 0 0 1 因此,3&5的值得1。如果参加&运算的是负数(如-3 & -5),则以补码形式表示为二进制数,然后按位进行“与”运算。
12.2 位运算 按位与的实际用途 将一个数的某位清零 如果想将某个数的某个比特清零,只要另找一个数,让对应的该比特置0,然后使两数进行按位与&运算,即可达到将此数的特定比特清零的目的。 如:有数为00101011,想让该数从右数的第4比特变成0,其他位保持不变。我们另找一个数,该数从右数的第4比特设它为0,其他位全为1:11110111,将两个数进行&运算: 0 0 1 0 1 0 1 1 & 1 1 1 1 0 1 1 1 0 0 1 0 0 0 1 1
12.2 位运算 取一个数中某些指定位 如有一个两个字节整数 a=(16a1)16,想取出其中的低字节。我们构造一个数b,它的高字节全为0,低字节全为1,即b=(00ff)16=(255)10,只需将a与b按位与即可: a = (16a1)16 = (0001011010100001)2 & b = (00ff )16 = (0000000011111111)2 (00a1)16 = (0000000010100001)2
12.2 位运算 按位或运算(|) 两个相应的二进位中只要有一个为1,该位的结果值为1。即0|0=0; 0|1=1; 1|0=1; 1|1=1。 例如: (060)8 | (017)8 , 将八进制数60与八进制数17进行按位或运算: (060)8 = (0 0 1 1 0 0 0 0)2 | (017)8 = (0 0 0 0 1 1 1 1)2 (077)8 = (0 0 1 1 1 1 1 1)2 按位或常用来对一个数据的某些位定值为1。如:a是一个整数(16位),有表达式a |0377,0377为8进制数。则低8位全置为1。高8位保留原样。
12.2 位运算 异或运算(∧) 异或运算符∧也称XOR运算符。它的规则是若参加运算的两个二进位相同,则结果为0;相异则为1(真): 0∧0=0; 0∧1=1; 1∧0=1; 1∧1=0; 如: 00111001 (十进制数57,八进制数071) ∧ 00101010 (十进制数42,八进制数052) 00010011 (十进制数19,八进制数023) 即071∧052,结果为023(八进制数)。
12.2 位运算 异或运算应用 使特定位翻转:假设有01111010,想使其低4位翻转,即1变为0,0变为1。可以将它与00001111进行∧运算,即 0 1 1 1 1 0 1 0 ∧ 0 0 0 0 1 1 1 1 0 1 1 1 0 1 0 1 要使哪几位翻转就将与其进行∧运算的该几位置为1即可。 与0相∧,保留原值。如012∧00=012 0 0 0 0 1 0 1 0 ∧ 0 0 0 0 0 0 0 0 因为原数中的1与0进行∧运算得1,0∧0得0,故保留原数。
12.2 位运算 交换两个值,不用临时变量:假如a=3,b=4。想将a和b的值互换,可以按以下三个步骤实现: a = a∧b; b = b∧a; 我们用下面的竖式来说明: a = 0 1 1 ∧ b = 1 0 0 a = 1 1 1 (a∧b的结果,a已变成7) b = 0 1 1 (b∧a的结果,b已变成3) ∧ a = 1 1 1 a = 1 0 0 (a∧b的结果,a变成4)
12.2 位运算 “取反”运算(~) 按位取反~是一个单目运算符。例如: (~) 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 (~) 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 取反应用 若一个16位整数a,想使最低一位为0,可以将a 与二进制数1111111111111110 ( 0177776)8 进行按位与: 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 1 & 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 但若将此程序移植到32位计算机上,由于整数用4个字节表示,想将最后一位变成0就不能用a&0177776了,应改用a & 037777777776。这个算法的移植性很差,可以改用a=a&~1:
12.2 位运算 因为在以2个字节存一个整数时,1的二进制形式为0000000000000001,~1是1111111111111110。以4个字节存储一个整数时,~1是1111111111111111111111111111110。 ~运算符的优先级别比算术运算符、关系运算符、逻辑运算符和其他位运算符都高。例如:~a&b,先进行~a运算,然后进行&运算。
12.2 位运算 左移运算(<<) 用来将一个数的各二进位全部左移若干位,右边低位补0 。 例如:若a=15, a=a<<2,即二进制数00001111,左移2位得00111100,即十进制数60。高位左移后溢出,舍弃不用。 左移1位相当于该数乘以2,左移2位相当于该数乘以22=4。上面举的例子15<<2 = 60,即乘了4。但此结论只适用于该数左移时被溢出舍弃的高位中不包含1的情况。 左移比乘法运算快得多,有些C编译程序自动将乘2的运算用左移一位来实现,将乘2n的幂运算处理为左移n位。
12.2 位运算 右移运算符(>>) a>>2表示将a的各二进位右移2位。移出右端的低位被舍弃,对无符号数,高位补0。如a=017时:a为 00001111,a>>2为 00000011|11 。 右移一位相当于除以2,右移n位相当于除以2n。右移需要注意符号位。对无符号数,右移时左边高位移入0。对于有符号的值,如果原来符号位为0(该数为正),则左边也是移入0,如同上例表示的那样。如果符号位原来为1(即负数),则左边移入0还是1,要取决于所用的计算机系统。有的系统移入0,有的移入1。移入0的称为“逻辑右移”,即简单右移。移入1的称为“算术右移”。例如,a的值为八进制数113755: a: 1001011111101101 a>>1: 0100101111110110 (逻辑右移时) a>>1: 1100101111110110 (算术右移时)
12.2 位运算 位运算赋值运算符 位运算符与赋值运算符可以组成复合赋值运算符如:&=, |=, >>=, <<=, ∧=。 例如: a & = b 相当于 a = a & b a << =2 相当于 a = a << 2
12.2 位运算 不同长度的数据进行位运算 如果两个数据长度不同(例如long型和int型)进行位运算时(如a & b, a为long型,b为int型),系统会将二者按右端对齐。如果b为正数,则左侧16位补满0。若b为负数,左端应补满1。如果b为无符号整数型,则左侧添满0。
12.3 位运算举例 例1:将16进制数按二进制输出 #include <stdio.h> void main() { int i,a; printf(“请输入一个十六进制数:"); scanf("%x", &a); printf(“其二进制表示为:"); for (i=15;i>=0;i- -) printf(“%1d”, a&1<<i?1:0); //注意优先级,先移位<<,结果再与a按位与 printf("\n"); } 运行结果: 请输入一个十六进制数:40 其二进制表示为:0000000001000000
12.3 位运算举例 例 2:取一个整数a从右端开始的4~7位。 分析:可以这样考虑: 先使a右移4位。下图a是未右移时的情况,下图b是右移4位后的情况。目的是使要取出的那几位移到最右端。 右移到右端可以用下面方法实现:a >> 4
12.3 位运算举例 设置一个低4位全为1,其余全为0的数。可用下面方法实现: ~ ( ~ 0 << 4 ) 设置一个低4位全为1,其余全为0的数。可用下面方法实现: ~ ( ~ 0 << 4 ) ~0的全部二进制为全1,左移4位,这样右端低4位为0。见下面所示: 0: 0000…000000 ~0: 1111…111111 ~0<<4: 1111…110000 ~(~0<<4): 0000…001111
12.3 位运算举例 将上面二数进行&运算。即:(a >> 4) & ~ ( ~ 0 << 4 ) 根据上一节介绍的方法,与低4位为1的数进行&运算,就能将这4位保留下来。程序如下: #include "stdio.h" void main( ) { unsigned a,b,c,d; scanf("%d",&a); b=a>>4; c=~(~0<<4); d=b&c; printf(“输入数 = %d\n该数右端开始的4~7位数 = %d\n",a,d); } 运行结果: 输入数 = 217 该数右端开始的4~7位数 = 13
12.3 位运算举例 可以任意指定从右面第m位开始向左取n位。只需将程序中的“b=a>>4”改成“b=a>>(m-n+1)”以及将“c=~(~0<<4)”改成“c=~(~0<<n)”即可。
12.4 位段 在计算机过程控制、参数检测、或数据通信领域中,大部分控制信息往往只需用一个或几个二进制位表示即可,若存储一个这样的信息仍然以字节为单位,必然造成内存空间的浪费。为节省存储空间,我们自然想到可以将多个这样的信息放在同一个字节中表示,为此,C语言引入了位段类型。
12.4 位段 位段的概念与定义 所谓位段类型,是一种特殊的结构类型,其所有成员均以二进制位为单位定义长度,并称成员为位段,或称位域。位段类型的定义与结构体类型的定义类似,只不过定义成员的宽度以二进制位为单位。 人为地将一个int型变量data分为几个部分,其中a、b、c、d分别占2位、6位、4位和4位,按位段类型可定义如下:
12.4 位段 struct packed_data { unsigned a:2; unsigned b:6; unsigned c:4; unsigned d:4; } struct packed_data data;
12.4 位段 以上代码定义了位段类型packed_data,它由四个段域组成,其中第0~1位为a,第2~7位b,第8~11位为c,第12~15位为d,共占用两个字节的存储空间,可见,位段类型数据大大节省了存储空间。对位段中的数据进行引用和访问,也类型结构类型,如: data.a = 2; data.b = 25; data.c = 15; 引用时,要注意位段允许的最大范围,如data.c的宽度为4,则它的数据范围为0~15(二进制数的0000~1111)。
12.4 位段 关于位段的定义和引用的几点说明 位段成员的类型必须是unsigned或int类型,可以用%d、%u、%0和%x等格式字符,以整数形式输出位段; 位段赋值时要注意取值范围,通常,长度为n的位段,其取值范围为0~(2n-1); 一个位段必须存储在同一个存储单元,不能跨两个存储单元。如果第一个存储单元空间不能容纳下一个位段,则该空间不用,而从下一个单元起存放该位段。 可以定义无名位段,表示相应空间不用,或定义长度为0的无名位段,表示下一个位段从下一个存储单元开始存放。如:
12.4 位段 struct packed_data { unsigned a:2; unsigned b:4; unsigned :0; /* a、b存储在同一个单元,c、d另存在一个单元 */ unsigned c:3; unsigned :3; /* 这三位空间不用 */ unsigned d:2; } data;
12.4 位段 若data是以上定义的位段类型变量,并有以下引用: data.a=2; data.b=11; data.c=3;data.d=1 则变量data的值如下图所示,其中c从下一字节开始存放,c和d之间有两个二进制位是暂时不用的。