- Apprendre
- Informatique et numérique
- Programmation
- Langage C : pointeurs, mémoire et structures
- Gestion dynamique de la mémoire en C
Gestion dynamique de la mémoire en C
DébutantUnité 5 sur 8 de la formation Formation : Langage C : pointeurs, mémoire et structures
Ce que tu vas apprendre
- Décrire la pile, le tas et la mémoire statique, la durée de vie des données dans chacun et les situations où un programme a besoin de mémoire dans le tas.
- Comparer la pile, le tas et le stockage statiqueDécrire les différences de durée de vie et de gestion entre le stockage automatique, statique et alloué, ainsi que ce qui arrive à chacun lors des appels de fonctions.
- Déterminer quand stocker les données dans le tasChoisir entre variables locales et allocation dans le tas selon la taille, la durée de vie et la croissance des données, et éviter de renvoyer l’adresse d’une variable locale.
- Comparer la pile, le tas et le stockage statique
- Réserver des blocs de la bonne taille avec malloc et calloc, vérifier que chaque demande a réussi et libérer chaque bloc exactement une fois sous la responsabilité d’un propriétaire clairement désigné.
- Allouer de la mémoire avec malloc et sizeofCalculer le nombre d’octets à demander, appeler malloc, vérifier l’échec éventuel et utiliser le bloc comme un tableau.
- Obtenir de la mémoire mise à zéro avec callocUtiliser calloc pour allouer des tableaux dont les éléments valent initialement zéro, et savoir quand cette fonction est préférable à malloc.
- Libérer la mémoire avec free et suivre la propriété des allocationsLibérer chaque bloc exactement une fois au bon moment, décider quelle partie du programme en est responsable et effectuer correctement le nettoyage lorsqu’une allocation échoue.
- Allouer de la mémoire avec malloc et sizeof
- Agrandir et réduire des blocs du tas sans perdre de données ni de mémoire, et construire des tableaux qui s’étendent à mesure que les données arrivent.
- Agrandir et réduire un bloc avec realloc en toute sécuritéUtiliser un pointeur temporaire avec realloc, préserver les données et le bloc d’origine en cas d’échec, et mettre à jour tous les pointeurs après un déplacement du bloc.
- Construire un tableau extensibleConserver une longueur et une capacité avec le pointeur, faire croître la capacité de façon géométrique et lire des données de longueur inconnue dans un tampon extensible.
- Agrandir et réduire un bloc avec realloc en toute sécurité
- Identifier les fuites de mémoire, les pointeurs pendants, les doubles libérations, les accès hors limites et les lectures de valeurs non initialisées dans du code C, expliquer leurs dangers et retrouver la cause d’une erreur de segmentation.
- Repérer et prévenir les fuites de mémoireExpliquer ce qu’est une fuite, repérer les constructions de code qui en provoquent et adopter des habitudes qui les évitent.
- Éviter les pointeurs pendants, les utilisations après libération et les doubles libérationsReconnaître les pointeurs vers une mémoire dont la durée de vie est terminée, expliquer pourquoi leur utilisation ou une nouvelle libération provoque un comportement indéfini, et adopter des habitudes qui l’évitent.
- Respecter les limites du bloc et éviter les débordements et les lectures de valeurs non initialiséesMaintenir chaque accès dans les bornes de son bloc, dimensionner correctement les tampons de chaînes et initialiser la mémoire avant de la lire.
- Expliquer les erreurs de segmentation et retrouver leur origineExpliquer ce qu’est une erreur de segmentation, citer ses causes fréquentes et remonter du plantage à la ligne fautive.
- Repérer et prévenir les fuites de mémoire
- Exécuter des programmes avec Memcheck de Valgrind et AddressSanitizer, lire leurs rapports et s’en servir pour corriger les fuites et les accès invalides jusqu’à obtenir une exécution sans erreur.
- Vérifier un programme avec Valgrind MemcheckCompiler un programme pour Valgrind, l’exécuter avec la recherche de fuites et lire ses messages d’erreur, son bilan du tas et ses catégories de fuites.
- Utiliser AddressSanitizer et LeakSanitizerCompiler un programme avec AddressSanitizer, lire ses rapports et choisir entre les sanitizers et Valgrind.
- Corriger les bogues de mémoire à partir du rapport d’un outilSuivre une procédure reproductible pour remonter du rapport d’un outil à la ligne fautive, corriger la cause plutôt que le symptôme et confirmer la correction par une exécution sans erreur.
- Vérifier un programme avec Valgrind Memcheck
Gestion dynamique de la mémoire en C : cours
1Expliquer où un programme C stocke ses données
Chaque objet a une durée de vie et un pointeur n’est utile que tant que sa cible existe. Les variables locales, les données statiques et les blocs alloués suivent des règles de durée de vie différentes. Relier ces règles à la pile et au tas explique pourquoi une fonction peut renvoyer un tableau alloué, mais ne peut pas renvoyer une adresse utilisable de son propre tableau local. La taille, la durée de vie et la croissance ultérieure guident le choix du stockage adapté à chaque besoin.
1.1 Stockage et durée de vie
L’adresse d’une variable indique où se trouvent ses données. Sa durée de stockage indique combien de temps ces données existent. Conserver une adresse ne prolonge pas la vie de l’objet.
Trois durées de vie
| Durée de stockage | Objets typiques | Fin de la durée de vie |
|---|---|---|
| Automatique | Variables locales ordinaires et paramètres | À la fin de leur bloc |
| Statique | Variables globales et variables locales déclarées static | À la fin du programme |
| Allouée | Blocs obtenus auprès des fonctions d’allocation | À la libération du bloc |
Sur les systèmes courants, les objets automatiques utilisent la pile d’appels et les blocs alloués utilisent le tas. Le C définit les durées de vie ; il n’impose pas ces régions physiques. Le code machine et les données statiques occupent d’autres régions, dont la disposition dépend du système.
Un appel de fonction crée normalement un cadre de pile contenant ses paramètres, ses variables locales et les informations de retour. Les appels s’imbriquent : le cadre le plus récent est retiré en premier. Le retour de la fonction libère le stockage automatique de cet appel, ce qui permet aux appels ultérieurs de réutiliser l’espace. L’allocateur du tas gère une réserve distincte. Il réserve un bloc contigu sur demande et fournit son adresse de début. Le retour d’une fonction ne libère pas ce bloc.
Le pointeur et son bloc sont des objets distincts
Considère cette fonction, déclarée avec <stdlib.h> disponible :
int *make_array(size_t n) {
if (n == 0 || n > SIZE_MAX / sizeof(int)) return NULL;
int *p = malloc(n * sizeof *p);
if (p == NULL) return NULL;
return p;
}
SIZE_MAX provient de <stdint.h>. Lorsque main appelle int *nums = make_array(5);, le paramètre n et la variable pointeur p appartiennent à l’appel. Ce n’est pas le cas du tableau alloué. Là où un int occupe 4 octets, le bloc contient 20 octets. Le retour copie son adresse dans nums ; il ne copie pas le tableau. La fonction appelante vérifie nums, utilise les éléments après leur avoir affecté une valeur, puis libère le bloc.
La variable locale p cesse d’exister au retour de la fonction. Le bloc subsiste, car sa durée de stockage est allouée et non automatique. Cette distinction fonctionne aussi dans l’autre sens : un pointeur local peut pointer vers un objet global et un pointeur global peut pointer vers un bloc du tas.
Les données statiques survivent aux appels
int next_id(void) {
static int id = 0;
return ++id;
}
Les appels successifs renvoient 1, 2 et 3. L’initialiseur s’exécute une fois et id existe pendant toute l’exécution du programme. Si l’on retire static, chaque appel crée une nouvelle variable automatique id, initialisée à zéro ; chaque appel renvoie alors 1.
L’espace de la pile est limité
Un grand tableau local et des appels profondément imbriqués consomment tous deux de l’espace dans la pile. Dépasser la limite provoque un débordement de pile, qui met généralement fin au programme. De nombreuses configurations Linux autorisent une pile de 8 MiB ; la réserve par défaut de Microsoft pour la pile du thread principal est généralement de 1 MiB. Ce sont des paramètres système, pas des règles du C. Un tableau local de 4 000 000 d’entiers de type int nécessite 16 000 000 octets là où un int occupe 4 octets, soit environ 15,3 MiB.
L’allocation dans le tas entraîne des coûts de gestion lors de la réservation et de la libération des blocs. Lire un élément n’est pas intrinsèquement plus lent simplement parce que son tableau se trouve dans le tas. Par ailleurs, la réserve de mémoire du tas n’a aucun rapport avec la structure de données appelée tas, utilisée dans une file de priorité.
Points clés
- La durée de stockage d’un objet détermine sa durée de vie.
- Les variables locales automatiques cessent d’exister à la fin de leur bloc ; les objets statiques existent pendant toute l’exécution du programme.
- Un pointeur local et le bloc du tas vers lequel il pointe ont des durées de vie distinctes.
- Les grands tableaux locaux et les appels profondément imbriqués peuvent épuiser la pile.
- Le C spécifie les durées de vie, tandis que la pile et le tas décrivent des implémentations courantes.
1.2 Choisir le stockage adapté au besoin
Choisis le stockage en te demandant combien d’espace est nécessaire, quand cette taille sera connue et combien de temps les données devront exister. La solution adaptée la plus simple est souvent un tableau local ordinaire.
Adapter le stockage aux données
| Besoin | Premier choix adapté |
|---|---|
| Un petit tableau de taille fixe utilisé pendant un appel | Tableau local |
| Un nombre lu dans les données d’entrée | Allocation vérifiée dans le tas |
| Un résultat qui survit à la fonction qui le crée | Allocation dans le tas ou tampon appartenant à la fonction appelante |
| Des données qui grandissent à mesure qu’elles arrivent | Tampon redimensionnable dans le tas |
| Un très grand tableau | Allocation dans le tas avec vérification des échecs |
Une semaine de températures quotidiennes nécessite sept valeurs : double temps[7]; est donc un choix naturel de tableau local. Pour une note par étudiant, la taille doit être obtenue à l’exécution. Avec n étudiants, utilise double *grades = malloc(n * sizeof *grades); après avoir vérifié la taille. Pour 250 étudiants et des valeurs double de 8 octets, cela demande 2 000 octets. Vérifie ensuite le résultat avant de remplir le tableau.
Pourquoi renvoyer un tampon local échoue
char *greeting(void) {
char buf[16] = "hello";
return buf; /* error: buf ends when this call returns */
}
L’adresse renvoyée ne peut pas prolonger la durée de vie de buf. La fonction appelante reçoit un pointeur pendant, même si les anciens octets semblent inchangés. GCC et Clang peuvent émettre un avertissement pour ce type de code.
Une solution consiste à laisser la fonction appelante fournir le stockage :
#include <string.h>
void greeting(char out[16]) {
strcpy(out, "hello");
}
/* In the caller: */
char message[16];
greeting(message);
Ici, le tableau de la fonction appelante reste en vie pendant tout l’appel et après celui-ci. Le paramètre se comporte toujours comme un pointeur ; la fonction appelante doit fournir suffisamment d’espace. Une deuxième solution alloue un résultat dans le tas et rend la fonction appelante responsable de sa libération. Une troisième renvoie un tampon statique. Ce tampon subsiste, mais toutes les fonctions appelantes le partagent et un appel ultérieur peut écraser son contenu. Choisis selon que les fonctions appelantes ont besoin ou non de résultats indépendants.
Une taille connue à l’exécution n’impose pas toujours le tas
C99 a introduit les tableaux de longueur variable, comme int a[n];. Leur durée de stockage est automatique et ils se trouvent généralement dans la pile. C11 a rendu leur prise en charge facultative et le compilateur de Microsoft ne les prend pas en charge. Une grande valeur saisie peut épuiser la pile et la déclaration du tableau ne fournit aucun résultat nul à vérifier. L’allocation dans le tas permet de vérifier les échecs et prend en charge le redimensionnement ultérieur.
Un pointeur n’indique pas la propriété
int value = 3;
int *p = &value;
p pointe vers un objet automatique. Le passer à free est invalide. La même règle interdit de libérer un tableau statique ou un littéral de chaîne. L’adresse doit appartenir à une allocation encore active dont le programme est responsable.
De même, char *s = "hello"; pointe vers un littéral de chaîne ; cette instruction ne crée pas de copie modifiable. Modifier le littéral provoque un comportement indéfini. Utilise char s[] = "hello"; pour obtenir une copie locale modifiable, ou alloue de l’espace pour une copie distincte dans le tas.
Points clés
- Choisis le stockage selon la taille des données, leur durée de vie nécessaire et leur besoin de croissance.
- Une adresse renvoyée par une fonction n’est utilisable que si sa cible existe encore.
- Avec un tampon fourni par la fonction appelante, celle-ci reste responsable de sa libération.
- Un tampon de résultat statique est partagé entre les appels.
- Les pointeurs peuvent viser des objets locaux, statiques ou alloués ; seuls les blocs alloués encore actifs peuvent être libérés.
- Les littéraux de chaîne ne sont pas des copies modifiables.
2Allouer et libérer de la mémoire dans le tas
Une demande d’allocation nécessite un nombre d’octets, un traitement en cas d’échec et un responsable de la libération. Pars du nombre d’éléments pour déterminer l’espace nécessaire, décide si leurs valeurs initiales doivent être nulles et utilise chaque bloc uniquement après une demande réussie. La propriété relie ensuite la dernière utilisation au nettoyage, notamment pour les allocations imbriquées et les fonctions dont une partie seulement des opérations réussit.
2.1 Demander le bon nombre d’octets
malloc réserve un bloc contigu d’octets et renvoie son adresse de début. Inclus <stdlib.h> pour obtenir sa déclaration. Une allocation réussie fournit un espace de stockage, mais ne le remplit pas de valeurs utiles.
Compter les éléments, puis déterminer leur taille
int *nums = malloc(n * sizeof *nums);
n est un nombre d’éléments ; sizeof *nums est le nombre d’octets par élément. Leur produit donne le nombre d’octets. Là où un int occupe 4 octets, dix entiers de type int nécessitent 40 octets. Dix valeurs double de 8 octets nécessitent 80 octets. Dix pointeurs nécessitent 80 octets là où chaque pointeur occupe 8 octets. Un char occupe toujours un octet C.
La forme sizeof *nums suit le type pointé si la déclaration change. Pour ce type ordinaire qui n’est pas de longueur variable, sizeof n’évalue pas *nums et ne déréférence donc pas un pointeur non initialisé. En C, le void * renvoyé est converti en type pointeur vers un objet sans conversion explicite.
malloc(n) alloue n octets, pas n entiers de type int. sizeof nums mesure la variable pointeur, pas un élément ni le bloc entier. Sur un système où les pointeurs occupent 8 octets et les entiers de type int 4 octets, malloc(n * sizeof nums) demande deux fois l’espace nécessaire, ce qui masque l’erreur dans l’expression. Avec d’autres types, l’espace alloué peut être insuffisant.
Vérifier la taille et l’échec avant utilisation
Ce programme complet rejette une demande vide, protège la multiplication contre le débordement, vérifie l’allocation et initialise chaque élément :
#include <stdint.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *nums;
if (n == 0 || n > SIZE_MAX / sizeof *nums) {
return EXIT_FAILURE;
}
nums = malloc(n * sizeof *nums);
if (nums == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < n; ++i) {
nums[i] = (int)(i * i);
}
free(nums);
return EXIT_SUCCESS;
}
SIZE_MAX, défini dans <stdint.h>, est la plus grande valeur de size_t. Vérifie par une division avant la multiplication. Si le produit reboucle d’abord, vérifier le résultat réduit après rebouclage ne permet pas de retrouver la taille voulue. Une demande non nulle peut échouer même lorsque son calcul est représentable : la vérification du pointeur nul est donc une étape distincte.
Les valeurs initialisées ici sont 0, 1, 4, 9 et 16. nums[i] accède au même élément que *(nums + i). L’arithmétique des pointeurs avance par éléments : le code ne multiplie donc pas i par la taille en octets.
Conserver le nombre d’éléments avec le pointeur
Aucune opération standard du C ne permet de retrouver le nombre d’éléments d’une allocation à partir de son pointeur. Stocke n séparément et passe le pointeur et le nombre aux fonctions. Ce nombre contrôle les boucles et les vérifications d’indices.
Une chaîne copiée nécessite ses caractères et le caractère de fin '\0'. La chaîne « Amsterdam » comporte neuf caractères et nécessite dix octets : demande donc strlen(s) + 1 pour sa copie. <string.h> fournit strlen.
Une demande malloc de taille nulle peut renvoyer un pointeur nul ou un pointeur qui peut être libéré mais pas déréférencé. Traite explicitement le cas où le nombre d’éléments est nul comme un cas vide, au lieu de considérer un tel pointeur comme un espace utilisable pour un tableau.
Points clés
- Pour obtenir la taille d’un tableau en octets, multiplie le nombre d’éléments par
sizeof *pointer. - Vérifie que la multiplication ne déborde pas avant d’appeler
malloc. - Vérifie que le résultat n’est pas
NULLavant de l’utiliser. - Initialise chaque élément alloué avec
mallocavant de le lire. - Conserve séparément le nombre d’éléments ;
sizeof pointerne permet pas de le retrouver. - Inclus le caractère nul de fin de chaîne lors de l’allocation d’une copie de chaîne.
- Traite explicitement le cas où le nombre d’éléments est nul.
2.2 Initialiser un tableau à zéro
Un compteur a besoin d’une valeur initiale définie avant la première incrémentation. calloc combine allocation et mise à zéro : un tableau de compteurs entiers est donc prêt à compter après la vérification de l’allocation.
Deux arguments décrivent le tableau
Inclus <stdlib.h> et écris :
int *tally = calloc(26, sizeof *tally);
if (tally == NULL) return EXIT_FAILURE;
Le premier argument est le nombre d’éléments et le second la taille d’un élément. Là où un int occupe 4 octets, cela réserve 104 octets. Chaque octet commence à zéro : chaque compteur entier commence donc à zéro. Appeler calloc avec un seul argument combiné ne compile pas ; son interface exige deux arguments.
Pour le mot « banana », incrémente le compteur correspondant à chaque lettre. Après son traitement, les compteurs de a, b et n contiennent 3, 1 et 2. Tous les autres restent à zéro. Avec un encodage où les lettres minuscules sont consécutives, l’expression d’indice est c - 'a'. La donnée d’entrée doit être une lettre minuscule avant que cette expression soit utilisée comme indice de tableau.
#include <stdlib.h>
int main(void) {
int *counts = calloc(3, sizeof *counts);
if (counts == NULL) return EXIT_FAILURE;
counts[1]++;
counts[1]++;
/* The counters now hold 0, 2, 0. */
free(counts);
return EXIT_SUCCESS;
}
Le code de comptage n’a écrit que dans le compteur du milieu. Ses deux voisins contiennent toujours des valeurs utiles parce que l’allocation les a mis à zéro.
Ce que signifie une mémoire mise à zéro
calloc met chaque bit à zéro. Cela donne la valeur zéro aux éléments entiers et char. Un tampon de caractères de taille positive mis à zéro contient donc initialement une chaîne vide : son premier caractère est '\0'.
Sur les plateformes courantes, les bits tous à zéro représentent aussi le zéro à virgule flottante et les pointeurs nuls. Le C portable ne garantit pas ces représentations pour les types à virgule flottante et les types pointeurs. Si le code a besoin de telles valeurs sur toutes les implémentations C, affecte explicitement 0.0 ou NULL aux éléments.
Choisir entre les fonctions d’allocation
| Utilisation initiale | Choix naturel |
|---|---|
| Incrémenter des compteurs entiers | calloc |
| Modifier progressivement des indicateurs entiers initialement tous à zéro | calloc |
| Écraser immédiatement chaque élément | malloc |
Un appel réussi à malloc suivi de memset peut mettre un bloc à zéro, mais la multiplication qui calcule la taille dans ce code doit toujours être protégée contre le débordement. Les implémentations courantes de calloc rejettent un produit trop grand pour size_t ; C23 l’exige explicitement. Le résultat doit toujours être vérifié, car une demande de taille correcte peut échouer et renvoyer un pointeur nul.
La mise à zéro s’applique à cette allocation. Si realloc agrandit ensuite le bloc, les octets ajoutés ne sont pas initialisés. Affecte une valeur à ces éléments avant de les incrémenter ou de les lire. Enfin, libère le bloc avec free, comme pour un bloc obtenu avec malloc.
Points clés
- Passe à
callocun nombre d’éléments et une taille d’élément. - Les compteurs entiers mis à zéro peuvent être incrémentés dès que l’allocation a été vérifiée.
- Des octets entièrement à zéro ne remplacent pas de façon portable l’affectation de zéro à virgule flottante ou de pointeurs nuls.
- Un agrandissement avec
reallocne met pas les octets ajoutés à zéro.
2.3 Un responsable et une libération finale
Pour chaque bloc alloué, il faut répondre clairement à la question « Qui le libère ? ». Cette responsabilité est la propriété. Une autre partie du code peut emprunter l’adresse sans obtenir l’autorisation de libérer le bloc.
Libérer après la dernière utilisation
free, déclarée dans <stdlib.h>, rend un bloc alloué encore actif disponible pour réutilisation. Passe son adresse de début exactement une fois. free(NULL) ne fait rien. Passer une adresse locale, un littéral de chaîne, une adresse intérieure comme p + 1 ou un bloc déjà libéré provoque un comportement indéfini.
free(p) n’affecte aucune valeur à la variable p et ne garantit pas l’effacement des octets. Après la libération, l’ancienne valeur du pointeur est invalide. Si la variable reste utilisée, affecte-lui p = NULL; sans examiner son ancienne valeur. Cela ne modifie que p ; les alias doivent eux aussi cesser d’utiliser le bloc libéré.
Une fonction qui renvoie de la mémoire nouvellement allouée en transfère la propriété à la fonction appelante :
#include <stdlib.h>
#include <string.h>
char *copy_string(const char *s) {
char *copy = malloc(strlen(s) + 1);
if (copy == NULL) return NULL;
strcpy(copy, s);
return copy;
}
La fonction appelante vérifie le résultat, utilise la copie et la libère. La chaîne d’entrée est empruntée ; cette fonction ne la libère pas. POSIX et C23 proposent strdup selon le même principe de propriété ; la fonction ci-dessus fonctionne aussi en C17.
Libérer les allocations imbriquées de l’intérieur vers l’extérieur
Une grille constituée d’un tableau de pointeurs de lignes comporte des blocs distincts. Trois pointeurs de lignes et trois lignes de quatre entiers de type int nécessitent quatre allocations. Avec des pointeurs de 8 octets et des entiers de type int de 4 octets, le bloc extérieur utilise 24 octets et les lignes 48 octets, soit 72 octets au total.
Libère chaque ligne tant que son adresse est encore disponible, puis libère le tableau de pointeurs. Libérer le bloc extérieur ne libère pas récursivement ses lignes. Accéder à grid[i] après avoir libéré grid utilise une zone de stockage libérée.
Nettoyer après une réussite partielle
Cet exemple complet suit le nombre de lignes effectivement créées :
#include <stdlib.h>
int main(void) {
size_t rows = 3, made = 0;
int **grid = malloc(rows * sizeof *grid);
if (grid == NULL) return EXIT_FAILURE;
for (; made < rows; ++made) {
grid[made] = malloc(4 * sizeof *grid[made]);
if (grid[made] == NULL) break;
}
int status = made == rows ? EXIT_SUCCESS : EXIT_FAILURE;
/* Use the grid here only if every row was allocated. */
for (size_t i = 0; i < made; ++i) free(grid[i]);
free(grid);
return status;
}
Si l’allocation de la deuxième ligne échoue, made vaut 1 : seule la ligne zéro est donc libérée. Aucun pointeur de ligne non initialisé n’est lu. Une autre méthode utile consiste à initialiser des pointeurs indépendants à NULL et à diriger chaque échec vers une section de nettoyage unique ; free(NULL) rend cette section sûre après une réussite partielle.
Compte les durées de responsabilité, pas les appels d’allocation. Un tampon créé par malloc et redimensionné avec succès deux fois nécessite un seul free final appliqué au dernier résultat ; un appel réussi à realloc met déjà fin à la durée de vie du bloc précédent. L’allocateur peut conserver l’espace libéré pour de futures allocations : un moniteur système n’affiche donc pas nécessairement une baisse immédiate de la mémoire du processus.
Points clés
- Le responsable libère chaque bloc après sa dernière utilisation.
- Seule l’adresse de début d’une allocation encore active, ou
NULL, est un argument valide pourfree. - Réinitialiser un pointeur ne corrige pas les alias.
- Libère les blocs intérieurs avant le bloc extérieur qui contient leurs adresses.
- Les chemins d’erreur doivent libérer tous les blocs obtenus auparavant.
- Après un redimensionnement réussi, le dernier bloc devra être libéré une fois avec
free.
3Redimensionner des tampons avec realloc
La taille finale des données d’entrée n’est pas toujours connue à l’avance. Un tampon redimensionnable permet à un programme de réserver davantage d’espace à mesure qu’il découvre ses besoins. Un agrandissement sûr préserve à la fois les valeurs existantes et la possibilité de réagir à un échec. Conserver ensemble un pointeur, une longueur et une capacité rend ces règles concrètes, tandis qu’une croissance géométrique limite les copies répétées.
3.1 Redimensionner sans perdre l’ancien bloc
realloc modifie la taille d’un bloc alloué. Elle peut agrandir ou réduire l’espace de stockage, et l’adresse renvoyée peut changer dans les deux cas. Inclus <stdlib.h> et traite la réussite et l’échec comme des événements distincts pour la durée de vie du bloc.
Ce qui est conservé et ce qui change
Lorsqu’un bloc grandit, son contenu existant est conservé jusqu’à l’ancienne taille. Les octets ajoutés ne sont pas initialisés. Lorsqu’il est réduit, seule la partie initiale qui tient dans la nouvelle taille est conservée. Un tableau contenant 3, 1, 4 et 1 conserve ces quatre valeurs lorsqu’il est agrandi à six entiers de type int ; les deux derniers éléments doivent recevoir une valeur avant utilisation. Avec des entiers de type int de 4 octets, la taille demandée passe de 16 à 24 octets.
L’allocateur peut agrandir le bloc sur place ou en allouer un autre ailleurs, copier les octets conservés et libérer l’ancien bloc. Ton code doit fonctionner dans les deux cas.
Recevoir le résultat dans un pointeur temporaire
int *tmp = realloc(nums, 6 * sizeof *nums);
if (tmp == NULL) {
/* nums still owns its original block. */
} else {
nums = tmp;
nums[4] = 5;
nums[5] = 9;
}
Pour cette demande de taille positive, un échec laisse le bloc d’origine alloué et inchangé. Le programme peut continuer à l’utiliser ou le libérer avant de signaler l’échec. En cas de réussite, la responsabilité porte désormais sur le bloc renvoyé. Affecte le nouveau résultat avant d’accéder au tableau redimensionné.
L’affectation directe nums = realloc(nums, bytes); fait perdre cette protection. Si l’appel échoue, elle écrase la seule adresse conservée par NULL. L’ancienne allocation reste réservée, mais le programme ne peut plus la libérer.
Recalculer les pointeurs enregistrés vers des éléments
Supposons que le code enregistre int *selected = &nums[2];. Un redimensionnement réussi met fin à la durée de vie de l’ancien objet, même si l’adresse renvoyée se trouve être identique. selected est invalide après cette réussite. Conserve l’indice 2 pendant le redimensionnement, puis recalcule selected = &nums[2]; à partir du nouveau pointeur si cet indice existe encore.
Ne compare pas l’ancien pointeur au résultat pour décider si les alias doivent être mis à jour. À chaque réussite, repars du nouveau résultat. Si la réduction a supprimé l’élément sélectionné, il n’y a aucun pointeur vers cet élément à recalculer.
Exemple complet d’agrandissement et de libération
#include <stdlib.h>
int main(void) {
int *nums = malloc(4 * sizeof *nums);
if (nums == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < 4; ++i) nums[i] = (int)i;
int *tmp = realloc(nums, 6 * sizeof *nums);
if (tmp == NULL) {
free(nums);
return EXIT_FAILURE;
}
nums = tmp;
nums[4] = 4;
nums[5] = 5;
free(nums);
return EXIT_SUCCESS;
}
Pour un nombre variable d’éléments, vérifie avant cet appel la multiplication qui donne le nombre d’octets, comme pour malloc. Demande des octets, pas des éléments : realloc(nums, 6) demande six octets.
realloc(NULL, positive_size) se comporte comme malloc, ce qui permet à un tampon vide de commencer avec un pointeur nul. Pour libérer un tampon, appelle explicitement free. Évite les redimensionnements à une taille nulle : le comportement des bibliothèques a varié par le passé, et le redimensionnement d’un bloc existant à zéro est un comportement indéfini en C23. Les tableaux locaux et les blocs déjà libérés ne sont pas des cibles valides pour un redimensionnement.
Points clés
- Utilise un pointeur temporaire pour qu’un échec de redimensionnement à une taille positive préserve l’adresse détenue par le responsable du bloc.
- L’agrandissement conserve l’ancien contenu, mais laisse les octets ajoutés non initialisés.
- La réduction ne conserve que la partie qui tient encore dans le bloc.
- Après une réussite, utilise le pointeur renvoyé et recalcule les pointeurs enregistrés vers des éléments.
- Utilise
freepour libérer l’espace de stockage ; traite explicitement les tailles nulles.
3.2 Un tableau qui grandit avec les données d’entrée
Un tableau extensible nécessite trois éléments d’état : son pointeur, sa longueur et sa capacité. La longueur compte les éléments initialisés et utilisés. La capacité compte les emplacements alloués pour les éléments. Maintiens la longueur inférieure ou égale à la capacité.
Ajouter dans un ordre sûr
Commence avec int *data = NULL; size_t len = 0, cap = 0;. Avant un ajout, vérifie si len == cap. Un tableau plein doit être agrandi avant l’écriture dans data[len]. Le pointeur et la capacité ne peuvent changer qu’après la réussite du redimensionnement. Stocke la nouvelle valeur, puis augmente la longueur.
L’exemple complet suivant ajoute dix valeurs avec une croissance géométrique :
#include <stdint.h>
#include <stdlib.h>
int main(void) {
int *data = NULL;
size_t len = 0, cap = 0;
for (int value = 0; value < 10; ++value) {
if (len == cap) {
size_t next;
if (cap == 0) {
next = 4;
} else {
if (cap > SIZE_MAX / 2 / sizeof *data) {
free(data);
return EXIT_FAILURE;
}
next = cap * 2;
}
int *tmp = realloc(data, next * sizeof *data);
if (tmp == NULL) {
free(data);
return EXIT_FAILURE;
}
data = tmp;
cap = next;
}
data[len++] = value;
}
free(data);
return EXIT_SUCCESS;
}
La vérification du débordement contrôle à la fois le doublement et la conversion en octets avant que l’un des deux produits puisse reboucler. La capacité initiale de quatre est elle aussi représentable pour ce type d’élément. En cas d’échec, le programme libère l’ancienne allocation dont il est toujours responsable et s’arrête ; il ne met pas la capacité à jour à tort.
Suivre séparément la longueur et la capacité
En partant d’une capacité de 4, les quatre premiers ajouts utilisent l’espace existant. Avant le cinquième, la capacité double et passe à 8. Avant le neuvième, elle double et passe à 16. Après dix ajouts, la longueur est de 10 et la capacité de 16. Si les deux agrandissements déplacent le bloc, ils copient au plus quatre et huit éléments, soit douze au total.
La capacité disponible est volontaire. Augmenter la capacité d’un élément à chaque ajout entraîne des copies répétées. En partant d’une capacité de 1, atteindre 1 000 éléments peut nécessiter 999 redimensionnements et 499 500 copies d’éléments. En doublant la capacité, on atteint 1 024 après dix redimensionnements et au plus 1 023 copies. Un redimensionnement particulier peut être coûteux, mais avec une croissance géométrique, le travail moyen par ajout reste constant.
Un facteur comme 1,5 laisse moins d’espace libre, mais entraîne des agrandissements plus fréquents. Si toutes les données ont été reçues, le programme peut réduire le bloc à la longueur utilisée en traitant l’échec avec la même méthode du pointeur temporaire. Un tableau vide est libéré avec free, et non par un redimensionnement à une taille nulle.
Les tampons de caractères nécessitent un emplacement supplémentaire
Pour une chaîne extensible, réserve de l’espace pour '\0'. Avant d’ajouter un caractère, agrandis le tampon si len + 1 >= cap. Avec une capacité de 8, sept caractères tiennent tout en laissant l’emplacement du caractère de fin ; « hello world » déclenche un agrandissement à 16 et nécessite douze octets, caractère de fin compris.
Un pointeur enregistré vers un élément devient invalide après un agrandissement réussi. Conserve un indice et retrouve l’élément à partir de la nouvelle adresse de début. Pour réinitialiser un tableau en vue d’une utilisation ultérieure, libère son bloc et exécute data = NULL; len = cap = 0;.
Points clés
- La longueur compte les éléments utilisés ; la capacité compte les emplacements disponibles.
- Agrandis le tableau avant un ajout lorsque la longueur est égale à la capacité.
- Ne mets à jour le pointeur et la capacité qu’après un redimensionnement réussi.
- La croissance géométrique limite les copies répétées au prix d’un espace libre supplémentaire.
- Protège les calculs de croissance contre le débordement avant la multiplication.
- Réserve un octet supplémentaire pour le caractère nul de fin d’une chaîne.
- Conserve les indices pendant l’agrandissement et recalcule ensuite les pointeurs vers les éléments.
4Reconnaître et prévenir les erreurs de mémoire
Les erreurs de mémoire proviennent du non-respect des règles de propriété, de durée de vie, de bornes ou d’initialisation. Chacune laisse une trace différente : une adresse perdue, un accès après libération, un indice hors du bloc ou une valeur lue trop tôt. Certaines erreurs provoquent immédiatement un plantage, tandis que d’autres corrompent des données et se manifestent plus tard. Lire le code en suivant les blocs et leur histoire aide à trouver la cause derrière le symptôme.
4.1 Trouver le chemin qui entraîne une fuite de mémoire
Une fuite de mémoire est une zone de stockage allouée dont un programme n’a plus besoin mais qu’il ne libère jamais. Souvent, le dernier pointeur disparaît, ce qui rend impossible un free ultérieur. Une fuite peut rester silencieuse pendant que l’utilisation de la mémoire augmente.
Suivre les adresses, pas les noms de variables
char *p = malloc(100);
if (p == NULL) return EXIT_FAILURE;
p = malloc(200); /* loses the only address of the first block */
if (p == NULL) return EXIT_FAILURE;
free(p);
Le free final ne libère que le second bloc. Le nom p a été réutilisé, mais pas la première allocation. Si la seconde demande échoue, le bloc de 100 octets est tout de même perdu. Libère le premier bloc avant de remplacer son adresse lorsqu’il n’est plus nécessaire, ou conserve son adresse dans une autre variable responsable de sa libération.
Affecter NULL au pointeur responsable d’un bloc avant de le libérer entraîne la même perte. L’affectation modifie une variable pointeur ; elle ne demande pas à l’allocateur de libérer quoi que ce soit. Les variables locales automatiques disparaissent à la fin d’un appel, mais les blocs alloués vers lesquels elles pointaient subsistent.
Examiner chaque sortie d’une fonction
Un retour anticipé peut sauter le nettoyage :
char *line = malloc(100);
if (line == NULL) return EXIT_FAILURE;
if (input_is_bad) {
free(line);
return EXIT_FAILURE;
}
/* Use line on the successful path. */
free(line);
Les deux chemins libèrent le bloc. Dans une fonction qui possède davantage de ressources, une section de nettoyage unique réduit le nombre de chemins à examiner. Une fonction qui renvoie un résultat alloué transfère la responsabilité de sa libération ; une fonction qui abandonne simplement un pointeur local responsable d’un bloc provoque une fuite.
Affecter directement le résultat d’un appel à realloc qui échoue est une autre façon de perdre un bloc. Les allocations imbriquées fuient lorsque leur tableau extérieur de pointeurs est libéré avant que les adresses intérieures aient servi au nettoyage.
La répétition rend les petites fuites coûteuses
Supposons qu’une boucle alloue 100 octets à chaque passage sans jamais les libérer. Après 1 000 passages, elle a abandonné 100 000 octets. Libère chaque tampon à la fin de son passage ou alloue-le une fois avant la boucle, réutilise le bloc et libère-le ensuite. La réutilisation évite aussi le travail répété de l’allocateur lorsque la même capacité suffit.
Un service qui perd 64 octets par requête perd 640 000 octets après 10 000 requêtes, soit exactement 625 KiB. Après un million de requêtes, il a perdu 64 000 000 octets, soit environ 61 MiB. Les programmes de longue durée peuvent finir par ne plus pouvoir allouer de mémoire ou ralentir le système. Le système d’exploitation récupère normalement la mémoire du processus à sa fin, mais cela n’aide pas le service pendant son exécution.
Blocs perdus et encore accessibles : des rapports différents
Si un pointeur global contient encore l’adresse de début d’une allocation à la fin du programme, Valgrind qualifie le bloc d’« encore accessible ». L’adresse n’a pas été perdue ; le programme n’a simplement pas libéré le bloc. « Définitivement perdu » signifie qu’aucun pointeur utilisable ne subsiste. Organise le nettoyage de manière explicite pour que ces rapports fassent clairement apparaître les allocations inexpliquées.
Points clés
- Écraser ou mettre à zéro le dernier pointeur responsable d’un bloc peut provoquer une fuite.
- Les chemins normaux, les retours anticipés et les chemins d’échec nécessitent tous un nettoyage.
- Les petites fuites répétées s’accumulent pendant l’exécution du programme.
- Réutiliser un tampon adapté peut éviter à la fois des allocations répétées et des fuites à chaque itération.
4.2 Lorsqu’un pointeur survit à sa cible
Un pointeur devient pendant lorsque la durée de vie de sa cible prend fin. La variable peut encore exister, mais lire ou écrire l’ancien objet à travers ce pointeur est invalide. Renvoyer une adresse locale et libérer un bloc alloué peuvent tous deux créer ce problème.
Trouver la dernière utilisation avant la libération
int *scores = malloc(3 * sizeof *scores);
if (scores == NULL) return EXIT_FAILURE;
scores[0] = 90;
scores[1] = 75;
scores[2] = 82;
free(scores);
printf("%d\n", scores[0]); /* use-after-free */
L’affectation à scores[0] a eu lieu pendant la durée de vie du bloc. Ce n’est pas le cas de la dernière lecture. Déplace free(scores) après l’affichage, une fois toutes les utilisations terminées. Affecter simplement scores = NULL avant l’affichage transforme l’accès invalide en déréférencement d’un pointeur nul ; cela ne fournit pas les données manquantes.
Il s’agit d’un comportement indéfini : le C n’impose aucune exigence sur le résultat. Le programme peut afficher une ancienne valeur, planter ou endommager silencieusement des données. Les octets libérés restent souvent en place jusqu’à ce que l’allocateur les réutilise. Un résultat plausible ne peut donc pas valider une lecture après libération.
Les alias partagent le problème de durée de vie
int *a = malloc(3 * sizeof *a);
if (a == NULL) return EXIT_FAILURE;
int *b = a;
free(a);
a = NULL;
b[0] = 1; /* b also lost its live target */
Il y avait un seul bloc, dont l’adresse était contenue dans deux variables pointeurs. Le libérer à travers a met fin à sa durée de vie pour les deux. Rendre a nul ne modifie que a ; cela ne peut pas mettre b à jour. Le code qui utilise des alias empruntés doit terminer son travail avant que le responsable libère le bloc, et ces alias ne doivent plus être utilisés ensuite.
La double libération utilise elle aussi une durée de vie terminée
Appeler free deux fois pour un même bloc constitue une double libération, y compris lorsque le second appel passe par un alias. Cela peut corrompre les informations de gestion de l’allocateur ou faire échouer une allocation ultérieure. Certaines bibliothèques la détectent immédiatement et arrêtent le programme, mais cette détection immédiate n’est pas garantie.
Affecter NULL à la variable responsable après la libération aide en cas d’appel répété au nettoyage, car free(NULL) est valide. Cela ne rend pas free(b) sûr lorsque b désigne encore l’ancien bloc. Un seul responsable doit décider du moment où le bloc est libéré.
Le redimensionnement met lui aussi fin à l’ancienne durée de vie
Un appel réussi à realloc remplace l’ancien objet même si la valeur numérique de l’adresse renvoyée est inchangée. Les pointeurs enregistrés vers l’intérieur de l’ancien bloc doivent être recalculés à partir du pointeur renvoyé. Stocke les positions sous forme d’indices tant qu’un agrandissement est possible. À l’inverse, un appel à realloc de taille positive qui échoue laisse l’ancienne allocation active.
Une fonction qui renvoie l’adresse d’une variable locale automatique crée une erreur de durée de vie similaire sans aucun appel à free : la cible cesse d’exister au retour de la fonction. Utilise un espace de stockage appartenant à la fonction appelante ou un résultat dans le tas dont le responsable est clairement désigné. L’utilisation après libération et la double libération peuvent aussi devenir des failles de sécurité lorsqu’un attaquant peut influer sur le contenu de l’espace réutilisé.
Points clés
- Un pointeur pendant vise un objet dont la durée de vie est terminée.
- Termine toutes les lectures et écritures avant de libérer le bloc.
- Une libération rend invalides tous les alias vers l’ancien objet.
- Rendre nul le pointeur responsable facilite les nettoyages répétés, mais ne corrige pas les alias empruntés.
- Une double libération peut endommager les informations de gestion de l’allocateur.
- Un appel réussi à
reallocrend invalides les pointeurs enregistrés vers l’intérieur de l’ancien bloc.
4.3 Bornes valides et valeurs initialisées
Un accès valide à un élément nécessite à la fois suffisamment d’espace et une valeur affectée avant sa lecture. Un bloc peut avoir la bonne taille mais contenir des données non initialisées, ou contenir des valeurs utiles mais être utilisé hors de ses bornes.
Vérifier la plage des indices
Pour n éléments, les indices valides vont de zéro à n - 1 inclus. Une boucle avec i <= n effectue un accès de trop. Avec dix entiers de type int de 4 octets, l’allocation est de 40 octets ; p[10] commence immédiatement après ces octets. Utilise i < n pour parcourir les dix éléments.
Un indice issu des données d’entrée doit aussi être vérifié avant utilisation. Une valeur signée saisie doit être positive ou nulle et inférieure au nombre d’éléments. Conserve le nombre avec le pointeur et passe les deux aux fonctions. Un pointeur seul ne peut pas décrire la plage autorisée.
Prévoir de la place pour le caractère de fin des chaînes
char *copy = malloc(strlen(s));
strcpy(copy, s); /* writes the terminator beyond the allocation */
Ce fragment incorrect nécessite aussi une vérification de l’échec. Pour corriger la taille, utilise malloc(strlen(s) + 1), puis vérifie le résultat avant de copier. « Amsterdam » comporte neuf caractères mais nécessite dix octets avec '\0'. Le caractère de fin est un octet effectivement stocké, même s’il n’est pas affiché dans le texte.
Un tableau de pointeurs ne stocke pas les chaînes
char **names = malloc(n * sizeof *names);
Cette instruction réserve n emplacements de pointeurs. Elle ne crée aucun tampon de caractères vers lequel ces pointeurs peuvent pointer. Avant d’utiliser names[i] comme chaîne, affecte-lui une adresse de chaîne valide. Il peut emprunter un tableau existant ou un littéral dont la durée de vie est suffisante, ou être responsable d’une copie de chaîne allouée séparément. Seules les chaînes dont le tableau est responsable sont libérées lors du nettoyage des blocs imbriqués.
Le même raisonnement sur la taille s’applique aux tableaux numériques : malloc(n) compte des octets et sizeof p mesure un pointeur. Utilise n * sizeof *p, avec une vérification du débordement et du pointeur nul, pour exprimer correctement un nombre d’éléments.
Écrire avant de lire
int *t = malloc(3 * sizeof *t);
if (t == NULL) return EXIT_FAILURE;
t[0] = 5;
t[1] = 7;
printf("%d\n", t[0] + t[1] + t[2]); /* t[2] was never set */
free(t);
t[2] se trouve dans le bloc alloué, mais sa valeur n’est pas initialisée. Affecte-lui une valeur avant la somme ou utilise calloc lorsque zéro est la valeur initiale voulue. Les octets ajoutés lors d’un agrandissement avec realloc demandent la même attention. Être dans les bornes n’initialise pas la mémoire.
Pourquoi le plantage peut se produire plus tard
Une écriture hors limites peut endommager les données voisines ou les informations de gestion de l’allocateur. Une lecture hors limites peut exposer des données sans rapport. Les deux provoquent un comportement indéfini. Un petit débordement peut toucher de la mémoire mappée et échapper à une détection immédiate ; un free ultérieur peut être le premier échec visible.
Vérifie séparément la taille de l’allocation, la plage des indices et l’historique d’initialisation. Une allocation plus grande ne peut pas fournir une valeur qui a un sens à un élément non initialisé, et la mise à zéro ne peut pas rendre valide un indice hors limites.
Points clés
- Pour
néléments, les indices valides vont de zéro àn - 1inclus. - Le stockage d’une chaîne comprend un octet pour le caractère nul de fin.
- Un tableau de pointeurs vers des chaînes n’alloue pas les chaînes elles-mêmes.
- Initialise toutes les valeurs lues par un calcul, y compris les éléments nouvellement ajoutés.
- Une écriture invalide peut corrompre des données bien avant un plantage visible.
4.4 Remonter d’un plantage à sa cause
Une erreur de segmentation est une réponse du système d’exploitation à un accès mémoire interdit. Ses causes fréquentes comprennent le déréférencement d’un pointeur nul ou non initialisé, l’écriture dans un littéral de chaîne, un accès très éloigné des bornes d’un tableau, l’utilisation d’un espace libéré et l’épuisement de la pile.
Ce que le message indique
Sous Linux, SIGSEGV arrête généralement le processus et un interpréteur de commandes peut afficher « Segmentation fault (core dumped) ». Un code de retour courant est 139, selon la convention qui additionne 128 et le numéro 11 du signal. macOS peut signaler SIGSEGV ou SIGBUS pour des accès invalides. Windows signale l’erreur comparable comme une violation d’accès, souvent avec le code d’exception 0xC0000005.
Ces messages indiquent un accès qui a échoué, pas toute son histoire. Un petit débordement peut corrompre un pointeur tôt dans l’exécution et provoquer un plantage bien plus tard. De nombreux accès invalides restent dans une mémoire que le processus peut adresser : ils corrompent alors silencieusement des données sans déclencher d’erreur.
Commencer par les informations de ligne
Compile un exécutable de débogage avec les avertissements :
cc -std=c17 -g -O0 -Wall -Wextra program.c -o program
Exécute-le ensuite dans un débogueur ou un outil de vérification de la mémoire. Une trace de pile liste les appels de fonctions actifs au moment du plantage. Trouve le cadre le plus haut qui appartient à ton propre code. Des fonctions de bibliothèque comme free peuvent être l’endroit où un état incorrect devient visible, alors qu’un appel antérieur dans ton programme l’a créé.
À la ligne signalée, identifie chaque pointeur déréférencé. Pour chacun, remonte de sa valeur à son origine. L’allocation a-t-elle été vérifiée ? Sa cible existe-t-elle encore ? L’indice est-il dans les bornes du bloc ? Une adresse valide a-t-elle été affectée au pointeur ?
Examiner un résultat nul
int *p = NULL;
printf("%d\n", *p); /* invalid dereference */
L’affichage nécessite une lecture à travers p, mais aucun objet entier n’existe à cette adresse. Si p provient d’une fonction d’allocation, vérifie le résultat avant cette ligne et suis un chemin d’erreur en cas d’échec. S’il a été mis à zéro après la libération, déplace l’utilisation nécessaire avant la libération ou revois l’endroit où les données sont conservées.
Un pointeur non initialisé, comme dans int *p; *p = 5;, n’a reçu aucune cible valide. Une allocation, l’adresse d’un objet encore actif ou un pointeur emprunté vérifié doit établir cette cible avant le déréférencement.
Suivre la cause lorsque les symptômes changent
Une fonction récursive sans fin peut épuiser les cadres de pile dans une version non optimisée. Une condition d’arrêt corrige la logique. Un compilateur optimisant peut toutefois transformer certains appels récursifs en boucle : l’absence de plantage de la pile ne rend donc pas utile ce calcul sans limite.
Les messages de progression affichés ne sont pas non plus une preuve parfaite. stdout est mis en tampon : une sortie peut ne pas être écrite lorsqu’un processus plante, surtout si elle est redirigée. Pour un message de diagnostic, utilise stderr ou appelle fflush(stdout) après un affichage de progression. Ajouter un affichage peut modifier la disposition de la mémoire et masquer une erreur ; cela n’a pas corrigé l’accès invalide. Relance le programme avec un outil de vérification pour trouver l’opération fautive elle-même.
Points clés
- Une erreur de segmentation signale un accès interdit ; la corruption initiale peut être antérieure.
- Compile avec les informations de débogage et repère le premier cadre pertinent dans ton propre code.
- Remonte à l’origine de chaque pointeur, à la durée de vie de sa cible et aux bornes des indices.
- La mise en tampon des sorties peut masquer un affichage exécuté avant un plantage.
- Un symptôme qui change après l’ajout d’un affichage doit toujours être examiné.
5Repérer les erreurs de mémoire avec Valgrind et les sanitizers
Les outils de vérification de la mémoire rendent visibles les erreurs cachées en signalant l’accès et l’histoire du bloc. Valgrind observe un exécutable de débogage ; AddressSanitizer ajoute des vérifications lors de sa compilation. Apprends à relier leurs messages aux bornes des éléments, aux points d’allocation et aux points de libération, puis à effectuer une correction qui respecte la propriété des blocs. Après une nouvelle exécution sans erreur, utilise d’autres données d’entrée pour parcourir d’autres chemins.
5.1 Lire un rapport Valgrind
L’outil par défaut de Valgrind, Memcheck, observe un programme compilé pendant son exécution. Il suit les bornes du tas, les durées de vie des allocations et l’initialisation des valeurs. Il ne nécessite pas de compilation avec un sanitizer, mais les informations de débogage rendent ses rapports bien plus faciles à exploiter.
Compiler et exécuter
cc -std=c17 -g -O0 -Wall -Wextra program.c -o program
valgrind --leak-check=full ./program
Place les arguments du programme après ./program. Ajoute --show-leak-kinds=all pour lister toutes les catégories de fuites et --track-origins=yes lorsque l’origine d’une valeur non initialisée n’est pas claire.
Valgrind fonctionne sous Linux et sur certains autres systèmes de type Unix, mais pas nativement sous Windows ni sur les Mac équipés d’une puce Apple. Memcheck ralentit souvent l’exécution d’environ 20 à 30 fois. Utilise d’abord une petite donnée d’entrée qui reproduit l’erreur, puis effectue d’autres tests. Un test sans erreur ne couvre que les chemins effectivement exécutés.
Lire séparément la taille de l’accès et celle du bloc
Un rapport commençant par « Invalid write of size 4 » décrit une écriture de quatre octets. Une adresse située à « 0 bytes after a block of size 40 alloc'd » commence immédiatement après une allocation de 40 octets. Si ce bloc contient dix entiers de type int de 4 octets, l’accès invalide commence à l’élément d’indice 10 ; le dernier élément valide a pour indice 9.
Le rapport indique aussi la ligne de l’accès et la pile d’appels de l’allocation. Utilise-les ensemble : l’allocation indique les bornes prévues et l’accès indique l’indice qui les a dépassées. « Size 4 » ne signifie pas que le programme a dépassé les bornes de quatre éléments.
« Invalid read of size N » décrit de même une lecture invalide. Une adresse à l’intérieur d’un bloc décrit comme « free'd » signale un problème de durée de vie ; compare les emplacements de son allocation, de sa libération et de l’accès. « Invalid free() / delete / delete[] / realloc() » peut indiquer une libération en double ou une adresse qui n’a jamais été le début d’une allocation valide.
Pour une condition utilisant des données non initialisées, Memcheck peut afficher « Conditional jump or move depends on uninitialised value(s) ». Cette orthographe appartient au message de l’outil. La condition signalée a utilisé la valeur ; --track-origins=yes aide à trouver l’origine de son stockage non initialisé.
Lire la catégorie de fuite
| Catégorie du rapport | Signification |
|---|---|
| Définitivement perdu | Aucun pointeur vers le bloc ne subsiste |
| Indirectement perdu | Le bloc n’est accessible qu’à travers un autre bloc perdu |
| Potentiellement perdu | Seul un pointeur intérieur a été trouvé |
| Encore accessible | Un pointeur subsiste à la fin, mais le bloc n’a pas été libéré |
Supposons qu’une fonction auxiliaire alloue dix entiers de type int de 4 octets dans un pointeur local et se termine sans libérer le bloc ni renvoyer son adresse. Son bloc de 40 octets est définitivement perdu. Un pointeur global conservant l’adresse de début rendrait au contraire un bloc non libéré encore accessible. Les totaux d’allocations et de libérations du bilan du tas décrivent des événements, notamment les redimensionnements ; ils ne remplacent pas le raisonnement sur la propriété.
Une exécution sans erreur peut afficher « All heap blocks were freed -- no leaks are possible » et « ERROR SUMMARY: 0 errors from 0 contexts ». Memcheck ne détecte généralement pas tous les dépassements de tableaux locaux ou globaux qui restent dans une mémoire par ailleurs valide. Corrige la première erreur signalée, relance le programme et teste d’autres données d’entrée, notamment une entrée vide et des chemins avec retour anticipé.
Points clés
- Utilise les informations de débogage pour que Memcheck puisse identifier les lignes de ton code source.
- Lis ensemble la taille de l’accès, les bornes du bloc et l’historique de l’allocation.
- Utilise
--track-origins=yespour retrouver l’origine des valeurs non initialisées. - Distingue les blocs perdus des blocs encore accessibles à la fin.
- Corrige la première erreur avant d’interpréter ses conséquences ultérieures.
- Utilise plusieurs données d’entrée, car un outil de vérification n’observe que les chemins exécutés.
5.2 Intégrer des vérifications au programme
AddressSanitizer, ou ASan, ajoute des vérifications des accès à la mémoire pendant la compilation. L’exécutable obtenu se vérifie lui-même pendant son exécution. Il est utile pour les tests réguliers de développement, car son exécution est généralement environ deux fois plus lente, un ralentissement bien inférieur à celui de Memcheck.
Compiler et effectuer l’édition de liens avec le sanitizer
cc -std=c17 -g -fsanitize=address -fno-omit-frame-pointer \
program.c -o program-asan
./program-asan
Utilise l’option du sanitizer à la compilation et à l’édition de liens finale lorsque ces étapes sont séparées. -g fournit les informations de ligne et -fno-omit-frame-pointer aide à conserver des piles d’appels utiles. Le compilateur de Microsoft utilise /fsanitize=address.
ASan détecte les débordements dans le tas, dans la pile et dans les tableaux globaux, les utilisations après libération, les libérations invalides et les doubles libérations. Les vérifications de durée de vie liée à la portée et, facultativement, des utilisations après retour de fonction peuvent aussi détecter les pointeurs vers d’anciennes variables locales. Il nécessite une recompilation et de la mémoire supplémentaire. Garde cette instrumentation dans les versions de développement et de test.
Lire les trois parties de l’historique
Un rapport d’utilisation après libération peut commencer par « ERROR: AddressSanitizer: heap-use-after-free ». La mention suivante « READ of size 4 » décrit l’opération invalide. Le rapport indique ensuite où la mémoire a été libérée avec « freed by thread T0 here: » et où elle a été allouée avec « previously allocated by thread T0 here: ».
Commence par la première ligne pertinente de ton code dans chaque pile d’appels. L’allocation crée le bloc ; la libération met fin à sa durée de vie ; la lecture signalée a lieu après cette fin. Décide s’il faut déplacer la libération après la lecture ou cesser d’accéder au bloc après sa libération. Le rapport fournit l’historique, tandis que la propriété prévue détermine la correction.
Pour « heap-buffer-overflow », une adresse « located 0 bytes after 20-byte region » commence juste après la région. Ne convertis les octets en éléments qu’après avoir vérifié la taille du type utilisé par le programme. ASan s’arrête normalement à la première erreur : la corriger peut donc en révéler une autre à l’exécution suivante.
La recherche de fuites et celle de valeurs non initialisées sont distinctes
LeakSanitizer recherche les allocations non libérées à la fin du programme. Il est normalement activé avec ASan sous Linux. Sur les chaînes d’outils macOS qui le prennent en charge, il peut être activé avec ASAN_OPTIONS=detect_leaks=1 ; certaines chaînes d’outils Mac ne le proposent pas. L’outil distinct leaks d’Apple est une autre solution pour rechercher les fuites sur ces systèmes.
ASan ne suit pas l’initialisation d’une valeur située dans les bornes. Pour ce problème, utilise Memcheck ou MemorySanitizer de Clang sur une configuration Linux prise en charge. UndefinedBehaviorSanitizer vérifie d’autres types de comportements indéfinis, comme le débordement d’entier signé, et peut être associé à ASan avec -fsanitize=address,undefined.
Prévoir une exécution distincte pour chaque outil
| Besoin | Outil utile |
|---|---|
| Vérifications rapides pendant le développement, notamment des bornes des tableaux locaux | Version compilée avec ASan |
| Suivi des valeurs non initialisées sans instrumentation par un sanitizer | Memcheck sur un système pris en charge |
| Fuites d’allocations à la fin du programme | LeakSanitizer ou Memcheck |
N’exécute pas un exécutable ASan sous Valgrind. Compile un exécutable de débogage ordinaire distinct pour Memcheck et n’associe pas ASan et MemorySanitizer dans une même compilation. Après une exécution sans erreur, varie les données d’entrée pour exécuter d’autres chemins d’allocation, d’agrandissement et de nettoyage.
Points clés
- ASan nécessite une compilation et une édition de liens avec son option d’instrumentation.
- Lis les piles d’appels de l’accès invalide, de la libération et de l’allocation pour reconstituer une erreur de durée de vie.
- LeakSanitizer recherche les fuites à la fin du programme lorsque la plateforme le prend en charge.
- ASan ne détecte pas les lectures ordinaires de valeurs non initialisées.
- Utilise des exécutions distinctes et des versions adaptées pour ASan et Memcheck.
5.3 Transformer un rapport en correction vérifiée
Un rapport de mémoire indique une erreur observée et une partie de son histoire. Une correction utile rétablit une taille, des bornes, une initialisation et une propriété correctes, puis vérifie à nouveau le programme corrigé.
Réduire et reproduire
Trouve les plus petites données d’entrée qui produisent encore l’erreur. Compile avec -g, les avertissements et peu d’optimisation, puis utilise Memcheck ou une version compilée avec ASan. Lis le premier rapport avant d’examiner les suivants ; un état corrompu peut produire une série d’échecs trompeurs.
Supposons qu’un programme contienne :
size_t n = 5;
int *v = malloc(n * sizeof *v);
for (size_t i = 0; i <= n; ++i) v[i] = (int)(i * i);
printf("%d\n", v[2]);
return 0;
Sur un système où les entiers de type int occupent 4 octets, ASan signale une écriture de quatre octets au premier octet situé après une allocation de 20 octets. L’écriture se trouve dans la boucle. Memcheck peut aussi signaler le débordement, puis une allocation non libérée ; le classement du bloc comme perdu ou encore accessible dépend des pointeurs qui subsistent à la fin.
Relier le rapport au code
Cinq entiers de type int occupent les indices de zéro à quatre. La condition i <= n atteint l’indice cinq. La taille de l’allocation est correcte, mais pas la borne de la boucle. Agrandir le bloc masquerait le symptôme tout en laissant le parcours incorrect.
Il manque aussi une vérification de l’allocation et le bloc dont le programme est responsable n’est pas libéré. Ces problèmes nécessitent leurs propres corrections, même si le premier rapport a arrêté le programme avant qu’il les atteigne.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *v = malloc(n * sizeof *v);
if (v == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < n; ++i) v[i] = (int)(i * i);
printf("%d\n", v[2]);
free(v);
return EXIT_SUCCESS;
}
Le programme corrigé initialise cinq éléments valides, affiche 4 pendant que le bloc existe et le libère. Ce petit nombre fixe est représentable ; pour un nombre issu des données d’entrée, il faut aussi vérifier le débordement du calcul de taille avant l’allocation.
Utiliser l’historique du rapport pour choisir la correction adaptée
Pour une utilisation après libération, compare les piles d’appels de la libération et de l’accès afin de trouver la dernière utilisation légitime. Déplace la libération après celle-ci ou supprime l’accès devenu invalide. Affecter NULL à un pointeur ne peut pas rendre à nouveau disponibles les données qu’il désignait.
Pour une fuite, la pile d’appels de l’allocation indique où le bloc a été créé. Elle ne désigne pas la bonne ligne de libération. Suis la propriété à travers les retours réussis et les échecs pour trouver où se termine la dernière utilisation. Pour une double libération, supprime la libération en double ou corrige les responsabilités contradictoires. Supprimer la seule libération valide crée une fuite.
Pour les données non initialisées, ajoute --track-origins=yes à Memcheck. La ligne qui utilise les données peut être éloignée de l’allocation qui a fourni les octets non initialisés. Initialise chaque valeur lue par le calcul ou utilise calloc lorsque zéro est la bonne valeur initiale.
Vérifier la correction sur plusieurs chemins
Recompile et relance le programme avec les données d’entrée qui reproduisaient initialement l’erreur. Continue jusqu’à obtenir un rapport sans erreur, puis essaie une entrée vide, les limites d’agrandissement, des données ordinaires et des retours d’erreur. Les avertissements du compilateur détectent certains problèmes avant l’exécution ; les outils d’exécution vérifient les chemins atteints par ces tests.
Points clés
- Reproduis l’erreur avec une petite donnée d’entrée et lis la première erreur.
- Utilise la taille du bloc et la position de l’accès pour identifier l’erreur de bornes réelle.
- Vérifie les échecs d’allocation et le nettoyage même lorsque la première erreur d’accès arrête l’exécution.
- Les rapports de fuite indiquent les points d’allocation ; la propriété détermine les points de libération.
- Après la correction, recompile, relance le cas de reproduction et parcours d’autres chemins.
Gestion dynamique de la mémoire en C : exemples
1Suivre un objet au fil des appels de fonctions
9 étapes
- 1IntroductionUne adresse nécessite un objet encore actif
Un pointeur permet à une partie du programme de retrouver des données créées ailleurs. Son utilité dépend du fait que ces données existent encore ou non lorsqu’il est utilisé.
Suis une fonction auxiliaire qui crée un tableau d’entiers, renvoie son adresse et laisse la fonction appelante utiliser le résultat. Pendant le suivi de l’appel, distingue le pointeur local du tableau : ils n’ont pas nécessairement la même durée de vie.
- 2ExplicationNommer les trois durées de vie
Une variable locale ordinaire a une durée de stockage automatique et cesse d’exister à la fin de son bloc. Une variable globale ou une variable locale
staticexiste pendant toute l’exécution du programme. Un bloc alloué existe jusqu’à sa libération.Sur les systèmes courants, les objets automatiques occupent la pile d’appels et les allocations utilisent le tas. Ces noms décrivent des implémentations courantes ; les règles du C définissent les durées de vie quelle que soit la disposition de la mémoire.
- 3ExempleCréer le pointeur local et le bloc
Dans
make_array,int *p = malloc(5 * sizeof *p);crée deux objets distincts : le pointeur local et, en cas de réussite, une allocation pour cinq entiers de type int.Là où un
intoccupe 4 octets, le bloc contient 20 octets. Vérifiepavant utilisation. La variable pointeur appartient à l’appel de fonction, mais le bloc alloué ne fait pas partie du stockage automatique de cet appel. - 4ExempleRenvoyer l’adresse
La fonction auxiliaire renvoie
pet la fonction appelante stocke le résultat dansint *nums. L’adresse est copiée ; le tableau ne l’est pas. Les variables locales de la fonction auxiliaire cessent alors d’exister.En cas de réussite,
numsdésigne toujours le tableau alloué. La fonction appelante vérifie le résultat, initialise et utilise ses éléments, puis le libère. Le retour n’a pas libéré le bloc, car sa durée de stockage est allouée. - 5DéfiQu’est-ce qui survit au retour ?
Une fonction auxiliaire crée une variable automatique
int countet un tableau distinct dans le tas. Elle renvoie l’adresse du tableau. Lequel de ces deux objets la fonction appelante peut-elle encore utiliser ?---reveal---
Le tableau du tas reste actif jusqu’à sa libération. La variable automatique
countde la fonction auxiliaire a cessé d’exister au retour. L’adresse copiée ne donne accès qu’au tableau ; conserver une adresse n’aurait pas préservécountsi la fonction auxiliaire avait renvoyé&countà la place. - 6ExempleEssayer de renvoyer un tableau local
Modifie maintenant la fonction auxiliaire pour qu’elle déclare
int values[5];et renvoievalues. Ce tableau est automatique. Il cesse d’exister au retour de la fonction auxiliaire, laissant la fonction appelante sans cible encore active.La taille reste de cinq entiers de type int, mais la durée de vie est différente. Pour obtenir un résultat utilisable, on peut le placer dans un tampon fourni par la fonction appelante ou l’allouer et le lui transférer en précisant clairement la responsabilité du nettoyage.
- 7LienChoisir les règles d’utilisation d’un tampon
Un tampon de résultat statique survit au retour, mais chaque appel le partage. Un appel ultérieur peut écraser un résultat précédent. Un résultat dans le tas peut donner à chaque appel son propre espace de stockage indépendant, dont la fonction appelante est responsable de la libération.
Pour un travail temporaire, un petit tableau local de taille fixe est plus simple. Pour les tableaux dont la taille vient des données d’entrée, les très grands tableaux ou les tableaux extensibles, une allocation vérifiée permet au programme de gérer les échecs et de contrôler la durée de vie.
- 8DéfiDeux résultats doivent rester indépendants
Une fonction appelante a besoin de deux résultats modifiables issus d’appels successifs et doit conserver les deux. Elle ne fournit aucun tampon. Un seul tableau de résultat statique répondrait-il à ce besoin ? Quelle solution convient ?
---reveal---
Un seul tableau statique serait partagé et écrasé. Renvoie un résultat alloué distinct à chaque appel, vérifie chaque résultat et laisse la fonction appelante libérer les deux après leurs dernières utilisations. La durée de vie dans le tas assure leur survie ; des blocs distincts assurent leur indépendance.
- 9RécapitulatifDistinguer l’adresse de la durée de vie
Suis l’objet visé par un pointeur : les variables locales automatiques cessent d’exister à la fin de leur bloc, les objets statiques existent pendant toute l’exécution et les blocs alloués doivent être libérés explicitement.
Pour un résultat renvoyé, demande-toi si le stockage survit et si les fonctions appelantes ont besoin de copies indépendantes. Pour un travail temporaire, demande-toi si un petit tableau local de taille fixe suffit. La taille et la durée de vie nécessaires déterminent la solution avant même l’écriture d’un appel d’allocation.
2Allouer une grille et nettoyer chaque chemin
13 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionPrévoir la réussite et l’échec
- 2ExplicationConvertir les nombres d’éléments en octets
- 3ConseilVérifier avant de multiplier
- 4DéfiTaille de l’élément ou du pointeur ?
- 5ExplicationDonner une valeur initiale aux compteurs
- 6ExempleUtiliser la ligne mise à zéro
- 7DéfiEt en cas d’agrandissement ultérieur ?
- 8ExplicationCompter les blocs, pas les noms de pointeurs
- 9ExempleArrêter la construction en cas d’échec partiel
- 10DéfiChoisir l’ordre du nettoyage
- 11Point cléUn responsable peut avoir des emprunteurs
- 12DéfiUn chemin s’arrête prématurément
- 13RécapitulatifPrévoir le nettoyage avec la construction
3Ajouter des valeurs sans perdre de données
12 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionAgrandir avant le stockage suivant
- 2ExplicationUn redimensionnement peut remplacer l’objet
- 3ExempleDistinguer l’échec de la réussite
- 4DéfiQue peut-on conserver après une réduction ?
- 5ExplicationLa longueur et la capacité répondent à des questions différentes
- 6ExempleFranchir la première limite d’agrandissement
- 7ExempleFranchir la limite suivante
- 8Point cléL’espace libre réduit le nombre de copies
- 9DéfiQuel état reste après un ajout qui échoue ?
- 10LienUne chaîne réserve un emplacement pour son caractère de fin
- 11DéfiUne sélection empruntée lors d’un agrandissement
- 12RécapitulatifNe valider l’état qu’après une réussite
4Examiner toute l’histoire d’un bloc
15 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionQuatre questions pour chaque accès
- 2ExemplePerdre une allocation en remplaçant son adresse
- 3DéfiRendre le pointeur nul effectue-t-il le nettoyage ?
- 4ConseilExaminer les sorties autant que les boucles
- 5ExempleLibérer une fois rend tous les alias invalides
- 6DéfiCorriger la dernière utilisation
- 7Point cléUn code invalide peut produire le résultat attendu
- 8ExempleCompter le caractère nul de fin de chaîne
- 9ExempleAlloué ne signifie pas initialisé
- 10DéfiDistinguer deux bornes
- 11ExplicationUne erreur est un symptôme
- 12ExempleExaminer l’accès qui provoque le plantage
- 13DéfiUn affichage fait disparaître le plantage
- 14DéfiCombiner propriété et bornes
- 15RécapitulatifIdentifier la règle enfreinte
5Lire un rapport et vérifier la correction
14 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionTransformer un message en historique de bloc
- 2ExplicationUtiliser une version de débogage ordinaire pour Memcheck
- 3ExempleInterpréter la description de l’adresse
- 4DéfiLire les octets comme des octets
- 5ConseilChoisir une option de rapport adaptée au problème
- 6ExplicationCompiler séparément un exécutable ASan
- 7ExempleSuivre une utilisation après libération dans trois piles d’appels
- 8DéfiUne exécution ASan sans erreur manque une erreur de valeur
- 9ExempleRelier le rapport à la boucle
- 10ExempleMaintenir le bloc actif pendant son utilisation
- 11DéfiOù faut-il corriger une fuite ?
- 12LienVérifier la fin du programme et la prise en charge par la plateforme
- 13DéfiTerminer le cycle de correction
- 14RécapitulatifUtiliser les indices, corriger et relancer
Gestion dynamique de la mémoire en C : QCM
Série 1 : Durées de vie, nombres d’octets et premières vérifications10 Questions Pratiques
Question 1 sur 10
Afficher la réponseMasquer la réponse
Question 2 sur 10
Afficher la réponseMasquer la réponse
Question 3 sur 10
sizeof(int) vaut 4. Combien d’octets l’allocation doit-elle demander ?Afficher la réponseMasquer la réponse
Question 4 sur 10
int *hits = calloc(6, sizeof *hits);, que vaut hits[4] avant toute affectation ultérieure ?Afficher la réponseMasquer la réponse
calloc met chaque octet à zéro, ce qui donne la valeur zéro aux éléments entiers. L’indice 4 se trouve dans le tableau de six éléments : sa valeur peut donc être lue comme zéro après vérification de l’allocation. Un espace qui vient d’être alloué par malloc ne serait pas initialisé ; cette règle ne s’applique pas à cet appel réussi à calloc.Question 5 sur 10
Afficher la réponseMasquer la réponse
Question 6 sur 10
int *tmp = realloc(data, bytes); et renvoie NULL. Le bloc d’origine était valide. Que peut faire maintenant le responsable du bloc ?Afficher la réponseMasquer la réponse
realloc de taille positive qui échoue laisse l’allocation d’origine et son contenu inchangés. Comme le résultat a été reçu dans tmp, data contient toujours son adresse valide. Libérer le pointeur temporaire nul ne fait rien ; cela ne peut pas libérer le bloc encore détenu que désigne data.Question 7 sur 10
Afficher la réponseMasquer la réponse
Question 8 sur 10
int *view = owner; free(owner); owner = NULL;. Que se passe-t-il s’il lit ensuite view[0] ?Afficher la réponseMasquer la réponse
view était un alias vers la même allocation, dont la durée de vie a pris fin avec free(owner). Affecter NULL à owner ne met à jour que cette variable pointeur. Cela ne modifie pas view et ne rétablit pas l’ancien bloc : la lecture ultérieure est donc une utilisation après libération qui provoque un comportement indéfini.Question 9 sur 10
strcpy dans un tampon nouvellement alloué dans le tas. Quelle est la plus petite allocation nécessaire en octets C ?Afficher la réponseMasquer la réponse
strcpy stocke aussi le caractère nul de fin de chaîne. Le plus petit tampon comporte donc huit octets. Sept octets contiendraient les lettres visibles, mais laisseraient le caractère de fin hors de l’allocation, provoquant une écriture hors limites même si l’affichage semblait fonctionner.Question 10 sur 10
Une variable globale int *saved vaut initialement NULL. Une fonction auxiliaire exécute ce code puis se termine. Les entiers de type int occupent 4 octets. Memcheck signale une écriture invalide immédiatement après l’allocation. Quelle erreur de parcours et quelle catégorie de fuite à la fin en résultent ?
int *p = malloc(7 * sizeof *p);
if (p == NULL) return;
for (size_t i = 0; i <= 7; ++i) p[i] = (int)i;
saved = p;
Afficher la réponseMasquer la réponse
Série 2 : Suivre les chemins d’allocation et de nettoyage10 Questions Pratiques
Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.
Question 1 sur 10
p. La fonction appelante stocke le résultat dans values. Qu’est-ce qui reste actif immédiatement après le retour de la fonction auxiliaire ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
n, quelle condition détecte avant l’allocation que n * sizeof *p n’est pas représentable dans size_t ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
a, le vérifie, puis tente d’allouer b. La seconde allocation échoue. La fonction est responsable de a et va immédiatement renvoyer un échec. Quel nettoyage faut-il effectuer ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
static int visits = 0; et exécute return ++visits;. Quelle valeur son troisième appel renvoie-t-il ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
free après une boucle qui remplit un tableau du tas. Que faut-il examiner avant de modifier l’appel de libération ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
Un programme commence par ce bloc vérifié, puis choisit une branche selon p[5]. Un redimensionnement de taille positive a réussi. ASan ne signale rien, tandis que Memcheck signale une valeur non initialisée au niveau de la branche. Quelle correction correspond à tout cet historique ?
int *p = calloc(4, sizeof *p);
if (p == NULL) return EXIT_FAILURE;
int *tmp = realloc(p, 6 * sizeof *p);
if (tmp == NULL) { free(p); return EXIT_FAILURE; }
p = tmp;
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Série 3 : Croissance, durées de vie et indices des rapports10 Questions Pratiques
Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.
Question 1 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
int *p = malloc(11 * sizeof p);. Combien d’octets demande-t-il et quel est le problème de l’expression ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
calloc pour un tampon de caractères de taille positive, quelle chaîne C contient-il initialement ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
p. Le code enregistre l’indice 4 et un pointeur vers cet élément. Une tentative d’agrandissement à neuf entiers échoue avec un pointeur temporaire. Une réduction ultérieure à trois réussit et le pointeur responsable est mis à jour. La sélection peut-elle encore être utilisée ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
old, exécute cap = 16, puis data = realloc(data, cap * sizeof *data). Cette demande de taille positive échoue avant qu’une autre valeur soit stockée. Quel état rétablit le tableau inchangé ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
buffer. Elle exécute buffer = NULL; free(buffer); après sa dernière utilisation. Qu’arrive-t-il au bloc d’origine ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
p, puis appelle plus tard free(alias), où alias a été copié depuis p avant la libération. De quelle erreur s’agit-il ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
stdout sans saut de ligne, puis le programme plante. Son texte n’apparaît jamais. Pourquoi ce fait ne permet-il pas de déterminer avec certitude jusqu’où l’exécution est allée ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
program-asan avec AddressSanitizer et souhaite effectuer une exécution indépendante avec Memcheck. Quel exécutable doit-elle fournir à Memcheck ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
items[n] = 0;, juste après une allocation pour n entiers de type int. La boucle utilise i <= n, le résultat n’est pas vérifié et le nettoyage est absent. Quelle correction traite la cause et les défauts restants de propriété ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Série 4 : Corriger la propriété lorsque les conditions changent10 Questions Pratiques
Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.
Question 1 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
calloc, puis a agrandi le bloc avec succès à sept compteurs. Que faut-il faire avant d’incrémenter le compteur 6 pour la première fois ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
p. Une tentative de redimensionnement à sept entiers échoue avec tmp et laisse p inchangé. Le code exécute ensuite p = realloc(p, 9 * sizeof *p), et cette demande échoue aussi. Il n’existe aucun alias. Quelle allocation est maintenant perdue ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
char **labels = malloc(4 * sizeof *labels);, vérifie le résultat, puis exécute immédiatement strcpy(labels[0], "east");. Quelle solution corrige la première entrée tout en précisant qui est responsable de sa libération ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Série 5 : Suivre des historiques de mémoire complets10 Questions Pratiques
Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.
Question 1 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
double *samples ordinaire, que mesure sizeof samples ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
X avec succès sans agrandissement et met à jour son caractère de fin. Le caractère suivant Y déclenche un agrandissement avec un pointeur temporaire, mais la demande échoue. L’API promet de préserver la chaîne en cas d’échec. Quel état subsiste ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
a et 40 octets dans la variable locale b. Sur le chemin d’erreur, elle libère a, exécute b = NULL et revient. Aucun alias ne subsiste. Quelle est la taille totale des allocations perdues et quelle modification l’évite ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
int *p = malloc(6 * sizeof p);, vérifie la réussite, initialise les éléments d’indices 0 à 5, puis additionne ceux d’indices 0 à 11. Quel diagnostic convient à toute la somme ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
p[index]. Que faut-il vérifier au sujet de cet accès ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
display, une libération antérieure dans reset et une allocation dans load. La fonction appelante invoque load, reset, puis display sur le même résultat. Elle a besoin de cet affichage. Quelle correction découle de l’histoire du bloc ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Tu bloques sur ton propre exercice ? Aide aux Devoirs
Mots-Clés
A
AddressSanitizer
alias
NULL au pointeur responsable après la libération laisse tous les alias enregistrés inutilisables pour des lectures ultérieures.allocation
allocation imbriquée
allocation perdue
NULL sans appeler d’abord free fait perdre l’adresse de l’allocation.B
bilan du tas
C
calloc
calloc réserve un tableau à partir d’un nombre et d’une taille d’élément, met ses octets à zéro et renvoie un pointeur nul si l’allocation échoue.calloc vérifiée commencent à zéro avant leur première incrémentation.capacité
caractère nul de fin de chaîne
chemin de nettoyage
comportement indéfini
croissance géométrique
D
débordement de pile
débordement de tampon dans le tas
débordement du calcul de taille
size_t et reboucle vers une valeur représentable plus petite.SIZE_MAX / sizeof *p avant de le multiplier par la taille d’un élément.définitivement perdu
donnée d’entrée reproduisant l’erreur
double libération
free sur ce qui était une seule allocation active partagée.durée de stockage
durée de vie d’un objet
realloc met fin à la durée de vie de l’ancien objet même si l’adresse renvoyée semble inchangée.E
encore accessible
erreur de décalage d’une unité
i <= n par i < n permet à un parcours commençant à zéro de visiter exactement les n éléments prévus.erreur de segmentation
exécution de vérification
F
free
free libère un bloc alloué encore actif pour qu’il puisse être réutilisé lorsqu’elle reçoit son adresse de début, et ne fait rien lorsqu’elle reçoit NULL.fuite de mémoire
I
indirectement perdu
initialisation
instrumentation
-fsanitize=address à la compilation et à l’édition de liens crée un exécutable instrumenté par ASan.L
LeakSanitizer
lecture d’une valeur non initialisée
littéral de chaîne de caractères
char word[] = "hello"; lorsqu’une copie locale modifiable est nécessaire plutôt qu’un pointeur vers le littéral.longueur
M
malloc
malloc réserve un nombre demandé d’octets et renvoie l’adresse d’un bloc ou un pointeur nul, sans initialiser les valeurs stockées.malloc nécessite des affectations avant que ses éléments soient additionnés ou comparés.Memcheck
mémoire mise à zéro
calloc commence par un caractère nul et contient une chaîne vide.MemorySanitizer
mise en tampon des sorties
fflush(stdout) après un message de progression rend son texte placé en tampon disponible avant l’opération suivante.P
pile d’appels
point d’allocation
pointeur emprunté
pointeur intérieur
pointeur nul
pointeur pendant
pointeur temporaire
tmp = realloc(p, bytes), un résultat nul laisse p disponible pour continuer à l’utiliser ou le nettoyer.potentiellement perdu
première erreur signalée
propriété
R
realloc
realloc remplace un bloc alloué par un objet de la taille demandée, en conservant la partie initiale qui y tient et en pouvant changer son adresse.réutilisation de tampon
S
size_t
size_t est le type entier non signé utilisé pour les tailles d’objets et les nombres d’octets à allouer, notamment pour le résultat de sizeof.size_t pour sa longueur et sa capacité afin qu’elles puissent décrire des tailles d’objets.sizeof
sizeof donne la taille d’un type ou d’un objet en octets C ; appliqué à un pointeur, il mesure le pointeur et non l’allocation qu’il vise.n * sizeof *p suit le type de l’élément, tandis que sizeof p ne permet pas de retrouver la longueur du tableau.stockage automatique
stockage statique
static utilisent.suivi de l’origine
--track-origins=yes de Memcheck peut expliquer pourquoi une branche ultérieure dépend de données du tas non initialisées.T
tableau de longueur variable
int a[n] ne fournit pas le résultat nul permettant de vérifier l’échec d’une allocation dans le tas.tableau dynamique
tampon fourni par la fonction appelante
tampon statique partagé
tas
trace de pile
U
UndefinedBehaviorSanitizer
-fsanitize=address,undefined pour activer les deux ensembles de vérifications compatibles.utilisation après libération
À propos de cette unité
Crée ton propre Matériel d'Étude
Transforme tes notes, diapos et documents en Guides d'Étude, Questions Pratiques, Flashcards et 6 autres outils.