找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 4726|回复: 3

【C】++

[复制链接]

85

主题

175

回帖

3990

积分

用户组: 超级版主

No. 418

UID
418
精华
14
威望
53 点
宅币
1974 个
贡献
1582 次
宅之契约
0 份
在线时间
252 小时
注册时间
2014-8-9
发表于 2017-4-5 12:56:47 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
在这片贴子中,我主要叙述一下C语言中的++(自增)运算符以及一些关于++运算符的扩展内容。
一、先从最基本的++运算符的作用说起。
自增运算符++分前缀++、和后缀++两种。
我们先来看一下ANSI C标准上对于后缀++运算符的说明:
6.5.2.4 Postfix increment and decrement operators
Constraints
        .        1  The operand of the postfix increment or decrement operator shall have qualified or unqualified real or pointer type and shall be a modifiable lvalue. 
Semantics 

        .        2  The result of the postfix ++ operator is the value of the operand. After the result is obtained, the value of the operand is incremented. (That is, the value 1 of the appropriate type is added to it.) See the discussions of additive operators and compound assignment for information on constraints, types, and conversions and the effects of operations on pointers. The side effect of updating the stored value of the operand shall occur between the previous and the next sequence point. 

        .        3  The postfix -- operator is analogous to the postfix ++ operator, except that the value of the operand is decremented (that is, the value 1 of the appropriate type is subtracted from it).

        解释如下(我力求将原文的意思表达出来,而不能漏掉一处信息。所以语言略显生硬,用词略显奇葩。而且在一些地方加了辅助空格来方便断句):
后置递增和递减运算符
约束
        1.后置递增和递减运算符必须拥有量化、非量化实型或者指针类型的具有可修改的左值。
语法
        2.后置++运算符的结果 是 被操作数的值。当取得结果后,被操作数递增。(那就是,值“1”先被转为合适的类型 然后被加到了被操作数的值上。)参见对于“加法运算符”和“复合类型赋值”的内容来获取约束,类型和转换相关的信息,还有该算子对于指针变量的效果的信息。该算子更新存储于变量中的值的副作用 应该出现在 上一个和下一个 序列节点 处。
        3.后置--运算符与后置++运算符类似,而后置--运算符将被操作数的值递减(那就是,被操作数的值减去了 被转为合适类型的值“1”。)

然后,我们接着来看我的对于前缀递增运算符的“蹩脚翻译”。

6.5.3.1 Prefix increment and decrement operators
Constraints
        .        1  The operand of the prefix increment or decrement operator shall have qualified or unqualified real or pointer type and shall be a modifiable lvalue. 
Semantics 

        .        2  The value of the operand of the prefix ++ operator is incremented. The result is the new value of the operand after incrementation. The expression ++E is equivalent to (E+=1). See the discussions of additive operators and compound assignment for information on constraints, types, side effects, and conversions and the effects of operations on pointers. 

        .        3  The prefix -- operator is analogous to the prefix ++ operator, except that the value of the operand is decremented.

前置递增和递减运算符
约束
        1.前置递增和递减运算符必须拥有量化、非量化实型或者指针类型的具有可修改的左值。
(译者注:为啥还是左值?在计算机科学中左值“lvalue”指的是将要被修改为新值的存储地址。有如下C语言代码:int a = 0, b = 2; a = b + 3; 执行完以上两句后,变量a的值从原来的0修改为5,而变量b的值自从赋初值过后未曾改变。所以我们称a是赋值运算“=”的左值。)
语法
        2.前置++运算符将被操作数的值递增。结果是 被操作数的值 递增更新后的 新值。表达式“++E”等同于“(E+=1)”。参见对于“加法运算符”和“复合类型赋值”的内容来获取约束,类型和转换相关的信息,还有该算子对于指针变量的效果的信息。
        3.前置--运算符与前置++运算符类似,而前置--运算符将被操作数的值递减1。

概念过后我们马上上机对后置++和前置++进行实测:
首先我使用一个x86平台的Windows7进行测试,编译环境为Visual C++ 2010.(后续内容中,我们将以上环境简称为“环境1”)
在环境1中我们分别编译4组代码,它们如下:
[A组].
#include <stdio.h>
int main()
{
        int a = 0, b = 1;
        a = b++;
        return 0;
}
[B组].
#include <stdio.h>
int main()
{
        int a = 0, b = 1;
        a = ++b;
        return 0;
}
[C组].
#include <stdio.h>
int main()
{
        int a = 0, b = 1;
        b = b++;
        return 0;
}
[D组].
#include <stdio.h>
int main()
{
        int a = 0, b = 1;
        b = ++b;
        return 0;
}
编译执行完的结果如下:
组别结果
A a == 1; b == 2;
B a == 2; b == 2;
C a == 0; b == 2;
D a == 0; b == 2;

       
如果认真理解过ANSI C标准中的内容,不难理解以上结果。在A组中,执行a=b++; 先将b的值(b == 1)取出作为后置++运算的结果。结果赋值给a后,b再进行递增1的操作。我们将A组在环境1下的反汇编代码给出并作解释:

  1.         ;        int a = 0, b = 1;
  2.         mov dword ptr [a],0  ; 将常量0放入a所处的地址中,因为是32位平台。所以a的地址为dword ptr。按照Intel和MASM的语法,变量a的内存地址写做“[a]”。
  3.         mov dword ptr [b],1  ; 同理,将变量b的值设置为1。
  4.         ;        a = b++;
  5.         mov eax,dword ptr [b]  ; 通过变量b的内存地址取出b的值,并放入寄存器EAX中。此时b的值为1,EAX的值将要被改写为1. eax = b;
  6.         mov dword ptr [a],eax  ; 将寄存器EAX的值放入a的地址中,也就是将“1”写入a。a = EAX;
  7.         mov ecx,dword ptr [b]  ; 通过变量b的内存地址取出b的值,并放入寄存器ECX中。ECX = b;
  8.         add ecx,1  ; 将“1”加到寄存器ECX上。ECX = ECX + 1;
  9.         mov dword ptr [b],ecx  ; 将寄存器ECX的值写入变量b的地址中去。b = ECX;
复制代码

再来看B组。a = ++b; 执行该语句的过程中赋值表达式的右手边子表达式“++b”拥有前缀++运算符。所以根据ANSI C标准,“++b”子表达式的结果等于b的原值递增1后的结果。而赋初值后的变量b值为1。递增后值为2。接着将2赋值给a。我们接着给出B组在环境1下的反汇编结果并加以适当的解释。

  1.         ;        int a = 0, b = 1;
  2.         mov dword ptr [a],0  ; 变量初始化过程。解释略。请参见A组反汇编结果的相关内容。a = 0;
  3.         mov dword ptr [b],1   ; 变量初始化过程。解释略。请参见A组反汇编结果的相关内容。b = 1;
  4.         ;        a = ++b;
  5.         mov eax,dword ptr [b]  ; EAX = b;
  6.         add eax,1  ; EAX = EAX + 1;
  7.         mov dword ptr [b],eax  ;  b = EAX;
  8.         mov ecx,dword ptr [b]  ; ECX = b;
  9.         mov dword ptr [a],ecx ; a = ECX;
复制代码

下面,我们合并解释C组、D组。
[C组在环境1下的反汇编]:

  1.         ;        int a = 0, b = 1;
  2.         mov dword ptr [a],0
  3.         mov dword ptr [b],1  ; b = 1;
  4.         ;        b = b++;
  5.         mov eax,dword ptr [b]  ; EAX = b;
  6.         mov dword ptr [b],eax  ; b = EAX;
  7.         mov ecx,dword ptr [b]  ; ECX = b;
  8.         add ecx,1  ; ECX = ECX + 1;
  9.         mov dword ptr [b],ecx  ; b = ECX;
复制代码

[D组在环境1下的反汇编]:

  1.         ;        int a = 0, b = 1;
  2.         mov dword ptr [a],0  
  3.         mov dword ptr [b],1  ; b = 1;
  4.         ;        b = ++b;
  5.         mov eax,dword ptr [b]  ; EAX = b;
  6.         add eax,1  ; EAX = EAX + 1;
  7.         mov dword ptr [b],eax  ; b = EAX;
  8.         mov ecx,dword ptr [b]  ; ECX = b;
  9.         mov dword ptr [b],ecx  ; b = ECX;
复制代码

        比较C组,D组的反汇编代码。我们不难发现,两组代码还是严格按照ANSI C标准编译的。严格到有读者可能会认为编译器太过死板了。在C组反汇编结果中有两行汇编代码内容形似:EAX = b; b = EAX; 看到这种编译结果的读者可能会咂舌:“这叫什么编译器,这明明可以不写的嘛!写上这两行,不仅仅增加了代码体积,还因为执行两句废话而拖慢了效率!”
        那么我认为:这样子做的话是有必要的。我们将环境1下的编译器分别开 /Os /Ot /GL 优化。这仨优化分别表示“Favor small code 最小代码体积优化”、“Favor fast code 最高代码执行效率优化”和“Whole Program Optimization 全局优化”。当开启“最高代码执行效率优化”,或者关闭该优化并打开“全局优化”时,反汇编结果与先前并无二异。如果打开“最小代码体积优化”,我们会发现仅仅是add eax,1;这一行被优化成了 inc eax; 为什么会出现这种情况?
        首先ANSI C标准必须跟随。因为ANSI/ISO C标准是C语言编译设备最低级标准。谁不满足这个标准,编译的代码就不能称之为C语言。如果不能满足ANSI/ISO C标准写个Hello World都有可能因为编译环境的不同而导致输出结果不同,更别提跨平台移植了。其次,最小代码体积优化的结果考虑到选取指令长度短者优先的问题,而“最高代码执行效率优化”则考虑到要对齐指令长度方便CPU调用执行的问题。所以不管哪种优化都是不能删掉“废话”的。就好比有时候我们的数学证明题要写“有且仅有,当且仅当”。这并不是废话,而是更加规矩、更佳国际化、更佳方便他人理解。如果我们的“人肉编译器”优化掉了所谓的废话,证明数学和写作文就没啥两样了。
        现在我们回到C组的代码中去。通过继续探究C组代码,我将引进两个新术语。在C组代码语句 b = b++; 中,通过仔细观察我们不难发现其中存在的歧义,那就是:对值b的更改到底发生在赋值之前,还是赋值之后,亦或是伴随着赋值现象同时产生?语句产生歧义,说明执行该语句可能导致额外的副作用。在计算机科学当中,副作用(Side-effect)指的是一个函数或者表达式造成了其作用域以外的可见影响。这些影响包括修改了该函数的调用者的一些状态。在语句“b = b++;”中副作用指的是在赋值运算完成后,后缀递增运算符的修改操作数的功能才得以实现。如果将语句换做A组中的:a = b++;  a将接收 “b递增以前”的值。而对b的修改将在“对a赋值完毕后”进行。(这里搞得糊涂的读者可以去看看上面的反汇编代码。)这样后置++运算产生了副作用,因为它影响到了自己作用域/语义以外的事情。另外,我们将一个计算机程序的执行序列中 任意 可以评估 先前已经发生过的所有副作用 的点称为一个 序列节点(squence point)。程序中的序列节点就是在执行下一步前所有的副作用都可以被评估了的点。在C语言中,有7处可以作为序列节点:
        1.在双目逻辑运算符“&& || ”做短路计算以前。和逗号运算符以前。比如表达式 a++ >= 2 && ++b >=3; 中,评估子表达式 a++ >= 2 的时刻即为一个序列节点。
        2.评估三目运算符“?:”之前。比如 a++ == 0 ? a = 1 : a = 2; 评估问号之前的子表达式的时刻是一个序列节点。
        3.在一条完整表达式的末尾。比如 a =1; 流程控制语句 if、switch、while、do-while。还有for循环内的三条子表达式。
        4.在一个函数被调用之前。比如 foo(a++) + bar(b++); 函数foo接受的是a的递增前的原值,但是a在进入foo的函数体之前就需要递增。
        5.在给定初值以前:int a = 5; int b = a++; b的初值必须确定为a的原值或是递增后的a值。
        6.在两个声明序列之间。int x = a++, y = a++;
        7.每一个IO函数需要确定或转换变量类型以前。比如语句 printf(“%d %ul”, a++, l++);
再来看 ANSI C 中对于后置++/--的说明:
The side effect of updating the stored value of the operand shall occur between the previous and the next sequence point.

为了E文不好的同学,再次补上我的蹩脚翻译:该算子更新存储于变量中的值的副作用 应该出现在 上一个和下一个 序列节点 处。
        何处为表达式“b = b++;”的“序列节点”?就是出现副作用的时候。根据7个现象我们发现副作用出现在该表达式的结尾处。所以不难解释C组反汇编的最后一行将递增后的b写入到了b的内存地址中。
  1. mov dword ptr [b],ecx
复制代码

        最后我们要注意的几点是:
1.当递增运算符作用在指针变量上的时候,一次就把该指针变量指向的内存地址加上a个单位。其中a等于该指针变量所指向变量的长度(长度以字节Byte为单位)。
    int a = 2;
    int b = 3;
    int * p = &a;
    *(--p) = 4;
绝大多数Intel编译环境下,执行以上代码后b就变成了4。而并不是野指针导致“任意门”事件发生。欲知原因请看完帖子“整数的存储 https://www.0xaa55.com/forum.php ... &extra=page%3D2 ”后思考之。提示:LSB, LSB……。

2.++也可以作用在double和float类型的变量上。float f = 2.2f; f++; // f++ 相当于 f = f + 1.0f; 执行后 f 约等于3.2.。(浮点运算要损失精度。)
3.++不可以作用在结构体/联合体变量上。但是可以作用在指向结构体/联合体变量的指针上。效果是将指针递增了一个结构体/联合体的大小。
        第一节中我们介绍了++的基本概念及其用法,第二节我们将进一步加深理解C语言中的前/后置递增/递减运算符。

本帖被以下淘专辑推荐:

In the beginning I was not the best.
And the world was also not the best.
But I still know that I am who I am.
Because I think that it is good.
I have been working hard.
I have been keeping growth with the world.
And it was so.
回复

使用道具 举报

85

主题

175

回帖

3990

积分

用户组: 超级版主

No. 418

UID
418
精华
14
威望
53 点
宅币
1974 个
贡献
1582 次
宅之契约
0 份
在线时间
252 小时
注册时间
2014-8-9
 楼主| 发表于 2017-4-5 13:23:58 | 显示全部楼层
二、更复杂的++--表达式以及更好玩的内容
        这一“劫”开始之前,我先总结一下上一节的内容。前置++运算符将被操作变量递增后赋值给后续内容。后置++运算符先将原值赋值后再进行递增。简单来记忆:++在前先加,++在后后加。‘--’类似,只不过把结果递减一。
        这次我们来看看很多个++的情况:还是在环境1中我们分别编译以下2组代码。
[X组]
#include <stdio.h>
int main()
{
        int b = 1;
        b = b+++++b+++b;
        return 0;
}
[Y组]
#include <stdio.h>
int main()
{
        int b = 1;
        b = b ++ + ++ b + ++ b;
        return 0;
}

结果如下:
X组:不能被编译,语法错误。编译器给出Error:C2105 ‘++’ needs l-value. (++需要左值。)
Y组:b == 10;
        如果你没法预测Y组中变量b的结果,没关系。我们将Y组中的代码原封不动,换个平台编译。我们将在作业系统为OSX EI Capitan 10.11开发环境为Xcode 8.2测试Y组代码。我们将这个环境称为:“环境2”。所以环境2中的编译器前端为 Clang 800.0 后端为 Apple LLVM 8.0。
001.png
编译执行后的结果为:b == 8;

        我们先来解释为什么X组会通不过编译。换个思路的话,如果你是编译器,你怎样将 b = b+++++b+++b; 这句表达式进行分词呢?(编译器前端的简介请参见帖子:lex与yacc简介 https://www.0xaa55.com/forum.php ... &extra=page%3D2 )如果你将如上语句看做:b = (b++) + (++b) + (++b); 的话也许会通过编译。但是所有的C编译器都不会这样理解。而恰恰,C编译器将如上语句看做 b = b ++ ++ + b ++ + b; 因为C编译器在词法分析的时候进行“Greedy Matching 贪婪匹配”。也就是说能多加一个字符构成合法标识符绝对不会少加那个字符。下面我们像词法分析器那样逐字扫描表达式“b = b+++++b+++b;”

b = b+++++b+++b;” 1.’b’=>b开头的某种标识符。
b = b+++++b+++b;” 2.’b空格’=>确定为标识符b。
“b = b+++++b+++b;” 3.‘=’=>以等号开始的某种运算符,可能是‘==’也可能是‘=’。
“b = b+++++b+++b;” 4.’=空格’=>确定为赋值运算符,将该词素传给语法分析器。
“b = b+++++b+++b;” 5.’b’=>b开头的某种标识符。
“b = b+++++b+++b;” 6.’b+’=>确定为标识符b。并将b传递给语法分析器。词法分析器保留扫描到的加号。
“b = b+++++b+++b;” 7.‘++’=>确定为++运算符。注意此时词法分析器无法确定这是后缀++。因为词法分析器不保留前面分析到的结果中有变量b。在语法分析器中++被识别为后置++。这一步中词法分析器仅做贪婪匹配,并将++词素传给Parser(语法分析器)。
“b = b+++++b+++b;” 8.‘+’=>加号开头的某种运算符,可能是自增加‘++’,也可能是双目加‘+’。
“b = b+++++b+++b;” 9.’++’=>确定为自增++,传递给Parser。
“b = b+++++b+++b;” 10.’+’=>加号开头的某种运算符,可能是自增加‘++’,也可能是双目加‘+’。
“b = b+++++b+++b;” 11.’+b’=>确定为双目加‘+’。扔给Parser。‘b’保留。识别为b开头的某种标识符。
“b = b+++++b+++b;” 12.’b+’=>确定为标识符b。扔给Parser词素‘b’。保留‘+’待定。
“b = b+++++b+++b;” 13.’++’=>确定为自增运算符‘++’。传给Parser。
“b = b+++++b+++b;” 14.‘+’=>加号开头的某种运算符,可能是自增加‘++’,也可能是双目加‘+’。
“b = b+++++b+++b;” 15.’+b’=>确定为双目加‘+’。扔给Parser。‘b’保留。识别为b开头的某种标识符。
“b = b+++++b+++b;16.’b;’=>确定为标识符b。扔给Parser词素‘b’。保留‘;’。
“b = b+++++b+++b;17.’;’=>语句结束。并告诉Parser语句结束。

        上面,我们用了17步完成了绝大多数C编译器在对表达式“b = b+++++b+++b;”做词法分析时的过程。相信大家已经理解什么是贪婪匹配了。(Tips:贪婪匹配不单单发生在运算符++上面。所有标识符都是贪婪匹配的。当然语法分析器不存在什么贪婪匹配。如果有如下语句: struct b {int a; int b;} B = { 0 }, * PB = &B; 那么 PB空格->a = 1; 没有语法错误。PB-空格>a = 1; 就会出现语法错误。原因是扫描到空格的时候‘-’已经被送到了语法分析器当中。语法分析器是没有“贪婪匹配”这一说法的。语法分析器中,前面是双目减,当前是关系运算符大于。因为‘-’缺少右侧表达式当然出现语法错误。)
        表达式“b = b+++++b+++b;”在词法分析的过程中被贪婪匹配为“b = b ++ ++ + b ++ + b;”。而根据 ANSI C 标准,后置递增和递减运算符必须拥有量化、非量化实型或者指针类型的具有可修改的左值。“b++”具有可以被修改的左值,而“b++ ++”中的后一个‘++’就缺少了可以被修改的左值。不难想象 Visual C++ 给出 C2105 错误。
        接下来解决Y组在环境1和环境2中答案不一致的问题。
我们直接来看环境1下Y组的反汇编结果:

  1.         ;        int a = 0, b = 1;
  2.         1. mov dword ptr [a],0  ; 赋值,不解释。
  3.         2. mov dword ptr [b],1  ; 同上。
  4.         ;        b = b ++ + ++ b + ++ b;
  5.         3. mov eax,dword ptr [b]  ; EAX = b;
  6.         4. add eax,1  ; EAX = EAX + 1;
  7.         5. mov dword ptr [b],eax  ; b = EAX;
  8.         6. mov ecx,dword ptr [b]  ; ECX = b;
  9.         7. add ecx,1  ; ECX = ECX + 1;
  10.         8. mov dword ptr [b],ecx  ; b = ECX;
  11.         9. mov edx,dword ptr [b]  ; EDX = b;
  12.         10. add edx,dword ptr [b]  ; EDX = EDX + b;
  13.         11. add edx,dword ptr [b]  ; EDX = EDX + b;
  14.         12. mov dword ptr [b],edx  ; b = EDX;
  15.         13. mov eax,dword ptr [b]  ; EAX = b;
  16.         14. add eax,1  ; EAX = EAX + 1;
  17.         15. mov dword ptr [b],eax  ; b = EAX;
复制代码

        把Y组代码在环境2中的反汇编结果拿来分析:
(首先要说明一个问题:前面说过“环境2”内OS为OSX 10.11.编译器是Clang。当然我的硬件包括一块Intel Core i7. 那么汇编代码如此不一样,差别在哪里?其实测试ABCD组和测试XY组时我使用的是同一块CPU。ABCD组在环境1中被反汇编为Intel语法的汇编语言。而在环境2中测试Y组程序被反汇编为AT&T语法的汇编语言。而且环境2是客户机上的真实CPU,总线宽度64位。环境1是在真实CPU上虚拟出来的x86环境,模拟了一块总线宽度为32位的CPU。在汇编语言层面上,Intel语法的汇编代码中,MOV EAX,EBX 表示将寄存器EBX中的值拷贝到寄存器EAX中。AT&T语法的汇编代码正好相反 movl %eax, %ebx 表示将寄存器EAX中的值拷贝到寄存器EBX中。‘%’符号是OSX平台上的‘gas(GNU Assembler:GNU汇编器)’为了区别Intel语法与AT&T语法而特有的标记。)

  1.     0x100000f76 <+6>:  movl   $0x0, -0x4(%rbp)
  2.     0x100000f7d <+13>: movl   $0x1, -0x8(%rbp)
  3.     0x100000f84 <+20>: movl   -0x8(%rbp), %ecx
  4.     0x100000f87 <+23>: movl   %ecx, %edx
  5.     0x100000f89 <+25>: addl   $0x1, %edx
  6.     0x100000f8c <+28>: movl   %edx, -0x8(%rbp)
  7.     0x100000f8f <+31>: movl   -0x8(%rbp), %edx
  8.     0x100000f92 <+34>: addl   $0x1, %edx
  9.     0x100000f95 <+37>: movl   %edx, -0x8(%rbp)
  10.     0x100000f98 <+40>: addl   %edx, %ecx
  11.     0x100000f9a <+42>: movl   -0x8(%rbp), %edx
  12.     0x100000f9d <+45>: addl   $0x1, %edx
  13.     0x100000fa0 <+48>: movl   %edx, -0x8(%rbp)
  14.     0x100000fa3 <+51>: addl   %edx, %ecx
  15.     0x100000fa5 <+53>: movl   %ecx, -0x8(%rbp)  
复制代码

下面来逐行解释以上反汇编结果:
<+ 6> 1. a = 0;
<+13> 2. b = 1;
<+20> 3. ECX = b;
<+23> 4. EDX = ECX;
<+25> 5. EDX = EDX + 1;
<+28> 6. b = EDX;
<+31> 7. EDX = b;
<+34> 8. EDX = EDX + 1;
<+37> 9. b = EDX;
<+40> 10. ECX = ECX + EDX;
<+42> 11. EDX = b;
<+45> 12. EDX = EDX + 1;
<+48> 13. b = EDX;
<+51> 14. ECX = ECX + EDX;
<+53> 15. b = ECX;


        汇编代码翻译完毕后,以上问题变成了与“后置++ 在原表达式中 哪一个序列节点上 进行评估 后缀++的结果”的等价问题。
在环境2的反汇编结果中,2~6行更新了一次b递增后的结果。并同时保存了 b 的初值到寄存器ECX中。对应后缀++的递增运算。7~9行再次更新了b递增后的结果。迭代初始值是b内存地址中的内容。保留更新结果在EDX中。对应第一个前置++运算。第10行将b在2~6行之前的初值1加上7~9行执行完毕后的结果。(我猜是为了保证后缀++运算没有影响到第10行。)对应第一个双目加法运算。11~13行又一次更新累加了b地址中的内容。这是第二次更新式的累加运算。应该对应第二个前置++运算符。14~15行将第二个双目加法运算的结果累加到最新的b地址中的内容中去,并完成计算。
        我们来看原表达式“b = b++ + ++b + ++b;”,为了方便理解我们将原表达式做如下标记:

  1. b = (b++) + (++b) + (++b);
  2.   f1  f2  f3 f4   f5 f6
复制代码

        接着我们把f2这个后置++运算分解为累加部分 f2a 和更新部分 f2b. 那么环境2中编译结果相对应的运算顺序则是:f2a, f4, f3, f6, f5, f1. 在环境1的反汇编结果中,3~5行更新了一次b递增后的结果。6~8行再次更新了b递增后的结果。9~13行在前两次更新的基础上完成了两个双目加法运算。14~15行递增并更新了当前内存中的b。所以环境1中编译结果相对应的运算顺序则是:f4, f6, f5, f3, f2a, f1, f2b.
        如果按照C语言运算符优先级顺序,加之从左到右的自上而下的分析,还有ANSI标准对副作用和序列节点的阐述,正确的运算执行序列应该是:f2a, f4, f6, f3, f5, f1, f2b. 首先因为前/后置++ - -拥有相同的运算优先级,具有从左到右的结合性。其优先级高于双目加法运算。所以 f2, f4, f6 依次运算。其次我们依次从左往右计算两个优先级相同的双目加法运算来避免它们之间存在的二义性冲突问题。所以f3与f5之间的执行顺序确定为 f3, f5. 最后执行完毕最低优先级的赋值运算后,根据帖子第一节所述的C语言的7种序列节点其中的第三条:“序列节点可以出现在一条完整表达式的末尾。”。我们将 f2b 放在最后运算。
        我们可以看出,环境1, 2中编译结果的不同是因为二义性文法带来的严重混乱。环境2的执行序列中将f3与f6的优先级忽略,还忽略了f2b运算。环境1下虽然没忽略f2b运算,但是f3与f2a之间颠倒了优先级顺序。

扩展阅读:
1. Keyword:l-value。
2. Keyword:sequence point.
3. Keyword: Side-effect Computer science.
4. Keywords: Assembler, Assembly, GAS, AS.
5. 书籍:《C陷阱与缺陷》。
开心一刻:
我经常把“反汇编”读成“huan fei bian”。觉得搞笑,你也把上文中所有的“反汇编”读成“huan fei bian”试试。最后改不过来别怪我。
写这篇帖子历时5个小时,OSX 因为某些 APP 死机3次。死机时间比高达60%(times per hour)
在完成表达式“b = b+++++b+++b;”的词法分析过程后,你已经具备了人肉词法分析的资质,去找份专业对口的工作吧!
写帖子的5个小时内,我一边补番“约会大作战”一边“huan fei bian”。结果导致番剧一个字也没听到。以后要重新加补5个小时。
In the beginning I was not the best.
And the world was also not the best.
But I still know that I am who I am.
Because I think that it is good.
I have been working hard.
I have been keeping growth with the world.
And it was so.
回复 赞! 靠!

使用道具 举报

1109

主题

1649

回帖

7万

积分

用户组: 管理员

一只技术宅

UID
1
精华
244
威望
743 点
宅币
24180 个
贡献
46222 次
宅之契约
0 份
在线时间
2294 小时
注册时间
2014-1-26
发表于 2017-4-6 07:08:51 | 显示全部楼层
那是“缓废编”,不是“反汇编”
呸我现在看到“反汇编”我都会读错

我是认为怎么写都可以,怎么写舒服怎么写,C语言是语言,编译器是根据这个语言来决定怎么生成指令的。
个人认为从语法层面干涉其生成指令(包括内嵌非SIMD汇编等)并不是可取的经验。因为C语言标准的存在,尽量不要针对特定编译器进行编码。

说起来你的这些例子都挺不错的啊,缓废编的说明也十分详细,嗯。
VC++的编译器对ANSI/C的支持度十分不足,自己的特性一大把,但根本的C特性则缺斤短两。

话说你那些扩展阅读的部分,弄个超链接URL可否?这样读者就可以不用手动输入那些东西去搜索了。
嗯虽然“代搜索”的服务我们也可以不用提供到这种程度的吧(国内用不了谷歌还真是不方便学习呢)

L-value
Sequence point
Side effect (computer science)
GNU Assembler
回复 赞! 靠!

使用道具 举报

85

主题

175

回帖

3990

积分

用户组: 超级版主

No. 418

UID
418
精华
14
威望
53 点
宅币
1974 个
贡献
1582 次
宅之契约
0 份
在线时间
252 小时
注册时间
2014-8-9
 楼主| 发表于 2017-4-30 12:03:45 | 显示全部楼层
我来说一下为什么【Y组】在Clang和VC两个编译器下出现“公说公有理,婆说婆有理”的现象。
我在第二节最后分析了VC的执行顺序和Clang的执行顺序。并且我给出了我对于执行顺序的建议。但是是不是说Clang,VC错的就一定没道理,或者我的建议就一定对呢?
不是的。
我们回头看对于“序列节点”的定义:一个计算机程序的执行序列中 任意 可以评估 先前已经发生过的所有副作用 的点称为一个 序列节点(squence point)。
那么在运算:
  1. b = (b++) + (++b) + (++b);
  2.   f1  f2  f3 f4   f5 f6
复制代码

中,
我可以称序列节点在f2上:因为f2出现修改左值的副作用,我应该在f2执行完毕后立即评估其产生的副作用。
同理我还可以称序列节点在f4,f6上:因为f4,f6出现修改左值的副作用,并且对整句话的语义出现影响,我应该在f4或者f6执行完毕后立即评估它们产生的副作用。
我还可以将以上语句的结束看作是序列节点。
一个“任意”不要紧,但是带来的是序列节点的定义产生歧义,进而使得ANSI/ISO C标准发生歧义。最后导致硬件环境虽然一致但是因为编译环境的不同,相同的代码出现不同的执行结果。
那么遇到类似以上多个++无法评估副作用的问题,我们应该敬而远之,能不写则不写。这就是《C陷阱与缺陷》一书提到的C语言的陷阱与缺陷之一。
In the beginning I was not the best.
And the world was also not the best.
But I still know that I am who I am.
Because I think that it is good.
I have been working hard.
I have been keeping growth with the world.
And it was so.
回复 赞! 靠!

使用道具 举报

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-3-28 21:45 , Processed in 0.046251 second(s), 37 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表