第1章 字符串与字符串函数:C语言中最常用的“数据快递员”
大家好,我是你们的老朋友。今天咱们聊聊C语言里一个特别基础、但又特别容易踩坑的话题——字符串。说实话,我刚开始学C语言那会儿,觉得字符串不就是一堆字符嘛,有啥好讲的?结果后来做项目,被各种段错误、乱码折磨得够呛。嗯,今天咱们就把这块彻底讲透。
1.1 字符串的存储:字符数组 vs 字符指针
字符串在C语言里,本质上就是一个以 '\0'(空字符)结尾的字符序列。存储方式主要有两种:字符数组和字符指针。这两者看起来很像,但底层逻辑完全不同。
1.1.1 字符数组存储
字符数组,说白了就是在栈上或者全局区开辟一块连续的内存,把字符一个个放进去。比如:
char str1[] = "Hello"; // 自动在末尾加 '\0'
char str2[10] = "Hi"; // 指定大小,剩余空间自动填 '\0'
这里有个细节:str1 的大小是6个字节('H','e','l','l','o','\0'),不是5个。我见过不少新手在这里栽跟头——声明数组时忘了给 '\0' 留位置。
str1[0] = 'h'; 是合法的。但数组名本身是常量指针,不能重新赋值。
1.1.2 字符指针存储
字符指针指向的是字符串常量区的地址。比如:
char *p = "Hello"; // p 指向常量区中的 "Hello"
这里有个大坑:不能通过指针修改字符串内容。为什么?因为 "Hello" 存储在只读数据段,你试图 p[0] = 'h'; 的话,程序直接崩溃给你看。我曾经在一个嵌入式项目中,因为没注意这个区别,导致设备在运行几个小时后随机死机——查了两天才发现是这里的问题。
核心区别总结:
- 字符数组:可读写,存储在栈/全局区,大小固定
- 字符指针:指向常量区,只读,指针本身可以指向别处
1.2 字符串的输入输出
输入输出这块,常用的函数有 printf、scanf、gets、puts 等。但说实话,scanf 和 gets 都是“危险分子”。
1.2.1 标准输入输出
char name[20];
printf("请输入名字:");
scanf("%s", name); // 注意:不用加 &,因为数组名就是地址
printf("你好,%s\n", name);
这里有个问题:scanf 遇到空格就停止读取。如果你输入 "Zhang San",它只会读到 "Zhang"。而且如果输入超过19个字符,缓冲区溢出就来了——这是很多安全漏洞的根源。
💡 我的建议: 用 fgets 代替 scanf 和 gets。它指定了最大读取长度,安全得多。
char buffer[100];
fgets(buffer, sizeof(buffer), stdin); // 安全!最多读99个字符 + '\0'
1.2.2 输出函数
puts 比 printf 简单,自动加换行:
puts("Hello World"); // 等价于 printf("Hello World\n");
但 puts 只能输出字符串,不能格式化。我一般调试时用 puts,正式输出用 printf。
1.3 字符串处理函数(核心四件套)
这部分是字符串操作的重头戏。C标准库提供了 string.h 头文件,里面有四个最常用的函数:strlen、strcpy、strcat、strcmp。
1.3.1 strlen:计算字符串长度
strlen 返回的是字符个数,不包括 '\0'。比如:
char s[] = "Hello";
printf("%zu\n", strlen(s)); // 输出 5,不是 6
它的实现原理很简单:从起始地址开始遍历,直到遇到 '\0' 停止。所以如果字符串没有 '\0',它会一直跑下去,直到内存中碰巧遇到一个0。嗯,这就是典型的“未定义行为”。
strlen 的返回值类型是 size_t(无符号整数)。不要直接和 int 比较,否则可能出问题。比如 if (strlen(s) > -1) 永远为假,因为 -1 会被转换成很大的无符号数。
1.3.2 strcpy:字符串拷贝
strcpy 把源字符串复制到目标缓冲区,包括 '\0':
char dest[20];
strcpy(dest, "Hello"); // dest 变成 "Hello"
但这里有个致命问题:不检查目标缓冲区大小。如果源字符串比目标缓冲区大,直接溢出。我曾经在一个网络协议栈里看到过这样的代码,结果就是黑客可以发送超长字符串来覆盖栈上的返回地址——典型的缓冲区溢出攻击。
💡 安全替代方案: 用 strncpy 或 snprintf。比如:
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // 确保以 '\0' 结尾
1.3.3 strcat:字符串拼接
strcat 把源字符串追加到目标字符串末尾:
char str[20] = "Hello";
strcat(str, " World"); // str 变成 "Hello World"
同样的问题:不检查目标缓冲区剩余空间。如果 str 只有20字节,追加的内容超过剩余空间,照样溢出。我个人的习惯是:永远不要用 strcat,改用 snprintf 或者自己写一个带长度检查的版本。
1.3.4 strcmp:字符串比较
strcmp 比较两个字符串的字典序:
if (strcmp(s1, s2) == 0) {
// 相等
} else if (strcmp(s1, s2) < 0) {
// s1 小于 s2
} else {
// s1 大于 s2
}
注意:不要用 == 比较字符串。因为 s1 == s2 比较的是指针地址,不是内容。我见过有人写 if (s1 == "admin"),结果永远不成立——因为 s1 的地址和常量 "admin" 的地址不同。
1.4 字符串查找函数
查找函数在实际项目中用得非常多。比如解析配置文件、处理网络协议等。常用的有 strchr、strrchr、strstr。
1.4.1 strchr 和 strrchr:查找字符
strchr 从左往右找第一个匹配的字符,strrchr 从右往左找最后一个:
char *p = strchr("Hello World", 'o'); // 返回指向第一个 'o' 的指针
char *q = strrchr("Hello World", 'o'); // 返回指向最后一个 'o' 的指针
如果没找到,返回 NULL。所以使用前一定要判空:
if (p != NULL) {
printf("找到了,位置是:%ld\n", p - str);
}
1.4.2 strstr:查找子串
strstr 查找子串第一次出现的位置:
char *p = strstr("Hello World", "World");
if (p != NULL) {
printf("找到了子串,剩余部分:%s\n", p); // 输出 "World"
}
这个函数在解析协议时特别有用。比如解析HTTP请求头,可以用 strstr 查找 "Content-Type:" 等关键字。
实战经验: 我在做嵌入式Web服务器时,用 strstr 解析HTTP请求。但要注意,strstr 是大小写敏感的。如果需要忽略大小写,可以用 strcasestr(非标准,但很多平台支持)。
知识体系总览
下面这张图把本章的核心知识点串起来了。你想想看,字符串操作其实就围绕“存储、输入输出、处理、查找”这四个维度展开。
避坑指南
最后,我把自己这些年踩过的坑总结一下,希望能帮你少走弯路:
- 忘记
'\0':声明字符数组时,大小一定要比字符串长度多1。比如存 "Hello" 需要6个字节。 - 用
==比较字符串:永远用strcmp,不要用==。 - 缓冲区溢出:
gets、strcpy、strcat都是高危函数。能用fgets、strncpy、snprintf就别用它们。 - 修改字符串常量:字符指针指向的常量区内容不可修改。如果需要修改,用字符数组。
- 返回值类型混淆:
strlen返回size_t,不要和int混用。
好了,字符串这块的内容就讲到这里。记住一句话:字符串操作,安全第一。下次写代码时,多想想缓冲区够不够大,有没有 '\0',用没用安全函数。养成习惯后,你会发现很多bug其实都可以避免。
公众号:蓝海资料掘金营,微信deep3321