POO-C++Eric LecolinetTélécom ParisTech
Programmation orientée objet et autres concepts en C++ Eric Lecolinet Télécom ParisTech / Dpt. Infres Octobre 2012 License: Début du cours (utilisez les flèches en haut à gauche pour naviguer) Index Tout dans un seul fichier: HTML / PDF |
Brève historique Langage C C++ Bjarne Stroustrup, AT&T Bell Labs initialement une extension objet du C (pré-compilateur) plusieurs versions: de 1985 à normalisation ANSI / ISO (1998, 2003) C++11 : nouvelle version (août 2011) Java inspiré de la partie objet de C++ (et d'ADA, Smalltalk ...) C# inspiré de Java, de C++, etc. |
Brève historique (2) Objective C une autre extension objet du C promue par NeXt puis Apple (MacOSX, iOS) simple mais puissant et compatible avec C et C++ syntaxe inhabituelle inspirée de Smalltalk Python, Ruby visent la simplicité d'écriture et la flexibilité interprétés et basés sur le typage dynamique (comme Objective C) Evolutions récentes progression de Objective C, C# et Python aux dépends de Java C/C++ reste stable et (largement) majoritaire |
C++ versus C Avantage : compatibilité C/C++ même syntaxe de base code C facilement compilable en C++ (faibles différences syntaxiques) un programme peut combiner des fichiers C et C++ => idéal pour rendre un programme C orienté objet Inconvénient : compatibilité C/C++ C++ hérite de certains choix malencontreux du langage C ! |
C++ versus Java Ressemblances syntaxe en partie similaire fonctionnalités objet similaires (à part l'héritage multiple) Différences gestion mémoire (pas de garbage collecting, etc.) héritage multiple redéfinition des opérateurs templates et STL pas de threads dans le langage (mais bibliothèques ad hoc) langage compilé (et ... plus rapide !) |
En résumé C++ = langage objet ET procédural contrairement à Java, purement OO vision anachronique: C++ = "sorte de mélange de C et de Java" Bon côtés orienté objet avec l'efficacité du C (et compatible avec C) richesse du langage... Moins bon côtés difficultés héritées du C plus quelques ambiguités richesse du langage: programmes parfois inutilement complexes Things should be made as simple as possible but not any simpler (A. Einstein) |
Références et liens Références Le langage C++, Bjarne Stroustrup (Pearson) www.cplusplus.com/reference www.cppreference.com Liens utiles Travaux Pratiques: www.enst.fr/~elc/cpp/TP.html Introduction au toolkit graphique Qt: www.enst.fr/~elc/qt Boost C++ Libraries: www.boost.org voir aussi liens en 1ere page... |
Compilateurs Attention aux incompabilités entre les versions de C++ syntaxiques binaires le programme et les librairies doivent être compilés avec des versions compatibles de C++ Salles Unix à Télécom g++ version 4.*.* |
Premier chapitre Des objets et des classes ... |
Programme C++ Un programme C++ est constituté : de classes réparties dans plusieurs fichiers (comme en Java) (éventuellement) de fonctions et variables globales (comme en C) Chaque fichier peut comprendre : un nombre arbitraire de classes (si ça a un sens ...) |
Déclarations et définitions Comme en langage C : déclarations dans fichiers headers : xxx.h (ou .hpp ou .hh) définitions dans fichiers d'implémentation : xxx.cpp (ou .cc ou .C) Règles de base : à chaque .cpp (définitions) correspond un .h (déclarations) le .h déclare l'API publique, c'est une interface avec le monde extérieur |
Déclarations et définitions Comme en langage C : déclarations dans fichiers headers : xxx.h (ou .hpp ou .hh) définitions dans fichiers d'implémentation : xxx.cpp (ou .cc ou .C) Règles de base : à chaque .cpp (définitions) correspond un .h (déclarations) le .h déclare l'API publique, c'est une interface avec le monde extérieur Remarque on peut aussi "cacher" des variables ou des fonctions en les déclarant : directement dans le .cpp ![]() dans un header privé (exemple: xxx_impl.h) => surtout pour les librairies |
Déclaration de classe // fichierCircle.h: header contenant les déclarations class Circle { public: int x, y; // variables d'instance unsigned int radius; virtual void setRadius(unsigned int); // méthodes d'instance virtual unsigned int getRadius() const; virtual unsigned int getArea() const; .... }; // !NE PAS OUBLIER LE ; Remarques le ; final est obligatoire après la } même sémantique que Java, syntaxe similaire mais ... l'implémentation est (de préférence) séparée des déclarations |
Implémentation de classe // Rappel des déclarations class Circle { public: int x, y; // variables d'instance unsigned int radius; virtual void setRadius(unsigned int); // méthodes d'instance virtual unsigned int getRadius() const; virtual unsigned int getArea() const; }; Implémentation // fichierCircle.cpp: contient l'implementation #include "Circle.h" // ne pas oublier d'inclure le header ! void Circle::setRadius(unsigned int r) { // noter le :: radius = r; } unsigned int Circle::getRadius() const { return radius; } unsigned int Circle::getArea() const { return 3.1416 * radius * radius; } |
Instanciation // fichiermain.cpp: main() est le point d'entrée du programme #include "Circle.h" // ne pas oublier d'inclure le header int main() { Circle* c = new Circle(); ..... } Ne pas mettre main() dans Circle.cpp sinon la classe Circle ne sera pas réutilisable dans un autre programme ! |
Instanciation // fichiermain.cpp#include"Circle.h" int main() { Circle*c =newCircle(); ..... } new cree un objet (= nouvelle instance de la classe) allocation mémoire puis appel du constructeur (à suivre) c = variable locale qui pointe sur le nouvel objet c est un pointeur |
Comparaison avec Java Pointeur C++ vs. référence Java C++: Circle*c = new Circle(); //pointeurC++ Java: Circle c = new Circle(); //référenceJava dans les 2 cas: une variable qui pointe sur un objet attention: "référence" a un autre sens en C++ (à suivre...) Gestion mémoire Java detruit les objets qui n'ont plus de référent (garbage collector) C++ nécessite une destruction explicite par l'opérateur delete |
Accès aux variables d'instance #include "Circle.h" int main() { Circle* c1 = new Circle(); c1->x = 100; // noter la -> c1->y = 200; c1->radius = 35; Circle *c2 = new Circle(); c2->x = c1->x; } Chaque objet possède sa propre copie des variables d'instance noter l'utilisation de la -> (comme en C, mais . en Java) c->x équivaut à: (*c).x encapsulation => restreindre l'accès aux variables d'instance |
Appel des méthodes d'instance int main() { Circle* c1 = new Circle(); Circle* c2 = new Circle(); // attention: c->x, c->y, c->radius pas initialisés ! unsigned int r = c1->getRadius(); unsigned int a = c2->getArea(); } Toujours appliquées à un objet ont accès à toutes les variables de cet objet propriété fondamentale de l'orienté objet ! unsigned int Circle::getRadius() const { // dans Circle.cpp return radius; } |
Constructeurs class Circle { int x, y; unsigned int radius; public: Circle(int x, int y, unsigned int r); // declaration .... }; Circle::Circle(int _x, int _y, unsigned int _r) { // implementation x = _x; y = _y; radius = _r; } Circle* c = newCircle(100, 200, 35); // instanciation Appelés à l'instanciation de l'objet pour initialiser les variables d'instance |
Constructeurs (suite) Deux syntaxes (quasi) équivalentes Circle::Circle(int _x, int _y, unsigned int _r) { // comme en Java x = _x; y = _y; radius = _r; } Circle::Circle(int _x, int _y, unsigned int _r) // propre à C++ :x(_x), y(_y), radius(_r){ } la 2eme forme est préférable (vérifie l'ordre des déclarations) Chaînage des constructeurs appel implicite des constructeurs des super-classes dans l'ordre descendant |
Constructeur par défaut Si aucun constructeur dans la classe on peut cependant écrire : Circle*c =newCircle(); car C++ crée un constructeur par défaut mais qui ne fait rien ! => variables pas initialisées (contrairement à Java) Conseils toujours definir au moins un constructeur et toujours initialiser les variables de plus, c'est une bonne idée de définir un constructeur sans argument : class Circle { int x, y; unsigned int radius; public: Circle(int x, int y, unsigned int r); Circle():x(0), y(0), radius(0){ } .... }; |
Destruction Circle* c = new Circle(100, 200, 35); ...deletec; // destruction de l'objet c= NULL; // c pointe sur aucun object } delete détruit un objet crée par new pas de garbage collector (ramasse miettes) comme en Java ! NB: on verra plus tard comment se passer de delete Attention: delete ne met pas c à NULL ! Remarque NULL est une macro qui vaut 0 (ce n'est pas un mot-clé) |
Destructeur class Circle { public: virtual~Circle(); // destructeur ... }; Circle* c = new Circle(100, 200, 35); ...deletec; // destruction de l'objet c = NULL; Fonction appelée à la destruction de l'objet un seul destructeur par classe (pas d'argument) Chaînage des destructeurs dans l'ordre ascendant (inverse des constructeurs) |
delete & destructeur Attention c'est delete qui detruit l'objet (qui'il y ait ou non un destructeur) le destructeur (s'il existe) est juste une fonction appelée avant la destruction Quand faut-il un destructeur ? si l'objet a des vars d'instance qui pointent vers des objets à detruire si l'objet a ouvert des fichiers, sockets... qu'il faut fermer pour la classe de base d'une hiérarchie de classes Et en Java ...? |
delete & destructeur Et en Java ? delete n'existe pas car GC (garbage collector) la methode finalize() joue le meme role que le destructeur, mais: pas de chainage des "finaliseurs" appel non deterministe par le GC (on ne sait pas quand l'objet est détruit) |
Surcharge (overloading) Plusieurs méthodes ayant le même nom mais des signatures différentes pour une même classe class Circle { Circle(); Circle(int x, int y, unsigned int r); .... }; Remarques la valeur de retour ne suffit pas à distinguer les signatures applicable aux fonctions "classiques" (hors classes) |
Paramètres par défaut class Circle { Circle(int x, int y, unsigned int r= 10); .... }; Circle* c1 = new Circle(100, 200, 35); Circle* c2 = new Circle(100, 200); // radius vaudra 10 Remarques en nombre quelconque mais toujours en dernier erreur de compilation s'il y a des ambiguités : class Circle { Circle(); Circle(int x, int y, unsigned int r = 10); // OK Circle(int x = 0, int y = 0, unsigned int r = 10); // AMBIGU! .... }; |
Variables de classe class Circle { // fichier Circle.h public: staticconst float PI; // variable declasse int x, y; // variables d'instance unsigned int radius; ... }; Représentation unique en mémoire mot-clé static existe toujours (même si la classe n'a pas été instanciée) Remarques const (optionnel) indique que la valeur est constante notion similaire aux variables "statiques" du C (d'où le mot-clé) |
Définition des variables de classe Les variables de classe doivent également être définies dans un (et un seul) .cpp, sans répéter static ce n'est pas nécessaire en Java ou C# // dans Circle.cpp const float Circle::PI = 3.1415926535; // noter le:: Exception les variables de classe const int peuvent être définies dans les headers // dans Circle.h staticconst intTAILLE_MAX = 100; |
Méthodes de classe //déclaration: fichier Circle.h class Circle { public: static const float PI; staticfloat getPI() {return PI;} ... }; //appel: fichier main.cpp float x = Circle::getPI(); Ne s'appliquent pas à un objet mot-clé static similaire à une fonction "classique" du C (mais évite collisions de noms) N'ont accès qu'aux variables de classe ! |
Namespaces namespace = espace de nommage solution ultime aux collisions de noms existent aussi en C#, similaires aux packages de Java namespaceGeom{// dans Circle.h class Circle { ... }; } ---------------------------------------------------------------- namespaceMath{// dans Math.h class Circle { // une autre classe Circle... ... }; } ---------------------------------------------------------------- #include "Circle.h" // dans main.cpp #include "Math.h" int main() { Geom::Circle* c1 = newGeom::Circle(); Math::Circle* c2 = newMath::Circle(); ... } |
Namespaces using namespace modifie les règles de portée les symboles déclarés dans ce namespace deviennent directement accessibles similaire à import en Java namespaceGeom{// dans Circle.h class Circle { ... }; } ---------------------------------------------------------------- #include "Circle.h" // dans main.cppusing namespaceGeom; int main() { Geom::Circle* c1 = newGeom::Circle(); Circle* c2 = new Circle(); // OK grace a using namespace... ... } |
Bibliothèque standard d'E/S #include <iostream> // E/S du C++ #include "Circle.h" using namespacestd; int main() { Circle* c = new Circle(100, 200, 35); cout<< "radius= " << c->getRadius() << "area= " << c->getArea() <<endl; std::cerr<< "c = " << c << std::endl; } Concaténation des arguments via << ou >> std::cout : sortie standard std::cerr : sortie des erreurs std::cin : entrée standard (utiliser >> au lieu de <<) |
Encapsulation / droits d'accès class Circle {private:: int x, y; unsigned int radius;public: static const float PI; Circle(); Circle(int x, int y, unsigned int r); }; Trois niveaux private (le défaut en C++) : accès réservé à cette classe protected : idem + sous-classes public NB: Java a un niveau package (défaut), C++ a également friend |
Encapsulation / droits d'accès (2) class Circle { // private: //privatepar defaut int x, y; unsigned int radius;public: static const float PI; // PI estpubliccar const Circle(); Circle(int x, int y, unsigned int r); }; Règles usuelles d'encapsulation l'API (méthodes pour communiquer avec les autres objets) est public l'implémentation (variables et méthodes internes) est private ou protected |
Encapsulation / droits d'accès (3) class Circle { friendclass Manager; friendbool equals(const Circle*, const Circle*); ... }; friend donne accès à tous les champs de Circle à une autre classe : Manager à une fonction : bool equals(const Circle*, const Circle*) |
struct struct = class + public structTruc { ... }; équivaut à : classTruc {public: ... }; struct est équivalent à class en C++ n'existe pas en Java existe en C# mais ce n'est pas une class existe en C mais c'est juste un agrégat de variables |
Méthodes d'instance: où est la magie ? Toujours appliquées à un objet class Circle { unsigned int radius; int x, y; public: virtualunsigned int getRadius()const;virtualunsigned int getArea()const; }; int main() { Circle* c = new Circle(100, 200, 35); unsigned int r = c->getRadius(); // OK unsigned int a =getArea(); // INCORRECT: POURQUOI? } Et pourtant : unsigned int Circle::getArea() const { return PI *getRadius()*getRadius(); // idem } unsigned int Circle::getRadius() const { returnradius; // comment getRadius() accede a radius ? } |
Le this des méthodes d'instance Paramètre caché this pointe sur l'objet qui appelle la méthode permet d'accéder aux variables d'instance unsigned int Circle::getArea() const { return PI *radius*getRadius(); } Circle* c = ...; unsigned int a =c->getArea(); Transformé par le compilateur en : unsigned int Circle::getArea(Circle* this) const { return Circle::PI *this->radius * getRadius(this); } Circle* c = ...; unsigned int a =Circle::getArea(c); //__ZNK6Circle7getAreaEv(c) avec g++ 4.2 |
Inline Indique que la fonction est implémentée dans le header // dans Circle.h class Circle { public: inlineint getX() const {return x;} int getY() const {return y;} // pareil: inline est implicite .... }; // inline doit être présent si fonction non-membreinlineCircle* createCircle() {return new Circle();} A utiliser avec discernement + : rapidité à l'exécution : peut éviter un appel fonctionnel (code dupliqué) - : augmente taille du binaire généré - : lisibilité - : contraire au principe d'encapsulation |
Point d'entrée du programme int main(int argc, char** argv) même syntaxe qu'en C arc : nombre d'arguments argv : valeur des arguments argv[0] : nom du programme valeur de retour : normalement 0, indique une erreur sinon |
Terminologie Méthode versus fonction méthodes d'instance == fonctions membres méthodes de classe == fonctions statiques fonctions classiques == fonctions globales etc. Termes interchangeables selon auteurs |
Doxygen /** modélise un cercle. * Un cercle n'est pas un carré ni un triangle. */ class Circle { /// retourne la largeur. virtual unsigned int getWidth() const; virtual unsigned int getHeight() const; ///< retourne la hauteur. virtual void setPos(int x, int y); /**< change la position. * voir aussi setX() et setY(). */ ... }; Système de documentation automatique similaire à JavaDoc mais plus général : fonctionne avec de nombreux langages documentation : www.doxygen.org |
Chapitre 2 : Héritage Concept essentiel de l'OO héritage simple (comme Java) héritage multiple (à manier avec précaution : voir plus loin) |
Règles d'héritage Constructeurs jamais hérités Méthodes héritées peuvent être redéfinies (overriding) : la nouvelle méthode remplace celle de la superclasse ! ne pas confondre surcharge et redéfinition ! Variables héritées peuvent être surajoutées (shadowing) : la nouvelle variable cache celle de la superclasse ! à éviter : source de confusions ! |
Exemple (déclarations) // header Rect.h class Rect { int x, y; unsigned int width, height; public:Rect(); Rect(int x, int y, unsigned int width, unsigned int height); virtual voidsetWidth(unsigned int); virtual voidsetHeight(unsigned int); virtual unsigned int getWidth() const {return width;} virtual unsigned int getHeight() const {return height;} /*...etc...*/ }; class Square: publicRect { //dérivationde classe public: Square(); Square(int x, int y, unsigned int width); virtual voidsetWidth(unsigned int); //redéfinitionde méthode virtual voidsetHeight(unsigned int); }; |
Exemple (implémentation) class Rect { // rappel des délarations int x, y; unsigned int width, height; public: Rect(); Rect(int x, int y, unsigned int width, unsigned int height); virtual voidsetWidth(unsigned int); virtual voidsetHeight(unsigned int); ... }; class Square: publicRect { public: Square(); Square(int x, int y, unsigned int width); virtual voidsetWidth(unsigned int) virtual voidsetHeight(unsigned int); }; // implémentation: Rect.cpp void Rect::setWidth(unsigned int w) {width = w;} void Square::setWidth(unsigned int w) {width= height= w;} Rect::Rect():x(0), y(0), width(0), height(0) {} Square::Square() {} Square::Square(int x, int y, unsigned int w): Rect(x, y, w, w) {} /*...etc...*/ |
Remarques Dérivation de classe class Square: publicRect { .... }; héritage public des méthodes et variables de la super-classe = extends de Java peut aussi être private ou protected Chaînage des constructeurs Square::Square() {} Square::Square(int x, int y, unsigned int w): Rect(x, y, w, w){ } 1er cas : appel implicite de Rect( ) 2e cas : appel explicite de Rect(x, y, w, w) Pareil en Java, sauf syntaxe : mot-clé super() |
Headers et inclusions multiples Problème ? class Shape { // dansShape.h ... }; //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #include"Shape.h"// dansCircle.h class Circle : public Shape { ... }; //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #include"Shape.h"// dansRect.h class Rect : public Shape { ... }; //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #include"Circle.h"// dansmain.cpp #include"Rect.h" int main() { Circle * c = new Circle(); Rect * r = new Rect(); ... }; |
Headers et inclusions multiples (2) Problème : transitivité des inclusions le header Shape.h est inclus 2 fois dans main.cpp => la classe Shape est déclarée 2 fois => erreur de syntaxe ! Pour empêcher les redéclarations #ifndef_Shape_h_ // dans Shape.h #define_Shape_h_ class Shape { ... }; #endif A faire systématiquement en C / C++ pour tous les headers Note: #import fait cela automatiquement mais n'est pas standard |
Polymorphisme 3eme caractéristique fondamentale de la POO class Rect { int x, y; unsigned int width, height; public: virtual voidsetWidth(unsigned int w) {width = w;} ... }; class Square : public Rect { public: virtual voidsetWidth(unsigned int w) {width = height = w;} ... }; int main() { Rect* obj = newSquare(); // obj est un Square ou un Rect ? obj->setWidth(100); // quelle methode est appelée ? } |
Polymorphisme et liaison dynamique Polymorphisme un objet peut être vu sous plusieurs formes Rect* obj = new Square(); // obj est un Square ou un Rect ? obj->setWidth(100); // quelle methode est appelée ? Liaison dynamique (ou "tardive") la méthode liée à l'instance est appelée le choix de la méthode se fait à l'exécution mécanisme essentiel de l'OO ! Laison statique le contraire : la méthode liée au pointeur est appelée |
Méthodes virtuelles Deux cas possibles en C++ class Rect { public: virtualvoidsetWidth(unsigned int); // methode virtuelle }; class Square : public Rect { public: virtualvoidsetWidth(unsigned int); }; int main() { Rect* obj = newSquare(); obj->setWidth(100); } Méthodes virtuelles mot clé virtual => liaison dynamique : Square::setWidth() est appelée Méthodes non virtuelles PAS de mot clé virtual => liaison statique : Rect::setWidth() est appelée |
Pourquoi des méthodes virtuelles ? Cohérence logique les méthodes d'instance doivent généralement être virtuelles pour éviter les incohérences ! exemple : Rect* obj = newSquare(); obj->setWidth(100); setWidth() pas virtuelle => Square pas carré ! Java et C# même comportement que méthodes virtuelles |
Pourquoi des méthodes NON virtuelles ? !!! DANGER !!! code erroné si la fonction est redéfinie plus tard dans une sous-classe ! Exceptionnellement pour optimiser l'exécution si on est sûr que la fonction ne sera pas redéfinie accesseurs, souvent "inline" dans ce cas cas extrêmes, méthode appelée 10 000 000 fois... en Java on déclarerait la méthode final n'existe pas en C++ => mettre un commentaire |
Redéfinition des méthodes virtuelles class Rect { public: virtualvoidsetWidth(unsigned int); // virtualnécessaire }; class Square : public Rect { public: /*virtual*/voidsetWidth(unsigned int); // virtualimplicite}; Les redéfinitions des méthodes virtuelles sont toujours virtuelles même si virtual est omis => virtual particulièrement important dans les classes de base ! elles doivent toutes avoir la même signature sauf pour le type de retour (covariance des types) |
Surcharge des méthodes virtuelles Il faut rédéfinir toutes les variantes class Rect { public: virtualvoid setWidth(unsigned int); virtualvoid setWidth(double); //surchargede setWidth() }; class Square: publicRect { public: virtualvoid setWidth(unsigned int); //PAScorrect, redéfinir les deux }; class Square: publicRect { public: virtualvoid setWidth(unsigned int); virtualvoid setWidth(double); //correct}; |
Méthode abstraite Spécification d'un concept dont la réalisation peut varier ne peut pas être implémentée doit être redéfinie (et implémentée) dans les sous-classes ad hoc class Shape { public: virtualvoid setWidth(unsigned int)= 0; ... }; en C++ : virtual et = 0 (pure virtual function) en Java et en C# : abstract |
Classe abstraite Contient au moins une méthode abstraite => ne peut pas être instanciée Les classes héritées instanciables : doivent implémenter toutes les méthodes abstraites class Shape { // classe abstraitepublic: virtualvoid setWidth(unsigned int)= 0; ... }; class Rect: publicShape { // Rect peut être instanciée public: virtualvoid setWidth(unsigned int); ... }; |
Classes abstraites (2) Objectifs "commonaliser" les déclarations de méthodes (généralisation) -> permettre des traitements génériques sur une hiérarchie de classes imposer une spécification -> que les sous-classes doivent obligatoirement implémenter peur aussi servir pour encapsulation -> séparer la spécification (API) et l'implémentation -> les implémentations sont dans les sous-classes instanciables Remarque pas de mot-clé abstract comme en Java il suffit qu'une méthode soit abstraite |
Exemple class Shape { //classe abstraite int x, y; public: Shape():x(0), y(0) {} Shape(int _x, int _y):x(_x), y(_y) {} virtual int getX() const {return x;} virtual int getY() const {return y;} virtualunsigned intgetWidth() const= 0; //methodesvirtualunsigned intgetHeight() const= 0; //abstraitesvirtualunsigned intgetArea() const= 0; // ... idem pour setters }; class Circle: publicShape { unsigned int radius; public: Circle() : radius(0) {} Circle(int x, int y, unsigned int r) : Shape(x, y), radius(0) {} virtual unsigned int getRadius() const {return radius;} //redefinition et implementation des methodes abstraitesvirtualunsigned intgetWidth()const {return 2 * radius;} virtualunsigned intgetHeight()const {return 2 * radius;} virtualunsigned intgetArea()const {return PI * radius * radius;} // ... idem pour setters } |
Traitements génériques #include <iostream> #include "Shape.h" #include "Rect.h" #include "Square.h" #include "Circle.h" int main(int argc, char** argv) { Shape**tab = newShape*[10]; //tableau de Shape* unsigned int count = 0; tab[count++] = newCircle(0, 0, 100); tab[count++] = newRect(10, 10, 35, 40); tab[count++] = newSquare(0, 0, 60); for (int k = 0; k < count; k++) { cout << "Area = " << tab[k]->getArea() << endl; } } Note: traitements génériques != programmation générique (que l'on verra plus tard) but (en gros) similaire, approche différente |
Bénéfices du polymorphisme (1) Gestion unifiée des classes dérivant de la classe abstraite sans avoir besoin de connaître leur type contrairement aux langages non objet classiques (par exemple C) // fichier print.cpp #include <iostream> #include "Shape.h" void printAreas(Shape**tab, int count) { for (int k = 0; k < count; k++) { cout << "Area = " << tab[k]->getArea() << endl; } } Evolutivité rajout de nouvelles classes sans modification de l'existant |
Remarque en passant sur les "tableaux" C/C++ // fichier print.cpp #include <iostream> #include "Shape.h" void printAreas(Shape**tab, int count) { for (int k = 0; k < count; k++) { cout << "Area = " << tab[k]->getArea() << endl; } } En fait tab n'est pas un tableau mais un pointeur ! qui pointe sur le premier élément => count est indispensable pour savoir où le tableau se termine ! Remarque: cette notation est équivalente void printAreas(Shape*tab[], int count) { .... } |
Bénéfices du polymorphisme (2) Spécification indépendante de l'implémentation les classes se conforment à une spécification commune => indépendance des implémentations des divers "modules" => développement en parallèle par plusieurs équipes |
Interfaces Classes totalement abstraites toutes les méthodes sont abstraites aucune implémentation -> pure spécification d'API (Application Programming Interface) En C++: cas particulier de classe abstraite pas de mot-clé interface comme en Java pas indispensable car C++ supporte l'héritage multiple |
Exemple d'interface class Shape { // interface // pas de variables d'instance ni de constructeurpublic: virtual int getX() const = 0; // abstract virtual int getY() const = 0;// abstract virtual unsigned int getWidth() const = 0; // abstract virtual unsigned int getHeight() const = 0; // abstract virtual unsigned int getArea() const = 0; // abstract }; class Circle: publicShape { int x, y; unsigned int radius; public: Circle(); Circle(int x, int y, unsigned int r = 10); // getX() et getY() doivent être implémentées virtual int getX() const {return x;} virtual int getY() const {return y;} virtual unsigned int getRadius() const {return radius;} ...etc... } |
Complément: factorisation du code Eviter les duplications de code gain de temps évite des incohérences lisibilité par autrui maintenance : facilite les évolutions ultérieures Comment ? technique de base : héritage -> découpage astucieux des méthodes, méthodes intermédiaires ... rappel des méthodes des super-classes : classNamedRect: publicRect{ public: virtual void draw() { // affiche le rectangle et son nom Rect::draw(); // trace le rectangle /* code pour afficher le nom */ } }; |
Classes imbriquées (1) class Rect { class Point{ //classe imbriquee int x, y; public: Point(x, y); }; Point p1, p2; // variables d'instance public: Rect(int x1, int y1, int x2, int y2); }; Technique de composition très utile souvent préférable à l'héritage multiple (à suivre) Visibilité des champs depuis la classe imbriquée les champs de Rect sont automatiquement visibles depuis Point en Java mais pas en C++ ! il faut explicitement rajouter un pointeur vers la classe contenante |
Classes imbriquées (2) class Rect { class Point{ //classe imbriquee int x, y; public: Point(x, y); }; Point p1, p2; // variables d'instance public: Rect(int x1, int y1, int x2, int y2); }; Implémentation (si pas dans le header) Rect::Rect(int x1, int y1, int x2, int y2) :p1(x1,y1), p2(x2,y2) { } // appel du const. de la classe imbriquée Rect::Point::Point(int _x, int _y) //Rect::Point::Point! :x(_x), y(_y) { } |
Chapitre 3 : Mémoire Les différents types de mémoire mémoire statique / globale : réservée dès la compilation: variables static ou globales pile / stack : variables locales ("automatiques") et paramètres des fonctions mémoire dynamique / tas / heap : allouée à l'exécution par new (malloc en C) void foo() { staticint count = 0; //statique count++; int i = 0; //pile i++; int*p =newint(0); //dynamique (*p)++; //les parenthèses sont nécessaires! } que valent count, i, *p si on appelle foo() deux fois ? |
Mémoire Durée de vie mémoire statique / globale : toute la durée du programme pile : pendant l'exécution de la fonction mémoire dynamique : de new à delete (de malloc à free en C) void foo() { staticint count = 0; //statiqueint i = 0; //pileint*p =newint(0); //dynamique} A la sortie de la fonction count existe encore (et conserve sa valeur) i est détruite p est détruite (elle est dans la pile) mais pas ce qu'elle pointe => attention aux fuites mémoire (pas de ramasse miettes en C/C++) |
Mémoire : compléments // fichier toto.cpp bool is_valid = true; //globale staticconst char* errmsg = "Valeur invalide"; //statique globale void foo() { staticint count = 0; //statique locale int i = 0; //pileint*p =newint(0); //dynamique is_valid = false; cerr << errmsg << endl; } les variables globales sont dangereuses !!! il existe un 4e type de mémoire : la mémoire constante/read only (parfois appelée statique !) Java pas de variables globales ni static (sauf dans les classes) new pas possible sur un type de base |
Mémoire et objets C++ permet d'allouer des objets dans les trois types de mémoire, contrairement à Java ! void foo() { staticSquare a(5,5,20); //statique Square b(5,5,20); //pile Square*c =newSquare(5,5,20); //dynamique, seul cas en Java } les variables a et b contiennent l'objet impossible en Java : que des types de base ou des références dans la pile la variable c pointe vers l'objet même chose qu'en Java (sauf qu'il n'y a pas de ramasse miettes en C/C++) |
Création et destruction des objets void foo() { staticSquare a(5,5,20); //statique Square b(5,5,20); //pile Square*c =newSquare(5,5,20); //dynamique} Dans tous les cas Constructeur appelé quand l'objet est créé ainsi que ceux des superclasses (chaînage descendant des constructeurs) Destructeur appelé quand l'objet est détruit ainsi que ceux des superclasses (chaînage ascendant des destructeurs) |
Création et destruction des objets (2) void foo() { staticSquare a(5,5,20); //statique Square b(5,5,20); //pile Square*c =newSquare(5,5,20); //dynamique} new et delete à chaque new doit correspondre un (et un seul) delete delete p ne fait rien si p vaut NULL (ou 0) ne pas faire delete sur des objets en mémoire statique ou dans la pile ils sont détruits automatiquement Comment se passer de delete ? avec des smart pointers (à suivre) la mémoire est toujours récupérée en fin de programme aucun delete : solution acceptable si peu d'objets pas trop gros |
. versus -> void foo() { staticSquare a(5,5,20); //statique Square b(5,5,20); //pile Square*c =newSquare(5,5,20); //dynamique unsigned int w = a.getWidth(); int y = b.getY(); int x = c->getX(); } . pour accéder à un membre d'un objet (ou d'une struct en C) -> même chose depuis un pointeur (comme en C) c->getX( ) == (*c).getX( ) |
Objets contenant des objets class Dessin { staticSquare a; // var. de classe quicontientl'objet Square b; // var. d'instance quicontientl'objet Square*c; // var. d'instance quipointevers un objet (comme Java) staticSquare*d; // var. de classe quipointevers un objet (comme Java) }; Durée de vie l'objet a est automatiquement créé/détruit en même temps que le programme l'objet b est automatiquement créé/détruit en même temps que l'instance de Dessin l'objet pointé par c est typiquement : créé par le constructeur de Dessin detruit par le destructeur de Dessin |
Création de l'objet class Dessin { staticSquare a; Square b; Square*c; public: Dessin(int x, int y, unsigned int w): b(x, y, w), // appelle leconstructeurdeb c(newSquare(x, y, w)) { //créel'objet pointé parc } }; // dansun(et un seul) fichier.cpp Square Dessin::a(10, 20, 300); // ne pas repeter "static" Qu'est-ce qui manque ? |
Destruction de l'objet Il faut un destructeur ! chaque fois qu'un constructeur fait un new (sinon fuites mémoires) class Dessin { Square b; Square*c; public: Dessin(int x, int y, unsigned int w):b(x, y, w), c(newSquare(x, y, w)) { } virtual ~Dessin() {deletec;} // détruire l'objet créé par le constructeur }; Remarques b pas créé avec new => pas de delete destructeurs généralement virtuels pour avoir le polymorphisme Qu'est-ce qui manque ? |
Initialisation et affectation class Dessin { Square b; Square*c; public: Dessin(int x, int y, unsigned int w); virtual ~Dessin() {deletec;} }; void foo() { Dessin d1(0, 0, 50); // d1 contient l'objet Dessin d2(10, 20, 300); d2=d1; //affectation(d'un objet existant) Dessin d3(d1); //initialisation(d'un nouvel objet) Dessin d4=d1; // idem } Quel est le probleme ? quand on sort de foo() ... |
Initialisation et affectation void foo() { Dessin d1(0, 0, 50); Dessin d2(10, 20, 300); d2=d1; //affectation Dessin d3(d1); //initialisation Dessin d4=d1; // idem } Problème le contenu de d1 est copié champ à champ dans d2, d3 et d4 => tous les Dessins pointent sur la même instance de Square ! => elle est détruite 4 fois quand on sort de foo (et les autres jamais) ! Solution il faut de la copie profonde, la copie superficielle ne suffit pas problème géréral qui n'est pas propre à C/C++ : quel que soit le langage chaque dessin devrait avoir son propre Square |
1ere solution : interdire la copie d'objets La copie d'objets est dangereuse s'ils contiennent des pointeurs ou des références ! Solution de base : pas de copie, comme en Java seuls les types de base peuvent être copiés avec l'opérateur = en Java class Dessin { ....private: Dessin(const Dessin&); // initialisation: Dessin a=b; Dessin&operator=(const Dessin&); // affectation: a=b; }; déclarer privés l'opérateur d'initialisation (copy constructor) et d'affectation (operator=) implémentation inutile interdit également la copie pour les sous-classes (sauf si elles redéfinissent ces opérateurs) |
2eme solution : redéfinir la copie d'objets Solution avancée : copie profonde en C++: les 2 opérateurs recopient les objets pointés (et non les pointeurs) en Java: même chose via une méthode "copy" ou "clone" class Dessin : public Graphique { ....public: Dessin(const Dessin&); // initialisation: Dessin a=b; Dessin&operator=(const Dessin&); // affectation: a=b; .... }; Dessin::Dessin(const Dessin&from) :Graphique(from) { b = from.b; if (from.c != NULL) c = new Square(*from.c); //copie profonde else c = NULL; } Dessin&Dessin |
POO-C++Eric LecolinetTélécom ParisTech
Programmation orientée objet et autres concepts en C++ Eric Lecolinet Télécom ParisTech / Dpt. Infres Octobre 2012 License: Début du cours (utilisez les flèches en haut à gauche pour naviguer) Index Tout dans un seul fichier: HTML / PDF |
Brève historique Langage C C++ Bjarne Stroustrup, AT&T Bell Labs initialement une extension objet du C (pré-compilateur) plusieurs versions: de 1985 à normalisation ANSI / ISO (1998, 2003) C++11 : nouvelle version (août 2011) Java inspiré de la partie objet de C++ (et d'ADA, Smalltalk ...) C# inspiré de Java, de C++, etc. |
Brève historique (2) Objective C une autre extension objet du C promue par NeXt puis Apple (MacOSX, iOS) simple mais puissant et compatible avec C et C++ syntaxe inhabituelle inspirée de Smalltalk Python, Ruby visent la simplicité d'écriture et la flexibilité interprétés et basés sur le typage dynamique (comme Objective C) Evolutions récentes progression de Objective C, C# et Python aux dépends de Java C/C++ reste stable et (largement) majoritaire |
C++ versus C Avantage : compatibilité C/C++ même syntaxe de base code C facilement compilable en C++ (faibles différences syntaxiques) un programme peut combiner des fichiers C et C++ => idéal pour rendre un programme C orienté objet Inconvénient : compatibilité C/C++ C++ hérite de certains choix malencontreux du langage C ! |
C++ versus Java Ressemblances syntaxe en partie similaire fonctionnalités objet similaires (à part l'héritage multiple) Différences gestion mémoire (pas de garbage collecting, etc.) héritage multiple redéfinition des opérateurs templates et STL pas de threads dans le langage (mais bibliothèques ad hoc) langage compilé (et ... plus rapide !) |
En résumé C++ = langage objet ET procédural contrairement à Java, purement OO vision anachronique: C++ = "sorte de mélange de C et de Java" Bon côtés orienté objet avec l'efficacité du C (et compatible avec C) richesse du langage... Moins bon côtés difficultés héritées du C plus quelques ambiguités richesse du langage: programmes parfois inutilement complexes Things should be made as simple as possible but not any simpler (A. Einstein) |
Références et liens Références Le langage C++, Bjarne Stroustrup (Pearson) www.cplusplus.com/reference www.cppreference.com Liens utiles Travaux Pratiques: www.enst.fr/~elc/cpp/TP.html Introduction au toolkit graphique Qt: www.enst.fr/~elc/qt Boost C++ Libraries: www.boost.org voir aussi liens en 1ere page... |
Compilateurs Attention aux incompabilités entre les versions de C++ syntaxiques binaires le programme et les librairies doivent être compilés avec des versions compatibles de C++ Salles Unix à Télécom g++ version 4.*.* |
Premier chapitre Des objets et des classes ... |
Programme C++ Un programme C++ est constituté : de classes réparties dans plusieurs fichiers (comme en Java) (éventuellement) de fonctions et variables globales (comme en C) Chaque fichier peut comprendre : un nombre arbitraire de classes (si ça a un sens ...) |
Déclarations et définitions Comme en langage C : déclarations dans fichiers headers : xxx.h (ou .hpp ou .hh) définitions dans fichiers d'implémentation : xxx.cpp (ou .cc ou .C) Règles de base : à chaque .cpp (définitions) correspond un .h (déclarations) le .h déclare l'API publique, c'est une interface avec le monde extérieur |
Déclarations et définitions Comme en langage C : déclarations dans fichiers headers : xxx.h (ou .hpp ou .hh) définitions dans fichiers d'implémentation : xxx.cpp (ou .cc ou .C) Règles de base : à chaque .cpp (définitions) correspond un .h (déclarations) le .h déclare l'API publique, c'est une interface avec le monde extérieur Remarque on peut aussi "cacher" des variables ou des fonctions en les déclarant : directement dans le .cpp dans un header privé (exemple: xxx_impl.h) => surtout pour les librairies |
Déclaration de classe // fichierCircle.h: header contenant les déclarations class Circle { public: int x, y; // variables d'instance unsigned int radius; virtual void setRadius(unsigned int); // méthodes d'instance virtual unsigned int getRadius() const; virtual unsigned int getArea() const; .... }; // !NE PAS OUBLIER LE ; Remarques le ; final est obligatoire après la } même sémantique que Java, syntaxe similaire mais ... l'implémentation est (de préférence) séparée des déclarations |
Implémentation de classe // Rappel des déclarations class Circle { public: int x, y; // variables d'instance unsigned int radius; virtual void setRadius(unsigned int); // méthodes d'instance virtual unsigned int getRadius() const; virtual unsigned int getArea() const; }; Implémentation // fichierCircle.cpp: contient l'implementation #include "Circle.h" // ne pas oublier d'inclure le header ! void Circle::setRadius(unsigned int r) { // noter le :: radius = r; } unsigned int Circle::getRadius() const { return radius; } unsigned int Circle::getArea() const { return 3.1416 * radius * radius; } |
Instanciation // fichiermain.cpp: main() est le point d'entrée du programme #include "Circle.h" // ne pas oublier d'inclure le header int main() { Circle* c = new Circle(); ..... } Ne pas mettre main() dans Circle.cpp sinon la classe Circle ne sera pas réutilisable dans un autre programme ! |
Instanciation // fichiermain.cpp#include"Circle.h" int main() { Circle*c =newCircle(); ..... } new cree un objet (= nouvelle instance de la classe) allocation mémoire c = variable locale qui pointe sur le nouvel objet c est un pointeur |
Comparaison avec Java Pointeur C++ vs. référence Java C++: Circle*c = new Circle(); //pointeurC++ Java: Circle c = new Circle(); //référenceJava dans les 2 cas: une variable qui pointe sur un objet attention: "référence" a un autre sens en C++ (à suivre...) Gestion mémoire Java detruit les objets qui n'ont plus de référent (garbage collector) C++ nécessite une destruction explicite par l'opérateur delete |
Accès aux variables d'instance #include "Circle.h" int main() { Circle* c1 = new Circle(); c1->x = 100; // noter la -> c1->y = 200; c1->radius = 35; Circle *c2 = new Circle(); c2->x = c1->x; } Chaque objet possède sa propre copie des variables d'instance noter l'utilisation de la -> (comme en C, mais . en Java) c->x équivaut à: (*c).x encapsulation => restreindre l'accès aux variables d'instance |
Appel des méthodes d'instance int main() { Circle* c1 = new Circle(); Circle* c2 = new Circle(); // attention: c->x, c->y, c->radius pas initialisés ! unsigned int r = c1->getRadius(); unsigned int a = c2->getArea(); } Toujours appliquées à un objet ont accès à toutes les variables de cet objet propriété fondamentale de l'orienté objet ! unsigned int Circle::getRadius() const { // dans Circle.cpp return radius; } |
Constructeurs class Circle { int x, y; unsigned int radius; public: Circle(int x, int y, unsigned int r); // declaration .... }; Circle::Circle(int _x, int _y, unsigned int _r) { // implementation x = _x; y = _y; radius = _r; } Circle* c = newCircle(100, 200, 35); // instanciation pour initialiser les variables d'instance |
Constructeurs (suite) Deux syntaxes (quasi) équivalentes Circle::Circle(int _x, int _y, unsigned int _r) { // comme en Java x = _x; y = _y; radius = _r; } Circle::Circle(int _x, int _y, unsigned int _r) // propre à C++ :x(_x), y(_y), radius(_r){ } la 2eme forme est préférable (vérifie l'ordre des déclarations) Chaînage des constructeurs appel implicite des constructeurs des super-classes dans l'ordre descendant |
Constructeur par défaut Si aucun constructeur dans la classe on peut cependant écrire : Circle*c =newCircle(); car C++ crée un constructeur par défaut mais qui ne fait rien ! => variables pas initialisées (contrairement à Java) Conseils toujours definir au moins un constructeur et toujours initialiser les variables de plus, c'est une bonne idée de définir un constructeur sans argument : class Circle { int x, y; unsigned int radius; public: Circle(int x, int y, unsigned int r); Circle():x(0), y(0), radius(0){ } .... }; |
Destruction Circle* c = new Circle(100, 200, 35); ...deletec; // destruction de l'objet c= NULL; // c pointe sur aucun object } delete détruit un objet crée par new pas de garbage collector (ramasse miettes) comme en Java ! NB: on verra plus tard comment se passer de delete Attention: delete ne met pas c à NULL ! Remarque NULL est une macro qui vaut 0 (ce n'est pas un mot-clé) |
Destructeur class Circle { public: virtual~Circle(); // destructeur ... }; Circle* c = new Circle(100, 200, 35); ...deletec; // destruction de l'objet c = NULL; Fonction appelée à la destruction de l'objet un seul destructeur par classe (pas d'argument) dans l'ordre ascendant (inverse des constructeurs) |
delete & destructeur Attention c'est delete qui detruit l'objet (qui'il y ait ou non un destructeur) le destructeur (s'il existe) est juste une fonction appelée avant la destruction Quand faut-il un destructeur ? si l'objet a des vars d'instance qui pointent vers des objets à detruire si l'objet a ouvert des fichiers, sockets... qu'il faut fermer pour la classe de base d'une hiérarchie de classes Et en Java ...? |
delete & destructeur Et en Java ? delete n'existe pas car GC (garbage collector) la methode finalize() joue le meme role que le destructeur, mais: pas de chainage des "finaliseurs" appel non deterministe par le GC (on ne sait pas quand l'objet est détruit) |
Surcharge (overloading) Plusieurs méthodes ayant le même nom mais des signatures différentes pour une même classe class Circle { Circle(); Circle(int x, int y, unsigned int r); .... }; Remarques la valeur de retour ne suffit pas à distinguer les signatures applicable aux fonctions "classiques" (hors classes) |
Paramètres par défaut class Circle { Circle(int x, int y, unsigned int r= 10); .... }; Circle* c1 = new Circle(100, 200, 35); Circle* c2 = new Circle(100, 200); // radius vaudra 10 Remarques en nombre quelconque mais toujours en dernier erreur de compilation s'il y a des ambiguités : class Circle { Circle(); Circle(int x, int y, unsigned int r = 10); // OK Circle(int x = 0, int y = 0, unsigned int r = 10); // AMBIGU! .... }; |
Variables de classe class Circle { // fichier Circle.h public: staticconst float PI; // variable declasse int x, y; // variables d'instance unsigned int radius; ... }; Représentation unique en mémoire mot-clé static Remarques const (optionnel) indique que la valeur est constante notion similaire aux variables "statiques" du C (d'où le mot-clé) |
Définition des variables de classe Les variables de classe doivent également être définies dans un (et un seul) .cpp, sans répéter static ce n'est pas nécessaire en Java ou C# // dans Circle.cpp const float Circle::PI = 3.1415926535; // noter le:: Exception les variables de classe const int peuvent être définies dans les headers // dans Circle.h staticconst intTAILLE_MAX = 100; |
Méthodes de classe //déclaration: fichier Circle.h class Circle { public: static const float PI; staticfloat getPI() {return PI;} ... }; //appel: fichier main.cpp float x = Circle::getPI(); Ne s'appliquent pas à un objet mot-clé static similaire à une fonction "classique" du C (mais évite collisions de noms) N'ont accès qu'aux variables de classe ! |
Namespaces namespace = espace de nommage solution ultime aux collisions de noms existent aussi en C#, similaires aux packages de Java namespaceGeom{// dans Circle.h class Circle { ... }; } ---------------------------------------------------------------- namespaceMath{// dans Math.h class Circle { // une autre classe Circle... ... }; } ---------------------------------------------------------------- #include "Circle.h" // dans main.cpp #include "Math.h" int main() { Geom::Circle* c1 = newGeom::Circle(); Math::Circle* c2 = newMath::Circle(); ... } |
Namespaces using namespace modifie les règles de portée les symboles déclarés dans ce namespace deviennent directement accessibles similaire à import en Java namespaceGeom{// dans Circle.h class Circle { ... }; } ---------------------------------------------------------------- int main() { Geom::Circle* c1 = newGeom::Circle(); Circle* c2 = new Circle(); // OK grace a using namespace... ... } |
Bibliothèque standard d'E/S #include <iostream> // E/S du C++ #include "Circle.h" using namespacestd; int main() { Circle* c = new Circle(100, 200, 35); cout<< "radius= " << c->getRadius() << "area= " << c->getArea() <<endl; std::cerr<< "c = " << c << std::endl; } Concaténation des arguments via << ou >> std::cout : sortie standard std::cerr : sortie des erreurs std::cin : entrée standard (utiliser >> au lieu de <<) |
Encapsulation / droits d'accès class Circle {private:: int x, y; unsigned int radius;public: static const float PI; Circle(); Circle(int x, int y, unsigned int r); }; Trois niveaux private (le défaut en C++) : accès réservé à cette classe protected : idem + sous-classes public NB: Java a un niveau package (défaut), C++ a également friend |
Encapsulation / droits d'accès (2) class Circle { // private: //privatepar defaut int x, y; unsigned int radius;public: static const float PI; // PI estpubliccar const Circle(); Circle(int x, int y, unsigned int r); }; Règles usuelles d'encapsulation l'API (méthodes pour communiquer avec les autres objets) est public l'implémentation (variables et méthodes internes) est private ou protected |
Encapsulation / droits d'accès (3) class Circle { friendclass Manager; friendbool equals(const Circle*, const Circle*); ... }; friend donne accès à tous les champs de Circle à une autre classe : Manager à une fonction : bool equals(const Circle*, const Circle*) |
struct struct = class + public structTruc { ... }; équivaut à : est équivalent à class en C++ n'existe pas en Java existe en C# mais ce n'est pas une class existe en C mais c'est juste un agrégat de variables |
Méthodes d'instance: où est la magie ? Toujours appliquées à un objet class Circle { unsigned int radius; int x, y; public: virtualunsigned int getRadius()const;virtualunsigned int getArea()const; }; int main() { Circle* c = new Circle(100, 200, 35); unsigned int r = c->getRadius(); // OK unsigned int a =getArea(); // INCORRECT: POURQUOI? } Et pourtant : unsigned int Circle::getArea() const { return PI *getRadius()*getRadius(); // idem } unsigned int Circle::getRadius() const { returnradius; // comment getRadius() accede a radius ? } |
Le this des méthodes d'instance Paramètre caché this pointe sur l'objet qui appelle la méthode permet d'accéder aux variables d'instance unsigned int Circle::getArea() const { return PI *radius*getRadius(); } Circle* c = ...; unsigned int a =c->getArea(); Transformé par le compilateur en : unsigned int Circle::getArea(Circle* this) const { return Circle::PI *this->radius * getRadius(this); } Circle* c = ...; unsigned int a =Circle::getArea(c); //__ZNK6Circle7getAreaEv(c) avec g++ 4.2 |
Inline Indique que la fonction est implémentée dans le header // dans Circle.h class Circle { public: inlineint getX() const {return x;} int getY() const {return y;} // pareil: inline est implicite .... }; // inline doit être présent si fonction non-membreinlineCircle* createCircle() {return new Circle();} A utiliser avec discernement + : rapidité à l'exécution : peut éviter un appel fonctionnel (code dupliqué) - : augmente taille du binaire généré - : lisibilité - : contraire au principe d'encapsulation |
Point d'entrée du programme int main(int argc, char** argv) valeur de retour : normalement 0, indique une erreur sinon |
Terminologie Méthode versus fonction méthodes d'instance == fonctions membres méthodes de classe == fonctions statiques fonctions classiques == fonctions globales etc. Termes interchangeables selon auteurs |
Doxygen /** modélise un cercle. * Un cercle n'est pas un carré ni un triangle. */ class Circle { /// retourne la largeur. virtual unsigned int getWidth() const; virtual unsigned int getHeight() const; ///< retourne la hauteur. virtual void setPos(int x, int y); /**< change la position. * voir aussi setX() et setY(). */ ... }; Système de documentation automatique similaire à JavaDoc mais plus général : fonctionne avec de nombreux langages documentation : www.doxygen.org |
Chapitre 2 : Héritage Concept essentiel de l'OO héritage simple (comme Java) héritage multiple (à manier avec précaution : voir plus loin) |
Règles d'héritage Constructeurs jamais hérités Méthodes héritées peuvent être redéfinies (overriding) : la nouvelle méthode remplace celle de la superclasse ! ne pas confondre surcharge et redéfinition ! Variables héritées peuvent être surajoutées (shadowing) : la nouvelle variable cache celle de la superclasse ! à éviter : source de confusions ! |
Exemple (déclarations) // header Rect.h class Rect { int x, y; unsigned int width, height; public:Rect(); Rect(int x, int y, unsigned int width, unsigned int height); virtual voidsetWidth(unsigned int); virtual voidsetHeight(unsigned int); virtual unsigned int getWidth() const {return width;} virtual unsigned int getHeight() const {return height;} /*...etc...*/ }; class Square: publicRect { //dérivationde classe public: Square(); Square(int x, int y, unsigned int width); virtual voidsetHeight(unsigned int); }; |
Exemple (implémentation) class Rect { // rappel des délarations int x, y; unsigned int width, height; public: Rect(); Rect(int x, int y, unsigned int width, unsigned int height); virtual voidsetWidth(unsigned int); virtual voidsetHeight(unsigned int); ... }; class Square: publicRect { public: Square(); Square(int x, int y, unsigned int width); virtual voidsetWidth(unsigned int) virtual voidsetHeight(unsigned int); }; // implémentation: Rect.cpp void Rect::setWidth(unsigned int w) {width = w;} void Square::setWidth(unsigned int w) {width= height= w;} Rect::Rect():x(0), y(0), width(0), height(0) {} Square::Square() {} Square::Square(int x, int y, unsigned int w): Rect(x, y, w, w) {} /*...etc...*/ |
Remarques Dérivation de classe class Square: publicRect { .... }; héritage public des méthodes et variables de la super-classe = extends de Java peut aussi être private ou protected Chaînage des constructeurs Square::Square() {} Square::Square(int x, int y, unsigned int w): Rect(x, y, w, w){ } 1er cas : appel implicite de Rect( ) 2e cas : appel explicite de Rect(x, y, w, w) Pareil en Java, sauf syntaxe : mot-clé super() |
Headers et inclusions multiples Problème ? class Shape { // dansShape.h ... }; //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #include"Shape.h"// dansCircle.h class Circle : public Shape { ... }; //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #include"Shape.h"// dansRect.h class Rect : public Shape { ... }; //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #include"Circle.h"// dansmain.cpp #include"Rect.h" int main() { Circle * c = new Circle(); Rect * r = new Rect(); ... }; Problème : transitivité des inclusions le header Shape.h est inclus 2 fois dans main.cpp => la classe Shape est déclarée 2 fois => erreur de syntaxe ! Pour empêcher les redéclarations #ifndef_Shape_h_ // dans Shape.h #define_Shape_h_ class Shape { ... }; #endif A faire systématiquement en C / C++ pour tous les headers Note: #import fait cela automatiquement mais n'est pas standard |
Polymorphisme 3eme caractéristique fondamentale de la POO class Rect { int x, y; unsigned int width, height; public: virtual voidsetWidth(unsigned int w) {width = w;} ... }; class Square : public Rect { public: virtual voidsetWidth(unsigned int w) {width = height = w;} ... }; int main() { Rect* obj = newSquare(); // obj est un Square ou un Rect ? obj->setWidth(100); // quelle methode est appelée ? } |
Polymorphisme et liaison dynamique Polymorphisme un objet peut être vu sous plusieurs formes Rect* obj = new Square(); // obj est un Square ou un Rect ? obj->setWidth(100); // quelle methode est appelée ? Liaison dynamique (ou "tardive") la méthode liée à l'instance est appelée le choix de la méthode se fait à l'exécution mécanisme essentiel de l'OO ! Laison statique le contraire : la méthode liée au pointeur est appelée |
Méthodes virtuelles Deux cas possibles en C++ class Rect { public: virtualvoidsetWidth(unsigned int); // methode virtuelle }; class Square : public Rect { public: virtualvoidsetWidth(unsigned int); }; int main() { Rect* obj = newSquare(); obj->setWidth(100); } Méthodes virtuelles mot clé virtual => liaison dynamique : Square::setWidth() est appelée Méthodes non virtuelles PAS de mot clé virtual => liaison statique : Rect::setWidth() est appelée |
Pourquoi des méthodes virtuelles ? Cohérence logique Rect* obj = newSquare(); obj->setWidth(100); setWidth() pas virtuelle => Square pas carré ! Java et C# même comportement que méthodes virtuelles |
Pourquoi des méthodes NON virtuelles ? !!! DANGER !!! code erroné si la fonction est redéfinie plus tard dans une sous-classe ! Exceptionnellement pour optimiser l'exécution si on est sûr que la fonction ne sera pas redéfinie accesseurs, souvent "inline" dans ce cas cas extrêmes, méthode appelée 10 000 000 fois... en Java on déclarerait la méthode final n'existe pas en C++ => mettre un commentaire |
Redéfinition des méthodes virtuelles class Rect { public: virtualvoidsetWidth(unsigned int); // virtualnécessaire }; class Square : public Rect { public: /*virtual*/voidsetWidth(unsigned int); // virtualimplicite}; Les redéfinitions des méthodes virtuelles sont toujours virtuelles même si virtual est omis => virtual particulièrement important dans les classes de base ! elles doivent toutes avoir la même signature sauf pour le type de retour (covariance des types) |
Surcharge des méthodes virtuelles Il faut rédéfinir toutes les variantes class Rect { public: virtualvoid setWidth(unsigned int); virtualvoid setWidth(double); //surchargede setWidth() }; class Square: publicRect { public: virtualvoid setWidth(unsigned int); //PAScorrect, redéfinir les deux }; class Square: publicRect { public: virtualvoid setWidth(unsigned int); virtualvoid setWidth(double); //correct}; |
Méthode abstraite Spécification d'un concept dont la réalisation peut varier ne peut pas être implémentée doit être redéfinie (et implémentée) dans les sous-classes ad hoc class Shape { public: virtualvoid setWidth(unsigned int)= 0; ... }; en C++ : virtual et = 0 (pure virtual function) en Java et en C# : abstract |
Classe abstraite => ne peut pas être instanciée Les classes héritées instanciables : doivent implémenter toutes les méthodes abstraites class Shape { // classe abstraitepublic: virtualvoid setWidth(unsigned int)= 0; ... }; class Rect: publicShape { // Rect peut être instanciée public: virtualvoid setWidth(unsigned int); ... }; |
Classes abstraites (2) Objectifs "commonaliser" les déclarations de méthodes (généralisation) -> permettre des traitements génériques sur une hiérarchie de classes imposer une spécification -> que les sous-classes doivent obligatoirement implémenter peur aussi servir pour encapsulation -> séparer la spécification (API) et l'implémentation -> les implémentations sont dans les sous-classes instanciables Remarque pas de mot-clé abstract comme en Java il suffit qu'une méthode soit abstraite |
Exemple class Shape { //classe abstraite int x, y; public: Shape():x(0), y(0) {} Shape(int _x, int _y):x(_x), y(_y) {} virtual int getX() const {return x;} virtual int getY() const {return y;} virtualunsigned intgetWidth() const= 0; //methodesvirtualunsigned intgetHeight() const= 0; //abstraitesvirtualunsigned intgetArea() const= 0; // ... idem pour setters }; class Circle: publicShape { unsigned int radius; public: Circle() : radius(0) {} Circle(int x, int y, unsigned int r) : Shape(x, y), radius(0) {} virtual unsigned int getRadius() const {return radius;} //redefinition et implementation des methodes abstraitesvirtualunsigned intgetWidth()const {return 2 * radius;} virtualunsigned intgetHeight()const {return 2 * radius;} virtualunsigned intgetArea()const {return PI * radius * radius;} // ... idem pour setters } |
Traitements génériques #include <iostream> #include "Shape.h" #include "Rect.h" Shape**tab = newShape*[10]; //tableau de Shape* unsigned int count = 0; tab[count++] = newCircle(0, 0, 100); tab[count++] = newRect(10, 10, 35, 40); tab[count++] = newSquare(0, 0, 60); for (int k = 0; k < count; k++) { cout << "Area = " << tab[k]->getArea() << endl; } } Note: traitements génériques != programmation générique (que l'on verra plus tard) but (en gros) similaire, approche différente |
Bénéfices du polymorphisme (1) Gestion unifiée des classes dérivant de la classe abstraite sans avoir besoin de connaître leur type contrairement aux langages non objet classiques (par exemple C) // fichier print.cpp #include <iostream> #include "Shape.h" void printAreas(Shape**tab, int count) { for (int k = 0; k < count; k++) { cout << "Area = " << tab[k]->getArea() << endl; } } Evolutivité rajout de nouvelles classes sans modification de l'existant |
Remarque en passant sur les "tableaux" C/C++ // fichier print.cpp #include <iostream> #include "Shape.h" void printAreas(Shape**tab, int count) { for (int k = 0; k < count; k++) { cout << "Area = " << tab[k]->getArea() << endl; } } En fait tab n'est pas un tableau mais un pointeur ! qui pointe sur le premier élément => count est indispensable pour savoir où le tableau se termine ! Remarque: cette notation est équivalente void printAreas(Shape*tab[], int count) { .... } |
Bénéfices du polymorphisme (2) Spécification indépendante de l'implémentation les classes se conforment à une spécification commune => indépendance des implémentations des divers "modules" => développement en parallèle par plusieurs équipes |
Interfaces Classes totalement abstraites toutes les méthodes sont abstraites aucune implémentation En C++: cas particulier de classe abstraite pas de mot-clé interface comme en Java pas indispensable car C++ supporte l'héritage multiple |
Exemple d'interface class Shape { // interface // pas de variables d'instance ni de constructeurpublic: virtual int getX() const = 0; // abstract virtual int getY() const = 0;// abstract virtual unsigned int getWidth() const = 0; // abstract virtual unsigned int getHeight() const = 0; // abstract virtual unsigned int getArea() const = 0; // abstract }; class Circle: publicShape { int x, y; unsigned int radius; public: Circle(); Circle(int x, int y, unsigned int r = 10); // getX() et getY() doivent être implémentées virtual int getX() const {return x;} virtual int getY() const {return y;} virtual unsigned int getRadius() const {return radius;} ...etc... } |
Complément: factorisation du code Eviter les duplications de code gain de temps évite des incohérences lisibilité par autrui maintenance : facilite les évolutions ultérieures Comment ? technique de base : héritage -> découpage astucieux des méthodes, méthodes intermédiaires ... rappel des méthodes des super-classes : classNamedRect: publicRect{ public: virtual void draw() { // affiche le rectangle et son nom Rect::draw(); // trace le rectangle /* code pour afficher le nom */ } }; |
Classes imbriquées (1) class Rect { class Point{ //classe imbriquee int x, y; public: Point(x, y); }; Point p1, p2; // variables d'instance public: Rect(int x1, int y1, int x2, int y2); }; Technique de composition très utile souvent préférable à l'héritage multiple (à suivre) Visibilité des champs depuis la classe imbriquée il faut explicitement rajouter un pointeur vers la classe contenante |
Classes imbriquées (2) class Rect { class Point{ //classe imbriquee int x, y; public: Point(x, y); }; Point p1, p2; // variables d'instance public: Rect(int x1, int y1, int x2, int y2); }; Implémentation (si pas dans le header) Rect::Rect(int x1, int y1, int x2, int y2) :p1(x1,y1), p2(x2,y2) { } // appel du const. de la classe imbriquée Rect::Point::Point(int _x, int _y) //Rect::Point::Point! :x(_x), y(_y) { } |
Chapitre 3 : Mémoire Les différents types de mémoire mémoire statique / globale : réservée dès la compilation: variables static ou globales pile / stack : variables locales ("automatiques") et paramètres des fonctions mémoire dynamique / tas / heap : allouée à l'exécution par new (malloc en C) void foo() { staticint count = 0; //statique count++; int i = 0; //pile i++; int*p =newint(0); //dynamique (*p)++; //les parenthèses sont nécessaires! } que valent count, i, *p si on appelle foo() deux fois ? |
Mémoire Durée de vie mémoire statique / globale : toute la durée du programme pile : pendant l'exécution de la fonction mémoire dynamique : de new à delete (de malloc à free en C) void foo() { staticint count = 0; //statiqueint i = 0; //pileint*p =newint(0); //dynamique} A la sortie de la fonction count existe encore (et conserve sa valeur) i est détruite p est détruite (elle est dans la pile) mais pas ce qu'elle pointe => attention aux fuites mémoire (pas de ramasse miettes en C/C++) |
Mémoire : compléments // fichier toto.cpp void foo() { staticint count = 0; //statique locale int i = 0; //pileint*p =newint(0); //dynamique is_valid = false; cerr << errmsg << endl; } les variables globales sont dangereuses !!! il existe un 4e type de mémoire : la mémoire constante/read only (parfois appelée statique !) Java pas de variables globales ni static (sauf dans les classes) new pas possible sur un type de base |
Mémoire et objets C++ permet d'allouer des objets dans les trois types de mémoire, contrairement à Java ! void foo() { staticSquare a(5,5,20); //statique Square b(5,5,20); //pile Square*c =newSquare(5,5,20); //dynamique, seul cas en Java } les variables a et b contiennent l'objet impossible en Java : que des types de base ou des références dans la pile la variable c pointe vers l'objet même chose qu'en Java (sauf qu'il n'y a pas de ramasse miettes en C/C++) |
Création et destruction des objets void foo() { staticSquare a(5,5,20); //statique Square b(5,5,20); //pile Square*c =newSquare(5,5,20); //dynamique} Dans tous les cas Constructeur appelé quand l'objet est créé ainsi que ceux des superclasses (chaînage descendant des constructeurs) Destructeur appelé quand l'objet est détruit ainsi que ceux des superclasses (chaînage ascendant des destructeurs) |
Création et destruction des objets (2) void foo() { staticSquare a(5,5,20); //statique Square b(5,5,20); //pile Square*c =newSquare(5,5,20); //dynamique} new et delete ne pas faire delete sur des objets en mémoire statique ou dans la pile ils sont détruits automatiquement Comment se passer de delete ? avec des smart pointers (à suivre) la mémoire est toujours récupérée en fin de programme aucun delete : solution acceptable si peu d'objets pas trop gros |
. versus -> void foo() { staticSquare a(5,5,20); //statique Square b(5,5,20); //pile Square*c =newSquare(5,5,20); //dynamique unsigned int w = a.getWidth(); int y = b.getY(); int x = c->getX(); } . pour accéder à un membre d'un objet (ou d'une struct en C) -> même chose depuis un pointeur (comme en C) c->getX( ) == (*c).getX( ) |
Objets contenant des objets class Dessin { staticSquare a; // var. de classe quicontientl'objet Square b; // var. d'instance quicontientl'objet Square*c; // var. d'instance quipointevers un objet (comme Java) staticSquare*d; // var. de classe quipointevers un objet (comme Java) }; Durée de vie l'objet a est automatiquement créé/détruit en même temps que le programme l'objet b est automatiquement créé/détruit en même temps que l'instance de Dessin l'objet pointé par c est typiquement : créé par le constructeur de Dessin detruit par le destructeur de Dessin |
Création de l'objet class Dessin { staticSquare a; Square b; Square*c; public: Dessin(int x, int y, unsigned int w): b(x, y, w), // appelle leconstructeurdeb c(newSquare(x, y, w)) { //créel'objet pointé parc } }; // dansun(et un seul) fichier.cpp Square Dessin::a(10, 20, 300); // ne pas repeter "static" Qu'est-ce qui manque ? |
Destruction de l'objet Il faut un destructeur ! chaque fois qu'un constructeur fait un new (sinon fuites mémoires) public: Dessin(int x, int y, unsigned int w):b(x, y, w), c(newSquare(x, y, w)) { } virtual ~Dessin() {deletec;} // détruire l'objet créé par le constructeur }; Remarques b pas créé avec new => pas de delete destructeurs généralement virtuels pour avoir le polymorphisme Qu'est-ce qui manque ? |
Initialisation et affectation class Dessin { Square b; Square*c; public: Dessin(int x, int y, unsigned int w); virtual ~Dessin() {deletec;} }; void foo() { Dessin d1(0, 0, 50); // d1 contient l'objet Dessin d2(10, 20, 300); d2=d1; //affectation(d'un objet existant) Dessin d3(d1); //initialisation(d'un nouvel objet) Dessin d4=d1; // idem } Quel est le probleme ? quand on sort de foo() ... |
Initialisation et affectation void foo() { Dessin d1(0, 0, 50); Dessin d2(10, 20, 300); d2=d1; //affectation Dessin d3(d1); //initialisation Dessin d4=d1; // idem } Problème le contenu de d1 est copié champ à champ dans d2, d3 et d4 => tous les Dessins pointent sur la même instance de Square ! => elle est détruite 4 fois quand on sort de foo (et les autres jamais) ! Solution il faut de la copie profonde, la copie superficielle ne suffit pas problème géréral qui n'est pas propre à C/C++ : quel que soit le langage chaque dessin devrait avoir son propre Square |
1ere solution : interdire la copie d'objets La copie d'objets est dangereuse s'ils contiennent des pointeurs ou des références ! Solution de base : pas de copie, comme en Java seuls les types de base peuvent être copiés avec l'opérateur = en Java class Dessin { ....private: Dessin(const Dessin&); // initialisation: Dessin a=b; Dessin&operator=(const Dessin&); // affectation: a=b; }; interdit également la copie pour les sous-classes (sauf si elles redéfinissent ces opérateurs) |
2eme solution : redéfinir la copie d'objets Solution avancée : copie profonde en C++: les 2 opérateurs recopient les objets pointés (et non les pointeurs) en Java: même chose via une méthode "copy" ou "clone" class Dessin : public Graphique { ....public: Dessin(const Dessin&); // initialisation: Dessin a=b; Dessin&operator=(const Dessin&); // affectation: a=b; .... }; Dessin::Dessin(const Dessin&from) :Graphique(from) { b = from.b; if (from.c != NULL) c = new Square(*from.c); //copie profonde else c = NULL; } Dessin&Dessin |