目录
1 字符数组(字符串)
1.1 介绍
1.2 字符数组的定义与初始化
1.2.1 使用字符列表进行初始化
1.2.1.1 VS Code 调试内存
1.2.2 使用字符串字面量进行初始化
1.3 字符数组的访问和遍历
2 多维数组
2.1 介绍
2.2 二维数组的定义
2.2.1 先定义后初始化
2.2.2 直接定义并初始化
2.2.2.1 以矩阵的形式初始化
2.2.2.2 通过连续的数值初始化
2.2.2.3 省略第一维的数组长度
2.3 二维数组的访问和遍历
2.3.1 计算行数和列数
2.3.2 案例演示
2.4 二维数组的内存分析
2.5 案例:计算三个班级的成绩平均分及总平均分
3 数组编程练习
3.1 循环输入并输出 5 个成绩
3.2 计算鸡的总体重与平均体重
3.3 创建并打印 26 个字母的数组
3.4 求出数组的最小值及其索引
4 测试题
1 字符数组(字符串)
1.1 介绍
用来存放字符的数组称为字符数组,也可以称为字符串。字符串的输入输出格式占位符是 %s。
%s 格式说明符专门用于输出 C 风格的字符串(最好是末尾要有结束符),即从指定的字符数组(或指针指向的位置)开始,直到遇到第一个 '\0' 字符为止的字符序列。这个结束符 '\0' 用于在内存中标记字符串的结束,但它不是字符串内容的一部分,因此在输出时不会包括它。
字符串结尾,会自动添加一个 \0 作为字符串结束的标志,所以字符数组最后一个元素必须(最好)是 \0。
\0 是 ASCII 码表中的第 0 个字符,用 NUL 表示,称为空字符,该字符既不能显示,也不是控制字符,输出该字符不会有任何效果,它在 C 语言中仅作为字符串的结束标志。
1.2 字符数组的定义与初始化
1.2.1 使用字符列表进行初始化
在给某个字符数组使用字符列表赋值时,如果没有显示的添加结束符 '\0':当赋值的元素个数小于字符数组的长度,则会自动在后面加 '\0' 表示字符串结束; 当赋值的元素的个数等于该数组的长度或不指定数组长度,则不会自动添加 '\0'。当赋值的元素的个数大于该数组的长度,编译器会报错,因为多余的初始化值没有对应的位置存储。
在上一章节中我们曾说过,当整型数组初始化的元素个数少于定义的数组长度时,未被初始化的元素将默认被赋值为 0。那么现在对于字符数组来说,未被初始化的元素将默认被赋值为 '\0'。
如果字符数组没有以 '\0' 结尾,则不应将其视为字符串进行字符串操作。字符串操作函数(如 strlen、strcpy、strcat 等)会一直读取字符直到遇到 '\0' 为止。如果数组没有以 '\0' 结尾,那么这些函数会继续读取数组后面的内存区域,直到偶然遇到某个内存位置恰好存储了 '\0' 为止,这可能导致访问不属于原数组的内存,引发安全问题。
#include <stdio.h>
int main()
{
// 显式地为字符串数组 str1 的每个字符赋值,包括空字符 '\0' 作为字符串的结束标识
// 这样定义字符串是安全的,因为明确指定了字符串的结束。
char str1[12] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0'};
// 为字符串数组 str2 的前三个字符赋值,数组大小是 4,编译器会自动在末尾添加 '\0'
// 这里编译器确实会自动添加 '\0',因为数组大小足够容纳一个额外的 '\0'。
char str2[4] = {'t', 'o', 'm'};
// str3 数组大小是 3,不会自动添加 '\0',这会导致未定义行为。
// 后续不应该使用 tr3 进行字符串操作,因为它不是以 '\0' 结尾的。
// 由于 str3 没有以 '\0'结尾,尝试将其作为字符串(如使用 %s 格式说明符)是未定义行为。
char str3[3] = {'t', 'o', 'm'};
// char str3[3] = {'t', 'o', 'm', 'm', 'm'}; // 报错
// 为字符数组 str4 的前四个字符赋值,但没有显式地添加 '\0'
// 这里数组大小是自动计算的,但不包括 '\0'。
// 使用 str4 作为字符串(如使用 %s 格式说明符)是不安全的,因为它不是以'\0'结尾的。
// 由于 str4 没有以'\0'结尾,尝试将其作为字符串将读取未定义的数据,直到遇到'\0'。
char str4[] = {'j', 'a', 'c', 'k'};
// 打印字符串
printf("str1=%s \n", str1); // 正确打印 "Hello World"
printf("str2=%s \n", str2); // 正确打印 "tom",因为编译器自动添加了 '\0'
// 下面的两行是未定义行为,因为 str3 和 str4 都不是以 '\0' 结尾的字符串
printf("str3=%s \n", str3); // 这里的行为是未定义的,因为 str3 没有以 '\0' 结尾
printf("str4=%s \n", str4); // 这里的行为也是未定义的,因为 str4 没有以 '\0' 结尾
// 替代方案:显式地打印 str3 和 str4 的前三个和前四个字符(不将它们视为字符串)
// 注意:这里仅为了演示,实际使用中应避免这种潜在的内存越界
printf("str3 first 3 chars: %c%c%c \n", str3[0], str3[1], str3[2]);
printf("str4 first 4 chars: %c%c%c%c \n", str4[0], str4[1], str4[2], str4[3]);
return 0;
}
输出结果如下所示:
1.2.1.1 VS Code 调试内存
对于上面这个程序,为什么输出结果是这样的呢?
下面我们通过调试程序来揭秘答案,首先在最后一行代码 return 0; 处添加一个断点,然后开启调试模式,如下图所示:
在左侧变量区域中,我们可以发现,str3(初始化时赋值的元素的个数等于该数组的长度)和 str4(数组定义时不指定数组长度)这两个字符数组最后没有结束符 '\0',如下图所示:
然后开始监视这四个数组,这需要添加监视的表达式,在左侧监视区域中,点击 + 号,依次输出 &str1,&str2,&str3,&str4 进行添加监视(也可以不用 & 符号,应为数组名就是首地址),如下图所示:
通过监视区域,可以看出这四个数组所在的内存地址,下面需要查看对应的二进制数据,操作如下图所示:
通过查看二进制数据文件,对于原程序的输出结果就一目了然了。
对于数组 str1 中存储的是: Hello World\0, H: 0x48,e: 0x65,l: 0x6c,l: 0x6c,o: 0x6f,(空格): 0x20,W: 0x57,o: 0x6f,r: 0x72,l: 0x6c,d: 0x64 ,\0:0x00,完整 ASCII 十六进制表示为:48656c6c6f20576f726c6400。对应的内存中的存储地址如下图所示:
由于 str1 数组最后一个元素存储了结束符 '\0',所以通过 printf 的格式占位符 %s 可以正确输出。
对于数组 str2 中存储的是:tom 以及编辑器自动添加的 '\0'。对应的内存中的存储地址如下图所示:
由于 str2 数组最后一个元素存储了结束符 '\0',所以通过 printf 的格式占位符 %s 也可以正确输出。
对于数组 str3 中存储的是:tom ,由于赋值的元素的个数等于该数组的长度,编译器不会自动添加 '\0'。如果通过 printf 的格式占位符 %s 进行输出,会一直读取输出字符直到遇到 '\0' 为止。所以,通过 printf 的格式占位符 %s 输出的内容是:tomtom,对应的内存中的存储地址如下图所示:
对于数组 str4 中存储的是:jack,由于初始化时未指定数组长度,编译器不会自动添加 '\0'。如果通过 printf 的格式占位符 %s 进行输出,会一直读取输出字符直到遇到 '\0' 为止。所以,通过 printf 的格式占位符 %s 输出的内容是:jacktomtom,对应的内存中的存储地址如下图所示:
1.2.2 使用字符串字面量进行初始化
如果一直使用之前那种方法进行数组的初始化,当字符较多时,就会显得不够便捷,因此现在介绍一种更简便的方法。
在 C 语言中,字符串字面量(如 "I am happy")在内存中是以字符数组的形式存储的,且每个字符串字面量都自动以空字符('\0')作为结束符。
当直接使用双引号包围的字符串字面量来初始化字符数组时,编译器会自动在字符串的末尾添加 '\0' 作为结束符。
#include <stdio.h>
int main()
{
// 因为字符串字面量本身就是以'\0'结尾的字符序列。
// 编译器会忽略大括号,并自动在字符串末尾添加'\0'。
char str1[] = {"I am happy"}; // 后面自动添加 \0,{} 的使用是不必要的
// 这是更常见的初始化字符串的方式,直接使用双引号包围的字符串字面量。
// 编译器会自动在字符串末尾添加 '\0'。
char str2[] = "I am happy"; // 省略 {} 号,后面自动添加 \0
printf("str1=%s \n", str1); // str1=I am happy
printf("str2=%s \n", str2); // str2=I am happy
return 0;
}
当使用字符串字面量来初始化字符数组时,初始化列表中如果还使用大括号 {} 来包围字符串字面量,虽然语法上允许,但通常不推荐,因为它可能会降低代码的可读性,并且容易让人误解为可以修改字符串的内容(实际上字符串字面量是不可修改的)。建议直接使用双引号包裹进行初始化!
1.3 字符数组的访问和遍历
字符数组(字符串)的访问和遍历,按照一般数组的方式访问和遍历即可。
#include <stdio.h>
int main()
{
// 定义字符串(实际上是一个字符数组,包含字符串 "Hello" 和结尾的空字符 '\0')
char string[] = "Hello!";
// 计算包含字符串的字符数组的总长度(包含结尾的空字符'\0')
int len = sizeof string / sizeof string[0];
// char 类型的数据占 1 字节,即 sizeof string[0] = 1
// 所以可以不用除以 sizeof string[0],
int len1 = sizeof string;
// 打印字符串
printf("%s \n", string); // Hello!
// 打印整个字符数组的长度(包含结尾的空字符'\0')
printf("数组长度(包括结尾的空字符): %d <-> %d\n", len, len1); // 7 <-> 7
// 访问字符串的第 5 个字符(索引为 4 )
printf("第5个字符: %c \n\n", string[4]); // o
// 修改字符串的第 1 个字符(索引为 0 )
string[0] = 'h';
// 修改字符串的第 6 个字符(索引为 5 )
string[5] = '?';
// 遍历整个字符数组(包括结尾的空字符'\0',但'\0'在输出时不会显示)
printf("第一次遍历字符数组:\n");
for (int i = 0; i < len; i++)
{
printf("string中的第%d字母为:%c \n", (i + 1), string[i]);
}
// 注意:上述循环中,虽然'\0'也会被计算在内,但由于它是空字符,所以在控制台上不会显示。
// 如果不想打印 '\0',可以减少一次循环
printf("第二次遍历字符数组:\n");
for (int i = 0; i < len - 1; i++)
{
printf("string中的第%d字母为:%c \n", (i + 1), string[i]);
}
return 0;
}
注意:上述循环中,虽然 '\0' 也会被计算在内,但由于它是空字符,所以在控制台上不会显示。
输出结果如下所示:
2 多维数组
2.1 介绍
如果数组的元素还是数组,这样的数组就称为多维数组。这种多层次的结构允许我们以表格或矩阵的方式组织数据,其中每个维度都对应于不同的行、列或更多的维度,使数据更加结构化和有组织。
多维数组可以分为二维数组、三维数组、四维数组 …… 等,这里我们以二维数组为例进行演示。
下图是一个四行六列的二维数组示意图:
2.2 二维数组的定义
二维数组的定义类似于一维数组,但需要指定两个维度的大小:行数和列数。语法格式如下:
数据类型 数组名[行数][列数];
2.2.1 先定义后初始化
在定义完二维数组后,可以通过逐个元素赋值的方式对其进行初始化。
// 定义一个 4 行 6 列的二维数组
int a[4][6];
// 进行初始化赋值
a[0][0] = 10; // 第一行第一列的元素
a[0][1] = 20; // 第一行第二列的元素
a[0][2] = 30; // 第一行第三列的元素
a[0][3] = 40; // 第一行第四列的元素
a[0][4] = 50; // 第一行第五列的元素
a[0][5] = 60; // 第一行第六列的元素
a[1][0] = 100; // 第二行第一列的元素
a[1][1] = 200; // 第二行第二列的元素
// ...
2.2.2 直接定义并初始化
在 C 语言中,我们不仅可以先定义二维数组然后再逐一初始化,还可以在定义的同时直接初始化数组。这种方式更加简洁并且易于阅读。
2.2.2.1 以矩阵的形式初始化
定义并初始化一个 4 行 6 列的二维数组,以矩阵的形式初始化:
int a[4][6] = {
{10, 20, 30, 30, 40, 60},
{100, 200, 300, 400, 500, 600},
{1000, 2000, 3000, 4000, 5000, 6000},
{10000, 20000, 30000, 40000, 50000, 60000}
};
在这个例子中,每一行的元素被明确地放置在一个大括号 {} 中,形成一个矩阵式的布局。这样的初始化方式使得数组的结构非常清晰。
2.2.2.2 通过连续的数值初始化
定义并初始化一个 4 行 6 列的二维数组,通过连续的数值初始化,编译器会自动匹配到各行和各列:
int b[4][6] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24};
这种方式中,虽然没有显式地使用大括号 {} 来分隔行,但是编译器会根据提供的元素数量和数组的维度自动填充到相应的行列中,就是看着不是很直观清晰。
2.2.2.3 省略第一维的数组长度
如果所提供的值的数量能够与数组元素的数量对应,可以省略第一维的数组长度【行数】:
int b[][6] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24};
这里,第一维的长度可以不指定,编译器会根据初始化列表中的元素数量自动推断出数组的行数。
注意:在一维数组中,可以省略数组的长度,编译器会自动计算。这一特性也可以应用于二维数组,不过只能省略行数,列数不能省略,否则编译器无法正确解析数组的结构。
2.3 二维数组的访问和遍历
访问二维数组的元素,需要使用两个下标(索引),一个用于访问行(第一维),另一个用于访问列(第二维),我们通常称为行下标(行索引)或列下标(列索引)。
遍历二维数组,需要使用双层循环结构。
2.3.1 计算行数和列数
计算行数: 使用 sizeof(a) 获取整个数组的大小,然后除以 sizeof(a[0]),即单行的大小,得到行数。int rows = sizeof(a) / sizeof(a[0]);
计算列数: 使用 sizeof(a[0]) 获取单行的大小,然后除以 sizeof(a[0][0]),即单个元素的大小,得到列数。int cols = sizeof(a[0]) / sizeof(a[0][0]); 或者 int cols = sizeof(a[0]) / sizeof(数组的基本数据类型);
2.3.2 案例演示
#include <stdio.h>
int main()
{
// 定义一个 3 行 4 列的二维数组
int map[3][4] = {
{1, 2, 3, 4}, // 第一行
{11, 12, 13, 14}, // 第二行
{21, 22, 23, 24} // 第三行
};
// 计算第一维度(行数)的长度
int rows = sizeof(map) / sizeof(map[0]);
// 计算第二维度(列数)的长度
int cols = sizeof(map[0]) / sizeof(int);
int cols2 = sizeof(map[0]) / sizeof(map[0][0]);
// 遍历并输出二维数组的每个元素
printf("二维数组的元素:\n");
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
printf("%d \t", map[i][j]); // 使用制表符 \t 使输出更加整齐
}
// 打印完一行就换行
printf("\n");
}
// 计算二维数组中所有元素的和
int sum = 0;
printf("计算所有元素的和:\n");
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
sum += map[i][j];
}
}
printf("所有元素的和:%d\n", sum); // 150
return 0;
}
输出结果如下所示:
2.4 二维数组的内存分析
用矩阵形式(如 3 行 4 列形式)表示二维数组,是逻辑上的概念,能形象地表示出行列关系。而在内存中,各元素是连续存放的,不是二维的,是线性的。
C 语言中,二维数组中元素排列的顺序是按行存放的。即:先顺序存放第一行的元素,再存放第二行的元素。
比如,举例,数组 a[3][4] 在内存中的存放:
通过调试内存,可以更直观的分析出二维数组在内存中的存储情况,以下面这个二维数组为例:
int map[3][4] = {
{1, 2, 3, 4}, // 第一行
{11, 12, 13, 14}, // 第二行
{21, 22, 23, 24} // 第三行
};
Clion 中的内存调试情况:
VS Code 中的内存调试情况:
2.5 案例:计算三个班级的成绩平均分及总平均分
现在有三个班,每个班五名同学,用二维数组保存他们的成绩,并求出每个班级平均分、以及所有班级平均分,数据要求从控制台输入。
#include <stdio.h>
int main()
{
// 定义一个 3 行 5 列的数组,用于存储不同班级的学生的成绩
double scores[3][5];
// 定义变量存储第一个维度长度(班级数)和第二个维度长度(学生数)
// int rows = 3, cols = 5;
int rows = sizeof scores / sizeof scores[0];
int cols = sizeof scores[0] / sizeof scores[0][0];
/* 暂时先不对用户输入的数据做校验 */
// 遍历二维数组进行赋值
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
// 提示用户输入成绩
printf("请输入第%d个班的第%d个学生的成绩:", i + 1, j + 1);
// 读取用户输入的成绩
scanf("%lf", &scores[i][j]);
}
}
// 遍历数组,计算每个班级的平均分和总的平均分
// 定义变量记录所有班级总分数
double total_sum = 0;
// 定义变量记录每个班级总分数
double class_sum = 0;
// 遍历班级(行)
for (int i = 0; i < rows; i++)
{
// 将当前班级总分数重置为 0,至关重要!!!
// 或者将这个变量的声明拿进来
class_sum = 0;
// 遍历当前班级的每个成绩(该行的每一列)
for (int j = 0; j < cols; j++)
{
// 累加成绩
class_sum += scores[i][j];
}
// 输出当前班级的平均分
printf("第%d个班级的平均分为:%.2f \n", i + 1, class_sum / cols);
// 将该班级总分加入到所有总分中
total_sum += class_sum;
}
// 输出所有班级的平均分
printf("所有班级的平均分为:%.2f \n", total_sum / (rows * cols));
return 0;
}
输出结果如下所示:
3 数组编程练习
3.1 循环输入并输出 5 个成绩
从终端循环输入 5 个成绩,保存到 double 数组,并输出。
#include <stdio.h>
int main()
{
// 定义一个 double 类型的数组,用于保存成绩
double grades[5];
// 计算数组的长度
int length = sizeof(grades) / sizeof(grades[0]);
// 遍历数组进行赋值
for (int i = 0; i < length; i++)
{
// 提示用户输入成绩
printf("请输入第 %d 个成绩:", i + 1);
// 读取用户输入的成绩
scanf("%lf", &grades[i]);
}
// 输出成绩
printf("成绩为:");
for (int i = 0; i < length; i++)
{
printf("%.2f ", grades[i]);
}
return 0;
}
输出结果如下所示:
3.2 计算鸡的总体重与平均体重
一个养鸡场有 6 只鸡,它们的体重分别是 3kg,5kg,1kg,3.4kg,2kg,50kg。请问这六只鸡的总体重是多少?平均体重是多少?
#include <stdio.h>
int main()
{
// 定义一个 double 类型的数组,用于保存鸡的体重
double weights[] = {3.0, 5.0, 1.0, 3.4, 2.0, 50.0};
// 计算数组的长度
int num_chickens = sizeof(weights) / sizeof(weights[0]);
// 初始化总体重为 0
double total_weight = 0.0;
// 遍历数组累加总体重
for (int i = 0; i < num_chickens; i++)
{
total_weight += weights[i];
}
// 计算平均体重
double average_weight = total_weight / num_chickens;
// 输出总体重和平均体重
printf("总体重为:%.2f kg\n", total_weight); // 64.40 kg
printf("平均体重为:%.2f kg\n", average_weight); // 10.73 kg
return 0;
}
3.3 创建并打印 26 个字母的数组
创建一个 char 类型的 26 个元素的数组,分别放置 'A'-'Z‘。使用 for 循环访问所有元素并打印出来。
#include <stdio.h>
int main()
{
// 长度 26 ,后面使用循环输出
char letters1[26];
// 长度 27 ,比 26 大一,用于保存结束符,后面使用 %s 输出
char letters2[27];
// 方法一
for (int i = 0; i < 26; i++)
{
letters1[i] = 'A' + i;
}
// 循环打印数组元素 - 可以没有'\0'
for (int i = 0; i < 26; i++)
{
printf("%c ", letters1[i]);
// A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
}
printf("\n");
// 方法二
for (char c = 'A'; c <= 'Z'; c++)
{
letters2[c - 'A'] = c;
}
// 末尾添加结束符,方便后面操作字符串
letters2[26] = '\0';
// 使用 %s 输出最好末尾要有结束符 '\0'
// %s 格式说明符被设计为仅输出字符串的可见部分,即直到第一个 '\0' 字符之前的所有字符。
printf("%s", letters2); // ABCDEFGHIJKLMNOPQRSTUVWXYZ
return 0;
}
提示:使用 %s 输出最好末尾要有结束符 '\0',%s 格式说明符被设计为仅输出字符串的可见部分,即直到第一个 '\0' 字符之前的所有字符。
3.4 求出数组的最小值及其索引
请求出一个数组的最小值,并得到对应的索引。
#include <stdio.h>
int main()
{
// 定义一个 double 类型的数组,用于保存成绩
double grades[] = {85.5, 92.0, 78.3, 90.1, 82.7};
// 计算数组的长度
int length = sizeof(grades) / sizeof(grades[0]);
// 初始化最小值,假设第一个是最值
double min_value = grades[0];
// 初始化索引
int min_index = 0;
// 寻找最小值及其索引
// i 可以从 1 开始
for (int i = 1; i < length; i++)
{
if (grades[i] < min_value)
{
min_value = grades[i];
min_index = i;
}
}
// 输出最小值及其索引
printf("最小值为:%.2f,索引为:%d\n", min_value, min_index);
// 最小值为:78.30,索引为:2
return 0;
}
4 测试题
1. 字符数组(字符串)的最后一个元素是什么?
【答案】\0
2. 请写出下面程序的运行结果:
int arr[3][2] = {{10,20},{30,40},{50,60}};
printf("%d", arr[1][1] + arr[2][0]);
【答案】90
【解析】arr[1][1] 可以获取到 40,arr[2][0] 可以获取到 50, 40+50=90。