Cours C++ les Classes imbriquées

Cours C++ les Classes imbriquées
...
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 ( )
......
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).
En programmation structurée traditionnelle, il est donc possible de définir un type de structure nommé point défini comme suit :
struct POINT
{
int x ; // coordonnée x du point
int y ; // coordonnée y du point
} ;
POINT a, b ;
// a , b deux variables de type structure point
x et y sont les champs ( ou les membres ) de la structure point. L'accès aux membres de a ou b se fait, bien entendu, par l'opérateur ' . ' ( point ) [ a.x ou b.y par exemple ].
A partir de cette définition de structure il est possible de décrire divers traitements manipulant des variables créées à ce type: sous-programmes Afficher ( ), Déplacer ( ), Intialiser ( ), Cacher ( ), etc.
1.1 : Déclaration d'une classe
La caractéristique de base de la P.O.O. est de pouvoir regrouper dans une entité unique les déclarations des différentes données composant la structures et les descriptions des différents sous-programmes manipulant ces données. Cette entité globale est une classe.
Dans l'exemple qui nous guide, les fonctions membres seront :
- initialise : pour donner des valeurs aux coordonnées d'un point,
- deplace : pour modifier les coordonnées,
- affiche : pour afficher le point à l'écran .
La structure point initiale et ses sous-programmes satellites deviennent alors :
class Point
{
// déclarations des données membres
int x ;
int y ;
// prototypes des fonctions membres ( méthodes )
void initialise ( int , int ) ;
void deplace ( int , int ) ;
void affiche ( ) ;
} ;
Par tradition, les identifiants de types structurés étaient écrits en majuscules. Cette tradition se perd avec la déclaration des classes mais on conserve l'habitude de nommer une classe avec se première lettre en majuscule.
Une donnée membre peut être un objet instancié par rapport à une autre classe, définie précédemment.
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.
1.3 : Déclaration et invocation des fonctions membres
Déclaration :
Il faut évidemment déclarer les fonctions membres de cette structure. Il existe pour ce faire deux manières :
1 / Si la fonction a un code court, elle peut être définie au sein même de la déclaration de celle de la classe [ fonction dite " inline" ].
2 / Dans l'alternative il faut définir les fonctions à l'extérieur de la déclaration de classe.
Fonctions inline :
On déclare ces fonctions selon le modèle suivant :
struct point
{
int x ;
int y ;
void initialise ( int abs , int ord )
{
x = abs ;
y = ord ;
}
// remarquez que le mot réservé "inline" n'apparaît pas
etc ..........
}
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
..........
- initialise ( 3 , 5 ) ;
// initialisation de a.x à 3 et a.y à 5
- deplace ( 1, -1 ) ;
// on a alors a.x = 4 et a.y = 4
cout << "Position de a : " << a.x a.y ;
Dans l'état actuel de la construction, on peut donc accéder aux données de l'objet à partir de n'importe quel endroit du code par l'opérateur ' .' , mais cela va à l'encontre du principe d'encapsulation.
1.4 : Membres statiques
Lorsqu'on crée différents objets à partir d'une même classe, chacun d'entre eux possède ses propres données membres .
Par exemple dans la structure Point on a :
Point a : Point b:
a . x b . x
a . y b . y
Il se peut néanmoins qu'une donnée soit commune à tous les objets de la classe. Ce qui revient à dire que toute modification réalisée sur la donnée membre d'un objet est répercutée sur la donnée membre équivalente de tous les objets instanciés de la classe.
Pour cela il suffit de déclarer la donnée concernée avec le mot clé static .
class Exemple
{
static int n ; // déclaration d'un membre statique
int z ;
} ;
Exemple a , b ;
/* création de deux objets : la donnée a.n et b.n est la même pour a et pour b */
Les membres statiques existent en un seul exemplaire indépendamment des objets de la classe correspondante
Les membres statiques sont toujours initialisés à 0. Mais ils ne peuvent pas être initialisés au même moment que leur définition .
On ne peut initialiser un membre statique qu'à la suite de la définition de la classe, en se référençant à cette dernière :
ex : int Exemple :: n = 2 ;
1.5 : Constructeur et destructeur
Le langage C++ implémente un mécanisme original permettant d'instancier ( = créer ) ou de détruire des objets à partir de la définition d'une classe. Il s'agit de l'utilisation de deux types de méthodes particulières : le (ou les) constructeur ( s ) et le destructeur.
Dans l'état actuel de la construction de la classe Point, il est nécessaire d'utiliser une fonction membre de la classe pour pouvoir initialiser les différentes données d'un objet après que celui-ci ait été déclaré ( = créé ).
Cette démarche implique que l'utilisateur de la classe pense à appeler la fonction adéquate, au bon moment, à chaque fois qu'il souhaite créer un nouvel objet.
L'utilisation du constructeur et du destructeur va permettre de faciliter la création et la destruction des objets tout en mettant à la disposition du programmeur des possibilités plus élaborées.
Le constructeur :
Un constructeur est une fonction membre spéciale définie au sein de chaque classe. Elle est appelée automatiquement à chaque création d'objet [ on verra plus tard que cette appel peut être statique, dynamique ou automatique ].
Par "automatiquement" il faut comprendre "sans appel explicite – au sein du source - de la part du programme. Une fois qu'un constructeur est créé, c'est le compilateur qui se charge de l'appeler lorsqu'il en a besoin, à chaque création d'objet.
Un constructeur :
- Est identifiable par le fait qu'il porte le nom de la classe auquel il appartient.
- Il ne retourne aucune valeur [ même le spécificateur void est omis ].
- Il peut admettre des arguments : ce sont, le plus souvent, les valeurs d'initialisation des différents champs de l'objet construit.
Exemple :
La classe Point définie précédemment devient :
class Point
{
int x ;
int y ;
public :
Point( int , int ) ; // constructeur
void deplace( int , int ) ;
void affiche( ) ;
} ;
avec :
Point :: Point ( int abs , int ord )
// définition du constructeur
{
x = abs ;
y = ord ;
}
Un constructeur ne peut être déclaré ni static, ni const, ni virtual.

A partir du moment où un constructeur est défini, on doit créer ( et initialiser ) un objet de la manière suivante :
Point a ( 1 , 2 ) ;
// création de l'objet a initialisé à ( 1 , 2 )
// il n'y a pas d'appel explicite au constructeur.
Il n'est plus possible de créer un objet sans fournir les arguments d'initialisation [ sauf si le constructeur ne possède pas d'argument ].
On peut alors avoir le programme suivant ( conventions habituelles ) :
int main ( )
{
point a ( 2 , 12 ) ;
// création et initialisation d'un point a
a . affiche ( ) ; // affichage à l'écran
a . deplace ( 2 ,5 ) ; // déplacement
a . affiche ( ) ; // affichage à l'écran
}
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.
1.6 : Construction, initialisation et destruction d'objet
En langage C traditionnel, une variable peut être créée de deux façons :
- Par une déclaration :
La variable peut alors être automatique, static ou globale en fonction de sa nature et de l'emplacement de sa déclaration.
Dans ces trois cas la variable est créée lors de la compilation. On dit qu'elle est statique.
- En faisant appel à des fonctions de gestion dynamique de la mémoire :
La variable est alors dite dynamique. Sa durée de vie est contrôlée par le programmeur.
En langage C++, on dispose des mêmes possibilités pour créer les objets. Leur gestion dynamique se fera néanmoins de préférence avec les opérateurs new et delete.
Il pourra donc y avoir des objets statiques ( automatiques, static et globaux ) ou dynamiques.
Objets statiques :
¤ Objets automatiques :
Ils sont créés par une déclaration réalisée au sein d'une fonction ou dans un bloc d'instructions dépendant d'une structures de contrôle. Ils sont détruits à la fin de l'exécution de la fonction ou à la sortie du bloc.
¤ Objets statiques et globaux :
Ils sont créés en dehors de toute fonction ou au sein d'une fonction, lorsqu'ils sont précédés du qualificatif static .
Ils peuvent être créés avant le début de l'exécution de main et détruits après la fin de son exécution .
Exemple :
#include <iostream.h>
class Point
{
int x , y ;
point (int abs , int ord ) // constructeur inline
{
x = abs ;
y = ord ;
cout << " Construction d'un point : "
<< x << " " << y << " \n";
}
~point ( ) // destructeur
{
cout << " Destruction du point :
" << x << " " << y << " \n " ;
}
} ;
Point a (1 ,1 ) ; // création d'un objet statique global
int main ( )
{
point b (10 , 10 ) ; // création d'un objet automatique
int i ;
for ( i = 1 ; i <= 3 ; i ++ )
{
cout << " Tour de boucle N° " << i << " \ n ";
point c ( i , 2* i ) ;
// objets automatiques créés dans un bloc
}
cout << " Fin de main ( ) " ;
}
Le programme affichera :
Construction d 'un point : 1 1
Construction d'un point : 10 10
Tour de boucle N°1
Construction d'un point : 1 2
Destruction du point : 1 2 // le destructeur est "appelé" automatiquement
// par le compilateur
Construction d'un point : 2 4
Destruction du point : 2 4 // idem
Construction d'un point : 3 6
Destruction du point : 3 6 // idem
Fin du main ( )
Destruction du point : 10 10 // l'affichage ne paraîtra qu'en visionnant la
Destruction du point : 1 1 // fenêtre user
Objets dynamiques :
On peut créer dynamiquement un objet en utilisant l'opérateur new :
Avec la classe Point définie, sans constructeur, comme suit :
class point
{
int x ; // déclarations des membres
int y ;
// déclarations( en-tête )des fonctions membres
void initialise ( int , int ) ;
void deplace ( int , int ) ;
void affiche ( ) ;
} ;
on peut créer dynamiquement un objet :
Point *p_adr ; // déclaration d'un pointeur de type point
p_adr = new Point ;
// création dynamique d'une zone mémoire "objet point"
..........
p_adr -> initialise ( 1 , 3 ) ;
// accès à la méthode de l'objet pointé par p_adr
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.
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 :
Le prototype d'une telle fonction est à intégrer dans la définition de la classe concernée.
Exemple :
On souhaite surdéfinir l'opérateur ' + ' afin qu'il soit en mesure de réaliser l'addition de deux objets points [ par convention, le résultat est un point dont les coordonnées sont égales à la somme des coordonnées ].
On a alors le prototype :
Point operator + ( Point ) ;
/* fonction membre à inclure dans la définition de la classe Point : elle s'applique à un objet Point et renvoie un objet Point */
Et la définition :
Point Point :: operator + ( Point a )
{
Point p ;
- x = x + a.x ;
- 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 ) ;
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.