programmation en C "Chapitre 6 : Tableaux"

Chapitre 6 : Tableaux.


5.1 Compléments sur les boucles
5.2 Tableaux à une dimension
5.3 Chaînes de caractères
5.4 Tableaux à plusieurs dimensions
5.5 Variables globales
5.6 Exercices
5.1 Compléments sur les boucles
5.1.1 Introduction
Dans les chapitres précédents, nous avons eu de nombreuses occasions de manipuler des boucles. Répéter plusieurs fois un certain nombre d'instructions est en effet indispensable dans la plupart des programmes. Nous avons jusqu'à présent toujours utilisé la boucle while. Il existe cependant plusieurs autres manières d'effectuer des boucles.
Dans ce chapitre, nous allons présenter comment les tableaux permettent de stocker de grandes quantités de données. Pour les manipuler, nous verrons que les boucles sont souvent indispensables. La boucle while n'étant cependant pas toujours très adaptée, nous allons vous présenter les autres types de boucles proposés par le langage C.

.....


A titre de comparaison, revenons sur la structure de la boucle while : while (condition) { //Instructions }
Il s'agit de la manière la plus simple d'effectuer une boucle : on continue l'exécution du bloc tant que la condition fournie est vraie. Si la condition est fausse dès le départ, le bloc n'est jamais exécuté. Pour fonctionner, la boucle while nécessite donc que les données relatives à la condition aient été initialisées avant la première exécution de la boucle et que ces données soient mises à jour à l'intérieur du bloc d'instruction (sans quoi on aurait simplement une boucle infinie).
5.1.2 La boucle for
Dans de nombreux exercices, nous avons utilisé la boucle while d'une manière souvent très similaire, comme dans l'exemple suivant : int colonne = 0; while (colonne < nb_colonnes) { //Autres instructions du bloc colonne++; }
La structure est toujours la même : on initialise une variable, puis on s'en sert dans la condition du while, pour finalement mettre à jour cette variable à la fin du bloc d'instructions, juste avant de recommencer la boucle. Le plus souvent, l'instruction de mise à jour est une simple incrémentation, ou décrémentation de la variable. Plus rarement, on peut avoir d'autres types de mises à jour et des conditions plus complexes qu'une simple comparaison, mais la structure générale reste très souvent la même : while () { //Instructions du bloc }
Cette manière de faire des boucles est la plus fréquemment utilisée et le sera tout particulièrement lors de la manipulation de tableaux. Un inconvénient de la boucle while est que les trois parties relatives à l'exécution de sa boucle (l'initialisation, la condition et la mise à jour de la variable) sont séparées les unes des autres. Pour se faire une idée précise de la manière dont la boucle fonctionne, on doit regarder à plusieurs endroits différents du source.
Pour faciliter la création et la lecture de ce type de boucles, le langage C propose une notation particulièrement adaptée, sous la forme de la boucle for. Sa structure est la suivante : for (; ; ) { //Instructions du bloc }
Le principe de la boucle for est de regrouper sur une même ligne, séparées par des points virgules, les trois parties définissant les limites de la boucle. Appliqué à notre exemple précédent, cela donne : int colonne; for (colonne = 0; colonne < nb_colonnes; colonne++) { //Autres instructions du bloc }
Nous pouvons remarquer dans cet exemple que la déclaration de la variable qui sert de compteur à notre boucle est toujours placée avant la boucle elle-même. Son initialisation est cependant placée dans l'instruction for et c'est bien elle qui apporte le plus d'information sur le nombre de répétitions du bloc d'instructions.
Il est important de bien comprendre que la notation de la boucle for correspond exactement à la structure du while présentée plus haut :
L'initialisation est exécutée une fois, avant le premier test de la condition et la première exécution du bloc.
Le bloc d'instruction n'est exécuté que tant que la condition est vraie.
La condition est testée avant l'exécution du bloc. Si elle est fausse dès la première fois, le bloc n'est jamais exécuté.
L'instruction de mise à jour est exécutée après le bloc d'instructions, juste avant la nouvelle évaluation de la condition.
L'ordre d'exécution des différentes parties est donc exactement le même que celui de notre boucle while précédente. Le fait de placer tous les éléments avant le bloc d'instructions peut être trompeur et il faut bien garder en tête la notation sous forme de while équivalente, où cet ordre est bien mis en évidence.
Le fait de regrouper dans le for tous les éléments définissant les limites de la boucle permet de les séparer du bloc, qui contient les instructions à répéter. Avec cette notation, on sépare nettement les deux parties : les limites de la boucle et le contenu lui-même. Par exemple : for (colonne = 0; colonne < nb_colonnes; colonne++)
En lisant simplement cette ligne, on sait que la boucle commence avec un compteur colonne qui vaut 0, et qu'il est incrémenté à chaque itération de la boucle, jusqu'à valoir nb_colonnes. On peut ainsi en déduire que le bloc d'instructions qui suit sera répété exactement nb_colonnes fois.
Il est important de bien respecter cette séparation en évitant au maximum de mettre dans le bloc des instructions qui pourraient modifier les limites de la boucle. On ne modifiera donc dans le bloc ni la variable qui sert de compteur, ni les variables avec lesquelles on les compare dans la condition. Dans le cas où une modification de ces variables est nécessaire au sein du bloc, on préférera l'utilisation du while, pour lequel il apparaît nettement que la condition dépend de l'exécution des instructions du bloc.
Comme dans le cas de la boucle while, si le bloc d'instructions ne contient qu'une instruction, on pourra se passer des accolades.
Exercice : modifiez le programme suivant en utilisant des boucles for int main(){ int ligne = 0; int nb_lignes, nb_colonnes; scanf("%d%d", &nb_lignes, &nb_colonnes); while (ligne < nb_lignes) { int colonne = 0; while (colonne < nb_colonnes) { printf("#"); colonne++; } printf("\n"); ligne++; } return 0;}
Solution : int main(){ int ligne, colonne; int nb_lignes, nb_colonnes; scanf("%d%d", &nb_lignes, &nb_colonnes); for (ligne = 0; ligne < nb_lignes; ligne++) { for (colonne = 0; colonne < nb_colonnes; colonne++) printf("#"); printf("\n"); } return 0;}
On peut remarquer que le programme contient beaucoup moins de lignes lorsque l'on utilise la boucle for, c'est un autre de ses avantages. Il ne faut cependant pas que les 3 éléments du for soient trop longs, sinon la ligne peut devenir très longue et difficile à lire. On peut dans ce cas soit les placer sur différentes lignes, soit utiliser une boucle while.
5.1.3 Les boucles do...while
Même si la structure de boucle présentée un peu plus haut est la plus fréquente, elle est loin d'être la seule. Dans certains cas, l'ordre des éléments du while n'est pas très adapté. Nous avons déjà vu des cas où l'on était obligé de recourir à des astuces, pour utiliser le while. Prenons par exemple la correction du problème "Sans espaces", du chapitre 2 : #include int main(){ char caractere_lu = 'A'; while (caractere_lu != '\n') { scanf("%c", &caractere_lu); if (caractere_lu == ' ') printf("%c", '_'); else printf("%c", caractere_lu); } return 0;}
Dans ce problème, on veut exécuter le corps de la boucle tant que le dernier caractère lu n'est pas un '\n'. Le problème est que la lecture d'un caractère se fait à l'intérieur du bloc d'instructions de la boucle elle-méme, donc que lors du premier test de la condition, aucun caractère n'a encore été lu. Mettre une lecture d'un caractère avant le début du while pourrait fonctionner, avec quelques modifications, mais cela nécessiterait quoi qu'il arrive d'avoir deux instructions de lecture de caractère.
L'astuce utilisée consistait à initialiser la variable caractere_lu à une valeur qui permette à la condition du while d'être toujours vraie lors de la première exécution. Le corps de la boucle est ainsi toujours exécuté au moins une fois. Il s'agit cependant d'une astuce et le caractère 'A' utilisé pour l'initialisation ne correspond à rien, relativement à l'objectif du programme.
Pour éviter d'avoir à recourir à ce genre d'astuces, qui rendent le programme moins simple à lire et obligent à ajouter des instructions qui ne sont pas directement liées à l'objectif du programme, le langage C propose un autre type de boucle, qui permet d'écrire les choses un peu plus simplement : la boucle do...while, dont la structure est la suivante : do { //Instructions du corps de la boucle } while ();
Cette structure est très proche de celle du while, la différence étant simplement que la condition est placée après le bloc d'instructions. Avec cette structure, le corps de la boucle est exécuté au moins une fois, quoi qu'il arrive. Ce n'est qu'après, que la condition est testée, pour déterminer si l'exécution doit recommencer (si la condition est vraie).
Le mot clé do, qui signifie "faire" en anglais, n'a pas d'autre rôle que de bien mettre en valeur le fait que le bloc d'instructions qui suit est le corps d'une boucle. On remarquera que l'instruction while de cette structure se termine par un point virgule, ce qui n'était pas le cas de la boucle while simple.
En utilisant cette nouvelle structure, l'exemple précédent peut s'écrire plus simplement : #include int main(){ char caractere_lu; do { scanf("%c", &caractere_lu); if (caractere_lu == ' ') printf("%c", '_'); else printf("%c", caractere_lu); } while (caractere_lu != '\n'); return 0;}
On évite ainsi le recours à une "astuce".
Exercice : réécrivez le programme de l'exercice "Lecture binaire" du chapitre 2, en utilisant cette fois une boucle do...while.
Solution : #include int main(){ char caractere_lu; int total = 0; do { scanf("%c", &caractere_lu); if (caractere_lu == '1') total = total * 2 + 1; else if (caractere_lu == '0') total *= 2; } while (caractere_lu != '\n'); printf("%d\n", total); return 0; }
5.2 Tableaux à une dimension
5.2.1 Principe
La force des ordinateurs est d'être capable de réaliser un très grand nombre de fois des tâches relativement simples. Il peut s'agir par exemple de traiter une grande quantité de données. Si l'on doit réaliser une opération sur quelques milliers d'objets de même type, on ne va pas s'amuser à donner un nom différent à chacun d'entre eux. L'idée d'un tableau, "array" en anglais, est de donner un nom à ce groupe d'objets, puis de numéroter dans un certain ordre tous les objets qui appartiennent à ce groupe.
On a modélisé une variable comme étant une boîte simple dans laquelle on pouvait mettre une unique valeur modifiable. Dans le même esprit, un tableau est une grande boîte avec plusieurs compartiments. Chaque compartiment, on dira aussi "case du tableau", est un peu comme une variable : on peut y mettre une valeur et la modifier si besoin est.
Une propriété importante à vérifier est que tous les compartiments doivent contenir des objets de même type. On ne fabriquera que plus tard des boîtes avec des compartiments de natures différentes, pouvant accueillir des objets de types différents.
Le nombre de compartiments contenus dans le tableau est fixé au moment de sa fabrication. On appelle ce nombre la taille ou la longueur du tableau. Il n'est pas possible de changer la taille d'un tableau après sa création. Si on veut augmenter le nombre d'objets numérotés dans le groupe, il faudra fabriquer un autre tableau.
Comme on l'a dit, chaque compartiment est identifié par un numéro unique, que l'on nomme indice. L'élément situé dans le compartiment numéro i est dit "élément d’indice i". Considérons un tableau contenant taille compartiments. Attention, les numéros commencent à partir de 0 et non à partir de 1. C’est un peu bizarre au début, mais on s’y habitue très vite. Ainsi, le premier élément du tableau est d'indice 0, le deuxième d'indice 1, le troisième d’indice 2, etc... Enfin, le dernier est d'indice taille-1 (et non pas d’indice taille puisqu'on ne commence pas à 1).
Suivant la même démarche que pour les variables, on va commencer par la création des tableaux, puis on passera à leur manipulation. On mettra alors le tout en pratique sur plusieurs exemples.
5.2.2 Création et initialisation
La déclaration d'un tableau est une déclaration de variable, au même titre que les variables entières ou caractères. Pour créer une variable tableau, on utilise la syntaxe suivante : [];
Pour créer un tableau contenant 5 cases de type entier, appelé tableau_exemple, on écrira ainsi : int tableau_entiers[5];
Comme dans le cas des variables, on peut dès la déclaration, initialiser le contenu du tableau. Il faut alors indiquer toutes les valeurs contenues dans le tableau, séparées par des virgules, à l'intérieur d'accolades : int tableau_entiers[5] = { 4, 2, 15, 17, 8 };
Exercice : déclarez et initialisez un tableau contenant les voyelles de l'alphabet, en caractères minuscules et sans accents.
Solution : comme dans le cas des variables simples, on choisit un identifiant qui correspond au contenu du tableau. char voyelles[6] = { 'a', 'e', 'i', 'o', 'u', 'y' };
Dans le cas où l'on initialise le tableau dès sa déclaration, il est possible de ne pas spécifier sa taille : elle est déduite automatiquement du nombre d'éléments entre les accolades.
De même qu'une variable simple, un tableau occupe un certain espace en mémoire. La mémoire occupée est simplement la mémoire occupée par une variable du type des cases du tableau, multipliée par le nombre d'éléments du tableau.
Exercice : calculez l'espace mémoire utilisé par les tableaux suivants : int tableau_entiers[5] = { 4, 2, 15, 17, 8 }; char voyelles[6] = { 'a', 'e', 'i', 'o', 'u', 'y' }; double evolution_prix [10000];
Solution : int tableau_entiers[5] = { 4, 2, 15, 17, 8 };
Une variable de type int occupe 32 bits sur la plupart des machines, soit 4 octets. Un tableau de 5 entiers occupe donc 5 * 4 = 20 octets de mémoire. Sur les machines où un int occupe 2 octets, ce tableau occupera 10 octets de mémoire. char voyelles[6] = { 'a', 'e', 'i', 'o', 'u', 'y' };
Une variable de type char occupe 1 octet. Le tableau occupe donc 6 octets de mémoire. double evolution_prix [10000];
Une variable de type double occupe 64 bits soit 8 octets. Le tableau occupe donc 10000 * 8 = 80000 octets, soit environ 80ko (on considère dans ce cours qu'un ko, ou kilo-octet, correspond à 1024 octets, mais il peut en valoir 1000 selon certaines conventions).
Comme pour toute variable, lorsque l'on déclare un tableau sans l'initialiser, il peut en général contenir n'importe quelles valeurs (ce qui se trouvait dans la mémoire à cet endroit là, avant la création du tableau).
5.2.3 Lecture / Ecriture
Maintenant que nous savons comment déclarer et initialiser un tableau, nous allons pouvoir en manipuler les éléments. On peut appliquer la règle suivante :
Pour représenter une case d'un tableau et la manipuler, on place l'indice de cette case entre crochets, à côté de l'identifiant du tableau. Le tout peut alors être manipulé comme une simple variable dans laquelle on peut lire ou écrire, ou que l'on peut passer en argument à une fonction.
Pour afficher le contenu de la première case de notre tableau de voyelles, on écrira ainsi : printf("%c", voyelles[0]);
Si on souhaite la modifier et la remplacer par un 'A' majuscule, on écrira : voyelles[0] = 'A';
Si l'on souhaite maintenant transformer toutes les lettres du tableau en majuscules, on pourra le faire de la manière suivante : for (cur_voyelle = 0; cur_voyelle < 6; cur_voyelle++) voyelles[cur_voyelle] = voyelles[cur_voyelle] - 'a' + 'A';
On aurait aussi pu utiliser la fonction toupper de la bibliothèque standard.
On remarque que dans notre boucle for, on a dû indiquer directement la taille du tableau sous la forme d'un nombre : 6. Il n'y a en effet pas de moyen, en C, de calculer la taille d'un tableau et il faut donc l'indiquer soi-même. Pour que le code soit plus facilement lisible, on peut définir une constante : #define NB_VOYELLES 6
On peut ensuite écrire la boucle de manière plus facile à comprendre : for (cur_voyelle = 0; cur_voyelle < NB_VOYELLES; cur_voyelle++) voyelles[cur_voyelle] = voyelles[cur_voyelle] - 'a' + 'A';
Exercice : écrivez un programme qui affiche toutes les consonnes de l'alphabet en minuscules, avec un espace entre chaque paire de lettres.
Solution : une première méthode consiste à déclarer un tableau contenant toutes les consonnes, puis à le parcourir pour les afficher. #include #define NB_CONSONNES 20 int main(){ char consonnes[] = { 'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'z' }; int cur_consonne; for (cur_consonne = 0; cur_consonne < NB_CONSONNES; cur_consonne++) { printf("%c", consonnes[cur_consonne]); if (cur_consonne != NB_CONSONNES - 1) printf(" "); } printf("\n"); return 0;}
Une deuxième méthode consiste à ne stocker qu'un tableau de voyelles, beaucoup plus petit, puis à afficher toutes les lettres qui ne sont pas des voyelles. On peut écrire une fonction est_voyelle, qui renvoie 1 si une lettre est une voyelle et 0 sinon. #include #define NB_VOYELLES 6 int est_voyelle(char lettre){ char voyelles[] = { 'a', 'e', 'i', 'o', 'u', 'y' }; int cur_voyelle; for (cur_voyelle = 0; cur_voyelle < NB_VOYELLES; cur_voyelle++) if (voyelles[cur_voyelle] == lettre) return 1; return 0;} int main(){ char cur_lettre; for (cur_lettre = 'a'; cur_lettre < 'z'; cur_lettre++) { if (est_voyelle(cur_lettre) == 0) printf("%c", cur_lettre); if (cur_lettre != 'z') printf(" "); } printf("\n"); return 0;}
L'inconvénient de cette deuxième solution est qu'elle est légèrement plus lente, puisque pour tester si une lettre est une voyelle, on doit parcourir tout le tableau de voyelles. On verra plus tard comment écrire une version plus rapide de la fonction est_voyelle.
5.2.4 Passage en paramètre
Comme toute variable, un tableau peut être passé en paramètre à une fonction. On peut ainsi déclarer une fonction qui prend en paramètre un tableau de cinq entiers : void affiche_entiers(int entiers[5]);
La taille d'un tableau ne pouvant pas être déterminée à l'exécution, il sera cependant possible d'appeler cette fonction en lui passant en paramètre des tableaux de tailles différentes de 5. On peut d'ailleurs vouloir écrire une fonction qui affiche un tableau de taille quelconque. Il faut cependant que la fonction connaisse la taille du tableau, donc que celle-ci lui soit passée en paramètre : void affiche_entiers(int entiers[], int nb_entiers){ int cur_entier; for (cur_entier = 0; cur_entier < nb_entiers; cur_entier++) { printf("%d", entiers[cur_entier]); if (cur_entier != nb_entiers - 1) printf(" "); } printf("\n");}
Il existe une différence importante entre le passage en paramètre d'une variable de type simple et celui d'un tableau. On a vu au chapitre 3 que lorsque l'on passe un entier ou un caractère en paramètre à une fonction, sa valeur est copiée, avant d'être placée dans une nouvelle variable. Ce n'est pas le cas d'un tableau : lorsqu'un tableau est passé en paramètre, son contenu n'est pas copié avant la transmission. Si la fonction modifie certaines valeurs du tableau, elles seront également modifiées pour le programme appelant, comme le montre le programme suivant : #include void affiche_entiers(int entiers[], int nb_entiers){ int cur_entier; for (cur_entier = 0; cur_entier < nb_entiers; cur_entier++) { printf("%d", entiers[cur_entier]); if (cur_entier != nb_entiers - 1) printf(" "); } printf("\n");} void ajoute_un(int valeurs[], int nb_valeurs){ int cur_valeur; for (cur_valeur = 0; cur_valeur < nb_valeurs; cur_valeur++) valeurs[cur_valeur]++;} int main(){ int quantites[4] = {1, 2, 3, 4}; affiche_entiers(quantites, 4); ajoute_un(quantites, 4); affiche_entiers(quantites, 4); ajoute_un(quantites, 4); affiche_entiers(quantites, 4); return 0;}
Son exécution donnera l'affichage suivant : 1 2 3 42 3 4 53 4 5 6
Si l'on souhaite effectuer une copie du tableau avant de le transmettre en paramètre, il faudra en copier les éléments un par un dans un tableau de même taille : int quantites[4] = {1, 2, 3, 4}; int quantites2[4]; int cur_quantite; for (cur_quantite = 0; cur_quantite < 4; cur_quantite++) quantites2[cur_quantite] = quantites[cur_quantite];
5.2.5 Exercice : translation de polygône
Ecrivez un programme qui lit les coordonnées des sommets d'un polygône, puis effectue une translation de ce polygône de telle sorte que toutes les coordonnées de ses sommets soient positives, et qu'il touche l'axe des abscisses et l'axe des ordonnées.
La première ligne de l'entrée contient un entier N (3 <= N <= 20) : le nombre de sommets du polygône.
Chacune des N lignes suivantes contient deux nombres réels, séparés par un espace : les coordonnées x et y d'un sommet du polygône.
Vous devez afficher N lignes sur la sortie, contenant chacune deux réels : les coordonnées du sommet correspondant, après translation du polygône. Toutes vos coordonnées doivent être positives ou nulles, au moins un x et un y doivent être nuls (pas nécessairement sur le même sommet). Les sommets doivent être fournis dans le même ordre que dans l'entrée.
Par exemple, pour l'entrée suivante : 62.35 3.112.67 0.985.83 0.783.0 -4.21-1.22 -2.85-1.83 1.71
Vorte programme devra afficher (aux format d'affichage des double près) : 4.180000 7.3200004.500000 5.1900007.660000 4.9900004.830000 0.0000000.610000 1.3600000.000000 5.920000
Solution : on stocke les coordonnées x et y des sommets dans deux tableaux, puis on parcourt ces deux tableaux pour trouver le plus petit x et le plus petit y. On affiche alors les coordonnées des sommets en y soustrayant les valeurs obtenues : #include #define MAX_SOMMETS 20 double xSommet[MAX_SOMMETS];double ySommet[MAX_SOMMETS]; double min(double a, double b){ if (a < b) return a; else return b;} int main(){ int nbSommets, sommet; double xMin, yMin; scanf("%d", &nbSommets); for (sommet = 0; sommet < nbSommets; sommet++) scanf("%lf%lf", &xSommet[sommet], &ySommet[sommet]); xMin = xSommet[0]; yMin = ySommet[0]; for (sommet = 1; sommet < nbSommets; sommet++) { xMin = min(xMin, xSommet[sommet]); yMin = min(yMin, ySommet[sommet]); } for (sommet = 0; sommet < nbSommets; sommet++) printf("%lf %lf\n", xSommet[sommet] - xMin, ySommet[sommet] - yMin); return 0;}
5.3 Chaînes de caractères
5.3.1 Principe
L'une des principales utilisations des tableaux est celle qui permet de manipuler des chaînes de caractères. Nous avons vu dès le début de ce cours comment on pouvait manipuler des caractères individuellement et nous avons déjà pu afficher du texte grâce à la fonction printf. Nous n'avons cependant pas vu comment déclarer une variable pouvant contenir du texte. C'est maintenant possible, puisqu'en C, les chaînes de caractères sont stockées sous la forme de tableaux.
Pour déclarer une variable de type chaîne de caractères, on déclare simplement un tableau de caractères : char nom_asso[] = "france-ioi";
On peut ainsi accéder à chacun des caractères de la chaîne individuellement : printf("Premier caractère du nom de l'association : %c\n", nom_asso[0]);
On peut également afficher la chaîne complète avec la fonction printf, en utilisant "%s" : printf("Nom de l'association : %s\n", nom_asso)
Nous avons vu qu'il n'est pas possible de connaître la taille d'un tableau en C. Pour que l'on puisse déterminer le nombre de lettres de la chaîne de caractère, on utilise un marqueur qui en indique la fin. Ainsi, une chaîne de caractère est toujours suivie d'un caractère spécial, de code ASCII 0 (on peut le noter '\0'). Par exemple, la chaîne "test" est représentée par un tableau de 5 caractères, contenant les caractères 't', 'e', 's', 't' et '\0'. Les deux lignes suivantes sont donc parfaitement équivalentes : char texte[] = { 't', 'e', 's', 't', '\0' }; char texte[] = "test";
Il est également possible de lire une chaîne de caractères tapée au clavier, avec l'instruction scanf : char mot_lu[20]; scanf("%s", mot_lu);
De cette manière, on peut lire un mot à la fois : scanf commence à stocker dans le tableau fourni en paramètre, tous les caractères à partir du premier qui ne soit pas un espace, une tabulation ou un retour à la ligne, jusqu'au dernier qui ne soit pas un tel caractère. On peut ainsi considérer que scanf va lire un mot, même si celui-ci peut contenir des chiffres ou des caractères de ponctuation (et plus généralement, tout ce qui n'est pas un espace, une tabulation ou un retour à la ligne). Par exemple, si l'on tape la phrase suivante : hello world!
La première fois que scanf sera appelée avec ce texte en entrée, le premier mot "hello" sera stocké dans le tableau passé en paramètre, suivi du caractère '\0'. Lors d'un appel suivant, ce sera le texte "world!" qui sera stocké (toujours suivi d'un '\0').
On peut remarquer que contrairement à l'utilisation de scanf pour lire un entier ou un simple caractère, on ne place pas de caractère "&" devant l'identifiant du tableau. Nous verrons pourquoi plus tard, essayez de vous en souvenir en attendant.
Un gros inconvénient de cette méthode est que dans le cas où l'on tape au clavier un mot de plus de 19 lettres, le tableau déclaré ne contiendra pas suffisamment de cases pour stocker le mot complet. Pour un mot de 20 lettres, les caractères seront stockés dans le tableau, mais le '\0' qui suit sera stocké en dehors. C'est ce que l'on appelle un dépassement de tableau. Cela a pour conséquence de modifier le contenu de la mémoire qui suit l'endroit où est stocké le tableau. Si d'autres variables s'y trouvent, ce qui est généralement le cas, elles seront effacées et votre programme aura un fonctionnement erronné.
Il existe plusieurs méthodes pour éviter ce problème. La plus simple pour l'instant consiste à indiquer à scanf le nombre maximum de caractères qui doit être lu et stocké dans le tableau. On place ce nombre juste devant le s, dans le "%s" : char mot_lu[20]; scanf("%19s", mot_lu);
Cette méthode a des inconvénients, entre autres le fait que les caractères suivants du mot seront lus par le prochain scanf. Nous verrons plus tard d'autres méthodes plus pratiques. En attendant, on considérera que les entrées de vos programmes ont toujours le format attendu.
5.3.2 exercices
Exercice : écrivez une fonction qui lit une ligne de caractères minuscules non accentués, sans espaces, puis affiche le contenu de cette ligne en passant tous les caractères en majuscules. On vous assure que la ligne ne contiendra pas plus de 200 caractères.
Solution : #include #include int main(){ char ligne[201]; int pos_caractere; scanf("%s", ligne); for (pos_caractere = 0; ligne[pos_caractere] != '\0'; pos_caractere++) printf("%c", toupper(ligne[pos_caractere])); printf("\n"); return 0;}
Exercice : écrivez une fonction qui affiche "Entrez le mot de passe :" suivi d'un retour à la ligne, puis lit une ligne de caractères sans espaces ni tabulations. Le programme doit afficher le texte "bienvenue" si le texte entré est "sesame" et recommencer sinon. Vous pouvez considérer que l'utilisateur ne tapera pas plus de 80 caractères sur la ligne.
Solution : #include int verifie_passe(char tentative[]){ char passe[] = "sesame"; int pos_caractere = 0; do { if (passe[pos_caractere] != tentative[pos_caractere]) return 0; pos_caractere++; } while (passe[pos_caractere] != '\0'); return 1;} int main(){ char tentative[81]; do { printf("Entrez le mot de passe :\n"); scanf("%80s", tentative); } while (verifie_passe(tentative) == 0); printf("bienvenue\n"); return 0;}

5.4 Tableaux à plusieurs dimensions
5.4.1 Déclaration et accès.
Les tableaux que nous avons vus jusqu'à présent étaient des tableaux à une dimension : ils contenaient un certain nombre de cases, auxquelles on pouvait accéder par un index. Il est possible de créer des tableaux à deux dimensions, c'est-à-dire où les cases sont organisées sous la forme d'un certain nombre de lignes et de colonnes. Il est en fait possible de créer des tableaux avec autant de dimensions que l'on souhaite, même si la plupart du temps, on se limitera à deux ou trois.
Pour déclarer une variable comme étant un tableau à plusieurs dimensions, on généralise le principe de la déclaration à une dimension, en plaçant entre crochets, après l'identifiant, le nombre de cases dans chaque dimension.
Voici par exemple comment déclarer un tableau de 3 lignes par 2 colonnes, contenant des entiers : int tableau2D[3][2];
On peut initialiser ce tableau dès sa déclaration, en généralisant le principe utilisé pour les tableaux à une dimension : on place entre accolades, séparées par des virgules, le contenu de chaque ligne. Chaque ligne est elle-même représentée comme un tableau, avec entre accolades, les différentes valeurs séparées par des virgules : int tableau2D[3][2] = {{1, 2}, {3, 4}, {5, 6}};
On peut représenter le contenu de ce tableau :
1
2
3
4
5
6
On peut ensuite accéder en lecture ou en écriture aux cases du tableau, en plaçant entre crochets, après l'identifiant, les coordonnées de la case à laquelle on veut accéder. Voici par exemple un programme qui affiche le tableau ci-dessus : #include int main(){ int tableau2D[3][2] = {{1, 2}, {3, 4}, {5, 6}}; int ligne, colonne; for (ligne = 0; ligne < 3; ligne++) { for (colonne = 0; colonne < 2; colonne++) printf("%d ", tableau2D[ligne][colonne]); printf("\n"); } return 0;}
De même, si l'on veut modifier une case du tableau, on peut écrire : tableau2D[2][1] = 42;
Le choix de la première dimension pour représenter les lignes et de la deuxième pour représenter les colonnes est totalement arbitraire. On pourrait aussi bien les inverser, cela n'aurait pas de conséquence notable. Il est cependant bon de garder toujours les mêmes notations, pour éviter les erreurs dues à des inversions. En général, on place la ligne avant la colonne. Cependant, dans les cas où on utilise des notations sous la forme d'abscisses (x) et d'ordonnées (y), on écrit plutôt tab[x][y], donc l'inverse.
Exercice : Ecrivez la déclaration d'un tableau de 2 lignes de 7 colonnes, avec un caractère dans chaque case, puis affichez-le. L'affichage doit être le suivant : BonjourMonde !
Solution : #include int main(){ char bonjour_monde[2][7] = { {'B', 'o', 'n', 'j', 'o', 'u', 'r'}, {'M', 'o', 'n', 'd', 'e', ' ', '!'}}; int ligne, colonne; for (ligne = 0; ligne < 2; ligne++) { for (colonne = 0; colonne < 7; colonne++) printf("%c", bonjour_monde[ligne][colonne]); printf("\n"); } return 0;}
Pour les tableaux de dimension 3 ou plus, le principe est le même. Voici par exemple la déclaration d'un tableau à 3 dimensions : int tableau3D[4][3][2] = {{{1, 2}, {3, 4}, {5, 6}}, {{2, 54}, {61, 5}, {0, 8}}, {{1, 7}, {43, 8}, {6, 23}}, {{3, 9}, {12, 4}, {5, 5}}};
Un tableau à deux dimensions a toujours le même nombre d'éléments sur chacune de ses lignes (ou chacune de ses colonnes). Cela s'applique également en 3 dimensions. D'autre part, dès que l'on crée un tableau à plus d'une dimension, il faut obligatoirement définir le nombre de cases de chaque dimension autre que la première. Ces valeurs sont nécessaires pour que le compilateur sache où trouver le contenu de chaque case.
5.4.2 Stockage en mémoire des tableaux
Un tableau à plusieurs dimensions prend autant de mémoire qu'il ne contient de cases, multiplié par l'espace mémoire utilisé par chaque case. Par exemple, un tableau de 3 lignes de 2 int occupera 3*2 int, soit 3*2*4 = 24 octets sur les machines où un int occupe 4 octets.
Exercice : indiquez la taille en octets utilisée par les tableaux suivants : char laby[1000][1000]; double cube[10][10][10];
Solution : char laby[1000][1000];
Ce tableau contient 1000*1000 caractères, soit un million d'octets. (Environ un méga-octet). double cube[10][10][10];
Ce tableau contient 10*10*10 nombres réels de 8 octets, ce qui fait 8000 octets, soit environ 8ko.
Les cases d'un tableau à deux dimensions sont stockées en mémoire les unes après les autres, dans le même ordre que celui dans lequel on fournit les valeurs lors de l'initialisation. Par exemple : int tableau2D[3][2] = {{1, 2}, {3, 4}, {5, 6}};
Dans la mémoire de l'ordinateur, le contenu sera donc simplement dans l'ordre :
1
2
3
4
5
6
Ceci a relativement peu d'importance, mais est bon à savoir dans plusieurs cas :
Si vous dépassez par erreur les limites du tableau, par exemple en écrivant : tableau2D[0][2] = 42;
Vous essayez d'écrire sur la 3ème colonne de la première ligne d'un tableau qui ne contient que 2 colonnes. En pratique, c'est la première colonne de la deuxième ligne qui sera modifiée, puisqu'elle se trouve en mémoire à l'endroit où se trouverait la valeur s'il y avait effectivement 3 colonnes :
1
2
42
4
5
6
En fait, la mémoire pouvant être vue comme un simple tableau à une dimension, pour accéder à la case tableau2D[ligne][colonne], le compilateur effectue une multiplication. La case correspondante est en fait à la position (ligne * nb_colonnes + colonne) après la toute première case du tableau.
Exercice : Déterminez l'état du tableau après l'instruction d'écriture suivante : int tableau2D[3][4] = {{1, 2, 3, 4}, {3, 4, 5, 6}, {5, 6, 7, 8}}; tableau2D[1][6] = 42;
Solution :
1
2
3
4
3
4
5
6
5
6
42
8
Ecrire en dehors des limites d'un tableau est bien sûr une erreur et doit absolument être évité. Notre explication a simplement pour but de vous aider à comprendre comment sont stockés les tableaux en mémoire, et à vous aider à comprendre ce qui peut se passer en cas d'erreur.
Une autre conséquence de l'ordre dans lequel sont stockées les cases en mémoire est qu'il est plus rapide de parcourir de grands tableaux dans l'ordre dans lequel ils sont stockés. Par exemple : int tab[1000][1000]; int ligne, colonne; for (ligne = 0; ligne < 1000; ligne++) for (colonne = 0; colonne < 1000; colonne++) tab[ligne][colonne] = 0;
L'exécution de ce code sera plus rapide que le suivant, même si le résultat est exactement identique : int tab[1000][1000]; int ligne, colonne; for (colonne = 0; colonne < 1000; colonne++) for (ligne = 0; ligne < 1000; ligne++) tab[ligne][colonne] = 0;
Ceci n'a réellement d'importance que dans le cas où la vitesse est un facteur très important pour vous, mais autant prendre l'habitude de parcourir ses tableaux dans cet ordre, tant que cela ne nuit pas à la clarté du code.
5.5 Variables globales
Lorsque l'on utilise des tableaux, il est souvent peu pratique d'avoir à les passer en paramètre à toutes les fonctions qui les manipulent, d'autant qu'il faut souvent passer aussi les dimensions de ce tableau. Il est possible en C de déclarer des variables qui seront accessibles directement par toutes les fonctions. C'est ce que l'on appelle des variables globales.
Pour déclarer une variable globale, il suffit de placer sa déclaration (et éventuellement son initialisation) en dehors de toute fonction, et avant le code de la première fonction qui va utiliser cette variable. En général on les place tout au début du programme : #include int tableau2D[3][2] = {{1, 2}, {3, 4}, {5, 6}}; int main(){ int ligne, colonne; for (ligne = 0; ligne < 3; ligne++) { for (colonne = 0; colonne < 2; colonne++) printf("%d ", tableau2D[ligne][colonne]); printf("\n"); } return 0;}
Ceci fonctionne avec n'importe quel type de variable. Il faut cependant faire attention à ne pas déclarer de variables à l'interieur des fonctions (on les appelle des variables locales), qui portent le même nom qu'une variable globale. Si vous le faites, la variable à laquelle vous accéderez dans cette fonction, par cet identifiant, sera la variable locale, et la variable globale n'y sera plus accessible.
L'utilisation des variables globales n'est pas trop conseillée en programmation, en général et il faut les réduire au minimum. Nous verrons plus tard des moyens d'éviter de trop en utiliser, mais en attendant, vous pouvez les utiliser quand cela permet de simplifier votre programme.

Commentaires