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

时间:2012-12-07 01:36:22

标签: c c99 undefined-behavior function-calls

以下编译并打印“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如果试图修改功能的结果   在下一个序列点之后调用或访问它,行为是   未定义。

我不明白它在哪里说“下一个序列点”。这是怎么回事?

3 个答案:

答案 0 :(得分:10)

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

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

  • 当数组表达式是一元&运算符的操作数时(它产生整个数组的地址);
  • 当它是一元sizeof 的操作数或(从C11开始)_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 ++)对临时工作的生命周期做出了更强有力的保证,因此这些标准下的行为不会被定义。