【C语言内存函数精选】memcpy、memset、memmove及仿真实现!掌握内存操作的艺术!
❤️博客主页: 小镇敲码人
🍏 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌞任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️我的努力求学没有得到别的好处,只不过是愈来愈发觉自己的无知。 💞 💞 💞
1. memcpy
1.1 memcpy的使用介绍
从以上图片我们可以知道关于memcpy的以下信息:
memcpy
的功能是进行内存拷贝,它可用作字符串的拷贝(类似于strcpy
的功能)、整形数组的拷贝、结构体的拷贝。memcpy
有三个参数: - 前两个参数是指针,都是void *
类型的指针,只不过另外一个是被拷贝的目的对象,一个是拷贝对象,所以用const
修饰表示里面的内容不可修改。- 最后一个参数类型是size_t
类型是传需要拷贝的字节数。memcpy
的返回值是一个(void *)指针,返回被拷贝对象的起始地址。
下面我们通过几段代码来演示
memcpy
函数的使用:
- 整形数组的拷贝
#include<stdio.h>#include<string.h>intmain(){int arr1[]={0,1,2,3,4,5,6,7,8,9,10};// 声明并初始化一个整型数组 arr1int arr2[20]={0};// 声明并初始化一个大小为 20 的整型数组 arr2,所有元素初始化为 0int* ret =(int*)memcpy(arr2, arr1,44);// 使用 memcpy 函数将 arr1 中的元素复制到 arr2 中,总共复制 44 个字节for(int i =0; i <11; i++){printf("%d\n",*(ret + i));// 打印复制后的 arr2 数组中的元素}return0;}
- 由于返回的是被拷贝数组的首元素地址,所以我们需要用
int*
类型的指针接受,但是返回的指针的类型是void*
类型的,这类指针在使用前需要进行强制类型的转换,具体可看我的这篇博客里面有对void*
类型指针使用的具体介绍,【C语言进阶技巧】指针掌握之道:深入挖掘指针的无尽潜力(第二部)。另外访问数组里面的元素也可以用[]
来访问。 运行结果: - 字符串的拷贝
#include<stdio.h>#include<string.h>intmain(){char arr1[]="abcdefwbwb";// 声明一个字符数组 arr1,并初始化为 "abcdefwbwb"char arr2[11]={0};// 声明一个字符数组 arr2,长度为 11,并初始化为全零char* ret =(char*)memcpy(arr2, arr1,11);// 使用 memcpy 函数将 arr1 的内容复制到 arr2 中,复制长度为 11printf("%s\n", ret);// 打印复制后的结果return0;}
运行结果:
- 单精度浮点数的拷贝
#include<stdio.h>#include<string.h>intmain(){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 中的元素}return0;}
运行结果:
1.2 memcpy的模拟实现
#include<stdio.h>#include<assert.h>// 自定义的 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,整型数组拷贝voidtest1(){int arr1[]={0,1,2,3,4,5,6,7,8,9,10};// 声明并初始化一个整型数组 arr1int arr2[20]={0};// 声明并初始化一个大小为 20 的整型数组 arr2,所有元素初始化为 0int* 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,字符串拷贝voidtest2(){char arr1[]="abcdefwbwb";char arr2[11]={0};printf("字符串拷贝:>\n");char* ret =(char*)my_memcpy(arr2, arr1,11);printf("%s\n", ret);}// 测试函数3,单精度浮点数数组拷贝voidtest3(){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));}}intmain(){test1();test2();test3();return0;}
这里对
my_memcpy
关键部分做一下阐述:
- 使用
void *
指针来接收地址,是因为内存拷贝传过来的对象指向的类型是,有可能是字符、结构体、整形或者浮点型,所以万能指针来接收不同类型的地址就可解决类型不符的问题。 void*
指针在使用之前需要进行强制类型转换,使用char*
来强制类型转换是因为这种指针的间接级别是最小的,加一减一只跳过了一个字节,假设我们使用int*
类型,加一减一跳过了4个字节,那如果遇见字符串拷贝的话,就无法找到每个字符的地址,从而不能拷贝成功,所以我们应该使用强制转换为char*
的指针,因为这样可以做到逐字节拷贝,不可能会遗漏掉内容,恰好我们memcpy
函数的第三个参数是num
指需要拷贝的字节数。 - 注意强制类型转换是临时的强转,当你想要使用void*
类型的指针时就必须强制类型转换,在将两个指针后移一个字节时,你可能会这样写(char*)dest++
,这种写法是错误的,因为强制类型转换是临时的,++
的优先级更高先作用于dest
,此时上一行的强转已经不起效了,而(char*)
没有和dest作用,导致dest的类型还是void*
类型,而void*
类型的指针不先强制类型转换是不能++
的,所以这里会报错,使用++(char*)dest
在有些编译器上面可以通过,但是为了一劳永逸,我们这样写更好dest = (char*)dest+1
。
运行结果:
但是如果我们想用这个
my_mencpy
函数进行重叠内存的拷贝,就欠妥了,请看如下代码:
#include<stdio.h>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;// 返回目标地址的起始位置}intmain(){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));}return0;}
从数组下标
i
等于0开始拷贝,拷贝到数组下标为2的位置往后5个元素,按理来说,答案应该是
1 2 1 2 3 4 5 8 9 10
,我们看运行结果:
- 可以看到结果和我们预期的不同,原地址的值被覆盖了,说明
my_memcpy
不适合重叠内存的拷贝,事实上,我们有一个的内存函数memmove
来负责重叠内存的拷贝。 - 细心的友友可能会发现这次博主的
my_memcpy
函数做了微调,由于目的地址和源地址在同一数组,我们想利用指针来访问数组应该把数组的首元素地址赋给ret
。 - 如果你发现有些编译器上
memcpy
也能拷贝重叠内存,那估计是它库函数的实现超过了预期,C语言规定是memcpy
不负责重叠内存拷贝,所以不能保证所有的编译器memcpy
都可以实现重叠内存的拷贝。
2. memmove
2.1 memmove的使用介绍
memmove
这个函数的参数和memcpy
的参数是一样的,它的功能是负责重叠内存的拷贝,它也可以实现不重叠的内存拷贝,下面我们通过一段代码来演示memmove
函数的使用。
#include<stdio.h>#include<string.h>intmain(){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]);}return0;}
运行结果:
这和我们之前预期的结果是一致的。
2.2 memmove的模拟实现
#include<stdio.h>#include<assert.h>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;// 返回目标地址的起始位置}intmain(){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]);}return0;}
我们通过画图来分析这段代码核心部分
my_memmove
的思路:
*((char*)dest+num) = *((char*)src+num);
这段代码的意思是从后往前开始拷贝,num--
后跳过19个字节,刚好指向了最后一个字节的内容。 运行结果:
- 值得一提的是,无论是
memcpy
还是memmove
函数,拷贝的字节数都不能比目的对象的空间要大,否则系统就会报错,这造成了缓冲区溢出的问题,请看如下代码:
intmain(){int arr1[]={1,2,3,4,5,6,7,8,9,10};int arr2[5]={0};memcpy(arr2, arr1,40);}
运行结果:
- 还有一种情况就是源对象的大小比
num
要小,但是目的空间大小足够,此时虽然在VS编译器上虽然不会报错,但是也不建议这样去做,因为还是进行了非法的内存访问,请看如下代码:
intmain(){int arr1[]={1,2,3,4,5,6,7,8,9,10};int arr2[20]={0};memcpy(arr2, arr1,80);}
编译器还是报了警告:
我们通过调试也可以看一看此时
arr2
中放的是什么:
可以看到除了前10个元素与
arr1
相同后10个元素是随机值。
- 这两种警告显然前一种更加致命,从编译器的反应就可以看出,因为前面一种是
arr2
的空间不足,然后你把不属于arr2
空间的地址也写入了内容,这就造成了缓冲区溢出的问题,更为严重,而后者只是非法访问了一下后面地址的内容,而没有进行其它的操作,相对较轻,但也应该尽可能避免,防止引发其它的错误。
3. memcmp
3.1 menmcmp的使用介绍
memcmp
是内存比较函数,num
是要比较的字节数。memcmp
函数是按字节进行比较的,并且它是无符号字节比较。在这种比较方式下,会先比较两个内存块的第一个字节,如果相等,则继续比较下一个字节,直到发现不相等的字节或者比较完所有字节。
下面我们通过下面代码来演示一下
memcmp
函数的使用:
#include<stdio.h>#include<string.h>intmain(){int arr1[]={1,2,1,4,5,6};int arr2[]={1,2,257};printf("%d\n",memcmp(arr1, arr2,9));return0;}
运行结果:
但是当我们比较到10个字节时它却返回-1,请看运行代码:
这是为什么呢?我们通过调试来看一下
arr1
和
arr2
的内存的存储就知道了,我们通过画图来分析:
因为
memcmp
是一个字节一个字节的比较,比较整形大小是不合适的,因为负数在内存中是补码的形式存储,而
memcmp
是无符号位字节,另外如果仅仅比较正整数,小端存储下也是不合适的,因为如果一个大数,前面一个低地址的字节处是都是0,而高地址处是非0的,当它和整数1比较,系统一个字节一个的比较,第一个字节处就不同了,系统就自动返回-1了,而不会管你后面的字节里面放的是什么,下面我们用一段代码来演示一下:
#include<stdio.h>#include<string.h>intmain(){int arr1[]={256};int arr2[]={1};printf("%d\n",memcmp(arr1, arr2,4));return0;}
运行结果:
如果你不信,可以比较前4个字节的数据:
我们这里依然画图分析一下:
- 一个字符只占一个字节,无论是大端存储还是小端存储都不会影响它在内存中的存储,而
memcmp
函数每次比较一个字节就相当于比较了一个字符,所以可以用memcmp
函数来比较字符对象。
通过下面代码我们来演示一下通过
memcmp
函数来比较字符串的大小:
#include<stdio.h>#include<string.h>intmain(){char arr1[]="abcfefabcdef";char arr2[]="abcg";printf("%d\n",memcmp(arr1, arr2,4));return0;}
运行结果:
当然你可能会说这可能是一个特例,我们将
g
字符改为
f
,按照字符串的比较规则,只比较前4个字符,应该返回0,我们看运行结果:
我们将
memcmp
函数的第三个参数改为5,比较前5个字符,由于
arr2
的第5个字符是
\0
,
\0
的ASCII码值是0,所以应该返回大于0的数字,我们来看运行结果:
结果确实和我们预期的一样,说明
memcmp
函数是可以进行字符对象的比较的,并且但看这一点,它与
strncmp
函数是非常相似的,如果你不了解这个函数可以看一下博主的这篇文章【C语言进阶技巧】探秘字符与字符串函数的奇妙世界。
字符在内存中是以ASCII码值来存储的,它本质上也是整形家族。
4. memset函数
4.1 memset函数的使用介绍
memset
函数的功能是进行内存设置,就是一个字节一个字节里面的数值。memset
函数有三个参数。 - 它的第一个参数是一个void*
的地址,也就是我们要设置的对象。- 它的第二个参数是一个int
型的value值,是我们设置的每个字节的数值。- 它的第三个参数是是一个size_t
类型的num值,它代表要设置的字节数。
下面我们通过具体的代码来演示
memset
函数是怎样使用的:
- 设置字符串
#include<stdio.h>#include<string.h>// 包含字符串处理相关的头文件intmain(){char arr1[]="hello world";// 定义一个字符数组 arr1,并初始化为字符串 "hello world"memset(arr1 +1,'x',4);// 使用 memset 函数将 arr1 中第二个字符及其后的 4 个字符都设置为字符 'x'printf("%s\n", arr1);// 打印修改后的字符串 arr1return0;// 返回 0,表示程序执行成功}
运行结果:
在上述代码中,我们希望将字符数组从第二个字符开始数往后的四个字符全部修改为字符
x
,我们传第二个字符的地址,和需要修改的字符,(这里注意:字符在内存中存贮是以ASCII码值的形式,所以字符本质上也是整形,我们用整形接收这个字符
x
实际上是接收了它的ASCII码值。),然后再传需要修改的字节数,这里由于
memset
函数是一个字节一个字节设置的,一个字符所占内存是一字节,所以设置一字节实际上就等同于设置了一个字符。
- 易错点:有朋友可能会使用字符指针去存字符串,然后再使用
memset
函数去修改却发现这时候程序崩了,这是因为指针指向了一个字符串常量,它是只读的,修改它是未定义行为。
如果我们想把整形数组里面的每个值都设置为1,是否可行呢?
- 设置整形数组
#include<stdio.h>#include<string.h>// 包含字符串处理相关的头文件intmain(){int arr1[10]={0};// 定义一个整型数组 arr1,大小为 10,并初始化所有元素为 0memset(arr1,1,8);// 使用 memset 函数将 arr1 中的前 8 个字节都设置为值为 1 的字节return0;// 返回 0,表示程序执行成功}
使用
memset
函数后,调试查看arr1数组在内存储存的值为:
可以看到此时
arr1
数组的前8个字节都被我们设置为了1,但是一个整形元素是4个字节,所以此时前两个元素并不是1,如果我们想把前2个元素都设置为1,显然用
memset
函数无法一次性达到,并且比较麻烦。
- 和
memcpy
、memmove
函数相同,如果你试图设置不属于你的内存,就会报缓冲区溢出的错误。
版权归原作者 小镇敲码人 所有, 如有侵权,请联系我们删除。