编译器编译的顺序是什么?

5

嗯,我想知道编译器“读取”代码的顺序。例如:

假设我有以下代码片段:

int N, M;

N = M = 0;

在这种情况下,编译器会为N和M分别分配一部分内存(int、4个字节),然后在第二行(引发我的疑问的位置),有两种可能:
1. 编译器“读取”N等于M并且都等于零。
2. 编译器“读取”零,将其放入M的内存中,然后获取M的值,即零,并将其放入N的内存中。
换句话说,是从右到左还是从左到右?我不知道我的疑问是否变得清晰了,但在我做的一个测试中:
int i=0; /*I declared the variable i, and assign zero value to it*/

printf("%d", i++); /*Prints 0*/

printf("%d", i); /*Prints 1*/

我理解上面的代码,在第二行,编译器似乎(从我理解的角度)从左到右“读取”,将i的值赋给类型%d,并在打印后递增变量i,因为在第三行它被打印为1。

下面的代码片段反转了++的位置:

int i=0; /*I declared i variable to zero*/

printf("%d", ++i); /*Prints 1*/

printf("%d", i); /*Prints 1*/

在这种情况下,在第二行中(我所理解的),编译器从左到右“阅读”,当编译器读取将要打印的内容时(逗号后留下的内容,这个空间的名称是什么?) ,首先“阅读”++并将该变量在以下情况下增加,即i,然后将%d分配给要打印的内容。
顺序是,编译器“阅读”的顺序是什么?我有些老师告诉我编译器从分号(;)的右边到左边“阅读”,但实际上编译器有一定的顺序吗?如果我上面说的话有错,请纠正我。 (我的英语不是很好)
谢谢!

1
就我理解你的例子,你实际上并不是在询问编译器如何读取/编译代码,而是在询问像 N=M=0; 这样的语句如何被解释。赋值运算符从右到左进行分组,也就是说,这个语句被解释为 N=(M=0); - dyp
1
处理的有效顺序由语言规范定义(或未定义)。 (而且,对于C语言的规则足够模糊且充满陷阱,通常最好不要进行多个赋值等操作。) - Hot Licks
谢谢你的回答,但如果我有以下这样的代码片段:"for(i=0; i<10; i++)",编译器会读取i=0一次,然后检查i是否小于10,然后增加i,但不使用增量值,是吗? - ViniciusArruda
1
@X0R40:一些评论和答案试图向您指出的是,您的问题被表述为“编译器做什么?”然而,编译器只是一个翻译器;它不执行程序。编译器读取和分析程序以找出如何将其翻译成机器语言。这种分析是以理解程序的方式进行的。当程序执行时,它按不同的顺序执行操作。尽管您的措辞似乎更多地涉及执行而不是翻译。 - Eric Postpischil
2
@X0R40:另一个复杂性在于,C程序的含义是它们指定了在抽象计算机中执行过程。当编译器翻译程序时,它可能会生成一个以不同方式执行操作的程序,只要它得到相同的结果。因此,在抽象计算机中程序执行的操作与在实际计算机中执行的操作之间存在差异。 - Eric Postpischil
显示剩余9条评论
4个回答

3
I understand the above code, at the second line, the compiler seems(from what i undestood)"read" from left to right, assigning to the type %d the i value, and after print, the variable i is incremented, because at the third line it is printed as 1.

这不是事情的顺序。

当你调用i++时,i会增加1。然后返回增加之前的值。

所以你的:printf("%d", i++)

实际上执行的是:

int i = somevalue;
int temp = i;
i = i + 1;
printf("%d", temp);

在打印之后,变量i没有被递增。实际上,它是在打印之前递增的。

当您执行以下操作时:printf("%d", ++i)

它会执行以下操作:

int i = somevalue;
i = i + 1;
printf("%d", i);

不同之处在于temp变量。

在这种情况下,你的老师是正确的,因为它从分号右侧向左进行处理,原因如下:

它必须先处理I的值,然后才能打印出来。

实际上,如果将每个操作或指令分解为单独的一行,则它实际上是从上到下进行处理,如上所示。


谢谢你的回答,但如果我有这样的代码片段:"for(i=0; i<10; i++)",编译器会先读取i=0,然后检查i是否小于10,然后递增i,但不使用递增值,是吗?还是编译器以不同的顺序读取"for()"操作? - ViniciusArruda
循环开始时,它将把 I 的值设置为 0。它将检查 I 是否小于 10。如果是,则运行循环体。循环结束时,它将增加 I。然后循环从顶部重新开始,并检查 I 是否小于 10。在 for 循环声明中,使用 I++ 还是 ++I 没有区别,因为值是在循环结束时递增的。如果您使用的变量不是整数,最好使用 ++I。这将摆脱未使用的临时变量。 - Brandon

2
根据C++标准(以及C标准):
赋值运算符(=)和复合赋值运算符都是从右向左分组的。
因此,在这个语句中:
N = M = 0;

编译器将0分配给M,根据标准,返回一个引用左操作数的lvalue。这在您的示例中是0,然后分配给N。

需要注意的是,在这种情况下,由于编译器优化的允许,M的值不必被读取并分配给N。可以同时或以任何顺序将0分配给M和N,因为最终结果与将0分配给M,然后将M分配给N完全相同。 - OmnipotentEntity
@OmnipotentEntity,你是对的,但重要的是要理解操作的顺序。 - Vlad from Moscow
@OmnipotentEntity:在这种情况下,顺序和精确语义并不重要。但请注意,例如在 float f; int i; f = i = 3.5; 中,C标准规定,在C计算模型中,3.5被转换为int,赋值给i,然后该值3,而不是3.5,被赋值给f。赋值的值是被赋的值,而不是右侧的值。 - Eric Postpischil
@EricPostpischil,确实如此!评论中重要的部分是“as if”从句,我本应该加粗它。 :) - OmnipotentEntity

2

为了获得实证证据,我编译了以下代码: int M,N; N = M = 0;

在我的Mac上没有使用编译器选项并对其进行了反汇编:

0000000100000f10    pushq   %rbp
0000000100000f11    movq    %rsp,%rbp
0000000100000f14    movl    %edi,0xfc(%rbp)
0000000100000f17    movq    %rsi,0xf0(%rbp)
0000000100000f1b    movl    $0x00000000,0xe4(%rbp)
0000000100000f22    movl    0xe4(%rbp),%eax
0000000100000f25    movl    %eax,0xe0(%rbp)
0000000100000f28    movl    $0x00000000,0xe8(%rbp)
0000000100000f2f    movl    0xe8(%rbp),%eax
0000000100000f32    movl    %eax,0xec(%rbp)
0000000100000f35    movl    0xec(%rbp),%eax
0000000100000f38    popq    %rbp
0000000100000f39    ret

看起来编译器已经决定将其编译为: N = 0; M = 0;


嗯...我预期会有一个 xor 0xe4(%rbp), 0xe4(%rbp),难道 xor 不能在堆栈上这样操作吗? - OmnipotentEntity
你是怎么做到的?我怎样才能看到这些指令?我在Ubuntu中使用gcc,在Windows中使用cygwin,我只会C语言,但我认为这是汇编语言? - ViniciusArruda
@X0R40 我在我的 Mac 上使用了 otool。我建议你参考这里,了解 Linux 的选项:https://dev59.com/tnRA5IYBdhLWcg3wwwvD - David
@OmnipotentEntity 我没有一个明确的答案来回答你的问题,但我猜测异或运算是在处理器中严格从寄存器中进行操作的。 - David
谢谢,我会尝试一下。不过有个疑问:编译器是否总是生成汇编代码,然后再生成机器码(也就是1和0),还是编译器从不将代码翻译为汇编代码,只翻译为机器码? - ViniciusArruda
@X0R40 在正常情况下,编译器将在可执行文件中输出汇编代码。我建议您参考ELF维基(http://en.wikipedia.org/wiki/Executable_and_Linkable_Format),更好地了解您的程序如何呈现给操作系统以供执行。 - David

2
只有词法分析器从左到右读取源代码。语法解析器构建AST并以各种方式读取它们,具体取决于找到的特定节点。
对于表达式,持有表达式的AST可能会按后序遍历读取,以生成后缀表达式,更适合基于堆栈的求值器(最易实现)。
对于赋值(实际上是表达式),AST先读取并生成RHS,然后是LHS,然后生成写入内存指令。
对于函数调用,AST可以从包含参数表达式的最后一个节点向第一个节点解析(如果使用C调用约定执行调用)。

你有一些材料或网站链接可以让我学习你所说的吗?我从未听说过AST、RHS和LHS,现在正在研究它们。谢谢! - ViniciusArruda
我现在想到的参考书是《编译器原理、技术与工具》。 - mcleod_ideafix

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接