返回包含数组的结构体

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", ff); 

工作正常。 两种版本在使用-std=c99编译时都可以工作。 我只是简单地调用未定义的行为,或者在标准中更改了某些内容,这样代码就可以在C99下工作了。 为什么在C89下崩溃?

Solutions Collecting From Web of "返回包含数组的结构体"

我相信C89 / C90和C99中的行为都是不确定的。

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

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

在这个特定情况下(一个数组是一个由函数返回的结构的元素)的问题是没有“数组对象”。 函数结果由值返回,因此调用foo()的结果是struct Foo类型的 ,而foo().fchar[25]类型的值(不是左值)。

就我所知,这是C中唯一的情况(高达C99),你可以有一个数组类型的非左值表达式。 我想说,试图访问它的行为是不确定的,可能是因为标准的作者(恕我直言,恕我直言)没有想到这种情况。 您可能会在不同的优化设置下看到不同的行为。

新的2011 C标准通过发明一个新的存储类来修补这个角落案例。 N1570 (链接是C11以前的草稿)在6.2.4p8中说:

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

所以程序的行为在C11中已经很好的定义了。 然而,直到你能够得到一个符合C11的编译器,最好的办法是将函数的结果存储在本地对象中(假设你的目标是工作代码而不是打破编译器):

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

printf有点有趣,因为它是可变参数的函数之一。 所以让我们通过编写一个辅助函数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* ,允许你通过引用传递数组。 但是,只有数组有地址(即左值),衰减才有效。 但是临时性 ,如功能的返回值,却生活在一个没有地址的神奇的地方。 所以你不能把地址&临时地址。 简而言之,我们不能接受临时地址,因此不能腐蚀指针。 我们无法通过值(因为它是一个数组),也不能通过引用(因为它是一个临时的)。

我发现下面的代码工作:

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

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

而要完成,这完美的作品应该是这样的:

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

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

printf,32位与64位,和怪异

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

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

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

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

这是我得到的输出( 在ideone上的代码 ):

 arg = 0xffc14eb3 // my_printf("%s\n", ff); // 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位指针或整数。 这个指针并不指向任何地方的明智的,因此当它试图访问该位置时程序崩溃。

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

在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位两种模式下编译没有警告(under -Wall -Wextra )的代码。 程序全部运行,不会崩溃。 这是我所期望的; 代码对我来说看起来很好。

也许在Ubuntu上的问题是GCC的特定版本中的一个错误,后来被修复了?