C语言中的操作符详解(含三目表达式和逗号表达式)
操作符
- 一.操作符的介绍
- 1.算术操作符
- a.+和-
- b.*,/和%
- 2.移位操作符
- a.<< 左移操作符
- b.>> 右移操作符
- c.注意事项
- 3.位操作符
- a.按位与 &
- b.按位或 |
- c.按位异或 ^
- d.按位取反 ~
- e.补充
- e.1 按位异或操作符的性质
- 4.赋值操作符
- 5.双目与单目操作符
- a.++和 - -
- b.+和-
- 6.逻辑操作符
- 7.关系操作符
- a.操作符介绍
- b.注意事项
- b.1 字符串比较
- b.2 多个关系运算符不宜连用
- 8.条件操作符(三目表达式)
- 9.逗号表达式
- 10.下标引用操作符和函数调用操作符
- 11.结构体成员访问操作符
- **.: 结构体变量.成员**
- **->: 结构体指针->成员**
- 二.操作符的属性
- 1.操作符的优先级
- 2.操作符的结合性
一.操作符的介绍
1.算术操作符
算数操作符有+,-,*,/,%,它们分别对应着加,减,乘,除,取余计算
下面我们来一一介绍它们,包括它们底层的二进制计算原理
注意:
1.这里只讨论整形类型间的算术运算原理,而不讨论浮点数的,标准二进制浮点数(float/double)无法精确完成十进制小数的加减乘除运算
原因有以下几点:
计算机以二进制存储数据,部分十进制有限小数(如 0.1、0.2)转二进制后是无限循环小数。而 float/double 尾数位数固定,存储时只能截断、舍入,存入的数本身就是原数的近似值,误差从存储阶段就已产生。
浮点数加减必须先对阶,阶数小的数尾数要右移,低位数据直接丢失;运算后规格化、舍入操作会进一步放大误差,最终结果偏离真实值。
要想对小数进行精准的运算,一个可行的方法是将小数统一放大为整数后再进行相应运算,运算完后再将整数还原回固定的小数位数,这个方法适合固定小数位数的场景。
2.且这里只讨论同种整形类型间的算术运算,因为实际上不同整型类型间进行算数运算时还会涉及到整型提升与算术转换的问题,这将在后续专门讲整型提升与算术转化的文章中讲解~
a.+和-
基本使用如下:
intmain(){inta=5;intb=4;intc=a+b;intd=5-4;inte=a-4;return0;}其底层的二进制计算原理可以总结为三步:整数转补码->加减进位削位->补码转原码得整数
不知啥是补码的看这里~
下面以int类型的整数为例
inta=15;intb=-10;计算 a+b:第一步:整数转补码15->00000000000000000000000000001111-10->11111111111111111111111111110110第二步:加减进位削位 (因为是加法,所以将补码相加,等于二的位数进位)0000000000000000000000000000111111111111111111111111111111110110100000000000000000000000000000101(相加得到的补码有33位,而int类型数据只有32位比特位用于编码,所以将多出来的第一位削去)00000000000000000000000000000101第三步:补码转原码得整数 (得到的补码第一位符号为0,所以为非负数,原反补码三码相同,所以结果的的原码就是得到的补码)00000000000000000000000000000101->5自己试着计算一下a-b吧b.*,/和%
*是乘法运算符,/是除法运算符,%为取余运算符
基本使用如下:
inta=7;intb=2;doublec=2.0;intd1=a*b;(结果为14)doubled2=a/b;(结果为3.0)doubled3=a/c;(结果为3.5)由上面d2的结果是3.0,d3的结果是3.5,我们可以知道/进行除法的特点为:
当除数与被除数都是整数时,进行整除
当有任何一个数是小数时,则执行浮点除法,保留小数部分(和算术转换有关)
下面说明 * ,/ 和%的底层二进制计算原理:
*:
1.当乘数为2的n次幂时:
将被乘数补码按照移位规则(在后面的移位操作符中会讲到),左移(<<)n位得到的补码就是原数乘该2的n次幂的结果的补码
即:X << n = X × 2ⁿ
(注意,当正数运算结果超出当前位数比特位能表示的最大值或负数运算结果小于当前位数比特位能表示的最小值时,移位会发生溢出结果会出错)
2.当乘数不是2的n次幂时:
a.乘数从最低位(第 0 位)向高位逐位遍历,若第n位二进制位=1,则被乘数左移n位(等价×2的n次方),得到一项累加值;
b.若第n位二进制位=0,则跳过该位,累加值为0;
c.遍历完后运算结果=各项累加值之和
3.若涉及到负数,数值计算以该数绝对值的补码形式进行,得到的是结果的绝对值,而结果的正负即符号位由乘数与被乘数的符号位异或得到(符号为相同为0,相异为1)
下面以3*5和(-3)*5为例:
3*5:(int)3(被乘数)的补码:000000000000000000000000000000115(乘数)的补码:00000000000000000000000000000101bit0=1,不移位:00000000000000000000000000000011bit1=0,不累加:00000000000000000000000000000000bit2=1,移两位:00000000000000000000000000000011<<2=00000000000000000000000000001100各项累加=00000000000000000000000000001111=15-3*5:(int) 数值计算用绝对值|-3|*|5|,同上得:00000000000000000000000000001111=15符号位异或得符号位:1^0=1(负)所以最终结果为:-3*5=-15实际中编译器会进行优化:将小常数乘法变成移位 + 加减
例:a * 7 = a * (8-1) = a<<3 - a,比硬件 MUL 更快。
/(整数除法):
1. 当除数为 2 的 n 次幂时:
将被除数补码按照移位规则右移(>>)n 位,无符号数、正数可等价实现原数除以该 2 的 n 次幂;负数需额外修正保证向零取整。
即:正数 X >> n = X ÷ 2ⁿ
(注意,运算超出当前比特位表示范围时会出错,负数不可直接单纯右移)
2. 当除数不是 2 的 n 次幂时:
a.余数寄存器的值R初始化为被除数的绝对值,商寄存器的值Q初始化为0
依次将余数左移 1 位后减去除数得R_tmp
R_tmp非负则当前商位记 1、R=R_tmp;R_tmp为负则当前商位记 0、余数R恢复为已完成移位操作但减去除数前的值
b.若碰到当前商位计1肯定不符合的情况,当前商位计0,余数R恢复为已完成移位操作但减去除数前的值
这样逐位确定商和余数,最终得到除法结果
3. 若涉及到负数,数值计算以被除数、除数的绝对值进行运算,得到商的绝对值;商的正负由被除数与除数的符号位异或得到(符号相同为 0,相异为 1)
%(取余)就是在进行 / 运算后直接获得余数寄存器R内的值,再按照负数符号规则修正符号,最终得到取余结果。
余数符号与被除数保持一致
下面以7/3,7%3为例:
7/3:(int) a.初始化R,Q:R=00000000000000000000000000000111Q=00000000000000000000000000000000b.逐位确定商和余数:先左移一位一次(R<<1)-000……00000011=00000000000000000000000000000111上面的结果大于0,所以当前商位应为1,即Q的第一个比特位为1但是若该位计1,商就比被除数还大,在这里肯定是不合理的,所以该商位记0,余数R恢复为已完成移位操作但减去除数前的值 即:R=00000000000000000000000000001110同上面情况一样,左移1~30位都是因为上面的情况而商位计0,余数R不变 下面左移1位第31次 R=11000000000000000000000000000000(R<<1)-000……00000011=01111111111111111111111111111101>0所以Q的右数第二位记作1,R=R_tmp Q=00000000000000000000000000000010,R=01111111111111111111111111111101下面左移1位第32次(R<<1)-000……00000011=11111111111111111111111111111010<0所以Q的右数第一位记作0,R=111111111111111111111111111111010所以7/3的商Q=2,7%3余数R=12.移位操作符
移位操作符有两种:(<<) 左移操作符和(>>)右移操作符
移位操作符的操作数只能是整数,移动的是二进制补码
下面介绍具体的移位规则
a.<< 左移操作符
移位规则:左边抛弃,右边补0(相当于数值大小乘2)
b.>> 右移操作符
右移操作就更加复杂了
右移运算分为两种:逻辑右移和算数右移,他们的移位规则不同
逻辑右移:左边用0补充,右边丢弃
算术右移:左边用该数原来的符号位填充,右边丢弃
右移具体采用哪种规则取决于编译器(大部分编译器为算术右移)
右移操作相当于除以2
c.注意事项
对移位操作符,不要移动负数位,因为标准未定义
3.位操作符
位操作符有四种:按位与&,按位或|,按位异或^和按位取反~
位操作符的操作数同样只能是整数,运算的是二进制补码
下面分别介绍具体的运算机制
a.按位与 &
进行按位与的两数的补码的对应二进制位上同时为1,运算结果的对应位上才为1,否则为0
#include<iostream>usingnamespacestd;intmain(){inta=-8,b=6;intc=a&b;cout<<c<<endl;return0;}//结果为 0b.按位或 |
进行按位或的两数的补码的对应二进制位上同时为0,运算结果的对应位上才为0,否则为1
#include<iostream>usingnamespacestd;intmain(){inta=-8,b=6;intc=a|b;cout<<c<<endl;return0;}//结果为 -2c.按位异或 ^
进行按位异或的两数的补码的对应二进制位上若相同,运算结果的对应位上为0,否则为1
#include<iostream>usingnamespacestd;intmain(){inta=-8,b=6;intc=a^b;cout<<c<<endl;return0;}//结果为 -2d.按位取反 ~
按位取反 ~ 和前三个位操作不一样,前三个位操作需要两个操作数,按位取反只需要一个操作数,也叫单目操作符
进行按位取反的数的运算结果的对应二进制位上的数是该数每一位上数的反面(若原数该位上为1,则运算结果该位上数为0)
#include<iostream>usingnamespacestd;intmain(){inta=-1;intb=~a;cout<<b<<endl;return0;}//结果为 0e.补充
e.1 按位异或操作符的性质
1.a^a=0 (相同两数异或为0)
2.0^a=a (0和任何数异或结果为原数)
3.a ^ a ^ b = a ^ b ^ a (异或支持交换律)
应用:如何在不创建第三个变量的情况下实现两个数的互换?
#include<iostream>usingnamespacestd;intmain(){inta=9;intb=7;a=a^b;b=a^b;a=a^b;cout<<"a="<<a<<" b="<<b<<endl;return0;}4.赋值操作符
在变量创建时给变量一个初始值叫初始化,而在变量创建好后再给变量值,叫做赋值
赋值操作符可分为 = 和 复合赋值符
=不再赘述,下面直接说明复合赋值符
平时,我们常会对一个数进行自增,自减操作(如下)
inta=6;a=a+7;a=a-5;c语言中提供了复合赋值符来帮助我们简化上述操作的书写,复合赋值符有:
+= , -=
*= , /= ,%=
>>= , <<=
&= , |= ,^=
它们实际上是将 计算 和 赋值 这两步合在一起,直接修改变量的值
上面的代码可以简化为:
inta=6;a+=7;a-=5;5.双目与单目操作符
前面说的大部分是需要有两个操作数的双目操作符,有一些操作符只有一个操作数,叫做单目操作符
下面介绍++,- -(两个减号,中间无空格),+,-这四个单目操作符
a.++和 - -
++是一种自增操作符,a++相当于a+=1,++分为前置++和后置++
- -是一种自减操作符,a- -相当于a-=1,- -分为前置- -和后置- -
前置和后置的区别:
前置后使用先运算;后置先使用后计算
//看下面代码#include<iostream>usingnamespacestd;intmain(){inta=9;intb=a++;cout<<"a = "<<a<<" b = "<<b<<endl;intc=++a;cout<<"a = "<<a<<" c = "<<c<<endl;return0;}//后置++先使用后计算,所以int b = a++这一条语句,b先被初始化为9,然后a+=1,a变成10//前置++后使用先运算,所以int c = ++a这一条语句,a先+=1,a变成11,然后c被初始化为11b.+和-
+表示正号,-表示负号
+对操作数的正负没有影响,-会改变操作数的正负号
#include<iostream>usingnamespacestd;intmain(){inta=9;intb=-a;cout<<"a = "<<a<<" b = "<<b<<endl;return0;}//这里b==-96.逻辑操作符
逻辑运算符提供逻辑判断功能,用于构建更复杂的表达式,主要有下面三个运算符:
!:逻辑取反运算符(改变单个表达式的真假)。
&&:逻辑与运算符,就是并且的意思(两侧的表达式都为真,则为真,否则为假)
||:逻辑或运算符,就是或者的意思(两侧至少有一个表达式为真,则为真,否则为假)
7.关系操作符
a.操作符介绍
c语言中的用于比较的表达式叫做“关系表达式”,其中使用的运算符叫做“关系运算符”
主要有下面6个:
> 大于运算符
< 小于运算符
>= 大于等于运算符
<= 小于等于运算符
== 相等运算符
!= 不等运算符
例如:2 > 9 , a != i等表达式就为关系表达式
关系表达式通常返回0或1,表示真假(0为假非0为真)
b.注意事项
b.1 字符串比较
字符串的比较规则是从头到尾依次比较字符的ASCLL码值,比出大小就停止,若从头到尾都是相等两字符串才相等
而c语言中,字符串的比较不能用以上的操作符进行比较,因为它比较的是字符串的内存地址(即首字符的地址)
#include<stdio.h>intmain(){chara[]="123";charb[]="123";printf("%d",a==b);return0;}//结果为0,因为a和b为两个独立的字符数组,所以内存地址不一样,尽管字符串内容一样c语言中字符串的比较应该用strcmp,需包含头文件string.h
strcmp返回值为0表示比较的两字符串相等,返回<0的数表示str1<str2 , >0表示str1>str2
在c++中,字符串可以用类string表示,该类里对以上的比较符号进行了重载,所以可以直接用以上符号比较string类的字符串
b.2 多个关系运算符不宜连用
请看下面例子
i<j<k上面的代码看似实现了 i 小于 j 小于 k 的比较,但实际上关系表达式从左到右执行的是下面的式子
(i<j)<k(i < j)返回0或1,然后0或1再和k比较大小,这显然不是我们想要的
要想实现数学中i < j < k这样的效果,需要利用逻辑运算符
(i<j)&&(j<k)//i < j 并且 j < k//类似的还有(i<j)||(j<k)!(i<j)8.条件操作符(三目表达式)
条件操作符也叫三目操作符,需要接受三个操作数的,形式如下:
exp1 ? exp2 : exp3
条件操作符的计算逻辑是:
如果 exp1 为真,exp2 计算,计算的结果是整个表达式的结果
如果 exp1 为假,exp3 计算,计算的结果是整个表达式的结果
下面使用条件操作符找到两个数中较大的那个
#include<iostream>usingnamespacestd;intmain(){inta=5;intb=8;cout<<"更大的数为"<<(a>b?'a':'b')<<endl;return0;}9.逗号表达式
逗号表达式,就是用逗号隔开的多个表达式
exp1, exp2, exp3, …expN
逗号表达式,从左向右依次执行,整个表达式的结果是最后一个表达式的结果
#include<iostream>usingnamespacestd;intmain(){inta=1;intb=2;intc=(a>b,a=b+10,a,b=a+1);cout<<"c = "<<c<<endl;return0;}10.下标引用操作符和函数调用操作符
在访问数组中特定位置的数据时,会用到[ ],里面填对应的数组下标就可以访问到数组对应位置的数据
[ ]就是下标引用操作符
而我们在使用printf函数打印内容时,如 printf(“hehe\n”) ,函数名后的括号()就是函数调用操作符,printf 和 "hehe\n"是它的两个操作数
易知()至少有一个操作数,即函数名,如 test()
11.结构体成员访问操作符
在访问结构体内的成员时,可以用到两个结构体成员访问操作符.和->
.: 结构体变量.成员
->: 结构体指针->成员
#include<stdio.h>// 定义结构体类型structStudent{charname[20];intage;};intmain(){// 1. 用 . 访问结构体变量的成员structStudentstu={"张三",18};printf("结构体变量访问:姓名=%s,年龄=%d\n",stu.name,stu.age);// 2. 用 -> 访问结构体指针的成员structStudent*p_stu=&stu;printf("结构体指针访问:姓名=%s,年龄=%d\n",p_stu->name,p_stu->age);return0;}二.操作符的属性
C 语言的操作符有 2 个重要的属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。
1.操作符的优先级
优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行
各种运算符的优先级是不一样的
但实际上,操作符的运算顺序可以通过()来自主地规定,因为括号在 C 语言里拥有最高的运算优先级,能够强行改变表达式原本默认的运算先后顺序,就像数学的结合律,与其死记运算符之间谁先谁后,用()显然更加容易
如
(a+b)*72.操作符的结合性
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了
根据运算符是左结合,还是右结合,决定执行顺序
大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符(=)。
同样地,可以用()来自主决定执行顺序,所以操作符的结合性也不用死记