memcpy()与memmove()

我正在尝试理解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
198133 次浏览

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

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

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

MemMove做了更多的工作,以确保它正确处理重叠。

编辑:

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

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

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

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

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

该memcpy可以被优化为:x = *(int*)some_pointer;

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

你的演示没有因为“坏”的编译器而暴露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

链接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

但现在您可以理解为什么记忆移动将处理重叠问题。

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

当试图将数据从位置str1复制到str1+2时,memcpy的输出是“aaaaaa ”。问题是怎么做? memcpy()将从左到右每次复制一个字节。如程序“aabbcc ”所示,然后 所有复制将按以下方式进行,

  1. aabbcc -> aaabcc

  2. aaabcc -> aaaacc

  3. aaaacc -> aaaaac

  4. aaaaac -> aaaaaa

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

  1. aabbcc(actual) -> aabbcc(temp)

  2. aabbcc(temp) -> aaabcc(act)

  3. aabbcc(temp) -> aaaacc(act)

  4. aabbcc(temp) -> aaaabc(act)

  5. aabbcc(temp) -> aaaabb(act)

输出为

memcpyaaaaaa

memmoveaaaabb

如在其他回答中已经指出的,memmovememcpy更复杂,使得它考虑存储器重叠。MEMMOVE的结果被定义为src被复制到缓冲器中,然后缓冲器被复制到dst中。这并不意味着实际实现使用任何缓冲区,但可能会执行一些指针运算。

C11标准草案

C11 N1570标准草案显示:

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更加宽容,并且允许重叠:STD:COPY是否处理重叠范围?