首先声明,此博客无照搬抄袭,融入个人理解,我所使用的编译器是VS2022
目录
1.下标访问操作符 []、函数调用操作符 ()
2.结构成员访问操作符
3.操作符的属性:优先级、结合性
4.表达式求值
一、下标访问 []、函数调用 ()
1.1下标引用操作符 []
下标引用操作符是双目操作符,两个操作数分别是:数组名和索引值(下标)
int arr[10]=0; //创建数组 arr[10]; //最常规写法 10[arr]; //第二种写法 []的两个操作数分别是arr和10最常规的写法就是arr[10],但是由于[]是双目操作符,+号也是双目操作符,用于两个整数的加法计算,3+5也可以写成5+3,所以我们类比,arr[10]也可以写成10[arr]
1.2函数调用操作符()
函数调用操作符,接受一个或多个操作数,其中从左到右,第一个操作数在括号外,紧贴着函数调用操作符(),为函数名,其他操作数在函数调用操作符()里面的为传递给函数的参数
#include <stdio.h> void test1() { printf("hehe\n"); } void test2(const char* str) { printf("%s\n", str); } int main() { test1(); //这⾥的()就是作为函数调⽤操作符。 test2("hello bit.");//这⾥的()就是函数调⽤操作符。 return 0; }二、结构成员访问操作符
C语言已经提供了很多内置类型,比如char,int,float,double等,还提供了可以储存多种同一种类型数据的数组,但光有这些内置类型和数组,并不能描述所有事物,比如我想描述一个学生,那就需要char[20]数组类型去储存名字,用char[20]去储存学号,用int age去储存年龄,用char[10]去储存性别,这个时候我们就提供了一种自定义类型,名为结构体类型
结构体类型的每个成员可以是不同类型的变量,能够用来描述现实中结构复杂、属性多样的事物。
2.1结构体类型的声明
struct tag //tag为创建结构体数据类型的标签名字 { member-list; //成员名的列表 }variable-list; //变量名的列表 注意!!!分号不能丢结构体创建一种自定义类型为:(struct + 标签名)整体为一个新的类型
比如要描述一个学生,我们可以创建结构体类型:
struct Stu { char name[20]; //姓名 int age; //年龄 char sex[10]; //性别 char id[20]; //学号 };2.2结构体变量的定义和初始化
结构体变量的定义
变量可以在结构体声明的同时就定义,也可以在声明之后定义
struct Stu { char name[20]; //姓名 int age; //年龄 char sex[10]; //性别 char id[20]; //学号 }s1; //在声明结构体的同时创建变量 struct Stu s2; //在声明后创建变量结构体变量的初始化
结构体可以按顺序一个个初始化,也可以用.成员访问操作符,来访问指定结构体成员名进行赋值
struct Stu s3 = { "Cam",21,"man","20260504" }; //按结构体变量创建的顺序初始化 struct Stu s4 = { .age = 21,.id = "20260504",.name = "Cam" ,.sex = "man" }; //自己指定顺序初始化结构体还可以嵌套初始化:当一个结构体的成员是另一个结构体时,可以对这个结构体成员使用嵌套大括号{}进行赋值,这就是结构体嵌套初始化。
struct Point { int x; int y; }; struct Node { int data; struct Point p; struct Node* next; }n1 = { 10,{4,5},NULL}; //嵌套初始化 struct Node n2 = { 20,{5,6},NULL }; //嵌套初始化 struct Node n3 = { .data = 30,.p = {.y = 10,.x = 20},.next = NULL };//自已指定顺序初始化2.3结构成员访问操作符
2.3.1 结构体成员的直接访问
结构体访问的直接访问是通过(.)操作符来实现的,(.)操作符有两个操作数
使用方式:结构体变量名.结构体成员名
#include <stdio.h> struct Point { int x; int y; }p1 = {.x=10,.y=20}; int main() { printf("%d %d\n", p1.x, p1.y); //(.)结构体直接成员访问操作符 return 0; }运行截图:
2.3.2 结构体成员的间接访问
有时候将结构体变量是指针形式存在时,访问其就需要用到箭头操作符(->)
使用方法:结构体指针->结构体成员
#include <stdio.h> struct Point { int x; int y; }p1 = { .x = 10,.y = 20 }; int main() { struct Point* ptr = &p1; printf("%d %d", ptr->x, ptr->y); return 0; }运行截图:
2.3.3综合举例
#include <stdio.h> #include <string.h> struct Stu { char name[15];//名字 int age; //年龄 }; void print_stu(struct Stu s) { printf("名字:% s 年龄:%d\n", s.name, s.age); } void set_stu(struct Stu* ptr) { strcpy(ptr->name,"王五"); ptr->age = 20; } int main() { struct Stu s1 = { "小明",18 }; print_stu(s1); set_stu(&s1); print_stu(s1); return 0; }运行截图:
我们设计了一个打印结构体成员的函数,和设置成员的函数,因为要保证修改形参的同时也修改到实参,所以我们在设置成员的函数中采用结构体指针来接收要修改结构体变量的地址,其中即用到了直接访问操作符(.),又用到了间接访问操作符(->)
但我们要注意新手容易混淆错误的一个点:
void set_stu(struct Stu *ptr) { ptr->name = "王五"; ptr->age = 20; }我们容易直接修改name的值,但我们要注意,name是字符串数组的首元素的地址,每次运行都是个常量地址,常量地址是无法被进行修改的,所以我们要借助strcpy库函数
strcpy库函数的工作原理是:在ptr->name拿到了name首元素的地址后,将字符串"王五"中的每个字符,逐个依次拷贝到数组对应的内存空间中,数组的访问位置会自动向后移动,最终完成整个字符串的覆盖修改。
比如举个例子:
#include <stdio.h> int main() { char ch[10] = "hello"; ch = "bit"; return 0; }这样编译器会报错:
表示ch是不能被修改的左值
这时我们就要借助strcpy库函数:
#include <stdio.h> #include <string.h> int main() { char ch[10] = "hello"; printf("修改前:%s\n", ch); strcpy(ch, "bit"); printf("修改后:%s\n", ch); return 0; }运行截图:
我们来调试看看strcpy前后ch字符串数组中元素的改变
修改前:
修改后:
可以看见,我们把bit'\0'四个字符逐个复制到指定的字符串数组ch中,但是由于bit'\0'是四个字符,而原来hello'\0'是六个字符,所以只复制覆盖了ch[0~3],ch[4]以后仍然没变,但由于%s的特性,只打印遇到的第一个'\0'之前的字符串,所以打印并不会出错
三、操作符的属性:优先级、结合性
优先级和结合性决定了表达式求值的运算过程,计算顺序
3.1优先级
优先级只决定:绑定谁,不决定什么时候算!
如果一个表达式中含有多个操作符,那么这些操作符的使用顺序会有一个优先顺序
比如a=3+5*8,优先级:(*)>(+)>(=),因为*的优先级最高,所以5和8被*绑定,5*8=40,然后是+号,将3和40绑定,3+40=43,最后才是赋值运算符,所以到最后只剩a=43,得到结果
3.2结合性
当优先级相同的操作符时,我们就要考虑结合性,考虑从左结合还是从右结合,一般的操作符都是从左往右结合,少数操作符如(=)赋值操作符是从右往左结合
比如10/2*3,优先级:(/)=(*),因为它们优先级相同,所以考虑结合性,乘除运算符是左结合操作符,所以计算顺序从左到右,先计算10/2=5,再计算5*3=15
下面给出一些常用的操作符优先级顺序,从上到下,优先级由高变低
括号 ()最高,括号永远最先算
单目运算符
!逻辑非、++、--、&取地址、*解引用算术运算符先乘除取模:
* / %后加减:+ -移位运算符
<<>>关系运算符
< <= > >=高于== !=位运算
&按位与 →^按位异或 →|按位或逻辑运算
&&逻辑与 高于||逻辑或三目运算符
表达式1 ? 表达式2 : 表达式3赋值运算符
= += -= *= /= %=这些最低
口诀!!!
括号第一,单目老二;乘除模先于加减,移位紧跟后边;关系再到位逻辑;三目在前,赋值垫底。
我们可以使用()来改变操作符的优先级,达到我们想要的效果
赋值操作符(=)还可以进行连续赋值,比如:a = b = c + 5;
#include <stdio.h> int main() { int b = 0; int a = 0; int c = 3; a = b = c + 5; printf("%d %d %d", a, b, c); return 0; }运行截图:
四、表达式求值
4.1整型提升
计算机运算时总是以整型为最小精度单位进行运算的,如果是short或char类型的数字进行运算,就要先进行整型提升,转化为int整型的精度,再进行运算
整型提升:只针对char / short
浮点型不做整型提升:直接走浮点转换规则
只要有float / double参与运算:整型 (int/long) 会自动转成浮点类型,再运算(表达式里只要有浮点,最终按 double 精度算)
所以具体是怎么进行整型提升的呢?下面我们来看例子
整型提升规则:
- 有符号整数提升是按照变量的数据类型的符号位来提升的
- 无符号整数提升,高位补 0
char a=-1; 因为char类型只能存1个字节,也就是8个bit的二进制,也就是11111111 我们整体提升后,高位补符号位,补齐到跟整型类型的精度一样的32位二进制数 也就是1111 1111 1111 1111 1111 1111 1111 1111 再比如正数char b=3; 一样的补码中存8个bit位,也就是00000011 整型提升后,和上面一样的规则 也就是0000 0000 0000 0000 0000 0000 0000 0011 如果我们那a和b相加,存到char c中去,计算机会拿整型提升之后的二进制补码进行相加计算 1111 1111 1111 1111 1111 1111 1111 1111 + 0000 0000 0000 0000 0000 0000 0000 0011 结果为1 0000 0000 0000 0000 0000 0000 0000 0010,最左边多一个进位的1,直接舍去 最后结果就为00000000 00000000 00000000 00000010 因为我们是用char类型的变量去接收这个结果,有效位为8个bit位 所以我们只取最后最低位的8位二进制数00000010 换算成十进制就是2 所以a+b结果为24.2算术转换
当操作符的操作数分别是不同类型的时候,我们就要进行算术转换
算术转换规则:不同类型一起运算时,统一转成「精度更高、范围更大」的类型,再计算。
char → short → int → long → long long → float → double
比如float a=0.0f;int b=0; a+b时就要进行算术转换 先把b转换成float类型,再进行计算4.3问题表达式解析
(1)表达式1
a*b + c*d + e*f我们首先已知*的优先级大于+,所以会将a和b绑定,c和d绑定,e和f绑定,但是加法的操作顺序,程序的计算流程,我们并不能知晓。第一个+号和第三个*号谁先算,我们无法知晓,导致这串表达式的计算流程不止一种,比如先算(a*b + c*d)先加起来,再算+e*f,还是,先把a*b,c*d,e*f三个都分别算出来,最后再相加,所以就有很多种运算路径,就会产生歧义,危险代码
(2)表达式2
c + --c;我们知道--的优先级大于+,但优先级只决定:--绑定谁,不决定什么时候算!所以我们不知道左边的操作数c是什么时候求值,就有两种运算路径,假设c=7。第一种可能:7+6;第二种可能:6+6;就会产生歧义,危险代码
(3)表达式3
#include <stdio.h> int main() { int i = 10; i = i-- - --i * (i = -3) * i++ + ++i; printf("i = %d\n", i); return 0; }这个的运算路径就更多了,每个编译器底下的运算流程都不一样,在一个表达式里多次既读又改同一个变量i,标准直接判定位未定义行为。运算顺序想怎样都行
运行路径:其一:9-8*(-3)*9+10=235 其二:9-8*(-3)*2+1=58
不同编译器下的结果:
4.4总结
优先级只是决定谁和谁绑定,结合性只是决定同级的操作符运算顺序,还有很多表达式靠这两个操作符的属性并不能确定唯一路径的计算顺序,这样写出来的就是危险代码,所以建议写清楚简单逻辑清晰的表达式,不要写嵌套过深、自增自减扎堆、中途修改变量的复杂表达式,避免结果混乱、逻辑出错。