完整教程:嵌入式软件工程师面经C/C++篇—重写 memcpy () 函数:从原理到注意事项(超详细入门)

memcpy() 是 C 语言标准库中最常用的内存操作函数之一,作用是按字节将源内存的数据复制到目标内存。虽然库函数已经实现好了,但手动重写它能帮我们理解内存操作的细节,还能应对面试中的常见问题。

本文会从「为什么要重写」「重写要注意什么」「完整代码示例」三个角度,用通俗的语言带大家掌握 memcpy() 的实现。

一、先搞懂:memcpy () 的基本用法

在重写前,先明确标准 memcpy() 的核心信息,避免偏离目标:

项目说明
函数原型void* memcpy(void* dest, const void* src, size_t n);
功能从 src 指向的内存,复制 n 个字节到 dest 指向的内存
参数dest:目标内存地址(要写数据)- src:源内存地址(要读数据,用 const 保护不被修改)- n:要复制的字节数(不是元素个数!)
返回值返回 dest(方便链式调用,比如 memcpy(d1, d2, n)[0] = 'a'
标准规定不处理内存重叠(如果 src 和 dest 地址有重叠,结果不确定)

简单示例:用标准 memcpy () 复制数据

#include 
#include  // 标准 memcpy() 头文件
int main() {
    // 1. 复制字符串(注意要包含 '\0',所以 n 是 strlen(src)+1)
    char src_str[] = "hello";
    char dest_str[20] = {0};
    memcpy(dest_str, src_str, strlen(src_str) + 1);
    printf("复制字符串:%s\n", dest_str); // 输出:hello
    // 2. 复制整型数组(n 是 数组元素个数 * 每个元素的字节数)
    int src_arr[] = {1, 2, 3, 4};
    int dest_arr[4] = {0};
    memcpy(dest_arr, src_arr, sizeof(src_arr)); // sizeof(src_arr) = 4*4=16 字节
    for (int i = 0; i < 4; i++) {
        printf("%d ", dest_arr[i]); // 输出:1 2 3 4
    }
    return 0;
}

二、重写 memcpy ():必须注意的 5 个核心问题

这是重写的关键!忽略任何一个问题,都会导致代码出错或不安全。

问题 1:空指针检查(避免程序崩溃)

dest 或 src 可能是 NULL(比如调用者误传空指针),如果直接操作空指针,程序会崩溃。解决办法:先判断两个指针是否为 NULL,是则返回 NULL(或报错)。

问题 2:类型转换(按字节复制)

标准 memcpy() 的参数是 void*(通用指针),但 void* 不能直接解引用(不知道要操作多少字节)。而 char* 是 1 字节,刚好适合按字节复制。解决办法:将 dest 和 src 强制转换为 char* 类型,再进行读写。

问题 3:内存重叠(最容易踩坑的点)

「内存重叠」指 src 和 dest 指向的内存区域有交叉,比如:

  • 场景 1:src = arrdest = arr+2(目标在源的右边,复制时会覆盖还没读的源数据)
  • 场景 2:src = arr+2dest = arr(目标在源的左边,复制时不会覆盖源数据)
举个反例:不处理重叠会出错
#include 
#include 
int main() {
    char arr[10] = "123456789";
    // 想把 arr[0..4]("12345")复制到 arr[2..6],期望结果是 "121234589"
    memcpy(arr+2, arr, 5);
    printf("%s\n", arr); // 实际输出:121212189(因为复制时覆盖了源数据)
    return 0;
}
为什么会错?

按「从左到右」复制(低地址→高地址)时:

  1. 先把 arr[0]('1')复制到 arr[2] → arr 变成 "121456789"
  2. 再把 arr[1]('2')复制到 arr[3] → arr 变成 "121256789"
  3. 接着把 arr[2](已被改成 '1')复制到 arr[4] → 出错!
解决办法:判断重叠方向,选择复制顺序
  • 如果 目标在源的左边dest < src)或 目标在源的右边且不重叠dest >= src + n):从左到右复制(低地址→高地址),安全。
  • 如果 目标在源的中间src < dest < src + n):从右到左复制(高地址→低地址),避免覆盖未读取的源数据。

问题 4:无符号的长度(size_t 类型)

n 的类型是 size_t(typedef 自 unsigned int 或 unsigned long),是无符号数。如果误把 n 当成有符号数,可能出现循环次数错误(比如 n 为负数时,while(n--) 会变成死循环)。解决办法:严格使用 size_t 作为 n 的类型,循环中不修改 n 的原始值(或用临时变量)。

问题 5:返回目标地址(符合标准)

标准 memcpy() 要求返回 dest,方便链式调用。解决办法:先保存 dest 的原始地址(因为后续会修改 dest 的指针),最后返回保存的地址。

三、完整代码:手写 memcpy ()(处理所有问题)

结合上面的注意事项,我们实现一个「安全、符合标准」的 my_memcpy()

#include 
#include  // 用于 assert() 断言(可选,增强安全性)
#include  // 用于 size_t 类型
// 重写 memcpy():返回 dest,处理空指针、内存重叠
void* my_memcpy(void* dest, const void* src, size_t n) {
    // 1. 空指针检查(用 assert 强制报错,也可以返回 NULL 并处理)
    assert(dest != NULL && src != NULL);
    // 2. 保存 dest 原始地址(后续要修改指针,最后返回用)
    char* dest_ptr = (char*)dest;
    const char* src_ptr = (const char*)src; // src 用 const 保护,避免被修改
    // 3. 判断内存重叠,选择复制顺序
    if (dest_ptr < src_ptr || dest_ptr >= src_ptr + n) {
        // 情况 1:目标在源左边,或目标在源右边且不重叠 → 从左到右复制
        while (n--) {
            *dest_ptr++ = *src_ptr++; // 先赋值,再移动指针
        }
    } else {
        // 情况 2:目标在源中间(重叠)→ 从右到左复制
        // 先把指针移到最后一个要复制的字节(n 是总字节数,所以减 1)
        dest_ptr += n - 1;
        src_ptr += n - 1;
        while (n--) {
            *dest_ptr-- = *src_ptr--; // 先赋值,再移动指针(向左)
        }
    }
    // 4. 返回原始的 dest 地址
    return dest;
}
// 测试代码
int main() {
    // 测试 1:正常复制(无重叠)
    char str1[20] = {0};
    my_memcpy(str1, "hello", 6); // 复制 "hello\0",共 6 字节
    printf("测试 1(无重叠):%s\n", str1); // 输出:hello
    // 测试 2:内存重叠(目标在源中间)
    char str2[10] = "123456789";
    my_memcpy(str2 + 2, str2, 5); // 复制 str2[0..4] 到 str2[2..6]
    printf("测试 2(重叠):%s\n", str2); // 输出:121234589(正确)
    // 测试 3:复制整型数组
    int arr1[] = {1, 2, 3, 4};
    int arr2[4] = {0};
    my_memcpy(arr2, arr1, sizeof(arr1)); // sizeof(arr1) = 16 字节
    printf("测试 3(整型数组):");
    for (int i = 0; i < 4; i++) {
        printf("%d ", arr2[i]); // 输出:1 2 3 4
    }
    return 0;
}

四、关键对比:my_memcpy () vs 标准 memcpy () vs memmove ()

很多人会混淆这三个函数,这里用表格明确区别:

函数处理内存重叠核心场景
标准 memcpy()❌ 不处理(重叠时结果不确定)明确知道内存不重叠时,效率高
我们写的 my_memcpy()✅ 处理(实际是按 memmove() 的逻辑实现)想兼顾安全和通用,避免重叠问题
标准 memmove()✅ 处理(标准要求必须处理重叠)内存可能重叠时(比如数组内部复制)

注意:标准 memcpy() 不处理重叠是为了效率 —— 如果每次都判断重叠,会多一层开销。而 memmove() 因为要处理重叠,效率会略低。

五、常见面试题:重写 memcpy () 相关问题

  1. :为什么 memcpy() 的参数用 void*void* 是通用指针,可以接受任何类型的地址(比如 int*char*struct*),实现 “复制任意类型数据” 的功能。

  2. :内存重叠时,memcpy() 和 memmove() 有什么区别?memcpy() 不处理重叠,结果不确定;memmove() 会判断重叠方向,选择从左到右或从右到左复制,结果确定。

  3. :为什么 n 的类型是 size_t 而不是 intsize_t 是无符号数,能表示更大的内存范围(比如 32 位系统最大是 4GB),且避免 n 为负数时的循环错误。

六、总结:重写 memcpy () 的核心步骤

  1. 空指针检查:用 assert 或条件判断,避免操作空指针;
  2. 类型转换:将 void* 转为 char*,按字节复制;
  3. 判断重叠:目标在源中间时,从右到左复制;
  4. 循环复制:根据重叠情况,逐字节复制数据;
  5. 返回地址:返回原始的 dest,符合标准。

记住这 5 步,就能写出一个安全、正确的 memcpy() 实现啦!

posted on 2025-11-07 22:46  blfbuaa  阅读(51)  评论(0)    收藏  举报