C语言梦开始的地方14:内存函数

本章和上章节的字符函数无本质区别,唯一的区别是字符函数只能用于字符串和字符数组,而该函数系列可以认为是通用函数,等下我们看其返回值就明白为什么说是通用函数了。

memcpy使用和模拟实现

void * memcpy ( void * destination, const void * source,  size_t num );
  • 函数memcpy从source的位置开始向后复制num个字节的数据到destination指向的内存位置。
  • 这个函数在遇到’\0’ 的时候并不会停下来。
  • 如果source和destination有任何的重叠,复制的结果都是未定义的。

memcpy对字符类型进行操作时等价于strncpy,而非strcpy。

#include <stdio.h>
#include <string.h>

int main()
{
    char str1[12];
    const char str2[] = "hello world";

    printf("%s\n",memcpy(str1,str2,12));
    return 0;
}

该函数也是在string.h的头文件中,但是memcpy在编程中使用不多。因为memcpy在复制中如果source和destination有任何的重叠,复制的结果都是未定义的。
我们先看一下正确使用的情况:

#include <stdio.h>
#include <string.h>

int main()
{
    int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int arr2[10] = {0};
    memcpy(arr2, arr1, 20);
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", arr2[i]);
    }
    return 0;
}

这里20是20个字节的意思,然后数组类型是int在当前编译器中占4个字节,也就是说复制只会复制arr1的前5个元素后面为0。
那么重叠情况呢?请看下面代码:

#include <stdio.h>
#include <string.h>

int main()
{
    int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int arr2[10] = {0};
    memcpy(arr2, arr1, sizeof(arr1));
    memcpy(arr2 + 2,arr2,20);
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", arr2[i]);
    }
    return 0;
}

那么为什么会这样呢?
这是因为memcpy复制时:是从低位地址到高位地址逐字节复制的

  1. 先复制 arr2[0] → arr2[2](此时 arr2[2] 变为 1)
  2. 再复制 arr2[1] → arr2[3](arr2[3] 变为 2)
  3. 接着复制 arr2[2] → arr2[4],但此刻 arr2[2] 已经被改为 1,所以 arr2[4] 变成 1(而非原来的 3)
  • 那么谁会在编程中傻到自己复制自己呢?那么有没有这种情况呢?
    首先要明确的是确实不会有人会使用memcpy对自己复制,但是有没有这种场景呢?是有的比如说网络编程中我们需要将未处理完的剩余数据挪到缓冲区的开头,腾出新空间给新数据。

  • 既然有这种情况?那么C语言是不是同样提供了一个安全的函数。
    没错C语言提供了一个memmove的函数就避免了这种情况

模拟实现一下memcpy

void* my_memcpy(void* dest,const void* src,int size)
{
    assert(dest != NULL);
    assert(src != NULL);

    void* ret = dest;

    while(size--)
    {
        *(char* )dest = *(char* )src;
        dest = (char* )dest + 1;
        src = (char* )src + 1;
    }

    return ret;
}

为什么这里是强制转换成char* 这是因为char是1字节的这样不管遇到那种类型都可以完美复制比如int,char只需要循环4次就可以复制到一个完整的int类型。这样设计也让memcpy不在局限一个场景而是能适应多个场景

注意:在测试memcpy代码时:
如果是gcc平台使用gcc -O2 -mstringop-strategy=byte_loop test.c -o test
如果是MSVC平台
1. 项目属性 → C/C++ → 优化 → 优化:设置为 禁用 (/Od)(关闭优化)。
2. 项目属性 → C/C++ → 代码生成 → 启用内部函数:设置为 否 (/Oi-)(禁用内部函数,防止编译器将 memcpy 替换为内联 SIMD 指令)。
3. 选择 Debug 配置,编译并运行。

就是有可能打印结果和预期不一样,这是因为底层给优化成混合了块拷贝与反向拷贝

memmove使用和模拟实现

void * memmove ( void * destination, const void * source,  size_t num );
  • 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
  • 如果源空间和目标空间出现重叠,就得使用memmove函数处理。
int main()
{
    int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    memmove(arr1 + 2,arr1,20);
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", arr1[i]);
    }
    return 0;
}

使用memmove就能稳定得到12123458910,而不是因为底层优化来获取到该结果。
那么memmove是怎么做到的呢?
memove中会进行判断高地址和低地址,比如上面这个代码arr1 + 2 > arr1那么此时它就会从最后一个字节开始,倒着往前复制,因为arr1 + 2是高地址,arr1是低地址。
那么arr1 在前面时就和memcpy差不多的处理方式

模拟实现memmove

void *my_memmove(void *dest, const void *src, int size)
{
    assert(dest != NULL);
    assert(src != NULL);

    if (size == 0 || dest == src)
        return dest;
    void *ret = dest;

    if (dest > src)
    {
        dest = (char *)dest + size - 1;
        src = (char *)src + size - 1;
        while (size--)
        {
            *(char *)dest = *(char *)src;
            dest = (char *)dest - 1;
            src = (char *)src - 1;
        }
    }
    else
    {
        while (size--)
        {
            *(char *)dest = *(char *)src;
            dest = (char *)dest + 1;
            src = (char *)src + 1;
        }
    }

    return ret;
}

memset函数的使用

void * memset ( void * ptr, int value, size_t num );
  • memset是用来设置内存的,将内存中的值以字节为单位设置成想要的内容。
int main()
{
    char str[] = "hello world";
    memset(str, 'x', 6);
    printf(str);
    return 0;
}

这里memset第二个参数明明给的int类型为什么给’x’还能成功运行,简单的说字符类型其实也是整型,所以这里可以使用字符类型的’x’。但是最好不要给大于int类型的比如float,duoble和long long这种。

memcmp函数的使用

int memcmp ( const void * ptr1, const void * ptr2, size_t num );
  • 比较从ptr1和ptr2指针指向的位置开始,向后的num个字节
  • 返回值如下:
    在这里插入图片描述
int main()
{
    char buffer1[] = "DWgaOtP12df0";
    char buffer2[] = "DWGAOTP12DF0";

    int n;

    n = memcmp(buffer1, buffer2, sizeof(buffer1));

    if (n > 0)
        printf("'%s' is greater than '%s'.\n", buffer1, buffer2);
    else if (n < 0)
        printf("'%s' is less than '%s'.\n", buffer1, buffer2);
    else
        printf("'%s' is the same as '%s'.\n", buffer1, buffer2);

    return 0;
}

总结
内存函数相比对应的字符函数就像相当于标准版和plus版,字符函数只能接受字符类型的而内存函数却可以接受C语言的绝大部分的类型。这是因为内存函数通过 void* + size 参数,绕过了 \0 终结符的限制,实现了“纯字节级”操作,因此更通用、更底层。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐