今天我们正式开始我们C语言在#include<string,h>这个头文件里头,内存函数的使用和学习。
在这篇博客里面我们将向大家隆重介绍的是下面四个函数:
memcpy,memmove,memset,memcmp
这四个函数又主要涵盖了内存拷贝,内存设置和内存比较三个方面的内容。
学习指南:对于内存拷贝部分的函数,我们需要了解其模拟实现,后面两个部分的函数我们只需要知道怎么使用即可。
一、内存拷贝:
memcpy函数:
1. 介绍:
函数原型:
void * memcpy ( void * dest, const void * sour, size_t num );
函数名 | 功能描述 | 头文件 |
memcpy | 进行各种数据类型的内存拷贝(不重叠的) | #include<string.h> |
参数说明:
- dest表示目标空间的起始地址。
- sour表示源空间的起始地址。
- num表示用户希望拷贝的字节数目。
- 函数最终的目的是将源空间sour里前num个字节的内容,拷贝到目标空间dest里面。
返回值说明:
最后返回目标空间的起始地址。
2. 基本使用:
对于memcpy函数,想必一些细心的小伙伴们已经发现了:这个函数和我们之前学习过的strncpy函数在名字和功能描述上都非常的相似。唯一的一个不同在于,memcpy函数是进行各种数据类型拷贝的,但是我们的strncpy函数只能进行我们的一个字符串或者说字符数组的拷贝。
那memcpy怎么使用也是非常简单啊,我们可以见下面这个例子:
#include<stdio.h>
#include<string.h>
int main()
{
int arr1[10] = { 0 };
int arr2[5] = { 1,2,3,4,5 };
memcpy(arr1, arr2, 20);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
【输出说明】
1 2 3 4 5 0 0 0 0 0
3. 模拟实现:
对于memcpy函数的模拟实现,大家可以参考下面这个代码:
#include<stdio.h>
void* my_memcpy(void* dest, const void* sour, size_t num)
{
void* ret = dest;
while (num--)
{
*(char*)dest = *(char*)sour;
dest = (char*)dest + 1;
sour = (char*)sour + 1;
}
return ret;
}
int main()
{
int arr1[10] = { 0 };
int arr2[6] = { 1,2,3,4,5,6};
my_memcpy(arr1, arr2, 24);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
注意:模拟实现中,对于dest = (char*)dest + 1这段代码有部分小伙伴认为可以用(char*)dest++或++(char*)dest来替代。但是由于++的优先级比强制类型转换来得更高,往往++操作会被编译器理解为是在强制类型转换之前(即便是后置++)。一方面程序不好理解。编译器对这个代码的不同解读也会产生意料之外的结果!
推介的做法:不要把++(自增自减)的这种操作符和强制类型转换放在一起使用。推介使用示例代码中的写法。
memmove函数:
1. 介绍:
void * memmove ( void * dest, const void * sour, size_t num );
函数名 | 功能描述 | 头文件 |
memmove | 进行各种数据类型的内存拷贝(重叠的) | #include<string.h> |
参数说明和返回值说明同memcpy函数。
2. 基本使用:
在正式开始介绍之前,我想小伙伴们已经迫不及待要问了。前面不是已经有一个memcpy函数了吗,现在为什么又来一个进行内存拷贝的函数memmove啊。
哈哈其实他们两个还真是不一样啊,想必大家也已经注意到了,我们关于两个函数的区别已经着重用红色字体进行了标注。但是用言语阐述还是太单薄了,这里我们就举一个例子:分别使用memcpy函数和memmove函数来拷贝我们的int数组arr:arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}。把12345拷贝到34567的位置上。
首先是我们的memcpy函数:
#include<stdio.h>
#include<string.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
memcpy(arr + 2, arr, 20);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
那你得到的结果可能是这样的:(不同的编译器在实现memcpy是用的源代码不一样,可能也会出现正确的结果)
那为什么是这样的呢?可想而知的是,在拷贝1 2 3 4 5到3 4 5 6 7的过程中,由于是从前往后拷贝的(见模拟实现代码),所以导致之后要被拷贝的3 4 5被1 2 1给覆盖掉了。
实际上啊我们的C语言标准(ANSIC)只是规定说:memcpy函数只需要负责好内存不重叠的内存块的拷贝就可以了。由我们前面提供的memcpy函数的模拟实现代码来看,memcpy在一般情况下只能完成不重叠内存的拷贝工作。但是不排除有一些编译器实现memcpy和memmove函数时用的是同一套代码。
那这个拷贝时内存有重叠的情况,就需要用到我们这里的memmove函数:(ANSIC规定说memmove要能够妥善处理好我们这里的重叠内存拷贝的问题)
3. 模拟实现:
模拟实现这里的memmove函数,有两种思路可供参考:
思路一:
创建新空间,将需要拷贝的内容放在新空间里面,然后把新空间的内容拷贝给dest。这种方式是一种不太友好的笨方式。
思路二:
不知道大家发现没,虽然前面的方法不能拷贝成功,但是在这里只要我们的拷贝方式不是从前往后的,而是从后往前的,就可以解决我们这里的问题。大家想一想是不是这样一回事。
另一个问题,就是说单纯的一个从后往前就可以了吗?显然不是的。还是上面那个数组,如果我希望你把3 4 5 6 7拷贝到1 2 3 4 5的位置上,这个时候是不是从前往后拷贝又好一些。
那什么时候从后往前,什么时候又从前往后呢。这个过程可以用下面这个示例图来形象概括:
图中的S表示源头,D表示目标空间。图中的信息告诉我们:
- 如果dest落在sour的左边,我们应该从前往后拷贝
- 如果是其他情况,我们从前往后拷贝即可。
据此我们可以写出下面这个代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
//模拟实现memmove函数:
void* my_memmove(void* dest, const void* src, size_t num)
{
assert(dest && src);
void* init = 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 init;
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("拷贝之前,我们的arr数组是:\n");
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
my_memmove(arr + 2, arr, 20);
printf("拷贝之后,我们的arr数组是:\n");
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
二、内存设置:
memset函数:
1. 介绍:
函数原型:
void * memset ( void * ptr, int value, size_t num );
函数名 | 功能描述 | 头文件 |
memset | 以字节为单位进行内存值的设置 | #include<string.h> |
参数说明:
- ptr表示待操作内存空间的首地址;
- value表示用户希望设置的值;
- num表示用户希望设置的内存空间的大小,以字节为单位。
- 注意设置内存的前提是ptr的所指向的内容允许被修改!换句话说ptr不能指向常量字符串且ptr指针指向的内容不能被const修饰。
返回值说明:
最后返回ptr本身。
2. 基本使用:
这个函数的使用比较简单,具体如何使用可以看下面这个例子:
#include<stdio.h>
#include<string.h>
int main()
{
char str[12] = "Hello World";
//我们使用memset函数将字符"Wor"设置为"ooo"
memset(str + 6, 'o', 3);
//看效果:
printf("%s", str);
return 0;
}
运行截图:
上面这个示例比较简单,有些小伙伴就想:既然参数ptr是void*的指针,那我应该也可以操作整型数组,于是就有了下面这两个同学写的代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
int main()
{
int i = 0;
int arr1[5] = { 1, 2, 3, 4, 5 };
int arr2[5] = { 2, 3, 4, 5, 6 };
//同学A希望把arr1中的元素全部设置为0
memset(arr1, 0, 20);
for (i = 0; i < 5; i++)
{
printf("%d ", arr1[i]);
}
printf("\n");
//同学B希望把arr2中的元素全部设置为1
memset(arr2, 1, 20);
for (i = 0; i < 5; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
运行截图:
小伙伴们想必已经发现,只有同学A得到了他想要的结果,那为什么同学B得不到他想要的结果呢。诶,在介绍这个函数那里我们就已经用红色笔给大家标注说:memset函数是以字节为单位进行内存设置的!也就是说他把arr2中从首元素开始的每一个字节都设置为9x01(十六进制的一)。
所以最终对于arr2而言,它的每一个元素的值都是0x01010101,而这个十六进制的值恰好就是16843009。
但是对于arr1就没有这个烦恼,因为是所有元素全部设置为0,每次将一个字节设置为0x00,最后每一个元素都是0x00000000,这恰好也是int类型的0
那有没有办法通过我们的memset来实现将一个数组中所有元素设置为除0以外的其他数字,就比如说1,其实也有办法,只是通过memset函数来设置稍微有点麻烦。由于int类型的1 = 0x0000 0001,所以每个元素最好设置为0x0000 0001好一些,据此我们可以对上面B同学的代码作如下处理:
#include<stdio.h>
#include<string.h>
int main()
{
int i = 0;
int arr2[5] = { 2, 3, 4, 5, 6 };
//同学B希望把arr2中的元素全部设置为1
//我们可以每四个字节,每四个字节进行设置,那20个字节就一共需要设置5次:
for (i = 0; i < 5; i++)
{
//第一步:将每个元素的最低位的第一个字节设置为0x01:
memset(arr2 + i, 1, 1);
//第二步:把每个元素中剩余的三个字节设置为0x00,值得注意的是要先把之前的指针强转成char*之后再加一:
memset(((char*)(arr2 + i)) + 1, 0, 3);
}
for (i = 0; i < 5; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
运行截图:
总结来说:memset函数更适合对字符数组这样的,每个元素的大小只有一个字节的,内存对象进行操作。
三、内存比较:
memcmp函数:
1. 介绍:
函数原型:
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
函数名 | 功能描述 | 头文件 |
memcmp | 用于比较两块内存块的大小 | #include<stdio.h> |
参数说明:
- ptr1和ptr2是待比较的两个内存块的地址;
- num是用户希望比较ptr1和ptr2前num个字节的内容。
返回值说明:
按照内存地址从低到高,每次取一个字节进行比较,直到出现不相等的结果,或者比较完num个字节为止。
最后返回一个整数表示ptr1和ptr2之间的大小关系:
Value | 含义 |
< 0 | 表示ptr1<ptr2 |
= 0 | 表示ptr1=ptr2 |
>0 | 表示ptr1>ptr2 |
这个大于0和小于0的数字到底是谁,具体得看编译器!
2. 基本使用:
细心的小伙伴们已经发现,这个函数和我们之前学过的strncmp特别像,唯一的不同就是memcmp这个函数是针对任意数据类型进行内存值大小的比较的,但是我们的strncmp只能对字符串或者说字符数组进行一个内存块的比较啊。
那具体如何使用,我们可以看下面这个例子啊:(这个函数的最终结果受到设备本身存储模式的影响,同一个例子放在一个不同的存储模式机器里面,最终的结果是不一样的。但钱博主使用的VS2022是小端字节序存储)
#include<stdio.h>
#include<string.h>
//当前机器是小端字节序存储(即低位字节序放在内存的低地址处)
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
//示范ptr1大于ptrr2的情况:
int arr2[3] = { 1,2,2 };
printf("如果ptr1 > ptr2: %d\n", memcmp(arr1, arr2, 12));
//示范ptr1等于ptr3的情况:
/*
* 解释 *
在内存中3用十六进制表示:
3 = 0x0000 0003
小段字节序下,排在前面的是0x03
同理对于十六进制数0x0101 0103
排在最前面的也是03,故相等。
*/
int arr3[3] = { 1,2,0x01010103 };
printf("如果ptr1 = ptr2: %d\n", memcmp(arr1, arr3, 7));
//示范ptr1小于ptr2的情况:
int arr4[3] = { 1,2,4 };
printf("如果ptr1 < ptr2: %d", memcmp(arr1, arr4, 12));
return 0;
}
运行截图:
上面的例子也告诉我们,分析memcmp最终运行的结果时,不能只是单纯地只看数组里面的值得大小,对于一些复杂得场景,要善于结合memcmp函数的各个参数(尤其是第三个参数num)以及当前机器的存储模式,来分析得出正确的结果。
四、完结撒花:
到这里,这个字符串函数系列的学习也是正式告一段路了,考虑到部分小伙伴可能忘记了大小端字节序的知识。博主把之前的相关博客链接给大家放在下面了:
数据在计算机中的存储方式(二)——整数在计算机中的存储进阶篇_一个整数型存储到计算机中,存储的值还等于原来的值吗-CSDN博客
也希望大家多多关注,你的支持是博主最大的动力😘。
最后如果小伙伴对于博客中有啥不懂的或者觉得博主总结不到位的地方,还请评论区留言,博主会第一时间回复的,谢谢啦!