前言
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); // 区域重叠,UBmemmove没有这个限制。它保证:即使源和目标重叠,复制完成后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);四、安全性
这两个函数都不做边界检查,调用方必须保证:
dest指向的缓冲区至少能容纳count字节。src指向的区域至少有count字节可读。- 两个指针非空(除非
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)得到的是指针大小,这是另一个常见错误。
常见坑点
- ❌ 重叠区域用
memcpy:
``cpp std::memcpy(buf + 1, buf, 5); // UB`✅ 用std::memmove(buf + 1, buf, 5);`
- ❌
count传元素个数而不是字节数:
``cpp std::memcpy(dst, src, 10); // 只复制 10 字节`✅std::memcpy(dst, src, 10 * sizeof(int));`
- ❌ 对
std::string等非平凡类型用memcpy:
``cpp std::memcpy(&b, &a, sizeof(std::string));`` ✅ 用拷贝构造或赋值。
- ❌ 对数组名做
sizeof却以为在函数里也有效:
``cpp void f(int a[]) { std::memcpy(dst, a, sizeof(a)); } // sizeof 是指针大小`✅ 显式传长度,或改用std::array/std::vector`。
- ❌ 复制 C 字符串忘记包含结尾的
'\0':
``cpp std::memcpy(dst, src, std::strlen(src)); // 缺 1 字节`✅std::strlen(src) + 1,或直接用strcpy`。
- ❌ 目标缓冲区不够大:
``cpp char dst[4]; std::memcpy(dst, src, 8); // 越界写`✅ 调用前校验count <= sizeof(dst)`。
- ❌ 用
memcpy复制多态对象:
``cpp Base b; std::memcpy(&b, &derived, sizeof(Base)); // 切片 + 破坏 vptr`✅ 用虚函数clone()` 或智能指针做多态复制。
- ❌ 认为
memmove一定比memcpy慢很多。
✅ 两者在无重叠时的实现可能相同;按语义选函数,不要为未测量的性能差异牺牲正确性。
总结
| 维度 | memcpy | memmove |
|---|
| 重叠区域 | 未定义行为 | 安全 |
| 复制方向 | 不保证 | 按重叠情况选择 |
| 适用类型 | 可平凡复制 | 可平凡复制 |
| 典型用途 | 不重叠的块复制 | 数组内移动、缓冲区搬移 |
| 头文件 | <cstring> | <cstring> |
选择规则只有一条:源和目标可能重叠就用memmove,确定不重叠才用memcpy。如果拿不准是否重叠,用memmove是安全的默认。再记住「字节数」和「只用于平凡类型」这两条边界,这两个函数就不会出错。