【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!

  • 1. memcpy
    • 1.1 memcpy的使用介绍
    • 1.2 memcpy的模拟实现
  • 2. memmove
    • 2.1 memmove的使用介绍
    • 2.2 memmove的模拟实现
  • 3. memcmp
    • 3.1 menmcmp的使用介绍
  • 4. memset函数
    • 4.1 memset函数的使用介绍

❤️博客主页: 小镇敲码人
欢迎关注:点赞 留言 收藏
任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。
❤️我的努力求学没有得到别的好处,只不过是愈来愈发觉自己的无知。

1. memcpy

1.1 memcpy的使用介绍

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第1张图片

从以上图片我们可以知道关于memcpy的以下信息:

  • memcpy的功能是进行内存拷贝,它可用作字符串的拷贝(类似于strcpy的功能)、整形数组的拷贝、结构体的拷贝。
  • memcpy有三个参数:
    • 前两个参数是指针,都是void *类型的指针,只不过另外一个是被拷贝的目的对象,一个是拷贝对象,所以用const修饰表示里面的内容不可修改。
    • 最后一个参数类型是size_t类型是传需要拷贝的字节数。
  • memcpy的返回值是一个(void *)指针,返回被拷贝对象的起始地址。

下面我们通过几段代码来演示memcpy函数的使用:

  • 整形数组的拷贝
#include 
#include 

int main()
{
    int arr1[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };  // 声明并初始化一个整型数组 arr1
    int arr2[20] = { 0 };  // 声明并初始化一个大小为 20 的整型数组 arr2,所有元素初始化为 0
    int* ret = (int*)memcpy(arr2, arr1, 44);  // 使用 memcpy 函数将 arr1 中的元素复制到 arr2 中,总共复制 44 个字节

    for (int i = 0; i < 11; i++)
    {
        printf("%d\n", *(ret + i));  // 打印复制后的 arr2 数组中的元素
    }
     return 0;
}

  • 由于返回的是被拷贝数组的首元素地址,所以我们需要用int*类型的指针接受,但是返回的指针的类型是void*类型的,这类指针在使用前需要进行强制类型的转换,具体可看我的这篇博客里面有对void*类型指针使用的具体介绍,【C语言进阶技巧】指针掌握之道:深入挖掘指针的无尽潜力(第二部)。另外访问数组里面的元素也可以用[]来访问。
    运行结果:
    【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第2张图片
  • 字符串的拷贝
#include
#include

int main()
{
    char arr1[] = "abcdefwbwb";  // 声明一个字符数组 arr1,并初始化为 "abcdefwbwb"
    char arr2[11] = { 0 };  // 声明一个字符数组 arr2,长度为 11,并初始化为全零
    char* ret = (char*)memcpy(arr2, arr1, 11);  // 使用 memcpy 函数将 arr1 的内容复制到 arr2 中,复制长度为 11

    printf("%s\n", ret);  // 打印复制后的结果

    return 0;
}

运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第3张图片

  • 单精度浮点数的拷贝
#include
#include

int main()
{
    float arr1[] = { 1.0, 2.0, 3.0 };  // 声明一个浮点数数组 arr1,并初始化为 {1.0, 2.0, 3.0}
    float arr2[10] = { 0 };  // 声明一个浮点数数组 arr2,长度为 10,并初始化为全零
    float* ret = (float*)memcpy(arr2, arr1, 12);  // 使用 memcpy 函数将 arr1 的内容复制到 arr2 中,复制长度为 12 字节

    for (int i = 0; i < 3; i++)
    {
        printf("%f\n", *(ret + i));  // 打印复制后的结果,即 arr2 中的元素
    }

    return 0;
}

运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第4张图片

1.2 memcpy的模拟实现

#include 
#include 

// 自定义的 memcpy 函数实现
void* my_memcpy(void* dest, void* src, size_t num)
{
    void* ret = dest;  // 保存目标地址的起始位置
    assert(dest && src);  // 确保目标地址和源地址非空
    while (num--)
    {
        *(char*)dest = *(char*)src;  // 逐字节复制
        dest = (char*)dest+1;  // 指针地址后移一位
        src = (char*)src + 1;  // 源地址也后移一位
    }
    return ret;  // 返回目标地址的起始位置
}

// 测试函数1,整型数组拷贝
void test1()
{
    int arr1[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };  // 声明并初始化一个整型数组 arr1
    int arr2[20] = { 0 };  // 声明并初始化一个大小为 20 的整型数组 arr2,所有元素初始化为 0
    int* ret = (int*)my_memcpy(arr2, arr1, 44);  // 使用 my_memcpy 函数将 arr1 中的元素复制到 arr2 中,总共复制 44 个字节
    printf("整型数组拷贝:>\n");
    for (int i = 0; i < 11; i++)
    {
        printf("%d\n", *(ret + i));  // 打印复制后的 arr2 数组中的元素
    }
}

// 测试函数2,字符串拷贝
void test2()
{
    char arr1[] = "abcdefwbwb";
    char arr2[11] = { 0 };
    printf("字符串拷贝:>\n");
    char* ret = (char*)my_memcpy(arr2, arr1, 11);
    printf("%s\n", ret);
}

// 测试函数3,单精度浮点数数组拷贝
void test3()
{
    float arr1[] = { 1.0, 2.0, 3.0 };
    float arr2[10] = { 0 };
    float* ret = (float*)my_memcpy(arr2, arr1, 12);
    printf("单精度浮点数数组拷贝:>\n");
    for (int i = 0; i < 3; i++)
    {
        printf("%f\n", *(ret + i));
    }
}

int main()
{
    test1();
    test2();
    test3();
    return 0;
}

这里对my_memcpy关键部分做一下阐述:

  1. 使用void *指针来接收地址,是因为内存拷贝传过来的对象指向的类型是,有可能是字符、结构体、整形或者浮点型,所以万能指针来接收不同类型的地址就可解决类型不符的问题。
  2. void*指针在使用之前需要进行强制类型转换,使用char*来强制类型转换是因为这种指针的间接级别是最小的,加一减一只跳过了一个字节,假设我们使用int*类型,加一减一跳过了4个字节,那如果遇见字符串拷贝的话,就无法找到每个字符的地址,从而不能拷贝成功,所以我们应该使用强制转换为char*的指针,因为这样可以做到逐字节拷贝,不可能会遗漏掉内容,恰好我们memcpy函数的第三个参数是num指需要拷贝的字节数。
    • 注意强制类型转换是临时的强转,当你想要使用void*类型的指针时就必须强制类型转换,在将两个指针后移一个字节时,你可能会这样写(char*)dest++,这种写法是错误的,因为强制类型转换是临时的,++的优先级更高先作用于dest,此时上一行的强转已经不起效了,而(char*)没有和dest作用,导致dest的类型还是void*类型,而void*类型的指针不先强制类型转换是不能++的,所以这里会报错,使用++(char*)dest在有些编译器上面可以通过,但是为了一劳永逸,我们这样写更好dest = (char*)dest+1

运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第5张图片

但是如果我们想用这个my_mencpy函数进行重叠内存的拷贝,就欠妥了,请看如下代码:

#include
void* my_memcpy(void* dest, void* src, size_t num)
{
    void* ret = dest;  // 保存目标地址的起始位置
    assert(dest && src);  // 确保目标地址和源地址非空
    while (num--)
    {
        *(char*)dest = *(char*)src;  // 逐字节复制
        dest = (char*)dest + 1;  // 指针地址后移一位
        src = (char*)src + 1;  // 源地址也后移一位
    }
    return ret;  // 返回目标地址的起始位置
}

int main()
{
     int arr1[] = { 1,2,3,4,5 ,6,7,8,9,10};
     int * ret = (int*)my_memcpy(arr1 + 2, arr1, 20);
    for (int i = 0; i < 10; i++)
    {   
        printf("%d",*(ret+i));
    }
    return 0;
}

从数组下标i等于0开始拷贝,拷贝到数组下标为2的位置往后5个元素,按理来说,答案应该是1 2 1 2 3 4 5 8 9 10,我们看运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第6张图片

  • 可以看到结果和我们预期的不同,原地址的值被覆盖了,说明my_memcpy不适合重叠内存的拷贝,事实上,我们有一个的内存函数memmove来负责重叠内存的拷贝。
  • 细心的友友可能会发现这次博主的my_memcpy函数做了微调,由于目的地址和源地址在同一数组,我们想利用指针来访问数组应该把数组的首元素地址赋给ret
  • 如果你发现有些编译器上memcpy也能拷贝重叠内存,那估计是它库函数的实现超过了预期,C语言规定是memcpy不负责重叠内存拷贝,所以不能保证所有的编译器memcpy都可以实现重叠内存的拷贝。

2. memmove

2.1 memmove的使用介绍

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第7张图片

  • memmove这个函数的参数和memcpy的参数是一样的,它的功能是负责重叠内存的拷贝,它也可以实现不重叠的内存拷贝,下面我们通过一段代码来演示memmove函数的使用。
#include
#include

int main()
{
     int arr1[] = { 1,2,3,4,5 ,6,7,8,9,10};
     memmove(arr1 + 2, arr1,20);
    for (int i = 0; i < 10; i++)
    {   
        printf("%d ",arr1[i]);
    }
    return 0;
}

运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第8张图片
这和我们之前预期的结果是一致的。

2.2 memmove的模拟实现

#include 
#include 

void* my_memmove(void* dest, void* src, size_t num)
{
    assert(src && dest);  // 确保源地址和目标地址非空

    void* ret = dest;  // 保存目标地址的起始位置

    if (dest < src)  // 如果目标地址在源地址之后,执行正向复制
    {
        while (num--)
        {
            *(char*)dest = *(char*)src;  // 逐字节复制数据
            dest = (char*)dest + 1;  // 目标地址指针后移一位
            src = (char*)src + 1;  // 源地址指针后移一位
        }
    }
    else  // 如果目标地址在源地址之前,执行反向复制
    {
        while (num--)
        {
            *((char*)dest + num) = *((char*)src + num);  // 逐字节复制数据
        }
    }

    return ret;  // 返回目标地址的起始位置
}

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

    // 调用 my_memmove 函数将 arr1 数组中的元素复制到 arr1 数组的第 3 个位置开始,总共复制 20 个字节
    my_memmove(arr1 + 2, arr1, 20);

    // 打印复制后的 arr1 数组中的元素
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", arr1[i]);
    }
    return 0;
}

我们通过画图来分析这段代码核心部分my_memmove的思路:
【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第9张图片

  • *((char*)dest+num) = *((char*)src+num);这段代码的意思是从后往前开始拷贝,num--后跳过19个字节,刚好指向了最后一个字节的内容。
    运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第10张图片

  • 值得一提的是,无论是memcpy还是memmove函数,拷贝的字节数都不能比目的对象的空间要大,否则系统就会报错,这造成了缓冲区溢出的问题,请看如下代码:
int main()
{
	int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
	int arr2[5] = { 0 };
	memcpy(arr2, arr1, 40);
}

运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第11张图片

  • 还有一种情况就是源对象的大小比num要小,但是目的空间大小足够,此时虽然在VS编译器上虽然不会报错,但是也不建议这样去做,因为还是进行了非法的内存访问,请看如下代码:
int main()
{
	int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
	int arr2[20] = { 0 };
	memcpy(arr2, arr1,80);
}

编译器还是报了警告:
【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第12张图片

我们通过调试也可以看一看此时arr2中放的是什么:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第13张图片

可以看到除了前10个元素与arr1相同后10个元素是随机值。

  • 这两种警告显然前一种更加致命,从编译器的反应就可以看出,因为前面一种是arr2的空间不足,然后你把不属于arr2空间的地址也写入了内容,这就造成了缓冲区溢出的问题,更为严重,而后者只是非法访问了一下后面地址的内容,而没有进行其它的操作,相对较轻,但也应该尽可能避免,防止引发其它的错误。

3. memcmp

3.1 menmcmp的使用介绍

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第14张图片

  • memcmp是内存比较函数,num是要比较的字节数。

  • memcmp函数是按字节进行比较的,并且它是无符号字节比较。在这种比较方式下,会先比较两个内存块的第一个字节,如果相等,则继续比较下一个字节,直到发现不相等的字节或者比较完所有字节。

下面我们通过下面代码来演示一下memcmp函数的使用:

#include
#include
int main()
{
	int arr1[] = { 1,2,1,4,5,6 };
	int arr2[] = { 1,2,257 };
	printf("%d\n",memcmp(arr1, arr2, 9));
	return 0;
}

运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第15张图片
但是当我们比较到10个字节时它却返回-1,请看运行代码:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第16张图片
这是为什么呢?我们通过调试来看一下arr1arr2的内存的存储就知道了,我们通过画图来分析:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第17张图片
因为memcmp是一个字节一个字节的比较,比较整形大小是不合适的,因为负数在内存中是补码的形式存储,而memcmp是无符号位字节,另外如果仅仅比较正整数,小端存储下也是不合适的,因为如果一个大数,前面一个低地址的字节处是都是0,而高地址处是非0的,当它和整数1比较,系统一个字节一个的比较,第一个字节处就不同了,系统就自动返回-1了,而不会管你后面的字节里面放的是什么,下面我们用一段代码来演示一下:

#include
#include
int main()
{
	int arr1[] = {256};
	int arr2[] = { 1};
	printf("%d\n",memcmp(arr1, arr2, 4));
	return 0;
}

运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第18张图片
如果你不信,可以比较前4个字节的数据:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第19张图片
我们这里依然画图分析一下:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第20张图片

  • 一个字符只占一个字节,无论是大端存储还是小端存储都不会影响它在内存中的存储,而memcmp函数每次比较一个字节就相当于比较了一个字符,所以可以用memcmp函数来比较字符对象。

通过下面代码我们来演示一下通过memcmp函数来比较字符串的大小:

#include
#include
int main()
{
	char arr1[] = "abcfefabcdef";
	char arr2[] = "abcg";
	printf("%d\n", memcmp(arr1, arr2,4));
	return 0;
}

运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第21张图片
当然你可能会说这可能是一个特例,我们将g字符改为f,按照字符串的比较规则,只比较前4个字符,应该返回0,我们看运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第22张图片
我们将memcmp函数的第三个参数改为5,比较前5个字符,由于arr2的第5个字符是\0,\0的ASCII码值是0,所以应该返回大于0的数字,我们来看运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第23张图片
结果确实和我们预期的一样,说明memcmp函数是可以进行字符对象的比较的,并且但看这一点,它与strncmp函数是非常相似的,如果你不了解这个函数可以看一下博主的这篇文章【C语言进阶技巧】探秘字符与字符串函数的奇妙世界。

字符在内存中是以ASCII码值来存储的,它本质上也是整形家族。

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第24张图片

4. memset函数

4.1 memset函数的使用介绍

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第25张图片

  • memset函数的功能是进行内存设置,就是一个字节一个字节里面的数值。
  • memset函数有三个参数。
    • 它的第一个参数是一个void*的地址,也就是我们要设置的对象。
    • 它的第二个参数是一个int型的value值,是我们设置的每个字节的数值。
    • 它的第三个参数是是一个size_t类型的num值,它代表要设置的字节数。

下面我们通过具体的代码来演示memset函数是怎样使用的:

  • 设置字符串
#include
#include   // 包含字符串处理相关的头文件

int main()
{
    char arr1[] = "hello world";  // 定义一个字符数组 arr1,并初始化为字符串 "hello world"
    memset(arr1 + 1, 'x', 4);     // 使用 memset 函数将 arr1 中第二个字符及其后的 4 个字符都设置为字符 'x'
    printf("%s\n", arr1);        // 打印修改后的字符串 arr1

    return 0;  // 返回 0,表示程序执行成功
}

运行结果:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第26张图片
在上述代码中,我们希望将字符数组从第二个字符开始数往后的四个字符全部修改为字符x,我们传第二个字符的地址,和需要修改的字符,(这里注意:字符在内存中存贮是以ASCII码值的形式,所以字符本质上也是整形,我们用整形接收这个字符x实际上是接收了它的ASCII码值。),然后再传需要修改的字节数,这里由于memset函数是一个字节一个字节设置的,一个字符所占内存是一字节,所以设置一字节实际上就等同于设置了一个字符。

  • 易错点:有朋友可能会使用字符指针去存字符串,然后再使用memset函数去修改却发现这时候程序崩了,这是因为指针指向了一个字符串常量,它是只读的,修改它是未定义行为。

如果我们想把整形数组里面的每个值都设置为1,是否可行呢?

  • 设置整形数组
#include
#include   // 包含字符串处理相关的头文件

int main()
{
    int arr1[10] = { 0 };    // 定义一个整型数组 arr1,大小为 10,并初始化所有元素为 0

    memset(arr1, 1, 8);      // 使用 memset 函数将 arr1 中的前 8 个字节都设置为值为 1 的字节

    return 0;                // 返回 0,表示程序执行成功
}

使用memset函数后,调试查看arr1数组在内存储存的值为:

【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!_第27张图片

可以看到此时arr1数组的前8个字节都被我们设置为了1,但是一个整形元素是4个字节,所以此时前两个元素并不是1,如果我们想把前2个元素都设置为1,显然用memset函数无法一次性达到,并且比较麻烦。

  • memcpymemmove函数相同,如果你试图设置不属于你的内存,就会报缓冲区溢出的错误。

你可能感兴趣的:(C语言进阶篇,c语言,算法,开发语言,青少年编程,程序人生)