Parcours
/
C
C
/
Programme
/
Pointeurs
Po

Pointeurs en C

{one: "%{count} exercice", many: "%{count} d'exercices", other: "%{count} exercices"}

À propos de Pointeurs

Un pointeur est une variable ou une constante dont la valeur est l'adresse d'une autre variable ou constante. Les pointeurs sont une fonctionnalité puissante et potentiellement dangereuse du C. Il peut être difficile de comprendre les pointeurs.

int i = 5;

Quand on affecte une valeur à une variable, on peut ensuite poser deux questions.

Qui es-tu ?

Je suis i.

Que possèdes-tu ?

Je possède l'entier 5.

On peut aussi poser une troisième question.

Où es-tu ?

Je suis à 0x6dfed4.

Attends, quoi ?

Les variables et les constantes stockent leur valeur dans un emplacement mémoire. Cet emplacement en mémoire est appelé l'adresse de la variable ou de la constante.

L'opérateur d'adresse et l'opérateur d'indirection (partie 1)

Pour obtenir l'adresse d'une variable, on utilise l'opérateur d'adresse, qui est &.

int i = 5;
int *i_address = &i;

Attends, que signifie ce * devant i_address ? Est-ce que ça ne veut pas dire « multiplier » ?

Un type suivi d'un * puis d'un identifiant est la façon de déclarer un pointeur vers une variable de ce type. Le * est appelé opérateur d'indirection parce qu'il se réfère indirectement à la valeur dont le pointeur détient l'adresse.

D'accord, mais pourquoi créer une variable juste pour contenir l'adresse d'une autre variable ?

Et d'ailleurs, pourquoi s'intéresser à l'adresse d'une variable ?

Eh bien, on se soucie de l'endroit où se trouvent les variables lorsqu'elles sont dans un tableau.

Tableaux et pointeurs

Quand on crée un tableau comme ceci

int my_ints[] = {1, 2};

La valeur de my_ints est en fait son adresse, qui se trouve aussi être la même que l'adresse de son premier élément. L'adresse de son deuxième élément sera l'adresse du premier élément plus la taille d'un int sur le système.

Pour mieux le voir, essaie d'exécuter ce qui suit

#include <stdio.h>

int main() {
    // my_int's value is its address
    int my_ints[] = {1, 2};
    
    // since my_ints is already an address
    // we don't need the address operator to make a pointer from it.
    // a pointer to my_ints holds the address of my_ints
    int *my_ptr = my_ints;
    
    // the address of my_ints and the value of my_ptr are the same
    printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
    
    // the address of the first element is the same as the values in the line above
    printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
    
    // the address of the second element has increased by the size of an int
    printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); // 
    
    return 0;
}

L'arithmétique des pointeurs, ou pourquoi les indices des tableaux commencent à 0

L'exemple précédent montre pourquoi l'indexation des tableaux commence à 0 au lieu de 1. L'indice dans un tableau est multiplié par la taille du type puis ajouté à l'adresse de départ du tableau. Comme le premier élément coïncide avec le début du tableau, on multiplie le type par 0 et on l'ajoute à l'adresse de départ pour obtenir le premier élément. Pour obtenir le deuxième élément, on multiplie le type par 1 et on l'ajoute à l'adresse de départ. Si le type fait quatre octets de long, l'adresse du deuxième élément sera quatre octets plus haut que celle du premier élément.

C'est aussi pour cela que les pointeurs sont déclarés avec un type précis. Si un pointeur ne savait pas vers quel type il pointe, il ne saurait pas de combien d'octets avancer quand on lui ajoute 1. Ajouter 1 à un pointeur ne signifie pas forcément qu'on ajoute un octet à l'adresse. Cela signifie qu'on ajoute 1 fois la longueur de son type en octets.

Mais ce n'est pas tout ! Tu ne croyais quand même pas qu'on en avait fini avec * ?

L'opérateur d'indirection (partie 2)

Maintenant qu'on comprend comment les adresses fonctionnent avec l'indexation des tableaux, à quoi bon si on ne récupère pas la valeur stockée à une adresse ? Comment fait-on ? Pour obtenir la valeur stockée à une adresse, on utilise l'opérateur d'indirection, qui, comme tu te le rappelles peut-être, est *. Dans cette situation, l'opérateur d'indirection est aussi parfois appelé opérateur de déréférencement, parce qu'il déréférence la valeur dont le pointeur détient l'adresse. L'appeler opérateur de déréférencement lorsqu'il sert à déréférencer permet de le distinguer de l'opérateur d'indirection utilisé pour déclarer un pointeur, mais techniquement, le * porte le nom d'opérateur d'indirection dans les deux cas.

Allez ! Tu plaisantes ? Combien d'utilisations a donc le * ?! La multiplication, la déclaration d'un pointeur, et maintenant le déréférencement ! C'est déroutant ! Et puis, qu'est-ce que le déréférencement, au juste ?

Un exemple montre comment utiliser l'opérateur d'indirection pour déréférencer

#include <stdio.h>

int main() {
    // define the variable
    int my_ints[] = {1, 2};
    
    // define the pointer from the address of the variable
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
    printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
    
    // prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
    printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
    
    return 0;
}

Utiliser des parenthèses peut aider à lever toute ambiguïté sur les intentions quand on fait de l'arithmétique de pointeurs. L'exemple suivant risque de ne pas afficher ce que le programmeur voulait

#include <stdio.h>

int main() {
    int my_ints[] = {1, 42};
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *++my_ptr is 42.
    printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
    
    // prints: my_ints[1] is 43. ++*my_ptr++ is 43.
    printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);

    return 0;
}

Le * et le ++ ont la même priorité, mais ils s'évaluent de droite à gauche. Sur la première ligne, my_ptr est d'abord incrémenté vers le deuxième élément, qui est ensuite déréférencé. Sur la deuxième ligne, my_ptr, qui a été incrémenté vers le deuxième élément sur la première ligne, est d'abord déréférencé, puis la valeur déréférencée est incrémentée. Si on ajoute cette ligne à l'exemple ci-dessus

// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);

Dans cette ligne, on a incrémenté le pointeur au-delà du tableau. On a dépassé le tableau et on pointe vers les quatre octets suivants en mémoire, que le pointeur interprète comme un int, mais en réalité, ces octets pourraient appartenir à une autre variable ou à d'autres variables. Ces quatre octets pourraient faire partie d'un double ou être quatre char. Le problème, c'est qu'on ne devrait même pas les regarder avec notre pointeur.

On aurait pu utiliser

// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);

Comme l'indexation des tableaux est implémentée à l'aide d'un pointeur, le même danger de dépassement du tableau s'applique.

Et comme une string est un tableau de char, on peut aussi utiliser des pointeurs avec des strings. Une string est souvent déclarée avec la syntaxe des pointeurs, comme ceci

char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";

Une question, cependant. Pourquoi met-on l'étoile à côté de l'identifiant ? Pourquoi ne pas la mettre à côté du type, puisqu'il s'agit d'un pointeur vers ce type ? Parce que ceci

int* my_ptr1, my_ptr2_that_is_really_an_int;

ne déclare pas deux pointeurs vers un int, mais un pointeur vers un int et un int. Pour déclarer deux pointeurs vers un int, il faudrait écrire

int *my_ptr1, *my_ptr2;

Il est donc préférable de placer le * près de l'identifiant pour minimiser l'ambiguïté. Ou de ne déclarer qu'un seul identifiant par ligne. Et si tu n'as pas encore eu ta dose de *, bienvenue dans...

Pointeurs vers des pointeurs

Il est courant de voir des tableaux de strings représentés comme ceci

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    printf("%s\n", *passed_in_strings);
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char *my_strings[] = {"One string", "Two string"};
    print_strings(my_strings);
    return 0;
}

Cela peut ne pas être évident, mais le tableau de strings est déclaré et initialisé comme un pointeur vers un tableau de tableaux de char. Quand le tableau de tableaux de char est passé à print_strings(), la signature le décrit comme un pointeur vers un ou plusieurs pointeurs vers char.

Alors comment savoir si char ** est vraiment un pointeur vers un tableau de pointeurs vers des tableaux de char, et non un pointeur vers un pointeur vers un seul char ?

On ne le sait pas.

C'est l'une des nombreuses choses qui peuvent rendre l'utilisation des pointeurs déroutante. Ce qui suit peut compiler

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    // prints: A (followed by gibberish)
    printf("%s\n", *passed_in_strings);
    // prints gibberish
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char my_char = 'A';
    char *my_ptr = &my_char;
    char **my_ptr_to_ptr = &my_ptr;
    print_strings(my_ptr_to_ptr);
    return 0;
}

Mais print_strings() n'est pas utilisée comme prévu. Utiliser une signature différente n'aide pas. En changeant la signature pour celle-ci

void print_strings(char *passed_in_strings[]) {
    // code snipped
}

le code peut quand même compiler et afficher des choses incompréhensibles. Mais il n'y a vraiment aucun intérêt à passer un pointeur vers un pointeur vers un seul char. Il y a de fortes chances que, quand tu vois char **, il s'agisse d'un pointeur vers un tableau de pointeurs vers des tableaux de char.

On a vu un pointeur passé à une fonction. Mais pourquoi passer un pointeur à une fonction ?

Passer un pointeur à une fonction

En C, les arguments sont passés aux fonctions par valeur. Cela signifie que la valeur de l'argument est copiée, et que cette copie est locale à la fonction. Quand la fonction se termine, la valeur copiée disparaît aussi.

Regarde ce qui se passe quand on essaie de modifier des valeurs à l'intérieur d'une fonction

#include <stdio.h>

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    //prints:  In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", a, b);
}

int main() {
    int a = 3, b = 4;
    swap(a, b);
    // prints: After 'swap', a is 3 and b is 4
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

C'est parce que, même si les variables portent les mêmes noms, les a et b de swap() ne sont pas les mêmes a et b que ceux de main(). Ce sont des copies des valeurs de a et b. Peut-on modifier swap pour qu'elle fonctionne comme voulu ?

Oui ! On peut passer les adresses de a et b sous forme de pointeurs. Mais à quoi bon, puisque la fonction copie les pointeurs ? C'est utile, parce qu'une adresse copiée reste la même adresse. En déréférençant l'adresse, on peut accéder à la valeur d'origine telle qu'elle a été définie dans main().

#include <stdio.h>

// pass in pointers to a and b
void swap(int *a, int *b) {
    // set temp to value at a's dereferenced address
    int temp = *a;

    // set a's value to value at b's dereferenced address
    *a = *b;

    // set b's value to temp
    *b = temp;

    // prints: In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}

int main() {
    int a = 3, b = 4;
    swap(&a, &b);
    // prints: After 'swap', a is 4 and b is 3.
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

Ça fait beaucoup de * dans swap maintenant, mais elle fonctionne désormais comme prévu.

On peut lire l'opérateur d'indirection comme « valeur à ». Ainsi, *a est la valeur à l'adresse a. Dans quelle mesure cela fonctionne-t-il avec les strings ?

Pointeurs vers char

On sait que la valeur d'une variable tableau est l'adresse où commence le tableau. Et on sait qu'on peut définir une string soit comme un tableau de char, char my_string[], soit comme un pointeur vers char, char *. Quelle est la différence ?

Dans ce code, on passe un argument char * à describe_me(). L'argument adjective est défini comme un tableau de char et est accepté par la fonction comme un pointeur vers char.

#include <stdio.h>
#include <string.h>

// takes pointer to char
void describe_me(char *adjective){
    strcpy(adjective, "evil");
    // prints: I am evil.
    printf("I am %s.\n", adjective);
}

int main() {
    // defined as a char array
    char adjective[] = "good";
    describe_me(adjective);
    // prints: I am really evil.
    printf("I am really %s.\n", adjective);
    
    return 0;
}

On parvient à modifier adjective de façon persistante, ce qui montre que le tableau de char défini peut être accepté comme un pointeur vers char.

Maintenant, essayons de définir adjective directement comme un pointeur vers char.

#include <stdio.h>
#include <string.h>

void describe_me(char *adjective){
    // segmentation fault on this line when program is executed
    strcpy(adjective, "evil");
    printf("I am %s.\n", adjective);
}

int main() {
    char *adjective = "good";
    describe_me(adjective);
    printf("I am really %s.\n", adjective);
    
    return 0;
}

Tu te souviens que les pointeurs sont puissants, mais aussi potentiellement dangereux ? Mais pourquoi ça ne fonctionne pas ? Un tableau est un pointeur, et un pointeur fonctionnait avec un tableau de char ! La raison, c'est qu'avant, adjective était défini comme un tableau de char, qui est une variable appartenant à main(). Maintenant, adjective est défini comme un littéral de string, ce qui signifie que les chars sont écrits dans une mémoire statique en lecture seule qui appartient à tout le programme. Ce sont tous les deux des pointeurs, mais ils pointent vers des choses différentes. Essayer de modifier la valeur littérale d'une string revient à essayer de modifier la valeur littérale de 5. Ce n'est pas permis. Donc, le problème n'est pas vraiment que ce soit un pointeur. Le problème, c'est ce vers quoi il pointe.

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet