La présentation est en train de télécharger. S'il vous plaît, attendez

La présentation est en train de télécharger. S'il vous plaît, attendez

IFT-2000: Structures de données Éléments techniques avancés du C et du C++ Dominic Genest, 2009.

Présentations similaires


Présentation au sujet: "IFT-2000: Structures de données Éléments techniques avancés du C et du C++ Dominic Genest, 2009."— Transcription de la présentation:

1 IFT-2000: Structures de données Éléments techniques avancés du C et du C++ Dominic Genest, 2009

2 const On sait que le mot-clé « const » sert à spécifier au compilateur qu’une variable ne devrait pas être modifiée. Par contre, la vraie définition du mot-clé « const » est plus complexe. Formellement, ce mot-clé qualifie le type qui le précède pour indiquer que les endroits désignés par ce type précédent ne fournissent qu’un accès en lecture seule, sauf dans le cas exceptionnel, même s’il est le plus courant, où le mot-clé const est placé au début d’une expression de type. Dans ce cas, le mot-clé « const » qualifie le type désigné uniquement par le mot-clé qui le suit immédiatement. Ces subtilités donnent lieu à des nuances importantes, notamment dans des déclarations de pointeurs. Dominic Genest, 2009

3 const Les deux expressions suivantes sont exactement équivalentes: int const x; const int x; Dominic Genest, 2009

4 « const T * » versus « T *const » versus « T const * » « const int * » ou « int const * » (les deux sont équivalents) int x=10; const int y = 25; const int *p = &x; const int *q = &y; *p=33; // Erreur de compilation! On ne peut pas écrire dans « *p » printf(‘’%d’’,*p); // Ok (lecture) p=q; //Ok, on peut écrire dans « p » lui-même (c’est différent d’une écriture dans « *p »!) « int * const » (très différent!) int x=10; int z=25; int * const p=&x; *p=33; // Ok! On a le droit d’écrire dans « *p » p=&z; // Erreur de compilation! On n’a pas le droit d’écrire dans « p ». Dominic Genest, 2009

5 const Un pointeur à quelque chose de « non const » ne peut pas servir à donner un accès en écriture à quelque chose qui a été déclaré « const ». Ainsi: int *p; const int x = 25; p = &x; // Erreur de compilation! Dominic Genest, 2009

6 Remarque sur les erreurs de compilation La plupart des compilateurs C ne donnent qu’un avertissement de compilation, et non pas une erreur, quand on enfreint une règle de « const ». Tous les compilateurs C++ donnent bel et bien une erreur de compilation quand on enfreint l’une de ces règles. En C++, on peut tricher à l’aide du mot-clé « const_cast ». Cela se fait de la façon suivante: const int x=22; const int *p = &x; *const_cast (p) = 44; Ceci est toutefois toujours évitable et devrait toujours être évité. Si on arrive à un point où on est obligé de faire ça dans notre programme, c’est signe qu’on a une erreur de conception et ça soulève souvent un problème de fond dans notre architecture. Dominic Genest, 2009

7 Les fonctions « const » en C++ Struct S { int x; void f() { x=5; } void h() const { printf(‘’%d\n’’,x); // Ok, x est utilisé en lecture. g(); // Ok, car g est const aussi. } void g() const { x=3; // Erreur de compilation! f(); // Erreur de compilation! } }; Dominic Genest, 2009

8 Les fonctions « const » en C++ Il s’agit obligatoirement d’une fonction membre (les fonctions globales « const », c’est absurde). Le mot-clé « const » est placé après la parenthèse fermante dans la déclaration de fonction. Ces fonctions n’ont pas le droit d’appeler des fonctions-membres de la même structure qui ne sont pas « const ». Dans ces fonctions, les variables-membres de la même structure sont accessibles seulement en lecture (elles sont donc « const » de leur point de vue). Dominic Genest, 2009

9 Pourquoi utiliser « const »? « const » fournit une garantie (sauf si on triche comme par exemple à l’aide du mot-clé « const_cast ») à l’usager de la structure que les données ne seront pas modifiées lors des appels aux fonctions « const ». « const » fournit une garantie à l’usager d’une fonction dont l’un des paramètres est un pointeur sur données « const » que ces données ne seront pas modifiées. Le gros avantage est que le non-respect de ces garanties provoque une erreur de compilation! Ainsi, cela peut éviter des erreurs importantes comme par exemple: Char x[100]; strcpy(‘’dominic’’,x); // Ceci provoque heureusement une erreur de compilation, puisque la chaîne immédiate ‘’dominic’’ est de type « const char[8] » et donc le compilateur interdira de promouvoir un « const char[8] » en « char* » (pas const). (il fallait écrire « strcpy(x,’’dominic’’); » Dominic Genest, 2009

10 Un autre exemple de l’avantage de « const » en C++ struct Point { float x,y; Point inverse(); }; Ici, on pourrait se demander si la fonction « inverse » va modifier les valeurs de x et de y puis retourner une copie du point, ou bien tout simplement retourner un autre point sans toucher à x et y. Nous ne pouvons pas répondre à cette question sans voir le code de la fonction « Point::inverse() ». Par contre, si la fonction avait été déclarée « const »: struct Point { float x,y; Point inverse() const; }; Alors nous aurions la réponse à la question sans même voir la documentation, car le fait que la fonction soit « const » nous garantie qu’elle ne modifiera pas les valeurs des variables membres. Dominic Genest, 2009

11 Pointeurs « void » D’abord, penchons-nous sur l’utilité du « type pointable » d’un pointeur. Baptisons « type pointable » le type en avant de l’étoile dans la déclaration d’un pointeur. Par exemple, si on a « int *p; », le type pointable de « p » est « int ». Les utilités du « type pointable » sont les suivantes: – Indiquer au compilateur le nombre d’octets à ajouter ou soustraire selon un nombre d’éléments dans des expressions appliquant les opérateurs ‘[]’, ‘+’ ou ‘-’ au pointeur. – Indiquer au compilateur le format et la taille des données à lire ou écrire lorsqu’on applique l’opérateur ‘*’ au pointeur. Les pointeurs déclarés « void * » sont des pointeurs pour lesquels le type pointable n’est pas spécifié. Dans ce cas, toute utilisation directe de l’opérateur [], +, -, ou * est interdite sur de tels pointeurs, et provoquera une erreur de compilation. Ces pointeurs ne servent finalement qu’à emmagasiner une adresse- mémoire, sans donner d’indication sur la façon d’interpréter les données qui se trouvent à l’adresse-mémoire qu’ils contiennent. Dominic Genest, 2009

12 Pointeurs « void » Les pointeurs void servent principalement dans les deux cas suivants: – Programmation de structures génériques en langage C, sans utiliser les éléments du C++ prévus à cet effet (sans utiliser les « template »). – Gestion de mémoire (par exemple, les fonctions « malloc », « free » et « realloc » manipulent des pointeurs « void »). Dominic Genest, 2009

13 Pointeurs à des fonctions De la même façon qu’un pointeur ordinaire peut contenir l’adresse-mémoire d’une variable, un pointeur à une fonction peut contenir l’adresse-mémoire d’une fonction. Pour déclarer un pointeur à une fonction, il faut suivre la recette suivante: 1.Écrire une déclaration de fonction (avec le type de la valeur de retour et les arguments) 2.Remplacer le nom de la fonction par le nom qu’on veut donner à notre pointeur 3.Ajouter des parenthèses autour de ce nom seul 4.Ajouter une étoile juste en avant du nom, à l’intérieur des parenthèses qu’on vient d’ajouter. 5.Supprimer le nom des arguments (mais pas leur type). Ceci est optionnel, on peut les laisser là si on veut, et cela n’impose rien de plus sur les noms des arguments des fonctions dont on veut y placer l’adresse. float ma_fonction(int a, double z) { return a+z; } int main() { float f; float (*mon_pointeur)(int, double); mon_pointeur = &ma_fonction; f = (*mon_pointeur)(55,3.7); printf(‘’%f’’,f); // Ceci affiche 58.7 return 0; } Dominic Genest, 2009

14 Pointeurs à des fonctions Pour récupérer les adresses de fonctions, les compilateurs C et les anciennes versions des compilateurs C++ permettaient d’omettre l’esperluette (le ‘&’), puisqu’ils devinaient que lorsqu’on indiquait le nom d’une fonction sans le suivre par des parenthèses, on voulait parler de son adresse. De la même façon, ces compilateurs permettaient de ne pas mettre l’étoile pour un appel de fonction via un pointeur à une fonction. Par contre, les nouvelles versions du C++ interdisent ces raccourcis, pour des raisons qui sortent du cadre de ce cours (il est question d’une ambiguïté à laquelle ces raccourcis pourraient mener dans certains cas d’utilisation avancée de génériques avec surchargement d’opérateurs, puis de déduction de types). Il vaut mieux donc éviter ces raccourcis et toujours employer l’esperluette et l’étoile. Dominic Genest, 2009

15 Les pointeurs à des fonctions Les pointeurs à des fonctions servent principalement aux systèmes de retours d’appels (« call-backs »), afin de permettre à un logiciel de réagir à un événement en fournissant l’adresse-mémoire d’une fonction qu’on doit appeler quand un événement survient (par exemple, un mouvement de souris, une touche enfoncée au clavier, un certain temps en secondes s’est écoulé). Un bon exemple d’une telle utilisation est dans la librairie SDL: http://libsdl.org Les pointeurs aux fonctions peuvent toujours être remplacés par des fonctionnalités du C++ comme les « fonctions virtuelles » ou les génériques (« templates »). Mais ils tendent à revenir d’usage dans les librairies les plus sophistiquées et les mieux conçues. Par exemple, les librairies « libsigc++ » « boost » (son module « boost::signal ») en font usage. Dominic Genest, 2009

16 L’opérateur conditionnel ternaire Un bout de code comme le suivant peut être simplifié grandement grâce à l’opérateur conditionnel ternaire ( ? : ): if(age>=18) printf(‘’Vous êtes majeur car vous avez %d ans.’’,age); else printf(‘’Vous êtes mineur car vous avez %d ans. »,age); On peut en effet le remplacer par: printf(age>=18 ? ‘’Vous êtes majeur car vous avez %d ans.’’ : ‘’Vous êtes mineur car vous avez %d ans.’’,age); Dominic Genest, 2009

17 L’opérateur conditionnel ternaire float f(float x) { if(f<10.0f) return f/2; if(f<20.0f) return f/3; return f/4; } float f(float x) { return f<10.0f ? f/2 : f<20.0f ? f/3 : f/4; } Dominic Genest, 2009

18 Organisation en mémoire des tableaux à plusieurs dimensions En mémoire, un tableau à plusieurs dimensions est « aplati » (forcément). En effet, un tableau à deux dimensions de N par M peut être vu comme un tableau à une dimension de N éléments dont chacun des éléments est lui-même un tableau à une dimension de M éléments. On peut avoir du mal à se rappeler si cette linéarisation se fait de gauche à droite ou de droite à gauche dans l’ordre de déclaration des dimensions. Par contre, en examinant le typage, une seule avenue est possible. Soit la déclaration « int x[2][3][4]; »: – Le type de l’expression « x » est « int[2][3][4] » – Le type de l’expression « x[0] » est « int[3][4] » – Le type de l’expression « x[0][0] » est « int[4] » – Le type de l’expression « x[0][0][0] » est « int ». x[0] x[1] x[0][0]x[0][1]x[0][2]x[1][0]x[1][1] x[1][2] Dominic Genest, 2009

19 Les tableaux dynamiques à plusieurs dimensions Comme pour les tableaux de chaînes de caractères, vus précédemment, l’utilisation de n’importe quel tableau à plusieurs dimensions implique un choix « dynamique » ou « statique » pour chaque dimension. Par exemple, un tableau de « int » à trois dimensions peut être déclaré de 8 façons différentes: 1.int x[2][3][4]; // 2 tableaux de 3 tableaux de 4 ints. 2.int *x[2][3]; //2 tableaux de 3 tableaux d’un nombre variable de ints. 3.int **x[2]; // 2 tableaux de nombres variables de tableaux à taille variable de ints. 4.int ***x; // Nombre variable de tableaux à tailles variables de tableaux à tailles variables de ints. 5.int (*x)[3][4]; // Nombre variable de tableaux de 3 par 4 ints. 6.int (**x)[4]; // Nombre variable de tableaux à tailles variables de 4 ints. 7.int (*x[2])[4]; // 2 tableaux d’un nombre variable de tableaux de 4 ints. 8.int *(*x)[3]; // Un nombre variables de tableaux de 3 tableaux de nombres variables de int. De telles déclarations, tout spécialement les cas #7 et #8 ci-haut, peuvent être grandement simplifiées à l’aide de déclarations « typedef ». Dominic Genest, 2009

20 Les trois sens du mot-clé « static » Attention! Le mot-clé « static » n’a RIEN À VOIR avec le terme « tableau statique » tel qu’employé au cours. Quand on dit « tableau statique », il s’agit, par opposition à « tableau dynamique », d’un tableau dont le cardinal est fixé à la compilation. Le mot-clé « static » n’a ABSOLUMENT RINE À VOIR avec ça! Le mot-clé « static » a trois sens complètement différents selon le contexte où il est utilisé: 1.À l’intérieur d’une fonction: Il indique que la variable ne doit être initialisée que la première fois que ce bloc de code est exécuté, et qu’elle préserve sa valeur pour le prochain appel. 2.Appliqué sur une variable ou une fonction globale: Il indique que la fonction ou la variable en question n’est disponible que pour le fichier «.c » (ou «.cpp ») en cours, et qu’elle ne sera pas rendue disponible lors de l’édition des liens (« link ») pour les autres fichiers «.c » compilés (attention, la notion de « fichier » est complexe ici: il faut tenir compte du fait que du point de vue du compilateur, un fichier «.h » inclus par « #define » est vu comme faisant partie du «.c » ou «.cpp » qui est en train de se faire compiler!) 3.À l’intérieur d’une structure (C++ seulement): S’il est employé pour qualifier une variable membre: – La variable en question sera partagée par toutes les instances de la structure. Il faut alors déclarer la variable aussi à l’extérieur de la structure, à l’aide de l’opérateur « :: », dans un fichier «.cpp ». S’il est employé pour qualifier une fonction membre: – La fonction en question pourra être appelée sans qu’on ait déclaré une instance de la structure. Cette fonction n’aura pas accès aux membres de la structure qui ne sont pas statiques eux-mêmes. Dominic Genest, 2009

21 « static » employé à l’intérieur d’une fonction void f() { int variable_normale = 0; static int compte = 0; // Ce zéro ne sera // utilisé qu’au tout premier appel de « f ». compte++; printf(‘’La fonction f a été appelée %d fois.\n’’,compte); variable_normale++; printf(‘’%d\n’’,variable_normale); // Ceci affiche // toujours 1 } Dominic Genest, 2009

22 « static » appliqué sur une variable ou une fonction globale //Allo.h void f(); static void g() { printf(‘’g’’); } void h() { printf(‘’h’’); } static void k(); extern int x; int y; //Allo.c #include ‘’Allo.h’’ int x; static int z = 3; void f() { printf(‘’f’’); k(); } static void k() { printf(‘’k’’); } //Salut.c #include ‘’Allo.h’’ static int z = 5; // Il s’agit ici d’un autre z. int main() { f(); g(); h(); x=44; z = 5; k(); // Erreur au « link »! « k » n’est disponible // que pour « Allo.c » return 0; } // Deux autres erreurs au « link » surviendront parce que // le « int y » et la fonction « h » se retrouvent en double // puisqu’ils sont déclarés directement dans « Allo.h » et // inclus par deux fichiers «.c ». Il aurait fallu déclarer y // « extern » dans « Allo.h », puis le déclarer pour vrai dans // un seul fichier «.c », comme avec la variable « x ». Allo.obj: x f h g (caché) z (caché) k (caché) Salut.obj main h y g (caché) z (caché) Dominic Genest, 2009

23 « static » sur une fonction membre ou variable membre (c++ seulement) struct S { int x; static int y; void f(); static void g() { x = 2; // Interdit! « g » n’a accès // qu’aux membres eux-mêmes // « static ». y = 5; // Ok. } }; int main() { S a,b; a.x = 5; b.x = 7; S::x = 5; // Interdit! Il faut savoir de // quel « S » on parle. a.y = 8; b.y = 9; // Il s’agit du même y! S::y = 10; // Il vaut mieux utiliser y de cette façon-ci. a.f(); b.f(); S::f(); // Interdit! Il faut savoir de // quel « S » on parle. S::g(); return 0; } Dominic Genest, 2009

24 Itérations Souvent, un algorithme peut avoir besoin d’un tableau en entrée. Dans ce cas, on peut avoir le réflexe de déclarer la fonction ainsi: – void afficher(float *x, int n) { int i; for(i=0;i<n;i++) printf(‘’%f\n’’,x[i]); } int main() { float x[5] = { 4.3f, 2.2f, 6.4f, 10.5f, -3.5f }; afficher(x,5); return 0; } Les algorithmes de la STL du C++ suivent une autre convention. Plutôt que de prendre l’adresse-mémoire du début d’un tableau puis un entier pour la taille, ceux-ci prennent l’adresse-mémoire du début du tableau, puis l’adresse-mémoire qui correspond à un élément plus loin que le dernier élément du tableau: – void afficher(float *x, float *fin) { for(;x!=fin;x++) printf(‘’%f\n’’,*x); } int main() { float x[5] = { 4.3f, 2.2f, 6.4f, 10.5f, -3.5f }; afficher(x,x+5); // Oui oui, même si x+5 dépasse le tableau, c’est la convention utilisée! En effet, l’algorithme ne // déréférencera jamais cette adresse-mémoire, donc il n’y a aucun danger. return 0; } Dominic Genest, 2009

25 Itérations Ceci comporte les avantages suivants: – Ça simplifie le code à l’intérieur de la fonction. – Ça optimise le code à l’intérieur de la fonction (le « *x » ci- haut, dans certains contextes, est plus rapide que « x[i] », car il y a une addition de moins à faire). – Ça généralise automatiquement l’algorithme pour des cas où on voudrait ne traiter qu’une partie du tableau. – Ça permet, en C++ à l’aide des génériques (« templates ») de traiter par exactement les mêmes algorithmes aussi bien les tableaux en C que tous les conteneurs de la librairie STL, même les plus évolués (arbres binaires, tables de hachages, etc.). – Et tous ces gains en généralité se font sans la moindre perte de performance. – Il s’agit sans doute de la convention la plus moderne. Dominic Genest, 2009


Télécharger ppt "IFT-2000: Structures de données Éléments techniques avancés du C et du C++ Dominic Genest, 2009."

Présentations similaires


Annonces Google