C语言位运算遇到的坑
在Data Lab中有一个logicalShift函数给定一个值x和需要移动的位数n,要求只是用运算符:~ & ^ | + << >>,实现逻辑右移运算。思考了很久,然后我写出了如下的代码:
/*
* logicalShift - shift x to the right by n, using a logical shift
* Can assume that 0 <= n <= 31
* Examples: logicalShift(0x87654321,4) = 0x08765432
Legal ops: ! & ^ | + << >>
* Max ops: 20
* Rating: 3
*/
int logicalShift(int x, int n) {
return ( x >> n ) & ~( ( ( x & 0x80000000) >> n ) << 0x1 );
}
我的思路是:
- 如果x是负值,x >> n会让所有位右移n位,前n位补1,而在逻辑右移中前n位补0。解决这个问题,只需要把除去最高位的其他位置0,然后右移n位,再左移1位(事实上这里只要右移n-1位即可,但题目不能用减号,然后也要考虑n=0的特殊情况),再把这个数取反,和原来右移n位的值按位and,即可得到正确的结果。当n取0,右边的运算结果刚好是0,而左边是x,结果是x,满足。
- 如果x是正值,因为x & 0x80000000得到的结果为0,因此~( ( ( x & 0x80000000) >> n ) << 0x1) )为全1,不影响左边的计算结果。而对于正值,逻辑右移和算术右移是相同的。
比如:
x | n | x >> n | x & 0x80000000 | (x & 0x80000000) >> n | ( ( x & 0x80000000) >> n ) << 0x1) | ~( ( ( x & 0x80000000) >> n ) << 0x1) ) | Result |
---|---|---|---|---|---|---|---|
0x80000000 | 2 | 0xe0000000 | 0x80000000 | 0xe0000000 | 0xc0000000 | 0x3fffffff | 0x20000000 |
一切看起来都很美好,但当运行上述代码的时候,发现输出的结果不是0x20000000而是0xa0000000.那么问题出现在哪?我尝试着打印(x & 0x80000000) >> n的值,发现结果居然是0x20000000,而不是理想中的0xe0000000。怎么回事,负数0x80000000右移2位后居然变成正数0x20000000,看起来有点逻辑右移的味道,所以这里的(x & 0x80000000)应该是unsigned,我试着把代码改成:
/*
* logicalShift - shift x to the right by n, using a logical shift
* Can assume that 0 <= n <= 31
* Examples: logicalShift(0x87654321,4) = 0x08765432
Legal ops: ! & ^ | + << >>
* Max ops: 20
* Rating: 3
*/
int logicalShift(int x, int n) {
int y = ( x & 0x80000000);
int z = y >> n;
return ( x >> n ) & ~( z << 0x1 );
}
然后,一切都没有问题了,顺利通过了。我又发现把最开始给出的错误代码中(x & 0x80000000)中的 0x80000000改成0x7fffffff,结果又被解释称int。
从这个问题,我得到的启发是:
- 只要位运算中出现了unsigned类型,无论另外一个是不是unsigned类型,得到的临时值都是unsigned。
- 负整数的16进制常量值(如0xffffffff表示-1)会被编译器当成unsigned类型,而非负整数则为int类型。
- 遇到位运算的问题时,尽量多做变量定义,不要想当然地错用了类型。