字符串操作2
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 表示函数不会通过该形参修改源数据 |
n | strncpy() 要向目标写入的字节数,类型是 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,
三行数据可分别解释为:
buf1原本就是以\0结尾的Hello,World;strcpy()把 11 个可见字符以及终止符一并复制到buf2;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 调用仍是未定义行为,不能把偶然输出当作字符串会自动终止的证明。