目录
操作符分类
1.算数操作符
2.移位操作符(只适用于整数范围)
(1)引入
(2)左移操作符<<
(2)右移操作符>>
3.位操作符
4.赋值操作符
复合赋值符
5.单目操作符
5.1单目操作符介绍
5.2单目操作符的一些常见场景
5.3sizeof和数组练习题
6.关系操作符
7.逻辑操作符
8.条件操作符
9.逗号表达式
10.下标引用、函数调用、结构成员
10.1下标引用操作符[ ],操作数:一个数组名+一个索引值
10.2函数调用操作符()
10.3成员访问操作符
编辑 11.表达式求值
11.1隐式类型转换
11.2算术转换
11.3操作符属性
操作符分类
1.算数操作符
算术操作符主要有:+(加)、-(减)、*(乘)、/(除)、%(取余)。
值得注意的几点有:(1)+、-、*、/都可用于整数与浮点数的运算,%又称整数取余,只适用于整数的运算,且返回的是整除之后的余数(求余数的操作符)。
(2)当 +、-、*、/这几个操作数在处理整数与浮点数的混合运算时,运算结果为浮点类型。
7 / 3的本质是整数/整数,发生的是整数除法,所以会先产生一个整数2,然后才会将2赋值给m,n。
7 / 3.0的本质是整数/浮点数,它会先产生一个浮点数2.333333,然后赋值给整型变量m的时候把小数部分截断了,但是浮点数变量n是能直接接收的。
2.移位操作符(只适用于整数范围)
(1)引入
整数的二进制表示形式有:原码、反码、补码。对于一个整型变量是4字节 = 32bit位。
原码:按照数值的正负,直接写出的二进制序列(整型变量就是32个bit位)就是原码。
对于有符号整数来说,第一位(最高位)是符号位,符号位为1表示是负数,符号位为0表示正数。
对于无符号整数来说,没有符号位,所有位都是有效位。
eg:10
原码->00000000 00000000 00000000 00001010从最低位到最高位(从右到左),第n位的位权为2^(n-1),但不包括符号位。
eg:-10
原码->10000000 00000000 00000000 00001010
对于正的整数来说,原码反码补码三者一致,而对于负的整数来说,反码是原码的符号位不变,其他位按位取反;补码是反码+1。在内存中存一个整数的时候,存的是它的补码二进制序列,同时,在计算的时候,也是用补码来计算。
(2)左移操作符<<
7的二进制序列是00000000 00000000 00000000 00000111,移位之后变成00000000 00000000 00000000 00001110,最高位要丢弃,最低位后面补0,也就是14,然后赋值给m。注意:n是不变的,还是7,它只是参与运算了。向左移动一位的时候和乘以2的效果是一样的。
n = -7->
原码:10000000 00000000 00000000 00000111
反码:11111111 11111111 11111111 11111000
补码:11111111 11111111 11111111 11111001(计算机拿来计算)
左移:11111111 11111111 11111111 11110010(还是补码)
补码求反码 - 1:11111111 11111111 11111111 11110001
反码求原码:10000000 00000000 00000000 00001110(展示)
左移的特点就是左边丢弃,右边补0。
(2)右移操作符>>
右移操作符分为两种:一种右移叫算术右移,另一种叫逻辑右移。
绝大多数的编译器采用的都是算术右移,但具体得根据编译器来判断。算术右移有除2的效果。
对于移位操作符,不要移动负数位,这个是标准未定义的。eg:10 >> -1//error
3.位操作符
位操作符有:
& 按位与
| 按位或
^ 按位异或
注:他们的操作数必须是整数。
eg:
int a = 3;
int b = -5
int c = a & b;
a的补码:00000000 00000000 00000000 00000011
b的原码:10000000 00000000 00000000 00000101
b的反码:11111111 11111111 11111111 11111010
b的补码:11111111 11111111 11111111 11111011
计算机计算过程拿补码来进行计算
所以a & b ->
00000000 00000000 00000000 00000011
11111111 11111111 11111111 11111011
=>00000000 00000000 00000000 00000011
因为符号位为0->正数,所以原码也是这个即3
按位或和按位异或都是这样计算的。就不举例计算了,需要知道在位操作上,1或上任何数=1,0或上任何数=任何数 异或就是相同为0,不同为1。异或有两条常用的数学性质:a ^ a = 0;0 ^ x = x。
不创建临时变量(第三个变量),实现两个数的交换
这是创建临时变量的做法。 可以用加减来实现不创建临时变量的做法->
这种方法会有溢出的风险,因为a + b可能超出int的范围 。
这种用位运算(异或操作)来实现两个数的交换,且不会有溢出风险。 注:异或操作是支持交换律的。但是这种方法只适用于整型,浮点数不支持异或操作。
练习:求一个整数存储在内存中的二进制中1的个数->
这个题的意思就是找出这个数在内存中的二进制序列中1的个数,可以使用按位与&1操作这个二进制序列上的每一位,如果是1,计数器++->
#include <stdio.h>
int main()
{
int num = 10;
int count = 0;//统计1的个数
for (int i = 0; i < 32; i++)
{
num = num >> i;
if ((num & 1) == 1)
count++;
}
printf("%d\n", count);
return 0;
}
当然也可以让1去左移31位去&num,判断num的二进制序列上的每一位是不是1,其实还有一种思路,就是在上述基础上判断那里可以改成num是否是奇数,因为最低位是1,表示它是个奇数,其他位的位权全是2的倍数。->
#include <stdio.h>
int main()
{
int num = 10;
int count = 0;//统计1的个数
while (num)
{
if (num % 2 == 1)
count++;
num = num / 2;
}
printf("%d\n", count);
return 0;
}
注:一定是/2,因为当num在内存中的二进制序列表示负数的时候,符号位也要算上去,如果是>>此时,编译器默认为是算术右移,会一直将符号位置1,导致陷入死循环->
总之第二种方法的在适用范围很小,很容易出错,慎用。
4.赋值操作符
#include <stdio.h>
int main()
{
int a = 20;//不是赋值,初始化
a = 30;//赋值
return 0;
}
赋值是变量存在的情况下,修改变量的值,赋值是能够连续赋值的->
int a = 30;
int x = 20;
int y = 10;
a = x = y * 5;
在连续赋值那块,是从右向左依次赋值,等效于下面->
x = y * 5;
a = x;
下面这种操作,逻辑较清晰,且易于调试。
复合赋值符
+= | -= | *= | /= | %= |
>>= | <<= | &= | |= | ^= |
这些运算符都可以拆分为 x = x () y;就是x和y运算后的结果存入x。
5.单目操作符
5.1单目操作符介绍
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置--,后置--
++ 前置++,后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
单目操作符的意思就是只有一个操作数。
5.2单目操作符的一些常见场景
#include <stdio.h>
int main()
{
int flag = 0;
int x = !flag;
if (!flag)
{
printf("%d\n", x);
printf("%d\n", !100);
}
return 0;
}
!就是将逻辑真(非0),变为逻辑假(0),逻辑假变为逻辑真。
#include <stdio.h>
int main()
{
int a = 50;
int b = +a;
printf("%d\n", b);
int c = -a;
printf("%d\n", c);
return 0;
}
正值+操作符本质上是没什么意义的,并不会改变操作数的值。
#include <stdio.h>
int main()
{
int a = 20;
int* p = &a;
int arr[20];
int (*pa)[10] = &arr;
return 0;
}
注:&arr是数组的地址,我们需要用数组指针来接收它的地址,比如整型变量a的地址用整型指针来接收a的地址,这里关于指针、地址后面会细讲,这里不必太关心细节。
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;
printf("%d\n", *p);
return 0;
}
*p的意思就是对p进行解引用,也就是通过p存放的地址,找到p指向的对象->例如变量a在内存中的地址是0x3f3f3f3f,这个地址存的数据就是a的值10,然后用指针变量p(p也是个变量,也有自己的地址)来存储变量a的地址0x3f3f3f3f,那我*p就是找到这块地址对应的值,也就是a。*p和a是同一块空间,p是另一块空间,这块空间存的是a那块空间的地址。
但其实在新版的编译器下,sizeof的返回值类型是size_t(无符号整型),对此类型的数据进行打印,可以使用%zd->
老版本的编译器没有size_t类型,所以没有%zd的打印格式,可以使用%d或者%u进行打印。sizeof a(这一点也可以说明sizeof是操作符而不是函数,因为函数调用需要函数调用操作符())和sizeof(a)都是可以的,但对于数据类型应该是sizeof(数据类型)。
sizeof 数组名是计算整个数组的大小,因为数组元素是int类型的,一个元素占4字节,那十个元素就是占四十字节。sizeof是在计算类型创建变量或者变量的大小,单位是字节。
#include <stdio.h>
int main()
{
int a = 0;
printf("%d\n", ~a);
return 0;
}
记得转换为二进制序列再操作,这是计算机计算的规则,换算回来就是-1。
那我们如何再换算回来呢?->
++自增操作符和--自减操作符->
这种++在a的前面称为前置++,在后面称为后置++;在前面还是在后面对操作数a是没有影响的,都是使其+1,但对于“别人”是有影响的->
前置++的计算口诀是先让操作数a+1,再使用操作数a;后置++的计算口诀是先使用操作数a,再让操作数a+1。++推广到--是类似的。
#include <stdio.h>
int main()
{
int a = 1;
int b = (++a) + (++a) + (++a);
printf("%d\n", b);
return 0;
}
这种代码是无意义的,在不同编译器、不同平台、不同环境,算出来的结果是不一样的。工程中应当尽量减少这种未定义行为!在vs2022下运行的结果是12,但是我换一个gcc环境->
这里的输出答案是10。同一段代码,在gcc环境和vs下的输出结果都不一样,编译器的输出都不一样,那人还怎么算?
我们知道这样初始化,编译器会将3.14识别为double类型,但a是个整型类型,所以编译器会弹出警告,要消除这个警告,可以用double的类型变量来接受这个3.14,或者是将3.14进行强制类型转换->
强制类型转换是到万不得已的情况才使用,毕竟“强扭的瓜不甜”。
5.3sizeof和数组练习题
#include <stdio.h>
void test1(int arr[])
{
printf("%d\n", sizeof(arr));//(2)
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch));//(4)
}
int main()
{
int arr[10] = {0};
char ch[10] = {0};
printf("%d\n", sizeof(arr));//(1)
printf("%d\n", sizeof(ch));//(3)
test1(arr);
test2(ch);
return 0;
}
问:
(1)、(2)两个地方分别输出多少?
(3)、(4)两个地方分别输出多少?
(1)、(3) 很明显是40和10,那(2)和(4)呢?
我这边是x64环境,输出的是8和8,因为数组传参的时候,传的是数组首元素的地址,是个指针类型的变量,即使形参写成那个样子,但那并不是本质,只是语法允许这样写,所以传过去的是个指针,当然在x64环境下就是8字节,在x86环境下就是4字节(这里就不验证了)。
只是语法上形参允许这样写,但底层是指针做形参,并不是用数组做形参,因为当数组很大的时候传参,形参是实参的一份拷贝,那这个效率就太低了,C语言是一门追求效率的语言,传地址过去,一样能找到那块空间。
6.关系操作符
关系操作符唯一需要注意的就是不要把赋值操作符=和关系操作符==搞混了。
>
>=
<
<=
!= 测试不等于
== 测试相等
7.逻辑操作符
&& 逻辑与 并且
|| 逻辑或 或者
#include <stdio.h>
int main()
{
int month = 0;
scanf("%d", &month);
if (month >= 3 && month <= 5)
printf("春季\n");
else if (month == 12 || month == 1 || month == 2)
printf("冬季\n");
return 0;
}
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\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
//程序输出的结果是什么?
i = a++ && ...我们知道a++表达式的值是0,然后a += 1,->i = 0 && ...我们知道0 && ...等于0,后面的表达式的值已经不重要了,因此这个逻辑表达式&&会发生逻辑短路,后面表达式的值不会再算下去,直接输出i = 0。这种逻辑运算符的这种特性叫做逻辑短路。
我们现在换成||逻辑运算符->
i = a++ || ++b || d++,a++表达式的值是0,然后a += 1;->i = 0 || ++b || ...,因为0 || ...的结果还可能是真,所以后面会继续计算,++b表达式的值是3,b也已经变成了3,0 || 3 -> 1所以变成1 || ...,此时不管后面的表达式是真还是假,整体一定是真,||左边遇1则整体为1(逻辑真),所以后面的表达式已经没有必要算下去了,即发生了逻辑短路。
所以,&&左边遇0(假)即发生逻辑短路,||左边遇真(非0)也会发生逻辑短路。或许我们可以利用这个特性,将优先级高的放左边,优先级低的放右边,然后就会发生一些不可思议的现象。
8.条件操作符
exp1 ? exp2 : exp3 其中expi是表达式,条件操作符是唯一的一个三目操作符。如果表达式1exp1的结果为真,那么就计算表达式2,不计算表达式3,表达式2的结果就是整个表达式的结果;如果表达式1exp1的结果为假,那就计算表达式3,不计算表达式2,表达式3的结果就是整个表达式的结果。
eg:计算a和b的较大值->
转换成条件表达式->
这两种写法是等效的。
9.逗号表达式
exp1,exp2,exp3,...expn。逗号表达式就是用逗号隔开的表达式。逗号表达式从,从左至右依次进行。整个表达式的结果是最后一个表达式的结果。
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);
c是多少?
从左至右依次计算过去,最后一个表达式的结果为整个表达式的结果。
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
a = get_val();
count_val(a);
}
因为上面和循环内部是重复的,所以可以利用逗号表达式的特点写成->
while (a = get_val(), count_val(a), a>0)
{
//业务处理
}
10.下标引用、函数调用、结构成员
10.1下标引用操作符[ ],操作数:一个数组名+一个索引值
int arr[10];//创建数组
arr[9] = 10;//实用下标引用操作符。
[ ]的两个操作数是arr和9。
数组的下标也叫数组元素的索引。
10.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;
}
对于函数调用操作符来说,有两个操作数,一个是函数名,另一个是参数。对于一个函数调用操作符来说,它至少要有一个操作数——函数名。
10.3成员访问操作符
. 结构体 .成员名-> 结构体指针 ->成员名
#include <stdio.h>
struct Book
{
char name[20];
int price;
};
int main()
{
struct Book book1 = { "C语言学习", 100 };
printf("%s %d\n", book1.name, book1.price);
return 0;
}
*pb就是book1这块空间,再.访问变量的成员,C语言语法支持直接pb->成员。二者是完全等价的。
11.表达式求值
表达式求值的顺序一部分是右操作符的优先级和结合性决定。同样,有些表达式的操作数在求值过程中可能需要转换成其他类型。
11.1隐式类型转换
C语言的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
1.整形提升
2.算术转换
整型提升的意义:
表达式的整型运算要在 CPU 的相应运算器件内执行, CPU 内整型运算器 (ALU) 的操作数的字节长度一般就是int 的字节长度,同时也是 CPU 的通用寄存器的长度。因此,即使两个 char 类型的相加,在 CPU 执行时实际上也要先转换为 CPU 内整型操作数的标准长度。通用 CPU ( general-purpose CPU )是难以直接实现两个 8 比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int 长度的整型值,都必须先转换为int 或 unsigned int ,然后才能送入 CPU 去执行运算。
int main()
{
char a = 5;
//5是整型:
//00000000 00000000 00000000 00000101
//a只能接受8位
//00000101
char b = 126;
//126是整型:
//00000000 00000000 00000000 01111110
//b只能接受8位
//01111110
char c = a + b;
//a、b发生整型提升
//a:00000000 00000000 00000000 00000101
//b:00000000 00000000 00000000 01111110
//相加:00000000 00000000 00000000 10000011
//c:10000011
//打印的时候,又整型提升:11111111 11111111 11111111 10000011
//补码转换为原码打印:10000000 00000000 00000000 01111101 ——-125
printf("%d\n", c);
return 0;
}
b和c的值被提升为普通整型,然后在执行加法运算。加法运算完成后,结果被截断,然后在存储在a中。int——signed int(默认)、char到底是signed char还是unsigned char是不确定的,C语言标准没有明确规定,是取决于编译器的,在当前使用的vs2022下,char默认为signed char。整型提升的时候,如果是有符号类型,记得在高位全部加0/1(原先的符号位是0就加0是1就加1)。CPU就是这样算的,但是我们直接5和126的补码直接开算也是对的。
//实例1
#include <stdio.h>
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if(a==0xb6)
printf("a");
if(b==0xb600)
printf("b");
if(c==0xb6000000)
printf("c");
return 0;
}
在判断a==0xb6的时候,a会发生整型提升,由于符号位是1,所以高位全部补1,可以想简单点就是,0xb6赋值给a的时候,a是个负数,0xb6是实打实的正数(整型),同理b也是如此。
//实例2
#include <stdio.h>
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
11.2算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系转换称为寻常算术转换。(小范围->大范围)
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个表格中排名较低,那么首先要转换为另一个操作数的类型后执行运算。注意: 数据类型在整型大小以下,才会发生整型提升,在整型大小及以上才会发生算术转换,且转换要合理,要不然会存在一些潜在问题。
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
11.3操作符属性
复杂表达式的求值有三个影响的因素——>
1.操作符的优先级
2.操作符的结合性
3.是否控制求值顺序
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。控制求值顺序比如逻辑操作符的逻辑短路和条件操作符的取其一求值。
操作符优先级:
操作
符
|
描述
|
用法示例
|
结果类
型
|
结合
性
|
是否控制求值
顺序
|
()
|
聚组
|
(表达式)
|
与表达
式同
|
N/A
|
否
|
()
|
函数调用
|
rexp
(
rexp
,
...,rexp
)
|
rexp
|
L-R
|
否
|
[ ]
|
下标引用
|
rexp[rexp]
|
lexp
|
L-R
|
否
|
.
|
访问结构成员
|
lexp.member_name
|
lexp
|
L-R
| 否 |
->
|
访问结构指针成员
|
rexp->member_name
|
lexp
|
L-R
| 否 |
++
|
后缀自增
|
lexp ++
|
rexp
|
L-R
| 否 |
--
|
后缀自减
|
lexp --
|
rexp
|
L-R
| 否 |
!
|
逻辑反
|
! rexp
|
rexp
|
R-L
| 否 |
~
|
按位取反
|
~ rexp
|
rexp
|
R-L
| 否 |
+
|
单目,表示正值
|
+ rexp
|
rexp
|
R-L
| 否 |
-
|
单目,表示负值
|
- rexp
|
rexp
|
R-L
| 否 |
++
|
前缀自增
|
++ lexp
|
rexp
|
R-L
| 否 |
--
|
前缀自减
|
-- lexp
| rexp |
R-L
|
否
|
* | 间接访问 |
* rexp
|
lexp
|
R-L
|
否
|
&
|
取地址
| & lexp | rexp |
R-L
|
否
|
sizeof
|
取其长度,以字节
表示
|
sizeof rexp sizeof(类型)
| rexp |
R-L
|
否
|
(
类
型)
|
类型转换
|
(
类型
) rexp
| rexp |
R-L
| 否 |
*
|
乘法
| rexp * rexp |
rexp
|
R-L
| 否 |
/
|
除法
|
rexp / rexp
|
rexp
|
L-R
|
否
|
%
|
整数取余
|
rexp % rexp
|
rexp
|
L-R
|
否
|
+
|
加法
|
rexp + rexp
|
rexp
|
L-R
|
否
|
-
|
减法
|
rexp - rexp
|
rexp
|
L-R
|
否
|
<<
|
左移位
|
rexp << rexp
|
rexp
|
L-R
|
否
|
>>
|
右移位
|
rexp >> rexp
|
rexp
|
L-R
|
否
|
>
|
大于
|
rexp > rexp
|
rexp
|
L-R
|
否
|
>=
|
大于等于
|
rexp >= rexp
|
rexp
|
L-R
|
否
|
<
|
小于
|
rexp < rexp
|
rexp
|
L-R
|
否
|
<=
|
小于等于
|
rexp <= rexp
|
rexp
|
L-R
|
否
|
==
|
等于
|
rexp == rexp
|
rexp
|
L-R
| 否 |
!=
|
不等于
|
rexp != rexp
|
rexp
|
L-R
| 否 |
&
|
位与
|
rexp & rexp
|
rexp
|
L-R
| 否 |
^
|
位异或
|
rexp ^ rexp
|
rexp
|
L-R
| 否 |
|
|
位或
|
rexp | rexp
|
rexp
|
L-R
| 否 |
&&
|
逻辑与
|
rexp && rexp
|
rexp
|
L-R
|
是
|
||
|
逻辑或
|
rexp || rexp
|
rexp
|
L-R
|
是
|
? :
|
条件操作符
|
rexp ? rexp : rexp
|
rexp
|
N/A
|
是
|
=
|
赋值
|
lexp = rexp
|
rexp
|
R-L
|
否
|
+=
|
以
...
加
|
lexp += rexp
|
rexp
|
R-L
|
否
|
-=
|
以
...
减
|
lexp -= rexp
|
rexp
|
R-L
|
否
|
*=
|
以
...
乘
|
lexp *= rexp
|
rexp
|
R-L
|
否
|
/=
|
以
...
除
|
lexp /= rexp
|
rexp
|
R-L
|
否
|
%=
|
以
...
取模
|
lexp %= rexp
|
rexp
|
R-L
|
否
|
<<=
|
以
...
左移
|
lexp <<= rexp
|
rexp
|
R-L
|
否
|
>>=
|
以
...
右移
| lexp >>= rexp |
rexp
|
R-L
|
否
|
&=
|
以
...
与
| lexp &= rexp |
rexp
| R-L | 否 |
^=
|
以
...
异或
| lexp ^= rexp |
rexp
| R-L | 否 |
|=
|
以
...
或
| lexp |= rexp |
rexp
| R-L | 否 |
,
|
逗号
|
rexp
,
rexp
|
rexp
| L-R | 是 |
int main()
{
//优先级:相邻操作符
int a = 10 + 5 * 2;
//结合性:优先级相同情况下,结合性才有作用
int b = 3 + 5 + 7;//先3 + 5还是先5 + 7呢?+的结合性是L-R就是先算3+5,从左到右计算。
return 0;
}
//表达式的求值部分由操作符的优先级决定。
//表达式1
a*b + c*d + e*f
注释:代码 1 在计算的时候,由于 * 比 + 的优先级高,只能保证, * 的计算是比 + 早,但是优先级并不能决定第三个 * 比第一个 + 早执行。
所以表达式的计算顺序可能是:
a * bc * da * b + c * de * fa * b + c * d + e * f或者a * bc * de * fa * b + c * da * b + c * d + e * f
如果abcdef是变量不论哪种计算路径答案都是对的,但如果是表达式呢?(可能用到相同的变量)。
//表达式2
c + --c;
注释:同上,操作符的优先级只能决定自减 -- 的运算在 + 的运算的前面,但是我们并没有办法得知,+ 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
+两边的操作数谁先准备呢?假设c=2,左边先准备,那就是2 + 1,右边先准备就是,--c,c变成1,在准备左边的c(已经变成了1),那就是1 + 1。
//代码3-非法表达式
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
表达式3在不同的编译器中测试结果:非法表达式程序的结果
所以在实践中不要写这种未定义行为的代码,连不同的编译器输出结果都不一样,那还怎么搞。
//代码4
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();我们只能通过操作符的优先级得知:先算乘法,再算减法。但函数调用先后顺序无法通过操作符的优先级确定。有两种可能:1. 2 - 3 * 4;2. 4 - 2 * 3。 vs2022下结果如下:
//代码5
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
可以试试看一下汇编代码,看看到底发生了啥,就可以分析清楚了->
记得先切到x86环境下,这个环境下较简单一些。
F11进入调试。记得调出右边的监视窗口。
然后对着代码鼠标右键跳转到反汇编->
记得关闭显示符号名->
逐步调试下来发现->
vs2022下它的计算逻辑是先算三个++i再计算两次+即4 + 4 + 4 = 12。
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那么这个表达式就是存在问题的。