字符串操作1

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

358.2.4.1 字符串操作 1

/* 引入 printf 函数的声明。 */
#include <stdio.h>
/* 引入 strlen 等字符串处理函数的声明。 */
#include <string.h>

/* 定义课程用于演示字符串长度与终止符的测试函数。 */
void StringLength_Test()
{
    /* 逐个写出 HelloWorld,并按视频最终状态显式添加字符串终止符。 */
    unsigned char buf1[] = {'H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd', '\0'};
    /* 用字符串字面量初始化数组,编译器会自动在末尾添加一个 '\0'。 */
    unsigned char buf2[] = "HelloWorld";

    /* 输出两个数组占用的字节数;这里的 %ld 按视频原样保留,类型问题见下文。 */
    printf("buf1 len:%ld, buf2 len:%ld\r\n", sizeof(buf1), sizeof(buf2));
    /* 按字符串形式输出 buf1;课程使用了 unsigned char 数组,类型注意事项见下文。 */
    printf("buf1 :%s\r\n", buf1);
    /* 按字符串形式输出 buf2。 */
    printf("buf2 :%s\r\n", buf2);
    /* 从返回类型为 void 的测试函数返回。 */
    return;
}

/* 程序从 main 函数开始执行。 */
int main()
{
    /* 输出本组字符串函数实验的标题。 */
    printf("GNU String Function Test\r\n");
    /* 运行本节编写的字符串长度与终止符实验。 */
    StringLength_Test();
    /* 返回 0,表示程序正常结束。 */
    return 0;
}

上面是本节视频结束时的完整 main.c。视频从上一节复制程序框架,新建 GNU_String/main.c,随后只编写了 StringLength_Test() 并在 main() 中调用它。本节虽然查询了 strlen() 的手册,但实验代码并没有真正调用 strlen(),而是用 sizeof 和 %s 对比带、未带空字符的数组。

课程配套源码是整个“字符串操作”小节结束后的汇总版本,其中还有 _GNU_SOURCE、buf3、memset、strcpy、strcat、strcmp、strchr 等后续内容;这些不是本节视频最终画面中的代码,因此没有提前放入顶部源码。

一、字符串操作

C 语言没有内建的“字符串对象”类型。通常所说的 C 字符串,是一个由 char 字节组成、并以第一个空字符 \0 结束的序列。字符串所在的数组可以比字符串本身更大:

数组内容:  H  e  l  l  o  \0  ?  ?
数组容量:  8 字节
字符串长度:5 字节

这里要区分三个概念:

概念含义
数组容量整个数组对象可容纳多少字节,数组仍在当前作用域时可用 sizeof array 获得
字符串长度从起始位置到第一个 \0 之前有多少字节,可用 strlen 获得
终止空字符数值为 0 的字节 \0,占用存储空间,但不计入 strlen 的结果

内存函数与字符串函数的共同点是都处理内存中的字节,关键差别在于边界来源:

函数类别典型函数如何确定处理边界适用数据
内存操作memcpy、memcmp、memmove调用者明确传入字节数任意对象表示或二进制数据
字符串操作strcpy、strcmp、strchr、strlen通常寻找 \0,部分接口另有最大长度参数满足接口前提的以空字符结尾的字节串

因此,“字符串函数处理字符”还不够准确:普通字符数组若没有可达的 \0,就不能直接交给要求 C 字符串的接口;二进制数据中即使碰巧含有 0 字节,也不代表程序一定要把它当作文本字符串。

课程把文本称为人与程序交互时最直观的数据形式,这个理解适合入门,但还要考虑字符编码。C 的这些窄字符串接口按字节工作;在 UTF-8 环境中,一个中文字符通常由多个字节编码,所以 strlen 返回的是字节数,不一定是用户看到的字符个数。

1.1 C语言中常用的字符串处理函数

视频思维导图列出的名称都是小写的标准库函数,并声明在 <string.h> 中:

函数本节给出的用途必须先记住的边界条件
strlen获取字符串长度输入必须能在有效可读区域内找到 \0;结果不含该终止符
strcpy复制字符串会连同 \0 一起复制;目标容量必须足够,源与目标不得重叠
strcat把源字符串追加到目标字符串末尾目标原先就必须是有效字符串,且剩余容量必须容纳追加内容和 \0
strcmp按字节顺序比较两个字符串返回值只应按小于 0、等于 0、大于 0 判断
strchr查找字符第一次出现的位置找不到时返回空指针;\0 本身也可以作为被查找字符

它们是 ISO C 标准库接口,不是 GNU C 独有语法;在视频的 GNU/Linux 环境中由 C 库实现。StringLength_Test() 则只是课程自定义的测试函数名,不是标准接口。

在 Linux 上可用第 3 节手册查询库函数,例如:

man 3 strlen
man 3 strcpy
man 3 strcat
man 3 strcmp
man 3 strchr

与其只看转载文章,更可靠的顺序是:先看本机 man 3,再核对 ISO C、POSIX 或所用 C 库的官方文档。手册中的函数原型、返回值、前置条件和标准归属尤其重要。

视频建立并验证程序框架的过程是:

cd ~/Desktop/GNU_String
gcc main.c -o main
./main

初始框架只输出:

GNU String Function Test

从旧项目复制头文件和 main() 可以减少重复输入,但复制后要立即修改项目标题、调用函数和目录,并删除与新实验无关的代码。否则虽然程序能编译,日志和实际业务却可能仍属于旧项目。

1.1.1 strlen
1.1.1.1 函数介绍

视频通过 man 3 strlen 查到的经典原型为:

size_t strlen(const char *s);
  • s 指向待测字符串的首字节;const 表示 strlen 不应通过该指针修改字符串内容。
  • 返回类型 size_t 是用于表示对象大小和长度的无符号整数类型。
  • 返回值是第一个 \0 之前的字节数,不包含终止空字符自身。
  • 调用者必须保证 s 不是空指针,并且从 s 开始的有效可读区域内存在 \0;内容为空的字符串 "" 则完全合法,其长度为 0。strlen 没有容量参数,无法在越过对象边界之前自动停下。
  • 从概念上看,strlen 需要从头寻找终止符,因此运行时间随字符串长度增长;循环条件中若反复对同一长字符串调用它,会造成不必要的重复扫描。

strlen 与 sizeof 不是可以互换的两个“长度函数”。sizeof 是运算符,strlen 才是库函数:

表达式得到什么是否包含 \0何时有效
sizeof array整个数组对象的字节数若终止符位于数组内,也计算它array 在此处仍是数组对象
sizeof pointer指针对象自身的字节数与所指字符串长度无关操作数已经是指针时
strlen(s)第一个 \0 之前的字节数不包含s 指向有效的、以 \0 结尾的字符串时

例如 char text[32] = "Hello"; 中,sizeof(text) 为 32,而 strlen(text) 为 5。若再写 char *p = text;,sizeof(p) 得到的是当前平台的指针大小,不会得到 32 或 5。

像视频中的 buf2[] 这样让编译器根据字符串字面量推导数组大小时,初始化内容会包含终止符:

"HelloWorld"  →  H e l l o W o r l d \0
下标             0 1 2 3 4 5 6 7 8 9 10

因此视频中的 buf2[] = "HelloWorld" 会自动推导成 11 字节。终止符并不是所有数组都会“隐藏拥有”的元素;只有字符串字面量初始化、显式写入、零初始化等确实产生了 0 字节时,它才存在于相应位置。

1.1.1.2 模拟实现

视频先让两个数组表达相同的十个可见字符,但最初的 buf1 只有十个字符元素,没有 \0:

数组初始内容sizeof是否是可供 %s 使用的有效 C 字符串
buf1(修改前)H e l l o W o r l d10否,没有终止空字符
buf2H e l l o W o r l d \011是,字符串字面量自动补了 \0

使用 %d 打印 sizeof 时,GCC 提示格式参数期待 int,而该平台上的 size_t 是 long unsigned int。视频随后把 %d 改成 %ld,警告在当时的编译选项下消失,但 %ld 要求的是 long int,与无符号的 size_t 仍不严格匹配。C99 起专门用于 size_t 的可移植写法是 %zu:

printf("buf1 len:%zu, buf2 len:%zu\n", sizeof(buf1), sizeof(buf2));

第一次运行时,视频观察到:

buf1 len:10, buf2 len:11
buf1 :HelloWorldHelloWorld
buf2 :HelloWorld

第二行不能解释为 %s 合法地“把两个连续数组拼起来”。%s 要求实参指向以 \0 结束的字符数组;buf1 中没有终止符,库函数读到 buf1 边界之外时,程序行为已经未定义。当次运行碰巧在随后区域读到了 buf2 和它的终止符,所以画面看起来重复了一次 HelloWorld;C 语言既不保证两个局部数组相邻,也不保证它们按声明顺序排列。换编译器、优化级别或一次普通代码改动,都可能得到乱码、泄露其他内存内容或直接崩溃。

这也纠正了视频中的另一处表述:printf 的 %s 在这里不断读取字节并输出,不会因为缺少 \0 就“把其他程序擦掉”。它越界读取仍然非常危险,但风险是未定义行为、崩溃或信息泄露,不是必然覆盖其他程序的数据。

随后视频在 buf1 的初始化列表末尾显式加入 \0。未指定数组长度时,数组大小由初始化器数量推导,因此 buf1 从 10 字节变成 11 字节,最终稳定输出:

GNU String Function Test
buf1 len:11, buf2 len:11
buf1 :HelloWorld
buf2 :HelloWorld

此时两个数组的对象大小都是 11 字节,可见文本长度都是 10 字节。若对类型正确的 char 字符串调用 strlen,结果应为 10,而不是 11。

还需注意,课程源码把文本数组声明成了 unsigned char[],但 strlen 的形参和 printf 的 %s 都要求 char *(只读场景通常是 const char *)。许多平台上它看起来能够输出相同字节,但严格、可移植的新代码应使用 char 数组保存普通 C 字符串;unsigned char 更适合明确表示原始字节数据。顶部源码按视频原样保留,没有擅自改型。

最后,不要混淆下面三个写法:

写法含义
\0 或 0(作为字符值)数值为 0 的空字符,可用作字符串终止符
'0'可见数字字符 0,在 ASCII 等兼容编码中数值为 48,不会终止字符串
NULL空指针宏,用于表示“不指向任何对象”;不是可以读取的字符串地址

参考资料