由三目运算符(? : ;)与++混用导致的陷阱而引申出++,-- 运算符使用说明

时间:2022-11-01 22:14:30

一. 陷入的陷阱

今天在编写测试代码,其中有一行代码如下:
static uint8_t su8Tmp = 0;
...
...
su8Tmp= (su8Tmp > 99) ? 0 : (su8Tmp++);

当时想当然的以为这行代码没问题,以为当 su8Tmp <= 99 时,会执行 su8Tmp++,最终使得 su8Tmp 加 1. 但结果却百思不得其解,为什么变量始终没有自加?

可能有人会认为,像这种类似情况,我都不用自加或自减,这固然是一种避免该陷阱的好办法.但是既然已陷入了这种陷阱,还是想以此篇博文深刻的警醒自己,同时也分享给同仁们,让大家少陷入类似陷阱,避免揪心.

二. 陷阱背后的真像

  1. 首先我利用VC编写了如下测试代码:
int main()
{
unsigned int u32Tmp = 0x123;

printf("Front u32Tmp:0x%x.\n",u32Tmp);
u32Tmp = (u32Tmp < 0x456) ? u32Tmp++: 0 ;
printf("After u32Tmp:0x%x.\n",u32Tmp);

return (0);
}
结果为:
Front u32Tmp:0x123.
After u32Tmp:0x123.
Press any key to continue

通过Alt + 8调出其汇编代码,截取存在陷阱的部分,如下所示:

9: u32Tmp = (u32Tmp < 0x456) ? u32Tmp++: 0 ;
00401040 cmp dword ptr [ebp-4],456h //比较 u32Tmp 与 0x456 的大小
00401047 jae main+4Ah (0040105a) //u32Tmp < 0x456 不成立,则转移到指定地址处
00401049 mov ecx,dword ptr [ebp-4] //把u32Tmp 值赋值到寄存器 ecx
0040104C mov dword ptr [ebp-8],ecx //把寄存器ecx的值赋值到(ebp-8)地址处, dword ptr:表明是一个双字的指针地址.
0040104F mov edx,dword ptr [ebp-4] //把u32Tmp 赋值给edx寄存器
00401052 add edx,1 //edx寄存器中的值加1,即u32Tmp 的副本加1
00401055 mov dword ptr [ebp-4],edx //把edx寄存器中的值赋值给u32Tmp (此时u32Tmp 自加了 1)
00401058 jmp main+51h (00401061)
0040105A mov dword ptr [ebp-8],0 //u32Tmp < 0x456 不成立,则转移到该处,并把 0 赋值给 地址为ebp-8处的临时变量
00401061 mov eax,dword ptr [ebp-8] //把地址为ebp-8处的临时变量赋值给 eax寄存器
00401064 mov dword ptr [ebp-4],eax //把eax寄存器中的值当作最终值,赋值给 u32Tmp

最终 u32Tmp 的值不管前面的判断是真还是假,其结果一直都是汇编代码中自动创建的临时变量 dword ptr [ebp-8] 地址处的值, 而当使用 u32Tmp++ 时,该临时变量记录的是自加前的值. 尽管三目运算符的汇编运算过程中 u32Tmp 变量确实自加了1, 但因为先运算自加, 而后被自加前的临时变量给清除了自加运算,使得 u32Tmp 始终无法自加.

以上就是其陷阱的内幕所在. 若代码中使用的 ++su8Tmp, 此处便不会有问题, 对应汇编如下:

 9: u32Tmp = (u32Tmp < 0x456) ? ++u32Tmp: 0 ;
00401040 cmp dword ptr [ebp-4],456h
00401047 jae main+4Ah (0040105a)
00401049 mov ecx,dword ptr [ebp-4]
0040104C add ecx,1
0040104F mov dword ptr [ebp-4],ecx
00401052 mov edx,dword ptr [ebp-4]
00401055 mov dword ptr [ebp-8],edx
00401058 jmp main+51h (00401061)
0040105A mov dword ptr [ebp-8],0
00401061 mov eax,dword ptr [ebp-8] //该临时变量已经是 自加1 的值了
00401064 mov dword ptr [ebp-4],eax

相信大家很容易看出其中的猫腻了!


解决方法评估:
1. (su8Tmp + 1) 比 su8Tmp++ 更加直观, 不容易出错.
2. ++su8Tmp 虽然符合编码需求, 但是不直观, 且很容易”蒙”错. 所以以后还是慎用这种 ++ , - - 运算符.

三. 扩展说明 + +, - -运算符

以下内容引用的是”C语言深度剖析(第2版)“一书.

1.++, - - 作为前缀,是先自加或自减, 然后再做别的运算, 但是作为后缀时, 到底什么时候自加, 自减, 是很多初学者迷糊的地方.

假设 i = 0; 例子如下:
A. j = (i++,i++,i++);
B. for(i=0;i<10;i++)
{
//code
}

C. k = (i++)+(i++)+(i++);

A.例子为逗号表达式,表达式最终的结果为最后一个 i++ 的值. i 在遇到每个逗号后,认为本计算单位结束, i 这个时候自加.最终结果为 j = 2 , i = 3;
B.例子中 i 与10进行比较后,认为本计算单位已结束, i 这个时候自加. (通过查看汇编代码执行过程,书中这样描述是有误的.汇编代码是先自加 1,再进行比较. 运行 i++ 时,已然认为该运算已结束,此时 i 已经自加, 其实此处若是 ++i, 两者的汇编代码是一样的,由此可见 for 循环括号中三个表达式语句,是各成一体)
C. 例子中 i 遇到分号才认为本计算单位已结束, i 这个时候自加.最终结果为k=0,i=3

总结: 后缀运算是在本计算单位计算结束之后再自加或自减(或者本表达式语句运行结束时自加或自减). C语言里的计算单位大体分为以上 3 类! 如何分隔表达式语句, 目前来看, 逗号和分号有此作用.

2.i+++i+++i+i
天啦,这到底是什么东西? 其实 C 语言中有这样一个规则: 每一个符号应该包含尽可能多的字符.也就是说,编译器将程序分解成符号的方法是:从左到右一个一个字符的读入,如果该字符可能组成一个符号,那么再读入下一个字符时,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号.
按照以上规则,则很容易判断该表达式具体解析成什么样子了,
解析如下:

    (i++)+(i++) +i +i

题外话:虽然以上扩展说明,在使用时,很多人都会尽量避免这样类似不确定的语句,但总归知道一下也不无坏处!

四. 扩展说明 - 编程之路

以下内容引用的是某篇博客内容,感谢博主

计算机组成原理→DOS命令→汇编语言→C语言(不包括C++)、代码书写规范→数据结构、编译原理、操作系统→计算机网络、数据库原理、正则表达式→其它语言(包括C++)、架构……

对学习编程者的忠告: 眼过千遍不如手过一遍! 书看千行不如手敲一行! 手敲千行不如单步一行! 单步源代码千行不如单步对应汇编一行!

VC调试时按Alt+8、Alt+7、Alt+6和Alt+5,打开汇编窗口、堆栈窗口、内存窗口和寄存器窗口看每句C对应的汇编、单步执行并观察相应堆栈、内存和寄存器变化,这样过一遍不就啥都明白了吗。 对VC来说,所谓‘调试时’就是编译连接通过以后,按F10或F11键单步执行一步以后的时候,或者在某行按F9设了断点后按F5执行停在该断点处的时候。
(Turbo C或Borland C用Turbo Debugger调试,Linux或Unix下用GDB调试时,看每句C对应的汇编并单步执行观察相应内存和寄存器变化。)

想要从本质上理解C指针,必须学习汇编以及C和汇编的对应关系。 从汇编的角度理解和学习C语言的指针,原本看似复杂的东西就会变得非常简单!
指针即地址。“地址又是啥?”“只能从汇编语言和计算机组成原理的角度去解释了。” 但我又不得不承认:
有那么些人喜欢或者适合用“先具体再抽象”的方法学习和理解复杂事物; 而另一些人喜欢或者适合用“先抽象再具体”的方法学习和理解复杂事物。
而我本人属前者。

不要企图依赖输出指针相关表达式的值【比如printf(“%p\n”,…)】来理解指针的本质,
而要依赖调试时的反汇编窗口中的C/C++代码【比如void *p=…】及其对应汇编指令以及内存窗口中的内存地址和内存值来理解指针的本质。

这辈子不看内存地址和内存值;只画链表、指针示意图,画堆栈示意图,画各种示意图,甚至自己没画过而只看过书上的图……能从本质上理解指针、理解函数参数传递吗?本人深表怀疑!
这辈子不种麦不收麦不将麦粒拿去磨面;只吃馒头、吃面条、吃面包、……甚至从没看过别人怎么蒸馒头,压面条,烤面包,……能从本质上理解面粉、理解面食吗?本人深表怀疑!!

提醒: “学习用汇编语言写程序” 和“VC调试(TC或BC用TD调试)时按Alt+8、Alt+7、Alt+6和Alt+5,打开汇编窗口、堆栈窗口、内存窗口和寄存器窗口看每句C对应的汇编、单步执行并观察相应堆栈、内存和寄存器变化,这样过一遍不就啥都明白了吗。 (Linux或Unix下可以在用GDB调试时,看每句C对应的汇编并单步执行观察相应内存和寄存器变化。)
想要从本质上理解C指针,必须学习C和汇编的对应关系。” 不是一回事!

不要迷信书、考题、老师、回帖; 要迷信CPU、编译器、调试器、运行结果。
并请结合“盲人摸太阳”和“驾船出海时一定只带一个指南针。”加以理解。
任何理论、权威、传说、真理、标准、解释、想象、知识……都比不上摆在眼前的事实!

有人说一套做一套,你相信他说的还是相信他做的? 其实严格来说这个世界上古往今来所有人都是说一套做一套,不是吗?

不要写连自己也预测不了结果的代码!

电脑内存或文件内容只是一个一维二进制字节数组及其对应的二进制地址;
人脑才将电脑内存或文件内容中的这个一维二进制字节数组及其对应的二进制地址的某些部分看成是整数、有符号数/无符号数、浮点数、复数、英文字母、阿拉伯数字、中文/韩文/法文……字符/字符串、汇编指令、函数、函数参数、堆、栈、数组、指针、数组指针、指针数组、数组的数组、指针的指针、二维数组、字符点阵、字符笔画的坐标、黑白二值图片、灰度图片、彩色图片、录音、视频、指纹信息、身份证信息……

十字链表交换任意两个节点C源代码(C指针应用终极挑战)http://download.csdn.net/detail/zhao4zhong1/5532495