字符串操作3

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

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 字节的版本核心用途
拼接strcatstrncat把源数据追加到目标字符串末尾
比较strcmpstrncmp按字节顺序比较两个字符串

函数名中的 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 的值比较:

  1. 当前两个字节相同且都不是 \0:继续比较下一对;
  2. 遇到第一对不同字节:结果的正负由这一对字节的大小关系决定;
  3. 同时遇到 \0:两个字符串完全相同,返回 0。

标准只保证返回值的符号:

返回值条件含义
ret < 0s1 按该字节比较规则排在 s2 前面
ret == 0两个字符串的全部字节都相同
ret > 0s1 按该字节比较规则排在 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 库的实现;可移植代码只应使用结果的正负和零:

s1s2视频输出可移植结论首个决定结果的位置
Hello,World1Hello,World2-1小于 0'1' 与 '2'
Hello,WorldHello,World0等于 0两边同时到达 \0
Hello,WorldHello,World2-50小于 0\0 与 '2'
Hello,World1Hello,World49大于 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() 当作所有安全场景的通用比较工具。

参考资料