未定义的行为:尝试访问函数调用的结果时

以下编译并打印“string”作为输出。

#include  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如果尝试修改函数调用的结果或在下一个序列点之后访问它,则行为未定义。

我不明白它在哪里说“下一个序列点”。 这里发生了什么?

你已经遇到了语言的一个微妙角落。

在大多数情况下,数组类型的表达式被隐式转换为指向数组对象的第一个元素的指针。 这里没有适用的例外情况是:

  • 当数组表达式是一元&运算符的操作数时(它产生整个数组的地址);
  • 当它是一元sizeof的操作数或(从C11开始) _Alignof运算符( sizeof arr产生数组的大小,而不是指针的大小); 和
  • 当它是初始化器中用于初始化数组对象的字符串文字时( char str[6] = "hello";不会将"hello"转换为char* 。)

( N1570草案错误地将_Alignof添加到例外列表中。事实上,由于不明确的原因, _Alignof只能应用于类型名称,而不能应用于表达式。)

请注意,有一个隐含的假设:数组表达式首先引用数组对象。 在大多数情况下,它确实(最简单的情况是当数组表达式是声明的数组对象的名称时) – 但在这种情况下, 没有数组对象

如果函数返回结构,则struct返回结果 。 在这种情况下,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); 

现在,您有一个具有自动而非临时存储持续时间的struct对象,因此它在执行printf调用期间和之后都存在。

序列点出现在完整表达式的末尾 – 即,在此示例中printf返回时。 还有其他情况会出现序列点

实际上,这条规则规定函数临时值不会超出下一个序列点 – 在这种情况下,它在使用后很好地发生,所以你的程序具有相当明确的行为。

这是一个没有明确定义的行为的简单示例:

 char* c = bar().c; *c = 5; // UB 

这里,在创建c之后满足序列点,并且它指向的内存被破坏,但我们然后尝试访问c ,从而产生UB。

在C99中,在对参数进行求值(C99 6.5.2.2/10)之后,在调用函数时会有一个序列点。

因此,当计算bar().c ,它会导致指向bar()返回的struct中char c[7]数组中第一个元素的指针。 但是,该指针被复制到printf()的参数(无用的参数printf() ,并且当实际调用printf()函数时,上面提到的序列点已经发生,所以成员即指针指向可能不再活着。

正如Keith Thomson所提到的 ,C11(和C ++)对临时工作的生命周期做出了更强有力的保证,因此这些标准下的行为不会被定义。