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 * +, -, *, / * <, >, ==, != * <<, >> * ... |