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

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 或(从C11开始)_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调用期间和之后都存在.


Pup*_*ppy 5

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

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

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

char* c = bar().c; *c = 5; // UB
Run Code Online (Sandbox Code Playgroud)

这里,序列点在c创建后得到满足,并且它指向的内存被销毁,但我们然后尝试访问c,从而产生UB.


Mic*_*urr 5

在C99中,在评估参数(C99 6.5.2.2/10)之后,在调用函数时有一个序列点.

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

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