C++_CHAPITRE2_MIASI_2014_2015

Embed Size (px)

Citation preview

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    1/24

    NOTION DE CLASSE

    Le premier des paradigmes de la programmation objet est l'encapsulation.  C'est la possibilité de nemontrer de l'objet que ce qui est nécessaire à son utilisation. D'où,

    simplification de l'utilisation des objetsmeilleure robustesse du programme

    simplification de la maintenance

    Elle a pour conséquence de

    rapprocher les données et leur traitement : c'est l'objet qui sait le mieux comment gérer unedemande ,masquer l'implémentation.

    Sont fournis à l'utilisateur :

    des mécanismes de construction ( destruction ) d'objets,

    des méthodes d'accès et de modification des données encapsulées.

    Une classe est la description d une famille d'objets ayant même structure et même comportement.

    Une classe regroupe un ensemble d'attributs ou membres répartis en

    un ensemble de données,un ensemble de fonctions appelées méthodes.

    Un objet est élément de la classe. C'est une instance de la classe. Il est obtenu par instanciation.

    La classe permet de produire autant d'exemplaires d'objets que nécessaire.Les valeurs des données membres peuvent différer d'une instance à l'autre ( sauf pour desdonnées statiques de classe ).Les méthodes sont les mêmes pour toutes les instances d'une classe. On distingue entre méthodesd'objet ( d'instance ) et méthodes de classe.

    La déclaration d'une classe donne la nature des membres ( type; signature ), et les droits d'accès :public, protected, private ( défaut ).

    La définition d'une classe fournit la définition des méthodes.

    L'encapsulation se réalise en donnant à l'utilisateur 

    un fichier en-tête contenant la déclaration de la classe,un module objet contenant la version compilée du fichier contenant la définition de la classe.

     

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    2/24

    Quelques explications s'imposent :

    La déclaration de la classe commence par le mot clef class et est encadrée par une paired'accolades. L'accolade finale doit impérativement  être suivie d'un point virgule. Pour l'instant, il vous suffit de savoir que les membres déclarés après le mot clef public formentl'interface de la classe alors que ceux suivant private sont invisibles de l'utilisateur.Les membres précédés du mot clef static sont partagés par tous les objets de la classe. De chacunil n'existe qu'un seul exemplaire par classe, quel que soit le nombre d'objets de la classe.L'ordre de déclaration des méthodes et des attributs est laissé au libre arbitre du programmeur.Toutefois, il est d'usage de déclarer les attributs à la fin car la partie la plus importante d'uneclasse du point de vue de l'utilisateur est son interface. En conséquence, il est important de placer les méthodes au début.

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    3/24

    La déclaration des attributs est semblable à la déclaration d'une variable alors que celle d'uneméthode ressemble à s'y méprendre au prototype d'une fonction ! Néanmoins, il ne faut pasoublier que chaque méthode possède un argument caché : l'objet sur lequel elle est invoquée.Lors de l'implémentation des méthodes, il est nécessaire de préfixer le nom de la méthodeimplémentée du nom de la classe suivi de '::'.Par exemple, l'implémentation de la méthode deplacerVers de la classe Point se fait enspécifiant Point::deplacerVersLe constructeur est une méthode particulière qui porte le même nom que la classe et dont le butest d'initialiser les attributs lors de la création d'un objet.Les méthodes x et y 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.

    Les mots clefs public et private sont des modificateurs d'accès. Leur portée s'étend jusqu'au prochain modificateur. Le modificateur par défaut est private. Les membres déclarés private nesont visibles que par les méthodes de la classe elle même. En revanche, tout membre déclaré publicaura une visibilité universelle. Le respect du principe d'encapsulation impose donc que :

    Tout attribut d'instance ou de classe sera déclaré private (abstraction de données),

    Toute méthode non nécessaire à l'utilisateur sera déclarée private,Toute méthode que vous souhaitez rendre disponible à l'utilisateur, et qui définit donc l'interfacede la classe est à déclarer public.

    Les membres de classe sont déclarés avec le mot clef static. Contrairement aux modificateurs d'accès,static n'a d'effet que sur une seule ligne de déclaration.

     Particularité du C++ : la déclaration d'une donnée membre static ne lui affecte pas d'adresse, il faudradéfinir cette donnée membre par ailleurs dans le fichier de définitions. C'est précisément le rôle de ladéfinition int Point::NbPoints_=0 qui définit la donnée membre de classe NbPoints et luiaffecte la valeur initiale 0.

     Rappelons que si les méthodes d'instance peuvent très bien utiliser les attributs de classe en plus desattributs d'instance, les méthodes de classe elles ne peuvent qu'utiliser les attributs de classe. En effet,sur les attributs de quelle instance devrait on appliquer les actions ?

    A l'exception de classes fortement reliées, il est conseillé de placer une seule classe par fichier source.En fait, une classe a même besoin de 2 fichiers sources : un fichier header ( .h) où l'on place ladéclaration de la classe et un fichier de définition des méthodes et des variables de classe ( .cpp).

    Afin d'éviter les inclusions redondantes, on place des balises de compilation avec des #ifndef comme

    dans l'exemple suivant :

    #ifndef  NOM_DE_LA_CLASSE_H#define NOM_DE_LA_CLASSE_H

    // Placer ici les inclusions et les déclarations externes nécessaires

    // Placer ici la déclaration de la classe

    class NomDeLaClasse

    { .... ....}; // Ne pas oublier ce ";"

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    4/24

    // Sauver sous le nom : nom_de_la_classe.h#endif 

    Le fichier de définition sera :

    #include "nom_de_la_classe.h"

    // autres inclusions nécessaires// Définitions des variables de classe

    // Définitions des méthodes// Sauver sous le nom : nom_de_la_classe.cpp

     

    Vous aurez remarqué un opérateur particulier "::" appelé « opérateur de résolution de portée ». Il sert à

    désigner à quelle classe appartient une méthode ou un attribut.Cet opérateur est nécessaire à la définition des méthodes car le langage C++ ne vous oblige pas àrespecter la règle de séparation de l'implémentation dans différents fichiers. Aussi, la définition dechaque méthode doit être précédée du nom de la classe à laquelle elle se rattache et de l'opérateur derésolution de portée.Par exemple, supposons que dans le même fichier, vous vouliez implémenter la méthode met1 de laclasse A  ainsi que la méthode met2 de la classe B, alors, vous auriez à spécifier :

    typeRetour A::met1(paramètres){  // code d'implémentation}

    typeRetour B::met2(paramètres){  // code d'implémentation}

    Pointeur this Nous allons à présent voir comment les fonctions membres, qui appartiennent à la classe, peuventaccéder aux données d'un objet, qui est une instance de cette classe. Ceci est indispensable pour biencomprendre les paragraphes suivants.

    À chaque appel d'une fonction membre, le compilateur passe en paramètre un pointeur sur les donnéesde l'objet implicitement. Ce paramètre est le premier paramètre de la fonction. Ce mécanisme estcomplètement invisible au programmeur, et nous ne nous attarderons pas dessus.

    En revanche, il faut savoir que le pointeur sur l'objet est accessible à l'intérieur de la fonction membre.

    Il porte le nom « this ». Par conséquent, *this représente l'objet lui-même. Nous verrons uneutilisation de this dans le chapitre 4 (redéfinition des opérateurs).

    this est un pointeur constant, c'est à dire qu'on ne peut pas le modifier (il est donc impossible de fairedes opérations arithmétiques dessus). Ceci est tout à fait normal, puisque le faire reviendrait à sortir de

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    5/24

    l'objet en cours (celui pour lequel la méthode en cours d'exécution travaille).

    Il est possible de transformer ce pointeur constant en un pointeur constant sur des données constantes pour chaque fonction membre. Le pointeur ne peut toujours pas être modifié, et les données de l'objetne peuvent pas être modifiées non plus. L'objet est donc considéré par la fonction membre concernéecomme un objet constant. Ceci revient à dire que la fonction membre s'interdit la modification desdonnées de l'objet. On parvient à ce résultat en ajoutant le mot-clé const à la suite de l'en-tête de lafonction membre. Par exemple :

    class Entier {  int i;

     public :  int lit(void) const;};

    int Entier::lit(void) const{  return i;}

    Dans la fonction membre lit, il est impossible de modifier l'objet. On ne peut donc accéder qu'enlecture seule à i.

    Il est à noter qu'une méthode qui n'est pas déclarée comme étant const modifie à priori les données del'objet sur lequel elle travaille. Si elle est appelée sur un objet déclaré const, une erreur de compilationse produit donc. Ce comportement est normal. Si la méthode incriminée ne modifie pas réellementl'objet, on devra donc toujours la déclarer const pour pouvoir laisser le choix de déclarer const ou nonun objet.

    Note: Le mot-clé const n'intervient pas dans la signature des fonctions en général lorsqu'ils'applique aux paramètres (tout paramètre déclaré const perd sa qualification dans lasignature). En revanche, il intervient dans la signature d'une fonction membre quand ils'applique à cette fonction (ou, plus précisément, à l'objet pointé par this). Il est donc possible de déclarer deux fonctions membres acceptant les mêmes paramètres, dont uneseule est const. Lors de l'appel, la détermination de la fonction à utiliser dépendra de lanature de l'objet sur lequel elle doit s'appliquer. Si l'objet est const, la méthode appelée seracelle qui est const.

    Les méthodes inline

    Il existe 2 manières de spécifier le code des méthodes. La première consiste à séparer la déclaration dela méthode de son implémentation. C'est celle que nous avons illustrée dans l'exemple précédent. Cesystème présente néanmoins un inconvénient :

    "Utiliser un appel de méthode pour récupérer la valeur d'un attribut c'est une perte de tempslamentable"

    Les méthodes inline ont été inventées pour cela ! La principale caractéristique des méthodes inlineest leur faculté à déveloper leur code en lieu et place d'un appel à la manière d'une macro. Voussaisissez tout de suite l'avantage : vous gagnez le temps nécessaire à l'appel d'une fonction, ainsi l'accèsà un attribut ne coûte plus rien !

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    6/24

    Il y a néanmoins un inconvénient, comme tout appel est remplacé par le développement du code de laméthode, il en résulte un accroissement de la taille du code cible. Aussi, ces méthodes doivent êtrelimitées à quelques instructions sous peine d'accroître quasi indéfininiment la taille de l'exécutable. Enoutre, certains compilateurs refusent de mettre en ligne les méthodes qui contiennent des boucles.

    Comment une méthode devient elle inline ? il existe 2 manières de le faire :

    1. Décrire l'implémentation de la méthode au niveau de sa déclaration. C'est la manière la plus

    simple mais elle présente un sérieux défaut : ne pas séparer l'implémentation de la déclaration cequi est contraire au principe de dissimulation.2. Par opposition aux méthodes décrites dans la déclaration d'une classe, on appelle méthode

    déportée une méthode dont le code n'est pas transcrit dans la déclaration de sa classe mais endehors. Notons qu'il est néanmoins possible de faire développer inline une méthode déportée. Ilfaut alors préfixer sa déclaration ainsi que sa définition du mot clef inline. En outre, il faut donner son implémentation dans le fichier de déclaration à la suite de la déclaration de la classe. En effet,si vous souhaitez que le compilateur puisse développer le code de la méthode sur le lieu de l'appel,il faut qu'il connaisse sa taille !

    Par exemple, nous allons réécrire la classe Point en utilisant des méthodes inline. Nous allons mettreinlineles deux méthodes d'accès aux attributs ainsi que le constructeur. Afin d'exploiter toutes les possibilités, les méthodes d'accès aux attributs seront placées inline dans la déclaration alors que leconstructeur sera mis inline externe. En respectant la structure en 2 fichiers, nous obtenons finalement :

    #ifndef POINT_H#define POINT_Hclass Point{  public:

      inline Point(int absc, int ordo); // Constructeur déclaré inline  // le compilateur va rechercher le code plus loin

      int x(void) const // Déclaration et définition inline  {  return abscisse_;  }

      int y(void) const // Déclaration et définition inline  {

      return ordonnee_;  }

      void deplacerDe(int incX, int incY); // Juste la déclaration, méthode NON inline  void deplacerVers(int dX, int dY); // Juste la déclaration, méthode NON inline

      static int NbPoints(void);  private:  int abscisse_;

      int ordonnee_; static int NbPoints_;

    };

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    7/24

    // Definition inline deportee du constructeur 

    inline Point::Point(int absc, int ordo){  abscisse_=absc;

      ordonnee_=ordo;}

    #endif 

    #include "Point.h"

    // Definition de l'attribut statique NbPoints_ // notez que l'on ne repete pas le mot clef static

    // la valeur d'initialisation est OBLIGATOIRE

    int Point::NbPoints_=0;

    void Point::deplacerDe(int incX, int incY){  abscisse_+=incX;  ordonnee_+=incY;

    }

    void Point::deplacerVers(int dX, int dY){  abscisse_=dX;  ordonnee_=dY;}

    Cycle de vie des objets

    La vie d'un objet se résume à trois épisodes fondamentaux : sa création, son utilisation puis sadestruction. La création et la destruction font appel à des méthodes particulières : respectivement lesconstructeurs et le destructeur.

    Création d'objets : les constructeurs

    La création d'objets repose sur des méthodes spéciales nommées constructeurs. Celles-ci ont pour butde réaliser la partie « instanciation » de la création d'un objet, c'est à dire, le positionnement de lavaleur initiale des variables d'instance.

    Les constructeurs sont faciles à repérer : ils portent le même nom que la classe.

    Par exemple, si l'on reprend la classe Point, son constructeur est facile à repérer :

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    8/24

    Point::Point(int absc=0, int ordo=0);

    Cette méthode n'a qu'un seul but : initialiser les attributs abscisse_ et ordonnee_  du nouvel objetcréé et tenir à jour le nombre d'instances disponibles dans le système. Notez au passage qu'elle utilise la possibilité offerte par le C++ de proposer des valeurs par défaut aux arguments des fonctions quis'étend tout naturellement aux méthodes. Examinons son code de plus près :

    Point::Point(int absc=0, int ordo=0){  abscisse_=absc;  ordonnee_=ordo;  NbPoints_++;}

    Ce code peut néanmoins être utilisé en utilisant une liste d'initialisation comme dans l'exemplesuivant :

    Point::Point(int absc=0, int ordo=0) :  abscisse_(absc),  ordonnee_(ordo){  NbPoints_++;}

    Vous notez ici que le corps de la méthode est limité à l'incrémentation du compteur vide . La listed'initialisation, où sont initialisés abscisse_  et ordonnee_ commence après le signe ":"

    La syntaxe de cette liste est :: attribut(valeur_initialisation) {, attribut(valeur_initialisation)}

    Ainsi, si l'on se réfère au code précédent, les attributs abscisse_  et ordonnee_  sont respectivementinitialisés avec les valeurs des paramètres absc et ordo. N'allez pas croire que vous pouvezuniquement utiliser des paramètres directs dans ce genre de construction, valeur_initialisation peut être n' importe quelle expression C++ valide comprenant des opérateurs, des appels de fonctions oude méthodes sur tout autre objet que celui en cours de création.Par exemple, si nous avions eu un attribut codant la distance du point à l'origine et nommé distance_ l'initialisation aurait pu être :

    Point::Point(int x=0, int y=0) :abscisse_(x),ordonnee_(y),

      distance_(sqrt(x*x)+(y*y)){  NbPoints_++;};

     

    Pour les attributs atomiques, vous avez le choix entre l'utilisation d'affectations dans le corps duconstructeur et la liste d'initialisation. Il est même possible de mixer les deux !

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    9/24

    class Point{

    // Code inutile ici

    Point(int x=0, int y=0) : abscisse_(x){

      ordonnee_ = y;  NbPoints_++;};

    Deux petites choses à retenir impérativement concernant les listes d'initialisation :

    1. Les attributs doivent apparaître dans leur ordre de déclaration. Par exemple si la classe déclare :

    class Chose{  // Code supprime

     private:  int a;  double b;};

    Alors, le code :

    class Chose{  public:  Chose (double valB, double valA) : b(valB), a (valA)  {  // code initialisation  }};

    est illégal car vous essayez d'initialiser b avant a, alors que a est déclaré avant b. Certains

    compilateurs effectuent d'eux mêmes la correction en émettant un message d'avertissement.Certains autres se contentent de générer du code faux sans avertissement. Il faut donc dans tousles cas faire attention à respecter l'ordre de déclaration !!! Le code correct, dans ce cas, est lesuivant :

    class Chose{  public:  Chose (double valB, double valA) : a (valA), b(valB)  {

      // code initialisation  }

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    10/24

    };

    2. Les initialisations réalisées dans la liste d'initialisation sont effectuées avant les instructions situéesdans le code du corps du constructeur prenez donc gare à ne jamais placer de code nécessitantl'exécution du corps du constructeur au sein de la liste d'initialisation.

    Instanciation des objets

    La création d'objets (opération d'instanciation) se fait en appelant le constructeur. La syntaxe diffèreselon la classe d'allocation de l'objet.

    Les classes d'allocations

    Il y a trois classes d'allocation :

    Allocation statique

    Elle concerne les objets placés en variable globale, c'est à dire à l'écart de toute fonction ouméthode.

    On recense également dans cette catégorie les objets explicitement mis en allocation statique dansune méthode ou fonction à l'aide du modificateur d'allocation static. Rappelons que dans cecas, ils gardent leur valeur d'un appel de la fonction / méthode sur l'autre.Créés dès le début du programme, ils sont détruits automatiquement à la fin de l'exécution decelui-ci sans que vous ayez à vous en occuper. Il n'existe qu'un seul cas où ces objets ne sont pasdétruits : l'arrêt du programme par la fonction abort.

    Allocation automatique

    Tout objet créé sur la pile est dit à allocation automatique. Cela concerne donc les variablestemporaires dans une fonction / méthode ainsi que les objets renvoyés par une fonction / méthodequi sont momentanément stockés sur la pile.

    Un objet automatique est créé à chaque lancement de la fonction / méthode à l'intérieur delaquelle il est déclaré et détruit automatiquement sans que vous ayez à vous en soucier dès leretour à l'appelant.

    Allocation dynamique

    Il s'agit des objets créés sur le tas et auxquels vous ne pouvez accéder qu'au travers d'un pointeur.Ils sont créés par un appel à l'opérateur new et doivent être détruits explicitement  par un appel àl'opérateur delete.

    Création d'instances simples

    La création d'une instance automatique ou statique est simple et répond à la syntaxe suivante :

      IdentificateurClasse identificateurObjet(parametres d'un constructeur)

    Dans le cas d'une instance dynamique, il faut commencer par déclarer un pointeur, puis appeler newsuivi du nom du constructeur avec ses paramètres. Ce qui peut se faire sur une ou plusieurs lignes

    IdentificateurClasse *identificateurPointeurObjet;

      identificateurPointeurObjet=new IdentificateurClasse(parametres d'un constructeur)

    Point P1; // Objet statique

    int main(int, char **){

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    11/24

      Point p1; // Utilise le constructeur Point::Point(int, int)// avec les valeurs par defaut des arguments

      Point p2(10,20); // Crée un nouveau point avec x=10 et y=20  Point *p3; // Déclaration d'un pointeur sur un objet de type Point  p3=new Point(20,30); // Instanciation de l'objet dynamique avec arguments explicites

      p4=new Point; // instanciation de l'objet dynamique avec arguments par défaut

      delete p3; // On n'oublie pas de rendre la mémoire !  delete p4; // Idem => voir section suivante !  return 0;}

    Appel de méthodes

    La syntaxe d'appel d'une méthode se différencie selon 2 modalités : le type de la méthode (instance ou

    méthode de classe) et, dans le cas d'une méthode d'instance, la classe d'allocation de l'objet cible.

    Appel de méthodes d'instance

    Le code se différencie selon que vous utilisez un objet d'allocation dynamique ou non. Pour les objets àclasse d'allocation statique ou automatique, la syntaxe d'appel est la suivante :

    objet.methodeInstance(paramètres);

    Par exemple, appliquons les opérations suivantes à l'objet de classe Point nommé p1 tel qu'il a étéinstancié au paragraphe précédent :

    1. afficher son abscisse2. le déplacer de 10 unités sur x et 20 unités sur y relativement à sa position actuelle3. afficher son ordonnée

    nous obtenons le code suivant :

    cout

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    12/24

    Ainsi, si vous souhaitez afficher le nombre d'objets graphiques dans votre système en invoquant laméthode NbPoints, vous devrez utiliser :

    cout

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    13/24

      }  void metNonConst(double h)  {  // Code non moins utile  }};

    void Chose::metConstDep(int i, double d) const{  // Code vachement important lui aussi}

    Utilisons maintenant ce beau code !

    int main(int, char **)

    {  const Chose OBJET_CONSTANT(params d'initialisation); // Objet constant !

      OBJET_CONSTANT.metConstDep(1,2.5);// Ok, methode constante sur objet constant

      OBJET_CONSTANT.metNonConst(35.5);// NON ! metNonConst est une méthode// non constante qui ne peut en aucun cas être appliquée sur// un objet constant

      OBJET_CONSTANT.metConstInk(3,"coucou");// Ok, methode constante sur objet constant

      return 0;}

    Très important : deux méthodes peuvent ne de différencier que par leur caractère constant ! nousreviendrons la dessus lors de la surcharge des opérateurs.

    Mort des objets

     Nous avons vu que la construction d'un objet s'effectue à l'aide d'une méthode spéciale : leconstructeur. La mort des objets s'accompagne de l'appel d'une autre méthode spéciale : ledestructeur. Notons immédiatement que si un constructeur peut être surchargé, le desctructeur lui ne peut pas l'être. En effet, son appel étant automatique, le système ne saurait pas quel destructeur appeler.

    La syntaxe du destructeur est simple. Si les constructeurs de la classe Classe s'appellentClasse::Classe , le destructeur lui est Classe::~Classe . A l'instar du constructeur, ledestructeur ne renvoie rien ; en outre, il ne prend pas de paramètre.

    Le but du destructeur est de procéder à toutes les opérations de nettoyage nécessaires à la destructioncorrecte d'un objet.

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    14/24

    Par exemple, si le constructeur ou une autre méthode réalise des allocations dynamiques, le destructeur veillera à rendre la mémoire afin d'éviter que celle-ci ne soit perdue. Autre cas intéressant, un objetutilise des ressources systèmes, par exemple des outils graphiques Windows, avant que l'objet ne soitdétruit, le destructeur rendra la ressource système.

    Il est important de noter que vous n'avez pas besoin de spécifier un destructeur si votre classe nenécessite pas de nettoyage spécifique. Si vous ne spécifiez pas explicitement un destructeur, undestructeur par défaut est créé automatiquement par le compilateur. Ce dernier est très simple : il ne fait

    rien !

    Le processus de destruction se passe en deux fois :

    1. Appel du destructeur.2.  Nettoyage de l'espace mémoire occupé par les données membres de l'objet.

    Par exemple, examinons le cas d'un objet possédant 2 attributs d'instance : un entier et un pointeur versun tableau d'entiers, l'entier spécifiant en fait la taille du tableau. Au cours de la construction de l'objet,nous allouons un tableau. Lors de la destruction, il faudra le détruire.

    class Tableau{  public:  Tableau(int laTailleDuTableau=5) : taille_(laTailleDuTableau)  {  if (taille_)  tab_=new int [taille_];  else  tab_=0;

      }

      ~Tableau(void)  {  delete [] tab_;  }  private:  int taille_;  int *tab_;

    };

     

    Lorsqu'un objet a les classes d'allocation statique (variable globale) ou automatique (variable locale dansune fonction/méthode), il est détruit automatiquement dès qu'il sort de portée, c'est à dire :

    A la fin du programme pour une variable globale.Dès que la fonction/méthode rend la main à l'appelant pour une variable locale.

    Il en est tout autre pour une variable dynamique, c'est à dire un objet créé dynamiquement à l'aide denew ou de new []. En effet, tout objet instancié dynamiquement doit être explicitement détruit :

    à un appel de new doit correspondre un appel de delete,un ensemble d'objets créés à l'aide de new [] doit être détruit par delete [].

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    15/24

    Remarque importante : il n'est pas nécessaire de spécifier la taille du tableau lors de sa destructionavec delete []. Seuls quelques compilateurs antiques et démodés l'exigent encore.

    Création et destruction de tableaux —constructeur par défaut

    L'utilisation de tableaux nécessite un constructeur par défaut. C'est à dire un constructeur pouvant ne prendre aucun paramètre lors de son invocation. Un tel constructeur peut :

     Ne prendre effectivement aucun paramètre.Avoir des paramètres qui acceptent tous une valeur par défaut.

    Par exemple, le constructeur Point::Point(int absc=0, int ordo=0) est un constructeur par défaut !

    Et pourquoi avons nous besoin d'un constructeur par défaut allez vous me demander ? et bien c'est lié àla syntaxe de construction des tableaux, par exemple, pour un vecteur à une dimension :

    IdentificateurClasse identificateurObjet[tailleTableau];

    Comme vous pouvez le constater, il n'y a pas de place pour des paramètres de construction. Laconstruction doit donc se faire sans paramètre, d'où la nécessité d'un constructeur par défaut.

    Que faire si l'on doit créer un tableau d'objets sans constructeur par défaut ? La solution est un peualambiquée :

    1. On créé un tableau de pointeurs2. On appelle le constructeur souhaité sur chacun des pointeurs

    A ce sujet, il ne faut pas confondre certaines notions. A partir d'une même classe T, on peut avoir :

    Type de tableau Construction des objets Destruction Remarques

    Tableau statiqued'instances statiques

    T tableau[TAILLE]; Automatique

    Constructeur par défaut obligatoire.Tout est détruitautomatiquement

    Tableau statiqued'instances dynamiques

    T *tableau[TAILLE];

    for (int i=0; i

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    16/24

    for (int i=0;i

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    17/24

    ...

    int main(int, char **){  ObjetGraphique unObjet(...);

      affichageObjetGraphique(unObjet);  return 0;}

    Trois points méritent que l'on s'y attarde :

    1. Tout d'abord, du point de vue de l'appelant, rien ne permet de distinguer que l'on a passé l'objet

     par référence plutôt que par valeur.2. Typiquement l'opérateur "&" servait à prendre une adresse. Ici, il désigne une référence sur unobjet.

    3. A l'intérieur de la fonction, on manipule la référence comme l'on manipulerait un objet.

    Manipuler les références

    Il faut voir que la manipulation des références est beaucoup sure que celle des pointeurs.

    Une référence est toujours initialisée sur une variable, il n'y a pas de référence pointant « dans lanature » ou de référence nulle.Une fois affectée à une variable la référence ne peut plus "bouger". Elle reste liée à cette variable jusqu'à sa disparition.Il n'y a pas d'arithmétique sur les références.

    Le fragment de code suivant illustre certaines caractéristiques des références :

    int main(int, char **){  int i=5;  int j=6;

      int &r=i; // r référence sur i  int *p; // p pointeur non initialise : correct  int *q=&i; // q pointeur sur i  int &z; // Erreur ! z est une référence non initialisée

      r=10; // Affecte 10 a la variable referencee par r soit i  r=j; // Affecte la valeur de j a la variable referencee par r soit i  // une erreur frequente est de croire qu'a la suite de cette instruction

      // r reference j. r reste liée à i

      r++; // effectue i++

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    18/24

      return 0;}

     

    Vous allez me dire pourquoi ne pas toujours utiliser le passage par référence afin d'éviter desinvocations inutiles de constructeurs de recopie et de destructeurs ? La réponse est simple : on vatoujours passer les objets par référence mais on va utiliser un garde fou dans le cas où un objet n'est pas destiné à être modifié. Cette précaution s'appelle une référence constante. La syntaxe est lasuivante :

    const Type &referenceConstante

    Il est très important de noter qu'un objet passé par référence constante dans une fonction ou uneméthode y est considéré comme constant. Vous ne pourrez donc invoquer dessus que des méthodesconstantes. C'est pourquoi il est si important de déclarer toutes les méthodes qui le supportent.

    Le clonage d'objets : recopie et affectation

    Recopier un objet dans un autre est opération assez fréquente. Deux fonctionnalités y sont dédiées enC++ : le constructeur par recopie et l'opérateur d'affectation.

    Le constructeur par recopie

    Le constructeur par recopie est très important car il permet d'initialiser un objet par clonage d'un autre.Attention, j'ai bien dit initialiser, ce qui signifie que l'objet est en cours de construction. En particulier, leconstructeur par recopie est invoqué dès lors que l'on passe un objet par valeur à une fonction ou une

    méthode.

    Syntaxe

    La syntaxe du constructeur par recopie d'une classe T est la suivante :

    T::T(const T& o);

    Il est extrèmement important de passer l'objet recopié par référence sous peine d'entrainer un appelrécursif infini !

    Que se passe t'il si vous ne fournissez pas de constructeur de recopie ?

    Si vous ne fournissez pas explicitement de constructeur par recopie, le compilateur en génèreautomatiquement un pour vous. Celui-ci effectue une recopie binaire optimisée de votre objet ... ce quiest parfait si celui-ci ne contient que des éléments simples.

    En revanche, si votre objet contient des pointeurs, ce sont les valeurs des pointeurs qui vont êtrecopiées et non pas les variables pointées, ce qui dans de nombreux cas, conduira directement à lacatastrophe.

    Quand doit on fournir un constructeur de recopie ?

    Vous devez fournir un constructeur de recopie dès lors que le clonage d'un objet par recopie binaire brute peut entraîner un disfonctionnement de votre classe, c'est à dire, en particulier :

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    19/24

    Utilisation de mémoire dynamique.Utilisation de ressources systèmes (fichiers, sockets, etc ...).

    L'opérateur d'affectation

    Mise en place

    L'opérateur d'affectation et le constructeur de recopie sont très proches dans le sens où ils sont requis

    dans les mêmes circonstances et qu'ils effectuent la même opération : cloner un objet dans un autre. Il ya tout de même une différence fondamentale :

    L'opérateur d'affectation écrase le contenu d'un objet déjà existant et donc totalement

    construit.

    Ce qui signifie que dans la majorité des cas, il faudra commencer par "nettoyer" l'objet à la manièred'un constructeur avant d'effectuer l'opération de clonage dessus.

    Syntaxe

    Le prototype de l'opérateur d'affectation d'une classe T est le suivant :

    T & operator=(const T& o);

    Exemple :

    Initialisation ou Affectation ?

    Il est parfois délicat de savoir si l'on a affaire à une affectation ou une initialisation car la syntaxe dusigne "=" peut être trompeuse. Il existe pourtant une règle simple :

    Toute opération d'initialisation ou d'affectation dans une déclaration est l'affaire d'un constructeur 

    Le tableau suivant résume quelques cas qui doivent être lus séquentiellement et où T et U sont desclasses quelconques :

    Instruction Description Méthode mise en jeu

    T t1; Initialisation par le constructeur par défaut T::T(void);

    T t2( params); Initialisation par un constructeur quelconque T::T(liste params);

    T t3(t1); Initialisation par le constructeur de recopie T::T(const T&);T t4();

    C'est le prototype de la fonction t4 qui ne prend pas de paramètre mais renvoie un objet de type T.

     

    T t5=t1

    Initialisation par le constructeur de recopieCette ligne est à remplacer par T t5(t1); qui faitexactement la même chose mais est moins ambigue du point de vue de la syntaxe.

    T::T(const T&);

    t5=t2 Affectation à l'aide de l'opérateur d'affectation T & T::operator=(const T&);

     

    Forme canonique de Coplien

    On dit qu'une classe T est sous forme canonique de Coplien si elle fournit les éléments suivants :

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    20/24

    Prototype Fonctionnalité

    T::T() Constructeur par défaut

    T::T(const T&) Constructeur par recopie

    T& T::operator=(const T&) Opérateur d'affectation

    T::~T() Destructeur  

    Si ces éléments sont codés correctement, alors l'utilisation de cette classe vis à vis de la mémoire estsécurisé. Dès qu'une classe utilise de la mémoire dynamique ou des ressources critiques, il estindispensable de la mettre sous forme canonique de Coplien.

    exemple : la classe Chaine

     Nous allons ici créer une classe permettant des manipulations simples sur les chaînes de caractères etillustrant le bien fondé de la forme canonique de Coplien. Notez bien que cette classe est présentéeuniquement à but pédagogique car la librairie standard du C++ propose la classe string qui lui est biensupérieure.

    Une bonne classe chaine de caractères doit proposer un stockage dynamique, c'est à dire pouvants'accroître avec le temps. Nous allons donc avoir besoin d'un pointeur et d'allocation dynamique.

    #ifndef CHAINE_H#define CHAINE_H#include class Chaine{  public:

      // Constructeur par defaut  Chaine(int taille=16):  longueur_(0),  capacite_(taille)  {  if (taille)  tableau=new char [taille];  else  tableau=0;  }

     // Constructeur prenant un pointeur sur charChaine(const char *pStr)

      {  if (pStr)  {  int taille=strlen(pStr);  longueur_=capacite_=taille;  if (taille)  {  tableau = new char [taille+1];  strcpy(tableau,pStr);  }  else

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    21/24

      tableau=0;}

      else  {  longueur_=0;  capacite_=0;  tableau=0;

      }  } 

    // Constructeur de recopie  Chaine(const Chaine &uneChaine) :  longueur_(uneChaine.longueur_),  capacite_(uneChaine.capacite_)  {  tableau = new char [capacite_+1];  if (longueur_)

      strcpy(tableau,uneChaine.tableau);  } 

    ~Chaine(void)  {  delete [] tableau;  } 

    // operateur d'affectation 

    Chaine &operator=(const Chaine &uneChaine)  {  if (this != &uneChaine)  {  delete [] tableau; 

    longueur_=uneChaine.longueur_;

      capacite_=uneChaine.capacite_;  tableau = new char [capacite_+1];  if (longueur_)  strcpy(tableau,uneChaine.tableau); 

    }  return *this;  }

      const char operator[](int index) const  {  return tableau[index];  } 

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    22/24

      char &operator[](int index){

      return tableau[index];  } 

     private:

      int longueur_;  int capacite_;  char *tableau;};

    #endif 

     

    Ainsi, les objets de la classe Chaine sont à l'abri des problèmes de mémoire. Notez au passage quel'opérateur d'affectation vérifie que l'on essaye pas d'affecter un objet à lui même. C'est le but del'instruction if (this != &uneChaine) qui compare les adresses de l'objet courant et de celui donton veut lui affecter la valeur.

    Vous noterez également que le code de l'opérateur d'affectation et celui du constructeur de recopie sontétonament proches. En fait, l'affectation peut se ramener à une opération de nettoyage (comme dans ledestructeur) suivie d'une recopie. Aussi, nous allons pouvoir factoriser le code dupliqué. Le programmesuivant réécrit le constructeur de recopie, l'opérateur d'affectation et le destructeur à l'aide de deuxméthodes privées que nous appellerons clonage et nettoyage.

    #ifndef CHAINE_H#define CHAINE_H#include

    class Chaine{  public:

      // Constructeur par defaut  Chaine(int taille=16):  longueur_(0),  capacite_(taille)  {  if (taille)  tableau=new char [taille];  else  tableau=0;  } 

    // Constructeur prenant un pointeur sur charChaine(const char *pStr)

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    23/24

      {  if (pStr)  {  int taille=strlen(pStr);  longueur_=capacite_=taille;  if (taille)  {

      tableau = new char [taille+1];  strcpy(tableau,pStr);  }  else  tableau=0;

    }  else  {  longueur_=0;  capacite_=0;

      tableau=0;  }  } 

    // Constructeur de recopie  Chaine(const Chaine &uneChaine)  {  clonage(uneChaine);  }

      ~Chaine()

      {  nettoyage();  } 

    // operateur d'affectation 

    Chaine &operator=(const Chaine &uneChaine)

      {  if (this != &uneChaine)  {  nettoyage();  clonage(uneChaine);  }  return *this;  } 

    const char operator[](int index) const  {  return tableau[index];  } 

  • 8/18/2019 C++_CHAPITRE2_MIASI_2014_2015

    24/24

      char &operator[](int index){

      return tableau[index];  }  private:

      int longueur_;

      int capacite_;  char *tableau; 

    void clonage(const Chaine &uneChaine)  {  longueur_=uneChaine.longueur_;  capacite_=uneChaine.capacite_;  tableau = new char [capacite_+1];  if (longueur_)

      strcpy(tableau,uneChaine.tableau);  } 

    void nettoyage(void)  {  delete [] tableau;  } 

    };

    #endif