【问题标题】:Returning struct containing array返回包含数组的结构
【发布时间】:2012-02-05 04:20:48
【问题描述】:

gcc 4.4.4下的以下简单代码段错误

#include<stdio.h>

typedef struct Foo Foo;
struct Foo {
    char f[25];
};

Foo foo(){
    Foo f = {"Hello, World!"};
    return f;
}

int main(){
    printf("%s\n", foo().f);
}

将最后一行改为

 Foo f = foo(); printf("%s\n", f.f);

工作正常。使用-std=c99 编译时,这两个版本都可以工作。我只是在调用未定义的行为,还是标准中的某些内容发生了更改,从而允许代码在 C99 下工作?为什么在 C89 下会崩溃?

【问题讨论】:

  • 如果你打开警告,我会收到warning: format ‘%s’ expects type ‘char *’, but argument 2 has type ‘char[25]’
  • 请记住,printf 是那些有趣的可变参数函数之一。如果它被传递给一个接受char* 参数的普通函数,那么它应该很好地衰减。但我认为它与printf 的互动很有趣。
  • 不是Invalid use of non-lvalue array
  • 我正在使用“gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3”。你在用什么?
  • C 中,除了按值将其复制到另一个位置(到函数或局部变量中)之外,是否允许您对临时进行任何操作?也许答案是否定的。

标签: c linux gcc language-lawyer


【解决方案1】:

我相信这种行为在 C89/C90 和 C99 中都是未定义的。

foo().f 是数组类型的表达式,具体来说是char[25]C99 6.3.2.1p3 说:

除非它是 sizeof 运算符的操作数或一元 & 运算符,或者是用于初始化数组的字符串文字, 类型为“type 的数组”的表达式被转换为 类型为“pointer to type”的表达式,指向初始 数组对象的元素并且不是左值。如果数组对象 有注册存储类,行为未定义。

在这种特殊情况下(作为函数返回的结构元素的数组)的问题是没有“数组对象”。函数结果是按值返回的,所以调用foo()的结果是struct Foo类型的foo().fchar[25]类型的值(不是左值)。

据我所知,这是 C 语言(直到 C99)中唯一可以使用数组类型的非左值表达式的情况。我会说尝试访问它的行为是由于遗漏而未定义的,可能是因为标准的作者(可以理解为恕我直言)没有想到这种情况。您可能会在不同的优化设置下看到不同的行为。

新的 2011 C 标准通过发明一个新的存储类来修补这个极端情况。 N1570(链接指向 C11 之前的后期草案)在 6.2.4p8 中说:

具有结构或联合类型的非左值表达式,其中 结构或联合包含具有数组类型的成员(包括, 递归地,所有包含的结构和联合的成员)指 具有自动存储期限和临时生命周期的对象。 它的生命周期从计算表达式及其初始值开始 value 是表达式的值。它的生命周期结束时 包含完整表达式或完整声明符的评估结束。 任何尝试修改具有临时生命周期的对象都会导致 未定义的行为。

所以程序的行为在 C11 中得到了很好的定义。但是,在您能够获得符合 C11 的编译器之前,最好的选择可能是将函数的结果存储在本地对象中(假设您的目标是工作代码而不是破坏编译器):

[...]
int main(void ) {
    struct Foo temp = foo();
    printf("%s\n", temp.f);
}

【讨论】:

  • +1。一个问题。 C11 标准是否说明了这些表达式的常量性?我猜您只能将const 指针指向它们?例如bar(&amp;foo());baz(foo().f); 其中 bar 和 baz 分别是 void bar(const Foo *);void baz(const char *);
  • @AaronMcDaid:我不这么认为。如果它是const,那么今天就没有必要尝试修改它具有未定义的行为。
  • 我会反过来解释。 (但我认为这有点模棱两可。)我认为它是“指针指向 const 类型,如果您将其转换为非 const 类型并尝试通过它进行修改,那么行为是未定义的”。我的逻辑是:如果你不应该修改它,那么类型不应该是const吗?
  • .. 但是,简而言之,最后一句话回答了我的基本问题。谢谢@KeithThompson
  • @AaronMcDaid:这就像字符串文字:它们不是const,但修改它们是未定义的。 (这是出于历史原因;我不确定他们为什么没有生成函数结果const。)
【解决方案2】:

printf 有点好笑,因为它是使用varargs 的函数之一。所以让我们通过编写一个辅助函数bar 来分解它。稍后我们将返回printf

(我正在使用“gcc(Ubuntu 4.4.3-4ubuntu5)4.4.3”)

void bar(const char *t) {
    printf("bar: %s\n", t);
}

改为调用它:

bar(foo().f); // error: invalid use of non-lvalue array

好的,这给出了一个错误。在 C 和 C++ 中,不允许按值传递数组。您可以通过将数组放入结构中来解决此限制,例如void bar2(Foo f) {...}

但我们没有使用这种解决方法——我们不允许按值传递数组。现在,您可能认为它应该衰减为char*,允许您通过引用传递数组。但衰减仅在数组具有地址(即是左值)时才有效。但是临时对象,例如函数的返回值,生活在一个没有地址的神奇之地。因此不能临时取地址&amp;。简而言之,我们不允许获取临时地址,因此它不能衰减为指针。我们无法通过值传递它(因为它是一个数组),也不能通过引用传递(因为它是一个临时的)。

我发现以下代码有效:

bar(&(foo().f[0]));

但老实说,我认为这是可疑的。这不是违反了我刚才列出的规则吗?

为了完整起见,这完全可以正常工作:

Foo f = foo();
bar(f.f);

变量f 不是临时变量,因此我们可以(隐式地,在衰减期间)获取它的地址。

printf,32 位与 64 位的对比,以及怪异

我答应再次提及printf。根据上述,它应该拒绝将 foo().f 传递给任何函数(包括 printf)。但是 printf 很有趣,因为它是这些可变参数函数之一。 gcc 允许自己将数组按值传递给 printf。

当我第一次编译和运行代码时,它处于 64 位模式。直到我用 32 位编译(-m32 到 gcc),我才看到我的理论得到证实。果然我得到了一个段错误,就像原来的问题一样。 (在 64 位时,我得到了一些乱码输出,但没有段错误)。

我实现了自己的my_printf(带有可变参数废话),它在尝试打印char* 指向的字母之前打印了char * 的实际值。我这样称呼它:

my_printf("%s\n", f.f);
my_printf("%s\n", foo().f);

这是我得到的输出 (code on ideone):

arg = 0xffc14eb3        // my_printf("%s\n", f.f); // worked fine
string = Hello, World!
arg = 0x6c6c6548        // my_printf("%s\n", foo().f); // it's about to crash!
Segmentation fault

第一个指针值0xffc14eb3 是正确的(它指向字符“Hello, world!”),但请看第二个0x6c6c6548。那是 Hell 的 ASCII 码(倒序 - 小字节序或类似的东西)。它已将数组按值复制到 printf 中,并且前四个字节已被解释为 32 位指针或整数。这个指针没有指向任何合理的地方,因此程序在尝试访问该位置时会崩溃。

我认为这违反了标准,仅仅是因为我们不应该被允许按值复制数组。

【讨论】:

  • 看起来,printf 无法衰减,实际上是按值传递数组。我并不是暗示这种行为是合法的,但printf("%c%c%c%c\n", foo().f); 会打印字符串中的每个sizeof(int)th 字符。我真的很想在标准中找到一些参考来解决这个问题。
  • 我刚刚注意到了同样的事情。现在看到我的答案的结尾。
【解决方案3】:

在 MacOS X 10.7.2 上,GCC/LLVM 4.2.1 ('i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1(基于 Apple Inc. build 5658)(LLVM build 2335.15。 00)') 和 GCC 4.6.1(我构建的)在 32 位和 64 位模式下编译代码没有警告(在-Wall -Wextra 下)。所有程序都运行而不会崩溃。这是我所期望的;代码对我来说看起来不错。

也许 Ubuntu 上的问题是特定版本的 GCC 中的一个已修复的错误?

【讨论】:

  • 我刚刚在 Ubuntu 上尝试使用 gcc 4.6.2(自己编译)。我再次遇到段错误,但输出略有不同。我没有得到我在其他答案中得到的0x6c6c6548 == "Hell"。我得到了0xffffffff。这一切都很奇怪。
  • 我挖出了一个 SuSE VM(Linux ids1150srvr 2.6.16.60-0.21-smp #1 SMP Tue May 6 12:41:02 UTC 2008 i686 i686 i386 GNU/Linux)和 GCC 4.1.2 和测试程序在没有警告的情况下编译并成功运行。所以,这个问题肯定是系统特定的。它当然不仅仅是 Linux 与其他软件的对比。但这不是额外的帮助。如果您正在调用未定义的行为(尽管我认为您可能不是),那么任何事情都可能发生并且它是有效的。
猜你喜欢
  • 2019-05-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多