返回包含数组的struct

时间:2012-01-08 01:50:17

标签: c linux gcc language-lawyer

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下崩溃?

3 个答案:

答案 0 :(得分:16)

我认为C89 / C90和C99中的行为都是未定义的。

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

  

除非是 sizeof 运算符或一元运算符的操作数   &amp; 运算符,或者是用于初始化数组的字符串文字,a   类型为“ type ”的表达式转换为   带有“指向 type 的指针”的表达式,指向初始值   数组对象的元素,而不是左值。如果是数组对象   具有寄存器存储类,行为未定义。

在这种特殊情况下(一个函数返回的结构元素的数组)的问题是没有“数组对象”。函数结果按值返回,因此调用foo()的结果是类型为struct Foofoo().f是值(不是左值)输入char[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 :(得分:13)

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*,允许您通过引用传递数组。但是,只有当数组具有地址(即是左值)时,衰减才有效。但是 temporaries ,例如来自函数的返回值,生活在一个他们没有地址的魔法之地。因此,您无法获取临时地址&。简而言之,我们不允许采用临时的地址,因此它不能衰减到指针。我们无法通过值(因为它是一个数组)传递它,也不能通过引用传递它(因为它是临时的)。

我发现以下代码有效:

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

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

只是要完整,这应该完美无缺:

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

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

printf,32位与64位,奇怪

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

当我第一次编译并运行代码时,它处于64位模式。在我用32位(-m32到gcc)编译之前,我没有看到我的理论的确认。果然,我得到了一个段错误,就像在最初的问题中一样。 (我得到了一些乱码输出,但是在64位时没有段错误。)

我实现了自己的my_printf(使用vararg废话),在尝试打印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位指针或整数。此指针不指向任何合理的位置,因此程序在尝试访问该位置时会崩溃。

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

答案 2 :(得分:0)

在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特定版本中的一个错误,后来被修复了?