第10章 指针与数组(6 变长数组)

发布于 2021-09-06  420 次阅读


变长数组(VLA)

为何只把数组的行数作为函数的形参,而列数却内置我在函数体内。
#define COLS 4
int sum2d(int ar[][COLS],int rows)
{
    int r;
    int c;
    int tot;

    for(r = 0;r < rows; r++)
        for(c = 0;c < COLS;c++)
            tot += ar[r][c];
    return tot;
}
假设声明了下列数组:
int array1[5][4];
int array2[100][4];
int array3[2][4];
可以用sum2d函数分别计算这些数组的元素之和:
tot = sum2d(array1,5);
tot = sum2d(array2,100);
tot = sum2d(array3,2);
sum2d之所以能处理这些数组,是因为这些数组的列数固定为4,而行数被传递给形参rows,rows是一个变量。但是如果是要计算6×5的数组,就不能用这个函数,必须船舰一个COLS为5的函数。因为C规定,数组的维数必须是常量,不能用变量来代替COLS。
要创建一个能处理任意大小的二维数组函数,比较繁琐(必须把数组作为一维数组传递,然后让函数计算每行的开始处)。
鉴于此,C99新增了边长数组(variable-lengh array,VLA),允许使用变量表示数组的维度,如下所示:
int quarters = 4;
int regions = 5;
double sales[regions][quarters];        //宇哥边长数组
变长数组有一些限制。变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern存储类别说明符,而且不能在声明中初始化它们。最终C11把变长数组作为一个可选特性,而不是必须强制实现的特性。
注意:变长数组不能改变大小
变长数组中的“变”不是指可以修改已创建数组的大小,一旦创建了变长数组,它的大小则保持不变。这里的变指的是:在创建数组时,可以使用变量指定数组的维度。
由于变长数组是C语言的新特性,目前完全支持这一特性的编译器不多。
Q:如何编写一个函数,计算int的二维数组所有元素之和。
首先要声明一个带二维变长数组参数的函数。:
int sum2d(int rows,int cols,int ar[rows][cols]);        //ar是一个变长数组(VAL)
注意前两个形参(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[*][*]);
其次,该函数的定义如下:
int sum2d(int rows,int cols,int [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二维数组,
#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];

    for(i=0;i<rs;i++)
        for(j=0;j<cs;j++)
            varr[i][j]=i*j+j;

    printf("3x4 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,COLS,morejunk));

    printf("3x10 array\n");
    printf("Sum of all elements = %d\n",sum2d(rs,cs,varr));

    return 0;

}
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;
}

file

需要注意的是,在函数定义的形参列表中声明的变长数组并未实际创建数组,和传统的语法类似,变长数组名实际上是一个指针。这说明带变长数组形参的函数实际上是在原始数组中处理数组。新词可以修改传入的数组。
下面的代码指出指针和实际数组是何时声明的:
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是一个nxm的int数组
    temp[0][0] = 2; //设置temp的一个元素是2
    ar[0][0] =2;    //设置thing[0][0]为2
}
如上代码所示调用twoset()时,ar成为指向thing[0]的指针,temp被创建为10X6的数组。因为ar和ting都是指向thing[0]的指针,ar[0][0]与thing[0][0]访问的数据位置相同。
const 和数组大小
是否可以在声明数组时使用const变量?
const int SZ = 80;
...
double ar[SZ];  //是否允许?
C90标准不允许。数组的到校必须是给定的整型常量表达式,可以是整型常量组合,入20、sizeof表达式或其他不是const的内容。由于C实现可以扩大整型常量表达式的范围。所以可能会允许使用const。但是这种代码可能无法移植。
C99/C11标准允许在声明变长数组时使用const变量。所以该数组的定义必须是声明在块中的自动存储类别数组
变长数组还允许动态内存分配,这说明可以在程序运行时指定数组大小。普通C数组都是静态内存分配,即在编译时确定数组的大小。由于数组大小是常量,所以编译器在编译时就知道了。

复合字面量

假设要给int类型形参的函数传递一个值,要传递int类型的变量,但是也可以传递int类型常量,如5.在C99标准以前,对于带数组形参的函数,情况不同,可以传递数组,但是没有等价的数组常量,
C99新增加了复合字面量(compound literal)。字面量是除了符号常量外的常量。例如,5是int类型字面量,81.3是double类型的字面量,'y'是char类型的字面量,'elephant'是字符字面量。发布C99标准的委员会认为v,ruguo有代表数组和结构内容的复合字面量,在编程时会更加方便。
对于数组,复合字面量类似数组初始化列表,前面是用括号括起来的类型名。例如:
int diva[2]={10,20};
下面的复合字面量创建了一个和diva数组相同的匿名数组,也有两个int类型的值:
(int [2]){10,20}        //复合字面量
注意,去掉声明中的数组名,留下的int[2]即是复合字面量的类型名。
初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略大小,编译器会自动计算数组当前元素的个数:
(int []){50,20,90}      //内含3个元素的复合字面量
因为复合字面量是匿名的,所以不能先创建然后在使用它,必须在创建的同时使用它的同时使用它。使用记录指针记录地址就是一种用法。:
int *pt1;
pt1 = (int [2]){10,20};
注意,该复合字面量的字面常量与上面创建的diva数组的字面量是完全相同的。与有数组名的数组类似,复合字面的类型名也代表首元素的地址,所以可以把它赋给指向int的指针。然后便可使用这个指针。例如,本例中*pt1是10,pt1[1]是20。
还可以把复合函数字面量作为实际参数传递给带有匹配形式参数的函数:
int sum(const int ar[],int n);
....
int tota13;
tota13 = sum((int []){4,4,4,5,5,5},6);
这里,第一个参数是内含6个int类型值的数组,和数组名类似,这同时也是该数组首元素的地址。这种用法的好处是,把信息传入函数之前,不用创建数组,这是复合字面量的典型用法。
可以把这种用法应用于二维数组或多维数组。例如:
int (*pt2)[4];      //声明一指向二维数组的指针,该数组内含2个数组元素
                    //每个元素是内含4个int类型的数组
pt2 = (int [2][4]){{1,2,3,-9},{4,5,6,-8}};
//  flc.c   ---有趣的常量
#include<stdio.h>
#define COLS 4
int sum2d(const int ar[][COLS],int rows);
int sum(const int ar[],int n);
int main(void)
{
    int total1,total2,total3;
    int *pt1;
    int (*pt2)[COLS];

    pt1 = (int[2]){10,20};
    pt2 = (int[2][COLS]){ {1,2,3,-9},{4,5,6,-8} };

    total1 = sum(pt1,2);
    total2 = sum2d(pt2,2);
    total3 = sum((int[]){4,4,4,5,5,5},6);

    printf("total1 = %d\n",total1);
    printf("total2 = %d\n",total2);
    printf("total3 = %d\n",total3);

    return 0;
 } 
int sum(const int ar[],int n)
{
    int i;
    int total = 0;

    for(i = 0;i<n;i++)
        total += ar[i];

    return total;
}
int sum2d(const 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 0;
}

file

复合字面量是提供只临时需要的值的一种手段。复合字面量具有块作用域,这意味着一旦离开定义复合字面量的块,程序将无法摆正该字面量是否存在。也就是说,复合字面量的定义再最内层的花括号中。

10.10 关键概念

数组用于存储相同类型的数据。C把数组看作是派生类型,因为数组是建立在其他类型的基础上。也就是说,无法简单地声明一个数组。在声明数组时,必须说明其元素的类型,如int类型的数组、float类型的数组,或其他类型的数组。所谓其他类型也可以是数组类型,这种情况下,创建的是数组的数组(或称为二维数组)
通常编写一个函数来处理数组,这样在特定的函数中解决特定的问题,有助于实现程序的模块化。再把数组名作为实际参数是,传递给函数的不是整个数组,而实数组的地址(因此,函数对应的形式参数是指针)。为了处理数组,函数必须知道从何开始读取数据和要处理多少个数组元素。数组地址提供了“地址”,“元素个数”可以内置在函数中或作为单独的参数传递。第二种方式更普遍,因为这样做可以让同一个函数处理不同大小的数组。
数组和指针的关系密切,同一个操作可以用数组表示法或执政表示法。他们之间的关系允许你在处理数组的函数中使用数组表示法,即使函数的形式参数是一个指针,而不是数组。
对于传统的C数组,必须用常量表达式指明数组的大小,所以数组大小在编译时就已确定。C99/C11新增了变长数组,可以用变量表示数组大小。这意味着变长数组的大小延迟到程序运行时才确定,

10.11 本章小结

数组是一组数据类型相同的元素。数组元素按顺序存储在内存中,通过整数下标(或索引)可以访问各元素。在C中,数组的首元素的下标是0,所以对于内含n个元素的数组,其最后一个元素的下标是n-1.
作为程序员,要确保使用有效的数组下标,因为编译器和运行的程序都不会检查下标的有效性。
声明一个简单的一维数组形式如下:
type name[size];
这里,type是数组中每个元素的数据类型,name是数组名,size是数组元素的个数。对于传统的C数组,要求size是整型常量表达式。但是C99/C11允许使用整型非常量表达式。这种情况下的数组被称为变长数组。
C把数组名解释为该数组首元素的地址。换言之,数组名与指向该数组首元素的指针等价。概括的说,数组和指针的关系十分密切。如果ar是一个数组,那么表达式ar[i]和*(ar+1)等价。
对于C语言而言,不能把整个数组作为参数传递给函数,但是可以传递数组的地址。然后函数可以使用传入的地址操控原始数组。如果函数没有修改原始数组的意图,应在声明函数形式参数时使用关键字const。在被调函数中可以使用数组表示法或指针表示法。无论用哪种表示法,实际上使用的都是指针变量。
指针加上一个整数或递增指针,指针的值以所指向的对象大小为单位改变。也就是说,如果pd指向一个数组的8字节double类型值,那么pd+1意味者其值+8,以便它指向该数组的下一个元素。
二维数组即是数组的数组。
double sales[5][12];
该数组名为sales,有5个元素,每个元素都是一个内含12个double类型值的数组。第一个一维数组是sales[0],第二个一维数组是sales[1],以此类推,每个元素都是内含12个double类型的数组。使用第二个下标可以访问这些一维数组中的特定元素。例如:sales[2][5]是sales[2]的第六个元素,而sales[2]是sales的第三个元素。
C语言传递多维数组的传统方法是把数组名(即数组的地址)传递给类型匹配的指针形参,声明这样的指针形参要指定所有的数组维度,除了第一个维度。传递的第一个维度通常作为第二个参数。例如
void display(double ar[][12],int rows);
...
display(sales,5);
变长数组提供第二种语法,把数组维度作为参数传递,在这种情况下,对应函数原型和函数调用如下:
void display(int rows,int cols,double ar[rows][cols]);
...
display(5,12,sales);
虽然上述讨论中使用的是int类型的数组和double类型的数组,其他类型的数组也是如此。然而,字符串有一些特殊的规则,这是由于其末尾的空字符所致。有了这个空字符,不用传递数组的大小。函数通过检测字符串的末尾也知道在何处停止。

擦肩而过的概率