数组(二):二维数组定义与初始化、二维数组遍历、字符数组与字符串、字符串处理函数

好,我们接着聊数组。上一章我们把一维数组翻了个底朝天,这一章来看看它的“升级版”——二维数组,以及C语言里非常特殊的“字符数组”和“字符串”。

说实话,二维数组在初学阶段容易让人懵。我当年第一次看到 int a[3][4] 这种写法,心里想的是:“这玩意儿到底怎么在内存里放的?” 后来做项目多了,才发现它其实就是一张表格,或者一个矩阵。你想想看,一张Excel表格,有行有列,二维数组就是干这个的。

二维数组的定义与初始化

二维数组的定义,语法很简单:

类型 数组名[行数][列数];

比如:

int matrix[3][4];  // 3行4列的整型数组

这表示我定义了一个3行4列的表格,总共可以存放12个整数。行号从0到2,列号从0到3。

初始化方式有好几种,我挑常用的说:

  1. 按行初始化:最直观的方式,每行用一对花括号括起来。
int a[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};
  1. 线性初始化:把所有元素按顺序写在一个花括号里。系统会自动按行填充。
int b[2][3] = {1, 2, 3, 4, 5, 6};  // 效果同上
  1. 部分初始化:只给部分元素赋值,未赋值的元素自动补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。这样代码读起来一目了然,比用 ij 要友好得多。尤其是代码量大了以后,回头维护时能省不少脑细胞。

字符数组与字符串

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,不要依赖具体的正负数值。

核心要点: 字符串处理函数不会自动检查目标数组的大小。如果你用 strcpystrcat,一定要确保目标数组有足够的空间。否则,轻则数据错乱,重则程序崩溃。安全起见,优先使用 strncpystrncat 这类带长度限制的版本。

说到字符数组,还有一个常见操作:用 scanf 读取字符串。但 scanf("%s", str) 有个问题——它遇到空格就停了。如果你想读取一整行(包括空格),可以用 fgets

char line[100];
fgets(line, sizeof(line), stdin);  // 读取一行,最多99个字符+'\0'

我个人更推荐 fgets,因为它能指定最大读取长度,避免缓冲区溢出。而且它会保留换行符,这一点和 gets 不同——gets 已经被C11标准废弃了,千万别再用。

好了,二维数组和字符串的内容就聊到这儿。这些东西在实际开发中几乎天天用,尤其是字符串处理,可以说是C语言编程的基本功。多写几遍,自然就熟了。

数组(二)知识体系 二维数组与字符串 二维数组 定义与初始化 双层循环遍历 内存布局(行优先) 字符数组与字符串 以'\0'结尾 初始化方式 fgets安全读取 字符串处理函数 strlen / strcpy strcat / strcmp strncpy(安全版) 核心:注意缓冲区大小,优先使用安全版本

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