| auteurs : LFE, Aurélien Regat-Barrel |
D'une façon très simple et en faisant le parallèle avec le C, on peut dire qu'une classe est une structure à laquelle on ajoute des
fonctions permettant de gérer cette structure.
Mais outre l'ajout de ces fonctions, il existe deux grandes nouveautés par rapport aux structures du C:
-
d'une part, les membres de classe, qu'ils soient des variables ou des fonctions, peuvent être privés c'est-à-dire inaccessibles en dehors de la classe (par opposition aux membres publics d'une structure C où tous ses membres sont accessibles).
-
d'autre part, une classe peut être dérivée. La classe dérivée hérite alors de toutes les propriétés et fonctions de la classe mère.
Une classe peut d'ailleurs hériter de plusieurs classes simultanément.
Une classe se déclare via le mot-clé class suivi du nom de la classe, d'un bloc (accolades ouvrante et fermante) et d'un point virgule (ne pas l'oublier !).
Les membres de la classe (variables ou fonctions) doivent être déclarés à l'intérieur de ce bloc, à la manière des structures en C.
class MaClasse
{
int a;
void b ();
} ;
|
|
| auteurs : Medinoc, 3DArchi |
On dit d'une classe qu'elle a une sémantique de valeur si deux objets
situés à des adresses différentes, mais au contenu identique, sont
considérés égaux.
Par exemple, une classe modélisant une somme d'argent a une sémantique
de valeur. Une somme de 100 € est toujours une somme de 100€. On peut
ajouter, soustraire, multiplier différentes sommes d'argent. Deux
sommes de 100 € sont identiques même si l'une est sous forme d'espèces
et l'autre sous forme de chèque.
On voit qu'il peut être pertinent :
- de redéfinir des opérateurs arithmétiques (+, -, *, /) ;
- d'avoir un opérateur d'affectation (=, constructeur par copie ?) ;
- de redéfinir des opérateurs de comparaison (==, <, etc.).
Inversement, une classe à sémantique de valeur n'a pas beaucoup de
sens pour servir de classe de base à un héritage. On ne trouvera donc
en général pas de fonction virtuelle dans une classe à sémantique de valeur.
|
| auteurs : Davidbrcz, 3DArchi |
A l'inverse des classes à sémantique de valeur, une classe a une
sémantique d'entité si toutes les instances de cette classe sont
nécessairement deux à deux distinctes, même si tous les champs de ces
instances sont égaux. Elle modélise un concept d'identité : chaque
objet représente un individu unique.
Une classe modélisant un compte a une sémantique d'entité. Deux comptes
sont distincts même s'ils ont la même somme d'argent. Cela n'a pas de
sens d'ajouter des comptes (on peut vider un compte pour le verser
dans un autre, mais ce n'est pas un ajout). En revanche, on peut avoir
des comptes courants, des comptes d'épargnes, des comptes titres, etc.
On voit qu'une classe à sémantique d'entité peut servir de base à
un héritage. Mais, une classe à sémantique d'entité :
- ne redéfinit pas les opérateurs arithmétiques (+,-,/*) ;
- n'a pas d'opérateur d'affectation (=, constructeur par copie) ;
- ne redéfinit pas les opérateurs de comparaison (==, <
etc.).
Si on veut créer une copie d'un objet à sémantique d'entité, on s'appuie
sur une méthode Clone spécifique retournant un nouvel objet dans un état
semblable.
|
| auteur : LFE |
this est un pointeur créé par défaut et qui désigne l'objet lui-même. A noter que this est un pointeur non modifiable, l'adresse pointée ne peut être changée (ce qui est d'ailleurs parfaitement logique).
|
| auteur : Marshall Cline |
Le jeu n'en vaut pas la chandelle, l'encapsulation est faite pour le code, pas pour les gens.
Ce n'est pas violer l'encapsulation pour un programmeur que de voir les parties privées et/ou protégées de vos classes, tant qu'il
n'écrit pas de code qui dépende d'une façon ou d'une autre de ce qu'il voit. En d'autres termes, l'encapsulation n'empêche pas les gens
de découvrir comment est constituée une classe; cela empêche que le code que l'on écrit ne soit dépendant de l'intérieur de la classe.
La société qui vous emploie ne doit pas payer un "contrat de maintenance" pour entretenir la matière grise qui se trouve entre vos 2
oreilles, mais elle doit payer pour entretenir le code qui sort de vos doigts. Ce que vous savez en tant que personne n'augmente pas le
coût de maintenance, partant du principe que le code que vous écrivez dépend de l'interface plus que de l'implémentation.
D'un autre coté, ce n'est rarement, voire jamais un problème. Je ne connais aucun programmeur qui ait intentionnellement essayé d'accéder
aux parties privées d'une classe. "Mon avis dans un tel cas de figure serait de changer le programmeur et non le code" (James Kanze, avec son
autorisation)
|
| auteur : LFE |
Pour dériver une classe à partir d'une autre, il suffit de faire suivre la déclaration de la classe dérivée de : suivi d'un
modificateur d'accès et du nom de la classe mère.
class mere
{
}
class fille : public mere
{
}
|
L'héritage peut être public (public), privé (private) ou protégé (protected).
Si l'héritage est public, les membres de la classe mère garderont leur accès, à savoir les membres publics restent publics et les
protégés restent protégés.
Si l'héritage est protected, les membres publics de la classe mère deviendront protégés dans la classe fille, et les protégés resteront tels quels.
Si l'héritage est private, tous les membres de la classe mère, qu'ils aient été publics ou protégés deviennent privés.
Les membres privés de la classe mère sont inaccessibles depuis la classe fille, quelque soit le type d'héritage.
|
| auteurs : Laurent Gomila, Aurélien Regat-Barrel |
Pour limiter le nombre d'instances d'une classe, on utilise le design pattern du singleton. Il permet de contrôler le nombre d'instances d'une classe (en général une seule). Voici un exemple typique d'implémentation en C++ :
# ifndef SINGLETON_H
# define SINGLETON_H
class Singleton
{
public :
static Singleton & GetInstance ();
private :
Singleton () { }
Singleton ( const Singleton & );
Singleton & operator = ( const Singleton & );
} ;
# endif
|
# include "Singleton.h"
Singleton & Singleton:: GetInstance ()
{
static Singleton instance;
return instance;
}
|
# include "Singleton.h"
int main ()
{
Singleton erreur;
Singleton & s = Singleton:: GetInstance ();
}
|
Bien sûr, ce n'est qu'un exemple d'implémentation. Selon les besoins on pourra coder notre singleton différemment. Par exemple, GetInstance() peut renvoyer un pointeur fraîchement alloué, ce qui peut être justifié si l'on veut disposer de plus d'une instance, ou si l'on veut contrôler le moment de sa destruction (dans le cas de dépendances entre plusieurs singletons par exemple). A chaque appel un compteur interne est incrémenté et au delà d'un certain nombre d'instances la fonction refuse d'en allouer de nouvelles. Il ne faut en revanche pas oublier de libérer les pointeurs ainsi obtenus.
On peut également modifier le code pour le rendre thread-safe en ajoutant une protection de type verrou dans GetInstance() si l'on fait du multithreading.
|
| auteurs : LFE, Laurent Gomila |
Un membre déclaré public dans une classe peut être accédé par toutes les autres classes et fonctions.
Un membre déclaré protected dans une classe ne peut être accédé que par les autres membres de cette même classe ainsi que par les
membres des classes dérivées.
Un membre déclaré private dans une classe ne peut être accédé que par les autres membres de cette même classe.
Ces mots-clés permettent également de modifier la visibilité des membres dans la classe dérivée lors d'un héritage :
|
Héritage |
Accès aux données |
public |
protected |
private |
public |
public |
protected |
private |
protected |
protected |
protected |
private |
private |
interdit |
interdit |
interdit |
Exemple :
class A
{
public :
int x;
protected :
int y;
private :
int z;
} ;
class B : private A
{
}
|
|
| auteurs : Laurent Gomila, Aurélien Regat-Barrel |
Contrairement à ce que l'on pourrait penser, les classes (class) et les structures (struct) sont équivalentes en C++.
Seules trois différences mineures existent :
1. La visibilité par défaut est public pour les structures, private pour les classes.
struct A
{
int a;
private :
int b;
} ;
class B
{
int b;
public :
int a;
} ;
|
2. Le mode d'héritage par défaut est public pour les structures, private pour les classes.
class Base { } ;
class A1 : public Base
{
} ;
struct A2 : Base
{
} ;
class B1 : Base
{
} ;
struct B2 : private Base
{
} ;
|
3. L'utilisation en tant que type template est interdite pour struct.
template < class T>
struct A
{
} ;
template < struct T>
class B
{
} ;
|
A noter que la norme permet même d'effectuer une déclaration anticipée de classe via le mot-clé struct, et inversement. Cependant,
certains compilateurs ne l'acceptent pas.
Fichier.h Fichier.cpp
class A
{
} ;
A * Exemple ()
{
return new A;
}
|
Classes et structures sont donc presque équivalentes, cependant on adopte souvent cette convention : on utilise struct pour les
structures type C (pas de fonction membre, d'héritage, de constructeurs, ...) et class pour tout le reste.
|
| auteurs : pipin, Laurent Gomila |
Il arrive souvent qu'une classe A contienne un attribut (ou un argument de fonction) de type classe B et que la classe B contienne elle aussi un attribut (ou un argument de fonction) de type classe A. Mais si l'on inclue A.h dans B.h et vice-versa, on se retrouve avec un problème d'inclusions cycliques.
La solution à ce problème est d'utiliser des déclarations anticipées (forward declaration). Au lieu d'inclure l'en-tête définissant une classe, on va simplement déclarer celle-ci pour indiquer au compilateur qu'elle existe. Cela marche car tant qu'on n'utilise qu'un pointeur ou une référence, le compilateur n'a pas besoin de connaître en détail le contenu de la classe. Il a juste besoin de savoir qu'elle existe. Par contre au moment d'utiliser celle-ci (appel d'une fonction membre par exemple) il faudra bien avoir inclus son en-tête, mais ce sera fait dans le .cpp et non plus dans le .h, ce qui élimine le problème d'inclusion cyclique.
A.h | class B;
class A
{
B* PtrB;
} ;
|
A.cpp | # include "A.h"
# include "B.h"
|
B.h | # include "A.h"
class B
{
A a;
} ;
|
De manière générale les déclarations anticipées sont à utiliser autant que possible, à savoir dès qu'on déclare dans une classe un pointeur ou une référence sur une autre classe. En effet, cela permet aussi de limiter les dépendances entre les fichiers et de réduire considérablement le temps de compilation.
|
| auteur : Marshall Cline |
C'est l'enchaînement des appels de ces fonctions membres, c'est pourquoi cela s'appelle le chaînage des fonctions.
La première chose qui est exécutée est
objet.methode1(). Cela renvoie un objet ou une référence sur un objet
(par ex. methode1() peut se
terminer en renvoyant *this, ou n'importe quel autre objet). Appelons cet objet retourné "ObjetB". Ensuite, ObjetB devient l'objet
auquel est appliqué methode2().
L'utilisation la plus courante du chaînage de fonctions est l'injection / extraction vers les flux standards.
fonctionne parce que
est une fonction qui retourne cout.
|
| auteurs : Laurent Gomila, JolyLoic |
Parfois, lorsqu'on manipule des objets polymorphes (ie. des classes dérivées via un pointeur sur leur classe de base), on voudrait connaître leur type réel, par exemple pour les copier. Malheureusement, conceptuellement ce n'est souvent pas la meilleure solution, et c'est parfois lourd à gérer. Le moyen le plus efficace de procéder à une copie d'objets polymorphes est sans doute d'utiliser le design pattern Clone :
struct Base
{
virtual ~ Base () { }
virtual Base* Clone () const = 0 ;
} ;
struct Derivee1 : public Base
{
virtual Base* Clone () const
{
return new Derivee1 (* this );
}
} ;
struct Derivee2 : public Base
{
virtual Base* Clone () const
{
return new Derivee2 (* this );
}
} ;
Base* Obj1 = new Derivee1;
Base* Obj2 = new Derivee2;
Base* Copy1 = Obj1- > Clone ();
Base* Copy2 = Obj2- > Clone ();
delete Obj1;
delete Obj2;
delete Copy1;
delete Copy2;
|
Le code précédent est simple et fonctionne parfaitement. Une variante plus subtile existe : elle consiste à utiliser comme type de retour
de Clone() la classe dans laquelle la fonction membre est définie au lieu d'utiliser la classe de base :
struct Derivee1 : public Base
{
virtual Derivee1* Clone () const
{
return new Derivee1 (* this );
}
} ;
|
|
| auteurs : LFE, Aurélien Regat-Barrel |
Une classe abstraite est une classe qui possède au moins une fonction membre virtuelle pure (lire Qu'est-ce qu'une fonction virtuelle pure ?).
Cette fonction devant être supplantée, ce type de classe ne peut pas être instancié, et est donc destiné à être dérivé pour être spécialisé.
La ou les classes filles doivent supplanter l'ensemble des fonctions virtuelles pures de leurs parents. On dit alors que les classes filles concrétisent la classe abstraite.
class Bienvenue
{
public :
virtual void Message () = 0 ;
} ;
class BienvenueEnFrancais : public Bienvenue
{
public :
void Message ()
{
std:: cout < < " Bienvenue !\n " ;
}
} ;
class BienvenueEnAnglais : public Bienvenue
{
public :
void Message ()
{
std:: cout < < " Welcome !\n " ;
}
} ;
|
|
lien : Qu'est-ce qu'une fonction virtuelle pure ?
|
| auteur : 3DArchi |
Le constructeur par défaut est le constructeur qui ne prend aucun
argument ou dont les arguments ont une valeur par défaut :
class CMyClass
{
public :
CMyClass ();
} ;
|
ou
class CMyClass
{
public :
CMyClass (T1 param1= def_value, T2 param2= T2 ());
} ;
|
Si la classe n'en définit pas, alors le compilateur en propose un
implicitement. Celui-ci appelle le constructeur par défaut des classes
de base et le constructeur par défaut des membres.
Pour les types POD, les valeurs sont laissées
non initialisées.
Une exception : si la classe définit un autre constructeur
(constructeur avec paramètres sans valeur par défaut ou
constructeur par copie), alors le compilateur ne génère pas de
constructeur par défaut. Dans ce cas, si cela a un sens pour la
classe, un constructeur par défaut doit alors être explicitement défini.
|
| auteur : 3DArchi |
Le constructeur par copie se base sur un autre objet du même type
pour construire l'objet en cours :
class CMyClass
{
public :
CMyClass (CMyClass const & );
} ;
|
Si la classe ne définit pas de constructeur par copie, le compilateur
génère un constructeur de copie implicitement. Celui-ci appelle le
constructeur par copie des classes parents et le constructeur par
copie des membres. Ceci peut être désactivé
en rendant la classe non copiable
.
Si on souhaite donner une sémantique de copie et que le constructeur
par copie ne convient pas (par exemple, parce qu'une ressource
est gérée), alors il faut définir un constructeur par copie.
Dans le cadre d'une utilisation polymorphe, on peut vouloir définir
un constructeur par copie pour permettre le clonage. Celui-ci est
alors protégé car la copie n'a de sens que dans ce cadre.
La fonction Clone est, elle, publique.
|
| auteur : 3DArchi |
Le destructeur est appelé pour libérer les ressources acquises par un
objet lorsque l'espace occupé par celui-ci doit être libéré :
class CMyClass
{
public :
~ CMyClass ();
} ;
|
Si la classe ne contient pas de destructeur par défaut, alors le
compilateur en génère un implicitement. Celui-ci va appeler les
destructeurs des différents membres puis celui des classes parents.
Si on doit utiliser la classe comme base dans un héritage pour une
utilisation polymorphe (utilisation via une référence ou un
pointeur sur la base), alors la classe de base doit définir
un destructeur virtuel.
|
lien : Pourquoi le destructeur d'une classe de base doit être public et virtuel ou protégé et non virtuel ?
|
| auteur : 3DArchi |
- Si une des quatres méthodes ci-dessus a été définie
de façon non triviale, alors il est fortement probable que les
trois autres doivent être définies.
- Si une classe doit servir de base pour une utilisation
polymorphe, alors le destructeur doit être déclaré comme
virtuel. A noter qu'il est fort probable que
la classe soit alors non copiable
.
- La sémantique d'une classe va imposer la politique
de définition des méthodes (
Quelle forme canonique adopter en fonction de la sémantique de la classe ?
).
- Si la définition implicite convient, compte tenu des
éléments ci-dessus, alors il n'est pas nécessaire de définir
explicitement ces méthodes. Et les définir peut avoir des
impacts négatifs : il existe des outils d'analyse de code
(et peut-être des futures évolutions du langage) qui vérifient
justement que l'on a respecté ces règles. Définir une fonction
ne faisant rien peut alors mettre en échec ces outils.
|
Consultez les autres F.A.Q.
|
|