数组
数组即为连续相同类型的多个元素,每个数组元素都有对应的下标
,对应各个元素的位置
,初始位置为
0。
例如,定义一个大小为 10
的数组:
//
// Created by NatsuYuki on 2022/9/7.
//
#include <stdio.h>
#define ARRAY_SIZE 10
int main() {
int array[ARRAY_SIZE];
for (int i = 0; i < ARRAY_SIZE; ++i) {
[i] = i;
array("%d\n", array[i]);
printf}
return 0;
}
得到:
0
1
2
3
4
5
6
7
8
9
Process finished with exit code 0
除了遍历赋值以外,我们也可以直接提供初始化列表赋值。
//
// Created by NatsuYuki on 2022/9/7.
//
#include <stdio.h>
#define ARRAY_SIZE 10
int main() {
int array[] = {0,1,2,3,4,5,6,7,8,9};
for (int i = 0; i < ARRAY_SIZE; ++i) {
("%d\n", array[i]);
printf}
return 0;
}
特别的,在非 MSVC 编译器中,我们还可以使用变量声明数组的长度,称为变长数组 (VLA)。
int size = 6;
int array[size]; // it works
元素的默认值
上述代码中,并没有给数组提供其大小,大小为提供的列表大小。
若初始化了一个大小大于所提供初始化值列表的数组,可能会导致混淆。
//
// Created by NatsuYuki on 2022/9/7.
//
#include <stdio.h>
int main() {
double array[5] = {1.5, 2.4, 3.4};
for (int i = 0; i < 5; ++i) {
[i] = i;
array("%lf\n", array[i]);
printf}
return 0;
}
1.500000
2.400000
3.400000
0.000000
0.000000
可以看见,未提供初始值的元素变成了
0.000000
。但初始值在不同编译器下有不同表现,应当注意。
指定元素下标初始化数组
在初始化列表中,我们也可以直接提供对应元素的下标插入:
//
// Created by NatsuYuki on 2022/9/7.
//
#include <stdio.h>
int main() {
char array[5] = {[2] = 'a'};
for (int i = 0; i < 5; ++i) {
("%c\n", array[i]);
printf}
return 0;
}
>>
得到以下结果
a
0 Process finished with exit code
同时,如果在 ‘a’ 后面再添加元素,下标则会从 3 开始算起。不过,这种特性在 C99 之后才有。
不可测的默认值
上面提到如果不提供初始化值,也会有默认值替换,但默认值到底是什么?
以 MinGW 为例,我们初始化一个大小为 10 的整型数组并打印:
7bff7f0
bd
5b4818e8
7ff7
f18113d0
25c
0
1
5b481000
7ff7
Process finished with exit code 0
MinGW 只负责划分一片内存区域出来给数组,并不会清理,其中的值可能会被污染。
再使用默认的 msvc:
cccccccc
可见,不同的编译器下默认值行为也不一致。但如果在函数外声明一个数组,则不会出现这种行为。
数组的边界
定义一个大小为 5 的数组。
int main() {
int array[5];
return 0;
}
使用调试 + Show In Memory View 查看数组分配的内存位置:
msvc 会将其初始化为 cc cc cc cc
。
每一个数组都有大小,因此也会有边界,我们可以通过 Memory View 模拟数组越界的行为,查看越界后元素的内存位置。
可以看到,C 语言并不会检查边界,而是直接获取下一段内存地址中的内容。你必须自行检查数组边界,否则会造成不可测的后果。
字符串
字符串本质上也是一个数组,我们可以近似将字符串理解为
char[]
。
比如表示 Hello World =>
char string[11] = "Hello World"
。
但 char[] 并不等价于字符串,给出以下例子:
//
// Created by NatsuYuki on 2022/9/9.
//
#include <stdio.h>
int main() {
char string[11] = "Hello World";
("%s\n", string);
printf
return 0;
}
运行一下,得到了 Hello World烫烫蘔苃!忑/╔?
(msvc
环境),这样的输出结果显然与我们的预期不符。
在 C 语言中,字符串必须以 NULL (\0) 结尾。所以,上述代码中字符串的大小应为 12,即 11 个字符 + 1 个 NULL。
中文字符
我们也可以使用 char[] 以及 wchar_t (宽字符) 来表示中文字符,二者分别使用 GBK 与 UTF-8 编码后的中文字符。
数组参数
数组可以作为参数传入函数中,以一个求和数组中值的代码为例:
#define SIZE 10
int SumIntArray(int array[SIZE]) {
int result = 0;
for (int i = 0; i < SIZE; ++i) {
+= array[i];
result }
return result;
}
一切都很完美,但是当尝试传入一个大小与 10 不一致的数组,结果就会发生误差。显然,在将数组作为参数时不能直接定义其大小。
由于数组在传入仅是一个内存地址,并不是数组本身,因此想通过
sizeOf
获取其大小也是不现实的。我们可以尝试传入一个大小为 6
的数组,并使用 sizeOf 获取其大小。
int array[6] = {1, 3, 2, 3, 1, 31};
("%d", sizeof(array) / sizeof(array[0]));
printf-----------------------------------------------
2 Output
显然,2 并不是这个数组的大小。
所以,在将数组作为参数时的函数中,通常需要多添加一个参数作为数组的大小。
int RefactorSum(int array[], int size) {
int result = 0;
for (int i = 0; i < size; ++i) {
+= array[i];
result }
return result;
}
在 Java 等现代语言中,数组传参时会附带其大小信息,因此不需要额外添加参数。
高维数组
高维数组即为数组的数组,例如
int multiArray[5][2];
定义了一个第一维长度为 5,二维长度为 2
的数组。
int multiArray[5][2] = {
{1, 1},
{4, 5},
{1, 4},
{1, 9},
{1, 9}
};
高维数组与普通数组相同,只不过其中的元素并不是你所熟悉的数字、字符串,而是一个数组,且元素数组的大小与对应维度所定义的大小一致,例如此处二维数组的大小为 2。
高维数组作为参数传入时,一维以后的数组大小需要直接指定,就像这样:
void Function(int x, int y, int z, int 3d[][y][z]) {}
如果想用数组作为返回值,应当让调用方提供数组,而不是像上面一样在函数中声明一个类似
int result[]
的变量用于返回。
因为这个变量默认为 auto
类型,一旦函数返回,这个数组也会被销毁,即使返回了这个数组也不再可用了,正确的做法应该是这样:
void Function(int x, int y, int z, int 3d[][y][z], int result[])
数组排序
终于到了喜闻乐见的快排时刻,虽然排序方式有很多种,但最常用的还属快速排列。
快速排列的机制是
使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的2个子序列,然后递归地排序两个子序列。
(摘自维基百科)。下面以排列整数数列为例:
int Swap(int array[], int from, int to) {
int temp = array[from];
[from] = array[to];
array[to] = temp;
array}
int Partition(int array[], int start, int end) {
int pivot = array[end];
int partition = low;
for (int i = low; i < end; ++i) {
if (array[i] < pivot) {
(array, i, partition++);
Swap}
}
(array, partition, end);
Swap}
void QuickSort(int array[], int start, int end) {
if (start >= end) return;
int partition = Partition(array, start, end);
(array, start, partition - 1);
QuickSort(array, partition + 1, end);
QuickSort}
快排的核心是挑选一个基准值 (pivot),将这一基准值与其他元素比较,并分割为两个不同数列,然后两个数列再进行相同操作,直到不可再分为止。这里基准值选择了数组末尾元素。
基准值的选择对排序的性能有着显著影响,此处仅介绍一种快速排列的实现,并非代表最优方法,可以参阅维基百科条目 快速排序。