Structures de données IFT-2000 Abder Alikacem Classes et objets en C++ Édition Septembre 2009 Département d’informatique et de génie logiciel Département d’informatique et de génie logiciel
Plan Classes et objets Constructeur et destructeur Imbrication de classes Composition de classes Méthode et classe friend Méthodes statiques Méthodes optimisées Surcharge des opérateurs Le pointeur this Constructeur de copie et opérateur d’affectation Opérateur de portée :: Conversion de types Portée des identificateurs Membres mutables Traitement des exceptions
Les classes Définition d’un nouveau type au sens C++ : ensemble de méthodes (fonctionnalités) auxquelles sont ajoutés des membres (données). Les classes sont utilisées afin de fournir aux programmeurs les outils nécessaires pour concevoir des nouveaux types qui soient aussi faciles d'utilisation que les types de base. Ce sont des ensembles de: données ayant un lien entre elles (comme struct en C) « données membres » + méthodes servant à manipuler les données membres « fonctions membres » C’est la structure de base des langages orientés objet.
Les classes Les classes sont comme les structures du langage C auxquelles on a ajouter des fonctionnalités supplémentaires. En particulier: Tout comme les structures en C, les classes peuvent contenir des définitions de variables. 2. Les classe peuvent en plus contenir des définitions de fonctions. 3. Par défaut les membres (variables ou fonctions) d'une classe sont privés et inaccessibles au reste du programme. Pour rendre un membre accessible celui-ci doit explicitement être déclaré public.
Les classes Une classe est une implémentation d’un type. Une variable de ce type (une instance de la classe) est appelée un objet. Une classe est une structure d'objets, c'est-à-dire la déclaration de l'ensemble des entités qui composeront des objets. Une classe peut être considérée comme un moule à partir duquel on peut créer des objets. Un objet est donc "issu" d'une classe. C'est une instanciation d'une classe, c'est la raison pour laquelle on pourra parler indifféremment d'objet ou d'instance.
Les classes Syntaxe de la définition d’une classe class <class name> {<data + functions>} <vars> ; La déclaration de la classe commence par le mot clef class et est encadrée par une paire d'accolades. L'accolade finale est suivie d'un point virgule. Les membres déclarés après le mot clef public forment l'interface de la classe. Ceux qui suivent le mot private sont invisibles de l'utilisateur. Par défaut, en l ‘absence de ces mots clés, les membres de la classe sont « privées ». L'ordre de déclaration des méthodes et des attributs est laissé au libre arbitre du programmeur. La déclaration des attributs est semblable à la déclaration d'une variable alors que celle d'une méthode ressemble à s'y méprendre au prototype d'une fonction ! Néanmoins, il ne faut pas oublier que chaque méthode possède un argument caché : l'objet sur lequel elle est invoquée.
Les classes Une classe est composée de deux parties: - Les attributs (parfois appelés données membres): il s'agit des données représentant l'état de l'objet. - Les méthodes (parfois appelées fonctions membres): il s'agit des opérations applicables aux objets. Généralement, les méthodes sont de quatre types : les constructeurs (initialisation correcte) destructeur (fin de vie correcte) les accesseurs (lecture seulement) les modificateurs (lecture+écriture)
Les fonctions membres (méthodes) Les fonctions membres peuvent être définies dans la classe même, dans un module séparé ou plus loin dans le code source. Dans ces cas, il suffit de préciser la classe pour laquelle la méthode déclarée doit être implémentée. Syntaxe de la définition hors de la classe d'une méthode : type Classe::nom_méthode( paramètres_formels ) { // corps de la fonction } Si le code n’est pas trop long, les méthodes définies à l’intérieur de la classe (méthodes inline) peuvent avoir leur corps recopié à chaque appel pour accélérer l’exécution. Ce n’est pas le cas de celle définies à l’extérieur.
Les classes Public, private, protected Par défaut, dans une classe, seules les méthodes de cette classe peuvent accéder aux autres méthodes et aux membres. On dit que ceux-ci sont en accès private. Pour être utilisable, une classe doit donc rendre publiques certaines méthodes. La spécification de public : (resp. private :) à l’intérieur de la classe change l’accès par défaut pour les membres et fonctions situés après cette déclaration. Plusieurs public : (resp. private :) peuvent apparaître dans une classe. Troisième accès : protected, l’accès est autorisé aussi aux classes dérivées (nous verrons cela plus tard), mais pas pour les utilisations externes.
Les classes Exemple d’une classe constructeur class FigureGeometrique { public: FigureGeometrique(); ~FigureGeometrique(); inline double surface() const; inline const char *nom() const; void surface (double); private: const char * nom; double surface; } destructeur accesseurs modificateur
Les classes Exemple d’une classe…suite Les fonctions « inline» doivent être définies dans le même fichier que celui dans lequel est déclarée la classe. Par défaut, toute fonction définie dans le corps de la classe est automatiquement « inline» (définition dans FigureGeometrique.h). Les fonctions définies en dehors de la classe doivent être préfixées par <class name>:: Elles peuvent être définies dans un autre fichier que celui de la Classe (définition dans FigureGeometrique.cpp): double FigureGeometrique::surface() const { return surface; } const char * FigureGeometrique::nom() const { return nom; void FigureGeometrique::surface(double s) { surface= s;
Les classes Exemple d’une classe…suite Exemple de fonctions définies à l‘intérieur du contexte de la déclaration dans le fichier FigureGeometrique.h. class FigureGeometrique { public: FigureGeometrique(); ~ FigureGeometrique(); inline double surface() const { return surface; } inline const char * nom() const { return nom; } void surface(double s) { surface= s; } private: const char *nom; double surface; };
Les classes Exemple d’une classe…suite Les objets sont les instances de classes. FigureGeometriqueX; // déclaration X.surface(2.0); // écriture double d = X.surface(); // lecture cout << d << endl; // affichage
Les classes Classe ou struct Il existe une autre déclaration de classe : struct (accès public par défaut pour tous les membres et méthodes). class (accès privé par défaut pour tous les membres et méthodes). class Point { // Deux membres, les coordonnées // du point, inaccessibles de l’extérieur (private) int x, y; // Interface utilisateur, accessible de l’extérieur public: // Méthodes d’accès (en lecture) aux membres int abscisse() { return x; } int ordonnee() { return y; } // Méthodes de modification des membres void affect_x(int x) { x = x; } void affect_y(int y) { y = y; } }; struct Point { // les coordonnées du point // sont accessibles de l’extérieur int x, y; private: // Ce qui suit, jusqu’au prochain // "public:" n’est pas accessible ... };
Les classes Nous allons reprendre ce concept et l’illustrer à l'aide de l'exemple des tableaux. Lorsque on passe un tableau en paramètre à une fonction, celle-ci connaît l'adresse du début du tableau mais pas l'adresse de la fin. C'est pourquoi il est presque toujours nécessaire de fournir aussi la taille du tableau. Exemple: void copier(int *A, int n, int *B, int m) { int min = (n<m)?n:m; for (int i=0; i<min; i++) B[i]=A[i]; } int echanger(int *A, int n, int *B, int m) if (n!=m) return 0; int* tmp=new int[n]; copier(A, n, tmp, n); copier(B, m, A, n); copier(tmp, n, B, m); return 1;
Les classes On doit donc fournir quatre paramètres alors qu'on ne veut réellement Passer que deux objets. Ce problème peut être résolu en définissant notre propre objet tableau qui inclue aussi sa taille: la classe Tableau. class Tableau{ private: int nbelements; int *T; public: Tableau(int); //constructeur ~Tableau(); //destructeur int longueur() const; //accesseur int element(int) const; //accesseur void modifier (int, int); //modificateur };
int Tableau::element(int i) const Les classes Lors de l'implémentation des méthodes, il est donc nécessaire de préfixer le nom de la méthode implémentée du nom de la classe suivi de '::'. Par exemple, l'implémentation de la méthode element() de la classe Tableau se fait en spécifiant: int Tableau::element(int i) const Le constructeur est une méthode particulière qui porte le même nom que la classe et dont le but est d'initialiser les attributs lors de la création d'un objet. Les méthodes element et longueur() sont déclarées constantes à l'aide du mot clef const. Cela signifie que leur code n'affecte en aucune manière la valeur des attributs de l'objet cible.
Méthodes constantes Exemple 1 Exemple 2 Pour permettre au compilateur de mieux optimiser le code, le mot-clé const peut être ajouté à une méthode pour indiquer que celle-ci ne modifiera pas les membres de la classe. Celui-ci ndique qu'une méthode n'est pas intrusive. class Point { int x, y; public: // Interface utilisateur int abscisse() const { return x; } void affect_x(int x) { x = x; } }; Exemple 1 class Forme { … double get_x () const; }; void foo(const Forme& f) { f.get_x(); // OK } Exemple 2
Méthodes constantes Exemple 3 class Date { public: ... int reqJour () const; int reqMois () const; int reqAnnee () const; }; Exemple 3 Le mot-clé const se retrouve dans l’interface et dans l’implantation int Date::reqJour () const { return m_jour; } Le compilateur fera la vérification que les attributs de l’objet ne seront pas modifiés dans cette méthode.
Méthodes constantes Ne compile pas! class Date { public: void imprime(); private: int m_jour; int m_mois; int m_annee; }; void Date::imprime() { cout << m_jour << ":" << m_mois << ":" << m_annee; } Ne compile pas! le compilateur ne peut deviner que vous ne modifiez pas la date en l’imprimant... class Message { public: void imprime() const; private: string m_expediteur; Date m_dateRecu; }; void Message::imprime() const { cout << m_expediteur << endl << m_dateRecu.imprime(); } Département d’informatique et de génie logiciel 20
Méthodes constantes Outil permettant d’améliorer la qualité du code de façon très importante il force la classification. Il faut adhérer à cette façon de faire si on désire partager du code et utiliser des librairies qui ont adhéré à cette norme. La librairie standard adhère à cette norme.
Les classes Implémentation des fonctions membres (méthodes) de la classe Tableau Tableau::Tableau(int n) { T=new int[n]; nbelements=n; } Tableau::~Tableau() delete[] T; int Tableau::longueur() const return nbelements; int Tableau::element(int i) const { return T[i]; } void Tableau::modifier(int i, int a) T[i]=a;
Les classes On peut utiliser la classe Tableau de la façon suivante: void copier(Tableau& A, Tableau& B) { int min=(A.longueur()<B.longueur()) ? A.longueur():B.longueur(); for (int i=0; i<min; i++) B.modifier(i,A.element(i)); } int echanger(Tableau &A, Tableau &B) if (A.longueur() != B.longueur()) return 0;; Tableau tmp(A.longueur()); copier(A, tmp); copier(B, A); copier(tmp, B); return 1;
Les classes Dans la fonction echanger, la variable tmp est créée en appelant le constructeur avec le paramètre A.longueur(). Cela nous assure que tmp est un tableau de même taille que A. Seuls les membres dont la déclaration apparaît après le mot clef public peuvent être utilisés ailleurs dans le programme. Les variables nbelements et T ne sont accessibles que par les fonctions membres (privées ou publiques) de la classe Tableau. Par conséquent, le code suivant est illégale: Tableau t(10); // tableau de 10 éléments int n=t.nbelements; // instruction illégale
Instanciation d’une classe D’une manière générale, comme pour struct , le nom de la classe représente un nouveau type de donnée. On peut donc définir des variables, de ce nouveau type; (créer des objets ou des instances) : Tableau t1; // une instance simple (statique) Tableau *t2; // un pointeur (non initialisé) t2 = new Tableau(10); // création (dynamique) d'une instance
Les objets Ce sont des instances d’une classe, et de la même manière qu'une classe, un objet est caractérisé par: Ses attributs: Il s'agit des données caractérisant l'objet. Ce sont des variables stockant des informations d'état de l'objet Ses Méthodes (appelées parfois fonctions membres): Les méthodes d'un objet caractérisent son comportement, c'est-à-dire l'ensemble des actions (appelées opérations) que l'objet est à même de réaliser. Ces opérations permettent de faire réagir l'objet aux sollicitations extérieures (ou d'agir sur les autres objets). De plus, les opérations sont étroitement liées aux attributs, car leurs actions peuvent dépendre des valeurs des attributs, ou bien les modifier De plus, un objet a aussi une Identitée.
Utilisation d’un objet Après avoir créé une instance, un objet de façon statique ou dynamique, on peut accéder aux attributs et méthodes de la classe. Cet accès se fait comme pour les structures à l'aide de l'opérateur . ou ->. cout << t1.longeur() << endl; cout << t2->element(0) << endl;
Constructeur On observe que la fonction membre Tableau(int) ne retourne aucune valeur et qu'elle possède le même nom que la classe dont elle est membre. Une telle fonction est appelée constructeur, il est utilisés à la déclaration de l’objet pour le construire (affecter les membres, allouer la mémoire, etc.) à partir de différents paramètres. Les constructeur sont utilisés afin d'initialiser les objets lors de leur création. Si aucun constructeur n'est présent dans la définition d'une classe alors un constructeur par défaut est utilisé. Celui-ci ne fait qu'allouer l'espace nécessaire pour représenter les instances de la classe concernée.
Constructeur - On peut avoir plusieurs constructeur pour une classe - Tous portent le même nom que la classe n ‘ont pas de type de retour sont invoqués implicitement lors de la création d‘un objet déclaration d‘une variable variable temporaire (compilateur) usage de new ou new [] - diffèrent par leur signature(forcément :-) class X { public : X(); … };
Constructeur Si le programmeur ne veut pas de constructeurs, le compilateur le fait automatiquement pour lui, cependant, il faut les spécifier à vide lors de la déclaration de la classe. class X { public: X() {} X(const X&) {} X& operator=(const X&) {} } ;
Constructeur Il peut donc y avoir plusieurs constructeurs. class X { public: X() X(const X&); X& operator = (const X&); … }; Constructeur par défaut Constructeur de copie Surcharge de = X a; //défaut X b = a; // copie X c(a); //copie X d = X(a); //double copie d = a //affectation
Constructeur Pourquoi 3 constructeurs ? Défaut = initialisation (*)Copie = recopie à partie d’un modèle Affectation = doit aussi traite le cas « X = X; » Notation fonctionnelle à la « constructeur de copie » int i(0) est équivalent à int i = 0; i(2) i = 2; (*) Utile lors des appels de fonctions (un objet peut être en argument), pour le retour d’une fonction (un objet peut être retournée) et lors d’instanciation D’un objet à partir d’un autre : X x1(); X x2(x1);
Constructeur Exemple 1 int x, y; public: Point() : x(0), y(0) { } class Point { int x, y; public: Point() : x(0), y(0) { } Point(int x, int y) : x(x), y(y) { } Point(const Point& P) : x(P.x), y(P.y) { } int abscisse() { return x; } int ordonnee() ; }; Point X; // appel du constructeur vide Point M(3,4); // appel du constructeur sur les entiers Point P(M); // appel du constructeur de recopie physique Exemple 1
Constructeur Exemple 2 class Date { public: Date(); Date(int jour, int mois, int annee); Date(const std::string& txtDate); }; Exemple 2 Constructeurs dans l’ordre Date e; Date d(31,3,1997); Date f("31 mars 1997"); Date g(31, "mars", 1997); Date vDate[10]; Date(int,string,int); n’existe pas! Date() appelé 10 fois
Construction des sous-objets Utilisez une liste d’initialisation: construire tous les sous-objets avant le corps du constructeur parce que lorsqu'on y est rendu, ces objets sont déjà construit, i.e. on a déjà appelé leur constructeur par défaut. Date::Date(int jour, int mois, int annee) { // Les attributs ont déjà été initialisés par défaut. m_jour = jour; m_mois = mois; m_annee = annee; } // Version préférable Date::Date(int jour, int mois, int annee) : m_jour(jour), m_mois(mois), m_annee(annee) { } Liste d'initialisation
Construction des sous-objets Exemple 2 : Message::Message(const string& expediteur, const string& destinataire, const Date& dateRecu, const string& titre, const string& message) : m_expediteur(expediteur), m_destinataire(destinataire), m_dateRecu(dateRecu), m_titre(titre), m_message(message) { } Construire un objet avec des valeurs par défaut pour ensuite les changer avec les valeurs désirées. Un appel de plus est fait inutilement.
Destructeur La fonction membre ~Tableau() est appelée destructeur, elle est appelé automatiquement quand l’objet ne sera plus utilisé (fermeture d’accolade }). Au retour de la fonction echanger, la variable locale tmp sera éliminée par le destructeur et l'espace qu'elle utilise sera libéré. Si aucun destructeur n‘était défini alors le destructeur par défaut serait utilisé et seuls tmp.T et tmp.nbelements seraient éliminés. Le tableau créé par le constructeur serait conservé mais inaccessible jusqu‘à la fin de l'exécution du programme, ce qui résulterait en une perte d'espace inacceptable.
Destructeur class X { public : ~X(); … }; - porte le même nom que la classe avec ~ n ‘a pas de type de retour sont invoqués lors de la destruction d‘un objet fin de portée de la déclaration d‘une variable fin de la variable temporaire (compilateur) usage de delete ou delete [] n’a jamais d’argument, il n’y en as donc qu’un est utile lorsque l’objet est responsable de ressources allouées dynamiquement.
Il est inutile s'il n'y a pas de désallocation de ressource. Destructeur Si le programmeur ne définit pas le destructeur, le compilateur le fait automatiquement pour lui, cependant, et comme les constructeurs, il faut le spécifier à « vide » lors de la déclaration de la classe. class X { public: ~X() {} … } ; Il est inutile s'il n'y a pas de désallocation de ressource.
Destructeur Exemple class Tableau { int * d; public: Tableau(int s = 10) { d = new int[s]; } ~Tableau() { delete [] d; } }; { Tableau t; // ici de taille 10 par défaut ... } // Appel de tous les destructeurs des objets déclarés dans ce groupe
Constructeur / Destructeur class Date { public: Date(int j, int m, int a); ~Date(); private: int m_jour; int m_mois; int m_annee; }; Date::Date(int j, int m, int a) : m_jour(j), m_mois(m), m_annee(a) } Date::~Date() Date déclarée Constructeur appelé void main() { Date d(1, 1, 1965); } Initialisation seulement Sortie du «scope», destructeur appelé Rien à faire? => inutile d’implanter
Constructeur / Destructeur string déclarée, constructeur appelé class string { public: string(const char* strP); ~string(); private: char *m_strP; }; string::string (const char* strP) m_strP = new char[strlen(strP)+1]; strcpy (m_strP, strP); } string::~string() delete [] m_strP; void main() { string s("Bonjour"); } Sortie du «scope», destructeur appelé Initialisation et allocation de ressources. Désallocation de ressources
Constructeur et destructeur Les données membres d'une classe doivent être initialisées par une méthode d'initialisation. De même, après avoir fini d'utiliser un objet, il est bon de prévoir une méthode permettant de détruire l'objet (en libérant par exemple la mémoire). Le constructeur est une méthode qui porte le même nom que la classe. C’est dans le constructeur que les attributs d’un objet sont initialisées lors de sa création. Un constructeur ne peut pas spécifier de valeur ou type de retour. Le constructeur d’une classe est éxécuté automatiquement à chaque fois qu’une instance de la classe est crée. Que se passe t ’il si l’instance est passée par valeur comme paramètre à une fonction? Une classe peut avoir ou non un constructeur par défaut. Il s’agit d’un constructeur sans aucun paramètre.
Constructeur et destructeur Si aucun constructeur n’a été défini dans la classe, C++ en crée un automatiquement. C’est un constructeur par défaut, et il ne fait rien. Une classe a donc toujours un constructeur, mais pas forcément un constructeur par défaut ; en effet, si on ne définit que des constructeurs qui prennent des paramètres, C++ ne fournit pas le constructeur par défaut automatiquement. De la même façon que pour les constructeurs, le destructeur est une fonction membre spécifique de la classe qui est appelée implicitement à la destruction de l'objet. Ce destructeur est une fonction qui porte comme nom, le nom de la classe précédé du caractère ~(tilda) , qui ne retourne pas de valeur (pas même un void) et qui n'accepte aucun paramètre (le destructeur ne peut donc pas être surchargé).
Constructeur Le constructeur est donc une fonction membre spécifique de la classe qui est appelée implicitement à l'instanciation de l'objet. class Cercle { private: int x, y; int rayon; public : Cercle(int, int=0, int=0); // constructeur }; Cercle::Cercle(int r, int cx, int cy) { rayon = r; x = cx; y = cy; } Cercle ballon(20,10,10); Lorsqu’un constructeur par défaut n’existe pas, on ne peut déclarer une instance de classe sans préciser de paramètres. Par exemple, pour l’exemple suivant, class Autre { private: double d; public: Autre(double dd) { d = dd; } } l’appel: Autre a; est illégal.
Constructeur Si on veut créer un tableau d’instances de la classe sans donner de valeurs initiales, il faut définir un constructeur par défaut. Il est alors appelé pour tous les éléments du tableau : Avion A[10]; //10 appels du constructeur par défaut de Avion Important: bien que les constructeurs aussi peuvent avoir des arguments par défaut comme toute autre fonction: class Autre { private: double d; public: Autre(double dd = 0){ d = dd;} }; Autre a; // ok Autre tab[3]; //NON, car pas de constructeur par défaut
Constructeur Un constructeur ne peut pas être appelé autrement que lors d’une initialisation. Cependant, il peut l’être de différentes façons. Par exemple, s’il existe un constructeur qui n’admet qu’un seul paramètre, ou plusieurs mais tel que tous les arguments sauf le premier ont une valeur par défaut, on peut l’appeler en écrivant le signe égal suivi du paramètre: Autre au = 1.2; // appel de Autre::Autre(1.2) Cette écriture est équivalente à la forme classique : Autre au(1.2); En outre, il est possible d’initialiser des tableaux de cette façon : Autre atab[4] = { 1.2, 2, 0.7, -9.9 }; Par contre, dans ce cas, il faut absolument préciser toutes les valeurs initiales parce que il n'y a pas de constructeur par défaut dans notre implémentation de la classe Autre.
Constructeur Il est parfaitement possible de préciser une valeur par défaut à un argument de type classe, pourvu qu’on utilise un constructeur : void f(exemple ex = exemple() ); void f2(exemple ex = exemple(1, 1) ); void g(Autre au = 0); Dans le dernier cas, on a encore utilisé le changement de type automatique.
Destructeur Le destructeur est donc une méthode particulière qui porte le même nom que la classe précédé du symbol ‘~’. Le destructeur est la dernière méthode appelée par une instance lorque le bloc de code dans laquelle celle ci évoluait a terminé son exécution. Le destructeur est aussi appelé lors d ’un appel d ’opérateur delete sur l ’instance. C’est dans le destructeur que la mémoire qui avait été allouée à la construction et pendant la vie de l ’instance est rendue au système. Que se passe t ’il si deux blocs mémoires sont partagé par deux instances de la même classe?
Destructeur Un destructeur n’a aucun résultat, comme les constructeurs, et n’admet aucun argument ; de ce fait, il ne peut y avoir qu’un destructeur par classe. D’une façon générale, le destructeur doit tout « remettre en ordre dans ce que l’instance de classe peut avoir modifié. Outre la libération de la mémoire prise, il peut aussi avoir à fermer des fichiers ouverts, à détruire des éléments provisoires, etc. Le destructeur standard (fournit par défaut) ne fait rien.
new et delete avec constructeurs et destructeurs Un appel à l ’opérateur new (création d ’une instance de manière dynamique) provoquera un appel de constructeur. Autre* C = new Autre(...); class show{ private: int a; public: show(int); int get_a(){return a;} }; show::show(int i){ a = i; cout<< "Appel du constructeur: show(int i)"; } int foo(show a){ cout<<"Dans foo"<<endl; return a.get_a(); int main(){foo(1);return 1;}//foo(show(1));serait également légale Dans ces cas les constructeurs adéquats sont appelés à l’entrée de la fonction (et les destructeurs à la sortie).
new et delete avec constructeurs et destructeurs L’opérateur new réserve donc la place mémoire nécessaire à l’objet dans le heap ; il appelle aussi un constructeur. Inversement, delete appelle d’abord le destructeur, puis libère la place mémoire. Comme une classe peut avoir plusieurs constructeurs, on peut préciser quel constructeur est appelé au moment de l’appel de new. Il suffit pour cela d’écrire la liste des arguments derrière le nom de classe qui suit new. exemple *pe1 = new exemple(1, 2); // appel du constructeur 2 exemple *pe2 = new exemple; // appel du constructeur 1 classexmpl *c2 = new classexmpl(*c1); // constructeur de copie
new et delete avec constructeurs et destructeurs Lorsqu’aucun paramètre n’est précisé le constructeur par défaut est appelé ; s’il n’existe pas, une erreur de compilation se produit. Il est possible de créer un tableau avec new, mais dans ce cas c’est le constructeur par défaut qui est obligatoirement appelé ; il n’y a pas de moyen d’en préciser un autre (contrairement aux tableaux statiques qui peuvent être initialisés par un constructeur à argument unique). Pour ce qui est de l’instruction delete, il n’y a pas le choix : chaque classe ayant un seul destructeur (possiblement implicite), c’est celui-là qui est appelé avant de supprimer la place mémoire.
new et delete avec constructeurs et destructeurs Problème particulier aux tableaux: exemple *pex = new exemple[10]; ... delete pex; // incorrect Le compilateur, qui n’a aucun moyen de connaître la taille du tableau pointé par pex, n’appellera le destructeur que pour le premier élément, ce qui peut poser problème. Pour lui demander de tout détruire, il faut préciser explicitement avec delete, le nombre d’éléments à supprimer : delete[10] pex; Il faut se rappeler de ceci dans la déclaration du destructeur.
Ordre d’appel des constructeurs et destructeur Les constructeurs des données membres sont appelés avant le constructeur De la classe qui les englobe et dans l ‘ordre de déclaration des données. Les destructeurs des données membres sont appelés après le destructeur de la classe qui les englobe et dans l ‘ordre inverse de déclaration des données. class X{ public : X() { cout << ‘x’;} ~X() {cout << ‘X’;} class Y y; class Z z; }; class Z{ public : Z() { cout << ‘z’;} ~Z() {cout << ‘Z’;} }; int main() { X x; cout << endl; return 0; }; class Y{ public : Y() { cout << ‘y’;} ~Y() {cout << ‘Y’;} };
Imbrication de classes On peut déclarer une classe dans le corps d’une autre classe class X { public: X(); … private: class Y {...}; ... };
Composition de classes Les données membres d’une classe peuvent être des objets, c’est-à-dire des instances d’une autre classe. On parle alors de composition ou agrégation de classes class Y {...}; class X { public: X(); … private: Y y; ... };
Méthode friend Ce que l’on pourrait prendre pour une méthode déclarée friend est en fait une fonction classique : ce n’est absolument pas une méthode de la classe! Le mot clef friend permet simplement d’indiquer à la classe que cette fonction pourra accéder à toutes les données privées (membres ou méthodes). L’ambiguïté vient du fait que cette indication passe pour une déclaration. class Point { int x, y; public: friend void affiche(const Point& M) { cout << M.x << <<M.y; } void affiche_interne() {cout << x << << y; } }; ... Point M(3,4); affiche( M ) ; // affiche est une fonction globale classique M.affiche_interne(); // affiche_interne est une méthode
Classe friend De même, une classe peut être indiquée friend à l’intérieur d’une autre classe. Là encore cela ressemble à une déclaration précédée du mot-clé friend. Ainsi les méthodes de cette classe (implémentées ultérieurement) pourront manipuler les données privées de la classe dont elle est «amie». class B{ public: B() {a = new A();}; ~B() {}; private: A *a; void reinit_a() { a->i = 0; a->f= 0.0; }; class A{ public: friend class B; A() {i = 0; f = 0.0;}; ~A() {}; private: int i; float f; };
Fonctions membres statiques Méthode de classe ne nécessitant pas la présence d'un objet pour le traitement méthode statique. Comme une fonction mais associée à la classe. Ne touche pas aux attributs de la classe, à moins que ceux-ci ne soient aussi statiques (attribut commun à tous les objets de la classe).
Fonctions membres statiques Le mot-clé static permet donc de définir des membres ou des méthodes «globales». Sorte de variable globale « locale à la classe » initialisée à 0 par le compilateur si pas initialisée par le programmeur class A { friend class B; public: A() { i =0; f = 0.0; n++;}; ~A() {}; static int n; private : int i; float f; } int A:: n =-1;
Fonctions membres statiques Un membre static est identique pour toutes les instances de la classe. Toute modification par un objet est reconnue par tous les autres objets. class A { friend class B; public: A() { i =0; f = 0.0; n++;}; ~A() {}; static void reset() { n = 0;} private: int i; float f; static int n; }; Fonction/méthode «de classe » qui seule peut modifier les données membres statiques A:: reset(); N'a pas besoin d'un objet du type de la classe pour être appelée. Norme : Toujours appeler les méthodes statiques avec le «scope» de la classe et ce, même si vous avez un objet de ce type entre les mains.
Fonctions membres statiques Une méthode est normalement appelée au travers d’un objet. Une méthode static est appelée par la classe (pour manipuler les membres static). class Repere { static Point origine; public: Repere() {}; static void changement_origine(const Point& P) { origine = P; } Point _origine() { return origine; } }; Point Repere:: origine(0,0); int main() { Repere R1, R2; Point M(3,4); Repere::changement_origine( M ); cout << "Abscisse de l’origine" <<(R2._origine()).abscisse() << endl; return 0; }
Fonctions membres statiques Exemple 1 Pas de const pour une méthode statique --- INTERFACE --- class Date { public: void asgDate (int jour, int mois, int annee); static bool valideDate (int jour, int mois, int annee); static bool estBissextile (int annee); private: … }; --- IMPLÉMENTATION --- int Date::jourParMois[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; bool Date::valideDate(int jour, int mois, int annee) { return jour > 0 && mois > 0 && mois <= 12 && ((mois == 2 && jour == 29 && Date::estBissextile(annee)) || (j < jourParMois[mois - 1])); } bool Date::estBissextile(int annee) return (((annee % 4 == 0) && (annee % 100 != 0)) || ((annee % 4 == 0) && (annee % 100 == 0) && (annee % 400 == 0)) ); La méthode peut être déclarée Statique puisque aucun attribut non statique n'est utilisé
Fonctions membres statiques Exemple 2 class Etudiant { public: Etudiant () {m_nbInstances++;} ~Etudiant () {m_nbInstances--;} static int reqNbInstances () {return m_nbInstances;} private: static int m_nbInstances; }; int Etudiant::m_nbInstances=0; void main() Etudiant et1, et2, et3; cout << "Résultat :" << endl; cout << Etudiant::reqNbInstances() << endl; Etudiant et4, et5; } On peut utiliser un attribut statique dans un objet Une méthode statique ne peut qu'utiliser des attributs statiques. Attribut commun à tous les objets de la classe Résultat: 3 5 On sort du "scope": appel au destructeur Département d’informatique et de génie logiciel 65
Fonctions membres statiques Exercice Grâce aux fonctions et aux données statiques, écrire une classe qui compte en permanence le nombre d’objets « actifs », le nombre d’objets « créés » et le nombre d’objets détruits. Ajoutez une fonction d’affichage de ces trois nombres par redéfinition de l’opérateur de sortie sur un flot.
Méthodes optimisées Solution en C++ pour les méthodes simples: les déclarer inline. = Le compilateur élimine l’appel de fonction en le remplaçant par un accès direct à la donnée. Le code de la méthode doit être très simple et ne pas avoir d’effet de bord. Les méthodes inline font croître rapidement la dimension du code exécutable...
Méthodes optimisées Date.h Date.cpp class Date { public: void asgDate(int jour,int mois,int annee); int reqMois() const; }; inline int Date::reqMois() const return m_mois; } Date.h Méthode inline Méthode non inline #include "Date.h" void Date::asgDate(int jour, int mois, int annee) { m_jour = jour; m_mois = mois; m_annee = annee; } Date.cpp
Fonctions inline ou macro? Les programmeurs en C font souvent usage du préprocesseur avec #define pour implanter un fonction inline : #define carre(x) (x)*(x); int x = 5; int y = carre(++x); Résultat : 49 !??! inline int carre(int x) {return x*x;} int x = 5; int y = carre(++x); Résultat : 36. Département d’informatique et de génie logiciel 69
Méthodes optimisées L'usage de remplacement de symboles avec #define est source d'erreur, notamment pour les fonctions mathématiques. En C++, utiliser les fonctions ou les méthodes inline à la place. Les méthodes inline assurent la protection des données (encapsulation) assurent la validation de type d'une fonction régulière. améliorent la performance en évitant les appels de fonction.
Opérateurs Un opérateur est une fonction ou une méthode avec un appel particulier. class Complexe { float re, im; public: // une méthode peut être un opérateur ... Complexe& operator+= (Complexe x) { re += x.re; im += x.im; return *this; } }; // ... tout comme une fonction classique Complexe operator+ (Complexe x, Complexe y) { Complexe r = x; return r += y; void f(Complexe x, Complexe y, Complexe z) { Complexe r1 = x + y + z; // r1 = operator+(x, operator+(y,z) ) Complexe r2 = x; r2 += y; // r2.operator+=( y ) r2 += z; // r2.operator+=( z ) }
Surcharge des opérateurs Il est plus intuitif et plus clair d'additionner par exemple deux matrices en surchargeant l'opérateur d'addition et en écrivant : result = m0 + m1; que d'écrire : matrice_add(result, m0, m1); La plupart des opérateurs sont surchargeables. Il faut veiller à respecter l'esprit de l'opérateur. Lorsque l'on surcharge un opérateur, il n'est pas possible de : changer sa priorité changer son associativité changer sa pluralité (unaire, binaire, ternaire) créer de nouveaux opérateurs
Surcharge des opérateurs L'opérateur = est le seul à être prédéfini pour toutes les classes: a=b; Par défaut, il affecte aux variables de champs de l'objet a les valeurs des variables de b. Il est important de le surcharger dans certains cas. Il est possible de surcharger tous les opérateurs, sauf: . (sélection de membre) :: (résolutionde nom) .* ?: sizeof Les opérateurs suivant peuvent tous être redéfinis: () [] -> + - ++ -- ! ~ * & new new[] delete * / % + - << >> < <= > >= == != & ^ || && | = += -= *= = ,
Surcharge des opérateurs On ne peut changer les règles de précédence l’associativité le nombre d’opérandes On ne peut créer de nouveaux opérateurs: |x| i.e. valeur absolue y := x i.e. assignation à la Pascal y = x**2 i.e. x à la puissance 2. Pour surcharger un opérateur, la syntaxe est la suivante: <resultat> operator<type d'operateur>(<arguments>) {...}
Surcharge des opérateurs Exemple 1. Opérateur () L’opérateur parenthèses permet de donner un type à une fonction (de définir une fonction de classe). Ainsi les fonctions peuvent être manipulées plus facilement (comme des objets). class Incrementeur { int inc; public: Incrementeur(int i) : inc(i) {} int operator() (int i) { return i+inc; } }; int main() { Incrementeur plus_un(1), plus_deux(2); // Appel du constructeur int a = plus_deux(3); // a <-- 5, par l’appel de la méthode () }
Surcharge des opérateurs Exemple 2. Opérateurs + et * class point { private : float abscisse, ordonnee; public : point(); point(float,float); void afficher (); point operator + (point); float operator * (point); }; point point::operator +(point p){ point resultat; resultat.abscisse = abscisse + p.abscisse; resultat.ordonnee = ordonnee + p.ordonnee; return resultat; } float point::operator *(point p){ return (abscisse * p.abscisse + ordonnee * p.ordonnee); }
Surcharge des opérateurs p1 + p2; est alors équivalent à: p1.operator +(p2); C'est la méthode de l'instance de gauche qui est appelée. Donc, si on définit: point point::operator +(int x){ point resultat; resultat.abscisse = abscisse + x; resultat.ordonnee = ordonnee; return resultat; } L'appel suivant est légal: p1 + 3; Mais celui ci ne l'est pas: 3 + p1;
Surcharge des opérateurs Quand l'opérateur + (par exemple) est appelé, le compilateur génère un appel à la fonction operator+. Ainsi, l'instruction a = b + c; Est équivalente aux instructions : a = operator+(b, c); // fonction globale a = b.operator+(c); // fonction membre
Surcharge des opérateurs Exercice Utilisez la classe suivante pour écrire un programme qui crée un nouvel objet C par « addition » de deux objets A et B. class X { public: X& operator+(const X& arg){ X *x = new X(); x->n = n + arg.n; return *x; } int n; };
Surcharge des opérateurs Exemple 3. Opérateurs ++ et -- operator++ et operator−− sont ambigus : s’agit-il de la définition de l’opérateur suffixé ou préfixé ? Un paramètre artificiel int dans la définition permet de lever cette ambiguïté. class Pointeur { int * p; public: Pointeur& operator++ (); // préfixe Pointeur operator++ (int); // suffixe Pointeur& operator-- (); // préfixe Pointeur operator-- (int); // suffixe int operator*() { return *p; } // déréférencement };
Surcharge des opérateurs Exemple 3. Opérateurs ++ et -- Exercice Écrivez une classe contenant au moins une donnée entière et dans laquelle les deux incrémentations et les deux décrémentations sont définies mais font +2et -2 en préfixé et +3 et -3 en infixé.
Surcharge des opérateurs Exemple 4. Opérateur [] L'utilité des classes est de pouvoir définir des objets qui soient aussi facile à utiliser que les types de base, ce n'est pas le cas du type Tableau que nous avons vu précédemment. Par exemple, si T est une variable de type Tableau, on aimerait pouvoir Écrire T[i]=T[i]+1 plutôt que T.modifier(i,T.element(i)+1). Cela est possible en remplaçant les fonctions membres element et modifier par une modification(surcharge) de l'opérateur []. class Tableau{ private: int nbelements; int *T; public: Tableau(int); ~Tableau(); int longueur() const; int& operator[](int); };
Surcharge des opérateurs Tableau::Tableau(int n) { T=new int[n]; nbelements=n; } Tableau::~Tableau() delete[] T; int Tableau::longueur() const { return nbelements; } int& Tableau::operator[](int i) return T[i]; La valeur retournée par la fonction operator[] est de type int& plutôt que int. Cela est essentiel si on veut pouvoir écrire: Tableau tab(20); tab[0]=666; Pour affecter la valeur 666 à l'objet tab.T[0] il est nécessaire que l'appel de la fonction tab[0] retourne l'objet lui-même plutôt qu'une simple copie.
Surcharge des opérateurs Exemple 5. Opérateurs << et >> Il est possible de définir les opérateurs << et >> pour n’importe quel type et ainsi chaque objet peut être facilement manipulé par les flots de sorties (de type std::ostream) std::cout et std::cerr pour le standard et l’erreur respectivement et le flot d’entrée (de type std::istream) std::cin (Comme l’indique le préfixe std::, tous ces objets sont dans le namespace std). Ces opérateurs permettant d'écrire ou de lire toutes sortes de données, types de base et tous les types désirés. Ils offrent en effet l'avantage de pouvoir les utiliser avec les opérateurs surchargés >> et << respectivement.
Surcharge des opérateurs Exemple 5… suite Les opérateurs de flot << et >> peuvent aussi être redéfinis mais comme ils sont définis dans les classes « stream » ils ne peuvent être redéfinis dans une nouvelle classe… Solution = fonction/méthode« friend » class A { friend ostream& operator <<(ostream&, const A&); friend istream& operator >>(istream&, const A&); public: A() {i=0; f=0.0;}; ~A() {}; private: int i; float f; };
Surcharge des opérateurs Exemple 5… suite Ainsi, pour les nouvelles classes, on peut surcharger les fonctions friend : std::ostream& operator<<(std::ostream&, const Type& obj); std::istream& operator>>(std::istream&, Type& obj); Exemple 1 ostream& operator <<(ostream& o, const A& a) { o << "i = "<< a.i << " f= "<< a.f<< endl; return o; } istream& iperator >>(istream& i, const A& a) i >> a.i >> a.f; return i;
Surcharge des opérateurs Exemple 5… suite Exemple 2 class Complexe { float re, im; ... friend std::ostream& operator<< (std::ostream& o, const Complexe& c); friend std::istream& operator>> (std::istream& i, Complexe& c) ; }; std::ostream& operator<< (std::ostream& o, const Complexe& c) { return o << c.re << ‘‘+’’ << c.im << ‘‘i’’; } std::istream& operator>> (std::istream& i, Complexe& c) { return i >> c.re >> c.im;
Surcharge des opérateurs Exercice Écrivez un programme dans lequel une classe contenant au moins deux chaînes de caractères « nom » et « prénom » avec des données privées et des méthodes publiques permettra de lire, écrire, saisir et afficher ces données en redéfinissant les opérateurs de flots.
Surcharge des opérateurs Exemple 6. Opérateurs == et < class Date { public: bool operator== (const Date& obj) const; bool operator< (const Date& obj) const; friend std::ostream& operator<<(std::ostream& os, const Date& obj); private: long m_temps; }; bool Date::operator==(const Date& obj) const return m_temps == obj.m_temps; } bool Date::operator<(const Date& obj) const return m_temps < obj.m_temps;
Surcharge des opérateurs Exemple 6… suite std::ostream& operator<<(std::ostream& os, const Date& obj) { os << obj.m_temps; return os; } // --- Exemple d'utilisation int main() Date d1; Date d2(4,11,2001); bool bEgal = (d1 == d2); //--- d1.operator==(d2) bool bInferieur = (d1 < d2); //--- d1.operator<(d2) cout << d1 << endl; //--- operator<<(cout, d1) cout << d2 << endl; //--- operator<<(cout, d2) return 0;
Constructeur de copie Utilisation Le constructeur par copie est une méthode implicite dans toute classe. Cette méthode est appelée automatiquement dans les opérations suivantes : Création et initialisation d ’une nouvelle instance X I2=I1; X I2(I1); passage d ’un argument par valeur retour d ’une fonction return (I); // une copie de I est retournée
Constructeur de copie et opérateur d’affectation Notre version de la classe Tableau ne se comporte pas encore tout à fait Comme un type de base. Par exemple, considérez le fragment de code suivant: int a; int b=a; a=b; Dans cet exemple, deux variables sont créées en faisant appel à deux constructeurs différents. Dans le premier cas, le constructeur est de la forme int::int() alors que dans le second cas il est de la forme int::int(const int&). On aurait d'ailleurs pu remplacer la seconde ligne par int b(a); Ainsi le symbole '=' n'a pas la même signification dans la seconde et dans la troisième ligne où il désigne l'opérateur d'affectation.
Constructeur de copie et opérateur d’affectation On aimerait pouvoir écrire: Tableau a(100) ; Tableau b=a; a=b; Pour ce faire nous allons faire deux modifications. Nous allons surcharger l'opérateur = et nous allons définir un constructeur de copie, c'est-à-dire un constructeur de la forme: Tableau::Tableau(const Tableau&). class Tableau{ private: int nbelements; int *T; public: Tableau(int); Tableau(const Tableau&); ~Tableau() {delete[] T;}; int longueur() const {return nbelements;}; int& operator[](int) const; Tableau& operator=(const Tableau&); };
Constructeur de copie et opérateur d’affectation Tableau::Tableau(int n) { T=new int[n]; nbelements=n; } Tableau::Tableau(const Tableau &tab) T=new int[tab.nbelements]; (*this)=tab; int& Tableau::operator[](int i) const return T[i];
Constructeur de copie et opérateur d’affectation Tableau& Tableau::operator=(const Tableau &tab) { if (nbelements<tab.nbelements) delete[] T; T=new int[tab.nbelements]; } nbelements=tab.nbelements; for (int i=0; i<nbelements; i++) T[i]=tab.T[i]; return *this;
Constructeur de copie et opérateur d’affectation Les fonctions membres peuvent être définies en même temps que leur déclaration comme c'est le cas du destructeur et de la fonction longueur(). On retrouve le mot clef const immédiatement après la liste des paramètres de longueur et de operator[]. Il indique donc que ces fonctions ne peuvent Pas modifier l'objet pour lequel ces fonctions sont appelées. Ainsi dans l'exemple: Tableau A(20); int n=A[0]; le constructeur par copie et l'opérateur [] sont appelés pour le Tableau A qui ne doit pas être modifié par ces fonctions. Finalement, l'expression this est un mot clef du C++ désignant le pointeur de l'objet pour lequel une fonction membre est appelée.
Le pointeur this Dans une méthode, ou un constructeur, le mot-clé this est un pointeur sur l’objet considéré. Ainsi, lorsque l'on appelle une méthode d'une classe, celle-ci reçoit en plus de ses paramètres, un paramètre caché : le pointeur this. Ce pointeur (constant) permet à la méthode d'accéder à l'objet qui l'a appelé. Exemple Point& Point::copie(const Point& P) { if (this == &P) return *this; // ce n’est pas la peine de se copier soit même. // Attention, ce test vérifie l’égalité en adresse mémoire // et non l’égalité mathématique (celle des membres). x = P.x; this->y = P.y; }
Constructeur de copie et opérateur d’affectation Avec cette nouvelle version de la classe Tableau, il est maintenant beaucoup plus simple d‘écrire une fonction pour échanger le contenu de deux variables. void echanger(Tableau &A, Tableau &B) { Tableau tmp=A; A=B; B=tmp; } Le problème de l'affectation d'un grand tableau dans un petit est résolue par la surcharge que nous avons fait de l'opérateur =: un tableau trop petit est simplement remplacé par un autre de la dimension appropriée.
Constructeur de copie Il y a donc un type spécial de constructeur, le constructeur de copie: Si un constructeur de copie est spécifié, il sera appelé à chaque fois qu’un objet est rendu par une fonction, et à chaque fois qu’un objet est passé par valeur à une fonction. C’est grâce au constructeur de copie que le programmeur évite un certain nombre de problèmes d’allocation de mémoire lorsque celle ci est référée par des variables membres de l’objet.
Constructeur de copie Toute classe a nécessairement un constructeur de copie. Lorsqu’aucun n’est défini explicitement, le compilateur en crée un automatiquement, qui se contente de recopier champ par champ l’argument dans this. Le constructeur de copie n’est appelé (comme tout constructeur) que lors d’une initialisation. Donc, si on écrit : c2 = c1; Ce n’est pas le constructeur qui est appelé, mais l’opérateur d’affectation =, qui par défaut recopie les champs un à un ; il faut donc également le redéfinir. Cette remarque met en relief un fait essentiel qui est que lors des deux écritures : Exemple c2 = c1; // appel du constructeur de copie c2 = c1 // appel de l'opérateur d'affectation; L’opérateur d’affectation n’est appelé qu’une fois (la seconde), tandis que c’est le constructeur de copie qui est appelé la première fois. Par défaut les deux appels provoquent le même effet ; Ce qui n'est pas le cas dans des classes définies par un programmeur.
Constructeur de copie Exemple 1 student& student::operator=(const student& s) { if (&s == this) return *this; delete [] name; name = new char[strlen(s.name) + 1]; strcpy(name,s.name); }
Constructeur de copie Exemple 2 class classexmpl { public : classexmpl(); // constructeur par défaut classexmpl(int i); // un autre constructeur classexmpl(classexmpl& c); // constructeur de copie // autres méthodes... }; classexmpl c1; classexmpl c2 = c1; // appel du constructeur de copie // équivaut à classexmpl c2(c1);
Constructeur de copie Exemple 3 class Obj{ private: int* a; public: Obj(); ~Obj(); }; Obj::Obj(){ cout << " --> In Obj::Obj() " << endl; a = new int[10]; for(int i=0; i<10;i++){a[i]=i;} } Obj::~Obj(){ cout << " --> In Obj::~Obj() " << endl; delete[] a; a = (int*)0; void foo(Obj Inst){ cout << " --> In foo(Obj) " << endl;
Constructeur de copie a test a int main(){ Dans foo(obj) Obj test; foo(test); } Dans foo(obj) Copy de test a test a Allocation mémoire du tableau
Constructeur de copie test a a int main(){ Obj test; foo(test); } Dans foo(obj) Copy de test test a a Allocation mémoire du tableau de l'objet d'origine Allocation mémoire de la copie de l'objet d'origine Où les deux tableaux sont égales, valeurs par valeurs.
La déclaration ressemble à : Constructeur de copie La déclaration ressemble à : class Obj{ private: int* a; public: Obj(); Obj(const Obj&); ~Obj(); }; Obj::Obj(const Obj& cpy){ a = new int[10]; for(int i=0; i<10;i++) { a[i]=cpy[i]; }
Constructeur de copie et opérateur d’affectation La double copie, qu’est ce que c’est ? X d = X(a); //double copie 1/ création d ‘un objet temporaire qui est une copie de a => X(a) 2/ recopie de l ‘objet temporaire dans le nouvel objet d 3/ libération (appel au destructeur) de l ‘objet temporaire X(a)
Classe orthodoxe et canonique La classe minimale correcte doit contenir les méthodes suivantes: class X { public: X() {...}; X(const X&) {...}; X& operator=(const X&) {...}; ~X() {...}; } ; On parle alors de classes selon la forme de Coplien : une classe doit impérativement contenir un constructeur par défaut, un constructeur de recopie, un destructeur et une surcharge de l'opérateur d'affectation.
Un « cauchemar » avec les const!! class X { public: const int * const int_ptr(const int) const; }; const int * const X::int_ptr(const int i) const { static const int ii = 10; static const int * const ptr = ⅈ return ptr; } const X x; const int i = 20; cout << *(x.int_ptr(i)) << endl;
Opérateur de portée :: L’opérateur de portée « :: » permet d’Accéder aux données et aux fonctions statiques des classes, mais aussi aux variables globales. int X:: i; int Y:: i; int i; const int I = 2; class X { public: static int i; static const int I = 1; }; class Y { static const int I = 3; int main(){ int i; const int I =4; i = I; ::i = ::I; X::i = X::I; Y::i = Y::I; cout << i << ::i; cout << X::i << Y::i; cout << endl; return 0; }
Conversion de types Il y a une conversion implicite vers un objet d’une classe s’il existe un constructeur ayant pour paramètre la valeur à convertir. class X { public: X() { n = 0;}; X(int i) { n = i;}; private: int n; }; X x = 0; 0 est converti en un objet X(0) car le constructeur X(int) existe.
Conversion de types Il y a une conversion implicite vers un objet d’une classe s’il existe un constructeur ayant pour paramètre la valeur à convertir. class X { public: X() { n = 0;}; X(int i) { n = i;}; private: int n; }; X x (0); 0 est converti en un objet X(0) car le constructeur X(int) existe.
Conversion de types Il y a une conversion implicite vers un objet d’une classe s’il existe un constructeur ayant pour paramètre la valeur à convertir. class X { public: X() { n = 0;}; X(int i) { n = i;}; private: int n; }; X x (0); 0 est converti explicitement En en un objet X(0) avant recopie dans x.
Conversion de types C++ permet de définir ses propres fonctions de conversion vers d’autres types prédéfinis, ou d’autres classes. Syntaxe: operator<type> ( ) class X { public: X() { n = 0;}; X(int i) { n = i;}; operator int () const {return n;}; private: int n; }; X x = 10; int b = a; int c = int(a);
Conversion de types Exemple d’une string Il peut être intéressant de définir une conversion en const char* pour passer facilement en argument de beaucoup de librairies. Il s'agit ici de déterminer ce qui est préférable : conversion implicite ou conversion explicite?
Conversion de types Exemple d’une string class string { public: operator const char* () const; const char* c_str () const; private: char* m_strP; }; // --- Conversion implicite string::operator const char*() const return m_strP; } // --- Conversion explicite const char* string::c_str() const
Conversion de types Exemple d’une string // --- fopen (const char* fP) est une fonction du // --- C standard qui permet d'ouvrir un fichier. int main() { string nomFichier("fichier.txt"); // --- Conversion implicite, i.e. appel // --- de l'opérateur de conversion en const char* FILE *file1P = fopen(nomFichier); // --- Conversion explicite, i.e. appel // --- explicite de la méthode string::c_str(). FILE *file2P = fopen(nomFichier.c_str()); return 0; } Il faut éviter les conversions implicites :On ne contrôle pas ce qui ce passe et on risque d'avoir des surprises et des imprévus...
Conversion de types Il est possible comme on l’a déjà vu de convertir un type vers un autre en passant par un constructeur. Exemple d’une string : Un char* peut être convertis en string parce que la string a défini un constructeur avec un const char* : string (const char* strP); conversion implicite. La string pourrait avoir un constructeur acceptant un double. - Pour contrôler quand cette conversion se fera : ajouter le mot-clé explicit devant le constructeur : explicit string (double d); conversion explicite.
Conversion de types Exemple d’une string : class string { public: string(const char* strP); explicit string(double d); private: char* m_strP; }; string::string(const char* strP) m_strP = new char[strlen(strP)+1]; strcpy(m_strP, strP); } string::string(double d) ostringstream os; os << d; *this = os.str();
Conversion de types Exemple d’une string : void foo (string str); int main() { double d = 2.2; // --- Conversion implicite par le constructeur foo ("Bonjour la police"); // Error : no implicit conversion from double to string foo (d); // --- Ok: appel explicite pour la conversion foo (string(d)); return 0; }
Conversion de types Pour conclure, en C++ deux syntaxes sont préférées : float re = float(a) est un appel au constructeur de float, de paramètre un entier. float re = static_cast<float>(a) est un transtypage plus contraint, donc plus précis. En C++, il existe trois autres transtypages possibles : dynamic_cast qui permet de réaliser des conversions valides, vérifiées à l’exécution, mais que le compilateur n’arrive pas à faire (car il ne connaît pas encore le véritable type – un template, par exemple –). reinterpret_cast est utilement atroce (permet par exemple de transformer un int en une adresse de pointeur). const_cast permet d’ajouter ou de retirer un qualificatif const (le comportement ensuite peut-être non spécifié).
Conversion de types Exercice Écrivez un programme dans lequel une classe Entier représente les entiers et permet de mélanger les objets instance de la classe Entier avec les véritables entiers dans les expressions arithmétiques contenant les quatre opérations de base. Ajoutez aussi les opérateurs de flots << et >>.
Portée des identificateurs Il y en a 5! Portée globale Portée de fichier Portée de bloc Portée de classe Portée de l’espace de nommage
Portée des identificateurs Portée globale L’allocation de mémoire est faite statiquement à la compilation, initialisation à 0 (comme toutes les variables globales non initialisées en C). … int N; int a =N + 1; N est connu dans toute l’application, les autres Fichiers doivent la déclarer « extern » pour Pouvoir y accéder sans la dupliquer.
Portée des identificateurs Portée de fichier L’allocation de mémoire est faite statiquement à la compilation, initialisation à 0 (comme toutes les variables globales non initialisées en C). … static int N; int a =N + 1; N est connu dans le fichier seulement, les autres fichiers ne peuvent y accéder, même s’ils déclarent une autre variable N ou tente de la qualifier « extern ».
Portée des identificateurs Portée de bloc L’allocation de mémoire est faite dynamiquement dans la pile. { … int N; } N n’est connu que dans cette zone. C’est-à-dire après sa déclaration et jusqu’à La fermeture du bloc englobant.
Portée des identificateurs Portée de bloc L’allocation de mémoire est faite statiquement à la compilation, initialisation à 0 (comme toutes les variables globales non initialisées en C). { … static int N; } N n’est connu que dans cette zone. C’est-à-dire après sa déclaration et jusqu’à La fermeture du bloc englobant.
Portée des identificateurs Portée de classe Un identificateur de membre d’une classe ne peut être utilisé que: - Dans une fonction membre de la classe Après l’opérateur « . » appliqué à un objet instance de la classe Après l’opérateur « -> » appliqué à un pointeur sur un objet instance de la classe Après l’opérateur de résolution de portée appliqué à la classe
Portée des identificateurs Portée de classe Dans une fonction membre de la classe class X { public: int a() { return b() + c();} private: int b() {…}; int c() {…}; int d() { return 2* a();} };
Portée des identificateurs Portée de classe Après l’opérateur « . » appliqué à un objet instance de la classe class X { public: int a() { return b() + c();} private: int b() {…}; int c() {…}; int d() { return 2* a();} }; X x; cout << x.a() << endl;
Portée des identificateurs Portée de classe Après l’opérateur « ->. » appliqué à un pointeur sur un objet instance de la classe class X { public: int a() { return b() + c();} private: int b() {…}; int c() {…}; int d() { return 2* a();} }; X *x = new X(); cout << x->a() << endl;
Portée des identificateurs Portée de classe Après l’opérateur de résolution de portée appliqué à la classe class X { public: static void A() {…}; int a() { return b() + c();} private: int b() {…}; int c() {…}; int d() { return 2* a();} }; X:: A();
Portée des identificateurs Portée d’espace de nommage Pourquoi ? Lorsque l’on se linke avec une Bibliothèque on réutilise les idfs Qui y sont définis. On ne peut pas Définir d’idf ayant le même nom Que l’un quelconque des idfs de la Bibliothèque dans son programme. Il y aurait une « double définition », Fatale pour le linker… namespace <idf> { <declarations> }
Portée des identificateurs Portée d’espace de nommage Comment ? Chaque fournisseur de bibliothèque Doit encapsuler ses déclarations dans Un espace de nommage. Les STL, les librairies standards du C++, Sont dans l’espace de nommage « std ». namespace <idf> { <declarations> }
Portée des identificateurs Portée d’espace de nommage Espace de nommage « anonyme » est équivalent à l’usage de « static » mais sans avoir besoin de « static » devant les déclarations. namespace { <declarations> } En effet, le standard veut que tous les symboles déclarés dans un espace de Nommage anonyme aient comme portée le fichier seulement.
Portée des identificateurs Portée d’espace de nommage Pour faire référence à un idf d’un espace de nommage : deux situation. Qualification <nom de l’espace>:: idf « using » using namespace <nom de l’espace>
Portée des identificateurs Portée d’espace de nommage Il est possible d’imbriquer des espaces de nommage. Il est également possible de créer des alias. namespace toto { <declarations> namespace tutu {…} } namespace titi = toto::tutu;
Constantes non constantes!! Ce n’est pas une blague de mauvais goût! Ce sont des membres dits « mutables » Mutable n’est pas compatible avec const et static Mutable signifie « ne sera jamais constant » Permet au programmeur de changer la valeur d’une donnée membre même Si l’objet dans lequel elle se trouve est déclaré comme constant! class X { public: X(int a =4) {i = a;}; int lireI() const { return i++;}; private: mutable int i; }; const X x; cout << x.lireI() << endl;
Le traitement des exceptions Le C++ contient un mécanisme très utile pour traiter les erreurs et autres exceptions. Lorsqu'une erreur est détectée, il est possible de transmettre (throw) un objet à une partie du code qui reçoit (catch) l'objet et traite l'erreur. Considérons, par exemple, la fonction operator[] de la classe Tableau. Il serait prudent de vérifier la valeur de l'opérande afin de s'assurer qu'elle corresponde bien à un indice valide du tableau. Si tel n'est pas le cas, que doit faire la fonction? Elle ne dispose pas de l'information nécessaire pour traiter elle-même l'erreur et ne peut pas signaler l'erreur par une valeur de retour ou un paramètre. Pour régler ce problème nous allons ajouter un membre publique et modifier la fonction operator[]. C’est une solution parmi tant d’autres, nous reviendrons plus en détails sur la gestion des exceptions dans un autre Chapitre dédié uniquement sur ce concept.
Le traitement des exceptions template<class elem> class Tableau{ elem nbelements; elem *T; public: struct erreur_indice { int indice; erreur_indice(int i){indice=i;}; }; Tableau(int); Tableau(const Tableau&); ~Tableau() {delete[] T;}; int longueur() const {return nbelements;}; elem& operator[](int) const; Tableau& operator=(const Tableau&);
Le traitement des exceptions template<class elem> elem& Tableau<elem>::operator[](const int i) const { if (i<0 || i>=nbelements) throw erreur_indice(i); return T[i]; } Nous avons ajouté à Tableau la définition d'une structure afin de définir un type d'erreur correspondant aux dépassement des limites. En C++ une structure est exactement comme une classe sauf que par défaut les membres sont publiques. Si operator[] détecte une erreur, il utilisera le constructeur de erreur_indice pour créer un objet qu'il transmettra via l'instruction throw. L'instruction throw se comporte un peu comme l'instruction return sauf qu'elle cherche dans la pile des appels de fonctions celle qui demande à recevoir l'objet.
Le traitement des exceptions template<class elem> void initialise(Tableau<elem>& t, const elem& E) { for (int i=0; i<=t.longueur(); i++) t[i]=E; } int main() Tableau<int> T(100); try initialise(T,0); catch(Tableau<int>::erreur_indice e) cerr<<"Erreur d'indice: <<e.indice<<endl; return 0; Exemple
Le traitement des exceptions La fonction main() utilise les mots clefs try et catch pour indiquer qu'elle appelle la fonction initialise et qu'elle désire traiter les erreurs de type Tableau<int>::erreur_indice. Au dernier tour de boucle de initialise, l'indice i aura dépassé les limites du tableau et Tableau<int>::operator[] transmettra un objet de type Tableau<int>::erreur_indice qui sera reçu par la fonction main().
Le traitement des exceptions Plusieurs catch peuvent apparaître après un try afin de recevoir et traiter plusieurs types d'erreurs. L'expression catch(...) permet de recevoir tous les types d'erreurs non capturés par les catch précédents: try{ instructions } catch(type 1){ catch(type 2){ catch(...){
Le traitement des exceptions L’importance de ce sujet nous fera revenir plus longuement dans un autre Chapitre dédié uniquement sur ce concept.