什么是数组

数据往往不是各不相关的,我们需要处理的数据往往是一系列大量的相同类型的值,有着完全相同行为和作用.

例如一个班级的所有学生的各个学号,它们都是一个个固定长度的正整数,均用于唯一识别一名学生;

再例如一家超市的所有商品名,它们都是一个个的字符串.

对于这些同类型的,并且往往是大量的数据,我们显然不能像过去那样分别声明一个个单独的变量去存储:

1
2
3
4
5
6
7
8
9
10
11
12
#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开头,我们只能一个个的去设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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语言中使用数组来存储大量的相同类型的值,我们可以在声明数组的时候设置其长度,即可以存储的具体数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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语言中一般的数组(已知常量大小数组)的声明语法如下:

1
<元素类型> <数组名>[数组长度];
  1. 元素类型

    可以是任意基本类型,例如int,double,char

    也可以是像结构体这样的自定义类型

    元素类型指定了整个数组的每一个元素的类型

  2. 数组名

    数组名一样也是标识符,同样要遵循标识符的命名规范,和单个变量一样,数组名唯一标识了整个数组

  3. 数组长度

    数组长度必须是整数常量表达式,说简单点,必须是能直接算出来的常数,而且必须是整数

    例如2*3,100这些都是允许的,反之,2*i这样的表达式就是不允许的(但不是绝对,后面提到的VLA就允许)

对数组元素进行访问十分简单:

1
<数组名>[下标]

C语言中(所有的编程语言)数组的下标都是从0开始的,这种设计十分合理,读者自行百度了解

我们要访问数组a的第1个元素,则它的下标(也就是编号)就是0,我们要访问它,只需要a[0]即可

例如,我可以定义长度为10的int数组来存1~10这10个数:

1
2
3
4
5
6
7
8
9
10
11
12
13
#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;
}

输出结果为:

image-20231024141939496

上面这种使用循环变量对数组进行逐个的访问的方法叫做遍历,顾名思义,就是一个一个有序地对数组这个序列进行访问,我们可以在遍历时仅仅获取它们的值,或者可以对它们进行修改.

对数组进行初始化

在上一节的例子中,我们使用循环对数组的每一个元素进行了赋值.

我们也可以在声明时就使用一些特定的值对数组进行初始化,这里涉及到初始化器这个概念,具体可以自行百度了解.

初始化列表

我们在声明时对数组的各个元素进行初始化,可以使用初始化列表来实现,语法如下:

1
<元素类型> <数组名>[数组长度] = {表达式,...};

可以看到,除了原先的声明,我们在[]后面紧跟一个={},其中包含有若干以,分隔的表达式,这些表达式的值用于对数组的每个元素依次进行赋值.

例如,我们上面的例子可以改写为:

1
2
3
4
5
6
7
8
9
10
#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;
}

运行结果不变.

注意:

  1. {}初始化列表中的表达式个数不能多于数组元素的个数(数组长度),它们是一一对应来赋值的

  2. 如果表达式个数少于数组元素的个数,则后面的值被填充为整型的0或浮点型的+0

    需要注意的是,自C23起,C语言才支持了使用={}这样的空初始化器来达成与 C++ 中的值初始化相同的语义

  3. 特别常用于int数组,我们可以使用={0}来用0填充整个数组,不过,对于浮点型,个人建议还是不要依赖这种填充

  4. 如果没有对数组进行初始化,那么数组的各个元素将会是垃圾值,我们必须对其赋初值后才能"使用"它们.

    注意:放在全局变量的数组和普通变量一样,会用0去填充,而不是垃圾值

指定初始化器

C99新增了一个指定初始化器的特性,这让我们可以初始化指定的数组元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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;
}

运行结果:

image-20231024160309066

对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为0.

并且需要注意上面的例子中,[5]之后的6,7继续顺延到对[6],[7]进行赋值.

未知大小数组

如果我们忽略数组长度,那么这就是一个不完整的类型.关于这个问题不好解释,可以去找标准参阅.

如果我们这样写,我们需要使用初始化列表来进行初始化,以便编译器确定数组的长度,否则,编译器会因无法得知数组的大小而无法分配空间,导致报错.

初始化列表中表达式的个数就会成为数组长度,另一方面,如果使用了指定初始化器,则会保证数组能够容纳下所有的元素,例如有={[5]=3},则数组的长度为6,刚好能容纳下元素[5].

例如:

1
2
3
4
5
6
7
8
9
#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.

字符数组的初始化

字符数组的初始化有个特例,我们不仅可以像这样正常的为字符数组进行逐个赋值:

1
2
3
4
5
6
7
8
9
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;
}
image-20231024161013244

我们还可以直接使用一个字符串常量对其进行赋值:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
// 字符数组的初始化有个特例,我们不仅可以像这样正常的为字符数组进行逐个赋值:
char a[100]="hello world";
for (int i = 0; i < 11; ++i)
printf("%c", a[i]);
return 0;
}
image-20231024161013244

效果是一样的,因为逐个赋值依旧是初始化列表,后面的'\0'字符串结束标志,也就是整数0同样被默认填充.

而使用字符串常量,进行的是复制操作,结尾的'\0'同样会被添加.

关于这方面的内容,后面讲解字符串时会进行详细讨论.

非常量长度数组

必须首先强调的是,非常量长度数组,或者叫变长数组,再或者缩写为VLA,目前的兼容性不好,例如VS默认的msvc编译器就不支持这种用法.

另外VLA是在C99引入的.

简单的说就是可以使用变量来声明数组,不是很建议使用,这里仅给出一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
#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;
}

运行结果:

image-20231024162521152

二维数组

二维数组实际上,就是"数组的数组",即一个数组的每个元素都是数组.读者自行想象,最终结果就是实现一个NxM的矩阵.

有些问题光有一维(线性)的数组是不够的,我们需要"二维的"空间来存储,例如一张迷宫的地图.此时,就需要所谓的二维数组.

必须指出的是,所谓的二维,包括可能用到的更多维数组,都是逻辑上的划分,实际上数组全部都是线性的,也就是说,它们仍然在内存上排成一列,只不过是把这一长串的内存划分成几个大块,每一块都是一个子数组,作为整个数组的一个元素来使用.

二维数组的声明

我们十分容易能够将一维推广到二维:

1
int a[3][4];

这将声明一个"3行4列"的二维数组,类似这样:

image-20231024163941327

事实上,在底层,它仍然是一段连续的内存单元,也就是长为3x4xsizeof(int),也就是3x4x4=48个字节的连续内存:

image-20231024164414243

只不过我们从逻辑上将其转换为一个相对"立体"的矩阵而已,底层上,仍然是线性存储的.

所以有一句话:“C语言没有二维数组”,这句话就是针对C的底层逻辑来描述的,当然,这不影响我们简单地将其视为二维来解释,事实上,既然C能够有这种写法,当然就是为了满足我们对多维数组的需求.

关于这方面的详细内容,我们将会在学习指针之后展开详细讨论(注:这是难点)

回到声明,我们可以看到,第一个[]代表第一维(可以理解为行),第二个[]代表第二维(可以理解为列),int a[3][4]就声明了一个3行4列的二维数组.

对其访问也十分简单,例如我们要访问第2行第3列的元素,其下标为[1][2]—下标仍然从0开始

则该元素就是a[1][2]

二维数组的初始化

对于初始化列表,同样可以应用于二维数组,我们既可以直接用一个花括号初始化所有的元素:

1
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

我们还可以进行嵌套,这样更加直观,可以很容易地看出我们正在初始化的是哪一行:

1
2
3
4
5
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9,10,11,12}
};

这两种写法等价,不过建议使用第二种方法,更为清晰.

例如我们可以输出1,2,3,4,…,12这些数组成的3x4的矩阵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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;
}

运行结果:

image-20231024165611507

至于多维数组,均同理:

1
int a[3][4][5]; // 声明一个三维数组

但是一般用的比较少,大多数情况下,二维数组就已经能满足各种需求了.


本章简单讲解了数组的使用方法,至于更加深入的内容,在没有讲解指针之前,都无法特别清楚的进行讨论.

---WAHAHA



上一篇:C语言教程-9-运算符及其优先级和求值顺序

下一篇:C语言教程-11-字符串