| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 440 人关注过本帖
标题:[轉供參考] C语言变长数组之剖析
只看楼主 加入收藏
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
结帖率:100%
收藏
 问题点数:0 回复次数:1 
[轉供參考] C语言变长数组之剖析
C语言变长数组之剖析(以后看)
2010-11-04 16:28:03

1、引言
我们知道,与C++等现代编程语言不同,传统上的C语言是不支持变长数组功能的,也就是说数组的长度是在编译期就确定下来的,不能在运行期改变。不过,在C99标准中,新增的一项功能就是允许在C语言中使用变长数组。然而,C99定义的这种变长数组的使用是有限制的,不能像在C++等语言中一样自由使用。

2、说明
参考文献[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.

以上就是对变长数组的说明,此外,在文献[1]中作者还说明,变长数组有以下限制:

1、变长数组必须在程序块的范围内定义,不能在文件范围内定义变长数组;

2、变长数组不能用static或者extern修饰;

3、变长数组不能作为结构体或者联合的成员,只能以独立的数组形式存在;

4、变长数组的作用域为块的范围,对应地,变长数组的生存时间为当函数执行流退出变长数组所在块的时候;

上述限制是最常见的一些限制因素,此外,当通过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中的变长数组进行了测试。本文中给出的就是对测试结果的说明和分析。

3、实例
第一个测试所用的源代码很简单,如下所示:

?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的确像我们通常所理解的一样,数组名即指针。只不过这个指针指向的位置在它的下方(堆栈向下生长),而不是像大多数时候一样指向上方的某个位置。

4、分析
上面的测试结果表明:变长数组的确是在栈空间中分配的;变长数组的数组名实际上就是一个地址指针,指向数组所在的栈顶位置;而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

原始鏈接:http://astrotycoon.blog.
搜索更多相关主题的帖子: ability 参考文献 编程语言 C语言 
2015-02-03 23:33
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
收藏
得分:0 
所謂的“變長數組”VLA,思想應源自解釋型語言的數組。在解釋型語言中,由於每一條指令都是臨時編譯執行的,在下一條指令編譯時,前面指令創造的數據已經生效,故不存在實際未知的數據,比如在用戶交互獲得整數n之後,對創建數組x[n]的指令來説,n是已知常數,這個時候,使用的是動態申請内存(即malloc())來創建數組,這就是VLA所模擬的動作。由於C是靜態編譯型語言,創建數組的指令必須在編譯時給出,此時并不知道在運行時才獲得的數據n的值,所以它是無法編譯出那個具體指令的,它唯一的實現辦法,就是執行時用malloc()動態申請内存,這是傳統的做法,也是那些解釋語言所采用的方法。gcc別出心裁,爲了回避釋放内存的麻煩,居然想出在棧上動態申請空間的歪招(因爲這種編譯器默認的棧尺寸是4M而非MS系列的1M,自覺溢出的機會不大而已),也就是在棧上的malloc(),它唯一追求的優勢,是可以自動釋放,但事實上帶來的限制和麻煩更多,具體如主文所言。

在堆上申請動態内存做變長數組的辦法,缺點在釋放内存上,這也是C/C++傳統的大難題了。但這個問題,在新的操作系統(從Windows NT開始),就已經在系統級上解決了,即操作系統可以保證在程序結束後收回一切它所占用的内存,不再如DOS時代那樣會殘留在系統中的了。而在C++的新類庫中,也有自動釋放内存的機制,比如我們用vector,從來不需主動釋放内存。而在CLR和Java類的語言中,垃圾回收機制也是爲解決此老大難問題而生。所以,在當代的編程中,基本上不再爲釋放内存煩惱的了,除了非要在老舊的環境中編程,再花精力培養的思維和開發自動釋放内存的工具,可以說是浪費時間。如果有人覺得自己有能力全部管理内存的分配和使用而不出問題,那是向幾十年的前人程序員們挑戰,我希望他真的成功,加上真主保佑你!

[ 本帖最后由 TonyDeng 于 2015-2-4 10:24 编辑 ]

授人以渔,不授人以鱼。
2015-02-04 10:15
快速回复:[轉供參考] C语言变长数组之剖析
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.022669 second(s), 8 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved