文章目录
拓展:
符号扩展是指将有符号类型的值转换为更宽的类型时,如果符号位为 1
,则在更高位填充 1
,以保持符号不变。
例如,将 signed char
类型的值 -1
转换为 int
类型时,符号位为 1
,因此在 int
类型的高位填充 1
,得到的值为 -1
。
在实现内存操作函数时,符号扩展可能会导致错误的结果。例如,当我们使用 char
类型表示内存中的每个字节时,如果对于一个带符号的 char
变量,其值为 -128
,那么在进行比较或赋值时,将会发生符号扩展,导致实际比较或赋值的值不是我们期望的值。
为了避免符号扩展的问题,我们应该使用无符号类型来表示内存中的每个字节。由于无符号类型不会发生符号扩展,它可以更好地保持实际值不变。在实际使用中,应该根据具体情况选择合适的类型,以确保函数的正确性和安全性。
Ⅰ. memcpy – 内存拷贝
1、函数介绍与使用
#include <string.h>
void *memcpy(void *dest, const void *src, size_t count);
memcpy
函数是一个用于拷贝两个不相关的内存块的函数。memcpy
函数会从 src
的位置开始向后复制 count
个字节的数据到 dest
的内存位置,并返回 dest
的首地址。
☢️注意:
memcpy
函数在遇到\0
的时候并不会停下来。- 若
dest
和src
有任意重叠,复制的结果都是未定义的(未拷贝内容被覆盖)。 memcpy
函数在实现的时候,不知道未来会被用来拷贝什么样的数据,所以参数的指针类型为void*
(可接收任意类型指针)。
举个例子,比如我们要将 arr1
数组中的 1,2,3,4
拷贝到 arr2
数组中。
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[4] = { 1, 2, 3, 4 };
int arr2[6];
memcpy(arr2, arr1, sizeof(int)*4);
for (int i = 0; i < 4; ++i)
printf("%d ", arr1[i]);
printf("\n");
for (int i = 0; i < 6; ++i)
printf("%d ", arr2[i]);
return 0;
}
// 运行结果:
1 2 3 4
1 2 3 4 -858993460 -858993460
因为一个整型的大小是 4
个字节,而我们要拷贝 4
个整型到 arr2
中,所以传参的时候第三个参数 count
的大小为 16
。
2、模拟实现
进入函数体首先保存 dest
的起始位置,便于之后返回。然后循环 count
次,每次将 src
中的一个字节的内存数据拷贝到 dest
中的对应位置,然后 dest
和 src
指针后移继续拷贝,拷贝结束后返回 dest
原来的首地址即可。
void* my_memcpy(void* dest, const void* src, size_t count)
{
if (dest == NULL || src == NULL) // 检查指针是否为空
return NULL;
char* pDest = (char*)dest;
const char* pSrc = (const char*)src;
if (pDest > pSrc && pDest < pSrc + count) // 检查是否重叠,也就是dest在src要复制空间的范围内
{
// 重叠要反向拷贝
for (size_t i = count; i > 0; --i)
pDest[i - 1] = pSrc[i - 1];
}
else
{
for (size_t i = 0; i < count; ++i)
pDest[i] = pSrc[i];
}
return dest;
}
这个 if
语句的作用是检查内存区域是否重叠。如果内存区域重叠,则需要反向拷贝,以免出现数据覆盖的情况。
具体来说,如果目标地址 dest
比源地址 src
更靠后(即 pDest > pSrc
),并且目标地址 dest
在源地址 src
和源地址 src+count
之间(即 pDest < pSrc + count
),则说明 内存区域重叠。此时我们需要反向拷贝,以免在拷贝过程中覆盖未拷贝的数据。
举个例子,假设有一个长度为 5
的内存区域,其起始地址为 0x100
。现在要将这个内存区域拷贝到起始地址为 0x104
的位置。在这种情况下,如果我们按照正常的方式从前往后拷贝,那么在拷贝到第 3
个字节时,会将原来的第 3
个字节覆盖掉。因此,我们需要从后往前拷贝,以免出现这种情况。
Ⅱ. memmove – 内存拷贝
1、函数介绍与使用(与memcpy函数的区别)
#include <string.h>
void *memmove( void *dest, const void *src, size_t count );
我们发现 memmove
函数的参数和返回值与 memcpy
函数一模一样。没错,memmove
函数和 memcpy
函数的功能一样,也是从 src
的位置开始向后复制 count
个节的数据到 dest
的内存位置,并返回 dest
的首地址。
那么它们有什么不同呢❓❓❓
memmove
函数和 memcpy
函数的差别就是,memmove
函数的源内存块和目标内存块是可以重叠的,而 memcpy
函数的源内存块和目标内存块是不可以重叠的。
举个例子,比如我们要将 arr
数组中的 1,2,3,4
拷贝到 3,4,5,6
的位置上去,让 arr
数组变为 1,2,1,2,3,4,7,8,9,10
。
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
memcpy(arr + 2, arr, 16);
for (int i = 0; i < 10; ++i)
printf("%d ", arr[i]);
return 0;
}
// 运行结果
1 2 1 2 1 2 7 8 9 10
我们可以看到,此时源内存块和目标内存块就发生了重叠。我们先把 1,2
拷贝放到了 3,4
的位置,但当我们想把 3,4
拷贝放到 5,6
的位置的时候发现 3,4
位置上放的数字变成了 1,2
, 所以就把 1,2
拷贝放到了 5,6
的位置,于是数组就变成了 1,2,1,2,1,2,7,8,9,10
。
memmove
函数便可以很好的解决这个问题。所以当源内存块和目标内存块发生重叠的时候,就得使用 memmove
函数处理。
☢️注意:现在的编译器和 memcpy
的实现已经解决了这个问题,但是我们还是会介绍 memmove
函数!
2、模拟实现
其实这个模拟实现和上面我们写的 my_memcpy
是一样的,具体参考上面!
void* my_memmove(void* dest, const void* src, size_t count)
{
if (dest == NULL || src == NULL) // 检查指针是否为空
return NULL;
char* pDest = (char*)dest;
const char* pSrc = (const char*)src;
if (pDest > pSrc && pDest < pSrc + count) // 检查是否重叠,也就是dest在src要复制空间的范围内
{
// 重叠要反向拷贝
for (size_t i = count - 1; i >= 0; --i)
pDest[i] = pSrc[i];
}
else
{
for (size_t i = 0; i < count; ++i)
pDest[i] = pSrc[i];
}
return dest;
}
Ⅲ. memcmp – 内存比较
1、函数介绍与使用
#include <string.h>
int memcmp( const void *buf1, const void *buf2, size_t count);
该是一个用于 比较两个内存块大小的函数。它会比较从 buf1
和 buf2
指针开始的 count
个字节,当 buf1
大于 buf2
的时候返回一个大于 0
的数;当 buf1
等于 buf2
的时候返回 0
;当 buf1
小于 buf2
的时候返回一个小于 0
的数。
举个例子:
int main()
{
int arr1[] = { 1, 2, 3, 4 };
int arr2[] = { 1, 2, 4, 5 };
int ret1 = memcmp(arr1, arr2, 8);
int ret2 = memcmp(arr1, arr2, 9);
printf("%d\n%d\n", ret1, ret2);
int arr3[] = { 1, 2, 4, 5 };
int ret3 = memcmp(arr2, arr3, 12);
printf("%d", ret3);
return 0;
}
// 运行结果:
0
-1
0
在VS编译器下,内存采用的是小端存储方式,arr1
和 arr2
在内存中的存储形式如图所示:
所以,当比较字节数为 8
时,返回值为 0
;当比较字节数为 9
时,返回值为一个负数。
2、模拟实现
int my_memcmp(const void* s1, const void* s2, size_t n)
{
if (s1 == NULL || s2 == NULL) // 检查指针是否为空
return 0;
// 转化为unsigned char类型来表示内存中的每个字节,以避免符号扩展的问题
const unsigned char* p1 = (const unsigned char*)s1;
const unsigned char* p2 = (const unsigned char*)s2;
for (size_t i = 0; i < n; ++i)
{
if (p1[i] != p2[i])
return p1[i] > p2[i] ? 1 : -1;
}
return 0; // 相等直接返回0
}
Ⅳ. memset – 内存设置
1、函数介绍与使用
#include <string.h>
void *memset( void *dest, int c, size_t count);
将内存块的某一部分设置为特定的字符,设置时候是一个字节一个字节地设置的。
dest
:开始设置内存的起始位置。c
:要将内存设置成的字符。count
:从起始位置开始需要设置的内存的字节数。
举个例子:
int main()
{
char arr[] = "hello world!";
memset(arr, '*', 5);
printf("%s", arr);
return 0;
}
// 运行结果:
***** world!
2、模拟实现
void* my_memset(void * ptr, int value, size_t num)
{
unsigned char* p = (unsigned char*)ptr;
unsigned char v = (unsigned char)value;
size_t i;
for (i = 0; i < num; i++)
{
p[i] = v;
}
return ptr;
}
该实现首先将 void
指针 ptr
强制转换为 unsigned char
指针 p
,以便能够按字节进行访问。然后将 value
强制转换为 unsigned char
类型 v
,确保只保留低 8
位。接下来,使用一个循环逐个将内存区域中的字节设置为 v
。最后,返回原始的指针 ptr
。
需要注意的是,memset
的实现可能因编译器和平台而异,上述实现只是一种常见的实现方式。