内存操作 下

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

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

参考资料