IFT-2000: Structures de données Tableaux Dominic Genest, 2009
Les tableaux Tableaux Statiques Chaînes de caractères Dynamiques À plusieurs dimensions Dominic Genest, 2009
Tableaux statiques float x[100]; x[0]=4.5f; x[1]=2.3f; printf(« %f »,x[0]); printf(« %f »,x[1]); printf(« %f »,x[2]); // ? Quand on utilise un tableau statique pour emmagasiner un nombre indéterminé de données, il faut l’accompagner d’une variable de type « int » pour savoir combien de données sont vraiment considérées comme faisant partie de notre structure. La taille du tableau lui-même est une limite maximale. typedef struct { float x[100]; int n; } Nombres; CodeErreur initNombres(Nombres *n); CodeErreur ajoutNombres(Nombres *n , float nouv_nbr); // etc. En C++: struct Nombres { Nombres(); void ajout(float nouv_nbr); // etc. private: float x[100]; int n; }; Dominic Genest, 2009
Tableaux statiques Quand « n » vaut zéro, on considère que le tableau est vide. Quand on initialise le tableau, on met 0 dans n. Quand on ajoute un élément, on incrémente ce n. Quand on en supprime un, on le décrémente. Quand on fait une recherche, on ne traite comme candidats que les éléments de 0 à n-1. Quand n vaut la taille allouée entre les crochets (dans l’exemple précédent, c’était 100), alors le tableau n’a plus d’espace pour ajouter quoi que ce soit. Le principal désavantage des tableaux statiques est d’avoir à fixer une telle limite. Dominic Genest, 2009
Tableaux statiques Une autre convention, pour déterminer la fin utilisable d’un tableau statique, est de remplacer l’utilisation d’un nombre entier à côté par une valeur spéciale supplémentaire dans le tableau lui-même, placée à la fin. C’est la convention adoptée pour les chaînes de caractères. Le caractère zéro est placé à la fin de toute chaîne de caractères en guise de marqueur de fin. Ce caractère est reconnu par les fonctions « strlen », « strcpy », « strcat », et compagnie. Dominic Genest, 2009
Tableaux statiques En C En C++ typedef enum { OK, Erreur } CodeErr; typedef struct { int matricule,nb_cours,age; char nom[50]; } Etudiant; Etudiant etudiants[40000]; int n; } Universite; CodeErr initUniversite(Universite *u); CodeErr ajoutUniversite(Universite *u, Etudiant et); int trouve_age(const Universite *u, int matricule); void detruitUniversite(Universite *u); struct Etudiant { public: int matricule,nb_cours,age; char nom[50]; }; struct Universite Universite(); void ajout(Etudiant et); int trouve_age(int matricule) const; ~Universite(); private: Etudiant etudiants[40000]; int nb_etudiants; Dominic Genest, 2009
Tableaux statiques En C En C++ CodeErr initUniversite(Universite *u) { u->n=0; return OK; } CodeErr ajoutUniversite(Universite *u, Etudiant et) if(u->n==40000) return Erreur; u->etudiants[u->n++] = et; int trouve_age(const Universite *u, int matricule) int i; for(i=0;i<u->n;i++) if(u->etudiants[i].matricule==matricule) return u->etudiants[i].age; return -1; // Convention pour matricule introuvable. void detruitUniversite(Universite *u) using namespace std; Universite::Universite() { n=0; } void Universite::ajout(Etudiant et) if(n==40000) throw runtime_error(‘’L’université est pleine!’’); etudiants[n++]=et; int Universite::trouve_age(int matricule) const for(int i=0;i<n;i++) if(etudiants[i].matricule==matricule) return etudiants[i].age; return -1; // Convention pour matricule introuvable. Universite::~Universite() Dominic Genest, 2009
Tableaux dynamiques Rappel: Une variable déclarée comme tableau utilisée à elle seule sans les crochets est de type « pointeur » et désigne l’adresse-mémoire du début de ce tableau. On peut lui appliquer toutes les opérations qu’on peut appliquer à un tel pointeur. Inversement, on peut appliquer l’opérateur [] à un pointeur, de sorte qu’on considère le contenu de ce pointeur comme étant l’adresse-mémoire du début d’un tableau auquel on veut accéder. float t[20]; float *p; p=t; p[4]=2.3f; printf(« %f »,t[4]); // Ceci affiche 2.3 Dominic Genest, 2009
Tableaux dynamiques En C En C++ float *p; p = malloc(sizeof(float)*20); p[4]=2.3f; printf(‘’%f’’,p[4]); free(p); float *p; p = new float[20]; p[4]=2.3f; printf(‘’%f’’,p[4]); delete[] p; Dominic Genest, 2009
La fonction « malloc » La fonction « malloc » sert à demander au système d’exploitation un certain espace mémoire, en cours d’exécution du programme. Son avantage est que la taille de ce bloc-mémoire peut ne pas encore être déterminée à la compilation. La fonction prend un paramètre qui est un nombre d’octets (donc il faut nous-mêmes multiplier le nombre d’éléments par le nombre d’octets pour chaque élément, à l’aide de l’opérateur sizeof). La fonction retourne l’adresse-mémoire du début du bloc-mémoire alloué, laquelle doit normalement être emmagasinée dans un pointeur. Si jamais le système d’exploitation n’arrive pas à allouer l’espace demandé, la valeur zéro est retournée. Si on veut vérifier tous les cas d’erreurs, il faut donc comparer la valeur retournée avec zéro (ou certains préfèrent la constante NULL, mais c’est exactement la même chose que zéro…) et traiter ce cas spécial avant de continuer. Dominic Genest, 2009
La fonction « free » La fonction free sert à libérer un espace-mémoire précédemment alloué à l’aide de malloc. On doit lui passer en paramètre le début d’un espace-mémoire, lequel doit obligatoirement avoir été retourné par un appel à malloc. On ne peut pas libérer une partie d’un bloc-mémoire alloué. On doit toujours le libérer en entier. Si on passe la valeur zéro à free, il ne se passe rien. Cela s’avère pratique dans certains cas. Dominic Genest, 2009
La fonction « realloc » #include<math.h> int i; float *t; t = malloc(sizeof(float)*10); If(t==0) { printf(‘’Pas assez de mémoire.’’); exit(-1); } for(i=0;i<10;i++) t[i]=sin(i*2*M_PI/10); // Disons qu’on se rend compte ici qu’on a // besoin de plus d’espace… t = realloc(t,sizeof(float)*14); for(i=10;i<14;i++) t[i]=sin(i*2*M_PI/10); free(t); Dominic Genest, 2009
La fonction « realloc » La fonction realloc prend deux paramètres: une adresse-mémoire obligatoirement retournée par un appel précédent à malloc (ou à realloc), puis un nouveau nombre d’octets désiré pour le bloc (qui peut être plus petit ou plus grand). realloc retourne l’adresse-mémoire du début du bloc-mémoire, qui peut très souvent être changé de place lors de l’agrandissement (ou même lors du rapetissement). Il y a deux cas spéciaux d’appels à realloc: Un appel à realloc avec zéro comme nouveau nombre d’octets (deuxième paramètre) est équivalent à un appel à « free ». Un appel à realloc avec zéro comme adresse-mémoire (premier paramètre) est équivalent à un appel à « malloc ». Dominic Genest, 2009
Tableaux dynamiques CodeErr initUniversite(Universite *u) { u->n=0; return OK; } CodeErr ajoutUniversite(Universite *u, Etudiant et) Etudiant *e; if(u->n==0) u->etudiants = malloc(sizeof(Etudiant)); if(!u->etudiants) return Erreur; u->etudiants[0]=et; u->n=1; e = realloc(u->etudiants,sizeof(Etudiant)*(u->n+1)); if(!e) return Erreur; u->etudiants = e; u->etudiants[u->n++] = et; void detruitUniversite(Universite *u) if(u->n>0) free(u->etudiants); typedef enum { OK, Erreur } CodeErr; typedef struct { int matricule,nb_cours,age; char nom[50]; } Etudiant; Etudiant *etudiants; int n; } Universite; CodeErr initUniversite(Universite *u); CodeErr ajoutUniversite(Universite *u, Etudiant et); int trouve_age(const Universite *u, int matricule); void detruitUniversite(Universite *u); Dominic Genest, 2009
Simplification grâce aux cas particuliers de « realloc » avec zéro CodeErr initUniversite(Universite *u) { u->n=0; u->etudiants=0; return OK; } CodeErr ajoutUniversite(Universite *u, Etudiant et) Etudiant *e = realloc(u->etudiants,sizeof(Etudiant)*(u->n+1)); if(!e) return Erreur; u->etudiants = e; u->etudiants[u->n++] = et; void detruitUniversite(Universite *u) free(u->etudiants); Dominic Genest, 2009
Remarques sur « malloc », « free » et « realloc » Les appels à ces trois fonctions sont très lents et on doit s’organiser pour en faire un minimum. Une implémentation comme l’exemple précédent est très peu performante car elle implique un appel à « realloc » lors de chaque ajout. Il est préférable d’agrandir le tableau plus rarement que les ajouts; cela se fait en réservant à l’avance de la place dans le tableau. Dominic Genest, 2009
Réduction du nombre d’allocations CodeErr initUniversite(Universite *u) { u->n=0; u->etudiants=0; u->nb_alloues=0; return OK; } CodeErr ajoutUniversite(Universite *u, Etudiant et) if(u->n==u->nb_alloues) { // On double la taille du tableau Etudiant *e = realloc(u->etudiants,sizeof(Etudiant)*((u->n+1)*2)); if(!e) return Erreur; u->etudiants = e; u->nb_alloues = (u->n+1)*2; u->etudiants[u->n++] = et; void detruitUniversite(Universite *u) free(u->etudiants); typedef enum { OK, Erreur } CodeErr; typedef struct { int matricule,nb_cours,age; char nom[50]; } Etudiant; Etudiant *etudiants; int n; int nb_alloues; } Universite; CodeErr initUniversite(Universite *u); CodeErr ajoutUniversite(Universite *u, Etudiant et); int trouve_age(const Universite *u, int matricule); void detruitUniversite(Universite *u); Dominic Genest, 2009
Chaînes de caractères dynamiques Puisqu’une chaîne de caractères n’est finalement qu’un tableau de caractères, on peut en faire une version dynamique. char *x; x = malloc(sizeof(char)*(strlen(‘’Bonjour’’)+1)); if(!x) { …erreur… } strcpy(x, ’’Bonjour’’); printf(‘’%s’’,x); free(x); Dominic Genest, 2009
Copies profondes et copies superficielles Lorsqu’une structure a un membre qui est un pointeur, copier le contenu de la structure d’une variable à une autre avec l’opérateur « = » ne va que copier le contenu du pointeur en question. Ainsi, les deux variables partageront un même espace-mémoire. Cela peut être désirable dans certains cas particuliers (si on implémente un index, par exemple), mais c’est rarement le cas. Par exemple, si on fait free sur l’une des copies, alors la deuxième copie n’est plus utilisable non plus et fera planter le logiciel. typedef struct { int matricule,age,nb_cours; char *nom; } Etudiant; Etudiant a,b; a.nom = malloc(sizeof(char)*(strlen(‘’Dominic’’)+1)); strcpy(a.nom,’’Dominic’’); b = a; // Dangereux! Dominic Genest, 2009
Tableaux à plusieurs dimensions Puisqu’une chaîne de caractères est un tableau, si on veut faire un tableau de chaînes de caractères, on a alors à faire à un tableau de tableaux. On a le choix entre: Un tableau statique de chaînes de caractères statiques Un tableau dynamique de chaînes de caractères statiques Un tableau statique de chaînes de caractères dynamiques Un tableau dynamique de chaînes de caractères dynamiques Dominic Genest, 2009
Les suppressions CodeErreur supprimer_dernier(Universite *u) { // Très rapide! if(u->n==0) return Erreur; u->n--; if(u->n<=u->nb_alloues/2) { // On compacte le tableau Etudiant *e = realloc(u->etudiants,sizeof(Etudiant)*(u->n)); if(!e) return Erreur; u->etudiants = e; u->nb_alloues = u->n; } return OK; CodeErreur supprimer_premier(Universite *u) { // Beaucoup plus long! int i; if(u->n==0) return Erreur; for(i=0;i<u->n-1;i++) u->etudiants[i]=u->etudiants[i+1]; return supprimer_dernier(u); } Dominic Genest, 2009
Les suppressions Dans un tableau tel qu’on l’a implanté jusqu’à maintenant, qu’il soit dynamique ou statique, la suppression du premier élément est beaucoup plus lente que la suppression du dernier, puisqu’on doit décaler tous les éléments. On pourrait croire que pour supprimer au début, il suffirait d’incrémenter le pointeur du tableau, mais cela ne fonctionnerait pas car nous perdrions l’adresse-mémoire du début du tableau qui est nécessaire aux appels à « realloc » ou à « free ». En effet, l’adresse-mémoire du début d’un bloc-mémoire alloué dynamiquement fait office d’identifiant de ce bloc-mémoire. On peut par contre déclarer un troisième nombre entier dans la structure, celui-ci permettant de faire varier le début logique du tableau. Il faut alors ajuster toutes les fonctions en conséquence. Dominic Genest, 2009
Optimisation de la suppression du premier élément grâce à un début variable. typedef struct { Etudiant *etudiants; int n,nb_alloues,debut; } Universite; CodeErreur initUniversite(Universite *u) u->etudiants=0; u->n=0; u->nb_alloues=0; u->debut=0; return OK; } CodeErreur ajoutUniversite(Universite *u, Etudiant et) if(u->debut+u->n==u->nb_alloues) Etudiant *e = realloc(u->etudiants,sizeof(Etudiant)*(u->debut+u->n+1)*2); if(!e) return Erreur; u->etudiants = e; u->nb_alloues=(u->n+1)*2; u->etudiants[u->debut+u->n++] = et; CodeErreur compacterUniversite(Universite *u) { // Puisqu’on doit utiliser ce code dans deux fonctions, il vaut mieux en faire une fonction utilitaire if(u->debut+u->n<=u->nb_alloues/2) Etudiant *e = realloc(u->etudiants,sizeof(Etudiant)*(u->debut+u->n)); u->nb_alloues = u->debut+u->n; CodeErreur supprimer_premier(Universite *u, Etudiant et) if(u->n==0) return Erreur; u->debut++; u->n--; return compacterUniversite(u); CodeErreur supprimer_dernier(Universite *u, Etudiant et) Dominic Genest, 2009
Tableaux circulaires Si on fait varier le début, on peut se retrouver à gaspiller un espace important avant le début. On peut utiliser cet espace en s’organisant pour que toutes nos fonctions n’utilisent plus simplement l’indice « u->debut+u->n », mais plutôt « (u->debut+u->n)%u->nb_alloues » (rappel: le symbole « % » veut dire « modulo », soit « reste de la division entière »). L’implémentation de l’exemple précédent avec cela est laissée en exercice (remarque: ceci implique qu’il faut réorganiser les éléments lors de l’agrandissement ou de la compaction du tableau). Dominic Genest, 2009