第八章:操作符详解
文章目录
- 第八章:操作符详解
- 1. 操作符分类
- 2. 操作符的属性
- 3. 原码、反码、补码
- 4. 左移和右移操作符
- 5. 位操作符
- 6. 逗号表达式
- 7. 结构成员访问操作符
- 8. 整形提升
- 9. 算术转换
- 总结
1. 操作符分类
- 算术操作符:
+、-、*、/、% - 移位操作符:
<<>> - 位操作符:
&、|、^ - 赋值操作符:
=、+=、-=、*=、/=、%=、<<=、>>=、&=、|=、^= - 单⽬操作符:
!、++、--、&、*、+、-、~、sizeof、( 类型 ) - 关系操作符:
>、>=、<、<=、==、!= - 逻辑操作符:
&&、|| - 条件操作符:
? : - 逗号表达式:
, - 下标引⽤:
[] - 函数调⽤:
() - 结构成员访问:
.、->
2. 操作符的属性
- 优先级:一个表达式包含多个运算符,哪个运算符应该优先执行
- 结合性:当两个运算符优先级相同,这时候就需要看结合性
常用运算符的优先级顺序,从高到低:
- 圆括号:
() - ⾃增运算符:
++,⾃减运算符-- - 单⽬运算符:
+和- - 乘法:
*,除法:/ - 加法:
+,减法:- - 关系运算符 :
<、>等 - 赋值运算符 :
=
圆括号的优先级最⾼,可以使⽤它改变其他运算符的优先级。
3. 原码、反码、补码
整数的二进制表示方式有三种,原码、反码、补码。
有符号整数:三种表示方式都有符号位和数值位两部分,二进制中最高位是被当作符号位,剩余的都是数值位,符号位用0表示正,1表示负。
- 正整数:三码相同
- 负整数:原码取反为反码,反码+1为补码。补码得到原码也可以使用取反,+1的操作。
无符号整数:三种表示方式相同,每一位都是数值位
整形在内存中存放的都是补码,使用补码,可以将符号位和数值域统一处理,加法和减法也可以统一处理(CPU只有加法器),同时补码和原码相互转换,不需要额外的硬件电路。
//原码、反码、补码//整数分为有符号整数和无符号整数////有符号整数(signed int)第一位是符号位,0表示正数,1表示负数//有符号整数又分为正数和负数//正数的原码、反码、补码都相同//负数的反码是符号位不变,其他位取反,补码是在反码的基础上加1////无符号整数(unsigned int)没有符号位,全部用于表示数值。原码、反码、补码都相同/////*// 在计算机中,整数统一以补码形式存储和运算,输出时(打印在屏幕上)会转换成原码形式// 例如 1-1 => 1 + (-1)// 假设按照原码进行计算// 00000000000000000000000000000001 (1的原码)// 10000000000000000000000000000001 (-1的原码)// 10000000000000000000000000000010 => -2(相加后错误)//// 假设按照补码进行计算// 00000000000000000000000000000001 (1的补码,正数的三码相同)// 11111111111111111111111111111110 (-1的反码,符号位不变)// 11111111111111111111111111111111 (-1的补码,反码+1)// 100000000000000000000000000000000 (1的补码和-1的补码相加不断进1,结果会多一个0)// 00000000000000000000000000000000 (丢弃高位保留低位,结果为0)// 此时计算后的是补码,但是第一个符号位是0,所以也是原码//*/4. 左移和右移操作符
<<左移操作符:左边抛弃,右边补0>>右移操作符:算数右移:左边用原该值的符号位填充,右边丢弃
移位操作符的操作数只能是整数
#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>//移位操作符(操作数只能是整数,移动的是存储在内存中的补码)intmain(){//左移操作符(左边抛弃,右边补0)inta=-3;intb=a<<1;printf("b = %d\n",b);// 10000000000000000000000000000011 (-3的原码)// 11111111111111111111111111111100 (-3的反码)// 11111111111111111111111111111101 (-3的补码)// << 1// 11111111111111111111111111111010 (左移后补码)// 此时补码符号位为负,需要转换成原码进行打印// 10000000000000000000000000000101 (补码取反)// 10000000000000000000000000000110 (再加1) => -6// 如果在左移过程中,除了符号位以外的数值没有被抛弃,那么左移相当于乘以2,否则结果不可预知//右移操作符(大部分编译器都是算术右移)//逻辑右移:左边用0填充,右边丢弃//算数右移:左边用原该值的符号位填充,右边丢弃//如果在右移过程中,除了符号位以外的数值没有被抛弃,那么右移相当于/2,print("%d\n",4>>1);// 2print("%d\n",5>>1);// 2print("%d\n",-4>>1);// -2print("%d\n",-5>>1);// -3return0;}5. 位操作符
&:按位与:两者1为1则为1|:逻辑与:一者为1则为1^:按位异或:相同为0,不同为1~:按位取反:0为1,1为0
以二进制的1为观察角度,&相当于取交集,| 取并集
操作数只能是整数
#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>//位操作符(除了取反都是两个操作数,并且操作数必须是整数)// & 按位与// | 按位或// ^ 按位异或// ~ 按位取反intmain(){//按位与(对应的二进制位进行于运算,两者都为1则为1,否则为0)printf("3 & -5 = %d\n",3&-5);// 00000000000000000000000000000011 (3的补码,正数三码一体)// 10000000000000000000000000000101 (-5的原码)// 11111111111111111111111111111010 (-5的反码)// 11111111111111111111111111111011 (-5的补码)// & 运算// 00000000000000000000000000000011 (3的补码)// 11111111111111111111111111111011 (-5的补码)// 00000000000000000000000000000011 => 3 (按位与结果的补码,此时补码符号位为是0,为正数,所以也是原码和反码)//按位或(对应的二进制位进行或运算,只要一者为1则为1,否则为0)printf("3 | -5 = %d\n",3|-5);// 00000000000000000000000000000011 (3的补码,正数三码一体)// 10000000000000000000000000000101 (-5的原码)// 11111111111111111111111111111010 (-5的反码)// 11111111111111111111111111111011 (-5的补码)// | 运算// 00000000000000000000000000000011 (3的补码)// 11111111111111111111111111111011 (-5的补码)// 11111111111111111111111111111011 (按位或结果的补码,此时补码符号位是1,为负数,所以需要取反,再加一得到原码)// 10000000000000000000000000000100 (取反后结果)// 10000000000000000000000000000101 (再加1) => -5//按位异或(对应的二进制位进行异或运算,相同为0,不同为1)printf("6 ^ -5 = %d\n",6^-5);// 00000000000000000000000000000110 (6的补码,正数三码一体)// 10000000000000000000000000000101 (-5的原码)// 11111111111111111111111111111010 (-5的反码)// 11111111111111111111111111111011 (-5的补码)// ^ 运算// 00000000000000000000000000000110 (6的补码)// 11111111111111111111111111111011 (-5的补码)// 11111111111111111111111111111101 (按位异或结果的补码,此时补码符号位是1,为负数,所以需要取反,再加一得到原码)// 10000000000000000000000000000010 (取反后结果)// 10000000000000000000000000000011 (再加1) => -3//按位取反(0变1,1变0)printf("~5 = %d\n",~5);// 00000000000000000000000000000101 (5的补码,正数三码一体)// ~ 运算// 11111111111111111111111111111010 (按位取反结果的补码,此时补码符号位是1,为负数,所以需要取反,再加一得到原码)// 10000000000000000000000000000101 (取反后结果)// 10000000000000000000000000000110 (再加1) => -6return0;}练习一:不使用第三个变量,实现两个整数的交换
#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>//练习一intmain(){//不使用中间变量,交换两个整数的值//使用按位异或操作符 ^(相同为0,不同为1)//两个相同的数异或结果为0// a ^ a = 0//0和任何数异或结果为该数本身// a ^ 0 = a//异或支持交换律// a ^ a ^ b = b// a ^ b ^ a = b//也可以这样理解// a ^ b = c// c ^ a = b// c ^ b = a//例如// 011 => 3// 101 => 5// 110 => 3 ^ 5// 110 => 3 ^ 5// 011 => 3// 101 => (3 ^ 5) ^ 3 = 5//实现利用异或交换两个整数inta=0;intb=0;scanf("%d %d",&a,&b);printf("交换前:a = %d, b = %d\n",a,b);a=a^b;//此时 a 存储的是 a ^ b 的结果b=a^b;// b = (a ^ b) ^ b = aa=a^b;// a = (a ^ b) ^ b(a) = bprintf("交换后:a = %d, b = %d\n",a,b);return0;}练习二:求一个整数存储在内存中的二进制中1的个数
方法一
#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>//方法一:逐位检查//思路:当我们求十进制1323中有几个3的时候,1323 % 10 得到最后一位数字3//然后让1323 / 10进位去掉最后一位数字,继续取余和除以10,直到1323变为0为止//那么二进制中也是类似的思路,此方法如果是负数计算则会为0//定义函数,计算一个整数存储在内存中的二进制中 1 的个数intcount_bit1(intnum){intcount=0;//记录1的个数while(num)//当num为0时结束循环{if(num%2==1){count++;}num/=2;//进位,这个操作我理解为底层发生了右移//例如输入的是15,其补码为:00000000000000000000000000001111//当第一次 15 % 2 = 7余1,接着 num = 15 / 2 = 7//此时补码为:00000000000000000000000000000111//当输入负数时// 10000000000000000000000000000001 => -1原码// 11111111111111111111111111111110 => -1反码// 11111111111111111111111111111111 => -1补码// -1 % 2 = -1,接着 -1 / 2 = 0// 00000000000000000000000000000000 计算后结果}returncount;}intmain(){intnum=0;scanf("%d",&num);printf("%d的二进制中1的个数为:%d\n",num,count_bit1(num));return0;}方法二
#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>//方法二// 数字取补码的最低位和1进行与运算,运算后将数字的补码进行右移,依次取最低位// 此时右移,不需要考虑符号位被影响,因为最终的结果是求补码中有多少个1,符号位也包括// 00000000000000000000000000001111 => 15的补码// 00000000000000000000000000000001 => 1的补码// & 与运算(两者为1才为1)// 00000000000000000000000000000001 => 最后一位两者为1才为1,与运算后再将数字的补码进行右移,依次取最低位//或者使用逻辑或和0比较,一样的逻辑intcount_bit1(intnum){intcount=0;//定义变量记数//错误写法如下:因为右移采用的是算术右移,符号位不会发生变化//while (num)//当 num = 0 的时候,即遍历结束//{// if ((num & 1) == 1)// {// count++;// }// num >>= 1;//num = num >> 1//}//当输入负数时// 10000000000000000000000000000001 => -1原码// 11111111111111111111111111111110 => -1反码// 11111111111111111111111111111111 => -1补码// 00000000000000000000000000000001 => 1原码// & 与运算// 00000000000000000000000000000001 &运算后结果// 11111111111111111111111111111111 -1右移,符号位不变,所以此时将陷入死循环//所以正确写法应该加入控制次数的循环for(inti=1;i<=32;i++)//有符号整数为4个字节,32位{if(num&1==1){count++;}num>>=1;}returncount;}intmain(){intnum=0;scanf("%d",&num);count_bit1(num);printf("%d的二进制中1的个数为:%d\n",num,count_bit1(num));return0;}方法三
#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>//方法三//首先要知道这个式子的规律 n = n & (n-1)//例如 n = 15// 00000000000000000000000000001111 => n (15的补码)// 00000000000000000000000000001110 => n-1 (14的补码)// & 运算 n = n & (n-1)// 00000000000000000000000000001110 => n (14的补码)// 00000000000000000000000000001110 => n (14的补码)// 00000000000000000000000000001101 => n-1 (13的补码)// & 运算 n = n & (n-1)// 00000000000000000000000000001100 => n (12的补码)// 00000000000000000000000000001100 => n (12的补码)// 00000000000000000000000000001011 => n-1 (11的补码)// & 运算 n = n & (n-1)// 00000000000000000000000000001000 => n (8的补码)// 00000000000000000000000000001000 => n (8的补码)// 00000000000000000000000000000111 => n-1 (7的补码)// & 运算 n = n & (n-1)// 00000000000000000000000000000000 => n (0的补码)//观察n的补码中1的变化,可以发现每次 & 运算后,最右边的1便消失了// 00000000000000000000000000001111 => n (15的补码)// 00000000000000000000000000001110 => n (14的补码)// 00000000000000000000000000001100 => n (12的补码)// 00000000000000000000000000001000 => n (8的补码)// 00000000000000000000000000000000 => n (0的补码)//代码实现:intcount_bit1(intnum){intcount=0;//不断消除补码中右边的1while(num)//当所有1消除完此时数字便为0,作为循环的结束条件{num&=num-1;count++;}returncount;}intmain(){intnum=0;scanf("%d",&num);count_bit1(num);printf("%d的二进制中1的个数为:%d\n",num,count_bit1(num));return0;}//此式子也可以判断n是否是2的次方数(2的次方数在补码中存储只有一个1)//所以 if (n&(n-1) == 0) 条件成立,就是2的次方数练习三:将数字的二进制序列的第n位修改为1,再改为0
#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>//练习三//将数字的二进制序列的第n位修改为1,然后再改为0//例如输入13// 00000000000000000000000000001101 13的补码// 00000000000000000000000000011101 第五位修改为1// 00000000000000000000000000001101 再改为0// 思路:13的补码或上第五位为1,其他为0的数字// 再与上第五位为0,其他为1的数字// 00000000000000000000000000001101 13的补码// 第五位为1,其他为0的数字通过1左移(5-1)位得到// 00000000000000000000000000000001 => 1的补码// 1<<4// 00000000000000000000000000010000// 13 和这个数字 | 运算// 00000000000000000000000000011101 => 29// 第五位为0,其他为1的数字,将左移后的结果按位取反即可// 00000000000000000000000000010000 => 1<<4// ^ 运算// 11111111111111111111111111101111// 13 和上面这个数字 & 运算// 00000000000000000000000000001101 结果就变会原来的补码intmain(){intnum=0;intn=0;printf("二进制序列修改的数字和位数:");scanf("%d%d",&num,&n);// 第n位修改为1num|=1<<(n-1);printf("第%d位修改为1后结果:%d\n",n,num);//第n位修改为0num&=~(1<<(n-1));printf("第%d位修改为0后结果:%d\n",n,num);return0;}6. 逗号表达式
用逗号隔开的多个表达式,从左向右执行。整个表达式的结果是最后一个表达式的结果。
#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>//逗号表达式intmain(){inta=1;intb=2;//从左到右执行,结果为最后一个表达式的结果intc=(a>b,a=b+10,a,b=a+1);//结果为13}7. 结构成员访问操作符
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如:标量,数组,指针,还可以是其他结构体。
//结构成员访问操作符 .//结构体变量 . 结构体成员//#define _CRT_SECURE_NO_WARNINGS//#include <stdio.h>////声明一个结构体structstrudent{//定义成员变量charname[10];intage;charid[11];floatscore;}s1,s2;//创建结构体的变量,此处声明的变量是全局变量structstrudents3;//这里也是全局变量intmain(){//创建局部变量并初始化,此时必须按照顺序structstrudents4={"张三",20,"123456",99.9f};//不按顺序初始化structstrudents5={.age=19,.name="李四",.id="1234567",.score=88.8f};return0;}结构体包含另一个结构体
#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>//结构体包含另一个结构体structebook//通讯录结构体{structpeople//人员信息结构体{//定义成员变量charname[10];charphone[12];}data[100];//定义变量 data[100] 存放一百个人的信息intcount;//通讯录存储个数};intmain(){structpeoplep1={"张三","18888888888"};structebookeb={{{"李四","16666666666"},{"王五","19999999999"}},2};//存储两个人的信息//打印信息printf("%s\n",p1.name);printf("%s\n",p1.phone);printf("%s\n",eb.data[0].name);printf("%s\n",eb.data->name);//通过指针间接访问return0;}详细:C Operator Precedence - cppreference.com
8. 整形提升
C语⾔中整型算术运算总是⾄少以默认整型类型的精度来进⾏的。 为了获得这个精度,表达式中的字符和短整型操作数在使⽤之前被转换为普通整型,这种转换称为整型提升。
- 有符号整数提升:按照变量的数据类型的符号位来提升
- 无符号整数提升:高位补0
//整形提升#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>intmain(){//c语言编译器数字默认为int,小数默认为double//小于int长度的整形值,都必须转换为int或unsigned int//所以表达式中的字符型和短整型操作数再使用之前会被转换为普通整形,称为整形提升chara=20;// 00000000000000000000000000010100 => 20的补码// int(4个字节)使用char(1个字节)存储会截断// 00010100 => acharb=120;// 00000000000000000000000001111000 => 120的补码// 截断// 01111000 => b//算术运算时发生整型提升://有符号整数提升按照变量的数据类型的符号位来提升//无符号整数提升,高位补0// 00010100 => a// 提升// 00000000000000000000000000010100 => a// 01111000 => b// 提升// 00000000000000000000000001111000 => bcharc=a+b;// 00000000000000000000000000010100 => a// 00000000000000000000000001111000 => b// 00000000000000000000000010001100 => 相加后结果(上下相加,逢2余0进1)// 此时将结果使用char类型存储(截断)// 10001100 => c(此时符号位为负)printf("%d",c);//%d:是以10进制的形式,打印一个有符号的整形(int),此时 c 是字符型,所以需要再次提升//c 进行提升,有符号整数提升按照变量的数据类型的符号位来提升// 11111111111111111111111110001100 => c(此时符号位为负)// 10000000000000000000000001110011 => 取反// 10000000000000000000000001110100 => +1,此时结果为 -116return0;}9. 算术转换
当操作符的各个操作数属于不同的类型,那么其中⼀个操作数就会转换为另⼀个操作数的类型,从低字节向高字节转换,否则操作就⽆法进⾏。
以下为高字节到低字节顺序:
- long double
- double
- float
- unsigned long int
- long int
- unsigned int
- int
总结
本文是在鹏哥 C 语言集训营学习过程中所记录的学习笔记,梳理了核心知识点,同时也记录了本人实操验证的代码案例,供后续学习复盘使用。