C 赋值语句的求值顺序
C order of evaluation of assignment statement
我遇到过跨平台代码在基本赋值语句上表现不同的情况。
一个编译器首先评估左值,其次评估右值,然后评估赋值。
另一个编译器先做右值,再做左值,然后再赋值。
如果左值影响右值的值,这可能会产生影响,如下例所示:
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,如果先计算右值,则 pmm->m 等同于在堆上分配的 MM 实例。
我的问题是是否有一个 C 标准来确定评估的顺序(没有找到),或者每个编译器都可以选择要做什么。
还有其他类似的陷阱我应该注意吗?
=
表达式求值的语义包括
The side effect of updating the stored value of the left operand is sequenced after the value computations of the left and right operands. The evaluations of the operands are unsequenced.
(C2011,6.5.16/3;已强调)
强调的条款明确允许您在不同编译器编译时观察到程序行为的差异。此外,unsequenced 意味着,即使在程序的同一构建的不同运行中,评估也可以以不同的顺序发生。如果多次调用出现无序求值的函数,则允许在同一程序执行期间的不同调用期间以不同顺序发生求值。
这已经回答了问题,但重要的是要看到更大的图景。修改对象或调用这样做的函数是副作用 (C2011, 5.1.2.3/2)。因此,这项关键规定开始发挥作用:
If a side effect on a scalar object is unsequenced relative to either a different side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined.
(C2011, 6.5/2)
被调用的函数具有修改存储在 main()
的变量 pmm
中的值的副作用,赋值的左侧操作数的计算涉及使用值的值计算pmm
,并且这些未排序,因此行为未定义。
不惜一切代价避免未定义的行为。因为您的程序的行为是未定义的,所以不限于您观察到的两种选择(以防万一还不够糟糕)。 C 标准对其可能执行的操作没有任何限制。相反,它可能会崩溃,将硬盘驱动器的分区 table 归零,或者,如果您有 suitable 硬件,则召唤鼻恶魔。或其他任何东西。其中大部分不太可能,但最好的观点是,如果您的程序有未定义的行为,那么您的程序就是 错误的。
使用简单赋值运算符时:=
,操作数的计算顺序未指定。评估之间也没有序列点。
例如,如果您有两个函数:
*Get() = logf(2.0f);
未指定它们在任何时候的调用顺序,但此行为已完全定义。
一个函数调用会引入一个序列点。它会在参数求值之后和实际调用之前发生。运算符 ;
也会引入一个序列点。这很重要,因为在没有插入序列点的情况下,不得修改对象两次,否则行为未定义。
由于未指定的行为,您的示例特别复杂,并且可能有不同的结果,具体取决于首先评估左操作数还是右操作数。
- 首先计算左操作数。
计算左操作数,指针 pmm
将指向结构 mm
。然后调用该函数,并出现一个序列点。它通过将指针指向分配的内存来修改指针 pmm
,然后由于运算符 ;
而后跟一个序列点。然后它将值 1000 存储到成员 m
,随后是另一个序列点,因为 ;
。函数 returns 100 并将其分配给左操作数,但由于左操作数首先被评估,值 100,它被分配给对象 mm
,更具体地说是它的成员 m
.
mm->m
的值为 100,ppm->m
的值为 1000。这是定义的行为,在序列点之间没有对象被修改两次。
- 首先计算右操作数。
该函数首先被调用,序列点出现,它通过将指针指向新分配的结构来修改指针 ppm
,然后是一个序列点。然后它将值 1000 存储到成员 m
,后跟一个序列点。然后是函数returns。然后计算左操作数,ppm->m
将指向新分配的结构,其成员 m
被修改为赋值 100。
mm->m
将具有值 500,因为它从未被修改,而 pmm->m
将具有值 100。在序列点之间没有对象被修改两次。行为已定义。
我遇到过跨平台代码在基本赋值语句上表现不同的情况。
一个编译器首先评估左值,其次评估右值,然后评估赋值。
另一个编译器先做右值,再做左值,然后再赋值。
如果左值影响右值的值,这可能会产生影响,如下例所示:
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,如果先计算右值,则 pmm->m 等同于在堆上分配的 MM 实例。
我的问题是是否有一个 C 标准来确定评估的顺序(没有找到),或者每个编译器都可以选择要做什么。 还有其他类似的陷阱我应该注意吗?
=
表达式求值的语义包括
The side effect of updating the stored value of the left operand is sequenced after the value computations of the left and right operands. The evaluations of the operands are unsequenced.
(C2011,6.5.16/3;已强调)
强调的条款明确允许您在不同编译器编译时观察到程序行为的差异。此外,unsequenced 意味着,即使在程序的同一构建的不同运行中,评估也可以以不同的顺序发生。如果多次调用出现无序求值的函数,则允许在同一程序执行期间的不同调用期间以不同顺序发生求值。
这已经回答了问题,但重要的是要看到更大的图景。修改对象或调用这样做的函数是副作用 (C2011, 5.1.2.3/2)。因此,这项关键规定开始发挥作用:
If a side effect on a scalar object is unsequenced relative to either a different side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined.
(C2011, 6.5/2)
被调用的函数具有修改存储在 main()
的变量 pmm
中的值的副作用,赋值的左侧操作数的计算涉及使用值的值计算pmm
,并且这些未排序,因此行为未定义。
不惜一切代价避免未定义的行为。因为您的程序的行为是未定义的,所以不限于您观察到的两种选择(以防万一还不够糟糕)。 C 标准对其可能执行的操作没有任何限制。相反,它可能会崩溃,将硬盘驱动器的分区 table 归零,或者,如果您有 suitable 硬件,则召唤鼻恶魔。或其他任何东西。其中大部分不太可能,但最好的观点是,如果您的程序有未定义的行为,那么您的程序就是 错误的。
使用简单赋值运算符时:=
,操作数的计算顺序未指定。评估之间也没有序列点。
例如,如果您有两个函数:
*Get() = logf(2.0f);
未指定它们在任何时候的调用顺序,但此行为已完全定义。
一个函数调用会引入一个序列点。它会在参数求值之后和实际调用之前发生。运算符 ;
也会引入一个序列点。这很重要,因为在没有插入序列点的情况下,不得修改对象两次,否则行为未定义。
由于未指定的行为,您的示例特别复杂,并且可能有不同的结果,具体取决于首先评估左操作数还是右操作数。
- 首先计算左操作数。
计算左操作数,指针 pmm
将指向结构 mm
。然后调用该函数,并出现一个序列点。它通过将指针指向分配的内存来修改指针 pmm
,然后由于运算符 ;
而后跟一个序列点。然后它将值 1000 存储到成员 m
,随后是另一个序列点,因为 ;
。函数 returns 100 并将其分配给左操作数,但由于左操作数首先被评估,值 100,它被分配给对象 mm
,更具体地说是它的成员 m
.
mm->m
的值为 100,ppm->m
的值为 1000。这是定义的行为,在序列点之间没有对象被修改两次。
- 首先计算右操作数。
该函数首先被调用,序列点出现,它通过将指针指向新分配的结构来修改指针 ppm
,然后是一个序列点。然后它将值 1000 存储到成员 m
,后跟一个序列点。然后是函数returns。然后计算左操作数,ppm->m
将指向新分配的结构,其成员 m
被修改为赋值 100。
mm->m
将具有值 500,因为它从未被修改,而 pmm->m
将具有值 100。在序列点之间没有对象被修改两次。行为已定义。