运算符关联性,优先级

Posted

技术标签:

【中文标题】运算符关联性,优先级【英文标题】:Operator associativity, precedence 【发布时间】:2021-05-05 12:40:06 【问题描述】:

我只是想知道,对于以下代码,编译器是否单独使用关联性/优先级或其他一些逻辑来评估。

int i = 0, k = 0;

i = k++;

如果我们根据关联性和优先级进行评估,postfix ++ 的优先级高于=,所以k++(变成1)首先被评估,然后是=,现在k 的值是是1 分配给i

所以ik 的值将是1。但是i is 0k is 1 的值。

所以我认为编译器将这个i = k++; 分成两个(i = k; k++;)。所以这里的编译器不是为了语句关联性/优先级,它也分割了行。有人能解释一下编译器是如何解析这些语句的吗?

【问题讨论】:

" 所以 k++(变成 1)" --> 不完全。 k 变为 1,但其先前的值 0 被“返回”。 “所以我认为编译器将这个i = k++; 分成两个i = k ; k++;。你试过了吗? 其实不需要在编译器层面解释。该行为由 C 语言规范强制执行。来自规范: 后缀 ++ 运算符的结果是操作数的值。作为副作用,操作数对象的值会递增,然后有关优先顺序的部分指定++ 位于= 之前,正如您所指出的。 与优先级无关。我与运算符返回的值有关。 @kaylum,你能告诉我规范的链接吗?那是 C11 (ISO/IEC 9899:2011) 吗? 【参考方案1】:

类似于(只是为了说明增加了一个序列点):

i = k; // i = 0
k = k + 1;  // k = 1

【讨论】:

这不一样,因为它在赋值和增量之间有一个原代码中不存在的序列点。 Stack Overflow 看到很多关于 ++ 运算符的问题重复出现,因为学生不了解排序的重要性和影响,因此正确教授它很重要。【参考方案2】:

后缀运算符的优先级高于赋值运算符。

这个带有赋值运算符的表达式

i = k++

包含两个操作数。

等价的可以改写成

i = ( k++ );

表达式k++ 的值为0。所以变量i会得到0的值。

赋值运算符的操作数可以按任意顺序求值。

根据 C 标准(6.5.2.4 后缀递增和递减运算符)

2 后缀 ++ 运算符的结果是操作数的值。 作为副作用,操作数对象的值递增(即 即,将相应类型的值 1 添加到其中)。

And(6.5.16 赋值运算符)

3 赋值运算符将值存储在由指定的对象中 左操作数。赋值表达式具有左边的值 赋值后的操作数,111) 但不是左值。一个类型 赋值表达式是左操作数之后的类型 左值转换。 更新存储值的副作用 左边的操作数在左边的值计算之后排序 和右操作数。操作数的求值是无序的。

【讨论】:

这篇文章永远不会回答这个问题;它从不解释增量是与主要评估分开的动作。它确实引用了标准,但没有解释它或说明为什么引用是相关的,并且仅在关于优先级和重写表达式的无用文本之后。 (OP对++应用于k这一事实并不感到困惑,这会受到优先级的影响,但关于增量不影响用于k的值这一事实,这是运算符,而不是优先级。)然后这个答案继续引用一个不相关的段落。【参考方案3】:

与 C++ 不同,C 没有“按引用传递”。仅“按值传递”。我将借用一些C++来解释。让我们将后缀和前缀的 ++ 功能实现为常规函数:

// Same as ++x
int inc_prefix(int &x)  // & is for pass by reference
    x += 1;
    return x;


// Same as x++
int inc_postfix(int &x) 
    int tmp = x;
    x += 1;
    return tmp;

所以你的代码现在相当于:

i = inc_postfix(k);

编辑:

对于更复杂的事情,它并不完全等价。例如,函数调用引入了序列点。但以上足以解释 OP 会发生什么。

【讨论】:

显示的代码不等价。函数调用引入了序列点,极大地改变了语义。【参考方案4】:

++ 做了两件事。

k++ 做了两件事:

在执行任何增量之前,它的值为k。 它递增k

这些是分开的:

产生k 的值是i = k++; 主要评估的一部分。 递增k 是一个副作用。它不是主要评估的一部分。程序可能会在计算表达式的其余部分之后或期间增加 k 的值。它甚至可以在表达式的其余部分之前递增该值,只要它“记住”用于表达式的预递增值。

不涉及优先级和关联性。

这实际上与优先级或关联性无关。 ++ 运算符的增量部分始终与表达式的主求值分开。用于k++ 的值始终是增量前k 的值,无论存在哪些其他运算符。

补充

重要的是要了解++ 的增量部分与主要评估分离并且在时间上有点“浮动” - 它没有锚定到代码中的某个位置,并且您无法控制当它发生时。这很重要,因为如果操作数有其他用途或修改,例如在k * k++ 中,则增量可能发生在其他事件的主要评估之前、期间或之后。发生这种情况时,C 标准不会定义程序的行为。

【讨论】:

【参考方案5】:

运算符关联性不适用于此处。运算符优先级仅说明哪个操作数坚持哪个运算符。在这种情况下它并不是特别相关,它只是说表达式应该被解析为 i = (k++); 而不是 (i = k)++; 这没有任何意义。

从那时起,如何评估/执行此表达式由每个运算符的特定规则指定。后缀运算符被指定为 (6.5.2.4):

结果的值计算在副作用之前排序 更新操作数的存储值。

也就是说,k++ 保证评估为 0,然后在稍后的某个时间点,k 增加 1。我们真的不知道什么时候,只知道它发生在k++ 被评估但在下一个序列点之前,在这种情况下,; 在行尾。

赋值运算符的行为类似于 (6.5.16):

更新左操作数的存储值的副作用是 在左右操作数的值计算之后排序。

在这种情况下,= 的右操作数在更新左操作数之前计算其值。

实际上,这意味着可执行文件可以如下所示:

k 被评估为 0 将i设置为0 将k 增加 1 分号/序列点

或者这个:

k 被评估为 0 将k 增加 1 将i设置为0 分号/序列点

【讨论】:

【参考方案6】:

这里的根本问题是优先级不是思考什么的正确方法

i = k=+;

意思。

让我们来谈谈k++ 的真正含义。 k++ 的定义是如果给你k 的旧值,然后将k 的存储值加1。 (或者,换一种说法,它将旧值 k 加 1,然后将其存储回 k,同时为您提供旧值 k。)

就表达式的其余部分而言,重要的是k++ 的值是多少。所以当你说

i = k++;

i 中存储了什么?”问题的答案是,“k 的旧值”。

当我们回答“i 中存储了什么?”这个问题时,我们根本不会考虑优先级。我们思考后缀++运算符的含义。

另见this older question。

后记:您必须真正小心的另一件事是,当您考虑附带问题时,“它何时将新值存储到 k?事实证明这是一个很难回答的问题,因为答案没有你想的那么好。新值在它所在的较大表达式结束之前的某个时间被存储回k(正式地,“在下一个序列点之前”),但我们不知道是否它发生在事物被存储到i 之前或之后,或者在表达式中其他有趣的点之前或之后。

【讨论】:

【参考方案7】:

啊,这是一个很有趣的问题。为了帮助您更好地理解,这就是实际发生的情况。

我将尝试使用 C++ 中的一些运算符重载概念来解释,如果你不懂 C++,请耐心等待。

这就是重载postfix-increment 运算符的方式:

int operator++(int) // Note that the 'int' parameter is just a C++ way of saying that this is the postfix and not prefix operator

    int copy = *this;  // *this just means the current object which is calling the function
    *this += 1;
    return copy;

postfix-increment 运算符的基本作用是创建操作数的副本,增加原始变量,然后返回副本

在您的i = k++ 的情况下,k++ 确实首先发生,但返回的值实际上是k(把它想象成一个函数调用)。然后将其分配给i

【讨论】:

假设他不懂 C++,你如何期望提出 C 问题的人理解你的答案? @Jabberwocky 我想我已经解释了流程执行的实际工作原理。再加上它是一段非常不言自明和直观的代码。我认为任何人都不应该有从其中抽象出细节的问题。【参考方案8】:

优先级和关联性仅影响运算符和操作数相互关联的方式 - 它们不影响表达式的计算顺序。优先规则规定

i = k++

被解析为

i = (k++)

而不是类似的东西

(i = k)++

后缀++ 运算符有一个结果和一个副作用。在表达式中

i = k++

k++结果k 的当前值,它被分配给i副作用是增加k

逻辑上等价于写作

tmp = k
i = tmp
k = k + 1

需要注意的是,对i 的分配和对k 的更新可以以任何顺序发生——这些操作甚至可以相互交错。重要的是i 在增量之前获得k 的值,而k 会增加,不一定是这些操作发生的顺序。

【讨论】:

以上是关于运算符关联性,优先级的主要内容,如果未能解决你的问题,请参考以下文章

Java 表达式顺序、运算符优先级和关联性之间的区别

C/C++ 编译器如何根据运算符的优先级和关联性分离标记?

在中缀表达式中反转运算符优先级

JavaScript中运算符的优先级

Java 运算符优先级

运算符的优先级