retour




Gestion de la mémoire en C : Les fonctions de base

Notice

Le présent document est publié sous la licence « Verbatim Copying » telle que définie par la Free Software Fondation dans le cadre du projet GNU.


Toute copie ou diffusion de ce document dans son intégralité est permise (et même encouragée), quel qu'en soit le support à la seule condition que cette notice, inclant les copyrights, soit préservée.


Copyright (C) 2002, 2003, 2004 Yann LANGLAIS, ilay.


La plus récente version en date de ce document est sise à l'url http://ilay.org/yann/articles/mem/.


Toute remarque, suggestion ou correction est bienvenue et peut être adressée à Yann LANGLAIS, ilay.org.

Introduction

Le langage C est souvent considéré comme un langage difficile et peu fiable. La seconde assertion n'est pas acceptable dans la mesure où la plupart des applications, y compris les L4G et RAD sont eux mêmes écrits en C.

En fait, ces idées reçues proviennent principalement de l'utilisation intensive des pointeurs. Si le C est souvent mal considéré, c'est bien souvent à cause de la méconnaissance du concept de pointeur et de la gestion de la mémoire qui lui est associé.

Le présent document se propose de présenter ces concepts depuis leurs fondements théoriques jusqu'à leur application pratique.

Il ne s'agit pas pour autant de définir les différentes notions avec la plus grande rigueur qui soit. Mon propos n'est pas de faire de l'initiation à l'assembleur, ni à l'électronique. Mon but est de présenter la gestion de la mémoire sous un angle différent de celui généralement adopté.

L'audience visée est principalement celle des développeurs ayant déjà été initiés au langage C.


Comment gérer la mémoire?

Allocation sur la pile

Lorsque l'on a affaire à des constantes, ou des variables bien définies et fixées en terme de taille, et a usage local seulement, l'allocation sur la pile est appropriée.

Allocation dynamique

Lorsque les variables ont des tailles non fixées ou encore ne sont pas à usage local, il est sûr d'utiliser l'allocation dynamique.


Manipulation dynamique de la mémoire

Allocation de la mémoire: malloc()

Prototype:

    #include <stdlib.h>
	void *malloc(size_t size);

Comportement:

Malloc prend en argument la taille de la zone mémoire désirée et retourne le pointeur sur la zone allouée ou 0 (pointeur nul) s'il n'y a pas assez d'espace mémoire contigu.


Une zone mémoire fraichement allouée par malloc n'est a priori pas initialisée et peut contenir n'importe quoi (données aléatoires provenant d'anciennes applications ayant utilisé le même espace, ou données aléatoires).


Il est donc nécessaire de l'initialiser avec des valeurs appropriées (par exemple 0) en fonction du type de donées à inserer dans ces zones allouées et / ou en fonction des règles de programmation utilisées. On pourra initialiser l'ensemble de la zone avec la fonction memset(), recopier des zones existantes avec memcpy(), ou encore initialiser les blocs en fonction de leur découpage (types, structures, tableaux, ...).

A retenir:
  • Il est impératif de toujours vérifier que le pointeur alloué est valide.
  • Plus généralement, il est toujours nécessaire de tester un pointeur passé en argument.
  • Il ne faut jamais préjuger du contenu d'une zone mémoire après son allocation par malloc
  • Il est nécessaire d'initialiser convenablement les zones de mémoire allouées


la fonction calloc()

Prototype :
    #include <stdlib.h>
    void *calloc(size_t nmemb, size_t size);
Comportement :

A la différence de malloc(), calloc() alloue un tableau de nmemb élements ayant chacun size pour taille. La mémoire allouée est initialisée à 0.


Pour le reste, calloc() fonctionne comme malloc(): si la mémoire a bien été réservée et initialisée, calloc() retourne le pointeur sur la zone. Sinon, calloc() returne 0 (pointeur nul).


Désallocation de la mémoire: free()

Prototype :

    #include <stdlib.h>
    void free(void *ptr);

Comportement :

La fonction free() annule la réservation d'une zone de mémoire pointée par ptr, allouée par malloc() ou calloc() ou par toute autre fonction réservant de la mémoire, comme strdup() qui duplique une chaine de caractères.


Si l'on tente de libérer un espace mémoire qui n'a pas été allouée par malloc() ou calloc(), directement ou par le biais d'une fonction appelant malloc() ou calloc(), la fonction free() enverra un signal 11 (SIGSEGV « Invalid memory reference ») qui terminera brutalement le programme.

La fonction free() n'écrase pas la mémoire de la zone.

Il est fortement recommandé d'annuler le pointeur qui vient d'être libéré par free(), afin de ne pas tenter d'adresser un espace qui pourra être réutilisé. On pourra par exemple réécrire une fonction:

    void *my_free(void *ptr) {
        if (ptr) free(ptr);
        return NULL;
    }

et l'utiliser de la manière suivante:

    ptr = my_free(ptr); 

ou encore écrire la macro qui s'utilise de la même manière que free():

    #define FREE(x) { if (x) free(x); x = NULL; }

Réallocation de la mémoire : realloc()

Prototype :

    #include <stdlib.h>
    void *realloc(void *ptr, size_t size);

Comportement :

La fonction realloc() prend en argument le pointeur sur la zone de mémoire (ptr) dont il faut modifier la taille, et la nouvelle taille désirée (size). Si l'opération s'est bien passée, realloc() returne le pointeur sur la zone mémoire.


Attention:
il est fort possible que le nouveau pointeur soit différent de l'ancien! En effet, si la zone mémoire doit être augmentée, et que la zone en cours n'est pas assez grande pour supporter la nouvelle taille, realloc() allouera une nouvelle zone de mémoire contigue et recopiera complètement l'ancienne zone.


Il est donc impératif de ne pas adresser directement par un pointeur un espace mémoire dont la taille doit/peut être modifiée par realloc:


Les zones en rouge sont des zones de mémoire occupées. La zone jaune est la zone de mémoire que nous avons réservé et modifié par realloc. Si nous conservons un pointeur sur la zone avant le réalloc, ce pointeur ne sera plus utilisable après car in adressera une zone non réservée et contenant des données aléatoires.


Si l'opération échoue, realloc() retourne 0, mais ne déasalloue pas la zone d'origine. Il est impératif de ne pas réassigner le même pointeur immédiatement avec le résultat de realloc() au risque de perdre toutes les données en mémoire et de ne jamais pouvoir les libérer.


Ce qu'il ne faut pas faire:

    ptr = realloc(ptr, newsize);

Ce qu'il faut faire:

    {
        void *p; 
        if ((p = realloc(ptr, newsize))) {
            /* la réallocation s'est bien passée , l'affectation est sûre: */
            ptr = p;
        } else {
            /* la réallocation a échoué, le processus ne peut pas 
               continuer mais la mémoire est encore utilisée: */
            free (ptr);
        }
        return NULL;
    }  

Fonctions annexes: memset(), memcpy(), memmove()

Initialisation d'une zone memoire: memset()

Prototype:
    #include <string.h>
    void *memset(void *s, int c, size_t n);

Comportement:

memset() prend en argument le pointeur sur la zone allouée (s) à initialiser, la valeur à affecter à toute la zone mémoire (c), et le nombre d'octets devant subir le traitement (n).


Il est à noter que seul le premier octet de la valeur d'initialisation est pris en compte.


memset retourne le pointeur d'entrée (s).


Copie de zone mémoire : memcpy()

Prototype:
    #include <string.h>
    void *memcpy(void *dest, const void *src, size_t n);
Comportement:

memcpy prend en entrée le pointeur zone de mémoire allouée de destination, le poinrteur sur la zone de mémoire cible, et le nombre d'octets devant être copiés.


memcpy retourne le pointeur de destination (dest).

Attention:


Déplacement de zone mémoire: memmove()

Prototype:
    #include <string.h>
    void *memmove(void *dest, const void *src, size_t n);
Comportement:

La fonction memmove() déplace le contenu d'une zone mémoire vers une autre. Elle prend en argument le pointeur vers la zone mémoire allouée vers laquelle déplacer le contenu mémoire (dest), le pointeur vers la zone de départ de la copie (src) et le nombre d'octets à déplacer. Elle retourne le pointeur vers la zone de destination (dest).


Contrairement à memcpy(), il est possible que les 2 zones se supperposent.



Gestion des chaines de caractères:

La manipulation de chaines de caractères est une source de nombreux problèmes dont sont victime autant les programmeurs débutants que les chevronnés. Ces quelques lignes permettrons peut être à certains d'éviter quelques écueils.


Rappels

Une chaine de caractères est un tableau d'octets terminé par un octet nul (ou caractère nul = 0 = '\0').


Si l'on veut réserver la mémoire pour la chaine « bonjour, monde », il est nécessaire de penser que cette chaine doit en fait s'écrire « bonjour, monde\0 » et réserver l'espace en conséquence.


La fonction strlen() retourne la taille d'une chaine de caractère sans son terminateur.


L'indexe d'un tableau commence toujours par 0 et non par 1. Aussi, lorsque l'on réserve 50 chars pour une chaine, le caractère nul devra se trouver au plus à l'index 49.


Certaines fonctions nécessitent une attention toute particulière:

strcpy()

Prototype:
    #include <string.h>
    char *strcpy(char *dest, const char *src);

Comportement:

La fonction strcpy recopie le contenu de la chaine src vers la chaine allouée pointée par dest et retourne le pointeur vers la chaine de destination (le caractère nul est inclus dans la copie).


Mais attention: il est nécessaire au préalable de s'être assuré que la taille réservé dans dest est supérieure ou égale à la taille de la chaine pointée par src.

Si ce n'est pas le cas, nous risquons d'avoir un écrasement d'une zone mémoire.

Cet écrasement peut modifier des données stockées sur la pile, ou pire, écraser une partie du programme lui même puisque la définition de la pile précède le code dans la mémoire.

Ce défaut est la base de la technique de piratage la plus répendue: le pirate fournit au programme une chaine assez longue pour empiéter sur le code de la fonction et contient un autre code au format binaire qui sera exécuté au lieu du code de la fonction (voir Linux Mag # ...)


strncpy()

Prototype:
    #include <string.h>
    char *strncpy(char *dest, const char *src, size_t n);
Comportement:

strncpy() prend les mêmes arguments que strcpy() plus n, le nombre maximal d'octets à recopier.


Mais, là encore, attention car strncpy() recopie au plus n caractères sans pour autant tester que le dernier caractère copié est bien NULL. Il est donc nécessaire de s'assurer après un strncpy() que le dernier caractère est bien nul:

    char* mystrncpy(char *dest, char *src, size_t n) {
		if (!dest) return 0;
        if (!src || n <= 1) return *dest = 0;
        strncpy(dest, src, n);
        dest[n - 1] = 0;
        return dest;
    }


strcat()

Prototype:
    #include <string.h>
    char *strcat(char *dest, const char *src);
Comportement:

strcat ajoute à la chaine dest la chaine src. Il est nécessaire d'être sûr d'avoir réservé assez de place dans la chaine dest pour recevoir la chaine src en plus de son contenu. De même que pour strncpy, il existe une variation permettant de limiter la longueur de la chaine globale:

    char *strncat(char *dest, const char *src, size_t n)

strncat() ne recopie que les n premiers octets de src à la fin de dest et ajoute 0 à la fin, 0 n'étant pas pris en compte dans n.


Pour ne pas faire de débordement mémoire, il faut donc que n soit égal à la taille réservée de dest, diminué de strlen(dest) et encore diminué de 1 pour le caractère nul final.



sprintf() et snprintf()

Prototype
    #include <stdio.h>
    int sprintf(char *str, const char *format, ...);
Comportement

Cette fonction « imprime » remplit une chaine de caractères allouée (str) à partir d'un format (format) et, eventuellement, des données (...) et retourne le nombre de caractères de la nouvelle chaine (caractère nul final non compris). Une fois encore, aucun test n'est effectué sur la taille maximale de la chaine de caractères de destination. Pour cette fonction aussi, il existe une « parade »: la fonction snprintf().

    int snprintf(char *str, size_t size, const  char  *format, ...);

snprintf() tronque la nouvelle chaine « str » à size caractères y compris le caractère nul final. Dans le cas où la chaine str est inférieure à size, la fonction retourne la taille de str. Sinon, la taille de str est size - 1 et la fonction retourne -1.


Il existe d'autres groupes de fonctions avec des propriétés similaires, telles que strcmp et strncmp, strdup et strndup, vsprintf et vsnprintf (les équivalent de sprintf et snprintf mais dont le le dernier argument est une va_list (voir man vsprintf et man stdarg).


Règles de base