1. 程式人生 > >C99可變長陣列VLA詳解

C99可變長陣列VLA詳解

C90及C++的陣列物件定義是靜態聯編的,在編譯期就必須給定物件的完整資訊。但在程式設計過程中,我們常常遇到需要根據上下文環境來定義陣列的情況,在執行期才能確知陣列的長度。對於這種情況,C90及C++沒有什麼很好的辦法去解決(STL的方法除外),只能在堆中建立一個記憶體映像與需求陣列一樣的替代品,這種替代品不具有陣列型別,這是一個遺憾。C99的可變長陣列為這個問題提供了一個部分解決方案。

可變長陣列(variable length array,簡稱VLA)中的可變長指的是編譯期可變,陣列定義時其長度可為整數型別的表示式,不再象C90/C++那樣必須是整數常量表達式。在C99中可如下定義陣列:

int n = 10, m = 20;

char a[n];

int b[m][n];

a的型別為char[n],等效指標型別是char*,b的型別為int[m][n],等效指標型別是int(*)[n]。int(*)[n]是一個指向VLA的指標,是由int[n]派生而來的指標型別。

由此,C99引入了一個新概念:可變改型別(variably modified type,簡稱VM)。一個含有源自VLA派生的完整宣告器被稱為可變改的。VM包含了VLA和指向VLA的指標,注意VM型別並沒有建立新的型別種類,VLA和指向VLA的指標仍然屬於陣列型別和指標型別,是陣列型別和指標型別的擴充套件。

一個VM實體的宣告或定義,必須符合如下三個條件:

1。代表該物件的識別符號屬於普通識別符號(ordinary identifier);

2。具有程式碼塊作用域或函式原型作用域;

3。無連結性。

Ordinary identifier指的是除下列三種情況之外的識別符號:

1。標籤(label);

2。結構、聯合和列舉標記(struct tag、uion tag、enum tag);

3。結構、聯合成員識別符號。

這意味著VM型別的實體不能作為結構、聯合的成員。第二個條件限制了VM不能具有檔案作用域,儲存連續性只能為auto,這是因為編譯器通常把全域性物件存放於資料段,物件的完整資訊必須在編譯期內確定。

VLA不能具有靜態儲存週期,但指向VLA的指標可以。

兩個VLA陣列的相容性,除了滿足要具有相容的元素型別外,決定兩個陣列大小的表示式的值也要相等,否則行為是未定義的。

下面舉些例項來對數種VM型別的合法性進行說明:

#include <stdio.h>

int n = 10;

int a[n];        /*非法,VM型別不能具有檔案作用域*/

int (*p)[n];      /*非法,VM型別不能具有檔案作用域*/

struct test

{

       int k;

       int a[n];     /*非法,a不是普通識別符號*/

       int (*p)[n];   /*非法,p不是普通識別符號*/

};

int main( void )

{

       int m = 20;

       struct test1

       {

              int k;

              int a[n];         /*非法,a不是普通識別符號*/

              int (*p)[n];       /*非法,a不是普通識別符號*/

       };

       extern int a[n];       /*非法,VLA不能具有連結性*/

       static int b[n];        /*非法,VLA不能具有靜態儲存週期*/

       int c[n];             /*合法,自動VLA*/

       int d[m][n];          /*合法,自動VLA*/

       static int (*p1)[n] = d; /*合法,靜態VM指標*/

       n = 20;

       static int (*p2)[n] = d; /*未定義行為*/

       return 0;

}

一個VLA物件的大小在其生存期內不可改變,即使決定其大小的表示式的值在物件定義之後發生了改變。有些人看見可變長几個字就聯想到VLA陣列在生存期內可自由改變大小,這是誤解。VLA只是編譯期可變,一旦定義就不能改變,不是執行期可變,執行期可變的陣列叫動態陣列,動態陣列在理論上是可以實現的,但付出的代價可能太大,得不償失。考慮如下程式碼:

#include <stdio.h>

int main( void )

{

       int n = 10, m = 20;

       char a[m][n];

       char (*p)[n] = a;

       printf( “%u %u”, sizeof( a ), sizeof( *p ) );

       n = 20;

       m = 30;

       printf( “/n” );

       printf( “%u %u”, sizeof( a ), sizeof( *p ) );

       return 0;

}

雖然n和m的值在隨後的程式碼中被改變,但a和p所指向物件的大小不會發生變化。

上述程式碼使用了運算子sizeof,在C90/C++中,sizeof從運算元的型別去推演結果,不對運算元進行實際的計算,運算子的結果為整數常量。當sizeof的運算元是VLA時,情形就不同了。sizeof必須對VLA進行計算才能得出VLA的大小,運算結果為整數,不是整數常量。

VM除了可以作為自動物件外,還可以作為函式的形參。作為形參的VLA,與非VLA陣列一樣,會調整為與之等效的指標,例如:

void func( int a[m][n] ); 等效於void func( int (*a)[n] );

在函式原型宣告中,VLA形參可以使用*標記,*用於[]中,表示此處宣告的是一個VLA物件。如果函式原型宣告中的VLA使用的是長度表示式,該表示式會被忽略,就像使用了*標記一樣,下面幾個函式原型宣告是一樣的:

void func( int a[m][n] );

void func( int a[*][n] );

void func( int a[ ][n] );

void func( int a[*][*] );

void func( int a[ ][*] );

void func( int (*a)[*] );

*標記只能用在函式原型宣告中。再舉個例:

#include<stdio.h>

void func( int, int, int a[*][*] );

int main(void)

{

       int m = 10, n = 20;

       int a[m][n];

       int b[m][m*n];

       func( m, n, a );     /*未定義行為*/

       func( m, n, b );    

    return 0;

}

void func( int m, int n, int a[m][m*n] )

{

       printf( “%u/n”, sizeof( *a ) );

}

除了*標記外,形參中的陣列還可以使用型別限定詞const、volatile、restrict和static關鍵字。由於形參中的VLA被自動調整為等效的指標,因此這些型別限定詞實際上限定的是一個指標,例如:

void func( int, int, int a[const][*] );

等效於

void func( int, int, int ( *const a )[*] );

它指出a是一個const物件,不能在func內部直接通過a修改其代表的物件。例如:

void func( int, int, int a[const][*] );

……..

void func( int m, int n, int a[const m][n] )

{

       int b[m][n];

       a = b;        /*錯誤,不能通過a修改其代表的物件*/

}

       static表示傳入的實參的值至少要跟其所修飾的長度表示式的值一樣大。例如:

void func( int, int, int a[const static 20][*] );

……

int m = 20, n = 10;

int a[m][n];

int b[n][m];

func( m, n, a );

func( m, n, b );     /*錯誤,b的第一維長度小於static 20*/

型別限定詞和static關鍵字只能用於具有陣列型別的函式形參的第一維中。這裡的用詞是陣列型別,意味著它們不僅能用於VLA,也能用於一般陣列形參。

總的來說,VLA雖然定義時長度可變,但還不是動態陣列,在執行期內不能再改變,受制於其它因素,它只是提供了一個部分解決方案。