memcpy() vs memmove()

207
我正在努力理解memcpy()memmove()之间的区别,并且我已经阅读了关于memcpy()不处理重叠源和目标的文本,而memmove()则会处理。
然而,当我在重叠的内存块上执行这两个函数时,它们都给出了相同的结果。例如,参考下面MSDN帮助页面上的memmove()示例:-
有没有更好的例子来理解memcpy的缺点以及memmove是如何解决这个问题的?
// crt_memcpy.c
// Illustrate overlapping copy: memmove always handles it correctly; memcpy may handle
// it correctly.

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

char str1[7] = "aabbcc";

int main( void )
{
    printf( "The string: %s\n", str1 );
    memcpy( str1 + 2, str1, 4 );
    printf( "New string: %s\n", str1 );

    strcpy_s( str1, sizeof(str1), "aabbcc" );   // reset string

    printf( "The string: %s\n", str1 );
    memmove( str1 + 2, str1, 4 );
    printf( "New string: %s\n", str1 );
}

输出:

memcpy():
The string: aabbcc
New string: aaaabb

memmove():
The string: aabbcc
New string: aaaabb

2
微软 CRT 已经有一个安全的 memcpy() 很长一段时间了。 - Hans Passant
45
我认为“安全”不是合适的词。一个安全的memcpyassert(断言)这些区域没有重叠,而不是故意掩盖代码中的错误。 - R.. GitHub STOP HELPING ICE
9
视情况而定,如果你是指“对开发者安全”还是“对最终用户安全”,我认为即使不符合标准,按照指示操作对最终用户来说是更安全的选择。 - kusma
2
微软的“安全”memcpy()是memmove()的后备 https://twitter.com/MalwareMinigun/status/737801492808142848 - vobject
5
关于“memcpy(...)可能出现什么问题”的一个带有图片的好例子可以在这里找到:memcpy vs memmove - deralbert
显示剩余2条评论
12个回答

157

我对你的示例没有出现奇怪的行为感到完全不惊讶。尝试将 str1 复制到 str1+2,然后观察会发生什么。(可能并没有实际影响,这取决于编译器/库)。

通常情况下,memcpy 的实现方式很简单(但是很快)。简单地说,它只是按顺序循环遍历数据,从一个位置复制到另一个位置。这可能会导致在读取源数据时被覆盖。

而 memmove 则做更多的工作以确保正确处理重叠部分。

编辑:

(不幸的是,我找不到合适的示例,但这些也可以)。比较一下这里展示的 memcpymemmove 的实现方式。memcpy 只是简单地循环遍历,而 memmove 则执行测试以确定循环方向,以避免破坏数据。这些实现方式相当简单。大多数高性能实现方式都更加复杂(涉及每次复制字大小块而不是每次复制字节)。


2
此外,在以下实现中,memmove在测试指针后的一个分支中调用了memcpy:http://www.student.cs.uwaterloo.ca/~cs350/common/os161-src-html/memmove_8c-source.html - Pascal Cuoq
听起来不错。看起来Visual Studio实现了一个“安全”的memcpy(gcc 4.1.1也是如此,我在RHEL 5上进行了测试)。从clc-wiki.net编写这些函数的版本可以清晰地了解情况。谢谢。 - user534785
3
memcpy不处理重叠问题,但是memmove可以。那么为什么不从库中删除memcpy呢? - Alcott
44
@Alcott:因为 memcpy 可能更快。 - Billy ONeal
1
Pascal Cuoq提供的固定/WebArchive链接如下:https://web.archive.org/web/20130722203254/http://www.student.cs.uwaterloo.ca/~cs350/common/os161-src-html/memmove_8c-source.html。 - JWCS

131
在`memcpy`中,内存不能重叠,否则会导致未定义的行为;而在`memmove`中,内存可以重叠。
char a[16];
char b[16];

memcpy(a,b,16);           // Valid.
memmove(a,b,16);          // Also valid, but slower than memcpy.
memcpy(&a[0], &a[1],10);  // Not valid since it overlaps.
memmove(&a[0], &a[1],10); // Valid. 

一些实现的memcpy可能仍然适用于重叠输入,但您不能指望这种行为。然而,memmove必须允许重叠输入。

5
谢谢你真的帮了我!+1赞同你提供的信息。 - Muthu Ganapathy Nathan

40

虽然 memcpy 不需要处理重叠区域,但这并不意味着它不正确地处理它们。在涉及重叠区域的情况下调用会产生未定义行为。未定义行为可能在一个平台上完全按照您的期望工作;但这并不意味着它是正确或有效的。


14
特别是,根据平台的不同,memcpy 可能会被实现为与 memmove 完全相同的方式。也就是说,编译器的作者没有费心编写一个独特的 memcpy 函数。 - Cam

20

memcpy和memmove都有类似的功能。

但是有一个区别:

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

char str1[7] = "abcdef";

int main()
{

   printf( "The string: %s\n", str1 );
   memcpy( (str1+6), str1, 10 );
   printf( "New string: %s\n", str1 );

   strcpy_s( str1, sizeof(str1), "aabbcc" );   // reset string


   printf("\nstr1: %s\n", str1);
   printf( "The string: %s\n", str1 );
   memmove( (str1+6), str1, 10 );
   printf( "New string: %s\n", str1 );

}

给出:

The string: abcdef
New string: abcdefabcdefabcd
The string: abcdef
New string: abcdefabcdef

3
依我之见,这个示例程序存在一些缺陷,因为它超出了 str1 缓冲区的访问范围(需要复制 10 个字节,而缓冲区只有 7 个字节的大小)。越界错误会导致未定义的行为。memcpy()/memmove() 调用所显示结果的差异是实现特定的。而且示例输出与上面的程序并不完全匹配... 此外,AFAIK strcpy_s() 不是标准 C 的一部分(仅适用于 MS,参见:https://dev59.com/4Jbfa4cB1Zd3GeqPqzsB#36724095) - 如果我有错,请纠正我。 - rel

14

你的演示未能揭示memcpy的缺点是因为"糟糕" 的编译器,在Debug版本中它帮了你一个忙。但是在发布版本中,你会得到相同的输出,但这是由于优化造成的。

    memcpy(str1 + 2, str1, 4);
00241013  mov         eax,dword ptr [str1 (243018h)]  // load 4 bytes from source string
    printf("New string: %s\n", str1);
00241018  push        offset str1 (243018h) 
0024101D  push        offset string "New string: %s\n" (242104h) 
00241022  mov         dword ptr [str1+2 (24301Ah)],eax  // put 4 bytes to destination
00241027  call        esi  

这里的寄存器%eax充当临时存储器,优雅地解决了重叠问题。

在复制6个字节时会出现缺陷,至少其中的一部分会出现问题。

char str1[9] = "aabbccdd";

int main( void )
{
    printf("The string: %s\n", str1);
    memcpy(str1 + 2, str1, 6);
    printf("New string: %s\n", str1);

    strcpy_s(str1, sizeof(str1), "aabbccdd");   // reset string

    printf("The string: %s\n", str1);
    memmove(str1 + 2, str1, 6);
    printf("New string: %s\n", str1);
}

输出:

The string: aabbccdd
New string: aaaabbbb
The string: aabbccdd
New string: aaaabbcc

看起来很奇怪,这也是由于优化引起的。

    memcpy(str1 + 2, str1, 6);
00341013  mov         eax,dword ptr [str1 (343018h)] 
00341018  mov         dword ptr [str1+2 (34301Ah)],eax // put 4 bytes to destination, earlier than the above example
0034101D  mov         cx,word ptr [str1+4 (34301Ch)]  // HA, new register! Holding a word, which is exactly the left 2 bytes (after 4 bytes loaded to %eax)
    printf("New string: %s\n", str1);
00341024  push        offset str1 (343018h) 
00341029  push        offset string "New string: %s\n" (342104h) 
0034102E  mov         word ptr [str1+6 (34301Eh)],cx  // Again, pulling the stored word back from the new register
00341035  call        esi  

这就是为什么我在尝试复制2个重叠的内存块时总是选择 memmove


5

C11标准草案

根据C11 N1570标准草案 第7.24.2.1节 "memcpy函数":

2 memcpy函数将从s2指向的对象复制n个字符到s1指向的对象中。如果在重叠对象之间进行复制,则该行为未定义。

第7.24.2.2节 "memmove函数":

2 memmove函数将从s2指向的对象复制n个字符到s1指向的对象中。复制过程就像先将s2指向的对象的n个字符复制到不与s1和s2指向的对象重叠的n个字符的临时数组中,然后再将临时数组中的n个字符复制到s1指向的对象中。

因此,对于任何在memcpy中发生的重叠,都会导致未定义的行为,可能会出现糟糕、无效甚至良好的结果(虽然良好的结果很少)。

然而,memmove很明显说明了所有操作都是如同使用一个中间缓冲区,因此重叠是可以的。

相比之下,C++的std::copy更加宽容,并允许重叠: Does std::copy handle overlapping ranges?


1
memmove使用额外的大小为n的临时数组,那么它是否会使用额外的内存呢?但是如果我们没有给它访问任何内存的权限,它怎么可能使用额外的内存呢?(它实际上使用了两倍的内存)。 - clamentjohn
@clmno 它会像其他函数一样在堆栈上分配或使用 malloc 函数进行内存分配,这是我所期望的 :-) - Ciro Santilli OurBigBook.com
1
我在这里提出了一个问题(https://dev59.com/ozEBtIcB2Jgan1znPS_9),并得到了一个很好的答案。谢谢你。看到了你在Hacker News上发布的一篇帖子(https://news.ycombinator.com/item?id=19428700),那个x86的 :) - clamentjohn

4
memcpymemmove的区别在于:
  1. memmove中,指定大小的源内存被复制到缓冲区,然后移动到目标位置。因此,如果内存重叠,不会产生副作用。

  2. 对于memcpy(),不会为源内存使用额外的缓冲区。直接在内存上进行复制,因此当存在内存重叠时,会得到意外的结果。

可以通过以下代码观察到这些差异:
//include string.h, stdio.h, stdlib.h
int main(){
  char a[]="hare rama hare rama";

  char b[]="hare rama hare rama";

  memmove(a+5,a,20);
  puts(a);

  memcpy(b+5,b,20);
  puts(b);
}

输出结果为:

hare hare rama hare rama
hare hare hare hare hare hare rama hare rama

7
memmove 没有要求将数据实际复制到单独的缓冲区中。 - jjwchoy
这个例子并不能帮助理解概念...因为大多数编译器会给出与mem move输出相同的结果。 - Jasdeep Singh Arora
1
@jjwchoy 从概念上讲是这样的。缓冲区通常会被优化掉。 - M.M
在Linux上,结果相同。 - CodyChan

3

正如其他答案所指出的那样,memmovememcpy 更加复杂,因为它考虑了内存重叠的情况。 memmove 的结果被定义为如果将 src 复制到缓冲区,然后将缓冲区复制到 dst。这并不意味着实际的实现使用任何缓冲区,而是可能使用了一些指针算术。


1
编译器可以优化memcpy,例如:
int x;
memcpy(&x, some_pointer, sizeof(int));

这个memcpy可以优化为: x = *(int*)some_pointer;

3
只有在支持非对齐 int 访问的体系结构上才可以进行这样的优化。在某些架构上(例如Cortex-M0),试图从不是4的倍数的地址获取32位 int 将导致崩溃(但 memcpy 可以正常工作)。如果将使用允许非对齐访问的 CPU,或者使用具有指令的编译器将整数从分别获取的字节中组装出来,则可以执行类似于 #define UNALIGNED __unaligned 然后 x = *(int UNALIGNED*)some_pointer; 的操作。 - supercat
2
有些处理器不允许非对齐的整数访问崩溃。 char x = "12345"; int *i; i = *(int *)(x + 1); 但是有些处理器可以,因为它们在出错时修复了拷贝。我曾经在一个类似的系统上工作过,花了一些时间才明白为什么性能如此差劲。 - user3431262
*(int *)some_pointer 是一个严格别名违规,但你可能的意思是编译器会输出汇编代码来复制一个整数。 - M.M

1
http://clc-wiki.net/wiki/memcpy中给出的memcpy代码似乎让我有点困惑,因为当我使用下面的示例实现它时,它没有给出相同的输出。
#include <memory.h>
#include <string.h>
#include <stdio.h>

char str1[11] = "abcdefghij";

void *memcpyCustom(void *dest, const void *src, size_t n)
{
    char *dp = (char *)dest;
    const char *sp = (char *)src;
    while (n--)
        *dp++ = *sp++;
    return dest;
}

void *memmoveCustom(void *dest, const void *src, size_t n)
{
    unsigned char *pd = (unsigned char *)dest;
    const unsigned char *ps = (unsigned char *)src;
    if ( ps < pd )
        for (pd += n, ps += n; n--;)
            *--pd = *--ps;
    else
        while(n--)
            *pd++ = *ps++;
    return dest;
}

int main( void )
{
    printf( "The string: %s\n", str1 );
    memcpy( str1 + 1, str1, 9 );
    printf( "Actual memcpy output: %s\n", str1 );

    strcpy_s( str1, sizeof(str1), "abcdefghij" );   // reset string

    memcpyCustom( str1 + 1, str1, 9 );
    printf( "Implemented memcpy output: %s\n", str1 );

    strcpy_s( str1, sizeof(str1), "abcdefghij" );   // reset string

    memmoveCustom( str1 + 1, str1, 9 );
    printf( "Implemented memmove output: %s\n", str1 );
    getchar();
}

输出:
The string: abcdefghij
Actual memcpy output: aabcdefghi
Implemented memcpy output: aaaaaaaaaa
Implemented memmove output: aabcdefghi

但是现在你可以理解为什么memmove会处理重叠问题了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接