La création de classes et d'objets


Sommaire du chapitre

Introduction

1. Les classes

2. Les objets

3. Les objets repérés par un pointeurs

4. Les classes emboitées

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)

11. Contrôle de l'accès aux membres d'une classe de base

12. Membres statiques

13. Structures de C

14. Pointeur this

15. Exportation de types


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).

retour au sommaire


1. Les classes

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.

retour au sommaire


2. Les objets

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>;

retour au sommaire


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.

retour au sommaire


4. Les classes emboitées

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.

retour au sommaire


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 .

retour au sommaire


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.

retour au sommaire


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);

retour au sommaire


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() ;

retour au sommaire


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.

retour au sommaire


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.

retour au sommaire


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.

retour au sommaire


12. Membres statiques

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 ;

retour au sommaire


13. Structures de C

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++.

retour au sommaire


14. Pointeur this

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.

retour au sommaire


15. Exportation de types

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 ;
}

retour au sommaire
chapitre suivant