C++数组维度与C99的变长数组(VLA)

问题起因

前几天在一个C++学习交流群里边有群友问了一个代码问题,其中它的代码包含了这样的语句

int n;
scanf("%d",&n);
std::string strs[n];

看到这样的操作,我没有继续看其他的代码,就直接指出了他的这个问题,我说这种静态数组在编译期就要确定内存大小,你这样肯定有问题,他表示不信,后来他的程序问题找到了并且告诉我,他的这种写法没问题,我当时就奇怪了,怎么与我一贯的理解不同呢,工作了这么多年,凡是使用数组的地方都是固定大小的。然后我在devC++这个IDE上测试了这个问题,当然编译器是GCC的,结果令我吃惊,还真是没问题。看来涉及到基础的问题,我还是不牢固啊,赶紧翻开《C++Primer》来看

C++Primer关于数组的说法

C++primer中文版第五版101页说到:

数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的,也就是说维度必须是一个常量表达式

unsigned cnt = 42;         //不是常量表达式
constexpr unsigned sz = 42;//常量表达式
int arr[10];               //含有10个整数的数组
int *parr[sz];             //含有42个整型指针的数组
string bad[cnt];           //错误:cnt不是常量表达式
string strs[get_size()];   //当get_size是constexpr时正确;否则错误

这么说来我原来的理解是没错的。那么为什么出现上边的问题呢,这就涉及到C99标准了

C99标准的变长数组(VLA)

参见维基百科的解释 https://en.wikipedia.org/wiki/Variable-length_array#C99

The following C99 function allocates a variable-length array of a specified size, fills it with floating-point values, and then passes it to another function for processing. Because the array is declared as an automatic variable, its lifetime ends when read_and_process() returns.

float read_and_process(int n)
{
    float vals[n];

    for (int i = 0; i < n; ++i)
        vals[i] = read_val();

    return process(n, vals);
}

In C99, the length parameter must come before the variable-length array parameter in function calls.[1]

Linus Torvalds has expressed his displeasure in the past over VLA usage for arrays with predetermined small sizes, with comments like "USING VLA'S IS ACTIVELY STUPID! It generates much more code, and much slower code (and more fragile code), than just using a fixed key size would have done." [6] With the Linux 4.20 kernel, Linux kernel is effectively VLA-free.

注意红字的部分,另外显然Linus不喜欢VLA

编译器对VLA的支持

GCC

还是上边的维基百科

那么我的疑问也得到了解答了,显然GCC是支持VLA的,而且是在栈上分配的内存

Visual C++

我自己在visual studio 2017上尝试了一下,要求数组维度必须是常量表达式

参见该链接

https://docs.microsoft.com/en-us/cpp/c-language/c-language-reference?view=vs-2019

The C Language Reference describes the C programming language as implemented in Microsoft C. The book's organization is based on the ANSI C standard (sometimes referred to as C89) with additional material on the Microsoft extensions to the ANSI C standard.

以及维基百科

https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#C

Although the product originated as an IDE for the C programming language, for many years the compiler's support for that language conformed only to the original edition of the C standard, dating from 1989, but not the C99 revision of the standard. There had been no plans to support C99 even in 2011, more than a decade after its publication

也就是visualC++虽然最初是为了C编程开发的,但是对C标准的支持并不够,C99并不怎么支持,后边还有内容会说到,其实后来的visual IDE 对C99的支持是不完备的

VLA的实现原理

既然数组是变长的,编译的时候又不能确定维度,那么我对怎么实现的很感兴趣,查了一些东西,感觉论坛里这个解释很好。

当有多个变长数组分配时,也就是编译器不能用仅有的几个寄存器保存当前的esp时,编译器就会划分一块区域(这块区域也在栈中,而且是先于变长数组分配划分好的)来记录每个数组的首地址。

例如,我昨晚试验的程序有9个变长数组,前三个数组的首地址存在三个通用寄存器中,而后面的6个的首地址则放在比如说ebp-40,ebp-44,ebp-48...的位置。然后如果引用第四个数组的元素,比如源代码是ar4[1] = 1;编译器会先取ebp-40的内容到一个临时寄存器,再用该值索引数组。也就是有类似如下的汇编代码:
movl (%ebp-40), %eax
movl $1,        4(%eax)

也就是说,从一个单一的概念模型上来说,对于碰到变长数组的情形,编译器可以按一个指针的大小为其预留一个slot,然后到运行的时候esp-eax分配了空间以后,把当前esp,即数组的首地址放入到这个slot中。以后对数组的引用,就要进行两次访存,一次取到数组的首地址,一次访问真正的数组元素。这与以前的数组访问的开销是不同的,以前的数组元素访问之需要一次访存操作,而变长数组的下标访问有点类似于指针的下标访问了。
变长数组的存储分配是在运行时,并且访问也需要两次访存,比原来的数组访问开销要大,但它与动态分配malloc还是有区别的。由于变长数组分配在栈中,只需要改变esp的值就能达到分配的目的,而malloc分配则需要runtime system来进行heap management,也就是说分配的时候需要一定的search operation来得到一块连续的存储,而释放的时候也要执行相应的代码来使得这块存储available for future use。

所以,变长数组的开销还是小于malloc的。

建议

根据个人的工作经验,还是不太建议使用变长数组,另外这里要注意的是 变长数组而不是动态数组,通常我们谈到动态数组就想到 malloc分配内存,或者是vector,如果遇到使用不确定长度的数组,还是使用vector吧。再者不是所有的编译器都支持VLA。

参考资料

https://www.cnblogs.com/hazir/p/variable_length_array.html

https://blog.csdn.net/qq_41024819/article/details/80113111

https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#C

https://en.wikipedia.org/wiki/Variable-length_array#C99

https://docs.microsoft.com/en-us/cpp/c-language/c-language-reference?view=vs-2019

《C++ Primer 中文版》第五版

https://bbs.csdn.net/topics/90350681

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值