这一章在干嘛?
把 string.h 和 ctype.h 的常用函数按「不受限制 / 长度受限 / 查找 / 内存操作」四类盘一遍。函数签名是标准接口(要背),行为细节和陷阱(更要懂)。
9.3 查找与标记:strchr / strstr / strtok
9.4 字符分类与内存操作:ctype.h 与 mem 家族
9.1 字符串基础与长度
字符串字面量 "hello" 存在静态区,类型是 char[6]——5 个字符 + 隐式结尾 ‘\0’。一切 str 函数都靠找 ‘\0’ 判断结束,所以 ‘\0’ 丢了就一路读到天边:
size_t my_strlen(const char *s) /* strlen 的典型实现思路 */
{
const char *start = s;
while (*s != '\0')
s++;
return (size_t)(s - start); /* 指针相减 = 元素个数 */
}
/* 返回类型 size_t:无符号!strlen(a) - strlen(b) 结果永不为负 */
size_t 无符号陷阱:
if (strlen(a) - strlen(b) > 0) 在 a 更短时并不为假——无符号下减出巨大正数。比较长度请用 strlen(a) > strlen(b)。
9.2 复制、连接与比较:str 家族主干
| 函数 | 签名要点 | 行为 / 陷阱 |
|---|---|---|
| strcpy(dst, src) | 返回 dst | 连 ‘\0’ 一起拷;不检查 dst 空间,src 过长即溢出 |
| strncpy(dst, src, n) | 最多拷 n 字节 | src 短则补 0 凑满 n;src 长则不写 ‘\0’——必须手工补 |
| strcat(dst, src) | 返回 dst | 把 src 接到 dst 尾部;dst 空间不足即溢出 |
| strncat(dst, src, n) | 最多追加 n 字符 | 好消息:它总会补 ‘\0’,比 strncpy 规矩 |
| strcmp(a, b) | 按字典序逐字符比 | 返回 <0 / 0 / >0;判断相等必须 == 0,strcmp 返回的不是布尔 |
| strncmp(a, b, n) | 最多比 n 字符 | 前缀比较用 |
char buf[16];
strncpy(buf, src, sizeof buf - 1); /* 留 1 字节给 '\0' */
buf[sizeof buf - 1] = '\0'; /* 手工保险:strncpy 可能不补 */
if (strcmp(cmd, "quit") == 0) /* 正确的相等判断 */
quit();
9.3 查找与标记:strchr / strstr / strtok
| 函数 | 找什么 | 返回 |
|---|---|---|
| strchr(s, c) | 单个字符(含 ‘\0’) | 首次出现处的指针,找不到 NULL |
| strrchr(s, c) | 单个字符,从右找 | 最后一次出现处 |
| strstr(s, sub) | 子串 | 首次出现处,找不到 NULL |
| strpbrk(s, set) | set 中任意一个字符 | 首个命中处 |
| strspn / strcspn(s, set) | 前缀连续匹配/不匹配 set | 长度(size_t) |
| strtok(s, delim) | 按分隔符切 token | 逐次调用返回各 token,完了 NULL |
/* strtok 的状态机用法:第一次传串,之后传 NULL */
char line[] = "a,b,,c";
for (char *t = strtok(line, ","); t; t = strtok(NULL, ","))
printf("[%s]\n", t); /* [a] [b] [c] —— 空字段被跳过 */
strtok 会修改原串
(把分隔符改写为 ‘\0’)且有静态内部状态——不可重入、不能用于多线程。要保留原串先拷贝一份,线程场景用可重入版本(strtok_r)。
9.4 字符分类与内存操作:ctype.h 与 mem 家族
ctype.h:isalpha / isdigit / isspace / isupper / islower / iscntrl…,转换 tolower / toupper——非字母原样返回,参数必须是 unsigned char 值或 EOF(传负的 char 值是未定义行为)。
mem 家族:不认 ‘\0’、按字节数干活,因此能处理任意二进制数据:
memcpy(dst, src, n); /* 快速拷贝 n 字节——两块内存不得重叠! */
memmove(dst, src, n); /* 允许重叠:先当 src 整体读出再写,慢一点但安全 */
memset(buf, 0, n); /* 清零 n 字节:结构体/缓冲区初始化标配 */
memcmp(a, b, n); /* 按字节比较,返回 <0/0/>0 */
选择口诀:
处理字符串(遇 ‘\0’ 止)用 str 家族;处理「指定长度的一坨字节」(图片、结构体、帧缓冲)用 mem 家族。重叠内存一律 memmove。
1. strncpy 的两个坑是什么?
① 源串不足 n 时用 \0 补满剩余空间,效率浪费;② 源串达到或超过 n 时目标串没有结尾 \0,后续 strlen/printf 会越界读。惯用法:strncpy 后手工 buf[n-1]=‘\0’,或用 strncat(空串) 技巧。
2. strcmp 怎么判断两串相等?
strcmp(a,b) 返回负/零/正表示字典序先后,相等返回 0。必须写 strcmp(a,b)==0;直接把返回值当布尔用(如 if (strcmp(a,b)))会在「相等」时判假、不等时判真,方向完全反了。
3. memcpy 和 memmove 的区别?为什么 mem 家族适合二进制数据?
memcpy 不允许源/目标重叠(重叠时未定义),memmove 内部按重叠方向调整拷贝顺序、允许重叠。mem 家族按显式字节数工作、不依赖 \0 结尾,因此能安全处理任意二进制数据(字符串 str 家族遇到 \0 提前停)。