写在前面:
由于本人非计算机专业,所以本文在一些专业用语表述方面可能不够严谨,甚至可能出现一些因个人理解产生的偏差,敬请读者谅解。我也将不断努力,尽量让自己的表述规范起来。
写这一篇关于++/--运算符的文章,直接原因是最近要给大一同学做C语言串讲,在重新拾起一年前的知识时,发现好多东西已经模糊甚至是淡忘了,只能从头开始一步一步调试,总结。另外其实大一的时候本来想好好总结一下关于这个的东西,但是因为当时还没有在CSDN开号,所以过了一段时间就忘记了。不过现在来写,从某种意义上来说,或许会比当时更好吧。
2022/10/10 更新的忠告
通过阅读《C++ Primer Plus》,我发现里面对连续使用的自增、自减表达式是这样描述的:
递增运算符和递减运算法都是漂亮的小型运算符,不过千万不要失去控制,在同一条语句中对同一个值递增或递减多次。问题在于,规则“使用后修改”和“修改后使用”可能会变得模糊不清,也就是说,在不同系统上将会生成不同结果。
所以,如果不是考试中某些老师故意刁难的话,推荐大家最好还是尽量避免在实际编程过程中使用上述提到的语句。有时无意义的炫技可能会把自己害死。
当很多同学在C语言相关考试题中看到这样的表达式,估计都会感觉比较崩溃:
它的输出结果是(在VS2019和DEVC++中的结果):
我记得自己当时学这部分内容时,老师是把它作为一个极端的例子来讲的。虽然当时经常翘课(QAQ),也没好好听到底原理是什么,但是类似这样的在printf中放一大堆++作为参数的题还是给我留下了难以磨灭的印象。
当然,上面这个例子其实挺简单的,像下面这样理解就行:
前两个+号跟着前面的x走,形成一个x++的后置自增表达式,对于这样的表达式的操作,上课时老师一般会说是先把x的值代入进行计算,算完之后再进行一步x = x + 1,就像下面这样:
这个对简单的表达式(只含有一个++运算符)固然是很容易理解并计算的,但是如果一个表达式中有不止一个++运算符(注意,是表达式,而不是一个语句,区分“表达式”与“语句”是理解自增运算符的前提),那么这一步x = x + 1在 什么时候执行,对于最终计算得到的结果会有至关重要的影响。
为了判断x = x + 1到底在什么时候执行,我们再来看一看下面这个例子:
(这里两个x++中间的空格只是为了将操作数区分开, 如果不这么做的话编译时会报运算符的错。)
输出结果如下:
我们在这个例子中的printf输出参数中仅使用了一个表达式:
可以发现,两个x++操作数最后计算得到的结果都是1,即变量x在表达式计算过程中并没有+1。所以,x = x + 1不是跟在每个x++之后执行的。但是现在x = x + 1执行的位置仍然存在两种可能,即在所在表达式结束时执行,以及在整条语句结束时执行。
再来看看这个例子:
它的结果是:
在开始这个例子之前,首先需要对printf的栈操作有一定了解。下面简单介绍一下:
简单地来说,就是printf中的若干个参数表达式会以从右往左的顺序进行求值 。
再回到我们上面的例子,对于如下语句:
printf("%d\n%d", x++, x++);
会先计算后一个x++,当计算完该表达式得到x的值1,并压入栈中之后,再开始计算前一个x++的值。而输出的顺序刚好相反,会按照人的阅读习惯,从左往右输出各表达式的值。
这样一来,结果就很清晰了。
后输出的是先计算的第二个x++,也就是1,这个没什么争议:
关键在于先输出的那个后计算的第一个x++,它的结果是2:
也就是说,在计算这个表达式之前,变量x的值已经由原来的1,变成了现在的2,即表明先计算的x++在表达式结束时已经产生了x = x + 1的效果!
现在我们可以得到初步的结论:
是不是觉得很简单?
不过,你以为这样就结束了吗?
如果真的这么简单的话,我也不用在感冒的时候白费数小时来写这篇文章了。
到目前为止,我们才刚刚讨论完后置自增运算符单独出现的场景,下面这个例子,将会让你对之前的结论产生怀疑:
我猜你会觉得他的结果是3,对吧?
其实我第一次看到这个表达式的时候,也是这么认为的。
但是它的结果却是:
这种类型的表达式,输出结果还可以更加离谱:
(同时,先埋个伏笔,以上测试均是在VS2019的编译环境中得到的结果。)
其实看上面这两个例子,一些细心的同学还是可以发现其中的规律:
2个自增表达式→输出结果为2x2
3个自增表达式→输出结果为3x3
……
如果感兴趣的话还可以继续去尝试一下,应该往后面得到的结果都会与以上形式相符合。但是怎么理解呢?为什么本来在表达式执行完后才自增的x++,会在中途便发生了变化呢?
我的理解是这样的:
加减乘除运算符需要左右两个操作数,而不管是x++还是++x,本质上最后都会成为这些运算符的操作数。那么他们最终输出到表达式两端进行运算的操作数究竟是什么呢?
按照我们的一般思维模式,在计算时,应该要等两端的值都为确定的常量时,再进行求和,但是依照上面的结果,如果我们假设x++和++x最终生成操作数时,都会输出x本身,即所要计算的表达式为,那么就能得出++x在这一步操作之前,先进行了一步x = x + 1,使得之后变量x在全局范围内所代表的值都比原来的x大1;
与此同时,VS2019所采用的编译器(具体是哪个版本我也没查)在计算含有多个同一变量x的表达式时,会在最后一步才代入x的值,于是在第二个例子中我们最后得到的表达式为:
其中x由于经历了两次的++x自增,值已经变成了3,代入后惊喜地发现我们居然得到了正确的结果9!
也就是说,我们得到的第二个结论是:
而第三个结论是有关于++x的:
也许你们会说,到这里应该就结束了吧,但是出于严谨的我,又耐心地跑到DEVC++上去做了同样的测试(其实是因为大一的同学们大多数用的编译器都是DEV,而且众所周知VS这个编译器由于“过于安全”,经常会出现许多过不了编译的安全问题,怕到时候别人一跑出问题就尴尬了),结果不试不知道,一试吓一跳:
此时我的内心百感交集……
(TM不会前面的推理全都是错的吧!!!)
冷静了一会之后,我想起了之前看过的数据结构中栈结构部分关于基本运算实现的后缀表示法部分,突然有了一点灵感。
如果还不了解后缀表达式(也叫逆波兰表达式),可以先去看看下面链接中的这篇博文:
(详细图解) 逆波兰表达式
所谓的后缀表达式,简单地来说就是将原先的a+b*c-d这样的表达式进行了一个“先乘除,后加减”的运算顺序的安排,每次取两个操作数计算出结果,再压回栈中。那么它和我们刚刚讨论的问题到底有什么关系呢?
表达式的计算顺序为:
(此时x经过一次自增,变成了2);
(此时x又经过一次自增,变成了3)
就得到了我们想要的结果。
也就是说,DEVC++在处理这类多项式计算时,采取的是每做一次运算,就将当前的x值代入得出结果的策略;而VS2019则采取了将表达式作为一个整体,以表达式结束处x的最后取值作为x的值进行代入计算(不知道这样的说法是否严谨)。
到这里我们所讨论的问题基本上算是正式结束了。如果文章中有什么错误的话,欢迎大家积极指正!