INFO-F-105Langages 1
Gilles GeeraertsAnnée académique 2009-2010
Introduction
Organisation
• Enseignant: Gilles Geeraerts (suppl. Yves Roggeman), bureau 2N8.117, tel. 5596, email [email protected]
• Assistants:
• Jérome Dossogne
• Vincent Ho
• Catharina Olsen
Objectifs
• Les types
• La visibilité
• La surcharge
• Les classes et les objets
• Les constructeurs et destructeurs
• Les opérateurs
• Les expressions
• Les conversions
• Les objets temporaires
Maîtriser des aspects de base du C++:
Objectifs• Pour chacun de ces concepts, on attendra
de l’étudiant:
• La connaissance du concept: être capable de définir...
• La compréhension du concept: pouvoir expliquer le fonctionnement, prévoir l’effet de code...
• La capacité d’application: savoir dans quels cas le concept est utile et savoir l’utiliser à bon escient...
Objectifs
• En plus, les étudiants devront faire preuve d’une maîtrise suffisante des outils nécessaires pour appliquer les concepts vus au cours:
• Maîtriser le compilateur et ses options
• Être capable de comprendre les messages d’erreur du compilateur
• Être capable de coder «proprement»
Moyens
• 6 Séances de cours théorique le lundi de 14h à 16h.
• Présentation des concepts
• Exemples et démonstrations
• 6 séances de TP le lundi de 16h à 18h.
• Certains TPs sur machine
• Mise en pratique à l’aide de la matière d’Algo 1
Evaluation
• L’évaluation me permet de m’assurer que vous avez atteint les objectifs
• Evaluation = Projet d’année + Défense orale (examen)
• Le projet met en pratique les concepts étudiés
• Il faut être capable d’expliquer les concepts et de motiver ses choix lors de la défense
Plan du cours
• Chap 0: Le compilateur et la mémoire
• Chap. 1: Types et déclarations
• Chap. 2 : Expressions et fonctions
• Chap. 3 : Fichiers source et programmes
• Chap. 4 : Classes et objets
• Chap. 5 : Opérateurs
• Chap. 6 : Usage du C++, et la STL
Livre de référence
Plan du cours vs.
• Chap. 1: Types et déclarations (= chap 4&5)
• Chap. 2 : Expressions et fonctions (= chap 6&7)
• Chap. 3 : Fichiers source et programmes (= chap 9)
• Chap. 4 : Classes et objets (= chap 10)
• Chap. 5 : Opérateurs (= chap 11)
Pourquoi C++ ?
• Bref historique:
• En 1972, Kernigan et Richie développent C pour leur nouvel OS: UNIX
• Le langage C a énormément de succès, grâce au succès de UNIX.
Pourquoi C++ ?• Bref historique:
• En 1979, Bjarne Stroustrup étend C pour qu’il supporte la notion de classe.
• Ce langage s’appelle «C with classes»
• En 1983, le langage est renommé C++
Pourquoi C++ ?• Bref historique:
• Le langage est progressivement standardisé, un premier standard officiel est publié en 1998
• Ce standard a été corrigé en 2003
• Aujourd’hui, c’est un des langages les plus populaires notamment dans l’industrie
Avantages du C++
• Il combine C et l’utilisation des classes (orienté objet):
• Le C est bas niveau, et très souple
• L’orienté objet permet de programmer de manière plus structurée (cfr. la suite...)
Désavantages du C++
• Il combine C et l’utilisation des classes (orienté objet):
• L’ensemble est parfois «trop» souple, et ne met pas de garde-fous pour le programmeur.
Chapitre 0Compilateur et
mémoire
Le compilateur
• Pour bien comprendre certaines caractéristiques du langage, il faut comprendre comment fonctionne le compilateur.
• Le plus important est de comprendre comment le compilateur gère la mémoire.
• Cfr. cours d’Architecture 1 !
La mémoire
• Rappel: la mémoire d’un ordinateur est divisé en «cases» appelées cellules mémoire, et contenant chacune 1 byte.
• Le compilateur va donc devoir gérer cette mémoire pour y stocker les variables nécessaires à l’exécution du programme.
La mémoire
• Premier constat: le compilateur ne peut pas toujours savoir quelle quantité de mémoire, quelles variables, etc seront nécessaires pour l’exécution
Exemple: mémoire dynamique
int main() {int n ;int * p ;cin >> n ;for (int i =0; i<n; ++i){...p = new int ;...
}return 0 ;
}
On va créer un nombre de «int» qui sera spécifié à
l’exécution
Exemple: récursivité
int facto(int i) {int r ;if (i != 0)r = i*facto(i-1) ;
elser = 1 ;
return r ;}
Le nombre de «r» nécessaire va dépendre du nombre d’appels récursifs
Modèle mémoire
• On voit qu’il y a deux types de zones mémoire:
• La mémoire dynamique, obtenue explicitement à l’aide d’un new
• La mémoire statique, qui contient le contenu des variables
Modèle mémoire
CodeHeapStack
Mémoire allouée au programme
Variables statiques
Mémoire dynamique
Code compilé
Utilisation des zones mémoire
• Les variables déclarées dans les blocs du code sont mises sur le stack
• Le heap stocke les zones mémoires allouées grâce à new
Le compilateur
• Lors de la compilation, le compilateur peut vous donner deux types de messages:
• Des erreurs: le compilateur a rencontré un problème bloquant. Il ne peut pas continuer son travail et ne produit rien.
• Des avertissements (warning): le compilateur a détecté quelque chose qui lui semble étrange mais qui ne l’empêche pas de produire un exécutable.
Le compilateur
• Règle n° 1: le compilateur est votre ami !
• Mauvaise attitude: corriger un à un les messages d’erreur jusqu’à ce que ça compile... Ouf, ne regardons pas les autres messages !
• Bonne attitude: tenter de comprendre tous les messages (y compris les warning) et les corriger.
Options du compilateur• Dans cet esprit, il vaut mieux demander au
compilateur de produire un maximum d’avertissements:
• Options de base:
• -Wall: donne (presque) tous les warnings
• -pedantic: respecte la norme iso
• -std=c++98: spécifie qu’il faut utiliser le standard iso C++ 1998.
Options du compilateur• Dans cet esprit, il vaut mieux demander au
compilateur de produire un maximum d’avertissements:
• Autres options:
• -Weffc++: produit des warnings si le code viole des règles du livre «Effective C++», Scott Meyer
• -W...: il existe encore d’autres warnings qui ne sont pas activés par -Wall. Voir la documentation.
Chapitre 1Types et déclarations
Types
• Définition: Un type est composé d’un ensemble de valeurs et d’un ensemble d’opérations admises sur ces valeurs.
• En C++, chaque nom (variable, fonction...) doit avoir un type associé
Exemple: int
• Le type int est composé:
• De toutes les valeurs entières signées qu’on peut représenter sur 32 bits: [-2 147 483 648, 2 147 483 647]
• Des opérations arithmétiques sur les entiers: +, -, *, /, ...
• D’opérations de conversion vers/venant d’autres types
• ...
Déclaration
• Comment associe-t-on un type à un nom ?
• En le déclarant !
•int x ; // on associe le type int à x
•int f(double a) ;
Déclaration
• La déclaration a pour effet de faire connaître un nom au compilateur, et d’y associer un type.
Définition
• Contrairement à la déclaration, la définition dit ce à quoi correspond le nom.
• Par exemple: pour une fonction, la définition donne le code de la fonction.
• Il ne faut donc pas confondre déclaration et définition, même si les deux ont parfois lieu en même temps !
• Les deux sont indispensables !
Déclaration vs Définitionint a ;int f(int i) ;int g(double f) ;
int f (int i) {cout << i <<endl ;
}
int g (double f) {cout << f <<endl ;
}
Déclaration vs Définitionint a ;int f(int i) ;int g(double f) ;
int f (int i) {cout << i <<endl ;
}
int g (double f) {cout << f <<endl ;
}
Decl. et Def.
Déclaration vs Définitionint a ;int f(int i) ;int g(double f) ;
int f (int i) {cout << i <<endl ;
}
int g (double f) {cout << f <<endl ;
}
Decl. et Def.Decl.
Déclaration vs Définitionint a ;int f(int i) ;int g(double f) ;
int f (int i) {cout << i <<endl ;
}
int g (double f) {cout << f <<endl ;
}
Decl. et Def.Decl.Decl.
Déclaration vs Définitionint a ;int f(int i) ;int g(double f) ;
int f (int i) {cout << i <<endl ;
}
int g (double f) {cout << f <<endl ;
}
Decl. et Def.Decl.Decl.
Def.
Déclaration vs Définitionint a ;int f(int i) ;int g(double f) ;
int f (int i) {cout << i <<endl ;
}
int g (double f) {cout << f <<endl ;
}
Decl. et Def.Decl.Decl.
Def.
Def.
Allocation
• Afin de pouvoir stocker les valeurs en mémoire, le compilateur doit allouer (réserver, prévoir) de la mémoire pour les variables
• L’utilisateur peut également allouer de la mémoire à la demande, grâce à l’opérateur new
Allocation
• Différence entre variables (déclarées) et mémoire obtenue par new:
• La mémoire qui correspond à une variable porte un nom (le nom de la variable).
• La mémoire obtenue grâce à new est anonyme (on reçoit une adresse).
Allocation
• Différence entre variables (déclarées) et mémoire obtenue par new:
• Les variables sont stockées sur le stack (cfr. plus tard)
• La mémoire allouée par new provient du heap
Portée
• La validité d’une déclaration ne s’étend pas à toute la durée de vie du programme
• La portion du programme dans laquelle une déclaration est valable s’appelle sa portée
Portée: blocs
• Un programme C++ est composé de plusieurs blocs:
• Le programme lui-même est un bloc
• Tout ce qui est compris entre une { et la } correspondante est un bloc
Blocs: exempleconst int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Blocs: exempleconst int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Blocs: exempleconst int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Blocs: exempleconst int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Blocs: exempleconst int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Blocs: exempleconst int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Blocs: exempleconst int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Portée et blocs
• Règle générale: la portée d’un nom s’étend de sa déclaration jusqu’à la fin du bloc dans lequel il a été déclaré.
const int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Blocs: exemple
const int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Blocs: exemple
Portée du l
Niveaux de blocs
• Comme les blocs sont imbriqués, on considérera que certains blocs sont de plus haut niveau que d’autres
• Un bloc est de plus haut niveau que les blocs qui le contiennent
Niveaux: exempleconst int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Niveaux: exempleconst int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Niveaux: exempleconst int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Niveaux: exempleconst int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Niveaux: exempleconst int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Niveaux: exemple
Le bloc du if(k>i) est de plus haut
niveau que: le bloc du main,
le bloc du if (f(...)==6)
et le bloc du programme
const int i = 5 ;
int f(int i, int j) {return i*j ;
}
int main() {int k, l ;cin >> k >> l ;if (f(k,l)==6) {if (k>i) { cout << k ; }else { cout << l ; }
}}
Portée et niveaux
• Exception à la règle générale: on peut re-déclarer un nom déjà déclaré à condition de le faire dans un bloc de plus haut niveau
• Dans ce cas, c’est la déclaration de plus haut niveau qui prévaut
• Les déclarations de plus bas niveau sont alors dites «cachées»
const int i = 5 ;
int main() { int j = 6 ; if (j > i) { int i, j ; i = 3 ; j = 4 ; cout << i << " " << j << endl ; } cout << i << " " << j << endl ; }
Exemple
bloc du iconst int i = 5 ;
int main() { int j = 6 ; if (j > i) { int i, j ; i = 3 ; j = 4 ; cout << i << " " << j << endl ; } cout << i << " " << j << endl ; }
Exemple
bloc du i
portée du i
const int i = 5 ;
int main() { int j = 6 ; if (j > i) { int i, j ; i = 3 ; j = 4 ; cout << i << " " << j << endl ; } cout << i << " " << j << endl ; }
Exemple
bloc du i
portée du i
portée du i
const int i = 5 ;
int main() { int j = 6 ; if (j > i) { int i, j ; i = 3 ; j = 4 ; cout << i << " " << j << endl ; } cout << i << " " << j << endl ; }
Exemple
Blocs et portée
• Peut-on forcer l’accès à des variables cachées ?
• Dans un seul cas: accès aux variables globales à l’aide de ::nom
Namespace• Pour contourner l’accès aux variables
cachées, on peut définir soi-même des portées, et leur donner un nom
• C’est ce qu’on appelle un namespace
• ex:namespace nom { int i ; double d ; void f(int i) { ... }}
Namespace
• Comme le namespace est un bloc, les noms qui y sont déclarés ne sont pas visibles à l’extérieur
• Quand une déclaration d a été faite dans un namespace n, on y accède à l’aide de n::d
• ex:namespace n { int i ; }int main() { n::i = 5 ;}
Namespace
• Grâce aux namespace on peut mieux structurer le code:
• En déclarant plusieurs variables qui ont le même nom...
• En déclarant plusieurs fonctions qui ont le même nom...
• ... dans des namespace différents
Directive using
• Si on utilise de manière répétée des noms d’un même namespace, il peut être fatiguant d’avoir à le répéter
• On peut alors utiliser la directive using namespace n ; qui indique au compilateur qu’il faut rechercher les noms dans le namespace n
• Pourquoi appelle-t-on cela une directive et non une instruction ?
Exemple#include <iostream>int main() { int i ; std::cin >> i ; if (i>5) std::cout << 1 << std::endl ; else std::cout << 2 << std::endl ;}
#include <iostream>using namespace std ;int main() { int i ; cin >> i ; if (i>5) cout << 1 << endl ; else cout << 2 << endl ;}
Directive using
• Attention aux ambiguïtés !
• cfr. exemple namespaceambigu.cpp
Variables en mémoire
• L’imbrication des blocs se retrouve dans la gestion mémoire du compilateur:
• Pour chaque variable déclarée, le compilateur alloue de l’espace sur le stack
• Quand on entre dans un bloc: push des nouvelles variables
• Quand on quitte le bloc: pop des variables du bloc
Exemple
int g(int k) {return 3*k ;
}
int f(int j) {return g(j) + 3 ;
}
int main() {int * p = new int ;int l ;*p = 3 ;l = f(*p) ;
}
stack heap
Code
Exemplestack heap
int g(int k) {return 3*k ;
}
int f(int j) {return g(j) + 3 ;
}
int main() {int * p = new int ;int l ;*p = 3 ;l = f(*p) ;
}
Codep
Exemplestack heap
int g(int k) {return 3*k ;
}
int f(int j) {return g(j) + 3 ;
}
int main() {int * p = new int ;int l ;*p = 3 ;l = f(*p) ;
}
Codep l
Exemplestack heap
int g(int k) {return 3*k ;
}
int f(int j) {return g(j) + 3 ;
}
int main() {int * p = new int ;int l ;*p = 3 ;l = f(*p) ;
}
Codep3
l
Exemplestack heap
int g(int k) {return 3*k ;
}
int f(int j) {return g(j) + 3 ;
}
int main() {int * p = new int ;int l ;*p = 3 ;l = f(*p) ;
}
Codep3
l
Exemplestack heap
int g(int k) {return 3*k ;
}
int f(int j) {return g(j) + 3 ;
}
int main() {int * p = new int ;int l ;*p = 3 ;l = f(*p) ;
}
Codep3
lj=3
Exemplestack heap
int g(int k) {return 3*k ;
}
int f(int j) {return g(j) + 3 ;
}
int main() {int * p = new int ;int l ;*p = 3 ;l = f(*p) ;
}
Codep3
lj=3
Exemplestack heap
int g(int k) {return 3*k ;
}
int f(int j) {return g(j) + 3 ;
}
int main() {int * p = new int ;int l ;*p = 3 ;l = f(*p) ;
}
Codep3
lj=3
k=3
Exemplestack heap
int g(int k) {return 3*k ;
}
int f(int j) {return g(j) + 3 ;
}
int main() {int * p = new int ;int l ;*p = 3 ;l = f(*p) ;
}
Codep3
lj=3
9
Exemplestack heap
int g(int k) {return 3*k ;
}
int f(int j) {return g(j) + 3 ;
}
int main() {int * p = new int ;int l ;*p = 3 ;l = f(*p) ;
}
Codep3
l 12
Exemplestack heap
int g(int k) {return 3*k ;
}
int f(int j) {return g(j) + 3 ;
}
int main() {int * p = new int ;int l ;*p = 3 ;l = f(*p) ;
}
Codep3l
=12
Types de base
• C++ connaît une série de types de base:
• Les types arithmétiques qui contiennent:
• 3 types entiers:
• Booléens: bool
• Caractères: char
• Entiers: int
• 1 type virgule flottante: float
Types de base
• Le type vide: void
• Des types dérivés construits à partir d’autres types:
• les pointeurs
• les tableaux
• les références
• les classes et les structures
Type bool• Valeurs autorisées: true et false
• Opérations: les opérations logiques et arithmétiques &&, ||, !, +, -, *
• Conversions bool → int:
• true → 1, false → 0
• Conversions int → bool
• ≠ 0 → true, = 0 → false
• Pourquoi ?
Type char
• Valeurs autorisées: n’importe quel caractère supporté par le système
• En général: un char est codé sur 8 bits → 256 valeurs différentes possibles
• Chaque valeur c de type char correspond à un entier int(c)
• Mais les valeurs ne sont pas standardisées !
Type char: constantes
• Il vaut donc mieux utiliser les constantes littérales:
• ‘a’, ‘b’, ‘c’,...
• \n: retour à la ligne
• \t: tabulation
• etc..
Type char
• Opérations autorisées: les opérations logiques et arithmétiques &&, ||, !, +, -, *
• Conversions vers et venant de int : naturelles étant donné que chaque char est un entier.
• Utilité de ces conversions ?
Type int• Il n’y a pas qu’un seul int !
• Il y a 3 formes:
• int: signé
• unsigned int: non-signé
•signed int: signé = int
• et 3 tailles:
•long
•short
• pas de taille spécifiée
Signé ou non-signé ?
• Intérêt de déclarer un entier unsigned:
• On gagne un bit dans la représentation (plus besoin de représenter la négation).
• Attention aux règles de conversion: assigner un nombre négatif à un unsigned ne rend pas ce nombre positif ! Pourquoi ?
Quelle taille ?• Quel est la sémantique exacte de long, short ?
• Dépend de l’implémentation !
• Pour connaître le nombre de bytes occupés en mémoire, on peut utiliser sizeof(type)
• En général:
• short int → 16 bits
• int, long int → 32 bits
Quelle taille ?• Les seuls garanties:
sizeof(short int) ≤ sizeof(int) ≤ sizeof(long int)
sizeof(N) = sizeof(signed N) = sizeof(unsigned N)
• Les signed et unsigned occupent donc la même place en mémoire, mais les unsigned utilisent un bit pour stocker le signe !
Int: Valeurs autorisées
• L’étendue des valeurs que l’on peut stocker dépend bien sûr de la place en mémoire.
• Utiliser numeric_limits<N>::max()numeric_limits<N>::min()pour connaître les max et min du type N.
Int: constantes• Les constantes entières en base 10 sont
représentées telles quelles:
• ex: 123, 456
• En base 8: on préfixe d’un 0
• ex: 0123, 0456
• Donc 123 ≠ 0123
• En base 16: on préfixe d’un 0x
• ex: 0xa23
Int: opérations
• Opérations arithmétiques: +, -, *, /, %
• Opérations logiques: cfr traduction int → bool.
• ex: 1 || 2 = true || true = true = 1
• ex: 0 && 35 = false && true = false = 0
• Attention aux conversions !
Type float
• Comme pour les int, il y a trois tailles de types en virgule flottante:
•float
•double
•long double
Quelle taille utiliser ?• Quelle est la sémantique exacte de float,
double, long double ?
• Dépend de l’implémentation !
• Stroustrup:« Chosing the right precision for a problem where the choice matters requires significant understanding of floating-point computation. If you don’t have that understanding, get advice, take the time to learn or use double and hope for the best... »
Quelle taille utiliser ?
• Valeurs typiques:
• sizeof(float) = 4 bytes
• sizeof(double) = 8 bytes
• sizeof(long double) = 16 bytes
• Pour un long double, les valeurs sont comprises entre 3.3621×10-4932 et 1.18973×10+4932
Virgule flottante: constantes
• Les constantes sont spécifiées dans le format anglo-saxon, avec un point comme séparateur décimal:
• 65.3, 0.234, .3455
• On peut utiliser le format scientifique:
•65.3e-5 ≣ 65,3×10-5
• Par défaut, les constantes sont de type double. On ajoute f pour avoir des constantes float:
•56.3f
Type void• Est considéré comme un type de base.
• Mais est en fait un artefact du langage
• Aucun objet ne peut être void !
• Utilisé pour indiquer qu’une fonction ne retourne rien:
•void f(int a) ;
• Utilisé pour indiquer qu’un pointeur pointe vers un objet de type inconnu:
•void * p ;
Types dérivés
• A partir des types existants, on peut obtenir des types dérivés:
• pointeurs
• tableaux
• constantes
• références
Pointeurs vers des objets
• Etant donné un type T connu, on définit le nouveau type T* comme un «pointeur vers un élément de type T».
• Les valeurs admises pour des variables de type T* sont des adresses mémoire d’éléments de type T.
• Rappel: pour obtenir l’adresse d’un objet x en mémoire, on utilise &x.
Objet pointé
• Etant donné un pointeur p, on peut retrouver l’objet pointé par p grâce à l’opérateur *
int i ;
int * p = &i ; // p pointe i
*p = 5 ; // modifie i
Objet pointé
• Comme on peut modifier l’objet pointé, le compilateur doit connaître son type !
• Cela explique pourquoi il n’y a pas de type «pointeur» générique, mais un type de pointeur par type d’objet pointé.
int i = 5 ; void * p ; p = &i ; *p = 6 ; // Erreur !
Objet pointé
• Comme on peut modifier l’objet pointé, le compilateur doit connaître son type !
• Cela explique pourquoi il n’y a pas de type «pointeur» générique, mais un type de pointeur par type d’objet pointé.
int i = 5 ; void * p ; p = &i ; *p = 6 ; // Erreur ! *((int *) p) = 6 ;
Exempleint i = 5 ;float f = .45 ;int * pi = &i ;float * pf = &f ;cout << *pi << " " << *pf << endl ;
pi = &f ;pf = &i ;cout << *pi << " " << *pf << endl ;
Erreur de compilation
Pointeurs
• Le mécanisme de pointeur est très puissant.
• Il permet potentiellement d’accéder à n’importe quelle partie de la mémoire.
• exemple:...
Valeur NULL
• C++ supporte la valeur de pointeur NULL, qui est une constante symbolique représentant 0
• La zone d’adresse 0 ne sera jamais accessible par un programme utilisateur
• La valeur NULL sert donc généralement à représenter une valeur d’erreur
Pointeurs vers des fonctions
• On peut aussi définir des pointeurs vers des fonctions
• Tout comme un pointeur vers un objet permet d’indiquer à une fonction une zone de mémoire à traiter, un pointeur vers une fonction permet d’indiquer à une autre fonction un traitement à appliquer
• cfr. Chapitre 2
Tableaux
• Etant donné un type T, T[n] est un tableau de n éléments de type T, contigus en mémoire.
• Les éléments sont numérotés de 0 à n-1. Pourquoi ?
• On accède à l’élément numéro i grâce à l’expressions T[i]
Tableaux
• Le nombre d’éléments d’un tableau doit être une constante. Pourquoi ?
• exemple: T[5] est autorisé, mais pas T[i] si i est une variable int.
• On peut avoir des tableaux à plusieurs dimensions:
• T[10][20], V[2][4][30],...
Initialisation• Il est possible d’initialiser un tableau à une
série de valeurs fixées grâce à la notation {..., ..., ...}
• ex: int V[3] = {1, 2, 3} ;
• On n’est pas obligé de spécifier toutes les valeurs, le compilateur suppose alors 0:
• ex: int V[5] = {1, 2, 3} ; donne V[3] = V[4] = 0
• N’est permis qu’à l’initialisation !
Tableaux et pointeurs
• Le nom d’un tableau peut être assigné à un pointeur: int T[5] ;T[0] = 1 ;int * p = T ;cout << *p ; // Affiche 1
• Il y a une conversion implicite
• Le pointeur p pointe vers le 1er élément du tableau
Tableaux et pointeurs
• Attention ! Un tableau n’est pas un pointeur
• La preuve: on ne peut pas écrireint T[] = {1,2,3,4,5} ;int V[] = {5,4,3,2,1} ;int * p = T ;int * q = V ;q = p ; // Assign. à un pteurV = T ; // Assign. à un tab.
Chaînes et tableaux
• Une chaîne de caractères est spécifiée entre doubles guillemets:
• ex: ‶Hello world‶
• Une chaîne de caractère contient toujours un caractère additionnel: \0
Chaînes de caractères
• Une chaîne de caractère peut être stockée dans un tableau de caractères:
• ex: char[] T = ‶Hello world‶
• Ainsi, la chaîne peut être modifiée:
• ex: T[6] = ‘W’ ;
• ce qui n’est pas possible avec:char * p = ‶Hello world‶ ;p[6] = ‘W’ ;
Constantes
• Etant donné un type T, on définit un nouveau type const T
• const T a les mêmes caractéristiques que T sauf que les objets de type const T ne peuvent pas être modifiés
• Il faut donc initialiser l’objet lors de la déclaration !
Constantes: utilité
• Permet de définir des constantes symboliques dans le code:
• cela améliore la lisibilité
• cela améliore la maintenance
• ex:const float pi = 3.14159265 ;// Taille max des tableaux:const int tailleMax = 50 ;
Pointeurs et constantes
• Quand on manipule un pointeur, deux objets sont potentiellement accessibles:
• le pointeur lui-même
• l’objet pointé
• On aimerait donc pouvoir spécifier lesquels peuvent être modifiés !
Pointeurs et constantes
• On peut modifier le pointeur mais pas l’objet vers lequel il pointe
• const int * p ;se lit: pointeur vers un const int. On ne peut donc pas modifier l’objet pointé.
Pointeurs et constantes
• On veut modifier l’objet mais pas le pointeur:
•int * const p ;se lit: pointeur const vers un int.
•On ne veut modifier ni l’un ni l’autre:
•const int * const p ;se lit: pointeur const vers un const int.
Références
• Etant donné un type T, on définit le nouveau type T&, comme le type «référence vers T»
• Attention, à ne pas confondre le & des références et l’opérateur & qui renvoie l’adresse !
Références• Un référence doit être vue comme un nom
alternatif pour un objet
• La référence se comporte donc comme l’objet (c’est l’objet !)
• L’objet référencé est spécifié à l’initialisation et ne change jamais
• ex:int i ;int &j = i ; j et i sont deux noms qui réfèrent la même variable
Références
• Attention !
• Une référence n’est pas un pointeur (même si ça y ressemble)
• Une référence n’est pas un nouvelle variable ! La référence a la même adresse que l’objet référé !
Références: usage
• Avec une fonction: passer les paramètres par référence a deux avantages:
• Si la référence n’est pas const, la fonction peut modifier la variable de l’appelant passée en paramètre.
• Aucune copie n’est effectuée: utile avec les gros objets
Références: usage
stack
void f(int j) {j++ ;
}
int main() {int i = 5 ;f(i) ;cout << i << endl ;return 0 ;
}i = 5
Références: usage
stack
void f(int j) {j++ ;
}
int main() {int i = 5 ;f(i) ;cout << i << endl ;return 0 ;
}i = 5
Références: usage
stack
void f(int j) {j++ ;
}
int main() {int i = 5 ;f(i) ;cout << i << endl ;return 0 ;
}i = 5j = 5
Références: usage
stack
void f(int j) {j++ ;
}
int main() {int i = 5 ;f(i) ;cout << i << endl ;return 0 ;
}i = 5j = 6
Références: usage
stack
void f(int j) {j++ ;
}
int main() {int i = 5 ;f(i) ;cout << i << endl ;return 0 ;
}i = 5
Références: usage
stack
void f(int j) {j++ ;
}
int main() {int i = 5 ;f(i) ;cout << i << endl ;return 0 ;
}i = 5
5
Références: usage
stack
void f(int &j) {j++ ;
}
int main() {int i = 5 ;f(i) ;cout << i << endl ;return 0 ;
}i = 5
Références: usage
stack
void f(int &j) {j++ ;
}
int main() {int i = 5 ;f(i) ;cout << i << endl ;return 0 ;
}i = 5
Références: usage
stack
void f(int &j) {j++ ;
}
int main() {int i = 5 ;f(i) ;cout << i << endl ;return 0 ;
}i = 5
j=
Pas de copie !
Références: usage
stack
void f(int &j) {j++ ;
}
int main() {int i = 5 ;f(i) ;cout << i << endl ;return 0 ;
}i = 6
j=
Références: usage
stack
void f(int &j) {j++ ;
}
int main() {int i = 5 ;f(i) ;cout << i << endl ;return 0 ;
}i = 6
Références: usage
stack
void f(int &j) {j++ ;
}
int main() {int i = 5 ;f(i) ;cout << i << endl ;return 0 ;
}i = 6
6
Les struct
• Une struct est un agrégat d’éléments de types différents, arbitraires
• contrairement à un tableau, qui est un agrégat d’éléments du même type
• Une struct permet donc de grouper, dans un même objet, des informations de types différents
Les struct
• Ce mécanisme permet donc au programmeur de définir ses propres types
• En conséquence, l’utilisation d’une struct passera nécessairement par deux étapes:
• 1) La définition du nouveau type
• 2) l’usage de ce nouveau type en déclarant des variables / allouant de la mémoire.
struct: déclaration
struct etudiant {char nom[50] ;char prenom[50] ;int matricule ;
} ;
Définit un nouveau typeappelé struct etudiant
ou etudiantcontenant 3 informations:
nomprenom
matricule
struct: utilisation• Une fois le type défini, on peut déclarer des
variables de ce type:etudiant e ;
• La variable e contient des zones de mémoire qui correspondent à la définition de la struct: 2 tableaux de char et un entier.
• Ces zones sont appelées des champs.
• Les champs ont un nom: matricule, prenom,...
struct: accès aux champs
• Supposons un type struct s, qui contient un champ appelé c
• Si v est le nom d’un objet de ce type, alors v.c est le nom de la zone mémoire du champ c contenu dans v.
• Si p est un pointeur vers un objet de ce type, on accède à c:
• soit par (*p).c
• soit par p->c
Déclaration et définition
• On peut séparer déclaration et définition d’une struct:struct S ; // déclaration
• Mais lorsqu’on utilise la struct, la définition doit avoir eu lieu
• Le compilateur doit savoir quelle place mémoire allouer pour les éléments !
Typedef
• Par souci de facilité de lecture , on veut parfois «renommer» un type
• Cela peut se faire à l’aide de typedef ancienNom nouveauNom
• ex:typedef int typeInfo ;typeInfo est maintenant équivalent à int
Chapitre 2Expressions et
fonctions
Opérateurs
• Le C++ connaît énormément d’opérateurs:
• +, -, *, /, %
• <, >, <=, >=
• <<, >>
• ++, --, typeid, sizeof, [], new, delete
• ...
Opérateurs
• Tableau des opérateurs: voir livre de référence, page 120
Priorité et associativité• Comme en mathématique les opérateurs
ont une priorité:
• ex: a+b*c équivaut a + (b*c)
• L’associativité est à droite pour les opérateurs unaires et l’assignation, et à gauche sinon
• ex: a+b+c équivaut (a+b)+c
• ex: *p++ équivaut *(p++)
• ex: a=b=c équivaut a=(b=c)
Type du résultat
• Quel est le type du résultat d’une expression arithmétique ?
• Cela dépend des types qui y apparaissent
• Règles générale: on va toujours vers le plus précis
• ex:long int i ;int j ;... = i+j ; // i+j est long int
L-value
• Définition: une l-value est une valeur qui a une adresse en mémoire
• exemples:
• ++x renvoie la nouvelle valeur de x. Cette valeur est donc stockée dans x, et a donc une adresse (celle de x)
• x++ renvoie l’ancienne valeur de x. Cette valeur n’est plus stockée en mémoire. Ce n’est pas une l-value.
Type de retour• De manière générale, les opérateurs qui
reçoivent une l-value renvoient une l-value. Cela permet de combiner les opérateurs
• exemples:
• x = y ; renvoie la nouvelle valeur de x. On peut donc écrire z = x = y ; La valeur de y sera d’abord copiée dans x, puis dans z.
• int *p = &++x ; fait pointer p vers x
• int * p = &x++ ; n’est pas correct.
Dépassements
• Aucune garantie n’est offerte quant au dépassement de capacité.
• Ceux-ci ne sont pas détectés
• Les effets ne sont pas garantis
• exemple: qu’imprime ?int i = 1;while (i > 0) i++ ;cout << i ;
Ordre d’évaluation
• L’ordre dans lequel les composants d’une expressions sont évalués n’est pas garanti !
• Exemple: on ne sait pas si f(1) sera évalué avant g(2) ou pas dans:int x = f(1) + g(2) ;
• Exemple: que fait ?int i = 1;V[i] = i++ ;
Ordre d’évaluation
• Il y a heureusement des exceptions: && et || évaluent de gauche à droite
• Utile dans un cas comme:while (p != NULL && p->info != i) {...}
Car si p=NULL et qu’on évalue d’abord p->info, on a une erreur de segmentation
Opérateurs && et ||
• De plus, ces opérateurs sont évalués de façon «paresseuse»:
• Dans e1 && e2, on évalue d’abord e1, et puis e2 seulement si e1 est vrai
• Dans e1 || e2, on évalue d’abord e1, et puis e2 seulement si e1 est faux
Incrément et décrément
• Le C++ connaît les opérateurs ++ et -- pour incrémenter et décrémenter
• Il en existe deux versions:
• Une version préfixe ++x, --x, qui renvoie la nouvelle valeur
• Une version postfixe x++, x--, qui renvoie l’ancienne valeur (et donc pas une l-value)
++ et -- sur les pointeurs
• Quand on applique ces opérateurs sur un pointeur T * p cela a pour effet d’incrémenter le pointeur de sizeof(T).
• Utilité: parcours de taleaux !
Exercice...
• Que fait le code ci-dessous ?
char S[10] = «Hello !» ;char T[10] ;char * p = S, *q = T ;while (*q++ = *p++) ;
new et delete
• new T alloue sur le heap un objet de type T et retourne son adresse
• delete p, où p est un pointeur, libère l’espace mémoire pointé par p, à condition que cet espace ait été alloué par new
Utilité de delete
int main() {int * p, *q;p = new int ;q = new int ;p = q ;return 0 ;
}
stack heap
Codep q
Utilité de delete
int main() {int * p, *q;p = new int ;q = new int ;p = q ;return 0 ;
}
stack heap
Codep q
Utilité de delete
int main() {int * p, *q;p = new int ;q = new int ;p = q ;return 0 ;
}
stack heap
Codep q
Utilité de delete
int main() {int * p, *q;p = new int ;q = new int ;p = q ;return 0 ;
}
stack heap
Codep q
Utilité de delete
int main() {int * p, *q;p = new int ;q = new int ;p = q ;return 0 ;
}
stack heap
Codep q
La zone initialement pointée par p n’est plus
accessible par le programme mais reste
«réservée». Elle est donc perdue.
Utilité de delete
int main() {int * p, *q;p = new int ;q = new int ;delete p ;p = q ;return 0 ;
}
stack heap
Codep q
Utilité de delete
int main() {int * p, *q;p = new int ;q = new int ;delete p ;p = q ;return 0 ;
}
stack heap
Codep q
delete
• Attention ! delete ne modifie pas le pointeur.
• delete p libère la zone mémoire pointée par p...
• ...mais le pointeur p contient toujours l’adresse de cette zone, qu’on ne peut plus utiliser
• ne pas déréférencer p après un delete !
new[] et delete[]
• new T[n] crée un tableau de taille n d’objets de type T, et renvoie l’adresse du premier élément du tableau
• De manière symétrique delete[] peut être utiliser pour libérer la mémoire occupée par un tableau alloué à l’aide de new[]
Fonctions
• Rappel: on peut séparer les déclaration et définition d’une fonction. Les deux sont nécessaires mais la définition doit être unique
• Rappel: prototype d’une fonction =type retour f(type arg1, type arg2,...)
Variables statiques
• Rappel: par défaut les variables déclarées dans une fonction sont détruites quand on quitte la fonction (elles sont allouées sur le stack)
• Peut-on contourner ce comportement ?
Variables statiques
• Par exemple, dans la fonction:void f(int x) { int i = 0 ; i+=x ; cout << i ;}
• On aimerait que i ne soit pas réinitialisée à chaque entrée dans la fonction, mais qu’elle stocke la somme des x
Variables statiques
• Solutionvoid f(int x) { static int i = 0 ; i+=x ; cout << i ;}
• En déclarant la variable static, l’initialisation ne sera effectuée qu’une seule fois !
Arguments
• Rappel: par défaut, quand on passe une valeur à une fonction, une copie de la valeur est effectuée dans une variable locale de la fonction.
• Ce comportement peut être contourné en utilisant des pointeurs ou des références
Arguments
• ex:void swap(int i, int j) { int c = i ; i = j ; j = c ;}
• Ne va pas avoir le comportement attendu car on manipule des copies locales
Arguments
• ex:void swap(int &i, int &j) { int c = i ; i = j ; j = c ;}
• Va fonctionner: on modifie les variables de l’appelant
Arguments• ex:void swap(int *i, int *j) { int c = *i ; *i = *j ; *j = c ;}
• Fonctionne aussi, mais est plus lourd: l’utilisateur doit passer des adresses
• Ce sont les adresses qui seront copiées dans i et j, pas les contenus
Arguments const
• Avantage des références: évite la copie
• Inconvénient: la fonction peut modifier la variable de l’appelant !
• Solution: pn peut aussi déclarer les arguments const, quand il s’agit de pointeurs ou de références
• Cela permet d’interdire à la fonction de modifier les objets pointés ou référencés (tout en évitant la copie qui est lourde)
Arguments const• ex:int strlen (const char* S) { int i = 0 ; while (S[i]) i++ ; return i ;}Cette fonction reçoit un pointeur vers un const, ce qui lui permet de consulter l’objet, mais pas de le modifier
• A utiliser absolument pour éviter les erreurs !
Arguments: tableaux
• Quand un tableau est passé à une fonction, la fonction reçoit un pointeur vers le premier élement
• Conséquences:
• La fonction peut modifier le tableau de l’appelant (les tableaux sont toujours passés par référence)
• La fonction ne connaît pas la taille du tableau
Arguments: tableaux
• Quand un tableau est passé à une fonction, la fonction reçoit un pointeur vers le premier élement
• Conséquences:
• Quand on passe un tableau à une dimension, on ne doit pas spécifier la taille dans le type du tableau
• ex:void f(int M[]) {...}
Arguments: tableaux• Quand un tableau est passé à une fonction,
la fonction reçoit un pointeur vers le premier élement
• Conséquences:
• Quand on passe un tableau à plusieurs dimensions, la taille de la première coordonnée peut être omise
• ex:void f(int M[][n]) {...}Pourquoi ?
Valeurs de retour
• La sémantique du return est de
• créer une nouvelle variable anonyme, du type de retour de la fonction
• y stocker la valeur à retourner
• passer cette variable à la fonction appelante
Valeurs de retourint f(int i) { int k ; k = i +1 ; return k ;}
int main(void) { int l ; l = f(5) ; return 0 ;}
l
Valeurs de retourint f(int i) { int k ; k = i +1 ; return k ;}
int main(void) { int l ; l = f(5) ; return 0 ;}
l
k6
Valeurs de retourint f(int i) { int k ; k = i +1 ; return k ;}
int main(void) { int l ; l = f(5) ; return 0 ;}
l
k6
6
Valeurs de retourint f(int i) { int k ; k = i +1 ; return k ;}
int main(void) { int l ; l = f(5) ; return 0 ;}
l6
k6
6
Quand on quitte la
fonction, k est désallouée
Valeurs de retour
• On ne peut donc pas retourner un pointeur ou une référence vers une variable locale !
• ex:int * f(void) { int i ; return &i ; // pas bien}
Surcharge• Principe général: Le compilateur tolère que
plusieurs fonctions différentes aient le même nom, à condition que les paramètres lui permettent de déterminer laquelle appeler en pratique
• ex:void print(int i) { cout << "Entier " << i ;}void print(char c) { cout << "Caractère " << c ;}
Surcharge• La surcharge peut être pratique, mais peut
donner lieu à des ambiguïtés, si une conversion doit avoir lieu:
• ex:void g(double d) { cout << "Double " << d ;}void g(float fl) { cout << "Float " << fl ;}int main() { g(1) ; }
Surcharge• La surcharge peut être pratique, mais peut
donner lieu à des ambiguïtés, si une conversion doit avoir lieu:
• ex:void g(double d) { cout << "Double " << d ;}void g(float fl) { cout << "Float " << fl ;}int main() { g(1) ; }
Ambigu !Doit-on convertir le
int en double ou en float ?
Surcharge
• Quand le compilateur ne sait «pas décider» il affiche une erreur
• Le mécanisme utilisé par le compilateur pour «prendre sa décision» (pour résoudre la surcharge) est complexe
Mécanisme de résolution• A-t-on une fonction avec le prototype exact ?
• Si non, peut-on utiliser des promotions ?
• bool ☞ int
• char ☞ int
• short ☞ int
• float ☞ double
• double ☞long double
Mécanisme de résolution• Si non, peut-on utiliser des conversion
standards ?
• int ☞ double
• double ☞ int
• int ☞ unsigned int
• Si non peut-on utiliser des conversions définies par l’utilisateur ?
• ...
Ambiguïté
• Les problèmes d'ambiguïté sont plus importants quand on a plusieurs paramètres
• Ce que le mécanisme de résolution peut résoudre pour un paramètre n’est pas toujours gérable pour plusieurs paramètres
Ambiguïté• Exemple:void e(double d, int i) { cout << "double " << d ; cout << "int " << i ;}
void e(int i, double d) { cout << "int " << i cout << "double " << d ;}
int main() { e(1, 1) ; }
Arguments par défaut
• Par moment, on aimerait permettre à l’utilisateur d’appeler une fonction sans devoir toujours donner une valeur à tous ses arguments, car certains sont «habituels»
• ex: une fonction qui affiche un nombre dans une certaine base, 10 par défaut:void f(int v, int base) {...}f(5) ; // affiche en base 10f(5, 16) ; // base 16f(5, 8) ; // base 8
Arguments par défaut
• Solution 1: surcharge:void f(int i) {...}void f(int i, int base) {...}
• Problème: on va dupliquer du code
• Il faudrait pouvoir dire que «10» est une valeur «par défaut» !
• Solution 2:void f(int i, int base=10) {..}
Arguments par défaut
• Pour que cela fonctionne, il faut que le compilateur puisse décider, quand il manque des arguments à l’appel, lesquels correspondent aux valeurs par défaut:
• Règle: seuls les derniers arguments peuvent être «par défaut» et on associe les valeurs aux arguments de gauche à droite
Arguments par défaut
• Exemples:int f(int, int =0, char* =0) ; OKint g(int =0, int =0, char*) ; KOint h(int =0, int, char* =0) ; KO
Arguments par défaut
• Exemple:int f(int i, int j= 0, int j= 0) ;
int main() { f(5) ; // f(5, 0, 0) f(5, 1) ; // f(5, 1, 0) // pas f(5, 0, 1) f(5, 0, 1) ;}
Arguments non spécifiés• Il est également possible de déclarer des
fonctions sans spécifier le nombre de paramètres
• ex: Une fonction qui affiche une chaîne de caractères suivie d’une liste de valeurs, chacune spécifiée comme un paramètreint i = 9 ;f("Liste", 1, i) ;f("Liste", 5, 9, 15, 20) ;f("Liste", 5+2) ;C’est toujours la même fonction qui est appelée !
Arguments non spécifiés
• Déclaration de la fonction:void f(char * n ...)
• Ensuite, on accède aux paramètres effectifs à l’aide de macros spécifiées dans cstdarg (à inclure)
Arguments non spécifiés• Exemple:void f(char * n ...) { va_list arguments ; va_start(arguments, n) ;
int i ; cout << n << " : " ; do { i = va_arg(arguments, int) ; if (i) cout << i << " " ; } while(i) ; cout << endl ;}
Pointeurs vers des fonctions
• On peut aussi définir des pointeurs vers des fonctions
• Tout comme un pointeur vers un objet permet d’indiquer à une fonction une zone de mémoire à traiter, un pointeur vers une fonction permet d’indiquer à une autre fonction un traitement à appliquer
Pointeurs vers des fonctions
• Syntaxe:Tr (*fp)(T1, T2,..., Tn)Déclare un pointeur fp vers une fonction qui retourne un élément de type Tr et prend n arguments de types T1, T2,... Tn
• Exemple:int (*fp)(char *)ne pas confondre avec:int * fp(char *)
Pointeurs vers des fonctions
• Utilisation: Comme un pointeur vers une variable:int f(char * S) {...}
int main() { int (*fp)(char *) ; fp = &f ; // adresse char * V = ... ; (*fp)(V) ; // déréf. return 0 ;}
Pointeurs vers des fonctions
• Mais en pratique, une fonction est un pointeur !
• Une fonction n’est jamais que l’adresse mémoire où se trouve la première instruction du code généré
• On n’est donc pas obligé d’écrire explicitement &f, ni *f
Pointeurs vers des fonctions
• Alternative: int f(char * S) {...}
int main() { int (*fp)(char *) ; fp = f ; // adresse char * V = ... ; fp(V) ; // déréf. return 0 ;}
Pointeurs vers des fonctions
• Exemple de code... voir pointeurfonction.cpp
Fonctions inline
• Par défaut, un appel de fonction donne lieu à un jump dans le code compilé
• Pour des petites fonctions appelées fréquemment, il peut être préférable d’éviter le jump, et de demander au compilateur de d’insérer le code de la fonction à l’endroit de l’appel
Fonctions inline
• Cela s’obtient en déclarant la fonction inline
• Dans ce cas, la déclaration et la définition doivent se faire au même endroit
• Exemple:inline int vabs(int i) { if (i<0) i = -i ; return i ;}
Chapitre 3Fichiers source et
programme
Compilation séparée
• Faire tenir tout un programme dans un seul fichier est en général difficile voire impossible (trop long...)
• Il est donc préférable de séparer le code source en plusieurs fichiers
Compilation séparée
• Cela a plusieurs avantages:
• Le code est plus facilement lisible
• Si la découpe en fichiers est bien faite (un «module» du programme par fichier), le code est plus facilement réutilisable
• On peut espérer n’avoir à recompiler qu’un seul fichier (et donc un seul module) à chaque modification
Compilation séparée
• Cela pose naturellement des difficultés au compilateur:
• On a la possibilité d’appeler dans un fichier X une fonction qui est définie dans un fichier Y et qui manipule un type défini dans un fichier Z...
• le compilateur doit donc «recoller les morceaux»
Rappel: compilation
• Le processus de compilation comporte en général deux étapes principales:
• la génération du code compilé pour chaque «partie» du code source
• l’édition des liens pour réaliser les liens nécessaires entre ces parties
Compilation: étape 1int g(int) ;
int f(int y) { int z = g(y) ;}
int g(int x) { ...}
int main(void) { int r = f(3) ;}
Compilation: étape 1int g(int) ;
int f(int y) { int z = g(y) ;}
int g(int x) { ...}
int main(void) { int r = f(3) ;}
code compilé de f
Compilation: étape 1int g(int) ;
int f(int y) { int z = g(y) ;}
int g(int x) { ...}
int main(void) { int r = f(3) ;}
code compilé de g
code compilé de f
Compilation: étape 1int g(int) ;
int f(int y) { int z = g(y) ;}
int g(int x) { ...}
int main(void) { int r = f(3) ;}
code compilé de g
code compilé de f
code compilé du main
Compilation: étape 1int g(int) ;
int f(int y) { int z = g(y) ;}
int g(int x) { ...}
int main(void) { int r = f(3) ;}
code compilé de g
code compilé de f
code compilé du main
f
g
main
Compilation: étape 2int g(int) ;
int f(int y) { int z = g(y) ;}
int g(int x) { ...}
int main(void) { int r = f(3) ;}
code compilé de g
code compilé de f
code compilé du main
f
g
main
g(...)
f(...)
Compilation: étape 2int g(int) ;
int f(int y) { int z = g(y) ;}
int g(int x) { ...}
int main(void) { int r = f(3) ;}
code compilé de g
code compilé de f
code compilé du main
f
g
main
g(...)
f(...)
La table construite à l’étape 2 est la table
des symboles
Compilation: étape 2int g(int) ;
int f(int y) { int z = g(y) ;}
int g(int x) { ...}
int main(void) { int r = f(3) ;}
code compilé de g
code compilé de f
code compilé du main
f
g
main
g(...)
f(...)
La table construite à l’étape 2 est la table
des symboles
Chaque symbole qui y apparaît doit être
unique !
Compilation: contraintes• Cette organisation de la compilation impose
des contraintes sur la découpe du code:
• Au moment de l’utilisation d’un nom, celui-ci doit avoir été déclaré, et les types qui interviennent dans sa déclaration doivent avoir été définis
• Cela permet la vérification de types, le calcul de la taille en mémoire, etc
• Par contre, la définition d’une fonction peut suivre sa première utilisation
Compilation: contraintes
• Cette organisation de la compilation impose des contraintes sur la découpe du code:
• On peut déclarer un nom plusieurs fois, à condition que les déclarations soient cohérentes
• Par contre la définition doit être unique
• Attention, dans certains cas, la déclaration a lieu en même temps que la définition !
Résumé
• Déclaration: multiple autorisée mais il faut rester cohérent !
• Définition:
• pour les types: avant la première utilisation et unique par unité de compilation
• pour les fonctions: unique dans tout le programme mais n’importe où
Compilation séparée
• Idée:
• Séparer le code en plusieurs fichiers (extension .cpp)
• Compiler chaque fichier séparément: g++ -c fichier.cppproduit fichier.o
• Réaliser l’édition des liens:g++ -o programme fichier1.o ... fichiern.o
Compilation séparée
• Comment découper ?
• En fonction de la logique du programme !
• Exemple: un programme qui manipule des listes:
• Un fichier pour la définition des listes et les fonctions de manipulation
• Un fichier avec le main qui fait appel aux types et fonctions de l’autre fichier
Fichier .h• Comment va-t-on assurer la
«communication» entre les cpp ?
struct s {int i ;int j ;
} ;
void f(s x) {...}
s.cpp
int main() {s y ;f(y) ;
}
prog.cpp
Fichier .h• Comment va-t-on assurer la
«communication» entre les cpp ?
struct s {int i ;int j ;
} ;
void f(s x) {...}
s.cpp
int main() {s y ;f(y) ;
}
prog.cpp
prog.cpp ne compile pas car s n’est ni
défini ni déclaré et f n’est pas déclarée
Fichier .h• Comment va-t-on assurer la
«communication» entre les cpp ?
struct s {int i ;int j ;
} ;
void f(s x) {...}
s.cppstruct s { int i ;int j ;
} ;void f(s x) ;int main() {s y ;f(y) ;
}
prog.cpp
Fichier .h• Comment va-t-on assurer la
«communication» entre les cpp ?
struct s {int i ;int j ;
} ;
void f(s x) {...}
s.cppstruct s { int i ;int j ;
} ;void f(s x) ;int main() {s y ;f(y) ;
}
prog.cpp
Pas pratique de devoir recopier les
déclarations de s.cpp
Fichier .h
• Solution: créer, pour chaque fichier X.cpp un fichier X.h qui contient:
• Les déclarations des fonctions accessibles à l’utilisateur du module
• Les définition des types utilisés dans le module
• Et demander au compilateur d’insérer cette information au début de X.cpp et de tout .cpp qui utilise X
Fichier .h#include «s.h»
void f(s x) {...}
s.cpp
#include «s.h»int main() {s y ;f(y) ;
}
prog.cpp
struct s {int i ;int j ;
} ;
void f(s x) ;
s.h
#include• En pratique la directive #include se
comporte comme un copier-coller:
• Elle se contente d’insérer le contenu du fichier inclus au point spécifié
• Les directives #include (et autres #...) sont prises en charge par un programme séparé cpp, le pré-processeur, appelé par le compilateur
• Cette étape est réalisée avant toute compilation
Fichier .h#include «s.h»int main() {s y ;f(y) ;
}
prog.cpp
struct s {int i ;int j ;
} ;
void f(s x) ;
s.h
Fichier .h#include «s.h»int main() {s y ;f(y) ;
}
prog.cpp
struct s {int i ;int j ;
} ;
void f(s x) ;
s.h
Fichier .hstruct s {int i ;int j ;
} ;
void f(s x) ;
int main() {s y ;f(y) ;
}
prog.cpp#include «s.h»int main() {s y ;f(y) ;
}
prog.cpp
struct s {int i ;int j ;
} ;
void f(s x) ;
s.h
Fichier .hstruct s {int i ;int j ;
} ;
void f(s x) ;
int main() {s y ;f(y) ;
}
prog.cpp#include «s.h»int main() {s y ;f(y) ;
}
prog.cpp
struct s {int i ;int j ;
} ;
void f(s x) ;
s.h
Ceci compile avec gcc -c
Fichier .h
struct s {int i ;int j ;
} ;
void f(s x) ;
s.h
#include «s.h»
void f(s x) {...}
s.cpp
Fichier .h
struct s {int i ;int j ;
} ;
void f(s x) ;
s.h
#include «s.h»
void f(s x) {...}
s.cpp
Fichier .h
struct s {int i ;int j ;
} ;
void f(s x) ;
void f(s x) {...}
prog.cpp
struct s {int i ;int j ;
} ;
void f(s x) ;
s.h
#include «s.h»
void f(s x) {...}
s.cpp
Fichier .h
struct s {int i ;int j ;
} ;
void f(s x) ;
void f(s x) {...}
prog.cpp
struct s {int i ;int j ;
} ;
void f(s x) ;
s.h
Ceci compile avec g++ -c
#include «s.h»
void f(s x) {...}
s.cpp
Fichier .h
• On peut maintenant réaliser l’édition des liens de prog.o et s.o grâce à g++ -o programme progr.o s.o
• En pratique, le type s aura été défini et compilé deux fois, mais cela ne pose pas de problème car les types ne se retrouvent pas dans le code compilé
• Par contre la définition de f est bien unique et l’édition des liens peut se faire.
#Include multiples• On peut aussi faire des #include dans un .h
• Exemple:
• on définit un type s et les fonctions qui le manipulent dans s.cpp et s.h
• on définit une liste contenant des infos de type s dans liste.cpp et liste.h
• liste.h commence par #include «s.h»
• on utilise la liste dans prog.cpp
•#include «liste.h»
#include multiples
struct s {...};
s.h
#include «s.h»...
liste.h
#include «liste.h»...
prog.cpp
...
??.cpp
...
#include multiples
struct s {...};
s.h
#include «s.h»...
liste.h
#include «liste.h»...
prog.cpp
...
??.cpp
...
#include multiples
struct s {...};
s.h
#include «s.h»...
liste.h
#include «liste.h»...
prog.cpp
...
??.cpp
struct s {...};
...
#include multiples
struct s {...};
s.h
#include «s.h»...
liste.h
#include «liste.h»...
prog.cpp
...
??.cpp
struct s {...};
...
...
#include multiples
struct s {...};
s.h
#include «s.h»...
liste.h
#include «liste.h»...
prog.cpp
...
??.cpp
struct s {...};
...
...
...
#include multiples
• Cela peut néanmoins poser des problèmes:
struct X{...} ;
X.h
#include «X.h»...
Y.h#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
#include multiples
• Cela peut néanmoins poser des problèmes:
struct X{...} ;
X.h
#include «X.h»...
Y.h#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp Ne compile pas car X est définie
2 fois !
#define et #ifndef• La solution consiste à:
• entourer les blocs de définition par des directive #ifndef C ... #endif, qui indique au pré-processeur d’ignorer le code si C est true
• C est une variable du pré-processeur, false par défaut. Elle doit être différente pour chaque bloc !
• utiliser la directive #define C après ou dans le bloc pour remplacer toute occurrence suivante de C par true
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
prog.cpp
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
prog.cpp
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
prog.cpp
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
prog.cpp
OK: _X_H est false !
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
struct X{...} ;
prog.cpp
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
struct X{...} ;
prog.cpp
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
struct X{...} ;
prog.cpp
_X_H est maintenant true
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
struct X{...} ;Suite de Y.h
prog.cpp
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
struct X{...} ;Suite de Y.h
prog.cpp
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
struct X{...} ;Suite de Y.h
prog.cpp
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
struct X{...} ;Suite de Y.h
prog.cpp
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
struct X{...} ;Suite de Y.h
prog.cpp
_X_H est maintenant true
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
struct X{...} ;Suite de Y.h
prog.cpp
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
struct X{...} ;Suite de Y.h
prog.cpp
ne change rien
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
struct X{...} ;Suite de Y.hSuite de Z.h
prog.cpp
#include multiples#ifndef _X_Hstruct X{...} ;#endif#define _X_H
X.h
#include «X.h»...
Y.h
#include «X.h»...
Z.h
#include «Y.h»#include «Z.h»
prog.cpp
struct X{...} ;Suite de Y.hSuite de Z.hSuite de prog.cpp
prog.cpp
Variables globales• Nous avons vu comment éviter les multiples
définitions de types dans un même .cpp, grâce à #ifndef
• Cela évite les problèmes lors de la compilation d’un seul .cpp
• Nous avons vu comment éviter de définir plusieurs fois une même fonction
• On sépare déclaration (dans le .h) et définition (dans le .cpp)
• Evite les problèmes d’édition des liens
Variables globales
• Comment traite-t-on les variables globales ?
• Le même problème que pour les fonctions se pose:
• on a besoin de déclarer le nom de la variable dans chaque .cpp qui l’utilise
• mais on ne peut la définir qu’une seule fois
Variables globales
#include val.h/* ... */
a.cpp
double pi =3.1415 ;
val.h
#include val.h/* ... */
b.cpp
contient le symbole pi
a.ocontient le symbole
pi
b.o
Variables globales
#include val.h/* ... */
a.cpp
double pi =3.1415 ;
val.h
#include val.h/* ... */
b.cpp
contient le symbole pi
a.ocontient le symbole
pi
b.o
Erreur d’édition des liens: pi défini deux fois !
Variables globales
• L’utilisation de #ifndef ne résout rien:
• Le pré-processeur agit avant la compilation, et donc avant l’édition des liens
• Il faut pouvoir séparer déclaration et définition d’une variable
• C’est ce qu’on obtient avec le mot-clef extern
Variables globales
#include val.h/* ... */
a.cpp
extern double pi ;
val.h
#include val.h/* ... */
b.cpp
pas de symbole pi
a.o
pas de symbole pi
b.o
double pi =3.1415 ;
val.cpp
contient le symbole pi
val.o
Variables globales
#include val.h/* ... */
a.cpp
extern double pi ;
val.h
#include val.h/* ... */
b.cpp
pas de symbole pi
a.o
pas de symbole pi
b.o
double pi =3.1415 ;
val.cpp
contient le symbole pi
val.o
Il n’y a plus aucun problème à faire l’édition des liens sur ces trois fichiers
Le symbole pi n’est présent que dans val.o
extern
• Quand une variable est déclarée extern:
• Le compilateur ne réserve pas de place mémoire dans le fichier objet (créé avec gcc -c) mais laisse le symbole «en attente»
• L’éditeur des liens se charge d’établir le lien entre ce symbole en attente et l’endroit où il est défini
Exemple de compilation séparée
• Voir listes/*.cpp
Fin de programme• Par défaut, le programme se termine quand
on atteint la fin du main
• Le type de main est int: il faut donc renvoyer un entier
• Cette valeur peut être récupérée par l’OS (cfr. cours d’archi)
• Conventionnellement, 0 signifie que tout s’est bien passé
• D’autres valeurs peuvent signaler des erreurs
Fin de programme
• On peut aussi quitter le programme à tout moment en appelant:
• void exit(int) qui appelle les destructeurs (cfr. chap 4) pour les objets statiques et renvoie la valeur spécifiée
• void abort(void) qui quitte le programme immédiatement
Paramètres du main
• Quand on appelle un programme en ligne de commande on lui passe souvent des options.
• e.g. : ls -la hello.cpp
• Comment peut-on «récupérer» ces valeurs dans le programme C++ ?
• Chacun des éléments entrés en ligne de commande est appelé un «argument»
ls -la hello.cpp
Paramètres du main• On déclare le main ainsi:
int main(int argc, char * argv[])
• Quand le programme est exécuté:
• argc contient le nombre d’arguments
• Chacune case de argv[i] pointe vers une chaîne de caractères contenant le i+1eme argument
Paramètres du main• On déclare le main ainsi:
int main(int argc, char * argv[])
• Quand le programme est exécuté:
• argc contient le nombre d’arguments
• Chacune case de argv[i] pointe vers une chaîne de caractères contenant le i+1eme argument
ls -la hello.cpp
Paramètres du main• On déclare le main ainsi:
int main(int argc, char * argv[])
• Quand le programme est exécuté:
• argc contient le nombre d’arguments
• Chacune case de argv[i] pointe vers une chaîne de caractères contenant le i+1eme argument
ls -la hello.cppargc=3
Paramètres du main• On déclare le main ainsi:
int main(int argc, char * argv[])
• Quand le programme est exécuté:
• argc contient le nombre d’arguments
• Chacune case de argv[i] pointe vers une chaîne de caractères contenant le i+1eme argument
ls -la hello.cpp
hello.cpp
-lalsargv
argc=3
Chapitre 4Classes et objets
Démarche d’abstraction
• Une grande partie du travail de programmation consiste à établir un lien entre:
• Des données abstraites qui correspondent au monde réel
• Les représentations concrètes de ces données pour l’ordinateur
• Ce travail sera plus ou moins important en fonction du langage choisi
Démarche d’abstraction
• Exemple: on veut écrire un programme pour gérer les inscriptions des étudiants aux cours d’une Faculté
• Données abstraites: les noms et prénoms des étudiants, leurs matricules, la liste des cours choisis...
• Données concrètes: des 0 et de 1 dans la mémoire de l’ordinateur
Démarche d’abstraction
ConcretRéalité
informatique0,1
AbstraitRéalité de
l’êtrehumain
Automatisme Travail du programmeur
Assembleur
Démarche d’abstraction
ConcretRéalité
informatique0,1
AbstraitRéalité de
l’êtrehumain
Automatisme Travail du programmeur
Types de baseint, char, ...
= C, Pascal
Démarche d’abstraction
ConcretRéalité
informatique0,1
AbstraitRéalité de
l’êtrehumain
Automatisme Travail du programmeur
Définition de types utilisateurstruct
= C
Démarche d’abstraction
• Si les struct permettent de créer des nouveaux types en spécifiant leur contenu, elles ne permettent:
• ni de spécifier les opérations pour les manipuler et de restreindre la manipulation des objets de ce type à ces seules opérations
• ni de spécifier des relations entre les types
Démarche d’abstraction
ConcretRéalité
informatique0,1
AbstraitRéalité de
l’êtrehumain
Automatisme Travail du programmeur
Orienté objet:On associe des opérationsaux types de l’utilisateur
On établit des liens entre les types
Exemple
• Supposons qu’on veuille définir un type «date»:
struct Date { int j, m, a ;} ;void init(Date &d, int, int, int) ; void ajout_an(Date &d, int n) ;void ajout_mois(Date &d, int n) ;void ajout_jour(Date &d, int n) ;
Exemple
• Pour établir un lien explicite entre la struct et les opérations, on peut les déclarer dans la struct:
struct Date { int j, m, a ; void init(int, int, int) ; void ajout_an(int n) ; void ajout_mois(int n) ; void ajout_jour(int n) ;} ;
Fonctions membres• Dans ce cas, les fonctions sont appelées des
«fonctions membres»
• On ne doit plus spécifier de paramètre de type Date !void ajout_an(Date & d, int n) { d.a+=n ;}
Fonctions membres• Dans ce cas, les fonctions sont appelées des
«fonctions membres»
• On ne doit plus spécifier de paramètre de type Date !void ajout_an(Date & d, int n) { d.a+=n ;}
Fonctions membres• Dans ce cas, les fonctions sont appelées des
«fonctions membres»
• On ne doit plus spécifier de paramètre de type Date !void ajout_an(Date & d, int n) { d.a+=n ;}void Date::ajout_an(int n) { a+=n ;}
Fonctions membres
• Même si la Date sur laquelle la fonction est appelée est implicite dans la signature de la fonction, il faut toujours spécifier l’objet concerné au moment de l’appel
int main (void) { Date d ; init(d, 1, 1, 2010) ;}
Fonctions membres
• Même si la Date sur laquelle la fonction est appelée est implicite dans la signature de la fonction, il faut toujours spécifier l’objet concerné au moment de l’appel
int main (void) { Date d ; init(d, 1, 1, 2010) ;}
Fonctions membres
• Même si la Date sur laquelle la fonction est appelée est implicite dans la signature de la fonction, il faut toujours spécifier l’objet concerné au moment de l’appel
int main (void) { Date d ; init(d, 1, 1, 2010) ;}int main(void) { Date d ; d.init(1, 1, 2010) ;}
Fonctions membres
• Même si la Date sur laquelle la fonction est appelée est implicite dans la signature de la fonction, il faut toujours spécifier l’objet concerné au moment de l’appel
int main (void) { Date d ; init(d, 1, 1, 2010) ;}int main(void) { Date d ; d.init(1, 1, 2010) ;}
Indique que les champs j, m, a qui apparaissent dans la fonction sont ceux de
l’objet d
Contrôle d’accès• Malheureusement, les struct ne
permettent pas de préciser que seules les opérations membres peuvent modifier les champs
• Une fonction externe à la struct peut très bien corrompre les données qui y sont stockées
• Exemple:void f(Date & d) { d.a = 0 ; // ?? }
Contrôle d’accès• Remarque: ce n’est pas le cas pour les types
de base du langage
• On peut manipuler un float, par exemple, à l’aides des opérations arithmétiques +, -,...
• Mais on n’a jamais accès à sa représentation interne (mantisse, biais, etc) ce qui garantit une certaine cohérence des données
• Comment avoir la même protection pour les types définis par l’utilisateur ?
Classes
• Réponse: en utilisant une classe:
class Date { int j, m, a ;public: void init(int, int, int) ; void ajout_an(int n) ; void ajout_mois(int n) ; void ajout_jour(int n) ;} ;
Classes
• Une classe est un type déclaré par l’utilisateur. Il contient:
• des champs
• des fonctions membres ou méthodes
• d’autres types
• ...
Objets
• Un objet est une instance d’un type (et en particulier d’une classe)
• C’est la réalisation en mémoire du «patron» défini par le type
• Quand le type est une classe, chaque objet possède donc sa propre copie des champs (sauf champs statiques, cfr. plus tard)
Objets
• Comme avec n’importe quel type, on peut créer un objet à partir d’une classe X soit:
• via une déclaration : X o ;
• via un new: X* p = new X ;
• On accède au champ c de l’objet o grâce à o.c
Objets• Une méthode d’une classe ne s’appelle
jamais seule (sauf méthodes statiques)
• On appelle une méthode m() sur un objet o grâce à o.m()
• Dans ce cas, o est un paramètre implicite de la méthode
• Quand on réfère à un champ c de la classe dans le code de m, c’est implicitement la copie de ce champ qui est dans o
Objets et classesclass X{ public: int i ; void inc() { i++ ; }} ;
int main() { X a ; X * p = new X ; a.i = 5; a.inc() ; p->i = 9 ; p->inc() ;}
int i
a
Objets et classesclass X{ public: int i ; void inc() { i++ ; }} ;
int main() { X a ; X * p = new X ; a.i = 5; a.inc() ; p->i = 9 ; p->inc() ;}
int i
a
int i
p
Objets et classesclass X{ public: int i ; void inc() { i++ ; }} ;
int main() { X a ; X * p = new X ; a.i = 5; a.inc() ; p->i = 9 ; p->inc() ;}
int i
a
int i
p
5
Objets et classesclass X{ public: int i ; void inc() { i++ ; }} ;
int main() { X a ; X * p = new X ; a.i = 5; a.inc() ; p->i = 9 ; p->inc() ;}
int i
a
int i
p
6
Objets et classesclass X{ public: int i ; void inc() { i++ ; }} ;
int main() { X a ; X * p = new X ; a.i = 5; a.inc() ; p->i = 9 ; p->inc() ;}
int i
a
int i
p
6 9
Objets et classesclass X{ public: int i ; void inc() { i++ ; }} ;
int main() { X a ; X * p = new X ; a.i = 5; a.inc() ; p->i = 9 ; p->inc() ;}
int i
a
int i
p
6 10
public, private
• Dans la déclaration d’une classe, il existe des sections publiques et privées
• Elles commencent par public: ou private: et se terminent au début de la section suivante
• Par défaut: private
public, private• Tout ce qui est private est «caché» pour
l’extérieur:
• On ne peut pas modifier les champs private, sauf dans des méthodes de la classe
• On ne peut pas appeler de méthode private à partir d’une méthode en-dehors de la classe
• On en peut pas utiliser de type private en-dehors de la classe
class et struct• Y a-t-il un différence entre class et struct ?
• Pas vraiment: une struct est une classe dans laquelle tout est public par défaut
• alors que tout est privé par défaut dans une classe...
class X { int i,j ; public: void f(void) ;} ;
struct X { private: int i,j ; public: void f(void) ;} ;
=
Classes et namespace
• Une classe est aussi un namespace
• Donc, tout nom n qui se trouve dans une classe C (et auquel on peut accéder depuis l’extérieur) est désigné par C::n
• Exemple:class X { public: int f(void) ;} ;int X::f(void) {...} ;
Méthode inline• Les méthodes, comme toutes les fonctions,
peuvent être inline
• Par défaut:
• celles qui sont définies dans la classe sont inline
• celles qui sont déclarées dans la classe mais définies en-dehors ne le sont pas
• Il faut donc ajouter inline si nécessaire
Constructeurs
• Quand un objet dont le type est une classe est déclaré, on aimerait en général l’initialiser
• eg: void init(int, int, int) ; dans l’exemple
• Néanmoins, on aimerait que cela soit automatique
• Pour éviter que l’utilisateur n’oublie l’appel à l’initialisation
Constructeurs• C’est le but des constructeurs !
• Un constructeur est une méthode qui est appelée automatiquement quand un objet est construit en mémoire
• C’est une méthode
• qui n’a pas de type de retour
• qui a le même nom que la classe
• qui peut avoir des paramètres
• qui peut être surchargée
Constructeurs
• Exemple:class Date { int j, m, a ;public: Date(int, int, int) ; ...} ;
Date::Date(int jj, int mm, int aa) { j=jj ; m=mm; a=aa;}
Constructeurs
• L’appel au constructeur est implicite:
• Quand on déclare un objet du type concerné
• Quand on fait un new
• Pour passer des paramètres au constructeur:
•Type T(paramètres)
•Type T* = new Type(paramètres)
Constructeurs
• On a le droit de surcharger le constructeur
• exemple:Date(int, int, int) ;Date() ; // par défautDate(const char *) ;
Date d1(1, 1, 2010) ;Date * d2 = new Date ;Date d3(«...») ;
Constructeurs
• Si aucun constructeur n’est défini, le compilateur génère un constructeur par défaut.
• Celui-ci se contente d’initialiser chaque champ de la classe en fonction de son type
• Par contre, si l’utilisateur définit au moins un constructeur, le compilateur ne génère plus de constructeur par défaut
Constructeur par défaut
• Exemple:class Date { int j, m, a ;public: // pas de constr. void ajout_an(int n) ; ...} ;
Date d ;
Constructeur par défaut
• Exemple:class Date { int j, m, a ;public: // pas de constr. void ajout_an(int n) ; ...} ;
Date d ;
OK, constructeur par défaut Date() généré
par le compilateur
Constructeur par défaut
• Exemple:class Date { int j, m, a ;public: Date(int, int, int) ; void ajout_an(int n) ; ...} ;
Date d1 ;Date d2(1,1,2010) ;
Constructeur par défaut
• Exemple:class Date { int j, m, a ;public: Date(int, int, int) ; void ajout_an(int n) ; ...} ;
Date d1 ;Date d2(1,1,2010) ;
Pas OK, on appelle Date() qui n’est pas
généré par le compilateur
Constructeur par défaut• Exemple:class Date { int j, m, a ;public: Date() ; Date(int, int, int) ; void ajout_an(int n) ; ...} ;
Date d1 ;Date d2(1, 1, 2010) ;
Constructeur par défaut• Exemple:class Date { int j, m, a ;public: Date() ; Date(int, int, int) ; void ajout_an(int n) ; ...} ;
Date d1 ;Date d2(1, 1, 2010) ;
OK, les bons constructeurs existent
Constructeur par défaut
• Exemple:class Date { int j, m, a ;public: Date() ; Date(int =1, int =1, int =2010) ; void ajout_an(int n) ; ...} ;
Date d1 ;Date d2(1, 1, 2010) ;
Constructeur par défaut
• Exemple:class Date { int j, m, a ;public: Date() ; Date(int =1, int =1, int =2010) ; void ajout_an(int n) ; ...} ;
Date d1 ;Date d2(1, 1, 2010) ;
Pas OK: l’appel à Date() est ambigu
Membres statiques• Une classe peut être vue comme un
«patron» qu’on recopie chaque fois qu’on crée un nouvel objet:
• Les fonctions et types déclarés dans la classe sont communs à tous les objets de la classe
• Les champs sont dupliqués dans chaque objet
• Peut-on avoir un champ qui soit commun à tous les objets de la classe ?
Membres statiques• Réponse: oui, en le déclarant static !
• cfr. variables statiques dans les fonctions
• Exemple:class X { public: static int j ;} ;
int X::j ;
int main() { X::j = 2 ; }
Membres statiques• Il faut prendre garde que la déclaration static int i ; dans la classe ne définit pas l’objet i.
• C’est pourquoi il est encore nécessaire de la définir (comme si c’était un variable globale) en-dehors de la classe:int X::i ;
• Autrement, rien n’est créé en mémoire pour stocker X::i, même si on a créé des objets de type X
Méthodes statiques
• De la même manière, on peut créer des méthodes statiques
• Ce sont des méthodes qui dépendent de la classe et non pas d’un objet en particulier
• Une méthode statique f de la classe C peut donc être appelée en invoquant C::f(), même si aucun objet de type C n’a été créé.
Membres statiques
• Dans notre exemple, cela nous permet de stocker une date par défaut dans la classe, comme membre statique, et d’avoir une méthode statique qui modifie cette valeur par défaut.
Membres statiquesclass Date { int j, m, a ; static Date Date_par_defaut ; public: ... static void defaut(int, int, int) ;} ;
void Date::defaut(int j, int m, int a) { Date::Date_par_defaut.j = j ; Date::Date_par_defaut.m = m ; Date::Date_par_defaut.a = a ;}
Méthodes const
• Rappel: Quand on veut spécifier qu’une fonction ne peut pas modifier un de ses paramètres (par référence, par exemple), on peut utiliser const:
• e.g.: void f(const X& p)reçoit l’objet p de type X passé par référence, mais ne peut pas le modifier
Méthodes const• On peut toujours le faire pour les paramètres
d’une méthode
• Mais comment spécifier que la méthode ne peut pas modifier l’objet courant ?
• Cet objet est maintenant un paramètre implicite !
• En insérant le mot-clef const après le nom de la méthode
• dans la déclaration de la classe
• lors de la définition
Méthodes constclass Date { int j, m, a ; static Date Date_par_defaut ; public: Date(int =0, int =0, int =0) ; void ajout_an(int n) ;... int jour() const {return j ;} int mois() const {return m ;} int an() const {return a ;} void affiche() const ;} ;void Date::affiche() const {...}
Méthodes const
• La déclaration const permet au compilateur de faire deux vérifications:
• On ne peut pas modifier l’objet courant dans une méthode const
• On ne peut pas appeler une méthode m qui n’est pas const sur un objet const O...
• ...et ce même si m ne modifie pas O !
Champs mutable
• Peut-on contourner const, et permettre à une méthode const de modifier malgré tout certains champs ?
• Exemple: on aimerait avoir une méthode qui détermine si l’année d’une date est bissextile.
• ...
Champs mutable• On considère que le calcul est coûteux, et
on veut éviter d’avoir à répéter le calcul.
• On ajoute donc dans la classe un entier b qui peut prendre trois valeurs:
• -1 pour indiquer qu’on ne sait pas si l’année est bissextile: il faut faire le calcul
• 1 pour indiquer que le calcul a été fait et que l’année est bissextile
• 0 pour indiquer que l’année n’est pas bissextile
Champs mutable
• Quand on fera appel à la méthode bool est_bis():
• on renverra b si celui-ci est égal à 0 ou 1
• on ferra le calcul autrement, et on mettra à jour b
• On remettra b à -1 chaque fois qu’on change l’année
Champs mutable• Logiquement, la méthode est_bis()
devrait être const, car elle renvoie une information sur la date et ne devrait donc pas la modifier
• Physiquement, l’objet pourra être modifié (champ b)
• Il faut donc indiquer que est_bis() est const, mais que b peut quand même être modifié
• On déclare donc b mutable
Champs mutable• Exemple:class Date { int j, m, a ; mutable int b ; ... bool est_bis() const ;} ;
bool Date::est_bis() const { if (b == -1) b = (a%4==0)&&(a%100!=0)||(a%400==0) ;
return b ;}
this• Il est parfois nécessaire de connaître l’objet
sur lequel une méthode est appelée
• Exemple: on voudrait pouvoir écrire:d.ajout_an(1).ajout_jour(1) ;pour ajouter un jour et un an à la date d
• Pour ce faire, il faut que d.ajout_an(1) renvoie l’objet d
• Or, actuellement, d.ajout_an(1) est de type void (c’est le type de retour de la méthode)
this
• La méthode ajout_an doit donc renvoyer l’objet sur lequel elle a été appelée
• Mais cet objet n’est pas un paramètre explicite de la méthode
• Solution: dans toute méthode, le pointeur this pointe vers l’objet courant
• Il suffit donc de renvoyer *this
this• Exemple:
class Date { int j, m, a ; static Date Date_par_defaut ; public: Date(int =0, int =0, int =0) ; Date& ajout_an(int n) ; ...} ;
Date& Date::ajout_an(int n) { a += n ; return *this ;}
Type de this
• Si la méthode appartient à la classe X:
• this est de type X* const quand la méthode n’est pas const
• this est de type const X* const quand la méthode est const
Destructeur
• Le destructeur d’une classe X est une méthode ~X() qui est appelée automatiquement quand l’objet est est supprimé de la mémoire:
• soit parce que sa portée s’éteint
• soit parce qu’on a fait appel à delete
• Par défaut, le destructeur libère la place mémoire occupée par l’objet (et pas plus)
Destructeur
• Ce comportement par défaut n’est pas toujours suffisant, car le constructeur a peut-être alloué de la mémoire
• Dans ce cas, la mémoire allouée ne fait pas partie de l’objet et le destructeur ne va pas la désallouer
Destructeur
• Exemple: Dans la classe Date, on ajoute une chaîne de caractères pour le nom du jour qu’on alloue lors de la constructionDate::Date(char nn[], int jj, int mm, int aa) { b = -1 ; nomJour = new char[9] ; if (nn) strcpy(nomJour, nn) ; else strcpy(nomJour,Date_par_defaut.nomJour) ; ...}
Destructeur
• Exemple: on ajoute alors dans la classe un destructeur pour désallouer le tableau:Date::~Date() { delete[] nomJour ;}
Constructeur, destructeur
• La règle générale «un constructeur est appelé quand on crée l’objet et le destructeur est appelé quand on détruit l’objet» doit être précisée
• Suivant la manière dont l’objet est créé/détruit, le comportement n’est pas le même...
• Nous allons préciser ceci dans les transparents qui suivent
Variables locales
• Le constructeur est exécuté chaque fois que l’exécution du programme passe par l’instruction qui déclare la variable
• Le destructeur est exécuté quand on quitte la portée de la variable
• Si plusieurs variables sont déclarées, on les détruit dans l’ordre inverse de leur création
Variables locales
• Exemplevoid f(void) { X a, b ;}
int main() { f() ; f() ;}
Ordre des appels:
Variables locales
• Exemplevoid f(void) { X a, b ;}
int main() { f() ; f() ;}
Ordre des appels:
Constructeur pour aConstructeur pour bDestructeur pour bDestructeur pour a
Constructeur pour aConstructeur pour bDestructeur pour bDestructeur pour a
Variables locales
• Remarque: Quand on utilise exit() pour quitter le programme, les variables locales du main ne sont pas détruites par le destructeur, car «on n’arrive jamais à la fin du main»
Création par copie• On peut créer un objet par copie, avec la
syntaxe:X a = b ;
• Dans ce cas, on ne construira pas a avant d’y copier le contenu de b, mais on appellera un constructeur qui fait les deux étapes en une: le constructeur de copie
• Par défaut, le compilateur génère un constructeur de copie qui fait la copie bit à bit
Copie d’objets
• La sémantique par défaut (dans les deux cas) est la copie bit à bit...
• ... ce qui peut ne pas être l’effet désiréclass X { int * p ;} ;
X a ;a.p = new int ;X b = a ;*(a.p) = 3 ;
int * p
a
Copie d’objets
class X { int * p ;} ;
X a ;a.p = new int ;X b = a ;*(a.p) = 3 ;
int * p
a
• La sémantique par défaut (dans les deux cas) est la copie bit à bit...
• ... ce qui peut ne pas être l’effet désiré
Copie d’objets
class X { int * p ;} ;
X a ;a.p = new int ;X b = a ;*(a.p) = 3 ;
int * p
a
int * p
b
• La sémantique par défaut (dans les deux cas) est la copie bit à bit...
• ... ce qui peut ne pas être l’effet désiré
Copie d’objets
class X { int * p ;} ;
X a ;a.p = new int ;X b = a ;*(a.p) = 3 ;
int * p
a
int * p
b 3
• La sémantique par défaut (dans les deux cas) est la copie bit à bit...
• ... ce qui peut ne pas être l’effet désiré
Copie d’objets
class X { int * p ;} ;
X a ;a.p = new int ;X b = a ;*(a.p) = 3 ;
int * p
a
int * p
b 3
• La sémantique par défaut (dans les deux cas) est la copie bit à bit...
• ... ce qui peut ne pas être l’effet désiré
On modifie donc aussi *(b.p), ce qui n’est peut-
être pas l’effet attendu !
Constructeur de copie
• Heureusement, l’utilisateur peut déclarer son propre constructeur de copie pour modifier ce comportement par défaut
• En pratique, l’instruction:X a = b ;est un «raccourci» pour:X a(b) ;
• Il s’agit donc d’un appel de constructeur, avec un objet de type X comme paramètre
Constructeur de copie
• Pour modifier la sémantique de la construction par copie, il faut donc déclarer un constructeur:X(X i) {...}
Constructeur de copie
• Idéalement il faudrait:
• Passer l’objet de type X par référence
• Le déclarer const (il est seulement consulté)
• Mais ce n’est pas obligatoire !
• On peut donc définir un constructeur de copie tel que X a = b ; modifie b !
Constructeur de copie
• Attention ! Le constructeur de copie n’est appelé que lors de la construction, pas lors d’une assignation «standard»:X a = b ;est différent de:X a ;a = b ;
• Dans ce cas c’est l’opérateur d’assignation qui est appelé (voir plus tard)
Constructeur de copie
• Dans notre exemple de classe Date, le constructeur de copie est nécessaire pour dupliquer le champ nomJourDate::Date(const Date &d) { nomJour = new char[9] ; j = d.j ; m = d.m ; a = d.a ; strcpy(nomJour, d.nomJour) ;}
Conversions
• Ce même mécanisme peut être utilisé pour initialiser un objet à partir d’un objet de type différent:Y a ;X b = a ;Initialise b à l’aide du constructeur X::X(Y)
Conversions
• Dans notre exemple de Date, cela nous permet d’écrire une constructeur Date::Date(const char t[]) qui analyse la chaîne t et en extrait une date dans un format fixé, pour construire l’objet
• On peut alors écrire:Date d = «lundi,22.10.2010» ;
Construction avec new
• Une allocation mémoire réalisée avec new entraîne l’appel du constructeur pour l’objet alloué
• Attention ! Comme les objets créés avec new sont sur le heap et anonymes, il n’y a pas de notion de portée
• Les objets ne sont donc pas détruits automatiquement
• Utiliser delete !
new et delete
• Exemple:int main() { X * p = new X ; X * q = new X(5) ; delete p ;}
• Appelle: X::X(), X::X(int), puis ~X().
Membres de type class
• Considérons une classe X dont certains champs sont eux-mêmes de type Y qui est aussi une classe
• Que se passe-t-il quand on construit un objet de type X ?
• Il faut réserver de la place en mémoire pour le champ de type Y
• Et donc le constructeur de Y sera appelé
Membres de type class• Exemple:
class Y { int i ;public: Y() { i = 0 ;}} ;
class X { int i ; Y y ;public: X() { i = 0; } // Appelle Y::Y()} ;
Membres de type class
• Lors de la construction de l’objet de type X:
• on appelle les constructeurs Y() de chaque membre de type Y
• dans l’ordre des déclarations et
• avant d’exécuter le constructeur de X
Membres de type class
• Les appels aux constructeurs des objets «encapsulés» sont donc tout à fait implicites
• comment peut-on leur passer des paramètres ?
Membres de type class• Exemple:class Y { int i ;public: Y(int j) { i = j; }} ;
class X { int i ; Y y ;public: X(int j) { i = j; } } ;
Membres de type class• Exemple:class Y { int i ;public: Y(int j) { i = j; }} ;
class X { int i ; Y y ;public: X(int j) { i = j; } } ;
On voudrait passer la valeur j au constructeur de Y
Membres de type class
• On peut utiliser la syntaxe suivante:X::X() : c1(v1), c2(v2), ... {...}où c1, c2,... sont les champs que l’on veut construire en passant les valeurs v1, v2,... à leurs constructeurs
Membres de type class• Exemple:class Y { int i ;public: Y(int j) { i = j; }} ;
class X { int i ; Y y ;public: X(int j) : y(j) { i = j; } } ;
Initialisations nécessaires
• Il y a des cas où la syntaxe d’initialisation champ(valeur) doit absolument être utilisée
• C’est le cas pour les champs:
•const
• références
• sans constructeur par défaut
Initialisations nécessairesclass Y { int k ;public: Y(int l) {k=l ;} } ;
class X { const int i ; int &j ; Y y ;public: X(int ii, int jj, Y yy) : i(ii), j(jj), y(yy) { } } ;
Initialisations nécessairesclass Y { int k ;public: Y(int l) {k=l ;} } ;
class X { const int i ; int &j ; Y y ;public: X(int ii, int jj, Y yy) : i(ii), j(jj), y(yy) { } } ;
Que se passe-t-il si on essaye d’éviter les «initialiseurs» ?
Initialisations nécessairesclass Y { int k ;public: Y(int l) {k=l ;} } ;
class X { const int i ; int &j ; Y y ;public: X(int ii, int jj, Y yy) : j(jj), y(yy) { i = ii ; } } ;
Initialisations nécessairesclass Y { int k ;public: Y(int l) {k=l ;} } ;
class X { const int i ; int &j ; Y y ;public: X(int ii, int jj, Y yy) : j(jj), y(yy) { i = ii ; } } ;
Erreur ! Assignation à un const
Initialisations nécessairesclass Y { int k ;public: Y(int l) {k=l ;} } ;
class X { const int i ; int &j ; Y y ;public: X(int ii, int jj, Y yy) : i(ii), y(yy) { j = jj ; } } ;
Initialisations nécessairesclass Y { int k ;public: Y(int l) {k=l ;} } ;
class X { const int i ; int &j ; Y y ;public: X(int ii, int jj, Y yy) : i(ii), y(yy) { j = jj ; } } ;
Erreur ! Assignation à une référence qui n’a pas été
initialisée !
Initialisations nécessairesclass Y { int k ;public: Y(int l) {k=l ;} } ;
class X { const int i ; int &j ; Y y ;public: X(int ii, int jj, Y yy) : i(ii), j(jj) { /* copie de yy dans y */ } } ;
Initialisations nécessairesclass Y { int k ;public: Y(int l) {k=l ;} } ;
class X { const int i ; int &j ; Y y ;public: X(int ii, int jj, Y yy) : i(ii), j(jj) { /* copie de yy dans y */ } } ;
Erreur ! Pour pouvoir faire cela, il faut faire appel à
Y::Y() pour construire y avant d’entrer dans
X::X(). Ce constructeur n’existe
pas !
Tableaux
• Dans un tableau d’éléments de type X, chaque case est initialisée par un appel à X::X()
• On ne peut pas passer d’argument au constructeur
• L’ordre des appels n’est pas spécifié (on peut très bien initialiser la dernière case en premier...)
Variables static
• Pour les variables static, le constructeur n’est exécuté que la première fois qu’on passe par la déclaration de la variable
• Le destructeur est appelé «à la fin du programme»
• «Exactly when is unspecified» Stroustrup.
Objets temporaires
• Quand la valeur d’une expression est calculée, des objets temporaires sont créés pour stocker les résultats des sous-expressions
• Exemple: Dans (a+b)*c on va stocker le contenu de a+b dans un objet temporaire
• Ces objets sont détruits au moment où l’expression complète est évaluée
Appels cachés
• Il faut prendre garde que les constructeurs (et en particulier le constructeur de copie) sont souvent appelés là où on ne les attend pas
• Exemple: dans un appel de fonction avec un passage par valeur, il y a une copie qui a lieu !
Chapitre 5Opérateurs
Opérateurs
• Qu’est-ce qu’un opérateur ?
• Un opérateur est un raccourci syntaxique pour le calcul d’une opération
• e.g.: i+j*5 est un raccourci pour «multiplier j par 5 et ajouter i au résultat»
Opérateurs
• Quels sont les opérateurs du C++ ?
:: . .*
Opérateurs
• Le langage C++ offre toute une série d’opérateurs, définis pour les types de base:
• Exemple: +, -, *, / pour les entiers
• Le compilateur génère également des opérateurs pour les types définis par l’utilisateur:
• Exemple:[] pour n’importe quel type
Opérateurs et fonctions• En C++ les opérateurs sont considérés
comme des raccourcis syntaxiques pour des fonctions
• Ces fonctions ont un nom de la forme operator@ ou @ est l’opérateur
• Par exemple, l’opérateur + sur le type X est un raccourci pour la fonction X operator+(X, X)
• Ceci n’est pas vrai pour les types de base !
Surcharge d’opérateurs
• Comme les opérateurs sont des fonctions, ils peuvent être surchargés
• On peut donc re-définir les sens de (presque) tous les opérateurs quand ils sont appelés sur des objets d’un type défini par l’utilisateur
Surcharge
• Quels opérateurs peut-on surcharger ?
Surcharge
• Quels opérateurs peut-on surcharger ?
On ne peut pas surcharger: :: . .*
Autres restrictions• On ne peut pas définir de nouveaux
opérateurs
• Exemple: définir une fonction operator“(X, X) et appeler a“b n’aura pas l’effet escompté
• On ne peut modifier ni la priorité ni l’associativité des opérateurs
• On ne peut pas surcharger les opérateurs des types de base
• Puisque ce ne sont pas des fonctions....
Opérateurs binaires
• Un opérateur binaire reçoit deux arguments et renvoie une valeur
• Un opérateur binaire @ pour deux objets de type X peut être surchargé:
• soit en définissant une fonction operator@(X,X) non-membre de X
• soit en définissant une méthode non statique X::operator@(X) (le premier paramètre est alors l’objet courant)
Opérateur binaire
• Exemple: Considérons la classe paireclass paire { int x, y ;public: paire(int xx=0, int yy=0) ; ...} ;
• Définissons un opérateur + tel que (x1, y1) + (x2, y2) = (x1+x2, y1+y2)
• Solution 1: dans la classeclass paire { double x, y ;public: paire(double xx=0, double yy=0) ; paire operator+(const paire &s) const ;} ;
paire operator+(const paire &s) const { return paire (x+s.x, y+s.y) ;}
Opérateurs binaires
• Solution 2: hors de la classe:paire operator+(const paire &s1, const paire &s2)
{ return paire(s1.getX()+s2.getX(), s1.getY()+s2.getY()) ;}
Opérateurs binaires
• Solution 2: hors de la classe:paire operator+(const paire &s1, const paire &s2)
{ return paire(s1.getX()+s2.getX(), s1.getY()+s2.getY()) ;}
On a dû ajouter des méthodes getX() et getY() pour consulter le contenu de la classe
qui est private
Opérateurs binaires
• L’opérateur binaire que nous avons défini crée et renvoie un nouvel objet
• Ce n’est pas toujours le cas: par exemple, l’opérateur += devrait logiquement modifier le paramètre de gauche
• Attention ! même si une définition existe pour + et pour = (assignation), le compilateur ne génère pas automatiquement operator+= !
Opérateurs binaires
• Exemple (dans la classe paire):paire& paire::operator+=(const paire &s) { x+=s.x ; y+=s.y; return *this; }
Opérateurs binaires
• Exemple (dans la classe paire):paire& paire::operator+=(const paire &s) { x+=s.x ; y+=s.y; return *this; }
La méthode n’est plus const (on doit pouvoir
modifier l’objet) !
Opérateurs binaires
• Exemple (dans la classe paire):paire& paire::operator+=(const paire &s) { x+=s.x ; y+=s.y; return *this; }
La méthode n’est plus const (on doit pouvoir
modifier l’objet) !
On peut maintenant renvoyer une référence !
Opérateurs binaires
• De la même manière que la définition de + et = n’entraîne pas la définition de +=, on n’obtient pas automatiquement la définition de ++...
• ...alors que pour les entiers, par exemple, ++i est équivalent à i=i+1
• On doit donc aussi pouvoir surcharger les opérateurs unaires
Opérateurs unaires
• Un opérateur unaire ne prend qu’un seul argument
• Par exemple: ++ et --
• Il en existe de deux types: préfixe et postfixe
• par exemple: ++i ou i++
Opérateurs préfixes
• Pour définir un opérateur unaire préfixe @ sur un objet de type X, on a à nouveau deux choix:
• Soit on écrit une méthode non-statique operator@() de la classe X
• Soit on écrit une fonction operator@(X) non-membre de la classe X
Opérateurs postfixes
• Pour définir un opérateur unaire postfixe @ sur un objet de type X, on a à nouveau deux choix:
• Soit on écrit une méthode non-statique operator@(int) de la classe X
• Soit on écrit une fonction operator@(X, int) non-membre de la classe X
Opérateurs postfixes
• Dans le cas des opérateurs postfixes, le paramètre int ne sert à rien dans la fonction
• C’est juste une indication pour le compilateur qu’il s’agit de la version postfixe de l’opérateur
• Quand le paramètre int est absent, il s’agit de la version préfixe
Opérateurs unaires• Exemple: L’incrément ajoute 1 dans chacune
des deux coordonnées de la pairepaire& paire::operator++() { x += 1 ; y += 1 ; return *this ;}paire paire::operator++(int a) { paire p(*this) ; x += 1 ; y += 1 ; return p ;}
Opérateurs unaires
• Remarques sur l’exemple:
• Le retour par référence est nécessaire dans le operator++() pour pouvoir écrire des expressions comme:++++p ;qui doit incrémenter p deux fois
• Que se passe-t-il si on supprime la référence ?
Opérateurs unaires
• Remarques sur l’exemple:
• Le retour par référence par contre est interdit dans le cas de operator++(int)
• Pourquoi ?
Opérandes différentes
• Nous avons vu comment définir des opérateurs sur des objets de même type.
• Exemple: p1 + p2 où p1 et p2 sont des paires.
• On aimerait aussi pour mélanger les types dans les opérations
• Exemple: écrire p + 1 ou p+=1, qui ajoute 1 aux deux coordonnées, et est donc équivalent à p++
Opérandes différentes
• C’est possible, toujours en utilisant le mécanisme de surcharge
• Si on veut pouvoir appliquer @ sur un objet de type X à gauche et un objet de type Y à droite, on peut définir:
• soit operator@(Y) dans la classe X
• soit operator@(X,Y) en-dehors de la classe X
Opérandes différentes
• Remarques:
• Dans ce cas, un des deux types X ou Y (mais pas les deux) peut être un type de base du langage
• Si c’est le type X (à gauche), l’opérateur devra obligatoirement être défini en-dehors d’une classe
Opérandes différentes
• Remarques:
• Le compilateur ne sait pas si l’opérateur est symétrique ou non !
• Si c’est le cas, il faut aussi définiroperator@(Y,X)
Opérandes différentes
• Exemple:paire paire::operator+(int i) const { return paire(x+i, y+i) ;}
paire operator+(int i, const paire &p) { return paire(p.getX()+i, p.getY()+i) ;}
Conversions implicites
• Maintenant que nous pouvons ajouter un int à une paire nous aimerions aussi pouvoir ajouter un double
• Que se passe-t-il si on essaye de compiler:p += 2.5 ;sans ajouter operator+=(double) ?
• Réponse: cela compile sans problème !
Conversion implicites
• Quel est alors l’opérateur qui est appelé ?
• C’est l’opérateur operator+=(int)
• le double est implicitement converti en int...
• ... avec un arrondi !
Initialisation• Il serait également pratique de pouvoir
initialiser une paire à partir d’un scalaire:paire p = 5.5 ;
• Ainsi que de pouvoir assigner un scalaire à une paire:paire p ;/* ... */p = 5.5 ;
• Dans les deux cas, le scalaire doit être copié dans les deux coordonnées
Initialisation
• Pour l’initialisation, il suffit de définir un constructeur qui prend un double comme paramètre:class paire { ...public: paire(double xx=0) : x(xx), y(xx) {} ...} ;
Initialisation
• Que faut-il ajouter pour le second cas ?p = 5.5 ; // sans déclaration
• Rien !
• Pourquoi ?
Initialisation• Quand le compilateur analyse p = 5.5 ; il
utilise le mécanisme classique pour résoudre la surcharge:
• p = 5.5 est interprété comme p.operator=(5.5)
• Il n’existe pas de méthode operator=(double) mais une méthode operator=(paire) générée par défaut
• Il existe un constructeur paire(double) qui permet de convertir un double en paire
Initialisation• Une autre possibilité serait de surcharger
l’opérateur d’assignation:paire&paire::operator=(double d) { x = d ; y = d ; return *this ;}
• Dans ce cas, il aura priorité sur la solution «conversion» car les paramètres sont du bon type.
Initialisationp = 5.5
Initialisationp = 5.5
On recherche soit paire::operator=(double) soit operator=(paire, double)
Initialisationp = 5.5
On recherche soit paire::operator=(double) soit operator=(paire, double)
Avec paire(double) Avec paire(double) et operator=(double)
Initialisationp = 5.5
On recherche soit paire::operator=(double) soit operator=(paire, double)
Avec paire(double) Avec paire(double) et operator=(double)
Candidats:paire::operator=(paire)
Initialisationp = 5.5
On recherche soit paire::operator=(double) soit operator=(paire, double)
Avec paire(double) Avec paire(double) et operator=(double)
Candidats:paire::operator=(paire)
Candidats:paire::operator=(double)paire::operator=(paire)
Initialisationp = 5.5
On recherche soit paire::operator=(double) soit operator=(paire, double)
Avec paire(double) Avec paire(double) et operator=(double)
Candidats:paire::operator=(paire)
Candidats:paire::operator=(double)paire::operator=(paire)
paire::operator=(double)
✔
Initialisationp = 5.5
On recherche soit paire::operator=(double) soit operator=(paire, double)
Avec paire(double) Avec paire(double) et operator=(double)
Candidats:paire::operator=(paire)
Candidats:paire::operator=(double)paire::operator=(paire)
paire::operator=(double)
✔
paire::operator=(paire)avec paire(double)
✔
Conversions
• Cette possibilité de faire des conversions implicites peut être exploitée pour éviter de dupliquer du code.
• Par exemple: operator+=(paire) et operator+=(double) font double emploi si on a une conversion double ☞
paire
Conversions• On peut faire la même chose avec le +
• Au lieu d’avoir:
•operator+(paire, double)
•operator+(double, paire)
•operator+(paire, paire)
• On se contente de
•operator+(paire, paire)
•paire(double)
Conversions
• Cette façon de faire est meilleure car:
• On doit de toute manière définir un constructeur pour convertir les double en paire
• On évite de répliquer du code
Conversion
• Attention ! L’opérateur + doit maintenant être défini en-dehors de la classe paire !
• Pourquoi ?
• Il n’y a pas de conversion implicite effectuée vers un type de l’utilisateur à gauche d’un . ou d’un ->
• On ne peut pas bénéficier de la conversion quand l’opérande de gauche est double
Conversion• En effet:
• Quand on écrit p+2.5, le compilateur essaye p.operator+(2.5) ou operator+(p, 2.5).
• Si operator+(paire) est dans la classe, on peut l’utiliser en utilisant la conversion paire(double)
• Si operator+(paire, paire) existe, on peut aussi l’utiliser en utilisant la conversion
Conversion• Par contre:
• Quand on écrit 2.5+p le compilateur essayeoperator+(2.5, p) et pas «2.5.operator+(p)» (double est un type de base !)
• Donc, la seule solution est d’avoir operator+(paire, paire) en-dehors de la classe et d’utiliser l’opérateur de conversion !
Factorisation de code
• L’utilisation de la conversion nous a permis de «factoriser» du code et d’éviter des doublons
• On peut aller plus loin:
• Pourquoi dupliquer le code dans operator+ et operator+= ?
• Clairement ces deux opérateurs sont liés !
Factorisation de code
• Solution: Quand on calcule p1 + p2:
• Créer une nouvelle paire p3 qu’on initialise à p1 à l’aide du constructeur de copie
• Appeler p3+=p2
• Renvoyer p3
Factorisation de code• Avantages de cette solution:
• On ne duplique pas le code qui calcule l’addition
• Seule la méthode operator+= fait partie de la classe
• C’est plus naturel car += doit modifier le champs de l’objet...
• ...tandis que + ne modifie aucun des deux objets
Factorisation de code• Cette constatation vient renforcer notre
choix de mettre l’opérateur + en-dehors de la classe
• En effet, si operator+ fait partie de la classe, l’évaluation de p1+p2 appelle une méthode sur l’objet p1
• Pourquoi p1 ? pourquoi pas p2 ?
• Le calcul de p1+p2 ne devrait pas être la responsabilité d’un des deux objets !
Factorisation de code
• De la même manière, on définit operator++() comme un +=1 (toujours avec la conversion)
• Et on définit operator++(int) en fonction d’operator++()
• Seule la valeur de retour change
Factorisation
• Au final:class paire { double x, y ;public: paire(double xx, double yy): x(xx), y(yy) {} paire(double xx=0): x(xx), y(xx) {} paire& operator+=(const paire &s) ; paire& operator-=(const paire &s) ; void affiche() const {...} double getX() const {return x ;} double getY() const {return y ;}} ;
Factorisationpaire& paire::operator+=(const paire &s) { x+=s.x ; y+=s.y ; return *this; }
paire& operator++(paire &p) { return p += 1 ;}
paire operator++(paire &p, int a){ paire ret(p) ; ++p ; return ret;}
Opérateur de conversion
• Malheureusement, on ne peut pas toujours utiliser un constructeur pour effectuer une conversion
• Supposons qu’on veuille pouvoir convertir le type Y en le type X
• Cela pose problème si on ne peut pas modifier X pour ajouter X::X(Y)
• Ce problème se présente en particulier quand X est un type de base
Opérateur de conversion
• Pour ce faire, on peut définir dans Y un opérateur de conversion vers X:operator Y::X() { ... return objetDeTypeX ;}
Opérateur de conversion
• Pour ce faire, on peut définir dans Y un opérateur de conversion vers X:operator Y::X() { ... return objetDeTypeX ;}
Le type de retour est encodé dans le nom de l’opérateur !
Opérateur de conversion
• Résumé: pour convertir de Y vers X:
• Soit on a un constructeur X::X(Y)
• Soit on a un opérateur de conversion operator Y::X()
• Attention à ne pas introduire d’ambiguïté en utilisant les deux !
Opérateur de conversion
• Exemple:Supposons qu’on veuille convertir une paire en un double: toute paire (x,y) est convertie en x+yoperator double() const { return x+y ;}
Opérateurs de conversion
• Une fois de plus, il faut prendre garde aux ambiguïtés !
• Dans notre exemple, nous avons:
•paire(double xx=0)
•operator double() const
•paire operator+(const paire, const paire)
•Ceci rend un appel à p + 2.5 ambigu (où p est une paire) !
Conversions et ambiguïtés
• En effet, le compilateur ne sait pas s’il doit convertir 2.5 en paire et utiliser operator+(paire, paire) ou convertir p en double et utiliser operator+(double, double)
Conversions et ambiguïtés
• Comment le compilateur résout il les ambiguïtés au niveau des conversions ?
• Règle générale: une assignation d’une valeur de type V à un objet de type X est légale s’il existe:
• un opérateur X::operator=(V)
• ou un opérateur X::operator=(Z) et une conversion unique de V vers Z
Conversions et ambiguïtés
• Le compilateur n’effectuera donc qu’une conversion définie par l’utilisateur au maximum
• Ceci ne fonctionnera donc pas:class Z { ... Z(X) ;}class X { ... X(int) ;}Z g(Z) ;int main() { g(1) ;}
Conversions et ambiguïtés
• class Z { ... Z(X) ;}class X { ... X(int) ;}Z g(Z) ;int main() { g(1) ;}
• Par contre, on peut aider le compilateur en forçant une des conversions nécessaires:
•int main() { g(X(1)) ;}appelle g(Z(X(1)))
•int main() { g(Z(1)) ;}appelle g(Z(X(1)))
Conversions et ambiguïtés
• Rappel: les règles de priorité vues auparavant restent valides:
• Les conversions «standards» sont toujours plus prioritaires que les conversions définies par l’utilisateur
• Exemple:class X{/*...*/ X(int) ;}void h(double) ;void h(X) ;int main() { h(1) ;}
Conversions et ambiguïtés
• Rappel: les règles de priorité vues auparavant restent valides:
• Les conversions «standards» sont toujours plus prioritaires que les conversions définies par l’utilisateur
• Exemple:class X{/*...*/ X(int) ;}void h(double) ;void h(X) ;int main() { h(1) ;}
Conversion int vers double
Fonctions friend• Nous avons vus qu’il est parfois plus
pratique de placer certains opérateurs en-dehors de la classe
• Malheureusement, cela empêche les opérateurs d’accéder au contenu (privé) des classes en question
• Solution: ajouter des fonctions de consultation du contenu
• Ce n’est pas toujours l’idéal, car alors n’importe qui peut les appeler...
Fonctions friend• Il faudrait un mécanisme pour permettre à
certaines fonctions seulement de contourner le caractère privé des champs private
• Cela peut se faire en déclarant la fonction friend dans la classe
• Exemple:class X{/* ... */ friend int f(...) } ;
Fonctions friend• Une fonction friend:
• est déclarée friend dans une ou plusieurs classes (c’est la même fonction !)
• est définie en-dehors de la classe
• n’appartient pas à la portée de la classe
• ne doit pas être appelée sur un objet de la classe
• accède aux champs privés de la classe
Fonctions friend
• Il faut donc bien avoir en tête qu’une fonction friend n’est pas une méthode de la classe
• C’est une fonction tout à fait extérieure à la classe, mais à laquelle on donne certains privilèges
Fonctions friend
• Exemple: Supposons qu’on dispose d’une classe X:class X { int i ; double d ; /*...*/} ;
• Définissons un opérateur d’addition qui reçoit un objet de type X et une paire (x,y) et renvoie la paire (x*d + i, y*d + i)
Fonctions friend
• Clairement, cet opérateur ne devrait se trouver ni dans la classe paire ni dans la classe X, car il a besoin de pouvoir accéder aux champs private des deux classes
• On en fait donc une fonction
• externe aux deux classes
• friend des deux classes pour permettre d’accéder aux champs
Fonctions friend
class X { int i ; double d ;public:/*...*/ friend paire operator+ (const paire, const X) ; friend paire operator+ (const X, const paire) ;} ;
Fonctions friend
class paire { double x, y ;public: /*...*/ friend paire operator+ (const paire, const X) ; friend paire operator+ (const X, const paire) ; /*...*/} ;
Fonctions friend
paire operator+(const X e, const paire p) { return paire(p.x*e.d + e.i, p.y*e.d + e.i) ;}
Fonctions friend• Remarques:
• Les fonctions membres d’une classe X peuvent très bien être friend d’une autre classe
• Si l’on veut que toutes les fonctions membres d’une classe X soient friend de Y, on peut utiliser le raccourci:class Y { /*...*/ friend class X ; } ;
Fonctions friend
• Le mécanisme des friend permet donc de permettre à n’importe quelle fonction d’accéder aux champs privés d’une classe
• Il permet donc de contourner la raison principale pour laquelle nous avons voulu utiliser des classes !
• A utiliser le moins possible !
Constructeurs explicit
• Comme nous l’avons vu les constructeurs qui ne prennent qu’un seul paramètre sont utilisés pour effectuer des conversions automatiques
• Exemple:class X{ X(int i) ; /*...*/}
int main() { X x = 5 ; }
Constructeurs explicit
• Ces conversions automatiques sont parfois gênantes car le compilateur génère parfois d’autres conversions.
• Exemple:class X{ X(int i) ; /*...*/}
int main() { X x = ‘a’ ; }
Constructeurs explicit
• Ces conversions automatiques sont parfois gênantes car le compilateur génère parfois d’autres conversions.
• Exemple:class X{ X(int i) ; /*...*/}
int main() { X x = ‘a’ ; }
Conversion de char vers int générée par le compilateur
Constructeurs explicit
• Dans certains cas le code généré ne sera peut-être pas ce qui est attendu
• Par exemple, si nous disposons d’une classe string
• pour représenter des chaînes de caractère
• dont le constructeur reçoit un entier qui est la taille de la chaîne
• l’instruction String s = ‘a’ ; n’aura sûrement pas l’effet attendu !
Constructeurs explicit
• Pour contourner ce problème, on peut définir le constructeur explicit
• Exemple:class X { /*...*/ explicit X(int i) ; /*...*/} ;int main () { X x1(5) ; // OK X x3 = 5 ; // KO X x2 = ‘a’ ; // KO}
Constructeurs explicit
• Quand un constructeur X::X(Y) est déclaré explicit il ne peut servir à effectuer des conversions que s’il est appelé explicitement
• X x(y) ; = conversion explicite
• X x = y ; = conversion implicite
Constructeurs explicit
• Quand un constructeur est déclaré explicit le compilateur n’ajoute pas de conversion automatique
• Par exemple:X x(z) ; ne compilera pas même s’il existe une conversion de z vers y
Opérateur []
• L’opérateur [] permet d’appliquer des indices à un objet d’une classe
• Cet opérateur doit toujours être membre de la classe
• Le type de l’objet passé comme indice peut être n’importe quoi (autre chose qu’un entier)
Opérateur []• Exemple: on aimerait pouvoir écrire p[‘x’]
ou p[‘y’] pour accéder au coordonnées des pairesdouble paire::operator[](const char c) { if (c == 'x') return x ; else if (c == 'y') return y ; else { cout << "Erreur" << endl ; exit(0) ; }}
Opérateur ()
• De la même manière qu’on a pu surcharger l’opérateur [], on peut surcharger
• Cela permet d’écrire des expressions de la forme o(expression)
• où o n’est pas une fonction mais un objet
• qui est interprétée commeo.operator()(expression)
Opérateur ()
• Exemple: on veut définir un accumulateur pour les paires
• C’est un objet qui contient une valeur de type paire
• et dans lequel on peut ajouter des paires pour qu’il en fasse la somme
• On aimerait pouvoir écrire:accumulateur Acc ;Acc(p) ; Acc(q) ; /* ... */
Opérateur ()class accumulateur { paire p ;public: accumulateur(): p(0) {} void operator()(const paire) ; void affiche() {p.affiche() ; }} ;
void accumulateur::operator() (const paire pp) { p += pp ;}
Opérateur ()• Cet exemple démontre une utilisation
typique d’operator()
• Il sert surtout quand on définit des objets qui doivent supporter une opération principale
• Dans ce cas, on peut appeler directement l’opération sur l’objet en utilisant le nom de celui-ci (et pas le nom de l’opération)
• Cela peut être vu comme un raccourci pour une méthode par défaut
Opérateur ()
• Cette technique est plus puissante que celle qui consiste à définir une fonction pour l’opération
• La fonction ne stocke pas de données (sauf dans ses variables statiques)
• La fonction ne permet qu’une seule opération
Opérateur <<
• L’opérateur << peut aussi être surchargé
• Cela permet d’écrire des expressions comme cout << x ; pour un objet de type déclaré par l’utilisateur
• Pour cela, il faut connaître le type de cout
• c’est un ostream
• On définit donc une fonctionostream& operator<<(ostream &, X)
Opérateur <<
• Exemple:ostream& operator<<(ostream& o, const paire &p) { o << "(" << p.x << "," ; o << p.y << ")" ; return o ;}
• Cette fonction est naturellement friend de paire
Opérateurs new et delete
• L’opérateur new (utilisé pour créer dynamiquement des éléments en mémoire) peut aussi être surchargé
• L’opérateur delete est appelé pour libérer cette mémoire
Opérateurs new et delete
• Quel est le travail de new ?
• Allouer en mémoire une zone qui peut stocker l’objet correspondant
• Retourner une pointeur void* vers cette zone
• Ce n’est pas new qui appelle le constructeur !
• Le compilateur génère le code qui appelle le constructeur sur la zone mémoire renvoyée par new
Opérateurs new et delete
• Quel est le travail de delete ?
• Libérer la zone mémoire dont il reçoit l’adresse
• Ce n’est pas delete qui appelle le destructeur !
• Le compilateur génère le code qui appelle le destructeur avant d’appeler delete
Opérateurs new et delete
• Pourquoi surcharger new et delete ?
• Pour modifier la politique de gestion mémoire
• Par défaut new alloue une nouvelle zone sur le heap
• C’est un appel système qui prend du temps
• On peut modifier ce comportement pour, par exemple, réutiliser une zone déjà allouée
Opérateurs new et delete
• On commence par réserver d’un coup une grande quantité de mémoire pour stocker des éléments
• On stocke ces éléments «pré-réservés» dans une structure (liste, par exemple)
• On modifie new pour qu’il aille piocher dans cette «réserve» si elle n’est pas vide
• Sinon, on réserve de la mémoire
• On modifie delete pour qu’il remette les éléments dans la réserve
class X { ...} ;
int main() { X* p = new X ; ... delete p ; p = new X ; ... delete p ;}
p
Opérateurs new et delete
class X { ...} ;
int main() { X* p = new X ; ... delete p ; p = new X ; ... delete p ;}
p
Opérateurs new et delete
class X { ...} ;
int main() { X* p = new X ; ... delete p ; p = new X ; ... delete p ;}
p
Opérateurs new et delete
class X { ...} ;
int main() { X* p = new X ; ... delete p ; p = new X ; ... delete p ;}
p
Opérateurs new et delete
class X { reserve ;} ;
int main() { X::init() ; X* p = new X ; ... X* q = new X ; delete q ; ... X* r = new X ;}
X XX
Opérateurs new et delete
class X { reserve ;} ;
int main() { X::init() ; X* p = new X ; ... X* q = new X ; delete q ; ... X* r = new X ;}
XX
p X
Version avec new, delete surchargé
Opérateurs new et delete
class X { reserve ;} ;
int main() { X::init() ; X* p = new X ; ... X* q = new X ; delete q ; ... X* r = new X ;}
X
p X
q X
Version avec new, delete surchargé
Opérateurs new et delete
class X { reserve ;} ;
int main() { X::init() ; X* p = new X ; ... X* q = new X ; delete q ; ... X* r = new X ;}
X
p X
q
Version avec new, delete surchargé
Opérateurs new et delete
class X { reserve ;} ;
int main() { X::init() ; X* p = new X ; ... X* q = new X ; delete q ; ... X* r = new X ;}
Version avec new, delete surchargé
X
p X
q
r X
Opérateurs new et delete
Opérateurs new et delete
• Comment peut-on réserver/désallouer de la mémoire «à la main» ?
• On peut utiliser la fonction de bas niveauvoid * malloc(size_t t)qui renvoie un pointeur vers une zone mémoire de taille t
• t est un entier non-signé qui indique le nombre de bytes désirés
• Exemple: int * p = (int *) malloc(sizeof(int)) ;
Opérateurs new et delete
• Comment peut-on réserver/désallouer de la mémoire «à la main» ?
• Symétriquement, on utilisevoid free(void * p)pour libérer la zone mémoire pointée par p
• Remarque: pour utiliser malloc et free, il faut inclure cstdlib
Opérateurs new et delete
• malloc et free ressemblent donc à new et delete mais sont fondamentalement différents
• malloc et free n’appellent pas les constructeurs et destructeurs
• Par contre, quand on fait un new/delete, le compilateur génère du code qui appelle le constructeur/destructeur
• ils renvoient de la mémoire «brute»
• A n’utiliser que quand c’est vraiment nécessaire !
Opérateurs new et delete
• Concrètement les opérateurs surchargés doivent être de la forme suivante:
•void* operator new(size_t t)
• doit renvoyer une zone mémoire de t bytes
•void operator delete(void *p)
• reçoit un pointeur vers la zone à libérer
• Considérés comme statiques par le compilateur !
Opérateurs new et delete
• Les opérateurs surchargés sont alors de la forme:void * X::operator new(size_t t) { void * p ; if(! reserve.empty()) { p = reserve.get() ; } else { p = malloc(t) ; } return p ;}
Opérateurs new et delete
• Les opérateurs surchargés sont alors de la forme:void X::operator delete(void * p) { reserve.add(p) ; }
Operateur =• Pourquoi y a-t-il une différence (au niveau du
code généré) entre ces deux expressions ?
• X a = e ; // constructeur
• X a; a = e; // assignation
• Réponse:
• Dans le premier cas, l’objet vient d’être alloué et est donc «vide»
• Dans le second cas, il contient déjà des données
Operateur =
• L’opérateur d’assignation doit donc prendre garde à désallouer les données contenues dans l’opérande de gauche...
• ... à condition que celle-ci soit différente de l’opérande de droite...
• ...avant de copier le contenu de l’opérande de droite dans l’opérande de gauche
Opérateur =
• Un opérateur d’assignation aura donc typiquement la forme suivante:X& X::operator= (const X& d) { if(this != &d) { /* désallouer les structures de this */ /* copier d dans this */ }}
Autres opérateurs
• Dans notre exemple, on aurait pu ajouter:
• Des opérateurs de comparaison ==, != >=, etc
• Des opérateurs de multiplication, division
• ...
Notion d’itérateur
• La possibilité de surcharger les opérateurs permet de définir des itérateurs pour parcourir aisément les structures de données
• Un itérateur est un objet qui permet de parcourir une structure tout en cachant les détails d’implémentation de cette structure
Itérateur: exemple
• Supposons que l’on crée une classe de liste, et que l’on désire que l’utilisateur puisse parcourir les éléments un à un
• Solution 1: permettre à l’utilisateur de manipuler directement des pointeurs vers les éléments de la liste
Itérateur: exemple• class liste {
public: class elem ;private: elem * tete ;public: class elem { ... public: elem * getNext() const ; int getInfo() const ; ... } ; elem * getTete() const { return tete ;}} ;
Itérateur: exemple
•int main() { liste L ; ... liste::elem *p=L.getTete() ; while (p != NULL) { cout << p->getInfo() ; p = p->getNext() ; }}
Itérateur: exemple• Cette solution pose plusieurs problèmes:
• L’utilisateur a conscience de la structure de la liste
• Si on désire changer plus tard le type des éléments, il faudra ré-écrire le code qui exploite la liste
• L’utilisateur doit «explicitement» manipuler le next et l’info d’un élément pour avancer et accéder à l’information
Itérateur: exemple• De manière générale, l’utilisateur ne devrait
pas avoir à manipuler d’elem, car ce qui l’intéresse ce sont les données stockées dans la liste (de type int)
• On va donc «cacher» les pointeurs vers des elem dans des objets de type itérateur
• Un itérateur donne donc accès à un élément de la liste
• Les méthodes de la classe permettent de parcourir la liste comme avec un pointeur
Itérateur
• De quoi a-t-on besoin en pratique ?
• D’avancer l’itérateur d’un élément: on utilise les opérateurs ++
• D’accéder à l’information contenue dans l’élément: on utilise l’opérateur de déréférencement *
• De pouvoir comparer deux itérateurs: on utilise les opérateurs == et != qui comparent les pointeurs
• D’accèder à la fin et au début de la liste: ce sont des méthodes de la classe liste qui renvoient des itérateurs.
Itérateur
• Si l’on dispose de cela, on peut alors parcourir la liste ainsi:
for (liste::iterator i= L.begin(); i != L.end(); ++i) { cout << *i << " " ;}
Classe liste• La classe liste a donc la structure suivante:class liste {public: class iterator ;private: class elem { ... } ; elem * tete ;public: class iterator { ... } ; iterator begin() const ; iterator end() const ;} ;
Itérateur: elem
class elem { int i ; elem * next ;public: elem(int ii=0, elem * pp=NULL) : i(ii), next(pp) {} friend class liste ; friend class liste::iterator ;} ;
Classe iteratorclass iterator { elem * p ;public: friend class liste ; iterator(elem * pp=NULL) : p(pp) {} iterator & operator++() ; iterator operator++(int) ; operator bool() const {return p ; } int & operator*() const {return p->i ;} bool operator==(const iterator &i) const {return i.p == p; } bool operator!=(const iterator &i) const {return i.p != p; }} ;
Classe iteratoriterator & iterator::operator++() { if(!p) cout << "Erreur" ; else p = p->next ; return * this ;}
iterator iterator::operator++(int) { iterator r(*this) ; if(!p) cout << "Erreur" ; else p = p->next ; return r ;}
Classe iteratoriterator & iterator::operator++() { if(!p) cout << "Erreur" ; else p = p->next ; return * this ;}
iterator iterator::operator++(int) { iterator r(*this) ; if(!p) cout << "Erreur" ; else p = p->next ; return r ;}
On peut tester le pointeur et capturer les
erreurs dues au pointeur NULL !
Itérateurs
• Une fois définie la notion d’itérateur, on peut s’en servir dans les méthodes de la classe liste
• Par exemple: une fonction insereApres, qui insère après un élément désigné par un itérateurliste & liste::insereApres( const iterator &it, int i)
Itérateurs
• Les itérateurs peuvent être définis pour toutes les structures qui ont un parcours naturel qui ressemble à celui d’une liste ou d’un tableau
• On veillera à toujours respecter la même syntaxe (begin(), end(), ++, *)
• La plupart des structures disponibles dans la STL admettent des itérateurs (cfr. chapitre 6)
Chapitre 6La STL
Introduction
• STL = Standard Template library
• C’est un ensemble de bibliothèques standards fournies avec tous les compilateurs C++
• Son contenu peut être considéré comme «faisant partie du langage»
Introduction
• La STL est composée de toute une série de classes qui:
• sont des templates
• sont structurées entre elles par héritage
• Comme il s’agit de matière de 2ème, on ne verra pas la STL en détail
• on se contentera d’un aperçu des ses possibilités
Introduction• Que trouve-t-on dans la STL ?
• Des routines d’entrée/sorties
• Une classe string pour représenter les chaînes de caractères (et remplacer char*)
• Des structures de données:
• vecteurs, listes, maps
• ...
• Tout cela se trouve dans le namespace std !
Entrées/sorties
• Les deux composantes de la STL les plus importantes pour faire de l’entrée/sortie sont:
• iostream pour l’affichage et la lecture sur la console
• fstream pour la lecture et l’écriture sur fichiers
Entrées/sorties• Ces deux modules de la STL (et d’autres)
partagent la notion de stream
• Un stream est un flux de données dont le but premier est d’effectuer des conversions et du formatage
• Exemple: cout et cin sont des streams.
• cout formatte les données pour un affichage correct
• cin extrait des données brutes le type demandé
Flux
• La STL contient plusieurs flux par défaut:
• cin pour la lecture sur la console
• cout pour l’affichage sur la console
• cerr pour l’affichage des erreurs
• Ces données seront en général aussi affichées sur la console, mais on peut les différencier de celles qui proviennent de cout (cfr. OS)
Flux et fichiers
• Il est aussi possible de créer ses propres flux, notamment pour la lecture et l’écriture sur fichier
• Concrètement, on peut associer à chaque fichier un ou plusieurs flux qui permettent de lire (uniquement), écrire (uniquement) ou lire et écrire sur le fichier
• On s’en sert alors comme cout ou cin
fstream
• Pour ce faire, on commence par créer un objet de type fstream
• Ensuite, on appelle la méthode fstream::open( char * filename, ios_base::openmode mode)sur ce flux en spécifiant le nom du fichier et les opérations permises sur ce fichier
fstream
• Le second paramètre doit être une combinaison (à l’aide de l’opérateur |) des valeurs suivantes:
• fstream::in ouverture en lecture
• fstream::out ouverture en écriture
• fstream::trunc supprime le contenu actuel du fichier
• fstream::app positionne le pointeur à la fin du fichier
• ...
fstream
#include <fstream>using namespace std;int main () { fstream filestr; filestr.open ("test.txt", fstream::out);
filestr << 3+4 << endl << «C++» ;
filestr.close(); return 0;}
Source: http://www.cplusplus.com/reference/iostream/fstream/open/
fstream
• Différentes méthodes permettent de tester l’état d’un flux:
• bool eof(): renvoie vrai ssi on est la fin du fichier
• bool fail(): indique s’il y a une erreur sur le flux (par exemple: erreur d’ouverture...)
• bool operator!(): synonyme de fail()
fstream#include <iostream>#include <fstream>using namespace std;
int main () { ifstream is; is.open ("test.txt"); if (!is) cerr << "Erreur 'test.txt'" ; return 0;}
Source: http://www.cplusplus.com/reference/iostream/ios/operatornot/
fstream
• Il existe encore bien d’autres méthodes permettant de manipuler les fstream de manière plus fine
• Voir les références
• par exemple: http://www.cplusplus.com/
Modificateurs• Comme on l’a dit, le travail d’un stream est
d’effectuer des conversions et du formatage
• Exemple: cout << i ; convertit la valeur contenue dans i en une chaîne de caractère qui peut être affichée
• Les modificateurs de flux et les méthodes des classes de stream permettent de contrôler la manière dont ces conversions/formatages s’effectuent.
Modificateur
• Un modificateur de flux est une valeur symbolique que l’on «envoie» dans le flux mais qui ne produit rien sur la sortie
• Par contre, il modifie l’état du flux et donc la manière dont il produit sa sortie
• Exemple:cout << scientific << 36.45 ;
Alignement
• Pour aligner les données affichées, on peut utiliser les méthodes width(int) et fill(char)
• width(int) indique un nombre minimum de caractères à afficher. S’il n’y a pas assez de caractères, la sortie sera complétée avec le caractère spécifié par fill(char) (espace par défaut)
• Les version width() et fill() renvoient la largeur et le caractère de remplissage
Alignement
• Par ailleurs, on peut utiliser les modificateurs left et right pour aligner la sortie à gauche ou à droite#include <iostream>using namespace std;int main () { cout << 100 << endl; cout.width(10); cout << 100 << endl; cout.fill('x'); cout.width(15); cout << left << 100 << endl; return 0 ;}
Source: http://www.cplusplus.com/reference/iostream/ios_base/width/
Flottants
• On peut également spécifier comment seront produits les nombres flottants:
• Le C++ offre 3 formats d’affichage:
• par défaut
• fixé (fixed)
• scientifique (scientific)
• et on peut également fixer une précision d’affichage
Flottants
• Par défaut, le nombre est affiché tel qu’il est encodé
• La précision donne le nombre maximum de chiffres produits pour représenter l’entièreté du nombre
• Les zéros inutiles sont ignorés
Flottants• En mode fixé le nombre est affiché avec un
point décimal (pas de x10...)
• La précision donne le nombre exact de chiffres qui seront produits après le point décimal
• En mode scientifique, le nombre est affiché au format scientifique (n.n...nen...n)
• La précision donne le nombre exact de chiffres qui seront produits après le point décimal
Flottants
• La précision peut être changée grâce à la méthode precision(int)
• Les modes d’affichages fixés ou scientifiques s’activent à l’aide des modificateurs fixed et scientific
• Pour désactiver fixed et scientific, sur le stream s, on utilise:s.unsetf(ios_base::floatfield);
Flottants
• Exemple:#include <iostream>using namespace std;int main () { double a,b,c; a = 3.1415926534; b = 2006.0; c = 1.0e-10; cout.precision(5); cout << a << b << c << endl; cout << fixed << a << b << c << endl; cout << scientific << a << b << c ; return 0;}
Source: http://www.cplusplus.com/reference/iostream/manipulators/scientific/
Flottants
• Exemple:#include <iostream>using namespace std;int main () { double a,b,c; a = 3.1415926534; b = 2006.0; c = 1.0e-10; cout.precision(5); cout << a << b << c << endl; cout << fixed << a << b << c << endl; cout << scientific << a << b << c ; return 0;}
Source: http://www.cplusplus.com/reference/iostream/manipulators/scientific/
3.1416 2006 1e-0103.14159 2006.00000 0.000003.14159e+000 2.00600e+003 1.00000e-010
Base• On peut également modifier la base du
nombre affiché avec les modificateurs:
•dec
• hex
•oct
• Exemple:cout << 35 << endl ;cout << hex << 35 << endl ;cout << oct << 35 << endl ;
Autres manipulateurs
• showbase / noshowbase: active / désactive l’affichage de la base
• uppercase / nouppercase: indique si les lettres utilisées en base 16 doivent être en majuscule ou non
• etc...
Classe string
• La STL contient également une classe permettant de gérer des chaînes de caractères...
• ... sans devoir utiliser explicitement un tableau de char «à la C»
• Grâce à la classe string, on manipule les chaînes de caractères avec la même facilité que les autres types de base
Classe string
• Constructeurs
• Le constructeur par défaut crée une chaîne vide
• Il existe des constructeurs pour construire une chaîne à partir d’un tableau de char
Classe string
• Opérateurs
• On peut concaténer deux chaînes avec +=
• On peut assigner un tableau de char à une string
• On peut utiliser les crochets [] pour accéder aux lettres individuelles
Classe string
• Méthodes:
• string::size() retourne le nombre de caractères
• string::empty() teste si la chaîne est vide
•string::insert(size_t pos1, const string& str) insère str dans l’objet courant à la position pos1
Classe string
• Méthodes:
•string::erase (size_t pos, size_t n) efface n caractères à partir de pos
• ...
• Consulter la documentation pour connaître l’ensemble des possibilités
Containers
• La STL contient également des classes qui implémentent différentes structures de données comme des listes...
• En pratique il vaut mieux les utiliser plutôt que de «ré-inventer l’eau chaude» et recoder une classe de liste
• cfr. deuxième année et les templates
Exemple de listeint main() { list<int> L ; L.push_back(1) ; L.push_back(2) ; L.push_back(3) ; for (list<int>::iterator it = L.begin(); it != L.end(); ++it) { cout << *it << " " ; } cout << "La liste est " ; if(!L.empty()) cout << "non" ; cout << " vide" << endl ; cout << "Elle contient " << L.size() ; cout << " éléments" << endl ;}
Chapitre 7Dernières remarques
Conception d’une classe
• Un des buts de l’orienté-objet est de cacher les détails d’implémentation pour l’utilisateur, et de ne lui permettre d’accéder aux données qu’à travers des primitives bien choisies
• Concrètement:
• Les données doivent être private
• Les méthodes doivent être bien choisies
Conception d’une classe
Conception d’une classe
• Une des responsabilités des méthodes est également de vérifier que l’encodage interne des données est cohérent
• Exemple: liste cohérente, date existante, etc...
• Cet encodage n’est pas visible de l’utilisateur, et il ne peut donc rien vérifier !
Les questions à se poser
• Quelles données ?
• Champs dans la section private
• Comment construire les données ?
• Définit le jeu de constructeurs/destructeur
• Y a-t-il des données qui dépendent de la classe plutôt que des objets ?
• Données static
Les questions à se poser
• Qui accède aux données ?
• On peut déclarer des classes/méthodes friend
• Autrement, utiliser des méthodes pour consulter/mettre à jour les données
• Méthodes static pour les données static ?
Les questions à se poser• Quel est le jeu minimal de méthodes
nécessaires pour effectuer toutes les opérations ?
• Approche «ADT»
• On limite le nombre de méthodes «de bas niveau» dans la classe, afin de faciliter le débogage
• Tout ce qui peut être réalisé à l’aide de ces méthodes peut être mis en-dehors de la classe
Les questions à se poser
• Quel est le jeu minimal de méthodes nécessaires pour effectuer toutes les opérations ?
• Exemple: Si je peux:
• Parcourir une liste
• Comparer le contenu des éléments
• Insérer après un élément
• Je peux facilement faire une insertion triée
• Cette méthode ne devrait donc pas figurer dans la classe
Les questions à se poser• Quels sont les opérateurs nécessaires ? Sur
quels types d’opérandes ?
• Essayer d’avoir des opérateurs génériques + des opérateurs de conversion
• Attention aux ambiguïtés et aux conversions introduites automatiquement par le compilateur
• C’est quand ça compile qu’il faut se méfier ;-)
Les questions à se poser
• Quelle sont les signatures des méthodes ?
• const !
• références !
• valeurs par défaut !
Les questions à se poser
• Quelle sont les signatures des méthodes ?
• const !
• références !
• valeurs par défaut !
const toutes les deux lignes
tu écriras !
Les références au maximum
tu utiliseras !
N’oubliez pas...
Et pour finir...
C makes it easy to shoot yourself in the foot;. C++ makes it harder, but when you do, it blows away your whole leg
B. Stroustrup
Questions ?
Top Related