Chapitre 2
CLASSES ET OBJETS
 
 
Précédent Suivant
 

Classes et objets
Différents types de variables
* Personne p; ==> objet
* Allocation sur la pile
* Visibilité et durée de vie limitée au bloc d'instructions
* Destruction à la fin du bloc
* Personne & p; ==> référence à un objet
* Aucune allocation (la variable fait référence à un objet existant)
* Visibilité limitée au bloc d'instructions
* Aucune destruction à la fin du bloc ==> la référence disparaît
* Personne * p; ==> pointeur sur un objet
* Allocation sur le tas (opérateur "new")
ou référence à un objet existant (récupération adresse)
* Visibilité limitée au bloc d'instructions
* Aucune destruction à la fin du bloc ==> le pointeur disparaît
Variables objets
* Déclaration d'une variable ==> allocation + construction d'un objet
* Personne p; ==> appel au constructeur par défaut s'il existe
* Personne p("Nawouak","Bruno"); ==> appel à un constructeur spécifique
* En C++, un constructeur par défaut est fourni pour chaque classe
* Tant qu'aucun constructeur n'a été défini dans la classe
* Destruction implicite à la fin du bloc d'instructions
* Appel automatique au destructeur
* Destructeur = méthode portant le nom de la classe précédé d'un "~"
* Exemple: ~Personne(void) { cout << "Destruction personne"; }
* Possibilité de créer un objet à la volée
* Objet temporaire (détruit à la fin de l'instruction)
* Exemple: inscrire(Personne("Nawouak","Bruno"));
Pointeurs
* Possibilité d'allouer sur le tas ==> pointeur
* Personne * p = new Personne("Nawouak","Bruno");
* Pointeur ~ entier en C++
* Pointeur "vide" ~ pointeur = 0
* p = 0;
* La destruction n'est pas automatique
* Doit être gérée par le développeur
* delete p;
* Création d'un tableau d'objets
* Personne * tab = new Personne[10];
* Appel au constructeur par défaut pour chaque élément
* Syntaxe pour la destruction: delete [] tab
Références (1/2)
* Possibilité de faire référence à un objet existant
* Utile pour le passage d'argument ou le retour de fonction
* Evite une recopie (dans le cas d'objets volumineux)
* Permet de modifier l'objet passé en paramètre (ou en retour)
* Passage d'argument primitif
* Argument qui n'a pas à être modifié ==> passage par copie
* f(int x)
* int g(void)
* Passage par référence constante possible: f(const int & x)
* Argument qui peut être modifié ==> passage par référence
* f(int & x)
* int & g(void)
Références (2/2)
* Passage d'argument objet
* Argument qui n'a pas à être modifié
==> passage par référence constante (évite la copie inutile)
* f(const Personne & p)
* const Personne & g(void)
* Argument qui peut être modifié ==> passage par référence
* f(Personne & p)
* Personne & g(void)
* Attention au retour de fonction
* Impossible de retourner une référence sur une variable locale
à la fonction
* Car celle-ci disparaît à la sortie de la fonction
Définition d'une classe (1/2)
* class nom_classe { ... };
* Une classe contient des membres
* Attributs
* Méthodes
* Classes / types internes
* Elle peut être décomposée en zones de visibilité différente
* public: accessible par tous
* protected: accessible par les classes filles
* private: accessible par la classe seulement
Définition d'une classe (2/2)
* Exemple
class Personne {
private:
string nom;
string prenom;
public:
const string & getNom(void) const { return nom; }
const string & getPrenom(void) const { return prenom; }
Personne(const string & n,const string & p) {
nom = n;
prenom = p;
}
...
};
Méthodes constantes (1/2)
* On distingue deux types de méthodes d'instance
* Celles qui ont vocation à modifier l'état de l'objet
* Celles dont on garantit qu'elles ne modifient pas l'état de l'objet
==> on parle de méthodes "constantes"
* Pour déclarer une méthode constante
* Mot-clé "const" à la fin de la déclaration
* const string & getNom(void) const
* Intérêt d'une méthode constante
* Garantit qu'elle ne modifie pas l'objet
* Peut être appelée sur un objet constant
Méthodes constantes (2/2)
* L'aspect constant fait partie intégrante de la signature de la méthode
* Exemple
class Personne {
...
const string & getNom(void) const { return nom; }
string & getNom(void) { return nom; }
...
};
...
const Personne p1;
Personne p2;
cout << p1.getNom(); ==> appel version constante
cout << p2.getNom(); ==> appel version constante
p1.getNom() = "Machin"; ==> erreur: tentative appel version non constante
p2.getNom() = "Machin"; ==> appel version non constante
Membres de classe (1/3)
* Déclaration d'un membre de classe ==> mot-clé "static"
* Exemple
class A {
...
static int x;
static void m(void);
...
};
* Membre accessible directement à partir de la classe
* int i = A::x;
* A::m();
* Attention à l'initialisation des attributs de classe
* Doit être faite à l'extérieur de la classe (détails plus tard)
* Constante: mots-clé "static" + "const"
* static const int MAX = 100;
Membres de classe (2/3)
* Exemple: compter les instances créées (1/2)
class Personne {
private:
static int compteur;
string nom;
string prenom;
public:
static int getCompteur(void) { return compteur; }
Personne(const string & n,const string & p)
{ nom = n; prenom = p; ++compteur; }
...
};
Membres de classe (3/3)
* Exemple: compter les instances créées (2/2)
int Personne::compteur = 0;
...
Personne p1("Nawouak","Bruno");
Personne p2("Dupont","Jean");
cout << "Nombre de personnes: "
<< Personne::getCompteur() << endl;
Structure générale d'un projet C++ (1/2)
* Un projet C++ contient deux types de fichiers
* Fichiers d'entête (.h / .hpp)
* Partie publique d'un module
* Aspects déclaratifs / interface
* Fichiers d'implémentation (.cpp)
* Partie privée d'un module
* Aspects d'implémentation
* En C++, compilation séparée
* Chaque fichier ".cpp" = une unité de compilation indépendante
* Fichier d'implémentation (.cpp) ==> code binaire (.o / .obj)
* Assemblage codes binaires ==> exécutable (.exe)
Structure générale d'un projet C++ (2/2)
* Lien entre unités ==> inclusion d'entêtes
* Pour exploiter une API ==> inclusion de l'entête décrivant cette API
* Directive de compilation: #include <personne.hpp>
* Conseils
* Ne jamais inclure de fichier ".cpp"
* Evite une duplication de code binaire
* N'inclure un entête qu'une seule fois dans une unité de compilation
* Evite une répétition des déclarations (interdit en C++)
* Astuce communément employée: le "gardien"
#ifndef NOM_GARDIEN
#define NOM_GARDIEN ==> s'assurer que le nom est unique
// Code entête
#endif
Mémoire dynamique d'un objet
* Exemple
class Personne {
private:
string nom;
string prenom;
unsigned nb_diplome;
string * diplomes; ==> mémoire dynamique
...
};
* Constructeur: allocation dynamique
Personne(...) {
...
diplomes = new string[nb_diplome];
...
}
* Destructeur: penser à libérer la mémoire
~Personne(void) { if (diplomes != 0) delete [] diplomes; }
Constructeur de copie (1/2)
* Personne p2(p1); ==> appel constructeur de copie
* Signature méthode: Personne(const Personne & p);
* Un constructeur de copie est fourni par défaut
* Appelle le constructeur de copie de chaque attribut
* Parfois la version par défaut n'est pas satisfaisante
* C'est le cas d'un objet avec mémoire dynamique
* Attention à la copie
* Réécrire le constructeur de copie et l'opérateur d'affectation
* Penser à la libération de la mémoire
* Lors de la destruction, mais aussi lors de l'affectation
Constructeur de copie (2/2)
* Retour sur l'exemple précédent
* Personne p1("Nawouak","Bruno"); ==> allocation du tableau
* Personne p2(p1); ==> p1 et p2 pointent même zone mémoire
* Destruction p1 ==> libération mémoire p1 ==> p2 pointe zone mémoire libre
* Surcharge du constructeur de copie
Personne(const Personne & p) {
nom = p.nom;
prenom = p.prenom;
nb_diplome = p.nb_diplome;
diplomes = new string[nb_diplome];
for (unsigned i = 0; i < nb_diplome; ++i)
diplomes[i] = p.diplomes[i];
}
Opérateur d'affectation (1/3)
* p2 = p1; ==> appel opérateur d'affectation
* Syntaxe équivalente
* p2.operator=(p1);
* Signature méthode
* Personne & operator=(const Personne & p);
* Mêmes problèmes que le constructeur de copie
* Surcharger en cas de gestion de mémoire dynamique
* Contrainte de chaînage
* p3 = p2 = p1 ? p3.operator=(p2.operator=(p1));
* L'opérateur doit retourner l'objet affecté
Opérateur d'affectation (2/3)
* Eviter la copie de soi-même
* p1 = p1; ==> inutile (voire périlleux) de faire l'opération de copie
* Test à l'entrée de la fonction sur l'adresse du paramètre
* Structure classique de l'opérateur d'affectation
Personne & operator=(const Personne & p) {
if (this != &p) {
// Code de copie
}
return *this;
}
Opérateur d'affectation (3/3)
* Exemple de surcharge (penser à libérer la mémoire)
Personne & operator=(const Personne & p) {
if (this != &p) {
nom = p.nom;
prenom = p.prenom;
nb_diplome = p.nb_diplome;
if (diplomes != 0) delete [] diplomes;
diplomes = new string[nb_diplome];
for (unsigned i = 0; i < nb_diplome; ++i)
diplomes[i] = p.diplomes[i];
}
return *this;
}
Surcharge d'opérateur
* En C++, possibilité de surcharger un opérateur
* Définir un nouveau comportement pour un opérateur
* En fonction des types de ses arguments
* Exemple: classe "Carte" qui représente une carte à jouer
* Définir l'opérateur "<" pour comparer deux cartes
bool operator<(const & Carte a,const & Carte b) {
return (a.getValeur() < b.getValeur());
}
* Possibilité de redéfinir de nombreux opérateurs
* +, -, *, /
* <, >, ==, !=
* <<, >>
* ...