C語言變長數(shù)組之剖析

C語言變長數(shù)組之剖析

(陳云川 ybc2084@163.com UESTC,CD

1、引言

我們知道,與C++等現(xiàn)代編程語言不同,傳統(tǒng)上的C語言是不支持變長數(shù)組功能的,也就是說數(shù)組的長度是在編譯期就確定下來的,不能在運(yùn)行期改變。不過,在C99標(biāo)準(zhǔn)中,新增的一項(xiàng)功能就是允許在C語言中使用變長數(shù)組。然而,C99定義的這種變長數(shù)組的使用是有限制的,不能像在C++等語言中一樣自由使用。

2、說明

參考文獻(xiàn)[1]中對變長數(shù)組的說明如下:

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.

以上就是對變長數(shù)組的說明,此外,在文獻(xiàn)[1]中作者還說明,變長數(shù)組有以下限制:

1、變長數(shù)組必須在程序塊的范圍內(nèi)定義,不能在文件范圍內(nèi)定義變長數(shù)組;

2、變長數(shù)組不能用static或者extern修飾;

3、變長數(shù)組不能作為結(jié)構(gòu)體或者聯(lián)合的成員,只能以獨(dú)立的數(shù)組形式存在;

4、變長數(shù)組的作用域?yàn)閴K的范圍,對應(yīng)地,變長數(shù)組的生存時間為當(dāng)函數(shù)執(zhí)行流退出變長數(shù)組所在塊的時候;

上述限制是最常見的一些限制因素,此外,當(dāng)通過typedef定義變長數(shù)組類型時,如何確定變長數(shù)組的長度,以及當(dāng)變長數(shù)組作為函數(shù)參數(shù)時如何處理,作者也做了一一說明。詳細(xì)的細(xì)節(jié)情況請參閱文獻(xiàn)[1]。由于變長數(shù)組的長度在程序編譯時未知,因此變長數(shù)組的內(nèi)存空間實(shí)際上是在棧中分配的。

gcc雖然被認(rèn)為是最遵守C語言標(biāo)準(zhǔn)的編譯器之一,但是它并不是嚴(yán)格按照ISO C標(biāo)準(zhǔn)規(guī)定的方式來實(shí)現(xiàn)的。gcc的實(shí)現(xiàn)方式采取了這樣的策略:最大限度地遵守標(biāo)準(zhǔn)的規(guī)定,同時從實(shí)用的角度做自己的擴(kuò)展。當(dāng)然,gcc提供了編譯選項(xiàng)給使用者以決定是否使用這些擴(kuò)展功能。gcc的功能擴(kuò)展分為兩種,一種是gnu自己定義的語言擴(kuò)展;另外一種擴(kuò)展是在C89模式中引入由C99標(biāo)準(zhǔn)定義的C語言特性。在參考文獻(xiàn)[2]中,有關(guān)gccC語言擴(kuò)展占據(jù)了將近120頁的篇幅,擴(kuò)展的語言功能多達(dá)幾十個,由此可看出gcc的靈活程度。

在參考文獻(xiàn)[2]中,對變長數(shù)組的描述如下:

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.

以上這段話并沒有詳細(xì)的說明gcc的變長數(shù)組實(shí)現(xiàn)和ISO C99的差異究竟體現(xiàn)在什么地方,但是從描述來看,基本上和文獻(xiàn)[1]中的描述是一致的。文獻(xiàn)[2]中沒有說明而在文獻(xiàn)[1]中給予了說明的幾點(diǎn)是:變長數(shù)組是否能用static或者extern修飾;能否作為復(fù)合類型的成員;能否在文件域起作用。

另外,在文獻(xiàn)[2]中提到,采用alloca()函數(shù)可以獲得和變長數(shù)組相同的效果。在作者所用的Red Hat 9.0Linux 2.4.20-8)上,這個函數(shù)被定義為一個庫函數(shù):

#include <alloca.h>

void *alloca(size_t size);

這個函數(shù)在調(diào)用它的函數(shù)的??臻g中分配一個size字節(jié)大小的空間,當(dāng)調(diào)用alloca()的函數(shù)返回或退出的時候,alloca()在棧中分配的空間被自動釋放。當(dāng)alloca()函數(shù)執(zhí)行成功時,它將返回一個指向所分配的棧空間的起始地址的指針;然而,非常特別的一點(diǎn)是,當(dāng)alloca()函數(shù)執(zhí)行失敗時,它不會像常見的庫函數(shù)那樣返回一個NULL指針,之所以會出現(xiàn)這樣的狀況,是由于alloca()函數(shù)中的棧調(diào)整通常是通過一條匯編指令來完成的,而這樣一條匯編指令是無法判斷是否發(fā)生溢出或者是否分配失敗的。alloca()函數(shù)通常被實(shí)現(xiàn)為內(nèi)聯(lián)函數(shù),因此它是與特定機(jī)器以及特定編譯器相關(guān)聯(lián)的,可移植性因此而大打折扣,實(shí)際上是不推薦使用的。

作者之所以會關(guān)注變長數(shù)組的問題是出于一次偶然的因素,在調(diào)試的時候發(fā)現(xiàn)gdb給出的變長數(shù)組的類型很怪異,由此引發(fā)作者對gcc中的變長數(shù)組進(jìn)行了測試。本文中給出的就是對測試結(jié)果的說明和分析。

3、實(shí)例

第一個測試所用的源代碼很簡單,如下所示:

 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,其工作是把參數(shù)argv[1]的值n加上1作為變長數(shù)組arr的長度,變長數(shù)組arr的類型為char。然后向數(shù)組中寫入一些字符,并將寫入的字符串輸出。

像下面這樣編譯這個程序:

[root@cyc test]# gcc -g -o dynarray dynarray.c

然后,用gdb觀察dynarray的執(zhí)行情況:

[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

這里,當(dāng)程序執(zhí)行流通過了為變長數(shù)組分配空間的第7行之后,用print/x命令打印出arr的值,結(jié)果居然是兩個字節(jié);而如果嘗試用ptype打印出arr的類型,得到的結(jié)果居然是arr是一個長度為2的字符數(shù)組。很明顯,在本例中,因?yàn)樘峁┙omain()函數(shù)的參數(shù)argv[1]6,因此按常理可知arr應(yīng)該是一個長度為7的字符數(shù)組,但很遺憾,gdb給出的卻并不是這樣的結(jié)果。用print &arr打印出arr的地址為0xbfffe1c8。繼續(xù)上面的調(diào)試過程:

(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可以發(fā)現(xiàn),棧頂指針esp恰好就指向的是0xbfffe5b0這個位置。于是,可以猜想,如果arr是一個指針的話,那么它指向的就恰好是當(dāng)前棧頂?shù)闹羔?。繼續(xù)上面的調(diào)試:

(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

注意上面表示為藍(lán)色的部分,由于Intel平臺采用的是小端字節(jié)序,因此藍(lán)色的部分實(shí)際上就是’ABCDEF’的十六進(jìn)制表示。而紅色的32位字則暗示著arr就是指向棧頂?shù)闹羔?。為了確認(rèn)我們的這一想法,下面通過修改arr的值來觀察程序的執(zhí)行情況(需要注意的是:每一次運(yùn)行時堆棧的地址是變化的):

(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,于是,當(dāng)程序運(yùn)行到向變長數(shù)組輸入數(shù)據(jù)完成之后,我們發(fā)現(xiàn)這次修改的地址的確是從0xbfffddc0開始的。這就表明arr的確像我們通常所理解的一樣,數(shù)組名即指針。只不過這個指針指向的位置在它的下方(堆棧向下生長),而不是像大多數(shù)時候一樣指向上方的某個位置。

4、分析

上面的測試結(jié)果表明:變長數(shù)組的確是在??臻g中分配的;變長數(shù)組的數(shù)組名實(shí)際上就是一個地址指針,指向數(shù)組所在的棧頂位置;而GDB無法判斷出變長數(shù)組的數(shù)組名實(shí)際上是一個地址指針。

GDB為什么無法準(zhǔn)確判斷出變長數(shù)組的類型的原因尚不清楚,但是作者猜測這和變長數(shù)組的動態(tài)特性有關(guān),由于變長數(shù)組是在程序動態(tài)執(zhí)行的過程生成的,GDB無法向?qū)ΥR?guī)數(shù)組一樣從目標(biāo)文件包含的.stabs節(jié)中獲得長度信息,于是給出了錯誤的類型信息。

另外,作者對變長數(shù)組的作用域進(jìn)行了測試,測試代碼根據(jù)上例修改得到,如下所示:

 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 }

當(dāng)如下編譯的時候,gcc會提示出錯:

[root@cyc test]# gcc -g dynarray.c

dynarray.c:2: variable-size type declared outside of any function

可見gcc不允許在文件域定義變長數(shù)組。

對于gcc中的變長數(shù)組能否用static修飾則使用如下代碼進(jìn)行測試:

 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 }

當(dāng)編譯此源文件的時候,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

根據(jù)提示,可知當(dāng)數(shù)組用static修飾的時候,不能將其聲明為變長數(shù)組。至于這里的提示說arr太大,作者猜測可能的原因是這樣的:對于整數(shù),gcc在編譯期賦予了一個非常大的值,于是導(dǎo)致編譯報(bào)錯,不過這僅僅是猜測而已。

最后需要說明的是,作者是出于對gcc如何實(shí)現(xiàn)變長數(shù)組的方式感興趣才進(jìn)行上面的這些測試的。對于編程者來說,不用做這樣的測試,也不需要知道變長數(shù)組是位于棧中還是其它地方,只要知道變長數(shù)組有上面這樣一些限制就行了。另外,本文中有很多地方充斥著作者的推斷和猜測。不過這并沒有太大的關(guān)系,又不是寫論文,誰在乎呢?

另外,上面的測試也說明了:盡管文獻(xiàn)[2]沒有像文獻(xiàn)[1]中那樣仔細(xì)說明變長數(shù)組的限制條件,但實(shí)際上它就是那樣工作的。再一次體現(xiàn)出gcc的確很好地遵守了C標(biāo)準(zhǔn)的規(guī)定。

參考文獻(xiàn)

[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