17.12【保姆级教程】内存拷贝函数:memcpy 和 memmove,新手必知的核心区别
这篇文章深入讲解了C语言中两个关键内存操作函数memcpy()和memmove()的区别与使用场景。主要内容包括:1) 它们解决了数组不能直接赋值的问题,提供高效的内存拷贝;2) 详细解析函数原型,包括void*指针的通用性和size_t参数的正确用法;3) 核心区别在于处理内存重叠时的行为——memcpy假设内存不重叠(否则行为未定义),而memmove能安全处理重叠情况;4) 通过实际代码演示
📢 专栏持续更新中!关注博主不迷路,跟着专栏系统学C语言底层开发,从语法入门到工程实战,逐章拆解,保姆级讲解,刚入门的同学跟着学,全程零压力~ 上一节我们掌握了断言库的用法,
assert()在运行时捕获逻辑错误,_Static_assert在编译期就把隐患揪出来,调试效率大幅提升。
今天这一节,我们聚焦两个在 C 语言中极其高频的内存操作函数——memcpy() 和 memmove()。它们都定义在 <string.h> 头文件中,作用都是从一块内存拷贝数据到另一块内存。很多新手分不清它们有什么区别、什么时候该用哪个、那个“重叠”到底是什么意思,甚至有人一直用 memcpy 从没出过事,就以为两个函数完全一样。
本章就把这两个函数彻底讲透:从为什么需要它们、函数的原型拆解,到那个让无数新手困惑的“内存重叠”问题的本质,再到实战中如何正确选择。全程配可直接复制运行的代码,刚入门的同学跟着做,就能彻底掌握。
本文默认使用 Visual Studio(Windows)作为演示环境,代码可直接运行。
本章核心知识点梳理(提前划重点,方便后续对照学习):
- 为什么需要
memcpy和memmove:不能用赋值语句拷贝整个数组,用循环效率低,这两个函数一步到位; - 函数原型彻底拆解:
void *参数的妙用、size_t n的含义(字节数≠元素个数)、restrict关键字的作用; memcpyvsmemmove的核心区别:内存重叠时的行为——memcpy假设不重叠(后果未定义),memmove能正确处理重叠;- 实战避坑:如何判断内存是否重叠、为什么
memmove更像“安全版”。
💡 提示:学习本章前,建议你已经熟练掌握指针和数组的基本概念。本章涉及的内存操作是底层开发、嵌入式编程和性能优化中必备的技能。
一、为什么需要 memcpy 和 memmove?
1.1 数组不能直接赋值,循环拷贝又太麻烦
在 C 语言中,有一个让新手很意外的事实:不能把一个数组直接用 = 赋给另一个数组。
int a[] = {1, 2, 3, 4, 5};
int b[5];
// ❌ 错误:数组名是常量指针,不能放在等号左边
b = a; // 编译错误!
要拷贝数组内容,传统做法是用循环逐个元素赋值:
int a[] = {1, 2, 3, 4, 5};
int b[5];
// ✅ 笨办法:循环逐个拷贝
for (int i = 0; i < 5; i++) {
b[i] = a[i];
}
这个循环对付小数组还行,但如果要处理成千上万个元素,或者要频繁调用,性能就堪忧了。对于字符数组,我们已经有 strcpy() 和 strncpy() 可以一次性搞定;但对于 任意类型 的数组(int、double、结构体等),就需要 memcpy() 和 memmove() 出马了——它们能一次性把整块内存的数据拷过去,既简洁又高效。
1.2 两个函数的原型(先混个脸熟)
#include <string.h>
void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
void *memmove(void *s1, const void *s2, size_t n);
看到这两个原型,你可能已经晕了——怎么全是 void *?那个 restrict 又是什么?别急,下面逐一拆解。
二、彻底拆解函数原型(新手也能看懂)
2.1 参数的含义
两个函数的参数结构完全一致:
| 参数 | 含义 | 新手理解 |
|---|---|---|
s1 |
目标内存的起始地址 | “把数据拷到哪儿去” |
s2 |
源数据内存的起始地址 | “数据从哪儿来” |
n |
要拷贝的字节数(不是元素个数!) | “拷多少字节” |
关键点:第三个参数 n 是字节数,不是元素个数!如果要拷贝一个有 10 个 double 类型元素的数组,n 应该写 10 * sizeof(double),而不是 10。
double src[10] = {1.1, 2.2, ...};
double dst[10];
// ❌ 错误:只拷了 10 个字节,相当于只拷了 10/8 ≈ 1 个完整的 double 元素
memcpy(dst, src, 10);
// ✅ 正确:拷了 10 * sizeof(double) = 80 个字节,10 个元素整整齐齐
memcpy(dst, src, 10 * sizeof(double));
记忆口诀:只要是拷贝数组,永远在 n 的位置写 元素个数 × sizeof(元素类型)。
2.2 为什么参数是 void *?
void * 是 C 语言中的“万能指针”——任何类型的指针都可以直接赋给 void *,无需强制类型转换。这精妙的设计让 memcpy 和 memmove 可以处理任意类型的数据:
int arr1[5], arr2[5];
double d1[3], d2[3];
struct Student { char name[20]; int score; } s1[10], s2[10];
// 全都可以直接用 memcpy,不用做任何类型转换
memcpy(arr2, arr1, 5 * sizeof(int)); // 拷贝 int 数组
memcpy(d2, d1, 3 * sizeof(double)); // 拷贝 double 数组
memcpy(s2, s1, 10 * sizeof(struct Student)); // 拷贝结构体数组
2.3 关于 restrict 关键字(memcpy 独有)
memcpy 的第二个和第三个参数前面有 restrict 关键字(C99引入),它的意思是:程序员向编译器承诺,s1 和 s2 指向的两块内存不会重叠。编译器得到这个承诺后,可以生成更优化的拷贝代码(例如使用 SIMD 指令一次拷贝 16 字节甚至更多)。
而 memmove 没有 restrict,所以它不能假定内存不重叠,内部实现会像“先把源数据拷贝到临时缓冲区,再从缓冲区拷到目标”,确保在重叠场景下也能正确工作——当然,这会比 memcpy 稍慢一点点。
这就是二者最本质的区别。
三、memcpy:快速但“有前提”的拷贝
3.1 基本用法(内存不重叠时)
当你可以确定源和目标内存完全不重叠时,memcpy 是首选。大多数数组拷贝都属于这种情况。
#include <stdio.h>
#include <string.h>
int main(void) {
int src[] = {1, 2, 3, 4, 5};
int dst[5];
// 拷贝整个数组
memcpy(dst, src, 5 * sizeof(int));
printf("源数组:");
for (int i = 0; i < 5; i++) printf("%d ", src[i]);
printf("\n目标数组:");
for (int i = 0; i < 5; i++) printf("%d ", dst[i]);
printf("\n");
return 0;
}
运行结果:
源数组:1 2 3 4 5
目标数组:1 2 3 4 5
拆解说明:
src和dst是两块完全独立的数组,不存在任何重叠;memcpy以最快的速度把 5 个int(共 20 字节)的数据直接搬过去;- 返回值是
dst的地址,但大多数情况下我们用不上,直接忽略即可。
3.2 如果内存重叠了会怎样?(行为未定义)
#include <stdio.h>
#include <string.h>
int main(void) {
char str[] = "abcdefg";
printf("拷贝前:%s\n", str);
// ❌ 危险:源和目标有重叠(从 str+2 拷到 str,重叠区域不可控)
memcpy(str, str + 2, 4);
// 期望结果变成 "cdefefg" 吗?不一定!
printf("拷贝后:%s\n", str);
return 0;
}
可能的运行结果(也许是 "cdefg",也许是 "cdefefg",甚至可能崩溃)——完全取决于编译器实现和优化等级。
这正是 memcpy 的“未定义行为”区域:一旦两块内存重叠,标准不保证任何事,程序可能看起来“正常”,也可能悄悄出错。作为程序员,有责任确保传给 memcpy 的地址不重叠。
四、memmove:安全处理重叠的拷贝
4.1 memmove 如何解决重叠问题?
当源和目标内存有重叠时,memmove 会像下面这样工作:
- 先把源数据拷贝到一个临时缓冲区;
- 再从临时缓冲区拷到目标位置。
这样就彻底规避了重叠导致的“数据踩踏”问题。也因此,只要你不能 100% 确定两块内存不重叠,就用 memmove。
4.2 最常见的重叠场景:同一数组内移动数据
假设你有一个字符数组,想把其中一部分字符向前或向后移动几个位置——这种操作在字符串修剪、删除/插入元素时经常出现。
#include <stdio.h>
#include <string.h>
int main(void) {
char str[] = "abcdefg";
printf("移动前:%s\n", str);
// ✅ 使用 memmove 安全地在重叠区域内移动
// 把 str+2 开始的 4 个字符 "cdef" 移到 str 开始处
memmove(str, str + 2, 4);
// 结果确定是 "cdefefg"
printf("移动后:%s\n", str);
return 0;
}
运行结果(可靠的):
移动前:abcdefg
移动后:cdefefg
拆解说明:
str和str+2显然重叠了(都是同一个数组的一部分);- 换成
memcpy就是未定义行为; memmove保证了内部用的是安全策略,结果确定可靠。
4.3 memmove 的性能代价
memmove 比 memcpy 慢吗?理论上,因为要中转临时缓冲区,会慢一些。但在现代编译器和硬件下,对于小数据量这个差距几乎可以忽略不计。除非你在大数据量、极高频调用的场景下经过实际剖析确认瓶颈,否则首选安全总是没错的。
五、memcpy vs memmove:核心区别与选择策略
5.1 一张表彻底分清
| 区别维度 | memcpy |
memmove |
|---|---|---|
restrict 关键字 |
有,承诺内存不重叠 | 无,不假定内存不重叠 |
| 内存重叠时行为 | 未定义(可能出错、可能正常) | 定义良好,保证正确 |
| 实现策略 | 直接拷贝,最快 | 内部使用临时缓冲区(或判断方向后直接拷贝) |
| 性能 | 更快(无额外开销) | 略慢(有额外处理) |
| 适用场景 | 两块独立不重叠的内存 | 不确定是否重叠,或明确有重叠时 |
5.2 选择口诀
不知道有没有重叠,就用
memmove;确定不重叠,用memcpy更快。
六、本章总结(新手必看,快速掌握核心)
| 核心知识点 | 一句话总结 |
|---|---|
| 为什么需要它们 | 不能用 = 直接赋给数组,这两个函数能一次性拷贝整块内存 |
| 参数含义 | s1 目标地址、s2 源地址、n 字节数(不是元素个数,记得乘 sizeof) |
void * 的参数 |
万能指针,任何类型不用强转就能传,成就了函数的通用性 |
memcpy 的特点 |
带了 restrict,假设内存不重叠,重叠会导致未定义行为 |
memmove 的特点 |
能安全处理重叠,本质像“备皮→贴回”,可靠但稍慢 |
| 选择策略 | 不能确定是否重叠用 memmove;确定不重叠用 memcpy |
✅ 入门行动清单:
- 在自己的代码中用
memcpy拷贝一个数组,确认用法和sizeof的作用; - 写一个示例故意让
memcpy操作重叠内存(如本章所示),观察在不同编译优化下结果的变化; - 用
memmove修改该示例,验证结果的可预测性。
到这里,memcpy 和 memmove 的区别已经深深印在脑子里。记住“确定不重叠用 memcpy,不确定就用 memmove”,以后写底层数据搬移的代码时心里就有数了。下一节,我们将继续拆解更多 <string.h> 中的关键函数,跟着专栏稳步推进,把 C 语言的底层能力和工程技巧全部拿下!
👉 关注博主,专栏持续更新,从基础到实战,保姆级讲解 C 语言核心特性和标准库,每一章都有详细示例、避坑指南和实战技巧,让你轻松搞定 C 语言工程开发!
#C语言 #C标准库 #string.h #memcpy #memmove #内存拷贝 #保姆级教程 #新手避坑 #嵌入式开发 #CSDN #C语言实战
🎁欢迎关注公众号,获取更多技术干货!
🚀 C语言宝藏资源包免费送!14 本 C++ 经典书 + 编译工具全家桶 + 高效编程技巧,搭配 C 语言精选书籍、20 + 算法源码 + 项目规范,还有 C51 单片机 400 例实战!从零基础到嵌入式开发全覆盖,学生党、职场人直接抄作业~ 关注文章末尾的博客同名公众号,回复【C 语言】一键解锁全部资源,手慢也有!
更多推荐




所有评论(0)