资讯动态

C/C++ string.h库中memcpy()和memmove()的使用

发布时间:2026/10/26 11:28:54 来源:尧图企业网站定制
前言memcpy和memmove是 C 标准库里的按字节复制函数声明在string.h中C 里对应的头文件是cstring名字位于std命名空间。两者签名几乎一样区别只有一个源区域和目标区域重叠时memcpy的行为是未定义的memmove则保证正确。这个区别看起来很小实际影响很大。比如在数组内做「整体左移一位」的操作源和目标是重叠的用memcpy可能得到正确结果也可能被优化后的实现破坏——它属于未定义行为不能靠「我这边跑着没问题」来判断。另有一个常见的拼写问题题面里的string.h在 C 中应写成cstring而strcopy是strcpy的常见笔误标准函数名是strcpy。本文按正确拼写讲解重点仍是memcpy与memmove的对比。本文讲两者的原型、重叠语义、实现思路、适用类型边界以及什么时候该改用std::copy。一、函数原型与重叠语义// 适用于 C17#include cstringvoid* 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指向的对象有重叠行为未定义。这里的「重叠」指两块内存区域有交集不要求起始地址相同。// 适用于 C17char buf[10] abcdefghi;std::memcpy(buf 1, buf, 5); // 区域重叠UBmemmove没有这个限制。它保证即使源和目标重叠复制完成后dest指向的区域内容等同于先把源区域复制到临时缓冲区、再写入目标的结果。// 适用于 C17char buf[10] abcdefghi;std::memmove(buf 1, buf, 5); // 安全为什么memcpy不允许重叠因为它可以采用「按块并行复制」等优化复制顺序不保证。如果目标在源之后且重叠先写目标可能覆盖还没读的源数据。二、memmove 的实现思路一个可移植的实现需要判断方向// 适用于 C17#include cstddef#include cstdintvoid* my_memmove(void* dest, const void* src, std::size_t count) {auto* d static_castunsigned char*(dest);const auto* s static_castconst unsigned char*(src);if (d s || count 0) {return dest;}const auto daddr reinterpret_caststd::uintptr_t(d);const auto saddr reinterpret_caststd::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对象会指向同一块堆内存析构时重复释放是未定义行为。// 适用于 C17std::string a hello;std::string b;std::memcpy(b, a, sizeof(std::string)); // 错误浅拷贝 双重释放正确做法是直接用赋值b a;它调用std::string的拷贝赋值做深拷贝。场景推荐复制 POD 数组memcpy区间可能重叠memmove复制类类型对象拷贝构造 / 赋值复制容器元素到另一容器std::copy重叠区间但类型已知std::copy_backward3.1 与 std::copy 的关系C 的std::copy(first, last, dest)按元素复制适用于任意可拷贝类型并且会正确调用拷贝赋值。但std::copy对重叠区间同样是未定义的——它按从前往后的顺序复制。如果目标区间在源区间之后且重叠用std::copy_backward(first, last, destEnd)它从后往前复制语义与memmove的后向分支一致。// 适用于 C17#include algorithm#include vectorstd::vectorint 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等加固选项能在部分情况下捕获已知大小的缓冲区溢出但不能替代代码层面的检查。完整示例// 适用于 C17#include cstddef#include cstring#include iostreamstruct 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. 重叠数组内左移一位必须用 memmoveint 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)得到的是指针大小这是另一个常见错误。常见坑点❌ 重叠区域用memcpycpp 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等非平凡类型用memcpycpp 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 字符串忘记包含结尾的\0cpp 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慢很多。✅ 两者在无重叠时的实现可能相同按语义选函数不要为未测量的性能差异牺牲正确性。总结维度memcpymemmove重叠区域未定义行为安全复制方向不保证按重叠情况选择适用类型可平凡复制可平凡复制典型用途不重叠的块复制数组内移动、缓冲区搬移头文件cstringcstring选择规则只有一条源和目标可能重叠就用memmove确定不重叠才用memcpy。如果拿不准是否重叠用memmove是安全的默认。再记住「字节数」和「只用于平凡类型」这两条边界这两个函数就不会出错。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑