提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
前言
1.算术操作符
2.移位操作符
3.位操作符
4.赋值操作符
5.单目操作符
6.关系操作符
7.逻辑操作符
8.条件操作符
9.逗号表达式
10.下标引用,函数调用,结构体成员操作符
11.表达式求值
12.操作符优先级表
总结
前言
暑假漫漫,复习了一下C语言操作符部分,发现操作符像是一个在C语言中被严重低估的部分,我们每天写代码一定会用到一些操作符,可是它的原理我们真的了解的很透彻么?在复习的过程中我发现真的有好多的细节我之前没学到位,甚至整型提升这一个复杂的过程之前都没怎么听说过。写这样一篇文章留给我今后复习用,同时也分享给大家,我尽量写的详细,争取在初识C语言这里将操作符这部分内容搞透彻。欢迎大家一键三连(比心? ?比心)
提示:以下是本篇文章正文内容,下面案例可供参考
1.算术操作符
1.? +? ? -? ? ?*:
这三个操作符其实没有什么值得一提的,进行加法,减法,乘法运算
2.? /? :
? (1) 当'/'两边都是整数时,执行的是整数除法
(2)当'/'操作数有浮点数时,执行浮点数除法
? ?结果类型与操作数有关,与类型无关
int ret=9/2; //ret=4
double ret=9/2 //ret=4.000000
double ret=9/2.0 //ret=4.500000
int ret=9/2.0 //编译器会报错
?3. %
取模操作符,即取余数
ret=9%2; //ret=1
注意'%'的操作数只能是整型
2.移位操作符
? ?<< :左移操作符
? ?>>:右移操作符
谈到移位操作符和位操作符,有必要复习一下原码,反码,补码的知识
对于整数的二进制存储有三种形式,原码,反码,补码。而在计算机中存储的数据为补码。
1.正数
正数的原码,反码,补码都是相同的,就是将该正数转换为二进制,以整型为例
10的原码,反码,补码是00000000 00000000 00000000 00001010 由于是整型,所以一共32个二进制位,第一位是符号位,所以为0,代表正数。
2.负数
负数的原码是将第一位符号位置为1,其他位按照十进制转换为二进制运算即可。
负数的反码是指符号位1不变,其他位取反。
负数的补码是由反码加一得到的。
以 -10 为例
原码:10000000 00000000 00000000 00001010
反码:11111111 11111111 11111111 11110101(0比1宽,视觉误差。。。我还查了一遍)
补码:11111111 11111111 11111111? 11110110
重要的事情说两遍,计算机存的是补码。?
1.<< :左移操作符
顾名思义,就是将二进制的补码向左移动n位,也就是将最左边n个二进制位删掉,右边补了n个0。
比如
int a=10;
int b=a<<1;
这段代码,b的值咋个算呢?
首先写出a的补码 :00000000 00000000 00000000 00001010
? ? ? ? ? ? ?然后移位 :00000000 00000000 00000000 00010100
得到b的补码,由于是正数所以也是原码。b的值就算出来了是20。
2.>> :右移操作符
右移操作符和左移操作符基本相似,但是有不同。
同样最右边的数丢弃,但是最左边补什么由你使用的编译器决定。
(1)正数最左边补的位一定是0
?(2)负数则需要看编译器支持哪种右移,支持逻辑右移则补0,支持算数右移则补1,大部分编译器支持的是算数右移
以-10为例,先写出-10的补码
补码:11111111 11111111 11111111? 11110110
逻辑右移:011111111 11111111 11111111 11111011
算数右移:11111111 11111111 11111111 11111011
两种方式算出的结果是不同的,所以当你用不同编译器算出的结果不同,请不要惊讶。
3.位操作符
&:按位与
|? :按位或
^ :? ?按位异或
这三种操作符都有两个操作数,而且都是利用操作数的补码进行运算的。
int a=3;
int b=-2;
用这两个数字举例,先写出他们的补码
a : 00000000 00000000 00000000 00000011
b : 11111111 11111111 11111111 11111110
1.&按位与?
有0则0,同1则1。
a&b=00000000 000000000 00000000 00000010
注意a和b的每一位都要对齐(1比0窄,博客不完美了,可恶啊。。。)
2.|按位或
有1则1,同0则0
a|b=11111111 11111111 11111111 11111111
由于每一位都有1存在,所以都是1,若有一位两者都是0的话,则为0
3.^按位异或
相同为0,不同为1。
a^b=11111111 11111111 11111111 11111101
值得一提的是想打出^,需要在英文模式下,中文模式下是……
注意:这三种位运算计算出的结果仍为补码。只要是用补码当操作数的,结果均为补码。
4.这三种位运算的用处
学习到这里你可能会问,这些运算在写程序的时候有什么用呢,似乎用的也不是很多。
(1)取到二进制位的最低位
只需要a&1,如果得到的结果是1,则最低位为1;如果得到的是0,则最低位是0。
(2)计算一个未知数的值
利用循环语句,每次循环将a的二进制序列向右移一位(>>),同时进行a&1,这样每一次循环都能得到a的一个二进制位,循环执行32次,则a的二进制序列便可得到,进而得到a的值。
(3)交换两个数的数值
通常情况下我们交换两个数的数值应该是这样的
int a=10;
int b=20;
int temp;
temp=a;
a=b;
b=temp;
我初学的时候一直觉得再创建一个变量挺麻烦的,不知道你们是否有同感。那有没有不用创建temp的方法呢?有,'^'呀。
int a=10;
int b=20;
a=a^b;
b=a^b;
a=a^b;
这样两个数就交换了,至于原理,你可以写一写二进制序列自己试一试。
4.赋值操作符
这个其实没什么可说的,就是一个‘=’,注意和==区分就行了
还有一些复合赋值符,我列出来吧
+=? ? -=? ? *=? ? /=? ?%=
>>=? <<=? ? &=? |=? ^=
还有注意一下字符串的赋值不能用=,要用strcpy
5.单目操作符
所谓单目,就是只有一个操作数的意思
! :逻辑反
-? ? :负
+? ?:正
&? ?:取地址
*? ?:解引用操作符
~? ?:对一个数的二进制按位取反
--? ?:前置,后置--
++? :前置,后置++
(类型):强制类型转换操作符
sizeof:操作数的类型长度
1.? !逻辑反
(1)非0数的逻辑反是0;
比如:a=10; 则!a=0;
? (2)? 0的逻辑反是1;
比如:a=0; 则!a=1;
2.? ?-与+
将某一值变成正数或者负数。
3.? ?&? *取地址操作符和解引用操作符
&取出某一个变量的地址,*通过地址找到变量的值。
int a=10;
int* p=&a;//取出a的地址放在指针p中
*p=20;//通过解引用操作符修改a的值
printf("%d",a);//a=20
这里需要一些指针的知识,注意一下(int*)代表指针的类型,这里的‘*’和后面的解引用操作符‘*’不同。
4.? ?~对一个数的二进制位按位取反
符号位也要取反
int a=0;//00000000 00000000 00000000 00000000
int b=~a;//11111111 11111111 11111111 11111111
用处:一般在修改单个二进制位的时候使用
5.++ --
这两个操作符一定要区分好前置和后置
int a=10;
int b=a++;
int c=++a;
printf("%d %d %d",a,b,c);//12 10 12
记住一点就可以了,前置++先计算后赋值,后置++先赋值后计算,--也是一样的
6.(类型)强制类型转换
int a=3.14;//编译器会报错
int a=(int)3.14//a=3,编译器不会报错
7.sizeof: 计算操作数的类型长度
(1)计算变量或者类型所占内存大小
int sz=sizeof(int);//sz=4,()不能省略
int a;
int sz=sizeof(a);//sz=4,()可以省略
int sz=sizeof a;//sz=4
计算变量时可以省略,但是计算类型时不能省略(),这是为什么呢?? 因为大乌龟上有小乌龟,规定上有新规定(狗头保命)
(2)注意只计算变量内存的大小,与变量中存的什么数据无关
char arr[10]="abc";
printf("%d",arr);//10
(3) sizeof内的数据不参与运算,举个例子
int a=5;
int s=10;
printf("%d",sizeof(s=a+2));//4
printf("%d",s);//10
在这里很多人会认为s应该打印的是数字7,然而并不是这样,这里的原因很容易理解
我们都知道一个程序由test.c文件要先经过“编译”变成.obj文件,再经过“链接”最后变成test.exe文件,计算的过程发生在.exe中
而在编译的过程中,sizeof内的数据的类型就已经确定了,是int型,编译器会将(s=s+a)这一模块直接看成int
既然已经是int了,s=s+a是否执行就没什么意义了,直接默认为不执行了,所以s的值并没有改变
?(4)计算数组的大小
在没有真正掌握sizeof之前,我们求数组大小的方式大部分是利用for循环,这样会很麻烦。那么有了sizeof后
int arr[]={1,2,3,4,5,6,7,8,9,0};
int a=sizeof(arr)/sizeof(arr[0]);//a=10
这里也说明当sizeof的操作数是数组时,数组名不在是首元素的地址,而变成了整个数组
6.关系操作符
>? ? <? ? >=? ? <=? ? !=? ? ?==
就这几个,有问题吗?没有问题。
7.逻辑操作符
&&? ||
1.&&:逻辑与
是并且的意思,全真则真,有假则假。
2.||? ?:逻辑或
是或者的意思,有真则真,全假则假
(1)区分一下逻辑和按位
1&2=0;//按位与后计算的结果是0
1&&2=1;//表示真的意思
1|2=3;//按位或后结果为3
1||2=1;//表示真的意思
(2)这两个操作符都控制了求值顺序(这个很重要)? 这是一道360的笔试题
? ?来看这样一段代码
int i=0,a=0,b=2,c=3,d=4;
i=a++&&++b&&d++;
printf("%d %d %d %d ",a,b,c,d);
该代码的执行结果是a=1,b=2,c=3,d=4。
但是为什么b和d两个数字没有像a一样++呢?
因为a是后置++,先赋值后++,a的值已经是0了,所以i的值一定为假(0),后面的值无论是多少也改变不了i已经是假的事实了,所以干脆不要计算后面的值了,因此++b和d++都没有参与运算。
||也是同理,这都是由于这两个操作符对求值顺序的控制。
再举一个例子
int i=0,a=0,b=2,c=3,d=4;
i=a++||++b||d++;
printf("%d %d %d %d ",a,b,c,d);
代码执行的结果是a=1,b=3,c=3,d=4,由于a的值赋的是0,无法判断表达式是否是真,所以要执行++b,而b的值已经是真了,就没有必要再去执行d++了,所以得到的是这样的结果。
8.条件操作符
exp1?exp2:exp3;
如果exp1为真,那么执行exp2,为假则执行exp3。
这个操作符主要是代提if语句使用的,但是我经常忘记使用它,现在开始要用起来。
if(a>5)
b=3;
else
b=-3;
a>5?b=3:b=5
这两个是等价的。
9.逗号表达式
exp1,exp2 ,exp3,? ……
逗号表达式中也有一些细节是我们没怎么注意到的。
(1)逗号表达式由左向右执行,整个表达式的结果是最后一个表达式的结果
int a = 5;
int b = 1;
int c = (a = b + 1,b = a - 2);
printf("%d", c);//c=0
c的结果是0,这说明逗号表达式确实从左到右执行了,而且整个逗号表达式的值是最后一个表达式的值
(2)根据逗号表达式的特性可以简化循环语句
a=get();
count(a);
while(a>0)
{
a=get();
b=count(a);
}
用逗号表达式可以这样写:
while(a=get(),count(a),a>0)
{ }
但其实不太建议这样去做,虽然很秀,但是容易把自己绕晕,而且do while语句可以达到同样的效果
10.下标引用,函数调用,结构体成员操作符
[]:下标引用操作符
():函数调用操作符
.? ->:结构体成员调用操作符
1.[]下标引用操作符
int arr[5]={1,2,3,4,5};
arr[4]=10;//将第五个元素的值变成10
这里就用到了下标引用操作符[],它的两个操作数分别是arr和4,我们甚至可以这样写
arr[4]=10;
4[arr]=10;
这两段代码是等价的。
2.()函数调用操作符
和下标引用操作符一样,我们每天都在使用,但是很少注意到它们其实也是一种操作符
例如我们调用函数strlen(),函数调用操作符的操作数就是函数名strlen和()里的内容,我们自定义的函数也是如此
3.结构体成员的引用操作符
#include<stdio.h>
struct Stu
{
char name[10];
int age;
}
void setage1(struct Stu stu)
{
stu.age=18;
}
void setage2(struct Stu* stu)
{
stu->age=18;
}
int main()
{
struct Stu stu;
struct Stu* pStu=&stu;
stu.age=20;
pStu->age=20;
setage1(stu);
setage2(pstu);
return 0;
}
这么一长串你看出了什么呢?当拿到的是结构体变量本身,修改它内部值只需要 结构体名.成员名=某个数。当拿到的是结构体变量的地址时,需要 结构体名->成员名=某个数。
11.表达式求值
1.整型提升
这个东西说起来比较复杂,但是理解起来还是挺容易的。所谓整型提升,就是把所占内存低于整型的char类型和short类型强行提升变成整型。
(1)官方术语:
C的整型算数运算总是至少以缺省整型类型的精度来提升,为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转化为普通整型,这种转化称为整型提升。
(2)整型提升的意义
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长 度。 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算。
总结一下就是推究本源是CPU在进行运算,但是CPU只能计算整型的数据,因此例如short类型的数据得先变成int型参加运算,然后再发生截断,编译器再把它变回short型。
(3)负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位: 1111111 因为 char 为有符号的 char 所以整形提升的时候,高位补充符号位,即为1 提升之后的结果是: 11111111111111111111111111111111
(4)正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位: 00000001 因为 char 为有符号的 char 所以整形提升的时候,高位补充符号位,即为0 提升之后的结果是: 00000000000000000000000000000001
下面我们用三个例子讲解一下整型提升的过程
(1)
char a=3;
char b=127;
char c=a+b;
printf("%d",c);
这样一个短短的代码其实就用到了三次整型提升,我来演示一下过程
a的补码:00000011
b的补码:01111111
由于a和b要参与运算,所以要进行整型提升
a:00000000 00000000 00000000 00000011
b:00000000 00000000 00000000 01111111
计算a+b=:00000000 00000000 00000000 10000010
由于c是char类型所以发生截断 。
c:10000010
在之前发生了a与b的两次整型提升。
之后需要用整型的形式(%d)打印c,再一次发生整型提升,打印整型提升之后的数
c:11111111 11111111 11111111 10000010
注意这个是c的补码,我们打印的是原码,所以结果为-126。
(2)
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if(a==0xb6)
printf("a");
if(b==0xb600)
printf("b");
if(c==0xb6000000)
printf("c");
return 0;
}
这段代码打印出来的结果是什么呢?? 只有一个c ,原因是a和b参与运算时都发生了整型提升变成了负数,不与原来的值相等,而c不发生整型提升。所以打印出来的是c。
(3)
int main()
{
char c = 1;
printf("%u\n", sizeof(c));//1
printf("%u\n", sizeof(+c));//4
printf("%u\n", sizeof(!c));//4
return 0;
}
第一个没有参与运算所以打印的是1,但是第二个和第三个参与了运算,所以打印的是整型大小4。
2. 算数转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换成另一个操作数的类型,否则操作无法进行,下面的层次体系称为寻常算数转换。
long double
double
float
unsigned? long? int
long int
unsigned? int
int
如果哪个操作数在上面这个表的排名较低,那么首先要转化为另一个操作数的类型后执行运算。
3.操作符的属性
复杂的表达式求值有三个影响的因素
(1)操作符的优先级
(2)操作符的结合性
(3)是否控制求值顺序
两个相邻的操作符先执行优先级高的那个,若优先级相同,取决于它们的结合性。
下面给出优先级和结合性的表格
12.操作符优先级表
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
---|
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | | () | 圆括号 | (表达式) 函数名(形参表) | | . | 成员选择(对象) | 对象.成员名 | | -> | 成员选择(指针) | 对象指针->成员名 | | 2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 | (类型) | 强制类型转换 | (数据类型)表达式 | | ++ | 自增运算符 | ++变量名 变量名++ | 单目运算符 | -- | 自减运算符 | --变量名 变量名-- | 单目运算符 | * | 取值运算符 | *指针变量 | 单目运算符 | & | 取地址运算符 | &变量名 | 单目运算符 | ! | 逻辑非运算符 | !表达式 | 单目运算符 | ~ | 按位取反运算符 | ~表达式 | 单目运算符 | sizeof | 长度运算符 | sizeof(表达式) | | 3 | / | 除 | 表达式 / 表达式 | 左到右 | 双目运算符 | * | 乘 | 表达式*表达式 | 双目运算符 | % | 余数(取模) | 整型表达式%整型表达式 | 双目运算符 | 4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 | - | 减 | 表达式-表达式 | 双目运算符 | 5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 | >> | 右移 | 变量>>表达式 | 双目运算符 | 6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 | >= | 大于等于 | 表达式>=表达式 | 双目运算符 | < | 小于 | 表达式<表达式 | 双目运算符 | <= | 小于等于 | 表达式<=表达式 | 双目运算符 | 7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 | != | 不等于 | 表达式!= 表达式 | 双目运算符 | 8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 | 9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 | 10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 | 11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 | 12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 | 13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 | 14 | = | 赋值运算符 | 变量=表达式 | 右到左 | | /= | 除后赋值 | 变量/=表达式 | | *= | 乘后赋值 | 变量*=表达式 | | %= | 取模后赋值 | 变量%=表达式 | | += | 加后赋值 | 变量+=表达式 | | -= | 减后赋值 | 变量-=表达式 | | <<= | 左移后赋值 | 变量<<=表达式 | | >>= | 右移后赋值 | 变量>>=表达式 | | &= | 按位与后赋值 | 变量&=表达式 | | ^= | 按位异或后赋值 | 变量^=表达式 | | |= | 按位或后赋值 | 变量|=表达式 | | 15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 | |
其实掌握了优先级表格也不能保证所写的代码就一定是合法的,比如这样一段代码
a*b+c*d+e*f ,这段代码在进行计算时,由于*比+的优先级高,只能保证*的计算比+早,但是优先级并不能决定第三个*比第一个执行地早。你可能会说似乎对结果没什么影响,但如果a,b,c,d,e,f是表达式呢?那结果就是天差地别,至于怎么运算取决于编译器,但是不同编译器执行后如果结果不同的话这段代码一定不是好代码,是问题代码。
这就需要我们多多总结一些常见的歧义代码的问题,和要更加了解你的编译器属性。
总结
经过一天的呕心沥血,终于将这篇文章呕完了,个人感觉对于我们大学生来说还是很细的,当然也感谢可以读到这里的你,本文的代码我是一一在VS2019上操作过的,如果你发现文章哪里有问题,或者你可以比我更细的话,欢迎和我来讨论。
最后,听说一键三连的人下一个七夕节会有人陪你过哦!!
|