文章目录
一、逗号表达式
逗号表达式就是由逗号隔开的表达式,如:
exp1, exp2, exp3 ,···expN
可以有n个表达式,中间由逗号隔开,它的特点就是每一个表达式都要进行运算,但是整个逗号表达式的结果是最后一个表达式的结果,如图下代码:
根据它的特点,首先我们要将每个表达式依次进行运算,然后取最后一个表达式的值作为最后的结果,那可能就有同学要问了,为什么要一个一个把前面表达式的值算出来,再取最后表达式的值作为结果,而不是直接算出最后表达式的结果
在我们上面举的例子就进行了很好的说明,后面表达式的b = a +1,而前面的表达式a = b +10,对a的值产生了影响,也就对最后的表达式表达式的b = a +1产生了影响,所以在计算逗号表达式时,我们要依次算出每个表达式的值,然后将最后一个表达式的值作为整个逗号表达式的结果
接着我们来看看上面所示代码的结果,首先第一个表达式是a > b,它的结果为假,也就是0,但是没有用变量存起来什么的,没有什么作用,第二个表达式的结果为12,然后将12赋值给了a,现在a就是12,后面那个表达式a单独存在也没有用,直接看最后一个表达式,b = a +1,表达式的结果为13,然后将13赋值给了b,所以由此得出a的值为12,b的值13,c的值为13,代码运行结果如图:
二、下标访问操作符[]、函数调用操作符()
这两个操作符我们都很熟悉了,这里简单再次介绍一下
1.下标访问操作符[]
它是一个双目操作符,它的两个操作数是数组名和一个索引值(下标),相信大家对它已经很熟了,在数组中已经讲过,这里举个例子:
int arr[10];//创建数组 arr[9] = 10;//使⽤下标访问操作符。 [ ]的两个操作数是arr和9
2.函数调用操作符
接受⼀个或者多个操作数:第⼀个操作数是函数名,剩余的操作数就是传递给函数的参数,但是至少会接受一个操作数,就是函数名,因为函数可能没有参数,比如我们之前在猜数字游戏和扫雷游戏中的菜单,menu()函数,它就没有参数,这里的函数调用操作符()就只有一个操作数函数名menu
再比如我们之前写的函数Add,它可以实现两个整型的相加,如图:
这里的函数调用操作符()就有三个操作数,分别是函数名Add、以及参数2、3,
三、结构成员访问操作符
这里我们简单介绍一下结构体和结构成员访问操作符,后期会专门出一篇博客来讲解结构体
1.结构体
C语⾔已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。
描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等;
描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问题,增加了结构体这种⾃定义的数据类型,让程序员可以⾃⼰创造适合的类型
2.结构的声明
需要使用关键字struct,具体格式如下:
struct 结构体名 { 成员列表 }变量列表; //变量列表可以省略,但是最后的分号不能丢
以上是语法规定的格式,成员列表就是这个结构体有哪些具体的结构体变量,变量列表就是,可以直接在那里创建结构体变量,也可以省略不创建,但是分号不能丢,看了是不是还是有点懵,我们具体举例来看看如何用结构体来描述一个学生:
struct Stu//结构体名 { //成员列表: char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }; //分号不能丢
3.结构体变量的定义和初始化
(1)结构体变量的定义
对结构体变量的定义有两种方式,一种是直接在创建结构体时,在最后直接写出结构体变量,注意分号不能丢,如下:
struct Point { int x; int y; }p1; //声明时定义变量p1
第二种就是像定义变量那样定义结构体变量,只要清楚一点:struct Point相当于就是它的类型,然后就跟普通的变量定义相同了,如下:
struct Point { int x; int y; };//分号不能少 //将struct Point当作 struct Point P2;
(2)结构体变量的初始化
结构体变量的初始化和数组有点相似,用大括号括起来,最简单的方法就是直接按顺序初始化,如:
struct Stu //类型声明 { char name[15];//名字 int age; //年龄 }; struct Stu s1 = {"zhangsan", 20};//初始化
如果我不想按顺序初始化,比如我想先初始化年龄,然后再初始化名字,就要写出成员名,并且在具体成员名前加一个.,然后初始化,如下:
struct Stu { char name[15]; int age; }; struct Stu s1 = {.age=20 , .name="zhangsan"};
当然,也会有特殊情况,比如当结构体当中嵌套了一个结构体,如下:
struct Point { int x; int y; }; struct Node { int data; struct Point p; }
现在结构体Node的成员包含了一个结构体,这种情况怎么初始化呢?这个时候就要使用大括号中的大括号,如下:
struct Point { int x; int y; }; struct Node { int data; struct Point p; }n1 = {10, {4,5}}; struct Node n = {20, {5, 6}};
4.结构成员访问符
(1)结构成员的直接访问
结构体成员的直接访问是通过点操作符(.)访问的,点操作符接受两个操作数,如下所⽰:
我们定义了一个结构体,并且创建了一个结构体变量n,我们如何访问结构体变量n中的成员呢,比如我要打印n中的x,这个时候就要用到点操作符,如下图所示:
(2)结构成员的间接访问
有时候我们得到的不是⼀个结构体变量,⽽是得到了⼀个指向结构体的指针,这个时候我们就可能会使用间接访问的方式来访问结构体,就会使用(->)符号,但是由于涉及到指针,这里只做简单介绍,在后续指针再讲,而结构体这个知识后面也会专门写博客讲解
四、操作符的属性:优先性、结合性
C语⾔的操作符有2个重要的属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序
1.优先级
优先级指的是,如果⼀个表达式包含多个运算符,哪个运算符应该优先执⾏。各种运算符的优先级是不⼀样的,举一个简单的例子:
3 + 4 * 5;
上⾯⽰例中,表达式 3 + 4 * 5 ⾥⾯既有加法运算符( + ),⼜有乘法运算符( * )。由于乘法的优先级⾼于加法,所以会先计算 4 * 5 ,⽽不是先计算 3 + 4
由于运算符的优先级顺序很多,下⾯是部分运算符的优先级顺序(按照优先级从⾼到低排列),建议⼤概记住这些操作符的优先级就⾏,其他操作符在使⽤的时候查看下⾯表格就可以了
- 圆括号( () )
- ⾃增运算符( ++ ),⾃减运算符( – )
- 单⽬运算符( + 和 - )
- 乘法( * ),除法( / )
- 加法( + ),减法( - )
- 关系运算符( < 、 > 等)
- 赋值运算符( = )
参考:https://zh.cppreference.com/w/c/language/operator_precedence
2.结合性
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,如:
5 * 6 / 2;
这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执⾏顺序。而乘除属于左结合,就从左到右计算,⼤部分运算符是左结合(从左到右执⾏),少数运算符是右结合(从右到左执⾏),⽐如赋值运算符( = )下面是运算符的优先级和结合性表,只需要记住最常用的那些,不常用的可以等需要时再查找
参考:https://zh.cppreference.com/w/c/language/operator_precedence
五、整型提升和算术转换
1.整型提升
C语⾔中整型算术运算总是⾄少以缺省(默认)整型类型的精度来进⾏的,为了获得这个精度,表达式中的字符和短整型操作数在使⽤之前被转换为普通整型,这种转换称为整型提升
整型提升的意义:表达式的整型运算要在CPU的相应运算器件内执⾏,CPU内整型运算器(ALU)的操作数的字节⻓度⼀般就是int的字节⻓度,同时也是CPU的通⽤寄存器的⻓度。因此,即使两个char类型的相加,在CPU执⾏时实际上也要先转换为CPU内整型操作数的标准⻓度
通⽤CPU(general-purpose CPU)是难以直接实现两个8⽐特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种⻓度可能⼩于int⻓度的整型值,都必须先转换为int或unsigned int,然后才能送⼊CPU去执⾏运算,如:
/实例1 char a,b,c; ... a = b + c;
这里的b和c都是char类型,只有一个字节,要被提升为普通整型,然后再执⾏加法运算,加法运算完成之后,这个时候会有四个字节,要重新存储进char类型的变量a中,所以结果将被截断,也就是将前面3个字节去掉,然后再存储于a中
如何进行整形提升呢?
1. 有符号整数提升是按照变量的数据类型的符号位来提升的,高位补符号位 2. ⽆符号整数提升,⾼位补0
下面举一个实例说明:
这里的c1,c2变量都是char类型,现在要对它们进行相加,就要先进行整形提升,然后再相加,最后把相加的值赋给c3,由于进行了整型提升,所以最后要进行截断,我们首先算出c1的补码,然后进行整型提升,由于这里c1是有符号整数,填充符号位补齐4个字节,过程如下:
char 1的提升类似,只是符号位变成了0,如下:
随后我们将整型提升得到的结果相加:
我们来看看程序运行结果:
2.算术转换
如果某个操作符的各个操作数属于不同的类型,那么除⾮其中⼀个操作数转换为另⼀个操作数的类型,否则操作就⽆法进⾏。下⾯的层次体系称为寻常算术转换
long double double float unsigned long int long int unsigned int int
如果某个操作数的类型在上⾯这个列表中排名靠后,那么⾸先要转换为另外⼀个操作数的类型后执⾏运算,比如整型和长整型相加,那么就要将整型转换为长整型
六、表达式求值举例
举例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
举例2
int main() { int a = 2; int b = a + --a; printf("%d\n", b); return 0; }
同上,操作符的优先级只能决定⾃减 – 的运算在 + 的运算的前⾯,但是我们并没有办法得知, + 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的
(1)如果先获取+的左操作数,那么左边就是2,右边就是1,加起来就是3
(2)如果先获取+的右操作数,那么右边就是1,此时左边也变成了1,加起来就是2
在不同环境下,不同编译器上,可能结果不同,比如VS2022上,结果就为2,所以可以得出,即使规定了优先级和结合性,也不能够保证一个表达式计算的唯一路径,所以为了保证唯一的计算路径,我们可以将一个复杂表达式拆成多个,这样才没有歧义,比如我想得到(1)的结果,我就可以这样写:
int main() { int a = 2; int b = a; a--; b+=a; printf("%d\n", b); return 0; }
虽然更加麻烦,但是确保了计算的唯一路径
举例3
//表达式3 int main() { int i = 10; i = i-- - --i * ( i = -3 ) * i++ + ++i; printf("i = %d\n", i); return 0; }
这个可以不用算了,实在太复杂了,不仅我们无从下手,连编译器都很懵,在不同的编译器上,结果基本上都不同,如下:
所以一定要规范的写表达式,避免歧义
举例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编译器,VS2013环境下都执⾏,看结果
看看同样的代码产⽣了不同的结果,这是为什么?
简单看⼀下汇编代码,就可以分析清楚,这段代码中的第⼀个 + 在执⾏的时候,第三个++是否执⾏,这个是不确定的,因为依靠操作符的优先级和结合性是⽆法决定第⼀个 + 和第三个前置 ++ 的先后顺序
6.总结
即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯⼀的计算路径,那这个表达式就是存在潜在⻛险的,建议不要写出特别复杂的表达式