本章目标:理解四大内存函数的底层原理 + 搞懂 memcpy 和 memmove 的本质区别 + 掌握 memset 的所有坑 + 手写模拟实现


引入:字符串函数管字符,内存函数管一切

前面我们学了很多字符串函数——strcpystrcatstrcmpstrlen——它们都有一个共同特点:\0 为结束标志,只管字符数据

但现实编程中,我们经常需要拷贝、设置、比较任意类型的数据:

拷贝一个 int 数组 → 字符串函数管不了
拷贝一个 double 数组 → 字符串函数管不了
拷贝一个自定义结构体 → 字符串函数管不了
把某块内存全部设成 0xAA → 字符串函数管不了

内存函数就是为了这些场景诞生的。

核心思维:内存函数不看数据类型,只看字节。不管你传进来的是 intfloatdouble、还是自定义结构体,在内存里统统都是一串字节。内存函数就是在字节层面做拷贝、设置、比较的工作。


第一部分:memcpy——内存块拷贝

1.1 函数原型逐字拆解

void *memcpy(void *destination, const void *source, size_t num);
参数 含义 注意事项
destination 目标地址(拷贝这里) 必须有足够的空间,否则越界
source 源地址(拷贝这里) 不能为 NULL
num 要拷贝的字节数(字节!不是元素个数! 单位是字节

返回值是 void*(目标起始地址),便于链式调用。

1.2 拷贝 int 数组

#include <stdio.h>
#include <string.h>

int main()
{
    int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int arr2[10] = {0};

    // 拷贝前 5 个 int → 5 × 4 字节 = 20 字节
    memcpy(arr2, arr1, 20);

    for (int i = 0; i < 10; i++)
        printf("%d ", arr2[i]);
    // 输出:1 2 3 4 5 0 0 0 0 0
    return 0;
}

⚠️ 提醒:注意这里传的是 20(字节数),不是 5(元素个数)!很多初学者写成 memcpy(arr2, arr1, 5),结果只拷贝了 5 个字节(相当于只拷贝了 arr1[0] 的前 5 个 bit,数据完全乱掉)。正确写法是 5 * sizeof(int) 或直接写 20(假设 int 是 4 字节)。

1.3 拷贝 double 数组

memcpy 的优势就体现在这里——字符串函数对 double 无能为力,但 memcpy 可以:

#include <stdio.h>
#include <string.h>

int main()
{
    double source[] = {1.5, 2.5, 3.5, 4.5, 5.5};
    double dest[5];

    // 拷贝整个 double 数组(5 × 8 = 40 字节)
    memcpy(dest, source, sizeof(source));

    for (int i = 0; i < 5; i++)
        printf("%.1f ", dest[i]);  // 输出:1.5 2.5 3.5 4.5 5.5
    return 0;
}

1.4 拷贝结构体

#include <stdio.h>
#include <string.h>

struct Stu {
    char name[20];
    int age;
    char sex[5];
};

int main()
{
    struct Stu s1 = {"张三", 20, "男"};
    struct Stu s2;

    // 一次性拷贝整个结构体(20 + 4 + 5 = 29 字节,实际按对齐后的大小)
    memcpy(&s2, &s1, sizeof(struct Stu));

    printf("%s %d %s\n", s2.name, s2.age, s2.sex);  // 输出:张三 20 男
    return 0;
}

💡 实战用法:结构体整体拷贝用 memcpy 非常方便,不用逐个成员赋值。但要确保目标结构体的空间足够。

1.5 内存重叠——memcpy 最危险的坑

什么是内存重叠? 简单说就是:源和目标在内存中是同一块区域的不同位置。

int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// 把 arr[0]~arr[4](5个int = 20字节)拷贝到 arr[2]~arr[6]
memcpy(&arr[2], &arr[0], 5 * sizeof(int));
arr[0] arr[1] arr[2] arr[3] arr[4] arr[5] arr[6] arr[7] arr[8] arr[9]
  1       2       3       4       5       6       7       8       9      10
              ↑───────────────────────────↑
              从这里拷贝到这里(源和目标重叠!)

memcpy 在重叠场景下会发生什么?

memcpy 的实现通常是从源地址逐字节向后拷贝。在重叠情况下,还没被拷贝的源数据,可能已经被刚才拷贝过去的"脏数据"覆盖了

假设 memcpy 从 &arr[2] 开始向前拷贝(实际取决于具体实现):
Step 1: arr[2] = arr[0] = 1   → arr[2] 从 3 变成 1 ✅(暂时正确)
Step 2: arr[3] = arr[1] = 2   → arr[3] 从 4 变成 2 ✅(暂时正确)
Step 3: arr[4] = arr[2] = 1   → ⚠️ arr[2] 已经被 Step 1 改成 1,所以这里拷的是脏数据!
Step 4: arr[5] = arr[3] = 2   → ⚠️ arr[3] 已经被 Step 2 改成 2,所以这里拷的是脏数据!
...

课件上的例子:{1,2,3,4,5,6,7,8,9,10} 经过 memcpy(&arr[2], &arr[0], 20) 后预期得到 {1,2,1,2,1,2,1,8,9,10}——运行起来结果确实是对的。

但这不代表 memcpy 是安全的。 C 标准明确说:

If the objects overlap, the behavior is undefined.

undefined = 编译器可以任意处理,可能这次对,下次优化后就不对了,在不同平台、不同编译器、不同优化级别下结果都可能不同。

🔑 安全结论:如果源和目标在同一个数组(或同一块内存区域),绝对不要用 memcpy,用 memmove。


第二部分:memmove——安全版内存拷贝

2.1 函数原型

void *memmove(void *destination, const void *source, size_t num);

参数和 memcpy 完全一样,唯一的区别:memmove 保证在重叠情况下结果一定正确。

2.2 代码演示

#include <stdio.h>
#include <string.h>

int main()
{
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 用 memmove 处理重叠拷贝
    memmove(&arr[2], &arr[0], 5 * sizeof(int));

    for (int i = 0; i < 10; i++)
        printf("%d ", arr[i]);
    // 输出:1 2 1 2 3 4 5 8 9 10
    return 0;
}

2.3 memmove 的实现原理:方向判断

memmove 能处理重叠的核心在于:它根据目标地址和源地址的相对位置,决定从哪个方向开始拷贝。

情况一:目标地址 > 源地址(向下覆盖)
arr[0] arr[1] arr[2] arr[3] arr[4] arr[5] arr[6] ...
  1       2       3       4       5       6       7  ...
                       ↑─────────────────────────────↑
                       目标(arr[2])        源(arr[0])

目标地址 > 源地址 → 从后往前拷贝
理由:后面先拷,不会覆盖前面还没拷的源数据
Step 1: 拷贝 arr[6] → arr[8](最后面开始,不影响前面)
Step 2: 拷贝 arr[5] → arr[7]
Step 3: 拷贝 arr[4] → arr[6]
Step 4: 拷贝 arr[3] → arr[5]
Step 5: 拷贝 arr[2] → arr[4]
Step 6: 拷贝 arr[1] → arr[3]
Step 7: 拷贝 arr[0] → arr[2]  ← 源数据全程没有被覆盖过 ✅
情况二:目标地址 < 源地址(向上覆盖)
arr[0] arr[1] arr[2] arr[3] arr[4] arr[5] arr[6] arr[7] ...
  1       2       3       4       5       6       7       8  ...
              ↑───────────────────────────────────────────↑
              目标(arr[1])                    源(arr[3])

目标地址 < 源地址 → 从前往后拷贝
理由:前面先拷,不会覆盖后面还没拷的源数据

2.4 memcpy vs memmove 一览表

memcpy memmove
函数原型 完全相同 完全相同
重叠安全性 未定义行为 保证正确
性能 略快(无方向判断) 略慢(有方向判断)
何时用 100% 确定不重叠 不确定是否重叠 / 同一数组内拷贝

💡 实操原则:如果有任何怀疑内存重叠的可能,直接用 memmove。现代编译器对 memmove 优化得很好(甚至和 memcpy 一样快),不要为了省那点性能赌未定义行为。


第三部分:memset——按字节设置内存

3.1 函数原型

void *memset(void *ptr, int value, size_t num);

ptr 指向的内存区域的前 num 个字节设置为 value

3.2 基础演示

#include <stdio.h>
#include <string.h>

int main()
{
    char str[] = "hello world";
    memset(str, 'x', 6);  // 前 6 个字节全部设成 'x'(ASCII 120)
    printf("%s\n", str);   // 输出:xxxxxxworld
    return 0;
}

3.3 memset 的最核心原则:按字节设置

⚠️ 提醒:memset 是逐字节设置的,不是按数据类型设置的。这个原则搞不清楚,会产生各种离谱的 bug。

为什么设 int 数组为 0 是对的?

int arr[5];
memset(arr, 0, sizeof(arr));  // 前 20 字节全设为 0x00

每个 int 变成 0x00000000 = 0,完全正确 ✅

为什么设 int 数组为 -1 也是对的?

int arr[5];
memset(arr, -1, sizeof(arr));  // 前 20 字节全设为 0xFF

-1 的二进制补码是 0xFFFFFFFF(全 1)。每个 int 变成 0xFFFFFFFF = -1(二进制补码),完全正确 ✅

为什么设 int 数组为 1 是错的?

int arr[5];
memset(arr, 1, sizeof(arr));  // 前 20 字节全设为 0x01

每个 int 变成 0x01010101 = 16843009(十进制),完全不是 1 ❌

3.4 memset 完整示例(含各种设值)

#include <stdio.h>
#include <string.h>

int main()
{
    int arr[5];

    // 正确:设 0
    memset(arr, 0, sizeof(arr));
    printf("%d ", arr[0]);   // 0 ✅

    // 正确:设 -1
    memset(arr, -1, sizeof(arr));
    printf("%d ", arr[0]);   // -1 ✅

    // 错误:设 1(陷阱!)
    memset(arr, 1, sizeof(arr));
    printf("%d ", arr[0]);   // 16843009 ❌(不是1!)

    return 0;
}

3.5 为什么 memset 只适合设 0 和 -1?

这涉及到 int 在内存中的存储方式(二进制补码):

设的值 每个字节 int(4字节)的值
memset(..., 0, ...) 0x00 × 4 0x00000000 = 0
memset(..., -1, ...) 0xFF × 4 0xFFFFFFFF = -1
memset(..., 1, ...) 0x01 × 4 0x01010101 = 16843009
memset(..., 127, ...) 0x7F × 4 0x7F7F7F7F = 2139062143

💡 记忆口诀memset 设 int 数组,只有 0 和 -1 是安全的,其他值都会产生奇怪的大数字。

3.6 实战:memset 的正确用法

#include <stdio.h>
#include <string.h>

int main()
{
    // 初始化 char 数组(随便设什么值都可以,因为 char 是单字节)
    char buf[100];
    memset(buf, 0, sizeof(buf));      // 全设为 \0 ✅
    memset(buf, 'A', 50);              // 前50个字节全设为 'A' ✅

    // 初始化 int 数组
    int nums[100];
    memset(nums, 0, sizeof(nums));    // 全设为 0 ✅
    memset(nums, -1, sizeof(nums));   // 全设为 -1 ✅

    // 错误示范:想设成 5,实际上每个 int 是 0x05050505
    int wrong[5];
    memset(wrong, 5, sizeof(wrong));
    printf("%d\n", wrong[0]);  // 输出 84215045,不是 5!❌

    return 0;
}

第四部分:memcmp——内存块比较

4.1 函数原型

int memcmp(const void *ptr1, const void *ptr2, size_t num);

ptr1ptr2 的起始地址开始,逐字节比较,最多比较 num 个字节。

4.2 返回值规则

比较结果 返回值
第一个不同的字节:ptr1 的字节 > ptr2 的字节 正数(> 0)
第一个不同的字节:ptr1 的字节 < ptr2 的字节 负数(< 0)
所有 num 个字节都相等 0

4.3 典型示例:比较字符串

#include <stdio.h>
#include <string.h>

int main()
{
    char buf1[] = "DWgaOtP12df0";
    char buf2[] = "DWGAOTP12DF0";
    int n = memcmp(buf1, buf2, sizeof(buf1));

    if (n > 0)
        printf("'%s' > '%s'\n", buf1, buf2);
    else if (n < 0)
        printf("'%s' < '%s'\n", buf1, buf2);
    else
        printf("'%s' == '%s'\n", buf1, buf2);

    return 0;
}
// 输出:'DWgaOtP12df0' > 'DWGAOTP12DF0'
// 原因:'g' (ASCII 103) > 'G' (ASCII 71)

4.4 memcmp vs strcmp:核心区别

char str1[] = "hello";
char str2[] = "hello\x00world";  // 中间有 \0

// strcmp:遇到 \0 就停止比较
strcmp(str1, str2) == 0;  // 相等(strcmp 只看到 "hello" 就停了)

// memcmp:严格按照 num 字节数比较,忽略 \0 的特殊含义
memcmp(str1, str2, 10) == 0;  // 不相等(\0 只是普通字节 0x00)

🔑 记忆口诀strcmp 看到 \0 就停,memcmp 眼里 \0 只是字节 0x00。

4.5 精确比较:比较前 N 个字节

memcmp 的精髓在于可以只比较一部分字节

#include <stdio.h>
#include <string.h>

int main()
{
    char version1[] = "V1.2.3";
    char version2[] = "V1.2.4";

    // 只比较前 5 个字节("V1.2.")
    int n = memcmp(version1, version2, 5);
    printf("%s\n", n == 0 ? "版本号前缀相同" : "版本号前缀不同");
    // 输出:版本号前缀相同

    // 比较全部
    n = memcmp(version1, version2, sizeof(version1));
    printf("%s\n", n == 0 ? "完全相同" : "不同");
    // 输出:不同(因为 '3' < '4')
    return 0;
}

第五部分:手写模拟实现

5.1 手写 memcpy

#include <assert.h>

void *my_memcpy(void *dest, const void *src, size_t num)
{
    assert(dest != NULL && src != NULL);  // 断言:地址不能为 NULL
    void *ret = dest;                     // 记住目标起始地址(返回值用)

    // 逐字节拷贝:从源逐个字节复制到目标
    while (num--) {
        *(char*)dest = *(char*)src;       // 强转为 char*,一次拷1字节
        dest = (char*)dest + 1;           // 目标地址后移1字节
        src  = (char*)src  + 1;           // 源地址后移1字节
    }
    return ret;
}

💡 为什么强转成 char* 因为 char 永远是 1 字节,用 char* 可以确保每次精确拷贝 1 个字节,不管原类型是什么。

5.2 手写 memmove

#include <assert.h>

void *my_memmove(void *dest, const void *src, size_t num)
{
    assert(dest != NULL && src != NULL);
    void *ret = dest;

    if (dest < src) {
        // 情况一:目标地址 < 源地址(向上覆盖)
        // 从前往后拷贝,前面先拷,不影响后面的源数据
        while (num--) {
            *(char*)dest = *(char*)src;
            dest = (char*)dest + 1;
            src  = (char*)src  + 1;
        }
    } else {
        // 情况二:目标地址 >= 源地址(向下覆盖)
        // 从后往前拷贝,后面先拷,不影响前面的源数据
        dest = (char*)dest + num;   // 先跳到最后
        src  = (char*)src  + num;
        while (num--) {
            dest = (char*)dest - 1;
            src  = (char*)src  - 1;
            *(char*)dest = *(char*)src;
        }
    }
    return ret;
}

第六部分:经典面试题精讲

面试题一:memcpy 和 memmove 的核心区别是什么?

参考答案

功能上:两者都可以拷贝任意类型的内存块,函数原型几乎相同。

核心区别:memcpy 不处理重叠,memmove 处理重叠。

  • memcpy:源和目标内存重叠时,行为未定义(可能对可能错)
  • memmove:通过判断目标地址和源地址的相对位置,选择从前往后或从后往前拷贝,保证在任何重叠情况下结果都正确

实操建议:不确定是否重叠时一律用 memmove。


面试题二:下面代码的输出是什么?

#include <stdio.h>
#include <string.h>

int main()
{
    int arr[5];
    memset(arr, 0, sizeof(arr));

    int i = 0;
    for (i = 0; i < 5; i++)
        printf("%d ", arr[i]);

    return 0;
}

推演

  • sizeof(arr) = 5 × 4 = 20 字节
  • memset(arr, 0, 20) → 前 20 字节全设为 0x00
  • 每个 int(4 字节)= 0x00000000 = 0

答案0 0 0 0 0


面试题二·变体:改成 memset(arr, 1, sizeof(arr)) 呢?

int arr[5];
memset(arr, 1, sizeof(arr));
printf("%d\n", arr[0]);

推演

  • memset(arr, 1, 20) → 前 20 字节全设为 0x01
  • arr[0] 的 4 个字节 = 0x01 0x01 0x01 0x01
  • 0x01010101 = 1×16777216 + 1×65536 + 1×256 + 1 = 16843009

答案:16843009(不是 1!)

⚠️ 提醒:这是 memset 最经典的面试题陷阱,90% 的初学者会答成 1,正确答案是 16843009。


面试题三:memcmp 比较的是什么单位的数据?

参考答案

memcmp字节逐个比较,不是按数据类型(int、double 等)比较。

int a = 0x12345678;   // 内存:78 56 34 12(小端)
int b = 0x12340000;
int ret = memcmp(&a, &b, 4);  // 比较 4 个字节
// 从低地址字节开始比:
//   字节1: 0x78 vs 0x00 → 78 > 00 → 正数
printf("%d\n", ret);  // 正数

常见混淆:初学者以为 memcmp 是"按 int 比较",实际上是"按字节比较"。


面试题四:实现一个函数,把 int 数组前 n 个元素全部设为 0,要求不能用 memset

void set_int_array_zero(int *arr, int n)
{
    // 方法一:循环赋值
    for (int i = 0; i < n; i++)
        arr[i] = 0;

    // 方法二:指针方式
    int *p = arr;
    while (n--)
        *p++ = 0;
}

💡 引申问题:为什么不用 memset 设 int 数组为 0 呢?因为 memset 按字节设,int 是 4 字节,设 0 恰好是对的。但如果我们想设其他值(比如每个 int 设成 0xAAAAAAAA),就得手动循环了。


本章总结

四大内存函数速查表:

函数 原型 功能 返回值 特别注意
memcpy memcpy(d,s,n) 拷贝 n 字节 目标地址 重叠时未定义
memmove memmove(d,s,n) 拷贝 n 字节 目标地址 重叠时也正确
memset memset(p,v,n) 前 n 字节设成 v 目标地址 v 会被截成单字节
memcmp memcmp(p1,p2,n) 比较 n 字节 正/0/负 按字节比,忽略 \0

memcpy vs memmove 核心区别:

场景 memcpy memmove
源和目标完全不重叠 正确 ✅ 正确 ✅
目标地址 > 源地址(向下覆盖) 未定义 ❌ 正确 ✅
目标地址 < 源地址(向上覆盖) 未定义 ❌ 正确 ✅
性能 略快 略慢(可忽略)

memset 安全性速查:

设的值 int 数组结果 是否安全
0 0x00000000 = 0 ✅ 安全
-1 0xFFFFFFFF = -1 ✅ 安全
1 0x01010101 = 16843009 ❌ 危险
127 0x7F7F7F7F = 2139062143 ❌ 危险
255 0xFFFFFFFF = -1(巧合安全) ⚠️ 碰巧

字节视角操作内存,memmove 是 memcpy 的安全升级版,memset 设 int 只有 0 和 -1 是保险的
memset(arr, 1, sizeof(arr)) 输出的不是 1,是 16843009——这条考过无数人

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐