Vous êtes sur la page 1sur 424

Département de génie électrique et de génie informatique

La programmation
en

C++

avec
Unix
7è Edition

Notes de cours

IFT-19965
Denis Laurendeau
& Denis Dion Jr.

Faculté des sciences et de génie Université Laval


++ C
1. Photo de la page couverture:
La figure de la page couverture a été empruntée à SLScan et
représente un magnifique spécimen de chat persan roux. Le
persan est un chat à poil long, de caractère doux et
affectueux, dont les caractéristiques génétiques ont été
développées avec les ans. Il se reconnaît principalement à
son museau aplati et sa fourrure soyeuse. C’est un
compagnon d’appartement idéal.
2. Remerciements:
Nous tenons à remercier les personnes suivantes pour leur
contribution à la rédaction de ces notes:
• Le professeur Marc Parizeau pour ses conseils sur le contenu
du cours.
• Les assistants du cours Algorithmes de l’ingénieur I qui est
l’ancêtre du présent cours: Antoine Escobar, Marielle
Mokhtari, Langis Pitre, Denis Dion et Nicolas Pelletier. Leur
aide a été précieuse pour identifier les points importants sur
lesquels il faut insister pour bien faire passer la matière très
dense qu’est le langage C++. Leur apport à la mise sur pied
des expériences de travaux pratiques a été inestimable.
• Merci également à Denis Dion et Nicolas Pelletier pour avoir
mis sur pied la page html du cours et à Antoine Escobar et
Jacques Tremblay pour leur effort de développement d’outils
WWW pour la conception d’exercices.
• Le collègue Dominic Grenier pour sa lecture attentive de la
6è édition.
• Les étudiants de première année en génie électrique et en
génie informatique pour leur participation active au cours et
pour leur suggestions sur la façon de l’améliorer.
• Mon chat Mini Big Puce, un persan écaille de tortue, qui m’a
soutenu lors de la rédaction de ces notes en venant sans
cesse marcher sur le clavier et fouiner dans mes documents,
y laissant au passage poils et saletés. Minibig est
maintenant au paradis des chats mais demeure toujours
dans mes pensées. Heureusement, Ms Marple (un gouttière
blanc) et Cléopâtre (un hymaléen bluepoint) sont venues la
remplacer.

2 Programmation en C++ IFT-19965


C
++

Table des matières

CHAPITRE 1 Introduction..............................................................................................................5
CHAPITRE 2 La compilation et l’exécution d’un programme simple...........................................9
CHAPITRE 3 La déclaration des variables en C++......................................................................17
CHAPITRE 4 Les expressions arithmétiques ...............................................................................23
CHAPITRE 5 La lecture des informations au clavier ...................................................................31
CHAPITRE 6 Les fonctions en C++.............................................................................................35
CHAPITRE 7 De l’intérêt d’utiliser les fonctions en C++ ...........................................................45
CHAPITRE 8 Les variables locales et les variables globales .......................................................47
CHAPITRE 9 La création des classes et des objets en C++ .........................................................55
CHAPITRE 10 La définition de fonctions membres ......................................................................65
CHAPITRE 11 Les constructeurs ...................................................................................................73
CHAPITRE 12 Les fonctions membres de lecture et d’écriture .....................................................79
CHAPITRE 13 Comment tirer profit de l’abstraction des données ................................................89
CHAPITRE 14 La protection contre les accès accidentels aux membres des classes ....................91
CHAPITRE 15 Les instructions de préprocesseur ..........................................................................99
CHAPITRE 16 La notion d’héritage en C++................................................................................107
CHAPITRE 17 La conception de hiérarchies de classes ..............................................................123
CHAPITRE 18 Les tests utilisant les prédicats numériques .........................................................127
CHAPITRE 19 Les énoncés conditionnels ...................................................................................131
CHAPITRE 20 La combinaison logique d’expressions booléennes.............................................139
CHAPITRE 21 Les itérations en C++...........................................................................................147
CHAPITRE 22 Le traitement des données contenues dans des fichiers .......................................155
CHAPITRE 23 Les tableaux de nombres......................................................................................161
CHAPITRE 24 Les tableaux d’objets ...........................................................................................165
CHAPITRE 25 La créations de flots de données d’entrée et de sortie .........................................169
CHAPITRE 26 La création d’objets au “run-time” ......................................................................175
CHAPITRE 27 Le stockage des pointeurs aux objets d’une classe. .............................................183
CHAPITRE 28 Introduction aux fonctions virtuelles (virtual) .....................................................189
CHAPITRE 29 Les énoncés conditionnels multiples ...................................................................211
CHAPITRE 30 Les énumérations en C++ ....................................................................................215
CHAPITRE 31 Appel de constructeurs à partir d’autres constructeurs ........................................219
CHAPITRE 32 Fonctions membres appelant d’autres fonctions membres ..................................225
CHAPITRE 33 Les variables privées (private) et protégées (protected) ......................................229
CHAPITRE 34 Les dérivations de classes protected et private ....................................................239
CHAPITRE 35 Les fonctions qui retournent des chaînes de caractères .......................................243
CHAPITRE 36 Le passage des paramètres par référence .............................................................255
CHAPITRE 37 La surdéfinition de l’opérateur d’insertion ..........................................................269
CHAPITRE 38 La surdéfinition des opérateurs: notions fondamentales......................................277

Programmation en C++ IFT-19965 3


++ C
CHAPITRE 39 La surdéfinition des opérateurs unaires ...............................................................283
CHAPITRE 40 La surdéfinition des opérateurs binaires ..............................................................289
CHAPITRE 41 La conception d’un programme à partir de plusieurs fichiers .............................297
CHAPITRE 42 L’utilitaire make: notions élémentaires................................................................315
CHAPITRE 43 L’utilitaire make: notions avancées......................................................................321
CHAPITRE 44 La lecture et l’écriture de chaînes de caractères dans un fichier..........................335
CHAPITRE 45 Les tests sur des chaînes de caractères.................................................................339
CHAPITRE 46 Le stockage des chaînes de caractères dans les objets.........................................343
CHAPITRE 47 La récupération de la mémoire (désallocation) grâce à l’opérateur delete et aux
fonctions membres appelées destructeurs ...........................................................351
CHAPITRE 48 Le constructeur par recopie (copy constructor) ...................................................369
CHAPITRE 49 Les classes et fonctions amies (friend) ................................................................393
CHAPITRE 50 La réutilisation des fonctions: la notion de fonction générique (template) .........399
CHAPITRE 51 La réutilisation des classes: la notion de patrons de classe (template) ................405
Bibliographie .......................................................................................................415

4 Programmation en C++ IFT-19965


C
++

CHAPITRE 1 Introduction

1 Le but de ces notes de cours est de vous faciliter l’apprentissage


des propriétés essentielles de la programmation objet par le
biais du langage de programmation C++.

2 Le langage de programmation C++ est le résultat d’une


évolution du langage de programmation C, l’un des langages les
plus populaires dans le monde de l’informatique. Malgré qu’il
est un descendant du langage C, le langage C++ est
significativement différent parce qu’il s’articule autour des
notions de programmation objet.

3 En programmation, un objet est un groupe d’informations


stocké dans une partie de la mémoire de l’ordinateur. A chaque
objet est associé un type de données, ce type définissant
l’utilisation qui sera faite des données. Tous les langages de
programmation possèdent des types de données internes1 ou
(built-in) tels les entiers ou les réels.

1. aussi appelées données intrinsèques

Programmation en C++ IFT-19965 5


C
++

4 Un langage de programmation objet incite l’ingénieur à


concevoir des programmes centrés sur les types de données et
sur les hiérarchies de types de données qu’il conçoit lui-même.
Par exemple, un langage de programmation objet permet de
définir des objets informatiques qui correspondent exactement
aux objets de la vie courante qu’on désire utiliser: cheval, auto,
aéroport, banque, etc.
Par ailleurs, un langage procédural incite plutôt l’ingénieur à
concevoir des programmes en fonction de la façon dont les
données seront traitées par les fonctions ou des procédures. De
tels langages sont par exemple le C, le FORTRAN ou le Pascal.

5 Le langage C++ devient de plus en plus populaire pour les


raisons suivantes:
• C++ est relativement facile à apprendre
• les programmes en C++ s’exécutent rapidement
• les programmes en C++ sont concis
• le C++ est disponible sur plusieurs plate-formes matérielles

6 Il y a deux principales raisons qui motivent l’apprentissage du


C++:
• la productivité d’un concepteur de logiciel est plus grande en
C++ qu’en C.
• plusieurs modules de logiciel sont déjà disponibles en C++ et
peuvent être utilisés simplement, réduisant ainsi le temps
de développement d’une application.

7 Dans ces notes de cours, on insiste principalement sur deux


principes fondamentaux en programmation: l’abstraction des
données1 et l’abstraction des procédures2.

8 On insiste également sur les principes suivants:


• la représentation explicite

1. “data abstraction” en anglais


2. “procedure abstraction” en anglais

6 Programmation en C++ IFT-19965


C
++

• la non-duplication
• la visibilité locale
• la simplicité
• les connaissances nécessaires
• la réduction des calculs inutiles

9 Dans les notes de cours, les idées principales sont énoncées


dans les paragraphes numérotés. Les concepts avancés sont
identifiés par un titre approprié. On rencontre aussi des
paragraphes d’exercices et des paragraphes de résumé qui
rassemblent les concepts importants vus dans un chapitre.

Résumé
10 ✔Le langage C++ repose sur la programmation objet qui insiste
sur les notions de types de données et de hiérarchie entre les
types de données.
✔C++ résulte de l’évolution du langage C.

Programmation en C++ IFT-19965 7


C
++

8 Programmation en C++ IFT-19965


C
++

CHAPITRE 2 La compilation et
l’exécution d’un
programme simple

11 Dans ce chapitre, les concepts de compilation et d’exécution


d’un programme simple en langage C++ sont présentés.

12 En C++, comme pour plusieurs autres langages, il faut d’abord


écrire un programme source avec un éditeur (comme par
exemple l’éditeur emacs dont un guide vous est suggéré dans le
cours).

13 Un programme source contient le texte décrivant les


instructions du langage dans lequel le programme est écrit.
L’éditeur permet d’entrer ce texte à l’ordinateur et de le
sauvegarder dans un fichier source sur le disque dur de
l’ordinateur, ce qui permet d’en conserver le contenu à long
terme et de le réutiliser ultérieurement.

14 Une fois le programme source (aussi appelé le code source)


entré, il faut ensuite le compiler avec un programme appelé
compilateur, pour produire ce qu’on appelle le code objet1 qui
est stocké dans un fichier objet sur le disque dur de l’ordinateur.
Ce code objet a une forme incompréhensible pour l’utilisateur
mais qui est pleine de sens pour l’ordinateur.

Programmation en C++ IFT-19965 9


++ C
Le code source peut être distribué dans plusieurs fichiers
sources différents qui peuvent être compilés séparément pour
mener à plusieurs fichiers de code objet. Pour joindre ces
fichiers de code objet en un programme fonctionnel, il suffit de
les lier ensemble à l’aide d’un programme appelé éditeur de
liens. Ce programme produit un dernier fichier, aussi stocké sur
le disque dur de l’ordinateur, contenant le code exécutable.
Finalement, on exécute le programme en appelant le fichier de
code exécutable.

15 En général, il est rare qu’un programme complexe fonctionne


dès le premier essai. Le cycle de développement d’un
programme prend plutôt la forme montrée dans le diagramme
de la FIGURE 1.
FIGURE 1 Etapes de la conception d’un programme

Edition du code source


Correction des
erreurs appa-
raissant lors
de la compilation
Compilation du code source

Edition de liens du code objet

Correction des
erreurs appa-
Exécution du code exécutable raissant lors de
l’exécution

16 Un programme type en C++ est généralement composé de:

1. Notez qu’il n’y a aucun lien entre le code objet et les objets dont il est question en programma-
tion objet. Pour des raisons historiques, l’appellation code objet est apparue bien avant la notion
de programmation objet et la nomenclature n’a pas été mise à jour.

10 Programmation en C++ IFT-19965


C
++

• définitions de fonctions contenant des déclarations de


variables informant le compilateur sur les différentes
variables qui seront utilisées
• d’énoncés informant le compilateur des tâches qui seront
effectuées par le programme.
Tout programme en C++ doit contenir la définition d’une
fonction appelée main. Lorsqu’un programme en C++ est
exécuté, les instructions contenues dans la fonction main sont
exécutées.

Notion avancée
17 En mathématiques, une fonction sert à relier des variables
d’entrée à des variables de sortie. En programmation en C++
cependant, le terme fonction est utilisé avec un sens différent
puisqu’une fonction peut avoir des effets secondaires1 cachés
autres que le simple lien entre les variables d’entrée et les
variables de sortie. Le terme procédure serait plus approprié
mais celui de fonction est tellement passé à l’usage depuis
longtemps que nous l’adopterons dans la suite des notes.

18 Le programme ci-dessous, qui calcule la puissance dissipée


dans une résistance de 10 ohms traversée par un courant de 20
ampères2, ne contient qu’une seule fonction, la fonction main:
main () {
10 * 20 * 20;
}
Cette fonction est formée du nom de la fonction (ici main), suivi
de deux parenthèses, dont nous traiterons plus tard, suivies
d’un énoncé unique (la multiplication de trois nombres) inséré
entre deux crochets. L’opérateur de multiplication en C++ est
noté *. Notez que l’énoncé se termine, comme la plupart des
énoncés en C++, par un point-virgule “;”. Les parenthèses,
crochets et “;” agissent comme des symboles de ponctuation
dans le programme. Le compilateur comprend ces symboles, ce
qui lui permet de transformer correctement le code source en
code objet.

1. “side effects” en anglais.


2. l’équation est simplement P = R I2

Programmation en C++ IFT-19965 11


C
++

Le petit programme ci-dessus est plutôt hermétique car il


n’accepte aucune donnée d’entrée et ne produit aucune donnée
de sortie dont l’utilisateur peut prendre connaissance. Si le
programme est exécuté, il se termine sans se manifester...Il est
donc peu utile.

19 Pour rendre le programme moins hermétique, il suffit d’ajouter


des énoncés informant le compilateur que l’utilisateur désire
que les résultats du calcul de la puissance dissipée s’affichent à
l’écran de l’ordinateur comme le montre le code source suivant:
#include <iostream.h>
main () {
cout << “La puissance dissipée est: “;
cout << 10 * 20 * 20;
cout << endl;
}
Ce nouveau programme introduit plusieurs nouveaux concepts
et symboles de ponctuation qui méritent qu’on s’y intéresse en
détail.

20 L’opérateur de sortie, << , aussi appelé opérateur


d’insertion, apparaît toujours entre une expression
produisant des données, située à sa droite, et, à la gauche, un
nom qui spécifie l’endroit où ces données doivent être placées.
Par exemple, lorsqu’on désire que les données apparaissent à
l’écran de l’ordinateur, le nom à utiliser à la gauche de << est
cout.
Dans le programme du paragraphe 19, on compte trois
instructions de sortie à l’écran, chacune faisant appel à
l’opérateur de sortie <<. La première instruction demande à
l’opérateur de sortie << d’afficher une chaîne de caractères (ici:
La puissance dissipée est: ) à l’écran de l’ordinateur. En C++,
une chaîne de caractères est délimitée à chaque extrémité par
le symbole “.
La seconde instruction de sortie affiche le résultat d’une
opération arithmétique: cout << 10 * 20 * 20;
La dernière instruction de sortie fait appel à l’acronyme endl
signifiant end line, ce qui informe le compilateur de terminer la
ligne courante et de démarrer une nouvelle ligne1.

12 Programmation en C++ IFT-19965


C
++

21 Pour employer l’opérateur de sortie, il faut informer le


compilateur que la bibliothèque de fonctions d’entrée-sortie du
C++ sera utilisée. On le fait en incluant l’énoncé #include
<iostream.h>.
Il y a beaucoup à dire sur la signification de cette ligne de code.
Pour le moment, nous la laisserons de côté en nous rappelant
simplement qu’elle doit être incluse dans tout programme où
l’opérateur de sortie << apparaît.

22 Il faut noter que le C++ fait fi des espaces blancs1. C++ traite
donc les espaces blancs, suite d’espaces blancs, tabulations et
retours de chariot comme s’il s’agissait d’un seul espace blanc.
Les différents programmes suivants sont équivalents:
#include <iostream.h>
main () {
cout << “La puissance dissipée est: “;
cout << 10 * 20 * 20;
cout << endl;
}

#include <iostream.h>
main () {
cout << “La puissance dissipée est: “;
cout << 10 * 20 * 20; cout << endl;
}

#include <iostream.h>
main () { cout << “La puissance dissipée est: “;
cout << 10 * 20 * 20;
cout << endl;
}

#include <iostream.h>
main () { cout << “La puissance dissipée est: “;
cout << 10 * 20 * 20; cout << endl; }
Il n’y a pas vraiment de standard pour l’écriture du code source.
Cependant, il y a tout avantage à le garder très aéré, ce qui
permet de bien identifier les différentes parties du programme.

1. Un peu comme lorsqu’on appuie sur la touche “return” d’une machine à dactylographier.
1. En anglais, on dit que C++ est “blank insensitive”

Programmation en C++ IFT-19965 13


C
++

23 Par ailleurs, C++ considère la différence entre les lettres


minuscules et les lettres majuscules1. Par exemple, si on écrit
Main ou MAIN au lieu de main, C++ ne peut comprendre la
différence et interpréter que l’on veut nommer une fonction par
le vocable main.

24 Jusqu’à maintenant, nous avons vu deux opérateurs en C++:


l’opérateur de multiplication (*) et l’opérateur de sortie (<<).
Ces opérateurs sont inclus par défaut dans le langage et
travaillent sur des opérandes.

25 En général, le programme qui contient le code source de


l’application est stocké dans un fichier. Il est important de
donner des noms de fichiers qui sont suffisamment explicites
pour permettre de les reconnaître facilement. Par exemple,
pour le programme du paragraphe 19, le nom puissance.C
serait un identificateur de fichier de code source adéquat. Les
règles changent selon les compilateurs mais, en ce qui nous
concerne, le fichiers de code source posséderont tous l’extension
“.C”.

26 Pour compiler le code source contenu dans un fichier source, il


suffit de suivre la démarche prescrite par le compilateur.
Malheureusement, la méthodologie n’est pas la même pour
tous les compilateurs. Dans le cadre de ces notes de cours, la
compilation du fichier source puissance.C s’effectue de la
manière suivante:
CC puissance.C -o puissance
Cette instruction contient plusieurs parties:
• CC signifie que l’on invoque le compilateur C++ du système
UNIX mis à votre disposition
• puissance.C signifie que l’on désire que le compilateur CC
traite le fichier source puissance.C
• -o puissance signifie que l’on désire que le fichier
contenant le code exécutable porte le nom puissance. En

1. En anglais, on dit que C++ est “case-sensitive”

14 Programmation en C++ IFT-19965


C
++

général, lorsque l’option “o” est omise, le programme


exécutable est stocké par défaut dans le fichier a.out

27 Sous le système d’exploitation UNIX, pour exécuter le code


contenu dans le programme exécutable puissance, il suffit
simplement de taper la commande suivante:
puissance
Le résultat suivant est alors affiché à l’écran de l’ordinateur:
La puissance dissipée est: 4000

Exercice
28 Ecrivez un programme calculant le volume de la terre en
mètres cubes

Résumé
29 ✔En C++, comme dans plusieurs autres langages de
programmation, on écrit d’abord du code source qu’on compile
pour obtenir le code objet. Ce code objet est traité par l’éditeur
de lien pour produire le code exécutable.
✔Les fonctions en C++ contiennent une série d’énoncés
permettant d’accomplir une tâche donnée.
✔Tous les programmes en C++ contiennent une fonction
appelée main. Lors de l’exécution d’un programme, on démarre
les opérations contenues dans cette fonction main.
✔Plusieurs instructions impliquent l’utilisation d’opérateurs
intégrés au C++ tels * et <<. Ces opérateurs travaillent sur des
opérandes.
✔Si l’opérateur << est utilisé, il faut en informer le compilateur
en incluant la ligne suivante au début du programme:
#include <iostream.h>

Programmation en C++ IFT-19965 15


C
++

16 Programmation en C++ IFT-19965


C
++

CHAPITRE 3 La déclaration des


variables en C++

30 Dans ce chapitre, la notion de déclaration de variables en C++


est introduite et la terminologie utilisée dans le reste des notes
est précisée.

31 En C++, un identificateur est un nom formé de lettres et de


chiffres, le premier symbole étant une lettre, avec le caractère
souligné “_”1 comptant pour une lettre.
Une variable est un identificateur qui sert de nom à une
région de la mémoire de l’ordinateur. Une variable identifie
donc cet espace de mémoire.
Le type de cette variable détermine les dimensions de l’espace
de mémoire qu’elle sert à identifier. Par exemple, sur plusieurs
modèles d’ordinateur, les nombres entiers sont de type int
(pour integer) et occupent 32 bits de mémoire.
L’espace mémoire identifié par la variable renferme le contenu
de cette variable. Lors de l’exécution d’un programme, le
contenu d’une variable peut changer mais le type de la variable
ne change jamais.

1. “underscore” en anglais.

Programmation en C++ IFT-19965 17


C
++

32 Comme chaque variable possède un type, le compilateur peut


réserver le bon espace de mémoire pour chacune.

33 Lorsque l’on indique le type d’une variable au compilateur


dans la fonction main, on procède alors à la déclaration de la
variable. Par exemple, deux variables de type entier (int) sont
déclarées dans le programme suivant:
main () {
int resistance;
int courant;
}
Les variables sont considérées comme des entiers parce que
leur identificateur est précédé du mot réservé int.

34 On peut regrouper plusieurs déclarations de variables du


même type en modifiant la syntaxe de la sorte:
main () {
int resistance, courant;
}
le séparateur virgule “,” sert à séparer les identificateurs des
différentes variables.

35 L’opération de stocker une valeur dans la mémoire réservée à


une variable est appelée “affectation”.

36 On peut initialiser une variable en procédant comme suit:


main () {
int resistance = 1000;
int courant = 20;
}
ou encore
main () {
int resistance = 1000, courant = 20;
}

37 Pour changer la valeur d’une variable, on utilise l’opérateur


d’affectation “=”. Par exemple:
#include <iostream.h>
main () {

18 Programmation en C++ IFT-19965


C
++

int puissance,resistance = 1000, courant = 20;


puissance = resistance * courant;
puissance = puissance * courant;
cout << “La puissance dissipee dans la resistance est:”;
cout << puissance;
cout << endl;
}

Résultat
La puissance dissipée est: 400000

Evidemment, ce programme est simplet car il divise le calcul de


la puissance en deux étapes (pour la mise au carré de la valeur
du courant ).

38 Remarquez que le nom d’une variable peut apparaître à la fois


à la droite et à la gauche de l’opérateur d’affectation. La valeur
actuelle de la variable lors de l’exécution de l’énoncé est utilisée
pour la partie de droite et la valeur de gauche est affectée
lorsque l’énoncé est complet.

39 Le C++ offre plusieurs possibilités pour stocker des valeurs de


type entier: char, short, int, long, long long. Il suffit de
se rappeler que le nombres d’octets1d’un char est < short <
int < long <= long long. Le type char est normalement
utilisé pour stocker des caractères alpha-numériques2.
Cependant, comme les caractères ont un code numérique3 qui
leur est associé, le type char peut aussi être vu comme une
façon de stocker des entiers.

40 Le C++ offre aussi plusieurs possibilités pour représenter des


nombres réels: float, double, long double. Il suffit de se

1. “bytes” en anglais. Un byte est un groupe de 8 bits.


2. on initialise une variable de type char comme suit: char c = ‘d’;
3. i.e. chaque caractère est représenté par un nombre dans la mémoire de l’ordinateur

Programmation en C++ IFT-19965 19


++C
rappeler que le nombres d’octets d’un float est < double <
long double.

41 Le nombre d’octets réservés pour chaque type de données est


toujours le même pour un type donné sur un modèle
d’ordinateur. Cependant, ce nombre peut varier d’un
ordinateur à l’autre. Il suffit de consulter la documentation de
votre ordinateur pour connaître la dimensions de chaque type
de données en nombre d’octets.

Notion avancée
42 Pour les plus curieux, il est possible de connaître la quantité de
mémoire qui est réservée pour un type de données grâce à
l’opérateur sizeof du C++. Le code suivant montre
l’utilisation de cet opérateur:
//&% sizeof.C. Ceci est un commentaire qui contient le nom
//du fichier Unix dans lequel est stockée la source.
#include <iostream.h>
main () {
cout<< “Type de donnees Octets” << endl
<< “char“ << sizeof(char) << endl
<< “short“ << sizeof(short) << endl
<< “int“ << sizeof(int) << endl
<< “long“ << sizeof(long) << endl
<< “float“ << sizeof(float) << endl
<< “double“ << sizeof(double) << endl
<< “long double“ << sizeof(long double) << endl;
}

Résultat
Type de donnees Octets
char 1
short 2
int 4
long 4
float 4
double 8
long double 16

20 Programmation en C++ IFT-19965


C
++

Notion avancée
43 Parce que long et long double sont souvent trop grands pour
stocker les nombres usuels, plusieurs compilateurs ne
supportent pas ces types de données et les convertissent
souvent en int et double.

44 Lorsque le temps d’exécution et l’occupation de l’espace


mémoire sont des contraintes importantes, les types short et
float sont généralement les plus appropriés.

45 Dans tout programme bien structuré, il est de mise d’inclure


des commentaires brefs mais néanmoins clairs. Ces
commentaires permettent à un programmeur de se remémorer
(ou d’apprendre pour la première fois) rapidement le sens d’une
instruction ou d’une section de code. Le C++ offre deux
méthodes différentes pour l’inclusion de commentaires dans les
programmes. Premièrement, lorsque le compilateur rencontre
deux barres obliques1 consécutives // dans le code source, il
ignore les deux barres obliques et tout ce qui les suit sur la
ligne:
//premiere syntaxe pour un commentaire

46 Une deuxième forme de syntaxe pour les commentaires utilise


la suite de caractères /* pour débuter un commentaire et la
suite */ pour le terminer. Le commentaire peut donc occuper
plusieurs lignes puisque tout ce qui est inclus entre /* et */ (y
compris ces caractères) est ignoré par le compilateur:
/* commentaire long pouvant résider sur plusieurs
lignes*/
Il important de remarquer que les commentaires ne peuvent
pas être imbriqués comme dans le segment de code suivant:
/* debut d’un commentaire /* et deuxième commentaire
imbriqué*/ ca ne marche pas */

Résumé
47 ✔ Une variable est un identificateur qui correspond à un espace
de mémoire.

1. “slash” en anglais

Programmation en C++ IFT-19965 21


++ C
✔ Pour insérer une variable, il faut la déclarer et identifiant
son type de données.
✔ On peut initialiser une variable lors de sa déclaration.
✔ Les types de données pour les valeurs entières sont: char,
short, int et long.

22 Programmation en C++ IFT-19965


C
++

CHAPITRE 4 Les expressions


arithmétiques

48 Ce chapitre présente plusieurs opérateurs du C++ et discute de


la priorité entre ces opérateurs.

49 Les quatre opérateurs arithmétiques fondamentaux du C++


sont +, -, * et /, soit l’addition, la soustraction, la
multiplication et la division. Les lignes qui suivent donnent
quelques exemples d’énoncés utilisant ces opérateurs:
6 + 3; //addition de deux constantes. Res = 9
6 - 3; //soustraction de deux constantes. Res= 3
6 * 3; //Multiplication de deux constantes. Res = 18
6 / 3; //Division de deux constantes entieres. Res = 2
6 + y; //Addition d’une constante et d’une variable
x - 3; //Soustraction de 3 d’une variable
x * y; //Multiplication de deux variables
x / y; //Division de deux variables

50 Lorsqu’un dénominateur entier ne divise pas entièrement un


numérateur entier, l’opérateur de division du C++ tronque le
résultat entier plutôt que de l’arrondir:
5 / 3; //Resultat donne 1

Programmation en C++ IFT-19965 23


C
++

51 L’opérateur modulo du C++, représenté par le symbole %,


produit le reste de la division entière:
5 % 3; //Resultat 2

52 La division de nombres en format point flottant donne un


nombre du même format:
5.0 / 3.0; //Resultat 1.66667

53 Les opérations arithmétiques peuvent contenir aucun, un seul


ou plusieurs opérateurs:
6;
y;
6 + 3;
6 + 3 + 2;
6 - 3 - 2;
6 * 3 * 2;
6 / 3 / 2;

54 Le C++ suit la pratique commune quand vient le temps


d’évaluer la priorité des opérateurs entre eux. Par exemple, le
compilateur C++ donne priorité à l’opérateur de multiplication
sur l’opérateur d’addition:
6 + 3 * 2;
est équivalent à:
6 + (3 * 2);

55 Quand deux opérateurs ont le même niveau de priorité, le


compilateur C++ évalue les expressions comme dans l’exemple
suivant:
6 * 3 / 2; //traite comme (6 * 3) / 2
6 / 3 * 2; //traite comme (6 / 3) * 2
On dit que les opérateurs * et / obéissent à une règle
d’associativité de gauche à droite. Cependant, les opérateurs du
C++ n’obéissent pas tous à cette règle comme nous le verrons
plus loin.

56 La priorité par défaut du C++ peut être contournée par


l’utilisation de parenthèses:
6 + 3 * 2; //Resultat = 12

24 Programmation en C++ IFT-19965


C
++

(6 + 3) * 2; //Resultat = 18
Remarquez qu’il n’est pas interdit de renforcer la priorité par
défaut du C++ pour la rendre explicite et plus facilement
compréhensible:
6 + 3 * 2;
et
6 + (3 * 2);
sont identiques et produisent le même résultat. Cependant, la
deuxième expression rend explicite la priorité des opérateurs et
clarifie le code. L’utilisation des parenthèses est une bonne
habitude de programmation, surtout lorsque vous avez à
travailler avec des expressions compliquées.

57 La plupart des opérateurs du C++ sont binaires, c’est-à-dire


qu’ils requièrent deux opérandes qui sont placées
immédiatement à gauche et à droite de l’opérateur.
D’autres opérateurs, comme par exemple l’opérateur de
négation arithmétique - , sont dits unaires parce qu’ils
n’acceptent qu’une seule opérande placée immédiatement à
droite de l’opérateur. La priorité de l’opérateur unaire “-” est
supérieure à celle des opérateurs +, - (binaire), * et /:
- 6 * 3 / 2; //Resultat= -9, equivalent a ((-6)*3) / 2

58 Quand des expressions arithmétiques contiennent des


éléments appartenant à plusieurs types de données différents
(ex. int, float et short), le compilateur tente de convertir les
nombres de façon à ne pas perdre d’information. Par exemple,
dans l’expression 5.0 * 2, le compilateur convertit le nombre
2 en notation point-flottant float avant d’effectuer la
multiplication.

59 Les compilateurs utilisent parfois des messages


1
d’avertissement lorsqu’ils doivent effectuer des conversions
avec perte d’information comme par exemple lorsqu’une valeur

1. “warnings” en anglais

Programmation en C++ IFT-19965 25


C
++

en notation point-flottant est affectée à un variable de type


int.

60 L’usager peut forcer la conversion explicite d’une variable d’un


certain type à un autre type en utilisant l’opérateur de
“casting”. Pour effectuer un casting, il suffit de placer le type
d’arrivée entre parenthèses et de placer le tout devant la
variable à convertir:
x = (double)i; /*force la conversion de la variable i en
type double*/
Il faut ajouter que le type de la variable i demeure inchangé
lors du casting.

61 Généralement, il est préférable d’éviter le casting car le


compilateur est susceptible de faire des choix de conversion
plus judicieux que ceux de l’utilisateur.

62 L’opérateur d’affectation =, comme tous les opérateurs du C++,


produit une valeur qui est la valeur de l’assignation. Par
exemple:
y = 5;
produit la valeur 5 en plus d’affecter cette valeur à la variable
y. Cela signifie que l’on peut écrire des énoncés du style:
x = (y = 5);//affecte 5 a x et y

Notion avancée
63 Contrairement aux autres opérateurs du C++, l’opérateur =
procède à une associativité de droite à gauche, ce qui signifie
que:
x = y = 5; //equivalent a x = (y = 5)
autrement, cela signifierait que l’on aurait (x = y) = 5, ce
qui n’a aucun sens.

64 Plusieurs opérateurs de sortie peuvent se retrouver dans le


même énoncé comme dans le programme suivant:
#include <iostream.h>
main () {
cout << “La puissance dissipee est:”
<< 1000 * 20

26 Programmation en C++ IFT-19965


C
++

<< endl;
}

Résultat
La puissance dissipée est: 20000

parce que l’opérateur de sortie << applique l’associativité de


gauche à droite, ce qui signifie que l’énoncé de sortie ci-dessus
est équivalent à:
((cout<<“La puissance dissipee est”)<<1000 * 20 )<<endl;
Pour comprendre la signification de ce qui précède, il faut
approfondir la notion d’opérateur de sortie et surtout la
signification de cout.
Plus précisément, cout est une variable et, comme toute
variable, elle correspond à un espace mémoire. Cet espace
mémoire informe le C++ de l’endroit où l’utilisateur désire
envoyer les données.
Ensuite, il faut savoir que chaque expression de sortie
correspond au même espace de mémoire identifié par
l’opérande située à gauche de l’opérateur de sortie <<. Cet
espace de mémoire associé à cout contient non seulement
l’information concernant la destination des données mais aussi
de l’information sur l’état actuel du traitement de sortie.

65 Contrairement à la plupart des variables, la valeur de cout ne


peut être affichée à l’écran de l’ordinateur et n’a de sens que
pour l’opérateur de sortie <<.

66 L’opérateur de sortie << a une priorité inférieure à celle de tous


les opérateurs arithmétiques. Des parenthèses sont donc
inutiles pour garder ensemble les opérations arithmétiques:
cout << x + y * z; //equivalent a cout << (x + (y * z))

67 Par ailleurs, l’opérateur de sortie a une priorité supérieure à


celle de l’opérateur d’affectation, ce qui signifie que les
parenthèses sont obligatoires dans l’énoncé suivant:

Programmation en C++ IFT-19965 27


++C
cout << (resistance = 15);

Notion avancée
68 La raison pour laquelle l’opérateur = a une priorité inférieure à
celle de l’opérateur de sortie << vient du fait que << a une
autre signification pour laquelle la priorité a un sens. Plus
spécifiquement, quand l’expression à la gauche de << est une
donnée de type entier, << a alors le sens de l’opérateur de
décalage à gauche, hérité du C.Nous verrons plus loin le sens
de cet opérateur.

69 Jusqu’à maintenant nous avons vu les opérateurs suivants:


+, -, *, /, %, <<, =.
En général, un opérateur en C++ est un symbole ou une
combinaison de symboles qui sont traitées de façon spéciale par
le compilateur. Rappelez-vous qu’un opérateur est une fonction
qui produit un résultat. La plupart des opérateurs sont
spéciaux dans le sens qu’ils acceptent des arguments via des
opérandes qui apparaissent à gauche et à droite du symbole
identifiant l’opérateur plutôt que d’être énumérées dans une
liste comprise entre parenthèses comme nous le verrons plus
loin lorsque nous traiterons des fonctions en C++. Certains
opérateurs tels new et delete, n’évaluent pas leurs opérandes
et d’autres, tel l’opérateur conditionnel, évaluent certaines
opérandes et d’autres non.

70 La priorité et l’associativité des opérateurs vus jusqu’à


maintenant sont résumées dans le tableau suivant:

TABLE 1 Priorité et associativité des opérateurs

Opérateurs Associativité
- (unaire), + (unaire) droite à gauche
*,/,% gauche à droite
+,- gauche à droite
<< gauche à droite
= droite à gauche

28 Programmation en C++ IFT-19965


C
++

Résumé
71 ✔C++ offre des opérateurs de négation, d’addition, de
soustraction, de multiplication, de division, de sortie et
d’affectation et suit les règles courantes de priorité et
d’associativité.
✔Pour rendre les expressions arithmétiques plus claires,
l’usage des parenthèse est recommandé.
✔L’opérateur de sortie à une priorité inférieure à celle des
opérateurs arithmétiques. L’opérateur d’affectation a une
priorité encore plus basse que tous ces opérateurs.

Programmation en C++ IFT-19965 29


C
++

30 Programmation en C++ IFT-19965


C
++

CHAPITRE 5 La lecture des


informations au clavier

72 Cette section présente l’opérateur d’entrée >> qui permet


l’entrée d’informations dans un programme à partir du clavier
de l’ordinateur.

73 L’opérateur d’entrée, >>, aussi appelé opérateur


d’extraction, est le complément de l’opérateur de sortie.
Lorsqu’il est utilisé de pair avec cin, correspondant à l’endroit
où les données doivent être obtenues, cet opérateur reçoit une
donnée du clavier de l’ordinateur et place cette information
dans une variable. Par exemple, le programme suivant montre
comment on peut tirer profit de l’opérateur d’extraction pour
lire une valeur entière au clavier et pour la stocker dans une
variable du programme:
//&% extraction.C
#include <iostream.h>
main () {
int a,b,c;
cout << “SVP, entrer des entiers au clavier:” << endl;
cin >> a;
cin >> b;
cin >> c;
cout << “Le produit des nombres est: “ << a*b*c << endl;
}

Programmation en C++ IFT-19965 31


C
++

Résultat
SVP, entrer des entiers au clavier:
1 2 3
Le produit des nombres est: 6

Dans le résultat précédent, la première et la troisième lignes


sont affichées par le programme tandis que la deuxième ligne
est tapée au clavier par l’utilisateur.

74 On sépare les différentes valeurs à entrer au clavier par des


espaces, des tabulations ou des retours de chariot. Remarquez
que sous le système d’exploitation UNIX, les caractères tapés
au clavier s’accumulent dans un tampon de mémoire avant
d’être envoyés au programme. Ils ne sont envoyés que lors
qu’un retour de chariot est tapé. Dans le programme du
paragraphe 73, le code ne fait rien tant que les trois entiers ne
sont pas entrés au clavier et qu’un retour de chariot est entré à
la fin de la ligne (après le chiffre 3 dans l’exemple suivant le
programme).

75 Un énoncé d’entrée peut contenir plusieurs opérateurs


d’entrée:
cin >> a >> b >> c;/* Equivalent a l’enonce du programme
du paragraphe 73 */

Notion avancée
76 Nous verrons un peu plus loin comment lire des données
contenues dans un fichier de données autre que le clavier. En
fait, le clavier est en quelque sorte un fichier spécial...
En attendant de voir plus en profondeur les mécanismes de
manipulation des fichiers à partir d’un programme en C++, les
utilisateurs de DOS ou de UNIX peuvent avoir recours au
mécanisme de redirection qui permet de spécifier à un
programme que les données provenant du clavier proviendront
plutôt d’un fichier de données. Par exemple, si le fichier
“test.data” contient les nombres 1 2 3, le programme

32 Programmation en C++ IFT-19965


C
++

extraction.C du paragraphe 73 pourrait utiliser la


redirection de la façon suivante:
extraction < test.data

Exercice
77 Concevez un programme qui calcule le volume d’une boîte
rectangulaire. Les longueurs des côtés de la boîte doivent être
lus au clavier de l’ordinateur lors de l’exécution du programme.

Résumé
78 ✔ Si l’opérateur d’entrée >> est utilisé, il faut en informer le
compilateur en incluant la ligne suivante au début du
programme: #include <iostream.h>
✔ Si le programme doit utiliser des données fournies au clavier,
il suffit d’utiliser un énoncé incluant cin: cin >> variable;
✔ Si on désire rediriger l’entrée d’un programme du clavier vers
un fichier de données, il suffit d’utiliser la redirection: nom du
programme < nom du fichier

Programmation en C++ IFT-19965 33


C
++

34 Programmation en C++ IFT-19965


C
++

CHAPITRE 6 Les fonctions en C++

79 Ce chapitre présente un concept important en C++1 soit la


notion de fonction. Nous avons déjà abordé la fonction main
essentielle à tout programme en C++. Nous verrons
maintenant comment définir des fonctions de façon plus
générale en présentant les notions importantes d’argument, de
paramètre et de valeur de retour.

80 Nous avons vu au paragraphe 19 un programme très simple


pour le calcul de la puissance dissipée dans une résistance. Le
programme est reproduit ici pour plus de clarté:
#include <iostream.h>
main () {
cout << “La puissance dissipée est: “;
cout << 10 * 20 * 20;
cout << endl;
}
Evidemment, ce programme ne fonctionne que pour calculer la
puissance dissipée dans une résistance de 10 ohms traversée
par un courant de 20 ampères. Si l’on veut calculer la puissance
dissipée dans la même résistance pour divers courants ou la
puissance dissipée dans des résistances différentes parcourues

1. et aussi dans tous les langages de programmation structurée.

Programmation en C++ IFT-19965 35


C
++

par différents courants, il serait intéressant de concevoir une


fonction appelée puissance_dissipee. Le code du
programme pourrait alors ressembler à celui montré ci-
dessous:
#include <iostream.h>
//...Placer ici le code de la fonction puissance_dissipee
main () {
cout << “La puissance dissipée est: “;
cout << puissance_dissipee(10,20);
cout << endl;
}
Dans cet exemple, la fonction puissance_dissipee accepte
deux arguments (10 et 20), séparés respectivement par une
virgule “,”. La syntaxe du C++ exige que les arguments d’une
fonction soient séparés par une virgule.

81 Dans l’exemple qui précède, les arguments sont des valeurs


constantes (10 et 20). Cependant, les arguments d’une fonction
peuvent aussi être des expressions contenant des variables, par
exemple puissance_dissipée(resistance,courant) ou
des expressions contenant des opérateurs comme par exemple
puissance_dissipee(2*resistance, 3*courant).

82 Dans le code du paragraphe 80, les énoncés de la fonction


puissance_dissipee ont été omis pour montrer comment
utiliser la fonction. Nous voyons maintenant comment définir
la fonction puissance_dissipee. La définition prend la
forme suivante:
int puissance_dissipee(int r, int c) {
return r * c * c;
}

36 Programmation en C++ IFT-19965


C
++

La signification de chaque partie du code de la fonction


puissance dissipée est donnée ci-dessous:

Indique au C++ le type de la valeur retournée par la fonction


Indique au C++ le nom de la fonction

Indique au C++ les noms et types


des paramètres de la fonction
Indique le début du corps de la fonction

int puissance_dissipee(int r, int c) {

return r * c * c;

La valeur de l’expression retournée

Indique qu’une valeur est retournée par la fonction


Marque la fin du corps de la fonction

83 A chaque fois que le nom de la fonction puissance_dissipee


apparaît dans le programme, le compilateur C++ doit effectuer
les tâches suivantes:
• réserver l’espace mémoire nécessaire pour stocker le contenu
des expressions reçues en argument
• écrire les valeurs de ces expressions dans l’espace mémoire
réservé
• identifier les espaces mémoire réservés avec des noms de
paramètres, disons r et c pour la résistance et le courant
• évaluer l’expression r * c * c qui est la puissance dissipée
• retourner la valeur de la puissance r * c * c pour l’utiliser
dans d’autres calculs.

84 Il faut noter ici un fait important: les paramètres de la


fonction ne sont que des variables qui sont initialisées à la
valeur des arguments à chaque fois que la fonction est appelée.

Programmation en C++ IFT-19965 37


C
++

85 Lors qu’une fonction est définie en C++, il est important de:


1. déclarer le type de chaque paramètre à l’endroit où le para-
mètre intervient
2. déclarer le type de la valeur de retour de la fonction dans
chaque programme en C++ à l’endroit où le nom de la fonc-
tion est introduit.

86 Maintenant que nous en savons plus sur la définition de


fonctions, le programme complet de calcul de la puissance
dissipée utilisant la fonction puissance_dissipee est donné
ci-dessous pour deux cas de résistances et courants (10,20 et
100, 50):
//&% fonction_simple.C
#include <iostream.h>

int puissance_dissipee (int r, int c){


return r * c * c;
}

main () {

cout << “La puissance dissipee est:”;


cout << puissance_dissipee(10,20);
cout << endl;
cout << “La puissance dissipee est:”;
cout << puissance_dissipee(100,50);
cout << endl;
}

Résultat
La puissance dissipee est:4000
La puissance dissipee est:250000

87 Il faut remarquer qu’il est absolument nécessaire de déclarer le


type de chaque paramètre individuellement car, contrairement

38 Programmation en C++ IFT-19965


C
++

à la déclaration des variables, les types ne se propagent pas au


travers des séparateurs (“,”). Le code suivant est donc erroné:
Type de c non déclaré

int puissance_dissipee (int r, c){


return r * c * c;
}

88 Le compilateur C++ exige normalement que le nom de chaque


fonction apparaisse avant qu’elle ne soit appelée par le
programme. Cela signifie que la fonction
puissance_dissipee doit être définie avant main puisque
main utilise puissance_dissipee. Nous verrons plus loin le
concept de prototype de fonction qui permet plus de flexibilité
dans la définition des fonctions et dans leur utilisation.

Notion avancée
89 Quand la valeur de retour d’une fonction est de type entier int,
il n’est pas nécessaire d’écrire ce détail dans la déclaration de la
fonction. Par exemple, la déclaration de la fonction
puissance_dissipee du paragraphe 86 pourrait s’écrire:
puissance_dissipee (int r, int c);
parce que lorsque le type de la valeur de retour est omis, le
compilateur assume que le type est int. Cependant, les bons
programmeurs incluent toujours le type de la valeur de retour
par souci de clarté.

90 Il peut arriver qu’une fonction ne retourne aucune valeur. Cela


se produit lorsqu’on conçoit une fonction pour afficher le
contenu de variables mais sans en faire nécessairement usage.
Dans ce cas, on indique au compilateur que la fonction ne
retourne aucune valeur en utilisant le mot réservé void. Par
exemple, si, dans le programme du paragraphe 86 on désire
écrire une fonction qui affiche le contenu des variables
contenant les valeurs de résistance et de courant, on pourrait
concevoir le code suivant:
void affiche_valeurs(int r, int c) {
cout << “Resistance: “ << r << endl;
cout << “Courant: “ << c << endl;
}

Programmation en C++ IFT-19965 39


C
++

Il est conseillé d’utiliser le vocable void comme valeur de


retour de la fonction principale main. Nous ne l’avions pas fait
jusqu’à maintenant puisque le concept de void n’avait pas
encore été discuté.

91 Il est important de noter qu’une fonction peut aussi appeler


une autre fonction. Le programme suivant donne un exemple
de ce concept:
//&% appel_appel.C

#include <iostream.h>
int fonction_1() {
return 1;
}

void fonction_appel(){
cout << “appel de la fonction “ << fonction_1();
cout << endl;
}

void main () {

fonction_appel();

Résultat
appel de la fonction 1

92 Lorsque la fonction puissance_dissipee est appelée avec


deux variables entières comme arguments, une copie du
premier argument est placée dans le premier paramètre r et
une copie du deuxième argument est placée dans le second
paramètre c. C’est le compilateur qui se charge d’inclure le
code qui copie les arguments dans les paramètres et le tout
demeure transparent à l’usager. Ce type de passage de
paramètres s’appelle “passage par valeur” puisque c’est la
valeur de la variable qui est utilisée et non l’espace mémoire
réservé pour la variable. Plus tard, nous verrons un autre mode

40 Programmation en C++ IFT-19965


C
++

de transmission des arguments appelé “passage par référence”


qui diffère significativement du mode dont nous venons juste
de discuter. Il serait cependant prématuré de discuter
maintenant de ces concepts plus avancés.

93 Il faut se rappeler que comme C++ tient compte des


majuscules, les fonctions puissance_dissipee et
PUISSANCE_DISSIPEE seraient différentes si elles
appartenaient au même programme. Il est peu judicieux de ne
distinguer les variables ou fonctions par les seules majuscules/
minuscules.

94 Le C++ offre aux programmeurs un outil intéressant appelé


“surdéfinition de fonctions” qui permet de définir des fonctions
ayant le même nom mais ayant des variantes dans le type ou
le nombre de paramètres ou dans le type de la valeur de retour.
A titre d’exemple, il serait potentiellement intéressant d’avoir
deux fonctions puissance_dissipee: une qui reçoit des
arguments de type int et qui retourne une valeur de type int
et l’autre qui reçoit des arguments de type double et qui
retourne une valeur de type double. Le C++ permet de telles
surdéfinitions comme le montre le code suivant:
//&% fonction_overld.C

#include <iostream.h>
int puissance_dissipee (int r, int c){
cout << “Version (int): “;
return r * c * c;
}

double puissance_dissipee (double r,double c){


cout << “Version (double): “;
return r * c * c;
}

void main () {

int res_int = 10, cour_int = 20;


double res_double = 100.35, cour_double = 5.23;

cout << “La puissance dissipee est ”;


cout << puissance_dissipee(res_int,cour_int);
cout << endl;
cout << “La puissance dissipee est ”;

Programmation en C++ IFT-19965 41


C
++

cout << puissance_dissipee(res_double,cour_double);


cout << endl;
}

Résultat
La puissance dissipee est:Version (int): 4000
La puissance dissipee est Version (double): 2744.86

On remarque que dépendant du type d’argument, le


compilateur C++ peut décider de la fonction qu’il faut appeler.
Cette caractéristique rend le C++ très puissant parce que le
contexte d’un calcul peut être pris en compte par le choix de la
fonction appropriée, ce qui donne beaucoup de souplesse au
programmeur pour développer un programme efficace. La
surdéfinition de fonctions est un élément important du
polymorphisme au run-time, concept qui sera abordé au
CHAPITRE 28.

95 Modifiez le programme du paragraphe 94 pour qu’il calcule le


carré du courant dans la fonction puissance_dissipee avec
une fonction appelée carre. Utilisez la surdéfinition de
fonction pour accepter des arguments de type int ou de type
double.

Exercice
96 Concevez une fonction qui reçoit en argument la valeur de deux
résistances et qui retourne la valeur de la combinaison en série
de ces résistances. Le prototype de la fonction devrait être le
suivant:
double r_serie(double r1, double r2);
La valeur de la combinaison en série de deux résistances est
simplement la somme de la valeur de chaque résistance.

Résumé
97 ✔ Lorsqu’une fonction est appelée dans un programme, ses
arguments sont évalués et copiés dans les paramètres. Ensuite,
les instructions du corps de la fonction sont exécutés. Lorsque
la fonction retourne une valeur, l’argument de l’énoncé return
est évalué et cette valeur devient la valeur de la fonction à

42 Programmation en C++ IFT-19965


C
++

l’appel.
✔ La déclaration d’une fonction exige que le type de chaque
argument soit spécifié de même que le type de la valeur de
retour (qui peut être void si la fonction ne retourne aucune
valeur).
✔ Le C++ supporte la surdéfinition de fonctions qui consiste à
définir deux fonctions ayant le même nom mais qui diffèrent de
par leurs paramètres ou leur valeur de retour.

Programmation en C++ IFT-19965 43


C
++

44 Programmation en C++ IFT-19965


C
++

CHAPITRE 7 De l’intérêt d’utiliser les


fonctions en C++

98 Au CHAPITRE 6, nous avons vu comment définir des fonctions


en C++. Le présent chapitre présente les avantages reliés à
l’utilisation des fonctions.

99 Lorsqu’un détail de calcul est déplacé du programme principal


à une fonction, on cache ces détails derrière la barrière de la
fonction. Cela facilite grandement la réutilisation du code: au
lieu de copier les lignes du programme principal effectuant un
certain calcul, on réutilise simplement la fonction définie par
un autre programmeur.

100 L’utilisation de fonctions reporte les détails des calculs dans la


fonction, hors de la vue du programmeur qui ne veut qu’utiliser
la fonction pour faire un calcul sans nécessairement connaître
en détail comment cette fonction est implantée.

101 L’utilisation des fonctions permet de segmenter le code en


petits segments indépendants faciles à déverminer1.

1. debugger quoi!

Programmation en C++ IFT-19965 45


C
++

102 Le fait de réutiliser une fonction à plusieurs endroits dans un


programme évite de recopier les instructions de cette fonction.
Le code de la fonction n’a qu’à être écrit à un seul endroit, le
reste du programme ne faisant qu’appeler la fonction
correspondant à ce code.

103 Si le programmeur décide de changer les détails d’implantation


du calcul effectué par une fonction sans changer le type de
résultat qu’elle produit, un programmeur utilisant la fonction
n’aura pas à changer son programme puisqu’il ne fait
qu’utiliser le nom de la fonction dans son programme.

46 Programmation en C++ IFT-19965


C
++

CHAPITRE 8 Les variables locales et


les variables globales

104 La durée de vie1 d’une variable est la période de temps


durant laquelle de l’espace mémoire est réservé pour cette
variable. La portée2 d’une variable est la portion d’un
programme pour laquelle elle peut être évaluée ou recevoir une
valeur par le processus d’affectation.
Dans ce chapitre, nous apprenons comment le C++ traite la
durée de vie et la portée des variables.

105 Il est important de mentionner que, lorsqu’une fonction est


appelée, la valeur des paramètres de celle-ci n’est disponible
qu’à l’intérieur de la fonction. Prenons comme exemple la
fonction puissance_dissipee:
int puissance_dissipee(int r, int c) {
return r * c * c;
}
Lorsque la fonction est appelée, la valeur de variables portant
le nom r ou c est protégée et n’est pas modifiée par l’appel.

1. “extent” en anglais.
2. “scope” en anglais.

Programmation en C++ IFT-19965 47


C
++

106 La raison expliquant ce fait est que C++ réserve un espace de


mémoire pour chaque paramètre de la fonction à chaque fois
que la fonction est appelée, sauf si la variable a l’attribut
static. La valeur des arguments est copiée dans l’espace
réservé pour le paramètre et est utilisé dans les calculs. Au
retour de la fonction, l’espace occupé par les paramètres est
libéré. Cette façon de procéder pour le passage des paramètres
s’appelle “passage par valeur”.

107 L’exemple qui suit illustre le propos.


//&% par_valeur.C
#include <iostream.h>

void fonction_1(int a, double b) {


a = a + 10;
b = b + 20.2;
cout << “Dans la fonction” << endl;
cout << “a: “ << a << “ b: “ << b << endl;
}

void main() {

int a = 5;
double b = 8.9;

cout << “Avant l’appel de la fonction” << endl;


cout << “a: “ << a << “ b: “ << b << endl;

fonction_1(a,b);

cout << “Apres l’appel de la fonction” << endl;


cout << “a: “ << a << “ b: “ << b << endl;
}

Résultat
Avant l’appel de la fonction
a: 5 b: 8.9
Dans la fonction
a: 15 b: 29.1
Apres l’appel de la fonction
a: 5 b: 8.9

48 Programmation en C++ IFT-19965


C
++

On remarque qu’il y a une différence entre les variables a et b,


dépendant qu’elles se trouvent à l’extérieur ou à l’intérieur de
la fonction fonction_1 et que la valeur des variables a et b
déclarées dans la fonction main ne sont pas modifiées en dépit
du traitement effectué dans la fonction fonction_1.

108 Le résultat de l’exemple qui précède nous permet d’arriver aux


deux conclusions suivantes:
1. les valeurs des paramètres d’une fonction ne sont pas acces-
sibles en dehors de celle-ci, autrement, le programme du
paragraphe 107 se comporterait très différemment
2. lorsqu’une fonction appelle une autre fonction, la valeur des
paramètres dans la fonction appelante ne sont pas accessi-
bles durant l’exécution de la fonction appelée. Dans l’exem-
ple du paragraphe 107, on voit bien qu’il est impossible que
la fonction fonction_1 puisse utiliser les variables de la
fonction appelante main puisque fonction_1 est déclarée
avant même que les variables de main ne soient elles-mêmes
déclarées dans ce cas spécifique.

109 En général, une déclaration est un élément d’un programme


qui fournit au compilateur l’information nécessaire afin qu’il
puisse comprendre que tel type de données est associé avec tel
identificateur. Par exemple, lorsqu’on spécifie le type d’une
variable ou d’un paramètre, on déclare cette variable ou ce
paramètre.
D’autre part, la définition d’une variable force le compilateur
à réserver un espace mémoire pour un identificateur lors de la
compilation. Par exemple, lorsqu’une variable apparaît en
dehors du corps de toutes les fonctions d’un programme, elle est
à la fois déclarée et définie parce que le compilateur est informé
sur le type de la variable et parce que le compilateur réserve de
la mémoire pour stocker le contenu de cette variable lors de la
compilation.
D’autre part, lorsqu’une variable apparaît à l’intérieur du corps
d’une fonction, cette variable n’est que déclarée parce que le
compilateur ne réserve pas de mémoire pour stocker la valeur
de cette variable lors de la compilation. L’espace ne sera réservé

Programmation en C++ IFT-19965 49


C
++

que lorsque la fonction sera appelée lors de l’exécution du


programme.
En général, on dit qu’une variable est définie lorsqu’elle est à la
fois déclarée et définie.
Une fonction est toujours à la fois définie et déclarée parce
qu’on doit spécifier le type de retour de la fonction et parce que
le compilateur réserve de l’espace pour le code de la fonction
lors de la compilation.

110 Une variable déclarée à l’intérieur d’une fonction est appelée


variable locale, ou encore automatique. Une variable définie
à l’extérieur de toute fonction est appelée variable globale.
Les règles suivantes s’appliquent aux variables locales:
1. les variables locales ne sont accessibles que dans la fonction
à l’intérieur de laquelle elles sont déclarées,
2. la valeur des variables locales d’une fonction n’est plus acces-
sible une fois que l’exécution de cette fonction est complétée,
3. lorsqu’une fonction en appelle une autre, la valeur des varia-
bles de la fonction appelante ne sont pas accessibles lors de
l’exécution de la fonction appelée.

111 Les règles pour les variables globales, celles qui sont définies à
l’extérieur de toutes les fonctions, sont très différentes:
1. la valeur des variables globales est accessible de toutes les
fonctions définies après la définition de la variable sauf dans
le cas où un paramètre ou une variable locale de la fonction
possède le même nom. Dans ce cas, on dit que la variable
locale masque la variable globale.
2. aux endroits où une variable globale n’est pas masquée par
une variable ou un paramètre local, sa valeur peut être
modifiée par une opération d’affectation. Ce changement est
permanent dans le sens où la nouvelle valeur stockée dans la
variable globale efface l’ancienne valeur qui s’y trouvait et
cette valeur est définitivement perdue.

112 L’espace mémoire réservé pour une variable globale n’est


jamais réalloué par le compilateur. Pour cette raison, on

50 Programmation en C++ IFT-19965


C
++

qualifie les variables globales de statiques et on dit qu’elles


ont une durée statique. Par contre, la mémoire allouée pour
les paramètres de fonction ou les variables locales est réallouée
dès que l’exécution de la fonction est complétée, c’est pourquoi
on qualifie les variables locales de dynamiques et qu’on dit
que les variables locales ont une durée dynamique.
Les variables globales peuvent être évaluées ou subir une
affectation dès leur définition. Elles ont donc une portée
universelle. Par conséquent, les paramètres de fonction et les
variables locales ont une portée locale.

113 Comme exemple, supposons que vous désiriez calculer la valeur


de la phase ωt d’un phaseur ejωt à l’instant t = 8.7 sec. La
fréquence d’oscillation du phaseur est f = 20 Hz. On sait que la
pulsation ω est donnée par:

ω = 2πf EQ 1

Le petit programme suivant donne une solution intéressante


utilisant une variable globale pour la valeur de π.
//&% var_glob.C
#include <iostream.h>

const double pi = 3.14159; /* Variable globale accessible


de toutes les parties
du programme*/

double phase(double f, double t) {


return 2*pi*f*t;
}

void main() {

double f = 20;
double t = 8.7;

cout << “Phase: “ << phase(f,t) << endl;

Résultat
Phase: 1093.27

Programmation en C++ IFT-19965 51


C
++

Parce que pi est non seulement une quantité globale mais


également une constante mathématique, nous avons spécifié
cette caractéristique en incluant le mot réservé const dans sa
définition. Lorsque le mot réservé const apparaît dans une
déclaration ou une définition, cela implique que la valeur de la
quantité ne pourra jamais être modifiée par un opération
d’affectation. Par exemple, dans le programme ci-dessus, une
instruction du genre:
pi = 3.0;
aurait causé une erreur lors de la compilation parce que pi est
défini comme une constante.

Notion avancée
114 Au lieu de définir nous-même la constante mathématique pi
dans le programme ci-dessous, nous aurions pu procéder de
façon plus judicieuse en utilisant directement les ressources
mises à notre disposition par les librairies de constantes et de
fonctions mathématiques du C++. Dans cette librairie, dont les
ressources sont disponibles dans le fichier d’inclusion math.h,
la constante mathématique π est identifiée par la macro M_PI
(notez bien les majuscules). Une macro est traitée par le
compilateur de façon telle que lorsqu’elle est rencontrée dans
un programme, elle est immédiatement remplacée dans le code
par sa valeur (ici 3.14159 pour M_PI). Le programme de calcul
de phase prendrait donc la forme suivante:
//&% var_glob_math.C
#include <iostream.h>
#include <math.h> //Inclusion de la lib. mat. du C++

double phase(double f, double t) {


return 2*M_PI*f*t;
}

void main() {

double f = 20;
double t = 8.7;

cout << “Phase: “ << phase(f,t) << endl;

52 Programmation en C++ IFT-19965


C
++

Résultat
Phase: 1093.27

115 Un énoncé composé, aussi connu sous le nom de bloc d’énoncés,


est un groupe d’énoncés compris entre des accolades ({ }). Un
énoncé composé peut contenir ses propres déclarations de
variables.
La portée des variables déclarées à l’intérieur d’un énoncé
composé se limite à l’énoncé composé lui-même. La durée de vie
des variables est le temps d’exécution de l’énoncé composé.
Notez que les fonctions sont un exemple d’énoncé composé.
Nous verrons d’autres exemples d’énoncés composés lorsque
nous traiterons des énoncés while, for et if.

Notion avancée
116 Les variables globales static sont des variables dont la portée
s’étend à un fichier d’un programme réparti sur plusieurs
fichiers. Une variable globale static peut être évaluée ou être
l’objet d’une affectation en tout point d’un fichier après qu’elle
ait été déclarée mais ne peut être évaluée ou être l’objet d’une
affectation dans les autres fichiers. Nous traiterons de ce détail
plus loin.

Résumé
117 ✔ Une variable locale est une variable qui est déclarée à
l’intérieur d’une fonction. Une variable globale est pour sa part
définie à l’extérieur de toutes les fonctions d’un programme.
✔ C++ isole les variables locales et les paramètres des variables
globales, ce qui permet d’en réutiliser le nom. La valeur des
paramètres et des variables locales n’est plus disponible après
le retour de la fonction. Lorsqu’une fonction appelle une autre
fonction, la valeur des paramètres et des variables locales de la
fonction appelante ne sont pas accessibles durant l’exécution de
la fonction appelée.
✔ La valeur d’une variable globale est accessible partout sauf
lorsque cette variable globale est masquée par une variable

Programmation en C++ IFT-19965 53


++ C
locale portant le même nom. On peut affecter une valeur à une
variable globale partout où elle est accessible (i.e. non
masquée).
✔ On peut avoir accès à la valeur d’une constante globale
partout où elle n’est pas masquée mais on ne peut changer sa
valeur par une affectation.

54 Programmation en C++ IFT-19965


C
++

CHAPITRE 9 La création des classes


et des objets en C++

118 Pour décrire une porte logique NAND à deux entrées, on pense
naturellement à la valeur des deux entrées et à la valeur de
sortie compte tenu de ces entrées. Ces quantités forment un
tout pour chaque porte NAND prise individuellement. Chaque
porte NAND à deux entrées d’un circuit peut être vue comme
un objet. Il en est de même pour une porte NOR à deux entrées.
Dans ce chapitre, nous abordons l’un des concepts qui font du
C++ un langage de programmation très puissant. Ce concept,
celui des classes et des objets, permet au programmeur de
créer, construire, et manipuler des groupes de données qui
décrivent des individus ou des catégories d’objets du monde
réel. Ce concept distingue le C++ de la plupart des autres
langages de programmation, y compris le C.

119 C++ invite le programmeur à définir des classes, comme les


classes NAND ou NOR par exemple, qui correspondent
naturellement à des catégories d’objets réels. Une fois qu’une
classe a été définie, on peut créer autant d’objets appartenant à
cette classe que l’application l’exige. Chaque objet de la classe
correspond à un individu propre de cette catégorie. La création
de classes permet donc de concevoir du code plus clair et plus
facile à réutiliser.

Programmation en C++ IFT-19965 55


C
++

120 Les types de données standards tel int, double, long, char,
float, sont également des classes. Plus spécifiquement, ils
sont des classes intrinsèques1 au C++. Les classes conçues
par le programmeur et utilisant les classes intrinsèques sont
appelés classes extrinsèques2.
Les individus appartenant à ces classes intrinsèques sont
appelés objets de données intrinsèques.

121 Par ailleurs, les objets appartenant à des classes extrinsèques


sont généralement composés de plusieurs parties, chaque
partie pouvant être manipulée individuellement. Les parties
individuelles sont elles-mêmes formées d’objets de données
intrinsèques aussi appelés objets atomiques. Les objets des
classes extrinsèques sont appelés objets composés.

122 Lorsqu’une classe est définie, on doit spécifier au C++ les


détails concernant les objets appartenant à cette classe. On
peut également définir des fonctions pouvant travailler avec
ces objets. La portion de code qui suit montre comment la classe
NAND peut être définie en C++:
//&% nand_ch_9_1.h

class
NAND_2
{
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

};
La FIGURE 2 montre la signification de chaque partie du code.
La définition de la classe NAND ne décrit que les variables
in_1, in_2 et out_1 seulement. Aucune définition de fonction
n’est incluse dans la définition de la classe. De plus, la
définition de la classe indique au compilateur, via le mot
réservé public:, que les variables de la classe seront

1. “built-in classes” en anglais.


2. “user-defined classes” en anglais.

56 Programmation en C++ IFT-19965


C
++

FIGURE 2 Classe nand_2 du paragraphe 122

class Informe le C++ qu’une classe est définie


NAND_2 Nom de la classe
{ Début du corps de la classe
public: Spécifie le niveau d’accès aux variables
int in_1; /*entree 1*/

int in_2; /*entree 2*/

int out_1; /*sortie */


} Variables de la classe

}; Fin du corps de la classe


Fin de la définition

accessibles pour la lecture et l’écriture (i.e. l’affectation) dès le


moment où la classe NAND aura été définie.

123 Au paragraphe 109, nous avons vu qu’une déclaration est un


élément d’un programme qui informe le compilateur de
l’existence d’une variable tandis qu’une définition force le
compilateur à réserver de l’espace mémoire pour stocker le
contenu d’une variable. Une définition de classe n’implique pas
toujours que de l’espace est réservé par le compilateur. On
pourrait donc croire qu’il s’agit plutôt d’une déclaration.
Cependant, nous allons quand même conserver le mot
définition lorsque nous décrirons les éléments d’une classe.

124 Les variables qui apparaissent dans une définition de classe,


par exemple in_1, in_2 et out_1 de la classe NAND, sont
appelées variables membres de la classe.

125 Une fois que la classe NAND est définie, on peut créer un objet
de type NAND_2 en déclarant une variable de la façon
suivante:
NAND_2 nnd;

Programmation en C++ IFT-19965 57


C
++

La syntaxe de déclaration est la même que pour une variable


de type intrinsèque int ou double par exemple.

126 Une fois qu’un objet de la classe NAND_2 a été créé, on peut
accéder au contenu des variables membres de cet objet en
utilisant le nom de l’objet et en faisant appel à l’opérateur
d’accès aux membres d’une classe, symbolisé par un point “.”,
suivi du nom de la variable membre. Par exemple, pour accéder
au contenu de la variable membre in_1 de l’objet nnd de la
classe NAND_2, il suffit d’utiliser la notation suivante:
nnd.in_1.
Une fois la méthode d’accès aux variables membres établie, on
peut lire le contenu des variables membres ou même en
modifier le contenu en effectuant des affectations à ces
variables. Par exemple, on peut concevoir un programme qui
affecte des valeurs aux variables membres d’entrée et qui
calcule, via une fonction, la valeur de la variable de sortie d’une
porte NAND. Le programme suivant exécute cette tâche:
//&% nand_ch_9_2.C
#include <iostream.h>

class NAND_2 {
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */
};

int set_out_1(int i1, int i2) {


return 1 - (i1 * i2);
}

void main() {

NAND_2 nnd;

cout << “La table de verite d’une porte nand” << endl;
cout << “est: “ << endl;
cout << endl << endl;
cout << “entree 1 entree 2 sortie” << endl;

nnd.in_1 = 0 ; nnd.in_2 = 0 ;

58 Programmation en C++ IFT-19965


C
++

cout << “ “ << nnd.in_1


<< “ “ << nnd.in_2 << “ “
<< set_out_1(nnd.in_1, nnd.in_2) << endl;

nnd.in_1 = 0 ; nnd.in_2 = 1 ;
cout << “ “ << nnd.in_1
<< “ “ << nnd.in_2 << “ “
<< set_out_1(nnd.in_1, nnd.in_2) << endl;

nnd.in_1 = 1 ; nnd.in_2 = 0 ;
cout << “ “ << nnd.in_1
<< “ “ << nnd.in_2 << “ “
<< set_out_1(nnd.in_1, nnd.in_2) << endl;

nnd.in_1 = 1 ; nnd.in_2 = 1 ;
cout << “ “ << nnd.in_1
<< “ “ << nnd.in_2 << “ “
<< set_out_1(nnd.in_1, nnd.in_2) << endl;
}

Résultat

La table de verite d’une porte nand


est:

entree 1 entree 2 sortie


0 0 1
0 1 1
1 0 1
1 1 0

Evidemment, comme nous n’avons vu que les opérateurs


arithmétiques courants, le calcul de la valeur logique de la
sortie est non conventionnelle mais donne quand même le
résultat escompté. Ce qu’il faut surtout remarquer dans
l’exemple précédent, c’est la façon dont on accède aux variables
membres de l’objet nnd de la classe NAND_2 avec l’opérateur
d’accès aux variables membres “.”.

Notion avancée
127 On peut se demander pourquoi une définition de classe doit se
terminer par un “;” comme c’est le cas pour la classe NAND_2 et
toute classe en C++. Pourquoi l’accolade droite “}” ne suffit-elle

Programmation en C++ IFT-19965 59


C
++

pas pour terminer la définition? Une raison est que, en utilisant


le “;”, il est possible de définir une classe tout en définissant des
variables globales dans un même énoncé. Par exemple, le
segment de code ci-dessous définit deux objets globaux nnd1 et
nnd2 de la classe NAND_2 en même temps que la classe NAND_2
est définie:
class NAND_2 {
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */
} nnd1, nnd2;
Le symbole de ponctuation “;” informe donc le compilateur C++
que la liste des variables globales se termine.

128 Dans le paragraphe 126, il serait possible de définir une


fonction set_out_1 qui reçoit un objet NAND_2 en argument
plutôt que de recevoir deux arguments de type int. Pour ce
faire, il faut retravailler le code de la fonction pour tenir compte
de ce changement:
int set_out_1(NAND_2 nd) {
return 1 - (nd.in_1 * nd.in_2);
}

129 Supposons maintenant que l’on désire ajouter une nouvelle


classe pour décrire les objets de type NOR_2 (porte NOR à deux
entrées) et qu’on désire également afficher la table de vérité de
ce type de porte avec une fonction également appelée
set_out_1. La classe NOR_2 pourrait être définie de la façon
suivante:

//&% nor_ch_9_1.h
class
NOR_2
{
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

};

60 Programmation en C++ IFT-19965


C
++

et le programme pour afficher les tables de vérité des portes


NAND et NOR pourrait ressembler à ce qui suit:
//&% nand_nor_ch_9_2.C
#include <iostream.h>

class
NAND_2
{
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */
};

class NOR_2 {
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */
};

int set_out_1(NAND_2 nd) {


return 1 - (nd.in_1 * nd.in_2);
}

int set_out_1(NOR_2 nr) {


return (1-nr.in_1) * (1-nr.in_2);
}

void main() {

NAND_2 nnd;
NOR_2 nor;

cout << “La table de verite d’une porte nand” << endl;
cout << “est: “ << endl;
cout << endl << endl;
cout << “entree 1 entree 2 sortie” << endl;

nnd.in_1 = 0 ; nnd.in_2 = 0 ;
cout << “ “ << nnd.in_1
<< “ “ << nnd.in_2 << “ “
<< set_out_1(nnd) << endl;

nnd.in_1 = 0 ; nnd.in_2 = 1 ;
cout << “ “ << nnd.in_1
<< “ “ << nnd.in_2 << “ “
<< set_out_1(nnd) << endl;

Programmation en C++ IFT-19965 61


C
++

nnd.in_1 = 1 ; nnd.in_2 = 0 ;
cout << “ “ << nnd.in_1
<< “ “ << nnd.in_2 << “ “
<< set_out_1(nnd) << endl;

nnd.in_1 = 1 ; nnd.in_2 = 1 ;
cout << “ “ << nnd.in_1
<< “ “ << nnd.in_2 << “ “
<< set_out_1(nnd) << endl << endl;

cout << “La table de verite d’une porte nor” << endl;
cout << “est: “ << endl;
cout << endl << endl;
cout << “entree 1 entree 2 sortie” << endl;

nor.in_1 = 0 ; nor.in_2 = 0 ;
cout << “ “ << nor.in_1
<< “ “ << nor.in_2 << “ “
<< set_out_1(nor) << endl;

nor.in_1 = 0 ; nor.in_2 = 1 ;
cout << “ “ << nor.in_1
<< “ “ << nor.in_2 << “ “
<< set_out_1(nor) << endl;

nor.in_1 = 1 ; nor.in_2 = 0 ;
cout << “ “ << nor.in_1
<< “ “ << nor.in_2 << “ “
<< set_out_1(nor) << endl;

nor.in_1 = 1 ; nor.in_2 = 1 ;
cout << “ “ << nor.in_1
<< “ “ << nor.in_2 << “ “
<< set_out_1(nor) << endl;
}

Résultat
La table de verite d’une porte nand
est:

entree 1 entree 2 sortie


0 0 1
0 1 1
1 0 1
1 1 0

La table de verite d’une porte nor

62 Programmation en C++ IFT-19965


C
++

est:

entree 1 entree 2 sortie


0 0 1
0 1 0
1 0 0
1 1 0

On remarque que la fonction set_out_1 est surdéfinie car elle


peut travailler avec un paramètre de la classe NAND_2 ou un
paramètre de la classe NOR_2. C’est le compilateur qui décide,
selon le contexte, laquelle des deux versions de la fonction il
faut choisir. Cela n’aurait pas été possible si la fonction avait
continué de recevoir deux paramètres de type int.

Notion avancée
130 La fonction set_out_1 ci-dessus reçoit un objet en paramètre.
Le C++ se comporte dans ce cas de la même manière que pour
le cas d’un type de données intrinsèque: l’objet passé en
argument est copié dans l’espace réservé pour le paramètre lors
de l’appel de la fonction et la fonction travaille donc sur une
copie de l’objet défini dans le programme main. Nous verrons
plus tard que cette façon de procéder est déconseillée en
programmation C++ et nous verrons les moyens de contourner
cette pratique.

Exercice
131 Concevez une classe OR_2 pour décrire une porte OR à deux
entrées.

Exercice
132 Concevez une fonction qui reçoit un objet de type OR_2 et qui
affiche sa table de vérité.

Résumé
133 ✔ En C++, les classes correspondent à des catégories et les
objets appartenant à cette classe correspondent à des individus
de cette catégorie.

Programmation en C++ IFT-19965 63


++ C
✔ La définition d’une classe comprend généralement des
variables membres.
✔ L’opérateur d’accès aux variables membres d’une classe
s’utilise comme suit: nom_objet.nom_variable_membre
✔ Une fonction peut être surdéfinie. Le C++ décide de la
version à utiliser selon le type des arguments qui lui sont
passés et non selon le type de retour.

64 Programmation en C++ IFT-19965


C
++

CHAPITRE 10 La définition de
fonctions membres

134 Ce chapitre décrit comment on peut définir une fonction


spécifique à une classe à l’intérieur de la définition même de la
classe. De telles fonctions deviennent parties intégrantes de la
définition de la classe et leur appel reflète cette relation
d’appartenance.

135 Au paragraphe 128, nous avons défini une fonction permettant


d’afficher la table de vérité d’une porte NAND:
int set_out_1(NAND_2 nd) {
return 1 - (nd.in_1 * nd.in_2);
}
On peut définir une fonction beaucoup plus spécifique aux
objets de type NAND_2 en déplaçant sa définition à l’intérieur
de la définition de la classe NAND_2. Une telle fonction, comme
une variable membre, est appelée fonction membre de la
classe dans laquelle elle est définie.

136 Par convention, des changements en apparence mineurs mais


néanmoins importants de la syntaxe distinguent les appels aux
fonctions membres des appels aux fonctions ordinaires. Plus
spécifiquement, chaque fonction membre possède un
argument spécial:

Programmation en C++ IFT-19965 65


C
++

• la valeur de cet argument spécial est un objet appartenant à


la même classe que la fonction membre
• cet argument spécial n’apparaît pas entre parenthèses
comme le font les autres arguments de la fonction. Il est
plutôt associé, via l’opérateur d’accès aux membres de la
classe (l’opérateur “.” vu précédemment) au nom de la
fonction membre d’une manière similaire à celle avec
laquelle on accède aux variables membres.
Par exemple, pour appeler la fonction set_out_1 qui
appartient à la classe NAND_2, pour un objet ayant pour nom
nnd, il suffit d’adopter la syntaxe suivante:
nnd.set_out_1();
Ce qu’il est important de remarquer dans l’énoncé précédent,
c’est que la fonction ne reçoit plus d’argument de type NAND_2,
cet argument étant maintenant implicite via l’objet nnd.

137 La définition des fonctions membres diffère de celle des


fonctions ordinaires sur les points suivants:
• les fonctions membres n’ont aucun paramètre correspondant
à l’objet pour lequel elles sont appelées
• dans les fonctions membres, il n’y a ni paramètre ni variable
associés aux variables membres via l’opérateur d’accès “.”.
Les variables membres de l’objet sont plutôt directement
accessibles et sont considérées comme appartenant à
l’argument spécial qui est l’objet pour laquelle la fonction
membre est appelée.
En suivant ces principes, la fonction membre set_out_1 de la
classe NAND_2 peut être définie comme suit:
//&% nand_ch_10_1.h
class
NAND_2
{
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

int set_out_1() {
return 1 - (in_1 * in_2);
}

};

66 Programmation en C++ IFT-19965


C
++

Lors que la fonction set_out_1 est appelée pour un objet de la


classe NAND_2, les variables membres in_1 et in_2
apparaissant dans la fonction sont considérées comme
appartenant automatiquement à cet objet.

138 On peut maintenant récrire le programme d’affichage de la


table de vérité d’une porte NAND à deux entrées vu au
paragraphe 126 en tirant profit de la notion de fonction
membre:
//&% nand_ch_10_2.C
#include <iostream.h>
class
NAND_2
{
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

int set_out_1() {
return 1 - (in_1 * in_2);
}
};

void main() {

NAND_2 nnd;

cout << “La table de verite d’une porte nand” << endl;
cout << “est: “ << endl;
cout << endl << endl;
cout << “entree 1 entree 2 sortie” << endl;

nnd.in_1 = 0 ; nnd.in_2 = 0 ;

cout << “ “ << nnd.in_1


<< “ “ << nnd.in_2 << “ “
<< nnd.set_out_1() << endl;

nnd.in_1 = 0 ; nnd.in_2 = 1 ;
cout << “ “ << nnd.in_1
<< “ “ << nnd.in_2 << “ “
<< nnd.set_out_1() << endl;

nnd.in_1 = 1 ; nnd.in_2 = 0 ;
cout << “ “ << nnd.in_1

Programmation en C++ IFT-19965 67


C
++

<< “ “ << nnd.in_2 << “ “


<< nnd.set_out_1() << endl;

nnd.in_1 = 1 ; nnd.in_2 = 1 ;
cout << “ “ << nnd.in_1
<< “ “ << nnd.in_2 << “ “
<< nnd.set_out_1() << endl;
}

Résultat
La table de verite d’une porte nand
est:

entree 1 entree 2 sortie


0 0 1
0 1 1
1 0 1
1 1 0

139 Remarquez que C++ ne copie pas l’argument spécial de l’objet


pour lequel la fonction membre est appelée. Si on réaffecte une
des valeurs membres de l’objet en appelant la fonction membre,
cette réaffectation est immédiate et l’ancienne valeur est
perdue.

140 Les fonctions membres peuvent recevoir des arguments


ordinaires autres que l’argument spécial de l’objet de la classe
qui les appelle. Par exemple, on pourrait être intéressé à
concevoir une fonction set_out_1 qui reçoit en argument
ordinaire la valeur de sortie que l’on veut avoir pour la porte
NAND. La déclaration de la classe NAND_2 serait alors:
//&% nand_ch_10_2.h
class
NAND_2
{
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

int set_out_1(int val) {

68 Programmation en C++ IFT-19965


C
++

return val;
}
};

Pour appeler la fonction membre set_out_1 pour l’objet nnd,


il suffit d’adopter la syntaxe suivante:
nnd.set_out_1(1);//pour forcer l’affichage de la sortie à 1
nnd.set_out_1(0);//pour forcer l’affichage de la sortie à 0

141 Evidemment, comme nous le verrons dans les paragraphes qui


suivent, une définition de classe peut contenir plusieurs
définition de fonctions membres. Or, la définition de plusieurs
fonctions membres dans une définition de classe peut rendre
cette définition très longue, ce qui va à l’encontre du principe de
programmation, appelé principe de visibilité, qui veut que l’on
essaie de garder les segments de code les plus courts possibles
afin de pouvoir en prendre connaissance rapidement et ainsi
les comprendre facilement. Pour faciliter la mise en application
du principe de visibilité, le C++ permet de fournir au
compilateur une information sur une fonction membre d’une
classe sans que la fonction ne soit effectivement définie à
l’intérieur de la classe elle-même. L’information sur la fonction
membre est fournie à la classe sous la forme d’un prototype de
fonction. Une fois le prototype inclus à la définition de la classe,
le définition de la fonction peut être faite à l’extérieur de la
définition de la classe. Reprenons l’exemple de classe du
paragraphe 137. Dans cette définition de la classe NAND_2, le
prototype de la fonction membre set_out_1 serait
simplement:
int set_out_1();

Ce prototype dit au C++ tout ce qui est important sur la


fonction membre set_out_1: le nom de la fonction, le type de
la valeur de retour de la fonction et la liste des arguments avec
leur type (ici, la fonction ne reçoit aucun argument).

Notion avancée
142 Remarquez qu’à proprement parler, l’inclusion du prototype de
la fonction membre dans la définition de la classe est une
déclaration de la fonction.

Programmation en C++ IFT-19965 69


C
++

143 La définition de la classe incluant le prototype est maintenant:


//&% nand_ch_10_3.h

class
NAND_2
{
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

int set_out_1();

};

Pour utiliser la fonction membre set_out_1 de la classe


NAND_2, il faut maintenant écrire le code associé à cette
fonction et spécifier que ce code s’applique à la fonction
set_out_1 de la classe NAND_2. Ce dernier point est
important parce que deux classes différentes peuvent avoir des
fonctions membres qui ont le même nom à cause du principe
d’encapsulation des données et des fonctions dans les classes
offert par le C++. La syntaxe adoptée par le C++ pour définir
les fonctions membres d’une classe à l’extérieur du corps de
celle-ci utilise l’opérateur d’évaluation de portée1 “::”:
//&% nand_ch_10_4.h
class
NAND_2
{
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

int set_out_1();

};

int NAND_2::set_out_1() {
return 1 - (in_1 * in_2);
}
Le nom de la classe et les deux points précédant le nom de la
fonction ont deux utilités:

1. “scope resolution operator” en anglais

70 Programmation en C++ IFT-19965


C
++

• ils annoncent qu’une définition est une définition de fonction


membre
• ils établissent à quelle classe la fonction membre appartient

Notion avancée
144 Nous verrons plus loin la notion de fonction en ligne. Le
compilateur tente de compiler ces fonctions définies à
l’intérieur des classes. Par contre, pour les fonctions déclarées
dans les classes grâce à un prototype et définies à l’extérieur de
celles-ci grâce à l’opérateur d’évaluation de portée, le
compilateur ne tente aucune compilation.

145 L’importance d’inclure les fonctions membres à l’intérieur des


classes repose sur le fait que les fonctions membres sont
maintenant plus directement associées aux objets de cette
classe ce qui rend donc la compréhension du code plus facile. De
plus, comme nous le verrons plus loin, l’avantage le plus
marqué de cette pratique est que les fonctions membres ont
certains privilèges d’accès aux variables membres via les
concepts de sections privées (private) et protégées
(protected) de la classe.

Résumé
146 ✔ Si l’on désire transformer une fonction acceptant un
paramètre appartenant à une classe en une fonction membre de
la classe, il suffit de définir la fonction à l’intérieur de la
définition de la classe et d’éliminer le paramètre de la fonction
correspondant à cette classe de la liste des paramètres de la
fonction et du nom des variables membres accédées via
l’opérateur d’accès aux membres d’une classe.
✔ Si une fonction membre comporte un grand nombre
d’énoncés, il est préférable d’inclure seulement son prototype à
la définition de la classe et d’écrire le code qui lui est associé à
l’extérieur de la définition de la classe en utilisant l’opérateur
d’évaluation de portée “::” flanqué à gauche du nom de la
classe et à droite du nom de la fonction membre pour spécifier à
quelle classe appartient la fonction membre:
nom_classe::nom_fonction
✔ Pour appeler une fonction membre, il suffit d’utiliser
l’opérateur d’accès aux membres d’une classe avec le nom de
l’objet pour lequel la fonction est appelée et le nom de la

Programmation en C++ IFT-19965 71


++ C
fonction membre avec ses paramètres ordinaires:
nom_objet.nom_fonction(liste paramètres)

72 Programmation en C++ IFT-19965


C
++

CHAPITRE 11 Les constructeurs

147 Dans ce chapitre, nous introduisons une catégorie spéciale de


fonctions membres appelées constructeurs1. Ces fonctions
membres spéciales sont appelées lorsque des objets d’une
classes sont créés.

148 Aux chapitres précédents, nous avons vu la syntaxe pour


déclarer des objets appartenant à des classes spécifiques:
NAND_2 nnd;
NOR_2 nr;
Pour initialiser les variables membres de ces classes, il suffit de
suivre la syntaxe suivante:
nnd.in_1 = 0;
nnd.in_2 = 0;
nr.in_1 = 1;
nr.in_2 = 0;
Pour plusieurs classes, les variables membres d’un objet ont
des valeurs initiales facilement prévisibles qu’il serait
préférable d’initialiser dès la création d’un objet. A cet égard, le
C++ nous permet de définir une fonction spéciale appelée
constructeur par défaut qui est appelé automatiquement

1. “constructor” en anglais

Programmation en C++ IFT-19965 73


C
++

à chaque fois qu’un objet d’une classe est créé. Un tel


constructeur défaut permet d’initialiser les variables membres
de chaque objet nouvellement créé.

149 Dans la définition d’une classe, le constructeur défaut , qui est


une fonction membre de la classe, se distingue des autres
fonctions membres de trois façons:
• le nom du constructeur par défaut est le même que celui de
la classe
• le constructeur par défaut n’a aucune valeur de retour
• le constructeur défaut ne peut pas recevoir d’arguments.

150 Dans la définition de la classe NAND_2 discutée dans les


paragraphes précédents, il serait intéressant d’initialiser les
variables d’entrée à une valeur connue et de fixer la valeur de
la sortie en fonction de la table de vérité de la porte. Nous
allons ajouter un constructeur par défaut à la définition de la
classe NAND_2 afin de procéder à ces opérations d’initialisation.
La définition de la classe incluant le constructeur par défaut
peut avoir la forme suivante:
//&% nand_ch_11_1.h
class
NAND_2
{
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

NAND_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

int set_out_1() {
return 1 - (in_1 * in_2);
}
};

La définition précédente comporte plusieurs points


intéressants. Premièrement, on remarque la définition du
constructeur par défaut NAND_2(). Ce constructeur par défaut

74 Programmation en C++ IFT-19965


C
++

ne reçoit aucun argument et ne retourne aucune valeur,


conditions que doivent respecter les constructeurs défauts. On
remarque aussi que le constructeur défaut initialise les
variables membres in_1 et in_2 à la valeur logique “0” par
une simple affectation de valeur à chaque variable membre.
Deuxièmement, le constructeur défaut appelle la fonction
membre set_out_1 pour calculer la valeur initiale de la
variable membre out_1 en fonction des valeurs des variables
membres in_1 et in_2 initialisées dans les deux énoncés
précédents. Il est plus judicieux d’utiliser la fonction membre
set_out_1 pour initialiser la valeur de out_1 étant donné que
cette variable membre dépend des deux variables d’entrée via
la relation logique de la porte NAND. Cet exemple montre
qu’une fonction membre peut être appelée d’une autre fonction
membre et ce, sans avoir à utiliser l’opérateur d’accès aux
membres “.” puisque la fonction est appelée directement pour
l’objet actuellement en traitement. On réalise ici une première
manifestation concrète de la puissance de la programmation
objet: un objet utilise des fonctions membres qui lui sont
associées indépendamment des fonctions membres des autres
objets. Les traitements propres à un objet sont effectués par des
fonctions membres qui lui sont associées. Un traitement sur un
objet est donc encapsulé dans cet objet propre, sans faire
référence aux autres éléments du programme.

151 Un bon style de programmation consiste à définir un


constructeur par défaut dans chaque classe. La justification
d’une telle pratique est que le C++ définit lui-même un
constructeur par défaut (invisible dans la définition de la
classe) si le concepteur du programme n’en a pas lui-même
défini un. Or, il est préférable d’avoir un constructeur défaut
apparaissant explicitement dans la définition de la classe que
d’en avoir un invisible et implicite.
Notez qu’il est possible de définir un constructeur par défaut
qui n’initialise pas les variables membres et dont le corps ne
contient aucun énoncé.

152 Comme le C++ supporte la surdéfinition des fonctions et que


les constructeurs sont des fonctions, il est donc logique de
penser qu’il est possible de définir plusieurs constructeurs pour

Programmation en C++ IFT-19965 75


C
++

une même classe. Il faut cependant noter qu’il ne peut y avoir


qu’un seul constructeur par défaut pour une classe donnée.
Supposons qu’on veuille initialiser les variables membres in_1
et in_2 à des valeurs précises différentes de celles fixées par le
constructeur par défaut lors de la création d’un objet de type
NAND_2. Il est possible de réaliser ceci en définissant un
constructeur différent du constructeur défaut qui reçoit les
valeurs d’initialisation en arguments. Ce constructeur porte le
même nom que la classe et ne retourne aucune valeur mais il se
distingue du constructeur défaut justement parce qu’il reçoit
des arguments. La syntaxe est la suivante:
//&% nand_ch_11_2.h
class
NAND_2 {
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

//Constructeur par defaut


NAND_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int set_out_1() {
return 1 - (in_1 * in_2);
}

};

153 Pour déclarer un objet utilisant des valeurs d’initialisation


différentes de celles fournies par le constructeur par défaut, il
suffit de définir une variable en respectant la syntaxe suivante:
NAND_2 nnd(0,1);

La présence des arguments informe le compilateur que le


constructeur qui doit être appelé n’est pas le constructeur par
défaut mais plutôt celui avec les deux arguments de type int.

76 Programmation en C++ IFT-19965


C
++

Notion avancée
154 Il faut absolument inclure un constructeur par défaut dans une
classe dans la situation suivante: un constructeur avec
arguments est défini dans la classe et on écrit, dans le
programme, une définition de variable telle que NAND_2 nnd;.
Dans ce cas, en l’absence de constructeur par défaut, le
compilateur conclut à une erreur de syntaxe puisque le
constructeur de la classe est appelé mais les arguments
manquent. Le compilateur ne prend pas sur lui d’appeler un
constructeur par défaut implicite.

Notion avancée
155 Il serait aussi possible de concecoir un constructeur avec
initialisation qui remplacerait en une seule étape le
constructeur par défaut et le constructeur avec arguments tels
qu’implantés au paragraphe 152. Ce constructeur aurait la
forme suivante:
NAND_2(int i1=0, int i2=0){
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

Exercice
156 Définissez un constructeur par défaut pour la classe NOR_2.

Résumé
157 ✔ Les constructeurs effectuent des opérations, tel
l’initialisation de variables membres, lors de la création d’objets
d’une classe.
✔ Un constructeur porte le nom de sa classe et ne retourne
aucune valeur.
✔ Le constructeur par défaut ne reçoit aucun paramètre. Il est
conseillé de toujours définir un constructeur par défaut
explicitement dans une classe.
✔ Si on veut initialiser les objets aux valeurs par défaut, il
suffit de déclarer ces objets en respectant la syntaxe suivante:
nom_de_la_classe nom_de_l’objet;.
✔ Si on veut initialiser un objet à des valeurs spécifiques lors de
sa création, il suffit de définir un constructeur avec paramètres
et de déclarer l’objet en respectant la syntaxe suivante:

Programmation en C++ IFT-19965 77


++ C
nom_de_la_classe nom_de_l’objet(arg 1,
arg2,...,argn);

78 Programmation en C++ IFT-19965


C
++

CHAPITRE 12 Les fonctions membres


de lecture et d’écriture

158 Ce chapitre présente le concept important de fonctions


membres de lecture et d’écriture. Ce type de fonctions
permet d’accéder aux valeurs des variables membres et
d’affecter des valeurs aux variables membres après leur
initialisation avec le constructeur.
Il est important de noter que les fonctions membres de lecture
et d’écriture n’ont rien à voir avec les fonctions d’entrée-sortie
discutées précédemment lorsque nous avons présenté les
opérateurs d’extraction “>>” et d’insertion “<<“.

159 Nous savons qu’il est possible d’accéder à la valeur d’une


variable membre d’un objet via l’opérateur d’accès aux
membres “.”:
nnd.in_1;

On peut aussi accéder au contenu d’une variable membre


indirectement en définissant une fonction membre qui retourne
la valeur de la variable d’intérêt. Par exemple, il est possible de
définir trois fonctions membres pour lire la valeur des variables
membres de la classe NAND_2. Ces fonctions membres sont
appelées fonctions membres de lecture:
//&% nand_ch_12_1.h

Programmation en C++ IFT-19965 79


C
++

class
NAND_2 {
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

//Constructeur par defaut


NAND_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int set_out_1() {
return 1 - (in_1 * in_2);
}

int lire_in_1() {
return in_1;
}

int lire_in_2() {
return in_2;
}

int lire_out_1() {
return out_1;
}

};

Un programme de test serait par exemple:


//&% nand_ch_12_1.C
#include <iostream.h>

class
NAND_2 {
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

80 Programmation en C++ IFT-19965


C
++

//Constructeur par defaut


NAND_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int set_out_1() {
return 1 - (in_1 * in_2);
}

int lire_in_1() {
return in_1;
}

int lire_in_2() {
return in_2;
}

int lire_out_1() {
return out_1;
}

};

void main() {

NAND_2 nnd_1;
NAND_2 nnd_2(0,1);

cout << “entree 1, nand 1: “ << nnd_1.lire_in_1() << endl;


cout << “entree 2, nand 1: “ << nnd_1.lire_in_2() << endl;
cout << “sortie, nand 1: “ << nnd_1.lire_out_1() << endl;
cout << “entree 1, nand 2: “ << nnd_2.lire_in_1() << endl;
cout << “entree 2, nand 2: “ << nnd_2.lire_in_2() << endl;
cout << “sortie, nand 2: “ << nnd_2.lire_out_1() << endl;

Résultat
entree 1, nand 1: 0
entree 2, nand 1: 0
sortie, nand 1: 1

Programmation en C++ IFT-19965 81


C
++

entree 1, nand 2: 0
entree 2, nand 2: 1
sortie, nand 2: 1

Dans le segment de code qui précède, nous avons défini des


fonctions membres de lecture pour chaque variable membre de
la classe NAND_2 et nous avons vérifié qu’elles permettent
d’accéder aux variables membres de façon indirecte.

160 Une fonction membre de lecture permet donc d’extraire de


l’information d’un objet. Une raison intéressante d’utiliser des
fonctions membres de lecture est qu’il est possible d’extraire de
l’information de l’objet mais également d’ajouter des opérations
en même temps. Par exemple, si on désire que la fonction
membre de lecture informe l’usager de la fréquence d’accès à
une variable membre, on peut par exemple inclure un message
dans le corps de la fonction membre d’accès. Par exemple, on
pourrait ajouter le message suivant à la fonction de lecture de
la variable out_1 de la classe NAND_2:
int lire_out_1() {
cout << “Acces a la variable membre out_1” << endl;
return out_1;
}

161 Un autre attrait des fonctions membres de lecture est qu’il est
possible d’accéder des variables membres fictives d’un objet.
Ces variables membres sont en fait calculées à partir des
valeurs des variables membres réelles. Par exemple, on
pourrait être intéressés à accéder au complément (appelé ici
out_1_not) de la valeur de la variable membre de sortie
out_1 d’un objet de la classe NAND_2. Il suffit simplement de
créer la fonction membre de lecture suivante et de l’inclure à la
définition de la classe:
int lire_out_1_not() {
return 1 - out_1;
}

82 Programmation en C++ IFT-19965


C
++

On remarque ici que out_1_not n’est pas une variable


membre de la classe mais la fonction membre de lecture
lire_out_1_not permet d’accéder à sa valeur à partir de la
valeur de la variable membre réelle out_1.

162 D’autre part, il est aussi possible d’affecter une valeur à une
variable membre par le biais d’une fonction membre
d’écriture plutôt que par l’opérateur d’accès aux membres “.”.
Par exemple, on peut définir une fonction membre d’écriture
pour changer la valeur des entrées d’une porte NAND_2. La
classe NAND_2 devient dans ce cas:
//&% nand_ch_12_2.h

class
NAND_2 {
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

//Constructeur par defaut


NAND_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int set_out_1() {
return 1 - (in_1 * in_2);
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

int lire_in_2() {
return in_2;

Programmation en C++ IFT-19965 83


C
++

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}

};

Un programme test serait le suivant:


//&% nand_ch_12_2.C

#include <iostream.h>

class
NAND_2 {
public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/
int out_1; /*sortie */

//Constructeur par defaut


NAND_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int set_out_1() {
return 1 - (in_1 * in_2);
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

84 Programmation en C++ IFT-19965


C
++

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}

};

void main() {

NAND_2 nnd_1;

cout << “in_1: “ << nnd_1.lire_in_1() << endl;


cout << “in_2: “ << nnd_1.lire_in_2() << endl;
cout << “out_1: “ << nnd_1.lire_out_1() << endl;

nnd_1.ecrire_in_1(1);
nnd_1.ecrire_in_2(1);
cout << “in_1: “ << nnd_1.lire_in_1() << endl;
cout << “in_2: “ << nnd_1.lire_in_2() << endl;
cout << “out_1: “ << nnd_1.lire_out_1() << endl;

Résultat
in_1: 0
in_2: 0
out_1: 1
in_1: 1
in_2: 1
out_1: 0

Il y a plusieurs choses à noter dans l’exemple précédent. Tout


d’abord, nous avons ajouté deux fonctions membres d’écriture à
la classe NAND_2, soit ecrire_in_1 et ecrire_in_2. Ces
fonctions reçoivent un paramètre qui contient la valeur que l’on

Programmation en C++ IFT-19965 85


C
++

désire affecter à la variable membre. Les fonctions ne


retournent aucune valeur et ont donc un type de valeur de
retour void. Il faut aussi noter que lorsqu’on change une
valeur d’entrée d’une porte NAND, il faut également voir à ce
que la sortie soit correcte, ce qui explique l’utilisation de la
fonction membre set_out_1 dans chacune des fonctions
d’écriture des variables membres d’entrée in_1 et in_2.
Finalement, nous n’avons pas inclus une fonction membre
d’écriture pour la variable membre out_1 parce que cela n’est
pas logique d’affecter une valeur arbitraire à la sortie d’une
porte NAND sans égard aux valeurs d’entrée. Il serait par
exemple illogique d’affecter la valeur logique “1” à la sortie
out_1 d’une porte NAND si les deux entrées sont à “1”
également, ce qui contredit la table de vérité de la porte.

163 Une raison intéressante d’utiliser des fonctions membres


d’écriture est qu’il est possible d’ajouter de l’information à
l’objet mais également d’ajouter des opérations en même
temps. Par exemple, si on désire que la fonction membre
d’écriture informe l’usager des affectations à une variable
membre, on peut par exemple inclure un message dans le corps
de la fonction membre d’écriture. Par exemple, on pourrait
ajouter le message suivant à la fonction d’écriture de la
variable in_1 de la classe NAND_2:
void ecrire_in_1(int val) {
cout << “Ecriture d’une valeur dans in_1” << endl;
in_1 = val;
out_1 = set_out_1();
}
Une implantation plus robuste vérifierait que la variable val
peut prendre uniquement les valeurs 0 ou 1 en utilisant les
énoncés conditionnels et les opérateurs logiques dont nous
traiterons au CHAPITRE 19 et CHAPITRE 20 respectivement.

164 Un autre attrait des fonctions membres d’écriture est qu’il est
possible d’affecter des variables membres fictives d’un objet.
Les variables membres réelles sont en fait calculées à partir
des valeurs des variables fictives. Par exemple, on pourrait être
intéressés à affecter une valeur au complément (appelé ici
in_1_not) de la valeur de la variable membre d’entrée in_1

86 Programmation en C++ IFT-19965


C
++

d’un objet de la classe NAND_2. Il suffit simplement de créer la


fonction membre d’écriture suivante et de l’inclure à la
définition de la classe:
void ecrire_in_1_not(int val) {
in_1 = 1 - val;
out_1 = set_out_1();
}

165 L’utilisation de lire_ et écrire_ dans les fonctions membres


de lecture et d’écriture est une convention personnelle
permettant de faciliter l’examen du code. Ce n’est pas une règle
formelle du C++.

Exercice
166 Ecrivez des fonctions membres de lecture et d’écriture pour la
classe NOR_2

Résumé
167 ✔ Les fonctions membres de lecture et d’écriture sont une
approche indirecte d’accès aux variables membres des objets
d’une classe.
✔ On peut définir des fonctions membres de lecture ou
d’écriture pour des variables membres réelles ou fictives.

Programmation en C++ IFT-19965 87


C
++

88 Programmation en C++ IFT-19965


C
++

CHAPITRE 13 Comment tirer profit de


l’abstraction des
données

168 Dans ce chapitre, on justifie l’intérêt d’utiliser les


constructeurs, fonctions membres d’écriture et fonctions
membres de lecture parce que ces outils permettent de faire ce
que l’on appelle généralement de l’abstraction des données1,
accroissant ainsi la facilité avec laquelle les programmes
peuvent être modifiés et maintenus en bon état de fonctionner.

169 En général, les fonctions membres d’écriture et de lecture et les


constructeurs isolent le programmeur des détails
d’implantation des classes. Une fois la classe bien conçue, le
programmeur peut se concentrer sur la tâche à accomplir avec
cette classe.

170 Les fonctions membres d’écriture et de lecture et les


constructeurs sont souvent appelés fonctions d’accès parce
qu’elles permettent justement d’accéder indirectement aux
variables membres d’une classe.

1. “data abstraction” en anglais

Programmation en C++ IFT-19965 89


C
++

171 Lorsqu’on utilise l’abstraction des données, on rend l’utilisation


du code plus universelle et on facilite le transfert de code entre
les usagers. On peut par exemple créer un répertoire de classes
et un autre utilisateur peut exploiter ce répertoire (en faisant
usage des fonctions membres d’écriture et de lecture et des
constructeurs) sans trop connaître les détails d’implantation de
la classe et les spécificités de manipulation des données
internes de la classe.

172 La décision de définir des fonctions d’accès pour les variables


membres d’une classe est un choix de désign que doit faire le
programmeur. Parfois, ce choix est dicté par de simples
considérations personnelles reliées au style de programmation.
Dans d’autres situations, c’est la logique qui dicte ce choix.
Rappelons-nous l’exemple de la classe NAND_2 pour laquelle il
est illogique de définir une fonction membre d’écriture pour la
variable out_1 étant donné qu’elle dépend des variables
d’entrée.

Résumé
173 ✔ Les constructeurs et les fonctions membres d’écriture et de
lecture sont appelées fonctions d’accès.
✔ Ces fonctions d’accès permettent de faire de l’abstraction de
données.
✔ Les avantages de l’abstraction de données sont: de faciliter la
réutilisation du code, de faciliter la lecture du code, de
permettre de modifier une classe sans avoir à modifier le
programme qui l’utilise et, enfin, de permettre d’améliorer la
façon dont les données d’une classe sont manipulées.

90 Programmation en C++ IFT-19965


C
++

CHAPITRE 14 La protection contre les


accès accidentels aux
membres des classes

174 Au CHAPITRE 12, nous avons appris à initialiser les objets via
les constructeurs et à aiguiller l’accès aux variables membres
des classes en définissant des fonctions membres d’accès à
l’intérieur même des classes. Dans ce chapitre, nous décrivons
comment on peut s’assurer que les accès aux variables
membres (affectation et lecture) sont canalisés uniquement à
travers de telles fonctions.

175 Dans la classe NAND_2, nous avons vu qu’il est illogique


d’affecter une valeur à la variable de sortie out_1 parce que
celle-ci dépend des entrées. Ainsi, il serait très dangereux
d’écrire l’instruction suivante pour affecter une valeur à la
variable membre out_1 de l’objet nnd:
nnd.out_1 = 1;

Un tel énoncé peut toujours être écrit par inattention même s’il
n’existe pas de fonction membre d’écriture pour la variable
out_1. Cela tient du fait que la variable membre out_1
appartient à la partie publique de la classe (via le mot clé
public: dont nous ne nous sommes pas souciés jusqu’ici).

Programmation en C++ IFT-19965 91


C
++

176 Le C++ prévoit heureusement un mécanisme de privatisation


des membres d’une classe qui les protège complètement des
accès potentiellement dangereux. Cette privatisation des
membres d’une classe se fait en définissant une partie privée
dans la classe avec le mot réservé private:.

177 Voyons comment la classe NAND_2 peut être modifiée pour


utiliser la privatisation de la variable out_1.
//&% nand_ch_14_1.h
class
NAND_2 {

private:

int out_1; /*sortie */

int set_out_1() {
return 1 - (in_1 * in_2);
}

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut


NAND_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

int lire_in_2() {

92 Programmation en C++ IFT-19965


C
++

return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}

};

On remarque que la définition de la classe NAND_2 possède


maintenant deux parties distinctes: une partie comprise entre
le mot réservé private: et le mot réservé public:. La partie
private: contient une variable membre, out_1, et une
fonction membre, set_out_1, que l’on désire garder privées,
c’est-à-dire inaccessibles via l’opérateur d’accès aux membres
“.”. La seconde partie de la classe se situe après le mot réservé
public: et contient les fonctions d’accès, le constructeur
défaut et le constructeur avec arguments.

178 Si maintenant on essaie d’accéder à la variable membre out_1


via l’opérateur d’accès aux membres “.”, voyons ce qui se
produit lors de la compilation du petit programme suivant:

//&% nand_ch_14_1.C
#include <iostream.h>

class
NAND_2 {

private:

int out_1; /*sortie */

int set_out_1() {
return 1 - (in_1 * in_2);
}

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

Programmation en C++ IFT-19965 93


C
++

//Constructeur par defaut


NAND_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}

};

void main() {

NAND_2 nnd;

cout << nnd.in_1 << endl;


cout << nnd.in_2 << endl;
cout << nnd.out_1 << endl;

Résultat
CC nand_ch_14_1.C -o nand_ch_14_1

94 Programmation en C++ IFT-19965


C
++

“nand_ch_14_1.C”, line 63: error: main() cannot


access NAND_2::out_1: private member
Compilation failed

On remarque que le compilateur refuse de compiler le


programme parce qu’une instruction tente d’accéder à la
variable membre out_1 que nous avons maintenant définie
comme faisant partie de la section privée de la classe NAND_2.

179 Par contre, si, comme dans le programme suivant, on tente


d’accéder au contenu de la variable out_1 via la fonction
membre de lecture lire_out_1, le compilateur ne refuse pas
l’instruction parce que la fonction membre lire_out_1 est
publique, donc visible de l’extérieur de la classe avec
l’opérateur d’accès aux membres “.”:
//&% nand_ch_14_2.C
#include <iostream.h>

class
NAND_2 {

private:

int out_1; /*sortie */

int set_out_1() {
return 1 - (in_1 * in_2);
}

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut


NAND_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();

Programmation en C++ IFT-19965 95


C
++

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}

};

void main() {

NAND_2 nnd;

cout << “in_1: “ << nnd.in_1 << endl;


cout << “in_2: “ << nnd.in_2 << endl;
cout << “out_1: “ << nnd.lire_out_1() << endl;

Résultat
in_1: 0
in_2: 0
out_1: 1

96 Programmation en C++ IFT-19965


C
++

180 Les variables membres et les fonctions membres situées dans


la partie publique de la définition de la classe forment ce que
l’on appelle l’interface publique de la classe. Une fois que la
variable out_1 a été placée dans la partie privée de la classe,
elle n’est plus accessible que via cette interface publique.

181 La fonction membre set_out_1 faisant partie de la partie


privée de la classe, elle est accessible des autres fonctions
membres publiques ou privées mais n’est pas accessible via
l’opérateur d’accès aux membres puisqu’elle n’est pas dans
l’interface publique.

182 L’ordre dans lequel les parties publiques et privées sont


déclarées n’est pas important. Ce détail est laissé à la
discrétion du programmeur. Il ne faut cependant pas oublier
que si aucun mot réservé public: ou private: n’est utilisé
dans la définition d’une classe, le compilateur C++ considère
par défaut que tous les membres de la classe sont privés.

Notion avancée
183 Il faut toujours que le constructeur défaut et les
constructeurs avec arguments soient placés dans la partie
publique de la classe. Autrement, le compilateur n’a pas accès
à ces fonctions membres spéciales lors de la création d’un objet
de la classe!

Notion avancée
184 La notation UML et la programmation orientée objet. Il existe
présentement un intérêt croissant pour une méthode graphique
de représentation des classes (ou des abstractions) en
programmation orientée objet. Cette méthode graphique est en
fait un langage formé de symboles simples qui permettent de
saisir d’un coup d’oeil la composition d’une classe et des
relations entre les classes. Ce langage s’appelle UML pour
Unified Modeling Language et a été mis au point par Booch,
Jacobsen et Rumbaugh, trois chercheurs dont les travaux
portent sur la conception d’applications en langage orienté
objet. Dans la notation UML, une classe est représentée par un
rectangle. Le rectangle est divisé en trois parties. La partie du
haut renferme le nom de la classe. La partie du centre contient

Programmation en C++ IFT-19965 97


++ C
les attributs de la classe (i.e. la liste des variables membres
avec leur type et leur statut (private, protected ou public). La
troisième partie contient les opérations que permet cette classe
sur ses attributs, c’est-à-dire la liste des fonctions membres,
leur statut (private, protected, public), et leur prototype si cela
s’avère nécessaire. La figure ci-dessous montre un exemple de
représentation graphique d’une classe en UML.

Nom de la classe

Liste des
attributs

Liste des
fonctions
membres

Exercice
185 Redéfinissez la classe NOR_2 pour que la variable out_1 et la
fonction membre set_out_1 soient situées dans la partie
privée de la classe.

Résumé
186 ✔ L’accès à une variable membre par erreur via l’opérateur
d’accès aux membres détruit souvent les efforts d’abstraction
des données.
✔ On peut empêcher ces accès en plaçant les membres dans la
partie privée d’une classe et en créant une interface publique
adéquate.
✔ Les fonctions membres en général, les constructeurs,
fonctions membres d’écriture et de lecture plus
particulièrement, ont accès aux variables membres, qu’elles
soient publiques ou privées.
✔ Les variables et fonctions membres situées dans la partie
publique de la classe forment l’interface publique de la classe.
✔ L’opposition public: - private: permet d’implanter
l’abstraction des données de façon plus systématique.

98 Programmation en C++ IFT-19965


C
++

CHAPITRE 15 Les instructions de


préprocesseur

187 Jusqu’à maintenant, nous avons toujours inclus le code des


définitions des classes à l’intérieur du fichier contenant le
programme principal main. Une telle pratique devient
cependant très lourde quand le programme principal utilise de
nombreuses définitions de classes: le fichier du programme
principal devient très long, ce qui rend sa consultation
fastidieuse. Heureusement, C++ offre des outils qui permettent
d’alléger considérablement la manipulation du code. Dans ce
chapitre, nous verrons comment les instructions de
préprocesseur peuvent être utilisées pour faciliter l’écriture du
code en C++.

188 Lors de la compilation d’un programme en C++, une étape dite


de “preprocessing” est effectuée avant que la compilation ne
commence à proprement parler. C’est durant cette étape de
preprocessing que le code contenu dans les fichiers d’inclusion
(d’extension .h), comme par exemple iostream.h, est copié
dans le fichier dans lequel l’instruction de préprocessing
#include <iostream.h> apparaît. Le symbole # au début
d’une ligne informe le compilateur C++ que l’instruction qui
suit en est une qui s’adresse au préprocesseur.

Programmation en C++ IFT-19965 99


C
++

Notion avancée
189 L’option -e du compilateur permet de voir quelles seront les
traitements qui seront effectués sans que ces traitements ne
s’effectuent à proprement parler.

190 Par exemple, l’instruction:


#include <iostream.h>

informe le préprocesseur que les instructions contenues dans le


fichier d’inclusion iostream.h doivent être copiées dans le
fichier courant. Cela évite donc au programmeur la tâche
pénible de taper toutes les instructions du fichier iostream.h
dans son propre fichier. Les curieux peuvent consulter le
contenu de iostream.h pour se rendre compte qu’il serait
extrêmement pénible d’en recopier le contenu.

191 L’instruction de préprocessing include n’est pas réservée


exclusivement au fichier iostream.h. Un programmeur peut
donc en tirer profit pour se faciliter la tâche. Il lui suffit de
créer des fichiers d’inclusion qu’il peut par la suite recopier
automatiquement à l’endroit qu’il le désire grâce à l’instruction
include. Supposons par exemple qu’un programmeur ait
défini une classe ayant pour nom C_A et une autre classe
appelée C_B. Il place le code de ces définitions dans le fichier
appelé C_A.h:
//&% C_A.h
#include <iostream.h>
class C_A {
public:
int n_1;

C_A() {
cout << “Constructeur classe C_A” << endl;
}
};

Il est nécessaire d’inclure la librairie iostream.h parce que la


classe contient un énoncé avec cout. Pour le fichier C_B.h on
a:
//&% C_B.h
#include <iostream.h>

100 Programmation en C++ IFT-19965


C
++

class C_B {
public:
int n_1;

C_B() {
cout << “Constructeur classe C_B” << endl;
}
};

Il est nécessaire d’inclure la librairie iostream.h parce que la


classe contient un énoncé avec cout. Si l’usager désire
maintenant créer un programme progr.C utilisant ces deux
classes, il n’a plus qu’à écrire le programme suivant et à le
compiler:
//&% progr.C

#include <iostream.h>
#include “C_A.h”
#include “C_B.h”

void main() {

C_A objet_1;
C_B objet_2;

cout << “Execution du programme progr.C”


<< endl;
}
Lors de la compilation le contenu des fichiers C_A.h et C_B.h
sera automatiquement inclus au contenu de progr.C et le
compilateur reconnaîtra les noms des classes C_A et C_B
lorsqu’il rencontrera la définition des objets objet_1 et
objet_2.
On peut formuler les deux remarques suivantes sur le code ci-
dessus:
1. avec include, on peut utiliser soit < > ou “ “ pour indiquer
le nom du fichier d’inclusion. Les symboles < > sont utilisés
lorsqu’on désire que le compilateur cherche lui-même le
répertoire où se trouve le fichier d’inclusion dans la liste des
répertoires (path) de recherche spécifiés dans les variables
d’environnement de l’usager1. Les symboles “ “ sont utilisés
lorsqu’on désire inclure un fichier situé dans le même réper-
toire que le programme qui est compilé.

Programmation en C++ IFT-19965 101


C
++

2. On pourrait penser que iostream.h est inclus trois fois


dans le fichier progr.C. En effet, on retrouve l’instruction:
#include <iostream.h>
dans progr.C, C_A.h et C_B.h. Il ne faut cependant pas
inclure plusieurs fois ce fichier puisqu’alors le compilateur
rencontrerait plusieurs fois les mêmes identificateurs, ce qui
mènerait à des erreurs de compilation. En fait, le fichier
iostream.h utilise un mécanisme permettant d’éviter
l’inclusion multiple de son contenu. Ce mécanisme repose
sur les instructions de préprocesseur #ifdef, #ifndef,
#define et #endif et sur ce qu’on appelle des drapeaux1.
Nous allons maintenant voir comment utiliser ces outils avec
les fichiers d’inclusion C_A.h et C_B.h. Le fichier
iostream.h en fait déjà usage sans que le programmeur
n’ait à s’en soucier.

192 Reprenons le fichier d’inclusion contenant la définition de la


classe C_A et ajoutons lui quelques lignes contenant des
instructions de préprocesseur et des drapeaux:
//&% C_A.h

#include <iostream.h>

#ifndef C_A_FLAG_
#define C_A_FLAG_

class C_A {
public:
int n_1;

C_A() {
cout << “Constructeur classe C_A” << endl;
}
};

#endif C_A_FLAG_

1. Voir un manuel de référence sur UNIX pour plus de renseignements sur les variables d’envi-
ronnement.
1. “flag” en anglais

102 Programmation en C++ IFT-19965


C
++

L’identificateur C_A_FLAG_, défini par l’usager, est ce que l’on


appelle un drapeau. L’instruction:
#ifndef C_A_FLAG_

demande au préprocesseur du C++ si le drapeau C_A_FLAG_


n’a pas déjà été défini (ifndef est l’abréviation de
ifnotdefined. S’il n’a pas été défini, cette instruction
demande au préprocesseur d’inclure les opérations qui suivent
jusqu’à ce que l’instruction #endif soit rencontrée (le #endif
en moins). L’instruction qui suit, soit #define C_A_FLAG_,
définit le drapeau C_A_FLAG_. Ainsi, si le drapeau C_A_FLAG_
avait déjà été défini ailleurs (par exemple dans un autre
fichier), le préprocesseur évitera d’inclure les instructions qui
suivent puisque le ifndef sera faux.

193 Alors, si par erreur ou par nécessité, le programmeur demande


plusieurs fois l’inclusion d’un fichier de déclaration, le
mécanisme des drapeaux combiné aux instructions de
préprocesseur évitera l’inclusion multiple. Par exemple, si,
dans le programme progr.C, le programmeur à dupliqué par
erreur l’instruction d’inclusion de C_A.h, le compilateur ne
donnera pas de message d’erreur:
//&% progr.C

#include <iostream.h>
#include “C_A.h”
#include “C_A.h”
#include “C_B.h”

void main() {

C_A objet_1;
C_B objet_2;

cout << “Execution du programme progr.C”


<< endl;
}

194 Les instructions de préprocesseur peuvent également être très


utiles lorsque l’on désire déverminer un programme. L’exemple
suivant montre comment les instructions de préprocesseur et

Programmation en C++ IFT-19965 103


C
++

les drapeaux peuvent être utilisés à cette fin. Soit le


programme suivant:
//&% debug_flag.C
#include <iostream.h>

void main() {

//#define DEBUG_
#ifdef DEBUG_

cout << “Ce message n’apparait que lorsque” << endl


<< “le drapeau DEBUG_ est defini” << endl;
#endif DEBUG_
#undef DEBUG_

cout << “Seule cette ligne s’imprime lorsque” << endl


<< “le drapeau DEBUG_ n’est pas defini” << endl;

#ifdef DEBUG_
cout << “Ce message n’apparait que lorsque” << endl
<< “le drapeau DEBUG_ est defini” << endl;
#endif DEBUG_

La sortie du programme est la suivante:

Résultat
Seule cette ligne s’imprime lorsque
le drapeau DEBUG_ n’est pas defini

Supposons maintenant que l’on retire les symboles de


commentaires de la ligne //#define DEBUG_:
//&% debug_flag.C
#include <iostream.h>

void main() {

#define DEBUG_
#ifdef DEBUG_

cout << “Ce message n’apparait que lorsque” << endl


<< “le drapeau DEBUG_ est defini” << endl;

104 Programmation en C++ IFT-19965


C
++

#endif DEBUG_
#undef DEBUG_

cout << “Seule cette ligne s’imprime lorsque” << endl


<< “le drapeau DEBUG_ n’est pas defini” << endl;

#ifdef DEBUG_

cout << “Ce message n’apparait que lorsque” << endl


<< “le drapeau DEBUG_ est defini” << endl;

#endif DEBUG_

La sortie devient alors:

Résultat
Ce message n’apparait que lorsque
le drapeau DEBUG_ est defini
Seule cette ligne s’imprime lorsque
le drapeau DEBUG_ n’est pas defini

Comme l’instruction de préprocesseur demande la définition


d’un drapeau DEBUG_, celui-ci est défini et l’instruction #ifdef
(ifdefined) demande au préprocesseur de considérer tout ce
qui suit jusqu’à l’instruction #endif. Ensuite l’instruction de
préprocesseur #undef (undefine) demande d’annuler la
définition du drapeau DEBUG_, ce qui fait que le second #ifdef
n’est pas vérifié et que ce qui apparaît jusqu’au #endif suivant
n’est pas pris en compte par le compilateur.
On remarque qu’il est donc possible d’utiliser les instructions
de préprocesseur pour faciliter le déverminage des
programmes.

195 Dans les chapitres qui suivent, nous allons utiliser ces outils de
preprocessing pour éviter d’avoir à inclure textuellement les
définitions de classes dans les programmes.

Programmation en C++ IFT-19965 105


C
++

106 Programmation en C++ IFT-19965


C
++

CHAPITRE 16 La notion d’héritage en


C++

196 Dans ce chapitre, nous apprenons comment on peut relier des


classes en une hiérarchie telle que des variables membres
définies dans une classe apparaissent automatiquement dans
des objets appartenant à une autre classe. Ce concept
d’héritage1 est un concept clé qui différencie les langages de
programmation orientée objet des langages traditionnels.

197 Jusqu’à maintenant, nous avons travaillé avec deux classes


principales, soit la classe NAND_2 et la classe NOR_2. Supposons
maintenant, que nous voulions ajouter de l’information
commune aux deux classes. On sait par exemple que les portes
logiques sont des circuits numériques ayant un nombre
maximum de portes qui peuvent être connectées à une sortie.
Ce paramètre, appelé fan-out, est une caractéristique
commune à tous les types de portes même si sa valeur
numérique peut changer pour deux types de portes différentes.
Une façon d’ajouter cette information commune serait par
exemple d’inclure une nouvelle variable membre appelée
fan_out aux classes NAND_2 et NOR_2 et d’accompagner cet
ajout des fonctions membres d’écriture-lecture pour cette

1. “inheritance” en anglais

Programmation en C++ IFT-19965 107


C
++

variable. Cela implique cependant de nombreuses opérations


pouvant mener à des erreurs. Si, de plus, on veut traiter
plusieurs types de portes comme les portes AND, OR, XOR,
NOT, etc, il faut ajouter la variable fan_out et ses fonctions
d’écriture-lecture à chaque classe décrivant chaque porte. Il ne
faut pas non plus oublier d’inclure l’initialisation de cette
nouvelle variable membre dans le constructeur de chaque
classe.

198 Maintenir plusieurs copies d’une même variable membre ou


d’une même fonction membre dans la définition de plusieurs
classes est une mauvaise approche de programmation orientée
objet qui rend le déverminage fastidieux.

199 Heureusement, le C++, via le concept d’héritage, encourage les


concepteurs à éviter la duplication inutile de variables
membres ou de fonctions membres communes à plusieurs
classes. L’organisation des classes en une hiérarchie, via le
processus d’héritage du C++, rend le développement de code
plus simple et plus facile à modifier.

200 Pour l’exemple des portes logiques, on peut par exemple dire
que les portes NAND et NOR sont des composantes
numériques d’un circuit. Les composantes numériques ont
une caractéristique commune soit le fan_out. Les circuits ont
des caractéristiques communes telles les tensions
d’alimentation numérique positive et négative, la tension
d’alimentation analogique et un numéro identifiant chaque
pièce montée sur la plaquette du circuit. Evidemment, toutes
les pièces d’un circuit portent un numéro différent. Cependant,
le fait de posséder un numéro est une caractéristique partagée
par toutes les composantes, qu’elles soient des portes logiques,
des résistances, des condensateurs ou des amplificateurs. Le
fait de posséder un fan_out est une caractéristique partagée
par les composantes numériques d’un circuit mais non pas par
les composantes analogiques comme les résistances par
exemple. Il faut donc organiser les catégories pour qu’elles
contiennent des caractéristiques pertinentes aux objets qu’elles
servent à décrire.

108 Programmation en C++ IFT-19965


C
++

201 Dans un programme complexe, un bon moyen d’organiser les


catégories est de dessiner un diagramme hiérarchique. La
FIGURE 3 montre un diagramme hiérarchique contenant les
catégories susceptibles de se retrouver dans un circuit
électronique. Dans chaque catégorie, nous avons indiqué les
FIGURE 3 Hiérarchie possible pour un circuit électronique

CIRCUIT
Description
v_num_plus Tension d’alim. numérique pos.
v_num_moins Tension d’alim. numérique nég.
v_analog_plus Tension d’alim. analogique pos.
numero Numéro d’une composante

NUMÉRIQUE
Variables membres Description
fan_out Nb. max sorties. ANALOGIQUE
Variables membres
puissance_max puissance max
dissipée.
type type de composante
NAND_2 NOR_2 (active ou passive).

in_1 entrée in_1 entrée


in_2 entrée in_2 entrée
out_1 sortie out_1 sortie

AMPLIFICATEUR RESISTANCE CONDENSATEUR


Var. mem. Desc. Var. mem.
valeur Desc.
valeur de la Var. mem. valeur
capacite Desc. du
gain gain de résistance condensateur
l’ampli. tolerance en % v_claquage en volts

paramètres se rattachant à la catégorie. Dans le diagramme de


la FIGURE 3, la classe CIRCUIT est appelée superclasse
parce qu’elle se trouve au sommet de la hiérarchie. Cette classe
contient de l’information très générale qui peut faire partie de

Programmation en C++ IFT-19965 109


C
++

la description de chaque pièce. Les classes NUMERIQUE et


ANALOGIQUE sont ensuite des sous-classes de la superclasse
CIRCUIT. Ces sous-classes contiennent des informations
moins générales que la classe CIRCUIT et ces informations
servent à décrire une quantité plus restreinte d’individus.
Finalement, au bas de la hiérarchie, nous avons les sous-
classes NAND_2, NOR_2, RESISTANCE, CONDENSATEUR, et
AMPLIFICATEUR qui décrivent des individus très spécifiques et
fondamentalement différents entre eux.
Les objets d’une classe de cette hiérarchie héritent des
variables membres de la classe à laquelle ils appartiennent et
de toutes les variables des superclasses de cette classe.
Par exemple, chaque objet de la classe RESISTANCE possède sa
propre copie des variables membres valeur et tolerance
mais également une copie des variables membres des
superclasses ANALOGIQUE et CIRCUIT soit puissance_max,
type et v_num_plus, v_num_moins, v_analog_plus, et
numero. Il faut noter que ANALOGIQUE est une sous-classe de
CIRCUIT mais est une superclasse pour RESISTANCE et
CONDENSATEUR.

202 En règle générale, on devrait placer les variables membres et


les fonctions membres dans les classes d’une hiérarchie qui
satisfont les deux critères suivants:
1. il n’y a pas de duplication inutile de variables membres
2. chaque variable membre et chaque fonction membre est utile
dans les sous-classes.

203 Une fois que nous avons défini une hiérarchie, il ne reste plus
qu’à définir les classes. Nous commençons par les classes
supérieures de la hiérarchie et nous descendons graduellement
vers le bas de celle-ci. La superclasse CIRCUIT est simple à
définir:
//&% circuit_15_1.h
#include <iostream.h>

#ifndef CIRCUIT_FLAG_
#define CIRCUIT_FLAG_

class CIRCUIT {

110 Programmation en C++ IFT-19965


C
++

private:
double v_num_plus; //Tension numerique positive (en volts)
double v_num_moins; //Tension numerique negative (en
volts)
double v_analog_plus; //Tension analogique positive (en
volts)
int numero; //Numero du composant

public:

// Constructeur par defaut


CIRCUIT () {
cout << “Appel du constructeur defaut de CIRCUIT” << endl;
v_num_plus = 5.0;
v_num_moins = 0.0;
v_analog_plus = 12.0;
numero = 0;
}

// Constructeur avec initialisation des variables membres


CIRCUIT (double v_n_p, double v_n_m, double v_a_p, int no)
{
cout << “Creation d’un objet CIRCUIT” << endl;
v_num_plus = v_n_p;
v_num_moins = v_n_m;
v_analog_plus = v_a_p;
numero = no;
}

// Reader de v_num_plus

double read_v_num_plus() {
return v_num_plus;
}

// Reader de v_num_moins

double read_v_num_moins () {
return v_num_moins;
}

// Reader de v_analog_plus

double read_v_analog_plus () {
return v_analog_plus;
}

// Reader de numero

double read_numero () {

Programmation en C++ IFT-19965 111


C
++

return numero;
}

// Writer de v_num_plus

void set_v_num_plus(double v) {
v_num_plus = v;
}

// Writer de v_num_moins

void set_v_num_moins(double v) {
v_num_moins = v;
}

// Writer de v_analog_plus

void set_v_analog_plus(double v) {
v_analog_plus = v;
}

// Writer de numero

void set_numero(int n) {
numero = n;
}
};

#endif CIRCUIT_FLAG_

Par ailleurs, la classe NUMERIQUE possède une définition très


simple:
//&% numerique_15_1.h
#include <iostream.h>
#include “circuit_15_1.h”

#ifndef NUMERIQUE_FLAG_
#define NUMERIQUE_FLAG_

class NUMERIQUE : public CIRCUIT {


private:
int fan_out; //Nombre admissible de sorties

public:

// Constructeur par defaut


NUMERIQUE () {

112 Programmation en C++ IFT-19965


C
++

cout << “Appel du constructeur defaut de NUMERIQUE” <<


endl;
fan_out = 10;
}

// Reader de fan_out
int read_fan_out() {
return fan_out;
}

// Writer de fan_out
void set_fan_out(int f_out) {
fan_out = f_out;
}
};

#endif NUMERIQUE_FLAG_

Nous sommes déjà familiers avec les classes NAND_2 et NOR_2


que nous modifions ici pour tenir compte de l’héritage. Pour la
classe NAND_2, nous avons:
//&% nand_2_15_1.h

#include <iostream.h>
#include “numerique_15_1.h”

#ifndef NAND_FLAG_
#define NAND_FLAG_

class NAND_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1() {
return 1 - (in_1 * in_2);
}

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut


NAND_2() {
cout<< “Appel du constructeur defaut de NAND_2”
<< endl;
in_1 = 0; in_2 = 0;
out_1 = set_out_1();

Programmation en C++ IFT-19965 113


C
++

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}
};

#endif NAND_FLAG_

et, pour la classe NOR_2:


//&% nor_2_15_1.h

#include <iostream.h>
#include “numerique_15_1.h”

#ifndef NOR_FLAG_
#define NOR_FLAG_

class NOR_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1() {
return ((1-in_1) * (1-in_2));

114 Programmation en C++ IFT-19965


C
++

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut


NOR_2() {
cout<< “Appel du constructeur defaut de NOR_2”
<< endl;
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NOR_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}
};

#endif NOR_FLAG_

Les définitions précédentes nous permettent de formuler


plusieurs commentaires sur la syntaxe permettant de créer une

Programmation en C++ IFT-19965 115


C
++

hiérarchie de classes en C++. On remarque que pour qu’une


sous-classe hérite d’une superclasse, comme par exemple la
classe NUMERIQUE qui hérite de la superclasse CIRCUIT, il
suffit simplement d’adopter la syntaxe suivante:
class NUMERIQUE : public CIRCUIT { ...};

et, de façon générale, la syntaxe est la suivante:


class NOM_CLASSE : public NOM_SUPERCLASSE { ...};

Si une sous-classe hérite de deux superclasses différentes, la


syntaxe générale est simplement:
class NOM_CLASSE : public NOM_SUPERCLASSE_1, public
NOM_SUPERCLASSE_2 { ...};

Pour spécifier qu’une classe hérite de superclasses, il suffit


donc simplement, après le nom de la classe, d’ajouter le nom
des superclasses desquelles la classe hérite en les précédant du
mot réservé public et du caractère “:”. Les noms des
superclasses sont séparés par des “,”.

204 Dans l’exemple qui précède, comme la classe NUMERIQUE est


directement sous la classe CIRCUIT, on dit que NUMERIQUE est
une classe dérivée1 de la classe de base2 CIRCUIT.
Remarquez qu’il faut toujours définir une classe de base
avant de définir une classe dérivée de cette classe.

205 Il faut remarquer que la classe NAND_2 et la classe NOR_2


héritent non seulement des variables membres et des fonctions
membres de la classe NUMERIQUE mais également des variables
membres et des fonctions membres de la classe CIRCUIT
puisque la classe NUMERIQUE hérite elle-même de la classe
CIRCUIT.

206 Lorsqu’un objet de la classe NAND_2 est créé, son constructeur


(par défaut ou avec paramètres selon la syntaxe adoptée) est

1. “derived class” en anglais


2. “base class” en anglais

116 Programmation en C++ IFT-19965


C
++

appelé et le constructeur par défaut des superclasses


NUMERIQUE et CIRCUIT est appelé automatiquement par le
C++. Nous allons donner ci-dessous un exemple de programme
montrant comment se comporte la définition de classes dans
une hiérarchie.
//&% test_hierarchie.C

#include <iostream.h>
#include “nand_2_15_1.h”
#include “nor_2_15_1.h”

void main() {

NAND_2 nnd;
NOR_2 nrd;

cout << “nand, tension analogique: “


<< nnd.read_v_analog_plus() << endl;

Résultat
Appel du constructeur defaut de CIRCUIT
Appel du constructeur defaut de NUMERIQUE
Appel du constructeur defaut de NAND_2
Appel du constructeur defaut de CIRCUIT
Appel du constructeur defaut de NUMERIQUE
Appel du constructeur defaut de NOR_2
nand, tension analogique: 12

Le programme définit deux objets, un objet de type NAND_2 et


un autre de type NOR_2. On remarque que comme ces deux
objets héritent des classes CIRCUIT et NUMERIQUE, les
constructeurs défaut de ces classes sont appelés en plus des
constructeurs spécifiques à chaque objet. On réalise ici
l’importance des constructeurs par défaut: ils sont appelés
automatiquement lors de la création d’objets descendant d’une
superclasse. On remarque aussi comment les instructions de
préprocesseur et les drapeaux rendent l’écriture d’un
programme beaucoup plus simple en évitant d’avoir à inclure le

Programmation en C++ IFT-19965 117


C
++

code des définitions des classes. Le préprocesseur se charge


automatiquement de cette tâche.
On voit finalement que comme la classe NAND_2 hérite de la
classe NUMERIQUE qui hérite elle-même de la classe CIRCUIT,
la variable membre v_analog_plus est accessible via la
fonction membre d’accès read_v_analog_plus().

207 Il faut finalement mentionner que si une fonction d’une classe


masque une fonction de la superclasse dont elle hérite, c’est la
fonction membre de la classe qui à la priorité. L’exemple qui
suit donne une illustration de ce principe:
//&% masque_15.C
#include <iostream.h>
class A {
public:
int var_A_1;
int var_A_2;

A() {
var_A_1 = 2;
var_A_2 = 3;
}

int produit() {
return var_A_1 * var_A_2;
}

};

class B : public A {

public:
int var_B_1;

B() {
var_B_1 = 0;
}

int produit() {
return 10 * var_A_1 * var_A_2;
}

};

void main() {

118 Programmation en C++ IFT-19965


C
++

A objet_A;
B objet_B;

cout << “objet_A.produit(): “ << objet_A.produit() << endl;


cout << “objet_B.produit(): “ << objet_B.produit() << endl;
}

Résultat
objet_A.produit(): 6
objet_B.produit(): 60

On voit bien ici que la fonction produit() de la classe B


masque celle de la classe A. Si on retire la fonction membre
produit() de la classe B, le programme devient:
//&% masque_15.C
#include <iostream.h>
class A {
public:
int var_A_1;
int var_A_2;

A() {
var_A_1 = 2;
var_A_2 = 3;
}

int produit() {
return var_A_1 * var_A_2;
}

};

class B : public A {

public:
intvar_B_1;

B() {
var_B_1 = 0;
}

/* int produit() {
return 10 * var_A_1 * var_A_2;
}

Programmation en C++ IFT-19965 119


C
++

*/
};

void main() {

A objet_A;
B objet_B;

cout << “objet_A.produit(): “ << objet_A.produit() << endl;


cout << “objet_B.produit(): “ << objet_B.produit() << endl;
}

le résultat est:

Résultat
objet_A.produit(): 6
objet_B.produit(): 6

Notion avancée
208 Pour forcer l’utilisation de la fonction produit de A (et non de
B), la syntaxe suivante est permise dans la fonction main():
cout << “objet_B.produit():” << objet_B.A::produit() << endl;

209 Dans les exemples, nous avons vu des relations entre une
classe dérivée et une classe de base dont l’expression de
l’héritage utilisait le mot réservé public :
class nom_classe_derivee : public nom_classe_de_base {...}
L’expression d’un tel héritage est appelée dérivation
publique.

Notion avancée
210 Nous verrons plus loin qu’il est aussi possible de définir des
dérivations protégées1 ou des dérivations privées2.

1. “protected” en anglais
2. “private” en anglais

120 Programmation en C++ IFT-19965


C
++

Notion avancée
211 Dans les exemples précédents, les classes de base avaient
seulement un constructeur par défaut qui était
automatiquement appelé lorsqu’une variable du type d’une
classe dérivée était déclarée. Nous verrons plus loin qu’il est
possible de définir des constructeurs avec paramètres même
pour les classes de base et que ces constructeurs peuvent être
appelés par les constructeurs des classes dérivées lors de la
création d’un objet.

Exercice
212 Définissez une classe NOT décrivant un inverseur logique. Les
variables membres de la classe NOT doivent porter le nom de in
et out respectivement. La classe NOT hérite de la classe
NUMERIQUE qui hérite elle-même de la classe CIRCUIT.
N’oubliez pas d’inclure les constructeurs et les fonctions
membres d’accès à la définition de la classe NOT. Stockez la
définition de la classe NOT dans le fichier not.h et utilisez les
instructions de préprocesseur et les drapeaux pour rendre son
utilisation possible dans un programme.

Résumé
213 ✔ Les hiérarchies reflètent les relations sous-classe -
superclasse entre les classes.
✔ Il est intéressant d’utiliser une hiérarchie parce que cela
permet: de décrire des catégories d’objets réels, d’éviter la
duplication inutile de variables membres, d’éviter d’ajouter des
bugs dans du code déjà déverminé et d’utiliser du code
disponible commercialement.
✔ Un objet hérite non seulement des variables membres et des
fonctions membres de la classe dont il est dérivé mais
également des variables membres et des fonctions membres de
la superclasse de cette classe.
✔ Quand une sous-classe contient des fonctions membres ayant
le même nom que des fonctions membres de sa superclasse, les
fonctions membres de la classe masquent celles de la
superclasse.
✔ Il y a plusieurs types de dérivation. Jusqu’à maintenant,
nous n’avons traité que des dérivations publiques.
✔ Il est recommandé de tracer un diagramme d’une hiérarchie

Programmation en C++ IFT-19965 121


++ C
avant de définir les classes qui permettront de l’implanter en
C++.

122 Programmation en C++ IFT-19965


C
++

CHAPITRE 17 La conception de
hiérarchies de classes

214 Au CHAPITRE 16, nous avons vu comment définir des


hiérarchies de classes. Le présent chapitre énonce quelques
principes fondamentaux pour la conception de telles
hiérarchies.

215 Les principes énoncés ci-dessous se veulent des lignes


directrices qu’il est intéressant de suivre lorsqu’on veut
concevoir des hiérarchies de classes utiles. Aucun de ces
principes ne doit être obligatoirement suivi. Cependant, le fait
de les contourner doit être accompagné d’une raison valable.

216 Le principe de représentation explicite: à chaque fois qu’on


rencontre une catégorie naturelle décrivant des individus de la
vie réelle, il faut définir une classe correspondant à cette
catégorie dans le programme.

217 Le principe de non-duplication: les variables membres et les


fonctions membres devraient être réparties dans la hiérarchie
de façon à ce qu’aucune duplication ne fasse apparaître des
membres inutilement.

Programmation en C++ IFT-19965 123


C
++

218 Le principe de visibilité locale: lorsque des éléments d’un


programme sont voisins à l’écran de l’ordinateur, il est facile
d’en comprendre la signification. Il importe donc de garder
ensemble les éléments qui sont intimement reliés.

219 Le principe de consultation directe: il est préférable


d’accéder à une variable membre représentant une quantité
fréquemment utilisée que de calculer cette quantité
indirectement à partir d’autres variables membres à chaque
fois qu’elle est requise.

220 Le principe de non-divulgation: le concepteur d’une classe


définit souvent plus de variables membres et plus de fonctions
membres qu’un utilisateur n’en a réellement besoin. Il
convient, dans l’interface publique d’une classe de n’offrir aux
utilisateurs de la classe que l’accès aux variables et fonctions
membres nécessaires à son utilisation efficace.

221 Le principe de simplicité: il convient de garder les fonctions


membres le plus simple possible afin d’en faciliter le
déverminage et l’utilisation.

222 Le principe de modularité: il convient de diviser un calcul en


blocs cohérents et de concevoir une solution pour chaque bloc
individuellement.

Notion avancée
223 Le langage graphique UML permet de bien montrer les liens
entre les classes grâce à un ensemble de symboles représentant

124 Programmation en C++ IFT-19965


C
++

des liens d’association, d’héritage (généralisation) et


d’aggrégation. Ces symboles sont montrés ci-dessous.
Association

Aggrégation
(composition)

Généralisation
(héritage)

On utilise ces symboles avec le symbole décrivant les classes


pour illustrer graphiquement les liens entre les classes. Une
association entre deux classes signifie qu’une classe utilise une
autre classe (par exemple comme paramètre d’une de ses
fonctions membres). Une aggrégation entre deux classes
signifie qu’une classe possède une variable membre qui
appartient à une autre classe. Une relation de généralisation
signifie que deux classes sont dans une relation d’héritage.
Pour le code suivant, on aurait le diagramme de classes ci-
dessous (A est composée de C et B hérite de A et C utilise B).
class A {
public:
int i;
C w;
int readI();
}

class B : public A{
public:
int k;
int readK();
}

class C {
public:
double u;
double readU();
void useB(B t);
}

Programmation en C++ IFT-19965 125


C
++

A
int A
C w

int readI()

C
double u

void useB(B t)
B
int k

int readK()

126 Programmation en C++ IFT-19965


C
++

CHAPITRE 18 Les tests utilisant les


prédicats numériques

224 Dans les chapitres qui suivent, nous délaissons


temporairement les classes et les objets pour concentrer nos
efforts sur la façon dont C++ permet d’implanter des tests,
branchements, itérations, récursions, et des manipulations de
tableaux. Le CHAPITRE 18 s’intéresse plus spécialement à la
façon dont les valeurs numériques peuvent être comparées.
Nous verrons qu’à cet égard, le C++ ressemble beaucoup aux
autres langages de programmation.

225 Les fonctions retournant une valeur représentant “vrai1” ou


“faux2” sont appelées prédicats. Le C++ offre plusieurs
prédicats intrinsèques permettant de vérifier la relation entre
une paire de nombres:

1. “true” en anglais
2. “false” en anglais

Programmation en C++ IFT-19965 127


C
++

TABLE 2 Les prédicats intrinsèques du C++

Prédicat Utilité
== Est-ce que les deux nombres sont égaux
!= Est-ce que deux nombres ne sont pas égaux
> Est-ce que le premier nombre est strictement
supérieur au second
< Est-ce que le premier nombre est strictement
inférieur au second
>= Est-ce que le premier nombre est supérieur ou
égal au second
<= Est-ce que le premier nombre est inférieur ou
égal au second

226 La valeur de l’expression 6 == 3, dans laquelle l’opérateur


d’égalité apparaît, est 0, ce qui signifie “faux” en C++.
La valeur de l’expression 6 != 3, dans laquelle l’opérateur
d’inégalité apparaît, est 1, ce qui signifie “vrai” en C++.

227 Une erreur fréquente est d’utiliser machinalement l’opérateur


d’affectation = au lieu de l’opérateur d’égalité == pour vérifier
l’égalité entre deux nombres. N’oubliez pas que l’instruction:
a = b

affecte la valeur de la variable b à la variable a tandis que


l’instruction:
a == b

retourne 1 si la valeur de la variable a est égale à la valeur de


la variable b et 0 si la valeur de la variable a n’est pas égale à
la valeur de la variable b.

228 Nous savons maintenant qu’à chaque fois que le caractère “!”
est immédiatement suivi du caractère “=”, les deux caractères
ensembles représentent l’opérateur d’inégalité.
Il faut noter cependant que le caractère “!” peut apparaître
seul. Dans ce cas, il représente l’opérateur de négation1 en
C++. Par exemple, la valeur de !0 est 1 et la valeur de !1 est

128 Programmation en C++ IFT-19965


C
++

0. Aussi, la valeur de !(6==3) est 1 tandis que la valeur de


!(6!=3) est 0.

229 Pour résumer, le C++ interprète tout entier différent de 0


comme étant vrai. Donc, l’opérateur de négation ! change tout
entier différent de 0 en 0.

230 Remarquez qu’il faut utiliser une opération de casting pour


comparer un entier avec un nombre en notation réelle. Par
exemple, pour comparer un nombre int i avec un nombre
double d, il faut faire:
(float) i == d

il faut cependant noter qu’il est très dangereux de comparer


des nombres en format float pour vérifier leur égalité car les
résultats risquent d’être faux. En effet, les effets d’arrondi
peuvent faire en sorte que deux nombres paraissent inégaux
alors qu’ils le sont en réalité si la précision de l’ordinateur était
infinie. Il est préférable de comparer des nombres float à une
très petite valeur (mais > 0.0). Par exemple:
float petit = 0.0001;
if ((a < petit) && (b < petit)) cout << “a == b”
else cout << “a != b”;

231 Notez également que les opérateurs ==, !=, >, <, >=, <= ont
une priorité inférieure à l’opérateur d’insertion <<. Il faut donc
utiliser des parenthèses comme dans l’exemple suivant:
cout << (i == (int)d) << endl;

Exercice
232 Ecrivez un programme qui accepte un nombre et qui affiche 1 si
le nombre est plus petit que 100 et 0 s’il est plus grand ou égal à
100.

1. “not” en C++

Programmation en C++ IFT-19965 129


++ C
Résumé
233 ✔ Un prédicat est une fonction qui retourne 0 ou 1.
✔ En C++, 0 signifie faux et 1 (ou tout autre entier différent de
0) signifie vrai
✔ Pour comparer deux nombres de types différents, il faut
utiliser le casting

130 Programmation en C++ IFT-19965


C
++

CHAPITRE 19 Les énoncés


conditionnels

234 Dans ce chapitre, nous voyons comment on peut utiliser les


énoncés conditionnels du C++ pour effectuer des calculs dont la
nature dépend de la valeur d’une expression impliquant
éventuellement un ou plusieurs prédicats.

235 Conceptuellement, une expression booléenne est une


expression qui produit la valeur faux ou vrai. En C++, cela
signifie que l’expression produit 0 (associé à faux) ou tout
entier différent de 0 (associé à vrai).

236 Un énoncé if comprend une expression booléenne contenue


entre des parenthèses, suivie d’un énoncé associé:
if (expression booléenne) énoncé associé

Lorsque l’expression booléenne d’un énoncé if produit une


valeur entière différente de 0, C++ considère que cette
expression est vraie et l’énoncé associé est alors exécuté.
Autrement, si l’expression booléenne produit la valeur 0, le
C++ considère cette expression comme étant fausse et l’énoncé
associé n’est pas exécuté.

Programmation en C++ IFT-19965 131


C
++

237 Supposons qu’on désire écrire un programme qui affiche un


message dépendant de la puissance dissipée dans une
résistance. Si la puissance dissipée est plus grande que la
puissance maximum (de 10 watts), on affiche “Puissance
trop élevée”. Si la puissance dissipée est inférieure ou égale
à 8 watts, on affiche “Puissance OK”:
//&% if_19_1.C

#include <iostream.h>

void main() {
int puissance;

cin >> puissance;


if (puissance <= 8) cout << “Puissance OK” << endl;
if (puissance > 10) cout << “Puissance trop elevee” <<
endl;
}

Résultat
3
Puissance OK

238 L’énoncé if-else ressemble à l’énoncé if sauf qu’il possède


un second énoncé associé qui suit le mot else:
if (expression booléenne) énoncé associé si vrai
else
énoncé associé si faux

Le premier énoncé associé est exécuté si l’expression booléenne


est vraie. Autrement, c’est le second énoncé qui est exécuté.

239 L’énoncé associé d’un énoncé if ou les énoncés associés d’un


énoncé if-else peuvent eux-mêmes contenir des énoncés if
ou if-else. L’exemple du paragraphe 237 peut par exemple
s’écrire:
//&% if_19_2.C

132 Programmation en C++ IFT-19965


C
++

#include <iostream.h>

void main() {
int puissance;

cin >> puissance;


if (puissance <= 8) cout << “Puissance OK” << endl;
else
if (puissance>10) cout<<“Puissance trop elevee”<< endl;
}

Résultat
3
Puissance OK

Dans le code qui précède, le programme n’affiche rien si la


puissance est comprise entre 9 et 10 (en notation entière). On
remarque que, dans l’énoncé if-else, l’énoncé associé à la
valeur fausse de l’expression booléenne est lui-même un énoncé
if.

240 La présentation1 des énoncés if ou if-else est laissée à la


discrétion du programmeur. Cependant, il faut s’assurer que la
présentation facilite la compréhension du programme.

241 Supposons qu’on veuille exécuter plus d’un énoncé lorsque


l’expression booléenne d’un énoncé if ou if-else est vraie ou
fausse. Dans ce cas, il suffit de créer un énoncé associé dont les
énoncés sont compris entre crochets:
{
énoncé 1
énoncé 2
énonce 3
...
énoncé n
}

1. i.e. la façon avec laquelle on les dispose dans la page

Programmation en C++ IFT-19965 133


C
++

Dans l’exemple qui suit, on veut non seulement afficher un


message lorsque la puissance dissipée dans une résistance est
trop élevée mais en plus affecter la valeur 1 à une variable pour
enregistrer ce dépassement.
//&% if_19_3.C

#include <iostream.h>

void main() {
int puissance;
int drapeau = 0;

cin >> puissance;


if (puissance > 10) {
drapeau = 1;
cout << “Puissance trop elevee” << endl;
cout << “drapeau: “ << drapeau << endl;
}

Résultat
16
Puissance trop elevee
drapeau: 1

242 Le code qui suit peut poser des problèmes d’interprétation:


//&% if_19_4.C
#include <iostream.h>
void main() {
int puissance;

cin >> puissance;


if (puissance > 10)
if (puissance < 25)
cout << “Puissance OK. “ << endl;
else cout << “Puissance ?. “ << endl;

En effet, par quoi devrait-on remplacer le “?” dans l’énoncé


else? En fait, le C++ assume que chaque else doit être pairé

134 Programmation en C++ IFT-19965


C
++

avec le if le plus près qui n’a pas déjà un else qui lui est
pairé. Donc, ci-dessus, il faudrait remplacer le “?” par “trop
élevée”.
Afin d’éviter toute ambiguïté, il est préférable d’utiliser des
crochets pour mieux délimiter les énoncés if-else.

243 Jusqu’ici, nous avons vu que l’énoncé if-else permet


d’exécuter l’un ou l’autre des deux énoncés associés dépendant
de la valeur de l’expression booléenne. Nous allons maintenant
voir l’opérateur conditionnel du C++. Cet opérateur permet
de calculer une valeur à partir de deux expressions
produisant chacune une valeur. Cet opérateur est
fréquemment utilisé dans les énoncés de sortie où il permet de
distinguer entre le singulier et le pluriel. Considérons par
exemple, le programme suivant:
//&% if_19_5.C

#include <iostream.h>

void main() {
int delta_puissance;

cin >> delta_puissance;

if (delta_puissance == 1)
cout<< “L’augmentation de puissance”
<< “ est de: “ << delta_puissance << “ watt” << endl;
else
cout<< “L’augmentation de puissance”
<< “ est de: “ << delta_puissance << “ watts” << endl;
}

Résultat
1
L’augmentation de puissance est de: 1 watt

Résultat
3

Programmation en C++ IFT-19965 135


C
++

L’augmentation de puissance est de: 3 watts

On voit que l’instruction conditionnelle permet de tenir compte


du singulier et du pluriel dans un programme. Cependant, cet
énoncé est plutôt lourd puisque la seule chose qui distingue les
deux énoncés associés au if-else est la lettre “s” au mot
watt.
Par conséquent, il serait plus élégant de placer le changement
dans un seul énoncé cout incluant une expression produisant
le mot “watt” ou le mot “watts” dépendant de la valeur de
delta_puissance. Le C++ offre cette possibilité.

244 La ligne qui suit est la forme générale adoptée par le C++ pour
l’opérateur conditionnel produisant une valeur:
Expression booléenne ? Expression si vrai: Expression si
faux

Remarquez que, contrairement aux autres opérateurs,


l’opérateur conditionnel possède une syntaxe combinant deux
symboles (soit “?” et “:”) séparant trois expressions. Pour cette
raison, on qualifie l’opérateur conditionnel d’opérateur
ternaire.
Remarquez aussi que seulement une des deux expressions (vrai
ou faux) est évaluée. Toute opération ou affectation dans
l’énoncé non évalué ne sera pas exécutée.

245 Voyons maintenant comment nous pouvons tirer profit de


l’opérateur conditionnel dans le programme du paragraphe
243:
//&% cond_19_6.C

#include <iostream.h>

void main() {
int delta_puissance;

cin >> delta_puissance;


cout << “L’augmentation de puissance est de: “

136 Programmation en C++ IFT-19965


C
++

<< delta_puissance
<< (delta_puissance == 1 ? “ watt” : “ watts”)
<< endl;

Résultat
1
L’augmentation de puissance est de: 1 watt

Résultat
3
L’augmentation de puissance est de: 3 watts

Si la valeur de delta_puissance est de 1 watt, l’opérateur


conditionnel produit la valeur “watt” autrement, il produit la
valeur “watts”. Les parenthèses sont ici importantes puisque
l’opérateur ternaire possède une priorité inférieure à celle de
l’opérateur d’insertion.

Résumé
246 ✔ Le C++ offre des énoncés conditionnels if et if-else
permettant de traiter les données en fonctions du résultat d’un
prédicat logique.
✔ L’utilisation de crochets rend possible l’exécution de
plusieurs énoncés associés à l’énoncé if ou if-else.
✔ L’opérateur conditionnel permet de produire un résultat en
fonction de la valeur d’un prédicat logique.

Programmation en C++ IFT-19965 137


C
++

138 Programmation en C++ IFT-19965


C
++

CHAPITRE 20 La combinaison logique


d’expressions
booléennes

247 Ce chapitre décrit comment les expressions booléennes simples


peuvent être combinées pour former des expressions plus
complexes.

248 L’ opérateur logique ET, symbolisé par &&, retourne la valeur 1


si ses deux opérandes ont une valeur entière différente de 0 et
retourne 0 autrement.

249 L’opérateur OU, symbolisé par ||, retourne la valeur 1 si au


moins une de ses opérandes a une valeur différente de 0 et
retourne 0 autrement.

250 Par conséquent, l’énoncé associé à l’énoncé if est exécuté


seulement si la puissance dissipée dans une résistance est
comprise entre 10 et 20 watts:
//&% if_20_1.C

#include <iostream.h>

void main() {
int puissance;

Programmation en C++ IFT-19965 139


C
++

cin >> puissance;


if (10 < puissance && puissance < 20)
cout << “Puissance normale” << endl;
}

Résultat
15
Puissance normale

251 On remarque que les opérateurs && et || ont une priorité


inférieure aux prédicats logiques (par exemple <).

252 L’évaluation des expressions comprenant && ou || est


compliquée par le fait que certaines sous-expressions peuvent
n’être jamais évaluées. Dans les expressions avec &&,
l’opérande de gauche est évaluée en premier. Si la valeur de
cette opérande de gauche est 0, l’opérande de droite est ignorée
et n’est pas évaluée et la valeur de l’expression && est 0.
Pour les expressions avec ||, l’opérande de gauche est aussi
évaluée en premier. Si la valeur de cette opérande est un entier
différent de 0, aucune autre action n’est prise et la valeur de
l’expression || est 1. Si les deux expressions prennent la
valeur 0, la valeur de l’expression || est aussi 0.

Notion avancée
253 Les opérateurs && et || sont utilisées pour exprimer le ET et le
OU logiques parce que les opérateurs & et | sont utilisés pour
les opérations logiques ET et OU sur les bits individuels des
opérandes. Les opérateurs & et | ne sont pas traités dans ces
notes parce que la compréhension de la manipulation des bits
individuels n’est pas nécessaire à l’apprentissage des
principaux points du C++. Les détails sur ces opérateurs sont
disponible dans tout livre sur le C ou le C++.

254 En général, le C++ ne spécifie pas l’ordre d’évaluation des


opérandes. Les opérateurs && et || font exception à cette règle,

140 Programmation en C++ IFT-19965


C
++

comme nous venons de le voir. L’opérateur conditionnel et


l’opérateur “virgule”, que nous verrons plus loin, font aussi
partie des exceptions.

255 Maintenant que nous avons vu les instructions if et les


opérateurs logiques && et ||, nous pouvons revenir sur la
définition des classes NAND_2 et NOR_2 pour modifier la
fonction membre set_out_1() afin qu’elle tire profit de ces
nouveaux outils plus appropriés à la manipulation des valeurs
logiques. La classe NAND_2 est maintenant la suivante:
//&% nand_2_20_1.h

#include <iostream.h>
#include “numerique_15_1.h”

#ifndef NAND_FLAG_
#define NAND_FLAG_

class NAND_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1() {
if (in_1 && in_2) return 0;
else return 1;
}

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut


NAND_2() {
cout<< “Appel du constructeur defaut de NAND_2”
<< endl;
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

Programmation en C++ IFT-19965 141


C
++

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}
};

#endif NAND_FLAG_

Tandis que la classe NOR_2 devient:


//&% nor_2_20_1.h

#include <iostream.h>
#include “numerique_15_1.h”

#ifndef NOR_FLAG_
#define NOR_FLAG_

class NOR_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1() {
if (in_1 || in_2 ) return 0;
else return 1;
}

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut

142 Programmation en C++ IFT-19965


C
++

NOR_2() {
cout<< “Appel du constructeur defaut de NOR_2”
<< endl;
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NOR_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}
};

#endif NOR_FLAG_

Un programme de test serait par exemple:


//&% nand_nor_20_1.C

#include <iostream.h>
#include “nand_2_20_1.h”
#include “nor_2_20_1.h”
void main() {

NAND_2 nnd;
NOR_2 nrd;

Programmation en C++ IFT-19965 143


C
++

cout << “La table de verite d’une porte nand” << endl;
cout << “est: “ << endl;
cout << endl << endl;
cout << “entree 1 entree 2 sortie” << endl;

nnd.ecrire_in_1(0); nnd.ecrire_in_1(0);
cout << “ “ << nnd.lire_in_1()
<< “ “ << nnd.lire_in_2() << “ “
<< nnd.lire_out_1() << endl;

nnd.ecrire_in_1(0); nnd.ecrire_in_2(1);
cout << “ “ << nnd.lire_in_1()
<< “ “ << nnd.lire_in_2() << “ “
<< nnd.lire_out_1() << endl;

nnd.ecrire_in_1(1); nnd.ecrire_in_2(0);
cout << “ “ << nnd.lire_in_1()
<< “ “ << nnd.lire_in_2() << “ “
<< nnd.lire_out_1() << endl;

nnd.ecrire_in_1(1); nnd.ecrire_in_2(1);
cout << “ “ << nnd.lire_in_1()
<< “ “ << nnd.lire_in_2() << “ “
<< nnd.lire_out_1() << endl << endl;

cout << “La table de verite d’une porte nor” << endl;
cout << “est: “ << endl;
cout << endl << endl;
cout << “entree 1 entree 2 sortie” << endl;

nrd.ecrire_in_1(0); nrd.ecrire_in_2(0);
cout << “ “ << nrd.lire_in_1()
<< “ “ << nrd.lire_in_2() << “ “
<< nrd.lire_out_1() << endl;

nrd.ecrire_in_1(0); nrd.ecrire_in_2(1);
cout << “ “ << nrd.lire_in_1()
<< “ “ << nrd.lire_in_2() << “ “
<< nrd.lire_out_1() << endl;

nrd.ecrire_in_1(1); nrd.ecrire_in_2(0);
cout << “ “ << nrd.lire_in_1()
<< “ “ << nrd.lire_in_2() << “ “
<< nrd.lire_out_1() << endl;

nrd.ecrire_in_1(1); nrd.ecrire_in_2(1);
cout << “ “ << nrd.lire_in_1()
<< “ “ << nrd.lire_in_2() << “ “
<< nrd.lire_out_1() << endl;
}

144 Programmation en C++ IFT-19965


C
++

Résultat
Appel du constructeur defaut de CIRCUIT
Appel du constructeur defaut de NUMERIQUE
Appel du constructeur defaut de NAND_2
Appel du constructeur defaut de CIRCUIT
Appel du constructeur defaut de NUMERIQUE
Appel du constructeur defaut de NOR_2
La table de verite d’une porte nand
est:

entree 1 entree 2 sortie


0 0 1
0 1 1
1 0 1
1 1 0

La table de verite d’une porte nor


est:

entree 1 entree 2 sortie


0 0 1
0 1 0
1 0 0
1 1 0

On remarque encore une fois le grand intérêt d’utiliser les


fonctions d’accès de lecture et d’écriture. Le fait de changer le
code interne d’une fonction membre comme set_out_1() ne
demande aucune modification du programme main(). Cela
évite un bon nombre d’erreurs et sauve beaucoup de temps de
développement.

Programmation en C++ IFT-19965 145


C
++

146 Programmation en C++ IFT-19965


C
++

CHAPITRE 21 Les itérations en C++

256 Ce chapitre présente les instructions while et for. Ces


opérations d’itération permettent de répéter une séquence
d’instructions un certain nombre de fois.

257 Les instructions d’itération du C++ permettent à un


programme ou à une fonction de répéter une séquence
d’opérations jusqu’à ce qu’une condition logique soit vérifiée.
L’instruction while du C++ est composée d’une expression
booléenne contenue entre parenthèses suivie d’un énoncé
associé (ou d’une suite d’énoncés contenus entre une paire de
crochets):
while (expression booléenne) Enoncé associé

L’expression booléenne est d’abord évaluée et si elle retourne


une valeur entière différente de 0, l’énoncé associé est exécuté.
Autrement, le C++ passe outre à l’énoncé associé et exécute les
instructions suivantes.

258 Par exemple, l’énoncé suivant décrémente la variable n jusqu’à


ce qu’elle prenne la valeur 0:
while (n != 0) n = n - 1;

Programmation en C++ IFT-19965 147


C
++

Exercice
259 L’énoncé suivant est-il équivalent à celui du paragraphe 258?
while (n) n = n -1;

260 En électronique numérique, il est souvent nécessaire d’évaluer


la puissance n du nombre 2, notée 2n. L’instruction while
permet d’implanter une fonction très intéressante pour le
calcul de la nième puissance de 2:
//&% puissance_2_21_1.C
#include <iostream.h>

int puissance_de_2(int n) {
int resultat = 1;

while (n != 0) {
resultat = resultat * 2;
n = n-1;
}
return resultat;
}

void main(){
int exposant= 10;

while (exposant) {
cout << “2 puissance “ << exposant
<< “ egale: “ << puissance_de_2(exposant)
<< endl;
exposant = exposant -1;
}
}

Résultat
2 puissance 10 egale: 1024
2 puissance 9 egale: 512
2 puissance 8 egale: 256
2 puissance 7 egale: 128
2 puissance 6 egale: 64
2 puissance 5 egale: 32
2 puissance 4 egale: 16
2 puissance 3 egale: 8
2 puissance 2 egale: 4

148 Programmation en C++ IFT-19965


C
++

2 puissance 1 egale: 2

L’instruction d’itération permet donc une répétition d’énoncés


qu’il serait fastidieux de récrire plusieurs fois.

261 Le principal ennui avec l’instruction while est qu’il faut trois
lignes pour implanter une itération: l’initialisation du
compteur, le test sur la valeur du compteur et troisièmement,
l’incrémentation (ou la décrémentation du compteur).
L’instruction for est une moyen simple de faire des itérations
en C++ sans avoir la lourdeur de l’instruction while. La
syntaxe de l’instruction for est la suivante:
for (expression d’entrée;
Expression booléenne;
Expression de continuation)
énoncé associé

L’expression d’entrée est évaluée une seule fois, lorsque le


programme commence à exécuter l’instruction for. Une fois
que l’expression d’entrée a été évaluée, l’expression
booléenne est ensuite évaluée et, si le résultat est un entier
différent de 0, l’énoncé associé est exécuté, suivi de l’énoncé
de continuation. En suite, la séquence expression booléenne-
énoncé associé-énoncé de continuation se répète jusqu’à ce que
l’expression booléenne prenne la valeur 0. Alors, le programme
passe à l’instruction suivante sans exécuter l’énoncé associé et
l’énoncé de continuation. Remarquez bien que l’ordre
d’exécution des différentes parties de l’instruction for est très
important.

262 Nous allons maintenant tirer profit de la puissance de


l’instruction for pour améliorer le programme de calcul des
puissances de 2 vu au paragraphe 260:
//&% puissance_2_21_2.C
#include <iostream.h>

int puissance_de_2(int n) {
int resultat = 1;
int compteur;

Programmation en C++ IFT-19965 149


C
++

for(compteur=n;compteur;compteur = compteur-1)
resultat = 2 * resultat;

return resultat;
}

void main(){
int exposant;

for (exposant=10;exposant;exposant = exposant - 1) {


cout << “2 puissance “ << exposant
<< “ egale: “ << puissance_de_2(exposant)
<< endl;
}
}

Résultat
2 puissance 10 egale: 1024
2 puissance 9 egale: 512
2 puissance 8 egale: 256
2 puissance 7 egale: 128
2 puissance 6 egale: 64
2 puissance 5 egale: 32
2 puissance 4 egale: 16
2 puissance 3 egale: 8
2 puissance 2 egale: 4
2 puissance 1 egale: 2

263 Dans l’énoncé for du programme du paragraphe 262, on


retrouve l’instruction suivante comme énoncé associé:
resultat = 2*resultat;

Comme ce type d’instruction se retrouve souvent, le C++ offre


une syntaxe abrégée appelée opération d’affectation
étendue prenant la forme suivante:
nom_variable operateur=expression;

Ainsi, l’instruction de multiplication par 2 de la variable


resultat peut tirer profit de l’opération d’affectation étendue
pour s’écrire:

150 Programmation en C++ IFT-19965


C
++

resultat *= 2;

264 Par rapport à ce qui vient d’être dit sur l’instruction


d’affectation étendue, nous pourrions écrire compteur -= 1
au lieu de compteur = compteur -1 dans l’énoncé de
continuation de la boucle for de la fonction puissance_de_2.
Cette syntaxe est cependant très peu fréquemment utilisée
parce que le C++ offre une syntaxe encore plus compacte pour
coder des instructions d’incrémentation/décrémentation par 1
d’une variable. Pour utiliser cette notation compacte, il suffit
simplement de laisser tomber les symboles 1 et = et d’utiliser
l’opérateur d’incrémentation ++ ou de décrémentation --
comme préfixe au nom de la variable. On remplace donc
compteur = compteur -1 par:
--compteur

pour décrémenter la variable compteur d’une unité.


Similairement, on utilise la syntaxe ++compteur pour
incrémenter la variable compteur de 1. Ces nouvelles
méthodes de syntaxe permettent d’écrire une version plus
simple du programme de calcul des puissances de 2:
//&% puissance_2_21_3.C
#include <iostream.h>

int puissance_de_2(int n) {
int resultat = 1;
int compteur;

for(compteur=n;compteur;--compteur)
resultat = 2*resultat;

return resultat;
}

void main(){
int exposant;

for (exposant=10;exposant;--exposant) {
cout << “2 puissance “ << exposant
<< “ egale: “ << puissance_de_2(exposant)
<< endl;
}
}

Programmation en C++ IFT-19965 151


C
++

265 Il faut retenir que l’opérateur -- (ou ++) produit une valeur.
Le C++ permet en principe de placer l’opérateur -- (ou ++) en
suffixe à une variable plutôt qu’en préfixe. On peut donc écrire
x++ et aussi ++x. Cependant, si cette opération
d’incrémentation est utilisée dans une opération plus complexe,
la valeur produite diffère suivant qu’une syntaxe en suffixe soit
utilisée plutôt qu’une syntaxe en préfixe.
Par exemple, si la valeur de la variable compteur est 3, la
valeur produite par l’expression --compteur est 2 et la
nouvelle valeur de la variable compteur est également 2. Par
ailleurs, si la valeur de la variable compteur est 3, la valeur
produite par l’expression compteur-- est 3 mais la nouvelle
valeur de la variable compteur est 2. Par exemple, le code ci-
dessous:
//&% test_inc_dec.C
#include <iostream.h>

void main() {
int i = 10;

cout << “--i: “ << (--i) << endl;


i = 10;
cout << “i--: “ << (i--) << endl;
}

produit le résultat suivant:


--i: 9
i--: 10

Exercice
266 Concevez un programme qui accepte deux entiers n et m au
clavier et qui calcule nm.

Résumé
267 ✔ Si vous désirez répéter un calcul tant qu’une expression
booléenne n’est pas 0, utilisez l’énoncé while.
✔ Si vous désirez répéter une opération incluant un énoncé de
départ, un test et un énoncé de continuation, utilisez
l’instruction for.
✔ Si vous désirez incrémenter ou décrémenter une variable de
l’unité, utilisez les opérateurs préfixes --variable ou
++variable.

152 Programmation en C++ IFT-19965


C
++

✔ Pour implanter une instruction variable = variable


opérateur expression, vous pouvez utiliser la notation
compacte variable opérateur = expression.

Programmation en C++ IFT-19965 153


C
++

154 Programmation en C++ IFT-19965


C
++

CHAPITRE 22 Le traitement des


données contenues
dans des fichiers

268 Dans ce chapitre, nous voyons comment on peut tirer profit des
instructions while et for pour traiter répétitivement des
données contenues dans des fichiers.

269 Il faut se rappeler que la valeur retournée par une expression,


telle que cin >> variable, est normalement la valeur même
de cin. Cela signifie qu’une expression d’entrée utilisant cin
est un argument de gauche acceptable pour une autre
expression d’entrée. On peut donc écrire cin >> variable_1
>> variable_2 >> variable_3. L’énoncé while ci-dessous
est donc valable tant que l’usager continue d’entrer des
nombres au clavier parce qu’alors, la valeur de cin n’est pas 0:
while(cin >> variable_1 >> variable_2 >> variable_3) ...

Pour mettre fin à l’exécution de la boucle while, il faut faire en


sorte que l’opérateur d’entrée retourne 0 au lieu de la valeur de
cin en tapant la combinaison de clés control-d1.

1. Cette combinaison de clés fonctionne sur DOS et UNIX.

Programmation en C++ IFT-19965 155


C
++

270 Il existe plusieurs combinaisons de clés. Malheureusement,


elles n’ont pas toujours la même signification sur DOS et sur
UNIX.

TABLE 3 Combinaisons de clés sur Unix et DOS

Fonction Unix DOS


Interrompre un programme control-c control-c
Forcer une expression d’entrée à 0 control-d control-z
(fin de fichier)

271 Le programme ci-dessous calcule la puissance de 2 entrée au


clavier tant que l’usager ne tape pas la combinaison de clés
control-d:
//&% puissance_2_22_1.C
#include <iostream.h>

int puissance_de_2(int n) {
int resultat = 1;
int compteur;

for(compteur=n;compteur;--compteur)
resultat = 2*resultat;

return resultat;
}

void main(){
int exposant;

while (cin >> exposant) {


cout << “2 puissance “ << exposant
<< “ egale: “ << puissance_de_2(exposant)
<< endl;
}
}

Résultat
3
2 puissance 3 egale: 8
4
2 puissance 4 egale: 16
8
2 puissance 8 egale: 256

156 Programmation en C++ IFT-19965


C
++

12
2 puissance 12 egale: 4096
100
2 puissance 100 egale: 0
45
2 puissance 45 egale: 0
30
2 puissance 30 egale: 1073741824

Les calculs ne cessent que lorsque la combinaison control-d


est tapée au clavier. Remarquez les résultats intéressants pour
2100 et 245. La valeur 0 signifie simplement que la capacité
d’un nombre entier int est dépassée par la valeur calculée. Le
résultat est par conséquent mauvais.

272 Remarquez que si les données sont contenues dans un fichier, il


est encore possible d’utiliser le programme du paragraphe 271
en tirant profit de l’utilitaire de redirection “<“ de DOS ou de
Unix. Par exemple, si les données tapées au clavier dans
l’exemple suivant le programme du paragraphe 271 sont plutôt
contenues dans le fichier de données portant le nom
nombres.dat, on calculer les puissances de 2 en utilisant la
commande suivante:
puissance_2_22_1 < nombre.dat

ou

cat nombre.dat | puissance_2_22_1

Notez que l’utilitaire de redirection “<“ n’est pas ici un


opérateur de C++ mais plutôt de Unix ou de DOS. Il informe
simplement le système d’exploitation de prendre les données
contenues dans le fichier nombre.dat au lieu d’accepter des
nombres au clavier. On redirige donc le clavier vers le fichier
nombre.dat. L’instruction cin >> exposant de la boucle
while retourne la valeur logique 0 lorsque la fin de fichier est
rencontrée.

273 On pourrait utiliser une instruction for au lieu d’une


instruction while pour calculer les puissances de 2. L’intérêt

Programmation en C++ IFT-19965 157


C
++

d’une instruction for est qu’elle permet facilement de compter


le nombre de données entrées au clavier ou contenues dans un
fichier si l’utilitaire de redirection est utilisé:
//&% puissance_2_22_2.C
#include <iostream.h>

int puissance_de_2(int n) {
int resultat = 1;
int compteur;

for(compteur=n;compteur;--compteur)
resultat = 2*resultat;

return resultat;
}

void main(){
int exposant;
int cptr;

for (cptr = 0; cin >> exposant; ++cprt) {


cout << “2 exposant “
<< exposant << “ = “
<< puissance_de_2(exposant) << endl;
}

cout << i << “ donnees ont ete entrees.” << endl;

Résultat
2
2 exposant 2 = 4
4
2 exposant 4 = 16
6
2 exposant 6 = 64
8
2 exposant 8 = 256
16
2 exposant 16 = 65536
32
2 exposant 32 = 0
9
2 exposant 9 = 512
7 donnees ont ete entrees.

158 Programmation en C++ IFT-19965


C
++

Résumé
274 ✔ Si on désire lire des données jusqu’à ce que l’on atteigne la fin
d’un fichier, il suffit d’utiliser l’instruction while avec un
énoncé d’extraction comme expression booléenne. Utiliser une
instruction for permet en plus de compter le nombre de
données qui ont été traitées.

Programmation en C++ IFT-19965 159


C
++

160 Programmation en C++ IFT-19965


C
++

CHAPITRE 23 Les tableaux de nombres

275 Ce chapitre présente comment des nombres peuvent être


stockés dans un tableau et comment ces mêmes nombres
peuvent être extraits d’un tableau.

276 Un tableau1 à une dimension2 est un ensemble d’espaces de


mémoire où l’on peut stocker des objets et les récupérer en
utilisant un indice3. Chaque objet du tableau est appelé
élément de ce tableau. En C++, le premier élément du tableau
correspond à une valeur d’indice égale à 0. On dit alors que
C++ possède des tableaux stockés en origine zéro. L’exemple
qui suit est par exemple un tableau d’entiers:

TABLE 4

0 1 2 3 4 indice
58 56 65 12 24 Nombre

1. “array” en anglais
2. un tableau à une dimension est aussi appelé vecteur
3. “index” en anglais

Programmation en C++ IFT-19965 161


C
++

277 Le nombre d’octets réservé pour chaque élément du tableau


dépend du type de données auquel appartient cet élément.

278 Remarquez que les éléments d’un tableau doivent tous


appartenir au même type de données. Un tableau d’entiers
(int) ne contient que des nombres entiers et ne peut pas
contenir d’éléments de type float, double, ou char par
exemple.

279 Pour définir un tableau global, c’est-à-dire situé hors de toute


fonction, il faut spécifier au C++ le nom du tableau, le type des
objets que contient le tableau et le nombre de dimensions du
tableau et la grandeur de chaque dimension.
Par exemple, la définition du tableau ci-dessous indique au
compilateur que le tableau distances possède une seule
dimension et que le nombre d’éléments dans le tableau est de 5
entiers:
int distances[5];

280 Pour utiliser un tableau et y stocker des données, il suffit


d’identifier le tableau par son nom et de donner l’indice de
l’élément à traiter placé entre crochets:
distances[3] = 22;

281 Pour récupérer une donnée stockée dans un tableau, il suffit de


donner le nom du tableau avec l’indice de l’élément à récupérer
placé entre crochets:
cout << distances[3];

282 Supposons que l’on veuille remplir un tableau d’entiers lus


dans un fichier (qui peut être le clavier1) et que l’on ne sache
pas à l’avance combien de nombres contient le fichier.
L’approche la plus simple, bien que peu élégante, consiste à

1. N’oubliez pas que le clavier est vu comme un fichier spécial par DOS ou Unix.

162 Programmation en C++ IFT-19965


C
++

définir un tableau ayant des dimensions que l’on croit assez


grandes pour contenir tous les nombres lus dans le fichier. On
peut ensuite lire séquentiellement les nombres jusqu’à ce que
la fin du fichier soit rencontrée.
Dans le programme qui suit, on lit des nombres au clavier
jusqu’à ce que l’opérateur entre la combinaison de clés
control-D et on affiche par la suite la somme des nombres
lus:
//&% array_23_1.C

#include <iostream.h>

void main() {

int vecteur[100];

int compteur,i;
int somme = 0;

for (compteur = 0; cin >> vecteur[compteur];++compteur);

for (i = 0; i< compteur; ++i) {


somme += vecteur[i];
}

cout << “La somme des “


<< compteur
<< “ nombres est: “
<< somme << endl;

Résultat
1
2
3
4
5
La somme des 5 nombres est: 15

Programmation en C++ IFT-19965 163


C
++

283 Il est possible de définir des tableaux à plusieurs dimensions. Il


suffit simplement d’ajouter autant de paires de crochets qu’il y
a de dimensions à considérer:
int matrice[2][3]

Le tableau matrice ci-dessus possède deux lignes et trois


colonnes. Les indices sur les lignes vont de 0 à 1 et ceux sur les
colonnes vont de 0 à 2.

Exercice
284 Ecrivez un programme qui effectue le produit de matrices 3 x 3.

Résumé
285 ✔ Lorsqu’on veut garder des valeurs de même type dans une
même variable, il suffit de définir un tableau. Une telle
définition comprend le type des données, le nom du tableau, les
dimensions du tableau et le nombre d’éléments pour chaque
dimensions.
✔ Les indices des dimensions d’un tableau sont notés en origine
zéro.

164 Programmation en C++ IFT-19965


C
++

CHAPITRE 24 Les tableaux d’objets

286 Ce chapitre montre comment les tableaux peuvent être utilisés


pour stocker non seulement des nombres mais également des
objets appartenant à des classes.

287 Supposons que l’on veuille manipuler plusieurs portes NAND


appartenant à un même circuit. Il semble logique de stocker les
objets de type NAND_2 dans un tableau de portes NAND. Dans
ce qui suit, nous allons voir comment on peut créer un tableau
d’objets et comment on peut accéder aux membres de chaque
objet correspondant à un élément du tableau. Supposons que
l’on dispose de 5 portes NAND à deux entrées et que l’on veuille
lire au clavier la valeur des deux entrées de chaque porte pour
ensuite afficher la valeur de la sortie de chacune. Le
programme suivant est un exemple de solution:
//&% array_obj_24_1.C

#include <iostream.h>
#include “nand_2_15_1.h”

void main() {

NAND_2 vec_nnd[5];

int i;

Programmation en C++ IFT-19965 165


C
++

int in_1, in_2;

for (i = 0; i < 5; ++i) {


cout << “--------------------------------” << endl;
cout << “Porte “ << i <<“ : “ << endl;
cout << “entree 1: “;
cin >> in_1;
cout << endl;
cout << “entree 2: “;
cin >> in_2;
cout << endl;
vec_nnd[i].ecrire_in_1(in_1);
vec_nnd[i].ecrire_in_2(in_2);
}

for (i = 0; i < 5; ++i) {


cout << “--------------------------------” << endl;
cout << “Sortie “
<< i << “: “
<< vec_nnd[i].lire_out_1()
<< endl;
}
}

Résultat
--------------------------------
Porte 0 :
entree 1: 1
entree 2: 0
--------------------------------
Porte 1 :
entree 1: 1
entree 2: 1
--------------------------------
Porte 2 :
entree 1: 0
entree 2: 0
--------------------------------
Porte 3 :
entree 1: 1
entree 2: 0
--------------------------------
Porte 4 :
entree 1: 0
entree 2: 1
--------------------------------
Sortie 0: 1
--------------------------------
Sortie 1: 0

166 Programmation en C++ IFT-19965


C
++

--------------------------------
Sortie 2: 1
--------------------------------
Sortie 3: 1
--------------------------------
Sortie 4: 1

288 Cet exemple permet de voir comment on peut stocker des objets
dans les éléments d’un tableau et comment en récupérer le
contenu. Pour définir un tableau d’objet, on procède exactement
comme avec un tableau d’entiers, c’est-à-dire qu’on donne le
type du tableau, son nom les dimensions et la grandeur de
chaque dimension:
NAND_2 vec_nnd[5];

289 Pour accéder à la fonction membre lire_out_1 de l’objet stocké


dans le troisième du tableau, il suffit simplement d’identifier
cet élément avec le bon indice et d’accéder à la fonction membre
via l’opérateur d’accès aux membres:
vec_nnd[2].lire_out_1()

N’oubliez pas que le troisième élément du tableau correspond à


l’indice 2.

Notion avancée
290 Lorsqu’on défini un tableau d’objets, le constructeur défaut de
la classe est appelé pour chaque élément de ce tableau.

Résumé
291 ✔ La manipulation des membres d’un objet faisant partie d’un
tableau se fait de la même manière que pour un objet seul. Il
suffit simplement d’identifier l’élément du tableau pour lequel
on désire accéder au membre via l’opérateur d’accès aux
membres “.”.
✔ Le constructeur par défaut est appelé pour chaque élément
d’un tableau d’objets.

Programmation en C++ IFT-19965 167


C
++

168 Programmation en C++ IFT-19965


C
++

CHAPITRE 25 La créations de flots de


données d’entrée et de
sortie

292 Dans ce chapitre, nous introduisons une approche d’accès direct


aux données contenues dans les fichiers, sans avoir appel à
l’utilitaire de redirection.

293 Un flot est une séquence d’objets de données. Ainsi, dans le


contexte des fichiers d’entrée et de sortie, cout est le nom d’un
flot de données se déplaçant du programme vers l’écran de
l’ordinateur. De façon similaire, cin est le nom d’un flot de
données s’écoulant du clavier vers le programme. Le
schéma ci-dessous illustre le principe des flots de données cin
et cout.
flot cin flot cout
Clavier programme écran

Pour lire des données contenues dans un fichier, il suffit de


créer un flot de données d’entrée à travers lequel les données
transitent du fichier vers le programme. Pour écrire des
données dans un fichier, il suffit de créer un flot de données de
sortie au travers duquel les données transitent du programme
vers le fichier.

Programmation en C++ IFT-19965 169


C
++

294 Pour créer un flot de données d’entrée ou de sortie, il faut


informer le C++ que l’on a l’intention d’utiliser les fonctions de
manipulation de fichiers du C++ contenues dans la
bibliothèque de fonctions fstream.h:
#include <fstream.h>

295 Pour créer un flot de données d’entrée, on utilise un énoncé


d’ouverture de fichier qui veille à créer un flot, à associer un
nom à ce flot, et à ouvrir le fichier d’entrée en suivant la
syntaxe ci-dessous:
ifstream nom_flot (“nom_du_fichier_d’entree”, ios::in);

L’exemple qui suit ouvre un flot d’entrée nnd_flot_in et le


relie au fichier nand_dat.in du répertoire courant:
ifstream nnd_flot_in (“nand_dat.in”, ios::in);

Donc, l’énoncé d’ouverture de fichier comprend le mot


ifstream (pour input file stream) suivi du nom du flot, tel
nnd_flot_in. Ensuite, on place entre parenthèses le nom du
fichier à ouvrir, tel “nand_dat.in”, et ios::in, qui dit au C++
que le fichier nand_dat.in est un fichier d’entrée. L’énoncé
d’ouverture du fichier peut inclure un chemin permettant
d’atteindre le fichier dans un répertoire donné:
ifstream nnd_flot_in (“/gel/usr/gel725/nand_dat.in”,
ios::in);

296 Une fois le flot de données d’entrées ouvert, il suffit de


substituer le nom du flot à cin pour accéder aux données du
fichier:
nnd_flot_in >> in_1 >> in_2;

297 Si ifstream permet de créer un flot de données permettant de


lire des données d’un fichier, ofstream (pour output file
stream) crée un flot qui permet au programme d’écrire des
données dans un fichier de sortie:
ofstream nom_flot(“nom_fichier_sortie”,ios::out);

170 Programmation en C++ IFT-19965


C
++

L’énoncé qui suit permet de créer le flot nnd_flot_out et


d’ouvrir le fichier “nand_dat.out”:
ofstream nnd_flot_out(“nand_dat.out”,ios::out);

La syntaxe est très similaire à celle qui est utilisée pour les
flots d’entrée.

298 Une fois le flot de sortie créé, il suffit de remplacer cout par le
nom du flot pour écrire des données dans le fichier:
nnd_flot_out << out_1 << endl;

Notion avancée
299 Du point de vue de l’implantation dans la bibliothèque de
fonctions du C++, cin et cout sont des objets des classes
istream et ostream respectivement. De même, nnd_flot_in
et nnd_flot_out sont des objets des classes ifstream et
ofstream respectivement.

300 Avec les nouveaux concepts vus dans ce chapitre, on peut


reprendre l’exemple du paragraphe 287 et lire les données
d’entrée dans un fichier, au lieu de les lire au clavier, et écrire
les valeurs des sorties dans un fichier de sortie au lieu de les
afficher à l’écran de l’ordinateur:
//&% fichier_25_1.C

#include <iostream.h>
#include <stdlib.h>
#include <fstream.h>
#include “nand_2_20_1.h”

void main() {
NAND_2 vec_nnd[5];
int i;
int in_1, in_2;

ifstream nnd_flot_in (“nand_dat.in”, ios::in);


ofstream nnd_flot_out(“nand_dat.out”, ios::out);

if (nnd_flot_in == 0){
exit(0);
}

Programmation en C++ IFT-19965 171


C
++

cout << “Lecture des donnees du fichier d’entree” << endl;

for (i = 0; i < 5; ++i) {


nnd_flot_in >> in_1;
nnd_flot_in >> in_2;

vec_nnd[i].ecrire_in_1(in_1);
vec_nnd[i].ecrire_in_2(in_2);
}

cout << “Donnees lues” << endl;

cout << “Ecriture des donnees dans le fichier de sortie” <<


endl;

for (i = 0; i < 5; ++i) {


nnd_flot_out << vec_nnd[i].lire_out_1() << “ “;
}

cout << “Donnees sauvegardees dans le fichier de sortie” <<


endl;

nnd_flot_in.close();
nnd_flot_out.close();

Résultat
Lecture des donnees du fichier d’entree
Donnees lues
Ecriture des donnees dans le fichier de sortie
Donnees sauvegardees dans le fichier de sortie

Si on observe le contenu des fichiers, on constate que les


données qu’ils contiennent sont correctes:

Résultat
bersimis% more nand_dat.in
0 0 0 1 1 0 1 1 1 0
bersimis% more nand_dat.out
1 1 1 0 1
bersimis%

172 Programmation en C++ IFT-19965


C
++

301 Le programme ci-dessus applique un concept dont nous n’avons


pas encore traité, celui de la fermeture des fichiers. En effet,
lorsqu’un programme se termine, une pratique judicieuse
consiste à fermer explicitement les fichiers préalablement
ouverts. On peut fermer un fichier en utilisant l’énoncé
suivant:
nom_flot.close();

La syntaxe reflète ici le fait que les flots sont des objets et que
la fonction close() est une fonction membre public accédée
par l’opérateur d’accès aux membres.

302 Il y a beaucoup plus à dire sur les fichiers d’entrée et de sortie


et les flots de données. Cependant, nous en resterons pour
l’instant aux notions de base vues dans ce chapitre.

Résumé
303 ✔ Pour manipuler les flots de données, il faut inclure la ligne
suivante dans le programme #include <fstream.h>
✔ Pour ouvrir un flot de données d’entrée, il suffit d’adopter la
syntaxe suivante: ifstream nom_flot(“nom_fichier”,
ios::in);
✔ Pour ouvrir un flot de données de sortie, il suffit d’adopter la
syntaxe suivante: ofstream
nom_flot(“nom_fichier”,ios::out);
✔ Pour accéder aux données d’entrée, il suffit d’adopter la
syntaxe nom_flot >> variables d’entrée;
✔ Pour stocker des données dans le flot de sortie, il suffit
d’adopter la syntaxe suivante: nom_flot << données;
✔ pour fermer les flots de données, il suffit d’adopter la syntaxe
suivante: nom_flot.close();

Programmation en C++ IFT-19965 173


C
++

174 Programmation en C++ IFT-19965


C
++

CHAPITRE 26 La création d’objets au


“run-time”

304 Lorsqu’on définit une variable globale, le compilateur C++


alloue la mémoire nécessaire pour stocker cette variable,
qu’elle soit une variable entière int ou une variable de type
NAND_2. Ce chapitre présente une approche d’allocation de
mémoire qui réserve seulement un petit espace mémoire pour
stocker un pointeur à une variable lors de la compilation et
permet ensuite de réserver l’espace complet pour stocker cette
variable lors du run-time1.
Au prochain chapitre, nous verrons plus en détails comment
l’allocation de mémoire lors du run-time permet de diminuer la
quantité de mémoire requise lorsque le nombre d’objets à
manipuler dans un programme est nettement inférieur au
nombre maximum d’objets estimé.

305 Une variable globale de type NAND_2 identifie l’adresse où


l’espace mémoire réservé pour cette variable commence.

306 Un pointeur est un espace mémoire qui contient l’adresse d’un


objet. Le nom d’un pointeur identifie l’espace mémoire où est

1. i.e. lors de l’exécution du programme.

Programmation en C++ IFT-19965 175


C
++

stockée l’adresse d’un objet. La FIGURE 4 montre la distinction


entre une variable globale et un pointeur.
FIGURE 4 Variable globale et pointeur

Mémoire de l’ordinateur
Adresse Contenu

0 in_1

}
le nom nnd
identifie 1 in_2
l’objet nnd
2 out_1 variable
nnd
3 .
.
.
4
5
6
pointeur
7 0 } ptr_nnd
le nom 8
ptr_nnd identifie
l’adresse 9
du début de
l’objet nnd 10
11
12

307 L’espace réservé pour un pointeur dépend de l’implantation du


C++ utilisée. Cependant, il faut retenir qu’un pointeur
occupe toujours le même espace, qu’il pointe à une variable
entière int, une variable double, à une variable de type
NAND_2, ou à toute autre type de variable. En général, le
programmeur n’a pas à se soucier de l’espace occupé par un
pointeur.

308 On définit une variable globale qui est un pointeur de la même


manière qu’on définit toute autre variable à l’exception du fait

176 Programmation en C++ IFT-19965


C
++

qu’on ajoute un astérisque”*”1 devant le nom de la variable


pour informer le compilateur que cette variable est un pointeur
du type indiqué dans la déclaration. Ainsi, le code ci-dessous
définit une variable de type int, une autre de type double et
une dernière de type NAND_2. On définit également un
pointeur à un int, un autre à un double, et un dernier à un
NAND_2 :
int var_entiere; //Alloue de l’espace pour un entier
double var_double; //Alloue de l’espace pour un double
NAND_2 var_NAND_2; /*Alloue de l’espace pour un
objet NAND_2*/
int *pointeur_a_entier; /*Alloue de l’espace pour
un pointeur à un int*/
double *pointeur_a_double; /*Alloue de l’espace pour
un pointeur à un double*/
NAND_2 *pointeur_a_NAND_2; /*Alloue de l’espace pour
un pointeur à un NAND_2*/
La définition double *pointeur_a_double signifie que
l’identificateur pointeur_a_double identifie l’espace réservé
à une adresse. La syntaxe *pointeur_a_double, incluant
l’astérisque, identifie l’espace de mémoire réservé à un
double.

309 En résumé, pointeur_a_double identifie un espace mémoire


qui contient une adresse correspondant à un double tandis
que *pointeur_a_double identifie un espace mémoire
contenant un double. On peut illustrer ceci par le diagramme
suivant.
correspond
pointeur_a_double adresse d’un double

*pointeur_a_double valeur d’un double

310 Lorsqu’on déclare une variable de type pointeur dans un


programme, seul l’espace nécessaire pour stocker une adresse
est réservé par le compilateur. Par exemple, la déclaration:

1. le symbole “*” sert aussi à la multiplication. Le compilateur, de par le contexte d’utilisation de


ce symbole, choisit la signification qu’il doit prendre.

Programmation en C++ IFT-19965 177


C
++

NAND_2 nnd;

réserve l’espace mémoire nécessaire au stockage d’un objet de


type NAND_2 appelé nnd.
Par ailleurs, la déclaration:
NAND_2 *nnd_ptr;

réserve l’espace pour l’adresse d’un objet de type NAND_2.

311 Tout programme en C++ peut accéder à un vaste champ de


mémoire appelé “freestore”. L’opérateur new permet
d’allouer de l’espace mémoire pour stocker un objet au run-
time en considérant le freestore comme un réservoir dans
lequel il peut aller chercher l’espace mémoire requis pour
stocker l’objet. Par exemple, l’instruction new double alloue
de la mémoire pour stocker un double et va chercher cet
espace dans le freestore. Il en est de même pour l’instruction
new NAND_2 qui alloue de la mémoire du freestore pour
stocker un objet de type NAND_2.

312 Le fait important dont il faut se rappeler est que l’opérateur


new retourne l’adresse de l’espace mémoire fraîchement alloué.
La syntaxe pour l’utilisation de l’opérateur new est donc la
suivante:
double *ptr_double;
ptr_double = new double;

L’opérateur new alloue de la mémoire pour stocker un double


et retourne l’adresse de cet espace de mémoire. Cette adresse
est stockée dans un pointeur à un double. De façon analogue,
on peut allouer de l’espace mémoire pour un objet de type
NAND_2 et stocker l’adresse de cet espace mémoire dans un
pointeur à un type NAND_2:
NAND_2 *ptr_nand_2;
ptr_nand_2 = new NAND_2;

Remarquez que le fait de créer un pointeur de type NAND_2


n’implique pas que le compilateur appelle le constructeur par
défaut de la classe. Cependant, lorsque l’opérateur new réserve
l’espace pour stocker l’objet de type NAND_2 et que l’adresse de

178 Programmation en C++ IFT-19965


C
++

cet espace est stocké dans le pointeur ptr_nand_2, alors le


constructeur par défaut de la classe est appelé. Nous verrons
plus loin qu’on peut faire en sorte que l’opérateur new provoque
l’appel de constructeurs avec arguments.

313 Comme ptr_nand_2 est un pointeur et que *ptr_nand_2 est


un objet de type NAND_2, on peut utiliser *ptr_nand_2 dans
des expressions plus complexes. On peut par exemple accéder
aux variables membres et fonctions membres de l’objet en
utilisant l’opérateur d’accès aux membres:
(*ptr_nand_2).ecrire_in_1(1);

L’instruction ci-dessus appelle la fonction membre d’écriture


ecrire_in_1 de l’objet *ptr_nand_2. Les parenthèses sont
nécessaires parce que “.” est prioritaire sur “*”. Ne pas mettre
les parenthèses, c’est-à-dire écrire
*ptr_nand_2.ecrire_in_1(1), revient à écrire
*(ptr_nand_2.ecrire_in_1(1)). Or cela signifie que
ptr_nand_2.ecrire_in_1(1) retourne un pointeur et que
l’astérisque tente d’accéder au contenu de la donnée pointée
par le pointeur...Ce qui n’a pas de sens.

314 En fait, peu de programmeurs utilisent la syntaxe ci-dessus


pour accéder aux membres d’un objet via *pointeur. La raison
est que la syntaxe est lourde et impose l’utilisation obligatoire
des parenthèses. Heureusement, il existe un opérateur plus
facile à utiliser appelé “opérateur de pointeurs aux
classes1”, noté ->, avec lequel on peut accéder aux membres
des classes en adoptant la syntaxe suivante:
ptr_nand_2->ecrire_in_1(1);

1. “class-pointer operator” en anglais.

Programmation en C++ IFT-19965 179


C
++

On peut voir l’opérateur -> comme un raccourci pour appeler


les opérateurs “*” et “.” d’un seul coup sans avoir à ajouter des
parenthèses.
équivalent
(*ptr_nand_2).ecrire_in_1(1) ptr_nand_2->ecrire_in_1(1)

315 On peut bien sûr utiliser l’opérateur “->” de la sorte:


ptr_nand_2->in_1 = 0;

Notion avancée
316 Du point de vue du type des données pour l’opération new
double, l’opérateur new génère, à l’interne, un pointeur de type
void *. Ce pointeur est ensuite automatiquement converti1 en
un pointeur de double. Il en est de même pour les types définis
par l’usager tel la classe NAND_2.

Notion avancée
317 Un autre moyen d’associer un pointeur à un objet est d’utiliser
l’opérateur “adresse-de2”, noté &nom_variable, qui retourne
l’adresse d’une variable. Ainsi, le code ci-dessous est correct:
NAND_2 *ptr_nand_2;
NAND_2 nnd;
ptr_nand_2 = &nnd;

La syntaxe de la dernière ligne est correcte puisque &nnd


retourne l’adresse de l’objet nnd de type NAND_2 et stocke cette
adresse (i.e. un pointeur) dans le pointeur ptr_nand_2 qui est
défini comme étant un pointeur à un objet de type NAND_2.

Résumé
318 ✔ Une variable ordinaire correspond à une donnée tandis
qu’une variable pointeur correspond à l’adresse d’une donnée.
✔ Les pointeurs à des données occupent en général beaucoup

1. par une opération de casting transparente à l’usager


2. “address-of” en anglais

180 Programmation en C++ IFT-19965


C
++

moins d’espace mémoire que le font les données elles-mêmes.


Pour définir une variable pointeur, il suffit d’adopter la syntaxe
suivante: type_de_donnee *nom_pointeur.
✔ Pour allouer de l’espace mémoire au run-time, il suffit
d’adopter la syntaxe suivante: nom_pointeur = new
type_de_donnee
✔ Pour accéder aux membres d’un objet via le pointeur à cet
objet, il suffit d’adopter l’une des deux approches suivantes:
(*nom_pointeur).variable_ou_fonction_membre, ou
encore nom_pointeur->variable_ou_fonction_membre.
La seconde approche est la plus judicieuse.
✔ Pour assigner une valeur à une variable membre d’un objet, il
suffit d’adopter l’une des deux approches suivantes:
(*nom_pointeur).variable_membre = valeur, ou encore
nom_pointeur->variable_membre = valeur. La seconde
approche est la plus judicieuse.

Programmation en C++ IFT-19965 181


C
++

182 Programmation en C++ IFT-19965


C
++

CHAPITRE 27 Le stockage des


pointeurs aux objets
d’une classe.

319 Dans l’exemple du paragraphe 282 du CHAPITRE 23, nous


avions déclaré un tableau de 100 composantes parce que nous
ne savions pas exactement comment de données allaient être
fournies au programme. Un tel tableau peut occuper un espace
mémoire très significatif si les éléments du tableau sont des
objets volumineux.
Heureusement, nous venons de voir qu’un pointeur à un objet
occupe souvent beaucoup moins d’espace que l’objet lui-même.
Donc, si au lieu de déclarer un tableau de 100 objets on déclare
un tableau de 100 pointeurs à des objets, l’espace réservé
initialement par le compilateur est considérablement réduit.
Ce chapitre présente comment les tableaux de pointeurs
peuvent être mis à profit pour sauver de l’espace mémoire lors
de la déclaration de tableaux de grandes dimensions.

320 Supposons qu’on reprenne l’exemple du paragraphe 300 du


CHAPITRE 25 avec les modifications suivantes:
le nombre de données dans le fichier d’entrée est inconnu,
c’est-à-dire qu’on ignore a priori le nombre de portes NAND
que le fichier sert à décrire.
Il faut donc prévoir un tableau d’objets de type NAND_2 qui
contient un assez grand nombre d’éléments pour faire face à un
grand nombre de cas. On pourrait simplement choisir de définir

Programmation en C++ IFT-19965 183


C
++

un tableau de 100 portes NAND et ensuite lire le fichier


d’entrée jusqu’à ce qu’il soit vide. Cependant, si les fichiers ont
en moyenne de faibles dimensions par rapport à 100, l’espace
réservé pour le tableau d’objets NAND_2 s’avère beaucoup trop
grand. Le code ci-dessous implante cette solution peu valable:
//&% tableau_bad.C
#include <iostream.h>
#include <fstream.h>
#include “nand_2_15_1.h”

NAND_2 vec_nnd[100];

void main() {

int i, compteur;
int in_1, in_2;

ifstream nnd_flot_in (“nand_dat.in”, ios::in);


ofstream nnd_flot_out(“nand_dat.out”, ios::out);
cout << “Lecture des donnees du fichier d’entree” << endl;
for (compteur = 0; nnd_flot_in >> in_1 >> in_2; ++compteur)
{
vec_nnd[compteur].ecrire_in_1(in_1);
vec_nnd[compteur].ecrire_in_2(in_2);
}
cout << compteur << “ donnees lues” << endl;
cout << “Ecriture des donnees dans le fichier de sortie” <<
endl;
for (i = 0; i < compteur; ++i)
{
nnd_flot_out << vec_nnd[i].lire_out_1() << “ “;
}
cout << “Donnees sauvegardees dans le fichier de sortie” <<
endl;
nnd_flot_in.close();
nnd_flot_out.close();
}

Résultat
Lecture des donnees du fichier d’entree
5 donnees lues
Ecriture des donnees dans le fichier de sortie
Donnees sauvegardees dans le fichier de sortie

184 Programmation en C++ IFT-19965


C
++

Le programme fonctionne correctement. Cependant, un vecteur


de 100 objets de type NAND_2 a été réservé alors que des
données d’entrée pour seulement 5 objets ont été lus dans le
fichier1. Donc, si le programmeur est pessimiste, il risque
souvent de réserver trop d’espace pour rien.

321 Une solution beaucoup plus judicieuse consiste à réserver de


l’espace pour des pointeurs à des objets et d’allouer
dynamiquement au run-time l’espace nécessaire avec
l’opérateur new. Comme les pointeurs occupent généralement
beaucoup moins d’espace que les objets eux-mêmes, l’espace
réservé en trop pour les pointeurs non utilisés est donc
négligeable. Le code ci-dessous implante cette solution qui
s’avère meilleure que celle du paragraphe 320:
//&% tableau_good.C

#include <iostream.h>
#include <fstream.h>
#include “nand_2_15_1.h”
NAND_2 *vec_nnd[100];

void main() {
int i, compteur;
int in_1, in_2;
ifstream nnd_flot_in (“nand_dat.in”, ios::in);
ofstream nnd_flot_out(“nand_dat.out”, ios::out);
cout << “Lecture des donnees du fichier d’entree” << endl;
for (compteur = 0; nnd_flot_in >> in_1 >> in_2; ++compteur)
{
vec_nnd[compteur] = new NAND_2;
vec_nnd[compteur]->ecrire_in_1(in_1);
vec_nnd[compteur]->ecrire_in_2(in_2);
}
cout << compteur << “ donnees lues” << endl;
cout << “Ecriture des donnees dans le fichier de sortie” <<
endl;
for (i = 0; i < compteur; ++i) {
nnd_flot_out << vec_nnd[i]->lire_out_1() << “ “;
}
cout << “Donnees sauvegardees dans le fichier de sortie” <<
endl;

1. Les même fichiers de données que pour le programme du paragraphe 300 au CHAPITRE 25
ont été utilisés.

Programmation en C++ IFT-19965 185


C
++

nnd_flot_in.close();
nnd_flot_out.close();
}

Résultat
Lecture des donnees du fichier d’entree
5 donnees lues
Ecriture des donnees dans le fichier de sortie
Donnees sauvegardees dans le fichier de sortie

Remarquez les différences importantes apportées au


programme:
1. on définit maintenant un tableau de pointeurs à des objets
NAND_2:
NAND_2 *vec_nnd[100];

au lieu d’un tableau d’objets NAND_2


2. au fur et à mesure qu’on lit une donnée dans le fichier, on
alloue l’espace pour un objet:
vec_nnd[compteur] = new NAND_2;

3. on stocke les entrées dans les variables membres de l’objet


nouvellement créé:
vec_nnd[compteur]->ecrire_in_1(in_1);
vec_nnd[compteur]->ecrire_in_2(in_2);

avec l’opérateur “->”.


Il faut aussi remarquer qu’en procédant de la sorte, le gain en
espace mémoire augmente au fur et à mesure que la taille des
objets est grande par rapport à la taille d’un pointeur.

322 Plus loin, nous verrons comment l’opérateur delete permet de


libérer de l’espace mémoire préalablement alloué avec new.

Exercice
323 L’opérateur sizeof calcule le nombre d’octets requis pour
stocker un objet. Par exemple, sizeof(char) retourne le

186 Programmation en C++ IFT-19965


C
++

nombre d’octets qui sont nécessaires pour stocker une variable


de type char.
Utilisez l’opérateur pour connaître le nombre d’octets requis
pour stocker un objet de type NAND_2. Utilisez la syntaxe
suivante dans votre programme:
cout << sizeof(NAND_2) << endl;

Faites de même avec un pointeur à un objet de type NAND_2


(par exemple sizeof(vect_nnd[1]) et calculez la différence
entre l’espace réservé pour un pointeur à un objet et l’espace
réservé pour l’objet lui-même.

Résumé
324 ✔ Si on désire traiter des données dont on ignore le nombre au
départ et si on désire ne pas trop mobiliser d’espace mémoire, il
est judicieux de déclarer un tableau de pointeurs à des objets.

Programmation en C++ IFT-19965 187


C
++

188 Programmation en C++ IFT-19965


C
++

CHAPITRE 28 Introduction aux


fonctions virtuelles
(virtual)

325 Jusqu’à maintenant, nous avons travaillé avec des tableaux


d’entiers, des tableaux d’objets de type NAND_2 et des tableaux
de pointeurs à des objets de type NAND_2. Maintenant, nous
allons travailler à la conception du programme
analyse_circuit, un programme qui travaille sur un
tableau de pointeurs à des objets de type NUMERIQUE, la
superclasse des classes NAND_2 et NOR_2.
Le rôle de ce programme est très simple: il lit le type de porte
(NAND_2 ou NOR_2) dans un fichier de même que la valeur des
entrées de chaque porte et stocke le type de porte et la sortie de
chaque porte dans un fichier de sortie. Il affiche ensuite un
rapport sur les entrées et les sorties de chaque porte du circuit
décrit par le fichier d’entrée.
Le programme analyse_circuit peut être vu comme un bloc
de base qui intègre plusieurs concepts importants en C++. Ce
bloc peut ensuite être modifié à loisir pour l’adapter à une
application différente.

326 Supposons qu’un concepteur de logiciel ait créé avant vous un


programme qui identifie une porte NAND à deux entrées par le
nombre entier 1 et une porte NOR à deux entrées par le

Programmation en C++ IFT-19965 189


C
++

nombre 2. Ce programme crée un fichier dont le contenu prend


la forme suivante:
type_porte entree_1 entree_2 type_porte entree_1
entree_2...

Supposons aussi qu’on veuille utiliser le numéro du type de


porte pour identifier le nom de la pièce (i.e. NAND ou NOR) et
ensuite afficher ce type. Un fichier typique aurait par exemple
l’allure suivante:

1 1 1 2 0 1 1 1 0
{

{
Porte Porte Porte
nand entrées entrées nand entrées
nor

le fichier contient l’information sur trois portes: une porte


NAND d’entrées (1,1), une porte NOR d’entrées (0,1) et une
deuxième porte NAND d’entrées (1,0).
On pourrait écrire le programme suivant pour produire le
rapport sur le circuit:
//&% it_fil_28_1.C
#include <iostream.h>
#include <stdlib.h>
#include <fstream.h>

void main() {
int type;
int in_1, in_2;

ifstream flot_in (“nand_dat_28_1.in”, ios::in);


if (flot_in == 0) {
exit(0);
}
while ( flot_in >> type >> in_1 >> in_2) {
if (type == 1) cout << “NAND_2” << endl;
else
if (type == 2) cout << “NOR_2” << endl;
else cout << “Erreur” << endl;
}

flot_in.close();
}

190 Programmation en C++ IFT-19965


C
++

Le rapport produit pour le fichier ci-dessus est:

Résultat
NAND_2
NOR_2
NAND_2

On appelle ce genre de programme un “filtre itératif” parce


qu’il transforme le flot d’entrée en un flot de sortie.

327 Le programme du paragraphe 326 est intéressant mais peut


traiter un nombre de cas limité. Par exemple, si on veut créer
dynamiquement des éléments d’une matrice lors de la lecture
du fichier, le programme ci-dessus est inadéquat. La raison en
est simple: comme le fichier contient de l’information sur des
portes NAND_2 et NOR_2, on ne peut créer un seul vecteur de
pointeurs capable de stocker les deux types d’informations. En
effet, nous avons vu que tous les éléments d’un vecteur doivent
être du même type. Donc, si on déclare un vecteur de pointeurs
à des objets de type NAND_2:
NAND_2 *nnd_pointeur[100];

on ne pourra qu’allouer de l’espace pour des objets de type


NAND_2 et stocker leur adresse dans le pointeur à l’élément en
traitement. Que faire alors de l’information sur les objets de
type NOR_2 contenue dans le fichier d’entrée? On pourrait
éventuellement créer un second vecteur de pointeurs:
NOR_2 *nor_pointeur[100];

et stocker les informations dans l’un ou l’autre des vecteurs


dépendant du type de porte. Cependant, cela implique la
déclaration de deux vecteurs de pointeurs, ce qui est peu
élégant.

328 Une raison pour laquelle C++ exige que les objets dans un
tableau soient tous du même type est qu’une telle exigence
nous assure que chaque objet occupe exactement le même

Programmation en C++ IFT-19965 191


C
++

espace de mémoire, ce qui rend la compilation plus efficace,


permettant ainsi d’accélérer l’accès aux éléments du tableau
par un indice.

329 Rappelons-nous cependant que les classes NAND_2 et NOR_2


héritent de la classe NUMERIQUE. Or, le C++ possède la
propriété intéressante que, lorsqu’on déclare un tableau
d’objets d’une classe donnée, les pointeurs peuvent contenir
l’adresse d’un objet de la classe à laquelle appartient l’objet
mais également l’adresse de tout objet appartenant à une sous-
classe. Par exemple, on pourrait créer un tableau de pointeurs
à des objets de type NUMERIQUE:
NUMERIQUE *num_circuit[100];

330 On peut par la suite allouer de l’espace pour un objet de type


NAND_2 et stocker le pointeur à cet espace dans l’élément
num_circuit[0]:

num_circuit[0] = new NAND_2;

331 A proprement parler, ce qui se produit dans ce cas est la chose


suivante: new NAND_2 produit un pointeur à un objet de type
NAND_2 . Parce que num_circuit est un tableau de pointeurs
à des objets de type NUMERIQUE, le pointeur à l’objet NAND_2
est transformé, par une opération de casting automatique, en
un pointeur de type NUMERIQUE. Le pointeur contient
néanmoins l’adresse d’un espace de mémoire capable de
contenir un objet de type NAND_2.

332 On peut maintenant améliorer notre programme de rapport


pour tenir compte des concepts vus ci-dessus:
//&% it_fil_28_2.C
#include <iostream.h>
#include <stdlib.h>
#include <fstream.h>
#include “numerique_28_2.h”
#include “nand_2_28_2.h”
#include “nor_2_28_2.h”

192 Programmation en C++ IFT-19965


C
++

void main() {

int i, type;
int in_1, in_2;
NUMERIQUE *num_ptr[100];

ifstream flot_in (“nand_dat_28_1.in”, ios::in);


if (flot_in == 0){
exit(0);
}
for (i = 0; flot_in >> type >> in_1 >> in_2 ; ++i) {
if (type == 1) {cout << “Objet NAND_2 cree” << endl;
num_ptr[i] = new NAND_2;}
else
if (type == 2) { cout << “Objet NOR_2 cree” <<
endl;
num_ptr[i] = new NOR_2;}
else cout << “Erreur” << endl;
}
cout << i << “ objets crees. “ << endl;
flot_in.close();
}

Résultat
Objet NAND_2 cree
Objet NOR_2 cree
Objet NAND_2 cree
3 objets crees.

333 Le code ci-dessus pourrait cependant être mieux conçu. En


effet, il est peu élégant, et aussi peu pratique pour la
modification du code, d’afficher le type de composante lors de sa
création à l’intérieur de l’énoncé if. Il serait plus judicieux
d’ajouter une fonction membre responsable d’afficher le type de
composante à laquelle la classe appartient. Modifions la
définition des classes CIRCUIT, NUMERIQUE, NAND_2 et NOR_2
pour apporter cette amélioration.
La classe CIRCUIT prend la forme:
//&% circuit_28_2.h

#include <iostream.h>

Programmation en C++ IFT-19965 193


C
++

#ifndef CIRCUIT_FLAG_
#define CIRCUIT_FLAG_

class CIRCUIT {
private:
double v_num_plus; //Tension numerique positive (en volts)
double v_num_moins; //Tension numerique negative (en
volts)
double v_analog_plus; //Tension analogique positive (en
volts)
int numero; //Numero du composant

public:

// Constructeur par defaut

CIRCUIT () {
// cout << “Appel du constructeur defaut de CIRCUIT” <<
endl;
v_num_plus = 5.0;
v_num_moins = 0.0;
v_analog_plus = 12.0;
numero = 0;
}

// Constructeur avec initialisation des variables membres

CIRCUIT (double v_n_p, double v_n_m, double v_a_p, int no)


{
// cout << “Creation d’un objet CIRCUIT” << endl;
v_num_plus = v_n_p;
v_num_moins = v_n_m;
v_analog_plus = v_a_p;
numero = no;
}

// Reader de v_num_plus

double read_v_num_plus() {
return v_num_plus;
}

// Reader de v_num_moins

double read_v_num_moins () {
return v_num_moins;
}

// Reader de v_analog_plus

194 Programmation en C++ IFT-19965


C
++

double read_v_analog_plus () {
return v_analog_plus;
}

// Reader de numero

int read_numero () {
return numero;
}

// Writer de v_num_plus

void set_v_num_plus(double v) {
v_num_plus = v;
}

// Writer de v_num_moins

void set_v_num_moins(double v) {
v_num_moins = v;
}

// Writer de v_analog_plus

void set_v_analog_plus(double v) {
v_analog_plus = v;
}

// Writer de numero

void set_numero(int n) {
numero = n;
}

// Affichage du type de composant


void affiche_type() {cout << “Composante de circuit”
<< endl;}
};

#endif CIRCUIT_FLAG_

La classe NUMERIQUE prend la forme:


//&% numerique_28_2.h
#include <iostream.h>
#include “circuit_28_2.h”

#ifndef NUMERIQUE_FLAG_
#define NUMERIQUE_FLAG_

Programmation en C++ IFT-19965 195


C
++

class NUMERIQUE : public CIRCUIT {


private:
int fan_out; //Nombre admissible de sorties

public:

// Constructeur par defaut


NUMERIQUE () {

// cout << “Appel du constructeur defaut de NUMERIQUE” <<


endl;
fan_out = 10;
}

// Reader de fan_out
int read_fan_out() {
return fan_out;
}

// Writer de fan_out
void set_fan_out(int f_out) {
fan_out = f_out;
}

// Affichage du type de composant


void affiche_type() {cout<< “Composante numerique”
<< endl;}
};

#endif NUMERIQUE_FLAG_

La classe NAND_2 prend la forme:


//&% nand_2_28_2.h

#include <iostream.h>
#include “circuit_28_2.h”
#include “numerique_28_2.h”

#ifndef NAND_FLAG_
#define NAND_FLAG_

class NAND_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1() {
return 1 - (in_1 * in_2);

196 Programmation en C++ IFT-19965


C
++

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut


NAND_2() {
/* cout<< “Appel du constructeur defaut de NAND_2”
<< endl;*/
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}
// Affichage du type de composant
void affiche_type() {cout<< “Composante NAND_2”
<< endl;}
};

#endif NAND_FLAG_

La classe NOR_2 prend la forme:

Programmation en C++ IFT-19965 197


C
++

//&% nor_2_28_2.h

#include <iostream.h>
#include “circuit_28_2.h”
#include “numerique_28_2.h”

#ifndef NOR_FLAG_
#define NOR_FLAG_

class NOR_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1() {
return ((1-in_1) * (1-in_2));
}

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut


NOR_2() {
/* cout<< “Appel du constructeur defaut de NOR_2”
<< endl;*/
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NOR_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {

198 Programmation en C++ IFT-19965


C
++

in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}

// Affichage du type de composant


void affiche_type() {cout<< “Composante NOR_2”
<< endl;}
};

#endif NOR_FLAG_

334 Récrivons maintenant le programme de rapport afin de tirer


profit de l’amélioration apportée à chaque classe de la
hiérarchie:
//&% it_fil_28_3.C

#include <iostream.h>
#include <stdlib.h>
#include <fstream.h>
#include “numerique_28_2.h”
#include “nand_2_28_2.h”
#include “nor_2_28_2.h”

void main() {

int i, compteur, type;


int in_1, in_2;
NUMERIQUE *num_ptr[100];

ifstream flot_in (“nand_dat_28_1.in”, ios::in);


if (flot_in == 0){
exit(0);
}
for (compteur = 0; flot_in >> type >> in_1 >> in_2 ;
++compteur) {

if (type == 1) num_ptr[compteur] = new NAND_2;


else
if (type == 2) num_ptr[compteur] = new NOR_2;
else cout << “Erreur” << endl;

Programmation en C++ IFT-19965 199


C
++

flot_in.close();
cout << compteur << “ objets crees. “ << endl << endl;
for (i = 0; i < compteur ; ++i) {
num_ptr[i]->affiche_type();
}
}
Le résultat de l’exécution de ce programme est le suivant:

Résultat
4 objets crees.

3
3
3
3

On remarque que les résultats obtenus ne correspondent pas à


ce qu’on souhaite. Au lieu d’imprimer le type de chaque
composante, le programme imprime un résultat qui dit que
tous les objets crées sont de la classe NUMERIQUE. En fait, ce
résultat est normal du point de vue de la compilation. Lors de
la compilation, tout ce que le compilateur connaît des pointeurs
du tableau num_ptr est qu’ils sont de type NUMERIQUE. Il en
déduit donc que, lors de l’appel de la fonction membre
affiche_type(), c’est la fonction de la classe NUMERIQUE qui
doit être appelée. En effet, les objets de type NAND_2 et NOR_2
étant crées au run-time, le compilateur n’a aucune information
lui permettant de décider de la fonction adéquate lors de la
compilation. Pour que le programme fonctionne correctement,
il faut informer le compilateur, au moment même de la
compilation, que le programme devra décider au run-time des
fonctions membres à utiliser. Le moyen qui a été adopté pour
transmettre cette information est d’ajouter le mot réservé
virtual devant la définition de la fonction membre
affiche_type(). De cette façon, le compilateur sait, en
rencontrant le mot réservé virtual, que le choix de la fonction
devra se faire au run-time plutôt que lors de la compilation.
Une fonction membre à laquelle on attribue la propriété
virtual est appelée fonction membre virtuelle. De telles
fonctions membres sont qualifiées de virtuelles parce que
l’information requise pour choisir la fonction à utiliser n’est pas

200 Programmation en C++ IFT-19965


C
++

disponible lors de la compilation. La fonction est donc virtuelle


(par opposition à une fonction réelle) lors de la compilation. Le
compilateur en étant informé, il prend les disposition
nécessaires pour que le choix de la fonction appropriée soit fait
en rapport avec le contexte du programme lors du run-time.

335 De façon générale, il faut définir une fonction comme étant


virtuelle dans la situation suivante:
1. Un pointeur a été défini pour pointer à un objet d’une classe
donnée. Supposons que cette classe se nomme
superclasse.
2. On alloue ensuite de l’espace à un objet appartenant à une
sous-classe de superclasse et on assigne l’adresse de cet
objet au pointeur. Supposons que la sous-classe se nomme
sous_classe.
3. On désire que le C++ choisisse une fonction membre, appelée
par exemple fonction_membre, sur la base de la sous-
classe sous_classe.
Dans ce cas, il faut:
1. Définir une fonction membre appelée fonction_membre
dans la classe superclasse
2. ajouter le mot réservé virtual devant le nom de
fonction_membre.
La fonction membre fonction_membre de superclasse et
toutes les versions de cette fonction dans les sous-classes de
superclasse sont appelées des fonctions membres virtuelles.

336 Une fois qu’une fonction membre reçoit le qualificatif virtual


dans une classe, toutes les versions de cette fonction des sous-
classes héritant de cette classe reçoivent automatiquement le
qualificatif virtual sans que le mot virtual n’ait à être ajouté
devant elles. Cependant, il est recommandé d’ajouter quand
même le mot virtual devant le nom des fonctions membres des
classes dérivées afin de bien marquer le caractère virtuel de
celles-ci.

Programmation en C++ IFT-19965 201


C
++

337 Si on modifie la définition des classes CIRCUIT, NUMERIQUE,


NAND_2 et NOR_2 pour rendre la fonction membre
affiche_type virtuelle, on obtient les définitions suivantes.
Pour la classe CIRCUIT:
//&% circuit_28_3.h

#include <iostream.h>

#ifndef CIRCUIT_FLAG_
#define CIRCUIT_FLAG_

class CIRCUIT {
private:
double v_num_plus; //Tension numerique positive (en volts)
double v_num_moins; //Tension numerique negative (en
volts)
double v_analog_plus; //Tension analogique positive (en
volts)
int numero; //Numero du composant

public:

// Constructeur par defaut

CIRCUIT () {
// cout << “Appel du constructeur defaut de CIRCUIT” <<
endl;
v_num_plus = 5.0;
v_num_moins = 0.0;
v_analog_plus = 12.0;
numero = 0;
}

// Constructeur avec initialisation des variables membres

CIRCUIT (double v_n_p, double v_n_m, double v_a_p, int no)


{
// cout << “Creation d’un objet CIRCUIT” << endl;
v_num_plus = v_n_p;
v_num_moins = v_n_m;
v_analog_plus = v_a_p;
numero = no;
}

// Reader de v_num_plus

double read_v_num_plus() {
return v_num_plus;

202 Programmation en C++ IFT-19965


C
++

// Reader de v_num_moins

double read_v_num_moins () {
return v_num_moins;
}

// Reader de v_analog_plus

double read_v_analog_plus () {
return v_analog_plus;
}

// Reader de numero

int read_numero () {
return numero;
}

// Writer de v_num_plus

void set_v_num_plus(double v) {
v_num_plus = v;
}

// Writer de v_num_moins

void set_v_num_moins(double v) {
v_num_moins = v;
}

// Writer de v_analog_plus

void set_v_analog_plus(double v) {
v_analog_plus = v;
}

// Writer de numero

void set_numero(int n) {
numero = n;
}

// Affichage du type de composant


virtual void affiche_type() {
cout << “Composante de circuit” << endl;}
};

#endif CIRCUIT_FLAG_

Programmation en C++ IFT-19965 203


C
++

Pour la classe NUMERIQUE:


//&% numerique_28_3.h
#include <iostream.h>
#include “circuit_28_3.h”

#ifndef NUMERIQUE_FLAG_
#define NUMERIQUE_FLAG_

class NUMERIQUE : public CIRCUIT {


private:
int fan_out; //Nombre admissible de sorties

public:

// Constructeur par defaut


NUMERIQUE () {

// cout << “Appel du constructeur defaut de NUMERIQUE” <<


endl;
fan_out = 10;
}

// Reader de fan_out
int read_fan_out() {
return fan_out;
}

// Writer de fan_out
void set_fan_out(int f_out) {
fan_out = f_out;
}

// Affichage du type de composant


virtual void affiche_type() {cout<< “Composante numerique”
<< endl;}
};

#endif NUMERIQUE_FLAG_

Pour la classe NAND_2:


//&% nand_2_28_3.h

#include <iostream.h>
#include “circuit_28_3.h”
#include “numerique_28_3.h”

#ifndef NAND_FLAG_
#define NAND_FLAG_

204 Programmation en C++ IFT-19965


C
++

class NAND_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1() {
return 1 - (in_1 * in_2);
}

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut


NAND_2() {
/* cout<< “Appel du constructeur defaut de NAND_2”
<< endl;*/
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}
// Affichage du type de composant

Programmation en C++ IFT-19965 205


C
++

virtual void affiche_type() {cout<< “Composante NAND_2”


<< endl;}
};

#endif NAND_FLAG_

Pour la classe NOR_2


//&% nor_2_28_3.h

#include <iostream.h>
#include “circuit_28_3.h”
#include “numerique_28_3.h”

#ifndef NOR_FLAG_
#define NOR_FLAG_

class NOR_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1() {
return ((1-in_1) * (1-in_2));
}

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut


NOR_2() {
/* cout<< “Appel du constructeur defaut de NOR_2”
<< endl;*/
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NOR_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;

206 Programmation en C++ IFT-19965


C
++

out_1 = set_out_1();
}

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}

// Affichage du type de composant


virtual void affiche_type() {cout<< “Composante NOR_2”
<< endl;}
};

#endif NOR_FLAG_

338 Avec ces nouvelles définitions de classes incluant les définitions


de fonctions membres virtuelles, le programme de rapport:
//&% it_fil_28_4.C

#include <iostream.h>
#include <stdlib.h>
#include <fstream.h>
#include “numerique_28_3.h”
#include “nand_2_28_3.h”
#include “nor_2_28_3.h”

void main() {

int i, compteur, type;


int in_1, in_2;
NUMERIQUE *num_ptr[100];

ifstream flot_in (“nand_dat_28_1.in”, ios::in);


if (flot_in == 0){
exit(0);
}
for (compteur = 0; flot_in >> type >> in_1 >> in_2 ;
++compteur) {

Programmation en C++ IFT-19965 207


C
++

if (type == 1) num_ptr[compteur] = new NAND_2;


else
if (type == 2) num_ptr[compteur] = new NOR_2;
else cout << “Erreur” << endl;

flot_in.close();

cout << compteur << “ objets crees. “ << endl << endl;
for (i = 0; i < compteur ; ++i) {
num_ptr[i]->affiche_type();
}

Les résultats de l’exécution du programme sont les suivants:

Résultat
3 objets crees.

Composante NAND_2
Composante NOR_2
Composante NAND_2

Le programme fonctionne maintenant correctement parce que


le choix des fonctions membres d’affichage affiche_type est
effectué au run-time.

339 Une fonction virtuelle d’une sous-classe d’une hiérarchie peut


en masquer une autre du même nom dans la superclasse. Dans
l’exemple ci-dessus, la fonction membre virtuelle
affiche_type des classes NAND_2 et NOR_2 masque la
fonction membre virtuelle de CIRCUIT et celle de NUMERIQUE.

340 Il peut arriver des cas où le programmeur n’a jamais l’intention


de déclarer des objets individuels de la classe NUMERIQUE. La
seule raison pour définir une fonction membre virtuelle
affiche_type dans cette classe est d’informer le compilateur
que cette fonction membre apparaît également dans les sous-

208 Programmation en C++ IFT-19965


C
++

classes NAND_2 et NOR_2. Si le programmeur omet,


délibérément ou non, de définir une fonction membre
affiche_type dans les classes NAND_2 ou NOR_2, alors, la
fonction membre virtuelle de la superclasse NUMERIQUE n’est
pas masquée et c’est celle-ci qui sera appelée dans le
programme de rapport sur le circuit.

341 Si le programmeur sait d’avance qu’une fonction membre


virtuelle d’une superclasse sera toujours masquée par une
fonction membre virtuelle dans chaque sous-classe héritant de
la superclasse, il convient alors de définir la fonction membre
virtuelle de la superclasse comme une fonction virtuelle
pure. La syntaxe a adopter dans ce cas est pour l’exemple de la
classe CIRCUIT:
virtual void affiche_type() = 0;

Cette façon de définir la fonction affiche_type dans la classe


CIRCUIT informe le compilateur que cette fonction membre est
virtuelle pure.

Résumé
342 ✔ Un pointeur qui est défini pour pointer à un objet d’une
superclasse peut aussi pointer à un objet appartenant à une
sous-classe de cette classe.
✔ Si on définit un tableau de pointeurs à des objets d’une classe
et que ces pointeurs pointent en réalité à des objets d’une sous-
classe, il faut définir des fonctions membres virtuelles si on
désire appeler ces fonctions membres.
✔ Une fonction membre virtuelle définie dans une classe est
automatiquement considérée comme étant virtuelle dans toutes
les sous-classes.

Programmation en C++ IFT-19965 209


C
++

210 Programmation en C++ IFT-19965


C
++

CHAPITRE 29 Les énoncés


conditionnels multiples

343 Nous savons comment utiliser l’énoncé if pour décider d’une


action à prendre en fonction du résultat d’un prédicat logique.
Cependant, l’imbrication de plusieurs énoncés if obscurcit
considérablement le code. L’énoncé switch facilite grandement
l’écriture d’expressions nécessitant plusieurs tests.

344 Le rôle de l’énoncé switch est de permettre l’exécution d’une


séquence particulière d’énoncés en fonction du résultat d’une
expression produisant une valeur entière (int).

345 La syntaxe de l’énoncé switch est la suivante:


switch (expression produisant une valeur entière) {
case constante entière 1: énoncés 1 break;
case constante entière 2: énoncés 2 break;
case constante entière 3: énoncés 3 break;
case constante entière 4: énoncés 4 break;
case constante entière 5: énoncés 5 break;
...
default : énoncé par défaut;
}

Quand le programme rencontre un énoncé switch, l’expression


est évaluée, produisant ainsi une valeur entière. Cette valeur

Programmation en C++ IFT-19965 211


C
++

est comparée aux constantes entières correspondant à chaque


mot réservé case. Lorsqu’il y a égalité entre les deux valeurs
entières, les énoncés correspondant sont exécutés jusqu’à ce
que le mot réservé break soit rencontré. A ce moment,
l’exécution de l’énoncé switch est complète et le programme
poursuit avec l’énoncé suivant. La ligne commençant avec le
mot réservé default est optionnelle. Si l’expression produit
une valeur entière qui ne peut être pairée avec aucune autre
valeur entière constante suivant les mots réservés case, alors
ce sont les énoncés correspondant à default qui sont exécutés.
Si la ligne default est omise, alors aucun énoncé n’est exécuté
s’il n’y a aucun pairage.

346 On peut maintenant profiter de l’énoncé switch pour


améliorer le programme de rapport en le récrivant comme suit:
//&% it_fil_29_1.C

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include “numerique_28_3.h”
#include “nand_2_28_3.h”
#include “nor_2_28_3.h”

void main() {

int i, compteur, type;


int in_1, in_2;
NUMERIQUE *num_ptr[100];

ifstream flot_in (“nand_dat_29_1.in”, ios::in);


if (flot_in == 0){
exit(0);
}
for (compteur = 0; flot_in >> type >> in_1 >> in_2 ;
++compteur) {
switch (type) {
case 1: num_ptr[compteur] = new NAND_2; break;
case 2: num_ptr[compteur] = new NOR_2; break;
default: cerr << “Erreur, piece “
<< type
<< “ non existante”
<< endl;
exit (0);
}

212 Programmation en C++ IFT-19965


C
++

flot_in.close();

cout << compteur << “ objets crees. “ << endl << endl;
for (i = 0; i < compteur ; ++i) {
num_ptr[i]->affiche_type();
}

On remarque plusieurs changements dans le programme de


rapport:
1. les énoncés if ont été remplacés par un switch
2. le fichier de données contenant les informations à analyser
porte le nom de nand_dat_29_1.in. Ce fichier contient une
donnée en erreur soit un type de pièce inexistant.
3. lorsque le programme rencontre une erreur, la partie
default de l’énoncé switch est exécutée et un message
d’erreur est transmis au flot d’affichage des erreurs du C++
appelé cerr. Le programme interrompt alors son exécution
pour revenir au système d’exploitation grâce à l’énoncé
exit(0) qui fait partie de la librairie stdlib.h du C++.
Cette librairie est incluse au programme grâce à l’instruction
de préprocesseur #include <stdlib.h>
Le fichier de données nand_dat_29_1.in contient les
informations suivantes:
1 1 1 2 0 1 1 1 0 6 1 1

L’exécution du programme donne le résultat suivant:


Erreur, piece 6 non existante

Le rapport ne s’imprime pas car une pièce en erreur est


détectée, ce qui provoque l’exécution du cas default de
l’énoncé switch. Cet énoncé contenant un exit(0), le
programme retourne immédiatement au système d’exploitation
sans afficher le rapport.

347 Revenons brièvement sur le flot cerr du C++. Lors de


l’exécution d’un programme, les messages sont généralement
communiqués à l’usager via le flot cout correspondant à

Programmation en C++ IFT-19965 213


++C
l’écran de l’ordinateur. Dans ce cas, les messages sont d’abord
placés dans un tampon qui se vide lorsqu’un symbole de fin de
ligne (endl) est rencontré. Par opposition, les messages
envoyés à cerr ne sont pas stockés dans un tampon mais
directement communiqués à l’usager, ce qui est plus immédiat.

Notion avancée
348 Si votre programme est appelé par un autre programme plutôt
que d’être appelé par une ligne de commande dans le système
d’exploitation, un énoncé exit provoque alors le retour
immédiat au programme d’appel. Il revient à ce programme de
décider des actions à prendre.

Notion avancée
349 Dans le programme de rapport, on utilise l’énoncé exit(0).Il
existe d’autres choix exit(i) qui ont diverses significations. Il
est hors du programme de ce cours d’aborder ces notions.

Résumé
350 ✔ L’énoncé switch permet de choisir parmi plusieurs actions
en fonction de la valeur entière produite par une expression.
✔ Dans un énoncé switch, l’expression peut produire un entier
appartenant à tous les types associés à des entiers soit int,
long int, char par exemple.
✔ Il est recommandé de pairer chaque mot réservé case avec
un mot réservé break pour éviter des bugs difficile à corriger.
✔ Si on désire que les message d’erreur s’affichent sans être
préalablement stockés dans un tampon, il suffit de les envoyer
au flot de données cerr du C++.
✔ Si on désire interrompre l’exécution d’un programme parce
qu’une erreur a été détectée, il suffit d’ajouter l’instruction
exit(0) dans le programme et d’inclure le fichier stdlib.h.

214 Programmation en C++ IFT-19965


C
++

CHAPITRE 30 Les énumérations en


C++

351 Dans ce chapitre, les énumérations sont présentées comme un


moyen de clarifier un programme en associant un mnémonique
avec un code numérique entier.

352 Nous avons vu que l’énoncé switch utilise des valeurs


constantes numériques entières pour choisir des actions à
prendre. Or, il est ennuyeux d’avoir à se rappeler la
signification de chaque valeur entière. Dans notre exemple de
rapport sur un circuit, la personne lisant le programme doit
savoir que le code entier 1 correspond à un type de porte
NAND_2 tandis que le code 2 correspond à un type NOR_2.

353 Un moyen d’associer un code numérique à un mnémonique


facile à retenir serait de déclarer des valeurs entières
constantes initialisées aux valeurs désirées:
const int nand_code = 1;
const int nor_code = 2;

354 Bien que rien n’empêche la pratique du paragraphe 353, il


existe un moyen encore plus simple d’associer un code

Programmation en C++ IFT-19965 215


C
++

numérique à un mnémonique soit l’utilisation d’un énoncé


d’énumération enum adoptant la syntaxe suivante:
enum { nand_2_code, nor_2_code};

Un tel énoncé déclare que les symboles entre crochets et


séparés par des virgules sont des constantes d’énumération et
assigne une valeur numérique entière à ces constantes. Par
défaut, la valeur numérique de la première constante
d’énumération (nand_2_code dans l’exemple ci-dessus) est 0.
La valeur de chaque constante suivante est la valeur
numérique suivante. Ici, nor_2_code prend donc la valeur 1.
D’après notre programme de rapport, les codes 0 pour
nand_2_code et 1 pour nor_2_code ne sont pas valides
puisque nous avons établi que nand_2_code devait prendre la
valeur 1 et nor_2_code devait prendre la valeur 2. On peut
heureusement modifier l’énoncé d’énumération pour tenir
compte de ceci:
enum {nand_2_code = 1, nor_2_code};

Le fait d’initialiser nand_2_code à un impose que les autres


constantes d’énumérations devront suivre à partir de cette
valeur.

355 Le programme de rapport peut donc être modifié pour utiliser


les énumérations1:
//&% it_fil_30_1.C

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include “numerique_28_3.h”
#include “nand_2_28_3.h”
#include “nor_2_28_3.h”

enum {nand_2_code = 1, nor_2_code};


void main() {

int i, compteur, type;


int in_1, in_2;

1. Notez que le fichier de données qui est lu est le fichier nand_28_1.in

216 Programmation en C++ IFT-19965


C
++

NUMERIQUE *num_ptr[100];

ifstream flot_in (“nand_dat_28_1.in”, ios::in);


if (flot_in == 0){
exit(0);
}
for (compteur = 0; flot_in >> type >> in_1 >> in_2 ;
++compteur) {
switch (type) {
case nand_2_code: num_ptr[compteur] = new NAND_2;
break;
case nor_2_code: num_ptr[compteur] = new NOR_2;
break;
default: cerr << “Erreur, piece “
<< type
<< “ non existante”
<< endl;
exit (0);
}

flot_in.close();

cout << compteur << “ objets crees. “ << endl << endl;
for (i = 0; i < compteur ; ++i) {
num_ptr[i]->affiche_type();
}

Résultat
3 objets crees.

Composante NAND_2
Composante NOR_2
Composante NAND_2

Notion avancée
356 On peut généraliser la syntaxe de l’énoncé enum comme suit:
enum {code_a, code_b, code_c, code_d = 5, code_e}

Programmation en C++ IFT-19965 217


C
++

Dans ce cas, on a les équivalences suivantes: code_a = 0,


code_b = 1, code_c = 2, code_d = 5, code_e = 6.

Notion avancée
357 Le C++ offre aussi la possibilité de définir des types de
données d’énumération. Ces types de données permettent
ensuite de définir des variables d’énumération. Par exemple,
la définition suivante crée un type d’énumération ayant pour
nom type_enum:
enum type_enum {code_a, code_b, code_c, code_d};

Par la suite, on peut créer la variable d’énumération type_var


de type d’énumération type_enum:
enum type_var type_enum;

Par exemple:
enum couleur {rouge,bleu,vert,blanc};
void affiche_couleur(couleur c){
switch(c) {
case rouge: cout << “rouge” << endl; break;
case bleu: cout << “bleu” << endl; break;
case vert: cout << “vert” << endl; break;
case blanc: cout << “blanc” << endl; break;
default: cout << “hamster” << endl; break;
}
}
main() {
couleur c1,c2,c3,c4;
c1 = rouge;
c2 = bleu;
c3 = vert;
c4 = 390;
affiche_couleur(c1);
affiche_couleur(c2);
affiche_couleur(c3);
affiche_couleur(c4);
}

Résumé
358 ✔ Les énumérations facilitent la lecture des programmes en
C++ en associant des constantes à des valeurs numériques
entières.

218 Programmation en C++ IFT-19965


C
++

CHAPITRE 31 Appel de constructeurs à


partir d’autres
constructeurs

359 Nous avons vu que les constructeurs sont des fonctions


membres qui sont appelées lors de la création d’objets
appartenant à une classe. Dans cette section nous verrons
comment le constructeur d’une classe peut appeler
explicitement le constructeur d’une autre classe. Cette manière
de procéder est intéressante parce qu’on peut par exemple
initialiser directement les variables membres d’une classe via
leur constructeur plutôt que d’appeler les fonctions membres
d’écriture-lecture.

360 Prenons par exemple la classe NUMERIQUE. Le constructeur par


défaut de cette classe initialise la variable membre fan_out à
la valeur 10. Il serait intéressant de pouvoir initialiser cette
variable membre à une valeur différente lors de la création
d’un objet appartenant à une classe héritant de NUMERIQUE
(NAND_2 par exemple) simplement via un constructeur avec
arguments. Ce constructeur pourrait prendre la forme
suivante:
// Constructeur avec arguments

NUMERIQUE (int fn_out) {


fan_out = fn_out;
}

Programmation en C++ IFT-19965 219


C
++

361 La classe NUMERIQUE devient alors:


//&% numerique_31_1.h
#include <iostream.h>
#include “circuit_28_3.h”

#ifndef NUMERIQUE_FLAG_
#define NUMERIQUE_FLAG_

class NUMERIQUE : public CIRCUIT {


private:
int fan_out; //Nombre admissible de sorties

public:

// Constructeur par defaut


NUMERIQUE () {

// cout << “Appel du constructeur defaut de NUMERIQUE” <<


endl;
fan_out = 10;
}

// Constructeur avec arguments


NUMERIQUE (int fn_out) {
fan_out = fn_out;
}

// Reader de fan_out
int read_fan_out() {
return fan_out;
}

// Writer de fan_out
void set_fan_out(int f_out) {
fan_out = f_out;
}

// Affichage du type de composant


virtual void affiche_type() {cout<< “Composante numerique”
<< endl;}
};

#endif NUMERIQUE_FLAG_

362 Si on désire par exemple que le constructeur de la classe


NAND_2 initialise la variable fan_out à la valeur 5 via le

220 Programmation en C++ IFT-19965


C
++

constructeur avec argument de la classe NUMERIQUE, il suffit


d’adopter la syntaxe suivante pour le constructeur par défaut
de la classe NAND_2:
NAND_2() : NUMERIQUE(5) {
/* cout<< “Appel du constructeur defaut de NAND_2”
<< endl;*/
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

Avec la syntaxe ci-dessus, lorsque le constructeur par défaut de


la classe NAND_2 est appelé, il appelle automatiquement le
constructeur avec arguments de la classe NUMERIQUE et lui
fournit la valeur numérique 5. On peut appeler plusieurs
constructeurs avec arguments à partir d’un constructeur par
défaut d’une classe dérivée (il faut cependant que ces oient des
constructeurs des classes situées immédiatement au dessus
dans la hiérarchie), il suffit simplement de les séparer par une
virgule:
classe_derivee : constructeur_1(arguments),
constructeur_2(arguments),...{
corps du constructeur par défaut de classe_derivee
}

Le programme de rapport peut être modifié pour afficher le


fan_out en tenant compte de la modification au constructeur
par défaut de la classe NAND_2:
//&% it_fil_31_1.C

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include “numerique_31_1.h”
#include “nand_2_31_1.h”
#include “nor_2_28_3.h”

enum {nand_2_code = 1, nor_2_code};


void main() {

int i, compteur, type;


int in_1, in_2;
NUMERIQUE *num_ptr[100];

ifstream flot_in (“nand_dat_28_1.in”, ios::in);


if (flot_in == 0){

Programmation en C++ IFT-19965 221


C
++

exit(0);
}

for (compteur = 0; flot_in >> type >> in_1 >> in_2 ;


++compteur) {
switch (type) {
case nand_2_code: num_ptr[compteur] = new NAND_2;
break;
case nor_2_code: num_ptr[compteur] = new NOR_2;
break;
default: cerr << “Erreur, piece “
<< type
<< “ non existante”
<< endl;
exit (0);
}

flot_in.close();

cout << compteur << “ objets crees. “ << endl << endl;
for (i = 0; i < compteur ; ++i) {
num_ptr[i]->affiche_type();
cout << “fan_out: “ << num_ptr[i]->read_fan_out() <<
endl;
}

Résultat
3 objets crees.

Composante NAND_2
fan_out: 5
Composante NOR_2
fan_out: 10
Composante NAND_2
fan_out: 5

On remarque que le fan_out est de 5 pour les composantes


NAND_2 et de 10 pour les composantes de type NOR_2 puisque
le constructeur de cette dernière classe n’a pas été modifié pour
appeler le constructeur avec arguments de la classe

222 Programmation en C++ IFT-19965


C
++

NUMERIQUE. Le constructeur par défaut est par conséquent


appelé et le fan_out est initialisé à 10.

363 L’appel d’un constructeur par un autre constructeur est aussi


possible lorsqu’un objet contient des variables membres qui
appartiennent à une classe conçue par l’utilisateur. Dans ce
cas, il est nécessaire d’initialiser les objets membres avec leur
constructeur à partir du constructeur de la classe contenant ces
objets:
(ex:constructeur():objet(param1,param2...,paramn)).

Résumé
364 ✔ Il est possible d’appeler un constructeur qui en appelle un
autre explicitement.

Programmation en C++ IFT-19965 223


C
++

224 Programmation en C++ IFT-19965


C
++

CHAPITRE 32 Fonctions membres


appelant d’autres
fonctions membres

365 Dans la définition de la classe NAND_2, nous avons abordé, sans


nous y arrêter, un sujet très important en C++ soit l’appel de
fonctions membres par d’autres fonctions membres d’une classe
seule ou d’une classe appartenant à une hiérarchie. Revenons
sur la définition de la classe NAND_2 et, plus spécifiquement
sur le constructeur par défaut et sur la fonction membre
set_out_1. Le constructeur par défaut est, pour la classe
NAND_2:
NAND_2() : NUMERIQUE(5) {
/* cout<< “Appel du constructeur defaut de NAND_2”
<< endl;*/
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

et la fonction set_out_1 est:


int set_out_1() {
return 1 - (in_1 * in_2);
}

Ce segment de code montre qu’une fonction membre d’une


classe, ici le constructeur par défaut de la classe NAND_2, peut
appeler une fonction membre de la même classe, ici
set_out_1. Il y a cependant une chose très importante qu’il

Programmation en C++ IFT-19965 225


C
++

faut remarquer: lorsqu’une fonction membre appelle une


fonction membre de la même classe (ou d’une classe dont elle
hérite) il n’est pas nécessaire d’utiliser l’opérateur d’accès aux
membres “.” comme nous en avons l’habitude pour accéder aux
membres d’une classe.

366 La raison de ce fait est que comme l’argument de la classe (par


exemple un objet donné nnd) et l’opérateur d’accès aux
membres “.” sont absent de l’appel de set_out_1 dans
NAND_2(), l’objet par défaut pour lequel set_out_1 est appelé
est le même que celui pour lequel NAND_2 est appelé, soit l’objet
courant. Cet objet est donc un argument implicite. L’opérateur
d’accès aux membres “.” est lui aussi implicite dans l’appel de
la fonction membre set_out_1 par NAND_2().

Notion avancée
367 L’argument implicite dont il est fait mention au paragraphe
366 est en fait un argument appelé le pointeur this. Chaque
fonction membre d’une classe possède un argument implicite
appelé this dont la valeur est un pointeur à l’objet
correspondant à la classe de cette fonction membre. Par
exemple, lorsqu’on accède à la fonction membre lire_in_1()
pour un objet nnd de la classe NAND_2 de la façon suivante:
nnd.lire_in_1();

le pointeur this pointe simplement à l’adresse de l’objet nnd.


Cela signifie que, dans l’appel de la fonction set_out_1 dans
le constructeur NAND_2, la syntaxe suivante serait aussi
acceptable:
NAND_2() : NUMERIQUE(5) {
/* cout<< “Appel du constructeur defaut de NAND_2”
<< endl;*/
in_1 = 0; in_2 = 0;
out_1 = (*this).set_out_1();
}

ce qui signifie simplement que set_out_1 est appelée pour


l’objet (*this) qui est, implicitement, l’objet NAND_2.
Evidemment, la syntaxe suivante serait aussi valide:
NAND_2() : NUMERIQUE(5) {

226 Programmation en C++ IFT-19965


C
++

/* cout<< “Appel du constructeur defaut de NAND_2”


<< endl;*/
in_1 = 0; in_2 = 0;
out_1 = this->set_out_1();
}

Heureusement, le C++ permet, par l’inclusion implicite du


paramètre this et de l’opérateur d’accès aux membres “.” (ou
->) de ne pas avoir à écrire explicitement ces opérateurs.

Résumé
368 ✔ Si on veut appeler une fonction membre public pour un
objet spécifique, il suffit d’utiliser l’opérateur d’accès aux
membres “.” de la façon suivante:
nom_objet.nom_fonction(arguments).
✔ Si on veut appeler une fonction membre à l’intérieur d’une
fonction membre, il suffit simplement d’adopter la syntaxe
suivante: nom_fonction(arguments).

Programmation en C++ IFT-19965 227


C
++

228 Programmation en C++ IFT-19965


C
++

CHAPITRE 33 Les variables privées


(private) et protégées
(protected)

369 Un principe fondamental en conception de logiciel est


d’empêcher autant que possible les usagers d’un programme de
corrompre les données par erreur ou d’utiliser des fonctions
incorrectement. Dans ce chapitre, nous allons nous arrêter plus
longuement à la notion de données et de fonctions privées
identifiées par le mot réservé private et nous allons
également aborder le concept de données et fonctions membres
protégées identifiées par le mot réservé protected.

370 Nous avons vu jusqu’à maintenant que les classes peuvent


renfermer une partie privée identifiée par le mot réservé
private et une partie publique identifiée par le mot réservé
public. Nous avons aussi vu que le contenu des variables
membres privées ne peut être consulté ou modifié que par le
biais de fonctions membres public appartenant à la même
classe. Par exemple, dans la classe NAND_2, nous avons jugé
plus prudent de placer la variable membre out_1 dans la
partie privée de la classe étant donné qu’il est préférable
d’éviter de donner un accès direct à cette variable membre .

371 Afin d’illustrer les principes de partie protégée (protected) et


privée (private) des classes dans une hiérarchie, nous allons

Programmation en C++ IFT-19965 229


C
++

temporairement abandonner l’exemple du rapport sur un


circuit numérique pour prendre des exemples plus génériques.

372 Supposons par exemple que la classe au haut d’une hiérarchie


de classes se nomme super_classe et ait la structure
suivante:
//&% sup_cls_33_1.h
#include <iostream.h>
#ifndef SUP_CLS_FLAG
#define SUP_CLS_FLAG

class super_classe {
public:
super_classe() {} //Constructeur par defaut
super_classe(double a, double b) {
sup_a = a; sup_b = b;
}
// Fonction membre calculant le produit a*b
double sup_produit(){
return sup_a * sup_b;
}
private:
double sup_a, sup_b;
};
#endif SUP_CLS_FLAG

373 Supposons également qu’une classe héritant de super_classe


porte le nom de sous_classe et ait la structure suivante:
//&% sous_cls_33_1.h
#include <iostream.h>
#ifndef SOUS_CLS_FLAG
#define SOUS_CLS_FLAG

class sous_classe : public super_classe {


public:
sous_classe() {}
sous_classe(double c) {
sous_c = c;
}
private:
double sous_c;
};
#endif SOUS_CLS_FLAG

230 Programmation en C++ IFT-19965


C
++

374 Comme les variables membres de la super-classe


super_classe sont privées et que seul le constructeur par
défaut ou le constructeur avec arguments ont accès à ces
variables, il n’y a aucun moyen que la classe sous_classe ait
accès aux variables membres de super_classe. Une
hiérarchie comme celle de super_classe et sous_classe est
peu utile parce que malgré que sous_classe hérite de
super_classe, elle ne peut utiliser aucun des éléments de
super_classe, sauf peut-être les constructeurs.

375 Un moyen simple de permette à sous_classe d’avoir accès


aux variables membres de super_classe est, comme nous
l’avons vu, de définir une fonction membre de lecture pour
chaque variable membre de super_classe:
//&% sup_cls_33_2.h
#include <iostream.h>
#ifndef SUP_CLS_FLAG_
#define SUP_CLS_FLAG_

class super_classe {

public:
super_classe() {} //Constructeur par defaut
super_classe(double a, double b) {
sup_a = a; sup_b = b;
}

// Fonction membre calculant le produit a*b


double sup_produit(){
return sup_a * sup_b;
}

double lire_sup_a() {return sup_a;}


double lire_sup_b() {return sup_b;}

private:
double sup_a, sup_b;

};
#endif SUP_CLS_FLAG_

Comme les fonctions membres de lecture lire_sup_a() et


lire_sup_b() sont définies dans la partie public de
super_classe, elles sont directement accessibles de

Programmation en C++ IFT-19965 231


C
++

sous_classe parce que sous_classe hérite de


super_classe. Cependant, comme aucune fonction membre
d’écriture n’a été définie dans super_classe, les variables
membres de super_classe ne peuvent être modifiées par des
fonctions de sous_classe. Seules des fonctions membres de
super_classe peuvent modifier les variables private de
super_classe. Une fonction de la classe sous_classe ne
peut pas modifier sup_a et sup_b. Le seul moyen de modifier
ces variables à partir de sous_classe est de définir des
fonctions membres d’écriture public dans super_classe.

376 En plus des fonctions membres de lecture, il existe un autre


moyen offert par le C++ pour accéder aux variables membres
de la classe super_classe à partir de la classe sous_classe:
il suffit simplement de déplacer les variables membres sup_a
et sup_b de super_classe dans sa partie protégée
(protected):
//&% sup_cls_33_3.h
#include <iostream.h>
#ifndef SUP_CLS_FLAG_
#define SUP_CLS_FLAG_

class super_classe {

public:
super_classe() {} //Constructeur par defaut
super_classe(double a, double b) {
sup_a = a; sup_b = b;
}

// Fonction membre calculant le produit a*b


double sup_produit(){
return sup_a * sup_b;
}

protected:

double sup_a, sup_b;

};
#endif SUP_CLS_FLAG_

Les variables membres et les fonctions membres placées dans


la partie protected d’une classe sont accessibles des fonctions

232 Programmation en C++ IFT-19965


C
++

membres définies dans la classe ou dans les sous-classes de


cette classe. L’accès par toute autre fonction est interdit.

377 Par exemple, si on définit une fonction membre somme() dans


la classe sous_classe qui retourne la somme des variables
membres sup_a et sup_b, on pourra obtenir un résultat
valable qui sera accepté lors de la compilation. Les classes sont
maintenant:
//&% sup_cls_33_3.h

#include <iostream.h>

#ifndef SUP_CLS_FLAG_
#define SUP_CLS_FLAG_

class super_classe {

public:

super_classe() {} //Constructeur par defaut

super_classe(double a, double b) {

sup_a = a; sup_b = b;
}

// Fonction membre calculant le produit a*b

double sup_produit(){

return sup_a * sup_b;


}

protected:
double sup_a, sup_b;

};

#endif SUP_CLS_FLAG_

pour la classe super_classe


//&% sous_cls_33_2.h

#include <iostream.h>
#include sup_cls_33_3.h

Programmation en C++ IFT-19965 233


C
++

#ifndef SOUS_CLS_FLAG_
#define SOUS_CLS_FLAG_

class sous_classe : public super_classe {

public:

sous_classe() : super_classe(3.0,9.0) {}

sous_classe(double c) {

sous_c = c;
}

double somme() {return (sup_a + sup_b);}

private:
double sous_c;
};
#endif SOUS_CLS_FLAG_

pour la classe sous_classe.


Un programme de test serait par exemple:
//&% prot_33_1.C
#include <iostream.h>
#include “sup_cls_33_3.h”
#include “sous_cls_33_2.h”

void main () {
sous_classe objet_1;
cout << objet_1.somme() << endl;
}

Résultat
12

378 Si maintenant on désire que l’information sur les variables


membres sup_a et sup_b soint accessible aux fonctions
membres définies dans la classe sous_classe mais qu’on
souhaite que cette information ne soit pas accessible de façon

234 Programmation en C++ IFT-19965


C
++

générale ni que des fonctions définies à l’extérieur de la classe


super_classe puissent modifier les valeurs de sup_a et
sup_b, il suffit de bien utiliser les différentes parties de la
classe super_classe. Ici, il faut procéder de la façon suivante:
1. Placer les variables sup_a et sup_b dans la partie private
de la définition de super_classe.
2. Placer les fonctions membres de lecture pour les variables
sup_a et sup_b dans la partie protected de la classe
super_classe.
En procédant de la sorte, on aura le comportement suivant
pour les classes super_classe et sous_classe:
1. Les variables sup_a et sup_b seront directement accessibles
en lecture et en écriture seulement par les fonctions mem-
bres définies dans la classe super_classe étant donné
qu’elles sont dans la partie private.
2. Les fonctions membres de lecture placées dans la partie
protected de super_classe pourront être utilisées dans
un objet de la classe sous_classe pour prendre connais-
sance du contenu de sup_a et sup_b puisque les membres
ayant le statut protected sont accessibles dans la classe où
elles sont définies et dans les classes dérivées de cette classe.
Comme il n’y a pas de fonction membre d’écriture, sup_a et
sup_b ne peuvent être modifiées par l’utilisation de telles
fonctions.
3. Les autres fonctions (par exemple dans le programme princi-
pal ou autre) n’ont aucun accès aux variables sup_a et
sup_b. Celles-ci sont donc complètement isolées du monde
extérieur.

379 On voit donc que par une utilisation judicieuse des différentes
parties d’une classe (public, protected, private) on peut
contrôler l’accès aux variables membres de façon très précise.

Programmation en C++ IFT-19965 235


C
++

380 Le schéma suivant résume les concepts reliés à l’accès des


membres définis dans une classe:

super_classe Les variables et fonctions


membres privées de la
classe super_classe ne
sont disponibles qu’aux
fonctions membres définies
dans la classe super_classe
private

Les variables et fonctions


sous_classe membres dans la partie
protégée de super_classe
ne sont disponible qu’aux
fonctions membres définies
dans super_classe et
sous_classe
protected

Les variables et fonctions


membres public de super_classe
sont accessibles à toute
fonction membre ou autre
public de partout dans le programme

381 Le rectangle intérieur est celui qui impose le plus de


contraintes pour l’accès aux variables et fonctions membres de
super_classe. Le rectangle extérieur est celui qui, pour sa
part, impose le moins de contraintes.

Résumé
382 ✔ Pour limiter l’accès aux variables et fonctions membres d’une
classe seulement aux fonctions définies dans la même classe, il
suffit de placer ces variables et fonctions membres dans la
partie private de la classe.
✔ Pour limiter l’accès aux variables et fonctions membres d’une
classe seulement aux fonctions définies dans la même classe ou
dans une sous-classe de la classe, il suffit de placer ces
variables et fonctions membres dans la partie protected de la
classe.

236 Programmation en C++ IFT-19965


C
++

✔ Pour ne pas limiter l’accès aux variables et fonctions


membres d’une classe, il suffit de placer ces variables et
fonctions membres dans la partie public de la classe.
✔ Pour éviter une modification des variables membres d’une
classe par une fonction d’une classe dérivée tout en permettant
la consultation du contenu de ces variables, il suffit de définir
les variables membres dans la partie private de la classe et de
placer les fonctions membres de lecture dans la partie
protected.

Programmation en C++ IFT-19965 237


C
++

238 Programmation en C++ IFT-19965


C
++

CHAPITRE 34 Les dérivations de


classes protected et
private

383 Dans le CHAPITRE 33, nous avons vu qu’il est possible de


contrôler l’accès aux variables membres d’une classe grâce à la
partie1 de la classe dans laquelle elles sont définies.

384 Le présent chapitre montre comment l’accès aux variables et


fonctions membres peut être encore plus contrôlé à l’endroit où
une sous-classe hérite de ces membres via les dérivations
private, protected ou public.

385 Jusqu’à maintenant, nous n’avons utilisé que des dérivations


de type public comme le montre l’exemple de définition de la
classe sous_classe au paragraphe 377
//&% sous_cls_33_2.h
#include <iostream.h>
#ifndef SOUS_CLS_FLAG_
#define SOUS_CLS_FLAG_

class sous_classe : public super_classe {

1. private, protected, public

Programmation en C++ IFT-19965 239


++ C
public:

sous_classe() : super_classe(3.0,9.0) {}

sous_classe(double c) {

sous_c = c;
}

double somme() {return (sup_a + sup_b);}

private:
double sous_c;
};
#endif SOUS_CLS_FLAG_

386 Si on change la dérivation public pour une dérivation


protected, chaque variable membre ou fonction membre
public de super_classe se comporte comme si elle était
dans la partie protected de la classe sous_classe.
Reprenons les définitions des classes super_classe et
sous_classe du CHAPITRE 33 et modifions la dérivation
pour la rendre protected, les variables membres de la classe
super_classe résidant dans la partie public de la classe:
//&% sup_cls_34_1.h

#include <iostream.h>

#ifndef SUP_CLS_FLAG_
#define SUP_CLS_FLAG_

class super_classe {

public:

super_classe() {} //Constructeur par defaut

super_classe(double a, double b) {

sup_a = a; sup_b = b;

// Fonction membre calculant le produit a*b

double sup_produit(){

240 Programmation en C++ IFT-19965


C
++

return sup_a * sup_b;

double sup_a, sup_b;

};

#endif SUP_CLS_FLAG_

et pour sous_classe:
//&% sous_cls_34_1.h

#include <iostream.h>

#ifndef SOUS_CLS_FLAG_
#define SOUS_CLS_FLAG_

class sous_classe : protected super_classe {

public:

sous_classe() : super_classe(3.0,9.0) {}

sous_classe(double c) {

sous_c = c;

double somme() {return (sup_a + sup_b);}

private:
double sous_c;
};
#endif SOUS_CLS_FLAG_

Dans cette forme de dérivation, les variables sup_a et sup_b


dont hérite la classe sous_classe se comportent comme si
elles étaient dans la partie protected de la classe
sous_classe. Elles sont donc accessibles des fonctions
membres de sous_classe de même que des fonctions de
classes dérivées de sous_classe.

Programmation en C++ IFT-19965 241


C
++

387 Un moyen extrême de limiter l’accès aux variables membres


serait de définir un héritage private pour la classe
sous_classe. Dans ce cas, les variables membres sup_a et
sup_b se comporteraient comme si elles avaient été définies
comme étant private dans sous_classe.

388 En somme, l’effet des dérivations protected et private est


d’élever la statut des variables membres et des fonctions
membres de la classe supérieure (ici super_classe):
1. une dérivation protected rend les variables membres et les
fonctions membres public de la superclasse protected si
celles-ci sont définies comme public.
2. une dérivation private rend les variables membres et les
fonctions membres de la superclasse private si celles-ci
sont définies comme public ou protected.

Résumé
389 La table ci-dessous résume les effets des divers types de
dérivations:

Dérivation Dérivation Dérivation


public protected private
Fonction membre public reste public devient protected devient private
Fonction membre protected reste protected reste protected devient private
Fonction membre private reste private reste private reste private

242 Programmation en C++ IFT-19965


C
++

CHAPITRE 35 Les fonctions qui


retournent des chaînes
de caractères

390 Dans ce chapitre, nous abordons la notion du retour d’une


chaîne de caractères par une fonction. Nous revenons au
programme de rapport sur le circuit formé de portes NAND et
NOR.

391 Analysons à nouveau le programme de rapport:


//&% it_fil_35_1.C

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include “numerique_31_1.h”
#include “nand_2_31_1.h”
#include “nor_2_28_3.h”

enum {nand_2_code = 1, nor_2_code};


void main() {

int i, compteur, type;


int in_1, in_2;
NUMERIQUE *num_ptr[100];

ifstream flot_in (“nand_dat_28_1.in”, ios::in);


if (flot_in == 0) {
exit(0);

Programmation en C++ IFT-19965 243


C
++

}
for (compteur = 0; flot_in >> type >> in_1 >> in_2 ;
++compteur) {
switch (type) {
case nand_2_code: num_ptr[compteur] = new NAND_2;
break;
case nor_2_code: num_ptr[compteur] = new NOR_2;
break;
default: cerr << “Erreur, piece “
<< type
<< “ non existante”
<< endl;
exit (0);
}

flot_in.close();

cout << compteur << “ objets crees. “ << endl << endl;
for (i = 0; i < compteur ; ++i) {
num_ptr[i]->affiche_type();
}

On remarque que dans la boucle d’affichage du rapport (la


dernière boucle for), l’envoi du type de porte est sous la
responsabilité de l’instruction:
num_ptr[i]->affiche_type();

ce qui est peu élégant parce que l’information comme quoi une
donnée est envoyée au flot cout est cachée dans la fonction
affiche_type(). Il serait intéressant de faire en sorte que
l’instruction d’envoi de données à cout apparaisse
explicitement dans le programme principal. Un programmeur
devant analyser le programme peut ainsi voir facilement où se
trouve l’instruction d’affichage sans avoir à consulter la
définition de la fonction affiche_type() dans les classes
NAND_2 et NOR_2.

392 Pour rendre explicite l’affichage du type de composante, il faut


donc modifier la fonction affiche_type() pour que son
utilisation soit la suivante:

244 Programmation en C++ IFT-19965


C
++

cout << num_ptr[i]->affiche_type();

Cela implique que la fonction modifiée affiche_type() doit


maintenant retourner une chaîne de caractères pour que le
compilateur accepte d’envoyer les données retournées à cout1.
Pour ce faire, il faut d’abord introduire les concepts importants
sur les chaînes de caractères.

393 Nous avons déjà vu qu’en C++, une chaîne de caractères est
représentée par un ensemble de caractères alphanumériques
insérés entre guillemets. Par exemple “porte nand” est une
chaîne de caractères valide. Lorsqu’on délimite une chaîne de
caractères par des guillemets, on commande au compilateur
C++ de créer un tableau dans lequel les éléments sont
justement les caractères contenus entre les guillemets. Le C++
stocke une chaîne de caractères de la façon suivante dans la
mémoire de l’ordinateur:

p o r t e n a n d \0
0111000 01101111 01110010 01110100 01100101 00100000 01101110 01100001 01101110 01100100 00000000

0 1 2 3 4 5 6 7 8 9 10

Indices du tableau

Les 10 caractères de la chaîne de caractères “porte nand”


sont stockés dans un tableau de 11 éléments. Le dernier
élément du tableau est le caractère nul2 marquant la fin de la
chaîne. Le caractère nul est noté \0 pour ne pas le confondre
avec le symbole pour représenter le chiffre zéro. Donc, les
tableaux de caractères se terminant par le caractère nul
sont appelés chaînes de caractères.

394 La raison pour laquelle les chaînes de caractères se terminent


par le caractère nul est que les fonctions de manipulation de

1. Remarquez qu’il est possible de surdéfinir l’opérateur << cependant, nous allons voir cette
notion avancée dans un prochain chapitre.
2. “null character” en anglais.

Programmation en C++ IFT-19965 245


C
++

chaînes de caractères, tant celles qui sont disponibles dans la


librairie du C++ que celles qui sont conçues par l’usager, n’ont
souvent aucun moyen de savoir la longueur de la chaîne. Le
repérage du caractère nul est donc un moyen judicieux de
repérer la fin de la chaîne. A titre d’exemple, l’opérateur
d’insertion << dépend du caractère nul lorsqu’il traite les
chaînes de caractères.

395 Lorsqu’une chaîne de caractères apparaît dans une expression,


la valeur de cette chaîne est le pointeur au premier élément
dans le tableau correspondant. Par exemple, la valeur de la
chaîne “porte nand” est simplement la valeur du pointeur
marquant le début de la chaîne dans la mémoire de
l’ordinateur.

396 La déclaration suivante crée une variable de type pointeur de


caractère et fait en sorte que la valeur de cette variable soit le
pointeur au début de la chaîne de caractère:
char *pointeur_carac = “porte_nand”;

397 Il n’y a donc pas de type de données appelé chaîne de


caractères. Lorsqu’une chaîne de caractère est fournie à une
fonction, ce qu’on fournit en définitive est le pointeur de
caractères correspondant au premier élément de la chaîne. Par
conséquent, lorsqu’on désire retourner une chaîne de
caractères d’une fonction, il faut retourner le pointeur au
premier élément de la chaîne.

398 Dans l’exemple qui nous intéresse, il faut modifier la définition


de la fonction affiche_type() de la façon suivante:
virtual char * affiche_type(){ return “Composante NAND_2”;}

dans la classe NAND_2 et de la façon suivante:


virtual char * affiche_type(){ return “Composante NOR_2”;}

dans la classe NOR_2.

246 Programmation en C++ IFT-19965


C
++

399 Avec ces nouvelles notions, voyons maintenant comment le


programme de rapport peut être modifié. La classe NAND_2
devient:
//&% nand_2_35_1.h

#include <iostream.h>
#include “circuit_35_1.h”
#include “numerique_35_1.h”

#ifndef NAND_FLAG_
#define NAND_FLAG_

class NAND_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1() {
return 1 - (in_1 * in_2);
}

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut


NAND_2() {
/* cout<< “Appel du constructeur defaut de NAND_2”
<< endl;*/
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

Programmation en C++ IFT-19965 247


C
++

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}
// Affichage du type de composant
virtual char *affiche_type() {return “Composante NAND_2”;}
};

#endif NAND_FLAG_

La classe NOR_2 devient:


//&% nor_2_35_1.h

#include <iostream.h>
#include “circuit_35_1.h”
#include “numerique_35_1.h”

#ifndef NOR_FLAG_
#define NOR_FLAG_

class NOR_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1() {
return ((1-in_1) * (1-in_2));
}

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut


NOR_2() {
/* cout<< “Appel du constructeur defaut de NOR_2”
<< endl;*/
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

248 Programmation en C++ IFT-19965


C
++

//Constructeur avec deux arguments


NOR_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

int lire_in_1() {
return in_1;
}

void ecrire_in_1(int val) {


in_1 = val;
out_1 = set_out_1();
}

int lire_in_2() {
return in_2;
}

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

int lire_out_1() {
return out_1;
}

// Affichage du type de composant


virtual char *affiche_type() {return “Composante NOR_2”;}
};

#endif NOR_FLAG_

La classe NUMERIQUE devient:


//&% numerique_35_1.h
#include <iostream.h>
#include “circuit_35_1.h”

#ifndef NUMERIQUE_FLAG_
#define NUMERIQUE_FLAG_

class NUMERIQUE : public CIRCUIT {


private:
int fan_out; //Nombre admissible de sorties

public:

// Constructeur par defaut

Programmation en C++ IFT-19965 249


++C
NUMERIQUE () {

// cout << “Appel du constructeur defaut de NUMERIQUE” <<


endl;
fan_out = 10;
}

// Constructeur avec arguments


NUMERIQUE (int fn_out) {
fan_out = fn_out;
}

// Reader de fan_out
int read_fan_out() {
return fan_out;
}

// Writer de fan_out
void set_fan_out(int f_out) {
fan_out = f_out;
}

// Affichage du type de composant


virtual char *affiche_type() {return “Composante
numerique”;}
};

#endif NUMERIQUE_FLAG_

et finalement, la classe CIRCUIT devient:


//&% circuit_35_1.h

#include <iostream.h>

#ifndef CIRCUIT_FLAG_
#define CIRCUIT_FLAG_

class CIRCUIT {
private:
double v_num_plus; //Tension numerique positive (en volts)
double v_num_moins; //Tension numerique negative (en
volts)
double v_analog_plus; //Tension analogique positive (en
volts)
int numero; //Numero du composant

public:

// Constructeur par defaut

250 Programmation en C++ IFT-19965


C
++

CIRCUIT () {
// cout << “Appel du constructeur defaut de CIRCUIT” <<
endl;
v_num_plus = 5.0;
v_num_moins = 0.0;
v_analog_plus = 12.0;
numero = 0;
}

// Constructeur avec initialisation des variables membres

CIRCUIT (double v_n_p, double v_n_m, double v_a_p, int no)


{
// cout << “Creation d’un objet CIRCUIT” << endl;
v_num_plus = v_n_p;
v_num_moins = v_n_m;
v_analog_plus = v_a_p;
numero = no;
}

// Reader de v_num_plus

double read_v_num_plus() {
return v_num_plus;
}

// Reader de v_num_moins

double read_v_num_moins () {
return v_num_moins;
}

// Reader de v_analog_plus

double read_v_analog_plus () {
return v_analog_plus;
}

// Reader de numero

double read_numero () {
return numero;
}

// Writer de v_num_plus

void set_v_num_plus(double v) {
v_num_plus = v;
}

Programmation en C++ IFT-19965 251


C
++

// Writer de v_num_moins

void set_v_num_moins(double v) {
v_num_moins = v;
}

// Writer de v_analog_plus

void set_v_analog_plus(double v) {
v_analog_plus = v;
}

// Writer de numero

void set_numero(int n) {
numero = n;
}

// Affichage du type de composant


virtual char *affiche_type() {return “Composante de
circuit”;}

};

#endif CIRCUIT_FLAG_

Le programme de rapport est maintenant plus explicite parce


que l’impression du type de composante est maintenant
directement fourni à l’opérateur d’insertion:
//&% it_fil_35_1.C

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include “numerique_35_1.h”
#include “nand_2_35_1.h”
#include “nor_2_35_1.h”

enum {nand_2_code = 1, nor_2_code};


void main() {

int i, compteur, type;


int in_1, in_2;
NUMERIQUE*num_ptr[100];

ifstream flot_in (“nand_dat_28_1.in”, ios::in);


if (flot_in == 0){exit(0);

252 Programmation en C++ IFT-19965


C
++

}
for (compteur = 0; flot_in >> type >> in_1 >> in_2 ;
++compteur) {
switch (type) {
case nand_2_code: num_ptr[compteur] = new NAND_2;
break;
case nor_2_code: num_ptr[compteur] = new NOR_2;
break;
default: cerr << “Erreur, piece “
<< type
<< “ non existante”
<< endl;
exit (0);
}

flot_in.close();

cout << compteur << “ objets crees. “ << endl << endl;
for (i = 0; i < compteur ; ++i) {
cout << num_ptr[i]->affiche_type() << endl;
}

Résultat
3 objets crees.

Composante NAND_2
Composante NOR_2
Composante NAND_2

Résumé
400 ✔ Les chaînes de caractères sont stockées dans un tableau à
une dimension et se terminent par le caractère nul.
✔ Pour déclarer une variable dont la valeur est une chaîne de
caractères, il suffit d’adopter la syntaxe suivante: char
*var_str = “chaine”.
✔ Pour qu’une fonction retourne une chaîne de caractères, il
suffit d’adopter la syntaxe suivante: char* nom_fonction...

Programmation en C++ IFT-19965 253


C
++

254 Programmation en C++ IFT-19965


C
++

CHAPITRE 36 Le passage des


paramètres par référence

401 Jusqu’à maintenant, nous avons vu qu’on pouvait passer des


paramètres à une fonction par valeur, c’est-à-dire que la
variable passée comme paramètre était d’abord copiée dans
une variable temporaire. Cette variable temporaire était
utilisée par la fonction et ensuite détruite au retour de celle-ci.
La valeur de la variable passée en paramètre demeurait ainsi
inchangée puisque la fonction ne travaillait que sur la copie.

402 Il survient cependant des situations où il faut passer un objet à


une fonction membre comme un argument ordinaire plutôt
que comme un argument implicite via les opérateurs -> ou .
(l’opérateur d’accès aux membres). Si par exemple, on veut
définir une fonction membre qui reçoit deux objets en
arguments, un seul au maximum peut être fourni via l’un des
deux opérateurs ci-dessus. Finalement, si on veut surdéfinir
un opérateur, notion qui sera approfondie plus loin, il faut
pouvoir passer des objets en arguments aux fonctions
(membres ou ordinaires) puisque les opérandes d’un opérateur
ne peuvent être fournies via -> ou . (l’opérateur d’accès aux
membres).

Programmation en C++ IFT-19965 255


C
++

403 Afin de mieux comprendre les concepts exposés dans ce


chapitre, nous allons développer les autres classes de la
hiérarchie présentée à la FIGURE 3 du CHAPITRE 16, plus
particulièrement les classes ANALOGIQUE, RESISTANCE et
CONDENSATEUR. Nous allons également modifier la classe
CIRCUIT pour inclure une fonction de calcul du courant
traversant une composante étant donné une tension
d’alimentation analogique donnée.

404 La définition de la classe CIRCUIT est la suivante:


//&% circuit_36_1.h

#include <iostream.h>

#ifndef CIRCUIT_FLAG_
#define CIRCUIT_FLAG_

class CIRCUIT {
private:
double v_num_plus; //Tension numerique positive (en volts)
double v_num_moins; /*Tension numerique negative (en
volts)*/
double v_analog_plus; /*Tension analogique positive
(en volts)*/
int numero; //Numero du composant

public:

// Constructeur par defaut

CIRCUIT () {
v_num_plus = 5.0;
v_num_moins = 0.0;
v_analog_plus = 12.0;
numero = 0;
}

// Constructeur avec initialisation des variables membres

CIRCUIT (double v_n_p, double v_n_m, double v_a_p, int no)


{
v_num_plus = v_n_p;
v_num_moins = v_n_m;
v_analog_plus = v_a_p;
numero = no;
}

256 Programmation en C++ IFT-19965


C
++

// Readers

double read_v_num_plus() {
return v_num_plus;
}
double read_v_num_moins () {
return v_num_moins;
}
double read_v_analog_plus () {
return v_analog_plus;
}
double read_numero () {
return numero;
}

// Writers

void set_v_num_plus(double v) {
v_num_plus = v;
}
void set_v_num_moins(double v) {
v_num_moins = v;
}
void set_v_analog_plus(double v) {
v_analog_plus = v;
}
void set_numero(int n) {
numero = n;
}

// Affichage du type de composant


virtual char *affiche_type() {return “Composante de
circuit”;}

//Fonction de calcul du courant


virtual double calcule_courant() {return 0.0;}
};

#endif CIRCUIT_FLAG_

La classe ANALOGIQUE prend la forme suivante:


//&% analogique_36_1.h
#include <iostream.h>
#include “circuit_36_1.h”

#ifndef ANALOGIQUE_FLAG_
#define ANALOGIQUE_FLAG_

Programmation en C++ IFT-19965 257


C
++

class ANALOGIQUE : public CIRCUIT {


private:
double puissance_max;
int type;

public:

// Constructeur par defaut


ANALOGIQUE () {}

// Constructeur avec arguments


ANALOGIQUE (double p_max, int typ) {
puissance_max = p_max;
type = typ;
}

//Readers
double read_puissance_max() {
return puissance_max;
}
int read_type() {
return type;
}

//Writers
void set_puissance_max(double p_max) {
puissance_max = p_max;
}
void set_type(int typ) {
type = typ;
}

// Affichage du type de composant


virtual char *affiche_type() {return “Composante
analogique”;}

//Calcul du courant
virtual double calcule_courant() {return 0.0;}
};

#endif ANALOGIQUE_FLAG_

La classe RESISTANCE prend la forme ci-dessous:


//&% resis_2_36_1.h

#include <iostream.h>
#include “circuit_36_1.h”
#include “analogique_36_1.h”

258 Programmation en C++ IFT-19965


C
++

#ifndef RESISTANCE_FLAG_
#define RESISTANCE_FLAG_

class RESISTANCE : public ANALOGIQUE {

private:
double res;
double tolerance;

public:
//Constructeur par defaut
RESISTANCE() {}

//Constructeur avec deux arguments


RESISTANCE(double resi, double tol) {
res = resi; tolerance = tol;
}
//Readers
double lire_resistance() {
return res;
}
double lire_tolerance() {
return tolerance;
}

//Writers
void ecrire_resistance(double val) {
res = val;
}
void ecrire_tolerance(double val) {
tolerance = val;
}

// Affichage du type de composant


virtual char *affiche_type() {return “Composante
RESISTANCE”;}
//Calcul du courant
virtual double calcule_courant() {return
read_v_analog_plus() / res;}
};

#endif RESISTANCE_FLAG_

Et, finalement, la classe CONDENSATEUR prend la forme


suivante:
//&% condens_2_36_1.h

#include <iostream.h>

Programmation en C++ IFT-19965 259


C
++

#include “circuit_36_1.h”
#include “analogique_36_1.h”

#ifndef CONDENSATEUR_FLAG_
#define CONDENSATEUR_FLAG_

class CONDENSATEUR : public ANALOGIQUE {

private:
double capacite;
double v_claquage;

public:
//Constructeur par defaut
CONDENSATEUR() {}

//Constructeur avec deux arguments


CONDENSATEUR(double cap, double claq) {
capacite = cap; v_claquage = claq;
}

//Readers
double lire_capacite() {
return capacite;
}
double lire_v_claquage() {
return v_claquage;
}

//Writers
void ecrire_capacite(double val) {
capacite = val;
}
void ecrire_v_claquage(double val) {
v_claquage = val;
}

// Affichage du type de composant


virtual char *affiche_type() {return “Composante
CONDENSATEUR”;}
//Calcul du courant
virtual double calcule_courant(){return 0.0;}
};

#endif CONDENSATEUR_FLAG_

405 On remarque les points suivants:

260 Programmation en C++ IFT-19965


C
++

1. La classe CIRCUIT contient maintenant une fonction mem-


bre virtuelle calcule_courant() qui calcule le courant
dans une composante étant donné la tension analogique
positive. Cette fonction virtuelle se retrouve également dans
la classe ANALOGIQUE
2. Les classes RESISTANCE et CONDENSATEUR contiennent les
constructeurs et fonctions membres d’écriture/lecture de
même qu’une fonction virtuelle de calcul du courant. Seule le
code est développé pour la classe RESISTANCE étant donné
qu’il est facile de calculer le courant traversant une résis-
tance avec la loi d’Ohm:
V = RI EQ 2

406 Supposons maintenant que le type d’une résistance soit 3 et


celui d’un condensateur 4. Nous désirons concevoir un
programme qui lit un fichier de composantes et calcule la
puissance dissipée dans les résistances seulement. Le
programme doit respecter les deux contraintes suivantes:
1. l’équation de calcul de la puissance dissipée dans une résis-
tance doit obligatoirement être la suivante:
P = VI EQ 3

2. le calcul de la puissance doit se faire via une fonction ordi-


naire (et non pas une fonction membre d’une classe) dans le
programme main(). La fonction doit recevoir un objet de
type CIRCUIT en argument.

407 Le programme suivant devrait faire l’affaire:


//&% it_fil_36_1.C

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include “circuit_36_1.h”
#include “analogique_36_1.h”
#include “resis_2_36_1.h”
#include “condens_2_36_1.h”

enum {nand_2_code = 1, nor_2_code, resis_code,


condens_code};

double puissance(CIRCUIT cir) {

Programmation en C++ IFT-19965 261


C
++

return cir.read_v_analog_plus() *
cir.calcule_courant();
}

void main() {

int i, compteur, type;


int in_1,in_2;
int r;
double res, tol;
CIRCUIT *cir_ptr[100];

ifstream flot_in (“it_dat_36_1.in”, ios::in);


if (flot_in == 0){
exit(0);
}
for (compteur = 0,r = 0; flot_in >> type; ++compteur) {
switch (type) {
case nand_2_code: flot_in >> in_1 >> in_2;
break;
case nor_2_code: flot_in >> in_1 >> in_2;
break;
case resis_code: flot_in >> res >> tol;
cir_ptr[r] = new RESISTANCE(res,tol);
++r;
break;
case condens_code : flot_in >> res >> tol;
break;
default: cerr << “Piece “
<< type
<< “ n’est pas une connue”
<< endl;
exit(0);
}

}
flot_in.close();
cout << “Nb de pieces total: “ << compteur << endl;
cout << “Nb de resistances detectees: “ << r << endl;

for (i = 0; i < r ; ++i) {


cout << “Puissance dans la resistance: “
<< i << “: “
<< puissance(*cir_ptr[i])
<< endl;
}

262 Programmation en C++ IFT-19965


C
++

On remarque que la fonction puissance reçoit bien un


argument de type CIRCUIT et qu’elle utilise la fonction
membre calcule_courant avant de calculer la puissance
avec l’ EQ 3. On remarque aussi que la puissance n’est calculée
que pour les composantes de type RESISTANCE. Que donne la
sortie du programme suivant pour un fichier de données ayant
pour nom it_dat_36_1.in et dont le contenu est le suivant?
3 25.0 10.0 3 12.0 10.0 1 1 1 4 25.0 10.0 2 1 1 3 24.0 10.0

Résultat
Nb de pieces total: 6
Nb de resistances detectees: 3
Puissance dans la resistance: 0: 0
Puissance dans la resistance: 1: 0
Puissance dans la resistance: 2: 0

Le programme compte bien le nombre de composantes total


dans le fichier et le nombre corrects de composantes de type
RESISTANCE. Cependant, le calcul de la puissance est
incorrect. Comment cela peut-il s’expliquer?

408 Pour comprendre le comportement, il faut s’attarder à la façon


dont le C++ manipule les arguments de fonctions qui sont des
objets. Il faut se rappeler que comme le passage du paramètre à
la fonction puissance se fait par valeur, le C++ réserve
généralement un espace de mémoire dans lequel est copié
l’objet passé en argument, la fonction travaillant ensuite sur la
copie. Si l’argument est une variable simple comme un int, sa
valeur est copiée dans le paramètre.

409 Or, l’argument de la fonction puissance est un objet de type


CIRCUIT. Cela signifie que lorsque la fonction est appelée, un
espace est réservé par le C++ pour stocker une copie d’un objet
de type CIRCUIT. Cependant, on remarque aussi que les objets
qui sont passés à la fonction puissance dans le programme
d’analyse sont en réalité des objets de type RESISTANCE
parce qu’on utilise la notation puissance(*cir_ptr[i]). Or

Programmation en C++ IFT-19965 263


C
++

on sait que, de par la structure du programme, seuls des objets


de type RESISTANCE sont créés au run-time, leur adresse étant
emmagasinée dans un pointeur de type CIRCUIT.
Malheureusement, le compilateur ne peut anticiper que les
objets qui seront passés à la fonction puissance seront du type
RESISTANCE lors du run-time, c’est pourquoi l’espace réservé
pour la copie lors de l’appel de la fonction ne peut que stocker
un objet de type CIRCUIT. Mais un objet de type RESISTANCE
occupe plus d’espace en mémoire qu’un objet de type CIRCUIT
puisqu’il possède des variables membres et des fonctions
membres additionnelles. La partie d’un objet RESISTANCE
différente de celle d’un objet CIRCUIT n’est cependant pas
copiée lors de l’appel de la fonction puissance. L’appel de la
fonction cir.calcule_courant(); qui est effectué dans
puissance n’est donc pas celui qui correspond à un objet
RESISTANCE mais à celui d’un objet CIRCUIT (qui retourne la
valeur 0.0). Le schéma ci-dessous illustre ce qui se produit lors
du passage de paramètre à la fonction puissance.

Mémoire Espace réservé


occupée par pour un paramètre
un objet CIRCUIT dans
RESISTANCE puissance

Copie

} Partie non copiée

410 Les programmes en C++ adoptent généralement la convention


du passage de paramètres par valeur. Cependant, on constate
que cette convention à comme conséquence que le programme

264 Programmation en C++ IFT-19965


C
++

ci-dessus n’a pas le résultat escompté. Heureusement, le C++


permet au programmeur de passer un paramètre par
référence. A chaque fois qu’un argument est passé par
référence, le paramètre de la fonction ne reçoit pas une copie de
l’argument mais partage plutôt l’espace mémoire occupé par
l’argument. Ainsi, aucune copie de l’argument n’a lieu dans le
paramètre. La figure ci-dessous montre ce qui se produit lors
du passage d’un argument par référence.

Mémoire Espace réservé


occupée par pour un paramètre
un objet CIRCUIT dans
RESISTANCE puissance

En fait, seule l’adresse de l’argument est passée au paramètre.


La fonction ne travaille donc pas sur une copie comme c’est le
cas dans le passage d’arguments par valeur mais travaille
plutôt directement sur l’objet. Cela a pour conséquence que la
fonction peut modifier l’objet fourni en argument. De plus,
comme seule l’adresse de l’objet est copiée, l’objet en entier est
visible de la fonction.

411 Ce qui précède est aussi valable pour tout type d’argument de
fonction.

412 Pour informer le compilateur C++ que l’argument doit être


passé par référence à la fonction, il suffit d’ajouter le caractère

Programmation en C++ IFT-19965 265


++ C
& devant le nom du paramètre, tout de suite après le type de
celui-ci. Par exemple, la syntaxe de la fonction puissance
devient, lorsqu’on veut lui passer un argument par référence:
double puissance(CIRCUIT& cir) {
return cir.read_v_analog_plus() *
cir.calcule_courant();
}

En adoptant le passage d’argument par référence ci-dessus, la


sortie du programme du paragraphe 407 est correcte et devient:

Résultat
Nb de pieces total: 6
Nb de resistances detectees: 3
Puissance dans la resistance: 0: 5.76
Puissance dans la resistance: 1: 12
Puissance dans la resistance: 2: 6

413 On peut résumer les notions suivantes comme suit:


1. En général, on utilise le passage d’arguments par valeur, le
mode par défaut du C++. Le C++ alloue toujours de l’espace
pour chaque paramètre recevant un argument par valeur et
la valeur de l’argument est copiée dans le paramètre. Cette
approche garantit que la fonction travaille sur une copie de
l’argument.
2. On utilise le passage d’arguments par référence lorsque
l’argument est un objet appartenant à une sous-classe du
type du paramètre. Cette approche permet de rendre l’objet
entièrement visible de la fonction plutôt que la seule portion
correspondant à la partie commune entre le paramètre et
l’argument de la fonction. Chaque argument passé par réfé-
rence partage un espace de mémoire avec le paramètre de la
fonction.

414 Il y a deux raisons importantes d’utiliser le passage de


paramètres par référence:

266 Programmation en C++ IFT-19965


C
++

1. Lors d’un passage par valeur, la copie de l’argument dans le


paramètre peut prendre un temps non négligeable, surtout
lorsque que l’argument est un objet de grandes dimensions.
Si la fonction est appelée fréquemment, le programme est
passablement ralenti par cette recopie systématique de
l’argument dans le paramètre.
2. On peut souhaiter écrire une fonction qui modifie un argu-
ment, ce qui est impossible si seulement une copie de celui-ci
est disponible à l’intérieur de la fonction.

Résumé
415 ✔ Le C++ utilise normalement le passage d’argument par
valeur. Dans ce cas, la valeur de l’argument est copiée dans
l’espace de mémoire réservé pour le paramètre lors de l’appel de
la fonction. C’est cet espace qui est utilisé par la fonction. Il est
libéré lorsque l’exécution de la fonction se termine. Il est
impossible de modifier la valeur de l’argument puisque la
fonction ne travaille que sur une copie.
✔ On peut forcer le compilateur C++ à recevoir un argument
par référence. Dans ce cas, l’espace de mémoire occupé par
l’argument est partagé par le paramètre.
✔ Une raison de forcer le passage par référence est que cette
façon de faire permet de nous assurer que toutes les parties
d’un objet sont accessibles de la fonction.
✔ Une autre raison d’utiliser le passage par référence est que
cette approche est plus rapide étant donné que seule l’adresse
de l’objet passé en argument à la fonction est copiée dans le
paramètre.
✔ Une dernière raison de passer des arguments par référence
est que la fonction peut modifier l’objet directement et non
seulement une copie de l’objet tout en évitant les fuites de
mémoire si le constructeur par recopie est absent. La notion de
constructeur par recopie sera abordée au CHAPITRE 48.
✔ Pour indiquer à une fonction qu’un argument est passé par
référence, il suffit d’adopter la syntaxe suivant:
type_de_retour nom_fonction(...,
type_arg & nom_parametre,...) {...}

Programmation en C++ IFT-19965 267


C
++

268 Programmation en C++ IFT-19965


C
++

CHAPITRE 37 La surdéfinition de
l’opérateur d’insertion

416 Ce chapitre et le chapitre suivant présentent une notion


extrêmement importante du C++: la surdéfinition
d’opérateurs1. Nous verrons tout d’abord au CHAPITRE 37
comment l’opérateur d’insertion peut être surdéfini pour
accepter n’importe quel type d’argument. Nous verrons alors
également la notion d’argument de transit2. Nous présenterons
des principes généraux et des exemples spécifiques de
surdéfinition à n’importe quel type d’opérateur standard du
C++ dans les prochains chapitres.

417 La mémoire qui est réservée pour chaque paramètre passé par
valeur ou chaque variable locale d’une fonction ne l’est que
temporairement, le temps que la fonction s’exécute. L’espace
est ensuite récupéré par le programme et peut être réutilisé à
d’autres fins. En fait, lorsqu’une fonction est appelée, les
paquets de mémoire réservés aux paramètres sont placés dans
un espace mémoire appelé “pile”3. On dit que les éléments sont
placés sur le dessus de la pile4. Au retour de la fonction, ces

1. “operator overloading” en anglais.


2. “pass-through” en anglais.
3. “stack” en anglais.

Programmation en C++ IFT-19965 269


++ C
paquets de mémoire sont récupérés et peuvent être utilisés à
nouveau par le programme.

418 Le diagramme ci-dessous montre l’évolution de la pile lors de


l’appel d’une fonction:
1. Tout d’abord, la pile est dans un état donné avant l’appel de
la fonction.
2. Ensuite, au début de l’appel de la fonction, des données sont
placées sur le dessus de la pile pour être utilisées par la fonc-
tion. La pile change donc de dimensions.
3. Lors du retour de la fonction, l’espace occupé par les élé-
ments placés sur le dessus de la pile est récupéré par le pro-
gramme et la pile diminue.

Avant l’appel de Pendant l’appel Après le


la fonction de la fonction retour de la
fonction

Vide Vide Vide

Vide Utilisé Vide

Vide Utilisé Vide

Utilisé Utilisé Utilisé

Utilisé Utilisé Utilisé

Utilisé Utilisé Utilisé

419 Supposons maintenant qu’une fonction doive retourner la


valeur d’un paramètre passé par valeur ou encore la valeur
d’une variable locale. En général, le C++ fait en sorte que la
valeur à retourner soit copiée sur la pile avant qu’elle ne soit

4. “pushed on stack”

270 Programmation en C++ IFT-19965


C
++

perdue au retour de la fonction comme le montre le diagramme


ci-dessous.

Vide

Vide
Réservé pour un paramètre Valeur copiée
passé par valeur qu’on retourne durant le retour
Utilisé de la fonction
via la pile
Utilisé

Utilisé
Mémoire réservée
par le programme
Utilisé pour stocker la
valeur de retour

420 Il faut cependant remarquer que le mécanisme de retour décrit


ci-dessus, et qui est le mode de fonctionnement par défaut du
C++, n’est pas toujours adéquat pour ce que le programmeur
désire implanter. En particulier, certaines fonctions impliquent
une catégorie de paramètres appelée objet de transit1: un tel
objet est passé en argument à une fonction par référence, donc
sans recopie dans un paramètre, et il est ensuite retourné au
programme comme étant la valeur de la fonction, également
sans recopie. Dans le reste de ce chapitre, nous présentons
comment l’opérateur d’insertion << peut être surdéfini en
utilisant une définition incluant un objet de transit.

421 Supposons que dans notre programme de rapport, on veuille


que la sortie du programme ait l’allure suivante:
[RESISTANCE]-[NAND_2]-[NOR_2]-[CONDENSATEUR]-[NAND_2]

lorsque des données sont lues d’un fichier. On pourrait adopter


le programme suivant:
//&% it_fil_37_1.C

#include <iostream.h>

1. “pass-through object” en anglais.

Programmation en C++ IFT-19965 271


C
++

#include <fstream.h>
#include <stdlib.h>
#include “circuit_37_1.h”
#include “nand_2_37_1.h”
#include “nor_2_37_1.h”
#include “analogique_37_1.h”
#include “resis_2_37_1.h”
#include “condens_2_37_1.h”

enum {nand_2_code = 1, nor_2_code, resis_code,


condens_code};

void main() {

int i, compteur, type;


int in_1,in_2;
double d_1, d_2;
CIRCUIT *cir_ptr[100];
ifstream flot_in (“it_dat_37_1.in”, ios::in);
if (flot_in == 0) {
exit(0);
}
for (compteur = 0; flot_in >> type; ++compteur) {
switch (type) {
case nand_2_code: flot_in >> in_1 >> in_2;
cir_ptr[compteur] = new NAND_2(in_1,in_2);
break;
case nor_2_code: flot_in >> in_1 >> in_2;
cir_ptr[compteur] = new NOR_2(in_1,in_2);
break;
case resis_code: flot_in >> d_1 >> d_2;
cir_ptr[compteur] = new RESISTANCE(d_1,d_2);
break;
case condens_code : flot_in >> d_1 >> d_2;
cir_ptr[compteur] = new CONDENSATEUR(d_1,d_2);
break;
default: cerr << “Piece “
<< type
<< “ n’est pas une connue”
<< endl;
exit(0);
}

}
flot_in.close();

cout << “[“ << cir_ptr[0]->affiche_type() << “]”;


//Premier element
//sans “-”
for (i = 1; i < compteur ; ++i) {

272 Programmation en C++ IFT-19965


C
++

cout << “-[“ << cir_ptr[i]->affiche_type() << “]”;


}
cout << endl;
}

Résultat
[RESISTANCE]-[NAND_2]-[CONDENSATEUR]-[NOR_2]-[RESISTANCE]

pour des données d’entrée qui sont les suivantes:


3 25.0 10.0 1 1 1 4 25.0 10.0 2 1 1 3 24.0 10.0

422 La solution ci-dessous n’est cependant pas tellement élégante à


cause de l’allure baroque de la boucle for d’affichage qui inclut
l’affichage des symboles “[“ et “]” tout en appelant la fonction
membre affiche_type():
cout << “[“ << cir_ptr[0]->affiche_type() << “]”;
//Premier element
//sans “-”
for (i = 1; i < compteur ; ++i) {
cout << “-[“ << cir_ptr[i]->affiche_type() << “]”;
}
cout << endl;
}

423 On sait qu’en général, l’opérateur d’insertion << est défini pour
les types de données standard1 supportés par le C++. Si on
pouvait faire en sorte que l’opérateur d’insertion accepte des
types de données définis par l’usager, cela permettrait d’avoir
un code beaucoup plus lisible comme par exemple la nouvelle
version de la boucle for d’affichage dans le programme du
paragraphe 421:
cout << *cir_ptr[0]; //Premier element sans “-”
for (i = 1; i < compteur ; ++i) {
cout << “-” << *cir_ptr[i];

1. comme int, float, long...

Programmation en C++ IFT-19965 273


C
++

On remarque qu’ici, on fournit directement un objet de type


CIRCUIT à l’opérateur d’insertion. Cette opération ne peut
fonctionner sans erreur de compilation que si on peut
surdéfinir l’opérateur d’insertion pour qu’il puisse accepter un
objet de type CIRCUIT en argument.

424 En C++, il est possible de surdéfinir les opérateurs tel


l’opérateur d’insertion de la même façon qu’il est possible de
surdéfinir des fonctions ordinaires définies par l’usager1. La
syntaxe à adopter dans ce cas est la suivante:
type_retour operator symbole (type_gauche nom,
type_droite nom) {...}

425 On sait que l’opérateur d’insertion reçoit deux opérandes:


l’opérande de gauche informe l’opérateur de l’endroit où les
données doivent être acheminées tandis que l’opérande de
droite est un objet quelconque comme par exemple un objet
appartenant à une classe définie par l’usager.
Pour surdéfinir l’opérateur d’insertion, il faut savoir que son
paramètre de gauche, comme par exemple cout, doit être de la
classe ostream. De plus, l’objet retourné par l’opérateur de
sortie doit être le même objet qui est fourni comme argument
de gauche afin de supporter les opérations imbriquées tel que
montré dans le diagramme ci-dessous.

cout << a << b << endl;


{

cout
{

cout

Par conséquent, pour surdéfinir l’opérateur d’insertion, on


modifie le patron général exposé au paragraphe 424 pour en
arriver à:

1. On n’a qu’à penser à la surdéfinition des constructeurs dans les classes comme exemple.

274 Programmation en C++ IFT-19965


C
++

ostream& operator <<


(ostream& output_stream, type_droite nom) {
énoncés
return output_stream;
}

426 Cette syntaxe s’explique comme suit:


1. le symbole & pour le paramètre de gauche de la fonction
signifie que le paramètre de type ostream est passé par
référence et n’est donc pas copié.
2. le symbole & dans la définition du paramètre de retour de la
fonction signifie que la valeur de retour n’est pas recopiée.
3. comme il n’y a pas de copie, les opérations imbriquées sont
supportées car elles travaillent toutes sur le même objet de
type ostream.

427 Si on revient à notre programme de rapport de circuit, on peut


donc surdéfinir l’opérateur d’insertion de la façon suivante:
ostream& operator << (ostream& output_stream, CIRCUIT&
cir){
output_stream << “[“ << cir.affiche_type() << “]”;
return output_stream;
}

428 Le programme s’exécute alors comme suit:

Résultat
[RESISTANCE]-[NAND_2]-[CONDENSATEUR]-[NOR_2]-[RESISTANCE]

429 On remarque que l’opérateur d’insertion reçoit une référence à


un objet de type ostream (ici cout) comme argument de
gauche et un objet de type CIRCUIT comme argument de droite.

Programmation en C++ IFT-19965 275


C
++

430 Il est ainsi possible de surdéfinir l’opérateur d’insertion pour


tous les types d’objets, ce qui contribue grandement à clarifier
le code.

431 On remarque ici que la surdéfinition de l’opérateur d’insertion


s’effectue via une fonction extérieure à toute classe. Il est
possible qu’une fonction de surdéfinition d’opérateur soit une
fonction membre de la classe pour laquelle l’opérateur est
surdéfini. Nous verrons aux prochains chapitres les contraintes
qui font qu’une surdéfinition d’opérateur doit ou peut être
incluse ou non comme fonction membre d’une classe.

Résumé
432 ✔ Si on veut surdéfinir un opérateur binaire (i.e. à deux
opérandes), on adopte le patron suivant:
type_retour operateur symbole
type_gauche nom, type_droite nom) {...}
✔ Pour surdéfinir l’opérateur d’insertion, on adopte le patron
suivant:
ostream& operator symbole
(ostream& output_stream, type_droite nom) {
énoncés
return output_stream;
}

276 Programmation en C++ IFT-19965


C
++

CHAPITRE 38 La surdéfinition des


opérateurs: notions
fondamentales

433 Au CHAPITRE 37, nous avons vu qu’il est possible de


surdéfinir l’opérateur d’insertion avec le mot réservé operator,
ce qui permet de clarifier grandement la syntaxe des
programmes.

434 En fait, presque tous les opérateurs du C++ peuvent être


surdéfinis pour accepter des arguments de types définis par le
programmeur. La surdéfinition des opérateurs est
particulièrement utile dans le cas des classes arithmétiques qui
manipulent beaucoup de nombres. En fait, dans le C++
standard, les opérateurs + et - sont déjà surdéfinis pour
accepter plusieurs types différents d’opérandes (par exemple
int + int, double + double, etc...). Nous verrons dans ce
chapitre, qu’il est possible d’étendre ces notions aux classes
définies par l’usager.

435 La surdéfinition d’un opérateur permet de le rendre sensible au


contexte auquel il est exposé puisqu’il réagit différemment,
selon la façon dont il est programmé, en fonction des
arguments qui lui sont fournis.

Programmation en C++ IFT-19965 277


C
++

436 Le C++ ne permet pas au programmeur de créer de nouveaux


opérateurs. Il permet cependant de surdéfinir les opérateurs
existants de sorte que, lorsqu’ils sont utilisés avec des objets de
types définis par l’usager, ils ont une signification appropriée
pour ce type d’objet.

437 La surdéfinition des opérateurs en C++ présente deux


avantages importants par rapport aux langages traditionnels
n’offrant pas cette possibilité:
1. elle permet l’extension du langage et de ses opérateurs stan-
dards à des situations variées et à des contextes d’utilisation
très différents,
2. elle permet souvent de rendre les programmes plus clairs
que si des fonctions conventionnelles avaient été utilisées
pour accomplir la même tâche.
Il faut cependant éviter d’utiliser la surdéfinition à outrance
lorsque celle-ci est incohérente, suit des règles mal définies ou
rend le programme difficile à lire.

438 Un opérateur est surdéfini en écrivant une fonction incluant un


en-tête et un corps comme toute fonction standard en C++. Le
nom de la fonction est:
operator symbole_de_l’operateur.

Par exemple, le nom de la fonction servant à surdéfinir


l’opérateur d’addition + serait le suivant:
operator +

439 Pour utiliser un opérateur sur un objet d’une classe définie par
un usager, l’opérateur doit être surdéfini. Deux exception
échappent cependant à cette règle:
1. l’opérateur d’affectation = peut être utilisé avec tout type
d’objet, standard ou non, sans avoir été surdéfini. Le compor-
tement par défaut de cet opérateur est une copie membre à
membre des éléments de chaque classe. Ce comportement
par défaut peut parfois donner des résultats erronés (par
exemple dans les cas d’allocation dynamique d’objets) et il

278 Programmation en C++ IFT-19965


C
++

faut de préférence surdéfinir l’opérateur pour être certain de


son comportement.
2. L’opérateur “adresse-de” & peut également être utilisé sur
des objets sans avoir été surdéfini. Dans ce cas, il retourne
simplement l’adresse de l’objet en mémoire. Cependant,
l’opérateur “adresse-de” peut être surdéfini pour adopter
un comportement différent.

440 La surdéfinition est particulièrement utile pour les classes


mathématiques. La surdéfinition de ce type d’opérateur permet
souvent d’éclaircir grandement le code et ainsi de le rendre plus
facile à lire et à modifier.

441 La surdéfinition d’opérateurs peut se faire de trois manières


différentes:
1. via une fonction membre de la classe pour laquelle l’opéra-
teur est surdéfini,
1
2. via une fonction amie de la classe,
3. via une fonction ordinaire (ni amie, ni membre d’une classe).
Nous allons voir, dans les prochains chapitres, comment chaque
approche peut être exploitée et quand chacune doit être choisie.

442 Plusieurs opérateurs du C++ qui peuvent être surdéfinis. La


Table ci-dessous résume la liste de ces opérateurs.

Opérateurs du C++ qui peuvent être surdéfinis


+ - * / % ^ & |
~ ! = < > += -= *=
/= %= ^= &= |= << >> >>=
<<= == != <= >= && || ++
-- ->* , -> [] () new delete

1. “friend” en anglais. Nous verrons comment déclarer une fonction amie un peu plus loin.

Programmation en C++ IFT-19965 279


C
++

443 Par contre, les opérateurs suivants ne peuvent être surdéfinis:

Opérateurs ne pouvant être surdéfinis


. .* :: ?: sizeof

444 La priorité d’un opérateur ne peut être modifiée par


surdéfinition. Cela signifie que les opérateurs gardent leur
ordre de priorité même s’ils sont surdéfinis. Il en va de même
de l’associativité entre les opérateurs. Il est impossible
d’utiliser des arguments par défaut avec un opérateur
surdéfini. Il est aussi impossible de changer le nombre
d’opérandes qu’un opérateur reçoit: un opérateur unaire
demeure unaire et un opérateur binaire demeure binaire et
doit recevoir deux opérandes. Les opérateurs qui peuvent êtres
unaires ou binaires selon le contexte conservent cette propriété
lorsqu’ils sont surdéfinis (i.e. ils peuvent être surdéfinis à la
fois comme étant unaires ou binaires. La surdéfinition doit
cependant clairement spécifier c’est le cas unaire ou binaire qui
est traité).

445 La surdéfinition de l’opérateur = et de l’opérateur + dans


l’expression suivante:
objet = objet_1 + objet_2;

ne signifie pas que l’opérateur += est automatiquement


surdéfini. Il faut donc le surdéfinir séparément.

446 Les fonctions de surdéfinition d’opérateurs peuvent, tel que


mentionné plus haut, être des fonctions membres ou des
fonctions ordinaires (normalement amies des classes sur
lesquelles elles travaillent). Les fonctions membres de
surdéfinition d’opérateur utilisent le pointeur this
implicitement pour obtenir l’un des objets arguments de
l’opérateur. Pour une fonction amie, l’objet doit être
explicitement nommé lors de l’appel de la fonction de
surdéfinition.

280 Programmation en C++ IFT-19965


C
++

447 La surdéfinition des opérateurs (), [], ->, ou = exige que la


fonction de surdéfinition soit une fonction membre de la classe.

448 Quand une fonction de surdéfinition d’opérateur est implantée


comme une fonction membre d’une classe, l’argument de
gauche seulement doit être un objet (ou une référence à un
objet) de la même classe.

449 Quand un argument de gauche appartient à une autre classe


ou à un type standard du C++, la fonction de surdéfinition de
l’opérateur doit être implantée comme une fonction ordinaire
(et peut-être aussi friend mais pas toujours).

450 Le comportement par défaut de l’opérateur = est de copier


membre à membre les variables de l’objet de droite dans celles
de l’objet de gauche y compris les variables héritées des
superclasses.

Programmation en C++ IFT-19965 281


C
++

282 Programmation en C++ IFT-19965


C
++

CHAPITRE 39 La surdéfinition des


opérateurs unaires

451 Ce chapitre présente les règles fondamentales de surdéfinition


des opérateurs unaires. Nous verrons ces règles dans le cadre
d’une application sur la classe NAND_2.

452 Un opérateur unaire d’une classe peut être surdéfini via une
fonction membre1 sans argument ou via une fonction ordinaire
avec un argument correspondant à un objet de la classe ou une
référence à un objet de la classe. Prenons par exemple la classe
NAND_2. Il serait intéressant de pouvoir inverser la sortie d’une
porte simplement en utilisant l’opérateur de négation ~ suivi
du nom de la porte. Il faut bien noter qu’on désire faire la
négation de la valeur de sortie mais sans changer la sortie.
Cela peut éventuellement servir en logique booléenne:
NAND_2 nnd;
cout << ~nnd;

Voyons comment on peut définir la fonction membre de


surdéfinition de l’opérateur pour la classe NAND_2:
//&% nand_2_39_1.h

1. la fonction membre doit être non-static

Programmation en C++ IFT-19965 283


C
++

#include <iostream.h>
#include “circuit_37_1.h”
#include “numerique_37_1.h”

#ifndef NAND_FLAG_
#define NAND_FLAG_

class NAND_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1() {
return 1 - (in_1 * in_2);
}

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

//Constructeur par defaut


NAND_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

//Readers
int lire_in_1() {
return in_1;
}

int lire_in_2() {
return in_2;
}

int lire_out_1() {
return out_1;
}

//Writers
void ecrire_in_1(int val) {
in_1 = val;
out_1 = set_out_1();

284 Programmation en C++ IFT-19965


C
++

void ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}
// Surdefinition des operateurs

int operator ~ () {
if (lire_out_1()) return 0;
else return 1;
}

// Affichage du type de composant


virtual char *affiche_type() {return “NAND_2”;}
};

#endif NAND_FLAG_

On remarque que le code de surdéfinition de l’opérateur de


négation pour la classe NAND_2 est très simple:
int operator ~ () {
if (lire_out_1()) return 0;
else return 1;
}

la fonction operator~ ne reçoit aucun argument et retourne


simplement la valeur complémentée de la sortie de la porte. Un
programme de test de la surdéfinition de l’opérateur unaire de
négation serait par exemple un programme qui affiche la table
de vérité de la porte en incluant aussi la négation de la sortie
dans le contenu de la table:
//&% sur_def_39_1.C

#include <iostream.h>
#include <fstream.h>
#include “circuit_37_1.h”
#include “numerique_37_1.h”
#include “nand_2_39_1.h”

ostream& operator << (ostream& output_stream, NAND_2& nd) {


output_stream << nd.lire_out_1();
return output_stream;
}

void main() {

Programmation en C++ IFT-19965 285


C
++

int i,j;
NAND_2 nnd;

cout << “Table de verite d’une porte NAND”


<< endl;
cout << “--------------------------------”
<< endl
<< “entree 1 entree 2 sortie neg. sortie”
<< endl;

for (i = 0; i < 2 ; i++) {


for (j = 0 ; j < 2 ; j++) {
nnd.ecrire_in_1(i);
nnd.ecrire_in_2(j);
cout << nnd.lire_in_1()
<< “ “
<< nnd.lire_in_2()
<< “ “
<< nnd
<< “ “
<< ~nnd
<< endl;
}
}
}

Résultat
Table de verite d’une porte NAND
--------------------------------
entree 1 entree 2 sortie neg. sortie
0 0 1 0
0 1 1 0
1 0 1 0
1 1 0 1

On remarque plusieurs choses dans cet exemple: la


surdéfinition de l’opérateur unaire de négation ~ est
relativement simple et suit les règles de définition énoncées au
CHAPITRE 38. Dans ce cas précis, la fonction operator ~ a
été définie comme une fonction membre de la classe NAND_2.
Elle ne reçoit aucun argument comme ça doit être le cas pour la
surdéfinition d’un opérateur unaire. Elle retourne un entier
(int) qui est la valeur complémentée de la sortie de la porte
NAND. Remarquez que l’opérateur d’insertion << a également

286 Programmation en C++ IFT-19965


C
++

été surdéfini comme fonction ordinaire du programme


principal. La fonction operator<< reçoit une référence à un
objet de type ostream et retourne une référence à une objet du
même type. Elle reçoit une référence à un objet NAND_2 comme
argument de droite, ce qui permet d’écrire la ligne suivante:
cout << nnd << ~nnd;

Remarquez que la priorité de l’opérateur ~ sur l’opérateur <<


n’a pas été modifiée même si les deux opérateurs ont été
surdéfinis et sont finalement appliqués sur le même objet nnd
dans la partie droite de l’instruction ci-dessus.
Sans l’exposer en détails, nous ajoutons également la
surdéfinition de l’opérateur unaire ~ à la classe NOR_2.
Remarquez que le code de cette surdéfinion est exactement le
même parce que les noms des fonctions membres d’accès à la
variable membre des deux classes sont identiques. Le fait
d’avoir surdéfini l’opérateur ~ pour la classe NAND_2 sert donc
directement1 à la surdéfinition de la classe NOR_2.

453 La surdéfinition par des fonctions membres des autres


opérateurs unaires du C++ suit la même philosophie que pour
l’exemple ci-dessus. Nous avons choisi l’exemple de l’opérateur
de négation ~ parce qu’il est pertinent aux portes logiques
modélisées par les classes définies jusqu’ici.

Résumé
454 ✔ La surdéfinition d’un opérateur unaire comme fonction
membre d’une classe suit la syntaxe suivante:
type_valeur_retour operateur symbole ()
{ énoncés;
return valeur_retour;}

1. le code peut être copié textuellement dans la classe NOR_2 sans changement

Programmation en C++ IFT-19965 287


C
++

288 Programmation en C++ IFT-19965


C
++

CHAPITRE 40 La surdéfinition des


opérateurs binaires

455 Ce chapitre décrit les principes fondamentaux de surdéfinition


des opérateurs binaires du C++, c’est-à-dire ceux qui exigent
deux opérandes. Les idées générales énoncées au CHAPITRE
38 seront suivies.

456 Si on reprend les classes RESISTANCE et CONDENSATEUR de


notre hiérarchie, on remarque que les objets appartenant à ces
classes feraient un usage fort pertinent de la surdéfinition des
opérateurs. En effet, on sait que les résistances et les
condensateurs peuvent être combinés en série ou en parallèle.
Plutôt que de concevoir des fonctions spéciales pour calculer la
combinaison parallèle de deux résistances, il serait beaucoup
plus pertinent d’écrire:
RESISTANCE r_1,r_2;
RESISTANCE r_serie, r_parallele;
r_serie = r_1 + r_2; //combinaison serie
r_parallele = r_1 || r_2; //Combinaison parallele

Cela suppose qu’il faut surdéfinir les opérateurs =, + et || pour


la classe RESISTANCE. On pourra d’ailleurs faire de même pour
la classe CONDENSATEUR.

Programmation en C++ IFT-19965 289


C
++

457 Reprenons la classe RESISTANCE et ajoutons les fonctions


membres permettant de surdéfinir les opérateurs =, + et ||. Le
code de surdéfinition de l’opérateur = peut par exemple être le
suivant:
//Surdefinition de l’operateur =, classe RESISTANCE.
//Tire du fichier source resis_2_40_1.h

const RESISTANCE& operator= (const RESISTANCE& right_res) {


//on copie d’abord les variables membres des superclasses
CIRCUIT::operator=(right_res);
ANALOGIQUE::operator=(right_res);
//on s’occupe ensuite de celles de la classe resistance
res = right_res.lire_resistance();
tolerance = right_res.lire_tolerance();
return *this;
}

La surdéfinition suit les règles générales pour un opérateur


binaire. L’argument de gauche de l’opérateur = est implicite ce
qui signifie que l’opération:
objet_a = objet_b;

revient en fait à procéder à l’appel de fonction suivant:


objet_a.operator=(objet_b);

L’argument de droite est passé comme une référence constante,


ce qui se traduit par la syntaxe:
(const RESISTANCE& right_res)

Notion avancée
458 Pourquoi imposer que la référence soit constante? Simplement
pour s’assurer que la fonction ne modifiera pas l’argument. En
effet, on sait que le fait de passer un argument par référence
permet à la fonction de modifier la valeur de l’argument et que
ce changement se propagera au-delà de l’appel de la fonction.
Pour éviter que l’opérande de droite d’une affectation soit
modifiée par l’opérateur, il suffit simplement de spécifier au
C++ que cette quantité est constante. Le compilateur donnera
alors un message d’erreur si le programmeur tente de modifier
la valeur de l’argument ce qui ajoute un niveau de sécurité
normalement absent lors du passage par référence. Il faut
remarquer que l’utilisation de l’attribut const pour une

290 Programmation en C++ IFT-19965


C
++

référence implique une contrainte additionnelle: seules les


fonctions membres ayant l’attribut const seront autorisées à
manipuler les objets ayant le même attribut. Un exemple de
déclaration d’une fonction membre ayant l’attribut const est
donné ci-dessous:
//&% constMembre.C
#include<iostream.h>

class A
{
private:
int n;

public:
int lire_n() const
{
return n;
}

void ecrire_n(int val)


{
n=val;
}
};

void fonction(const A & obj)


{
cout << “La valeur de n est: “ << obj.lire_n() << endl;
}

main()
{
A ob1;
ob1.ecrire_n(5);

fonction(ob1);
}

459 Il faut aussi comprendre que la fonction de surdéfinition


operator= retourne une référence constante pour les mêmes
raisons.
La fonction ne fait que recopier le contenu des variables
membres de l’opérande de droite dans les membres
correspondants de l’opérande de gauche et retourne le contenu

Programmation en C++ IFT-19965 291


C
++

de l’objet courant pointé par this. La structure de la fonction


permet ainsi l’écriture d’expression du type:
objet_c = objet_b = objet_c;

460 La surdéfinition de l’opérateur + pour les objets de type


RESISTANCE est implantée de façon telle qu’elle calcule la
combinaison série de deux résistances.
//Surdefinition de l’operateur +, classe RESISTANCE.
//Tire du fichier source resis_2_40_1.h

RESISTANCE operator+ (const RESISTANCE& right_res) const {


RESISTANCE res_temp;

res_temp.ecrire_resistance(res +
right_res.lire_resistance());
res_temp.ecrire_tolerance(right_res.lire_tolerance());
return res_temp;
}

La fonction reçoit un argument par référence constante de type


RESISTANCE correspondant à l’opérande de droite et retourne
un objet de type RESISTANCE. Il faut remarquer une chose très
importante: la fonction crée un objet temporaire res_temp
pour calculer la résistance série. L’objet temporaire est
nécessaire car la fonction a besoin de l’argument implicite
(opérande de gauche) pointé par this et de l’argument de
droite (opérande de droite) pour calculer la résistance série. Cet
objet temporaire est ensuite retourné par la fonction avant
d’être détruit à la sortie de la fonction. Il faut aussi dire que,
contrairement au cas de la surdéfinition de l’opérateur =, il ne
FAUT PAS retourner une référence parce que cette référence
(i.e. donc une adresse) serait celle d’un objet temporaire
immédiatement détruit à la sortie de la fonction...ce qui
correspond à une adresse qui pointe à une case mémoire sans
signification pour le programme. Avec la surdéfinition de
l’opérateur + implantée par la fonction ci-dessus, l’instruction:
res_1 + res_2;

équivaut à l’appel de fonction suivant:


res_1.operator+(res_2);

292 Programmation en C++ IFT-19965


C
++

La fonction telle que surdéfinie permet les instructions du


genre:
res_1 + res_2 + res_3;

461 Une fois la fonction de surdéfinition de l’opérateur + implantée,


il est très facile d’implanter la fonction de surdéfinition de
l’opérateur || visant à calculer la combinaison parallèle de
résistances:
//Surdefinition de l’operateur ||, classe RESISTANCE.
//Tire du fichier source resis_2_40_1.h

RESISTANCE operator|| (const RESISTANCE& right_res)


const {
RESISTANCE res_temp;

res_temp.ecrire_resistance((res *
right_res.lire_resistance()) /
(res + right_res.lire_resistance()));
res_temp.ecrire_tolerance(right_res.lire_tolerance());
return res_temp;
}

La fonction possède les mêmes caractéristiques que celle de la


surdéfinition de la fonction + en ce qui concerne le type du
paramètre d’entrée et celui de la valeur de retour. La fonction
implante simplement l’équation de combinaison en parallèle:

R1 R2
R p = -----------------
- EQ 4
R1 + R 2

462 Le programme ci-dessous permet de tester la validité des


fonctions de surdéfinition des opérateurs =, + et || pour des
objets de type RESISTANCE:
//&% sur_def_40_1.C

#include <iostream.h>
#include “circuit_37_1.h”
#include “analogique_37_1.h”
#include “resis_2_40_1.h”

ostream& operator << (ostream& output_stream,


RESISTANCE& r) {

Programmation en C++ IFT-19965 293


C
++

output_stream << r.lire_resistance();


return output_stream;
}

void main() {

RESISTANCE res_serie, res_parallele;


RESISTANCE r_1(1000,10), r_2(1000,10), r_3(1000,10);

res_serie = r_1 + r_2 + r_3;


res_parallele = r_1 || r_2 || r_3;

cout << “Resistance serie: “ << res_serie << endl;


cout << “Resistance parallele: “ << res_parallele << endl;

Le résultat est le suivant:

Résultat
Resistance serie: 3000
Resistance parallele: 333.333

Cet exemple démontre clairement la puissance de la


surdéfinition des opérateurs en C++: lorsque la tâche consiste à
additionner des résistances, on utilise l’opérateur + sur des
objets de type RESISTANCE, on affecte le résultat à un objet de
type RESISTANCE grâce à la surdéfinition de l’opérateur = et le
tour est joué! On peut même afficher le résultat à l’écran grâce
à la surdéfinition de l’opérateur d’insertion << . Le programme
se lit facilement et est très concis.

Exercice
463 Ecrivez une fonction de surdéfinition des opérateurs =, + et ||
pour les combinaisons en série et en parallèle de
condensateurs. Rappelez-vous que la combinaison parallèle
(opérateur ||) de deux condensateurs se calcule comme suit:

C paral = C 1 + C 2 EQ 5

et que la combinaison série (opérateur +) s’obtient de


l’expression suivante:

294 Programmation en C++ IFT-19965


C
++

C1 C 2
C ser = ------------------- EQ 6
C1 + C 2

Résumé
464 ✔ La surdéfinition d’opérateurs binaires comme fonctions
membres d’une classe comprend un argument implicite
(opérande de gauche), un argument ordinaire passé par
référence (opérande de droite) et une valeur de retour.
✔ Il faut utiliser le passage de paramètres par référence avec
prudence en utilisant la notion de référence constante (const)
lorsque nécessaire.

Programmation en C++ IFT-19965 295


C
++

296 Programmation en C++ IFT-19965


C
++

CHAPITRE 41 La conception d’un


programme à partir de
plusieurs fichiers

465 Jusqu’à maintenant, chaque fonction membre que nous avons


créée pour les classes de la hiérarchie ont été ajoutées
directement dans la définition de la classe. Or, cette pratique a
un inconvénient majeur, soit celui d’allonger considérablement
le fichier source de la définition de la classe, violant ainsi le
principe de visibilité locale énoncé au paragraphe 218 du
CHAPITRE 17.

466 Rappelons-nous cependant, que, lorsque nous avons vu les


principes de base de définition de fonctions membres au
CHAPITRE 10, il existe un moyen de séparer la déclaration
d’une fonction membre d’une classe de sa définition grâce à
l’opérateur d’évaluation de portée :: .

467 Ce chapitre présente une façon très pratique de séparer la


déclaration des fonctions membres d’une classe de leur
définition. Ainsi, la classe est définie dans un fichier
d’inclusion portant l’extension .h. Les fonctions membres ne
sont que déclarées dans ce fichier d’inclusion. Elles sont
définies dans un fichier source d’extension .C qui est
compilé séparément. Le programme principal est ensuite

Programmation en C++ IFT-19965 297


++ C
compilé et lié aux modules correspondant à chaque classe avec
l’éditeur de liens et les différentes options qu’il offre.

468 Revenons aux classes des chapitres précédents. Nous avons la


liste des classes suivantes:
1. CIRCUIT
2. ANALOGIQUE
3. NUMERIQUE
4. NAND_2
5. NOR_2
6. RESISTANCE
7. CONDENSATEUR
Chaque classe est décrite par un fichier d’inclusion d’extension
.h. Le code de chaque fonction membre fait partie intégrante
des définitions des classes. Ce que nous proposons de faire est
de
1. créer un fichier d’inclusion d’extension .h pour chaque classe
contenant la définition de la classe mais seulement la
déclaration des fonctions membres,
2. créer un fichier de code source d’extension .C contenant la
définition des fonctions membres de la classe.

469 Voyons comment on peut traiter la classe CIRCUIT pour tenir


compte des règles du paragraphe 468. La définition de la classe
devient:
//&% circuit_41_1.h

#include <iostream.h>

#ifndef CIRCUIT_FLAG_
#define CIRCUIT_FLAG_

class CIRCUIT {
private:
double v_num_plus; //Ten. num. positive (en volts)
double v_num_moins; //Ten. num. negative (en volts)
double v_analog_plus; //Ten. an. positive (en volts)
int numero; //Numero du composant

public:

298 Programmation en C++ IFT-19965


C
++

CIRCUIT ();
CIRCUIT (double, double, double, int);
double read_v_num_plus();
double read_v_num_moins ();
double read_v_analog_plus ();
int read_numero ();
void set_v_num_plus(double);
void set_v_num_moins(double);
void set_v_analog_plus(double);
void set_numero(int);
virtual char *affiche_type();
virtual double calcule_courant();
};

#endif CIRCUIT_FLAG_

470 et le code de définition des fonctions membres de la classe


CIRCUIT est:
//&% circuit_41_1.C
//Definitions des fonctions membres de la classe CIRCUIT

#include <iostream.h>
#include “circuit_41_1.h”

//Constructeur defaut

CIRCUIT::CIRCUIT () {
v_num_plus = 5.0;
v_num_moins = 0.0;
v_analog_plus = 12.0;
numero = 0;
}

// Constructeur avec initialisation des variables membres

CIRCUIT::CIRCUIT (double v_n_p, double v_n_m, double v_a_p,


int no) {
v_num_plus = v_n_p;
v_num_moins = v_n_m;
v_analog_plus = v_a_p;
numero = no;
}

// Readers

double CIRCUIT::read_v_num_plus() {

Programmation en C++ IFT-19965 299


C
++

return v_num_plus;
}
double CIRCUIT::read_v_num_moins () {
return v_num_moins;
}
double CIRCUIT::read_v_analog_plus () {
return v_analog_plus;
}
int CIRCUIT::read_numero () {
return numero;
}

// Writers

void CIRCUIT::set_v_num_plus(double v) {
v_num_plus = v;
}
void CIRCUIT::set_v_num_moins(double v) {
v_num_moins = v;
}
void CIRCUIT::set_v_analog_plus(double v) {
v_analog_plus = v;
}
void CIRCUIT::set_numero(int n) {
numero = n;
}

// Affichage du type de composant


char * CIRCUIT::affiche_type() {return “Circuit”;}

//Fonction de calcul du courant


double CIRCUIT::calcule_courant() {return 0.0;}

471 La définition de la classe ANALOGIQUE devient;


//&% analogique_41_1.h
#include <iostream.h>
#include “circuit_41_1.h”

#ifndef ANALOGIQUE_FLAG_
#define ANALOGIQUE_FLAG_

class ANALOGIQUE : public CIRCUIT {


private:
double puissance_max;
int type;

public:

300 Programmation en C++ IFT-19965


C
++

ANALOGIQUE ();
ANALOGIQUE (double, int);
double read_puissance_max();
int read_type();
void set_puissance_max(double);
void set_type(int);
virtual char *affiche_type();
virtual double calcule_courant();
};

#endif ANALOGIQUE_FLAG_

472 le code de définition des fonctions membres de la classe


ANALOGIQUE est:
//&% analogique_41_1.C
//Definitions des fonctions membres de la classe ANALOGIQUE

#include <iostream.h>
#include “circuit_41_1.h”
#include “analogique_41_1.h”

// Constructeur par defaut


ANALOGIQUE::ANALOGIQUE () {}

// Constructeur avec arguments


ANALOGIQUE::ANALOGIQUE (double p_max, int typ) {
puissance_max = p_max;
type = typ;
}

//Readers
double ANALOGIQUE::read_puissance_max() {
return puissance_max;
}
int ANALOGIQUE::read_type() {
return type;
}

//Writers
void ANALOGIQUE::set_puissance_max(double p_max) {
puissance_max = p_max;
}
void ANALOGIQUE::set_type(int typ) {
type = typ;
}

// Affichage du type de composant

Programmation en C++ IFT-19965 301


C
++

char *ANALOGIQUE::affiche_type() {return “Analogique”;}

//Calcul du courant
double ANALOGIQUE::calcule_courant() {return 0.0;}

473 La définition de la classe NUMERIQUE:


//&% numerique_41_1.h
#include <iostream.h>
#include “circuit_41_1.h”

#ifndef NUMERIQUE_FLAG_
#define NUMERIQUE_FLAG_

class NUMERIQUE : public CIRCUIT {


private:
int fan_out; //Nombre admissible de sorties

public:

NUMERIQUE ();
NUMERIQUE (int);
int read_fan_out();
void set_fan_out(int);
virtual char *affiche_type();
};

#endif NUMERIQUE_FLAG_

474 le code de définition des fonctions membres de la classe


NUMERIQUE est:
//&% numerique_41_1.C
//Definition des fonctions dela classe NUMERIQUE

#include <iostream.h>
#include “circuit_41_1.h”
#include “numerique_41_1.h”

// Constructeur par defaut


NUMERIQUE::NUMERIQUE () {
fan_out = 10;
}

// Constructeur avec arguments


NUMERIQUE::NUMERIQUE (int fn_out) {
fan_out = fn_out;

302 Programmation en C++ IFT-19965


C
++

// Readers
int NUMERIQUE::read_fan_out() {
return fan_out;
}

// Writers
void NUMERIQUE::set_fan_out(int f_out) {
fan_out = f_out;
}

// Affichage du type de composant


char * NUMERIQUE::affiche_type() {return “Numerique”;}

475 La définition de la classe NAND_2 s’écrit:


//&% nand_2_41_1.h

#include <iostream.h>
#include “circuit_41_1.h”
#include “numerique_41_1.h”

#ifndef NAND_FLAG_
#define NAND_FLAG_

class NAND_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1();

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

NAND_2();
NAND_2(int, int);
int lire_in_1();
int lire_in_2();
int lire_out_1();
void ecrire_in_1(int);
void ecrire_in_2(int);
int operator ~ ();
virtual char *affiche_type();
};

Programmation en C++ IFT-19965 303


C
++

#endif NAND_FLAG_

476 et le code des fonctions membres de la classe NAND_2 est:


//&% nand_2_41_1.C
//Definition des fonctions membres de la classe NAND_2

#include <iostream.h>
#include “circuit_41_1.h”
#include “numerique_41_1.h”
#include “nand_2_41_1.h”

int NAND_2::set_out_1() {
return 1 - (in_1 * in_2);
}

//Constructeur par defaut


NAND_2::NAND_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2::NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

//Readers
int NAND_2::lire_in_1() {
return in_1;
}

int NAND_2::lire_in_2() {
return in_2;
}

int NAND_2::lire_out_1() {
return out_1;
}

//Writers
void NAND_2::ecrire_in_1(int val) {
in_1 = val;
out_1 = set_out_1();
}

void NAND_2::ecrire_in_2(int val) {

304 Programmation en C++ IFT-19965


C
++

in_2 = val;
out_1 = set_out_1();
}

// Surdefinition des operateurs


int NAND_2::operator ~ () {
if (lire_out_1()) return 0;
else return 1;
}

// Affichage du type de composant


char * NAND_2::affiche_type() {return “NAND_2”;}

477 Celle de la classe NOR_2 devient:


//&% nor_2_41_1.h

#include <iostream.h>
#include “circuit_41_1.h”
#include “numerique_41_1.h”

#ifndef NOR_FLAG_
#define NOR_FLAG_

class NOR_2 : public NUMERIQUE {

private:
int out_1; /*sortie */
int set_out_1();

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

NOR_2();
NOR_2(int, int);
int lire_in_1();
int lire_in_2();
int lire_out_1();
void ecrire_in_1(int);
void ecrire_in_2(int);
int operator ~ ();
virtual char *affiche_type();
};

#endif NOR_FLAG_

Programmation en C++ IFT-19965 305


C
++

478 Le code des fonctions membres de la classe NOR_2 est:


//&% nor_2_41_1.C

#include <iostream.h>
#include “circuit_41_1.h”
#include “numerique_41_1.h”
#include “nor_2_41_1.h”

int NOR_2::set_out_1() {
return ((1-in_1) * (1-in_2));
}

//Constructeur par defaut


NOR_2::NOR_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NOR_2::NOR_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

//Readers
int NOR_2::lire_in_1() {
return in_1;
}
int NOR_2::lire_in_2() {
return in_2;
}
int NOR_2::lire_out_1() {
return out_1;
}

//Writers
void NOR_2::ecrire_in_1(int val) {
in_1 = val;
out_1 = set_out_1();
}
void NOR_2::ecrire_in_2(int val) {
in_2 = val;
out_1 = set_out_1();
}

// Surdefinition des operateurs


int NOR_2::operator ~ () {
if (lire_out_1()) return 0;

306 Programmation en C++ IFT-19965


C
++

else return 1;
}
// Affichage du type de composant
char * NOR_2::affiche_type() {return “NOR_2”;}

479 La classe RESISTANCE prend la forme suivante:


//&% resis_2_41_1.h

#include <iostream.h>
#include “circuit_41_1.h”
#include “analogique_41_1.h”

#ifndef RESISTANCE_FLAG_
#define RESISTANCE_FLAG_

class RESISTANCE : public ANALOGIQUE {

private:
double res;
double tolerance;

public:

RESISTANCE();
RESISTANCE(double, double);
double lire_resistance() const;
double lire_tolerance() const;
void ecrire_resistance(double);
void ecrire_tolerance(double);
const RESISTANCE& operator= (const RESISTANCE&);
RESISTANCE operator+ (const RESISTANCE&)const;
RESISTANCE operator|| (const RESISTANCE&)const;
virtual char *affiche_type();
virtual double calcule_courant();
};

#endif RESISTANCE_FLAG_

480 Le code des fonctions membres de RESISTANCE est:


//&% resis_2_41_1.C

#include <iostream.h>
#include “circuit_41_1.h”
#include “analogique_41_1.h”
#include “resis_2_41_1.h”

Programmation en C++ IFT-19965 307


C
++

//Constructeur par defaut


RESISTANCE::RESISTANCE() {}

//Constructeur avec deux arguments


RESISTANCE::RESISTANCE(double resi, double tol) {
res = resi; tolerance = tol;
}

//Readers
double RESISTANCE::lire_resistance() const {
return res;
}

double RESISTANCE::lire_tolerance() const {


return tolerance;
}

//Writers
void RESISTANCE::ecrire_resistance(double val) {
res = val;
}

void RESISTANCE::ecrire_tolerance(double val) {


tolerance = val;
}

//Surdefinition des operateurs

const RESISTANCE& RESISTANCE::operator= (const RESISTANCE&


right_res) {
res = right_res.lire_resistance();
tolerance = right_res.lire_tolerance();
return *this;
}

RESISTANCE RESISTANCE::operator+ (const RESISTANCE&


right_res) const {
RESISTANCE res_temp;

res_temp.ecrire_resistance(res +
right_res.lire_resistance());
res_temp.ecrire_tolerance(right_res.lire_tolerance());
return res_temp;
}

RESISTANCE RESISTANCE::operator|| (const RESISTANCE&


right_res) const {
RESISTANCE res_temp;

308 Programmation en C++ IFT-19965


C
++

res_temp.ecrire_resistance((res *
right_res.lire_resistance()) /
(res + right_res.lire_resistance()));
res_temp.ecrire_tolerance(right_res.lire_tolerance());
return res_temp;
}

// Affichage du type de composant

char * RESISTANCE::affiche_type() {return “RESISTANCE”;}

//Calcul du courant
double RESISTANCE::calcule_courant() {
return read_v_analog_plus() / res;
}

481 La classe CONDENSATEUR s’écrit finalement:


//&% condens_2_41_1.h

#include <iostream.h>
#include “circuit_41_1.h”
#include “analogique_41_1.h”

#ifndef CONDENSATEUR_FLAG_
#define CONDENSATEUR_FLAG_

class CONDENSATEUR : public ANALOGIQUE {

private:
double capacite;
double v_claquage;

public:
CONDENSATEUR();
CONDENSATEUR(double, double);
double lire_capacite();
double lire_v_claquage();
void ecrire_capacite(double);
void ecrire_v_claquage(double);
virtual char *affiche_type();
virtual double calcule_courant();
};

#endif CONDENSATEUR_FLAG_

482 et le code des fonctions membres de CONDENSATEUR s’écrit:

Programmation en C++ IFT-19965 309


C
++

//&% condens_2_41_1.C

#include <iostream.h>
#include “circuit_41_1.h”
#include “analogique_41_1.h”
#include “condens_2_41_1.h”

//Constructeur par defaut


CONDENSATEUR::CONDENSATEUR() {}

//Constructeur avec deux arguments


CONDENSATEUR::CONDENSATEUR(double cap, double claq) {
capacite = cap; v_claquage = claq;
}

//Readers
double CONDENSATEUR::lire_capacite() {
return capacite;
}
double CONDENSATEUR::lire_v_claquage() {
return v_claquage;
}

//Writers
void CONDENSATEUR::ecrire_capacite(double val) {
capacite = val;
}
void CONDENSATEUR::ecrire_v_claquage(double val) {
v_claquage = val;
}

// Affichage du type de composant


char * CONDENSATEUR::affiche_type() {return
“CONDENSATEUR”;}

//Calcul du courant
double CONDENSATEUR::calcule_courant(){return 0.0;}

483 On remarque que le fichier d’extension .h de chaque classe ne


renferme que les déclarations des fonctions membres avec la
valeur de retour, le nom de la fonction et la liste des paramètres
avec leur type. Pour sa part, le fichier d’extension .C contient le
code des fonctions membres. Pour identifier le fait que la
fonction membre est associée à une classe spécifique, on ajoute
le nom de la classe au nom de la fonction grâce à l’opérateur
d’évaluation de portée ::. Ainsi, deux fonctions membres
portant le même nom ne peuvent être confondues par le

310 Programmation en C++ IFT-19965


C
++

compilateur parce que le nom de la classe apparaissant avant


le nom de la fonction vient lever l’ambiguïté.

484 Il faut aussi remarquer que le mot virtual ne doit apparaître


que dans la déclaration d’une fonction virtuelle dans la classe
et non dans la définition de cette fonction.

485 On peut maintenant compiler chaque fichier source d’extension


.C pour produire un fichier objet d’extension .o. Pour ce faire,
il faut utiliser l’option -c du compilateur CC. Cette option
informe le compilateur de ne produire que le code objet du
fichier source sans procéder à l’édition de lien:
CC -c circuit_41_1.C
CC -c analogique_41_1.C
CC -c numerique_41_1.C
CC -c nand_2_41_1.C
CC -c nor_2_41_1.C
CC -c resis_2_41_1.C
CC -c condens_2_41_1.C

486 Si on reprend l’exemple du CHAPITRE 40 pour le calcul de la


résistance en série et en parallèle, on a le code suivant:
//&% sur_def_41_1.C

#include <iostream.h>
#include “circuit_41_1.h”
#include “analogique_41_1.h”
#include “resis_2_41_1.h”

ostream& operator << (ostream& output_stream, RESISTANCE&


r) {
output_stream << r.lire_resistance();
return output_stream;
}

void main() {

RESISTANCE res_serie, res_parallele;


RESISTANCE r_1(1000,10), r_2(1000,10), r_3(1000,10);

res_serie = r_1 + r_2 + r_3;


res_parallele = r_1 || r_2 || r_3;

Programmation en C++ IFT-19965 311


++C
cout << “Resistance serie: “ << res_serie << endl;
cout << “Resistance parallele: “ << res_parallele << endl;
}

On doit maintenant compiler cette source pour tenir compte du


fait que le code objet des classes CIRCUIT, ANALOGIQUE et
RESISTANCE est maintenant contenu dans les fichiers objets
d’extension .o. On procède comme suit:
CC sur_def_41_1.C -o sur_def_41_1 circuit_41_1.o \
analogique_41_1.o resis_2_41_1.o

Cette ligne signifie qu’on veut:


1. compiler le programme source sur_def_41_1.C
2. donner au fichier exécutable le nom sur_def_41_1
3. et procéder à l’édition de lien du programme avec les modu-
les d’extension .o nécessaires à l’application soit
circuit_41_1.o, analogique_41_1.o, et
resis_2_41_1.o. Le symbole \ sert simplement à étaler la
ligne de commande de compilation sur deux lignes physiques
de la fenêtre de commande.
En lançant ainsi le programme sur_def_41_1, on obtient
évidemment les mêmes résultats qu’au CHAPITRE 40. La
seule chose qui diffère est que le code est beaucoup mieux
organisé et que sa mise à jour est maintenant plus facile parce
que la définition de la classe (contenue dans le fichier
d’extension .h) est plus courte étant donné que le code des
fonctions membres est maintenant reporté dans un autre
fichier (celui d’extension .C). On peut prendre connaissance
plus rapidement et plus aisément du contenu de la classe sans
s’attarder à la façon dont les fonctions membres sont
implantées.

Résumé
487 ✔ Il est préférable, pour respecter les principes de
programmation par objets, de conserver la définition d’une
classe et le code des fonctions membres dans des fichiers
séparés.
✔ La définition de la classe est placée dans le fichier
d’extension .h et le code d’implantation des fonctions membres
est placé dans un fichier source d’extension .C aussi appelé
module.

312 Programmation en C++ IFT-19965


C
++

✔ Pour compiler un module, il suffit d’utiliser la commande CC


avec l’option -c. Cette commande produit un fichier objet
d’extension .o.
✔ Pour procéder à l’édition de liens entre le programme
principal et les différents modules il suffit de compiler le
programme en énumérant ensuite les modules avec lesquels il
doit être lié et auxquels il fait appel.

Programmation en C++ IFT-19965 313


C
++

314 Programmation en C++ IFT-19965


C
++

CHAPITRE 42 L’utilitaire make:


notions élémentaires

488 Nous avons vu au CHAPITRE 41 qu’il est intéressant de


séparer la définition d’une classe et les fonctions membres dans
des fichiers différents afin de respecter les principes de
programmation par objets, notamment le principe de
visibilité. Or, dans un projet de développement logiciel
d’envergure, il peut y avoir des dizaines de classes et donc
plusieurs fichiers à manipuler. Or, les fichiers dépendent
souvent les uns des autres. Si on pense aux exemples traités
jusqu’à maintenant, on compte 7 classes: CIRCUIT,
ANALOGIQUE, NUMERIQUE, NAND_2, NOR_2, RESISTANCE et
CONDENSATEUR. Comme les classes forment une hiérarchie, un
changement à la classe CIRCUIT se répercute sur toutes les
sous-classes tandis qu’un changement à la classe NAND_2 n’a
une influence que sur cette classe. De plus, comme les fichiers
d’extension .o dépendent du contenu des fichiers d’extension
.h, il devient très difficile de garder en mémoire toutes les
dépendances entre ces fichiers et encore plus difficile de se
rappeler les opérations de compilation nécessaires à la mise à
jour d’un programme principal lorsqu’un changement est
apporté à l’une des classes ou à la façon dont une fonction
membre d’une classe est implantée.

Programmation en C++ IFT-19965 315


C
++

489 Dans la hiérarchie de classes que nous utilisons dans le cadre


des notes, nous pouvons construire un schéma décrivant les
dépendances entre les fichiers. Chaque flèche du diagramme

circuit_41_1.h

circuit_41_1.C

circuit_41_1.o

analogique_41_1.h

analogique_41_1.C

analogique_41_1.o

resis_2_41_1.h

resis_2_41_1.C

resis_2_41_1.o
sur_def_41_1.C

sur_def_41_1.o

sur_def_41_1

316 Programmation en C++ IFT-19965


C
++

pointe d’un fichier à un fichier qui dépend de son contenu. Les


fichiers d’extension .o dépendent des fichiers d’extension .C et
de ceux d’extension .h. Finalement, le programme principal
sur_def_41_1 dépend de tous les fichiers d’extension .o.
Donc, si on change le contenu du fichier circuit_41_1.h, il
faut recompiler circuit_41_1.C, resis_2_41_1.C,
analogique_41_1.C et sur_def_41_1.C. Par contre, si on
change le fichier resis_2_41_1.h, il faut recompiler
resis_2_41_1.C et sur_def_41_1.C. Il n’est donc pas facile
de se rappeler exactement de la séquence des opérations à
suivre lors du développement d’un programme.

490 Heureusement, Unix offre un utilitaire appelé make facilitant


grandement le développement de programmes impliquant de
nombreux fichiers ayant divers niveaux de dépendance. Ce
chapitre présente les principes de base de l’utilitaire make. Le
CHAPITRE 43 s’intéresse aux notions plus avancées de make.
Un moyen de connaître les dépendances d’une source aux
autres sources est d’utiliser l’option -xM du compilateur:
CC -xM nomDuFichierSource.C

491 Sous sa forme la plus simple, make traite des relations de


dépendance dans un fichier appelé makefile et résidant dans
le répertoire courant. Les relations de dépendance, qui peuvent
être réparties sur plusieurs lignes du fichier makefile,
expriment les relations existant entre les fichiers en spécifiant
pour un fichier cible les fichiers prérequis desquels il
dépend. Si la modification d’un fichier prérequis est plus
récente que celle d’un fichier cible, make remet à jour ce fichier
cible en se basant sur des commandes de construction
contenues sur une ligne exprimant la relation de dépendance
dans le makefile. L’utilitaire make interrompt le traitement
s’il rencontre une erreur lors du processus de construction.

492 La forme la plus simple d’un fichier makefile1 adopte le


format suivant:

1. l’expression “fichier makefile” est souvent remplacée par l’expression abrégée “makefile”

Programmation en C++ IFT-19965 317


C
++

cible: liste de prérequis


(tabulation) commandes de construction

la ligne exprimant une dépendance se compose de la cible et de


la liste des prérequis. La cible et la liste sont séparés par le
caractère :. La ligne de construction doit
OBLIGATOIREMENT commencer par un symbole de
tabulation1 et suivre la ligne de dépendance.
La cible est simplement le nom du fichier qui dépend des
fichiers contenus dans la liste des prérequis. Les commandes de
construction sont des commandes standard de Unix qui
permettent de reconstruire la cible (généralement en la
compilant et/ou en procédant à l’édition de liens). L’utilitaire
make exécute la commande de construction lorsque la date (et
l’heure) de modification de l’un ou l’autre des fichiers de la
liste des prérequis est plus récente que celle de la cible, ce qui
signifie que la cible n’est plus à jour.

493 Reprenons l’exemple du calcul de la résistance parallèle et série


du CHAPITRE 41 et voyons comment on peut faciliter la mise à
jour du programme grâce à l’utilitaire make opérant sur un
fichier makefile. Un makefile fonctionnel mais non
nécessairement optimal pour cet exemple serait le suivant:
sur_def_41_1: circuit_41_1.o analogique_41_1.o resis_2_41_1.o

CC sur_def_41_1.C -o sur_def_41_1 circuit_41_1.o


analogique_41_1.o resis_2_41_1.o

circuit_41_1.o: circuit_41_1.h circuit_41_1.C

CC -c circuit_41_1.C

analogique_41_1.o: analogique_41_1.h analogique_41_1.C


circuit_41_1.h

CC -c analogique_41_1.C

resis_2_41_1.o: resis_2_41_1.h resis_2_41_1.C analogique_41_1.h


circuit_41_1.h

CC -c resis_2_41_1.C

1. communément appelé “tab”

318 Programmation en C++ IFT-19965


C
++

Supposons que le fichier circuit_41_1.h ait été modifié1. En


tapant simplement la commande:
make

dans le répertoire courant, on déclenche la séquence d’actions


suivantes:
saguenay% make
CC -c circuit_41_1.C
CC -c analogique_41_1.C
CC -c resis_2_41_1.C
CC sur_def_41_1.C -o sur_def_41_1 circuit_41_1.o\
analogique_41_1.o resis_2_41_1.o

On remarque que comme le fichier circuit_41_1.h est


important pour plusieurs fichiers, la commande make permet
de recompiler tous les modules affectés par un changement.
Par ailleurs, si on ne change que le fichier resis_2_41_1.h,
voyons ce que la commande make déclenchera comme
opérations:
saguenay% make
CC -c resis_2_41_1.C
CC sur_def_41_1.C -o sur_def_41_1 circuit_41_1.o\
analogique_41_1.o resis_2_41_1.o

Ici, comme resis_2_41_1.h n’a d’influence que sur


resis_2_41_1.C et sur sur_def_41_1, seuls ces fichiers sont
recompilés selon les instructions contenues dans les lignes de
constructions pour chaque relation de dépendance contenue
dans le fichier makefile. L’utilisation d’un makefile et de
l’utilitaire make a donc l’avantage d’éviter au programmeur
d’avoir à se rappeler de toutes les modifications qui ont été
effectuées et de l’implication de ces changements sur la
séquence des opérations à effectuer pour mettre à jour le
programme principal.

1. un moyen simple de “simuler” la modification d’un fichier pour vérifier le fonctionnement


d’un makefile est d’utiliser la commande touch nom_du_fichier. Cette commande change la
date et l’heure de modification du fichier ce qui a comme conséquence de faire comme si le
fichier avait été effectivement modifié.

Programmation en C++ IFT-19965 319


C
++

320 Programmation en C++ IFT-19965


C
++

CHAPITRE 43 L’utilitaire make: notions


avancées

494 Jusqu’à maintenant, nous avons gardé les fichiers d’extension


.h, .C, .o et les programmes exécutables dans le même
répertoire. Cette pratique est acceptable lorsqu’on manipule un
petit nombre de fichiers mais devient vite très lourde lorsqu’un
grand nombre de fichiers doit être traité. En général, dans un
projet de développement de logiciel d’envergure, on tente de
garder les fichiers d’extension .h dans un répertoire appelé
include, les fichiers source d’extension .C dans un répertoire
appelé source (ou également src) incluant le fichier makefile
et les fichiers objets d’extension .o, et le programme exécutable
dans un répertoire appelé bin. La structure des répertoires du
projet que nous traitons prend donc l’allure de la FIGURE 5.
En adoptant une telle hiérarchie de répertoires, il devient plus
facile de repérer intuitivement un fichier.

495 Le makefile du paragraphe 493 est intéressant mais ne


demeure pas moins assez difficile à construire parce qu’il tient
compte explicitement de toutes les dépendances.
Heureusement, l’utilitaire make est conçu de telle façon qu’il
peut lui-même tenir implicitement compte d’un grand
nombre de dépendances et de commandes de construction, ce
qui facilite grandement l’écriture d’un makefile.

Programmation en C++ IFT-19965 321


C
++

FIGURE 5 Structure des répertoires d’un projet type.

Projet_41_1

include_41_1 source_41_1 bin_41_1

circuit_41_1.h circuit_41_1.C
sur_def_41_1
analogique_41_1.h analogique_41_1.C
resis_2_41_1.h resis_2_41_1.C

sur_def_41_1.C

circuit_41_1.o

resis_2_41_1.o

analogique_41_1.o

makefile

L’utilisateur peut également définir lui-même ses propres


règles implicites1.

496 Comme exemple de règle implicite, si aucune ligne de


construction n’est incluse dans le makefile pour un fichier
objet d’extension .o, make assume une dépendance avec un
fichier source de même nom permettant de construire la cible
d’extension .o2. Donc, si un prérequis pour une cible est le
fichier fff.o et qu’il n’y a aucune ligne avec une commande de
construction suivant la ligne de dépendance, l’utilitaire make
cherche alors dans le répertoire courant si un fichier ayant

1. Appelées “suffix rules” en anglais


2. make cherche donc un fichier ayant le même nom que le fichier .o d’extension .c pouvant
être compilé avec le compilateur cc, un fichier d’extension .C pouvant être compilé avec CC ou
finalement un fichier d’extension .s pouvant être assemblé avec as

322 Programmation en C++ IFT-19965


C
++

l’une des extension contenues dans la table ci-dessous existe. Si


tel est le cas, make invoque alors une commande de
construction par défaut1 adaptée au fichier source en appelant
le compilateur, l’assembleur, ou l’utilitaire approprié pour créer
une version à jour de la cible.

Nom fichier Type


fff.C Code source en C++
fff.c Code source en C
fff.r Code source en RATFOR
fff.f Code source en FORTRAN77
fff.y Code source en YACC
fff.l Code source en Lex
fff.s Code source en assembleur
fff.mod Code source en Modula 2
fff.p Code source en Pascal
fff.sh Code source pour un “shell script”
C++, C, RATFOR, FORTRAN77, Modula 2, et Pascal sont des
langages de programmation standard sous Unix tandis que
YACC, Lex et “shell script” sont des utilitaires pour créer
des langages de commandes.

497 Pour éviter une répétition fastidieuse de noms de fichiers dans


ce makefile, make offre deux caractéristiques importantes: les
macros et les règles implicites de dépendance.

498 Une définition de macro adopte la syntaxe suivante:


nom = chaîne de caractères

Les références à cette macro se font en adoptant la syntaxe:


$(nom)

ou encore
${nom}

1. Qu’il va chercher dans ses propres fichiers de configuration

Programmation en C++ IFT-19965 323


C
++

comme nous le verrons, l’utilisation de macros permet entre


autres de modifier facilement le makefile pour changer les
répertoires où sont stockés les différents fichiers. Notez
finalement qu’en général, on utilise par convention des lettres
majuscules pour définir les macros.

499 Afin de clarifier plusieurs points techniques, nous allons


présenter un makefile pour effectuer la même tâche que celui
du paragraphe 493 mais de façon plus flexible et plus
universelle1. Le nouveau makefile est le suivant:
#*******************************************************
# Makefile pour sur_def_41_1 *
#*******************************************************

#*******************************************************
# Definition des macros *
#*******************************************************

PROGRAMME = sur_def_41_1
OBJECTS = circuit_41_1.o analogique_41_1.o resis_2_41_1.o
INC_DIR = ../include_41_1
BIN_DIR = ../bin_41_1
INCLUDES = $(INC_DIR)/circuit_41_1.h\
$(INC_DIR)/ analogique_41_1.h\
$(INC_DIR)/resis_2_41_1.h
CC = CC
CFLAGS =
CPPFLAGS = -I$(INC_DIR)

#*******************************************************
# Definition des regles implicites de dependance *
#*******************************************************

.SUFFIXES : .o .C

.C.o :
$(CC) $(CFLAGS) $(CPPFLAGS) -c $<

#*******************************************************
# Regle de construction du programme principal *
#*******************************************************

1. make est un utilitaire écrit par des nerds, documenté par des nerds et utilisé par des nerds. En
résumé, make est un utilitaire particulièrement difficile à maîtriser par une personne ordinaire!

324 Programmation en C++ IFT-19965


C
++

$(PROGRAMME) : $(OBJECTS) $(INCLUDES)


$(CC) $(PROGRAMME).C -o $(PROGRAMME) $(CPPFLAGS)\
$(CFLAGS) $(OBJECTS)

#*******************************************************
# Regle de construction des objets *
#*******************************************************

$(OBJECTS) : $(INCLUDES)

#*******************************************************
# Regle de nettoyage des fichiers objets *
#*******************************************************

# On efface les fichiers .o pour


# economiser de l’espace disque
# en executant la commande make clean
# On copie ensuite le fichier executable
# dans le repertoire bin

clean:
rm *.o; mv $(PROGRAMME) $(BIN_DIR)

Le makefile, situé dans le répertoire source_41_1,


commence par un en-tête identifiant son utilité, soit celle de
construire le programme exécutable sur_def_41_1. Dans
l’utilitaire make, une ligne de commentaire commence par le
caractère #. Le fichier makefile contient ensuite un ensemble
de définitions de macros respectant la syntaxe définie au
paragraphe 498. La macro PROGRAMME est associée au nom du
programme sur_def_41_1 tandis que la macro OBJECTS est
associée aux noms des fichiers objets pertinents au projet
(circuit_41_1.o, analogique_41_1.o et
resis_2_41_1.o). Les macros INC_DIR et BIN_DIR
identifient le chemin où l’on peut trouver les fichiers include
d’extension .h et le fichier exécutable (binaire) par rapport au
répertoire dans lequel se trouve le makefile (ici, le répertoire
source_41_1). La macro INCLUDES est associée aux fichiers
d’inclusion d’extension .h du projet. On remarque que la
définition d’une macro peut utiliser une macro déjà définie (ici
la macro INC_DIR) en y reférant grâce à la syntaxe
$(NOM_MACRO) (ici, $(INC_DIR)).

Programmation en C++ IFT-19965 325


++ C
Les trois macros suivantes (CC, CFLAGS et CPPFLAGS) sont des
macros prédéfinies de make1. Cela signifie que si
l’utilisateur ne définit pas lui-même ces macros, elles sont
quand même accessibles dans make et prennent des valeurs
par défaut. Si l’utilisateur définit lui-même les macros, c’est sa
définition qui a la priorité sur la définition par défaut.
La macro CC est associée au compilateur qui sera utilisé dans
l’application, ici nous choisissons le compilateur C++ ayant
pour nom CC. La macro CFLAGS est incluse mais n’est pour
l’instant associée à rien. Cependant, si l’utilisateur veut plus
tard lui associer quelque chose, il peut le faire en ne faisant
qu’ajouter cette information dans le makefile. Il n’a alors qu’a
changer une ligne du fichier makefile, celle contenant la
définition de la macro CFLAGS2. La macro CPPFLAGS est
associée à une option du compilateur CC3, l’option -I qui
permet au préprocesseur de chercher les fichiers include
d’extension .h contenus entre guillemets dans les fichiers
sources d’extension .C dans le répertoire identifié après
l’option -I. Par exemple, dans le fichier source circuit_41_1.C,
on retrouve l’instruction de préprocesseur
#include “circuit_41_1.h”

ayant pour effet d’aller chercher les informations contenues


dans le fichier circuit_41_1.h et de les inclure dans le
fichier circuit_41_1.C. Cependant, comme nous le savons,
cette instruction cherche le fichier d’extension .h dans le
répertoire courant. Or, de par la structure des répertoires de
notre application, tous les fichiers d’extension .h sont dans le
répertoire include_41_1 alors que les fichiers sources
d’extension .C sont dans le répertoire source_41_1. Le
préprocesseur ne pourra pas trouver les fichiers include si on
ne lui dit pas où aller les chercher. C’est précisément la fonction
de l’option -I du compilateur qu’on associe avec la macro

1. make contient un très grand nombre de macros prédéfinies. Consulter un livre de référence sur
make pour plus de détails.
2. Par exemple, l’usager peut être intéressé à utiliser un programme de debugging pour le déve-
loppement de son programme. La documentation du compilateur CC indique qu’il faut ajouter
l’option -g lorsque le programme est compilé si on veut utiliser le debugger. Il suffit alors de défi-
nir la macro CFLAGS comme étant justement -g et de lancer le makefile.
3. Pour plus d’informations sur les nombreuses options du compilateur CC, faire la commande
man CC.

326 Programmation en C++ IFT-19965


C
++

CPPFLAGS qui utilise également la macro $(INC_DIR). On


peut mettre autant d’options -I qu’il y a de répertoires à
traiter.

500 Suite aux définitions des macros, le makefile contient la


définition des règles de dépendance implicites qui seront
considérées par make dans les commandes de construction. Tel
que mentionné au paragraphe 496, make comporte des règles
de dépendance par défaut1. L’usager peut de surcroît établir
lui-même ses propres règles de dépendance implicites. C’est ce
qui est fait dans les lignes suivantes du makefile:
.SUFFIXES : .o .h .C

.C.o :
$(CC) $(CFLAGS) $(CPPFLAGS) -c $<

La ligne .SUFFIXES : .o .C ressemble à une ligne de


dépendance avec la différence qu’elle informe make que les
suffixes qu’il devra considérer dans les commandes de
constructions sont ceux associés aux fichiers d’extension .o et
.C. Suivent ensuite la définition des règles implicites que devra
suivre make. La première règle s’intéresse à la dépendance des
fichiers .o aux fichiers .C. Dans ce cas, la règle implicite
demande simplement de compiler le fichier source si le fichier
objet est plus récent. C’est ce que signifie la règle implicite de
construction:
$(CC) $(CFLAGS) $(CPPFLAGS) -c $<

qui est particulièrement flexible à cause de l’utilisation des


macros. La règle de construction contient $<, une autre macro
prédéfinie de make. La macro $< est associée au prérequis
nécessaire à la cible courante. Par exemple, si make est à
vérifier que le fichier circuit_41_1.o est moins récent que le
fichier circuit_41_1.C, $< est associé à circuit_41_1.C.
La règle de construction est donc simplement dans ce cas (grâce
aux macros):
CC -I ../include_41_1 -c circuit_41_1.C

1. Pour plus de détails, consulter les nombreux ouvrages de référence sur make ou faire la com-
mande man make.

Programmation en C++ IFT-19965 327


C
++

501 Une fois les règles implicites définies, le makefile contient les
lignes d’instructions nécessaires à la construction du
programme principal et des autres cibles essentielles à son bon
fonctionnement. Les dépendances, prérequis et commandes de
constructions sont les suivantes:
#*******************************************************
# Regle de construction du programme principal *
#*******************************************************

$(PROGRAMME) : $(OBJECTS) $(INCLUDES)


$(CC) $(PROGRAMME).C -o $(PROGRAMME) $(CPPFLAGS)\
$(CFLAGS) $(OBJECTS)

#*******************************************************
# Regle de construction des objets *
#*******************************************************

$(OBJECTS) : $(INCLUDES)

La ligne de dépendance
$(PROGRAMME) : $(OBJECTS) $(INCLUDES)

indique, via les macros, que les dépendances sont les suivantes
pour le programme principal:
sur_def_41_1 : circuit_41_1.o analogique_41_1.o\
resis_2_41_1.o\
../include_41_1/circuit_41_1.h\
../include_41_1/analogique_41_1.h\
../include_41_1/resis_2_41_1.h

Il faut remarquer que lorsqu’une liste est trop longue et qu’elle


doit s’étendre sur plusieurs lignes dans le makefile, il suffit de
terminer la ligne par le caractère \ et de continuer la liste sur
la ligne suivante. Si un fichier d’extension .o ou d’extension .h
est plus récent que sur_def_41_1, alors, la ligne de
commande de construction est exécutée:
$(CC) $(PROGRAMME).C -o $(PROGRAMME) $(CPPFLAGS)\
$(CFLAGS) $(OBJECTS)

En remplaçant les macros par leur valeur, on conclut que


l’opération de construction est la suivante:
CC sur_def_41_1.C -o sur_def_41_1 -I ../include_41_1\

328 Programmation en C++ IFT-19965


C
++

circuit_41_1.o analogique_41_1.o resis_2_41_1.o

Ici, le programme exécutable est stocké dans le répertoire


source_41_1 et non dans le répertoire bin_41_1 comme il le
devrait. Nous verrons bientôt comment pallier à ce problème.
La deuxième dépendance du makefile est:
$(OBJECTS) : $(INCLUDES)

et exprime la nécessité de reconstruire les fichiers objets


lorsque les fichiers d’inclusion ont changé. Dans ce cas précis,
on ne donne pas de commande de construction, ce qui signifie
que c’est la règle implicite énoncée plus haut qui sera effective
soit:
$(CC) $(CFLAGS) $(CPPFLAGS) -c $<

502 La dernière ligne de dépendance du makefile est un peu


spéciale: la cible clean ne dépend de rien et make ne tentera
donc jamais d’effectuer la commande de construction. Par
contre, on peut forcer make à effectuer la construction en
lançant la commande make clean comme nous le verrons plus
loin. Dans ce cas, seule la commande de construction associée à
clean est exécutée, ce qui efface tous les fichiers d’extension
.o du repère source_41_1. Ces fichiers sont maintenant
inutiles puisque le programme sur_def_41_1 a été construit.
La commande copie aussi le fichier sur_def_41 dans le
répertoire bin_41_1 comme il se doit. Cette ligne permet aussi
de voir qu’on peut demander l’exécution de plusieurs
instructions dans une commande de construction en les
séparant par un ;.
Voyons maintenant comment le makefile fonctionne en
donnant plusieurs exemples de son fonctionnement.

503 Supposons d’abord que nous avons complété l’écriture de tous


les fichiers .h et .C et qu’on a conçu le makefile discuté ci-
dessus. On est prêt à exécuter le makefile pour construire les
différentes cibles qu’il contient. En tapant la commande:
make

dans le répertoire source_41_1, make cherche un fichier


makefile contenu dans le même répertoire et tente par défaut

Programmation en C++ IFT-19965 329


C
++

de construire la première cible qui, en l’occurrence, est le


programme sur_def_41_1. Le diagnostic donné par make lors
de son exécution est:
CC -I../include_41_1 -c circuit_41_1.C
CC -I../include_41_1 -c analogique_41_1.C
CC -I../include_41_1 -c resis_2_41_1.C
CC sur_def_41_1.C -o sur_def_41_1 -I../include_41_1\
circuit_41_1.o analogique_41_1.o resis_2_41_1.o

Comme le programme dépend des fichiers .o et .h et que les


fichiers .o sont inexistants, make construit d’abord ces fichiers
comme le montrent les trois premières lignes du diagnostic.
Une fois les fichiers .o construits, le programme
sur_def_41_1 peut être construit à l’aide de ceux-ci comme le
montrent les deux dernières lignes du diagnostic. Le
programme sur_def_41_1 est construit et placé par défaut
dans le même répertoire que le fichier makefile soit ici dans le
répertoire source_41_1.
Si tous les fichiers sont à jour et qu’on lance à nouveau la
commande make, on obtient le diagnostic suivant:
`sur_def_41_1' is up to date.

make voit que tous les fichiers .h et .o sont à jour par rapport
à sur_def_41_1 et qu’il n’y a donc pas lieu de reconstruire
ceux-ci. Il se contente donc d’informer l’utilisateur que
sur_def_41_1, la première cible du makefile, est à jour et
peut donc être utilisée directement.

504 Supposons maintenant que le fichier analogique_41_1.h soit


modifié pour une raison ou une autre. En cours de
développement d’un projet, il arrive fréquemment que les
fichiers doivent être changés et make facilite le travail car il
tient compte de tous ces changements de manière
automatique1. Le diagnostic fourni par make est le suivant:
CC -I../include_41_1 -c circuit_41_1.C
CC -I../include_41_1 -c analogique_41_1.C
CC -I../include_41_1 -c resis_2_41_1.C

1. On peut simuler la modification du fichier en faisant la commande touch


analogique_41_1.h

330 Programmation en C++ IFT-19965


C
++

CC sur_def_41_1.C -o sur_def_41_1 -I../include_41_1\


circuit_41_1.o analogique_41_1.o resis_2_41_1.o

En réalité, seuls les fichiers analogique_41_1.o,


resis_2_41_1.o et sur_def_41_1 doivent être reconstruits
parce que circuit_41_1.o ne dépend pas de
analogique_41_1.h. Cependant, nous avons préféré garder
le makefile simple en ne tenant pas compte de ces détails et
en reconstruisant tous les .o lorsqu’un .h change. Dans un
petit projet, cet inconvénient est mineur.

505 Par contre, si on modifie le fichier analogique_41_1.o et


qu’on relance make, on obtient le diagnostic suivant:
CC sur_def_41_1.C -o sur_def_41_1 -I../include_41_1\
circuit_41_1.o analogique_41_1.o resis_2_41_1.o

Ici, comme seul sur_def_41_1 dépend de


analogique_41_1.o, seul cette cible doit être reconstruite, ce
que reconnaît make.

506 Une fois le projet complété, on peut effacer les fichiers


d’extension .o et transporter le programme exécutable dans le
répertoire bin_41_1 en faisant:
make clean

make transmet alors le diagnostic suivant:


rm *.o; mv sur_def_41_1 ../bin_41_1

ce qui efface les fichiers .o du répertoire courant


(source_41_1) et déplace le fichier sur_def_41_1 dans le
répertoire bin_41_1.

507 Lorsqu’on veut obtenir plus d’informatioin sur le diagnostic de


make, on peut utiliser l’option -d:
make -d

Dans ce cas, on obtient le diagnostic suivant si l’un des fichiers


.h a été modifié:
MAKEFLAGS value:

Programmation en C++ IFT-19965 331


C
++

Building circuit_41_1.o because it is out of date


relative to ../include_41_1/circuit_41_1.h
Building circuit_41_1.o because it is out of date
relative to ../include_41_1/analogique_41_1.h
Building circuit_41_1.o because it is out of date
relative to ../include_41_1/resis_2_41_1.h
Building circuit_41_1.o using suffix rule for .C.o
because it is out of date relative to circuit_41_1.C
CC -I../include_41_1 -c circuit_41_1.C
Building sur_def_41_1 because it is out of date relative
to circuit_41_1.o
Building analogique_41_1.o because it is out of date
relative to ../include_41_1/circuit_41_1.h
Building analogique_41_1.o because it is out of date
relative to ../include_41_1/analogique_41_1.h
Building analogique_41_1.o because it is out of date
relative to ../include_41_1/resis_2_41_1.h
Building analogique_41_1.o using suffix rule for .C.o
because it is out of date relative to analogique_41_1.C
CC -I../include_41_1 -c analogique_41_1.C
Building sur_def_41_1 because it is out of date relative
to analogique_41_1.o
Building resis_2_41_1.o because it is out of date
relative to ../include_41_1/circuit_41_1.h
Building resis_2_41_1.o because it is out of date
relative to ../include_41_1/analogique_41_1.h
Building resis_2_41_1.o because it is out of date
relative to ../include_41_1/resis_2_41_1.h
Building resis_2_41_1.o using suffix rule for .C.o
because it is out of date relative to resis_2_41_1.C
CC -I../include_41_1 -c resis_2_41_1.C
Building sur_def_41_1 because it is out of date relative
to resis_2_41_1.o
Building sur_def_41_1 because it is out of date relative
to ../include_41_1/circuit_41_1.h
Building sur_def_41_1 because it is out of date relative
to ../include_41_1/analogique_41_1.h
Building sur_def_41_1 because it is out of date relative
to ../include_41_1/resis_2_41_1.h
CC sur_def_41_1.C -o sur_def_41_1 -I../include_41_1\
circuit_41_1.o analogique_41_1.o resis_2_41_1.o

make explique toutes les étapes à travers lesquelles il passe


pour reconstruire les différentes cibles. L’option -d de make
sert surtout lorsqu’on veut debugger le makefile d’un projet.

508 Le makefile conçu dans ce chapitre est très flexible parce qu’il
utilise les macros. Par exemple, si on désire changer le nom du

332 Programmation en C++ IFT-19965


C
++

programme exécutable, il suffit simplement de changer la


définition de la macro et le reste du makefile est encore
valide. Il en va de même des autres macros. On peut donc
réutiliser le même makefile pour différents projets en ne
changeant que quelques lignes et en adoptant une hiérarchie
de répertoires comme celle définie dans ce chapitre.

509 L’utilitaire comprend de nombreux aspects que nous n’aurons


malheureusement pas le temps de voir. La meilleure façon d’en
apprécier les différentes possibilités et de l’utiliser à
profusion1.

1. Et de passer régulièrement à la confession parce que la maîtrise de make entraîne souvent


quelques débordements d’humeur portant à des abus de langage pieux.

Programmation en C++ IFT-19965 333


C
++

334 Programmation en C++ IFT-19965


C
++

CHAPITRE 44 La lecture et l’écriture de


chaînes de caractères
dans un fichier

510 Dans ce chapitre, nous allons revenir à l’exemple du


programme d’analyse de circuit que nous avions quelque peu
délaissé dans les chapitres précédents. Plus spécifiquement, le
présent chapitre s’intéresse à la façon dont on peut lire des
chaînes de caractères dans des fichiers et comment stocker des
chaînes de caractères dans un tableau.

511 Jusqu’à maintenant, le type de composante était représenté


par un entier compris entre 1 et 4 (1 = NAND, etc...). Il serait
intéressant de pouvoir identifier les composantes par leur nom
respectif plutôt que par un numéro choisi arbitrairement. On
pourrait par exemple choisir d’identifier chaque composante
par une chaîne de caractères avec la convention contenue dans
la table ci-dessous:

Composante Code descriptif


NAND 2 entrées NAND_2_1
NOR 2 entrées NOR__2_2
RESISTANCE RESIS__3
CONDENSATEUR COND___4
La convention est simplement que le code est sur 8 caractères,
les quatre premiers caractères identifient le nom de la

Programmation en C++ IFT-19965 335


C
++

composante et le dernier caractère identifie le numéro de


composante que nous avions utilisé précédemment. Les
caractères inutiles sont simplement remplacés par des “_”.

512 Pour lire une chaîne de caractères dans un fichier, il faut tout
d’abord créer un tableau dans lequel elle sera stockée
temporairement. Il faut s’assurer que le tableau est
suffisamment grand pour les chaînes à traiter en incluant le
caractère nul servant de marqueur de fin de chaîne. Pour notre
programme d’analyse de circuit, une chaîne de caractères de
100 composantes est nettement suffisante:
char input_chaine[100];

513 Il faut savoir que lorsqu’on définit une chaîne de caractères, le


nom de cette chaîne devient une constante dont la valeur
est l’adresse du premier élément du tableau1.

514 L’opérateur d’extraction >> reconnaît les opérandes qui sont


des pointeurs à des chaînes de caractères. En voyant un tel
pointeur, >> lit la chaîne provenant du flot de données d’entrée
jusqu’au premier caractère blanc2 et place son contenu dans le
tableau identifié par le pointeur. L’opération de lecture est donc
simplement accomplie de la façon suivante:
cin >> input_chaine;

Résumé
515 ✔ Le nom d’une chaîne de caractères est en fait le nom d’un
pointeur qui pointe au début de la chaîne.
✔ On ne peut réassigner quelque chose au pointeur d’une
chaîne parce que le C++ considère cet objet comme une
constante.
✔ Pour lire une chaîne de caractères dans un fichier, il faut
d’abord créer un tableau assez grand pour stocker la plus
longue chaîne qu’il contient.

1. le nom de la chaîne est donc un pointeur constant au début de la chaîne


2. espace, tab ou retour de chariot

336 Programmation en C++ IFT-19965


C
++

✔ Pour lire la chaîne de caractères il suffit d’adopter la syntaxe


suivante: cin >> nom_chaine;

Programmation en C++ IFT-19965 337


C
++

338 Programmation en C++ IFT-19965


C
++

CHAPITRE 45 Les tests sur des chaînes


de caractères

516 Ce chapitre s’intéresse à la façon dont on peut extraire des


caractères d’une chaîne de caractères et comment on peut
utiliser ces caractères pour décider des actions à prendre.

517 Dans le programme d’analyse du CHAPITRE 37, paragraphe


422, la boucle suivante lit le type de composante du flot de
données et l’assigne à une variable entière appelée type:
for (compteur = 0; flot_in >> type; ++compteur) {...}

De façon analogue, la boucle suivante lit des chaînes de


caractères d’un flot de données et les stocke successivement
dans un tableau appelé input_chaine:
for (compteur = 0; flot_in >> input_chaine ; ++compteur)
{...}

518 Tel que mentionné au CHAPITRE 44, l’opérateur d’extraction


>> lit une chaîne de caractères jusqu’à ce qu’il rencontre un
blanc. Il faut donc s’assurer que les différentes chaînes de
caractères dans un fichier soient séparées par un tel symbole. Il
ne faut pas non plus oublier de placer un caractère blanc à la

Programmation en C++ IFT-19965 339


C
++

fin de la dernière chaîne de caractères d’un fichier afin que


>> puisse la lire correctement.

519 Selon la convention établie au CHAPITRE 44, le numéro du


type de la composante est contenu dans le huitième caractère
du code stocké dans une chaîne de caractères. Comme la chaîne
est stockée dans un tableau, il suffit simplement d’accéder à
son huitième élément pour en extraire le numéro. Le
programme d’analyse de circuit pourrait donc prendre la forme
suivante:
//&% it_fil_45_1.C
#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include "circuit_45_1.h"
#include "nand_2_45_1.h"
#include "nor_2_45_1.h"
#include "analogique_45_1.h"
#include "resis_2_45_1.h"
#include "condens_2_45_1.h"

enum {nand_2_code = '1', nor_2_code = '2',


resis_code = '3', condens_code = '4'};

char extract_code(char * inpt_ch) {


return inpt_ch[7];
}

void main() {

int i, compteur;
int in_1,in_2;
double d_1, d_2;
char input_chaine[100];
CIRCUIT *cir_ptr[100];
ifstream flot_in ("it_dat_45_1.in", ios::in);
if (flot_in == 0) {
exit(0);
}
for (compteur = 0; flot_in >> input_chaine; ++compteur) {
switch (extract_code(input_chaine)) {
case nand_2_code: flot_in >> in_1 >> in_2;
cir_ptr[compteur] = new NAND_2(in_1,in_2);
break;
case nor_2_code: flot_in >> in_1 >> in_2;
cir_ptr[compteur] = new NOR_2(in_1,in_2);

340 Programmation en C++ IFT-19965


C
++

break;
case resis_code: flot_in >> d_1 >> d_2;
cir_ptr[compteur] = new RESISTANCE(d_1,d_2);
break;
case condens_code : flot_in >> d_1 >> d_2;
cir_ptr[compteur] = new CONDENSATEUR(d_1,d_2);
break;
default: cerr << "Piece "
<< extract_code(input_chaine)
<< " n'est pas une connue"
<< endl;
exit(0);
}

}
flot_in.close();

cout << "[" << cir_ptr[0]->affiche_type() << "]";


//Premier element
//sans "-"
for (i = 1; i < compteur ; ++i) {
cout << "-[" << cir_ptr[i]->affiche_type() << "]";
}
cout << endl;
}

On remarque plusieurs choses dans le programme ci-dessus:


1. les classes n’ont pas été changées, seule le programme d’ana-
lyse a subi des modifications. La première étant que l’énumé-
ration contient maintenant des initialisations en caractères
plutôt qu’en valeurs entières:

enum {nand_2_code = '1', nor_2_code = '2',


resis_code = '3', condens_code = '4'};

2. on a également ajouté une fonction appelée extract_code


qui va chercher le huitième caractère d’une chaîne de carac-
tères qui lui est fournie en argument et qui retourne ce
caractère au programme.
3. le fichier des données contient maintenant les informations
sur le circuit sous le format suivant:
RESIS__3 25.0 10.0
NAND_2_1 1 1
COND___4 25.0 10.0
NOR__2_2 1 1
RESIS__3 24.0 10.0

Programmation en C++ IFT-19965 341


++ C
La sortie du programme est:

Résultat
[RESISTANCE]-[NAND_2]-[CONDENSATEUR]-[NOR_2]-[RESISTANCE]

Ce qui est correct. Nous avons décidé d’implanter la lecture du


huitième caractère du code des composantes par une fonction
(appelée extract_code) parce que cela facilite la lecture du
code et permet aussi d’utiliser la fonction dans l’énoncé
switch.

Résumé
520 ✔ Pour lire une chaîne de caractère dans un fichier, il suffit de
déclarer un vecteur de caractères suffisamment long pour
stocker la plus grande chaîne attendue. Ensuite, l’opérateur
d’extraction >>, qui reçoit le nom de la chaîne en argument, lit
les caractères jusqu’au premier caractère blanc (espace, retour
de chariot, nouvelle ligne ou tabulation).
✔ Le nom d’une chaîne de caractères est un pointeur à son
premier élément. La chaîne se termine par le caractère nul \0.
✔ Pour accéder à un caractère d’une chaîne de caractère, il
suffit simplement d’adresser l’élément du vecteur servant à
stocker cette chaîne. Il faut se rappeler que les vecteurs sont en
origine zéro, c’est-à-dire que le premier élément est situé à
l’indice 0.

342 Programmation en C++ IFT-19965


C
++

CHAPITRE 46 Le stockage des chaînes


de caractères dans les
objets

521 Cette section décrit comment on peut transférer le contenu


d’une chaîne de caractères, stockée dans une variable tampon
temporaire, dans une variable membre d’une classe. Nous
verrons entre autres comment utiliser quelques fonctions de
manipulation de chaînes de caractères disponibles dans la
bibliothèque de fonctions du C++ dont les déclarations sont
contenues dans string.h.

522 Dans notre programme d’analyse de circuit, il serait


intéressant de pouvoir stocker le code de chaque composante du
fichier d’entrée directement dans l’objet qui lui est associé. Par
exemple, le code RESIS__3 serait stocké dans l’objet créé pour
une résistance. Pour accomplir cette tâche, il faut d’abord
modifier la définition des classes afin qu’elles puissent contenir
une variable membre sous la forme d’une chaîne de caractères
pouvant stocker le code. Comme toutes les composantes ont un
code, il semble logique d’ajouter cette variable membre dans la
classe CIRCUIT puisqu’elle est au sommet de la hiérarchie. La
définition de la classe CIRCUIT devient donc:
//&% circuit_45_1.h

#include <iostream.h>
#include <string.h>

Programmation en C++ IFT-19965 343


++ C
#ifndef CIRCUIT_FLAG_
#define CIRCUIT_FLAG_

class CIRCUIT {
private:
double v_num_plus; /*Tension numerique positive
(en volts)*/
double v_num_moins; /*Tension numerique negative
(en volts)*/
double v_analog_plus; /*Tension analogique positive
(en volts)*/
int numero; //Numero du composant

public:

CIRCUIT ();
CIRCUIT (char *);
CIRCUIT (double, double, double, int);
double read_v_num_plus();
double read_v_num_moins ();
double read_v_analog_plus ();
double read_numero ();
char * read_code();
void set_v_num_plus(double);
void set_v_num_moins(double);
void set_v_analog_plus(double);
void set_numero(int);
virtual char *affiche_type();
virtual double calcule_courant();

char* code; //Code du composant

};

#endif CIRCUIT_FLAG_

On rajoute simplement:
1. l’instruction de pré-processeur permettant d’inclure les infor-
mations pertinentes à la manipulation de chaînes de caractè-
res:
#include <string.h>

2. la variable membre code est placée dans la partie publique


de la classe pour simplifier les notions qui seront vues dans
les chapitres suivants,

344 Programmation en C++ IFT-19965


C
++

3. le constructeur avec argument CIRCUIT (char *)permettant


d’initialiser la variable membre code lors de la création d’un
objet
4. une fonction membre de lecture appelée read_code() pour
lire le code de chaque composante.

523 La source est la suivante pour le constructeur:


CIRCUIT::CIRCUIT(char* inpt_str) {
code = new char[strlen(inpt_str) + 1];
strcpy(code, inpt_str);
}

On remarque deux choses:


1. l’espace nécessaire pour stocker la chaîne de caractères est
alloué dynamiquement en réservant le nombre exact de
caractères. Ce nombre est obtenu en utilisant la fonction du
C++ appelée strlen (pour string length). Cette fonction
retourne le nombre de caractères contenu dans une chaîne
en excluant le caractère fin de chaîne. C’est la raison pour
laquelle on additionne 1 au résultat fourni par strlen dans
l’instruction new. Pour allouer dynamiquement l’espace
nécessaire à une chaîne de 10 caractères (incluant le carac-
tère fin de chaîne) il suffit simplement de faire: new
char[10].
2. Comme l’argument fourni au constructeur est une variable
utilitaire, il faut ensuite copier le contenu de cette variable
utilitaire dans la variable membre code grâce à la fonction
strcpy (pour string copy) du C++. Cette fonction copie le
contenu de la chaîne fournie en second argument dans la
chaîne fournie en premier argument en incluant le caractère
nul \0.

524 Le code de la fonction membre de lecture est simplement:


char * CIRCUIT::read_code () {
return code;
}

La fonction retourne un pointeur de caractère qui peut être


utilisé par l’opérateur d’insertion << .

Programmation en C++ IFT-19965 345


C
++

525 Le programme d’analyse de circuit permettant de stocker le


code de chaque composante pourrait avoir la structure
suivante:
//&% it_fil_45_1.C

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include "circuit_45_1.h"
#include "nand_2_45_1.h"
#include "nor_2_45_1.h"
#include "analogique_45_1.h"
#include "resis_2_45_1.h"
#include "condens_2_45_1.h"

enum {nand_2_code = '1', nor_2_code = '2',


resis_code = '3', condens_code = '4'};

char extract_code(char * inpt_ch) {


return inpt_ch[7];
}

void main() {

int i, compteur;
int in_1,in_2;
double d_1, d_2;
char input_chaine[100];
CIRCUIT *cir_ptr[100];
ifstream flot_in ("it_dat_45_1.in", ios::in);
if (flot_in == 0){
exit(0);
}
for (compteur = 0; flot_in >> input_chaine; ++compteur) {
switch (extract_code(input_chaine)) {
case nand_2_code: flot_in >> in_1 >> in_2;
cir_ptr[compteur] =
new NAND_2(in_1,in_2,input_chaine);
break;
case nor_2_code: flot_in >> in_1 >> in_2;
cir_ptr[compteur] =
new NOR_2(in_1,in_2,input_chaine);
break;
case resis_code: flot_in >> d_1 >> d_2;
cir_ptr[compteur] =
new RESISTANCE(d_1,d_2,input_chaine);
break;
case condens_code : flot_in >> d_1 >> d_2;

346 Programmation en C++ IFT-19965


C
++

cir_ptr[compteur] =
new CONDENSATEUR(d_1,d_2,input_chaine);
break;
default: cerr << "Piece "
<< extract_code(input_chaine)
<< " n'est pas une connue"
<< endl;
exit(0);
}

}
flot_in.close();

cout << "[" << cir_ptr[0]->affiche_type() << "]";


//Premier element
//sans "-"
for (i = 1; i < compteur ; ++i) {
cout << "-[" << cir_ptr[i]->affiche_type() << "]";
}
cout << endl;

for (i = 0; i < compteur ; ++i) {


cout << cir_ptr[i]->read_code() << endl;
}
cout << endl;
}

En fait, nous avons seulement apporté que des changements


mineurs au programme. Premièrement, nous avons modifié la
ligne d’allocation dynamique des objets pour utiliser un
constructeur acceptant une chaîne de caractères en argument.
Par exemple, l’allocation dynamique d’un objet de type NAND_2
se fait maintenant comme suit:
new NAND_2(in_1,in_2,input_chaine);

Cela signifie qu’il faut ajouter un constructeur à la classe


NAND_2 qui accepte une chaîne de caractères et qui appelle
simplement le constructeur de la classe CIRCUIT:
NAND_2::NAND_2(int i1, int i2,
char* inpt_str) : CIRCUIT(inpt_str){
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

Nous avons ici un excellent exemple de constructeur qui


appelle un autre constructeur. Cependant, si on essaie de

Programmation en C++ IFT-19965 347


++ C
compiler le fichier source de la classe NAND_2, la syntaxe ci-
dessus produit une erreur parce que la classe NAND_2 n’hérite
pas directement de la classe CIRCUIT mais le fait plutôt en
héritant de la classe NUMERIQUE. Il faut plutôt procéder comme
suit:
1. inclure un constructeur recevant un argument de type
chaîne de caractères dans la classe NUMERIQUE en déclarant
le constructeur suivant dans le fichier d’inclusion de la classe
NUMERIQUE:
NUMERIQUE (char *);

et en incluant le code suivant dans le fichier source de la


classe:
NUMERIQUE::NUMERIQUE (char *inpt_str) :
CIRCUIT(inpt_str) {}

On remarque que le constructeur de la classe NUMERIQUE ne


contient aucun énoncé mais qu’il appelle directement le
constructeur de la classe CIRCUIT permettant d’initialiser la
variable membre code.
2. inclure un constructeur recevant un argument de type
chaîne de caractères dans la classe NAND_2 en déclarant le
constructeur suivant dans le fichier d’inclusion de la classe
NAND_2:
NAND_2(int, int, char*);

et en incluant le code suivant dans le fichier source de la


classe NAND_2 :
NAND_2::NAND_2(int i1, int i2, char* inpt_str) :
NUMERIQUE(inpt_str){
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

Ainsi, le constructeur de NAND_2 appelle le constructeur de


NUMERIQUE qui appelle lui-même le constructeur de CIRCUIT
qui initialise le code de la composante.
Nous avons aussi ajouté au programme d’analyse de circuit une
boucle d’affichage de la variable membre code de chaque objet
pour nous assurer que les changements aux classes sont
corrects.

348 Programmation en C++ IFT-19965


C
++

Exercice
526 Modifiez les classes NOR_2, ANALOGIQUE, RESISTANCE et
CONDENSATEUR pour tenir compte des notions énoncées pour la
classe NAND_2.

527 Lorsqu’on exécute le programme d’analyse, sur le fichier de


données du paragraphe 519, on obtient le résultat correct
suivant:

Résultat
[RESISTANCE]-[NAND_2]-[CONDENSATEUR]-[NOR_2]-[RESISTANCE]
RESIS__3
NAND_2_1
COND___4
NOR__2_2
RESIS__3

Résumé
528 ✔ Lorsqu’on désire créer un tableau au run-time pour stocker
une chaîne de caractères, il suffit d’adopter la syntaxe suivante:
variable de type pointeur de caractère = new
char[nb caractères +1]
✔ Si on désire utiliser les fonctions de traitement de chaînes de
caractères du C++, il suffit d’ajouter la ligne #include
<string.h> au programme.
✔ Pour connaître la longueur d’une chaîne de caractères en
excluant le caractère fin de chaîne, il suffit d’adopter la syntaxe
suivante: strlen(nom_de_la_chaîne).
✔ Pour copier une chaîne dans une autre, il suffit d’adopter la
syntaxe suivante: strcpy(nom_chaîne_destination,
nom_chaîne_source).

Programmation en C++ IFT-19965 349


C
++

350 Programmation en C++ IFT-19965


C
++

CHAPITRE 47 La récupération de la
mémoire (désallocation)
grâce à l’opérateur
delete et aux fonctions
membres appelées
destructeurs

529 Dans le programme d’analyse de circuit traité jusqu’ici, le nom


du fichier de données est contenu directement dans le
programme. Il est impossible de choisir entre plusieurs fichiers
de données différents sinon en les renommant pour qu’ils
correspondent à l’identificateur dans le programme (par
exemple le fichier de données pour le programme du
paragraphe 525 est it_dat_45_1.in). Nous allons voir dans
cette section comment modifier le programme pour que
l’utilisateur puisse entrer le nom du fichier qu’il désire
analyser directement au clavier de l’ordinateur. Cependant, si
on regarde attentivement la structure de notre programme,
cette nouvelle modification fera apparaître des problèmes
importants. En effet, lorsqu’on analyse un fichier de données, le
programme alloue de la mémoire pour chaque nouvel objet
décrit dans le fichier et stocke les pointeurs à ces objets dans un
tableau de pointeurs. Si on analyse plusieurs fichiers avec le
même programme sans en sortir à chaque fois, les objets
anciennement créés lors d’une analyse précédente deviennent
non seulement inutiles mais le programme ne peut même plus
accéder au contenu de ceux-ci. Nous verrons comment
récupérer l’espace alloué pour stocker ces objets grâce à
l’opérateur delete du C++.

Programmation en C++ IFT-19965 351


C
++

530 Supposons qu’on veuille transformer le programme d’analyse


de circuit pour lui apporter les modifications suivantes:
1. On désire que le programme demande à l’usager le nom du
fichier à analyser. L’usager entre le nom du fichier au clavier.
2. On désire que le travail effectué jusqu’à maintenant par le
programme main soit dorénavant effectué par une fonction
appelée analyse_circuit en vue de procéder à des analy-
ses répétitives.
3. On désire que le programme main appelle la fonction
analyse_circuit tant que l’utilisateur entre un nouveau
nom de fichier de données.

531 Supposons que nous désirions concevoir un programme


analyse_circuit qui réponde aux exigences du paragraphe
530. Que faut-il prendre en compte:
1. il faut d’abord que le programme main accepte le nom d’un
fichier de données, ouvre le fichier et vérifie que le fichier est
correctement ouvert. Cela peut être fait très facilement avec
les énoncés suivants:
cin >> input_chaine;
ifstream flot_in (input_chaine,ios::in);
if (!flot_in) {
cerr << “Fichier inexistant.” << endl;
return 0;
}

2. Il faut aussi ajouter le code nécessaire à la fermeture du


fichier une fois que son traitement est terminé. La fermeture
explicite d’un fichier est une méthode de programmation
judicieuse qu’il faut mettre en pratique systématiquement:
flot_in.close();

3. Pour répéter un séquence d’instructions ad infinitum1, il suf-


fit simplement d’implanter une boucle sans fin:
while(1) {...}

La structure de notre programme serait donc la suivante:

1. le seul moyen de sortir de cette boucle étant d’entrer la combinaison ctrl-c.

352 Programmation en C++ IFT-19965


C
++

main () {
while(1)
cout << “Entrer un nom de fichier au clavier.”
<< endl <<“:”;
cin >> input_chaine;
ifstream flot_in (input_chaine,ios::in);
if (!flot_in) {
cerr << “Fichier inexistant.” << endl;
return;
}

analyse_circuit(flot_in);
flot_in.close();
}
}

532 Si on regarde de plus près la structure de la fonction


analyse_circuit, celle-ci doit satisfaire les contraintes
suivantes:
1. le type de la valeur de retour est void
2. le paramètre reçu du programme principal main est le nom
du flot de données d’entrées
La structure suivante est donc adéquate:
void analyse_circuit(ifstream &flot_entree) {...}

533 Le programme d’analyse prend maintenant la forme suivante.


Il faut noter que les définitions des classes de la hiérarchie sont
inchangées.
//&% it_fil_47_1.C

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include "circuit_47_1.h"
#include "nand_2_47_1.h"
#include "nor_2_47_1.h"
#include "analogique_47_1.h"
#include "resis_2_47_1.h"
#include "condens_2_47_1.h"

char input_chaine[100];
CIRCUIT *cir_ptr[100];

Programmation en C++ IFT-19965 353


C
++

enum {nand_2_code = '1', nor_2_code = '2',


resis_code = '3', condens_code = '4'};

/*------------------------------------------------------*/

char extract_code(char * inpt_ch) {


return inpt_ch[7];
}

/*------------------------------------------------------*/
void analyse_circuit(ifstream &flot_entree) {

int i,compteur;
int in_1,in_2;
double d_1, d_2;

for (compteur = 0; flot_entree >> input_chaine; ++compteur)


{
switch (extract_code(input_chaine)) {
case nand_2_code: flot_entree >> in_1 >> in_2;
cir_ptr[compteur] =
new NAND_2(in_1,in_2,input_chaine);
break;
case nor_2_code: flot_entree >> in_1 >> in_2;
cir_ptr[compteur] =
new NOR_2(in_1,in_2,input_chaine);
break;
case resis_code: flot_entree >> d_1 >> d_2;
cir_ptr[compteur] =
new RESISTANCE(d_1,d_2,input_chaine);
break;
case condens_code : flot_entree >> d_1 >> d_2;
cir_ptr[compteur] =
new CONDENSATEUR(d_1,d_2,input_chaine);
break;
default: cerr << "Piece "
<< extract_code(input_chaine)
<< " n'est pas une connue"
<< endl;
exit(0);
}

//Rapport sur le circuit

//Premier element sans "-"

cout << "[" << cir_ptr[0]->affiche_type() << "]";

354 Programmation en C++ IFT-19965


C
++

//Elements suivants

for (i = 1; i < compteur ; ++i) {


cout << "-[" << cir_ptr[i]->affiche_type() << "]";
}
cout << endl;
}
/*-----------------------------------------------------*/

int main() {

while (1) {

cout << "Entrer un nom de fichier au clavier."


<< endl <<":";
cin >> input_chaine;
ifstream flot_in (input_chaine,ios::in);
if (!flot_in) {
cerr << "Fichier inexistant." << endl;
return (0);
}
analyse_circuit(flot_in);
flot_in.close();
}

Si on exécute le programme avec trois fichiers de données, on


obtient le résultat suivant:

Résultat
Entrer un nom de fichier au clavier.
:it_dat_47_1.in
[RESISTANCE]-[NAND_2]-[CONDENSATEUR]-[NOR_2]-[RESISTANCE]
Entrer un nom de fichier au clavier.
:it_dat_47_2.in
[NAND_2]-[CONDENSATEUR]-[RESISTANCE]-[NAND_2]-
[CONDENSATEUR]-[NOR_2]-[NAND_2]-[CONDENSATEUR]-
[RESISTANCE]
Entrer un nom de fichier au clavier.
:it_dat_47_3.in
[NAND_2]-[CONDENSATEUR]-[RESISTANCE]-[NAND_2]-
[CONDENSATEUR]-[NOR_2]-[NAND_2]-[CONDENSATEUR]-
[RESISTANCE]-[NAND_2]-[CONDENSATEUR]-[RESISTANCE]-
[NAND_2]-[CONDENSATEUR]-[NOR_2]-[NAND_2]-[CONDENSATEUR]-
[RESISTANCE]
Entrer un nom de fichier au clavier.

Programmation en C++ IFT-19965 355


C
++

:in_dat_47_4.in
Fichier inexistant.

534 Le programme semble fonctionner correctement mais il cache


un problème sérieux. En effet, si on l’exécute un très grand
nombre de fois ou si on l’exécute en traitant des fichiers de
grandes dimensions, la mémoire de l’ordinateur se remplira
sans arrêt jusqu’à ce qu’il devienne impossible d’allouer de
l’espace pour des nouveaux objets. Pour comprendre ceci, il
suffit de comprendre que lorsqu’un fichier contenant par
exemple trois objets a été traité, la mémoire impliquée dans
l’analyse ressemble au schéma ci-dessous (FIGURE 6):
FIGURE 6 Mémoire après l’analyse d’un premier fichier de trois objets

Premier pointeur Deuxième pointeur Troisième pointeur

Premier objet Deuxième objet Troisième objet

535 Si on exécute l’analyse pour un deuxième fichier de trois objets,


on retrouve maintenant à la FIGURE 7 la configuration de
mémoire utilisée par le programme. Comme la mémoire
occupée par les objets du premier fichier n’a pas été récupérée,
elle est toujours occupée par ceux-ci. Cependant, les pointeurs
qui permettaient d’y accéder ont été récupérés pour l’analyse
du deuxième fichier. La conséquence est que les trois premiers
blocs de mémoire sont inaccessibles. Si on continue ainsi, toute
la mémoire libre sera occupée et inaccessible...

356 Programmation en C++ IFT-19965


C
++

Lorsqu’un bloc de mémoire devient inaccessible, on le qualifie


de détritus1. Lorsqu’un programme produit des détritus, on dit
FIGURE 7 Mémoire après l’analyse de deux fichiers de trois objets chacun.

Premier pointeur Deuxième pointeur Troisième pointeur

Quatrième objet Cinquième objet Sixième objet

Données innaccessibles
car aucun pointeur ne
permet d’en accéder le
contenu Premier objet Deuxième objet Troisième objet

alors que le programme génère des fuites de mémoire2.

536 Les conditions suivantes mènent aux fuites de mémoire dans


un programme:
1. Un pointeur ou un tableau de pointeurs sont créés
2. la mémoire est allouée dynamiquement pour stocker des
informations et les pointeurs pointent sur cette information
3. le programme fait en sorte que les pointeurs pointent ensuite
sur d’autres espaces alloués dynamiquement sans effectuer
une désallocation de la mémoire à laquelle ils pointaient ini-
tialement.

537 Dans la fonction analyse_circuit, l’énoncé:

1. “garbage” en anglais.
2. “memory leak” en anglais.

Programmation en C++ IFT-19965 357


C
++

cir_ptr[compteur] =
new NAND_2(in_1,in_2,input_chaine);

et les autres énoncés similaire du switch sont responsables de


la fuite de mémoire.
Pour éviter les fuites de mémoire, il suffit d’utiliser l’opérateur
delete avant de réassigner un pointeur à un nouvel espace de
mémoire, permettant ainsi de récupérer la mémoire à laquelle
il pointait. Dans l’exemple du programme d’analyse de circuit,
il suffit d’ajouter l’instruction
delete cir_ptr[compteur];

juste avant l’énoncé switch. Cette instruction réclame la


mémoire à laquelle le pointeur cir_prt[compteur] pointe et
la rend à nouveau accessible au programme pour une allocation
dynamique ultérieure. C’est le gestionnaire de mémoire du C++
qui décide du moment où la mémoire sera réutilisée.

538 Lors de l’analyse du premier fichier de données, les pointeurs


du vecteur de pointeurs cir_ptr sont tous nuls car ils n’ont
pas encore été traités avec une instruction new...Cela ne pose
pas de problème car le fait d’appliquer l’opérateur delete à un
pointeur nul n’a aucune conséquence (i.e. le C++ ne fait rien
dans ce cas). Le programme d’analyse de circuits utilisant
l’opérateur delete est maintenant, en utilisant l’opérateur
delete:
//&% it_fil_47_1.C

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include "circuit_47_1.h"
#include "nand_2_47_1.h"
#include "nor_2_47_1.h"
#include "analogique_47_1.h"
#include "resis_2_47_1.h"
#include "condens_2_47_1.h"

char input_chaine[100];
CIRCUIT *cir_ptr[100];

enum {nand_2_code = '1', nor_2_code = '2',


resis_code = '3', condens_code = '4'};

358 Programmation en C++ IFT-19965


C
++

/*------------------------------------------------------*/

char extract_code(char * inpt_ch) {


return inpt_ch[7];
}

/*-----------------------------------------------------*/
void analyse_circuit(ifstream &flot_entree) {

int i,compteur;
int in_1,in_2;
doubled_1, d_2;

for (compteur = 0; flot_entree >> input_chaine; ++compteur)


{
delete cir_ptr[compteur];
switch (extract_code(input_chaine)) {
case nand_2_code: flot_entree >> in_1 >> in_2;
cir_ptr[compteur] =
new NAND_2(in_1,in_2,input_chaine);
break;
case nor_2_code: flot_entree >> in_1 >> in_2;
cir_ptr[compteur] =
new NOR_2(in_1,in_2,input_chaine);
break;
case resis_code: flot_entree >> d_1 >> d_2;
cir_ptr[compteur] =
new RESISTANCE(d_1,d_2,input_chaine);
break;
case condens_code : flot_entree >> d_1 >> d_2;
cir_ptr[compteur] =
new CONDENSATEUR(d_1,d_2,input_chaine);
break;
default: cerr << "Piece "
<< extract_code(input_chaine)
<< " n'est pas une connue"
<< endl;
exit(0);
}

//Rapport sur le circuit

//Premier element sans "-"

cout << "[" << cir_ptr[0]->affiche_type() << "]";

Programmation en C++ IFT-19965 359


C
++

//Elements suivants

for (i = 1; i < compteur ; ++i) {


cout << "-[" << cir_ptr[i]->affiche_type() << "]";
}
cout << endl;
}
/*------------------------------------------------------*/

void main() {

while (1) {

cout << "Entrer un nom de fichier au clavier."


<< endl <<":";
cin >> input_chaine;
ifstream flot_in (input_chaine,ios::in);
if (!flot_in) {
cerr << "Fichier inexistant." << endl;
return;
}
analyse_circuit(flot_in);
flot_in.close();
}

539 On peut s’interroger sur la raison pour laquelle le C++ ne gère


pas lui-même la récupération de mémoire à chaque fois qu’un
pointeur se voit réassigné à un espace mémoire créé par new.
En fait, la raison est très simple: comme plusieurs pointeurs
peuvent permettre d’accéder le même espace de mémoire (grâce
aux variables pointeurs), le C++ ne peut décider lui-même si un
espace de mémoire peut être récupéré sans dommage. La
gestion de la mémoire est donc laissée au programmeur.

540 Le programme du paragraphe 538 utilise l’opérateur delete


pour récupérer l’espace mémoire occupé par des objets périmés.
Cependant, comme ces objets contiennent eux-mêmes des
pointeurs pointant à des espaces de mémoire alloués
dynamiquement (en l’occurrence la variable membre code qui
pointe à une chaîne de caractère allouée dynamiquement lors
de la création des objets) le programme comporte encore des
fuites de mémoire puisque l’espace occupé par les chaînes de

360 Programmation en C++ IFT-19965


C
++

caractères n’est pas récupéré...La FIGURE 8 illustre ce qui se


passe dans ce cas. Il faut remarquer ici qu’il est possible de
FIGURE 8 Fuites de mémoire causées par la non récupération de l’espace
mémoire alloué pour la chaîne de caractère pointée par la variable
membre code.

Après delete
Avant delete

Pointeur Pointeur

Objet

code

C C
h h
a a
î Fuite î
n n
e e

faire un delete d’un pointeur alors qu’il n’y a jamais de new


qui a été appliqué...cela n’est possible que parce que le tableau
cir_ptr est global et est par conséquent initialisé à 0
automatiquement lors de la compilation. Il n’en est pas de
même avec les variables locales. Si on tente de faire un delete
sur un pointeur non initialisé, une erreur de segmentation
(segmentation fault) est générée au run-time.

541 Le C++ offre le mécanisme des fonctions membres appelées


destructeurs pour récupérer l’espace mémoire alloué
dynamiquement et accédé par des variables membres de type
pointeur contenues dans des objets. Un destructeur est une
fonction membre qui est exécutée quand la mémoire allouée

Programmation en C++ IFT-19965 361


C
++

pour stocker un objet est récupérée1. Le destructeur, comme le


constructeur, porte le même nom que la classe dont il est une
fonction membre avec la différence que le nom du destructeur
commence avec le symbole ~. Ainsi, le nom du destructeur de la
classe NAND_2 est ~NAND_2().

542 Afin de nous assurer que tout l’espace alloué pour un objet est
réclamé avec l’utilisation de l’opérateur delete, il nous faut
inclure des fonctions membres destructeurs à chaque classe.
On inclut donc les déclarations suivantes aux différents fichiers
d’inclusion:

Fichier d’inclusion Déclaration à ajouter


circuit_47_1.h virtual ~CIRCUIT();
analogique_47_1.h virtual ~ANALOGIQUE();
numerique_47_1.h virtual ~NUMERIQUE();
nand_2_47_1.h virtual ~NAND_2();
nor_2_47_1.h virtual ~NOR_2();
resis_2_47_1.h virtual ~RESISTANCE();
condens_2_47_1.h virtual ~CONDENSATEUR();
Les destructeurs sont virtual parce que, dans une hiérarchie,
il peut être nécessaire de choisir au run-time le destructeur
approprié pour l’objet sous traitement. L’exemple ci-dessous
illustre clairement la nécessité de donner l’attribut virtual
aux destructeurs.
#include<iostream.h>

class A
{
public:
A()
{
cout << “Constructeur A” << endl;
}

// Important que celui-ci soit virtual


virtual ~A()
{

1. Soit par delete, soit en sortant d’une fonction, etc.

362 Programmation en C++ IFT-19965


C
++

cout << “Destructeur A” << endl;


}
};

class B : public A
{
public:
B()
{
cout << “Constructeur B” << endl;
}

~B()
{
cout << “Destructeur B” << endl;
}
};

main()
{

A *ptr;

ptr = new B;
delete ptr;

543 Les instructions des destructeurs sont placées dans les fichiers
source de chaque classe. Pour le cas qui nous intéresse, les
fonctions suivantes ont été implantées:

Fichier source Définition


circuit_47_1.C CIRCUIT::~CIRCUIT(){delete [] code; }
analogique_47_1C ANALOGIQUE::~ANALOGIQUE() {}

numerique_47_1.C NUMERIQUE::~NUMERIQUE(){}

nand_2_47_1.C NAND_2::~NAND_2(){}
nor_2_47_1.C NOR_2::~NOR_2(){}
resis_2_47_1.C RESISTANCE::~RESISTANCE(){}
condens_2_47_1.C CONDENSATEUR::~CONDENSATEUR(){}

Programmation en C++ IFT-19965 363


C
++

On remarque que le destructeur de la classe CIRCUIT récupère


l’espace alloué pour la chaîne de caractère code tandis que les
destructeurs des classes ANALOGIQUE, NUMERIQUE, NAND_2,
NOR_2, RESISTANCE et CONDENSATEUR ne font rien. Comme la
chaîne de caractères est en soi un tableau, le C++ exige qu’on
adopte la syntaxe suivante pour détruire chaque composante
du tableau:
delete [] nom_tableau;

les crochets suivant delete informent le C++ que l’espace


réservé pour toutes les composantes du tableau doit être
récupéré. Dans l’exemple de la hiérarchie CIRCUIT, on place le
delete dans le destructeur de la classe CIRCUIT car c’est par le
constructeur de cette classe que l’espace avait été alloué à
l’origine lors de la création d’objets appartenant à cette
hiérarchie. Il faut préciser que lorsqu’un objet d’une sous-classe
est détruit, son destructeur de même que les destructeurs des
super-classes dont il hérite sont appelés dans l’ordre inverse à
celui des constructeurs lors de la création de l’objet. L’exemple
ci-dessous illustre l’utilisation des variables static. Dans ce
programme, nous sommes intéressés à compter le nombre de
d’instructions new et de delete qui sont effectuées lors de
l’exécution et non de seulement compter le nombre d’appels au
constructeur et au destructeur.
#include<assert.h>
#include<iostream.h>

class A
{
int * tableau;
static int balance;

public:
A()
{
tableau = new int[25];
if (!tableau)
{
cerr << "Erreur..." << endl;
}

balance++;
cout << balance << " new sans delete" << endl;
}
~A()

364 Programmation en C++ IFT-19965


C
++

{
delete [] tableau;
balance--;
cout << balance << " new sans delete" << endl;
}
};

int A::balance=0;
// ou
// int A::balance; // Initialisee a zero par defaut.

main()
{

int i,cpt;
cout << "Creer combien d’objets??" << endl;
cin >> i;

A *vect[100];
//A **vect;
//vect = new A *[i];
// assert(vect);

for (cpt=0;cpt<i;cpt++)
{
vect[cpt] = new A;
assert(vect[cpt]);
}

for(cpt=0; cpt<i; cpt++)


{
delete vect[cpt];
}

//delete [] vect;

544 Un moyen simple de s’assurer qu’un programme ne comporte


pas de fuites de mémoire est de compter le nombre de fois que
l’opérateur new est utilisé et de contrebalancer ce nombre par
un nombre d’appels équivalent à l’opérateur delete. Dans le
programme d’analyse de circuits, new est appelé une fois lors
de la création de chaque objet et une fois lorsque le
constructeur de l’objet est appelé pour allouer l’espace
nécessaire pour stocker le code de chaque composante. On

Programmation en C++ IFT-19965 365


C
++

effectue un delete avant de réallouer l’espace associé à un


pointeur et un autre delete lors de l’appel du destructeur
associé à la destruction de chaque objet. On a donc deux appels
à new et deux appels à delete, ce qui garantit l’absence de
fuites de mémoire.

Notion avancée
545 Un moyen efficace de garder en mémoire le nombre d’objets qui
ont été crées pour une classe est d’inclure une variable membre
static dans chaque classe. Contrairement aux variables
membres non static qui sont dupliquées pour chaque objet
d’une classe, les variables membres static sont partagées par
tous les objets d’une même classe. En incrémentant la variable
à chaque fois que le constructeur est appelé et en la
décrémentant à chaque fois qu’un objet est détruit, la variable
contient toujours le nombre actuel d’objets de la classe.
L’initialisation d’une variable membre static ne peut
évidemment pas se faire via le constructeur. Il faut plutôt
l’initialiser en adoptant la syntaxe suivante:
type nom_classe::nom_variable_static = valeur_initiale;

Une variable static est déclarée en adoptant la syntaxe


suivante:
static type nom_variable;

Résumé
546 ✔ La mémoire allouée mais inaccessible est appelée mémoire
détritus (garbage).
✔ Quand un programme produit de la mémoire détritus, on dit
qu’il possède des fuites (leaks).
✔ Quand on désire colmater une fuite de mémoire, il suffit
d’adopter l’une des syntaxes suivantes:
delete nom_pointeur;
delete nom_tableau[indice];
delete [] nom_tableau ;
✔ Un destructeur est une fonction membre qui est appelée
lorsque la mémoire allouée à un objet est récupérée par le
programme.
✔ Il est nécessaire que les destructeurs récupèrent de la
mémoire additionnelle à celle réservée pour l’objet en
appliquant l’opérateur delete aux variables membres qui sont

366 Programmation en C++ IFT-19965


C
++

des pointeurs à des données qui ont été allouées


dynamiquement lors de la création d’un objet.

Programmation en C++ IFT-19965 367


C
++

368 Programmation en C++ IFT-19965


C
++

CHAPITRE 48 Le constructeur par


recopie (copy
constructor)

547 Au CHAPITRE 47, nous avons vu comment colmater les fuites


de mémoire grâce à l’opérateur delete. Dans le CHAPITRE
48, nous allons pousser plus loin notre investigation de la
gestion de la mémoire en abordant le concept de constructeur
par recopie (copy constructor).

548 Quand nous avons abordé le concept de fonction en C++, nous


avons vu que le passage de paramètres est généralement fait
par valeur, c’est-à-dire que la valeur de l’argument à la fonction
est copié dans une variable temporaire (le paramètre) pour la
durée de vie de la fonction et que cette variable temporaire est
ensuite détruite au retour de la fonction. Cette pratique mène à
des problèmes lorsque l’argument appartient à une sous-classe
de la classe du paramètre parce qu’alors, seule la partie
appartenant à la classe est copiée. Nous avons aussi vu qu’un
moyen d’éviter que l’argument ne soit recopié dans le
paramètre était d’utiliser le passage de paramètres par
référence. Dans ce cas, seule l’adresse de l’argument est passée
au paramètre et l’objet n’est pas recopié. Le problème évoqué
ci-dessus est alors éliminé.

Programmation en C++ IFT-19965 369


C
++

549 Un problème de gestion de mémoire très différent mais


également très subtil survient lorsque l’utilisateur conçoit ses
propres fonctions membres destructeurs et que des objets
sont passés par valeur à des fonctions. Supposons par
exemple que, dans notre exemple d’analyse de circuit, une
fonction abcd reçoive un argument de type NAND_2 et que cet
argument soit passé par valeur. La déclaration de la fonction
sera donc la suivante:
void abcd(NAND_2 arg);

Lorsqu’un argument de type NAND_2, appelé par exemple


porte_1, est passé à la fonction abcd lors d’un appel de celle-
ci par le programme principal, le C++ réserve de l’espace
mémoire pour le paramètre arg et copie l’argument porte_1
dans le paramètre arg. Lorsque la fonction se termine et que
l’exécution du programme se poursuit, l’espace initialement
réservé pour le paramètre arg est récupéré. Il faut noter que le
destructeur pour cet argument est appelé puisque la mémoire
qu’il occupait est récupérée par le programme.
Lorsque le C++ copie un argument dans un paramètre, il le fait
en appelant une fonction membre appelée constructeur par
recopie (copy constructor). Si l’usager n’a pas défini une telle
fonction membre, le C++ en adopte une par défaut qui copie
l’argument membre à membre dans le paramètre. Ce qu’il faut
retenir c’est que, lorsqu’un objet contient des pointeurs
comme variables membres, le constructeur par recopie défaut
copie la valeur du pointeur de l’argument dans le pointeur du
paramètre mais ne copie pas le contenu de la mémoire à
laquelle pointe le pointeur de l’argument.
On sait qu’un objet de type NAND_2 contient une variable
membre pointeur de caractères appelée code héritée de la
classe CIRCUIT. En passant un argument de type NAND_2 à la
fonction abcd, le pointeur code de l’argument est copié dans la
variable membre code du paramètre mais la chaîne de
caractères à laquelle pointe code est partagée par les deux
objets (l’argument et le paramètre).

370 Programmation en C++ IFT-19965


C
++

Le schéma ci-dessous montre la configuration de l’espace


mémoire lors de l’appel de la fonction abcd.
Argument Paramètre

Objet Objet

code code

C
h
a
î
n
e

550 Lors du retour de la fonction abcd, le paramètre est détruit et


comme le destructeur est appelé, celui-ci désalloue la mémoire
réservée pour la chaîne de caractères parce qu’il contient
l’instruction:
delete [] code;

Au retour de la fonction, la configuration de l’espace mémoire


ressemble au schéma ci-dessous:
Argument

Objet

code

On remarque évidemment le problème: la chaîne de caractères


de l’argument à été détruite par le destructeur du paramètre et

Programmation en C++ IFT-19965 371


++ C
n’est donc plus accessible alors qu’elle devrait l’être. Il faut
noter que le pointeur à la chaîne de caractères existe encore
mais il pointe maintenant à un espace mémoire qui a été
désalloué par le programme via le destructeur du paramètre.
Cela signifie que le programme peut se comporter correctement
pendant un certain temps (i.e. la chaîne de caractères peut
demeurer accessible) tant que la mémoire occupée par la chaîne
de caractères n’est pas réaffecté pour un autre usage...Ce type
d’erreur est donc particulièrement difficile à identifier parce
que le bug n’apparaît pas forcément près de l’endroit où la
fonction a été appelée. De plus, le bug n’apparaît pas toujours
au même endroit lors de l’exécution du programme, la gestion
générale de l’allocation dynamique étant hors du contrôle de
l’utilisateur mais demeurant sous la gouverne du C++.

551 Il y a deux moyens de résoudre le problème exposé dans les


paragraphes précédents:
1. l’utilisateur peut définir lui-même un constructeur par reco-
pie dont le code veille à ce que non seulement les pointeurs
soient recopiés mais également ce à quoi ils pointent. Ainsi,
lorsque le paramètre est détruit, seule la copie est complète-
ment détruite et l’argument reste intact. La syntaxe adoptée
par le C++ pour déclarer un constructeur par recopie pour la
classe NAND_2 est la suivante:
NAND_2(const NAND_2 &);

La syntaxe générale est:


nom_constructeur(const nom_classe &);

Il faut absolument que l’argument du constructeur par


recopie soit passé par référence sinon on tomberait dans un
problème de récursivité insoluble. Ici, on choisit une
référence à une constante pour nous assurer que
l’argument ne sera pas modifié par la fonction.
2. l’utilisateur peut éviter de passer des objets comme des argu-
ments par valeur en adoptant le passage des paramètres par
référence. De cette façon, aucune copie de l’argument n’étant
faite, aucune réclamation de mémoire n’est faite à la sortie
de la fonction et le destructeur du paramètre n’est pas
appelé. Il peut arriver que le programmeur fasse par erreur

372 Programmation en C++ IFT-19965


C
++

un passage de paramètre par valeur alors qu’il désire en réa-


lité passer le paramètre par référence. En effet, il est fré-
quent qu’un programmeur oublie d’inclure le caractère & lors
de la définition de la fonction. Une telle erreur en apparence
banale est souvent très difficile à repérer parce que son effet
peut être imperceptible sur une longue période d’utilisation
du programme. Un moyen simple de repérer ce type d’erreur
est de concevoir un constructeur par recopie qui ne fait rien
et qui est placé dans la partie private de la classe. Ainsi,
lorsque le compilateur arrive à la fonction et qu’il voit qu’une
fonction ayant le statut private est utilisée alors que cela
est interdit, un message d’erreur est immédiatement envoyé
à l’utilisateur. L’erreur est découverte au moment de la com-
pilation et peut être facilement corrigée.
De façon générale, la seconde approche est préférable parce que
les objets servent à représenter des entités du monde réel et ils
ne devraient pas être copiés et détruits simplement parce qu’ils
sont passés en arguments à des fonctions. Cependant, il peut y
avoir des situations où il faut absolument passer un objet par
valeur et il faut alors que l’utilisateur conçoive lui-même un
constructeur par recopie. Il y a aussi une autre situation où la
conception d’un constructeur par recopie est absolument
essentielle. Dans le segment de code suivant:
NAND_2 nnd_1(0,1);
NAND_2 nnd_2 = nnd_1;

on déclare une variable nnd_1 de type NAND_2. On déclare


ensuite une seconde variable appelée nnd_2 de type NAND_2 et
on l’initialise à la valeur de nnd_1. Dans ce cas, c’est le
constructeur par recopie qui est appelé pour copier le contenu
de nnd_1 dans nnd_2. Les problèmes associés au constructeur
par recopie par défaut discutés plus haut peuvent alors
survenir et doivent à tout prix être évités.

552 Les classes de la hiérarchie doivent être modifiées pour inclure


un constructeur par recopie. Les fichiers d’inclusion et les
fichiers source de chaque classe sont donnés à la fin du
chapitre. On remarque que:
1. On a déclaré et défini un constructeur par recopie pour cha-
que classe. Pour les sous-classes de la hiérarchie, il est néces-
saire d’initialiser chaque variable membre et chaque

Programmation en C++ IFT-19965 373


++ C
variable membre héritée. En effet, l’appel des constructeurs
par recopie des superclasses n’est pas automatique lors de
l’appel du constructeur par recopie d’une sous-classe. Il faut
aussi remarquer que la plupart des variables étant private,
il est nécessaire d’utiliser les fonctions membres d’écriture
pour les initialiser.
2. les fonctions membres de lecture ont été déclarées comme
constantes (const). Cela signifie que ces fonctions membres
peuvent être appelées pour des objets constants, ce qui est le
cas pour le paramètre du constructeur par recopie. Le fait de
travailler avec un objet constant comme paramètre nous
assure que la fonction ne pourra modifier le contenu de
l’objet passé en argument.
3. Les constructeurs par recopie copient chaque variable mem-
bre incluant les pointeurs en plus de recopier ce à quoi poin-
tent les pointeurs.

Notion avancée
553 La conception des constructeurs par recopie nous amène à
reconsidérer la surdéfinition de l’opérateur = pour des objets
contenant des pointeurs pointant à des espaces mémoire
alloués dynamiquement. Lorsque nous avons abordé la
surdéfinition de l’opérateur = au paragraphe 457 du
CHAPITRE 40, nous avons vu qu’il fallait concevoir la fonction
membre operator= de façon telle qu’elle affecte chaque
variable membre de l’objet à gauche de l’opérateur avec la
valeur correspondante de la variable membre de l’objet situé à
droite du signe =. Après ce que nous venons de voir sur le
constructeur par recopie, nous pouvons étendre la notion de
surdéfinition de l’opérateur = aux objets ayant des variables
membres qui sont des pointeurs. Dans ce cas, la fonction
operator= doit veiller à ce que non seulement la valeur du
pointeur soit affectée à l’objet de gauche, mais également les
données auxquelles pointe ce pointeur.

Résumé
554 ✔ En général, les objets passés en argument par valeur à des
fonctions peuvent conduire à des problèmes subtils de
désallocation de mémoire. Autant que possible, il faut éviter de
passer des objets par valeur à des fonctions. Un moyen de
s’assurer que les objets sont passés par référence est de définir

374 Programmation en C++ IFT-19965


C
++

une fonction membre appelée constructeur par recopie:


nom_classe(nom_classe&);
et de la placer dans la partie private de la classe. Ainsi, le
compilateur signalera une erreur lorsque le programmeur
tentera de passer un objet par valeur à une fonction.
✔ Lorsqu’il n’est pas possible de passer les objets uniquement
par référence pour éviter la recopie dans le paramètre, il faut
définir une fonction membre appelée constructeur par recopie
et veiller à ce que les variables membres de l’argument soient
copiées correctement dans le paramètre, incluant les pointeurs
et ce à quoi ils pointent.

555 Le programme de test pour les constructeurs par recopie est le


simple programme suivant:
//&% cp_constr_48_1.C

#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
#include "circuit_48_1.h"
#include "nand_2_48_1.h"
#include "nor_2_48_1.h"
#include "analogique_48_1.h"
#include "resis_2_48_1.h"
#include "condens_2_48_1.h"

void test_copy_constructor(NAND_2 nd){


cout << nd.code;
}

void main() {

char * str = "code_bidon";


NAND_2nand_1(0,1,str);

cout << "Valeur de code dans la fonction: ";


test_copy_constructor(nand_1);
cout << endl;

cout << "Valeur de code pour l'objet dans main: "


<< nand_1.code
<< endl;

Programmation en C++ IFT-19965 375


C
++

556 Fichier d’inclusion pour la classe CIRCUIT:


//&% circuit_48_1.h

#include <iostream.h>
#include <string.h>

#ifndef CIRCUIT_FLAG_
#define CIRCUIT_FLAG_

class CIRCUIT {
private:
double v_num_plus; /*Tension numerique positive
(en volts)*/
double v_num_moins; /*Tension numerique negative
(en volts)*/
double v_analog_plus; /*Tension analogique positive
(en volts)*/
int numero; //Numero du composant

public:

CIRCUIT ();
CIRCUIT (char *);
CIRCUIT (double, double, double, int);
CIRCUIT (const CIRCUIT &);
virtual ~CIRCUIT();
double read_v_num_plus() const;
double read_v_num_moins () const;
double read_v_analog_plus () const;
int read_numero () const;
char * read_code() const;
void set_v_num_plus(double);
void set_v_num_moins(double);
void set_v_analog_plus(double);
void set_numero(int);
virtual char *affiche_type();
virtual double calcule_courant();

char* code; //Code du composant

};

#endif CIRCUIT_FLAG_

557 Fichier source pour la classe CIRCUIT:

376 Programmation en C++ IFT-19965


C
++

//&% circuit_48_1.C
//Definitions des fonctions membres de la classe CIRCUIT

#include <iostream.h>
#include "circuit_48_1.h"

//Constructeur defaut

CIRCUIT::CIRCUIT () {
v_num_plus = 5.0;
v_num_moins = 0.0;
v_analog_plus = 12.0;
numero = 0;
}

// Constructeur avec initialisation du code

CIRCUIT::CIRCUIT(char* inpt_str) {
code = new char[strlen(inpt_str) + 1];
strcpy(code, inpt_str);
}

// Constructeur avec initialisation des variables membres

CIRCUIT::CIRCUIT (double v_n_p, double v_n_m,


double v_a_p, int no) {
v_num_plus = v_n_p;
v_num_moins = v_n_m;
v_analog_plus = v_a_p;
numero = no;
}

//Copy constructor
CIRCUIT::CIRCUIT(const CIRCUIT & cr){

v_num_plus = cr.read_v_num_plus();
v_num_moins = cr.read_v_num_moins ();
v_analog_plus = cr.read_v_analog_plus ();
numero = cr.read_numero ();
code = new char[strlen(cr.code) + 1];
strcpy(code, cr.code);
}

//Destructeur
CIRCUIT::~CIRCUIT(){}

// Readers

double CIRCUIT::read_v_num_plus() const{


return v_num_plus;

Programmation en C++ IFT-19965 377


C
++

}
double CIRCUIT::read_v_num_moins () const{
return v_num_moins;
}
double CIRCUIT::read_v_analog_plus () const{
return v_analog_plus;
}
int CIRCUIT::read_numero () const{
return numero;
}

char * CIRCUIT::read_code () const{


return code;
}

// Writers

void CIRCUIT::set_v_num_plus(double v) {
v_num_plus = v;
}
void CIRCUIT::set_v_num_moins(double v) {
v_num_moins = v;
}
void CIRCUIT::set_v_analog_plus(double v) {
v_analog_plus = v;
}
void CIRCUIT::set_numero(int n) {
numero = n;
}

// Affichage du type de composant


char * CIRCUIT::affiche_type() {return "Circuit";}

//Fonction de calcul du courant


double CIRCUIT::calcule_courant() {return 0.0;}

558 Fichier d’inclusion pour la classe ANALOGIQUE:


//&% analogique_48_1.h
#include <iostream.h>
#include "circuit_48_1.h"

#ifndef ANALOGIQUE_FLAG_
#define ANALOGIQUE_FLAG_

class ANALOGIQUE : public CIRCUIT {


private:
double puissance_max;

378 Programmation en C++ IFT-19965


C
++

int type;

public:
ANALOGIQUE ();
ANALOGIQUE (double, int);
ANALOGIQUE (char *);
ANALOGIQUE (const ANALOGIQUE & an);
virtual ~ANALOGIQUE();
double read_puissance_max() const;
int read_type() const;
void set_puissance_max(double);
void set_type(int);
virtual char *affiche_type();
virtual double calcule_courant();
};

#endif ANALOGIQUE_FLAG_

559 Fichier source pour la classe ANALOGIQUE


//&% analogique_48_1.C
//Definitions des fonctions membres de la classe ANALOGIQUE

#include <iostream.h>
#include "circuit_48_1.h"
#include "analogique_48_1.h"

// Constructeur par defaut


ANALOGIQUE::ANALOGIQUE () {}

// Constructeur avec arguments


ANALOGIQUE::ANALOGIQUE (double p_max, int typ) {
puissance_max = p_max;
type = typ;
}

ANALOGIQUE::ANALOGIQUE (char* inpt_ptr) :


CIRCUIT(inpt_ptr) {}

//Copy constructor
ANALOGIQUE::ANALOGIQUE(const ANALOGIQUE & an){

set_v_num_plus(an.read_v_num_plus());
set_v_num_moins(an.read_v_num_moins());
set_v_analog_plus(an.read_v_analog_plus());
set_numero(an.read_numero());
code = new char[strlen(an.code) + 1];
strcpy(code, an.code);

Programmation en C++ IFT-19965 379


C
++

set_puissance_max(an.read_puissance_max());
set_type(an.read_type());
}

//Destructeur
ANALOGIQUE::~ANALOGIQUE() {}

//Readers
double ANALOGIQUE::read_puissance_max() const{
return puissance_max;
}
int ANALOGIQUE::read_type() const{
return type;
}

//Writers
void ANALOGIQUE::set_puissance_max(double p_max) {
puissance_max = p_max;
}
void ANALOGIQUE::set_type(int typ) {
type = typ;
}

// Affichage du type de composant


char *ANALOGIQUE::affiche_type() {return "Analogique";}

//Calcul du courant
double ANALOGIQUE::calcule_courant() {return 0.0;}

560 Fichier d’inclusion pour la classe NUMERIQUE


//&% numerique_48_1.h
#include <iostream.h>
#include "circuit_48_1.h"

#ifndef NUMERIQUE_FLAG_
#define NUMERIQUE_FLAG_

class NUMERIQUE : public CIRCUIT {


private:
int fan_out; //Nombre admissible de sorties

public:

NUMERIQUE ();
NUMERIQUE (int);
NUMERIQUE (char *);
NUMERIQUE (const NUMERIQUE &);

380 Programmation en C++ IFT-19965


C
++

virtual ~NUMERIQUE();
int read_fan_out() const;
void set_fan_out(int);
virtual char *affiche_type();
};

#endif NUMERIQUE_FLAG_

561 Fichier source pour la classe NUMERIQUE


NUMERIQUE::NUMERIQUE () {
fan_out = 10;
}

// Constructeur avec arguments


NUMERIQUE::NUMERIQUE (int fn_out) {
fan_out = fn_out;
}

NUMERIQUE::NUMERIQUE (char *inpt_str) : CIRCUIT(inpt_str)


{}

//Copy constructor
NUMERIQUE::NUMERIQUE(const NUMERIQUE & nm) {

set_v_num_plus(nm.read_v_num_plus());
set_v_num_moins(nm.read_v_num_moins());
set_v_analog_plus(nm.read_v_analog_plus());
set_numero(nm.read_numero());
code = new char[strlen(nm.code) + 1];
strcpy(code, nm.code);
set_fan_out(nm.read_fan_out());
}

//Destructeur
NUMERIQUE::~NUMERIQUE(){}

// Readers
int NUMERIQUE::read_fan_out() const{
return fan_out;
}

// Writers
void NUMERIQUE::set_fan_out(int f_out) {
fan_out = f_out;
}

// Affichage du type de composant

Programmation en C++ IFT-19965 381


C
++

char * NUMERIQUE::affiche_type() {return "Numerique";}

562 Fichier d’inclusion pour la classe NAND_2


//&% nand_2_48_1.h

#include <iostream.h>
#include "circuit_48_1.h"
#include "numerique_48_1.h"

#ifndef NAND_FLAG_
#define NAND_FLAG_

class NAND_2 : public NUMERIQUE {

private:
int out_1; /*sortie */

int set_out_1();

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

NAND_2();
NAND_2(int, int);
NAND_2(int, int, char*);
NAND_2(const NAND_2 &);
virtual ~NAND_2();
int lire_in_1() const;
int lire_in_2() const;
int lire_out_1() const;
void ecrire_in_1(int);
void ecrire_in_2(int);
int operator ~ ();
virtual char *affiche_type();
};

#endif NAND_FLAG_

563 Fichier source pour la classe NAND_2


//&% nand_2_48_1.C
//Definition des fonctions membres de la classe NAND_2

#include <iostream.h>
#include "circuit_48_1.h"

382 Programmation en C++ IFT-19965


C
++

#include "numerique_48_1.h"
#include "nand_2_48_1.h"

int NAND_2::set_out_1() {
return 1 - (in_1 * in_2);
}

//Constructeur par defaut


NAND_2::NAND_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NAND_2::NAND_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

//Constructeur avec trois arguments


NAND_2::NAND_2(int i1, int i2, char* inpt_str) :
NUMERIQUE(inpt_str){
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

//Copy constructor
NAND_2::NAND_2(const NAND_2 & nnd) {

set_v_num_plus(nnd.read_v_num_plus());
set_v_num_moins(nnd.read_v_num_moins());
set_v_analog_plus(nnd.read_v_analog_plus());
set_numero(nnd.read_numero());
code = new char[strlen(nnd.code) + 1];
strcpy(code, nnd.code);
set_fan_out(nnd.read_fan_out());
ecrire_in_1(nnd.in_1);
ecrire_in_2(nnd.in_2);
}

//Destructeur
NAND_2::~NAND_2(){
delete [] code;
}

//Readers
int NAND_2::lire_in_1() const{
return in_1;
}

Programmation en C++ IFT-19965 383


C
++

int NAND_2::lire_in_2() const{


return in_2;
}

int NAND_2::lire_out_1() const{


return out_1;
}

//Writers
void NAND_2::ecrire_in_1(int val) {
in_1 = val;
out_1 = set_out_1();
}

void NAND_2::ecrire_in_2(int val) {


in_2 = val;
out_1 = set_out_1();
}

// Surdefinition des operateurs


int NAND_2::operator ~ () {
if (lire_out_1()) return 0;
else return 1;
}

// Affichage du type de composant


char * NAND_2::affiche_type() {return "NAND_2";}

564 Fichier d’inclusion pour la classe NOR_2


//&% nor_2_48_1.h

#include <iostream.h>
#include "circuit_48_1.h"
#include "numerique_48_1.h"

#ifndef NOR_FLAG_
#define NOR_FLAG_

class NOR_2 : public NUMERIQUE {

private:
int out_1; /*sortie */
int set_out_1();

public:
int in_1; /*entree 1*/
int in_2; /*entree 2*/

384 Programmation en C++ IFT-19965


C
++

NOR_2();
NOR_2(int, int);
NOR_2(int, int, char*);
NOR_2(const NOR_2 &);
virtual ~NOR_2();
int lire_in_1() const;
int lire_in_2() const;
int lire_out_1() const;
void ecrire_in_1(int);
void ecrire_in_2(int);
int operator ~ ();
virtual char *affiche_type();
};

#endif NOR_FLAG_

565 Fichier source pour la classe NOR_2


//&% nor_2_48_1.C

#include <iostream.h>
#include "circuit_48_1.h"
#include "numerique_48_1.h"
#include "nor_2_48_1.h"

int NOR_2::set_out_1() {
return ((1-in_1) * (1-in_2));
}

//Constructeur par defaut


NOR_2::NOR_2() {
in_1 = 0; in_2 = 0;
out_1 = set_out_1();
}

//Constructeur avec deux arguments


NOR_2::NOR_2(int i1, int i2) {
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

//Constructeur avec trois arguments


NOR_2::NOR_2(int i1, int i2, char *inpt_str) :
NUMERIQUE(inpt_str){
in_1 = i1; in_2 = i2;
out_1 = set_out_1();
}

Programmation en C++ IFT-19965 385


C
++

//Copy constructor
NOR_2::NOR_2(const NOR_2 & nr) {
set_v_num_plus(nr.read_v_num_plus());
set_v_num_moins(nr.read_v_num_moins());
set_v_analog_plus(nr.read_v_analog_plus());
set_numero(nr.read_numero());
code = new char[strlen(nr.code) + 1];
strcpy(code, nr.code);
set_fan_out(nr.read_fan_out());
ecrire_in_1(nr.in_1);
ecrire_in_2(nr.in_2);
}

//Destructeur
NOR_2::~NOR_2(){
delete [] code;
}

//Readers
int NOR_2::lire_in_1() const{
return in_1;
}
int NOR_2::lire_in_2() const{
return in_2;
}
int NOR_2::lire_out_1() const{
return out_1;
}

//Writers
void NOR_2::ecrire_in_1(int val) {
in_1 = val;
out_1 = set_out_1();
}
void NOR_2::ecrire_in_2(int val) {
in_2 = val;
out_1 = set_out_1();
}

// Surdefinition des operateurs


int NOR_2::operator ~ () {
if (lire_out_1()) return 0;
else return 1;
}
// Affichage du type de composant
char * NOR_2::affiche_type() {return "NOR_2";}

386 Programmation en C++ IFT-19965


C
++

566 Fichier d’inclusion pour la classe RESISTANCE


//&% resis_2_48_1.h

#include <iostream.h>
#include "circuit_48_1.h"
#include "analogique_48_1.h"

#ifndef RESISTANCE_FLAG_
#define RESISTANCE_FLAG_

class RESISTANCE : public ANALOGIQUE {

private:
double res;
double tolerance;

public:

RESISTANCE();
RESISTANCE(double, double);
RESISTANCE(double, double, char*);
RESISTANCE(const RESISTANCE &);
virtual ~RESISTANCE();
double lire_resistance() const;
double lire_tolerance() const;
void ecrire_resistance(double);
void ecrire_tolerance(double);
const RESISTANCE& operator= (const RESISTANCE&);
RESISTANCE operator+ (const RESISTANCE&);
RESISTANCE operator|| (const RESISTANCE&);
virtual char *affiche_type();
virtual double calcule_courant();
};

#endif RESISTANCE_FLAG_

567 Fichier source pour la classe RESISTANCE


//&% resis_2_48_1.C

#include <iostream.h>
#include "circuit_48_1.h"
#include "analogique_48_1.h"
#include "resis_2_48_1.h"

//Constructeur par defaut


RESISTANCE::RESISTANCE() {}

Programmation en C++ IFT-19965 387


C
++

//Constructeur avec deux arguments


RESISTANCE::RESISTANCE(double resi, double tol) {
res = resi; tolerance = tol;
}

//Constructeur avec trois arguments


RESISTANCE::RESISTANCE(double resi,
double tol,
char* inpt_str) : ANALOGIQUE(inpt_str)
{
res = resi; tolerance = tol;
}

//Copy constructor
RESISTANCE::RESISTANCE(const RESISTANCE & rs) {
set_v_num_plus(rs.read_v_num_plus());
set_v_num_moins(rs.read_v_num_moins());
set_v_analog_plus(rs.read_v_analog_plus());
set_numero(rs.read_numero());
code = new char[strlen(rs.code) + 1];
strcpy(code, rs.code);
set_puissance_max(rs.read_puissance_max());
set_type(rs.read_type());
ecrire_resistance(rs.lire_resistance());
ecrire_tolerance(rs.lire_tolerance());
}

//Destructeur
RESISTANCE::~RESISTANCE(){
delete [] code;
}

//Readers
double RESISTANCE::lire_resistance() const {
return res;
}

double RESISTANCE::lire_tolerance() const {


return tolerance;
}

//Writers
void RESISTANCE::ecrire_resistance(double val) {
res = val;
}

void RESISTANCE::ecrire_tolerance(double val) {


tolerance = val;
}

388 Programmation en C++ IFT-19965


C
++

//Surdefinition des operateurs

const RESISTANCE& RESISTANCE::operator= (const RESISTANCE&


right_res) {
set_v_num_plus(right_res.read_v_num_plus());
set_v_num_moins(right_res.read_v_num_moins());
set_v_analog_plus(right_res.read_v_analog_plus());
set_numero(0);
set_puissance_max(right_res.read_puissance_max());
code = new char[strlen(rs.code) + 1];
strcpy(code, rs.code);
set_type(right_res.read_type());
res = right_res.lire_resistance();
tolerance = right_res.lire_tolerance();
return *this;
}

RESISTANCE RESISTANCE::operator+ (const RESISTANCE&


right_res) {
RESISTANCE res_temp;

res_temp.ecrire_resistance(res +
right_res.lire_resistance());
res_temp.ecrire_tolerance(right_res.lire_tolerance());
return res_temp;
}

RESISTANCE RESISTANCE::operator|| (const RESISTANCE&


right_res) {
RESISTANCE res_temp;

res_temp.ecrire_resistance((res *
right_res.lire_resistance()) /
(res + right_res.lire_resistance()));
res_temp.ecrire_tolerance(right_res.lire_tolerance());
return res_temp;
}

// Affichage du type de composant

char * RESISTANCE::affiche_type() {return "RESISTANCE";}

//Calcul du courant
double RESISTANCE::calcule_courant() {
return read_v_analog_plus() / res;
}

Programmation en C++ IFT-19965 389


C
++

568 Fichier d’inclusion pour la classe CONDENSATEUR


//&% condens_2_48_1.h

#include <iostream.h>
#include "circuit_48_1.h"
#include "analogique_48_1.h"

#ifndef CONDENSATEUR_FLAG_
#define CONDENSATEUR_FLAG_

class CONDENSATEUR : public ANALOGIQUE {

private:
double capacite;
double v_claquage;

public:
CONDENSATEUR();
CONDENSATEUR(char *);
CONDENSATEUR(double, double);
CONDENSATEUR(double, double, char*);
CONDENSATEUR(const CONDENSATEUR &);
virtual ~CONDENSATEUR();
double lire_capacite() const;
double lire_v_claquage() const;
void ecrire_capacite(double);
void ecrire_v_claquage(double);
virtual char *affiche_type();
virtual double calcule_courant();
};

#endif CONDENSATEUR_FLAG_

569 Fichier source pour la classe CONDENSATEUR


//&% condens_2_48_1.C

#include <iostream.h>
#include "circuit_48_1.h"
#include "analogique_48_1.h"
#include "condens_2_48_1.h"

//Constructeur par defaut


CONDENSATEUR::CONDENSATEUR() {}

//Constructeur avec deux arguments


CONDENSATEUR::CONDENSATEUR(double cap, double claq) {

390 Programmation en C++ IFT-19965


C
++

capacite = cap; v_claquage = claq;


}

//Constructeur avec trois arguments


CONDENSATEUR::CONDENSATEUR(double cap,
double claq,
char* inpt_str) : ANALOGIQUE(inpt_str){
capacite = cap; v_claquage = claq;
}

//Copy constructor)
CONDENSATEUR::CONDENSATEUR(const CONDENSATEUR & cnd){

set_v_num_plus(cnd.read_v_num_plus());
set_v_num_moins(cnd.read_v_num_moins());
set_v_analog_plus(cnd.read_v_analog_plus());
set_numero(cnd.read_numero());
code = new char[strlen(cnd.code) + 1];
strcpy(code, cnd.code);
set_puissance_max(cnd.read_puissance_max());
set_type(cnd.read_type());
ecrire_capacite(cnd.lire_capacite());
ecrire_v_claquage(cnd.lire_v_claquage());
}

//Destructeur
CONDENSATEUR::~CONDENSATEUR(){
delete [] code;
}

//Readers
double CONDENSATEUR::lire_capacite() const{
return capacite;
}
double CONDENSATEUR::lire_v_claquage() const{
return v_claquage;
}

//Writers
void CONDENSATEUR::ecrire_capacite(double val) {
capacite = val;
}
void CONDENSATEUR::ecrire_v_claquage(double val) {
v_claquage = val;
}

// Affichage du type de composant


char * CONDENSATEUR::affiche_type() {return
"CONDENSATEUR";}

Programmation en C++ IFT-19965 391


C
++

//Calcul du courant
double CONDENSATEUR::calcule_courant(){return 0.0;}

392 Programmation en C++ IFT-19965


C
++

CHAPITRE 49 Les classes et fonctions


amies (friend)

570 La programmation orientée objet impose de facto


l’encapsulation des données. Les variables membres privées
d’une classe ne sont accessibles qu’aux fonctions membres de la
classe ou via les fonctions membres public de la classe si on
veut accéder aux variables membres à partir de l’extérieur de
la classe. Ce principe d’encapsulation interdit évidemment à
une fonction membre d’une classe d’avoir accès directement
aux variables membres private d’une autre classe.

571 Il existe cependant des situations où il est utile qu’une classe


ait accès aux variables membres private d’une autre classe.
Supposons par exemple que nous ayons défini une classe
vecteur et une classe matrice. Il serait intéressant de
pouvoir définir une fonction permettant d’effectuer le produit
d’une matrice par un vecteur. Or, comme les deux classes sont
séparées (i.e. aucune n’hérite de l’autre), cette fonction de
produit ne peut être définie ni dans la classe vecteur, auquel
cas les données de la classe matrice lui seraient inaccessibles
sans les fonctions membres de lecture, ni dans la classe
matrice, auquel cas les données de la classe vecteur lui
seraient inaccessibles sans les fonctions membres de lecture.
Une fonction externe aux deux classes ne pourrait évidemment
pas faire mieux! Un moyen de s’en sortir serait évidemment de

Programmation en C++ IFT-19965 393


C
++

rendre public les variables membres des deux classes.


Cependant, cela irait à l’encontre du principe d’encapsulation
des données et rendrait inutile les principes fondamentaux du
C++. Une meilleure façon de faire serait évidemment d’utiliser
les fonctions membres public d’écriture-lecture, ce qui
respecte intégralement le principe d’encapsulation des
données. Cependant, dans une application comme le produit
d’un vecteur par une matrice, une telle procédure est lourde et
ralentit considérablement la vitesse d’exécution parce qu’elle
exige de nombreux appels de fonctions membres.

572 La notion de fonction amie et de classe amie1 va apporter


une solution intéressante au problème d’encapsulation formelle
des données private et public. Il est en effet possible, lors de
la définition d’une classe, de spécifier qu’une fonction ou
plusieurs fonctions2 sont amies de la classe. Une fonction amie
d’une classe peut avoir accès aux variables membres private
au même titre qu’une fonction membre de la classe. Cela peut
sembler contradictoire qu’une fonction amie puisse avoir accès
aux variables membres private si on se place du point de vue
de l’encapsulation des données. Cependant, il ne faut pas
oublier qu’une fonction ne peut être amie d’une classe que si
cette caractéristique est spécifiée directement dans la classe
elle-même. Le programmeur contrôle donc aussi les fonctions
qui peuvent accéder aux données. Il faut cependant utiliser
l’amitié avec précautions parce qu’un programmeur peut
arriver à contourner la protection en faisant passer une
fonction pour une autre via les pointeurs de fonctions.

573 Il existe plusieurs relations d’amitié:


1. fonction indépendante amie d’une classe;
2. fonction membre d’une classe amie d’une autre classe;
3. fonction amie de plusieurs classes;
4. toutes les fonctions membres d’une classe amies d’une autre
classe (dans ce cas, on dit qu’une classe est amie d’une autre
classe).

1. friend en anglais
2. extérieures à la classe

394 Programmation en C++ IFT-19965


C
++

Nous allons voir comment déclarer chaque type d’amitié en


prenant deux classes fictives A et B définies comme suit:
class A {
private:
int xa;
public:
A() {}
int f_membre_a();
};

class B {
private:
int xb;
public:
B(){}
int f_membre_b();
};

574 Pour spécifier qu’une fonction indépendante funct() recevant


un argument de type A et un argument de type B est amie de la
classe A et retournant un entier, il suffit d’adopter la syntaxe
suivante pour la déclaration de la classe A:
class A {
private:
int xa;
public:
A() {}
int f_membre_a();
friend int funct(A &a, B &b);
};

575 Pour spécifier qu’une fonction indépendante funct() recevant


un argument de type A et un argument de type B est amie de la
classe A et de la classe B et retournant un entier, il suffit
d’adopter la syntaxe suivante pour les déclarations des classes
A et B:
class A {
private:
int xa;
public:
A() {}
int f_membre_a();
friend int funct(A &a, B &b);

Programmation en C++ IFT-19965 395


C
++

};

class B {
private:
int xb;
public:
b(){}
int f_membre_b();
friend int funct(A &a, B &b);
};

576 Pour spécifier que la fonction membre f_membre_a() de la


classe A est amie de la classe B, il suffit d’adopter la syntaxe
suivante pour la classe B:
class B {
private:
int xb;
public:
b(){}
int f_membre_b();
friend int funct(A &a, B &b);
friend int A::fmembre_a();
};

577 Si toutes les fonctions membres d’une classe A sont amies d’une
autre classe B, il suffit alors de spécifier l’amitié en adoptant la
syntaxe suivante:
class B {
private:
int xb;
public:
b(){}
int f_membre_b();
friend int funct(A &a, B &b);
friend class A;
};

578 La surdéfinition d’opérateurs est une situation où les fonctions


amies sont fréquemment utilisées. Au lieu d’inclure la fonction
membre surdéfinissant l’opérateur à l’intérieur de la classe, on
peut simplement définir cette fonction à l’extérieur de la classe
en la déclarant amie de la classe. Par exemple, pour la

396 Programmation en C++ IFT-19965


C
++

surdéfinition de l’opérateur + pour la classe RESISTANCE, on


aurait pu avoir la définition suivante:
//&% resis_2_49_1.h

#include <iostream.h>
#include "circuit_48_1.h"
#include "analogique_48_1.h"

#ifndef RESISTANCE_FLAG_
#define RESISTANCE_FLAG_

class RESISTANCE : public ANALOGIQUE {

private:
double res;
double tolerance;

public:

RESISTANCE();
RESISTANCE(double, double);
RESISTANCE(double, double, char*);
RESISTANCE(const RESISTANCE &);
virtual ~RESISTANCE();
double lire_resistance() const;
double lire_tolerance() const;
void ecrire_resistance(double);
void ecrire_tolerance(double);
const RESISTANCE& operator= (const RESISTANCE&);
friend RESISTANCE
operator+ (const RESISTANCE&, const RESISTANCE&);
RESISTANCE operator|| (const RESISTANCE&);
virtual char *affiche_type();
virtual double calcule_courant();
};

#endif RESISTANCE_FLAG_

On remarque que l’opérateur + est maintenant défini comme


une fonction indépendante amie de la classe et qu’elle reçoit
deux opérandes de type RESISTANCE. On définit la fonction
comme suit:
RESISTANCE operator+
(RESISTANCE & r_1, RESISTANCE & r_2) {
RESISTANCE r_tot;
r_tot.res = r_1.res + r_2.res;
r_tot.tolerance = r_1.tolerance;
return r_tot;

Programmation en C++ IFT-19965 397


++ C
}
On remarque que la fonction + est symétrique car elle reçoit
deux arguments du même type sans utilisation d’un argument
implicite via le pointeur this comme nous l’avons vu
précédemment.

579 On peut finalement formuler les remarques suivantes


concernant les fonctions amies:
1. L’emplacement de la déclaration d’amitié d’une fonction (ou
d’une classe) dans une classe n’a pas d’importance.
2. Une fonction amie ne reçoit plus d’argument implicite this
comme c’était le cas pour une fonction membre.
3. Il arrive qu’une fonction amie d’une classe retourne un objet
du même type que la classe et il est fréquent que cet objet
soit un objet qui a été défini localement à la fonction. Dans ce
cas, l’objet doit obligatoirement être retourné par valeur et
non par référence. Un retour par référence ne ferait que
retourner une référence à un espace mémoire qui est libéré
lors du retour de la fonction puisque l’objet défini localement
est détruit par le destructeur.

Résumé
580 ✔ Les variables membres private d’une classe ne sont pas
directement accessibles à des fonctions indépendantes de la
classe.
✔ Un moyen d’accéder aux variables membres private d’une
classe est d’utiliser les fonctions membres public d’écriture-
lecture.
✔ Un autre moyen moins sécuritaire mais souvent plus
pratique est de déclarer la fonction comme étant amie de la
classe. Dans ce cas, la fonction amie a accès aux variables
membres private (et évidemment public) de la classe.
✔ La surdéfinition d’opérateurs peut être implantée via les
fonctions amies.

398 Programmation en C++ IFT-19965


C
++

CHAPITRE 50 La réutilisation des


fonctions: la notion de
fonction générique
(template)

581 Nous avons vu qu’il est possible de surdéfinir des fonctions en


leur donnant un nom unique mais en leur faisant réaliser un
travail différent. La notion de fonction générique (ou
template) permet plutôt d’écrire une fonction une seule fois
pour qu’elle puisse être appliquée à des données de types
différents. Supposons par exemple qu’une fonction ait été
définie pour recevoir des arguments de type int, les
additionner et retourner la valeur de la somme:
int func_int(int a, int b){ return a + b;}

Cette fonction ne peut que recevoir des arguments entiers et


retourner une valeur entière1. Si on veut une fonction qui
reçoit des arguments float mais qui fait exactement la même
opération, il faut définir une nouvelle fonction:
float funct_float(float a, float b) { return a + b;}

Cela signifie que le code peut être encombré de plusieurs


fonctions effectuant le même traitement mais sur des données
de type différent.

1. On suppose qu’on n’utilise pas de casting des arguments...

Programmation en C++ IFT-19965 399


C
++

Le C++ offre heureusement un mécanisme, appelé fonction


générique, patron ou template, qui permet de définir une
seule fonction qui peut être appliquée à plusieurs types de
données sans avoir à modifier la fonction. Le patron de la
fonction est le même pour tous les types de données.

582 Supposons qu’on veuille définir une fonction générique qui


reçoit deux valeurs numériques et qui retourne la valeur
minimum. La syntaxe pour définir le patron (template) de la
fonction est la suivante:
//&% patron_50_1.C

#include <iostream.h>

template <class T> T min( T a, T b) {


if (a < b) return a;
else return b;
}

int i1 = 2;
int i2 = 5;
float f1 = 3.4;
float f2 = 7.9;

void main() {

cout << "Valeurs entieres: " << min(i1,i2) << endl;


cout << "Valeurs reelles : " << min(f1,f2) << endl;

Le résultat de l’exécution du programme est:

Résultat
Valeurs entieres: 2
Valeurs reelles : 3.4

583 Dans la définition du patron ci-dessus, nous constatons que la


syntaxe template < class T> est utilisée pour définir un

400 Programmation en C++ IFT-19965


C
++

patron dans lequel apparaît un paramètre de type nommé T.


Le C++ a décidé d’utiliser le mot clé class pour préciser que T
est un paramètre de type...ce qui est un peu confondant
puisque cela n’a rien à voir avec le mot clé class servant à
définir une classe. Donc ici, T est un paramètre de type du
patron. Le reste de la déclaration T min( T a, T b) sert
uniquement à spécifier que la fonction min reçoit deux
paramètres ayant le type T et retourne une valeur de type T. Ce
n’est que lorsque les paramètres sont effectivement fournis à la
fonction que T est fixé. Ainsi, si on fournit deux arguments
entiers le paramètre de type T devient int. Lorsqu’on passe
des arguments float, T devient float.

584 En se basant sur l’exemple ci-dessus, les instructions de


définitions d’un patron ressemblent à des instructions
exécutables de définition d’une fonction classique. Cependant,
le mécanisme même des patrons en C++ fait que ces
instructions sont utilisées par le compilateur pour fabriquer la
fonction à chaque fois qu’elle est appelée. On ne peut dont
placer les patrons dans un module objet exécutable une fois
pour toutes. Il faut donc placer les patrons dans un fichier
d’extension .h et inclure ce fichier dans toutes les sources
nécessitant l’instanciation du patron dans un contexte précis.

585 Le patron de la fonction min peut être utilisé pour tout type de
données standard (char, char *, int, float, double, long,
etc) ou pour tout type défini par l’usager. Par exemple, on
pourrait utiliser la fonction min pour des objets de type
RESISTANCE à condition que l’opérateur < (plus petit que) ait
été surdéfini pour cette classe.

Notion avancée
586 Un patron de fonction pourra s’appliquer à un patron de
classe.

587 Dans le cas où un patron de fonction reçoit plusieurs paramètre


de type, il suffit simplement de généraliser la syntaxe:

Programmation en C++ IFT-19965 401


C
++

template <class T,class U,class V> U fct(T a, V *b, U c){


U interne;
interne = a + (*b);
return interne;
}

Ici, le patron utilise trois paramètres de type. La fonction


fct reçoit deux arguments, l’un de type T et l’autre de type
pointeur à V, et retourne une valeur de type U. On utilise donc T,
U et V comme s’ils étaient des types. Ils seront remplacés plus
tard par un type standard ou un type défini par l’usager. Tous
les opérateurs vus jusqu’à maintenant sont encore valides avec
les paramètres de type. Par exemple, on peut écrire
T * adr;
adr = new T [10];

pour allouer l’espace nécessaire à un tableau de paramètre de


type T.

588 Il faut remarquer que le compilateur ne fait aucune conversion


de type entre les arguments fournis au patron. Ainsi, si on a:
template <class T, class U>
fct (T a, U b, T c){...}

il faut absolument que les arguments qu’on fournit pour les


paramètres a et c soient du même type, autrement le
compilateur signalera une erreur.

589 Un patron de fonction peut contenir des paramètres qui ne sont


pas des paramètres de type:
template <class T> T abcd(T t1, int t2) {...}

Ici, la fonction abcd est formée d’un paramètre de type et d’un


paramètre régulier.

590 Il faut noter qu’il est possible de surdéfinir des patrons de


fonction.

402 Programmation en C++ IFT-19965


C
++

591 On peut aussi spécialiser un patron de fonction en définissant


une fonction ayant le même nom que le patron mais exécutant
un code spécifique mieux adapté à l’application:
template <class T> abcd(T t1, T t2){...}
void abcd(char* t1, char* t2){...}

Résumé
592 ✔ Lorsqu’une fonction doit être utilisée de la même façon pour
divers types de données standards ou définis par l’utilisateur, il
est intéressant de définir un patron pour cette fonction.
✔ La syntaxe de définition d’un patron est
template <class T, class U,..., class P>
retour nom_fonction(paramètres) {...}
✔ On peut surdéfinir un patron de fonction.
✔ On peut spécialiser un patron de fonction.

Programmation en C++ IFT-19965 403


C
++

404 Programmation en C++ IFT-19965


C
++

CHAPITRE 51 La réutilisation des


classes: la notion de
patrons de classe
(template)

593 Nous avons vu tout au cours de ces notes que la conception


d’une classe demande un travail important. Il serait donc
intéressant de pouvoir utiliser une classe complexe sans avoir à
la modifier en profondeur. De la même manière que le C++
permet de réutiliser des fonctions via les patrons de fonctions,
il permet également de réutiliser des classes complètes via les
patrons de classe (template). Il suffira donc de définir une
classe une seule fois, le compilateur se chargeant de l’adapter à
différents types.

594 Il faut souligner que les patrons de classe se distinguent


significativement des patrons de fonctions et que leur
utilisation n’est pas qu’une généralisation aux classes des
concepts vus pour les patrons de fonctions.

595 Pour illustrer le concept de patron de classe, nous allons donner


l’exemple d’une classe très simple appelée complexe. Cette
classe contient deux variables membres donnant les parties
réelle et imaginaire d’un nombre complexe.
//&% cplx_51_1.C

#include <iostream.h>

Programmation en C++ IFT-19965 405


C
++

class complexe {
private:
int reelle;
int imaginaire;
public:
complexe(){
reelle = 0;
imaginaire = 0;
}

complexe(int re, int im) {


reelle = re;
imaginaire = im;
}

int lit_reelle() {
return reelle;
}
int lit_imaginaire() {
return imaginaire;
}

void ecrit_reelle(int re) {


reelle = re;
}

void ecrit_imaginaire(int im) {


imaginaire = im;
}

void affiche_nombre(){
cout << "Partie reelle: " << lit_reelle() << endl;
cout << "Partie imaginaire: "
<< lit_imaginaire() << endl;
}

};
Cette classe est intéressante mais ne peut que servir à décrire
des objets qui sont des nombres complexes avec des
composantes entières (int). Pour décrire des nombres
complexes ayant des composantes réelles, il faudrait
normalement définir une autre classe où les variables membres
sont de type double.

596 Heureusement, le C++ permet de définir des patrons de


classes permettant de réutiliser une classe avec plusieurs

406 Programmation en C++ IFT-19965


C
++

types de données différents. Par exemple, pour définir un


patron de classe de nombres complexes, il suffit d’adopter la
syntaxe suivante:
//&% cplx_51_2.C

#include <iostream.h>

template <class T> class complexe {


private:
T reelle;
T imaginaire;
public:
complexe(){
reelle = 0;
imaginaire = 0;
}

complexe(T re, T im) {


reelle = re;
imaginaire = im;
}

T lit_reelle() {
return reelle;
}
T lit_imaginaire() {
return imaginaire;
}

void ecrit_reelle(T re) {


reelle = re;
}

void ecrit_imaginaire(T im) {


imaginaire = im;
}

void affiche_nombre(){
cout << "Partie reelle: " << lit_reelle() << endl;
cout << "Partie imaginaire: " << lit_imaginaire() <<
endl;
}

};
On remarque que le mot réservé int a été remplacé par le
paramètre de type T.
Pour définir un patron de classe, il faut spécifier au
compilateur qu’il s’agit bien d’un patron et donner ensuite la

Programmation en C++ IFT-19965 407


C
++

liste des paramètres de type précédés par le mot class1 et


séparés par des virgules:
template <class T, class U, class V)
class nom_classe {...};

597 Dans le cas du patron de la classe du paragraphe 596, la


fonction membre affiche_nombre est en ligne (inline),
c’est-à-dire que le code de la fonction réside directement dans la
classe. Si le code de la fonction n’est pas défini à l’intérieur de
la classe mais plutôt à l’extérieur de celle-ci, il faut adopter la
syntaxe suivante pour définir la fonction affiche_nombre:
//&% cplx_51_3.C

#include <iostream.h>

template <class T> class complexe {


private:
T reelle;
T imaginaire;
public:
complexe(){
reelle = 0;
imaginaire = 0;
}

complexe(T re, T im) {


reelle = re;
imaginaire = im;
}

T lit_reelle() {
return reelle;
}
T lit_imaginaire() {
return imaginaire;
}

void ecrit_reelle(T re) {


reelle = re;
}

void ecrit_imaginaire(T im) {

1. Encore ici, l’usage du mot réservé class est confondant...

408 Programmation en C++ IFT-19965


C
++

imaginaire = im;
}

void affiche_nombre();
};

template <class T> void complexe<T>::affiche_nombre(){


cout << "Partie reelle: " << lit_reelle() << endl;
cout << "Partie imaginaire: " << lit_imaginaire() <<
endl;
}

Il spécifier au compilateur que la fonction membre recevra un


paramètre de type (en écrivant template <class T>) et que
la classe utilise ce paramètre de type (en écrivant
complexe<T>::affiche_nombre)1.

598 Pour utiliser un patron de classe il suffit de procéder comme


dans l’exemple suivant:
//&% cplx_51_4.C

#include <iostream.h>

template <class T> class complexe {


private:
T reelle;
T imaginaire;
public:
complexe(){
reelle = 0;
imaginaire = 0;
}

complexe(T re, T im) {


reelle = re;
imaginaire = im;
}

T lit_reelle() {
return reelle;
}
T lit_imaginaire() {
return imaginaire;

1. Le deuxième <T> semble redondant mais est nécessaire dans la syntaxe du C++.

Programmation en C++ IFT-19965 409


C
++

void ecrit_reelle(T re) {


reelle = re;
}

void ecrit_imaginaire(T im) {


imaginaire = im;
}

void affiche_nombre();
};

template <class T> void complexe<T>::affiche_nombre(){


cout << "Partie reelle: " << lit_reelle() << endl;
cout << "Partie imaginaire: " << lit_imaginaire() <<
endl;
}

void main(){

complexe <int> c_i(2,3);


complexe <double> c_d(3.7,7.9);

c_i.affiche_nombre();
c_d.affiche_nombre();

Pour utiliser le patron, il suffit simplement de déclarer un objet


de la classe complexe, de spécifier le type de son paramètre de
type (ici <int> ou <double>) et de donner le nom de la
variable et les valeurs initiales à fournir au constructeur avec
arguments. L’exécution du programme donne:

Résultat
Partie reelle: 2
Partie imaginaire: 3
Partie reelle: 3.7
Partie imaginaire: 7.9

410 Programmation en C++ IFT-19965


C
++

599 Comme pour les patrons de fonctions, la définition des patrons


de classes et de leurs fonctions membres (en ligne ou non) sont
nécessaire au compilateur afin qu’il puisse les instancier
correctement lors de la compilation. Il n’est donc pas possible
de fournir à l’utilisateur un fichier de déclaration de la classe
patron (d’extension .h) et un fichier de code objet (d’extension
.o) pour qu’il puisse s’en servir dans son application. Il faut
fournir à l’utilisateur les instructions source de toutes les
fonctions membres. C’est là un inconvénient majeur des
templates en C++.

600 Il faut noter que, dans un patron de classe, les paramètres de


type peuvent être en nombre quelconque:
template <class T, class U, class V> class classe_a{...};

Pour utiliser le patron, il suffit d’adopter la syntaxe suivante:


classe_a <int, double, char> nom_objet;

601 Il est même possible d’utiliser un patron de classe instancié


comme valeur de paramètre de type d’un autre patron. Par
exemple, si un patron de classe abcd a été défini, on peut
écrire:
classe_a <int, abcd <int> , char> nom_objet;

602 Un patron de classe peut contenir des variables membres


static. Dans ce cas, il faut savoir que chaque instanciation de
la classe possède son jeu de variables static.

603 Un patron de classe peut contenir des paramètres spécifiques


au niveau du type:
template <class T, int n> class table{
private:
T x[n];
public:
table(){}
};
Qu’on utilise ensuite comme suit:

Programmation en C++ IFT-19965 411


++ C
table <int,4> t;

604 On peut spécialiser un patron de classe soit en spécialisant


seulement certaines fonctions membres, soit en spécialisant la
classe au complet. Par exemple, si on veut que la fonction
membre affiche_nombre de la classe complexe ait un
comportement spécial pour des données de type char, il suffit
d’adopter la syntaxe suivante:
//Fonction non specialisee
template <class T> void complexe<T>::affiche_nombre(){
cout << "Partie reelle: " << lit_reelle() << endl;
cout << "Partie imaginaire: " << lit_imaginaire() <<
endl;
}

//Fonction specialisee pour les char


void complexe<char>::affiche_nombre(){...}

605 Pour spécialiser la classe au complet, il suffit simplement


d’adopter la syntaxe suivante:
class complexe<char> {...}

On peut alors traiter les objets de type complexe formés de


composantes char de façon spécifique.

606 Au sein d’un patron de classe, on peut effectuer trois sortes de


déclaration d’amitié:
1. déclaration de classes ou fonctions amies ordinaires :

template <class T> class classe_a{


private:
T x;
public:
classe_a(){}
friend int fct(int a);
};

2. déclaration d’instances particulières d’un patron de classe ou


d’un patron de fonction (syntaxe 1):
template <class T> int funct(T i) {...}

412 Programmation en C++ IFT-19965


C
++

template <class T> class classe_b{


private:
T x,y;
public:
classe_b(){}
friend int fct(int a);
friend int funct(double i);
};
3. déclaration d’instances particulières d’un patron de classe ou
d’un patron de fonction (syntaxe 2):
template <class T, class U> int funct(T i) {...}

template <class T> classe_b{


private:
T x,y;
public:
classe_b(){}
friend int fct(int a);
friend int funct(U i);
};
4. déclaration d’un autre patron de fonction ou d’un autre
patron de classe:
template <class T> class point {...}
template <class T> int fct (T i) {...}
template <class T, class U> class class_c{
private:
int x,y,z;
public:
classe_c(){}
template <class X> friend class point<X>;
template <class X> frient fct(point <X>);
};

Dans ce dernier exemple, qui est loin d’être évident, toutes les
instances du patron point sont amies de n’importe quelle
instance du patron classe_c...

607 Les patrons peuvent aussi être soumis aux règles de l’héritage.
Cependant, ces notions sont très avancées. Les références [7],
[3] et [4] traitent plus en détails ces aspects “obscurs” du C++.

Résumé
608 ✔ On peut réutiliser des classes en créant des patrons de classe
aussi appelés templates.

Programmation en C++ IFT-19965 413


++ C
✔ Toutes les notions vues pour les classes s’appliquent aux
patrons.
✔ L’utilisation des patrons est une notion avancée du C++.

414 Programmation en C++ IFT-19965


C
++

Bibliographie

Références sur le C++

Programmation en C++ IFT-19965 415


++ C
[1] J.J. Barton, L.R. Nackman, “C++, An Introduction with Advanced
Techniques and Examples,” Addison-Wesley, 1994, 671 p., ISBN
0-201-53393-6.
[2] R. Decker, S. Hirshfield, “The Object Concept: An Introduction to
Computer Programming Using C++,” PWS, 1995, 454 p., ISBN 0-
534-20496-1.
[3] H.M Deitel, P.J. Deitel, “C++ How to Program,” Prentice Hall,
1994, 950 p., ISBN 0-13-117334-0.
[4] Claude Delannoy, “Programmer en langage C++,” Eyrolles,
1993,438 p., ISBN 2-212-08774-8.
[5] B. Flamig, “Turbo C++ Step-by-Step,” Wiley, 1991, 402 p., ISBN
0-471-58056-2.
[6] S. Lippman, “C++ Primer,” 2nd Edition, Addison-Wesley, 1991,
614 p., ISBN 0-201-54848-8.
[7] B. Stroustroup, “The C++ Programming Language,” Addison-
Wesley, 1991.
[8] P.H. Winston, “On to C++,” Addison-Wesley, 1994, 305 p., ISBN
0-201-58043-8.

416 Programmation en C++ IFT-19965


C
++

Références sur Unix


[9] P.W. Abrahams, B.R. Larson, “UNIX for the Impatient,” Addison-
Wesley, 1992, 559 p., ISBN 0-201-55703-7.
[10] S.R. Bourne, “The UNIX System,” Addison-Wesley, 1983, 351 p.,
ISBN 0-201-13791-7.
[11] A. Oram, S. Talbot, “Managing Projects with MAKE,” O’Reilly &
Associates, 1991, ISBN 0-937175-0.
[12] G. Todino, J. Strang, P. Peek, “Learning the UNIX Operating Sys-
tem,” O’Reilly & Associates, 1993, 92p., ISBN 1-56592-060-0.

Programmation en C++ IFT-19965 417


C
++

418 Programmation en C++ IFT-19965


C
++

Symbols
A
abstaction des données 89
abstraction des données 6
abstraction des procédures 6
addition 23
adresse 175
affectation 18
arguments 36
associativité 24
astérisque 177
automatique 50

B
barres obliques 21
binaire 25
break 212

C
C++ 5
case 212
casting 26, 129
catégories 55
cerr 213
chaîne de caractères 12, 245
char 19
cin 31
classe 56
classe ami 394
classe de bas 116
classe dérivée 116
classes 55
classes extrinsèques 56
classes intrinsèques 56
code exécutable 10
code objet 9
code source 9
commandes de construction 317
commentaires 21
compilateur 9
compiler 9
const 52
constructeur par défaut 73
constructeur par recopie 369, 370, 375
constructeurs 73
consultation directe 124
contenu 17
copy constructor 369
cout 12

D
décalage à gauche 28
déclaration 18, 49, 57, 297
default 212
définition 49, 57, 297
delete 186, 351, 358
dérivation publique 120
dérivations privée 120
dérivations protégées 120

Programmation en C++ IFT-19965 419


C
++

destructeur 370
destructeurs 361
disque dur 9
division 23
données internes 5
double 19
drapeaux 102
dynamiques 51

E
éditeur 9
éditeur de liens 10, 298
effets secondaires 11
élément 161
else 132
endl 12
énoncé associé 131, 147, 149
énoncés 11
entiers 5, 17
enum 216
ET 139
expression booléenne 131, 149
expression d’entrée 149

F
fichier cible 317
fichier objet 9
fichier source 9
fichiers prérequis 317
float 19
flot 169
fonction 36
fonction amie 279, 394
fonction générique 399, 400
fonction membre 65
fonction membre d’écriture 83
fonction membre virtuelle 200
fonction virtuelle pure 209
fonctions 6, 11
fonctions d’accès 89
FORTRAN 6
freestore 178

H
héritage 107
hiérarchie 6

I
identificateur 17, 49
if 131, 132
if-else 132
indice 161
initialiser 18
inline 408
instructions 9
int 17, 19
interface publique 97

L
langage procédural 6

420 Programmation en C++ IFT-19965


C
++

long 19
long double 19

M
macros 323
macros prédéfinies 326
main 11
make 317
makefile 317
masque 50
modularité 124
modulo 24
mot reservé 18
multiplication 23

N
négation 128
new 178
non-divulgation 124
non-duplication 123
nul 245, 336

O
objet 5
objet de transit 271
objets 55
objets atomiques 56
objets de données intrinsèques 56
opérateur conditionnel 28, 135, 136
opérateur d’accès aux membres 58
opérateur d’évaluation de portée 70, 71, 297
opérateur d’extraction 31
opérateur d’insertion 12
opérateur de pointeurs aux classes 179
opérateur de sortie 12
opération d’affectation étendue 150
opérations d’itération 147
operator 277
origine zéro 161
OU 139

P
paramètre de type 401, 407
paramètres 37
Pascal 6
passage par valeur 48
patron de classe 401
patron de la fonction 400
patrons de classe 405
patrons de classes 406
pointeur 175
portée locale 51
portée universelle 51
prédicats 127
préfixe 151
principe de visibilité locale 297
priorité 24
private 229
procédure 11
procédures 6

Programmation en C++ IFT-19965 421


C
++

programmation objet 5
programme source 9
protected 229
protégée 232
prototype 69

R
redirection 32
réels 5
référence 265
référence à une constante 372
règles suffixes 323
relations de dépendance 317
représentation explicite 123
réutilisation 45
run-time 175

S
short 19
simplicité 124
sizeof 20, 186
sous-classes 110
soustractio 23
statiques 51
superclasse 109
surdéfinir un opérateur 255
surdéfinition de fonctions 41
switch 211, 215
symboles ponctuation 11

T
tableau 161
template 399, 400, 405
ternaire 136
this 226, 280, 398
touch 319
type 5, 17, 49
types de données d’énumération 218

U
unaire 25

V
variable 17
variable globale 50
variable locale 50
variables 11
variables d’énumération 218
variables membres 57
virtual 200, 311
visibilité 315
visibilité locale 124
void 39
vrai 127

W
while 147

422 Programmation en C++ IFT-19965


C
++

Programmation en C++ IFT-19965 423


Après avoir bien travaillé en C++...

...il faut se reposer.

Vous aimerez peut-être aussi