【问题标题】:Undefined behavior: when attempting to access the result of function call未定义的行为:尝试访问函数调用的结果时
【发布时间】:2012-11-25 04:35:11
【问题描述】:

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

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

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

【问题讨论】:

  • 你从哪里得知这是 UB?
  • 下一个序列点位于完整表达式的末尾,也就是分号 (;)。这里没有未定义的行为。
  • @Luchian:当我读到or to access it after the next sequence point, the behavior is undefined时,我产生了疑问,如何在序列点之后访问函数调用的结果?

标签: c c99 undefined-behavior function-calls


【解决方案1】:

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

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

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

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

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

【讨论】:

    【解决方案2】:

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

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

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

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

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

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

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

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

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

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

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

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

    【讨论】:

    • 从逻辑上讲,我建议这只是一个措辞澄清,而不是真正的改变。我很难相信 C99 标准不能被解释为 bar().c 不是一个对象。
    • @DeadMG:恐怕你已经因为三阴性而失去了我。你是说bar().c C99 中的一个对象吗?
    • @Keith:标准将“对象”定义为“执行环境中的数据存储区域,其内容可以表示值”。所以我认为从函数返回的值仍然可以被认为是一个对象 - 只是一个不能修改并且只能访问(只有生命周期)直到下一个序列点(根据 C99 6.5.2.2/5)的对象。跨度>
    • @MichaelBurr:明智,但不是 C99 标准所说的。这:int func(void) { return 42; } 不会产生 int 类型的 object,而只会产生 int 类型的 value。在没有特定语句的情况下(如 2011 年标准中添加的语句),返回结构的函数也是如此:调用函数会为您提供返回类型的值,而不是返回类型的对象。 C99 在 6.2.4 中定义了对象的生命周期(存储持续时间);它没有说明生命周期在序列点结束的对象。这是 C99 中的一个遗漏,在 C2011 中更正。
    • @Keith:按照这种思路,printf("%d", bar().x); 也将是未定义的行为,因为 . 运算符“指定结构或联合对象的成员”(C99 6.5.2.3/3) .这意味着 6.5.2.3/6 中给出的示例 1 是不正确的。所以不仅仅是 C99 中的一个遗漏,而是一个非规范示例中的相关错误。
    【解决方案3】:

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

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

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

    【讨论】:

    • 那么,如果我有 int foo() { return 42; } int main() { printf("%d", foo()); },这是否也是 C99 中未定义的行为,例如因为函数 foo 返回一个在该序列点之后可能不再可用的右值?
    • @cpx: 不,那里没有未定义的行为 - foo() 返回的值的 copy 是作为参数传递给 printf() 的内容。跨度>
    猜你喜欢
    • 2017-01-04
    • 1970-01-01
    • 1970-01-01
    • 2016-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多