第七章 数组(二):二维数组与字符串

各位同学,咱们接着聊数组。上一章我们把一维数组翻了个底朝天,这一章要上点难度了——二维数组和字符串。说实话,这两个东西在实际项目里出现的频率,比你想的要高得多。我做过的一个嵌入式项目里,光二维数组就用了七八个,用来存各种参数表。字符串就更不用说了,串口通信、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 本章知识体系

下面这张图把本章的核心内容串起来了,你看看能不能理清关系:

第七章 数组(二):二维数组与字符串 二维数组 定义:类型 数组名[行][列] 初始化:花括号嵌套 / 平铺 / 部分初始化 遍历:外层行 + 内层列(行优先) 内存布局:按行连续存储 字符数组与字符串 字符数组:char str[10] 字符串:以'\0'结尾的字符数组 初始化:双引号自动加'\0' 注意:预留'\0'的位置 字符串处理函数(<string.h>) strlen() — 求长度(不含'\0') strcpy()/strncpy() — 拷贝(推荐用安全版本) strcat()/strncat() — 拼接(注意缓冲区) strcmp() — 比较(返回0表示相等)

我的建议:字符串处理函数这块,别死记硬背。你只要记住几个核心的——strlen、strcpy、strcat、strcmp,其他的用到时查手册就行。我做了十几年嵌入式,也不敢说把所有函数都背下来了。关键是理解原理:字符串就是'\0'结尾的字符数组,所有函数都是围绕这个约定来设计的。

嗯,二维数组和字符串这块内容就讲到这里。代码写多了你会发现,这两个东西几乎无处不在。二维数组用来存表格数据、图像像素、矩阵运算;字符串用来处理文本、协议解析、日志输出。把基础打牢了,后面学指针和动态内存时会更轻松。


公众号:蓝海资料掘金营,微信deep3321