[英]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().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);
}
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
。 根據以上所述,它應該拒絕將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.