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 |