【问题标题】:memcpy() vs memmove()memcpy() 与 memmove()
【发布时间】:2011-05-23 21:19:09
【问题描述】:

我试图了解memcpy()memmove() 之间的区别,并且我已经阅读了memcpy() 不处理重叠源和目标而memmove() 处理的文本。

但是,当我在重叠的内存块上执行这两个函数时,它们都会给出相同的结果。例如,以 memmove() 帮助页面上的以下 MSDN 示例为例:-

有没有更好的例子来理解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 );
}

输出:

The string: aabbcc
New string: aaaabb
The string: aabbcc
New string: aaaabb

【问题讨论】:

  • 微软 CRT 有一个安全的 memcpy() 已经有一段时间了。
  • 我不认为“安全”是正确的词。安全的memcpyassert 区域不重叠,而不是故意掩盖代码中的错误。
  • 取决于您的意思是“对开发人员安全”还是“对最终用户安全”。我会争辩说,即使不符合标准,按指示行事也是最终用户更安全的选择。
  • 微软的“安全”memcpy() 是 memmove() 的后备twitter.com/MalwareMinigun/status/737801492808142848
  • 可以在此处找到有关“memcpy(...) 可能出错的问题”主题的图片示例:memcpy vs memmove

标签: c memcpy memmove


【解决方案1】:

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

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

Memmove 做了更多工作来确保正确处理重叠。

编辑:

(不幸的是,我找不到像样的例子,但这些都可以)。对比此处显示的memcpymemmove 实现。 memcpy 只是循环,而 memmove 执行测试以确定循环的方向以避免损坏数据。这些实现相当简单。大多数高性能实现更复杂(涉及一次复制字大小的块而不是字节)。

【讨论】:

  • +1 此外,在以下实现中,memmove 在测试指针后在一个分支中调用memcpystudent.cs.uwaterloo.ca/~cs350/common/os161-src-html/…
  • 听起来不错。似乎 Visual Studio 实现了一个“安全”的 memcpy(连同 gcc 4.1.1,我也在 RHEL 5 上进行了测试)。从 clc-wiki.net 编写这些函数的版本可以清楚地看到。谢谢。
  • memcpy 不处理重叠问题,但 memmove 可以。那为什么不从 lib 中删除 memcpy 呢?
  • @Alcott:因为memcpy 可以更快。
  • 上面 Pascal Cuoq 的固定/网络存档链接:web.archive.org/web/20130722203254/http://…
【解决方案2】:

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 必须允许重叠。

【讨论】:

  • 这真的帮了我大忙!为您的信息 +1
【解决方案3】:

仅仅因为memcpy 不必处理重叠区域,并不意味着它不能正确处理它们。具有重叠区域的调用会产生未定义的行为。未定义的行为可以在一个平台上完全按照您的预期工作;这并不意味着它是正确或有效的。

【讨论】:

  • 特别是,根据平台的不同,memcpy 的实现方式可能与memmove 完全相同。也就是说,编写编译器的人不会费心编写唯一的memcpy 函数。
【解决方案4】:

memcpy 和 memove 都做类似的事情。

但要找出一个区别:

#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

【讨论】:

  • 恕我直言,这个示例程序有一些缺陷,因为 str1 缓冲区被越界访问(要复制 10 个字节,缓冲区大小为 7 个字节)。越界错误会导致未定义的行为。 memcpy()/memmove() 调用的显示结果的差异是特定于实现的。并且示例输出与上面的程序不完全匹配......另外, strcpy_s() 不是标准 C AFAIK 的一部分(特定于 MS,另请参见:stackoverflow.com/questions/36723946/…) - 如果我错了,请纠正我。跨度>
【解决方案5】:

您的演示没有因为“坏”编译器而暴露 memcpy 缺点,它在调试版本中对您有利。但是,发布版本会为您提供相同的输出,但由于优化。

    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

【讨论】:

    【解决方案6】:

    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
    

    【讨论】:

    • -1 - memmove 不需要将数据实际复制到单独的缓冲区中
    • 这个例子对理解这个概念没有帮助......因为大多数编译器都会给出与 mem move 输出相同的输出
    • @jjwchoy 从概念上讲确实如此。缓冲区通常会被优化
    • 同样的结果,在 Linux 上。
    【解决方案7】:

    C11 标准草案

    C11 N1570 standard draft 说:

    7.24.2.1 “memcpy 函数”:

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

    7.24.2.2 “memmove 函数”:

    2 memmove 函数将 s2 指向的对象中的 n 个字符复制到 s1 指向的对象。复制就像对象中的 n 个字符一样发生 由 s2 指向的对象首先被复制到一个临时数组中,该数组由 n 个字符组成 重叠 s1 和 s2 指向的对象,然后从 临时数组被复制到 s1 指向的对象中

    因此,memcpy 上的任何重叠都会导致未定义的行为,并且任何事情都可能发生:坏的、没有的,甚至是好的。好的很少见:-)

    memmove 但是清楚地表明一切都像使用中间缓冲区一样发生,因此显然重叠是可以的。

    C++ std::copy 然而更宽容,允许重叠:Does std::copy handle overlapping ranges?

    【讨论】:

    • memmove 使用了一个额外的临时数组 n,那么它是否使用了额外的内存?但是,如果我们没有给它访问任何内存的权限,它怎么可能呢。 (它使用了 2 倍的内存)。
    • @clmno 它像我期望的任何其他函数一样在堆栈或 malloc 上分配 :-)
    • 我问了一个问题here,也得到了很好的答案。谢谢你。看到你的黑客新闻post 病毒式传播(x86 的):)
    【解决方案8】:

    正如在其他答案中已经指出的那样,memmovememcpy 更复杂,因此它考虑了内存重叠。 memmove 的结果被定义为好像 src 被复制到缓冲区中,然后缓冲区被复制到 dst 中。这并不意味着实际的实现使用任何缓冲区,但可能会进行一些指针运算。

    【讨论】:

      【解决方案9】:

      编译器可以优化memcpy,例如:

      int x;
      memcpy(&x, some_pointer, sizeof(int));
      

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

      【讨论】:

      • 这种优化只允许在允许未对齐的int 访问的架构上进行。在某些架构(例如 Cortex-M0)上,尝试从不是四的倍数的地址获取 32 位 int 会导致崩溃(但 memcpy 会起作用)。如果一个人要么使用允许未对齐访问的 CPU,要么使用带有关键字的编译器,该关键字指示编译器在必要时从单独获取的字节中组装整数,则可以执行类似#define UNALIGNED __unaligned 然后 `x=*( int UNALIGNED*)some_pointer;
      • 一些处理器不允许未对齐的 int 访问崩溃 char x = "12345"; int *i; i = *(int *)(x + 1); 但有些处理器允许,因为它们在故障期间修复了副本。我在这样的系统上工作过,花了一些时间来理解为什么性能如此差。
      • *(int *)some_pointer 是严格的别名违规,但您可能的意思是编译器会输出复制 int 的程序集
      【解决方案10】:

      链接 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 会处理重叠问题。

      【讨论】:

        【解决方案11】:

        我尝试使用 Eclipse 运行相同的程序,它显示了 memcpymemmove 之间的明显区别。 memcpy() 不关心内存位置重叠导致数据损坏,而memmove() 会先将数据复制到临时变量,然后再复制到实际内存位置。

        尝试将数据从位置str1 复制到str1+2 时,memcpy 的输出为“aaaaaa”。问题是如何? memcpy() 将从左到右一次复制一个字节。如您的程序“aabbcc”所示,然后 所有的复制都将如下进行,

        1. aabbcc -&gt; aaabcc

        2. aaabcc -&gt; aaaacc

        3. aaaacc -&gt; aaaaac

        4. aaaaac -&gt; aaaaaa

        memmove() 会先将数据复制到临时变量,然后再复制到实际内存位置。

        1. aabbcc(actual) -&gt; aabbcc(temp)

        2. aabbcc(temp) -&gt; aaabcc(act)

        3. aabbcc(temp) -&gt; aaaacc(act)

        4. aabbcc(temp) -&gt; aaaabc(act)

        5. aabbcc(temp) -&gt; aaaabb(act)

        输出是

        memcpyaaaaaa

        memmove : aaaabb

        【讨论】:

        • 欢迎来到 Stack Overflow。请尽快阅读About 页面。有各种各样的问题需要解决。首先,您在 18 个月左右前添加了一个包含多个答案的问题的答案。为了保证添加,您需要提供令人吃惊的新信息。其次,您指定了 Eclipse,但 Eclipse 是一个使用 C 编译器的 IDE,但您没有标识运行代码的平台或 Eclipse 使用的 C 编译器。我很想知道您如何确定 memmove() 复制到中间位置。它应该在必要时反向复制。
        • 谢谢。关于编译器,所以我在 linux 上使用 gcc 编译器。在 linux 中有一个 memove 的手册页,其中明确指定 memove 将复制临时变量中的数据以避免数据重叠。这是该手册页的链接linux.die.net/man/3/memmove
        • 它实际上是在说“好像”,但这并不意味着它就是实际发生的事情。诚然,它可以真的这样做(虽然会有关于它从哪里获得备用内存的问题),但如果它实际上是这样做的,我会感到非常惊讶。如果源地址大于目标地址,从头到尾复制就足够了(forwards copy);如果源地址小于目标地址,则从末尾复制到开头(向后复制)就足够了。不需要或使用辅助存储器。
        • 尝试用代码中的实际数据来解释你的答案,这样会更有帮助。
        猜你喜欢
        • 2015-01-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-06-09
        • 2011-04-17
        • 2015-04-21
        • 2011-07-21
        • 2013-08-13
        相关资源
        最近更新 更多