温馨提示
大家好我是Cbiltps,在我的博客中如果有难以理解的句意,难以用文字表达的重点,我会有配图。所以我的博客配图非常重要!!!
如果你对我感兴趣请看我的!
本章重点
- 各种操作符的介绍。
- 表达式求值
正文开始
1. 操作符分类
- 算术操作符 移位操作符 位操作符 赋值操作符 单目操作符 关系操作符 逻辑操作符 条件操作符 逗号表达式 下标引用、函数调用和结构成员
2.算数操作符
算数操作符有:+ – * / % 除了%操作符之外,其他的几个操作符可以作用于整数和浮点数。
2.1 /操作符
#include <stdio.h> int main() { int ret = 9 / 2;//对于 /(除号) 两边都是整数,执行的整数除法 double ret2 = 9 / 2;//它的值是什么? double ret3 = 9 / 2.0;//它的值是什么? printf("%d ", ret); printf("%lf ", ret2); printf("%lf ", ret3); return 0; }
运行结果如下: 第二个结果还是4.0,为什么呢? 因为操作数中有浮点数,才执行浮点数除法。
2.2 %操作符
%操作符是取模操作符,也叫取余操作符。
int ret4 = 10 % 4; printf("%d", ret4);//打印出来是2
注意:%操作符只能针对整形类型
3. 位移操作符
<< 左移操作符 >> 右移操作符
3.1 <<左移操作符
int a = 5; int b = a << 1; printf("%d ", b);//打印出来的是 10 //要看懂下面的图解,要明白一些知识点: //移位操作符,移动的是二进制位 //对于整数的二进制有3中表示形式:原码、反码、补码 //正整数 - 原码、反码、补码相同 //负整数 //原码 - 直接按照数字的正负写出的二进制序列 //反码 - 原码的符号位不变,其他位按位取法得到的 //补码 - 反码+1 //整数 在内存中存储的是二进制的补码
如果是一个负数的话,看代码:
int c = -1; int d = c << 1; printf("%d ", d);//打印的是原码的值,打印出来是 -2 //10000000000000000000000000000001 - 原码 //11111111111111111111111111111110 - 反码 //11111111111111111111111111111111 - 补码
移位规则:左边抛弃、右边补0
关于原码、反码、补码,请大家看!
3.2 >>左移操作符
移位规则: 1. 逻辑移位:左边用0填充,右边丢弃 2. 算术移位:左边用原该值的符号位填充,右边丢弃
举例:
int a = 5; int b = a >> 1; printf("%d ", b);//打印出来的是 2
再用负数举个例子:
int c = -1; int d = c << 1; printf("%d ", d);//打印出来还是 -1
在这里是补了原来的符号位,所以 VS2019 采用算术右移!
警告⚠ : 1:对于移位运算符,不要移动负数位,这个是标准未定义的。 2:不管是被移动数还是移动的位数都必须是整数。
例如(错误演示):
int num = 10; num>>-1;//error
4. 位操作符
位操作符有:
- & 按位与 | 按位或 ^ 按位异或
注:他们的操作数必须是整数。
4.1 &按位与操作符
计算规则: 1:二进制位上只要有0,那就是0 2:二进制位上两个同时为1,那就是1
int a = 3; int b = -2; int c = a & b; printf("%d ", c);//打印出来是 2 //%d - 说明我们要打印c的值,以有符号的形式 //00000000000000000000000000000011 -3的原码 //11111111111111111111111111111110 -2的补码 //00000000000000000000000000000010 这个数是2
4.2 |按位或操作符
计算规则: 二进制位只要有1,就为1
int a = 3; int b = -2; int c = a | b; printf("%d ", c);//打印 -1 //00000000000000000000000000000011 -3的原码 //11111111111111111111111111111110 -2的补码 //11111111111111111111111111111111 //11111111111111111111111111111111 补码 //11111111111111111111111111111110 反码 //10000000000000000000000000000001 原码 它的值是-1
4.3 ^按位异或操作符
计算规则: 二进制位相同为0,相异为1
int a = 3; int b = -2; int c = a ^ b;//打印出来是 -3 printf("%d ", c); //00000000000000000000000000000011 -3的原码 //11111111111111111111111111111110 -2的补码 //11111111111111111111111111111101 //11111111111111111111111111111101 补码 //11111111111111111111111111111100 反码 //10000000000000000000000000000011 原码 它的值是-3
一道变态的面试题:
不能创建临时变量(第三个变量),实现两个数的交换。
#include <stdio.h> int main() { int a = 3; int b = 5; printf("交换前:a=%d b=%d ", a, b); a = a ^ b; b = a ^ b; a = a ^ b; printf("交换后:a=%d b=%d ", a, b); return 0; }
思路图解:
注意:这样写代码的可读性不够好,而且只适用于整型
5. 赋值操作符
赋值操作符可以让你得到一个你之前不满意的值,也就是你可以给自己重新赋值。
int weight = 120;//体重 weight = 89;//不满意就赋值 double salary = 10000.0; salary = 20000.0;//使用赋值操作符赋值
赋值操作符可以连续使用,比如:
int a = 10; int x = 0; int y = 20; a = x = y + 1;//连续赋值
这样的代码感觉怎么样?
那这样写:
x = y + 1; a = x;
更加清晰爽朗而且易于调试。
复合赋值符:
- += -= *= /= %= <<= &= |= ^=
这些运算符都可以写成复合的效果,比如:
int x = 10; x = x + 10; x += 10;//复合赋值
6. 单目操作符
单目操作符就是只有一个操作数的操作符
6.1 各种单目操作符
! 逻辑反操作
– 负值
+ 正值
& 取地址
&arr[0];//数组首元素的地址 &arr[9];//取出的是第10个元素的地址 &arr;//取出数组的地址
sizeof 操作数的类型长度(以字节为单位)
- sizeof是操作符,不是函数 sizeof是计算变量或者类型创建变量的内存大小(单位:字节),单位是和内存中存放什么数据没有关系
//以下几种写法都是一样的 printf("%d ", sizeof(a));//4 printf("%d ", sizeof a);//4 printf("%d ", sizeof(int));//4
//这里有一个问题; int a = 5; short s = 10; printf("%d ", sizeof(s = a + 2));//打印出来是 2 printf("%d ", s); //打印出来是 10 因为:sizeof 内部的表达式不参与运算
~ 对一个数的二进制按位取反
- 包括符号位也可以按位取反
#incluide <stdio.h> int main() { int a = 0; //00000000000000000000000000000000 int b = ~a; printf("%d ", b); //00000000000000000000000000000000 //11111111111111111111111111111111 所有位按位取反 // //11111111111111111111111111111110 反码 //10000000000000000000000000000001 原码 //-1 return 0; }
— 前置、后置–
- 前置- -:先- -,后使用
int a = 10; int b = a--; printf("%d ", b);//这里打印出来还是 10
- 后置- -:先使用,后- –
int a = 10; int b = --a; printf("%d ", b);//这里打印出来才是9
++ 前置、后置++
- 前置++:先++,后使用 后置++:先使用,后++
千万注意:不要这样子写代码(垃圾代码),会被公司开除的!
int main() { int a = 1; int b = (++a) + (++a) + (++a);//err printf("b=%d ", b); return 0; }
这里两个编译器下不一样!
* 间接访问操作符(解引用操作符),可以和& 取地址搭配使用
(类型) 强制类型转换
int a = (int)3.14;//默认写出的浮点数是double的,所以可以强制转换 printf("%d ", a); return 0;
6.2 sizeof 和 数组
我们来看一个题,看它们分别输出的是多少:
#include <stdio.h> void test1(int arr[])//它的本质数是(int* arr[]) { printf("%d ", sizeof(arr));//传的是首元素的地址 } void test2(char ch[]) { printf("%d ", sizeof(ch));//(4) } int main() { int arr[10] = { 0 }; char ch[10] = { 0 }; printf("%d ", sizeof(arr));//数组名单独放在sizeof内部,数组名表示整个数组 printf("%d ", sizeof(ch));//(3) test1(arr); test2(ch); return 0; }
输出结果: 更多关于数组的知识请看!
7. 关系操作符(后面的操作符不单独出现在目录中)
关系操作符在同类之间比较才有意义!
- > >= < <= != 用于测试“不相等” == 用于测试“相等”
这些关系运算符比较简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱。 注意:在编程的时候 = 和 == 写错,会导致出错!
8. 逻辑操作符
- && 逻辑与 一假必假:
int a = 0; int b = 3; int c = a && b;//只判断真假,所以打印出来是 0
- || 逻辑或 一真必真:
int a = 0; int b = 3; int c = a || b; printf("%d ", c);//打印出来是 1
来看一道 360 的笔试题:
#include <stdio.h> int main() { int i = 0, a = 0, b = 2, c = 3, d = 4; i = a++ && ++b && d++; //i = a++||++b||d++; printf(" a = %d b = %d c = %d d = %d ", a, b, c, d);//打印出来的是1 2 3 4 return 0; }
思路图解: 那如果是逻辑或的话:
#include <stdio.h> int main() { int i = 0, a = 0, b = 2, c = 3, d = 4; i = a++ || ++b || d++; printf(" a = %d b = %d c = %d d = %d ", a, b, c, d);//打印错来的是1 3 3 4 return 0; }
思路图解:
9. 条件操作符
条件操作符也叫三目操作符
exp1 ? exp2 : exp3
int a = 0; int b = 0; if (a > 5) b = 3; else b = -3; //这里有更简单的写法: (a > 5) ? (b = 3) : (b = -3);//直接搞定
10. 逗号表达式
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。 逗号表达式规则:从左向右依次执行,整个表达式的结果是最后一个表达式的结果。
代码演示:
//代码1 int a = 1; int b = 2; int c = (a>b, a=b+10, a, b=a+1);//c是多少?看的是最后一个表达式 //代码2 if (a =b + 1, c=a / 2, d > 0)//最后一个表达式判断的是 d > 0
//代码3 a = get_val(); count_val(a); while (a > 0)//注意;这样写非常的冗余 { //业务处理 a = get_val(); count_val(a); } //如果使用逗号表达式,改写: while (a = get_val(), count_val(a), a > 0) { //业务处理 }
11. 下标引用、函数调用和结构成员
[ ] 下标引用操作符 操作数:一个数组名 + 一个索引值
int arr[10];//创建数组 arr[9] = 10;//实用下标引用操作符。 //[ ]的两个操作数是arr和9。
//下面表达的意思都是相同的 arr[4] -- > *(arr+4) --> *(4+arr) --> 4[arr]
( ) 函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
void test() { printf("hehe "); } int main() { test();//这里就是函数调用操作符 return 0; }
结构成员访问操作符
- . 结构变量.成员名 -> 结构体指针->成员名
#include <stdio.h> struct Book { char name[20]; float price; char id[10]; }; void print1(struct Book b) { printf("书名: %s ", b.name);//在这里访问结构体成员 printf("价格: %f ", b.price); printf("书号: %s ", b.id); //*(b.name); } void print2(struct Book* pb) { /*printf("书名: %s ", (*pb).name); printf("价格: %f ", (*pb).price); printf("书号: %s ", (*pb).id);*/ printf("书名: %s ", pb->name);//也可以这样访问 printf("价格: %f ", pb->price); printf("书号: %s ", pb->id); } int main() { struct Book b = { "C语言程序设计", 55.5f, "C20190201"}; print2(&b); //print1(b); //结构成员访问操作符 //结构变量.成员名 //结构体指针->成员名 //(*结构体指针).成员名 return 0; }
12. 表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。 同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
12.1 隐式类型转换
C语言的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在 CPU 的相应运算器件内执行,CPU 内整型运算器 (ALU) 的操作数的字节长度 一般就是 int 的字节长度,同时也是 CPU 的通用寄存器的长度。
因此,即使两个 char 类型的相加,在 CPU 执行时实际上也要先转换为 CPU 内整型操作数的标准长 度。
通用 CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转 换为 int 或 unsigned int,然后才能送入 CPU 去执行运算。
如何进行整体提升呢? 整形提升是按照变量的数据类型的符号位来提升的
- 负数的整型提升:
char c1 = -1; //变量c1的二进制位(补码)中只有8个比特位: //1111111 //因为 char 是有符号的 char //所以整形提升的时候,高位补充符号位,即为1 //提升之后的结果是: //11111111111111111111111111111111
- 正数的整型提升:
char c2 = 1; //变量c2的二进制位(补码)中只有8个比特位: //00000001 //因为 char 是有符号的 char //所以整形提升的时候,高位补充符号位,即为0 //提升之后的结果是: //00000000000000000000000000000001
注意:无符号整形提升,高位补0
//举例: #include <stdio.h> int main() { char a = 3;//a是1byte - 8bit //00000000000000000000000000000011 //00000011 - a char b = 127;//b是1byte - 8bit //00000000000000000000000001111111 //01111111 - b //a和b都是char类型,自身大小的都是1byte,所以这里计算的时候要进行整型提升 //00000000000000000000000000000011 a整形提升 //00000000000000000000000001111111 b整形提升 //00000000000000000000000010000010 相加后 // char c = a + b; //10000010 - c //11111111111111111111111110000010 整形提升后的是补码 //11111111111111111111111110000001 反码 //10000000000000000000000001111110 原码 //-126 printf("%d ", c); return 0; }
a和b的值被提升为普通整型,然后再执行加法运算; 加法运算完成之后,结果将被截断,然后再存储于c中。
12.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类 型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
1.long double 2.double 3.float 4.unsigned long int 5.long int 6.unsigned int 7.int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为高一级操作数的类型后执行运算。
int a = 4; float f = 4.5f; float r = a + f; //a首先转换成 float类型 然后和f相加
警告:算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14; int num = f;//这样隐式转换,会有精度丢失
12.3 操作符的属性
复杂表达式的求值有三个影响的因素:
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
两个相邻的操作符先执行哪个? 取决于他们的优先级;如果两者的优先级相同,取决于他们的结合性。
//两个相邻的操作符: int a = 2; int b = 4; int c = a * b + 3;// * 的优先级比 + 高,所以先算乘法,再算加法
//两者的优先级相同: int a = 2; int b = 4; int c = a + b + 3;//两者优先级相同,看它们的结合性(看下面的操作符优先级表),所以是从左到右加
操作符优先级表(由高到低展示):
然后,大家想一想,是不是掌握了表达式里面操作符的优先级、结合性、是否控制求值顺序以及前面讲的整型提升还有算数转换就可以进行计算了呢?是唯一的结果吗?
统一回答一下:答案是否定的
看几个有问题的表达式:
//表达式的求值部分由操作符的优先级决定 //问题表达式1: a*b + c*d + e*f
注释:代码1在计算的时候,由于 * 比 + 的优先级高,只能保证,*的计算是比 + 早,但是优先级并 不能决定第三个 * 比第一个 + 早执行。
//问题表达式2 c + --c;
注释:同上,操作符的优先级只能决定自减 – 的运算在 + 的运算的前面,但是我们并没有办法得 知,+ 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义 的。
//问题代码3-非法表达式 int main() { int i = 10; i = i-- - --i * (i = -3) * i++ + ++i; printf("i = %d ", i); return 0; }
有大佬在不同的编译器下面测试了结果,发现各不相同! 例子还有很多,我就不一一举例了!
最后做一个总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
全文结束
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/290670.html