Bootstrap

【C语言】内存函数详解与模拟实现


在这里插入图片描述

拓展:

符号扩展是指将有符号类型的值转换为更宽的类型时,如果符号位为 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 的时候并不会停下来。
  • destsrc 有任意重叠,复制的结果都是未定义的(未拷贝内容被覆盖)。
  • 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 中的对应位置,然后 destsrc 指针后移继续拷贝,拷贝结束后返回 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);

​ 该是一个用于 比较两个内存块大小的函数。它会比较从 buf1buf2 指针开始的 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编译器下,内存采用的是小端存储方式,arr1arr2 在内存中的存储形式如图所示:

在这里插入图片描述

​ 所以,当比较字节数为 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 的实现可能因编译器和平台而异,上述实现只是一种常见的实现方式。
在这里插入图片描述

;