Tableaux
Centre Informatique pour les Lettres
et les Sciences Humaines
1 - Vrais tableaux..................................................................................................................... 2
4 - Bon, c'est gentil tout ça, mais ça fait déjà 12 pages. Qu'est-ce que je dois vraiment en
retenir ? ............................................................................................................................ 13
5 - J'ai rien compris, est-ce que quelqu'un d'autre pourrait m'expliquer ça un peu plus
clairement ? ...................................................................................................................... 13
1 - Vrais tableaux
Lorsqu'il s'agit de stocker en mémoire une collection de données du même type, le langage C++
permet de ne créer qu'une seule variable, de type "tableau de…". La syntaxe utilisée pour créer
un tableau exige simplement que le nom de la variable soit suivi du nombre de valeurs qui
doivent pouvoir être stockées dans le tableau, placé entre crochets. La ligne
int unTableau[4];
définit donc une variable de type "tableau d'int", nommée unTableau et susceptible de stocker
simultanément 4 valeurs entières. Si cet exemple fait intervenir le type int, il reste bien
entendu que celui-ci ne dispose d'aucun privilège particulier, et que tous les types de données
(y compris les types définis, tels que les types énumérés ou les classes) peuvent, de la même
façon, donner naissance à des tableaux.
Les tableaux partagent avec les classes la particularité de posséder des "sous-variables". Alors
que les "sous-variables" d'une classe s'appellent des "variables membre", celles d'un tableau
s'appellent des "éléments". Si les variables membre peuvent être de types différents, les
éléments d'un tableau sont nécessairement tous du même type. Une autre différence
importante est que les variables membre possèdent des noms différents, que le programmeur
doit spécifier lorsqu'il définit la classe, alors que les éléments d'un tableau sont simplement
numérotés à partir de 0, sans que le programmeur ait à intervenir. Ces numéros sont plus
communément appelés des "index". La syntaxe qui permet d'accéder aux sous variables est
également différente, puisqu'on désigne une variable membre en connectant son nom à celui
de l'instance concernée à l'aide de l'opérateur de sélection ".", alors qu'on désigne un élément
d'un tableau en plaçant, après le nom du tableau, une paire de crochets encadrant l'index de
l'élément. Si unTableau est défini comme ci-dessus, on affectera, par exemple, la valeur nulle à
l'élément d'indice 2 en écrivant :
unTableau[2] = 0;
Remarquez que, si les crochets utilisés pour encadrer un index (lorsqu'on accède à un
élément d'un tableau) et ceux utilisés pour encadrer la taille (lorsqu'on définit un tableau)
sont typographiquement identiques (les symboles utilisés sont, dans un cas comme dans
l'autre, [ et ]), ils correspondent évidemment à des opérations fort différentes.
Comme à son habitude, le langage C++ numérote les éléments à partir de zéro. Ceci signifie
qu'après la déclaration
int tableau[4];
les éléments qui existent sont tableau[0], tableau[1], tableau[2] et tableau[3]. En
conséquence :
Un tableau ne possède pas d'élément dont l'index soit égal au nombre d'éléments du tableau !
Il est tout à fait primordial de ne jamais oublier ce détail car, pour des raisons qui deviendront
claires un peu plus loin dans cette Leçon :
Lorsque vous accédez à un élément d'un tableau, c'est à vous de vous assurer que l'index
utilisé correspond effectivement à un élément du tableau.
Le parallélisme entre classes et tableaux peut être illustré par les deux ensembles
d'instructions suivants, qui créent tous deux une variable composée de deux sous-variables de
type double, et placent dans ces sous variables les valeurs 3.14 et 2.7.
Avec une classe Avec un tableau
Définition du type class demo
{
public: //inutile
double membreZero;
double membreUn;
};
Définition de la variable demo uneInstance; double unTableau[2];
Accès aux sous-variables uneInstance.membreZero = 3.14; unTableau[0] = 3.14;
uneinstance.membreUn = 2.7; unTableau[1] = 2.7;
Classes et tableaux diffèrent cependant énormément quant aux possibilités qu'ils offrent.
Les classes sont capables d'avoir des variables membre de types différents, et peuvent
comporter des fonctions membre. Elles peuvent aussi être définies les unes par rapport aux
autres, ce qui donne naissance à de nombreuses possibilités sur lesquelles nous reviendrons
dans la suite du cours.
Bien qu'ils soient considérablement plus simples que les classes, les tableaux présentent des
caractéristiques puissantes. Essayez, par exemple, de transposer le fragment de code suivant
pour qu'il utilise une classe au lieu d'utiliser un tableau :
1 int tab[325612];
2 int index;
3 for(index = 0 ; index < 325612 ; index = index + 1)
4 tab[index] = index;
La propriété des tableaux qui est exploitée dans ce fragment de code est le fait que l'on
n'accède pas aux éléments à l'aide de noms, mais à l'aide d'un index appliqué à une variable
unique (le tableau lui-même). La ligne 4 est exécutée un très grand nombre de fois et, lors de
chacune de ces exécutions, elle accède à un élément différent du tableau, chose qui serait
impossible si chacun de ceux-ci était une variable dotée d'un nom spécifique (comme le sont,
par exemple, les variables membre d'une classe). Nous avons déjà rencontré un phénomène
semblable : déréférencer un pointeur permet aussi à une même ligne de code d'accéder à une
donnée différente chaque fois qu'elle est exécutée (il suffit que la valeur du pointeur ait
changé entre temps). Il y a là bien plus qu'une similitude, et cette parenté profonde entre
tableaux et pointeurs va constituer un des thèmes majeurs de cette Leçon.
Etant donné qu'un tableau contient plusieurs valeurs, son initialisation nécessite une liste de
données. Cette liste doit simplement figurer entre accolades, chaque valeur étant séparée de la
suivante par une virgule. L'instruction suivante initialise un tableau d'entiers :
int tab[4] = {3, 2, 1, 0};
On peut, nous l'avons vu, affecter une valeur à un élément d'un tableau, mais il est
impossible d'affecter, en une seule opération, une valeur à chacun des éléments, ce qui
pourrait, légitimement, s'appeler "affecter une valeur (de type tableau) à un tableau".
1 int tab1[3] = {36, 42, 12}; //ceci est une INITIALISATION
2 int tab2[3];
3 tab2 = tab1; //ERREUR : on ne peut pas AFFECTER une valeur à un tableau
4 tab2 = {36, 42, 12}; //même remarque
toute modification de la liste nécessiterait alors d'ajuster la valeur de cette constante, ce qui est
une source d'erreurs dont il vaut mieux se passer. Les quelques secondes qu'il faut pour écrire
1 int tab[] = {15, 36, 18, 12};
2 int index;
3 for (index = 0 ; index < sizeof(tab)/sizeof(tab[0]) ; index = index + 1)
4 traitement(tab[index]);
plutôt que
1 int tab[] = {15, 36, 18, 12};
2 int index;
3 for (index = 0 ; index < 4 ; index = index + 1) //TRES DANGEREUX !
4 traitement(tab[index]);
vous épargneront des heures de débugage, ou, pire encore, des résultats faux.
Les tableaux de char présentent une particularité : ils tiennent lieu de type "chaîne" dans le
langage C, qui est dépourvu de type spécifiquement prévu pour la manipulation de textes.
Le langage C++ ne possède pas non plus de "type spécifiquement prévu pour la manipulation
de textes", mais le mécanisme des classes permet de remédier très facilement à cet état de
fait, et d'obtenir un type dont l'usage est bien plus facile et sûr que celui des tableaux de
char. La Standard Templates Library3 comporte d'ailleurs une telle classe, simplement
nommée string. Les langages C et C++ ne sont toutefois pas assez disjoints pour qu'un
programmeur C++ puisse envisager d'ignorer totalement la façon dont C manipule les textes.
Nous avons vu (Leçon 2) que le type char, bien qu'étant un type entier, se prête bien à la
représentation des symboles alphanumériques : il suffit d'associer conventionnellement4 une
valeur différente à chacun de ces symboles et de prendre soin, lorsqu'on affiche le contenu
d'une variable de ce type, de ne pas utiliser les conventions habituelles, mais de dessiner le
symbole qui a été associé à la valeur à afficher. Dans ce contexte, une valeur de quarante-huit
ne se présente pas sous la forme "48", mais sous la forme "0".
Utiliser des tableaux de char pour représenter des chaînes de caractères ne pose guère qu'un
seul problème : comment peut-on déterminer où se trouve la fin de la chaîne ?
Il est possible qu'une réponse à cette question vous semble aller de soi : la fin de la chaîne
correspond à la fin du tableau. Deux raisons font que cette façon de voir les choses ne peut
absolument pas être adoptée :
- La taille des tableaux est fixée lors de leur définition, et elle ne peut pas réellement être
modifiée par la suite. La plupart des programmes ont pourtant besoin de manipuler des
chaînes de caractères dont le contenu (et donc, éventuellement, la longueur) est variable.
- Comme nous allons le voir dans la suite de cette Leçon, il existe de nombreux contextes où
la taille d'un tableau ne peut pas être déterminée, et constituerait donc un bien piètre
indicateur de fin de chaîne.
Il existe deux types de réponses au problème de la spécification de la fin d'une série telle
qu'une chaîne de caractères : soit on indique explicitement le nombre d'éléments, soit on utilise
une valeur particulière pour symboliser la fin. Ces deux méthodes créent des contraintes
différentes : dans le premier cas, il faut d'une part effectuer des calculs pour tenir à jour ce
compte et, d'autre part, réserver quelques octets de mémoire pour le stocker5, alors que dans le
second cas, le choix d'une valeur particulière pour matérialiser la fin de la série fait que cette
valeur ne peut plus figurer dans la série. La première méthode est, notamment, utilisée par le
langage Pascal et par certains BASIC. Pour ce qui est du langage C, il adopte la seconde :
Lorsqu'un tableau de char est utilisé pour stocker une chaîne de caractères, la fin de cette
chaîne est matérialisée par un char de valeur nulle.
3 Sans faire réellement partie du langage C++, la STL n'en est pas moins, comme son nom l'indique, considérée comme
"standard". Elle est donc, normalement, disponible dans tous les environnements de développement C++.
4 La convention généralement en vigueur est appelée le "code ASCII".
5 Cette nécessité de représenter en mémoire le nombre d'éléments de la série risque d'introduire une limite absolue à la
longueur admissible. Si, par exemple, on ne réserve qu'un octet pour stocker la longueur, on ne peut pas utiliser de
séries ayant plus de 255 éléments.
Il reste une propriété des tableaux dont il est important d'être conscient : les seuls cas où le
langage les traite effectivement comme des tableaux est l'application des opérateurs "adresse
6 Notons au passage que, si elle offre un confort incontestable, l'initialisation d'un tableau de caractères à l'aide d'une
chaîne littérale constitue un abus syntaxique manifeste, puisqu'une valeur unique (la chaîne) est décomposée pour
attribuer des valeurs différentes aux différents éléments du tableau. Cette opération, tout à fait exceptionnelle, est
effectuée par le compilateur (ce qui permet de l'utiliser pour effectuer des initialisations) mais n'est pas réalisable lors
de l'exécution du programme (ce qui interdit son usage dans le cadre d'une affectation).
Pour permettre l'évaluation d'une expression dans laquelle figure le nom d'un vrai tableau, le
compilateur remplace ce nom par l'adresse du premier élément du tableau en question.
Il n'est donc, en pratique, pas possible d'utiliser réellement les vrais tableaux sans comprendre
comment on utilise les pointeurs pour les imiter.
2 - Faux tableaux
La clé de la compréhension des rapports entre tableaux et pointeurs réside dans la prise de
conscience de la vraie nature des crochets que nous avons utilisés pour encadrer les index
dans les exemples précédents. La vérité est que ces crochets ne sont qu'une façon de
déréférencer un pointeur, et que leur usage n'est nullement lié à l'existence d'un tableau.
En toute rigueur syntaxique, un index entre crochets n'a de sens que s'il est utilisé pour
déréférencer un pointeur. Cette notation n'est donc pas simplement "utilisable même en
l'absence de vrai tableau", elle est bel et bien "non applicable aux vrais tableaux". C'est
seulement grâce à la règle d'évaluation des expressions énoncée ci-dessus que tout se passe
comme si nous utilisions les crochets pour accéder aux éléments d'un vrai tableau.
Le déréférencement d'un pointeur à l'aide d'un index entre crochets peut être défini d'une façon
simple et brutale :
Cette définition présente toutefois un défaut : sa dernière ligne fait appel à l'addition d'un
pointeur et d'une valeur entière, une opération nouvelle pour nous.
En conséquence, lorsqu'on ajoute 1 à un pointeur, ce n'est pas 1 que le langage C++ ajoute à la
valeur que ce pointeur contient, mais le nombre d'octets occupé en mémoire par la
représentation d'une donnée du type correspondant à celui du pointeur. De même, lorsqu'on
ajoute une quantité n à un pointeur, la valeur qu'il contient augmente en fait de n fois la taille
du type pointé. Cette façon de procéder aux additions impliquant des pointeurs peut sembler
étrange, mais elle se traduit par une conséquence extrêmement bénéfique : tout fonctionne
comme si tous les types de données occupaient un seul octet en mémoire, et on peut utiliser
les pointeurs sans se préoccuper de la taille des objets qu'ils désignent7.
L'addition d'un nombre entier est certainement la plus importante des opérations
arithmétiques pouvant être appliquées aux pointeurs. Il est également possible de calculer la
différence entre deux pointeurs du même type : le résultat obtenu est une valeur entière qui
correspond aux nombres d'éléments du type concerné qui pourraient être stockés entre les
deux adresses.
Ces notations ne sont évidemment acceptables que si machin est un tableau ou un pointeur.
Si ces deux notations sont interchangeables, il semble toutefois que, pour de nombreux
programmeurs, elles suggèrent des situations différentes : lorsqu'un pointeur est déréférencé à
l'aide de la notation indexée, c'est généralement pour accéder à l'une des valeurs d'une série en
comportant plusieurs, de type identique. Le code suivant, quoique parfaitement correct d'un
point de vue syntaxique, est tout à fait atypique :
1 double uneVar = 0;
2 double * unPtr = & uneVar;
3 unPtr[0] = 12.8; //ok, mais suggère à tort l'existence d'une série de valeurs
La connotation inverse est bien moins prononcée, et le fait qu'un pointeur soit déréférencé à
l'aide de l'étoile ne doit pas être interprété comme un indice suggérant qu'il n'existe pas une
série de données auxquelles le programme va accéder en faisant varier la valeur du pointeur.
Puisque les crochets ne sont qu'une nouvelle façon de déréférencer un pointeur, il n'est pas
étonnant qu'ils n'offrent pas un degré de protection supérieur à celui proposé par l'opérateur
étoile : c'est vous seul qui êtes responsable non seulement de la validité du pointeur, mais
aussi du fait que lui ajouter la valeur de l'index utilisé ne conduit pas à accéder à une zone de
mémoire située en dehors du tableau (en clair : si le pointeur utilisé contient l'adresse du
premier élément, l'index doit rester strictement inférieur au nombre d'éléments).
Après la définition
char messageUn[] = "Salut, ça va ?";
nous savons que, en vertu de la règle énoncée page 7, une instruction telle que
7 Ce qui est fort heureux car, rappelons le, la taille des différents types de données n'est pas la même sur tous les
systèmes. S'il fallait tenir compte de cette taille dans le texte source, il deviendrait très difficile d'écrire des programmes
pouvant être compilés et fonctionner de façon correcte sur divers ordinateurs.
Déréférencer un pointeur à l'aide de la notation indexée n'a guère d'intérêt que s'il existe une
série de valeurs auxquelles on va pouvoir accéder en faisant varier la valeur de l'index. Si,
comme nous venons de le voir, cette condition peut être réalisée en stockant dans le pointeur
l'adresse du premier élément d'un (vrai) tableau, cette technique reste d'un intérêt limité, et il
est généralement nécessaire de pouvoir créer des séries de données indépendamment de
l'existence de vrais tableaux. L'opérateur new [] permet de réserver une zone mémoire d'une
taille adéquate à la constitution d'une telle série. Il suffit pour cela de préciser, entre les
crochets, le nombre d'éléments que doit comporter la série :
1 double * ptr;
2 ptr = new double[4];
L'usage d'une allocation dynamique entraîne bien entendu les conséquences habituelles : il
faut tout d'abord s'assurer que new [] a été en mesure de réserver la mémoire requise, et il ne
faut pas oublier de libérer cette mémoire lorsqu'on n'en a plus besoin.
La mémoire allouée par new [] doit être libérée par delete[]
Ces conditions étant remplies, le pointeur peut-être utilisé pratiquement comme s'il s'agissait
d'un vrai tableau :
1 double * ptr;
2 ptr = new double[4];
3 if(ptr != NULL)
4 {
5 int i;
6 for (i=0 ; i < 4 ; i = i + 1)
7 ptr[i] = 3 * i; //ne dirait-on pas qu'il s'agit d'un tableau ?
8 utilise(ptr); //appel d'une fonction qui fait quelque chose d'intéressant
9 delete[] ptr;
10 }
L'opérateur delete[] doit systématiquement être utilisé lorsque la mémoire a été allouée par
new[]. A la différence de new[], delete[] n'a besoin d'aucune spécification de taille, et son
couple de crochets reste donc toujours vide.
Le fait que la valeur du pointeur soit obtenue par allocation dynamique n'a pas pour seules
conséquences la nécessité de tester sa validité avant de le déréférencer et de libérer
explicitement la mémoire qui lui a été attribuée. Un des avantages déterminants de ces faux
tableaux par rapport aux vrais est que leur taille peut être choisie lors de l'exécution du
programme. Imaginons qu'il existe une fonction nommée demandeTaille() qui instaure un
dialogue avec l'utilisateur et renvoie le nombre de données que celui-ci désire traiter. On peut
alors écrire :
1 int taille = demandeTaille();
2 double * ptr = new double[taille];
3 if(ptr != NULL)
4 {
5 utilise(ptr); //appel d'une fonction qui fait quelque chose d'intéressant
6 delete[] ptr;
7 }
Une telle souplesse est inaccessible aux vrais tableaux, dont la taille doit être connue au
moment de la compilation du programme. Il reste malheureusement encore un prix à payer
pour cette souplesse, sous la forme de deux restrictions d'usage :
En effet, la variable que nous manipulons n'est qu'un pointeur, pas un tableau. Lui appliquer
l'opérateur sizeof()permet de vérifier que le système utilisé représente les adresses sur 32
bits, soit quatre octets, mais ne dit absolument rien sur le nombre de données présentes dans
le faux tableau. D'autre part,
Il faut donc recourir à des opérations d'affectation pour attribuer leur première valeur aux
éléments d'un faux tableau.
Remarquez que, de toute façon, si vous ignorez la taille qu'aura effectivement le faux tableau,
vous seriez bien en peine de fournir une liste correcte de valeurs initiales…
Il est définitivement impossible qu'une fonction reçoive comme paramètre un vrai tableau.
Imaginons que nous disposions d'un vrai tableau d'int que nous souhaitons communiquer à
une fonction nommée sommeTab() qui serait chargée de faire l'addition de toutes les valeurs
qu'il contient. Nous écrivons donc quelque chose comme :
1 int monTab[] = {10, 15, 3, 12, 42, 18};
2 int total = sommeTab(monTab);
Lorsque le programme est sur le point d'appeler la fonction somme(), il lui faut évaluer
l'expression figurant entre les parenthèses, de façon à déterminer la valeur qui doit être utilisée
pour "initialiser" le paramètre de la fonction. Comme cette expression est le nom d'un vrai
tableau, la règle énoncée page 7 entre en vigueur, et la valeur que reçoit la fonction est donc
l'adresse du premier élément du tableau, c'est à dire l'adresse d'un int.
La fonction sommeTab() doit donc disposer d'un paramètre de type "pointeur sur int".
Ce pointeur peut tout à fait être utilisé pour accéder aux valeurs contenues dans le tableau. Il
suffit pour cela de le déréférencer (soit au moyen de la notation indexée, soit au moyen de
l'étoile). Un léger problème se pose toutefois : ce pointeur n'est pas un vrai tableau, et la
fonction est donc incapable de déterminer combien il y a d'éléments.
Il nous faut donc trouver un moyen de communiquer la taille du tableau à la fonction, et
plusieurs solutions sont envisageables : réserver le premier élément du tableau à cet usage,
utiliser une valeur "impossible" pour signaler la fin des données8, ou utiliser un second
paramètre pour indiquer la taille du tableau.
Le fait que l'opérateur sizeof() soit incapable de déterminer la taille d'un faux tableau n'est
pas un détail secondaire. Comme nous le voyons ici, combiné avec le fait que les fonctions ne
peuvent recevoir que de faux tableaux, il donne naissance à des contraintes dont la levée est
une des raisons d'être du langage C++. Dans l'absolu, la meilleure réponse au problème
soulevé par notre exemple est "Ne stockez pas vos données dans un tableau". Etant donnés la
nature du langage et le contexte historique (il reste encore beaucoup de librairies et de
programmeurs C) il ne semble pourtant pas réaliste de chercher à maîtriser les techniques
plus évoluées avant d'avoir bien compris les mécanismes élémentaires qui les sous-tendent.
Si nous adoptons la dernière des solutions envisagées ci-dessus, notre fonction somme() peut
être définie ainsi :
1 int sommeTab(int * tab, int nbElements)
2 {
3 int index;
4 int somme = 0;
5 for (index = 0 ; index < nbElements ; index = index + 1)
6 somme = somme + tab[index];
7 return somme;
8 }
et elle pourra être invoquée à l'aide de l'instruction
int total = sommeTab(monTab, sizeof(monTab)/sizeof(monTab[0]);
Remarquez que la simple mention du nom du tableau suffit à assurer la transmission d'une
adresse (en vertu de la règle d'évaluation des expressions comportant des noms de tableaux).
L'appel de la fonction sommeTab() ne nécessite donc aucun usage de l'opérateur "adresse de".
La réalité du processus peut être mieux explicitée en écrivant
int total = sommeTab(&(monTab[0]), sizeof(monTab)/sizeof(monTab[0]);
8 Si, par exemple, les données ne peuvent logiquement être que positives, on peut utiliser une valeur négative pour
Le fait que la fonction reçoit l'adresse du tableau la rend capable de modifier le contenu du
tableau. Selon le rôle joué par la fonction, il s'agit là soit d'un avantage, soit d'une source
d'erreurs que nous préférerions éviter.
D'autre part, étant donné que la valeur qui lui est transmise est de toutes façons une adresse,
une même fonction peut indifféremment traiter des vrais et des faux tableaux. Notre fonction
sommeTab() pourrait tout à fait être utilisée ainsi :
1 int taille = 10;
2 int * fauxTab = new int[taille];
3 if (fauxTab != NULL)
4 {//affecte des valeurs aux éléments (l'initialisation est impossible…)
5 int index;
6 for (index=0 ; index < taille ; index = index + 1)
7 fauxTab[index] = index;
8 sommeTab(fauxTab,taille); //calcule la somme
9 }
Un faux tableau étant, par définition, un pointeur, il ne pose évidemment aucun problème à
une fonction qui attend une adresse pour initialiser son paramètre !
Cette très agréable indifférence des fonctions à l'authenticité des tableaux qui leurs sont
fournis est malheureusement limitée au cas des tableaux unidimensionnels, comme vous le
constaterez si vous étudiez l'Annexe 3 : Tableaux multidimensionnels.
Notations malsaines
Pour des raisons historiques, le paramètre d'une fonction qui reçoit un "pointeur sur…" peut
également être déclaré à l'aide d'une notation utilisant les crochets. Ainsi, notre fonction
sommeTab[] pourrait non seulement être déclarée
int sommeTab(int *tab, int nb); //ma version préférée
mais aussi
int sommeTab(int tab[], int nb); //un bluff éhonté
ou même
int sommeTab(int tab[10], int nb); //un délire total
Ces deux dernières notations présentent le grave défaut de laisser supposer que la fonction
reçoit comme paramètre un (vrai) tableau, ce qui est totalement inexact. Le fait d'utiliser l'une
ou l'autre de ces déclarations ne change rien à la nature du paramètre transmis, qui reste
inexorablement un simple pointeur, comme le prouvent les trois constatations suivantes.
1) Aucune des trois formes de déclaration possibles ne rend le compilateur capable de rejeter
une séquence du type :
1 int * ptr;
2 int total = sommeTab(ptr, 12); //le pointeur ptr n'est même pas valide !
Le compilateur n'exige donc jamais que le premier paramètre transmis à sommeTab() soit un
tableau de 10 entiers. Il n'est même pas nécessaire qu'il s'agisse réellement d'un tableau : un
simple "pointeur sur int" convient, même s'il est dépourvu de contenu valide ! Dans ces
conditions, prétendre que ce paramètre est autre chose qu'un pointeur apparaît clairement
comme une promesse qui ne sera pas tenue.
2) Quelle que soit la déclaration adoptée, si la fonction applique l'opérateur sizeof() à la
valeur qu'elle reçoit comme premier argument, elle pourra en déduire brillamment la taille
d'un pointeur, mais elle n'aura jamais accès à la taille du tableau de cette façon.
D'ailleurs, si la troisième de ces déclarations signifiait réellement ce qu'elle prétend, pourquoi
serait-il nécessaire de passer un second paramètre ?
3) Ces trois versions de la déclaration sont en fait parfaitement équivalentes du point de vue du
compilateur.
Ceci est illustré de façon spectaculaire par le fait la fonction peut être déclarée trois fois dans
le même programme, en utilisant les trois versions de la déclaration, et ce sans provoquer la
Vous pouvez penser ce que vous voulez du fait que le nom d'un tableau est "dégradé" en
l'adresse de son premier élément lorsque les expressions sont évaluées, mais vous n'y
changerez rien en utilisant des notations qui masquent ce fait incontournable. Vous risquez
tout au plus de l'oublier, ce qui vous empêchera d'en tirer parti lorsque c'est un avantage, et en
aggravera les conséquences lorsque c'est un inconvénient.
4 - Bon, c'est gentil tout ça, mais ça fait déjà 12 pages. Qu'est-ce
que je dois vraiment en retenir ?
1) Il existe deux sortes de tableaux, les vrais et les faux.
2) Les faux tableaux ne sont que des pointeurs rendus valides par le fait qu'ils contiennent
l'adresse d'un bloc de mémoire réservé à l'aide de new[].
3) Lorsqu'ils cessent d'être utiles, les faux tableaux doivent être détruits avec delete[].
4) Les crochets ([ et ]) sont un moyen de déréférencer un pointeur : on ajoute au pointeur la
valeur de l'index figurant entre les crochets, puis on accède à l'adresse ainsi obtenue.
5) La seule chose que le langage sache faire avec un vrai tableau, c'est donner sa taille.
6) Le langage masque son incapacité à travailler réellement sur les vrais tableaux en utilisant
secrètement l'adresse de leur premier élément. Ceci lui permet de faire semblant d'accéder
aux éléments d'un vrai tableau à l'aide des crochets.
7) Cette imposture s'écroule (notamment) dès qu'un tableau est passé comme argument à une
fonction : celle-ci doit se contenter du simple pointeur qu'elle reçoit.
8) Les vrais amis n'essaient pas de vous faire prendre de faux tableaux pour des vrais.
6 - Pré-requis de la Leçon 10
La Leçon 10 traite d'un sujet voisin de celui de la Leçon 9, puisqu'elle aborde une autre façon
d'organiser les données, qui peut parfois constituer une alternative aux tableaux. Plutôt que de
constituer un pré-requis l'une pour l'autre, ces deux Leçons devraient s'éclairer mutuellement :
voir comment on peut NE PAS créer un tableau est aussi un bon moyen de comprendre ce
qu'est un tableau.
9 Rappel : un objet ne peut être défini qu'une fois, mais il peut faire l'objet de déclarations multiples, à condition que
celles-ci soient toutes équivalentes (cf. Leçon 5, page 5).
10 Pour les tableaux multidimensionnels, vous pouvez également consulter l'Annexe 3 .