不同编译器下自增自减表达式输出不同值的研究
2023-12-25陈贤敏汤海晨陈治帆
陈贤敏 汤海晨 陈治帆
摘要:同一个C语言的自增、自减表达式为什么在不同集成系统开发环境中产生不一样的结果。使用GCC和Clang两种不同的编译器来验证此实例表达式的值,并把两种编译好的C语言程序反编译成汇编语言,再来分析代码,目的是让大家真正从底层了解为什么相同的表达式值会产生不同的结果。
关键词:自增自减;GCC;Clang
中图分类号:TP311 文献标识码:A
文章编号:1009-3044(2023)31-0059-02
开放科学(资源服务)标识码(OSID)
0 引言
C语言中的自增自减运算符(++和--)简洁、紧凑、灵活。学生在做练习中遇到由自增自减运算符组成的复杂算术表达式,如p=(i++) +(i++) +(i++) 时,在不同的编译器中可能会得到不同的运行结果[1],会让用户产生茫然疑惑。
国内对于C语言中的++、--用例有许多针对性的探讨和研究,朱恩亮[2]在《Visual C ++与Turbo C处理自增自减运算表达式的区别》一文中列举了在Visual C ++、Turbo C编程语言环境中,通过操作符的连接,可以构造出具有不同复杂程度的表达式。分析了i=3,表达式s1=(++y)+(++y)+(++y)值分别是s1=5+5+6=16和s1=6+6+6=18,产生不同结果是由于在C语言中操作符存在着优先级与结合性,并构造表达式后,不同语言系统的编译会产生不同的结果。
夏超群[3]在《浅析C语言自增自减运算符的使用》一文,其例2的语中p=(i++) +(i++) (i++) 从理论上分析该语句中表达式(i++) +(i++) +(i++) 的值应为5+6+7,实际却是VC++6.0环境下运算结果都是5+5+5。文中说明后置自增自减运算符的“先用后变”的“变”是指在下一条语句执行前统一改变,而不是刚用完就变。故该语句等价于:p=i+i+i;i=i+1;i=i+1;i=i+1。造成这种结果是因为高级语言的一条语句经编译解释成若干条机器指令,这若干条机器指令的顺序最终决定该等价高级语言语句的執行结果。
为什么编译系统会产生这两种结果,以上作者的文中并未从源头给出答案。因此本文通过把C程序代码编译后转换成汇编语言并分析代码的执行顺序,真正从底层了解不同的编译器为什么会产生不同的结果。
1 C语言实例
#include <stdio.h>
int main() {
int accumulate,i=5;
accumulate=(++y)+(++y)+(++y);
printf("accumulate=%d,i=%d",accumulate,y);
return 0;
}
大家看到此实例中的表达式accumulate=(++y)+(++y)+(++y),第一反应给出的答案为6+7+8,结果值为21,y的值为8,那是否正确呢?其实还可以给出第二种答案accumulate的值为22,y的值为8。
此实例C语言中的表达式accumulate=(++y)+(++y)+(++y)的背后运算到底是怎样的一个过程,为什么此实例程序中的变量y等于5,最终程序运行会产生两种不同的结果,分别是22和21。下面通过GCC编译器模式和Clang编译器模式验证此实例在不同的编译器得出的结果值是不一样的。
1.1 实验1
实验环境操作系统是CentOS Linux,GCC编译器模式,值为accumulate=22,y=8。通过GCC反编译实例后如图1,为了便于理解,图2根据图1一一对应写成C语言格式,使读者更好地理解汇编语句。
可以看出,表达式accumulate=(++y)+(++y)+(++y),语句(1) 先把[rbp-4]当成i变量,理解成把5赋给[rbp-4]变量,add DWORD PTR [rbp-4],1;语句(2) 相当于表达式中的第一项(++y) ,加1执行后[rbp-4]变量值6,i变量自然也是6了,执行语句(3) 汇编语句,相当于表达式中的第二项(++y) ,在原来[rbp-4]的基础上又加1,得[rbp-4]值为7,自然i的值为7。语句(4) 简单点说,就是把7赋给eax。难道继续加1是i的值被替换成8吗?显然不是,语句(5) lea edx,[rax+rax] ,理解为edx=7+7=14此时才明白为什么在C语言中前两项 (++y)+(++y)相加,也就是accumulate=7+7+(++y)。到这里读者有点不理解了,这个rax为什么等于7。简单解释一下,32位和64位寄存器不是分开的寄存器,它们是重叠的:64位的rax,具有eax作为其底部的32位。因此,对32位寄存器的修改会反映在相应的64位寄存器中,反之亦然。继续看语句(6) 中[rbp-4]是多少呢,它在执行语句(3) 后值没有改变过,依旧是7,因此执行(6) add DWORD PTR [rbp-4],1。相当于表达式中的第三项(++y) ,[rbp-4]值为8,语句(7) [rbp-4]赋给了eax,eax值为8。继续执行语句(8) add eax,edx ,可以把该语句转换成熟悉的语句eax=eax+edx,已知文中语句(5) 已得出edx=14,因此可得出eax=eax+edx=8+14=22,如图3所示。
1.2 实验2
分析完了GCC编译器模式,现以值为accumulate=21,y=8,按照同样的方式分解一下由Clang编译器模式编译的程序,就会发现情况有所不同。
通过图4、图5可以看出,表达式accumulate=(++y)+(++y)+(++y),先通过语句(1) 、(2) ,把5赋给eax,语句(3) 累加1后把值赋给eax寄存器,eax值为6,相当于表达式第一项(++y)的值,注意先把第一项值6保留在eax寄存器,通过语句(4) 、(5) 把eax值为6赋给ecx,执行语句(6) ecx累加1后,值为7,相当于表达式第二项(++y),把第二项值也是独立保存在ecx寄存器中。语句(8) eax=eax+ecx,得eax=6+7=13,从语句(9) 、(10) 可得出表达式第三项(++y)值为ecx=7+1=8,语句(12) 的结果为eax=eax+ecx=13+8=21,如图6所示。
2 实验结果分析
现在才明白实验1中编译器前两项自增后,GCC编译是前两次y,(++y)+(++y)+(++y),原因是首先扫描求解前半部分,即(++y)+(++y)的值,先对y变量进行两次自增运算,y的值变为7,再计算y+y的值为7+7=14,然后再求解后半部分,即14+(++y)的值,先对变量y自增1次,y的值变为8,再计算14+8=22,因此最终accumulate的值为22,y的值为8。
实验2中,用Clang编譯器,再反编译为汇编语言。通过汇编语言可以看出,前两项y的值都分别独立赋给不同的寄存器eax和ecx,因此前两项(++y) 中的y就不会累加后再赋给相同的寄存器。而实验1中(2) 、(3) 中两条语句是对y进行累加后赋给,由原来的初值5变化7后,再赋给寄存器eax。
文中的实例accumulate=(++y)+(++y)+(++y)只是来证明这样的式子在不同的编译系统中会产生不同的值,因此在实际的工作生产和教学中不建议这样来使用。任何类似(y++)+(y++)、(++j)+(++j)这样的表达式,都属于未定义行为。可以这么说,在C语言标准中没有规定这样的表达式应该如何计算,完全由编译器自行决定,说到底编译器也是由程序员来开发出来,当然不同的人有不同的逻辑理解,这样也就导致不同的集成开发环境编译器下自增和自减处理逻辑的不同。也进一步说明,由于自增自减表达式结果的不确定性,也就不具有可移植性,是十分不友好的表达式。
通过以上实例验证,相同的表达式运行结果得出的值不一样是由不同集成开发环境中的不同编译器编译造成的。实验1、实验2论证了使用GCC、Clang两种不同的编译器对同一段C语言源代码进行编译,得到了不同的值,它们按照各自思路转化为了汇编代码,通过对汇编语言的分析,最终找到了相同源代码在不同的集成开发环境下结果是不一致的,这也是写本篇论文的目的和任务。也通过上述实验发现Clang的编译器更能符合正常人的思维逻辑,在今后的学习工作中更推荐使用Clang编译器的开发环境。
3 结束语
C语言中自增运算符为“++”,其作用是使变量的值增1;自减运算符为“--”,其作用是使变量的值减1[4]。当初制定这两个C语言运算符是为了方便程序使用,但在使用y++、--y不恰当时会造成混淆,给刚入门学习C语言的人员带来混乱。
当然,像文中的实例代码移植性差,需要大家尽量避免,分析这样的运算顺序也没有任何意义。建议大家尽量不要去研究这样的表达式,也更不要在实际编程中写出这样的表达式。所以,在编写程序时,有选择地小心谨慎使用自增(自减)运算符来简化程序,在一些容易出错的地方可以用其他方法代替,从而保证程序的执行万无一失[5]。当然在实际项目中,考虑到项目迁移等问题,建议不要使用连续自增、自减进行运算,这种逻辑问题在项目维护过程中很难被发现和维护。
参考文献:
[1] 袁玲.在DEV C++环境下C语言自加自减运算符使用分析[J].电脑知识与技术,2016,12(27):248-249.
[2] 朱恩亮.Visual C+ +与Turbo C处理自增自减运算表达式的区别[J].盐城工学院学报(自然科学版),2003,16(3):27-28,31.
[3] 夏超群.浅析C语言自增自减运算符的使用[J].武汉工程职业技术学院学报,2010,22(3):47-49.
[4] 周亮.浅谈C语言中自增自减运算符的应用[J].电脑知识与技术,2010,6(17):4714-4715.
[5] 唐婷,吕浩音.C语言自增(自减)运算符运算规律的探讨[J].陇东学院学报,2016,27(5):8-11.
【通联编辑:谢媛媛】