数组 - array

这节课,我们开始进行数组的学习。

由于变量只能保存一个值,如果我们想要处理多个数据,变量就会显得力不从心。比如要保存 100 名学生的成绩,我们一个一个的去定义变量是不是就显得十分复杂,再进一步每个学校有几万个学生,我们还要一个一个定义变量,恐怕连变量名都不够用,例如:

int student_1_grade = 97;
int student_2_grade = 76;
int student_3_grade = 45;
int student_4_grade = 64;
int student_5_grade = 87;
int student_6_grade = 68;
int student_7_grade = 91;
...
int student_100_grade = 99;

如果全校有几万人,难道我们要定义上万个变量吗?这样明显是不合理的,在 C 语言中使用数组就可以解决这一问题,一个数组可以看做是一组变量,将所有学生的成绩存放到一个数组里面。

如果将单个变量看成是一个单格收纳格,只能存放一种物品,那么数组就可以看做是一个带有多个固定编号格子的收纳盒,每个格子大小相同、紧密排列并且相互独立,用于存放同类型物品。

变量可以看作是一个格子,数组则可以看作是多个
变量可以看作是一个格子,数组则可以看作是多个

数组 是 C 语言中用于存储同类型数据元素的连续内存空间。核心特性:

  1. 元素类型一致(如 int、char)。
  2. 内存连续分配,支持高效随机访问。

创建数组

要想使用数组之前肯定要先创建数组,那么如何创建数组呢?

声明数组时需要指定数组的数据类型、数组名和大小,其结构如下:

数据类型 数组名[大小];

我们只需要在定义变量时,在变量名之后加一对 [],在 [] 内写出包含的元素个数,就是定义了一个数组。

比如我们要声明一个包含 5 个整数的数组:

// 声明一个包含 5 个整数的数组
int arr[5];

其中:

  • int:数据类型,也可以是其他数据类型,如:doublecharfloat
  • arr:数组名,同变量名一样不可以随便乱起,遵循变量命名规则。
  • [5]:数组长度,可以存储的数据值(元素)的数量

声明数组也是一个语句千万不要忘记加英文状态下的分号。

除了整型外,还可以创建其他基本数据类型的数组,代码如下:

// 定义数组存储 100 个学生的身高
double student_height[100]; 
// 定义数组存储 26 个英文字母的数组
char array[26];

同变量一样,声明完数组也需要对数组进行初始化操作(赋值),语法结构如下:

数据类型 数组名[大小] = {数据值1, 数据值2, 数据值3, ······, 数据值N};

{} 中的数据值,也被称为 元素

例如声明并初始化一个包含 5 个整数的数组,代码如下:

// 声明并初始化一个包含 5 个整数的数组
int arr[5] = {1, 2, 3, 4, 5};  

初始化值的数量必须要小于等于数组大小,大于数组大小会报错。初始化值小于数组大小时那么剩余的没有被初始化的元素则补上默认值,如:

具体补什么样的默认值,取决于前面的数据类型:

  • 整数的默认值是: 0
  • 小数的默认值是: 0.0
  • 字符的默认值是: \0(其实是一个空白字符);
  • 字符串的默认值是:NULL,表示不存在。

声明数组并初始化时,如果不写数组长度也是可以的,此时数据值的个数就是数组的长度,例如:

// 虽然没有填写数组长度大小但是我们可以通过元素的个数知道数组的长度为 5。
int arr[] = {1, 2, 3, 4, 5};

注意

如果声明数组时未赋值,必须显式声明数组长度,否则编译器无法确定内存分配大小。

int arr[5];      // 合法:未初始化但指定长度(分配 5 个 int 的空间)
int arr[];       // 非法!编译错误:数组大小未指定

如果我们不想声明数组时就初始化,那我们可以在之后再向里面添加数据吗?当然也是可以的,但是此时就需要我们去逐个进行赋值:

// 先声明数组
数据类型 数组名[大小];
// 逐个赋值
数组名[索引] = 值;

示例:

int arr[5];
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
arr[3] = 4;
arr[4] = 5;

索引就是数组的一个编号,也叫做:角标、下标。其特点是从 0 开始,连续 +1,不间断,最大的索引值为 数组的长度 - 1 。这就是为什么在刚才的示例中,给第一个数赋值是从 arr[0] 开始的。

我们来看一个初始化数组的练习:

存储五个学生的成绩,分别为:87,77,97,67,57

直接使用声明数组并初始化方法,代码如下:

int grade[5] = {87, 77, 97, 67, 57};

使用先声明后逐个赋值方法,代码如下:

int grade[5];
grade[0] = 87;
grade[1] = 77;
grade[2] = 97;
grade[3] = 67;
grade[4] = 57;

我们也可以通过赋值的方式去写该数组中某个元素的值

访问数组元素

访问即获取数组元素也需要用到索引,再次强调:索引从 0 开始依次 +1 连续不间断,最大的索引值为 数组的长度 - 1,如下图:

获取对应的数组元素的语法如下:

数组名[索引]

获取到数组元素之后我们不能孤零零的把它扔在这里不管,这时我们需要直接打印输出数组数据或者使用一个变量来接收数组数据以便之后使用,比如:

// 直接打印数据
printf("占位符", 数组名[索引]);

代码如下:

// 先声明并初始化一个数组
int arr[5] = {1, 2, 3, 4, 5};

// 直接打印数组第三个数的值
printf("%d", arr[2]);


// 使用变量接收数字 3
int num = arr[2];
// 打印结果
printf("%d", num);

我们已经学会如何获取数组了,但是我们有时候想要修改一下数组中的一个元素除了直接在数组中修改,有没有什么办法在数组外进行修改呢?当然是有的,我们只需要用到数组元素的修改即可,具体操作如下:

数组名[索引] = 新的数据值;

只需要知道我们要修改的数组中的第一个值的索引,再给他一个新的值即可。如我们想要把第 4 个数的值改为 6 ,代码如下:

// 先声明并初始化一个数组
int arr[5] = {1, 2, 3, 4, 5};

// 查看第 4 个元素修改前的值
printf("修改前的 arr[3]: %d", arr[3]);

// 修改第四个数的值为 6
arr[3] = 6;

// 查看第 4 个元素修改后的值
printf("修改后的 arr[3]: %d", arr[3]);

这样我们就将数组中第 4 个数的值改为 6 了。

数组的遍历

我们使用之前的知识点每次只能获取到一个数据,数组长度小一点还能一个一个的手动获取,那如果数组长度超过一百一千,那我们肯定不行去一个一个获取数据,那有什么办法一次性把所有元素全部拿出来呢?

我们可以直接使用 for 循环,与数组搭配使用。因为 for 循环是有限循环,而数组的长度也是固定的,示例:

#include <stdio.h>


int main() {
    // 先声明并初始化一个数组
    int arr[5] = {1, 2, 3, 4, 5};

    // 使用 for 循环遍历数组
    for(int i = 0; i < 5; i++) {
        printf("第 %d 个元素的值为:%d\n", i+1, arr[i]);
    }

    return 0;
}

sizeof 运算符

上面例子中我们可以清楚的数出数组的长度为 5,但是实际情况是我们在现实生活中要存储大量的数据,此时我们一个一个数出数组的长度是不可能的,万一一个不小心记错了数,就会导致遍历数组时出现错误,这时我们需要用到 sizeof 运算符先把数组长度计算出来之后,再进行循环遍历。

sizeof 作用是用于获取数据类型或表达式在内存中所占的字节数。

sizeof(数据类型)
sizeof(变量名)
sizeof(数组名)

// sizeof 运算符返回的是一个 size_t 类型的值,一般为无符号整数
printf("%zu", sizeof(数据类型))     // 打印数据类型的大小
printf("%zu", sizeof(变量名)) // 打印变量的大小

在使用 sizeof 之前,我们还需要简单的了解一下计算机是如何存储数据的。

计算机需要把数据转化为由 0 或 1 组成的二进制数,然后在存储区域中开辟一块空间再把转换的二进制数依次对应的填写进去,这样计算机就完成了数据的存储。

这里我们需要先了解 2 个概念,就是什么是 比特位 bit字节 byte

  1. Bit:Binary digit 的缩写,二进制数字中的位,是计算机内部数据的最小单位。计算机底层实际上只有 0 和 1 能够表示,这时如果我们要存储一个数据,比如十进制的 3,转换成二进制格式为 11,占用两个位置,再比如我们要表示十进制的 15,这时转换为二进制就是 1111 占用四个位置(4 个 bit 位)来保存。
  2. Byte: 用于计量数据大小常用的一种基本单位。
1 Byte(字节) = 8 Bits(二进制位)

我们常说的内存大小 1G、2G 等,实际上就是按照下面的进制进行计算的:

8 bit = 1 B
1024 B = 1 KB
1024 KB = 1 MB
1024 MB = 1 GB
1024 GB = 1 TB

现实生活中,我们一般会使用米、厘米来测量长度,而不是使用毫米、纳米,因为毫米、纳米实在是太小了,要是来用作测量单位的话,得到的数字会太大。同样的,因为 bit 很小,所以在衡量计算机一些数据类型大小时,我们一般用的是 Byte,并且计算机也是按照字节来存储数据的。

之前在学习变量时我们提到了基本数据类型,但是我们并不知道他们的大小,现在我们知道了计算机是如何处理数据的之后,我们就可以补充介绍所有的基本数据类型。

先重温一下我们之前的知识,C 语言中常用的 4 种基本数据类型:

类型 含义 所占字节
int 整型 4
char 字符类型(可以表示字符和小的整数) 1
float 单精度浮点数 4
double 双精度浮点数 8

整型变量除了常用的 int 以外呢,还有几种需要了解,只要看到能够认识就行的整数类型:

  • short:表示变量为短整型,通常占用 2 个字节。
  • long:表示变量为长整型,通常占用 4 个字节。
  • unsigned:表示变量为无符号型,即可保存正数和 0,但不能保存负数的整型变量。

以上修饰词在定义整型变量时可选用 0 个或多个,以定义不同规格的整型变量;但 short 和 long 不能同时选用。将他们组合起来:

类型 含义 所占字节
short int 短整型(int 可省略) 2
long int 长整型(int 可省略) 4
unsigned int 无符号基本整型(int 可省略) 4
unsigned short int 无符号短整型(int 可省略) 2
unsigned long int 无符号长整型(int 可省略) 4
long long int 超长整型(C99 标准才有,int 可省略) 8
unsigned long long int 无符号超长整型(int 可省略) 8

以上添加了修饰关键字的数据类型,均可以把 int 省略不写,作用也是一样的。除了这些以外,还有 long double 用来表示超长浮点数类型。

这里,我们可以使用 sizeof 来获取不同数据类型所占内存的大小,代码如下:

#include <stdio.h>


int main() {
    printf("%zu\n", sizeof(char));
    printf("%zu\n", sizeof(short));
    printf("%zu\n", sizeof(int));
    printf("%zu\n", sizeof(long));
    printf("%zu\n", sizeof(long long));
    printf("%zu\n", sizeof(float));
    printf("%zu\n", sizeof(double));
    printf("%zu\n", sizeof(long double));
    return 0;
}

这里使用 %d 也是可以的,但是最好换成 %zu%zu 用于输出 size_t 类型的值,通常用于表示内存大小、数组长度等。

比如,我们顺便再来使用 sizeof 打印一个变量的值,代码如下:

#include <stdio.h>


int main(){
    int a = 5;
    long long b = 5;
    short c = 5;

    // 打印变量长度
    printf("变量 a 的长度是:%zu\n", sizeof(a));
    printf("变量 b 的长度是:%zu\n", sizeof(b));
    printf("变量 c 的长度是:%zu\n", sizeof(c));
    return 0;
}

运行结果为:

变量 a 的长度是:4
变量 b 的长度是:8
变量 c 的长度是:2

这样我们就可以知道变量所占的字节大小与变量值无关,只与变量的数据类型有关。

我们知道了不同数据类型与变量所占的字节数之后,有什么作用呢?

我们可以根据数据类型所占内存的大小来推算出,不同的数据类型可以存储的数据范围。

变量在内存中的存储方式如下图:

变量在内存中的存储方式
变量在内存中的存储方式

最后,我们再用 sizeof 获取一下不同数据类型的数组的长度:

#include <stdio.h>


int main(){
    char array_char[4] = {'a', 't', 'o', 'm'};
    int array_int[4] = {5, 6, 7, 8};
    double array_double[4] = {1.24, 77.99, 89.3, 12.879};

    printf("数组 array_char 长度: %zu\n", sizeof(array_char));
    printf("数组 array_int 长度: %zu\n", sizeof(array_int));
    printf("数组 array_double 长度: %zu\n", sizeof(array_double));
    return 0;
}

运行结果如下:

数组 array_char 长度: 4
数组 array_int 长度: 16
数组 array_double 长度: 32

尽管上述代码中的 3 个数组的长度均为 4,但是,使用 sizeof 获取到的结果是完全不同的。sizeof 用于计算表达式或数据类型所占据的字节大小。由于 3 个数组的数据类型不同,因此,在创建数组时,占用的空间大小也不同,我们先以 array_int 为例,看一下整型数组是如何在内存中存储的,如下图:

array_int 在计算机内存中的存储方式
array_int 在计算机内存中的存储方式

array_int 实际上就是 4 个连续存储的整型变量,每个整型变量占据 4 个字节,因此,长度位 4 的 int 类型的数组 array_int 占据的内存大小为:4x4 = 16。

同理,char 类型占 1 个字节,长度为 4 的 char 类型的数组 array_char 占据的空间大小为:4x1 = 4;double 类型占据 8 个字节,长度为 4 的 array_double 占据的内存为 4x8 = 32。

这样,计算数组的长度也就很好理解,计算数组长度大小就是先计算整个数组在内存中占据的完整字节大小,再除以数组中任意一个数据值占据的字节大小,那么得到的就是整个数组的长度大小,

sizeof(数组名) / sizeof(数组的数据类型)
sizeof(数组名) / sizeof(数组中的第一个元素)

示例:

#include <stdio.h>


int main() {
    // 先声明并初始化一个数组
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 计算该数组的长度
    int len = sizeof(arr) / sizeof(arr[0]);

    // 使用 for 循环遍历数组
    for(int i = 0; i < len; i++) {
        printf("第 %d 个元素的值为:%d\n", i+1, arr[i]);
    }

    return 0;
}

当然,我们把 sizeof(arr) / sizeof(arr[0]) 更换为 sizeof(arr) / sizeof(int) 也是正确的。

练习 1:已知数组元素为 {24, 13, 37, 29, 6},请找出数组中最大值并打印,代码如下:

#include <stdio.h>


int main() {
    // 定义数组
    int arr[] = {24, 13, 37, 29, 6};

    // 定义变量 max, 记录数组的最大值
    // 此处最大值 max 的默认规则为 max 的默认值一定要是数组中已经存在的数据
    int max = arr[0];

    // 遍历数组得到每一个元素,将遍历的值与 max 进行比较
    // 遍历到的元素 <= max 当前记录的数据,则不做任何操作
    // 遍历到的元素 > max 当前记录的数据,则将 max 修改为当前新的值
    int len = sizeof(arr) / sizeof(arr[0]);

    for (int i = 0; i < len; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }

    // 输出最大值
    printf("数组中的最大值为:%d\n", max);

    return 0;
}

二维数组

我们都知道几何中点线面的关系,点动成线,线动成面,面动成体。我们可以把点想象成变量,那么线就是数组,因为数组就是紧密排列的变量组成的,那么面就是二维数组。

二维数组其实就可以理解为数组的嵌套。现在,我们学习的数组还只是一维数组,一维数组的元素就是单个的数据,比如整数 1,字符 'a',浮点数 3.14 等等。

而二维数组的元素还是数组,代码如下:

// 变量
int a = 1;
// 一维数组
int array_1[3] = {1, 2, 3};
// 二维数组
int matrix[3][3] = {
   {1, 2, 3},
   {2, 5, 7},
   {4, 5, 9},
}

二维数组 matrix 由一个大括号 {}包裹着三个小的数组,也就是说二维数组的元素是一维数组,这就是数组的嵌套。

拿学生的成绩来举例子,学校中的科目分为语文、数学、英语,班级里有 20 个学生,如果我们使用一维数组,代码如下:

int chinese_score[20] = {100, ..., 86};
int math_score[20] = {87, ..., 89};
int english_score[20] = {78, ..., 96};

二维数组则可以表达的更加直观,代码如下:

int score[20][3] = {
   {100, 87, 78},    // 语文、数学、英语
   ...
   {86, 89, 96}
};

接下来我们就以二维数组为例进行讲解,二维数组的语法如下:

类型 数组名[行数][列数];

类型 数组名[行数][列数] = {
    {数据值, 数据值,···,数据值},
    {数据值, 数据值,···,数据值},
            ···,
    {数据值, 数据值,···,数据值}
};

例如:

int a[3][4];      //定义一个 3 行 4 列的数组
char b[5][6];     // 定义一个 5 行 6 列的二维数组

// 定义一个 3 行 2 列的二维数组并赋值
int matrix[3][2] = {
   {1, 2},
   {2, 5},
   {4, 5},
}

虽然是二维数组,但是它在计算机中存储的方式依然是线性的

练习:声明一个 3x3 的整型二维数组,并初始化为单位矩阵(主对角线全为 1,其余为 0)。

int matrix[3][3] = {
    {1, 0, 0},
    {0, 1, 0},
    {0, 0, 1}
};

与一维数组类似的也需要用到索引值,只不过二维数组就是需要用到行索引与列索引:

数组名[行索引][列索引];

同样的与一维数组访问数组元素类似,获取到数组元素之后我们不能孤零零的把它扔在这里不管,这时我们需要直接打印输出数组数据或者使用一个变量来接收数组数据以便之后使用。示例如下:

#include <stdio.h>


int main() {
    // 声明并初始化一个三行两列的二维数组
    int matrix[3][2] = {
            {1, 2}, // 第一行
            {3, 4}, // 第二行
            {5, 6}  // 第三行
    };

    // 把第 2 行,第 1 列的数据值存入变量 num 中
    int num = matrix[1][0];

    // 使用变量打印第 2 行,第 1 列的数据值
    printf("%d\n", num);

    // 直接打印第 2 行,第 1 列的数据值
    printf("%d\n", matrix[1][0]);

    return 0;
}

如果想要对二维数组进行遍历的话,我们也需要使用嵌套循环来遍历,代码如下:

#include <stdio.h>


int main() {
    // 声明并初始化一个三行两列的二维数组
    int matrix[3][2] = {
            {1, 2}, // 第一行
            {3, 4}, // 第二行
            {5, 6}  // 第三行
    };

    // 嵌套循环遍历二维数组
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 2; ++j) {
            // 将该行的所有元素打印在一行,并用空格分割
            printf("%d ", matrix[i][j]);
        }
        // 当前行的元素打印完毕之后,进行换行操作
        printf("\n");
    }

    return 0;
}

二维数组需要用到两层嵌套循环,同理,三维数组则需要用到三层嵌套循环。

练习:用嵌套循环给一个 5x5 的二维数组赋值,使每个元素的值等于其行号与列号之和(如 matrix[i][j] = i + j),如下:

0 1 2 3 4
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
4 5 6 7 8

代码如下:

int arr[5][5];

for (int i = 0; i < 5; i++) {
    for (int j = 0; j < 5; j++) {
        arr[i][j] = i + j;
    }
}