Structures de données IFT-2000

Slides:



Advertisements
Présentations similaires
Introduction au Langage C,C++
Advertisements

Programmation Orienté Objet en C++
Rappels C.
Sémantique des déclarations pour le langage Z minimal
La classe String Attention ce n’est pas un type de base. Il s'agit d'une classe défini dans l’API Java (Dans le package java.lang) String s="aaa"; // s.
GEF 243B Programmation informatique appliquée Types dérivés, structures et tableaux §
3- Déclaration et accès aux objets
C.
Paramètres et pointeurs
Structures et unions types énumérés Qu'est-ce qu'une structure
FLSI602 Génie Informatique et Réseaux
Points importants de la semaine Les fonctions. La portée. La passage par copie. Les tableaux.
Regrouper des éléments de même type et pouvoir y accéder à laide dun identificateur et dun indice. Objectif des tableaux.
Cours 7 - Les pointeurs, l'allocation dynamique, les listes chaînées
Les méthodes en java Une méthode est un regroupement d’instructions ayant pour but de faire un traitement bien précis. Une méthode pour être utilisée.
Séances de soutien Projet informatique 2A
Structures de données IFT-2000
Leçon 2 : Surcharge des opérateurs IUP 2 Génie Informatique Méthode et Outils pour la Programmation Françoise Greffier Université de Franche-Comté.
Les pointeurs Enormément utilisé en C/C++ ! Pourquoi? A quoi ça sert?
HistoriqueHistorique Langage C++, parution du livre Bjarne Stroustrup Normalisation ANSI.
Les pointeurs Modes d’adressage de variables. Définition d’un pointeur. Opérateurs de base. Opérations élémentaires. Pointeurs et tableaux. Pointeurs et.
IFT-2000: Structures de données Les graphes Dominic Genest, 2009.
Structures de données IFT Abder Alikacem Transtypage Module 1 Département d’informatique et de génie logiciel Édition Septembre 2009.
Structures de données IFT-2000
Structures de données IFT Abder Alikacem La classe string Département dinformatique et de génie logiciel Édition Septembre 2009 Département dinformatique.
Structures de données IFT Abder Alikacem Espace de nommage Département d’informatique et de génie logiciel Édition Septembre 2009.
Structures de données IFT-2000
Programme de baccalauréat en informatique Programmation Orientée Objets IFT Thierry EUDE Module 5 : La surcharge des opérateurs Département dinformatique.
Structures de données IFT-2000
Structures de données IFT-2000
Sixième cours Les chaînes de caractères et le passage de paramètres par référence Passage de paramètres par référence String.h.
Les enregistrements (struct) suite. Struct Rappel Enregistrement : Suite de données pouvant être de types différents, accessibles via une seule variable.
Points importants de la semaine Le préprocesseur. La conversion de types. Les fonctions.
Standard Template Library
CSI 1502 Principes fondamentaux de conception de logiciels
Méthode et Outils pour la Programmation
Structures de données IFT-2000
Structures de données IFT-2000 Abder Alikacem La récursivité Semaine 5 Département dinformatique et de génie logiciel Édition Septembre 2009.
Structures de données IFT-2000 Abder Alikacem La récursivité Département d’informatique et de génie logiciel Édition Septembre 2009.
Analyse d’algorithmes
Structures de données IFT-10541
Structures de données IFT-2000
Structures de données IFT-2000
Types de données fondamentaux
Structures de données IFT Abder Alikacem Semaine 2 Tests sur les pointeurs Département d’informatique et de génie logiciel Édition Janvier 2009.
L’essentiel du langage C
Structures des données
Le langage C Structures de données
Le langage C Rappel Pointeurs & Allocation de mémoire.
4 Introduction des objets. Les chaînes et tableaux
Les Pointeurs et les Tableaux Statiques et Tableaux Dynamiques
Notions de pointeurs en C
Les pointeurs Suite.
Labo II : Tableaux et pointeurs
La notion de type revisitée en POO
et quelques rappels sur certains éléments du langage C
Les adresses des fonctions
SIF-1053 Architecture des ordinateurs
ALGORITHMIQUE ET PROGRAMMATION C
7ième Classe (Mardi, 24 novembre) CSI2572. Devoir 3 ?
1 Structures des données. 2  Le tableau permettait de désigner sous un seul nom un ensemble de valeurs de même type, chacune d'entre elles étant repérée.
HistoriqueHistorique Langage C++, parution du livre Bjarne Stroustrup Normalisation ANSI.
ISBN Chapitre 10 L'implémentation des sous- programmes.
Classe 1 CSI2572 Autres modificateurs de déclaration de variables: & volatile & register & static & auto & extern & const volatile Indique au compilateur.
Conception de Programmes - IUT de Paris - 1ère année Cours 2 – Références et passage de paramètres Les Références –Introduction aux références.
Conception de Programmes - IUT de Paris - 1ère année Quelques éléments du langage C++ Les références La surcharge de fonctions Les fonctions «
Les bases de l’algorithmique
8PRO107 Éléments de programmation Les adresses et les pointeurs.
Exercices.
Transcription de la présentation:

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 Édition Septembre 2009 Département d’informatique et de génie logiciel

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

Les pointeurs Définition Déclaration 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

Opérations d’adressage 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. 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

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!

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

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

Quelques éléments de syntaxe 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;

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

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

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

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

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

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

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

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

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

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

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 *

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.

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

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 <double *> (ptr) = 2.4; //uneVariable prend la valeur 2.4 ptrDouble = static_cast <double *> (ptr); //ptrDouble pointe uneVariable cout << *static_cast <double *> (ptr) << endl; ptr = &i; // ptr pointe maintenant sur la variable entière i

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<char *>(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<int*>(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).

Pointeurs sur structures ou classes Exemple Pixel 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 char r Image temp char g char b int largeur ptr char r int longueur char g char b char r char g char b ...

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?

Passage par valeur 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. 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! }

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

Passage par valeur, exemple #include <iostream> #include <string> using namespace std; string MettreEnMajuscule (string texte) { unsigned int dim = texte.size(); for (int i=0; i<dim; i++) { texte[i] = (char)toupper(texte[i]); } return texte; } int main() { string nom = "programmation"; string NOM = MettreEnMajuscule(nom); cout << "Minuscule : " << nom << endl; cout << "Majuscule : " << NOM << endl; return 0; } Résultat: Minuscule : programmation Majuscule : PROGRAMMATION

Passage par adresse (pointeur) 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. 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! }

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

Passage par adresse, exemple Passage par pointeur : Accès direct aux données originales #include <iostream> #include <string> using namespace std; string MettreEnMajuscule (string* texteP) { unsigned int dim = texteP->size(); for (int i=0; i<dim; i++) { (*texteP)[i] = (char)toupper((*texteP)[i]); } return *texteP; } int main() { string nom = "programmation"; string NOM = MettreEnMajuscule(&nom); cout << "Minuscule : " << nom << endl; cout << "Majuscule : " << NOM << endl; return 0; } Résultat: Minuscule : PROGRAMMATION Majuscule : PROGRAMMATION Département d’informatique et de génie logiciel 31

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.

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.

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; // affichage de : i= 2 ir= 2 ptr = &ir; *ptr = 3; // affichage de : i= 3 ir= 3

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.

Passage par référence 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. 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; . . .

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

Passage par référence, autre exemple Passage par référence : Accès direct aux données originales #include <iostream> #include <string> using namespace std; string MettreEnMajuscule (string& texte) { unsigned int dim = texte.size(); for (int i=0; i<dim; i++) { texte[i] = (char)toupper(texte[i]); } return texte; } int main() { string nom = "programmation"; string NOM = MettreEnMajuscule(nom); cout << "Minuscule : " << nom << endl; cout << "Majuscule : " << NOM << endl; return 0; } Résultat: Minuscule : PROGRAMMATION Majuscule : PROGRAMMATION

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) }

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 !

Passage par référence constante, exemple Passage par référence constante : Accès direct aux données originales sans pouvoir les modifier. #include <iostream> #include <string> 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<dim; i++) { texte2[i] = toupper(texte[i]); } return texte2; } int main() { string nom = "programmation"; string NOM = MettreEnMajuscule(nom); cout << "Minuscule : " << nom << endl; cout << "Majuscule : " << NOM << endl; return 0; } Résultat: Minuscule : programmation Majuscule : PROGRAMMATION

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

Résumé des passage de paramètres 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; } Double(&a); //a==4 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

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)

Calcul d’adresses b o n j o u r \0 qui sont contiguës 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 \0 0  1 2 3 4 5 6 7

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

X Calcul d’adresses b o n j o u r \0 tab[0] = *tab = *chaine 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)" ) X tab b  o  n   j  o  u  r  \0 0    1 2 3 4 5 6 7 chaîne

X X Calcul d’adresses Qu’affiche le programme suivant? note liste 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; X X note liste 0          1 2 3

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

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]

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

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

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); }

Exemple 2 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 3 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 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); …

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.

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

Gestion dynamique de la mémoire Les types de mémoire 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)

Gestion dynamique de la mémoire Zone de la pile d’appel 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 d’adressage statique Gestion dynamique de la mémoire Zone d’adressage statique 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’allocation dynamique Gestion dynamique de la mémoire Zone d’allocation dynamique 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.

Gestion dynamique de la mémoire 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.

Gestion dynamique de la mémoire En C++ 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;

Gestion dynamique de la mémoire ptr Soit le pointeur int *ptr = 0; int* ptr 2) On alloue un espace mémoire à l’endroit où le pointeur pointe, avec ptr = new int; int* int ptr 3) On libère l’espace mémoire alloué avec delete ptr; int* 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).

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

Gestion dynamique de la mémoire Tableau dynamique 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.

Mécanisme d’allocation Gestion dynamique de la mémoire Mécanisme d’allocation tab // 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]; 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.

Gestion dynamique de la mémoire 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 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)

Gestion dynamique de la mémoire Différence entre le C et le C++ En C int * p = (int*)malloc(sizeof(int)); free(p); p = 0; double * q = (double*)calloc(10,sizeof(double)); q = (double*)realloc(q,100*sizeof(double)); 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. En C++ int * p = new int; delete p; p =0; double * p = new double[10]; delete[] p; p = 0;

Gestion dynamique de la mémoire 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.

Se créer un tableau dynamique 2D Gestion dynamique de la mémoire Se créer un tableau dynamique 2D p NULL 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<dim1; i++) pour chaque ligne p[i]=new int[dim2] on alloue dim2 colonnes p dim1 p dim2

Se créer un tableau dynamique 2D Gestion dynamique de la mémoire 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 p; }

Désallocation d’un tableau dynamique 2D Gestion dynamique de la mémoire 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<dim1; i++) delete[] p[i]; p Finalement, on désalloue le tableau de pointeurs sur les rangées avec: delete []p;

Note sur la désallocation Gestion dynamique de la mémoire 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. 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. p ? Comment les désallouer

Désallocation d’un tableau dynamique 2D Gestion dynamique de la mémoire 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 Autre exemple Allocation d’une matrice nxm. 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) delete[] m[i]; delete[] m; m = 0; Libération de l’espace mémoire Alloué pour la matrice

Gestion dynamique de la mémoire Remarque 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 <new>, ainsi new retournera 0 si elle échoue.

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 <new> … 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!

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.

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: int x[2][3][4]; // 2 tableaux de 3 tableaux de 4 ints. int *x[2][3]; //2 tableaux de 3 tableaux d’un nombre variable de ints. int **x[2]; // 2 tableaux de nombres variables de tableaux à taille variable de ints. int ***x; // Nombre variable de tableaux à tailles variables de tableaux à tailles variables de ints. int (*x)[3][4]; // Nombre variable de tableaux de 3 par 4 ints. int (**x)[4]; // Nombre variable de tableaux à tailles variables de 4 ints. int (*x[2])[4]; // 2 tableaux d’un nombre variable de tableaux de 4 ints. 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 ».

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[1] x[0] x[1][2] x[0][0] x[0][1] x[0][2] x[1][0] x[1][1]

Utilisation des pointeurs 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 Liste simplement chaînée

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

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

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

Accès aux objets pointées 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; courant el el el debut 3 suivant suivant suivant t1 t2 t3

Accès aux objets pointées 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; courant el el el debut 3 4 suivant suivant suivant t1 t2 t3

Accès aux objets pointées 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; courant el el el debut 3 4 suivant suivant suivant t1 t2 t3

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

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

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

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

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

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

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

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

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

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 el debut suivant pile tas

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 el debut suivant pile tas

Allocation de mémoire 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; el debut 3 suivant pile tas

Allocation de mémoire 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; el debut 3 suivant pile tas

Libération de mémoire 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; delete debut; debut = 0; /* et non l’inverse */ el debut 3 suivant pile tas

x Libération de mémoire 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; delete debut; debut = 0; /* et non l’inverse */ x el debut 3 suivant pile tas

x Libération de mémoire 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; delete debut; debut = 0; /* et non l’inverse */ x el debut 3 suivant pile tas

Libération de mémoire 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 */ el debut 3 suivant pile tas

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

Chaînage d’objets 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 el suivant el debut 3 Érreur à éviter car, entre autres, on a créé un déchet (memory leack) suivant pile tas

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; nouveau courant debut pile tas

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; nouveau courant el debut suivant pile tas

Chaînage d’objets 3 /* 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; nouveau courant el debut 3 suivant pile tas

Chaînage d’objets 3 /* 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; nouveau courant el debut 3 suivant pile tas

Chaînage d’objets 3 /* 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; nouveau courant el debut 3 suivant pile tas

Chaînage d’objets 3 /* 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; nouveau el courant suivant el debut 3 suivant pile tas

Chaînage d’objets ... 3 /* 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; nouveau el ... courant suivant el debut 3 suivant pile tas

Chaînage d’objets ... 3 /* 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; nouveau el ... courant suivant el debut 3 suivant pile tas

Chaînage d’objets ... 3 /* 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; nouveau el ... courant suivant el debut 3 suivant pile tas

Chaînage d’objets ... 3 3 /* 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; nouveau el ... courant suivant el debut 3 3 suivant pile tas

Chaînage d’objets ... 3 3 /* 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; nouveau el ... courant suivant el debut 3 3 suivant pile tas

Chaînage d’objets ... 3 3 el /* 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; suivant nouveau el ... courant suivant el debut 3 3 suivant pile tas

Chaînage d’objets ... ... 3 3 el /* 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; suivant nouveau el ... courant suivant el debut 3 3 suivant pile tas

Chaînage d’objets ... ... 3 3 el /* 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; suivant nouveau el ... courant suivant el debut 3 3 suivant pile tas

Chaînage d’objets ... ... 3 3 el /* 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; suivant nouveau el ... courant suivant el debut 3 3 suivant pile tas

Chaînage d’objets ... ... 3 3 el /* 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; suivant nouveau el ... courant suivant el debut 3 3 suivant pile tas

Chaînage d’objets Constructeur!! /* 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; Constructeur!! On est en présence ici de 2 parties de code identiques, on va les rassembler dans une fonction/méthode appelée constructeur.

Chaînage d’objets 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 /* pour le premier élément */ debut = new Noeud(3); courant = debut; /* pour les autres éléments */ nouveau = new Noeud(…); courant->suivant = nouveau; courant = nouveau; nouveau courant debut pile tas

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

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

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

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

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

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

Chaînage d’objets Rappels et mise en garde! 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.

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.

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

Le mot-clé const 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.

« const T * » versus « T *const » versus « T const * » « int * const » (très différent!) « 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 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 ».

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

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<int*>(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.

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! };

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

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

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 »: Point inverse() const; 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.

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!

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é: À 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. 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!)

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.

« 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   <<compte << endl; variable_normale++; cout << variable_normale << endl; // Ceci affiche toujours 1 }

« 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é)

« static » sur une fonction membre ou variable membre

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++) cout << x[i] << endl; } int main() { float x[5] = { 4.3f, 2.2f, 6.4f, 10.5f, -3.5f }; afficher(x,5); return 0; }

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; }

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.