info-f-105 langages 1di.ulb.ac.be/verif/ggeeraer/info-f-105/slides/info-f-105.pdf · objectifs •...

Post on 31-Mar-2020

7 Views

Category:

Documents

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

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 gigeerae@ulb.ac.be

• 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