课件

4.pdf

思维导图

4. 数组与结构(1) - 图1

4.1 数组

引言

4.1.1 引言.mp4 (12.17MB)

一维数组基础理论

4.1.2 一维数组基础理论.mp4 (119.8MB) 00:00:21 | demo | 找出最高分

image.png

假设 10 人:
image.png

按照我们之前学习的基本数据类型,很可能会直接定义 10 个变量来存这 10 个人的成绩。但是,如果成员过多,我们还是按照传统方式,一个人定义一个变量去存储,显然就不合适了。
image.png

image.png
正确的做法是使用数组

00:02:10 | 定义数组

image.png

注意:

  • 数组中的所有成员的类型都是一致的
  • 在定义数组的同时,就需要初始化数组的长度

image.png

定义数组时初始化数组:

  • int a[5] = { 12, 34, 56, 78, 9 };
    • 全部初始化
  • int a[5] = { 0 };
    • 用 0 初始化所有数据
  • int a[] = { 11, 22, 33, 44, 55 };
    • [] 内不写数组长度,根据初始化时成员个数来设置数组长度,存入几个成员,长度就是几
  • int a[5] = { 11 };
    • 第一个成员初始化为 11,后续全部用 0 初始化
    • a[0] == 11
    • a[1] a[2] a[3] a[4] 都是 0

00:08:24 | 访问一维数组的成员

image.png
image.png

00:09:19 | 一维数组的存储

image.png

理解数组成员在内存中的存储机制:
image.png
image.png

这就是为什么 数组 可以 速通过 下标访问 数组成员的原因

00:11:31 | 【例1】如何使两个数组的值相等?

image.png

这种做法是错误的:
image.png
image.png

PS:这种做法在 JS 中是正确的……

正确的做法:
image.png

00:13:35 | 【例2】显示用户输入的月份拥有的天数(不包括闰年的月份)

image.png

源码:
image.png

一维数组实例

4.1.3 一维数组实例.mp4 (94.18MB) 00:00:47 | 题目描述

image.png

00:01:17 | debug

image.png

错误 1:定义数组的时候,数组的长度必须是一个常量,这里我们传入的 n 是一个变量,这是错误的。
image.png

解决方式:将 n 定义为一个常量

image.png

注意:这种写法是存在兼容性的,在 .cpp 结尾的文件中可行,但是在 .c 结尾的文件中不可行。

错误 2:下标越界,数组的下标是从 0 开始的,如果下标取到数组长度值,那么下标会越界。
image.png

错误 3:这一部分需要使用取地址符,正确写法:&scores[i]

二维数组

4.1.4 二维数组.mp4 (70.34MB)

字符数组

4.1.5 字符数组.mp4 (122.07MB)

notes

简述

理解数组名 👉🏻 5.2 指针与数组

数组类型 和 结构体类型 的应用场景举例

  1. 数据存储和处理:
    • 数组可以用来存储和处理大量的数据,如存储学生的成绩、存储一个城市的人口数据等。
    • 结构体可以用来存储一个对象的多个属性,如一个人的姓名、年龄、性别等。
  2. 算法实现:
    • 数组可以用来实现排序算法、查找算法等。
    • 结构体可以用来实现树、图等数据结构。
  3. 图形处理:
    • 数组可以用来存储图像数据
    • 结构体可以用来表示一个点、一条线、一个多边形等
  4. 网络编程:
    • 数组可以用来存储和处理网络数据,如 TCP/IP 协议中的数据包。
    • 结构体可以用来表示网络数据包的头部、负载等。
  5. 数据库编程:
    • 数组可以用来存储查询结果集
    • 结构体可以用来表示表的一条记录
  6. 游戏编程:
    • 数组可以用来存储游戏地图、存档等数据
    • 结构体可以用来表示游戏中的角色、武器等

综上,掌握好数组和结构是非常有必要的,数组和结构是编程中非常基础和重要的数据结构,可以用来解决各种不同的问题。

补充:几乎所有高级语言都有类似“数组”、“结构体”这样的概念,它们的特点几乎都是一样的。要知道这玩意掌握好之后,是一劳永逸的就对了。

理解数组名

在 C 语言中,对于数组名,我们需要知道以下几点:

  • 数组名表示数组首元素的地址
  • 数组名是一个指向数组第一个元素的指针
  • 数组名是常量,因此不能被重新赋值
    • 由于数组名是常量,无法直接赋值,所以 无法通过数组名来直接拷贝数组
  • 可以将数组名看做是一个“常量”指针(不能修改其指向)
  • 数组元素可以使用下标来访问,也可以使用指针来访问

一维数组

定义一维数组的语法:
type arrayName[arraySize];

  • type
    • 数组元素的数据类型
    • 数组中所有成员的类型都是 type 类型
  • arrayName:数组名
  • arraySize:数组成员的个数
  1. int myArray[10]; // 定义一个由 10 个整数组成的数组 myArray
  2. float prices[5]; // 定义一个由 5 个浮点数组成的数组 prices

对于数组,我们还需要知道以下概念:

  • 数组成员:数组中的每一个元素
  • 连续存储
    • 数组在内存空间中的存储是 连续
    • 只要是数组,那么它在内存中的存储机制就是连续存储的,无论维度是多少
  • 数组下标
    • 从 0 开始,到数组长度减 1 的位置
    • 第一个成员下标是 0
    • 最后一个成员下标是 数组长度 - 1
  • 越界错误:如果使用一个超过数组长度或小于 0 的下标值进行访问,就会产生越界错误。
  1. #include <stdio.h>
  2. int main() {
  3. int arr[5] = {1, 2, 3, 4, 5}; // 定义一个数组并初始化
  4. // 访问数组中的元素
  5. printf("arr[0] = %d\n", arr[0]);
  6. printf("arr[1] = %d\n", arr[1]);
  7. printf("arr[2] = %d\n", arr[2]);
  8. printf("arr[3] = %d\n", arr[3]);
  9. printf("arr[4] = %d\n", arr[4]);
  10. // 修改数组中的元素
  11. arr[2] = 10;
  12. printf("arr[2] = %d\n", arr[2]);
  13. return 0;
  14. }
  15. /* 运行结果:
  16. arr[0] = 1
  17. arr[1] = 2
  18. arr[2] = 3
  19. arr[3] = 4
  20. arr[4] = 5
  21. arr[2] = 10
  22. */
  1. #include <stdio.h>
  2. int main() {
  3. int arr[5] = {11}; // 第一个成员初始化为 11,后续成员都缺省了,对于缺省的部分,会全部用 0 初始化
  4. printf("arr[0] = %d\n", arr[0]);
  5. printf("arr[1] = %d\n", arr[1]);
  6. printf("arr[2] = %d\n", arr[2]);
  7. printf("arr[3] = %d\n", arr[3]);
  8. printf("arr[4] = %d\n", arr[4]);
  9. return 0;
  10. }
  11. /* 运行结果:
  12. arr[0] = 11
  13. arr[1] = 0
  14. arr[2] = 0
  15. arr[3] = 0
  16. arr[4] = 0
  17. */
  1. #include <stdio.h>
  2. int main() {
  3. int arr[] = {1, 2, 3, 4, 5}; // [] 内不写数组长度,根据初始化时成员个数来设置数组长度,存入几个成员,长度就是几
  4. int len = sizeof(arr) / sizeof(arr[0]); // 计算长度
  5. printf("数组的长度为:%d\n", len); // 5
  6. printf("数组的 第一个成员:%d\n", arr[0]); // 1
  7. printf("数组的 最后一个成员:%d\n", arr[len - 1]); // 5
  8. return 0;
  9. }
  10. /* 运行结果:
  11. 数组的长度为:5
  12. 数组的 第一个成员:1
  13. 数组的 最后一个成员:5
  14. */

计算数组长度:sizeof(arr) / sizeof(arr[0])

  • sizeof(arr) 获取整个数组 arr 所占用的总字节数
  • sizeof(arr[0]) 获取一个数组成员所占用的字节数
  • 有关数组长度计算的详细描述 👉🏻 链接

数组拷贝

需求:
现在有一个 int arr[5] = {1, 2, 3, 4, 5}; arr 数组,我们需要拷贝一份值一模一样的数组 arr_copy。

  1. int arr[5] = {1, 2, 3, 4, 5}, arr_copy[5];
  2. // 定义俩长度为 5 的整型数组 arr、arr_copy
  3. arr_copy = arr; // error
  4. // 数组名 arr_copy 是常量,无法被重新赋值
  1. #include <stdio.h>
  2. int main() {
  3. int arr[] = {1, 2, 3, 4, 5};
  4. int arr_copy[5];
  5. // 逐个拷贝
  6. arr_copy[0] = arr[0];
  7. arr_copy[1] = arr[1];
  8. arr_copy[2] = arr[2];
  9. arr_copy[3] = arr[3];
  10. arr_copy[4] = arr[4];
  11. arr_copy[2] = 30; // 修改拷贝之后的数组,并不会影响到原数组 arr
  12. printf("原数组:");
  13. for (int i = 0; i < 5; i++) {
  14. printf("%d ", arr[i]);
  15. }
  16. printf("\n拷贝数组:");
  17. for (int i = 0; i < 5; i++) {
  18. printf("%d ", arr_copy[i]);
  19. }
  20. return 0;
  21. }
  22. /* 运行结果:
  23. 原数组:1 2 3 4 5
  24. 拷贝数组:1 2 30 4 5
  25. */
  1. #include <stdio.h>
  2. int main() {
  3. int arr[] = {1, 2, 3, 4, 5};
  4. int arr_copy[5];
  5. int len = sizeof(arr) / sizeof(arr[0]);
  6. // 通过循环赋值拷贝
  7. for (int i = 0; i < len; i++) {
  8. arr_copy[i] = arr[i];
  9. }
  10. arr_copy[2] = 30; // 修改拷贝之后的数组,并不会影响到原数组 arr
  11. printf("原数组:");
  12. for (int i = 0; i < 5; i++) {
  13. printf("%d ", arr[i]);
  14. }
  15. printf("\n拷贝数组:");
  16. for (int i = 0; i < 5; i++) {
  17. printf("%d ", arr_copy[i]);
  18. }
  19. return 0;
  20. }
  21. /* 运行结果:
  22. 原数组:1 2 3 4 5
  23. 拷贝数组:1 2 30 4 5
  24. */

数组拷贝的实现方式还有很多,上述记录的这两种方式是最为基础的实现方式。

打印用户输入的月份拥有的天数(不考虑闰年)

  1. #include <stdio.h>
  2. int main() {
  3. int month;
  4. printf("请输入月份:");
  5. scanf("%d", &month);
  6. switch (month) {
  7. case 1:
  8. case 3:
  9. case 5:
  10. case 7:
  11. case 8:
  12. case 10:
  13. case 12:
  14. printf("%d 月有 31 天\n", month);
  15. break;
  16. case 4:
  17. case 6:
  18. case 9:
  19. case 11:
  20. printf("%d 月有 30 天\n", month);
  21. break;
  22. case 2:
  23. printf("%d 月有 28 天\n", month);
  24. break;
  25. default:
  26. printf("输入的月份无效\n");
  27. break;
  28. }
  29. return 0;
  30. }
  31. /* 运行结果:
  32. 请输入月份:3
  33. 3 月有 31 天
  34. 请输入月份:9
  35. 9 月有 30 天
  36. 请输入月份:2
  37. 2 月有 28 天
  38. 请输入月份:24
  39. 输入的月份无效
  40. */

输入 3 人成绩,输出成绩最高的人

  1. #include <stdio.h>
  2. int main() {
  3. const int totalCount = 3;
  4. int scores[totalCount];
  5. int max_score = 0;
  6. int max_index = 0;
  7. // 输入 3 个人的成绩
  8. printf("请输入 3 个人的成绩:\n");
  9. for (int i = 0; i < totalCount; i++) {
  10. printf("第 %d 个人的成绩:", i + 1);
  11. scanf("%d", &scores[i]);
  12. }
  13. // 找到最高分及所在位置
  14. for (int i = 0; i < totalCount; i++) {
  15. if (scores[i] > max_score) {
  16. max_score = scores[i];
  17. max_index = i + 1;
  18. }
  19. }
  20. // 输出结果
  21. printf("最高分为:%d,所在位置为第 %d 个人\n", max_score, max_index);
  22. return 0;
  23. }
  24. /* 运行结果:
  25. 请输入 3 个人的成绩:
  26. 第 1 个人的成绩:30
  27. 第 2 个人的成绩:90
  28. 第 3 个人的成绩:60
  29. 最高分为:90,所在位置为第 2 个人
  30. */

scanf("%d", &scores[i]); 等效写法 scanf("%d", scores+i);

如何区分数组的维度

一个数组的维数可以 通过数组定义时方括号 **[]** 的数量来确定

例:

  • 一个一维数组在定义时只有一个方括号
  • 一个二维数组则有两个方括号

通常情况下,方括号的数量对应着数组的维度

定义多维数组的语法

type array_name[size1][size2]...[sizeN];

  • type 数组元素的类型
  • array_name 表示数组名
  • size1size2 直到 sizeN 表示每一维数组的大小

例:

  • 定义一个二维数组 arr,它有 3 行 4 列,元素类型为 int:int arr[3][4];
  • 定义一个三维数组 cube,它有 2 个 3 行 4 列的二维数组,元素类型为 double:double cube[2][3][4];
  • …… 依此类推,可以定义任意维度的数组

二维数组与多维数组

  1. #include <stdio.h>
  2. int main() {
  3. int arr[3][4]; // 定义一个 3 行 4 列的二维数组
  4. // 初始化二维数组的值
  5. for (int i = 0; i < 3; i++) { // 遍历行
  6. for (int j = 0; j < 4; j++) { // 遍历列
  7. arr[i][j] = (i + 1) * (j + 1);
  8. }
  9. }
  10. // 遍历二维数组并输出每个元素的值
  11. printf("二维数组的值为:\n");
  12. for (int i = 0; i < 3; i++) { // 遍历行
  13. for (int j = 0; j < 4; j++) { // 遍历列
  14. printf("%d ", arr[i][j]);
  15. }
  16. printf("\n");
  17. }
  18. return 0;
  19. }
  20. /* 运行结果:
  21. 二维数组的值为:
  22. 1 2 3 4
  23. 2 4 6 8
  24. 3 6 9 12
  25. */
  1. #include <stdio.h>
  2. int main() {
  3. int arr[2][3][4] = {{
  4. {1, 2, 3, 4},
  5. {5, 6, 7, 8},
  6. {9, 10, 11, 12}
  7. },{
  8. {13, 14, 15, 16},
  9. {17, 18, 19, 20},
  10. {21, 22, 23, 24}}
  11. };
  12. // 遍历数组
  13. for (int i = 0; i < 2; i++) { // 2 层
  14. for (int j = 0; j < 3; j++) { // 3 行
  15. for (int k = 0; k < 4; k++) { // 4 列
  16. printf("%d ", arr[i][j][k]);
  17. }
  18. printf("\n");
  19. }
  20. printf("\n");
  21. }
  22. return 0;
  23. }
  24. /* 运行结果:
  25. 1 2 3 4
  26. 5 6 7 8
  27. 9 10 11 12
  28. 13 14 15 16
  29. 17 18 19 20
  30. 21 22 23 24
  31. */

数组初始化时缺省信息

缺省成员:

  • 整型数组:缺省初始化为 0
  • 字符型数组:缺省初始化为 '\0'
  • 浮点型数组:缺省初始化为 0.0

缺省长度:会根据实际在赋值时,赋值的成员个数来自动推断数组的长度。

  1. #include <stdio.h>
  2. int main() {
  3. int arr1[5] = {1, 2, 3}; // 未给出初始值的元素被自动初始化为0
  4. int arr2[5] = {0}; // 所有元素都被初始化为0
  5. int arr3[5] = {1}; // 第一个元素为1,其他元素被初始化为0
  6. int arr4[] = {1, 2, 3, 4, 5}; // 不指定数组长度,根据初始值自动推导数组长度
  7. printf("arr1: ");
  8. for (int i = 0; i < 5; i++) {
  9. printf("%d ", arr1[i]);
  10. }
  11. printf("\n");
  12. printf("arr2: ");
  13. for (int i = 0; i < 5; i++) {
  14. printf("%d ", arr2[i]);
  15. }
  16. printf("\n");
  17. printf("arr3: ");
  18. for (int i = 0; i < 5; i++) {
  19. printf("%d ", arr3[i]);
  20. }
  21. printf("\n");
  22. printf("arr4: ");
  23. for (int i = 0; i < 5; i++) {
  24. printf("%d ", arr4[i]);
  25. }
  26. printf("\n");
  27. return 0;
  28. }
  29. /* 运行结果:
  30. arr1: 1 2 3 0 0
  31. arr2: 0 0 0 0 0
  32. arr3: 1 0 0 0 0
  33. arr4: 1 2 3 4 5
  34. */

这个程序中定义了4个整型数组,分别是 arr1、arr2、arr3 和 arr4,并且分别使用不同的方式进行了初始化。会发现在初始化的时候:

  • 如果缺省了初始化成员,那么会自动补零
  • 如果初始化的时候没有指定数组的长度,那么会根据我们赋值的成员个数来决定该数组的长度是多少
  1. #include <stdio.h>
  2. int main() {
  3. int arr[3][4] = {{1, 2}, {4}, {5, 6, 7}};
  4. // 打印二维数组中的元素
  5. printf("arr:\n");
  6. for (int i = 0; i < 3; i++) {
  7. for (int j = 0; j < 4; j++) {
  8. printf("%d ", arr[i][j]);
  9. }
  10. printf("\n");
  11. }
  12. return 0;
  13. }
  14. /* 运行结果:
  15. arr:
  16. 1 2 0 0
  17. 4 0 0 0
  18. 5 6 7 0
  19. */

Q:初始化二维数组的时候可以省略列数嘛?

不可以

  • 在定义二维数组时,可以省略行数,但必须指定列数
  • 原因:在初始化二维数组时,如果省略列数,则编译器无法确定每一行的元素个数,因此不能够正确地分配存储空间。
  • 在初始化数组的时候,如果我们能够明确数组的结构,最好不要省略,一开始就写清楚。

使用 sizeof 计算数组的长度

计算公式:4. 数组与结构(1) - 图29

  1. #include <stdio.h>
  2. int main() {
  3. int arr[5] = {1, 2, 3, 4, 5};
  4. int size = sizeof(arr);
  5. int length = sizeof(arr) / sizeof(int);
  6. printf("数组 arr 占用的存储空间为 %d 字节\n", size);
  7. printf("数组 arr 的长度为 %d\n", length);
  8. return 0;
  9. }
  10. /* 运行结果:
  11. 数组 arr 占用的存储空间为 20 字节
  12. 数组 arr 的长度为 5
  13. */
  1. #include <stdio.h>
  2. int main() {
  3. int arr[3][4];
  4. // 计算数组占用的存储空间大小
  5. size_t arr_size = sizeof(arr);
  6. printf("数组的大小为 %lu 字节。\n", arr_size);
  7. // 计算每个元素占用的存储空间大小
  8. size_t elem_size = sizeof(arr[0][0]);
  9. printf("每个元素占用 %lu 字节。\n", elem_size);
  10. // 计算数组中元素的个数
  11. size_t num_elems = arr_size / elem_size;
  12. printf("数组中包含 %lu 个元素。\n", num_elems);
  13. return 0;
  14. }
  15. /* 运行结果:
  16. 数组的大小为 48 字节。
  17. 每个元素占用 4 字节。
  18. 数组中包含 12 个元素。
  19. */

使用一维数组字面量给二维数组初始化

  1. #include <stdio.h>
  2. int main() {
  3. int arr[][3] = {1, 2, 3, 4, 5, 6, 7};
  4. const int ROWS = sizeof(arr) / sizeof(arr[0]);
  5. const int COLS = sizeof(arr[0]) / sizeof(int);
  6. for (int i = 0; i < ROWS; i++) {
  7. for (int j = 0; j < COLS; j++) {
  8. printf("%d ", arr[i][j]);
  9. }
  10. printf("\n");
  11. }
  12. printf("数组有 %d 行,%d 列。\n", ROWS, COLS);
  13. return 0;
  14. }
  15. /* 运行结果:
  16. 1 2 3
  17. 4 5 6
  18. 7 0 0
  19. 数组有 3 行,3 列。
  20. */

int arr[][3] = {1, 2, 3, 4, 5, 6, 7};

  • 这行代码的作用是定义了一个 3 行 3 列的二维数组,并对其中的元素进行了初始化,缺省位自动补零。
  • 列:这行代码定义了一个二维数组 arr,它有两个维度,第一个维度未指定大小,第二个维度(列)为 3。
  • 行:由于第一个维度未指定大小,编译器会根据初始化列表中提供的元素个数自动计算出数组的行数,这里的初始化列表中提供了 7 个元素,所以第一个维度(行)的大小被自动计算为 3(向上取整,确保所有成员都能装下)。

存储空间:

  • sizeof(arr) 整个 arr 数组的存储空间
  • sizeof(arr[0]) 一行中所有成员的存储空间
  • sizeof(int)sizeof(arr[0][0]) 一个成员的存储空间

string.h 中常见的字符串处理函数

string.h 库中提供了很多常用的 字符串处理函数,下面列举一些常用的函数及其功能:

  • **strlen()** 函数:用于求一个字符串的长度。
  • **strcpy()** 函数:用于将一个字符串复制到另一个字符串中。
  • **strcat()** 函数:用于将一个字符串拼接到另一个字符串的尾部。
  • **strcmp()** 函数:用于比较两个字符串是否相等。
  • strncpy() 函数:用于将一个字符串的一部分复制到另一个字符串中。
  • strncat() 函数:用于将一个字符串的一部分拼接到另一个字符串的尾部。
  • strncmp() 函数:用于比较两个字符串的前 n 个字符是否相等。
  • strchr() 函数:用于在一个字符串中查找指定字符的位置。
  • strrchr() 函数:用于在一个字符串中查找指定字符的最后一个位置。
  • strstr() 函数:用于在一个字符串中查找指定子字符串的位置。
  • strtok() 函数:用于分解一个字符串成为一组子字符串。
  • memset() 函数:用于将一段内存赋值为指定的值。
  • memcpy() 函数:用于将一段内存复制到另一段内存中。
  • memcmp() 函数:用于比较两段内存是否相等。

这些字符串处理函数大多数都比较常用,并且都是标准 C 库函数,使用起来非常方便。

注意:

  • 在使用这些函数的过程中,需要注意参数的类型和数量,以及要处理的字符串的长度和结尾符等因素。
  • 在使用这些函数时,应该注意避免出现内存泄漏、缓冲区溢出等问题,并将代码编写得简洁、高效、易于维护。

用于输出字符、字符串的格式控制字符%c%s

  • %c:用于输出单个字符。
  • %s:用于输出一个字符串。

字符串和字符数组之间的关系

问:字符串是字符数组?
答:是的

字符串本质上就是一串以空字符 **'\0'** 结尾的一组字符序列,因此可以使用字符数组来存储和操作字符串

问:字符数组是字符串?
答:不一定

得看字符数组的结尾字符是不是空字符 '\0'

  • 如果结尾字符是 '\0' 那么可以将这个字符数组视作一个字符串
  • 如果结尾字符不是 '\0' 那么该字符数组就不是一个字符串

字符数组是由一组字符组成的数组,每个字符占用一个字节的存储空间,结尾字符不一定是空字符 **'\0'**

strlen

函数声明:size_t strlen(const char *s);

  • const char *s 是一个指向字符型(即字符串)的指针,表示待计算长度的字符串
  • size_t 是一个无符号整数类型,用于表示字符串的长度
  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. char str[] = "Hello";
  5. size_t len = strlen(str);
  6. printf("字符串的长度为 %lu。\n", len);
  7. return 0;
  8. }
  9. /* 运行结果:
  10. 字符串的长度为 5。
  11. */

char str[] = "Hello";char str[] = {'H', 'e', 'l', 'l', 'o', '\0'}; 两种写法是等效的。

  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. char str[] = {'H', 'e', 'l', 'l', 'o', '\0'};
  5. size_t len = strlen(str);
  6. printf("字符串的长度为 %lu。\n", len);
  7. return 0;
  8. }
  9. /* 运行结果:
  10. 字符串的长度为 5。
  11. */

strlen 的返回结果依旧是 5,可见 '\0' 结尾字符并不会被计入长度。

  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. char str[] = {'H', 'e', 'l', '\0', 'l', 'o', '\0'};
  5. size_t len = strlen(str);
  6. printf("字符串的长度为 %lu。\n", len);
  7. return 0;
  8. }
  9. /* 运行结果:
  10. 字符串的长度为 3。
  11. */

如果字符串中包含 '\0' 字符,strlen 函数会返回 '\0' 字符前的字符数

注意:

  • strlen 函数的参数必须是一个以 '\0' 结尾的字符串,否则行为是未定义的
  • 如果字符串中包含 '\0' 字符,strlen 函数会返回 '\0' 字符前的字符数
  • strlen 函数计算字符串长度时,不包括 '\0' 终止符
  • 如果传递给 strlen 函数的指针是空指针 null,则会引发未定义的行为

strcpy

  • 函数声明:char *strcpy(char *dest, const char *src);
  • dest 目标字符串
  • src 源字符串
  • 用于将源字符串 src 复制到目标字符串 dest 中,并返回目标字符串的指针。
  • dest 指向的字符串必须具有足够的空间来存放 src 中的所有字符,包括字符串结束符 \0。否则,strcpy() 函数可能会导致缓冲区溢出,从而破坏程序的内存空间。
  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. char src[] = "hello world";
  5. char dest[20];
  6. printf("调用 strcpy 之前:\n");
  7. printf("src = [%s]\n", src);
  8. printf("dest = [%s]\n", dest);
  9. strcpy(dest, src);
  10. printf("调用 strcpy 之后:\n");
  11. printf("src = [%s]\n", src);
  12. printf("dest = [%s]\n", dest);
  13. return 0;
  14. }
  15. /* 运行结果:
  16. 调用 strcpy 之前:
  17. src = [hello world]
  18. dest = []
  19. 调用 strcpy 之后:
  20. src = [hello world]
  21. dest = [hello world]
  22. */

strcat

  • 函数声明:char *strcat(char *dest, const char *src);
  • dest 目标字符串
  • src 源字符串
  • 作用:将源字符串 src 拼接到目标字符串 dest 的末尾
  • 返回:指向 dest 的指针
  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. char str1[10] = "Hello, ";
  5. char str2[] = "world!";
  6. printf("[%s] 的长度是:%lu\n", str1, strlen(str1));
  7. strcat(str1, str2);
  8. printf("[%s] 的长度是:%lu\n", str1, strlen(str1));
  9. return 0;
  10. }
  11. /* 运行结果:
  12. [Hello, ] 的长度是:7
  13. [Hello, world!] 的长度是:13
  14. */

尽管最终的结果是符合我们预期的,但是上述代码是存在问题的。

因为 str1 只有 10 个字符的空间,但是将 "Hello, ""world!" 两个字符串拼接在一起后会超过这个空间,从而导致未定义的行为。

补充:虽然结果是符合预期的,看似也没出现什么问题,这主要是因为我们的程序规模小,总共就这么几行代码,所以破坏一些内存结构也不会出现什么问题。如果规模大了,这种做法会导致一些内存中存储的数据被抹掉,后果是不堪设想的。

为了修复这个问题,需要将 str1 的空间扩大到足够容纳两个字符串拼接后的结果。

  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. char str1[20] = "Hello, ";
  5. char str2[] = "world!";
  6. printf("[%s] 的长度是:%lu\n", str1, strlen(str1));
  7. strcat(str1, str2);
  8. printf("[%s] 的长度是:%lu\n", str1, strlen(str1));
  9. return 0;
  10. }
  11. /* 运行结果:
  12. [Hello, ] 的长度是:7
  13. [Hello, world!] 的长度是:13
  14. */

strcmp

  • 函数声明:int strcmp(const char *s1, const char *s2);
  • 作用:用于比较两个字符串的大小
  • 返回值:strcmp() 函数会逐个比较两个字符串中的字符,直到出现不同字符或字符串结束符为止。当出现不同字符时,函数会根据字符的 ASCII 码或 Unicode 码将两个字符做差,并将差作为比较结果返回。
    • 如果 s1 小于 s2,则返回值为 负数
    • 如果 s1 等于 s2,则返回值为
    • 如果 s1 大于 s2,则返回值为 正数
  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. char s1[] = "world";
  5. char s2[] = "hello";
  6. char s3[] = "world";
  7. int result1, result2, result3;
  8. result1 = strcmp(s1, s2); // 将较大的字符串排在前面,返回正整数
  9. result2 = strcmp(s2, s3); // 将较小的字符串排在前面,返回负整数
  10. result3 = strcmp(s1, s3); // 两个字符串相同,返回0
  11. printf("result1 = %d\n", result1);
  12. printf("result2 = %d\n", result2);
  13. printf("result3 = %d\n", result3);
  14. return 0;
  15. }
  16. /* 运行结果:
  17. result1 = 15
  18. result2 = -15
  19. result3 = 0
  20. */

从上述结果来看,可以得出以下公式:
4. 数组与结构(1) - 图30
至于为何是首字母,主要是因为第一个字母就是不同的字符。

  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. printf("'b' - 'a' = %d\n", 'b' - 'a');
  5. printf("'a' - 'b' = %d\n", 'a' - 'b');
  6. printf("'w' - 'h' = %d\n", 'w' - 'h');
  7. printf("'h' - 'w' = %d\n", 'h' - 'w');
  8. return 0;
  9. }
  10. /* 运行结果:
  11. 'b' - 'a' = 1
  12. 'a' - 'b' = -1
  13. 'w' - 'h' = 15
  14. 'h' - 'w' = -15
  15. */
  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. printf("strcmp(\"abc\", \"aaa\") = %d\n", strcmp("abc", "aaa"));
  5. printf("strcmp(\"aaa\", \"aba\") = %d\n", strcmp("aaa", "aba"));
  6. printf("strcmp(\"abc\", \"aba\") = %d\n", strcmp("abc", "aba"));
  7. printf("strcmp(\"abb\", \"aba\") = %d\n", strcmp("abb", "aba"));
  8. printf("strcmp(\"ab\", \"aba\") = %d\n", strcmp("ab", "aba"));
  9. return 0;
  10. }
  11. /* 运行结果:
  12. strcmp("abc", "aaa") = 1
  13. strcmp("aaa", "aba") = -1
  14. strcmp("abc", "aba") = 1
  15. strcmp("abb", "aba") = 1
  16. strcmp("ab", "aba") = -1
  17. */

结合 strcmp 的功能描述,我们得知它的返回值应该是:“两个字符串第一个不同字符的 ASCII 码值之差。”

但是,实际的测试结果可能与描述不符:

  • 符合预期 strcmp("abc", "aaa") = 1
    • 比较到下标为 1 的位置,发现了第一个不同的字符
    • str1 在该位置的字符是 'b'
    • str2 在该位置的字符是 'a'
    • 最终返回值是 'b' - 'a' 也就是 1
  • 不符合预期 strcmp("abc", "aba") = 1
    • 比较到下标为 2 的位置,发现了第一个不同的字符
    • str1 在该位置的字符是 'c'
    • str2 在该位置的字符是 'a'
    • 最终返回值是 'c' - 'a' 也就是 2 但是实际返回的是 1

不过无论如何,自测下来,结果的正负号是没有问题的

  • 如果 s1 小于 s2,则返回值为 负数
  • 如果 s1 等于 s2,则返回值为
  • 如果 s1 大于 s2,则返回值为 正数

字符数组初始化的多种方式

  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. // 直接初始化,这种做法,编译器会自动为我们在字符串的最后追加一个 '\0' 结尾符。
  5. char str1[] = "Hello, world!";
  6. // 使用 strcpy 函数初始化
  7. char str2[20];
  8. strcpy(str2, str1);
  9. char str3[20];
  10. strcpy(str3, "Hello, world!");
  11. // 逐个赋值,需要手动添加结尾的结束符 '\0' 【容易出错,不建议使用】
  12. char str4[20];
  13. for (int i = 0; i <= 12; i++) {
  14. str4[i] = "Hello, world!"[i];
  15. }
  16. str4[13] = '\0';
  17. char str5[20];
  18. for (int i = 0; i <= strlen(str1); i++) {
  19. str5[i] = str1[i];
  20. }
  21. str5[13] = '\0';
  22. printf("str1: %s\n", str1);
  23. printf("str2: %s\n", str2);
  24. printf("str3: %s\n", str3);
  25. printf("str4: %s\n", str4);
  26. printf("str5: %s\n", str5);
  27. return 0;
  28. }
  29. /* 运行结果:
  30. str1: Hello, world!
  31. str2: Hello, world!
  32. str3: Hello, world!
  33. str4: Hello, world!
  34. str5: Hello, world!
  35. */

注意:

  1. 保证字符数组足够长
  2. 注意结尾符的问题

gets、fgets

  • gets 函数声明:char *gets(char *str);
  • 作用:gets() 函数用于从标准输入设备(例如键盘)获取一行字符串,并将该字符串存储到指定的字符数组 str 中。
  • 注意:
    • gets() 函数 不进行缓冲区溢出检查,因此容易出现安全问题,因此 C11 已将该函数标记为过时。
    • 通常应该尽量使用安全的输入函数,例如 fgets() 函数。
  • fgets 函数声明:char *fgets(char *str, int size, FILE *stream);
  • 作用:同 gets 函数
  • 参数:
    • str 是一个字符数组指针,用于存储读取到的字符串;
    • size 是一个整型变量,指定字符数组的最大容量;
    • stream 是一个文件指针,指定从哪个流(例如标准输入、文件等)中读取数据;
  • 返回值:
    • 如果读取成功,则会返回读取到的字符串指针
    • 如果读取失败,返回 NULL
  • 注意:
    • fgets() 函数会把换行符也读入字符串中,因此需要在字符串末尾去掉换行符
    • if (str[strlen(str) - 1] == '\n') { str[strlen(str) - 1] = '\0'; }
  1. #include <stdio.h>
  2. int main() {
  3. char str[100];
  4. printf("请输入一行字符串:");
  5. fgets(str, 100, stdin);
  6. printf("你输入的是:%s\n", str);
  7. return 0;
  8. }
  9. /* 运行结果:
  10. 请输入一行字符串:hello world! 123 abc
  11. 你输入的是:hello world! 123 abc
  12. */

error: use of undeclared identifier ‘gets’ 由于直接使用 gets 出现了报错 error: use of undeclared identifier 'gets'

这个错误提示是因为使用了 C 语言标准库函数 gets(),但是在较新的 C 语言标准(C11 及之后的版本)中,gets() 函数已被废弃,不再被推荐使用。因此,在使用较新的编译器时,编译器会给出警告或错误提示。

推荐使用较新的函数 fgets() 来代替 gets(),fgets() 具有更好的安全性和可控性,使用方法类似,例如:

  1. fgets(str, sizeof(str), stdin);

sizeof(str) 表示字符数组 str 的大小
stdin 表示标准输入流,即从控制台获取用户输入

注意:fgets() 函数会把换行符也读入字符串中,因此需要在字符串末尾去掉换行符。可以使用以下代码去掉换行符:

  1. if (str[strlen(str) - 1] == '\n') {
  2. str[strlen(str) - 1] = '\0';
  3. }

补充:有关 fgets 的更多内容,在介绍后续知识点时还会详细说明……

puts

  • 函数声明:int puts(const char *str);
  • 作用:用于将一个字符串输出到标准输出设备(例如屏幕)该函数会自动在字符串的末尾添加换行符 \n,从而保证输出格式的正确性。
  • 注意:
    • puts() 函数不会检查字符串是否超出了缓冲区长度
    • 在使用该函数输出字符串时,需要确保字符串长度不会超过目标缓冲区的长度
    • 如果需要更加安全的字符串输出函数,可以使用 printf()fwrite() 函数。
  1. #include <stdio.h>
  2. int main() {
  3. char str[] = "Hello, world!";
  4. puts(str);
  5. return 0;
  6. }
  7. /* 运行结果:
  8. Hello, world!
  9. */

字符串的结束标志

'\0'

4.1.5 练习 | 寻找 James

需求:查找以 James 开头的人名。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. int main() {
  5. const int n = 20, m = 5;
  6. char name[][n] = {"Kate.Wate", "James.Tan", "Bull.Ben",
  7. "Jimes.Tide", "James.Ting", "K.James.T"};
  8. char James[] = "James";
  9. int i, j, len = strlen("James");
  10. for (i = 0; i < m; ++i) {
  11. for (j = 0; j < len; ++j) {
  12. if (name[i][j] != James[j])
  13. break;
  14. }
  15. if (j == len) {
  16. printf("%s has James\n", name[i]);
  17. }
  18. }
  19. return 0;
  20. }
  21. /* 运行结果:
  22. James.Tan has James
  23. James.Ting has James
  24. */

扩展:查找名字中含有 James 的成员。
提示:可以使用 strcmp 函数来比较,如果匹配上子串 James 那么将返回 0

  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4. const int MAX_LEN = 20;
  5. const char SUBSTR[] = "James";
  6. const int SUBSTR_LEN = strlen(SUBSTR);
  7. char name[][MAX_LEN] = {"Kate.Wate", "James.Tan", "Bull.Ben",
  8. "Jimes.Tide", "James.Ting", "K.James.T"}; // name 相当于一个二维数组
  9. const int num_names = sizeof(name) / sizeof(name[0]); // 一共多少个名字
  10. for (int i = 0; i < num_names; i++) {
  11. int name_len = strlen(name[i]);
  12. for (int j = 0; j < name_len - SUBSTR_LEN + 1; j++) {
  13. if (strncmp(&name[i][j], SUBSTR, SUBSTR_LEN) == 0) {
  14. printf("%s has %s\n", name[i], SUBSTR);
  15. break;
  16. }
  17. }
  18. }
  19. return 0;
  20. }
  21. /* 运行结果:
  22. James.Tan has James
  23. James.Ting has James
  24. K.James.T has James
  25. */

4.2 结构

结构

4.2.1 结构.mp4 (179.12MB)

notes

结构类型

  • 结构(Structure)类型是 C 语言中一种 自定义 复合 数据类型。
  • 结构 将一组相关的变量看作一个存储单元,而不是各自独立的实体
  • 结构有助于组织复杂的数据
  • struct 是用于定义结构体的关键字
  • 通过 struct 关键字可以将多个变量打包为一个复合类型的数据结构,方便对这些变量进行管理和维护

struct 关键字的语法格式:

  1. struct structure_name {
  2. member_type1 member_name1;
  3. member_type2 member_name2;
  4. ...
  5. member_typeN member_nameN;
  6. } struct_instance;
  • structure_name 表示结构体的名称。
  • member_type1member_type2、……、member_typeN 表示结构体的成员类型。
  • member_name1member_name2、……、member_nameN 表示结构体的成员名称。
  • struct_instance 是结构体的实例(或称对象),用于在程序中创建并操作结构体

4. 数组与结构(1) - 图32

定义一个学生结构,要求该结构中存放学生的 id、姓名 name、分数 score

  1. #include <stdio.h>
  2. #include <string.h>
  3. // 定义结构体
  4. struct Student {
  5. int id;
  6. char name[20];
  7. float score;
  8. } stu;
  9. int main() {
  10. // 写 | 接头体实例 stu 中的成员
  11. stu.id = 1000;
  12. strcpy(stu.name, "Alice");
  13. stu.score = 95.5;
  14. // 读 | 结构体实例 stu 中的成员
  15. printf("学生信息:\n");
  16. printf("学号:%d\n", stu.id);
  17. printf("姓名:%s\n", stu.name);
  18. printf("分数:%.1f\n", stu.score);
  19. return 0;
  20. }
  21. /* 运行结果:
  22. 学生信息:
  23. 学号:1000
  24. 姓名:Alice
  25. 分数:95.5
  26. */

在上述代码中,我们使用 struct 关键字定义了一个名为 Student 的结构体,其中包含三个成员变量:

  • id 表示学号
  • name 表示姓名
  • score 表示分数

main() 函数中,我们声明了一个结构体实例 stu,通过 . 运算符为结构体的成员赋值,并输出了结构体实例的信息。

结构体初始化的多种方式

  1. #include <stdio.h>
  2. struct {
  3. int id;
  4. char name[20];
  5. float score;
  6. } stu = {1001, "Alice", 95.5};
  7. // 定义了一个 匿名结构体,并在定义时即可完成成员变量的初始化,然后将其赋给了 stu。
  8. int main() {
  9. printf("学生信息:\n");
  10. printf("学号:%d\n", stu.id);
  11. printf("姓名:%s\n", stu.name);
  12. printf("分数:%.1f\n", stu.score);
  13. return 0;
  14. }
  15. /* 运行结果:
  16. 学生信息:
  17. 学号:1000
  18. 姓名:Alice
  19. 分数:95.5
  20. */
  1. #include <stdio.h>
  2. //定义结构体
  3. struct Student {
  4. int id;
  5. char name[20];
  6. float score;
  7. } stu = { .id = 1000, .name = "Alice", .score = 95.5 };
  8. // 使用成员初始化列表的方式,直接为结构体成员变量分别指定初始值,其中使用了点 . 运算符引用结构体成员变量。
  9. int main() {
  10. printf("学生信息:\n");
  11. printf("学号:%d\n", stu.id);
  12. printf("姓名:%s\n", stu.name);
  13. printf("分数:%.1f\n", stu.score);
  14. return 0;
  15. }
  16. /* 运行结果:
  17. 学生信息:
  18. 学号:1000
  19. 姓名:Alice
  20. 分数:95.5
  21. */
  1. #include <stdio.h>
  2. struct Student {
  3. int id;
  4. char name[20];
  5. float score;
  6. };
  7. struct Student stu = {1001, "Alice", 95.5};
  8. // 我们使用 {} 直接给结构体成员变量赋值,在定义结构体实例 stu 时即可完成初始化。
  9. int main() {
  10. printf("学生信息:\n");
  11. printf("学号:%d\n", stu.id);
  12. printf("姓名:%s\n", stu.name);
  13. printf("分数:%.1f\n", stu.score);
  14. return 0;
  15. }
  16. /* 运行结果:
  17. 学生信息:
  18. 学号:1000
  19. 姓名:Alice
  20. 分数:95.5
  21. */
  1. #include <stdio.h>
  2. //定义结构体
  3. struct Student {
  4. int id;
  5. char name[20];
  6. float score;
  7. };
  8. struct Student temp = {1000, "Alice", 95.5};
  9. struct Student stu = temp;
  10. // 先定义了一个名为 temp 的结构体变量,并给其成员变量赋值,然后将 temp 的值赋给了 stu,实现了 stu 的初始化。
  11. int main() {
  12. printf("学生信息:\n");
  13. printf("学号:%d\n", stu.id);
  14. printf("姓名:%s\n", stu.name);
  15. printf("分数:%.1f\n", stu.score);
  16. return 0;
  17. }
  18. /* 运行结果:
  19. 学生信息:
  20. 学号:1000
  21. 姓名:Alice
  22. 分数:95.5
  23. */

结构体的拷贝

  • 数组无法直接拷贝:前面我们学习过数组,知道数组名表示的是数组的首地址,是一个不可改变的常量,所以即便是对于同类型的数组,我们也没法通过数组名来直接完成拷贝。
  • 结构体可以直接拷贝:在结构体中,同类型的结构体之间是可以允许直接赋值的。
  1. #include <stdio.h>
  2. //定义结构体
  3. struct Student {
  4. int id;
  5. char name[20];
  6. float score;
  7. };
  8. struct Student temp = {1000, "Alice", 95.5};
  9. struct Student stu = temp;
  10. int main() {
  11. // 不同结构体之间的数据互不影响
  12. temp.score++;
  13. stu.id = 1001;
  14. printf("stu 学生信息:\n");
  15. printf("学号:%d\n", stu.id);
  16. printf("姓名:%s\n", stu.name);
  17. printf("分数:%.1f\n", stu.score);
  18. printf("temp 学生信息:\n");
  19. printf("学号:%d\n", temp.id);
  20. printf("姓名:%s\n", temp.name);
  21. printf("分数:%.1f\n", temp.score);
  22. return 0;
  23. }
  24. /* 运行结果:
  25. stu 学生信息:
  26. 学号:1001
  27. 姓名:Alice
  28. 分数:95.5
  29. temp 学生信息:
  30. 学号:1000
  31. 姓名:Alice
  32. 分数:96.5
  33. */

注意:

  • 类型要求相同:同类型的结构体可以直接拷贝,拷贝时会将结构体的所有成员逐一复制到目标结构体中
  • 指针共享问题:如果结构体中包含指针成员,直接拷贝可能会导致指针指向的内存地址被多个结构体共享,从而产生潜在的问题

课件题目 | 录入学生信息,并根据语文成绩降序

请帮老师写一个程序,要求存储本年级 100 个学生的姓名,学号,语文,数学,外语三门课程成绩,并 根据语文成绩递减排序,按名次输出所有学生信息。

100 个太多了些,先输 4 个意思意思就好……

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. const int n = 4, m = 20;
  4. typedef struct student {
  5. char name[m];
  6. int id;
  7. float chinese;
  8. float english;
  9. float math;
  10. } student;
  11. int main() {
  12. student s[n], tmp;
  13. int i, max, j;
  14. for (i = 0; i < n; ++i)
  15. scanf("%s%d%f%f%f", s[i].name, &s[i].id, &s[i].chinese, &s[i].english,
  16. &s[i].math);
  17. for (i = 0; i < n; ++i) {
  18. max = i;
  19. for (j = i + 1; j < n; ++j)
  20. if (s[j].chinese > s[max].chinese)
  21. max = j;
  22. tmp = s[i];
  23. s[i] = s[max];
  24. s[max] = tmp;
  25. }
  26. for (i = 0; i < n; ++i)
  27. printf("%s %d %.2f %.2f %.2f\n", s[i].name, s[i].id, s[i].chinese,
  28. s[i].english, s[i].math);
  29. }
  30. /* 运行结果:
  31. 张三 1 87 45 90
  32. 李四 2 100 44 92
  33. 王五 3 34 66 87
  34. wu 4 72 89 78
  35. 李四 2 100.00 44.00 92.00
  36. 张三 1 87.00 45.00 90.00
  37. wu 4 72.00 89.00 78.00
  38. 王五 3 34.00 66.00 87.00
  39. */