SCC LM
SCC LM
Programmation Orientée
Objets en C++
Plan Pédagogique du cours
1
ESE : Electronique pour les Systèmes Embarqués, I : Instrumentation, RT : Réseaux et télécommunications, IB :
Instrumentation Biomédicale
1
SOMMAIRE
2
Chapitre I: Introduction à la Programmation Orientée Objets
I.1 Introduction
La conception par objet trouve ses fondements dans une réflexion menée autour de la
vie du logiciel. D’une part, le développement de logiciels de plus en plus importants
nécessite l’utilisation de règles permettant d’assurer une certaine qualité de réalisation.
D’autre part, la réalisation même de logiciel composée de plusieurs phases, dont le
développement ne constitue que la première partie. Elle est suivie dans la majorité des cas
d’une phase dite de maintenance qui consiste à corriger le logiciel et à le faire évoluer.
On estime que cette dernière phase représente 70 % du coût total d’un logiciel, ce qui exige
plus encore que la phase de développement doit produire du logiciel de qualité.
La conception objet est issue des réflexions effectuées autour de cette qualité. Celle-ci peut-
être atteinte à travers certains critères [6]:
La validité: c’est-à-dire le fait qu’un logiciel effectue exactement les tâches pour
lesquelles il a été conçu.
I.2 Modularité
Les critères énoncés au paragraphe précédent influent sur la façon de concevoir un
logiciel, et en particulier sur l’architecture logicielle. En effet, beaucoup de ces critères
ne sont pas respectés lorsque l’architecture d’un logiciel est obscure. Dans ces
conditions, le moindre changement de spécification peut avoir des répercutions très
importantes sur le logiciel, imposant une lourde charge de travail pour effectuer les mises à
jour.
On adopte généralement une architecture assez flexible pour parer à ce genre de
problèmes, basée sur les modules. Ceux-ci sont des entités indépendantes intégrées dans
une architecture pour produire un logiciel.
Fonction 4
4
Dans les langages procéduraux, les procédures s’appellent entre elles et peuvent
donc agir sur les mêmes données. Il ya donc un risque de partage de données
(écriture en même temps dans le même fichier).
De ces problèmes est issus une autre manière de programmer c’est la programmation par
objet ou bien L’approche orientée objet (Début des années 80). Selon cette approche, un
programme est vu comme un ensemble d’entités (ou objets). Au cours de son exécution, ces
entités collaborent en s’envoyant des messages dans un but commun [13].
Objet 1
Données
Fonctions d’accès aux données
Objet 2 Objet 3
Données Données
Fonctions d’accès aux données Fonctions d’accès aux données
Nous avons dans ce schéma un lien fort entre les données et les fonctions qui y accèdent.
Mais qu’appelle-t-on un objet ? Que représente un objet ?
5
Interface : décrit ce que fait l’objet.
Implémentation : définit comment réaliser l’interface.
Le principe de l’encapsulation est qu’on ne peut agir que sur les propriétés publiques d’un
objet:: les données sont toutes privées, leur manipulation se fait à travers les méthodes
publiques.
Communication par messages : Les objets communiquent entre eux par des
messages (un objet demande à un autre objet un service).
Identité et classification
sification:: consiste à regrouper les objets ayant le même
comportement pour former un même ensemble, appelé CLASSE (cette
( notion n’est
autre que la généralisation de la notion de type).
Un objet d’une classe s’appelle INSTANCE de cette classe.
6
Exemple:
Objet 1 (Instance 1): EMPLOYE 1
EMPLOYE 1
Classe « Employe » 125A
Kadi
EMPLOYE Mohammed
MATRICULE …., Oran
NOM_EMP Technicien
PRENOM_EMP entrer()
ADRESSE changerPoste()
QUALIFICATION changerLieuTravail()
entrer()
Objet 2 (Instance 2): EMPLOYE 2
changerPoste()
changerLieuTravail() EMPLOYE 2
175C
Oukil
Yacine
…., Alger
Ingénieur
entrer()
changerPoste()
changerLieuTravail()
Héritage: consiste à définir une nouvelle classe à partir d’une classe existante, à
laquelle on ajoute des nouvelles propriétés (données ou méthodes).
7
Chapitre II : Principes de base du langage C++
II.1 Introduction
Le langage C++ peut être considéré comme un perfectionnement du langage C qui offre
les possibilités de la POO.
Les notions de base de la programmation en C restent valables en C++, néanmoins C et C++
diffèrent sur quelques conventions (déclaration des variables et des fonctions, nouveaux
mots clés…)
Ce chapitre retrace ses différences, et traite les autres outils de la programmation structurée
ajouté à C++
La fonction main est la fonction appelée par le système d’exploitation lors de l'exécution du
programme
{ et } délimitent le corps de la fonction
main retourne un entier au système: 0 (zéro) veut dire succès
Chaque expression doit finir par; (point virgule)
II.2.2 Commentaires
En C et C++: Commentaires sur plusieurs lignes : délimités par /* (début) et */ (fin).
/* Un commentaire en une seule ligne */
/*
* Un commentaire sur plusieurs
* lignes
*/
En C++ uniquement : Commentaires sur une seule ligne : délimités par // (début) et fin de
ligne (n’existe pas en C)
8
Exemple:
Objet 1 (Instance 1): EMPLOYE 1
EMPLOYE 1
Classe « Employe » 125A
Kadi
EMPLOYE Mohammed
MATRICULE …., Oran
NOM_EMP Technicien
PRENOM_EMP entrer()
ADRESSE changerPoste()
QUALIFICATION changerLieuTravail()
entrer()
Objet 2 (Instance 2): EMPLOYE 2
changerPoste()
changerLieuTravail() EMPLOYE 2
175C
Oukil
Yacine
…., Alger
Ingénieur
entrer()
changerPoste()
changerLieuTravail()
Héritage: consiste à définir une nouvelle classe à partir d’une classe existante, à
laquelle on ajoute des nouvelles propriétés (données ou méthodes).
7
II.2.6 Espace de noms en C++
Pour des raisons liées à la POO (généricité, modularité…) et pour éviter certains conflits
qui peuvent surgir entre les différents noms des outils utilisés (prédéfinis ou définis par
l'utilisateur), C++ introduit la notion de namespace (espace de noms), ce qui permet de
définir des zones de déclaration et de définitions des différents outils (variables, fonctions,
types,…). Ainsi, chaque élément défini dans un programme ou dans une bibliothèque
appartient désormais à un namespace. La plupart des outils d’Entrées / Sorties de la
bibliothèque de C++ appartiennent à un namespace nommé "std".
Syntaxe:
using namespace std;
Exemple d’utilisation:
#include <iostream>
#include <conio.h>
using namespace std; // Importation de l'espace de nom
void main()
{ double x, y;
// Le préfixe n'est plus requis :
cout << "X:";
cin >> x;
cout << "Y:";
// Il est toujours possible d'utiliser le préfixe :
std::cin >> y;
cout << "x * y = " << x * y << endl;
_getch(); // Les fonctions de la bibliothèque C sont toujours utilisables
}
Il est possible aussi de définir un espace de nom alors les identificateurs de cet espace seront
préfixés.
Syntaxe :
namespace nom
{
// Placer ici les déclarations faisant partie de l'espace de nom
}
Exemple:
#include <iostream>
using namespace std; // Importation de l'espace de nom
namespace USTOMB
{
void afficher_adresse()
{ cout << "USTOMB" << endl;
cout << "El Mnaouar, BP 1505 , "<<endl<< " Bir El Djir 31000"<<endl ;
}
10
void afficher_coordonnees_completes()
{ afficher_adresse(); // Préfixe non requis, car dans le même espace de nom :
cout << "Tel : 041 61 71 46\n";
}
}
int main()
{ USTOMB::afficher_coordonnees_completes();
return 0;
}
Tous les caractères de formatage comme '\t', '\n' peuvent être utilisés. Par ailleurs,
l’expression endl permet le retour à la ligne et le vidage du tampon.
Exemple:
#include <iostream> // Standard C++ I/O
using namespace std;
int main()
{ int val1, val2;
cout << "Entrer deux entiers: " << endl;
cin >> val1 >> val2;
cout << "Valeurs entrées: " << val1 << " et " << val2 << endl;
cout << "valeur 1+valeur 2 =" << val1 + val2 << endl;
return 0;
}
11
On peut citer quelques avantages des nouvelles possibilités d'E/S :
Vitesse d'exécution plus rapide.
Il n'y plus de problème de types
Autres avantages liés à la POO.
Exemple:
#include <iostream>
#include <iomanip> // attention a bien inclure cette librairie
using namespace std ;
int main()
{ int i=1234;
float p=12.3456;
cout << "|" << setw(8) << setfill('*‘) << hex << i << "|" <<endl
<< "|" << setw(6) << setprecision(4) << p <<"|"<< endl;
return 0;
}
Affichage de
l’exécution du code:
12
Exemple:
ageEtudiant, nom_etudiant, NOMBRE_Etudiants: Noms valides.
Ageétudiant, nom etudiant: Noms non valides.
13
void afficher_coordonnees_completes()
{ afficher_adresse(); // Préfixe non requis, car dans le même espace de nom :
cout << "Tel : 041 61 71 46\n";
}
}
int main()
{ USTOMB::afficher_coordonnees_completes();
return 0;
}
Tous les caractères de formatage comme '\t', '\n' peuvent être utilisés. Par ailleurs,
l’expression endl permet le retour à la ligne et le vidage du tampon.
Exemple:
#include <iostream> // Standard C++ I/O
using namespace std;
int main()
{ int val1, val2;
cout << "Entrer deux entiers: " << endl;
cin >> val1 >> val2;
cout << "Valeurs entrées: " << val1 << " et " << val2 << endl;
cout << "valeur 1+valeur 2 =" << val1 + val2 << endl;
return 0;
}
11
const double PI (3.14); //exemple de constante rèel
const string motDePasse("pass_2017"); //exemple de constante chaine de caractères
b. Références
En C++, on peut coller plusieurs étiquettes à la même case mémoire (ou variable). On
obtient alors un deuxième moyen d'y accéder. On parle parfois d'alias, mais le mot correct
15
en C++ est référence. Pour déclarer une référence sur une variable, on utilise une
esperluette (&).
Exemple:
#include <iostream>
using namespace std;
int main()
{ int i(5);
int & j(i); // la variable ‘j ‘ fait référence à ‘i '. On peut utiliser à partir d'ici
'ici 'j ' ou 'i '
// indistinctement puisque ce sont deux étiquettes de la même case en mémoire
int k=j ;
cout<<" Valeur de i: "<<i<< " || "<<" Valeur de j: "<<j<<" || "<<" Valeur de k: "<<k<< "<<k endl;
j=j+2;
k=i ;
cout<<" Valeur de i: "<<i<< " || "<<" Valeur de j: "<<j<<" || "<<" Valeur de k: "<<k<< "<<k endl;
return 0;
}
Affichage de l’exécution du
code:
Remarque: Si la variable est définie dans un autre module il faut la déclarer avant de
l’utiliser en utilisant
ant le mot clé « extern »
Exemple: [1]
extern float maxCapacity; // declaration
float limit = maxCapacity * 0.90; // usage
c. Tableaux
Un tableau est une collection d’objets du même typetype, chacun
hacun de ces objets
objet est accédé
par sa position. La dimension
imension du tableau doit être connue en temps de compilation.
compilation
Exemple:
const int numPorts = 200;
double portTable[numPorts];
int bufferSize;
char buffer[bufferSize]; // ERROR: the value of bufferSize is unknown
16
Le tableau peut être initialisé lors de la définition :
int groupTable[3] = {134, 85, 29};
int userTable[] = {10, 74, 43, 45, 89}; // 5 positions
17
Peut être aussi écrit
result = result + int(10.890);
Permet d’utiliser des pointeurs génériques
char* name;
void* genericPointer;
name = (char*)genericPointer; // Conversion explicite
Il est clair que « l’addition » unaire ne fait rien pour les types simples (nombres
entiers et réels), mais grâce à la surcharge des opérateurs, on peut donner un sens à
cet opérateur sur des types (=classes) complexes
c. Opérateurs de décalage
Opérateur Signification Arité Associativité
<< Décalage à droite Binaire Gauche à droite
>> Décalage à gauche binaire Gauche à droite
Ces opérateurs sont assez peu employés dans leur contexte « C » (décalage de bits),
mais ils prennent une importance considérable en C++ dans la manipulation des flux
d’entrées/sorties.
Exemple :
int b=100 ; // b=1100100 en code binaire
std ::cout<< " (b<<1) = " <<(b<<1) << std ::endl ;
std ::cout<< " (b<<2) = " <<(b<<2) << std ::endl ;
std ::cout<< " (b>>1) = " <<(b>>1) << std ::endl ;
std ::cout<< " (b>>2) = " <<(b>>2) << std ::endl ;
18
Affichage de l’exécution du code:
d. Opérateurs relationnels
Opérateur Signification Arité Associativité
< Inférieur à binaire Gauche à droite
> Supérieur à binaire Gauche à droite
<= Inférieur ou égal à binaire Gauche à droite
>= Supérieur ou égal à binaire Gauche à droite
en C++, ces opérateurs retournent un type bool (valeurs true ou false) contrairement
au C où ils retournent un type int (valeurs 0 ou 1)
e. Opérateurs d’égalité
Opérateur Signification Arité Associativité
== Egalité binaire Gauche à droite
!= Inégalité binaire Gauche à droite
Ces opérateurs travaillent sur les bits, comme les décalages. Attention de ne pas les
confondre avec les opérateurs logiques.
g. Opérateurs logiques
Opérateur Signification Arité Associativité
&& Et logique binaire Gauche à droite
|| Ou logique binaire Gauche à droite
e1 ?e2 :e3 Condition if - else ternaire De droite à gauche
h. Opérateurs d’affectation
Opérateur Signification Arité Associativité
= Affectation R=a Droite à gauche
*= Multiplication et affectation a = a*b Droite à gauche
/= Division et affectation a = a/b Droite à gauche
%= Modulo et affectation a = a%b Droite à gauche
19
+= Addition et affectation a = a+b Droite à gauche
-= Soustraction et affectation a = a-b Droite à gauche
<<= Décalage à gauche et affectation a = a<<b Droite à gauche
>>= Décalage à droite et affectation a = a>>b Droite à gauche
&= Et binaire et affectation a = a&b Droite à gauche
|= Ou binaire a = a|b Droite à gauche
^= Ou exclusif binaire et affectation a = a^b Droite à gauche
++ Incrémentation a = a+1 ≈ a++ Droite à gauche
-- Décrémentation a = a-1 ≈ a-- Droite à gauche
Exemple:
int a, b=3, c, d=3 ;
a = ++b ; // équivalent à b++ ; puis a=b ; => a=b=4
c = d++ ; // équivalent à c=d ; puis d++ ; => c=3 et d=4
où condition est une expression dont la valeur est booléenne ou entière. Toute valeur non
nulle est considérée comme vraie. Si le test est vrai, instruction est exécutée.
Il est possible de définir plusieurs conditions à remplir avec les opérateurs ET et OU
(&& et ||). Par exemple, l'instruction ci-dessous exécutera les instructions si l'une ou
l'autre des deux conditions est vraie :
Exemple:
if ((condition1) || (condition2))
{ instruction ; }
20
Syntaxe :
if (condition)
{ instruction ; }
else
{ autre instruction ; }
21
Le tableau peut être initialisé lors de la définition :
int groupTable[3] = {134, 85, 29};
int userTable[] = {10, 74, 43, 45, 89}; // 5 positions
17
II.5.2 Structures de contrôle itératives [4a]
Les structures de contrôle itératives sont des structures qui permettent d'exécuter
plusieurs fois la même série d'instructions jusqu'à ce qu'une condition ne soit plus réalisée.
On appelle ces structures des boucles.
Il existe 3 types de boucles à connaître :
la boucle for,
la boucle while et
la boucle do … while.
int main()
{ int compteur(0);
for (compteur = 1 ; compteur < 10 ; compteur++)
cout << compteur <<" || " ;
cout << endl ;
return 0 ;
}
Affichage de
l’exécution du code:
23
Exemple :
#include <iostream>
using namespace std;
int main()
{ int i = 1;
while (i < 10) //affiche les valeurs de 1 jusqu’à 9
{ cout <<" "<< i <<endl ;
i++ ;
}
return 0 ;
}
Affichage de l’exécution du
code:
Syntaxe:
do
{
instructions ;
} while (condition) ;
Exemple:
#include <iostream>
using namespace std;
int main()
{ int i = 1;
do
{ cout << i << endl;
i++;
} while(i<10); //affiche les valeurs de 1 jusqu’à 9
return 0;
}
24
+= Addition et affectation a = a+b Droite à gauche
-= Soustraction et affectation a = a-b Droite à gauche
<<= Décalage à gauche et affectation a = a<<b Droite à gauche
>>= Décalage à droite et affectation a = a>>b Droite à gauche
&= Et binaire et affectation a = a&b Droite à gauche
|= Ou binaire a = a|b Droite à gauche
^= Ou exclusif binaire et affectation a = a^b Droite à gauche
++ Incrémentation a = a+1 ≈ a++ Droite à gauche
-- Décrémentation a = a-1 ≈ a-- Droite à gauche
Exemple:
int a, b=3, c, d=3 ;
a = ++b ; // équivalent à b++ ; puis a=b ; => a=b=4
c = d++ ; // équivalent à c=d ; puis d++ ; => c=3 et d=4
où condition est une expression dont la valeur est booléenne ou entière. Toute valeur non
nulle est considérée comme vraie. Si le test est vrai, instruction est exécutée.
Il est possible de définir plusieurs conditions à remplir avec les opérateurs ET et OU
(&& et ||). Par exemple, l'instruction ci-dessous exécutera les instructions si l'une ou
l'autre des deux conditions est vraie :
Exemple:
if ((condition1) || (condition2))
{ instruction ; }
20
Chapitre III : Fonctions en C++
Exemple:
Type de la Nom de la Liste des
valeur de retour fonction paramètres
Il est possible de créer plusieurs fonctions ayant le même nom. Il faut alors que la liste des
arguments des deux fonctions soit différente. C'est ce qu'on appelle la surcharge d'une fonction.
26
La variable choix est évaluée en premier. Son type doit être entier. Selon le résultat de
l’évaluation, l’exécution du programme se poursuit au cas de même valeur. Si aucun des cas
ne correspond et si default est présent, l’exécution se poursuit après default. Si en revanche
default n’est pas présent, on sort du switch. Pour forcer la sortie du switch, on doit utiliser le
mot-clé break.
Exemple: [4a]
#include <iostream>
using namespace std;
int main()
{ int a;
cout << "Tapez la valeur de a : ";
cin >> a; // Ce programme demande à l'utilisateur de taper une
switch(a) // valeur entière et la stocke dans la variable a. On teste
{ case 1 : // ensuite la valeur de a : en fonction de cette valeur on
cout << "a vaut 1" << endl; // affiche respectivement les messages "a vaut 1","a vaut 2
break; // ou 4","a vaut 3, 7 ou 8", ou "valeur autre".
case 2 :
case 4 :
cout << "a vaut 2 ou 4" << endl;
break;
case 3 :
case 7 :
case 8 :
cout << "a vaut 3, 7 ou 8" << endl;
break;
default :
cout << "valeur autre" << endl;
}
return 0;
}
22
Exemple:
#include <iostream>
using namespace std;
void presenterPgm () ;
int main ()
{
presenterPgm();
return 0;
}
void presenterPgm ()
{
cout << "ce pgm …";
}
Une fonction produit toujours au maximum un résultat, c’est-à-dire qu’Il n'est pas possible de
renvoyer plus qu'une seule valeur.
return 0;
}
Affichage de l’exécution:
29
Exemple :
#include <iostream>
using namespace std;
int main()
{ int i = 1;
while (i < 10) //affiche les valeurs de 1 jusqu’à 9
{ cout <<" "<< i <<endl ;
i++ ;
}
return 0 ;
}
Affichage de l’exécution du
code:
Syntaxe:
do
{
instructions ;
} while (condition) ;
Exemple:
#include <iostream>
using namespace std;
int main()
{ int i = 1;
do
{ cout << i << endl;
i++;
} while(i<10); //affiche les valeurs de 1 jusqu’à 9
return 0;
}
24
II.5.3 Ruptures de Séquence
II.5.3.1 break
L'instruction break sert à "casser" ou interrompre une boucle (for, while et do … while), ou
un switch. L’exécution reprend immédiatement après le bloc terminé.
Exemple:
#include <iostream>
using namespace std;
int main()
{ int i;
for (i = 0 ; i < 10 ; i++)
{ cout <<" i= " <<i<< endl;
if (i==3)
break;
}
cout<<" Valeur de i lors de la sortie de la boucle : " << i <<endl ; return 0;
}
Affichage de l’exécution du
code:
II.5.3.2 continue
L'instruction continue sert à "continuer" une boucle (for, while et do … while) avec la
prochaine itération.
Exemple:
#include <iostream>
using namespace std;
int main()
{ int i;
for (i = 0 ; i < 5 ; i++)
{ if (i==3)
continue;
cout <<" i= " <<i<< endl;
}
cout<<" Valeur de iaprès la sortie de la boucle : " << i <<endl ; return 0;
}
Affichage de l’exécution
du code:
25
Exemple :
#include <iostream>
using namespace std;
void sp (int *); // ajout de l’étoile au niveau du prototype
int main()
{int n = 3;
cout << "n=" << n << endl;
sp (&n); //appel de la fonction
cout << "n=" << n<<endl ;
return 0 ;
}
void sp (int * nbre) //Ajout de la référence dans la fonction
{ cout << "----------"<<endl ;
cout << "nbre=" << *nbre << endl;
*nbre = *nbre + 2;
cout << "nbre=" <<* nbre<<endl ;
cout << "----------"<<endl ;
}
Affichage de l’exécution:
32
Chapitre III : Fonctions en C++
Exemple:
Type de la Nom de la Liste des
valeur de retour fonction paramètres
Il est possible de créer plusieurs fonctions ayant le même nom. Il faut alors que la liste des
arguments des deux fonctions soit différente. C'est ce qu'on appelle la surcharge d'une fonction.
26
IV.1.1.2 Manipulation des éléments
Un élément du tableau (repéré par le nom du tableau et son indice) peut être manipulé
exactement comme une variable, on peut donc effectuer des opérations avec (ou sur) des
éléments de tableau.
Le point fort des tableaux, c'est qu'on peut les parcourir en utilisant une boucle. On peut
ainsi effectuer une action sur chacune des cases d'un tableau, l'une après l'autre : par
exemple afficher le contenu des cases.
#include <iostream>
using namespace std;
int main()
{
int const taille(10); //taille du tableau
int tableau[taille]; //declaration du tableau
for (int i(0); i<taille; i++ )
{ tableau[i]=i*i; cout<<"Le tableau ["<<i<<"] contient la valeur "<<tableau[i]<<endl; }
return 0;
}
Affichage de l’exécution:
34
Un tableau de cette forme ne connaît pas sa taille.
On ne peut pas faire d'affectation globale.
une fonction ne peut pas retourner de tableaux.
Pour remédier ces trois problèmes, Le C++ a introduit la notion des tableaux dynamiques
ces derniers sont des tableaux dont le nombre de cases peut varier au cours de l'exécution
du programme. Ils permettent d'ajuster la taille du tableau au besoin du programmeur.
int main()
{
int const TailleTab(5);
//Déclaration du tableau
vector<int> Scores(TailleTab);
//Remplissage du tableau
Scores[0] = 100432; Scores[1] = 87347; Scores [2] = 64523; Scores[3] = 31415; Scores[4] = 118218;
35
IV.1.2.2 Modification de la taille
On peut modifier la taille d’un tableau soit en ajoutant des cases à la fin d'un tableau ou en
supprimant la dernière case d'un tableau.
Commençons par ajouter des cases à la fin d'un tableau.
a. Fonction push_back()
Il faut utiliser la fonction push_back(). On écrit le nom du tableau, suivi d'un point et du
mot push_back avec, entre parenthèses, la valeur qui va remplir la nouvelle case.
Exemple :
vector<int> tableau(3,2); //Un tableau de 3 entiers valant tous 2
tableau.push_back(8); //On ajoute une 4ème case au tableau qui contient la valeur 8
b. Fonction pop_back()
On peut supprimer la dernière case d'un tableau en utilisant la fonction pop_back() de la
même manière que push_back(), sauf qu'il n'y a rien à mettre entre les parenthèses.
Exemple :
vector<int> tableau(3,2); //Un tableau de 3 entiers valant tous 2
tableau.pop_back(8); // Il y a plus que 2 éléments dans le tableau
36
int main()
{ double x, y,z, resultat;
cout << "Tapez la valeur de x : "; cin >> x;
cout << "Tapez la valeur de y : "; cin >> y;
cout << "Tapez la valeur de z : "; cin >> z;
//appel de notre fonction somme (double,double)
resultat = somme(x, y);
cout << x << " + " << y << " = " << resultat << endl;
//appel de notre fonction somme (int,int)
resultat = somme(static_cast<int>(x), static_cast<int> ( y));
cout << static_cast<int>(x) << " + " << static_cast<int>( y) << " = " << resultat << endl;
//appel de notre fonction somme (double, double, double)
resultat = somme(x, y,z);
cout<< x << " + " << y << " + " << z<< " = " << resultat << endl;
return 0;
}
Affichage de l’exécution:
29
Affichage de l’exécution:
Il y a quelques règles que vous devez retenir pour les valeurs par défaut:
Seul le prototype doit contenir les valeurs par défaut.
Les valeurs par défaut doivent se trouver à la fin de la liste des paramètres.
Vous pouvez rendre tous les paramètres de votre fonction facultatifs.
Affichage de l’exécution:
30
Dans ce cas, le tableau dynamique ne peut pas être modifié. Pour chang changer le contenu du
tableau, il faut utiliser un passage par référence tout simple (sans le mot const donc). Ce qui
donne :
void afficheTableau(vector<int>&
>& tab)
{…}
Pour appeler une fonction recevant un vector en argument, il suffit de mettre le nom du
tableau dynamique comme paramètre entre les parenthèses lors de l'appel. Ce qui donne :
Il est possible
ble d'écrire une fonction renvoyant un vector.
vector<int> premierCarres(int n)
{ vector<int> tab(n);
for(int i = 0; i < tab.size();
(); i++)
tab[i] = i * i;
return tab;
}
39
Exemple :
#include <iostream>
using namespace std;
void sp (int *); // ajout de l’étoile au niveau du prototype
int main()
{int n = 3;
cout << "n=" << n << endl;
sp (&n); //appel de la fonction
cout << "n=" << n<<endl ;
return 0 ;
}
void sp (int * nbre) //Ajout de la référence dans la fonction
{ cout << "----------"<<endl ;
cout << "nbre=" << *nbre << endl;
*nbre = *nbre + 2;
cout << "nbre=" <<* nbre<<endl ;
cout << "----------"<<endl ;
}
Affichage de l’exécution:
32
IV.3.3 Instanciation et initialisation de chaînes
Pour initialiser notre objet au moment de la déclaration (et donc lui donner une valeur !),
il y a plusieurs possibilités:
int main()
{ string maChaine("Bonjour !"); //Création d'un objet 'maChaine' de type string et initialisation
String s3 = "chaine 3";
return 0;
}
Par défaut, lorsqu'on saisit une chaîne de caractères en utilisant cin, le séparateur est
l'espace : cela empêche de saisir une chaîne de caractères comportant un espace.
La fonction getline(iostream &,string) permet de saisir une chaîne de caractères en
utilisant le passage à la ligne comme séparateur : notre chaîne de caractères peut alors
comporter des espaces.
On peut utiliser une autre syntaxe de concaténation: s1.append (s2) qui est équivalente à
s1 = s1+s2
41
IV.3.6 Quelques méthodes utiles du type « string » [16]
Index: permet d'indiquer à partir de quel caractère on doit couper (ce doit être un
numéro de caractère).
Num : permet d'indiquer le nombre de caractères que l'on prend. Par défaut, la
valeur est npos, ce qui revient à prendre tous les caractères qui restent. Si vous
indiquez 2, la méthode ne renverra que 2 caractères.
Exemple:
int main()
{ string chaine("Bonjour !");
cout << chaine.substr(3) << endl;
return 0;
}
42
Jour ! Affichage de l’exécution
int main()
{ string chaine("Bonjour !");
cout << chaine.substr(3, 4) << endl;
return 0;
}
Jour Affichage de l’exécution
On a demandé à prendre 4 caractères en partant du caractère n°3, ce qui fait qu'on a
récupéré « jour ».
BONJOUR
AU REVOIR
Affichage de l’exécution
Dans cet exemple, c1 est un tableau de 8 char contenant la chaîne "BONJOUR " sans
oublier le caractère de fin de chaîne '\0'.
Le pointeur c2 est un pointeur vers un tableau non modifiable de char.
Les variables s1 et s2 sont des string. On peut affecter directement s1=c1 : le tableau de
char sera transformé en string.
Dans c2, on peut récupérer une chaîne « de type C » identique à notre string en écrivant
c2=s2.c_str(). On peut transformer aisément un string en tableau de char et inversement.
43
IV.3.6.5 Méthode «istr() »
Pour transformer une chaîne en double ou en int, il faut transformer la chaîne en flot
de sortie caractères : il s'agit d'un istringstream. Ensuite, nous pourrons lire ce flot de
caractères en utilisant les opérateurs usuels >>.
La méthode eof() sur un istringstream permet de savoir si la fin de la chaîne a été
atteinte. Chaque lecture sur ce flot grâce à l'opérateur >> renvoie un booléen qui
nous indique d'éventuelles erreurs.
Exemple :
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main (void)
{
string s;
cout << "Tapez une chaine : "; getline (cin, s);
istringstream istr(s);
int i;
if (istr >> i) cout << "VOUS AVEZ TAPE L'ENTIER " << i << endl;
else cout << "VALEUR INCORRECTE" << endl;
return 0;
}
Dans cet exemple, s est une chaîne : on saisit une chaîne au clavier en utilisant
getline(cin,s). On crée ensuite un istringstream appelé istr et construit à partir de s.
On peut lire un entier i à partir de istr en utilisant : istr>>i . (istr>>i) renvoie true si un
entier valide a pu être lu et renvoie false sinon. De la même manière, on pourrait lire des
données d'autres types, double par exemple.
44
IV.44 Pointeurs et allocation dynamique [9]
Dans un programme, pour garder un lien vers une donnée (une variable), on utilise des
références ou des pointeurs. En programmation, les pointeurs et références servent
essentiellement à trois choses
es:
1. Permettre
ermettre à plusieurs portions de code de partager des
objets (données, fonctions,.. ) sans les dupliquer
=> Référence
3. Pouvoir
ouvoir manipuler des objets dont la durée de vie dépasse
la portée
=> Allocation
llocation dynamique
IV.4.1 Différents
ifférents pointeurs et références [9]
En C++, il existe plusieurs
lusieurs sortes de « pointeurs » (pointeur et références) :
Les références: totalement gérées en interne par llee compilateur. Très sûres, donc;
donc
mais sont fondamentalement différentes des vrais pointeurs.
Les
es « pointeurs intelligents » (smart pointers)
pointers): gérés par le programmeur,
programm mais avec
des gardes-fous. Il en existe 3: unique_ptr, shared_ptr, weak_ptr (avec #include
<memory>)
Les
es « pointeurs « hérités de C » » (build
(build-in pointers): les plus puissants (peuvent
tout faire) mais les plus « dangereux »
IV.4.1.1 Référence
Une référence est un autre nom pour un objet existant, un synonyme, un alias.
La syntaxe de déclaration
éclaration d’une référence est: <type>&>& nom_reference(identificateur) ;
Après une telle déclaration, nom_reference peut être utilisé partout
out où identificateur peut
l’être [9].
int val(1) ;
int&
& x(val) ;
45
Exemple 1 :
#include <iostream>
using namespace std;
void f(int & a )
{ ++a ; }
int main (void)
{ int b=1;
f(b) ;
cout << " APRES b = "<<b;
return 0;
}
Résultat de l’exécution
Résultat de l’exécution
On
n ne peut donc pas faire de tableau de références
46
IV.4.1.2 Pointeurs [9]
Une variable est physiquement iidentifiée de façon unique par son adresse,
adresse c’est-à-dire
l’adresse de l’emplacement mémoire qui contient sa valeur.
Un pointeur est une variable qui contient l’adresse d’un autre objet informatique (par ex,
variable) => une « variable de variable »
Notes :
Une référence n’est pas un «vrai pointeur» car ce n’est pas une variable en tant que telle.
Une référence est «une autre étiquette»
étiquette».
Un pointeur «une variable contenant une adresse»
L’initialisation
’initialisation d’un pointeur se fait selon la syntaxe suivante:
type* identificateur(adresse) ;
Exemple:
Int* ptr(NULL);
int * ptr(&i);
int * ptr(new int(33));
(33));
nullptr : mot clé C++, spécifie que le pointeur ne pointe sur rien
Pour le compilateur, une variable est un emplaceme
emplacement nt dans la mémoire,
Cet emplacement est identifié par une adresse
Chaque variable possède une et une seule adresse.
Un pointeur permet d’accéder à une variable par son adresse.
Les pointeurs sont des variables faites pour contenir des adresses.
47
* est l’opérateur qui retourne la valeur pointée par une variable pointeur. Si px est de
type type*,, (*px) est la valeur de type type pointée par px.
int x(3);
int * px(NULL);
px = &x;
cout<< "LeLe pointeur px contient l’adresse
l’adresse"<<*px<<endl ;
48
pti += 1 ; fait avancer, le pointeur d’une case ce qui fait qu’il contient l’adresse du
2ème élément du tableau.
Nous pouvons déduire de cette arithmétique de pointeur que :
tab[i] est équivalent à *(tab +i).
*(pti+i) est équivalent à pti[i].
La figure suivante est un exemple dans lequel sont décrites les différentes façons d’accéder
aux éléments d’un tableau tab et du pointeur pt après la définition suivante:
int tab[6], *pt = tab;
49
IV.4.2 Allocation dynamique
Il y a trois façons d’allouer de la mémoire en C++
1. Allocation statique : La réservation mémoire est déterminée à la compilation.
L'espace alloué statiquement est déjà réservé dans le fichier exécutable du
programme lorsque le système d'exploitation charge le programme en mémoire pour
l'exécuter. L'avantage de l'allocation statique se situe essentiellement au niveau des
performances, puisqu'on évit
évitee les coûts de l'allocation dynamique à l'exécution.
l'exécution
int* p;
p = new int (2);
cout<< " p = "<< p <<" *p = "<< *p <<endl ;
50
Exemple :
int* px(NULL);
px = new int ;
*px = 20 ;
cout<< "*px = "<<*px ;
delete px ;
px = NULL ;
Notes :
Une variable allouée statiquement est désallouée automatiquement (à la fermeture
du bloc).
Une variable (zone mémoire) allouée dynamiquement doit être désallouée
explicitement par le programmeur.
int* px(NULL);
{px = new int(4) ; int n=6 ;}
cout<< "*px = "<<*px ;
delete px ; px = NULL ;
cout<< "*px = "<<*px <<endl;
cout<< "n = "<< n ;
Attention
Si on essaye d’utiliser la valeur pointée par un pointeur pour lequel aucune mémoire n’a été
réservée, une erreur de type Segmentation fault se produira à l’exécution.
int* px;
*px = 20 ; // ! Erreur : px n’a pas été alloué
cout<< "*px = "<<*px<<endl;
Compilation : Ok
Exécution: arrêt programme
(Segmentation fault)
Bonnes pratiques
int* px(NULL);
Initialisez toujours vos pointeurs
if(px != NULL) Utilisez NULL si vous ne connaissez pas
{ *px = 20 ; encore la mémoire pointée au moment de
cout<< "*px = "<<*px<<endl; l’initialisation
}
51
IV.4.2.4 Allocation dynamique de tableau bidimensionnel
Syntaxe :
type **data;
data = new type*[ligne]; // Construction des lignes
for (int j = 0; j < ligne; j++)
data[j] = new type[colonne]; // Construction des colonnes
Syntaxe :
for (int i = 0; i < ligm; i++)
delete[lignes] data[i]; // Suppression des colonnes
delete[] data; // Suppression des lignes
Permet de libérer l'emplacement mémoire désigné par data alloué préalablement par
new[]
Exemple 1:
#include <iostream>
#include <ctime>
Using namespace std;
int main(void)
{ double **data;
int m,n; //m: lignes , n:colonnes
srand(time(NULL));
52
data = new double*[m];
for (int j = 0; j < m; j++)
data[j] = new double[n];
Exemple 2:
#include <iostream>
Using namespace std;
struct Etudiant
{ string nom ;
double moyenne ;
char sexe ; // ’F’ ou ‘M’
};
void initialiserEtudiant(Etudiant *e) ;
void afficherEtudiant(Etudiant *e) ;
int main(void)
{ Etudiant *ptrEtudiant ;
53
Chapitr
Chapitre V : Classes et objets
54
V.2.2 Comportement
Le comportement d’un objet se défini par l’ensemble des opérations qu’il peut exécuter en
réaction aux messages envoyés (un message = demande d’exécution d’une opération) par
les autres objets.
Exemple:
Un point peut être déplacé, tourné autour d'un autre point, etc.
Un autre objet
Message
V.2.3 Identité
Propriété d'un objet qui permet de le distinguer des autres objets de la même classe.
Classes
Des objets similaires peuvent être informatiquement décrits par une même abstraction: une
classe
même structure de données et méthodes de traitement
valeurs différentes pour chaque objet
55
V.3.1 Déclaration
Une classe est une structure. Sauf que:
Le mot réservé class remplace struct
Certains champs sont des fonctions.
En C++, une classe se définit grâce au mot clef "class" suivi du nom de celle-ci. Ce nom
commence généralement par une majuscule. A la suite, les données membres et les
méthodes sont déclarées entre deux accolades. La définition de la classe se termine
obligatoirement par un point virgule:
Syntaxe:
class Nom
{
//données membres et fonctions membres
};
Prenons un exemple concret et simple: l'écriture d'une classe Point:
En C, nous aurions fait une structure comme suit:
struct Point
{
int x; // Abscisse du point
int y; // Ordonnée
};
La déclaration précédente fonctionne parfaitement en C++. Mais nous aimerions rajouter
des fonctions qui sont fortement liées à ces données, comme l'affichage d'un point, son
déplacement, etc.
56
void placer (int a ,int b)
{ x=a;
y=b;
}
void deplace(int a, int b)
{ x += a;
y += b;
}
};
Commentaires:
Les champs x et y sont les attributs de classes, appelés aussi variables membres.
Les fonctions afficher, placer et deplace sont appelées méthodes ou fonctions
membres.
Le terme public signifie que tous les membres qui suivent (données comme
méthodes) sont accessibles de l'extérieur de la classe. Nous verrons les différentes
possibilités plus tard.
57
p->x=10;
p->y=20;
p->afficher();
p->placer(1,5);
p->afficher();
}
58
V.3.4 Objets transmis en argument d'une fonction membre
Nous pouvons maintenant imaginer vouloir comparer deux points, afin de savoir s'ils sont
égaux. Pour cela, nous allons mettre en œuvre une méthode "Coincide" qui renvoie "true"
lorsque les coordonnées des deux points sont égales :
class Points
{ public :
int x;
int y;
bool Coincide(Point p)
{ if( (p.x==x) && (p.y==y) )
return true;
else
return false;
}
};
La solution qui vous vient à l'esprit dans un premier temps est probablement de passer par
un pointeur. Cette solution est possible, mais n'est pas la meilleure, dans la mesure où nous
savons fort bien que ces pointeurs sont toujours sources d'erreurs (lorsqu'ils sont non
initialisés, par exemple).
La vraie solution offerte par le C++ est de passer par des références. Avec ce type de passage
de paramètre, aucune erreur est possible puisque l'objet à passer doit déjà exister (être
instancier). En plus, les références offrent une simplification d'écriture, par rapport aux
pointeurs :
#include <iostream>
class Points
{ public :
int x;
int y;
bool Coincide(Point &p)
{ if( (p.x==x) && (p.y==y) )
return true;
else
return false;
}
};
void main()
{
Points p;
Points pp;
pp.x=0;pp.y=1;
p.x=0; p.y=1;
if( p.Coincide(pp) )
59
cout << "p et pp coincident !" << endl;
if( pp.Coincide(p) )
cout << "pp et p coincident !" << endl;
}
60
char Prenom[50] ;
int NCIN ;
Date DN
//...
};
La structure Date est connue dans tout le programme.
void main()
{ Etudiant E ;
Date D ;
D.j=1 ; D.m=1 ; D.a=2008 ;
strcpy(E.Nom, "ali") ;
E.DN=D ;
};
2ème méthode : La structure est définie à l’intérieur de la classe
class Etudiant
{ typedef struct Date
{ int j, m, a; } ;
public :
char Nom[30] ;
char Prenom[30] ;
int NCIN ;
Date DN ;
//...
};
La structure Date est connue uniquement à l’intérieur de la classe Etudiant.
void main()
{ Etudiant E ;
Date D ; // erreur de compilation
E.DN.j=1 ;
D.DN.m=1 ;
D.DN.a=2008 ;
strcpy(E.Nom, "Nabil") ;
//...
};
61
class Etudiant
{ public :
char Nom[30] ;
char Prenom[30] ;
int NCIN ;
Date DN ; // DN est une instance de Date
void afficher()
{
cout<< "Nom="<<Nom<<endl ;
cout<< "Prénom="<<Prenom<<endl ;
cout<< "numéro de CIN="<<NCIN<<endl ;
cout<< "Date de naissance= " ;
DN.afficher() ;
}
//...
};
Déclaration de la classe Date dans Etudiant :
class Etudiant
{
class Date
{public :
int j;
int m;
int a;
void afficher()
{ cout<<j<< "/"<<m<<"/"<<a<<endl ;}
void init(int jj, int mm, int aa)
{ j=jj ; m=mm ; a=aa ; }
};
public :
char Nom[30] ;
char Prenom[30] ;
int NCIN ;
Date DN ; // DN est une instance de Date
void afficher()
{ cout<< "Nom="<<Nom<<endl ;
cout<< "Prénom="<<Prenom<<endl ;
cout<< "numéro de CIN="<<NCIN<<endl ;
cout<< "Date de naissance= " ;
DN.afficher() ;
}
//...
};
62
Chapitre VI: Notions
otions d’Encapsulation / Constructeurs et Destructeurs
D
Interfaces Implémentation
63
Programme principal:
#include <iostream>
#include "Points.cpp"
using namespace std ;
void main()
{ Points *p=new Points;
Points *p1=new Points;
p1->x=1;
p1->y=15;
p1->couleur=1; // erreur de compilation car couleur est un membre privé
p1->colorier(10); // erreur de compilation car colorier() est une fonction privée
}
Messages d’erreurs:
Remarque: Les mots clés public, private et protected peuvent apparaître à plusieurs
reprises dans la déclaration
ation d'une même classe.
64
VI.2 Initialisation et constructeur
VI.2.1 Initialisation
L’initialisation permet d’affecter des valeurs à des variables lors de leur déclaration.
Comment pouvons-nous initialiser un objet? Quel mécanisme offre la programmation
orientée objet pour l’initialisation des objets.
Initialisation : Affectation de valeur lors de la déclaration
Exemple:
class compte
{ private:
int numero; //numéro du compte
float solde ;
public :
void init(int n)
{ numero=n ;
solde=0.f ;
}
void consulter()
{ cout <<"Le compte numero:"<<numero ;
cout <<"\n a le solde : "<<solde ;
}
};
- Correcte mais nous n’avons pas fait d’initialisation, en effet, l’affectation est faite
après la déclaration. En plus nous pouvons avoir des comptes qui sont manipulés
sans avoir un numéro de compte parce que nous pouvons déclarer des objets
compte sans être obligé d’appeler init.
2ème solution:
compte b={100,0.f} ;
- Correcte mais numéro et solde sont privés donc nous ne pouvons pas les accéder. En
plus même si dans une classe tous ses attributs sont publiques, il faut tous les
initialiser et dans l’ordre de leur apparition dans la classe, ce qui n’est pas aisée.
Vous remarquez donc que pour initialiser un objet nous avons besoin d’un nouveau mécanisme:
Constructeur.
VI.2.2 Constructeurs
Un constructeur est une fonction membre spéciale dont la tâche est d'initialiser l'objet;
elle porte le même nom que la classe, elle n'a pas de résultat, et elle peut avoir 0 ou
plusieurs paramètres.
Elle est appelée lors de la déclaration de l'objet d'une façon implicite.
65
Exemple :
class Cpoint
{ float abscisse;
float ordonne;
public:
Cpoint (float x =0.f, float y =0.f)
{ abscisse = x;
Ordonne = y;
}
};
void main()
{ Cpoint p1(5,3); //p1 a les coordonnées (5,3)
Cpoint p2(15); //p2 a les coordonnées (15,0)
Cpoint p3; //p3 a les coordonnées (0,0)
}
66
Exemple:
compte C[5]; //le constructeur sera appelé 5 fois
compte *pc=new compte[10]; //le constructeur sera appelé 10 fois
int x, y;
char *label;
};
67
Regardons le Programme principal:
#include <iostream>
#include "PointNomme.cpp"
using namespace std ;
void main()
{
PointNomme *a= new PointNomme(1,1);
PointNomme *b=a; //a et b partagent les mêmes valeurs!
b->label="Bonjour"; //regardez cette affectation!
a->label="Amine";
cout <<"La chaine de a est changé mais la chaine de b vaut aussi :"<<b->label<<endl;
}
On remarque que nous avons effectué l’affectation avant de changer l’attribut label de a
et pourtant b->label a aussi changé!
Exécution :
#include <iostream>
#include "PointNomme.cpp"
using namespace std ;
void main()
{ PointNomme *a= new PointNomme(1,1);
PointNomme *b=new PointNomme(*a);
b->label="Bonjour";
a->label="Amine";
cout<<" La chaine de a vaut :"<<a->label<<" et la chaine de b vaut :"<<b->label <<endl;
}
68
Résultat de l’exécution:
Voici un exemple : On considère la classe Point et la classe Segment formée par deux objets
membres origine de classe Point et extremite de classe Point. L’appel des constructeurs
s’effectue de la façon suivante :
class Point
{ public:
Point(int px,int py)
{ x=px; y=py; }
private:
int x;
int y;
};
class Segment
{ Point origine; //Objet membre
Point extremite; //Objet membre
int epaisseur;
public:
Segment(int ox,int oy,int ex,int ey,int ep): origine(ox,oy),extremite(ex,ey)
{ epaisseur=ep; }
};
IV.5 Destructeurs
Tout comme il existe un constructeur, on peut spécifier un destructeur. Ce dernier est
appelé lors de la destruction de l'objet, explicite ou non.
Un destructeur d’une classe donnée est une méthode exécutée automatiquement à
chaque fois qu’une instance de la classe donnée disparait.
69
Exemple:
class Point
{ double x, y ;
int norm ;
public :
// Les constructeurs
Point(double xx, double yy);
Point(int n);
// Le destructeur
~Point();
};
Grâce à la surcharge des noms de fonctions, il peut y avoir plusieurs constructeurs, mais il
ne peut y avoir qu’un seul destructeur.
Le destructeur d’une classe C est appelé implicitement:
A la disparition de chaque objet de classe C.
A la fin du main() pour toutes les variables statiques, locales au main() et les variables
globales.
A la fin du bloc dans lequel la variable automatique C est déclarée.
A la fin d’une fonction ayant un argument de classe C.
Lorsqu’une instance de classe C est détruite par DELETE.
Lorsqu’un objet qui contient un attribut de type classe C est détruit.
Lorsqu’un objet d’une classe dérivée de C est détruit.
Exemple :
Le programme suivant illustre quelques cas:
#include <iostream>
using namespace std ;
class test
{ int attr;
public:
test()
{ cout<<"----- contructeur par defaut!"<<endl; }
~test()
{ cout<<"----- Le destructeur ! "<<endl; }
};
void blocLocal()
{ cout<<"BLOC LOCAL : "<<endl; test CTlocal; }
void main()
{ cout<<"BLOC MAIN : "<<endl;
test *CTmain = new test;
cout<<"Appel du bloc local dans main : "<<endl;
blocLocal ();
cout<<"APPEL DE DELETE : "<<endl;
delete CTmain;
}
Résultat de l’exécution :
70
Chapitre VII: Patrons et amies " Fonctions et Classes "
71
La définition des 3 fonctions (perte de temps, source d’erreur) mène à des
instructions identiques, qui ne sont différenciées que par le type des variables
qu’elles manipulent.
Si on souhaite étendre la définition de cette fonction à de nouveaux types, il faut
définir une nouvelle implantation de la fonction min par type considéré.
Une autre solution est de définir une fonction template, c’est-à-dire générique. Cette
définition définit en fait un patron de fonction, qui est instancié par un type de données (ici
le type T) pour produire une fonction par type manipulé.
Exemple :
template <class T> // T est le paramètre de modèle
T min (T a, T b)
{ if ( a < b)
return a
else
return b;
}
void main(){
int a = min(1, 7); // int min(int, int)
float b = min(10.0, 25.0); // float min(float, float)
char c = min(’z’, ’c’); // char min(char, char)
}
Définition de la fonction min générique : il n’est donc plus nécessaire de définir une
implantation par type de données. On définit donc bien plus qu’une fonction, on définit une
méthode permettant d’obtenir une certaine abstraction en s’affranchissant des problèmes
de type.
Remarques :
Il est possible de définir des fonctions template acceptant plusieurs types de
données en paramètre. Chaque paramètre désignant une classe est alors précédé du
mot-clé class, comme dans l’exemple : template <class T, class U> ....
Chaque type de données paramètre d’une fonction template doit être utilisé dans la
définition de cette fonction.
Pour que cette fonctionnalité soit disponible, les fonctions génériques doivent être
définies au début du programme ou dans des fichiers d’interface (fichiers .h).
VII.1.2 Classe template : patron de classes
Il est possible, comme pour les fonctions, de définir des classes template, c’est-à-dire
paramétrées par un type de données. Cette technique évite ainsi de définir plusieurs classes
similaires pour décrire un même concept appliqué à plusieurs types de données différents.
Elle est largement utilisée pour définir tous les types de containers (comme les listes, les
tables, les piles, etc.), mais aussi des algorithmes génériques par exemple.
La syntaxe permettant de définir une classe template est similaire à celle qui permet de
définir des fonctions template.
72
Exemple :
int x, y ;
public :
point (int abs=0, int ord=0) ;
void affiche () ;
// .....
};
Lorsque nous procédons ainsi, nous imposons que les coordonnées d'un point soient de
valeurs de type int. Si nous souhaitons disposer de points à coordonnées d'un autre type
(float, double, long ...), nous devons définir une autre classe en remplaçant simplement,
dans la classe précédente, le mot clé int par le nom de type voulu.
Ici encore, nous pouvons simplifier considérablement les choses en définissant un seul
patron de classe de cette façon:
Comme dans le cas des patrons de fonctions, la mention template <class T> précise que
l'on a affaire à un patron (template) dans lequel apparaît un paramètre de type nommé T ;
La définition de notre patron de classes n'est pas encore complète puisqu'il y manque la
définition des fonctions membres, à savoir le constructeur point et la fonction affiche().
Pour ce faire, la démarche va légèrement différer selon que la fonction concernée est en
ligne ou non. Voici par exemple comment pourrait être défini notre constructeur en ligne:
point (T abs=0, T ord=0)
{ x = abs ; y = ord ; }
En revanche, lorsque la fonction est définie en dehors de la définition de la classe, il est
nécessaire de rappeler au compilateur :
que, dans la définition de cette fonction, vont apparaître des paramètres de type ;
pour ce faire, on fournira à nouveau la liste de paramètre sous la forme:
template <class T>
le nom du patron concerné. Par exemple, si nous définissons ainsi la fonction affiche,
son nom sera: point<T>::affiche ()
73
Voici ce que pourrait être finalement la définition de notre patron de classe point :
template <class T> class point // Création d'un patron de classe
{ Tx,y;
public :
point (T abs=0, T ord=0)
{ x = abs ; y = ord ; }
void affiche () ;
};
template <class T> void point<T>::affiche ()
{ cout << "Paire : " << x << " " << y << "\n" ; }
Exemple récapitulatif :
Voici un programme complet comportant :
la création d'un patron de classes point dotée d’un constructeur en ligne et d’une
fonction membre (affiche) non en ligne,
la création d’un patron de fonctions min (en ligne dans la patron de classes) qui
retourne le minimum des arguments,
un exemple d'utilisation (main).
74
#include <iostream>
#include <string>
using namespace std ;
T min ()
{ if ( x < y)
return x;
else
return y;
}
};
void main ()
{ point <int> ai (3, 5) ; // T prend la valeur int pour la classe point
ai.affiche() ;
cout << " Min : ... " << ai.min() << endl;
point <char> ac ('z', 't') ; ac.affiche() ;
cout << " Min : ... " << ac.min() << endl;
point <double> ad (1.5, 9.6) ; ad.affiche() ;
cout << " Min : ... " << ad.min() << endl;
point <string> as ("Salut", " A vous") ; as.affiche() ;
cout << " Min : ... " << as.min() << endl;
}
Résultat de l’exécution:
Remarque :
Il faut noter aussi que le nombre de paramètres n'est pas limité à 1, on peut en avoir
plusieurs :
75
template < class A, class B, class C, ... >
class MaClasse
{
// ...
public:
// ...
};
76
Solution:[2]
Rendre publiques les données membres des classes.
Inconvénient: on perd leurs protections.
- Ajout de fonctions d’accès aux membres privés.
Inconvénient : temps d’exécution pénalisant.
- Fonctions amies : Il est possible de déclarer qu’une ou plusieurs fonctions (extérieurs
à la classe), sont des « amies » ; une telle déclaration d’amitié les autorise alors à
accéder aux données privées au même titre que n’importe quelle fonction membre.
L’avantage de cette méthode est de permettre le contrôle des accès au niveau de la
classe concernée.
Exemple de déclaration d’une fonction amie:
class A
{ private :
int i ;
friend class B ;
friend void f();
}
class B
{
//tout ce qui appartient à B,
//peut se servir des données membres privés de A
// comme si elle était de A.
...
}
void f(A& a)
{ a.i = 10 ; }
77
return (1);
else
return (0);
};
void main()
{ point a(1,0), b (1), c ;
if ( coincide (a,b) )
cout<< "A coincide avec B”<<endl;
else
cout<<”A et B sont différents”<<endl;
};
Résultat de l’exécution :
78
VII.2.2.3 Fonction amie de plusieurs classes
Rien n’empêche qu’une même fonction (indépendante ou membre) fasse l’objet de
déclaration d’amitié dans différentes classes.
Toutes les fonctions d’une classe sont amies d’une autre classe
friend class B ; // dans la classe A
VII.2.4 Toutes les fonctions membres d’une classe, amie d’une autre classe
C’est une généralisation du cas précédent. On pourrait d’ailleurs effectuer autant de
déclarations d’amitié qu’il n’y a de fonctions concernées. Mais il est plus simple d’effectuer
une déclaration globale. Ainsi pour dire que toutes les fonctions membres de la classe B sont
amies de la classe A, on placera, dans la classe A, la déclaration [2] :
friend class B ;
Exemple :
class A
{ // partie privée
......
// partie publique
friend class B;
......
};
class B
{ ......
//on a accès totale aux membres privés de tout
//objet de type A
......
};
79
Chapitre VIII: Surcharge d'opérateurs
80
La fonction operator+ doit disposer de deux arguments de types point et fournir une valeur
de retour de même type.
Cette fonction peut être définie en tant que fonction membre de la classe (méthode) ou
fonction indépendante (fonction amie).
81
Exemple:
class point
{ private:
int abs, ord;
public:
point (int abs, int ord);
point operator+ (point a);
void affiche();
};
point point ::operator+ (point a);
{ point p;
p.x = x + a.x;
p.y = y + a.y;
return (p) ;
}
void main()
{
point a (1,2);
point b (2,5);
point c;
c= a+b; c.afficher();
c= a+b+c; c.afficher();
}
Remarque :
Dans ce cas: c = a+b; c = a.operator+ (b);
Dans le 1er cas : a= operator+(a,b);
82
Chapitre IX: HERITAGE EN C++
IX.1 Introduction
IX.1.1 Exemple [7]
Imaginons que nous devions fabriquer un logiciel qui permet de gérer une bibliothèque.
Cette bibliothèque comporte
te plusieurs types de documents
documents;; des livres, des CDs, ou des
DVDs. Une première étude nous amène à mettre en œuvre les classes suivantes:
suivantes
Nous remarquons que dans les trois types de documents, un certain nombre de
caractéristiques se retrouvent systématiquement.
En effet, nous pouvons dire que, d'une façon générale, et quelque soit le ty
type de document,
il comporte au moins un titre, un auteur, etc. il semble aller de soi, que le nom de cette
nouvelle classe générale s'appelle justement Document.
83
référence. En fait, la classe Livre hérite de tout ce qu
quee possède la classe Document, les
attributs comme les méthodes.
Dans cet exemple, nous avons un seul niveau d'héritage, mais il est bien entendu possible
d'avoir une hiérarchie beaucoup plus développée. D'ailleurs, si nous regardons de plus prêt,
nous remarquons
arquons que nous pouvons appliquer une nouvelle fois la généralisation en
factorisant la durée du support CD et du support DVD. En fait, il s'agit dans les deux cas d'un
support commun appelé Multimédia.
IX.1.2 Définitions
L’héritage est une
ne technique permettant de construire une classe à partir d’une ou de
plusieurs autres classes dites : classe mère ou superclasse ou classe de base.
La classe dérivée est appelée: Classe fille ou sous-classe.
Les sous-classes
classes héritent des caract
caractéristiques
éristiques de leurs classes parents.
Les attributs et les méthodes déclarées dans la classe mère sont accessibles dans les
classes fils comme s’ils avaient été déclarés localement.
Créer facilement de nouvelles classes à partir de classes existantes (réutilisation du
code)
84
IX.2 Héritage simple
IX.2.1 Définition: La classe dérivée hérite les attributs et les méthodes d’une seule classe
mère.
Syntaxe:
class ClasseMere
{ //...
};
class ClasseDerivee: <mode> ClasseMere
{ //...
};
mode: optionnel permet d’indiquer la nature de l’héritage: private, protected ou public. Si
aucun mode n’est indiqué alors l’héritage est privé.
Exemple :
class Point
{ int x;
int y;
public:
void initialise (int , int) ;
void afficher() ;
};
void main()
{ Pointcolor p ;
p.initialiser (10,20) ;
p.setcol(5) ;
p.afficher() ; // (10, 20)
}
IX.2.2 Utilisation des membres de la classe de base dans une classe dérivée
L’exemple précédent, destiné à montrer comment s’exprime l’héritage en C++, ne
cherchait pas à explorer toutes les possibilités.
Or la classe pointcolor telle que nous l’avons définie présente des lacunes. Par exemple,
lorsque nous appelons affiche pour un objet de type pointcolor nous n’obtenons aucune
information sur sa couleur.
85
Une première façon d’améliorer cette situation consiste à écrire une nouvelle fonction
membre public :
void Affichercolor()
{ cout << "("<<x<<","<<y<<")"<<endl ;
cout << "couleur="<< couleur<<endl ;
}
Mais alors cela signifierait que la fonction Affichercolor membre de la classe Pointcolor aurait un
accès aux membres privés de point ce qui serait contraire au principe d’encapsulation.
En revanche, rien n’empêche à une classe dérivée d’accéder à n’importe quel membre public
de sa classe de base. D’ou une définition possible d’ Affichercolor:
void Affichercolor()
{ afficher() ;
cout << "couleur="<< couleur<<endl ;
}
D’une manière analogue, nous pouvons définir dans pointcolor une fonction d’initialisation comme
initialisercolor :
void initialisercolor(int a, int b, int c)
{ initialiser(a,b) ;
couleur=c ;
}
Remarque :
Lorsqu’une classe dérivée possède des fonctions amies, ces derniers disposent
exactement des mêmes autorisations d’accès que les fonctions membres de la classe
dérivée. En particulier, les fonctions amies d’une classe dérivée auront bien accès aux
membres déclarés protégés dans sa classe de base.
En revanche, les déclarations d’amitié ne s’héritent pas. Ainsi, si f a été déclaré amie
d’une classe A et si B dérive de A, f n’est pas automatiquement amie de B.
86
Exemple :
int* px(NULL);
px = new int ;
*px = 20 ;
cout<< "*px = "<<*px ;
delete px ;
px = NULL ;
Notes :
Une variable allouée statiquement est désallouée automatiquement (à la fermeture
du bloc).
Une variable (zone mémoire) allouée dynamiquement doit être désallouée
explicitement par le programmeur.
int* px(NULL);
{px = new int(4) ; int n=6 ;}
cout<< "*px = "<<*px ;
delete px ; px = NULL ;
cout<< "*px = "<<*px <<endl;
cout<< "n = "<< n ;
Attention
Si on essaye d’utiliser la valeur pointée par un pointeur pour lequel aucune mémoire n’a été
réservée, une erreur de type Segmentation fault se produira à l’exécution.
int* px;
*px = 20 ; // ! Erreur : px n’a pas été alloué
cout<< "*px = "<<*px<<endl;
Compilation : Ok
Exécution: arrêt programme
(Segmentation fault)
Bonnes pratiques
int* px(NULL);
Initialisez toujours vos pointeurs
if(px != NULL) Utilisez NULL si vous ne connaissez pas
{ *px = 20 ; encore la mémoire pointée au moment de
cout<< "*px = "<<*px<<endl; l’initialisation
}
51
IX.3 Héritage multiple [7]]
La classe dérivée hérite les attributs et les méthodes à partir de plusieurs classes mères.
Exemple:
Personne
Enseignant Chercheur
Enseignant chercheur
Remarque :
L’héritage multiple est possible
ible en C++ mais permet de poser quelques problèmes :
Si les deux classes mères ont des attributs ou des méthodes de même nom (collision
des noms lors de la propagation).
La classe D hérite deux fois les attributs de A, une fois à travers la classe B et ll’autre
fois à travers C.
88
Chapitr
Chapitre V : Classes et objets
54
Point(int abs=0, int ord=0)
{ cout <<"++constr. point: "<<abs<<"," <<ord<<endl ;
x=abs ; y=ord ;
}
~Point()
{ cout <<"-- desctr. point: "<<x<<","<<y<<endl ; }
};
void main()
{ Pointcolor a(10,15,3) ;
Pointcolor b(2,3) ;
Pointcolor c(12) ;
Pointcolor *adr;
adr = new Pointcolor(12,25);
delete adr ;
}
Résultat de l’exécution :
Remarque :
Quelque soit les situations, nous disposons toujours des quatre mêmes phases pour la
création de l'objet et toujours dans le même ordre.
1. Allocation mémoire nécessaire pour contenir tous les attributs que comporte l'objet.
2. Appel du constructeur de la classe dérivée. Ce constructeur est appelé mais pas
encore exécuté. Appel du constructeur de la classe de base spécifié par la liste
d'initialisation en récupérant les bons arguments pour les attributs de la classe de
base.
90
3. Appel et exécution du constructeur de la classe de base
base.. A moins que la classe de base
soit elle-même
même une classe dérivée d'une autre classe de base, les instructions qui
constituent le corps du constructeur sont exécutées.
4. Exécution
n du constructeur de la classe dérivée
dérivée.. Puisque la partie générale est bien
initialisée, nous pouvons nous occuper de la partie spécifique à la classe dérivée. Les
instructions du corps du constructeur sont donc exécutées.
void main()
{ Forme f1(2,3); //1
Cercle c1(15, -1, 50) ; //2
c1.deplacer(3, 4) ; //3
c1.agrandir(10) ; //4
f1=c1 ; //5
f1.deplacer(5, -8) ; //6
c1=f1 ; //7
}
Commentaire:
1. Création de l'objet f1.
2. Création de l'objet c1.
3. Possibilité de déplacer le cercle c1 puisque la méthode a été héritée.
91
4. Agrandissement du cercle c1 grâce à la méthode agrandir définie par la classe Cercle.
7. Tentative d’affectation d'une forme dans un cercle. Nous obtenons également une
erreur de compilation
compilation.. Il s'agit également d'un changement de type. Si ce casting
était toléré, cela voudrait dire que nous autoriserions d'avoir des attributs avec des
valeurs aléatoires !
Effectivement f1 ne dispose pas de rayon, ainsi l'attribut rayon de c2 se retrouverait
sans aucune valeur bien précise. Cette démarche n'est pas tolérée par le compilateur
et nous le comprenons.
92
V.3.4 Objets transmis en argument d'une fonction membre
Nous pouvons maintenant imaginer vouloir comparer deux points, afin de savoir s'ils sont
égaux. Pour cela, nous allons mettre en œuvre une méthode "Coincide" qui renvoie "true"
lorsque les coordonnées des deux points sont égales :
class Points
{ public :
int x;
int y;
bool Coincide(Point p)
{ if( (p.x==x) && (p.y==y) )
return true;
else
return false;
}
};
La solution qui vous vient à l'esprit dans un premier temps est probablement de passer par
un pointeur. Cette solution est possible, mais n'est pas la meilleure, dans la mesure où nous
savons fort bien que ces pointeurs sont toujours sources d'erreurs (lorsqu'ils sont non
initialisés, par exemple).
La vraie solution offerte par le C++ est de passer par des références. Avec ce type de passage
de paramètre, aucune erreur est possible puisque l'objet à passer doit déjà exister (être
instancier). En plus, les références offrent une simplification d'écriture, par rapport aux
pointeurs :
#include <iostream>
class Points
{ public :
int x;
int y;
bool Coincide(Point &p)
{ if( (p.x==x) && (p.y==y) )
return true;
else
return false;
}
};
void main()
{
Points p;
Points pp;
pp.x=0;pp.y=1;
p.x=0; p.y=1;
if( p.Coincide(pp) )
59
void A ::f()
{ cout<< "A::f()" <<endl; }
class B: public A
{ …
public:
void f();
…
};
void B::f()
{ cout<< “B::f()” <<endl; }
void main()
{ A a;
B b;
A *p;
P= &a; p->f(); // affiche A::f()
p=&b; p->f(); // affiche B::f() aussi
}
Remarques :
Le choix de la fonction est maintenant conditionné par le type exact de l’objet pointé par
p connu au moment de l’exécution puisque p nous emmène sur un B* alors on utilise B ::f()
94
~A()
{ delete [] p ;
cout<<"~A()"<<endl ;
}
};
class B : public A
{ int *q;
public :
B()
{ p=new int[20] ;
cout<<"B()"<<endl ;
}
~B()
{ delete [] p ;
cout<<"~B()"<<endl ;
}
};
void main()
{
for (int I =0; I<4;I++)
{ A *pa = new B();
delete pa;
}
}
Affiche ceci :
95
virtual ~A()
{ delete [] p ;
cout<<"~A()"<<endl ;
}
};
A l’exécution, on a ceci:
En C++, nous pouvons toujours définir de telles classes, en déclarant des fonctions
membres virtuelles dont on ne précise pas le contenu dans la classe de base. Seules les
classes de base possèdent alors, éventuellement une description du corps de ces fonctions
virtuelles. On les appelle des fonctions virtuelles pures.
Une fonction virtuelle pure se déclare en remplaçant le corps de la fonction par les
symboles = 0.
Syntaxe :
class NomClasse
{
// …
virtual TypeRetout nomFonction (liste des arguments)=0;
//…
}
Remarque:
Une classe comportant au moins une fonction virtuelle pure est considérée comme
abstraite et il n’est plus possible de déclarer des objets de son type.
Une fonction déclarée virtuelle pure dans une classe de base doit obligatoirement
être redéfinie dans une classe dérivée ou déclarée à nouveau virtuelle pure ; dans ce
dernier cas, la classe dérivée est aussi abstraite.
96
Exemple:
class Figure
{
Public:
virtual float perimetre()=0;
virtual float surface()=0;
virtual void dessiner();
}
class Figure
{ public:
virtual float perimetre()=0;
virtual char * getNom()=0;
};
97
void main()
{
UnRectangle *Rect=new UnRectangle(10,5);
UnCarre *Carre=new UnCarre(10);
Figure *T[2];
T[0]=Rect;
T[1]=Carre;
for (int i=0;i<=1;i++)
{
cout<<"Le
<<"Le PERIMETRE DU "<<T[i]
"<<T[i]->getNom()<<" = "<< T[i]->perimetre()<<endl
endl;
}
}
Mais ici l’on souhaite que les différentes informations puissent être de types différents.
Aussi cherchons-nous
nous à isoler dans une classe (nommé liste)) toutes les fonctionnalités de
gestion de la liste elle-mêmee sans entrer dans les détails spécifiques aux objets concernés.
Nous appliquerons alors ce schéma :
struct element
{ element *suivant ;
mere *contenu ;
};
98
class Liste
{ element *tete ;
public:
Liste() ;
~Liste() ;
void ajouter(mere *) ;
void afficher() ;
.....
};
Exemple :
class Article
{ long code;
char nom[100];
double quantite;
double prix;
public:
virtual void afficher()
{ cout <<"code="<<code<<endl ;
cout <<"Nom="<<nom<<endl ;
cout <<"Quantité="<<quantite<<endl ;
cout <<"Prix unitaire="<<prix<<endl ;
}
virtual void saisir()
{ cout <<"code="<<endl; cin>>code ;
cout <<"Nom="<< endl ; cin >> nom ;
cout <<"Quantité="<< endl ; cin >> quantite ;
cout <<"Prix unitaire="<< endl ; cin>> prix ;
}
};
99
void afficher()
{ cout << "c’est un confiture"<<endl ;
Article ::afficher() ;
cout << "Poids="<<poids<<endl ;
}
void saisir()
{ cout << "c’est un confiture"<<endl ;
Article ::saisir() ;
cout << "Poids= "<<endl ; cin>> poids ;
}
};
typedef struct element
{ element *suivant ;
Article *contenu ;
};
class Liste
{ element *tete ;
public :
Liste()
{ tete=NULL ;}
~Liste()
{ delete tete ;}
void ajouter(Article *a) // au début de la liste
{ element *nouv= new element ;
nouv->contenu=a ;
nouv->suivant=tete ;
tete=nouv ;
}
void afficher()
{ elemnet *parc=tete ;
while(parc !=NULL)
{ parc->contenu->afficher() ;
parc=parc->suivant ;
}
}
};
void main()
{
Confiture c ;
c.saisir() ;
Boissons b ;
b.saisir() ;
Liste L ;
L.ajouter(&c ) ;
L.ajouter(&b ) ;
L.afficher() ; //affichage de toute la liste
}
100
Chapitre XI : Gestion des exceptions
XI.1 Introduction
Les méthodes traditionnelles de gestion d'erreurs d'exécution consistent à:
Traiter localement l'erreur : la fonction doit prévoir tous les cas possibles qui peuvent
provoquer l'erreur et les traiter localement.
Retourner un code d'erreur : la fonction qui rencontre une erreur retourne un code
et laisse la gestion de l'erreur à la fonction appelante.
Arrêter l'exécution du programme (abort,…)
Etc. …
Ainsi, le code d'une fonction s'écrit normalement sans tenir compte des cas particuliers
puisque ces derniers seront traités par le gestionnaire des exceptions.
Une exception levée est interceptée par le gestionnaire d'exceptions correspondant qui
vient juste après le bloc try et qui est formé par un bloc catch.
101
Syntaxe :
catch( type_exception & id_e)
{
// Code de gestion des exceptions levée par un paramètre de type // type_exception
// l'argument id_e est facultatif, il représente le paramètre avec // lequel l'exception a été levée
// le passage de ce paramètre peut être par valeur ou par référence
}
Plusieurs gestionnaires peuvent être placés après le bloc try. Ces gestionnaires diffèrent
par le type de leur argument. Le type de l'argument d'un gestionnaire précise le type de
l'exception à traiter.
Chaque exception levée dans le bloc try, est interceptée par le premier gestionnaire
correspondant (dont le type d'argument correspond à celui de l'exception levée) qui vient
juste après le bloc try.
C++ permet l'utilisation d'un gestionnaire sans argument, dit aussi Le gestionnaire
universel qui permet d'intercepter toutes les exceptions. Ce gestionnaire est normalement
placé à la fin pour intercepter les gestions qui n'ont pas de bloc catch correspondant:
Syntaxe :
catch( ... )
{
// Code de gestion de l'exception levée par un paramètre de type quelconque
}
Exemple [8]:
L'exemple suivant montre une utilisation simple des exceptions en C++.
#include <iostream>
using namespace std;
// classe d'exceptions
class erreur
{ const char * nom_erreur;
public:
erreur ( const char * s):nom_erreur(s){ }
const char * raison()
{ return nom_erreur;}
};
// classe test
class T
{ int _x;
public:
T(int x = 0):_x(x)
{ cout << "\n +++ Constructeur\n";}
~T()
{ cout << "\n --- Destructeur\n";}
void Afficher()
{ cout << "\nAffichage : " << _x << endl;}
};
102
void fct_leve_exception() // fonction qui lève une exception de type 'erreur'
{ cout << "\n-----------entree fonction \n";
throw erreur("Exception de type 'erreur'");
cout << "\n-----------sortie fonction \n";
}
int main()
{ int i;
cout << "entrer 0 pour lever une exception de type classe\n";
cout << " 1 pour lever une exception de type entier\n";
cout << " 2 pour lever une exception de type char\n";
cout << " 3 pour ne lever aucune exception \n";
cout << " ou une autre valeur pour lever une exception de type quelconque \n";
try
{ T t(4);
cout << "---> "; cin >> i;
switch(i)
{ case 0:
fct_leve_exception(); // lance une exception de type // 'erreur'
break;
case 1:
{ int j = 1;
throw j; // lance une exception de type int
}
break;
case 2:
{ char c = ' ';
throw c; // lance une exception de type char
}
break;
case 3:
break;
default:
{ float r = 0.0;
throw r; // lance une exception de type float
}
}
}
/******** Gestionnaires des exceptions : aucune instruction ne doit figurer ici *************/
catch(erreur & e)
{ cout << "\nException : " << e.raison() << endl; }
catch(int & e)
{ cout << "\nException : type entier " << e << endl; }
catch(char & e)
{ cout << "\nException : type char " << e << endl; }
catch(...)
{ cout << "\nException quelconque" << endl; }
return 0;
}
103
XI.3 Système d'exceptions [8]
Voici une implémentation de l'opérateur /= permettant de diviser un complexe par un
flottant quelconque:
complexe& complexe::operator/=(float x)
{ r /= x;
i /= x;
return *this;
}
La fonction se contente de "lancer" un const char*. Celui-ci sera "rattrapé" par une fonction
située dans la pile d'appels (c'est-à-dire la fonction appelante, ou la fonction ayant appelé la
fonction appelante, etc.) par exemple la fonction main, dont voici une première
implémentation:
int main()
{
complexe c(5,6);
try
{ float x;
cout << "Entrez un diviseur: ";
cin >> x;
c /= x;
}
catch ( const char * c )
{ cout << c << "\n"; }
return 0;
}
La fonction main a "attrapé" l'objet envoyé (ici un const char *) et l'a simplement affiché.
La version suivante va plus loin: elle demande à l'utilisateur de rentrer une valeur jusqu'à ce
que celle-ci soit différente de 0.
int main()
{ complexe c(5,6);
do
{ try
{ float x;
cout << "Entrez un diviseur: "; cin >> x;
104
c /= x;
break;
}
catch ( const char * msg )
{ cout << msg << " Recommencez\n"; }
} while (true);
return 0;
}
On voit donc ici que si le traitement de l'erreur (dans la fonction main) a changé, la
génération de l'erreur, elle, est la même. Le code suivant montre une troisième manière de
procéder: tout le traitement d'erreur se fait ici au niveau de la fonction input_et_divise:
void input_et_divise(complexe& c)
{ do
{ try
{ float x;
cout << "Entrez un dividende: ";
cin >> x;
c /= x;
break;
}
catch ( const char * msg )
{ cout << msg << " Recommencez\n"; }
} while (true);
}
int main()
{ complexe c(5,6);
input_et_divise(c);
cout << "Partie reelle : " << c.get_r() << "\n";
cout << "Partie imaginaire: " << c.get_i() << "\n";
}
105
Nom Dérive de Constructeur Signification
Problème d'allocation
mémoire, peut être
bad_alloc Exception bad_alloc() lancée par l'opérateur new
Problème d'entrées-sorties,
d'entrées
peut être lancé
ios_base::failure Exception failure(const string&)
par les fonctions d'entrées-
d'entrées
sorties
106
domain_error(const Erreur de domaine (au sens
domain_error logic_error string&) mathématique du terme).
Exemple: division par 0
invalid_argument logic_error invalid_argument(const Mauvais argument passé à
string&) une fonction
Vous avez voulu créer un
objet trop grand
length_error logic_error length_error(const pour le système (par
string&) exemple une chaîne
plus longue que
std::string::max_size()
Il est très simple d'utiliser ces exceptions dans votre programme. L'opérateur
précédent peut être réécrit de la manière suivante:
complexe& complexe::operator/=(float x)
{ if ( x == 0 )
{ domain_error e ("division par zero" );
throw (e);
}
r /= x;
i /= x;
return *this;
}
ou encore, de manière plus concise:
complexe& complexe::operator/=(float x)
{ if ( x == 0 )
{ throw domain_error( "division par zero" ); }
r /= x;
i /= x;
return *this;
}
Le traitement d'erreur première manière s'écrira cette fois:
int main()
{ complexe c(5,6);
try
{ float x;
cout << "Entrez un diviseur: "; cin >> x;
c /= x;
}
catch ( exception & e )
{ cout << e.what() << "\n"; }
return 0;
}
107
Le traitement d'erreur troisième manière s'écrira comme indiqué ci-dessous. Si une
exception de type domain_error est attrapée par la fonction input_et_divise, elle la
traite. Si une autre exception dérivant du type générique exception est émise, elle ne
sera pas attrapée par input_et_divise, mais elle sera traitée de manière générique
par main.
void input_et_divise(complexe& c)
{ do
{ try
{ float x;
cout << "Entrez un diviseur: ";
cin >> x;
c /= x;
break;
}
catch ( const domain_error& e )
{ cout << e.what() << " Recommencez\n"; }
} while (true);
}
int main()
{ complexe c(5,6);
try
{ input_et_divise(c);
cout << "Partie reelle : " << c.get_r() << "\n";
cout << "Partie imaginaire: " << c.get_i() << "\n";
}
catch ( const exception& e )
{ cout << e.what() << "\n"; }
}
108
Références
109
[14] Programmation JAVA: Héritage simple, Emmanuel Remy.
http ://programmation-java.1sur1.com/c++/pdf/heritagesimple.pdf
[15] Apprendre le C++, Claude Delannoy,2007.
[16] Programmation C++, la classe string, Fresnel.
http ://www.fresnel.fr/perso/stout/langage_c/chap_12_la_classe_string.pdf
[17] Programmation objet en langage C++, Guidet A., 2008
[18] C++ Demystified: A Self-Teaching Guide, Kent J., 2004
[19] Comment programmer en C++, Introduction à la Conception Orientée Objets avec
l’UML, Deitel et Deitel, 2003
[20] Programmer en langage C++, Delannoy C., 2000
[21] L’essentiel du C++, Lippman S. B. 1999
[22] Le langage C++, Stroustrup B. 1998
[23] The C++ Programming Language Stroustrup B., 1997
[24] Programming in C++, D’orazio T., 2009
[25] Programmation Orientée Objets, cours/exercices en UML avec C++, Bersini H., 2009
110