一、字符数组与字符串:从底层到实战
大家好,我是你们的嵌入式软件工程讲师。今天我们来聊聊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. 字符串的输入输出
字符串的输入输出,常用的有三种方式:gets、puts 和 scanf。我一个个来说。
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> 里。我挑四个最核心的来讲:strlen、strcpy、strcat、strcmp。
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. 知识体系总览
下面这张图,是我自己整理的知识结构。你可以把它当成一个快速索引,随时回来查阅。
6. 实战建议
最后,我根据自己多年的嵌入式开发经验,给你几条实用建议:
- 定义字符数组时,多预留一个字节给 '\0'。比如你要存10个字符,就定义
char buf[11]; - 尽量使用带长度限制的函数,比如
strncpy()、strncat()、snprintf(),而不是它们的不安全版本。 - 每次操作字符串后,手动检查一下结尾是否有 '\0'。尤其是在嵌入式环境里,内存被意外修改的情况并不少见。
- 不要用 == 比较字符串。这个错误我见过太多次了,包括我自己早期也犯过。
嗯,关于字符数组和字符串的基础知识,今天就讲到这里。这些内容虽然基础,但它们是后续所有字符串操作的地基。地基打不牢,后面盖多高的楼都会塌。
一个小练习:试着写一个程序,用 fgets() 读取用户输入的一行字符串,然后用 strlen() 计算它的长度,最后用 puts() 输出。注意观察 fgets() 会保留换行符这个特点。
公众号:蓝海资料掘金营,微信 deep3321