1.算术操作符
+ (加) - (减) *(乘) /(除) % (取模)
对于这些算术操作符,我们在初识C语言已经有了基本的了解。
这里,我们需要对 / 和% 再着重强调下。
#include<stdio.h> int main() { // 1./得到的是商 // 2.%得到的是余数 printf("%d\n", 10 / 2);// 5 printf("%d\n", 10 % 2);// 0 return 0; }
对于/ 操作符如果两个操作符都为整数,执行整数除法,而只要有一个操作符为浮点数,那么执行的就是浮点数除法。
#include<stdio.h> int main() { //lf打印double类型 printf("%lf\n", 10 / 3.0);// 3.333333 printf("%lf\n", 10.0 / 3.0);// 3.333333 return 0; }
对于%操作符,两个操作数必须为整数。返回的是整除之后的余数。
2.移位操作符
<< (左移操作符) >> (右移操作符)
2.1 左移操作符
移位操作符移动的是二进制位数字,什么是二进制数字?
这里就不得不详细牵扯一下整数在内存中存储的相关知识。
我们平常说的“173”、“56”、“1924”,这些都是十进制数字,即由0~9的数字组成。拿“173”的这个十进制数字来说,个位数‘3’的权重为10^0,十位数‘7’的权重为10^1,百位数‘1’的权重为10^2,即 1*10^2+7*10^1+3*10^0 = 173 。
那么由此可以知道二进制就是由0~1的数字组成的,考虑二进制的1010表示的数值(十进制)是多少呢?
所以,二进制的1010所代表的就是数值10。
有关整数的二进制表示形式
整数的二进制表示形式有三种:原码、反码、补码
原码:我们把一个数按照正负直接翻译成二进制序列就是原码。比如说‘10’ 和‘-10’,这两个整数是存放到整型变量当中的,一个整型变量为4个字节(32bit),上面描述过,10用二进制表示就是1010,但是共有32个bit位,我们将高位补充0,于是得到:0000 0000 0000 0000 0000 0000 0000 1010
那么如何表示“-10”呢?在最高位(符号位)将‘0’置为‘1’即可,即:
1000 0000 0000 0000 0000 0000 0000 1010
我们需要记住,正整数的原码、反码、补码均相同,而负整数的反码是按照原码的符号位(最高位)不变,其他位按位取反,补码则是在反码基础上加1。
那么“-10”的反码和补码是什么呢?
反码:1111 1111 1111 1111 1111 1111 1111 0101(符号位不变,其他的将0置为1,1置为0)
补码:1111 1111 1111 1111 1111 1111 1111 0110(在最低位+1,即1+1 = 2 ,逢2进1)
整数在内存中的存储形式是补码(二进制序列)
下面我们用编译器调试,查看整数“10”和“-10”在内存中是如何存储的。
测试整数“10”:我们通过调试到“内存”中输入地址:&a,列:4,找到了“0a 00 00 00”。
我们知道“10”的补码是0000 0000 0000 0000 0000 0000 0000 1010,我们把它转换为十六进制位(每4个二进制位对应1个十六进制位),为00 00 00 0a,对应了内存中的“0a 00 00 00”,编译器上的存储为倒序的,即小端存储。
测试整数“-10”:我们通过调试到“内存”中输入地址:&b,列:4,找到了“f6 ff ff ff”,我们知道‘-10’的补码是:1111 1111 1111 1111 1111 1111 1111 0110 ,将其计算转换为十六进制为ff ff ff f6,对应了内存中的“f6 ff ff ff”。
终于,我们知道了移位操作符移动的是整数的补码二进制形式,我们往下看,直接上代码:
整数‘10’的左移操作符运算
#include<stdio.h> int main() { int a = 10; // 补码 0000 0000 0000 0000 0000 0000 0000 1010 //a向左移动1位 int b = a << 1;// 0000 0000 0000 0000 0000 0000 0001 0100 printf("%d\n", b);// 20 printf("%d\n", a);// 10 return 0; }
解释:我们知道移动的是补码的二进制序列, a << 1表示a的补码向左移动1位,高位移出的0会被丢弃,而低位空余的部分会补充0,计算之后的补码为 0000 0000 0000 0000 0000 0000 0001 0100 ,将补码转化为原码,原反补相同,所以计算后值为20,赋值给b。
a的值参与了运算,但是a本身并不会被改变,就像 b = a + 1,计算的值赋给了b,但是a没有改变。
整数‘-10’的左移操作运算
#include<stdio.h> int main() { int c = -10; // 原码 1000 0000 0000 0000 0000 0000 0000 1010 // 反码 1111 1111 1111 1111 1111 1111 1111 0101 // 补码 1111 1111 1111 1111 1111 1111 1111 0110 int d = c << 1; //补码转换为原码 //方法1(减1取反) //补码 1111 1111 1111 1111 1111 1111 1110 1100 //反码 1111 1111 1111 1111 1111 1111 1110 1011 //原码 1000 0000 0000 0000 0000 0000 0001 0100 --> -20 //方法2(取反加1) // 1111 1111 1111 1111 1111 1111 1110 1100 // 1000 0000 0000 0000 0000 0000 0001 0011 // 1000 0000 0000 0000 0000 0000 0001 0100 --> -20 printf("%d\n", d); printf("%d\n", c); return 0; }
解释:先将‘-10’转换为二进制的补码,左移1位后,再转换为原码,即补码减1得到反码,反码符号位不变,其他位按位取反得到原码(也可尝试补码取反再加1得到的也是原码。)
结论:
左移规则:左边丢弃,右边补0。
2.2 右移操作符
那么对于右移操作符的规则与左移操作符的规则是略有差别的,右移操作符分为算术右移和逻辑右移。
算术右移:右边丢弃,左边用原来的符号位填充。
逻辑右移:右边丢弃,左边直接用0填充。
那么代码运行的时候,到底采用的是算术右移呢?还是逻辑右移呢?其实是根据编译器而定的。
正整数在测试时是看不出区别的,因为正整数的符号位为0,所以依旧我们用整数“-10”来测试,下面上代码:
#include<stdio.h> int main() { int a = -10; //原码 1000 0000 0000 0000 0000 0000 0000 1010 //反码 1111 1111 1111 1111 1111 1111 1111 0101 //补码 1111 1111 1111 1111 1111 1111 1111 0110 int b = a >> 1; printf(" a = %d\n", a); printf(" b = %d\n", b); return 0; }
打印代码,我们发现b的值为-5,是一个负数,由此我们想到补充的是按移位之前‘-10’的补码的符号位来填充的,是否是这样的呢?
我们来计算一下,我们把“-10”的补码1111 1111 1111 1111 1111 1111 1111 0110,向右移动1位,得到1111 1111 1111 1111 1111 1111 1111 1011,计算它的反码为
1111 1111 1111 1111 1111 1111 1111 1010,符号位不变,按位取反得到原码为
1000 0000 0000 0000 0000 0000 0000 0101,转换数值为-5,与打印的值吻合,所以我们的编译器(visual studio 2019)是按照算术右移的方式进行计算的
⚠️注意:对于移位运算符,不要移动负数位,这个是标准未定义的。
以下代码就是错误的。
int main() { int a = 10; a >> - 1; //error return 0; }
3.位操作符
& (按位与) | (按位或) ^(按位异或)
这里的位是什么呢?是指二进制位。
语法: 两操作数各对应的补码的二进位进行计算。
⚠️注意:操作数必须是整数。
3.1 & 按位与
运算规则:只有对应的两个二进位均为1时,结果位才为1 ,否则为0
#include<stdio.h> int main() { int a = -3; int b = 5; //a 原码 1000 0000 0000 0000 0000 0000 0000 0011 // 反码 1111 1111 1111 1111 1111 1111 1111 1100 // 补码 1111 1111 1111 1111 1111 1111 1111 1101 //b 补码 0000 0000 0000 0000 0000 0000 0000 0101 //c 按位与0000 0000 0000 0000 0000 0000 0000 0101 --> 5 int c = a & b; printf("%d", c); return 0; }
解释:a的补码与b的补码的二进制位序列进行一一对照, 两者都为1时,结果才为1,否则为0。计算的结果为补码,发现最高位为0,所以判断为正数,转为数值5。
运算规则:只要对应的两个二进位有一个为1时,结果位就为1
#include<stdio.h> int main() { int a = -3; int b = 5; //a 原码 1000 0000 0000 0000 0000 0000 0000 0011 // 反码 1111 1111 1111 1111 1111 1111 1111 1100 // 补码 1111 1111 1111 1111 1111 1111 1111 1101 //b 补码 0000 0000 0000 0000 0000 0000 0000 0101 //c按位或1111 1111 1111 1111 1111 1111 1111 1101 (补码) // 1111 1111 1111 1111 1111 1111 1111 1100 (反码) // 1000 0000 0000 0000 0000 0000 0000 0011 (原码) int c = a | b; printf("%d", c); return 0; }
3.3 ^ 按位异或
运算规则:对应的二进制位,相同为0,相异为1。
#include<stdio.h> int main() { int a = -3; int b = 5; //a 原码 1000 0000 0000 0000 0000 0000 0000 0011 // 反码 1111 1111 1111 1111 1111 1111 1111 1100 // 补码 1111 1111 1111 1111 1111 1111 1111 1101 //b 补码 0000 0000 0000 0000 0000 0000 0000 0101 //c按位异或1111 1111 1111 1111 1111 1111 1111 1000 (补码) // 1111 1111 1111 1111 1111 1111 1111 0111 (反码) // 1000 0000 0000 0000 0000 0000 0000 1000(原码) int c = a ^ b; // -8 printf("%d", c); return 0; }
练习一下
不创建临时变量(第三个变量),实现两个数的交换。
#include<stdio.h> int main() { int a = 10; int b = 20; printf("交换前:a = %d b = %d\n", a, b); a = a + b; b = a - b; a = a - b; printf("交换后:a = %d b = %d\n", a, b); return 0; }
我们发现方法一的代码存在一定的缺陷,假设a和b是一个很大值,但是两者没超过int类型的范围,如果它们相加就可能超过了int类型的最大范围,会出现溢出情况,会丢失精度。
#include<stdio.h> int main() { int a = 10; int b = 20; printf("交换前:a = %d b = %d\n", a, b); a = a ^ b; //code1 b = a ^ b; //code2 a = a ^ b; //code3 printf("交换后:a = %d b = %d\n", a, b); return 0; }
解释:我们知道,异或的运算法则是相同为0,相异为1。
那么就有a ^ a = 0,0 ^ a = a的结论,3 ^ 3 ^ 5 = 5 ,那么3 ^ 5 ^ 3的结果呢?我们经过计算也是为5,即3 ^ 3 ^ 5 等价于 3 ^ 5 ^ 3,故而我们可以将其理解为异或的交换律。
观察方法二的代码,code1将a ^ b的值给a,那么code2的代码可以替换为a ^ b ^ b 即为a的值赋值给了b ,code3的代码a ^ b中的a还是code1的a ^ b,b被赋了原来a的值,故可以转换为a ^ b ^ a ,即为b的值,再赋值给a。
方法二的操作并不会导致溢出,可能适宜这个练习的代码,但是这种方法并不比直接根据临时变量交换要好。
理由如下:
它对于运算的操作数的要求必须是整数;
代码可读性较差。
4.赋值操作符
赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值
#include<stdio.h> int main() { int weight = 120;//体重 weight = 89;//不满意就赋值 return 0; }
4.1复合赋值符
+= -= *= /= %= >>= <<= &= |= ^=
简单举几个例子,操作基本都类似
#include<stdio.h> int main() { int a = 3; a = a << 1; //等价于 a <<= 1; int b = 5; b = b ^ 5; //等价于 b ^= 5 return 0; }
5.单目操作符
* 操作符和 & 操作符
int main() { int a = 10; int* p = &a;//&取地址操作符 *p = 20;//*为解引用操作符(间接引用操作符) //int arr[10]; //&arr;//取出数组的地址 return 0; }
sizeof操作符: 返回给定类型的变量所占用的字节数
#include<stdio.h> int main() { int a = 10; printf("%d\n", sizeof(a));//4 printf("%d\n", sizeof a); printf("%d\n", sizeof(int));//4 printf("%d\n", sizeof int) int arr[10] = { 0 }; printf("%d\n", sizeof(arr));//40 return 0; }
sizeof和数组
观察以下代码:
#include <stdio.h> void test1(int arr[]) { printf("%d\n", sizeof(arr));//code 1 } void test2(char ch[]) { printf("%d\n", sizeof(ch));//code 2 } int main() { int arr[10] = {0}; char ch[10] = {0}; printf("%d\n", sizeof(arr));//code 3 printf("%d\n", sizeof(ch));//code 4 test1(arr); test2(ch); return 0; } //问: //code 1、code 2两个地方分别输出多少? //code 3、code 4两个地方分别输出多少?
解释:test1()和test2()传入的实参是数组名,实际传入的是数组首元素的地址,形参在接收时,形式上是数组的表现形式,但本质上是一个指针,所以code1和code2在计算时,计算的是指针变量的大小,所以在x86的环境下,两者计算的均为4个字节,在x64环境下,两者计算的均为8个字节。
那么对于code3和code4呢?我们在数组部分就知道,sizeof(数组名)计算的是整个数组的大小,所以code3计算的值为40,code4计算的值为10
~操作符 :对一个数的二进制按位取反
#include<stdio.h> int main() { int a = 0; // 0000 0000 0000 0000 0000 0000 0000 0000 (补码) // 1111 1111 1111 1111 1111 1111 1111 1111 (按位取反) // 1111 1111 1111 1111 1111 1111 1111 1110 (反码) // 1000 0000 0000 0000 0000 0000 0000 0001 (原码) printf("%d\n", ~a); return 0; }
~ 操作符与位操作符 & 、 | 的灵活运用
#include<stdio.h> int main() { int a = 3; // 0000 0000 0000 0000 0000 0000 0011 // 如果要将二进制的第四位(从右开始)的0改为1,其他都不变 // 0000 0000 0000 0000 0000 0000 1000 //将1向左移动3位 // 那么我们只需要按位或以上二进制序列 就能得到 // 0000 0000 0000 0000 0000 0000 1011 --> 11 a |= (1 << 3); printf("%d\n", a); //11 // 0000 0000 0000 0000 0000 0000 1011 //如果要将二进制的第四位(从右开始)的1改回0,其他不变 // 1111 1111 1111 1111 1111 1111 0111 //将1向左移动3位,再使用~操作符,按位取反 //那么我们只需要按位与以上二进制序列,就能得到 // 0000 0000 0000 0000 0000 0000 0011 --> 3 a &= (~(1 << 3)); printf("%d\n", a); //3 return 0; }
对 while( ~ scanf(“%d”,&n)) 中的 ~如何理解?我们知道scanf读取失败的时候返回的是 EOF,C语言对于EOF定义的值为 -1,
-1的原码为 1000 0000 0000 0000 0000 0000 0000 0001 ,
反码为 1111 1111 1111 1111 1111 1111 1111 1110
补码为 1111 1111 1111 1111 1111 1111 1111 1111
对补码按位取反 即为全0,跳出while循环。