数组(二):二维数组定义与初始化、二维数组遍历、字符数组与字符串、字符串处理函数
好,我们接着聊数组。上一章我们把一维数组翻了个底朝天,这一章来看看它的“升级版”——二维数组,以及C语言里非常特殊的“字符数组”和“字符串”。
说实话,二维数组在初学阶段容易让人懵。我当年第一次看到 int a[3][4] 这种写法,心里想的是:“这玩意儿到底怎么在内存里放的?” 后来做项目多了,才发现它其实就是一张表格,或者一个矩阵。你想想看,一张Excel表格,有行有列,二维数组就是干这个的。
二维数组的定义与初始化
二维数组的定义,语法很简单:
类型 数组名[行数][列数];
比如:
int matrix[3][4]; // 3行4列的整型数组
这表示我定义了一个3行4列的表格,总共可以存放12个整数。行号从0到2,列号从0到3。
初始化方式有好几种,我挑常用的说:
- 按行初始化:最直观的方式,每行用一对花括号括起来。
int a[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
- 线性初始化:把所有元素按顺序写在一个花括号里。系统会自动按行填充。
int b[2][3] = {1, 2, 3, 4, 5, 6}; // 效果同上
- 部分初始化:只给部分元素赋值,未赋值的元素自动补0。
int c[2][3] = {
{1},
{4, 5}
};
// 结果是:第0行:1, 0, 0;第1行:4, 5, 0
注意: 二维数组定义时,行数可以省略,但列数绝对不能省。比如 int d[][3] = {{1,2},{3,4,5}}; 是合法的,编译器会根据初始化数据推断行数为2。但 int d[2][] 是错的——编译器不知道每行该分多少个元素。
二维数组的遍历
遍历二维数组,说白了就是“挨个访问每个格子”。通常用两层循环:外层控制行,内层控制列。
#include <stdio.h>
int main() {
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", arr[i][j]);
}
printf("\n"); // 每行结束后换行
}
return 0;
}
输出结果就是一张3行4列的表格。我在项目中处理图像数据时,经常用这种双层循环去遍历像素矩阵。嗯,这里有个小技巧:如果你想让代码跑得更快一点,可以把内层循环的列数提取成一个变量,避免每次循环都计算 sizeof 或访问数组边界。
个人习惯: 我写遍历时,喜欢把外层循环变量叫 row,内层叫 col。这样代码读起来一目了然,比用 i、j 要友好得多。尤其是代码量大了以后,回头维护时能省不少脑细胞。
字符数组与字符串
C语言里没有专门的“字符串”类型。字符串是用字符数组来存储的,以空字符 '\0' 结尾。这一点和Python、Java完全不同,刚转过来的同学容易踩坑。
字符数组的定义和初始化:
char str1[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 显式加'\0'
char str2[] = "Hello"; // 编译器自动加'\0',长度为6(包含结尾符)
char str3[10] = "Hi"; // 只用了前3个字符,其余自动补'\0'
这里有个关键点:str2 的长度是6,不是5。因为字符串字面量 "Hello" 在内存中实际占6个字节。我见过不少新手在定义字符数组时,忘记给 '\0' 留位置,结果字符串操作函数跑飞了。
我曾经 在做一个通信协议解析模块时,因为字符数组长度少算了1个字节,导致 strcpy 把数据写到了相邻变量的内存区域。查了两天才定位到问题——从那以后,我定义字符数组时都会刻意多留一个位置。
字符串处理函数
C标准库提供了 <string.h> 头文件,里面有一组常用的字符串处理函数。我挑几个最常用的说说:
| 函数 | 功能 | 注意事项 |
|---|---|---|
strlen(s) |
返回字符串长度(不含'\0') | 不是 sizeof,别搞混 |
strcpy(dest, src) |
将src复制到dest | dest空间要足够大,否则缓冲区溢出 |
strcat(dest, src) |
将src追加到dest末尾 | dest必须能容纳拼接后的结果 |
strcmp(s1, s2) |
比较两个字符串 | 相等返回0,s1<s2返回负数,s1>s2返回正数 |
strncpy(dest, src, n) |
安全版复制,最多复制n个字符 | 如果src长度<n,dest剩余部分补'\0' |
举个例子:
#include <stdio.h>
#include <string.h>
int main() {
char name[20] = "Alice";
char greeting[30] = "Hello, ";
// 计算长度
printf("name的长度: %zu\n", strlen(name)); // 输出5
// 拼接字符串
strcat(greeting, name);
printf("%s\n", greeting); // 输出 "Hello, Alice"
// 比较字符串
if (strcmp(name, "Bob") == 0) {
printf("名字是Bob\n");
} else {
printf("名字不是Bob\n");
}
return 0;
}
这里要特别提醒一下 strcmp 的返回值。很多人以为它返回0表示相等,1表示大于,-1表示小于。其实标准只规定了正、负、零三种情况,具体数值取决于实现。我建议只判断是否等于0,不要依赖具体的正负数值。
核心要点: 字符串处理函数不会自动检查目标数组的大小。如果你用 strcpy 或 strcat,一定要确保目标数组有足够的空间。否则,轻则数据错乱,重则程序崩溃。安全起见,优先使用 strncpy、strncat 这类带长度限制的版本。
说到字符数组,还有一个常见操作:用 scanf 读取字符串。但 scanf("%s", str) 有个问题——它遇到空格就停了。如果你想读取一整行(包括空格),可以用 fgets:
char line[100];
fgets(line, sizeof(line), stdin); // 读取一行,最多99个字符+'\0'
我个人更推荐 fgets,因为它能指定最大读取长度,避免缓冲区溢出。而且它会保留换行符,这一点和 gets 不同——gets 已经被C11标准废弃了,千万别再用。
好了,二维数组和字符串的内容就聊到这儿。这些东西在实际开发中几乎天天用,尤其是字符串处理,可以说是C语言编程的基本功。多写几遍,自然就熟了。
公众号:蓝海资料掘金营,微信 deep3321