表达式就是表示如何计算值的公式,最简单的表达式就是变量和常量,复杂的表达式还包括运算符
变量:程序运行时计算的值
常量:不变的值
运算符:用于构建表达式
运算符一共分为以下几种
1)算数运算符:如,加减乘除
2)关系运算符:如,大于小于这种比较运算符
3)逻辑运算符:如,与或非等
算数运算符:
一元运算符:
+ 一元正号运算符
- 一元负号运算符
二元运算符:
加法类:
+ 加法运算符
- 减法运算符
乘法类:
* 乘法运算符
/ 除法运算符
% 取余运算符
这里解释一下元的意思
这里的元指的是运算符所要连接的表达式个数,比如你想强调一个数是正数或负数,只需要写出
+1
-1
而二元运算符最少需要两个表达式
1+1
2-2
%取余运算符
i%j取的是i除以j所剩的余数
注意:
1)算数运算符可以混合两种不同的数据类型,
如9+2.5f = 11.5
8.88f/2 = 4.44
2)当两个int型使用/运算时将变为整除,
如3/2 != 1.5 而是等于1
3)%要求只能使用整形操作数,不能出现其他浮点型,否则编译通不过
4)0不能作为/或%的左值
5)/或%用于操作负数时,结果会很难确定
C89标准下,取出的负数结果可能向下或者向上取整
如-9/7的结果既可以为-1也可以为-2
-9%7的结果可能为-2或者5
C99标准下,取出的数结果只有一种,和上面一样,但C99标准只按照其中绝对值最小的值计算
如-9/7的结果既为-1
-9%7的结果为-2
总结:尽量避免除法和取余运算中有负数参与,如果无法避免,要明确自己使用的那种C语言标准
这种现象出现的原因:
《C语言与程序设计》一书中给出的解释是:
C89、C99都要保证(a/b)*b + a%b = a
也就是说只要除法a与b运算的结果,无论是整除/还是取余%运算,只要满足上面这个公式,C89都会认为结果正确,
当-9/7时,C89认为-1、-2都满足,当-9%7时,C89认为--2或者5都满足
结果就是:
-1*7+(-2) = -9//结果1
-2*7+5 = 9//结果2
这样就造成了两种结果均为正确的问题
C99这里的大多数CPU都习惯以除法中绝对值最小的为正确值,也就是上面的第一种结果
C语言与硬件强相关,根据CPU的不同,策略不同,所以同一台计算机在不换CPU的情况下,除法与取余的结果通常是确定的,但不同计算机上可能就会出现问题,尤其是一些年代久远的平台上。
运算符的优先级和结合性
这里可以参考
赋值运算符
赋值运算符用于将表达式的值存储在变量当中
v = e
v是存值得变量,e是表达式,注意,如果v与e类型不同,会将e转化为v的类型
并且这个v一定要为左值,这个左值我们可以理解为变量,或者任何对象结构体的句柄,并且一定不是常量的
int i = 12.66f
i = 12 而不会等于12.66
所以,赋值运算的两个操作数不同时,一定要注意由此产生的精度丢失问题
赋值运算符与算数运算符的不同点:
赋值运算符做赋值操作会改变左操作数的值
而算数运算符不会改变左操作数
int i = 0;
i = 1;//这里 i的值因为赋值发生改变,i此时应该为1
而算数运算符
int i = 0;
i + 1;
此时,i仍然为0,“i + 1;”当做一个共同的整体时才为1,在没有赋值前,表达式值是一个运算结果
所以当我们不希望改变原有值时可以使用运算符表达式代替
if((i+1)>0)
//dosm
因为赋值运算符可以连续如
#include <stdio.h>
int main()
{
int a,b, c;
a = b =c = 6;
printf("a = %d b = %d c = %d\n",a,b,c);
return 0;
}
输出结果为
a = 6 b = 6 c = 6
如果这样写:
int a,b, c= 10;
那么只有c会被赋值为6,因为赋值运算符是右结合的。
而a和b会输出意料之外的值,具体是什么看CPU心情。
又因为赋值运算符是右结合的,所以
a = b =c = 6;的复制顺序如下
a = (b= (c = 6)).
注意!!!:
使用连续赋值运算符时,要保持变量类型一致,否则极可能出现精度丢失问题
#include <stdio.h>
int main()
{
float a,c;
int b;
a = b =c = 12.066f;
printf("a = %f b = %d c = %f\n",a,b,c);
return 0;
}
输出结果为
a = 12.000000 b = 12 c = 12.066000
嵌入赋值运算符的表达式
#include <stdio.h>
int main()
{
int a,b,c;
a = 1;
b = 1 +(c = a);
printf("a = %d b = %d c = %d\n",a,b,c);
return 0;
}
输出结果为
a = 1 b = 2 c = 1
但是,强烈不建议这样写,首先他不利于阅读,其次容易引发其他错误
左值
左值也就是是赋值运算符左面的值,他不能为常量,可以为变量或者结构体的属性
复合赋值
如i = i + 2;
可以写成如下形式
i += 2;
同样+、-、*、/、%都可以这样结合
不过,实际开发中同样也不建议使用这种复合运算符赋值的方式
原因有以下两点
1)书写错误问题
熟悉if语句的人一般都知道,if(表达式)使用表达式判断==时如果我们少写一个 = 会变成赋值表达式,这样编译器是不会报错的,如if(a = 0),相当于if(a),又因为C语言表达式0相当于false,这样我们的选择表达式就失去了判断的意义
所以我们一般这样写if(0 == a),把常量作为左值,这样如果我们少写一个 = 时,if语句变为if(0 = a)
这样常量表达式作为赋值运算符左值时,编译器会报错,这样的习惯会帮助我们发现书写错误
同理,如果我们将复合赋值运算符写反了,会造成什么后果呢?
如下
int main()
{
int a = -1;
int b = 1;
b =- a;
printf("a = %d b = %d \n",a,b );
return 0;
}
输出结果为
a = -1 b = 1
编译器并没有报错,因为+、-在作为一元运算符时与操作数左结合时会优先被编译器识别为一元正号运算符和一元符号运算符,实际输出的结果会与我们的期望天差地别,这种疏忽导致的问题在查找起来非常的麻烦
2) 优先级问题
int main()
{
int a = 2;
int b = 3;
int c = 4;
int d = 5;
a *= b +c +d;
printf("a = %d \n",a );
return 0;
}
输出为
a = 24
a *= b +c +d;
会被编译器视为a *=(b +c +d);
而很多人可能会理解为
a = a *b +c +d;
但实际结果天差地别,会让不熟悉的人难以阅读
结合方式:
复合赋值运算符与普通赋值运算符都是右结合的
也就是说
a += b += c
等价与
a += (b += c)
自增/自减运算符
使用++或者--与变量结合可以达到自增加1和自减少1的操作,
i++ 相当于 i += 1
i-- 相当于 i-=1
上面这种是后缀的自增减
如果少写一个符号的话,编译时会报错
还有一种是前缀形式的
++i
--i
两者有什么区别呢?
前缀:立即变化
后缀:随后变化
立即变化很好理解,如++i时,这个i直接发生改变,和i += 1结束一样
而后缀自增减,是要等待自增减这一行结束才会变化,
如何证明呢
int main()
{
int a = 1;
printf("a = %d \n",a++ );
return 0;
}
输出
a = 1
其实这个时候a++ 就相当于打印a,证明这行结束前,这个a还是为1的
我们换种写法
int main()
{
int a = 1;
a++;
printf("a = %d \n",a );
return 0;
}
结果为
a = 2
可见a++;结束完成后,才执行了a = a +1;也就是(a += 1) 的赋值操作,
你也可以理解为先计算,在表达式执行完毕后赋值。
实际使用中容易出现的问题
int main()
{
int a = 1;
int b = 2;
int c;
c = a++ + b ++;
printf("a = %d \n",a );
printf("b = %d \n",b );
printf("c = %d \n",c );
return 0;
}
先看看自己的期待结果是什么。。。。
。
。
。
。
。
。
。
。
实际输出
a = 2
b = 3
c = 3
如果这个结果和你期待的一致,那就证明你理解的正确,如果不一致,那这里就再说明一次
为什么a、b都有增加,但c却跟b一样,而不是5,
因为运算中的a++再运行到;前始终是a的状态,只有;结束的时候,才会自变化。也就是说,没出;时这个运算的表达式a、b、c还是1,2,3的状态,因为c的被赋值操作已经完成,还是(1+2)3,而a和b才去完成他们的赋值操作(+1)。
未定义行为
看了上面关于表达式运算结束后,后缀自增的解释后,是不是觉得在表达式中书写赋值语句其实也没什么,只要按照优先级最高的结合方式运行就没问题了?
那就大错特错了,实际上,在运算表达式中使用赋值语句在c语言编程中被可以被称作未定义行为,(这种只是未定义行为的其中一种)
未定义行为产生的结果是未知的,不同的编译器会有不同的运算方式
以下面为例:
#include <stdio.h>
int main()
{
int a = 2;
int b = a * a++;
printf("b == %d \n",b);
return 0;
}
使用Qt的cmake编译器的输出结果为
b == 6
是不是大吃一惊,前面我们提到,后缀自变化++在表达式计算结束后生效,理论上,应该是2*2结果为4才对,因为在;之前++自增的变化未对a造成改变,实际上在编译器手里,自己曾经定义的优先级策略完全不被编译器遵守了,或许,某一种编译器会按照2*2的结果执行,但是我们也不能这样编写代码,这个问题我也查找 很多,难以解决,或许只有对汇编语言很了解的同学才能解答
更新于2022年11月2日 找到了下方的这两篇文章
C语言面试必备——C语言自增/自减操作的陷阱_从善若水的博客-CSDN博客_c语言 自减
C语言自增、自减运算符使用中应注意的问题 王红_xiaofei0859的博客-CSDN博客_自增自减运算符的运算对象只能为
这两篇回答讲解的很好,看完了直接理解了原因
感兴趣的可以去看看
以下是我基于上两篇回答和个人的整理出的一些观点,也许不对,欢迎指正
1)不同编译器对后缀自增的“后”的认定方式是不同的
对于目前的C编译器来说,以我现在使用的cmake或者
C 在线工具 | 菜鸟工具在编辑器上输入简单的 C 代码,可在线编译运行。..https://c.runoob.com/compile/11/
对于计算:
int i = 3;
int b = i++ + I++ + i++;
这两种都认为
最终的结果 相当于3+4+5(我自己暂且将其称为I型)
而在一些老的C编译器版本上如TC3.0(我自己没有用过,参照第二篇回答)
认为结果为
3+3+3(我自己暂且将其称为II型)
根据结果我猜测的两种模式大概的计算原理
I型后缀自增:1个变量在每次后缀自增结束后不会变化,编译器隐藏了“+ 1”操作,当这个变量被再次访问时(访问的意思包括了printf输出、表达式计算、赋值语句等任意引用),这时编译器会将之前寄存的“+1”操作执行,所以会出现“3+4+5”的I型后缀
II型后缀自增(说实话我只是在别人的文章上看到过):
在;结束之后(或者在整个{作用域})结束之后编译器统一执行自增操作,这样就会出现“3+3+3”的情况
2)同级子表达式的顺序并不是严格的从左到右的
在第一篇引用的文章中作者使用的编译器先计算了I型右边子表达式的结果,之后又计算了左边表达式的结果,导致这位作者最终结果等于21,作者给出的解释是C编译器在设计时为了提升效率,会自行选择先对表达式中的哪个子表达式求值。
也就是说编译器认为先计算含有后缀自增的表达式效率会更高(至于为编译器什么认为效率高,还不知道)
结论:
可以想到的是,编译器将一行表达式按照优先级最低的算数运算符拆成了两个部分,编译器认为最低运算处于最后一步运算,那么先计算左与右是相同的,比如3*4+5*6与5*6+3*4,无论是先计算前半部分还是后半部分在正常情况下都是无影响的
以数学上的观点来理解这个现象可以得出:
显然C编译器是以一元函数的思想在处理非一元的表达式,如当我们计算y = a + (a+1)*a时,当y确定的时候,无论怎么去计算,这个a一定是个定值,我们可以理解为只要开始计算函数,函数就相当于计算机语言中的常量
但是,这一切的结果都是建立在a这个值只有唯一解的情况,未定义行为出现在I型后缀自增中的主要原因在a可以在下一次访问后被赋值
b = a*a++ 根据第而篇回答中介绍的说法,和我自己的猜测,之前的“3+4+5”其实在C编译器眼里的计算方式应该是“5+4+3”而
int a = 2;
b = a*a++;
他在C编译器眼里的执行顺序应该是“3*2”,而不是“2*3”,如果你看到这里一下子就明白了,那说明我的讲解能力还是可以的
以a*a++;为例
下面直接说下想法,先结合第2)点中的编译器并不是严格的从左到右计算,
编译器认为以当前最低优先级的“*”运算符将表达式分割成两个部分,编译器认为先计算后缀自增效率最高,计算a++后右侧子表达式结果未变,当绑定于a的“+1”运算仍在“寄存器”中(说寄存器或许不是很合理,因为我不懂这些东西如果不恰当也请指正),最后计算左侧子表达式访问了1次a,那么就会出现a*a++ = 6的情况了,同时这个理论可以解释回答1中遇到的所有问题
总之:
自增/减表达式一定要单独计算,不要与其他表达式进行混合,否则极易出现未定义问题,因为自增自减属于赋值表达式,在各个编译器左右运算优先级的干扰下,非常容易产生意料之外的值,我们不能因为了解这个编译器的左右运算优先级是什么样的,就认为这个值应该等于什么,而应该就认为其是不确定,虽然编译器不会报错,我们也认为这种写法是错误写法,因为你不能保证你的代码会不会被人在其他的平台使用,如果你的代码被移植,那么结果很大的概率会出现错误
表达式语句
就是表达式以;结尾可以成为一个单独的语句
如:
a+b;//这个就可以成为一个单独的语句不会报错
但要注意,赋值语句(+=、-=、++、--、=)会改变值并存到变量中,
但如a+b这种结果如果单独执行的话出了;后就会被丢弃
其他狗血代码
a[i++] = += 2;
a[i++] = a[i++] + 2;
//从下面可以看出上面是未定义的