【问题标题】:How do Prefix (++x) and Postfix (x++) operations work?前缀 (++x) 和后缀 (x++) 操作如何工作?
【发布时间】:2011-12-10 08:41:56
【问题描述】:

有人能告诉我前缀/后缀运算符是如何工作的吗?我一直在网上寻找很多,但没有找到任何东西。

据我所知,prefex 首先递增,然后执行操作,然后分配。
Postfix 会先做运算,然后赋值,再递增。

但是我的代码有点问题:

int x, y;
x = 1;
y = x + x++; // (After operation y = 2)(x=2)

但是当我这样做时:

y = x++ + x; // (After operation y = 3)(x=2)

我不确定为什么这些操作会有所不同。我有两个问题:

  • 你能解释一下区别吗?

  • 这如何应用于其他运算符前缀?

【问题讨论】:

  • 您真正对哪种语言感兴趣? C# 和 C 在这里的工作方式可能非常不同......
  • @VladLazarenko 不,不是,因为您只修改 x 一次。
  • @LuchianGrigore:它确实是一个 UB,因为这里没有定义参数的评估顺序。
  • @timmy:C# 对操作发生的顺序做出了更多保证。
  • 简短建议:不要使用它们,除非作为 x = x+1 的快捷方式。考虑代码的可读性。

标签: c# c++ c sequence-points


【解决方案1】:

这个问题被问了很多。请注意,每次有人问这个问题时,都会有很多人发布错误的答案。很多人对这些运算符的工作方式有错误的想法,包括编写编程书籍并因此向他人传授谎言的人。请仔细阅读此处的其他答案。

有关 C# 行为的正确分析,请参阅:

What is the difference between i++ and ++i?

对于 C++,任何行为都是正确的行为,在您观察到副作用的情况下。 C++ 没有定义增量的副作用何时可见。 任何两个编译器都可以做不同的事情。

要遵循的一个好的规则是不要依赖任何语言中副作用发生的顺序,但当然在 C++ 中不要依赖它,因为它不可靠。

查看您的具体情况:

int x, y;     
x = 1;     
y = x + x++; 

您报告 x 和 y 都是 2。这在 C# 中是正确的。在 C# 中,正确的行为是:

  • 将 y 视为变量
  • 将 x 计算为一个值——它是 1
  • 将 x++ 评估为一个值。这会将 x 作为一个变量进行计算,然后获取其原始值 1,然后将该值递增 2,然后将 2 分配给 x,然后得到原始值 1。
  • 计算 1 + 1,即 2
  • 将 2 分配给 y。

所以 x 和 y 在 C# 中都是 2。

C++ 可以做同样的事情,但允许以从右到左的顺序计算加法。也就是说,允许这样做:

  • 将 x++ 评估为一个值。这会将 x 作为一个变量进行计算,然后获取其原始值 1,然后将该值递增 2,然后将 2 分配给 x,然后得到原始值 1。
  • 将 x 计算为一个值——它是 2
  • 计算 1 + 2,即 3
  • 将 y 视为变量
  • 将 3 分配给 y。

C++ 也可以这样做:

  • 将 x++ 评估为一个值。这会将 x 作为一个变量进行计算,然后取其原始值 1,然后将该值递增 2...此处缺少步骤...然后得到原始值 1。
  • 将 x 计算为一个值——它是 1
  • 计算 1 + 1,即 2
  • 将 2 分配给 x -- 之前缺少的步骤。
  • 将 y 视为变量
  • 将 2 分配给 y。

因此,在 C++ 中,您可以将 y 设为 3 或 2,这取决于编译器编写者的心血来潮。在 C# 中,您总是知道 y 是 2。在 C++ 中,增量的赋值可以随时发生,只要它确实发生了。在 C# 中,增量的赋值必须发生在之后计算增量值并且之前使用原始值。 (当从正在执行的线程中观察时;如果您试图从另一个或多个线程中观察这些东西,那么所有的赌注都没有了。)

在你的第二个例子中:

y = x++ + x; 

在 C# 中,要求的行为是:

  • 将 y 视为变量
  • 将 x++ 评估为一个值。这会将 x 作为一个变量进行计算,然后获取其原始值 1,然后将该值递增 2,然后将 2 分配给 x,然后得到原始值 1。
  • 将 x 计算为一个值——它是 2
  • 计算 1 + 2,即 3
  • 将 3 分配给 y。

所以 C# 中的正确答案是 y 是 3,x 是 2。

同样,C++ 可以按任何顺序执行这些步骤。允许 C++ 做:

  • 将 x 计算为一个值——它是 1
  • 将 x++ 评估为一个值。这会将 x 作为一个变量进行计算,然后获取其原始值 1,然后将该值递增 2,然后将 2 分配给 x,然后得到原始值 1。
  • 计算 1 + 1,即 2
  • 将 y 视为变量
  • 将 2 分配给 y。

同样,在 C++ 中,正确的答案是 y 是 2 或 3,这取决于编译器编写者的心血来潮。在 C# 中,正确答案是 y 是 3。

【讨论】:

  • 总而言之:如果你有x++++x,那么x 最好不要在该行的其他任何地方。
  • @MooingDuck:这是一个很好的经验法则。 C++ 中的实际规则是了解“序列点”的规则并遵循它们。 (如果您不知道 C++ 中的“序列点”是什么,请在 Wikipedia 上查找。)
  • @EricLippert 感谢您的详细解释!
  • @timmy:好问题!简短的回答是,C# 编译器会根据具体情况以及是否开启优化来生成各种不同的 MSIL 操作码。但是 IL 当然实际上并没有运行。抖动将这些操作码转换为特定于机器的指令。抖动如何选择分配临时内存取决于它;它注册温度的可能性很大。有关确切的细节,要么(1)编写一堆执行不同类型 ++ 的 C# 程序,看看 ILDASM 和调试器告诉你什么,要么(2)提出一个新问题。
  • @timmy:当然,如果抖动可以证明这样做是安全的,它就不需要分配任何临时的。如果它知道没有观察到值和副作用,它可以简单地注册变量并将其更新为正确的值。显然这是常见的情况;如果您有for(int i = 0; i < count; ++i),那么表达式的值是什么并不重要;唯一可以观察到的是副作用,所以温度可以完全消失。
【解决方案2】:
  • 在 C# 中,+ 的操作数按从左到右的顺序计算。
  • 在 C 和 C++ 中,未指定 + 操作数的计算顺序。

对于 C#,您的示例工作如下:

 y = x + x++;
     ^ x is 1
         ^ x is increased to 2, but the postfix increment returns the old value (1)
 y = 2

 y = x++ + x;
     ^ x becomes 2, but postfix increment returns the old value (1)
           ^ x is now 2 here
 y = 3

【讨论】:

  • 但这并没有真正解决他的问题,在这两个例子中他都使用了后缀。我手边没有 C 或 C# 环境,所以我很好奇其他人要说什么。
  • @MarkByers 感谢 Mark,这是对正在发生的事情的一个非常好的视觉效果。
  • @Matt Greer 实际上确实如此 - x 在“x++”返回之后是 2,而不是在整个语句之后。这仅在提到的语言中的 C# 中定义。在 C/C++ 中,它是未定义的,x 可能为零、无穷大或介于两者之间的任何值,否则您的计算机可能会爆炸。
  • 注意马克的措辞。正如 Mark 所指出的,(在 C# 中)x++ 表示“增加 x,然后返回 x 的旧值。x++ not 的意思是,“返回 x 的值,然后递增x
【解决方案3】:

在 C 和 C++ 中:
输出是未指定

参考 - C++03 标准:

第 5 节:表达式,第 4 段:

除非另有说明 [例如&& 和 ||] 的特殊规则、单个运算符的操作数和单个表达式的子表达式的求值顺序以及副作用发生的顺序是未指定的。

在 C99 第 6.5 节中。

"运算符和操作数的分组由语法指示。72) 除稍后指定(函数调用 ()、&&、||、?: 和逗号运算符)外,子表达式的求值顺序并且副作用发生的顺序都未指定。”

【讨论】:

  • 未指定不是未定义的行为,是吗?我虽然未指定意味着实现定义。 (我可能错了)
  • 不,不:行为不是未指定的,它是未定义的。您忘记了§5/4 的其余部分:“在前一个序列点和下一个序列点之间,标量对象的存储值最多只能通过表达式的评估修改一次。此外,只能访问先验值以确定值被储存起来。” OP的代码违反了第二句。
【解决方案4】:

在这两种情况下,增量都是在使用 x 之后应用的。首先,它被评估如下: y = 1 + 1(递增到 2)

在第二个

y = 1(增加到 2)+ 2。

这就是你得到不同答案的原因。

【讨论】:

    【解决方案5】:

    表达式x++++x 同时具有结果(或值)和副作用

    如果我们将讨论限制在整型操作数上,x++结果 就是 x 的当前值。 副作用是将x 增加1。因此,给定代码

    x = 0;
    y = x++;
    

    结果将是 x == 1 和 y == 0(假设 xy 是整数类型)。

    对于++x结果是 1 加上 x 的当前值。 副作用是将x 增加1。因此,给定代码

    x = 0;
    y = ++x;
    

    结果将是x == y == 1。

    C 和 C++ 与 C# 的区别在于何时计算操作数以及何时应用副作用。 C# 保证表达式中的操作数总是从左到右计算。 C 和 C++ 仅保证 &&||?:、逗号和函数调用 () 运算符的从左到右计算 - 对于所有其他运算符,计算操作数的顺序是 未指定。

    同样,在 C# 中,x++++x 的副作用将在表达式被计算后立即应用,而 C 和 C++ 只要求在下一个 序列点之前应用副作用

    C# 的求值规则保证 x = x++a = b++ * b++a[i] = i++ 等表达式是明确定义的,而 C 和 C++ 语言定义明确表示此类表达式会导致 undefined行为(任何结果都是可能的)。

    【讨论】:

      【解决方案6】:

      x + x++ 和 x++ + x 是您不想依赖的病理性副作用案例的示例。 x++ 和 ++x 都增加 x,但是在添加 x 时,评估的顺序是不确定的 - 编译器可以选择它首先评估的“边”。

      【讨论】:

      • 它不是未定义的。在这两种情况下,您只需修改 x 一次,操作顺序完全由标准定义。
      • @n8wrl 不确定我是否同意。括号总是有帮助的。
      • 括号确实有帮助,但这并不能改变事实。
      • @LuchianGrigore:operators 的执行顺序已定义。 操作数的评估顺序和副作用变得可见的顺序在 C++ 中没有定义。在原始海报给出的情况下,行为实际上是 C++ 规范未指定
      【解决方案7】:

      考虑:

      y = x + x++;
      

      无论它的行为是否已定义(它在 C 和 C++ 中未定义;显然它在 C# 中已明确定义),无论您尝试做什么,都一定有更好的方式来表达它。

      如果你假设严格的从左到右的评估,那么上面可以写成:

      y = x * 2;
      x ++;
      

      对于任何知道=*++ 含义的读者来说,其含义是清晰明确的,并且您的代码的未来维护者不会试图追捕您。

      如果您不相信编译器会生成高效的代码,也可以写成x + xx << 1,但这种不信任通常是错误的。

      如果你坚持,你甚至可以写:

      y = x++ * 2;
      

      这对我个人的口味来说有点简洁,但它仍然是明确的。

      如果您想理解别人的代码(诚然,这是程序员花费大量时间做的事情),那么理解复杂的表达式可能很重要。但是,当您编写自己的代码时,清晰比节省击键(或炫耀您对运算符优先级图表的了解程度)更重要。

      【讨论】:

        猜你喜欢
        • 2016-06-17
        • 2012-06-16
        • 1970-01-01
        • 2020-07-06
        • 2012-10-12
        • 2018-02-14
        • 2012-08-24
        • 1970-01-01
        • 2021-11-19
        相关资源
        最近更新 更多