1. memcpy —— 内存拷贝
1.1 memcpy 的声明与用处
我们学习了 strcpy 这个函数,那么对于 memcpy 这个函数是很好理解的。mem 表 memory ,即内存,cpy 表 copy ,即拷贝。这个函数的优势在于可以拷贝任意类型的数据,而不是仅仅局限于字符串。那么我们通过 cplusplus 这个网站来观察这个函数的声明以及各个参数的意义。
我们翻译一下即可知道 memcpy 作用,即可以把 source 指针指向的内容拷贝复制至 destination 指针指向的空间。这个函数是不限数据类型的,并且可以精确拷贝 num 个字节的数据。
那么仔细观察这个函数的参数,与 strncpy 是比较类似的。这里也提到一句,即 memcpy 是不应该处理空间重叠的数据拷贝。
1.2 memcpy 的用法
#include <stdio.h>
#include <string.h>
int main()
{
int dest[5] = { 0 };
int src[5] = { 1,2,3,4,5 };
memcpy(dest, src, 20);//将 src 中的 20 个字节的数据拷贝至 dest 数组中
for (int i = 0; i < 5; i++)
{
printf("%d ", dest[i]);
}
return 0;
}
上面是典型的正确用法,那么刚才提到的 memcpy 不应该处理空间重叠的内容拷贝是什么意思呢?
#include <stdio.h>
#include <string.h>
int main()
{
int dest[10] = { 1,2,3,4,5,6,7,8,9,10 };
memcpy(dest + 3, dest, 20);
return 0;
}
这种做法是不安全的,在某些编译器运行时,会产生令人失望的效果。但在 Visual Studio 2022 或者其他版本的编译器中,memcpy 是能够处理空间重叠的拷贝的。如果在其他的编译器、其他的配置环境中,有可能是处理不了的,即产生令人失望的效果。这种效果是什么呢?就好比像我们的这段代码是想要将 1,2,3,4,5 拷贝至 4,5,6,7,8 的内存空间中,但是真正要拷贝的时候,1 会放到 4 的位置上,2 会放到 5 的位置上,3 会放到 6 的位置上,这时候 4 的位置上放的是 1 了,5 的位置上放的是 2 了,这就导致拿不到以前的 4,5 了。但是 VS 系列编译器是不会发生这种情况的。
1.3 memcpy 的模拟实现
#include <stdio.h>
#include <assert.h>
void* my_memcpy(void* dest, const void* src, unsigned int num)
{
assert(dest && src);//确保是有效指针
void* ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;//既然是一个字节一个字节拷贝,那么就将指针类型转换为 char* 类型
//强制类型转换只是一个临时变量,所以不适合自增或自减操作
dest = (char*)dest+1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int dest[5] = { 0 };
int src[5] = { 1,2,3,4,5 };
my_memcpy(dest, src, 20);
for (int i = 0; i < 5; i++)
{
printf("%d ", dest[i]);
}
return 0;
}
2. memcmp —— 内存比较
2.1 memcmp 的声明与用处
memcmp 与 strncpy 十分相似,都是比较每一对字节的数据,其工作原理也是一样。只不过对于 memcmp 来说,能操作的数据类型更加广泛。我们通过 cplusplus 这个网站来观察 memcmp 的声明以及各个参数的意义。
这个函数的参数意义以及用法与 strncmp 并没有本质上的差别,所以我们在这里就不过多介绍了。
2.2 memcmp 的用法
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[5] = { 1,3,3,4,5 };
int arr2[5] = { 1,2,3,4,5 };
int ret = memcmp(arr1, arr2, 20);
if (ret > 0)
printf("arr1 > arr2\n");
else if (ret < 0)
printf("arr1 < arr2\n");
else
printf("arr1 == arr2\n");
return 0;
}
我们需要注意的是,就算是进行整形数据的比较,实际上进行比较的时候还是一个字节一个字节的进行。
但是当我们碰到这段代码,memcmp 的劣势就体现出来了。
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[5] = { 1,16842752,3,4,5 };
int arr2[5] = { 1,2,3,4,5 };
int ret = memcmp(arr1, arr2, 20);
if (ret > 0)
printf("arr1 > arr2\n");
else if (ret < 0)
printf("arr1 < arr2\n");
else
printf("arr1 == arr2\n");
return 0;
}
我们掌握了上面分析的原理以及上一篇博客中 strcmp 的工作原理,我们就不难理解这一个“令人惊讶”的输出结果。
那么想要解决这个 “bug” C 语言是做不到的,因为 C 语言没有判断数据类型的函数,所以想要解决这个 “bug” 就要使用 C++ 的某些方法。
2.3 memcmp 的模拟实现
#include <stdio.h>
#include <assert.h>
int my_memcmp(const void* p1, const void* p2, unsigned int num)//不需要改变 p1、p2 指针指向的内容,所以用 const 修饰
{
assert(p1 && p2);//确保指针有效性
while (num-- && *(char*)p1 == *(char*)p2)
{
p1 = (char*)p1 + 1;
p2 = (char*)p2 + 1;
}
return *(char*)p1 - *(char*)p2;
}
int main()
{
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 1,2,2,4,5 };
int ret = my_memcmp(arr1, arr2, 20);
if (ret > 0)
printf("arr1 > arr2\n");
else if (ret < 0)
printf("arr1 < arr2\n");
else
printf("arr1 == arr2\n");
return 0;
}
3. memmove —— 内存拷贝
3.1 memmove 的声明与用处
memmove 的作用是与 memcpy 没区别的,都是内存拷贝。但是 memmove 的某些构造原理与 memcpy 是有一些细小的区别的。那这里就不在赘述 memmove 的声明以及各种参数的意义了,因为它与 memcpy 一样。
3.2 memmove 的用法
#include <stdio.h>
#include <string.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
memmove(arr + 2, arr, 20);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
我们在这里分析一下为什么 memmove 能够实现空间重叠的拷贝。
3.3 memmove 的模拟实现
#include <stdio.h>
#include <assert.h>
void* my_memmove(void* dest, const void* src, unsigned int num)
{
assert(dest && src);//确保指针有效性
void* ret = dest;
if (src < dest)//当 src 小于 dest 的地址时,从后向前拷贝
{
while (num--)
{
*((char*)dest + num) = *((char*)src + num);
}
}
else//其他情况则从前向后拷贝
{
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
return ret;
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
my_memmove(arr + 2, arr, 20);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
4. memset —— 内存设置
4.1 memset 的声明与用处
mem,表 memory,即内存,set 表 设置,memset 的用处是内存设置,但更适合叫做内存初始化。我们通过 cplusplus 这个网站来观察 memset 的声明以及各个参数的意义。
我们通过翻译可以理解,memset 需要三个参数,第一个为需要初始化空间的地址,第二个参数为需要初始化为什么的值,第三个参数为字节,即要初始化多少个字节。
4.2 memset 的用法
#include <stdio.h>
#include <string.h>
int main()
{
int arr[10];
memset(arr, 1, 40);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
不要奇怪,不要奇怪,这个函数的初始化也仅仅只能针对一个字节,整形是有 4 个字节的,每个字节都初始化为 1 ,那么打印的出来的结果很大,并不奇怪。
4.3 memset 的模拟实现
#include <stdio.h>
#include <assert.h>
void* my_memset(void* dest, int value, unsigned int num)
{
assert(dest);
void* ret = dest;
while (num--)
{
*(char*)dest = value;// int 类型数据向 char 类型空间赋值会发生截断,所以语法没有错误
dest = (char*)dest + 1;
}
return ret;
}
int main()
{
int arr[10];
my_memset(arr, 1, 40);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}