本文为译文,原文链接

相比于定长数组,变长数组会产生额外的代码,使代码运行速度更慢,鲁棒性更差 ~ Linus Torvalds

变长数组缩写为VLA(variable-length array),它是一种在运行时才确定长度的数组(地址空间连续的数组,并不是表现得像数组的多段内存组成的数据结构),而非编译期。

以一种或多种方式提供VLAs支持的语言包括:Ada, Algol 68, APL, C, C#, COBOL,
Fortran, J, Object Pascal。正如你所见,除了C和C#,其他的都不是现在主流的语言。

VLA 在C99版本中出现。一开始,它们看起来似乎既方便又高效,然而这都是假象。事实上,他们往往是不断出现问题的根源。如果没有这个污点,C99本应该是一个很好的版本。

正如你在文章开头的引用中所见,Linux 内核曾经是一个广泛使用 VLA 的项目。
开发者付出了巨大的努力去摆脱 VLA,终于在2018年的4.20版本中得尝所愿,去掉了全部的VLA。

在栈上分配空间

VLAs 通常在栈上分配内存空间,而这正是大多数问题的根源所在。让我们来看一个简单的例子:

#include <stdio.h>int main(void) {int n;scanf("%d", &n);long double arr[n];printf("%Lf", arr[0]);return 0;
}

这里获取一个用户输入,作为数组的长度。试着把他跑起来,看看具体多大的数会使程序由于栈溢出导致的segmentation fault而报错。在我这里,最大可以到50万。这只是对于原始类型的数据,如果是结构体数组,这个上限会更小。又或者这个数组不是在main()里面,而是在递归调用中,上限会急剧减小。

然而,对于栈溢出,你并没有什么好的办法去补救,因为程序已经崩溃了。所以,你必须要在声明数组之前严格检查数组大小,或者你可以指望用户不要输入太大的数(这种赌博的结局显而易见)。

程序员必须保证变长数组的大小不会超过一个安全的最大值,但实际上,如果有人能知道这个安全的最大值的话,他没有任何理由不使用它(也就是说这个值是不可知的译者注)。

更糟糕的是

事实上,在 VLA 处理不当时,segmentation fault已经是最好的结果了。最坏的情况是,这是一个可利用的漏洞,攻击者可以选择一个合适的数组大小,利用数组覆盖其他的地址空间,以让他们控制这些地址空间。这简直就是安全噩梦。

以牺牲性能为代价,你可以在 GCC 中使用 -fstack-clash-protection 参数。
该参数作用为,在变长栈空间分配前后添加额外的指令,以在分配时探测每一页内存。
样可以减轻“栈冲突”攻击的作用,该指令保证所有的栈内存分配都是有效的,如果存在无效的,
就直接丢出 segementation fault异常,这样就把一个可能的代码攻击变成了拒绝服务。

改进之前的例子

如果确实需要用户输入数组大小,而又不想浪费空间去提前申请大数组,应该怎么做呢?使用malloc()!:

#include <stdio.h>
#include <stdlib.h>int main(void) {int n;scanf("%d", &n);long double* arr = malloc(n * sizeof (*arr));printf("%Lf", arr[0]);free(arr);return 0;
}

在这个例子中,我可以最多可以输入13亿,不出现segementation fault的前提下,差不多比之前多了2500倍,然而还是会有一个导致

segementation fault的上限。不同的是,这里可以检查malloc()函数的返回值,以知晓地址空间分配是否成功。

     long double* arr = malloc(n * sizeof (*arr));if (arr == NULL) {perror("malloc()"); // 输出: "malloc(): Cannot allocate memory"}
有这样一个相反的观点,C语言通常被用作编写系统或者嵌入式系统,在这种情况下,可能用不了 malloc()。
我必须要在这里重复一遍我的看法,因为这真的很重要。
在这些设备上,你所拥有的栈空间也不会很多。
所以,你应该确定你需要多少空间,然后使用定长数组,而不是在栈上动态的分配空间。
当在栈很小的系统上使用动态数组时,很容易出现,虽然看起来一切正常,
但是由于较深的函数调用、大量的数据分配而造成栈崩溃的情况。
如果你总是分配一个固定大小的栈空间,测试时就不会出现这些问题。
不要做对自己没有好处的事。

在意料之外产生

不同于其他危险的C语言功能,VLA 是广为人知的。许多新手通过反复试错学会使用 VLA,但是并不了解陷阱。有时候即使是经验丰富的程序员也会在不经意间使用 VLA。以下代码就会悄无声息的产生一个不必要的 VLA:

const int n = 10;
int A[n];

值得庆幸的是,编译器会察觉并优化这样的 VLA,但是万一没有察觉到呢?又或者基于其他的考虑(比如安全)没有优化呢?大概不会有更糟的情况了吧?

比定长更慢

没有编译器优化的情况下,在传入数组之前,使用 VLA 的代码的汇编指令数是使用定长数组的代码的7倍。实际上,优化之后,情况也是一样的。见下例:

#include <stdio.h>
void bar(int*, int);#if 1 // 1 for VLA, 0 for VLA-freevoid foo(int n) {int A[n];for (int i = n; i--;) {scanf("%d", &A[i]);}bar(A, n);
}#elsevoid foo(int n) {int A[1000];  // Let's make it bigger than 10! (or there won't be what to examine)for (int i = n; i--;) {scanf("%d", &A[i]);}bar(A, n);
}#endifint main(void) {foo(10);return 0;
}void bar(int* B, int n) {for (int i = n; i--;) {printf("%d %d", i, B[i]);}
}

为了更好的说明情况,-01级别的优化更合适(汇编会更清楚,另外-02级别的优化对 VLA 的优化并不明显)

编译 VLA 的版本后,在for循环对应的指令之前,我们可以看到:

push    rbp
mov     rbp, rsp
push    r14
push    r13
push    r12
push    rbx
mov     r13d, edi
movsx   r12, edi       ; "VLA"在这里开始
sal     r12, 2         ;
lea     rax, [r12+15]  ;
and     rax, -16       ;
sub     rsp, rax       ;
mov     r14, rsp       ; 这里结束

而非 VLA 的版本是这样的:

push    r12
push    rbp
push    rbx
sub     rsp, 4000      ; 这里是数组的定义
mov     r12d, edi

可见,定长数组的代码更简短。为什么使用 VLA 会造成这么多的函数头部开销呢?我们也许不必考虑所有的事情,但这绝不仅仅是指针碰撞。

这些区别必然是值得关心的。

不允许初始化

为了减少无意间使用 VLA 时的麻烦,以下操作是不允许的:

int n = 10;
int A[n] = { 0 };

即使有编译器的优化,初始化 VLAs 也是不允许的。所以尽管我们希望编译器能在技术上提供一个定长的数组,这种操作也是不允许的。

编译器作者的麻烦事

几个月前,我保存了 Reddit上的一个评论,是关于编译器作者如何看待 VLA 带来的问题的。在此引用:

  • A VLA applies to a type, not an actual array. So you can create a typedef of a VLA type, which “freezes” the value of the expression used, even if elements of that expression change at the time the VLA type is applied
  • VLAs can occur inside blocks, and inside loops. This means allocating and deallocating variable-sized data on the stack, and either screwing up all the offsets, or needing to do things indirectly via pointers.
  • You can use goto into and out of blocks with active VLAs, with some things restricted and some not, but the compiler needs to keep track of the mess.
  • VLAs can be used with multi-dimensional arrays.
  • VLAs can be used as pointer targets (so no allocation is done, but it still needs to keep track of the variable size).
  • Some compilers allow VLAs inside structure definitions (I really have no idea how that works, or at what point the VLA size is frozen, so that all instances have the same VLA(s) sizes.)
  • A function can have dozens of VLAs active at any one time, with some being created or destroyed at different times, or conditionally, or in loops.
  • sizeof needs to be specially implemented for VLAs, and all the necessary info (for actual VLAs, VLA-types, and hybrid VLA/fixed-size types and arrays and pointed-to VLAs).
  • ‘VLA’ is also the term used for multi-dimensional array parameters, where the dimensions are passed by other parameters.
  • On Windows, with some compilers (GCC at least), declaring local arrays which make the stack frame size over 4 KiB, mean calling a special allocator (__chkstk()), as the stack can only grow a page at a time. When a VLA is declared, since the compiler doesn’t know the size, it needs to call __chkstk for every such function, even if the size turns out to be small.

你在浏览其他的C语言论坛时,肯定也见过更多不同的抱怨。

减少支持

由于上文提到的这些问题,一些编译器提供者决定不完全支持 C99,一开始是微软的 MSVC。C语言标准协会也注意到这个问题,并且在 C11 版本中,VLAs 是可选的(大多数都选择弃用)。

这就意味着,使用 VLA 的代码不一定可以用 C11 的编译器编译,所以使用时需要检查编译器是否支持_SRDC_NO_VLA_宏,并且编写不适用 VLA 的版本作为备用。既然需要写不使用 VLA 的版本,那为什么还要写使用 VLA 的版本呢?

值得一提,C++没有 VLA,也没有迹象表明以后会支持。C++并非破坏者,却任然反对C语言中的 VLA 。

(挑剔的理由)打破了惯例

也许显得苛刻,却也是一个不喜欢 VLA 的理由。以下为广泛使用的传入二维数组的传参方式,我们习惯于先传入数组:

void foo(int** arr, int n, int m) { /* arr[i][j] = ... */ }

C99 中,当函数参数列表中有数组时,数组大小会被立即解析。也就意味着,使用 VLA ,就不能使用下面一种传参方式了

void foo(int arr[n][m], int n, int m) { /* arr[i][j] = ... */ } // INVALID!

你只能选择下面的方式:

  • 打破常规:

    void foo(int n, int m, int arr[n][m]) { /* arr[i][j] = ... */ }
    
  • 使用过时的语法

    void foo(int[*][*], int, int);
    void foo(arr, n, n)int n;int m;int arr[n][m]
    {// arr[i][j] = ...
    }
    

某些情况下还有点用

有一种需要使用 VLA 的情景:动态分配多维数组,数组的内层维度要到运行时才知道。这里甚至没有安全问题,因为没有随意分配栈空间。

int (* A)[m] = malloc(n * sizeof (*A)); // m 和 n 是数组维度
if (A) {// A[i][j] = ...;free(A);
}

不使用 VLA,可以有以下替代方式:

  • 一行一行使用malloc()申请:

    int** A = malloc(n * sizeof (*A));
    if (A) {for (int i = 0; i < m; ++i) {A[i] = malloc(m * sizeof (*A[i]));}// A[i][j] = ...for (int i = 0; i < m; ++i) {free(A[i]);}free(A);
    }
    
  • 一维数组加上偏置:

    int* A = malloc(n * m * sizeof (*A));
    if (A) {// A[i*n + j] = ...free(A);
    }
    
  • 使用大的定长数组:

    int A[SAFE_SIZE][SAFE_SIZE]; // SAFE_SIZE must be safe for SAFE_SIZE*SAFE_SIZE
    // A[i][j] = ...;
    

总结

简而言之,避免使用 VLA。它带来了危险,却没有带来任何好处。如果你真的想使用的话,请牢记它的限制。

值得一提的是,VLA 是问题更多的`alloca()`的解决方案(并非标准)。

C语言中变长数组的陷阱相关推荐

  1. c语言变长数组参数,使用gdb跟踪C语言中变长数组的实现

    项目的代码中出现的一个问题,问题的表现是,在一个函数中使用到了变长数组,而对超过这个数组 范围的一个赋值,导致了数组首地址为空. 我把这个问题抽出来形成了一个示例函数,在i386下也出现类似的问题,代 ...

  2. C语言,变长数组的用法

    ​ 在我的<C语言,结构体成员的地址>文章中,定义了一个demo_node结构体,其中用到变长数组char addr[0].本文以此为例,对C语言变长数组的基本用法展开介绍. #pragm ...

  3. C语言变长数组 struct中char data[0]的用法

    摘要:在实际的编程中,我们经常需要使用变长数组,但是C语言并不支持变长的数组.此时,我们可以使用结构体的方法实现C语言变长数组. struct MyData  {  int nLen;  char d ...

  4. C语言变长数组data[0]【总结】

    C语言变长数组data[0][总结] 1.前言 今天在看代码中遇到一个结构中包含char data[0],第一次见到时感觉很奇怪,数组的长度怎么可以为零呢?于是上网搜索一下这样的用法的目的,发现在li ...

  5. c++什么时候数组溢出_C语言,营养丰富的C语言五,变长数组不是动态数组

    大家好,感谢朋友的支持阅读和关注,虽然我提出的这些小知识点看得人很少,但是每涨一个阅读和关注,都能让我开心很久,所以再次感谢一起学习的朋友们. 查余补漏: 在前几次的讲解中,有朋友提出C语言的内存分配 ...

  6. C语言高级教程-C语言数组(六):变长数组

    C语言高级教程-C语言数组(六):变长数组 一.本文的编译环境 二.一维数组在执行期间确定长度 三.二维数组在执行期间确定长度 四.一维变长数组实例 五.完整程序 5.1 Main.h 文件程序 5. ...

  7. 在C++中实现变长数组

    1.变长一维数组 这里说的变长数组是指在编译时不能确定数组长度,程序在运行时需要动态分配内存空间的数组.实现变长数组最简单的是变长一维数组,你可以这样做: //文件名: array01.cpp #in ...

  8. 第六章 C语言数组_C语言变长数组:使用变量指明数组的长度

    在<C语言的三套标准:C89.C99和C11>一节中我们讲到,目前经常使用的C语言有三个版本,分别是 C89.C99 和 C11.C89(也称 ANSI C)是较早的版本,也是最经典的版本 ...

  9. C99中的变长数组(VLA)

    处理二维数组的函数有一处可能不太容易理解,数组的行可以在函数调用的时候传递,但是数组的列却只能被预置在函数内部.例如下面这样的定义: #define COLS 4 int sum3d(int ar[] ...

  10. 如何在java中创建变长数组

    传统的数组创建 在java中我们都知道创建简单数组较为简单,和C很相似.如下是创建1.2.3维数组的代码. int [] array = new int[5]; int [][] array = ne ...

最新文章

  1. apache 安装后默认主页无法打开_CAD教程:CAD软件打开图纸后钢筋符号无法读取的解决办法...
  2. Android游戏开发之OpenGL之视图-投影矩阵 杂谈
  3. tableau linux无网络安装_举个栗子!Tableau 技巧(110)两种方法实现正态分布 Normal distribution...
  4. oracle恢复指定数据文件,Oracle特殊恢复-BBED修改某个数据文件头
  5. 系统间账号认证系统同步方案
  6. 嵌入式 Linux 的分类
  7. 京东金融以支付开启出海之旅,未来或拓展至消费金融
  8. 唤起那些年你对IDL的记忆(二)
  9. Sencha Cmd 6 和 Ext JS 6 指南文档(部分官方文档中文翻译)
  10. arccos用计算机,arccos(arccos在线计算器)
  11. 应用网易轻舟,德邦快递核心系统入选云原生应用十大优秀案例
  12. 王牌战士怎么用电脑玩 王牌战士模拟器玩法教程
  13. Navicat Premium 16 隆重登场
  14. 官方公布中国自行车排名十强辐轮王土拨鼠全世界碳纤维自行车品牌
  15. c语言上20级台阶递归法,c语言递归算法.pptx
  16. python+opencv读取视频并设置可调整窗口大小
  17. 人力资源和社会保障部——拟新增职业“密码技术应用员”
  18. Fragment has not been attached yet 解决方法及源码详解
  19. js判断H5页面是否是在QQ\UC浏览器中打开
  20. OpenGL ES之GLSL实现“瘦身大长腿”美颜滤镜效果

热门文章

  1. 不是一个PDF文件或该文件已损坏
  2. 上传本地文件到服务器:not a regular file
  3. zblogphp 广告联盟_zblog模板添加广告位置的方法
  4. 深度学习——安装Nvidia 驱动(亲测有效)
  5. dvi是什么意思_VGA线和DVI线,VGA线和DVI线是什么意思
  6. 网络安全入门学习资源汇总
  7. Linux三剑客(grep、sed、awk)
  8. 中国移动商城登陆WP8应用商店
  9. 四月之 诗四首和五十六句话
  10. 7-5 有理数比较 (10 分)