C语言教程-10-数组
什么是数组
数据往往不是各不相关的,我们需要处理的数据往往是一系列大量的相同类型的值,有着完全相同行为和作用.
例如一个班级的所有学生的各个学号,它们都是一个个固定长度的正整数,均用于唯一识别一名学生;
再例如一家超市的所有商品名,它们都是一个个的字符串.
对于这些同类型的,并且往往是大量的数据,我们显然不能像过去那样分别声明一个个单独的变量去存储:
#include <stdio.h>
int main() {
int stu1=10001;
int stu2=10002;
int stu3=10003;
int stu4=10004;
int stu5=10006;
int stu6=10007;
return 0;
}
显然,这样的存储非常繁琐,并且限制非常大,各个变量都是互相独立的(尽管他们的变量名都相似),如果我们重新编排学号,例如从100开头变为200开头,我们只能一个个的去设置:
#include <stdio.h>
int main() {
int stu1=10001;
int stu2=10002;
int stu3=10003;
int stu4=10004;
int stu5=10006;
int stu6=10007;
stu1 = 20001;
stu2 = 20002;
stu3 = 20003;
stu4 = 20004;
// ...
return 0;
}
这样显然不现实,幸运的是,高级语言提供了各种用于存储这种一系列值的功能.
C语言中使用数组来存储大量的相同类型的值,我们可以在声明数组的时候设置其长度,即可以存储的具体数量:
#include <stdio.h>
int main() {
int students[6] = {
10001, 10002, 10003, 10004, 10006, 10007
}; // 声明并初始化一个数组用于存储各个学号,数组的长度为6,代表最多可以存储6个学号
for (int i = 0; i < 6; i++) {
students[i] = 20000 + i + 1;
} // 使用循环"遍历"数组中的每个"元素",并对其进行修改,注意元素"下标"从0到5而不是从1到6
for (int i = 0; i < 6; i++) {
printf("%d\n", students[i]);
} // 同样使用循环来遍历数组中的每个元素,只不过这次我们对其输出而不是修改值
return 0;
}
C语言的数组
事实上,数组是线性分配的,也就是说,一个数组的每个元素在内存中是紧挨着的存储(分配)的.
例如一个长度为10的int数组,一共占用4x10个,也就是40个字节,其中4为一个int变量占用的空间.
下面介绍数组的最基本的内容.
已知常量大小数组
C语言中一般的数组(已知常量大小数组)的声明语法如下:
<元素类型> <数组名>[数组长度];
-
元素类型
可以是任意基本类型,例如
int,double,char等也可以是像结构体这样的自定义类型
元素类型指定了整个数组的每一个元素的类型
-
数组名
数组名一样也是标识符,同样要遵循标识符的命名规范,和单个变量一样,数组名唯一标识了整个数组
-
数组长度
数组长度必须是
整数常量表达式,说简单点,必须是能直接算出来的常数,而且必须是整数例如
2*3,100这些都是允许的,反之,2*i这样的表达式就是不允许的(但不是绝对,后面提到的VLA就允许)
对数组元素进行访问十分简单:
<数组名>[下标]
C语言中(所有的编程语言)数组的下标都是从0开始的,这种设计十分合理,读者自行百度了解
我们要访问数组a的第1个元素,则它的下标(也就是编号)就是0,我们要访问它,只需要a[0]即可
例如,我可以定义长度为10的int数组来存1~10这10个数:
#include <stdio.h>
int main() {
//定义长度为10的int数组来存1~10这10个数
int a[10];
for (int i = 0; i < 10; i++) {
a[i] = i + 1;
}
for (int i = 0; i < 10; ++i) {
printf("%d ", a[i]); // 获取数组中的每一个值
}
return 0;
}
输出结果为:
上面这种使用循环变量对数组进行逐个的访问的方法叫做遍历,顾名思义,就是一个一个有序地对数组这个序列进行访问,我们可以在遍历时仅仅获取它们的值,或者可以对它们进行修改.
对数组进行初始化
在上一节的例子中,我们使用循环对数组的每一个元素进行了赋值.
我们也可以在声明时就使用一些特定的值对数组进行初始化,这里涉及到初始化器这个概念,具体可以自行百度了解.
初始化列表
我们在声明时对数组的各个元素进行初始化,可以使用初始化列表来实现,语法如下:
<元素类型> <数组名>[数组长度] = {表达式,...};
可以看到,除了原先的声明,我们在[]后面紧跟一个={},其中包含有若干以,分隔的表达式,这些表达式的值用于对数组的每个元素依次进行赋值.
例如,我们上面的例子可以改写为:
#include <stdio.h>
int main() {
//定义长度为10的int数组来存1~10这10个数
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (int i = 0; i < 10; ++i) {
printf("%d ", a[i]);
}
return 0;
}
运行结果不变.
注意:
-
{}初始化列表中的表达式个数不能多于数组元素的个数(数组长度),它们是一一对应来赋值的 -
如果表达式个数少于数组元素的个数,则后面的值被填充为整型的0或浮点型的+0
需要注意的是,自C23起,C语言才支持了使用
={}这样的空初始化器来达成与 C++ 中的值初始化相同的语义 -
特别常用于int数组,我们可以使用
={0}来用0填充整个数组,不过,对于浮点型,个人建议还是不要依赖这种填充 -
如果没有对数组进行初始化,那么数组的各个元素将会是垃圾值,我们必须对其赋初值后才能"使用"它们.
注意:放在全局变量的数组和普通变量一样,会用0去填充,而不是垃圾值
指定初始化器
C99新增了一个指定初始化器的特性,这让我们可以初始化指定的数组元素:
#include <stdio.h>
int main() {
//C99新增了一个`指定初始化器`的特性,这让我们可以初始化指定的数组元素
int a[10] = {[2] = 1, [5] = 2, 6, 7, [9] = 3};
// 等价于
// int a[10] = {0};
// a[2] = 1;
// a[5] = 2;
// a[9] = 3;
for (int i = 0; i < 10; ++i)
printf("%d ", a[i]);
return 0;
}
运行结果:
对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为0.
并且需要注意上面的例子中,[5]之后的6,7继续顺延到对[6],[7]进行赋值.
未知大小数组
如果我们忽略数组长度,那么这就是一个不完整的类型.关于这个问题不好解释,可以去找标准参阅.
如果我们这样写,我们需要使用初始化列表来进行初始化,以便编译器确定数组的长度,否则,编译器会因无法得知数组的大小而无法分配空间,导致报错.
初始化列表中表达式的个数就会成为数组长度,另一方面,如果使用了指定初始化器,则会保证数组能够容纳下所有的元素,例如有={[5]=3},则数组的长度为6,刚好能容纳下元素[5].
例如:
#include <stdio.h>
int main() {
int a[]={1,2,3,4,5,6,7,8,9,10};
for (int i = 0; i < 10; ++i)
printf("%d ", a[i]);
return 0;
}
数组a的长度为10.
字符数组的初始化
字符数组的初始化有个特例,我们不仅可以像这样正常的为字符数组进行逐个赋值:
#include <stdio.h>
int main() {
// 字符数组的初始化有个特例,我们不仅可以像这样正常的为字符数组进行逐个赋值:
char a[100]={'h','e','l','l','o',' ','w','o','r','l','d'};
// char a[100]="hello world";
for (int i = 0; i < 11; ++i)
printf("%c", a[i]);
return 0;
}
我们还可以直接使用一个字符串常量对其进行赋值:
#include <stdio.h>
int main() {
// 字符数组的初始化有个特例,我们不仅可以像这样正常的为字符数组进行逐个赋值:
char a[100]="hello world";
for (int i = 0; i < 11; ++i)
printf("%c", a[i]);
return 0;
}
效果是一样的,因为逐个赋值依旧是初始化列表,后面的'\0'字符串结束标志,也就是整数0同样被默认填充.
而使用字符串常量,进行的是复制操作,结尾的'\0'同样会被添加.
关于这方面的内容,后面讲解字符串时会进行详细讨论.
非常量长度数组
必须首先强调的是,非常量长度数组,或者叫变长数组,再或者缩写为VLA,目前的兼容性不好,例如VS默认的msvc编译器就不支持这种用法.
另外VLA是在C99引入的.
简单的说就是可以使用变量来声明数组,不是很建议使用,这里仅给出一个简单的例子:
#include <stdio.h>
int main() {
int n;
scanf("%d", &n); // 输入10
int a[n];
for (int i = 0; i < n; ++i)
a[i] = i + 1;
for (int i = 0; i < n; ++i)
printf("%d ", a[i]);
return 0;
}
运行结果:
二维数组
二维数组实际上,就是"数组的数组",即一个数组的每个元素都是数组.读者自行想象,最终结果就是实现一个NxM的矩阵.
有些问题光有一维(线性)的数组是不够的,我们需要"二维的"空间来存储,例如一张迷宫的地图.此时,就需要所谓的二维数组.
必须指出的是,所谓的二维,包括可能用到的更多维数组,都是逻辑上的划分,实际上数组全部都是线性的,也就是说,它们仍然在内存上排成一列,只不过是把这一长串的内存划分成几个大块,每一块都是一个子数组,作为整个数组的一个元素来使用.
二维数组的声明
我们十分容易能够将一维推广到二维:
int a[3][4];
这将声明一个"3行4列"的二维数组,类似这样:
事实上,在底层,它仍然是一段连续的内存单元,也就是长为3x4xsizeof(int),也就是3x4x4=48个字节的连续内存:
只不过我们从逻辑上将其转换为一个相对"立体"的矩阵而已,底层上,仍然是线性存储的.
所以有一句话:"C语言没有二维数组",这句话就是针对C的底层逻辑来描述的,当然,这不影响我们简单地将其视为二维来解释,事实上,既然C能够有这种写法,当然就是为了满足我们对多维数组的需求.
关于这方面的详细内容,我们将会在学习指针之后展开详细讨论(注:这是难点)
回到声明,我们可以看到,第一个[]代表第一维(可以理解为行),第二个[]代表第二维(可以理解为列),int a[3][4]就声明了一个3行4列的二维数组.
对其访问也十分简单,例如我们要访问第2行第3列的元素,其下标为[1][2]---下标仍然从0开始
则该元素就是a[1][2]
二维数组的初始化
对于初始化列表,同样可以应用于二维数组,我们既可以直接用一个花括号初始化所有的元素:
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
我们还可以进行嵌套,这样更加直观,可以很容易地看出我们正在初始化的是哪一行:
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9,10,11,12}
};
这两种写法等价,不过建议使用第二种方法,更为清晰.
例如我们可以输出1,2,3,4,...,12这些数组成的3x4的矩阵:
#include <stdio.h>
int main() {
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9,10,11,12}
};
for(int i=0;i<3;i++) {
for (int j = 0; j < 4; j++)
printf("%d ", a[i][j]);
printf("\n");
}
return 0;
}
运行结果:
至于多维数组,均同理:
int a[3][4][5]; // 声明一个三维数组
但是一般用的比较少,大多数情况下,二维数组就已经能满足各种需求了.
本章简单讲解了数组的使用方法,至于更加深入的内容,在没有讲解指针之前,都无法特别清楚的进行讨论.
下一篇:C语言教程-11-字符串

浙公网安备 33010602011771号