10.8 变长数组(VLA)
为何只把数组的行数作为函数的形参,而列数却内置在函数体内。例如:
#define COLS 4
int sum2d( int ar[][COLS], int rows ){
int r;
int c;
int tot = 0;
for( r = 0; r < rows; r++ ){
for( c = 0; c < COLS; c++ ){
tot += ar[r][c];
}
}
return tot;
}
假设声明了下列数组:
int array[5][4];
int array2[100][4];
int array3[2][4];
可以用sum2d()函数分别计算这些数组的元素之和:
tot = sum2d( array, 5 ); //5*4数组的元素之和
tot = sum2d( array2, 100 ); //100*4数组的元素之和
tot = sum2d( array3, 2 ); //2*4数组的元素之和
sum2d()之所以能处理这些数组,是因为这些数组的列数固定为4,而行数被传递给形参rows,rows是一个变量。如果列数不为4,这就不能使用这个函数。因为C规定,数组的位数必须为常量,不能用变量来代替COLS。
要创建一个能处理任意大小二维数组的函数,比较繁琐(必须把数组作为移位数组传递,然后让函数计算每行的开始处)。而且,这种方法不好处理FORTRAN的子例程,这些子例程都允许在函数调用中指定两个维度。虽然FORTRAN是比较老的编程语言,但是在过去的几十年里,数值计算领域的专家已经用FORTRAN开发出许多有用的计算库。C正逐渐替代FORTRAN,如果能直接转换现有的FORTRAN库就好了。
鉴于此,C99新增了变长数组(variable-length array, VLA),允许使用变量表示数组的维度。例如:
int quarters = 4;
int regions = 5;
double sales[regions][quarters]; //一个变长数组(VLA)
变长数组必须是自动存储类型,这意味着无论在函数中声明还是作为函数的形参声明,都不能使用static或extern存储类别说明符。而且,不能在声明中初始化它们。最终,C11把变长数组作为一个可选特性,而不是必须强制实现的特性。
注意,变长数组不能改变大小
变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变,这里的“变”指的是:在创建数组时,可以使用变量指定数组的维度。
变长数组是C语言的新特性,目前完全支持这一特性的编译器不多。
计算二维数组所有元素之和。
声明一个带二维变长数组参数的函数,如下所示:
int sum2d( int rows, int cols, int ar[rows][cols] ); //ar是一个变长数组(VLA)
注意前两个形参(rows和cols)用作第3个形参二维数组ar的两个维度。因为ar的声明要使用rows和cols,所以在形参列表中必须声明ar之前声明这两个形参。因此,下面的原型是错误的:
int sum2d( int ar[rows][cols], int rows, int cols ); //无效的顺序
C99/C11标准规定,可以省略原型中的形参名,但是在这种情况下,必须用星号来代替省略的维度:
int sum2d( int, int, int ar[*][*] ); //ar是一个变长数组(VLA),省略了维度形参名
其次,该函数的定义为:
int sum2d( int rows, int cols, int ar[rows][cols] ){
int r;
int c;
int tot = 0;
for( r = 0; r < rows; r++ ){
for( c = 0; c < cols; c++ ){
tot += ar[r][c];
}
}
return tot;
}
该函数头与传统的C函数不同外,还把符号常量COLS替换成变量cols。这是因为在函数头中使用了变长数组。由于用变量代表行数和列数,所以新的sum2d()现在可以处理任意大小的二维int数组。
程序要求编译器支持变长数组特性。另外,该程序还演示了以变长数组作为形参的函数既可处理传统C数组,也可处理变长数组。
//vararr2d.c -- functions using VLAs
#include <stdio.h>
#define ROWS 3
#define COLS 4
int sum2d(int rows, int cols, int ar[rows][cols]);
int main(void)
{
int i, j;
int rs = 3;
int cs = 10;
int junk[ROWS][COLS] = {
{2,4,6,8},
{3,5,7,9},
{12,10,8,6}
};
int morejunk[ROWS-1][COLS+2] = {
{20,30,40,50,60,70},
{5,6,7,8,9,10}
};
int varr[rs][cs]; // VLA
for (i = 0; i < rs; i++)
for (j = 0; j < cs; j++)
varr[i][j] = i * j + j;
printf("3x5 array\n");
printf("Sum of all elements = %d\n",
sum2d(ROWS, COLS, junk));
printf("2x6 array\n");
printf("Sum of all elements = %d\n",
sum2d(ROWS-1, COLS+2, morejunk));
printf("3x10 VLA\n");
printf("Sum of all elements = %d\n",
sum2d(rs, cs, varr));
return 0;
}
// function with a VLA parameter
int sum2d(int rows, int cols, int ar[rows][cols])
{
int r;
int c;
int tot = 0;
for (r = 0; r < rows; r++)
for (c = 0; c < cols; c++)
tot += ar[r][c];
return tot;
}
/* 输出:
*/
需要注意的是,在函数定义的形参列表声明的变长数据并未实际创建数组。和传统的语法类似,变长数组名实际上是一个指针。这说明带变长数组形参的函数实际上是在原始数组中处理数组,因此可以修改传入的数据。下面的代码段指出指针和实际数组是如何声明的:
int thing[10][6];
twoset( 10, 6, thing );
...
void twoset( int n, int m, int ar[n][m] ) //ar是一个指向数组(内含m个int类型的值)的指针
{
int temp[n][m]; //temp是一个n*m的int数组
temp[0][0] = 2; //设置temp的一个元素为2
ar[0][0] = 2; //设置thing[0][0]为2
}
调用twoset()时,ar成为指向thing[0]的指针,temp被创建为10*6的数组。因为ar和thing都是指向thing[0]的指针,ar[0][0]与thing[0][0]访问的数据位置相同。
const和数组的大小
是否可以在声明数组时使用const变量?
const int SZ = 80;
...
double ar[SZ]; //是否允许?
C90标准不允许(也可能允许)。数组的大小必须是给定的整型常量表达式,可以是整型常量组合,如sizeof表达式或其他不是const的内容。由于C实现可以扩大整型常量表达式的范围,所以可能会允许使用const,但是这种代码可能无法移植。
C99/C11标准允许在声明变长数组时使用const变量。所以该数组的定义必须是声明在块中的自动存储类别数组。
变长数组还允许动态分配内存,这说明可以在程序运行时指定数组的大小。普通C数组都是静态内存分配,即在编译时确定数组的大小。由于数组大小是常量,所以编译器在编译时就知道了。