阅读:0

二维数组,C语言二维数组完全攻略

 数学中的行列矩阵,通常使用二维数组来描述,即用二维数组的第一维表示行,第二维表示列;生活中凡是能抽象为对象及对象的若干同类型属性的问题,一般用二维数组来描述。

例如,若表示一个班级学生的语文、数学、外语、C 语言等 4 门课的成绩数据。该问题可把每个学生看成一个对象,用二维数组的第一维来表示,如果有 50 个学生,则可设定二维数组第一维的大小为 50;成绩可看成每个对象的属性,且均可使用整型表示,可用二维数组的第二维来表示,每个对象(学生)含 4 个属性(4 门课程),故第二维大小可设为 4。

再比如,某公司若统计某产品的某个月份的销量数据,该问题可以把一周当成一个对象,一个月含 4 周,故 4 个对象,二维数组第一维可设为 4;日销售量可看成每个对象的属性,可用二维数组的第二维表示,对象(每周)含有 7 个属性(7 天的日销售量),故二维数组的第二维可设为 7。

二维数组的定义

同一维数组一样,既支持 C89 标准的二维静态数组,又支持 C99 标准的二维动态数组或变长数组。某些 C 编译器还没更新到支持 C99 标准的语法,故可能在一些编译器中变长数组会报错。如无特殊说明,教程中所指二维数组,均默认为静态数组。

静态二维数组定义的一般格式为:

类型 数组名[第一维大小][第二维大小];

其中,第一、二维的大小一般均为常量表达式。

例如:
int a[4][5];
定义了一个 4 行 5 列的 int 型二维数组 a。
float sc[3][4];
定义了一个 3 行 4 列的 float 型二维数组 sc。

如下二维数组的定义形式均是错误的。
int a[][3];//错误。编译器无法确定所需空间
int a[2][];//错误。缺少列下标,编译器无法确定所需空间
动态数组例子如下(仅做了解)。
int n=2;
int a[n][3];//动态数组,正确的C99语法。但在某些编译器中可能报错
int a[2][n];//动态数组,正确的C99语法
定义时未初始化的数组,其数据元素的值一般为无意义的随机值,如:
int a[2][3];//该数组的6个元素均为随机值
可以把二维数组看成一个特殊的一维数组,它的每个元素又是一个一维数组。例如,定义一个表示 3 个学生 4 门课程成绩的二维数组:
int sc[3][4];
定义了一个 3 行 4 列的二维数组 sc,该二维数组可表示 3 个对象(学生),从这个角度看,该二维数组可以看成含 3 个对象(学生)的一维数组,3 个对象(元素)分别为:sc[0]、sc[1]、sc[2],其中 sc 为该一维数组名。

每个对象(元素)sc[i] 又是一个包含 4 个属性(4 门成绩)的一维数组,4 个属性分别 为:sc[i][0](语文)、sc[i][1](数学)、sc[i][2](外语)、sc[i][3](C 语言)。每一行表示一个学生,每一列表示一门课程,形成如下所示的行列矩阵形式。

                           语文     数学     外语   c语言
sc[0](第一个学生)-----sc[0][0] sc[0][1] sc[0][2] sc[0][3]

sc[1](第二个学生)-----sc[1][0] sc[1][l] sc[1][2] sc[1][3]

sc[2](第三个学生)-----sc[2][0] sc[2][1] sc[2][2] sc[2][3]

二维数组名 sc 是首对象(第一个学生)sc[0] 的地址,即 sc=&sc[0]。二维数组名加 1 表示跳过一个对象,即 sc+1 为第二个对象(学生)sc[1] 的地址,sc+2 为第三个对象(学生)sc[2] 的地址。

从行列式角度分析,二维数组名即首行的地址,C 语言中的地址一般均是空间首地址。故二维数组名是首行首地址,该数组名加 1 表示跳过一整行,到达第二行的首地址,以此类推。

【例 1】以下二维数组 sc 用于保存 3 个学生的 4 门课程(语数外及 C 语言)成绩。根据程序的运行结果,分析二维数组名的含义。
#include<stdio.h>
int main (void)
{
    int sc[3][4];
    printf("sc=%p\n",sc);
    printf ("&sc[0]=%p\n",&sc[0]);
    printf("sc+1=%p\n",sc+1);
    printf("sc+2=%p\n",sc+2);
    return 0;
}
程序某次运行的结果:
sc=0060FEE0
&sc[0]=0060FEE0
sc+1=0060FEF0
sc+2=0060FF00

程序分析:
1) 本例中定义的二维数组为 3 行 4 列,含 3 个学生对象,对应 3 行,每个学生对象包括 4 个属性,对应 4 列。

2) 二维数组名 sc 相当于首对象即第一个学生 sc[0] 的地址,即 sc==&sc[0]。输出地址一般使用格式控制符 %p 或 %x 或 %08x。由某次运行结果可知,sc 和 &sc[0] 的值均为十六进制数 0060FEE0。

由此可得:二维数组名为首对象或首行的地址。

3) 二维数组名加 1 表示跳过一个对象(一行)的空间,为下一个对象(下一行)的地址。即跳过一个对象所有属性(一行中所有列元素)对应的空间,到达下一个对象(下一行)的起始位置。

而本例中一个对象有 4 个属性,每个属性均为整型变量,在 VC++6.0 中占 4 字节,故一个对象(一行)共占 4X4=16 个字节。故 sc、sc+1、sc+2 均相差 16 个字节,转换为十六进制为 0x10。如运行结果所示。

4) 程序每次运行为某变量空间分配的起始地址可能有差别。

二维数组的引用

二维数组的引用格式为:

数组名[行下标][列下标];

注意引用数组元素时不能加类型,行下标、列下标均从 0 开始。且行下标和列下标的形式可以为常量、变量或表达式。

若 M 和 N 均为已定义的正整数:
int i,j; //i和j分别表示行下标和列下标.
int a[M][N]; //定义了一个M行N列的二维整型数组a
行下标i的范围为 0〜M-1。

列下标j的范围为 0〜N-1。

称第几个时,习惯上是从第 1 个开始,第 2 个,第 3 个,…,而不从第 0 个开始。但是二维数组的行下标及列下标均是从 0 开始的,为统一起见,本书中对数组元素的引用,采用行列序号来描述,如 a[i][j] 为 i 行 j 列元素,而不说成第 i 行第 j 列元素。例如:
int a[3][4];
a[0][0]; //为0行0列元素
a[2][1]; //为2行1列元素
a[1][1+2]; //为1行3列元素
例如:
int n=4, i, j ;
int a[3][4]; //定义了一个3行4列的二维数组a
对该数组的引用形式为 a[i][j],其中,行下标 i 的范围为 0〜2,列下标 j 的范围为 0〜3。以下对该二维数组的引用均是正确的。
a[2][n-1]=a[0][1+1] +a[2][2-1]; //a[0][2]、a[2][1]相加后赋给a[2][3]
a[3-1][n-1];//引用2行3列元素
以下对该二维数组的引用形式是错误的。
a[-2][1]; //行下标-2越界,应为0〜2
a[3][2]; //行下标3越界,应为0〜2
a[2][n]; //列下标n=4越界,应为0〜3
a[2,3]; //引用形式错误,应为a[2][3]
a[2][]; //缺少列下标
int a[1][n-2]; //引用不能加类型,若为定义,第二维含有变量n,错误
【例 2】定义一个 2 行 3 列的二维数组,从键盘上输入 6 个数值,依次给该二维数组的每个元素赋值,按每行三个元素输出该二维数组及所有元素的和。

程序分析:
例题涉及对二维数组的赋值,一般使用双重循环,外层循环控制行下标,内层循环控制列下标。二维数组的每个元素 a[i][j] 像普通变量一样使用,使用 scanf 函数输入时,一定要加 &。输入时,可以输入完 6 个数据后按一次回车键。

本题要求每输出一行后换行,即输出完一行中的所有列数据(内层循环结束)后换行。

实现代码:
#include<stdio.h>
int main (void)
{
    int a[2][3]; //先定义,后赋值
    int i,j,s=0;
    printf("Input 6 integers:");
    for(i=0;i<2;i++)
    {
        for (j=0; j<3; j++)
        {
            scanf ("%d",&a[i][j]); //勿忘 &
            s+=a[i][j] ; //与上一条语句不能颠倒
        }
    }
    for(i=0;i<2;i++)
    {
        for (j=0; j<3; j++)
            printf("%d\t",a[i][j]); //使用\t 的作用
        printf ("\n"); //注意该输出换行符的位置
    }
    printf("s=%d\n", s);
    return 0;
}
运行结果为:
Input 6 integers:1 2 3 4 5 6
1       2       3
4       5       6
s=21

二维数组的初始化

二维数组可以先定义,后赋值,在显式赋值之前,二维数组的各数据元素是随机值。也可以在定义二维数组的同时,采用初始化列表的形式对其元素赋初值,称为二维数组的初始化

二维数组的初始化方式通常有以下几种。

1) 分行给出初始化数据,且每行的初始化数据个数等于列数。例如:
int a[2][3]={{1,2,3},{4,5,6}};
该初始化列表给出了两行数据,每一行数据用一对大括号 {} 括起来,一行中的数据及行与行之间均用逗号隔开。这是一种较常用的二维数组的初始化方式。

该初始化语句相当于如下 6 条赋值语句。
a[0][0]=1; a[0][1]=2; a[0][2]=3;
a[1][0]=4; a[1][1]=5; a[1][2]=6;
由于初始化列表中明确给出了两行数据,故定义该数组时,其第一维的大小可省略,编译器能间接算出该数组的行数为 2,故依然可以确定其空间大小,因此,在对二维数组进行初始化时,其第一维的大小可以省略,即写成如下形式:
int a[][3]={{l,2,3},{4,5,6}};
注意:第二维的大小一定不能省略!如下初始化均是错误的。
int a[2][] = {{l,2,3},{4,5,6}}; //错误。不能省略第二维大小
int a[][] = {{l,2,3}, {4,5,6}}; //错误。不能省略第二维大小
int a[][3]; //错误。没有提供初始化列表时,两维的大小都必须显式给出
int a[2][3] = {{l,2,3},{4,5,6},{7,8,9}}; //错误。初始行数多于数组行数
如果把上面一条初始化语句改为省略第一维的大小,便是正确的,即:
int a[][3] = {{1,2,3}, {4,5,6}, {7,8,9}}; //正确。间接得知该数组为三行
2) 分行给出初始化数据,但每行的初始化数据个数少于列数。例如:
int a[2][3]={{l,2},{4}};
该初始化列表给出了两行数据,第一行给出两个数据,少一个,对 int 型默认为补 0;第二行仅给出一个数据,少两个,补两个 0。所以上述初始化语句相当于:
int a[2][3]={{1,2,0},{4,0,0}};
同理:
int a[][3]={{0},{0}};
相当于:
int a[][3] = {{0,0,0},{0,0,0}}; //2行3列
3) 初始化数据没有分行,容易产生混乱,不推荐这种方式。

如果初始化数据的个数是列数的整数倍,即:
int a[2][3]={l,2,3,4,5,6};
初始化数据以列数三个为一组,共分为两组,且每组数据个数恰好等于列数 3,故第一组赋值给第 1 行,第二组赋值给第 2 行。初始化后,数组中各元素为:

1 2 3
4 5 6

例如:
int a[2][3]={1,2,3,4};
由于该二维数组列数为 3,初始化数据以三个为一组,共分为两组,但第二组仅一个数据,少于列数 3,对 int 型数组用 0 补齐。故第一组数据 1,2,3 赋值给第一行,第二组补齐为三个数据 4,0,0 后,赋值给第二行。相当于 int a[][3]={1,2,3,4,0,0}; 初始化后, 数组中各元素为:

1 2 3
4 0 0

int a[][3] = {1,2,3,4,5,6,7,8}; //正确,可间接得知该数组为三行,不推荐
第三行不够三个数据,用 0 补齐。故该语句相当于:
int 3[][3] = {1,2,3,4,5,6,7,8,0};
初始化后,数组中各元素为:

1 2 3
4 5 6
7 8 0

如果第一维大小没有省略,则初始化数据的个数一定不能超过数组元素的总个数,否则报错。例如:
int a[2][3] = {1,2,3,4,5,6,7,8};//错误。初始数据个数8多于数组总个数6

二维数组的存储

二维数组在逻辑(表现形式)上可理解为矩阵形式(分行分列),但其物理存储形式却是连续的,即存完第一行,在其后面接着存储第二行,第三行,…,如无特殊说明,本书中涉及对二维数组的表述一般指的是其逻辑形式即矩阵形式。

如 int a[3][4]; 其逻辑结构是 3 行 4 列的矩阵形式,如图 1)  所示,而其存储结构是连续的(线性的),如图 2) 所示。

二维教组的应用举例

【例 3】一个班级有 N 名学生,每个学生有 4 门课程(语文、数学、外语、C 语言),计算每个学生的平均分。编写程序实现该功能需求。

问题分析:该问题可把每个学生当成一个对象,而每个对象(学生)有 5 个属性:4 门课程成绩及平均分。故该问题可使用二维数组来处理数据,该二维数组含有 N 个对象,故第一维的大小为 N,每个对象有 5 个属性,故第二维的大小为 5。为便于验证,本例中学生个数 N 设为 3 个。

实现代码:
#include<stdio.h>
#define N 3
int main (void)
{
    float a[N][5],sum; //sum用来累加每个学生4门课的总成绩
    int i, j ;
    printf ("输入%d个学生信息(语、数、外、C语言成绩):\n",N);
    for(i=0;i<N;i++)
    {
        sum=0.0; //对每个学生sum均初始为0
        printf ("NO%d:",i+1);
        for (j=0; j<4; j++) //每个学生仅输人4门课成绩,注意j<4
        {
            scanf("%f",&a[i][j]);
            sum+=a[i][j];
        }
        a[i][4]=sum/4; //每个学生的平均成绩是计算出来的
    }
    printf ("\n学号\t语文\t数学\t外语\tC语言\t平均成绩\n");
    for(i=0;i<N;i++)
    {
        printf("NO%d:\t", i+1);
        for(j=0;j<5;j++)
            printf ("%.1f\t",a[i][ j]) ; //保留一位小数,注意格式%.1f
        printf ("\n");
    }
    return 0;
}
运行结果为:
输入3个学生信息(语、数、外、C语言成绩):
NO1:82 91 88 93
NO2:83 84 80 91
NO3:73 79 86 81

学号    语文    数学    外语    C语言   平均成绩
NO1:    82.0    91.0    88.0    93.0    88.5
NO2:    83.0    84.0    80.0    91.0    84.5
NO3:    73.0    79.0    86.0    81.0    79.8