一、算术操作符
即基本的+、-、*、/ 和 %。但也有几个需要注意的地方:
-
除了 ‘%’ 取模操作符只能作用整数,其他可以作用于整数和浮点数
-
对于除法,只要有操作数为浮点数就执行浮点数除法。如果两个操作数都为整数,执行整数除法,此时当你想创建一个浮点型变量来接收时,编译器会给出警告。(注意:进行相应运算后准备输出时,要注意好输出的格式,不要把本应是用 %f 输出的浮点型习惯写成了 %d)
二、移位操作符
分为左移操作符 ‘<<’ 与 右移操作符 ‘>>’
所谓移指的就是将一个数的二进制补码(即整数在内存中的存储方式)整体向左或向右移动(一定情况下,其实可以相应地视为×2或÷2)
1. 左移操作符
移位规则:移出左边界的抛弃,右边空着的补0
需要注意的是:这个移位的动作仅用来理解,若操作数在没有被赋值的情况下自身的值不会发生变化。即 int n = 2;n<<1; 此时n的值不变,还是2,若想让n为移位后的结果就需写成:n = n<<1;
2. 右移操作符
移位规则:对于右移有两种
(1)逻辑右移:同左移的移位规则,只是移动方向不同
(2)算术右移:移出右边界的抛弃,左边空着的用原该值符号位填充
说明:对二进制原码,符号位是1代表其为负数,为0代表其为正数,因为正数的原、反、补三码相同,所以这个规则主要还是针对负数
其他注意点:
(1)对于移位运算符,不要移动负数位,这个是标准未定义的行为
(2)我在用VS2022时发现:左移负的无符号数时会警告这是未定义的行为,如:int a = -2<<1; 但程序可以运行且能算出正确的值
三、位操作符
共有3个:按位与‘&’、按位或 ‘|’ 和按位异或 ‘^’ 。他们的操作数都是整数,运算过程是相应二级制码的对比运算。
即两个操作数相应的二进制数逐位进行对比,满足 “与” 逻辑或 “或” 逻辑或 “异或” 逻辑后,那一位的值就为1。每一位都对比完后所得到的新二进制序列就是计算的结果。
- 简单应用:
(1)按位与:可求一个整数存储在内存中二进制中1的个数,如:
int main()
{
int n;
scanf("%d", &n);
int count = 0;
while(n)
{
count++;
n = n & (n - 1);
}
printf("%d", count);
return 0;
}
解释:核心:语句n = n & (n - 1);
能把n二进制序列最右边的1变为0
假设n = 15;则如下为执行过程:
第一次:n二进制为1111;n-1二进制为1110;相与后得 1110
第二次:n二进制为1110;n-1二进制为1101;相与后得 1100
第三次:n二进制为1100;n-1二级制为1011;相与后得 1000
第四次:n二进制为1000;n-1二进制为0111;相与后得 0000
可以发现,n从原来的1111(4个1),先变为了1110(3个1),再变为了1100(2个1),再变为1000(1个1),最后变为0000(0个1)。其变化次数其实就是其二进制序列中含有1的个数。
原理:减1之后,一定会出现0,1。若最低位上已经为0,那么会朝高位借1,一直借到有1的那个高位,借完之后,高位上的1就为0,前面的0由于借位就全为1了。进行相与运算之后,原来(没减1)二进制序列中最右边的1就变为0了。
如:在第二次中,n为1110,其最右边的1在从右往左数第二个位上;那么在n - 1进行借1时,有1的那个高位其实就是上述最右边的1,借完之后得到n-1的结果为1101。此时n和n-1进行相与运算:
1 1 1 0
&
1 1 0 1
=
1 1 0 0
最终效果就是把n二进制序列中最右边的1边为了0,即1110 -> 1100
(2)按位或:可将一个整数的二进制补码的某一位上的0改为1,如:
n = n | (1 << i);
执行完上面这条语句后,n的二进制补码中的第 i+1 位若是0则会被改为1
(3)按位异或:可不创建临时变量而实现两个数的交换
int main()
{
int a = 1, b = 2;
a = a ^ b;
b = a ^ b;
//逻辑层面的理解:此时a为a^b,故a^b^b等价于a^0,也就是a本身
a = a ^ b;
//同理,右边是a^b^a等价于b^0,也就是b本身
printf("%d %d", a, b);
return 0;
}
//也可通过二进制补码的具体变化来理解
补充:当然也可以通过两变量间的加减法实现,但相比于只在二进制位上进行0 1互换的异或运算来说,两变量的加减法含有可能因为进位问题而造成溢出的风险。这里作为一个知识点学会就行,实际在交换两个数最常使用的还是创建一个临时变量。因为异或交换两个数其实有以下三个缺点:
可读性较差;执行效率不如创建临时变量的方法;只能支持整数的交换
四、赋值操作符
顾名思义,就是给一个变量进行赋值操作。此部分比较需要注意的地方就是其使用的代码风格问题。例:
a=x=y+1;
与 a=x;x=y+1;
二者所表达的一样,但前者的连续赋值相对后者就没这么清晰与便于调试。此外,就是能使语句更整洁的符合运算符,如 +=、-=等等。
五、单目操作符
即操作数只有一个的操作符,共有如下这些:
‘ ! ’ 逻辑反操作
‘ - ’ 负值
‘ + ’ 正值
‘ & ’ 取地址
‘ sizeof ’ 操作数的类型长度(以字节为单位)
‘ ~ ’ 对一个数的二进制按位取反
‘ - - ’ 前置、后置- -
‘ ++ ’ 前置、后置++
‘ * ’ 间接访问操作符(解引用操作符)
‘ (类型) ’ 强制类型转换
其实大部分都是经常使用的,都比较好理解。其中比较需要注意的就是 sizeof 和 ~。
- 按位取反:和上面的位操作符相似,把一个数的二进制的每一位,包括符号位,按位取反(把0和1置换)每一位都置换完后所得到的新二进制序列就是计算的结果。
简单应用:可将二进制数的某一位上的1改为0:
n = n & ~(1 << i);
如上代码,n的二进制补码中的第 i+1 位会被改为1
- sizeof:sizeof的作用是以字节为单位来计算操作数的类型长度。但sizeof括号中的表达式是不参与运算的,具体例子如下面这段代码:
int main()
{
short s = 5;
int a = 2;
printf("%d\n",sizeof(s=a+2));
printf("%d\n", s);
return 0;
}
运算结果:
可以这么理解:s=a+2在编译期间就已处理过了,而括号中的表达式只是说把a+2这个结果放到s中,让s说了算,即sizeof的目的只是找到最终要计算的类型的大小。所以s中最终结果还是5。
(1)sizeof与数组的关系:
如下面这段代码所示
void print_(int arr[])
{
printf("%d", sizeof(arr));
}
int main()
{
int arr[5] = { 0 };
printf("%d\n",sizeof(arr));
print_(arr);
return 0;
}
输出结果:
解释:主函数中的sizeof(数组名)计算的是整个数组的长度,故与数组本身的类型有关,由于该数组为整型数组,共有5个元素,故结果为10
而函数调用中的sizeof(数组名)计算的其实是该数组首元素地址的大小(数组传参,传递的是首元素的地址),既然是地址的大小,那此时就与数组本身的类型无关了,地址的大小在32位机上为4,在64位机上为8.
(2)sizeof计算字符串与strlen计算字符串:
如上述,sizeof的作用是计算操作数的类型长度,故若操作数为字符串时,它就会去计算字符串中每个字符的类型大小(1字节)并相加后返回,当然也就包括了字符串的结束标志 ‘ \0 ’。而strlen计算字符串的大小是以 ‘ \0 ’为结束标志,看其之前有几个字符,统计数量并返回。
如下代码:
printf("%d\n", sizeof("abc"));
printf("%d\n", strlen("abc"));
输出结果:
六、关系操作符
就是两操作数间的关系运算,满足关系则运算结果为1,反之为0。
总共有下面六种关系:
’ > ’
’ >= ’
’ < ’
’ <= ’
’ != ’
’ == ’
也是属于常见和常用的,没有太多需要深入理解的地方,唯一需要注意的就是其中的 “==” 与赋值操作符 “=” 的区别。
七、逻辑操作符
共有两种:逻辑与 ‘&&’ 和 逻辑或 ‘ || ’
- 注意二者与单目操作符按位与 “&” 和 按位或 “|” 的区别
如:1&2结果为0,而1&&2结果为1。前者是在操作数的二进制补码上的操作运算,而后者相当于是对操作数数本身进行逻辑判断运算。
- 特有的运算规则(短路运算):
(1)表达式1 && 表达式2 只要表达式1的值为假,后面不再算
(2)表达式1 || 表达式2 只要表达式1的值为真,后面不再算
如以下这段代码:
#include <stdio.h>
int main()
{
int i = 0,a=0,b=2,c =3,d=4;
i = a++ && ++b && d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
输出结果:
解释:a++先使用后自增,即&&的左操作数为假,后面不在算
八、条件操作符
格式为:exp1?exp2:exp3
也称为三目操作符,有三个操作数,可以理解为是 if else 语句的“简化”版
即:if (a>b) max = a; else max = b;
等价于 max = (a>b) ? a : b
九、逗号表达式
格式为:(表达式1,表达式2,…表达式N)
形式就是用逗号隔开的多个表达式
运算过程:从左向右依次执行,整个表达式的结果即为最后一个表达式的结果。
如下面这段代码:
int a = 1;
int b = 2;
int c = (a = b + 10, a++,b = a + 1);
c的结果为14,但a与b的值也相应发生改变,为13和14
应用:可以让代码更简洁,如下代码:
a = get_();
count_(a);
while (a > 0)
{
……
}
//用逗号表达式改写如下
while (a = get_(),count_(a),a > 0)
{
……
}
十、下标引用、函数调用和结构体访问
也是属于常见常用的操作符
-
下标引用 “[ ]” 操作数:数组名+索引值
-
函数调用 “()” 操作数:一个或多个。第一个操作数就是函数名,其余的就是传给函数的参数
-
结构体访问 “.” 和 “->” 操作数:前者为 “结构体 . 成员名”; 后者为 “结构体指针 -> 成员名”
十一、使用操作符进行表达式求值时的注意事项
表达式求值的顺序一部分由操作符的优先级与结合性决定
1. 隐式类型转换(整型提升)
C的整型算术运算总是至少以(缺省)整型的精度来进行的。即若操作数在运算的过程中自身的大小达不到一个整型的大小时,也就是类型的精度不够时,就会在使用前被转化为整型,这种转化就称作整型提升。所以实质上就两种类型会发生整型提升:短整型 short(大小为2个字节)和 字符型 char(大小为1个字节)
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度
一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
提升的方法:按照变量的数据类型的符号位来提升
例:对于有符号数:
负数:char c = -1 变量c的二进制补码为11111111(8个比特位),整型提升时,高位补符号位,也就是1,故提升之后的结果就为:11111111111111111111111111111111(32个比特位)
正数:char c = 1 ,提升前二进制补码为:00000001
提升后为:00000000000000000000000000000001
对于无符号数的整型提升,高位补0
通过下面这段代码加深理解:
#include <stdio.h>
int main()
{
char a = 127;
char b = 3;
char c = a+b;
printf("%d", c);
return 0;
}
输出结果:
其中发生了两次整型提升:
第一次:c = a + b
变量a和b发生整型提升来进行加法运算,运算的结果为:00000000000000000000000100000010(二进制补码)
运算结束后,需把这个结果赋值给变量c,故提升后的结果会发生截断而恢复原来的类型,则运算结束后c的补码为10000010。
第二次:printf(“%d”, c);
即需要将c以整型的形式输出。此时在第一次整型提升基础上进行提升,故提升之后的二级制补码就为11111111111111111111111110000010,转换为原码就是屏幕上输出的-126。(其实在第一次整型提升结束后,即c的补码为10000010时,c的值就已经是-126了。)
再看一段代码:
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
输出结果:
解释:这里先补充一个知识点:表达式可以归结两个属性,值属性和类型属性,sizeof想获取的就是类型属性,因而会发生整型提升而不是上面在介绍运算符sizeof时所说的sizeof括号中的内容不参与运算
2. 算术转换
如果某个操作符的各操作数属于不同类型,那么只有当其中一个操作数向另一个操作数的类型转换时,操作才能进行。操作数精度较低的类型转为精度较高的类型后再执行运算的转换体系称为寻常算术转换,也是合理的算术转换。
注意不合理的算术转换,如:float f = 3.14;int n = f
此时会发生隐式转换(如 1 中的代码例,精度高向精度低转换会发生截断),会有精度的丢失
3. 操作符的属性
此属性主要服务于复杂表达式的求值。两个相邻的操作符的执行取决于他们的优先级,若两者优先级相同,则取决于他们的结合性。还有一点需要注意的其是否控制了求值的顺序,如“&&” “||” 和 “ ? : ” 及逗号表达式等。
下面举几个问题表达式的例子加深一下印象:
(1)a*b + c*d + e*f;
解释: 对以上表达式,由于乘法运算符比加法运算符的优先级高,只能保证,乘法的计算是比加法早,但是优先级并不能决定第三个乘号比第一个加号早执行。即表达式计算的顺序就可能是:
a * b 然后 c * d 然后 a * b + c * d 然后 e * f 最后 a * b + c * d + e * f
或者是:
a * b 然后 c * d 然后 e * f 然后 a * b + c * d 最后 a * b + c * d + e * f
那么这里我们如果把a b c d e f看作是会互相影响的表达式而不只是简单的变量就会出现一些问题了。
(2)c + --c;
解释: 同(1),操作符的优先级只能决定自减 - - 的运算在 + 的运算的前面,但是我们并没有办法得知,+ 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
如:若c的值为3,那么表达式可能是3 + 2 = 5;也有可能是2 + 2 = 4
(3)answer = fun() - fun() * fun();
解释: 上述代码中我们只能通过操作符的优先级得知:先算乘法,
再算减法。但函数的调用先后顺序无法通过操作符的优先级确定。
(4)int i = 1; int ret = (++i) + (++i) + (++i);
解释: 这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。
所以,我们在写表达式的时候要严格按照操作符的属性来确定唯一的计算路径,若计算路径不唯一确定,那么该表达式就会存在问题,它的最终结果在不同编译环境下的值可能就不同了。所以如果所需计算路径确实复杂而自己又没有把握的情况下,不妨多分几步来写。
以上就是我对操作符这一部分知识的总结啦。
看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或有误地方的地方还恳请过路的朋友们留个评论,多多指点,谢谢朋友们🌹🌹🌹!