Académique Documents
Professionnel Documents
Culture Documents
INF-112-99
NOTES DE COURS
PAR
MARTIN LESAGE
CHARGÉ DE COURS
INF-11299 – PROGRAMMATION II
INTRODUCTION
clics de souris; et
menus.
La fenêtre du code
LA FENÊTRE DES MENUS
Les menus sont déroulants de haut en bas (« pull-down menus ») et peuvent contenir des
sous-menus.
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.
¾ 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)
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.
Il suffit maintenant de cliquer sur la fiche à l’endroit où vous voulez placer votre
composante graphique.
BARRE DE COMPOSANTES « STANDARD »
Cette fenêtre permet de modifier les propriétés (attributs) des objets ou des composantes
étant sur les fiches.
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):
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
.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 ».
Exemples :
bcc32 prog1.cpp
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.
INF-11299 – PROGRAMMATION II
La compagnie Borland a développé une version du langage C++ qui a été nécessaire pour
supporter l’environnement graphique de C++ Builder.
¾ leur apparence; et
¾ 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 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.
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.
¾ 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.
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.
I. encapsulation,
II. héritage,
III. polymorphisme; et
¾ Encapsulation
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
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
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.
Ces points sont importants à retenir lorsque l’on parle de la programmation orientée-objet :
//---------------------------------------------------------------------------
#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;
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);
return 0;
}
Exemple de polymorphisme
#include <iostream>
using namespace std;
int main()
{
cout << sqr_it(10) << "\n";
cout << sqr_it(11.0) << "\n";
cout << sqr_it(9L) << "\n";
return 0;
}
return i*i;
}
return d*d;
}
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 ».
# ---------------------------------------------------------------------------
# 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
[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$@ $<
# ---------------------------------------------------------------------------
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 :
#include "Ex1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
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
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 :
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 :
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:
nom_de_la_structure.nom_du_champ
Exemple :
addr_info.customer_num = 88 ;
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;
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 :
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.
struct fred {
char x;
int y;
float z;
char s[10];
} mike;
Voici des exemples de passage de champs d’enregistrements par adresse (par référence) avec
l’opérateur d’adresse « & » :
#include <stdio.h>
int main(void)
{
struct struct_type arg; /* declare arg */
arg.a = 1000;
f1(arg);
return 0;
}
struct bal {
float balance;
char name[80];
} person;
p = &person;
L’accès aux champs via un pointeur d’enregistrement se fait des deux façons suivantes :
p->balance ;
ou
(*p).balance ;
#include <stdio.h>
#include <conio.h>
struct my_time {
int hours;
int minutes;
int seconds;
};
systime.hours = 0;
systime.minutes = 0;
systime.seconds = 0;
for(;;) {
update(&systime);
display(&systime);
if(kbhit()) return 0;
}
}
void mydelay(void)
{
long int t;
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] ;
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");
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 :
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 */
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 :
Note : Si plusieurs pointeurs doivent être déclarés, l'étoile doit être répétée :
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 ;
23443 = pe1 ;
461 = *pe1 ;
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>
int main(void)
{
sp_to_dash("this is a test");
return 0;
}
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:
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++ :
if(n==1) return(1);
answer = factr(n-1)*n;
return(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 ».
# ---------------------------------------------------------------------------
# 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
[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$@ $<
# ---------------------------------------------------------------------------
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 :
#include "Ex1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
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
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 :
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 :
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:
nom_de_la_structure.nom_du_champ
Exemple :
addr_info.customer_num = 88 ;
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;
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 :
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.
struct fred {
char x;
int y;
float z;
char s[10];
} mike;
Voici des exemples de passage de champs d’enregistrements par adresse (par référence) avec
l’opérateur d’adresse « & » :
#include <stdio.h>
int main(void)
{
struct struct_type arg; /* declare arg */
arg.a = 1000;
f1(arg);
return 0;
}
struct bal {
float balance;
char name[80];
} person;
p = &person;
L’accès aux champs via un pointeur d’enregistrement se fait des deux façons suivantes :
p->balance ;
ou
(*p).balance ;
#include <stdio.h>
#include <conio.h>
struct my_time {
int hours;
int minutes;
int seconds;
};
systime.hours = 0;
systime.minutes = 0;
systime.seconds = 0;
for(;;) {
update(&systime);
display(&systime);
if(kbhit()) return 0;
}
}
void mydelay(void)
{
long int t;
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] ;
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");
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 :
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 */
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 :
Note : Si plusieurs pointeurs doivent être déclarés, l'étoile doit être répétée :
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 ;
23443 = pe1 ;
461 = *pe1 ;
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>
int main(void)
{
sp_to_dash("this is a test");
return 0;
}
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:
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++ :
if(n==1) return(1);
answer = factr(n-1)*n;
return(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.
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 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.
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();
};
void queue::qput(int i)
{
if(sloc==99) {
cout << "Queue is full.\n";
return;
}
sloc++;
q[sloc] = i;
}
a.init();
Le programme suivant définit une liste circulaire implantée à l’aide d’un vecteur :
#include <iostream>
using namespace std;
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);
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.
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 »).
#include <iostream>
using namespace std;
int main()
{
cout << sqr_it(10) << "\n";
cout << sqr_it(11.0) << "\n";
cout << sqr_it(9L) << "\n";
return 0;
}
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;
}
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;
int main()
{
int i;
double d;
long l;
cout << i << " " << d << " " << l;
return 0;
}
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.
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 ») :
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 »).
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();
};
int road_vehicle::get_wheels()
{
return wheels;
}
int road_vehicle::get_pass()
{
return passengers;
}
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::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 :
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
(« ~ »).
Il est à noter que dans cet exemple, le destructeur n’effectue aucune opération.
#include <iostream>
using namespace std;
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);
return 0;
}
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.
La variable (“who”) est utilisée pour mémoriser un numéro d’identification identifiant la file:
queue a = queue(101);
ou
queue a(101);
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;
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);
return 0;
}
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;
}
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
return 0;
}
X ob = X(99);
X ob(99);
ou
X ob = 99;
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 :
#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();
} ;
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::show_line()
{
int i;
textcolor(color);
gotoxy(startx, starty);
l.define_line(2, 2, 10);
l.set_color(2);
l.show_line();
return 0;
}
void f(int i = 1)
{
// Code de la fonction f
}
ou
#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
xyout("hello", 10, 10);
xyout(" there");
xyout("I like C++", 40); // this is still on line 10
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 ».
inline function_declaration
Exemple:
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);
};
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;
}
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.
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.
#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";
}
f(o);
cout << "This is i in main: ";
cout << o.get_i() << "\n";
return 0;
}
Constructing 1
This is local i : 2
Destroying 2
This is i in main: 1
Destroying 1
#include <iostream>
using namespace std;
class myclass {
int i;
public:
void set_i(int n) { i=n; }
int get_i() { return i; }
};
o = f();
cout << o.get_i() << "\n";
return 0;
}
myclass f()
{
myclass x;
x.set_i(1);
return x;
}
#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
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 ») :
#include <iostream>
using namespace std;
class display {
public:
} ;
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);
return 0;
}
#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;
return 0;
}
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;
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 en se servant d’un pointeur sur cet objet, il faut utiliser
l’opérateur (« -> »).
#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
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.
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;
return 0;
}
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.
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); }
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);
return 0;
}
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;
return 0;
}
#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); }
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();
return 0;
}
// .....
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;
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;
int main() {
cout << myfunc('c'); // this calls myfunc(char)
cout << myfunc(88) << " "; // ambiguous
return 0;
}
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 main() {
cout << myfunc(4, 5) << " "; // unambiguous
cout << myfunc(10); // ambiguous
return 0;
}
int myfunc(int i) {
return i;
}
pour pointer à une fonction ayant deux arguments, il aurait fallu faire la déclaration suivante:
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 main()
{
int (*fp)(int a); // pointer to int xxx(int)
return 0;
}
int myfunc(int a)
{
return a;
}
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 »).
ob.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;
#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;
}
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 :
#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();
return 0;
}
1, 2, 3
10, 10, 10
12, 12, 13
22, 24, 26
1, 2, 3
1, 2, 3
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;
// 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;
}
// 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; // increment c
c.show();
return 0;
}
¾ 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
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);
} ;
// 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;
}
// 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; // 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 :
#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;
}
temp.count = ob.count + i;
return temp;
}
int main() {
CL obj;
obj = 10;
cout << obj.count << " "; // outputs 10
#include <iostream>
using namespace std;
int main()
{
int x, y;
x = 99;
y = 88;
return 0;
}
// C-like, explicit pointer version of swap().
void swap(int *a, int *b)
{
int t;
t = *a;
*a = *b;
*b = t;
}
#include <iostream>
using namespace std;
int main()
{
int x, y;
x = 99;
y = 88;
return 0;
}
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;
}
Constructing 1
-10
Destructing 1
Fonctions ou méthodes retournant des références (des adresses ou des
pointeurs)
#include <iostream>
using namespace std;
int main()
{
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
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
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);
} ;
// 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;
}
// 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; // increment c
c.show();
return 0;
}
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);
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);
return 0;
}
Le programme suivant vérifie les limites du vecteur avant son accès :
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);
};
int main()
{
atype ob(1, 2, 3);
class str_type {
char string[80];
public:
str_type(char *str = "\0") { strcpy(string, str); }
strcpy(temp.string, string);
strcat(temp.string, str.string);
return temp;
}
strcpy(string, str);
strcpy(temp.string, string);
return 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";
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.
HÉRITAGE ET POLYMORPHISME
L’héritage et le polymorphisme sont deux aspects essentiels d’un langage orienté objet.
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.
C’est pourquoi le langage C++ supporte le polymorphisme à l’exécution par l’usage des classes
dérivées et des fonctions virtuelles.
¾ 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 ».
¾ 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 :
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.
#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"; }
} ;
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;
}
#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"; }
} ;
class Z : public Y {
public:
void f();
} ;
int main()
{
Y var;
Z var2;
var.make_k();
cout << var.get_k();
cout << "\n";
var2.f();
// var2.put_ij(); no longer accessible
return 0;
}
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "\nBase created\n"; }
};
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"; }
};
int main() {
D_class1 d1;
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"; }
};
int main()
{
D_class1 d1;
D_class2 d2;
return 0;
}
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).
#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; }
} ;
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;
}
} ;
int main()
{
Z i;
cout << i.make_ab();
return 0;
}
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.
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; }
} ;
return 0;
}
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:
#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 << " "; }
} ;
D_class *dp;
D_class D_ob;
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é.
class Base {
public:
virtual void who() { // specify a virtual function
cout << "Base\n";
}
};
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;
}
Base
First derivation
Second derivation
Les classes de base appellent généralement les fonctions virtuelles à l’aide de paramètres de
fonctions :
class Base {
public:
virtual void who() { // specify a virtual function
cout << "Base\n";
}
};
int main()
{
Base base_obj;
first_d first_obj;
second_d second_obj;
return 0;
}
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";
}
};
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;
}
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";
}
};
int main()
{
figure *p; /* create a pointer to base type */
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";
}
} ;
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.
class figure {
double x, y;
public:
void set_dim(double i, double j=0) {
x = i;
y = j;
}
virtual void show_area() = 0; // pure
} ;
//--------------------------------------------------------------------
#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
} ;
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 :
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.
¾ 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.
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é.
En voici un exemple :
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);
}
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é.
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 :
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 étapes des opérations dans les fichiers sont généralement similaires :
¾ fermeture du fichier.
Les classes C++ : « ifstream », « ofstream » et « fstream » nous permettent d’effectuer les
opérations suivantes :
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 ::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;
out.close();
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;
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();
in.close();
char ch;
cout << "Pressez une touche pour continuer: > " <<endl;
cin >> ch;
return 0;
}
//----------------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
ofstream out("fictext.txt");
if(!out)
{
cout << "Impossible d'ouvrir le fichier\n";
return 1;
}
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é.
0! = 1
1! = 1
N! = N*(N-1)!
int fact(int n)
{
int res = 1;
for (int i = 2; i <= n; i++) res *= i;
return res;
}
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;
}
int fibo(int n)
{
if (n < 3) return 1;
else return fibo(n-1) + fibo(n-2);
}
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:
É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
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
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
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
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
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.
L’état(State) d’une Table peut être différent selon les opération que l’on effectue sur celle-ci.
dsInactive
Post Post
(échec) (échec)
Open Close
Insert
Edit
Append
dsInsert dsBrowse dsEdit
dsSetkey
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
ObjetTable->First;
ObjetTable->Next;
ObjetTable->Prior;
ObjetTable->Last;
ObjetTable->MoveBy(x);
ObjetTable->BOF; Beginning Of File
ObjetTable->EOF; End Of File
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.
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
Composants
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
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:
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
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
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 :
La gestion des exceptions en C++ est implantée à l’aide de trois mots réservés : « try »,
« catch » et « throw ».
throw 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 .
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.
int main()
{
cout << "Start\n";
return 0;
}
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 »:
int main()
{
cout << "Start\n";
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
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 » :
int main()
{
cout << "Start\n";
return 0;
}
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;
int main()
{
cout << "Start\n";
Xhandler(1);
Xhandler(2);
Xhandler(0);
Xhandler(3);
return 0;
}
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 :
¾ 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
a. Fonction « malloc »
Librairie : <stdlib.h>
Description :
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.
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;
b. Fonction « calloc»
Prototype de la fonction :
Librairie : <stdlib.h>
Description :
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 ».
float *get_mem(void)
{
float *p;
c. Fonction « free »
Librairie : <stdlib.h>
Description :
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.
int main(void)
{
char *str[100];
int i;
return 0;
}
char *p;
p = (char *) malloc(25);
int *p;
p = (int *) malloc(50*sizeof(int));
int *p;
if((p = (int *) malloc(100))==NULL) {
printf("Out of memory.\n");
exit(1);
}
char *p;
p = malloc(1000); /* get 1000 bytes */
#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;
}
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;
}
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 pouvoir parcourir une liste entièrement, il suffit de connaître l'adresse de sa première
cellule, contenue dans un pointeur.
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
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);
}
};
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;
}
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;
}
~ListeSimple()
{
while(DebutListe) DebutListe=SuppressionRecursive(DebutListe);
delete DebutListe;
}
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.
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;
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;
v. Les résultats
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.
class ListeDouble
{
struct cellule
{
cellule *Precedent;
int Element;
cellule *Suivant;
}
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
ListeDouble()
{
DebutListe = NULL;
int Valeur = 0;
cout<< "Entrez des valeurs entières (terminez par -1) :"
for(;;)
{
cin>>Valeur;
if (Valeur == -1) break;
AjoutCelluleDebut(Valeur);
}
};
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.
};
ii. Le destructeur
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;
};
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 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";
};
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 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();
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:
};
iii. Le constructeur
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 :
ArbreBinaire UnArbreBinaire;
iv. Le destructeur
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 :
~ArbreBinaire()
{
Suppression(Racine);
}
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.
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;
};
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;
}
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
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.
¾ 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,
4. interfaces explicites; et
5. rétention d’information.
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.
À 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 :
2. la phase d’analyse,
3. la phase de conception,
4. l’écriture du programme; et
5. la phase de tests.
¾ 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.
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.
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 :
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,
Les jeux d'essais mis en œuvre pour ces tests sont basés sur les valeurs limites de la boucle.
On testera ainsi:
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:
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.
¾ 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.
¾ Le test des interfaces usager graphiques(GUI) présentent des défis intéressants pour les
concepteurs de logiciels :
¾ 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 :
¾ 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 :
¾ Les tests suivants sont significatifs dans la vérification d’une interface usager
graphique(GUI) :
o simplicité d’utilisation,
¾ décrire textuellement des algorithmes, des fonctions, des routines, des variables
et des constantes,
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,
¾ 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.
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.
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 :
2. Conception interactive.
4. Conception itérative.
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.
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.
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.
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,
• prototypage rapide,
• enregistrements caméra,
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 :
• é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 :
À 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.
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. :
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.
- 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.
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.
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.
Description
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];
};
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:
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:
ff_fdate:
The structure ftime declared in io.h uses time and date bit fields
similar in structure to ff_ftime and ff_fdate.
Return Value
-1 is returned
errno is set to
Syntax
#include <dir.h>
int findnext(struct ffblk *ffblk );
int _wfindnext(struct _wffblk *ffblk );
Description
-1 is returned
errno is set to
Et on donne un exemple :
#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;
}
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
Type
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.
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 :
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;
TdirData::TDirData()
{
Files = new TstringList; Notes = "";
}
// 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;