Vous êtes sur la page 1sur 32

Cours POO et C ++

Chapitre2 : Les entrées/sorties et les fonctions C++ 11

1. Bibliothèque d’entrée/sortie :

Les opérations standards d’entrée et de sortie sont fournies par trois flots (streams), désignés
par les variables suivantes :
 cin (ou std ::cin) : désigne le flot d’entrée standard (typiquement, votre clavier),
 cout (ou std ::cout): désigne le flot de sortie standard (typiquement, la fenêtre
d’exécution sur votre écran),
 cerr (ou std ::cerr): désigne le flot standard des messages d’erreur.

Pour utiliser les utiliser les entrées/sortie standard il faut inclure le fichier d’entête « iostream »
#include <iostream>
Les opérateurs « << » et «>> » assurent :
 le transfert de l’information
 le formatage de l’information
L’opérateur « << », également appelée opérateur d’insertion, sera utilisé pour réaliser des
écritures sur un flux de données, tandis que l’opérateur « >> », ou opérateur d’extraction,
permettra de réaliser la lecture d’une nouvelle donnée dans le flux d’entrée. Ces deux opérateurs
renvoient tous les deux le flux de données utilisé, ce qui permet de réaliser plusieurs opérations
d’entrée / sortie successivement sur le même flux.
Exemple :
#include <iostream>
using namespace std;
int main(void)
{
int i;
// Lit un entier :
cin >> i;
// Affiche cet entier et le suivant :
cout << i << " " << i+1 << endl;
return 0;
}

De plus, la librairie standard définie ce que l’on appelle des manipulateurs permettant de réaliser
des opérations simples sur les flux d’entrée / sortie. Le manipulateur le plus utilisé est sans nul

10
doute le manipulateur endl, qui, comme son nom l’indique, permet de signaler une fin de ligne
et d’effectuer un retour de chariot lorsqu’il est employé sur un flux de sortie.
Bien entendu, la bibliothèque iostream fournit de nombreuses autres fonctionnalités
d’entrée/sortie, et la bibliothèque fstream fournit les fonctionnalités de manipulation de
fichiers.

Un tampon, également appelé cache, est une zone mémoire dans laquelle les opérations
d’écriture et de lecture se font et dont le contenu est mis en correspondance avec les données
d’un média physique sous-jacent. Les mécanismes de cache ont essentiellement pour but
d’optimiser les performances des opérations d’entrée / sortie. En effet, l’accès à la mémoire
cache est généralement beaucoup plus rapide que l’accès direct aux supports physiques ou aux
médias de communication. Les opérations effectuées par le programme se font donc, la plupart
du temps, uniquement au niveau du tampon, et ce n’est que dans certaines conditions que les
données du tampon sont effectivement transmises au média physique. Le gain en performance
peut intervenir à plusieurs niveaux. Les cas les plus simples étant simplement lorsqu’une
donnée écrite est écrasée peu de temps après par une autre valeur (la première opération
d’écriture n’est alors jamais transmise au média) ou lorsqu’une donnée est lue plusieurs fois (la
même donnée est renvoyée à chaque lecture). Bien entendu, cela suppose que les données
stockées dans le tampon soient cohérentes avec les données du média, surtout si les données
sont accédées au travers de plusieurs tampons. Tout mécanisme de gestion de cache permet
donc de vider les caches (c’est-à-dire de forcer les opérations d’écriture) et de les invalider
(c’est-à-dire de leur signaler que leurs données sont obsolètes et qu’une lecture physique doit
être faite si on cherche à y accéder).
Les mécanismes de mémoire cache et de tampon sont très souvent utilisés en informatique, à
tous les niveaux. On trouve des mémoires cache dans les processeurs, les contrôleurs de disque,
les graveurs de CD, les pilotes de périphériques des systèmes d’exploitation et bien entendu
dans les programmes.
Chacun de ces caches contribue à l’amélioration des performances globales en retardant au
maximum la réalisation des opérations lentes et en optimisant les opérations de lecture et
d’écriture (souvent en les effectuant en groupe, ce qui permet de réduire les frais de
communication ou d’initialisation des périphériques). Il n’est donc absolument pas surprenant
que la librairie standard C++ utilise elle aussi la notion de tampon dans toutes ses classes
d’entrée / sortie...
 les manipulateurs « hex », « oct » et « dec » permettent la réalisation des entrées/sorties

11
des entiers respectivement en base hexadécimale, en base octale et en base décimale.
 Les manipulateurs « fixed », « scientific » activent la représentation respective en
virgule fixe et virgule flottante des nombres flottant (réels).
Ces manipulateurs ne nécessitent pas l’importation du fichier d’entête « iomanip ».
Exemple :
#include <iostream>
using namespace std;

int main() {
int n = 75;
double d = 34.56789120654987;
cout << n << endl;
cout << hex << n << endl;
cout << oct << n << endl;
cout << dec << n << endl;
cout << d << endl;
cout << scientific << d << endl;
cout << fixed << d << endl;
return 0;
}

 Le manipulateur « setbase(base) » joue le même rôle que les manipulateurs « hex »,


« oct » et « dec » qui ne sont réellement que des alias de celui-ci.
int a = 195;
cout << "valeur en base 16 = " << setbase(16) << a << endl;
//affiche : valeur en base 16 = C3
cout << "valeur en base 10 = " << setbase(10) << a << endl;
//affiche : valeur en base 10 = 195
cout << "valeur en base 8 = " << setbase(8) << a << endl;
//affiche : valeur en base 8 = 303

 Le manipulateur « setw(widh) » : indique que les prochaines données doivent être


écrites sur les nombres de caractères indiqués en ajoutant des caractères espaces avant.
int a = 15;
cout << "valeur de a = " << setw(5) << a << endl;
//affiche : la valeur de a = 15

 Le manipulateur « setfill(char) » permet de spécifier le caractère à utiliser pour


compléter les données utilisant le manipulateur « setw »

12
int a = 15;
cout << "valeur de a = " << setfill(‘x’) << setw(5) << a << endl;
//affiche : la valeur de a = xxx15

 Le manipulateur « setprecision(digits) » spécifie que les prochains nombres à virgule


flottante doivent êtres écrits avec la précision donnée (le nombre de chiffres à écrire
après la virgule)
double f = 3.56789 ;
cout << setprecision(4) << f << endl;
//affiche : 3.5679
cout << setprecision(10) << f << endl;
//affiche : 3.5678900000

2. Les fonctions en C++ :

Le C++ ne permet de faire que des fonctions, pas de procédures. Une procédure peut être faite
en utilisant une fonction ne renvoyant pas de valeur (ou en ignorant la valeur retournée).
La définition des fonctions se fait comme suit :
type identificateur(paramètres)
{
... /* Instructions de la fonction. */
}
Où :
 type est le type de la valeur renvoyée,
 identificateur est le nom de la fonction,
 et paramètres est une liste de paramètres.

Toute fonction doit être déclarée avant d’être appelée pour la première fois (prototype). La
définition d’une fonction peut faire office de déclaration.
Il peut se trouver des situations où une fonction doit être appelée dans une autre fonction définie
avant elle. Comme cette fonction n’est pas définie au moment de l’appel, elle doit être déclarée.
Le rôle des déclarations est donc de signaler l’existence des fonctions aux compilateurs afin de
les utiliser, tout en reportant leur définition de ces fonctions plus loin ou dans un autre fichier.
La syntaxe de la déclaration d’une fonction (ou prototype) est la suivante :

13
type identificateur (paramètres) ;
Exemple :
int Min(int a, int b); /* Déclaration de la fonction minimum
*/
/* Prototype */
/* Fonction principale. */
int main(void)
{
int i = Min(2,3); /*Appel à la fonction Min, déjà déclarée.*/
return 0;
}
/* Définition de la fonction min. */
int Min(int i, int j)
{
if (i<j) return i;
else return j;
}
 valeur par défaut :
En C++, il est possible de donner des valeurs par défaut aux paramètres dans une déclaration,
et ces valeurs peuvent être différentes de celles que l’on peut trouver dans une autre déclaration.
Dans ce cas, les valeurs par défaut utilisées sont celles de la déclaration visible lors de l’appel
de la fonction.
Exemple :
#include <iostream>

using namespace std;


int nombresDeSecondes(int heures, int minutes = 0, int secondes = 0);
int main() {
cout << "Hello, World!" << endl;
cout << nombresDeSecondes(3) << endl;
return 0;
}
int nombresDeSecondes(int heures, int minutes, int secondes)
{
int total = 0;
total = heures * 3600;
total += minutes * 60;
total += secondes;
return total;
}

 Passage de paramètre par référence : passer un paramètre par référence a les mêmes
avantages que le passage d’un paramètre par pointeur. Celui-ci est également
modifiable. La différence est que l’opérateur de référencement n’est pas utilisé car il est
déjà d’une référence.
Le passage de paramètre par référence utilise la même syntaxe similaire au passage par pointeur

14
dans la déclaration de la fonction, en utilisant « & » au lieu de « * ».
Exemple
#include <iostream>

using namespace std;


void incrementer(int& val);

int main() {
int a = 5;
cout << " a =" << a << endl;
incrementer(a);
cout << " a =" << a << endl;
return 0;
}
void incrementer (int& val)
{
val++;
}

 Fonction de type « inline » :


Le C++ dispose du mot-clé inline, qui permet de modifier la méthode d’implémentation des
fonctions. Placé devant la déclaration d’une fonction, il propose au compilateur de ne pas
instancier cette fonction. Cela signifie que l’on désirerait que le compilateur remplace l’appel
d’une fonction par le code correspondant.
Si la fonction est grosse ou si elle est appelée souvent, le programme devient plus gros, puisque
la fonction est réécrite à chaque fois qu’elle est appelée. En revanche, il devient nettement plus
rapide, puisque les mécanismes d’appel de fonctions, de passage des paramètres et de la valeur
de retour sont ainsi évités.
De plus, il faut connaître les restrictions des fonctions inline :
 elles ne peuvent pas être récursives ;
 elles ne sont pas instanciées, donc on ne peut pas faire de pointeur sur une fonction
inline.
Exemple :
#include <iostream>

using namespace std;


inline int Max (int , int);
int main() {

cout << Max (5,12) << endl;


return 0;
}
inline int Max(int i, int j)
{
if (i>j)

15
return i;
else
return j;
}

 Surcharge des fonctions :


Il est interdit en C de définir plusieurs fonctions qui portent le même nom. En C++, cette
interdiction est levée, moyennant quelques précautions. Le compilateur peut différencier deux
fonctions en regardant le type des paramètres qu’elle reçoit. La liste de ces types s’appelle la
signature de la fonction.
En revanche, le type du résultat de la fonction ne permet pas de l’identifier, car le résultat peut
ne pas être utilisé ou peut être converti en une valeur d’un autre type avant d’être utilisé après
l’appel de cette fonction.
Il est donc possible de faire des fonctions de même nom (on dit que ce sont des fonctions
surchargées) si et seulement si toutes les fonctions portant ce nom peuvent être distinguées par
leurs signatures.
La fonction qui sera appelée sera choisie parmi les fonctions de même nom, et ce sera celle dont
la signature est la plus proche des valeurs passées en paramètre lors de l’appel.
Exemple :
#include <iostream>
using namespace std;

int double_it(int entier) {


cout << "Appel de double_it pour un entier." << endl;
return 2*entier;
}

double double_it(double double_entier) {


cout << "Appel de double_it pour un double." << endl;
return 2*double_entier;
}

int main () {
int int_var = 6;
double dbl_var = 6.1;
cout << int_var << " est la moitié de : " << double_it(int_var) << endl;
cout << dbl_var << " est la moitié de : " << double_it(dbl_var) << endl;

return 0;
}
Le choix de la fonction appelée est fait en fonction du type des paramètres. Pour la valeur 6 la
fonction utilisée est la première car 6 est un entier. Ensuite, pour 6.1, qui peut être représenté
par un double, c'est la deuxième forme de double_it qui est utilisée.

16
 Fonction avec expression constante :
Le standard C++11 généralise la notion d'expressions constantes pour inclure les appels à des
fonctions simples (fonctions constexpr).
Une fonction est une fonction expression constante si :
 Elle renvoie une valeur (le type void n'est pas autorisé)
 Le corps de la fonction est composé d'une seule instruction, l'instruction return :
return expr;
 Elle est déclarée avec le mot clé constexpr.
Exemple :
//fonction à expression constante
constexpr int add(int x, int y) {
return x + y;
}
// Constante calculée à base d’une fonction à expression constante
constexpr double var = 2 * add(3, 4);

 Pointeur sur fonction :


Les pointeurs de fonction sont des pointeurs comme des pointeurs de variables, mais qui
pointent sur l’adresse d’une fonction. On doit garder à l’esprit qu’un programme qui est exécuté
prend un certain espace dans la mémoire principale. L’exécutable et les variables utilises sont
tous deux placés dans la mémoire principale.
Ainsi une fonction dans le code du programme est comme, par exemple un champ de caractère,
rien de plus qu’une adresse. La seule chose importante est comment l’on décrit, ou comment le
compilateur décrit sur quelle zone mémoire pointe le pointeur.
Il y est possible par exemple de passer des pointeurs de fonctions en paramètre à une fonction,
ou de retourner un pointeur de fonction.
Voici trois exemples de pointeurs de fonction :

int (*pf1) (void);


pf1 pointe sur une fonction qui a int pour type de retour et qui n'a pas de paramètres.

double (*pf2) (double, double);


pf2 pointe sur une fonction qui renvoie un double et qui a deux double en paramètres.

void (*pf3) ();

17
pf3 pointe sur une fonction qui ne renvoie rien. On ne connait pas les paramètres de la fonction
(cela vient de l'ancienne formulation des fonctions en C).
Dans la définition du pointeur de fonction, la parenthèse est obligatoire (la priorité de l'opérateur
* est trop faible).
Prenons par exemple, la fonction suivante :

double max (double a, double b) {


return (a>b)? a:b;
}

On associe pointeur de fonction et fonction comme cela : (les deux lignes sont équivalentes)
pf2 = max /* forme la plus portable */
pf2 = &max; /* forme la plus "cohérente" */
Maintenant (*pf2) (10.5,21.0); , pf2(10.5,21.0) et max(10.5,21.0); donnent le même résultat.
La forme avec * est préconisée.
Prototype d'une fonction qui a pour paramètre un pointeur de fonction :
void appliquer (double, double, double (*)(double, double));
void appliquer (double a, double b, double (*pf) (double, double));

Exemple :
Dans l’exemple suivant, on s’intéresse à la tâche qui effectue l’une des quatre opérations
arithmétiques basiques.
// 1.2 Exemple introductif : Comment remplacer la commande switch ?
// Réaliser une des quatre opérations basiques spécifiées par les caractères ’+’, ’-’ ’*’ ou ’/’
// Les quatre opérations arithmétiques une de ces fonctions est sélectionnée par une routine //
// contenant l’une commande switch ou un pointeur de fonction
float Plus (float a, float b) { return a+b; }
float Minus (float a, float b) { return a-b; }
float Multiply (float a, float b) { return a*b; }
float Divide (float a, float b) { return a/b; }
// solution utilisant la commande switch (opCode) spécifie quel opération exécuter
void Switch(float a, float b, char opCode)
{
float result;

18
switch(opCode)
{
case ’+’ : result = Plus (a, b); break;
case ’-’ : result = Minus (a, b); break;
case ’*’ : result = Multiply (a, b); break;
case ’/’ : result = Divide (a, b); break;
}
cout << "switch: 2+5=" << result << endl;
// Affiche le résultat
}
// Solution avec les pointeurs de fonction (pt2Func) est un pointeur de fonction qui pointe sur
// une fonction qui prend deux nombres à virgule flottante en argument et qui retourne un
nombre à virgule flottante. Le pointeur de fonction ‘‘spécifie’’ quelle opération exécuter.
void Switch_With_Function_Pointer(float a, float b, float
(*pt2Func)(float, float))
{
float result = pt2Func(a, b);
cout << "switch replaced by function pointer: 2-5=";
// Affiche le resultat
cout << result << endl;
}
// exécute le code d’exemple
void Replace_A_Switch()
{
cout << endl << "Executing function ’Replace_A_Switch’" << endl;
Switch(2, 5, ’+’);
Switch_With_Function_Pointer(2, 5, &Minus);
}
Le template std::function est un adaptateur générique de fonction. En d'autres termes,
une instance de std::function permet de manipuler une cible appelable (i.e. pour laquelle
operator() est défini) , par exemple : une lambda (fonction anonyme), un bind (application
partielle de fonction), un foncteur (objet-fonction), une fonction...

 Lambda calcul (fonctions anonymes):

19
En C++11, une expression lambda, souvent appelée lambda, est un moyen pratique de définir
un objet de fonction anonyme à l'emplacement où il est appelé ou passé comme argument à une
fonction. Les expressions lambda sont généralement utilisées pour encapsuler quelques lignes
de code qui sont passées à des algorithmes ou méthodes asynchrones.
Syntaxe générale :
[capture](parametres)->type-retour{corps}
[capture](parametres){corps}
[capture]{corps}
Exemple :
#include <iostream>
using namespace std;

int main() {

auto func = [] { cout << "Hello, World!" << endl; };


func();
return 0;
}

Exemple :
// function example
#include <iostream> // std::cout
#include <functional> // std::function, std::negate

// a function:
int half(int x) {return x/2;}

// a function object class:


struct third_t {
int operator()(int x) {return x/3;}
};

// a class with data members:


struct MyValue {
int value;
int fifth() {return value/5;}
};

20
int main () {
std::function<int(int)> fn1 = half; // function
std::function<int(int)> fn2 = &half; // function pointer
std::function<int(int)> fn3 = third_t(); // function object
std::function<int(int)> fn4 = [](int x){return x/4;}; // lambda expression
std::function<int(int)> fn5 = std::negate<int>(); // standard function
object

std::cout << "fn1(60): " << fn1(60) << '\n';


std::cout << "fn2(60): " << fn2(60) << '\n';
std::cout << "fn3(60): " << fn3(60) << '\n';
std::cout << "fn4(60): " << fn4(60) << '\n';
std::cout << "fn5(60): " << fn5(60) << '\n';

// stuff with members:


std::function<int(MyValue&)> value = &MyValue::value; // pointer to data
member
std::function<int(MyValue&)> fifth = &MyValue::fifth; // pointer to member
function

MyValue sixty {60};

std::cout << "value(sixty): " << value(sixty) << '\n';


std::cout << "fifth(sixty): " << fifth(sixty) << '\n';

return 0;
}

21
Chapitre 3 : Notions de base en Programmation Orientée
Objets

1. Notion d’objet et classe :

Théoriquement, il y a une nette distinction entre les données et les opérations qui leur sont
appliquées. En tout cas, les données et le code ne se mélangent pas dans la mémoire de
l’ordinateur, sauf cas très particuliers.
Cependant, l’analyse des problèmes à traiter se présente d’une manière plus naturelle si l’on
considère les données avec leurs propriétés. Les données constituent les variables, et les
propriétés les opérations qu’on peut leur appliquer. De ce point de vue, les données et le code
sont logiquement inséparables, même s’ils sont placés en différents endroits de la mémoire de
l’ordinateur.

Ces considérations conduisent à la notion d’objet. Un objet est un ensemble de données et sur
lesquelles des procédures peuvent être appliquées. Ces procédures ou fonctions applicables
aux données sont appelées méthodes. La programmation d’un objet se fait donc en donnant les
données de l’objet et en définissant les procédures qui peuvent lui être appliquées.

Il se peut qu’il y ait plusieurs objets identiques, dont les données ont bien entendu des valeurs
différentes, mais qui utilisent le même jeu de méthodes. On dit que ces différents objets
appartiennent à la même classe d’objet. Une classe constitue donc une sorte de type, et les
objets de cette classe en sont des instances.

La classe définit donc la structure des données, alors appelées champs ou variables d’instances,
que les objets correspondants auront, ainsi que les méthodes de l’objet. À chaque instanciation,
une allocation de mémoire est faite pour les données du nouvel objet créé. L’initialisation de
l’objet nouvellement créé est faite par une méthode spéciale, le constructeur. Lorsque l’objet
est détruit, une autre méthode est appelée : le destructeur. L’utilisateur peut définir ses propres
constructeurs et destructeurs d’objets si nécessaire.

22
Figure1 : exemple d’objet

2. Déclaration d’une classe :

class nomClasse
{
private :
// Déclarations des données et fonctions-membres privées

public :
// Déclarations des données et fonctions-membres publiques
} ;

Exemple :
//fichier « Etudiant.h »
class Etudiant{
private:
char nom[20];
int age;
float note_DS, note_Exam, moyenne_mat;
public:
float Moyenne ();
void Affiche();
};

//fichier « Etudiant.cpp »
23
#include <iostream>
#include "Etudiant.h"
using namespace std;

Etudiant::Etudiant() {
cout << "donner le nomm :"<< endl;
cin >> Etudiant::nom;
cout << "donner note DS :"<< endl;
cin >> Etudiant::note_DS;
cout << "donner note Examen :"<< endl;
cin >> Etudiant::note_Exam;
}
float Etudiant::Moyenne() {
moyenne_mat = (note_DS + (2 * note_Exam)) / 3.0;
return moyenne_mat;
}
void Etudiant::Affiche() {
cout << "la moyenne de " << Etudiant::nom << " est :" <<
Etudiant::moyenne_mat;
}

Démarche « DDU » :
En C++, la programmation d’une classe se fait en trois phases : déclaration, définition,
utilisation.
 Déclaration : c’est la partie interface de la classe. Elle se fait dans un fichier dont le
nom se termine par .h
 Définition : c’est la partie implémentation de la classe. Elle se fait dans un fichier dont
le nom se termine par .cpp. Ce fichier contient les définitions des fonctions-
membres de la classe, c’est-à-dire le code complet de chaque fonction.
 Utilisation : elle se fait dans un fichier dont le nom se termine par.cpp (programme
principal)
Rappelons que la directive d’inclusion #include permet d’inclure un fichier de déclarations
dans un autre fichier : on écrira #include <bib.h> s’il s’agit d’un fichier standard livré
avec le compilateur C++, ou #include "untel.h" s’il s’agit d’un fichier écrit par nous-
mêmes.

24
Figure 2 : démarche DDU

3. Encapsulation des données :

Les données des objets peuvent être protégées : c’est-à-dire que seules les méthodes de l’objet
peuvent y accéder. Ce n’est pas une obligation, mais cela accroît la fiabilité des programmes.
Si une erreur se produit, seules les méthodes de l’objet doivent être vérifiées. De plus, les
méthodes consti-tuent ainsi une interface entre les données de l’objet et l’utilisateur de l’objet
(un autre programmeur).
Cet utilisateur n’a donc pas à savoir comment les données sont gérées dans l’objet, il ne doit
utiliser que les méthodes. Les avantages sont immédiats : il ne risque pas de faire des erreurs
de programmation en modifiant les données lui-même, l’objet est réutilisable dans un autre
programme parce qu’il a une interface standardisée, et on peut modifier l’implémentation
interne de l’objet sans avoir à refaire tout le programme, pourvu que les méthodes gardent le
même nom et les mêmes paramètres.
Cette notion de protection des données et de masquage de l’implémentation interne aux
utilisateurs de l’objet constitue ce que l’on appelle l’encapsulation.
Pour réaliser l’encapsulation, on utilise les mots-clés suivants :
 public : les accès sont libres ;

25
 private : les accès sont autorisés dans les fonctions de la classe seulement ;
 protected : les accès sont autorisés dans les fonctions de la classe et de ses
descendantes seulement. Le mot-clé protected n’est utilisé que dans le cadre de
l’héritage des classes (le prochain chapitre détaillera ce point).

Par défaut les membres d’une classe sont privés.


On accède aux membres des objets en C++ comme on accède aux membres des structures en C
à travers l’opérateur point « . ».
D’autre part, dans la définition d’une fonction-membre, on doit ajouter <nom_classe>::
devant le nom de la fonction. Etudiant::Moyenne()
Les règles suivantes sont appliquées pour identifier l’accès aux attributs et méthodes membres :
 Quand des membres d'un objet apparaissent dans une expression écrite dans une
fonction du même objet on dit que ce dernier fait un accès à ses propres membres.
 Un membre public d'une classe peut être accédé partout où il est visible ;
 un membre privé ne peut être accédé que depuis une fonction membre de la classe

4. Constructeur et destructeur :

Le constructeur se définit comme une méthode normale. Cependant, pour que le compilateur
puisse la reconnaître en tant que constructeur, les deux conditions suivantes doivent être
vérifiées :
 elle doit porter le même nom que la classe ;
 elle ne doit avoir aucun type, pas même le type void.
Un constructeur est appelé automatiquement lors de l’instanciation de l’objet.
Les constructeurs pourront avoir des paramètres. Ils peuvent donc être surchargés. Cela signifie
qu’en pratique, on connaît le contexte dans lequel un objet est créé.
Les constructeurs qui ne prennent pas de paramètres, ou dont tous les paramètres ont une valeur
par défaut, remplacent automatiquement les constructeurs par défaut définis par le
compilateur lorsqu’il n’y a aucun constructeur dans les classes.

Exemple :
class Point {
private:
int x, y;

26
public:
Point::Point(int a, int b) {
//validation de a et b
x = a; y = b;
}
Point::Point(int a) {
//validation de a
x = a; y = 0;
}
Point::Point() {
x = y = 0;
}

};

Un constructeur est toujours appelé lorsqu'un objet est créé, soit explicitement, soit
implicitement. Les appels explicites peuvent être écrits sous deux formes :
Point a(3, 4);
Point b = Point(5, 6);
Dans le cas d'un constructeur avec un seul paramètre, on peut aussi adopter une forme qui
rappelle l'initialisation des variables de types primitifs.
Point e = 7; // équivaut à : Point e = Point(7)

27
Un objet alloué dynamiquement est lui aussi toujours initialisé, au mois implicitement. Dans
beaucoup de cas il peut, ou doit, être initialisé explicitement. Cela s'écrit :
Point *pt;
...
pt = new Point(1, 2);
Les constructeurs peuvent aussi être initialisés pour initialiser des objets temporaires,
anonymes. En fait, chaque fois qu'un constructeur est appelé, un objet nouveau est créé, même
si cela ne se passe pas à l'occasion de la définition d'une variable.
L'appel d'un constructeur dans une expression comportant un signe « = » peut prêter à
confusion, à cause de sa ressemblance avec une affectation. Or, en C++, l'initialisation et
l'affectation sont deux opérations distinctes, du moins lorsqu'elles concernent des variables d'un
type classe : l'initialisation consiste à donner une première valeur à une variable au moment où
elle commence à exister ; l'affectation consiste à remplacer la valeur courante d'une variable
par une autre valeur ; les opérations mises en œuvre par le compilateur, constructeur dans un
cas, opérateur d'affectation dans l'autre, ne sont pas les mêmes.
Point a;
Point b = Point(5, 6);
a = b;

 Constructeur par copie


Le constructeur par copie d'une classe C est un constructeur dont le premier paramètre est de
type « C & » (référence sur un C) ou « const C & » (référence sur un C constant) et dont les
autres paramètres, s'ils existent, ont des valeurs par défaut. Ce constructeur est appelé lorsqu'un
objet est initialisé en copiant un objet existant. Cela arrive parfois explicitement, mais souvent

28
implicitement, notamment chaque fois qu'un objet est passé comme paramètre par valeur à une
fonction ou rendu comme résultat par valeur (c.-à-d. autre qu'une référence) d'une fonction.
Si le programmeur n'a pas défini de constructeur de copie pour une classe, le compilateur
synthétise un constructeur par copie consistant en la recopie de chaque membre d'un objet dans
le membre correspondant de l'autre objet. Si ces membres sont de types primitifs ou des
pointeurs, cela revient à faire la copie « bit à bit » d'un objet sur l'autre.
Exemple :
A titre d'exemple le programme suivant introduit une nouvelle variété de point ; à chacun est
associée une étiquette qui est une chaîne de caractères :
class PointNomme {
private:
int x, y;
char *label;

public:
PointNomme(int a, int b, char *s = "") {
x = a; y = b;
label = new char[strlen(s) + 1];
strcpy(label, s);
}

};

Soit a est une variable de type Point préalablement construite.


Point b = a;

Bien entendu, la copie du pointeur n'a pas dupliquée la chaîne pointée : les deux objets, l'original
et la copie, partagent la même chaîne. Très souvent, ce partage n'est pas souhaitable, car difficile
à gérer et dangereux : toute modification de l'étiquette d'un des deux points se répercutera

29
immédiatement sur l'autre.
Pour résoudre ce problème il faut équiper notre classe d'un constructeur par copie :
class PointNomme {
...
PointNomme(PointNomme &p) {
x = p.x; y = p.y;
label = new char[strlen(p.label) + 1];
strcpy(label, p.label);
}
...
};

L’appel se fait donc :


b = PointNomme(a) ;

 Construction des objets membres


Lorsque des membres d'une classe sont à leur tour d'un type classe on dit que la classe a des
objets membres.
L'initialisation d'un objet de la classe nécessite alors l'initialisation de ces objets membres. Il en
est toujours ainsi, indépendamment du fait que l'on emploie ou non un constructeur explicite,
et qu'à son tour ce constructeur appelle ou non explicitement des constructeurs des objets
membres.
Lorsque les objets membres n'ont pas de constructeurs par défaut, une syntaxe spéciale permet
de préciser les arguments des constructeurs des membres :
NomDeLaClasse(paramètres)
: membre(paramètres), ... membre(paramètres) {
corps du constructeur
}
A titre d'exemple, imaginons que notre classe Point ne possède pas de constructeur sans
arguments, et qu'on doive définir une classe Segment ayant deux points pour membres (un
segment est déterminé par deux points).
Voici comment on devra écrire son constructeur :
class Segment {
30
Point origine, extremite;
int epaisseur;
public:
Segment(int ox, int oy, int ex, int ey, int ep)
: origine(ox, oy), extremite(ex, ey) {
epaisseur = ep;
}
...
};

Autre version :
class Segment {
...
Segment(int ox, int oy, int ex, int ey, int ep) {
origine = Point(ox, oy); // Version erronée
extremite = Point(ex, ey);
epaisseur = ep;
}
...
};

Mais il faut comprendre que cette version est très maladroite, car faite de deux affectations
(les deux lignes qui forment le corps du constructeur ne sont pas des déclarations). Ainsi, au
lieu de se limiter à initialiser les membres origine et extrémité, on procède successivement à
 la construction d’origine et extrémité en utilisant le constructeur sans arguments de la
classe Point,
 la construction de deux points anonymes, initialisés avec les valeurs de ox, oy, ex et
ey,
 l'écrasement des valeurs initiales d’origine et extrémité par les deux points ainsi
construits.

 Données membres constantes

Une donnée membre d'une classe peut être qualifiée const. Il est alors obligatoire de
l'initialiser lors de la construction d'un objet, et sa valeur ne pourra par la suite plus être
modifiée.
A titre d'exemple voici une nouvelle version de la classe Segment, dans laquelle chaque objet
reçoit, lors de sa création, un « numéro de série » qui ne doit plus changer au cours de la vie de
l'objet :
class Segment {

31
Point origine, extremite;
int epaisseur;
const int numeroDeSerie;
public:
Segment(int x1, int y1, int x2, int y2, int ep, int num);
};
Constructeur, version erronée :
Segment::Segment(int x1, int y1, int x2, int y2, int ep, int num)
: origine(x1, y1), extremite(x2, y2) {
epaisseur = ep;
numeroDeSerie = num; // ERREUR : tentative de modification d'une constante
}
Constructeur, version correcte, en utilisant la syntaxe de l'initialisation des objets membres :
Segment::Segment(int x1, int y1, int x2, int y2, int ep, int num)
: origine(x1, y1), extremite(x2, y2), numeroDeSerie(num) {
epaisseur = ep;
}

 Fonctions membres constante :


Le mot const placé à la fin de l'en-tête d'une fonction membre indique que l'état de l'objet à
travers lequel la fonction est appelée n'est pas changé du fait de l'appel. C'est une manière de
déclarer qu'il s'agit d'une fonction de consultation de l'objet, non d'une fonction de
modification :
class Point {
... e
void placer(int a, int b); // modifie l'objet
float distance(Point p) const; // ne modifie pas l'objet
...
};
Il est conseillé de qualifier const toute fonction qui peut l'être cela élargit son champ
d'application.
La qualification const d'une fonction membre fait partie de sa signature. Ainsi, on peut
surcharger une fonction membre non constante par une fonction membre constante ayant,
appart cela, le même en-tête. La fonction non constante sera appelée sur les objets non
constants, la fonction constante sur les objets constants.

32
 Destructeur

De la même manière qu'il y a des choses à faire pour initialiser un objet qui commence à exister,
il y a parfois des dispositions à prendre lorsqu'un objet va disparaitre.
Un destructeur est une fonction membre spéciale. Il a le même nom que la classe, précédé du
caractère « ~ ». Il n'a pas de paramètre, ni de type de retour. Il y a donc au plus un destructeur
par classe.
Le destructeur d'une classe est appelé lorsqu'un objet de la classe est détruit, juste avant que la
mémoire occupée par l'objet soit récupérée par le système.
Par exemple, voici le destructeur qu'il faut ajouter à notre classe PointNomme. Sans ce
destructeur, la destruction d'un point n'entrainerait pas la libération de l'espace alloué pour son
étiquette :

class PointNomme {
...
~PointNomme() {
delete [] label;
}
...
};

Si le programmeur n'a pas écrit de destructeur pour une classe, le compilateur en synthétise un,
de la manière suivante :
o si la classe n'a ni objets membres ni classes de base, alors il s'agit du destructeur trivial
qui consiste à ne rien faire,
o si la classe a des classes de base ou des objets membres, le destructeur synthétise
consiste à appeler les destructeurs des données membres et des classes de base, dans
l'ordre inverse de l'appel des constructeurs correspondants.

5. Membres statiques :

Chaque objet d'une classe possède son propre exemplaire de chaque membre ordinaire (membre
non statique) de la classe :
 pour les données membres, cela signifie que de la mémoire nouvelle est allouée lors de
la création de chaque objet ;

33
 pour les fonctions membres, cela veut dire qu'elles ne peuvent être appelées qu'en
association avec un objet (on n'appelle pas « la fonction f » mais « la fonction f sur
l'objet x »).
A l'opposé de cela, les membres statiques, signalés par la qualification static précédant leur
déclaration, sont partagés par tous les objets de la classe. De chacun il n'existe qu'un seul
exemplaire par classe, quel que soit le nombre d'objets de la classe.
Les données et fonctions membres non statiques sont donc ce que dans d'autres langages
orientés objets on appelle variables d'instance et méthodes d'instance, tandis que les données
et fonctions statiques sont appelées dans ces langages variables de classe et méthodes de classe.

 Données membres statiques :

class Point {
int x, y;
public:
static int nombreDePoints;
Point(int a, int b) {
x = a; y = b;
nbrPoints++;
}
};
Chaque objet Point possède ses propres exemplaires des membres x et y mais, quel que soit le
nombre de points existants à un moment donné, il existe un seul exemplaire du membre
nombreDePoints.
La ligne mentionnant nombreDePoints dans la classe Point est une simple « annonce ».
Il faut encore créer et initialiser cette donnée membre (ce qui, pour une donnée membre non
statique, est fait par le constructeur lors de la création de chaque objet). Cela se fait par une
formule analogue à une définition de variable, écrite dans la portée globale, même s'il s'agit de
membres privés :
int Point::nombreDePoints = 0;
(la ligne ci-dessus doit être écrite dans un fichier « .cpp » , non dans un fichier « .h »). L'accès
à un membre statique depuis une fonction membre de la même classe s'écrit comme l'accès à
un membre ordinaire (voyez l'accès à nombreDePoints fait dans le constructeur Point ci-
dessus).
L'accès à un membre statique depuis une fonction non membre peut se faire à travers un objet,
n'importe lequel, de la classe :

34
Point a, b, c;
...
cout << a.nombreDePoints << "\n";
Mais, puisqu'il y a un seul exemplaire de chaque membre statique, l'accès peut s'écrirai aussi
indépendamment de tout objet, par une expression qui met bien en évidence l'aspect « variable
de classe » des données membres statiques :
cout << Point::nombreDePoints << "\n";

 méthode membre statique


Une fonction membre statique n'est pas attachée à un objet. Par conséquent :
 elle ne dispose pas du pointeur this,
 de sa classe, elle ne peut référencer que les fonctions et les membres statiques.

Par exemple, voici la classe Point précédente, dans laquelle le membre nombreDePoints a
été rendu privé pour en empêcher toute modification intempestive. Il faut donc fournir une
fonction pour en consulter la valeur, nous l'avons appelée combien :
class Point {
int x, y;
static int nombreDePoints;
public:
static int combien() {
return nombreDePoints;
}
Point(int a, int b) {
x = a; y = b;
nbrPoints++;
}
};
Pour afficher le nombre de points existants on devra maintenant écrire une expression comme
(a étant de type Point) :
cout << a.combien() << "\n";
ou, encore mieux, une expression qui ne fait pas intervenir de point particulier :
cout << Point::combien() << "\n";

35
6. Fonctions amis :

Une fonction amie d'une classe C est une fonction qui, sans être membre de cette classe, a le
droit d'accéder à tous ses membres, aussi bien publics que privés.
Une fonction amie doit être déclarée ou définie dans la classe qui accorde le droit d'accès,
précédée du mot réservé friend. Cette déclaration doit être écrite indirectement parmi les
membres publics ou parmi les membres privés :
class Tableau {
int *tab, nbr;
friend void afficher(const Tableau &);
public:
Tableau(int nbrElements);
...
};
et, plus loin, ou bien dans un autre fichier :
void afficher(const Tableau &t) {
cout << '[';
for (int i = 0; i < t.nbr; i++)
cout << ' ' << t.tab[i];
cout << ]" ;"
}

 Classes amis :

Une classe amie d'une classe C est une classe qui a le droit d'accéder à tous les membres de C.
Une telle classe doit être déclarée dans la classe C (la classe qui accorde le droit d'accès),
précédé du mot réservé friend, indirectement parmi les membres privés ou parmi les
membres publics de C.
Exemple : les deux classes Maillon et Pile suivantes implémentent la structure de données pile
(structure « dernier entrée premier sorti ») d'entiers :
class Maillon {
int info;
Maillon *suivant;
Maillon(int i, Maillon *s) {
info = i; suivant = s;

36
}
friend class Pile;
};
class Pile {
Maillon *top;
public:
Pile() {
top = 0;
}
bool vide() {
return top == 0;
}
void empiler(int x) {
top = new Maillon(x, top);
}
int sommet() {
return top->info;
}
void depiler() {
Maillon *w = top;
top = top->suivant;
delete w;
}
};
On notera la particularité de la classe Maillon ci-dessus : tous ses membres sont privés, et la
classe Pile est son amie (on dit que Maillon est une classe « esclave » de la classe Pile).
Autrement dit, seules les piles ont le droit de créer et de manipuler des maillons ; le reste du
système n'utilise que les piles et leurs opérations publiques, et n'a même pas à connaitre
l'existence des maillons.

7. Surcharge des opérateurs :

En C++ on peut redéfinir la sémantique des opérateurs du langage, soit pour les étendre à des
objets, alors qui n'étaient initialement définis que sur des types primitifs, soit pour changer
l'effet d'opérateurs prédéfinis sur des objets. Cela s'appelle surcharger des opérateurs.
Il n'est pas possible d'inventer de nouveaux opérateurs ; seuls des opérateurs déjà connus du
compilateur peuvent être surchargés. Tous les opérateurs de C++ peuvent être surchargés, sauf
les cinq suivants :
. .* :: ? : sizeof
Il n'est pas possible de surcharger un opérateur appliqué uniquement à des données de type
standard : un opérande au moins doit être d'un type classe.

37
Une fois surchargés, les opérateurs gardent leur pluralité, leur priorité et leur associativité
initiales. En revanche, ils perdent leur éventuelle commutativité et les éventuels liens
sémantiques avec d'autres opérateurs.
Par exemple, la sémantique d'une surcharge de ++ ou <= n'a pas à être liée avec celle de + ou
<.
Surcharger un opérateur revient à définir une fonction ; tout ce qui a été dit à propos de la
surcharge des fonctions s'applique donc à la surcharge des opérateurs.
Plus précisément, pour surcharger un opérateur ÷ (ce signe représente un opérateur quelconque)
il faut définir une fonction nommée operator÷. Ce peut être une fonction membre d'une classe
ou bien une fonction indépendante. Si elle n'est pas membre d'une classe, alors elle doit avoir
au moins un paramètre d'un type classe.

 Surcharge d'un opérateur par une fonction membre


Si la fonction operator÷ est membre d'une classe, elle doit comporter un paramètre de moins
que la pluralité de l'opérateur : le premier opérande sera l'objet à travers lequel la fonction a être
appelée. Ainsi, sauf quelques exceptions :
 « obj÷ » ou « ÷obj » équivalent à « obj.operator÷() »
 « obj1 ÷ obj2 » équivaut à « obj1.operator ÷ (obj2) »
Exemple :
class Point {
int x, y;
public:
Point(int = 0, int = 0);
int X() const { return x; }
int Y() const { return y; }
Point operator+(const Point) const; //surcharge de + par une fonction
membre
...
};
Point Point::operator+(const Point q) const {
return Point(x + q.x, y + q.y);
}
Utilisation :
Point p, q, r;

38
...
r = p + q; // compris comme : r = p.operator+(q);

 Surcharge d'un opérateur par une fonction non membre


Si la fonction operator÷ n'est pas membre d'une classe, alors elle doit avoir un nombre de
paramètres égal à la pluralité de l'opérateur. Dans ce cas :
 « obj÷ » ou « ÷obj » équivalent à « obj.operator÷() »
 « obj1 ÷ obj2 » équivaut à « operator ÷ (obj1, obj2) »
Exemple :
Point operator+(const Point p, const Point q) { // surcharge de + par une
return Point(p.X() + q.X(), p.Y() + q.Y()); // fonction non membre
}
Utilisation :
Point p, q, r;
...
r = p + q; // compris maintenant comme : r = operator+(p, q);

A cause des conversions implicites, la surcharge d'un opérateur binaire symétrique par une
fonction non membre, comme la précédente, est en général préférable, car les deux opérandes
y sont traités symétriquement. Exemple :
Point p, q, r;
int x, y;
Surcharge de l'opérateur + par une fonction membre :
r = p + q; // Oui : r = p.operator+(q);
r = p + y; // Oui : r = p.operator+(Point(y));
r = x + q; // Erreur : x n'est pas un objet
Surcharge de l'opérateur + par une fonction non membre :
r = p + q; // Oui : r = operator+(p, q);
r = p + y; // Oui : r = operator+(p, Point(y));
r = x + q; // Oui : r = operator+(Point(p), q);
Lorsque la surcharge d'un opérateur est une fonction non membre, on a souvent intérêt, ou
nécessite, à en faire une fonction amie. Par exemple, si la classe Point n'avait pas possède les
« accesseurs » publics X() et Y(), on aurait dû surcharger l'addition par une fonction amie :
class Point {

39
int x, y;
public:
Point(int = 0, int = 0);
friend Point operator+(const Point, const Point);
...
};
Point operator+(const Point p, Point q) {
return Point(p.x + q.x, p.y + q.y);
}
Les deux surcharges de + comme celles montrées ci-dessus, par une fonction membre et par
une fonction non membre, ne peuvent pas être définies en même temps dans un même
programme ; si tel était le cas, une expression comme p + q serait trouvée ambigüe par le
compilateur.

40

Vous aimerez peut-être aussi