导语
这一篇会让你更加熟练合理的利用操作符,有一些代码是很坑爹的:
int i = 1;
int ret = (++i) + (++i) + (++i);
是不是很熟悉?
其实这个是个有问题的代码,至于是为什么?
我们往下看。
操作符分类:
1.算术操作符
+ - * / %
这些都是我们常用的算术操作符,注意以下的三个点就好了。
- 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
- 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
- % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
2. 移位操作符
这两个操作符是关于二进制的问题。
<< 左移操作符 >> 右移操作符
注:移位操作符的操作数只能是整数。
2.1 左移操作符
移位规则:
左边抛弃、右边补0
我们来定义一个整型:
int num = 10;
10的二进制是这样的
00000000000000000000000000001010
向左位移1位(num<<1)
00000000000000000000000000010100
num = 20
当然,如果num没有被赋值,就无法使用位移操作符。
2.2 右移操作符
移位规则:
首先右移运算分两种:
- 逻辑移位
左边用0填充,右边丢弃 - 算术移位
左边用原该值的符号位填充,右边丢弃
至于到底是哪个?还要看我们的编译器。(这里用的是VS2022)
首先我们来定义一个整型,如果它是一个正数,按照两个规则,都是左边补零,毫无意义,所以我们要定义一个负数。
int num = -1;
我们之前了解过,最左边的是符号位,1是负数,0是整数。
正常来说,我们认为二进制应该是这样子的:
10000000000000000000000000000001
其实在内存中并不是这个样子的,我们把这种能人工转换成其他进制的叫做原码。我们内存当中存的是补码。
我们在原码转换成补码的时候还有一个反码的过程。
反码就是把除了符号位的其他部位都反过来,补码在反码的基础上加1就可以了。至于正数?正码反码补码全都相同。
现在我们来看代码:
#include <stdio.h> int main() { int a = -1; printf("%d\n", a); a = a >> 1;//向右位移一位 printf("%d\n", a); return 0; }
代码运行如下:
也就是说我们目前VS2022编译器用的是算数移位。
注意:对于移位运算符,不要移动负数位,这个是标准未定义的。
int num = 10; num>>-1;//错误的
3. 位操作符
位操作符有:
& //按位与 | //按位或 ^ //按位异或 注:他们的操作数必须是整数
这些操作符也是二进制方面的操作。
我们用代码举例:
#include <stdio.h> int main() { int num1 = 1; int num2 = 2; int a = num1 & num2; int b = num1 | num2; int c = num1 ^ num2; printf("%d\n", a); printf("%d\n", b); printf("%d\n", c); return 0; }
代码运行的结果是这样的:
具体是怎么回事呢?
我们首先来看a:
按位与是两个整形变量相对应的二进制位如果有两个1就变成1,如果只有一个1或者是没有1那么只能是0。
也就是说我们a的二进制全都是0,那么a转换为十进制就是0。
b:
按位或是两个整形变量相对应的二进制位如果有一个或者是两个1就变成1,如果没有1只能是0。
b的二进制转换成十进制就变成了3。
c:
按位异或是两个整型变量,相应的二进制位值不同则为1,否则为0。
c的二进制转换成十进制是3。
有一道面试题就涉及到了位操作符:
不能创建临时变量(第三个变量),实现两个数的交换。
我们原本的方法是需要第三个变量来交换,这次却不可以,这道题的参考代码是:
#include <stdio.h> int main() { int a = 10; int b = 20; a = a ^ b;//一 b = a ^ b;//二 a = a ^ b;//三 printf("a = %d b = %d\n", a, b); return 0; }
过程是这样的:
一
a 001010
b 010100
a 100001
二
a 100001
b 010100
b 001010
三
a 100001
b 001010
a 010100
我们发现a^b^b=a这样一个情况,以后可以适当利用。
代码运行结果为:
20 10
再来一道让我们熟悉上面的操作符
求一个整数存储在内存中的二进制中1的个数。
我们有三种方法,参考代码如下:
//方法1 #include <stdio.h> int main() { int num = 10; int count = 0;//计数 while (num) { if (num % 2 == 1) count++; num = num / 2; } printf("二进制中1的个数 = %d\n", count); return 0; } //思考这样的实现方式有没有问题? //方法2: #include <stdio.h> int main() { int num = -1; int i = 0; int count = 0;//计数 for (i = 0; i < 32; i++) { if (num & (1 << i)) count++; } printf("二进制中1的个数 = %d\n", count); return 0; } //思考还能不能更加优化,这里必须循环32次的。 //方法3: #include <stdio.h> int main() { int num = -1; int i = 0; int count = 0;//计数 while (num) { count++; num = num & (num - 1); } printf("二进制中1的个数 = %d\n", count); return 0; }
方法一:
10的二进制是1010
10模2是0
10/2=5
5的二进制是101
我们发现少了一个0,如果一直如下下去就等于右位移操作符的原理。
可是如果是负数呢?就造成了死循环。
方法二:
那么我们限制它的位数不就可以了吗?
我们知道32位系统就有32个比特位,32个二进制位。
i是1~31的数字,也就是说让1循环向左位移,一共循环32次,也就是说1的二进制中的1会在32个比特位上都会出现。
然后把你要算的整型按位与上面一直循环的1就可以了。
方法三:
这个方法真的难以想到,不过这样可以避免因为32位和64位系统位数从而导致不同的结果。
11111111111111111111111111111111
11111111111111111111111111111110
11111111111111111111111111111110
11111111111111111111111111111110
11111111111111111111111111111101
11111111111111111111111111111100
11111111111111111111111111111100
11111111111111111111111111111011
11111111111111111111111111111000
11111111111111111111111111111000
11111111111111111111111111110111
11111111111111111111111111110000
以此循环就能算出来二进制中有多少个1。
4. 赋值操作符
赋值操作符这是我们经常用的一个操作符,同时也是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
double salary = 10000.0;//如果你对工资不满意,那就修改 salary = 30000.0; int weight = 150;//体重 weight = 120;//不满意就赋值 //赋值操作符可以连续使用,比如: int a = 10; int x = 0; int y = 20; a = x = y+1;//连续赋值 //但是这样的代码读起来感觉不是很好,所以我们最好不要这样写 x = y+1; a = x; //这么写代码可读性才高
复合赋值符
+= -= *= /= %= >>= <<= &= |= ^=
这些运算符都可以写成复合的效果。
比如:
int x = 10; x = x+10; //和下面的表达式效果相同 x += 10;//复合赋值 //其他运算符一样的道理。这样写更加简洁。
5. 单目操作符
什么是单目操作符?就是需要一个操作数控制的操作符,比如说我们之前见过的&(取地址操作符)后面需要一个变量才能使用,后面的变量就是操作数。
5.1 单目操作符介绍
! //逻辑反操作 - //负值 + //正值 & //取地址 sizeof //操作数的类型长度(以字节为单位) ~ //对一个数的二进制按位取反 -- //前置、后置-- ++ //前置、后置++ * //间接访问操作符(解引用操作符) (类型) 强制类型转换
关于sizeof其实我们之前已经见过了,可以求变量(类型)所占空间的大小。
参考代码:
#include <stdio.h> int main() { int a = -10; int* p = NULL; printf("%d\n", !2); printf("%d\n", !0); a = -a; printf("%d\n", a); p = &a; printf("%d\n", *p);//解引用是把指针变量储存的地址给打开然后访问。这里就等于a printf("%d\n", sizeof(a)); printf("%d\n", sizeof(int)); printf("%d\n", sizeof a);//这样写行不行? printf("%d\n", sizeof int);//这样写行不行? return 0; }
如果把这一段代码放在编译器里会发现sizeof int编译不过去,会给你报错,sizeof是一个操作符不是一个函数,可以后面省略括号,但是如果是计算数据类型只能加括号,这是语法规定!
我们把最后一段表达式注释掉,然后运行代码:
!号是逻辑反操作符。我们知道,非零是真,零是假。
!2是让真变成假,!0让假变成真,我们计算机默认真打印1,假打印0。
~是按位取反的操作符,也是关于二进制:
我们定义一个整型变量
#include <stdio.h> int main() { int a = -1; printf("%d", ~a); return 0; }
这就是按位取反,这个会把符号位也取反。
还有前置和后置的符号:
#include <stdio.h> int main() { int a = 1; printf("%d\n", a++); printf("%d\n", a); printf("%d\n", ++a); printf("%d\n", a); return 0; }
前置是前++后使用,后置是先使用后++,- -也是一样。
代码运行结果是:
至于强制类型转换:
double a = 1.0; int b = (int)a;
正常来说double类型储存到int类型的会丢失精度,编译器也会报警告,我们这时就要用强制类型转换,把a强制类型转换成int类型然后储存进a中。
5.2 sizeof 和 数组
我们来看一下这段代码:
#include <stdio.h> void test1(int arr[]) { printf("%d\n", sizeof(arr));//(2) } void test2(char ch[]) { printf("%d\n", sizeof(ch));//(4) } int main() { int arr[10] = { 0 }; char ch[10] = { 0 }; printf("%d\n", sizeof(arr));//(1) printf("%d\n", sizeof(ch));//(3) test1(arr); test2(ch); return 0; }
问:
(1)、(3)两个地方分别输出多少?
(2)、(4)两个地方分别输出多少?
(这里用的是32位平台)
我们看一下代码运行的结果:
(1)和(3)很容易理解,(2)和(4)是什么情况?
我们知道,数组传参传的是首元素地址,那么函数中的sizeof算的就是首元素地址的长度,不同数据类型的地址的长度在32位平台下是4个字节,64位平台下是8个字节。
6. 关系操作符
也可以是双目操作符
> >= < <= != //用于测试“不相等” == //用于测试“相等”
这些关系运算符比较简单,没什么可说的,但是我们要注意一些运算符使用时候的陷阱。
例如:在编程的过程中== 和=不小心写错,导致的错误。
7. 逻辑操作符
&& //逻辑与 || //逻辑或
参考代码如下:
#include <stdio.h> int main() { int a = 1; int b = 1; int c = 0; int d = 0; if (a && b) { printf("Q\n"); } if (a && c) { printf("W\n"); } if (c && d) { printf("E\n"); } if (a || b) { printf("R\n"); } if (a || c) { printf("T\n"); } if (c || d) { printf("Y\n"); } return 0; }
输出结果:
逻辑与是两边的操作数都为真才能通过,逻辑与是两边的操作数有一个为真就会通过。
区分逻辑与和按位与
区分逻辑或和按位或
1&2----->0
1&&2---->1
1|2----->3
1||2---->1