一. 什么是数组?
- 数组是由数据类型相同的一系列元素组成的。需要使用数组时,通过声明数组告诉编译器数组中内含多少元素和这些元素的类型。编译器根据这些信息正确的创建数组。普通变量可以使用的类型,数组元素都可以使用。
二. 一维数组
1. 一维数组的创建和初始化
1.1. 一维数组的创建
int arr[4];
上面便创建了一个整型数组:
- int 是数组的类型,在这里是整型。
- arr 是数组名。
- [] 是下标引用操作符也是数组的标志,这里我们在里面给上一个4,代表一个数组名为arr的整型数组里面放了四个元素。
值得注意的是,我们在定义数组时,[] 里的必须是一个常量表达式(const),这是C99标准之前规定的。也就是说,如果我们在前面创建一个整型变量给个初始值10,在创建数组时[]里放的是这个整型变量的变量名,这样编译器会报错,在语法上也是不支持的(C99标准之前)。
当然,我们还可以用宏定义一个常量来作为数组[]里的表达式从而创建一个数组:
#include <stdio.h> #define num 100 int main() { int arr[num]; // 这里是支持这样去创建的 return 0; }
1.2. 一维数组的初始化
首先我们要知道什么是数组的初始化?也就是说,在我们创建了一个数组后,给数组一个{0},或者把这个数组填满,这都可以算作数组初始化。
我还是建议大家在创建一个数组后能够给数组初始化,这样在后面使用时,就不会出现一些奇奇怪怪的值。这是因为,当你在给一个元素个数为4的整型数组初始化{0}后,你去打印这个数组,他会默认你这数组的四个元素都为0,不然就是一些很奇怪的随机值。
如果不指定数组大小而给数组某些值,那么这个数组的大小会根据你给的这些值的个数来确定。
#include <stdio.h> int main() { int arr[4] = { 0 }; return 0; }
- 如果一个整型数组它初始化不完全,那么没初始化的元素他也会默认为零:
int main() { int arr[4] = { 1 ,2 }; return 0; }
- 当然,其它类型的初始化也是一样(char等)。
2. 一维数组的使用
这里首先讲解一下[]操作符,它名为下标引用操作符。在创建一个数组时,它是数组的标志,也起定义数组大小的作用。在使用一个数组时,他的作用便是下标引用了。值得注意的是,数组的下标是这个元素从左往右数的位数减一,也就是说数组的下标是从零开始的,第一个元素的下标为0,第二个元素的下标为1,以此类推。所以我们在使用数组时,一定要注意下标的使用是否正确,避免出现数组下标越界的问题。
以下是一维数组的初级使用(创建—打印):
#include <stdio.h> int main() { int arr[4] = { 1, 2, 3 ,4 }; int i = 0; int sz = sizeof(arr) / sizeof(arr[0]); for (i = 0; i < sz; i++) { printf("%d", arr[i]); } return 0; }
我们可以看到,打印一维数组时用了一个循环。
上述代码中使用了sizeof来求数组的长度,sizeof其实是求数组字节的大小,一个整型为4个字节,而数组arr里有4个整型,那么大小为16,后面除上这个数组的第一个元素的字节大小(4),结果为4,这样求数组大小可以避免一些数组元素过多的问题。并且数组的长度计算还会在你改变这个数组时而改变,可以说非常方便,这是一个常用的求数组长度的方法,希望大家牢记。
3.一维数组在内存中的存储
- 一维数组在内存中的存储是连续的,地址由低到高,这里放段代码来展示其效果:
#include <stdio.h> int main() { int arr[4] = { 1, 2, 3, 4 }; int i = 0; int sz = sizeof(arr) / sizeof(arr[0]); for (i = 0; i < sz; i++) { printf("&arr[%d] = %p\n", i, &arr[i]); } return 0; }
- 我们可以看到,这四个元素的地址是连续的,他们之间相差4,是因为一个整型为四个字节,当一个整型值在内存中存储时,内存会开辟四个单元空间(每个单元为一个字节并且对应一个地址)用来存放这个整型值,所以在一个整型数组中也是如此,他们每个整型元素相差4。
三. 二维数组
1. 二维数组的创建和初始化
1.1. 二维数组的创建
int arr[2][3];
上面便创建了一个二维数组,它形式上就是比一维数组多了一个[]。
这里直接说明,二维数组和一维数组的通性是一样的,上述arr[2][3],通俗来说,2表示这个二维数组有两行(两条),3表示每一行(每一条)有几列(也就是几个元素),所以我们可以认为,arr[2][3]其实就是由两个arr[3]构成的。所以我们说二维数组其实就是一维数组。
1.2. 二维数组的初始化
- 二维数组的初始化与一维数组的初始化大同小异,但二维数组它是分行和列的,因此二维数组的初始化又有不同。
int arr[2][3] = { {1,2,3 }, {4, 5, 6} };
a.这里函数大小与初始化大小相同,所以这段代码表示第一个大括号表示为数组的第一行,第二个大括号表示为数组的第二行,而这里只有两行,其中每一行有三个元素(每行三列)。这是最为正确且直观的初始化方式。
int arr[2][3] = { 1,2,3,4,5,6 };
b.这里没有打大括号,但其与a展现的效果是相同的,他会自动将数组的前三个元素作为第一行,后三个元素最为第二行,这样不是很好读。
int arr[2][3] = { 1,2,4,5,6 };
c.这段代码数组初始化的元素个数就少于整个数组的元素个数的大小了,所以前三个元素还是默认第一行,后两个元素默认第二行,但最后一个没给的值默认为零(没有大括号的情况)。
int arr[2][3] = { {1,2 }, 4, 5, 6 };
d.这里给了大括号之后,则前面两个元素为第一行,后三个为第二行。当在打印数组内容时,第一行空缺的元素会默认为零打印。
- 值得注意的是,二维数组在初始化的时候,行可以省略,但列不能省略,因为由列可以确定你这一行,而行缺失了列的限制就无从初始化了。
2. 二维数组的使用
先展示代码来表达效果:
#include <stdio.h> int main() { int arr[2][3] = { {1,2 }, 4, 5, 6 }; int i = 0; int j = 0; for (i = 0; i < 2; i++) // 打印每行 { for (j = 0; j < 3; j++) // 打印这一行的每一列 { printf("%d ", arr[i][j]); } printf("\n"); // 每打印一行换行 } return 0; }
我们可以看到,打印一个二维数组要用到两个循环(循环之间为嵌套关系),使用二维数组可以达到一个版面的效果,在前面三子棋和扫雷的使用效果会更明显。
3. 二维数组在内存中的存储
- 我们说二维数组其实就是由一维数组构成的,所以二维数组在内存中的储存,可以说几乎与一位数组相同。
- 二维数组在内存中的储存也是连续的,他每一行的地址紧跟在前一行的地址后面,地址由低到高:
四. 多维数组
- 前面讨论的二维数组的相关内容都适用于三维数组或者更多维的数组。可以这样声明一个三维数组int arr[10][20][30];
- 我们可以把一维数组想象成一行数据,把二维数组想象成数据表,把三维数组想象成一叠数据表。例如,把上面声明的三维数组arr想象成由10个二维数组(每个二维数组都是20行30列)堆叠起来。
- 还有一种理解arr的方法是,把arr看作数组的数据。也就是说,arr内含10个元素,每个元素是内含20个元素的数组,这20个数组元素中的每个元素是内含30个元素的数组。或者,可以简单地根据所需的下标值去理解数组。
- 通常,处理三维数组要使用3重嵌套循环,处理四维数组要使用4重嵌套循环。对于其它多维数组,以此类推。一般来说,我们只会使用到二维数组。
五. 数组和指针(初级)
1.1. 指针在数组中的使用
指针是提供一种以符号形式使用地址方法,因为计算机的硬件指令非常依赖地址,指针在某种程度上把程序员想要传达的指令以更接近机器的方式表达。因此,使用指针的程序更有效率。尤其是,指针能有效地处理数组。数组表示法其实是在变相地使用指针。
数组名其实就是首元素的地址,&arr[0]与arr的效果相同,但&arr与他俩又不相同,他是整个数组的地址,下面是代码的展示效果:
#include <stdio.h> int main() { int arr[] = { 1,2,3,4,5 }; printf("%p\n", arr); printf("%p\n", &arr[0]); printf("%p\n", &arr); printf("\n"); printf("%p\n", arr + 1); // 数组名+1 printf("%p\n", &arr[1]); // 第二个元素的地址 printf("%p\n", &arr + 1); // 整个数组地址+1 return 0; }
我们可以看到(&arr + 1)它跳过的是整个数组的地址,而(arr + 1)只数组中第二个元素的地址。
- 以下是指针在数组中的具体的使用:
#include <stdio.h> #include <string.h> int main() { int arr1[] = { 1,2,3,4 }; char arr2[] = "abcd"; int i = 0; int sz1 = sizeof(arr1) / sizeof(arr1[0]); int sz2 = strlen(arr2); int* p1 = arr1; char* p2 = arr2; for (i = 0; i < sz1; i++) { printf("%d ", *p1 + i); // 解引用操作找到对应元素打印 } printf("\n"); for (i = 0; i < sz2; i++) { printf("%c ", *p2 + i); } return 0; }
运行结果:
1.2. 函数,数组与指针
- 当我们在调用函数时,想传递一个数组过去,我们直接传递数组名就行了,因为数组名就是数组首元素的地址,所以当函数接受时需要一个指针来接收传递过来的地址,值得注意的是,在函数接受时,用类型加数组而不用指针也是可以的,这两个的效果是相同的,例:
#include <stdio.h> void print(int* arr, int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } } int main() { int arr[] = { 1,2,3,4,5 }; int sz = sizeof(arr) / sizeof(arr[0]); print(arr, sz); return 0; }
#include <stdio.h> void print(int arr[], int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } } int main() { int arr[] = { 1,2,3,4,5 }; int sz = sizeof(arr) / sizeof(arr[0]); print(arr, sz); return 0; }
可以看到, 函数形参接受时的两种方式是等效的。
六. 变长数组概述
- 变长数组实际上就是数组[]里可以放变量表达式,在Linux环境下gcc可以使用,不过要注意的是,这样创建一个数组是不能初始化的。当然支持C99标准的编译器底下也可以这样使用。
七. 总结
- 数组的灵活运用可以高效地解决一些比较困难的问题,它可以很好地训练我们的编程思维。
- 我们在使用数组时最好是要运用指针来操作,这样程序会更高效,占用内存会更少。