跳到主要内容 C++中 memcpy 和赋值拷贝的核心区别 | 极客日志
C++ 算法
C++中 memcpy 和赋值拷贝的核心区别 本文对比了 C++ 中 memcpy 与赋值拷贝的区别。memcpy 是内存层面的二进制逐字节拷贝,适用于平凡可拷贝类型,但存在安全风险;赋值拷贝是语法层面的逻辑拷贝,支持内置及自定义类型,更安全。建议优先使用赋值拷贝,仅在特定性能场景下使用 memcpy,避免在非平凡可拷贝类型上使用导致崩溃。
C++中 memcpy 和赋值拷贝的核心区别
简单来说,二者是完全不同层面的拷贝方式:memcpy 是内存层面的二进制逐字节拷贝,属于 C 语言的底层内存操作;赋值拷贝是 C++ 语法层面的拷贝,会根据数据类型触发对应的拷贝逻辑(内置类型直接拷贝值,自定义类型调用拷贝构造/赋值运算符)。
二者的核心差异体现在拷贝逻辑、适用类型、安全性、面向的编程范式上。memcpy 更偏向'裸内存操作',灵活但危险;赋值拷贝是 C++ 的原生语法,贴合面向对象特性,安全且符合语言规范。下面从核心区别、适用场景、安全性、性能、典型示例五个维度讲透,同时明确哪些场景绝对不能用 memcpy(新手最容易踩坑的点)。
一、核心底层逻辑(最本质区别)
先从底层原理理解二者的不同,这是所有差异的根源:
1. memcpy:二进制逐字节'硬拷贝'
是 C 标准库 中的函数,函数原型:
memcpy
<cstring>
void * memcpy (void * dest, const void * src, size_t n) ;
核心行为 :从 src 指向的内存地址开始,逐字节把 n 个字节的二进制数据,复制到 dest 指向的内存地址,完全不关心内存中存储的是什么数据类型;
操作层面 :直接操作内存地址和二进制流,属于无类型的底层内存操作,编译器不会做任何类型检查或逻辑处理;
本质 :把一块内存的'二进制快照'原封不动复制到另一块内存。
2. 赋值拷贝:语法层面的'逻辑拷贝' 赋值拷贝分两种场景(本质都是 C++ 语法的原生行为):
内置类型 (int/char/float/指针等):直接值拷贝(底层也是逐字节拷贝,但由编译器自动完成,封装了内存操作);
自定义类型 (class/struct):触发 C++ 的拷贝构造函数(初始化时,如 A a = b;)或赋值运算符重载(已初始化的赋值,如 a = b;),执行程序员定义的逻辑拷贝(可深拷贝、浅拷贝,或自定义其他逻辑)。
操作层面 :面向数据类型/对象,编译器会根据变量的类型,自动选择对应的拷贝逻辑,属于 C++ 面向对象范式的一部分;
本质 :按'类型规则'拷贝数据,而非单纯的内存二进制拷贝。
二、核心区别对比表 为了直观理解,用表格总结二者在所有关键维度的差异,这是实际开发中选择的核心依据:
对比维度 memcpy 赋值拷贝 所属范式 C 语言,底层内存操作 C++ 语法,面向类型/对象 拷贝逻辑 无类型,二进制逐字节拷贝 n 个字节 有类型,内置类型值拷贝/自定义类型调用拷贝函数 适用类型 仅适用于平凡可拷贝类型 所有 C++ 数据类型(内置/自定义) 类型检查 无(void*接收任意指针,编译器不检查) 严格类型检查(类型不匹配编译报错) 安全性 低(手动控制字节数,易越界/浅拷贝坑) 高(编译器兜底,自定义类型可手动控制深/浅拷贝) 是否触发函数 不触发任何构造/析构/重载函数 自定义类型触发拷贝构造/赋值运算符 使用成本 高(需手动计算拷贝字节数 n) 低(编译器自动处理,直接用=) 性能 极致高效(纯内存操作,无额外开销) 内置类型与 memcpy 持平,自定义类型取决于拷贝逻辑 错误来源 手动传参错误(n 过大/过小、地址重叠) 自定义类型的拷贝函数逻辑错误(如浅拷贝导致野指针)
三、关键概念:平凡可拷贝类型(memcpy 的唯一安全适用类型) 上面表格中提到 memcpy 仅适用于平凡可拷贝类型(Trivially Copyable Type),这是 C++ 标准定义的概念,也是新手最容易踩坑的点——非平凡可拷贝类型用 memcpy 拷贝会直接导致程序崩溃/未定义行为。
1. 什么是平凡可拷贝类型?
内置类型 :int/char/short/long/float/double/指针等,都是平凡可拷贝类型;
自定义 struct/class :
没有自定义的拷贝构造函数、赋值运算符、析构函数;
所有成员变量都是平凡可拷贝类型;
没有虚函数/虚基类(虚函数会引入虚函数表指针,memcpy 拷贝会导致虚表指针混乱);
继承体系中的所有基类都是平凡可拷贝类型。
简单来说:没有自定义拷贝/析构逻辑、没有虚函数、成员都是内置类型的简单结构体/类 ,就是平凡可拷贝类型。
2. 非平凡可拷贝类型(绝对不能用 memcpy)
包含动态内存分配的类(如 std::string、std::vector、自定义的链表/树类);
有自定义拷贝构造/赋值运算符/析构函数的类;
包含虚函数/虚基类的类;
嵌套了非平凡可拷贝类型的结构体/类。
四、典型示例:正确使用 vs 错误使用(新手必看) 通过代码示例,直观感受二者的使用场景和错误后果,分为内置类型/简单结构体(memcpy 和赋值拷贝都可用)、自定义复杂类型(仅赋值拷贝可用,memcpy 必错)两个场景。
场景 1:内置类型/简单平凡可拷贝结构体(二者都安全,性能持平) #include <cstring>
#include <iostream>
using namespace std;
struct Point {
int x;
int y;
};
int main () {
int a = 10 , b = 0 ;
b = a;
memcpy (&b, &a, sizeof (int ));
Point p1 = {1 , 2 }, p2 = {0 , 0 };
p2 = p1;
memcpy (&p2, &p1, sizeof (Point));
cout << p2. x << "," << p2. y << endl;
return 0 ;
}
说明 :此场景下,memcpy 和赋值拷贝的效果完全一致,性能几乎没有区别(编译器对赋值拷贝的优化会和 memcpy 持平),实际开发中用赋值拷贝更简洁(无需写 sizeof)。
场景 2:自定义复杂类型(非平凡可拷贝,memcpy 拷贝直接崩溃) 以包含 std::string 的类为例(std::string 是典型的非平凡可拷贝类型,内部有动态内存分配):
#include <cstring>
#include <iostream>
#include <string>
using namespace std;
struct Person {
string name;
int age;
};
int main () {
Person p1 = {"Tom" , 20 }, p2;
p2 = p1;
cout << "赋值拷贝:" << p2. name << "," << p2. age << endl;
Person p3;
memcpy (&p3, &p1, sizeof (Person));
cout << "memcpy 拷贝:" << p3. name << "," << p3. age << endl;
return 0 ;
}
std::string 内部维护了指向堆内存的指针(存储字符串内容)、字符串长度、容量等成员;
memcpy 逐字节拷贝 Person 对象时,只是把 p1.name 中的堆指针值复制到 p3.name,而非复制堆内存中的字符串(浅拷贝);
当 p1/p3 析构时,会两次释放同一块堆内存(double free),直接导致程序崩溃;
而赋值拷贝会触发 std::string 的赋值运算符重载,执行深拷贝(重新分配堆内存,复制字符串内容),避免了浅拷贝问题。
场景 3:含虚函数的类(memcpy 拷贝导致虚表指针混乱) #include <cstring>
#include <iostream>
using namespace std;
class Base {
public :
virtual void show () {
cout << "Base" << endl;
}
int a = 10 ;
};
class Derived : public Base {
public :
void show () override {
cout << "Derived" << endl;
}
int b = 20 ;
};
int main () {
Derived d;
Base b1, b2;
b1 = d;
b1. show ();
memcpy (&b2, &d, sizeof (Base));
b2. show ();
return 0 ;
}
错误原因 :含虚函数的类会有一个虚表指针(vptr),指向类的虚函数表(vtable);memcpy 直接拷贝虚表指针的二进制值,会导致目标对象的虚表指针指向非法地址,调用虚函数时触发未定义行为。
五、性能对比:memcpy 是否一定更快? 很多人认为 memcpy 是底层操作,性能一定比赋值拷贝好,其实大部分场景下二者性能持平,只有超大块连续平凡可拷贝数据时,memcpy 才有微弱优势。
1. 内置类型/简单结构体:性能一致 编译器对 C++ 的赋值拷贝有极致优化(如 GCC/Clang 的 -O2/-O3 优化),会将内置类型的连续赋值拷贝直接优化为底层内存拷贝,和 memcpy 的汇编代码完全一致,性能没有区别。
int arr1[10000 ] = {1 , 2 , 3. ..};
int arr2[10000 ] = {0 };
arr2 = arr1;
memcpy (arr2, arr1, sizeof (arr1));
2. 超大块连续平凡可拷贝数据:memcpy 微优 当拷贝的内存块极大(如 100MB 以上的连续数组),memcpy 的底层实现(通常是汇编优化的块拷贝,如 x86 的 rep movsb 指令)会比编译器自动优化的赋值拷贝略快,因为 memcpy 是专门为内存拷贝设计的函数,无任何额外逻辑。
3. 自定义类型:赋值拷贝的性能取决于拷贝逻辑 如果自定义类型的拷贝构造/赋值运算符是深拷贝(如 std::vector),赋值拷贝的性能远低于 memcpy(但此时 memcpy 不能用,否则崩溃);如果是浅拷贝(平凡可拷贝类型),则和 memcpy 性能持平。
六、实际开发中的选择原则(避坑核心) 开发中到底该用 memcpy 还是赋值拷贝?遵循'能用人赋值拷贝,就不用 memcpy'的原则,仅在特定极致性能场景下使用 memcpy,具体选择规则:
优先使用赋值拷贝 :
适用于所有场景(内置/自定义类型),语法简洁、安全,符合 C++ 面向对象范式;
无需关心字节数、类型是否可拷贝,编译器会兜底做类型检查和逻辑处理;
自定义类型可通过重写拷贝构造/赋值运算符,灵活实现深拷贝/浅拷贝,避免内存问题。
仅在以下场景使用 memcpy :
拷贝的是平凡可拷贝类型(内置类型/简单结构体);
拷贝的是大块连续的内存数据(如超大数组、缓冲区),且对性能有极致要求;
底层 C 风格编程(如操作裸指针、缓冲区、网络数据解析),需要直接操作内存二进制流。
绝对禁止用 memcpy 的场景 :
拷贝非平凡可拷贝类型(含 std::string/std::vector、自定义拷贝逻辑、虚函数的类);
拷贝的内存地址存在重叠(此时应用 memmove,而非 memcpy,memcpy 不处理地址重叠);
无法准确计算拷贝的字节数 n(n 过大导致内存越界,n 过小导致拷贝不完整)。
七、补充:memcpy 与 memmove 的区别(避免地址重叠坑) 新手容易混淆 memcpy 和 memmove,这里简单补充:
memcpy:不处理内存地址重叠 ,如果 dest 和 src 的内存区域有重叠,会导致拷贝数据错乱;
memmove:处理内存地址重叠 ,内部会先把 src 的数据拷贝到临时缓冲区,再复制到 dest,避免错乱;
性能 :memcpy 略快于 memmove(无临时缓冲区开销),无地址重叠时用 memcpy,有重叠时必须用 memmove。
int arr[5 ] = {1 , 2 , 3 , 4 , 5 };
memcpy (arr, arr + 1 , 4 * sizeof (int ));
memmove (arr, arr + 1 , 4 * sizeof (int ));
总结 C++ 中 memcpy 和赋值拷贝的核心区别与使用原则,用 3 句话概括:
本质区别 :memcpy 是 C 语言底层无类型的二进制逐字节拷贝,赋值拷贝是 C++ 语法层面的有类型逻辑拷贝(内置类型值拷贝,自定义类型调用拷贝函数);
安全边界 :memcpy 仅适用于平凡可拷贝类型,非平凡可拷贝类型(含动态内存/虚函数/自定义拷贝逻辑)用 memcpy 会导致崩溃/未定义行为,赋值拷贝则安全适配所有类型;
选择原则 :优先使用赋值拷贝 (简洁、安全、符合 C++ 范式),仅在大块连续平凡可拷贝数据的极致性能场景 下使用 memcpy,绝对避免在非平凡可拷贝类型上使用 memcpy。
新手最核心的避坑点:不要用 memcpy 拷贝 C++ 的自定义对象(尤其是 STL 容器、含虚函数/动态内存的类) ,赋值拷贝才是 C++ 的正确选择。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown 转 HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
HTML 转 Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online