|
|
Introductionretour sommaireLa 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:
I. Une amélioration du Cretour sommaireCertaines 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éfautretour sommaireEn 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: 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:
I.2. Les référencesretour sommairePour ê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: Dans un appel de fonction la référence peut être considérée comme un passage par adresse.
I.3. Les fonctions inlineretour sommaireConsidé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 deleteretour sommaireLe 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:
Pointeur = new Type [ (Valeur) ]
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'encapsulationretour sommaireEn 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:
Ils existent trois types de classe:
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:
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 |
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 |
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)
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
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();
}
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:
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();
}
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();
}
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.
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 niveau 3 contient sept classes parmi lesquelles on peut distinguer quatre groupes:
Le niveau 4 contient trois classes. Il est possible de distinguer deux groupes.
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.
Nous expliciterons plutôt le rôle et le fonctionnement de certaines classes en donnant des exemples simples et largement commentés.
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:
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;
}
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;
}
|