• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

Ce cours existe en livre papier.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 14/02/2024

Créez des variables grâce à l'allocation dynamique

Quand on déclare une variable, on dit qu'on demande à allouer de la mémoire :

int monNombre = 0;

Lorsque le programme arrive à une ligne comme celle-ci, il se passe en fait les choses suivantes :

  1. Votre programme demande au système d'exploitation (Windows, Linux, Mac OS…) la permission d'utiliser un peu de mémoire.

  2. Le système d'exploitation répond à votre programme en lui indiquant où il peut stocker cette variable (il lui donne l'adresse qu'il lui a réservée).

  3. Lorsque la fonction est terminée, la variable est automatiquement supprimée de la mémoire. Votre programme dit au système d'exploitation : "Je n'ai plus besoin de l'espace en mémoire que tu m'avais réservé à telle adresse".

Trouvez la taille d'une variable en fonction de son type

Selon le type de variable que vous demandez de créer, vous avez besoin de plus ou moins de mémoire. Le problème, c'est que l'espace pris en mémoire dépend des machines : peut-être que chez vous un int occupe 8 octets, qui sait ?

Pour connaître la taille d'un int  , on écrit :

sizeof(int)

À la compilation, cela sera remplacé par un nombre : le nombre d'octets que prend int en mémoire. Testez pour voir, en affichant la valeur à l'aide d'un printf  , par exemple :

printf("char : %d octets\n", sizeof(char));
printf("int : %d octets\n", sizeof(int));
printf("long : %d octets\n", sizeof(long));
printf("double : %d octets\n", sizeof(double));

Peut-on afficher la taille d'un type personnalisé qu'on a créé (une structure) ?

Oui ! sizeof marche aussi sur les structures :

typedef struct Coordonnees Coordonnees;
struct Coordonnees
{
    int x;
    int y;
};

int main(int argc, char *argv[])
{
    printf("Coordonnees : %d octets\n", sizeof(Coordonnees));

    return 0;
}
Coordonnees : 8 octets

Revenons à nos moutons : si on déclare une variable de type int  :

int nombre = 18;

… et que sizeof(int) indique 4 octets sur notre ordinateur, alors la variable occupera 4 octets en mémoire !

Supposons que la variable nombre soit allouée à l'adresse 1600 en mémoire. On aurait alors :

Un int occupant 4 octets en mémoire
Un int occupant 4 octets en mémoire

Notre variable nombre de type int qui vaut 18 occupe 4 octets dans la mémoire. Elle commence à l'adresse 1600 et termine à l'adresse 1603. La prochaine variable ne pourra donc être stockée qu'à partir de l'adresse 1604 !

Si on avait fait la même chose avec un char  , on n'aurait occupé qu'un seul octet en mémoire :

Un char occupant 1 octet en mémoire
Un char occupant 1 octet en mémoire

Imaginez maintenant un tableau de int  ! Chaque case du tableau occupera 4 octets.

Si notre tableau fait 100 cases :

int tableau[100];

… on occupera alors en réalité 4 * 100 = 400 octets en mémoire.

Même si le tableau est vide, il prend 400 octets ?

Bien sûr ! La place en mémoire est réservée : aucun autre programme n'a le droit d'y toucher (à part le vôtre). Une fois qu'une variable est déclarée, elle prend immédiatement de la place en mémoire.

Notez que si on crée un tableau de type Coordonnees  :

Coordonnees tableau[100];

… on utilisera cette fois : 8 * 100 = 800 octets en mémoire.

Il est important de bien comprendre ces petits calculs pour la suite du chapitre.

Allouez manuellement de la mémoire au système

Commencez par inclure la bibliothèque <stdlib.h>  .  Elle contient deux fonctions dont nous allons avoir besoin :

  1. malloc  (pour "Memory Allocation" ou allocation de mémoire, en français) : elle demande au système d'exploitation la permission d'utiliser de la mémoire.

  2. free  (libérer, en français) : elle indique au système que l'on n'a plus besoin de la mémoire qu'on avait demandée. La place en mémoire est libérée, un autre programme peut maintenant s'en servir au besoin.

Pour faire une allocation manuelle de mémoire, vous devez toujours suivre ces trois étapes :

  1. Appeler malloc pour demander de la mémoire.

  2. Vérifier la valeur retournée par malloc pour savoir si le système a bien réussi à allouer la mémoire.

  3. Libérer la mémoire avec free une fois qu'on a fini d'utiliser la mémoire. Si on ne le fait pas, on s'expose à des fuites de mémoire, c'est-à-dire que votre programme risque de prendre beaucoup de mémoire alors qu'il n'a en réalité plus besoin de tout cet espace.

Étape 1 : Demandez une allocation de mémoire avec malloc

Voici le prototype de la fonction malloc :

void* malloc(size_t nombreOctetsNecessaires);

La fonction prend en paramètre le nombre d'octets à réserver. Il suffit donc d'écrire sizeof(int)  dans ce paramètre pour réserver suffisamment d'espace pour stocker un int  .

Mais regardez ce que la fonction renvoie : un void*  !

Dans le chapitre sur les fonctions, je vous avais dit que void signifiait "vide", et qu'on utilisait ce type pour indiquer que la fonction ne retournait aucune valeur. Alors ici, on aurait une fonction qui retourne un "pointeur sur vide" ?

En fait, cette fonction renvoie un pointeur indiquant l'adresse que le système a réservée pour votre variable. Si le système a trouvé de la place pour vous à l'adresse 1600, la fonction renvoie donc un pointeur contenant l'adresse 1600.

Le problème, c'est que la fonction malloc ne sait pas quel type de variable vous cherchez à créer puisque vous ne lui donnez qu'un paramètre : le nombre d'octets en mémoire dont vous avez besoin. Si vous demandez 4 octets, ça pourrait aussi bien être un int qu'un long   , par exemple.

Passons à la pratique : si je veux créer manuellement une variable de type int en mémoire, je devrai indiquer à malloc que j'ai besoin de sizeof(int) octets en mémoire. Je récupère le résultat du malloc dans un pointeur sur int  :

int* memoireAllouee = NULL; // On crée un pointeur sur int

memoireAllouee = malloc(sizeof(int)); // La fonction malloc inscrit dans notre pointeur l'adresse qui a été reservée.

memoireAllouee est un pointeur contenant une adresse qui vous a été réservée par le système, par exemple l'adresse 1600 pour reprendre mes schémas précédents.

Étape 2 : Testez le pointeur pour vérifier la valeur retournée par malloc

La fonction malloc a donc renvoyé dans notre pointeur memoireAllouee l'adresse qui a été réservée pour vous en mémoire. Deux possibilités :

  1. Si l'allocation a marché, notre pointeur contient une adresse.

  2. Si l'allocation a échoué, notre pointeur contient l'adresseNULL  .

Il est peu probable qu'une allocation échoue, mais ça peut arriver : si vous demandez à utiliser 34 Go de mémoire vive, il y a peu de chances que le système vous réponde favorablement.

Il est néanmoins recommandé de toujours tester si l'allocation a marché.

Si l'allocation a échoué, c'est qu'il n'y avait plus de mémoire de libre (c'est un cas critique). Dans un tel cas, le mieux est d'arrêter immédiatement le programme parce que, de toute manière, il ne pourra pas continuer convenablement.

int main(int argc, char *argv[])
{
    int* memoireAllouee = NULL;

    memoireAllouee = malloc(sizeof(int));
    if (memoireAllouee == NULL) // Si l'allocation a échoué
    {
        exit(0); // On arrête immédiatement le programme
    }

    // On peut continuer le programme normalement sinon

    return 0;
}

Si le pointeur est différent de NULL  , le programme peut continuer, sinon il faut afficher un message d'erreur, ou même mettre fin au programme.

Étape 3 : Libérez de la mémoire avec free

void free(void* pointeur);

La fonction free a juste besoin de l'adresse mémoire à libérer. On va donc lui envoyer notre pointeur : memoireAllouee dans notre exemple :

int main(int argc, char *argv[])
{
    int* memoireAllouee = NULL;

    memoireAllouee = malloc(sizeof(int));
    if (memoireAllouee == NULL) // On vérifie si la mémoire a été allouée
    {
        exit(0); // Erreur : on arrête tout !
    }

    // On peut utiliser ici la mémoire
    free(memoireAllouee); // On n'a plus besoin de la mémoire, on la libère

    return 0;
}

Analysez un exemple concret d'utilisation

On va programmer quelque chose qu'on a appris à faire il y a longtemps : demander l'âge de l'utilisateur et le lui afficher. La seule différence avec ce qu'on faisait avant, c'est qu'ici la variable va être allouée manuellement :

int main(int argc, char *argv[])
{
    int* memoireAllouee = NULL;

    memoireAllouee = malloc(sizeof(int)); // Allocation de la mémoire
    if (memoireAllouee == NULL)
    {
        exit(0);
    }

    // Utilisation de la mémoire
    printf("Quel age avez-vous ? ");
    scanf("%d", memoireAllouee);
    printf("Vous avez %d ans\n", *memoireAllouee);

    free(memoireAllouee); // Libération de mémoire

    return 0;
}
Quel age avez-vous ? 31
Vous avez 31 ans

Bref : on y a alloué dynamiquement une variable de type int  .
Ce qu'on a écrit revient finalement au même que d'utiliser la méthode automatique :

int main(int argc, char *argv[])
{
    int maVariable = 0; // Allocation de la mémoire (automatique)

    // Utilisation de la mémoire
    printf("Quel age avez-vous ? ");
    scanf("%d", &maVariable);
    printf("Vous avez %d ans\n", maVariable);

    return 0;
} // Libération de la mémoire (automatique à la fin de la fonction)
Quel age avez-vous ? 31
Vous avez 31 ans

Dans cette vidéo, revenons sur ce qu'on vient de voir pour créer une variable grâce à l’allocation dynamique :

Je trouve la méthode dynamique un peu compliquée et inutile, j'ai tort ?

Un peu plus compliquée… certes. Mais inutile, non ! Nous sommes parfois obligés d'allouer manuellement de la mémoire, comme nous allons le voir maintenant.

Créez un tableau dont la taille n'est connue qu'à l'exécution

Pour le moment, on a utilisé l'allocation dynamique uniquement pour créer une petite variable. Or en général, on ne se sert pas de l'allocation dynamique pour cela. On utilise la méthode automatique qui est plus simple.

Quand a-t-on besoin de l'allocation dynamique, alors ?

Le plus souvent, on s'en sert pour créer un tableau dont on ne connaît pas la taille avant l'exécution du programme.

Imaginons par exemple un programme qui stocke l'âge de tous les amis de l'utilisateur dans un tableau. Vous pourriez créer un tableau de int pour stocker les âges, comme ceci :

int ageAmis[15];

Mais qui vous dit que l'utilisateur a 15 amis ? Peut-être qu'il en a plus que ça !

Lorsque vous écrivez le code source, vous ne connaissez pas la taille que vous devez donner à votre tableau. Vous ne le saurez qu'à l'exécution, lorsque vous demanderez à l'utilisateur combien il a d'amis.

L'intérêt de l'allocation dynamique est là : on va demander le nombre d'amis à l'utilisateur, puis on fera une allocation dynamique pour créer un tableau ayant exactement la taille nécessaire (ni trop petit, ni trop grand). Si l'utilisateur a 15 amis, on créera un tableau de 15 int  ; s'il en a 28, on créera un tableau de 28 int  , etc.

Comme je vous l'ai appris, il est interdit en C de créer un tableau en indiquant sa taille à l'aide d'une variable :

int amis[nombreDAmis];

Demandons au malloc de nous réserver nombreDAmis * sizeof(int)  octets en mémoire :

amis = malloc(nombreDAmis * sizeof(int));

Ce code permet de créer un tableau de type int qui a une taille correspondant exactement au nombre d'amis !

Voici ce que fait le programme dans l'ordre :

  1. Il demande à l'utilisateur combien il a d'amis.

  2. Il crée un tableau de int ayant une taille égale à son nombre d'amis (via malloc  ).

  3. Il demande l'âge de chacun de ses amis un à un, qu'on stocke dans le tableau.

  4. Il affiche l'âge des amis pour montrer qu'on a bien mémorisé tout cela.

  5. À la fin, puisqu'on n'a plus besoin du tableau contenant l'âge des amis, il le libère avec la fonction free  .

int main(int argc, char *argv[])
{
    int nombreDAmis = 0, i = 0;
    int* ageAmis = NULL; // Ce pointeur va servir de tableau après l'appel du malloc

    // On demande le nombre d'amis à l'utilisateur
    printf("Combien d'amis avez-vous ? ");
    scanf("%d", &nombreDAmis);

    if (nombreDAmis > 0) // Il faut qu'il ait au moins un ami (je le plains un peu sinon :p)
    {
        ageAmis = malloc(nombreDAmis * sizeof(int)); // On alloue de la mémoire pour le tableau
        if (ageAmis == NULL) // On vérifie si l'allocation a marché ou non
        {
            exit(0); // On arrête tout
        }

        // On demande l'âge des amis un à un
        for (i = 0 ; i < nombreDAmis ; i++)
        {
            printf("Quel age a l'ami numero %d ? ", i + 1);
            scanf("%d", &ageAmis[i]);
        }

        // On affiche les âges stockés un à un
        printf("\n\nVos amis ont les ages suivants :\n");
        for (i = 0 ; i < nombreDAmis ; i++)
        {
            printf("%d ans\n", ageAmis[i]);
        }

        // On libère la mémoire allouée avec malloc, on n'en a plus besoin
        free(ageAmis);
    }

    return 0;
}
Combien d'amis avez-vous ? 5
Quel age a l'ami numero 1 ? 16
Quel age a l'ami numero 2 ? 18
Quel age a l'ami numero 3 ? 20
Quel age a l'ami numero 4 ? 26
Quel age a l'ami numero 5 ? 27

Vos amis ont les ages suivants :
16 ans
18 ans
20 ans
26 ans
27 ans

J'ai choisi cet exemple parce qu'il est simple. Dans la suite du cours, nous aurons l'occasion d'utiliser le malloc pour des choses bien plus intéressantes que le stockage de l'âge de ses amis.

En résumé

  • Une variable occupe plus ou moins d'espace en mémoire en fonction de son type.

  • On peut connaître le nombre d'octets occupés par un type à l'aide de sizeof()  .

  • L'allocation dynamique consiste à réserver manuellement de l'espace en mémoire pour une variable ou un tableau.

  • L'allocation est effectuée avec malloc() et il ne faut surtout pas oublier de libérer la mémoire avec free() dès qu'on n'en a plus besoin.

  • L'allocation dynamique permet notamment de créer un tableau dont la taille est déterminée par une variable au moment de l'exécution.

Vous êtes maintenant incollable sur les bases de la programmation en C. Enfin presque ! Il nous reste un dernier chapitre avant de terminer cette partie : la saisie de texte sécurisée. Allez, on y va !

Exemple de certificat de réussite
Exemple de certificat de réussite