第七章 数组(二):二维数组与字符串
各位同学,咱们接着聊数组。上一章我们把一维数组翻了个底朝天,这一章要上点难度了——二维数组和字符串。说实话,这两个东西在实际项目里出现的频率,比你想的要高得多。我做过的一个嵌入式项目里,光二维数组就用了七八个,用来存各种参数表。字符串就更不用说了,串口通信、LCD显示、日志输出,哪哪都有它。
7.1 二维数组的定义与初始化
二维数组,说白了就是「数组的数组」。你想想看,一维数组是一排格子,那二维数组就是一张表格——有行有列。
定义格式很简单:
类型 数组名[行数][列数];
比如:
int matrix[3][4]; // 3行4列的整型数组
这就定义了一个3行4列的表格,总共12个元素。内存里怎么排的?按行优先——先存第0行所有列,再存第1行所有列,以此类推。
初始化有几种方式,我直接上代码:
// 方式1:全部初始化
int a[2][3] = {{1,2,3}, {4,5,6}};
// 方式2:省略内层花括号(按行优先顺序赋值)
int b[2][3] = {1,2,3,4,5,6}; // 效果同上
// 方式3:部分初始化,未指定的补0
int c[2][3] = {{1}, {4}};
// 结果:c[0] = {1,0,0}, c[1] = {4,0,0}
// 方式4:省略行数(必须指定列数)
int d[][3] = {{1,2,3}, {4,5,6}, {7,8,9}}; // 自动推断行数为3
我的习惯:写二维数组初始化时,我永远用内层花括号。虽然省略也能编译通过,但可读性差太多了。有一次我接手别人的代码,看到一堆数字平铺着,数了半天才知道哪行是哪行——那叫一个痛苦。
7.2 二维数组的遍历
遍历二维数组,标准做法是双重循环。外层循环行,内层循环列。
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("%4d", arr[i][j]);
}
printf("\n");
}
输出效果:
1 2 3 4
5 6 7 8
9 10 11 12
这里有个细节要注意——行优先还是列优先。C语言是行优先的,所以外层循环走行、内层循环走列,这样内存访问是连续的,缓存命中率高。
我曾经踩过的坑:有一次写图像处理算法,把循环顺序写反了——外层列、内层行。结果同样的数据量,处理时间多了好几倍。后来用性能分析工具一看,全是缓存未命中。从那以后,我写二维数组遍历一定会先确认循环顺序。
7.3 字符数组与字符串
C语言里没有专门的字符串类型。字符串就是用字符数组存的,以'\0'(空字符)结尾。
// 字符数组
char str1[] = {'H', 'e', 'l', 'l', 'o'}; // 没有'\0',不是字符串
char str2[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 是字符串
char str3[] = "Hello"; // 自动加'\0',长度6(包含'\0')
char str4[10] = "Hello"; // 预留了空间,后面自动补'\0'
你看,用双引号括起来的字符串常量,编译器会自动在末尾加'\0'。这是C语言的约定,所有字符串处理函数都依赖这个约定来工作。
字符数组和字符串的区别,说白了就一条:有没有'\0'结尾。有,就是字符串;没有,就是普通字符数组。
重要:定义字符数组时,一定要给'\0'留位置。比如要存"Hello"(5个字符),数组长度至少是6。
7.4 字符串处理函数
C标准库提供了
7.4.1 strlen() — 求字符串长度
char str[] = "Hello";
int len = strlen(str); // len = 5,不包含'\0'
注意,strlen()返回的是字符个数,不包括结尾的'\0'。而sizeof()返回的是数组占用的总字节数,包括'\0'。
char str[] = "Hello";
printf("strlen = %zu\n", strlen(str)); // 5
printf("sizeof = %zu\n", sizeof(str)); // 6
7.4.2 strcpy() 和 strncpy() — 字符串拷贝
char src[] = "Hello";
char dest[10];
strcpy(dest, src); // 把src拷贝到dest,包括'\0'
// 更安全的做法
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // 手动加'\0'
我曾经犯过的错:早期写代码时直接用strcpy(),没检查目标数组大小。结果有一次源字符串比目标数组长,直接缓冲区溢出,程序跑飞了。从那以后,我只要拷贝字符串,一律用strncpy(),并且手动保证结尾有'\0'。
7.4.3 strcat() 和 strncat() — 字符串拼接
char str[20] = "Hello ";
strcat(str, "World!"); // str变成"Hello World!"
// 安全版本
strncat(str, "World!", sizeof(str) - strlen(str) - 1);
7.4.4 strcmp() — 字符串比较
char s1[] = "Hello";
char s2[] = "World";
char s3[] = "Hello";
if(strcmp(s1, s2) == 0) {
printf("s1和s2相等\n");
} else {
printf("s1和s2不相等\n");
}
if(strcmp(s1, s3) == 0) {
printf("s1和s3相等\n");
}
strcmp()的返回值:相等返回0,s1小于s2返回负数,s1大于s2返回正数。比较规则是按字典序,一个字符一个字符比ASCII码。
7.4.5 其他常用函数
| 函数 | 功能 | 注意 |
|---|---|---|
| sprintf() | 格式化输出到字符串 | 注意缓冲区大小,建议用snprintf() |
| sscanf() | 从字符串读取格式化数据 | 返回成功匹配的项数 |
| strchr() | 查找字符在字符串中首次出现的位置 | 返回指针,没找到返回NULL |
| strstr() | 查找子串在字符串中首次出现的位置 | 返回指针,没找到返回NULL |
| strtok() | 分割字符串 | 会修改原字符串,注意线程安全 |
7.5 本章知识体系
下面这张图把本章的核心内容串起来了,你看看能不能理清关系:
我的建议:字符串处理函数这块,别死记硬背。你只要记住几个核心的——strlen、strcpy、strcat、strcmp,其他的用到时查手册就行。我做了十几年嵌入式,也不敢说把所有函数都背下来了。关键是理解原理:字符串就是'\0'结尾的字符数组,所有函数都是围绕这个约定来设计的。
嗯,二维数组和字符串这块内容就讲到这里。代码写多了你会发现,这两个东西几乎无处不在。二维数组用来存表格数据、图像像素、矩阵运算;字符串用来处理文本、协议解析、日志输出。把基础打牢了,后面学指针和动态内存时会更轻松。
公众号:蓝海资料掘金营,微信deep3321