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 2 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, suivit de ';' si on ne désire pas l'initialiser à la déclaration (peu recommandé). Sinon, on utilise l'opérateur '=', suivit de la valeur d'initialisation. Si le pointeur est déclaré dans un bloc, on peut utiliser la valeur retournée par une fonction.
/*
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 '*'.
/*
Definition
de
la
variable
'a'
valant
4
*/
int
a =
4
;
/*
Definition
d'un
pointeur
'p'
initialise'
avec
l'adresse
de
la
variable
'a'
*/
int
*
p =
&
a;
/*
Definition
de
la
variable
'b'
non
initialisee
*/
int
b;
/*
recuperation
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 :
/*
pointeur
sur
une
fonction
avec
2
parametres
*/
int
(*
pf) (int
, char
*
*
);
/*
prototype
d'un
fonction
ayant
un
pointeur
de
fonction
comme
parametre
*/
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.
/*
definition
d'un
alias
*/
typedef
int
fun_f (int
, char
*
*
);
/*
definition
d'un
pointeur
de
fonction
de
ce
type
*/
fun_f *
pf;
/*
prototype
d'une
fonction
ayant
un
pointeur
de
fonction
comme
parametre
*/
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.
/*
definition
d'un
alias
*/
typedef
int
fun_f (int
);
/*
definition
d'un
pointeur
de
fonction
de
ce
type
*/
fun_f *
pf;
/*
prototype
d'une
fonction
du
meme
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.
/*
Definition
du
pointeur.
Il
est
initialise
a
l'etat
invalide
*/
int
*
p =
NULL
;
/*
le
pointeur
est
initialise
avec
l'adresse
d'un
tableau
*
dynamique
de
4
elements
*/
p =
malloc (4
*
sizeof
*
p);
/*
en
cas
d'échec
d'allocation,
malloc()
retourne
NULL
*/
if
(p !
=
NULL
)
{
/*
...
*/
/*
apres
utilisation,
l'espace
memoire
est
libere
*/
free (p);
/*
le
pointeur
est
force
a
l'etat
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. 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 :
{
int
x =
123
;
f (x);
}
avec
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 permettre 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 :
{
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 :
{
int
x =
123
;
f (&
x);
}
avec
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]). Etant de la même nature qu'un pointeur, les même 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 :
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 se faire, l'adresse du début du tableau et le type des éléments suffisent à mettre en oeuvre l'arithmétique des pointeurs. Un paramètre 'pointeur' est donc exactement ce qu'il faut.
XXIV-B. Tableau à une dimension▲
Soit l'appelant :
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 :
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 :
void
clear (int
*
p)
{
*
(p +
0
) =
0
; /*
premier
element,
*/
*
(p +
1
) =
0
; /*
deuxieme
element,
*/
*
(p +
2
) =
0
; /*
troisieme
element,
*/
*
(p +
3
) =
0
; /*
quatrieme
element,
*/
*
(p +
4
) =
0
; /*
dernier
element
(cinquieme)
*/
}
Afin d'alléger l'écriture, le langage C autorise l'utilisation de la syntaxe des tableaux pour accéder aux éléments :
void
clear (int
*
p)
{
p[0
] =
0
; /*
premier
element,
*/
p[1
] =
0
; /*
deuxieme
element,
*/
p[2
] =
0
; /*
troisieme
element,
*/
p[3
] =
0
; /*
quatrieme
element,
*/
p[4
] =
0
; /*
dernier
element
(cinquieme)
*/
}
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 auto adaptatif.
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 à 2 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 :
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 :
#
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.
T *
f
();
T représente le type d'un élément du tableau
Evidemment, 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.
/*
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.
char
c;
char
*
pa =
&
c;
char
*
pb =
0
;
char
*
pc =
NULL
;
char
s[] =
"
hello
"
;
char
*
pd =
s;
char
*
pe =
s +
3
;
/*
une
chaine
litterale
n'etant
pas
modifiable,
*
il
est
conseille
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é :
char c;
:---------:--------:
: adresse : valeur :
: : :
:---------:--------:
: &c : ??? :
:---------:--------:
Représentation graphique d'un objet 'c' de type char après initialisation :
c = 'A';
:---------:--------:
: adresse : valeur :
: : :
:---------:--------:
: &c : 'A' :
:---------:--------:
Représentation graphique d'un objet 'p' de type char * non initialisé :
char *p;
:---------:--------:---------:
: adresse : valeur : valeur :
: : : pointée :
:---------:--------:---------:
: &p : ??? : ??? :
:---------:--------:---------:
Représentation graphique d'un objet 'p' de type char * après initialisation (NULL) :
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) :
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) :
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 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 double 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
char
s[4
] =
"
ab
"
;
le tableau est initialisé avec {'a', 'b', 0, 0} : Chaine valide
char
s[4
] =
"
abc
"
;
le tableau est initialisé avec {'a', 'b', 'c', 0} : Chaine valide
char
s[4
] =
"
abcd
"
;
le tableau est initialisé avec {'a', 'b', 'c', 'd'} : Chaine invalide /!\
char
s[4
] =
"
abcde
"
;
Ne compile pas (trop d'initialisateurs)
XXVIII. Qu'est-ce qu'une chaine litterale ?▲
Une chaine littérale, telle qu'elle apparait dans un source C, est une séquence de caractères entourée de guillemets (double quotes)
"
hello
"
Elle ne doit pas être confondue avec la liste de caractères servant à initialiser un tableau de char, par exemple :
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 :
static
char
const
identificateur_connu_seulement_du_compilateur[] =
{
'
h
'
,'
e
'
,'
l
'
,'
l
'
,'
o
'
,0
}
;
Si la chaine apparait dans un paramètre de fonction :
f ("
hello
"
);
c'est cette valeur (l'adresse) qui est passée à la fonction dans son paramètre :
void
f (char
const
*
s);
Si la chaine sert à initialiser un pointeur :
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 :
#
include
<stdlib.h>
...
{
/*
creation
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 :
{
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 impropre à 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 (comme 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
{
int
*
p =
malloc (sizeof
(int
));
}
Si le type change, on est obligé de modifier 2 fois le code:
{
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 :
{
int
*
p =
malloc (sizeof
*
p);
}
Le changement de type se trouve largement simplifié :
{
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 :
#
include
<stdlib.h>
<
...>
{
/*
allocation
d'un
tableau
de
10
int
*
Pour
pouvoir
gerer
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
resultat
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
ete
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 à 2 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 :
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 :
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 fraichement alloué sont indéfinies.
Enfin, selon les principes énoncés ici, on peut simplifier le codage comme ceci :
T *
*
pp =
malloc (sizeof
*
pp *
N);
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 :
size_t i;
for
(i =
0
; i <
N; i+
+
)
{
free
(pp[i]), pp[i] =
NULL
;
}
free
(pp), pp =
NULL
;