前兩天看程序,發現在某個函數中有下面這段程序:
int n; //define a variable n
int array[n]; //define an array with length n
在我所學的C語言知識中,這種數組的定義在編譯時就應該有問題的,因為定義數組時,數組的長度必須要是一個大於0的整型字面值或定義為 const 的常量。例如下面這樣
int array1[10]; //valid
int const N = 10;
int array2[N]; //valid
int n = 10;
int array3[n]; //invalid
但從上面看第三種定義數組的方法也是正確的,於是,我用 gcc 去編譯這段程序,發現確實沒報錯,而且我對此數組進行一些操作,結果也都是正確!這簡直顛覆了我的知識框架!難道大學老師教我的、我平時看的書,都是錯誤的嗎?!我開始尋找答案...
C 語言中變長數組
最官方的解釋應該是 C 語言的規范和編譯器的規范說明了。
- 在 ISO/IEC9899 標准的 6.7.5.2 Array declarators 中明確說明了數組的長度可以為變量的,稱為變長數組(VLA,variable length array)。(注:這里的變長指的是數組的長度是在運行時才能決定,但一旦決定在數組的生命周期內就不會再變。)
- 在 GCC 標准規范的 6.19 Arrays of Variable Length 中指出,作為編譯器擴展,GCC 在 C90 模式和 C++ 編譯器下遵守 ISO C99 關於變長數組的規范。
這下,終於安心了,原來這種語法確實是 C 語言規范,GCC 非常完美的支持了 ISO C99。但令人遺憾的是,我們的大學老師教給我們的還是老一套,雖然關系不是很大,但這也從側面反映了我們的教育是多么地滯后!而且我們讀的 C 語言書,在不加任何限定的條件下,就說某某語法是不對的,讀書的人只能很痛苦地記下!小小吐槽一下,下面繼續...
這種變長數組有什么好處呢?你可以使用 alloca
函數達到類似的動態分配數組的效果,但 alloca 函數分配的空間在函數退出時還依然存在,你需要手動地去釋放所分配的空間 alloca 函數用來在棧上分配空間,當函數返回時自動釋放,無需手動再去釋放;VLA 就不一樣了,在數組名生命周期結束之后,所分配的空間也就隨之釋放。
當然,關於 VLA 還有很多限制,例如 ISO/IEC9899 給出了下面這個例子:
extern int n;
int A[n]; // invalid: file scope VLA
extern int (*p2)[n]; // invalid: file scope VM
int B[100]; // valid: file scope but not VM
void fvla(int m, int C[m][m]); // valid: VLA with prototype scope
void fvla(int m, int C[m][m]) // valid: adjusted to auto pointer to VLA
{
typedef int VLA[m][m]; // valid: block scope typedef VLA
struct tag {
int (*y)[n]; // invalid: y not ordinary identifier
int z[n]; // invalid: z not ordinary identifier
};
int D[m]; // valid: auto VLA
static int E[m]; // invalid: static block scope VLA
extern int F[m]; // invalid: F has linkage and is VLA
int (*s)[m]; // valid: auto pointer to VLA
extern int (*r)[m]; // invalid: r has linkage and points to VLA
static int (*q)[m] = &B; // valid: q is a static block pointer to VLA
}
至於上面語法的原因,請參考 ISO/IEC9899 。
GCC 中零長數組
GCC 中允許使用零長數組,把它作為結構體的最后一個元素非常有用,下面例子出自 gcc 官方文檔。
struct line {
int length;
char contents[0];
};
struct line *thisline = (struct line *) malloc (sizeof (struct line) + this_length);
thisline->length = this_length;
從上例就可以看出,零長數組在有固定頭部的可變對象上非常適用,我們可以根據對象的大小動態地去分配結構體的大小。
在 Linux 內核中也有這種應用,例如由於 PID 命名空間的存在,每個進程 PID 需要映射到所有能看到其的命名空間上,但該進程所在的命名空間在開始並不確定(但至少為 init 命名空間),需要在運行是根據 level 的值來確定,所以在該結構體后面增加了一個長度為 1 的數組(因為至少在一個init命名空間上),使得該結構體 pid 是個可變長的結構體,在運行時根據進程所處的命名空間的 level 來決定 numbers 分配多大。(注:雖然不是零長度的數組,但用法是一樣的)
struct pid
{
atomic_t count;
unsigned int level;
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX];
struct rcu_head rcu;
struct upid numbers[1];
};
參考資料
- ISO/IEC9899
- GCC Online Documents