☰
C/C++ string.h库中memcpy()和memmove()的使用
2026/10/26 11:28:49 网站建设 项目流程

前言


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是安全的默认。再记住「字节数」和「只用于平凡类型」这两条边界,这两个函数就不会出错。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询