Vous êtes sur la page 1sur 270

PROGRAMMATION II

INF-112-99

NOTES DE COURS

PAR

MARTIN LESAGE

CHARGÉ DE COURS

SESSION : HIVER 2004


COURS 01

INF-11299 – PROGRAMMATION II

Familiarisation avec le logiciel C++ Builder


Référence : Schildt, Herbert & Guntle, Greg, Borland C++ Builder, The complete reference, Osborne/McGraw-
Hill, Berkeley, 2001, 977pp, ISBN: 0-07-212778-3.

INTRODUCTION

Le logiciel C++ Builder a deux modes séparés d’opération :

IDE (Integrated Development Environment). L’environnement IDE permet l’édition, la


compilation, l’exécution et le déverminage de programmes par l’itermédiaire d’une
interface facile à utiliser. Les moyens d’iteraction de l’usager avec cette interface
sont les suivants :

caractères entrés au clavier,

clics de souris; et

menus.

Cette interface est très facile d’apprentissage et les commandes se retrouvent


dans les menus.

Mode traditionnel d’appel du compilateur par une ligne de commande au système


d’exploitation (« shell »). Ce mode est utilisé par certains programmeurs voulant
compiler un programme rapidement sans utiliser l’interface graphique.
Lorsque l’utilisateur appelle l’application C++ Builder, quatre fenêtres flottantes
apparaissent :

La fenêtre des menus,

La fenêtre de l’inspecteur d’objets,

La fiche (form window); et

La fenêtre du code
LA FENÊTRE DES MENUS

La fenêtre du haut, ou fenêtre des menus, est la fenêtre de contrôle principal de


l’environnement IDE. Elle contient plusieurs menus pouvant faire accomplir à l’interface
certaines tâches telles que de charger un fichier, de compiler un programme ou d’activer
une option.

Les menus sont déroulants de haut en bas (« pull-down menus ») et peuvent contenir des
sous-menus.

LES BARRES D’OUTILS

Les rangées d’icônes qui sont en-dessous de la barre de menus sont les barres d’outils.

Les barres d’outils contiennent les commandes les plus utilisées des menus.

Il y a six types de barres d’outils :

¾ Standard,

¾ View,

¾ Debug,

¾ Custom,

¾ Component Palette; et

¾ Desktops.

Le nombre de barres d’outils visibles peut être déterminé par l’option « toolbar » du menu
« view ».
Ce tableau résume le contenu des menus principaux :

Item Description
File Sert à créer de nouvelles applications; des fiches, charge et
enregistre des projets et des fichiers; gère les impressions et
permet de sortir de l’application.
Edit Effectue les opérations standard d’édition (faire, défaire, copier,
coller) et sert à aligner les composantes graphiques placées sur
une fiche.
Search Effectue diverses recherches et remplacements de chaines de
caractères.
View Fournit un accès au gestionnaire de projets, à l’inspecteur
d’objets, permet l’affichage des fiches et des fenêtres de code
C++, contrôle l’affichage des barres d’outils (toolbar) du menu
principal et des fenêtres du dévermineur (débugger).
Project Crée, enregistre, construit et fait la gestion des modules des
projets.
Run Permet l’exécution des programmes et le contrôle de l’exécution
des programmes par le dévermineur.
Component Permet l’édition et l’insertion d’objets, de composantes et de
contrôles activeX.
Tools Effectue la gestion des options de configuration de
l’environnement de l’application, de l’éditeur, du dévermineur. Ce
menu permet également de lancer un éditeur d’images et d’icônes
similaire à MS Paint.
Help Permet l’accès à des informations sous forme de chapitres ou par
recherche d’un sujet. La fonction d’aide de cet environnement est
très complète et contient beaucoup d’informations sur les
fonctions et la programmation en C++.
PANORAMAS D’ÉCRAN GRAPHIQUE (FICHES/FORM)

Une fiche constitue le ou les panoramas d’écran graphique de l’application à produire.

Lors de l’activation de l’environnement C++ Builder, une fiche vide apparaît.

Toutes les composantes graphiques se placent sur cette fiche.

Pour commencer une application, il suffit de sélectionner un onglet de la librairie des composantes
visuelles (VCL) et de cliquer sur l’une des composantes graphiques affichées.

Il suffit maintenant de cliquer sur la fiche à l’endroit où vous voulez placer votre composante
graphique
LA FENÊTRE DU CODE (UNIT)

Lors de l’entrée dans l’environnement IDE, cette fenêtre est généralement cachée par la
fiche. L’utilisateur peut accéder à cette fenêtre en cliquant au bas de la fiche ou en
sélectionnant la fenêtre de code désirée par le menu « View ».

Cette fenêtre est également appelée fenêtre de code et est un éditeur de fichier en format
texte. Toute la programmation et les modules de codes sont placés dans ces fenêtres.

Le format de ces fichiers est du texte ASCII standard. Vous pouvez éditer ces fichiers avec
MS Notepad ou tout autre éditeur de texte.
VCL (LIBRAIRIE DES COMPOSANTES GRAPHIQUES)
Une application, dans l’environnement C++ Builder, est constituée de composantes
graphiques provenant de la librairie des composantes graphiques.

Ces items sont seulement disponibles dans le cas des applications Windows. Une
application console (application DOS) fonctionne en mode textuel dans une fenêtre DOS.

Pour commencer une application, il suffit de sélectionner un onglet de la librairie des


composantes visuelles (VCL) et de cliquer sur l’une des composantes graphiques
affichées.

Il suffit maintenant de cliquer sur la fiche à l’endroit où vous voulez placer votre
composante graphique.
BARRE DE COMPOSANTES « STANDARD »

Icône Nom Description


Pointer Le pointeur sert à sélectionner les composantes
graphiques de tous les onglets. Il sert également à
déselectionner une composante.
Frames Affiche une boite de dialogue contenant une liste de
frames (« cadres ») faisant partie du projet.
MainMenu Composante qui crée un menu principal à l’application.

PopupMenu Menu caché qui apparaît sur une commande de


l’utilisateur.
Label Composante d’étiquette. Cette composante écrit du texte
sur la fiche.
Edit Composante d’affichage et de saisie de texte (une ligne
seulement).
Memo Composante d’affichage et de saisie de blocs de texte (une
ou plusieurs lignes).
Button Crée un bouton standard.

CheckBox Crée des boutons cochés.

RadioButton Crée des boutons mutuellement exclusifs.

ListBox Crée une liste de choix.

ComboBox Peut effectuer de la saisie de texte ou créer un menu


déroulant.
ScrollBar Permet d’augmenter ou de diminuer des valeurs à l’aide du
déplacement d’un curseur.
GroupBox Permet de grouper des éléments similaires.

RadioGroup Permet de regrouper des boutons radio afin qu’ils


deviennent mutuellement exclusifs.
Panel Permet de délimiter et de regrouper des composantes sur
un panneau.
ActionList Associe des commande à des actions spécifiques. Utilisé
conjointement avec des menus.
BARRE DE COMPOSANTES ADDITIONNELLES « ADDITIONAL »

Icône Nom Description


BitBtn Crée un bouton pouvant avoir des icônes (fichiers
graphiques) comme motifs.
SpeedButton Crée un bouton pouvant représenter graphiquement
son état : sélectionné, désélectionné, actif (« On »),
inactif (« Off »).
MaskEdit Permet de faire de la saisie masquée de chaînes de
caractères.

StringGrid Matrice de chaînes de caractères.

DrawGrid Matrice d’objets graphiques.

Image Permet d’importer des images (format JPEG et BMP).

Shape Composante graphique permettant d’afficher des


formes géométriques (cercles, triangles, carrés,
rectangles).
Bevel Permet de faire ressortir des formes en biseau (carrés
et rectangles).
ScrollBox Espace pourvu de navigation avec des curseurs.

CheckListBox Combinaison de ListBox et de CheckBox.

Splitter Permet de séparer des composantes, des menus et


des contrôles à l’exécution.

StaticText Composante similaire à la composante Label.

ControlBar Permet de faire rouler et dérouler des barres de


boutons ou des barres d’outils.
ApplicationEvents Sert à faire des associations entre les événements et
les fiches.
BARRE DE COMPOSANTES « WIN32 »

Icône Nom Description


TabControl Crée une interface contrôlable par les tabulateurs.

PageControl Crée une interface à onglet.

ImageList Gestionnaire de listes d’images graphiques.

RichEdit Composante « Memo » pouvent gérer le format


« Microsoft RichText ».
TrackBar Curseur pouvant générer des valeur variables afin de
contrôler des composantes graphiques.
ProgressBar Barre de progrès.

UpDown Composantes pouvant incrémenter ou décrémenter des


valeurs en cliquant sur les flèches.
HotKey Création de combination de touches pouvant activer
certaines opérations.
Animate Affiche une fenêtre de contrôle d’animation pour la
gestion et l’affichage des fichiers de format AVI.
DateTimePicker Crée un calendrier permettant de sélectionner une date
ou une heure.
MonthCalendar Crée un calendrier permettant de sélectionner les mois.

TreeView Crée une fenêtre permettant d’afficher les répertoires et


les fichier de la même façon que l’explorateur Windows.
ListView Permet d’afficher des listes d’objets sous forme de
colonnes.
HeaderControl Permet de commenter les colonnes.

StatusBar Crée une barre de statut au bas d’une fenêtre ou d’un


panorama d’écran.
ToolBar Crée une barre d’outils comportant des boutons
intégrés.
CoolBar Crée une barre de contrôle pouvant être déplacée et
redimensionnée.
PageScroller Permet de regrouper et déplacer des composantes.
BARRE DE COMPOSANTES SYSTÈME « SYSTEM »

Icône Nom Description


Timer Composante invisible effectuant la gestion du temps et pouvant
déclencher des événements à un temps précis ou périodiquement
dans un intervalle de temps.

BARRE DE COMPOSANTES DE DIALOGUE « DIALOG »

Icône Nom Description


OpenDialog Affiche la fenêtre d’ouverture des fichiers.

SaveDialog Affiche la fenêtre de sauvegarde des fichiers.

OpenPictureDialog Affiche une fenêtre d’ouverture des fichiers mais affiche


seulement les fichiers graphiques et ajoute une fenêtre
de pré-visualisation.
SavePictureDialog Affiche la fenêtre de sauvegarde des fichiers et ajoute
une fenêtre de pré-visualisation.
FontDialog Affiche une fenêtre de choix de la police de caractères.

ColorDialog Affiche un choix de couleurs.

PrintDialog Affiche une fenêtre d’impression des documents.

PrinterSetupDialog Affiche une fenêtre de configuration d’imprimante.

FindDialog Affiche une fenêtre de recherche de chaînes de


caractères.
ReplaceDialog Affiche une fenêtre de recherche et de remplacement de
chaînes de caractères.
L’INSPECTEUR D’OBJETS
La fenêtre de l’inspecteur d’objet donne un aperçu détaillé aux propriétés des objets qui
sont sur les fiches. Cette fenêtre est une connexion entre l’aspect visible des panoramas
d’écran d’une application et la programmation s’y rattachant.

Cette fenêtre permet de modifier les propriétés (attributs) des objets ou des composantes
étant sur les fiches.

Les différentes composantes peuvent être sélectionnées en cliquant dans le menu


déroulant (« drop down selector ») contenant le nom de l’objet.

La fenêtre de l’inspecteur d’objets contient deux onglets :

Properties :

Affiche une liste de propriétés disponible et leurs valeurs associées. Cette liste
change dynamiquement pour chacune des composantes graphiques
sélectionnées.

Events (messages):

L’onglet « Events » comme l’onglet « Properties » est lié dynamiquement à l’objet


inspecté. Cette liste contient la liste des différents événements (messages)
pouvant interagir avec l’objet sélectionné.
PROCESSUS DE COMPILATION

Le processus de compliation de l’environnement IDE du logiciel C++ builder fonctionne


de deux façons :

Mode automatique par commande de menu géré par l’environnement IDE :


Une application C++ builder est un ensemble de panoramas d’écrans. Chacun de
ces panoramas est supporté par du code.

Le fait de lancer une compilation peut seulement compiler le module courant


(« unit »).

Pour vous assurer de compiler votre projet au complet, vous pouvez utiliser la
commande « Add to Project ». Ces commandes font la gestion automatique des
fichiers de projets « .BPR ».
EXEMPLE DE FICHIER « .BPR »
# ---------------------------------------------------------------------------
VERSION = BCB.04.04
# ---------------------------------------------------------------------------
!ifndef BCB
BCB = $(MAKEDIR)\..
!endif
# ---------------------------------------------------------------------------
PROJECT = earthpng.exe
OBJFILES = earthpng.obj EP.obj
RESFILES = EarthPng.res
RESDEPEN = $(RESFILES) EP.dfm
LIBFILES =
IDLGENFILES =
IDLFILES =
LIBRARIES = VCL40.lib
SPARELIBS = VCL40.lib
DEFFILE =
PACKAGES =
PATHASM = .;
PATHCPP = .;
PATHPAS = .;
PATHRC = .;
DEBUGLIBPATH = $(BCB)\lib\debug
RELEASELIBPATH = $(BCB)\lib\release
SYSDEFINES = NO_STRICT
USERDEFINES =
# ---------------------------------------------------------------------------
CFLAG1 = -I$(BCB)\include;$(BCB)\include\vcl -Od -Hc -H=$(BCB)\lib\vcl40.csm -w -Ve -r- \
-k -y -v -vi- -D$(SYSDEFINES);$(USERDEFINES) -c -b- -w-par -w-inl -Vx -tWM
CFLAG2 =
CFLAG3 =
IDLCFLAGS = -src_suffixcpp -I$(BCB)\include -I$(BCB)\include\vcl
PFLAGS = -U$(BCB)\lib;$(BCB)\lib\obj;$(DEBUGLIBPATH) -I$(BCB)\include;$(BCB)\include\vcl \
-v -JPHNV -M
RFLAGS = -i$(BCB)\include;$(BCB)\include\vcl
AFLAGS = /i$(BCB)\include /i$(BCB)\include\vcl /mx /w2 /zd
LFLAGS = -L$(BCB)\lib;$(BCB)\lib\obj;$(DEBUGLIBPATH) -aa -Tpe -x -v
IFLAGS =

!if !$d(BCC32)
BCC32 = bcc32
!endif

!if !$d(DCC32)
DCC32 = dcc32
!endif

!if !$d(LINKER)
LINKER = ilink32
!endif

!if !$d(BRCC32)
BRCC32 = brcc32
!endif

!if !$d(IDL2CPP)
IDL2CPP = idl2cpp
!endif
# ---------------------------------------------------------------------------
ALLOBJ = c0w32.obj sysinit.obj $(OBJFILES)
ALLRES = $(RESFILES)
ALLLIB = $(LIBFILES) $(LIBRARIES) import32.lib cp32mt.lib
# ---------------------------------------------------------------------------
.autodepend

!if $d(IDEOPTIONS)
!endif

!ifdef IDEOPTIONS

[Version Info]
IncludeVerInfo=0
AutoIncBuild=0
MajorVer=1
MinorVer=0
Release=0
Build=0
Debug=0
PreRelease=0
Special=0
Private=0
DLL=0
Locale=1033
CodePage=1252

[Excluded Packages]
d:\PRESTO\Bin\dclmid35.bpl=Borland Midas Components

[HistoryLists\hlIncludePath]
Count=1
Item0=$(BCB)\include;$(BCB)\include\vcl

[HistoryLists\hlLibraryPath]
Count=1
Item0=$(BCB)\lib;$(BCB)\lib\obj

[Debugging]
DebugSourceDirs=

[Parameters]
RunParams=
HostApplication=
RemoteHost=
RemotePath=
RemoteDebug=0

[Compiler]
InMemoryExe=0
ShowInfoMsgs=0

[CORBA]
AddServerUnit=1
AddClientUnit=1
PrecompiledHeaders=1

!endif

$(PROJECT): $(IDLGENFILES) $(OBJFILES) $(RESDEPEN) $(DEFFILE)


$(BCB)\BIN\$(LINKER) @&&!
$(LFLAGS) +
$(ALLOBJ), +
$(PROJECT),, +
$(ALLLIB), +
$(DEFFILE), +
$(ALLRES)
!
.pas.hpp:
$(BCB)\BIN\$(DCC32) $(PFLAGS) { $** }

.pas.obj:
$(BCB)\BIN\$(DCC32) $(PFLAGS) { $** }

.cpp.obj:
$(BCB)\BIN\$(BCC32) $(CFLAG1) $(CFLAG2) -o$* $*

.c.obj:
$(BCB)\BIN\$(BCC32) $(CFLAG1) $(CFLAG2) -o$* $**

.rc.res:
$(BCB)\BIN\$(BRCC32) $(RFLAGS) $<
# ---------------------------------------------------------------------------
Pour compiler le panorama d’écran courant, il faut choisir la commande « Compile
Unit ».

Pour compiler le projet au complet, il faut exécuter la commande « Build Project » ».


Le programme est ainsi compilé et prêt à être exécuté.

Mode de commande au système d’exploitation (« shell »)

bcc32 [options] fichier_source [fichier_source2 … fichier_sourceN]

Cette commande génère des programmes exécutables (.EXE) et fait


automatiquement l’édition des liens.

Exemples :

Compilation d’un seul fichier

bcc32 prog1.cpp

Compilation de plusieurs fichiers

bcc32 prog1.cpp module1.cpp module2.cpp

Le module exécutable, dans le cas précédent, s’appellera prog1.exe

GESTIONNAIRE DE PROJETS
Le gestionnaire de projet permet de savoir exactement tous les fichiers et panoramas
d’écrans qui constituent un projet donné en plus de leurs chemins respectifs (« path »). Ce
gestionnaire de projet combiné à l’environnement IDE facilite la tâche des programmeurs
en effectuant la compilation automatiquement.

Tel que mentionné, la compilation peut également se faire manuellement en écrivant une
commande en mode ligne du système d’exploitation.

Le progiciel effectue automatiquement l’ajout et l’enlèvement d’items du projet par les


commandes suivantes :

EXÉCUTION ET DÉVERMINAGE(UTILISATION DU DÉBUGGER)


Pour exécuter un programme, il faut exécuter la commande « Run » ou presser sur la
touche « F9 ».

L’exécution pas à pas se fait avec la commande « F7 ».

Le dévermineur de l’environnement C++ builder est très complet et permet de visualiser


des paramètres de pile, des variables et des points d’arrêt.
Pour des applications de bas niveau, le dévermineur vous laisse même voir ce code
assembleur généré, le contenu de la mémoire et le contenu des différents registres.
COURS 02

INF-11299 – PROGRAMMATION II

INTRODUCTION À LA PROGRAMMATION ORIENTÉE OBJET(POO)


Référence : Schildt, Herbert & Guntle, Greg, Borland C++ Builder, The complete reference, Osborne/McGraw-
Hill, Berkeley, 2001, 977pp, ISBN: 0-07-212778-3.

La compagnie Borland a développé une version du langage C++ qui a été nécessaire pour
supporter l’environnement graphique de C++ Builder.

La programmation orientée objet est une méthodologie de conception et de développement de


programmes fortement axée sur la modularité des programmes et la réutilisation du code. La
base de la programmation orientée objet repose sur une structure de données appelée objet. La
notion d’objet est une notion pouvant être reliée à certaines choses et réalités de la vie courante
ayant les deux caractéristiques fondamentales suivantes pouvant les décrire :

¾ leur apparence; et

¾ les choses qu’ils peuvent faire.

Les objets peuvent être :

¾ des entités externes (ex. : autres systèmes, périphériques, individus, etc.) produisant ou
consommant de l’information étant utilisée par des systèmes informatiques,

¾ des choses (ex. : rapports, panoramas d’écran, lettres, signaux, etc.) du domaine de
l’information du système,

¾ des occurrences ou événements (ex. : mouvements d’un robot, etc.) se produisant lors du
contexte de l’opération du système,

¾ des rôles (ex. : gestionnaire, ingénieur, vendeur, etc.) définissant les fonctions des
individus interagissant avec le système,

¾ des unités organisationnelles (ex. : division, groupe, département, équipes, etc.)


interagissant avec le système d’information,

¾ des endroits (ex. : plancher, quai d’embarquement, etc.) nécessaires aux fonctionnalités
du système; et

¾ des structures (ex. : capteurs, véhicules, ordinateurs, etc.) définissant des classes
d’objets.
L’encapsulation résulte du confinement (packaging) des caractéristiques et des comportements
dans une simple structure de données appelée objet. Cette structure de données protège
l’intégrité du code, des fonctions et sous-programmes par la restriction d’accès et accentue la
programmation structurée.
Un objet est une structure de données englobant du code et des données en un seul item. La
description technique d’un objet définit l’objet comme étant une instance d’une classe. Une
classe ou type de classe est une structure composée de champs, de méthodes et de propriétés.
C’est le modèle par lequel l’objet est construit ou instancié. Les champs, les méthodes et les
propriétés d’une classe sont appelés ses composantes ou ses membres.
La structure de l’objet comprend :

¾ les champs ou attributs: variables faisant partie de l’objet. Comme les champs d’une
variable d’enregistrement, les champs d’une classe représentent des données qui
existent dans chaque instance de la classe. Plusieurs instances de même type de
champs peuvent exister en mémoire lorsque plusieurs instances de l’objet sont créées.
Les champs peuvent être accédés directement par le programme.

¾ les méthodes sont des fonctions ou des procédures faisant partie d’une définition de
classes. Les méthodes décrivent les fonctions ou les comportements effectués par l’objet
et peuvent seulement être accédées que par l’objet lui-même. Ceci fait en sorte que les
méthodes traitent les bonnes données parce que les méthodes et les données sont
encapsulées dans l’objet. Certaines méthodes agissent sur les types de classes eux-
mêmes(class methods)
¾ une propriété est une interface pour les données associées avec un objet (souvent
placées dans un champ). Les propriétés ont des descripteurs d’accès (access specifiers),
qui déterminent comment les données sont lues et modifiées. Pour les autres parties du
programme extérieures à l’objet, une propriété apparaît comme un champ. Les propriétés
décrivent les caractéristiques des objets et peuvent avoir des valeurs par défaut. Les
propriétés ne peuvent pas être modifiées directement par le programme. L’accès aux
propriétés doit se faire par l’entremise de fonctions spécifiques (méthodes) avant que
l’objet effectue les changements.

Analogie avec des composantes d’ordinateur :

Tout le monde utilise le concept d’objet car il peut être inclus ou représenté dans la vie de tous
les jours.

Prenons le cas de la carte vidéo d’un ordinateur, nous pouvons tirer les avantages suivants de la
conception modulaire d’un ordinateur :

¾ advenant un bris de la carte vidéo, l’utilisateur doit seulement changer celle-ci et non
l’ordinateur au complet,
¾ l’utilisateur moyen sait seulement que la carte vidéo est une composante de l’ordinateur
sans en connaître son fonctionnement interne,

¾ la carte vidéo est l’un des objets composant l’ordinateur et nous avons seulement à
connaître le mode de configuration (attributs) et les principales fonctions (méthodes) de la
carte vidéo,

¾ les composantes sont ré-utilisables. Vous pouvez placer une nouvelle carte vidéo dans
votre ordinateur et l’ancienne pourra également se trouver dans un autre ordinateur,

¾ le montant d’expertise est minimal. Vous pouvez assembler et faire fonctionner beaucoup
de composantes sans nécessairement connaître le fonctionnement interne des
composantes. Pour ce qui est du logiciel, les analystes peuvent construire, acheter ou
réutiliser des composantes logicielles; et

¾ l’entretien est simplifié par l’identification du défaut à une composante. Il s’agit alors de
remplacer celle-ci.

DÉFINITION DE CERTAINS TERMES DE LA POO

¾ Classe : une classe ou une définition de classe est la composante principale de


l’environnement orienté-objet. La classe définit les propriétés (attributs) et les méthodes
(fonctions/procédures) de tous les objets crées de cette classe. La classe définit
seulement l’architecture d’un objet et n’est pas l’objet lui-même.

¾ Objet : un objet est une entité réelle créée au temps de l’exécution d’un programme et qui
réside dans la mémoire vive de l’ordinateur. Un objet est généré par une définition de
classe qui détermine les attributs (propriétés) et les méthodes de cet objet. Certaines
valeurs initiales de propriétés spécifiques seront également définies lors de la création de
l’objet.

Une fois l’objet crée, il n’a plus aucune relation avec la classe qui a généré l’objet.

Tout changement fait à un objet lors de l’exécution n’occasionne aucun effet ou aucun
changement aux autres objets créés par cette classe.

¾ Instanciation : l’instanciation est un nom donné au processus de la création d’objets en


mémoire par une définition de classe. Ce processus crée donc une instance de la classe.

¾ Constructeur : Chaque définition de code va posséder une section de code indiquant au


système comment instancier un objet de cette classe. Ce code d’instanciation est appelé
le constructeur.

Le contructeur est appelé pour fournir de la mémoire à une instance d’un objet, initialiser
ses propriétés (attributs) et exécuter différentes routines d’initialisation requises par
l’objet.
¾ Destructeur : la définition d’une classe a également besoin d’un destructeur qui annule
l’effet du constructeur. Le destructeur est une section de code contrôlant le processus
d’effacement de l’instance d’un objet de la mémoire. Il doit également exécuter certaines
routines de terminaison nécessaire à la restitution des ressources employées par une
instance d’objets particulière. Le destructeur est donc très important pour la restitution de
ressources telles que la mémoire.

¾ Classe parente : une classe parente est la classe immédiate de laquelle les définitions
des propriétés et des méthodes sont héritées directement pour créer une classe enfant.

¾ Classe ancêtre : une classe ancêtre est une classe par laquelle les définitions des
propriétés et des méthodes sont héritées indirectement ou directement pour créer une
classe enfant.

¾ Classe descendante : toute classe ayant été définie par l’héritage d’une autre classe est
appelée une classe descendante de cet ancêtre.

¾ Sous-classification : le processus de création de classes descendantes par l’héritage


d’une classe parente.

¾ Héritage simple/Héritage Multiple : Des environnements de programmation orientée objet


peuvent permettre au programmeur de créer des définitions de classes héritant de plus
d’une classe parente. La particularité d’avoir plus d’une classe parente est définie comme
étant l’héritage multiple. L’environnement de programmation C++ supporte l’héritage
multiple.

CONCEPTS FONDAMENTAUX DE LA POO

La programmation orientée-objet est basée sur quatre principaux concepts :

I. encapsulation,

II. héritage,

III. polymorphisme; et

IV. tâches des programmeurs.

¾ Encapsulation

Le premier concept de la programmation orientée-objet est l’encapsulation. Les structures de


données (attributs) et les routines de code (méthodes) sont cloisonnées ou encapsulées en
une seule entité appelée objet.
Avant la programmation orientée-objet, il y avait des moyens de regrouper le code par la
programmation structurés sous la forme de librairies ou de « packages ». La programmation
structurée est limitée parce qu’elle ne restreint pas l’accès aux données (variables et bases
de données globales).

Le fait de ne pas avoir d’encapsulation de variables peut parfois permettre à des pointeurs et
à des variables globales de modifier le code d’autres routines.
¾ Héritage

Le second concept de la programmation orientée-objet est appelé l’héritage. L’héritage permet


au programmeur de créer des définitions de classes ou objets héritant des méthodes et des
structures de données des classes parentes.

Par l’héritage, un programmeur peut simplement utiliser les méthodes héritées et les propriétés
des classes parentes sans avoir à les recréer. Le programmeur peut seulement programmer les
méthodes différentes entre les objets parents et enfants. Ce concept est appelé la
programmation par exception.

Le temps de programmation est optimisé par la concentration des efforts à la création des
propriétés et des méthodes uniques à cet objet.

L’héritage permet d’écrire moins de code et la programmation modulaire facilite l’entretien des
programmes.
¾ Polymorphisme

Le polymorphisme est le troisième concept de la programmation orientée-objet. Tous les


langages de programmation donnent aux programmeurs la possibilité de créer un ensemble de
fonctions disponibles publiquement (méthodes publiques) pour chaque objet.

Le polymorphisme permet au programmeur l’utilisation d’un même nom de méthode pour


effectuer différentes opérations basées sur le type d’objets référencés par la méthode.
Le polymorphisme est également utilisé lors de la surcharge d’opérateurs :

Code Résultats Opération effectuée

″Bon″+ ″soir″ ″Bonsoir″


5+7 12 Adition mathématique
Concaténation
1/25/95 + 14 2/8/95 Opérations sur les dates

¾ Tâches des programmeurs

Le développement des applications orientées objet nécessite deux tâches spécifiques de


programmation :

1. production de nouveaux objets; et

2. utilisation des objets pour créer des applications

Chaque tâche nécéssite des talents particuliers et différentes habiletés de


programmation. Généralement, les développeurs travaillant au développement des
classes d’objets ne seront pas les mêmes qui vont utiliser ces classes pour créer des
applications.

La division des tâches permet des augmentations de productivité et de plusieurs


avantages accessibles à beaucoup de sortes de programmeurs.
Le code des spécialiste et des ingénieurs peut ainsi être utilisé par des programmeurs
d’applications simplement par les connaissances des opérations effectuées par ces
modules.

Classes versus objets.

Il faut comprendre la différence entre les classes et les objets. Bien que très reliés, le code placé
dans une classe va avoir différents effets que le code opérant dans les routines à l’exécution.

Utilisation Processus de création Méthodologie de


programmation
Classe Plan de conception Sous-classification Programmation par
exceptions
Objet Opération à l’exécution Instantiation Programmation par
propriétés

Ces points sont importants à retenir lorsque l’on parle de la programmation orientée-objet :

¾ Le but ultime de la programmation orientée-objet est de créer des composantes logicielles


réutilisables.

¾ En faisant abstraction de l’environnement de programmation, les caractéristiques de la


programmation orientée objet sont l’encapsulation, l’héritage et le polymorphisme.

¾ L’encapsulation fournit un contrôle d’accès aux variables et au code.

¾ La combinaison de l’héritage et du polymorphisme augmente la productivité des


programmeurs.
L’encapsulation standard de l’environnement C++ Builder

//---------------------------------------------------------------------------
#ifndef Ex1H
#define Ex1H
//---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ExtCtrls.hpp>
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published: // IDE-managed Components
// Déclarations publiées - visible par toutes les classes
// Ces classes sont gérées par l'inspecteur d'objet de Builder

TPanel *Panel1;
TButton *Button2;
TButton *Button3;
TButton *Button4;
void __fastcall Button1Click(TObject *Sender);
void __fastcall Button4Click(TObject *Sender);
void __fastcall FormCreate(TObject *Sender);
void __fastcall Button2Click(TObject *Sender);
void __fastcall Button3Click(TObject *Sender);
private: // User declarations
// déclarations privées qui appartiennent seulement à cette classe
TButton * tuile[15];
int position[15]; // position originale des tuiles
int vide; // position de la tuile vide

protected:
//déclarations protégées - visible seulement par cette
// classe et les classes descendantes
public: // User declarations
//déclarations publiques - accessibles à toutes les classes
__fastcall TForm1(TComponent* Owner);
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif
Exemple d’encapsulation

#include <iostream>
using namespace std;

// This creates the class queue.


class queue {
int q[100];
int sloc, rloc;
public:
queue(); // constructor
~queue(); // destructor
void qput(int i);
int qget();
};

// This is the constructor function.


queue::queue()
{
sloc = rloc = 0;
cout << "Queue initialized.\n";
}

// This is the destructor function.


queue::~queue()
{
cout << "Queue destroyed.\n";
}

void queue::qput(int i)
{
if(sloc==99) {
cout << "Queue is full.\n";
return;
}
sloc++;
q[sloc] = i;
}

int queue::qget()
{
if(rloc == sloc) {
cout << "Queue underflow.\n";
return 0;
}
rloc++;
return q[rloc];
}
int main()
{
queue a, b; // create two queue objects

a.qput(10);
b.qput(19);

a.qput(20);
b.qput(1);

cout << a.qget() << " ";


cout << a.qget() << " ";
cout << b.qget() << " ";
cout << b.qget() << "\n";

return 0;
}
Exemple de polymorphisme

#include <iostream>
using namespace std;

// sqr_it is overloaded three ways


int sqr_it(int i);
double sqr_it(double d);
long sqr_it(long l);

int main()
{
cout << sqr_it(10) << "\n";
cout << sqr_it(11.0) << "\n";
cout << sqr_it(9L) << "\n";

return 0;
}

// Define sqr_it for ints.


int sqr_it(int i)
{
cout << "Inside the sqr_it() function that uses ";
cout << "an integer argument.\n";

return i*i;
}

// Overload sqr_it for doubles.


double sqr_it(double d)
{
cout << "Inside the sqr_it() function that uses ";
cout << "a double argument.\n";

return d*d;
}

// Overload sqr_it again, this time for longs.


long sqr_it(long l)
{
cout << "Inside the sqr_it() function that uses ";
cout << "a long argument.\n";

return l*l;
}
COURS 03

INF-11299 – PROGRAMMATION II

PROGRAMMATION C++
Référence : Schildt, Herbert & Guntle, Greg, Borland C++ Builder, The complete reference,
Osborne/McGraw-Hill, Berkeley, 2001, 977pp, ISBN: 0-07-212778-3.

¾ Modules(Units)

Une application ou un programme dans l’environnement C++ Builder est défini comme étant
un projet. Le projet possède son propre fichier de définition et possède l’extension « .BPR ».

Le fichier de projet contient des instructions au compilateur et à l’éditeur de liens. Voici un


exemple de contenu du fichier « Project2.BPR » :
# ---------------------------------------------------------------------------
!if !$d(BCB)
BCB = $(MAKEDIR)\..
!endif

# ---------------------------------------------------------------------------
# IDE SECTION
# ---------------------------------------------------------------------------
# The following section of the project makefile is managed by the BCB IDE.
# It is recommended to use the IDE to change any of the values in this
# section.
# ---------------------------------------------------------------------------

VERSION = BCB.04.04
# ---------------------------------------------------------------------------
PROJECT = Project2.exe
OBJFILES = Project2.obj Ex1.obj
RESFILES = Project2.res
RESDEPEN = $(RESFILES) Ex1.dfm
LIBFILES =
LIBRARIES =
SPARELIBS = VCL40.lib
PACKAGES = VCL40.bpi VCLX40.bpi VCLJPG40.bpi bcbsmp40.bpi QRPT40.bpi VCLDB40.bpi \
ibsmp40.bpi VCLDBX40.bpi TEEUI40.bpi TEEDB40.bpi TEE40.bpi nmfast40.bpi \
dclocx40.bpi
DEFFILE =
# ---------------------------------------------------------------------------
PATHCPP = .;
PATHPAS = .;
PATHASM = .;
PATHRC = .;
DEBUGLIBPATH = $(BCB)\lib\debug
RELEASELIBPATH = $(BCB)\lib\release
USERDEFINES =
SYSDEFINES = _RTLDLL;NO_STRICT;USEPACKAGES
# ---------------------------------------------------------------------------
CFLAG1 = -I$(BCB)\include;$(BCB)\include\vcl -Od -Hc -H=$(BCB)\lib\vcl40.csm -w -Ve -r- \
-a8 -k -y -v -vi- -c -b- -w-par -w-inl -Vx -tW -tWM \
-D$(SYSDEFINES);$(USERDEFINES)
PFLAGS = -U$(BCB)\lib\obj;$(BCB)\lib;$(RELEASELIBPATH) \
-I$(BCB)\include;$(BCB)\include\vcl -$YD -$W -$O- -v -JPHNE -M
RFLAGS = -i$(BCB)\include;$(BCB)\include\vcl
AFLAGS = /i$(BCB)\include /i$(BCB)\include\vcl /mx /w2 /zd
LFLAGS = -L$(BCB)\lib\obj;$(BCB)\lib;$(RELEASELIBPATH) -aa -Tpe -x -Gn -v
# ---------------------------------------------------------------------------
ALLOBJ = c0w32.obj Memmgr.Lib $(PACKAGES) sysinit.obj $(OBJFILES)
ALLRES = $(RESFILES)
ALLLIB = $(LIBFILES) $(LIBRARIES) import32.lib cp32mti.lib
# ---------------------------------------------------------------------------
!ifdef IDEOPTIONS

[Version Info]
IncludeVerInfo=0
AutoIncBuild=0
MajorVer=1
MinorVer=0
Release=0
Build=0
Debug=0
PreRelease=0
Special=0
Private=0
DLL=0
Locale=1036
CodePage=1252

[Version Info Keys]


CompanyName=
FileDescription=
FileVersion=1.0.0.0
InternalName=
LegalCopyright=
LegalTrademarks=
OriginalFilename=
ProductName=
ProductVersion=1.0.0.0
Comments=

[Debugging]
DebugSourceDirs=$(BCB)\source\vcl

[Parameters]
RunParams=
HostApplication=
RemoteHost=
RemotePath=
RemoteDebug=0

[Compiler]
InMemoryExe=0
ShowInfoMsgs=0

!endif

# ---------------------------------------------------------------------------
# MAKE SECTION
# ---------------------------------------------------------------------------
# This section of the project file is not used by the BCB IDE. It is for
# the benefit of building from the command-line using the MAKE utility.
# ---------------------------------------------------------------------------
.autodepend
# ---------------------------------------------------------------------------
!if !$d(BCC32)
BCC32 = bcc32
!endif

!if !$d(CPP32)
CPP32 = cpp32
!endif

!if !$d(DCC32)
DCC32 = dcc32
!endif

!if !$d(TASM32)
TASM32 = tasm32
!endif

!if !$d(LINKER)
LINKER = ilink32
!endif

!if !$d(BRCC32)
BRCC32 = brcc32
!endif

# ---------------------------------------------------------------------------
!if $d(PATHCPP)
.PATH.CPP = $(PATHCPP)
.PATH.C = $(PATHCPP)
!endif

!if $d(PATHPAS)
.PATH.PAS = $(PATHPAS)
!endif

!if $d(PATHASM)
.PATH.ASM = $(PATHASM)
!endif

!if $d(PATHRC)
.PATH.RC = $(PATHRC)
!endif
# ---------------------------------------------------------------------------
$(PROJECT): $(OBJFILES) $(RESDEPEN) $(DEFFILE)
$(BCB)\BIN\$(LINKER) @&&!
$(LFLAGS) +
$(ALLOBJ), +
$(PROJECT),, +
$(ALLLIB), +
$(DEFFILE), +
$(ALLRES)
!
# ---------------------------------------------------------------------------
.pas.hpp:
$(BCB)\BIN\$(DCC32) $(PFLAGS) {$< }

.pas.obj:
$(BCB)\BIN\$(DCC32) $(PFLAGS) {$< }

.cpp.obj:
$(BCB)\BIN\$(BCC32) $(CFLAG1) -n$(@D) {$< }

.c.obj:
$(BCB)\BIN\$(BCC32) $(CFLAG1) -n$(@D) {$< }

.c.i:
$(BCB)\BIN\$(CPP32) $(CFLAG1) -n. {$< }

.cpp.i:
$(BCB)\BIN\$(CPP32) $(CFLAG1) -n. {$< }

.asm.obj:
$(BCB)\BIN\$(TASM32) $(AFLAGS) $<, $@

.rc.res:
$(BCB)\BIN\$(BRCC32) $(RFLAGS) -fo$@ $<
# ---------------------------------------------------------------------------

Afin d’éditer un programme/projet/application dans l’environnement C++ Builder, il faut


exécuter la commande « Open Project » ou « CTRL-F11 »
Le programme/projet/application utilise une ou plusieurs fiches. Ces fiches sont créées et
initialisées automatiquement à l’exécution du fichier « .CPP » associé au projet. Dans le cas
qui nous intéresse, voici un exemple de contenu du fichier « Project2.CPP » :
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
USERES("Project2.res");
USEFORM("Ex1.cpp", Form1);
//---------------------------------------------------------------------------
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
//---------------------------------------------------------------------------

Dans le cas présent, cette application n’a qu’un seul panorama d’écran intitulé « FORM1 ».
Un autre cas serait l’application « TP3 » possédant deux panoramas d’écran : « FORM1 » et
« FORM2 ».
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
USERES("TP3.res");
USEFORM("Unit1.cpp", Form1);
USEFORM("Unit2.cpp", Form2);
//---------------------------------------------------------------------------
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->CreateForm(__classid(TForm2), &Form2);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
//---------------------------------------------------------------------------
Chaque fiche contient deux éléments :

1. Un fichier « .CPP » intitulé « UNIT » contenant du code C++ représentant les


méthodes utilisées dans le panorama d’écran ou la « Fiche ».
Voici un exemple de contenu de ce fichier. Nous avons ici le fichier « Ex1.CPP » :
//---------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <vcl.h>
#pragma hdrstop

#include "Ex1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)


{
//
// un bouton est cliqué.
TButton* b = (TButton*) Sender; // obligatoirement
int ct = (b->Left-2) / 52;
int rt = (b->Top-2) / 52;
int cv = vide % 4;
int rv = vide / 4;
if (abs(rt-rv) + abs(ct-cv) == 1)
{
b->Left = b->Left + (cv-ct)*52;
b->Top = b->Top + (rv-rt) * 52;
vide = rt*4 + ct;
}
}
//---------------------------------------------------------------------------

void __fastcall TForm1::Button4Click(TObject *Sender)


{
Application->Terminate();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
// appelé à la création de la fiche. C'est l'endroit idéal pour créer et
// initialiser toutes les variables (tuile, vide, position)
// 1 - créer et initialiser les 15 tuiles et leur positions
for (int i = 0; i < 15; i++)
{
tuile[i] = new TButton(Panel1);
tuile[i]->Parent = Panel1;
tuile[i]->Width = 50; tuile[i]->Height = 50;
tuile[i]->Left = (i % 4) * 52 + 2;
tuile[i]->Top = (i / 4) * 52 + 2;
tuile[i]->Caption = IntToStr(i+1);
tuile[i]->Font->Size = 18;
tuile[i]->Font->Style << fsBold;
tuile[i]->OnClick = Button1Click; // le nom de la fonction où vous
// avez inséré deux / (étape 7)
position[i] = i;
}
vide = 15;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
// brasser les positions
for (int i = 0; i < 15; i++)
{
int j = random(15);
int temp = position[i]; position[i] = position[j]; position[j] = temp;
}
Button3Click(Sender); // le nom de la fonction associée au bouton reprendre
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button3Click(TObject *Sender)
{
// Ici, il faut replacer les boutons selon le contenu du tableau position
for (int i = 0; i < 15; i++)
{
tuile[i]->Left = (position[i] % 4) * 52 + 2;
tuile[i]->Top = (position[i] / 4) * 52 + 2;
vide = 15; // on suppose !
}
}
//---------------------------------------------------------------------------

2. Un fichier « .H » ou « .HPP » contenant la description des classes et des


méthodes utilisées dans le panorama d’écran ou la « Fiche ». Pour accéder à ce
fichier, il faut faire la commande « CTRL-F6 » ou de cliquer à droite sur l’onglet du
fichier « *.CPP » et de sélectionner l’option « Open Source/Header File » :
Voici un exemple de contenu de ce fichier. Nous avons ici le fichier « Ex1.H » contenant la
définition des classes et le prototypage des méthodes utilisées dans cette fiche :
//---------------------------------------------------------------------------
#ifndef Ex1H
#define Ex1H
//---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ExtCtrls.hpp>
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published: // IDE-managed Components
// Déclarations publiées - visible par toutes les classes
// Ces classes sont gérées par l'inspecteur d'objet de Builder

TPanel *Panel1;
TButton *Button2;
TButton *Button3;
TButton *Button4;
void __fastcall Button1Click(TObject *Sender);
void __fastcall Button4Click(TObject *Sender);
void __fastcall FormCreate(TObject *Sender);
void __fastcall Button2Click(TObject *Sender);
void __fastcall Button3Click(TObject *Sender);
private: // User declarations
// déclarations privées qui appartiennent seulement à cette classe
TButton * tuile[15];
int position[15]; // position originale des tuiles
int vide; // position de la tuile vide

protected:
//déclarations protégées - visible seulement par cette
// classe et les classes descendantes
public: // User declarations
//déclarations publiques - accessibles à toutes les classes
__fastcall TForm1(TComponent* Owner);
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif

Sachant qu’un programme/projet/application peut utiliser une ou plusieurs fiches,


l’environnement C++ Buider comprend des éléments de navigation permettant de se déplacer
ou de repérer certaines fiches ou certains éléments de code.
Voici une brève description de ces commandes (boutons) de l’environnement C++ Builder :

Commande Action
Visualiser la liste de tous les fichiers « .CPP » ou « units ». Une fois la
liste affichée, l’utilisateur peut cliquer sur le nom d’un fichier affiché
pour éditer ce fichier.
Visualiser la liste de tous les fiches « Forms ». Une fois la liste
affichée, l’utilisateur peut cliquer sur le nom d’une fiche pour y
accéder.
Passer d’une fiche à son code correspondant ou du code d’un module
« Unit/.CPP » à sa fiche correspondante.
Créer une nouvelle fiche.

¾ Enregistrements(struct)

Le langage C permet à l’utilisateur de créer des types de données de différentes façons. Nous
allons ici étudier deux manières de créer de nouveaux types de données autres que ceux natifs
du langage C :

1. le type enregistrement « structure » qui est un regroupement de variables de type


standard du C sous un même nom ; et

2. le mot réservé « typedef » pouvant définir plusieurs variables d’un même type avec des
nouveaux noms.

Une structure est un regroupement de variables référencées sous un même nom, fournissant
un moyen pratique de regrouper des informations logiquement reliées.

Une déclaration de structure fournit un gabarit servant à créer des objets structurés. Les
variables composant la structure sont appelées membres, éléments ou champs de la
structure.
struct addr {
char name[30] ;
char street[40] ;
char city[20];
char state[3];
char zip[11];
int customer_num;
} ;

Dans le cas suivant, « addr » identifie cette structure de données particulière et en est son
spécificateur de type. Le nom de la structure est également appelé le « tag ». Aucune
variable n’est encore déclarée. Pour déclarer une variable de cette structure il faut faire
l’opération suivante :

struct addr addr_info;

Ceci déclare une variable de type addr appelée addr_info :

Pour déclarer plusieurs variables de ce type nous pouvons écrire la définition suivante :

struct addr {
char name[30] ;
char street[40] ;
char city[20];
char state[3];
char zip[11];
int customer_num;
} addr_info, binfo, cinfo;

Ceci déclare des variables de type addr appelées addr_info, binfo et cinfo.

La structure n’a pas besoin d’être nommée si nous n’avons à déclarer qu’une variable de ce
type :

struct {
char name[30] ;
char street[40] ;
char city[20];
char state[3];
char zip[11];
int customer_num;
} addr_info;
Accès aux champs de l’enregistrement:

Les champs de l’enregistrement peuvent être référencés avec l’utilisation du « . ». La forme


générale en est :

nom_de_la_structure.nom_du_champ

Exemple :

addr_info.customer_num = 88 ;

L’affectation des enregistrements.

L’information contenue dans une structure peut être affectée à une autre structure du même
type en utilisant l’instruction d’affectation simple. L’affectation de la structure par la copie de
chacun de ses champs n’est pas nécessaire. L’exemple suivant le prouve :
int main(void)
{
struct {
int a;
int b;
} x, y;

x.a = 10;
x.b = 20;

y = x; /* assign one structure to another */


printf("Contents of y: %d %d.", y.a, y.b);

return 0;
}

Vecteurs d’enregistrements :

Une des applications courantes que nous pouvons faire avec des enregistrements est de
construire des vecteurs d’enregistrements. Ces vecteurs peuvent être déclarés de la façon
suivante :

struct addr addr_info[100] ;

Ceci déclare un vecteur de 100 enregistrements de type addr.

Pour accéder au code postal du troisième client, nous pouvons faire l’opération suivante :

printf("%s", addr_info[2].zip);
Enregistrements passés en paramètres à des fonctions.

Dans le cas de variables scalaires, elles sont soit globales aux programmes ou locales aux
modules et aux fonctions. Ces variables peuvent également être passées par paramètres aux
fonctions.

Nous allons ici voir comment passer des champs ou des parties de variables structurées à
des fonctions.

Passage des champs d’enregistrements par paramètres :

Soit la structure suivante :

struct fred {
char x;
int y;
float z;
char s[10];
} mike;

Voici des exemples de passage de champs d’enregistrements par valeur :

func(mike.x); /* passes character value of x */


func2(mike.y); /* passes integer value of y */
func3(mike.z); /* passes float value of z */
func4(mike.s); /* passes address of string s */
func(mike.s[2]); /* passes character value of s[2] */

Voici des exemples de passage de champs d’enregistrements par adresse (par référence) avec
l’opérateur d’adresse « & » :

func(&mike.x); /* passes address of character x */


func2(&mike.y); /* passes address of integer y */
func3(&mike.z); /* passes address of float z */
func4(mike.s); /* passes address of string s */
func(&mike.s[2]); /* passes address of character s[2] */
Passage d’enregistrements complets en paramètres à des fonctions.
Nous avons ici un exemple de passage de structure par valeur. La structure est donc passée en
argument à la fonction et ses valeurs n’auront aucune modification :

#include <stdio.h>

/* declare a structure type */


struct struct_type {
int a, b;
char ch;
};

void f1(struct struct_type parm);

int main(void)
{
struct struct_type arg; /* declare arg */

arg.a = 1000;
f1(arg);

return 0;
}

void f1(struct struct_type parm)


{
printf("%d", parm.a);
}

Pointeurs sur des enregistrements.


Les langages C et C++ permettent les pointeurs sur les enregistrements tels que la plupart des
autres langages évolués.

Déclaration de pointeur sur un enregistrement :

struct addr *addr_pointer;

Ici, addr_pointer pointe sur une structure de type addr.


Utilisation et référencement des structures de pointeurs.
Afin d’obtenir l’adresse d’une variable d’enregistrement, il faut utiliser l’opérateur d’adresse
«&»:

struct bal {
float balance;
char name[80];
} person;

struct bal *p; /* declare a structure pointer */

Ici, p contient l’adresse de l’enregistrement personne :

p = &person;

L’accès aux champs via un pointeur d’enregistrement se fait des deux façons suivantes :

p->balance ;

ou

(*p).balance ;

L’exemple suivant utilise les notions précédentes :

/* Display a software timer. */

#include <stdio.h>
#include <conio.h>

#define DELAY 128000

struct my_time {
int hours;
int minutes;
int seconds;
};

void update(struct my_time *t), display(struct my_time *t);


void mydelay(void);
int main(void)
{
struct my_time systime;

systime.hours = 0;
systime.minutes = 0;
systime.seconds = 0;

for(;;) {
update(&systime);
display(&systime);
if(kbhit()) return 0;
}
}

void update(struct my_time *t)


{
t->seconds++;
if(t->seconds==60) {
t->seconds = 0;
t->minutes++;
}
if(t->minutes==60) {
t->minutes = 0;
t->hours++;
}
if(t->hours==24) t->hours = 0;
mydelay();
}

void display(struct my_time *t)


{
printf("%02d:", t->hours);
printf("%02d:", t->minutes);
printf("%02d\n", t->seconds);
}

void mydelay(void)
{
long int t;

for(t=1; t<DELAY; ++t) ;


}
Structures d’enregistrements ayant des vecteurs ou des structures comme champs.

Structures d’enregistrement ayant un vecteur comme champ :

struct x {
int a[10][10]; /* 10 x 10 array of ints */
float b;
} y;

Ceci nous permet de référencer l’entier de la 3ième ligne et de la 7ième colonne de la structure y :

y.a[3][7] ;

Structures d’enregistrement ayant une autre structure comme champ :


struct emp {
struct addr address;
float wage;
} worker;

Ici, la structure « addr » est contenue dans la structure « emp ». Les opérations suivantes
assignent un salaire de $65,000.00 et le code postal 98765 à la variable « worker ».

worker.wage = 65000.00;
strcpy(worker.address.zip,"98765");

Utilisation du mot réservé « typedef »

Les langages C et C++ permettent à l’utilisateur de définir de nouveaux types de données à l’aide
de l’instruction « typedef ». La forme générale de déclaration des types à l’aide de typedef est la
suivante :

typedef type nouveau_nom_du_type

où type est un type ou une structure de données existante :

Cette déclaration donne un deuxième nom au type « float »

typedef float balance;

Nous pouvons maintenant déclarer un réel de la façon suivante :

balance past_due;
L’instruction « typedef » peut également servir à définir des types structurés :

typedef struct {
float due;
int over_due;
char name[40];
} client; /* here client is the new type name */

client clist[NUM_CLIENTS]; /* define array of


structures of type client */

Utillsation de l’instruction « struct » dans la déclaration des classes

Dans le langage C++, l’instruction « struct » a des fonctions supplémentaires que celle du C
standard. Le langage C++ relie étroitement les instructions « class » et « struct ».

L’instruction struct peut servir à déclarer des classes et tous les éléments du « struct » sont
de type « public » par défaut :

#include <iostream>
using namespace std;

struct cl {
int get_i(); // these are public
void put_i(int j); // by default
private:
int i;
} ;

int cl::get_i()
{
return i;
}

void cl::put_i(int j)
{
i = j;
}

int main()
{
cl s;

s.put_i(10);
cout << s.get_i();

return 0;
}
Lorsque l’instruction « class » est utilisés, tous ses éléments sont de type « private » par défaut:

#include <iostream>
using namespace std;

class cl {
int i; // private by default
public:
int get_i();
void put_i(int j);
} ;

int cl::get_i()
{
return i;
}

void cl::put_i(int j)
{
i = j;
}

int main()
{
cl s;

s.put_i(10);
cout << s.get_i();

return 0;
}

Pointeurs

Une adresse est une valeur. On peut donc stocker cette valeur dans une variable. Les pointeurs
sont justement des variables qui contiennent l'adresse d'autres objets, par exemple l'adresse
d'une autre variable. On dit que le pointeur pointe sur la variable pointée. Ici, pointer signifie «
faire référence à ». Les adresses sont généralement des valeurs constantes, car en général un
objet ne se déplace pas en mémoire. Toutefois, la valeur d'un pointeur peut changer. Cela ne
signifie pas que la variable pointée est déplacée en mémoire, mais plutôt que le pointeur pointe
sur autre chose.

Afin de savoir ce qui est pointé par un pointeur, les pointeurs disposent d'un type. Ce type est
construit à partir du type de l'objet pointé. Cela permet au compilateur de vérifier que les
manipulations réalisées en mémoire par l'intermédiaire du pointeur sont valides.

Le type des pointeur se lit « pointeur de ... », où les points de suspension représentent le nom du
type de l'objet pointé.
Les pointeurs se déclarent en donnant le type de l'objet qu'ils devront pointer, suivi de leur
identificateur précédé d'une étoile :

int *pe; // pe est un pointeur d'entier.

Note : Si plusieurs pointeurs doivent être déclarés, l'étoile doit être répétée :

int *pe1, *pe2, i, j, *pe3;

Ici, pe1, pe2 et pe3 sont des pointeurs d'entiers et i, j sont des entiers.
Avec l’image précédente, nous pouvons constater que :

int *pe1 ;
int i, j ;

L’opérateur « & » est un opérateur unaire retournant l’adresse mémoire de son opérande:

45 = i ;
23444 = &i ;

461 = j ;
23443 = &j ;

L’opérateur « * » est un opérateur unaire retournant la valeur ou le contenu de la variable étant à


cette adresse:

23443 = pe1 ;
461 = *pe1 ;

Ces deux opérateurs peuvent être utilisés simultanément :

pe1 = &i ;

alors

23444 = pe1 ; et
45 = *pe1 ;

L’opérateur « const » dans le cas des pointeurs indique au compilateur que la valeur de ce
pointeur reste inchangée lors d’un appel de fonction :

#include <stdio.h>

void sp_to_dash(const char *str);

int main(void)
{
sp_to_dash("this is a test");
return 0;
}

void sp_to_dash(const char *str)


{
while(*str) {
if(*str == ' ') printf("%c", '-');
else printf("%c", *str);
str++;
}
}
En C++, le nom d’un vecteur ou d’une variable non scalaire (structurée) est considéré comme
étant l’adresse du vecteur ou de la structure qui pointe à la première occurrence de cette
structure. Considérons l’exemple suivant :

char ch_car[80], *p1;

qui est la déclaration d’une chaîne de caractères et d’un pointeur sur une chaîne de caractères.
Soit :

p1 = ch_car ;

p1 pointe maintenant sur la chaine de caractères. Si nous voulons accéder le cinquième élément
du vecteur, nous pouvons faire les opérations suivantes :

str[4] ou *(p1 + 4) ;

Il est possible de faire un pointeur sur une structure dans une structure en indiquant le nom de la
structure comme type du pointeur:

typedef struct nom


{
struct nom *pointeur; /* Pointeur sur une structure "nom". */
...
} MaStructure;

Ce type de construction permet de créer des listes de structures, dans lesquelles chaque
structure contient l'adresse de la structure suivante dans la liste. Nous verrons plus loin un
exemple d'utilisation de ce genre de structure.

Il est également possible de créer des pointeurs sur des fonctions, et d'utiliser ces pointeurs pour
paramétrer un algorithme, dont le comportement dépendra des fonctions ainsi pointées. Nous
détaillerons plus loin ce type d'utilisation des pointeurs.

Récursivité

Une fonction récursive est une fonction qui s’appelle d’elle-même jusqu’à la rencontre d’une
condition d’arrêt. Ce type de fonction peut servir à implanter certains types d’algorithmes
s’éxécutant jusqu’à une ou plusieurs conditions d’arrêt. Il est à noter que ces algorithmes peuvent
également être implantés avec des programmes itératifs à l’aide de boucles conventionnelles.

Une fonction récursive possède généralement un code plus compact qu’une fonction itérative. La
fonction récursive modèle plus intégralement la définition l’algorithme et la pile gère les itérations
au lieu du programmeur.
Voici une fonction récursive calculant la factorielle d’un nombre en C++ :

/* Compute the factorial of a number. */


int factr(int n) /* recursive */
{
int answer;

if(n==1) return(1);
answer = factr(n-1)*n;
return(answer);
}

Voici une fonction itérative calculant la factorielle d’un nombre en C++ :

/* Compute the factorial of a number. */


int fact(int n) /* non-recursive */
{
int t, answer;

answer = 1;
for(t=1; t<=n; t++)
answer=answer*(t);
return(answer);
}
COURS 03

INF-11299 – PROGRAMMATION II

PROGRAMMATION C++
Référence : Schildt, Herbert & Guntle, Greg, Borland C++ Builder, The complete reference,
Osborne/McGraw-Hill, Berkeley, 2001, 977pp, ISBN: 0-07-212778-3.

¾ Modules(Units)

Une application ou un programme dans l’environnement C++ Builder est défini comme étant
un projet. Le projet possède son propre fichier de définition et possède l’extension « .BPR ».

Le fichier de projet contient des instructions au compilateur et à l’éditeur de liens. Voici un


exemple de contenu du fichier « Project2.BPR » :
# ---------------------------------------------------------------------------
!if !$d(BCB)
BCB = $(MAKEDIR)\..
!endif

# ---------------------------------------------------------------------------
# IDE SECTION
# ---------------------------------------------------------------------------
# The following section of the project makefile is managed by the BCB IDE.
# It is recommended to use the IDE to change any of the values in this
# section.
# ---------------------------------------------------------------------------

VERSION = BCB.04.04
# ---------------------------------------------------------------------------
PROJECT = Project2.exe
OBJFILES = Project2.obj Ex1.obj
RESFILES = Project2.res
RESDEPEN = $(RESFILES) Ex1.dfm
LIBFILES =
LIBRARIES =
SPARELIBS = VCL40.lib
PACKAGES = VCL40.bpi VCLX40.bpi VCLJPG40.bpi bcbsmp40.bpi QRPT40.bpi VCLDB40.bpi \
ibsmp40.bpi VCLDBX40.bpi TEEUI40.bpi TEEDB40.bpi TEE40.bpi nmfast40.bpi \
dclocx40.bpi
DEFFILE =
# ---------------------------------------------------------------------------
PATHCPP = .;
PATHPAS = .;
PATHASM = .;
PATHRC = .;
DEBUGLIBPATH = $(BCB)\lib\debug
RELEASELIBPATH = $(BCB)\lib\release
USERDEFINES =
SYSDEFINES = _RTLDLL;NO_STRICT;USEPACKAGES
# ---------------------------------------------------------------------------
CFLAG1 = -I$(BCB)\include;$(BCB)\include\vcl -Od -Hc -H=$(BCB)\lib\vcl40.csm -w -Ve -r- \
-a8 -k -y -v -vi- -c -b- -w-par -w-inl -Vx -tW -tWM \
-D$(SYSDEFINES);$(USERDEFINES)
PFLAGS = -U$(BCB)\lib\obj;$(BCB)\lib;$(RELEASELIBPATH) \
-I$(BCB)\include;$(BCB)\include\vcl -$YD -$W -$O- -v -JPHNE -M
RFLAGS = -i$(BCB)\include;$(BCB)\include\vcl
AFLAGS = /i$(BCB)\include /i$(BCB)\include\vcl /mx /w2 /zd
LFLAGS = -L$(BCB)\lib\obj;$(BCB)\lib;$(RELEASELIBPATH) -aa -Tpe -x -Gn -v
# ---------------------------------------------------------------------------
ALLOBJ = c0w32.obj Memmgr.Lib $(PACKAGES) sysinit.obj $(OBJFILES)
ALLRES = $(RESFILES)
ALLLIB = $(LIBFILES) $(LIBRARIES) import32.lib cp32mti.lib
# ---------------------------------------------------------------------------
!ifdef IDEOPTIONS

[Version Info]
IncludeVerInfo=0
AutoIncBuild=0
MajorVer=1
MinorVer=0
Release=0
Build=0
Debug=0
PreRelease=0
Special=0
Private=0
DLL=0
Locale=1036
CodePage=1252

[Version Info Keys]


CompanyName=
FileDescription=
FileVersion=1.0.0.0
InternalName=
LegalCopyright=
LegalTrademarks=
OriginalFilename=
ProductName=
ProductVersion=1.0.0.0
Comments=

[Debugging]
DebugSourceDirs=$(BCB)\source\vcl

[Parameters]
RunParams=
HostApplication=
RemoteHost=
RemotePath=
RemoteDebug=0

[Compiler]
InMemoryExe=0
ShowInfoMsgs=0

!endif

# ---------------------------------------------------------------------------
# MAKE SECTION
# ---------------------------------------------------------------------------
# This section of the project file is not used by the BCB IDE. It is for
# the benefit of building from the command-line using the MAKE utility.
# ---------------------------------------------------------------------------
.autodepend
# ---------------------------------------------------------------------------
!if !$d(BCC32)
BCC32 = bcc32
!endif

!if !$d(CPP32)
CPP32 = cpp32
!endif

!if !$d(DCC32)
DCC32 = dcc32
!endif

!if !$d(TASM32)
TASM32 = tasm32
!endif

!if !$d(LINKER)
LINKER = ilink32
!endif

!if !$d(BRCC32)
BRCC32 = brcc32
!endif

# ---------------------------------------------------------------------------
!if $d(PATHCPP)
.PATH.CPP = $(PATHCPP)
.PATH.C = $(PATHCPP)
!endif

!if $d(PATHPAS)
.PATH.PAS = $(PATHPAS)
!endif

!if $d(PATHASM)
.PATH.ASM = $(PATHASM)
!endif

!if $d(PATHRC)
.PATH.RC = $(PATHRC)
!endif
# ---------------------------------------------------------------------------
$(PROJECT): $(OBJFILES) $(RESDEPEN) $(DEFFILE)
$(BCB)\BIN\$(LINKER) @&&!
$(LFLAGS) +
$(ALLOBJ), +
$(PROJECT),, +
$(ALLLIB), +
$(DEFFILE), +
$(ALLRES)
!
# ---------------------------------------------------------------------------
.pas.hpp:
$(BCB)\BIN\$(DCC32) $(PFLAGS) {$< }

.pas.obj:
$(BCB)\BIN\$(DCC32) $(PFLAGS) {$< }

.cpp.obj:
$(BCB)\BIN\$(BCC32) $(CFLAG1) -n$(@D) {$< }

.c.obj:
$(BCB)\BIN\$(BCC32) $(CFLAG1) -n$(@D) {$< }

.c.i:
$(BCB)\BIN\$(CPP32) $(CFLAG1) -n. {$< }

.cpp.i:
$(BCB)\BIN\$(CPP32) $(CFLAG1) -n. {$< }

.asm.obj:
$(BCB)\BIN\$(TASM32) $(AFLAGS) $<, $@

.rc.res:
$(BCB)\BIN\$(BRCC32) $(RFLAGS) -fo$@ $<
# ---------------------------------------------------------------------------

Afin d’éditer un programme/projet/application dans l’environnement C++ Builder, il faut


exécuter la commande « Open Project » ou « CTRL-F11 »
Le programme/projet/application utilise une ou plusieurs fiches. Ces fiches sont créées et
initialisées automatiquement à l’exécution du fichier « .CPP » associé au projet. Dans le cas
qui nous intéresse, voici un exemple de contenu du fichier « Project2.CPP » :
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
USERES("Project2.res");
USEFORM("Ex1.cpp", Form1);
//---------------------------------------------------------------------------
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
//---------------------------------------------------------------------------

Dans le cas présent, cette application n’a qu’un seul panorama d’écran intitulé « FORM1 ».
Un autre cas serait l’application « TP3 » possédant deux panoramas d’écran : « FORM1 » et
« FORM2 ».
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
USERES("TP3.res");
USEFORM("Unit1.cpp", Form1);
USEFORM("Unit2.cpp", Form2);
//---------------------------------------------------------------------------
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->CreateForm(__classid(TForm2), &Form2);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
//---------------------------------------------------------------------------
Chaque fiche contient deux éléments :

1. Un fichier « .CPP » intitulé « UNIT » contenant du code C++ représentant les


méthodes utilisées dans le panorama d’écran ou la « Fiche ».
Voici un exemple de contenu de ce fichier. Nous avons ici le fichier « Ex1.CPP » :
//---------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <vcl.h>
#pragma hdrstop

#include "Ex1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)


{
//
// un bouton est cliqué.
TButton* b = (TButton*) Sender; // obligatoirement
int ct = (b->Left-2) / 52;
int rt = (b->Top-2) / 52;
int cv = vide % 4;
int rv = vide / 4;
if (abs(rt-rv) + abs(ct-cv) == 1)
{
b->Left = b->Left + (cv-ct)*52;
b->Top = b->Top + (rv-rt) * 52;
vide = rt*4 + ct;
}
}
//---------------------------------------------------------------------------

void __fastcall TForm1::Button4Click(TObject *Sender)


{
Application->Terminate();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
// appelé à la création de la fiche. C'est l'endroit idéal pour créer et
// initialiser toutes les variables (tuile, vide, position)
// 1 - créer et initialiser les 15 tuiles et leur positions
for (int i = 0; i < 15; i++)
{
tuile[i] = new TButton(Panel1);
tuile[i]->Parent = Panel1;
tuile[i]->Width = 50; tuile[i]->Height = 50;
tuile[i]->Left = (i % 4) * 52 + 2;
tuile[i]->Top = (i / 4) * 52 + 2;
tuile[i]->Caption = IntToStr(i+1);
tuile[i]->Font->Size = 18;
tuile[i]->Font->Style << fsBold;
tuile[i]->OnClick = Button1Click; // le nom de la fonction où vous
// avez inséré deux / (étape 7)
position[i] = i;
}
vide = 15;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
// brasser les positions
for (int i = 0; i < 15; i++)
{
int j = random(15);
int temp = position[i]; position[i] = position[j]; position[j] = temp;
}
Button3Click(Sender); // le nom de la fonction associée au bouton reprendre
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button3Click(TObject *Sender)
{
// Ici, il faut replacer les boutons selon le contenu du tableau position
for (int i = 0; i < 15; i++)
{
tuile[i]->Left = (position[i] % 4) * 52 + 2;
tuile[i]->Top = (position[i] / 4) * 52 + 2;
vide = 15; // on suppose !
}
}
//---------------------------------------------------------------------------

2. Un fichier « .H » ou « .HPP » contenant la description des classes et des


méthodes utilisées dans le panorama d’écran ou la « Fiche ». Pour accéder à ce
fichier, il faut faire la commande « CTRL-F6 » ou de cliquer à droite sur l’onglet du
fichier « *.CPP » et de sélectionner l’option « Open Source/Header File » :
Voici un exemple de contenu de ce fichier. Nous avons ici le fichier « Ex1.H » contenant la
définition des classes et le prototypage des méthodes utilisées dans cette fiche :
//---------------------------------------------------------------------------
#ifndef Ex1H
#define Ex1H
//---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ExtCtrls.hpp>
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published: // IDE-managed Components
// Déclarations publiées - visible par toutes les classes
// Ces classes sont gérées par l'inspecteur d'objet de Builder

TPanel *Panel1;
TButton *Button2;
TButton *Button3;
TButton *Button4;
void __fastcall Button1Click(TObject *Sender);
void __fastcall Button4Click(TObject *Sender);
void __fastcall FormCreate(TObject *Sender);
void __fastcall Button2Click(TObject *Sender);
void __fastcall Button3Click(TObject *Sender);
private: // User declarations
// déclarations privées qui appartiennent seulement à cette classe
TButton * tuile[15];
int position[15]; // position originale des tuiles
int vide; // position de la tuile vide

protected:
//déclarations protégées - visible seulement par cette
// classe et les classes descendantes
public: // User declarations
//déclarations publiques - accessibles à toutes les classes
__fastcall TForm1(TComponent* Owner);
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif

Sachant qu’un programme/projet/application peut utiliser une ou plusieurs fiches,


l’environnement C++ Buider comprend des éléments de navigation permettant de se déplacer
ou de repérer certaines fiches ou certains éléments de code.
Voici une brève description de ces commandes (boutons) de l’environnement C++ Builder :

Commande Action
Visualiser la liste de tous les fichiers « .CPP » ou « units ». Une fois la
liste affichée, l’utilisateur peut cliquer sur le nom d’un fichier affiché
pour éditer ce fichier.
Visualiser la liste de tous les fiches « Forms ». Une fois la liste
affichée, l’utilisateur peut cliquer sur le nom d’une fiche pour y
accéder.
Passer d’une fiche à son code correspondant ou du code d’un module
« Unit/.CPP » à sa fiche correspondante.
Créer une nouvelle fiche.

¾ Enregistrements(struct)

Le langage C permet à l’utilisateur de créer des types de données de différentes façons. Nous
allons ici étudier deux manières de créer de nouveaux types de données autres que ceux natifs
du langage C :

3. le type enregistrement « structure » qui est un regroupement de variables de type


standard du C sous un même nom ; et

4. le mot réservé « typedef » pouvant définir plusieurs variables d’un même type avec des
nouveaux noms.

Une structure est un regroupement de variables référencées sous un même nom, fournissant
un moyen pratique de regrouper des informations logiquement reliées.

Une déclaration de structure fournit un gabarit servant à créer des objets structurés. Les
variables composant la structure sont appelées membres, éléments ou champs de la
structure.
struct addr {
char name[30] ;
char street[40] ;
char city[20];
char state[3];
char zip[11];
int customer_num;
} ;

Dans le cas suivant, « addr » identifie cette structure de données particulière et en est son
spécificateur de type. Le nom de la structure est également appelé le « tag ». Aucune
variable n’est encore déclarée. Pour déclarer une variable de cette structure il faut faire
l’opération suivante :

struct addr addr_info;

Ceci déclare une variable de type addr appelée addr_info :

Pour déclarer plusieurs variables de ce type nous pouvons écrire la définition suivante :

struct addr {
char name[30] ;
char street[40] ;
char city[20];
char state[3];
char zip[11];
int customer_num;
} addr_info, binfo, cinfo;

Ceci déclare des variables de type addr appelées addr_info, binfo et cinfo.

La structure n’a pas besoin d’être nommée si nous n’avons à déclarer qu’une variable de ce
type :

struct {
char name[30] ;
char street[40] ;
char city[20];
char state[3];
char zip[11];
int customer_num;
} addr_info;
Accès aux champs de l’enregistrement:

Les champs de l’enregistrement peuvent être référencés avec l’utilisation du « . ». La forme


générale en est :

nom_de_la_structure.nom_du_champ

Exemple :

addr_info.customer_num = 88 ;

L’affectation des enregistrements.

L’information contenue dans une structure peut être affectée à une autre structure du même
type en utilisant l’instruction d’affectation simple. L’affectation de la structure par la copie de
chacun de ses champs n’est pas nécessaire. L’exemple suivant le prouve :
int main(void)
{
struct {
int a;
int b;
} x, y;

x.a = 10;
x.b = 20;

y = x; /* assign one structure to another */


printf("Contents of y: %d %d.", y.a, y.b);

return 0;
}

Vecteurs d’enregistrements :

Une des applications courantes que nous pouvons faire avec des enregistrements est de
construire des vecteurs d’enregistrements. Ces vecteurs peuvent être déclarés de la façon
suivante :

struct addr addr_info[100] ;

Ceci déclare un vecteur de 100 enregistrements de type addr.

Pour accéder au code postal du troisième client, nous pouvons faire l’opération suivante :

printf("%s", addr_info[2].zip);
Enregistrements passés en paramètres à des fonctions.

Dans le cas de variables scalaires, elles sont soit globales aux programmes ou locales aux
modules et aux fonctions. Ces variables peuvent également être passées par paramètres aux
fonctions.

Nous allons ici voir comment passer des champs ou des parties de variables structurées à
des fonctions.

Passage des champs d’enregistrements par paramètres :

Soit la structure suivante :

struct fred {
char x;
int y;
float z;
char s[10];
} mike;

Voici des exemples de passage de champs d’enregistrements par valeur :

func(mike.x); /* passes character value of x */


func2(mike.y); /* passes integer value of y */
func3(mike.z); /* passes float value of z */
func4(mike.s); /* passes address of string s */
func(mike.s[2]); /* passes character value of s[2] */

Voici des exemples de passage de champs d’enregistrements par adresse (par référence) avec
l’opérateur d’adresse « & » :

func(&mike.x); /* passes address of character x */


func2(&mike.y); /* passes address of integer y */
func3(&mike.z); /* passes address of float z */
func4(mike.s); /* passes address of string s */
func(&mike.s[2]); /* passes address of character s[2] */
Passage d’enregistrements complets en paramètres à des fonctions.
Nous avons ici un exemple de passage de structure par valeur. La structure est donc passée en
argument à la fonction et ses valeurs n’auront aucune modification :

#include <stdio.h>

/* declare a structure type */


struct struct_type {
int a, b;
char ch;
};

void f1(struct struct_type parm);

int main(void)
{
struct struct_type arg; /* declare arg */

arg.a = 1000;
f1(arg);

return 0;
}

void f1(struct struct_type parm)


{
printf("%d", parm.a);
}

Pointeurs sur des enregistrements.


Les langages C et C++ permettent les pointeurs sur les enregistrements tels que la plupart des
autres langages évolués.

Déclaration de pointeur sur un enregistrement :

struct addr *addr_pointer;

Ici, addr_pointer pointe sur une structure de type addr.


Utilisation et référencement des structures de pointeurs.
Afin d’obtenir l’adresse d’une variable d’enregistrement, il faut utiliser l’opérateur d’adresse
«&»:

struct bal {
float balance;
char name[80];
} person;

struct bal *p; /* declare a structure pointer */

Ici, p contient l’adresse de l’enregistrement personne :

p = &person;

L’accès aux champs via un pointeur d’enregistrement se fait des deux façons suivantes :

p->balance ;

ou

(*p).balance ;

L’exemple suivant utilise les notions précédentes :

/* Display a software timer. */

#include <stdio.h>
#include <conio.h>

#define DELAY 128000

struct my_time {
int hours;
int minutes;
int seconds;
};

void update(struct my_time *t), display(struct my_time *t);


void mydelay(void);
int main(void)
{
struct my_time systime;

systime.hours = 0;
systime.minutes = 0;
systime.seconds = 0;

for(;;) {
update(&systime);
display(&systime);
if(kbhit()) return 0;
}
}

void update(struct my_time *t)


{
t->seconds++;
if(t->seconds==60) {
t->seconds = 0;
t->minutes++;
}
if(t->minutes==60) {
t->minutes = 0;
t->hours++;
}
if(t->hours==24) t->hours = 0;
mydelay();
}

void display(struct my_time *t)


{
printf("%02d:", t->hours);
printf("%02d:", t->minutes);
printf("%02d\n", t->seconds);
}

void mydelay(void)
{
long int t;

for(t=1; t<DELAY; ++t) ;


}
Structures d’enregistrements ayant des vecteurs ou des structures comme champs.

Structures d’enregistrement ayant un vecteur comme champ :

struct x {
int a[10][10]; /* 10 x 10 array of ints */
float b;
} y;

Ceci nous permet de référencer l’entier de la 3ième ligne et de la 7ième colonne de la structure y :

y.a[3][7] ;

Structures d’enregistrement ayant une autre structure comme champ :


struct emp {
struct addr address;
float wage;
} worker;

Ici, la structure « addr » est contenue dans la structure « emp ». Les opérations suivantes
assignent un salaire de $65,000.00 et le code postal 98765 à la variable « worker ».

worker.wage = 65000.00;
strcpy(worker.address.zip,"98765");

Utilisation du mot réservé « typedef »

Les langages C et C++ permettent à l’utilisateur de définir de nouveaux types de données à l’aide
de l’instruction « typedef ». La forme générale de déclaration des types à l’aide de typedef est la
suivante :

typedef type nouveau_nom_du_type

où type est un type ou une structure de données existante :

Cette déclaration donne un deuxième nom au type « float »

typedef float balance;

Nous pouvons maintenant déclarer un réel de la façon suivante :

balance past_due;
L’instruction « typedef » peut également servir à définir des types structurés :

typedef struct {
float due;
int over_due;
char name[40];
} client; /* here client is the new type name */

client clist[NUM_CLIENTS]; /* define array of


structures of type client */

Utillsation de l’instruction « struct » dans la déclaration des classes

Dans le langage C++, l’instruction « struct » a des fonctions supplémentaires que celle du C
standard. Le langage C++ relie étroitement les instructions « class » et « struct ».

L’instruction struct peut servir à déclarer des classes et tous les éléments du « struct » sont
de type « public » par défaut :

#include <iostream>
using namespace std;

struct cl {
int get_i(); // these are public
void put_i(int j); // by default
private:
int i;
} ;

int cl::get_i()
{
return i;
}

void cl::put_i(int j)
{
i = j;
}

int main()
{
cl s;

s.put_i(10);
cout << s.get_i();

return 0;
}
Lorsque l’instruction « class » est utilisés, tous ses éléments sont de type « private » par défaut:

#include <iostream>
using namespace std;

class cl {
int i; // private by default
public:
int get_i();
void put_i(int j);
} ;

int cl::get_i()
{
return i;
}

void cl::put_i(int j)
{
i = j;
}

int main()
{
cl s;

s.put_i(10);
cout << s.get_i();

return 0;
}

Pointeurs

Une adresse est une valeur. On peut donc stocker cette valeur dans une variable. Les pointeurs
sont justement des variables qui contiennent l'adresse d'autres objets, par exemple l'adresse
d'une autre variable. On dit que le pointeur pointe sur la variable pointée. Ici, pointer signifie «
faire référence à ». Les adresses sont généralement des valeurs constantes, car en général un
objet ne se déplace pas en mémoire. Toutefois, la valeur d'un pointeur peut changer. Cela ne
signifie pas que la variable pointée est déplacée en mémoire, mais plutôt que le pointeur pointe
sur autre chose.

Afin de savoir ce qui est pointé par un pointeur, les pointeurs disposent d'un type. Ce type est
construit à partir du type de l'objet pointé. Cela permet au compilateur de vérifier que les
manipulations réalisées en mémoire par l'intermédiaire du pointeur sont valides.

Le type des pointeur se lit « pointeur de ... », où les points de suspension représentent le nom du
type de l'objet pointé.
Les pointeurs se déclarent en donnant le type de l'objet qu'ils devront pointer, suivi de leur
identificateur précédé d'une étoile :

int *pe; // pe est un pointeur d'entier.

Note : Si plusieurs pointeurs doivent être déclarés, l'étoile doit être répétée :

int *pe1, *pe2, i, j, *pe3;

Ici, pe1, pe2 et pe3 sont des pointeurs d'entiers et i, j sont des entiers.
Avec l’image précédente, nous pouvons constater que :

int *pe1 ;
int i, j ;

L’opérateur « & » est un opérateur unaire retournant l’adresse mémoire de son opérande:

45 = i ;
23444 = &i ;

461 = j ;
23443 = &j ;

L’opérateur « * » est un opérateur unaire retournant la valeur ou le contenu de la variable étant à


cette adresse:

23443 = pe1 ;
461 = *pe1 ;

Ces deux opérateurs peuvent être utilisés simultanément :

pe1 = &i ;

alors

23444 = pe1 ; et
45 = *pe1 ;

L’opérateur « const » dans le cas des pointeurs indique au compilateur que la valeur de ce
pointeur reste inchangée lors d’un appel de fonction :

#include <stdio.h>

void sp_to_dash(const char *str);

int main(void)
{
sp_to_dash("this is a test");
return 0;
}

void sp_to_dash(const char *str)


{
while(*str) {
if(*str == ' ') printf("%c", '-');
else printf("%c", *str);
str++;
}
}
En C++, le nom d’un vecteur ou d’une variable non scalaire (structurée) est considéré comme
étant l’adresse du vecteur ou de la structure qui pointe à la première occurrence de cette
structure. Considérons l’exemple suivant :

char ch_car[80], *p1;

qui est la déclaration d’une chaîne de caractères et d’un pointeur sur une chaîne de caractères.
Soit :

p1 = ch_car ;

p1 pointe maintenant sur la chaine de caractères. Si nous voulons accéder le cinquième élément
du vecteur, nous pouvons faire les opérations suivantes :

str[4] ou *(p1 + 4) ;

Il est possible de faire un pointeur sur une structure dans une structure en indiquant le nom de la
structure comme type du pointeur:

typedef struct nom


{
struct nom *pointeur; /* Pointeur sur une structure "nom". */
...
} MaStructure;

Ce type de construction permet de créer des listes de structures, dans lesquelles chaque
structure contient l'adresse de la structure suivante dans la liste. Nous verrons plus loin un
exemple d'utilisation de ce genre de structure.

Il est également possible de créer des pointeurs sur des fonctions, et d'utiliser ces pointeurs pour
paramétrer un algorithme, dont le comportement dépendra des fonctions ainsi pointées. Nous
détaillerons plus loin ce type d'utilisation des pointeurs.

Récursivité

Une fonction récursive est une fonction qui s’appelle d’elle-même jusqu’à la rencontre d’une
condition d’arrêt. Ce type de fonction peut servir à implanter certains types d’algorithmes
s’éxécutant jusqu’à une ou plusieurs conditions d’arrêt. Il est à noter que ces algorithmes peuvent
également être implantés avec des programmes itératifs à l’aide de boucles conventionnelles.

Une fonction récursive possède généralement un code plus compact qu’une fonction itérative. La
fonction récursive modèle plus intégralement la définition l’algorithme et la pile gère les itérations
au lieu du programmeur.
Voici une fonction récursive calculant la factorielle d’un nombre en C++ :

/* Compute the factorial of a number. */


int factr(int n) /* recursive */
{
int answer;

if(n==1) return(1);
answer = factr(n-1)*n;
return(answer);
}

Voici une fonction itérative calculant la factorielle d’un nombre en C++ :

/* Compute the factorial of a number. */


int fact(int n) /* non-recursive */
{
int t, answer;

answer = 1;
for(t=1; t<=n; t++)
answer=answer*(t);
return(answer);
}
COURS 04

INF-11299 – PROGRAMMATION II

PROGRAMMATION C++
Référence : Schildt, Herbert & Guntle, Greg, Borland C++ Builder, The complete reference,
Osborne/McGraw-Hill, Berkeley, 2001, 977pp, ISBN: 0-07-212778-3.

LES ORIGINES DU C++

Le langage C++ a débuté comme étant une entension du langage C. Les extensions du C++ ont
été inventées par Bjarne Stroustrup en 1979 aux Bell Laboratories à Murray Hill dans le New
jersey.

Bjarne Stroustrup avait initialement appelé son langage « C with Classes », mais ce nom a été
changé en 1983 pour celui de C++.

Ce langage a été inventé afin d’améliorer une faiblesse du langage C qui faisait en sorte que les
programmes en C avaient tendance à devenir incompréhensibles à des grosseurs de plus de 25
000 lignes de code.

Quelques unes des caractéristiques orientées objet du C++ ont été inspirées d’un autre langage
orienté-objet appelé Simula67.

La communauté informatique a pleinement reconnu ce langage et un comité des standards ANSI


et ISO a défini des normes pour le C++ en 1994.

La notion de librairie de routines génériques (« Standard Template Library/STL ») est une autre
approche de programmation renforcissant la puissance du C++.

Une norme du C++ ANSI/ISO concernant la syntaxe et la sémantique de son langage a été
internationalement reconnue en 1997.

Définition d’une classe en C++

class class-name {
private data and functions
public:
public data and functions
} object-list;
Prenons par exemple la déclaration de la classe d’une liste de nombres :
// This creates the class queue.
class queue {
int q[100];
int sloc, rloc;
public:
void init();
void qput(int i);
int qget();
};

L’opérateur d’appartenance ou « scope resolution operator » représenté par le symbole « :: »


définit la portée des méthodes:

void queue::qput(int i)
{
if(sloc==99) {
cout << "Queue is full.\n";
return;
}
sloc++;
q[sloc] = i;
}

L’énoncé « queue::qput(int i) » signifie que la méthode « qput(int i) » appartient à la classe


« queue ».

L’exemple d’appel d’une méthode suivant appelle la méthode init() de l’objet a :

a.init();

Le programme suivant définit une liste circulaire implantée à l’aide d’un vecteur :

#include <iostream>
using namespace std;

// This creates the class queue.


class queue {
int q[100];
int sloc, rloc;
public:
void init();
void qput(int i);
int qget();
};
void queue::init()
{
rloc = sloc = 0;
}

void queue::qput(int i)
{
if(sloc==99) {
cout << "Queue is full.\n";
return;
}
sloc++;
q[sloc] = i;
}

int queue::qget()
{
if(rloc == sloc) {
cout << "Queue underflow.\n";
return 0;
}
rloc++;
return q[rloc];
}

int main()
{
queue a, b; // create two queue objects

// now, access the queues through their member functions


a.init();
b.init();

a.qput(10);
b.qput(19);

a.qput(20);
b.qput(1);

cout << a.qget() << " ";


cout << a.qget() << " ";
cout << b.qget() << " ";
cout << b.qget() << "\n";

return 0;
}
Les éléments déclarés dans la section « private: » de la classe sont accessibles seulement par
des éléments membres de la classe.

L’énoncé :

a.rloc = 0;

ne peut pas être placé dans le programme principal parce que la variable « rloc » est déclarée
privée.

Surcharge des fonctions et des opérateurs (« Function Overloading »)

Un aspect du polymorphisme du C++ est par l’utilisation de la surcharge des méthodes et des
opérateurs. En C++, deux ou plusieurs fonctions peuvent partager le même nom en autant que
leurs déclarations de paramètres soit différente. Dans cette situation, les fonctions partageant le
même nom sont catégorisées comme étant surchargées (« overloaded »).

Le programme suivant en illustre le concept :

#include <iostream>
using namespace std;

// sqr_it is overloaded three ways


int sqr_it(int i);
double sqr_it(double d);
long sqr_it(long l);

int main()
{
cout << sqr_it(10) << "\n";
cout << sqr_it(11.0) << "\n";
cout << sqr_it(9L) << "\n";

return 0;
}

// Define sqr_it for ints.


int sqr_it(int i)
{
cout << "Inside the sqr_it() function that uses ";
cout << "an integer argument.\n";

return i*i;
}
// Overload sqr_it for doubles.
double sqr_it(double d)
{
cout << "Inside the sqr_it() function that uses ";
cout << "a double argument.\n";

return d*d;
}

// Overload sqr_it again, this time for longs.


long sqr_it(long l)
{
cout << "Inside the sqr_it() function that uses ";
cout << "a long argument.\n";

return l*l;
}
Un autre exemple de surchargement de fonctions permettant l’affichage de différents types de
données sans créer différents noms de fonctions d’affichage:

#include <iostream>
using namespace std;

void prompt(char *str, int *i);


void prompt(char *str, double *d);
void prompt(char *str, long *l);

int main()
{
int i;
double d;
long l;

prompt("Enter an integer: ", &i);


prompt("Enter a double: ", &d);
prompt("Enter a long: ", &l);

cout << i << " " << d << " " << l;

return 0;
}

// Prompt for an int.


void prompt(char *str, int *i)
{
cout << str;
cin >> *i;
}
// Prompt for a double.
void prompt(char *str, double *d)
{
cout << str;
cin >> *d;
}

// Prompt for a long.


void prompt(char *str, long *l)
{
cout << str;
cin >> *l;
}

Un principe de bonne programmation en C++ stipule se surcharger seulement des fonctions


ayant un but général commun ou des fonctions similaires.

L’héritage (Inheritance)
L’héritage est l’un des trois aspects principaux des langages orientés-objet. Dans le langage
C++, l’héritage (« inheritance ») est supporté par la capacité d’inclure une classe dans la
déclaration d’une autre classe.

L’héritage permet la construction d’une hiérarchie de classes évoluant d’un concept général à un
concept spécifique.

Une hiérarchie des classes débute par la définition d’une classe de base, définissant toutes les
qualités et les attributs communs à tous les objets dérivés de la classe de base. La classe de
base définit un concept de la façon la plus générale.

Les classes dérivées de la classe de base sont généralement catégorisées comme étant des
classes dérivées. Une classe dérivée inclut tous les aspects de la chasse de base générique et
ajoute les opérations et les attributs spécifiques à sa définition.

Voici un exemple d’une hiérarchie de classes définissant des véhicules :

class road_vehicle {
int wheels;
int passengers;
public:
void set_wheels(int num);
int get_wheels();
void set_pass(int num);
int get_pass();
};
En utilisant les aspects généraux définis dans la classe des véhicules routiers (« road_vehicle »),
nous pouvons définir la classe camion (« truck ») :

class truck : public road_vehicle {


int cargo;
public:
void set_cargo(int size);
int get_cargo();
void show();
};

La forme générale de l’héritage dans le langage C++ se définit de la façon suivante :

class new-class-name : access inherited-class {


// body of new class
}

Dans cette déclaration, l’accès est optionnel. Si l’accès est mentionné, il doit être de type public,
private ou protected.

Pour le moment, toutes les classes qui héritent vont utiliser le mode d’accès public.

Ceci veut dire que tous les éléments déclarés publiquement dans les classes ancêtres vont être
également publics dans les classes héritées.

Cependant, les opérations de la fonction (« truck ») n’ont pas accès aux éléments privés de la
classe (« road_vehicle »).

L’exemple suivant illustre la création par l’héritage de deux sous-classes de (« road_vehicle ») :


(« truck ») et (« automobile ») :

listing 10
#include <iostream>
using namespace std;

class road_vehicle {
int wheels;
int passengers;
public:
void set_wheels(int num);
int get_wheels();
void set_pass(int num);
int get_pass();
};
// Extend road_vehicle for trucks.
class truck : public road_vehicle {
int cargo;
public:
void set_cargo(int size);
int get_cargo();
void show();
};

enum type {car, van, wagon};

// Extend road_vehicle for cars.

class automobile : public road_vehicle {


enum type car_type;
public:
void set_type(enum type t);
enum type get_type();
void show();
};

void road_vehicle::set_wheels(int num)


{
wheels = num;
}

int road_vehicle::get_wheels()
{
return wheels;
}

void road_vehicle::set_pass(int num)


{
passengers = num;
}

int road_vehicle::get_pass()
{
return passengers;
}

void truck::set_cargo(int num)


{
cargo = num;
}

int truck::get_cargo()
{
return cargo;
}
void truck::show()
{
cout << "Wheels: " << get_wheels() << "\n";
cout << "Passengers: " << get_pass() << "\n";
cout << "Cargo capacity in cubic feet: " << cargo << "\n";
}

void automobile::set_type(enum type t)


{
car_type = t;
}

enum type automobile::get_type()


{
return car_type;
}

void automobile::show()
{
cout << "Wheels: " << get_wheels() << "\n";
cout << "Passengers: " << get_pass() << "\n";
cout << "Type: ";
switch(get_type()) {
case van: cout << "Van\n";
break;
case car: cout << "Car\n";
break;
case wagon: cout << "Wagon\n";
}
}

int main()
{
truck t1, t2;
automobile c;

t1.set_wheels(18);
t1.set_pass(2);
t1.set_cargo(3200);
t2.set_wheels(6);
t2.set_pass(3);
t2.set_cargo(1200);
t1.show();
t2.show();
c.set_wheels(4);
c.set_pass(6);
c.set_type(van);
c.show();
return 0;
}
Constructeurs (« constructors ») et destructeurs (« destructors »)
Il est parfois fréquent qu’une routine, un programme ou un objet ait besoin d’une initialisation de
ses paramètres avant son utilisation.

Le besoin d’initialisation étant un phénomène assez fréquent, le langage C++ en tient compte en
permettant aux objets de s’initialiser automatiquement à leur création. Cette initialisation
automatique est faite à l’aide d’un constructeur.

Un constructeur est une fonction spéciale membre d’une classe et qui a le même nom que cette
classe.

Voicl l’exemple d’une liste circulaire se servant d’un constructeur pour s’initialiser :

// This creates the class queue.


class queue {
int q[100];
int sloc, rloc;
public:
queue(); // constructor
void qput(int i);
int qget();
};

Le constructeur de la liste est déclaré de la facon suivante:

// This is the constructor function.


queue::queue()
{
sloc = rloc = 0;
cout << "Queue initialized.\n";
}

Il est à noter qu’il n’y aucune valeur de retour dans la déclaration de cette fonction. En C++, les
constructeurs ne retournent pas de valeurs.

Le constructeur de l’objet est automatiquement exécuté lorsque l’objet est crée, c’est-à-dire lors
de sa déclaration.

La fonction inverse du constructeur est appelée le destructeur. Son rôle est d’exécuter certaines
opérations lorsque l’objet est détruit telles que la désallocation de variables ou d’espace mémoire
alloué dynamiquement. Il est exécuté automatiquement à la fin de l’utilisation de l’objet (à la
sortie d’une sous-routine ou à la fin du programme).
Le destructeur a le même nom que le constructeur sauf qu’il est précédé du caractère tilde
(« ~ »).

Voici un exemple du destructeur de la liste circulaire :

// This is the destructor function.


queue::~queue()
{
cout << "Queue destroyed.\n";
}

Il est à noter que dans cet exemple, le destructeur n’effectue aucune opération.

Voici l’exemple complet d’implantation de la liste circulaire :

#include <iostream>
using namespace std;

// This creates the class queue.


class queue {
int q[100];
int sloc, rloc;
public:
queue(); // constructor
~queue(); // destructor
void qput(int i);
int qget();
};

// This is the constructor function.


queue::queue()
{
sloc = rloc = 0;
cout << "Queue initialized.\n";
}

// This is the destructor function.


queue::~queue()
{
cout << "Queue destroyed.\n";
}
void queue::qput(int i)
{
if(sloc==99) {
cout << "Queue is full.\n";
return;
}
sloc++;
q[sloc] = i;
}

int queue::qget()
{
if(rloc == sloc) {
cout << "Queue underflow.\n";
return 0;
}
rloc++;
return q[rloc];
}

int main()
{
queue a, b; // create two queue objects

a.qput(10);
b.qput(19);

a.qput(20);
b.qput(1);

cout << a.qget() << " ";


cout << a.qget() << " ";
cout << b.qget() << " ";
cout << b.qget() << "\n";

return 0;
}

Le programme affiche les résultats suivans:

Queue initialized
Queue initialized
10 20 19 1
Queue destroyed
Queue destroyed
Constructeurs paramétrisés
Dans le langage C++, nous pouvons fournir des paramètres comme arguments à des
constructeurs. Ces paramètres servent à initialiser un objet lors de sa création.

Voici un exemple donnant un numéro d’identification à chacune des files créées :

// This creates the class queue.


class queue {
int q[100];
int sloc, rloc;
int who; // holds the queue's ID number
public:
queue(int id); // parameterized constructor
~queue(); // destructor
void qput(int i);
int qget();
};

La variable (“who”) est utilisée pour mémoriser un numéro d’identification identifiant la file:

// This is the constructor function.


queue::queue(int id)
{
sloc = rloc = 0;
who = id;
cout << "Queue " << who << " initialized\n";
}

Le passage de valeurs en paramètres au constructeur d’un objet s’effectue lors de la déclaration


d’un objet. Il existe deux méthodes de passage de paramètres :

queue a = queue(101);

ou

queue a(101);

La forme générale du passage d’arguments à une fonction est la suivante :

class-type obj(arg-list);
L’exemple suivant utilise le programme de liste circulaire afin d’illustrer le passage de paramètres
à un constructeur :

#include <iostream>
using namespace std;

// This creates the class queue.


class queue {
int q[100];
int sloc, rloc;
int who; // holds the queue's ID number
public:
queue(int id); // parameterized constructor
~queue(); // destructor
void qput(int i);
int qget();
};

// This is the constructor function.


queue::queue(int id)
{
sloc = rloc = 0;
who = id;
cout << "Queue " << who << " initialized.\n";
}

// This is the destructor function.


queue::~queue()
{
cout << "Queue " << who << " destroyed.\n";
}

void queue::qput(int i)
{
if(sloc==99) {
cout << "Queue is full.\n";
return;
}
sloc++;
q[sloc] = i;
}
int queue::qget()
{
if(rloc == sloc) {
cout << "Queue underflow.\n";
return 0;
}
rloc++;
return q[rloc];
}

int main()
{
queue a(1), b(2); // create two queue objects

a.qput(10);
b.qput(19);

a.qput(20);
b.qput(1);

cout << a.qget() << " ";


cout << a.qget() << " ";
cout << b.qget() << " ";
cout << b.qget() << "\n";

return 0;
}

Le programme affiche les résultats suivans:

Queue 1 initialized
Queue 2 initialized
10 20 19 1
Queue 2 destroyed
Queue 1 destroyed
L’exemple suivant illustre que nous pouvons passer plusieurs paramètres à un constructeur :

#include <iostream>
using namespace std;

class widget {
int i;
int j;
public:
widget(int a, int b);
void put_widget();
} ;

widget::widget(int a, int b)
{
i = a;
j = b;
}

void widget::put_widget()
{
cout << i << " " << j << "\n";
}

int main()
{
widget x(10, 20), y(0, 0);

x.put_widget();
y.put_widget();

return 0;
}

Le programme affiche les résultats suivants :

10 20
00
Cas particulier : Constructeurs n’acceptant qu’un seul paramètre
Si un constructeur n’a seulement qu’un seul paramètre, il existe une troisième méthode de
passage de paramètres :

#include <iostream>
using namespace std;

class X {
int a;
public:
X(int j) { a = j; }
int geta() { return a; }
};

int main()
{
X ob = 99; // passes 99 to j

cout << ob.geta(); // outputs 99

return 0;
}

Nous pouvons passer la valeur « 99 » en paramètres de la façon suivante :

X ob = X(99);

qui est équivalente à :

X ob(99);

ou

X ob = 99;

Fonctions mutuelles/communes/amies (« Friend functions »)


Il est possible pour une fonction ou méthode externe à une classe d’accéder à des éléments
déclarés privés d’une classe en la déclarant mutuelle, commune ou amie (« friend ») de cette
classe.

Ici, « frd() » est déclarée comme étant une méthode amie de la classe « cl ». Le mot réservé
(« friend ») précède la décalration de la fonction :
class cl {
// ...
public:
friend void frd();
// ...
};

Une des raisons de l’implantation de fonctions communes en C++ est pour résoudre le cas où
deux classes doivent utiliser la même fonction. Voici un exemple de définition de la fonction
commune (« same_color() ») partagée par les deux classes (« lines ») et (« box ») :

class line;

class box {
int color; // color of box
int upx, upy; // upper left corner
int lowx, lowy; // lower right corner
public:
friend int same_color(line l, box b);
void set_color(int c);
void define_box(int x1, int y1, int x2, int y2);
void show_box();
} ;

class line {
int color; // color of line
int startx, starty; // coordinates
int len; // length

public:
friend int same_color(line l, box b);
void set_color(int c);
void define_line(int x, int y, int l);
void show_line();
} ;

La fonction (« same_color() ») n’est pas membre d’aucune des deux classes mais définie comme
étant commune aux deux classes. La fonction retourne la valeur vraie si les objets (« line ») et
(« box ») sont de la même couleur. Cette fonction accède les propriétés privées (« color ») des
deux classes. La fonction (« same_color() ») est définie de la façon suivante :

// Return true if line and box have same color.


int same_color(line l, box b)
{
if(l.color==b.color) return 1;
return 0;
}
Voici un programme complet utilisant cette fonction. Il est à noter que la classe (« line ») est
prototypée afin que le compilateur C++ ne donne pas d’erreur lors de la compilation de la
fonction (« same_color() ») de la classe (« box ») :

#include <iostream>
#include <conio.h>
using namespace std;

class line;

class box {
int color; // color of box
int upx, upy; // upper left corner
int lowx, lowy; // lower right corner
public:
friend int same_color(line l, box b);
void set_color(int c);
void define_box(int x1, int y1, int x2, int y2);
void show_box();
} ;

class line {
int color; // color of line
int startx, starty; // coordinates
int len; // length
public:
friend int same_color(line l, box b);
void set_color(int c);
void define_line(int x, int y, int l);
void show_line();
} ;

// Return true if line and box have same color.


int same_color(line l, box b)
{
if(l.color==b.color) return 1;
return 0;
}

void box::set_color(int c)
{
color = c;
}

void line::set_color(int c)
{
color = c;
}
void box::define_box(int x1, int y1, int x2, int y2)
{
upx = x1;
upy = y1;
lowx = x2;
lowy = y2;
}

void box::show_box()
{
int i;

textcolor(color);

gotoxy(upx, upy);
for(i=upx; i<=lowx; i++) cprintf("-");

gotoxy(upx, lowy-1);
for(i=upx; i<=lowx; i++) cprintf("-");

gotoxy(upx, upy);
for(i=upy; i<=lowy; i++) {
cprintf("|");
gotoxy(upx, i);
}

gotoxy(lowx, upy);
for(i=upy; i<=lowy; i++) {
cprintf("|");
gotoxy(lowx, i);
}
}

void line::define_line(int x, int y, int l)


{
startx = x;
starty = y;
len = l;
}

void line::show_line()
{
int i;

textcolor(color);

gotoxy(startx, starty);

for(i=0; i<len; i++) cprintf("-");


}
int main()
{
box b;
line l;

b.define_box(10, 10, 15, 15);


b.set_color(3);
b.show_box();

l.define_line(2, 2, 10);
l.set_color(2);
l.show_line();

if(!same_color(l, b)) cout << "Not the same.\n";


cout << "\nPress a key.";
getch();

// now, make line and box the same color


l.define_line(2, 2, 10);
l.set_color(3);
l.show_line();

if(same_color(l, b)) cout << "Are the same color.\n";

return 0;
}

Fonctions/Méthodes comportant des paramètres par défaut


Le compilateur C++ permet à une fonction/méthode d’avoir des paramètres par défaut
lorsqu’aucune variable ou valeur correspondant au paramètre n’est spécifié dans l’appel de la
fonction. Le paramètre par défaut est spécifié de la même manière qu’une initialisation de
variables. L’exemple suivant déclare la fonction « f() » comme ayant un paramètre entier de
valeur « 1 » par défaut :

void f(int i = 1)
{
// Code de la fonction f
}

La fonction « f() » peut être appelée des deux façons suivantes :

f(10); // argument explicite de valeur 10

ou

f(); // utilisation de la valeur par défaut


L’exemple suivant illustre bien ce concept :

#include <iostream>
#include <conio.h>
using namespace std;

void xyout(char *str, int x=0, int y=0)


{
if(!x) x = wherex();
if(!y) y = wherey();
gotoxy(x, y);
cout << str;
}

int main()
{
xyout("hello", 10, 10);
xyout(" there");
xyout("I like C++", 40); // this is still on line 10

xyout("This is on line 11.\n", 1, 11);


xyout("This follows on line 12.\n");
xyout("This follows on line 13.");
return 0;
}

Le programme produit les sorties suivantes:

hello there I like C++


This is on line 11.
This follows on line 12.
This follows on line 13.

Nous pouvons également déclarer des paramètres par défaut dans les constructeurs des objets :

listing 14
/* This is the constructor function that uses
a default value. */
queue::queue(int id=0)
{
sloc = rloc = 0;
who = id;
cout << "Queue " << who << " initialized.\n";
}

Dans cet exemple, un objet déclaré par défaut aura sa propriété « id » initialisée à 0 :

queue a, b(2);
Ici, deux objets sont déclarés, « a » et « b ». La propriété « id » de « a » sera « 0 » et celle de
« b » sera « 2 ».

Les macros (« Inline Functions »)


Une caractéristique intéressante du C++ est de faire des fonctions macros qui seront traitées par
le préprocesseur du C++ en générant du code au lieu d’avoir un appel de fonction. Ceci
augmente la vitesse d’exécution des programmes en supprimant les nombreuses manipulations
de pile nécessaires à l’appel des sous-routines. La forme générale de déclararion d’une macro
est la suivante :

inline function_declaration

Exemple:

inline int f()


{
// Code de la macro (généralement une ligne de code)
}

Le programme suivant utilise la notion des macros pour définir les méthodes « get_i() » et
« put_i() » :

#include <iostream>
using namespace std;

class cl {
int i;
public:
int get_i();
void put_i(int j);
};

inline int cl::get_i()


{
return i;
}

inline void cl::put_i(int j)


{
i = j;
}

int main()
{
cl s;

s.put_i(10);
cout << s.get_i();
return 0;
}
Définition de macro-instructions à l’intérieur d’une classe
Les macro-instructions peuvent également être définies à l’intérieur d’une déclaration de classe.
Toute fonction définie à l’intérieur d’une déclaration de classe est automatiquement interprétée
comme étant une macro-instruction. Il n’est pas nécessaire de précéder les déclarations par le
mot réservé (« inline ») :

#include <iostream>
using namespace std;

class cl {
int i;
public:
// automatic inline functions
int get_i() { return i; }
void put_i(int j) { i = j; }
} ;

int main()
{
cl s;

s.put_i(10);
cout << s.get_i();

return 0;
}

La définition précédente de macro-instructions nous montrait une manière compacte de le faire.


Ces macros-instructions auraient également pu être définies de la façon suivante :

class cl {
int i;
public:
// automatic inline functions
int get_i()
{
return i;
}

void put_i(int j)
{
i = j;
}
} ;

Il est à noter que les fonctions/méthodes d’une ligne ou deux sont généralement définies sous la
forme de macro-instructions.
Une macro-instruction étant une instruction de génération de code, celle-ci interdit généralement
les fonctions de branchement telles que « if », « loop », « switch » et « goto ». Ces instructions
peuvent faire générer des erreurs de branchement par le pré-processeur de code.

Passage d’objets à des fonctions


En langage C++, les objets peuvent être passés par valeur à des fonctions de la même façon
que tout autre type de variables scalaires ou structurées. Nous allons ici étudier le mode de
passage par valeur. Le mode de passage de paramètres par valeur implique qu’une copie de
l’objet est effectuée sur la pile lors d’un passage par valeur à la fonction.

Dans le cas qui nous intéresse, la copie implique la création d’un autre objet. Le constructeur
n’est cependant pas appelé afin de préserver l’état courant de l’objet.

Lors de la fin de l’appel de la fonction, le destructeur de l’objet est appelé afin de détruire la copie
temporaire de l’objet avant le retour au programme principal.

L’exemple suivant illustre un appel à la fonction « f », ayant l’objet « ob » comme paramètre :

#include <iostream>
using namespace std;

class myclass {
int i;
public:
myclass(int n);
~myclass();
void set_i(int n) { i=n; }
int get_i() { return i; }
};

myclass::myclass(int n)
{
i = n;
cout << "Constructing " << i << "\n";
}

myclass::~myclass()
{
cout << "Destroying " << i << "\n";
}

void f(myclass ob);


int main()
{
myclass o(1);

f(o);
cout << "This is i in main: ";
cout << o.get_i() << "\n";

return 0;
}

void f(myclass ob)


{
ob.set_i(2);

cout << "This is local i: " << ob.get_i();


cout << "\n";
}

Le programme produit les résultats suivants:

Constructing 1
This is local i : 2
Destroying 2
This is i in main: 1
Destroying 1

Retour d’objets par les fonctions


Dans la plupart des langages évolués, les fonctions peuvent retourner des valeurs entières,
réelles ou booléenes. Elles ne peuvent généralement pas retourner de types structurés. Le
langage C++ permet aux fonctions de retourner des types structurés qui sont des objets :

#include <iostream>
using namespace std;

class myclass {
int i;
public:
void set_i(int n) { i=n; }
int get_i() { return i; }
};

myclass f(); // return object of type myclass


int main()
{
myclass o;

o = f();
cout << o.get_i() << "\n";

return 0;
}

myclass f()
{
myclass x;

x.set_i(1);
return x;
}

Méthode d’affectation des objets (« Objet Assignment »)


Le langage C++ permet l’affectation des types simples, structurés et des objets. L’opérateur
d’affectation des objets est l’opérateur standard (« = »). L’affectation est valide seulement avec
deux objets de même type. Lors de l’affectation, les données de l’objet de droite sont copiées
dans celui de gauche.

Le programme suivant affecte l’objet « ob1 » à « ob2 » :

#include <iostream>
using namespace std;

class myclass {
int i;
public:
void set_i(int n) { i=n; }
int get_i() { return i; }
};

int main()
{
myclass ob1, ob2;

ob1.set_i(99);
ob2 = ob1; // assign data from ob1 to ob2

cout << "this is ob2's i: " << ob2.get_i();

return 0;
}
Le résultat de ce programme est l’affichage de:

this is ob2’s i : 99

Vecteurs d’objets
Le langage C++ permet la création de vecteurs de types simples et structurés. Il permet
également la création de vecteurs d’objets. La déclaration de vecteurs d’objets se fait de façon
similaire à celle de vecteurs de types structurés.

L’exemple suivant définit une classe regroupant des infomations sur des écrans d’ordinateurs
(« display »). Le programme effectue ensuite des opérations sur un vecteur d’objets (« display »)
et le nom du vecteur est défini comme étant (« monitors ») :

// An example of arrays of objects.

#include <iostream>
using namespace std;

enum resolution { r640x480, r800x600, r1024x768 };


enum coloroption { c16, c256, cHighColor, cTrueColor };

class display {

coloroption coption; // color option


resolution res; // resoltuion

public:

void set_coloropt(coloroption opt) { coption = opt; }


coloroption get_coloropt() { return coption; }
void set_res(resolution r) { res = r; }
resolution get_res() { return res; }

} ;

char options[4][20] = {
"16 Colors",
"256 Colors",
"High Color (16 bit)",
"True Color (32 bit)"
} ;

char resvals[3][20] = {
"640 x 480",
"800 x 600",
"1024 x 768"
} ;
int main()
{
display monitors[3];
register int i;

monitors[0].set_coloropt(c16);
monitors[0].set_res(r640x480);

monitors[1].set_coloropt(cTrueColor);
monitors[1].set_res(r640x480);

monitors[2].set_coloropt(c256);

monitors[2].set_res(r1024x768);

for(i=0; i<3; i++) {


cout << options[monitors[i].get_coloropt()] << " ";
cout << "with resolution of " << resvals[monitors[i].get_res()];
cout << "\n";
}

return 0;
}

Le programme produit les résultats suivants:

16 Colors with resolution of 640 x 480


True Color (32 bits) with resolution of 640 x 480
256 Colors with resolution of 1024 x 768

Initialisation de vecteurs d’objets


La méthode exacte d’initialisation de vecteurs d’objets va être déterminée par le nombre de
paramètres du constructeur.

Voici un exemple d’initialisation d’un vecteur d’objet où le constructeur ne possède seulement


qu’un paramètre :

#include <iostream>
using namespace std;

class cl {
int i;
public:
cl(int j) { i=j; } // constructor
int get_i() { return i; }
};
int main()
{
cl ob[3] = {1, 2, 3}; // initializers
int i;

for(i=0; i<3; i++)

cout << ob[i].get_i() << "\n";

return 0;
}

Le programme affiche les résultats suivants:

1
2
3

L’exemple suivant illustre un cas d’initialisation d’un vecteur d’objets où le constructeur a deux
paramètres :

#include <iostream>
using namespace std;

class cl {
int h;
int i;
public:
cl(int j, int k) { h=j; i=k; } // constructor
int get_i() { return i; }
int get_h() { return h; }
};

int main()
{
cl ob[3] = {
cl(1, 2),
cl(3, 4),
cl(5, 6)
}; // initializers

int i;

for(i=0; i<3; i++) {


cout << ob[i].get_h();
cout << ", ";
cout << ob[i].get_i() << "\n";
}

return 0;
}
Pointeurs sur des objets
Le langage C++ permet l’utilisation de pointeurs sur des types scalaires, structurés et sur les
objets.

Pour accéder à un élément d’un objet, il faut utiliser l’opérateur (« . »).

Pour accéder à un élément d’un objet en se servant d’un pointeur sur cet objet, il faut utiliser
l’opérateur (« -> »).

L’exemple suivant crée la classe (« P_example ») et un objet (« ob ») de cette classe. Le


pointeur (« p ») pointe sur des objets de type (« P_example ») :

// A simple example using an object pointer.

#include <iostream>
using namespace std;

class P_example {
int num;
public:
void set_num(int val) { num = val; }
void show_num();
};

void P_example::show_num()
{
cout << num << "\n";
}

int main()
{
P_example ob, *p; // declare an object and pointer to it

ob.set_num(1); // access ob directly


ob.show_num();
p = &ob; // assign p the address of ob
p->show_num(); // access ob using pointer

return 0;
}
Lorsqu’un pointeur est incrémenté ou décrémenté, il pointe sur l’élément suivant ou précécent de
son type. L’incrémentation ou la décrémentation des pointeurs sur des objets donne des résultats
similaires par le pointage (référencement) à l’objet suivant ou précédent.

// Incrementing an object pointer


#include <iostream>
using namespace std;

class P_example {
int num;
public:
void set_num(int val) { num = val; }
void show_num();
};

void P_example::show_num()
{
cout << num << "\n";
}

int main()
{
P_example ob[2], *p;

ob[0].set_num(10); // access objects directly


ob[1].set_num(20);

p = &ob[0]; // obtain pointer to first element


p->show_num(); // show value of ob[0] using pointer

p++; // advance to next object


p->show_num(); // show value of ob[1] using pointer

p--; // retreat to previous object


p->show_num(); // again show value of ob[0]

return 0;
}

Les résultats de ce programme sont:

10
20
10
COURS 05

INF-11299 – PROGRAMMATION II

PROGRAMMATION C++
Référence : Schildt, Herbert & Guntle, Greg, Borland C++ Builder, The complete reference,
Osborne/McGraw-Hill, Berkeley, 2001, 977pp, ISBN: 0-07-212778-3.

SURCHARGE DES CONSTRUCTEURS


Bien qu’ayant un rôle particulier, les constucteurs ont des similarités avec les fonctions et les
méthodes. L’une de ces similarités est que les constructeurs peuvent être surchargés. Il s’agit de
déclarer les différentes variations. Ceci ajoute beaucoup de flexibilité aux programmes.

Dans l’exemple suivant, le constructeur est surchargé pour permettre de spécifier le temps du
chronomètre en secondes à l’aide d’un entier ou d’une chaîne de caractères. Nous pouvons
spécifier également le temps en minutes et en secondes à l’aide de deux entiers.

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

class timer{
int seconds;
public:
// seconds specified as a string
timer(char *t) { seconds = atoi(t); }

// seconds specified as integer


timer(int t) { seconds = t; }

// time specified in minutes and seconds


timer(int min, int sec) { seconds = min*60 + sec; }

void run();
} ;
void timer::run()
{
clock_t t1, t2;

t1 = t2 = clock()/CLK_TCK;
while(seconds) {
if(t1/CLK_TCK+1 <= (t2=clock())/CLK_TCK) {
seconds--;
t1 = t2;
}
}
cout << "\a"; // ring the bell
}

int main()
{
timer a(10), b("20"), c(1, 10);

a.run(); // count 10 seconds


b.run(); // count 20 seconds
c.run(); // count 1 minute, 10 seconds

return 0;
}

Endroit de la déclaration de variables


Il y a une différence entre le C et le C++ au niveau de la déclaration des variables. En C, la
déclaration des variables locales se fait au début d’un programme ou d’une fonction (bloc de
code) avant les instructions. Le langage C ne permet pas la déclaration d’une variable après une
instruction du langage.

L’exemple suivant est invalide en langage C parce que l’affectation de la variable « i » se situe
entre les déclarations des variables « i » et « j » :

/* Incorrect in C */
void f()
{
int i;
i = 10;
int j;
/* ... */
}

En langage C++, la fonction précédente est syntaxiquement correcte. En C++, les variables
peuvent être déclarées à n’importe quel endroit dans le code. En accord avec le principe
d’encapsulation des variables, le fait de déclarer des variables près de leur endroit d’utilisation
réduit les effets de bord.
#include <iostream>
#include <cstring>
using namespace std;

int main()
{
int i;
i = 10;

int j = 100; // perfectly legal in C++

cout << i*j << "\n";


cout << "Enter a string: ";
char str[80]; // also legal in C++
cin >> str;

// display the string in reverse order


int k; // in C++, declare k where it is needed
k = strlen(str);
k--;
while(k>=0) {
cout << str[k];
k--;
}

return 0;
}

Endroit de la déclaration des objets


Similairement aux variables, les objets peuvent être déclarés à n’importe quel endroit dans le
code. Ceci permet au programme de pouvoir connaître certaines valeurs qu’il doit fournir aux
constructeurs paramétrés. Ces valeurs peuvent parfoit être connues seulement qu’à l’exécution
du programme. Ceci évite la création d’objets non initialisés.

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

class timer{
int seconds;
public:
// seconds specified as a string
timer(char *t) { seconds = atoi(t); }

// seconds specified as integer


timer(int t) { seconds = t; }

// time specified in minutes and seconds


timer(int min, int sec) { seconds = min*60 + sec; }

void run();
} ;
void timer::run()
{
clock_t t1, t2;

t1 = t2 = clock()/CLK_TCK;
while(seconds) {
if(t1/CLK_TCK+1 <= (t2=clock())/CLK_TCK) {
seconds--;
t1 = t2;
}
}
cout << "\a"; // ring the bell
}
int main()
{
timer a(10);
a.run();

cout << "Enter number of seconds: ";


char str[80];
cin >> str;
timer b(str); // initialize at runtime using a string
b.run();

cout << "Enter minutes and seconds: ";


int min, sec;
cin >> min >> sec;
timer c(min, sec); /* initialize at runtime
using minutes and seconds */
c.run();

return 0;
}

Ambiguïté dans la surcharge des fonctions


Lors de la surcharge de fonctions, il est possible de produire des erreurs de compilation lors de la
déclaration de fonctions. Il peut se produire des situations dans lesquelles le compilateur C++ est
incapable de choisir entre plusieurs fonctions surchargées. Lorsque ce cas se produit, la situation
est considérée comme ambiguë et une erreur de compilation se produit. Une des causes
principales de l’ambiguïté est la conversion automatique des types des arguments des fonctions.
Prenons par exemple le fragment de code suivant :

int myfunc(double d);

// .....
cout << myfunc('c'); // not an error, conversion applied

Ce fragment de code ne comporte pas d’erreurs parce que le compilateur C++ convertit le
caractère « c » en réels « double ». Les différentes conversions automatiques du C++ peuvent
causer des ambiguïtés dans l’appel des fonctions surchargées. Dans l’exemple suivant, le
compilateur ne sait pas s’il faut convertir l’entier 10 en « float » ou « double » :
#include <iostream>
using namespace std;

float myfunc(float i);


double myfunc(double i);

int main()
{
cout << myfunc(10.1) << " "; // unambiguous, calls myfunc(double)
cout << myfunc(10); // ambiguous

return 0;
}

float myfunc(float i)
{
return i;
}

double myfunc(double i)
{
return -i;
}
Cet exemple illustre l’ambiguïté du choix du compilateur lorsque le paramètre entier « 88 » est
fourni en entrée à la fonction « myfunc ». Le compilateur ne sait pas choisir entre la fonction
ayant « char » ou « unsigned char » comme paramètres :

#include <iostream>
using namespace std;

char myfunc(unsigned char ch);


char myfunc(char ch);

int main() {
cout << myfunc('c'); // this calls myfunc(char)
cout << myfunc(88) << " "; // ambiguous
return 0;
}

char myfunc(unsigned char ch) {


return ch-1;
}

char myfunc(char ch) {


return ch+1;
}

Les ambiguïtés peuvent également provenir des paramètres des fonctions initialisés par défaut.
Ici, à l’appel de la fonction avec un seul paramètre, le compilateur est indécis quant au traitement
du deuxième paramètre :
#include <iostream>
using namespace std;

int myfunc(int i);


int myfunc(int i, int j=1);

int main() {
cout << myfunc(4, 5) << " "; // unambiguous
cout << myfunc(10); // ambiguous

return 0;
}

int myfunc(int i) {
return i;
}

int myfunc(int i, int j) {


return i*j;
}

Détermination de l’adresse d’une fonction surchargée


En C++, il est facile à un pointeur de pointer sur une fonction non surchargée. Lorsque la fonction
est surchargée, il est plus difficile de pointer sur les fonctions.

Voici un programme définissant une fonction « myfunc » surchargée et un pointeur « fp » qui


pointe sur une fonction à un argument retournant un entier :

int (*fp) (int a);

pour pointer à une fonction ayant deux arguments, il aurait fallu faire la déclaration suivante:

int (*fp) (int a, int b)

Lorsqu’un pointeur référence une fonction surchargée, c’est la déclaration du pointeur sur la
fonction qui détermine l’adresse du pointeur.
#include <iostream>
using namespace std;

int myfunc(int a);


int myfunc(int a, int b);

int main()
{
int (*fp)(int a); // pointer to int xxx(int)

fp = myfunc; // points to myfunc(int)


cout << fp(5);

return 0;
}

int myfunc(int a)
{
return a;
}

int myfunc(int a, int b)


{
return a*b;
}

Le pointeur à l’objet appelant (« this »)


L’un des mots réservés le plus important est le pointeur (« this ») servant à la surcharge
d’opérateurs.

Lors de l’appel d’une fonction, le pointeur (« this ») permet de pointer et de modifier l’objet
appelant. Ce pointeur est un paramètre implicite à tous les fonctions sauf les fonctions amies
(« friend »).

Soit l’appel de la méthode suivante :

ob.f();

un paramètre (« this ») pointant sur « ob » est passé automatiquemant à la fonction « f() ».

Une méthode peut accéder aux attributs de sa classe directement. Soit la classe suivante :

class cl {
int i;
// ....
};
une méthode peut affecter la variable « i » de la façon suivante :

i = 10;

qui équivaut à

this->i = 10;

Le programme suivant en illustre un exemple :

#include <iostream>
using namespace std;

class cl {
int i;
public:
void load_i(int val) { this->i = val; } // same as i = val
int get_i() { return this->i; } // same as return i
} ;

int main()
{
cl o;

o.load_i(100);
cout << o.get_i();

return 0;
}

Ce programme affiche la valeur 100.

Surcharge des opérateurs


Une des caractéristiques du langage C++ se rapportant à la surcharge des fonctions est la
surcharge des opérateurs. Lorsqu’un opérateur est surchargé, sa définition initiale est conservée.

Pour surcharger un opérateur, vous devez définir la signification de la méthode appliquée sur la
classe. Une fonction d’opérateur définissant l’action doit être spécifiée de la façon suivante :

type classname ::operator#(arg-list)


{
// operation defined relative to the class
}
Voici un exemple faisant la surcharge des opérateurs + et = :

#include <iostream>
using namespace std;

class three_d {
int x, y, z; // 3-d coordinates
public:
three_d operator+(three_d t);
three_d operator=(three_d t);

void show() ;
void assign(int mx, int my, int mz);
} ;

// Overload the +.
three_d three_d::operator+(three_d t)
{
three_d temp;

temp.x = x+t.x;
temp.y = y+t.y;
temp.z = z+t.z;
return temp;
}

// Overload the =.
three_d three_d::operator=(three_d t)
{
x = t.x;
y = t.y;
z = t.z;
return *this;
}

// Show X, Y, Z coordinates.
void three_d::show()
{
cout << x << ", ";
cout << y << ", ";
cout << z << "\n";
}

// Assign coordinates.
void three_d::assign(int mx, int my, int mz)
{
x = mx;
y = my;
z = mz;
}
int main()
{
three_d a, b, c;

a.assign(1, 2, 3);
b.assign(10, 10, 10);

a.show();
b.show();

c = a+b; // now add a and b together


c.show();

c = a+b+c; // add a, b and c together


c.show();

c = b = a; // demonstrate multiple assignment


c.show();
b.show();

return 0;
}

Le programme produit les résultats suivants:

1, 2, 3
10, 10, 10
12, 12, 13
22, 24, 26
1, 2, 3
1, 2, 3

Surcharge des opérateurs « ++ » et « -- »


#include <iostream>
using namespace std;

class three_d {
int x, y, z; // 3-d coordinates
public:
three_d operator+(three_d op2); // op1 is implied
three_d operator=(three_d op2); // op1 is implied
three_d operator++(); // op1 is also implied here

void show() ;
void assign(int mx, int my, int mz);
} ;
// Overload the +.
three_d three_d::operator+(three_d op2)
{
three_d temp;

temp.x = x+op2.x; // these are integer additions


temp.y = y+op2.y; // and the + retains its original
temp.z = z+op2.z; // meaning relative to them
return temp;
}

// Overload the =.
three_d three_d::operator=(three_d op2)
{
x = op2.x; // these are integer assigments
y = op2.y; // and the = retains its original
z = op2.z; // meaning relative to them
return *this;
}

// Overload a unary operator.


three_d three_d::operator++()
{
x++;
y++;
z++;
return *this;
}

// Show X, Y, Z coordinates.
void three_d::show()
{
cout << x << ", ";
cout << y << ", ";
cout << z << "\n";
}

// Assign coordinates.
void three_d::assign(int mx, int my, int mz) {
x = mx;
y = my;
z = mz;
}

int main() {
three_d a, b, c;

a.assign(1, 2, 3);
b.assign(10, 10, 10);

a.show();
b.show();
c = a+b; // now add a and b together
c.show();

c = a+b+c; // add a, b and c together


c.show();

c = b = a; // demonstrate multiple assignment


c.show();
b.show();

++c; // increment c
c.show();
return 0;
}

Lors de la définition du surchargement des opérateurs, il faut respecter certaines règles :

¾ la précédence des opérateurs n’est pas modifiable,

¾ le nombre d’opérandes de l’opérateur n’est pas modifiable,

¾ les opérateurs surchargés peuvent être hérités des classes dérivées sauf l’opérateur
« = ». Chaque classe dérivée doit en effet définir son propre opérateur « = » surchargé,

¾ chaque classe dérivée peut surcharger des opérateurs pour ses propres applications; et

¾ les seuls opérateurs non surchargeables sont : « . », « :: », « .* » et « ? ».

Définition d’opérateurs communs


Il est possible pour une fonction d’opérateur d’être commune ou attachée à une classe
(« friend ») au lieu d’en être membre. Les fonctions communes, non membre des classes n’ont
pas le paramètre implicite (« this »).

Lorsqu’une fonction commune est utilisée pour surcharger un opérateur, un paramètre est utilisé
dans le cas des fonctions à un argument (monadiques) et deux paramètres sont utilisés dans le
cas des fonctions à deux arguments (dyadiques).

Les seuls opérateurs ne pouvant pas être définis par des fonctions communes sont : « = », « () »,
« [ ] » et « -> ».
L’exemple suivant définit une fonction commune redéfinissant l’opérateur « + » :

#include <iostream>
using namespace std;

class three_d {
int x, y, z; // 3-d coordinates
public:
friend three_d operator+(three_d op1, three_d op2);
three_d operator=(three_d op2); // op1 is implied
three_d operator++(); // op1 is implied here, too

void show() ;
void assign(int mx, int my, int mz);
} ;

// This is now a friend function.


three_d operator+(three_d op1, three_d op2)
{
three_d temp;

temp.x = op1.x + op2.x; // these are integer additions


temp.y = op1.y + op2.y; // and the + retains its original
temp.z = op1.z + op2.z; // meaning relative to them
return temp;
}

// Overload the =.
three_d three_d::operator=(three_d op2) {
x = op2.x; // these are integer assignments
y = op2.y; // and the = retains its original
z = op2.z; // meaning relative to them
return *this;
}

// Overload a unary operator.


three_d three_d::operator++() {
x++;
y++;
z++;
return *this;
}

// Show X, Y, Z coordinates.
void three_d::show() {
cout << x << ", ";
cout << y << ", ";
cout << z << "\n";
}
// Assign coordinates.
void three_d::assign(int mx, int my, int mz) {
x = mx;
y = my;
z = mz;
}

int main()
{
three_d a, b, c;

a.assign(1, 2, 3);
b.assign(10, 10, 10);

a.show();
b.show();

c = a+b; // now add a and b together


c.show();

c = a+b+c; // add a, b and c together


c.show();

c = b = a; // demonstrate multiple assignment


c.show();
b.show();

++c; // increment c
c.show();

return 0;
}
Un cas particulier de l’utilisation des fonctions amies est de permettre des opérations sur un type
structuré lorsque ce type structuré est du côté gauche de l’opération. L’exemple suivant illustre le
surchargement de l’opérateur d’addition pour permettre les opérations suivantes :

résultat = objet + entier; et résultat = entier + objet;

#include <iostream>
using namespace std;

class CL {
public:
int count;
CL operator=(int i);
friend CL operator+(CL ob, int i);
friend CL operator+(int i, CL ob);
};

CL CL::operator=(int i) {
count = i;
return *this;
}
// This handles ob + int.
CL operator+(CL ob, int i) {
CL temp;

temp.count = ob.count + i;
return temp;
}

// This handles int + ob.


CL operator+(int i, CL ob) {
CL temp;

temp.count = ob.count + i;
return temp;
}

int main() {
CL obj;
obj = 10;
cout << obj.count << " "; // outputs 10

obj = 10 + obj; // add object to integer


cout << obj.count << " "; // outputs 20

obj = obj + 12; // add integer to object


cout << obj.count; // outputs 32
return 0;
}

Passage de paramètre par référence aux fonctions (par adresse)


Exemple classique du langage C :

#include <iostream>
using namespace std;

void swap(int *a, int *b);

int main()
{
int x, y;

x = 99;
y = 88;

cout << x << " " << y << "\n";

swap(&x, &y); // exchange their values

cout << x << " " << y << "\n";

return 0;
}
// C-like, explicit pointer version of swap().
void swap(int *a, int *b)
{
int t;

t = *a;
*a = *b;
*b = t;
}

En langage C++, le passage par adresse se fait à l’aide de paramètres de référence :

#include <iostream>
using namespace std;

void swap(int &a, int &b); // declare as reference parameters

int main()
{
int x, y;

x = 99;
y = 88;

cout << x << " " << y << "\n";

swap(x, y); // exchange their values

cout << x << " " << y << "\n";

return 0;
}

/* Here, swap() is defined as using call-by-reference,


not call-by-value. */
void swap(int &a, int &b)
{
int t;

t = a;
a = b; // this swaps x
b = t; // this swaps y
}

Il n’est pas nécessaire de précéder les paramètres donnés en entrée à la fonction pas l’opérateur
« & » et de placer l’opérateur « * » dans les paramètres de sortie de la fonction « swap ».
Passage d’objets par référence
Le passage par valeur fournit une copie de l’objet à la fonction ou à la méthode afin que l’objet
initial reste intact. Dans le cas d’un passage par référence, l’adresse de l’objet est donnée à la
fonction. L’objet n’est pas détruit, mais il est modifié. Le constructeur et le destructeur de l’objet
ne sont pas activés lors de l’appel de la fonction.

#include <iostream>
using namespace std;

class cl {
int id;
public:
int i;
cl(int i);
~cl();
void neg(cl &o) {o.i = -o.i;}
};

cl::cl(int num)
{
cout << "Constructing " << num << "\n";
id = num;
}

cl::~cl()
{
cout << "Destructing " << id << "\n";
}

int main()
{
cl o(1);

o.i = 10;
o.neg(o);
cout << o.i << "\n";

return 0;
}

Voici les résultats du programme:

Constructing 1
-10
Destructing 1
Fonctions ou méthodes retournant des références (des adresses ou des
pointeurs)
#include <iostream>
using namespace std;

char &replace(int i); // return a reference


char s[80] = "Hello There";

int main()
{

replace(5) = 'X'; // assign X to space after Hello


cout << s;

return 0;
}

char &replace(int i)
{
return s[i];
}

Ce programme remplace l’espace entre “Hello” et “There” par un “X”. Le programme produit donc
la sortie :

HelloXThere

Variables d’adresses indépendantes (pointeurs)

Le langage C++ supporte la déclaration de variables de pointeurs indépendantes comme en


langage Pascal.

Un pointeur non initialisé est considéré comme étant flou. Il pointe à une valeur indéterminée en
mémoire. Pour s’en servir, il faut initialiser l’adresse du pointeur à celle d’un objet ou d’une
variable déclarée :

#include <iostream>
using namespace std;

int main()
{
int j, k;
int &i = j; // independent reference to j

j = 10;
cout << j << " " << i; // outputs 10 10

k = 121;
i = k; // copies k's value into j -- not k's address
cout << "\n" << j; // outputs 121

return 0;
}
Ce programme donne la sortie suivante:

10 10
121

Surchargement d’un opérateur monadique en utilisant le passage par référence

// This version uses a friend operator++() function.


#include <iostream>
using namespace std;

class three_d {
int x, y, z; // 3-d coordinates
public:
friend three_d operator+(three_d op1, three_d op2);
three_d operator=(three_d op2); // op1 is implied
// use a reference to overload the ++
friend three_d operator++(three_d &op1);

void show() ;
void assign(int mx, int my, int mz);
} ;

// This is now a friend function.


three_d operator+(three_d op1, three_d op2)
{
three_d temp;

temp.x = op1.x + op2.x; // these are integer additions


temp.y = op1.y + op2.y; // and the + retains its original
temp.z = op1.z + op2.z; // meaning relative to them
return temp;
}

// Overload the =.
three_d three_d::operator=(three_d op2)
{
x = op2.x; // these are integer assigments
y = op2.y; // and the = retains its original
z = op2.z; // meaning relative to them
return *this;
}

/* Overload a unary operator using a friend function.


This requires the use of a reference parameter. */
three_d operator++(three_d &op1)
{
op1.x++;
op1.y++;
op1.z++;
return op1;
}
// Show X, Y, Z coordinates.
void three_d::show()
{
cout << x << ", ";
cout << y << ", ";
cout << z << "\n";
}

// Assign coordinates.
void three_d::assign(int mx, int my, int mz)
{
x = mx;
y = my;
z = mz;
}

int main()
{
three_d a, b, c;
a.assign(1, 2, 3);
b.assign(10, 10, 10);

a.show();
b.show();

c = a+b; // now add a and b together


c.show();

c = a+b+c; // add a, b and c together


c.show();

c = b = a; // demonstrate multiple assignment


c.show();
b.show();

++c; // increment c
c.show();

return 0;
}

Surcharge de l’opérateur d’indicage « [ ] »


En C++, l’opérateur « [ ] » est considéré comme étant dyadique si surchargé. Cet opérateur doit
être surchargé par une fonction membre de la classe et non une fonction commune « friend ». La
forme générale est la suivante :

type class-name ::operator[ ] (int i)


{
// ....
}

Dans l’exemple suivant, la classe « atype » déclare un vecteur de trois éléments. La fonction
surchargée de l’index retourne les éléments du vecteur correspondant à l’index :
#include <iostream>
using namespace std;

class atype {
int a[3];
public:
atype(int i, int j, int k) {
a[0] = i;
a[1] = j;
a[2] = k;
}
int operator[](int i) { return a[i]; }
};

int main()
{
atype ob(1, 2, 3);

cout << ob[1]; // displays 2

return 0;
}

L’exemple suivant fait en sorte que nous pouvons lire ou modifier les éléments de la classe:

#include <iostream>
using namespace std;

class atype {
int a[3];
public:
atype(int i, int j, int k) {
a[0] = i;
a[1] = j;
a[2] = k;
}
int &operator[](int i) { return a[i]; }
};

int main()
{
atype ob(1, 2, 3);

cout << ob[1]; // displays 2


cout << " ";

ob[1] = 25; // [] on left of =


cout << ob[1]; // now displays 25

return 0;
}
Le programme suivant vérifie les limites du vecteur avant son accès :

// A safe array example.


#include <iostream>
#include <cstdlib>
using namespace std;

class atype {
int a[3];
public:
atype(int i, int j, int k) {
a[0] = i;
a[1] = j;
a[2] = k;
}
int &operator[](int i);
};

// Provide range checking for atype.


int &atype::operator[](int i)
{
if(i<0 || i> 2) {
cout << "Boundary Error\n";
exit(1);
}
return a[i];
}

int main()
{
atype ob(1, 2, 3);

cout << ob[1]; // displays 2


cout << " ";

ob[1] = 25; // [] appears on left


cout << ob[1]; // displays 25

ob[3] = 44; // generates runtime error, 3 out-of-range


return 0;
}
Exemple de synthèse définissant une chaîne de caractères :
// Expanding the string type.
#include <iostream>
#include <cstring>
using namespace std;

class str_type {
char string[80];
public:
str_type(char *str = "\0") { strcpy(string, str); }

str_type operator+(str_type str);


str_type operator+(char *str);

str_type operator=(str_type str);


str_type operator=(char *str);

void show_str() { cout << string; }


} ;

str_type str_type::operator+(str_type str) {


str_type temp;

strcpy(temp.string, string);
strcat(temp.string, str.string);
return temp;
}

str_type str_type::operator=(str_type str) {


strcpy(string, str.string);
return *this;
}

str_type str_type::operator=(char *str)


{
str_type temp;

strcpy(string, str);
strcpy(temp.string, string);
return temp;
}

str_type str_type::operator+(char *str)


{
str_type temp;

strcpy(temp.string, string);
strcat(temp.string, str);
return temp;
}
int main()
{
str_type a("Hello "), b("There"), c;

c = a + b;
c.show_str();
cout << "\n";

a = "to program in because";


a.show_str();
cout << "\n";

b = c = "C++ is fun";
c = c+" "+a+" "+b;
c.show_str();

return 0;
}
COURS 06

INF-11299 – PROGRAMMATION II

PROGRAMMATION C++
Référence : Schildt, Herbert & Guntle, Greg, Borland C++ Builder, The complete reference,
Osborne/McGraw-Hill, Berkeley, 2001, 977pp, ISBN: 0-07-212778-3.

Reisdorph, Kent, Using properties in C++ classes

Duncan, Jeff, Using Builder’s __property extension,


jduncan@computer-guidance.com

HÉRITAGE ET POLYMORPHISME
L’héritage et le polymorphisme sont deux aspects essentiels d’un langage orienté objet.

L’héritage permet la création de structure hiérarchisées. L’héritage permet la création d’une


classe générale définissant les traits communs d’un ensemble d’items. Cette classe est ensuite
héritée de d’autres classes plus spécifiques ajoutant uniquement les aspects spécifiques à la
classe dérivée.

L’héritage est également important pour une autre raison qui est le support du polymorphisme à
l’exécution.

Le polymorphisme peut être parfois défini par la phrase suivante : « Une interface, plusieurs
méthodes ». Ceci signifie qu’une classe générale d’opérations soit accédée de la même manière
en dépit des différences entre les actions spécifiques associées à chaque opération.

Dans le langage C++, le polymorphisme est supporté à l’exécution et à la compilation. La


surcharge des opérateurs et des fonctions sont des exemples de polymorphisme exécuté à la
compilation. Bien que ces opérations soient très efficaces et puissantes, elles ne peuvent pas
supporter toutes les opérations d’un langage orienté-objet.

C’est pourquoi le langage C++ supporte le polymorphisme à l’exécution par l’usage des classes
dérivées et des fonctions virtuelles.

L’héritage et les descripteurs d’accès


Nous allons maintenant établir une relation entre l’encapsulation et l’héritage par l’entremise des
descripteurs d’accès du C++.
Établissons les définitions suivantes :

¾ une classe qui est héritée par une autre classe est nommée « classe de base », « classe
parente » ou « superclasse »; et

¾ une classe recevant des attributs ou des méthodes par héritage est appelée « classe
dérivée », « classe descendante » ou sous-classe ».

Identification des descripteurs d’accès


Dans le langage C++, une classe peut catégoriser ses membres en trois classifications :

¾ public : un attribut, une fonction ou une méthode déclarée publique peut être accédée par
toute autre fonction ou méthode dans le programme,

¾ private : un attribut, une fonction ou une méthode déclarée privé peut être seulement
accédé par les fonctions, les méthodes et les fonctions communes de sa classe; et

¾ protected : un attribut, une fonction ou une méthode déclarée protégé est similaire à la
déclaration privée sauf pour l’héritage.

Lorsqu’une classe hérite d’une autre classe, tous les items publics de la classe de base
deviennent des items publics de la classe dérivée et deviennent accessibles aux fonctions et
méthodes de la classe dérivée. Cependant, les items privés d’une classe de base sont
inaccessibles aux classes dérivées comme dans l’exemple suivant :

class X {
int i;
int j;
public:
void get_ij();
void put_ij();
} ;

class Y : public X {
int k;
public:
int get_k();
void make_k();
} ;

Dans cet exemple, la classe « Y » hérite et peut accéder aux fonctions publiques de la classe
« X » : « get_ij() » et « put_ij() », mais ne peut pas accéder aux variables « i » et « j » parce
qu’elles sont déclarées privées dans la classe « X ». Dans tous les cas, un item privé reste privé
à la classe dans laquelle il est déclaré. Les items privés ne participent donc pas à l’héritage.
Le fait que les items privés ne peuvent pas être hérités soulève une question : Que devons-nous
faire pour conserver un item privé et qu’il soit cependant accessible aux classes dérivées?

Cette lacune est comblée par le mot réservé « protected ». Un item protégé est similaire à un
item privé sauf qu’il est accessible par les classes dérivées. Le fait de déclarer un item protégé
fait en sorte qu’il est accessible seulement dans la hiérarchie des classes comme dans l’exemple
suivant :

class X {
protected:
int i;
int j;
public:
void get_ij();
void put_ij();
} ;

class Y : public X {
int k;
public:
int get_k();
void make_k();
} ;

Ici, la classe « Y » peut accéder aux éléments « i » et « j » même si ceux-ci sont inaccessibles au
reste des items du programme.

Lorsqu’un item est déclaré protégé, son accès est restreint sauf aux classes héritées. Lorsqu’un
item est déclaré privé, il est inaccessible aux classes héritées et au reste du programme.

Les descripteurs d’accès peuvent être répétés ou placés dans n’importe quel ordre. Il est
cependant recommandé par principe de bonne programmation et de clareté du code de ne pas
répéter les descripteurs.

class my_class {
protected:
int i;
int j;
public:
void f1();
void f2();
protected:
int a;
public:
int b;
} ;
Contrôle de l’accès à la classe de base
La définition de l’héritage d’une classe dérivée affecte le statut des membres hérités. La forme
générale de la déclaration de l’héritage est :

class class-name : access class-name {


// ....
};

Ici, access détermine les modalités de l’héritage de la classe dérivée et doit être défini comme
étant : « private », « public » ou « protected ».

Si l’accès est public, tous les items publics et protégés de la classe de base deviennent des
items publics et protégés de la classe dérivée.

Si l’accès est privé, tous les items publics et protégés de la classe de base deviennent des items
privés de la classe dérivée.

Si l’accès est protégé, tous les items publics et protégés de la classe de base deviennent des
items protégés de la classe dérivée.

L’exemple suivant illustre ce concept :

#include <iostream>
using namespace std;

class X {
protected:
int i;
int j;
public:
void get_ij() {
cout << "Enter two numbers: ";
cin >> i >> j;
}
void put_ij() { cout << i << " " << j << "\n"; }
} ;

// In Y, i and j of X become protected members.


class Y : public X {
int k;
public:
int get_k() { return k; }
void make_k() { k = i*j; }
} ;

/* Z has access to i and j of X, but not to


k of Y, since it is private. */
class Z : public Y {
public:
void f();
} ;
// i and j are accessible here
void Z::f()
{
i = 2; // ok
j = 3; // ok
}

int main()
{
Y var;
Z var2;

var.get_ij();
var.put_ij();

var.make_k();
cout << var.get_k();
cout << "\n";

var2.f();
var2.put_ij();

return 0;
}

La déclaration de l’héritage de la classe « Y » étant publique par rapport à la classe « X » fait en


sorte que les éléments protégés de la classe « X » deviennent des éléments protégés de la
classe « Y ». Ces éléments peuvent être hérités par la classe « Z ».

Si nous changeons la déclaration de l’héritage de la classe « Y » étant privée par rapport à la


classe « X », la classe « Z » ne peut plus accéder aux variables « i » et « j » :

#include <iostream>
using namespace std;

class X {
protected:
int i;
int j;
public:
void get_ij() {
cout << "Enter two numbers: ";
cin >> i >> j;
}
void put_ij() { cout << i << " " << j << "\n"; }
} ;

// Now, i and j are converted to private members of Y.


class Y : private X {
int k;
public:
int get_k() { return k; }
void make_k() { k = i*j; }
} ;
/* Because i and j are private in Y, they
cannot be inherited by Z. */

class Z : public Y {
public:
void f();
} ;

// This function no longer works.


void Z::f()
{
// i = 2; i and j are no longer accessible
// j = 3;
}

int main()
{
Y var;
Z var2;

// var.get_ij(); no longer accessible


// var.put_ij(); no longer accessible

var.make_k();
cout << var.get_k();
cout << "\n";

var2.f();
// var2.put_ij(); no longer accessible

return 0;
}

Les constructeurs et les destructeurs des classes dérivées


Il est possible pour une classe de base et une classe dérivée d’avoir chacune un constructeur.
Lorsqu’une classe de base possède un constructeur, le constructeur de la classe de base est
exécuté avant le constructeur de la classe dérivée :

#include <iostream>
using namespace std;

class Base {
public:
Base() { cout << "\nBase created\n"; }
};

class D_class1 : public Base {


public:
D_class1() { cout << "D_class1 created\n"; }
};
int main()
{
D_class1 d1;

// do nothing but execute constructors


return 0;
}

Ce programme crée un objet de type « D_class1 » et produit les résultats suivants:

Base created
D_class1 created

Les destructeurs des classes dérivées sont exécutés avant celui de la classe de base:

#include <iostream>
using namespace std;

class Base {
public:
Base() { cout << "\nBase created\n"; }
~Base() { cout << "Base destroyed\n\n"; }
};

class D_class1 : public Base {


public:
D_class1() { cout << "D_class1 created\n"; }
~D_class1() { cout << "D_class1 destroyed\n"; }
};

int main() {
D_class1 d1;

cout << "\n";


return 0;
}
Le programme produit les sorties suivantes:

Base created
D_class1 created

D_class1 created
Base destroyed

En général, les constructeurs sont exécutés dans leur ordre de dérivation et les destructeurs
dans l’ordre inverse.
#include <iostream>
using namespace std;

class Base {
public:
Base() { cout << "\nBase created\n"; }
~Base() { cout << "Base destroyed\n\n"; }
};

class D_class1 : public Base {


public:
D_class1() { cout << "D_class1 created\n"; }
~D_class1() { cout << "D_class1 destroyed\n"; }
};

class D_class2 : public D_class1 {


public:
D_class2() { cout << "D_class2 created\n"; }
~D_class2() { cout << "D_class2 destroyed\n"; }
};

int main()
{
D_class1 d1;
D_class2 d2;

cout << "\n";

return 0;
}

Le programme produit la sortie suivante:

Base created
D_class1 created

Base created
D_class1 created
D_class2 created

D_class2 destroyed
D_class1 destroyed
Base destroyed

D_class1 destroyed
Base destroyed
Héritage multiple
Il est possible à une classe d’hériter des attributs de deux ou plusieurs classes dans la même
déclaration (en même temps).

Une déclaration d’héritage multiple se fait selon le format suivant :

class derived-class-name : base-class list


{
// ....
};

L’exemple suivant illustre que la classe Z hérite des classes X et Y:

#include <iostream>
using namespace std;

class X {
protected:
int a;
public:
void make_a(int i) { a = i; }
};

class Y {
protected:
int b;
public:
void make_b(int i) { b = i; }
} ;

// Z inherits both X and Y


class Z : public X, public Y {
public:
int make_ab() { return a*b; }
} ;

int main()
{
Z i;

i.make_a(10);
i.make_b(12);
cout << i.make_ab();

return 0;
}
Voici un cas d’héritage multiple ou toutes les classes ont des constructeurs:

#include <iostream>
using namespace std;

class X {
protected:
int a;
public:
X() {
a = 10;
cout << "Initializing X\n";
}
};

class Y {
protected:
int b;
public:
Y() {
cout << "Initializing Y\n";
b = 20;
}
} ;

// Z inherits both X and Y


class Z : public X, public Y {
public:
Z() { cout << "Initializing Z\n"; }
int make_ab() { return a*b; }
} ;

int main()
{
Z i;
cout << i.make_ab();
return 0;
}

Le programme donne les résultats suivants:

Initializing X
Initializing Y
Initializing Z
200

En général, lorsqu’une liste de classes de base est utilisée, les constructeurs sont appelés dans
l’ordre de leur déclaration de gauche à droite et les destructeurs de droite à gauche.
Passage de paramètres à une classe de base
Lorsque le constructeur d’une classe de base a des arguments, les classes dérivées doivent
passer les arguments nécessaires à ce constructeur.

La déclaration de constructeurs paramétrés est la suivante :

derived-constructor(arg-list) : base1(arg-list), base2(arg-list), …, baseN(arg-list)


{
// ...
}

Ici, base1, base2, ..., baseN sont les noms des classes de base héritées par la classe dérivée.
Le symbole « : » est utilisé pour séparer la liste de paramètres du constructeur des classes de
base héritées. Les listes de base associées avec les classes de base peuvent contenir des
constantes, des variables globales ou des paramètres du constructeur de la classe dérivée. Étant
donné que l’initialisation de l’objet se produit à l’exécution, toute variable accessible peut être
utilisée.

Le programme suivant illustre la manière de passer des arguments aux classes de base d’une
classe dérivée :

#include <iostream>
using namespace std;

class X {
protected:
int a;
public:
X(int i) { a = i; }
};

class Y {
protected:
int b;
public:
Y(int i) { b = i; }
} ;

// Z inherits both X and Y

class Z : public X, public Y {


public:
/* Initialize X and Y via Z's constructor.
Notice that Z does not actually use x or y
itself, but it could, if it so chooses. */
Z(int x, int y) : X(x), Y(y)
{
cout << "Initializing\n";
}
int make_ab() { return a*b; }
} ;
int main()
{
Z i(10, 20);

cout << i.make_ab();

return 0;
}

Pointeurs et références aux types dérivés


En C++, un pointeur sur une classe de base peut pointer sur un objet d’une classe dérivée.

B_class *p; // pointer to object of type B_class


B_class B_ob; // object of type B_class
D_class D_ob; // object of type D_class

Selon ces affirmations, les enoncés suivants sont valides:

p = &B_ob; // p points to object of type B_class

p = &D_ob; /* p points to object of type D_class,


which is an object derived from B_class. */

Le programme suivant définit une classe de base appelée « B_class » et une classe dérivée
s’appelant « D_class ». La classe dérivée implante un petit répertoire de numéros de téléphone:

// Using pointers on derived class objects.

#include <iostream>
#include <cstring>
using namespace std;

class B_class {
char name[80];
public:
void put_name(char *s) { strcpy(name, s); }
void show_name() { cout << name << " "; }
} ;

class D_class : public B_class {


char phone_num[80];
public:
void put_phone(char *num) {
strcpy(phone_num, num);
}
void show_phone() { cout << phone_num << "\n"; }
};
int main()
{
B_class *p;
B_class B_ob;

D_class *dp;
D_class D_ob;

p = &B_ob; // address of base

// Access B_class via pointer.


p->put_name("Thomas Edison");

// Access D_class via base pointer.


p = &D_ob;
p->put_name("Albert Einstein");

// Show that each name went into proper object.


B_ob.show_name();
D_ob.show_name();
cout << "\n";

/* Since put_phone and show_phone are not part of the


base class, they are not accessible via the base
pointer p and must be accessed either directly,
or, as shown here, through a pointer to the
derived type.
*/
dp = &D_ob;
dp->put_phone("555 555-1234");
p->show_name(); // either p or dp can be used in this line
dp->show_phone();

return 0;
}

Dans cet exemple, le pointeur « p » est défini comme un pointeur sur la classe « b ». Cependant,
ce pointeur peut pointer sur un objet de la classe dérivée « D_class » et peut être utilisé pour
accéder les éléments de la classe dérivée qui sont définis dans la classe de base.

Un pointeur pointant sur la classe de base ne peut accéder les éléments spécifiés dans les
classes dérivées sans la conversion de type (« type cast »). C’est pourquoi la méthode
« show_phone() » est accédée en utilisant le pointeur « dp », qui est un pointeur sur la classe
dérivée.

L’accès par un pointeur de base aux éléments définis par un type dérivé se fait par la conversion
du type du pointeur de base dans le type dérivé. Cette ligne de code appelle la méthode
« show_phone() » de l’objet « D-ob » :

((D_class *)p)->show_phone();
Les fonctions virtuelles
L’accomplissement du polymorphisme à l’exécution est réalisé par l’utilisation des types dérivés
et des fonctions virtuelles.

Une fonction virtuelle est une fonction déclarée virtuelle dans sa classe de base et redéfinie dans
une ou plusieurs de ses classes dérivées. Les fonctions virtuelles sont spéciales parce que la
décision d’appel d’une fonction ou d’une méthode déterminée est décidé à l’exécution par le
programme C++ lors d’un appel de ces fonctions par une classe de base.

Lorsque différents objets dérivés d’une classe virtuelle sont référencés, différentes versions de la
fonction virtuelle sont exécutées.

Une classe contenant une ou plusieurs fonctions virtuelles sont appelées des « classes
polymorphiques ».

Une fonction virtuelle est déclarée comme étant virtuelle dans sa classe de base en précédant sa
déclaration par le mot réservé « virtual ». Lorsqu’une fonction virtuelle est redéfinie dans une
classe dérivée, le mot réservé « virtual » ne doit pas être répété.

Voici un exemple simple de fonctions virtuelles :

// A short example that uses virtual functions.


#include <iostream>
using namespace std;

class Base {
public:
virtual void who() { // specify a virtual function
cout << "Base\n";
}
};

class first_d : public Base {


public:
void who() { // define who() relative to first_d
cout << "First derivation\n";
}
};

class second_d : public Base {


public:
void who() { // define who() relative to second_d
cout << "Second derivation\n";
}
};
int main()
{
Base base_obj;
Base *p;
first_d first_obj;
second_d second_obj;

p = &base_obj;
p->who(); // access Base's who

p = &first_obj;
p->who(); // access first_d's who

p = &second_obj;
p->who(); // access second_d's who

return 0;
}

Le programme produit les résultats suivants:

Base
First derivation
Second derivation

Les classes de base appellent généralement les fonctions virtuelles à l’aide de paramètres de
fonctions :

/* Here, a base class reference is used to access


a virtual function. */
#include <iostream>
using namespace std;

class Base {
public:
virtual void who() { // specify a virtual function
cout << "Base\n";
}
};

class first_d : public Base {


public:
void who() { // define who() relative to first_d
cout << "First derivation\n";
}
};

class second_d : public Base {


public:
void who() { // define who() relative to second_d
cout << "Second derivation\n";
}
};
// Use a base class reference parameter.
void show_who(Base &r) {
r.who();
}

int main()
{
Base base_obj;
first_d first_obj;
second_d second_obj;

show_who(base_obj); // access Base's who


show_who(first_obj); // access first_d's who
show_who(second_obj); // access second_d's who

return 0;
}

Ré-écriture de fonctions virtuelles (« overriding »)


Le terme ré-écriture (« overriding ») est utilisé pour décrire la redéfinition de fonctions virtuelles.

Lorsqu’une classe dérivée ne redéfinit pas une fonction virtuelle, la version de la fonction dans la
classe de base est utilisée :

#include <iostream>
using namespace std;

class Base {
public:
virtual void who() {
cout << "Base\n";
}
};

class first_d : public Base {


public:
void who() {
cout << "First derivation\n";
}
};

class second_d : public Base {


// who() not defined
};
int main()
{
Base base_obj;
Base *p;
first_d first_obj;
second_d second_obj;

p = &base_obj;
p->who(); // access Base's who()

p = &first_obj;
p->who(); // access first_d's who()

p = &second_obj;
p->who(); /* access Base's who() because
second_d does not redefine it */

return 0;
}

Le programme produit les résultats suivants:

Base
First Derivation
Base

Voici un programme calculant l’aire de figures en deux dimensions. Le programme se sert d’une
fonction d’iterface virtuelle « show_area() » permettant d’interfacer plusieurs méthodes.

#include <iostream>
using namespace std;

class figure {
protected:
double x, y;
public:
void set_dim(double i, double j) {
x = i;
y = j;
}
virtual void show_area() {
cout << "No area computation defined ";
cout << "for this class.\n";
}
} ;
class triangle : public figure {
public:
void show_area() {
cout << "Triangle with height ";
cout << x << " and base " << y;
cout << " has an area of ";
cout << x * 0.5 * y << ".\n";
}
};

class square : public figure {


public:
void show_area() {
cout << "Square with dimensions ";
cout << x << "x" << y;
cout << " has an area of ";
cout << x * y << ".\n";
}
};

int main()
{
figure *p; /* create a pointer to base type */

triangle t; /* create objects of derived types */


square s;

p = &t;
p->set_dim(10.0, 5.0);
p->show_area();
p = &s;
p->set_dim(10.0, 5.0);
p->show_area();

return 0;
}

Ici, le calcul de l’aire d’un cercle est implanté par l’ajout de paramètres aux méthodes :

#include <iostream>
using namespace std;

class figure {
protected:
double x, y;
public:
void set_dim(double i, double j=0) {
x = i;
y = j;
}
virtual void show_area() {
cout << "No area computation defined ";
cout << "for this class.\n";
}
} ;

class triangle : public figure {


public:
void show_area() {
cout << "Triangle with height ";
cout << x << " and base " << y;
cout << " has an area of ";
cout << x * 0.5 * y << ".\n";
}
};

class square : public figure {


public:
void show_area() {

cout << "Square with dimensions ";


cout << x << "x" << y;
cout << " has an area of ";
cout << x * y << ".\n";
}
};

class circle : public figure {


public:
void show_area() {
cout << "Circle with radius ";
cout << x;
cout << " has an area of ";
cout << 3.14 * x * x;
}
} ;

int main()
{
figure *p; /* create a pointer to base type */
triangle t; /* create objects of derived types */
square s;
circle c;

p = &t;
p->set_dim(10.0, 5.0);
p->show_area();

p = &s;
p->set_dim(10.0, 5.0);
p->show_area();

p = &c;
p->set_dim(9.0);
p->show_area();

return 0;
}
Fonctions virtuelles pures
Une fonction virtuelle pure est une fonction déclarée dans une classe de base n’ayant aucune
définition relative à cette base. Étant donné que la classe de base ne possède aucune définition
de cette fonction, chaque type dérivé doit déclarer sa propre version de la fonction.

Une fonction virtuelle pure se définit de la façon suivante :

virtual type func_name(parameter list) = 0;

Voici un exemple de la déclaration de la fonction « show_area() » de manière purement virtuelle :

class figure {
double x, y;
public:
void set_dim(double i, double j=0) {
x = i;
y = j;
}
virtual void show_area() = 0; // pure
} ;

L’exemple suivant nous donne un exemple de fonctions purement virtuelles:


#pragma hdrstop
#include <condefs.h>
#include <iostream>

//--------------------------------------------------------------------
#pragma argsused
using namespace std;

class figure {
protected:
double x, y;
public:
void set_dim(double i, double j=0) {
x = i;
y = j;
}
virtual void show_area() = 0; // pure
} ;

class triangle : public figure {


public:
void show_area() {
cout << "Triangle with height ";
cout << x << " and base " << y;
cout << " has an area of ";
cout << x * 0.5 * y << ".\n";
}
};
class square : public figure {
public:
void show_area() {
cout << "Square with dimensions ";
cout << x << "x" << y;
cout << " has an area of ";
cout << x * y << ".\n";
}
};

class circle : public figure {


public:
void show_area() {
cout << "Circle with radius ";
cout << x;
cout << " has an area of ";
cout << 3.14 * x * x;
}
} ;

int main(int argc, char* argv[])


{
figure *p; // create a pointer to base type
triangle t; // create objects of derived types */
circle c;
square s;

p = &t;
p->set_dim(10.0, 5.0);
p->show_area();

p = &s;
p->set_dim(10.0, 5.0);
p->show_area();

p = &c;
p->set_dim(2.0);
p->show_area();

return 0;
}

Notez que la fonction virtuelle « show_area() » doit être définie dans toutes les classes
descendantes (triangle, circle et square) pour que le programme fonctionne. Le programme
donne les résultats suivants :

Triangle with height 10 and base 5 has an area of 25


Square with dimension 10x5 has an area of 50
Circle with radius 2 has an area of 12.56
Les propriétés
Du point de vue du développeur d’applications, les propriétés ressemblent à des variables. Les
développeurs peuvent définir ou lire les valeurs des propriétés comme s’il s’agissait de données
membres. La seule opération interdite avec une propriété et autorisée avec une variable consiste
à la transmettre comme argument par adresse à une méthode.

Une pratique de la bonne programmation stipule que les attributs d’une classe ne doivent pas
être déclarés publiques. Les propriétés sont un moyen efficace d’accéder aux données d’une
classe. Les méthodes d’accès aux données peuvent donc ne pas être déclarées publiquement et
faire appel à des propriétés qui elles, sont déclarées publiquement. Ceci est un autre cas
d’encapsulation.

Déclaration des propriétés


Une propriété est déclarée dans la déclaration de sa classe. La déclaration d’une propriété
comprend les trois éléments suivants :

¾ le nom de la propriété,

¾ le type de la propriété; et

¾ les méthodes utilisées pour lire et écrire la valeur de la propriété. Si aucune méthode
d’écriture n’est déclarée, la propriété n’est accessible uniquement en lecture.

Les propriétés déclarées dans une section « __published » de la déclaration de classe du


composant sont modifiables dans l’inspecteur d’objets lors de la conception. Les propriétés
déclarées dans une section « public » sont accessibles à l’exécution et peuvent être lues ou
définies par le code du programme.

Voici une déclaration typique pour une propriété appelée « Count » :

class PACKAGE TyourComponent : public Tcomponent


{
private:
int Fcount;
int __fastcall GetCount();
void __fastcall SetCount( int Acount );
public:
__property int Count = {read=GetCount, write=SetCount}
}

Accès direct
L’accès direct est le moyen le plus simple d’accéder aux données d’une propriété. Les parties
« read » et « write » de la déclaration d’une propriété spécifient que l’affectation ou la lecture de
la valeur de la propriété s’effectue directement dans la donnée membre de stockage interne sans
appel à une méthode d’accès. En voici un exemple :
class PACKAGE TsampleComponent : public TComponent
{
private:
bool FreadOnly;
__published:
__property bool ReadOnly = {read=FreadOnly, write=FReadOnly}
}

Méthode « read »
La méthode « read » d’une propriété est une fonction qui n’accepte aucun paramètre et renvoie
une valeur du même type que la propriété. Par convention, le nom de la fonction est « Get » suivi
du nom de la propriété. Par exemple, la méthode « read » pour une propriété intitulée « Count »
serait « GetCount ». La méthode « read » manipule les données internes afin de générer une
valeur de la propriété respectant le type demandé.

Si aucune méthode « read » n’est déclarée, la propriété fonctionne uniquement en écriture.

En voici un exemple :

class TMyClass : public TObject


{
private:

//private member variables

AnsiString mStringVar;
int mIntVar;
char *mPCharVar;
bool mIsModified;
char * __fastcall GetMyPChar();
void __fastcall SetMyPChar(char *aNewVal);
void __fastcall SetMyString(AnsiString aNewVal);

protected:

public:

__fastcall TMyClass();
__fastcall ~TMyClass();

//public properties
//Note that the read and write clauses of the __property
//statement are both optional and that either can
//reference a member variable or a method with the proper
//signature
__property AnsiString MyString = {read=mStringVar, write=SetMyString};
__property int MyInt = {read=mIntVar, write=mIntVar};
__property char *MyPChar = {read=GetPChar, write=SetPChar};
__property bool IsModified = {read=mIsModified};

};

__fastcall TMyClass::TMyClass()
: TObject(),
mStringVar(""),
mPCharVar(NULL),
mIsModified(false)
{
}

__fastcall TMyClass::~TMyClass()
{
if (mPCharVar)
free(mPCharVar);
}

void __fastcall TMyClass::SetMyString(AnsiString aNewVal)


{
if (mStringVar != aNewVal)
{
mStringVar = aNewVal;
mIsModified = true;
}
}

char * __fastcall TMyClass::GetMyPChar()


{
return mPCharVar; //may be NULL
}

void __fastcall TMyClass::SetMyPChar(char *aNewVal)


{
if (mPCharVar)
{
free(mPCharVar);
mPCharVar = NULL;
}
if (aNewVal)
{
mPCharVar = strdup(aNewVal);
mIsModified = true;
}
}
//then to use the class
void __fastcall UseMyClass()
{
TMyClass *myClass = new TMyClass;

myClass->MyString = "I am an AnsiString!";


myClass->MyInt = 0;
myClass->MyPChar = "I am a pointer to char";
if (myClass->IsModified)
DoSomethingReallyCool();
delete myClass;
}

Méthode « write »
La méthode « write » d’une propriété est une fonction membre acceptant un seul paramètre du
même type que la propriété. Le paramètre peut être transmis par référence ou par valeur. Par
convention, le nom de la méthode « write » est « Set » suivi du nom de la propriété. Par exemple,
la valeur « write » d’une propriété intitulée « Count » serait « SetCount ». La valeur transmise en
paramètre devient la nouvelle valeur de la propriété. La méthode « write » doit accomplir les
manipulations nécessaires pour placer les données concernées à l’emplacement de stockage
interne de la propriété.

Lorsqu’aucune méthode « write » est déclarée, la propriété fonctionne uniquement en lecture.


Pour qu’une propriété puisse être utilisée au moment de la conception, elle doit être accessible
en lecture/écriture.

Les méthodes « write » vérifient normalement si une nouvelle valeur diffère de la valeur actuelle
avant de modifier la propriété. En voici un exemple :

class TMyPoint {
private:
int FX;
int FY;
void SetX(int x) {
if (x > 100 || x < 0)
throw ("Range error");
else
FX = x;
}
void SetY(int y) {
if (y > 100 || y < 0)
throw ("Range error");
else
FY = y;
}
public:
__property int X = {read=FX, write=SetX};
__property int Y = {read=FY, write=SetY};
};
Valeur par défaut d’une propriété
Pour déclarer la valeur par défaut d’une propriété, ajoutez un signe égal « = » après le nom de la
propriété et une paire d’accolades contenant le mot clef « default » et la valeur elle-même :

__property bool IsTrue = { default=true };

Spécification d’aucune valeur par défaut


Pour déclarer qu’une propriété ne dispose d’aucune valeur par défaut, ajoutez un signe égal
« = » après le nom de la propriété et une paire d’accolades contenant le mot clef « nodefault » :

__property int NewInteger = { nodefault };


COURS 07

INF-11299 – PROGRAMMATION II

PROGRAMMATION C++
Référence : Schildt, Herbert & Guntle, Greg, Borland C++ Builder, The complete
reference, Osborne/McGraw-Hill, Berkeley, 2001, 977pp, ISBN: 0-07-
212778-3.

Christian Potvin, Notes de cours, CÉGEP de Rivière-du-Loup.

I. Les entrées/sorties sur des fichiers


Le fichier d’en-tête « fstream.h » est destiné aux opérations d’écriture et de lecture dans les
fichiers. Grâce à ce fichier, l’ensemble des déclarations et définitions requises pour
manipuler des fichiers sont intégrées dans le fichier en cours de développement.

Les étapes des opérations dans les fichiers sont généralement similaires :

¾ ouverture d’un fichier,

¾ lecture/écriture/positionnement dans le fichier; et

¾ fermeture du fichier.

Les classes C++ : « ifstream », « ofstream » et « fstream » nous permettent d’effectuer les
opérations suivantes :

¾ lecture de fichiers : « ifstream »,

¾ écriture de fichiers : « ofstream »; et

¾ opérations de lecture et d’écriture simultanée dans un fichier : « fstream ».

Il faut associer à un fichier un mode de lecture ou d’écriture. Les modes sont les suivants :

¾ ios ::out : détermine que le fichier doit être ouvert en écriture, ce qui supprime du
fichier toutes les données qui s’y trouvaient,

¾ ios ::in : détermine que le fichier doit être ouvert en lecture,

¾ ios ::app : détermine que le fichier est ouvert en ajout; et

¾ ios ::binary : spécifie que le fichier est binaire. Par défaut, tous les fichiers C++ sont
des fichiers textes. Lorsque des données autres que du texte sont traitées, le fichier
doit être défini comme étant binaire.
II. Fichiers séquentiels
a. fichiers de caractères

#pragma hdrstop
#include <iostream>
#include <fstream>
#include <condefs.h>
using namespace std;

//----------------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
ofstream out("fcar.dat", ios::out | ios::binary);
if(!out)
{
cout << "Impossible d'ouvrir le fichier\n";
return 1;
}

char ch = '*';
for(int i=0; i <= 100; i++)
{
out.put(ch);
}

out.close();

ifstream in("fcar.dat");
if(!in)
{
cout << "Impossible d'ouvrir le fichier\n";
return 1;
}

while(in)
{
in.get(ch);
cout << ch;
}

in.close();

return 0;
}
b. fichiers d’entiers

#pragma hdrstop
#include <iostream>
#include <fstream>
#include <condefs.h>
using namespace std;

//----------------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
int i;

ofstream out("fentier.dat", ios::out | ios::binary);


if(!out)
{
cout << "Impossible d'ouvrir le fichier\n";
return 1;
}

for(i=0; i <= 100; i++)


{
out.write(reinterpret_cast <const char *> (&i), sizeof i);
}

out.close();

ifstream in("fentier.dat", ios::in | ios::binary);


if(!in)
{
cout << "Impossible d'ouvrir le fichier\n";
return 1;
}

while(in)
{
in.read(reinterpret_cast <char*>(&i), sizeof i);
cout << i;
cout << "\n";
}

in.close();

return 0;
}
c. fichiers d’enregistrements

#pragma hdrstop
#include <iostream>
#include <fstream>
#include <condefs.h>
using namespace std;

//----------------------------------------------------------------------
#pragma argsused

typedef struct
{
int noclient;
char prenom[20];
char nom[20];
double solde;
} enrg_client;

int main(int argc, char* argv[])


{
enrg_client client;

ofstream out("clients.dat", ios::out | ios::binary);


if(!out)
{
cout << "Impossible d'ouvrir le fichier\n";
return 1;
}

cout << "Entrez le numero du client : " <<endl;


cin >> client.noclient;

while(client.noclient != 0)
{
cout << "Entrez le prenom du client : " <<endl;
cin >> client.prenom;
cout << "Entrez le nom du client : " <<endl;
cin >> client.nom;
cout << "Entrez le solde du client : " <<endl;
cin >> client.solde;
out.write(reinterpret_cast <const char *> (&client), sizeof client);
cout << "Entrez le numero du client : " <<endl;
cin >> client.noclient;
}

out.close();

ifstream in("clients.dat", ios::in | ios::binary);


if(!in)
{
cout << "Impossible d'ouvrir le fichier\n";
return 1;
}
while(!in.eof())
{
in.read(reinterpret_cast <char*>(&client), sizeof client);
cout << "le numero du client : "<< client.noclient << endl;
cout << "le prenom du client : "<< client.prenom << endl;
cout << "le nom du client : "<< client.nom << endl;
cout << "le solde du client : "<< client.solde << endl;
}

in.close();

char ch;
cout << "Pressez une touche pour continuer: > " <<endl;
cin >> ch;
return 0;
}

III. Fichiers textes


#pragma hdrstop
#include <iostream>
#include <fstream>
#include <condefs.h>
using namespace std;

//----------------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
ofstream out("fictext.txt");
if(!out)
{
cout << "Impossible d'ouvrir le fichier\n";
return 1;
}

out << "Ceci est la ligne 1 du texte\n";


out << "Ceci est la ligne 2 du texte";
out << "Ceci est encore la ligne 2 du texte\n";
out << "Ceci est la ligne 3 du texte\n";

out.close();

ifstream in("fictext.txt");
if(!in)
{
cout << "Impossible d'ouvrir le fichier\n";
return 1;
}

char ch;
while(in)
{
in.get(ch);
cout << ch;
}

in.close();

return 0;
}

IV. Récursivité
La récursion en programmation peut être définie comme l’appel d’une fonction par elle-même.
Habituellement, plusieurs appels récursifs sont effectués en même temps.

Une fonction appelante n’est tout d’abord pas exécutée lorsqu’elle est appelée car son exécution
dépend du résultat de l’appel suivant. Du point de vue de la programmation, lorsque des
fonctions récursives sont appelées, elles sont empilées sur une pile sans être exécutées. Cette
opération se poursuit jusqu’au dernier appel.

En vertu de ce principe, le dernier appel récursif de la fonction est exécuté en premier. Le résultat
de cette exécution permet ensuite l’exécution de l’avant-dernier appel, et ainsi de suite, jusqu’à
ce que le programme ait exécuté tous les appels au fond de la pile.

Certains algorithmes ou calculs mathémathiques ont tendance à être décrits très précisément par
le récursivité.

Ainsi, la notion de factorielle :

0! = 1
1! = 1
N! = N*(N-1)!

L’algorithme de la factorielle peut être implanté de façon itérative :

int fact(int n)
{
int res = 1;
for (int i = 2; i <= n; i++) res *= i;
return res;
}

Il est à noter que l’implantation récursive représente plus clairement l’algorithme :

int rfact(int n)
{
if (n < 2) return 1;
else return n * rfact(n-1);
}
La suite de Fibonacci est formée de nombres qui sont la somme des deux précédents de la suite.
Cette suite est : 1 1 2 3 5 8 13 21 . . . Ainsi F(n) = F(n-1) + F(n-2).

L’algorithme de génération des nombres de Fibonacci peut être implanté de façon itérative :

int fibo(int n)
{
int nm1 = 1, nm2 = 1, res = 1;
for (int i = 3; i <= n; i++)
{
res = nm1+nm2; nm2 = nm1; nm1 = res;
}
return res;
}

Cet algorithme est également implanté facilement de façon récursive:

int fibo(int n)
{
if (n < 3) return 1;
else return fibo(n-1) + fibo(n-2);
}

L’algorithme suivant compte le nombre de caractère de type « ch » dans une chaine de


caractères :

int freq(char ch, char * s)


{
int f = 0;
for (int i = 0; i < strlen(s); i++)
if (s[i] == ch) f++;
return f;
}

Cet algorithme peut également être représenté de façon récursive :

int freq(char ch, char * s)


{
if (s[0] == 0) return 0;
else return freq(ch,&s[1]) + (s[0] == ch) ? 1 : 0; //méchant !
}
Voici une fonction récursive permettant d’élever un nombre à une puissance quelconque :

int power(int x int n)


{
if (n == 0)
return 1 ;
else return x * power(x, n-1);
}
V. Interfaçage de l’environnement C++ Builder et d’une base de données
MICROSOFT ACCESS
La majorité des applications graphiques sont reliées à une ou plusieurs base de données car une
BD est presque toujours le meilleur moyen de conserver l’information sur de larges ensembles de
données.

Dans C++ Builder, le lien avec une BD est très simple, il s’agit de définir un nom logique de base
de données qui pointe sur un nom physique d’une base de données. C’est le même principe que
pour l’ouverture d’un fichier en programmation C avec la fonction fopen.

Dans l’environnement de programmation nous sommes donc prêts à utiliser le nom logique afin
d’accéder à la base peut importe où elle est située physiquement.
Définition des Alias
C++ Builder utilise des Alias pour définir l'emplacement de tables de bases de données et les paramètres de
connexion aux serveurs de bases de données. Il existe deux façons d'établir un alias:

La permière méthode est par l'administrateur BDE :

- Administrateur BDE du menu "Borland C++ Builder 4"

- Onglet "Bases de données"

- Aller dans l'Item Objet du Menu

- Sélectionner le choix "Nouveau"

- Choisir le pilote "MS ACCESS"

- Spécifier le répertoire "Path" du ficher de la base de données MS ACCESS

- Pressez sur la flèche "Appliquer"

- Ouvrir le programme C++ Builder et commencer la session de travail


La seconde méthode est par l'administrateur ODBC :

- Menu Démarrer/Paramètres/Panneau de configuration/Sources de Données ODBC

- Sélectionner l'objet "MS Access Database" de l'Item "Sources de données Utilisateur"

- Presses le bouton "Ajouter"

- Sélectionner l'item "Microsoft Access Driver (*.mdb)"

- Presser sur le bouton "Terminer"

- Placer le nom de l'alias dans le champ "Nom de la source de données"

- Presser sur le bouton "Sélectionner"

- Spécifier le répertoire "Path" du ficher de la base de données MS ACCESS


™ Schéma de l'interface entre une application C++ Builder et une base de donnée
sous le système d'exploitation Windows

Élément: TTable Élément: TDataSource Élément:


TDBNavigator
Name:Table1 Name:DataSource1
Database Name : GARAGEBD DataSet: Table1
Name:
TableName: Client
DBNavigator1
DataSource:
DataSource1

Élément:
TDBGrid

Name:
DBgrid1
DataSource:
DataSource1
Alias
Définition de l'Alias GARAGEBD à
partir de la base de données en MS Élément:
ACCESS "Garage.MDB" TDBEdit

Name:
DBEdit1
DataSource:
DataSource1
DataField:
Nom

Base de données en MS ACCESS sous Windows: GARAGE.MDB

Client Automobile
Numéro No Série
Nom Marque
Possède
Prénom Modèle
Adresse Couleur
Tél Année
™ Composantes servant à Interfacer des applications C++ Builder à des
applications de bases de données sous Windows

Permet au logiciel C++ Builder de contrôler la connexion de l'interface C++ Builder(Alias) avec une
base de données sous le système d'exploitation Windows
Composant DataBase

Propriétés
Alias Name Nom de l'Alias
Connected Si le composant est en interaction avec la base de données
NatabaseName Nom logique définissant la base de données. Cette variable sera donnée en entrée à la
valeur DatabaseName du composant Table
LoginPrompt Le filtre permet d'afficher seulement les enregistrements qui satisfont la condition de
filtrage. (Table1->Filtered:= True)
Si la propriété filtered était laissée à false, tous les enregistrements de la base de
données seraient accessibles.

Permet au logiciel C++ Builder d'accéder à une table particulière d'une base de données
Composant Table

Propriétés
Active Pour charger la table, il faut mettre la valeur à True
DatabaseName Nom de l'Alias
Filter Permet de faire une requête sur les enregistrements à l'aide d'une condition.
Table1->Filter:= 'ItemsTotal < 1000)
Filtered Le filtre permet d'afficher seulement les enregistrements qui satisfont la condition de
filtrage. (Table1->Filtered:= True)
Si la propriété filtered était laissée à false, tous les enregistrements de la base de
données seraient accessibles.
Name Nom de la table dans le programme C++ Builder
TableName Nom de la table de la base de données physique (entité)

Composant DataSource
Permet de définir la table et le fichier de bases de données à accéder. Sert d'interface entre le
composant table et les différents composants de la palette ContrôleBD tels que: DBNavigator, DBGrid
DBText, DBEdit, DBMemo, DBImage, DBListBox, DBComboBox, DBCheckBox, DBRadioGroup, etc.
Propriétés
DataSet Nom de la table dans l'application C++ Builder
Enabled Active le composant DataSource
Name Nom du Composant DataSource dans le Programme C++ Builder
Permet de pointer et d'accéder aux différents enregistrements d'une table.
Composant DBNavigator

Propriétés
DataSource Nom du composant DataSource dans le Programme C++ Builder
Enabled Active le composant DBNavigator
Name Nom du Composant DBNavigator dans le Programme C++ Builder
Visible Rend le composant visible ou non
VisbleButtons->nbFirst Le déplacement du pointeur au premier enregistrement
VisbleButtons->nbPrior Le déplacement du pointeur à l'enregistrement précédent
VisbleButtons->nbNext Le déplacement du pointeur à l'enregistrement suivant
VisbleButtons->nbLast Le déplacement du pointeur au dernier enregistrement
VisbleButtons->nbInsert L'ajout d'un enregistrement à l'emplacement du pointeur
VisbleButtons->nbDelete La destruction d'un enregistrement à l'emplacement du pointeur
VisbleButtons->nbEdit L'édition d'un enregistrement à l'emplacement du pointeur
VisbleButtons->nbPost La confirmation de la modification ou de l'ajout
VisbleButtons->nbCancel L'annulation d'une opération
VisbleButtons->nbRefresh Le rafraîchissement des champs de données

Présente les données de la table sous forme tabulaire


Composant DBGrid

Propriétés
DataSource Nom du composant DataSource de l'application C++ Builder
Enabled Active le composant DBGrid
Name Nom du Composant DBGrid dans le Programme C++ Builder
Visible Le composant est visible ou invisible à l'exécution

Affiche les données provenant des attributs/des champs textuels de la base de donnée en écriture
seulement
Composant DBText

Propriétés
DataSource Nom du composant DataSource de l'application C++ Builder
DataField Nom de l'attribut/du champ de la base de données physique
Enabled Active le composant DBText
Name Nom du Composant DBText dans le Programme C++ Builder
Visible Le composant est visible ou invisible à l'exécution
Champ de saisie des données en lecture/écriture ou affichage des données en lecture seulement
Composant DBEdit

Propriétés
DataSource Nom du composant DataSource de l'application C++ Builder
DataField Nom de l'attribut/du champ de la base de données physique
Enabled Active le composant DBEdit
Name Nom du Composant DBEdit dans le Programme C++ Builder
ReadOnly Lecture seulement ou lecture/écriture
Visible Le composant est visible ou invisible à l'exécution

Champ de saisie de bloc de texte en lecture/écriture ou affichage d'un bloc de texte en lecture
seulement
Composant DBMemo

Propriétés
DataSource Nom du composant DataSource de l'application C++ Builder
DataField Nom de l'attribut/du champ de la base de données physique
Enabled Active le composant DBMemo
Name Nom du Composant DBMemo dans le Programme C++ Builder
ReadOnly Lecture seulement ou lecture/écriture
Visible Le composant est visible ou invisible à l'exécution

Champ de saisie de bloc de texte formaté en lecture/écriture ou affichage d'un bloc de texte formaté
en lecture seulement
Composant DBRichEdit

Propriétés
DataSource Nom du composant DataSource de l'application C++ Builder
DataField Nom de l'attribut/du champ de la base de données physique
Enabled Active le composant DBRichEdit
Name Nom du Composant DBRichEdit dans le Programme C++ Builder
ReadOnly Lecture seulement ou lecture/écriture
Visible Le composant est visible ou invisible à l'exécution
Affiche des images enregistrées dans la base de données
Composant DBImage

Propriétés
DataSource Nom du composant DataSource de l'application C++ Builder
DataField Nom de l'attribut/du champ de la base de données physique
Enabled Active le composant DBImage
Name Nom du Composant DBImage dans le Programme C++ Builder
ReadOnly Lecture seulement ou lecture/écriture
Visible Le composant est visible ou invisible à l'exécution

Boîte à cocher
Composant DBCheckBox

Propriétés
Caption Texte descriptif
DataSource Nom du composant DataSource de l'application C++ Builder
DataField Nom de l'attribut/du champ de la base de données physique
Enabled Active le composant DBCheckBox
Name Nom du Composant DBCheckBox dans le Programme C++ Builder
ReadOnly Lecture seulement ou lecture/écriture
Visible Le composant est visible ou invisible à l'exécution

Objet contenant un ensemble de boutons radio


Composant DBRadioGroup

Propriétés
Caption Texte descriptif de l'ensemble des boutons radio
DataSource Nom du composant DataSource de l'application C++ Builder
DataField Nom de l'attribut/du champ de la base de données physique
Enabled Active le composant DBRadioGroup
ItemIndex Indice indiquant la position des boutons radio
-1 : Aucun bouton radio sélectionné
0: Adresse du premier bouton radio
Items Ensemble de chaîne de caractères constituant les descriptions des boutons radio
Name Nom du Composant DBRadioGroup dans le Programme C++ Builder
ReadOnly Lecture seulement ou lecture/écriture
Visible Le composant est visible ou invisible à l'exécution
Liste Déroulante (Drop Down List ou Combo Box)
Composant DBComboBox

Propriétés
DataSource Nom du composant DataSource de l'application C++ Builder
DataField Nom de l'attribut/du champ de la base de données physique
Enabled Active le composant DBComboBox
ItemIndex Indice indiquant la position des éléments de la liste déroulante
-1 : Aucun élément sélectionné
0: Adresse du premier élément de la liste
Items Ensemble de chaîne de caractères constituant les descriptions des éléments de la liste
déroulante
Name Nom du Composant DBComboBox dans le Programme C++ Builder
ReadOnly Lecture seulement ou lecture/écriture
Visible Le composant est visible ou invisible à l'exécution

Liste de sélection (ListBox)


Composant DBLlistBox

Propriétés
DataSource Nom du composant DataSource de l'application C++ Builder
DataField Nom de l'attribut/du champ de la base de données physique
Enabled Active le composant DBListBox
ItemIndex Indice indiquant la position des éléments de la liste de sélection
-1 : Aucun élément sélectionné
0: Adresse du premier élément de la liste
Items Ensemble de chaîne de caractères constituant les descriptions des éléments de la liste
de sélection
Name Nom du Composant DBListBox dans le Programme C++ Builder
ReadOnly Lecture seulement ou lecture/écriture
Visible Le composant est visible ou invisible à l'exécution

Liste Déroulante (Drop Down List ou Combo Box) d'éléments chargés à partir d'une table
Composant DBLookupComboBox

Propriétés
DataSource Nom du composant DataSource de l'application C++ Builder
DataField Nom de l'attribut/du champ de la base de données physique
Enabled Active le composant DBLookupComboBox
KeyField Champ écrit dans la table d'entrée
ListField Ensemble des enregistrements affichés dans le ComboBox
ListSource Nom du composant DataSource à charger dans le ComboBox
Name Nom du Composant DBLookupComboBox dans le Programme C++ Builder
ReadOnly Lecture seulement ou lecture/écriture
Visible Le composant est visible ou invisible à l'exécution

Liste de sélection (ListBox) d'éléments chargés à partir d'une table


Composant DBLookpListBox

Propriétés
DataSource Nom du composant DataSource de l'application C++ Builder
DataField Nom de l'attribut/du champ de la base de données physique
Enabled Active le composant DBLookupListBox
KeyField Champ écrit dans la table d'entrée
ListField Ensemble des enregistrements affichés dans le ListBox
ListSource Nom du composant DataSource à charger dans le ListBox
Name Nom du Composant DBLookupListBox dans le Programme C++ Builder
ReadOnly Lecture seulement ou lecture/écriture
Visible Le composant est visible ou invisible à l'exécution
™ La manipulation d’une table de la base de données
Les classes TDataSet et TBDEDataSet: les classes TDataSet et TBDEDataSet sont les classes ancêtres de
TTable, TQuery et TStoredProc.

Principales propriétés de TDataSet et de TBDEDataSet(TTable, Tquery et TstoredProc)


Active True: Ouvrir l'ensemble de données/False: le fermer
Bof True: Le curseur se trouve sur le premier enregistrement de l'ensemble de données
DataSource Le nom du composant Datasource associé à cet ensemble de données
DataBaseName Le nom de l'alias BDE ou l'alias défini par le composant TDataBase
Eof True: Le curseur est à la fin du fichier
FieldCount Le nombre de champs dans l'ensemble de données
Fields Un tableau d'objets TFields qui contient des informations sur les champs dans l'ensemble de
données
FieldValues Renvoie la valeur du champ spécifié dans l'enregistrement courant
Filter Une expression qui détermine quels doivent être les enregistrements contenus dans un ensemble de
données.
Filtered True:Le filtre est appliqué/False: L'ensemble de données tout entier est renvoyé en résultat
Found Indique si l'opération de recherche a été fructueuse
Modified Indique si l'enregistrement courant a été modifié
RecordCount Renvoie le nombre d'enregistrements dans l'ensemble des données
State Renvoie l'état actuel de l'ensemble de données(dsEdit, dsBrowse, dsInsert, etc.)

Principales Méthodes de TDataSet et de TBDEDataSet(TTable, Tquery et TstoredProc)


Append Crée un enregistrement vide et l'ajoute à la fin de l'ensemble de données
Cancel Annule toutes les modifications à l'enregistrement en cours si ces modifications n'ont pas
été enregistrées.
Close Ferme l'ensemble de données (Active = False;)
Delete Supprime l'enregistrement en cours
DisableControls Suspend toute possibilité de saisie pour tous les contrôles de données associés à
l'ensemble de données
Edit Autorise la possibilité de modifier l'enregistrement en cours
EnableControls Autorise la saisie pour tous les contrôles de données associés à l'ensemble de données
FieldByName Renvoie l'objet TField qui correspond à un nom de fichier
FindFirst Recherche le premier enregistrement qui correspond au filtre en cours
FindLast Recherche le dernier enregistrement qui correspond au filtre en cours
FindNext Recherche l'enregistrement suivant qui correspond au filtre en cours
FindPrior Recherche le précédent enregistrement qui correspond au filtre en cours
First Place le curseur sur le premier enregistrement de l'ensemble de données
Insert Insère un enregistrement et fait passer l'ensemble de données en mode édition
Last Positionne le curseur sur le dernier enregistrement de l'ensemble de données
Locate Recherche un enregistrement particulier dans l'ensemble de données
Lookup Repère un enregistrement par le moyen le plus rapide et renvoie les données contenues
dans cet enregistrement
Next Déplace le curseur sur l'enregistrement suivant
Open Ouvre l'ensemble de données (Active = True)
Post Écrit dans la base de données, ou dans le tampon du cache des mises à jour, les données
modifiées qui se trouvent dans l'enregistrement
Prior Positionne le curseur sur l'enregistrement précédant
Refresh Met à jour les données situées dans l'ensemble de données à partir de celles de la base de
données
Principaux événements de TDataSet et de TBDEDataSet(TTable, Tquery et TstoredProc)
AfterCancel Généré après l'annulation des modifications apportées à un enregistrement
AfterClose Généré après la fermeture d'un ensemble de données
AfterDelete Généré après la suppression d'un enregistrement dans l'ensemble de données
AfterEdit Généré après l'édition d'un enregistrement
AfterInsert Généré après l'insertion d'un enregistrement
AfterOpen Généré après l'ouverture de l'ensemble de données
AfterPost Généré après que des modifications dans un enregistrement sont enregistrées
BeforeCancel Généré avant l'annulation de modifications
BeforeClose Généré avant la fermeture d'un ensemble de données
BeforeDelete Généré avant la suppression d'un enregistrement
BeforeEdit Généré avant le passage en mode édition d'un ensemble de données
BeforeInsert Généré avant l'insertion d'un enregistrement
BeforeOpen Généré juste avant l'ouverture d'un ensemble de données (entre le moment où active est
mis à True et le moment où l'ensemble de données est effectivement ouvert)
BeforePost Généré avant que les modifications soient enregistrées dans la base (dans le cache des
mises à jour)
OnCalcFields Généré quand les calculs sont effectués sur des champs calculés
OnDeleteError Généré si une erreur survient pendant la suppression d'un enregistrement
On EditError Généré si une erreur se produit pendant la modification d'un enregistrement
OnFilterRecord Généré chaque fois qu'on accède à une nouvelle ligne(enregistrement) et que Filter vaut
True
OnNewRecord Généré quand on ajoute un nouvel enregistrement à l'ensemble de données
OnPostError Généré quand une erreur se produit pendant l'enregistrement des modifications
apportées à un enregistrement
OnUpdateError Généré quand une erreur se produit pendant l'écriture du cache des mises à jour dans la
base de données
OnUpdateRecord Généré quand une mise à jour du cache est appliquée à un enregistrement

L’état(State) d’une Table peut être différent selon les opération que l’on effectue sur celle-ci.

État(State) d'une table (Composant TTable)


DsEdit L'enregistrement actif peut être modifié
dsBrowse Les données peuvent être visualisées, mais non modifiées. Il s'agit de l'état par défaut
d'un ensemble de données ouvert.
dsInsert L'enregistrement actif est un tampon nouvellement inséré qui n'a pas été transmis. Cet
enregistrement peut être modifié, puis transmis ou abandonné.
dsInactive L'ensemble de données est fermé, ses données sont donc indisponibles.
dsSetKey TTable et TclientDataSet uniquement. La recherche d'enregistrements est activée ou une
opération Set Range est en cours. Un ensemble restreint de données peut être visualisé et
aucune donnée ne peut-être modifiée ni insérée
dsCalcFields Un événement OnCalcFields est en cours. Les champs non calculés ne peuvent pas être
modifiés et les nouveaux enregistrements ne peuvent pas être insérés.
Relations entre l'état visualisation et les autres états d'un ensemble de données

dsInactive

Post Post
(échec) (échec)
Open Close

Insert
Edit
Append
dsInsert dsBrowse dsEdit

Post (réussite) Post (réussite)


Cancel Cancel
Delete Delete

SetKey Post, Cancel


EditKey GotoKey, FindKey
SetRange ApplyRange, CancelRange

dsSetkey

Connaître l’état Méthode pour changer l’état

ObjetTable->State = dsEdit ObjetTable->Edit;


dsBrowse ObjetTable->Browse;
dsInsert ObjetTable->Insert;
dsInactive
dsSetKey
dsCalcFields
ObjetTable->Post;
ObjetTable->Cancel;
ObjetTable->Append;
L’accès à un champ de la table
ObjetTable->Field[x].AsString; où x, > 0, est l’indice du champs dans l’enregistrement

ObjetTable->FieldByName(‘nom du champ’).AsString;

Afin de lire le bon format de données l’on précise le type sous la forme :
.AsString
.AsInteger
.AsFloat
.AsDateTime
.AsCurrency
.AsBoolean
.AsVariant

La navigation dans la table


ObjetTable->DisableControls; Pour désactiver les contrôles pour une exécution plus rapide

ObjetTable->First;
ObjetTable->Next;
ObjetTable->Prior;
ObjetTable->Last;
ObjetTable->MoveBy(x);
ObjetTable->BOF; Beginning Of File
ObjetTable->EOF; End Of File

ObjetTable->EnableControls; Pour remettre les contrôles à la normale pour l’usager

La recherche dans une table


ObjetTable->SetKey; ObjetTable->FindKey([‘Boucher’]);
ObjetTable->FieldByName(‘nom’).AsString:= ‘Boucher’; ObjetTable->FindNearest([‘Boucher’]);
ObjetTable->GotoKey; ObjetTable->FindKey([‘B’, ‘D’]);
ObjetTable->GotoNearest; ObjetTable->FindNearest([‘B’, ‘D’]);

™ La validation des informations avant le transfert dans la BD. Si la validation n’est pas faite avant le transfert dans
la BD, c’est la BD qui retournera un message d’erreur qui quelques fois n’est pas très significatif. Si l’on désire
faire en sorte que les messages de la BD ne parviennent pas à l’usager, nous pouvons programmer les
événements et les commandes suivantes au DataSet:

Événement Commande
OnEditError Action = daAbort;
OnPostError Action = daAbort;
OnUpdateError UpdateAction = uaAbort;

daAbort: Abandonne l'opération qui a conduit à une erreur sans afficher de message.
uaAbort :Abandonne l’opération de mise à jour sans renvoyer de message d’erreur.
™ En général, la validation peut se faire à quatre (4) endroits :
1) ContôleDB

On utilise soit l'événement OnChange qui appelle une fonction qui valide les éléments entrés dans le contrôle au
fur et à mesure que l’usager les entre.

Nous pouvons également utiliser l'événement OnExit qui appelle une fonction qui valide les éléments entrés
lorsque l'usager sort du champ de saisie actuel (Exit) pour se déplacer dans un autre champ de saisie.

L'événement OnEnter nous permet d'appeler une fonction de validation lorsque l'usager place son curseur
dans un champ de saisie.

Une manière plus rapide est de définir un masque de saisie qui précise le format des données à entrer.

ControleDB->Field.EditMask := ‘(000) 000-0000;1;*’;


ControleDB->Field.EditMask := ‘L0L 0L0;0;*’;
DBEdit1->Field.EditMask:= 'L0L 0L0;0;_';
Chaine:’Format;Inclusion des caractères littéraux(0/non – 1/oui/Caractère du contrôle de saisie’ ;

2) DBNavigator

On utilise soit l’événement OnClick qui appelle une fonction qui effectue des opérations selon le bouton de la
barre de navigation.

Switch (Button)
{
case nbFirst :
break;
case nbPrior :
break;
case nbNext :
break;
case nbLast :
break;
case nbRefresh :
break;
case nbDelete :
break;
case nbCancel :
break;
case nbEdit :
break;
case nbInsert :
break;
case nbPost :
break;
}
3) DataSource

On utilise soit l’événement OnUpdateData qui appelle une fonction qui valide les éléments importants avant que
les informations soient envoyées à la BD.

4) DataSet

On utilise soit l’événement BeforePost qui appelle une fonction qui valide les éléments important avant que les
informations soit envoyées à la BD. C’est équivalent à faire OnUpdatedata avec le composant DataSource.

™ Modules de données
Les modules de données sont des fiches spécialisées. Lorsqu'il y a trop de composants sur une fiche, nous pouvons
déplacer les composants dans un module de données et relier ce module à la fiche.

Fiche

Composants

Fiche Lien entre la Module de données


fiche et le
module de
données

Composants

Pour créer un module de données:


1) Choisissez Fichier | Nouveau. Quand le référentiel d'objets apparaît, double-cliquez sur l'icône "Module
de données",
2) Transférez les composant de votre fiche dans le module de données et/ou placez de nouveaux
composants,
3) Sauvegardez le module de données sous un nom donné ; et
4) Choisissez Fichier | Utiliser l'Unité et sélectionnez le nom de votre module de données dans la boîte de
saisie.
™ Création de liens/relations/jointures entre les tables dans l'environnement C++
Builder
Base de données Garage[Relations un(maître) à plusieurs(détail)]

Propriété MasterFields: Le ou les champs qui devraient assurer la jointure entre les deux tables
Propriété MasterSource: La table à utiliser comme table maître lorsque la table est utilisée comme table détail

Table 2
Table1 Masterfields : Numéro du Client
MasterSource: dataSource 1

DataSource1 DataSource 2

Client Automobile

*Numéro du Client *Numéro de Série


Prénom du Client Possède Marque
Nom du Client Modèle
No Civique Couleur
Adresse Année
Ville Lien Client
™ Le composant DBCtrlGrid

DBCtrlGrid est un composant qui permet de créer des grilles personnalisées disposant de barres de défilement. Vous
pouvez placer tout composant orienté données (ou n'importe quel autre composant d'ailleurs) sur la première cellule
d'un composant DBCtrlGrid, et le progiciel C++ Builder dupliquera ces composants pour chacun des enregistrements
dans l'ensemble de données sous-jacent.

Permet d’afficher une suite d’enregistrements d’une table dans un formulaire déroulant
Composant DBCtrlGrid

Propriétés
DataSource Nom du composant DataSource de l'application C++ Builder
Enabled Active le composant DBCtrlGrid
Name Nom du Composant DBCtrlGrid dans le Programme C++ Builder
Visible Le composant est visible ou invisible à l'exécution
™ Schéma de l'interface entre une application C++ Builder et une base de données
sous le système d'exploitation Windows pour l'application de requêtes SQL

Élément: TQuery Élément: TDataSource Élément:


TDBNavigator
Name: Query1 Name:DataSource1
Database Name : GARAGEBD DataSet: Table1
Name:
DataSource: DBNavigator1
SQL: 'Select Prénom from Client' DataSource:
DataSource1

Élément:
TDBGrid

Name:
DBgrid1
DataSource:
DataSource1
Alias
Définition de l'Alias GARAGEBD à
partir de la base de données en MS Élément:
ACCESS "Garage.MDB" TDBEdit

Name:
DBEdit1
DataSource:
DataSource1
DataField:
Prénom

Base de données en MS ACCESS sous Windows: GARAGE.MDB

Client Automobile
Numéro No Série
Nom Marque
Possède
Prénom Modèle
Adresse Couleur
Tél Année
™ Partage d’un composant TDataSource entre une table et une requête

Table1

DBGrid1
DataSource1 DataSource :DataSource1
DataSet :
Query1/Table1

Query1

™ Le composant Query

Permet au logiciel C++ Builder d’effectuer des requêtes SQL sur une base de données
Composant TQuery

Propriétés
Active Pour charger la table, il faut mettre la valeur à True
DatabaseName Nom de l'Alias
DataSource Nom du composant DataSource de l'application C++ Builder
Filter Permet de faire une requête sur les enregistrements à l'aide d'une condition.
Table1->Filter:= 'ItemsTotal < 1000)
Filtered Le filtre permet d'afficher seulement les enregistrements qui satisfont la condition de
filtrage. (Table1->Filtered:= True)
Si la propriété filtered était laissée à false, tous les enregistrements de la base de
données seraient accessibles.
Name Nom du composant TQuery dans le programme C++ Builder
SQL Chaîne de caractères contenant la requête SQL

La démarche la plus courante pour accéder à des données dans des bases de données client-serveur met en jeu le
composant Query.
Propriété SQL : La propriété SQL est une TStringList qui contient les instructions SQL à exécuter. La valeur de la
propriété SQL est définie au moyen de l’inspecteur d’objets en mode conception, ou par l’ajout d’instructions en
cours d’exécution.
Lors de l’ajout des lignes à la propriété SQL en cours d’exécution, le contenu initial doit être effacé par la méthode
« Clear »
Query1.SQL->Clear ;
Query1.SQL->add(‘select * from pays’);
COURS 08

INF-11299 – PROGRAMMATION II

PROGRAMMATION C++
Référence : Schildt, Herbert & Guntle, Greg, Borland C++ Builder, The complete reference,
Osborne/McGraw-Hill, Berkeley, 2001, 977pp, ISBN: 0-07-212778-3.

Richter, Pr. Dr. Claus, Programmation C++, Micro Application, Paris, 2000,
383pp., ISBN : 2-7429-1637-7

VI. Gestion des exceptions(interruptions/erreurs)


Afin qu’un programme soit plus tolérant aux fautes et aux erreurs, l’environnement C++
permet un traitement généralisé des exceptions permettant aux programmes de réagir à
des erreurs de calculs ou d’entrées/sorties sans provoquer une fin anormale des
programmes.

La gestion des exceptions permet au programmeur la gestion de certaines erreurs


d’exécution prévisibles d’une façon définie par le langage.

En utilisant le traitement des exceptions du C++, les programmes peuvent


automatiquement appeler des routines d’interruption lors des erreurs d’exécution.

Le principal avantage du traitement des exceptions est qu’il automatise le traitement des
erreurs. Ce traitement des erreurs devait auparavant être prévu et codé par les concepteurs
des applications en langages classiques. En voici un exemple :

void Rational::SetDenominator(int denom) {


if (denom != 0) {
DenominatorValue = denom;
}
else {
cerr << "Illegal denominator: " << denom
<< "using 1" << endl;
DenominatorValue = 1;
}
}

La gestion des exceptions en C++ est implantée à l’aide de trois mots réservés : « try »,
« catch » et « throw ».

¾ try : En termes plus généraux, les instructions du programme à surveiller pour le


traitement des erreurs doivent être contenues dans un bloc try,

¾ throw : Sert à signaler l’apparition d’une erreur dans le bloc try.


La forme générale du « throw » est la suivante :

throw exception;

¾ catch : un bloc « catch » sert à récupérer et à traiter une exception.

catch (Type de paramètre Nom du paramètre) {


instructions de traitement de l’exception
}

Toute instruction utilisant l’instruction « throw » doit être placée dans un bloc « try ».
Chacune des interruptions doit être traitée par une instruction « catch » suivant le
bloc « try » ayant généré l’interruption.

Lorsqu’une instruction « throw » génère une interruption, elle doit être traitée par
une instruction « catch » du type correspondant .

La forme générale des blocs « try » et « catch » est illustrée ici :

try {
// instructions du bloc try
throw type d'interruption
}

catch(type1 arg1) {
// instructions du traitement de l'interruption du type 1
}

catch(type2 arg2) {
// instructions du traitement de l'interruption du type 2
}

catch(type3 arg3) {
// instructions du traitement de l'interruption du type 3
}

// .....

catch(typeN argN) {
// instructions du traitement de l'interruption du type N
}

S’il n’y a aucune erreur de produite dans le bloc « try », aucun des blocs « catch »
ne sera exécuté.

Si une exception d’un certain type est générée sans avoir d’instruction « catch » de
type correspondant, le programme se terminera anormalement.

Le programme C++ suivant est un exemple simple de traitement d’erreur :


// A simple exception handling example.
#include <iostream>
using namespace std;

int main()
{
cout << "Start\n";

try { // start a try block


cout << "Inside try block\n";
throw 100; // throw an error
cout << "This will not execute";
}
catch (int i) { // catch an error
cout << "Caught an exception -- value is: ";
cout << i << "\n";
}

cout << "End";

return 0;
}

Le programme produit les résultats suivants:

Start
Inside try block
Caught an exception – value is : 100
End

Voici un exemple d’un programme se terminant anormalement parce que type de l’exception
n’est pas implanté avec les instructions « catch »:

// This example will not work.


#include <iostream>
using namespace std;

int main()
{
cout << "Start\n";

try { // start a try block


cout << "Inside try block\n";
throw 100; // throw an error
cout << "This will not execute";
}
catch (double i) { // Won't work for an int exception
cout << "Caught an exception -- value is: ";
cout << i << "\n";
}

cout << "End";

return 0;
}
Le programme donne les résultats suivants parce qu’une erreur de type entier n’est pas traitée
par la routine d’interruption pour les réels « catch (double i) »:

Start
Inside try block

Abnormal program termination

Une interruption peut être implantée dans une fonction appelée dans un bloc « try ». Il n’est pas
nécessaire que l’instruction « throw » soit à l’intérieur du bloc « try » :

/* Throwing an exception from a function outside the


try block.
*/
#include <iostream>
using namespace std;

void Xtest(int test)


{
cout << "Inside Xtest, test is: " << test << "\n";
if(test) throw test;
}

int main()
{
cout << "Start\n";

try { // start a try block


cout << "Inside try block\n";
Xtest(0);
Xtest(1);
Xtest(2);
}
catch (int i) { // catch an error
cout << "Caught an exception -- value is: ";
cout << i << "\n";
}

cout << "End";

return 0;
}

Le programme produit les résultats suivants:

Start
Inside try block
Inside Xtest, test is : 0
Inside Xtest, test is 1
Caught an exception – value is 1
End
Un traitement d’interruption peut être implanté à l’intérieur d’une fonction et seulement influencer
cette fonction sans affecter l’exécution générale du programme principal :

#include <iostream>
using namespace std;

// A try/catch can be inside a function other than main().


void Xhandler(int test)
{
try{
if(test) throw test;
}
catch(int i) {
cout << "Caught Exception #: " << i << '\n';
}
}

int main()
{
cout << "Start\n";

Xhandler(1);
Xhandler(2);
Xhandler(0);
Xhandler(3);

cout << "End";

return 0;
}

Le programme donne les résultats suivants :

Start
Caught Exception # : 1
Caught Exception # : 2
Caught Exception # : 3
End
VII. Gestion de la mémoire dynamique
Une fois compilés, tous les programmes C et C++ divisent la mémoire de l’ordinateur en
quatre régions :

¾ les instructions du programme : le code exécutable,

¾ les données du programmes : données spécifiées dans le code du programme telles


que les variables et les constantes,

¾ la pile du programme : utilisée pour emplier des données, des adresses et des
variables lors de l’appel des sous-routines récursives ou non récursives; et

¾ le tas (heap) : espace-mémoire géré par le programme servant à l’allocation de


mémoire dynamique.

a. Fonction « malloc »

Prototype de la fonction : void *malloc(size_t size)


« size_t » doit être un entier non signé

Librairie : <stdlib.h>

Description :

La fonction « malloc » retourne un pointeur sur le premier octet d’un bloc de


mémoire de grosseur « size » ayant été alloué dans l’expace mémoire réservé du
programme (« heap »). S’il n’y a pas assez de place dans la mémoire du
programme pour allouer l’espace demandé, la fonction retourne le pointeur
« null ».

L’avantage avec cette façon d’allouer de la mémoire, c’est que nous pouvons
libérer de l’espace mémoire avant la fin du programme.

L’exemple suivant alloue de façon dynamique de l’espace mémoire pour une


structure du type « addr ».
#include <stdlib.h>

struct addr {
char name[40];
char street[40];
char city[40];
char state[3];
char zip[10];
};
/* ... */
struct addr *get_struct(void)
{
struct addr *p;

if(!(p=(struct addr *)malloc(sizeof(addr)))) {


printf("Allocation error.");
exit(0);
}
return p;
}

b. Fonction « calloc»

Prototype de la fonction :

void *calloc(size_t num, size_t size)

Librairie : <stdlib.h>

Description :

La fonction « calloc() » retourne un pointeur sur un espace-mémoire égal à la taille


d’un élement multiplié par le nombre d’occurrences de cet élément (num * size).

La fonction « calloc » retourne un pointeur sur le premier octet d’un bloc de


mémoire ayant été alloué dans l’expace mémoire réservé du programme
(« heap »).

S’il n’y a pas assez de place dans la mémoire du programme pour allouer l’espace
demandé, la fonction retourne le pointeur « null ».

Le programme suivant retourne un pointeur sur un vecteur de 100 réels (float)


alloué dynamiquement :
#include <stdlib.h>
#include <stdio.h>

float *get_mem(void)
{
float *p;

p = (float *) calloc(100, sizeof(float));


if(!p) {
printf("Allocation failure.");
exit(1);
}
return p;
}

c. Fonction « free »

Prototype de la fonction : void free(void *ptr)

Librairie : <stdlib.h>

Description :

La fonction « free() » désalloue l’espace mémoire et il se rajoute à l’espace-


mémoire disponible du programme (« heap ») pour des futures allocations.

La fonction « free() » doit détruire uniquement des pointeurs alloués avec les
fonctions d’allocation dynamiques : « malloc() », « realloc() » ou « calloc() ».

Tout autre pointeur détruit avec cette fonction peut provquer l’arrêt du programme
ou du système d’exploitation.

Le programme suivant alloue dynamiquement une chaine de caractères et libère


ensuite l’espace mémoire :
#include <stdlib.h>
#include <stdio.h>

int main(void)
{
char *str[100];
int i;

for(i=0; i<100; i++) {


if((str[i]=(char *)malloc(128))==NULL) {
printf("Allocation error.");
exit(0);
}
gets(str[i]);
}

/* now free the memory */


for(i=0; i<100; i++) free(str[i]);

return 0;
}

d. L’allocation dynamique et les pointeurs

Ce fragment de code fait l’allocation dynamique d’un espace-mémoire de 25


octets :

char *p;
p = (char *) malloc(25);

Ce fragment de code fait l’allocation dynamique d’un espace-mémoire de 50


entiers peu importe la représentation interne de ces entiers :

int *p;
p = (int *) malloc(50*sizeof(int));

Ce fragment de code nous permet de vérifier si l’allocation d’un espace-mémoire


est valide:

int *p;
if((p = (int *) malloc(100))==NULL) {
printf("Out of memory.\n");
exit(1);
}

e. L’allocation dynamique des vecteurs

Dans certains cas où il faut économiser l’espace-mémoire, il faut définir les


vecteurs de façon dynamique.
Cette façon de faire nous permet de restituer l’espace-mémoire ayant fini d’être
utilisé et de définir des vecteurs dont la taille sera seulement connue à l’exécution.

Ce fragment de code définit un vecteur de 1000 occurrences d’un octet :

char *p;
p = malloc(1000); /* get 1000 bytes */

Voici une autre façon de définir ce même vecteur :

p = (char *) malloc(1000); /* get 1000 bytes */

Le programme suivant définit un vecteur dynamique de 80 caractères :

/* Print a string backwards using dynamic allocation. */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
char *s;
register int t;

s = (char *) malloc(80);

if(!s) {
printf("Memory request failed.\n");
exit(1);
}

gets(s);
for(t=strlen(s)-1; t>=0; t--) putchar(s[t]);
free(s);

return 0;
}

f. Allocation de mémoire dynamique par les opérateurs « new » et « delete »

Le langage C++ possède deux opérateurs effectuant des fonctions d’allocation et


de libération de mémoire dynamique d’une manière simple et efficace. Ces
opérateurs sont « new » et « delete ». Leur prototypage est :

variable_pointeur = new type_de_la_variable;

delete variable_pointeur;
Voici un exemple simple utilisant ces deux opérateurs :

#include <iostream>
#include <new>
using namespace std;

int main()
{
int *p;

try {
p = new int; // allocate memory for int
} catch (bad_alloc xa) {
cout << "Allocation failure.\n";
return 1;
}

*p = 20; // assign that memory the value 20


cout << *p; // prove that it works by displaying value

delete p; // free the memory

return 0;
}

Si une opération d’allocation de mémoire avec l’opérateur « new » échoue, l’environnement C++
produit une interruption de type « bad_alloc ».
VIII. Structures de données dynamiques
Il serait beaucoup plus intéressant de disposer d'une structure de données plus malléable,
une structure qui permettrait si on le désire une destruction volontaire ou, à l'inverse, qui
permettrait sa survie après même la fin du programme, ou encore d'en détruire une partie.

On veut en fait que le système gère le moins possible, et donc prendre en charge la gestion
totale d'une telle structure : cette structure sera dite dynamique, par gestion de la mémoire,
et les listes chaînées en sont l'exemple parfait.

Pour gérer la mémoire, il faut disposer de pointeurs, d'endroits où sauvegarder


l'emplacement d'un élément. Une liste chaînée est un enchaînement de cellules, une cellule
contenant une valeur et un pointeur sur une autre cellule (ou deux pointeurs dans le cas
des listes doublement chaînées).

Pour pouvoir parcourir une liste entièrement, il suffit de connaître l'adresse de sa première
cellule, contenue dans un pointeur.

a. Listes simplement chaînées

Les listes chaînées sont une enfilade de cellules de structure:

class ListeSimple
{
public :

struct cellule
{
int element;
cellule *suivant;
};

cellule *DebutListe;
};

« DebutListe » est le pointeur sur la première cellule de la liste, et c'est grâce à celui-
ci que l'on peut accéder à toutes les cellules de la liste sans exception. Lorsqu'on
arrive en fin de liste, le pointeur sur la cellule suivante pointe sur le pointeur vide («
Null »).
i. Les constructeurs

On peut déclarer deux constructeurs. Le premier permet à l'utilisateur de


créer une liste chaînée contenant des valeurs aléatoires, en indiquant leur
nombre. Le second constructeur permet de montrer plus explicitement la
création dynamique d'une liste chaînée en insérant des valeurs une par une
et en terminant la saisie par -1.

ListeSimple(int NbDeCellules)
{
DebutListe=NULL;
for(int i=0;i<NbDeCellules;i++)
DebutListe=InsertionRecursive(DebutListe,rand()%50);
};

ListeSimple()
{
DebutListe=NULL;
int Valeur = 0;
cout << "Entrez des valeurs entières (terminez par -1) :";
for(;;)
{
cin >> Valeur;
if (Valeur == -1) break;
DebutListe=InsertionRecursive(DebutListe, Valeur);
}
};

Ces constructeurs permettent de comprendre que la structure n'a pas une


taille prédéfinie, mais que les éléments se rajoutent un à un. Il est possible
d'insérer une valeur à n'importe quel moment du programme.

Dans cet exemple de classe, la création de la liste est faite de telle sorte que
chaque élément ajouté est automatiquement trié, inséré à sa place entre un
élément inférieur ou égal et un élément supérieur.

Pour ce faire, on utilise une fonction d'insertion récursive, que l'on applique à
la racine de la liste. Si la liste est vide, on insère une nouvelle cellule
contenant la nouvelle valeur, sinon on applique la fonction sur la liste qui
débute sur la cellule suivante jusqu'à ce que la nouvelle valeur soit inférieure
à la racine pointée.
cellule *InsertionRecursive(cellule *UneListe, int val)
{
if (UneListe)
if(UneListe->element<val)
{
UneListe->suivant=InsertionRecursive(UneListe->suivant, val);
return UneListe;
}
cellule *NouvelleCellule=new cellule;
NouvelleCellule->element=val;
NouvelleCellule->suivant=UneListe;
return NouvelleCellule;
}

Lors de la création d'une nouvelle variable de type « liste chaînée » dans le


programme principal, on aura:

ListeSimple UneListe;

ou :

ListeSimple UneListe(NombreDeValeurs);

ii. Le destructeur

Pour détruire une liste chaînée entière nous devons détruire toutes les
cellules une par une, pour enfin détruire le pointeur sur le début de la liste.
Pour ce faire, il nous faut une fonction qui permette de détruire une cellule,
tout en faisant de la liste qu'elle reste une liste. Nous choisirons de
supprimer le dernier élément de la liste, et de faire pointer le pointeur suivant
de la cellule qui la précédait sur « Null ».

Cette fonction doit par conséquent être récursive, puisque le seul endroit par
lequel on peut visiter la liste est le pointeur du début de la liste. Si la
première cellule de la liste pointe sur « Null », on supprime celle ci et on fait
pointer « DebutListe » sur « Null », sinon la fonction de suppression étudie la
liste qui commence par la deuxième cellule et qui retournera « Null » s'il
s'agit de la dernière cellule, ou une liste composée de cette dernière et de la
liste retournée par la fonction de suppression sur la liste commençant par la
troisième cellule. Et ainsi de suite jusqu'à pointer sur la dernière cellule, celle
à supprimer. Bien sûr, une méthode plus itérative nous ferait positionner un
pointeur qui voyagerait jusqu'à la fin de la liste, pour supprimer le dernier
élément pointé. Mais l'exercice n'aurait plus d'intérêt; cet exemple simple de
suppression récursive nous permet de visualiser plus clairement le principe
de la récursivité.
cellule *SupperssionRecursive(cellule *UneListe)
{
if(UneListe->suivant== NULL)
{
delete UneListe;
return NULL;
}
UneListe->suivant=SuppressionRecursive(UneListe->suivant);
return UneListe;
}

Le destructeur utilisera cette fonction sur « DebutListe » jusqu'à ce que


toutes les cellules de la liste soient supprimées (le pointeur « DebutListe »
pointera alors sur « Null »), et jusqu'à détruire le pointeur initial.

~ListeSimple()
{
while(DebutListe) DebutListe=SuppressionRecursive(DebutListe);
delete DebutListe;
}

iii. Les méthodes publiques

Nous voulons permettre à tout utilisateur de la classe « ListeSimple » de


supprimer n'importe quelle valeur choisie de la liste (et bien sûr la cellule qui
la contient) ou d'en ajouter une nouvelle. Il est important de connaître les
différentes étapes d'une suppression dans la liste.

Supposons une liste de trois cellules, dont on veut supprimer celle du milieu.
Le pointeur suivant de la première cellule doit pointer la troisième cellule. Si
l'on fait cela immédiatement, il sera impossible de détruire physiquement la
deuxième cellule puisque plus aucun pointeur ne se dirige vers elle. Il faut
par conséquent, avant toute chose, positionner un pointeur temporaire sur la
cellule à supprimer, que l'on détruira après la redirection du pointeur suivant
de la première cellule.
La fonction qui exécutera ces phases sera récursive et utilisera la même
idée que la fonction de suppression du dernier élément, sauf qu'ici on
parcourra la liste triée, élément par élément; si aucune cellule ne contient la
valeur recherchée, alors nous n'avons rien à faire.

cellule *SuppressionElement(cellule *UneListe, int val)


{
if(!UneListe)
{
cout << "Element " << val << " non-trouve.\n";
return NULL;
}
if(UneListe->element == val)
{
cellule *Tempo=UneListe;
UneListe = UneListe->suivant;
delete Tempo;
}
else
UneListe->suivant=SuppressionElement(UneListe->suivant, val);
return UneListe;
};

La fonction d'insertion d'un élément est évoquée plus haut; il s'agit de la


fonction récursive d'insertion triée. Pour améliorer la facilité d'accès, nous
créerons deux fonctions d'insertion et de suppression, qui permettront à
l'utilisateur de ne pas gérer les retours des fonctions récursives.
void Supperssion(int Valeur)
{
DebutListe=SuppressionElement(DebutListe, Valeur);
};

void Insertion(int val)


{
DebutListe=InsertionRecursive(DebutListe, val);
};

iv. Le code
#pragma hdrstop
#include <condefs.h>
#include <stddef.h>
#include <stdlib.h>
#include <iostream.h>

//----------------------------------------------------------------------
#pragma argsused

class ListeSimple
{

public :

struct cellule
{
int element;
cellule *suivant;
};

cellule *DebutListe;

// Constructeur : liste chainées à "NbDeCellules" cellules de


// valeurs aléatoires
ListeSimple(int NbDeCellules)
{
DebutListe=NULL;
for(int i=0;i<NbDeCellules;i++)
DebutListe=InsertionRecursive(DebutListe,rand()%50);
};

// Constructeur : liste chainée, l'utilisateur ajoute autant de


// cellules qu'il veut
ListeSimple()
{
DebutListe=NULL;
int Valeur=0;
cout<<"Entrez des valeurs entières (terminez par -1) :";
for(;;)
{
cin>>Valeur;
if(Valeur==-1)break;
DebutListe=InsertionRecursive(DebutListe,Valeur);
}
};

//Destructeur : suppression totale


~ListeSimple()
{ while(DebutListe)DebutListe=SuppressionRecursive(DebutListe);
delete DebutListe;
};
cellule *InsertionRecursive(cellule *UneListe, int val)
{
if(UneListe)
if(UneListe->element<val)
{
UneListe->suivant=InsertionRecursive(UneListe->suivant,val);
return UneListe;
}
cellule *NouvelleCellule=new cellule;
NouvelleCellule->element=val;
NouvelleCellule->suivant=UneListe;
return NouvelleCellule;
};

cellule *SuppressionRecursive(cellule *UneListe)


{
if(UneListe->suivant==NULL)
{
delete UneListe;
return NULL;
}
UneListe->suivant=SuppressionRecursive(UneListe->suivant);
return UneListe;
};

cellule *SuppressionElement(cellule *UneListe,int val)


{
if(!UneListe)
{
cout<<"Element "<<val<<" non-trouvé.\n";
return NULL;
}
if(UneListe->element==val)
{
cellule *Tempo=UneListe;
UneListe=UneListe->suivant;
delete Tempo;
}
else
UneListe->suivant=SuppressionElement(UneListe->suivant,val);
return UneListe;
};

void Suppression(int Valeur)


{
DebutListe=SuppressionElement(DebutListe,Valeur);
};

void Insertion(int val)


{
DebutListe=InsertionRecursive(DebutListe,val);
};

void AffichageListe()
{
cellule *courant;
courant=DebutListe;
while(courant)
{
cout<<"["<<(courant->element)<<"]->";
courant=courant->suivant;
}
cout<<"NULL\n\n";
};
};
int main(int argc, char* argv[])
{
int Valeur;

cout<<"Creation d'une Liste Chainee Simple :\n";


ListeSimple UneListe2;
UneListe2.AffichageListe();
cout<<"\nDonnez une valeur à insérer :";
cin>>Valeur;
UneListe2.Insertion(Valeur);
UneListe2.AffichageListe();
cout<<"\nDonnez une valeur à supprimer :";
cin>>Valeur;
UneListe2.Suppression(Valeur);
UneListe2.AffichageListe();

cout<<"Combien d elements dans la nouvelle liste chainee ?: ";


cin>>Valeur;
cout<<"Creation d'une Liste Chainee Simple de "<<Valeur ;
cout<<" cellules a valeurs aleatoires :\n";
ListeSimple UneListe1(Valeur);
UneListe1.AffichageListe();
cout<<"\nDonnez une valeur à insérer :";
cin>>Valeur;
UneListe1.Insertion(Valeur);
UneListe1.AffichageListe();
cout<<"\nDonnez une valeur à supprimer :";
cin>>Valeur;
UneListe1.Suppression(Valeur);
UneListe1.AffichageListe();
return 0;
}

v. Les résultats

Création d’une Liste Chaînée Simple :


Entrez des valeurs entières (terminez par -1) : 12
15
0
2
98
56
-1
[0]->[2]->[12]->[15]->[56]->[98]->NULL

Donnez une valeur à insérer : 13


[0]->[2]->[12]->[13]->[15]->[56]->[98]->NULL

Donnez une valeur à supprimer : 15


[0]->[2]->[12]->[13]->[56]->[98]->NULL

Combien d’éléments dans la nouvelle liste chaînée ?: 6


Création d’une Liste Chaînée Simple de 6 cellules à valeurs aléatoires :
[15]->[27]->[33]->[35]->[36]->[43]->NULL

Donnez une valeur à insérer : 18


[15]->[18]->[27]->[33]->[35]->[36]->[43]->NULL

Donnez une valeur à supprimer : 13


Élément 13 non-trouvé
[15]->[18]->[27]->[33]->[35]->[36]->[43]->NULL
b. Listes doublement chaînées

Une liste simplement chaînée nous permettait un parcours unidirectionnel. On


pouvait connaître la cellule en suivant une autre; mais pour accéder à la cellule qui
la précédait, ou il nous fallait recommencer le parcours du début de la liste, ou il
fallait prévoir la sauvegarde des adresses mémoires dans un pointeur temporaire.

Les listes doublement chaînées sont doubles justement du fait que leurs cellules
possèdent deux pointeurs, un sur la cellule précédente et un sur la cellule
suivante. Il est donc plus aisé de se déplacer à notre gré dans la liste.

La structure de cellule utilisée par les cellules et la classe ListeDouble seront:

class ListeDouble
{
struct cellule
{
cellule *Precedent;
int Element;
cellule *Suivant;
}

cellule *DebutListe, *FinListe;


}

Nous avons toujours un pointeur sur le début de la liste et, uniquement pour des
raisons d'esthétique, nous positionnerons un pointeur à la fin de la liste.
Remarquons que la liste étant bidirectionnelle, il n'est pas évident de différencier
le début et la fin de la liste, le début de la liste peut être en réalité la fin, et la fin
être le début.
i. Les constructeurs

En créant deux constructeurs, un qui prend en paramètre le nombre de


cellules de la liste, et l'autre qui ne prend pas de paramètres, nous offrirons
la possibilité à un utilisateur de se servir de celui qui lui semble le plus
intéressant. Cette fois, nous laisserons dans les deux cas à l'utilisateur le
choix de valeurs à ajouter dans la liste.
ListeDouble(int NbDeCellules)
{
int ValeurAjout;
DebutListe = NULL;
cout<< "Création d’une Liste Chaînée Double de "<<NbDeCellules<<"\n ";
cout<< "Insérez les valeurs :\n";
for(int i=0;i<NbDeCellules;i++)
{
cin>>ValeurAjout;
AjoutCelluleDébut(ValeurAjout);
}
};

ListeDouble()
{
DebutListe = NULL;
int Valeur = 0;
cout<< "Entrez des valeurs entières (terminez par -1) :"
for(;;)
{
cin>>Valeur;
if (Valeur == -1) break;
AjoutCelluleDebut(Valeur);
}
};

Dans le corps du programme principal, on pourra écrire:

ListeDouble UneListe(NombreDeCellules);

ou :

ListeDouble UneListe;
Contrairement à la fonction d'insertion triée des listes simplement chaînées,
nous ne voulons pas créer une liste triée. La liste sera créée en plaçant
chaque nouvelle valeur au début de la liste sans s'occuper de son
importance. Nous nous occuperons de la phase de tri plus tard.

void AjoutCelluleDebut(int val)


{
cellule *Nouveau=new cellule;
Nouveau->Element=val;
Nouveau->Suivant=NULL;
Nouveau->Precedent=NULL;
if (DebutListe == NULL)
DebutListe=FinListe=Nouveau;
else
{
Nouveau->Suivant=DebutListe;
DebutListe->Precedent=Nouveau
DebutListe=Nouveau;
}

};

« AjoutCelluleDebut » crée une nouvelle cellule avec la valeur à ajouter.


Celle ci devient le début de la liste, et son pointeur suivant pointe sur
l'ancienne cellule de début, et son pointeur précédent sur « Null » (si la liste
n'était pas vide). Le pointeur précédent dans l'ancienne cellule de début
pointe sur la nouvelle cellule.

ii. Le destructeur

Il s'agit toujours du même principe de destruction. Il faut supprimer toutes les


cellules une par une, mais pour changer nous allons commencer au début
de la liste.

La fonction « SuppressionCelluleDebut » n'est pas compliquée: on fait


pointer le pointeur de début de liste sur son suivant, et on détruit l'ancienne
cellule de début de liste, sans oublier de faire pointer le pointeur suivant de
la nouvelle cellule de début sur « Null ».

void SuppressionCelluleDebut()
{
cellule *Tempo=DebutListe;
DebutListe=Tempo->Suivant;
DebutListe->Precedent=NULL;
delete Tempo;
};
Dans le destructeur, il faudra appliquer cette fonction tant que la liste n'est
pas vide :

~ListeDouble()
{
while (DebutListe->Suivant) SuppressionCelluleDebut();
delete DebutListe;
};

iii. Le code
#pragma hdrstop
#include <condefs.h>
#include <stddef.h>
#include <iostream.h>

//----------------------------------------------------------------------
#pragma argsused

class ListeDouble
{

struct cellule
{
cellule *Precedent;
int Element;
cellule *Suivant;
};

cellule *DebutListe, *FinListe;

public :
// Constructeur
ListeDouble(int NbDeCellules)
{
int ValeurAjout;
DebutListe=NULL;
cout<<"Création d une Liste Chainee Double de "<<NbDeCellules<<" cellules\nInserez
les valeurs : \n";
for(int i=0;i<NbDeCellules;i++)
{
cin>>ValeurAjout;
AjoutCelluleDebut(ValeurAjout);
}
};

// Constructeur
ListeDouble()
{ DebutListe=NULL;
int Valeur=0;
cout<<"Entrez des valeurs entières (terminez par -1) :";
for(;;)
{
cin>>Valeur;
if(Valeur==-1)break;
AjoutCelluleDebut(Valeur);
}

};
// destructeur
~ListeDouble()
{
while(DebutListe->Suivant)SuppressionCelluleDebut();
delete DebutListe;
};

void AjoutCelluleDebut(int val)


{
cellule *Nouveau=new cellule;
Nouveau->Element=val;
Nouveau->Suivant=NULL;
Nouveau->Precedent=NULL;
if(DebutListe==NULL)
DebutListe=FinListe=Nouveau;
else
{
Nouveau->Suivant=DebutListe;
DebutListe->Precedent=Nouveau;
DebutListe=Nouveau;
}
};

void SuppressionCelluleDebut()
{
cellule *Tempo=DebutListe;
DebutListe=Tempo->Suivant;
DebutListe->Precedent=NULL;
delete Tempo;
};

void Affiche()
{
cellule *Tempo=DebutListe;
cout<<"Null";
while(Tempo)
{
cout<<"<-["<<Tempo->Element<<"]->";
Tempo=Tempo->Suivant;
};
cout<<"Null\n";
};

cellule* Partage(cellule *C1,cellule *C2,cellule *Pivot)


{
cellule *Gauche,*Droite;
Gauche=C1;
Droite=C2;
Echange(Pivot,C2);
while(Droite&&TestDeLocalisation(Gauche,Droite)>0)
{
while((Gauche->Element)<(C2->Element))Gauche=Gauche->Suivant;
while(Droite&&(Droite->Element)>=(C2->Element))Droite=Droite->Precedent;
if(TestDeLocalisation(Gauche,Droite)==2)
{
Echange(Gauche,Droite);
Gauche=Gauche->Suivant;Droite=Droite->Precedent;
}
};

Echange(Gauche,C2);
return Gauche;
};
int TestDeLocalisation(cellule *C1, cellule *C2)
{
cellule *Tempo=DebutListe;
if(C1==C2) return 1; //retourne 1 si C1 et C2 pointe sur la
// même cellule
while(Tempo&&Tempo!=C1&&Tempo!=C2)Tempo=Tempo->Suivant;
if(Tempo==C1&&C2)return 2; //retourne 2 si C1 pointe avant C2
if(Tempo==C2&&C1)return 0; //retourne 0 si C2 pointe avant C1
return -1;//retourne -1 si erreur ou si un des pointeurs est NULL
};

void Echange(cellule *C1, cellule *C2)


{
int Tempo;
if(C1&&C2) //si les deux cellules ne sont pas NULL
{
Tempo=C1->Element;
C1->Element=C2->Element;
C2->Element=Tempo;
}
};

cellule* Milieu(cellule *C1, cellule *C2)


{
cellule *Tempo=C1;
int retour=0,i;
while(Tempo!=C2)
{
Tempo=Tempo->Suivant;
retour++;
};
Tempo=C1;
for(i=0;i<(retour/2);Tempo=Tempo->Suivant,i++);
return Tempo;
};

void Tri(cellule *C1, cellule *C2)


{
cellule *Pivot,*Tempo;
if(C1&&C2&&TestDeLocalisation(C1,C2)==2)
{
Pivot=Milieu(C1,C2);
Tempo=Partage(C1,C2,Pivot);
if(C1!=Tempo->Precedent)
Tri(C1,Tempo->Precedent);
if(C2!=Tempo->Suivant)
Tri(Tempo->Suivant,C2);
}
};

void TriFusion()
{
Tri(DebutListe,FinListe);
};
};
int main(int argc, char* argv[])
{
int NbCellules=10;
cout<<"Combien de cellules dans votre Liste Chainées Double ?: ";
cin>>NbCellules;
ListeDouble UneListe1(NbCellules);
cout<<"Liste non triée :\n";
UneListe1.Affiche();
UneListe1.TriFusion();
cout<<"Liste Triée : \n";
UneListe1.Affiche();

cout<<"\nCréation d'une nouvelle Liste Chainée Double :\n";


ListeDouble UneListe2;
cout<<"Liste non triée :\n";
UneListe2.Affiche();
UneListe2.TriFusion();
cout<<"Liste Triée : \n";
UneListe2.Affiche();
return 0;
}

iv. Les résultats

Combien de cellules dans votre Liste Chaînée Double ? : 5


Création d’une Liste Chaînée Double de 5 cellules
Insérez les valeurs :
6
4
5
1
2
Liste non triée :
Null<-[2]-><-[1]-><-[5]-><-[4]-><-[6]->Null
Liste Triée :
Null<-[1]-><-[2]-><-[4]-><-[5]-><-[6]->Null

Création d’une nouvelle Liste Chaînée Double :


Entrez des valeurs entières (terminez par -1) :6
6
7
3
4
5
8
1
-1
Null<-[1]-><-[8]-><-[5]-><-[4]-><-[3]-><-[7]-><-[6]-><-[6]->Null
Liste Triée :
Null<-[1]-><-[3]-><-[4]-><-[5]-><-[6]-><-[6]-><-[7]-><-[8]->Null
c. Arbres binaires

i. Les structures de données arborescentes

Il s'agit d'une des structures de données les plus utilisées en informatique.


La gestion par arbre est la manière la plus pratique de résoudre un
problème, et ce par sa décomposition en sous problèmes. Autrement dit, un
arbre permet de structurer une analyse en dégageant des sous analyses. La
structuration d'un livre en chapitres, eux mêmes divisés en sections,
paragraphes... et le découpage d'un programme C++ en procédures et en
fonctions sont deux applications directes des arbres.

ii. Les arbres binaires

En passant des tableaux aux listes chaînées, nous avions résolu le


problème du dynamisme des structures linéaires. Le but du jeu est
maintenant de « dimensionner » ces structures: en clair, on ne veut plus
une structure dans laquelle un objet ne possède qu'un successeur (un seul
fils), mais une structure dite « arborescente ».

Pour digérer plus facilement le passage entre ces deux types de


structures, le premier exemple étudié aura la caractéristique suivante :
chaque objet possédera au plus deux successeurs. Cette structure est
appelée arbre binaire.

Un arbre binaire est constitué d'un ensemble d'objets (de 0 à 2) et d'une


racine. Chaque objet (communément appelé noeud) est lui même la racine
d'un autre arbre (appelé sous arbre) ou la terminaison d'une branche (la
feuille). Il est cependant utile de différencier une feuille (arbre binaire à 0
objet) et un sous arbre (arbre binaire de 0, 1 ou 2 objets).

On peut donc voir l'arbre binaire comme un sommet auquel on accroche


deux sous arbres et en déduire une approche récursive de la structure.
Nous avons déjà vu l'utilité de la récursivité dans certains algorithmes des
listes chaînées, mais là où, dans la gestion de ces listes, nous pouvions
être tentés de remplacer un appel récursif par une boucle (While, For ... ),
cette opération se révèle ici beaucoup plus compliquée, puisque d'une
racine de l'arbre binaire plusieurs possibilités sont offertes: descendre
d'abord vers la gauche, ou d'abord vers la droite. La récursivité nous
permettra de simplifier au maximum la gestion d'un arbre binaire.

Nous pouvons dès à présent créer une classe « ArbreBinaire » en


remarquant, comme avec les listes chaînées, que pour avoir accès à un
arbre binaire entier il suffit de connaître l'adresse de sa racine et d'y
accrocher un pointeur.

class ArbreBinaire
{
private:

struct Sommet
{
int Element; // donnée à inscrire dans l’arbre
Sommet *FilsGauche // pointeur sur le sous-arbre gauche
Sommet *FilsDroit // pointeur sur le sous-arbre droit
}

public:

Sommet *Racine; // pointeur sur la racine de l’arbre


// binaire

};
iii. Le constructeur

Le constructeur de la classe « ArbreBinaire » ne possède pas d'algorithme


particulier. L'utilisateur qui voudra créer un arbre binaire sera le seul à
savoir comment il veut hiérarchiser les éléments; il peut par conséquent
placer des noeuds où bon lui semble. Par exemple, si l'on veut un arbre
binaire à sept éléments :

Le constructeur sera :

ArbreBinaire()
{
Racine=NULL;
Racine=Insertion(Racine,10);
Racine->FilsGauche=Insertion(Racine->FilsGauche,20);
Racine->FilsDroit=Insertion(Racine->FilsDroit,30);

Racine->FilsGauche->FiIsGauche=
Insertion(Racine->FilsGauche->FilsGauche,40);

Racine->FilsGauche->FilsDroit=
Insertion(Racine->FilsGauche->FilsDroit,50);

Racine->FilsDroit->FilsGauche=
Insertion(Racine->FilsDroit->FilsGauche,60);

Racine->FilsDroit->FilsDroit=
Insertion(Racine >FilsDroit >FilsDroit,70);

}
et utilise la méthode :

Sommet *Insertion(Sommet *UnAB,int Valeur)


{
if(!UnAB) // Seulement s'il n y a pas déjà un nœud
{
Sommet *Nouveau=new Sommet;
Nouveau->Element=Valeur;
Nouveau >FilsGauche=NULL;
Nouveau >FilsDroit=NULL;
return Nouveau;
}
return UnAB;
}

Dans notre programme principal nous créerons un arbre binaire de la


manière suivante :

ArbreBinaire UnArbreBinaire;

iv. Le destructeur

C'est le premier exemple de récursivité. Le destructeur va permettre de


libérer toute la mémoire qui a été utilisée pour la création de l'arbre binaire.
Penser qu'il suffit de supprimer la racine de l'arbre binaire serait une
erreur, car on ne libérerait que l'espace mémoire du premier objet, et non
pas celui de chaque objet. Nous allons en fait supprimer tous les objets
(les sous arbres) de l'arbre binaire un par un, par appel récursif, en faisant
en sorte que chaque sous-arbre traité devienne une feuille avant d'être
supprimé.

Pour mieux comprendre, prenons un arbre binaire très simple : une racine
et deux feuilles. On commence par traiter le fils gauche : comme il s'agit
d'une feuille, on la supprime par (delete Racine->FilsGauche) et on fait
pointer le fils gauche de la racine sur « NULL ». Même chose pour le fils
droit et pour la racine devenue feuille. La destruction est terminée lorsqu'on
a détruit le pointeur de la racine.
Pour ce faire, il nous faut la fonction « Suppression » (méthode de la classe
« ArbreBinaire »), qui supprime récursivement un arbre binaire donné. Cette
fonction peut d'ailleurs être utilisée en dehors du destructeur, par exemple si
on veut supprimer un sous-arbre :

Sommet* Suppression(Sommet *UnAB)


{
if(UnAB->FilsGauche)UnAB->FilsGauche=Suppression(UnAB->FilsGauche);
if(UnAB->FilsDroit)UnAB->FilsDroit=Suppression(UnAB->FilsDroit);
delete UnAB;
return NULL;
}

Le destructeur de la classe « ArbreBinaire » sera tout simplement :

~ArbreBinaire()
{
Suppression(Racine);
}

v. Les méthodes publiques

Le travail sur un arbre binaire nécessite de pouvoir parcourir la totalité des


nœuds. Pour cela plusieurs techniques de parcours peuvent être utilisées:
le parcours symétrique, le parcours suffixe et le parcours préfixe.

Le parcours symetrique: le traitement un noeud intervient après celui de


son sous-arbre gauche, mais avant le traitement de son sous-arbre droit.
void ParcoursSym(Sommet *UnAB)
{
if (UnAB->FilsGauche)ParcoursSym(UnAB->FilsGauche);
cout<<UnAB->Element<<" ";
if(UnAB->FilsDroit)ParcoursSym(UnAB->FilsDroit);
};

Le parcours suffixe: le traitement d'un nœud intervient après celui de son


sous-arbre gauche et celui de son sous-arbre droit.

void ParcoursSuf(Sommet *UnAB)


{
if(UnAB->FilsGauche)ParcoursSuf(UnAB->FilsGauche);
if(UnAB->FilsDroit)ParcoursSuf(UnAB->FilsDroit);
cout<<UnAB->Element<<" ";
};

Le parcours préfixe: le traitement d'un nœud intervient avant celui de son


sous-arbre gauche et celui de son sous-arbre droit.

void parcoursPre(Sommet *UnAB)


{
cout<<UnAB->Element<<" ";
if(UnAB->FilsGauche)ParcoursPre(UnAB->FilsGauche);
if(UnAB->FilsDroit)ParcoursPre(UnAB->FilsDroit);
}

Ces trois petites méthodes ne font qu'afficher à l'écran les valeurs des
nœuds d'un arbre binaire selon différents parcours; nous verrons plus tard
comment stocker ces valeurs pour qu'un programmeur, ou même un
utilisateur, puisse les ré-utiliser plus tard, après même la fin du
programme.

La profondeur d'un arbre correspond au nombre de niveaux ou, plus


clairement, au nombre de nœuds contenus dans la plus grande branche
de l'arbre. Par exemple, un arbre binaire ne possédant comme noeuds
qu'une unique racine serait de profondeur 1.

Pour ce faire, on utilise une méthode récursive, qui incrémente la valeur de


la profondeur de la branche la plus longue partant du nœud traité.

int Profondeur(Sommet *UnAB)


{
int valeur;
if(!UnAB)return 0;
valeur=l+Maximum(Profondeur(UnAB->FilsGauche),
Profondeur(UnAB >FilsDroit));
return valeur;
}
Comme son nom l'indique la fonction « Maximum » retourne le plus grand
de deux entiers :

int Maximum(int Val1,int Val2)


{
if(Val1>Val2)return Val1;
return Val2;
}

La dernière méthode utilisée nous permettra d'afficher les informations sur


un arbre binaire :

void ImprimeArbre()
{
cout "\nProfondeur de l'arbre :"<<Profondeur(Racine);
cout«"\nParcours Prefixe : ";
ParcoursPre(Racine);
cout«"\nParcours Symetrique : "
ParcoursSym(Racine);
cout "\nParcours Suffixe : "
ParcoursSuf(Racine);
};

vi. Le code
#pragma hdrstop
#include <condefs.h>
#include <stddef.h>
#include <iostream.h>
//----------------------------------------------------------------------
#pragma argsused

class ArbreBinaire
{
struct Sommet
{
int Element;
Sommet *FilsGauche;
Sommet *FilsDroit;
};

public :

Sommet *Racine;

ArbreBinaire()
{
Racine=NULL;
Racine=Insertion(Racine,10);
Racine->FilsGauche=Insertion(Racine->FilsGauche,20);
Racine->FilsDroit=Insertion(Racine->FilsDroit,30);
Racine->FilsGauche->FilsGauche=Insertion(Racine->FilsGauche>FilsGauche,40);
Racine->FilsGauche->FilsDroit=Insertion(Racine->FilsGauche->FilsDroit,50);
Racine->FilsDroit->FilsGauche=Insertion(Racine->FilsDroit->FilsGauche,60);
Racine->FilsDroit->FilsDroit=Insertion(Racine->FilsDroit->FilsDroit,70);
};

~ArbreBinaire()
{
Suppression(Racine);
};
Sommet *Insertion(Sommet *UnAB,int Valeur)
{
if(!UnAB)
{
Sommet *Nouveau=new Sommet;
Nouveau->Element=Valeur;
Nouveau->FilsGauche=NULL;
Nouveau->FilsDroit=NULL;
return Nouveau;
}
return UnAB;
};

// Suppression d'un sous- arbre (fonction récursive)


Sommet* Suppression(Sommet *UnAB)
{
if(UnAB->FilsGauche)UnAB->FilsGauche=Suppression(UnAB->FilsGauche);
if(UnAB->FilsDroit)UnAB->FilsDroit=Suppression(UnAB->FilsDroit);
delete UnAB;
return NULL;
};

int Profondeur(Sommet *UnAB)


{
int valeur;
if(!UnAB)return 0;
valeur=1+Maximum(Profondeur(UnAB->FilsGauche),Profondeur(UnAB->FilsDroit));
return valeur;
};

int Maximum(int Val1,int Val2)


{
if(Val1>Val2)return Val1;
return Val2;
};

void ParcoursPre(Sommet *UnAB)


{
cout<<UnAB->Element<<" ";
if(UnAB->FilsGauche)ParcoursPre(UnAB->FilsGauche);
if(UnAB->FilsDroit)ParcoursPre(UnAB->FilsDroit);
};

void ParcoursSym(Sommet *UnAB)


{
if(UnAB->FilsGauche)ParcoursSym(UnAB->FilsGauche);
cout<<UnAB->Element<<" ";
if(UnAB->FilsDroit)ParcoursSym(UnAB->FilsDroit);
};

void ParcoursSuf(Sommet *UnAB)


{
if(UnAB->FilsGauche)ParcoursSuf(UnAB->FilsGauche);
if(UnAB->FilsDroit)ParcoursSuf(UnAB->FilsDroit);
cout<<UnAB->Element<<" ";
};

void ImprimeArbre()
{
cout<<"\nProfondeur de l'arbre : "<<Profondeur(Racine);
cout<<"\nParcour Prefixe : ";
ParcoursPre(Racine);
cout<<"\nParcour Symetrique : ";
ParcoursSym(Racine);
cout<<"\nParcour Suffixe : ";
ParcoursSuf(Racine);
};
};
int main(int argc, char* argv[])
{
ArbreBinaire UnArbreBinaire;
UnArbreBinaire.ImprimeArbre();
return 0;
}

vii. Les résultats

L’arbre étudié :

Les résultats :

Profondeur de l'arbre : 3
Parcours Préfixé : 10 20 40 50 30 60 70
Parcours Symétrique : 40 20 50 10 60 30 70
Parcours Suffixe : 40 50 20 60 70 30 10
COURS 09

INF-11299 – PROGRAMMATION II

Développement des programmes orientés objet et conception des


interfaces graphiques
Références : Meyer, Bertrand, Conception et programmation orientées objet,
Eyrolles, Paris, 2000, 1223pp. ISBN : 2-212-09111-7.

Cardon, Alain & Dabancourt, Christophe, Initiation à l’algorithmique objet


Eyrolles, Paris, 2001, 379pp. ISBN : 2-212-09258-X.

Pressman, Roger S., Software engineering : a practitioner's approach, 5ième édition


New York, N.Y. : McGraw-Hill, c2001.

Approche et concepts de la programmation


structurée
Le concept principal de la programmation structurée peut se décrire brièvement par
l’évitement de l’emploi d’instructions de branchement direct (exemple : instruction
« GOTO ») lors de la programmation du code.

Les langages de programmation évolués implantent des constructions de programmation


structurée qui sont :

la séquence : instructions ou blocs d’instructions accessibles de manière séquentielle,

la conditionnelle : les instructions de type : IF-THEN-ELSE, CASE, SWITCH, etc; et

la boucle : instructions de type : FOR, WHILE, REPEAT, etc.

L’approche de la programmation structurée stipule que toutes les instructions de


branchement direct peuvent être implantées avec les constructions de la programmation
structurée.

Approche et concepts de la programmation modulaire


La nécéssité d’une architecture flexible des systèmes, composée de composante
logicielles autonomes, découle des objectifs d’extensibilité et de réutilisabilité qui sont
deux facteur de qualité de la conception du code. Le concept de modularité combine ces
deux facteurs de qualité.
La programmation modulaire était autrefois conçue comme la construction de
programmes par assemblage de petites unités, typiquement des sous-programmes. Mais
une telle technique ne peut améliorer l’extensibilité et la réutilisabilité que si nous pouvons
garantir aux parties résultantes – les modules – une meilleure organisation autonome au
sein d’architectures stables. Toute définition complète de la modularité doit viser ces
propriétés.

Une méthode de construction logicielle est donc modulaire si elle aide les concepteurs à
produire des systèmes logiciels à partir d’éléments autonomes connectés par une
structure simple et cohérente.

Une méthode de conception digne d’être appelée modulaire devrait répondre à cinq
exigences fondamentales :

1. décomposabilité,

2. composabilité,

3. compréhensibilité,

4. continuité; et

5. protection.

¾ Décomposabilité modulaire : une méthode de construction du logiciel vérifie la


décomposabilité modulaire si elle facilite la tâche de décomposition d’un problème
logiciel en un petit nombre de sous-problèmes moins complexes, reliés entre eux
par une structure simple, et suffisamment indépendants pour permettre de
travailler séparément sur chacun d’eux.

L’exemple le plus évident d’une méthode conçue pour satisfaire le critère de


décomposabilité est la conception descendante (l’approche « top-down »). Cette
méthode incite les concepteurs à commencer par une description la plus abstraite
possible de la fonctionnalité du système, puis à raffiner cette vision à travers des
étapes successives, en décomposant, à chaque étape, chaque sous-système en
un petit nombre de sous-systèmes plus simples, jusqu’à ce que tous les éléments
restants soient d’un niveau d’abstraction suffisamment bas pour être directement
implémentés.

¾ Composabilité modulaire : Une méthode satisfait à la composabilité modulaire si


elle facilite la production d’éléments logiciels qui peuvent ensuite être combinés
librement avec d’autres pour produire de nouveaux systèmes, éventuellement
dans un environnement très différent de celui pour lequel ils ont été initialement
développés.

La composabilité est directement reliée à l’objectif de réutilisabilité. Exemple :


bibilothèque de sous-programmes.
¾ Compréhensibilité modulaire : une méthode favorise la compréhensibilité
modulaire si elle facilite la production de logiciels dans lesquels un lecteur humain
peut comprendre chaque module sans avoir à connaître les autres ou, au pire,
après examen de quelques uns.

¾ Continuité modulaire : une méthode vérifie la continuité modulaire si, dans les
architectures logicielles qu’elle aide à créer, un changement mineur dans la
spécification du problème ne déclenche un changement que dans un seul module
ou un petit nombre de modules.

¾ Protection : une méthode vérifie la protection modulaire si elle tend à produire des
architectures dans lesquelles l’effet d’une condition anormale se produisant à
l’exécution, dans un module donné sera confiné à ce module ou, au pire, ne se
propagera qu’à quelques modules voisins.

Des critères précédents, on déduit cinq règles que nous devons observer pour
assurer la modularité :

1. correspondance directe,

2. peu d’interfaces,

3. petites interfaces (couplage faible),

4. interfaces explicites; et

5. rétention d’information.

¾ correspondance directe : la structure modulaire envisagée lors du processus


de construction d’un système logiciel devrait rester compatible avec toute
structure modulaire envisagée lors du processus de modélisation du domaine
du problème.

¾ peu d’interfaces : chaque module devrait communiquer avec le minimum


d’autres modules.

¾ petites interfaces (couplage faible) : si deux modules communiquent, ils


doivent échanger aussi peu d’informations que possible.

¾ interfaces explicites : lorsque deux modules « A » et « B » communiquent, cela


doit apparaître de manière évidente dans le code de A, de B ou des deux.

¾ rétention d’information (encapsulation) : le concepteur de tout module doit


choisir un sous-ensemble des propriétés du module pour représenter
l’information officielle de ce module, information qui sera rendue publique aux
auteurs des modules clients.
Techniques d’élaboration des programmes
L’étape de conception d’un algorithme est celle durant laquelle on définit l’architecture de
l’algorithme (la façon dont il est composé), ce qui va permettre ensuite de calculer la
solution du problème. Les instructions que nous avons décrites jusqu’à maintenant
permettent de construire des algorithmes simples en les considérant comme des suites
linéaires d’instructions.

Il sera nécessaire, lorsque l’algorithme ne sera pas trivial, de préciser son architecture,
ses différents éléments et la manière dont ils interagissent, et de bien définir les rôles de
ces éléments. Pour faciliter cette conception très modulaire, nous introduirons la notion
centrale d’objet, sur laquelle nous fonderons l’architecture de tous les algorithmes.

Les phases de développement d’un programme :

À partir de la définition d’un problème, la démarche classique pour obtenir des résultats
revient à suivre cinq étapes successives. Ces étapes sont les suivantes :

1. le cahier des charges,

2. la phase d’analyse,

3. la phase de conception,

4. l’écriture du programme; et

5. la phase de tests.

¾ Le cahier des charges : On appréhende le problème par un énoncé simple, écrit


en langage naturel, c’est à dire dans la langue courante. Ce premier énoncé
général est une spécification initiale du problème. Cela permet de préciser ce que
l’on veut, et sur quelles hypothèses on se base et sous quelle forme on veut
produire les résultats.

¾ La phase d’analyse : Il faut ensuite transformer ce cahier des charges pour


dégager les différentes fonctions de traitement dont les résultats successifs
produiront, par agrégation, les sorties du programme. Les questions que l’on se
pose alors sont :

a. Quelles sont exactement les fonctions ou les modules de traitement?

b. Quelles sont les données à traiter et quel est le résultat attendu du


programme?

c. Que fait-il exactement faire?


Nous analysons ici le problème.

¾ La phase de conception : Dans cette phase, nous décrivons précisément les


différentes étapes des calculs, où l’on nomme les fonctions avec leurs variables,
où l’on précise leurs résultats, comment et où et quand on manipule les valeurs
des variables. On s’intéresse à la façon de faire ce qui a été précisé dans la phase
d’analyse. C’est la phase de conception de l’algorithme.

¾ L’écriture du programme : Dans cette phase, on traduira les différentes fonctions


en langage algorithmique, en précisant très exactement les instructions utilisées.
On traduira ensuite l’algorithme en langage de programmation. Ce sera la phase
dite d’écriture de l’algorithme et de la programmation.

¾ La phase de tests : cette phase vérifie le travail accompli durant les phases
précécentes. Les programmes sont compilés et exécutés avec des jeux d’essais
afin de savoir si les résultats prédits ont été obtenus.

Ces cinq étapes sont les étapes classiques du développement des produits informatiques
selon la démarche du génie logiciel.

Conception d’algorithmes à l’aide de la programmation orientée objet


Les algorithmes sont réalisés selon une approche modulaire représentant tout système
logiciel comme étant un ensemble d’entités qui effectuent des calculs et des traitements
spécifiques et qui sont capables de s’envoyer des messages pour demander le lancement
des calculs et l’obtention des résultats.

Dans cette approche, il s’agira d’abord de trouver l’ensemble de ces entités représentant
les éléments (classes et objets) du problème et de préciser les messages
(méthodes/interruptions) que les éléments sont capables d’envoyer de de recevoir.

CONSTRUCTION DE LOGICIEL ORIENTÉ OBJET


La construction de logiciel orienté objet est la méthode de développement logiciel qui
fonde l’architecture de tout système logiciel sur des modules déduits des types des objets
qu’il manipule (plutôt que sur la ou les fonctions que le système est supposé remplir).

Le but de la conception orientée objet est la décomposition des tâches de programmation


en en méthodes faisant partie de classes déterminées.
La méthode proposée pas Booch définit les étapes suivantes de la conception du
logiciel :

¾ identification des classes,

¾ identification des fonctionnalités des classes (méthodes); et;

¾ identification des relations entre les classes (héritage et aggrégation).

Vérification du code
Tests boîte blanche

Les tests boite blanche consistent à créer des jeux d'essais validant le code de l'application. On
peut ainsi vérifier si :

• tous les chemins ont été parcourus au moins une fois,

• les conditions vrai/faux ont été validées,

• les boucles sont exécutées correctement,

• les valeurs limites sont correctement définies,

• les structures de données internes sont valides,

• il n'y a pas de fuite mémoire, etc.

Tests de chemin

Ces tests ont pour but de vérifier que tous les chemins prévus sont parcourrus au moins une fois
pendant les tests. On définit ainsi des graphiques de flux dans lesquels les nœuds représentent
des modules, les bordures décrivent les contrôles de flux (une bordure doit terminer un nœud
même s'il ne contient pas d'information).

On définit aussi des régions qui sont des zones fonctionnelles bordées par des nœuds et des
bordures ainsi que des nœuds à prédicats qui contiennent une condition.
Tests de boucles

Les tests de boucles valident les conditions d'exécution des boucles. Quatre classes de boucles
sont ainsi définies :

• boucles simples,

• boucles imbriquées,

• boucles concaténées (boucles utilisant le même compteur); et

• boucles non structurées (sortie de la boucle par l'instruction GOTO).

Les jeux d'essais mis en œuvre pour ces tests sont basés sur les valeurs limites de la boucle.

On testera ainsi:

• le cas où la boucle n'est pas du tout exécutée,

• le cas où elle n'est exécutée qu'une seule fois; et

• le cas où elle est exécutée plusieurs fois.

Tests de boîte noire

Les tests boîte noire sont exécutés par des testeurs ayant une connaissance du métier, aidés par
des programmeurs si des outils de tests automatiques sont utilisés.

Les tests boîte noire sont des tests fonctionnels (au sens large). Ils ont pour objectif de
rechercher ou de valider:

• le respect des exigences,

• les fonctions incorrectes ou manquantes,

• les incohérences au niveau de l'interface,

• les erreurs d'accès aux données,

• les problèmes de performance de l'application; et

• les conditions initiales ou finales d'une fonction.

On constate que ces tests s'attaquent à des problèmes de nature différente que ceux abordés
dans la phase de tests boîte blanche.
Comme on ne connaît pas le fonctionnement interne de l'application, on doit définir un
comportement supposé, et vérifier si l'application est conforme à ce comportement. Ces tests, et
les anomalies constatées, sont donc directement liés aux spécifications.

L'intérêt principal des tests boîte noire est qu'ils valident les fonctionnalités du logiciel plus que la
qualité du développement. Ils sont donc plus proches des préoccupations quotidiennes des
utilisateurs.

Génération de séquences de test (jeux d’essais)


RÉFÉRENCE : PRESSMAN, CHAPITRE 17

17.6.3 ANALYSE DES CAS LIMITES

¾ Un plus grand nombre d’erreurs a tendance à se produire aux limites d’un domaine de
valeurs d’entrée. C’est pour cela que l’analyse des cas limites a été développée comme
étant l’une des techniques du test de logiciel.

¾ L’analyse des valeurs limites conduit à une série de scénarios de tests au niveau des
valeurs limites des entrées-sorties.

¾ Lignes directrices :

o limites [a,b] : tester des valeurs un peu plus petites et un peu plus grandes que
« a » et « b ». Si une entrée possède un domaine, des devis d’essais devraient
tester les limites de ce domaine en fournissant des valeurs d’entrées un peu
inférieures et un peu supérieures aux limites du domaine.

Exemple : domaine [0 et 100]


a = -1, -0.1, 0, 0.1, 1
b = 99, 99.9, 100, 100.1, 101

o données structurées (vecteurs, matrices) :

Vérifier la structure en fournissant des valeurs d’indices un peu inférieures et un


peu supérieures aux dimensions de la structure.

Exemple : Vecteur_Nbre = ARRAY[1..100] OF INTEGER;

Fournir des valeurs d’indices telles que 0 et 101


17.7.1 TEST DES INTERFACES USAGER GRAPHIQUES(GUI)

¾ Le test des interfaces usager graphiques(GUI) présentent des défis intéressants pour les
concepteurs de logiciels :

o vérification de composantes logicielles réutilisables; et

o évolution constante de la complexité de ces interfaces.

¾ En raison de leur similarité, les interfaces graphiques peuvent avoir une série de tests
standard vérifiant des données spécifiques et des composantes graphiques :

o composantes graphiques (menus, icônes, etc.); et

o données spécifiques (paramètres de configuration).

¾ La multitude de fonctions des interfaces graphique fait en sorte qu’il serait préférable que
certains éléments de l’interface graphique pourraient être vérifiés avec des outils
automatisés :

o tests de configuration des périphériques tels que l’imprimante, l’écran, la souris,


numériseur, modem, etc.

o fonction « plug and play » du système d’exploitation Windows.

¾ Les tests suivants sont significatifs dans la vérification d’une interface usager
graphique(GUI) :

o temps de réponse(dans l’ordre de la seconde),

o simplicité d’utilisation,

o facilité d’installation et de configuration,

o fiabilité (pannes/blocage du logiciel),

o fonctions d’aide(conviviale « friendly user » ou rudimentaire); et

o documentation de système et manuel de l’utilisateur.


Documentation du code (commentaires)
Dans tout programme développé de manière professionnelle, des commentaires doivent
être placés.

Les commentaires servent principalement à :

¾ décrire textuellement des algorithmes, des fonctions, des routines, des variables
et des constantes,

¾ aider à la lecture du code,

¾ désactiver certaines instructions ou fragments de code; et

¾ certains commentaires sont laissés à la discrétion des programmeurs ou des


analystes et fournissent des renseignements supplémentaires tels que :

o le nom du ou des programmeurs,

o dates de conception ou de révision du programme,

o environnement de développement; et

o type de compilateur.

Les élements suivants du code ont généralement besoin d’être décrits par des
commentaires :

¾ constantes,

¾ variables,

¾ fichiers,

¾ sous-routines,

¾ fonctions,

¾ structures de données,

¾ classes, attributs et méthodes; et

¾ instructions de branchement.
Le fait de produire du code sans y placer de commentaires ne constitue pas une
économie de temps. Il faudra ultérieurement allouer plus de temps à la lecture et à
la compréhension du code.

Un moyen sûr d’obtenir du code bien documenté est de rédiger les commentaires
durant la programmation et non après.

Les firmes de consultation informatique importantes ont généralement développé


des normes de documentation du code et de rédaction des commentaires.

Manuel de l’usager
Les manuels doivent être écrits pour différents types d'utilisateurs:

1. Les utilisateurs finaux. Ils utilisent les instructions pour interpréter les rapports, choisir
différentes options disponibles et accéder au système. Fondamentalement, le manuel
décrit le « quoi » du système.

2. Les utilisateurs secondaires. Pour satisfaire cette catégorie, les manuels incluent diverses
instructions sur la façon d'entrer les données, ils précisent surtout « comment » le
système fonctionne.

3. Le personnel de l'informatique. Les manuels précisent les procédures de maintenance


devant être effectuées par les opérateurs et le personnel chargé du contrôle de
fonctionnement. Ces procédures comprennent les instructions d'assurance-qualité, des
informations sur les fichiers de sauvegarde et la documentation.

4. La fomation. Souvent un manuel est développé pour la mise en place du système et la


formation.

Il est important que les procédures et la documentation soient mises à jour à chaque fois qu'il y a
des modifications.
Conception et évaluation des interfaces graphiques

PROCESSUS DE CONCEPTION ET DE
DÉVELOPPEMENT
¾ Principes de Gould & Lewis, 1983 :

1. Focaliser dès le début sur les utilisateurs.

2. Conception interactive.

3. Mesures empiriques (évaluation formative).

4. Conception itérative.

¾ Principes de Carroll & Rosson 1985 :

1. La conception est un processus, ce n’est pas un état et elle ne peut pas être
adéquatement représentée de façon statique.

2. Le processus de conception est non-hiérarchique, il n’est ni strictement ascendant


(bottom-up), ni strictement descendant (top-down).

3. Ce processus est radicalement évolutif, impliquant le développement de solutions


partielles et temporaires qui peuvent ne jouer aucun rôle dans la conception finale.

¾ Les 8 règles d’or de Shneiderman, 1987

1. Cohérence : ce principe est celui qui est le plus fréquemment enfreint, et pourtant le
plus facile à identifier et à éviter. Des séquences d’actions cohérentes devraient être
retenues dans des situations similaires, une terminologie identique devrait être utilisée
dans les invites (prompts), les menus et les écrans d’aide. Les exceptions telles que
le caractère non imprimable d’un mot de passe ou l’absence d’abréviations pour la
commande « delete » sont justifiées mais devraient être d’une utilisation limitée.

2. Abréviations : il s’agit ici de permettre aux utilisateurs experts d’utiliser des


abréviations dans la syntaxe des commandes, des cléfs de fonctions, des
commandes cachées, des macros, etc. Les utilisateurs novices doivent cependant
pouvoir utiliser un style d’interaction plus convivial sur le plan ergonomique tel que la
manipulation directe (exemple de Macintosh), les menus déroulants, etc.

3. Feedback informatifs : à chaque action usager devrait correspondre un feedback du


système informant l’utilisateur de l’état d’avancement de l’exécution et de la
conséquence de cette action.
4. Complétude du dialogue : les séquences de dialogue logiciel-usager devraient être
organisées en groupes avec un début, un milieu et une fin.

5. Traitement simple des erreurs : concevoir le système de façon à ce que l’utilisateur ait
le minimum de chance de faire une erreur grave. Lors d’une erreur de la part de
l’usager, essayer de faire en sorte que le système puisse la détecter et offrir à l’usager
des mécanismes simples et compréhensibles pour la traiter. Par exemple, l’usager ne
devrait pas avoir à retaper entièrement une commande sur laquelle une erreur aurait
été détectée.

6. Réversibilité des actions : s’assurer, chaque fois que c’est possible, qu’une action
puisse être réversible. Ceci rassure l’utilisateur car il sait que toute erreur peut être
facilement corrigée et l’encourage ainsi à explorer de nouvelles options du système.

7. Contrôlabilité du système : les utilisateurs doivent avoir la sensation que ce sont eux
qui contrôlent le système et non l’inverse.

8. Réduction de la charge mentale à court terme : les limites du traitement de


l’information par l’humain dues aux limites de la mémoire à court terme doivent
amener les concepteurs à prévoir une terminologie simple, un découpage de
l’information délivrée à l’utilisateur lorsque celle-ci est volumineuse et une durée
d’apprentissage suffisante pour l’assimilation des codes, des séquences d’actions,
etc.

PROCESSUS D’ÉVALUATION
On distingue deux méthodes(ou approches) principales pour l’évaluation. La première, de
nature empirique, informelle et pratique repose sur l’observation d’usagers en cours
d’utilisation du logiciel. Elle consiste à collecter des données sur l’observation pour
ensuite les analyser en vue de tirer les renseignements qui permettront d’apporter les
corrections nécessaires aux choix de conception ou d’implantation.

La seconde, de nature plus formelle et théorique, repose sur l’analyse de tâche et part de
l’hypothèse de base suivante : la compréhension des tâches à effectuer suffit pour la
prise de décisions en matière d’alternatives de conception.
L’évaluation formative (quantifiée qualitativement) est celle qui est effectuée durant le
processus itératif de conception sur une version incomplète d’un logiciel. Les techniques
principales de l’évaluation formative sont les suivantes :

• études pilotes,

• simulations (dessins d’écrans, enchaînement des séquences de dialogue, etc.),

• prototypage rapide,

• enregistrements caméra,

• tests d’acceptabilité (ou d’utilisabilité); et

• études expérimentales contrôlées.

L’évaluation sommative (quantifiée par un pourcentage ou des notes) est celle qui est
effectuée à la toute fin du processus de conception et de développement sur la version
opérationnelle d’un logiciel. Les techniques principales de l’évaluation sommative sont les
suivantes :

• questionnaires (sur papier ou par format électronique),

• interviews et discussions de groupe (forme verbale),

• assistance téléphonique ou en ligne,

• boîtes à suggestions en ligne; et

• études de cas.
COURS 10
INF-11299 – PROGRAMMATION II
Conception et programmation d’un programme de gestion et d’exploration des fichiers sur des CD
ROMs (CD-EXPLORER)

Références : Notes de cours et programme faits par Monsieur André Jacques, professeur
d’informatique au Département de mathématiques, d’informatique et de Génie de
l’Université du Québec à Rimouski

TP 2 : L’EXPLORATEUR DE CDS.

Il y a 5 étapes dans la conception d’un programme informatique. La première étape est l’analyse du
problème. À cette étape, on doit déterminer précisément :

I. Ce que l’on veut.


II. Ce que l’on a pour le faire.
III. Décider de la possibilité de réaliser le projet.
IV. Identifier les ressources nécessaires (langage, composantes, utilitaires, etc.).

À ce stade, on n’a pas de lignes de codes à écrire. Toutefois, on aura peut-être à vérifier le fonctionnement
de certaines composantes, librairies, algorithmes, etc, avant de poursuivre.

Voici donc un petit programme qui correspond à ce qu’on veut avoir : CDExplorer.exe et son fichier de
données : Set01-50.CDR.

Maintenant qu’on a une idée plus précise de ce que l’on a à faire, continuons l’étape d’analyse du
problème.

Est-il possible de faire le travail : OUI puisqu’on a déjà le programme.

La conception d’une telle application est-elle à ma portée ? Non pour la plupart d’entre-vous. Toutefois,
nous en ferons la conception ensemble.

Les ressources nécessaires : ici, j’ai utilisé une composante TTreeView et un TListView. Bien entendu, j’ai
utilisé beaucoup d’autres composantes, mais elles sont plus courantes et simples à utiliser.

Description du TP #2 :
- Au cours des 2 à 3 prochaines semaines, nous allons voir ensemble la conception et la
codification de l’application.
- Cette application est en Pascal. Votre travail sera de l’implanter en C++.
Les objectifs :
Être capable de lire et comprendre une application écrite dans un autre langage de programmation courant. :

- Apprendre à utiliser en C++ les composantes les plus courantes.


- S’initier au concept de récursivité.
- Adapter un programme à C++.
- Approfondir notre connaissance du C++ .
- Tout cela sans avoir à faire la conception du programme.

Fonctionnement général du programme

Au départ, notre programme doit faire certaines choses :

1) Il doit lire l’information sur des CDs déjà intégrés dans la base de connaissance (le fichier .CDR). Pour
ce faire, il aura besoin de connaître le répertoire où lire ces informations. Le nom de ce répertoire peut
être demandé à l’usager ou peut être stocké dans la base de registres de Windows. La première méthode
est casse-pieds mais n’implique pas une installation du logiciel. Elle est plus simple à implanter et plus
versatile. Au moins pour une première version, c’est ce que nous ferons.

2) Un fois qu’on connaît le répertoire de travail, il peut se produire plusieurs choses :

- Le répertoire n’existe peut-être pas. On peut contourner ce problème on obligeant l’usager à spécifier
un répertoire existant (option des dialogues prédéfinis).

- Il faudrait que notre logiciel lise les informations déjà existantes. On peut décider que pour chaque
CD intégré dans la base, il y aura un fichier avec extension CDR. Le logiciel n’aurait donc qu’à lire
tous les fichiers CDR. On peut aussi décider de tout stocker dans le même fichier. C’est le choix qui a
été fait.

3) À ce stade, la fenêtre principale du logiciel apparaît. On peut donc explorer les structures des CDs déjà
insérés dans la base. Il faut maintenant offrir des options à l’usager : Ajout et Suppression de CDs ainsi
qu’un outil de recherche de fichiers.

Voici donc, un peu plus détaillée, la séquence des choses à faire :


a) À l’ouverture du programme : demander à l’usager de spécifier une base existante (Ouvrir une base) ou
d’en créer une nouvelle (vide évidemment).
b) Ouvrir et interpréter le fichier avec extension CDR.
c) Afficher son contenu.
d) Réagir aux interactions de l’usager.
Interactions :

QUITTER : quitter le programme après avoir vérifié qu’on peut le faire (rien à sauvegarder).

Nouvelle base : Permettre de sauvegarder la base courante (si nécessaire) et créer une base vide.

Ouvrir une base : Permettre de sauvegarder, ouvrir, lire et afficher le contenu de la base.

Insérer un CD : Permettre de spécifier le périphérique à lire, lire la structure de répertoire et l’insérer en


mémoire (TTreeView) et/ou sur le fichier.

Mettre à jour un CD : On peut écrire plusieurs sessions sur un CD. La mise à jour revient à supprimer un
CD et en insérer un nouveau. Ce ne devrait pas être difficile à implanter.

Pour la suppression, veut-on supprimer seulement des CD ou permettre de supprimer une partie de
l’arborescence ?

La recherche : on doit pouvoir spécifier ce qu’on recherche et où. La recherche peut concerner un fichier,
un répertoire ou un CD.

Autres éléments :

Il serait bien de prévoir un champ pour insérer une note explicative sur le contenu d’un CD. La question
devient maintenant où ? Laissons à l’usager décider.

Il serait bien de pouvoir donner un nom au CD qui serait différent de son « label ».

Il ne faudra pas oublier de lire et faire suivre les éléments suivants concernant un fichier : nom, taille, date.

Puisque les TTreeView et TListView permettent des icônes, il faudrait utiliser cette possibilité (esthétique).
Toutefois, dans un premier temps, on peut ne pas se préoccuper de ceux-ci (ce sont des boules de Noël
qu’on pourra ajouter facilement plus tard).
Voyons comment lire le contenu d’un disque.

L’aide de C++ Builder parle de deux fonctions : FindFirst et FindNext.


Header File : dir.h
Category : Directory Control Routines
Syntax
#include <dir.h>
int findfirst(const char *pathname,
struct ffblk *ffblk, int attrib);
int _wfindfirst( const wchar_t *pathname,
struct _wffblk *ffblk, int attrib);

Description

Searches a disk directory. findfirst begins a search of a disk


directory for files specifed by attributes or wildcards.

pathname is a string with an optional drive specifier path and file


name of the file to be found. Only the file name portion can contain
wildcard match characters (such as ? or *). If a matching file is found
the ffblk structure is filled with the file-directory information.

When Unicode is defined, the_wfindfirst function uses the following


_wffblk structure.

struct _wffblk {
long ff_reserved;
long ff_fsize;
unsigned long ff_attrib;
unsigned short ff_ftime;
unsigned short ff_fdate;
wchar_t ff_name[256];
};

For Win32, the format of the structure ffblk is as follows:

struct ffblk {
long ff_reserved;
long ff_fsize; /* file size */
unsigned long ff_attrib; /* attribute found */
unsigned short ff_ftime; /* file time */
unsigned short ff_fdate; /* file date */
char ff_name[256]; /* found file name */
};
attrib is a file-attribute byte used in selecting eligible files for
the search. attrib should be selected from the following constants
defined in dos.h:

FA_RDONLY Read-only attribute


FA_HIDDEN Hidden file
FA_SYSTEM System file
FA_LABEL Volume label
FA_DIREC Directory
FA_ARCH Archive

A combination of constants can be ORed together.

For more detailed information about these attributes refer to your


operating system documentation.

ff_ftime and ff_fdate contain bit fields for referring to the current
date and time. The structure of these fields was established by the
operating system. Both are 16-bit structures divided into three fields.

ff_ftime:

Bits 0 to 4 The result of seconds divided by 2 (for example 10 here


means 20 seconds)
Bits 5 to 10 Minutes
Bits 11 to 15 Hours

ff_fdate:

Bits 0-4 Day


Bits 5-8 Month
Bits 9-15 Years since 1980 (for example 9 here means 1989)

The structure ftime declared in io.h uses time and date bit fields
similar in structure to ff_ftime and ff_fdate.
Return Value

findfirst returns 0 on successfully finding a file matching the search


pathname.

When no more files can be found, or if there is an error in the file


name:

-1 is returned
errno is set to

ENOENT Path or file name not found

_doserrno is set to one of the following values:

ENMFILE No more files

ENOENT Path or file name not found


Et pour Findnext :

Syntax

#include <dir.h>
int findnext(struct ffblk *ffblk );
int _wfindnext(struct _wffblk *ffblk );

Description

Continues findfirst search.

findnext is used to fetch subsequent files that match the pathname


given in findfirst. ffblk is the same block filled in by the findfirst
call. This block contains necessary information for continuing the
search. One file name for each call to findnext will be returned until
no more files are found in the directory matching the pathname.
Return Value

findnext returns 0 on successfully finding a file matching the search


pathname. When no more files can be found or if there is an error in
the file name

-1 is returned

errno is set to

ENOENT Path or file name not found

_doserrno is set to one of the following values:

ENMFILE No more files

ENOENT Path or file name not found

Et on donne un exemple :

/* findfirst and findnext example */

#include <stdio.h>
#include <dir.h>

int main(void)
{
struct ffblk ffblk;
int done;
printf("Directory listing of *.*\n");
done = findfirst("*.*",&ffblk,0);
while (!done)
{
printf(" %s\n", ffblk.ff_name);
done = findnext(&ffblk);
}

return 0;
}

Testons cet exemple (voir exemple1.exe) :


Ce programme n’affiche pas le contenu des sous-répertoires, seulement le contenu du répertoire courant.
Modifions le un peu : (voir exemple2.bpr).

La prochaine étape consiste maintenant à tester le fonctionnement d'un TTreeView. On le fera d'abord
avec les fichiers du répertoire principal. Voir Exemple3.

Essayons maintenant avec les sous-répertoires (Exemple4). Comme on dit : "C'est bien parti !"
Voici la fiche principale du programme.
À gauche, vous avez un TTreeView. Au centre, en haut, on a un TListView. Juste en bas de celui-ci, vous
avez un TMemo qui vous permet d’écrire des notes ou commentaires sur le contenu. À droite, vous avez les
différents boutons, deux TRadioGroupBox et un champ de saisie (TEdit). En bas, s’étendant sur toute la
fiche, on a un TStatusBar subdivisé en trois « Panels ». Évidemment, ici je n’ai pas à attirer votre attention
sur les éléments purement esthétiques.

Le programme aura trois autres fiches. La première est la fiche donnant les résultats d’une recherche. Cette
fiche indique l’emplacement exact des éléments trouvés. Il y a aussi deux autres petites fiches sur lesquelles
nous reviendrons.
À ce stade ci, vous devriez être en mesure de créer les différentes fiches.

Avec Delphi, voici le code généré pour le UNIT1 (équivalent au Unit1.hpp de C++Builder) :

unit Unit1;

interface

uses // equivalent aux clauses #include de C++


Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
Buttons, StdCtrls, ExtCtrls, ComCtrls, ImgList, MyStream;

Type

TAppState = (asNew, asLoad, asSave, asView); // type énuméré


TDirData = class
Files : TStringList;
Notes : String;
constructor Create;
constructor Load(var f : BStream);
procedure Write(var F : BStream);
procedure Show;
procedure Save;
end;
TForm1 = class(TForm)
// Composantes accessoires
GroupBox2: TGroupBox; GroupBox3: TGroupBox;
GroupBox4: TGroupBox; GroupBox5: TGroupBox;
GroupBox6: TGroupBox; GroupBox7: TGroupBox;
GroupBox8: TGroupBox;
CadreTV: TGroupBox; // cadre autour du TtreeView
// Composantes à gérer
TV: TTreeView; // liste des fichiers d'un répertoire
Chaine: TEdit; // texte à rechercher
Notes: TMemo; // notes sur un dossier
Icones: TImageList; // icônes à associer aux élém. de l'arborescence
SIcones: TImageList; // icônes des nœuds sélectionnés
OpenDialog: TOpenDialog; // dialogue pour l’ouverture de fichier
SaveDialog: TSaveDialog; // dialogue pour l’enregistrement d’un fichier
Status: TStatusBar; // la barre de status
// Boutons
Nouvelle: TButton; // nouvelle base
Ouvrir: TButton; // ouvrir une base
Insere: TButton; // insérer un nouveau CD
Supprime: TButton; // supprimer un noeud
Maj: TButton; // mettre à jour un CD
Sauver: TButton; // sauvegarder la base
Quoi: TRadioGroup; // quoi rechercher
Ou: TRadioGroup; // où le rechercher
Recherche: TButton; // lancer la recherche
Quitter: TBitBtn;
Contenu: TListView; // Contenu du répertoire courant
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure FormCreate(Sender: TObject);
procedure NouvelleClick(Sender: TObject);
procedure OuvrirClick(Sender: TObject);
procedure SauverClick(Sender: TObject);
procedure InsereClick(Sender: TObject);
procedure MajClick(Sender: TObject);
procedure SupprimeClick(Sender: TObject);
procedure RechercheClick(Sender: TObject);
procedure QuitterClick(Sender: TObject);
procedure NotesChange(Sender: TObject);
procedure TVDeletion(Sender: TObject; Node: TTreeNode);
procedure TVChange(Sender: TObject; Node: TTreeNode);
procedure ContenuColumnClick(Sender: TObject; Column: TListColumn);
procedure ContenuCompare(Sender: TObject; Item1, Item2: TListItem;
Data: Integer; var Compare: Integer);
private
{ Déclarations privées }
public
{ Déclarations publiques }
FAppState : TAppState; // Etat de l'application
FHasChanged : Boolean; // L'info a changé ?
NotesHasChanged : Boolean; // Les notes sur un répertoire ont changé ?
FCurrentNode : TTreeNode; // Le noeud courant
CurrentData : TDirData; // Pointeur sur les données du noeud courant
Done : Integer;
ColumnToSort : integer;
SortOrder : integer;
procedure ChangeState(S : TAppState);
procedure IsModified(M : Boolean);
procedure ChangeNode(T : TTreeNode);
property AppState :TAppState read FAppState write ChangeState;
property HasChanged : Boolean read FHasChanged write IsModified;
property CurrentNode : TTreeNode read FCurrentNode write ChangeNode;
procedure SaveToFile(const FileName: string);
procedure LoadFromFile(const FileName: string);
procedure ScanDrive(mask : String; ttn : TTreeNode);
procedure GetDiskLabel(drive : char; var nom : String);
procedure LoadTreeNode(ttn : TTreeNode; var F : BStream);
procedure SaveTreeNode(ttn : TTreeNode; var F : BStream);
procedure SearchTV(StartNode : TTreeNode; What : Integer; Str : String);
procedure CheckForHit(Node : TTreeNode; What : Integer; Str : String);
end;

var
Form1: TForm1;
const
icCDR : integer = 1; // index des icônes de CDR
icFLDR : integer = 0; // index des icônes de répertoires

En gras, vous avez du code que j’ai ajouté pour le fonctionnement du programme. On verra ce code au fur et à
mesure. Certains éléments importants n’apparaissent pas dans ce code. Ce sont essentiellement des propriétés
données aux composantes lors de la conception.

TV: TTreeView; // liste des fichiers d'un répertoire


Ici, il faut indiquer au TTreeView que les images et StateImages sont dans les TImageList Icones et
SIcones.
Contenu: TListView; // Contenu du répertoire courant
Mettre la propriété ViewStyle à VSReport et définir la propriété columns pour réfléter les 4 colonnes de la
liste.
Status: TStatusBar; // la barre de status
Subdiviser le Panel en trois sections
Notes: TMemo; // notes sur un dossier
Ajouter une barre de défilement verticale.
Si on revient à l’exemple 4 :
Pour lire le contenu d’un disque, on appelait cette fonction avec :

TTreeNode * ttn = TreeView1->Items->Add(0,"Disque courant");


ttn->ImageIndex = 0;
ScanDir("\\",ttn);
Ici, on crée un TTreeNode (ttn) en l’insérant à la racine (paramètre 0) du TreeView1. On lui
associe, en même temps, la chaîne " disque courant ".

L’intruction ttn->ImageIndex = 0; ne sert qu’à associer l’icône 0 à ce nœud. On lance ensuite le


parcours du répertoire principal du disque courant (//) en donnant comme nœud de base ttn.
Dans ScanDir, on recherche tous les fichiers du répertoire passé en paramètre (/ au départ).
Lorsqu’on trouve un répertoire, on crée un nœud avec AddChild, lui associe l’icône 1 et on
appelle récursivement ScanDir sur ce sous répertoire.
void TForm1::ScanDir(AnsiString dir, TTreeNode * ttn)
{
bool done;
TTreeNode * nttn;
struct ffblk ffblk;
AnsiString rep = dir+"*.*";
done = findfirst(rep.c_str(),&ffblk,FA_DIREC);
while (!done)
{
if ((ffblk.ff_attrib & FA_DIREC) && (strcmp(".",ffblk.ff_name)) &&
(strcmp("..",ffblk.ff_name)))
{
nttn = TreeView1->Items->AddChild(ttn,ffblk.ff_name);
nttn->ImageIndex = 1;
ScanDir(dir+ffblk.ff_name+"\\",nttn);
}
else if (!(ffblk.ff_attrib & FA_DIREC))
{
nttn = TreeView1->Items->AddChild(ttn,ffblk.ff_name);
nttn->ImageIndex = 2;
};
done = findnext(&ffblk);
}
}
Par contre, lorsqu’on trouve un fichier, on se contente de créer un nœud (AddChild) et de lui
associer l’icône 2.
En résumé, Add insère à la racine et AddChild insère sur le nœud passé en paramètre.
Revenons à notre application. On veut pouvoir stocker des structures de répertoires de plusieurs
disques dans un fichier afin d’être capable d’en parcourir le contenu par la suite.
Nous savons comment lire un disque et insérer l’information dans un TTreeView.
Il nous faudra être capable de sauvegarder et recharger le contenu d’un TTreeView. Pour ce faire,
nous devons, dès maintenant réfléchir sur la structure de données que nous utiliserons.
Nous avons des disques (CDs).
Ces CD contiennent des répertoires (au moins le répertoire principal).
Un répertoire peut contenir des sous-répertoires et des fichiers.
Traiter un sous-répertoire n’est pas vraiment différent que de traiter le répertoire principal.
Les fichiers peuvent être nombreux et le temps d’initialisation d’un TTreeView est directement
proportionnel au nombre de nœuds. Il sera donc préférable de ne pas créer de nœud pour chaque
fichier, mais bien d’associer une liste de fichiers à chaque répertoire.
Cette liste de fichiers contiendra, pour chaque fichier, son nom, sa taille et sa date de création.
Dans le code Pascal, j'ai défini la classe TDirData qui contiendra l'information relative à un
répertoire :
TDirData = class
Files : TStringList;
Notes : String;
constructor Create;
constructor Load(var f : BStream);
procedure Write(var F : BStream);
procedure Show;
procedure Save;
end;

L'équivalent C++ de ce code serait :

class TDirData{
TStringList * Files; // liste des fichiers du répertoire
AnsiString Notes; // les notes sur le répertoire
TDirData(); // constructeur
TDirData(ifstream & f);
void Write(ofstream & f); // ofstream n'est pas équivalent à BStream
void Show(); // afficher le contenu dans le TListView
void Save(); // Sauver le champ Notes
};

Un TStringList est une liste de chaînes de caractères (AnsiString). Chaque chaîne contiendra le
nom, la date et la taille d'un fichier. Ici, l'expérience compte car je sais qu'un TListView affiche
des chaînes de caractères.
CONSTRUCTION DE LA FICHE
- limiter la taille de la fiche à 800x600.
- placer le TStatusBar car la taille verticale des autres éléments en dépendra.
- placer un TGroupBox pour contenir le TtreeView.
- placer un TMemo dans un GroupBox, juste en haut du ProgressBar. Ajouter un
ProgressBar vertical.
- placer un TListView et le configurer pour qu'il soit semblable à celui de l'explorateur
Windows (ViewStyle = vsReport) et définir "Columns".
- placer les différents boutons.
Puisque vous avez le code Pascal, vous vous simplifierez la tâche en donnant les mêmes noms aux
composantes. Sauvegarder le tout.
Lancez le programme. Que se passe-t-il ? Rien !
Au départ, le programme construit la fiche1. On devrait en profiter pour initialiser toutes les
variables de la form1 et toutes les variables globales du programme. Pour ce faire, associer une
méthode au TForm1::FromCreate et insérez y les codes d'initialisation. Pour le moment, on a rien
à initialiser, mais ça viendra.
Lorsqu'on lance le programme, on devrait n'avoir de disponibles que les boutons NouvelleBase et
Ouvrir une base. Pour ce faire, il faudrait désactiver tous les autres boutons. Un peu partout dans
le code, on aura à activer et désactiver des boutons (et autres éléments). Aussi bien se créer une
méthode pour ce faire.
En Pascal, j'ai déclaré :
TAppState = (asNew, asLoad, asSave, asView);
en C++, l'équivalent serait :
typedef enum TAppState {asNew, asLoad, asSave, asView};
C'est l'état de l'application et je vais déclarer une variable (FappState) de ce type dans TForm1. Je
vais y associer une propriété AppState et l'initialiser (dans FromCreate) à asNew. La déclaration
de la propriété est :

property AppState :TAppState read FAppState write ChangeState;


procedure ChangeState(S : TAppState);
En C++, la déclaration équivalente serait :

__property TappState AppState = {read=FAppState, write=ChangeState};


Pour le moment, considérons que ChangeState fait le travail. Nous l'écrirons plus tard.
La première fois que le programme sera utilisé, ce sera pour créer une base. Voyons ce qui se
passe :
procedure TForm1.NouvelleClick(Sender: TObject);
var
Continue : Boolean;
begin
Continue := true;
If HasChanged then
case MessageDlg('L''ensemble de CD a changé. Sauver ?', mtConfirmation, mbYesNoCancel,0)
of
mrYes : SauverClick(Sender); // éventuellement, cette fonction sera écrite
mrNo : ; // on fait rien
mrCancel : begin
ShowMessage('Opération annulée');
Continue := false;
end
end; // du Case
if Continue then
begin
TV.Items.Clear; // on a peut-être déjà quelque chose là
Contenu.Items.Clear; // idem
Notes.Clear; // idem
InsereClick(Sender); Demander d'insérer un premier CD
Status.Panels.Items[0].Text := 'Nouvel ensemble';
OpenDialog.FileName := 'NewSet.CDR'; // mettre un nom par défaut dans le dialogue
end;
end;
Ce code implique qu'on a déclaré HasChanged dans TForm1. Les autres variables ont été
déclarées à la conception de la fiche. Il nous faut écrire les méthodes SauverClick et InsereClick.
Commençons par InsereClick.
procedure TForm1.InsereClick(Sender: TObject);
var
nom : String;
ttn : TTreeNode;
begin
if Form2.ShowModal = mrOK then
begin
GetDiskLabel(Form2.DriveComboBox1.Drive,nom);
ttn := TV.Items.Add(Nil,nom);
with ttn do
begin
ImageIndex := icCDR; SelectedIndex := icCDR;
Data := TDirData.Create;
end;
AppState := asLoad;
done := 0;
ScanDrive(Form2.DriveComboBox1.Drive+':\',ttn);
HasChanged := true; AppState := asView;
end;
end;

La fiche 2:
Elle permet de sélectionner une unité physique à lire. Une fois qu'on connaît l'unité, on va
chercher son nom de volume avec GetDiskLabel et on insère un nouveau nœud à la racine du
TTreeView TV. On initialise ensuite les champs de ttn. Un TTreeNode a une propriété data qui est
un pointeur non typé (void*). On va s'en servir pour référencer le TDirData associé à ce nœud.
L'application change d'état et on lance la lecture de la structure du disque sélectionné. Une fois
cela fait, on est dans l'état asView.
Voyons le détail de GetDiskLabel :
procedure TForm1.GetDiskLabel(drive: char; var nom: String);
var
SR : TSearchRec;
Stop : Boolean;
begin
nom := drive+':\*.*';
Stop := findfirst(nom,faAnyFile,SR) <> 0;
while (Not Stop) and (SR.Attr <> faVolumeId) do Stop := FindNext(SR) <> 0;
if Stop then nom := InputBox('Identification du CD','Nom : ','Sans Nom')
else nom := InputBox('Identification du CD','Nom : ',SR.Name);
end;

Ici, on va chercher le nom du volume dont la lettre de l'unité est drive. On donne la chance à
l'usager de changer le nom de volume utilisé dans l'affichage et on retourne nom. Remarquez
qu'ici on aurait pu écrire une fonction retournant une chaîne.
Ce code est assez cryptique. Toutefois, si vous regardez les exemples de l'aide sur findfirst et
findnext, il vous deviendra plus clair.
Il est temps de regarder le code des méthodes de TdirData :
class TDirData{
TStringList * Files; // liste des fichiers du répertoire
AnsiString Notes; // les notes sur le répertoire
TDirData(); // constructeur
TDirData(ifstream & f);
void Write(ofstream & f); // ofstream n'est pas équivalent à BStream
void Show(); // afficher le contenu dans le TListView
void Save(); // Sauver le champ Notes
};

constructor TDirData.Create;
begin
Files := TStringList.Create; Notes := '';
end;

Le code C++ équivalent serait :

TdirData::TDirData()
{
Files = new TstringList; Notes = "";
}

Les autres méthodes de la classe seront vues plus loin.


Le constructeur de la classe est surchargé. Et on verra le code de ce deuxième
constructeur plus loin. Pour le moment, voyons le code de ScanDrive:

procedure TForm1.ScanDrive(mask: String; ttn: TTreeNode);


var
sr : TSearchRec;
done : Boolean;
nttn : TTreeNode;
dir : String;
begin
// établir le masque pour la recherche
dir := mask+'*.*'; // par exemple, si l'appel est ScanDrive("F:\",…) dir vaudra "F:\*.*"

// lancer la recherche
done := FindFirst(dir,faAnyFile,sr) <> 0;
while not done do
begin
if (sr.Attr and faDirectory <> 0) and (sr.Name <> '.') and (sr.Name <> '..') then
begin // on a un répertoire !
// Créer un TtreeNode et l'insérer comme fils du nœud courant
nttn := TV.Items.AddChild(ttn,sr.Name);
// Initialiser les différentes propriétés de ce noeud
nttn.ImageIndex := icFLDR; nttn.SelectedIndex := icFLDR;
nttn.Data := TDirData.Create;
ScanDrive(mask+sr.Name+'\',nttn);
end
else if (sr.Attr and (faDirectory + faVolumeId + faSysFile) = 0) then
// on a un fichier: on ajout l'info dans le TdirData associé au noeud
with TDirData(ttn.Data) do
begin
Files.Add(sr.Name + '|'+ IntToStr(sr.Size shr 10) + '|' +
DateTimeToStr(FileDateToDateTime(sr.time)));
Notes := '';
end;
done := FindNext(sr) <> 0;
end;
end;

À ce stade, notre programme peut lire un disque et stocker l'information dans


les TTreeNodes du TTreeView. Toutefois, il n'y a rien qui apparaît dans le
TListView. Le code nécessaire sera dans TdirData.Show(). Mais, avant, quelques
petit détails. Le répertoire dont le contenu sera affiché dans le TListView
(contenu), sera le répertoire courant. Si celui-ci change, il faudrait que cela
se réflète dans le TListView. Pour ce faire, on va déclarer un champs
FCurrentNode et une propriété CurrentNode. On va aussi associer l'événement à
un changement de nœud courant (OnChange). Voici le code impliqué :
FCurrentNode : TTreeNode; // Le noeud courant
procedure ChangeNode(T : TTreeNode);
property CurrentNode : TTreeNode read FCurrentNode write ChangeNode;
Et le code de ChangeNode :

procedure TForm1.ChangeNode(T: TTreeNode);


begin
FCurrentNode := T;
if T <> NIL then CurrentData := TDirData(T.Data)
else CurrentData := NIL;
end;
et le code associé à l'événement OnChange :
procedure TForm1.TVChange(Sender: TObject; Node: TTreeNode);
var
T : TTreeNode;
Path : String;
begin
if (CurrentNode <> NIL) and NotesHasChanged then
TDirData(CurrentNode.Data).Save; // ((TdirData*)CurrentNode->Data)->Save() en C++
CurrentNode := TV.Selected; T := CurrentNode;
if (CurrentNode <> NIL) then
begin
Path := CurrentNode.Text;
While T.Parent <> NIL do
begin
T := T.Parent; Path := T.Text+'\'+Path;
end;
Status.Panels.Items[3].Text := Path;
CurrentData := TDirData(CurrentNode.Data);
CurrentData.Show;
end;
end;

Le code de TdirData.Save est :


procedure TDirData.Save;
begin
Notes := Form1.Notes.Text;
Form1.NotesHasChanged := False;
end;

et celui de TdirData.Show est :


procedure TDirData.Show;
var
item : TListItem; // TlistItem est un élément correspondant à une ligne de texte dans un
//TListView
s, ext : string;
i, j, p : integer;
begin
Form1.Contenu.Items.Clear; // Items est l'ensemble des lignes du ListView
for i := 0 to Files.Count-1 do
begin
item := Form1.Contenu.Items.Add; // on crée une ligne vide
s := Files.Strings[i]; // s est l'information sur le fichier structurée
// "nom|taille|date"
p := Pos('|',S);
item.Caption := Copy(S,1,P-1); // le nom est ce qui vient avant le premier |
// l'extension est ce qui vient après le point dans la chaîne s
ext := copy(S,P-4,4);
j := Pos('.',ext);
if j = 0 then ext := ''
else ext := Copy(ext,j+1,Length(ext)-j+1);
item.SubItems.Add(ext);
s := Copy(S,P+1,Length(S)-P); // s contient maintenant "taille|date"
p := Pos('|',s);
item.SubItems.Add(Copy(S,1,P-1)); // avant le |, c'est la taille
item.SubItems.Add(Copy(S,P+1,Length(S)-p)); // le reste est la date
end;
Form1.Notes.Text := Notes;
Form1.NotesHasChanged := False;
end;
Nous avons maintenant lu un disque, lu sa structure et stocké cette information
dans les TTreeNode du TTreeView. Cette information s'affiche automatiquement
sauf pour celle à mettre dans le TlistView. On veut naviguer dans le TTreeView
et le TListView se met à jour automatiquement. On peut même insérer un autre
disque car InsereClick est fonctionnel.

La tâche suivante est de sauvegarder l'information afin de pouvoir la relire


éventuellement. On va donc s'attaque au SauverClick.

procedure TForm1.SauverClick(Sender: TObject);


begin
if NotesHasChanged and (CurrentNode <> NIL) then
TDirData(CurrentNode.Data).Save;
SaveDialog.FileName := OpenDialog.FileName;
if SaveDialog.Execute then
begin
SaveToFile(SaveDialog.FileName);
Status.Panels[0].Text := SaveDialog.FileName;
end;
end;

Vous devinez que le gros du travail se fait dans SaveToFile :


procedure TForm1.SaveToFile(const FileName: string);
var
F : BStream;
nCD, i: Integer;
begin
AppState := asSave;
F.Open(FileName); Done := 0;
Form4.Show; Form4.ProgressBar1.Position := 0;
nCD := 0;
// Le fichier contiendra :
// un entier indiquant le nombre de CD
// les informations de chaque CD
// 1 - compter les CD
for i := 0 to TV.Items.Count-1 do
if (tv.Items.Item[i].ImageIndex = icCDR) then inc(nCD);
F.Write(@nCD,SizeOf(Integer));
// 2 - Pour chaque CD, écrire son contenu
for i := 0 to TV.Items.Count-1 do
if (TV.Items.Item[i].ImageIndex = icCDR) then
begin
Form4.Label1.Caption := InttoStr(ncd)+' CD à écrire';
dec(ncd);
SaveTreeNode(tv.Items.Item[i],F);
end;
F.Close;
AppState := asView; HasChanged := False;
Form4.Hide;
end;
Encore un fois, on a repoussé le travail plus loin. J'appelle cela la méthode du coin gauche.
D'autres appellent cela l'approche descendante ou structurée.

Le code de SaveTreeNode est :


procedure TForm1.SaveTreeNode(ttn: TTreeNode; var F: BStream);
var
IIndex,SIndex,i, n : Integer;
begin
// Écrire le nom du répertoire
F.WriteString(ttn.Text);
// Écrire le nombre de sous-répertoires, les numéros d'icônes
n := ttn.Count; F.Write(@n,Sizeof(integer));
IIndex := ttn.ImageIndex; F.Write(@IIndex,SizeOf(Integer));
SIndex := ttn.SelectedIndex; F.Write(@SIndex,SizeOf(Integer));
// Écrire le contenu du TdirData associé à ce répertoire
TDirData(ttn.Data).Write(F);
// Écrire chaque sous-répertoire
for i := 0 to ttn.Count-1 do SaveTreeNode(ttn.Item[i],F);
end;

Il faut maintenant écrire WriteString et TDirData.Write.


Puisque j'écris dans le fichier des entiers et des chaînes, le fichier sera un fichier binaire
(ofstream). Pour écrire un entier, c'est facile. Pour écrire une chaîne, c'est un peu plus compliqué.
Les chaînes à écrire auront de 0 à quelques milliers de caractères. Il faut donc faire en sorte de
stocker les chaînes efficacement. La méthode utilisée est la suivante : (1) écrire un entier donnant
la longueur de la chaîne et ensuite les caractères de la chaîne.
void WriteString(AnsiString s, ofstream & f)
{
int n = s.Length(); f.write((char*)&n, sizeof(int));
f.write(s.c_str(),n);
}
procedure TDirData.Write(var F: BStream);
begin
F.WriteString(Files.Text); F.WriteString(Notes);
Inc(Form1.Done);
If Form1.Done mod 100 = 0 then
begin
Form4.ProgressBar1.Position := (Form1.Done*100) div Form1.TV.Items.Count;
Form4.Update;
end;
end;

dont le code C++ équivalent serait :


void TdirData::Write(ofstream & f)
{
WriteString(Files->Text,f); WriteString(Notes,f);
Form1->Done++;
if (Form1->Done % 100 == 0)
{ Form4->ProgressBar1->Position = (Form1->Done * 100) / Form1->TV->Items->Count;
Form4->Update(); }
}

Vous aimerez peut-être aussi