<string.h> 中的每个函数都是在 char 数组上跑的一个小循环,遇到空结束符就停止。明白了这一点,这个库就不再是一串晦涩的名字,而是几个你自己也会写的循环。本页介绍承担了几乎所有实际工作的那几个函数,以及防止它们写过缓冲区末尾的大小规则。
这里的所有内容只需要一个 include:
#include <string.h>
strlen:有多少个字符
strlen 计算结束符之前的字节数,不包括结束符本身。它返回 size_t,所以用 %zu 打印。
strlen 是 6,sizeof 是 32。前者是文本,后者是存储空间。为什么这个区别如此重要,请看字符串一页。
strlen 每次调用都要遍历整个字符串,所以对于同一个不变的字符串,不要把它放在循环条件里:
/* re-counts the whole string on every iteration */
for (size_t i = 0; i < strlen(s); i++) { ... }
/* count once */
size_t n = strlen(s);
for (size_t i = 0; i < n; i++) { ... }
strcpy 和 strncpy:复制
strcpy(dst, src) 复制所有字符以及结束符。它不知道 dst 有多大,所以调用者必须保证 dst 至少能容纳 strlen(src) + 1 个字节。
strncpy 接收一个最大数量,但它有一个出了名的陷阱:当源字符串长度至少为 n 字节时,它恰好复制 n 个字符,并且不复制结束符。一定要自己补上结束符:
sizeof dst - 1 加上显式的结束符,是需要记住的写法。(strncpy 还会把较短的源字符串用零填充到 n 字节,对大缓冲区来说是白费功夫。它是为定宽记录设计的,不是为了安全。)
strcat 和 strncat:拼接
strcat(dst, src) 把 src 追加到 dst 现有内容的末尾。目标必须能容纳两个字符串加一个结束符,而且其中必须已经是一个有效的字符串。往未初始化的内存上追加是未定义行为。
注意这里写的是 char path[64] = "/home/ada"; 而不是 char path[64];,正是这个初始化让它在第一次 strcat 时就是一个有效的字符串。
strncat(dst, src, n) 最多追加 n 个字符,并且总会加上结束符,所以 n 是剩余的空间,而不是缓冲区的总大小:
strncat(dst, src, sizeof dst - strlen(dst) - 1);
每次调用 strcat 都要重新扫描目标字符串来找到末尾。用这种方式在循环中拼出一个长字符串,耗时是平方级的。数据量较大时,请自己记录写入位置,或者使用 snprintf,字符串转换一页中有介绍。
strcmp:比较
== 比较的是地址,所以对两个存着相同文本的不同数组,结果是假。strcmp 比较字符,并返回差值的符号。
三条规则:
- 用
strcmp(a, b) == 0判断相等。写成if (strcmp(a, b))的意思是“如果它们不同”,几乎所有人读起来都会觉得是反的。 - 只有正负号是有定义的。不要把结果和
1或-1比较。 - 排序依据是字节值,所以在 ASCII 中
"Zebra"排在"apple"前面。要不区分大小写地比较,先把两个副本都转成小写。常见的strcasecmp是 POSIX 扩展,不是标准 C。
strncmp(a, b, n) 只比较前 n 个字符,这是判断前缀的简洁写法:
strchr 和 strstr:查找
strchr(s, ch) 查找某个字符第一次出现的位置,strrchr 查找最后一次。strstr(haystack, needle) 查找子字符串。三者都返回指向原字符串内部的指针,没有匹配时返回 NULL。
用返回的指针减去起始地址就得到下标,它的类型是 ptrdiff_t,用 %td 打印。由于结果指向原数组内部,at + 1 就是字符串的剩余部分,完全不需要复制。这是非常常见的 C 写法。
解引用之前一定要检查 NULL。对一个不含 @ 的字符串执行 strchr(email, '@') + 1,会基于 NULL 计算地址,程序的行为就不再有定义了。
分割字符串
C 没有分割函数,但 strchr 加上结束符就能原地完成。下面的例子把一个邮箱地址拆成用户名和域名:
memcpy 按精确的字节数复制,不关心结束符,在你已经知道长度时正好合适。然后再手动写入结束符。
完整示例:规范化姓名
把上面讲的内容组合起来:去除空格、比较、复制和拼接:
每次写入都以目标自身的 sizeof 为界,每次 strncpy 之后都跟着一个显式的结束符。始终如一地坚持这套做法,C 字符串代码才会安全。
要牢记的大小规则
strcpy需要目标中有strlen(src) + 1个字节的空闲空间。strcat需要strlen(dst) + strlen(src) + 1。strncpy之后,自己写上dst[n - 1] = '\0'。- 对于
strncat,数量是剩余的空间:sizeof dst - strlen(dst) - 1。 - 使用
strchr和strstr的结果之前,先检查是否为NULL。 - 只有当
dst是真正的数组时才用sizeof dst。在接收char *dst的函数内部,sizeof得到的是指针的大小,应该把缓冲区长度作为单独的参数传入。
常见问题
C 语言中 strcmp 返回什么?
两个字符串相同时返回零,第一个排在第二个之前时返回负值,排在之后时返回正值。只有正负号有意义,不要假设它返回 -1 或 1。你需要的判断几乎总是 if (strcmp(a, b) == 0)。
C 语言中为什么不能用 == 比较字符串?
a == b 比较的是两个地址,而不是字符。两个都存着 "cat" 的独立数组位于不同的地址,所以即使文本相同,比较结果也是假。应该用 <string.h> 中的 strcmp(a, b) == 0。
strcpy 和 strncpy 有什么区别?
strcpy 一直复制到源字符串的 '\0',完全不知道目标有多大。strncpy 最多复制 n 个字节就停止,但如果源字符串有那么长,它不会复制结束符,所以你必须自己写 dst[n - 1] = '\0';。两者都不会自动保证安全,控制大小是你的责任。
C 语言中如何查找子字符串?
strstr(haystack, needle) 返回指向第一次出现位置的指针,没有找到时返回 NULL。因为它返回的是指向原字符串内部的指针,found - haystack 就是下标。如果只查找单个字符,用 strchr。