Langage C : sizeof(char) ou non ?

Creative Commons License
Cette création est mise à disposition sous un contrat Creative Commons.

L'article original se trouve sur http://ymettier.free.fr/articles_lmag/.

Article publié dans le numéro 70 (mars 2005) de GNU/Linux France Magazine


Table des matières

1. Introduction
2. Sizeof : le principe
3. Avantages
4. Quelques exemples
4.1. Les cas simples
4.2. Les cas complexes
5. Du bon usage de malloc()
6. Conclusion
7. Remerciements...

Résumé

Pour tout programmeur C la gestion de la mémoire est un point important sinon critique. L'allocation et la libération de la mémoire se fait manuellement et comme pour bien des langages, il existe plusieurs manières de procéder, plusieurs écoles et plusieurs motivations...

1. Introduction

Pour réserver de la mémoire pour une chaîne de caractères, certains écrivent

			
char *chaine;
chaine = malloc (N * sizeof (char));
			
		

Et d'autres écrivent

			
char *chaine;
chaine = malloc (N);
			
		

D'un point de vue, sizeof() est nécessaire quand il ne s'agit pas de caractères. Et par souci de garder la bonne habitude d'écrire sizeof() certains préfèrent l'écrire aussi pour des caractères. Par cohérence ou homogénéité disent-ils. D'un autre point de vue, en C, par définition sizeof(char) == 1, est toujours vrai. Alors pourquoi ne pas en profiter pour ne pas écrire ce sizeof(char) qui ne sert à rien et gène la lisibilité de certains ?

Il existe une alernative qui mérite d'être présentée pour les quelques avantages qu'elle apporte par rapport aux deux manières de faire ci-dessus.

2. Sizeof : le principe

L'opérateur sizeof() renvoie la taille de l'objet fourni en argument. Ainsi, sizeof(char) renvoie 1 et sizeof(int) renvoie 4 sur un PC. Mais pour la variable chaine de type char*, sizeof(chaine) renvoie 4 sur un PC, ce qui ne nous interesse pas, et sizeof(*chaine) renvoie 1, à savoir la taille d'un élément pointé par chaine. Et cet élément est bien de type char.

Le principe consiste donc à utiliser cette caractéristique de l'opérateur sizeof pour allouer la mémoire en fonction du nombre et du type d'éléments. Voici comment allouer une chaîne de caractères :

			
char *chaine;
chaine = malloc (N * sizeof (*chaine));
			
		

3. Avantages

Cette manière d'allouer un espace mémoire présente plusieurs avantages. Le premier est que vous multipliez systématiquement le nombre d'éléments par leur taille. Il n'est plus question de se demander si sizeof(char) est utile ou non. Au contraire, vous éviterez des erreurs comme celle-ci :

			
int *tableau;
tableau = malloc (N);
/* Trop petit : nous avons oublié de multiplier par la taille d'un entier */

			
		

Avantage suivant, vous n'avez plus besoin de vous demander de quel type est l'élément qui va peupler votre tableau. Ainsi, quel que soit le type de la variable tableau, multipliez le nombre d'éléments par sizeof(*tableau) et vous aurez la taille totale à allouer sans erreur possible due au type des éléments.

L'avantage précédent en cache un autre, encore plus intéressant. En effet, si l'humeur ou les besoins du programme vous poussent à changer le type de la donnée, vous pouvez ainsi modifier la taille nécessaire au stockage. Si elle augmente, et que vous avez utilisé sizeof(type), vous devez modifier le type partout où vous l'avez écrit, avec le risque d'en oublier. Si elle diminue, vous devriez faire de même pour ne pas gaspiller la mémoire. Par contre, si vous utilisez sizeof(*tableau), vous ne changez le type des éléments du tableau qu'une seule fois, et le reste suit.

Note

Renommer une variable vous donnera un peu plus de travail, puisqu'il y a une occurrence de plus à changer par appel à malloc(). Mais nous ne pouvons pas vraiment considérer ceci comme un inconvénient car le préprocesseur nous aide en nous rappelant que la variable a changé de nom lorsque nous avons oublié d'effectuer des modification.

4. Quelques exemples

Nous allons voir quelques exemples, car si pour un tableau d'entiers ou une chaîne de caractères l'utilisation de sizeof() est triviale, ce n'est pas forcément le cas pour d'autres structures de données.

4.1. Les cas simples

Lorsque nous souhaitons allouer de la mémoire pour un objet lui-même, cela est simple. Les objets concernés sont entre autres les types simples comme int, char. Entre autres aussi les structures, comme par exemple une cellule d'une liste chaînée liste_t déclarée ainsi :

typedef struct liste_t
{
  void *data;
  struct liste_t *prev;
  struct liste_t *next;
} liste_t;
			

Parmi les cas simples, citons encore les tableaux (mais pas leurs éléments). Cela donne des exemples comme ceci :

int *entier;
int *tableau;
float *tableau_f;
char *string;
liste_t *cellule;

entier = malloc (sizeof (*entier));
tableau = malloc (N * sizeof (*tableau));
tableau_f = malloc (N * sizeof (*tableau_f));
string = malloc (TAILLE * sizeof (*string));
cellule = malloc (sizeof (*cellule));
			

4.2. Les cas complexes

Le premier cas que vous risquez de rencontrer est l'élément d'un tableau. Vous le trouvez dès que vous utilisez un tableau de chaînes de caractères. En effet, si vous déclarez le tableau et allouez la mémoire de manière simple comme ci-dessus, l'allocation de la mémoire de chaque chaîne change un peu. Vous pourriez penser à ceci :

char **tabstr;
int i;

tabstr = malloc (N * sizeof (*tabstr));
/* Pour simplifier, toutes les chaînes ont la même taille ici */
for (i = 0; i < N, i++)
  tabstr[i] = malloc (TAILLE * sizeof (*tabstr[i]));
			

Cependant, même si cela marche, pour éviter de mettre une référence à la variable i, nous préférons remplacer la dernière ligne par ceci :

  tabstr[i] = malloc (TAILLE * sizeof (**tabstr));
			

Tant que nous y sommes, si vous rencontrez réellement un tel cas, où les chaînes ont toutes la même taille, vous ne programmerez pas comme ci-dessus mais utiliserez un vecteur de chaînes. En d'autres termes, vous ne faites appel à malloc() qu'une seule fois, et les chaînes se trouvent les unes à la suite des autres. Cela évite de fragmenter la mémoire disponible, et fait gagner quelques cycles d'horloge (un malloc() en utilise 15 000 en moyenne d'après Nicolas Boulay, auteur entre autres de l'article Tests de l'AMD Athlon FX53 du Linuxmag n°67 et de benchmarks sur cet animal). L'exemple suivant réserve l'espace pour N chaînes et initialise tabstr pour qu'il fasse référence aux chaînes comme dans l'exemple précédent.

char *vecstr;
char **tabstr;
vecstr = malloc (TAILLE * N * sizeof (*vecstr));
tabstr = malloc (N * sizeof (*tabstr));
for (i = 0; i < N, i++)
  tabstr[i] = tabvec + i * TAILLE;
			

Vous noterez ainsi deux manières d'accéder au cinquième octet de la troisième chaîne de caractères, contre une avec l'exemple d'avant. La manière commune est tabstr[2][4]. Mais comme les chaînes sont consécutives, vous pouvez aussi maintenant utiliser vecstr[2*TAILLE+4].

Un autre cas complexe, plus complexe encore, consiste à allouer de la mémoire à un élément d'une structure contenue dans un tableau. Supposons que nous ayons une liste de n'importe quoi, stockée sous forme de structures, regroupées dans un tableau. L'allocation de la mémoire nécessaire au tableau est encore un cas simple. Mais pour un champ de la structure, voici ce à quoi vous pouvez penser :

typedef struct
{
  char *nom;
/* ... */
} nimporte_quoi_t;
nimporte_quoi_t *nimp;

nimp = malloc (N * sizeof (*nimp));
/* ... */

/* Initialisation du nom du n-ième champ : */
nimp[n].nom = malloc (TAILLE * sizeof (*nimp[n].nom));
			

Encore une fois, il faut éviter la référence à la variable n, ce qui donne pour la dernière ligne :

  nimp[n].nom = malloc (TAILLE * sizeof (*nimp->nom));
			

Ces deux cas complexes montrent qu'il faut éviter de faire référence à une variable compteur dans le sizeof() d'une allocation mémoire, et que vous pouvez vous en sortir en mettant un pointeur vers l'élément comme s'il était seul, comme s'il n'y avait pas de tableau. C'est ainsi que *nimp[n].nom devient *nimp->nom car *nimp.nom (ou nimp[0].nom) et nimp->nom pointent sur la même chose.

5. Du bon usage de malloc()

La fonction malloc() a ce prototype :

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

Quand vous oubliez le fichier d'en-tête stdlib.h, le préprocesseur se plaint que malloc() ne renvoie pas le bon type, ce qui pousse certains à utiliser un cast. Ceci est à éviter, même si cela fut nécessaire du temps des dinosaures (et peut-être même avant). Aujourd'hui, pour éviter ce genre d'insultes de la part du préprocesseur, veuillez ajouter le fichier d'en-tête que vous avez oublié et il ne vous embêtera plus à ce sujet.

La fonction malloc() se contente d'allouer de la mémoire et de renvoyer un pointeur vers l'espace ainsi réservé. Pour éviter certaines erreurs, en particulier lorsque vous utilisez des chaînes de caractères ou tout autre structure dont le dernier élément est nul, vous pouvez aussi utiliser calloc() qui, non contente d'effectuer le même travail que malloc(), s'occupe de mettre à zéro tout l'espace que vous avez demandé.

Les fonctions d'allocation de mémoire renvoient un pointeur vers la zone réservée, ou NULL si cela n'est pas possible. Il est indispensable de tester systématiquement la valeur de retour pour ne pas continuer le programme comme prévu si une erreur s'est produite lors de l'allocation de mémoire. En général, si vous ne pouvez obtenir la mémoire souhaitée, vous n'avez plus rien d'autre à faire que de clore proprement votre programme. Tout code d'allocation de mémoire doit ressembler à celui-ci si vous n'avez pas de raisons de ne pas quitter suite à une erreur d'allocation :

			
if (NULL == (variable = malloc (N * sizeof (*variable))))
  {
/* 1/ Afficher un message d'erreur
 *      ou le mettre dans un fichier de journalisation (log)
 * 2/ Clore proprement
 * 3/ Quitter
 */
    exit (EXIT_FAILURE);
  }
			
		

6. Conclusion

Ces quelques idées sur l'allocation de mémoire vous pousseront peut-être à définir une macro comme ceci :

#define new(var, nb) ((var) = malloc((nb) * sizeof(*(var))))

/* ... */
int *tableau;
new (tableau, N);
		

Nous aurions tendance à penser que cette manière de faire est à éviter. Cependant, je n'ai trouvé aucun exemple spectaculaire montrant qu'il ne faut pas le faire. Certaines constructions viennent à l'esprit, mais si la macro est bien écrite, cela doit fonctionner :

int *tableau;
tableau = new (tableau, N);
		

ou

int *tableau;
if (NULL == (tableau = new (tableau, N)))
  {
    /* Message d'erreur */
    exit (EXIT_FAILURE);
  }
		

Que penser de cette macro ? Est-elle révolutionnaire, ou est-il tellement évident de ne pas l'utiliser que nous ne voyons pas pourquoi ? J'hésite à vous la conseiller et j'en reste à la bonne vieille utilisation de malloc() qui a fait ses preuves.

7. Remerciements...

... à Dave Neary qui m'a fait prendre connaissance de l'alternative à sizeof(char), et qui a bien voulu passer du temps à répondre à mes questions rebelles.

création est mise à disposition sous un contrat Creative Commons