首先需要指出,对于C语言来说a=a++;这种写法是完全错误的不规范的写法,其结果a到底应该是多少根本不值得讨论,甚至可以认为它的结果是不可预期的。
然后我们再讨论为什么你这里结果会是22:
#include <stdio.h> int main(int argc, char *argv[]) { int a = 22; a = (a++); printf("a = %d
", a); return 0; }
编译执行:
# gcc -o mytest mytest.c -Wall mytest.c: In function ‘main’: mytest.c:6:4: warning: operation on ‘a’ may be undefined [-Wsequence-point] a = (a++); ~~^~~~~~~ # ./mytest a = 22
这里我们已经看到,合格一点的编译器这里已经可以发现你的问题,指出“a=(a++);”不知道你在表达什么。但是编译器只是给出了警告没有直接错误退出,所以很多时候编译器的警告最好不要直接忽视,我还写过一篇关于编译警告的文章,有兴趣可以看一下
言归正传,我们继续看这里为什么是22,而不是你以为的23。为了探寻指令到底是怎么执行的,我们需要查看a=a++;这句C语言语句被编译器编译后的汇编代码:
# gcc -S -o mytest.S mytest.c -Wall # less mytest.S 1 .file "mytest.c" 2 .text 3 .section .rodata 4 .LC0: 5 .string "a = %d
" 6 .text 7 .globl main 8 .type main, @function 9 main: 10 .LFB0: 11 .cfi_startproc 12 pushq %rbp 13 .cfi_def_cfa_offset 16 14 .cfi_offset 6, -16 15 movq %rsp, %rbp 16 .cfi_def_cfa_register 6 17 subq $32, %rsp 18 movl %edi, -20(%rbp) 19 movq %rsi, -32(%rbp) 20 movl $22, -4(%rbp) 21 movl -4(%rbp), %eax 22 leal 1(%rax), %edx 23 movl %edx, -4(%rbp) 24 movl %eax, -4(%rbp) 25 movl -4(%rbp), %eax 26 movl %eax, %esi 27 movl $.LC0, %edi 28 movl $0, %eax 29 call printf 30 movl $0, %eax 31 leave 32 .cfi_def_cfa 7, 8 33 ret 34 .cfi_endproc 35 .LFE0: 36 .size main, .-main 37 .ident "GCC: (GNU) 8.3.1 20190223 (Red Hat 8.3.1-2)" 38 .section .note.GNU-stack,"",@progbits
如上所示,那么a=22; a=a++;对应的就是下面这几行:
20 movl $22, -4(%rbp) 21 movl -4(%rbp), %eax 22 leal 1(%rax), %edx 23 movl %edx, -4(%rbp) 24 movl %eax, -4(%rbp)
第20行,将22赋值到main函数的堆栈-4(%rbp)处,-4(%rbp)对应的就是main函数的临时变量a。这句也就是a=22;
a=a++;对应21~24四行指令,分别如下:
到此就可以看出a为什么是22了,后面就是把a作为参数给printf了,这里printf打印出来的当然是22。
(编译器表示:惊不惊喜?意不意外?没想到我还有这种操作吧[手动滑稽])
注意“这里”这个词。因为我最开始就说过了,这种写法是非常不规范的甚至错误的,其结果往往是无法评估的,不同的编译器不同的系统可能给你编译出不同的结果。所以,讨论其结果并没有意义,记住不要这样写才是有用的。
补充一:对于讨论较多的两个问题我说一下纯个人看法:
即使你对一个编译器做大量的测试,确认其行为在某一版本后一直保持“一致”,这样仍是没有用的。在程序员界有这样一种开玩笑的说法:“A documented bug is a feature, an undocumented feature is a bug.” 意思就是一种被文档和标准明确定义的行为,即使再古怪也可以被认为是一种“特性”。而一个没有被明确定义的行为,即使你用的再“舒服”也是一个定时炸弹。
我来举个例子吧。大家都知道RHEL-8.0现在已经发布了,其实在其开发过程中出现了一个很严重的“特性”问题(问题已经过去了,我觉得应该可以说了),是我在做文件系统相关工作时发现并立即上报后不断力争其得到“修复”的。在一次内部小版本更新后突然很多工具出现使用异常,研究后发现类似[a-z]等写法都不能正常工作了,类似的正则表达式写法不再匹配以前人们“熟知”的范围。
简单看一下一个简单的测试:
$ echo ABCD | egrep '[a-z]' ABCD
发现问题了没?我们所熟知的[a-z]应该是只匹配小写的字母a到字母z的,但是它现在却匹配的更多内容,甚至包括大写字母。为什么呢?
因为glibc在一次更新后终于加入了它认为应有的一次“完善”工作,这种完善可以让其语义更明确,但是却导致在不同的语言环境下类似[a-z]这种表达将具有不同的意义。我所熟知的[a-z]代表字母a到字母z的理解其实一直是建立在默认LANG=C的语言环境之上的,而如果语言环境是其它的,比如LANG=en_US.utf-8,它的排序不是和ASCII一样的,[a-z]里面不再只是小写26个字母。所以这个正则表达式才匹配了更多。而我们在使用类似[a-z]这种用法时,其实应该是预置语言环境是LANG=C,否则你最好使用例如"[[:lower:]]"这种。
这本来是一次特性的完善更新,结果却发现漫长的使用过程中很多人将这种不规范的写法用的到处都是,现在特性被完善后问题一下子都出来了。所以说不是你一直以来觉得对的用法就一定是对的。不过对于上面这个问题,为了不给customers带来更多的困扰,我还是提议做了妥协,glibc的人最终也在下面这个patch中“修复”了部分问题:
commit 7cd7d36f1feb3ccacf476e909b115b45cdd46e77 Author: Carlos O'Donell <carlos@redhat.com> Date: Wed Jul 25 17:00:45 2018 -0400 Keep expected behaviour for [a-z] and [A-z] (Bug 23393).
决定对如en_US.utf-8等常用默认语言环境维持[a-z]等正则表达式长久以来的“预期结果”,但并不是所有语言环境都是这样的“预期结果”。这个问题虽然最终没有给大客户们带来更多的麻烦。但是对于[a-z]等正则表达式的不全面理解和使用确实是一个隐藏的问题。建议明确定义语言环境再使用。(对上述问题感兴趣的可以尝试使用上游glibc的2.26-27版本,就能看见这个问题了。在正规的操作系统发行版中,这个问题应该是已经“修复”的)。
2. 一个语言在某一新版本规范中定义了此前未定义的行为,那是否代表我就可以无顾忌的使用了?
我觉得还是应该酌情考虑,尽量不要使用兼容性不好的“特性”。一个特别新的“特性”,在它没有大范围普及和替换原有版本前,最好别太早将其引入项目(特别是如果这个“特性”除了能让你耍帅以外并不能带来任何大的好处的时候。)。如果你过早引入一个语言特性,则必须明确限定编译环境。比如像a=a++这种问题,如果你觉得新规范已经定义了,于是你在项目中使用这样的写法,并且预期其等于规范中的结果。但是如果其它人不知道你在项目中引入了只有最新标准才定义的行为,然后获取源码后在别的很多地方大量编译使用,那么问题将是严重的。
作为工程师,我们除了遵守规范来做产品以外,还要考虑产品的适用性普遍性以及对已有用户的影响等很多因素。就像上面那个[a-z]的例子,glibc那些定义规则的大佬们觉得自己这么多年来终于将自己长久以来没有完善的一个特性完善了。他们喜出望外,觉得自己特别了不起。但没想到他们却打破了大部分用户日积月累下来了大量他们认为不规范的错误的用法。而这个时候一味的耍牛脾气是没有用的,只能作出折中的妥协,然后让这种新的“规范”在日后慢慢得到普及。
补充二:
唉,被某评论气到了。我一再强调,不要用你以为的方式去理解编译器的行为,更不要瞎抬杠,编译器的行为跟你所学的语言语法里的行为是完全两个概念,否则为什么学完C语言程序设计还需要学编译原理等高阶课程? 老郭有句笑话说的话糙理不糙:“我和火箭专家碰面,我问他火箭是不是火柴点的,对方看我一眼算他输。”
以前宫博就老说我,让我多看看书再来和他接着讨论问题,更不要在什么都不知道的时候先质疑他。他不讨厌别人质疑他,他喜欢别人发现他的实质性错误,并指出来大家一起探讨,他只是不喜欢和解释不清的人讨论对方的质疑。我觉得他说的有道理,所以后来每次我要和他讨论问题前,不看够几本相关书籍,都不敢张嘴。就这样,我每次都还要被他鄙视一遍,再回去多看点再回来讨论。我并没有觉得他不尊重“小辈”,反而觉得他是为我好,是值得尊重的,反而是我的行为有“挑衅”他的嫌疑(虽然我不是这么想的)。
咱也不是那不讲道理的人,比如下面这个回答,当时李海怪直接“踩”的我说我写的偏题,XXX比我写的好。但是我看了之后发现那确实是我疏忽了,那咱就得好好感谢人家,然后把回答尽量改好。
类似的还有一些。所以说问问题归问问题,回答归回答,讨论问题归讨论问题,抬杠归抬杠。
最后我补充一下gcc的manual文档里对a=a++类似问题的一点面向使用者的说明(里面明确说到a=a++)
题外话:
很多人表示这种问题的根源应该怪某些大学教材。说实话,我也从事计算机专业相关学习和工作十多年了,我真的没有印象我在哪本教材里看到过类似这样的问题。我从一开始就是从外界的讨论声中听到的,根本没见过一个有“官方”支持的地方公开出现过这种问题。
当然我也不能说是读了万卷书,总是有我没见过的书。所以谁要是找到一半带有这样问题的教材或习题什么的,哪怕是大学老师的板书,请拍照把问题描述清楚的情况下给我看看,我很欢迎。
对于这种类似问题的根源,我觉得不一定要归结给大学教材。因为当一个人在知识不够充足的时候,往往就爱在自己仅掌握的一点非常有局限性的知识里“专研”各种钻牛角尖似的问题。殊不知很多问题和习题并不适合不断深入的“钻”研,它们只是阶段性的让你巩固一下当下的知识而已,并没有过多停留下来“深入”研究展开的必要。要做的应该是继续向前,在掌握更多知识后再时不时回头看,从更高的角度看待问题。
诸如本问题这样的问题,明显就是对一个非常局限的知识点过分“钻研”的结果和产物。至于问题到底是从哪里出来的,我觉得并不重要,因为所有在小角落里过分“钻研”这种问题的人都有可能发出这样的疑问,并深以为然。
更多内容请参阅: