memcpy () vs memmove ()


157

Я намагаюся зрозуміти різницю між 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

1
У Microsoft CRT вже довгий час була безпечна пам’ятка ().
— Ганс Пасант

32
Я не думаю, що "безпечний" - це правильне слово для цього. Безпечно memcpy, assertщо регіони не перекриватимуться, а не навмисно приховувати помилки у вашому коді.
— R .. GitHub СТОП ДОПОМОГАТИ ICE

6
Залежить від того, ви маєте на увазі "безпечний для розробника" чи "безпечний для кінцевого користувача". Я заперечую, що безпечнішим вибором для кінцевого споживача є навіть те, що сказано, навіть якщо це не відповідає стандартам.
— кусма

оскільки glibc 2.19 - не працює The string: aabbcc New string: aaaaaa The string: aabbcc New string: aaaabb
— askovpen

Ви також можете побачити тут .
— Ren

Відповіді:


124

Я не зовсім здивований, що ваш приклад не проявляє дивної поведінки. Спробуйте скопіювати str1в str1+2замість цього і бачити те , що відбувається потім. (Може насправді не змінити значення, залежить від компілятора / бібліотеки.)

Загалом, memcpy реалізується просто (але швидко). Простіше кажучи, він просто перетинає дані (по порядку), копіюючи з одного місця в інше. Це може призвести до перезапису джерела під час його читання.

Memmove робить більше роботи, щоб переконатися, що вона правильно обробляє перекриття.

Редагувати:

(На жаль, я не можу знайти гідних прикладів, але це вдасться). Контрастність тетсра і memmove реалізації , показаний тут. memcpy просто циклічно, в той час як memmove виконує тест, щоб визначити, в якому напрямку потрібно циклічити, щоб уникнути пошкодження даних. Ці реалізації досить прості. Більшість високоефективних реалізацій є складнішими (передбачають копіювання блоків розміру слова за раз, а не байти).


2
+1 Також у наступній реалізації memmoveдзвінки memcpyв одну гілку після тестування покажчиків: student.cs.uwaterloo.ca/~cs350/common/os161-src-html/…
— Паскаль Куок

Звучить чудово. Схоже, Visual Studio реалізує "безпечну" мемкпію (разом з gcc 4.1.1, я також тестував на RHEL 5). Написання версій цих функцій із сайту clc-wiki.net дає чітку картину. Дякую.
— user534785

3
memcpy не піклується про перекриття, але memmove робить. Тоді чому б не усунути мемкпі з ліба?
— Алькотт

37
@Alcott: Тому що memcpyможе бути швидше.
— Біллі ONeal

Виправлений / веб-архівне посилання з Паскаля Куока вище: web.archive.org/web/20130722203254/http://…
— JWCS

94

Пам'ять у 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 все ще можуть працювати для перекриття входів, але ви не можете розраховувати на цю поведінку. У той час як пам'ятка повинна допускати перекриття.


3
це дуже допомогло мені thaks! +1 для вашої інформації
— Муту Ганапатій Натан

33

Тільки тому, що memcpyйому не доводиться мати справу з регіонами, що перекриваються, не означає, що він не працює з ними правильно. Виклик із регіонами, що перекриваються, викликає не визначену поведінку. Невизначена поведінка може працювати повністю так, як ви очікуєте на одній платформі; це не означає, що це правильно чи дійсно.


10
Зокрема, залежно від платформи, можливо, memcpyвона реалізується точно так само, як і memmove. Тобто той, хто писав компілятор, не переймався написанням унікальної memcpyфункції.
— Cam

19

І 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

IMHO, ця прикладна програма має деякі недоліки, оскільки буфер str1 має доступ поза межами (10 байт для копіювання, буфер має розмір 7 байт). Помилка поза межами призводить до невизначеної поведінки. Відмінності в показаних результатах викликів memcpy () / memmove () є специфічними для реалізації. І приклад виведення точно не відповідає програмі, наведеній вище ... Крім того, strcpy_s () не є частиною стандартного C AFAIK (специфічний для MS, див. Також: stackoverflow.com/questions/36723946/… ) - Будь ласка, виправте мене, якщо я я помиляюся
— rel

7

Ваша демонстрація не виявила недоліки 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 перекриті блоки пам'яті.


3

Різниця між 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

6
-1 - для memmove немає потреби фактично копіювати дані в окремий буфер
— jjwchoy

цей приклад не допомагає зрозуміти концепцію .... так як більшість компіляторів видасть те саме, що і результат переходу mem
— Jasdeep Singh Arora

1
@jjwchoy Концептуально це робить. Зазвичай буфер оптимізовано
— MM

Той самий результат і в Linux.
— CodyChan

2

Як уже вказувалося в інших відповідях, він memmoveє більш складним, ніж memcpyтакий, що припадає на перекриття пам'яті. Результат memmove визначається так, як якщо б srcфайл було скопійовано в буфер, а потім було скопійовано буфер dst. Це НЕ означає, що реальна реалізація використовує будь-який буфер, але, ймовірно, має деяку арифметику вказівника.


1

компілятор може оптимізувати memcpy, наприклад:

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

Ця пам’ять може бути оптимізована як: x = *(int*)some_pointer;


3
Така оптимізація допустима лише для архітектур, які дозволяють нестандартний intдоступ. У деяких архітектурах (наприклад, Cortex-M0) спроба отримати 32-бітний intадресу, що не є кратним чотирьох, спричинить збій (але memcpyбуде працювати). Якщо ви будете використовувати або центральний процесор, який дозволяє несанкціонований доступ, або використання компілятора з ключовим словом, яке спрямовує компілятора збирати цілі числа з окремо підібраних байтів, коли це необхідно, можна зробити щось на кшталт, #define UNALIGNED __unalignedа потім `x = * (int UNALIGNED * ) some_pointer;
— supercat

2
Деякі процесори не допускають нерівномірного збою доступу до int, char x = "12345"; int *i; i = *(int *)(x + 1);але деякі - тому, що вони виправляють копію під час помилки. Я працював над такою системою, і це знадобило трохи часу, щоб зрозуміти, чому продуктивність настільки низька.
— користувач3431262

*(int *)some_pointerє суворим порушенням чищення, але ви, мабуть, маєте на увазі, що компілятор видасть збірку, яка копіює інт
— MM

1

Код, наведений у посиланнях http://clc-wiki.net/wiki/memcpy for 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 піклується про проблему, що перекривається.


1

С11 чернетка

Стандартна чернетка C11 N1570 говорить:

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

2 Функція memcpy копіює n символів з об'єкта, на який вказує s2, в об'єкт, на який вказує s1. Якщо копіювання відбувається між об'єктами, які перекриваються, поведінка не визначена.

7.24.2.2 "Функція пам'яті":

2 Функція пам'яті копіює n символів з об'єкта, на який вказує s2, в об'єкт, на який вказує s1. Копіювання відбувається так, ніби n символів з об'єкта, на які вказує s2, спочатку копіюються у тимчасовий масив з n символів, який не перекриває об'єкти, на які вказують s1 та s2, а потім n символів з тимчасового масиву копіюються в об’єкт, на який вказує s1

Тому будь-яке перекриття memcpyпризводить до невизначеної поведінки, і може статися все, що завгодно: погано, нічого або навіть добре. Хоча рідко, хоча :-)

memmove однак чітко сказано, що все відбувається так, ніби використовується проміжний буфер, тому чітко перекриваються в порядку.

std::copyОднак C ++ є більш прощальним і дозволяє перекриватись: чи діапазони перекриття ручки std :: copy копіюють?


memmoveвикористовувати додатковий тимчасовий масив n, так чи використовується додаткова пам'ять? Але як це зробити, якщо ми не надали йому доступ до жодної пам'яті. (Для цього використовується 2x пам'ять).
— clmno

@clmno він виділяє стек або malloc, як і будь-яка інша функція, яку я очікував :-)
— Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

1
Я тут поставив запитання , отримав хорошу відповідь. Дякую. Бачив ваш hackernews пост , який пішов вірусний (х86) один :)
— clmno

-4

Я спробував запустити ту саму програму за допомогою 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


2
Ласкаво просимо до переповнення стека. Будь ласка, прочитайте сторінку About About скоро. Є різні питання, які потрібно вирішити. Перш за все, ви додали відповідь на запитання з декількома відповідями, починаючи з 18 місяців. Для того, щоб підтвердити доповнення, вам потрібно буде надати приголомшливу нову інформацію. По-друге, ви вказуєте Eclipse, але Eclipse - це IDE, який використовує компілятор C, але ви не визначаєте платформу, де працює ваш код або використовує компілятор C Eclipse. Мені буде цікаво дізнатись, як ви визначаєте ці memmove()копії в проміжне місце. Він повинен просто скопіювати зворотний при необхідності.
— Джонатан Леффлер

Дякую. Про компілятор, тому я використовую компілятор gcc на Linux. У linux є сторінка man для пам'яті, яка чітко вказує, що memove буде копіювати дані у тимчасову змінну, щоб уникнути перекриття даних. Ось посилання на сторінку цієї людини linux.die.net/man/3/memmove
— Pratik Panchal

3
Це насправді говорить «як би», що не означає, що це те, що відбувається насправді. Зрозуміло, це насправді може зробити це саме так (хоча виникають запитання щодо того, звідки у нього запасна пам’ять), але я був би більш ніж трохи здивований, якби це було насправді. Якщо адреса джерела більше, ніж цільова адреса, достатньо скопіювати від початку до кінця (вперед копія); якщо адреса джерела менша, ніж цільова адреса, достатньо скопіювати її від кінця до початку (зворотна копія). Ніяка допоміжна пам'ять не потрібна і не використовується.
— Джонатан Леффлера

спробуйте пояснити свою відповідь фактичними даними в коді, що було б корисніше.
— HaseeB Mir
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.