前言


memcpy 和 memmove 是 C 标准库里的按字节复制函数,声明在 <string.h> 中;C++ 里对应的头文件是 <cstring>,名字位于 std 命名空间。两者签名几乎一样,区别只有一个:源区域和目标区域重叠时,memcpy 的行为是未定义的,memmove 则保证正确。


这个区别看起来很小,实际影响很大。比如在数组内做「整体左移一位」的操作,源和目标是重叠的,用 memcpy 可能得到正确结果,也可能被优化后的实现破坏——它属于未定义行为,不能靠「我这边跑着没问题」来判断。


另有一个常见的拼写问题:题面里的 string.h 在 C++ 中应写成 <cstring>;而 strcopy 是 strcpy 的常见笔误,标准函数名是 strcpy。本文按正确拼写讲解,重点仍是 memcpy 与 memmove 的对比。


本文讲两者的原型、重叠语义、实现思路、适用类型边界,以及什么时候该改用 std::copy。


一、函数原型与重叠语义


// 适用于 C++17
#include <cstring>

void* std::memcpy(void* dest, const void* src, std::size_t count);
void* std::memmove(void* dest, const void* src, std::size_t count);

参数含义:



  • dest:目标缓冲区起始地址。

  • src:源缓冲区起始地址。

  • count:要复制的字节数,不是元素个数。


两者都返回 dest,便于链式调用或作为表达式的一部分。


count 的单位是字节,这是最常见的错误来源。复制一个 int 数组,count 应该是 n * sizeof(int),不是 n。


1.1 重叠语义


标准对 memcpy 的要求是:如果 src 和 dest 指向的对象有重叠,行为未定义。这里的「重叠」指两块内存区域有交集,不要求起始地址相同。


// 适用于 C++17
char buf[10] = "abcdefghi";
std::memcpy(buf + 1, buf, 5); // 区域重叠,UB

memmove 没有这个限制。它保证:即使源和目标重叠,复制完成后 dest 指向的区域内容,等同于先把源区域复制到临时缓冲区、再写入目标的结果。


// 适用于 C++17
char buf[10] = "abcdefghi";
std::memmove(buf + 1, buf, 5); // 安全

为什么 memcpy 不允许重叠?因为它可以采用「按块并行复制」等优化,复制顺序不保证。如果目标在源之后且重叠,先写目标可能覆盖还没读的源数据。


二、memmove 的实现思路


一个可移植的实现需要判断方向:


// 适用于 C++17
#include <cstddef>
#include <cstdint>

void* my_memmove(void* dest, const void* src, std::size_t count) {
auto* d = static_cast<unsigned char*>(dest);
const auto* s = static_cast<const unsigned char*>(src);

if (d == s || count == 0) {
return dest;
}

const auto daddr = reinterpret_cast<std::uintptr_t>(d);
const auto saddr = reinterpret_cast<std::uintptr_t>(s);

if (daddr < saddr) {
// 目标在源之前,从前向后复制不会覆盖未读数据
for (std::size_t i = 0; i < count; ++i) {
d[i] = s[i];
}
} else {
// 目标在源之后,从后向前复制
for (std::size_t i = count; i > 0; --i) {
d[i - 1] = s[i - 1];
}
}
return dest;
}

这里用 std::uintptr_t 比较地址,而不是直接比较 d < s。直接比较两个指向不同对象的指针,结果在标准里是未指定的(unspecified),不保证有全序。转成整数再比较是常见做法,但要注意指针转整数本身是实现定义的,跨平台代码应谨慎。实际的标准库实现会使用编译器内建函数,不受这个限制。


d == s 时直接返回,避免无意义的复制。count == 0 也直接返回,虽然标准对零长度复制的指针有效性表述比较微妙,提前返回是稳妥的。


unsigned char 是按字节访问对象表示(object representation)的标准类型,用 char* 也可以,但 unsigned char 语义更明确。


三、适用类型与 std::copy


两个函数都只做「按字节复制」,不调用任何构造函数、不处理指针成员、不维护引用计数。因此它们只适用于可平凡复制(trivially copyable)的类型:



  • 内置类型、POD 结构体、C 风格数组。

  • 不含虚函数、不含自定义拷贝构造/析构的简单结构。

  • 不含 std::string、std::vector、std::shared_ptr 等管理资源的成员。


对 std::string 用 memcpy,复制的是对象内部的指针和大小字段,两个 string 对象会指向同一块堆内存,析构时重复释放,是未定义行为。


// 适用于 C++17
std::string a = "hello";
std::string b;
std::memcpy(&b, &a, sizeof(std::string)); // 错误:浅拷贝 + 双重释放

正确做法是直接用赋值 b = a;,它调用 std::string 的拷贝赋值,做深拷贝。




场景推荐



复制 POD 数组memcpy

区间可能重叠memmove

复制类类型对象拷贝构造 / 赋值

复制容器元素到另一容器std::copy

重叠区间但类型已知std::copy_backward



3.1 与 std::copy 的关系


C++ 的 std::copy(first, last, dest) 按元素复制,适用于任意可拷贝类型,并且会正确调用拷贝赋值。但 std::copy 对重叠区间同样是未定义的——它按从前往后的顺序复制。


如果目标区间在源区间之后且重叠,用 std::copy_backward(first, last, destEnd),它从后往前复制,语义与 memmove 的后向分支一致。


// 适用于 C++17
#include <algorithm>
#include <vector>

std::vector<int> v{1, 2, 3, 4, 5};
// 把前 4 个元素整体右移一位
std::copy_backward(v.begin(), v.begin() + 4, v.begin() + 5);

四、安全性


这两个函数都不做边界检查,调用方必须保证:



  1. dest 指向的缓冲区至少能容纳 count 字节。

  2. src 指向的区域至少有 count 字节可读。

  3. 两个指针非空(除非 count 为 0,但标准对此表述不明确,最好避免)。


任何一条不满足,都是未定义行为,可能表现为越界读写、崩溃或静默的数据损坏。安全敏感的场景应使用带边界检查的接口,或者在调用前显式校验长度。编译器的 _FORTIFY_SOURCE 等加固选项能在部分情况下捕获已知大小的缓冲区溢出,但不能替代代码层面的检查。


完整示例


// 适用于 C++17
#include <cstddef>
#include <cstring>
#include <iostream>

struct Record {
int id;
double value;
};

int main() {
// 1. 不重叠:memcpy 复制结构体数组
Record src[3] = {{1, 1.5}, {2, 2.5}, {3, 3.5}};
Record dst[3];
std::memcpy(dst, src, sizeof(src));

for (const auto& r : dst) {
std::cout << r.id << ':' << r.value << ' ';
}
std::cout << '\n';

// 2. 重叠:数组内左移一位,必须用 memmove
int arr[6] = {1, 2, 3, 4, 5, 6};
std::memmove(arr, arr + 1, 5 * sizeof(int));
for (int x : arr) {
std::cout << x << ' ';
}
std::cout << '\n';

// 3. 重叠:右移一位
int arr2[6] = {1, 2, 3, 4, 5, 6};
std::memmove(arr2 + 1, arr2, 5 * sizeof(int));
for (int x : arr2) {
std::cout << x << ' ';
}
std::cout << '\n';

return 0;
}

按标准语义,第一行输出三个记录的 id 和 value;第二行是 2 3 4 5 6 6(左移后最后一个元素保持原值);第三行是 1 1 2 3 4 5(右移后第一个元素保持原值)。


注意 sizeof(src) 得到的是整个数组的字节数,这是 sizeof 作用于数组名时的正确用法。如果 src 作为参数传给函数,它会退化为指针,此时 sizeof(src) 得到的是指针大小,这是另一个常见错误。


常见坑点



  1. ❌ 重叠区域用 memcpy:


``cpp std::memcpy(buf + 1, buf, 5); // UB ` ✅ 用 std::memmove(buf + 1, buf, 5);`



  1. ❌ count 传元素个数而不是字节数:


``cpp std::memcpy(dst, src, 10); // 只复制 10 字节 ` ✅ std::memcpy(dst, src, 10 * sizeof(int));`



  1. ❌ 对 std::string 等非平凡类型用 memcpy:


``cpp std::memcpy(&b, &a, sizeof(std::string)); `` ✅ 用拷贝构造或赋值。



  1. ❌ 对数组名做 sizeof 却以为在函数里也有效:


``cpp void f(int a[]) { std::memcpy(dst, a, sizeof(a)); } // sizeof 是指针大小 ` ✅ 显式传长度,或改用 std::array / std::vector`。



  1. ❌ 复制 C 字符串忘记包含结尾的 '\0':


``cpp std::memcpy(dst, src, std::strlen(src)); // 缺 1 字节 ` ✅ std::strlen(src) + 1,或直接用 strcpy`。



  1. ❌ 目标缓冲区不够大:


``cpp char dst[4]; std::memcpy(dst, src, 8); // 越界写 ` ✅ 调用前校验 count <= sizeof(dst)`。



  1. ❌ 用 memcpy 复制多态对象:


``cpp Base b; std::memcpy(&b, &derived, sizeof(Base)); // 切片 + 破坏 vptr ` ✅ 用虚函数 clone()` 或智能指针做多态复制。



  1. ❌ 认为 memmove 一定比 memcpy 慢很多。


✅ 两者在无重叠时的实现可能相同;按语义选函数,不要为未测量的性能差异牺牲正确性。


总结




维度memcpymemmove



重叠区域未定义行为安全

复制方向不保证按重叠情况选择

适用类型可平凡复制可平凡复制

典型用途不重叠的块复制数组内移动、缓冲区搬移

头文件<cstring><cstring>



选择规则只有一条:源和目标可能重叠就用 memmove,确定不重叠才用 memcpy。如果拿不准是否重叠,用 memmove 是安全的默认。再记住「字节数」和「只用于平凡类型」这两条边界,这两个函数就不会出错。




Logo

更多推荐