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.