Académique Documents
Professionnel Documents
Culture Documents
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.
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".
Opérateur point
Programmation Page 1
Initiation à la programmation orientée 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".
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.
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.
class Point
{
// déclarations des données membres
int x ;
int y ;
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 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.
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.
class Point
{
// déclarations des données membres
int x ;
int y ;
Point a , b ;
// déclaration de deux objets de type Point.
Programmation Page 4
Initiation à la programmation orientée objet
Objet a
x
ptaff
Module objet de la
méthode deplace
Objet b x
y
Module objet de la
ptin méthode affiche
ptdep
ptaff
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 ;
etc ..........
Programmation Page 5
Initiation à la programmation orientée objet
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.
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.
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.
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
Lorsqu'on crée différents objets à partir d'une même classe, chacun d'entre eux possède ses propres données
membres .
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 ;
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 :
class Point
{
int x ;
int y ;
public :
Point( int , int ) ; // constructeur
void deplace( int , int ) ;
void affiche( ) ;
} ;
avec :
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 ].
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 :
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.
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
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.
Programmation Page 10
Initiation à la programmation orientée objet
Programmation Page 11
Initiation à la programmation orientée objet
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 ;
Programmation Page 12
Initiation à la programmation orientée objet
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 :
Objets dynamiques :
On peut créer dynamiquement un objet en utilisant l'opérateur new :
class point
{
int x ; // déclarations des membres
int y ;
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 */
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 :
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 :
Et la définition :
p. x = x + a.x ;
p. y = y + a.y ;
return p ;
}
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
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.
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
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 :
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 :
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
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 ;
}
void affiche( )
{
cout << " coord. x : " << x <<
" coord. y : " << y << "\n" ;
}
}
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
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 ].
Exemple :
class Point
{
.........
friend int coincide ( Point , Point ) ; // prototype
.........
}
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 ].
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 :
- 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.
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 */
.......
} ;
Programmation Page 20
Initiation à la programmation orientée objet
class A
{
............
friend void famie( A , B ) ;
............
} ;
class B
{
..........
friend void famie( A , 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 :
class Point
{
.............
friend Point operator + ( Point , Point ) ;
// prototype au sein de la classe
..............
} ;
p. x = a . x + b . x ;
p. y = a . y + b . y ;
return p ;
} ;
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
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 "
}
Programmation Page 23
Initiation à la programmation orientée objet
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.
Syntaxe :
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
.....
} ;
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
}
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.
a . coincide ( x ) ;
Programmation Page 25
Initiation à la programmation orientée objet
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é .
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 */
}
En dehors de toute considération objet, on peut définir, avec le langage C/C++ des pointeurs sur des
fonctions :
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.
Programmation Page 26
Initiation à la programmation orientée objet
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.
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
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( ) ;
} ;
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.
Syntaxe :
On a donc :
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 ).
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 :
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 :
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 :
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 */
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é :
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 :
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
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.
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:
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 */
L'opérateur ' = ' est redéfini pour effectuer des affectations d'objets.
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 .
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 ( )
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.
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
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
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.
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 .
Programmation Page 35
Initiation à la programmation orientée objet
Programmation Page 36
Initiation à la programmation orientée objet
Exemple :
class Point
{
int x , y ;
public :
Point ( int abs = 0 , int ord = 0 ) ;
void affiche ( )
{
identifie ( ) ;
cout << "Coordonnées : " << x << " "<< y << "\n";
}
} ;
public :
..........
void identifie ( )
{
cout << "Je suis un point coloré de couleur :" << coul << "\n" ;
}
} ;
.............
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
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.
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.
En utilisant le mot réservé template on peut donc définir des familles de classes.
Exemple :
Liste ( ) // 1° constructeur
{
courant = debut ;
}
Programmation Page 39
Initiation à la programmation orientée objet
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:
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