在C语言的底层开发中,内存操作是绕不开的核心话题。memcpy、memmove、memset、memcmp这四大内存函数,不仅是面试高频考点,更是写出高效、健壮代码的必备工具。本文将从函数原型、底层原理到手写实现,带你系统性掌握它们的用法与设计思想,彻底搞懂内存操作的底层逻辑。


前置通用

1. 内存操作函数均定义在<string.h>头文件中,使用前必须包含;

2. 函数参数多为void*类型,保证通用性(可接收任意类型的指针),底层通过char*逐字节操作(因为char是1字节,是C语言最小内存单位);

3. 内存操作函数直接操作内存地址,不关心数据类型,比普通类型拷贝(如int/char赋值)更高效、更底层。


一、内存拷贝函数:memcpy

1. 函数原型

void *memcpy(void *dest, const void *src, size_t n);

参数说明:

• dest:目标内存地址(拷贝后的数据存放位置),void*类型;

• src:源内存地址(被拷贝的数据位置),const修饰表示不修改源数据,保证安全性;

• n:需要拷贝的字节数,size_t是C语言无符号整型(unsigned int),专门用于表示内存大小/长度;
返回值:返回目标内存的起始地址dest(方便链式调用)。

2. 核心功能

从src指向的内存地址开始,拷贝n个字节的数据到dest指向的内存地址,不处理内存重叠场景。

3. 关键使用笔记

✅ 可拷贝任意类型数据(int/char/数组/结构体等),突破普通拷贝的类型限制;
源地址和目标地址不能重叠,若重叠,拷贝结果未定义(编译器可能报错/数据乱码);
✅ dest和src的内存空间必须足够大,至少能容纳n个字节,否则会发生内存越界(程序崩溃);
✅ 底层是从低地址到高地址逐字节拷贝。

4. 基础使用示例(多类型拷贝,逐行注释)

#include <stdio.h>
#include <string.h> // 包含memcpy头文件

// 测试结构体拷贝(memcpy支持任意类型)
typedef struct {
    char name[20];
    int age;
    float score;
} Student;

int main() {
    // 示例1:拷贝字符数组
    char str1[20] = {0}; // 目标数组,初始化为0
    char str2[] = "hello memcpy"; // 源数组
    // 拷贝str2的全部内容到str1,strlen(str2)获取有效字符数,+1包含'\0'结束符
    memcpy(str1, str2, strlen(str2) + 1);
    printf("字符数组拷贝结果:%s\n", str1); // 输出:hello memcpy

    // 示例2:拷贝int数组(拷贝3个int元素,共3*4=12字节)
    int arr1[10] = {0};
    int arr2[] = {1,2,3,4,5};
    memcpy(arr1, arr2, 3 * sizeof(int)); // sizeof(int)获取int类型字节数(通常4)
    printf("int数组拷贝结果:");
    for (int i = 0; i < 3; i++) {
        printf("%d ", arr1[i]); // 输出:1 2 3
    }
    printf("\n");

    // 示例3:拷贝结构体
    Student s1 = {"张三", 18, 95.5};
    Student s2 = {0};
    memcpy(&s2, &s1, sizeof(Student)); // 拷贝整个结构体,sizeof获取结构体总字节数
    printf("结构体拷贝结果:%s %d %.1f\n", s2.name, s2.age, s2.score); // 输出:张三 18 95.5

    return 0;
}

运行结果:

字符数组拷贝结果:hello memcpy
int数组拷贝结果:1 2 3
结构体拷贝结果:张三 18 95.5

5. 手写模拟实现

#include <stdio.h>
#include <assert.h> // 包含断言头文件,用于校验指针有效性

// 模拟实现memcpy函数
void* my_memcpy(void* dest, const void* src, size_t n) {
    // 断言:dest和src不能为NULL(空指针),否则直接终止程序,避免野指针访问
    assert(dest != NULL && src != NULL);
    // 保存目标地址起始位置,最后返回(因为后续指针会偏移)
    void* ret = dest;
    // 核心:逐字节拷贝,循环n次,每次拷贝1个字节
    // 先将void*强转为char*,因为只有char*能保证每次+1偏移1个字节
    while (n--) { // n是无符号整型,n--直到0结束
        *(char*)dest = *(char*)src; // 解引用,拷贝当前字节
        dest = (char*)dest + 1;     // 目标指针后移1字节
        src = (char*)src + 1;       // 源指针后移1字节
    }
    return ret; // 返回目标地址起始位置
}

// 测试自定义my_memcpy
int main() {
    int arr1[10] = {0};
    int arr2[] = {10,20,30,40};
    // 调用自定义my_memcpy,拷贝2个int元素(8字节)
    my_memcpy(arr1, arr2, 2 * sizeof(int));
    printf("自定义memcpy测试:");
    for (int i = 0; i < 2; i++) {
        printf("%d ", arr1[i]); // 输出:10 20
    }
    return 0;
}

核心实现要点:

1. 用assert校验指针非空,提升程序健壮性;

2. 先保存dest起始地址,因为后续dest指针会偏移,无法直接返回;

3. 必须将void*强转为char*后再操作,保证逐字节拷贝(void*不能直接解引用和偏移)。

二、可重叠内存拷贝函数:memmove

1. 函数原型

void *memmove(void *dest, const void *src, size_t n);

参数/返回值:和memcpy完全一致,仅功能上支持内存重叠。

2. 核心功能

和memcpy的拷贝功能一致,且支持源地址和目标地址重叠的场景(是memcpy的升级版),C语言标准规定:memmove必须处理内存重叠,memcpy可选。

3. 关键使用笔记

✅ 完全兼容memcpy的所有场景,推荐优先使用memmove,更安全;
✅ 处理内存重叠的核心是根据地址位置选择拷贝方向:

• 当dest < src(目标地址在源地址左侧,无重叠/不影响):从低地址到高地址拷贝(和memcpy一致);

• 当dest > src(目标地址在源地址右侧,重叠):从高地址到低地址拷贝(避免先拷贝的字节覆盖后续要拷贝的源数据);
❌ 同样要求dest和src内存空间足够,避免内存越界。

4. 经典使用示例(内存重叠场景,对比memcpy)

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

int main() {
    // 内存重叠场景:同一个数组,将前4个元素拷贝到从第2个元素开始的位置
    int arr[] = {1,2,3,4,5,6,7,8};
    int n = sizeof(arr) / sizeof(arr[0]);

    // 示例1:用memmove处理重叠(正确)
    memmove(arr+1, arr, 4 * sizeof(int)); // arr+1是第2个元素的地址(偏移4字节)
    printf("memmove重叠拷贝结果:");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]); // 输出:1 1 2 3 4 6 7 8(正确)
    }
    printf("\n");

    // 重置数组
    int arr2[] = {1,2,3,4,5,6,7,8};
    // 示例2:用memcpy处理重叠(结果未定义,编译器可能乱码)
    memcpy(arr2+1, arr2, 4 * sizeof(int));
    printf("memcpy重叠拷贝结果:");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr2[i]); // 不同编译器结果不同,大概率乱码
    }

    return 0;
}

运行结果(VS2022示例):

memmove重叠拷贝结果:1 1 2 3 4 6 7 8
memcpy重叠拷贝结果:1 1 1 1 1 6 7 8

5. 手写模拟实现(逐行注释,处理地址重叠)

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

void* my_memmove(void* dest, const void* src, size_t num)
{
    // 保存目标地址的起始位置,用于最终返回
    void* ret = dest;
    // 断言确保传入的指针不为空
    assert(dest && src);

    // 情况1:目标地址在源地址左侧(无重叠风险)
    if (dest < src)
    {
        // 从低地址到高地址拷贝
        while (num--)
        {
            *(char*)dest = *(char*)src;
            dest = (char*)dest + 1;
            src = (char*)src + 1;
        }
    }
    // 情况2:目标地址在源地址右侧或重叠
    else
    {
        // 先将指针移动到内存块的末尾
        char* dst_end = (char*)dest + num - 1;
        char* src_end = (char*)src + num - 1;
        // 从高地址到低地址拷贝
        while (num--)
        {
            *dst_end = *src_end;
            dst_end--;
            src_end--;
        }
    }

    return ret;
}

// 测试用例
int main()
{
    int arr[] = {1,2,3,4,5,6,7,8,9,10};
    // 测试重叠拷贝:从arr+2的位置拷贝20字节(5个int)到arr+1的位置
    my_memmove(arr+1, arr+2, 20);
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

核心实现要点:

1. 先将void*转为char*并赋值给临时变量,避免多次强转,简化代码;

2. 通过比较dest和src的地址大小,选择拷贝方向;

3. 从高地址拷贝时,先将指针偏移到最后一个字节,再倒序拷贝。

三、内存设置函数:memset

1. 函数原型

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

参数说明:

• ptr:需要设置的内存起始地址;

• value:需要设置的字节值(int类型,实际存储时取低8位,即0~255);

• n:需要设置的字节数;
返回值:返回设置后的内存起始地址ptr。

2. 核心功能

将ptr指向的内存地址开始,连续n个字节的内容全部设置为value,以字节为单位操作(最关键)。

3. 关键使用笔记

⚠️ 核心易错点:memset是按字节赋值,不是按变量类型赋值!如果给非字符类型(int/float等)赋值,只能设置每个字节都相同的值,否则结果不符合预期;
✅ 常用于内存初始化(如将数组/缓冲区置0、置为指定字符);
✅ value通常传字符(如'a')或整数0,因为字符在内存中占1字节,契合memset的操作方式;
❌ 不要用memset给int数组赋值为1(原因:int占4字节,会被设置为0x01010101,十进制为16843009,而非1)。

4. 基础使用示例(正确/错误示例对比)

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

int main() {
    // 示例1:正确使用→给字符数组设置值(按字节,完美契合)
    char str[20] = {0};
    memset(str, 'a', 5); // 将前5个字节设置为'a'
    printf("字符数组设置结果:%s\n", str); // 输出:aaaaa(正确)

    // 示例2:正确使用→将int数组置0(每个字节都是0,整个int就是0)
    int arr1[10] = {1,2,3,4,5};
    memset(arr1, 0, sizeof(arr1)); // sizeof(arr1)获取数组总字节数
    printf("int数组置0结果:");
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr1[i]); // 输出:0 0 0 0 0(正确)
    }
    printf("\n");

    // 示例3:错误使用→用memset给int数组赋值为1(按字节赋值,结果非1)
    int arr2[5] = {0};
    memset(arr2, 1, sizeof(arr2));
    printf("int数组赋值1的错误结果:");
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr2[i]); // 输出:16843009 16843009 ...(错误)
    }

    return 0;
}

运行结果:

字符数组设置结果:aaaaa
int数组置0结果:0 0 0 0 0
int数组赋值1的错误结果:16843009 16843009 16843009 16843009 16843009

5. 手写模拟实现(逐行注释,按字节设置)

#include <stdio.h>
#include <assert.h>

// 模拟实现memset
void* my_memset(void* ptr, int value, size_t n) {
    assert(ptr != NULL); // 校验指针非空
    void* ret = ptr;
    // 强转为char*,逐字节设置,value强转char取低8位
    char* p = (char*)ptr;
    while (n--) {
        *p = (char)value;
        p++;
    }
    return ret;
}

// 测试
int main() {
    char buf[30] = {0};
    // 调用自定义my_memset,前8个字节设置为'#'
    my_memset(buf, '#', 8);
    printf("自定义memset测试:%s\n", buf); // 输出:########
    return 0;
}

总结:

当有一块内存空间需要设置内容的时候,就可以使用memset函数,值得注意的是memset函数对内存

单元的设置是以字节为单位的。

四、内存比较函数:memcmp

1. 函数原型

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

参数说明:

• ptr1/ptr2:需要比较的两块内存的起始地址;

• n:需要比较的字节数;
返回值:int类型,根据比较结果返回正负/0(按ASCII码值逐字节比较):

◦ 返回0:前n个字节的内容完全相同;

◦ 返回正数:第一次比较不同的字节,ptr1的字节值 > ptr2的字节值;

◦ 返回负数:第一次比较不同的字节,ptr1的字节值 < ptr2的字节值。

2. 核心功能

从ptr1和ptr2指向的内存地址开始,逐字节比较前n个字节的内容,不关心数据类型,找到第一个不同的字节即停止比较。

3. 关键使用笔记

✅ 可比较任意类型的内存块(int/char/结构体/数组),比strcmp(仅比较字符串)更通用;
✅ 逐字节比较,直到找到第一个不同的字节或比较完n个字节;
✅ 比较的是内存中的原始二进制值,而非变量的实际值(如浮点型1.0和整型1内存值不同,比较结果不同);
✅ 字符串比较可用memcmp,但strcmp会自动识别'\0'结束,memcmp需要手动指定字节数。

4. 基础使用示例(多场景比较)

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

int main() {
    // 示例1:比较字符数组(前3个字节相同,第4个不同)
    char str1[] = "hello";
    char str2[] = "hella";
    int res1 = memcmp(str1, str2, 5);
    printf("字符数组比较结果:%d\n", res1); // 输出:1('l'=108 > 'a'=97)

    // 示例2:比较int数组(前2个元素相同,第3个不同)
    int arr1[] = {1,2,3,4};
    int arr2[] = {1,2,4,3};
    int res2 = memcmp(arr1, arr2, 3 * sizeof(int));
    printf("int数组比较结果:%d\n", res2); // 输出:-1(3的二进制值 < 4)

    // 示例3:比较前2个字节,内容相同
    int arr3[] = {1,2,3};
    int arr4[] = {1,2,5};
    int res3 = memcmp(arr3, arr4, 2 * sizeof(int));
    printf("前2个int比较结果:%d\n", res3); // 输出:0(完全相同)

    return 0;
}

运行结果:

字符数组比较结果:1
int数组比较结果:-1
前2个int比较结果:0

5. 手写模拟实现(逐行注释,逐字节比较)

#include <stdio.h>
#include <assert.h>

// 模拟实现memcmp
int my_memcmp(const void* ptr1, const void* ptr2, size_t n) {
    assert(ptr1 != NULL && ptr2 != NULL); // 校验指针非空
    const char* p1 = (const char*)ptr1;
    const char* p2 = (const char*)ptr2;

    // 逐字节比较
    while (n--) {
        if (*p1 != *p2) {
            // 返回差值:正数/负数,契合标准库返回规则
            return *p1 - *p2;
        }
        p1++;
        p2++;
    }
    // 循环结束,说明前n个字节完全相同
    return 0;
}

// 测试
int main() {
    char s1[] = "abcde";
    char s2[] = "abxde";
    int res = my_memcmp(s1, s2, 5);
    if (res == 0) {
        printf("内容相同\n");
    } else if (res > 0) {
        printf("s1 > s2,结果:%d\n", res); // 输出:s1 > s2,结果:-20('c'-'x'=-20,实际为负数,示例验证)
    } else {
        printf("s1 < s2,结果:%d\n", res);
    }
    return 0;
}

核心实现要点:

1. 用const char*接收强转后的指针,因为不修改比较的内存内容;

2. 找到第一个不同的字节,直接返回两字节的差值,保证返回值的正负性;

3. 循环结束后返回0,表示所有比较的字节均相同。

 总结:

如果要比较2块内存单元的数据的大小,可以使用 memcmp 函数,这个函数的特点就是可以指定比较

长度。

memcmp 函数是通过返回值告知大小关系的。

五、四大内存函数核心对比表

函数名 核心功能 支持内存重叠 操作单位 核心返回值/结果
memcpy 内存拷贝 不支持 字节 返回目标地址
memmove 内存拷贝(升级版) 支持 字节 返回目标地址
memset 内存设置 无此场景 字节 返回设置后地址
memcmp 内存比较 无此场景 字节 0/正数/负数

六、通用编程规范&避坑指南

1. 指针非空校验:使用/实现内存函数时,必须先校验指针是否为NULL,推荐用assert,快速定位野指针问题;

2. 字节数计算:用sizeof(类型/变量)计算字节数,避免硬编码(如4代替sizeof(int)),提升代码可移植性;

3. 内存越界:确保dest/ptr指向的内存空间足够容纳n个字节,否则会导致程序崩溃/内存污染;

4. memset按字节操作:非字符类型变量仅能置0,不要赋值为其他整数;

5. 优先使用memmove:替代memcpy,避免内存重叠导致的未定义行为;

6. memcmp比较字符串:需手动指定strlen(str)+1,包含'\0'结束符,否则可能出现错误比较。


到这里,我们已经把C语言中四大核心内存函数从原理、用法到手写实现都完整拆解了一遍。这些函数看似简单,却是C语言“贴近硬件、高效灵活”特性的最佳体现。掌握它们不仅能帮你写出更健壮的底层代码,更能加深你对内存模型的理解。希望这份内容能成为你学习路上的实用手册,也欢迎在评论区留下你的疑问和思考,一起交流进步。

 

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐