memcpy ou memmove en C : comprendre le chevauchement

Comprends quand choisir memcpy ou memmove en C : chevauchement, sens de copie, insertion dans un tampon et octets nuls, avec exemples et exercice.

CPar
Publié le

Tu veux décaler une partie d'un tableau pour insérer du texte. Les tailles semblent correctes, mais une boucle de copie répète les mêmes caractères au lieu de déplacer les données.

En bref

Utilise memcpy(3) pour copier des octets entre zones qui ne se chevauchent pas. Si source et destination peuvent partager des octets, choisis memmove(3) : la destination recevra les valeurs que la source contenait avant l'appel. Les deux fonctions prennent un nombre d'octets, renvoient la destination et n'ajoutent aucun '\0' ; tu dois aussi garantir la place disponible.

Que veut dire « zones qui se chevauchent » ?

Deux zones se chevauchent quand au moins un octet appartient aux deux. Pour copier cinq octets de buf vers buf + 2, la source occupe les indices 0 à 4 et la destination les indices 2 à 6. Les indices 2, 3 et 4 sont communs.

ZoneIndices concernésContenu avant copie
Source0, 1, 2, 3, 401234
Destination2, 3, 4, 5, 623456
Partie commune2, 3, 4234

Être dans le même tableau ne suffit pas à créer un chevauchement : deux fenêtres disjointes de ce tableau peuvent se copier avec memcpy(3). Ce sont les plages d'octets qui comptent, pas le nom des pointeurs.

Pourquoi une copie de gauche à droite peut-elle écraser la source ?

Elle écrit dans une case dont elle aura encore besoin. Voici une boucle volontairement naïve, avec des accès tous situés dans le tableau.

naive.c
#include <stddef.h>#include <stdio.h>
int main(void){    char buf[] = "0123456789";    for (size_t i = 0; i < 5; ++i)        buf[i + 2] = buf[i];    puts(buf);    return 0;}
Terminal
$ gcc -std=c17 -Wall -Wextra -Werror naive.c -o naive$ ./naive0101010789

Au premier tour, la case 2 reçoit '0'. Au troisième, la boucle lit cette case 2, qui ne contient déjà plus le '2' initial.

Tour iOctet luÉcritureTableau après écriture
0'0'buf[2]0103456789
1'1'buf[3]0101456789
2'0'buf[4]0101056789
3'1'buf[5]0101016789
4'0'buf[6]0101010789

Cette sortie décrit cette boucle précise. Elle ne prédit pas le résultat d'un memcpy(buf + 2, buf, 5) : cet appel aurait un comportement indéfini. Une bibliothèque peut copier dans un autre ordre, traiter plusieurs octets à la fois ou sembler donner le bon résultat. Aucun de ces résultats ne rend l'appel valide.

Comment memmove conserve-t-il les octets d'origine ?

memmove(3) garantit que chaque octet de destination reçoit l'octet source correspondant d'avant la copie, y compris en cas de chevauchement. Dans notre décalage vers la droite, une copie de droite à gauche permettrait de respecter ce contrat : lire la case 4 avant d'écrire la case 6, puis la case 3 avant la case 5, jusqu'à la case 0.

L'autre sens existe aussi. Déplacer une zone vers des indices plus petits peut se faire de gauche à droite. Voici les deux décalages sur des tableaux initialement identiques :

shift.c
#include <stdio.h>#include <string.h>
int main(void){    char right[] = "0123456789";    char left[] = "0123456789";    memmove(right + 2, right, 5);    memmove(left, left + 2, 5);    puts(right);    puts(left);    return 0;}
Terminal
$ gcc -std=c17 -Wall -Wextra -Werror shift.c -o shift$ ./shift01012347892345656789

Le nombre 5 ne déplace que cinq octets. Les cases hors destination restent telles qu'elles étaient ; le terminateur à l'indice 10 n'est pas touché. Le nom memmove(3) ne signifie pas que la source est effacée après copie.

Le sens de parcours explique une implémentation possible, pas une obligation sur le code interne de ta libc. Pour l'exercice, commence par raisonner sur deux fenêtres d'un même tableau. Une comparaison dst > src entre pointeurs vers des objets indépendants ne fournit pas, en C portable, un test général de chevauchement.

C

memmove

32-bit

Réécris la copie avec chevauchement dans gm_memmove. Vérifie le décalage dans les deux sens, les fenêtres identiques et n == 0, sans confondre octet nul et fin de copie.

Ouvrir l'exercice

Comment insérer du texte sans perdre le terminateur ?

Décale le suffixe avec son '\0', après avoir vérifié la capacité. Ce programme réserve neuf octets, conserve le préfixe ab, ouvre deux cases, puis y copie XY.

insert.c
#include <stdio.h>#include <string.h>
int main(void){    char text[9] = "abcd";    size_t at = 2;    size_t added = 2;    size_t length = strlen(text);
    if (at > length || added > sizeof text - length - 1)        return 1;    memmove(text + at + added, text + at, length - at + 1);    memcpy(text + at, "XY", added);    puts(text);    return 0;}
Terminal
$ gcc -std=c17 -Wall -Wextra -Werror insert.c -o insert$ ./insertabXYcd

Le déplacement copie length - at + 1 octets : le + 1 inclut le terminateur. memcpy(3) convient ensuite pour XY, dont le stockage est distinct de text. Les deux octets copiés n'incluent pas son terminateur : celui du suffixe déplacé termine déjà le résultat.

La vérification utilise ici sizeof text parce que text est un tableau dans cette portée. Dans une fonction qui reçoit char *text, passe la capacité séparément : un pointeur ne donne pas la taille du tableau. Cette vérification suppose aussi que le texte initial est déjà terminé dans le tampon.

Est-ce que memmove s'arrête sur un octet nul ?

Non. Un octet nul fait partie des données comme n'importe quel autre octet.

bytes.c
#include <stdio.h>#include <string.h>
int main(void){    unsigned char data[] = {0x41, 0x00, 0x42, 0x43, 0x44, 0x45};    memmove(data + 1, data, 4);    for (size_t i = 0; i < sizeof data; ++i)        printf("%02X%c", (unsigned int)data[i],               i + 1 == sizeof data ? '\n' : ' ');    return 0;}
Terminal
$ gcc -std=c17 -Wall -Wextra -Werror bytes.c -o bytes$ ./bytes41 41 00 42 43 45

Les quatre octets 41 00 42 43 ont été copiés, y compris ceux qui suivent 00. Pour du texte, tu décides si le compte inclut le terminateur ; pour des données binaires, conserve une longueur explicite. Le guide sur les chaînes en C explique ce que le terminateur change pour les fonctions de texte.

Questions fréquentes

memcpy est-il toujours plus rapide que memmove ?

Non. Cela dépend des tailles, des adresses, du compilateur et de la bibliothèque. Choisis d'abord une fonction dont le contrat est respecté ; aucun gain de performance ne justifie un chevauchement avec memcpy(3).

memmove agrandit-il la destination ?

Non. Il ne réserve rien et ne vérifie pas la capacité. La source doit permettre la lecture des octets demandés et la destination leur écriture.

Puis-je copier une zone sur elle-même ?

Oui avec memmove(3), si la zone et le compte sont valides. Pour une copie non vide, utilise-le plutôt que de faire coïncider source et destination avec memcpy(3).

Que signifie n == 0 ?

Aucun octet n'est copié. Cela ne donne pas pour autant une autorisation générale de passer des pointeurs invalides aux fonctions de bibliothèque ; garde des pointeurs valides dans tes appels.

Sources

À toi d'écrire le code.