返回包含数组的struct

Dav*_*ave 23 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);
}
Run Code Online (Sandbox Code Playgroud)

将最后一行更改为

 Foo f = foo(); printf("%s\n", f.f);
Run Code Online (Sandbox Code Playgroud)

工作良好.这两个版本在编译时都有效-std=c99.我只是调用未定义的行为,或者标准中的某些内容已更改,这允许代码在C99下工作?为什么在C89下崩溃?

Kei*_*son 16

我相信在C89/C90和C99中都没有定义这种行为.

foo().f是一个数组类型的表达式,具体而言char[25]. C99 6.3.2.1p3说:

除了当它是的操作数的sizeof操作者或一元 操作者,或是用于初始化一个数组,其具有输入"的阵列的表达一个字符串文字类型 "被转换为与类型"指针到表达 "即指向数组对象的初始元素,而不是左值.如果数组对象具有寄存器存储类,则行为未定义.

在这种特殊情况下(一个函数返回的结构元素的数组)的问题是没有"数组对象".函数结果按值返回,因此调用的结果foo()是type 的struct Foo,并且foo().f是类型的值(不是左值)char[25].

据我所知,这是C(最高为C99)中唯一可以有数组类型的非左值表达式的情况.我会说,尝试访问它的行为是由于遗漏而未定义,可能是因为该标准的作者(可以理解的是恕我直言)没有想到这种情况.您可能会在不同的优化设置中看到不同的行为.

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

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

因此,程序的行为在C11中得到了很好的定义.但是,在您能够获得符合C11的编译器之前,最好的办法是将函数的结果存储在本地对象中(假设您的目标是使用代码而不是破坏编译器):

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


Aar*_*aid 13

printf有点好笑,因为它是那些需要varargs的功能之一.所以让我们通过编写辅助函数来分解它bar.我们稍后再回来printf.

(我正在使用"gcc(Ubuntu 4.4.3-4ubuntu5)4.4.3")

void bar(const char *t) {
    printf("bar: %s\n", t);
}
Run Code Online (Sandbox Code Playgroud)

然后调用它:

bar(foo().f); // error: invalid use of non-lvalue array
Run Code Online (Sandbox Code Playgroud)

好的,这给出了一个错误.在C和C++中,不允许按值传递数组.例如,您可以通过将数组放在结构中来解决此限制void bar2(Foo f) {...}

但我们没有使用该解决方法 - 我们不允许按值传递数组.现在,您可能认为它应该衰减为a char*,允许您通过引用传递数组.但是,只有当数组具有地址(即是左值)时,衰减才有效.但临时性,例如来自功能的返回值,生活在一个他们没有地址的魔法之地.因此你不能拿&一个临时的地址.简而言之,我们不允许采用临时的地址,因此它不能衰减到指针.我们无法通过值(因为它是一个数组)传递它,也不能通过引用(因为它是临时的)传递它.

我发现以下代码有效:

bar(&(foo().f[0]));
Run Code Online (Sandbox Code Playgroud)

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

只是为了完成,这应该完美地工作:

Foo f = foo();
bar(f.f);
Run Code Online (Sandbox Code Playgroud)

变量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);
Run Code Online (Sandbox Code Playgroud)

这是我得到的输出(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
Run Code Online (Sandbox Code Playgroud)

第一个指针值0xffc14eb3是正确的(它指向字符"Hello,world!"),但请看第二个0x6c6c6548.这是ASCII代码Hell(反向顺序 - 小字节序或类似的东西).它已按值将数组复制到printf中,前四个字节已被解释为32位指针或整数.此指针不指向任何合理的位置,因此程序在尝试访问该位置时崩溃.

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