6.关系操作符6.关系操作符
关系运算符的应用
关系运算符的运算结果只有 0 或 1。当条件成立时结果为 1,条件不成立结果为 0。
我们来看一段代码,如下:
#include <stdio.h> int main() { printf("%d\n", 1 > 2); printf("%d\n", 1 < 2); return 0; }
得到的输出结果为:
0 1
原因就是1 > 2在数学上是不成立的,所以结果为0;而1 < 2在数学上是不成立的,所以结果为1
7.逻辑操作符
1.&& 逻辑与 判断规则:两真则真,一假就假
2.|| 逻辑或 判断规则:一真则真,两假才假
详解:
位操作符中的 & 和 | 操作的是数的二进制补码 逻辑操作符中的 && 和 || 操作的是数本身 (数为0为假,数为非0为真)
使用逻辑操作符之后,若表达式整体判断为真,则值为1(1表示真,固定值) 若表达式整体判断为假,则值为0(0表示假,固定值)
举例一:
#include<stdio.h> int main() { int i = 0, a = 0, b = 2, c = 3, d = 4; i = a++ && ++b && d++; //a++ 先使用a的值0,再++, 判断a=0后,后面的 ++b && d++均不执行 printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d); return 0; }
总结:
逻辑与操作&& : 左边判断为假后,右边的表达式不再执行,直接停止,返回值0(假)
逻辑或操作 || 左边判断为真后,右边的表达式不再执行,直接停止,返回值1(真)
8.条件操作符
exp1 ? exp2 : exp3
解释:表达式1进行判断,若结果为真,则 exp1 ? exp2 : exp3 整体的结果为 表达式 exp2 的结果
若结果为假,则exp1 ? exp2 : exp3 整体的结果为 表达式 exp3 的结果
exp是expression的缩写,中文意思为“表达”
举例:
#include<stdio.h> int main() { int a = 10; int b = 20; int max; //if (a > 5) // b = 3; //else // b = -3; //b = a > 5 ? 3 : -3; max = a > b ? a : b; printf("a = %d\nb = %d\nmax = %d\n", a, b, max); return 0; }
9.逗号表达式
exp1, exp2, exp3, …….expN
逗号表达式:就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行,整个表达式的结果是最后一个表达式的结果。
举例1:
#include<stdio.h> int main() { int a = 1; int b = 2; int c = (a > b, a = b + 10, a, b = a + 1); printf("%d\n", c); return 0; }
讲解:这里我们要输出c的值,那么c的值是什么呢?
int c = (a > b, a = b + 10, a, b = a + 1);
这里初始化c的时候应用了逗号表达式,逗号表达式从左向右依次执行,整个表达式结果为最后一个表达式的结果,我们按照这个原则去推导,a > b 执行但是没有结果 a = b + 10 相当于a = 2 + 10 = 12, a执行但不产生结果,b = a + 1 = 12 + 1 = 13,整个表达式的值也等于b 即c = 13。
对于这个结果我们可以运行程序验证一下。
拓展:那这里a, b的值分别为多少呢?
a = 12, b = 13 不再是原本的1和2,说明int c = (a > b, a = b + 10, a, b = a + 1); 中运算的结果有效接保留了下来。
10.下标引用、函数调用和结构成员
1.下标引用操作符 [ ]
操作数: 一个数组名 + 一个索引值
#include<stdio.h> int main() { int a[10] = { 0 };//1.定义一个数组 //2.如果我们想要访问第5个数组元素,并给其赋值 a[4] = 10;//3.用数组变量名+[]+下标索引数字 //4.[ ] 对应的两个操作数一个是变量名a 另外一个就是下标/索引值 4 printf("%d\n", a[4]); return 0; }
2.函数调用操作符()
接收一个或者多个操作数,第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include<stdio.h> //2.这个地方的()不是函数调用操作符,是函数定义的语法规则 int get_max(int x, int y) { return x > y ? x : y; } int main() { int a = 10; int b = 20; //1.调用函数的时候使用的() 就是函数调用操作符 int max = get_max(a, b); //3.这里的函数调用操作符()的操作数为 函数名get_max,函数参数a,函数参数b 总共三个操作数 //4.对于函数调用操作符()而言,其操作数至少要有一个(函数名),可以有多个 printf("max = %d\n", max); return 0; }
3.访问一个结构的成员 . ->
思考:如何创建结构体类型和结构体类型变量呢?
#include<stdio.h> //1.现实中为了描述复杂的对象,构建结构体类型 //2.比如学生,有姓名,年龄,学号等信息 //创建一个结构体类型 struct Stu //3.struct Stu是一个结构体类型,表示学生类型 //类型是用来创建变量的 { char name[20];//姓名 int age;//年龄 char id[20];//学号 }; int main() { int a = 10; //使用struct Stu这个类型创建了一个学生对象s1,并初始化 struct Stu s1 = { "张三",20,"20210403" }; return 0; }
结构体类型和结构体类型变量详解:
结构体类型和结构体类型变量的关系类似于图纸和房子的关系
在盖房子之前,需要有房屋盖建的设计图纸 。有了图纸,才能根据图纸盖出好房子。
在设计图纸环节,我们并不会真正去盖房子,所以就不会占用土地。
同样的道理,在创建结构体类型的时候,我们并不会真正去存储什么信息,只有在利用结构体类型创建变量的时候,才会去存储相应的信息,向内存申请存储空间。
创建好结构体变量后,我们想要打印相关的信息来看一下,这时候就会用到访问一个结构的成员.或 ->
1.结构体变量.成员名.操作符的操作数一个是结构体变量,另一个是成员名
2.将结构的地址取出来放入指针,通过指针进行访问
第2中方式有点太啰嗦了,先要对指针解引用,如何通过.操作符访问结构体变量的成员。
实际上通过指针访问的方式可以更加精简一些
3.结构体指针->成员名 通过指针的方式直接访问到变量的成员。
完整代码:
#include<stdio.h> //1.现实中为了描述复杂的对象,构建结构体类型 //2.比如学生,有姓名,年龄,学号等信息 //创建一个结构体类型 struct Stu //3.struct Stu是一个结构体类型,表示学生类型 //类型是用来创建变量的 { char name[20];//姓名 int age;//年龄 char id[20];//学号 }; int main() { int a = 10; //使用struct Stu这个类型创建了一个学生对象s1,并初始化 struct Stu s1 = { "张三",20,"20210403" }; struct Stu* ps = &s1; //结构体指针->成员名 printf("%s\n", ps->name); printf("%d\n", ps->age); printf("%s\n", ps->id); //printf("%s\n", (*ps).name); //printf("%d\n", (*ps).age); //printf("%s\n", (*ps).id); //printf("%s\n", s1.name); //printf("%d\n", s1.age); //printf("%s\n", s1.id); //结构体变量.成员名 .操作符的操作数一个是结构体变量,另一个是成员名 return 0; }
表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
隐式类型转换(悄悄的进行类型转换)
①C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
是不是看的不太懂?这里我们举例说明:
#include<stdio.h> int main() { char a = 3; //00000000 00000000 00000000 00000011 整数3 //00000011 ->a a是char类型,只有一个字节,这时候就会发生截断 //截断的规则:挑最小最低位的字节内容 char b = 127; //00000000 00000000 00000000 01111111 整数127 //01111111 ->b //a+b a和b如何相加 按照变量数据类型的符号位来提升的 //00000011 ->00000000 00000000 00000000 00000011 //01111111 ->00000000 00000000 00000000 01111111 // 00000000 00000000 00000000 10000010 char c = a + b;//这时候a,b被提升为普通整型 //10000010 ->c //后面我们要以%d的形式打印,需要进行整型提升:按照变量数据类型的符号位来提升的 //11111111 11111111 11111111 10000010 ---补码 //11111111 11111111 11111111 10000001 ---反码 //10000000 00000000 00000000 01111110 ---原码 -126 printf("%d\n", c); printf("%c\n", c); return 0; }
至于为什么% 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
整型提升举例:
解析:
0xb6 b:10 / 1011 6 / 0110 b6 10110110 整型提升后 高位补符号位1 变成了
11111111 11111111 11111111 10110110
负数 0xb600 10110110 00000000
整型提升后 高位补符号位1
变成了 11111111 11111111 10110110 00000000 负数、
实例1中if语句里面进行了比较,比较也是一种运算,其中的a, b要进行整型提升, 但是c不需要整型提升a, b整型提升之后, 变成了负数, 所以表达式a == Oxb6, b == Oxb600的结果是假, 但是c不发生整型提升, 则表达式 c == Oxb6000000的结果是真, 所程序输出的结果是 : c
解析:
实例2中的, c只要参与表达式运算, 就会发生整型提升
表达式 + c, 就会发生提升, 所以sizeof(+c)是4个字节.
表达式c,表达式(!c)中c实际参与了运算,会发生整型提升,在vs编译器中显示的这个结果实际上是有问题的,在Linux,gcc编译器下结果为4,实际结果也应该为4。
总结:什么时候会参与整型提升?
②算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long doub1e
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。(比如说int类型跟float类型参与运算,先要将int转换为float类型,然后float类型与float类型运算)
int a = 1;
float b = 2.0
b = a + b;
(会先将a转换为float类型 a = 1.00000)
警告 : 但是算术转换要合理,要不然会有一些潜在的问题。
例如:排名高的类型向低位转换的时候会出现精度丢失。
float f = 3.14;int num = f;//隐式转换,会有精度丢失
③操作符的属性
复杂表达式的求值有三个影响的因素。
1.操作符的优先级
2.操作符的结合性
3.是否控制求值顺序。
两个相邻的操作符先执行哪个 ? 取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
C语言操作符优先级、结合性参考表:
常见问题解答:
1、如何记住运算符的15种优先级和结合性?
C语言中运算符种类比较繁多,优先级有15种,结合性有两种。
2、如何记忆两种结合性和15种优先级?下面讲述一种记忆方法。
结合性有两种,一种是自左至右,另一种是自右至左,大部分运算符的结合性是自左至右,只有单目运算符、三目运算符的赋值运算符的结合性自右至左。
优先级有15种。
记忆方法:
记住一个最高的:构造类型的元素或成员以及小括号。
记住一个最低的:逗号运算符。
剩余的是一、二、三、赋值。
意思是单目、双目、三目和赋值运算符。
在诸多运算符中,又分为:
算术、关系、逻辑。
两种位操作运算符中,移位运算符在算术运算符后边,逻辑位运算符在逻辑运算符的前面。再细分如下:
算术运算符分 * , / , % 高于 + , - 。
关系运算符中,〉,〉 = , < , <= 高于 == ,! = 。
逻辑运算符中,除了逻辑求反(!)是单目外,逻辑与( && )高于逻辑或( || )。
逻辑位运算符中,除了逻辑按位求反(~)外,按位与(&)高于按位半加(^),高于按位或( | )。
这样就将15种优先级都记住了,再将记忆方法总结如下:
去掉一个最高的,去掉一个最低的,剩下的是一、二、三、赋值。双目运算符中,顺序为算术、关系和逻辑,移位和逻辑位插入其中。
问题表达式
下面我们来看一些问题表达式:
//表达式的求值部分由操作符的优先级决定。
表达式1
a * b + c * d + e * f;
注释∶代码1在计算的时候,由于比 + 的优先级高,只能保证,的计算是比 + 早,但是优先级并不能决定第三个 * 比第一个 + 早执行。
所以表达式的计算机顺序就可能是︰
a * bc * d
a * b + c * de * f
a * b + c * d + e * f
或者 :
a * bc * de * f
a * b + c * d
a * b + c * d + e * f
表达式2
c + --c;
注释︰同上,操作符的优先级只能决定自减–的运算在 + 的运算的前面,但是我们并没有办法得知, + 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
代码3
非法表达式
#include<stdio.h> int main() { int i = 10; i = i-- - --i * (i = -3) * i++ + ++i; printf("i = %d\n",i); return 0; }
表达式3在不同编译器中测试结果不同,例如:
代码4
#include<stdio.h> int fun() { static int count = 1; return ++count; } int main() { int answer; answer = fun(-fun() * fun()); printf("%d\n",answer);//输出多少? return 0; }
这个代码有没有实际的问题 ?
有问题!
虽然在大多数的编译器上求得结果都是相同的。
但是上述代码answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知︰先算乘法,再算减法。函数的调用先后顺序无法通过操作符的优先级确定。
代码5
#include <stdio.h> int main() { int i = 1; int ret = (++i) +(++i) +(++i); printf("%d\n", ret); printf("%d\n",i); return 0; }
尝试在Linux环境gcc编译器,vS2019环境下都执行,看结果。
Linux运行结果:10 4
VS2019运行结果:12 4
看看同样的代码产生了不同的结果,这是为什么 ?
简单看一下汇编代码.就可以分析清楚.
这段代码中的第一个 + 在执行的时候,第三个是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个+和第三个前置的先后顺序。
总结∶我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。