memmove和memcpy有什么区别?

138

memmovememcpy 有什么区别?你通常使用哪一个以及如何使用?


请注意可能出现的问题:https://lwn.net/Articles/414467/ - Zan Lynx
9个回答

176

memcpy函数不允许目标地址与源地址有任何重叠部分;而memmove则可以。这意味着memmove函数可能比memcpy函数略微慢一些,因为它不能做出相同的假设。

例如,memcpy在拷贝时可能总是从低地址到高地址进行,如果目标地址在源地址后面并且与其有重叠部分,则意味着一些地址将在被拷贝之前被覆盖。在这种情况下,memmove将检测到这一点,并按相反方向——从高到低——进行拷贝。然而,检查这一点并切换到另一种(可能不那么高效)的算法需要时间。


1
使用memcpy时,我如何确保src和dest地址不重叠?我需要亲自确保src和dest不重叠吗? - Alcott
8
如果你不确定内存重叠的情况,请使用memmove而不是memcpy。当内存没有重叠时,memmove和memcpy是等效的(尽管memcpy可能会稍微快一点)。 - bdonlan
如果您正在使用长数组并希望保护复制过程,可以使用“restrict”关键字。例如,如果您的方法将输入和输出数组作为参数,并且必须验证用户未将相同地址作为输入和输出传递,则必须使用该关键字。在此处阅读更多信息:https://dev59.com/S3RA5IYBdhLWcg3w8SiJ - DanielHsH
11
“restrict”是你向编译器作出的承诺,而不是由编译器“强制执行”的。如果你在参数上使用“restrict”,并且实际上存在重叠(或更普遍的情况是从多个地方派生指向受限制数据的指针),程序的行为将变得未定义,会出现奇怪的错误,并且编译器通常不会发出警告。 - bdonlan
1
@Mecki,那么我看不出你的评论和bdonlan之间有什么区别。似乎你们在说同样的事情。限制是对编译器做出的承诺。但编译器会让你违反这个承诺,可能导致未定义的结果。将“承诺”替换为“要求”,你就在说同样的事情了。 - iheanyi
显示剩余6条评论

38

memmove 可以处理重叠的内存,memcpy 不能。

请考虑

char[] str = "foo-bar";
memcpy(&str[3], &str[4], 4); // might blow up

显然,源和目标现在重叠,我们正在用“bar”覆盖“-bar”。如果源和目标重叠,则使用memcpy是未定义的行为,因此在这种情况下,我们需要使用memmove

memmove(&str[3], &str[4], 4); // fine

5
因为它可能使用需要内存不重叠的低级汇编语言实现。如果有重叠,例如可以生成一个信号或硬件异常给处理器来终止应用程序。文档指定不处理此条件,但标准没有规定这些条件被违反时会发生什么(这被称为未定义行为)。未定义行为可以做任何事情。 - Martin York
1
使用gcc 4.8.2,即使是memcpy也接受重叠的源和目标指针,并且正常工作。 - Jagdish
7
@jagsgediya 当然可能会这样,但由于memcpy文档中并不支持此操作,您不应该依赖于这种实现特定的行为,这就是为什么有memmove()存在的原因。在另一个版本的gcc中可能会有所不同。如果gcc内联了memcpy而不是调用glibc中的memcpy(),则可能会有所不同,在较旧或较新版本的glibc上也可能会不同。 - nos
从实践来看,memcpy和memmove似乎做了相同的事情。这是一种深层次的未定义行为。 - Life

24

假设您需要同时实现这两个功能,具体实现可能如下:

void memmove ( void * dst, const void * src, size_t count ) {
    if ((uintptr_t)src < (uintptr_t)dst) {
        // Copy from back to front

    } else if ((uintptr_t)dst < (uintptr_t)src) {
        // Copy from front to back
    }
}

void memcpy ( void * dst, const void * src, size_t count ) {
    if ((uintptr_t)src != (uintptr_t)dst) {
        // Copy in any way you want
    }
}

这里有一个很好的解释,可以阐述两者之间的区别。memmove始终以一种安全的方式进行复制,即使src和dst重叠,也是安全的;而memcpy则不关心,正如文档所说,使用memcpy时,这两个内存区域不能重叠。
例如,如果memcpy将“前向后”复制,并且内存块对齐如下:
[---- src ----]
            [---- dst ---]

src的第一个字节复制到dst中已经在复制之前破坏了src的最后几个字节的内容。只有采用“从后往前”复制才能得到正确的结果。

现在交换srcdst

[---- dst ----]
            [---- src ---]

在这种情况下,只有将内容从前往后复制才是安全的。如果从后往前复制,则会在复制第一个字节时破坏已经靠近其前面的src

您可能已经注意到,上面的memmove实现甚至没有测试它们是否重叠,它只检查它们的相对位置,但仅凭这一点就足以使复制变得安全。由于memcpy通常在任何系统上都使用最快的方式来复制内存,因此memmove通常被实现为:

void memmove ( void * dst, const void * src, size_t count ) {
    if ((uintptr_t)src < (uintptr_t)dst
        && (uintptr_t)src + count > (uintptr_t)dst
    ) {
        // Copy from back to front

    } else if ((uintptr_t)dst < (uintptr_t)src
        && (uintptr_t)dst + count > (uintptr_t)src
    ) {
        // Copy from front to back

    } else {
        // They don't overlap for sure
        memcpy(dst, src, count);
    }
}

有时,如果memcpy总是按“从前到后”或“从后到前”的顺序复制,memmove在重叠的情况下也可能使用memcpy,但是memcpy甚至可以根据数据的对齐方式和/或要复制的数据量以不同的方式进行复制,因此即使您测试了memcpy在系统上的复制方式,也不能保证测试结果始终正确。
那么,在决定调用哪个函数时,这对您意味着什么呢?
  1. 除非您确定srcdst不重叠,请调用memmove,因为它总是会产生正确的结果,并且通常是您所需的复制情况下速度最快的。

  2. 如果您确定srcdst不重叠,请调用memcpy,因为在这种情况下,无论您调用哪一个都会产生正确的结果,但是memmove永远不会比memcpy更快,如果您不幸的话,它甚至可能更慢,所以只有调用memcpy才能赢得胜利。


4
+1是因为你的“ASCII图示”有助于理解为什么不能有重叠而不损坏数据。 - Scylardor
请注意,比较指向不同对象/数组的指针的关系的结果是未指定的。因此,没有安全的方法来检测两个对象是否重叠。实际上,如果dst < src,则memmove的典型实现将数据从前向后复制,否则将数据从后向前复制。这是安全的,因为如果存在重叠,则结果是定义(和正确的),因此行为是安全的;否则,如果不存在重叠,则结果是未指定的,但在两个方向上进行复制是安全的。 - VainMan

23

来自memcpy手册页。

memcpy()函数将n个字节从内存区域src复制到内存区域dest。这些内存区域不应该重叠。如果内存区域重叠,请使用memmove(3)。


12
memmove()memcpy()的主要区别在于,memmove()使用一个缓冲区 - 临时内存 - 因此不存在重叠的风险。另一方面,memcpy()直接从指向的位置复制数据到目标指向的位置。(引自http://www.cplusplus.com/reference/cstring/memcpy/

考虑以下示例:

  1. #include <stdio.h>
    #include <string.h>
    
    int main (void)
    {
        char string [] = "stackoverflow";
        char *first, *second;
        first = string;
        second = string;
    
        puts(string);
        memcpy(first+5, first, 5);
        puts(first);
        memmove(second+5, second, 5);
        puts(second);
        return 0;
    }
    

    正如您所预期的那样,这将打印出:

    stackoverflow
    stackstacklow
    stackstacklow
    
    但是在这个例子中,结果将不会相同:
  2. #include <stdio.h>
    #include <string.h>
    
    int main (void)
    {
        char string [] = "stackoverflow";
        char *third, *fourth;
        third = string;
        fourth = string;
    
        puts(string);
        memcpy(third+5, third, 7);
        puts(third);
        memmove(fourth+5, fourth, 7);
        puts(fourth);
        return 0;
    }
    

    输出:

    stackoverflow
    stackstackovw
    stackstackstw
    
    这是因为 "memcpy()" 执行以下操作:

1.  stackoverflow
2.  stacksverflow
3.  stacksterflow
4.  stackstarflow
5.  stackstacflow
6.  stackstacklow
7.  stackstacksow
8.  stackstackstw

3
但是,你提到的输出似乎是反过来的!! - kumar
1
当我运行相同的程序时,我得到以下结果:stackoverflow stackstackstw stackstackstw // 这意味着memcpy和memmove之间的输出没有区别。 - kumar
8
“memmove()”函数中使用的是缓冲区,即临时内存,这句话不是真实的。实际上,“memmove()”函数使用“好像是”缓冲区的方式进行操作,但并不一定非得这样处理。这点很重要,因为大多数“memmove()”实现只需进行异或交换即可。 - dhein
5
我认为实现memmove()并不需要使用缓冲区。只要每次读取在写入同一地址之前完成,就可以完全在原地移动。 - Toby Speight

10

一个函数(memmove)可以处理重叠的目标区域,而另一个函数(memcpy)则不能。


7

根据ISO/IEC:9899标准,这已经有很好的描述了。

7.21.2.1 memcpy函数

[...]

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

7.21.2.2 memmove函数

[...]

2 memmove函数将从s2指向的对象复制n个字符到s1指向的对象中。复制会发生好像来自s2指向的n个字符首先被复制到一个不与s1和s2指向的对象重叠的临时数组中一样,然后从临时数组中复制n个字符到s1指向的对象中。

根据问题通常使用哪一个函数,取决于我需要什么功能。

简而言之,memcpy() 不允许 s1s2 重叠,而 memmove() 则允许。


1

实现 mempcpy(void *dest, const void *src, size_t n)(忽略返回值)有两种明显的方法:

  1. for (char *p=src, *q=dest;  n-->0;  ++p, ++q)
        *q=*p;
    
  2. char *p=src, *q=dest;
    while (n-->0)
        q[n]=p[n];
    
在第一个实现中,复制过程从低地址到高地址进行;在第二个实现中,复制过程从高地址到低地址进行。如果要复制的范围重叠(例如,滚动帧缓冲区),那么只有一个方向的操作是正确的,另一个方向会覆盖随后将被读取的位置。 memmove() 的实现最简单的方法是以某种平台相关的方式测试 dest<src,并执行适当方向的 memcpy()
用户代码当然不能这样做,因为即使将 srcdst 强制转换为某些具体指针类型,它们也不(一般情况下)指向同一个对象,因此无法进行比较。但标准库可以具备足够的平台知识来执行这样的比较而不会导致未定义行为。
请注意,实际应用中,实现往往会更加复杂,以获得更大的传输性能(当对齐允许时),和/或良好的数据缓存利用率。上面的代码只是为了尽可能简单地阐述这一点。

0

memmove函数可以处理重叠的源和目标区域,而memcpy函数则不能。在这两个函数中,memcpy函数更加高效。因此,如果可以的话最好使用memcpy函数。

参考资料:https://www.youtube.com/watch?v=Yr1YnOVG-4g Jerry Cain博士,(斯坦福大学系统介绍课程 - 第7讲),时间:36:00


1
这个回答说“可能稍微快一点”,并提供了数量数据,表明只有轻微的差异。而另一个回答则断言其中一个方法“更加高效”。你发现更快的那个方法有多高效呢?顺便说一下:我想你是指memcpy()而不是memcopy() - chux - Reinstate Monica
该评论是基于Jerry Cain博士的讲座所做的。我建议您在36:00时候收听他的演讲,只需要2-3分钟即可。谢谢你的提醒。 :D - Ehsan

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