.. _08-arrays: Les tableaux ============ La manipulation d'une collection d'objets se fait en Python avec une :class:`list`. La structure de données la plus proche en langage C est le tableau. De ce que l'on a vu pour le moment, C ne manipule que des types primitifs (et des pointeurs) et on aura donc des tableaux d'entiers, de nombres rééls ou de caractères (et de pointeurs). Le tableau de caractères est la façon de représenter une chaîne de caractères en C. Les tableaux C ne contiennent que des données homogènes. Un tableau d'entiers ne peut contenir que des entiers. Les données stockées dans un tableau sont stockées dans des cases mémoires contigües. On exploitera cette caractéristique lorsqu'on manipulera les tableaux avec des pointeurs. .. warning:: On verra dans le chapitre :ref:`10-structures` que l'on peut aggréger dans une ``structure`` plusieurs types primitifs pour les manipuler comme une entité unique. Sans surprise, on pourra manipuler une collection de ces ``structure`` dans un tableau de ``structure``. Déclaration ----------- En C, comme toute variable, un tableau doit être déclaré avec la syntaxe ``type nom [ taille ];``. Ainsi : - ``int x[10];`` déclare un tableau ``x`` permettant de stocker 10 entiers ; - ``double y[10];`` déclare un tableau ``y`` permettant de stocker 10 rééls ; - ``char z[10];`` déclare un tableau ``z`` permettant de stocker une chaîne de 10 caractères ; - ``int* p[10];`` déclare un tableau ``p`` permettant de stocker 10 pointeurs vers des entiers. La déclaration définit un espace **réservé** qui doit être plus vaste que l'espace rééllement **utilisé**. Il y a ici une différence majeure entre C et Python : - Python fait croitre/décroitre dynamiquement (au cours de l'exécution du programme) et automatiquement (sans intervention du programmeur) la taille des listes ; - en C la déclaration est statique (définie à la compilation) et la mémoire allouée de cette façon ne peut pas être modifiée au cours de l'exécution du programme. On verra dans le chapitre :ref:`11-allocdyn` comment gérer la mémoire dynamiquement (c'est à dire au cours de la vie du programme). Mais contrairement à Python, ça nécessitera l'intervention du programmeur. Initialisation -------------- Dans certains langages, la déclaration d'un tableau initialise également les valeurs à 0. Ce n'est pas le cas en C. Ainsi, le programme suivant, qui déclare un tableau (ligne 5) mais ne l'initialise pas, conduit à un résultat imprévisible (et non reproductible) : .. code-block:: c :linenos: //tab.c #include int main() { int x[10] ; // ABSENCE d'INITIALISATION for ( int i = 0; i < 10; i++ ){ printf("%d\n", x[i]); } return 0; } Lorsqu'on l'exécute dans un terminal, on obtient des valeurs étranges:: -1409518872 32573 -2096975408 22037 0 0 -2096975744 22037 898055648 32766 A la déclaration, le compilateur réserve 40 octets (4 octets pour chacun des 10 éléments du tableau) quelque part dans la mémoire. L'affichage reflète l'état de cette portion de mémoire. Encore plus ennuyeux, l'affichage d'un élément qui n'appartient pas au tableau est un code C valide (en Python ça déclenche une erreur) : .. code-block:: c printf("%d ", x[10]); Dans le meilleur des cas, une autre valeur étrange est retournée. Dans le pire des cas, cet accès illégal peut provoquer le crash du programme :: Segmentation fault Il est donc indispensable de : #. déclarer le tableau pour réserver de la place en mémoire ; #. initialiser ses valeurs, sinon ce sont des nombres aléatoires ; #. et contrôler l'accès aux éléments d'un tableau, sinon on risque un accès à une zone illégale de la mémoire et un crash du programme. Pour les tableaux de taille réduite, on peut déclarer/initialiser en une seule instruction. La taille du tableau n'est pas requise, elle est déduite de la partie RHS (Right Hand Side) de l'affectation : .. code-block:: c int x[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} ; On peut également (d'abord) déclarer, puis (ensuite) initialiser le tableau dans une autre partie du programme : .. code-block:: c int x[10] ; for ( int i = 0; i < 10; i++ ){ x[i] = i ; printf("%d ", x[i]); } .. warning:: La déclaration/initialisation globale dans une même instruction est correcte : .. code-block:: c int x[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} ; La déclaration, suivie de l'initialisation élément par élément est correcte également : .. code-block:: c int x[10]; for (int i=0 ; i<10 ; i++) { x[i] = i; } Mais la déclaration, suivie d'une initialisation globale échoue à la compilation : .. code-block:: c int x[10]; x = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; Le compilateur renvoie une erreur de compilation car hors déclaration, l'accolade est utilisée pour délimiter un bloc de code: .. code-block:: bash error: expected expression before ‘{’ token x = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; Tableau multidimensionnel ------------------------- Le langage C permet la manipulation de tableaux multi dimensionnels avec la syntaxe ``type nom [ x ][ y ];`` qui déclare un tableau de ``x`` lignes et ``y`` colonnes. Voici deux façons de déclarer / initialiser un tableau à 2 dimensions. La première fait apparaître explicitement la structuration en lignes et en colonnes : .. code-block:: c int x[2][3] = { {0, 1, 2} , {3, 4, 5} }; La seconde non. Le tableau est rempli de gauche à droite et de haut en bas (ligne par ligne) : .. code-block:: c int x[2][3] = {0, 1, 2, 3, 4, 5}; Chacune des deux déclaration / initialisation conduit au même résultat : .. image:: ../images/tableau.png :scale: 100% L'accès à un des éléments est direct : ``x[1][2] = 5``. .. _recherche-dans-un-tableau: Exercice 1 : recherche dans un tableau -------------------------------------- L'objectif de cet exercice est de se familiariser avec les tableaux en écrivant quelques fonctions pour les manipuler. Le code source sera stocké dans le fichier :file:`tab.c`. Initialisation .............. Dans la fonction principale :func:`main` : - déclarer un tableau ``t`` d'entiers capable de stocker 100 valeurs ; - déclarer un entier ``size`` qui définit la taille effective du tableau. Initialiser à 10 dans la même instruction ; - écrire une boucle d'initialisation du tableau ``t`` en lui affectant ``size`` valeurs aléatoires entre 0 et 49 (lire ci dessous). A chaque itération, afficher la valeur dans le terminal ; .. tip:: La fonction :func:`rand` ne prend pas de paramètres et retourne un entier choisi aléatoirement entre 0 et une valeur dépendante de la machine. L'instruction ``rand() % n`` retourne une valeur entre ``0`` et ``n-1``. Le prototype de :func:`rand` est dans :file:``. .. int t[100]; // t peut stocker jusqu'à 100 valeurs int size = 10; for (int i = 0; i < size; i++) { t[i] = rand() % 50; printf("%d\n", t[i]); } Minimum ....... Ecrire la fonction secondaire :func:`mini` retournant la valeur minimale contenue dans le tableau. Le prototype de la fonction (c'est à dire le code de la fonction dont le corps a été remplacé par un ``;``) est défini ci dessous. Contrairement à Python, le tableau C ne connait pas sa taille. Lors de l'écriture de fonctions manipulant les tableaux, on passera donc systématiquement en paramètres, le tableau lui même **et** la taille du tableau. .. code-block:: c int mini(int t[], int size); Tester la conformité de la fonction en déclenchant son appel depuis :func:`main` et en affichant la valeur retournée. Un exemple d'exécution est donné plus bas. .. int mini(int t[], int size) { int min = 999; for (int i = 0; i < size; i++) if (t[i] < min) min = t[i]; return min; } Indice du minimum ................. Ecrire la fonction secondaire :func:`imini` retournant l'indice de la valeur minimale contenue dans le tableau. Le prototype de la fonction est défini ci dessous. .. code-block:: c int imini(int t[], int size); Tester la conformité de la fonction en déclenchant son appel depuis :func:`main` et en affichant la valeur retournée. Un exemple d'exécution est donné plus bas. .. int imini(int t[], int size){ int min = 999; int imin; for (int i=0 ; i max){ max = t[i]; imax = i; } return imax; } Indice du maximum ................. Ecrire la fonction secondaire :func:`imaxi` retournant l'indice de la valeur maximale contenue dans le tableau. Le prototype de la fonction est défini ci dessous. .. code-block:: c int imaxi(int t[], int size); Tester la conformité de la fonction en déclenchant son appel depuis :func:`main` et en affichant la valeur retournée. Un exemple d'exécution est donné plus bas. .. int imaxi(int t[], int size) { int imax = 0; int max = -999; for (int i = 0; i < size; i++) if (t[i] > max) imax = i; return imax; } Somme ..... Ecrire la fonction secondaire :func:`somme` retournant la somme des éléments du tableau. Le prototype de la fonction est défini ci dessous. .. code-block:: c int somme(int t[], int size); Tester la conformité de la fonction en déclenchant son appel depuis :func:`main` et en affichant la valeur retournée. Un exemple d'exécution est donné plus bas. .. int somme(int t[], int size) { double sum = 0; for (int i = 0; i < size; i++) sum += t[i]; return sum; } Moyenne ....... Ecrire la fonction secondaire :func:`moyenne` retournant la moyenne des éléments du tableau. Utiliser une des fonctions précédentes pour effectuer le calcul sans redondance de code. On portera une attention particulière au type des entités manipulées. On peut modifier le type d'une expression avec un cast comme on l'a déjà vu dans l'exercice sur les nombres premiers du chapitre :ref:`05-controle`. Le prototype de la fonction est défini ci dessous. .. code-block:: c double moyenne(int t[], int size); Tester la conformité de la fonction en déclenchant son appel depuis :func:`main` et en affichant la valeur retournée. Un exemple d'exécution est donné plus bas. .. double moyenne(int t[], int size) { return (double)somme(t, size) / size; } Comptage ........ Ecrire la fonction secondaire :func:`compte` retournant le nombre d'éléments du tableau compris entre deux valeurs. Le prototype de la fonction est défini ci dessous. .. code-block:: c int compte(int t[], int size, int lowest, int highest); Choisir les paramètres ``lowest`` et ``highest`` en fonction des valeurs générées dans le tableau. Tester la conformité de la fonction en déclenchant son appel depuis :func:`main` et en affichant la valeur retournée. Un exemple d'exécution est donné ci dessous. .. int compte(int t[], int size, int lowest, int highest) { int count = 0; for (int i = 0; i < size; i++) { if (t[i] < lowest) continue; if (t[i] > highest) continue; count++; } return count; } Trié ou pas ? ............. Ecrire la fonction secondaire :func:`est_trie` retournant ``1`` si le tableau est croissant, ``-1`` s'il est décroissant, et ``0`` sinon. Le prototype de la fonction est défini ci dessous. .. code-block:: c int est_trie(int t[], int size); Tester la conformité de la fonction en déclenchant son appel depuis :func:`main` sur des tableaux de nature différentes (trié croissant, trié décroissant, pas trié) et en affichant la valeur retournée. Un exemple d'exécution est donné ci dessous. .. int est_trie(int t[], int size) { int tc = 1; int td = 1; int i, ref; // Le tableau est il croissant ? i = 0; ref = t[0]; while (i < size && tc) { if (t[i] < ref) { tc = 0; } i++; } if (tc) return 1; // Le tableau est il décroissant ? i = 0; ref = t[0]; while (i < size && td) { if (t[i] > ref) { td = 0; } i++; } if (td) return -1; // Le tableau n'est pas ordonné return 0; } Exemple d'exécution ................... Une fois entièrement terminé, votre programme doit s'exécuter comme ci dessous (les valeurs peuvent différer) :: $ gcc -std=c99 -Wall -Wextra tab.c -o tab $ ./tab 33 36 27 15 43 35 36 42 49 21 minimum : 15 indice du minimum : 3 maximum : 49 indice du maximum : 8 somme : 337 moyenne : 33.700000 4 valeurs de t comprises entre 36 et 43 est trié ? 0 Compilation modulaire ..................... Comme on l'a vu dans le chapitre :ref:`02-compilation`, il peut être intéressant de fabriquer un fichier objet séparé contenant uniquement des fonctions d'une même famille. Pour mettre en oeuvre ce mécanisme, structurer le programme précédent en trois fichiers : - un fichier :file:`utils.c` ne contenant que les fonctions :func:`mini`, :func:`imini`, :func:`maxi`, :func:`imaxi`, :func:`somme`, :func:`moyenne` et :func:`compte`. Ce fichier n'est pas destiné à produire d'exécutable ; - un fichier :file:`utils.h` ne contenant que le prototype des fonctions précédentes ; - un fichier :file:`tab2.c` contenant la fonction :func:`main` faisant appel aux fonctions de :file:`utils.c`. Ne pas oublier la directive ``#include`` correspondante. Vous compilerez indépendemment les deux fichiers source:: $ gcc -std=c99 -Wall -Wextra -c utils.c $ gcc -std=c99 -Wall -Wextra -c tab2.c Si la compilation s'est déroulée sans erreur, il y a maintenant dans le répertoire de travail : - les fichiers sources :file:`tab2.c` et :file:`utils.c` ; - le fichier d'en têtes :file:`utils.h` ; - et les fichiers objet correspondants :file:`tab2.o` et :file:`utils.o`. Le résultat de la commande :command:`ls`:: $ ls tab2.c tab2.o utils.c utils.h utils.o Le fichier exécutable :file:`tab2` est produit à partir des deux fichiers objet :file:`tab2.o` et :file:`utils.o` avec la commande :: $ gcc -std=c99 -Wall -Wextra utils.o tab2.o -o tab2 Rigoureusement, cette phase ne réalise que l'édition de liens. L'exécution fournit le résultat attendu:: $ ./tab2 33 36 27 15 43 35 36 42 49 21 minimum : 15 indice du minimum : 3 maximum : 49 indice du maximum : 8 somme : 337 moyenne : 33.700000 4 valeurs de t comprises entre 36 et 43 Manipulation avec des pointeurs ------------------------------- Un tableau est une zone réservée de la mémoire, possédant les caractéristiques suivantes : - une adresse de début ; - un type d'élément ; - une taille (en nombre d'éléments). La taille est définie lors de la déclaration du tableau et ne peut pas être modifiée par la suite. .. warning:: Contrairement à Python, hors de la fonction déclarante, un tableau C ne connait pas le nombre de ses éléments. Il y a plusieurs conséquences : - on doit impérativement passer la taille du tableau en paramètre des fonctions manipulant les tableaux ; - on ne peut pas déterminer la taille d'un tableau en utilisant la syntaxe ``sizeof(t)/sizeof(t[0])`` hors de la fonction déclarante. Puisqu'un tableau est caractérisé par l'adresse de son premier élément, il est donc naturel d'imaginer manipuler un tableau avec des pointeurs. On va même voir que ça donne une grande flexibilité. Considérons le programme suivant qui initialise un tableau avec les premières valeurs de la suite de Fibonacci. .. code-block:: C :linenos: // tab3.c #include #define SIZE 20 int main() { // Déclaration int x[100]; // int* p = x; // Initialisation x[0] = 1; x[1] = 1; for (int i = 2; i < SIZE; i++) x[i] = x[i - 1] + x[i - 2]; } On peut afficher les valeurs de façon traditionnelle, avec la syntaxe ``x[i]`` : .. code-block:: C :linenos: for (int i = 0; i < 10; i++) printf("x[%d] = %d\n",i, x[i]); Le résultat est sans surprise:: x[0] = 1 x[1] = 1 x[2] = 2 x[3] = 3 x[4] = 5 x[5] = 8 x[6] = 13 x[7] = 21 x[8] = 34 x[9] = 55 Observons le code suivant qui affiche respectivement le tableau sous forme de pointeur, l'adresse de la variable déclarée comme tableau, et l'adresse du premier élément du tableau: .. code-block:: C printf(" x = %p\n", x); printf(" &x = %p\n", &x); printf("&x[0] = %p\n", &x[0]); Le résultat peut sembler étonnant au premier abord, puisque les 3 valeurs sont identiques:: x = 0x7fffe5322070 &x = 0x7fffe5322070 &x[0] = 0x7fffe5322070 Cela signifie que : - le nom de la variable qui désigne un tableau est en fait... un pointeur ; - qui représente l'adresse du tableau global ; - et qui n'est rien d'autre que l'adresse de son premier élément. L'équivalence tableau - pointeur est donc naturelle parce que pour le langage C ces deux notions se confondent ! On peut tirer profit de cette équivalence pour manipuler différemment les éléments d'un tableau. Puisque ``x`` est un pointeur vers le début du tableau : - ``x+i`` est un pointeur vers la ième case du tableau ; - et ``*(x+i)`` est le contenu de la ième case du tableau. .. note:: Si ``x`` est un pointeur, dans l'expression ``x+1``, ``1`` ne représente pas un entier mais la taille d'un élément du tableau ! C'est ce qu'on appelle l'arithmétique des pointeurs. Ainsi ``x+1`` décalera l'adresse ``x`` de : - ``4`` octets si c'est un tableau d'entiers ; - ``8`` octets si c'est un tableau de doubles ; - ``1`` octet si c'est un tableau de char ; Ainsi, puisque la syntaxe ``*(x+i)`` est strictement équivalente à ``x[i]``, le code ci dessous : .. code-block:: C for (int i = 0; i < 10; i++) printf("*(x+%d) = %d\n",i ,*(x+i) ); produit le même résultat que précédemment. Puisque l'on manipule des pointeurs, on peut observer l'occupation mémoire avec l'opérateur ``&`` : .. code-block:: C for (int i = 0; i < 10; i++) printf("&x[%d] = %p\n",i , x+i ); Le résultat de l'exécution:: &x[0] = 0x7fffe5322070 &x[1] = 0x7fffe5322074 &x[2] = 0x7fffe5322078 &x[3] = 0x7fffe532207c &x[4] = 0x7fffe5322080 &x[5] = 0x7fffe5322084 &x[6] = 0x7fffe5322088 &x[7] = 0x7fffe532208c &x[8] = 0x7fffe5322090 &x[9] = 0x7fffe5322094 Tout ceci est résumé dans le graphique suivant. .. image:: ../images/tab_pointeurs.png :align: center .. quiz:: quizz-01 :title: Tableaux et pointeurs (1) - Quel est le type des éléments du tableau ? :quiz:`{"type":"FB","answer":"int", "size":5}` - :quiz:`{"type":"TF","answer":"T"}` Le plan d'adressage est cohérent avec le type. - :quiz:`{"type":"TF","answer":"T"}` Les cases mémoires réservées pour chaque élément du tableau sont contigües. Un programmeur étourdi a réalisé le même programme en utilisant un tableau de doubles pour stocker les éléments de la suite de Fibonacci. Inférer le résultat de l'exécution précédente en faisant l'hypothèse que l'adresse de départ est la même. .. quiz:: quizz-02 :title: Tableaux et pointeurs (2) - Quel est l'écart entre deux cases mémoire consécutives ? :quiz:`{"type":"FB","answer":"8", "size":5}` - Quel est le gaspillage mémoire (en octets) sur un tableau de 100 cases ? :quiz:`{"type":"FB","answer":"400", "size":5}` Exercice 2 : ordonnancement de 2 tableaux ----------------------------------------- L'objectif de cet exercice est de parcourir les valeurs de deux tableaux ``t1`` et ``t2`` déja triés par ordre croissant et de produire un affichage ordonné de l'ensemble de leurs valeurs. Le code source sera stocké dans le fichier :file:`order.c`. Un exemple d'exécution:: $ gcc -std=c99 -Wall -Wextra order.c -o order $ ./order Tableau t1 : 2, 7, 10, 15, 21, 23, Tableau t2 : 1, 1, 7, 9, 15, Valeurs ordonnées : 1, 1, 2, 7, 7, 9, 10, 15, 15, 21, 23, La fonction principale :func:`main` ................................... Déclarer 2 tableaux ``t1`` et ``t2`` pouvant contenir 20 nombres entiers. Déclarer 2 entiers ``size1`` et ``size2`` (représentant le nombre de valeurs effectives stockées dans ``t1`` et ``t2``) avec des valeurs aléatoires comprises entre 10 et 19. La première valeur de chacun des tableaux sera un nombre aléatoire entre 0 et 4. Les valeurs suivantes seront obtenues par des incréments aléatoires compris entre 0 et 9. Etes vous convaincu que les tableaux sont croissants par construction ? Lorsque les deux tableaux ont été initialisés, afficher leur contenu, comme dans l'exemple d'exécution ci dessus. .. int main() { int size1; int size2; int t1[100]; // t peut stocker jusqu'à 100 valeurs int t2[100]; // t peut stocker jusqu'à 100 valeurs size1 = 10 + rand() % 10; size2 = 10 + rand() % 10; // Remplissage de t1 t1[0] = rand() % 5; for (int i = 1; i < size1; i++) { t1[i] = t1[i - 1] + rand() % 10; } // Affichage de t1 printf("Tableau t1 : "); for (int i = 0; i < size1; i++) { printf("%d, ", t1[i]); } printf("\n"); // Remplissage de t2 t2[0] = rand() % 5; for (int i = 1; i < size2; i++) { t2[i] = t2[i - 1] + rand() % 10; } // Affichage de t2 printf("Tableau t2 : "); for (int i = 0; i < size2; i++) { printf("%d, ", t2[i]); } printf("\n"); printf("Valeurs ordonnées : \n"); order_and_display(t1, size1, t2, size2); return 0; } L'ordonnancement ................ Créer une fonction secondaire :func:`order_and_display` dont le prototype est: .. code-block:: c int order_and_display(int* t1, int size1, int* t2, int size2); L'objectif de cette fonction est de produire un affichage ordonné de **l'ensemble** des valeurs des deux tableaux passés en argument, comme dans l'exemple d'exécution ci dessus. La taille des tableaux peut différer. La contrainte est de travailler avec des tableaux passés sous forme de pointeurs. On se rappellera donc que la syntaxe ``x[i]`` est strictement équivalente à la syntaxe ``*(x+i)``. Par défaut la source du générateur de nombres aléatoires est constante. L'intérêt est que la même suite est générée à chaque exécution. C'est intéressant d'avoir quelque chose de reproductible pour la mise au point du programme. Si on souhaite générer une séquence différente à chaque exécution, on initialise la source avec la fonction :func:`srand` **avant** de faire appel à :func:`rand`: .. code-block:: c #include ... srand(time(0)); .. int order_and_display(int* t1, int size1, int* t2, int size2) { int k1 = 0; int k2 = 0; if (size1 > size2) { while (k2 < size2) { if (*(t1+k1) < *(t2+k2) ) { printf("%d, ", *(t1+k1) ); k1++; } else { printf("%d, ", *(t2+k2) ); k2++; } } while (k1 < size1) { printf("%d, ", *(t1+k1) ); k1++; } } if (size1 < size2) { while (k1 < size1) { if ( *(t1+k1) < *(t2+k2) ) { printf("%d, ", *(t1+k1) ); k1++; } else { printf("%d, ", *(t2+k2) ); k2++; } } while (k2 < size2) { printf("%d, ", *(t2+k2) ); k2++; } } printf("\n"); return 0; }