C語言變長數組之剖析express (陳雲川 ybc2084@163.com UESTC,CD)編程 一、引言咱們知道,與C++等現代編程語言不一樣,傳統上的C語言是不支持變長數組功能的,也就是說數組的長度是在編譯期就肯定下來的,不能在運行期改變。不過,在C99標準中,新增的一項功能就是容許在C語言中使用變長數組。然而,C99定義的這種變長數組的使用是有限制的,不能像在C++等語言中同樣自由使用。數組 二、說明參考文獻[1]中對變長數組的說明以下:編程語言 C99 gives C programmers the ability to use variable length arrays, which are arrays whose sizes are not known until run time. A variable length array declaration is like a fixed array declaration except that the array size is specified by a non-constant expression. When the declaration is encountered, the size expression is evaluated and the array is created with the indicated length, which must be a positive integer. Once created, variable length array cannot change in length. Elements in the array can be accessed up to the allocated length; accessing elements beyond that length results in undefined behavior. There is no check required for such out-of-range accesses. The array is destroyed when the block containing the declaration completes. Each time the block is started, a new array is allocated.ide 以上就是對變長數組的說明,此外,在文獻[1]中做者還說明,變長數組有如下限制:函數 一、變長數組必須在程序塊的範圍內定義,不能在文件範圍內定義變長數組;測試 二、變長數組不能用static或者extern修飾;ui 三、變長數組不能做爲結構體或者聯合的成員,只能以獨立的數組形式存在;lua 四、變長數組的做用域爲塊的範圍,對應地,變長數組的生存時間爲當函數執行流退出變長數組所在塊的時候;spa 上述限制是最多見的一些限制因素,此外,當經過typedef定義變長數組類型時,如何肯定變長數組的長度,以及當變長數組做爲函數參數時如何處理,做者也作了一一說明。詳細的細節狀況請參閱文獻[1]。因爲變長數組的長度在程序編譯時未知,所以變長數組的內存空間其實是在棧中分配的。 gcc雖然被認爲是最遵照C語言標準的編譯器之一,可是它並非嚴格按照ISO C標準規定的方式來實現的。gcc的實現方式採起了這樣的策略:最大限度地遵照標準的規定,同時從實用的角度作本身的擴展。固然,gcc提供了編譯選項給使用者以決定是否使用這些擴展功能。gcc的功能擴展分爲兩種,一種是gnu本身定義的語言擴展;另一種擴展是在C89模式中引入由C99標準定義的C語言特性。在參考文獻[2]中,有關gcc的C語言擴展佔據了將近120頁的篇幅,擴展的語言功能多達幾十個,由此可看出gcc的靈活程度。 在參考文獻[2]中,對變長數組的描述以下: Variable-length automatic arrays are allowed in ISO C99, and as an extension GCC accepts them in C89 mode and in C++. (However, GCC’s implementation of variable-length arrays does not yet conform in detail to the ISO C99 standard.) These arrays are declared like any other automatic arrays, but with a length that is not a constant expression. The storage is allocated at the point of declaration and deallocated when the brace-level is exited. 以上這段話並無詳細的說明gcc的變長數組實現和ISO C99的差別究竟體如今什麼地方,可是從描述來看,基本上和文獻[1]中的描述是一致的。文獻[2]中沒有說明而在文獻[1]中給予了說明的幾點是:變長數組是否能用static或者extern修飾;可否做爲複合類型的成員;可否在文件域起做用。 另外,在文獻[2]中提到,採用alloca()函數能夠得到和變長數組相同的效果。在做者所用的Red Hat 9.0(Linux 2.4.20-8)上,這個函數被定義爲一個庫函數: #include <alloca.h> void *alloca(size_t size); 這個函數在調用它的函數的棧空間中分配一個size字節大小的空間,當調用alloca()的函數返回或退出的時候,alloca()在棧中分配的空間被自動釋放。當alloca()函數執行成功時,它將返回一個指向所分配的棧空間的起始地址的指針;然而,很是特別的一點是,當alloca()函數執行失敗時,它不會像常見的庫函數那樣返回一個NULL指針,之因此會出現這樣的情況,是因爲alloca()函數中的棧調整一般是經過一條彙編指令來完成的,而這樣一條彙編指令是沒法判斷是否發生溢出或者是否分配失敗的。alloca()函數一般被實現爲內聯函數,所以它是與特定機器以及特定編譯器相關聯的,可移植性所以而大打折扣,其實是不推薦使用的。 做者之因此會關注變長數組的問題是出於一次偶然的因素,在調試的時候發現gdb給出的變長數組的類型很怪異,由此引起做者對gcc中的變長數組進行了測試。本文中給出的就是對測試結果的說明和分析。 三、實例第一個測試所用的源代碼很簡單,以下所示: 1 int 2 main(int argc, char *argv[]) 3 { 4 int i, n; 5 6 n = atoi(argv[1]); 7 char arr[n+1]; 8 bzero(arr, (n+1) * sizeof(char)); 9 for (i = 0; i < n; i++) { 10 arr[i] = (char)('A' + i); 11 } 12 arr[n] = '\0'; 13 printf("%s\n", arr); 14 15 return (0); 16 } 上述程序名爲dynarray.c,其工做是把參數argv[1]的值n加上1做爲變長數組arr的長度,變長數組arr的類型爲char。而後向數組中寫入一些字符,並將寫入的字符串輸出。 像下面這樣編譯這個程序: [root@cyc test]# gcc -g -o dynarray dynarray.c 而後,用gdb觀察dynarray的執行狀況: [root@cyc test]# gdb dynarray (gdb) break main Breakpoint 1 at 0x80483a3: file dynarray.c, line 6. (gdb) set args 6 (gdb) run Starting program: /root/source/test/a.out 6
Breakpoint 1, main (argc=2, argv=0xbfffe224) at dynarray.c:6 6 n = atoi(argv[1]); (gdb) next 7 char arr[n+1]; (gdb) next 8 bzero(arr, (n+1) * sizeof(char)); (gdb) print/x arr $2 = {0xb0, 0xe5} (gdb) ptype arr type = char [2] (gdb) print &arr $3 = (char (*)[2]) 0xbfffe1c8 這裏,當程序執行流經過了爲變長數組分配空間的第7行以後,用print/x命令打印出arr的值,結果竟然是兩個字節;而若是嘗試用ptype打印出arr的類型,獲得的結果竟然是arr是一個長度爲2的字符數組。很明顯,在本例中,由於提供給main()函數的參數argv[1]是6,所以按常理可知arr應該是一個長度爲7的字符數組,但很遺憾,gdb給出的卻並非這樣的結果。用print &arr打印出arr的地址爲0xbfffe1c8。繼續上面的調試過程: (gdb) x/4x &arr 0xbfffe5c8: 0xbfffe5b0 0xbfffe5c0 0x00000006 0x40015360 (gdb) x/8x $esp 0xbfffe5b0: 0xbffffad8 0x42130a14 0xbfffe5c8 0x0804828d 0xbfffe5c0: 0x42130a14 0x4000c660 0xbfffe5b0 0xbfffe5c0 能夠看到,在&arr(即地址0xbfffe5c8)處的第一個32位值是0xbfffe5b0,而經過x/8x $esp能夠發現,棧頂指針esp剛好就指向的是0xbfffe5b0這個位置。因而,能夠猜測,若是arr是一個指針的話,那麼它指向的就剛好是當前棧頂的指針。繼續上面的調試: (gdb) next 9 for (i = 0; i < n; i++) { (gdb) next 10 arr[i] = (char)('A' + i); (gdb) next 9 for (i = 0; i < n; i++) { (gdb) until 12 arr[n] = '\0'; (gdb) next 13 printf("%s\n", arr); (gdb) x/8x $esp 0xbfffe5b0: 0x44434241 0x42004645 0xbfffe5c8 0x0804828d 0xbfffe5c0: 0x42130a14 0x4000c660 0xbfffe5b0 0xbfffe5c0 注意上面表示爲藍色的部分,因爲Intel平臺採用的是小端字節序,所以藍色的部分實際上就是’ABCDEF’的十六進制表示。而紅色的32位字則暗示着arr就是指向棧頂的指針。爲了確認咱們的這一想法,下面經過修改arr的值來觀察程序的執行狀況(須要注意的是:每一次運行時堆棧的地址是變化的): (gdb) run The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /root/source/test/dynarray 6
Breakpoint 1, main (argc=2, argv=0xbfffde24) at dynarray.c:6 6 n = atoi(argv[1]); (gdb) next 7 char arr[n+1]; (gdb) next 8 bzero(arr, (n+1) * sizeof(char)); (gdb) print/x &arr $3 = 0xbfffddc8 (gdb) x/8x $esp 0xbfffddb0: 0xbffffad8 0x42130a14 0xbfffddc8 0x0804828d 0xbfffddc0: 0x42130a14 0x4000c660 0xbfffddb0 0xbfffddc0 (gdb) set *(unsigned int*)&arr=0xbfffddc0 (gdb) x/8x $esp 0xbfffddb0: 0xbffffad8 0x42130a14 0xbfffddc8 0x0804828d 0xbfffddc0: 0x42130a14 0x4000c660 0xbfffddc0 0xbfffddc0 (gdb) next 9 for (i = 0; i < n; i++) { (gdb) next 10 arr[i] = (char)('A' + i); (gdb) next 9 for (i = 0; i < n; i++) { (gdb) until 12 arr[n] = '\0'; (gdb) next 13 printf("%s\n", arr); (gdb) x/8x $esp 0xbfffddb0: 0xbffffad8 0x42130a14 0xbfffddc8 0x0804828d 0xbfffddc0: 0x44434241 0x40004645 0xbfffddc0 0xbfffddc0 地址0xbfffddc8(也就是arr的地址)處的值原本爲0xbfffddb0,咱們把它改爲了0xbfffddc0,因而,當程序運行到向變長數組輸入數據完成以後,咱們發現此次修改的地址的確是從0xbfffddc0開始的。這就代表arr的確像咱們一般所理解的同樣,數組名即指針。只不過這個指針指向的位置在它的下方(堆棧向下生長),而不是像大多數時候同樣指向上方的某個位置。 四、分析上面的測試結果代表:變長數組的確是在棧空間中分配的;變長數組的數組名實際上就是一個地址指針,指向數組所在的棧頂位置;而GDB沒法判斷出變長數組的數組名其實是一個地址指針。 GDB爲何沒法準確判斷出變長數組的類型的緣由尚不清楚,可是做者猜想這和變長數組的動態特性有關,因爲變長數組是在程序動態執行的過程生成的,GDB沒法向對待常規數組同樣從目標文件包含的.stabs節中得到長度信息,因而給出了錯誤的類型信息。 另外,做者對變長數組的做用域進行了測試,測試代碼根據上例修改獲得,以下所示: 1 int n; 2 char arr[n+1]; 3 4 int 5 main(int argc, char *argv[]) 6 { 7 int i; 8 9 n = atoi(argv[1]); 10 bzero(arr, (n+1) * sizeof(char)); 11 for (i = 0; i < n; i++) { 12 arr[i] = (char)('A' + i); 13 } 14 arr[n] = '\0'; 15 printf("%s\n", arr); 16 17 return (0); 18 } 當以下編譯的時候,gcc會提示出錯: [root@cyc test]# gcc -g dynarray.c dynarray.c:2: variable-size type declared outside of any function 可見gcc不容許在文件域定義變長數組。 對於gcc中的變長數組可否用static修飾則使用以下代碼進行測試: 1 int 2 main(int argc, char *argv[]) 3 { 4 int i, n; 5 6 n = atoi(argv[1]); 7 static char arr[n+1]; 8 bzero(arr, (n+1) * sizeof(char)); 9 for (i = 0; i < n; i++) { 10 arr[i] = (char)('A' + i); 11 } 12 arr[n] = '\0'; 13 printf("%s\n", arr); 14 15 return (0); 16 } 當編譯此源文件的時候,gcc給出以下錯誤提示: [root@cyc test]# gcc -g dynarray.c dynarray.c: In function `main': dynarray.c:7: storage size of `arr' isn't constant dynarray.c:7: size of variable `arr' is too large 根據提示,可知當數組用static修飾的時候,不能將其聲明爲變長數組。至於這裏的提示說arr太大,做者猜想可能的緣由是這樣的:對於整數,gcc在編譯期賦予了一個很是大的值,因而致使編譯報錯,不過這僅僅是猜想而已。 最後須要說明的是,做者是出於對gcc如何實現變長數組的方式感興趣才進行上面的這些測試的。對於編程者來講,不用作這樣的測試,也不須要知道變長數組是位於棧中仍是其它地方,只要知道變長數組有上面這樣一些限制就好了。另外,本文中有不少地方充斥着做者的推斷和猜想。不過這並無太大的關係,又不是寫論文,誰在意呢? 另外,上面的測試也說明了:儘管文獻[2]沒有像文獻[1]中那樣仔細說明變長數組的限制條件,但實際上它就是那樣工做的。再一次體現出gcc的確很好地遵照了C標準的規定。 參考文獻[1] Samuel P. Harbison III, Guy L. Steele Jr.; C: A Reference Manual Fifth Edition; Prentice Hall, Pearson Education, Inc.; 2002 [2] Richard M. Stallman and the GCC Developer Community; Using the GNU Compiler Collection; FSF; May 2004 |