–通过在不同的IDE里编译程序,观察各个IDE的调用约定的细节与差别。
一、压栈规则/调用约定:
首先,在c/c++中,printf的本质是一个在
在这里,我们举一个例子:
ps:这里的博客实际上参考了https://blog.csdn.net/qq1184810369/article/details/14323555?locationNum=15这篇博客,在此基础上有了一些拓展的思考和研究
int i=3;
printf("%d,%d,%d,%d,%d,%d,%d",i++,++i,i--,--i,i--,i--,i+5);
按照我们常规的思路,根据自增运算符的规则,这句printf应该会在输出其中的每个自增表达式之前或者之后将对应的变量i进行改变,所以结果应该是(3,5,5,3,3,2,6)
但实际上,我们在如devc(codeblocks)中运行时:
在vs2019中运行:
我们看到输出的数据是以一种很诡异的如“0,1,0,1,2,3,6”,和“0,1,0,1,2,3,8”的形式输出的。如果是正常的思路的话怎么想都不对。
那么这里我们提到一个词–函数的调用约定,即函数的参数传递。
函数的参数传递大体有一、传给寄存器,二、传入栈中。这些参数的传递根据编译器的不同有不同的传递方式,根据实际经验对于c/c++来说,将参数入栈是比较常见的作法(还有如指针和c++的引用的地址入栈)。并且入栈顺序也有不同,如c/c++的许多ide是按参数从右向左入栈,也有从左向右入栈,还有的编译器或者解释器是按更为复杂的入栈方式。
那么这里联想到前面所说的,printf()的本质其实就是一个函数,那么我们所给它的那些信息也就会被入栈。而这里也不难看出,因为栈的传递规则是FILO(先入后出),所以对于我们的表达式,若要从左到右输出数据,那么我们就要从右向左将数据入栈才可以。
而在这里,我们虽然知道了这些参数是从右向左入栈的,但是还是不能解释为什么输出的是那些东西。那么这里就提到一个问题:
我们所传入的是什么?
具体问题具体分析,我们这里的代码如果按照参数传递的规则来的话,传递的应该是:“%d,”,i++,++i,i–,--i,i+5,(这里先不看前面的字符串),很容易看到的一点是,我们所传入的不单单可能只是一个值,这里传入的都是如i++(i=i+1),i+5这样的 运算表达式。
而这里涉及到一个知识点,既然传入的参数是一些表达式,那么表达式肯定是要经过计算的,但这里的计算分为两种情况,一种是再传入之前,因为编译器有相关的调用约定,所以是将表达式计算完之后直接将值入栈。另一种是直接将表达式入栈,而后函数提取出来再进行运算。
并且,对于codeblocks,vs,devc(devc和codeblocks的调用约定未发现差别)来说,有如下规则:
1、printf()操作分两步完成:
第一步:参数入栈:
在入栈时,各种变量运算进行执行。
第二步:参数出栈:
在出栈时,输出栈中的结果,如果栈中压入的是变量,则输出变量本身的值,如果压入的计算公式,则需要重新计算(对VS的"i+常量"而言),而如果压入的是数值,则将该数值输出。
2、printf()压栈规则:
后入栈,也就是参数从右往左入栈。
3、前置加加与后置加加的区别:
前置操作压栈时,压入的是变量;后置操作压栈时,压入的是常量(即运算结果)。
前置操作在压栈时,已经进行了前置运算。也就是说,对于++i,压栈时,i已经完成了自加,并且,压入栈的是i本身,而不是i的值。
后置操作在压栈后,相关变量才完成后置操作运算。也就是说,对于i++,压栈时,i并没有完成自加,并且,压入栈的是i的值,而非i变量本身。
4、VS与CodeBlock/devc中,i+常量操作的处理:
在VS中“i+常量”操作在压栈时,压入的是“i+常量”运算,此处的i是变量。
在CodeBlocks/devc中,“i+常量”操作在压栈时,压入的是“i+常量”的运算结果,压入的是数值。
因此,我们对于之前输出结果的疑惑可以有了解答:
在codeblocks/devc中,因为所有表达式都是在完成运算之后再压栈,对于这个情况也就是说,printf里所要输出的值,从右向左进行计算,i+5=>3+5=8,入栈, i–=>3入栈,3=3-1…输出…
而vs因为其调用约定对于i+5不同于前面的自增和自减,压入的是i+5这个表达式,也就是我们说的,在函数调用到这个值的时候再进行表达式的运算,那么此时printf里面的i经过前面的运算已经如输出所见,i=3这个变量先经过实际编译器的处理,i–,i–,--i,i–,++i,i++等操作,那么就是=>3入栈,3-1=2,2入栈,2-1=1,1-1入栈,1-1=0…0入栈,0+1=1。而最后我们的输出也就是将这些数据一个一个出栈,并且最后在i=1的时候,进行i+5的操作,输出6。
ps:这里提到的那篇博客真的很厉害,思路十分清晰,讲得十分明白,这里的一些论述也引用了那篇博客,推荐大家关注一波。
---------------------------------------更新
这里我加了ida对代码的分析,发现实际情况跟想象中的不太一样。
devc:
vs:
cc的编译方法在处理printf里的表达式时,从右往左先对表达式进行了处理,将结果计算好了放在内存单元里,之后由printf函数进行调用。
但vs的编译方法是将变量i存储起来后,之后的每一步运算都直接对i进行操作,再放入对应的内存单元(后置++操作是先放入内存单元再对i操作),当所有运算完成之后,再同样按从右到左的顺序入栈。
因此,用dev(cb)跑出来的结果,i+5=8,实际上就是3+5=8,而vs输出的i+5=6则某种意义上就是"i+5=6".