MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C 语言memcpy()和memmove() 深入解析

2022-06-064.3k 阅读

1. 函数简介

在 C 语言中,memcpy()memmove() 都是用于内存复制的函数,它们在标准库 <string.h> 中声明。这两个函数的原型如下:

void *memcpy(void *dest, const void *src, size_t n);
void *memmove(void *dest, const void *src, size_t n);

它们的功能都是将 src 所指向的内存区域的前 n 个字节复制到 dest 所指向的内存区域。返回值都是 dest 指针,这使得函数调用可以被嵌套使用。

2. 内存重叠问题

这两个函数最大的区别在于对内存重叠情况的处理。所谓内存重叠,是指源内存区域和目标内存区域有部分或全部重叠的情况。

2.1 不重叠情况

当源内存区域和目标内存区域不重叠时,memcpy()memmove() 的行为是完全一样的。例如:

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

int main() {
    char source[10] = "hello";
    char destination[10];

    // 使用 memcpy 进行复制
    memcpy(destination, source, strlen(source) + 1);
    printf("使用 memcpy 复制后: %s\n", destination);

    // 使用 memmove 进行复制
    memmove(destination, source, strlen(source) + 1);
    printf("使用 memmove 复制后: %s\n", destination);

    return 0;
}

在上述代码中,sourcedestination 是两个独立的数组,不存在内存重叠。无论是 memcpy() 还是 memmove(),都能正确地将 source 中的字符串复制到 destination 中。

2.2 重叠情况

当内存重叠时,memcpy()memmove() 的行为就有所不同了。memcpy() 并不保证在内存重叠的情况下能正确复制,而 memmove() 则可以保证在任何情况下(包括内存重叠)都能正确复制。

内存重叠可以分为两种情况:正向重叠和反向重叠。

正向重叠:目标内存区域的起始地址大于源内存区域的起始地址,且目标内存区域的起始地址小于源内存区域的起始地址加上复制的字节数。例如:

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

int main() {
    char str[] = "abcdef";
    // 正向重叠
    memcpy(str + 2, str, 4);
    printf("使用 memcpy 正向重叠后: %s\n", str);

    char str2[] = "abcdef";
    memmove(str2 + 2, str2, 4);
    printf("使用 memmove 正向重叠后: %s\n", str2);

    return 0;
}

在上述代码中,memcpy(str + 2, str, 4) 是正向重叠的情况。memcpy() 不保证正确复制,实际运行结果可能因编译器而异。而 memmove() 则能正确处理这种情况,将前 4 个字符复制到从第 3 个字符开始的位置。

反向重叠:目标内存区域的起始地址小于源内存区域的起始地址,且目标内存区域的起始地址加上复制的字节数大于源内存区域的起始地址。例如:

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

int main() {
    char str[] = "abcdef";
    // 反向重叠
    memcpy(str, str + 2, 4);
    printf("使用 memcpy 反向重叠后: %s\n", str);

    char str2[] = "abcdef";
    memmove(str2, str2 + 2, 4);
    printf("使用 memmove 反向重叠后: %s\n", str2);

    return 0;
}

同样,memcpy() 在反向重叠情况下不保证正确复制,而 memmove() 可以正确处理。

3. 实现原理

为了深入理解这两个函数的差异,我们来探讨一下它们可能的实现原理。

3.1 memcpy 的实现原理

memcpy() 通常是通过逐字节复制的方式实现的,效率相对较高。在不重叠的情况下,它可以直接从源地址开始,依次将字节复制到目标地址。以下是一个简单的 memcpy() 模拟实现:

void *my_memcpy(void *dest, const void *src, size_t n) {
    char *d = (char *)dest;
    const char *s = (const char *)src;
    while (n--) {
        *d++ = *s++;
    }
    return dest;
}

这种实现方式在不重叠的情况下工作得很好,但在内存重叠时可能会出现问题。因为它是从源地址的起始位置开始复制,当目标地址在源地址内部且靠前时,可能会覆盖尚未复制的源数据。

3.2 memmove 的实现原理

memmove() 为了处理内存重叠的情况,需要更复杂的逻辑。它通常会先判断源地址和目标地址的位置关系,然后根据不同情况选择正向复制或反向复制。以下是一个简单的 memmove() 模拟实现:

void *my_memmove(void *dest, const void *src, size_t n) {
    char *d = (char *)dest;
    const char *s = (const char *)src;

    if (s < d && d < s + n) {
        // 正向重叠,从后往前复制
        d += n;
        s += n;
        while (n--) {
            *--d = *--s;
        }
    } else {
        // 不重叠或反向重叠,从前往后复制
        while (n--) {
            *d++ = *s++;
        }
    }

    return dest;
}

在这个实现中,首先判断是否存在正向重叠的情况。如果存在,就从源内存区域的末尾开始,反向复制到目标内存区域,这样可以避免覆盖尚未复制的源数据。如果不存在正向重叠(即不重叠或反向重叠),则从前往后复制,与 memcpy() 的复制方式相同。

4. 性能比较

在不重叠的情况下,memcpy()memmove() 的性能基本相同,因为它们都可以采用逐字节复制的高效方式。然而,由于 memmove() 需要额外的逻辑来处理内存重叠,在不重叠的情况下,memcpy() 可能会有轻微的性能优势,因为它不需要进行额外的地址比较。

在内存重叠的情况下,memmove() 虽然能够正确复制,但由于其复杂的逻辑,性能会有所下降。特别是在处理大数据量的重叠复制时,memmove() 的性能劣势会更加明显。

为了验证性能差异,我们可以编写一个简单的性能测试代码:

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

#define SIZE 1000000

void test_memcpy() {
    char source[SIZE];
    char destination[SIZE];
    for (int i = 0; i < SIZE; i++) {
        source[i] = (char)i;
    }

    clock_t start = clock();
    for (int i = 0; i < 1000; i++) {
        memcpy(destination, source, SIZE);
    }
    clock_t end = clock();

    double time_spent = (double)(end - start) / CLOCKS_PER_SEC;
    printf("memcpy 运行时间: %f 秒\n", time_spent);
}

void test_memmove() {
    char source[SIZE];
    char destination[SIZE];
    for (int i = 0; i < SIZE; i++) {
        source[i] = (char)i;
    }

    clock_t start = clock();
    for (int i = 0; i < 1000; i++) {
        memmove(destination, source, SIZE);
    }
    clock_t end = clock();

    double time_spent = (double)(end - start) / CLOCKS_PER_SEC;
    printf("memmove 运行时间: %f 秒\n", time_spent);
}

int main() {
    test_memcpy();
    test_memmove();

    return 0;
}

运行上述代码,可以观察到在大数据量复制且不重叠的情况下,memcpy() 的运行时间略短于 memmove()

5. 使用场景

根据 memcpy()memmove() 的特点,它们适用于不同的场景。

5.1 memcpy 的使用场景

当可以确保源内存区域和目标内存区域不重叠时,应优先使用 memcpy()。例如,在复制独立的数组、结构体等数据结构时,memcpy() 是一个高效的选择。它的代码实现相对简单,性能也较好。

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

typedef struct {
    int id;
    char name[20];
} Person;

int main() {
    Person p1 = {1, "Alice"};
    Person p2;

    memcpy(&p2, &p1, sizeof(Person));
    printf("p2 的 id: %d, 名字: %s\n", p2.id, p2.name);

    return 0;
}

在上述代码中,p1p2 是两个独立的结构体变量,不存在内存重叠,使用 memcpy() 可以高效地复制结构体内容。

5.2 memmove 的使用场景

当无法确定源内存区域和目标内存区域是否重叠时,应使用 memmove()。例如,在处理字符串的插入、删除等操作时,可能会出现内存重叠的情况,此时 memmove() 是唯一正确的选择。

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

void insert_char(char *str, char c, int pos) {
    int len = strlen(str);
    memmove(str + pos + 1, str + pos, len - pos + 1);
    str[pos] = c;
}

int main() {
    char str[] = "hello";
    insert_char(str, 'x', 2);
    printf("插入字符后: %s\n", str);

    return 0;
}

在上述代码中,memmove() 用于将字符串中指定位置之后的字符向后移动一位,以插入新的字符。由于移动过程可能会出现内存重叠,使用 memmove() 可以保证操作的正确性。

6. 注意事项

在使用 memcpy()memmove() 时,有一些注意事项需要牢记。

6.1 目标内存区域的大小

无论是 memcpy() 还是 memmove(),都不会检查目标内存区域是否足够大。如果目标内存区域不足以容纳复制的数据,将会导致缓冲区溢出,这是一种严重的安全漏洞。

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

int main() {
    char source[] = "very long string";
    char destination[5];

    // 这将导致缓冲区溢出
    memcpy(destination, source, strlen(source) + 1);
    printf("复制后: %s\n", destination);

    return 0;
}

在上述代码中,destination 数组的大小为 5,而 source 字符串的长度超过了 5,使用 memcpy() 进行复制会导致缓冲区溢出。

6.2 类型转换

memcpy()memmove() 的参数类型都是 void *,这意味着可以传递任何类型的指针。然而,在使用时需要确保源和目标指针的类型兼容性。例如,不要将指向 int 的指针直接传递给 memcpy() 并期望将其复制为 char 数组,除非明确知道数据的内存布局。

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

int main() {
    int num = 12345;
    char buffer[4];

    // 这样的复制可能会导致未定义行为
    memcpy(buffer, &num, sizeof(int));
    for (int i = 0; i < sizeof(int); i++) {
        printf("%02hhx ", buffer[i]);
    }
    printf("\n");

    return 0;
}

在上述代码中,将 int 类型的数据复制到 char 数组中,由于不同系统中 int 的字节序和内存布局可能不同,这样的操作可能会导致未定义行为。

6.3 内存对齐

在一些系统中,内存对齐对于性能和正确性非常重要。虽然 memcpy()memmove() 本身并不处理内存对齐问题,但在复制复杂数据结构(如结构体)时,需要注意内存对齐。如果结构体成员的对齐方式与目标内存区域的对齐方式不匹配,可能会导致性能下降或运行时错误。

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

#pragma pack(push, 1)
typedef struct {
    char c;
    int i;
} Struct1;
#pragma pack(pop)

typedef struct {
    char c;
    int i;
} Struct2;

int main() {
    Struct1 s1 = {'a', 123};
    Struct2 s2;

    // 由于 Struct1 和 Struct2 的内存布局不同,复制可能会出错
    memcpy(&s2, &s1, sizeof(Struct1));
    printf("s2.c: %c, s2.i: %d\n", s2.c, s2.i);

    return 0;
}

在上述代码中,Struct1 使用了 #pragma pack(1) 来指定 1 字节对齐,而 Struct2 使用默认对齐方式。直接使用 memcpy() 复制这两个结构体可能会导致数据错误,因为它们的内存布局不同。

7. 总结

memcpy()memmove() 是 C 语言中用于内存复制的重要函数。memcpy() 适用于源和目标内存区域不重叠的情况,性能较高;memmove() 则可以处理内存重叠的情况,确保复制的正确性。在使用这两个函数时,需要注意目标内存区域的大小、类型转换以及内存对齐等问题,以避免出现缓冲区溢出、未定义行为等错误。根据具体的应用场景,合理选择 memcpy()memmove(),可以提高程序的性能和可靠性。