25 字符数组与字符串及多维数组详解:定义与初始化、访问与遍历、%s 格式符、内存剖析、编程实战
1 字符数组与字符串
1.1 字符数组
字符数组是 C 语言中用于存储一系列字符的基本数据结构。其定义方式与其他类型的数组类似,使用 char 类型来指定数组的元素类型。例如:
char arr[10]; // 定义一个可存储 10 个字符的数组
此数组 arr 能够存储 10 个字符。若用于存储字符串,数组中可包含字符串的结束符 \0。字符数组的元素可以是任意有效的 char 类型值,涵盖字母、数字、标点符号、控制字符(如换行符 \n)以及空字符 \0。
1.2 字符串
在 C 语言中,字符串是一系列以空字符 \0 结尾的字符序列。由于这一特性,字符串可通过字符数组来表示,但字符串并非 C 语言中的独立数据类型。
字符串末尾的结束符 \0 起着至关重要的作用,它是程序识别字符串结束位置的标志。在计算字符串长度等相关操作时,\0 虽不作为字符串实际内容的一部分参与诸如输出等显示操作,但在确定字符串边界上不可或缺,并且计算字符串长度等相关函数(如 sizeof 等操作)也会将其占用空间计算在内 。
字符串字面量(例如 "Hello World!")在 C 语言中会被编译器自动转换为字符数组,并在末尾添加一个 \0 作为结束符。例如,当编写:
char str[] = "Hello World!";
编译器会分配足够的空间来存储字符 H、e、l、l、o、空格、W、o、r、l、d、! 以及结束符 \0,共计 13 个字符,并将它们存储在 str 数组中。
1.3 字符数组与字符串的关系
- 字符数组可存储字符串:由于字符串以 \0 结尾,所以任何以 \0 结尾的字符数组均可被视为字符串。
- 字符串通过字符数组实现:在 C 语言中,字符串实际上借助字符数组来实现。字符串字面量会被编译器转换为字符数组,并在末尾添加 \0。
- 字符数组不一定存储字符串:字符数组能够存储任意 char 类型的值,包括非 \0 结尾的字符序列,此类数组不能被视为字符串。
2 格式占位符 %s
2.1 printf 中使用 %s
在 printf 函数中,%s 格式说明符用于输出以 \0 结尾的字符串。具体行为如下:
- 读取规则:printf 会从字符串的首字符开始输出,直到遇到 \0(空字符,ASCII 码为 0)时停止。
- 终止符作用:\0 仅作为字符串的结束标志,不会被输出到控制台或缓冲区。
- 未终止字符串的风险:若字符串未以 \0 结尾,printf 可能越界读取内存,导致输出异常(如乱码、含其他数据)或程序崩溃(如段错误)。
#include <stdio.h>int main()
{char str[] = "Hello World!";// sizeof(str) 返回数组总大小(13 字节,包括 '\0')// sizeof(str[0]) 返回单个字符大小(1 字节)printf("The string length is: %zu\n", sizeof(str) / sizeof(str[0])); // 输出:13printf("The string is: %s\n", str); // 输出:Hello World!return 0;
}
程序在 VS Code 中的运行结果如下所示:
2.2 scanf 中使用 %s
在 scanf 等输入函数中,%s 格式说明符用于从标准输入(如键盘)读取字符串,其行为如下:
- 读取规则:scanf 会持续读取字符,直到遇到第一个空白字符(如空格、制表符 \t 或换行符 \n)时停止。
- 终止符处理:scanf 会在读取的字符串末尾自动添加 \0,使其符合 C 语言字符串的规范。
关键注意事项:
- 无法读取含空格的字符串:
- scanf 遇到空白字符会立即停止读取,因此 scanf("%s", str) 无法读取包含空格的字符串(如 "Hello World" 仅能读取 "Hello")。
- 若需读取含空格的字符串,应使用 fgets 函数(例如 fgets(str, sizeof(str), stdin)),相关用法将在后续章节介绍。
- 缓冲区溢出风险:
- 若输入的字符串长度超过目标数组的容量,scanf 会越界写入字符。这将导致定义行为(Undefined Behavior),程序可能崩溃、数据损坏,甚至引发安全漏洞(如内存越界访问或恶意代码注入)。
- 防御措施:在 %s 前指定最大长度(如 %49s),确保输入不超过数组容量(假设数组容量为 50,需预留 1 字节给 \0)。
正确用法示例 :
#include <stdio.h>int main()
{// 定义一个字符数组 str,大小为 50 字节char str[50] = ""; // 初始化为一个空字符串,方便后续观察内存状态printf("请输入一个字符串(不含空格):");scanf("%49s", str); // 限制输入长度为 49,避免溢出// 输出输入的字符串printf("您输入的是:%s\n", str);// 计算字符串长度,这将会输出 50,因为在定义时,数组大小为 50 字节printf("字符串长度:%zu\n", sizeof(str) / sizeof(str[0])); // 输出:50// 单独验证printf("sizeof(str) = %zu\n", sizeof(str)); // 50printf("sizeof(str[0]) = %zu\n", sizeof(str[0])); // 1return 0; // 在此行打上断点,开启调试运行
}
程序在 VS Code 中的运行结果如下所示:
为了直观验证 scanf 在遇到空白字符时停止读取输入的行为,我们在程序的第 18 行(return 0; 所在行)设置了断点,并启动调试模式运行程序。在程序执行过程中,我们在终端窗口输入了测试字符串 abcdegf hijklmn。随后,通过分析调试过程中生成的二进制数据文件(或直接观察内存内容),可以清晰地看到:只有输入字符串中的 abcdegf 被成功存储到了数组中,而后续的输入内容(hijklmn)并未被存入数组,如下所示:
错误用法示例及风险:
#include <stdio.h>int main()
{char str[5]; // 数组容量为 5(实际可存储 4 字符 + '\0')printf("请输入一个字符串: ");scanf("%s", str); // 没有进行长度限制,输入过长会导致缓冲区溢出// %s 用于输出以 '\0' 结尾的字符串// 如果没有以 '\0' 结尾,输出结果可能异常或崩溃printf("您输入的是: %s\n", str);printf("字符串长度: %zu\n", sizeof(str) / sizeof(str[0])); // 输出:5// 单独验证printf("sizeof(str) = %zu\n", sizeof(str)); // 5printf("sizeof(str[0]) = %zu\n", sizeof(str[0])); // 1return 0; // 在此行打上断点,开启调试运行
}
程序在 VS Code 中的运行结果如下所示:
同样,我们在程序的第 19 行(return 0; 所在行)设置了断点,并启动调试模式运行程序。在程序执行过程中,我们在终端窗口输入了一个长度超过目标数组容量(5 字节)的测试字符串 123456789。随后,通过分析调试过程中生成的二进制数据文件(或直接观察内存内容),如下所示:
可以清晰地观察到以下现象:
-
越界写入行为:
-
当输入的字符串长度超过目标数组容量时,scanf 会发生越界写入,将多余的字符写入数组边界之外的内存空间。
-
-
内存破坏风险:
-
越界写入的字符会覆盖不属于数组自身的内存区域,如其他变量或程序数据,带来以下严重后果:
- 数据损坏:其他变量的值可能被意外修改,导致程序逻辑错误。
- 程序崩溃:可能引发段错误(Segmentation Fault)或未定义行为(Undefined Behavior),使程序异常终止。
- 安全漏洞:恶意输入可能利用缓冲区溢出漏洞执行任意代码,如代码注入攻击,威胁系统安全。
-
- 越界读取问题:
- 使用 printf 输出字符串时,它会从数组起始位置开始读取,直到遇到结束符 \0 才会停止。当发生缓冲区溢出时,printf 不仅会输出数组中的数据,还会继续读取并输出被覆盖的其他内存空间上的数据,导致越界读取内存。这种越界读取可能泄露敏感信息,进一步加剧安全风险。
3 字符数组的初始化
3.1 使用字符列表初始化
在使用字符列表初始化字符数组时,\0 的添加规则如下:
- 若赋值的元素数量少于数组长度,未显式初始化部分自动填 \0(类似整型数组默认初始化为 0)。
- 若赋值的元素数量等于数组长度或未指定数组长度(由编译器自动推断),则不会自动添加 \0。
- 若赋值的元素数量超过数组长度,编译器会警告或报错。
char str1[4] = {'t', 'o', 'm'}; // 编译器自动添加 '\0',等效于 "tom\0"
char str2[3] = {'t', 'o', 'm'}; // 无 '\0',不可作为字符串操作
char str3[] = {'j', 'a', 'c', 'k'}; // 数组大小为 4,无 '\0'
char str4[3] = {'a', 'b', 'c', 'd'}; // 编译警告:数组大小为 3,元素超过 3个
关键注意事项:
- 只有以 \0 结尾的字符数组才能视为有效字符串。
- 字符串操作函数(strlen、strcpy、strcat、printf 的 %s 等)会一直读取直到遇到 \0。
- 如果数组缺少 \0 结尾,这些函数会继续读取后续内存区域,可能导致非法内存访问。
#include <stdio.h>int main()
{// 正确初始化方式 1:显式包含'\0'char str1[13] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0'};// 正确初始化方式 2:自动添加'\0'(数组大小足够)char str2[4] = {'t', 'o', 'm'}; // 自动添加'\0'// 不安全初始化:没有 '\0' 结尾char str3[3] = {'t', 'o', 'm'}; // 不应以字符串形式使用char str4[] = {'j', 'a', 'c', 'k'}; // 不应以字符串形式使用// 安全打印printf("str1=%s\n", str1); // 正确printf("str2=%s\n", str2); // 正确// 不安全打印(未定义行为)printf("str3=%s\n", str3); // 由于没有结束标识,会包括相邻内存的数据,直到遇到结束标记printf("str4=%s\n", str4); // 由于没有结束标识,会包括相邻内存的数据,直到遇到结束标记// 安全替代方案:逐个字符打印(利用 for 循环)return 0;
}
程序在 VS Code 中的运行结果如下所示:
为了更直观地分析程序运行结果,我们在第 25 行(return 0;语句处)设置断点,以调试模式运行程序。通过调试过程中观察内存内容或分析生成的二进制数据文件,发现数组 str1 的二进制数据呈现如下特征:
数组 str2 的二进制数据呈现如下特征:
数组 str3 的二进制数据呈现如下特征:
数组 str4 的二进制数据呈现如下特征:
3.2 使用字面量初始化
在 C 语言中,字符串字面量(如 "I am happy")在内存中以字符数组形式存储,并自动以空字符 '\0' 结尾。因此,可以直接用字符串字面量初始化字符数组,编译器会自动在末尾添加 '\0',无需手动处理。
#include <stdio.h>int main()
{/* 可以直接用字符串字面量初始化字符数组,编译器会自动在末尾添加 '\0',无需手动处理 */// 方式 1:使用大括号(不推荐,降低可读性)char str1[] = {"I am happy"}; // 编译器自动添加 '\0',{} 非必需// 方式 2:直接使用字符串字面量(推荐方式)char str2[] = "I am happy"; // 编译器自动添加 '\0'printf("str1=%s \n", str1); // 输出:str1=I am happyprintf("str2=%s \n", str2); // 输出:str2=I am happy// str2 = "不可以对一个已经定义过的数组名进行重新赋值"; // 错误:不能对数组名进行赋值// 表达式必须是一个 lvalue(左值),而数组名是一个不可修改的 lvalue。return 0;
}
程序在 VS Code 中的运行结果如下所示:
注意:
不可以对一个已经定义过的数组名进行重新赋值。在 C 语言中,数组名是一个指向数组首元素的常量指针,这意味着它的值(即数组的起始地址)在数组定义时就已经确定,并且在程序的整个生命周期中是不可更改的。
4 字符数组的访问与遍历
4.1 基本操作
字符数组(字符串)的访问和遍历方式与普通数组一致,但需注意字符串以空字符 '\0' 结尾的特性。
#include <stdio.h>int main()
{// 定义字符串(字符数组,包含 "Hello!" 和结尾的 '\0')char string[] = "Hello!";// 计算数组长度(包含 '\0')// 因为字符类型占用一个字节,所以可以直接使用 sizeof 来计算长度int len = sizeof(string); // 等价于 sizeof(string)/sizeof(string[0])// 打印数组信息printf("数组长度(含结尾空字符): %d\n", len); // 输出:7printf("原始字符串: %s\n\n", string); // 输出:Hello!// 访问单个字符printf("第1个字符: %c\n", string[0]); // Hprintf("第5个字符: %c\n", string[4]); // oprintf("最后一个字符是空字符,不显示: %c\n", string[len - 1]); // 无输出('\0'不显示)printf("倒数第二个字符是: %c\n", string[len - 2]); // !// 修改字符内容string[0] = 'h'; // 首字母改为小写string[5] = '?'; // 替换感叹号// 遍历整个数组(包含 '\0',但 '\0' 不显示)printf("修改后的完整遍历(含空字符):\n");for (int i = 0; i < len; i++) // 包括最后一个字符('\0'){printf("string[%d]: %c ", i, string[i]);}// 遍历时跳过空字符(推荐方式)printf("\n\n修改后且跳过空字符的遍历:\n");for (int i = 0; i < len - 1; i++) // 忽略最后一个字符('\0'){printf("string[%d]: %c ", i, string[i]);}return 0;
}
程序在 VS Code 中的运行结果如下所示:
4.2 案例:反转字符串
#include <stdio.h>int main()
{char str[] = "Hello!"; // 可修改的字符数组printf("原始字符串: %s\n", str); // 输出:Hello!int length = sizeof(str); // 计算字符串长度,包括结尾的 '\0'char new_str[length]; // 新字符数组,长度与原字符串相同// 这种写法是 变长数组(Variable-Length Array, VLA)// 它允许在运行时根据变量值确定数组长度,VLA 是 C99 标准引入的特性// 计算过程// new_str[0] = str[length - 2];// new_str[1] = str[length - 3];// ......for (int i = 0; i < length - 1; i++) // length - 1 是因为要跳过结尾的 '\0'{new_str[i] = str[length - 2 - i]; // 反转字符串,-2 是因为要跳过结尾的 '\0'}new_str[length] = '\0'; // 显式添加字符串结束符,更安全printf("反转后字符串: %s\n", new_str); // 输出:!olleHreturn 0;
}
程序在 VS Code 中的运行结果如下所示:
5 多维数组
5.1 简介
多维数组是一种特殊的数据结构,其数组元素本身仍是数组。这种递归嵌套的特性,使得数据能够以类似表格、矩阵乃至更高维度的形式进行组织。每个维度分别对应数据的不同属性层级,例如行、列以及更高阶的层次,从而让数据呈现更加清晰、有序的结构化特征。
多维数组根据维度数量可分为二维数组、三维数组、四维数组等。接下来,我们以常见的二维数组为例展开详细说明。
下图是一个四行六列的二维数组示意图:
5.2 二维数组的定义
二维数组的定义方式与一维数组类似,但需要额外指定两个维度的大小,即行数和列数。其语法格式如下:
数据类型 数组名[行数][列数];
例如,定义一个 3 行 4 列的整数二维数组,可以写成:
int matrix[3][4];
5.3 二维数组的初始化
逐个元素赋值:
定义二维数组后,可以通过逐个元素赋值的方式进行初始化。以下是一个示例,定义了一个 4 行 6 列的二维数组 a,并逐一初始化其元素:
// 定义一个 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; // 第二行第二列的元素
// ...(其余元素以此类推)
以矩阵的形式初始化:
在 C 语言中,除了逐个元素赋值外,还可以在定义二维数组的同时直接初始化,这种方式更加简洁且易于阅读。以下是一个示例,定义并初始化了一个 4 行 6 列的二维数组 a:
int a[4][6] = {{10, 20, 30, 40, 50, 60},{100, 200, 300, 400, 500, 600},{1000, 2000, 3000, 4000, 5000, 6000},{10000, 20000, 30000, 40000, 50000, 60000}
};
在这个例子中,每一行的元素被明确地放置在一个大括号 {} 中,形成了一个清晰的矩阵式布局。
通过连续的数值初始化:
另一种初始化二维数组的方式是使用连续的数值,编译器会自动将这些数值匹配到相应的行列中。以下是一个示例,定义并初始化了一个 4 行 6 列的二维数组 b:
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};
这种方式虽然没有显式地使用大括号 {} 来分隔行,但编译器会根据提供的元素数量和数组的维度自动填充到相应的行列中。不过,这种方式在直观性和清晰性上可能稍逊一筹。
省略行数:
在完全初始化二维数组时,可以省略数组名后的第一个方括号中的大小(即行数),但列数(每个子数组的大小)是必须指定的。这是因为编译器可以通过初始化时提供的元素数量来推断出数组的行数。然而,出于清晰和可读性的考虑,通常建议同时指定行数和列数。以下是一个示例:
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};
// 编译器可以推断出有 4 行int matrix[][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}
};
// 编译器可以推断出有 3 行
需要注意的是,列数是不能省略的。以下是一个错误的示例:
int matrix[][] = { // 错误:列数不能省略 {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}
};
5.4 二维数组的内存分析
逻辑层面看,我们常用矩阵形式(如 3 行 4 列)来表示二维数组,这种表示方法能够直观地体现出行与列之间的关系,便于我们理解和操作数据。然而,在计算机的内存世界里,二维数组并非以二维的形式存储,而是采用线性、连续的方式存放所有元素。
在 C 语言中,二维数组的元素是按照行优先的顺序进行排列的。也就是说,编译器会先顺序存放第一行的所有元素,紧接着存放第二行的所有元素,以此类推,直至将整个二维数组的所有元素都存放在连续的内存空间中。以一个 3 行 4 列的二维数组 a[3][4] 为例,其在内存中的存放顺序如下所示:
为了更直观地分析二维数组各元素在内存中的存储情况,我们可以通过调试运行程序并查看内存空间来实现。以下面这个具体的二维数组为例:
#include <stdio.h>int main()
{int map[3][4] = {{1, 2, 3, 4}, // 第一行{11, 12, 13, 14}, // 第二行{21, 22, 23, 24} // 第三行};return 0; // 在此行打上断点,分析内存
}
在 VS Code 集成开发环境中,我们可以设置断点并运行程序,当程序执行到断点位置时,利用调试工具查看 map 数组在内存中的存储情况。通过观察内存地址和对应的元素值,可以清晰地看到元素是如何按照行优先的顺序连续存放的,如下所示:
5.5 二维数组元素的访问和遍历
访问二维数组的元素时,需要使用两个下标(索引),一个用于指定行(第一维),另一个用于指定列(第二维),这两个下标通常被称为行下标和列下标。为了遍历二维数组的所有元素,我们需要使用双层循环结构,外层循环控制行的遍历,内层循环控制列的遍历。
行数与列数的计算
要计算一个二维数组的行数和列数,我们可以利用 sizeof 运算符。具体方法如下:
-
计算行数:可以通过将整个数组的大小除以单行的大小来得到行数。
int rows = sizeof(array) / sizeof(array[0]);
- 计算列数:可以通过将单行的大小除以单个元素的大小来得到列数。这里有两种等效的方式:
int cols = sizeof(array[0]) / sizeof(二维数组的基本数据类型,如 int double 等);
// 或者
int cols = sizeof(array[0]) / sizeof(array[0][0]);
案例:计算元素和
以下是一个完整的示例,展示了如何定义一个 3 行 4 列的二维数组,并计算其所有元素的和:
#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);// 遍历并输出二维数组的每个元素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); // 输出结果为 150return 0;
}
程序在 VS Code 中的运行结果如下所示:
案例:计算多个平均成绩
以下是一个完整的示例,展示了如何使用二维数组存储三个班级(每个班级五名学生)的成绩,并计算每个班级的平均分以及所有班级的总平均分:
#include <stdio.h>int main()
{// 定义一个 3 行 5 列的数组,用于存储不同班级的学生的成绩double scores[3][5];// 计算行数和列数int rows = sizeof scores / sizeof scores[0];int cols = sizeof scores[0] / sizeof scores[0][0];// int cols = sizeof scores[0] / sizeof(double); 也可以使用这种方式计算列数// 遍历二维数组进行赋值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; // 记录所有班级总分数// 遍历班级(行)for (int i = 0; i < rows; i++){double class_sum = 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;
}
这个程序通过用户输入来填充二维数组,然后计算并输出每个班级的平均分以及所有班级的总平均分。通过这个案例,我们可以更好地理解如何在实际应用中使用二维数组进行数据存储和处理。
程序在 VS Code 中的运行结果如下所示:
案例:使用单层循环遍历二维数组
在 C 语言中,二维数组在内存中是按行优先顺序连续存储的。这意味着,二维数组中的所有元素在内存中是连续排列的,先存储第 0 行的所有元素,然后是第 1 行的所有元素,依此类推。因此,我们可以将二维数组视为一个一维数组来遍历,并通过简单的数学计算来访问每个元素的行号和列号。
以下是一个完整的示例,展示了如何使用一个单层循环来遍历二维数组:
#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);// 使用一个 for 循环遍历二维数组printf("二维数组的元素:\n");for (int index = 0; index < rows * cols; index++){// 计算当前元素的行和列int row = index / cols; // 整除运算符获取行号int col = index % cols; // 取模运算符获取列号// 访问元素并输出printf("%d ", map[row][col]);// 每输出一个元素后,检查是否需要换行if ((index + 1) % cols == 0){printf("\n"); // 每行输出完成后换行}}return 0;
}
行号计算:row = index / cols:
- index 是当前元素在一维视图中的线性索引。
- cols 是二维数组的列数。
- index / cols 计算的是当前元素位于第几行。这是因为每行有 cols 个元素,所以通过整除 cols,我们可以得到当前元素所在的行号。例如:
- 如果 index 是 0,cols 是 4,那么 0 / 4 = 0,表示该元素位于第 0 行(从 0 开始计数)。
- 如果 index 是 1,cols 是 4,那么 1 / 4 = 0,表示该元素位于第 0 行(从 0 开始计数)。
- 如果 index 是 2,cols 是 4,那么 2 / 4 = 0,表示该元素位于第 0 行(从 0 开始计数)。
- 如果 index 是 3,cols 是 4,那么 3 / 4 = 0,表示该元素位于第 0 行(从 0 开始计数)。
- 如果 index 是 4,cols 是 4,那么 4 / 4 = 1,表示该元素位于第 1 行(从 0 开始计数)。
- 如果 index 是 5,cols 是 4,那么 5 / 4 = 1,表示该元素位于第 1 行(从 0 开始计数)。
列号计算:col = index % cols:
- index % cols 计算的是当前元素在所在行中的第几列。这是因为取模运算 cols 可以得到当前元素在其所在行中的偏移量。例如:
- 如果 index 是 0,cols 是 4,那么 0 % 4 = 0,表示该元素位于该行的第 0 列(从 0 开始计数)。
- 如果 index 是 1,cols 是 4,那么 1 % 4 = 1,表示该元素位于该行的第 1 列(从 0 开始计数)。
- 如果 index 是 2,cols 是 4,那么 2 % 4 = 2,表示该元素位于该行的第 2 列(从 0 开始计数)。
- 如果 index 是 3,cols 是 4,那么 3 % 4 = 3,表示该元素位于该行的第 3 列(从 0 开始计数)。
- 如果 index 是 4,cols 是 4,那么 4 % 4 = 0,表示该元素位于该行的第 0 列(从 0 开始计数)。
- 如果 index 是 5,cols 是 4,那么 5 % 4 = 1,表示该元素位于该行的第 1 列(从 0 开始计数)。
- 换行逻辑:(index + 1) % cols == 0 可以用于判断当前元素是否是该行的最后一个元素。
程序在 VS Code 中的运行结果如下所示:
5.6 二维数组的越界访问
在 C 语言中,数组越界访问是指访问数组之外的内存位置。对于二维数组来说,如果访问的索引超出了数组的定义范围,就会发生越界访问。数组越界访问是一种未定义行为,可能导致不可预测的结果、程序崩溃或安全漏洞。以下是一个示例,说明越界访问的风险:
#include <stdio.h>int main()
{int arr[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};// 正确访问printf("arr[1][0] = %d\n", arr[1][0]); // 输出 5// 越界访问(仅用于说明,实际中应避免)printf("arr[0][4] (越界访问) = %d\n", arr[0][4]); // 输出不确定的值// 注意:越界访问可能导致未定义行为,实际运行时可能会崩溃或输出垃圾值if (arr[0][4] == arr[1][0]){printf("在某些编译环境下,越界访问的arr[0][4] 可能偶然等于 arr[1][0]");}return 0;
}
程序在 VS Code 中的运行结果如下所示:
在这个示例中,arr[0][4] 尝试访问第 0 行第 5 列的元素,但数组只有 4 列,因此这是一个越界访问。虽然在这个特定的程序运行中 arr[0][4] 可能偶然返回了 arr[1][0] 的值,但这只是一个偶然现象,不能作为通用规则。在不同的编译环境或运行时环境中,结果可能会有很大不同。
6 编程练习
6.1 二维数组存储与遍历字符串
在 C 语言中,使用二维字符数组可以存储多个字符串,并通过循环遍历输出。以下示例展示了如何定义一个二维字符数组,存储 "xiaoma"、"xiaolu" 和 "xiaoqi" 三个字符串,并使用循环输出这些字符串。
#include <stdio.h>int main()
{// 定义并初始化二维字符数组,存储三个字符串// 注意:需要留一个空字符作为字符串结束符char msg[3][7] = {"xiaoma", "xiaolu", "xiaoqi"};// 循环遍历并输出每个字符串for (int i = 0; i < 3; i++){printf("%s\n", msg[i]); // 输出每个字符串}return 0;
}
程序在 VS Code 中的运行结果如下所示:
6.2 数组元素排序(升序)
定义一个包含 5 个整数的数组,将其元素按升序排列并输出。
#include <stdio.h>int main()
{int numbers[5] = {5, 3, 1, 4, 2}; // 定义并初始化一个整数数组int temp; // 定义一个临时变量用于交换数组元素int len = sizeof(numbers) / sizeof(int); // 计算数组长度// 简单的冒泡排序算法for (int i = 0; i < len - 1; i++) // 外层循环控制排序轮数{for (int j = 0; j < len - i - 1; j++) // 内层循环控制每轮比较的次数{if (numbers[j] > numbers[j + 1]) // 如果当前元素大于下一个元素,则交换它们{temp = numbers[j]; // 将当前元素存入临时变量numbers[j] = numbers[j + 1]; // 将下一个元素赋值给当前元素numbers[j + 1] = temp; // 将临时变量赋值给下一个元素}}}printf("排序后的数组: ");for (int i = 0; i < len; i++){printf("%d ", numbers[i]);}printf("\n");return 0;
}
程序在 VS Code 中的运行结果如下所示:
6.3 字符数组(字符串)长度计算
定义一个字符数组(字符串),计算并输出其长度,不使用 strlen 函数和 sizeof 运算符。
#include <stdio.h>int main()
{char str[] = "Hello World!"; // 定义并初始化一个字符串int length = 0; // 初始化字符串长度为 0// 计算字符串长度,不包括字符串结束符 '\0'while (str[length] != '\0') // 当未到达字符串结束符 '\0' 时{length++;}printf("字符串的长度为: %d\n", length); // 12 (不包括 '\0')return 0;
}
程序在 VS Code 中的运行结果如下所示:
6.4 整数数组的逆序
定义一个包含 6 个整数的数组,将数组元素逆序排列并输出。
#include <stdio.h>int main()
{int numbers[6] = {1, 2, 3, 4, 5, 6}; // 定义并初始化一个整数数组int temp; // 定义一个临时变量用于交换数组元素for (int i = 0; i < 3; i++) // 只需要遍历数组的一半{temp = numbers[i]; // 将当前元素存入临时变量numbers[i] = numbers[5 - i]; // 将对应的对称元素赋值给当前元素numbers[5 - i] = temp; // 将临时变量赋值给对应的对称元素}printf("逆序后的数组: ");for (int i = 0; i < 6; i++){printf("%d ", numbers[i]);}printf("\n");return 0;
}
程序在 VS Code 中的运行结果如下所示:
6.5 字符数组(字符串)中查找特定字符
定义一个字符数组(字符串),查找并输出特定字符在字符串中的位置(索引),如果不存在则输出 -1。
#include <stdio.h>int main()
{char str[] = "programming"; // 定义并初始化一个字符串char target = 'a'; // 要查找的字符int position = -1; // 初始化位置为 -1,表示未找到for (int i = 0; str[i] != '\0'; i++) // 遍历字符串直到遇到结束符 '\0'{if (str[i] == target) // 如果当前字符与目标字符相同{position = i; // 记录位置break; // 退出循环}}printf("字符 '%c' 的位置为(0 开始计数): %d\n", target, position); // 5return 0;
}
程序在 VS Code 中的运行结果如下所示:
6.6 二维整数数组的列和
定义一个 3x4 的二维整数数组,计算并输出每一列的和。
#include <stdio.h>int main()
{// 定义一个 3 行 4 列的二维数组int matrix[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};for (int j = 0; j < 4; j++) // 遍历每一列{int col_sum = 0; // 初始化列和为 0for (int i = 0; i < 3; i++) // 遍历每一行{col_sum += matrix[i][j]; // 累加当前列的元素}printf("第 %d 列的和为: %d\n", j + 1, col_sum);}return 0;
}
程序在 VS Code 中的运行结果如下所示:
6.7 整数数组的元素去重
定义一个包含重复元素的整数数组,去除重复元素并输出结果。
#include <stdio.h>int main()
{// 定义一个包含重复元素的整数数组int numbers[8] = {1, 2, 2, 3, 4, 4, 5, 1};// 定义一个用于存储唯一元素的数组int unique[8];// 初始化唯一元素数组的索引int k = 0;// 遍历原始数组中的每个元素for (int i = 0; i < 8; i++){// 假设当前元素是唯一的int is_unique = 1;// 检查当前元素是否已经在唯一数组中for (int j = 0; j < k; j++){// 如果当前元素在唯一数组中存在,则标记为非唯一if (numbers[i] == unique[j]){is_unique = 0;break;}}// 如果当前元素是唯一的,则将其添加到唯一数组中if (is_unique){unique[k++] = numbers[i];}}// 输出去重后的数组printf("去重后的数组: ");for (int i = 0; i < k; i++){printf("%d ", unique[i]);}printf("\n");return 0;
}
程序在 VS Code 中的运行结果如下所示: