运算符详解


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.运算符总结

  1. 标识符、常量、字符串和用括号()套起来的表达式是组成表达式的基本元素,在运算中做操作数,优先级最高。
  2. 后缀运算符,包括数组取下标[]、函数调用()、结构体取成员.、结构体指针取成员->、后缀自增++、后缀自减--。如果后边有多个后缀,则按照离操作数由近及远的顺序运算,比如 a.name++ ,先算 .,即先取出 a.name ,之后在进行自增运算。
  3. 单目运算符包括前缀自增++,前缀自减--sizeof,强制转换(),取地址运算&,间接寻址*,按位取反~,逻辑非!,正号+,负号-。如果有多个前缀运算符,那么按里操作数由近及远的顺序来进行计算,即由右向左。比如:~!a,先算!a,再取反。
  4. *、除/、取模% 运算符,左结合。
  5. +,减- ,左结合。
  6. 位移运算<<>>,左结合。
  7. 关系运算符<><=>=,左结合。
  8. 相等运算符== !=,左结合。
  9. 按位与&,左结合
  10. 按位异或^,左结合
  11. 按位或|,左结合
  12. 逻辑与&&,左结合
  13. 逻辑或||,左结合
  14. 条件运算符:? 这里会涉及到一个问题,a ? b : c ? d : e是看成(a ? b : c) ? d : e还是a ? b : (c ? d : e)呢?C语言规定是后者。类似于 if/else 的处理。
  15. 赋值=和各种复合赋值,在双目运算符中只有这一项是右结合。
  16. 逗号运算符 ,,左结合。

以下代码得到的sum是0xffff,对吗?

int i = 0;
unsigned int sum = 0;
for (; i < 16; i++)
	sum = sum + 1U<<i;

答案是不是的,由于+优先级要比位移运算高,因此应该加上()才行。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值