物联网工程师之C语言 数组技术
本章重点
• 一维数组的声明、初始化和存取
• 一维数组的应用
• 二维数组的使用
• 数组在内存中的存储方式
与人相比,计算机具有两大优势:计算得快,存储得多。计算速度比人脑快这一点自然不用多说,存储空间也是随着科技的进步越来越大,短短二三十年的工夫,存储设备有了极大发展:从原始的720 KB软盘,到后来几十MB的早期硬盘,再到现在动辄1、2 TB的移动硬盘,都是为了满足人们对数据的渴望。
有了如此大的存储空间,接下来人们面对的问题就是如何高效地存取这些数据。在之前的章节中讨论过C语言中的基本数据类型,可是没有哪种基本数据类型可以容纳如此之多的数据。难道只能逐个定义要用到的变量了么?当然不是。
C语言为大家提供了一个强有力的数据结构来处理大量连续存储的同类型数据,这就是本章中要介绍的知识——数组。
7.1 数组概述
经过前面七章的学习,大家肯定已经了解什么是数据、什么是变量了。那么究竟什么是数组呢?本节将详细介绍数组的基础知识,以帮助大家进入数组的世界。
7.1.1 什么是数组
顾名思义,“数组”就是一组数,是指可以容纳多个值的一种变量类型。C语言要求存入同一个数组中的所有值必须为相同的数据类型。数组中的每个值被称为一个元素,每个元素用起来和普通的变量没有什么区别。
7.1.2 数组的用途
班上的一个学生想用C语言写一个小程序来存储自己的成绩单。他的期中成绩单如表7-1所示。
表 71 学生的成绩单
语文 |
代数 |
几何 |
英语 |
物理 |
化学 |
政治 |
85 |
90 |
88 |
94 |
91 |
75 |
64 |
为了记录该学生每门课的成绩,需要在程序中新建一个数组:
例程 71 定义数组
int score_sheet[7];
在例程7-1中,“int”表示这个数组存放的是整型的数据,“score_sheet”是这个数组的名字——“成绩单”。中括号中间的数字7则是这个数组的大小:这个数组最多可以存放7个整型数据。编译器看到这句声明后,会自动为数组变量score_sheet分配足够的内存。
图7-1就是这个刚刚定义的数组的示意图:
图 71 成绩单数组示意
当然,为了将每门课的成绩存入整型数组,还要把每门课的名字对应到一个整数上。请想想该如何实现。
动手体验:为课程名称创建枚举量
下面来回顾一下如何使用枚举量:
enum courses {CHINESE = 1, ALGEBRA, GEOMETRY, ENGLISH, PHYSICS, CHEMISTRY, POLITICS};
通过枚举量,将“语文”设为1,“代数”设为2,“几何”设为3……这样就可以在数组中存下七门课的成绩啦!
请注意,在C语言中,因为字符串的长度并不固定,所以直接将字符串存入数组有些麻烦。如果程序中要用到的字符串是固定的,往往将其转换为枚举量,再以整数的形式进行存储。
7.1.3 数组的存储方式
C语言中的变量都是存放在内存中的。下面的语句声明了三个整型变量,并为它们赋了初值:
int a = 0;
int b = 1;
int c = 2;
同样地,可以用数组完成同样的工作:
int array[3] = {0, 1, 2};
图7-2展示了在内存中这两种声明的存储方式:
图 72 数组的存储方式
容易看出,它们是完全一样的!对计算机而言,这两种定义方式没有区别:数据都是以整型变量的形式存放在内存里,每个变量(对于数组array而言,则是每个元素)的大小为4字节。一般而言,连续声明的同类型变量会被存储在内存中相邻的位置(不考虑编译器的优化等特殊情况),而数组也会占据内存中连续的位置。
对于数组array而言,array[2]的真正含义是什么呢?其实很简单,编译器知道array这个数组的起始地址,还知道array数组中每个元素的大小(因为array为整型数组,所以每个元素的大小为4字节),那么array[2]相当于告诉计算机“访问从array数组起始地址处向后偏移两个整型变量大小的位置,并将其也看作整型变量”。
7.2 一维数组
上一节中学习了数组的基础知识,并且接触了最基本的数组定义方法。下面来看看如何初始化刚刚定义的数组,并将其它的数值存入数组中。
7.2.1 一维数组的定义和初始化
1. 一维数组的定义
上一小节用下面的语句定义了一个大小为7的整型数组score_sheet:
int score_sheet[7];
“score_sheet”是数组的名称。为数组取名字和为变量取名字是完全相同的,因此也要遵循相同的规范——因为数组也是一种变量。“int”则用来指定数组中存储元素的类型。同理,可以声明一个大小为10的字符型数组:
例程 72 定义字符型数组
char a[10];
也可以声明一个大小为25的short型数组:
例程 73 定义short型数组
short b[25];
2. 一维数组的初始化
大家为该学生的成绩单建立了数组,还为每门课分配了一个整数(枚举量),还差什么工作没有做呢?对,数组score_sheet还是空的,里面没有数据。既然成绩单上的成绩是固定的,那就可以在定义成绩单数组的同时完成对数组的赋值。这个过程叫做数组的初始化。
使用下面的语句初始化学生成绩单数组,如例程7-4所示:
例程 74 初始化成绩单数组
int score_sheet[7] = {85, 90, 88, 94, 91, 75, 64};
图7-3是刚刚初始化过的数组的示意图:
图 73 初始化后的数组示意
假设学生在学校里只上这七门课,既然元素的数量(课程的数量)是确定的,那么在定义数组时就可以偷懒了:
例程 75 声明数组,并省略数组的大小
int score_sheet[] = {85, 90, 88, 94, 91, 75, 64};
注意上一行语句中省略了数组的大小。编译器会检测到一共有七个初始元素,于是自动将score_sheet的长度设置为7。
例程 76 声明数组
int score_sheet[7] = {85, 90, 88, 94, 91, 75};
上面的声明删去了政治课的成绩,导致声明的成绩单数组长度为7,可是只提供了6个初始值。这样声明之后,数组score_sheet的长度仍然是7,编译器会自动将缺少的元素(最后一门课的成绩)初始化为0的。
同理可知,如果该学生想初始化一个空的成绩单数组,可以这样写:
例程 77 声明空的成绩单数组
int score_sheet[7] = {0};
这样整个成绩单数组都会被初始化为0。
使用未经初始化的数组会发生什么呢?例程7-8进行了演示。
例程 78 使用未初始化的数组
#include <stdio.h>
int main()
{
int array[10];
int i;
for(i = 0; i < 10; ++i)
{
printf("array[%d] = %d\n", i, array[i]);
}
}
运行后的结果如图7-4所示:
图 74 未初始化的数组
在大家机器上运行的结果可能不同,甚至在同一台机器上多次运行的结果可能也有差别。这是因为在初始化之前,数组元素的值就是原来相应内存地址上的值,是不确定的。所以在声明数组时进行初始化是个好习惯,可以避免很多莫名其妙的错误。
看到C语言程序为他记录了期中考试的所有成绩,该学生很高兴,可他并不知道怎么把每门课的成绩从成绩单数组中取出来。为解决这个问题,在下一小节中,大家将学习如何访问数组中的元素。
多学一招:使用const关键字声明数组
有时在程序中需要用到只读的数组。只读的数组是指在程序中除了初始化之外,只会从数组中读取数值,而不会修改数组中的元素。这种情况下建议使用const关键字来修饰数组声明。
下面的语句声明了一个名为array的只读整型数组:
const int array[10] = {1, 2, 3, 4};
这样程序中就不能修改数组array的值了,编译时,数组中的每个元素都会被作为常量来处理。由于数组的值不能被修改,因此必须在声明时就完成数组的初始化。
下面的程序试图修改一个const数组的值,会发生什么呢?
#include <stdio.h>
int main()
{
const int array[10] = {1, 2, 3, 4};
array[4] = 5;
}
编译时会出现图7-5中的错误:
图 75 试图修改只读数组时发生错误
3. 数组的下标
为了区分并访问数组中的元素,可以通过指定数组名称和元素的位置来唯一确定要访问的那个元素——显然不可能存在两个元素同时占据某个数组相同位置的情形。假设学生的父亲想调取他语文课的成绩,他要怎么实现这个功能呢?下面的表达式可以从成绩单数组中取出第一条记录(也就是语文考试的成绩)。
score_sheet[0]
中括号中的数字就是要访问的元素在数组中的位置,在C语言中称为数组的下标。下标永远从0开始计数,假如数组的大小为N,那么最大的下标就是(N – 1)。从下标为0的元素到下标为(N – 1)的元素正好有N个,说明这个数组的大小就是N。举个例子,某个数组的最大下标为10,那这个数组的大小就是(10 + 1) = 11。如果要读取该学生第四门课的成绩,就要用下面的表达式:
score_sheet[3]
之前定义过的枚举量可以这样使用:
score_sheet[ENGLISH]
这样就取出了第四门课——英语课的成绩。
一般而言,如果有一个长度为N的数组a,它的第一个元素(重复一遍,它的下标为0)可以用a[0]来表示,它的最后一个元素(再重复一遍,最后一个元素的下标为(N – 1))可以用a[N – 1]来表示。有了上面的知识作基础,就可以帮该学生把所有的成绩都读取出来了。
动手体验:输出成绩单中每门课的成绩
在例程7-9中,先定义代表每一门课的枚举量,然后初始化成绩单数组,通过循环输出每门课的成绩,最后使用枚举量访问语文课的成绩。
例程 79 输出成绩单
#include <stdio.h>
enum courses { CHINESE = 1, ALGEBRA, GEOMETRY, ENGLISH, PHYSICS, CHEMISTRY, POLITICS };
int main()
{
int score_sheet[7] = { 85, 90, 88, 94, 91, 75, 64 };
int i = 0;
for (i = 0; i < 7; ++i)
{
printf("该学生第 %d 门课的成绩是 %d。\n", i + 1, score_sheet[i]);
}
printf("该学生在语文考试中的成绩是 %d。\n", score_sheet[CHINESE]);
}
运行后,程序的输出如图7-6所示:
图 76 输出成绩单
脚下留心:数组大小与数组下标的区别
数组的大小和下标都用整数表示,还都放在中括号里,那什么时候中括号中的值是数组大小,什么时候又是指数组的下标呢?
大家一定看出来了,区别很简单:定义数组时使用的是数组的大小(例如 int score_sheet[7];),使用数组中的元素时用到的是数组的下标(例如 score_sheet[0],这是学生语文课的成绩)。
7.2.2 为一维数组赋值
之前通过数组初始化的方式为数组完成了赋值,同样地,在程序执行过程中也是可以修改数组中元素的值的,这叫做为数组赋值。
下面的语句将修改成绩单数组的第四个元素(也就是同学的英语课成绩)为满分:
例程 710 为数组赋值
score_sheet[3] = 100;
要为数组赋值,只要在数组元素后写一个等号,再加上相应的值就可以了。是不是很容易呢?
例程7-11的功能是打印出每个月的天数。
例程 711 打印一年中每个月的天数
#include <stdio.h>
int main()
{
/* 直接初始化前六个月的天数 */
int days[12] = {31, 28, 31, 30, 31, 30};
int i;
/* 用赋值的方式设置后六个月的天数
* 注意数组下标是从 0 开始的,因此 days[6] 就是七月的天数
*/
days[6] = 31;
days[7] = 31;
days[8] = 30;
days[9] = 31;
days[10] = 30;
days[11] = 31;
for(i = 0; i < 12; ++i)
{
printf("%d 月有 %d 天。\n", (i + 1), days[i]);
}
}
程序运行后的结果如图7-7所示。
图 77 输出每个月的天数
注意上述程序输出的是一般情况,即在非闰年时,结果是正确的。闰年的二月有二十九天,而不是二十八天。
多学一招:利用数组找出最大元素
有一个整型数组array,怎样才能找到数值最大的那个元素呢?
基本思路是:先将数组中的第一个数作为最大的数并存起来,然后从第二个数开始,逐个和当前最大的数进行比较;如果被比较的数更大,就将被比较的数作为新的最大数,否则保持原有最大数不变。这样进行一轮循环之后,得到的最大数就是所有数中最大的那个数了。
解决这个问题的示例程序如例程7-12:
例程 712 输出最大数
#include <stdio.h>
int main()
{ int arr[10] = {9, 80, 75, 4, 33, -5, 102, 29, 66, -7};
int max = arr[0];
int i;
for(i = 0; i < 10; ++i)
{
if(arr[i] > max)
{
max = arr[i];
}
}
printf("最大的数是 %d。\n", max);
}
编译并运行程序后,输出见图7-8:
图 78 查找最大值的运行结果
结果102和你手工找到的最大值相同么?如果不同的话,一定是你算错了。
多学一招:利用数组求平均数
该学生最近在练习记账,想用C语言写个程序来计算一周内每天的伙食费均值。你能帮帮他么?
基本思路:使用数组记录每天的伙食费,然后通过循环将每天的伙食费相加,最后除以总天数即可。示例程序见例程7-13:
例程 713 利用数组求平均数
#include <stdio.h>
#define N 7
int main()
{
double fee[N] = {0};
double sum = 0.0;
double average = 0.0;
int i = 0;
/* 输入每天的伙食费 */
for(i = 0; i < N; ++i)
{
printf("请输入第 %d 天的伙食费: ", (i + 1));
scanf("%lf", &fee[i]);
}
for(i = 0; i < N; ++i)
{
sum += fee[i];
}
average = sum / N;
printf("该学生本周平均每天消费 %.2lf 元。\n", average);
}
程序运行的结果如下图(图7-9):
图 79 计算伙食费的平均值
值得注意的一点是,程序中通过预处理指令#define定义了要考虑的天数为7天。这样下次该学生要计算一个月的伙食费花销时,只要修改语句“#define N 7”为“#define N 30”就可以了。编译之前,编译器会自动将代码中的“N”替换成“7”或者“30”。关于预处理指令的更多知识将在后面的章节中加以介绍。
7.2.3 下标越界
虽然在声明数组变量时手动指定了数组的大小,但令人遗憾的是,出于性能考虑,C语言编译器不会检查每次访问数组时使用的下标是否都在允许的范围内。例如成绩单数组的大小为7,大家完全可以访问该数组的第八个元素score_sheet[7]。此时程序会傻傻地去访问“从score_sheet数组起始地址处向后偏移七个整型变量大小的位置”,那么就会发生下标越界的问题。
下标越界会带来什么影响呢?请写个小程序测试一下:
动手体验:下标越界的影响
请在VS中新建一个工程,并输入下列代码:
例程 714 数组的越界访问
#include <stdio.h>
int main()
{
int array[10];
int i = 0;
for(i = 0; i < 20; ++i)
{
array[i] = 0;
}
}
在上面的程序中,第五行声明了一个大小为10的整型数组,但第七行至第十行通过循环为20个数赋了值,这显然会导致下标越界。在Debug模式下运行程序,就可以看到程序的崩溃提示了:
图 710 数组越界访问使程序崩溃
为什么程序会崩溃呢?越界访问把数据写到了数组之外,而在VS的Debug模式下,存放数组的内存空间中有一些专门用来检测越界访问的标记,一旦越界访问发生,那些标记就会被覆盖掉,VC运行时库检测到标记被覆盖,因此使程序报错退出。在Release模式下,出于性能方面的考虑,相关的检测会少很多,因此不是所有的越界访问都会导致程序报错的。
请牢记:C语言中,数组元素的下标从0开始,而不是从1开始;如果数组大小为N,那么末尾元素的下标是(N – 1),而不是N。
动手体验:下标越界一定会使程序崩溃么?
请在VS中新建一个工程,并输入下列代码(例程7-15)。
例程 715 数组越界访问不一定会使程序崩溃
#include <stdio.h>
int main()
{
int array_1[10];
int array_2[10];
int i = 0;
for(i = 0; i < 10; ++i)
{
array_1[i] = -(i + 1) * 1000;
array_2[i] = (i + 1) * 1000;
}
printf("array_1[0] = %d\n", array_1[0]);
printf("array_2[9] = %d\n", array_2[9]);
printf("array_2[12] = %d\n", array_2[12]);
printf("array_2[13] = %d\n", array_2[13]);
}
完成后,请使用Debug模式编译运行程序,屏幕上会输出如下信息(图7-11):
图 711 数组越界访问不一定会使程序崩溃
在上述程序中,通过使用array_2[12]和array_2[13]成功访问到了数组array_1里面的值。这个现象说明什么呢?这说明在内存中,数组array_2位于array_1之前,且两个数组间有8个字节的间隔(可以通过array_2[10]和array_2[11]来访问)。当然也可以通过array_2[12]来修改array_1[0]的值,且不会触发任何错误。实际中,内存访问越界的错误是最难调试的,因为只要访问的内存地址有效,程序就不会立即出错,而是到未来的某个时刻才发生错误——甚至永远不会报错,就像这个示例程序一样。因此在使用下标时一定要慎重啊。
7.3 一维数组的应用
经过前面的学习,大家肯定都见识到了数组的强大功能。如果能将数组和强大的函数结合起来,就更能发挥C语言的威力了。这一节就为大家详细介绍如何将数组作为函数的参数,以及如何保护数组中的元素。
7.3.1 将数组作为函数的参数
普通的变量可以作为函数的参数进行传递,那数组呢?当然也是可以的。假如大家要编写一个函数,接收一个整型数组作为参数,返回这个整型数组中所有元素之和,该如何定义这个函数呢?
回想之前有关函数的知识,这个函数(假定函数名为sum)应该是这样调用的:
int numbers[10] = {1, 2, 3, …};
int s = sum(numbers);
由于numbers是一个整型数组,因此函数sum的定义如下:
int sum(int array[])
{
/* 具体实现略 */
}
注意到函数sum的形参是一个数组(int array[]),但是并没有提供大小。
那要如何实现函数sum呢?下面的例程7-16提供了参考。
例程 716 计算数组中所有元素之和
#include <stdio.h>
int sum(int array[])
{
int i;
int s = 0;
for(i = 0; i < 10; ++i)
{
s += array[i];
}
return s;
}
int main()
{
int arr[10] = {5, 6, 8, 7, 9, 10};
int s = sum(arr);
printf("数组 arr 的所有元素之和为 %d。\n", s);
}
运行程序后输出如图7-12:
图 712 计算数组中所有元素之和
上面实现的sum函数有个缺点:只能用于有10个元素的数组。要想对一个有20个元素的数组求和,就必须修改sum函数的实现,把第六行中的“i < 10”改成“i < 20”,实在是太麻烦了。通用的解决方法是将数组元素的个数作为函数sum的参数传进去,这样函数sum就可以灵活地适用于不同大小的数组了。修改后的程序如下(例程7-17):
例程 717 支持不同大小数组的求和函数
#include <stdio.h>
int sum(int array[], int n)
{
int i;
int s = 0;
for(i = 0; i < n; ++i)
{
s += array[i];
}
return s;
}
int main()
{
int arr[20] = {5, 6, 8, 7, 9, 10};
int s = sum(arr, sizeof(arr) / sizeof(int));
printf("数组 arr 的所有元素之和为 %d。\n", s);
}
运行后的程序给出如图7-13所示结果:
图 713 计算数组中所有元素之和
脚下留心:用数组作为形参时不应直接给定数组大小
大家可能会感到奇怪:为什么不直接在定义sum函数时指定数组参数的大小呢?只要写在形参里面不就可以了吗?像下面这样:
int sum(int array[20])
{
/* 函数实现略 */
}
这样做是不行的。因为对于函数而言,数组作为形参只是提供了数组的地址而已,并不包括数组大小的信息。即使这样的定义可以正常通过编译,但是对于函数sum而言,与函数定义
int sum(int array[])
是完全没有区别的。这意味着形参中提供的数组大小被忽略了。
下面的例程7-18通过sizeof()的结果演示了形参中提供数组大小是没有意义的。
例程 718 sizeof与作为形参的数组
#include <stdio.h>
145 void func_1(int array[3])
146 {
147 printf("sizeof(array[3]) = %d\n", sizeof(array));
148 }
149 void func_2(int array[4])
150 {
151 printf("sizeof(array[4]) = %d\n", sizeof(array));
152 }
153 void func_3(int array[])
154 {
155 printf("sizeof(array[]) = %d\n", sizeof(array));
156 }
157 int main()
158 {
159 int array[20] = {1, 2, 3, 4, 5};
160 printf("sizeof(array[20]) = %d\n", sizeof(array));
161 func_1(array);
162 func_2(array);
163 func_3(array);
164 }
编译并运行程序后,输出如图7-14所示:
图 714 sizeof与作为形参的数组
注意到无论是如何定义函数中数组形参的大小,sizeof(array)的值都是4。而且sizeof的值与传入函数中的实参的大小也没有关系。因此指定数组形参的大小毫无意义,必须通过额外的参数来指定数组实参的大小。
实际上,在使用数组作为函数形参时,下面两种函数定义方式是等价的:
int sum(int array[])
int sum(int *array)
后者的含义是一个整型的指针,代表了一个内存地址,这个内存地址处存储了要传入的整型数组。由于32位的程序中指针(内存地址)占用4个字节,因此在上面的例程里,func_1、func_2和func_3中调用sizeof(array)返回的都是4——其实“int array[]”中的array就是一个指针,sizeof(array)取得的是指针的大小,而不是整个数组的大小。
关于指针的具体概念和应用将在下一章中加以介绍。
7.3.2 保护数组元素
在函数一章中介绍过,实参是不会被函数改变的。但是这个说法并不适用于数组作为参数的情形。这是因为传递普通变量作为实参时,变量(原件)首先被复制了一份(副本),再作为实参传递到函数中——这时函数中所作修改都是对副本的修改,不会对原件有任何影响。但是在传递数组时,由于传递的是数组的地址,因此相当于直接把原件传递给函数了。这时函数对数组所做的修改就会体现在原来的数组中——因为本来就只有一份数组。例程7-19演示了数组作为参数传入函数后,被函数改写的情形。
例程 719 函数改变了数组的内容
#include <stdio.h>
165 void func(int array[])
166 {
167 int i;
168 for(i = 0; i < 5; ++i)
169 {
170 array[i] = 42;
171 }
172 }
173 int main()
174 {
175 int array[5] = {1, 2, 3, 4, 5};
176 int i;
177 printf("调用前:\n");
178 for(i = 0; i < 5; ++i)
179 {
180 printf("array[%d] = %d\n", i, array[i]);
181 }
182 func(array);
183 printf("调用后:\n");
184 for(i = 0; i < 5; ++i)
185 {
186 printf("array[%d] = %d\n", i, array[i]);
187 }
188 }
程序运行后输出如图7-15:
图 715 函数改变了数组的内容
注意到经过函数func的调用后,主函数中array的值被修改成42了。这个特性使程序员可以直接修改原数组中的值。
但是大多数情形下,程序员希望传入函数中的数组和普通的元素一样,都是只读的,在函数中对数组元素所做修改不会被传回调用者那里。如何解决这个问题呢?一个方法是在函数中将数组复制一份,然后只在复制出来的新数组中进行操作——如果数组很大的话,这样会极大降低程序的性能。
另一个方法是将形参设置为只读,这适用于那些函数被设计为不应改变数组内容的情况。如何将形参设置为只读呢?只要使用const关键字修饰形参就可以了。将形参声明为只读后,可以避免程序员在程序中不小心修改数组的值。如果编译器发现对只读数组的修改操作(赋值),将会在编译时报错。
上一小节中实现的函数sum就是个很好的例子:要计算数组中所有元素之和,完全不需要修改数组中元素的值。下面的例程7-20演示了新的求和函数sum。
例程 720 使用const关键字修饰数组形参
1 #include <stdio.h>
189 int sum(const int array[], int n)
190 {
191 int i;
192 int s = 0;
193 for(i = 0; i < n; ++i)
194 {
195 s += array[i];
196 }
197 return s;
198 }
199 int main()
200 {
201 int arr[20] = {5, 6, 8, 7, 9, 10};
207 int s = sum(arr, sizeof(arr) / sizeof(int));
208 printf("数组 arr 的所有元素之和为 %d。\n", s);
209 }
程序运行后的输出如下图:
图 716 计算数组中所有元素之和
脚下留心:理解const型的数组形参
需要注意的是,使用const来修饰数组形参,并不是说传入的原始数组也是只读的——const只是说在该函数中,这个数组应当是只读的、不可修改的;在该函数之外,完全可以照常使用并修改这个数组。
总而言之,如果函数想修改作为参数传入的数组,就不应该使用const来修饰形参;否则最好使用const关键字来修饰形参以避免错误。
7.4 二维数组
除了之前讲过的形式之外,数组还有其它的形式。一根数轴、一张表格、一个魔方都可以是数组的模型。下面就一起来学习什么是二维数组。
7.4.1 数组的维度
前面用到的数组都是一维数组。所谓数组的维度,是指一个数组元素所对应的下标的数量。最简单的一维模型是数轴:
图 717 数轴
注意到数轴上的每个点都可以对应到一个唯一的下标。比如在上面的数轴上,左数第一个点的下标是-2,右数第一个点的下标是5。在一维模型中,不会出现一个点对应多个下标的情形,也不会出现一个下标对应多个点的情形。
既然提到了一维数组,那自然就有二维数组。
表7-2是一张某培训机构的每周课程安排表:
表 72 课程安排表
星期/教室 |
101 |
102 |
103 |
104 |
106 |
星期一 |
C语言基础 |
- |
Java入门 |
- |
- |
星期二 |
C++基础 |
- |
Java入门 |
- |
C语言基础 |
星期三 |
C++基础 |
PHP入门 |
- |
- |
C语言基础 |
星期四 |
- |
- |
Java入门 |
- |
C++基础 |
星期五 |
- |
PHP入门 |
- |
C++基础 |
- |
与前文中成绩单不同,在这张课程表中,一门课是由两个变量决定的,分别是星期几和教室编号。例如星期一的101教室对应着“C语言基础”,星期四的106教室对应于“C++基础”,星期三的103教室则没有安排课程。由行和列构成的表格是生活中最常见的二维模型。如果好学的同学打算去蹭课,拿着这样一张课程安排表是必须的。
如果想要将课程安排表存到一个二维数组里,只要将行标题和列标题转换成对应的下标即可。在转换前请注意,C语言中的数组下标永远是从0开始计算的。转换后的结果如表7-3所示:
表 73 转换后的二维数组
下标 |
0 |
1 |
2 |
3 |
4 |
0 |
C语言基础 |
- |
Java入门 |
- |
- |
1 |
C++基础 |
- |
Java入门 |
- |
C语言基础 |
2 |
C++基础 |
PHP入门 |
- |
- |
C语言基础 |
3 |
- |
- |
Java入门 |
- |
C++基础 |
4 |
- |
PHP入门 |
- |
C++基础 |
- |
假设这张转换后的表格对应着一个名为array的数组,那么要想访问“星期一101教室的课程”,就应该使用表达式array[0][0];要访问“星期二103教室的课程”,就应该输入表达式array[1][2]。注意到array的后面跟了两个中括号,因此array是一个二维数组——要唯一确定一个元素(课程名),必须提供两个下标,而不是一个。
在转换之后,大家会发现这张表格里丢掉了原来的行标题与列标题的信息。这是因为对于二维数组来说,真正有意义的是这个数组的大小(对应表格的长度和宽度)以及数组中每个位置存储的数据,而不是每个行标题与列标题。要保存行标题与列标题的信息,一种方法是在程序中用代码手动实现,例如像下面这样:
例程 721 手动输出行标题与列标题
/* 假设变量i是某元素对应的行数(即某门课对应的星期数) */
if(i == 0)
{
printf(“星期一”);
}
else if(i == 1)
{
printf(“星期二”);
}
else if …
这种方法解决了输出时的问题,但在程序中用到行数时还是只能使用数字0至5。要如何解决这个问题呢?请回顾之前讲过的枚举量,并为“星期一”至“星期五”分别定义枚举量。一定要牢记,数组下标的起始值是0,因此“星期一”对应的枚举量也应该是0!
脚下留心:如何理解“唯一确定一个元素”?
大家会发现在“唯一确定一个元素”这句话中,“唯一”的说法是有问题的。例如array[1][0]和array[2][0]对应的课程都是“C++基础”,这两个值应该是相同的,为什么还被称为“唯一确定一个元素”呢?
这是个好问题。请考虑现实生活中的情形:教授“C++基础”这门课的老师在周二和周三讲的内容肯定是不一样的,虽然它们有相同的课程名——否则你为什么要把一节课听上两遍呢?对于这两节课来说,它们实际上是不同的,于是在表格中就可以视为一个特有的、和其它元素不同的元素了。
从本质上来说,“唯一确定一个元素”指的是通过指定两个下标,可以在表格(或二维数组)中唯一确定一个位置,而这个位置上可以放置任意的元素。这些元素之间可以相等、也可以不相等,但是因为它们在内存中的位置是不一样的,所以它们实际上是不同的元素。
生活中还有哪些二维模型的例子呢?请再举出一些例子来,然后想想该如何把它们转化成C语言中的二维数组。
7.4.2 二维数组的定义及初始化
可以通过下面的语句为上一小节中提到的课程表建立一个5 X 5的整型二维数组:
int course_plan[5][5];
二维数组下标的使用方式是先行后列,即从左数第一个“5”是说课程安排表中一共有五行(从星期一到星期五),第二个“5”是说课程安排表一共有五列(五间不同的教室)。和一维数组相同的下标相同,二维数组每个维度的下标范围也是0 ~ (N-1)。在这个例子中,二维数组course_plan的两个维度的N均为5,那么这两个纬度的下标最小值都是0,最大值都是4。
要在声明时同时初始化这个二维数组,可以使用如下语句:
int course_plan[5][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25};
由于这个二维数组对应于一个五行五列的表格,因此需要提供5 X 5 = 25个初始值以初始化所有元素。
脚下留心:分隔符号的使用
请注意,每两个初始值之间都要使用英文逗号“,”分隔。下面是忘记使用逗号分隔初始值的示例:
int a[2][3] = {1 2 3 4 5 6};
错误提示如图7-18所示。
图 718 未使用逗号分隔初始值时的编译错误
如果初始化时没有提供足够多的值,那么相应的元素将被初始化为默认值0。因此为了将整个二维数组初始化为0,可以使用和初始化一维数组时相同的代码:
int course_plan[5][5] = {0};
这样整个course_plan数组的所有25个元素就会被初始化为0了。
一般的二维数组往往包括多行,因此将所有初始值写在同一行上不便于程序员阅读和理解代码。大家当然可以手动进行分行,如下所示:
int course_plan[5][5] =
{1, 2, 3, 4, 5,
6, 7, 8, 9, 10,
11, 12, 13, 14, 15,
16, 17, 18, 19, 20,
21, 22, 23, 24, 25};
C语言还允许使用另一种基于括号的分组初始化方式,即将同一行的元素用一对大括号括在一起:
int course_plan[5][5] = {{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}, {11, 12, 13, 14, 15}, {16, 17, 18, 19, 20}, {21, 22, 23, 24, 25}};
注意每对大括号一定要前后匹配才可以。每对大括号之间也要用英文逗号隔开。
7.4.3 二维数组的赋值
二维数组的赋值与一维数组类似,没有什么特殊的地方。下面用一个例子来示范如何对二维数组进行赋值。
多学一招:输出杨辉三角
杨辉三角,又称帕斯卡三角形,是指下面这个形似三角形的正整数排列:
图 719 杨辉三角示例
图7-18列出了杨辉三角形的前9行,而实际上杨辉三角形的高度是无限的。可以很快观察出来,杨辉三角形满足如下性质:
每一层左右两端的数都是1;
每一层都是左右对称的;
从n = 0开始,第n层有(2n + 1)个数;
从第1层开始,每个不位于左右两端的元素等于上一层左右两个数相加之和。
如果对数学中的二项式有一定了解,会发现杨辉三角形的第n层(从n = 0开始)对应于二项式展开后每一项的系数。例如第3层的四个值是1、3、3和1,那么展开二项式则等于。
通过下面的代码(例程7-22),就可以通过C语言程序实现杨辉三角形的生成和输出了。
例程 722 生成并输出杨辉三角
1 #include <stdio.h>
210 #define LEVEL 9
211 /* 生成并输出杨辉三角形(又称帕斯卡三角形) */
212 int main()
213 {
214 int array[LEVEL][LEVEL];
215 int i /* 每一层 */, j /* 一层中的每个元素 */;
216 /* 生成杨辉三角形 */
217 for(i = 0; i < LEVEL; ++i)
218 {
219 for(j = 0; j < LEVEL; ++j)
220 {
221 if(j == 0 || j == i)
222 {
223 /* 每一层最左边或最右边的元素 */
224 array[i][j] = 1;
225 }
226 else
227 {
228 /* 其它元素 */
229 array[i][j] = array[i - 1][j - 1] + array[i - 1][j];
230 }
231 }
232 }
233 /* 输出杨辉三角形 */
234 for(i = 0; i < LEVEL; ++i)
235 {
236 /* 输出第一个元素,左边要留出足够的空格。 */
237 printf("%*d", 25 - i * 2, array[i][0]);
238 for(j = 1; j < i + 1; ++j)
239 {
240 /* 输出其它元素 */
241 printf("%4d", array[i][j]);
242 }
243 printf("\n");
244 }
245 }
例程7-22的输出如下图(图7-20)所示:
图 720 输出杨辉三角
7.4.4 二维数组在内存中的排列
二维数组在内存中是按照先行后列的方式顺序存储的。例如对于整型数组int array[2][3],内存中的数组元素排布如下图:
图 721 二维数组元素在内存中的排布
例程7-23可以帮助大家更好地理解二维数组在内存中的排布方式。
例程 723 理解二维数组在内存中的排布
int array[2][3] = {1, 2, 3, 4, 5, 6};
246 int *pointer = (int*)array;
247 if(pointer[0] == array[0][0])
248 {
249 printf("第一个元素相同\n");
250 }
251 if(pointer[3] == array[1][0])
252 {
253 printf("第四个元素相同\n");
254 }
255 if(pointer[4] == array[1][1])
256 {
257 printf("第五个元素相同\n");
258 }
编译并运行例程7-28后,输出如下(图7-22):
图 722理解二维数组在内存中的排布
动手体验:完善上述验证程序
大家一定很快发现了上述验证程序中的问题所在:两个元素的值相同,不代表这两个元素就一定处于内存中的同一个位置!为了验证元素的地址相同,需要用到取地址运算“&”。请提前阅读“指针”一章的内容,并用取地址运算完善这个程序吧。
7.5 高维数组
在学习了一维数组和二维数组之后,大家完全可以通过举一反三的方式写出定义和操作高维数组的代码。但请注意,因为高维数组不易于想象、难易调试且较为占用内存,在实际开发中很少用到三维以上的数组。因此在使用高维数组时,请多加小心。
动手体验:三维数组的使用
例程7-24演示了三维数组的初始化、赋值和访问,供参考。
例程 724 三维数组的使用示例
1 #include <stdio.h>
259 int main()
260 {
261 int arr[3][4][5] = {1, 2, 3, 4, 5}; /* 只初始化了前5个元素 */
262 int i, j, k;
263 /* 赋值 */
264 for(j = 0; j < 4; ++j)
265 {
266 for(k = 0; k < 5; ++k)
267 {
268 arr[1][j][k] = -1;
269 }
270 }
271 arr[2][0][0] = 100;
272 /* 访问 */
273 for(i = 0; i < 3; ++i)
274 {
275 printf("arr[%d] = \n", i);
276 for(j = 0; j < 4; ++j)
277 {
278 for(k = 0; k < 5; ++k)
279 {
280 printf("%4d", arr[i][j][k]);
281 }
282 printf("\n");
283 }
284 printf("\n");
285 }
286 }
例程7-24的运行结果如下:
图 723 三维数组的使用
7.6 本章小结
本章详细介绍了一维数组与二维数组的定义、初始化、赋值与读取,并通过图表和示例程序演示了数组在内存中的存储方式。在C语言中,数组是很重要的知识,也是今后理解字符串的基础,希望大家能够认真掌握本章的内容。
全书第十三章《程序开发流程简介》中提供了一个有关数组使用的非常好的程序示例,推荐大家提前阅读。
- 点赞
- 收藏
- 关注作者
评论(0)