Você está na página 1de 40

Initiation à la programmation orientée objet

Initiation à la programmation orientée


objet

Certains langages ont été conçus de toutes pièces pour permettre la programmation orientée objet (P.O.O.)
[ les langages Simula, Smalltalk et Eiffel pour ne citer que les plus connus ]. Le langage C, comme d'autres langages
(tel le Pascal... et le Cobol), ont évolué. En se transformant, le langage C/C++ dispose de tous les mécanismes pour
réaliser ce type de programmation.

Pour ce qui est du langage C/C++ cette évolution :


- Préserve l'essentiel de l'existant (la syntaxe de base du langage C et une grande partie de ses bibliothèques
de fonctions).
- Apporte certaines améliorations de la syntaxe utilisables dans tous les types de programmation, y compris
non-objet.

Si cette transition "en douceur" permet de faire évoluer les mentalité et préserve les investissements (en
hommes, en matériels et en programmes réalisés), ce qui explique son succès, le risque est que l'on passe "à coté" de
certains concepts fondamentaux de la P.O.O. et que l'on n'utilise les outils "objets" que dans un environnement resté
fondamentalement structuré.
Cependant, en l'absence de la mise en place de méthodes permettant d'assurer enfin une cohérence globale du
cycle conception  développement, cette voie mixte reste privilégiée.

 Une des difficultés d'apprentissage de la P.O.O. provient du fait que le vocabulaire employé n'est pas
toujours "stabilisé". Il y a parfois plusieurs synonymes pour caractériser les différentes notions
manipulées ( objet = instances, méthodes = fonctions membre, etc. ). Parfois même les termes utilisés
recouvrent des réalités différentes ( les messages chers à la P.O.O. ne sont pas de même nature que
les messages ( événements ) utilisés en programmation événementielle ). Il est parfois difficile, pour un
néophyte, de s'y retrouver.

Le langage C++ étant directement dérivé du langage C, les mécanismes mis en œuvre pour implémenter les
concepts objets sont fortement entachés par leur origine. En particulier le mécanisme de définition des "classes" n'est
qu'une généralisation de celui de définition des structures. Il s'en suit que la notion de "messages" envoyés par les
différents objets afin qu'ils interagissent entre eux, se traduit par un usage systématique de la "notation pointée"
utilisée pour accéder de longue date aux différents champs d'une variable structurée.

De fait, quand un objet veut interagir sur un autre objet, il réalise cette interaction, au sein du code lui
appartenant (soit au sein de ses fonctions membres, soit au sein d'un gestionnaire d'événement qu'il déclenche) en
invoquant, grâce à la notation pointée une méthode de l'objet "cible".

..... Action à faire réaliser par l'objet


ObjetCible . methode ( ) cible
......

Opérateur point

Objet cible auquel l'objet


source souhaite envoyer un
message

Programmation Page 1
Initiation à la programmation orientée objet

"Programmation Orientée Objet" et "Programmation par Objet" :

Il fut une époque, pas si lointaine, où, pour pouvoir manipuler le plus simple des objets, il fallait avoir une
connaissance plutôt solide des concepts et des mécanismes mis en œuvre. La création du moindre source se
révélait donc très laborieuse puisqu'il fallait mettre en application pour ce faire une grande partie des
(nombreuses) connaissances fraîchement acquises pour cela.

Les choses ont heureusement évolué depuis et il est possible d'utiliser un langage objet et des objets prédéfinis,
pour construire une application, sans pour cela devoir être un expert de la P.O.O.. La manipulation des objets ne
nécessite que des notions sur les concepts mis en œuvre et permet de se familiariser avec eux sans craindre
d'être rebuté par leur complexité d'implémentation. On parle alors de "Programmation par objet".

L'apprentissage d'un langage objet se fait donc aujourd'hui en deux phases :


- Une première phase où la syntaxe et les mécanismes généraux du langage sont étudiés. Cette phase
s'achève par la création de programmes utilisant des objets fournis par les environnements de
développement.
- Une deuxième phase, facultative, où l'implémentation des concepts est étudiée en profondeur. Cela de
manière à être en mesure de créer ses propres classes et d'utiliser des objets personnalisés au sein de ses
applications.

Méthodologie pédagogique :

Comme il est parfois difficile de comprendre exactement les spécificités de la programmation orientée objet,
nous allons, tout au long de ce chapitre, constituer "pas à pas" une classe particulière, simple à implémenter de
manière à ne pas se perdre dans les lignes de code, mais suffisante pour mettre en œuvre la plupart des concepts
objets.

La classe de départ sera la classe point qui modélise un point susceptible d'être manipulé dans de nombreuses
applications.

: Classes et objets

Une classe est la généralisation de la notion de "type défini par l'utilisateur", permettant de décrire une entité
logique dans laquelle se trouvent associées à la fois des données [ données membres ] et des méthodes [ fonctions
membres ou méthodes ].
Les données peuvent être encapsulées : elles ne peuvent plus alors être modifiées qu'en faisant appel
aux fonctions membres.

 Au niveau conceptuel, un objet est une entité regroupant des caractéristiques et ayant un
comportement spécifique. Au niveau de la programmation, cette entité est modélisée par un type
classe dans lequel les caractéristiques sont assimilées à des données et les comportements sont
décrits par des sous-programmes, inclus dans la classe, appelés méthodes.

Le point de départ de la construction de la classe point est la structure struct point qu'il serait aisé de décrire
en termes "équivalents" (mêmes données et sous-programmes manipulant la structure réalisant les mêmes
traitements).

Programmation Page 2
Initiation à la programmation orientée objet

En programmation structurée traditionnelle, il est donc possible de définir un type de structure nommé point
défini comme suit :

struct POINT
{
int x ; // coordonnée x du point
int y ; // coordonnée y du point
} ;

POINT a, b ;
// a , b deux variables de type structure point

 x et y sont les champs ( ou les membres ) de la structure point. L'accès aux membres de a ou b
se fait, bien entendu, par l'opérateur ' . ' ( point ) [ a.x ou b.y par exemple ].

A partir de cette définition de structure il est possible de décrire divers traitements manipulant des variables
créées à ce type: sous-programmes Afficher ( ), Déplacer ( ), Intialiser ( ), Cacher ( ), etc.

1.1 : Déclaration d'une classe

La caractéristique de base de la P.O.O. est de pouvoir regrouper dans une entité unique les déclarations des
différentes données composant la structures et les descriptions des différents sous-programmes manipulant ces
données. Cette entité globale est une classe.

Dans l'exemple qui nous guide, les fonctions membres seront :

- initialise : pour donner des valeurs aux coordonnées d'un point,


- deplace : pour modifier les coordonnées,
- affiche : pour afficher le point à l'écran .

La structure point initiale et ses sous-programmes satellites deviennent alors :

class Point
{
// déclarations des données membres
int x ;
int y ;

// prototypes des fonctions membres ( méthodes )


void initialise ( int , int ) ;
void deplace ( int , int ) ;
void affiche ( ) ;
} ;

 Par tradition, les identifiants de types structurés étaient écrits en majuscules. Cette tradition se perd
avec la déclaration des classes mais on conserve l'habitude de nommer une classe avec se première lettre en
majuscule.

 Une donnée membre peut être un objet instancié par rapport à une autre classe, définie
précédemment.

Programmation Page 3
Initiation à la programmation orientée objet

Une classe est un type défini par l'utilisateur. C'est un modèle à partir duquel on va pouvoir créer des variables
objet qui seront utilisées par le programme. On dit que l'on instancie un objet à partir d'une classe (ou qu'un objet
particulier est une instance de sa classe) lorsque l'on crée des objets à partir de la définition d'une classe.

 En fait, en C++, il existe 4 catégories de classes :


- Les structures,
- Les classes,
- Les unions,
- Les énumérations.
Dans tout ce qui suit, nous ne considérerons que le cas des "vraies" classes.

 En général, une classe comportera différentes méthodes, que l'on peut regrouper en quatre catégories
:
- Celles chargées de créer les objets ( les constructeurs ) ;
- Celle chargée de détruire les objets devenus inutiles ( le destructeur ) ;
- Celles qui accèdent aux données membres "en lecture" ;
- Celles qui y accèdent "en écriture", pour modification.

1.2 : Déclaration d'objet

A partir de la déclaration d'une classe, on peut déclarer des objets selon le formalisme habituel suivant :

Point a , b ;
// déclaration de deux objets de type Point.

La déclaration d'un objet provoque la réservation d'une zone mémoire par le compilateur. En fait cette
réservation est réalisée dans deux zones différentes :
- Les codes correspondant aux différentes méthodes associées à la classe sont construits dans une zone
particulière du segment de code du programme.
- Les données sont stockées dans un autre segment, sous forme de structures. Ces structures contiennent des
pointeurs vers les différentes méthodes constituant la classe.

Si on reprend l'exemple de la classe Point on a :

class Point
{
// déclarations des données membres
int x ;
int y ;

// prototypes des fonctions membres ( méthodes )


void initialise ( int , int ) ;
void deplace ( int , int ) ;
void affiche ( ) ;
} ;

Point a , b ;
// déclaration de deux objets de type Point.

Programmation Page 4
Initiation à la programmation orientée objet

Objet a
x

ptin Module objet de la


méthode initialise
ptdep

ptaff
Module objet de la
méthode deplace
Objet b x

y
Module objet de la
ptin méthode affiche

ptdep

ptaff

1.3 : Déclaration et invocation des fonctions membres

Déclaration :
Il faut évidemment déclarer les fonctions membres de cette structure. Il existe pour ce faire deux
manières :

1 / Si la fonction a un code court, elle peut être définie au sein même de la déclaration de celle de la classe [
fonction dite " inline" ].
2 / Dans l'alternative il faut définir les fonctions à l'extérieur de la déclaration de classe.

 Fonctions inline :
On déclare ces fonctions selon le modèle suivant :

struct point
{
int x ;
int y ;

void initialise ( int abs , int ord )


{
x = abs ;
y = ord ;
}

// remarquez que le mot réservé "inline" n'apparaît pas

etc ..........

Programmation Page 5
Initiation à la programmation orientée objet

 Fonctions définies à l'extérieur de la structure :

La déclaration de la méthode se fait en se référant à la classe d'appartenance en utilisant l'opérateur ' :: ' de
résolution de portée.

La syntaxe de déclaration est alors :

void point :: deplace ( int dx , int dy )


{
x = x + dx ;
y = y + dy ;
}

 L'opérateur ' :: ' indique que l'identifiant deplace dont il est question est celui défini dans la
structure Point .

 x et y quant à eux ne sont ni des arguments ni des variables locales: ils désignent les membres
x et y correspondant à la classe de type Point. L'association est réalisée par l'opérateur ' :: '
de l'en-tête.

Dans les faits la syntaxe "inline" se révèle rapidement assez lourde d'emploi. Une classe étant constituée en
général de dizaines de données et d'autant de méthodes, la description de la classe ne comporte que les
déclarations de ses différents composants, le code des méthodes étant reporté plus loin.

La tendance actuelle est de faire en sorte qu'une classe, un tant soit peu complexe, soit définie dans un fichier
source qui lui est propre et qui contient toutes les descriptions qui la concernent.

 Il est possible de définir une fonction membre comme constante.


La syntaxe de déclaration est alors :

type-renv NomClasse :: nom_fonction ( arguments ) const


{
......
}

Une telle fonction ne peut modifier aucune des valeurs des données membres ni même retourner
une référence non constante ou un pointeur non constant d'une donnée membre (ce qui
reviendrait, dans le cas contraire, à pouvoir modifier ultérieurement cette donnée).

Invocation :
On invoque, pour exécution, les différentes méthodes d'une classe à l'aide de la notation pointée.

Cette notation permet de faire exécuter la méthode spécifiée sur un objet précis.

point a ; // déclaration de l'objet


..........
a. initialise ( 3 , 5 ) ;
// initialisation de a.x à 3 et a.y à 5
a. deplace ( 1, -1 ) ;
// on a alors a.x = 4 et a.y = 4
cout << "Position de a : " << a.x a.y ;

 Dans l'état actuel de la construction, on peut donc accéder aux données de l'objet à partir de
n'importe quel endroit du code par l'opérateur ' .' , mais cela va à l'encontre du principe
d'encapsulation.

Programmation Page 6
Initiation à la programmation orientée objet

1.4 : Membres statiques

Lorsqu'on crée différents objets à partir d'une même classe, chacun d'entre eux possède ses propres données
membres .

Par exemple dans la structure Point on a :

Point a : Point b:
a.x b.x
a.y b.y

Il se peut néanmoins qu'une donnée soit commune à tous les objets de la classe. Ce qui revient à dire que toute
modification réalisée sur la donnée membre d'un objet est répercutée sur la donnée membre équivalente de tous les
objets instanciés de la classe.
Pour cela il suffit de déclarer la donnée concernée avec le mot clé static .

class Exemple
{
static int n ; // déclaration d'un membre statique
int z ;
} ;

Exemple a , b ;
/* création de deux objets : la donnée a.n et b.n est la même pour a et
pour b */

 Les membres statiques existent en un seul exemplaire indépendamment des objets de la classe
correspondante

Les membres statiques sont toujours initialisés à 0. Mais ils ne peuvent pas être initialisés au même moment
que leur définition .
On ne peut initialiser un membre statique qu'à la suite de la définition de la classe, en se référençant à cette
dernière :

ex : int Exemple :: n = 2 ;

1.5 : Constructeur et destructeur

Le langage C++ implémente un mécanisme original permettant d'instancier ( = créer ) ou de détruire des
objets à partir de la définition d'une classe. Il s'agit de l'utilisation de deux types de méthodes particulières : le (ou les)
constructeur ( s ) et le destructeur.

Dans l'état actuel de la construction de la classe Point, il est nécessaire d'utiliser une fonction membre de la
classe pour pouvoir initialiser les différentes données d'un objet après que celui-ci ait été déclaré ( = créé ).
Cette démarche implique que l'utilisateur de la classe pense à appeler la fonction adéquate, au bon moment, à
chaque fois qu'il souhaite créer un nouvel objet.
L'utilisation du constructeur et du destructeur va permettre de faciliter la création et la destruction des objets
tout en mettant à la disposition du programmeur des possibilités plus élaborées.

Le constructeur :
Un constructeur est une fonction membre spéciale définie au sein de chaque classe. Elle est appelée
automatiquement à chaque création d'objet [ on verra plus tard que cette appel peut être statique, dynamique
ou automatique ].

Programmation Page 7
Initiation à la programmation orientée objet

Par "automatiquement" il faut comprendre "sans appel explicite – au sein du source - de la part du
programme. Une fois qu'un constructeur est créé, c'est le compilateur qui se charge de l'appeler lorsqu'il en a
besoin, à chaque création d'objet.

Un constructeur :
- Est identifiable par le fait qu'il porte le nom de la classe auquel il appartient.
- Il ne retourne aucune valeur [ même le spécificateur void est omis ].
- Il peut admettre des arguments : ce sont, le plus souvent, les valeurs d'initialisation des différents champs
de l'objet construit.

Exemple :

La classe Point définie précédemment devient :

class Point
{
int x ;
int y ;

public :
Point( int , int ) ; // constructeur
void deplace( int , int ) ;
void affiche( ) ;
} ;

avec :

Point :: Point ( int abs , int ord )


// définition du constructeur
{
x = abs ;
y = ord ;
}

 Un constructeur ne peut être déclaré ni static, ni const, ni virtual.

A partir du moment où un constructeur est défini, on doit créer ( et initialiser ) un objet de la manière
suivante :

Point a ( 1 , 2 ) ;
// création de l'objet a initialisé à ( 1 , 2 )
// il n'y a pas d'appel explicite au constructeur.

 Il n'est plus possible de créer un objet sans fournir les arguments d'initialisation [ sauf si le
constructeur ne possède pas d'argument ].

On peut alors avoir le programme suivant ( conventions habituelles ) :

int main ( )
{
point a ( 2 , 12 ) ;
// création et initialisation d'un point a
a . affiche ( ) ; // affichage à l'écran
a . deplace ( 2 ,5 ) ; // déplacement
a . affiche ( ) ; // affichage à l'écran

Programmation Page 8
Initiation à la programmation orientée objet

Programmation Page 9
Initiation à la programmation orientée objet

Il est possible de définir, grâce aux possibilités offertes par la surdéfinition des méthodes, plusieurs
constructeurs. Ils ont alors tous le même nom mais se distinguent par le nombre variable d'arguments et les
types de ces derniers.

Exemple :
Point ( ) ; // constructeur sans argument ;
Point ( int a , int b ) ;
// constructeur avec deux arguments d'initialisation.

On peut même fournir des valeurs par défaut aux arguments du constructeur :

Point ( int a = 0 , int b = 0 );

L'utilisateur de la classe appelle implicitement le constructeur souhaité, en fonction de ses besoins, en


fournissant le nombre d'arguments nécessaires.

 On appelle constructeur par défaut le constructeur ayant une liste vide d'arguments ou ayant
des valeurs par défaut pour tous ses arguments.

 On appelle constructeur de recopie le constructeur procédant à la création d’un objet à


partir
d’un autre objet pris comme modèle.
Prototype habituel d’un constructeur de recopie :
T :: T(const T&) ;
Le constructeur de recopie a également deux autres utilisations spécifiées dans le langage :
• Lorsqu’un objet est passé en paramètre par valeur à une fonction (ou méthode), il y a
appel du constructeur de recopie pour générer l’objet utilisé en interne dans celle-ci.
• Au retour d’une fonction (ou méthode) renvoyant un objet, il y a création d’un objet
temporaire par le constructeur de recopie.

Le destructeur :
Selon les mêmes principes on peut définir un destructeur. Celui-ci porte le nom de la classe précédé du signe
'~' [ tilde ]. Il est lui aussi appelé automatiquement lorsqu'il faut détruire un objet d'une classe

 Là encore c'est le compilateur qui décide de l'appel du destructeur et non le programme.

La déclaration d'un destructeur se fait selon la syntaxe :

Point :: ~Point ( ) ;
{
}

En général il n'y a pas de code associé à un destructeur. Il n'est donc pas nécessaire de la déclarer. Cependant,
lors de la mise au point d'un programme, il peut être utile de mettre un message à l'intérieur du destructeur
afin de s'assurer de la destruction des objets.

Le destructeur est appelé automatiquement :


- Lors de la destruction d'un objet de type automatique ( à la sortie du bloc dans lequel il est défini ) ;
- Lors de l'utilisation de l'opérateur delete sur un objet.

Programmation Page 10
Initiation à la programmation orientée objet

Programmation Page 11
Initiation à la programmation orientée objet

1.6 : Construction, initialisation et destruction d'objet

En langage C traditionnel, une variable peut être créée de deux façons :


- Par une déclaration :
La variable peut alors être automatique, static ou globale en fonction de sa nature et de l'emplacement de
sa déclaration.
Dans ces trois cas la variable est créée lors de la compilation. On dit qu'elle est statique.
- En faisant appel à des fonctions de gestion dynamique de la mémoire :
La variable est alors dite dynamique. Sa durée de vie est contrôlée par le programmeur.

En langage C++, on dispose des mêmes possibilités pour créer les objets. Leur gestion dynamique se fera
néanmoins de préférence avec les opérateurs new et delete.
Il pourra donc y avoir des objets statiques ( automatiques, static et globaux ) ou dynamiques.

Objets statiques :
¤ Objets automatiques :
Ils sont créés par une déclaration réalisée au sein d'une fonction ou dans un bloc d'instructions dépendant
d'une structures de contrôle. Ils sont détruits à la fin de l'exécution de la fonction ou à la sortie du bloc.
¤ Objets statiques et globaux :
Ils sont créés en dehors de toute fonction ou au sein d'une fonction, lorsqu'ils sont précédés du qualificatif
static .
Ils peuvent être créés avant le début de l'exécution de main et détruits après la fin de son exécution .

Exemple :

#include <iostream.h>

class Point
{
int x , y ;

point (int abs , int ord ) // constructeur inline


{
x = abs ;
y = ord ;
cout << " Construction d'un point : "
<< x << " " << y << " \n";
}
~point ( ) // destructeur
{
cout << " Destruction du point :
" << x << " " << y << " \n " ;
}
} ;

Programmation Page 12
Initiation à la programmation orientée objet

Point a (1 ,1 ) ; // création d'un objet statique global

int main ( )
{
point b (10 , 10 ) ; // création d'un objet automatique

int i ;
for ( i = 1 ; i <= 3 ; i ++ )
{
cout << " Tour de boucle N° " << i << " \ n ";
point c ( i , 2* i ) ;
// objets automatiques créés dans un bloc
}
cout << " Fin de main ( ) " ;
}

Le programme affichera :

Construction d 'un point : 1 1


Construction d'un point : 10 10
Tour de boucle N°1
Construction d'un point : 1 2
Destruction du point : 1 2 // le destructeur est "appelé" automatiquement
// par le compilateur
Construction d'un point : 2 4
Destruction du point : 2 4 // idem
Construction d'un point : 3 6
Destruction du point : 3 6 // idem
Fin du main ( )
Destruction du point : 10 10 // l'affichage ne paraîtra qu'en visionnant la
Destruction du point : 1 1 // fenêtre user

Objets dynamiques :
On peut créer dynamiquement un objet en utilisant l'opérateur new :

Avec la classe Point définie, sans constructeur, comme suit :

class point
{
int x ; // déclarations des membres
int y ;

// déclarations( en-tête )des fonctions membres


void initialise ( int , int ) ;
void deplace ( int , int ) ;
void affiche ( ) ;
} ;

on peut créer dynamiquement un objet :

Point *p_adr ; // déclaration d'un pointeur de type point

p_adr = new Point ;


// création dynamique d'une zone mémoire "objet point"
..........
p_adr -> initialise ( 1 , 3 ) ;
// accès à la méthode de l'objet pointé par p_adr

Programmation Page 13
Initiation à la programmation orientée objet

Si la classe possède un constructeur, on peut créer des objets dynamiquement en employant la syntaxe :

Point *padr ;
padr = new Point ( 2 , 5 ) ;
/* création d'un objet en mémoire grâce au constructeur de la classe point */

 La zone mémoire allouée à l'objet est libérée par appel de l'opérateur delete.

delete padr ;
/* le destructeur de l'objet référencé par padr est appelé automatiquement */

1.7 : Surdéfinition d'opérateur

Généralités :
Une fois les classes définies, apparaît un problème de taille : on ne peut pratiquement pas manipuler les objets
qui en sont issus avec les opérateurs "classiques" fournis par le langage C/C++ traditionnel. Dès que l'on
souhaite réaliser une opération sur ces objets il faut redescendre au niveau de chaque donnée membre ( via,
normalement, les méthodes d'accès ).

Par exemple pour pouvoir, ne serait-ce qu'additionner deux objets, il faut réaliser les additions donnée par
donnée.

La solution à ce problème est donnée par la possibilité de surdéfinir les opérateurs utilisés par le langage
C/C++.
On peut surdéfinir pratiquement n'importe quel opérateur existant dans la mesure où cette surdéfinition
s'applique à au moins un objet.

Par ce biais on peut créer des opérateurs parfaitement adaptés à la manipulation des objets.

Limites de la surdéfinition :
¤ Un opérateur surdéfini garde son niveau de priorité et ses règles d'associativité.
¤ L'opérateur ' . ' ne peut pas être redéfini. De même tous les opérateurs ayant une signification spéciale en
P.O.O . ( ' :: ' , ' .* ' , ' ?: ' ) et sizeof.
¤ La surdéfinition doit conserver la pluralité de l'opérateur de base : un opérateur unaire surdéfini doit rester
un opérateur unaire, etc. . De même elle conserve les règles de priorités et d'associativité propres à cet
opérateur.

Programmation Page 14
Initiation à la programmation orientée objet

Syntaxe :
Pour surdéfinir un opérateur il faut utiliser le mot clé operator. On réalise la surdéfinition en déclarant des
fonctions de surdéfinition dont la déclaration se fait selon la syntaxe suivante :

Syntaxe :

type_retour operator <opérateur redéfini > ( type_arg_concerné )

Classe qui utilisera


l'opérateur surdéfini

Signe caractérisant l'opérateur à redéfinir

mot clé de surdéfinition

Type de l'objet renvoyé

 Le prototype d'une telle fonction est à intégrer dans la définition de la classe concernée.

Exemple :

On souhaite surdéfinir l'opérateur ' + ' afin qu'il soit en mesure de réaliser l'addition de deux objets
points [ par convention, le résultat est un point dont les coordonnées sont égales à la somme des
coordonnées ].

On a alors le prototype :

Point operator + ( Point ) ;


/* fonction membre à inclure dans la définition de la classe
Point : elle s'applique à un objet Point et renvoie un objet Point */

Et la définition :

Point Point :: operator + ( Point a )


{
Point p ;

p. x = x + a.x ;
p. y = y + a.y ;
return p ;
}

A partir de ce moment on peut avoir des instructions du type :

c = a + b ;
// interprété comme c = a.operator + ( b ) ;

Programmation Page 15
Initiation à la programmation orientée objet

 La définition de la fonction operator + fait apparaître une dissymétrie entre les deux objets : un des
objets est référencé implicitement par ses composants ( x, y ), alors que le second est référencé
explicitement ( a.x , a.y ).

2 : Encapsulation

2.1 : Généralités sur le mécanisme

Le langage C++ n'implémente pas d'une manière rigoureuse le concept de l'encapsulation. Il laisse à l'initiative
du concepteur de la classe de définir les données et/ou les méthodes qui pourront être accessibles par d'autres modules
du programme et celles qui ne le pourront pas.
Pour ce faire les données et les fonctions membres peuvent être déclarées public ou private.
- Les données ou méthodes déclarées public peuvent être accessibles par des instructions extérieures à
l'objet où elles sont déclarées.
- Les données ou méthodes déclarées private ne sont accessibles qu'aux fonctions membres déclarées dans
l'objet.

Par défaut, en l'absence d'autres spécifications, les données et/ou les méthodes d'une classe sont considérées
comme private.

A partir du moment où le mot clé 'public : ' est utilisé, toutes les déclarations qui suivent concernent des
données et/ou des méthodes accessibles. Cela jusqu'à ce que le mot 'private : ' soit de nouveau utilisé ou que l'on soit
arrivé à la fin des déclarations de la classe.

2.2 : Principes de mise en œuvre de l'encapsulation

Pour satisfaire au mieux au principe d'encapsulation il est souhaitable que les données d'une class soient à
déclarées private [ donc protégées vis à vis des accès extérieurs ].
Seules des fonctions membres conservent ont un statut public afin que l'on puisse "manipuler" l'objet.

 Si une classe n'a que des membres private, les objets qui en sont instanciés sont
inaccessibles de l'extérieur.

Dans la pratique il sera souhaitable de conserver la plupart des données avec un statut private. Les données
publiques doivent rester des exceptions qu'il faudra justifier.

Il est par contre utile de déclarer un certain nombre de méthodes avec le statut private. Ces méthodes
constituent des mécanismes internes de la classe et n'ont pas à être accessibles aux utilisateurs de cette dernière.

 Les fonctions à accès private ne peuvent être invoquées que par d'autres fonctions membres
( publiques ou privées ) de la classe.

 Seules les données et méthodes publiques sont documentées. Il faut disposer des sources de la
classe pour découvrir les données et méthodes privées.

Programmation Page 16
Initiation à la programmation orientée objet

2.3 : Déclaration des restrictions d'accès

En règle générale les données et/ou méthodes privées sont déclarées en premier, sans recourir au mot clé
'private :' ( puisqu'il s'agit alors du mode de déclaration par défaut ). Les données et/ou méthodes publiques sont
déclarées ensuite, après l'écriture du mot clé 'public :'.

Exemple :

class Point // déclaration d'une classe


{
// déclarations de membres privés ( par défaut )
int x ;
int y ;
// déclarations de membres publics
public :
void initialise (int , int ) ;
void deplace( int , int ) ;
void affiche( ) ;
} ;

 Les définitions ( extérieures ) des fonctions membres ne sont pas modifiées, même si elles sont
déclarées private.

 Il est possible de redéfinir d'autres portions de déclarations 'private ' et 'public' mais cela ne facilite pas
la lisibilité de la classe.

A partir du moment où une données est déclarée 'private', il n'est plus possible d'accéder aux données privées
par des instructions du type a.x = 5.
Il faudra utiliser une des fonctions membres ( initialise ( ) ou deplace ( ) ) pour modifier les coordonnées du
point.

Exemple :
Avec a et b deux objets de la classe point. Si x est une donnée publique et y une donnée privée on aura :

a.x = b.x ; // possible


a.y = b.y ;
// illégal, provoque une erreur à la compilation
a = b ;
// possible : recopie de tous les membres de b dans a

REMARQUE : Pour pouvoir réaliser l'opération a = b il faut, on le verra plus loin, "redéfinir" l'opérateur
d'affectation ' = ' .

Programmation Page 17
Initiation à la programmation orientée objet

Exemple complet d'écriture de la classe Point :

class Point
{
int x , y ; // données privées

public :
Point ( int abs = 0, int ord = 0 )
// constructeur avec arguments par défaut
{
x = abs ;
y = ord ;
}

Point operator + ( point );


// surdéfinition de l'opérateur +

void affiche( )
{
cout << " coord. x : " << x <<
" coord. y : " << y << "\n" ;
}
}

Point Point :: operator + ( Point a )


{
point p ;

x = x + ax ;
y = y + ay ;
return p ;
}

int main ( )
{
Point a ( 1 , 2 ) ;
a . affiche ( ) ;
Point b ( 5 , 10 ) ;
Point c ; // c prend les valeurs par défaut
c = a + b ;
c . affiche ( ) ;
getch ( ) ;
return 1 ;
}

Programmation Page 18
Initiation à la programmation orientée objet

3 : Propriétés des membres d'une classe

3.1 : Fonctions amies

Le principe d'encapsulation interdit, dans le cas général, l'accès aux données d'un objet à d'autres fonctions
qu'aux fonctions membres de la classe dont il est originaire.
Dans certains cas, il est néanmoins utile de pouvoir accéder à ces données à partir de fonctions autonomes ( =
déclarées hors d'une classe ) ou de fonctions membres d'une autre classe.
La solution retenue par le langage C++ est de déclarer les fonctions pouvant accéder aux données en tant que
fonctions amies [ en anglais : friends ].

Déclaration d'une fonction "amie" autonome


La déclaration d'une fonction amie se fait, au sein de la déclaration de la classe concernée, selon la syntaxe :

friend < prototype de la fonction amie > ;

Exemple :

class Point
{
.........
friend int coincide ( Point , Point ) ; // prototype
.........
}

int coincide ( Point p , Point q )


/* pas de '::' car coincide n'est pas une fonction membre de la classe */
{
if ( ( p.x == q.x ) && ( p.y == q.y ) )
return 1 ;
else
return 0 ;
}

/* REMARQUE : la fonction coincide peut être utilisée par d'autres parties du


programme */

La déclaration de la fonction amie peut se faire à n'importe quel endroit au sein de la déclaration de la
classe concernée.

 Le fait que la déclaration du caractère "amie" d'une fonction ne peut se faire qu'au sein même
de la classe concernée est une garantie que n'importe quelle fonction ne pourra pas se
prétendre amie de la classe afin de pouvoir y accéder de manière non contrôlée [ c'est la classe
qui décide qu'elles sont ses fonctions amies ].

Déclaration d'une fonction "amie" membre d'une autre classe :


Il est possible qu'une fonction amie d'une classe soit une fonction membre d'une autre classe. Il faut alors, au
moment de la compilation de la fonction amie, préciser la classe à laquelle elle appartient ( à l'aide de
l'opérateur ' :: ' ).

Programmation Page 19
Initiation à la programmation orientée objet

Il se pose alors le problème de la compilation des codes contenant les différentes déclarations ( en particulier si
les deux classes sont déclarées dans des fichiers différents ). Pour comprendre le problème qui se pose il faut
s'aider d'un exemple :

Exemple :
Soient 2 classes A et B et une fonction membre famie de la classe B dont le prototype est :

int famie ( char, A ) ;


// paramètres : un caractère et un objet de classe A

- Pour que la fonction famie ( ) puisse accéder aux membres privés de A, elle doit être déclarée amie au
sein de la classe A.
- Pour compiler la classe A, il faut que le compilateur connaisse les caractéristiques de la classe B. La classe
A doit donc être compilée après la classe B.
- Pour compiler la classe B ( en particulier la fonction int famie (char, A ) ), le compilateur n'a pas besoin
de connaître A ( il lui suffit de savoir que c'est une classe ). Il suffit alors de "l'annoncer " avant la
déclaration de la classe B.
- La déclaration de la fonction famie ( ) nécessite quant à elle la connaissance des caractéristiques de A et
B.

On a donc le code de déclaration suivant :

class A ; // annonce de la classe A

class B
{
................
int famie ( char , A ) ;
// prototype de fonction membre
................
} ;

class A
{
// membres privés
.............
// membres publics
friend int B :: famie (char , A ) ;
/* déclaration de la fonction amie famie appartenant à la classe B */
.......
} ;

int B :: famie( char c , A ...... )


// déclaration de la fonction famie
{
..............
}

Programmation Page 20
Initiation à la programmation orientée objet

Fonction "amie" de plusieurs classes :


En reprenant pratiquement tel quel l'exemple précédent on pourrait avoir :

class B ; // annonce de la classe B

class A
{
............
friend void famie( A , B ) ;
............
} ;

class B
{
..........
friend void famie( A , B ) ;
..........
}

void famie ( A ..... , B ........ )


/* famie a accès aux membres privés de n'importe quel objet de
type A ou B */
{
...........
}

Cas où toutes les fonctions d'une classe sont "amies" d'une autre classe :
Dans ce cas il est plus simple d'effectuer une déclaration "globale".
On a alors :

class A
{
.....
friend class B ;
/* cette instruction dans la déclaration de la classe A signifie que
toutes les fonctions de la classe B sont des fonctions amies de la
classe A */
.....
}

 Il faut cependant toujours annoncer la classe amie B [ ligne : class B; ] avant de déclarer la
classe A.

Programmation Page 21
Initiation à la programmation orientée objet

Redéfinition d'opérateurs
On peut utiliser la notion de fonction amie pour réaliser des surdéfinitions d'opérateurs. Dans ce cas la syntaxe
est la suivante :

Prototype : friend type operator <opérateur> ( arg , arg ....) ;

En reprenant l'exemple précédent de surdéfinition de l'opérateur '+' on a alors :

class Point
{
.............
friend Point operator + ( Point , Point ) ;
// prototype au sein de la classe
..............
} ;

Point operator + ( Point a , Point b )


// déclaration de la redéfinition
{
Point p ;

p. x = a . x + b . x ;
p. y = a . y + b . y ;
return p ;
} ;

3.2 : Surdéfinition de fonction membre

On a déjà vu que le langage C++ permettait d'effectuer la surcharge ( surdéfinition ) de fonction. Cette
possibilité est utilisée fréquemment lorsqu'on crée des classes.

Toutes les fonctions membres d'une classe, y compris le constructeur [ mais pas le destructeur car il n'accepte
pas de paramètres ] peuvent bénéficier de cette possibilité .

De cette manière on peut appeler des fonctions membres, qui possèdent le même identificateurs mais dont
l'action est différente, en fonction du but recherché .

Exemple :
class Point
{
int x, y ;

public :
Point ( ) ; // constructeur sans argument
Point ( int ) ; // constructeur avec un argument
Point ( int , int ) ;
// constructeur avec 2 arguments
void affiche ( ) ; // fonction affiche1 sans argument
void affiche ( char * ) ;
// fonction affiche2 avec un argument chaîne
} ;

Programmation Page 22
Initiation à la programmation orientée objet

Point :: Point ( ) // déclaration 1° constructeur


{
x = 0 ;
y = 0 ;
}

Point :: Point ( int abs ) // déclaration du 2° constructeur


{
x = y = abs ;
}

Point :: Point ( int abs, int ord )


// déclaration du 3° constructeur
{
x = abs ;
y = ord ;
}

void Point :: affiche ( )


{
cout << " je suis en : " << x << " " << y << " \ n ";
}

void Point :: affiche ( char *message )


{
cout << message;
affiche ( ) ;
}

main ( )
{
Point a ; // création d'un objet avec le 1° constructeur
a . affiche ( ) ;
// affichage de "je suis en 0 0 "
Point b ( 5 ) ;
// création d'un objet avec le 2° constructeur
b . affiche ( " Point b : ") ;
// affichage de Point b : je suis en 5 5 "
Point c ( 3 , 12 ) ;
// création d'un objet avec le 3° constructeur
c . affiche ( ) ;
// affichage de " je suis en 3 12 "
}

 Le compilateur détermine la fonction à appliquer en fonction du nombre et du type des arguments.


C'est ce qui explique que l'on ne peut pas surdéfinir les destructeurs .

 La fonction surdéfinie peut utiliser des arguments par défaut.

Programmation Page 23
Initiation à la programmation orientée objet

3.3 : Passage d'objet en argument

Il est possible de donner des arguments objets d'une classe à une fonction, membre de la classe ou d'une autre
classe. Ce passage d'objet en paramètre peut être réalisé, conformément à la syntaxe du langage C++ :
- Par valeur ;
- En utilisant des pointeurs ( par adresse ) ;
- Par référence.

Passage par valeur :


Dans ce cas, la syntaxe de déclaration de la fonction utilisant des objets est la suivante :

Syntaxe :

type_retour class1 :: fonction membre ( classe2 nom_objet_classe2 )

Classe de l'objet Nom de l'objet passé


passé en paramètre en paramètre

nom de la fonction membre

classe de la fonction membre

Type renvoyé par la fonction membre

Exemple ( en reprenant l'exemple de la classe Point ) :

class Point
{
int x , y ;
public :
Point ( int abs = 0 , int ord = 0 )
// constructeur avec arguments par défaut
{
x = abs ;
y = ord ;
}
int coincide ( Point ) ;
// prototype fonction membre utilisant un objet
.....
} ;

int Point :: coincide ( Point pt )


{
if ( ( pt . x == x ) && ( pt . y == y ) )
return 1 ;
else
return 0 ;
}

Programmation Page 24
Initiation à la programmation orientée objet

int main ( )
{
Point a , b ( 1 ) , c ( 1 , 0 ) ;
// soit : a ( 0,0 ), b ( 1,0 ), c ( 1,0 )
cout << " a et b : " << a. coincide ( b ) ;
// affiche a et b : 0
cout << " b et c : " << b. coincide ( c ) ;
// affiche b et c : 1
}

Passage par adresse :


Il est possible de transmettre explicitement en argument une adresse d'objet. Dans ce cas on a les déclarations
suivantes :

int Point :: coincide ( Point * adr_pt )


{
if ( ( adr_pt -> x == x ) && ( adr_pt -> y == y ) )
return 1 ;
else
return 0 ;
}

Le prototype de la fonction coincide est :

int coincide ( Point * ) ;

Son appel se fait par l'instruction :

a . coincide ( & x ) ;

 A partir du moment où l'on fournit une adresse d'objet à une fonction membre, celle-ci peut en
modifier les valeurs : elle a accès à tous les membres s'il s'agit d'un objet du type de sa classe
ou aux seuls membres publics dans les autres cas.

Passage par référence :


En utilisant cette possibilité spécifique au langage C++ on a :

int Point :: coincide ( Point& pt )


{
if ( ( pt . x == x ) && ( ( pt . y == y ) )
...... etc .............

Le prototype de la fonction est alors :

int coincide ( Point & );

Son appel se fait avec une instruction du type :

a . coincide ( x ) ;

Programmation Page 25
Initiation à la programmation orientée objet

3.4 : Auto référence

Jusqu'à présent on se contentait de noter qu'une fonction membre d'une classe utilisait "certaines
informations" lui permettant d'accéder à l'objet l'ayant appelé. Sans plus de précision.
Il est cependant utile, dans certains cas, de manipuler explicitement l'adresse de l'objet en question.

Exemple :
Pour gérer une liste chaînée d'objets de même nature il faut bien que la fonction membre, pour insérer un
nouvel objet, place son adresse dans l'objet précédent de la liste.

Pour arriver à réaliser cela on utilise le mot clé this qui correspond à l'adresse de l'objet appelant la fonction
membre.
- Ce mot clé n'est utilisable qu'au sein d'une fonction membre ;
- Il désigne un pointeur sur l'objet l'ayant appelé .

En reprenant l'exemple de la classe Point on a la déclaration de la fonction membre affiche ( ) suivante :

void Point :: affiche ( )


{
cout << "Adresse de l'objet : " << this << " \n "
<< "Coordonnées " << x << " "<< y << " \n";
}

int main ( )
{
Point a ( 5 , 2 ) ; // création de deux points
a . affiche ( ) ;
/* affiche l'adresse de l'objet et les coord. du point a */
}

3.5 : Pointeurs sur des fonctions

En dehors de toute considération objet, on peut définir, avec le langage C/C++ des pointeurs sur des
fonctions :

int fonct ( char , float ) ; // prototype d'une fonction


........
int ( *pfonc ) ( char , float ) ;
/* déclaration d'un pointeur sur une fonction
( char , float ) sont les arguments de la fonction */
........
pfonc = fonct ; // initialisation du pointeur
( *pfonc ) ( 'c' , 5.2 ) ;
/* appel de la fonction, via le pointeur, avec les valeurs des arguments */

En P.O.O. on peut étendre cette possibilité aux fonctions membres. Il faut cependant noter que, dans ce cas, il
faut tenir compte aussi du type de la classe dans laquelle la fonction membre est définie.

On déclare les pointeurs sur des fonctions membres selon la syntaxe :

type_renv ( classe : : * nom_point ) ( < types arguments de la fonction > )

En reprenant les exemples précédents on a donc :

Programmation Page 26
Initiation à la programmation orientée objet

void ( Point : : *pfonc )( int , int ) ;

Où pfonc est un pointeur sur une fonction de la classe Point. Cette fonction reçoit deux arguments de
type entier et ne renvoie rien.

pfonc = Point :: deplace ;


// initialisation du pointeur sur la fonction deplace
( a . *pfonc ) ( 3 , 5 ) ;
// déplacement du point a de dx = 3 , dy = 5

4 : Héritage et polymorphisme
Le concept d'héritage est le deuxième pilier de la P.O.O. C'est celui qui- avec le polymorphisme – permet de
constituer des bibliothèques "cohérentes "de classes, réutilisables dans différents programmes.

L'héritage permet en effet, en constituant des classes dérivées d'une classe de base, de réutiliser des composant
logiciels déjà éprouvés : la classe dérivée "hérite" des capacités de la classe de base tout en lui en ajoutant de
nouvelles. Et ainsi de suite .....l'héritage pouvant se réaliser sur plusieurs niveaux de classes.

Le polymorphisme, dans un souci de simplification, permet d'appeler par les mêmes noms ( homonymies )
des fonctions appelées à réaliser le même type de traitement sur les différents objets créés à partir des classes dérivées.

Ainsi si un objet rectangle dérive, plus ou moins directement, de l'objet point, le programmeur pourra appeler
la même fonction membre affiche( ) pour réaliser les affichages à l'écran [ même si, en interne, il y a deux
fonctions membres différentes agissant différemment sur chaque type d'objet ] .

Pour mettre en œuvre les puissants mécanismes nécessaires pour implémenter ces concepts, le compilateur
doit être considéré comme étant "intelligent" car il est amené parfois à effectuer des choix complexes à la simple
lecture du source. Son comportement entraîne une certaine difficulté à appréhender l'ensemble des mécanismes mis
en action

4.1 : Mise en œuvre de l'héritage

Pour comprendre le mécanisme de l'héritage le mieux est de reprendre l'exemple de la classe Point telle qu'elle
était à ses débuts ( sans constructeur ) :

class point
{
int x ; // déclarations des membres privés
int y ;

public :
// déclarations ( en-tête ) des fonctions membres
void initialise ( int , int ) ;
void deplace ( int , int ) ; /
void affiche( ) ;
} ;

// les déclarations de ces fonctions sont inchangées

Programmation Page 27
Initiation à la programmation orientée objet

L'on veut définir une nouvelle classe nommée Pointcoul destinée à manipuler des points colorés. On peut
définir cette classe à partir celle de classe Point à laquelle on ajoutera une information sur la couleur .
Dans ces conditions on dit que la classe Pointcoul est une classe dérivée de la classe Point.

 Il faut noter que, sans mécanisme complémentaire étudié ultérieurement, une fonction membre d'une
classe dérivée, n'a pas accès aux membres privés de la classe dont elle est issue.

Une classe dérivée est définie comme suit :

Syntaxe :

class nomclassderive : < accès > nomclassebase

Nom de la classe de base


modificateur d'accès ( mode de dérivation )

Nom de la classe dérivée

On a donc :

class Pointcoul : public Point


// Pointcoul dérive de Point
{
int coul ;

public : void couleur ( int cl ) // déclaration inline


{
coul = cl ;
}
};
/* public dans la déclaration de la classe signifie que les membres publics de
la classe de base sont membres publics de la classe dérivée */

 Le mode de dérivation est, par défaut 'private'. Le mot peut être omis si l'on souhaite conserver ce
mode.

A partir de cette déclaration on peut alors déclarer des objets, instances de la classe Pointcoul :

Pointcoul a , b ;

Dans l'exemple, chaque objet de type Pointcoul peut faire appel aux méthodes publiques de Pointcoul ( la
fonction membre couleur ) et à celles de la classe de base Point.

Dans la déclaration on se contente de décrire les nouvelles données membres, les nouvelles fonctions membres
et/ou celles qui sont surchargées.

Programmation Page 28
Initiation à la programmation orientée objet

 Une fonction de la classe dérivée surchargée porte le même nom que la méthode "équivalente" de la
classe de base. On peut néanmoins continuer à appeler, à partir de la classe dérivée, la fonction
homonyme de la classe de base en utilisant l'opérateur de résolution de portée ' :: '.

Un objet p de la classe Pointcoul sera affiché par la fonction affiche() redéfinie dans la classe
Pointcoul. Si l'on veut absolument afficher le point p avec la fonction affiche( ) de la classe Point il
faudra écrire l'instruction :

p . point :: affiche ( ) ;

4.2 : Accès aux membres de la classe de base par des objets instanciés de
la classe dérivée

L'accès aux membres de la classe de base dépend conjointement des conditions d'accès indiquées dans la
définition de la classe de base et de celles indiquées dans la déclaration de la classe dérivée ( via les mots réservés
public, protected et private ).

Si on se réfère au cas général :

class A
{
xxxxx :
// xxxxx : mot clé protected, private ou public
int x ;
..........
} ;

class B : yyyyy A
// la classe B est une classe dérivée de la classe A
// yyyyy : mot clé private ou public [ mais pas protected ]
{ ...........
}

Compte tenu des valeurs pouvant être prises par xxxxx et yyyyy il y a 6 possibilités différentes de droits
d'accès :

mode de dérivation statut du membre dans la statut du membre dans


( yyyyy ) classe de base la classe dérivée (*)
( xxxxx )
public public
public protected protected
private inaccessible
public protected
protected protected protected
private inaccessibble
public protected
private protected private
private private
(* ) Statut des membres hérités. Ceux des membres définis dans la classe dérivée dépendent du terme utilisé.

 Dans tous les cas une classe dérivée n'a pas accès aux données privées de sa classe de base.

Programmation Page 29
Initiation à la programmation orientée objet

 En règle générale les droits d'accès sont "réduits" en passant d'une classe à l'autre : au mieux ils
sont conservés, ils ne sont jamais augmentés.

 Les membres protected d'une classe de base restent inaccessibles à l'utilisateur de la classe mais
sont accessibles aux membres d'une classe dérivée [ tout en restant inaccessibles aux utilisateurs de
cette classe dérivée ].

 Pour qu'une fonction membre de la classe dérivée puisse avoir accès aux données privées de la
classe de base, elle doit faire appel aux fonctions membres publiques de cette classe .
Ce qui revient à dire que :
- protected = private pour une tentative d'accès directe ;
- protected = public pour un accès par l'intermédiaire d'une classe dérivée.

Exemple :
Si l'on veut utiliser les coordonnées d'un point ( données privées de la classe Point ), pour afficher un point
en couleur [ objet de la classe Pointcoul ], il faut ajouter à la classe Pointcoul une fonction membre déclarée
comme suit :

void pointcoul :: affichecoul ( )


{
affiche ( ) ;
cout << "la couleur est : " << coul << " \n " ;
}

La fonction affichecoul ( ) en appelant la fonction affiche ( ) de la classe Point récupère les coordonnées d'un
objet .
On fait appel à cette fonction sans spécifier à quel objet elle doit être appliquée : par convention il s'agit de
l'objet ayant appelé affichecoul ( ) .

4.3 : Polymorphisme

Le polymorphisme est mis en œuvre simplement en utilisant les possibilités de redéfinition des fonctions
proposé par le langage C++.

Dans l'exemple précédent les fonctions membres affiche( ) et affichecoul ( ) réalisent en fait le même type
d'action, chacune pour afficher des objets de leur classe de définition. On peut souhaiter leur donner le même nom .

 Cependant si l'on souhaite appeler, depuis la classe dérivée la fonction de la classe de base il faut
utiliser l'opérateur de résolution de portée ' :: '.

Exemple :

Si l'on définit deux fonctions affiche ( ), une au sein de la classe Point et une dans la classe Pointcoul, et
que l'on veut accéder à la fonction affiche de la classe de base on a :

void Pointcoul :: affiche ( )


/* déclaration de la fonction affiche de la classe Pointcoul */
{
point :: affiche ( ) ;
/* appel de la fonction affiche de la classe Point
cout << " La couleur est : " << coul << " \n ";
}

Programmation Page 30
Initiation à la programmation orientée objet

 L'appel de la fonction affiche ( ) se fait sans spécifier à quel objet cette fonction doit être appliquée. Par
convention il s'agit de l'objet ayant appelé la fonction conteneur.

 On peut utiliser directement la fonction point::affiche( ) pour un point de couleur. Dans ce cas
l'instruction :

Pointcoul pc ;
pc . Point :: affiche ( ) ;
/* affiche pc selon le traitement Point :: affiche, c'est à dire
sans la couleur */

4.4 : Appel de constructeurs et de destructeur

On utilise le principe des constructeurs ( et du destructeur ) pour créer (ou détruire)des objets d'une classe
dérivée.

Par rapport au mécanisme de base, la différence essentielle vient du fait qu'il y a mise en place d'une
"hiérarchisation" dans la construction de l'objet concerné :

Pour créer un objet de type B [ classe dérivée de la classe A ] il faut :


- Dans un premier temps, créer un objet de type A, et donc appeler le constructeur de A.
- Ensuite le compléter par ce qui est spécifique à B, en faisant appel au constructeur de B.

Ces opérations sont réalisées automatiquement par le compilateur.

Toutefois si le constructeur de A nécessite des arguments, l'en-tête complet du constructeur de B ( situé dans la
déclaration de la classe ) est de la forme :

class_deriv ( arguments_B ) : class_base ( arguments_A ) ;

Où :
arguments_A : arguments passés au constructeur de la classe de base pour construire un objet de la
classe A.
arguments_B : arguments passés au constructeur de la classe B pour transformer l'objet de classe A
en objet de classe B .

Exemple :

Pointcoul ( int abs, int ord , char coul ):Point( abs, ord ) ;
// Le compilateur transmet au constructeur de point les informations abs et ord

On a alors l'appel du constructeur suivant ( dans la fonction main ( ) ) :

Pointcoul a ( 10,15,5);

ce qui entraîne :
- l'appel du constructeur Point avec les arguments 10 et 15 ;
- l'appel du constructeur Pointcoul avec les arguments 10, 15, 5.

Programmation Page 31
Initiation à la programmation orientée objet

4.5 : Compatibilité entre objets d'une classe de base et objets d'une classe
dérivée

On considère qu'un objet d'une classe dérivée peut "remplacer" un objet d'une classe de base :
- Tout ce que l'on trouve dans une classe de base se trouve également dans la classe dérivée.
- Toute action réalisable sur une classe de base peut l'être sur une classe dérivée.

Ex : Un point coloré peut toujours être traité comme un point. On peut alors afficher ses coordonnées
comme on le ferait pour un point de la classe de base.

 Ces possibilités ne s'appliquent que dans les cas de dérivation publique.

Conversions implicites :
Le principe de base énoncé plus haut se traduit par l'existence de possibilités de conversions implicites :

 Un objet d'une classe dérivée peut être converti en un objet d'une classe de base:

Point a ; // création d'un objet de la classe Point


Pointcoul b ;
// création d'un objet de la classe Pointcoul

a = b ;
/* instruction légale. Il y a conversion de b dans le type Point
( avec perte d'information pour les données supplémentaires ) et
affectation du résultat dans a */

Bien entendu l'inverse n'est pas possible.

 L'opérateur ' = ' est redéfini pour effectuer des affectations d'objets.

 Conversion au niveau des pointeurs :


Un pointeur sur une classe dérivée peut être converti en un pointeur sur une classe de base.

L'intérêt des conversions de pointeur est qu'on peut accéder à tous les types d'objets définis dans les
différentes classes dérivées d'une classe de base en n'utilisant qu'un pointeur, au type de la classe de
base : la conversion de ce pointeur en fonction des besoins permet d'accéder aux différents objets .

On peut donc avoir :

Point *p ;
/* déclaration d'un pointeur sur un objet de la classe Point */

Pointcoul *pc ;
// idem au niveau de la classe Pointcoul
.....
/* initialisation des deux pointeurs sur des objets adéquats */

p = pc ;
/* cette instruction correspond à une conversion du type *pc en
*p : p pointe maintenant sur un objet de type Pointcoul */

Programmation Page 32
Initiation à la programmation orientée objet

 L'opération inverse est possible par transtypage mais n'est guère usitée.
pc = ( pc * ) p ;

Si les deux classes possèdent chacune une fonction affiche ( ), lorsque on a une séquence d'instructions
comme :

Point pt ( 3 , 5 ), *p ;
Pointcoul ptc ( 8, 6, 2 ), *pc ;

p = &pt ;
pc = &ptc ;

alors :

p -> affiche ( )
// appelle la fonction Point :: affiche ( )
pc -> affiche ( )
// appelle la fonction Poincoul :: affiche ( )

Cependant, lorsqu'on réalise la conversion des pointeurs :

p = pc ; // conversion de pointeur

il apparaît un "gros problème". En effet p est du type Point mais pointe maintenant sur un objet de type
Poincoul. On atteint là les limites du typage statique.

Limites liées au typage statique des objets:


Les possibilités de conversion précédentes, réalisées statiquement par le compilateur lors de la création des
modules objets, peuvent conduire à certains problèmes.

Reprenons en effet le cas précédent et créons deux objets :

Point p ( 3 , 5 ) , *pt ;
Pointcoul pc ( 5,5,3 ) , *ptc ;
pt = &p ;
ptc = &pc ;

On voit que lorsqu'on fait la conversion de pointeur p = pc un problème apparaît lorsqu'on veut réaliser une
instruction impliquant une méthode redéfinie dans les différentes classes.
Cela est dû au fait que l'on se trouve dans le cas d'une édition de liens statique: dans ces conditions, l'éditeur de
liens met en place le code de la fonction correspondant au type défini par le pointeur .

Dans l'exemple qui précède il s'agit en l'occurrence du code de Point :: affiche () alors que, du fait de
la conversion des pointeurs, c'est un objet Pointcoul qui appelle la fonction.

De fait, après la conversion, on ne peut plus accéder à la fonction Pointcoul :: affiche ni même à tout membre
qui ne serait défini que dans la classe dérivée.

Programmation Page 33
Initiation à la programmation orientée objet

Utilisation de pointeurs sur des fonctions membres :


On peut utiliser un pointeur pour accéder à une fonction membre d'une classe dérivée.
Prenons, pour cela, les déclarations suivantes :

class point class Pointcoul : public Point


{ {
public: public:
... ...
void deplhor (int); void couleur(int);
void deplvert (int); ...
... };
};

Déclarons maintenant deux pointeurs sur les fonctions membres :

void ( Point :: *pf )( int ) ;


// pointeur sur des fonctions de la classe Point
void ( Pointcoul :: *pfc )( int ) ;
// idem sur les fonctions de la classe Pointcoul

 Les différentes fonctions pointées par le pointeur doivent avoir le même prototype ( mêmes
types d'arguments et même type de valeur renvoyée).

On a donc :

pf = Point :: deplhor ;
// initialisation sur la fonction Point :: deplhor
pfc = Pointcoul :: couleur ;
// idem sur la fonction Pointcoul :: couleur

 Comme les deux fonctions membres "déplacement" de la classe Point sont aussi fonctions
membres de la classe Pointcoul, on pourrait avoir aussi :
pfc = Pointcoul :: deplhor ; ou
pfc = Pointcoul :: deplvert ;

Il y a donc conversion implicite d'un pointeur sur une fonction membre d'une classe dérivée en un pointeur sur
une fonction membre d'une classe de base.
Cela permet, à partir d'un objet d'une classe dérivée, d'accéder aux fonctions membres déclarées dans la classe
de base .

Programmation Page 34
Initiation à la programmation orientée objet

4.6 : Édition dynamique de liens et classes virtuelles

Avec l'édition statique des liens, on peut :


- Réaliser des conversions sur des pointeurs afin de pouvoir accéder, avec un même pointeur, à des objets
de types différents. Mais on n'a aucun moyen pour prendre pleinement en compte le type de l'objet pointé.
- On peut aussi convertir un pointeur sur une fonction d'une classe dérivée en un pointeur sur une fonction
de la classe de base.
- On peut enfin utiliser des fonctions membres qui ont les mêmes noms symboliques et réalisent les mêmes
types d'actions, au sein d'une hiérarchie de classes dérivées .

Ces différentes possibilités permettent d'adresser toutes les fonctions homonymes d'une hiérarchie de classes
avec un seul pointeur, celui-ci pointant sur la fonction membre située le plus en amont.
Néanmoins, il subsiste toujours le problème fondamental cité précédemment : lorsqu'on applique, par un
pointeur, une fonction membre à un objet d'une classe dérivée, c'est la fonction correspondant à la classe du type
initial du pointeur qui est appelée.

Ce problème est résolu par le typage dynamique des objets qui débouche sur la notion de fonction virtuelle,
très importante pour pouvoir utiliser pleinement les possibilités offertes par les notions d'héritage et de
polymorphisme.

Pour pouvoir obtenir l'appel de la méthode correspondant au type pointé il faut que le type de l'objet ne soit
pris en considération qu'au moment de l'exécution [ le type de l'objet désigné par un même pointeur pourra donc varier
au cours du déroulement du programme ].
On parle alors de typage dynamique. Cette possibilité est employée dans le cas où une hiérarchie de classes est
constituée et où :
- Les méthodes de classes différentes réalisant les mêmes types d'action ont le même identifiant
( polymorphisme ) ;
- On accède aux objets des différentes classes par un pointeur pointant sur la classe de base ;
- On cherche à appeler les méthodes homonymes des classes dérivées via les pointeurs.

Mécanisme des fonctions virtuelles :


Pour pouvoir accéder aux méthodes correspondant au type de l'objet pointé on utilise le mot clé virtual, ce qui
a pour effet de déclarer comme étant "virtuelle" la méthode homonyme de la classe de base [ celle du type du
pointeur pointant l'objet de la classe dérivée ].

class Point
{
.............
virtual void affiche ( ) ;
.............
}

Grâce à cette déclaration le compilateur sait que les éventuels appels à la fonction affiche ( ) devront être
résolu par une ligature dynamique et non plus statique.
Pour cela, lors de l'analyse du source, à chaque fois qu'il rencontre des instructions du type
"pobj ->affiche ( ); " il se contente de mettre en place un dispositif permettant de n'effectuer le choix de
la fonction à appeler qu'au moment de l'exécution du programme. Ce choix étant basé sur le type exact de
l'objet ayant effectué l'appel de la fonction.

 Il n'est pas nécessaire de déclarer ' virtuelle' dans les classes dérivées une fonction déclarée
virtuelle dans une classe de base.

 Le typage dynamique est limité à un ensemble de classes dérivées les unes des autres .

 Un constructeur ( ou un destructeur ) ne peut pas être déclaré virtual.

Programmation Page 35
Initiation à la programmation orientée objet

Programmation Page 36
Initiation à la programmation orientée objet

Extension de la notion de "virtuelle":


Il y a un autre cas où le typage statique est pris en défaut et où il faut recourir à un typage dynamique :

Exemple :

class Point
{
int x , y ;

public :
Point ( int abs = 0 , int ord = 0 ) ;

void identifie ( ) // déclaration inline


{
cout << "Je suis un point \n ";
}

void affiche ( )
{
identifie ( ) ;
cout << "Coordonnées : " << x << " "<< y << "\n";
}
} ;

class Pointcoul : public Point // classe dérivée


{
int coul ;

public :
..........
void identifie ( )
{
cout << "Je suis un point coloré de couleur :" << coul << "\n" ;
}
} ;

Dans le programme on crée ensuite un point de couleur :

.............
Pointcoul pcoul ( 5 , 5, 2 ) ;
pcoul . affiche ( ) ;
.............

A l'exécution ce sera néanmoins la fonction Point :: identifie( ) qui sera appelée, au sein de la méthode affiche
( ).

La raison en est que lors de la compilation de l'appel pcoul.affiche( ), le compilateur a appelé la fonction
Point :: affiche( ). Mais, dans le corps de cette fonction, l'appel identifie( ) a déjà été compilé en un appel
de Point :: identifie ( ) .
Pour que ce soit la fonction Pointcoul :: identifie ( ) qui soit appelée, il faut, là aussi, utiliser le mot clé virtual
lors de la définition de la fonction Point :: identifie ( ).
Dans ce cas l'appel de la fonction n'est plus réalisé par l'objet lui-même mais par la fonction affiche ( ). Le fait
de déclarer la fonction Point :: affiche ( ) virtuelle fait que le compilateur "remonte à l'origine de l'appel " et
appelle la fonction qui correspond au type de l'objet ayant réalisé cet appel .

Programmation Page 37
Initiation à la programmation orientée objet

Classes abstraites ( ou virtuelles ) :


Avec cette notion de fonction virtuelle il est enfin possible d'imaginer de faire "remonter" toutes les
déclarations des fonctions homonymes au niveau de la classe de base, de manière à ce que ce soit le pointeur
pointant sur cette classe qui soit utilisé dans tous les cas.

Pour cela on déclare dans la classe de base un certain nombre de fonctions virtuelles. Si ces fonctions n'ont pas
d'utilité au sein de cette classe on peut les déclarer avec un corps vide ( corps réduit à { } ) .

Quand cela est réalisé, le programmeur qui utilisera la hiérarchie de classes ainsi constituée sera sûr
d'appliquer les bonnes méthodes sur les différents objets manipulés, sans avoir à connaître exactement
comment l'appel de la fonction est réalisé.

On peut être amené [ et c'est même conseillé lorsqu'on réalise une hiérarchisation importante de classes ] à
définir des classes qui ne serviront pas à créer des objets mais simplement à donner naissance à des hiérarchies
de classes, par héritage, et à faciliter leurs manipulations.

Ces classes, qui ne peuvent être que des classes de base, sont dites classes abstraites.

Exemple :
En reprenant l'exemple de la classe Point on peut imaginer une classe de base appelée Position, située
en amont, dont la structure serait :

class Position
{ int x , y ;
public :
Position ( int initx , int inity )// constructeur
{
x = initx ;
y = inity ;
}
}

Cette classe ne fait rien. Mais il est possible d'y définir les "squelettes" des méthodes qui seront redéfinies
dans le corps des classes qui en seront dérivées (dont la classe Point ).

Il faut donner une définition à ces fonctions virtuelles même si on ne sait pas encore quelles actions elles
réaliseront dans les classes dérivées. Une solution est de prévoir des définitions vides [ = bloc d'instruction
vide ], mais cela peut, dans certains cas, induire des erreurs.

On peut pallier à cet inconvénient en définissant des fonctions virtuelles pures: ces fonctions virtuelles ont une
définition nulle et non seulement vide.

Elles sont déclarées comme suit :

virtual type_renv nom_fonc ( ...) = 0 ;

Exemple :
virtual void affiche ( ... ) = 0 ;

Une classe contenant au moins une fonction virtuelle pure est considérée comme étant une classe abstraite et
il n'est plus possible de déclarer des objets à 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.
Si elle est de nouveau déclarée virtuelle pure, la classe dérivée est elle aussi abstraite .

Programmation Page 38
Initiation à la programmation orientée objet

5 : Classes génériques
Il y a des cas où la création d'une classe, permettant de gérer un certain type d'objet, bien que satisfaisante à
l'emploi, se révèle à l'usage limitée au niveau de sa réutilisabilité. Cela parce qu'elle a été construite avec des données
membres de types particuliers. Si l'on veut utiliser cette classe avec des données membres d'autres types, il faut créer
une nouvelle classe, de structure strictement équivalente, ce qui n'est pas satisfaisant .

Exemple :
Une classe a été créée et permet de construire et manipuler d'une manière sûre des tableaux d'entiers. Si
l'on veut utiliser des entiers longs, ou des flottants, il faut reconstruire de nouvelles classes pour pouvoir
réaliser les mêmes types de traitements avec ces nouveaux types.

La notion de classes génériques ( ou de classes patrons ou templates ) est introduite par la langage C++
pour remédier à cet inconvénient.

 Ce mécanisme est une généralisation de celui de fonction générique ( ou fonctions patrons ) vu


précédemment.

En utilisant le mot réservé template on peut donc définir des familles de classes.

template < class type_var > class nom_classe


{
..................
type_var membre_classe ;
.................
}

où : type_var est un type quelconque de classe ;


membre_classe est une instance de ce type .

Exemple :

template < class type_var > class Liste


{
public :
struct ELEMENT // déclaration d'une structure
{
type_var valeur ;
type_var *suivant ;
} ;

void ajoute ( type_var& valeur ) // fonction membre


{
ELEMENT *temp ;

temp = new ELEMENT ;


temp -> valeur = valeur ;
temp -> suivant = NULL ;
courant -> suivant = temp ;
courant = temp ;
}

Liste ( ) // 1° constructeur
{
courant = debut ;
}

Programmation Page 39
Initiation à la programmation orientée objet

Liste ( type_var& val ) // 2° constructeur


{
courant = debut ;
ajoute ( val ) ;
}

protected :
ELEMENT *debut ;
ELEMENT *courant ;
} ;

On constate que l'on a là une définition générale de classe: il n'y a plus qu'à définir précisément les types à
donner à type_var pour disposer de classes de fonctionnalités identiques adaptées aux différents types de variables
manipulées.

Pour cela on crée les nouvelles classes, à partir de cette classe générique, en utilisant la syntaxe suivante:

nom_classe_gen < type > nom classe_cree ;

Dans l'exemple précédent on peut créer les classes suivantes :

Liste < int > list_entiers ;


/* création de la classe list_entiers utilisant des entiers */

Liste < long > list_long ;


// idem pour des variables de type long

 Il est possible d'initialiser la classe ainsi créée :

Liste < int > list_entiers ( 10 ) ;


// le premier élément à la valeur 10

 Il est possible de créer plusieurs classes au même type :

liste < int > listentiers1 ( 10 ) ;


liste < int > listentiers2 ;
// 2 classes au même type

 La généricité peut s'hériter : dans ce cas la classe dérivée doit aussi être déclarée comme une classe
générique .

Programmation Page 40

Você também pode gostar