繁体   English   中英

返回包含数组的结构

[英]Returning struct containing array

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

我相信C89 / C90和C99中的行为均未定义。

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

除非它是sizeof运算符或一元运算符的操作数,或者是用于初始化数组的字符串文字,否则将类型为“ array of type ”的表达式转换为类型为“ pointer to type ”的表达式,指向数组对象的初始元素,并且不是左值。 如果数组对象具有寄存器存储类,则该行为是不确定的。

在这种特殊情况下(作为函数返回的结构元素的数组)的问题是没有“数组对象”。 函数结果按值返回,因此调用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有点有趣,因为它是使用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 ++中,不允许通过value传递数组。 您可以通过将数组放入结构中来解决此限制,例如void bar2(Foo f) {...}

但是我们没有使用该解决方法-我们不允许按值传递数组。 现在,您可能认为它应该衰减为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位时,我一直得到一些混乱的输出,但没有段错误)。

我实现了自己的my_printf (使用vararg废话),在尝试打印char*指向的字母之前,它打印了char *的实际值。 我这样称呼它:

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

这是我得到的输出( 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位指针或整数。 该指针没有指向任何明智的位置,因此,当它尝试访问该位置时,程序将崩溃。

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

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

也许Ubuntu上的问题是特定版本的GCC中的错误,此错误已得到修复?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM