字符串操作4

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

361.2.4.4.字符串操作4

/* 引入 printf 函数的声明;这行位于 _GNU_SOURCE 之前,因而会让后面的特性宏定义得过晚。 */
#include <stdio.h>
/* 按视频最终源码保留该位置;正确位置应当是所有头文件之前。 */
#define _GNU_SOURCE

/* 引入字符串处理函数的声明;由于特性宏生效过晚,本次编译没有获得 strchrnul 的声明。 */
#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 字节;该长度沿用上一节结束时的源码。 */
    strncpy(buf3, buf1, sizeof(buf1) - 1);
    /* 输出 buf3;其终止符来自清零后未被覆盖的 buf3[11]。 */
    printf("buf3 data:%s \r\n", buf3);

    /* 从返回类型为 void 的测试函数返回。 */
    return;
}

/* 保留前面课程建立的字符串拼接测试函数;本节结束时 main 没有调用它。 */
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;
}

/* 保留前面课程建立的字符串比较测试函数;本节结束时 main 没有调用它。 */
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;
}

/* 定义本节新增的字符查找测试函数。 */
void StringStrchr_Test()
{
    /* 保存待查找字符串 "Hello",数组末尾另有一个 '\0'。 */
    unsigned char buf1[] = "Hello";

    /* 查找第一个 'l';unsigned char * 实参的不兼容类型按视频源码保留。 */
    char *ptr = strchr(buf1, 'l');
    /* 输出数组首地址;严格匹配 %p 时应把实参转换为 void *。 */
    printf("strchr buf1:%p \r\n", buf1);
    /* 输出 strchr 返回的地址。 */
    printf("strchr ptr:%p \r\n", ptr);

    /* 从字符串末尾方向查找最后一个 'l'。 */
    char *ptr2 = strrchr(buf1, 'l');
    /* 输出 strrchr 返回的地址。 */
    printf("strrchr ptr:%p \r\n", ptr2);

    /* 查找不存在的 '7';本次编译缺少 strchrnul 声明,这一赋值会收到关键警告。 */
    char *ptr3 = strchrnul(buf1, '7');
    /* 输出 ptr3;视频中的结果已经是被错误转换后的无效地址。 */
    printf("strchrnul ptr:%p \r\n", ptr3);

    /* 再用标准 strchr 查找不存在的 '7'。 */
    char *ptr4 = strchr(buf1, '7');
    /* 如果没有找到,则立即结束测试函数,避免继续使用空指针。 */
    if (ptr4 == NULL)
    {
        /* 从测试函数返回;因此当前数据下不会执行后面的 printf。 */
        return;
    }
    /* 仅当找到 '7' 时才输出它的地址。 */
    printf("strchr find '7' ptr:%p \r\n", ptr4);

    /* 查找成功并输出地址后,从测试函数返回。 */
    return;
}

/* 程序从 main 函数开始执行。 */
int main()
{
    /* 输出这一组 GNU/Linux 字符串函数实验的标题。 */
    printf("GNU String Function Test\r\n");
    /* 旧的字符串长度测试在本节结束时已被注释,不会执行。 */
    // StringLength_Test();
    /* 旧的字符串复制测试在本节结束时已被注释,不会执行。 */
    // StringStrcpy_Test();
    /* 旧的字符串拼接测试在本节结束时已被注释,不会执行。 */
    // StringStrcat_Test();
    /* 旧的字符串比较测试在本节结束时已被注释,不会执行。 */
    // StringStrcmp_Test();
    /* 最终只调用本节新增的字符查找测试函数。 */
    StringStrchr_Test();
    /* 返回 0,表示程序正常结束。 */
    return 0;
}

上面是本节视频结束时的完整 main.c:保留前面课程的四个测试函数,新建 StringStrchr_Test(),并将 main() 的活动调用切换到它。源码按照画面如实收录,因此也保留了 _GNU_SOURCE 位于 <stdio.h> 之后、字符串数组使用 unsigned char[]、%p 实参未显式转换成 void * 等问题;这些不是推荐写法,正文会逐一说明。

特别要区分“视频最后一次运行”和“视频最后的源码”。画面在加入 ptr4 == NULL 判断之前运行过程序,之后只完成了判断代码而没有再次编译运行。因此,最后一次屏幕输出中还能看到 strchr find '7' ptr:(nil);若执行顶部最终源码,查找失败后会直接 return,这一行不会再输出。

一、字符串操作

本节的三个函数都在以 \0 结尾的字节字符串中查找一个字符。strchr() 和 strrchr() 属于标准 C 库;strchrnul() 是 GNU 扩展,可移植性不同。

函数匹配成功时匹配失败时可移植性
strchr(s, c)指向第一个匹配字符NULLISO C、POSIX
strrchr(s, c)指向最后一个匹配字符NULLISO C、POSIX
strchrnul(s, c)指向第一个匹配字符指向字符串末尾的 \0GNU 扩展

三者返回的都是指向原字符串内部的指针,并不复制字符,也不返回“地址索引”。传入的 s 必须指向一个在有效可读区域内能遇到 \0 的字符串;否则查找会越界,行为未定义。它们查找的是字节值,不理解 UTF-8 字符边界;用于 ASCII 字符没有这个歧义。

1.1 例题: strchr函数演示

strchr() 的经典函数原型是:

char *strchr(const char *s, int c);

它从 s[0] 开始向后查找 c,找到后返回指向第一次出现位置的指针,找不到则返回 NULL。字符串终止符 \0 也被视为字符串的一部分,所以 strchr(s, '\0') 会返回指向终止符的指针。

视频使用 "Hello" 查找 'l',内存位置可表示为:

索引:  0   1   2   3   4   5
内容: 'H' 'e' 'l' 'l' 'o' '\0'
                ↑
           strchr 返回 buf1 + 2

因此 ptr 指向索引 2,也就是第一个 'l'。视频画面中某次运行的 buf1 地址末尾为 ...b2,ptr 地址末尾为 ...b4,两者相差两个字节,这与索引 2 一致。具体地址由每次运行时的内存布局决定,不能写死,也不应通过肉眼做十六进制减法来编写程序。

在 C 中,正确取得索引的方式是对同一数组中的两个指针做减法:

#include <stddef.h>

char buf[] = "Hello";
char *p = strchr(buf, 'l');
if (p != NULL) {
    ptrdiff_t index = p - buf;
    printf("index:%td\n", index);
}

指针相减只在两个指针都指向同一个数组对象(或尾后位置)时有定义,结果类型是 ptrdiff_t;对 char 数组来说,差值同时就是字节偏移和从零开始的元素索引。必须先判断 p != NULL,不能拿空指针参与减法。

视频最初误用 %d 输出指针,编译器因“格式要求 int,实际却是指针”而报警,随后改成了 %p。%p 是 C 的 printf() 格式,不是 Linux 独有语法;其对应实参严格来说应为 void *,推荐写成:

printf("%p\n", (void *)p);

%p 的具体显示形式由实现决定。在视频所用环境中,空指针显示为 (nil),但可移植程序不能依赖这一文本格式。

课程代码把普通字符串声明为 unsigned char[],而 strchr() 的参数是 const char *。这两种字符指针类型并不兼容,启用警告后通常会收到诊断。保存普通 C 字符串时宜使用 char[];如果处理的是任意二进制字节块,应改用带长度的 memchr(),而不是依赖 \0 的字符串函数。

1.2 strrchr函数介绍

strrchr() 的经典函数原型为:

char *strrchr(const char *s, int c);

它返回 c 在字符串中最后一次出现的位置;找不到时同样返回 NULL。虽然可以把效果理解为“从末尾找”,但接口语义只保证返回最后一个匹配位置,不应据此假定某种特定内部实现。

对于 "Hello" 中的 'l':

调用返回位置索引
strchr(buf1, 'l')第一个 'l'2
strrchr(buf1, 'l')第二个 'l'3

视频先加入了 ptr2,但第一次仍误把 ptr 传给 printf(),所以两个地址相同;随后将实参改为 ptr2,画面中的三个地址末尾依次为 ...a42、...a44、...a45,正确显示了首地址、索引 2 和索引 3。最后一个 'l' 到终止符 \0 的距离是两个字节;第一个 'l' 到终止符的距离则是三个字节,必须说明比较基准,不能笼统写成“地址增加 2”。

与 strchr() 一样,当待查字符就是 \0 时,strrchr() 也返回终止符的位置;字符串中只有这一个终止符属于本次搜索范围。

1.3 strchrnul函数介绍

strchrnul() 的函数原型为:

char *strchrnul(const char *s, int c);

它与 strchr() 都返回第一个匹配位置,区别只在查找失败时:strchr() 返回 NULL,strchrnul() 返回指向字符串终止符 \0 的指针。这里不是指向“最后一个可见字符”;例如 "Hello" 最后一个可见字符 'o' 的索引为 4,而终止符的索引为 5。

strchrnul() 是 GNU 扩展。使用 glibc 时,需要让 <string.h> 暴露它的声明,正确的源码顺序是:

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>

也可以在编译命令中定义,例如 gcc -D_GNU_SOURCE ...。关键条件不是“放在 <string.h> 之前”这么简单,而是必须在任何头文件之前定义特性测试宏:头文件可能彼此包含,先包含 <stdio.h> 后再定义就可能已经太晚。

视频和顶部原样源码采用了:

#include <stdio.h>
#define _GNU_SOURCE
#include <string.h>

因此本次编译没有得到 strchrnul() 的正确声明。宏名和函数名也要准确:原笔记转写出的 string_x_new、strchr_new 都不是本节函数,正确名称只有 strchrnul。

1.4 strchrnul函数应用

视频用三个调用展示 "Hello" 的查找结果。先只看接口在声明正确时应有的语义:

表达式应返回的位置相对 buf1 的偏移
strchr(buf1, 'l')第一个 'l'2
strrchr(buf1, 'l')最后一个 'l'3
strchrnul(buf1, '7')末尾 \05
strchr(buf1, '7')NULL不可计算

但视频中的实际编译缺少 strchrnul() 原型,最后一次运行显示的关键地址是:

strchr buf1:0x7ffcaeb87c52
strchr ptr:0x7ffcaeb87c54
strrchr ptr:0x7ffcaeb87c55
strchrnul ptr:0xffffffffaeb87c57
strchr find '7' ptr:(nil)

前三项分别对应基址、基址加 2、基址加 3。第四项却不是合法的“基址加 5”:它的低位虽然以 ...7c57 结尾,高位已经从 0x7ffc... 变成 0xffffffff...。这是缺少函数声明后,编译器按错误的整型返回值处理,再把它转换成指针所造成的截断/符号扩展现象。这个 ptr3 是坏指针,不能解引用或继续使用;画面中把它当成正确字符串末尾地址的解释是错误的。

最后一项来自标准 strchr(),(nil) 表示本次实现对空指针的输出。视频随后添加:

if (ptr4 == NULL)
{
    return;
}

它能阻止后续代码使用查找失败得到的 ptr4。由于添加判断后没有再次运行,屏幕上的 (nil) 属于添加判断之前的运行;若运行顶部最终源码,会在该判断处返回,不再执行其后的 printf()。

strchrnul() 也不能笼统称为“更安全”。它只是用结尾指针替代 NULL,适合调用者无论是否匹配都需要继续扫描到字符串末尾的场景;如果业务必须区分“找到了字符”和“没找到”,调用者仍要检查返回位置指向的是不是 \0,并注意查找字符本身为 \0 时也会返回同一位置。标准 C 中可移植的等价思路是先调用 strchr(),失败时再令指针指向 s + strlen(s)。

在嵌入式工具链中能否使用 strchrnul(),取决于所用 C 库及其头文件,而不能只看编译器是否名为 GCC。若库不提供该 GNU 扩展,应使用标准 strchr() 并检查 NULL,或者自己编写受测试和维护的兼容封装。

1.5 编译警告

本节出现的警告都不能直接忽略:

  1. 把指针交给 %d:%d 读取的是 int,与指针类型和大小都可能不同。视频已改成 %p;严格写法还要将对应实参转换成 void *。
  2. strchrnul 隐式声明:根因是 _GNU_SOURCE 定义得太晚,导致 <string.h> 没有声明该函数。C99 及之后不允许依赖旧式的隐式函数声明。
  3. 由整数生成指针:这是上一项错误的连锁结果。64 位指针通常比旧式假定的 int 更宽,视频输出已经直观展示了高位丢失后生成的无效地址。
  4. unsigned char * 与 char * 不兼容:本节所有字符串函数都声明为接收 const char *,课程源码却传入 unsigned char[]。顶部为忠实记录而保留,新代码应使用 char[] 保存普通字符串。
  5. %p 的实参类型:变参调用不会自动把任意对象指针转换成 void *,可移植写法应显式转换,例如 printf("%p\n", (void *)ptr);。

建议在练习时开启较完整的诊断,并把关键警告当作错误处理,例如:

gcc -std=gnu11 -Wall -Wextra -Wpedantic -Werror main.c -o main

这里使用 GNU 方言是因为示例包含 strchrnul();若目标是严格 ISO C 可移植代码,则应改用标准接口并选择 -std=c11 等标准模式。警告是编译器指出类型契约可能已被破坏的诊断,本节的错误地址正说明“程序还能生成并运行”不等于行为正确。

参考资料