La vérité sur les tableaux et pointeurs en C

Formation

En Ligne

Prix sur demande

Appeler le centre

Avez-vous besoin d'un coach de formation?

Il vous aidera à comparer différents cours et à trouver la solution la plus abordable.

Description

  • Typologie

    Formation

  • Méthodologie

    En ligne

Grâce à cette formation vous pourrez acquérir les connaissances nécessaires qui vous permettrons d’ajouter des compétences à votre profil et obtenir de solides aptitude qui vous offriront de nombreuses opportunités professionnelles.

Questions / Réponses

Ajoutez votre question

Nos conseillers et autres utilisateurs pourront vous répondre

À qui souhaitez-vous addresser votre question?

Saisissez vos coordonnées pour recevoir une réponse

Nous ne publierons que votre nom et votre question

Les Avis

Les matières

  • C++

Le programme

Introduction du cours

Explications détaillées sur les tableaux et pointeurs en C.

IntroductionBienvenue

Ainsi, tu es à la recherche de la vérité sur les tableaux et pointeurs en C?
Mais laisse-moi d'abord deviner ce qui te conduit à lire ce tutoriel...

Peut-être as-tu présomptueusement affirmé qu'un tableau est un pointeur? Et quelqu'un qui connait la vérité t'aura dirigé ici...

Ou alors tu auras essayé de passer un tableau de tableaux en argument d'une fonction qui attend un pointeur de pointeur, comme ceci:

void f(int **ppint); void g(void) { int tab[3][4]; f(tab); /* grossière erreur ! */ }

Et le compilateur aura émis un avertissement que tu ne comprends pas:

ex1.c: In function ‘g’: ex1.c:5:2: attention : passing argument 1 of ‘f’ from incompatible pointer type ex1.c:1:6: note: expected ‘int **’ but argument is of type ‘int (*)[4]’

Et tu auras peut-être même fait fi de l'avertissement du compilateur , lancé ton programme, et il plante lamentablement à la première utilisation de ppint dans la fonction f...

On m'aurait menti?

Oui, les tutoriels mentent, car les débutants ne pourraient pas affronter la vérité toute nue sur les tableaux et les pointeurs.

Mais pour commencer, revoyons ce qui est à l'origine des malentendus, c'est-à-dire le pieux mensonge enseigné aux débutants. Il est généralement écrit dans des termes ressemblant à ceux-ci: «Un tableau se comporte comme un pointeur constant sur son élément initial.»

Notons d'abord qu'un (bon) tutoriel ne prétendra jamais qu'un tableau est un pointeur, mais qu'il se comporte comme, ou se convertit en, etc. Affirmer qu'un tableau est un pointeur est abusif.

Récapitulation de ce qui devrait être déjà connu

Revoyons en quoi «un tableau se comporte comme un pointeur constant sur son élément initial»:

#include <stdio.h> int main(void) { int tab[3]; int *p; p = tab; p = tab + 2; tab[1] = 5; p[-1] = 6; printf("hello, world\n"); /* tab = p; /* ne compile pas */ return 0; }

Explications ligne par ligne
int tab[3];
Un tableau de 3 int appelé tab est défini.

int *p;
Un pointeur de int appelé p est défini.

p = tab;
tab se comporte comme un pointeur sur son élément initial, qui est affecté à p. Le pointeur p pointe donc sur tab[0].

Essaie de deviner la suite des explications avant d'ouvrir la partie secrète qui suit...

p = tab + 2;
tab se comporte comme un pointeur sur son élément initial, 2 est ajouté à ce pointeur (arithmétique de pointeur), et le résultat est affecté à p. Le pointeur p pointe donc sur tab[2].

tab[1] = 5;
L'instruction tab[1] = 5; est équivalente à *(tab + 1) = 5;, car par définition T[N] est équivalent à *((T)+(N)). tab est donc converti en un pointeur sur son élément initial, puis 1 est ajouté (arithmétique de pointeur), puis le pointeur résultant est déréférencé pour affecter 5 à l'objet pointé.

p[-1] = 6;
C'est le cas simplifié de tab[1] = 5; car p est un pointeur, il n'y a même pas besoin de conversion. À noter que malgré l'indice négatif, on pointe toujours dans le même tableau car p pointe sur tab[2].

printf("hello, world\n");
La chaine "hello, world\n" est un tableau de 14 char. Ce tableau se comporte comme un pointeur sur son élément initial, soit la lettre 'h'. Ce pointeur est passé à la fonction printf, qui attend justement un pointeur de char.
Eh oui, au cas où tu ne t'en serais pas rendu compte, on tombe sur une conversion de tableau en pointeur dès le hello world!

tab = p;
Cette ligne ne compile pas car tab est constant. Il est donc impossible d'affecter une valeur à tab.

Déclaration de tableau et pointeur

Ce chapitre repasse en revue ce qui est mis en mémoire lorsqu'on déclare un tableau, et lorsqu'on déclare un pointeur. Ça devrait être connu, mais sait-on jamais. :p

Quand on déclare int t1[3]; on obtient trois int en mémoire, et rien d'autre.
Quand on déclare int *p1; on obtient un pointeur de int en mémoire, et rien d'autre.
Quand on déclare int t2[3][4]; on obtient trois tableaux de quatre int, soit douze int en mémoire, et rien d'autre.
Quand on déclare int **p2; on obtient un pointeur de pointeur de int en mémoire, et rien d'autre.
Quand on déclare int *t3[3]; on obtient trois pointeurs de int en mémoire, et rien d'autre.
Quand on déclare int (*p3)[3]; on obtient un pointeur de tableau de 3 int en mémoire, et rien d'autre.

Initialisation et représentation en mémoire

Logiquement (si si, c'est logique, les trucs illogiques, c'est pour les derniers chapitres), on peut écrire:

int t1[3] = { 1, 2, 3 }; int *p1 = &t1[0]; int t2[4][3] = {{1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}}; int **p2 = &p1; int *t3[3] = { &t1[1], &t2[0][0], &t2[1][2] }; int (*p3)[3] = &t2[3];

Voici ce que ça pourrait donner en mémoire, avec des int de 4 bytes et des pointeurs de 8 bytes:

int t1[3] = { 1, 2, 3 };

adresses ...|8   |12  |16  |20 données  ...|0001|0002|0003|...int *p1 = &t1[0];adresses ...|24      |32 données  ...|00000008|...int t2[4][3] = {{1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}};adresses ...|32  |36  |40  |44  |48  |52  |56  |60  |64  |68  |72  |76  |80 données  ...|0001|0002|0003|0004|0005|0006|0007|0008|0009|0010|0011|0012|...int **p2 = &p1;adresses ...|80      |88 données  ...|00000024|...int *t3[3] = { &t1[1], &t2[0][0], &t2[1][2] };adresses ...|88      |96      |104     |112 données  ...|00000012|00000032|00000052|...int (*p3)[3] = &t2[3];adresses ...|112     |120 données  ...|00000068|...

Enfin, on ne peut évidemment pas écrire ce qui suit, puisqu'un tableau n'est pas un pointeur, et un pointeur n'est pas un tableau:

int t1[3] = 0; /* FAUX */ int *p = { 1, 2, 3 }; /* FAUX */ Déclarations externes

Comme un tableau n'est pas un pointeur, lorsqu'on déclare un tableau ou un pointeur sans le définir (avec le mot clé extern), il faut que la définition soit respectivement celle d'un tableau ou d'un pointeur. Sinon, apparaît un bogue qui révèle bien les différences de représention mémoire entre tableau et pointeur.

Observons le comportement du programme bogué suivant, composé des fichiers source main.c et text.c:

/* main.c */ #include <stdio.h> extern char *p1; extern char tab1[]; extern char p2[]; /* déclaration de tableau alors que p2 est défini en pointeur */ extern char *tab2; /* déclaration de pointeur alors que tab2 est défini en tableau */ int main(void) { puts(p1); puts(tab1); puts(p2); puts(tab2); return 0; } /* text.c */ char *p1 = "p1"; char tab1[] = "tab1"; char *p2 = "p2"; char tab2[] = "tab2"; $ gcc main.c text.c -Wall $ ./a.out p1 tab1 ÿ@ Erreur de segmentation $

Les deux premières chaines s'affichent parfaitement, comme il se doit, puisque les déclarations externes correspondent bien aux définitions.

Pour la troisième chaine, l'affichage est incohérent; en effet, en compilant main.c mon compilateur a cru que p2 est un tableau, et a produit du langage machine qui affiche les bytes constituant ce qui est en fait un pointeur. D'où le ÿ@ sur mon PC. Selon la norme C, il s'agit d'un comportement indéterminé (C99 §6.2.7 All declarations that refer to the same object or function shall have compatible type; otherwise, the behavior is undefined.)

Enfin, le puts(tab2) crée une erreur de segmentation, car les bytes de la chaine "tab2" sont pris pour un pointeur, qui pointe sans surprise à une adresse invalide. Évidemment, encore un comportement indéterminé.

La vérité vraieLa source de la vérité

La vérité sur les tableaux et les pointeurs se trouve actuellement dans le chapitre 6.3.2.1 Lvalues, arrays, and function designators, paragraphe 3, de la norme internationale ISO/IEC 9899:1999 (C99 pour les intimes):

Citation : C99

Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type "array of type" is converted to an expression with type "pointer to type" that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.

Eh oui, la vérité nue est en anglais. Ça permet d'amortir le choc. ^^
Voici une traduction:

Citation : C99 traduit

Sauf quand elle est l'opérande de l'opérateur sizeof ou de l'opérateur unaire &, ou est une chaine de caractères littérale utilisée pour initialiser un tableau, une expression de type "tableau de type" est convertie en une expression de type "pointeur de type" qui pointe sur l'élément initial de l'objet tableau et n'est pas une lvalue. Si le tableau est d'une classe de stockage registre, le comportement est indéterminé.

Le mot clé, c'est le mot expression.
Une expression n'est pas un objet en mémoire, c'est un bout de code source, comme 2+3 , &i , ou simplement tab .
Quand il lit l'expression 2+3 , le compilateur peut directement calculer le résultat: 5 (de type int ).
Quand il lit l'expression &i , le compilateur génère les instructions machine qui calculent l'adresse de i (de type int* si i est de type int ).
Quand il lit l'expression tab , le compilateur génère les instructions machine qui calculent l'adresse de l'élément initial de tab (de type int* si tab est de type int[] ). Sauf avec sizeof , & , et "chaine d'initialisation" , mais c'est le chapitre suivant.

Enfin, je rappelle qu'une déclaration n'est pas une expression. Déclarer un tableau ne revient donc évidemment pas du tout à déclarer un pointeur. Quand on écrit int*p,tab[3]; il y a deux déclarations, mais aucune expression, donc aucune conversion. (Et le premier de classe qui lève la main pour me dire que, si, le 3 tout seul dans tab[3] est une expression constante, je lui demanderai de se taire pour ne pas embrouiller les autres.)

Je dois encore signaler que la vérité a un peu évolué. Du temps de C89 (trouvé sur http://flash-gordon.me.uk/ansi.c.txt), c'était:

Citation : C89

Except when it is the operand of the sizeof operator or the unary & operator, or is a character string literal used to initialize an array of character type, or is a wide string literal used to initialize an array with element type compatible with wchar_t, an lvalue that has type "array of type" is converted to an expression that has type "pointer to type" that points to the initial member of the array object and is not an lvalue.

En C89, ce ne sont pas toutes les expressions qui sont converties, mais seulement les lvalues. Étant donné qu'un tableau est une lvalue, c'est une différence subtile qui ne se révèle que dans des cas assez tordus. Et pour ne pas décourager les lecteurs qui ont suivi jusqu'ici, je n'illustrerai cette différence qu'au dernier chapitre du tutoriel.

Bon, maintenant que la vérité est dévoilée, j'espère que tout est devenu beaucoup plus clair. :D Sinon, des exemples sont donnés dans la suite du tutoriel...

sizeof tab, &tab, et char tab[] = "chaine"

Citation

Sauf quand elle est l'opérande de l'opérateur sizeof ou de l'opérateur unaire &, ou est une chaine de caractères littérale utilisée pour initialiser un tableau, une expression de type "tableau de type" est convertie en une expression de type "pointeur de type" qui pointe sur l'élément initial de l'objet tableau et n'est pas une lvalue.

Comme revu dans l'introduction, lorsqu'une expression de type tableau est l'opérande de l'opérateur d'affectation (p=tab ), d'addition (tab+2 ), d'appel de fonction (printf("hello, world\n") ), et de beaucoup d'autres opérateurs, alors elle se comporte comme un pointeur constant sur l'élément initial du tableau. En fait, il n'existe que trois cas où une expression de type tableau se comporte clairement comme un tableau. Ils sont soulignés dans l'extrait de la norme ci-dessus, et illustrés ci-dessous.

sizeof tab#include <stdio.h> int main(void) { char tab[3]; char *p = tab; ...

Appeler le centre

Avez-vous besoin d'un coach de formation?

Il vous aidera à comparer différents cours et à trouver la solution la plus abordable.

La vérité sur les tableaux et pointeurs en C

Prix sur demande