字符串操作2

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

359.2.4.2 字符串操作 2

/* 引入 printf 函数的声明。 */
#include <stdio.h>
/* 引入 memset、strcpy 和 strncpy 函数的声明。 */
#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;
}

/* 定义本节新建的字符串复制测试函数。 */
void StringStrcpy_Test()
{
    /* 保存源字符串;数组共有十二字节,其中最后一字节是 '\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 的全部字节清零;后面的第 7 字节会继续充当终止符。 */
    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);

    /* sizeof(buf1) 为 12,因此这里只把开头六字节 "Hello," 复制到 buf3。 */
    strncpy(buf3, buf1, sizeof(buf1) - 6);
    /* 输出 buf3;本次能够结束,是因为 buf3[6] 仍是预先写入的零。 */
    printf("buf3 data:%s \r\n", buf3);

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

/* 程序从 main 函数开始执行。 */
int main()
{
    /* 输出这一组 GNU/Linux 字符串函数实验的标题。 */
    printf("GNU String Function Test\r\n");
    /* 视频中已经注释掉旧测试函数,所以本次不会执行它。 */
    // StringLength_Test();
    /* 调用本节新建的字符串复制测试函数。 */
    StringStrcpy_Test();
    /* 返回 0,表示程序正常结束。 */
    return 0;
}

上面是本节视频结束时的完整 main.c。本节先用 man strcpy 查看手册,再新建 StringStrcpy_Test(),先验证 strcpy() 的整串复制,随后加入 buf3 和 strncpy(),验证限制复制字节数时的结果。视频中的旧 StringLength_Test() 在两节之间又变回了“buf1 没有 \0”的版本,并多出一个填充为 '-' 的 buf3;本节没有演示这次改动的过程,而且最终也没有调用该函数,所以上方按实际画面保留,不把它误写成本节的新知识。

课程配套目录里的 main.c 是后续多节完成后的汇总版本,其中已经把这次调用改成 strncpy(buf3, buf1, sizeof(buf1) - 1),并加入 _GNU_SOURCE、strcat、strcmp、strchr 等内容。它不能代替本节末帧:本节视频实际使用的是 sizeof(buf1) - 6,因此这里没有提前并入后续代码。

一、string copy函数

标题里的 “string copy” 是对“字符串复制函数”这一类操作的口头称呼,真正的标准 C 函数名是 strcpy 和 strncpy,函数名中没有空格。它们都在 <string.h> 中声明,也都要求源和目标区域不能重叠;若区域可能重叠,应根据数据类型和需求另行选择合适的处理方法,不能继续调用这两个函数。

在 Linux 终端中可查询第 3 节库函数手册:

man 3 strcpy
man 3 strncpy

man strcpy 通常会同时展示这两个接口。阅读手册时不能只看一句“复制字符串”,还要核对函数原型、参数约束、返回值以及错误或未定义行为条件。

1.1 功能作用

strcpy() 用来复制一个完整的、以空字符 \0 结尾的 C 字符串。它从 src 的第一个字节开始读取,一直复制到第一个 \0,并且把这个终止符也写入 dest:

src : H e l l o , W o r l d \0
      └───────────────┬────┘
strcpy 复制的范围:全部可见字符和末尾的 \0

因此,strcpy(dest, src) 能正确执行至少需要满足:

  • src 指向一个有效的、在可读区域内能遇到 \0 的字符串;
  • dest 至少能容纳 strlen(src) + 1 字节,其中额外的一字节用于 \0;
  • dest 指向可写区域;
  • 源区域与目标区域不重叠。

strcpy() 没有接收目标容量的参数,也不会替调用者检查空间是否足够。目标过小时是未定义行为,可能表现为数据破坏、异常退出,也可能暂时看不出问题;不能把它简化成“一定会崩溃”。

strncpy() 接收第三个参数 n,但它并不是简单的“更安全版 strcpy”。可以把它的行为准确概括为:向 dest 写入恰好 n 个字节,具体内容取决于源字符串的终止符是否出现在前 n 字节内。

源数据情况strncpy(dest, src, n) 写入的内容结果是否必然是字符串
前 n 字节内遇到 \0先复制到该 \0,再用额外的零字节填满总计 n 字节是,且可能有多个尾随零字节
前 n 字节内没有 \0复制恰好 n 个非零字节,不再补终止符否;除非目标在第 n 字节之后本来就另有可用的零字节

所以原笔记中“遇到 \0 就截断,无法拷贝完整长度 n”并不准确:遇到 \0 后虽然不再从源字符串读取可见字符,但 strncpy() 会继续用零填充,最终仍写满 n 字节。真正容易遗漏的是另一种情况——源字符串的前 n 字节没有 \0 时,复制结果不会自动获得终止符。

手册可能用“一个循环复制非零字节,再一个循环补零”的代码帮助说明语义,但那只是等价描述,不是标准强制规定的库内部实现。不同 C 库可以采用不同的优化实现,只要外部可观察行为符合接口约定即可。

1.2 参数分析

这两个函数的经典 C 原型为:

char *strcpy(char *restrict dest, const char *restrict src);
char *strncpy(char *restrict dest, const char *restrict src, size_t n);

各参数和返回值的含义如下:

项目含义
dest目标区域首字节的地址,函数会向这里写数据
src源字符序列首字节的地址;const 表示函数不会通过该形参修改源数据
nstrncpy() 要向目标写入的字节数,类型是 size_t
restrict调用者承诺本次操作涉及的源、目标访问不会以不允许的方式别名重叠;这也与“不允许复制区域重叠”的前提一致
返回值返回传入的 dest 指针值,方便把调用结果继续用于表达式

“返回 dest”不是返回“目标首元素的数据”,而是返回目标首地址。视频代码没有接收返回值完全合法:忽略返回值不会改变复制动作,也不会改变函数本身的返回类型。

对 strncpy(dest, src, n) 而言,dest 至少要有 n 个可写字节。仅有“n 不大于目标容量”还不等于结果可作为 %s 字符串使用:若前 n 个源字节中没有 \0,调用者还必须以其他方式保证终止符存在,并且终止符仍位于目标对象边界内。

还要区分 n 的单位:它表示字节数,不是“字符个数”。对本节只包含 ASCII 字符的数据,两者碰巧一一对应;对 UTF-8 中文等多字节编码,一个用户眼中的字符通常会占多个字节,随意按字节截断还可能切在编码序列中间。

课程源码把普通 C 字符串数组声明成了 unsigned char[],但标准原型的形参是 char * / const char *,printf 的 %s 也要求指向 char 的指针。二者不是严格兼容的指针类型,启用警告后编译器可能给出诊断。顶部代码为了忠实于视频没有擅自改型;新写的文本字符串代码通常应使用 char[],原始二进制字节数据才更适合使用 unsigned char[]。

1.3 原型复制

视频先创建源数组和第一块目标缓冲区:

unsigned char buf1[] = "Hello,World";
unsigned char buf2[128];

字符串字面量包含 11 个可见字符,编译器还会在数组末尾加入 \0,所以这里的 sizeof(buf1) 是 12,而不是 11:

下标:  0 1 2 3 4 5 6 7 8 9 10 11
内容:  H e l l o , W o r l d  \0
字节数:<------------- 12 ------------>

随后代码清零 buf2 并复制:

memset(buf2, 0, sizeof(buf2));
strcpy(buf2, buf1);

strcpy() 本次会写入 12 字节,包括 buf1 末尾的 \0。由于 buf2 有 128 字节,容量充足。只要这次 strcpy() 成功执行,为正确得到字符串而预先清零整个 buf2 并非必需,因为 strcpy() 自己会复制终止符;清零的作用是让未被复制覆盖的其余字节也处于已知状态。

视频接着建立并清零 buf3:

unsigned char buf3[128];
memset(buf3, 0, sizeof(buf3));
strncpy(buf3, buf1, sizeof(buf1) - 6);

这里最关键的计算是:

sizeof(buf1) - 6 = 12 - 6 = 6

第三个参数只是复制字节数,不会让源地址产生偏移。因此这行代码从 buf1[0] 开始复制前六字节,即 H e l l o ,;它不是“从源地址向前或向后移动六个字符”。若要从 W 开始读取,源指针表达式才应出现类似 buf1 + 6 的偏移,但视频没有这样写,顶部也不应改成那种代码。

此外,源字符串的 \0 在下标 11,不在前六字节内,所以这次 strncpy() 本身没有向目标写入终止符。之所以随后能看到稳定的 Hello,,是因为之前已经把 buf3 全部清零,复制只覆盖 buf3[0] 到 buf3[5],buf3[6] 仍为零:

调用前 buf3:  \0 \0 \0 \0 \0 \0 \0 ...
复制后 buf3:   H  e  l  l  o  , \0 ...
                └── strncpy 写入 ──┘ ↑
                                     预先清零留下的终止符

若删去 memset(),又不在有效范围内显式安排终止符,那么紧接着用 %s 输出 buf3 就可能越过已复制的六字节继续读取,构成未定义行为。

1.4 运行演示

视频中的编译和运行流程为:

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

第一次只完成 strcpy() 时,视频曾因忘记在 main() 中调用新测试函数而只看到标题;把旧调用注释掉并加入 StringStrcpy_Test(); 后,测试代码才真正执行。这说明“成功编译”只表示源文件通过了当前编译流程,不等于想验证的函数一定已经进入执行路径。

本节最终输出为:

GNU String Function Test
buf1 data:Hello,World
buf2 data:Hello,World
buf3 data:Hello,

三行数据可分别解释为:

  1. buf1 原本就是以 \0 结尾的 Hello,World;
  2. strcpy() 把 11 个可见字符以及终止符一并复制到 buf2;
  3. strncpy() 只复制开头六字节 Hello,,而 buf3 预先保留的零字节让 %s 在逗号后停止。

原笔记把这两次操作分别写成 memcpy 和 memmove,属于函数名识别错误。视频实际演示的是 strcpy 和 strncpy,没有演示重叠内存复制,也没有调用 memcpy() 或 memmove()。尤其不能由本节例子推导出“带 n 的函数都具有相同的终止符语义”:内存复制函数按原始字节工作,而字符串复制函数对 \0 有专门约定。

若在自己的代码上做实验,建议额外启用常见警告:

gcc -std=c11 -Wall -Wextra -Wpedantic -Wformat=2 main.c -o main

这样能更早看到本例中 unsigned char * 与字符串接口形参不兼容、sizeof 结果与 %ld 格式不严格匹配等问题。sizeof 的结果类型是 size_t,可移植的输出格式是 %zu;不过顶部的 %ld 仍按视频实际源码保留。

最后,虽然本节 main() 没有调用 StringLength_Test(),仍应认清其中的遗留风险:buf1 只有十个可见字符,没有 \0,却被交给 %s。旁边的 buf3 即使曾经初始化为零,随后也被五个 '-' 全部覆盖,而且 C 不保证几个局部数组按源码声明顺序相邻。若重新启用该函数,这次 %s 调用仍是未定义行为,不能把偶然输出当作字符串会自动终止的证明。

参考资料