IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Notes sur le langage C


précédentsommairesuivant

XXI. Les pointeurs démythifiés !

XXI-A. Introduction

Le langage C est indissociable de la notion de pointeur. Ce mot mythique en effraye plus d'un, et il est bon de démythifier enfin les pointeurs, sources de tant d'erreurs de codage, de discussions sans fin et de contre-vérités…

XXI-B. Définition

Un pointeur est une variable dont la valeur est une adresse.

On distingue deux grandes familles de pointeurs :

  • les pointeurs sur objet ;
  • les pointeurs de fonction.

XXI-B-1. Pointeur sur objet

En C, un objet est essentiellement une variable mémoire (par opposition à registre ou constante), modifiable ou non, mais munie d'une adresse.

Un pointeur sur objet (aussi appelé communément 'pointeur') est une variable qui peut contenir l'adresse d'un objet.

Pour définir un pointeur, on utilise un type, puis le signe '*' et enfin l'identificateur, suivi de ';' si on ne désire pas l'initialiser à la déclaration (peu recommandé). Sinon, on utilise l'opérateur '=', suivi de la valeur d'initialisation. Si le pointeur est déclaré dans un bloc, on peut utiliser la valeur retournée par une fonction.

 
Sélectionnez
/* Definition d'un pointeur sur int */
   int *p_int;

Pour initialiser un pointeur, on peut soit :

  • lui donner la valeur 0 ou NULL, qui signifie « invalide, ne pas utiliser » ;
  • lui donner l'adresse d'une variable ;
  • lui donner la valeur retournée par les fonctions malloc(), realloc(), calloc() ;
  • s'il est de type void ou FILE, lui donner la valeur retournée par fopen() ;
  • s'il est de même type, ou void, lui donner la valeur d'un autre pointeur réputé correctement initialisé.

Pour accéder à la valeur pointée, le pointeur doit être typé (donc différent de void). Dans ce cas, on peut obtenir la valeur en utilisant l'opérateur de déréférencement '*'.

 
Sélectionnez
   /* Définition de la variable 'a' valant 4 */
   int a = 4;
 
   /* Définition d'un pointeur 'p' initialisé avec l'adresse de la variable 'a' */
   int *p = &a;
 
   /* Définition de la variable 'b' non initialisée */
   int b;
 
   /* Récupération de la valeur de 'a' dans 'b' via le pointeur 'p' */
   b = *p;
 
   /* 'b' vaut maintenant 4 */

XXI-B-2. Pointeur de fonction

Une fonction a une adresse qui est le nom de cette fonction. Un pointeur de fonction peut recevoir cette adresse. Il est possible, via un pointeur de fonction correctement initialisé, d'appeler une fonction.

Cette capacité du langage C lui confère une puissance rarement égalée, qui permet d'écrire du code flexible, dont les adresses des fonctions peuvent être définies à l'exécution. Cela permet de 'personnaliser' des fonctions génériques selon les besoins.

La définition des pointeurs de fonctions est un peu complexe, et a tendance à alourdir le code :

 
Sélectionnez
   /* pointeur sur une fonction avec deux paramètres */
   int (*pf) (int, char **);
 
   /* prototype d'un fonction ayant un pointeur de fonction comme paramètre */
   int fun (int (*pf) (int, char **));

Si on doit manipuler des fonctions qui retournent un pointeur de fonction, ou des tableaux de pointeurs de fonction, le code devient rapidement illisible. C'est pourquoi il est fortement conseillé, et ce dans tous les cas, d'utiliser un typedef pour créer un alias sur le type de la fonction.

 
Sélectionnez
   /* définition d'un alias */
   typedef int fun_f (int, char **);
 
   /* définition d'un pointeur de fonction de ce type */
   fun_f *pf;
 
   /* prototype d'une fonction ayant un pointeur de fonction comme paramètre */
   int fun (fun_f *pf);
 
   /* tableau de pointeurs de fonction */
   fun_f *pf[10];
 
   /* prototype d'une fonction retournant un pointeur de fonction */
   fun_f *getfunc (int);

La lecture et la maintenance du code s'en trouvent considérablement allégées.

Pour initialiser un pointeur de fonction, on peut soit :

  • lui donner la valeur 0 ou NULL, qui signifie « invalide, ne pas utiliser » ;
  • lui donner l'adresse d'une fonction ;
  • s'il est de même type, lui donner la valeur d'un autre pointeur réputé correctement initialisé.

Pour utiliser le pointeur, il suffit de l'invoquer comme une fonction.

 
Sélectionnez
   /* définition d'un alias */
   typedef int fun_f (int);
 
   /* définition d'un pointeur de fonction de ce type */
   fun_f *pf;
 
   /* prototype d'une fonction du même type */
   int function (int);
 
   /* NOTA : on peut aussi utiliser le type */
   fun_f function;
 
   /* initialisation du pointeur de fonction */
   pf = fonction;
 
   /* appel de la fonction via le pointeur de fonction */
   pf (123);
XXI-B-2-a. Remarques importantes
  • void* n'est pas un type correct pour un pointeur de fonction ;
  • Il n'existe pas de type générique pour les pointeurs de fonctions.

XXI-C. Du bon usage des pointeurs

Un pointeur est avant tout une variable. Comme toutes les variables, elle doit être initialisée avant d'être utilisée.

Pour un pointeur en général, l'utilisation signifie le passage de sa valeur à une fonction. Pour un pointeur sur objet, c'est le déréférencement par l'opérateur '*'. Pour un pointeur de fonction, c'est l'appel de cette fonction via le pointeur.

Il est recommandé de donner une valeur significative à un pointeur. Soit il est invalide, et on lui donne la valeur 0 ou NULL, soit il est valide, et dans ce cas sa valeur est celle de l'adresse d'un objet ou d'une fonction valide. Si le bloc mémoire ou la fonction deviennent invalides, il est recommandé de donner au pointeur la valeur 0 ou NULL.

 
Sélectionnez
   /* Définition du pointeur. Il est initialisé à l'état invalide */
   int *p = NULL;
 
   /* le pointeur est initialisé avec l'adresse d'un tableau
    * dynamique de 4 éléments
    */
   p = malloc (4 * sizeof *p);
 
   /* en cas d'échec d'allocation, malloc() retourne NULL */
   if (p != NULL)
   {
      /* ... */
 
      /* après utilisation, l'espace mémoire est libéré */
      free (p);
 
      /* le pointeur est forcé à l'état invalide */
      p = NULL;
   }

XXII. Les pointeurs, ça sert à quoi ?

Les pointeurs sont un peu l'essence même du C et de tous les autres langages. Sauf que dans beaucoup de langages, cette notion est considérée comme honteuse (ou trop technique), et des astuces sont utilisées pour 'cacher' les pointeurs.

En C, on n'a pas honte des pointeurs et on les affiche ostensiblement.

XXII-A. À quoi ça sert?

XXII-A-1. Petit rappel.

Un paramètre de fonction est une variable locale de la fonction dont la valeur est donnée par l'appelant. Si on modifie cette valeur, on ne fait que modifier une variable locale. Exemple :

 
Sélectionnez
{
   int x = 123;
   f (x);
}

avec :

 
Sélectionnez
void f (int a)
{
   a++;
}

Déroulement des opérations :

  • x prend la valeur 123 ;
  • appel de la fonction f() avec en paramètre la valeur de x (soit 123) ;
  • dans f(), la variable locale a prend la valeur 123 ;
  • la valeur de a est augmentée de 1 et devient 124 ;
  • la fonction se termine ;
  • la valeur de x n'a pas changé, elle vaut toujours 123.

Si on avait voulu modifier x en appelant f(), c'est raté.

XXII-A-2. Comment faire ?

Tout simplement en donnant un moyen à la fonction qui lui permette de modifier la variable x. Ce moyen est simple. Il suffit de lui donner l'adresse de x et elle pourra alors modifier x grâce à un pointeur et à l'opérateur de déréférencement (*).

En fait, on va faire ceci :

 
Sélectionnez
{
   int x = 123;
   int *p = &x;
   (*p)++;
}

C'est-à-dire

  • x prend la valeur 123 ;
  • p prend l'adresse de x ;
  • x est incrémenté via p et l'opérateur de déréférencement. Il vaut maintenant 124.

sauf que les opérations vont être réparties entre l'appelant et l'appelé comme ceci :

 
Sélectionnez
{
   int x = 123;
   f (&x);
}

avec

 
Sélectionnez
void f (int *p)
{
   (*p)++;
}
  • x prend la valeur 123 ;
  • l'adresse de x est passée à la fonction ;
  • dans f(), la variable locale p est initialisée avec l'adresse de x ;
  • la valeur de x est incrémentée de 1 via p et l'opérateur de déréférencement. Elle vaut maintenant 124 ;
  • la fonction se termine ;
  • la valeur de x a changé, elle vaut maintenant 124.

Voilà donc un exemple d'utilisation des pointeurs. Pour un tableau, par exemple, il n'y a pas le choix. La seule façon de faire est de passer l'adresse du premier élément du tableau via un pointeur sur le même type que l'élément.

Ensuite, on a accès à tous les éléments du tableau. Non seulement le [0] en *p , mais aussi aux autres, grâce aux propriétés de l'arithmétique des pointeurs.

En effet, le type étant connu, l'adresse des autres éléments est tout simplement p+1, p+2, etc.

L'élément lui-même se trouve donc en *(p + 1), *(p + 2), etc. Le langage C définit cette écriture comme strictement équivalente à p[1], p[2], etc. On dit alors que la 'notation tableau' peut s'appliquer aux pointeurs. Mais cela ne signifie pas qu'un pointeur soit un tableau ni inversement comme on le lit parfois.

Ce principe est massivement utilisé avec les chaines de caractères, qui, rappelons-le, sont des tableaux de char initialisés avec des valeurs de caractères et terminés par un 0.

XXIII. Un tableau n'est pas un pointeur ! Vrai ou faux ?

Le tableau est probablement le concept le plus difficile à définir correctement en C. Il y a en effet beaucoup de confusion sur les termes : tableau, adresse, pointeur, indices… Ce petit article essaye de tirer les choses au clair :

  • un tableau est une séquence d'éléments de types identiques ;
  • le nom du tableau est invariant. Il a la valeur et le type de l'adresse du premier élément du tableau. C'est donc une adresse typée (encore appelée pointeur constant[1]). Étant de la même nature qu'un pointeur, les mêmes règles d'adressage s'appliquent, à savoir que le premier élément est en tab, soit tab + 0 et que son contenu est donc *(tab + 0). De même le contenu du deuxième élément est *(tab + 1), etc. ;
  • cette syntaxe étant un peu lourde, le langage C définit une simplification qui est tab[0], tab[1], etc. Le nombre entre les crochets est appelé indice. Son domaine de définition pour un tableau de taille N est 0 à N-1.

représentation graphique d'un tableau de 4 éléments :

 
Sélectionnez
tab    : |---------------|
tab[0] : |---|
tab[1] :     |---|
tab[2] :         |---|
tab[3] :             |---|

[1] C'est là que ce situe la difficulté. Le langage C parle de non-modifiable L-value ce qui signifie que c'est un objet (il a une adresse), non modifiable. On ne peut pas changer sa valeur. On ne peut changer que la valeur de ses éléments.

XXIV. Passer un tableau à une fonction

XXIV-A. Introduction

Rappel : en langage C, les passages de paramètres se font exclusivement par valeur.

Le langage C n'autorise pas le passage d'un tableau en paramètre à une fonction. La raison est probablement une recherche d'efficacité, afin d'éviter des copies inutiles.

Le but de 'passer un tableau' à une fonction est en fait de permettre à celle-ci d'accéder aux éléments du tableau en lecture ou en écriture. Pour ce faire, l'adresse du début du tableau et le type des éléments suffisent à mettre en œuvre l'arithmétique des pointeurs. Un paramètre 'pointeur' est donc exactement ce qu'il faut.

XXIV-B. Tableau à une dimension

Soit l'appelant :

 
Sélectionnez
int main (void)
{
   int tab[5];
 
   clear (tab);
 
   return 0;
}

Le prototype de la fonction appelée doit comporter un pointeur du même type que les éléments du tableau, pour recevoir l'adresse du premier élément de celui-ci, soit :

 
Sélectionnez
void clear (int *p);

La fonction va donc utiliser le paramètre pointeur, dont la valeur est l'adresse du premier élément du tableau, pour accéder aux éléments du tableau. Par exemple, les mettre à 0 :

 
Sélectionnez
void clear (int *p)
{
   *(p + 0) = 0; /* premier élément, */
   *(p + 1) = 0; /* deuxième élément, */
   *(p + 2) = 0; /* troisième élément, */
   *(p + 3) = 0; /* quatrième élément, */
   *(p + 4) = 0; /* dernier élément (cinquième) */
}

Afin d'alléger l'écriture, le langage C autorise l'utilisation de la syntaxe des tableaux pour accéder aux éléments :

 
Sélectionnez
void clear (int *p)
{
   p[0] = 0; /* premier élément, */
   p[1] = 0; /* deuxième élément, */
   p[2] = 0; /* troisième élément, */
   p[3] = 0; /* quatrième élément, */
   p[4] = 0; /* dernier élément (cinquième) */
}

Cette implémentation est évidemment théorique, car dans la pratique, on utilisera une boucle et un paramètre supplémentaire (nombre d'éléments) afin d'écrire un code plus souple et autoadaptatif.

 
Sélectionnez
void clear (int *p, size_t nb)
{
   size_t i;
 
   for (i = 0; i < nb; i++)
   {
      p[i] = 0;
   }
}
 
int main (void)
{
   int tab[5];
 
   clear (tab, 5);
 
   return 0;
}

XXIV-C. Tableau à n dimensions

Rappelons que lorsqu'on définit un paramètre, les syntaxes type *param et type param[] sont sémantiquement équivalentes.

Pour définir un paramètre de type pointeur sur un tableau à deux dimensions, on serait tenté d'écrire type p[][], ce qui serait une erreur de syntaxe. En effet, la notation [] est une notation abrégée de [TAILLE] dans les cas où cette taille est ignorée par le compilateur, c'est-à-dire lorsque la dimension concernée est la plus à gauche. Les syntaxes suivantes sont légales :

 
Sélectionnez
type_retour fonction (int p[])
type_retour fonction (int p[12])
type_retour fonction (int p[][34])
type_retour fonction (int p[56][78])
etc.

Pour déterminer le nombre d'éléments d'un tableau, on peut utiliser les propriétés des tableaux. En effet, un tableau est une suite contiguë d'éléments identiques. Le nombre d'éléments est donc tout simplement le rapport entre la taille en bytes du tableau (sizeof tab) et la taille d'un élément en bytes (sizeof tab[0] ou sizeof *tab), que l'on généralise sous la forme d'une macro bien connue :

 
Sélectionnez
#define NB_ELEM(a) (sizeof (a) / sizeof *(a))

XXV. Retourner un tableau

En C une fonction ne sait pas 'retourner un tableau'.

Ce qu'elle sait faire, c'est retourner une valeur. La pratique courante est de retourner l'adresse du premier élément du tableau. Pour cela, on définit le type retourné comme un pointeur sur le type d'un élément du tableau.

 
Sélectionnez
T *f();

T représente le type d'un élément du tableau

Évidemment, cette adresse doit être valide après exécution de la fonction. Même si c'est techniquement possible, il est donc hors de question de retourner l'adresse d'un élément appartenant à un tableau local :

  • soit on passe à la fonction l'adresse du premier élément d'un tableau existant, et elle peut retourner l'adresse de ce tableau ;
  • soit la fonction fait une allocation dynamique et retourne l'adresse du bloc alloué ;
  • on peut aussi retourner l'adresse du premier élément d'une chaine littérale. Celle-ci est statique (attention accès en lecture seule, qualificateur 'const' recommandé) ;
  • enfin, il est techniquement possible de retourner l'adresse du premier élément d'un tableau statique, mais cette pratique est déconseillée, car elle rend la fonction non réentrante, donc impropre à plusieurs utilisations comme les appels imbriqués ou les threads… Plusieurs fonctions du C sont malheureusement victimes de ce défaut (ctime() asctime(), strtok() etc.)

XXVI. char* char**

XXVI-A. Introduction

En langage C, beaucoup de problèmes de codage et d'exécution proviennent d'une confusion entre tableaux et pointeurs. C'est particulièrement vrai avec les chaines de caractères, au point d'utiliser le terme 'Char étoile' à la place du terme 'Chaine de caractères' ou 'Char étoile étoile' à la place de 'Tableau de chaines'. Qu'en est-il exactement ?

XXVI-B. Chaine de caractères

Une chaine de caractères est un tableau de char terminé par un 0. Une chaine littérale n'est pas modifiable.

 
Sélectionnez
     /* interdit */
   "hello"[2] = 'x';
 
   /* autorise' */
   char s[] = "hello";
 
   s[2] = 'x';

XXVI-C. Le type char *

Un pointeur sur char est une variable qui peut contenir NULL, l'adresse d'un char ou celle d'un élément d'un tableau de char. Si c'est un tableau, on n'a aucune information sur le nombre d'éléments du tableau pointé. Néanmoins, si c'est une chaine valide, elle est terminée par un 0 qui sert de balise de fin.

 
Sélectionnez
char c;
   char *pa = &c;
 
   char *pb = 0;
   char *pc = NULL;
 
   char s[] = "hello";
 
   char *pd = s;
   char *pe = s + 3;
 
   /* une chaine littérale n'étant pas modifiable,
    * il est conseillé de qualifier l'objet avec
    * 'const' (read-only)
    */
   char const *pf = "hello";
   char const *pg = "hello" + 2;

Un petit schéma pour modéliser :

Représentation graphique d'un objet 'c' de type char non initialisé :

 
Sélectionnez
char c;
 
   :---------:--------:
   : adresse : valeur :
   :         :        :
   :---------:--------:
   : &c      : ???    :
   :---------:--------:

Représentation graphique d'un objet 'c' de type char après initialisation :

 
Sélectionnez
c = 'À';
 
   :---------:--------:
   : adresse : valeur :
   :         :        :
   :---------:--------:
   : &c      : 'A'    :
   :---------:--------:

Représentation graphique d'un objet 'p' de type char * non initialisé :

 
Sélectionnez
char *p;
 
   :---------:--------:---------:
   : adresse : valeur : valeur  :
   :         :        : pointée :
   :---------:--------:---------:
   : &p      : ???    : ???     :
   :---------:--------:---------:

Représentation graphique d'un objet 'p' de type char * après initialisation (NULL) :

 
Sélectionnez
p = NULL;
 
   :---------:--------:---------:
   : adresse : valeur : valeur  :
   :         :        : pointée :
   :---------:--------:---------:
   : &p      : NULL   : ???     : --> NULL
   :---------:--------:---------:

Représentation graphique d'un objet 'p' de type char * après initialisation (adresse d'une variable) :

 
Sélectionnez
p = &c;
 
   :---------:--------:---------:     :---------:--------:
   : adresse : valeur : valeur  :     : adresse : valeur :
   :         :        : pointée :     :         :        :
   :---------:--------:---------:     :---------:--------:
   : &p      : &c     : 'A'     : --> : &c      : 'A'    :
   :---------:--------:---------:     :---------:--------:

Représentation graphique d'un objet 'p' de type char * après initialisation (adresse d'une chaine modifiable) :

 
Sélectionnez
char s[]="ab";
   p = s;
 
   :---------:---------:---------:     :---------:--------:
   : adresse : valeur  : valeur  :     : adresse : valeur :
   :         :         : pointée :     :         :        :
   :---------:---------:---------:     :---------:--------:
   : &p      : &s[0]   : 'a'     : --> : s+0     : 'a'    :
   :         : ou s+0  :         :     :---------:--------:
   :         : ou s    :         :     : s+1     : 'b'    :
   :---------:---------:---------:     :---------:--------:
                                       : s+2     : 0      :
                                       :---------:--------:

Le pointeur sur char est principalement utilisé pour les paramètres 'chaines de caractères' des fonctions et pour des manipulations de chaines de caractères.

Mais cela n'autorise pas à utiliser le terme 'char étoile' à la place de 'chaine de caractères'.

XXVI-D. Le type char **

Un pointeur sur pointeur de char est une variable qui peut contenir NULL, l'adresse d'un pointeur de char ou celle d'un élément d'un tableau de pointeurs de char. Si c'est un tableau, on n’a aucune information sur le nombre d'éléments du tableau pointé. On peut parfois ajouter un élément de valeur NULL pour délimiter le tableau de pointeurs. Un petit schéma pour modéliser :

XXVII. Initialisation des tableaux de caractères

Il est possible d'initialiser un tableau de char au moment de sa définition avec une constante qui ressemble a une chaine de caractères. Il faut cependant faire attention, car il n'est pas garanti que le tableau ainsi initialisé forme une chaine C valide (c'est-à-dire terminée par un 0).

En effet, la liste de caractères placée entre doubles quotes et servant à initialiser le tableau de char n'est en aucun cas une chaine de caractères. C'est une simple liste de caractères. Les zéros que l'on voit dans le tableau sont le résultat du comportement standard du C qui complète à 0 les tableaux partiellement initialisés.

Exemples :

 
Sélectionnez
char s[4] = "ab";

le tableau est initialisé avec {'a', 'b', 0, 0} : Chaine valide

 
Sélectionnez
char s[4] = "abc";

le tableau est initialisé avec {'a', 'b', 'c', 0} : Chaine valide

 
Sélectionnez
char s[4] = "abcd";

le tableau est initialisé avec {'a', 'b', 'c', 'd'} : Chaine invalide /!\

 
Sélectionnez
char s[4] = "abcde";

Ne compile pas (trop d'initialisateurs)

XXVIII. Qu'est-ce qu'une chaine littérale ?

Une chaine littérale, telle qu'elle apparaît dans un source C, est une séquence de caractères entourée de guillemets (doubles quotes)

 
Sélectionnez
"hello"

Elle ne doit pas être confondue avec la liste de caractères servant à initialiser un tableau de char, par exemple :

 
Sélectionnez
char s[] = "hello";

(les détails sont indiqués ici).

Une chaine littérale désigne en réalité l'adresse du premier élément d'un tableau de char anonyme non modifiable, situé en mémoire statique, initialisé avec la séquence de caractères mentionnés et terminé par un 0.

Tout se passe comme si on avait ceci :

 
Sélectionnez
static char const identificateur_connu_seulement_du_compilateur[] = {'h','e','l','l','o',0};

Si la chaine apparaît dans un paramètre de fonction :

 
Sélectionnez
f ("hello");

c'est cette valeur (l'adresse) qui est passée à la fonction dans son paramètre :

 
Sélectionnez
void f (char const *s);

Si la chaine sert à initialiser un pointeur :

 
Sélectionnez
char const *p = "hello";
 
p = "bye";

c'est cette valeur (l'adresse) qui est stockée dans le pointeur.

Rappel : le mot clé const sert à qualifier l'objet de non modifiable

XXIX. Bien utiliser malloc()

Il est fréquent de rencontrer ce genre de code :

 
Sélectionnez
#include <stdlib.h>
...
{
   /* création d'un tableau de 10 int */
   size_t n = 10;
   int *p = (int*) malloc (sizeof (int) * n);
 
   if (p != NULL)
   {
      ...
}

Ce code est correct et ne présente pas de comportement indéterminé. Cependant, il est inutilement compliqué et peut être amélioré de plusieurs façons.

XXIX-A. Suppression du cast

Il est d'usage d'éviter les casts en C. Certains sont indispensables, d'autres non. Ici, par exemple, et contrairement aux idées reçues, le cast est inutile, et on peut parfaitement écrire :

 
Sélectionnez
{
   int *p = malloc (sizeof (int) * n);
}

Il est cependant des cas rares où le cast est indispensable :

  • le compilateur n'est pas conforme à ISO C-90 ou ISO C-99 ;
  • le compilateur n'est pas C, mais par exemple préC++98.

XXIX-A-1. Compilateur non ISO

Il est rare de nos jours d'utiliser un compilateur datant d'avant la normalisation du langage C (1989 aux USA, 1990 au niveau international). En effet, ces compilateurs ne supportent pas les prototypes, ce qui les rend impropres à produire du code cohérent, à moins d'utiliser un outil de vérification indépendant comme PCLint.

Le cas peut cependant se produire, s'il s'agit de maintenir du code ancien avec une chaine de compilation ancienne. Dans ce cas, effectivement, le cast est indispensable si le type du pointeur et différent de char*.

L'opportunité de conserver une telle pratique est donc laissée à l'appréciation du programmeur. Il semble cependant assez évident que dans les nouveaux développements utilisant un compilateur ISO, il est inutile d'ajouter le cast.

XXIX-A-2. Compilateur C++

Il est techniquement possible, à de rares exceptions syntaxiques près, de faire compiler du code C par un compilateur C++. Néanmoins, cette pratique est rarement justifiée et est largement déconseillée.

En effet, en dehors des points syntaxiques évidents (par exemple la conversion de type explicite void* <-> type* qui justement oblige à utiliser le cast) plusieurs points de sémantique diffèrent entre les deux langages. En l'état actuel des normes, les spécifications C++98 et C99 ont même plutôt tendance à diverger (cette situation pourrait changer en 2005 avec une nouvelle révision de C++ intégrant les nouveautés de C99).

On peut aussi se demander pourquoi on utiliserait malloc() et free() en C++, alors que ce langage dispose des opérateurs new et delete . D'autre part, en C++98, un cast se fait avec static_cast<…>.

Lire à ce sujet : Incompatibilities Between ISO C and ISO C++

XXIX-B. Déterminer la taille sans le type

Il est courant de déterminer la taille d'un objet en utilisant son type

 
Sélectionnez
{
   int *p = malloc (sizeof (int));
}

Si le type change, on est obligé de modifier deux fois le code :

 
Sélectionnez
{
   long *p = malloc (sizeof (long));
}

Lorsqu'il s'agit d'un pointeur typé, il existe une technique alternative qui consiste à utiliser la taille d'un élément pointé par ce pointeur :

 
Sélectionnez
{
   int *p = malloc (sizeof *p);
}

Le changement de type se trouve largement simplifié :

 
Sélectionnez
{
   long *p = malloc (sizeof *p);
}

Quelques compléments sur malloc() et l'allocation dynamique en général dans l'article Description des mécanismes d'allocation dynamique de mémoire en langage C

XXX. Bien utiliser realloc()

La fonction realloc(), bien que souvent décriée pour sa lenteur, offre une alternative intéressante pour gérer des tableaux de taille variable. Bien sûr il ne faut pas allouer les objets un par un, mais par blocs (doublage, par exemple).

Pour utiliser correctement realloc(), quelques précautions doivent être prise. Par exemple :

 
Sélectionnez
#include <stdlib.h>
 
<...>
{
   /* allocation d'un tableau de 10 int
    * Pour pouvoir gérer la taille, on utilise
    * une variable 'taille'. Une structure comprenant
    * l'adresse du tableau et sa taille est aussi envisageable.
    */
 
   size_t size = 10;
   int *p = malloc (size * sizeof *p);
 
<...>
 
   /* Agrandissement du tableau a 15 int */
   {
      /* reallocation. Le résultat est stocke'
       * dans une variable temporaire
       */
      size = 15;
      type_s *p_tmp = realloc (p, size * sizeof *p_tmp);
 
      if (p_tmp != NULL)
      {
         /* si la nouvelle valeur est valide,
          * le pointeur original est mis a jour.
          */
         p = p_tmp;
      }
      else
      {
         /* l'ancien bloc est valide, mais il n'a pas été agrandi */
      }
   }
}

Il faut aussi garder à l'esprit que la partie nouvellement allouée n'est pas initialisée.

XXXI. Comment créer un tableau dynamique à deux dimensions ?

Il y a plusieurs façons de créer un tableau dynamique à deux dimensions de type T. La plus courante consiste à créer un tableau de N lignes contenant les adresses des N tableaux de M colonnes. L'avantage de cette méthode est qu'elle permet un usage habituel du tableau avec la notation [i][j].

Comme le tableau de N lignes contient des adresses, ses éléments sont donc des pointeurs sur T. Il se définit ainsi :

 
Sélectionnez
T* *pp = malloc (sizeof (T*) * N);

T représente le type d'un élément du tableau

Ensuite, chaque élément reçoit l'adresse du premier élément d'un tableau alloué de M colonnes. Chaque élément est donc de type T :

 
Sélectionnez
size_t i;
   for (i = 0; i < N; i++)
   {
      pp[i] = malloc (sizeof (T) * M);
   }

Bien sûr, pour une utilisation correcte, il faut en plus tenir compte du fait que malloc() peut échouer et qu'il faut libérer les blocs alloués après usage.

D'autre part, je rappelle que les valeurs d'un bloc fraîchement alloué sont indéfinies.

Enfin, selon les principes énoncés ici, on peut simplifier le codage comme ceci :

 
Sélectionnez
T **pp = malloc (sizeof *pp * N);
 
Sélectionnez
size_t i;
   for (i = 0; i < N; i++)
   {
      pp[i] = malloc (sizeof *pp[i] * M);
   }

ce qui facilite la maintenance et évite bien des erreurs de type, le choix étant confié au compilateur.

Il va sans dire qu'il faut ensuite libérer le tableau alloué selon le procédé inverse :

 
Sélectionnez
size_t i;
   for (i = 0; i < N; i++)
   {
     free(pp[i]), pp[i] = NULL;
   }
   free(pp), pp = NULL;

précédentsommairesuivant

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.