Docstoc

C++ - PDF

Document Sample
C++ - PDF Powered By Docstoc
					Ecole Supérieure des Procédés Electroniques et Optiques




                 COURS DE C++




                                      Christophe LÉGER
                                              1999-2000
2
                        1. ENCAPSULATION


1.1 INTRODUCTION
        C++ est un langage orienté objet dérivé du langage C. Le langage C a été développé
dans les années 1970 par B. Kernighan et D. Ritchie pour en faire le langage de
programmation structurée du système UNIX. Mais son utilisation est aujourd'hui beaucoup
plus répandue. Il est employé pour l'écriture de logiciels dans des domaines très divers
(programmation scientifique, informatique industrielle, gestion, ...) et avec des systèmes
d'exploitation différents (UNIX bien sûr, mais aussi MSDOS sur PC, ...). Le langage C est
donc réputé pour être facilement portable sur de multiples plates-formes (PC, SUN, HP, IBM,
DOS, UNIX, ...). Il est également renommé pour son efficacité (code compact et proche des
instructions machine), mais aussi pour son grand laxisme (le programmeur ne dispose pas des
garde-fous du Pascal) qui peut conduire à écrire des programmes illisibles ou faux.
        A chaque langage sont associés des modèles de programmation qui présentent
l'ensemble des techniques à appliquer lors de la conception et l'implémentation des
programmes. Le C++ a été développé en 1983 par Bjarne Stroustrup, des laboratoires AT&T
Bell, USA. Il permet d'améliorer la qualité des programmes écrits en C, grâce à l'ajout des
principes de la programmation orientée objets : la modularité, l'encapsulation des données,
l'héritage, la surcharge des fonctions et le polymorphisme. Le passage du C au C++
correspond à une évolution naturelle de la programmation structurée classique vers la
programmation orientée objets. C'est pourquoi le C++ représente de plus en plus la nouvelle
manière de programmer des développeurs en C. Son apprentissage direct se justifie donc
pleinement, surtout pour les programmeurs déjà familiarisés à la programmation classique.


1.2 ABSTRACTION DES DONNÉES
        L'abstraction des données est un des concepts fondamentaux de la POO
[Programmation Orientée Objets]. En programmation structurée, les données qui ont un lien
logique sont regroupées en structure, les enregistrements. De la même manière, les
instructions qui ont un lien logique sont rassemblées dans des sous-programmes. Ainsi, les
données et le code sont structurés, et on atteint un certain niveau d'abstraction puisqu'un seul
identificateur regroupe plusieurs champs d'un enregistrement ou plusieurs instructions d'un
sous-programme. L'exécution d'un programme consiste alors à utiliser des sous-programmes
pour initialiser, et/ou modifier, et/ou afficher des données. Ces sous-programmes doivent donc
connaître la structure des données qu'ils utilisent. Pour cela, il faut définir les structures des
données avant celles des sous-programmes, et déclarer ces données à travers les interfaces des
sous-programmes (paramètres). On fait donc coexister de manière autonome des éléments qui
ont un lien logique. Grâce à l'encapsulation, la POO propose de relier données et code
structurés dans des structures nouvelles qui permettent d'utiliser des sous-programmes sans
connaître la structure des données qu'ils manipulent.




                                                                                                 3
1.3 ENCAPSULATION
        Pour permettre l'abstraction des données, les LOOs [Langage Orienté Objets]
fournissent une structure qui regroupe (ou encapsule) les données et les sous-programmes qui
les utilisent. En C++, une telle structure est appelée classe, et l'instance d'une classe (la
déclaration d'une variable de type classe) est appelée objet. Les classes sont composées de
données membres (que l'on peut comparer aux champs des enregistrements de la
programmation structurée) et de fonctions membres, qui définissent les opérations à réaliser
sur les données membres.
        Les programmes qui manipulent des objets ne connaissent pas les identificateurs des
données membres de ces objets. Les échanges de données entre le programme et les objets se
font au moyen d'une interface clairement définie (les fonctions membres) et donc
indépendante des données membres. Aussi longtemps que cette interface ne change pas, les
données-membres et le code des fonctions membres de l'objet peuvent changer sans nécessiter
la modification du programme qui utilise l'objet. Cela permet une modularité plus grande et
une indépendance vis à vis du choix de la représentation interne des données.


1.4 CLASSES
       Une classe est composée d'un ensemble de données membres et de fonctions membres.
La syntaxe de déclaration d'une classe est la suivante :

       class identificateur
       {                                             où      <déclarations> est de la forme :
         private: (par défaut)
               <déclarations>                                       type1 donnée_membre1;
         protected:                                          type2 donnée_membre2;
               <déclarations>                                       ...
         public:                                             fonction_membre1 (paramètres);
               <déclarations>                                fonction_membre2 (paramètres);
       };                                                    ...

                              Par exemple :

                                     class booleen
                                     {
                                             short b;
                                       public:
                                             void init (short);
                                             void not ();
                                             void affiche ();
                                     };

       1.4.1 Contrôle d'accès
        Par défaut, les données membres et les fonctions membres des classes sont privées,
c'est à dire inaccessibles de l'extérieur de la classe, ce qui permet d'imposer l'abstraction des
données. Mais il est possible de contrôler l'accès des différents membres à l'aide des mots
réservés private, protected et public.

4
        Généralement, les membres privés sont les données et les membres public sont les
fonctions (nous reviendrons plus loin sur les membres protected). En effet, pour bénéficier
pleinement de l'abstraction des données, il faut interdire la manipulation directe des données
membres à l'extérieur d'une classe (celles-ci ne doivent être utilisées que par les fonctions
membres). Par contre, les fonctions membres étant la seule interface entre la classe et
l'extérieur, celles-ci doivent être déclarées publiques. Ainsi, il est possible de les référencer à
l'extérieur de la classe.
        Les déclarations privées, publiques ou protégées peuvent se faire plusieurs fois et dans
n'importe quel ordre. Cependant, pour des raisons de lisibilité, on les utilisera dans l'ordre
private, protected, public, d'abord pour les données membres, puis pour les fonctions
membres.
        Conséquence directe du fait que l'on s'interdit d'accéder directement aux données
membres d'une classe, il faut toujours déclarer une fonction membre qui permet d'initialiser
les données membres de cette classe. De la même manière, mais moins systématiquement, on
déclare souvent une fonction membre qui, à la fin du programme, retourne ou affiche les
données membres.


1.5 DONNÉES MEMBRES
       Les données membres comprennent n'importe quelle donnée de type fondamental (type
de base du langage), composé ou défini par l'utilisateur. Elles sont déclarées de la même
manière que les variables, mais ne peuvent être automatiquement initialisées lors de leur
déclaration. Les données membres doivent rester privées.

       1.5.1 Membres statiques
         Une donnée membre d'une classe peut être précédée du mot réservé static, afin de
spécifier que cette donnée est partagée par toutes les instances de la classe. Il s'agit donc d'une
zone de donnée unique, commune à toutes les variables de la classe. Mais attention, cette
utilisation de données membres statiques doit rester très exceptionnelle car elle nuit à la
lisibilité des programmes.


1.6 FONCTIONS MEMBRES
       Une fonction membre d'une classe peut accéder à tous les membres (données ou
fonctions) de cette classe, qu'ils soient privés, protégés ou publics. Pour définir une fonction
membre, il existe deux possibilités : les fonctions inline et les fonctions "non inline".

       1.6.1 Fonctions « non inline »
        Elles sont déclarées dans la définition d'un classe puis définies à l'extérieur de cette
classe. Comme plusieurs classes peuvent définir des fonctions ayant même identificateur, il
faut spécifier l'appartenance des fonctions lors de leur définition. Ceci est réalisé en faisant
précéder le nom de la fonction du nom de la classe et de l'opérateur ::, appelé opérateur de
résolution de portée.




                                                                                                  5
       Exemple :      void booleen::init (short bi=0)
                      {
                        b = bi;
                      }

                      void booleen::not ()
                      {
                        b = !b;
                      }

                      void booleen::affiche ()
                      {
                        if (b==0)
                          cout << "Faux";
                        else
                          cout << "Vrai";
                      }

       1.6.2 Fonctions inline
       L'utilisation de fonctions dans un programme améliore énormément la lisibilité, mais
peut dans certains cas nuire à l'efficacité des programmes (appel de la fonction plus long que
son exécution). Le spécificateur inline permet d'améliorer la lisibilité en demandant au
compilateur de remplacer l'appel de la fonction par son code, à l'endroit de l'appel. Les
fonctions inline ne doivent généralement comporter que peu d'instructions.

       Syntaxe :      il suffit de définir le corps de la fonction lors de sa déclaration, dans la
                      classe, ou alors de faire précéder la définition de la fonction par le mot
                      réservé inline.

       Exemple :      class booleen
                      {
                              short b;
                        public:
                              void init (short);
                              void not () { b != b; };
                              void affiche ();
                      };
                      inline void booleen::affiche ()
                      {
                        ...
                      }

       1.6.3 Fonctions amies (friend)
       Une fonction amie d'une classe est une fonction (membre ou non d'une autre classe)
qui bénéficie des mêmes droits d'accès que les membres de cette classe. Les fonctions amies
résolvent par exemple le problème suivant : lorsque plusieurs classes sont définies dans un
même programme, il peut arriver que les fonctions d'une classe travaillent sur les objets d'une
autre classe. Pourtant, l'utilisation des données membres (privées) de cette classe sont

6
interdites à l'extérieur de la classe, et une même fonction ne peut être simultanément membre
de plusieurs classes. Les fonctions amies permettent donc d'accéder de l'extérieur aux
membres privés d'une classe.
        Pour déclarer une fonction membre amie, il suffit de faire précéder sa définition par le
mot réservé friend. Bien évidemment, le corps des fonctions amies d'une classe n'est pas
défini lors de la déclaration de cette classe. Pour résoudre les éventuels problèmes
d'appartenance, on utilise l'opérateur ::. L'utilisation des fonctions amies nuit énormément à la
bonne POO. Il ne faut donc les utiliser qu'en dernier recours.


1.7 DÉCLARATION D'INSTANCES
       La déclaration et l'utilisation d'un objet (instance d'une classe) se font de la manière
suivante :

Syntaxe :                                           Exemple :
      class identificateur_de_classe                                   class booleen
      {                                                     {
              ...                                                  short b;
        public:                                              public:
              fonction_membre1 (paramètres);                       void init (short)
              fonction_membre2 (paramètres);                       void not ();
              ...                                                  void affiche ();
      };                                                    };

       main ()                                              main ()
       {                                                    {
         identificateur_de_classe objet;                      booleen fini;

           objet.fonction_membre1 (paramètres);                 fini.init (1);
           objet fonction_membre2 (paramètres);                 fini.not ();
           ...                                                  fini.affiche ();
       }                                                    }

       Grâce à l'encapsulation, les fonctions membres deviennent l'interface entre les classes
et le monde extérieur. La programmation se réduit alors à une succession d'ordres. C'est pour
cela que l'on appelle souvent les LOOs "langages acteurs".


1.8 MOTS CLEFS
       Abstraction des données
       Classe
       Déclaration
       Définition
       Donnée membre
       Encapsulation
       Fonction amie
       Fonction inline
       Fonction membre

                                                                                                7
    Identificateur
    Instanciation
    Objet
    Paramètres
    Structure du code
    Structure des données




8
               2. ÉLÉMENTS DE SYNTAXE


2.1 STRUCTURE D'UN PROGRAMME C++
        Voici deux exemples de programme destinés à donner un aperçu de ce qu'est un
programme en C++. Il faut simplement noter qu'un programme écrit en C++ est constitué
d'une suite de fonctions. Chaque fonction est composée d'un entête et d'un corps. L'entête
contient le nom et le type des paramètres, le corps est composé de déclarations et
d'instructions. Parmi toutes les fonctions, l'une doit être principale. C'est celle qui sera
exécutée en premier ; son nom doit obligatoirement être main.

Exemple 1 : architecture minimale d'un programme.
     main ()
     {
     }

Toute déclaration de fonction doit être suivie des parenthèses (), même si elle ne comporte pas
de paramètre. Le corps de la fonction est délimité par les accolades {} ({ équivaut au begin du
Pascal, et } au end).

Exemple 2 : programme qui affiche la chaîne de caractères ESPEO.
     #include <iostream.h>
     main ()
     {
       cout << "ESPEO" << endl;
     }

La ligne #include <iostream.h> sert à inclure le fichier de définition pour les opérations
d'entrées/sorties (pour utiliser cout).


2.2 ELÉMENTS DE BASE


       2.2.1 Caractères
       Le compilateur C++ utilise l'ensemble des caractères alphabétiques ou numériques
suivants :
       abcdefghijklmnopqrstuvwxyz0123456789
       ABCDEFGHIJKLMNOPQRSTUVWXYZ
Attention, contrairement au compilateur Pascal, C++ fait la différence entre les minuscules et
les majuscules.
C++ reconnait aussi les caractères spéciaux :
       ! " # % & ' ( ) * + , - . / : ; < = > ? [ \ ] ^ _ { } ~ | espace tab entrée


                                                                                              9
Comme certains caractères spéciaux ne sont pas disponibles sur tous les claviers, il est
possible d'en remplacer certains par la combinaison ??x :
        # ≡ (équivaut à) ??=, [ ≡ ??(, ] ≡ ??), \ ≡ ??/, ^ ≡ ??', | ≡ ??!, { ≡ ??<, } ≡ ??>, ~ ≡ ??-
D'autre part, certains caractères "non imprimables" (rassemblés sous l'appellation séquence
d'échappement) sont néanmoins disponibles en les remplaçant par la combinaison \x :
\a ≡ sonnerie, \b ≡ retour arrière, \f ≡ nouvelle page, \n ≡ nouvelle ligne, \r ≡ entrée, \t ≡
tabulation horizontale, \v ≡ tabulation verticale, \' ≡ ', \" ≡ ", \? ≡ ?, \\ ≡ \
De cette manière, il est aussi possible de spécifier directement la valeur du code ASCII d'un
caractère (en octal ou hexadécimal). Exemple : \141 ou \x61 représentent le caractère a de
code ASCII 97.

       2.2.2 Commentaires
         Pour placer une seule ligne en commentaire, on utilise //. Le commentaire commence
après // et se termine à la fin de la ligne. Pour placer plusieurs lignes en commentaire, on
utilise /* (début de commentaire) et */ (fin de commentaire).

       2.2.3 Délimiteurs
        Les délimiteurs sont des caractères qui permettent au compilateur de reconnaître les
différents éléments syntaxiques du langage. Les principaux délimiteurs sont les suivants :
        ;      : termine une déclaration de variable ou une instruction,
        ,      : sépare deux éléments dans une liste,
        ()     : encadre une liste de paramètres,
        []     : encadre la dimension ou l'indice d'un tableau,
        {}     : encadre un bloc d'instructions ou une liste de valeurs d'initialisation.

       2.2.4 Identificateurs
        Les identificateurs définis par le programmeur peuvent être constitués de n'importe
quelle combinaison de caractères alphabétiques ou numériques, mais doivent obligatoirement
commencer par une lettre. Le caractère souligné _ est considéré comme une lettre. Il n'y a pas
de taille limite maximale pour les identificateurs, mais ceux-ci doivent être choisis courts et
représentatifs de ce qu'ils identifient. C++ fait la différence entre les majuscules et les
minuscules. Par exemple, true, et TRUE sont des identificateurs différents.

       2.2.5 Mots réservés
        Les mots réservés du C++ sont des mots privilégiés du langage qui ne doivent pas être
utilisés comme identificateurs. En voici la liste :
        asm           continue       float          new          signed        try
        auto          default        for            operator     sizeof        typedef
        break         delete         friend         private      static        union
        case          do             goto           protected    struct        unsigned
        catch         double         if             public       switch        virtual
        char          else           inline         register     template      void
        class         enum           int            return       this          volatile
        const         extern         long           short        throw         while




10
         2.2.6 Types de base
         Il existe trois types de base : caractère, entier et double.

   TYPE : CARACTÈRE                            TAILLE                         LIMITES
 Signed char                                    1 octet                       -128 à 127
 Unsigned char (char)                           1 octet                         0 à 255

      TYPE : ENTIER                            TAILLE                          LIMITES
 Signed short (short)                           2 octets                    -32768 à 32767
 Unsigned short                                 2 octets                       0 à 65535
 Signed int (int)                               4 octets                -214783648 à 214783647
 Unsigned int                                   4 octets                    0 à 4294967295
 Signed long (long)                             4 octets                -214783648 à 214783647
 Unsigned long                                  4 octets                    0 à 4294967295

    TYPE : RÉEL                 TAILLE            LIMITE                LIMITE MANTISSE
                                                 EXPOSANT
 Float                           4 octets          -38 à 38          1.175494350822875 10-38 à
                                                                     3.4028234663852886 10+38
 Double                          8 octets         -308 à 308        2.2250738585072015 10-308 à
                                                                     1.7976931348623158 10+308
 Long double                     8 octets         -308 à 308        2.2250738585072015 10-308 à
                                                                     1.7976931348623158 10+308

         2.2.7 Valeurs littérales
       A chaque type de base, peuvent être associées des valeurs littérales correspondantes.
En voici la liste :

Valeur caractère :
        Une valeur caractère est représentée par un caractère ou une séquence d'échappement
(voir paragraphe caractères) entouré d'apostrophes. Exemple : 'X', '\'', '0', '\117'.

Valeur entière :
        Pour les valeurs entières, le suffixe u ou U indique que la valeur est signée. De même,
le suffixe l ou L indique que la valeur est de type long.
        décimales : Les valeurs entières décimales sont composées d'une séquence de
                       chiffres de 0 à 9. Elles ne doivent pas commencer par 0. Exemple : 128,
                       799, 128L, 32744u.
        octales:       Les valeurs entières octales sont composées d'une séquence de chiffres
                       de 0 à 7. Elles doivent obligatoirement commencer par 0. Exemple :
                       0732, 0899.
        hexa :         Les valeurs entières hexadécimale sont composées d'une séquence de
                       chiffres de 0 à 9 ou de lettres de A à F (majuscules ou minuscules).
                       Elles doivent obligatoirement commencer par 0x ou 0X. Exemple :
                       0x3F, 0Xe2d4.

Valeur réelle :
                                                                                                  11
        Les valeurs réelles sont constituées d'une partie entière, d'un point, d'une partie
fractionnaire, et d'une partie exponentielle, signée ou non, précédée de la lettre e ou E. La
partie entière ou la partie fractionnaire peut être omise, mais pas les deux. Le suffixe f ou F
indique une valeur de type float. Le suffixe l ou L indique une valeur de type long double.
Sans suffixe, la valeur est de type double. Exemple : .5E1 (5.0, double), 50e-1F (5.0, float),
5.0L (5.0, long double).

Valeur chaîne de caractères :
        Une valeur chaîne de caractères est constituée d'une suite de caractères (lettres ou
chiffres ou signes de ponctuation) placée entre guillemets. Pour continuer une chaîne de
caractères sur une ligne suivante, utiliser \ à la fin de la première ligne suivi d'un retour
chariot. Le caractère \0 est ajouté à la fin de chaque chaîne de caractères.
Exemple : "Le C++ est un LOO" est une chaîne de 18 caractères (\0 à la fin).

           2.2.8 Déclaration des variables
           Syntaxe :     type identificateur;
                         ou
                         type identificateur1, identificateur2, ..., identificateurn;

           Remarque :    Dans un programme en C++, les variables peuvent être déclarées
                         n'importe où dans le programme du moment qu'elles le sont avant leur
                         utilisation.
                         Déclarer une variable revient à donner un identificateur à un
                         emplacement mémoire dont la taille est fixée par type, et à préciser le
                         format interne de stockage des données.

           Exemple :     int i, j;
                         double t, r;

           2.2.9 Attributs des variables
        Il est possible de spécifier des attributs de variables. Ceux-ci sont au nombre de
quatre : static, auto, register et extern.

static :          Les variables statiques ont un emplacement mémoire réservé au moment de la
                  compilation. Leur allocation est donc permanente en mémoire, et leur contenu,
                  qui peut évoluer, est conservé pendant toute la durée d'exécution d'un
                  programme. Les variables statiques sont surtout utilisées pour les variables
                  locales des fonctions. Elles permettent en effet de conserver la valeur d'une
                  variable entre deux appels d'une même fonction. Mais attention, ceci nuit
                  énormément à la lisibilité des programmes et ne doit être utilisé qu'en dernier
                  recours. Exemple : static int i;
auto :            Les variables auto, propres à une fonction, sont allouées lors de l'appel de cette
                  fonction, puis libérées au retour de la fonction. Leur contenu est donc perdu
                  entre deux appels consécutifs à la fonction. Les variables locales des fonctions
                  sont auto par défaut. Exemple : auto int n;
register :        L'attribut register permet de placer une variable dans un registre interne du
                  microprocesseur. Il est généralement employé avec des variables intensément
                  utilisées comme les indices de boucles. A cause du nombre limité de registres,

12
               quelques variables seulement peuvent être placées dans les registres. D'autre
               part, il faut bien évidemment que la taille de la variable corresponde à la taille
               des registres (16 ou 32 bits). Exemple : register int i;
extern :       L'utilisation de variables externes permet de référencer des variables définies
               dans d'autres fichiers. Une déclaration extern ne remplace pas une définition.
               Elle décrit simplement une variable définie ailleurs. Exemple : extern int i;

       2.2.10 Initialisation des variables
        Les variables peuvent être initialisées à l'aide de l'opérateur d'affectation =.
L'expression à droite du signe = doit être évaluable, c'est à dire représenter une valeur. Le
membre à gauche de l'affectation doit lui représenter une variable, c'est à dire un contenu.
Toutes les variables doivent être initialisées avant leur utilisation. Il est possible d'initialiser
les variables au moment de leur déclaration.

       Exemple :
            main ()
            {
              int i, j=0;
              double e=2.71828, a;

                   j = 3;
                   a = 5.6;
               }

       2.2.11 Constantes typées
        Pour déclarer une constante typée en C++, il suffit de faire précéder la déclaration
d'une variable initialisée au moment de sa déclaration par le mot réservé const. Ainsi, la valeur
de cette variable ne peut plus être modifiée. Exemple : const double rac2 = 1.41421. On verra
la déclaration et l'utilisation des constantes non typées au paragraphe sur les directives pour le
préprocesseur.


2.3 INSTRUCTIONS
        Les fonctions d'un programme C++ sont constituées d'un ensemble de données et
d'instructions exécutables. Une instruction est une séquence d'opérateurs, opérandes et signes
de ponctuation. Une expression peut conduire ou non à un résultat. Le déroulement des
instructions dans le corps d'une fonction est séquentiel, sauf dans le cas de rupture de
séquence, de terminaison de boucle ou de retour de fonction. Toute instruction doit
obligatoirement se terminer par un point virgule (;) qui est un terminateur d'instruction. Une
instruction composée regroupe plusieurs instructions entourées par des accolades ({ et }). Une
instruction composée ne se termine pas par un point virgule.


2.4 OPÉRATEURS
       C++ offre une grande richesse d'opérateurs. En voici la liste.


                                                                                                  13
       2.4.1 Opérateurs arithmétiques
       +      :       addition
       −      :       soustraction
       *      :       multiplication
       /      :       division
       %      :       modulo ou reste de la division

       2.4.2 Opérateurs de manipulation de bits
       &      :       ET logique (AND). Cet opérateur est utilisé conjointement avec un
                      masque de bits.
       |      :       OU logique (OR)
       ^      :       OU logique exclusif (XOR)
       <<     :       décalage vers la gauche
       >>     :       décalage vers la droite
       ~      :       complémentation à un unaire

       2.4.3 Opérateurs d'affectation, d'incrémentation et de décrémentation
       =      :       affectation. Des simplifications peuvent survenir lorsque la partie
                      gauche du signe = se répète à droite. On peut remplacer par exemple x =
                      x + 1; par x += 1;. On a de la même manière -=, *=, /=, %=, &=, |=, ^=,
                      ~=, <<= et >>=.
       ++     :       incrémentation. x = x + 1; peut s'écrire x += 1;, mais aussi ++x;
                      (préfixe) ou x++; (suffixe). En préfixe, la variable est incrémentée avant
                      son utilisation, alors qu'en suffixe elle est incrémentée après son
                      utilisation. Par exemple : x = 3; y = x++; conduira à avoir 3 dans y et 4
                      dans x alors que x=3; y = ++x; conduira à avoir 4 dans x et y.
       −−     :       décrémentation. x = x − 1; peut s'écrire x −= 1;, mais aussi −−x;
                      (préfixe) ou x−−; (suffixe). En préfixe, la variable est décrémentée
                      avant son utilisation, alors qu'en suffixe elle est décrémentée après son
                      utilisation. Par exemple : x = 3; y = x−−; conduira à avoir 3 dans y et 2
                      dans x alors que x=3; y = −−x; conduira à avoir 2 dans x et y.

       2.4.4 Opérateurs relationnels
       Les opérateurs relationnels mettent en relation deux expressions, et le résultat est une
expression booléenne fausse (0) ou vraie (tout entier différent de 0).
       >      :      supérieur
       >=     :      supérieur ou égal
       <      :      inférieur
       <=     :      inférieur ou égal
       ==     :      égalité
       !=     :      inégalité

Attention, en C++, de nombreuses erreurs proviennent de la confusion des opérateurs
d'affectation (=) et d'égalité (==).



14
       2.4.5 Opérateurs logiques
        Les expressions reliées par ces opérateurs sont évaluées de la gauche vers la droite.
L'évaluation prend fin dès que le résultat d'une seule expression entraîne un résultat définitif
pour l'expression globale.

       !       :      négation unaire (NOT). Cet opérateur a pour effet d'inverser la valeur
                      du résultat de l'expression qui le suit.
       &&      :      ET logique (AND)
       ||      :      OU logique (OR)

       2.4.6 Opérateur conditionnel

       L'opérateur conditionnel est un opérateur ternaire mettant en relation trois expressions.
L'expression résultante est booléenne. Cet opérateur est composé des deux signes ? et :.
Exemple : (expression1) ? (expression2) : (expression3) qui peut se traduire par si
(expression1) alors (expression2) sinon (expression3). Voici un exemple de calcul d'une
valeur absolue : val = (n>0) ? n : -n;

       2.4.7 Opérateur sizeof
       sizeof donne la taille en octets de l'opérande qui lui est associé. Par exemple, sizeof
(double) donne 8, ou int i; sizeof (i) donne 4.

       2.4.8 Opérateur ,
      L'opérateur , permet de manipuler une liste d'expressions comme une seule expression.
Deux expressions séparées par une virgule sont évaluées de gauche à droite et l'ensemble a
comme valeur celle de l'expression la plus à droite.
Exemple c = (a = 1, b = 2); affecte 1 a "a" et 2 a "b", et affecte 2 a "c".

       2.4.9 Opérateur .
        L'opérateur . est utilisé pour accéder aux membres (données ou fonctions) d'une classe,
d'une structure ou d'une union. Exemple : class X x; x.init (); x.done ();

       2.4.10 Opérateur ::
        L'opérateur :: est utilisé pour relier la définition d'une fonction à la déclaration d'une
classe. Exemple : class X {...} int X::x()...

       2.4.11 Opérateur ( ) : conversion de type
       L'opérateur ( ) permet de forcer le type d'une expression. Par exemple, après la
déclaration int i; double a;, on peut convertir la valeur de a en entier pour la stocker dans i en
écrivant : i = (int) a;

       2.4.12 Autres opérateurs
       Il existe d'autres opérateurs comme new, delete, throw, ->, ., [ ], &, * sur lesquels
nous reviendrons plus loin.

                                                                                                15
       2.4.13 Précédence des opérateurs
        Dans une expression qui comporte plusieurs opérateurs, l'évaluation s'effectue suivant
un ordre de priorité décroissant. A chaque opérateur est associé une priorité appelée
précédence d'opérateur. Dans le tableau suivant, les opérateurs placés sur une même ligne sont
d'égale précédence, les lignes étant disposées par priorité décroissante.

                    Opérateur             Associativité
 ( ) { } -> .                Gauche → Droite
 ! ~ ++ -- - ( ) * & sizeof  Droite → Gauche
 * / %                       Gauche → Droite
 + -                         Gauche → Droite
 << >>                       Gauche → Droite
 < <= > >=                   Gauche → Droite
 == !=                       Gauche → Droite
 &                           Gauche → Droite
 ^                           Gauche → Droite
 |                           Gauche → Droite
 &&                          Gauche → Droite
 ||                          Gauche → Droite
 ?:                          Droite → Gauche
 = += -= *= /= %= >>= <<= &= Droite → Gauche
 |= ^=
 ,                           Gauche → Droite


2.5 STRUCTURES CONDITIONNELLES


       2.5.1 if ... else ...
       Syntaxe :        if (expression) instruction1;
                        ou
                         if (expression) instruction1; else instruction2;
                        instruction peut être une instruction composée de la forme :
                        { instruction1; instruction2; ... ; }

       Remarques :       expression doit être de type entier.
                         Si expression est vraie, instruction1 est exécutée. Sinon, c'est instruction2
                         qui est exécutée.
                         Lorsque plusieurs if sont imbriqués, else se réfère au dernier if.

       Exemple :         if (score1==score2) nul = 1; else nul = 0;

       2.5.2 switch
       Syntaxe :        switch (expression)
                        {
                          case valeur1 : instruction;

16
                   case valeur2 : instruction;
                   case valeur3 : instruction;
                   ...
                   default : instruction;
                  }
                  instruction peut être une instruction composée de la forme :
                  { instruction1; instruction2; ... ; }. Les accolades peuvent alors même être
                  omises.

    Remarques :   L'évaluation a lieu dans l'ordre des "case".
                  expression doit être de type entier.
                  Dès que la valeur de l'expression correspond à la valeur qui suit un
                  "case", toutes les instructions qui suivent ce "case" sont exécutées (même
                  celles des autres "case"). Pour provoquer une sortie immédiate du switch,
                  on utilise l'instruction break (voir paragraphe branchements).
                  L'instruction qui suit le "default" est exécutée si aucun branchement dans
                  un "case" n'est réalisé.

    Exemple :     switch (c)
                  {
                   case 'a' : cout << "Lettre a"; break;
                   case 'b' : cout << "Lettre b"; break;
                   default : cout << "Erreur";
                  }


2.6 STRUCTURES ITÉRATIVES


    2.6.1 Boucle while
    Syntaxe :      while (expression) instruction;
                  instruction peut être une instruction composée de la forme :
                  { instruction1; instruction2; ... ; }

    Remarques :   Le test de l'expression a lieu avant exécution de l'instruction.
                  Lorsque l'expression est fausse, la boucle while ... do n'est pas exécutée.
                  L'expression à tester doit être de type entier.

    Exemple :     int i=0;
                  while (i<N) cout << "i = " << i++;

    2.6.2 Boucle do ... while
    Syntaxe :      do instruction; while (expression)
                  instruction peut être une instruction composée de la forme :
                  { instruction1; instruction2; ... ; }

    Remarques :   Le test de l'expression a lieu après l'exécution de l'instruction.
                  La boucle do ... while est toujours exécutée au moins une fois.

                                                                                                17
                        L'expression à tester doit être de type entier.
        Exemple :       int i=10;
                        do cout << "i = " << i--;
                        while (i>0);

        2.6.3 Boucle for
        Syntaxe :        for (expression1 ; expression2 ; expression3) instruction
                        instruction peut être une instruction composée de la forme :
                        { instruction1; instruction2; ... ; }
                         expression1 sert d'initialisation, expression2 réalise le test de la boucle,
                         expression3 est exécutée à chaque itération.

        Remarques :     On choisira une boucle for lorsque le nombre d'itérations est connu à
                        l'entrée dans la boucle.
                        Les différentes expressions peuvent être omises (exemple : boucle sans
                        fin si expression2 est toujours vraie, boucle sans initialisation si
                        expression1 n'apparait pas).
                        Plusieurs expressions peuvent être regroupées pour former expression1,
                        expression2 ou expression3. Elles sont alors séparées pas des virgules
                        (exemple : for (i=0,j=N;i<N;i++,j--) instruction).
                        La boucle for n’est pas exécutée si la valeur de départ du compteur est
                        supérieure à la valeur de fin.
                        L'expression à tester doit être de type entier.

        Exemple :       for (i=0;i<N;i++) cout << i;


2.7 BRANCHEMENTS
         Toutes les instructions de branchement nuisent à la clarté d'un programme. Il faut donc
s'interdire de les utiliser. Toutefois dans des cas très particuliers, on peut être amené à les utiliser
(exemple : break dans l'instruction switch).

        2.7.1 break
        L'instruction break permet de quitter une instruction itérative (do, for ou while), ou un
switch. L'instruction qui est exécutée après un break est la première instruction après la boucle
ou le switch.

        Remarque :      L'instruction break est généralement utilisée dans les instructions switch.

        Exemple :       for (i=0;i<5;i++)
                        {
                         if (string[i] == '\0') break;
                          length++;
                        }
                        Lorsque le caractère nul est rencontré dans string, l'exécution se poursuit
                        sur l'instruction qui suit la boucle for.


18
       2.7.2 continue
        L'instruction continue impose le passage immédiat à l'itération suivante dans une boucle
(do, for ou while).


       Exemple :      for (i=0;i<N;i++)
                      {
                        k=i-1;
                       if (k==0) continue;
                       a = 1/k;
                      }
                      Lorsque k est égal à zéro, on passe directement à l'itération suivante de la
                      boucle (i est incrémenté et l'instruction k=i-1 est exécutée). Tout se passe
                      comme si les instructions de la boucle qui suivent continue n'étaient pas
                      exécutées.

       2.7.3 goto
        L'instruction goto permet un saut inconditionnel vers l'instruction qui suit l'étiquette
spécifiée avec le goto.

       Syntaxe :       goto label;
                      ...
                      label : instruction

       Remarque :      L'utilisation d'un goto dans un programme du module C++ conduit
                       immédiatement a un zéro dans ce module.

       Exemple :      for (j=0;j<=N;j++)
                      {
                       if ( (j<0) || (j>N) ) goto etiq1;
                      }
                      etiq1 : cout << "Erreur : cas impossible";

       2.7.4 return
       L'instruction return arrête l'exécution d'une fonction.

       Syntaxe :      return
                      ou
                      return valeur

       Remarque :     L'utilisation de return dans les fonctions est facultative, mais ne pas en
                      mettre dans les fonctions typées provoque un warning.
                      Utiliser return dans les fonctions déclarées void provoque une erreur.
                      Une fonction peut contenir plusieurs return.

       Exemple :      int somme (int a, int b)
                      {
                        return (a+b);
                                                                                                19
                       }
                       return permet de quitter somme en rendant la main au (sous-) programme
                       d'appel, après avoir affecté la valeur entière a+b à la fonction.




2.8 DIRECTIVES POUR LE PRÉPROCESSEUR
       Ces directives ne sont pas des instructions du C++. Elles ne sont utilisées que par le
préprocesseur qui réalise la première phase de la compilation, et non par le compilateur lui-
même.

       2.8.1 #define et #undef
        #define permet de remplacer dans un fichier toutes les occurrences d'un symbole par une
suite de caractères. Exemple : #define N 64 remplacera tous les caractères N du texte qui suit
#define par les caractères 64.
        #undef annule la définition d'un symbole pour le reste d'un fichier. Exemple : #undef N.

       2.8.2 #include
          #include permet d'insérer, à la place de la ligne #include, le contenu d'un fichier source
courant à compiler (généralement un fichier de définitions .h). Deux syntaxes existent : #include
"nom de fichier" ou #include <nom de fichier>. Dans la première forme, le fichier appartient à
l'utilisateur et doit se trouver dans le répertoire courant, ou alors il faut spécifier son chemin
d'accès complet. Dans la seconde, le fichier est recherché dans le répertoire /usr/include.
Exemple : #include <iostream.h>.

       2.8.3 Compilation conditionnelle #if/#ifdef/#ifndef ... #else ... #endif
       Ces directives de compilation permettent de compiler des lignes d'un programme si une
condition est vérifiée.

       Syntaxe :       #if expression
                               séquence compilée si expression est vraie (≠0)
                       #else
                               séquence compilée si expression est fausse (=0)
                       #endif
                       poursuite de la compilation

       Syntaxe :       #ifdef symbole
                               séquence compilée si symbole a été défini par #define
                       #else
                               séquence compilée si symbole n'a pas été défini par #define
                       #endif
                       poursuite de la compilation

       Syntaxe :       #ifndef symbole
                               séquence compilée si symbole n'a pas été défini par #define
                       #else
20
                           séquence compilée si symbole a été défini par #define
                  #endif
                  poursuite de la compilation

    Remarque :     L'expression qui suit #if peut contenir des parenthèses, des opérateurs
                   unaires, binaires ou ternaire.



2.9 MOTS CLEFS
    Attribut de variable
    Constante non typée
    Constante typée
    Délimiteur
    Directive
    Identificateur
    Mot réservé
    Opérateur
    Préprocesseur
    Variable




                                                                                        21
             3. POINTEURS ET FONCTIONS


3.1 POINTEURS ET VARIABLES DYNAMIQUES
        Les variables de type pointeur sont des variables qui contiennent des adresses. Avec les
opérateurs de réservation et de libération mémoire, les pointeurs permettent de manipuler des
variables dynamiques, c'est à dire créées et détruites, selon les besoins, au fur et à mesure des
programmes.

       3.1.1 Type pointeur
        Une variable de type pointeur est une variable qui contient l'adresse d'une zone mémoire.
La plupart des pointeurs en C++ sont typés, c'est à dire qu'ils représentent à la fois une zone
mémoire et le type d'interprétation de cette zone (zone de stockage d'un réel, d'un entier, d'un
objet, d'une fonction, ...). Pour déclarer une variable de type pointeur, il suffit de faire précéder
son identificateur du caractère *.

        Syntaxe :       type   *identificateur_de_variable;

        Remarques :     La valeur d'une variable pointeur peut être soit la valeur réservée NULL
                        (pointeur sur rien), soit l'adresse d'une variable.
                        L'erreur la plus fréquente est d'omettre l'initialisation des variables
                        pointeurs qui pointent alors n'importe où en mémoire (données, code,
                        système d'exploitation, etc ...).
                        Attention : déclarer une variable pointeur revient à réserver un
                        emplacement qui permet de stocker une adresse, mais pas l'emplacement
                        pointé par cette adresse (voir opérateur new).

        Exemples :      int *pi; définit une variable pi qui contient l'adresse d'un emplacement
                        mémoire pouvant contenir un entier,
                        double *px; définit un pointeur px sur un double,
                        float **pa; définit une variable pa qui pointe sur un pointeur qui pointe
                        sur un float.

       3.1.2 Pointeur sur void
        Le type particulier "pointeur sur void" définit un pointeur qui pointe sur une variable sans
type prédéfini.

        Syntaxe :       void *identificateur_de_variable;

       Exemple :       void *p;




                                                                                                   23
       3.1.3 Opérateur &
       L'opérateur & crée un pointeur sur une variable. & est un opérateur unaire ayant comme
opérande un identificateur de variable ou de fonction. Il renvoie un pointeur void qui peut par
conséquent être affecté à toute variable pointeur.

       Syntaxe :       &identificateur

       Remarque :      On dit aussi que & retourne l'adresse de son opérande.

       Exemple :       int i, *pi = &i; le pointeur sur un entier pi contient l'adresse de la variable
                       entière i.

       3.1.4 Opérateur *
         L'opérateur * (appelé parfois opérateur de déréférenciation) permet d'accéder au contenu
de la zone mémoire dont l'adresse est dans un pointeur. Il suffit pour cela de faire précéder
l'identificateur du pointeur par le symbole *.

       Syntaxe :       *variable_pointeur

       Remarque :      on dit souvent que l'opérateur * permet de passer du pointeur à la variable
                       pointée.

       Exemple :       int *pi, i = 4, j;     // la variable i contient 4
                       pi = &i;               // pi contient l'adresse de i : il pointe sur i
                       (*p)i ++;              // le contenu pointé par pi (donc i) est incrémenté
                       j = *pi;               // la valeur 5 est affectée à j

       3.1.5 Variables dynamiques, opérateurs new et delete
         L'opérateur new crée une variable dynamique et initialise un pointeur sur cette variable.
La zone allouée par new n'est pas initialisée automatiquement, mais pour faciliter l'initialisation,
il est possible de spécifier une valeur entre parenthèses.

       Syntaxe :       pointeur = new type (valeur);

       Remarques :     En cas de succès, new retourne l'adresse de la variable créée ; en cas
                       d'échec, (par exemple lorsqu'il n'y pas assez de mémoire disponible), new
                       retourne la valeur NULL.
                       Comme pour toutes les variables, c'est au programmeur de veiller à leur
                       initialisation. Dans le cas de variables dynamiques celui-ci doit veiller à
                       initialiser à la fois le pointeur sur la variable dynamique et la variable
                       dynamique elle-même.

       Exemple :       double *pi *e;       // pointeurs sur un double, non initialisés
                       pi = new double;     // réservation mémoire et initialisation du pointeur
                       *pi = 3.1415927;     // initialisation de la variable dynamique
                       e = new double (2.71828); // méthode plus rapide, rationnelle et sûre

A l'inverse de new, delete libère la mémoire d'une variable dynamique.
24
       Syntaxe :       delete pointeur;

       Remarques :     L'opérande de delete doit être un pointeur initialisé par new.
                       Si une tentative d'allocation par new échoue, le pointeur retourné par new
                       (NULL) peut quand même être utilisé avec delete.
                       Lorsque la variable pointée est un tableau, il est préférable d'utiliser
                       l'opérateur delete [ ].

       Exemple :       (suite de l'exemple précédent)
                       delete pi;             // libération mémoire
                       e = delete e;          // plus rationnel puisque e pointe sur NULL

       3.1.6 Pointeurs constants
       Pour déclarer un pointeur constant, il faut, lors de la déclaration de ce pointeur, placer le
mot réservé const entre le caractère * et son identificateur.

       Exemple :       int z = 10;
                       int * const y;         // y est un pointeur constant sur un entier

        De même, pour déclarer un pointeur sur une donnée constante, il faut placer le mot
réservé const avant le caractère * et l'identificateur du pointeur.

       Exemple :       const int *y;          // y est un pointeur sur un entier constant

       L'instruction *y = z; est autorisée dans le premier exemple, mais pas dans le second.
Inversement, l'instruction y = &z est correcte dans le second exemple mais pas dans le premier.

       3.1.7 Opérations sur les pointeurs
       Il est possible de réaliser des additions et des soustractions avec des variables de type
pointeur. Par exemple px + 1 pointe l'emplacement mémoire qui suit celui pointé par px (le
décalage (en octets) réalisé dépend de la taille du type de px : 4 pour int, 8 pour double, ...).

       Exemple 1 :     int *pi, *pj;
                       *(pi+1) = 3;           // affecte 3 dans pj : utilisation très discutable

       Exemple 2 :     while (*s++=*t++);     // copie d'une chaîne t dans une chaîne s


3.2 TABLEAUX

       Un tableau est une donnée structurée dont tous les éléments sont de même type. Les
tableaux peuvent être classés en deux catégories : les tableaux unidimensionnels et les tableaux
multidimensionnels. Un tableau est caractérisé par 3 éléments : son identificateur, son type et sa
dimension. En C++, la dimension d'un tableau est une constante entière qui donne le nombre
d'éléments, le premier élément étant toujours indicé zéro.


                                                                                                   25
       3.2.1 Déclaration
       La déclaration d'un tableau se fait de la manière suivante :

       Syntaxe :        type identificateur [dimension];                 // tableau unidimensionnel
                         ou
                        type identificateur [dim1] [dim2] [dim3]...      // multidimensionnel

       Remarques :      Rappel : le premier indice d'un tableau est toujours 0.
                        Il est possible de ne pas donner de dimension lors de la déclaration d'un
                        tableau. Dans ce cas, aucun espace mémoire n'est alloué. Ceci permet de
                        référencer un tableau dont la dimension est définie ailleurs, par exemple
                        avec l'opérateur new. Lors de la réservation mémoire d'un tableau,
                        l'adresse retournée par new est l'adresse du premier élément de ce tableau.
                        La réservation mémoire d'un tableau par new se fait de la manière
                        suivante : new type [dimension];
                        Les tableaux multidimensionnels sont vectorisés. Par exemple, la
                        déclaration double a[3][6]; conduit à l'équivalence, très utilisée en C++,
                        a[i][j] ≡ a[i*6+j]; (6 groupes de 3 éléments chacun).

       Exemple :        int ti[8];                               // tableau de 8 entiers
                        char tc[80];                             // tableau de 80 caractères
                        extern double td[10];                    // tableau externe de 10 réels
                        unsigned char ima[512][512];             // tableau carré 512´512 (pixels)

                        for (i=0;i<10;i++)                       // initialisation de td
                         td[i] = (double) i;

                        for (i=0;i<512;i++)                      // initialisation de ima
                         for (j=0;j<512;j++)
                          ima[i][j] = 0;

       3.2.2 Relation entre tableaux et pointeurs
        En C++, les pointeurs et les tableaux sont des variables de même nature. Un tableau est
en fait un pointeur constant qui contient l'adresse du premier élément du tableau. La seule
différence entre pointeurs et tableaux réside dans le fait que l'adresse du premier élément du
tableau ne peut être modifiée. Par exemple, s'il est possible d'incrémenter la valeur d'un pointeur
(p++), cela est interdit pour un tableau. Pour fixer les esprits, le tableau suivant présente sous
forme synthétique certaines équivalences d'écriture entre pointeurs et tableaux.

                      test identique à           &(t[0])
                      test l'adresse de            t[0]
                      t+1                   est l'adresse de                t[1]
                      t+iest l'adresse de          t[i]
                      *(t+i) = 1            est identique à               t[i] = 1
                                   t ayant été déclaré par int t[10];
                   ou t ayant été déclaré par int *t; et alloué par t = new int [10] ;



26
       3.2.3 Tableau de pointeurs
               Il est possible de déclarer des tableaux de pointeurs en utilisant la syntaxe :

       Syntaxe :       type *nom[dimension];

       Remarques :     On obtient des tableaux bidimensionnels à structure non régulière ; le
                       nombre de composants est variable.

       Exemple :       char * t1[10];         // t1 est un tableau de 10 pointeurs sur caractère
                                              // à ne pas confondre avec :
                       char (*t2)[10];        // t2 est un pointeur sur un tableau de 10 char

       3.2.4 Chaînes de caractères
        En C++, les chaînes de caractères sont strictement définies comme des tableaux de
caractères terminés par le caractère nul : '\0'. Toutes les remarques sur les tableaux et les
équivalences tableaux-pointeurs s'appliquent donc pour les chaînes de caractères. Les chaînes ne
sont pas directement manipulables car il n'y a pas d'opérateurs prédéfinis dans le langage
(contrairement à Turbo Pascal). Il existe cependant de nombreuses fonctions standards (dont les
prototypes sont définis dans le fichier string.h, voir plus loin bibliothèques standards) qui
permettent de réaliser les principales fonctions :
        −= strcpy (s1, s2) pour recopier une chaîne s2 dans la chaîne s1,
        −= strcmp (s1, s2) pour comparer les deux chaînes s1 et s2 (strcmp renvoie une valeur
           entière négative, nulle ou positive selon que s1 est alphabétiquement plus petite, égale
           ou plus grande que s2),
        −= strlen (s) pour obtenir la longueur de la chaîne s,
        −= strcat (s1, s2) pour concaténer la chaîne s2 à la chaîne s1,
        etc, ...

       3.2.5 Initialisation des tableaux
        Un tableau peut être initialisé au moment de la compilation uniquement si son attribut est
externe ou statique.
        Le cas le plus simple est celui des chaînes de caractères initialisées pendant leur
déclaration. Cependant, des précautions doivent être prises car la manipulation des chaînes de
caractères est bien souvent source d'erreur en C++.

       Exemple 1 :     static char text[6] = "ESPEO"; déclare une chaîne de 6 caractères, les 5
                       premiers prenant respectivement les valeurs 'E', 'S', 'P', 'E', 'O', et le
                       dernier la valeur '\0'. Pour éviter au programmeur de compter les
                       caractères du texte, le compilateur calcule automatiquement la longueur
                       de la chaîne 9tableau de 6 caractères) si l'on tape :
                       static char text[ ] = "ESPEO";

       Exemple 2 :     char chaine[10]; suivi de chaine = "ESPEO"; conduit à une erreur car
                       chaine est un tableau, donc un pointeur constant que l'affectation tente de
                       modifier par l'adresse de la chaîne constante "ESPEO".

       Exemple 3 :     char *chaine; suivi de chaine = "ESPEO" est correct car le pointeur
                       chaine peut être initialisé par l'adresse de la chaîne constante "ESPEO".
                                                                                                   27
                       Mais attention, toute modification sur chaine entraînera la même
                       modification sur la constante "ESPEO" puisque chaine pointe sur cette
                       constante.

        Pour initialiser un tableau de valeurs dans le cas général, ses éléments doivent être
spécifiés un à un sous la forme d'une liste :

       Exemple 4 :     static int chiffres[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
                       Dans le cas où la dimension est spécifiée, le nombre d'éléments de la liste
                       d'initialisation doit être strictement égal à la dimension du tableau.


3.3 STRUCTURES, UNIONS ET ÉNUMÉRATIONS
Les structures et les unions peuvent apparaître comme des extensions du type tableau, dans la
mesure où ce sont des variables dont les éléments peuvent avoir des types différents. Une
variable de type structure ou union est en effet la juxtaposition structurée de plusieurs variables
qui constituent ses champs. Les énumérations permettent de définir des listes de constantes.

       3.3.1 Structures
       Les structures servent donc à regrouper plusieurs variables sous un unique identificateur.

       Syntaxe :   struct nom_de_structure
                   {
                     type1 nom_de_champ_1;
                     type2 nom_de_champ_2;
                     type3 nom_de_champ_3;
                     ...
                   };
       Remarques : Une fois défini, le nom de la structure devient un nouveau type et s'utilise
                   comme les types prédéfinis.
                   Pour accéder aux champs d'une structure, on utilise l'opérateur point (.)
                   en regroupant le nom de la variable structure et celui du champ auquel on
                   veut accéder : variable_structure.nom_de_champ,
                   Lorsqu'on utilise des pointeurs sur des structures, la notation -> permet de
                   simplifier    l'écriture   :    (*variable_structure).nom_de_champ         ≡
                   variable_structure->nom_de_champ,
                   On peut considérer les structures comme des classes simplifiées (sans
                   fonctions membres). Mais comme il n'existe pas de fonction membre
                   pour accéder aux champs, on perd le bénéfice de l'abstraction des
                   données.

       Exemple :       struct date            // déclaration d'une structure date
                       {
                         int jour;
                         char mois[10];
                         int annee;
                       };


28
                      date   d,             // déclaration d'une variable d de type date
                             *pd;           // déclaration d'un pointeur pd de type date
                      d.annee = 1995;       // initialisation du champ année de d
                      pd->jour = 19;        // équivalent à (*pd).jour = 19;

       3.3.2 Unions
       Le type union est une extension du type structure qui permet de manipuler une même
zone mémoire selon différentes interprétations. Comme les structures, les unions contiennent des
champs. Mais les zones mémoires de ces champs ont la même adresse, le compilateur réservant
suffisamment d'espace mémoire pour stocker le plus grand des champs de l'union.

       Syntaxe :      union nom_d'union
                      {
                        type1 nom_de_champ_1;
                        type2 nom_de_champ_2;
                        type3 nom_de_champ_3;
                        ...
                      };

       Remarque :     On ne peut accéder simultanément qu'à un seul champ d'une union.

       Exemple :      union valeur          // déclaration de l'union valeur
                      {
                        int entier;
                        double reel;
                      };

                      valeur v;             // définition d'une variable de type valeur

                      v.entier = 4;         // v est utilisée comme un entier
                      v.reel = 3.1415;      // v est utilisée comme un réel

       3.3.3 Énumérations
        Une liste de constantes peut se définir par énumération. Le premier identificateur de la
liste a la valeur 0 par défaut, les valeurs des identificateurs suivants étant incrémentées pour
chaque nouvel identificateur.

       Syntaxe :      enum nom_d'enumeration
                      {
                        identificateur_1,
                        identificateur_2,
                        identificateur_3,
                        ...
                      }

       Remarques : Il n'y a aucune vérification de débordement.
                    Il est possible de modifier les valeurs par défaut des identificateurs :
                    enum couleur {JAUNE=-2, ROUGE, VERT=4, BLEU};
                                                                                              29
       Exemple :       enum jour {LUNDI, MARDI, MERCREDI, JEUDI,
                              VENDREDI, SAMEDI, DIMANCHE};

                       jour j1, j2;

                       j1 = MARDI;             // j1 vaut MARDI qui vaut 1
                       j2 = j1+3;              // j2 vaut VENDREDI qui vaut 4


3.4 DÉFINITION DE TYPE : TYPEDEF
        Il est possible de définir de nouveaux types en C++ à l'aide du mot réservé typedef. Ceci
est particulièrement utile lorsque l'on utilise des types imbriqués les uns dans les autres. La
syntaxe d'utilisation de typedef, similaire à celle de déclaration des variables, est présentée dans
les exemples suivants :

        Syntaxe :       typedef type identificateur;

       Exemples :      typedef unsigned char BYTE; // déclaration du type BYTE
                       typedef BYTE TAB[10];       // TAB est un tableau sur 10 BYTEs
                       typedef TAB *PTAB;          // PTAB est un pointeur sur TAB


3.5 FONCTIONS
         Certains langages comme le Pascal distinguent deux sortes de sous-programmes : les
fonctions (qui retournent une valeur) et les procédures (qui ne retournent aucune valeur). A
l'inverse en C++, tous les sous-programmes sont des fonctions. Un fichier source C++ est donc
une succession de fonctions. Le programme principal est lui-même une fonction, dont
l'identificateur est obligatoirement main, spécifiant que cette fonction principale doit être
exécutée en premier.

       3.5.1 Déclaration et définition
        La démarche naturelle consiste à déclarer toutes les fonctions avant de les utiliser ou les
définir. Ainsi, il est possible pour le compilateur de lever d'éventuelles ambiguïtés et de réaliser
le maximum de vérifications.

         La déclaration d'une fonction permet au compilateur de connaître le prototype de celle-
ci, c'est à dire le type des paramètres qu'elle utilise et de la valeur retournée. Ainsi, lorsque cela
est possible, le compilateur est en mesure de faire les conversions de types nécessaires à une
transmission correcte des paramètres et de la valeur renvoyée.

        La syntaxe de déclaration d'une fonction est la suivante. Le nom de la fonction
(identificateur) doit être précédé du type de la valeur retournée par la fonction, et suivi (entre
parenthèses) de la liste de ses paramètres.

        Syntaxe :       type_retourné identificateur (type1, type2, type3, ...);
                       ou
                       type_retourné identificateur (type1 param1, type2 param2, ...);
30
        Remarques :     Si une fonction ne retourne pas de valeur, elle doit être déclarée void.
                        Si une fonction n'a pas de paramètre, il suffit de la déclarer avec une liste
                        vide ( ), ou avec le type void entre parenthèses (void).

       Exemple :       double sin (double);
                       float moyenne (int n, float *x);
                       void init (double);

        Attention :     Avec certains compilateurs, l'utilisation d'une fonction non déclarée au
                        préalable conduit à la définition d'un prototype par défaut, induit par le
                        type des paramètres rencontrés lors du premier appel. De même, si le type
                        de la valeur retournée par la fonction n'est pas défini, le type entier est
                        pré-supposé. Pour lever toute ambiguïté, il est donc indispensable de
                        déclarer les fonctions avant de les utiliser.

        La syntaxe de la définition des fonctions est la suivante. Vient tout d'abord l'entête de la
fonction, rappel éventuel de sa déclaration mais en précisant obligatoirement le nom des
paramètres et en ne mettant pas de point-virgule. Juste après, le corps de la fonction est placé
entre accolades ({}) et contient les déclarations locales et les instructions. Lorsque la fonction
retourne une valeur, il faut utiliser, en n'importe quel point de cette fonction, l'instruction return
qui permet de sortir immédiatement.

        Syntaxe :      type_retourné identificateur (type1 param1, type2 param2, ...)
                       {
                         ...
                       }

       Exemple :       int divisible (int a, int b)
                       {
                        if ( (b == 0) || ((a%b) != 0) ) return (0);
                        else return (1);
                       }

       3.5.2 Paramètres d'entrée : transmission par valeur
        Par défaut, le passage des paramètres dans les fonctions se fait par valeur. Ceci signifie
que ce sont les valeurs des paramètres qui sont transmises aux fonctions, et qu'aucune
modification de ces valeurs dans les fonctions n'est répercutée dans la fonction d'appel (lors de
l'appel d'une fonction, les paramètres sont recopiés dans une zone mémoire qui est libérée
lorsqu'on en sort). Cette transmission des paramètres par valeur est utilisée pour les paramètres
d'entrée, qui ne sont pas modifiés dans les fonctions.

       3.5.3 Paramètres de sortie : transmission par adresse
        Pour modifier des valeurs de paramètres à l'intérieur d'une fonction, on peut utiliser des
pointeurs. En effet, en transmettant l'adresse de variables en paramètres, il est possible, grâce à
l'opérateur *, d'accéder aux valeurs de ces variables dans les fonctions. On ne transmet donc plus
directement les valeurs des variables, mais leurs adresses. A partir de ces adresses, on accède
aux contenus. Toute modification de la valeur est donc automatiquement répercutée dans la

                                                                                                    31
fonction d'appel, puisque c'est le même emplacement mémoire qui est modifié. Ce mécanisme
de transmission par adresse est utilisé pour les paramètres de sortie des fonctions.

        Syntaxe :       type nom_de_fonction (type1 *param1, type2 *param2, ...)

        Remarques :     Dans tout le corps de la fonction, parami représente l'adresse du
                        paramètre, et *parami le contenu pointé par parami.
                        Quand la fonction est appelée, il faut prendre garde à transmettre les
                        variables par adresse en les faisant précéder de l'opérateur & (adresse de
                        la variable).

        Exemple :       void swap (int *i, int *j)
                        {
                         int t = *i;
                         *i = *j; *j = t;
                        }

                        void main (void)
                        {
                         int a = 10, b = 20;
                         swap (&a, &b);                 // a = 20 et b = 10
                        }

        3.5.4 Paramètres de sortie : transmission par référence
        Le mécanisme de la transmission de paramètres par référence permet, comme l'utilisation
des pointeurs, de modifier la valeur de variables dans les fonctions. La transmission par
référence utilise des paramètres de type "référence". Une variable de type référence est une
variable qui contient l'adresse d'une autre variable, et qui peut être utilisée comme cette variable.
Une variable référence doit être initialisée lors de sa déclaration (avec l'adresse d'une autre
variable) et cette affectation est définitive. Pour déclarer une variable de type référence, on fait
précéder son nom du caractère &, et on le fait suivre par une affectation. Par exemple, int i, &j =
i; déclare une variable référence j sur l'entier i, qui peut être utilisée indifféremment à la place de
i. Ces variables références sont très utilisées pour la transmission de paramètres par adresse dans
les fonctions car elle simplifie grandement l'écriture de la transmission par adresse. La syntaxe
est alors :

        Syntaxe :       type nom_de_fonction (type1 &param1, type2 &param2, ...)

        Remarques :     Pour modifier le contenu d'un paramètre dans une fonction, il suffit de
                        faire précéder sa déclaration par le caractère &.
                        Lors de l'appel de la fonction, les paramètres peuvent être écrits
                        directement.
                        L'utilisation des variables références dans la transmission des paramètres
                        par adresse est identique à celle des pointeurs. Cependant, elle simplifie
                        énormément l'écriture et réduit ainsi les risques d'erreur (& ou * oublié ou
                        en trop).
                        La valeur de retour d'une fonction peut être aussi de type référence, ce qui
                        autorise à mettre une fonction dans le membre de gauche d'une
                        affectation.
32
       Exemple :      void swap (int &i, int &j)
                      {
                       int t = i;
                       i = j; j = t;
                      }

                      void main (void)
                      {
                       int a = 10, b = 20;
                        swap (a, b);         // a = 20 et b = 10
                      }

       3.5.5 Paramètres multivalués : transmission par référence constante
       Pour transmettre en entrée des paramètres de grosse taille sans recopie temporaire
(optimisation de la mémoire et de l'exécution du programme), il faut utiliser des paramètres par
référence constante. Ainsi, les paramètres, bien que transmis par référence, ne peuvent être
modifiés dans la fonction. Exemple : void print (const gros_type & grosse_variable);

       3.5.6 Valeurs par défaut
         En C++, il est possible d'omettre certains paramètres lors de l'appel d'une fonction, et
d'utiliser alors des valeurs par défaut pour ces paramètres.

       Syntaxe :      type nom (type1 param1, type2 param2=val2, type3 param3=val3, ...);

       Remarques :     Il faut noter que dans la déclaration de la fonction, les paramètres par
                       défaut doivent obligatoirement être les derniers de la liste de paramètres.
                       De même, lors de l'appel de la fonction, on ne peut omettre que le ou les
                       derniers paramètres, parmi ceux qui ont une valeur par défaut.
                       Il est possible, dans la déclaration d'une fonction, de définir des
                       paramètres qui n'étaient pas déjà définis.

       Exemple :      double division (double a, double b=1)
                      {
                       if (b != 0) return (a/b);
                      }
                      double division (double a = 0, double b);     // par défaut, a=0 et b=1

                      void main (void)
                      {
                       double c = division ();                      // c = 0;
                      }

       3.5.7 Paramètres de la ligne de commande
        Il est possible de récupérer dans un programme C++ les paramètres de la ligne de
commande d'un programme, c'est à dire la chaîne de caractères qui a déclenché son exécution
(nom du programme et paramètres éventuels). Pour cela, il suffit de déclarer deux paramètres à
la fonction main : argc et argv. Le premier paramètre, entier, argc, contient le nombre de champs
qui composent la ligne de commande (au minimum 1, le nom du programme). Le second
                                                                                                33
paramètre, argv, est un tableau de chaînes de caractères qui permet d'accéder à chacun de ces
champs. L'exemple suivant affiche tous les champs de la ligne de commande.

       Exemple :     #include <iostream.h>
                     void main (int argc, char *argv[])
                     {
                       int i;
                       for (i=0;i<argc;i++)
                              cout << "Paramètre " << i << " : " << argv[i] << endl;
                     }


3.6 MOTS CLEFS
       Allocation dynamique
       Déclaration
       Définition
       Déréférenciation
       Énumération
       Libération dynamique
       Paramètre d'entrée
       Paramètre de sortie
       Paramètre multivalué
       Pointeur
       Prototype
       Structure
       Tableau
       Transmission par adresse
       Transmission par référence
       Transmission par référence constante
       Transmission par valeur
       Type référence
       Union
       Variable dynamique




34
               4. CONSTRUCTEURS ET
            DESTRUCTEURS, SURCHARGE


4.1 CONSTRUCTEURS ET DESTRUCTEURS
        Comme toute variable d'un programme C++, l'instance d'une classe est créée au moment
de sa déclaration, et supprimée à la fin du bloc où elle est déclarée. La création d'une instance
correspond à la réservation d'emplacements mémoire pour stocker les données membres de la
classe, tandis que sa destruction libère les zones allouées. Réalisées automatiquement pour des
données membres non dynamiques, ces allocations et libérations de mémoire doivent être
contrôlées par le programmeur lorsque les données membres sont des pointeurs sur des variables
qui nécessitent une réservation dynamique de mémoire. Pour cela, on utilise deux fonctions
membres spéciales appelées constructeur et destructeur.

       4.1.1 Constructeurs
        Un constructeur est une méthode appelée systématiquement lors de la création d'une
instance d'une classe, c'est à dire lors de sa déclaration ou de sa création dynamique par new,
juste après la réservation de mémoire pour les données membres. Un constructeur optimise les
allocations mémoires liées aux variables dynamiques, et initialise les données membres des
classes. Par exemple, la création d'une instance d'une classe avec des données membres
pointeurs réserve bien évidemment de la place pour les pointeurs, mais non pour les zones
pointées. Le constructeur sert donc à la fois à réserver ces zones pointées et à les initialiser. Pour
rendre systématique l'initialisation des données membres des classes, toute classe doit contenir
un constructeur.
Comme les autres fonctions membres, les constructeurs sont déclarés dans la définition des
classes. Ils peuvent être inline ou non, et avoir des paramètres par défaut. Cependant, ils ne
retournent pas de valeur et on ne peut pas obtenir leur adresse. L'identificateur d'un constructeur
est identique à celui de la classe.

        Syntaxe :      class X
                       {
                               ...
                         public:
                               X (paramètres);                 // constructeur de la classe X
                               ...
                       }
                       X::X (paramètres)
                       {
                               ...
                       }

        Remarques :     Le constructeur est automatiquement appelé lors de la déclaration de
                        l'objet, et il est interdit d'appeler des constructeurs en dehors de cette
                        déclaration.
                                                                                                    35
                       Les objets qui ont des constructeurs avec des paramètres peuvent être
                       initialisés en passant les valeurs des paramètres entre parenthèses à la
                       suite de la déclaration de l'objet. Exemple : identificateur objet (val1,
                       val2, val3, ...).
                       Pour simplifier l'écriture, un constructeur avec un seul paramètre peut
                       être appelé en utilisant simplement le signe =. Exemple : identificateur
                       objet = 4;
                       Les paramètres des constructeurs peuvent être initialisés par défaut.
                       Les constructeurs peuvent initialiser les données membres de deux
                       manières : soit en les initialisant dans le corps du constructeur, soit en les
                       initialisant par une liste placée avant le corps du constructeur. Exemple :
                       complx (double r, double i = 0.0) {re = r; im = i; }ou complx (double r,
                       double i = 0.0) : re (r), im (i) { instructions; }

       4.1.2 Constructeurs copies
        Les constructeurs copies sont utilisés pour faire une copie d'un objet d'une classe dans un
objet de même type classe. Ils n'ont qu'un seul paramètre de type référence constante à leur
propre classe (les paramètres de même type que la classe ne sont pas admis : seules sont
autorisées les références constantes).

       Exemple :       class X
                       {
                                int a, b;
                         public:
                                X (const X&);
                       }
                       X::X (const X& x)
                       {
                         a = x.a;
                         b = x.b;
                       }

       Remarques :     Si un constructeur copie n'existe pas et est néanmoins nécessaire au cours
                       de l'exécution d'un programme, la plupart des compilateurs en définissent
                       un automatiquement.
                       Un constructeur copie correspond par défaut à l'affectation de la zone de
                       données d'un objet dans la zone de données d'un autre objet.
                       Il est indispensable de définir explicitement un constructeur copie lorsque
                       la zone de données contient un pointeur sur une zone mémoire qu'il faut
                       aussi recopier (données membres pointeurs sur des variables
                       dynamiques).
                       Les constructeurs copies sont les seules fonctions où les données
                       membres d'une classe sont utilisées directement.

       4.1.3 Destructeurs
        A l'inverse d'un constructeur, un destructeur est une méthode qui est appelée
systématiquement lors de la destruction d'une instance d'une classe, c'est à dire en fin de bloc ou
lors de l'utilisation de l'opérateur delete. Il est appelé juste avant la suppression de la zone de

36
données et sert à contrôler la libération des zones mémoires allouées pour les variables
dynamiques.
        Les destructeurs sont eux-aussi déclarés dans la définition des classes. Ils peuvent être
inline ou non, mais n'ont pas de paramètre et ne retournent pas de valeur. Comme les
constructeurs, on ne peut obtenir leur adresse. L'identificateur d'un destructeur est identique à
celui de la classe, mais il est précédé du caractère tilde (~) pour le différencier du constructeur.

        Syntaxe :       class X
                        {
                                ...
                          public:
                                X (paramètres);          // constructeur de la classe X
                                ~X ( );                  // destructeur de la classe X
                                ...
                        }
                        X::~X ()
                        {
                                ...
                        }


4.2 POINTEUR THIS
         Le pointeur "this" est un pointeur spécial, automatiquement ajouté aux données membres
et initialisé par le constructeur d'une classe. Dès que l'instance d'une classe est créée, le pointeur
this de cette classe contient l'adresse de l'objet instancié. Il identifie ainsi toujours la classe elle-
même. Ce pointeur this est passé comme paramètre caché à toutes les fonctions membres d'une
classe, et ne peut être utilisé que dans les fonctions membres. Par exemple, utiliser une donnée
membre a dans une fonction membre est équivalent à utiliser this->a. Pour éviter toute
ambiguïté, il est interdit au programmeur de déclarer un pointeur this comme donnée membre
d'une classe et de l'initialiser.
         Le pointeur this permet de résoudre certaines identifications équivoques. Par exemple,
une fonction membre ne peut normalement pas avoir de paramètre ayant même identificateur
qu'une donnée membre. Cela est néanmoins possible si partout dans la fonction la donnée
membre est référencée par this->.

        Exemple :       class X
                        {
                               int a;
                         public:
                               X (int a) {this->a = a;}
                               ...
                        }


4.3 SURCHARGE
         La surcharge consiste à donner le même identificateur à des fonctions ou des opérateurs
qui ont des paramètres différents. En C++, il est possible de redéfinir les fonctions et la plupart
des opérateurs standards. Généralement, fonctions et opérateurs sont surchargés pour étendre
leur utilisation sur des types de données différents de ceux pour lesquels ils ont été écrits.
                                                                                                       37
       4.3.1 Surcharge de fonctions
        Pour surcharger une fonction, il suffit de la déclarer plusieurs fois avec le même
identificateur, à condition que ces déclarations diffèrent par le type ou le nombre d'arguments de
la fonction. A chaque appel, le compilateur choisit alors la fonction à utiliser en fonction du type
et du nombre de paramètres de l'appel. On appelle polymorphisme [plusieurs formes] cette
faculté de pouvoir redéfinir le corps d'une même fonction.

      La surcharge s'applique à toute fonction d'un programme C++, donc aussi, bien
évidemment, aux fonctions membres des classes.

       Exemple :       #include <iostream.h>
                       void print (int i)
                       {
                        cout << "Entier : " << i << endl;
                       }

                       void print (double f)
                       {
                        cout << "Réel : " << f << endl;
                       }

                       void print (char *c)
                       {
                        cout << "Caractère : " << *c << endl;
                       }

                       void main (void)
                       {
                        print (2);             // appelle print (int)
                        print (2.718); // appelle print (double)
                        print ("Deux");        // appelle print (char*)
                       }

                       Ces trois fonctions print au même identificateur réalisent la même
                       opération sur des types de données différents.

       Pour le compilateur, deux fonctions sont identiques si les trois conditions suivantes sont
respectées:
       1. elles ont le même identificateur,
       2. elles sont déclarées dans le même bloc,
       3. les listes de leurs arguments sont identiques.

Ainsi, pour pouvoir surcharger des fonctions sans ambiguïté, il faut remarquer que :
        −= la valeur retournée par deux fonctions ne suffit pas à les distinguer (deux fonctions
           qui diffèrent seulement par la valeur retournée ne doivent pas avoir le même
           identificateur),
        −= un type déjà connu mais redéfini par typedef n'est pas considéré comme un nouveau
           type,
        −= pointeurs et tableaux sont considérés comme identiques.
38
En conclusion, seuls le nombre et les types des arguments permettent de distinguer deux
fonctions surchargées.

       4.3.2 Surcharge de constructeurs
        La notion de surcharge s'applique aussi aux constructeurs. Il est ainsi possible de prévoir
plusieurs constructeurs pour une même classe, ceux-ci devant se différencier par le type ou le
nombre des paramètres. Cette possibilité, très utilisée, permet une grande souplesse lors de la
création de l'instance d'une classe. Il est ainsi courant de définir au moins deux constructeurs par
classe : un sans paramètre qui initialise automatiquement les données membres de la classe avec
des valeurs par défaut, l'autre avec paramètres pour initialiser les données membres avec des
valeurs spécifiées par l'utilisateur.

       4.3.3 Surcharge d'opérateurs
         L'écriture utilisée pour travailler sur des instances d'objet est souvent lourde et peu
lisible. Pour remédier à cela, les opérateurs classiques du C++ peuvent être redéfinis, amenant
ainsi une programmation lisible et agréable. Par exemple, avec la déclaration complex a, b, c;,
l'instruction a = b + c; est plus lisible que a.addition (b,c);. L'opérateur + est redéfini, celui-ci
restant bien entendu utilisable pour l'addition de deux nombres non complexes. Remarque :
l'opérateur classique + dépend lui même du type des opérandes : en effet l'addition de deux
entiers n'est pas implémentée de la même manière que l'addition de deux réels.

        Un opérateur surchargé est appelé fonction opérateur. Sa syntaxe de déclaration est
identique à celle des fonctions membres. Il est simplement déclaré avec le préfixe operator. Un
opérateur surchargé est différent d'une fonction surchargée, mais comme pour ces fonctions, la
sélection des opérateurs surchargés se fait sur le nombre et le type des opérandes utilisés.

        Les opérateurs qui peuvent être surchargés sont les suivants. Certains de ces opérateurs
sont unaires (un seul opérande), d'autres binaires (deux opérandes), et certains à la fois unaires et
binaires :
            +        −        *         /        %         ^      &           |         ~
            !        =        <         >       +=        -=      *=         /=       %=
           ^=       &=       |=        >>       <<       >>=     <<=        ==         !=
           <=       >=      &&          ||      ++        −−       ,          *        ->
           ()       []      new      delete

       4.3.4 Règles générales de surcharge des opérateurs
       Deux règles fondamentales régissent la surcharge des opérateurs unaires ou binaires :
       1. si un opérateur est membre d'une classe, son premier opérande est toujours du
          type de la classe à laquelle il appartient. Le type du second opérande, s'il existe,
          doit correspondre au type déclaré lors de la surcharge de l'opérateur,
       2. quand un opérateur n'est pas membre d'une classe, au moins un de ses
          opérandes doit être de type classe. Comme précédemment, le type du second
          opérande, s'il existe, doit correspondre au type déclaré lors de la surcharge de
          l'opérateur.



                                                                                                   39
       Habituellement, les opérateurs surchargés sont utilisés en appliquant la syntaxe classique
des opérateurs. Il est cependant possible de les utiliser de manière explicite, même si cela nuit
souvent à la lisibilité des programmes.

       4.3.5 Surcharge d'opérateurs unaires
        Les opérateurs unaires qui peuvent être redéfinis sont :
               ++      −−     !       ~       +       −       *      &
Les quatre premiers sont essentiellement unaires tandis que les quatre derniers peuvent
également être binaires. Comme ils n'ont qu'un paramètre, ces opérateurs ne peuvent être
redéfinis que pour des opérandes objet (voir les règles générales de surcharge des opérateurs).
Un opérateur unaire redéfini est un opérateur fonction membre (sans paramètre) ou amie
(paramètre de type classe) d'une classe. Les syntaxes de déclaration et de définition restent celles
des fonctions membres ou amies.

       Syntaxe :       type operator ( );                     // opérateur fonction membre
                       ou
                       friend type operator (classe);         // opérateur fonction amie

       Utilisation :        objet ou objet.operator    ()     // opérateur fonction membre
                       ou
                            objet ou operator   (objet)       // opérateur fonction amie

       Remarques :     Pour les fonctions membres ou amies, les deux écritures présentées
                       ci-dessus sont totalement équivalentes. La première est appelée notation
                       opératoire ou implicite, la seconde notation fonctionnelle classique ou
                       explicite.
                       Pour les opérateurs ++ et −−, on peut définir deux sens différents comme
                       ++x et x++. La déclaration suffixe est réalisée en déclarant deux
                       arguments respectivement de type classe et entier pour les opérateurs
                       surchargés hors d'une classe. Pour les opérateurs membres d'une classe,
                       un seul paramètre de type entier suffit pour spécifier l'opérateur suffixe.
                       Dans tous les autres cas, l'opérateur est considéré comme un opérateur
                       préfixe.

       Exemple :       #include <iostream.h>

                       class complx
                       {
                        private:
                               double re,             // partie réelle
                                       im;            // partie imaginaire
                         public:
                               complx (double r = 0, double i = 0) { re = r; im = i; }
                               complx operator - ( ); // opérateur conjugué
                               friend double operator & (const complx &); // op. module
                       };

                       complx complx::operator - ( )
                       {
40
                         complx         resultat;
                        resultat.re = re;
                        resultat.im = - im;
                         return resultat;
                       }

                       double operator & (const complx & c)
                       {
                        return sqrt (c.re*c.re + c.im*c.im);
                       }

                       void main (void)
                       {
                        complx x (4,4);
                        complx y = -x;                        // appel implicite
                        complx z = x.operator - ( );          // appel explicite
                        double m = &x;                        // appel implicite
                        double n = operator & (x);            // appel explicite
                       }

       4.3.6 Surcharge de l'opérateur d'affectation =
        Lorsque les données membres d'une classe contiennent des pointeurs sur des variables
dynamiques, il est indispensable de redéfinir l'opérateur d'affectation =. En effet, dans la plupart
des cas, il faut non seulement recopier les données membres de la classe, mais aussi recopier les
valeurs des variables dynamiques pointées, après leur avoir réservé un emplacement mémoire
propre (cf constructeur copie).

         Lors de la surcharge de l'opérateur =, il est judicieux de passer l'unique paramètre en
référence constante pour ne pas risquer de le modifier. De même, on peut donner à cet opérateur
une valeur de retour du même type classe. Cela permet de "cascader" les affectations, comme
c'est l'usage en C++.

       Exemple :       class X
                       {
                         ...
                         public:
                               X operator = (const X&);
                       }

                       X X::operator = (const X & x)
                       {
                         ...
                         return (*this);
                       }

                       void main (void)
                       {
                        X x1, x2, x3, x4;
                         ...
                                                                                                  41
      x1 = x2 = x3 = x4;
     }




42
       4.3.7 Surcharge d'opérateurs binaires
         Les opérateurs binaires qui peuvent être redéfinis sont :
         +      -         *        /      %          ^        &         |         ||      &&
         =      <         >       +=      -=        *=        /=      %=         ^=       &=
         |=    <<        >>       <<=    <<=        ==        !=       <=        >=
Les opérateurs binaires se définissent de manière analogue aux opérateurs unaires. Il s'agit
encore, soit d'opérateurs fonctions membres, soit d'opérateurs fonctions amies de classes. Un des
deux opérandes doit obligatoirement être une instance d'une classe, le deuxième pouvant être
soit l'instance d'une classe (la même ou une autre), soit un autre type (voir les règles générales de
surcharge des opérateurs). Les syntaxes de déclaration et de définition restent celles des
fonctions membres ou amies.

       Syntaxe :        retourné operator (type);                      // opérateur fn membre
                        ou
                        friend retourné operator (classe, type);       // opérateur fonction amie

        Utilisation :   objet    var   objet.operator     (var)        //opérateur fn membre
                        ou
                        objet   var    operator     (objet, var)       // opérateur fonction amie

        Remarques :     Dans la syntaxe d'utilisation des opérateurs binaires ci-dessus, var
                        représente soit un objet, soit un paramètre de type quelconque.
                        Comme pour les opérateurs unaires, les deux écritures implicites et
                        explicites sont complètement équivalentes.
                        Lorsqu'une redéfinition concerne un opérateur dont le membre de gauche
                        n'est pas une classe, cette redéfinition doit être obligatoirement réalisée
                        par une fonction amie. En effet, le premier opérande d'un opérateur
                        binaire redéfini dans une classe est nécessairement une instance de cette
                        classe.

       Exemple :        #include <iostream.h>
                        class complx
                        {
                          private:
                                double        re,     // partie réelle
                                              im;     // partie imaginaire
                          public:
                                complx (double r = 0, double i = 0) { re = r; im = i; }
                                complx operator + (const complx &);
                                friend complx operator + (double, const complx &);
                        };

                        complx complx::operator + (const complx& c)
                        {
                          complx         resultat;
                         resultat.re = re + c.re;
                         resultat.im = im + c.im;
                          return resultat;
                                                                                                    43
                       }

                       complx operator + (double r, const complx& c)
                       {
                         complx         resultat;
                        resultat.re = r + c.re;
                        resultat.im = r + c.im;
                         return resultat;
                       }

                       void main (void)
                       {
                        complx x (4,4);
                        complx y (6,6);
                        complx t = x.operator + (y);          // appel explicite
                        complx u = x + y;                     // appel implicite
                        complx v = operator + (3.0, x);
                         complx w = 3.0 + x;
                       }

       4.3.8 Surcharge des opérateurs [ ] et ( )
       La redéfinition de l'opérateur d'indiçage permet d'utiliser une classe tableau définie par le
programmeur, tout en conservant la notation traditionnelle des tableaux. Cet opérateur [ ] admet
comme un paramètre de type quelconque, ce qui permet de travailler avec des tableaux de
tableaux. Une utilisation astucieuse de cet opérateur consiste à définir une classe tableau qui
conduise à une véritable notion de tableau dynamique avec vérification de débordement. De
même, en donnant une valeur de retour qui soit de type référence à l'opérateur [], il sera possible
de mettre cet opérateur [] à gauche d'un signe d'affectation, et donc de modifier le contenu de
l'élément considéré du tableau.

       Exemple :       class CTab
                       {
                         private:
                               double t[TMAX];
                         public:
                               double& operator [] (int);
                       };

                       double& CTab::operator [] (int i)
                       {
                        if ( (i<0) || (i>TMAX-1) )
                                cerr << "Débordement de tableau" << endl;
                         return t[i];
                       }

                       void main (void)
                       {
                         ...
                        t[4] = t[3];
44
                       }

         L'opérateur ( ) peut lui aussi être redéfini. Il correspond à la fonction operator ( ). Son
utilisation est tout à fait similaire à celle de l'opérateur [ ].

       4.3.9 Surcharge des opérateurs new et delete
      Comme pour les autres opérateurs, il est possible de redéfinir new et delete, et par ce
moyen fournir une utilisation simplifiée de l’allocation dynamique d’objets.

       4.3.10 Correspondance des paramètres
        Quand une fonction ou un opérateur surchargé est appelé dans un programme C++, le
compilateur choisit la déclaration de fonction ou d'opérateur qui correspond "le mieux" à l'appel.
Pour cela, le compilateur compare les paramètres passés lors de l'appel avec ceux de la
déclaration. Trois cas de figure se présentent alors :
        1. la correspondance est exacte,
        2. il n'y a pas de correspondance,
        3. il y a une correspondance ambiguë.
Les deux premiers cas ne posent pas de problème. Quand au troisième, il faut absolument le
bannir pour éviter tout fonctionnement incorrect ou aléatoire des programmes.


4.4 MOTS CLEFS
       Appel explicite
       Appel fonctionnel
       Appel implicite
       Appel opératoire
       Constructeur
       Constructeur copie
       Destructeur
       Fonction opérateur
       Opérande
       Opérateur binaire
       Opérateur unaire
       Pointeur this
       Polymorphisme
       Portée
       Redéfinition
       Règle de surcharge
       Surcharge




                                                                                                  45
                                    5. HÉRITAGE

        Les classes peuvent être définies et utilisées de manière autonome, chaque classe
constituant un ensemble homogène indépendant de toute autre classe. Cependant, il peut être très
utile qu'une classe soit construite à partir d'une autre classe, en conservant les propriétés de la
classe d'origine et en en acquérant de nouvelles. En C++, ce processus est possible et s'appelle
héritage ou dérivation. Il conduit à la notion de hiérarchie de classe.


5.1 HÉRITAGE
        L'héritage (ou dérivation) est un mécanisme qui permet de construire des classes à partir
d'autres classes, en définissant une nouvelle classe, appelée classe dérivée, comme une extension
d'une classe existante, appelée classe de base. La dérivation permet à une classe dérivée d'hériter
des propriétés, c'est à dire des données et fonctions membres, d'une classe de base. Ainsi, il est
possible de compléter des classes, en rajoutant données ou des fonctions membres, et/ou de les
personnaliser, en redéfinissant des données ou des fonctions membres.

       5.1.1 Syntaxe
       Dans la définition d'une classe dérivée, le nom de la classe de base, séparé par le signe
deux points (:), suit le nom de la nouvelle classe.

       Syntaxe :       class base
                       {
                         ...
                       };

                       class dérivée : base
                       {
                         ...
                       };

       Remarques :     Quand une classe dérivée hérite d'une classe de base, les données et les
                       fonctions membres de cette classe de base sont incorporées aux données
                       et fonctions membres de la classe dérivée.
                       Une classe dérivée peut redéfinir des données ou des fonctions membres
                       d'une classe de base. Pour lever toute ambiguïté, l'opérateur de résolution
                       de portée :: peut être utilisé.
                       Les classes de base qui apparaissent dans la déclaration de classes
                       dérivées doivent avoir été définies préalablement.
                       Les classes de base sont souvent aussi appelées classes ancêtres. De
                       même, les classes dérivées sont aussi appelées classes filles.
                       Grâce à l'héritage, il est possible de définir des lignées d'objets, c'est à
                       dire des successions de classes qui héritent les unes des autres. Dans ce
                       schéma, une classe de base qui apparait explicitement dans la déclaration
                       de sa classe dérivée est appelée classe directe. A l'inverse, une classe qui

                                                                                                 47
                        n'apparait pas directement dans la déclaration d'une classe dérivée (mais
                        qui cependant appartient à la hiérarchie) est appelée classe indirecte.

        Exemple :       #include <iostream.h>

                       class point
                       {
                               double x,y;     // coordonnées (x,y) d'un point
                         public:
                               ...
                       }

                       class cercle : point
                       {
                               double ray;     // rayon d'un cercle
                         public:
                               ...
                       }

                        Dans cet exemple, la classe cercle a trois données membres : x, y et ray.

       5.1.2 Affectation
        En programmation orientée objet, un objet de classe dérivée peut être manipulé comme
un objet de classe de base. Ceci signifie par exemple qu'un objet dérivé peut être affecté à un
objet de base, contrairement à la règle traditionnelle de l'affectation qui précise que les membres
à gauche et à droite du signe = doivent être de même type. En fait, pour être autorisée, il suffit
que l'affectation (ou projection) d'un objet dans un deuxième objet initialise tous les champs de
ce deuxième objet. C'est le cas lorsqu'un objet dérivé est projeté sur un objet de base, mais pas
l'inverse. Valide pour l'affectation d'objets, cette propriété est aussi très utilisée pour le passage
de paramètres objets dans les fonctions et pour l'affectation de pointeurs sur les objets.

       Exemple : (suite de l'exemple précédent)
                      void main ( )
                      {
                        point p;
                        cercle c;

                         p = c;                // valide, toutes les données de p sont initialisées
                         c = p;                // interdit, ray n'est pas initialisé.
                       }

       5.1.3 Constructeurs et destructeurs
         Lorsque la classe de base d'une classe dérivée possède un constructeur (ce qui devrait
toujours être le cas), cette classe dérivée doit elle aussi obligatoirement avoir un constructeur,
sauf s'il y a dans la classe de base un constructeur sans paramètre. En effet, lors de la création
d'une instance d'une classe dérivée, le constructeur de la classe de base est exécuté préalablement
à celui de la classe dérivée. Quand une classe dérivée possède plusieurs classes de base
(hiérarchie), le constructeur appelé en premier est celui de la classe la plus ancêtre, le dernier

48
étant toujours celui de la classe dérivée. Ce mécanisme permet ainsi d'initialiser
systématiquement en cascade toutes les données membres des classes d'une hiérarchie.

         Dans une classe dérivée, pour passer des paramètres au constructeur de la classe de base,
il suffit de préciser la liste de ces paramètres à la suite de l'en-tête du constructeur de la classe
dérivée (en séparant cette liste de l'en-tête par le symbole :).

       Syntaxe :       class base
                       {
                               ...
                         public:
                               base (param_base) {corps};
                       };

                       class dérivée : base
                       {
                               ...
                         public:
                               dérivée (param_dérivée) : base (param_base) {corps};
                       };

        Remarque :      Lorsqu'une donnée membre d'une classe est un objet, le constructeur de
                        cette classe doit passer des paramètres au constructeur de l'objet membre.
                        Ceci est réalisé en appelant explicitement le constructeur de l'objet
                        membre, avec ses paramètres, à la suite de l'entête du constructeur de la
                        classe.

        Exemple :      #include <iostream.h>
                       class point
                       {
                               double x,y;    // coordonnées d'un point
                         public:
                               point (double a=0, double b=0) { x = a; y = b; }
                       }
                       class cercle : point
                       {
                               double ray;    // rayon
                         public:
                               cercle (double a, double b, double r=0) : point (a,b) {ray = r;}
                       }

        Dans une hiérarchie, les destructeurs sont appelés dans l'ordre inverse des constructeurs.
Ainsi, le destructeur appelé en premier est celui de la classe dérivée, le second celui de la classe
immédiatement supérieure, et ainsi de suite jusqu'au destructeur de la classe la plus ancêtre. Ce
mécanisme permet ainsi de libérer systématiquement en cascade toutes les données membres des
classes d'une hiérarchie.




                                                                                                   49
           5.1.4 Accès aux membres hérités
       Les classes dérivées n'héritent pas automatiquement des privilèges des classes de base.
Ainsi, dans une classe dérivée, les spécificateurs private, public et protected permettent de
contrôler précisément l'accès aux membres des classes de base.

           Membres privés :      Les membres privés (private) d'une classe sont inaccessibles à
                                 toute autre classe, y compris à une classe dérivée. De cette
                                 manière, le principe de l'abstraction des données est
                                 complètement préservé.

           Membres protégés :    Les membres d'une classe qui sont déclarés protégés (protected)
                                 sont privés, sauf pour les classes dérivées dans lesquelles ils
                                 peuvent être utilisés directement.

           Membres publics:      Les membres publics (public) d'une classe sont accessibles
                                 partout.

        Parallèlement à ce contrôle en amont, c'est à dire lors de la déclaration d'une classe, il est
possible de contrôler l'accès des différents membres d'une classe de base en aval, c'est à dire lors
de la déclaration d'une classe dérivée. Pour cela, un spécificateur d'accès peut précéder la
déclaration d'une classe de base dans la déclaration d'une classe dérivée. Ceci ne modifie pas
bien sûr l'accès aux membres de la classe de base dans la classe de base, mais l'accès aux
membres de la classe de base dans la classe dérivée.

           Syntaxe :     class base
                         {
                                ...
                         }

                         class dérivée1 : public base
                         {
                                 ...
                         }

                         class dérivée2 : private base
                         {
                                 ...
                         }

                  Remarque :     Lorsqu'aucun contrôleur d'accès n'est spécifié, la classe de base
                                 est privée par défaut.

           Trois cas se présentent, selon que la classe de base est déclarée publique, protégée ou
privée :
           1. Si une classe de base est déclarée publique, les membres publics et protégés de la
              classe de base restent membres publics et protégés de la classe dérivée,
           2. Si une classe de base est déclarée privée (accès par défaut), les membres publics et
              protégés de la classe de base deviennent membres privés de la classe dérivée,


50
        3. Si une classe de base est déclarée protégée, les membres publics et protégés de la
           classe de base deviennent membres protégés de la classe dérivée.
Dans tous les cas, les membres privés d'une classe de base restent privés dans les classes
dérivées. C'est le concepteur d'une classe (et lui seul) qui autorise ou non l'accès aux membres de
sa classe.

        Il est aussi possible de restaurer localement l'accès d'un membre d'une classe de base
dans une classe dérivée. Cette déclaration d'accès est permise par exemple pour les membres
publics d'une classe de base déclarée privée ou protégée dans la classe dérivée. En effet, tous les
membres d'une classe de base déclarée privée ou protégée deviennent privés ou protégés pour la
classe dérivée. En les redéclarant publics de manière explicite dans la classe dérivée, leur accès
public est restauré.

       Syntaxe :       class base
                       {
                               ...
                         public:
                               int i;
                       }

                       class dérivée1 : private base // les membres de base sont privés
                       {
                               ...
                         public:
                               base::i;        // restauration de i publique dans la classe dérivée
                       }

       Remarque :      Pour ne pas cacher le membre i de la classe de base, il faut utiliser
                       l'opérateur de résolution de portée :: et nommer explicitement le membre
                       dont il faut modifier l'accès. Dans le cas contraire, la donnée membre
                       publique i déclarée dans la classe dérivée rend inaccessible la donnée
                       membre privée de la classe de base.


5.2 HÉRITAGE MULTIPLE
       En C++, il est possible de créer des classes dérivées à partir de plusieurs classes de base.
On parle alors d'héritage multiple.

       5.2.1 Héritage direct
       La figure suivante présente un cas simple où une classe X hérite de trois classes A, B et
C:




                                                                                                      51
        La syntaxe de déclaration et d'utilisation d'une classe dérivée qui hérite de plusieurs
classes de base est totalement identique à celle des héritages linéaires. Il suffit de faire suivre la
déclaration de la classe dérivée par la liste des classes de base :

        Syntaxe :       class A
                        {
                          ..
                        };

                       class B
                       {
                         ..
                       };

                       class C
                       {
                         ..
                       };

                       class X : public A, private B, public C
                       {
                         ...
                       };

        Remarque :      L'ordre de déclaration des classes de base ne sert qu'à déterminer l'ordre
                        d'appel des constructeurs et des destructeurs de la hiérarchie.
                        Comme pour une hiérarchie linéaire, les constructeurs des classes de base
                        sont placés, avec leurs paramètres, dans l'ordre de déclaration des classes
                        de base, à la suite de l'entête du constructeur de la classe dérivée : X::X
                        (paramXs) : A (paramAs), B (paramBs), C (paramCs).

       5.2.2 Classes de base identiques
        Une classe de base directe ne peut apparaître plus d'une fois dans la déclaration d'une
classe dérivée. Par contre, il peut arriver qu'une classe dérivée hérite d'une classe indirecte par
plusieurs classes directes. On aboutit alors au schéma suivant, où la classe X hérite de la classe
A par les classes B et C :



52
        Bien évidemment, un tel schéma conduit à des ambiguïtés puisque deux objets de classe
A accessibles dans X existent. Il est néanmoins possible de lever ces ambiguïtés en utilisant
l'opérateur de résolution de portée pour référencer explicitement B::A ou C::A.

       5.2.3 Classes de base virtuelles
       Lorsqu'au moins deux classes d'une hiérarchie ont une classe de base commune, il est
possible de déclarer cette classe commune virtuelle, de manière à forcer le partage d'une unique
instance plutôt que deux copies séparées. Ainsi, dans le schéma suivant, les classes B et C
ancêtres de X partagent la même classe A.




        Pour obliger la classe X à n'hériter que d'un objet de classe A, il suffit de préciser le mot
réservé virtual devant A lors de la déclaration des classes B et C.

        Syntaxe :       class A
                        {
                          ..
                        };

                       class B : virtual public A
                       {
                         ..
                       };

                       class C : virtual public A
                       {
                         ..
                       };

                       class X : public B, public C
                                                                                                   53
                       {
                        ...
                       };

       Remarque :      Il est possible qu'une classe dérivée ait à la fois des classes de base
                       virtuelles et non virtuelles.
                       Dans une hiérarchie qui comprend une classe de base virtuelle accessible
                       par plusieurs chemins, on accède à cette classe de base par le chemin qui
                       procure le plus de droit d'accès (c'est à dire d'abord public, puis protected
                       puis private).
                       Dans des hiérarchies complexes, il faut faire très attention aux ambiguïtés
                       (données ou fonctions membres identiques) qui peuvent survenir.

       5.2.4 Types de hiérarchies
       Deux approches différentes peuvent être utilisées pour construire une hiérarchie.

       La première consiste à spécialiser les classes dérivées au fur et à mesure de la hiérarchie.
On parle alors de spécialisation des classes, puisque les classes dérivées sont plus spécialisées
que les classes de base. Cette spécialisation conduit souvent à des hiérarchies linéaires ou
arborescentes croissantes.

       Mais il est aussi possible de procéder de manière complètement opposée en construisant
une hiérarchie basée sur une généralisation de caractéristiques. Dans ce cas, plusieurs classes qui
partagent des propriétés identiques sont regroupées dans une classe de base. Les graphes
engendrés par les généralisations sont alors bien souvent arborescents décroissants.


5.3 POLYMORPHISME
        En programmation orientée objets, l'édition de liens dynamiques [dynamic binding ou
late binding] permet d'écrire des programmes encore plus modulaires qu'en programmation
structurée classique. En effet, grâce aux fonctions virtuelles, il est possible de définir dans les
classes d'une hiérarchie plusieurs implémentations d'une même fonction membre, commune aux
classes de cette hiérarchie. Les programmes d'application peuvent alors utiliser ces fonctions
membres sans connaître le type (classe) de l'objet courant au moment de la compilation : c'est
pendant l'exécution du programme que le système sélectionne la fonction membre appelée, selon
le type de l'objet courant. Comme pour la surcharge des fonctions, ce processus qui permet de
donner plusieurs implémentations à des fonctions identiques dans toute une hiérarchie de classes
s'appelle polymorphisme.

       5.3.1 Fonctions virtuelles
        Grâce aux règles de compatibilité entre classe de base et classe dérivée, un pointeur sur
une classe dérivée est aussi un pointeur sur une classe de base (mais la réciproque n'est pas
vraie). Toutefois, par défaut, le type des objets pointés est défini lors de la compilation.

       Ainsi, dans l'exemple suivant,
                       class A                                class B : public A
                       {                                      {
                               ...                                    ...
54
                        public:                                 public:
                              void fct ( );                           void fct ( );
                              ...                                     ...
                       };                                      };

                                                 main (void)
                                                 {
                                                   A *pa;
                                                   B *pb;
                                                 }

l'affectation pa = pb est autorisée, mais quel que soit le contenu de pa (pointeur sur un objet de
classe A ou B), pa->fct ( ) appelle toujours la fonction fct ( ) de la classe A. En effet, pendant la
compilation, le compilateur lie le pointeur pa à la fonction A.fct ( ), et quel que soit le contenu
de pa pendant l'exécution du programme, c'est toujours la fonction A.fct ( ) qui est appelée.

        L'utilisation de fonctions virtuelles permet de contourner ce problème. Lorsqu'une
fonction est déclarée virtuelle dans une classe (il suffit pour cela de faire précéder sa déclaration
du mot réservé virtual), les appels à cette fonction sont résolus pendant l'exécution du
programme, et non plus à la compilation. Ainsi, pour reprendre l'exemple précédent, la fonction
A.fct ( ) est appelée quand le pointeur pa contient un objet de la classe A, et la fonction B.fct ( )
est appelée lorsque le pointeur pa contient un objet de la classe B. Dans ce cas, on parle de
ligature dynamique des fonctions, puisque le choix de la fonction à appeler est réalisé lors de
l'exécution du programme et non plus lors de la compilation.

        Syntaxe :       public:
                                  virtual type fonction ( );

        Remarques :     Pour qu'une fonction soit virtuelle dans toute une hiérarchie, il faut que le
                        nombre ou le type de ses arguments soit rigoureusement identique dans
                        toutes les classes dérivées.
                        Une fonction déclarée virtuelle dans une classe de base peut ou non être
                        redéfinie dans des classes dérivées.
                        Le mot réservé virtual ne s'emploie qu'une fois pour une fonction donnée,
                        impliquant que toute redéfinition de cette fonction dans des classes
                        dérivées est virtuelle.
                        Un constructeur ne peut pas être virtuel mais un destructeur peut l'être.
                        Une fonction virtuelle peut être déclarée amie dans une autre classe.
                        Lorsqu'une fonction déclarée virtuelle est explicitement référencée à
                        l'aide de l'opérateur ::, le mécanisme d'appel virtuel n'est pas utilisé.

       5.3.2 Fonctions virtuelles pures
        Une classe abstraite est une classe de base qui regroupe des caractéristiques communes à
toute une hiérarchie de classes. Dans la mesure où ses classes dérivées la complètent, cette classe
de base ne doit généralement pas être instanciée. En C++, pour interdire l'instanciation d'une
classe, il suffit de donner à cette classe une fonction membre virtuelle pure, publique, de la
forme suivante.

       Syntaxe :       public:
                                                                                                   55
                     virtual type fonction ( ) = 0;

     Remarques : Une fonction virtuelle pure n'a pas de définition et ne peut être appelée.
                 Puisqu'une fonction virtuelle pure n'est pas définie, tout appel de cette
                 fonction est indéfini. Cependant, l'appel d'une fonction virtuelle pure ne
                 produit pas d'erreur.
                 Aucun objet d'une classe ayant une fonction virtuelle pure ne peut être
                 déclaré.
                 Si une classe de base contient une fonction virtuelle pure et que sa classe
                 dérivée ne redéfinit cette fonction virtuelle pure, alors la classe dérivée
                 est elle aussi une classe abstraite qui ne peut être instanciée.


5.4 MOTS CLEFS
     Classe abstraite
     Classe de base
     Classe dérivée
     Classe directe
     Classe indirecte
     Classe virtuelle
     Dérivation
     Fonction virtuelle
     Fonction virtuelle pure
     Héritage
     Héritage multiple
     Hiérarchie
     Membre privé
     Membre protégé
     Membre public
     Polymorphisme
     Projection
     Virtual




56
                                        6. FLUX

        Les fonctions d'entrées/sorties ne sont pas définies dans le langage C++, mais sont
implémentées dans une libraire standard C++ appelée I/O stream [Input/Output stream ≡ flots
d'entrées/sorties]. En C++, les entrées/sorties sont considérées comme des échanges de
séquences de caractères ou octets, appelées flots de données. Un flux est un périphérique, un
fichier ou une zone mémoire, qui reçoit (flux d'entrée) ou au contraire qui fournit (flux de sortie)
un flot de données.


6.1 LIBRAIRIE I/O STREAM
       La libraire I/O stream contient deux classes de base, ios et streambuf, et plusieurs classes
dérivées, comme indiqué sur la figure ci-dessous.




        La classe streambuf et ses classes dérivées définissent des tampons pour les flots, tandis
que la classe ios et ses classes dérivées définissent les opérations sur ces flots. Les tampons sont
des zones mémoires qui servent de relais entre les variables du programme (via les fonctions de
                                                                                                  57
la classe ios) et les périphériques, fichiers et zones mémoires (via les fonctions du système
d'exploitation). Ils sont toujours utilisés car ils optimisent les échanges en réduisant le nombre
des appels systèmes qui sont souvent bien longs.


6.2 ENTRÉES-SORTIES STANDARDS
        Pour lire ou écrire des données dans un flux, les deux opérateurs >> et << sont
communément utilisés avec les deux instances prédéfinies cin et cout des classes istream (flux
d'entrée) et ostream (flux de sortie). cin est un flux associé à l'entrée par défaut, le clavier, tandis
que cout un est un flux associé à la sortie par défaut, l'écran. Il existe aussi deux autres flux
prédéfinis, cerr (non bufferisé) et clog (bufferisé), qui sont reliés à la sortie erreur standard.

        L'opération de sortie de flot est réalisée par l'opérateur d'insertion <<, défini dans la
classe ostream. A l'inverse, l'opération d'entrée de flot est réalisée par l'opérateur d'extraction >>,
défini dans la classe istream. Bien évidemment, ces opérateurs ne sont définis que pour les types
de base du langage, mais il est possible de les redéfinir pour les adapter à d'autres types de
données. Pour cela, il faut les déclarer comme fonction opérateur amie d'une classe et utiliser les
schémas suivants :

                ostream & operator << (ostream & sortie, const classe & objet)
                {
                 // utiliser l'opérateur classique pour des types de base. Ex : sortie << ...
                 return sortie;
                }
ou
                istream & operator >> (istream & entree, const classe & objet)
                {
                  // utiliser l'opérateur classique pour des types de base. Ex : entree >> ...
                  return entree;
                }

        6.2.1 États de formats
        Pour formater les sorties par défaut, on utilise des manipulateurs simples ou on appelle
directement des fonctions membres de la classe de base ios. Les manipulateurs simples
s'emploient sous la forme
               flux << manipulateur            ou            flux >> manipulateur
et sont regroupés dans le tableau de la page ci-contre.

        Exemple :       int    i = 512;
                        cout << oct << i << endl;       // affiche 1000
                        cout << hex << i << endl;       // affiche 200
                        cout << 255 << endl;            // affiche FF
                        cout << dec << i << endl;       // affiche 512




58
       Manipulateur            Utilisation                              Action
 dec                        Entrée/Sortie       Conversion décimale
 hex                        Entrée/Sortie       Conversion hexadécimale
 oct                        Entrée/Sortie       Conversion octale
 ws                         Entrée              Suppression des espaces dans le tampon
 endl                       Sortie              Insertion d'un caractère saut de ligne
 ends                       Sortie              Insertion d'un caractère fin de chaîne \0
 flush                      Sortie              Vidage du tampon

        Plusieurs fonctions membres de la classe ios modifient les données membres de cette
classe pour contrôler les formats d'entrée et/ou de sortie des flots. Elles s'appliquent sur les
objets de la classe ios ou de ses classes dérivées. Le tableau suivant présente ces manipulateurs
de manière synthétique. La notation * indique que, utilisée sans paramètre, la fonction membre
retourne les valeurs courantes des données membres correspondantes.

        Manipulateur                                             Rôle
 int precision (int)*           fixe le nombre de décimales des réels (défaut : 6)
 int fill (char)*               fixe le caractère de remplissage (défaut : espace)
 long flags (long)*             fixe toutes les options de format (voir le tableau d'options)
 long setf (long)               fixe plusieurs options de format (voir le tableau d'options)
 long setf (long, long)         fixe une option de format exclusive (1er paramètre) après
                                réinitialisation du groupe (2ème paramètre, ios::basefield,
                                ios::floatfield ou ios::adjustfield)
 int skip (int)                 fixe l'option de format skips (voir le tableau d'options)
 long unsetf (long)             supprime les options de format spécifiées
 int width (int)*               spécifie la largeur de l'affichage

         Exemple :        double pi = 3.1415926535897932385;
                          cout << pi;                              // affiche 3.141592
                          cout.precision (8);
                          cout << pi                               // affiche 3.14159265
                          cout.setf (ios::showpoint,ios::floatfield); // setf pour options exclusives
                          cout << 10.0;                            //affiche 10.00000000
                          cout.setf (ios::fixed,ios::floatfield); // notation scientifique
                          cout.width (6);                          // affichage sur 6 caractères
                          cout.fill ('@');                         // caractère de remplissage @
                          cout << 12.5;                            // affiche @@12.5
                          cout.setf (ios::showpos);                // affiche + pour les nombres > 0
                          cout.setf (ios::left, ios::adjustfield); // justification à gauche
                          cout << 12.5;                            // affiche +12.5@
                                                                                                        59
       Les fonctions membres flags, setf, skips et unsetf utilisent des options de format comme
paramètres. La liste de ces options est donnée dans le tableau suivant. Les exposants 1/2/3
indiquent les options qui sont mutuellement exclusives. Utiliser simultanément des options
exclusives conduit à des résultats imprévisibles. Il est possible de préciser plusieurs options
simultanément en utilisant l'opérateur | (ou sur bits).

     Option de Format                                           Rôle
 ios::skipws                 supprime les espaces tapés pendant la saisie de nombres
 ios::left1                  justifie les valeurs à gauche -1.25e10xxx
 ios::right1                 justifie les valeurs à droite xxx-1.25e10
 ios::internal1              ajoute des caractères de remplissage interne -x1.25ex10
 ios::dec2                   conversion décimale
 ios::oct2                   conversion octale
 ios::hex2                   conversion hexadécimale
 ios::showbase               conversion en constante entière
 ios::showpos                ajoute le caractère + pour les valeurs décimales positives +3.
 ios::showpoint              affiche toujours la virgule et complète par des zéros 2.000
 ios::scientific3            affichage en notation scientifique 3.4e10
 ios::fixed3                 affichage en notation décimale 123.987
 ios::uppercase              affichage des caractères en majuscules 12F4, 10E34
 ios::unitbuf                réalise un flush à chaque insertion

        6.2.2 États d'erreurs
       Les états d'erreurs permettent de garder trace des erreurs qui peuvent éventuellement
survenir pendant la manipulation des flux, de la classe ios et de toutes ses classes dérivées. Il est
donc fondamental de les connaître.

      États d'erreur                                       Signification
 ios::goodbit                aucun bit d'erreur n'est activé
 ios::eofbit                 marque fin de flux rencontrée pendant une extraction
 ios::failbit                erreur d'allocation mémoire pendant l'utilisation d'un flux
 ios::badbit                 erreur fatale sur le tampon (streambuf) associé au flux
 ios::hardfail               erreur interne à la librairie : ne jamais l'utiliser

        Pour préserver l'encapsulation des données, on accède aux valeurs des différents états
d'erreurs au moyen de fonctions membres publiques dont la liste est donnée dans le tableau
suivant.
60
   Fonctions membres                                               Rôle
 int bad ( ) const             retourne une valeur non nulle si ios::badbit est activé
 void clear (int)              efface les états d'erreur spécifiés (l'opérateur de bit | peut spécifier
                               plus d'un état) ou tous les états si le paramètre est nul
 int eof ( ) const             retourne une valeur non nulle si ios::eofbit est activé
 int fail ( ) const            retourne une valeur non nulle si ios::badbit ou ios::failbit sont
                               activés
 int good ( ) const            renvoie une valeur non nulle si aucun bit d'erreur est activé
 int rdstate ( ) const         retourne toutes les valeurs courantes de l'état d'erreur
 operator void* ( )            convertit le flux courant en pointeur pour pouvoir le comparer à
 operator const void* ()       NULL. Retourne 0 si ios::failbit ou ios::badbit sont activés.
 int operator ! ( ) const      ! ( ) retourne une valeur non nulle si ios::badbit ou ios::failbit sont
                               activés
 streambuf* rdbuf ( )          retourne un pointeur sur l'objet streambuf associé au flux

        Exemple :        if (!cin)
                         {
                           cout << "ios::failbit ou ios::badbit est activé" << endl;
                           if (cin.bad ( ))
                             cin.clear (ios::badbit|cin.rdstate( )); // réinitialise ios::badit
                         }


6.3 MANIPULATION DE FLUX
        Avant d'être utilisé, un flux doit toujours être associé à un tampon, puisque toutes les
opérations d'entrée/sortie sont réalisées dans ce tampon. C'est pourquoi les classes ios, istream,
ostream et iostream reçoivent un tampon comme unique paramètre pour leurs constructeurs. Il
faut alors penser, préalablement à toute utilisation de ces classes, à déclarer et initialiser
correctement un objet tampon dérivé de streambuf.

        Afin de réduire cette contrainte, les trois classes ofstream, ifstream, et fstream (dont les
entêtes se trouvent dans le fichier stream.h), dérivées respectivement de ostream, istream et
iostream, permettent de manipuler très simplement des flux en créant automatiquement un
tampon associé au flux. Pour cela, on utilise principalement quatre fonctions membres de ces
classes.

        6.3.1 Constructeur [i/o]fstream ( )
        Il y a quatre versions différentes des constructeurs des classes [i/o]fstream.
        [i/o]fstream ( );
        [i/o]fstream (int d);
        [i/o]fstream (const char* fname, int mode, int prot=filebuf::openprot);
        [i/o]fstream (int d, char *p, int len);
                                                                                                      61
           La première version n'a pas de paramètre et construit un objet [i/o]fstream qui n'est pas
ouvert.
        La seconde version reçoit un unique paramètre et construit un objet [i/o]fstream relié à
un descripteur de flux d. Ce descripteur peut correspondre par exemple à un flux déjà associé à
un tampon. Si d n'a pas été préalablement ouvert, ios::failbit est initialisé.
        La troisième version a trois arguments. Elle permet d'ouvrir un flux fname, avec le mode
d'ouverture spécifié par mode et le mode de protection spécifié par prot. Le tableau suivant
précise les différents modes d'ouverture possibles, qui peuvent être combinés entre eux à l'aide
de l'opérateur ou (|).
     Mode d'ouverture                                         Action
 ios::in                       ouverture en lecture seule (obligatoire pour la classe ifstream)
 ios::out                      ouverture en écriture seule (obligatoire pour ofstream)
 ios::binary                   ouverture en mode binaire
 ios::app                      ouverture en ajout de données (écriture en fin de flux)
 ios::ate                      déplacement en fin de flux après ouverture
 ios::trunc                    si le flux existe, son contenu est effacé (obligatoire si ios::out est
                               activé sans ios::ate ni ios::app)
 ios::nocreate                 le flux doit exister préalablement à l'ouverture
 ios::noreplace            le flux ne doit pas exister préalablement à l'ouverture (sauf si
                           ios::ate ou ios::app est activé)
       La quatrième version reçoit elle aussi trois paramètres. Le premier est un objet associé à
un descripteur de flux d. Si d n'a pas été préalablement ouvert, ios::failbit est initialisé. Ce
constructeur permet de construire un objet tampon streambuf, de taille len caractères et qui
commence à l'adresse p. Si p vaut NULL ou que len est égale à zéro, l'objet associé filebuf n'est
pas tamponné.

           6.3.2 Ouverture de flux open ( )
        La fonction
        [i/o]fstream::open (const char* fname, int mode, int prot=filebuf::openprot);
permet d'ouvrir un flux fname, avec le mode d'ouverture spécifié par mode et le mode de
protection spécifié par prot. Le tableau précédent précise les différents modes d'ouverture
possibles, qui peuvent être combinés entre eux à l'aide de l'opérateur ou (|). Cette fonction
membre est essentiellement utilisée lorsque l'objet flux est initialisé par le constructeur sans
paramètre.

           6.3.3 Pointeur de tampon rdbuf ( )
        La fonction [i/o]fstream::rdbuf ( ) retourne un pointeur sur le tampon attaché à l'objet
de la classe [i/o]fstream.

           6.3.4 Fermeture du flux close ( )
         La fonction fstreambase::close ( ) ferme le tampon attaché à l'objet de la classe
[i/o]fstream en supprimant la connexion entre l'objet et le descripteur de flux.
62
       6.3.5 Flux en écriture
         La fonction ostream& ostream::put (char c) insère le caractère c dans le tampon
associé à l'objet de la classe [i/o]fstream. Pour insérer plusieurs caractères simultanément, il faut
utiliser la fonction ostream& ostream::write (char* cp, int n) qui insère n caractères stockés à
l'adresse pointée par cp.

        Pour déplacer le pointeur d'élément dans le flux, il faut utiliser la fonction
ostream& ostream::seekp (streamoff n, ios::seek_dir dir) qui positionne le pointeur à n
octets de la position dir. dir peut prendre l'une des valeurs du tableau suivant.

    Position dans flux                                      Action
 ios::beg                    Début du flux
 ios::cur                    position courante du flux
 ios::end                    fin du flux

Si on tente de déplacer le pointeur d'élément vers une position invalide, seekp () bascule la
valeur ios::badbit.

      Pour connaître la valeur courante du pointeur d'élément, on utilise la fonction
streampos ostream::tellp ( ).

       6.3.6 Flux en lecture
        La fonction istream& istream::get (char c) extrait un caractère du tampon associé à
l'objet de la classe [i/o]fstream et le place dans c. Pour extraire plusieurs caractères
simultanément, il faut utiliser la fonction istream& istream::read (char* cp, int n) qui extrait
n caractères du tampon et les stocke à l'adresse pointée par cp. Il existe d'autres versions de la
fonction get, mais elles ne sont pas souvent utilisées.

        Pour déplacer le pointeur d'élément dans le flux, il faut utiliser la fonction
istream& istream::seekg (streamoff n, ios::seek_dir dir) qui positionne le pointeur à n octets
de la position dir (les différentes valeurs de dir sont regroupées dans le tableau précédent).

      Pour connaître la valeur courante du pointeur d'élément, on utilise la fonction
streampos istream::tellg ( ).

        Trois fonctions supplémentaires permettent de manipuler les flux en lecture.
int istream::gcount ( ) retourne le nombre de caractères réellement extraits du tampon lors du
dernier appel de get ou read. int istream::peek ( ) retourne le prochain caractère du tampon,
sans l'extraire réellement (s'il n'y a pas de caractère, peek ( ) retourne eof).
istream& istream::putback (char c) replace le caractère c dans le tampon (c doit
obligatoirement être le dernier caractère extrait du tampon).




                                                                                                   63
Exemples :
      // Écriture dans un flux texte                // Écriture dans un flux binaire

       #include <iostream.h>                        #include <iostream.h>
       #include <fstream.h>                         #include <fstream.h>
       #include <stdlib.h>                          #include <stdlib.h>
       #include <math.h>                            #include <math.h>
       #define N 16                                 #define N 16

       void main (void)                             void main (void)
       {                                            {
         int i;                                      int i;
        double s[N], t[N];                           double s[N], t[N];
        char * flux_name = "data.txt";                char * flux_name = "data.res";

        for (i=0;i<N;i++)                            for (i=0;i<N;i++)
          s[i] = sin (2 * M_PI * i / (double) N);      s[i] = sin (2 * M_PI * i / (double) N);
        ofstream fr (flux_name, ios::out);           ofstream fr (flux_name, ios::out);
        if (!fr)                                     if (!fr)
        {                                            {
          cerr << "Erreur d'ouverture de ";            cerr << "Erreur d'ouverture de ";
          cerr << flux_name << endl;                   cerr << flux_name << endl;
          exit (1);                                    exit (1);
        }                                            }
        for (i=0;i<N;i++)                            fr.write ((char*) s, N*sizeof (double));
          fr << s[i];                                fr.close ( );
        fr.close ( );

        ifstream fd (flux_name, ios::in);            ifstream fd (flux_name, ios::in);
        if (!fd)                                     if (!fd)
         {                                           {
          cerr << "Erreur d'ouverture de ";            cerr << "Erreur d'ouverture de ";
          cerr << flux_name << endl;                   cerr << flux_name << endl;
          exit (1);                                    exit (1);
         }                                           }
        for (i=0;i<N;i++)                            fd.read ((char*) t, N*sizeof (double));
          fd >> t[i];                                fd.close ( );
        fd.close ( );
       }                                                   }




64
6.4 FICHIERS IOSTREAM.H ET FSTREAM.H
       Les classes présentées ci-après sont des extraits des fichiers iostream.h et fstream.h.
Pour avoir de plus amples renseignements, il faut se reporter directement à ces deux fichiers.

              6.4.1 Déclaration de la classe ios (iostream.h)
class ios
{
 public:
              enum io_state              { goodbit=0, eofbit=1, failbit=2, badbit=4, hardfail=0200 };
              enum open_mode             { in=1, out=2, ate=4, app=010, trunc=020, nocreate=040, noreplace=0100 };
              enum seek_dir              { beg=0, cur=1, end=2 };
              enum                       { skipws=01,left=02,right=04, internal=010, dec=020, oct=040, hex=0100, showbase=0200, showpoint=0400,
                                         uppercase=01000, showpos=02000, scientific=04000, fixed=010000, unitbuf=020000, stdio=040000 } ;
              static const long          basefield;                                    /* dec|oct|hex */
              static const long          adjustfield;                                  /* left|right|internal */
              static const long          floatfield;                                   /* scientific|fixed */
                                         ios          (streambuf*);
              virtual                    ~ios         ();
              long                       flags        () const                         { return x_flags ; }
              long                       flags        (long f);
              long                       sef          (long setbits, long field);
              long                       setf         (long);
              long                       unsetf       (long);
              int                        width        () const                         { return x_width ; };
              int                        width        (int w)                          {int i = x_width ; x_width = w ; return i ; }
              ostream*                   tie          (ostream*);
              ostream*                   tie          ()                               { return x_tie ; }
              char                       fill         (char);
              char                       fill         () const                         { return x_fill ; }
              int                        precision (int);
              int                        precision () const                            { return x_precision ; }
              int                        rdstate      () const                         { return state ; }
                                         operator void*()                              { if (state&(failbit|badbit|hardfail)) return 0 ; else return this ; }
                                         operator const void*() const                  { if (state&(failbit|badbit|hardfail)) return 0 ; else return this ; }
              int                        operator! () const                            { return state&(failbit|badbit|hardfail); }
              int                        eof          () const                         { return state&eofbit; }
              int                        fail         () const                         { return state&(failbit|badbit|hardfail); }
              int                        bad          () const                         { return state&badbit ; }
              int                        good         () const                         { return state==0 ; }
              void                       clear        (int i =0);
              streambuf*                 rdbuf        ()                               { return bp ; }
              long &                     iword        (int);
              void* &                    pword        (int);
              static long                bitalloc     ();
              static int                 xalloc       ();
              static void                sync_with_stdio ();
              int                        skip         (int);
 protected:
              ostream*                   x_tie;
              long                       x_flags;
              short                      x_precision;
              char                       x_fill;
              short                      x_width;
              void                       init       (streambuf*);
                                         ios        ();                     /* No initialization at all. Needed by multiple inheritance versions */
              int                        assign_private;                    /* needed by with_assgn classes */
 private:
                            ios                     (ios&);
              void          operator =              (ios&);
};




                                                                                                                                                          65
              6.4.2 Déclaration de la classe istream (iostream.h)
class istream : virtual public ios
{
 public:
                                     istream              (streambuf*);
                                     virtual ~istream     ();
 public:
              int                    ipfx                 (int noskipws=0);
              istream&               seekg                (streampos p);
              istream&               seekg                (streamoff o, ios::seek_dir d);
              streampos              tellg                ();
              istream&               operator >>           (istream& (*f)(istream&))            { return (*f)(*this) ; }
              istream&               operator >>          (ios& (*f)(ios&) );
              istream&               operator >>          (char*);
              istream&               operator >>          (signed char*);
              istream&               operator >>          (unsigned char*);
              istream&               operator >>          (char&);
              istream&               operator >>          (signed char&);
              istream&               operator >>          (unsigned char&);
              istream&               operator >>          (short&);
              istream&               operator >>          (int&);
              istream&               operator >>          (long&);
              istream&               operator >>          (unsigned short&);
              istream&               operator >>          (unsigned int&);
              istream&               operator >>          (unsigned long&);
              istream&               operator >>          (float&);
              istream&               operator >>          (double&);
              istream&               operator >>          (long double&);
              istream&               operator >>          (streambuf*);
              istream&               get                  (char* , int lim, char = '\n');
              istream&               get                  (signed char*, int, char = '\n');
              istream&               get                  (unsigned char*, int, char = '\n');
              istream&               getline              (char*, int, char = '\n');
              istream&               getline              (signed char*, int, char = '\n');
              istream&               getline              (unsigned char*, int, char = '\n');
              istream&               get                  (streambuf&, char = '\n');
              istream&               get                  (char&);
              istream&               get                  (signed char&);
              istream&               get                  (unsigned char&);
              int                    get                  ();
              int                    peek                 ();
              istream&               ignore               (int = 1,int = EOF);
              istream&               read                 (char *, int);
              istream&               read(                signed char * s, int n)               { return read((char*)s,n) ; }
              istream&               read                 (unsigned char *s, int n)             { return read((char*)s,n) ;}
              int                    gcount               ();
              istream&               putback              (char);
              int                    sync                 ()                                    { return bp->sync() ; }
 protected:
                                     istream              ();
 private:
              int                    x_gcount;
              void                   xget                 (char *);
};


              6.4.3 Déclaration de la classe istream_with_assign (iostream.h)
class istream_withassign : public istream
{
 public:
                                   istream_withassign     ();
            virtual                 ~istream_withassign   ();
            istream_withassign& operator =                (istream&);
            istream_withassign& operator =                (streambuf *);
};


              6.4.4 Déclaration de la classe ostream (iostream.h)
class ostream : virtual public ios
{
66
 public:
                                  ostream               (streambuf*);
              virtual             ~ostream              ();
 public:
              int                 opfx                  ()                                { if ( ospecial ) return do_opfx() ; else return 1 ; }
              void                osfx                  ()                                { if ( osfx_special ) do_osfx() ; }
              ostream&            flush                 ();
              ostream&            seekp                 (streampos);
              ostream&            seekp                 (streamoff, ios::seek_dir);
              streampos           tellp                 ();
              ostream&            put                   (char );
              ostream&            operator <<           (char c);
              ostream&            operator <<           (signed char);
              ostream&            operator <<           (unsigned char);
              ostream&            operator <<           (const char*);
              ostream&            operator <<           (const signed char*);
              ostream&            operator <<           (const unsigned char*);
              ostream&            operator <<           (int);
              ostream&            operator <<           (long);
              ostream&            operator <<           (double);
              ostream&            operator <<           (long double);
              ostream&            operator <<           (float);
              ostream&            operator <<           (unsigned int);
              ostream&            operator <<           (unsigned long);
              ostream&            operator <<           (const void*);
              ostream&            operator <<           (streambuf*);
              ostream&            operator <<           (short i)                         { return *this << (int)i ; }
              ostream&            operator <<           (unsigned short i)                { return *this << (int)i; }
              ostream&            operator <<           (ostream& (*f)(ostream&))         { return (*f)(*this) ; }
              ostream&            operator <<           (ios& (*f)(ios&) );
              ostream&            write                 (const char *,int);
              ostream&            write                 (const signed char* s, int n)     { return write((const char*)s,n); }
              ostream&            write                 (const unsigned char* s, int n)   { return write((const char*)s,n); }
 protected:
                                  ostream               ();
};


              6.4.5 Déclaration de la classe ostream_with_assign (iostream.h)
class ostream_withassign : public ostream
{
 public:
                                  ostream_withassign    ();
            virtual               ~ostream_withassign   ();
            ostream_withassign& operator =              (ostream&);
            ostream_withassign& operator =              (streambuf*);
};


              6.4.6 Déclaration de la classe iostream (iostream.h)
class iostream : public istream, public ostream
{
 public:
                                   iostream             (streambuf*);
            virtual                ~iostream            ();
 protected:
                                   iostream             ();
};




                                                                                                                                                   67
           6.4.7 Déclaration de la classe iostream_with_assign (iostream.h)
class iostream_withassign : public iostream
{
 public:
                                  iostream_withassign   ();
            virtual ~             iostream_withassign   ();
            iostream_withassign& operator =             (ios&);
            iostream_withassign& operator =             (streambuf*);
};


           6.4.8 Déclaration des variables globales du fichier iostream.h
extern istream_withassign cin;
extern ostream_withassign cout;
extern ostream_withassign cerr;
extern ostream_withassign clog;

ios&       dec         (ios&);
ostream&   endl        (ostream& i);
ostream&   ends        (ostream& i);
ostream&   flush       (ostream&);
ios&       hex         (ios&);
ios&       oct         (ios&);
istream&   ws          (istream&);


           6.4.9 Déclaration de la classe fstreambase (fstream.h)
class fstreambase : virtual public ios
{
 public:
                                    fstreambase         ();
                                    fstreambase         (const char*, int , int = filebuf::openprot);
                                    fstreambase         (int);
                                    fstreambase         (int fd, char*, int);
                                    ~fstreambase        ();
            void                    open                (const char*, int, int = filebuf::openprot);
            void                    attach              (int fd);
            void                    close               ();
            void                    setbuf              (char*, int);
            filebuf*                rdbuf               ()                                   { return &buf ; }
 private:
            filebuf                 buf;
 protected:
            void                    verify              (int);
};


           6.4.10 Déclaration de la classe ifstream (fstream.h)
class ifstream : public fstreambase, public istream
{
 public:
                                   ifstream             ();
                                   ifstream             (const char*, int = ios::in, int = filebuf::openprot);
                                   ifstream             (int fd);
                                   ifstream             (int, char*, int);
                                   ~ifstream            ();
             filebuf*              rdbuf                ()                                    { return fstreambase::rdbuf(); }
             void                  open                 (const char*, int = ios::in, int = filebuf::openprot);
};




68
           6.4.11 Déclaration de la classe ofstream (fstream.h)
class ofstream : public fstreambase, public ostream
{
 public:
                                  ofstream            ();
                                  ofstream            (const char*, int = ios::out, int = filebuf::openprot);
                                  ofstream            (int);
                                  ofstream            (int, char*, int);
                                  ~ofstream           ();
            filebuf*              rdbuf               ()                                    { return fstreambase::rdbuf(); }
            void                  open                (const char* , int = ios::out, int = filebuf::openprot);
};


           6.4.12 Déclaration de la classe fstream (fstream.h)
class fstream : public fstreambase, public iostream
{
 public:
                                   fstream            ();
                                   fstream            (const char*, int, int = filebuf::openprot);
                                   fstream            (int);
                                   fstream            (int, char*, int);
                                   ~fstream           ();
            filebuf*               rdbuf              ()                                   { return fstreambase::rdbuf(); }
            void                   open               (const char*, int, int = filebuf::openprot);
};



6.5 MOTS CLEFS
           Buffer
           cerr/cin/cout
           Descripteur de flux
           Entrées/Sorties
           Etat de format
           Etat d'erreur
           Flot
           Flux
           Flux de sortie
           Flux d'entrée
           Flux d'erreur
           fstream.h
           iostream.h
           Périphérique
           Pointeur d'élément
           Tampon




                                                                                                                               69
                                 7. GÉNÉRICITÉ


7.1 INTRODUCTION
         Un langage fortement typé comme le C++ peut parfois sembler être un obstacle à
l'implémentation de fonctions que l'on écrirait naturellement par ailleurs. Par exemple, bien que
l'algorithme de la fonction min () ci-dessous soit simple, il est nécessaire de définir une fonction
pour chaque paire de paramètres de types différents que l'on souhaite comparer.

        Entiers                  Réels simples                        Caractères

int min (int a, int b)           float min (float a, float b)         char min (char a, char b)
{                                {                                    {
  return (a<b) ? a : b;            return (a<b) ? a : b;                return (a<b) ? a : b;
}                                }                                    }

       Une alternative envisageable mais dangereuse consisterait à utiliser la directive #define
du pré-processeur au lieu d'écrire explicitement chaque instance de min ().

        #define min(a,b) ( ( (a) < (b) ) ? (a) : (b) )

        En effet, bien que cette définition fonctionne correctement pour des cas simples, elle ne
se comporte pas comme on l'attend pour des appels plus complexes, puisque le pré-processeur
effectue une simple substitution du texte de ses paramètres. Ainsi, le plus petit des paramètres
est évalué deux fois : une première fois pendant le test (a < b), et une seconde fois pendant
l'exécution de l'instruction retournée.

        Cas simples : fonctionnement correct
                       min (10, 20);         // retourne 10
                       min (10.0, 20.0);     // retourne 10.0
                       min ('d', 'f');       // retourne 'd'

        Cas plus complexes : fonctionnement anormal
                      int i=10;
                      min (i++,20);        // retourne 11, et i vaut 12 (i++ est réalisée 2 fois)

        La notion de généricité permet de tirer parti de la notation compacte de la directive
#define du pré-processeur, sans perdre aucun avantage des langages fortement typés. Elle
autorise le programmeur à paramétrer à volonté le type des paramètres, le type des variables
locales, et le type de retour de fonctions dont le corps reste le même (instructions identiques).
Cette possibilité s'appelle "généricité", puisqu'on décrit toute une famille de fonctions que le
compilateur instancie à la demande.

       De la même manière qu'on définit des fonctions génériques, aussi appelées fonctions
templates, il est aussi possible de décrire toute une famille de classes à l'aide d'une seule
                                                                                                    71
définition comportant des paramètres de type et/ou des paramètres expression. On aboutit alors à
la notion de classe générique, ou classe template.


7.2 FONCTIONS GÉNÉRIQUES
       Ce paragraphe présente la syntaxe de déclaration ou de définition des fonctions
génériques.

       7.2.1 Déclaration
        Le mot réservé template est toujours placé au début de la définition et de la déclaration
des fonctions génériques. Vient ensuite une liste constituée des types paramètres séparés par des
virgules, délimitée par une paire de signes d'inégalité (< et >). Cette liste est appelée liste des
paramètres formels de la fonction générique. La définition ou la déclaration de la fonction suit la
liste des paramètres formels.

       Syntaxe :       template <class t1, class t2, ...> type nom_fonction (paramètres)
                       {
                       }

       Remarques :     La liste des paramètres formels ne doit pas être vide.
                       Tous les paramètres formels doivent apparaître comme type des
                       paramètres de la fonction.
                       Chaque déclaration de paramètre formel comprend le mot réservé class
                       suivi d'un identificateur (ce mot réservé class indique que le type
                       paramètre peut éventuellement être un type construit ou défini par
                       l'utilisateur).
                       Une fois déclarés, les paramètres formels servent de spécificateur de type
                       pour le reste de la définition de la fonction générique.
                       L'identificateur d'un paramètre formel doit être unique à l'intérieur d'une
                       liste de paramètres génériques, mais il peut être réutilisé pour d'autres
                       déclarations, partout où un type effectif est autorisé.
                       Excepté la présence de paramètres formels comme spécificateurs de type,
                       la définition d'une fonction générique est la même que celle d'une
                       fonction classique.
                       Lors de multiples déclarations, les identificateurs des paramètres formels
                       n'ont pas besoin d'être identiques.
                       Une fonction générique, de la même manière qu'une fonction classique,
                       peut être déclarée extern, inline ou static. Dans ces cas, le spécificateur
                       doit suivre, et non précéder, la liste de paramètres formels.

       Exemple :       template <class Type> static Type min (Type a, Type b)
                       {
                         return (a<b) ? a : b;
                       }




72
       7.2.2 Instanciation
       A chaque fois qu'une fonction générique est employée, le compilateur utilise la définition
générique pour créer (instancier) une fonction adéquate. Pour cela, il cherche à réaliser une
correspondance exacte des types. Par exemple, avec les déclarations suivantes,

       int i, j;
       double x, y;

       min (12, 14);           instancie la fonction           int min (int, int);
       min (i, j);             instancie la fonction           int min (int, int);
       min (x,y);              instancie la fonction           double min (double, double);

le compilateur crée deux instanciations différentes, l'une utilisant des entiers, l'autre des doubles,
à partir de la fonction générique min (). Pour cela, il est nécessaire que chaque paramètre de type
apparaisse au moins une fois dans l'entête de la fonction.

      Le type courant à donner aux paramètres formels est déterminé en évaluant les
paramètres passés à la fonction. Le type de retour n'est pas pris en compte.

       Par exemple,

       double d = min (3,4)

instancie la fonction int min (int, int), mais la valeur renvoyée est transtypée en double avant
l'affectation à d.

          Une fonction générique ne peut être compilée à partir de sa seule définition, puisque c'est
l'utilisation de cette fonction qui permet au compilateur d'implémenter le code conforme aux
types utilisés. Mais elle doit être connue du compilateur pour qu'il puisse instancier la fonction
appropriée. C'est pourquoi les définitions de fonctions génériques figurent en général dans des
fichiers de déclaration d'extension .h, de façon à pouvoir les compiler dès leur utilisation.

       7.2.3 Redéfinition de fonctions génériques
       Il est possible de définir plusieurs fonctions génériques de même identificateur,
possédant des paramètres formels en nombre ou de types différents, qui puissent permettre au
compilateur de distinguer sans ambiguïté les instances de ces fonctions génériques.

         Par ailleurs, il peut arriver que l'instanciation d'une fonction générique ne soit pas
appropriée pour un type particulier de données. Il est alors possible de fournir la définition d'une
ou plusieurs fonctions particulières, qui seront utilisées en lieu et place de celles instanciées à
partir de la fonction générique. On parle alors de définition explicite de fonction générique. Par
exemple, l'instanciation de la fonction min (T, T) pour deux paramètres de type char* ne
donnera pas le résultat attendu si le programmeur souhaite que chaque paramètre soit interprété
comme une chaîne de caractères et non comme un pointeur de caractère. Pour résoudre cela, il
suffit de fournir une instance spécialisée de la fonction min :
template <class Type> Type min (Type a, Type b)                 char *min (char *s1, char *s2)
{                                                               {
  return (a<b) ? a : b;                                           return (strcmp (s1,s2)<0?s1:s2);
}                                                               }
                                                                                                    73
        L'algorithme d'instanciation ou d'appel d'une fonction générique, surchargée ou non, est
le suivant :

       1. le compilateur recherche tout d'abord une correspondance exacte avec les fonctions
          ordinaires (non génériques). Des conversions triviales sont réalisées si elles
          conduisent à une correspondance exacte de type. S'il y a ambiguïté, la recherche
          échoue,
       2. si aucune fonction ordinaire ne convient, le compilateur examine toutes les fonctions
          génériques de même identificateur. Si une seule correspondance exacte est trouvée, la
          fonction correspondante est instanciée. S'il y en a plusieurs, la recherche échoue. Là
          aussi, des conversions triviales sont réalisées si elles conduisent à une correspondance
          exacte de type,
       3. enfin, si aucune fonction générique ne convient, le compilateur examine à nouveau
          toutes les fonctions ordinaires en les considérant comme des fonctions redéfinies
          (conversions explicites).


7.3 CLASSES GÉNÉRIQUES
        Comme les fonctions génériques, les classes génériques permettent de construire des
classes en paramétrant des types. Ceci conduit à des descriptions générales de classes qui, selon
les utilisations, conduisent à instancier des classes différentes à partir d'un masque (ou
empreinte) unique. Avec les classes génériques, on obtient une génération automatique
d'instances de classes, liées à un type particulier.

        Les classes génériques se comportent exactement comme les classes non génériques.
Même si l'écriture d'une classe générique est au premier abord "intimidante", le code revêt en
définitive un aspect plutôt habituel.

       7.3.1 Déclaration
        Comme pour les fonctions génériques, le mot réservé template est toujours placé au
début de la déclaration des classes génériques. Vient ensuite la liste des paramètres formels
séparés par des virgules, délimitée par une paire de signes d'inégalité (< et >), et suivie de la
déclaration de la classe.

         Si une fonction membre est définie à l'extérieur de la définition d'une classe générique
(ce qui est souvent le cas), il faut donner la liste des paramètres formels précédée du mot réservé
template avant l'identificateur de la classe, puis répéter cette liste de paramètres formels après
l'identificateur de la classe.

       Syntaxe :       template <class t1, class t2, ..., type v1, type v2, ...> class X
                       {
                         private:
                               ...
                         public:
                               X ();
                               ~X ();
                               membre (...);
                       }
74
                     template <class t1, class t2, ..., type v1, type v2, ...>
                     X <t1, t2, ..., v1, v2, ...>::membre (...)
                     {
                       ...
                     }

       Remarques :   La liste de paramètres formels ne doit pas être vide.
                     La liste des paramètres formels peut contenir des déclarations de
                     paramètres classiques (par exemple int i, char *b, ...). Cette
                     caractéristique est généralement utilisée pour définir des constantes dans
                     la classe générique (dimension de tableaux par exemple).
                     Chaque paramètre formel comprend le mot réservé class suivi d'un
                     identificateur (ce mot réservé class indique que le type paramètre peut
                     éventuellement être un type construit ou défini par l'utilisateur).
                     Une fois déclarés, les paramètres formels servent de spécificateur de type
                     dans toute la déclaration de la classe, partout où un type effectif est
                     autorisé.
                     Excepté la présence de paramètres formels comme spécificateurs de type,
                     la définition d'une classe générique est la même que celle d'une classe
                     ordinaire.
                     Une classe générique, de la même manière qu'une classe ordinaire, peut
                     être déclarée extern ou static. Dans ces cas, le spécificateur doit suivre, et
                     non précéder, la liste de paramètres formels.
                     L'identificateur d'une classe générique doit toujours (en dehors de la
                     définition de la classe) être suivi de la liste des paramètres formels,
                     délimitée par une paire de signes d'inégalité (< et >).

       Exemple :      template <class Type, int dim> class essai
                     {
                       private:
                             Type a;               // donnée membre de type Type
                             Type *pa;             // donnée membre pointeur sur Type
                             Type pa[dim];         // tableau de dim Type
                             ...
                       public:
                             essai (Type);         // construct. à un paramètre de type Type
                             ~essai ();            // destructeur
                             membre (Type);        // fonction membre
                             ...
                     }
                     template <class Type, int dim> essai <Type, dim>::essai (Type ai)
                     {
                       ...
                     }


       7.3.2 Instanciation
      Une classe générique est instanciée en donnant une liste complète de types (appelés
paramètres effectifs et associés aux paramètres formels), délimitée par une paire de signes
                                                                                                 75
d'inégalité (< et >), à la suite de l'identificateur de la classe, lors de la déclaration d'un objet. Le
spécificateur de type d'une classe générique peut être utilisé partout où est utilisé habituellement
un spécificateur de type ordinaire. Les objets d'une classe générique instanciée sont déclarés de
la même façon que les objets d'une classe ordinaire.

        Par exemple :
        essai <double, 3> e;
déclare un objet e de la classe essai.

        Les paramètres effectifs (types ou expressions) doivent correspondre aux paramètres
figurant dans la liste. Les paramètres expressions doivent obligatoirement être des expressions
constantes du même type que celui figurant dans la liste donnée lors de la déclaration de la
classe.

        Un paramètre effectif peut lui-même être une classe générique. Une classe générique
peut comporter des membres (données ou fonctions) statiques ; dans ce cas, chaque instance
différente de la classe dispose de son propre jeu de membres statiques.

        La définition d'une classe générique n'est pas compilée tant qu'il n'existe pas d'objet de
cette classe. Le compilateur n'émet donc pas d'erreur tant que cette classe n'est pas instanciée.

        7.3.3 Spécialisation d'une classe générique
        Contrairement aux fonctions génériques, une classe générique ne peut pas être redéfinie
(on ne peut pas définir deux classes de même identificateur). Par contre, on peut spécialiser une
classe générique de deux manières : soit en spécialisant une fonction membre, soit en
spécialisant la classe. Par exemple, on peut spécialiser la classe essai de la manière suivante :

                        class essai <char, 10>
                        {
                                ...    // nouvelle définition de essai
                        }

        7.3.4 Identité de classes génériques
        En C++, on ne peut affecter entre eux que deux objets de même type. Dans le cas d'objets
de type classe générique, il y a identité de type lorsque leurs paramètres de types sont identiques
et que les paramètres expressions ont même valeur.

        7.3.5 Classes génériques et héritage
        Il y a trois manières de combiner des classes génériques dans une hiérarchie de classes :

     1. classe ordinaire dérivée d'une classe générique : on obtient une seule classe dérivée. Par
        exemple, si A est une classe générique définie par :
                template <class T> A
        la classe :
                class B : public A <int>
        dérive de la classe A <int> et est unique.


76
    2. classe générique dérivée d'une classe ordinaire : on obtient une famille de classes
       génériques. Par exemple, A étant une classe ordinaire, la classe :
               template <class T> class B : public A
       définit une famille de classes de paramètre de type T.

    3. classe générique dérivée d'une classe générique : on obtient là aussi des familles de
       classes. Par exemple, si A est une classe générique définie par :
                template <class T> A
       la classe :
                template <class T> class B : public A <T>
       engendre une famille de classes dérivées dont le nombre est identique au nombre de
           classes de base instanciables.
       Par contre, avec la définition,
                template <class T, class U> class B : public A <T>
           chaque classe de base instanciable peut engendrer une famille de classes dérivées de
           paramètre de type U.


7.4 DIFFÉRENCES ENTRE CLASSES ET FONCTIONS GÉNÉRIQUES
        L'identificateur d'une classe générique est toujours suivi de la liste des paramètres
formels (définition de la classe) ou effectifs (déclaration d'un objet, instanciation), entourée des
signes d'inégalité (< et >). Toute référence à une classe générique doit utiliser cette syntaxe
complète. En C++ en effet, le compilateur impose une convention d'appel explicite stricte pour
assurer la génération de classes appropriées.

       Exemple:        template <class T, int range> class ex
                       {
                         ...
                       }
                       ex <double, 20> obj1;          // valide
                       ex <double> obj2;              // erreur
                       ex obj3;                       // erreur

       A l'inverse, l'instanciation d'une fonction générique ne diffère pas de l'appel d'une
fonction ordinaire, le choix d'une implémentation particulière étant déterminé par le type des
paramètres passés lors de l'appel de la fonction.


7.5 EXERCICES D'APPLICATION
       1-      Créer une fonction générique qui permet de calculer le carré d'une valeur de type
               quelconque (le résultat possèdera le même type). Ecrire un programme qui utilise
               cette fonction générique.

       2-      Soit cette définition de fonction générique :
                       template <class T, class U> T fct (T a, U b, T c)
                       {
                         ...
                       }
               avec les déclarations suivantes :
                                                                                                  77
                   int      n, p, q;
                   float x;
                   char t[20];
                   char c;
            Quels sont les appels corrects et, dans ce cas, quels sont les prototypes des
            fonctions instanciées ?
                   fct (n, p, q);               // appel n°1
                   fct (n, x q);                // appel n°2
                   fct (x, n, q);               // appel n°3
                   fct (t, n, &c);              // appel n°4

     3-     Créer une fonction générique qui permet de calculer la somme des éléments d'un
            tableau de type quelconque, le nombre d'éléments du tableau étant fourni en
            paramètre. Ecrire un petit programme d'utilisation.

     4-     Soit la définition suivante d'une classe générique :
                    template <class T, int n> class essai
                    {
                      private:
                            T       tab [n];
                      public:
                            essai (T);
                    };

            a) Donnez la définition du constructeur de la classe essai en supposant que le
            constructeur recopie la valeur reçue en paramètre dans chacun des éléments du
            tableau tab.

            b) Soient les déclarations :
                    const int n = 3;
                    int     p = 5;
            Quelles sont les instructions correctes et les classes instanciées ? Pour chaque
            classe instanciée, on fournira une définition équivalente sous la forme d'une
            classe ordinaire.
                    essai <int, 10> ei (3);                      // cas n°1
                    essai <float, n> ef (0.0);                   // cas n°2
                    essai <double, p> ed (2.5);                  // cas n°3


7.6 MOTS CLEFS
     Classe générique
     Fonction générique
     Généricité
     Générique
     Instanciation de classe
     Instanciation de fonction
     Paramètre de type
     Paramètre effectif
     Paramètre expression
78
Paramètre formel
Template




                   79
                              8. EXCEPTIONS

        Les exceptions sont des situations inhabituelles qui surviennent au moment de
l'exécution d'un programme. Par exemple, une division par zéro, un dépassement arithmétique,
un dépassement de tableau, un manque de mémoire, sont des exceptions. Pour gérer ces
situations particulières, C++ fournit un gestionnaire d'exceptions.


8.1 GESTION DES EXCEPTIONS
       La gestion des exceptions permet de répondre aux anomalies d'un programme au
moment de son exécution. Une syntaxe et un style standard sont proposés, qui peuvent bien
évidemment être affinés et adaptés au type d'exception rencontrée. Mais le mécanisme de
gestion des exceptions peut réduire de façon significative la taille et la complexité du code du
programme en éliminant le besoin de tester les anomalies explicitement.

       C++ fournit trois mots réservés pour manipuler les exceptions :

         1. throw : expression pour lancer une exception, c'est à dire suspendre l'exécution
            normale du programme à l'endroit où survient l'anomalie et passer la main au
            gestionnaire d'exceptions,
         2. try : bloc qui regroupe une ou plusieurs instructions susceptibles de rencontrer des
            exceptions,
         3. catch : bloc placé à la suite du bloc try ou d'un autre bloc catch pour exécuter les
            instructions particulières au traitement d'une exception.

       8.1.1 Throw
        Lorsqu'une exception survient, l'expression throw est utilisée pour envoyer, ou "lancer",
un objet au gestionnaire d'exception. Cet objet peut avoir été créé explicitement pour la gestion
de l'exception, ou bien être directement l'objet cause de l'erreur.

       Syntaxe :      throw objet;

       Remarques :     N'importe quel objet (de type prédéfini ou non) peut être lancé à
                       condition qu'il puisse être copié et détruit dans la fonction dans laquelle
                       l'exception survient.
                       Une instruction throw ressemble beaucoup à une instruction de retour de
                       fonction, mais ce n'en est pas une.
                       Une expression throw vide passe simplement l'exception au bloc try
                       englobant suivant.
                       Une expression throw vide ne peut apparaître qu'à l'intérieur d'un
                       gestionnaire catch.
                       Une fonction peut spécifier la série des exceptions qu'elle lèvera au
                       moyen d'une liste throw, déclarée entre parenthèses à la suite de l'entête
                       de la fonction.


                                                                                                81
       Exemple :      class A {...}
                      if (condition) throw A();

       8.1.2 Try
        Pour pouvoir être interceptées, les exceptions doivent se produire dans un bloc
d'instructions appelé bloc d'essai.

        Ce bloc d'essai regroupe donc une ou plusieurs instructions susceptibles de rencontrer
des exception. Il commence par le mot réservé try, suivi d'une séquence d'instructions comprises
entre deux accolades.

       Syntaxe :      try
                      {
                        ...
                      }

       Remarques :     Un bloc try regroupe une série d'instructions dans lesquelles des excep-
                       tions peuvent survenir.

       Exemple :      try
                      {
                        func1 ();
                        func2 ();
                      }

       8.1.3 Catch
       Une fois l'exception lancée, celle-ci doit être interceptée par un gestionnaire d'exception
pour pouvoir être traitée de manière appropriée.

        Un tel gestionnaire est un bloc d'instructions appelé bloc catch. Ce bloc commence par le
mot réservé catch, suivi par une déclaration (entre parenthèses) d'exceptions. Vient ensuite une
séquence d'instructions entre accolades. La déclaration permet de spécifier les types des objets
que le gestionnaire d'exceptions doit "attraper".

       Syntaxe :      catch (objet identificateur)
                      {
                        ...
                      }

       Remarques :     Un bloc catch suit immédiatement un bloc try, ou un autre bloc catch.
                       Plusieurs blocs catch peuvent se succéder les uns aux autres. La sélection
                       du gestionnaire d'exception est alors réalisée en fonction du type de
                       l'exception envoyée.
                       Une adéquation entre l'exception lancée et celle attendue par un bloc
                       catch est réalisée si :
                       1. les deux types sont exactement les mêmes,
                       2. le type du gestionnaire catch est une classe de base publique de l'objet
                           envoyé,
82
                    3. le type de l'objet envoyé est un pointeur qui peut être converti de
                       manière implicite en pointeur du type de celui du gestionnaire catch.
                    Les gestionnaires catch sont évalués dans l'ordre de leur apparition à la
                    suite du bloc try, mais une fois qu'une adéquation est réalisée, les
                    gestionnaires catch suivant ne sont pas examinés.
                    Si aucun bloc catch ne peut intercepter l'exception, la fonction prédéfinie
                    terminate () est appelée.
                    L'utilisation de trois points de suspension (...) dans la déclaration qui suit
                    le mot réservé catch permet d'intercepter n'importe quelle exception.

    Exemple :      catch (A)
                   {
                    cout << "Message d'erreur" << endl;
                     exit (1);
                   }


8.2 EXEMPLE 1
    #include <iostream.h>
    #include <stdlib.h>

    class zero
    {
     private:
            ...
     public:
            zero ();
            ~zero ();
    };

    void testzero (double d)
    {
     if (d==0.0) throw zero ();
    }

    void main ()
    {
     double a;

     cout << "Entrez un nombre : " << endl ;
     cin >> a;

     try
     {
       testzero (a);
       cout << "L'inverse de " << a << " est : " << 1.0/a << endl;
     }


                                                                                                83
         catch (zero)
         {
          cout << "Il est impossible de déterminer l'inverse de zéro" << endl;
          exit (1);
         }

        exit (0);
       }


8.3 FONCTIONS SPÉCIALES
       Les exceptions lancées ne peuvent pas forcément toutes être interceptées correctement
par des blocs catch. Il y a des situations où la meilleure solution pour gérer l'exception est de
terminer le programme. En C++, deux fonctions spéciales permettent de traiter les exceptions
non gérées par les gestionnaires : unexpected () et terminate ().

       8.3.1 unexpected ()
         Quand une fonction avec une liste d'exceptions envoie une exception qui n'est pas dans
cette liste, la fonction prédéfinie unexpected () est appelée. Cette fonction appelle à son tour une
fonction spécifiée à l'aide de la fonction set_unexpected (). Par défaut, unexpected () appelle la
fonction terminate () qui, à son tour, appelle par défaut la fonction abort () (définie dans le
fichier inclus stdlib.h), terminant ainsi le programme.

       8.3.2 terminate ()
       Dans certains cas, le mécanisme de gestion des exceptions échoue, et la fonction
prédéfinie terminate () est appelée. Cet appel de terminate () se produit lorsque :

       1.   la fonction terminate () est appelée explicitement,
       2.   aucun gestionnaire catch ne peut être trouvé pour gérer une exception lancée,
       3.   des problèmes de piles sont rencontrés lors de la gestion d'une exception,
       4.   la fonction unexpected () est appelée.

Par défaut, la fonction terminate () appelle la fonction abort () qui arrête l'exécution du
programme. Mais cet appel à abort () peut être remplacé par un appel à une autre fonction au
moyen de la fonction prédéfinie set_terminate ().

       8.3.3 set_unexpected () et set_terminate ()
        Il est possible d'utiliser les fonctions set_unexpected () et set_terminate () pour
remplacer respectivement les appels aux fonctions terminate () et abort () des fonctions
unexpected () et terminate (). Les déclarations de ces deux fonctions set_unexpected () et
set_terminate () sont incluses dans les fichiers <unexpected.h> et <terminate.h>. Le type de
retour et le paramètre de ces deux fonctions sont un pointeur sur une fonction sans paramètre et
ne retournant pas de valeur. La fonction passée en paramètre remplace alors les fonctions
appelées dans unexpected () et terminate (). Les deux valeurs retournées par set_unexpected ()
et set_terminate () sont des pointeurs sur les fonctions appelées auparavant par unexpected () et
terminate (). Ainsi, en sauvant les valeurs retournées, il est possible de restaurer les fonctions
d'origine.
84
8.4 EXEMPLE 2
        L'exemple suivant présente une utilisation des fonctions spéciales dans le mécanisme de
gestion des exceptions :

       #include <terminate.h>
       #include <unexpected.h>
       #include <iostream.h>

       class X {...};
       class Y {...};
       class Z {...};

       void new_terminate ()
       {
        cout << "Appel à new_terminate." << endl;
       }

       void new_unexpected ()
       {
        cout << "Appel à new_unexpected." << endl;
       }

       void f () throw (X, Y) // f est autorisée à lancer des objets des classes X et Y
       {
        A obj;
        throw (obj);                    // erreur f () ne peut pas lancer d'objet de classe A
       }

       typedef void (*pfv) (); // pfv est un pointeur sur une fonction retournant void

       void main (void)
       {
         pfv old_terminate = set_terminate (new_terminate);
         pfv old_unexpected = set_unexpected (new_unexpected);

        try
        { f(); }

        catch (X)
        { ... }

        catch (Y)
        { ... }

        catch (...)
        { ... }

        set_unexpected (old_unexpected);

                                                                                                85
      try
      { f(); }

      catch (X)
      { ... }

      catch (Y)
      { ... }

      catch (...)
      { ... }
     }

     A l'exécution, le programme s'execute de la manière suivante :
     1. L'appel à set_terminate () assigne à old_terminate l'adresse de la fonction passée
        comme paramètre lors du dernier appel de set_terminate ().
     2. L'appel à set_unexpected () assigne à old_unexpected l'adresse de la fonction passée
        comme paramètre lors du dernier appel de set_unexpected ().
     3. La fonction f () est appelée dans un bloc de test. Comme f () lance une exception
        incorrecte, un appel à unexpected () est réalisé. A son tour, unexpected () appelle
        new_unexpected (), et le message de new_unexpected () s'affiche.
     4. Le second appel à set_unexpected () remplace la fonction new_unexpected () par
        l'adresse de la fonction d'origine (terminate (), appelée par défaut par unexpected ()).
     5. Dans le second bloc de test, la fonction f () est appelée à nouveau. Comme f () lance
        toujours une exception incorrecte, un appel à unexpected () est réalisé. La fonction
        terminate () est alors appelée automatiquement, qui appelle à son tour new_terminate
        ().
     6. Le message de new_terminate () s'affiche.


8.5 MOTS CLEFS

     Bloc d'essai
     Catch
     Exception
     Gestionnaire d'exceptions
     Interception
     Throw
     Try




86
                    TABLE DES MATIÈRES

1. ENCAPSULATION _______________________________________________________ 3
 1.1 INTRODUCTION ___________________________________________________________ 3
 1.2 ABSTRACTION DES DONNEES ______________________________________________ 3
 1.3 ENCAPSULATION __________________________________________________________ 4
 1.4 CLASSES __________________________________________________________________ 4
   1.4.1 Contrôle d'accès__________________________________________________________________ 4
 1.5 DONNEES MEMBRES_______________________________________________________ 5
   1.5.1 Membres statiques________________________________________________________________ 5
 1.6 FONCTIONS MEMBRES ____________________________________________________ 5
   1.6.1 Fonctions « non inline » ___________________________________________________________ 5
   1.6.2 Fonctions inline __________________________________________________________________ 6
   1.6.3 Fonctions amies (friend) ___________________________________________________________ 6
 1.7 DECLARATION D'INSTANCES ______________________________________________ 7
 1.8 MOTS CLEFS ______________________________________________________________ 7
2. ÉLÉMENTS DE SYNTAXE ________________________________________________ 9
 2.1 STRUCTURE D'UN PROGRAMME C++ _______________________________________ 9
 2.2 ELEMENTS DE BASE _______________________________________________________ 9
   2.2.1 Caractères ______________________________________________________________________ 9
   2.2.2 Commentaires __________________________________________________________________ 10
   2.2.3 Délimiteurs ____________________________________________________________________ 10
   2.2.4 Identificateurs __________________________________________________________________ 10
   2.2.5 Mots réservés___________________________________________________________________ 10
   2.2.6 Types de base __________________________________________________________________ 11
   2.2.7 Valeurs littérales ________________________________________________________________ 11
   2.2.8 Déclaration des variables__________________________________________________________ 12
   2.2.9 Attributs des variables ____________________________________________________________ 12
   2.2.10 Initialisation des variables ________________________________________________________ 13
   2.2.11 Constantes typées ______________________________________________________________ 13
 2.3 INSTRUCTIONS ___________________________________________________________ 13
 2.4 OPERATEURS ____________________________________________________________ 13
   2.4.1 Opérateurs arithmétiques__________________________________________________________            14
   2.4.2 Opérateurs de manipulation de bits __________________________________________________         14
   2.4.3 Opérateurs d'affectation, d'incrémentation et de décrémentation ___________________________   14
   2.4.4 Opérateurs relationnels ___________________________________________________________           14
   2.4.5 Opérateurs logiques______________________________________________________________             15
   2.4.6 Opérateur conditionnel ___________________________________________________________            15
   2.4.7 Opérateur sizeof ________________________________________________________________             15
   2.4.8 Opérateur ,_____________________________________________________________________              15
   2.4.9 Opérateur ._____________________________________________________________________              15
   2.4.10 Opérateur :: ___________________________________________________________________             15
   2.4.11 Opérateur ( ) : conversion de type __________________________________________________        15
   2.4.12 Autres opérateurs_______________________________________________________________             15
   2.4.13 Précédence des opérateurs________________________________________________________            16



                                                                                                       87
     2.5 STRUCTURES CONDITIONNELLES _________________________________________ 16
       2.5.1 if ... else ... _____________________________________________________________________16
       2.5.2 switch _________________________________________________________________________16
     2.6 STRUCTURES ITERATIVES ________________________________________________ 17
       2.6.1 Boucle while ____________________________________________________________________17
       2.6.2 Boucle do ... while _______________________________________________________________17
       2.6.3 Boucle for ______________________________________________________________________18
     2.7 BRANCHEMENTS _________________________________________________________ 18
       2.7.1 break __________________________________________________________________________18
       2.7.2 continue _______________________________________________________________________19
       2.7.3 goto___________________________________________________________________________19
       2.7.4 return _________________________________________________________________________19
     2.8 DIRECTIVES POUR LE PREPROCESSEUR___________________________________ 20
       2.8.1 #define et #undef ________________________________________________________________20
       2.8.2 #include _______________________________________________________________________20
       2.8.3 Compilation conditionnelle #if/#ifdef/#ifndef ... #else ... #endif ____________________________20
     2.9 MOTS CLEFS _____________________________________________________________ 21
3. POINTEURS ET FONCTIONS ____________________________________________ 23
     3.1 POINTEURS ET VARIABLES DYNAMIQUES _________________________________ 23
       3.1.1 Type pointeur ___________________________________________________________________23
       3.1.2 Pointeur sur void_________________________________________________________________23
       3.1.3 Opérateur & ____________________________________________________________________24
       3.1.4 Opérateur * _____________________________________________________________________24
       3.1.5 Variables dynamiques, opérateurs new et delete ________________________________________24
       3.1.6 Pointeurs constants _______________________________________________________________25
       3.1.7 Opérations sur les pointeurs ________________________________________________________25
     3.2 TABLEAUX _______________________________________________________________ 25
       3.2.1 Déclaration _____________________________________________________________________26
       3.2.2 Relation entre tableaux et pointeurs __________________________________________________26
       3.2.3 Tableau de pointeurs______________________________________________________________27
       3.2.4 Chaînes de caractères _____________________________________________________________27
       3.2.5 Initialisation des tableaux __________________________________________________________27
     3.3 STRUCTURES, UNIONS ET ENUMERATIONS ________________________________ 28
       3.3.1 Structures ______________________________________________________________________28
       3.3.2 Unions ________________________________________________________________________29
       3.3.3 Énumérations ___________________________________________________________________29
     3.4 DEFINITION DE TYPE : TYPEDEF __________________________________________ 30
     3.5 FONCTIONS ______________________________________________________________ 30
       3.5.1 Déclaration et définition ___________________________________________________________30
       3.5.2 Paramètres d'entrée : transmission par valeur ___________________________________________31
       3.5.3 Paramètres de sortie : transmission par adresse _________________________________________31
       3.5.4 Paramètres de sortie : transmission par référence ________________________________________32
       3.5.5 Paramètres multivalués : transmission par référence constante _____________________________33
       3.5.6 Valeurs par défaut________________________________________________________________33
       3.5.7 Paramètres de la ligne de commande _________________________________________________33
     3.6 MOTS CLEFS _____________________________________________________________ 34
4. CONSTRUCTEURS ET DESTRUCTEURS, SURCHARGE_____________________ 35
     4.1 CONSTRUCTEURS ET DESTRUCTEURS_____________________________________ 35
       4.1.1 Constructeurs ___________________________________________________________________35
       4.1.2 Constructeurs copies______________________________________________________________36
       4.1.3 Destructeurs ____________________________________________________________________36

88
  4.2 POINTEUR THIS __________________________________________________________ 37
  4.3 SURCHARGE _____________________________________________________________ 37
    4.3.1 Surcharge de fonctions ___________________________________________________________           38
    4.3.2 Surcharge de constructeurs ________________________________________________________          39
    4.3.3 Surcharge d'opérateurs ___________________________________________________________           39
    4.3.4 Règles générales de surcharge des opérateurs __________________________________________      39
    4.3.5 Surcharge d'opérateurs unaires _____________________________________________________         40
    4.3.6 Surcharge de l'opérateur d'affectation = ______________________________________________      41
    4.3.7 Surcharge d'opérateurs binaires_____________________________________________________         43
    4.3.8 Surcharge des opérateurs [ ] et ( ) ___________________________________________________      44
    4.3.9 Surcharge des opérateurs new et delete _______________________________________________       45
    4.3.10 Correspondance des paramètres ___________________________________________________           45
  4.4 MOTS CLEFS _____________________________________________________________ 45
5. HÉRITAGE ____________________________________________________________ 47
  5.1 HERITAGE _______________________________________________________________ 47
    5.1.1 Syntaxe _______________________________________________________________________              47
    5.1.2 Affectation_____________________________________________________________________             48
    5.1.3 Constructeurs et destructeurs_______________________________________________________         48
    5.1.4 Accès aux membres hérités ________________________________________________________           50
  5.2 HERITAGE MULTIPLE ____________________________________________________ 51
    5.2.1 Héritage direct __________________________________________________________________           51
    5.2.2 Classes de base identiques_________________________________________________________          52
    5.2.3 Classes de base virtuelles _________________________________________________________         53
    5.2.4 Types de hiérarchies _____________________________________________________________           54
  5.3 POLYMORPHISME________________________________________________________ 54
    5.3.1 Fonctions virtuelles ______________________________________________________________ 54
    5.3.2 Fonctions virtuelles pures _________________________________________________________ 55
  5.4 MOTS CLEFS _____________________________________________________________ 56
6. FLUX _________________________________________________________________ 57
  6.1 LIBRAIRIE I/O STREAM ___________________________________________________ 57
  6.2 ENTREES-SORTIES STANDARDS ___________________________________________ 58
    6.2.1 États de formats _________________________________________________________________ 58
    6.2.2 États d'erreurs __________________________________________________________________ 60
  6.3 MANIPULATION DE FLUX _________________________________________________ 61
    6.3.1 Constructeur [i/o]fstream ( ) _______________________________________________________        61
    6.3.2 Ouverture de flux open ( ) _________________________________________________________         62
    6.3.3 Pointeur de tampon rdbuf ( ) _______________________________________________________         62
    6.3.4 Fermeture du flux close ( ) ________________________________________________________         62
    6.3.5 Flux en écriture _________________________________________________________________           63
    6.3.6 Flux en lecture __________________________________________________________________           63
  6.4 FICHIERS IOSTREAM.H ET FSTREAM.H____________________________________ 65
    6.4.1 Déclaration de la classe ios (iostream.h) ______________________________________________     65
    6.4.2 Déclaration de la classe istream (iostream.h) __________________________________________     66
    6.4.3 Déclaration de la classe istream_with_assign (iostream.h) ________________________________   66
    6.4.4 Déclaration de la classe ostream (iostream.h) __________________________________________     66
    6.4.5 Déclaration de la classe ostream_with_assign (iostream.h)________________________________    67
    6.4.6 Déclaration de la classe iostream (iostream.h) _________________________________________     67
    6.4.7 Déclaration de la classe iostream_with_assign (iostream.h) _______________________________   68
    6.4.8 Déclaration des variables globales du fichier iostream.h__________________________________   68
    6.4.9 Déclaration de la classe fstreambase (fstream.h) ________________________________________    68
    6.4.10 Déclaration de la classe ifstream (fstream.h)__________________________________________     68


                                                                                                       89
       6.4.11 Déclaration de la classe ofstream (fstream.h) __________________________________________69
       6.4.12 Déclaration de la classe fstream (fstream.h) ___________________________________________69
     6.5 MOTS CLEFS _____________________________________________________________ 69
7. GÉNÉRICITÉ __________________________________________________________ 71
     7.1 INTRODUCTION __________________________________________________________ 71
     7.2 FONCTIONS GENERIQUES_________________________________________________ 72
       7.2.1 Déclaration _____________________________________________________________________72
       7.2.2 Instanciation ____________________________________________________________________73
       7.2.3 Redéfinition de fonctions génériques _________________________________________________73
     7.3 CLASSES GENERIQUES____________________________________________________ 74
       7.3.1 Déclaration _____________________________________________________________________74
       7.3.2 Instanciation ____________________________________________________________________75
       7.3.3 Spécialisation d'une classe générique _________________________________________________76
       7.3.4 Identité de classes génériques _______________________________________________________76
       7.3.5 Classes génériques et héritage ______________________________________________________76
     7.4 DIFFERENCES ENTRE CLASSES ET FONCTIONS GENERIQUES ______________ 77
     7.5 EXERCICES D'APPLICATION ______________________________________________ 77
     7.6 MOTS CLEFS _____________________________________________________________ 78
8. EXCEPTIONS__________________________________________________________ 81
     8.1 GESTION DES EXCEPTIONS _______________________________________________ 81
       8.1.1 Throw _________________________________________________________________________81
       8.1.2 Try ___________________________________________________________________________82
       8.1.3 Catch__________________________________________________________________________82
     8.2 EXEMPLE 1 _______________________________________________________________ 83
     8.3 FONCTIONS SPECIALES ___________________________________________________ 84
       8.3.1 unexpected () ___________________________________________________________________84
       8.3.2 terminate () _____________________________________________________________________84
       8.3.3 set_unexpected () et set_terminate () _________________________________________________84
     8.4 EXEMPLE 2 _______________________________________________________________ 85
     8.5 MOTS CLEFS _____________________________________________________________ 86




90
       Christophe LÉGER
       Université d'Orléans
           LESI/ESPEO
          12, rue de Blois
              BP 6744
   45067 ORLÉANS cedex 2
       Tél : 02 38 49 45 63
      Fax : 02 38 41 72 45
E-mail : leger@lesi.univ-orleans.fr




                                      91

				
DOCUMENT INFO
Shared By:
Categories:
Stats:
views:36
posted:10/30/2010
language:French
pages:91