完整教程:嵌入式软件工程师面经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 = arr,dest = arr+2(目标在源的右边,复制时会覆盖还没读的源数据) - 场景 2:
src = arr+2,dest = 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;
}
为什么会错?
按「从左到右」复制(低地址→高地址)时:
- 先把
arr[0]('1')复制到arr[2]→ arr 变成 "121456789" - 再把
arr[1]('2')复制到arr[3]→ arr 变成 "121256789" - 接着把
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 () 相关问题
问:为什么
memcpy()的参数用void*?答:void*是通用指针,可以接受任何类型的地址(比如int*、char*、struct*),实现 “复制任意类型数据” 的功能。问:内存重叠时,
memcpy()和memmove()有什么区别?答:memcpy()不处理重叠,结果不确定;memmove()会判断重叠方向,选择从左到右或从右到左复制,结果确定。问:为什么
n的类型是size_t而不是int?答:size_t是无符号数,能表示更大的内存范围(比如 32 位系统最大是 4GB),且避免n为负数时的循环错误。
六、总结:重写 memcpy () 的核心步骤
- 空指针检查:用
assert或条件判断,避免操作空指针; - 类型转换:将
void*转为char*,按字节复制; - 判断重叠:目标在源中间时,从右到左复制;
- 循环复制:根据重叠情况,逐字节复制数据;
- 返回地址:返回原始的
dest,符合标准。
记住这 5 步,就能写出一个安全、正确的 memcpy() 实现啦!
浙公网安备 33010602011771号