59. memcpy 与 memmove:它们有什么区别?
说实话,这个问题我面试过不少人,十有八九都答不全。很多人觉得「不就是内存拷贝嘛,一个快一个慢呗」。嗯,没那么简单。
今天咱们就把这两个函数掰开揉碎,看看它们到底差在哪。
先看原型
#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
void *memmove(void *dest, const void *src, size_t n);
从接口上看,一模一样。都是把 src 开头的 n 个字节拷贝到 dest。返回值也都是目标地址。
那区别在哪?
核心就一句话:memcpy 不处理内存重叠,memmove 处理。
什么叫内存重叠?
举个例子。你有一个数组 char buf[10] = "Hello",你想把 buf[0] 到 buf[4] 这 5 个字节,拷贝到 buf[2] 到 buf[6] 的位置。
这时候 dest 和 src 指向的是同一块内存区域,而且有交叉。这就是内存重叠。
用 memcpy 会出问题吗?
会的。而且我告诉你,在某些情况下它看起来是对的,在某些情况下它直接崩。这恰恰是最坑的地方。
为什么 memcpy 会出问题?
咱们画个图就明白了。
场景:把 buf[0..4] 拷贝到 buf[2..6]
初始状态:buf = [H, e, l, l, o, ?, ?, ?, ?, ?]
我们希望:buf = [H, e, H, e, l, l, o, ?, ?, ?]
memcpy 是怎么干的?它从 src 的第一个字节开始,一个一个往 dest 里搬。
第一步:把 buf[0] 的 'H' 拷贝到 buf[2]。没问题。
第二步:把 buf[1] 的 'e' 拷贝到 buf[3]。没问题。
第三步:把 buf[2] 的 'l' 拷贝到 buf[4]。等等——buf[2] 已经被第一步改成了 'H'!
所以第三步拷贝的是 'H',而不是原来的 'l'。结果就变成了 [H, e, H, e, H, l, o, ...]。
看到了吗?数据被污染了。
memmove 怎么解决的?
memmove 聪明的地方在于:它会判断 dest 和 src 的位置关系。
- 如果 dest 在 src 后面(即 dest > src),它就从后往前拷贝。
- 如果 dest 在 src 前面(即 dest < src),它就从前往后拷贝。
- 如果没有重叠,那就跟 memcpy 一样。
还是刚才那个例子。dest 在 src 后面,memmove 从后往前拷:
第一步:把 buf[4] 的 'o' 拷贝到 buf[6]。没问题。
第二步:把 buf[3] 的 'l' 拷贝到 buf[5]。没问题。
第三步:把 buf[2] 的 'l' 拷贝到 buf[4]。没问题。
第四步:把 buf[1] 的 'e' 拷贝到 buf[3]。没问题。
第五步:把 buf[0] 的 'H' 拷贝到 buf[2]。没问题。
结果正确: [H, e, H, e, l, l, o, ?, ?, ?]。
一张图看懂核心逻辑
实际项目中怎么选?
我个人习惯是:只要不确定有没有重叠,一律用 memmove。
为什么?因为 memmove 的性能损失微乎其微。它只是多了一个判断分支,真正的拷贝操作跟 memcpy 一样快。现代 CPU 的分支预测能力很强,这个判断几乎不花时间。
我在项目中遇到过一件事。有个同事写了一个网络协议栈的缓冲区管理模块,他用 memcpy 来移动数据。测试的时候一切正常,因为测试用例里没有重叠的情况。结果上线后,在某个特定流量模式下,数据包解析全乱了。查了两天才发现是 memcpy 把数据覆盖了。
从那以后,我定了个规矩:除非你能 100% 确定 dest 和 src 没有重叠,否则就用 memmove。
性能对比
| 场景 | memcpy | memmove |
|---|---|---|
| 无重叠 | 快(直接拷贝) | 快(多一次判断,几乎无影响) |
| 有重叠(dest < src) | 可能出错 | 正确(从前往后) |
| 有重叠(dest > src) | 可能出错 | 正确(从后往前) |
| 完全重叠(dest == src) | 无意义 | 无意义(但安全) |
小提示:很多标准库的实现里,memcpy 其实就是调用了 memmove。比如 glibc 在某些平台上,memcpy 直接就是 memmove 的别名。所以别太纠结性能,正确性第一。
避坑指南
我曾经在嵌入式项目里踩过一个坑。当时用 memcpy 拷贝一个结构体数组,结构体里有指针成员。我以为只是拷贝数据,结果指针指向的内容被覆盖了。嗯,这里要注意:memcpy 和 memmove 都是浅拷贝。它们只拷贝指针本身,不拷贝指针指向的内容。
如果你需要深拷贝,得自己写循环,或者用专门的序列化函数。
总结一下
- memcpy:快,但不处理重叠。适合确定没有重叠的场景。
- memmove:稍慢一丁点,但安全。适合不确定是否有重叠的场景。
- 我的建议:默认用 memmove。除非你在写性能极度敏感的内核代码,并且你能证明没有重叠。
说白了,memmove 就是 memcpy 的「安全版」。多花那一个判断分支的时间,换来的是代码的健壮性。这笔账,怎么算都划算。
公众号:蓝海资料掘金营,微信deep3321