tableau

Les tableaux

Un objet tableau en C est une suite contiguë d’éléments de même type.

A la déclaration du tableau, on en précise le nombre d’éléments (la taille) ainsi que le type des éléments. La taille, qui doit être connue à la compilation, ne sert qu’à allouer l’espace mémoire nécessaire au stockage du tableau. Nous reviendrons sur cet aspect.

Remarque à sauter en première lecture.

Il y a aussi d’autres « tableaux », ceux que l’on crée à l’aide de l’instruction calloc. Ce sont plutôt des octets contigües en mémoire avec un pointeur positionné sur le premier octet.

La déclaration de tableaux

int tabent [10] ; /*tabent est un tableau de 10 entiers (indicés de 0 à 9) */

static char tabcar [23] ;/* tabcar est un tableau de 23 caractères (indicés de 0 à 22) et que sa classe d’allocation est statique (oubliez cette notion dans un premier temps) */


int i ; /* pas de valeur d'initialisation par défaut */

int tabent [i] ;

/* interdit, I doit être connu à la compilation. On ne peut pas, dans une fonction, déclarer un tableau dont la taille est donnée par un paramètre de la fonction. C’est possible dans un grand nombre de langages : Java, Ada, C++ */

Un tableau en C se caractérise :

  • Son nom (identificateur ou identifiant) ;
  • Le type des éléments ;
  • Sa taille (le nombre d'éléments);
  • Sa classe d’allocation.

Les indices de tableau commencent toujours par 0.

Initialisation d’un tableau

Les tableaux peuvent être initialisés en faisant suivre leur déclarations d’une listes de valeurs constantes séparées des virgules, le tout placé entre accolades. Cette notation, dite d’agrégat , ne peut servir qu’à l’initialisation.

int tabint [5] = {0, 1, 2, 3, 4} ;/* Un tableau de 5 entiers. Le premier vaut 0, le deuxième vaut 1, .... */

On peut même initialiser de cette façon les tableaux de classe automatiques (pile) dans la norme ANSI. On ne pouvait auparavant initialiser que les tableaux externes ou statiques.

On ne peut cependant pas écrire ceci, avec l’espoir que tab2ent soit initialisé avec le contenu du tableau tab1nt.

int tab2ent = tabint ;

Interdit. En C, l’affectation de tableau n’existe pas.

On peut laisser le compilateur compter le nombre de valeurs pour caculer la taille du tableau :

int tabentier [ ] = {1, 2, 3, 4, 5, 6 } ; /* taille égale à 6 */

Laisser le compilateur calculer la taille.

Lorsque la taille est précisée, le nombre d’éléments placés entre accolades doit être inférieur ou égal à la taille ; le compilateur complète par des 0 .

int tab [4] ={1, 2 } ; /* équivalent à {1, 2, 0, 0 } */

Pour un tableau de caractères, l’initialisation peut se faire de deux manières :

  • En donnant une liste de caractères entre accolades :

char tabchar [8] = {‘b’, ‘o’, ‘n’, ‘j’, ‘o’, ‘u’ , ‘r’, ‘/0’} ;

  • En utilisant la notation particulière de valeur de chaîne :

char tabchar [8] = « bonjour » ;

char tabchar [ ] = « bonjour » ; /* Mieux, le compilateur ajuste la taille du tableau*/

Notez le caractère (invisible dans la deuxième notation) ‘\0’, de valeur 0 ; ce caractères marque la fin de chaîne. Ne pas oublier de le donner (1ère notation) ou de le compter (2ème notation).

char tabchar [7] = « bonjour » ; /* Incorrect, problème de taille*/

char tabchar [9 ] = « bonjour » ; /* OK, complétée par des zéros*/

Les tableaux (les opérations sur les tableaux) et les pointeurs

Le moyen le plus simple d’accéder à un élément de tableau est d’utiliser la notation d’indice.

int tabint[5] = {0, 1, 2, 3, 4} ;

tabint [4] = tabint [0] ; /* On affecte à l’élément d’indice 4 la valeur de l’élément d’indice 0 */

Attention, pas de contrôle de bornes.

La définition ci-dessus introduit (alloue l’espace pour) un tableau de 5 entiers, numérotés de 0 à 4. Mais, contrairement aux langages qui effectuent des contrôles de bornes, tabint[-1] et tabint[9] sont des accès dangereux, mais autorisés. La raison est qu’en C, les notions de pointeurs et de tabelaux sont liés.

Nom de tableaux

En C, nom[i] est équivalent à *(nom+i). dans cette notation, nom désigne l’adresse du 1er élément (1er octet) du tableau nome, nom+i donne l’adresse de l’élément d’indice i, et *(nom+i) donne la valeur de cet élément.

Voici l’exemple d’un tableau de caractères imprimé en procédant de deux manières différentes :

main () {

char *t ;

char nom[] = {‘o’, ‘g’, ‘o’, ‘r’ } ;

int i = 0 ;

while ( *(nom+i) != ‘\0’ ) {

putchar (*(nom+i)) ; i++ ;

};

t= nom ;

while ( *(t) != ‘\0’ ) {

putchar (*(t)) ; t++ ;

}

}

Ce programme donne : ogorogor.

La seconde manière est plus efficace

Le fait que le nom d’un tableau dans une expression est converti en une valeur entraîne que :

  • l’affectation de tableau est interdite. Si t1 et t2 sont deux tableaux, on ne peut pas écrire t1 = t2 ; les comparaisons ==, !=, etc correspondent à des comparaisons d’adresse et non de tableaux (contenus). t1 == t2 teste si les deux tableaux sont à la même adresse en mémoire !!!
  • t1++ est interdit car le nom t1 désigne une valeur (et non un objet modifiable).

Il y a deux exceptions à cette conversion :

  • sizeof(t1) donne bien la taille en octets du tableau.
  • &t donne l’adresse du tableau (et non, ce qui n’a aucun sens, l’adresse d’une valeur).

Quand on déclare un objet tableau, on n’a pas, en machine, de pointeurs sur le 1er élément.

Ne pas se laisser tromper par les notation suivantes :

char message[] = « bonjour » ; char * chaine = « bonjour » ;

Dans les deux cas, on a un tableau, dont la taille est calculée par le compilateur (8 caractères, les sept de bonjour, plus le caractère \0).

  • Dans le cas de message cependant, seul le message existe.
  • Dans le cas de chaine, il y a un pointeur, plus un tableau.

Le langage C est tout de même compliqué : 2 façons différentes d’introduire un tableau de caractères, sachant qu’avec la fonction calloc, il y a une 3ème façon.

Le programme suivant, faisant intervenir deux fichiers compilés séparément, comporte une erreur dont l’effet est indéfini.

Fichier 1

Fichier 2

int tab [3] ; /*définition externe */

void f(void) ; /*déclaration externe */

void g (void ) {

tab[] = 4 ;

f() ;

}

extern int * tab ; /*déclaration externe */

void f(void) /*définition externe */ {

int x ;

x = *tab ;

}

La fonction f tente un accès via le pointeur tab qui contient la valeur 4 (contenu du premier élément) et non l’adresse du tableau tab.

L’affectation entre tableaux est interdite, pas celle entre pointeurs

Soit deux variables chaines de caractères déclarées de la façon suivante :

char *message1 = « bonjour » ; char * message2 ;

Comme message 2 et message 1 sont des ponteurs, l’instruction

message2 = message1 ;

affecte à message2 le contenu de la varaiable message1, c’est une affectation d’adresse. La chaîne de caractères « bonjour » n’existe qu’en un seul exemplaire en mémoire, elle n’a pas été dupliquée.

Soit deux autres variables déclarées sous forme de tableaux de caractères :

char chaine1[] = « bonjour » ; char chaine2[8] ;

Comme chaine 1 et chaine2 sont des tableaux, l’instruction suivante :

chaine2 = chaine1 ;

sera refusée par le compilateur ;, puisque le nom du tableau désigne l’adresse du premier octet de ce tableau, c’est donc une adresse constante.

Passage de tableaux en paramètres

On dispose de deux façons de passer un tableau en paramètres. Nous donnons deux version de la fonction C strcpy, copiant une chaîne de caractères t dans une autre (copie profonde, la totalité de chaîne est copiée).

Version a utilisant des tableaux

Version b utilisant des pointeurs

void strcpy (char s[] , const char t []) {

int i = 0 ;

while ((s[i] = t[i]) != ‘\0’)

i++

}

void strcpy (char *s , const char *t {

while ((s++= t++) != ‘\0’)

}

Notez la concision de la version b.

Dans la version a, on déclare en paramètres 2 tableaux de caractères dont on ne connait pas la taille.

Dans la version b, on déclare un pointeur sur un caractère.

Dans les 2 cas, c’est l’adresse du tableau qui est passé. Il n’y a pas besoin d’utiliser l’opérateur & (adresse de). Les deux versions de la fonction s’appellent de la même façon :

strcpy (chaine1, chaine2) ;

En C, le passage des paramètres tableaux s’effectue par adresse, contrairement aux autres types (passage par valeur).

Notez l’utilisation de const pour indiquer que le contenu de la chaîne t n’est pas modifié. Notez aussi que les deux versions fonctionnent quelque soit la taille des tableaux (c’est d’ailleurs inquiétant). Rien n’empêche de déborder du tableau, paramètre effectif, correspondant au paramètre s. Le langage C fait confiance au programmeur.

Attention, il ne faut pas croire que le paramètre formel s de la version a est de type char[], son type est char const * (pointeur constant sur caractères).

Exemple illustrant le problème du vrai type d’un paramètre formel tableau.

typedf int vecteur [3] ;

vecteur v ;

vecteur * ptv = &v ; /* ptv est de type int * [3] comme l’est &v. Pas de problème ù/

void f(vecteur v) {

vecteur *ptv = & v ;

/* erreur détectée par le compilateur. tv est bien de type int*[3], pointeur sur un tableau de 3 entiers, mais &v est un pointeur sur un pointeur constant d’entiers int* (const *). */

}

v a desux significations différentes suivant que v est un paramètre formel ou un objet déclaré, bien que les déclarations sont identiques : vecteur ; Il faut utiliser une conversion explicite dans le cas du paramètre :

vecteur *ptv (*vecteur) &v

Les tableaux multidimensionnels

La déclaration et l’utilisation

En C, les tableaux multidimensionnels sont en fait des tableaux de tableaux. La déclaration se fait de la manière suivante :

int tabint [5][20] ;

tabint c’est un tableau de taille 5 pointant chacun sur un tableau de 20 entiers.

tabint[i] est d’un type tableau de 20 entiers ;

tabint[i] [j] est de type int, c’est le jème élément du ième tableau de 20 entiers ;

Il est possible d’utiliser un typedef définissant un alias (synonyme) de type tableau :

typedef int tab [20] ; pour ensuite déclarer un tableau de tableau :

tab tabint [5] ; :* revient à int tabint [5][20] ; */

Rangement en mémoire et calcul d’adresse.

Les tableaux multidimensionnels sont rangés en mémoire de façon contiguë (le n+1ème indice variant plus vite que le nième). Pour un tableau à deux dimensions, cela correspond à un rangement de ligne.

Un tableau à plusieurs dimensions est un cas particulier de tableau. Un nom d’objet tableau est donc converti en l’adresse de son premier élément, et l’expression tabint [i][j] est équivalente à *(*(tabint+i)+j).

En fait le compilateur, rencontrant un accès aux éléments d’indice i et j du tableau toto tab [N] [M] effectuera le calcul d’adresse suivant, ce qui nécessite 2 additions et deux multiplications :

(pointeur_d-octet) &tab [0][0] + (i * sizeof(toto[M])) + (j*sizeof(toto)) avec sizeof(toto[M]

donnant la taille d’une ligne et sizeof(toto) la taille d’un élément.

L’initialisation

int tabint[2][10] = {

{1,2,3,4,5,6,7,8,9,10} /*tabint [0] */ ,

{3,4,7,6,9,9,7,0,4,8} /*tabint [1] */

} ;

Une liste incomplète est automatiquement complétée de 0.

On peut aussi faire l’initialsation de la façon suivante :

int tabint [2][2][2] = {1,2,3,4,5,6} ;

Atention, ne pas confondre :

int tab [2] [2] = { {1} , {1} } ; /* { {1, 0} , {1, 0} } */

et

int tab [2] [2] = {1,1} ; /* { {1, 1} , {0, 0} } */

Le tableau de pointeurs sur des tableaux

Dans un texte, les lignes sont en général de longueurs variables. Si ces lignes doivent rangées, bout à bout, dans un grand tableau de caractères, il est intéressant de pouvoir accéder directement à une ligne du texte. Pour résoudre ce problème, on peut utiliser un tableau dont les éléments pointeraient sur le premier caractère de chaque ligne.

C’est le but de la déclaration suivante qui crée un tableau de nbr_lignes pointeurs de caractères :

char *lignes[nbr_lignes] ;

Les tableaux de pointeurs sont aussi intéressants (dans le cas où les lignes sont de tailles égales) quand on ne connaît pas à la compilation les dimensions du tableau. On utilise alors l’allocation dynamique (calloc). On peut aussi noter que l’accès à un élément est plus rapide dans le cas d’un tableau de pointeurs.

Comparaison entre pointeurs de tableaux et tableaux multidimensionnels

Il en faut pas confondre un tableau de pointeurs et un tableau à deux dimensions. Prenons comme exemple :

char * saison [] = {« hiver », « printemps », « été » , « automne »} ;

char tableau_de_saison [] [10] = {« hiver », « printemps », « été » , « automne »} ;

Les représentations en mémoire sont distinctes dans les deux cas.

On peut remarquer qu’un tableau de pointeurs sur tableaux peut occuper moins de place mémoire qu’un tableau à 2 dimensions, lorsque les lignes ont des tailles différentes.

Le « miracle » est que pour ces deux tableaux, l’accès à un élément (un caractère) se faite de la même façon, car la notation tableau_de_saison [i] [j] est équivalente à *( tableau_de_saison [i]+j) où tableau_de_saison [i] donne l’adresse du premier élément du ième tableau.

Mais l’accès à un élément en utilisant en utilisant un tableau de pointeurs est plus rapide.

Si on veut que les lignes se suivent en mémoire, …..

Paramètres du programme principal

» Glossaire du langage C