C 返回包含数组的结构
以下是gcc 4.4.4下的简单代码C 返回包含数组的结构,c,linux,gcc,language-lawyer,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); } 很好。使用-std=c99编译时,这两个版本都可以工作。我只是调用未定义的行为,还是标准中的
#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);
}
很好。使用
-std=c99
编译时,这两个版本都可以工作。我只是调用未定义的行为,还是标准中的某些内容发生了更改,从而允许代码在C99下工作?为什么在C89下会崩溃?printf
有点滑稽,因为它是需要执行的函数之一。因此,让我们通过编写一个helper函数bar
来分解它。稍后我们将返回到printf
(我使用的是“gcc(ubuntu4.4.3-4ubuntu5)4.4.3”)
取而代之的是:
bar(foo().f); // error: invalid use of non-lvalue array
好的,这是一个错误。在C和C++中,不允许按值传递数组。您可以通过将数组放在结构中来解决此限制,例如voidbar2(foof){…}
但是我们没有使用这种变通方法——我们不允许按值传入数组。现在,您可能认为它应该衰减为char*
,允许您通过引用传递数组。但是,只有当数组有地址(即是左值)时,衰减才起作用。但是临时变量,比如函数的返回值,生活在一个没有地址的魔域中。因此,您不能使用临时用户的地址&
。简而言之,我们不允许获取临时地址,因此它不能衰减为指针。我们不能通过值传递它(因为它是一个数组),也不能通过引用传递它(因为它是临时的)
我发现以下代码有效:
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位时没有segfault)
我实现了我自己的my_printf
(使用vararg无意义),它在打印char*
指向的字母之前打印了char*
的实际值。我这样称呼它:
my_printf("%s\n", f.f);
my_printf("%s\n", foo().f);
这是我得到的输出():
第一个指针值0xffc14eb3
是正确的(它指向字符“Hello,world!”),但请看第二个0x6c6548
。这是地狱的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位模式编译无警告的代码(在
-Wall-Wextra
下)。这些程序运行时都没有崩溃。这就是我所期望的;代码在我看来很好
也许Ubuntu上的问题是特定版本的GCC中的一个bug,这个bug已经被修复了?我相信C89/C90和C99中都没有定义这个行为
foo().f
是数组类型的表达式,特别是char[25]
。6.3.2.1p3表示:
除非它是sizeof运算符的操作数或一元数
&运算符,或用于初始化数组的字符串文字,或
类型为“类型的数组”的表达式转换为
类型为“指针指向类型”且指向初始值的表达式
数组对象的元素,并且不是左值。如果数组对象
已注册存储类,行为未定义
在这种特殊情况下(由函数返回的结构元素数组)的问题是没有“数组对象”。函数结果由值返回,因此调用foo()
的结果是struct foo
类型的值,foo().f
是char[25]
类型的值(不是左值)
据我所知,这是C(直到C99)中唯一一个可以使用数组类型的非左值表达式的情况。我要说的是,试图访问它的行为没有被遗漏定义,很可能是因为标准的作者(可以理解的是IMHO)没有想到这个案例。在不同的优化设置下,您可能会看到不同的行为
新的2011年C标准通过发明一种新的存储类来修补这个死角。(链接到C11之前的最新草案)如6.2.4p8所示:
具有结构或并集类型的非左值表达式,其中
结构或联合包含数组类型的成员(包括,
递归地,所有包含的结构和联合的成员)引用
具有自动存储持续时间和临时生存期的对象。
它的生存期从表达式求值及其初始值开始
value是表达式的值。当
对包含完整表达式或完整声明符的的求值结束。
任何使用临时生存期修改对象的尝试都会导致
未定义的行为
因此,程序的行为在C11中有很好的定义。直到你得到C
Foo f = foo();
bar(f.f);
my_printf("%s\n", f.f);
my_printf("%s\n", foo().f);
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
[...]
int main(void ) {
struct Foo temp = foo();
printf("%s\n", temp.f);
}