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下崩溃?
答案 0 :(得分:16)
我认为C89 / C90和C99中的行为都是未定义的。
foo().f
是数组类型的表达式,特别是char[25]
。 C99 6.3.2.1p3说:
除非是 sizeof 运算符或一元运算符的操作数 &amp; 运算符,或者是用于初始化数组的字符串文字,a 类型为“ type ”的表达式转换为 带有“指向 type 的指针”的表达式,指向初始值 数组对象的元素,而不是左值。如果是数组对象 具有寄存器存储类,行为未定义。
在这种特殊情况下(一个函数返回的结构元素的数组)的问题是没有“数组对象”。函数结果按值返回,因此调用foo()
的结果是类型为struct Foo
的值,foo().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
。根据以上所述,它应该拒绝将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特定版本中的一个错误,后来被修复了?