結構中最後一個元素允許是未知大小的數組,這個數組就是柔性數組。但結構中的柔性數組前面必須至少一個其他成員,柔性數組成員允許結構中包含一個大小可變的數組,sizeof返回的這種結構大小不包括柔性數組的內存。包含柔數組成員的結構用malloc函數進行內存的動態分配,且分配的內存應該大於結構的大小以適應柔性數組的預期大小。柔性數組到底如何使用?
不完整類型
C和C++對於不完整類型的定義是一樣的,不完整類型是這樣一種類型,它缺乏足夠的信息例如長度去描述一個完整的對象。
不完整類型舉例:
前向聲明就是一種常用的不完整類型
struct test; //test 只給出了聲明,沒有給出定義
不完整數據類型必須通過某種方式補充完整,才能使它們進行實例化。否則只能用於定義指針或引用,因為此時實例化的是指針或引用本身,不是base和test對象
一個未知長度的數組也屬於不完整類型:
extern int a[];
extern 關鍵字不能去掉,因為數組的長度未知,不能作為定義出現。不完整類型的數組需要補充完整才能使用。不完整類型的數組可以通過幾種方式補充完整,大括號形式的初始化就是其中的一種方式:
int a[] = { 10,20 };
結構體
首先,我們需要知道——所謂變量,其實是內存地址的一個抽像名字罷了。在靜態編譯的程序中,所有的變量名都會在編譯時被轉成內存地址。機器是不知道我們取的名字的,只知道地址。
所以有了——棧內存區,堆內存區,靜態內存區,常量內存區,我們代碼中的所有變量都會被編譯器預先放到這些內存區中。
有了上面這個基礎,我們來看一下結構體中的成員的地址是什麼?我們先簡單化一下代碼:
struct test{
int i;
char *p;
};
上面代碼中,test結構中i和p指針,在C的編譯器中保存的是相對地址——也就是說,他們的地址是相對於struct test的實例的。如果我們有這樣的代碼:
struct test t;
下面做個實驗:
#include<stdio.h>
struct test{
int i;
char *p;
};
int main(void)
{
struct test t;
printf("%p\n", &t);
printf("%p\n", &(t.i));
printf("%p\n", &(t.p));
return 0;
}
運行結果:
我們可以看到,t.i的地址和t的地址是一樣的,t.p的址址相對於t的地址多了個8。說白了,t.i 其實就是(&t + 0×0), t.p 的其實就是 (&t + 0×8)。0×0和0×8這個偏移地址就是成員i和p在編譯時就被編譯器給hard code了的地址。於是,你就知道,不管結構體的實例是什麼——訪問其成員其實就是加成員的偏移量。
下面再來做個實驗:
#include<stdio.h>
struct test{
int i;
short c;
char *p;
};
int main(void)
{
struct test *pt=NULL;
printf("%p\n", &(pt->i));
printf("%p\n", &(pt->c));
printf("%p\n", &(pt->p));
return 0;
}
運行結果:
注意:上面的pt->p的偏移之所以是0×8而不是0×6,是因為內存對齊了(我在64位系統上)。關於內存對齊,可參看《C語言內存對齊詳解》一文。
柔性數組
柔性數組成員(flexible array member)也叫伸縮性數組成員,這種代碼結構產生於對動態結構體的需求。在日常的編程中,有時候需要在結構體中存放一個長度動態的字符串,一般的做法,是在結構體中定義一個指針成員,這個指針成員指向該字符串所在的動態內存空間,例如:
struct s_test
{
int a;
double b;
char* p;
};
p指向字符串,這種方法造成字符串與結構體是分離的,不利於操作。把字符串和結構體連在一起的話,效果會更好,可以修改如下:
char a[] = "Hello world";
struct s_test *ptest = (struct s_test*)malloc(sizeof(s_test)+streln(a)+1);
strcpy(ptest+1,a);
這樣一來,(char*)(ptestt + 1)就是字符串“hello world”的地址。這時候p成了多余的東西,可以去掉。但是,又產生了另外一個問題:老是使用(char*)(ptestt + 1)不方便。如果能夠找出一種方法,既能直接引用該字符串,又不占用結構體的空間,就完美了,符合這種條件的代碼結構應該是一個非對象的符號地址,在結構體的尾部放置一個0長度的數組是一個絕妙的解決方案。不過,C/C++標准規定不能定義長度為0的數組,因此,有些編譯器就把0長度的數組成員作為自己的非標准擴展,例如:
struct s_test2
{
int a;
double b;
char c[0];
};
c就叫柔性數組成員,如果把ptest指向的動態分配內存看作一個整體,c就是一個長度可以動態變化的結構體成員,柔性一詞來源於此。c的長度為0,因此它不占用test的空間,同時ptest->c就是“hello world”的首地址,不需要再使用(char*)(ptestt + 1)這麼丑陋的語法了。
鑒於這種代碼結構所產生的重要作用,C99甚至把它收入了標准中:
As a special case, the last element of a structure with more than one named member may have an incomplete array type; this is called a flexible array member.
C99使用不完整類型實現柔性數組成員,標准形式是這樣的:
struct s_test
{
int a;
double b;
char c[];
};
c同樣不占用test的空間,只作為一個符號地址存在,而且必須是結構體的最後一個成員。柔性數組成員不僅可以用於字符數組,還可以是元素為其它類型的數組,例如:
struct s_test
{
int a;
double b;
float[];
};
首先,我們要知道,0長度的數組在ISO C和C++的規格說明書中是不允許的。這也就是為什麼在VC++2012下編譯你會得到一個警告:“arning 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)代表是想分配的數據的長度。