1. 程式人生 > >C99 可變陣列LVA

C99 可變陣列LVA

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雖然定義時長度可變,但還不是動態陣列,在執行期內不能再改變,受制於其它因素,它只是提供了一個部分解決方案。