1.位运算
整数在计算机中用二进制表示,C语言中提供了一些运算符直接对整数的位进行操作,叫做位运算,对于位运算操作数必须为整型的。
1.1 按位与、或、异或、取反运算
在C语言中提供了 按位与 &
(Bitwise AND)、按位或|
(Bitwise OR)、按位异或^
(Bitwise XOR)、按位取反~
(Bitwise NOT)。在计算机中数的表示有写过这几种运算的真值表,这里再次说明^
的记忆,当两个操作位相同时取值为0,不同时取值为1。
注意:使用^、|、&时会进行Usual Arithmetic Conversion (其中有一部分是 Integer Promotion) ,~也要做Integer Promotion 所以在C语言中进行位运算时至少会将类型提升为 int
。
同时编译器遇到非整型作位运算时编译无法通过。
unsigned char i = 0xfc;
unsigned int j = ~i;
比如上边的两行代码,0xfc是 int
型,当赋值时要进行Usual Arithmetic Conversion ,此时值不变,当把i升为int型为000000fc然后取反,为ffffff03,如果看成8位取反得到3就错了。
1.2 位移运算(操作数类型可以不一致的运算符)
位移运算包括左移<<
和右移>>
,作用是将数分别向对应方向移动相应位数。
例如:
0x cfff fff3 << 2 = 0x 3fff ffcc .
0x cfff fff3 = 1100 1111 1111 1111 1111 1111 1111 0011
向左移动两位之后变为
0x 3fff ffcc = 0011 1111 1111 1111 1111 1111 1100 1100
最高位的11被移出去,之后最低为由于左移产生的空余部分用0填补。但要注意所移动位数要小于该类型所占位
数,例如 unsigned int 当移动位数超过32时所得到的结果是Undefined。
还有这里可以发现对于位运算来说,运算符两端的操作数类型不一致,但两边操作数都要做Integer Promotion
运算技巧: 我们可以利用位运算来提高乘法的效率,在一定范围内,位移运算可以取代乘二和除二的运算,而且执行效率会更高,这种方法对有符号数也是用,不过要保证在进行位运算后最高位符号位不变,如果变了那么就不是乘2了,所以才说要在一定范围内。可以用下面代码观察一下:
int i = 0xcffffff3;
printf("%d,%x\n",i,i);
i <<= 2;
printf("%d,%x\n",i,i);
i = 0xcffffff3;
i >>= 1;
printf("%d,%x\n",i,i);
// 0xcfff fff3 = 1100 1111 1111 1111 1111 1111 1111 0011
上边分别进行了左移和右移,在向左移动的时候会将符号位移除变为0,符号变换,当向右移时最高位补位是Implementation-define。
因此在操作数是无符号位时,在一定范围内进行唯一操作是有效的。
当操作数是有符号型时:
- 如果是正数,那么高位右移移入0;
- 如果是负数,右移时的补位就是Implementation-define,对于×86平台的gcc编译器,最高位移入1,也就是仍然保持负号,这样的话依然可以实现右移除以二的特性。
因此,可以看出有符号数的位运算设计到的规则十分繁琐,很不方便操作,因此建议在位操作时使用无符号数。
int i = 0xcffffff3; // 有符号型
printf("%x\n", 0xcffffff3>>2); //无符号型, 右移补位为0,因此打印的值应该为 0x 33ff fffc
printf("%x\n", i>>2); //有符号行,右移部位为1,因此打印的值应该为 0x f3ff fffc
1.3 掩码
如果要对一个整数中的位进行操作,就要用到位操作运算符,下面介绍几种常用手段,其中可以用掩码来表示所要获取的位的位置,例如0x0000ff00表示对一个32位的整数的8~15位进行操作。
1、 去除8~15位
unsigned int a, b, c, mask = 0x0000ff00;
a = 0x12345678;
b = (mask & a) >> 8; // b = 0x00000056;
c = (a >> 8) & ~( ~0U << 8); // c = 0x00000056
以上两种方法均可获得,第一种方法直接使用掩码和按位与获得,而第二种方法使用 ~0U
进行取反和左移等操作来获得掩码,也是很方便的办法,而且不用再创建一个变量,不过这里要注意的是,0后边的U是十分必要的,因为如果不加U的话那么取反后得到的就不是无符号数而是有符号的,而且上一小节提到过,有符号的位运算是十分繁琐的,所以为保证是无符号数运算,所以这里一定要加上U。
2、将8~15位清零
unsigned int a, b, mask = 0x0000ff00;
a = 0x12345678;
b = a & ~mask; // b = 12340078;
这里同样利用掩码,将相应位置置为全1,再进行操作。
3、将8~15位置一
unsigned int a, b, mask = 0x0000ff00;
a = 0x12345678;
b = a | mask; // b = 0x1234ff78;
以上三种操作我们可以发现,一般来说对那个位操作就把掩码相应位置置为1,再利用位运算来进行操作。
下面是几个使用掩码实现的代码
1、统计一个无符号整数的二进制表示中1的个数,函数原型是int countbit(unsigned int x);
include <stdio.h>
int count_bit(unsigned int num)
{
int flag = 0;
for(int i = 0;i < 32;i++)
if((num >> i) & 1U == 1)
flag++;
return flag;
}
int main(void)
{
unsigned num;
printf("Input a number:");
scanf("%d", &num);
printf("%x,%x\n", num, count_bit(num));
return 0;
}
2、用位操作实现无符号整数的乘法运算,
函数原型是unsigned int multiply(unsigned int x, unsigned int y);。
例如:(11011)2×(10010)2=((11011)2<<1)+((11011)2<<4)。
#include <stdio.h>
unsigned int mutiply(unsigned int a, unsigned int b)
{
int flag = 0, result = 0;
for(int i = 0;i < 32;i++)
if(((b >> i) & 1U) == 1) result += a << i;
return result;
}
int main(void)
{
int a, b;
printf("Input two numbers:");
scanf("%d %d", &a, &b);
printf("%x,%d\n",mutiply(a,b),mutiply(a,b));
return 0;
}
这里可以参考十进制的竖式运算的原理来理解二进制的运算。
3、对一个32位无符号整数做循环右移,
函数原型是unsigned int rotate_right(unsigned int x);。
所谓循环右移就是把低位移出去的部分再补到高位上去,
例如rotate_right(0xdeadbeef, 16)的值应该是0xefdeadbe。
#include <stdio.h>
unsigned int rotate_right( unsigned int num, unsigned int x)
{
unsigned int mask = ~(~0 << x); //先获得掩码
mask = mask & num; // 获得由于移动而失去的位的内容
mask = mask << (32-x); // 移动到num移动后相应空缺的位置
num >>= x; // 实现右移
num |= mask; // 由于右移时左侧补位为0,所以使用按位或将右侧数据补上
return num;
// return ((~(~0U << x) & num) << (32-x)) | ( num >> x); // 这行代码也可以实现上边相同效果
}
int main(void)
{
unsigned int num = 0xdeadbeaf;
num = rotate_right(num, 16);
printf("%x\n", num);
return 0;
}
1.4 异或运算的一些特性
1、一个数和自己做异或的结果是0。如果需要一个常数0,x86平台的编译器可能会生成这样的指令:xorl %eax, %eax。不管eax寄存器里的值原来是多少,做异或运算都能得到0,这条指令比同样效果的movl $0, %eax指令快,因为前者只需要在CPU内部计算,而后者需要访问内存。
2.从异或的真值表可以看出,不管是0还是1,和0做异或保持原值不变,和1做异或得到原值的相反值。可以利用这个特性配合掩码实现某些位的翻转,例如:
unsigned int a, b, mask = 1U << 6; // mask = 0x 0000 0020;
a = 0x12345678;
b = a ^ mask; // b = 0x 1234 56E8;
3、如果a1 ^ a2 ^ a3 ^ … ^ an的结果是1,由于0做按位异或不改变值,所以可知则a1、a2、a3…an之中1的个数为奇数个,否则为偶数个。这条性质可用于奇偶校验(Parity Check),比如在串口通信过程中,每个字节的数据都计算一个校验位,数据和校验位一起发送出去,这样接收方可以根据校验位粗略地判断接收到的数据是否有误。
4、x ^ x ^ y == y,因为x ^ x 0,0 ^ y == y。这个性质有什么用呢?我们来看这样一个问题:交换两个变量的值,不得借助额外的存储空间,所以就不能采用temp = a; a = b; b = temp;的办法了。利用位运算可以这样做交换:
a = a ^ b;
b = b ^ a;
a = a ^ b;
分析一下这个过程。为了避免混淆,把a和b的初值分别记为a0和b0。第一行,a = a0 ^ b0;第二行,把a的新值代入,得到b = b0 ^ a0 ^ b0,等号右边的b0相当于上面公式中的x,a0相当于y,所以结果为a0;第三行,把a和b的新值代入,得到a = a0 ^ b0 ^ a0,结果为b0。注意这个过程不能把同一个变量自己跟自己交换,而利用中间变量temp则可以交换。
2.其他运算符
2.1 复合赋值运算符
复合赋值运算符包括:+=
、-=
、*=
、/=
、%=
、>>=
、<<=
、&=
、|=
、^=
,一边运算一边赋值。例如 a += 1
等于 a = a + 1
。但有一点差别,前者运算一次,后者运算两次,如果a是一个复杂表达式,求值一次和求值两次效率是不一样的,例如a[foo()] += 1和a[foo()] = a[foo()] + 1,如果foo()函数调用有Side Effect,比如会打印一条消息,那么前者只打印一次,而后者打印两次。
2.2 条件运算符
条件运算符是一种三目运算符格式为: A ? B : C
先对A求值如果为真则执行B,如果为假则执行C,值为B或C的值,同时要求A表达式的值为标量类型。
2.3 逗号运算符
逗号运算符是一种双目运算符,形式如 表达式1,表达式2
先对表达式1求值,之后把值扔到,对表达式2求值即为整个运算结果。逗号运算符是左结合的,类似于四则运算,会有表达式1,表达式2,表达式3……表达式n
,这样表达式n的值就是运算结果。
另外在函数的参数列表中也有逗号,不过这是分隔符不是逗号运算符,但是可以这样使用逗号运算符f(a,(n=3,n+1),b);
。在这个函数调用中有三个参数,其中第二个参数为n+1的值。
2.4 sizeof 运算符和 typedef 类型声明
sizeof 有两种运算形式," sizeof 表达式 " 和 " sizeof (类型名) "。
对于不加括号的类型,不计算表达式的值而是根据类型转换规则进行类型转换,最后将所得类型所占字节的个数最为结果。在表达式的位置也可以有括号,和return(1)的括号是相同的。
而第二种,必须在类型名处加上括号,整个表达式的值也就是这个类型所占的字节数。
例如用 sizeof 来求运算符的长度:
int a[12];
printf("%d\n", sizeof a / sizeof a[0]);
在这个例子中由于 sizeof 不需要求值,所以在编译的时候就将后边的表达式替换为12了。
sizeof 的运算结果是 size_t
类型,在 stddef.h
中定义的,定义为一种无符号整型,声明如下:
typedef unsigned long size_t;
由于不同平台定义 size_t
类型时定义的类型不同,所以才使用类型声明,这样使代码具有可移植性,同时注意不要将 size_t
当作 unsigned long
使用。
例如:
unsigned long x;
size_t y;
x = y;
如果在IPL32平台中,会把 size_t
定义为 unsigned long long
所以在赋值时会将y的高位截掉,就过就有可能会出错。
类型名命名也遵循标识符命名规则,而且通常在末尾加一个 _t
后缀表示Type。
3.运算符总结
- 标识符、常量、字符串和用括号
()
套起来的表达式是组成表达式的基本元素,在运算中做操作数,优先级最高。 - 后缀运算符,包括数组取下标
[]
、函数调用()
、结构体取成员.
、结构体指针取成员->
、后缀自增++
、后缀自减--
。如果后边有多个后缀,则按照离操作数由近及远的顺序运算,比如a.name++
,先算.
,即先取出a.name
,之后在进行自增运算。 - 单目运算符,包括前缀自增
++
,前缀自减--
,sizeof
,强制转换()
,取地址运算&
,间接寻址*
,按位取反~
,逻辑非!
,正号+
,负号-
。如果有多个前缀运算符,那么按里操作数由近及远的顺序来进行计算,即由右向左。比如:~!a,先算!a,再取反。 - 乘
*
、除/
、取模%
运算符,左结合。 - 加
+
,减-
,左结合。 - 位移运算
<<
和>>
,左结合。 - 关系运算符
<
、>
、<=
、>=
,左结合。 - 相等运算符
==
!=
,左结合。 - 按位与
&
,左结合。 - 按位异或
^
,左结合。 - 按位或
|
,左结合。 - 逻辑与
&&
,左结合。 - 逻辑或
||
,左结合。 - 条件运算符
:?
这里会涉及到一个问题,a ? b : c ? d : e是看成(a ? b : c) ? d : e还是a ? b : (c ? d : e)呢?C语言规定是后者。类似于 if/else 的处理。 - 赋值
=
和各种复合赋值,在双目运算符中只有这一项是右结合。 - 逗号运算符
,
,左结合。
以下代码得到的sum是0xffff,对吗?
int i = 0;
unsigned int sum = 0;
for (; i < 16; i++)
sum = sum + 1U<<i;
答案是不是的,由于+优先级要比位移运算高,因此应该加上()才行。