【C】C系列五 - 数组

数组是什么?为什么下标是从 0 开始的?

一、什么是数组?

从字面上看,就是一组数据的意思,它的作用就是用来存储一组数据。数组中存放的每一个数据,一般称为“元素”。

1.1. 声明一个数组

格式:

1
元素类型 数组名[元素的数量];

示例:

1
2
3
4
5
// 声明一个数组ages,并且进行初始化,ages可以存放5个int类型的元素
int ages[5] = {10, 20, 50, 40, 60};

// 声明一个数组heights,并且进行初始化,heights可以存放5个float类型的元素
float heights[5] = {1.68, 1.75, 2.01, 1.85, 1.92};

1.2. 数组元素的访问

数组中的每一个元素都有一个唯一的索引(index,也叫做下标),是从 0 开始计算的。我们可以元素的索引访问元素。

示例:

1
int ages[5] = {10, 20, 50, 40, 60};

上面示例数组的下标:

索引 0 1 2 3 4
元素 10 20 50 40 60

访问示例数组的元素:

1
2
3
4
5
printf("%d\n", ages[1]); // 输出:20
printf("%d\n", ages[3]); // 输出:40

ages[1] = 100; // 赋值
printf("%d\n", ages[1]); // 输出:100

示例数组的元素重新赋值后:

索引 0 1 2 3 4
元素 10 100 50 40 60

1.3. 数组的遍历

遍历的意思是:将数组里面的每一个元素都访问一遍。遍历数组常用手法就是循环遍历。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int ages[5] = {10, 20, 50, 40, 60};

for (int i = 0; i < 5; i++) {
printf("%d\n", ages[i]);
if (i == 2) {
ages[i] = ages[i] * 100;
printf("索引i=%d被修改后的值%d\n", i, ages[i]);
}
}
/*
输出:
10
20
50
索引i=2被修改后的值5000
40
60
*/

二、内存细节

数组占用内存大小 = 元素类型占用内存大小 * 元素个数。

示例:

1
2
3
4
int ages[5] = {10, 20, 50, 40, 60};
printf("%zd\n", sizeof(ages)); // 输出:20

// 分析:数组ages存放了五个元素,元素类型是int类型,int类型占用内存4个字节,所以数组ages占用内存4*5=20个字节

分析元素内存地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
printf("&ages=%p\n", ages);
for (int i = 0; i < 5; i++) {
printf("&ages[%d]=%p\n", i, &ages[i]);
}
/*
输出:
&ages=0x7fbff560
&ages[0]=0x7fbff560
&ages[1]=0x7fbff564
&ages[2]=0x7fbff568
&ages[3]=0x7fbff56c
&ages[4]=0x7fbff570
*/

从上面的内地地址可以看出:

  • 数组元素的地址是连续分配的,且每个元素占用对应类型的字节数(如 int 类型占用 4 个字节);
  • 数组的地址等于数组首元素的地址。

三、数组的初始化细节

  • 如果没有初始化,数组元素的值是不确定的
1
2
3
int a[3];
printf("%d", a[0]); // 输出:0
// 注意:每次输出虽然是0,那是因为编译器做了优化,在实际开发中不建议这样使用
  • 可以先声明,后初始化
1
2
3
4
int a[3];
a[0] = 10;
a[1] = 20;
a[2] = 30;
  • 如果在声明的同时进行初始化,可以不指定元素的数量
1
2
3
int a[] = {10, 20, 30};
// 等价于
int a[3] = {10, 20, 30};
  • 可以在声明的时候只初始化部分元素,其他元素默认会初始化为 0
1
2
3
4
5
6
7
8
int a[5] = {10, 20, 30};
// {10, 20, 30, 0, 0}

int b[5] = {0};
// {0, 0, 0, 0, 0}

int c[5] = {};
// {0, 0, 0, 0, 0} 和上面第一种情况类似,虽然编译器支持,但不要这样写
  • 可以在大括号里面指定索引(注意:指定索引时,一定要标注索引 0 从哪个位置开始,否则会数据异常)
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
26
27
// 案例一:
int ages[5] = {[4]=40, [0]=10, 22, 33, 44};
for (int i = 0; i < 5; i++) {
printf("%d\n", ages[i]);
}
/*
输出:
10
22
33
44
40
*/

// 案例二:
int ages[5] = {10, 20, [1]=200, 33, 44};
for (int i = 0; i < 5; i++) {
printf("%d\n", ages[i]);
}
/*
输出:
10
200
33
44
0
*/
  • 数组的初始化元素数量不能超过数组的长度
1
2
3
// 编译器会警告
int ages[3] = {10, 20, 30, 40, 50};
printf("%d", ages[3]); // 输出:-652672928
  • 数组在声明的同时进行初始化,其长度不能是变量
1
2
3
int count = 5;
// 错误写法,编译器报错
int ages[count] = {10};
  • 在数组声明以后,不能再通过大括号进行赋值
1
2
3
4
5
6
int ages[3];
// 编译器报错
ages[3] = {10, 20, 30};

// 这样更不可能了,ages代表是数组的内存地址,是一个固定值
ages = {10, 20, 30};
  • 数组的索引越界后,访问的数值是不确定的
1
2
int ages[3] = {10, 20, 30};
printf("%d", ages[3]); // 输出:-652672928

四、数组的索引为什么是从 0 开始的?

访问一个数组元素的值,是从 0 开始的,为什么呢?我们从内存角度分析下。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int a[5] = {10, 20, 30, 40, 50};
printf("a的内存地址:%p\n", a);
for (int i = 0; i < 5; i++) {
printf("a[%d]的内存地址:%p\n", i, &a[i]);
}
/*
输出:
a的内存地址:0x7fbff560
a[0]的内存地址:0x7fbff560 a + 0 * 4 = a + 0
a[1]的内存地址:0x7fbff564 a + 1 * 4 = a + 4
a[2]的内存地址:0x7fbff568 a + 2 * 4 = a + 8
a[3]的内存地址:0x7fbff56c a + 3 * 4 = a + 12
a[4]的内存地址:0x7fbff570 a + 4 * 4 = a + 16
*/

从上面的代码输出可以看出,数组的地址是数组首元素的地址,
可以推算出:数组的元素地址 = 数组首元素地址 + 数组的索引 * 元素类型占用内存大小
数组 a 的内存地址:0x7fbff560
数组元素索引为 2 的地址:&a[2] = 0x7fbff560 + 1 * 4 = 0x7fbff564

假设:如果数组的索引从 1 开始
数组元素索引为 1 的地址(即首元素):&a[1] = 0x7fbff560 + 1 * 4 = 0x7fbff564
数组元素索引为 2 的地址:&a[2] = 0x7fbff560 + 2 * 4 = 0x7fbff568
数组 a 的内存地址:0x7fbff564

可以看出,如果元素所以从 1 开始,数组前面就会空出 4 个字节,浪费内存空间。

假如这时候把索引进行-1操作呢?
数组元素索引为 1 的地址(即首元素):&a[1] = 0x7fbff560 + (1 - 1) * 4 = 0x7fbff560
数组元素索引为 2 的地址:&a[2] = 0x7fbff560 + (2 - 1) * 4 = 0x7fbff564
数组 a 的内存地址:0x7fbff560

发现,如果数组的索引从 1 开始,然后对索引进行-1操作,确实解决了内存空间问题。但是,这样代码运行效率就变得有点低效了,因为每次都要进行减法运算。所以从 0 开始不仅可以解决内存空间问题,也能让代码快速高效地运行。