第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 字符串的输入输出

输入输出这块,常用的函数有 printfscanfgetsputs 等。但说实话,scanfgets 都是“危险分子”。

1.2.1 标准输入输出

char name[20];
printf("请输入名字:");
scanf("%s", name);  // 注意:不用加 &,因为数组名就是地址
printf("你好,%s\n", name);

这里有个问题:scanf 遇到空格就停止读取。如果你输入 "Zhang San",它只会读到 "Zhang"。而且如果输入超过19个字符,缓冲区溢出就来了——这是很多安全漏洞的根源。

💡 我的建议:fgets 代替 scanfgets。它指定了最大读取长度,安全得多。

char buffer[100];
fgets(buffer, sizeof(buffer), stdin);  // 安全!最多读99个字符 + '\0'

1.2.2 输出函数

putsprintf 简单,自动加换行:

puts("Hello World");  // 等价于 printf("Hello World\n");

puts 只能输出字符串,不能格式化。我一般调试时用 puts,正式输出用 printf

1.3 字符串处理函数(核心四件套)

这部分是字符串操作的重头戏。C标准库提供了 string.h 头文件,里面有四个最常用的函数:strlenstrcpystrcatstrcmp

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"

但这里有个致命问题:不检查目标缓冲区大小。如果源字符串比目标缓冲区大,直接溢出。我曾经在一个网络协议栈里看到过这样的代码,结果就是黑客可以发送超长字符串来覆盖栈上的返回地址——典型的缓冲区溢出攻击。

💡 安全替代方案:strncpysnprintf。比如:

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 字符串查找函数

查找函数在实际项目中用得非常多。比如解析配置文件、处理网络协议等。常用的有 strchrstrrchrstrstr

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(非标准,但很多平台支持)。

知识体系总览

下面这张图把本章的核心知识点串起来了。你想想看,字符串操作其实就围绕“存储、输入输出、处理、查找”这四个维度展开。

字符串与字符串函数 存储方式 字符数组 栈/全局区,可修改 字符指针 常量区,只读 输入输出 scanf / printf 不安全,易溢出 fgets / puts 安全,推荐使用 处理函数 strlen:计算长度 strcpy:拷贝 strcat:拼接 strcmp:比较 查找函数 strchr / strrchr 查找字符 strstr 查找子串 核心原则 1. 始终确保字符串以 '\0' 结尾 2. 使用安全函数(fgets/strncpy)避免缓冲区溢出

避坑指南

最后,我把自己这些年踩过的坑总结一下,希望能帮你少走弯路:

  • 忘记 '\0':声明字符数组时,大小一定要比字符串长度多1。比如存 "Hello" 需要6个字节。
  • == 比较字符串:永远用 strcmp,不要用 ==
  • 缓冲区溢出getsstrcpystrcat 都是高危函数。能用 fgetsstrncpysnprintf 就别用它们。
  • 修改字符串常量:字符指针指向的常量区内容不可修改。如果需要修改,用字符数组。
  • 返回值类型混淆strlen 返回 size_t,不要和 int 混用。

好了,字符串这块的内容就讲到这里。记住一句话:字符串操作,安全第一。下次写代码时,多想想缓冲区够不够大,有没有 '\0',用没用安全函数。养成习惯后,你会发现很多bug其实都可以避免。


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