Langage C:Tableaux
Un article de WikiTuto.
Sommaire |
Tableaux unidimensionnels
Un tableau est un regroupement, dans une même variable, de plusieurs variables simples, toutes de même type.
déclaration
[classe] type nom [nombre_d'éléments];
exemple
int tab[10];
Ceci réserve en mémoire un espace contigu pouvant contenir 10 entiers. Le premier est tab[0], jusqu'à tab[9]. Attention, en utilisant tab[10] ou plus, aucune erreur ne sera signalée et vous utiliserez une partie de mémoire qui a certainement été réservée pour autre chose. Il est possible de définir un tableau de n'importe quel type de composantes (scalaires, pointeurs, structures et même tableaux). Il est également possible de définir un type tableau par typedef : typedef float vecteur[3]; vecteur x,y,z;
On peut aussi initialiser un tableau. Dans ce cas la dimension n'est pas nécessaire. Mais si elle est donnée, et est supérieure au nombre de valeurs données, les suivantes seront initialisées à 0 : vecteur vect0={0,0,0}; int chiffres[]={0,1,2,3,4,5,6,7,8,9}; int tableau[20]={1,2,3}; /* les 17 autres à 0 */
On peut également déclarer un tableau sans en donner sa dimension. Dans ce cas là le compilateur ne lui réserve pas de place, elle aura du être réservée autre part (par exemple tableau externe ou argument formel d'une fonction).
Exercice (moyenne)
Ecrire le programme qui lit une liste de Nb nombres, calcule et affiche la moyenne puis l'écart entre chaque note et cette moyenne. Cliquez ici pour une solution.
Tableaux et pointeurs
arithmétique des pointeurs
En déclarant, par exemple, int TAB[10]; l'identificateur TAB correspond en fait à l'adresse du début du tableau. Les deux écritures TAB et &TAB[0] sont équivalentes (ainsi que TAB[0] et *TAB). On définit l'opération d'incrémentation pour les pointeurs par TAB+1=adresse de l'élément suivant du tableau. L'arithmétique des pointeurs en C a cette particularité que l'opération dépend du type de variable pointée, ajouter 1 consistant à ajouter à l'adresse la taille de l'objet pointé. On définit donc l'addition (pointeur+entier): TAB+i=&TAB[i], la soustraction (pointeur - entier), mais également la soustraction (pointeur - pointeur) qui donne un nombre d'éléments. Les opérations de comparaisons entre pointeurs sont donc également possibles.
- Déclarons : int TAB[10],i,*ptr;
Ceci réserve en mémoire - la place pour 10 entiers, l'adresse du début de cette zone est TAB, - la place pour l'entier i, - la place pour un pointeur d'entier (le type pointé est important pour définir l'addition).
- Analysons les instructions suivantes :
ptr=TAB; /*met l'adresse du début du tableau dans ptr*/
for(i=0;i<10;i++)
{
printf("entrez la %dième valeur :\n",i+1);
/* +1 pour commencer à 1*/
scanf("%d",ptr+i);
/* ou &TAB[i] puisque scanf veut une adresse*/
}
puts("affichage du tableau");
for(ptr=TAB;ptr<TAB+10 /* ou &TAB[10] */;ptr++)
printf("%d ",*ptr);
puts(" ");
/* attention actuellement on pointe derrière le tableau ! */
ptr-=10; /* ou plutôt ptr=TAB qui lui n'a pas changé */
printf("%d",*ptr+1); /* affiche (TAB[0])+1 */
printf("%d",*(ptr+1)); /* affiche TAB[1] */
printf("%d",*ptr++); /* affiche TAB[0] puis pointe sur TAB[1] */
printf("%d",(*ptr)++); /* affiche TAB[1] puis ajoute 1 à TAB[1]*/
TAB est une "constante pointeur", alors que ptr est une variable (donc TAB++ est impossible). La déclaration d'un tableau réserve la place qui lui est nécessaire, celle d'un pointeur uniquement la place d'une adresse.
Pour passer un tableau en argument d'une fonction, on ne peut que le passer par adresse (recopier le tableau prendrait de la place et du temps).
exemple utilisant ces deux écritures équivalentes :
#include <stdio.h>
void annule_tableau(int *t,int max)
{
for(;max>0;max--)*(t++)=0;
}
void affiche_tableau(int t[], int max)
{
int i;
for(i=0;i<max;i++) printf("%d : %d\n",i,t[i]);
}
int main(void)
{
int tableau[10];
annule_tableau(tableau,10);
affiche_tableau(tableau,10);
}
Exercice (rotation)
Ecrire un programme qui lit une liste de Nb nombres, la décale d'un cran vers le haut (le premier doit se retrouver en dernier), l'affiche puis la décale vers le bas. On pourra décomposer le programme en fonctions. Cliquez ici pour une solution.
Exercice (classer)
Classer automatiquement un tableau de Nb entiers puis l'afficher dans l'ordre croissant puis décroissant. On pourra utiliser des fonctions de l'exercice précédent. On pourra créer un (ou plusieurs) tableau temporaire (donc local). Si vous vous en sentez la force, prévoyez le cas de valeurs égales. Cliquez ici pour une solution.
Chaînes de caractères
En C, comme dans les autres langages, certaines fonctionnalités ont été ajoutées aux tableaux dans le cas des tableaux de caractères. En C, on représente les chaînes par un tableau de caractères, dont le dernier est un caractère de code nul (\0). Une constante caractères est identifiée par ses délimiteurs, les guillemets " (double quote).
exemples
puts("salut");
char mess[]="bonjour"; /* évite de mettre ={'b','o',..,'r','\0'} */
puts (mess);
mess est un tableau de 8 caractères (\0 compris). On peut au cours du programme modifier le contenu de mess, à condition de ne pas dépasser 8 caractères (mais on peut en mettre moins, le \0 indiquant la fin de la chaîne). Mais on peut également initialiser un pointeur avec une chaîne de caractères :
char *strptr="bonjour";
Le compilateur crée la chaîne en mémoire de code (constante) et une variable strptr contenant l'adresse de la chaîne. Le programme pourra donc changer le contenu de strptr (et donc pointer sur une autre chaîne), mais pas changer le contenu de la chaîne initialement créée.
Exercice (chaînes)
écrire un programme qui détermine le nombre et la position d'une sous-chaîne dans une chaîne (exemple ON dans FONCTION : en position 1 et 6). Cliquez ici pour une solution.
Bibliothèques de fonctions pour tableaux et chaînes
Toutes les fonctions standard d'entrée / sortie de chaînes considèrent la chaîne terminée par un \0, c'est pourquoi en entrée elles rajoutent automatiquement le \0 (gets, scanf), en sortie elles affichent jusqu'au \0 (puts, printf). La bibliothèque de chaînes (inclure string.h) possède des fonctions utiles à la manipulation de chaînes
- int strlen(chaîne) donne la longueur de la chaîne (\0 non compris)
- char *strcpy(char *destination,char *source) recopie la source dans la destination, rend un pointeur sur la destination (dest=source quand à lui ne recopie pas la chaîne mais uniquement son adresse !). Faites attention de ne pas dépasser la dimension de la destination, sinon utilisez le suivant.
- char *strncpy(char *destination,char *source,int longmax) idem strcpy mais s'arrête au \0 ou longmax (qui doit comprendre le \0)
- char *strcat(char *destination,char *source) recopie la source à la suite de la destination, rend un pointeur sur la destination
- char *strncat(char *destination,char *source,int longmax) idem, mais plus sûr
- int strcmp(char *str1,char*str2) rend 0 si str1= =str2, <0 si str1<str2, >0 si str1>str2 (classé alphabétiquement, donc test du premier caractère, si différents test du second, etc...) Les majuscules sont différentes des minuscules (inférieures). Idem pour strncmp, qui lui vérifie la dimension.
Des fonctions similaires, mais pour tous tableaux (sans s'arrêter au \0) sont déclarées dans mem.h. La longueur est à donner en octets (on peut utiliser sizeof) :
- int memcmp(void *s1,void *s2,int longueur);
- void *memcpy(void *dest,void *src,int longueur);
fonctions utiles à la conversion entre scalaires et chaînes
On possède également des fonctions de conversions entre scalaires et chaînes, déclarées dans stdlib.h
- int atoi(char *s) traduit la chaîne en entier (s'arrête au premier caractère impossible, 0 si erreur dès le premier caractère)
- de même atol et atof
J'utilise également sscanf et sprintf pour les conversions, qui offrent un maximum de possibilités.
Allocation dynamique de mémoire
La taille déclarée d'un tableau est définie à la compilation (alors que quand elle n'est pas constante il serait utile de la définir lors de l'exécution du programme). Dans le cas d'une taille inconnue à l'avance, il faut surdimensionner le tableau (et donc réserver des mémoires dont on ne servira que rarement, aux dépends d'autres variables ou tableaux). C'est pour cela qu'on les appelle des tableaux statiques.
En C, le lien entre les tableaux et pointeurs permet la notion de tableaux dynamiques : on peut définir la dimension d'un tableau lors de l'exécution. Il faut d'abord réserver une zone de mémoire contiguë, de la taille désirée (mais pas plus). Il faut avoir déclaré une variable pointeur qui contiendra l'adresse du début du tableau. A l'exécution, lorsque l'on connaît la taille désiré, on peut réserver une zone mémoire (dans la zone appelée "tas" ou "heap") par les fonctions :
- void *malloc(int taille) : réserve une zone mémoire contiguë de taille octets, et retourne un pointeur sur le début du bloc réservé. Retourne le pointeur NULL en cas d'erreur (en général car pas assez de mémoire).
- void *calloc(int nb, int taille) : équivalent à malloc(nb*taille).
exemple
float *tab;
int nb;
puts("taille désirée ?");
scanf("%d",&nb);
tab=(float*)calloc(nb,sizeof(float));
malloc et calloc nécessitent un cast pour que le compilateur ne signale pas d'erreur.
- void free(void *pointeur) libère la place réservée auparavant par malloc ou calloc. Pointeur est l'adresse retournée lors de l'allocation. En quittant proprement le programme, la mémoire allouée est automatiquement restituée même si on omet d'appeler free.
- void *realloc(void *pointeur,int taille) essaie, si possible, de réajuster la taille d'un bloc de mémoire déjà alloué (augmentation ou diminution de taille). Si nécessaire, le bloc est déplacé et son contenu recopié. En retour, l'adresse du bloc modifié (pas nécessairement la même qu'avant) ou le pointeur NULL en cas d'erreur.
Ces fonctions sont définies dans stdlib.h ou alloc.h (suivant votre compilateur).
Une fois la zone de mémoire réservée, et son adresse de début connue (supposons l'avoir stockée dans le pointeur nommé "tab" comme décrit dans l'exemple ci-dessus), on peut y accéder soit par "l'écriture pointeurs" soit par "l'écriture tableaux", puisqu'elles sont équivalentes :
- premier élément : *tab ou tab[0]
- iiéme élément : *(tab+i) ou tab[i]
- adresse du iiéme élément (pour scanf par exemple) : tab+i ou &(tab[i])
On peut remarquer que ces tableaux ne sont pas totalement dynamiques : la dimension doit être connue (et l'allocation faite) avant la première utilisation du tableau. Elle est ensuite difficilement modifiable (du moins pour l'augmenter). On ne peut par exemple pas dire "entrez vos mots, terminez par une ligne vide", mais "combien de mots ?" puis "entrez vos mots".
Une erreur fréquente consiste à "perdre" l'adresse du début de la zone allouée (par tab=quelque chose, ou parce que tab était une variable locale qui ne vit plus) et donc il devient alors impossible d'accéder au début de la zone, ni de la libérer.
Tableaux multidimensionnels
On peut déclarer par exemple int tab[2][3] : matrice de 2 lignes de 3 éléments. Un tableau peut être initialisé : int t[2][3]={{1,2,3},{4,5,6}} mais cette écriture est équivalente à {1,2,3,4,5,6}, car il place dans l'ordre t[0][0], t[0][1], t[0][2], t[1][0], t[1][1], t[1][2], c'est à dire ligne après ligne. Dans un tableau multidimensionnel initialisé, la dimension la plus à gauche peut être omise (ici int t[][3]={...} était à la rigueur possible).
t correspond à l'adresse &t[0][0], mais t[1] est aussi un tableau (une ligne), donc désigne l'adresse &t[1][0]. En fait, une matrice est un tableau de lignes. On peut expliciter cela par typedef (c'est d'après moi la meilleure solution pour déclarer des tableaux multidimensionnels) :
typedef int ligne[3]; typedef ligne matrice[2]; matrice t; /* 3 lignes au lieu d'une mais tellement plus clair */
En utilisant pointeurs et allocation dynamique, pour gérer un tableau de NBLIG lignes de NBCOL éléments, on peut :
- soit créer une matrice complète : allocation par t=malloc(NBLIG* NBCOL* sizeof(élément)), accès à l'élément l,c par *(t+l*NBCOL+c).
- soit créer un tableau de NBLIG pointeurs de lignes, puis chaque ligne séparément. Ceci permet une optimisation si les lignes n'ont pas toutes la même longueur (traitement de textes par exemple) mais aussi de manipuler facilement les lignes (exemple : échanger deux lignes sans recopier leur contenu). Cette méthode est plus rapide que la précédente, car les adresses de chaque début de ligne sont immédiatement connues, sans calcul.
- soit utiliser des pointeurs de pointeurs (même principe que le cas précédent, mais remplacement du tableau de pointeurs (dimension prévue à l'avance) par une allocation dynamique.
Exercice (matrices)
faire le calcul de multiplication d'une matrice (M lignes, L colonnes) par une matrice (L,N) : résultat (M,N). Cliquez ici pour une solution.
Exercice (déterminant)
écrire un programme qui calcule le déterminant d'une matrice carrée (N,N), sachant qu'il vaut la somme (sur chaque ligne) de l'élément de la ligne en 1ère colonne par le déterminant de la sous-matrice obtenue en enlevant la ligne et la 1ère colonne (en changeant le signe à chaque fois). Le déterminant d'une matrice (1,1) est sont seul élément. On utilisera bien évidement la récursivité. Il existe (heureusement) d'autres méthodes plus rapides.Cliquez ici pour une solution.
Voir aussi
- Instructions
- Structures de contrôle
- Déclaration et stockage des variables
- Fonctions
- Variables scalaires
- Tableaux
- Structures et unions
Auteur :Patrick TRAU. Copyright : utilisation de ces documents libre pour tout usage personnel. Utilisation autorisée pour tout usage public non commercial, à condition de citer son auteur (Patrick TRAU, IPST, Université Louis Pasteur Strasbourg, email : Patrick.Trau (à) ipst-ulp.u-strasbg.fr ) et de me signaler tout usage intensif. Utilisation commerciale interdite sans accord écrit de ma part.



