一、字符数组与字符串:从底层到实战

大家好,我是你们的嵌入式软件工程讲师。今天我们来聊聊C语言里一个特别基础、但又特别容易踩坑的话题——字符数组与字符串。

说实话,我刚开始学C语言那会儿,总觉得字符数组和字符串就是一回事。直到我在一个嵌入式项目里,因为字符串结尾符的问题,让整个设备死机了三次……嗯,从那以后,我再也不敢小看这两个概念了。

1. 字符数组的定义

字符数组,说白了就是存放字符的数组。它的定义方式和普通数组完全一样,只是元素类型是 char

char str1[10];          // 定义一个能存10个字符的数组
char str2[] = {'H', 'e', 'l', 'l', 'o'};  // 初始化,长度自动为5
char str3[6] = {'H', 'e', 'l', 'l', 'o'}; // 手动指定长度,剩余元素自动填0

这里有个细节我想强调一下:字符数组的每个元素都是独立的字符。你完全可以把它当成普通数组来用,比如 str1[0] = 'A'; 这样赋值。

核心要点:字符数组只是数组的一种,它不自动包含字符串结尾符 '\0'。这一点非常重要,我后面会反复提到。

2. 字符串的概念

字符串是什么?在C语言里,字符串其实就是一个以 '\0'(空字符)结尾的字符数组。这个 '\0' 就是字符串的结束标志。

你想想看,如果没有这个结尾符,计算机怎么知道字符串到哪儿结束?它只能一直往后读内存,直到遇到一个随机的0值——这就会导致各种奇怪的问题。

char str1[] = "Hello";      // 实际占用6个字节:H e l l o \0
char str2[] = {'H','e','l','l','o','\0'};  // 和上面完全等价

我个人习惯用双引号来定义字符串,因为编译器会自动帮你加上 '\0'。用花括号初始化时,你必须手动加 '\0',否则它就是个普通字符数组,不是字符串。

我曾经踩过的坑:在一个串口通信项目中,我定义了一个字符数组用来接收数据,忘了预留 '\0' 的位置。结果每次打印接收到的数据时,末尾总会多出一堆乱码。排查了整整一下午,才发现是结尾符的问题。

3. 字符串的输入输出

字符串的输入输出,常用的有三种方式:getsputsscanf。我一个个来说。

3.1 gets() 和 puts()

gets() 用于从键盘读取一行字符串,puts() 用于输出字符串并自动换行。

char name[50];
printf("请输入你的名字:");
gets(name);      // 读取一行,包括空格
puts(name);      // 输出:你好,[name]

但是!我强烈建议你不要在生产代码中使用 gets()。为什么?因为它不检查缓冲区长度。如果你输入的内容超过了数组大小,就会发生缓冲区溢出——这是安全漏洞的常见来源。

我的建议:在嵌入式开发中,我一般用 fgets() 替代 gets(),因为 fgets() 可以指定最大读取长度,更安全。

3.2 scanf() 读取字符串

scanf()%s 格式符读取字符串,但它有个特点:遇到空格、制表符或换行符就会停止读取。

char str[20];
scanf("%s", str);   // 输入 "Hello World",只读到 "Hello"

所以,如果你要读取带空格的字符串,scanf 就不太合适了。这时候用 fgets() 或者自己写个循环读取更靠谱。

函数特点安全性适用场景
gets()读取整行,包括空格不安全(无长度限制)不推荐使用
fgets()读取指定长度,包括空格安全推荐用于输入
scanf("%s")遇到空白字符停止相对安全(需指定宽度)简单单词输入
puts()输出字符串并换行安全简单输出

4. 字符串处理函数

C语言标准库提供了几个非常常用的字符串处理函数,都在 <string.h> 里。我挑四个最核心的来讲:strlenstrcpystrcatstrcmp

4.1 strlen() — 求字符串长度

strlen() 返回字符串的长度,不包括结尾的 '\0'

char str[] = "Hello";
int len = strlen(str);   // len = 5,不是6

我记得有一次,一个同事用 sizeof(str) 代替 strlen(str),结果在动态分配内存时多分配了一个字节。虽然没出大问题,但这种细节还是要注意。

区别:sizeof 是运算符,计算的是数组占用的总字节数(包括 '\0');strlen 是函数,计算的是字符串的实际长度(不包括 '\0')。

4.2 strcpy() — 字符串拷贝

strcpy() 把源字符串拷贝到目标数组,包括结尾的 '\0'。

char src[] = "World";
char dst[20];
strcpy(dst, src);   // dst 的内容变成 "World"

我曾经犯过的错:目标数组太小,导致拷贝时越界。比如目标数组只有5个字节,源字符串有10个字节,strcpy 会毫不犹豫地写过去,覆盖掉后面的内存。这种 bug 极难排查,因为它可能不会立即崩溃,而是在某个随机时刻引发问题。

安全替代方案:用 strncpy(),可以指定最大拷贝长度。

4.3 strcat() — 字符串拼接

strcat() 把源字符串追加到目标字符串的末尾。

char str1[20] = "Hello ";
char str2[] = "World";
strcat(str1, str2);   // str1 变成 "Hello World"

这里有个前提:目标数组必须有足够的剩余空间。否则,后果和 strcpy 越界一样严重。

4.4 strcmp() — 字符串比较

strcmp() 比较两个字符串是否相等。注意,它不能直接用 == 比较,因为 == 比较的是地址,而不是内容。

char str1[] = "Hello";
char str2[] = "Hello";
if (strcmp(str1, str2) == 0) {
    printf("两个字符串相等\n");
}

返回值规则:

  • 相等返回 0
  • str1 < str2 返回负数
  • str1 > str2 返回正数

比较的是字符的 ASCII 码值,按字典序进行。

5. 知识体系总览

下面这张图,是我自己整理的知识结构。你可以把它当成一个快速索引,随时回来查阅。

字符数组与字符串 字符数组定义 • char arr[10]; • 元素类型为 char • 不自动包含 '\0' 字符串概念 • 以 '\0' 结尾的字符数组 • 双引号自动加 '\0' • 花括号需手动加 '\0' 输入输出 • gets() / puts() • scanf("%s") • fgets() 更安全 字符串处理函数 • strlen() — 求长度 • strcpy() — 拷贝 • strcat() — 拼接 • strcmp() — 比较 ⚠️ 注意事项 • 缓冲区溢出风险 • 目标数组必须足够大 • 优先用安全版本函数 • 区分 sizeof 和 strlen

6. 实战建议

最后,我根据自己多年的嵌入式开发经验,给你几条实用建议:

  • 定义字符数组时,多预留一个字节给 '\0'。比如你要存10个字符,就定义 char buf[11];
  • 尽量使用带长度限制的函数,比如 strncpy()strncat()snprintf(),而不是它们的不安全版本。
  • 每次操作字符串后,手动检查一下结尾是否有 '\0'。尤其是在嵌入式环境里,内存被意外修改的情况并不少见。
  • 不要用 == 比较字符串。这个错误我见过太多次了,包括我自己早期也犯过。

嗯,关于字符数组和字符串的基础知识,今天就讲到这里。这些内容虽然基础,但它们是后续所有字符串操作的地基。地基打不牢,后面盖多高的楼都会塌。

一个小练习:试着写一个程序,用 fgets() 读取用户输入的一行字符串,然后用 strlen() 计算它的长度,最后用 puts() 输出。注意观察 fgets() 会保留换行符这个特点。


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