哈喽~新手小伙伴们!咱们之前聊完了字符串函数,今天来啃另一块超重要的骨头 ——内存函数!和字符串函数只处理字符不同,内存函数是 “万能选手”,能操作任意类型的数据(int、char、结构体…),但正因为通用,新手也更容易踩坑。咱们从 “核心逻辑→用法→模拟实现→避坑” 一步步讲,保证听得懂、用得对~

一、先搞懂:内存函数的核心 ——“按字节操作”

字符串函数只认'\0',但内存函数不管这些,它只认 “字节”:比如要拷贝 10 个字节、比较 8 个字节、设置 4 个字节… 所有内存函数的参数里,都会有一个size_t num(要操作的字节数),这是最关键的点,先记牢!

常见类型字节数

字符 (char) 永远 1,短整型 (short) 固定 2;

整型 (int) 是 4,长长整型 (long long) 8;

浮点型 float4、double8;

二、memcpy:内存版 “搬运工”—— 拷贝任意内存数据

memcpy 的核心是:从源地址拷贝指定字节数的数据到目标地址,和 strcpy 不同,它不关心'\0'只认字节数,能拷贝任何类型的数据。

1.1 代码演示(新手易上手版)

#include <stdio.h>
#include <string.h> // 内存函数都要包含这个头文件

int main() {
    // 示例1:拷贝整型数组
    int arr1[10] = {0};
    int arr2[] = {1,2,3,4,5};
    // 拷贝arr2的前5个元素(每个int占4字节,5*4=20字节)
    memcpy(arr1, arr2, 20);
    // 打印arr1:1 2 3 4 5 0 0 0 0 0
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr1[i]);
    }
    printf("\n");

    // 示例2:拷贝字符数组(对比strcpy,更灵活)
    char str1[10] = {0};
    char str2[] = "abcdef";
    memcpy(str1, str2, 3); // 只拷贝前3个字节(a、b、c)
    // 打印str1:abc(不用手动补'\0',因为只拷贝3字节)
    printf("%s\n", str1);

    return 0;
}

关键点第三个参数是 “字节数”,不是 “元素个数”!比如拷贝 5 个 int,要算5*sizeof(int),而不是直接写 5。

1.2 模拟实现(懂底层更易避坑)

核心逻辑:把地址强转成char*因为 char 是 1 字节,方便逐个拷贝),然后循环拷贝 num 个字节,直到拷贝完。

解释:

计算机内存的最小可操作单位字节(1Byte),而 char 类型在 C 标准中被定义为「占用 1 字节存储空间」—— 这是 char 最核心的特性。

// 模拟memcpy:返回目标地址(和库函数一致)
// void* 表示通用指针,能接收任意类型的地址
void* my_memcpy(void* dest, const void* src, size_t num) {
    // 1. 保存目标地址(最后要返回)
    void* ret = dest;
    // 2. 强转成char*,逐个字节拷贝(必须!因为只有char是1字节)
    char* d = (char*)dest;
    const char* s = (const char*)src;

    // 3. 循环拷贝num个字节
    while (num--) {
        *d++ = *s++; // 拷贝1个字节,指针都往后挪
    }

    return ret; // 返回目标地址,支持链式调用
}

// 测试模拟实现
int main() {
    float f1[5] = {0.0};
    float f2[] = {1.1,2.2,3.3};
    // 拷贝3个float(3*4=12字节)
    my_memcpy(f1, f2, 12);
    // 打印:1.1 2.2 3.3 0.0 0.0
    for (int i = 0; i < 5; i++) {
        printf("%.1f ", f1[i]);
    }
    return 0;
}

1.3 新手必注意(避坑!)

① 第三个参数是字节数,不是元素数:新手最容易写memcpy(arr1, arr2, 5)(想拷贝 5 个 int),但实际只拷贝 5 字节(1 个 int+1 字节),数据直接乱掉!正确写法是memcpy(arr1, arr2, 5*sizeof(int))

② 源地址和目标地址不能重叠:标准的 memcpy 不处理 “内存重叠”(比如把 arr [0-4] 拷贝到 arr [2-6]),重叠拷贝会出问题,这时候要用到 memmove(下面会讲);

③ 目标内存要够大:和 strcpy 一样,目标地址的内存空间必须≥要拷贝的字节数,否则内存溢出!

三、memmove:升级版 “内存搬运工”—— 处理重叠拷贝

memmove 和 memcpy 功能几乎一样,唯一的区别是memmove 能安全处理 “源地址和目标地址重叠” 的情况新手如果不确定内存是否重叠,优先用 memmove 更稳妥

2.1 代码演示(重点看重叠场景)

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

int main() {
    // 场景:内存重叠——把arr[0-4]拷贝到arr[2-6]
    int arr[] = {1,2,3,4,5,6,7,8,9,10};
    // 如果用memcpy,结果可能乱(不同编译器表现不同);用memmove绝对安全
    memmove(arr+2, arr, 20); // 拷贝20字节(5个int)

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

2.2 模拟实现(核心:处理重叠逻辑)

内存重叠分两种情况,模拟实现的关键是 “判断拷贝方向”:

1.如果目标地址 < 源地址:从前往后拷贝(和 memcpy 一样);

2.如果目标地址 > 源地址:从后往前拷贝避免先拷贝的字节覆盖还没拷贝的源数据)。

void* my_memmove(void* dest, const void* src, size_t num) {
    void* ret = dest;
    char* d = (char*)dest;
    const char* s = (const char*)src;

    // 情况1:目标地址 < 源地址 → 从前往后拷贝
    if (d < s) {
        while (num--) {
            *d++ = *s++;
        }
    }
    // 情况2:目标地址 > 源地址 → 从后往前拷贝(避免重叠覆盖)
    else {
        d += num - 1; // 指向目标最后一个字节
        s += num - 1; // 指向源最后一个字节
        while (num--) {
            *d-- = *s--;
        }
    }

    return ret;
}

// 测试重叠拷贝
int main() {
    int arr[] = {1,2,3,4,5,6,7,8};
    my_memmove(arr+1, arr, 16); // 拷贝4个int(16字节)到arr+1位置
    // 打印:1 1 2 3 4 6 7 8(正确!)
    for (int i = 0; i < 8; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

这样纯代码可能还是有点难以理解,所以我们在来详细讲讲。

测试代码里的关键参数:

int arr[] = {1,2,3,4,5,6,7,8};
// 把arr[0]-arr[3](4个int,16字节)拷贝到arr[1]-arr[4]
my_memmove(arr+1, arr, 16);

先换算成内存地址(方便理解,咱们假设 arr 的起始地址是0x100):

int 占 4 字节,所以 arr 每个元素的地址:

元素值 1 2 3 4 5 6 7 8
地址 0x100 0x104 0x108 0x10C 0x110 0x114 0x118 0x11C

拷贝需求:把0x100-0x10F16 字节,对应 1、2、3、4)拷贝到0x104-0x113(对应 2、3、4、5 的位置)。

0x10F:0x10C+3;0x113:0x110+3;

因为int 占 4 字节,0x10C是arr[4]的首地址;而我们拷贝是要将arr[4]完全拷贝,所以要将arr[4]占的4个字节全部拷贝。

第一步:先看「如果不分方向,直接从前往后拷贝会出什么问题?」

假如我们像 memcpy 一样,直接从前往后拷贝(不判断地址大小),步骤会是这样:

  1. 拷贝地址0x100(值 1)→ 0x104(原 2 的位置)→ arr 变成:1,1,3,4,5,6,7,8
  2. 拷贝地址0x104(现在值 1,不是原来的 2 了!)→ 0x108(原 3 的位置)→ arr 变成:1,1,1,4,5,6,7,8
  3. 后续拷贝的都是被覆盖后的值,最终结果会变成1,1,1,1,1,6,7,8—— 完全错了!

原因目标地址在源地址后面,从前往后拷贝会先覆盖源地址还没拷贝的部分,就像 “抄作业时,先把要抄的内容擦掉了”。

第二步:拆解 my_memmove 的正确逻辑(从后往前拷贝)

咱们代码里判断出dest(0x104)> src(0x100),所以走「从后往前拷贝」分支,分步看:

步骤 1:定位到最后一个要拷贝的字节

d += num - 1; // d原本是0x104,num=16 → 0x104 + 15 = 0x113
s += num - 1; // s原本是0x100,0x100 + 15 = 0x10F

d(目标最后 1 字节):0x113(对应 arr [4] 的最后 1 字节);

s(源最后 1 字节):0x10F(对应 arr [3] 的最后 1 字节)。

步骤 2:从后往前逐个字节拷贝(共 16 次,对应 4 个 int)

咱们把 16 字节拷贝拆成 4 个 int(更易理解),核心是「先拷贝后面的元素,再拷贝前面的」:

  1. 先拷贝 arr [3](值 4,地址 0x10C-0x10F)→ arr [4](地址 0x110-0x113)→ arr 变成:1,2,3,4,4,6,7,8
  2. 再拷贝 arr [2](值 3,地址 0x108-0x10B)→ arr [3](地址 0x10C-0x10F)→ arr 变成:1,2,3,3,4,6,7,8
  3. 再拷贝 arr [1](值 2,地址 0x104-0x107)→ arr [2](地址 0x108-0x10B)→ arr 变成:1,2,2,3,4,6,7,8
  4. 最后拷贝 arr [0](值 1,地址 0x100-0x103)→ arr [1](地址 0x104-0x107)→ arr 变成:1,1,2,3,4,6,7,8

最终结果和代码注释一致:1 1 2 3 4 6 7 8—— 完全正确!

再补一个「从前往后拷贝」的例子(目标地址 < 源地址)

比如把 arr [2]-arr [5](3,4,5,6)拷贝到 arr [0]-arr [3],此时:

src=arr+2(地址 0x108),dest=arr(地址 0x100)→ dest < src;

走「从前往后拷贝」分支,步骤:

拷贝 3→arr [0],4→arr [1],5→arr [2],6→arr [3];

因为源地址在后面,拷贝前面的元素不会覆盖源数据,结果正确:3,4,5,6,5,6,7,8

核心总结(一句话记住)

1.目标地址 < 源地址:从前往后拷贝(不会覆盖源数据);

2.目标地址 > 源地址:从后往前拷贝(避免覆盖源数据);

本质:先拷贝 “不会被覆盖的部分”,再拷贝 “可能被覆盖的部分”

可视化画图(新手秒懂版)

// 重叠场景:dest在src后面(要从后往前)
src:  [1][2][3][4]  → 源数据
dest:    [2][3][4][5]  → 目标位置
        ↑ 先拷4→5,再拷3→4,再拷2→3,最后拷1→2 → 不覆盖

// 非重叠场景:dest在src前面(从前往后)
src:      [3][4][5][6]  → 源数据
dest:  [1][2][3][4]  → 目标位置
        ↑ 先拷3→1,再拷4→2,再拷5→3,最后拷6→4 → 不覆盖

这样拆解下来,是不是就明白为什么 my_memmove 要分方向拷贝了?核心就是避免 “还没拷贝的源数据被提前覆盖”,新手可以自己把地址和值写在纸上,一步步模拟拷贝过程,马上就懂~

2.3 新手必注意(避坑!)

① memcpy 和 memmove 的选择:如果确定内存不重叠,两者都能用;如果不确定 / 明确重叠,必须用 memmove

② 不要依赖编译器的 memcpy:有些编译器会让 memcpy 也支持重叠,但标准里 memcpy 不保证,跨平台写代码时,重叠场景一定要用 memmove

四、memset:内存 “粉刷匠”—— 按字节设置内存值

memset 的核心是:把指定内存区域的每个字节都设置成同一个值新手常用来初始化数组、清空内存,超实用,但也超容易踩坑!

3.1 代码演示(正确用法 + 错误示例)

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

int main() {
    // 示例1:正确用法——设置字符数组(char是1字节,完美匹配)
    char str[10] = {0};
    memset(str, 'a', 5); // 把前5个字节设为'a'
    printf("%s\n", str); // 输出:aaaaa

    // 示例2:正确用法——清空数组(设为0)
    int arr[5] = {1,2,3,4,5};
    memset(arr, 0, sizeof(arr)); // 把整个arr的每个字节设为0
    // 打印:0 0 0 0 0
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 示例3:新手大坑——用memset设置非0的int值(必错!)
    int arr2[5] = {0};
    memset(arr2, 1, sizeof(arr2)); // 想把arr2设为1,但实际每个字节是1
    // 打印结果:16843009 16843009 16843009 16843009 16843009(不是1!)
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr2[i]);
    }

    return 0;
}

3.2 核心总结 + 避坑!

✅ 什么时候用:只要需要 “批量设置某块内存的内容”,就用 memset(比如初始化、清空内存);

❌ 关键坑点:memset 是按字节设置,不是按元素设置!

字符数组(char):每个元素 1 字节,随便设('a'、0、'1' 都可以);

整型 / 浮点型数组:只能设 0(因为 0 的每个字节都是 0),设 1、2 等非 0 值会出错(比如 int 占 4 字节,设 1 会变成 0x01010101=16843009);

✅ 小技巧:想初始化 int 数组为 1,别用 memset,用循环逐个赋值!

五、memcmp:内存 “裁判”—— 比较任意内存数据的大小

memcmp 的核心是:按字节比较两块内存的数据,直到找到不同字节或比较完指定字节数,返回规则和 strcmp 一样,但它能比较任意类型的数据

4.1 代码演示

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

int main() {
    // 示例1:比较整型数组
    int arr1[] = {1,2,3,4};
    int arr2[] = {1,2,4,5};
    // 比较前8字节(前2个int):1和1相等,2和2相等 → 返回0
    int ret1 = memcmp(arr1, arr2, 8);
    // 比较前12字节(前3个int):第三个字节3 < 4 → 返回负数
    int ret2 = memcmp(arr1, arr2, 12);
    printf("比较前8字节:%d\n", ret1); // 0
    printf("比较前12字节:%d\n", ret2); // 负数(比如-1)

    // 示例2:比较字符数组(对比strcmp,更灵活)
    char str1[] = "abcdef";
    char str2[] = "abCxyz";
    // 比较前3字节:'c'(99) > 'C'(67) → 返回正数
    int ret3 = memcmp(str1, str2, 3);
    printf("比较前3字节:%d\n", ret3); // 正数(比如32)

    return 0;
}

4.2 核心总结 + 避坑!

什么时候用:需要比较 “非字符串” 的内存数据(比如结构体、整型数组),或只想比较字符串的前 n 个字节;

关键坑点

比较的是 “字节的 ASCII 码 / 二进制值”,不是元素的实际值(比如比较 float 数组,要注意二进制存储规则);

第三个参数是 “字节数”,不是元素数(和 memcpy 一样,新手别写错!);

和 strcmp 的区别:strcmp 只比较字符串(到 '\0' 为止),memcmp 能比较任意内存,且能指定比较长度

六、新手避坑总清单(重中之重!)

  1. 所有内存函数的第三个参数都是 “字节数”:别把 “元素数” 直接传进去,一定要算元素数*sizeof(类型)
  2. memset 只适合设 0 或字符非 0 的整型 / 浮点型别用 memset,用循环
  3. 内存重叠用 memmove,不用 memcpy:哪怕编译器支持 memcpy 重叠拷贝,也别依赖,跨平台代码要严谨;
  4. 目标内存空间要够大:拷贝 / 设置内存前,确认目标地址的空间≥要操作的字节数,避免溢出;
  5. void * 指针不能直接解引用:模拟实现时,一定要强转成 char * 再操作(因为 char 是 1 字节,能精准操作每个字节)。

结束语

好啦~内存函数其实比字符串函数更 “通用”,核心就抓两点按字节操作 + 指定操作长度

刚开始可能会混淆 “字节数” 和 “元素数”,没关系,多敲几遍代码(比如用 memcpy 拷贝不同类型的数组,用 memset 试一下设 1 的坑),踩过一次就记住了~

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐