字符串操作3
360.2.4.3 字符串操作 3
/* 引入 printf 函数的声明。 */
#include <stdio.h>
/* 引入 memset、strcpy、strncpy、strcat、strncat、strcmp 和 strncmp 的声明。 */
#include <string.h>
/* 保留前面课程建立的字符串长度测试函数;本节结束时 main 没有调用它。 */
void StringLength_Test()
{
/* 保存十个可见字符;该数组没有字符串终止符 '\0'。 */
unsigned char buf1[] = {'H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'};
/* 创建五字节数组,并先把全部元素初始化为零。 */
unsigned char buf3[5] = {0};
/* 用字符串字面量初始化数组,末尾会自动带有 '\0'。 */
unsigned char buf2[] = "HelloWorld";
/* 将 buf3 的五个字节全部改写为字符 '-'。 */
memset(buf3, '-', sizeof(buf3));
/* 输出两个数组所占的字节数;%ld 按视频源码保留。 */
printf("buf1 len:%ld, buf2 len:%ld\r\n", sizeof(buf1), sizeof(buf2));
/* 把没有终止符的 buf1 当作字符串输出,这行存在未定义行为。 */
printf("buf1 :%s\r\n", buf1);
/* 输出具有终止符的 buf2。 */
printf("buf2 :%s\r\n", buf2);
/* 从返回类型为 void 的测试函数返回。 */
return;
}
/* 保留前面课程建立的字符串复制测试函数;本节结束时 main 没有调用它。 */
void StringStrcpy_Test()
{
/* 保存源字符串;数组包含 11 个可见字符和末尾的 '\0'。 */
unsigned char buf1[] = "Hello,World";
/* 为 strcpy 的结果准备 128 字节目标缓冲区。 */
unsigned char buf2[128];
/* 为 strncpy 的结果准备另一块 128 字节目标缓冲区。 */
unsigned char buf3[128];
/* 把 buf2 的全部字节清零。 */
memset(buf2, 0, sizeof(buf2));
/* 把 buf3 的全部字节清零。 */
memset(buf3, 0, sizeof(buf3));
/* 将 buf1 连同末尾的 '\0' 一起复制到 buf2。 */
strcpy(buf2, buf1);
/* 输出源字符串 buf1。 */
printf("buf1 data:%s \r\n", buf1);
/* 输出 strcpy 复制得到的 buf2。 */
printf("buf2 data:%s \r\n", buf2);
/* 复制 buf1 的前 11 字节;这处长度已在本节开始前改成 sizeof(buf1) - 1。 */
strncpy(buf3, buf1, sizeof(buf1) - 1);
/* 输出 buf3;其终止符来自清零后未被覆盖的 buf3[11]。 */
printf("buf3 data:%s \r\n", buf3);
/* 从返回类型为 void 的测试函数返回。 */
return;
}
/* 定义本节新建的字符串拼接测试函数。 */
void StringStrcat_Test()
{
/* 保存准备追加到其他字符串末尾的源字符串 "Hello"。 */
unsigned char buf1[] = "Hello";
/* 创建容量为 128 字节的目标数组,并让它先保存字符串 "world"。 */
unsigned char buf2[128] = "world";
/* 创建第二个目标数组,并让它先保存字符串 "ncat"。 */
unsigned char buf3[128] = "ncat";
/* 把完整的 buf1 追加到 buf2 的字符串末尾。 */
strcat(buf2, buf1);
/* 输出 strcat 拼接后的 buf2。 */
printf("buf2:%s\r\n", buf2);
/* 最多取 buf1 的前两个非零字节追加到 buf3,并在结果末尾写入 '\0'。 */
strncat(buf3, buf1, 2);
/* 输出 strncat 拼接后的 buf3。 */
printf("buf3:%s\r\n", buf3);
/* 从返回类型为 void 的测试函数返回。 */
return;
}
/* 定义本节新建的字符串比较测试函数。 */
void StringStrcmp_Test()
{
/* 保存第一个待比较字符串,其末尾可见字符是 '1'。 */
unsigned char buf1[] = "Hello,World1";
/* 保存第二个待比较字符串,其末尾可见字符是 '2'。 */
unsigned char buf2[] = "Hello,World2";
/* 定义整型变量,用于保存字符串比较函数的返回值。 */
int ret;
/* 比较两个完整字符串,并把结果保存到 ret。 */
ret = strcmp(buf1, buf2);
/* 输出 strcmp 的比较结果。 */
printf("ret val:%d\r\n", ret);
/* 最多比较两个字符串开头的五个字节。 */
ret = strncmp(buf1, buf2, 5);
/* 输出 strncmp 的比较结果。 */
printf("strncmp ret:%d\r\n", ret);
/* 从返回类型为 void 的测试函数返回。 */
return;
}
/* 程序从 main 函数开始执行。 */
int main()
{
/* 输出这一组 GNU/Linux 字符串函数实验的标题。 */
printf("GNU String Function Test\r\n");
/* 旧的字符串长度测试在本节结束时已被注释,不会执行。 */
// StringLength_Test();
/* 旧的字符串复制测试在本节结束时已被注释,不会执行。 */
// StringStrcpy_Test();
/* 本节前半段的字符串拼接测试最终也被注释,不会在末次运行中执行。 */
// StringStrcat_Test();
/* 最终只调用本节后半段建立的字符串比较测试函数。 */
StringStrcmp_Test();
/* 返回 0,表示程序正常结束。 */
return 0;
}
上面是本节视频结束时的完整 main.c。本节分成两个连续实验:先完成 StringStrcat_Test(),演示 strcat() 和 strncat();随后注释掉该调用,完成 StringStrcmp_Test(),演示 strcmp() 和 strncmp()。末次运行只有比较测试处于活动状态。
视频开头还显示,上一节的 strncpy(buf3, buf1, sizeof(buf1) - 6) 已在课间改成 sizeof(buf1) - 1。这次修改过程不属于本节讲解,但它确实是 360 视频中的源文件状态,所以上方如实保留。课程配套源码则是后续课程完成后的汇总版,其中还有 _GNU_SOURCE 和字符查找函数;这些内容在 360 尚未出现,因此没有提前加入。
一、字符串操作
本节涉及四个声明在 <string.h> 中的标准 C 库函数:
| 操作 | 完整字符串版本 | 最多处理 n 字节的版本 | 核心用途 |
|---|---|---|---|
| 拼接 | strcat | strncat | 把源数据追加到目标字符串末尾 |
| 比较 | strcmp | strncmp | 按字节顺序比较两个字符串 |
函数名中的 str 是 string,cat 是 concatenate(连接),cmp 是 compare(比较)。这里的 n 表示最多处理的字节数,并不自动等于目标缓冲区总容量,也不一定等于用户看到的字符数。
1.1 strcat函数
strcat() 和 strncat() 的经典函数原型为:
char *strcat(char *restrict dest, const char *restrict src);
char *strncat(char *restrict dest, const char *restrict src, size_t n);
strcat(dest, src) 会先在 dest 中寻找原有字符串末尾的 \0,用 src 的首字节覆盖这个终止符,然后继续复制 src 的其余字节以及 src 末尾的 \0。因此结果仍然是一个以空字符结尾的字符串:
调用前 dest: w o r l d \0 ...
调用前 src : H e l l o \0
调用后 dest: w o r l d H e l l o \0 ...
原终止符位置被 H 覆盖 ──┘
要让调用有定义,至少必须同时满足:
dest在调用前就已经是有效的、以\0结尾的字符串;src是有效的、以\0结尾的字符串;- 目标数组可写,而且容量至少为
strlen(dest) + strlen(src) + 1; - 源字符串和目标字符串所占区域不能重叠。
strcat() 不接收目标容量,也不会自动扩展数组。空间不足或目标中根本找不到 \0 都会产生未定义行为,而不是可靠地返回错误。
strncat(dest, src, n) 最多从 src 取前 n 个非零字节追加到 dest,随后还会再写一个终止符 \0。因此它和上一节的 strncpy() 有一个非常重要的差别:
| 函数 | n 的含义 | 当前 n 个源字节内没有 \0 时 |
|---|---|---|
strncpy(dest, src, n) | 向目标写入 n 字节 | 只写这 n 字节,不自动补 \0 |
strncat(dest, src, n) | 最多追加 n 个非零源字节 | 追加这 n 字节后,再额外写一个 \0 |
strncat() 同样要求 dest 在调用前已经是有效字符串,并要求源、目标区域不重叠。如果 src 的前 n 字节内出现 \0,复制会在这里停止;如果前 n 字节都非零,则只要这 n 字节全部可读,src 不必在这段范围内另有终止符。无论哪一种情况,目标都要为函数最后补写的 \0 预留空间。
所以,strncat() 的第三个参数不是“目标缓冲区大小”,也不能因为名字中多了 n 就叫作绝对安全版本。目标至少需要:
strlen(dest) + strnlen(src, n) + 1
字节容量,其中最后的 1 用来存放新终止符。若已知目标数组总容量为 capacity,通常应先根据当前 strlen(dest) 计算还剩多少可写空间;直接把 capacity 原样作为 n 传入,反而可能越界。
strcat() 和 strncat() 都返回原来的 dest 指针。视频中返回 void 的是课程自己编写的包装测试函数 StringStrcat_Test(),不能把它误认为库函数的返回类型。
1.1.1 示例
视频中的第一组数据是:
unsigned char buf1[] = "Hello";
unsigned char buf2[128] = "world";
strcat(buf2, buf1);
buf2 的数组容量为 128 字节,但初始字符串只占 world\0 六字节。buf1 的 Hello\0 会从原终止符的位置开始写入,因此结果是 worldHello,中间没有自动加入空格:
原 buf2:w o r l d \0
buf1: H e l l o \0
结果: w o r l d H e l l o \0
第一次运行拼接测试时,视频输出:
GNU String Function Test
buf2:worldHello
原笔记中的 “word hello” 同时存在拼写和内容错误:源码写的是小写 world,实际结果是无空格的 worldHello。
接着,视频加入:
unsigned char buf3[128] = "ncat";
strncat(buf3, buf1, 2);
这里最多追加 buf1 的前两个字节,即大写 H 和小写 e。strncat() 再在其后写入 \0,所以 buf3 变成 ncatHe,完整输出为:
GNU String Function Test
buf2:worldHello
buf3:ncatHe
注意,结果不是“把 he 复制进空缓存区”,而是把 He 追加到原有字符串 ncat 末尾;大小写和目标原有内容都属于结果的一部分。两个目标数组均有 128 字节,本例所需容量分别是 11 字节和 7 字节,因而空间充足。
1.2 strcmp函数
strcmp() 和 strncmp() 的经典函数原型为:
int strcmp(const char *s1, const char *s2);
int strncmp(const char *s1, const char *s2, size_t n);
二者都是 ISO C 标准库函数;strncmp() 不是 Linux 自行扩展出来的接口,也不应仅凭名称称作“安全字符串比较函数”。它只是给比较过程增加“至多 n 字节”的上限。
strcmp() 要求 s1、s2 都是在各自有效可读区域内以 \0 结尾的字符串。strncmp() 若在前 n 字节内遇到 \0,也按字符串结尾处理;若两边的前 n 字节内都没有 \0,则只需保证两边至少各有 n 个可读字节。长度上限不会让空指针、越界地址或不足 n 字节的对象自动变安全。
strcmp() 从两个字符串的首字节开始,依次按 unsigned char 的值比较:
- 当前两个字节相同且都不是
\0:继续比较下一对; - 遇到第一对不同字节:结果的正负由这一对字节的大小关系决定;
- 同时遇到
\0:两个字符串完全相同,返回0。
标准只保证返回值的符号:
| 返回值条件 | 含义 |
|---|---|
ret < 0 | s1 按该字节比较规则排在 s2 前面 |
ret == 0 | 两个字符串的全部字节都相同 |
ret > 0 | s1 按该字节比较规则排在 s2 后面 |
不能依赖不同字符串一定返回 -1 或 1,也不能把某次观察到的 -50、49 当成所有 C 库都必须采用的精确值。判断字符串相等应写 strcmp(s1, s2) == 0,而不是判断是否等于某个非零常数。
如果一个字符串是另一个字符串的完整前缀,比较过程会用较短字符串的 \0 与较长字符串的下一个可见字节比较。例如 "Hello" 与 "Hello2" 在第六对字节处比较的是 \0 和 '2',所以前者小于后者;这不是先单独计算两个字符串长度再比较。
strncmp(s1, s2, n) 使用相同规则,但至多检查开头 n 个字节;若在到达 n 之前已经遇到差异或字符串结尾,就会提前得出结果。它比较的是前缀,不会证明两个完整字符串相同。对于 UTF-8 文本,n 仍然是字节数,截在多字节字符中间时也不会按人眼所见的字符边界处理。
课程源码继续使用 unsigned char[] 保存普通文本,但上述函数的形参类型是 const char *,所以严格来说存在不兼容指针类型问题;启用编译器警告后通常会得到诊断。顶部代码遵循视频原样保留,新编写普通 C 字符串时应优先使用 char[]。
1.2.1 示例
视频先后修改字符串并进行了四组 strcmp() 实验。画面中的精确数值来自当时所用 C 库的实现;可移植代码只应使用结果的正负和零:
s1 | s2 | 视频输出 | 可移植结论 | 首个决定结果的位置 |
|---|---|---|---|---|
Hello,World1 | Hello,World2 | -1 | 小于 0 | '1' 与 '2' |
Hello,World | Hello,World | 0 | 等于 0 | 两边同时到达 \0 |
Hello,World | Hello,World2 | -50 | 小于 0 | \0 与 '2' |
Hello,World1 | Hello,World | 49 | 大于 0 | '1' 与 \0 |
在视频使用的 ASCII 兼容环境和 C 库实现中,后三个非零数值可以理解为首对不同字节的差值,例如 0 - 50 = -50、49 - 0 = 49。但这是对本次观察结果的解释,不是要求所有实现返回该差值的跨平台保证。
最终源码恢复为:
unsigned char buf1[] = "Hello,World1";
unsigned char buf2[] = "Hello,World2";
完整比较在最后一个可见字节处发现 '1' 与 '2' 不同,所以视频中的 strcmp() 结果为负数;随后只比较开头五字节:
buf1 前五字节:H e l l o
buf2 前五字节:H e l l o
这五字节完全相同,因此最终运行输出:
GNU String Function Test
ret val:-1
strncmp ret:0
这里的 strncmp(..., 5) == 0 只说明前五字节相同,不能说明完整用户名、命令或协议字段相同。若业务要求完整相等,应使用 strcmp(),或在使用前缀比较时同时核对预期长度。对用户输入还要注意 fgets() 可能保留换行符;字符串看起来相同但一个末尾多了 \n 时,strcmp() 会正确判定为不同。
strcmp() 是区分大小写的字节序比较,也不负责自然语言的排序规则;它适合判断程序标识符、固定命令和普通字符串是否完全一致。密码、令牌等秘密数据还可能要求恒定时间比较,以免泄露比较到哪一字节才不同的信息,不能直接把普通 strcmp() 当作所有安全场景的通用比较工具。