第1章 字符串与字符数组:从底层到实战
大家好,我是你们的老朋友。今天咱们来聊聊C语言里一个绕不开的话题——字符串与字符数组。说实话,我见过太多新手甚至工作两三年的工程师,在这上面栽跟头。我自己刚入行时,也因为在字符串拷贝时没注意'\0',导致一个通信协议栈解析出了乱码,排查了整整两天。嗯,从那以后,我对字符串的处理就格外小心了。
1.1 字符数组与字符串:看似一样,实则不同
先问大家一个问题:字符数组和字符串,到底是不是一回事?
说白了,字符数组就是一块连续的内存,每个元素存一个字符。而字符串,是字符数组的一种特殊形式——它必须以'\0'(空字符)结尾。这个'\0'就是字符串的结束标志。
核心区别:字符串 = 字符数组 + '\0' 结尾
举个例子:
char str1[] = {'H', 'e', 'l', 'l', 'o'}; // 字符数组,长度5,不是字符串
char str2[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 字符串,长度6
char str3[] = "Hello"; // 字符串,编译器自动加'\0'
我在项目中遇到过这样一个坑:有人用strlen()去计算str1的长度,结果返回了一个随机数。为什么?因为strlen会一直往后找'\0',直到遇到内存中的某个'\0'才停下。这要是运气不好,可能把整个栈内存都遍历一遍,甚至导致程序崩溃。
警告:没有'\0'结尾的字符数组,绝对不能当作字符串使用!
1.2 字符串处理函数:C标准库的四大金刚
C语言标准库提供了四个最常用的字符串处理函数:strlen、strcpy、strcmp、strcat。它们都定义在
1.2.1 strlen:计算字符串长度
strlen返回字符串中字符的个数,不包括结尾的'\0'。它的实现原理很简单:从起始地址开始,逐个字节判断是否为'\0',遇到'\0'就停止计数。
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello";
size_t len = strlen(str); // len = 5
printf("Length: %zu\n", len);
return 0;
}
注意,strlen返回的是size_t类型,在32位系统上是unsigned int,在64位系统上是unsigned long long。我曾经见过有人直接用int接收返回值,在字符串长度超过2GB时(虽然很少见),就会溢出变成负数。嗯,这种bug排查起来相当隐蔽。
1.2.2 strcpy:字符串拷贝
strcpy把源字符串(包括'\0')拷贝到目标地址。它的原型是:
char *strcpy(char *dest, const char *src);
这里有个关键点:目标缓冲区必须足够大,能容纳源字符串的全部内容加上'\0'。否则,就会发生缓冲区溢出——这是C语言最经典的安全漏洞之一。
避坑指南:我曾经在嵌入式设备上写固件升级程序时,用strcpy拷贝了一个版本号字符串,结果目标缓冲区只分配了16字节,源字符串却有20字节。程序运行正常,但升级完成后设备随机重启。查了三天才发现,是strcpy把栈上的返回地址给覆盖了。从那以后,我坚持用strncpy或者自己写带长度检查的拷贝函数。
1.2.3 strcmp:字符串比较
strcmp逐字节比较两个字符串的ASCII码值。返回值:
- 等于0:两个字符串相等
- 小于0:第一个字符串小于第二个
- 大于0:第一个字符串大于第二个
if (strcmp(input, "admin") == 0) {
printf("登录成功\n");
} else {
printf("用户名错误\n");
}
你想想看,如果直接用==比较两个字符串,那比较的是指针地址,而不是内容。这个错误我见过太多次了,尤其是在面试题里。
1.2.4 strcat:字符串拼接
strcat把源字符串追加到目标字符串的末尾。它会覆盖目标字符串的'\0',然后在新字符串末尾加上'\0'。
char path[64] = "/home/user/";
strcat(path, "documents"); // path = "/home/user/documents"
这里同样有缓冲区溢出的风险。我建议在使用strcat之前,先用strlen计算一下当前长度,确保剩余空间足够。
1.3 字符串输入输出:小心缓冲区
字符串的输入输出,常用的有printf、scanf、gets、fgets等。其中gets函数已经被C11标准废弃了,因为它不检查缓冲区大小,极其危险。
警告:永远不要使用gets()!永远!
推荐使用fgets:
char buffer[100];
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
// 处理输入
// 注意:fgets会保留换行符'\n'
buffer[strcspn(buffer, "\n")] = '\0'; // 去掉换行符
}
这段代码里,strcspn(buffer, "\n")找到换行符的位置,然后把它替换成'\0'。这是我个人比较喜欢的写法,简洁又安全。
1.4 知识体系总览
下面这张图,把本章的核心知识点串了起来。你可以把它当作一个快速索引,遇到问题时回来对照一下。
1.5 实战建议
说了这么多,最后给大家几条实战建议:
- 初始化要彻底:定义字符数组时,最好用
= {0}或memset全部清零,避免残留数据干扰。 - 边界检查不能省:每次调用
strcpy、strcat前,先确认目标缓冲区够大。 - 优先用安全版本:如果编译器支持C11,可以用
strcpy_s、strcat_s等带下标的版本。如果不支持,自己封装一个带长度检查的函数也不难。 - 调试时打印ASCII码:遇到字符串乱码,用
printf("%02x ", str[i])逐字节打印十六进制,比肉眼盯着字符靠谱得多。
好了,这一章的内容就到这里。字符串处理看似基础,但越是基础的东西,越容易出大问题。希望大家在实际编码中,能时刻绷紧「安全」这根弦。