C语言中的位级运算、逻辑运算和移位运算
位级运算
C语言的一个很有用的特性就是它支持按位布尔运算。事实上,我们在布尔运算中使用的那些符号就是C语言所使用的:|就是OR (或), &就是AND (与), ~就是NOT (取反),而^就是EXCLUSIVE-OR (异或),这些运算能运用到任何“整型”的数据类型上,也就是那些声明为char或者int的数据类型,无论它们有没有像short. long、long long或者unsigned这样的限定词。以下是一些对char数据类型表达式求值的例子。
位级运算的一个常见用法就是实现掩码运算,这里掩码是一个位模式,表示从一个字中选出的位的集合。让我们来看一个例子,掩码0xFF (最低的8位为1)表示一个字的低位字节。位级运算x&0xFF生成一个由x的最低有效字节组成的值,而其他的字节就被置为0。比如,对于x=0x89ABCDEF,其表达式将得到0x000000EF表达式~0将生成一个全1的掩码,不管机器的字大小是多少。尽管对于一个32位机器来说,同样的掩码可以写成0xFFFFFFFF,但是这样的代码不是可移植的。
逻辑运算
C语言还提供了一组逻辑运算符||、 &&和! ,分别对应于命题逻辑中的OR,AND和NOT运算。逻辑运算很容易和位级运算相混淆,但是它们的功能是完全不同的。逻辑运算认为所有非零的参数都表示TRUE,而参数0表示FALSE.它们返回1或者0,分别表示结果为TRUE或者为FALSE。以下是一些表达式求值的示例。
表达式 | 结果 |
---|---|
!0x41 | 0x00 |
!0x00 | 0x01 |
!!0x41 | 0x01 |
0x69&&0x55 | 0x01 |
0x69||0x55 | 0x01 |
可以观察到,按位运算只有在特殊情况下,也就是参数被限制为0或者1时,才和与其对应的逻辑运算有相同的行为。 逻辑运算符&&和||与它们对应的位级运算&和|之间第二个重要的区别是,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。 因此,例如,表达式a&&5/a
将不会造成被零除,而表达式p&&*p++
也不会导致间接引用空指针。
举个栗子:
假设x和y的字节值分别为0x66和0x39. 填下表,指明各个C表达式的字节值.
表达式 值 x & y x | y ~x | ~y x & !y x && y x || y !x||!y x && ~y
- 注意:C语言中的逻辑运算非零参数都是为1,即TRUE;而参数零表示FALSE,他们只会返回1和0.
答案:
表达式 值 x & y 0x20 x | y 0x7F ~x | ~y 0xDF x & !y 0x00 x && y 0x01 x || y 0x01 !x||!y 0x00 x && ~y 0x01
再来个小练习:
只使用位级和逻辑运算,编写一个C表达式, 它等价于 x==y. 换句话说, 当x和y相等时它将返回1,否则返回0.
答案:
表达式: !(x^y)
当且仅当x的每一位和y相应的每一位匹配时, x ^ y 等于0,然后利用!来判定一个字是否包含任何非零位.
移位运算
C语言中的移位运算
C语言还提供了一组移位运算,以便向左或者向右移动位模式。对于一个位表示为[xn-1 , xn-2 ,… ,x0 ]的操作数x,C表达式 x<<k 会生成一个值,其位表示为[xn-k-1 , xn-k-2 , … ,x0 , … , 0]也就是说, x向左移动k位,丢弃最高的k位,并在右端补k个0。移位量应该是一个0 ~ n-1之间的值。移位运算是从左至右可结合的,所以x<<j<<k等价于(x<<j) << k。有一个相应的右移运算x>>k,但是它的行为有点微妙。一般而言,机器支持两种形式的右移:逻辑右移和算术右移。逻辑右移在左端补k个0, 得到的结果是[0, … , 0, xn-1 , xn-2 , … ,xk ]。算术右移是在左端补k个最高有效位的值,得到的结果是[xn-1 , … , xn-1 , Xn-1 , xn-2, … ,xk ]。这种做法看上去可能有点奇特,但是我们会发现它对有符号整数数据的运算非常有用。
注意:逻辑左移和算术左移是一样的,逻辑右移和算术右移是不一样的
附加:
- 涉及计算机中的移位操作指令的问题
在计算机中只能以二进制的形式进行移位,向左移动 |n| 位,就相当于乘于2n ,其中n也分正负值,向左为正值,向右为负值。
算术左移,操作数左移,最低位补0,逻辑左移,操作数左移,最低位补0。
算术右移,操作数右移,最高位不变,逻辑右移,操作数右移,最高位补0。
左移操作相当于乘法操作,它是会让操作数变大的,也就是说,它随时存在着溢出的风险。若左移操作保留符号位的话,那么左移会出现越来越小的情况,但是你的本意是让这个数变大最后却发现左移后的数变小了,为了规避这种溢出带来的问题,统一了左移操作,也就是说,在左移操作中,寄存器只负责储存这个数(不管是否合乎人意,它只要是个数就行),若没有溢出的时候,则原样存储。所以说左移操作是会改变符号位的。
我们知道右移操作的算术操作是除法,那么在计算机中是肯定让这个数变小的,所以不存在溢出的风险,那么设计者就把是否保留符号位的权力交给了程序编写人员,所以
SAR
和SHR
是两个不同的操作。
sal,sar,shl,shr (其中sal和shl功能完全等价)
维基百科中有介绍:
Logical shifts are best used with unsigned numbers
逻辑移位最好用于无符号数In an arithmetic shift, the spaces are filled in such a way to preserve the sign of the number being slid. For this reason, arithmetic shifts are better suited for signed numbers in two’s complement format
在算术移位中,移位空间被补0或者保留高位(即符号位)的方式处理,因此,算术移位操作更适用于两个有符号数的补码操作
C语言标准并没有明确定义应该使用哪种类型的右移。对于无符号数据(也就是以限定词unsigned声明的整型对象),右移必须是逻辑的。而对于有符号数据(默认的声明的整型对象),算术的或者逻辑的右移都可以。不幸的是,这就意味着任何假设一种或者另一种右移形式,的代码都潜在着可移植性问题。然而,实际上,几乎所有的编译器/机器组合都对有符号数据使用算术右移,且许多程序员也都假设机器会使用这种右移。另一方面, Java对于如何进行右移有明确的定义。表达式x>>k会将x算术右移k个位置,而x>>>k会对x做逻辑右移。
举个栗子:
填下表,说明不同移位运算对单字节数的影响.思考移位的最好方式是使用二进制表示. 将最初的值转换为二进制执行移位运算,然后再转换回十六进制.每个答案都应该是8个二进制数字或者2个十六进制数字.
注:单字节数(一个字节位8位,因此只有8位二进制数),有溢出风险,可能会使得原本移位后变大的数经过移位后变小.
答案:
此例子可以帮助理解不同的移位运算