以下编译并打印“string”作为输出。
#include <stdio.h>
struct S { int x; char c[7]; };
struct S bar() {
struct S s = {42, "string"};
return s;
}
int main()
{
printf("%s", bar().c);
}
显然,根据
,这似乎会调用未定义的行为C99 6.5.2.2/5如果试图修改功能的结果 在下一个序列点之后调用或访问它,行为是 未定义。
我不明白它在哪里说“下一个序列点”。这是怎么回事?
答案 0 :(得分:10)
你遇到了语言的一个微妙角落。
在大多数情况下,数组类型的表达式隐式转换为指向数组对象的第一个元素的指针。这里没有适用的例外是:
&
运算符的操作数时(它产生整个数组的地址); sizeof
_Alignof
sizeof arr
产生数组的大小,而不是大小一个指针);和char str[6] = "hello";
不会将"hello"
转换为char*
。)(N1570草稿错误地将_Alignof
添加到例外列表中。事实上,由于原因不明确,_Alignof
只能应用于类型名称,而不是一个表达。)
请注意,有一个隐含的假设:数组表达式首先引用数组对象。在大多数情况下,确实如此(最简单的情况是当数组表达式是声明的数组对象的名称时) - 但在这种情况下,没有数组对象。
如果函数返回结构,则结果按值返回。在这种情况下,struct包含一个数组,给我们一个数组 value ,没有相应的数组 object ,至少在逻辑上。所以数组表达式bar().c
衰减到指向...的第一个元素的指针,嗯,......一个不存在的数组对象。
2011 ISO C标准通过引入“临时生命期”来解决这个问题,该标准仅适用于“具有结构或联合类型的非左值表达式,其中结构或联合 包含数组类型为“(N1570 6.2.4p8)的成员。此类对象可能无法修改,其生命周期在包含完整表达式或完整声明符的末尾结束。
从C2011开始,您的程序行为已明确定义。 printf
调用获取指向数组的第一个元素的指针,该数组是具有临时生命周期的struct对象的一部分;在printf
调用完成之前,该对象将继续存在。
但是从C99开始,行为是未定义的 - 不一定是因为你引用的子句(据我所知,没有插入序列点),但是因为C99没有定义数组对象是printf
工作所必需的。
如果你的目标是使这个程序工作,而不是理解它可能失败的原因,你可以将函数调用的结果存储在一个显式对象中:
const struct s result = bar();
printf("%s", result.c);
现在你有一个带有自动的结构对象,而不是临时,存储持续时间,所以它在执行printf
调用期间和之后都存在。
答案 1 :(得分:5)
序列点出现在完整表达式的末尾 - 即,在此示例中返回printf
时。还有其他情况发生序列点
实际上,这条规则规定函数临时值不会超出下一个序列点 - 在这种情况下,它在使用后很好地发生,所以你的程序具有相当明确的行为。
这是一个没有明确定义的行为的简单示例:
char* c = bar().c; *c = 5; // UB
这里,在创建c
之后满足序列点,并且它所指向的内存被销毁,但我们尝试访问c
,从而产生UB。
答案 2 :(得分:5)
在C99中,在对参数进行求值后,在调用函数时会有一个序列点(C99 6.5.2.2/10)。
因此,当评估bar().c
时,它会导致指向char c[7]
返回的结构中bar()
数组中第一个元素的指针。但是,该指针被复制到printf()
的参数(无效参数),并且当实际调用printf()
函数时,上面提到的序列点已经发生,所以指针所指向的成员可能不再活着。
As Keith Thomson mentions,C11(和C ++)对临时工作的生命周期做出了更强有力的保证,因此这些标准下的行为不会被定义。