gcc 0長陣列學習

KingsLanding發表於2014-04-04

首先,我們要知道,0長度的陣列在ISO C和C++的規格說明書中是不允許的。這也就是為什麼在VC++2012下編譯你會得到一個警告:“warning C4200: 使用了非標準擴充套件 : 結構/聯合中的零大小陣列”。

那麼為什麼gcc可以通過而連一個警告都沒有?那是因為gcc 為了預先支援C99的這種玩法,所以,讓“零長度陣列”這種玩法合法了。關於GCC對於這個事的文件在這裡:“Arrays of Length Zero”,文件中給了一個例子(我改了一下,改成可以執行的了):

#include <stdlib.h>
#include <string.h>
 
struct line {
   int length;
   char contents[0]; // C99的玩法是:char contents[]; 沒有指定陣列長度
};
 
int main(){
    int this_length=10;
    struct line *thisline = (struct line *)
                     malloc (sizeof (struct line) + this_length);
    thisline->length = this_length;
    memset(thisline->contents, 'a', this_length);
    return 0;
}

 

上面這段程式碼的意思是:我想分配一個不定長的陣列,於是我有一個結構體,其中有兩個成員,一個是length,代表陣列的長度,一個是contents,程式碼陣列的內容。後面程式碼裡的 this_length(長度是10)代表是我想分配的資料的長度。(這看上去是不是像一個C++的類?)這種玩法英文叫:Flexible Array,中文翻譯叫:柔性陣列。

我們來用gdb看一下:

(gdb) p thisline
$1 = (struct line *) 0x601010
 
(gdb) p *thisline
$2 = {length = 10, contents = 0x601010 "\n"}
 
(gdb) p thisline->contents
$3 = 0x601014 "aaaaaaaaaa"

 

我們可以看到:在輸出*thisline時,我們發現其中的成員變數contents的地址居然和thisline是一樣的(偏移量為0×0??!!)。但是當我們輸出thisline->contents的時候,你又發現contents的地址是被offset了0×4了的,內容也變成了10個‘a’。(我覺得這是一個GDB的bug,VC++的偵錯程式就能很好的顯示)

我們繼續,如果你sizeof(char[0])或是 sizeof(int[0]) 之類的零長度陣列,你會發現sizeof返回了0,這就是說,零長度的陣列是存在於結構體內的,但是不佔結構體的size。你可以簡單的理解為一個沒有內容的佔位標識,直到我們給結構體分配了記憶體,這個佔位標識才變成了一個有長度的陣列。

看到這裡,你會說,為什麼要這樣搞啊,把contents宣告成一個指標,然後為它再分配一下記憶體不行麼?就像下面一樣。

struct line {
   int length;
   char *contents;
};
 
int main(){
    int this_length=10;
    struct line *thisline = (struct line *)malloc (sizeof (struct line));
    thisline->contents = (char*) malloc( sizeof(char) * this_length );
    thisline->length = this_length;
    memset(thisline->contents, 'a', this_length);
    return 0;
}

 

這不一樣清楚嗎?而且也沒什麼怪異難懂的東西。是的,這也是普遍的程式設計方式,程式碼是很清晰,也讓人很容易理解。即然這樣,那為什麼要搞一個零長度的陣列?有毛意義?!

這個事情出來的原因是——我們想給一個結構體內的資料分配一個連續的記憶體!這樣做的意義有兩個好處:

第一個意義是,方便記憶體釋放。如果我們的程式碼是在一個給別人用的函式中,你在裡面做了二次記憶體分配,並把整個結構體返回給使用者。使用者呼叫free可以釋放結構體,但是使用者並不知道這個結構體內的成員也需要free,所以你不能指望使用者來發現這個事。所以,如果我們把結構體的記憶體以及其成員要的記憶體一次性分配好了,並返回給使用者一個結構體指標,使用者做一次free就可以把所有的記憶體也給釋放掉。(讀到這裡,你一定會覺得C++的封閉中的解構函式會讓這事容易和乾淨很多)

第二個原因是,這樣有利於訪問速度。連續的記憶體有益於提高訪問速度,也有益於減少記憶體碎片。(其實,我個人覺得也沒多高了,反正你跑不了要用做偏移量的加法來定址)

我們來看看是怎麼個連續的,用gdb的x命令來檢視:(我們知道,用struct line {}中的那個char contents[]不佔用結構體的記憶體,所以,struct line就只有一個int成員,4個位元組,而我們還要為contents[]分配10個位元組長度,所以,一共是14個位元組)

(gdb) x /14b thisline
0x601010:       10      0       0       0       97      97      97      97
0x601018:       97      97      97      97      97      97

 

從上面的記憶體佈局我們可以看到,前4個位元組是 int length,後10個位元組就是char contents[]。

如果用指標的話,會變成這個樣子:

(gdb) x /16b thisline
0x601010:       1       0       0       0       0       0       0       0
0x601018:       32      16      96      0       0       0       0       0
(gdb) x /10b this->contents
0x601020:       97      97      97      97      97      97      97      97
0x601028:       97      97

 

上面一共輸出了四行記憶體,其中,

  • 第一行前四個位元組是 int length,第一行的後四個位元組是對齊。
  • 第二行是char* contents,64位系統指標8個長度,他的值是0×20 0×10 0×60 也就是0×601020。
  • 第三行和第四行是char* contents指向的內容。

從這裡,我們看到,其中的差別——陣列的原地就是內容,而指標的那裡儲存的是內容的地址

 

注:該文轉自酷客,選取了其中的關於0長陣列的部分,以前0長陣列也見過,但是為什麼要用呢,有什麼好處呢,通過該文應該有一個瞭解!

相關文章