Malloc : allouer de la mémoire en C

Table des matières

Dans les langages de programmation compilés comme le C, il est souvent intéressant voire nécessaire d’allouer de la mémoire de façon dynamique sur le tas, pour accommoder des variables de grande taille ou de taille incertaine. C’est la fonction malloc qui nous permet de demander au système d’exploitation d’allouer une zone de mémoire à utiliser dans notre programme.

Avant de pouvoir utiliser la fonction malloc de façon efficace, il faut comprendre comment marchent les deux facettes de la mémoire vive que nos programmes utilisent pour fonctionner.

La stack et la heap de la mémoire vive

Un programme peut utiliser deux zones de mémoire vive distinctes pour stocker ses données. Ce sont la stack et la heap (respectivement, la pile ou le tas en français). Le système d’exploitation gère ces zones de mémoire complètement différemment, ce qui donne à chacune des caractéristiques propres.

La stack (ou la pile) est la zone de mémoire utilisée par défaut lorsqu’on déclare des variables dans une fonction. C’est la plus optimisée et performante grâce à son fonctionnement séquentiel et à la réutilisation constante de ses adresses. On stocke les données dans la stack comme dans une pile, les unes “posées” au dessus des autres. Dès qu’une fonction prend fin, ses variables déclarées sont automatiquement retirées dans l’ordre, depuis le haut de la pile. La stack est aussi de taille fixe; on ne peut donc y stocker des données trop importantes sans risque de stack overflow.

La heap (ou le tas), quand à elle, permet un contrôle arbitraire de l’allocation et de la libération de mémoire. Elle n’a pas de contrainte de taille. Lorsqu’un processus a besoin de plus de mémoire, il lui suffit d’en faire la demande au système d’exploitation. La flexibilité de la heap rend sa gestion plus complexe, ce qui impacte sa performance par rapport à la stack.

En règle générale, on va devoir d’allouer de la mémoire sur le tas dans ces cas-ci :

  • lorsqu’on ne connait pas la taille d’une variable à l’avance.
  • si on veut manipuler des données de taille importante.
  • quand on veut éviter la libération d’une variable à la fin de la fonction dans laquelle on l’a déclarée afin d’y accéder depuis une autre fonction.

Allouer de la mémoire avec malloc

La fonction malloc (memory allocation) sert à demander au système d’exploitation d’allouer une zone de mémoire d’une certaine taille dans la heap. Pour l’utiliser, il faut inclure la librairie stdlib.h comme suit :

#include <stdlib.h>

Voici le prototype de la fonction malloc :

void *malloc(size_t size);

Elle renvoie un pointeur de type void (qui peut donc être interprété comme n’importe quel autre type de donnée) et prend en paramètre une taille en octets.

Sachant qu’un char est composé de 1 octet mais qu’il faut 4 octets pour stocker un int, il nous faut une façon pratique pour déterminer le nombre d’octets dont on a besoin pour stocker telle ou telle donnée. Heureusement, on peut faire appel au mot-clef sizeof : par exemple, sizeof(char) nous donnera un octet, c’est à dire 8 bits, tandis que sizeof(int) nous donnera 4 octets, ou 32 bits. Il suffit ensuite de multiplier cette taille par le nombre d’ints ou de chars qu’on veut allouer.

int  *i;     //Déclaration d'un pointeur de type int
i = malloc(sizeof(int)); //Malloc de la taille d'un int (4 octets)

char *c;     //Déclaration d'un pointeur de type char
c = malloc(sizeof(char)); //Malloc de la taille d'un char (1 octet)

Mais, vu que sizeof peut aussi mesurer la taille d’une variable, ce serait certainement une meilleure pratique (et plus lisible !) d’écrire :

int  *i;    //Déclaration d'un pointeur de type int
i = malloc(sizeof *i); //Malloc de la taille de ce qui est à i (un int)

char *c;    //Déclaration d'un pointeur de type char
c = malloc(sizeof *c); //Malloc de la taille de ce qui est à c (un char)

De cette façon, si on décide plus tard de transformer notre int i en long (8 au lieu de 4 octets), on n’aura à modifier aucun malloc.

Pour malloc une chaîne de caractères, il faut aussi multiplier le nombre d’octets d’un char par le nombre de chars qu’il nous faut dans notre chaîne, sans oublier d’ajouter un char de plus pour le \0 final.

char *cpy;
char str[7] = "Coucou";
// Disons qu'on veut malloc assez d'espace pour faire une copie de str.
//Il nous faudra donc multiplier la taille d'un char par la longueur de str,
//+ 1 pour le \0 final.
// [C][o][u][c][o][u][\0] = 6 char + 1 char
cpy = malloc(sizeof *cpy * (strlen(str) + 1));

Enfin, pour allouer un tableau de chaînes de caractères, il faut d’abord allouer le tableau lui-même, pour ensuite allouer chaque chaîne l’une après l’autre.

char **strs;

// On malloc le tableau de chaînes. Disons qu'il y aura
// 3 chaînes dans notre tableau :
strs = malloc(sizeof *strs * (3 + 1)); // +1 pour le dernier \0
// Et ensuite, on malloc les chaînes de caractères
// (on peut aussi faire ceci dans une boucle, au besoin) :
strs[0] = malloc(sizeof **strs * (6 + 1)); // +1 pour le \0 final
strs[1] = malloc(sizeof **strs * (2 + 1));
strs[2] = malloc(sizeof **strs * (3 + 1));
strs[3] = malloc(sizeof **strs * 1);
/*
A titre d'exemple, ceci donne un tableau vide qui,
une fois rempli de caractères, ressemblera à ça:
 [C][o][u][c][o][u][\0]
 [c][a][\0]
 [v][a][?][\0]
 [\0]
*/

Bonnes pratiques concernant les mallocs

Caster un malloc ?

Il n’est pas nécessaire et même déconseillé de caster un malloc. En effet, vu que la fonction malloc renvoie un pointeur de type void, il est automatiquement converti lors de l’assignation. En plus d’encombrer sans raison la lecture du code, cela pourrait de plus avoir l’effet indésirable de dissimuler certaines erreurs…

int *i;

i = (int *)malloc(sizeof(int) * 1); //Moche et risqué
i = malloc(sizeof(int) * 1);        //Mieux
i = malloc(sizeof *i * 1);          //Joli et sécurisé

La dernière option est non seulement plus facile à lire, elle a aussi l’avantage de s’adapter automatiquement au type de la variable si on doit la modifier ultérieurement.

Sécuriser un malloc

Par contre, il est fortement conseillé d’ajouter de façon systématique une sécurité après un malloc pour gérer le cas où le système d’exploitation échoue à l’allocation de mémoire. Deux lignes de plus suffisent :

int *x;

x = malloc(sizeof *x * 10);
if (x == NULL)
  return ;  //ou return (-1); ou return (NULL);

Libérer de la mémoire avec free

A tout malloc correspond son free. Libérer la mémoire allouée dans la heap relève de la responsabilité du programmeur.

Certes, les systèmes d’exploitation modernes libèrent automatiquement toute mémoire utilisée lorsque le programme touche à sa fin, mais c’est tout de même une bonne habitude à prendre. Si le programme doit tourner 24 heures sur 24, sur un serveur, par exemple, libérer la mémoire est indispensable. De même pour un programme qui doit pouvoir s’exécuter dans des conditions avec de faibles ressources. Et pour les étudiants de 42, c’est une obligation.

#include <stdlib.h>

int main(void)
{
  char *ptr;

  ptr = malloc(sizeof *ptr * 25 + 1);
  if (!ptr)
    return (-1);
  free(ptr);
  return (0);
}

Pour libérer un tableau de chaînes de caractères par exemple, il faut évidemment free chacune des chaîne puis le tableau lui-même.

Toute mémoire allouée sur le tas doit être libérée.

Règle d’or de la programmation en C.

Il y a plusieurs façons de vérifier qu’il n’y a aucune fuite de mémoire dans un programme. Sur MacOS, on peut compiler le programme avec l’option -fsanitize=address. S’il y a une fuite, elle sera clairement visible lors de l’exécution du programme. La meilleure façon de détecter et de déboguer une fuite mémoire reste cependant Valgrind, sous Linux, qui fournit plus d’informations quant à sa provenance.

Codons un malloc simple

Dans cet exercice, on va allouer de la mémoire pour un tableau d’entiers à l’aide de la fonction malloc. Puis on le remplira de chiffres de 0 à 9 et on imprimera le tableau. Enfin, on n’oubliera pas de libérer la mémoire à la fin. (Tout cela, si et seulement si l’allocation de mémoire n’échoue pas !)

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
  int *tab;
  int i;

  tab = malloc(sizeof *tab * 10);
  if (tab == NULL) //Si l'allocation a échoué
    return (-1); //On arrête tout

  i = 0;
  while (i < 10) //Remplis le tableau de chiffres
  {
    tab[i] = i;
    i++;
  }

  i = 0;
  while (i < 10) //Imprime le tableau
  {
    printf("%d ", tab[i]);
    i++;
  }

  printf("\n");
  free(tab);
  return (0);
}

Sources et lectures supplémentaires

  • Manuel du programmeur Linux : malloc(3) [lien]
  • Zeste de Savoir, Le langage C : L’allocation dynamique [lien]
  • Buzut, RAM, Stack, Heap : les différents types de mémoires informatiques [lien]
  • Stack Overflow, Do I cast the result of malloc? [lien]
  • Stack Overflow, What REALLY happens when you don’t free after malloc? [lien]
  • Wikipedia, sizeof [lien]

Commentaires

Articles connexes

Threads, mutex et programmation concurrente en C

Par souci d’efficacité ou par nécessité, un programme peut être construit de façon concurrente et non séquentielle.

Lire la suite

Guide CTF : Wonderland de TryHackMe

Wonderland est un défi de capture du drapeau (CTF, “capture the flag” en anglais), créé par NinjaJc01 et disponible gratuitement sur TryHackme.

Lire la suite

Manipuler un fichier à l'aide de son descripteur en C

Les appels systèmes disponibles en C pour créer ou ouvrir un fichier, le lire, y écrire et le supprimer font toutes usage d’un descripteur de fichier.

Lire la suite