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". ..... ObjetCible . methode ( ) ......
Action à faire réaliser par l'objet 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 ; int y ; } ;
// //
coordonnée x du point 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 y ptin ptdep ptaff
Objet b
x y
Module objet de la méthode initialise
Module objet de la méthode deplace
Module objet de la méthode affiche
ptin 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 { x = abs ; y = ord ; }
abs , int
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 ;
Programmation
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. 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 a.y
b.x 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 ; int z ; } ;
// déclaration d'un membre statique
Exemple a , b ; /* création de deux objets : la donnée a.n et b.n est la pour b */
même pour a et
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 ) ; // void deplace( int , int ) ; void affiche( ) ; } ;
constructeur
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 constructeur ne possède pas d'argument ].
[ sauf si le
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
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 Construction d'un point : 2 4 Destruction du point : 2 4 Construction d'un point : 3 6 Destruction du point : 3 6 Fin du main ( ) Destruction du point : 10 10 Destruction du point : 1 1
// le destructeur est "appelé" automatiquement // par le compilateur // idem // idem // l'affichage ne paraîtra qu'en visionnant la // 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 ; int y ;
// déclarations des membres
// 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 ( 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
Point :: operator
+ ( Point a )
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 a.y // a = //
= b.x ; // possible = b.y ; illégal, provoque une erreur à la compilation 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 : " << " coord. y : " << y }
+
x << << "\n" ;
} Point Point :: operator + ( Point a ) { point p ; x = x + ax ; y = y + ay ; return p ; }
int main ( ) { Point a ( 1 a . affiche Point b ( 5 Point c ; c = a + b ; c . affiche getch ( ) ; return 1 ; }
Programmation
, 2 ) ; ( ) ; , 10 ) ; // c prend les valeurs par défaut ( ) ;
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 { if ( ( p.x == q.x ) && ( p.y == q.y ) ) return 1 ; else return 0 ; } /* REMARQUE : la fonction coincide peut être programme */
membre de la classe */
utilisée par d'autres parties du
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 type A ou B */ { ........... }
objet de
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 */ ..... }
Programmation
Il faut cependant toujours annoncer la classe amie B [ ligne : class B; ] avant de déclarer la classe A.
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 ( 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 { x = 0 ; y = 0 ; }
( )
//
Point :: Point ( int abs ) { x = y = abs ; }
déclaration 1° constructeur
// déclaration du 2° constructeur
Point :: Point ( int abs, int ord ) // déclaration du 3° constructeur { x = abs ; y = ord ; } void Point :: affiche ( ) { cout << " je suis en : " << x << " } void Point :: affiche ( char { cout << message; affiche ( ) ; }
" << y
<< " \ n ";
*message )
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 // cout << " a // cout << " b // }
( 1 ) , c ( 1 , soit : a ( 0,0 et b : " << a. affiche a et b et c : " << b. affiche b et c
0 ) ; ), b ( 1,0 ), c ( 1,0 ) coincide ( b ) ; : 0 coincide ( 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 int y ;
membres
privés
public : // déclarations ( en-tête ) des fonctions membres void initialise ( int , int ) ; void deplace ( int , int ) ; / void affiche( ) ; } ; // Programmation
les déclarations de ces fonctions sont inchangées 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 : int x ; .......... } ;
mot clé protected, private ou
public
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 ( yyyyy ) public protected private
statut du membre dans la classe de base ( xxxxx ) public protected private public protected private public protected private
statut du membre dans la classe dérivée (*) public protected inaccessible protected protected inaccessibble protected 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. l'instruction : Pointcoul pc ; pc . Point :: affiche ( ) ; /* affiche pc selon le traitement Point :: sans la couleur */
Dans ce cas
affiche, c'est à dire
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 { public: ... void deplhor (int); void deplvert (int); ... };
class Pointcoul : public Point { public: ... void couleur(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 << " } } ; class Pointcoul : public Point { int coul ;
//
"<< y << "\n";
classe dérivée
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 .
Programmation
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 .
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 ; ................. } type_var est un type quelconque de classe ; membre_classe est une instance de ce type .
où :
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& { ELEMENT *temp ;
valeur )
//
fonction membre
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 ) { courant = debut ; ajoute ( val ) ; }
//
2° constructeur
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