指针递增,精确序列

10

我刚开始学习C语言,我了解到

*a = *b;
a++;
b++;

*a++ = *b++

这两个表达式是等价的,但这确实是当这行代码被执行时发生的事情吗?

*a++ = *b++

这被称为什么?有人能澄清编译器如何解释第二行吗?我知道右到左的优先级等等,但有人能精确地写出编译器用于解释这行代码的步骤吗?


1
编译器可以自由地进行任何操作,只要程序的行为正确,因此一般情况下很难对其进行具体翻译。你所能做的就是使用你所用的编译器将其编译,并查看生成的代码 - 这只会告诉你你的特定编译器在这个特定情况下的操作。 - Paul R
1
尝试观察并理解由gcc -S生成的汇编代码。 - Grijesh Chauhan
*a++ = *b++; 中,++ 是后缀操作符。因此,首先将 *b 赋值给 *a,然后执行 b++a++ - Grijesh Chauhan
2
你正在犯一个常见的初学者错误,混淆了优先级副作用顺序。它们实际上与彼此关系很小。当你在C语言中说A() + B() * C()时,并不要求B()C()必须在A()之前调用,仅仅因为*的优先级比+高。只要在*之前调用B()C(),在+之前调用A(),在*之前调用+,函数可以以任何顺序被调用。编译器可以选择满足这些约束条件的任何调用顺序。 - Eric Lippert
2
@GrijeshChauhan:你犯了一个常见的初学者错误,即假设后缀操作具有增量发生的定义点。实际上并没有。符合规范的编译器可以在任何时候执行增量操作。如果你不明白为什么,请仔细阅读我的答案。 - Eric Lippert
3个回答

15
您说过您相信:

You said that you believe that:

*a = *b; a++; b++;

等同于

*a++ = *b++;

但那是错误的,所以你持有错误的信念。让我们纠正你的错误信念。

在第一个情况下,必须发生以下事情:

  • VAR:必须评估*a以产生一个变量,称之为var
  • VAL:必须评估*b以产生一个值,称之为val
  • ASSIGN:必须将val分配给var
  • INCA:必须递增a
  • INCB:必须递增b

编译器如何安排这些的约束条件?

  • VAR和VAL必须在ASSIGN之前发生。
  • ASSIGN必须在INCA之前发生。
  • INCA必须在INCB之前发生。

规则是语句的所有副作用必须在下一条语句开始之前完成。因此,存在两种合法的排序。VAR VAL ASSIGN INCA INCB或VAL VAR ASSIGN INCA INCB。

现在让我们考虑第二种情况。

*a++ = *b++;
我们有相同的五个操作,但它们的顺序约束完全不同,因为它们都在同一语句中,所以关于语句的规则不适用。现在的约束条件是:
  • VAR和VAL必须在ASSIGN之前发生。
  • VAR的评估必须使用a的原始值
  • VAL的评估必须使用b的原始值
请注意,我没有说必须在增量之后进行。相反,我说过必须使用原始值只要使用了原始值,增量就可以在任何时候发生。 例如,生成以下代码也是合法的:
var = a;
a = a + 1; // increment a before assign
*var = *b;
b = b + 1; // increment b after assign

这样做也是合法的:

val = *b;
b = b + 1; // increment b before assign
*a = val;
a = a + 1; // increment a after assign

按照您的建议执行任务,先进行赋值操作,然后按从左到右的顺序增加两个操作数,也是合法的。以及,先进行赋值操作,然后按从右到左的顺序增加两个操作数,也是合法的。

C编译器有广泛的自由度来生成此类表达式的代码。请确保你已经非常清楚了解这一点,因为大多数人都会弄错:仅仅因为++出现在变量后面并不意味着增量晚发生。编译器可以在任何时候进行增量操作,只要编译器确保使用了原始值即可。

这是C和C++的规则。在C#中,语言规范要求赋值左边的副作用先于赋值右边的副作用,并且两者都要在赋值的副作用之前发生。相同的代码在C#中将被要求生成如下:

var_a = a;
a = a + 1;
// must pointer check var_a here
var_b = b;
b = b + 1;
val = *var_b; // pointer checks var_b
*var_a = val;

"指针检查"是指C#要求运行时验证var_a是否为有效的指针的点;换句话说,即确认*var_a实际上是一个变量。如果不是,则必须在评估b之前抛出异常。

同样,C编译器可以采用C#的方式进行,但并非必须。


2
因此,表达式a=*p++有两种执行方式 (1) 首先将 p 赋值给某个 VAR,然后因为 ++ 的优先级更高,所以 p 更新为指向下一个位置,然后在 第二步 中,将 p 的旧值 = VAR 分配给 a,即 a=*VAR。**(2)** 首先将 *p 赋值给 a,因为 ++ 是后缀运算符,然后 p 更新为指向下一个位置。 - Grijesh Chauhan
2
@GrijeshChauhan:正确;在C语言中,任何一种顺序都是合法的。在C#中,规范要求首先产生左侧(如果有)的副作用,然后发生p增量的副作用,接着发生对旧值p解引用的副作用(请记住,在C#中,如果指针无效,则解引用会产生副作用),最后发生赋值的副作用。也就是说,在C#中,子表达式的副作用从左到右发生,运算符的优先级顺序为准。 - Eric Lippert
1
在读完这个答案之前,我从未知道表达式 a = *P++ 可以像 var = p; p = p + 1; a = *var 一样被求值! - haccks
@haccks:同样的想法。我也认为,尽管命名方式有所不同,但这并不意味着前置递增会先写入内存然后再返回结果,也不意味着后置递增会先返回结果再写入内存。但这种误解现在已经被纠正了。 - Destructor
@EricLippert:我认为var=a;在第二种情况下的代码生成是错误的。应该是var=*a;如果我错了,请纠正我。 - Destructor

4

1)

*a = *b;
a++;
b++;

等同于

*a = *b;
a = a+1;
b = b+1

2)

x = *a++

等同于

x = *a;
a = a+1;

并且
*b++ = x

等同于

*b = x;
b = b+1;

so

*a++ = *b++

等同于

*a = *b;
a = a+1;
b = b+1

3)

*(++a) = *(++b)

等同于

a = a+1;
b = b+1
*a = *b;

“x=*a++”是否确实等同于“x=*a;a=a+1;”?这是必须的吗?符合规范的编译器可以将其实现为“temp=a;a=a+1;x=*temp;”吗? - Eric Lippert

3
表达式求值和副作用的具体顺序是未指定的,保证的只有*b++(指针b当前所指向的值)的结果被赋值给*a++(指针a当前所指向的值),并且两个指针都会前进。操作的确切顺序会有所不同。
如果你想知道你的平台如何处理它,你可以查看生成的机器代码,但请注意,它仍然可能因编译器设置或周围代码而有所不同。

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