Menu

Les pointeurs en C : guide complet avec exemples

Un pointeur est une variable qui stocke une adresse mémoire. Cette page construit l'idée depuis la base - les opérateurs & et *, la déclaration et le déréférencement, pourquoi le type d'un pointeur compte, et la fonction swap() qui montre pourquoi les pointeurs existent.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Toute variable de votre programme vit quelque part en mémoire, et chaque « quelque part » a un numéro - son adresse. Un pointeur n'est qu'une variable qui stocke l'un de ces numéros. C'est toute l'idée. Tout le reste de cette page n'est que syntaxe et conséquences.

Si les pointeurs comptent tant en C, c'est que le C remet à chaque fonction une copie de ses arguments. Une fonction à qui l'on donne le nombre 7 peut modifier sa copie autant qu'elle veut ; la variable de l'appelant ne bouge jamais. Donnez plutôt une adresse à la fonction, et elle peut remonter et changer l'original. Les tableaux, les chaînes, la mémoire dynamique et toute structure de données que vous construirez un jour en C reposent sur cette unique capacité.

La mémoire, les adresses, et pourquoi tout en a une

Quand vous écrivez int score = 42;, le compilateur réserve quelques octets de mémoire et leur donne un nom utilisable. Ces octets ont aussi une adresse numérique. Voyez la mémoire comme une très longue rue de maisons numérotées : score est la famille qui habite au numéro 0x7ffd4c2a - le nom est pour vous, le numéro pour la machine.

L'opérateur & demande ce numéro :

Lancez-le. Les trois adresses diffèrent parce que les trois variables occupent des octets différents, et elles changeront d'une exécution à l'autre - c'est normal, et c'est pourquoi on n'écrit jamais une adresse en dur. Notez le cast (void *) : %p est spécifié pour prendre un void *, et passer tout autre type de pointeur est techniquement un comportement indéfini. Faites-en une habitude.

L'imprévisibilité des adresses n'est pas un problème, car vous ne vous souciez presque jamais de ce qu'est une adresse. Vous vous souciez de l'avoir.

Déclarer un pointeur

Une déclaration de pointeur nomme le type pointé :

int    *p;      // p peut contenir l'adresse d'un int
double *q;      // q peut contenir l'adresse d'un double
char   *name;   // name peut contenir l'adresse d'un char

Lisez int *p; comme « *p est un int » - déréférencer p vous donne un int. Cette lecture survit à toutes les déclarations compliquées que le C pourra vous envoyer plus tard.

L'endroit où vous placez le * est un choix de style que le compilateur ignore : int* p, int *p et int * p déclarent tous la même chose. La plupart du code C écrit int *p, et il y a une raison pratique :

int* a, b;   // a est un pointeur sur int ; b est un int SIMPLE, pas un pointeur
int *a, *b;  // les deux sont des pointeurs - le * s'attache au nom, pas au type

Le * s'attache au déclarateur, pas au type. L'écrire à côté du nom garde cela visible.

& et * : les deux opérateurs

Deux opérateurs font tout le travail, et ils sont exactement opposés.

  • &x - adresse-de. Produit l'adresse où vit x.
  • *p - déréférencement. Va à l'adresse contenue dans p et vous donne l'objet qui y vit.

Les deux dernières lignes sont tout l'intérêt des pointeurs. Rien n'a été affecté directement à age, et pourtant age a changé - parce que *p = 31 signifie « stocke 31 à l'adresse que p contient », et cette adresse est celle de age.

Voici l'image en mémoire :

      p                        age
 +------------+           +----------+
 | 0x7ffd1c40 | --------> |    31    |
 +------------+           +----------+
 a 0x7ffd1c38             a 0x7ffd1c40

 p    -> l'adresse 0x7ffd1c40
 *p   -> l'int qui y est stocke, 31
 &age -> 0x7ffd1c40, la meme adresse que p contient
 &p   -> 0x7ffd1c38, ou p lui-meme vit

Remarquez la dernière ligne : le pointeur est lui-même une variable quelque part en mémoire, il a donc sa propre adresse. Ce n'est pas une question piège, c'est juste de la cohérence - et c'est ainsi que fonctionnent les pointeurs sur pointeurs (int **).

Parce que & et * s'annulent, *&age n'est que age, et &*p n'est que p.

Pourquoi le type d'un pointeur compte

Un pointeur stocke une adresse, et une adresse n'est qu'un nombre - alors pourquoi le compilateur insiste-t-il pour savoir s'il pointe sur un int ou un double ? Deux raisons :

  1. Combien d'octets lire. Déréférencer un int * lit 4 octets (typiquement) ; déréférencer un double * en lit 8. Sans le type, *p n'aurait aucun sens.
  2. Comment interpréter ces octets. Le même motif de bits est un nombre en tant qu'int et un tout autre en tant que float.

La taille du pointeur lui-même, en revanche, ne dépend pas de ce qu'il pointe - une adresse est une adresse :

Sur une machine 64 bits, les trois pointeurs font 8 octets, tandis que les choses qu'ils pointent font 4, 8 et 1. Le type pointé est ce qui donne un sens à *p et à l'arithmétique de pointeurs.

Affecter entre types de pointeurs est une erreur dont le compilateur se plaindra, et à raison - pi = &d; vous préparerait à lire 4 octets d'un double et à appeler le résultat un entier.

La motivation canonique : swap()

Voici la fonction que tout cours de C convoque, parce qu'elle échoue exactement de la façon qui explique les pointeurs.

La fonction annonce qu'elle les a échangés - et main annonce qu'il ne s'est rien passé. broken_swap a reçu des copies de x et y. Elle a parfaitement échangé ses copies, puis ces copies ont cessé d'exister au retour de la fonction.

Donnez-lui plutôt les adresses :

Maintenant a et b sont toujours des copies - des copies de deux adresses - mais une copie d'adresse pointe exactement la même variable que l'originale. *a est x, d'où que la fonction soit appelée.

C'est le motif derrière toute fonction C qui modifie les données de son appelant, et derrière les paramètres de fonction en général : le C n'a pas de passage par référence, donc vous passez un pointeur et vous le déréférencez. C'est aussi pourquoi scanf a besoin d'un & :

int n;
scanf("%d", &n);   // scanf doit pouvoir ecrire dans n, il lui faut donc son adresse

Renvoyer plus d'une valeur

Une fonction C renvoie une valeur. Les pointeurs sont la façon de contourner cela : passez des adresses pour les résultats supplémentaires.

La valeur de retour porte l'état et les pointeurs portent les résultats. Cette séparation est partout dans la bibliothèque standard du C et dans les API des systèmes d'exploitation.

Les pointeurs sur pointeurs

Puisqu'un pointeur est une variable, vous pouvez prendre son adresse, et le type de cette adresse est « pointeur sur pointeur sur int » :

  pp              p               value
+------+       +------+        +------+
| &p   | ----> | &val | -----> |   8  |
+------+       +------+        +------+

Vous rencontrerez le ** pour de vrai quand une fonction doit changer un pointeur que l'appelant détient - par exemple, un allocateur qui pointe le pointeur de l'appelant sur de la mémoire fraîche - et dans le char *argv[] des arguments de ligne de commande.

Quatre erreurs à connaître avant de les commettre

Utiliser un pointeur qui n'a jamais été pointé nulle part. Un pointeur non initialisé contient les octets qui traînaient là. Le déréférencer lit ou écrit une adresse aléatoire.

int *p;      // p contient du garbage
*p = 10;     // comportement indefini - probablement un plantage

Initialisez chaque pointeur, avec une vraie adresse ou avec NULL. Voir les pointeurs nuls pour la discipline autour de cela.

Confondre *p et p. p = 5; met le pointeur à l'adresse 5 (absurde) ; *p = 5; stocke 5 là où il pointe. Le compilateur avertira sur la première, et les avertissements méritent d'être lus.

Oublier le & dans scanf. scanf("%d", n) passe la valeur de n comme adresse. Cela compile avec un avertissement puis écrit dans la mémoire que ce nombre désigne.

Renvoyer l'adresse d'une variable locale. La locale disparaît à l'instant où la fonction retourne ; l'adresse est un reçu pour un bien démoli.

int *bad(void) {
    int local = 42;
    return &local;   // l'appelant recoit un pointeur pendouillant
}

Si vous avez besoin de mémoire qui survive à l'appel, allouez-la - c'est à cela que sert la mémoire dynamique.

Où aller ensuite

Les pointeurs ne s'arrêtent pas à « contenir une adresse ». Ajouter à un pointeur le fait avancer en mémoire par unités de son type, ce qui est le sujet de l'arithmétique de pointeurs ; et un nom de tableau en C se dégrade en pointeur sur son premier élément, c'est pourquoi pointeurs et tableaux est effectivement le même sujet une fois les deux connus. Lisez ces deux pages ensuite, et le reste du C cesse de ressembler à de la magie.

Questions fréquentes

Qu'est-ce qu'un pointeur en C ?

Un pointeur est une variable dont la valeur est une adresse mémoire - l'emplacement d'un autre objet en mémoire. int *p = &age; fait que p contient l'adresse de age, et *p lit ou écrit alors la variable age à travers cette adresse.

Quelle est la différence entre * et & en C ?

&x est l'opérateur adresse-de : il produit l'adresse où vit x. *p est l'opérateur de déréférencement : il va à l'adresse stockée dans p et vous donne l'objet qui s'y trouve. Ils s'annulent, donc *&x n'est que x.

Pourquoi le * apparaît-il deux fois - dans la déclaration et à l'usage du pointeur ?

Ce sont deux usages différents du même symbole. Dans int *p; le * fait partie du type et dit « p est un pointeur sur int ». Dans *p = 5; le * est l'opérateur de déréférencement et dit « stocke 5 à l'adresse que p contient ». Lire la déclaration comme « *p est un int » donne du sens aux deux.

Pourquoi ai-je besoin des pointeurs en C ?

Le C passe chaque argument par valeur, donc une fonction ne peut jamais changer une variable de l'appelant à moins qu'on ne lui donne l'adresse. Les pointeurs permettent aussi de parcourir des tableaux efficacement, de renvoyer plus d'un résultat, de construire des structures chaînées et d'utiliser de la mémoire allouée à l'exécution avec malloc.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER