memcpy() против memmove()

Я пытаюсь понять разницу между 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 );
}

Выход:

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

12 ответов

Решение

Я не совсем удивлен, что ваш пример не демонстрирует странного поведения. Попробуйте скопировать str1 в str1+2 вместо этого и посмотрим, что будет потом. (Может не иметь никакого значения, зависит от компилятора / библиотек.)

В общем, memcpy реализован простым (но быстрым) способом. Проще говоря, он просто перебирает данные (по порядку), копируя их из одного места в другое. Это может привести к перезаписи источника во время чтения.

Memmove делает больше работы, чтобы обеспечить правильную обработку перекрытия.

РЕДАКТИРОВАТЬ:

(К сожалению, я не могу найти достойных примеров, но они подойдут). Сравните реализации memcpy и memmove, показанные здесь. memcpy просто зацикливается, а memmove выполняет тест, чтобы определить направление зацикливания, чтобы избежать повреждения данных. Эти реализации довольно просты. Большинство высокопроизводительных реализаций являются более сложными (включая копирование блоков размером в слово за раз, а не байтов).

Память в 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 не должен иметь дело с перекрывающимися регионами, не означает, что он не имеет с ними дело правильно. Вызов с перекрывающимися регионами вызывает неопределенное поведение. Неопределенное поведение может работать полностью так, как вы ожидаете на одной платформе; это не значит, что это правильно или правильно.

И memcpy, и memove делают схожие вещи.

Но чтобы заметить одно отличие:

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

char str1[17] = "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( "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 из-за "плохого" компилятора, оно делает вам одолжение в отладочной версии. Однако версия выпуска дает тот же результат, но из-за оптимизации.

    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  

Вот почему я всегда выбираю memmove при попытке скопировать 2 перекрывающихся блока памяти.

С11 стандартная тяга

Проект стандарта C11 N1570 гласит:

7.24.2.1 "Функция memcpy":

2 Функция memcpy копирует n символов из объекта, на который указывает s2, в объект, на который указывает s1. Если копирование происходит между объектами, которые перекрываются, поведение не определено.

7.24.2.2 "Функция memmove":

2 Функция memmove копирует n символов из объекта, на который указывает s2, в объект, на который указывает s1. Копирование происходит так, как будто n символов из объекта, на который указывает s2, сначала копируются во временный массив из n символов, который не перекрывает объекты, на которые указывают s1 и s2, а затем n символов из временного массива копируются в объект, на который указывает s1

Следовательно, любое совпадение memcpy ведет к неопределенному поведению, и может случиться все что угодно: плохо, ничего или даже хорошо. Хорошо это редко, хотя:-)

memmove однако ясно говорит, что все происходит так, как будто используется промежуточный буфер, поэтому ясно, что перекрытия в порядке.

C++ std::copy однако, более прощающий и допускает перекрытия: обрабатывает ли std::copy перекрывающиеся диапазоны?

Разница между memcpy а также memmove в том, что

  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

Как уже указывалось в других ответах, memmove более сложный, чем memcpy так что он учитывает перекрытия памяти. Результат memmove определяется как src был скопирован в буфер, а затем буфер скопирован в dst, Это НЕ означает, что фактическая реализация использует какой-либо буфер, но, вероятно, выполняет некоторую арифметику указателей.

Компилятор может оптимизировать memcpy, например:

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

Этот memcpy может быть оптимизирован как: x = *(int*)some_pointer;

Код, приведенный в ссылках 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 позаботится о перекрывающихся проблемах.

Это пример, который я написал, пожалуйста, обратитесь к нему. Проблема перезаписи копии памяти возникает только в том случае, если область, в которой находится dst, находится за областью src.

      #include "stdio.h"
#include "string.h"

#define ARRAY_SIZE(a) sizeof(a) / sizeof(a[0])

#define PRINT_ARRAY(arr, size, format)      \
    do {                                    \
        for (size_t i = 0; i < size; i++) { \
            printf(format, arr[i]);         \
        }                                   \
        printf("\n");                       \
    } while (0)

int test_memcpy_memmove(void)
{
    /* T1 src and dst memory do not overlap at all */
    char s1[] = {'a', 'b', 'c', '\0'};
    char s2[] = {'1', '2', '3', '\0'};

    printf("T1--------------------------:\n");
    printf("before s1 and s2:\n");
    PRINT_ARRAY(s1, ARRAY_SIZE(s1), "%c");
    PRINT_ARRAY(s2, ARRAY_SIZE(s1), "%c");
    printf("after use memcpy s2:\n");
    memcpy(s2, s1, 3);
    PRINT_ARRAY(s2, ARRAY_SIZE(s1), "%c");

    /* T2 src and dst memory overlap, and the area where dst is located is in front of the src area */
    printf("T2--------------------------:\n");
    char a[10] = {'a', 'b', 'c', 'd', 'e', 'f', '\0', '\0', '\0', '\0'};
    char b[10] = {0};
    memcpy(b, a, sizeof(char) * 10);
    printf("before a and b:\n");
    PRINT_ARRAY(a, ARRAY_SIZE(a), "%c");
    PRINT_ARRAY(b, ARRAY_SIZE(b), "%c");

    printf("after use memcpy a:\n");
    memcpy(&a[0], &a[2], 3);
    PRINT_ARRAY(a, ARRAY_SIZE(a), "%c"); // cdedef

    printf("after use memmove b:\n");
    memmove(&b[0], &b[2], 3);
    PRINT_ARRAY(a, ARRAY_SIZE(a), "%c"); // cdedef

    /* T3 !!! The memory of src and dst overlap, and the area of dst is behind the src area. */
    printf("T3--------------------------:\n");
    char c[10] = {'a', 'b', 'c', 'd', 'e', 'f', '\0', '\0', '\0', '\0'};
    char d[10] = {0};
    memcpy(d, c, sizeof(char) * 10);
    printf("before c and d:\n");
    PRINT_ARRAY(c, ARRAY_SIZE(c), "%c");
    PRINT_ARRAY(d, ARRAY_SIZE(d), "%c");

    printf("after use memcpy c:\n");
    memcpy(&c[2], &c[0], 3);
    PRINT_ARRAY(c, ARRAY_SIZE(c),
                "%c"); // ababaf >>uses memcpy to overwrite the original characters expected to be copied

    printf("after use memmove d:\n");
    memmove(&d[2], &d[0], 3);
    PRINT_ARRAY(d, ARRAY_SIZE(d),
                "%c"); // ababcf >>uses memmove and does not overwrite the characters to be copied
    return 0;
}

Я попытался запустить ту же программу, используя Eclipse, и она показывает четкую разницу между memcpy а также memmove, 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)

Выход

memcpy: aaaaaa

memmove: aaaabb

Другие вопросы по тегам