Point Eurs
Point Eurs
Point Eurs
Les pointeurs sont des variables contenant des adresses. Ils permettent donc de faire de
l'adressage indirect. Ainsi :
int* px;
déclare une variable px qui est un pointeur sur un entier. La variable pointée par px est notée *px.
Inversement, pour une variable
int x;
on peut accéder à l'adresse de x par la notation &x. Ainsi, je peux écrire :
px = &x;
ou
x = *px;
Voici une autre manière d'écrire la fonction swap() qui échange deux entiers, cette fois-ci en
passant par des pointeurs :
swap(int* px, int* py)
{
int temp; // variable temporaire
temp = *px;
*px = *py;
*py = temp;
}
et pour échanger deux paramètres on appellera :
int a,b;
swap(&a,&b);
Attention : un des pièges les plus classiques en C++ est celui du pointeur non initialisé. Le fait
d'avoir déclaré une variable de type pointeur ne suffit pas pour pouvoir déréférencer ce pointeur.
Encore faut-il qu'il pointe sur une << case >> mémoire valide. Pour reprendre l'exemple
précédent, si j'écris
int* px;
*px = 3;
j'ai de très fortes chances d'avoir une erreur à l'exécution, puisque px ne désigne pas une adresse
mémoire dans laquelle j'ai le droit d'écrire. Ce n'est qu'après avoir écrit par exemple px = &x;
comme dans l'exemple ci-dessus que l'instruction *px = 3; devient valide.
Les tableaux
pa = &a[0];
ou
pa = a;
Mais attention, il y a des différences dues au fait que a est une adresse constante alors que pa est
une variable. Ainsi, on peut écrire
1
pa = a;
mais il n'est pas valide d'écrire
a = pa;
Quand on veut passer un tableau en paramètre formel d'une fonction, il est équivalent d'écrire :
void funct(int tab[])
ou
void funct(int* tab)
car dans les deux cas on passe une adresse.
Remarque : comme en Java, les indices, qui correspondent à des déplacements, commencent
toujours à 0.
Voyons maintenant comment on peut utiliser cette équivalence entre pointeurs et tableaux pour
parcourir un tableau sans recalculer systématiquement l'adresse du point courant. Le problème est
de calculer la moyenne d'une matrice 200 × 200 d'entiers.
int tab[200][200];
long int moyenne=0;
register int* p = tab;
int tab[200][200];
long int moyenne=0;
register int* p = tab;
register int* stop = p + 200 * 200;
for ( ; p < stop ; ) /*on ne fait plus p++ ici*/
moyenne += *p++; /*on accède à la valeur pointée
par p, puis on l'incrémente*/
moyenne /= 40000;
Mais attention : le programme devient ainsi à peu près illisible, et je déconseille d'abuser de telles
pratiques, qui ne sont justifiées que dans des cas extrêmes, où l'optimisation du code est un
impératif.
Notez aussi qu'il est exclu de réaliser des << affectations globales >> sur les tableaux, autrement
que par le mécanisme des pointeurs (pas de recopie globale).
L'allocation et la libération dynamique de mémoire sont réalisées par les opérateurs new et delete.
Une expression comprenant l'opération new retourne un pointeur sur l'objet alloué. On écrira
donc par exemple :
int* pi = new int;
Pour allouer un tableau dynamique, on indique la taille souhaitée comme suit :
2
int* tab = new int[20];
Attention : Contrairement à Java, C++ n'a pas de mécanisme de ramasse-miettes ; c'est donc à
vous de libérer la mémoire dynamique dont vous n'avez plus besoin
delete pi;
delete [] tab;
L'exemple ci-dessous reprend et illustre l'utilisation de new et de delete pour des variables et des
tableaux :
main()
{
int *pi = new int;
int *tab = new int[10];
Un certain nombre d'opérations arithmétiques sont possibles sur les pointeurs, en particulier
l'incrémentation.
Tout d'abord, on peut leur ajouter ou leur soustraire un entier n. Cela revient à ajouter à l'adresse
courante n fois la taille d'un objet du type pointé. Ainsi, dans un tableau, comme nous l'avons vu,
l'instruction p++ (qui est la même chose que p = p+1) fait pointer p sur la case suivante dans le
tableau, c'est-à-dire que l'adresse est incrémentée de la taille (en octets) du type pointé.
On peut comparer deux pointeurs avec les opérateurs relationnels. Évidemment, cela n'a de sens
que s'ils << pointent >> dans une même zone (tableau par exemple).
Enfin, on peut soustraire deux pointeurs. Le résultat est un entier indiquant le nombre de << cases
>> de la taille du type pointé entre les deux pointeurs. Là encore, cela n'a de signification que si
les deux pointeurs pointent dans la même zone contiguë.
On pourrait encore dire beaucoup sur les pointeurs. Nous nous contentons ici de signaler
quelques points que le lecteur intéressé par la poétique de C++ pourra approfondir dans la
littérature appropriée :
C++ propose deux manières de représenter les chaînes de caractères : celle héritée de C
et le type string de la bibliothèque standard C++. Nous vous conseillons bien entendu
d'utiliser ce dernier.
3
Mais comme vous risquez d'être parfois confrontés à des chaînes de caractères << à
l'ancienne >> (c'est-à-dire à la mode C), sachez que ce sont des tableaux de caractères
terminés par le caractère nul (de code 0, et noté comme l'entier 0 ou le caractère \0).
On peut bien sûr utiliser des tableaux de pointeurs, des pointeurs de pointeurs, des
pointeurs de pointeurs de pointeurs, etc. Bref, vous voyez ce que je veux dire...
On peut même manipuler des tableaux de fonctions, des pointeurs de fonctions, ce qui
permet d'appeler plusieurs fonctions différentes en se servant du même pointeur.
La bibliothèque d'entrées-sorties
Nous ne prétendons pas couvrir dans ce polycopié les très nombreuses fonctionnalités couvertes
par la bibliothèque standard C++. Cependant, il nous semble utile de donner quelques indications
sur les entrées-sorties.
Les opérateurs << et >> sont redéfinis pour permettre des écritures et lectures aisées :
#include <iostream>
#include <string>
4
Les fichiers
Les entrées-sorties sur les fichiers sont également réalisées avec des flots en C++. Ce type
d'opérations nécessite l'inclusion de l'en-tête fstream en plus de l'en-tête iostream. Les deux
grandes classes permettant de réaliser ces opérations sont :
La classe ofstream : cette classe est dédiée aux écritures réalisées dans des fichiers. La
classe ofstream est dérivée de la classe ostream et bénéficie donc de toutes les méthodes
définies dans cette classe. Voici un exemple d'utilisation :
#include <iostream>
#include <fstream>
int main()
{
// Deux modes d'ouverture sont possibles :
// - ios::out -> création, fichier écrasé si existant
// - ios::app -> ajout en fin de fichier
if (!fichierSortie) {
cerr << "Problème d'ouverture de fichier" << endl;
exit(1);
}
// Fermeture du fichier
fichierSortie.close();
}
La classe ifstream : cette classe est dédiée aux lectures réalisées dans des fichiers. La
classe ifstream est dérivée de la classe istream et bénéficie donc de toutes le méthodes
définies dans cette classe. Voici un exemple d'utilisation :
#include <iostream>
#include <fstream>
int main()
{
// Ouverture du fichier
char buf[1024];
while (!fichierEntree.eof()) {
fichierEntree.getline(buf, 1024);
cout << buf << endl;
}
// Fermeture du fichier
fichierEntree.close();
}