IFT-2000: Structures de données Plan de cours Théorie du contrat Types abstraits Dominic Genest, 2009
Dominic Genest Disponibilité sur rendez-vous (quand vous voulez) Site web du cours: http://w3.ift.ulaval.ca/~dogen19/ift Remise des TP par l’Intranet Pixel Pour toute question sur la matière ou sur les travaux: Pour communiquer directement avec moi par MSN: ift- (ne m’écrivez pas de courriel à cette adresse-ci, il sera perdu)ift- Les cours en direct ont lieu là: Dominic Genest, 2009
Le plan de cours Pour cette session d’été 2009, un problème technique m’empêche de saisir les dates d’examens sur l’intranet Pixel. Elles ont tout de même été fixées: – Examen #1 (33%): Le dimanche 7 juin 2009 de 13h30 à 16h30 – Examen #2 (33%): Le dimanche 12 juillet 2009 de 13h30 à 16h30 Dominic Genest, 2009
Travaux à remettre Cinq travaux à remettre aux dates suivantes: – TP #1 (10%): 29 mai h00 – Série d’exercices #1 (2%): 1 er juin h00 – TP #2 (10%): 19 juin h00 – Série d’exercices #2 (2%): 29 juin h00 – TP #3 (10%): 6 juillet h00 Dominic Genest, 2009
Théorie du contrat Non-respect de la théorie du contrat // Nombres.h #define MAX 1000 typedef struct { float t[MAX]; int nb; } Nombres; // SuperProg.c #include ‘’Nombres.h’’ int main() { Nombres mes_nombres; mes_nombres.nb=3; mes_nombres.t[0]=33.12f; mes_nombres.t[1]=2537.6f; mes_nombres.t[1]=302.5f; return 0; } Respect de la théorie du contrat // Nombres.h typedef enum { OK,ERREUR } CodeErreur; typedef struct { float t[MAX]; int nb; } Nombres; CodeErreur initNombres(Nombres *nombres); CodeErreur ajoutNombres(Nombres *nombres, float nouveau_nombre); float rechercherNombres(const Nombres *nombres, int indice, CodeErreur *err); CodeErreur detruireNombres(Nombres *nombres); // Nombres.c CodeErreur initNombres(Nombres *nombres) { nombres->nb=0; return OK; } CodeErreur ajoutNombres(Nombres *nombres, float nouveau_nombre) { if(nombres->nb==MAX) return ERREUR; nombres->t[nombres- >nb++)=nouveau_nombre; return OK; } float rechercherNombres(const Nombres *nombres, int indice, CodeErreur *err) { float *x; if(indice>=nombres->nb) { *err=ERREUR; return 0.0f; } *err=OK; return nombres->t[indice]; } CodeErreur detruireNombres(Nombres *nombres) { return OK; } // SuperProg.c #include ‘’Nombres.h’’ int main() { Nombres mes_nombres; initNombres(&mes_nombres); ajoutNombres(&mes_nombres,33.12f); ajoutNombres(&mes_nombres,2537.6f); ajoutNombres(&mes_nombres,302.5f); return 0; } Dominic Genest, 2009
On change l’implémentation Code originel // Nombres.h typedef enum { OK,ERREUR } CodeErreur; typedef struct { float t[MAX]; int nb; } Nombres; CodeErreur initNombres(Nombres *nombres); CodeErreur ajoutNombres(Nombres *nombres, float nouveau_nombre); float rechercherNombres(const Nombres *nombres, int indice, CodeErreur *err); CodeErreur detruireNombres(Nombres *nombres); // Nombres.c CodeErreur initNombres(Nombres *nombres) { nombres->nb=0; return OK; } CodeErreur ajoutNombres(Nombres *nombres, float nouveau_nombre) { if(nombres->nb==MAX) return ERREUR; nombres->t[nombres- >nb++)=nouveau_nombre; return OK; } float rechercherNombres(const Nombres *nombres, int indice, CodeErreur *err) { if(indice>=nombres->nb) { *err=ERREUR; return 0.0f; } *err=OK; return nombres->t[indice]; } CodeErreur detruireNombres(Nombres *nombres) { return OK; } // SuperProg.c #include ‘’Nombres.h’’ int main() { Nombres mes_nombres; initNombres(&mes_nombres); ajoutNombres(&mes_nombres,33.12f ); ajoutNombres(&mes_nombres, f); ajoutNombres(&mes_nombres,302.5f ); return 0; } Code changé pour un tableau dynamique // Nombres.h typedef enum { OK,ERREUR } CodeErreur; typedef struct { float *t; int nb; } Nombres; CodeErreur initNombres(Nombres *nombres); CodeErreur ajoutNombres(Nombres *nombres, float nouveau_nombre); float rechercherNombres(const Nombres *nombres, int indice, CodeErreur *err); CodeErreur detruireNombres(Nombres *nombres); // Nombres.c CodeErreur initNombres(Nombres *nombres) { nombres->nb=0; nombres->t=0; return OK; } CodeErreur ajoutNombres(Nombres *nombres, float nouveau_nombre) { float *nouveau_t = realloc(nombres- >t,sizeof(float)*(nombres->nb+1)); if(!nouveau_t) return ERREUR; nombres->t=nouveau_t; nombres->t[nombres- >nb++)=nouveau_nombre; return OK; } float rechercherNombres(const Nombres *nombres, int indice, CodeErreur *err) { if(indice>=nombres->nb) { *err=ERREUR; return 0.0f; } *err=OK; return nombres->t[indice]; } CodeErreur detruireNombres(Nombres *nombres) { free(nombres->t); return OK; } // SuperProg.c #include ‘’Nombres.h’’ int main() { Nombres mes_nombres; initNombres(&mes_nombres); ajoutNombres(&mes_nombres,33.12f ); ajoutNombres(&mes_nombres, f); ajoutNombres(&mes_nombres,302.5f ); return 0; } Dominic Genest, 2009
Avantages de la notion de type abstrait Non-respect de la théorie du contrat On modifie sauvagement les données dans structures à tous les endroits où on a besoin des structures. On considère que tous les membres de la structure sont accessibles. Ça semble plus facile à faire pour un débutant. Un changement de conception d’une structure devient impossible dès que le logiciel prend de l’envergure. Respect de la théorie du contrat L’idée est de préparer le logiciel à un changement radical du contenu de la structure. On passe obligatoirement par des fonctions pour accéder aux membres structures. On ne fait jamais de supposition sur l’existence de tel membre. Plus difficile à réaliser pour un débutant. Ça facilite les changements de conception de structures. Dominic Genest, 2009
Théorie du contrat Cela est peu convaincant avec un si petit exemple, mais lorsque le programme devient un peu plus grand, cela a un impact majeur sur la réussite d’un projet. On doit toujours favoriser la simplicité et la durée de vie du code qui utilise les structures, et non pas du code qui implémente ces structures. Dominic Genest, 2009
Codes d’erreurs À quoi bon retourner un code d’erreur quand la fonction retourne toujours le même? CodeErreur initNombres(Nombres *nombres) { nombres->nb=0; return OK; } C’est pour préparer un éventuel changement pour une implémentation où on pourrait imaginer qu’une erreur survienne (un manque de mémoire par exemple) à l’initialisation. Dominic Genest, 2009
Fonction de destruction À quoi bon faire une fonction de destruction vide? CodeErreur detruireNombres(Nombres *nombres) { return OK; } C’est pour préparer une éventuelle implémentation où on aurait vraiment quelque chose à faire (libérer de la mémoire, sans doute) lorsque l’objet n’est plus utilisé. Dominic Genest, 2009
Théorie du contrat en C++ Le langage C++ permet de faire en sorte que le compilateur interdisse l’utilisation de certains membres de structures, par le mot-clé « private ». Aussi, les fonctions peuvent être placées à l’intérieur-même de la structure, évitant ainsi d’avoir à passer un paramètre supplémentaire à chaque fonction. Dominic Genest, 2009
En C++ // Nombres.h struct Nombres { public: // Ceci permet l’accès aux membres suivants depuis l’extérieur Nombres(); // On appelle cette fonction membre spéciale un // « constructeur » void ajout(float nouveau_nombre); float rechercher(int indice) const; ~Nombres(); // On appelle cette fonction-ci un « destructeur » private: // Ceci interdit l’accès aux membres suivants depuis l’extérieur // Seul le code à l’intérieur des fonctions membres ont le droit // d’y accéder. float *t; int nb; }; // Nombres.c Nombres::Nombres() { nb=0; t=0; } void Nombres::ajout (float nouveau_nombre) { float *nouveau_t = realloc(t,sizeof(float)*(nb+1)); if(!nouveau_t) throw -1; t=nouveau_t; t[nb++]=nouveau_nombre; } float Nombres::rechercher (int indice) const { if(indice>=nb) throw -1; return t[indice]; } CodeErreur detruireNombres(Nombres *nombres) { free(nombres->t); return OK; } // SuperProg.c #include ‘’Nombres.h’’ #include // Pour « cout » et « endl », qui remplacent « printf » // en C++ using namespace std;// Pour éviter d’avoir à toujours écrire « std::cout » // plutôt que juste « cout ». int main() { try { // Si jamais un « throw » est effectué dans l’un ou l’autre // des appels suivants, on saute immédiatement au bloc // « catch » Nombres mes_nombres; // Ceci appelle // automatiquement le constructeur Nombres::Nombres() mes_nombres.ajout(33.12f); mes_nombres.ajout (2537.6f); mes_nombres.ajout (302.5f); } // L’accolade fermante termine la portée de la variable // « mes_nombres », ce qui appelle automatiquement // son destructeur Nombres::~Nombres catch(int code_erreur) { cout << ‘’Erreur #’’ << code_erreur << ‘’!’’ << endl; return -1; } return 0; } Dominic Genest, 2009
Les types abstraits Quand on développe une structure, on doit le considérer comme un « type abstrait », c’est-à-dire dont la spécification ne dépend pas de l’implémentation. Pour établir cette spécification (qui risque moins de changer que l’implémentation), il faut avoir recours à une documentation complète de chacune des fonctions qui permettent de manipuler les objets du type en question. La meilleure façon de procéder est d’employer une convention sur la façon de documenter les fonctions. La convention de documentation la plus utilisée à l’heure actuelle est sans doute celle employée par l’utilitaire Doxygen Doxygen permet de générer automatiquement, à partir du code, une documentation dans un format pratique (le plus souvent html). Dominic Genest, 2009
Principales différences entre C++ et C en ce qui concerne le cours C malloc(sizeof(float)*25) free(x) realloc(x,sizeof(float)*25); typedef struct { // […] } MaStruct; void fonction(MaStruct *m,float x); Aucun équivalent pour « private: », il faut user de discipline. float f(float a, int x, int *err) { if(x<a) { *err=PROBLEME; return 0.0f; } // […] } int x; for(x=0;x<2;x++) C++ new float[25] delete x ou delete[] x Pas d’équivalent pour « realloc » struct MaStruct { void fonction(float x); }; Possibilité de rendre des membres privés à l’aide de « private: » float f(float a, int x) { if(x<a) throw PROBLEME; // […] } for(int x=0;x<2;x++) Dominic Genest, 2009
Les pointeurs Il s’agit en réalité tout simplement d’un nombre emmagasiné sur 4 octets (comme un « int »). Le fait qu’une variable soit déclarée du type pointeur indique au compilateur que le nombre que contiendra cette variable devra être interprété comme une adresse-mémoire. Le « type pointé », c’est-à-dire le type indiqué en avant de l’étoile lors de la déclaration, sert à donner au compilateur une indication sur le nombre d’octets à lire ou écrire en mémoire à partir de l’adresse-mémoire indiquée par le contenu du pointeur. – Par exemple, l’instruction « *x=25 » écrira 4 octets si x a été déclaré comme un « int* » tandis qu’elle écrira deux octets si x a été déclaré comme un « short* ». Ce type pointé sert aussi à donner au compilateur une indication sur le décalage d’adresse-mémoire à effectuer lors de l’utilisation de l’opérateur []. – Par exemple, l’instruction « x[3]=25 » écrira à 12 octets plus loin que « x[0]=25 » si x a été déclaré comme un « int* » tandis qu’elle écrira à seulement 6 octets plus loin si x a été déclaré comme un « short* ». Ce type pointé sert aussi à l’arithmétique effectué sur les pointeurs à l’aide des opérateurs + et -. En effet, les opérateurs + et – appliqués aux pointeurs donnent des décalages en « nombre d’éléments » et non pas en « nombre d’octets », tout comme l’opérateur []. – Par exemple, l’instruction « *(x+3)=25 » a toujours exactement le même effet que « x[3]=25 », et les décalages effectués sont multipliés par sizeof(type pointé) de la même façon qu’avec [], en respect du type pointé. Dominic Genest, 2009