内存操作 下
357.2.3.3 内存操作(下)
/* 引入 printf 函数的声明。 */
#include <stdio.h>
/* 引入 memset、memcmp、memcpy 和 memmove 的声明。 */
#include <string.h>
/* 定义前两节沿用下来的内存填充测试函数。 */
void Memset_Test()
{
/* 定义含 15 个 int 元素的局部数组。 */
int buf[15];
/* 输出当前测试项目的提示文字。 */
printf("memset test func\r\n");
/* 输出课程计算的元素个数和 int 大小;%ld 与 size_t 并不严格匹配。 */
printf("sizeof buf byte num:%ld, int size:%ld\r\n", sizeof(buf) / sizeof(int), sizeof(int));
/* 视频中暂时注释了填充操作。 */
// memset(buf, 0, sizeof(buf));
/* 视频中暂时注释了原来的遍历循环。 */
// for (char i = 0; i < sizeof(buf); i++)
/* 这是被注释循环原来的左花括号。 */
// {
/* 这是被注释循环原来的元素输出语句。 */
// printf("%d ", buf[i]);
/* 这是被注释循环原来的右花括号。 */
// }
/* 视频中也暂时注释了循环后的换行。 */
// printf("\r\n");
}
/* 定义内存比较测试函数。 */
void Memcmp_Test()
{
/* 定义第一段 5 字节的待比较数据。 */
unsigned char buf1[5] = {1, 2, 3, 4, 5};
/* 定义第二段数据;只有最后一个字节与 buf1 不同。 */
unsigned char buf2[5] = {1, 2, 3, 4, 9};
/* 只比较前 4 字节,因此故意忽略最后一个不同字节。 */
int ret = memcmp(buf1, buf2, sizeof(buf1) - 1);
/* 输出比较结果;本例只比较前 4 字节时结果为 0。 */
printf("ret val = :%d\r\n", ret);
/* 从返回类型为 void 的函数返回。 */
return;
}
/* 定义不允许源、目标区域重叠的内存拷贝测试函数。 */
void Memcpy_Test()
{
/* 定义并初始化 8 字节的源数组。 */
unsigned char buf1[8] = {1, 2, 3, 4, 5, 6, 7, 8};
/* 定义同样大小的目标数组。 */
unsigned char buf2[8];
/* 把目标数组的全部字节预先清零。 */
memset(buf2, 0, sizeof(buf2));
/* 把 buf1 的全部 8 字节复制到 buf2。 */
memcpy(buf2, buf1, sizeof(buf1));
/* 依次遍历源数组对应的 8 个下标。 */
for (unsigned char i = 0; i < sizeof(buf1); i++)
{
/* 输出复制到 buf2 的当前元素;源码没有添加分隔空格。 */
printf("%d", buf2[i]);
}
/* 所有元素输出完毕后换行。 */
printf("\r\n");
/* 从返回类型为 void 的函数返回。 */
return;
}
/* 定义允许源、目标区域重叠的内存搬移测试函数。 */
void Memmove_Test()
{
/* 定义恰好 10 个字符的数组;其中没有字符串结尾的空字符。 */
unsigned char buf1[] = {'H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'};
/* 让 overlap 指向下标 5,也就是字符 'W' 所在的位置。 */
unsigned char *overlap = buf1 + 5;
/* 输出原始数据的标签。 */
printf("src: ");
/* 逐一遍历 buf1 的 10 个字节。 */
for (unsigned char i = 0; i < sizeof(buf1); i++)
{
/* 把当前字节按字符形式输出。 */
printf("%c", buf1[i]);
}
/* 原始数据输出完毕后换行。 */
printf("\r\n");
/* 把前 5 字节 Hello 复制到后 5 字节的起始位置。 */
memmove(overlap, buf1, 5);
/* 输出操作后数据的标签。 */
printf("dest: ");
/* 再次逐一遍历 buf1 的 10 个字节。 */
for (unsigned char i = 0; i < sizeof(buf1); i++)
{
/* 把操作后的当前字节按字符形式输出。 */
printf("%c", buf1[i]);
}
/* 操作后的数据输出完毕后换行。 */
printf("\r\n");
/* 从返回类型为 void 的函数返回。 */
return;
}
/* 程序从 main 函数开始执行。 */
int main()
{
/* 输出整个内存函数实验的标题。 */
printf("GNU Memory Function Test\r\n");
/* 视频最终停用内存填充测试。 */
// Memset_Test();
/* 视频最终停用内存比较测试。 */
// Memcmp_Test();
/* 视频最终停用普通内存拷贝测试。 */
// Memcpy_Test();
/* 视频最终只运行允许重叠的内存搬移测试。 */
Memmove_Test();
/* 返回 0,表示程序正常结束。 */
return 0;
}
上面按视频最终状态收录了完整 main.c:本节依次编写并运行 Memcmp_Test()、Memcpy_Test() 和 Memmove_Test(),最后在 main() 中只保留 Memmove_Test() 为活动调用。课程配套源码后来把 Memset_Test() 恢复成了 char 数组和活动填充循环;本节顶部仍保留视频画面中的 int buf[15] 与注释状态,避免把课后整理版本误当成课堂现场代码。
四个内存函数均声明在 <string.h> 中,长度参数的单位都是字节。本节新增的三个函数可先用一句话区分:
| 函数 | 操作 | 源、目标能否重叠 | 返回值 |
|---|---|---|---|
memcmp | 比较两段区域的前 n 字节 | 不涉及写入,区域可相交 | 小于、等于或大于 0 的 int |
memcpy | 把源区域的前 n 字节复制到目标区域 | 不能重叠 | 目标地址 dest |
memmove | 把源区域的前 n 字节复制到目标区域 | 可以重叠 | 目标地址 dest |
一、内存比较
1.1 内存比较测试函数
准确的函数名是 memcmp,可用下面的命令查看第 3 节库函数手册:
man 3 memcmp
视频新建 Memcmp_Test(),把上一节的 Memset_Test() 调用注释掉,再在 main() 中调用比较测试。Memcmp_Test 是课程自行起的测试函数名,memcmp 才是标准库接口。
视频中的函数定义使用空参数列表:
void Memcmp_Test()
在 C17 及更早版本中,声明里的空括号表示“没有给出参数类型信息”,并不是严格的无参数原型。新代码通常写成 void Memcmp_Test(void);顶部为保持课程源码原样未作修改。return; 对 void 函数是合法的,位于函数末尾时也可以省略。
1.2 memcmp 函数原型
原笔记标题误写成“memcpy 函数原型”,这一节实际查询的是 memcmp:
int memcmp(const void *s1, const void *s2, size_t n);
s1、s2分别指向两段至少可读n字节的区域;n是比较的字节数,因此比较范围是偏移0到n - 1,不是“第 0 个到第 n 个”;- 每个字节都按
unsigned char解释,不受普通char在当前平台上是否有符号影响; - 两段的前
n字节完全一致时返回0;否则返回值的符号由第一对不同字节的大小关系决定。
返回值只能依赖三种关系:
| 条件 | 返回值保证 |
|---|---|
| 第一段按字节序小于第二段 | < 0 |
前 n 字节全部相同 | == 0 |
| 第一段按字节序大于第二段 | > 0 |
视频完整比较两个 5 字节数组时显示 -1。这只是当时实现的具体结果,标准不保证“不相同就返回 -1”,也不保证返回第一对字节的算术差;可移植代码只能检查负、零、正。
1.3 测试程序
两个数组只有最后一个字节不同:
下标 0 1 2 3 4
buf1 1 2 3 4 5
buf2 1 2 3 4 9
^ 唯一差异
第一次用 sizeof(buf1) 比较全部 5 字节,结果为负值,因为第一对不同字节满足 5 < 9。随后课程改成:
memcmp(buf1, buf2, sizeof(buf1) - 1)
第三个参数变成 4,只比较下标 0~3,所以返回 0。这并不表示两个完整数组相同,只表示指定的前 4 字节相同。比较固定前缀时应明确写出协议字段长度,避免随意的“总长度减一”掩盖真正应检查的数据。
memcmp 适合比较原始字节块,例如确认两份固定长度 Flash 数据是否逐字节一致;用于其他场景要注意:
- 比较两段数据前,应先确认它们的有效长度一致,否则只比较较短长度会把“相同前缀”误认为完整相同;
- 两段文字只有在编码、长度和规范化方式一致时,逐字节相同才等于所需的文本相同;
- 两张视觉上相同的图片可能因压缩、元数据或文件格式不同而具有不同字节,
memcmp判断的是文件字节,不是视觉内容; - 结构体可能含有填充字节,不宜普遍用
memcmp代替逐成员比较; - 普通
memcmp往往在遇到首个差异时结束,不适合直接比较密码、密钥等需要恒定时间处理的秘密数据。
二、内存拷贝
2.1 内存拷贝函数查询
准确的函数名是 memcpy,查询命令为:
man 3 memcpy
memcmp 中的 cmp 来自 compare,memcpy 中的 cpy 来自 copy。若把函数名拼错,常见结果是编译器报告未声明函数,或链接器报告找不到对应符号;不是调用一个意思相近的函数后悄悄产生错误结果。开启 -Wall -Wextra 并认真处理诊断,比只靠肉眼防止拼写问题更可靠。
2.2 memcpy 函数定义
便于初学者阅读的经典原型为:
void *memcpy(void *dest, const void *src, size_t n);
参数顺序可按赋值方向记忆:左边是目标 dest,右边是源 src。函数把 src 起始处的 n 字节复制到 dest,不是复制“n 个任意类型元素”。调用者必须同时保证:
src至少有n个可读字节;dest至少有n个可写字节;- 两个半开区间
[src, src + n)与[dest, dest + n)不重叠。
若区域重叠,C 标准规定行为未定义。某次运行碰巧得到正确结果也不能依赖,因为实现可能按不同方向、字宽或向量块复制。函数返回 dest,调用者可以忽略这个返回值,也可以在适合的表达式中继续使用;视频所谓“删除返回值”实际只是没有接收 memcpy 的调用结果,并没有也不能改变标准函数的原型。
2.2.1 测试函数演示
课程创建 8 字节源数组 buf1 和同样大小的目标数组 buf2,执行:
memset(buf2, 0, sizeof(buf2));
memcpy(buf2, buf1, sizeof(buf1));
这里 sizeof(buf1) 为 8,目标数组也恰好为 8 字节,所以整个 buf2 都被覆盖。前面的 memset 能展示清零接口的用法,但对这次完整覆盖不是正确性所必需;如果只复制部分字节,预先清零才会决定未覆盖区域的初始内容。
循环按 %d 连续打印且没有空格,视频结果是:
12345678
这是八个一位数相邻显示,不是一个被 memcpy 构造出的整数。若元素可能大于 9,调试输出应加空格或逗号以免歧义。
memcpy 只复制指定字节,不会自动补字符串结尾的 \0。复制字符串时,若后续要把目标当作 C 字符串使用,长度计算必须包含终止空字符,且目标容量必须足够。
课程用 unsigned char 作为循环变量。当前数组只有 8 个元素,不会溢出;通用代码仍应使用与长度相匹配的 size_t,避免较大数组使窄类型计数器回绕。
三、内存搬移
3.1 内存搬移函数定义
准确的函数名是 memmove:
void *memmove(void *dest, const void *src, size_t n);
它也把 src 开始的 n 字节复制到 dest 并返回 dest,但允许两个区域重叠。标准把结果规定为:好像先把源区域的 n 字节复制到一块不与两者重叠的临时数组,再从临时数组复制到目标区域。
“好像使用临时数组”描述的是必须达到的可观察结果,不要求库函数内部真的申请一块缓冲区。高效实现常根据地址关系选择从前向后或从后向前复制,也可能使用更宽的加载、存储或平台专用指令。
尽管名字是 move,源区域不会在操作后被自动清零;它仍然是一次复制,只是目标写入可能因重叠而同时改变源对象的一部分。
3.1.1 测试函数演示
课程数组实际包含 10 个字符,没有空格,也没有结尾 \0:
下标 0 1 2 3 4 | 5 6 7 8 9
原数据 H e l l o | W o r l d
源范围 ● ● ● ● ● | · · · · · buf1,长度 5
目标范围 · · · · · | ● ● ● ● ● buf1 + 5,长度 5
overlap = buf1 + 5 指向字符 W,随后执行:
memmove(overlap, buf1, 5);
得到:
src: HelloWorld
dest: HelloHello
这里必须纠正课程中的关键表述:源范围是下标 0~4,目标范围是下标 5~9,两段区域只是首尾相接,并没有重叠。因此这个调用用 memcpy 也具有已定义行为,变量名 overlap 并没有让区域真的重叠;本例只展示了 memmove 能完成普通复制,没有验证其重叠处理能力。
真正的重叠示例可以是:
unsigned char demo[] = {'A', 'B', 'C', 'D', 'E', 'F'};
memmove(demo + 1, demo, 5);
源范围为下标 0~4,目标范围为 1~5,两者共享下标 1~4;标准保证结果为 AABCDE。若这里误用 memcpy,行为未定义,不能根据某台机器的一次输出判断是否“可用”。
课程没有给 buf1 添加 \0,所以它不是 C 字符串,不能安全地用 %s 输出。视频通过 sizeof(buf1) 控制循环并逐字节使用 %c,正好输出 10 个字符,这是适合当前数据布局的做法。
最初视频把原始数据的打印放到了 memmove 之后,于是 src 和 dest 都显示 HelloHello;把第一段打印移到调用之前,才能观察变化前后的两个状态。程序语句按执行顺序修改对象,标签叫 src 并不会自动保存历史副本。
编译时仍需确认终端位于正确目录:
cd ~/Desktop/GNU_Mem
gcc -std=c99 -Wall -Wextra main.c -o main
./main