课件
思维导图
4.1 数组
引言
一维数组基础理论
假设 10 人:
按照我们之前学习的基本数据类型,很可能会直接定义 10 个变量来存这 10 个人的成绩。但是,如果成员过多,我们还是按照传统方式,一个人定义一个变量去存储,显然就不合适了。
正确的做法是使用数组
00:02:10 | 定义数组
注意:
- 数组中的所有成员的类型都是一致的
- 在定义数组的同时,就需要初始化数组的长度
定义数组时初始化数组:
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 | 访问一维数组的成员
00:09:19 | 一维数组的存储
理解数组成员在内存中的存储机制:
这就是为什么 数组 可以 快 速通过 下标访问 数组成员的原因
00:11:31 | 【例1】如何使两个数组的值相等?
这种做法是错误的:
PS:这种做法在 JS 中是正确的……
正确的做法:
00:13:35 | 【例2】显示用户输入的月份拥有的天数(不包括闰年的月份)
源码:
一维数组实例
00:01:17 | debug
错误 1:定义数组的时候,数组的长度必须是一个常量,这里我们传入的 n 是一个变量,这是错误的。
解决方式:将 n 定义为一个常量
注意:这种写法是存在兼容性的,在 .cpp 结尾的文件中可行,但是在 .c 结尾的文件中不可行。
错误 2:下标越界,数组的下标是从 0 开始的,如果下标取到数组长度值,那么下标会越界。
错误 3:这一部分需要使用取地址符,正确写法:&scores[i]
二维数组
字符数组
notes
简述
理解数组名 👉🏻 5.2 指针与数组
数组类型 和 结构体类型 的应用场景举例
- 数据存储和处理:
- 数组可以用来存储和处理大量的数据,如存储学生的成绩、存储一个城市的人口数据等。
- 结构体可以用来存储一个对象的多个属性,如一个人的姓名、年龄、性别等。
- 算法实现:
- 数组可以用来实现排序算法、查找算法等。
- 结构体可以用来实现树、图等数据结构。
- 图形处理:
- 数组可以用来存储图像数据
- 结构体可以用来表示一个点、一条线、一个多边形等
- 网络编程:
- 数组可以用来存储和处理网络数据,如 TCP/IP 协议中的数据包。
- 结构体可以用来表示网络数据包的头部、负载等。
- 数据库编程:
- 数组可以用来存储查询结果集
- 结构体可以用来表示表的一条记录
- 游戏编程:
- 数组可以用来存储游戏地图、存档等数据
- 结构体可以用来表示游戏中的角色、武器等
综上,掌握好数组和结构是非常有必要的,数组和结构是编程中非常基础和重要的数据结构,可以用来解决各种不同的问题。
补充:几乎所有高级语言都有类似“数组”、“结构体”这样的概念,它们的特点几乎都是一样的。要知道这玩意掌握好之后,是一劳永逸的就对了。
理解数组名
在 C 语言中,对于数组名,我们需要知道以下几点:
- 数组名表示数组首元素的地址
- 数组名是一个指向数组第一个元素的指针
- 数组名是常量,因此不能被重新赋值
- 由于数组名是常量,无法直接赋值,所以 无法通过数组名来直接拷贝数组
- 可以将数组名看做是一个“常量”指针(不能修改其指向)
- 数组元素可以使用下标来访问,也可以使用指针来访问
一维数组
定义一维数组的语法:type arrayName[arraySize];
type
- 数组元素的数据类型
- 数组中所有成员的类型都是 type 类型
arrayName
:数组名arraySize
:数组成员的个数
int myArray[10]; // 定义一个由 10 个整数组成的数组 myArray
float prices[5]; // 定义一个由 5 个浮点数组成的数组 prices
对于数组,我们还需要知道以下概念:
- 数组成员:数组中的每一个元素
- 连续存储
- 数组在内存空间中的存储是 连续 的
- 只要是数组,那么它在内存中的存储机制就是连续存储的,无论维度是多少
- 数组下标
- 从 0 开始,到数组长度减 1 的位置
- 第一个成员下标是
0
- 最后一个成员下标是
数组长度 - 1
- 越界错误:如果使用一个超过数组长度或小于 0 的下标值进行访问,就会产生越界错误。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5}; // 定义一个数组并初始化
// 访问数组中的元素
printf("arr[0] = %d\n", arr[0]);
printf("arr[1] = %d\n", arr[1]);
printf("arr[2] = %d\n", arr[2]);
printf("arr[3] = %d\n", arr[3]);
printf("arr[4] = %d\n", arr[4]);
// 修改数组中的元素
arr[2] = 10;
printf("arr[2] = %d\n", arr[2]);
return 0;
}
/* 运行结果:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
arr[2] = 10
*/
#include <stdio.h>
int main() {
int arr[5] = {11}; // 第一个成员初始化为 11,后续成员都缺省了,对于缺省的部分,会全部用 0 初始化
printf("arr[0] = %d\n", arr[0]);
printf("arr[1] = %d\n", arr[1]);
printf("arr[2] = %d\n", arr[2]);
printf("arr[3] = %d\n", arr[3]);
printf("arr[4] = %d\n", arr[4]);
return 0;
}
/* 运行结果:
arr[0] = 11
arr[1] = 0
arr[2] = 0
arr[3] = 0
arr[4] = 0
*/
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5}; // [] 内不写数组长度,根据初始化时成员个数来设置数组长度,存入几个成员,长度就是几
int len = sizeof(arr) / sizeof(arr[0]); // 计算长度
printf("数组的长度为:%d\n", len); // 5
printf("数组的 第一个成员:%d\n", arr[0]); // 1
printf("数组的 最后一个成员:%d\n", arr[len - 1]); // 5
return 0;
}
/* 运行结果:
数组的长度为:5
数组的 第一个成员:1
数组的 最后一个成员:5
*/
计算数组长度:sizeof(arr) / sizeof(arr[0])
sizeof(arr)
获取整个数组 arr 所占用的总字节数sizeof(arr[0])
获取一个数组成员所占用的字节数- 有关数组长度计算的详细描述 👉🏻 链接
数组拷贝
需求:
现在有一个 int arr[5] = {1, 2, 3, 4, 5};
arr 数组,我们需要拷贝一份值一模一样的数组 arr_copy。
int arr[5] = {1, 2, 3, 4, 5}, arr_copy[5];
// 定义俩长度为 5 的整型数组 arr、arr_copy
arr_copy = arr; // error
// 数组名 arr_copy 是常量,无法被重新赋值
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
int arr_copy[5];
// 逐个拷贝
arr_copy[0] = arr[0];
arr_copy[1] = arr[1];
arr_copy[2] = arr[2];
arr_copy[3] = arr[3];
arr_copy[4] = arr[4];
arr_copy[2] = 30; // 修改拷贝之后的数组,并不会影响到原数组 arr
printf("原数组:");
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
printf("\n拷贝数组:");
for (int i = 0; i < 5; i++) {
printf("%d ", arr_copy[i]);
}
return 0;
}
/* 运行结果:
原数组:1 2 3 4 5
拷贝数组:1 2 30 4 5
*/
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
int arr_copy[5];
int len = sizeof(arr) / sizeof(arr[0]);
// 通过循环赋值拷贝
for (int i = 0; i < len; i++) {
arr_copy[i] = arr[i];
}
arr_copy[2] = 30; // 修改拷贝之后的数组,并不会影响到原数组 arr
printf("原数组:");
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
printf("\n拷贝数组:");
for (int i = 0; i < 5; i++) {
printf("%d ", arr_copy[i]);
}
return 0;
}
/* 运行结果:
原数组:1 2 3 4 5
拷贝数组:1 2 30 4 5
*/
数组拷贝的实现方式还有很多,上述记录的这两种方式是最为基础的实现方式。
打印用户输入的月份拥有的天数(不考虑闰年)
#include <stdio.h>
int main() {
int month;
printf("请输入月份:");
scanf("%d", &month);
switch (month) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
printf("%d 月有 31 天\n", month);
break;
case 4:
case 6:
case 9:
case 11:
printf("%d 月有 30 天\n", month);
break;
case 2:
printf("%d 月有 28 天\n", month);
break;
default:
printf("输入的月份无效\n");
break;
}
return 0;
}
/* 运行结果:
请输入月份:3
3 月有 31 天
请输入月份:9
9 月有 30 天
请输入月份:2
2 月有 28 天
请输入月份:24
输入的月份无效
*/
输入 3 人成绩,输出成绩最高的人
#include <stdio.h>
int main() {
const int totalCount = 3;
int scores[totalCount];
int max_score = 0;
int max_index = 0;
// 输入 3 个人的成绩
printf("请输入 3 个人的成绩:\n");
for (int i = 0; i < totalCount; i++) {
printf("第 %d 个人的成绩:", i + 1);
scanf("%d", &scores[i]);
}
// 找到最高分及所在位置
for (int i = 0; i < totalCount; i++) {
if (scores[i] > max_score) {
max_score = scores[i];
max_index = i + 1;
}
}
// 输出结果
printf("最高分为:%d,所在位置为第 %d 个人\n", max_score, max_index);
return 0;
}
/* 运行结果:
请输入 3 个人的成绩:
第 1 个人的成绩:30
第 2 个人的成绩:90
第 3 个人的成绩:60
最高分为:90,所在位置为第 2 个人
*/
scanf("%d", &scores[i]);
等效写法 scanf("%d", scores+i);
如何区分数组的维度
一个数组的维数可以 通过数组定义时方括号 **[]**
的数量来确定。
例:
- 一个一维数组在定义时只有一个方括号
- 一个二维数组则有两个方括号
通常情况下,方括号的数量对应着数组的维度。
定义多维数组的语法
type array_name[size1][size2]...[sizeN];
type
数组元素的类型array_name
表示数组名size1
、size2
直到sizeN
表示每一维数组的大小
例:
- 定义一个二维数组 arr,它有 3 行 4 列,元素类型为 int:
int arr[3][4];
- 定义一个三维数组 cube,它有 2 个 3 行 4 列的二维数组,元素类型为 double:
double cube[2][3][4];
- …… 依此类推,可以定义任意维度的数组
二维数组与多维数组
#include <stdio.h>
int main() {
int arr[3][4]; // 定义一个 3 行 4 列的二维数组
// 初始化二维数组的值
for (int i = 0; i < 3; i++) { // 遍历行
for (int j = 0; j < 4; j++) { // 遍历列
arr[i][j] = (i + 1) * (j + 1);
}
}
// 遍历二维数组并输出每个元素的值
printf("二维数组的值为:\n");
for (int i = 0; i < 3; i++) { // 遍历行
for (int j = 0; j < 4; j++) { // 遍历列
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
/* 运行结果:
二维数组的值为:
1 2 3 4
2 4 6 8
3 6 9 12
*/
#include <stdio.h>
int main() {
int arr[2][3][4] = {{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
},{
{13, 14, 15, 16},
{17, 18, 19, 20},
{21, 22, 23, 24}}
};
// 遍历数组
for (int i = 0; i < 2; i++) { // 2 层
for (int j = 0; j < 3; j++) { // 3 行
for (int k = 0; k < 4; k++) { // 4 列
printf("%d ", arr[i][j][k]);
}
printf("\n");
}
printf("\n");
}
return 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
*/
数组初始化时缺省信息
缺省成员:
- 整型数组:缺省初始化为
0
- 字符型数组:缺省初始化为
'\0'
- 浮点型数组:缺省初始化为
0.0
缺省长度:会根据实际在赋值时,赋值的成员个数来自动推断数组的长度。
#include <stdio.h>
int main() {
int arr1[5] = {1, 2, 3}; // 未给出初始值的元素被自动初始化为0
int arr2[5] = {0}; // 所有元素都被初始化为0
int arr3[5] = {1}; // 第一个元素为1,其他元素被初始化为0
int arr4[] = {1, 2, 3, 4, 5}; // 不指定数组长度,根据初始值自动推导数组长度
printf("arr1: ");
for (int i = 0; i < 5; i++) {
printf("%d ", arr1[i]);
}
printf("\n");
printf("arr2: ");
for (int i = 0; i < 5; i++) {
printf("%d ", arr2[i]);
}
printf("\n");
printf("arr3: ");
for (int i = 0; i < 5; i++) {
printf("%d ", arr3[i]);
}
printf("\n");
printf("arr4: ");
for (int i = 0; i < 5; i++) {
printf("%d ", arr4[i]);
}
printf("\n");
return 0;
}
/* 运行结果:
arr1: 1 2 3 0 0
arr2: 0 0 0 0 0
arr3: 1 0 0 0 0
arr4: 1 2 3 4 5
*/
这个程序中定义了4个整型数组,分别是 arr1、arr2、arr3 和 arr4,并且分别使用不同的方式进行了初始化。会发现在初始化的时候:
- 如果缺省了初始化成员,那么会自动补零
- 如果初始化的时候没有指定数组的长度,那么会根据我们赋值的成员个数来决定该数组的长度是多少
#include <stdio.h>
int main() {
int arr[3][4] = {{1, 2}, {4}, {5, 6, 7}};
// 打印二维数组中的元素
printf("arr:\n");
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
/* 运行结果:
arr:
1 2 0 0
4 0 0 0
5 6 7 0
*/
Q:初始化二维数组的时候可以省略列数嘛?
不可以
- 在定义二维数组时,可以省略行数,但必须指定列数。
- 原因:在初始化二维数组时,如果省略列数,则编译器无法确定每一行的元素个数,因此不能够正确地分配存储空间。
- 在初始化数组的时候,如果我们能够明确数组的结构,最好不要省略,一开始就写清楚。
使用 sizeof 计算数组的长度
计算公式:
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int size = sizeof(arr);
int length = sizeof(arr) / sizeof(int);
printf("数组 arr 占用的存储空间为 %d 字节\n", size);
printf("数组 arr 的长度为 %d\n", length);
return 0;
}
/* 运行结果:
数组 arr 占用的存储空间为 20 字节
数组 arr 的长度为 5
*/
#include <stdio.h>
int main() {
int arr[3][4];
// 计算数组占用的存储空间大小
size_t arr_size = sizeof(arr);
printf("数组的大小为 %lu 字节。\n", arr_size);
// 计算每个元素占用的存储空间大小
size_t elem_size = sizeof(arr[0][0]);
printf("每个元素占用 %lu 字节。\n", elem_size);
// 计算数组中元素的个数
size_t num_elems = arr_size / elem_size;
printf("数组中包含 %lu 个元素。\n", num_elems);
return 0;
}
/* 运行结果:
数组的大小为 48 字节。
每个元素占用 4 字节。
数组中包含 12 个元素。
*/
使用一维数组字面量给二维数组初始化
#include <stdio.h>
int main() {
int arr[][3] = {1, 2, 3, 4, 5, 6, 7};
const int ROWS = sizeof(arr) / sizeof(arr[0]);
const int COLS = sizeof(arr[0]) / sizeof(int);
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
printf("数组有 %d 行,%d 列。\n", ROWS, COLS);
return 0;
}
/* 运行结果:
1 2 3
4 5 6
7 0 0
数组有 3 行,3 列。
*/
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
是一个无符号整数类型,用于表示字符串的长度
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello";
size_t len = strlen(str);
printf("字符串的长度为 %lu。\n", len);
return 0;
}
/* 运行结果:
字符串的长度为 5。
*/
char str[] = "Hello";
、char str[] = {'H', 'e', 'l', 'l', 'o', '\0'};
两种写法是等效的。
#include <stdio.h>
#include <string.h>
int main() {
char str[] = {'H', 'e', 'l', 'l', 'o', '\0'};
size_t len = strlen(str);
printf("字符串的长度为 %lu。\n", len);
return 0;
}
/* 运行结果:
字符串的长度为 5。
*/
strlen 的返回结果依旧是 5,可见 '\0'
结尾字符并不会被计入长度。
#include <stdio.h>
#include <string.h>
int main() {
char str[] = {'H', 'e', 'l', '\0', 'l', 'o', '\0'};
size_t len = strlen(str);
printf("字符串的长度为 %lu。\n", len);
return 0;
}
/* 运行结果:
字符串的长度为 3。
*/
如果字符串中包含 '\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() 函数可能会导致缓冲区溢出,从而破坏程序的内存空间。
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "hello world";
char dest[20];
printf("调用 strcpy 之前:\n");
printf("src = [%s]\n", src);
printf("dest = [%s]\n", dest);
strcpy(dest, src);
printf("调用 strcpy 之后:\n");
printf("src = [%s]\n", src);
printf("dest = [%s]\n", dest);
return 0;
}
/* 运行结果:
调用 strcpy 之前:
src = [hello world]
dest = []
调用 strcpy 之后:
src = [hello world]
dest = [hello world]
*/
strcat
- 函数声明:
char *strcat(char *dest, const char *src);
dest
目标字符串src
源字符串- 作用:将源字符串 src 拼接到目标字符串 dest 的末尾
- 返回:指向 dest 的指针
#include <stdio.h>
#include <string.h>
int main() {
char str1[10] = "Hello, ";
char str2[] = "world!";
printf("[%s] 的长度是:%lu\n", str1, strlen(str1));
strcat(str1, str2);
printf("[%s] 的长度是:%lu\n", str1, strlen(str1));
return 0;
}
/* 运行结果:
[Hello, ] 的长度是:7
[Hello, world!] 的长度是:13
*/
尽管最终的结果是符合我们预期的,但是上述代码是存在问题的。
因为 str1 只有 10 个字符的空间,但是将 "Hello, "
和 "world!"
两个字符串拼接在一起后会超过这个空间,从而导致未定义的行为。
补充:虽然结果是符合预期的,看似也没出现什么问题,这主要是因为我们的程序规模小,总共就这么几行代码,所以破坏一些内存结构也不会出现什么问题。如果规模大了,这种做法会导致一些内存中存储的数据被抹掉,后果是不堪设想的。
为了修复这个问题,需要将 str1 的空间扩大到足够容纳两个字符串拼接后的结果。
#include <stdio.h>
#include <string.h>
int main() {
char str1[20] = "Hello, ";
char str2[] = "world!";
printf("[%s] 的长度是:%lu\n", str1, strlen(str1));
strcat(str1, str2);
printf("[%s] 的长度是:%lu\n", str1, strlen(str1));
return 0;
}
/* 运行结果:
[Hello, ] 的长度是:7
[Hello, world!] 的长度是:13
*/
strcmp
- 函数声明:
int strcmp(const char *s1, const char *s2);
- 作用:用于比较两个字符串的大小
- 返回值:
strcmp()
函数会逐个比较两个字符串中的字符,直到出现不同字符或字符串结束符为止。当出现不同字符时,函数会根据字符的 ASCII 码或 Unicode 码将两个字符做差,并将差作为比较结果返回。- 如果 s1 小于 s2,则返回值为 负数
- 如果 s1 等于 s2,则返回值为 零
- 如果 s1 大于 s2,则返回值为 正数
#include <stdio.h>
#include <string.h>
int main() {
char s1[] = "world";
char s2[] = "hello";
char s3[] = "world";
int result1, result2, result3;
result1 = strcmp(s1, s2); // 将较大的字符串排在前面,返回正整数
result2 = strcmp(s2, s3); // 将较小的字符串排在前面,返回负整数
result3 = strcmp(s1, s3); // 两个字符串相同,返回0
printf("result1 = %d\n", result1);
printf("result2 = %d\n", result2);
printf("result3 = %d\n", result3);
return 0;
}
/* 运行结果:
result1 = 15
result2 = -15
result3 = 0
*/
从上述结果来看,可以得出以下公式:
至于为何是首字母,主要是因为第一个字母就是不同的字符。
#include <stdio.h>
#include <string.h>
int main() {
printf("'b' - 'a' = %d\n", 'b' - 'a');
printf("'a' - 'b' = %d\n", 'a' - 'b');
printf("'w' - 'h' = %d\n", 'w' - 'h');
printf("'h' - 'w' = %d\n", 'h' - 'w');
return 0;
}
/* 运行结果:
'b' - 'a' = 1
'a' - 'b' = -1
'w' - 'h' = 15
'h' - 'w' = -15
*/
#include <stdio.h>
#include <string.h>
int main() {
printf("strcmp(\"abc\", \"aaa\") = %d\n", strcmp("abc", "aaa"));
printf("strcmp(\"aaa\", \"aba\") = %d\n", strcmp("aaa", "aba"));
printf("strcmp(\"abc\", \"aba\") = %d\n", strcmp("abc", "aba"));
printf("strcmp(\"abb\", \"aba\") = %d\n", strcmp("abb", "aba"));
printf("strcmp(\"ab\", \"aba\") = %d\n", strcmp("ab", "aba"));
return 0;
}
/* 运行结果:
strcmp("abc", "aaa") = 1
strcmp("aaa", "aba") = -1
strcmp("abc", "aba") = 1
strcmp("abb", "aba") = 1
strcmp("ab", "aba") = -1
*/
结合 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,则返回值为 正数
字符数组初始化的多种方式
#include <stdio.h>
#include <string.h>
int main() {
// 直接初始化,这种做法,编译器会自动为我们在字符串的最后追加一个 '\0' 结尾符。
char str1[] = "Hello, world!";
// 使用 strcpy 函数初始化
char str2[20];
strcpy(str2, str1);
char str3[20];
strcpy(str3, "Hello, world!");
// 逐个赋值,需要手动添加结尾的结束符 '\0' 【容易出错,不建议使用】
char str4[20];
for (int i = 0; i <= 12; i++) {
str4[i] = "Hello, world!"[i];
}
str4[13] = '\0';
char str5[20];
for (int i = 0; i <= strlen(str1); i++) {
str5[i] = str1[i];
}
str5[13] = '\0';
printf("str1: %s\n", str1);
printf("str2: %s\n", str2);
printf("str3: %s\n", str3);
printf("str4: %s\n", str4);
printf("str5: %s\n", str5);
return 0;
}
/* 运行结果:
str1: Hello, world!
str2: Hello, world!
str3: Hello, world!
str4: Hello, world!
str5: Hello, world!
*/
注意:
- 保证字符数组足够长
- 注意结尾符的问题
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'; }
#include <stdio.h>
int main() {
char str[100];
printf("请输入一行字符串:");
fgets(str, 100, stdin);
printf("你输入的是:%s\n", str);
return 0;
}
/* 运行结果:
请输入一行字符串:hello world! 123 abc
你输入的是:hello world! 123 abc
*/
error: use of undeclared identifier ‘gets’
由于直接使用 gets 出现了报错 error: use of undeclared identifier 'gets'
这个错误提示是因为使用了 C 语言标准库函数 gets()
,但是在较新的 C 语言标准(C11 及之后的版本)中,gets() 函数已被废弃,不再被推荐使用。因此,在使用较新的编译器时,编译器会给出警告或错误提示。
推荐使用较新的函数 fgets() 来代替 gets(),fgets() 具有更好的安全性和可控性,使用方法类似,例如:
fgets(str, sizeof(str), stdin);
sizeof(str)
表示字符数组 str
的大小stdin
表示标准输入流,即从控制台获取用户输入
注意:fgets()
函数会把换行符也读入字符串中,因此需要在字符串末尾去掉换行符。可以使用以下代码去掉换行符:
if (str[strlen(str) - 1] == '\n') {
str[strlen(str) - 1] = '\0';
}
补充:有关
fgets
的更多内容,在介绍后续知识点时还会详细说明……
puts
- 函数声明:
int puts(const char *str);
- 作用:用于将一个字符串输出到标准输出设备(例如屏幕)该函数会自动在字符串的末尾添加换行符
\n
,从而保证输出格式的正确性。 - 注意:
puts()
函数不会检查字符串是否超出了缓冲区长度- 在使用该函数输出字符串时,需要确保字符串长度不会超过目标缓冲区的长度
- 如果需要更加安全的字符串输出函数,可以使用
printf()
或fwrite()
函数。
#include <stdio.h>
int main() {
char str[] = "Hello, world!";
puts(str);
return 0;
}
/* 运行结果:
Hello, world!
*/
字符串的结束标志
'\0'
4.1.5 练习 | 寻找 James
需求:查找以 James
开头的人名。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
const int n = 20, m = 5;
char name[][n] = {"Kate.Wate", "James.Tan", "Bull.Ben",
"Jimes.Tide", "James.Ting", "K.James.T"};
char James[] = "James";
int i, j, len = strlen("James");
for (i = 0; i < m; ++i) {
for (j = 0; j < len; ++j) {
if (name[i][j] != James[j])
break;
}
if (j == len) {
printf("%s has James\n", name[i]);
}
}
return 0;
}
/* 运行结果:
James.Tan has James
James.Ting has James
*/
扩展:查找名字中含有 James
的成员。
提示:可以使用 strcmp 函数来比较,如果匹配上子串 James
那么将返回 0
#include <stdio.h>
#include <string.h>
int main() {
const int MAX_LEN = 20;
const char SUBSTR[] = "James";
const int SUBSTR_LEN = strlen(SUBSTR);
char name[][MAX_LEN] = {"Kate.Wate", "James.Tan", "Bull.Ben",
"Jimes.Tide", "James.Ting", "K.James.T"}; // name 相当于一个二维数组
const int num_names = sizeof(name) / sizeof(name[0]); // 一共多少个名字
for (int i = 0; i < num_names; i++) {
int name_len = strlen(name[i]);
for (int j = 0; j < name_len - SUBSTR_LEN + 1; j++) {
if (strncmp(&name[i][j], SUBSTR, SUBSTR_LEN) == 0) {
printf("%s has %s\n", name[i], SUBSTR);
break;
}
}
}
return 0;
}
/* 运行结果:
James.Tan has James
James.Ting has James
K.James.T has James
*/
4.2 结构
结构
notes
结构类型
- 结构(Structure)类型是 C 语言中一种 自定义 的 复合 数据类型。
- 结构 将一组相关的变量看作一个存储单元,而不是各自独立的实体
- 结构有助于组织复杂的数据
struct
是用于定义结构体的关键字- 通过
struct
关键字可以将多个变量打包为一个复合类型的数据结构,方便对这些变量进行管理和维护
struct
关键字的语法格式:
struct structure_name {
member_type1 member_name1;
member_type2 member_name2;
...
member_typeN member_nameN;
} struct_instance;
structure_name
表示结构体的名称。member_type1
、member_type2
、……、member_typeN
表示结构体的成员类型。member_name1
、member_name2
、……、member_nameN
表示结构体的成员名称。struct_instance
是结构体的实例(或称对象),用于在程序中创建并操作结构体。
定义一个学生结构,要求该结构中存放学生的 id、姓名 name、分数 score
#include <stdio.h>
#include <string.h>
// 定义结构体
struct Student {
int id;
char name[20];
float score;
} stu;
int main() {
// 写 | 接头体实例 stu 中的成员
stu.id = 1000;
strcpy(stu.name, "Alice");
stu.score = 95.5;
// 读 | 结构体实例 stu 中的成员
printf("学生信息:\n");
printf("学号:%d\n", stu.id);
printf("姓名:%s\n", stu.name);
printf("分数:%.1f\n", stu.score);
return 0;
}
/* 运行结果:
学生信息:
学号:1000
姓名:Alice
分数:95.5
*/
在上述代码中,我们使用 struct
关键字定义了一个名为 Student
的结构体,其中包含三个成员变量:
id
表示学号name
表示姓名score
表示分数
在 main()
函数中,我们声明了一个结构体实例 stu
,通过 .
运算符为结构体的成员赋值,并输出了结构体实例的信息。
结构体初始化的多种方式
#include <stdio.h>
struct {
int id;
char name[20];
float score;
} stu = {1001, "Alice", 95.5};
// 定义了一个 匿名结构体,并在定义时即可完成成员变量的初始化,然后将其赋给了 stu。
int main() {
printf("学生信息:\n");
printf("学号:%d\n", stu.id);
printf("姓名:%s\n", stu.name);
printf("分数:%.1f\n", stu.score);
return 0;
}
/* 运行结果:
学生信息:
学号:1000
姓名:Alice
分数:95.5
*/
#include <stdio.h>
//定义结构体
struct Student {
int id;
char name[20];
float score;
} stu = { .id = 1000, .name = "Alice", .score = 95.5 };
// 使用成员初始化列表的方式,直接为结构体成员变量分别指定初始值,其中使用了点 . 运算符引用结构体成员变量。
int main() {
printf("学生信息:\n");
printf("学号:%d\n", stu.id);
printf("姓名:%s\n", stu.name);
printf("分数:%.1f\n", stu.score);
return 0;
}
/* 运行结果:
学生信息:
学号:1000
姓名:Alice
分数:95.5
*/
#include <stdio.h>
struct Student {
int id;
char name[20];
float score;
};
struct Student stu = {1001, "Alice", 95.5};
// 我们使用 {} 直接给结构体成员变量赋值,在定义结构体实例 stu 时即可完成初始化。
int main() {
printf("学生信息:\n");
printf("学号:%d\n", stu.id);
printf("姓名:%s\n", stu.name);
printf("分数:%.1f\n", stu.score);
return 0;
}
/* 运行结果:
学生信息:
学号:1000
姓名:Alice
分数:95.5
*/
#include <stdio.h>
//定义结构体
struct Student {
int id;
char name[20];
float score;
};
struct Student temp = {1000, "Alice", 95.5};
struct Student stu = temp;
// 先定义了一个名为 temp 的结构体变量,并给其成员变量赋值,然后将 temp 的值赋给了 stu,实现了 stu 的初始化。
int main() {
printf("学生信息:\n");
printf("学号:%d\n", stu.id);
printf("姓名:%s\n", stu.name);
printf("分数:%.1f\n", stu.score);
return 0;
}
/* 运行结果:
学生信息:
学号:1000
姓名:Alice
分数:95.5
*/
结构体的拷贝
- 数组无法直接拷贝:前面我们学习过数组,知道数组名表示的是数组的首地址,是一个不可改变的常量,所以即便是对于同类型的数组,我们也没法通过数组名来直接完成拷贝。
- 结构体可以直接拷贝:在结构体中,同类型的结构体之间是可以允许直接赋值的。
#include <stdio.h>
//定义结构体
struct Student {
int id;
char name[20];
float score;
};
struct Student temp = {1000, "Alice", 95.5};
struct Student stu = temp;
int main() {
// 不同结构体之间的数据互不影响
temp.score++;
stu.id = 1001;
printf("stu 学生信息:\n");
printf("学号:%d\n", stu.id);
printf("姓名:%s\n", stu.name);
printf("分数:%.1f\n", stu.score);
printf("temp 学生信息:\n");
printf("学号:%d\n", temp.id);
printf("姓名:%s\n", temp.name);
printf("分数:%.1f\n", temp.score);
return 0;
}
/* 运行结果:
stu 学生信息:
学号:1001
姓名:Alice
分数:95.5
temp 学生信息:
学号:1000
姓名:Alice
分数:96.5
*/
注意:
- 类型要求相同:同类型的结构体可以直接拷贝,拷贝时会将结构体的所有成员逐一复制到目标结构体中
- 指针共享问题:如果结构体中包含指针成员,直接拷贝可能会导致指针指向的内存地址被多个结构体共享,从而产生潜在的问题
课件题目 | 录入学生信息,并根据语文成绩降序
请帮老师写一个程序,要求存储本年级 100 个学生的姓名,学号,语文,数学,外语三门课程成绩,并 根据语文成绩递减排序,按名次输出所有学生信息。
100 个太多了些,先输 4 个意思意思就好……
#include <stdio.h>
#include <stdlib.h>
const int n = 4, m = 20;
typedef struct student {
char name[m];
int id;
float chinese;
float english;
float math;
} student;
int main() {
student s[n], tmp;
int i, max, j;
for (i = 0; i < n; ++i)
scanf("%s%d%f%f%f", s[i].name, &s[i].id, &s[i].chinese, &s[i].english,
&s[i].math);
for (i = 0; i < n; ++i) {
max = i;
for (j = i + 1; j < n; ++j)
if (s[j].chinese > s[max].chinese)
max = j;
tmp = s[i];
s[i] = s[max];
s[max] = tmp;
}
for (i = 0; i < n; ++i)
printf("%s %d %.2f %.2f %.2f\n", s[i].name, s[i].id, s[i].chinese,
s[i].english, s[i].math);
}
/* 运行结果:
张三 1 87 45 90
李四 2 100 44 92
王五 3 34 66 87
wu 4 72 89 78
李四 2 100.00 44.00 92.00
张三 1 87.00 45.00 90.00
wu 4 72.00 89.00 78.00
王五 3 34.00 66.00 87.00
*/