8

以下编译并打印“字符串”作为输出。

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

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

4

3 回答 3

10

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

在大多数情况下,数组类型的表达式会隐式转换为指向数组对象第一个元素的指针。例外情况(此处均不适用)是:

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

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

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

如果函数返回结构,则结构结果由 value返回。在这种情况下,结构包含一个数组,至少在逻辑上给我们一个没有对应数组对象的数组值。所以数组表达式衰减为指向...的第一个元素的指针,呃,嗯,...一个不存在的数组对象。bar().c

2011 ISO C 标准通过引入“临时生命周期”解决了这个问题,它仅适用于“具有结构或联合类型的非左值表达式,其中结构或联合包含具有数组类型的成员”(N1570 6.2.4p8)。这样的对象不能被修改,并且它的生命周期在包含完整表达式或完整声明符的末尾结束。

因此,从 C2011 开始,您的程序的行为已明确定义。该printf调用获取指向数组的第一个元素的指针,该数组是具有临时生命周期的结构对象的一部分;该对象将继续存在,直到printf调用完成。

但是从 C99 开始,行为是未定义的——不一定是因为您引用的子句(据我所知,没有中间序列点),而是因为 C99 没有定义数组对象对于工作printf

如果您的目标是让该程序运行,而不是了解它可能失败的原因,您可以将函数调用的结果存储在显式对象中:

const struct s result = bar();
printf("%s", result.c);

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

于 2012-12-07T02:03:22.187 回答
5

在 C99 中,在对参数进行评估后,调用函数有一个序列点 (C99 6.5.2.2/10)。

因此,当bar().c被评估时,它会产生一个指针,指向由 .char c[7]返回的结构中的数组中的第一个元素bar()。但是,该指针被复制到一个参数(碰巧是一个无名参数)中printf(),并且在实际对printf()函数进行调用时,上面提到的序列点已经发生,因此指针指向的成员可能没有不再活着。

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

于 2012-12-07T02:32:38.517 回答
5

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

实际上,这条规则规定函数临时函数不会超过下一个序列点——在这种情况下,它会在使用后很好地发生,因此您的程序具有非常明确的行为。

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

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

这里,序列点是在c创建之后遇到的,它指向的内存被破坏了,但是我们随后尝试访问c,导致了UB。

于 2012-12-07T01:50:23.503 回答