C赋值语句的评估顺序

我遇到过跨平台代码在基本赋值语句中表现不同的情况。

一个编译器首先评估Lvalue,然后评估Rvalue,然后评估分配。

另一个编译器首先执行Rvalue,然后执行Lvalue,然后执行赋值。

如果Lvalue影响Rvalue的值,则可能会产生影响,如下例所示:

struct MM { int m; } int helper (struct MM** ppmm ) { (*ppmm) = (struct MM *) malloc (sizeof (struct MM)); (*ppmm)->m = 1000; return 100; } int main() { struct MM mm = {500}; struct MM* pmm = &mm pmm->m = helper(&pmm); printf(" %d %d " , mm.m , pmm->m); } 

上面的例子,行pmm->m = helper(&mm); ,取决于评估的顺序。 如果首先评估左值,则pmm-> m等于mm.m,如果首先计算的Rvalue大于pmm-> m,则等于堆上分配的MM实例。

我的问题是,是否有一个C标准来确定评估的顺序(没有找到任何),或者每个编译器都可以选择做什么。 还有其他类似的陷阱我应该知道吗?

评估=表达式的语义包括

在左右操作数的值计算之后,对更新左操作数的存储值的副作用进行排序。 对操作数的评估是不确定的。

(C2011,6.5.16 / 3;重点补充)

强调的规定明确允许您在不同编译器编译时观察到程序行为的差异。 此外,未经测序的意思是,即使在程序的同一构建的不同运行中,也允许评估以不同的顺序发生。 如果多次调用出现未序列评估的函数,则允许在同一程序执行期间的不同调用期间以不同的顺序进行评估。

这已经回答了这个问题,但重要的是要看到更大的图景。 修改对象或调用执行此操作的函数是副作用(C2011,5.1.2.3/2)。 因此,这项关键条款发挥作用:

如果对标量对象的副作用相对于对同一标量对象的不同副作用或使用相同标量对象的值进行的值计算未进行排序,则行为未定义。

(C2011,6.5 / 2)

被调用的函数具有修改存储在main()的变量pmm的值的副作用,赋值的左侧操作数的评估涉及使用pmm的值进行的值计算,并且这些是未被排序的,因此行为是未定义。

不惜一切代价避免未定义的行为。 由于您的程序行为未定义,因此不限于您观察到的两种替代方案(如果不够糟糕)。 C标准对其可能做的事情没有任何限制。 它可能会崩溃,将您的硬盘驱动器的分区表归零,或者,如果您有合适的硬件,则召唤鼻子恶魔。 或其他任何东西。 大多数这些都不太可能,但最好的观点是,如果你的程序有不确定的行为,那么你的程序是错误的

使用简单赋值运算符时: = ,未指定操作数的计算顺序。 评估之间也没有序列点。

例如,如果您有两个function:

 *Get() = logf(2.0f); 

它没有指定在任何时候调用它们的顺序,但是这种行为是完全定义的。

函数调用将引入序列点。 它将在评估参数之后和实际调用之前发生。 经营者; 还将介绍一个序列点。 这很重要,因为在没有插入序列点的情况下,不得修改对象两次,否则行为未定义。

由于未指定的行为,您的示例特别复杂,并且可能会有不同的结果,具体取决于首先计算左或右操作数。

  1. 首先评估左操作数。

计算左操作数,指针pmm将指向结构mm 。 然后调用该函数,并出现一个序列点。 它通过将指针指向已分配的内存来修改指针pmm ,然后是因为操作符而导致的序列点; 。 然后它将值1000存储到成员m ,然后是另一个序列点,因为; 。 该函数返回100并将其分配给左操作数,但由于首先计算左操作数,值为100,因此将其分配给对象mm ,更具体地说是其成员m

mm->m的值为100, ppm->m的值为1000.这是定义的行为,在序列点之间没有对象被修改两次。

  1. 首先评估右操作数。

首先调用该函数,发生序列点,它通过将指针指向新分配的结构,然后是序列点来修改指针ppm 。 然后它将值1000存储到成员m ,然后是序列点。 然后函数返回。 然后评估左操作数, ppm->m将指向新分配的结构,并且通过为其赋值100来修改其成员m

mm->m将具有值500,因为它从未被修改,并且pmm->m将具有值100.没有对象在序列点之间被修改两次。 行为已定义。