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

Structures de données IFT-2000 Abder Alikacem Pointeurs, références et gestion dynamique de la mémoire. QQ éléments techniques du C++. Semaine 2 Département.

Présentations similaires


Présentation au sujet: "Structures de données IFT-2000 Abder Alikacem Pointeurs, références et gestion dynamique de la mémoire. QQ éléments techniques du C++. Semaine 2 Département."— Transcription de la présentation:

1 Structures de données IFT-2000 Abder Alikacem Pointeurs, références et gestion dynamique de la mémoire. QQ éléments techniques du C++. Semaine 2 Département d’informatique et de génie logiciel Édition Septembre 2009

2 Plan  Rappels  Définition du type pointeur  Arithmétique des pointeurs  Application des pointeurs  Les références  Gestion dynamique de la mémoire  Tableaux dynamiques, tableaux à une et plusieurs dimensions  Pointeurs sur des objets et chaînage de la mémoire  Listes simplement chaînées  Quelques éléments techniques du C++  Le mot clé const  Le mot clé static  Les itérations

3 Les pointeurs  Définition Un pointeur est une variable contenant l’adresse d’une donnée (dans le vocabulaire des langages C/C++, le procédé qui consiste à atteindre une donnée via un pointeur sur cette donnée s’appelle une indirection).  Toujours une valeur absolue  Adresses admissibles = entiers de longueur fixe (uniformité => référence uniforme et homogène)  Le type pointeur est considéré comme étant un type de base.  Déclaration char *ptC; // Un pointeur sur un caractère float *ptT; // Un pointeur sur un réel int *ptP; // Un pointeur sur un entier

4  L'opérateur & &variable : donne l'adresse d'une variable  partout dans un programme  si une adresse est mise dans une autre variable, cette autre variable doit être un pointeur. Opérations d’adressage x :contenu de la case mémoire associée à x &x : adresse de x (adresse de la case mémoire associée à x) ptr = &x : ptr sera un pointeur sur la case mémoire associée à x

5 Opérations d’adressage  L’opérateur * désigne le contenu de la variable dont l’adresse est l’opérande.  Notez que dans une déclaration, l’opérateur * n’a pas le même sens. Par exemple, la déclaration int* ptr; signifie que n est un pointeur sur un entier.  *ptr : réfère donc à la variable pointée (opérateur d'indirection), c’est le contenu de la variable dont l’adresse est ptr. On parle de déréférenciation.  Si ptr est un pointeur, ptr et *ptr sont des lvalue: ils sont modifiables.  Il n'en va pas de même de &ptr : (&ptr)++; elle sera rejetée à la compilation.  int* ptr; réserve un emplacement en mémoire pour un pointeur sur un entier… elle ne réserve pas en plus un emplacement pour un entier!

6 Exemple 1 int i1=1,i2=2; int *p1,*p2; p1=&i1; p2=p1; cout << *p1 << endl;/*affiche ?*/ p2=&i2; *p2=*p1; cout << i2 << endl; /*affiche ?*/ Les pointeurs en C/C++

7 Exemple 2 int main() {int i; int * j; i = 1; j = &i; *j = 2; cout << "i vaut : " << i << endl; /*affiche ?*/ return 0; } Les pointeurs en C/C++

8 Exemple 3 int* ad1, * ad2, * ad; int n = 10, p = 20; ad1 = &n; ad2 = &p; *ad1 = *ad2 + 2; /*identique à n=p+2; */ (*ad1)++; /* identique à n++;*/ ad++; ad += 10; ad -= 25; n= ad1-ad2; Quelques éléments de syntaxe

9 Représentation graphique affectations int *ptP; int p; ptP = &p; *ptP = 3; ptPp

10 Représentation graphique Affectations int *ptP; int p; ptP = &p; *ptP = 3; ptPp

11 Représentation graphique affectations int *ptP; int p; ptP = &p; *ptP = 3; ptPp 3

12 Représentation graphique affectations: int **ptPtP; int * ptP; int p; ptP = &p; *ptP = 3; ptPtP = &ptP; ptPpptPtP 3

13 Représentation graphique ptPpptPtP 3 affectations: int **ptPtP; int * ptP; int p; ptP = &p; *ptP = 3; ptPtP = &ptP;

14 Représentation graphique ptPpptPtP 34 x affectations: int **ptPtP; int * ptP; int p; ptP = &p; *ptP = 3; ptPtP = &ptP; **ptPtP = 4;

15 Pointeur NULL affectations: #include pour la définition de NULL mais il est préférable en C++ d’utiliser 0 à la place. *ptPtP = 0; /* utile pour tester */ ptPpptPtP 4 X

16 Représentation graphique affectations: *ptPtP = 0; /* utile pour tester */ if (*ptPtP == 0) … (p est toujours accessible par son nom!!) ptPpptPtP 4

17 Pointeur NULL: affectation affectations: *ptPtP = 0; ptPtP = 0; (et non l’inverse!!!) ptPpptPtP 4 X

18 Représentation graphique affectations: *ptPtP = 0; ptPtP = 0; if (ptPtP == 0)... ptPpptPtP 4

19 Affectations de pointeurs On peut affecter un pointeur à une variable pointeur de même type. Sauf: si on affecte l ’entier 0 (pour NULL) pt1 = 0; ou un pointeur de type générique void *

20 Pointeurs génériques  Il arrive parfois que les circonstances empêchent le type du pointeur de refléter le type de l’objet pointé, soit parce que ce type est inconnu et sans importance, soit parce que ce type est variable et doit donc être indiqué par autre chose que le type du pointeur (qui ne saurait, évidemment, changer d’un instant à l’autre…).  Le type void est un candidat naturel pour signifier que la nature de l’objet résidant à l’adresse contenue dans un pointeur est non spécifiée. On crée donc un pointeur générique en écrivant:void * ptr;  Dans ce cas, toute utilisation directe de l’opérateur [], +, -, ou * est interdite sur de tels pointeurs, et provoquera une erreur de compilation.

21 Pointeurs génériques Un pointeur générique accepte de prendre pour valeur l’adresse de n’importe quel objet, quel qu’en soit le type. Exemple. void * ptr = 0; //un pointeur générique int entier = 4; ptr = &entier; //un pointeur générique peut désigner un int.. char lettre = ‘c’; ptr = &lettre; //..ou un char… Cependant, on ne peut déférencer un pointeur générique sans recourir au transtypage (casting). En effet, déférencer un pointeur générique reviendrait à accéder à une zone mémoire en ignorant tout, opération impossible car le compilateur est incapable de déterminer quelles instructions doivent être générées pour respecter les conventions de représentation en vigueur dans la zone de mémoire concernée (zone mémoire dont il ne connaît même pas la taille)!).

22 Pointeurs génériques Transtypage d’adresses en C++ Le transtypage d’une adresse peut être effectuée à l’aide de l’opérateur static_cast ( ) qui exige deux opérandes: le type souhaité et la valeur à convertir. Comme tous les autres opérateurs de transtypage, ils sont dénués d’effet. Exemple. double uneVariable = -3.92; void* ptr = &uneVariable; int i = 18; double * ptrDouble; *static_cast (ptr) = 2.4; //uneVariable prend la valeur 2.4 ptrDouble = static_cast (ptr); //ptrDouble pointe uneVariable cout (ptr) << endl; ptr = &i; // ptr pointe maintenant sur la variable entière i

23 Pointeurs génériques Opérateurs de transtypage Le C++ en connaît 3 autres:  reinterprete_cast qui est nécessaire lorsqu’une conversion est effectuée entre des types de natures différentes, comme lorsqu’on stocke une adresse dans un int par exemple. int i; char *cptr; cptr = reinterpret_cast (i);  const_cast qui permet de s’affranchir du caractère constant d’une valeur. const int x=22; const int *p = &x; *const_cast (p) = 44;  dynamic_cast qui est indispensable lorsque le type concernée ne sera connu que lors de l’exécution du programme (son utilisation dépasse un peu le cadre du cours).

24 Pointeurs sur structures ou classes Exemple struct Pixel { unsigned char r,g,b; }; struct Image { Pixel *bitmap; int largeur; int longueur; }; Image temp, *ptr; (...) ptr=&temp; cin >> ptr->Largeur >> (*ptr).Longueur >>...; //Ptr->Largeur est la même chose que (*Ptr).Largeur int largeur int longueur char g char b char r Image temp Pixel ptr... char g char b char r char g char b char r

25 Passage de paramètre Types de passage de paramètres en C/C++ // passage par valeur, la valeur ne peut pas changer agrandirX3(img); // passage par adresse, le contenu peut changer agrandirX0_5(&img); // passage par reference (seulement en C++), la valeur peut changer agrandirX2(img); Quand utiliser chacun de ces types?

26 Passage par valeur void agrandirX3(Image img) { /*--- modification de l'image transmise a la fonction ----*/ img.bitmap = imgX2.bitmap; img.largeur = imgX2.largeur; img.hauteur = imgX2.hauteur; // <- img RECU EN VALEUR, NE CHANGERA PAS! } Contrairement à la transmission par référence, la transmission par valeur fait une copie temporaire de la variable dans la fonction, et donc une modification de la variable ne sera pas conservée à la sortie de la fonction, puisque la modification a été faite sur une variable temporaire.  pas d'«effet de bord»  Les modifications sur les données passées n'ont pas d'influence sur les données originales.  Mais il y a un coût de la copie lorsqu'on passe par valeur.

27 void agrandirX3(image img) {(..)} Schématisation du passage par valeur image= iImage iImage image= Le contenu de iImage est copié dans le paramètre img img

28 Passage par valeur, exemple #include #include using namespace std; string MettreEnMajuscule (string texte) { unsigned int dim = texte.size(); for (int i=0; i

29 Passage par adresse (pointeur) void agrandirX0_5( Image* img) { /*--- modification de l'image transmise a la fonction ----*/ img->bitmap = imgX0_5.bitmap; img->largeur = imgX0_5.largeur; img->hauteur = imgX0_5.hauteur; // img RECU PAR ADRESSE, // LE CONTENU CHANGERA! } La transmission par adresse est similaire à celle par référence, puisque c’est l’adresse de la variable qui est transmise à la fonction, et donc une modification sur le contenu pointé par le pointeur sera conservée lors du retour à la fonction appelante. C’est le mode typique de transmission de paramètre en C.

30 Schématisation du passage par adresse image void agrandirX0_5(image* img) { (..) (..)} iImage iImage image* Le paramètre img est en fait l’adresse de la variable iImage img img

31 Passage par adresse, exemple Département d’informatique et de génie logiciel 31 #include #include using namespace std; string MettreEnMajuscule (string* texteP) { unsigned int dim = texteP->size(); for (int i=0; i

32 Les références Les types références En C, il n’y avait qu'un seul mode de passage des paramètres : le passage par valeur (même le passage par adresse correspond à passer une adresse par valeur ). Supposons qu'une fonction ait besoin de modifier la valeur de l'un de ses paramètres. Comme seul le passage par valeur est supporté, il ne reste plus qu'à passer un pointeur sur l'objet à modifier. En C++, on peut aussi passer un paramètre par référence. Les références sont donc principalement utilisées en C++ pour le passage des paramètres et le retour de valeurs par les fonctions. En particulier, le passage de paramètres par référence permet d‘éviter l'utilisation de pointeurs.

33 Les références Considérez la déclaration suivante: int x=0; int y=x; Les noms x et y représentent deux objets distincts qui contiennent la même valeur. En C++, il est aussi possible de déclarer deux noms de variables faisant référence au même objet. Par exemple dans la déclaration: int a=0; int& b=a; a et b sont deux noms se référant au même objet. Ainsi les instructions: a = 666; cout << b; auront pour effet d'afficher la valeur 666.

34 Les références Exemple : int i; int & ir = i; // ir référence à i int *ptr; i=1; cout << "i= " << i << " ir= " << ir << endl; // affichage de : i= 1 ir= 1 ir=2; cout << "i= " << i << " ir= " << ir << endl; // affichage de : i= 2 ir= 2 ptr = &ir; *ptr = 3; cout << "i= " << i << " ir= " << ir << endl; // affichage de : i= 3 ir= 3

35 Les références Une référence à un paramètre de fonction en C++ sera un alias pour son argument correspondant. Pour indiquer qu’un paramètre de fonction doit être passé par réference par exemple, il suffit de faire suivre le type du paramètre de la fonction par le symbole &: char* foo(int &a, char b); C’est la même chose pour la déclaration d’un alias à une variable comme on vient de le voir (une variable de type référence): int a; int & ar = a; Les variables de type référence doivent être initialisées lors de leur déclarations. De plus, elle ne peuvent plus être réassignées. Attention: La syntaxe est néanmoins trompeuse car le signe de référence "&" est le même que celui de la prise d'adresse.

36 Passage par référence void agrandirX2(Image & img) //img change de valeur { /*--- modification de l'image transmise a la fonction ----*/ img.bitmap = imgX2.bitmap; img.largeur = imgX2.largeur; img.hauteur = imgX2.hauteur;... En ajoutant un & devant le paramètre reçu, une référence à l’adresse est transmise, et en modifiant la valeur du paramètre dans la fonction, la modification sera conservée à la sortie de la fonction. Le passage par référence, tout comme le passage par pointeur, donne directement accès aux données originales. Pas de coût associé au passage par référence mais il y a risque de corruption des données par les fonctions appelées.

37 Schématisation du passage par référence image void agrandirX2(Image & img) { (..) } iImage iImage L’objet reçoit temporairement un nouveau nom qui sera utilisé par la fonction. img img

38 Passage par référence, autre exemple #include #include using namespace std; string MettreEnMajuscule (string& texte) { unsigned int dim = texte.size(); for (int i=0; i

39 Passage par référence constante Lorsque un paramètre passé par valeur dans une fonction prend beaucoup d'espace mémoire, le passer par référence évite de copier une grande quantité d'information. En ajoutant le qualificatif const, on rend impossible la modification du paramètre. struct GrosseStructure {... }; void f(const GrosseStructure& s) {... }

40 Passage par référence constante Le langage C++ offre donc la possibilité de protéger les données originales en utilisant le passage par référence ET le mot const. Il n'y a plus de coût associé à la copie des données et il n'y a plus de risque de modification des données originales. Le passage par référence constante donne le même résultat que le passage par valeur, la copie en moins !

41 Passage par référence constante, exemple #include #include using namespace std; string MettreEnMajuscule (const string& texte) { unsigned int dim = texte.size(); string texte2 = texte; //ou string texte2(texte); for (int i=0; i

42 Conclusion sur le passage de paramètre  Ne jamais utiliser le passage par valeur sauf pour les types de base du langage.  Toujours utiliser le passage par référence constante si on désire que les données originales ne soient pas modifiées.  Toujours utiliser le passage par référence si on désire que les données originales soient modifiées.  Le passage par pointeur pourra être utilisé dans quelques rares cas, notamment pour le polymorphisme (voir le cours de POO).

43 Les références Résumé des passage de paramètres void e( T i ) ; // i est copié, la copie est utilisée par la fonction. void f( T& i ) ; // les modifications sur i seront valables globalement. void g(const T i ) ; // i est copié, la copie n’est utilisée qu’en lecture. void h(const T& i ) ; // i ne peut pas être modifié. void Double(int x) { x= x*2; } int a=2; Double(a); //a==2 void Double(int& x) {x=x * 2; } Double(a); //a==4 void Double(int* x) {*x=*x * 2; } Double(&a);//a==4

44 Les pointeurs et le calcul d’adresses Si ptr est un pointeur: *ptr : retourne le contenu de la case mémoire pointée par ptr ptr+i :nouveau pointeur qui pointe i cases plus loin que la case pointée par ptr *(ptr+i) : retourne le contenu la ième case après la case pointée par ptr ptr[i] : même chose que *(ptr+i)

45 Calcul d’adresses 1 tableau = une suite de cases, qui qui sont contiguës peuvent être accédées à partir d’une adresse de base, par un déplacement (indice) ex: char tab[8];//chaîne de caractères à la C b o n j o u r \

46 Calcul d’adresses nom du tableau = adresse de départ pointeur sur un char char tab[8] = “bonjour”; char *chaine; chaine = tab; /* tab est une valeur constante! */ b o n j o u r \ tab chaîne

47 Calcul d’adresses tab[0] = *tab = *chaine tab[i] = *(tab+i) tab[i] = *(tab+i) = *(chaine+i) &tab[0] = &(*(tab+0)) = &*tab = tab i = déplacement en nb d’éléments tab[i] = *(tab+(i* "sizeof(char)" ) b o n j o u r \ tab chaîne X

48 Calcul d’adresses  int note[4]={56, 23,67,89};  int *liste;  note[i] = *(note+i) = *(note+(i* "sizeof(int)" );  liste=note;  liste[i] = *(liste+i) = *(liste+(i* "sizeof(int)" );  cout << note << liste;  cout << *++liste; cout << ++*liste;  cout << *liste++; cout << *liste);  cout << liste-note << endl; note liste X X Qu’affiche le programme suivant?

49 Exemple: strlen de  strlenV1: utilisant un tableau strlenV1  strlenV2: utilisant un pointeur strlenV2  strlenV3: calcul d’adresse strlenV3  appel: strlen(tab), strlen(“bonjour”), strlen(chaine)  autres appels: strlen(&tab[3]), strlen(&chaine[4]) b o n j o u r \ tab chaîne int strlenV3(char *ch ) {char *debut; /*A: ch contient le caractère '\0' */ debut = ch; while (*ch != '\0') ch++; return ch - debut; }

50 Calcul d’adresses char *tab[ ] = { "Eleves","Prof","Stage"};  tab : est équivalent à : &tab[0] : c'est l'adresse du premier élément du tableau.  *tab : est équivalent à : tab[0] : c'est le premier élément du tableau, c'est à dire l'adresse du premier caractère de la chaîne "Eleves".  tab[1] : est l'adresse sur le premier caractère de la chaîne "Prof".  *(tab+1) : est équivalent à : tab[1]

51 Calcul d’adresses char *tab[ ] = { "Eleves","Prof","Stage"};  *tab[1] : est le caractère pointé par tab[1] : c'est le caractère 'P' de la chaîne "Prof".  **tab : est équivalent à : *tab[0] :c'est le caractère 'E' de la chaîne"Eleves".  **(tab+1) : est équivalent à : *tab[1] :c'est le caractère 'P' de la chaîne"Prof".  *(tab[2]+1) : est équivalent à : tab[2][1] :c'est le caractère 't' de la chaîne"Stages".

52 Pointeur sur une fonction int tab[10];  tab est l'adresse du premier octet du tableau. void fonctionQc(int i, int j){…}  À l’instar du nom d’un tableau, fonctionQc est l'adresse du premier octet implantant la fonction. Comme le nom d’une fonction est une adresse, il est alors possible de déclarer une variable de type pointeur sur la fonction :  void (*f)(int, int); À la déclaration, il faut définir :  le type des paramètres  le type de retour

53 Exemple 1 int addition(int i, int j); int main() { int (*fonction)(int, int); /*déclaration d'un pointeur sur une fonction*/ int i=1, j=2, k; //déclaration de 3 entiers fonction = &addition; // ou bien fonction = addition k = fonction(i,j); k = (*fonction)(i,j); k = addition(i,j); k = (*addition)(i,j); return(0); }

54 int addition(int i, int j) { return i+j; } int main() { int (*fonction)(int, int); /*déclaration d'un pointeur sur une fonction*/ int i=1, j=2, k; //déclaration de 3 entiers fonction = &addition; //ou simplement fonction = addition cout << addition << endl; //on affiche l’adresse de addition cout << fonction << endl; // et de fonction: on vérifie s’ils sont égales return(0); } Exemple 2

55 Un pointeur sur une fonction comme paramètre d'une fonction void tri(int *tab, int size, bool (*compare)(int, int)) { void swap(int *, int *); for (int idx = 1; idx <= size - 1; idx++) for (int count = 0; count <= size - 2; count++) if ((*compare)(tab[count], tab[count + 1])) swap(&tab[count], &tab[count + 1]); } L'ordre du tri est défini par la fonction compare Exemple 3

56 Exemple 4. Une calculette Un tableau de pointeurs sur des fonctions int addition(int n1, int n2) { return n1 + n2; } int soustraction(int n1, int n2) { return n1 - n2;} int multiplication(int n1, int n2) { return n1 * n2;} int division(int n1, int n2) {return n1 / n2; } int main() {int i, j,k; int (*ptf[ ])(int, int) = {addition, soustraction, multiplication, division}; k = ptf[0](i, j); k = (*ptf[0])(i, j); …

57 Pointeur sur une fonction  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.

58 Pointeur sur une fonction  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:  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.

59 3 grandes zones de mémoire:  Zone de la pile d'appel  Zone d'adressage statique  Zone d'allocation dynamique sur le tas/monceau (heap) Les types de mémoire Gestion dynamique de la mémoire

60  Zone de la pile d'appel permet de stocker des variables temporaires dont la durée de vie correspond à la durée de la fonction à laquelle ces variables sont associées.  une variable locale apparaît au moment d'un appel de fonction et disparaît lorsque cette même fonction se termine.  La pile empile et dépile continuellement des données. Zone de la pile d’appel Gestion dynamique de la mémoire

61  Permet de définir  des variables globales,  des variables définies hors des fonctions  des variables déclarées comme statique.  La visibilité de ces variables dépend de leur déclaration.  Leur durée de vie coïncide avec le démarrage de l’exécutable et l’arrêt de celui-ci. Zone d’adressage statique Gestion dynamique de la mémoire

62 Les deux premières zones ont leur utilité mais demeurent insuffisantes pour la plupart des programmes sérieux.  Dans un programme, on ne peut  estimer la quantité de mémoire nécessaire  prévoir à quel moment celle-ci sera nécessaire  Réserver une très grande partie de la mémoire simplement parce qu'on prévoit en avoir besoin? Utiliser l'allocation dynamique pour obtenir et libérer de la mémoire lorsqu'on en a vraiment besoin. Zone d’allocation dynamique Gestion dynamique de la mémoire

63  Le monceau (heap) est le nom de la zone mémoire qui sert à l'allocation dynamique de blocs de mémoire de taille variable.  Le seul risque est la fragmentation du heap, par allocation et libération successives. Il n'existe pas en C++ (comme en C) de mécanisme de "ramasse-miettes" comme dans le langage Java (garbage collector).  L'allocation de mémoire dynamique a tendance à être un peu plus problématique pour le programmeur. C’est lui qui l'alloue, qui la gère et qui n'oublie pas de la rendre au système quand il n'en a plus besoin (comme en C). Sinon, attendez vous à des problèmes!!!  Pour une application sérieuse, i.e. une application vouée à fonctionner pendant des jours et des jours sans corrompre sa mémoire, les «déchets» (ou «memory leaks») ne sauraient être tolérés. Il faut toujours libérer la mémoire qui a été allouée sur le monceau.

64  Pour allouer de la mémoire dynamiquement.  opérateur new : X* p = new X(arguments du constructeur);  On doit absolument récupérer l'espace alloué par le new sur le monceau  sinon on laisse des déchets (memory leaks).  Appel de l'opérateur delete sur la mémoire obtenue à partir d'un new : delete p; En C++ Gestion dynamique de la mémoire

65 1)Soit le pointeur int *ptr = 0; int* ptr 0 int* ptr 2) 2) On alloue un espace mémoire à l’endroit où le pointeur pointe, avec ptr = new int; int Gestion dynamique de la mémoire int* ptr 3) 3) On libère l’espace mémoire alloué avec delete ptr; int !! En effet, ptr continuera quand même de pointer la mémoire libérée. Le compilateur a pris acte de la libération, mais un programme peut continuer a utiliser cette mémoire si on ne fait pas attention. Bien sûr on court des risques énormes ai on laisse la chose ainsi. On doit s’assurer d’assigner à ptr une autre adresse qui « existe » pour le programme, sinon, lui octroyer la valeur NULL (l’adresse 0).

66 Double indirection ou pointeur ** Soit int **ptr: int** ptr int**int* int**int* ptr = new int* ptr *ptr = new int int ptr Il s’agit donc d’un pointeur sur un pointeur (ou sur un tableau de pointeurs) Gestion dynamique de la mémoire

67  Souvent, le nombre de dossiers usager est inconnu avant l’exécution du programme, et ce serait un gaspillage de mémoire de se créer un tableau statique de 100 (Tab[100]) si on ne devait stocker que 10 items.  Un tableau dynamique consiste à se déclarer un pointeur, qui pointe à un espace mémoire, et qui sera alloué dynamiquement lors de l’exécution du programme.  Avantage du tableau dynamique : donne la flexibilité au programmeur de pouvoir créer des tableaux sans savoir sa taille au préalable. Tableau dynamique Gestion dynamique de la mémoire

68 Mécanisme d’allocation Gestion dynamique de la mémoire  L'espace est réservé dans une zone de la mémoire du monceau (heap).  Ce n'est pas sur la pile d'exécution. Par conséquent l'espace n'est pas éliminé après l'exécution d'un bloc ou d'une fonction. Attention cependant à la variable pointeur qui conserve l'adresse de l'espace; celle-ci peut être perdue, auquel cas on n'aurait plus accès à l'espace mémoire réservé.  Lorsqu'on a terminé avec l'usage de la mémoire: delete [] tab;  L'opérateur delete libère l'espace mémoire alloué par new à un seul objet, tandis que l'opérateur delete[] libère l'espace mémoire alloué à un tableau d'objets. // il est sage de toujours initialiser // les pointeurs (au moins à NULL = 0) int *tab = 0, nb; // réserve l'espace pour 5 entiers et // renvoie l'adresse du premier octet // ou ??? en cas d’échec nb = 5; tab = new int[nb]; tab

69 struct date {int jour, mois, an; }; date *ptr4, *ptr5, *ptr6, d = {25, 4, 1952}; ptr4 = new date; // allocation dynamique d'une structure ptr5 = new date[10]; // allocation dynamique d'un tableau de structure ptr6 = new date(d); // allocation dynamique d'une struct. avec init. … delete ptr4; //Rappel: après l’exécution de delete, l’espace mémoire delete[] ptr5;//est désalloué, mais les pointeurs continuent de pointer à // cet endroit. Il faut donc prendre la bonne habitude de le mettre à NULL // (ou plutôt 0) int *p =0, *r =0; double *q =0; p = new int; // p pointe vers un nouvel entier q = new double[10]; // q pointe vers un tableau de 10 doubles r = new int(10); // allocation d'un entier avec initialisation... delete p; // l'espace mémoire de l'entier est libéré delete[] q; // l'espace mémoire du tableau est libéré … Exemples Gestion dynamique de la mémoire

70 int * p = (int*)malloc(sizeof(int)); free(p); p = 0; double * q = (double*)calloc(10,sizeof(double)); q = (double*)realloc(q,100*sizeof(double)); free(p); p = 0; int * p = new int; delete p; p =0; double * p = new double[10]; delete[] p; p = 0; En C En C++ Gestion dynamique de la mémoire La fonction realloc du C, n'a pas d’équivalent en C++. Cela ne cause cependant aucun inconvénient puisque les tableaux peuvent être remplacés par des variables de type vector qui éliminent, à toute fin pratique, les problèmes de redimensionnement. Différence entre le C et le C++

71 Remarque Soit la fonction de désallocation suivante: void detruireImage(image* & p, int* NbImages) { delete []p;//desalloue l'espace memoire p =0;// fait pointer le pointeur a NULL. } p doit être passé par référence, puisque sa valeur change dans la fonction (p = 0 ou NULL) et on veut que ce changement soit effectif dans la fonction qui a appelé detruireImage. Gestion dynamique de la mémoire

72 Se créer un tableau dynamique 2D 1)A partir d’un double pointeur int **p=NULL, on a: 2) Par la suite, on alloue la première dimension de notre tableau, avec: p=new int*[dim1], on a alors: 3) Finalement, on déclare la deuxième dimension par une boucle, c’est-à-dire que pour chaque ligne, on se déclare un nombre dim2 de colonnes: for(int i=0; i

73 Se créer un tableau dynamique 2D Exemple d’une fonction d’allocation 2D : int ** tableau2d(int dim1,int dim2) { int **p; if (( p = new int*[dim1])== 0) return 0; for ( int i = 0; i < dim1; i++) if ((p[i]= new int[dim2])== 0) return 0; return p; } Gestion dynamique de la mémoire

74 Désallocation d’un tableau dynamique 2D Afin de désallouer un tableau 2D, on effectue une boucle for qui passe à travers le tableau de pointeurs sur les rangées pour y désallouer chaque rangée: for(int i=0; i

75 Note sur la désallocation  La désallocation s’effectue en sens inverse de l’allocation, c’est-à-dire que si on alloue en premier le tableau de pointeurs sur les rangées, il faut désallouer les rangées avant de désallouer ce tableau. p  En effet, si on désallouait d’abord le tableau de pointeurs sur les rangées, on n’aurait plus aucun pointeur qui pourrait retrouver les rangées, et donc l’espace mémoire resterait alloué mais inaccessible. ? Comment les désallouer Gestion dynamique de la mémoire

76 Désallocation d’un tableau dynamique 2D Exemple d’une fonction de désallocation : void detruitTableau2D( int ** & p, int dim1, int dim2) { for ( int i = 0; i < dim1; i++) delete [] p[i]; delete []p; p = 0; } Remarque: p doit être passé par référence, puisque sa valeur change dans la fonction (p = 0) et on veut que ce changement soit effectif dans la fonction qui a appelé detruitTableau2D Gestion dynamique de la mémoire

77 double ** allocMatrix(int n, int m) { double ** m = new double*[n]; for(int i=0; i< n; ++i) m[i] = new double[m]; return m; } void freeMatrix(double ** &m, int n) { for(int i=0; i< n; ++i) delete[] m[i]; delete[] m; m = 0; } Autre exemple Allocation d’une matrice nxm. Gestion dynamique de la mémoire Libération de l’espace mémoire Alloué pour la matrice

78 Remarque Gestion dynamique de la mémoire La fonction new génère une exception (on verra la gestion des exceptions Un peu plus tard) s'il n'arrive pas à allouer de la mémoire. Dans ce cas, il n'y a aucune garantie qu‘elle retourne 0 (ou NULL) si l’allocation échoue. On peut cependant l'obliger à le faire en faisant new(nothrow). Il faudrait inclure dans ce cas la librairie, ainsi new retournera 0 si elle échoue.

79 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. #include … char *x; x = new(nothrow) char[strlen(‘’Bonjour’’)+1]; if(!x) { // Erreur…l’allocation a échoué } //Si l’allocation a réussi, la ligne suivante peut se faire… strcpy(x, ’’Bonjour’’); cout << x << endl; delete[] x; À ne pas oublier!

80 Chaînes de caractères dynamiques  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  La librairie standard de C++ met à notre disposition le type string (en réalité une classe), permettant une représentation efficace des chaînes. Bien sûr, on pourra parler également de tableaux de string voire des vectors de string. La version des chaînes que nous avons invoquer, bien que reconnue en C++, est attachée plutôt au langage C. Nous aurons l’occasion dans le cours de présenter les types string et vector.

81 Les tableaux à plusieurs dimensions  Comme pour les tableaux de chaînes de caractères, 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 ».

82 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]

83 Allocation dynamique de la mémoire Obtenir des variables (de la mémoire) selon les besoins de l’application gestion dynamique de la mémoire mais: variables sans noms! => accès par pointeur debut Utilisation des pointeurs Liste simplement chaînée

84 Objets chaînés struct Noeud { int el; /* l’information à stocker */ Noeud*suivant; } ; Noeud *debut, *courant, *nouveau; Noeud t1, t2, t3; debut = &t1; debut el suivant t1 el suivant t2 el suivant t3 courant

85 t1.el = 3; ou (*debut).el = 3; ou debut->el = 3; debut el suivant t1 3 el suivant t2 el suivant t3 courant Accès aux objets pointées

86 t1.el = 3; ou (*debut).el = 3; ou debut->el = 3; t1.suivant = 0; ou (*debut).suivant = 0 ou debut->suivant = 0; debut el suivant t1 3 el suivant t2 el suivant t3 courant Accès aux objets pointées

87 t1.el = 3; ou (*debut).el = 3; ou debut->el = 3; t1.suivant = NULL; ou (*debut).suivant = 0 ou debut->suivant = 0; t1.suivant = &t2; ou (*debut).suivant = &t2 ou debut->suivant = &t2; debut el suivant t1 3 el suivant t2 el suivant t3 courant Accès aux objets pointées

88 t1.el = 3; ou (*debut).el = 3; ou debut->el = 3; t1.suivant = NULL; ou (*debut).suivant = 0 ou debut->suivant = 0; t1.suivant = &t2; ou (*debut).suivant = &t2 ou debut->suivant = &t2; debut->suivant->el = 4; debut el suivant t1 3 el suivant t2 el suivant t3 courant 4 Accès aux objets pointées

89 t1.el = 3; ou (*debut).el = 3; ou debut->el = 3; t1.suivant = NULL; ou (*debut).suivant = 0 ou debut->suivant = 0; t1.suivant = &t2; ou (*debut).suivant = &t2 ou debut->suivant = &t2; debut->suivant->el = 4; debut->suivant->suivant = 0; debut el suivant t1 3 el suivant t2 el suivant t3 courant 4 Accès aux objets pointées

90 courant = debut->suivant; debut el suivant t1 3 el suivant t2 el suivant t3 courant Accès aux objets pointées

91 courant = debut->suivant; (*courant).el = 4; ou courant->el = 4; debut el suivant t1 3 el suivant t2 el suivant t3 courant 4 Accès aux objets pointées

92 courant = debut->suivant; (*courant).el = 4; ou courant->el = 4; (*courant).suivant = 0; ou courant->suivant = 0; debut el suivant t1 3 el suivant t2 el suivant t3 courant 4 Accès aux objets pointées

93 courant = debut->suivant; (*courant).el = 4; ou courant->el = 4; (*courant).suivant = 0; ou courant->suivant = 0; courant->suivant = &t3; debut el suivant t1 3 el suivant t2 el suivant t3 courant 4 Accès aux objets pointées

94 courant = debut->suivant; (*courant).el = 4; ou courant->el = 4; (*courant).suivant = 0; ou courant->suivant = 0; courant->suivant = &t3; courant = courant->suivant; debut el suivant t1 3 el suivant t2 el suivant t3 courant 4 Accès aux objets pointées

95 courant = debut->suivant; (*courant).el =...; ou courant->el =...; (*courant).suivant = 0; ou courant->suivant = 0; courant->suivant =...; courant = courant->suivant; debut el suivant t1 3 el suivant t2 el suivant t3 courant 4... Accès aux objets pointées

96 courant = debut->suivant; (*courant).el =...; ou courant->el =...; (*courant).suivant = 0; ou courant->suivant = 0; courant->suivant =...; courant = courant->suivant; debut el suivant t1 3 el suivant t2 el suivant t3 courant 4... Accès aux objets pointées

97 courant = debut->suivant; (*courant).el =...; ou courant->el =...; (*courant).suivant = 0; ou courant->suivant = 0; courant->suivant =...; courant = courant->suivant; debut el suivant t1 3 el suivant t2 el suivant t3 courant 4... Accès aux objets pointées

98 courant = debut->suivant; (*courant).el =...; ou courant->el =...; (*courant).suivant = 0; ou courant->suivant = 0; courant->suivant =...; courant = courant->suivant; debut el suivant t1 3 el suivant t2 el suivant t3 courant 4... Accès aux objets pointées

99 Allocation de mémoire obtenir les objets au besoin: debut = new Noeud; réserve de l’espace sur le « heap » (tas) retourne un pointeur sur un objet Noeud lance une exception si plus de mémoire debut el suivant taspile

100 obtenir les objets au besoin: debut = new Noeud; réserve de l’espace sur le « heap » (tas) retourne un pointeur sur un objet Noeud lance une exception si plus de mémoire debut el suivant taspile Allocation de mémoire

101 obtenir les objets au besoin: debut = new Noeud; réserve de l’espace sur le « heap » (tas) retourne un pointeur sur un objet Noeud lance une exception si plus de mémoire debut->el = 3; debut->suivant = 0; Allocation de mémoire debut el suivant taspile 3

102 debut el suivant taspile 3 obtenir les objets au besoin: debut = new Noeud; réserve de l’espace sur le « heap » (tas) retourne un pointeur sur un objet Noeud lance une exception si plus de mémoire debut->el = 3; debut->suivant = 0; Allocation de mémoire

103 Libération de mémoire obtenir les objets au besoin: debut = new Noeud; réserve de l’espace sur le « heap » (tas) retourne un pointeur sur un objet Noeud lance une exception si plus de mémoire debut->el = 3; debut->suivant = 0; delete debut; debut = 0; /* et non l’inverse */ debut el suivant taspile 3

104 Libération de mémoire obtenir les objets au besoin: debut = new Noeud; réserve de l’espace sur le « heap » (tas) retourne un pointeur sur un objet Noeud lance une exception si plus de mémoire debut->el = 3; debut->suivant = 0; delete debut; debut = 0; /* et non l’inverse */ debut el suivant taspile 3 x

105 Libération de mémoire debut el suivant taspile 3 x obtenir les objets au besoin: debut = new Noeud; réserve de l’espace sur le « heap » (tas) retourne un pointeur sur un objet Noeud lance une exception si plus de mémoire debut->el = 3; debut->suivant = 0; delete debut; debut = 0; /* et non l’inverse */

106 Libération de mémoire debut el suivant taspile 3 obtenir les objets au besoin: debut = new Noeud; réserve de l’espace sur le « heap » (tas) retourne un pointeur sur un objet Noeud lance une exception si plus de mémoire debut->el = 3; debut->suivant = 0; debut = 0; delete debut; /* et non l’inverse */

107 debut = new Noeud; debut->el = 3; debut->suivant = NULL; debut = new unNoeud; debut el suivant taspile 3 Chaînage d’objets

108 debut el suivant taspile el suivant 3 debut = new Noeud; debut->el = 3; debut->suivant = NULL; debut = new Noeud; //non, il faut utiliser un autre //pointeur que debut, le sommet de la liste chaînée en //construction, pour poursuivre la //construction de la liste chaînée Chaînage d’objets Érreur à éviter car, entre autres, on a créé un déchet (memory leack)

109 /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau; debut taspile nouveau courant Chaînage d’objets

110 debut el suivant taspile nouveau courant /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau; Chaînage d’objets

111 debut el suivant taspile 3 nouveau courant /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau; Chaînage d’objets

112 debut el suivant taspile 3 nouveau courant Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

113 debut el suivant taspile 3 courant nouveau Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

114 debut el suivant taspile 3 nouveau courant el suivant Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

115 debut el suivant taspile 3 nouveau courant el suivant... Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

116 debut el suivant taspile 3 nouveau courant el suivant... Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

117 debut el suivant taspile 3 nouveau courant el suivant... Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

118 debut el suivant taspile 3 nouveau courant el suivant 3... Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

119 debut el suivant taspile 3 nouveau courant el suivant 3... Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

120 debut el suivant taspile 3 nouveau courant el suivant 3... el suivant Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

121 debut el suivant taspile 3 nouveau courant el suivant 3... el suivant... Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

122 debut el suivant taspile 3 nouveau courant el suivant 3... el suivant... Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

123 debut el suivant taspile 3 nouveau courant el suivant 3... el suivant... Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

124 debut el suivant taspile 3 nouveau courant el suivant 3... el suivant... Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau;

125 Constructeur!! Chaînage d’objets /* pour le premier élément */ debut = new Noeud; debut->el = 3; debut->suivant = 0; courant = debut; /* pour les autres éléments */ nouveau = new Noeud; nouveau->el =...; nouveau->suivant = 0; courant->suivant = nouveau; courant = nouveau; On est en présence ici de 2 parties de code identiques, on va les rassembler dans une fonction/méthode appelée constructeur.

126 struct Noeud { int el; // l’information à stocker Noeud*suivant; // lien avec le suivant Noeud (const int& data_item, Noeud* next_ptr = 0) : el(data_item), suivant(next_ptr) {} //constructeur } ; Chaînage d’objets

127 /* pour le premier élément */ debut = new Noeud(3); courant = debut; /* pour les autres éléments */ nouveau = new Noeud(…); courant->suivant = nouveau; courant = nouveau; debut taspile nouveau courant Chaînage d’objets

128 debut taspile nouveau courant el suivant 33 Chaînage d’objets /* pour le premier élément */ debut = new Noeud(3); courant = debut; /* pour les autres éléments */ nouveau = new Noeud(…); courant->suivant = nouveau; courant = nouveau;

129 debut taspile nouveau courant el suivant 33 Chaînage d’objets /* pour le premier élément */ debut = new Noeud(3); courant = debut; /* pour les autres éléments */ nouveau = new Noeud(…); courant->suivant = nouveau; courant = nouveau;

130 debut taspile nouveau courant el suivant 33 el suivant... Chaînage d’objets /* pour le premier élément */ debut = new Noeud(3); courant = debut; /* pour les autres éléments */ nouveau = new Noeud(…); courant->suivant = nouveau; courant = nouveau;

131 debut taspile nouveau courant el suivant 33 el suivant... Chaînage d’objets /* pour le premier élément */ debut = new Noeud(3); courant = debut; /* pour les autres éléments */ nouveau = new Noeud(…); courant->suivant = nouveau; courant = nouveau;

132 debut taspile nouveau courant el suivant 33 el suivant... Chaînage d’objets /* pour le premier élément */ debut = new Noeud(3); courant = debut; /* pour les autres éléments */ nouveau = new Noeud(…); courant->suivant = nouveau; courant = nouveau;

133 debut taspile nouveau courant el suivant 33 el suivant... Chaînage d’objets /* pour le premier élément */ debut = new Noeud(3); courant = debut; /* pour les autres éléments */ nouveau = new Noeud(…); courant->suivant = nouveau; courant = nouveau; Liste simplement chaînée

134 Chaînage d’objets  Le C++, comme le C, n'offre pas de mécanisme de récupération automatique de la mémoire (garbage collecting). Attention donc aux déchets (memory leack).  Allocation et libération des ressources : la responsabilité du programmeur et attention aux références pendantes!  Pour gérer les ressources à l'intérieur des classes, il faut correctement implanter certaines fonctions/méthodes pour ne pas se retrouver avec des problèmes. Nous en reparlerons. Rappels et mise en garde!

135 Copies profondes et copies de surface  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.  Par exemple, si on fait delete sur l’une des copies, alors la deuxième copie n’est plus utilisable non plus et fera planter le logiciel. Noeud n1(10), n2; n2 = n1; //  Dangereux!  Nous verrons bientôt comment régler ce problème par le biais de la surcharge de l’opérateur d’affectation.

136 Éléments techniques avancées du C++  Le mot clé const  Le mot clé static  Les itérations

137  On sait que le mot-clé « const » sert à spécifier au compilateur qu’une variable ne devrait pas être modifiée:  Les deux expressions suivantes sont exactement équivalentes: int const x; const int x;  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. Le mot-clé const

138 « 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 » cout << *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 ».

139  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! Le mot-clé const

140 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.

141 Les fonctions « const » en C++ class S { public: int x; void f() { x=5; } void h() const //on ne peut modifier les attributs de la classe { cout << x << endl; // Ok, x est utilisé en lecture. g(); // Ok, car g est const aussi. } void g() const //idem { x=3; // Erreur de compilation! f(); // Erreur de compilation! } };

142 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).

143 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’’); »

144 Un autre exemple de l’avantage de « const » en C++ class Point { public: Point inverse(); private: float x,y; }; Par contre, si la fonction avait été déclarée « const »: class Point { public: Point inverse() const; private: float x,y; }; 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() ». 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.

145 Le 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!

146 Les trois sens du mot-clé « static » 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 «.cpp » en cours, et qu’elle ne sera pas rendue disponible lors de l’édition des liens (« link ») pour les autres fichiers «.cpp » 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 «.cpp » qui est en train de se faire compiler!)

147 Les trois sens du mot-clé « static » 3.À l’intérieur d’une structure :  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.

148 « 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++; cout <<  La fonction f a été appelée  <

149 « static » appliqué sur une variable ou une fonction globale //Allo.h void f(); static void g() { cout <<‘’g’’; } void h() { cout <<‘’h’’; } static void k(); extern int x; int y; //Allo.cpp #include ‘’Allo.h’’ int x; static int z = 3; void f() { cout <<‘’f’’; k(); } static void k() { cout <<‘’k’’; } //Salut.cpp #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 «.cpp ». Il aurait fallu déclarer // « extern » dans « Allo.h », puis le déclarer pour vrai dans // un seul fichier «.cpp », 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é)

150 « static » sur une fonction membre ou variable membre

151 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

152 Itérations  Les algorithmes de la STL (Standard Template Library 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++) cout << *x << endl; } 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; }

153 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.


Télécharger ppt "Structures de données IFT-2000 Abder Alikacem Pointeurs, références et gestion dynamique de la mémoire. QQ éléments techniques du C++. Semaine 2 Département."

Présentations similaires


Annonces Google