Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Ce cours existe en eBook.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !
Apprenez à programmer en C !

Apprenez à programmer en C !

Mis à jour le lundi 11 août 2014
  • 4 semaines
  • Moyen

L'heure est venue pour vous de découvrir les pointeurs. Prenez un grand bol d'air avant car ce chapitre ne sera probablement pas une partie de plaisir. Les pointeurs représentent en effet une des notions les plus délicates du langage C. Si j'insiste autant sur leur importance, c'est parce qu'il est impossible de programmer en langage C sans les connaître et bien les comprendre. Les pointeurs sont omniprésents, nous les avons d'ailleurs déjà utilisés sans le savoir.

Nombre de ceux qui apprennent le langage C titubent en général sur les pointeurs. Nous allons faire en sorte que ce ne soit pas votre cas. Redoublez d'attention et prenez le temps de comprendre les nombreux schémas de ce chapitre.

Un problème bien ennuyeux

Un des plus gros problèmes avec les pointeurs, en plus d'être assez délicats à assimiler pour des débutants, c'est qu'on a du mal à comprendre à quoi ils peuvent bien servir.

Alors bien sûr, je pourrais vous dire : « Les pointeurs sont totalement indispensables, on s'en sert tout le temps, croyez-moi ! », mais je sais que cela ne vous suffira pas.

Je vais donc vous poser un problème que vous ne pourrez pas résoudre sans utiliser de pointeurs. Ce sera en quelque sorte le fil rouge du chapitre. Nous en reparlerons à la fin de ce chapitre et verrons quelle est la solution en utilisant ce que vous aurez appris.

Voici le problème : je veux écrire une fonction qui renvoie deux valeurs. « Impossible » me direz-vous ! En effet, on ne peut renvoyer qu'une valeur par fonction :

int fonction()
{
    return valeur;
}

Si on indique int, on renverra un nombre de type int (grâce à l'instruction return).

On peut aussi écrire une fonction qui ne renvoie aucune valeur avec le mot-clé void :

void fonction()
{

}

Mais renvoyer deux valeurs à la fois… c'est impossible. On ne peut pas faire deux return.

Supposons que je veuille écrire une fonction à laquelle on envoie un nombre de minutes. Celle-ci renverrait le nombre d'heures et minutes correspondantes :

  1. si on envoie 45, la fonction renvoie 0 heure et 45 minutes ;

  2. si on envoie 60, la fonction renvoie 1 heure et 0 minutes ;

  3. si on envoie 90, la fonction renvoie 1 heure et 30 minutes.

Soyons fous, tentons le coup :

#include <stdio.h>
#include <stdlib.h>

/* Je mets le prototype en haut. Comme c'est un tout
petit programme je ne le mets pas dans un .h, mais
en temps normal (dans un vrai programme), j'aurais placé
le prototype dans un fichier .h bien entendu */

void decoupeMinutes(int heures, int minutes);

int main(int argc, char *argv[])
{
    int heures = 0, minutes = 90;

    /* On a une variable minutes qui vaut 90.
    Après appel de la fonction, je veux que ma variable
    "heures" vaille 1 et que ma variable "minutes" vaille 30 */

    decoupeMinutes(heures, minutes);

    printf("%d heures et %d minutes", heures, minutes);

    return 0;
}

void decoupeMinutes(int heures, int minutes)
{
    heures = minutes / 60;  // 90 / 60 = 1
    minutes = minutes % 60; // 90 % 60 = 30
}

Résultat :

0 heures et 90 minutes

Zut, zut, zut et rezut, ça n'a pas marché. Que s'est-il passé ? En fait, quand vous « envoyez » une variable à une fonction, une copie de la variable est réalisée. Ainsi, la variable heures dans la fonction decoupeMinutes n'est pas la même que celle de la fonction main ! C'est simplement une copie !

Votre fonction decoupeMinutes fait son job. À l'intérieur de decoupeMinutes, les variables heures et minutes ont les bonnes valeurs : 1 et 30.

Mais ensuite, la fonction s'arrête lorsqu'on arrive à l'accolade fermante. Comme on l'a appris dans les chapitres précédents, toutes les variables créées dans une fonction sont détruites à la fin de cette fonction. Vos copies de heures et de minutes sont donc supprimées. On retourne ensuite à la fonction main, dans laquelle vos variables heures et minutes valent toujours 0 et 90. C'est un échec !

Bref, vous aurez beau retourner le problème dans tous les sens… vous pouvez essayer de renvoyer une valeur avec la fonction (en utilisant un return et en mettant le type int à la fonction), mais vous n'arriveriez à renvoyer qu'une des deux valeurs. Vous ne pouvez pas renvoyer les deux valeurs à la fois. De plus, vous ne pouvez pas utiliser de variables globales car, comme on l'a vu, cette pratique est fortement déconseillée.

Voilà, le problème est posé. Comment les pointeurs vont-ils nous permettre de le résoudre ?

La mémoire, une question d'adresse

Rappel des faits

Petit flash-back. Vous souvenez-vous du chapitre sur les variables ?

Quelle que soit la réponse, je vous recommande très vivement d'aller relire la première partie de ce chapitre, intitulée « Une affaire de mémoire ». Il y avait un schéma très important que je vous propose ici à nouveau (fig. suivante).

Organisation de la mémoire vive

C'est un peu comme ça qu'on peut représenter la mémoire vive (RAM) de votre ordinateur.

Il faut lire ce schéma ligne par ligne. La première ligne représente la « cellule » du tout début de la mémoire vive. Chaque cellule a un numéro, c'est son adresse (le vocabulaire est très important, retenez-le). La mémoire comporte un grand nombre d'adresses, commençant à l'adresse numéro 0 et se terminant à l'adresse numéro (insérez un très grand nombre ici). Le nombre d'adresses disponibles dépend en fait de la quantité de mémoire dont dispose votre ordinateur.

À chaque adresse, on peut stocker un nombre. Un et UN SEUL nombre. On ne peut pas stocker deux nombres par adresse.

Votre mémoire n'est faite que pour stocker des nombres. Elle ne peut stocker ni lettres ni phrases. Pour contourner ce problème, on a inventé une table qui fait la liaison entre les nombres et les lettres. Cette table dit par exemple : « Le nombre 89 représente la lettre Y ». Nous reviendrons dans un prochain chapitre sur la gestion des caractères ; pour l'instant, nous nous concentrons sur le fonctionnement de la mémoire.

Adresse et valeur

Quand vous créez une variable age de type int par exemple, en tapant ça :

int age = 10;

… votre programme demande au système d'exploitation (Windows, par exemple) la permission d'utiliser un peu de mémoire. Le système d'exploitation répond en indiquant à quelle adresse en mémoire il vous laisse le droit d'inscrire votre nombre.

C'est d'ailleurs justement là un des rôles principaux d'un système d'exploitation : on dit qu'il alloue de la mémoire aux programmes. C'est un peu lui le chef, il contrôle chaque programme et vérifie que ce dernier a l'autorisation de se servir de la mémoire à l'endroit où il le fait.

Revenons à notre variable age. La valeur 10 a été inscrite quelque part en mémoire, disons par exemple à l'adresse n° 4655.
Ce qu'il se passe (et c'est le rôle du compilateur), c'est que le mot age dans votre programme est remplacé par l'adresse 4655 à l'exécution. Cela fait que, à chaque fois que vous avez tapé le mot age dans votre code source, il est remplacé par 4655, et votre ordinateur voit ainsi à quelle adresse il doit aller chercher en mémoire ! Du coup, l'ordinateur se rend en mémoire à l'adresse 4655 et répond fièrement : « La variable age vaut 10 ! ».

On sait donc comment récupérer la valeur de la variable : il suffit tout bêtement de taper age dans son code source. Si on veut afficher l'âge, on peut utiliser la fonction printf :

printf("La variable age vaut : %d", age);

Résultat à l'écran :

La variable age vaut : 10

Rien de bien nouveau jusque-là.

Le scoop du jour

On sait afficher la valeur de la variable, mais saviez-vous que l'on peut aussi afficher l'adresse correspondante ?

Pour afficher l'adresse de la variable, on doit utiliser le symbole %p (le p du mot « pointeur ») dans le printf. En outre, on doit envoyer à la fonction printf non pas la variable age, mais son adresse… Et pour faire cela, vous devez mettre le symbole & devant la variable age, comme je vous avais demandé de le faire pour les scanf, il y a quelque temps, sans vous expliquer pourquoi.

Tapez donc :

printf("L'adresse de la variable age est : %p", &age);

Résultat :

L'adresse de la variable age est : 0023FF74

Ce que vous voyez là est l'adresse de la variable age au moment où j'ai lancé le programme sur mon ordinateur. Oui, oui, 0023FF74 est un nombre, il est simplement écrit dans le système hexadécimal, au lieu du système décimal dont nous avons l'habitude. Si vous remplacez %p par %d, vous obtiendrez un nombre décimal que vous connaissez.

Où je veux en venir avec tout ça ? Eh bien en fait, je veux vous faire retenir ceci :

  • age : désigne la valeur de la variable ;

  • &age : désigne l'adresse de la variable.

Avec age, l'ordinateur va lire la valeur de la variable en mémoire et vous renvoie cette valeur. Avec &age, votre ordinateur vous dit en revanche à quelle adresse se trouve la variable.

Utiliser des pointeurs

Jusqu'ici, nous avons uniquement créé des variables faites pour contenir des nombres. Maintenant, nous allons apprendre à créer des variables faites pour contenir des adresses : ce sont justement ce qu'on appelle des pointeurs.

Mais… Les adresses sont des nombres aussi, non ? Ça revient à stocker des nombres encore et toujours !

C'est exact. Mais ces nombres auront une signification particulière : ils indiqueront l'adresse d'une autre variable en mémoire.

Créer un pointeur

Pour créer une variable de type pointeur, on doit rajouter le symbole * devant le nom de la variable.

int *monPointeur;

Comme je vous l'ai appris, il est important d'initialiser dès le début ses variables, en leur donnant la valeur 0 par exemple. C'est encore plus important de le faire avec les pointeurs !
Pour initialiser un pointeur, c'est-à-dire lui donner une valeur par défaut, on n'utilise généralement pas le nombre 0 mais le mot-clé NULL (veillez à l'écrire en majuscules) :

int *monPointeur = NULL;

Là, vous avez un pointeur initialisé à NULL. Comme ça, vous saurez dans la suite de votre programme que votre pointeur ne contient aucune adresse.

Que se passe-t-il ? Ce code va réserver une case en mémoire comme si vous aviez créé une variable normale. Cependant, et c'est ce qui change, la valeur du pointeur est faite pour contenir une adresse. L'adresse… d'une autre variable.

Pourquoi pas l'adresse de la variable age ? Vous savez maintenant comment indiquer l'adresse d'une variable au lieu de sa valeur (en utilisant le symbole &), alors allons-y ! Ça nous donne :

int age = 10;
int *pointeurSurAge = &age;

La première ligne signifie : « Créer une variable de type int dont la valeur vaut 10 ». La seconde ligne signifie : « Créer une variable de type pointeur dont la valeur vaut l'adresse de la variable age ».

La seconde ligne fait donc deux choses à la fois. Si vous le souhaitez, pour ne pas tout mélanger, sachez qu'on peut la découper en deux temps :

int age = 10;
int *pointeurSurAge; // 1) signifie "Je crée un pointeur"
pointeurSurAge = &age; // 2) signifie "pointeurSurAge contient l'adresse de la variable age"

Vous avez remarqué qu'il n'y a pas de type « pointeur » comme il y a un type int et un type double. On n'écrit donc pas :

pointeur pointeurSurAge;

Au lieu de ça, on utilise le symbole *, mais on continue à écrire int. Qu'est-ce que ça signifie ? En fait, on doit indiquer quel est le type de la variable dont le pointeur va contenir l'adresse. Comme notre pointeur pointeurSurAge va contenir l'adresse de la variable age (qui est de type int), alors mon pointeur doit être de type int* ! Si ma variable age avait été de type double, alors j'aurais dû écrire double *monPointeur.

Vocabulaire : on dit que le pointeur pointeurSurAge pointe sur la variable age.

La fig. suivante résume ce qu'il s'est passé dans la mémoire.

Mémoire, adresses et pointeurs

Dans ce schéma, la variable age a été placée à l'adresse 177450 (vous voyez d'ailleurs que sa valeur est 10), et le pointeur pointeurSurAge a été placé à l'adresse 3 (c'est tout à fait le fruit du hasard).

Lorsque mon pointeur est créé, le système d'exploitation réserve une case en mémoire comme il l'a fait pour age. La différence ici, c'est que la valeur de pointeurSurAge est un peu particulière. Regardez bien le schéma : c'est l'adresse de la variable age !

Ceci, chers lecteurs, est le secret absolu de tout programme écrit en langage C. On y est, nous venons de rentrer dans le monde merveilleux des pointeurs !

Et… ça sert à quoi ?

Ça ne transforme pas encore votre ordinateur en machine à café, certes. Seulement maintenant, on a un pointeurSurAge qui contient l'adresse de la variable age.
Essayons de voir ce que contient le pointeur à l'aide d'un printf :

int age = 10;
int *pointeurSurAge = &age;

printf("%d", pointeurSurAge);
177450

Hum. En fait, cela n'est pas très étonnant. On demande la valeur de pointeurSurAge, et sa valeur c'est l'adresse de la variable age (177450).
Comment faire pour demander à avoir la valeur de la variable se trouvant à l'adresse indiquée dans pointeurSurAge ? Il faut placer le symbole * devant le nom du pointeur :

int age = 10;
int *pointeurSurAge = &age;

printf("%d", *pointeurSurAge);
10

Hourra ! Nous y sommes arrivés ! En plaçant le symbole * devant le nom du pointeur, on accède à la valeur de la variable age.

Si au contraire on avait utilisé le symbole & devant le nom du pointeur, on aurait obtenu l'adresse à laquelle se trouve le pointeur (ici, c'est 3).

Qu'est-ce qu'on y gagne ? On a simplement réussi à compliquer les choses ici. On n'avait pas besoin d'un pointeur pour afficher la valeur de la variable age !

Cette question (que vous devez inévitablement vous poser) est légitime. Après tout, qui pourrait vous en vouloir ? Actuellement l'intérêt n'est pas évident, mais petit à petit, tout au long des chapitres suivants, vous comprendrez que tout cela n'a pas été inventé par pur plaisir de compliquer les choses.

Faites l'impasse sur la frustration que vous devez ressentir (« Tout ça pour ça ? »). Si vous avez compris le principe, c'est l'essentiel. Les choses s'éclairciront d'elles-mêmes par la suite.

À retenir absolument

Voici ce qu'il faut avoir compris et ce qu'il faut retenir pour la suite de ce chapitre :

  • sur une variable, comme la variable age :

    • age signifie : « Je veux la valeur de la variable age »,

    • &age signifie : « Je veux l'adresse à laquelle se trouve la variable age » ;

  • sur un pointeur, comme pointeurSurAge :

    • pointeurSurAge signifie : « Je veux la valeur de pointeurSurAge » (cette valeur étant une adresse),

    • *pointeurSurAge signifie : « Je veux la valeur de la variable qui se trouve à l'adresse contenue dans pointeurSurAge ».

Contentez-vous de bien retenir ces quatre points. Faites des tests et vérifiez que ça marche.
Le schéma de la fig. suivante devrait bien vous aider à situer chacun de ces éléments.

Désignation des variables et pointeurs

Tout cela est fon-da-men-tal. Il faut connaître cela par cœur et surtout le comprendre. N'hésitez pas à lire et relire ce qu'on vient d'apprendre. Je ne peux pas vous en vouloir si vous n'avez pas compris du premier coup et ce n'est pas une honte non plus, d'ailleurs. Il faut en général quelques jours pour bien comprendre et souvent quelques mois pour en saisir toutes les subtilités.

Si vous vous sentez un peu perdus, pensez à ces gens qui sont aujourd'hui de grands gourous de la programmation : aucun d'entre eux n'a compris tout le fonctionnement des pointeurs du premier coup. Et si jamais cette personne existe, il faudra vraiment me la présenter.

Envoyer un pointeur à une fonction

Le gros intérêt des pointeurs (mais ce n'est pas le seul) est qu'on peut les envoyer à des fonctions pour qu'ils modifient directement une variable en mémoire, et non une copie comme on l'a vu.

Comment ça marche ? Il y a en fait plusieurs façons de faire. Voici un premier exemple :

void triplePointeur(int *pointeurSurNombre);

int main(int argc, char *argv[])
{
    int nombre = 5;

    triplePointeur(&nombre); // On envoie l'adresse de nombre à la fonction
    printf("%d", nombre); // On affiche la variable nombre. La fonction a directement modifié la valeur de la variable car elle connaissait son adresse

    return 0;
}

void triplePointeur(int *pointeurSurNombre)
{
    *pointeurSurNombre *= 3; // On multiplie par 3 la valeur de nombre
}
15

La fonction triplePointeur prend un paramètre de type int* (c'est-à-dire un pointeur sur int). Voici ce qu'il se passe dans l'ordre, en partant du début du main :

  1. une variable nombre est créée dans le main. On lui affecte la valeur 5. Ça, vous connaissez ;

  2. on appelle la fonction triplePointeur. On lui envoie en paramètre l'adresse de notre variable nombre ;

  3. la fonction triplePointeur reçoit cette adresse dans pointeurSurNombre. À l'intérieur de la fonction triplePointeur, on a donc un pointeur pointeurSurNombre qui contient l'adresse de la variable nombre ;

  4. maintenant qu'on a un pointeur sur nombre, on peut modifier directement la variable nombre en mémoire ! Il suffit d'utiliser *pointeurSurNombre pour désigner la variable nombre ! Pour l'exemple, on fait un simple test : on multiplie la variable nombre par 3 ;

  5. de retour dans la fonction main, notre nombre vaut maintenant 15 car la fonction triplePointeur a modifié directement la valeur de nombre.

Bien sûr, j'aurais pu faire un simple return comme on a appris à le faire dans le chapitre sur les fonctions. Mais l'intérêt, là, c'est que de cette manière, en utilisant des pointeurs, on peut modifier la valeur de plusieurs variables en mémoire (on peut donc « renvoyer plusieurs valeurs »). Nous ne sommes plus limités à une seule valeur !

Quel est l'intérêt maintenant d'utiliser un return dans une fonction si on peut se servir des pointeurs pour modifier des valeurs ?

Ça dépendra de vous et de votre programme. C'est à vous de décider. Il faut savoir que les return sont bel et bien toujours utilisés en C. Le plus souvent, on s'en sert pour renvoyer ce qu'on appelle un code d'erreur : la fonction renvoie 1 (vrai) si tout s'est bien passé, et 0 (faux) s'il y a eu une erreur pendant le déroulement de la fonction.

Une autre façon d'envoyer un pointeur à une fonction

Dans le code source qu'on vient de voir, il n'y avait pas de pointeur dans la fonction main. Juste une variable nombre. Le seul pointeur qu'il y avait vraiment était dans la fonction triplePointeur (de type int*).

Il faut absolument que vous sachiez qu'il y a une autre façon d'écrire le code précédent, en ajoutant un pointeur dans la fonction main :

void triplePointeur(int *pointeurSurNombre);

int main(int argc, char *argv[])
{
    int nombre = 5;
    int *pointeur = &nombre; // pointeur prend l'adresse de nombre

    triplePointeur(pointeur); // On envoie pointeur (l'adresse de nombre) à la fonction
    printf("%d", *pointeur); // On affiche la valeur de nombre avec *pointeur

    return 0;
}

void triplePointeur(int *pointeurSurNombre)
{
    *pointeurSurNombre *= 3; // On multiplie par 3 la valeur de nombre
}

Comparez bien ce code source avec le précédent. Il y a de subtiles différences et pourtant le résultat est strictement le même :

15

Ce qui compte, c'est d'envoyer l'adresse de la variable nombre à la fonction. Or, pointeur vaut l'adresse de la variable nombre, donc c'est bon de ce côté ! On le fait seulement d'une manière différente en créant un pointeur dans la fonction main.
Dans le printf (et c'est juste pour l'exercice), j'affiche le contenu de la variable nombre en tapant *pointeur. Notez qu'à la place, j'aurais pu écrire nombre : le résultat aurait été identique car *pointeur et nombre désignent la même chose dans la mémoire.

Dans le programme « Plus ou Moins », nous avons utilisé des pointeurs sans vraiment le savoir. C'était en fait en appelant la fonction scanf. En effet, cette fonction a pour rôle de lire ce que l'utilisateur a entré au clavier et de renvoyer le résultat. Pour que la fonction puisse modifier directement le contenu de votre variable afin d'y placer la valeur tapée au clavier, elle a besoin de l'adresse de la variable :

int nombre = 0;
scanf("%d", &nombre);

La fonction travaille avec un pointeur sur la variable nombre et peut ainsi modifier directement le contenu de nombre.
Comme on vient de le voir, on pourrait créer un pointeur qu'on enverrait à la fonction scanf :

int nombre = 0;
int *pointeur = &nombre;
scanf("%d", pointeur);

Attention à ne pas mettre le symbole & devant pointeur dans la fonction scanf ! Ici, pointeur contient lui-même l'adresse de la variable nombre, pas besoin de mettre un & ! Si vous faisiez ça, vous enverriez l'adresse où se trouve le pointeur : or c'est de l'adresse de nombre dont on a besoin.

Qui a dit : "Un problème bien ennuyeux" ?

Le chapitre est sur le point de s'achever, il est temps de retrouver notre fil rouge. Si vous avez compris ce chapitre, vous devriez être capables de résoudre le problème, maintenant. Qu'est-ce que vous en dites ? Essayez !

Voici la solution pour comparer :

void decoupeMinutes(int* pointeurHeures, int* pointeurMinutes);

int main(int argc, char *argv[])
{
    int heures = 0, minutes = 90;

    // On envoie l'adresse de heures et minutes
    decoupeMinutes(&heures, &minutes);

    // Cette fois, les valeurs ont été modifiées !
    printf("%d heures et %d minutes", heures, minutes);

    return 0;
}

void decoupeMinutes(int* pointeurHeures, int* pointeurMinutes)
{
    /* Attention à ne pas oublier de mettre une étoile devant le nom
    des pointeurs ! Comme ça, vous pouvez modifier la valeur des variables,
    et non leur adresse ! Vous ne voudriez pas diviser des adresses,
    n'est-ce pas ? ;o) */
    *pointeurHeures = *pointeurMinutes / 60;
    *pointeurMinutes = *pointeurMinutes % 60; 
}

Résultat :

1 heures et 30 minutes

Rien ne devrait vous surprendre dans ce code source. Toutefois, comme on n'est jamais trop prudent, je vais râbacher une fois de plus ce qui se passe dans ce code afin d'être certain que tout le monde me suit bien. C'est un chapitre important, vous devez faire beaucoup d'efforts pour comprendre : je peux donc bien en faire moi aussi pour vous !

  1. Les variables heures et minutes sont créées dans le main.

  2. On envoie à la fonction decoupeMinutes l'adresse de heures et minutes.

  3. La fonction decoupeMinutes récupère ces adresses dans des pointeurs appelés pointeurHeures et pointeurMinutes. Notez que là encore, le nom importe peu. J'aurais pu les appeler h et m, ou même encore heures et minutes. Je ne l'ai pas fait car je ne veux pas que vous risquiez de confondre avec les variables heures et minutes du main, qui ne sont pas les mêmes.

  4. La fonction decoupeMinutes modifie directement les valeurs des variables heures et minutes en mémoire car elle possède leurs adresses dans des pointeurs. La seule contrainte, un peu gênante je dois le reconnaître, c'est qu'il faut impérativement mettre une étoile devant le nom des pointeurs si on veut modifier la valeur de heures et de minutes. Si on n'avait pas fait ça, on aurait modifié l'adresse contenue dans les pointeurs, ce qui n'aurait servi… à rien.

En résumé

  • Chaque variable est stockée à une adresse précise en mémoire.

  • Les pointeurs sont semblables aux variables, à ceci près qu'au lieu de stocker un nombre ils stockent l'adresse à laquelle se trouve une variable en mémoire.

  • Si on place un symbole & devant un nom de variable, on obtient son adresse au lieu de sa valeur (ex. : &age).

  • Si on place un symbole * devant un nom de pointeur, on obtient la valeur de la variable stockée à l'adresse indiquée par le pointeur.

  • Les pointeurs constituent une notion essentielle du langage C, mais néanmoins un peu complexe au début. Il faut prendre le temps de bien comprendre comment ils fonctionnent car beaucoup d'autres notions sont basées dessus.

Exemple de certificat de réussite
Exemple de certificat de réussite