La création de classes et d'objets
Sommaire du chapitre
3. Les objets repérés par un pointeurs
5. Classes emboîtées avec pointeurs
6. Limite de visibilité (accolades)
7. Surcharge et paramètres par défaut
8. Constructeur et destructeur d'une classe
9. Placement du code des méthodes (inline)
10. Code commun réentrant (offline)
Introduction : La programmation orientée objet
La classe et l' objet sont à la base de la programmation dite
orientée objet.
Le C++ est justement une extension de C qui inclut précisément ces concepts.
Définitions
Classe : définition d'un module contenant des variables et des
fonctions
Objet : instance d'une classe
Dans un langage impératif normal (Pascal, C) chaque variable possède un type,
simple ou structuré qui décrit ce que contient la variable.
On peut décrire les types séparément des variables puis créer autant de variables
de ce type qu'il est nécessaire, soit par des déclarations (variables statiques)
soit en leur allouant de la place sur le tas (variable dynamique).
Les classes
Une classe peut être vue comme une structure C ou un enregistrement Pascal
dans lequel on a défini des fonctions en plus des champs habituels.
Exemple discuté de définition d'une classe
class <MonExempleDeClasse>
{
public:
int x;
void f();
void g(int x) { <instructions> };
} ;
Une classe est définie par le mot-clé class suivi du nom de la classe,
suivi de la définition des membres de la classe entre accolades.
Le mot-clé public indique que les membres qui suivent peuvent être accédés
par des procédures ou des objets extérieurs.
Ce point sera revu plus loin, mais dans les premières explications on mettra
toujours public , pour qu'on puisse illustrer la syntaxe d'accès.
La classe A ci-dessus contient un entier de nom x et deux fonctions f() et g().
Des paramètres peuvent être définis dans les définitions de fonctions comme
dans les fonctions normales de C.
Il y a deux façons de définir le code des fonctions, ainsi que nous le verrons
en détail plus loin: à l'intérieur de la classe ou à l'extérieur de la classe,
en répétant le nom de la classe et celui de la fonction, A::f(), devant le code.
Le code de la fonction f()}doit donc être défini quelque part en dehors de la
classe.
Initialisation de variables
Il y a diverses façons d'initialiser des variables et des classes en C++ :
/* initialisation identique à celle de C */
int i=3;
/* initialisation inspirée de Simula */
int j (3);
/* initialisation qui utilise une variable précédemment utilisée */
int k (j+2);
/* déclaration de classe et instanciation simultannée */
class ClassePersonne { char prenom[10] , nom[10] } ObjetPersonne[]
= {"Charles","Bronson"};
On peut initialiser des classes comme des structures C.
On a défini comme objet ObjetPersonne un tableau de class ClassePersonne
de longueur indéfinie, cette longueur est définie par le nombre de noms d'initialisation.
Les objets
L'objet est obtenu par la création (ou instanciation ) d'une classe.
Exemple discuté d'instanciation d'un objet
/* Instanciation */
<MonExempleDeClasse> <MonInstance>;
Ayant défini la classe <MonExempleDeClasse>, on peut créer un objet correspondant
à cette classe, de la même manière qu'on crée une variable d'un certain type
en C, dans n'importe quel autre objet ou procédure incluant la classe.
Pour accéder à un membre de l'objet ainsi créé, on ajoute un point et le nom
du membre après le nom d'objet:
/* Mise a jour d'une valeur */
<MonInstance>.x = 0;
/* evaluation de f() */
<MonInstance>.f();
Lors des instanciations, seule la place pour les membres correspondant aux
données est réservée.
Le code des fonctions n'est pas reproduit dans chaque objet.
On ne gaspille donc pas de place en créant beaucoup d'objets du même type (pour
être précis, on a bien une référence à une table de classe pour chaque objet
créé).
Remarque :
Il est possible de combiner la déclaration et l'instanciation en plaçant le
nom de l'objet entre l'accolade droite et le point virgule. Cela explique pourquoi
le point virgule est obligatoire après l'accolade qui ferme une classe, même
s'il n'y a pas de nom.
/* Déclaration avec instanciation */
class <MonExempleDeClasse> { <DefinitionDeLaClasse> }
<MonInstance>;
3. Objets repérés par un pointeur
Les pointeurs sur les objets ont la même forme que les pointeurs sur les variables des langages simples.
Exemple :
MonExempleDeClasse *Pointeur;
Pointeur = new MonExempleDeClasse;
ou
MonExempleDeClasse *Pointeur = new MonExempleDeClasse;
La premiere définition définit un pointeur sur un objet de la classe
MonExempleDeClasse.
L'appel a la commande new réserve de la place pour cet objet sur le tas
et dépose son adresse dans le pointeur.
On verra plus loin comment utiliser des paramètres d'initialisation.
Dans la seconde définition, on voit qu'on peut regrouper en une seule
instruction la définition du pointeur, et l'allocation de sa mémoire.
Pointeur >x = 0;
Pointeur >f();
Ces expressions montrent comment référencer un membre à partir d'un pointeur sur l'objet.
delete Pointeur;
Finalement on peut récupérer la place qui avait été réservée pour l'objet par
l'appel à l'instruction delete.
L'instruction delete est donc l'opposé de new.
Comme les types normaux, les classes peuvent en inclure d'autres :
class MonExempleDeClasseEmboitee { public : int a;};
class MonExempleDeClasseEmboitant
{
public :
int b;
MonExempleDeClasseEmboitee mb;
}
Ces lignes définissent une classe MonExempleDeClasseEmboitant qui a comme membres un entier et un objet contenant toute la structure de la classe MonExempleDeClasseEmboitee.
MonExempleDeClasseEmboitant Objet;
Objet.b = 0;
Objet.mb.a = 0;
La classe B s'utilise de la façon habituelle (2 premières lignes).
La dernière ligne montre comment utiliser le sous-membre a du membre
mb.
Pour chaque nouvel emboîtement on ajoute un point.
5. Classes emboîtées avec pointeurs
class A
{
public :
int a ;
int *b = new int;
}
class B
{
public :
A *md = new A;
A me;
}
Dans le cas ci-dessus, la classe B contient un pointeur à la classe A , qui elle-même contient un pointeur sur un entier.
B *Objet = new B ;
Lorsque l'on instancie la classe B deux objets de classe A sont instanciés.
Objet->mb->a =0 ;
Objet->mc.a = 0 ;
Lorsque l'on fait référence aux champs, on fait suivre le nom d'un point lorsqu'il s'agit d'une variable normale et d'une flèche lorsqu'il s'agit d'un pointeur.
*(Objet->md->b) =0 ;
Cette ligne fait référence à l'entier dont la référence est contenue dans le membre b .
6. Limite de visibilité (accolades)
class XX
{
A var1 ;
void FonctionMembre()
{
B var2 ;
var1 = 0;
{
C var3;
var3 = var1 ;
}
}
}
Les accolades forment des blocs imbriqués les uns dans les autres.
Les variables et les objets peuvent être déclarés n'importe où, mais ils ne
sont connus que depuis leur définition et jusqu'à la fin du bloc qui les contient.
var1 est connue jusqu'à la fin de la classe
var2 sur l'ensemble de FonctionMembre
var3 uniquement dans le groupe compris dans FonctionMembre et entre accolades.
Les membres non pointeurs sont instanciés lorsque l'on arrive dans leur bloc
et détruits lorsqu'on en sort.
Les pointeurs ne sont naturellement instanciés que sur demande.
7. Surcharge et paramètres par défaut
Dans l'exemple ci-dessous on a défini trois fois la fonction f().
Chacune des définitions se différencie des autres par le nombre ou le type des
paramètres :
class A
{
public :
void f();
void f(char);
void f(int , int = 0,int = 15);
}
Dans la dernière définition, les deux derniers paramètres ont
des valeurs par défaut, c'est-à-dire qu'on peut appeler cette fonction avec
1, 2 ou 3 paramètres.
Si on l'appelle avec 1 paramètre, les deux derniers sont reçus dans la fonction
avec la valeur indiquée par défaut.
La fonction ensuite appelée dans le code du programme sera reconnue en fonction du nombvre de paramètres de celle-ci :
Exemples d'appel :
A aa;
aa.f("m");
aa.f(3,12);
8. Constructeur et destructeur d'une classe
class XXX
{
method(...) { ... }
public :
XXX(int i); /* premier constructeur */
XXX(); /* second constructeur */
~XXX(); /* destructeur */
}
Dans toute classe, il est possible de définir deux sortes de fonctions particulières,
nommées respectivement constructeur et destructeur.
Le constructeur est une méthode dont le nom correspond exactement à celui de
la classe.
Il peut y avoir zéro, un ou plusieurs constructeurs, différenciés par le nombre
ou le type (classe) de leurs paramètres.
Les paramètres par défaut définis au paragraphe sont utilisables pour les constructeurs
comme pour les méthodes.
Le constructeur est exécuté à l'instanciation, qu'elle soit faite par déclaration
ou par appel à new.
Il sert à initialiser les variables locales :
XXX r(12) ; /* appel au premier constructeur */
XXX s ; /* appel au second constructeur */
XXX *v = new XXX(15) ;
XXX *w = new XXX();
Remarque :
Notons que s'il s'agit d'une instanciation d'objet pointé et qu'il n'y a pas
de paramètres, on a le choix de mettre des parenthèses vides ou de n'en pas
mettre.
Si la définition est placée à un endroit où il est possible de définir une fonction
(dans une autre fonction, par exemple), on ne peut pas laisser les parenthèses
(par exemple : XXX s; dans ce cas, il ne faut pas en mettre, car le compilateur
ne fait pas la différence entre cette définition et une définition de fonction).
Le destructeur est la méthode dont le nom est formé du signe ~ suivi du nom
de la classe.
Il n'a pas de paramètre.
Il ne peut donc y avoir qu'un seul destructeur.
Il est automatiquement exécuté à la libération de l'objet, c'est-à-dire lorsque
le programme passe l'accolade droite du bloc qui le contient dans le cas des
membres ou que la fonction delete est appelée dans le cas des objets
pointés.
Si un destructeur est défini dans l'objet, on peut le détruire au moyen de
l'appel à sa fonction :
s.~XXX() ;
9. Placement du code des méthodes (inline)
Il y a deux façons de placer le code des méthodes, des constructeurs et des destructeurs définis dans les classes: "inline" ou "offline" :
class XXX
{
method ( ... ) { code }
public :
XXX(int i) { code }
~XXX() { code }
}
Quand le code est placé comme ci-dessus, entre accolades à la suite de la définition,
alors le code est copié à l'endroit de chaque appel.
Chaque instruction d'appel d'une de ces méthodes est donc remplacée par le code-même.
Ceci évite de placer des instructions de saut pour appeler la fonction et accélère
l'exécution du code.
Cette disposition est donc intéressante si le code est court ou si les appels
sont en petit nombre, ce qui arrive relativement fréquemment, ainsi que le montre
l'expérience.
10. Code commun réentrant (offline)
class XXX
{
method (int i);
public :
XXX(int i) ;
~XXX();
}
XXX::method(int i) { code }
int XXX::XXX(int i) { code }
XXX::~XXX() { code }
Les lignes déclaratives ne contiennent pas de code, seulement des paramètres
entre parenthèses.
On peut mettre le nom des paramètres et leur type ou seulement leur type.
Les dernières lignes définissent le code des méthodes à l'extérieur
de la classe.
Pour définir le code d'une méthode, on répète le nom de la classe, suivi de
deux fois : , du nom de la méthode, des paramètres avec leurs types, en accord
avec ce qui a été déclaré dans la classe et finalement du code entre accolades.
Pour le constructeur, on place naturellement deux fois le même nom.
Dans le cas "offline", le compilateur place le code à l'endroit de la
définition et code des appels sous forme de sauts à ce code réentrant.
Cette possibilité économise la place, puisque le code n'est déposé qu'une fois
en mémoire.
Elle est utilisée pour les codes d'une certaine taille.
11. Contrôle de l'accès aux membres d'une classe de base
class MaClasse
{
<Declarations des membres>
private :
<Déclarations des membres>
protected :
<Déclarations des membres>
public :
<Déclarations des membres>
};
Tous les membres que nous avons définis jusqu'à maintenant étaient placés après
le mot-clé public , ce qui permettait de les accéder depuis l'extérieur.
En fait, il existe deux autres mots-clés: private et protected.
Les membres qui sont déclarés après le mot-clé private ou qui sont placés
avant tout mot-clé ne sont pas connus de l'extérieur.
Ces fonctions et ces membres sont destinés à construire les objets sans que
leur structure soit visible de l'extérieur.
Ainsi, il est possible, dans de nouvelles versions des objets, de modifier ces
fonctions en étant sûr que cela n'a aucune conséquence pour les programmes qui
existent déjà et qui se sont fiés aux déclarations rendues publiques, pour autant
bien entendu que le fonctionnement global n'ait pas changé.
Cette possibilité est fondamentale pour avoir une réutilisabilité maximale des
logiciels développés.
Le mot-clé protected représente un niveau de contrôle intermédiaire
entre public et privé.
Tous les membres déclarés protected ne peuvent être accédés ni par les
objets extérieurs ni par les objets qui ont emboîté la classe qui les contient.
Par contre ils sont atteignables par toutes les classes qui héritent ces membres
en héritant la classe qui les contient.
Cette dernière remarque aura un sens après avoir vu l'héritage.
Lorsque l'on crée une classe, il est souvent nécessaire d'avoir des informations
sur les autres objets précédemment créés à partir de cette classe: nombre d'objets
créés, liste des objets, etc.
Pour gérer cela il est possible de définir des variables qui apparaissent une
seule fois pour tous les objets d'une même classe.
Ces variables sont donc liées à la classe, pas aux objets.
Elles correspondent à des variables globales mais leur nom n'est connu qu'en
relation avec la classe.
class X
{
static int Numero;
public :
static int NombreCourant;
}
int X::Numero = 0;
int X::NombreCourant = 0;
Les variables static définies dans la classe sont instanciées une seule
fois, avant toute instanciation d'objet.
Elles sont connues des fonctions définies dans la classe.
Le compilateur exige que les membres static soient initialisés et cela à l'extérieur
de la classe, d'ou les deux dernières lignes dela définition précédente...
On montre ci-dessous comment accéder aux membres publiques :
m = X::NombreCourant ;
m = ObjetDeClasseX.NombreCourant ;
m = PointeurSurX->NombreCourant ;
type def struct A { <Membres> } B ;
est équivalent à :
class A { public : <Membres> } B ;
En C++ on peut utiliser des structures définies par le langage C.
De plus on peut définir une structure sans mettre typedef devant et l'utiliser
pour définir un champ sans mettre struct devant la définition, comme
c'était nécessaire dans les anciennes versions de C.
En fait, une structure correspond exactement à une classe dont tous les membres
seraient des membres de données, placés après le mot-clé public.
Les union sont également définis en C++.
Dans certains cas on désire utiliser un identificateur de son propre objet.
Cet identificateur est donné par le symbole this.
Des exemples d'utilisation sont donnés ci-dessous.
class A
{
public :
int par ;
A (int par) { this->par = par }/* REPERE 1 */
void f() { delete this ;}/* REPERE 2 */
};
void main()
{
A *aa = new A(0);
aa->f();
}
A la ligne /* REPERE 1 */, on veut mémoriser un paramètre transmis par le constructeur.
Plutôt que d'inventer un nouveau symbole, on fait la différence entre le paramètre
apporté par la procédure et le membre local en ajoutant this devant le
membre.
A la ligne /* REPERE 2 */, on efface son propre environnement au moyen de delete
this.
La situation montrée ici n'est pas très pertinente, mais on verra que cela permet
de terminer des objets actifs lorsqu'ils ont fini leur travail.
class A
{
public :
enum colors { red , yellow = 100 , green };
typedef int 11 ;
struct B { int m ; char c ; };
}
Ainsi qu'on l'a vu, on accède aux champs et aux méthodes d'un objet par le
biais de sa référence.
Par contre pour atteindre les types définis dans une classe, on utilise le nom
de la classe.
La classe A ci-dessus définit un certain nombre de symboles publiques correspondant
à des types.
Pour accéder à ces symboles depuis l'extérieur, on ajoute le nom de la classe suivi de deux fois deux points, comme cela est fait ci-dessous :
void main()
{
A::tt x ;
A::colors c = A::red ;
A::B VariableDeStructureB ;
}