C++



Remarque : Ce texte a été écrit en 1995. Depuis 10 ans, j'ai utilisé le C++. Je n'ai pas mis à jour ce document. Il y aurait beaucoup de choses extrêmement importantes à ajouter... Notamment en ce qui concerne la qualité du code (méthodes de développements).


  1. Une amélioration du C
    1. Les arguments par défaut
    2. Les références
    3. Les fonctions inline
    4. Les opérateurs new et delete
  2. L'encapsulation
    1. Struct et class: définition et déclaration
    2. Accés aux membres d'une classe
    3. Exemple récapitulatif sur les classes
    4. Constructeurs et destructeurs
      1. Les constructeurs
      2. Les destructeurs
      3. Exemple récapitulatif sur les constructeurs et les destructeurs
  3. La surcharge
    1. Surcharge des fonctions
    2. La surcharge des opérateurs
  4. L'héritage
    1. Héritage simple
    2. Héritage: les constructeurs et les destructeurs
    3. Héritage multiple
    4. Fonctions virtuelles
    5. Fonctions virtuelles pures et classes abstraites
  5. Les flux
    1. Présentation générale
      1. La grande hiérarchie
      2. La petite hiérarchie
    2. Utilisation
      1. Manipulation de fichiers sur disque avec utilisation d'un buffer
      2. Manipulation de flux chaîne

Introduction

retour sommaire
La différence essentielle du C++ vis à vis du C réside dans l'approche dite orientée objet de la programmation. La programmation orientée objet (POO) n'est pas une nouveauté en soi; en C nous pouvons parfaitement pratiquer la POO sans le savoir. Le C++ conserve tous les acquis du C (autrement dit un programme écrit en C sera compilé par le compilateur du C++) mais il introduit de nouvelles notions.

En C traditionnel il était déjà possible de décomposer un programme en modules (fonctions, includes,... ) ayant chacun une fonction précise. Le C++ conserve cette possibilité mais va plus loin car il permet de rendre ces modules plus indépendants les uns des autres. Les avantages sont les suivants:

  • Une plus grande sécurité: en effet les risques d'interactions indésirables entre plusieurs modules sont réduits. Bien qu'un certain nombre d'effets spéciaux puissent apparaître.
  • La maintenance d'un programme est simplifiée: si l'on veut modifier une caractéristique du programme il suffit de modifier ou de remplacer le module adéquat.
  • Tel module développé pour tel programme pourra facilement être réutilisé pour un autre programme.
Nous aborderons les principales notions propres à la programmation orientée objet (POO) telles:

I. Une amélioration du C

retour sommaire
Certaines nouveautés du C++ ressemblent plus à une amélioration du C. Elles ne sont pas fondamentales en ce qui concerne la POO.

I.1. Les arguments par défaut

retour sommaire
En C++ il est possible de déclarer des fonctions possédant des arguments par défaut. La syntaxe est très proche de celle utilisée en C pour déclarer une fonction.

Par exemple:
int Fonc (int a, float b, int c=10, int d=50)
Si lors de l'appel de la fonction Fonc les deux derniers arguments ne sont pas précisés, la fonction considère que c a la valeur 10 et d la valeur 50.

Mais il faut respecter une règle importante: Seuls les derniers arguments de la fonction peuvent avoir une valeur par défaut. Autrement dit si dans la déclaration de la fonction un argument re&231#;oit une valeur par défaut, tous les arguments qui le suivent doivent aussi posséder une valeur par défaut.

Par exemple:
La déclaration int Fonc (int a, int b, int c=1, int d=2) est correcte.
La déclaration int Fonc (int a, int b, int c=1, int d) est incorrecte.

I.2. Les références

retour sommaire
Pour être simple nous dirons que le fait d'associer une référence à une variable revient à donner un deuxième nom à la variable. Toute manipulation de la variable est ressentie par la référence et inversement.

La déclaration de la référence est la suivante:
Type& Nom_Reference = Nom_Variable
Type est le type de la variable (noter l'emploie du caractère &).

Dans un appel de fonction la référence peut être considérée comme un passage par adresse.

I.3. Les fonctions inline

retour sommaire
Considérons un programme en C dont le code source comporte des fonctions. Lors de son exécution le système va appeler les fonctions et les passages d'arguments (ainsi que les valeurs renvoyées) se feront via la pile. Ce processus demande un certain temps.

Les fonctions inline permettent de s'affranchir de cette étape. En effet le mot clé inline placé devant la déclaration de la fonction indique au compilateur qu'il ne doit pas compiler la fonction mais remplacer chaque appel dans le corps du programme par le corps de la fonction elle même.

L'avantage réside dans l'accroissement de la vitesse d'exécution du programme, la gestion de la pile étant moins importante. Mais d'un autre coté la taille du programme augmentera. Il faut donc réserver cette déclaration à des fonctions de petite taille si l'on veut que l'accroissement de la taille du programme reste raisonnable.

I.4. Les opérateurs new et delete

retour sommaire
Le C++ comporte deux nouveaux opérateurs: new et delete. Ce sont les équivalents des fonctions malloc() et free() du C. Ils travaillent sur tous les types de donnée, y compris les objets.

Comparativement à malloc(), new possède de nombreux avantages:

  • Le type du pointeur renvoyé par new est du type attendu. Avec malloc il faut procéder à une conversion explicite.
  • La quantité de mémoire allouée est calculée automatiquement.
  • Il est possible d'affecter une valeur à la variable pointée (si toutefois il ne s'agit pas d'un tableau).
La syntaxe permettant d'utiliser new est on ne peut plus simple:
Pointeur = new Type [ (Valeur) ]
  • Type désigne n'importe quel type de donnée (une structure, une classe, un entier ...).
  • Pointeur désigne un pointeur sur le type Type.
  • Valeur (facultatif) permet de donner une valeur à la variable pointée par Pointeur.
La syntaxe permettant d'utiliser delete est elle aussi très simple:
delete (Pointeur)

Note: New et delete sont bien des opérateurs et non des fonctions comme malloc() et free(). Cela signifie qu'il n'est pas nécessaire de procéder à un include pour pouvoir les utiliser.

II. L'encapsulation

retour sommaire
En informatique on différencie les données des instructions. On peut schématiser en disant qu'un programme est une suite d'instructions destinées à manipuler des données. L'encapsulation (comme son nom le suggère) consiste à isoler dans un même module des données et des instructions. Les instructions d'un module donne ne s'appliquent qu'aux données du même module.

Cette description de l'encapsulation est caricaturale, néanmoins elle donne une bonne idée de cette nouvelle notion. En effet un module n'est utile que s'il peut interagir avec d'autres modules (sinon il ne sert à rien). Aussi tout module doit posséder quelques instructions lui permettant d'agir sur son environnement.

En C, pour créer une variable de type structure il fallait procéder en deux étapes:

  • Définir une structure (processus qui ne provoque pas d'allocation de place mémoire par le compilateur).
  • Déclarer une variable de type structure (préalablement définie).
En C++ on ne parlera pas de "variable de type classe" mais "d'objet" de classe (ou encore d'occurrence ou d'instance). De la même fa&231#;on que pour les structures, la création d'un objet se déroule en deux temps:
  • Définition de la classe (variables et fonctions associées).
  • Déclaration de l'objet dont la classe a été précédemment définie.
Les fonctions associées à une classe se nomment les "méthodes" ou "fonctions membres". Les variables associées sont appelées "données membres", elles se comportent comme des variables globales pour les méthodes associées. On désigne par le terme générique "membre" tout élément de la classe, que ce soit une donnée membre ou une méthode.

Ils existent trois types de classe:

  • Le type struct
  • Le type class
  • Le type union
Certaines méthodes spéciales (les constructeurs et les destructeurs) sont utilisées de la même fa&231#;on par les classes de type struct et de type class. Nous consacrerons un paragraphe à leur description.

Nous n'étudierons que les deux premiers types, le troisième (union) étant peu employée. La seule différence entre les types struct et class concerne les modes d'accès aux membres. Ils existent trois modes d'accès possibles pour qualifier un membre:

public
Un membre public est accessible de l'extérieur de la classe:
Si c'est une variable, on peut consulter et modifier sa valeur.
Si c'est une méthode, on peut l'appeler (pour l'exécuter).
private
Un membre private est absolument inaccessible de l'extérieur. Une donnée membre déclarée private est exclusivement accessible aux méthodes de l'objet dans lequel elle a été déclarée.
protected
Le mode protected est lié à la notion d'héritage que nous aborderons par la suite.
Par défaut tous les membres d'un objet de classe struct sont déclarés public.
Par défaut tous les membres d'un objet de classe class sont déclarés private.

II.1. Struct et class: définition et déclaration

retour sommaire
Note: Dans la suite TypeClasse sera employé pour remplacer les mots clé struct ou class. Autrement dit il sera possible de remplacer le mot TypeClasse par struct ou class.

Définition:
TypeClasse NomClasse
{
déclaration des données membres;
déclaration des méthodes;
}
NomClasse désigne le nom de la classe que l'on définit.

Pour modifier le mode d'accès par défaut des membres de la classe, il faut utiliser la syntaxe suivante lors de leur déclaration:
ModeAcces: Déclarations

  • ModeAcces représente l'un des trois mots clés suivant (dont les effets ont déjà été décrits): public, private ou protected.
  • Déclarations représente la liste des déclarations des membres de la classe sur lesquels doit s'appliquer le changement de mode.
La déclaration des données membres utilise la même syntaxe que celle utilisée pour déclarer habituellement des variables.

Pour les méthodes, les choses sont un peu différentes:

  • Soit le corps de la méthode s'inscrit dans la définition de la classe. Dans ce cas il n'y a pas de différence avec la déclaration habituelle. Nous emploierons l'expression "définition non déportée".
  • Soit le corps de la méthode est définie en dehors de la classe, nous emploierons dans ce cas l'expression
  • définition déportée".
La première solution est utilisée pour les méthodes de petite taille. Pour les méthodes de taille plus importantes la deuxième solution est préférable. Dans ce cas:
  • La méthode doit obligatoirement être définie en aval de la définition de la classe.
  • Il faut faire précéder le nom de la méthode du nom de la classe à laquelle elle est associée en appliquant la syntaxe suivante: NomClasse::NomMethode
Déclaration d'un objet: NomClasse NomObjet
  • NomClasse représente le nom de la classe (précédemment définie) à laquelle est associé l'objet.
  • NomObjet représente le nom de l'objet.

II.2. Accés aux membres d'une classe

retour sommaire
On accède aux membres d'une classe de la même manière que l'on accède aux données d'une structure:
  • Soit via l'opérateur "." s'il s'agit d'un objet statique.
  • Soit via l'opérateur "->" s'il s'agit d'un pointeur sur un objet.
Bien sur l'accès à un membre n'est possible que si le mode d'accès associé à cet élément est public.

II.3. Exemples récapitulatifs sur les classes

retour sommaire
Il ne faut pas chercher dans cet exemple une quelconque utilité. C'est un exemple simple et largement commenté qui expose clairement les notions vues plus haut.


#include < stdio.h>

// Définition d'une classe struct
struct Pixel
{
        // déclaration
        // les données membres suivantes sont publiques (par défaut)
        int posx, posy;

        // déclaration
        // les méthodes suivantes sont publiques (par défaut)
        // leur définition est déportée
        int GetX ();
        int GetY ();
        void inputx (int);
        void inputy (int);
        Pixel (void);

        // définition non déportée
        // Affiche est publique (par défaut)
        void affiche (void)
        {
                fprintf (stdout, "\n affiche : \n");
                fprintf (stdout, "x=%d \n y=%d \n", x, y);

                fprintf (stdout, "red=%d \n green=%d \n blue=%d \n", red, green, blue);
        }

         private: //concerne toute la suite

        // déclaration
        // les données membres suivantes sont privées
        int x, y;
        char red, green, blue;

        // définition non déportée
        // duplique est privée
        void duplique (void)
        {
                posx=x;
                posy=y;
                if (x<255) red = x; else x=255;
                if (y<255) green = y; else y=255;
                if ((x+y) < 255) blue = (x+y); else blue = 255;
        }
 };


 // définition déportée de Pixel, méthode de Pixel
 // ceci est un constructeur (cf suite)
 Pixel::Pixel (void)
 {
        x = 0;
        y = 0;
        red = 15;
        green = 150;
        blue = 200;
 }

// définition déportée de GetX, méthode de Pixel
int Pixel::GetX (void)
{
        return (x);
}

// définition déportée de GetY, méthode de Pixel
int Pixel::GetY (void)
{
        return (y);
}

// définition déportée de inputx, méthode de Pixel
void Pixel::inputx (int a)
{
        x = a;
        duplique (); // déjà définie
}

// définition déportée de inputy, méthode de Pixel
void Pixel::inputy (int a)
{
        y = a;
        duplique (); // déjà définie
}


void main (void)
{
        // déclaration de PIX, objet de la classe Pixel
        Pixel PIX;
        PIX.posx = 50;
        PIX.posy = 150;

        // posx et posy sont publiques -> on peut y accéder et modifier leur valeur
        fprintf (stdout, "posx=%d \n", PIX.posx); PIX.posx = 140;
        fprintf (stdout, "posy=%d \n", PIX.posy); PIX.posy = 150;

        // GetX, GetY, inputx, inputy et affiche sont publiques
        PIX.inputx (10);
        PIX.inputy (20);
        fprintf (stdout, "GetX -> x = %d \n", PIX.GetX());
        fprintf (stdout, "GetY -> y = %d \n", PIX.GetY());
        PIX.affiche();
}
Nous pouvons aussi utiliser le type class pour cet exemple. Pour obtenir le même comportement il est nécessaire de modifier les déclarations des membres de la classe Pixel:

  • Tous les membres implicitement public (par défaut) doivent être explicitement déclarés public.
  • Pour les membres explicitement déclarés private, il n'est plus nécessaire d'utiliser le mot clé private.
Seule la définition de la classe change, voici la nouvelle déclaration:

class Pixel
{
        // déclaration
        // les données membres suivantes sont privées (par défaut)
        int x, y;
        char red, green, blue;

        // définition non déportée
        // duplique est privée (par défaut)
        void duplique (void)
        {
                posx=x;
                posy=y;
                if (x<255) red = x; else x=255;
                if (y<255) green = y; else y=255;
                if ((x+y) < 255) blue = (x+y); else blue = 255;
        }

        public: // s'applique à tout ce qui suit
        // déclaration
        // les données membres suivantes sont publiques
        int posx, posy;

        // déclaration
        // les méthodes suivantes sont publiques
        // leur définition est déportée
        int GetX ();
        int GetY ();
        void inputx (int);
        void inputy (int);
        Pixel (void);

        // définition non déportée
        // Affiche est publique
        void affiche (void)
        {
                fprintf (stdout, "\n affiche : \n");
                fprintf (stdout, "x=%d \n y=%d \n", x, y);
                fprintf (stdout, "red=%d \n green=%d \n blue=%d \n", red, green, blue);
        }
};
On remarque aisément que, dans le cas particulier de cet exemple, le type struc est mieux adapté que le type class. En effet il y a plus de membres public que de membres private (l'écriture est moins lourde).

II.4. Constructeurs et destructeurs

retour sommaire
Un constructeur est une fonction membre spéciale permettant d'effectuer des opérations lors de la déclaration d'un objet d'une classe donnée.

Un destructeur est une fonction membre spéciale permettant d'effectuer des opérations lors de la destruction d'un objet.

II.4.1. Les constructeurs

retour sommaire
Déclaration d'un constructeur:
Le constructeur associé à une classe doit porter le même nom que la classe à laquelle il est associé. D'autre part un constructeur ne renvoie rien, pas même le type void. [ModeAcces:] NomClasse (liste des arguments)
  • ModeAcces représente le mode d'accès au constructeur (le plus souvent il est public). ModeAcces est facultatif.
  • NomClasse représente à la fois le nom du constructeur et le nom de la classe à laquelle il est associé.
  • La liste des arguments peut bien sur être vide.
Définition d'un constructeur:
La définition d'un constructeur est la même que pour tout autre méthode. Elle peut être déportée ou non.

I.4.2. Les destructeurs

retour sommaire
Déclaration d'un destructeur:
Le destructeur associé à une classe doit porter le même nom, précédé du caractère "~", que la classe à laquelle il est associé. D'autre part un destructeur ne renvoie rien, pas même void. [ModeAcces:] ~NomClasse (liste des arguments)
  • ModeAcces représente le mode d'accès au destructeur (le plus souvent il est publique). NomAcces est facultatif.
  • NomClasse représente à la fois le nom du destructeur et le nom de la classe à laquelle il est associé.
  • La liste des arguments peut bien sur être vide.
Définition d'un destructeur:
La définition d'un destructeur est la même que pour tout autre méthode. Elle peut être déportée ou non.

II.4.3. Exemple récapitulatif sur les constructeurs et les destructeurs

retour sommaire

#include < stdio.h>

class IncClasse
{
        // Déclaration des données membres privées (par défaut)
        int valeur, inc;

        public: // concerne toute la suite

        // Déclaration du destructeur publique
        ~IncClasse (void);

        // Définition non déportée de la méthode calc publique
        int calc (void)
        {
                 valeur += inc;
                 return (valeur);
        }

        // Définition non déportée du constructeur public
        // Remarquer l'utilisation d'arguments par défaut
        IncClasse (int a = 1, int b = 1)
        {
                valeur = a;
                inc = 1;
                fprintf (stdout, "\n Création d'un objet de la classe IncClasse \n");
        }
};

// définition déportée du destructeur
IncClasse::~IncClasse (void)
{
        fprintf (stdout, "\n Destruction de l'objet alloues \n");
}

void main (void)
{
        int i;

        // Utilisation du constructeur de la classe IncClasse sur une variable allouée
        // (en vue d'une restauration de place mémoire)
        IncClasse *Pointeur;

        // Allocation de variable
        Pointeur = new IncClasse;

        // utilisation de la méthode calc
        // noter l'opérateur -> car Pointeur est un pointeur.
        for (i=0; i<10; i++)
                fprintf (stdout, "%d", Pointeur->calc());

        // utilisation du destructeur de la classe IncClasse
        Pointeur->IncClasse::~IncClasse ();

        // restauration de la place mémoire au système
        delete Pointeur;
}
Comme nous pouvons le remarquer sur cet exemple, l'appel du destructeur ne suffit pas pour libérer l'espace mémoire. Il faut utiliser delete. D'autre part la syntaxe utilisée pour appeler le destructeur est plutôt lourde.

Aussi la règle suivante est très utile:
Lors de l'appel de delete sur un objet, le destructeur de la classe à laquelle appartient l'objet est automatiquement appelé. Ainsi l'écriture du programme est allégée.

III. La surcharge

retour sommaire
La pratique de la surcharge permet de rendre un programme plus lisible et dans certains cas facilite grandement la tâche du concepteur.

III.1. Surcharge des fonctions

retour sommaire
En C il est impossible de donner le même nom à deux fonctions différentes. En C++ cette interdiction est levée à condition de respecter la règle suivante: Deux fonctions sont autorisées à porter le même nom si et seulement si leurs signatures sont différentes. Le terme "signature" est utilisé pour designer la liste des arguments d'une fonction.

Exemple de surcharge de fonction:
#include < stdio.h>

int somme (int a, int b)
{
        int resultat;

        resultat = a + b;
        return (resultat);
}

int somme (int a, int b, int c)
{
        int resultat;

        resultat = a + b + c;
        return (resultat);
}

void main (void)
{
        // Deux fonctions distinctes portant le même nom
        // Leur signature sont différentes
        int somme (int, int);
        int somme (int, int, int);

        fprintf (stdout, "somme(1, 2)=%d \n", somme(1, 2));
        fprintf (stdout, "somme(1, 2, 3)=%d \n", somme(1, 2, 3));
}
Remarques:

  • Les définitions suivantes sont correctes
    int somme (int a, int b)
    int somme (float a, float b)
    float somme (int a, float b)
    int somme (float a, int b)
  • Par contre les définitions qui suivent provoquent une erreur de compilation
    int somme (int a, int b)
    float somme (int a, int b)
    Dans ce cas la signature est identique pour les deux fonctions. Or le compilateur ne se base que sur la signature de la fonction pour opérer son choix. Peut être pensez vous: et si la valeur renvoyée par somme est affectée à une variable de type float, ne pourrait on pas en déduire la fonction à utiliser ? Ce serait oublier un peu vite le mécanisme de conversion implicite.
  • De même les déclarations suivantes sont incorrectes
    int somme (int a, int b, int c=0)
    int somme (int a, int b)
    Supposons que dans le programme vous rencontrez l'expression somme(1, 2). Vous (et a fortiori le compilateur) ne pourrez pas savoir quelle fonction appliquer.

III.2. La surcharge des opérateurs

retour sommaire
En C la surcharge n'existait pas officiellement. Néanmoins les opérateurs arithmétiques étaient surchargés. En effet, prenons le cas de l'opérateur +: on peut l'appliquer aussi bien sur des entiers, sur des flottants... c'est donc un opérateur surchargé.

La nouveauté en C++, c'est que l'utilisateur peut choisir de redéfinir un opérateur afin que celui ci puisse s'appliquer sur n'importe quel type d'objet. Il faut bien noter que l'on ne peut surcharger un opérateur que pour le faire agir sur un objet d'une classe.

Exemple de surcharge d'opérateur:

#include < stdio.h>

class Vecteur
{
        // vecteur (x,y,z), Vect privé par défaut
        int Vect[3];

        public: // concerne toutes les déclarations qui suivent

        // constructeur publique avec arguments par défaut
        Vecteur (int a=0, int b=0, int c=0)
        {
                Vect[0] = a;
                Vect[1] = b;
                Vect[2] = c;
        }

        // Fonction d'affichage
        void affiche (void)
        {
                fprintf (stdout, "x = %d \n", Vect[0]);
                fprintf (stdout, "y = %d \n", Vect[1]);
                fprintf (stdout, "z = %d \n", Vect[2]);
        }

        // Vin nécessaire car Vect est privé
        void Vin (int a, int b, int c)
        {
                Vect[0] = a;
                Vect[1] = b;
                Vect[2] = c;
        }

        Vecteur operator +(Vecteur V)
        {
                Vecteur Vres; // (0,0,0) par défaut
                int i;

                // Vres = Vect + V
                // Vect est le vecteur à gauche du signe +
                // V est le vecteur à droite du signe +
                // + appartient à l'objet Vect

                Vres.Vin (V.Vect[0]+Vect[0], V.Vect[1]+Vect[1], V.Vect[2]+Vect[2]);

                return (Vres);
        }
};

void main (void)
{
        Vecteur V1 (1,2,3);
        Vecteur V2 (4,5,6);
        Vecteur V3; // (0,0,0) par défaut

        // + fait partie de l'objet V1
             // Il faut respecter l'usage de l'opérateur +

        V3 = V1 + V2;

        fprintf (stdout, "V1: \n");
        V1.affiche();
        fprintf (stdout, "V2: \n");
        V2.affiche();
        fprintf (stdout, "V3: \n");
        V3.affiche();
}
Dans cet exemple l'opérateur + agit sur les objets de la classe Vecteur.

Certains opérateurs ne peuvent pas être surchargés, en voici la liste:

  • l'opérateur . accès aux membres
  • l'opérateur .* pointeur sur membre
  • l'opérateur :: résolution de portée
  • l'opérateur ?: ternaire
  • l'opérateur sizeof taille en octet d'un objet

IV. L'héritage

retour sommaire
L'objectif de la POO est de décomposer un programme en un ensemble de modules, ces derniers étant au maximum indépendants les un des autres. Chaque module doit remplir une tâche précise. Mais supposons qu'un module ait besoin d'accéder directement aux données membres d'un autre module. C'est la qu'intervient la notion d'héritage. Dire qu'une classe D (pour dérive) hérite d'une classe C équivaut à dire que D accède directement à certains membres de C (voir tous). Nous dirons que D dérive de C.

IV.1. L'héritage simple

retour sommaire
Pour indiquer que la classe D hérite de la classe C, la syntaxe utilisée est la suivante lors de la définition de D:
TypeClasse NomClasseD : [ModifAcces] NomClasseC
  • TypeClasse est utilisé pour remplacer l'un des trois mots clés struct, class ou union.
  • NomClasseD représente le nom de la classe D.
  • NomClasseC représente le nom de la classe C.
  • ModifAcces représente l'un des deux modificateurs d'accès suivants: public ou private. ModifAcces est facultatif. En cas d'absence le compilateur attribut les modificateurs par défauts, à savoir:
    • public si B est de type struct.
    • private si B est de type class.
Les droits d'accès de D sur les membres de C dépendent de deux paramètres:
  • modes d'accès spécifiques de chaque membre de C.
  • modificateur d'accès utilis2 pour décrire la fa&231#;on dont D hérite de C (en l'occurrence ModifAcces utilisé ci dessus pour la définition de A).
Nous n'avions pas abordé le mode d'accès private, en effet il fallait auparavant aborder la notion d'héritage. Un membre déclaré private se comporte de la fa&231#;on suivante: Supposons que le membre M de la classe C soit déclaré protected. Alors M n'est utilisable que par les fonctions membres de C et les fonctions membres des classes dérivées de C (sous certaines conditions).

Tableaux donnant le principe de fonctionnement des modificateurs d'accès
acces en classe mere modificateur d'acces acces en classe derivee
public public public
proteded public protected
private public inaccessible
public private private
protected private private
private private inaccessible

IV.2. Héritage: les constructeurs et les destructeurs

retour sommaire
Note: Dans toute la suite nous supposerons que la classe D dérive de la classe C. Soit M un membre de C.

Une fausse question vient naturellement à l'esprit: Lors de la déclaration d'un objet de la classe D, que se passe t'il si aucun objet de la classe C n'est déjà déclaré ?

Heureusement cette question ne se pose pas. En effet lors de la déclaration d'un objet de classe D (Nous l'appellerons OD), un objet de classe C est automatiquement créé (Nous l'appellerons OC). L'objet OC est accessible via OD mais il n'est pas directement accessible car dans le programme il ne porte pas de nom.

Il est possible de définir un constructeur permettant d'effectuer des opérations sur OD mais aussi sur OC. Ceci est indispensable si OD a besoin de certaines variables définies dans OC lors de son initialisation. Pour déclarer un tel constructeur on utilise la syntaxe suivante:
[NomClasseD::]NomClasseD (liste des arguments pour D) : NomClasseC (liste des arguments pour C)

  • NomClasseD représente le nom de la classe dérivée D. La liste des arguments pour D peut bien sur être vide.
  • NomClasseC représente le nom de la classe mère C, et en particulier le nom du constructeur associé à C. La liste des arguments pour C peut être vide si le constructeur de C n'a pas besoin d'argument.
  • [NomClasseD::] n'est utilisé que pour les définitions déportées.
Lors de l'appel du constructeur NomClasseD le constructeur NomClasseC sera automatiquement appelé. De plus NomClasseC s'exécutera avant NomClasseD, ce qui est logique si l'on tient compte de la remarque précédente.

Si aucun constructeur sur OC n'est spécifié, le compilateur fera appel au constructeur de C (avec ses arguments par défaut) s'il existe. Si ce dernier n'existe pas OC sera créé par appel du constructeur par défaut.

Pour accéder au supérieur hiérarchique de D la syntaxe est la suivante:
ObjetClasseD.NomClasseC::MembreDeC

  • ObjetClasseD représente un objet de la classe dérivée D.
  • NomClasseC représente le nom de la classe dont dérive la classe de ObjetClasseD.
  • MembreDeC représente un membre de la classe NomClasseC.
Exemple récapitulatif sur l'héritage simple:

#include < stdio.h>

class Mere
{
        // x, y, z private par défaut
        int x, y, z;

        // Définition non déportée du constructeur public
        public:
        void Mere (int a=0, int b=0, int c=0)
        {
                x=a;
                y=b;
                z=c;
        }

        // Déclaration de getx, gety, getz protected
        protected:
        int getx (void);
        int gety (void);
        int getz (void);


        // Définition non déportée de Print public
        public:
        void Print (void)
        {
                fprintf (stdout, "Classe Mere: x=%d, y=%d, z=%d \n", x, y, z);
        }
}

// définition déportée de getx
int Mere::getx (void)
{
        return (x);
}

// définition déportée de gety
int Mere::gety (void)
{
        return (y);
}

// définition déportée de getz
int Mere::getz (void)
{
        return (z);
}

class Fille
{
        // show private par défaut
        int show;

        // Définition du constructeur public
        public:
        Fille (val=0) : Mere (val, 1)
        {
                show = val;
        }

        // Défnition non déportée de affiche
        public:
        void affiche (void)
        {
                if (show)
                {
                        fprintf (stdout, "Fille: \n");
                        fprintf (stdout, "X=%d \n", getx());
                        fprintf (stdout, "Y=%d \n", gety());
                        fprintf (stdout, "Z=%d \n", getz());
                }
                else
                {
                        fprintf (stdout, "Desole! show=0 \n");
                }
        }
}

void main (void)
{
        // déclaration d'un objet de classe fille
        Fille Fclasse (10);

        // déclaration d'un objet de classe Mere
        Mere Mclasse (1, 2, 3);

        // Appel d'un membre de la classe Mere associé à Fclasse
        // L'accés à cette classe ne peut se faire que via l'objet Fclasse.
        Fclasse.Mere::Print; // x=10 y=1 z=0

        // Appel d'un membre de Fille
        Fille.afiiche();

        // Appel d'un membre de Mclasse
        Mclasse.Print; // x=1 y=2 z=3

        // On remarque que Mclasse n'a aucun lien de parentée avec Fclasse.
        // La classe Fille dépend de la classe Mere. Mais l'inverse n'est pas vrai.
}

IV.3. Héritage multiple

retour sommaire
Note: Dans la suite nous supposerons que la classe D dérive des classes C1, C2, ..., CN.

Jusqu'à présent nous avons considéré le cas ou une classe héritait d'une seule classe. Mais il se peut qu'une classe hérite de plusieurs classes. C'est ce que l'on appelle l'héritage multiple. Tout ce qui a été dit pour l'héritage simple reste valable pour l'héritage multiple.

Comme pour l'héritage simple, il est possible de définir un constructeur agissant sur D, C1, C2, ..., CN. La syntaxe utilisée pour déclarer un tel constructeur n'est qu'une généralisation de la syntaxe vue précédemment:
NmClasseD (liste des arguments de D): NomClasseC1 (liste des arguments de C1), NomClasseC2 (liste des arguments de C2), ..., NomClasseCN (liste des arguments de CN)

Lors de l'appel du constructeur NomClasseD les constructeurs de NomClasseC1, ..., NomClasseCN, seront automatiquement appelés.

Exemple sur l'héritage multiple:

#include < stdio.h>

class Mere1
{
        // private par défaut
        int a, b;

        // définition non déportée du constructeur
        protected:
        Mere1 (int v1=0,int v2=0)
        {
                 a = v1;
                 b = v2;
        }

        // définition non déportée de affiche1
        public:
        void affiche1 (void)
        {
                 fprintf (stdout, "Mere1: a=%d et b=%d \n", a, b);
        }
};

class Mere2
{
        // private par défaut
        int a, b;

        // définition non déportée du constructeur
        protected:
        Mere2 (int v1=0 ,int v2=0)
        {
                 a = v1;
                 b = v2;
        }

        // définition non déportée de affiche1
        public:
        void affiche2 (void)
        {
                 fprintf (stdout, "Mere2: a=%d et b=%d \n", a, b);
        }
};

class Fille : private Mere1, private Mere2
{
        // private par défaut
        int a, b;

        public: // concerne toute la suite

        // définition non déportée de affiche
        void affiche (void)
        {
                 affiche1(); // hérite de la classe Mere1
                 affiche2(); // hérite de la classe Mere2
                 fprintf (stdout, "Fille: a=%d et b=%d \n", a, b);
        }

        // définition non déportée du constructeur
        // concerne l'initialisation simultanée de Mere1, Mere2 et Fille
        Fille (int v1=0 ,int v2=0) :
        Mere1 (v2, v1), Mere2 (v1, v1)
        {
                 a = v1;
                 b = v2;
        }
};

void main (void)
{
        Fille Fclasse (10, 20);
        // remarquer qu'aucun objet de classe Mere1 et Mere2 n'est déclaré

        // à part les constructeurs, affiche est le seul membre public
        Fclasse.affiche();
}

IV.4. Fonctions virtuelles

retour sommaire
La notion de fonction virtuelle est plutôt délicate à aborder. Aussi nous commencerons par exposer un exemple permettant de montrer le cadre de leur utilisation.

Supposons que vous possédiez une collection d'objets graphiques (des points, des cercles, des carrés, ...) et que vous vouliez les afficher à l'écran. Pour un programmeur habitué au C, la première idée qui vient à l'esprit est de créer une procédure d'affichage pour chaque objet. En effet chaque objet ayant des caractéristiques différentes, on ne peut pas les afficher de la même manière.

Cependant si l'on étudie le problème de plus près, on peut trouver des points communs à chaque procédure d'affichage. Ainsi chaque procédure nécessitera par exemple: une position, une couleur d'affichage, un style (hachuré, plein ...)... On pourrait imaginer une procédure d'affichage globale, absolument indépendante de l'objet à afficher. Autrement dit le jour ou nous rajoutons un nouvel objet à notre collection, il ne sera pas nécessaire de modifier (un tant soit peu) cette fonction d'affichage.

Bien sur, en se torturant l'esprit, on peut imaginer écrire une telle fonction en C. Mais alors il serait sûrement plus simple d'écrire une fonction d'affichage spécifique à chaque objet. En C++, l'utilisation des fonctions virtuelles rend cette tache très simple.

Comment allons nous procéder ?

Tout d'abord nous remarquons que tous les objets nécessitent un certain nombre d'informations communes nécessaires à leur affichage. Parmi ces informations nous en choisirons deux: la position et la couleur. Autrement dit (d'une fa&231#;on "orientée objet") tout objet graphique dérive de deux classes: la classe position et la classe couleur. Ensuite il est clair que chaque objet ne se dessine pas de la même fa&231#;on. Par conséquent nous seront obligés (il n'y a pas de mystère) de définir une procédure d'affichage pour chaque objet.

Mais si nous devons écrire une procédure d'affichage pour chaque objet, en quoi cette démarche diffère t'elle de la démarche traditionnelle (utilisée en C) ?

L'astuce est la suivante: toutes les procédures d'affichage porteront le même nom, posséderont la même signature et renverront le même type de donnée. Nous choisirons le nom Print pour appeler cette fonction. Ainsi dans la fonction globale d'affichage, que nous nommerons Affich, nous utiliserons la fonction Print. La fonction Affich sera donc indépendante de l'objet à afficher.

Le choix de la position de la déclaration de Affich dans la hiérarchie est important. En effet cette position doit remplir deux conditions:

  • Affich doit pouvoir accéder aux informations position et couleur. Par conséquent Affich doit être déclarée dans une classe héritière des classes position et couleur (ou dans l'une de ces deux classes, l'important étant qu'elle accède aux informations).
  • Toutes les classes définissant les objets graphiques doivent hériter de la classe dans laquelle a été déclarée Affich.
En ce qui concerne la fonction Print, celle ci doit être déclarée comme fonction virtuelle. Il suffit pour cela de faire précéder la première déclaration (en effet il y en aura plusieurs) de Print du mot clé virtual. D'autre part Affich doit pouvoir accéder à Print, autrement dit la première déclaration de print doit se trouver dans une classe de niveau hiérarchique supérieur ou égal à celui de Affich. Les déclarations suivantes de Print (dans chaque classe d'objet graphique) n'auront pas besoin du mot clé Virtual lors de leur déclaration (cependant ce seront aussi des fonctions virtuelles).

Mais comment le compilateur sait il quelle fonction Print utiliser si elles ont exactement la même déclaration ?

En fait Print est une fonction virtuelle. Et à ce titre le choix de l'utilisation de telle ou telle version de Print ne se fera pas lors de la compilation (mécanisme de ligature statique) mais lors de l'exécution du programme (ligature dynamique). Ainsi lors de l'appel de Affich, Affich sera automatiquement reliée à la bonne fonction Print correspondant à l'objet à afficher.

Note: Une fonction virtuelle doit obligatoirement être une fonction membre. Autrement dit elle doit appartenir à une classe. Exemple sur les fonctions virtuelles:
#include < stdio.h>

#include < conio.h>

class Position_Etoile
{
        protected:
        int x, y;


        public: // concerne toute la suite

        // constructeur
        Position_Etoile (int a=0, int b=0)
        {
                x = a;
                y = b;
        }

        // déclaration d'une fonction virtuelle
        virtual void Print (void)
        {
                fprintf (stdout, "* Une Etoile");
        }

        // déclaration utilisant la fonction virtuelle Print
        void affiche()
        {
                gotoxy (x, y);
                Print ();
        }
};

class Deux_Etoiles : public Position_Etoile
{
        public:

        // remarquer que le corps du constructeur est vide
        Deux_Etoiles (int a=10, int b=10) : Position_Etoile (a, b)
        { }

        // déclaration de la fonction virtuelle
        void Print (void)
        {
                fprintf (stdout, "** Deux Etoiles");
        }
};

class Trois_Etoiles : public Position_Etoile
{
        public:

        // remarquer que le corps du constructeur est vide
        Trois_Etoiles (int a=15, int b=15) : Position_Etoile (a, b)
        { }

        // déclaration de la fonction virtuelle
        void Print (void)
        {
                fprintf (stdout, "*** Trois Etoiles");
        }
};

void main (void)
{
        Position_Etoile Une (2); // donc (2,0) par défaut
        Deux_Etoiles Deux (8); // donc (8,10) par défaut
        Trois_Etoiles Trois (16, 16);

        Une.affiche();
        Deux.affiche();
        Trois.affiche();
}

IV.5. Fonctions virtuelles pures et classes abstraites

retour sommaire
Une fonction virtuelle doit être definie, si elle ne l'est pas elle doit être declarée virtuelle pure. Pour cela il suffit de faire suivre la déclaration de la fonction de l'opérateur pure "=0". La syntaxe est la suivante: virtual NomFonction (liste des arguments) = 0;

Note: Une fonction virtuelle pure est, comme son nom l'indique, une fonction virtuelle. Par conséquent elle doit obligatoirement faire partie d'une classe.

Une classe abstraite est une classe qui contient au moins une fonction virtuelle pure. Une telle classe ne peut servir que de classe de base pour d'autres classes. Il est interdit de déclarer un objet de classe abstraite. Cela se comprend aisément car une classe abstraite contient une fonction membre virtuelle pure qui n'est pas définie.

Exemple de classe abstraite:

Reprenons l'exemple précédent en déclarent Print comme étant virtuelle pure.

#include < stdio.h>

#include < conio.h>

class Position // Classe virtuelle pure
{
        protected:
        int x, y;


        public: // concerne toute la suite

        // constructeur
        Position (int a=0, int b=0)
        {
                x = a;
                y = b;
        }

        // déclaration d'une fonction virtuelle pure
        virtual void Print (void) = 0;


        // déclaration utilisant la fonction virtuelle Print
        void affiche()
        {
                gotoxy (x, y);
                Print ();
        }
};

class Deux_Etoiles : public Position
{
        public:

        // remarquer que le corps du constructeur est vide
        Deux_Etoiles (int a=10, int b=10) : Position (a, b)
        { }

        // déclaration de la fonction virtuelle
        void Print (void)
        {
                fprintf (stdout, "** Deux Etoiles");
        }
};

class Trois_Etoiles : public Position
{
        public:

        // remarquer que le corps du constructeur est vide
        Trois_Etoiles (int a=15, int b=15) : Position (a, b)
        { }

        // déclaration de la fonction virtuelle
        void Print (void)
        {
                fprintf (stdout, "*** Trois Etoiles");
        }
};

void main (void)
{
        Deux_Etoiles Deux (8); // donc (8,10) par défaut
        Trois_Etoiles Trois (16, 16);

        Deux.affiche();
        Trois.affiche();
}

V. Les flux

retour sommaire

V.1. Présentation générale

retour sommaire
En C++ un flux est un flot de données entre une source et une destination. On peut caractériser un flux par:
  • Le type de source ou de destination auquel le flux est associé.
  • La fa&231#;on dont les données transitent: individuellement ou par groupe.
  • Le sens de transit des données: lecture seule, écriture seule ou lecture / écriture simultanée.
Parmi les sources et les destinations possibles, on peut distinguer les fichiers et la mémoire vive. Nous qualifierons les flux en mémoire vive de flux chaîne.

Pour manipuler les données on peut utiliser un tampon. Un tampon permet de stocker une certaine quantité de donnée dans le but de les faire transiter en groupe. En général cette solution permet d'accroître les performances d'un programme.

Les librairies du C++ comportent un certain nombre de classes destinées à manipuler les flux. Ces classes sont organisées en deux hiérarchies: l'une concerne les tampons (nous l'appellerons la petite hiérarchie car elle comporte beaucoup moins de classes que la seconde) et l'autre pas (nous l'appellerons la grande hiérarchie). Ces deux hiérarchies sont étroitement liées comme nous le verrons par la suite.

V.1.1 La grande hiérarchie

retour sommaire
Cette hiérarchie comprend seize classes et se développe sur quatre niveaux. On distingue trois grands types de classes:
  • Les classes spécialisées dans un type de flux particulier (flux chaîne, flux sur fichiers ou flux standards).
  • Les classes spécialisées dans une opération particulière (lecture, écriture, lecture/écriture) parmi lesquelles on distingue celle utilisant ou non un tampon.
  • Les classes dérivant simultanément de deux types de classes précédemment décrits. Ces classes sont très spécialisées.
Ils existent différents shémas permettant d'illustrer la grande hiérarchie. Nous la développerons niveaux par niveaux. Nous remarquons une certaine symétrie dans l'arbre généalogique:
  • Les classes spécialisées dans le traitement des flux sur fichiers sont à droite et celles spécialisées dans le traitement des flux chaînes à gauche.
  • Relativement à chaque type de flux les classes spécialisées dans les opérations de lecture (Input) sont à droite et celles spécialisées dans les opérations d'écritures (Output) sont à gauche.
  • Les classes spécialisées dans les opérations de lectures / écritures simultanées sont au centre.
Note: Pour des raisons de clarté la classe constream (dérivée de Ostream) n'est pas représentée sur le schéma (elle se situe normalement au niveau 3).

Niveau 1

Ios: Cette classe au sommet de la hiérarchie offre des opérations communes aux entrées et aux sorties (pour tous les types de flux). Les classes dérivées de Ios sont spécialisées dans les entrées / sorties avec des fonctions de formatage de haut niveau pour certains flux. La classe Ios possède un pointeur vers la classe de base de la petite hiérarchie (à savoir la classe Streambuf).

Niveau 2

Le niveau 2 de contient quatre classes dérivées de Ios. Parmi ces quatre classes on peut distinguer deux groupes:

  • Le premier groupe comprend les classes StrStreambase et Fstreambase. Il est spécialisé dans les opérations sur un type de flux particulier. StrStreambase traite des flux chaînes et fstreambase traite des flux de fichier.
  • Le deuxième groupe comprend les classes Ostream et Istream. Il est spécialisé dans la gestion des tampons. Ostream s'occupe des sorties formatées ou non vers un tampon Streambuf tandis que Istream s'occupe des entrées.
Niveau 3

Le niveau 3 contient sept classes parmi lesquelles on peut distinguer quatre groupes:

group 1
Le premier comprend les classes Ostrstream, Istrstream , Ofstream et Ifstream. Il est spécialisé dans les opérations avec tampon sur des types de flux particuliers. Chaque type de flux (chaîne ou fichier) dispose d'un type de tampon spécialisé (Strstreambuf ou Filebuf).
Ostrstream (dérivée de StrStreambase et Ostream) traite les sorties de flux chaîne vers un Strstreambuf.
Istrstream (dérivée de StrStreambase et Istream) traite les entrées de flux chaîne depuis un Strstreambuf.
Ofstream (dérivée de Fstreambase et Ostream) traite les sorties de flux sur fichier vers un Filebuf.
Ifstream (dérivée de Fstreambase et Istream) traite les entrées de flux sur fichier depuis un Filebuf.
groupr 2
Le deuxième groupe comprend les classes Ostream_Withassign et Istream_Withassign. Il est utilisé pour la définition des flux standards.
Ostream_Withassign (dérivée de Ostream) comprend la définition des flux de sortie standards (cout, cerr, clog).
Istream_Withassign (dérivée de Istream) comprend la définition du flux d'entrée standards (cin).
groupe 3
Le troisième groupe se compose de la classe Iostream (dérivée de Istream et de Ostream). La classe Iostream s'occupe des opérations d'entrées / sorties simultanées avec utilisation de tampon Streambuf sur tous les types de flux.
groupe 4
Le quatrième groupe se compose de la classe Constream (dérivée de Ostream) qui est spécialisée dans les sorties vers la console. Pour ne pas surcharger le schéma illustrant la grande hiérarchie cette classe n'est pas représentée.
Niveau 4

Le niveau 4 contient trois classes. Il est possible de distinguer deux groupes.

groupe 1
Le premier groupe contient les classes Strstream et Fstream. Il traite des opérations d'entrées / sorties simultanées avec tampon sur des types de flux particuliers.
Strstream (dérivée de Iostream et Strstreambase) s'occupe des entrées / sorties simultanées avec tampon sur des flux chaînes.
Fstream (dérivée de Fstreambase et de Iostream) s'occupe des entrées / sorties simultanées avec tampon sur des flux de fichiers.
groupe 2
Le second groupe se compose de la classe Iostream_Withassign.

V.1.2. La petite hiérarchie

retour sommaire
La petite hiérarchie ne comporte que quatre classes et se développe sur deux niveaux. Ces classes sont spécialisées dans la manipulation des tampons.

niveau 1 Le niveau 1 correspond à la classe Streambuf. Cette dernière sert de classe de base aux trois autres classes de tampon. La classe Streambuf est accessible par les classes de la grande hiérarchie via un pointeur (pointeur de Ios vers Streambuf).

niveau 2 Le niveau 2 comprend trois classes spécialisées dans un type de flux particulier.

  • La classe Strstreambuf s'occupe de la gestion des tampons associés aux flux chaînes.
  • La classe Conbuf traite les tampons associés aux sorties sur console.
  • La classe Filebuf gère les tampons attachés aux flux sur fichiers.

V.2 Utilisation

retour sommaire
L'ensemble des classes constituant la hiérarchie des flux constitue un vaste réservoir de fonctions membres. La plupart de ces fonctions sont surchargées. Notre but n'est pas de passer en revue toutes ces fonctions, le guide du C++ est fait pour &231#;a.

Nous expliciterons plutôt le rôle et le fonctionnement de certaines classes en donnant des exemples simples et largement commentés.

V.2.1. Manipulations de fichiers sur disque avec utilisation d'un buffer

retour sommaire
Qu'est ce qu'un buffer ?
Un buffer est une zone réservée en mémoire vive destinée à contenir une certaine quantité d'information. Son utilisation permet d'accélérer le traitement des fichiers stockés sur disque. En effet si l'on tient compte de la lenteur des accès disque il est préférable de manipuler une grande quantité d'information (stockée dans le buffer) d'un coup plutôt que de les manipuler individuellement.

Comment associe t'on un buffer à un fichier ?
Pour associer un buffer à un fichier ils existent plusieurs méthodes. Pour bien comprendre les diverses fa&231#;ons de procéder il faut savoir que trois objets (dans le sens des classes) sont nécessaires à cette opération:

  • Un fichier (de type File bien sur.
  • Un buffer (de type Filebuf ou Streambuf) bien sur.
  • Un flux (de type Ifstream, Ofstream ou Iofstream) permettant d'effectuer des opérations sur le buffer (et donc sur le fichier).
Pour pouvoir manipuler le fichier il faut relier entre eux ces trois objets. L'opération peut se faire en une ou plusieurs étapes. On peut par exemple ouvrir un fichier, puis on peut lui associer un buffer et un flux (trois étapes). On peut aussi (c'est peut être plus pratique) déclarer un flux en le reliant directement à un fichier et à un buffer (une étape).

Comment manipuler le fichier ?
Pour cela il n'y a pas de mystère, il faut utiliser les fonctions membres des classes de la hiérarchie. Mais il ne faut pas oublier les notions d'héritage et de surcharge. Ainsi un objet pourra bénéficier des fonctions membres des classes supérieures. Prenons le cas de la classe Fstream, elle ne contient que deux fonctions membres accessibles: open et rdbuf. Avec &231#;a on ne va pas très loin. Mais heureusement nous pouvons utiliser les fonctions membres de la classe Istream (dont dérive Fstream via Iostream).

Exemple 1:

Ouverture d'un fichier.
Association d'un buffer au fichier précédemment ouvert.
Lecture puis affichage du fichier.

#include < iostream.h>

#include < stdiostr.h>

#include < fstream.h>

#include < fcntl.h>

#include < io.h>

int main (void)
{
        int fd; // numéro d'identification du fichier
        char tmp[100];

        // Ouverture d'un fichier sur le disque (utilisation de open)
        if ((fd = open ("fichier.cpp", O_RDONLY)) == -1)
        {
                perror("Error:");
                return 1;
        }

        // affichage du numéro d'identification du fichier
        cout < < "fd = " < < fd < < "\n";

        // association d'un buffer au fichier
        filebuf buffer (fd);

        // affichage du numéro d'identification du fichier associé au buffer
        // noter l'utilisation de la fonction membre fd (classe Filebuf).
        cout < < "identificateur de buffer = " < < buffer.fd() < < "\n\n";

        // lecture de 20 caractères à partir du fichier. Les caractères lus sont placés dans la
        // chaîne
        // tmp
        // Noter que cette opération ne provoque pas d'accès disque (tout se passe en mémoire
        // vive).
        // Noter l'utilisation de sgetn. Cette fonction n'est un membre de Filebuf (comme
        // buffer).
        // sgetn est membre de Streambuf, classe dont dérive Filebuf.
        buffer.sgetn (tmp, 20);
        tmp[20] = '\0';
        cout < < tmp;
        buffer.sgetn (tmp, 20);
        tmp[20] = '\0';
        cout < < tmp;

        return 0;
}
Exemple 2:

Ouverture d'un flux sur fichier en lecture seule avec assignation simultanée d'un buffer.
Ouverture d'un flux sur fichier en écriture seule avec assignation simultanée d'un buffer.
Lecture du fichier source puis écriture des données lues dans le fichier cible (avec des commentaires).

#include < iostream.h>

#include < stdiostr.h>

#include < fstream.h>

void main (void)
{
        char tampon[100];

        // ouverture d'un flux sur fichier en lecture seule avec assignation simultanée d'un
        // buffer.
        // Noter l'utilisation du constructeur ifstream ainsi que l'appel à ios::in.
        ifstream Entree ("fichier.cpp", ios::in);

        // ouverture d'un flux sur fichier en écriture seule avec assignation simultanée d'un
        // buffer.
        // Noter l'utilisation du constructeur ofstream.
        ofstream Sortie ("fichier.cpy", ios::out);

        // lecture de 30 caractères dans le fichier source.
        // Noter l'emploi de la fonction get qui n'est pas un membre de ifstream.
        // get est membre de istream, classe dont dérive ifstream.
        Entree.get (tampon, 30, EOF);
        cout < < "première lecture \n" < < tampon;

        // Ecriture des données précédemment lues dans le fichier source (avec ajout de
        // commentaires).
        // noter l'emploie de l'opérateur < < surchargé.
        Sortie < < "première lecture \n" < < tampon;

        Entree.get (tampon, 30, EOF);
        cout < < "deuxième lecture \n" < < tampon;
        Sortie < < "deuxième lecture \n" < < tampon;

        // La syntaxe directe Sortie < < Entree.rdbuf() est correct
}
Exemple 3:

Ouverture d'un flux sur fichier en écriture seule avec utilisation d'un buffer.
Ecriture de données dans le fichier précédemment ouvert.
Fermeture du fichier.
Ouverture d'un flux sur le même fichier en lecture / écriture avec utilisation d'un buffer.
Lecture des données du fichier puis réécriture après modification.

#include < iostream.h>

#include < stdiostr.h>

#include < fstream.h>

#include < fcntl.h>

#include < io.h>

int main (void)
{
        char c;
        int i;

           // Ouverture d'un sur fichier en écriture seule avec utilisation d'un buffer.
        ofstream Ecriture ("fichier.cpp", ios::out);

         // Ecriture des données.
        c=65;
        for (i=0; i<25000; i++)
        {
                Ecriture < < c;
                if (c>90) c = 65;
                c++;
        }

        // Fermeture du flux (ainsi que du fichier associé et du buffer).
        Ecriture.close();

        // Ouverture d'un flux sur fichier en lecture / écriture avec utilisation d'un buffer.
        fstream Flux ("fichier.cpp", ios::in | ios::out);

        // Lecture, modification puis réécriture des données dans le fichier
        c=(char)Flux.get();
        while (c != EOF)
          {
                // repositionnement de la tête de lecture / écriture.
                // Noter l'utilisation de seekg, membre de istream.
         Flux.seekg (-1L, ios::cur);

                // Réécriture
         Flux < < (char)(c + 1);

                // lecture du caractère suivant
         c=Flux.get();
          }

          return 0;
}

V.2.2. Manipulation de flux chaîne

retour sommaire
Qu'appelle t'on exactement flux chaîne ?
Un flux chaîne est un flux qui n'est associé à aucun fichier sur disque (ou tout autre support). Toutes les opérations ont lieux en mémoire vive.

A quoi sert un flux chaîne ?
Les flux chaînes sont utilisés pour manipuler des informations qui n'ont aucune raison d'être liées à un fichier disque. C'est le cas par exemple des informations entrées par l'utilisateur au clavier. Mais rien n'empêche de les envoyer vers un fichier par l'intermédiaire d'un flux sur fichier.

Exemple

#include < iostream.h>

#include < stdiostr.h>

#include < fstream.h>

#include < fcntl.h>

#include < io.h>

#include < strstrea.h>

void main (void)
{
        char StringPut[100];
        char StringGet[100];
        char Mot[10];

          // Ouverture d'un flux fichier en écriture seule avec utilisation d'un buffer
        ofstream Fichier ("Fichier.cpp", ios::out);

        // Ouverture d'un flux chaîne en écriture avec tampon (StringPut) de 100 caractères.
        ostrstream FluxPut (StringPut, 100);

        // Ecriture dans le flux chaîne, noter l'utilisation de l'opérateur < < surchargé.
        FluxPut < < "Ecriture dans Fichier via un ostrstream \n\n";

             // Injection du flux chaîne dans le flux fichier.
        Fichier < < FluxPut.rdbuf();

        // Utilisation du flux d'entrée standard cin (c'est un flux chaîne)
        coût < < "Entrer une chaîne SVP -> ";
           cin.getline (StringGet, 100);

            // Ouverture d'un flux chaîne en lecture seule avec tampon (StringGet) de 100
        // caractères.
        // Noter que la lecture se fait à partir du tampon. Ce dernier contient les données qui
        // ont été précédemment entrées via le clavier.
        istrstream FluxGet (StringGet, 100);

            // Lecture de 9 caractères à placer dans la variable Mot.
        // Noter l'utilisation de getline, membre de istream. La lecture s'arrête dès la rencontre
        // du caractère '\n' qui annonce un saut de ligne
        FluxGet.getline(Mot, 9);
        Mot[9] = '\0';

            // Injection de Mot dans le fichier
        Fichier < < Mot;

        FluxGet.getline(Mot, 9);
        Mot[9] = '\0';
        Fichier < < Mot;
}