📢 专栏持续更新中!关注博主不迷路,跟着专栏系统学C语言底层开发,从语法入门到工程实战,逐章拆解,保姆级讲解,刚入门的同学跟着学,全程零压力~ 上一节我们掌握了断言库的用法,assert()在运行时捕获逻辑错误,_Static_assert在编译期就把隐患揪出来,调试效率大幅提升。

今天这一节,我们聚焦两个在 C 语言中极其高频的内存操作函数——memcpy()memmove()。它们都定义在 <string.h> 头文件中,作用都是从一块内存拷贝数据到另一块内存。很多新手分不清它们有什么区别、什么时候该用哪个、那个“重叠”到底是什么意思,甚至有人一直用 memcpy 从没出过事,就以为两个函数完全一样。

本章就把这两个函数彻底讲透:从为什么需要它们、函数的原型拆解,到那个让无数新手困惑的“内存重叠”问题的本质,再到实战中如何正确选择。全程配可直接复制运行的代码,刚入门的同学跟着做,就能彻底掌握。

本文默认使用 Visual Studio(Windows)作为演示环境,代码可直接运行。

本章核心知识点梳理(提前划重点,方便后续对照学习):

  1. 为什么需要 memcpymemmove:不能用赋值语句拷贝整个数组,用循环效率低,这两个函数一步到位;
  2. 函数原型彻底拆解void * 参数的妙用、size_t n 的含义(字节数≠元素个数)、restrict 关键字的作用;
  3. memcpy vs memmove 的核心区别:内存重叠时的行为——memcpy 假设不重叠(后果未定义),memmove 能正确处理重叠;
  4. 实战避坑:如何判断内存是否重叠、为什么 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() 可以一次性搞定;但对于 任意类型 的数组(intdouble、结构体等),就需要 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 *,无需强制类型转换。这精妙的设计让 memcpymemmove 可以处理任意类型的数据:

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引入),它的意思是:程序员向编译器承诺,s1s2 指向的两块内存不会重叠。编译器得到这个承诺后,可以生成更优化的拷贝代码(例如使用 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

拆解说明

  • srcdst 是两块完全独立的数组,不存在任何重叠;
  • 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 会像下面这样工作:

  1. 先把源数据拷贝到一个临时缓冲区
  2. 再从临时缓冲区拷到目标位置。

这样就彻底规避了重叠导致的“数据踩踏”问题。也因此,只要你不能 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

拆解说明

  • strstr+2 显然重叠了(都是同一个数组的一部分);
  • 换成 memcpy 就是未定义行为;
  • memmove 保证了内部用的是安全策略,结果确定可靠。

4.3 memmove 的性能代价

memmovememcpy 慢吗?理论上,因为要中转临时缓冲区,会慢一些。但在现代编译器和硬件下,对于小数据量这个差距几乎可以忽略不计。除非你在大数据量、极高频调用的场景下经过实际剖析确认瓶颈,否则首选安全总是没错的

五、memcpy vs memmove:核心区别与选择策略

5.1 一张表彻底分清

区别维度 memcpy memmove
restrict 关键字 有,承诺内存不重叠 无,不假定内存不重叠
内存重叠时行为 未定义(可能出错、可能正常) 定义良好,保证正确
实现策略 直接拷贝,最快 内部使用临时缓冲区(或判断方向后直接拷贝)
性能 更快(无额外开销) 略慢(有额外处理)
适用场景 两块独立不重叠的内存 不确定是否重叠,或明确有重叠

5.2 选择口诀

不知道有没有重叠,就用 memmove;确定不重叠,用 memcpy 更快。

六、本章总结(新手必看,快速掌握核心)

核心知识点 一句话总结
为什么需要它们 不能用 = 直接赋给数组,这两个函数能一次性拷贝整块内存
参数含义 s1 目标地址、s2 源地址、n 字节数(不是元素个数,记得乘 sizeof
void * 的参数 万能指针,任何类型不用强转就能传,成就了函数的通用性
memcpy 的特点 带了 restrict,假设内存不重叠,重叠会导致未定义行为
memmove 的特点 能安全处理重叠,本质像“备皮→贴回”,可靠但稍慢
选择策略 不能确定是否重叠用 memmove;确定不重叠用 memcpy

入门行动清单

  1. 在自己的代码中用 memcpy 拷贝一个数组,确认用法和 sizeof 的作用;
  2. 写一个示例故意让 memcpy 操作重叠内存(如本章所示),观察在不同编译优化下结果的变化;
  3. memmove 修改该示例,验证结果的可预测性。

到这里,memcpymemmove 的区别已经深深印在脑子里。记住“确定不重叠用 memcpy,不确定就用 memmove”,以后写底层数据搬移的代码时心里就有数了。下一节,我们将继续拆解更多 <string.h> 中的关键函数,跟着专栏稳步推进,把 C 语言的底层能力和工程技巧全部拿下!

👉 关注博主,专栏持续更新,从基础到实战,保姆级讲解 C 语言核心特性和标准库,每一章都有详细示例、避坑指南和实战技巧,让你轻松搞定 C 语言工程开发!

#C语言 #C标准库 #string.h #memcpy #memmove #内存拷贝 #保姆级教程 #新手避坑 #嵌入式开发 #CSDN #C语言实战

🎁欢迎关注公众号,获取更多技术干货!

🚀 C语言宝藏资源包免费送!14 本 C++ 经典书 + 编译工具全家桶 + 高效编程技巧,搭配 C 语言精选书籍、20 + 算法源码 + 项目规范,还有 C51 单片机 400 例实战!从零基础到嵌入式开发全覆盖,学生党、职场人直接抄作业~ 关注文章末尾的博客同名公众号,回复【C 语言】一键解锁全部资源,手慢也有!​
在这里插入图片描述

Logo

更多推荐