Académique Documents
Professionnel Documents
Culture Documents
Les pointeurs sont des variables très utilisées en C et en C++. Ils doivent être considérés
comme des variables, il n'y a rien de sorcier derrière les pointeurs. Cependant, les pointeurs
ont un domaine d'application très vaste.
Les références sont des identificateurs synonymes d'autres identificateurs, qui permettent de
manipuler certaines notions introduites avec les pointeurs plus souplement. Elles n'existent
qu'en C++.
Notion d'adresse
Tout objet manipulé par l'ordinateur est stocké dans sa mémoire. On peut considérer que cette
mémoire est constituée d'une série de « cases », cases dans lesquelles sont stockées les valeurs
des variables ou les instructions du programme. Pour pouvoir accéder à un objet (la valeur
d'une variable ou les instructions à exécuter par exemple), c'est-à-dire au contenu de la case
mémoire dans laquelle cet objet est enregistré, il faut connaître le numéro de cette case.
Autrement dit, il faut connaître l'emplacement en mémoire de l'objet à manipuler. Cet
emplacement est appelé l'adresse de la case mémoire, et par extension, l'adresse de la variable
ou l'adresse de la fonction stockée dans cette case et celles qui la suivent.
Toute case mémoire a une adresse unique. Lorsqu'on utilise une variable ou une fonction, le
compilateur manipule l'adresse de cette dernière pour y accéder. C'est lui qui connaît cette
adresse, le programmeur n'a pas à s'en soucier.
Notion de pointeur
Une adresse est une valeur. On peut donc stocker cette valeur dans une variable. Les pointeurs
sont justement des variables qui contiennent l'adresse d'autres objets, par exemple l'adresse
d'une autre variable. On dit que le pointeur pointe sur la variable pointée. Ici, pointer signifie
« faire référence à ». Les adresses sont généralement des valeurs constantes, car en général un
objet ne se déplace pas en mémoire. Toutefois, la valeur d'un pointeur peut changer. Cela ne
signifie pas que la variable pointée est déplacée en mémoire, mais plutôt que le pointeur
pointe sur autre chose.
Afin de savoir ce qui est pointé par un pointeur, les pointeurs disposent d'un type. Ce type est
construit à partir du type de l'objet pointé. Cela permet au compilateur de vérifier que les
manipulations réalisées en mémoire par l'intermédiaire du pointeur sont valides. Le type des
pointeur se lit « pointeur de ... », où les points de suspension représentent le nom du type de
l'objet pointé.
Les pointeurs se déclarent en donnant le type de l'objet qu'ils devront pointer, suivi de leur
identificateur précédé d'une étoile :
int *pi1, *pi2, j, *pi3;
Déréférencement, indirection
Un pointeur ne servirait strictement à rien s'il n'y avait pas de possibilité d'accéder à l'adresse
d'une variable ou d'une fonction (on ne pourrait alors pas l'initialiser) ou s'il n'y avait pas
moyen d'accéder à l'objet référencé par le pointeur (la variable pointée ne pourrait pas être
manipulée ou la fonction pointée ne pourrait pas être appelée).
Il est très important de s'assurer que les pointeurs que l'on manipule sont tous initialisés (c'est-
à-dire contiennent l'adresse d'un objet valide, et pas n'importe quoi). En effet, accéder à un
pointeur non initialisé revient à lire ou, plus grave encore, à écrire dans la mémoire à un
endroit complètement aléatoire (selon la valeur initiale du pointeur lors de sa création). En
général, on initialise les pointeurs dès leur création, ou, s'ils doivent être utilisés
ultérieurement, on les initialise avec le pointeur nul. Cela permettra de faire ultérieurement
des tests sur la validité du pointeur ou au moins de détecter les erreurs. En effet, l'utilisation
d'un pointeur initialisé avec le pointeur nul génère souvent une faute de protection du
programme, que tout bon débogueur est capable de détecter. Le pointeur nul se note NULL.
Notion de référence
En plus des pointeurs, le C++ permet de créer des références. Les références sont des
synonymes d'identificateurs. Elles permettent de manipuler une variable sous un autre nom
que celui sous laquelle cette dernière a été déclarée.
Note : Les références n'existent qu'en C++. Le C ne permet pas de créer des références.
Par exemple, si « id » est le nom d'une variable, il est possible de créer une référence « ref »
de cette variable. Les deux identificateurs id et ref représentent alors la même variable, et
celle-ci peut être accédée et modifiée à l'aide de ces deux identificateurs indistinctement.
Toute référence doit se référer à un identificateur : il est donc impossible de déclarer une
référence sans l'initialiser. De plus, la déclaration d'une référence ne crée pas un nouvel objet
comme c'est le cas pour la déclaration d'une variable par exemple. En effet, les références se
rapportent à des identificateurs déjà existants.
Exemple :
int i=0;
int &ri=i; // Référence sur la variable i.
ri=ri+i; // Double la valeur de i (et de ri).
Il est possible de faire des références sur des valeurs numériques. Dans ce cas, les références
doivent être déclarées comme étant constantes, puisqu'une valeur est une constante :
Il y a deux méthodes pour passer des variables en paramètre dans une fonction : le passage par
valeur et le passage par variable. Ces méthodes sont décrites ci-dessous.
La valeur de l'expression passée en paramètre est copiée dans une variable locale. C'est cette
variable qui est utilisée pour faire les calculs dans la fonction appelée.
Si l'expression passée en paramètre est une variable, son contenu est copié dans la variable
locale. Aucune modification de la variable locale dans la fonction appelée ne modifie la
variable passée en paramètre, parce que ces modifications ne s'appliquent qu'à une copie de
cette dernière.
Exemple en C :
int main(void)
{
int i=2;
test(i); /* Le contenu de i est copié dans j.
i n'est pas modifié. Il vaut toujours 2. */
test(2); /* La valeur 2 est copiée dans j. */
return 0;
}
Passage par variable
La deuxième technique consiste à passer non plus la valeur des variables comme paramètre,
mais à passer les variables elles-mêmes. Il n'y a donc plus de copie, plus de variable locale.
Toute modification du paramètre dans la fonction appelée entraîne la modification de la
variable passée en paramètre.
Il n'y a qu'une solution : passer l'adresse de la variable. Cela constitue donc une application
des pointeurs.
int main(void)
{
int i=3;
test(&i); /* On passe l'adresse de i en paramètre. */
/* Ici, i vaut 2. */
return 0;
}
À présent, il est facile de comprendre la signification de & dans l'appel de scanf : les variables
à entrer sont passées par variable.
la syntaxe est lourde dans la fonction, à cause de l'emploi de l'opérateur * devant les
paramètres ;
la syntaxe est dangereuse lors de l'appel de la fonction, puisqu'il faut
systématiquement penser à utiliser l'opérateur & devant les paramètres. Un oubli
devant une variable de type entier et la valeur de l'entier est utilisée à la place de son
adresse dans la fonction appelée (plantage assuré, essayez avec scanf).
Le C++ permet de résoudre tous ces problèmes à l'aide des références. Au lieu de passer les
adresses des variables, il suffit de passer les variables elles-mêmes en utilisant des paramètres
sous la forme de références. La syntaxe des paramètres devient alors :
Les tableaux sont étroitement liés aux pointeurs parce que, de manière interne, l'accès aux
éléments des tableaux se fait par manipulation de leur adresse de base, de la taille des
éléments et de leurs indices. En fait, l'adresse du n-ième élément d'un tableau est calculée
avec la formule :
Afin de pouvoir utiliser l'arithmétique des pointeurs pour manipuler les éléments des tableaux,
le C++ effectue les conversions implicites suivantes lorsque nécessaire :
identificateur[n]
et :
*(identificateur + n)
Il en résulte parfois quelques ambiguïtés lorsqu'on manipule les adresses des tableaux. En
particulier, on a l'égalité suivante :
&tableau == tableau
en raison du fait que l'adresse du tableau est la même que celle de son premier élément. Il faut
bien comprendre que dans cette expression, une conversion a lieu. Cette égalité n'est donc pas
exacte en théorie. En effet, si c'était le cas, on pourrait écrire :
*&tableau == tableau
Par ailleurs, certaines caractéristiques des tableaux peuvent être utilisées pour les passer en
paramètre dans les fonctions.
Il est autorisé de ne pas spécifier la taille de la dernière dimension des paramètres de type
tableau dans les déclarations et les définitions de fonctions. En effet, la borne supérieure des
tableaux n'a pas besoin d'être précisée pour manipuler leurs éléments (on peut malgré tout la
donner si cela semble nécessaire).
Cependant, pour les dimensions deux et suivantes, les tailles des premières dimensions restent
nécessaires. Si elles n'étaient pas données explicitement, le compilateur ne pourrait pas
connaître le rapport des dimensions. Par exemple, la syntaxe :
int tableau[][];
utilisée pour référencer un tableau de 12 entiers ne permettrait pas de faire la différence entre
les tableaux de deux lignes et de six colonnes et les tableaux de trois lignes et de quatre
colonnes (et leurs transposés respectifs). Une référence telle que :
tableau[1][3]
ne représenterait rien. Selon le type de tableau, l'élément référencé serait le quatrième élément
de la deuxième ligne (de six éléments), soit le dixième élément, ou bien le quatrième élément
de la deuxième ligne (de quatre éléments), soit le huitième élément du tableau. En précisant
tous les indices sauf un, il est possible de connaître la taille du tableau pour cet indice à partir
de la taille globale du tableau, en la divisant par les tailles sur les autres dimensions (2 = 12/6
ou 3 = 12/4 par exemple).
int main(void)
{
test(tab); /* Passage du tableau en paramètre. */
return 0;
}
Les pointeurs sont surtout utilisés pour créer un nombre quelconque de variables, ou des
variables de taille quelconque, en cours d'exécution du programme.
En temps normal, les variables sont créées automatiquement lors de leur définition. Cela est
faisable parce que les variables à créer ainsi que leurs tailles sont connues au moment de la
compilation (c'est le but des déclarations que d'indiquer la structure et la taille des objets, et
plus généralement de donner les informations nécessaires à leur utilisation). Par exemple, une
ligne comme :
int tableau[10000];
signale au compilateur qu'une variable tableau de 10000 entiers doit être créée. Le programme
s'en chargera donc automatiquement lors de l'exécution.
Mais supposons que le programme gère une liste de personnes. On ne peut pas savoir à
l'avance combien de personnes seront entrées, le compilateur ne peut donc pas faire la
réservation de l'espace mémoire automatiquement. C'est au programmeur de le faire. Cette
réservation de mémoire (appelée encore allocation) doit être faite pendant l'exécution du
programme. La différence avec la déclaration de tableau précédente, c'est que le nombre de
personnes et donc la quantité de mémoire à allouer, est variable. Il faut donc faire ce qu'on
appelle une allocation dynamique de mémoire.
Allocation dynamique de mémoire en C
malloc(taille)
free(pointeur)
free (pour « FREE memory ») libère la mémoire allouée. Elle attend comme paramètre le
pointeur sur la zone à libérer et ne renvoie rien.
Lorsqu'on alloue une variable typée, on doit faire un transtypage du pointeur renvoyé par
malloc en pointeur de ce type de variable.
Pour utiliser les fonctions malloc et free, vous devez mettre au début de votre programme la
ligne :
#include <stdlib.h>
En plus des fonctions malloc et free du C, le C++ fournit d'autres moyens pour allouer et
restituer la mémoire. Pour cela, il dispose d'opérateurs spécifiques : new, delete, new[] et
delete[]. La syntaxe de ces opérateurs est respectivement la suivante :
new type
delete pointeur
new type[taille]
delete[] pointeur
es deux opérateurs new et new[] permettent d'allouer de la mémoire, et les deux opérateurs
delete et delete[] de la restituer.
La syntaxe de new est très simple, il suffit de faire suivre le mot clé new du type de la variable
à allouer, et l'opérateur renvoie directement un pointeur sur cette variable avec le bon type. Il
n'est donc plus nécessaire d'effectuer un transtypage après l'allocation, comme c'était le cas
pour la fonction malloc. Par exemple, l'allocation d'un entier se fait comme suit :
Les opérateurs new[] et delete[] sont utilisés pour allouer et restituer la mémoire pour les
types tableaux. Ce ne sont pas les mêmes opérateurs que new et delete, et la mémoire allouée
par les uns ne peut pas être libérée par les autres. Si la syntaxe de delete[] est la même que
celle de delete, l'emploi de l'opérateur new[] nécessite de donner la taille du tableau à allouer.
Ainsi, on pourra créer un tableau de 10000 entiers de la manière suivante :
int *Tableau=new int[10000];
delete[] Tableau;
L'opérateur new[] permet également d'allouer des tableaux à plusieurs dimensions. Pour cela,
il suffit de spécifier les tailles des différentes dimensions à la suite du type de donnée des
élements du tableau, exactement comme lorsque l'on crée un tableau statiquement.
#include<iostream>
{
for(i=0 ; i<5 ; i++)
t[i] = i * i;
for(i=0 ; i<5 ; i++)
cout << t[i] << " ";
cout << endl;
free(t);
}
t = (int *) malloc( 10 * sizeof(int) );
if (t==NULL)
cout << "pas assez de mémoire" << endl;
else
{
for(i=0;i<10;i++)
t[i] = i * i;
for(i=0 ; i<10 ; i++)
cout << t[i] << " ";
cout << endl;
free(t);
}
return 0;
}
Explications
• Dans cet exemple, t est un pointeur vers un entier.
• Après l'appel à la fonction malloc dans l'instruction t=(int *)malloc(5*sizeof(int)), t contient
l'adresse d'une zone mémoire dont la taille est 5 fois la taille d'un entier. La variable t devient
ainsi un tableau de
5 entiers qu'on peut utiliser comme n'importe quel tableau d'entiers.
• Si t n'est pas NULL, ce qui signifie qu'il y avait assez de mémoire disponible, alors on peut
accéder à n'importe
quelle élément du tableau en écrivant t[i] (i étant bien sûr compris entre 0 et 4).
• Dans ce programme, on remplit les 5 cases du tableau t en mettant i*i dans la case i et on
affiche ce tableau.
• Ensuite, on libère l'espace occupé par le tableau en appelant la fonction free(t). t devient
alors un pointeur
non initialisé et on a plus le droit d'accéder aux différentes cases du tableau qui a été détruit.
• On appelle ensuite la fonction t=(int *)malloc(10*sizeof(int)); t devient alors cette fois-ci un
tableau à 10 cases (sauf si t est NULL) et on peut accéder à la case i en écrivant t[i] (i compris
entre 0 et
9).
• Dans ce programme, on remplit les 10 cases du tableau t en mettant i*i dans la case i et on
affiche ce
tableau.
• On détruit ensuite la tableau t en appelant free(t)
#include<iostream>
{
for ( i=0 ; i<5 ; i++ )
t[i] = i * i;
for ( i=0 ; i<5 ; i++ )
cout << t[i] << " ";
cout << endl;
delete [] t;
t = new int[10];
if (t == NULL)
cout << "pas assez de mémoire" << endl;
else
{
for ( i=0 ; i<10 ; i++ )
t[i] = i * i;
for ( i=0 ; i<10 ; i++ )
cout << t[i] << " ";
cout << endl;
delete [] t;
}
}
return 0;
}
Explications
• Dans cet exemple, t est un pointeur vers un entier.
• Grâce à l'opérateur new, on transforme t en un tableau de 5 entiers grâce à l'instruction.
t=new int[5]; On remplit
ce tableau et on affiche le contenu des 5 cases de ce tableau. On détruit ce tableau en utilisant
l'instruction delete [
]t;
• On crée ensuite un autre tableau comportant cette fois 10 cases, on le remplit, on l'affiche et
on le détruit de la
même manière que précédemment.
La classe string
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
char texte[100]="Ceci est une chaine de caractères.";
char *chaine=NULL;
chaine=malloc(100*sizeof(chaine));
if(chaine==NULL)
{
perror("erreur d'allocation");
return 1;
}
strcpy(chaine,texte );
printf("%s\n",chaine);
free(chaine);
return 0;
}
La classe string
#include <iostream>
#include <string>
Séparateurs
• Par défaut, lorsqu'on saisit une chaîne de caractères en utilisant cin, le séparateur est l'espace
: cela empêche de
saisir une chaîne de caractères comportant une espace.
• La fonction getline(iostream &,string) permet de saisir une chaîne de caractères en utilisant
le passage à la ligne
comme séparateur : notre chaîne de caractères peut alors comporter des espaces.
#include <iostream>
Explications
• Dans cet exemple, la chaîne de caractères s1 est saisie grâce à l'instruction getline(cin,s1) :
cela permet de saisir
au clavier la chaîne s1, la fin de la chaîne est alors indiquée lorsqu'on tape sur ENTREE.
• On saisit ensuite la chaîne s2 et on affiche la concaténation des deux chaînes
Analyse de chaînes
• Nombre de caractères d'une chaîne : size() est une méthode de la classe string qui renvoie le
nombre de
caractères utiles.
• Récupération du i-ième caractère : la méthode const char at(int i) permet de récupérer le i-
1ième caractère. (0 =
1er)
#include <iostream>
#include<string>