cpx*_*cpx 8 c c99 function-calls undefined-behavior
以下编译并打印"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);
}
Run Code Online (Sandbox Code Playgroud)
显然,这似乎引发了一个未定义的行为
C99 6.5.2.2/5如果尝试修改函数调用的结果或在下一个序列点之后访问它,则行为未定义.
我不明白它在哪里说"下一个序列点".这里发生了什么?
Kei*_*son 10
你已经遇到了语言的一个微妙角落.
在大多数情况下,数组类型的表达式被隐式转换为指向数组对象的第一个元素的指针.这里没有适用的例外情况是:
&
符的操作数(产生整个数组的地址)时;sizeof
_Alignof
sizeof arr
产生数组的大小,而不是指针的大小); 和char str[6] = "hello";
不转换"hello"
为char*
.)时(N1570草案错误地添加_Alignof
到例外列表中.事实上,由于不明确的原因,_Alignof
只能应用于类型名称,而不能应用于表达式.)
请注意,有一个隐含的假设:数组表达式首先引用数组对象.在大多数情况下,它确实(最简单的情况是当数组表达式是声明的数组对象的名称时) - 但在这种情况下, 没有数组对象.
如果函数返回结构,则结果结果按值返回.在这种情况下,struct包含一个数组,给我们一个没有相应数组对象的数组值,至少在逻辑上.所以数组表达式衰减到指向...的第一个元素的指针,嗯,......一个不存在的数组对象.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);
Run Code Online (Sandbox Code Playgroud)
现在,您有一个具有自动而非临时存储持续时间的struct对象,因此它在执行printf
调用期间和之后都存在.
序列点出现在完整表达式的末尾 - 即,printf
在此示例中返回时.还有其他情况会出现序列点
实际上,这条规则规定函数临时值不会超出下一个序列点 - 在这种情况下,它在使用后很好地发生,所以你的程序具有相当明确的行为.
这是一个没有明确定义的行为的简单示例:
char* c = bar().c; *c = 5; // UB
Run Code Online (Sandbox Code Playgroud)
这里,序列点在c
创建后得到满足,并且它指向的内存被销毁,但我们然后尝试访问c
,从而产生UB.
在C99中,在评估参数(C99 6.5.2.2/10)之后,在调用函数时有一个序列点.
因此,在bar().c
计算时,它会导致指向char c[7]
返回的结构中数组中第一个元素的指针bar()
.但是,该指针被复制到一个参数(它发生的无名参数)中printf()
,并且当实际调用该printf()
函数时,上面提到的序列点已经发生,所以指针指向的成员可能没有更长的活着.
正如Keith Thomson所提到的,C11(和C++)对临时工作的生命周期提供了更强有力的保证,因此这些标准下的行为不会被定义.