目录
我们接着上节课的来讲
七.关系操作符
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
这些关系运算符比较简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱。
警告: 在编程的过程中== 和=不小心写错,导致的错误。
八.逻辑操作符
&& 并且的意思,同时为真的时候是真,其他是假
|| 或者的意思,同时为假的时候是假,其他是真
位操作符是二进制中计算的,这俩就是用于判断
区分逻辑与和按位与
区分逻辑或和按位或
1 & 2----->0
1 && 2---->1
1 | 2----->3
1 || 2---- > 1
1.简单用法
int main()
{
int a = 0;
int b = 0;
if (a || b)
{
printf("haha\n");
}
}
判断闰年,多个判断
1. 能被4整除,并且不能被100整除
2. 能被400整除
int main()
{
int y = 2048;
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
{
printf("Yes\n");
}
return 0;
}
2.360面试题目
&& 操作符,左边为假,右边无需计算
|| 操作符,左边为真,右边无需计算
对比下面四个,理解好什么时候短路,什么时候继续往下 ,理解好逻辑操作符的原理
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
//打印出来是1 2 3 4 因为我们的a是0,而且最开始还没++,所以直接判断为假了,后面就不计算了,直接短路了然后a++,所以是1234
int i = 0, a = 1, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
//打印出来是2 3 3 5 因为我们a不是0,前面是真,我们后面继续去算,是真的,i的值就是1,后面都去计算
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ || ++b || d++;
//打印出来是1 3 3 4 因为我们的a是0,最开始a先使用再加加,所以第一个a用好了,然后第二个b的时候,是真的了,所以后面就短路了,就算到b就行了
int i = 0, a = 1, b = 2, c = 3, d = 4;
i = a++ || ++b || d++;
//打印出来是2 2 3 4 因为我们a就是1,先使用再加加,但是前面已经是真的了,后面不用看也是真的,所以后面都是真的了,就不计算了,直接短路了
printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
return 0;
}
九.条件操作符
exp1 ? exp2 : exp3
1为真,2计算,3不计算,2是结果
1为真,2不计算,3计算,3是结果
int main()
{
int a = 10;
int b = 20;
int m = 0;
if (a > b)
m = a;
else
m = b;
//上面判断直接用条件操作符来写
m = (a>b?a:b);
return 0;
}
再看一个例子
if (a > 5)
b = 3;
else
b = -3;
//上面的改写就是下面的
int main()
{
int a = 0;
int b = (a > 5 ? 3 : -3);
return 0;
}
十.逗号表达式
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
//即使第一个是假的,也不影响后面的计算,最后一个是结果
printf("c=%d\n", c);
return 0;
}
输出结果是13,从前到后,尽管第一个判断不成立
再看一个改写为逗号表达式
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
a = get_val();
count_val(a);
}
//如果使用逗号表达式,改写:
while (a = get_val(), count_val(a), a>0)
{
//业务处理
}
十一.下标引用、函数调用和结构成员
1.下标引用
操作数是一个数组名+一个索引值
int main()
{
int arr[10] = { 1,2,3,4,5 };
printf("%d\n", arr[4]);//[] - 下标引用操作符,操作数是:arr , 4
//3 + 4;
return 0;
}
2.函数调用
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
至少有一个,当函数没有参数的时候
int main()
{
int len = strlen("abcdef");//()就是函数调用操作符,操作数:strlen, "abcdef"
return 0;
}
3.结构成员
char int short long long long float double
内置类型——自定义类型——结构体——枚举——联合体
我们来看结构体,生活中有些对象要被描述的话,不能简单的使用单个内置类型
书:书名,作者,出版社,定价,...
学生:名字,年龄,学号..
所以我们先创建一个类型
struct Book
{
char name[20];
char author[30];
int price;
};
我们主函数里面,我们上面创建好了类型,我们这里面就创建变量,然后初始化
然后我们打印的时候,就需要引用我们创建的结构体,变量名.成员名来引用
int main()
{
struct Book b1 = {"鹏哥C语言", "鹏哥", 66};//创建变量初始化
struct Book b2 = { "杭哥C++", "比特杭哥", 88 };//创建变量初始化
printf("《%s》 %s %d\n", b1.name, b1.author, b1.price);
//然后我们打印,引用的时候就是变量名.成员名,也就是我们的结构体.成员名
printf("《%s》 %s %d\n", b2.name, b2.author, b2.price);
//结构体变量.成员名
print1(&b1);
//我们用函数调用指针接收也是一样的
return 0;
}
我们用函数打印也是一的,我们上面取了b1的地址,我们传到函数里面用指针取接收
也可以用变量名.成员名,注意指针需要解引用,*p就是b1
或者直接用地址来,结构体指针->成员名来引用,也是一样的
void print1(struct Book* p)
{
printf("%s %s %d\n", (*p).name, (*p).author, (*p).price);
//用指针接收的时候,*p就是我们指针解引用,也就是b1,然后加上 .成员名就能引用了,不加*是b1的地址,我们加上*解引用才是b1
printf("%s %s %d\n", p->name, p->author, p->price);
//或者我们直接用结构体指针来写也是一样的
//结构体指针->成员名
}
我们再来看一个例子,加深印象,好好分析
#include <stdio.h>
struct Stu
{
char name[10];
int age;
char sex[5];
double score;
};
最开始是一样的先创建类型,注意格式,我们来看两个函数我拆来来看。
下面这个打印出来不能改为18,因为要改变里面的值,我们不能用传值,只是一份临时拷贝,不能达到改变值得效果,需要传址来进行,注意结构体的用法,要记得创建变量,用 . 来引用
void set_age1(struct Stu stu)
{
stu.age = 18;
}
//第一个函数不能改变值,因为是地址,是传值,就改变不了,这里需要地址来改变值
int main()
{
struct Stu stu; //创建变量
stu.age = 20;//初始化,结构成员访问
set_age1(stu);
printf("%d\n", stu.age);//这里打印还是20
return 0;
}
这一个就打印出来是18,我们创建结构体指针,然后用变量名->成员名,来引用,函数里面用结构体指针来接受,因为是地址,地址直接取改值,也可以用*解引用来改,但是必须传址
void set_age2(struct Stu* pStu)
{
pStu->age = 18;//结构成员访问
}
//第二个函数可以,接收的是地址,后面也是指针,就可以改变
int main()
{
struct Stu* pStu = &stu;//创建指针变量
pStu->age = 20;//直接用指针来初始化,结构成员访问
set_age2(pStu);
printf("%d\n", stu.age);//这个打印就是18
return 0;
}
十二.表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。 同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
1.隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的。 为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长 度。 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算。
char short int long ...
1 2 4 ...
对应里面放的字节,一个字节八个比特位,本来是char就八个比特位,但是放的整型,我们只要后八个,然后计算的时候,就整型提升,然后计算,然后再变成八个
整型提升,提升的是是符号位,就是补上来的是符号位的值
int main()
{
//char --> signed char
char a = 3;
char b = 127;
char c = a + b;
printf("%d\n", c);
return 0;
}
我们一个一个来看
char a = 3;
//截断
//00000000000000000000000000000011
//00000011 - a
char b = 127;
//截断
//00000000000000000000000001111111
//01111111 - b
进行整型提升,补上符号位的值,然后进行计算
char c = a + b;
//00000011
//01111111
//整型提升
//00000000000000000000000000000011
//00000000000000000000000001111111
//00000000000000000000000010000010
//10000010 - c 然后也只要后八位
printf("%d\n", c);
//%d 是打印十进制的整数,再次整型提升,然后求原码打印
//11111111111111111111111110000010 - 补码
//11111111111111111111111110000001
//10000000000000000000000001111110 - 原码
//-126
只要关乎计算表达求值的操作,类型和变量不符合的,要进行整型提升来解决,记得打印的是原码,我们整型提升的也是我们的符号位的值
char - 有符号的char的取值范围是:-128~127
无符号的char的取值范围是:0~255
再来看一个例子
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6)//10110110
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}
实例1中的a, b要进行整形提升, 但是c不需要整形提升,a, b整形提升之后, 变成了负数, 所以表达式 a == 0xb6, b == 0xb600 的结果是假, 但是c不发生整形提升, 则表达式 c == 0xb6000000 的结果是真
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;
}
实例2中的, c只要参与表达式运算, 就会发生整形提升, 表达式 + c, 就会发生提升, 所以 sizeof(+c) 是4个字节,表达式 - c 也会发生整形提升, 所以 sizeof(-c) 是4个字节, 但是 sizeof(c), 就是1个字节.
2.算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。
下面的层次体系称为寻常算术转换。如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。跟上面整型提升就是一个意思。
>= int 的时候,算术转换
<= int 的时候,整型转换
long double
double
float
unsigned long int
long int
unsigned int
int
向上转换的,只要转换就往上走
警告:但是算术转换要合理,要不然会有一些潜在的问题。
3.操作符的属性
复杂表达式的求值有三个影响的因素。
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
首先确定优先级,相邻操作符按照优先级高低计算,优先级相同的情况下,结合性才起作用拿不准的就加上括号,或者拆开去写
例子1
int main()
{
int a = 1;
int b = 2;
int c = 4;
int d = a + b + c;
//int d = a * 4 + b / 3 + c;
return 0;
}
注释:代码1在计算的时候,由于* 比 + 的优先级高,只能保证, * 的计算是比 + 早,但是优先级并不能决定第三个 * 比第一个 + 早执行。
例子2
c + --c;
注释:同上,操作符的优先级只能决定自减--的运算在 + 的运算的前面,但是我们并没有办法得知, + 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
例子3
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(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。函数的调用先后顺序无法通过操作符的优先级确定。
例子4
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
简单看一下汇编代码.就可以分析清楚.这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。我们看vs里和vs code里也是不一样的值
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
结束语
我们把操作符讲完了,注意里面的细节还有要特别记住的地方。