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] 的位置。

这时候 destsrc 指向的是同一块内存区域,而且有交叉。这就是内存重叠。

用 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, ?, ?, ?]。

一张图看懂核心逻辑

memcpy vs memmove 决策流程 开始内存拷贝 内存是否重叠? memcpy 直接拷贝 dest > src? (目标在源后面) memmove 从后往前拷贝 memmove 从前往后拷贝 拷贝完成

实际项目中怎么选?

我个人习惯是:只要不确定有没有重叠,一律用 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