www.developpez.com
Inscrivez-vous dès maintenant
à la lettre d'information développeurs
C O N S E I L S  E T   E X E M P L E S   C++ B U I L D E R


Janvier 2002

Remarques de développement avec C++ Builder 5

Partie I - par Gilles Louise



Avant-propos : vue d'ensemble de C++ Builder 5

1. À propos de la fonction "Exécuter|jusqu'au curseur"
2. Points d'arrêt
3. Point d'arrêt pendant l'exécution
4. Ouverture d'un projet
5. Conversion AnsiString en Char*
6. Aide en ligne
7. Erreur de compilation
8. Construire et Make
9. Appel d'une fonction à l'intérieur du code d'un événement
10. Arrêt d'exécution
11. Extraction à l'intérieur d'un AnsiString d'un ou plusieurs caractères
12. La fonction IntToHex
13. Longueur de vos buffers
14. Explorateur de classes
15. Initialisations
16. Déclaration des variables
17. Fenêtre d'exécution
18. Grand fichier cpp
19. Gestionnaire de projet
20. Conseil personnel
21. Remarques et exemple autour de malloc (memory allocation)
22. Autres exemples de malloc
23. Exemple de malloc pointant des structures
24. Malloc dans un cas de tableaux de pointeurs de séries de structures
25. À propos de la réallocation mémoire (realloc)
26. À propos des unsigned
27. À propos du signe * (étoile)
28. Polices de caractères du source C++
29. Recherche dans le source
30. Les pointeurs de pointeurs

Continuer : Partie II


Vue d'ensemble de C++ Builder 5


Quand vous entrez dans C++Builder, l’écran se divise en quatre parties :

1. le menu du progiciel en haut
2. l’inspecteur d’objets sur la gauche
3. une première fenêtre grise
4. le source de base correspondant (en dessous de la fenêtre, faites F12 pour le voir)

Deux touches sont à connaître dès à présent :

1. F12 qui permute la fenêtre grise avec le source. Si la fenêtre est visible, F12 fera apparaître le source et inversement.
2. F11 qui fait apparaître l’inspecteur d’objets.

La toute première chose à faire quand vous entrez dans C++Builder est :

1. soit de sauvegarder le projet vide proposé dès l’entrée
2. soit d’ouvrir un projet existant (via "Réouvrir" du menu principal)

La raison en est que C++Builder, par sécurité, sauvegarde de lui-même le projet courant même complètement vide au bout d’une minute ou deux (il prend la main de son propre chef quelques secondes pour cette sauvegarde automatique), il choisit le nom général de "unit1" pour le source et "projet1" pour l’application en général. Si donc vous n’exécutez pas une des deux opérations données ci-dessus, vous allez vous retrouver avec ce type de fichiers (unit1 et projet1) dans le répertoire courant utilisé.

Notez que la fenêtre source C++ contient sur sa gauche une autre fenêtre, l'explorateur de classes. Cet explorateur, comme son nom l'indique répertorie les classes de votre application ainsi que le contenu de ces classes, données et fonctions. Comme il n'est pas toujours nécessaire de disposer de cette fenêtre, je vous conseille de ne pas toujours l'afficher ce qui donne plus de place pour le source en lui-même. On se débarrasse bien sûr de cette fenêtre en cliquant sur le petit x de la fenêtre mais si vous n'en voulez pas pour une longue durée, demandez à ce quelle ne soit plus affichée à partir de maintenant en faisant "Outils|Options d'environnement", là sélectionnez l'onglet "explorateur de classes" et décochez simplement la case "Montrer l'explorateur", case que vous pouvez recocher à tout moment. Vous disposez en plus de la fonction du menu "Voir|explorateur de classes".

Pour bien naviguer durant votre développement de l'inspecteur d'objets à une fiche (qui sera très souvent la fiche principale Form1) et inversement, je vous conseille de ne pas faire se chevaucher ces fenêtres en mettant l'inspecteur d'objets à gauche et la fiche à droite. Vous passez alors agréablement de l'un à l'autre d'autant que quand vous modifiez des éléments de la fiche, les transformations dans l'inspecteur se font en temps réel et inversement allant de l'inspecteur à la fiche.

Je vous conseille de vous créer un répertoire spécial de test, lui-même sous-répertoire du répertoire officiel créé à l’installation "projects", appelez-le par exemple "Essai". Il vous permettra de tester certaines syntaxes ou composants. Ainsi, pour tester un composant avec C++Builder, entrez dans le progiciel (ou ce qui revient au même faites "Nouvelle application"), sauvegardez tout de suite ce projet vide nouveau dans le répertoire Essai (choisissez "Enregistrer le projet sous" du menu principal). Je vous conseille de choisir non pas unit1 proposé par défaut mais unit01 et projet01. Ainsi, à chaque nouveau test, vous passez à l’indice suivant, unit02 et projet 02, unit03 et projet03 et ainsi de suite. La raison en est qu’avec deux chiffres, vos tests seront toujours triés même au-delà de dix alors que si vous choisissez un indice d’un seul chiffre, vous aurez projet10 au dessous de projet1 et non pas au dessous de projet9 alors que projet10 viendra logiquement au dessous de projet09. Si vous devez créer une nouvelle fiche, celle-ci sera nécessairement associée à une nouvelle unité (c'est la règle avec C++Builder), utilisez alors les lettres dans le nom de l’unité. Par exemple, vous en êtes à unit05 et projet05, imaginons que ce projet05 doit avoir un seconde fiche. Vous faites donc logiquement "nouvelle fiche" du menu principal, sauvegardez immédiatement cette nouvelle unité (choisissez "enregistrez sous") et choisissez comme nom unit05b, si vous devez en avoir une autre, sauvegardez-la sous le nom de unit05c et ainsi de suite. Tous vos tests personnels seront ainsi triés et structurés.

Je vous conseille aussi de tenir à jour, par exemple avec Notepad, un petit fichier de syntaxes C++ pour les avoir toujours à disposition car on ne peut pas se souvenir de tout. Quand, dans un de vos tests ci-dessous décrits, une syntaxe C++ vous semble importante et difficile à retenir, faites un copier-coller de l’unité cpp (c plus plus) vers ce petit fichier Notepad doté éventuellement d’un petit commentaire. Ces petits tests structurés et ce petit bloc notes personnel vous permettront d’évaluer votre propre avancée et de ne pas perdre votre travail.

Quant à l’application particulière que vous allez développer, créez-la dans un répertoire nouveau et spécifique à cette application, je vous conseille d'avoir un répertoire par application. Ce répertoire sera logiquement un sous-répertoire de "projects" dans lequel vous avez déjà créé "Essai" pour vos tests. Comme toujours, vous allez sauvegarder tout de suite le projet vide dès l’entrée dans C++Builder mais choisissez cette fois-ci un nom signifiant pour votre projet. Pour l’unité (par défaut unit1), donnez un nom qui représente une notion importante du projet, pour le nom du projet, donnez le nom le plus global qui soit, ce sera aussi in fine le nom de l’exécutable. Bien entendu, ces noms peuvent changer au cours du développement mais une bonne habitude consiste à donner des noms significatifs dès le début.

Notez que pour l’unité (par défaut unit1 proposé), C++Builder vous crée et le cpp (le source C++) et le h (header). Pour faire apparaître le header, faites ctrl F6 (contrôle F6) ou alors, cliquez à droite pour avoir le menu surgissant et choisissez la toute première possibilité "ouvrir le fichier source/en-tête". Ceci est notamment utile si vous devez rajouter des méthodes à la classe créée d’office par C++Builder.

Il est important de comprendre les fonctions les plus courantes dans le menu de C++Builder. Par exemple, nous avons dit que F12 permet de permuter la fiche avec le source, vous avez la possibilité d'avoir la même fonction via une icône. En laissant le curseur quelques instants sur une option du menu, une bulle d'aide (hint en anglais) apparaît, essayez de trouver l'icône correspondant à F12 (l'icône montre deux petits objets, l'un grisé l'autre blanc, munis de deux petites flèches pour évoquer la permutation), la bulle d'aide affichera "Basculer Fiche/Unité [F12]", il sera donc équivalent de faire F12 ou de cliquer cette option du menu. Dans le menu "Voir" vous avez également l'option "Basculer Fiche/Unité" qui fait la même chose.

Notez que le menu de C++Builder se constitue de toolbars (on reconnaît ce composant par ses deux petits traits verticaux sur le côté gauche). Vous pouvez disposer ces toolbars comme vous l'entendez, vous pouvez même les sortir de leur cadre (espace nommé controlbar), ils deviennent alors de petites fenêtres que vous pouvez aussi supprimer. Pour faire réapparaître une toolbar en cas de disparition, faites "Voir|Barre d'outils" et sélectionner la barre qui vous manque. Vous pouvez aussi cliquer à droite en pointant la zone toolbars de C++Builder, la liste des toolbars s'active et vous pouvez en faire réapparaître ou disparaître.

Pour tout enregistrer facilement, repérer l'icône qui représente plusieurs disquettes en cascade, il suffit de cliquer dessus, l'option se désactive alors. Dès qu'il y a la moindre modification dans votre projet, elle se réactive.

Pour réouvrir facilement un projet, vous pouvez faire "Fichier|Réouvrir" mais vous pouvez aussi repérer une icône jaune qui représente une sorte de dossier qui s'ouvre, juste à côté il y a une petite flèche, cliquez-la et choisissez votre projet. C++Builder a mémorisé pour vous les projets les plus récents. Si vous ouvrez toujours le dernier projet (ce qui est le cas quand on travaille toujours sur un même projet), il vous suffit d'appuyer sur la touche zéro, ce qui revient à sélectionner le premier élément de la liste, donc le plus récent.

Si vous cliquez une icône du menu dans la zone toolbar, vous obtenez alors la fenêtre de dialogue correspondant à cette fonction avec toujours une possibilité d'aide qui vous explique en français les diverses possibilités. Si vous passer par le menu déroulant, il vous suffit de faire F1, vous aurez l'explication de l'option qui était en surbrillance à ce moment-là. De même si vous sélectionnez un composant de la palette, faites F1 pour avoir de l'aide sur ce composant. De même quand le curseur se trouve sur un mot clé de votre source, F1 vous donnera de l'aide sur cette fonction. Il est impossible de travailler sans ces aides car il est impossible de se souvenir de tout.

Quand vous utilisez l’aide sur un composant après avoir sélectionné un composant de la palette et fait F1, vous tombez sur une fenêtre avec notamment "propriétés" et "méthodes", il s’agit évidemment des propriétés et des méthodes liées au composant sélectionné, vous avez aussi "événements". Choisissez par exemple "propriétés", une fenêtre apparaît mais l’autre reste visible. Faites en sorte que ces deux fenêtres ne se chevauchent pas, mettez par exemple la première fenêtre à droite et quand vous cliquez sur "propriétés", la fenêtre qui s’affiche à gauche sans chevauchement. Cette petite astuce va accélérer votre recherche car quand vous allez cliquer maintenant une propriété, l’aide correspondante va s’afficher sur la fenêtre de droite et vous passez facilement de l’une à l’autre. En général, on cherche un exemple pour avoir les syntaxes d’accès.

Pour commencer avec C++Builder, le mieux est un bon tutoriel c'est-à-dire un exemple complet expliqué au pas à pas, le fichier d’aide vous donne deux tutoriels. D'une manière générale, la règle du jeu est très simple. En entrant dans C++Builder, une fiche principale est déjà créée avec le code associé automatiquement écrit par le progiciel. Sauvez en premier lieu ce projet dans votre répertoire "Essai", pour ce faire cliquez l'icône qui représente des "disquettes en cascade" et choisissez unit01 et projet01. À ce stade, vous pouvez déjà cliquer la petite flèche verte pour exécuter (ou faire F9), le code écrit par C++Builder lui-même se compile puis s'exécute. Vous ne voyez qu'une fenêtre mais vous pouvez la déplacer, elle "sait" se redessiner, vous pouvez modifier sa taille et son menu système (i.e. les trois petits boutons en haut à droite de la fenêtre) est opérationnel. En cliquant le petit x de ce menu pour sortir, vous revenez à C++Builder.

Le principe de C++Builder consiste simplement à ajouter des composants de la palette de composants et à écrire le code relatif à certains événements. Par exemple, sélectionnez le composant "Bouton" en cliquant dessus, il se trouve dans l'onglet "Standard" de la palette de composants, c'est un petit rectangle à l'intérieur duquel on lit "ok". Une fois sélectionné dans la palette, cliquez n'importe où dans la fenêtre principale que C++Builder a nommé Form1. Vous venez de déposer ce composant dans la fenêtre principale et l'inspecteur d'objets confirme la présence de ce bouton. C++Builder suppose que vous n'allez pas garder ce nom de "Button1", c'est pourquoi dans l'inspecteur d'objets vous voyez la propriété Caption (ce qui signifie "nom affiché à l'écran") activée, mettez le curseur dans cette case et choisissez un autre nom par exemple "TEST". Vous voyez qu'en temps réel, l'affichage est mis à jour dans la fenêtre principale. Remarquez le nombre important de propriétés disponibles par exemple Left et Top qui sont les coordonnées de ce bouton dans la fiche. Déplacez ce bouton dans la fiche, vous verrez que C++Builder a mis a jour ces deux propriétés. De même Width et Height qui sont les dimensions du bouton. Si maintenant nous faisons F9 pour exécuter, ça marche mais nous n'avons pour l'instant associer aucune action au bouton.

Revenons sous C++Builder (cliquez le petit x du menu système pour stopper l'exécution) et sélectionnez l'onglet "événements" de l'inspecteur d'objets. Vérifiez bien que le bouton est bien sélectionné car nous avons maintenant deux objets à savoir Form1 et Button1. Si vous cliquez le bouton, l'inspecteur d'objets sélectionne le bouton mais si vous cliquez la fiche, il sélectionne logiquement Form1. D'ailleurs, en haut de l'inspecteur d'objets vous disposez d'un petit menu déroulant pour choisir un objet. L'objet Button1 étant visé par l'inspecteur d'objets, sélectionnez l'onglet "événements", on vous montre tous les événements que vous pouvez programmer. Double-cliquez sur l'événement OnClick, C++Builder vous crée ce qu'on appelle le gestionnaire d'événement associé dans le source, c'est simplement la méthode qui sera appelée quand le bouton sera cliqué et il positionne le curseur entre les deux parenthèses de la méthode car C++Builder attend que vous programmiez ce qui se passera quand le bouton sera cliqué.

void __fastcall TForm1::Button1Click(TObject *Sender)
{

}

Il s'agit bien d'une méthode de TForm1, laquelle a été déclarée dans la classe TForm1, faites ctrl-F6 (contrôle F6) pour faire afficher le source en-tête, vous lisez clairement void __fastcall Button1Click(TObject *Sender); dans la section "published". Affichons un message quand le bouton sera cliqué, à la position du curseur on écrit par exemple :

Application->MessageBox("Bouton cliqué!", "Ok", MB_OK);

La méthode Button1Click se présente donc sous cette forme :

void __fastcall TForm1::Button1Click(TObject *Sender)
{
Application->MessageBox("Bouton cliqué!", "Ok", MB_OK);
}

Faites F9 pour compiler-exécuter, cliquez sur le bouton, vous voyez bien le message apparaître.

La règle du jeu est donc toujours la même : on dépose des composants dans la fiche, on initialise ses propriétés dans l'inspecteur de d'objets, en général C++Builder offre de bonnes initialisations par défaut mais vous pouvez les adapter, on programme les événements associés à ces composants.

Notez que dès le départ, C++Builder a créé pour vous le constructeur de la fiche,

__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}

c'est là que vous écrirez les initialisations générales de l'application. Quant aux variables générales de l'application, on les déclare juste en dessous de la déclaration du pointeur Form1. Voici les deux endroits clés par rapport au source créé par C++Builder au départ.

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop

#include "Unit01.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
/* Toutes les variables et constantes générales ici */
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
/* profitez de ce constructeur pour initialiser votre application*/
}
//---------------------------------------------------------------------------

Un autre principe fondamental est la création logicielle de composants c'est-à-dire par programme. On crée les composants par new et on les supprime symétriquement par delete le moment venu. Par exemple nous allons faire la même chose que précédemment mais par programme. Repartons d'un projet vide, appelons-le unit02 et projet02.

Dans les variables générales (juste après TForm1 *Form1;), déclarons un pointeur sur un bouton.

TButton *B;

Dans le constructeur de TForm1, nous allons créer le bouton par logiciel.

//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
B=new TButton(this);
B->Parent=this;
B->Caption="TEST";
B->Height=20;
B->Width=60;
B->Left=15;
B->Top=15;
B->OnClick=Button1ClickX;
}

On crée le bouton par new, on indique son Parent (ici "this" c'est-à-dire Form1, on peut d'ailleurs remplacer this par Form1, ça marchera tout aussi bien), on donne son Caption (le mot qui sera affiché dessus), ses dimensions et sa position à l'écran et aussi la méthode qui sera exécutée en cas de click.

Maintenant pour construire cette méthode, on procède comme suit. Comme on n'est pas censé connaître par avance la syntaxe d'appel de la méthode OnClick pour un bouton, on pose un bouton sur Form1 puis on double-clique sur l'événement OnClick dans l’inspecteur d’objets, et ce, pour que C++Builder donne la syntaxe d'appel. Il faut donc :

1. sélectionner ce bouton dans l’inspecteur d’objets
2. sélectionner l’onglet "événements"
3. repérer l’événement OnClick et double-cliquer sur la case de droite en regard.

C++Builder crée pour vous la méthode dans le source :

void __fastcall TForm1::Button1Click(TObject *Sender)
{

}

On copie-colle cette méthode à la fin du source et on lui ajoute une lettre pour changer son nom, j'ajoute un ici X à la fin donc on a :

void __fastcall TForm1::Button1ClickX(TObject *Sender)
{

}

Maintenant on fait ctrl-F6 (contrôle F6) pour afficher l'en-tête, dans la section published, on lit :

void __fastcall Button1Click(TObject *Sender);

C'est la déclaration de la méthode pour le bouton qu'on a déposé sur la fiche. On copie-colle cette ligne en la mettant dans la section "public" et on lui rajoute le X, donc on a copié :

void __fastcall Button1ClickX(TObject *Sender);

Maintenant, on supprime le bouton de Form1 dont on n'a plus besoin, il nous a seulement servi à créer les syntaxes d'appel, on est sûr de ne pas se tromper puisque c'est C++Builder lui-même qui a créé les syntaxes. Maintenant qu'on a déclaré notre propre méthode, on programme comme précédemment la ligne, on obtient :

void __fastcall TForm1::Button1ClickX(TObject *Sender)
{
Application->MessageBox("Bouton cliqué!", "ok",MB_OK);
}

C'est la même chose que précédemment, on affichera un message quand le bouton sera cliqué. Faites F9 pour compiler-exécuter, vous voyez le bouton "TEST" apparaître et le message s'affiche suite à un click du bouton.

Comme le bouton a été créé par new, il est bon de le détruire par delete. Sélectionnez Form1 dans l'inspecteur d'objets et double-cliquez sur l'événement "OnDestroy", C++Builder vous crée le gestionnaire suivant :

void __fastcall TForm1::FormDestroy(TObject *Sender)
{

}

Cette méthode est exécutée au moment de la destruction de la fenêtre principale (donc à la fin de l'exécution de l'application), il suffit d'écrire à l’emplacement du curseur (suite à la création d’un gestionnaire d’événement, C++Builder positionne automatiquement le curseur entre les deux parenthèses car il attend que vous écriviez le code de la méthode), écrivez simplement :

delete B;

pour détruire le bouton.

Pour que vous ayez une vision globale, voici le programme complet. Une grande partie a été écrite automatiquement par C++Builder (en rouge), une partie a été écrite par C++Builder suite à une demande de création de gestionnaire d’événement (en vert) et quelques instructions écrites par nous pour compléter (en bleu).

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop

#include "Unit07.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
TButton *B;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
B=new TButton(this);
B->Parent=this;
B->Caption="TEST";
B->Height=20;
B->Width=60;
B->Left=15;
B->Top=15;
B->OnClick=Button1ClickX;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1ClickX(TObject *Sender)
{
Application->MessageBox("Bouton cliqué!", "ok",MB_OK);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete B;
}

Voici également l'en-tête :

//-----------------------------------------------------------------------
#ifndef Unit07H
#define Unit07H
//-----------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
//------------------------------------------------------------------------
class TForm1 : public TForm
{
__published: // Composants gérés par l'EDI
void __fastcall FormDestroy(TObject *Sender);
private: // Déclarations utilisateur
public: // Déclarations utilisateur
__fastcall TForm1(TComponent* Owner);
void __fastcall Button1ClickX(TObject *Sender);
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif

C'est donc un petit peu plus compliqué par programme mais guère plus. On crée le composant par new, on accède logiciellement à ses propriétés, on déclare des méthodes associées aux événements traités. Pour connaître les événements, utilisez l'aide en ligne ou alors déposez le composant sur la fiche pour l'étudier grâce à l'inspecteur d'objets, voyez les événements accessibles, créez-les pour en avoir la syntaxe en double-cliquant dans la case en regard de l’événement, copiez-collez la méthode vide en ajoutant par exemple une lettre, faites de même dans l'en-tête et mettez la déclaration résultante dans la section "public", puis virez le composant de la fiche. Vous vous êtes ainsi créé une méthode personnalisée, en bleu ci-dessus. Une autre méthode consiste à avoir dans un petit fichier de type Notepad ces syntaxes d’événements (en plus de syntaxes C++ particulières) que vous aurez collectées pendant vos tests dans le répertoire Essai prévu à cet effet, ainsi il vous suffira de faire un copier-coller du Notepad vers l’éditeur de C++Builder.

Notez également que vous pouvez ouvrir n’importe quel autre fichier C++ et procéder ainsi à des copier-coller d’un fichier à un autre. Ouvrir un fichier signifie simplement "ouvrir un fichier" et non l’inclure au projet, ce pourquoi vous pouvez toujours ouvrir n’importe quel fichier étranger au projet pour procéder à des copier-coller. En revanche, si vous faites "Nouveau|fichier cpp", là C++Builder crée un nouveau cpp et l’inclut au projet.

Quand dans un source cpp vous avez à faire des modifications périlleuses dont vous n’êtes pas sûr, une bonne méthode consiste à travailler sur une copie. Ce fichier cpp étant sélectionné dans l’éditeur, faites alors "Fichier|Enregistrer sous" et donnez-lui un autre nom, par exemple rajoutez "_copie" à la fin de son nom, C++Builder a automatiquement exclu l'ancien fichier du projet et l'a remplacé par ce nouveau qui n'est pour l'instant qu'une copie de l'ancien. Si vos modifications ont fonctionné, vous pouvez toujours continuer ainsi (ou alors vous pouvez faire de nouveau "Fichier|Enregistrer sous" pour lui redonner son nom d’origine) mais si vous voulez revenir en situation antérieure, il suffit de supprimer du projet le fichier devenu fautif avec _copie et d’ajouter au projet l’ancien fichier. Dans les icônes du menu de C++Builder, vous devez apercevoir une icône avec un + et une icône avec un -, ce sont ces fonctions qui vous permettront de réaliser cette suppression et cette addition. Sinon, dans le menu "Projet", vous avez clairement "ajouter au projet" et "retirer du projet". Donc vous retirez du projet le fichier avec _copie et vous ajoutez au projet l’ancien fichier correct, par ces deux opérations vous êtes revenu en situation antérieure.

Quand vous déposez un composant sur une fiche, vous ne vous occuper pas de son "instanciation" c’est-à-dire de sa création en mémoire, C++Builder s’en occupe. En revanche, quand vous instanciez un objet par new, vous le déclarez parent de la fiche principale ou d’un autre objet de la fiche, et vous le supprimez par delete au bon moment. De même pour toute allocation mémoire par malloc, n’oubliez pas de libérer cette zone par free au bon moment.

Notez qu'au moment de la compilation, C++Builder se débarrasse des gestionnaires vides. Par exemple double-cliquez sur un événement quelconque de Form1 pour créer la méthode vide correspondant à cet événement puis faites tout de suite ctrl-F9 (contrôle F9) pour compiler seulement l'unité en cours, vous observez que C++Builder a bien vu que la méthode était vide, donc il l'a enlevée du source. Il en est de même quand vous sauvegardez le projet (icône "disquettes en cascade"), les méthodes vides sont supprimées du source cpp et h. La seule méthode que le progiciel garde toujours, c'est le constructeur de la fiche (TForm1::TForm1) même si la méthode ne contient rien. Le plus généralement, cette méthode contiendra quelque chose puisque c’est là qu’on peut initialiser l’application. On peut l’initialiser aussi à l’événement OnCreate de la fiche principale mais puisque C++Builder nous offre ce constructeur, on s’en sert logiquement pour l'initialisation de l'application.

Si vous êtes amené à créer une autre fiche à la conception, vous aurez fatalement une autre unité .cpp et un autre en-tête .h. Mais ces deux fichiers ne peuvent pas communiquer pour l’instant. Si vous avez par exemple Form1 et Form2, vous ne pourrez pas accéder à Form1 dans unit2.cpp ni à Form2 dans unit1.cpp. Par exemple dans unit1.cpp vous ne pourrez pas écrire Form2->Visible=false car Form2, et donc ses propriétés et autres, ne sont pas encore accessibles à partir d’unit1. Ainsi imaginons que dans unit1.cpp vous vouliez accéder à Form2, sélectionnez dans l’éditeur unit1.cpp de manière à ce que ce soit le fichier actif (car l’éditeur n’a qu’un fichier de code actif à la fois) puis faites "Fichier|inclure l’en-tête d'unité", là C++Builder vous propose logiquement d’inclure l’en-tête de Form2 (unit2.h), acceptez, à partir de ce moment, Form2 devient accessible dans unit1.cpp. Réciproquement, si unit2.cpp doit accéder à Form1, sélectionnez unit2.cpp dans l’éditeur et incluez l’en-tête proposé qui est unit1.h, là Form1 sera accessible dans unit2.cpp car C++Builder a rajouté l’include concerné dans le source. Vous pouvez aussi faire cette opération à la main, ça marchera de la même façon mais en règle générale, il est préférable de passer par les fonctions du progiciel puisqu'il s'occupe de tout.

//-----------------------------------------------------------------------

#include "vcl.h"
#pragma hdrstop

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

Dans l'exemple précédent, on suppose que Form1 est accessible par unit09.h et Form2 par unit09b.h. Comme il y a autant d'unités que de fiches et de cadres réunis (car un nouveau cadre crée aussi une unité cpp et h), C++Builder connaît l'ensemble des en-têtes possibles pour le projet en cours et vous propose logiquement au moment de l'exécution de la fonction "Fichier|inclure l’en-tête d'unité" d'inclure un des en-têtes manquants au source cpp actif de l'éditeur. Ce principe est vrai pour un nombre plus grand de fenêtres et cadres. En revanche, si vous créez une fenêtre par logiciel via l’opérateur new, il n’y a pas de création d’unité cpp ni h, vous n’avez alors pas d’en-tête à inclure pour accéder à la fenêtre, vous y accédez directement au même titre qu’à tout objet instancié par new.

Notons pour clore ce petit survol qu'un grand nombre d'exemples d'applications programmées se trouvent dans le répertoire Program Files\Borland\CBuilder5\Examples. Chaque répertoire contient un ou plusieurs programmes d'un certain type, il suffit de faire "Fichier|Ouvrir un projet", de choisir un projet (repérable par le logo jaune C++) et de faire F9 pour l'exécuter. Cela vous donnera une idée sur la façon dont sont conçues les applications.

-:-


1. À propos de la fonction "Exécuter|jusqu'au curseur"

Votre curseur se trouvant sur une ligne quelconque du source, cliquer Exécuter du menu général de C++ Builder et choisissez l'option "jusqu'au curseur" du menu déroulant. Le programme arrêtera son exécution à ce moment là mais il faut préciser que le curseur doit se trouver sur une ligne du source où il y a effectivement une instruction, par exemple juste avant cette instruction mais ça marche aussi si le curseur se trouve au milieu de cette instruction, l'arrêt se fera précisément à cette instruction avant son exécution. Mais si le curseur se trouve sur une ligne blanche ou sur une ligne où il n'y a qu'une accolade, l'arrêt ne se fait pas. La doc ne précise pas ce point ce qui fait qu'on peut croire à un dysfonctionnement. Le mieux est de regarder sur la gouttière à gauche de la fenêtre, il y a un petit point bleu qui s'allume, cela signifie que cette ligne est un point d'arrêt possible, il faut donc toujours positionner le curseur sur un ligne doté d'un tel point bleu. Le mieux est d'utiliser le raccourci F4. Vous positionnez le curseur avant ou sur une instruction dans votre code, vous faites F4 pour exécuter le programme jusqu'à cette position. Dès que l'instruction est rencontrée, le logiciel vous affiche la fenêtre de code et l'instruction d'arrêt est surlignée. Là vous pouvez interroger certaines variables via Exécuter|Inspecter ou encore Exécuter|Évaluer/modifier (Ctrl F7). C'est évidemment très pratique pour interroger des variables parce que vous n'avez qu'à donner leur nom sans vous occuper du type (alors qu'un MessageBox ou un ShowMessage dans le programme à titre d'affichage de debug n'affiche qu'une variable du type Char* pour le premier et AnsiString pour le second).

Notez au passage que pour visualiser des tableaux entiers, il vaut mieux passer par "Exécuter|Inspecter" qui donne plusieurs visualisations différentes pour chaque élément du tableau (mode caractère, équivalent décimal, équivalent hexadécimal) alors que "Exécuter|Évaluer/modifier" est plus adéquat pour des variables à visualiser et éventuellement à modifier avant de reprendre l'exécution du programme. En utilisant "Exécuter|Inspecter", vous donnez d'abord le nom du tableau. Vous ne voyez certes que les premiers éléments mais vous pouvez agrandir cette zone de visibilité. Appuyez sur le bouton droit de la souris et choisissez "Étendue" (ou faites Ctrl R, ce qui revient au même), là vous pouvez indiquez le nombre d'éléments à voir.

Une autre méthode peut-être même plus rapide consiste à pointer quelques instants le curseur dans le source sur le nom d'une variable, le progiciel vous indiquera le contenu de la variable dans une toute petite fenêtre. Vous avez aussi la possibilité de passer par ce qu'on appelle des "points de suivi", voir alinéa 68 en troisième partie.

2. Points d'arrêt

Même principe pour les points d'arrêt, le curseur doit se trouver sur une ligne où il y a effectivement une instruction sinon l'arrêt ne se fait pas. Vous positionnez le curseur sur l'instruction où se fera le point d'arrêt, vous faites F5, la ligne où se trouve l'instruction est alors surlignée. On supprime ce point d'arrêt en faisant F5 de nouveau sur cette même ligne. Vous pouvez aussi cliquer sur la bande grise à gauche appelée gouttière qui sert de marge à la fenêtre du source C juste en face d'une ligne de code, un point d'arrêt sera de la même façon mémorisé à cet endroit, en cliquant de nouveau à ce niveau, sur le point rouge de repère, le point d'arrêt est supprimé. Ces points d'arrêt sont très pratiques, ils vous permettent de contrôler le contenu de certaines variables, ils se suppriment facilement (on pointe la ligne avec le curseur et on fait F5 ou l'on clique sur le point rouge) et votre code n'a pas été altéré par un ordre quelconque d'affichage des variables. Notez que pour continuer l'exécution du programme à partir du point d'arrêt il faut faire Exécuter|Exécuter (ce qui ne va pas de soi, on aurait pu envisager un ordre dans le menu Exécuter du type "continuer l'exécution", la doc ne parle pas de cette possibilité). Notez que Exécuter|Exécuter revient à cliquer sur la petite flèche verte en haut vers la gauche du menu ou encore à appuyer sur la touche F9.

3. Point d'arrêt pendant l'exécution

Quand vous exécutez le programme pour le tester en cliquant sur la petite flèche verte ou via F9, vous remarquerez dans la ou les fenêtres dédiées au source que de petits points bleus s'affichent sur la colonne grisée de gauche en face de chaque instruction. En cliquant sur un de ces petits points, vous créez un point d'arrêt à l'instruction correspondante et vous pouvez ainsi créer facilement d'autres points d'arrêt en cliquant d'autres petits points bleus. On les supprime en recliquant dessus. C'est très pratique car cela se fait pendant l'exécution même du programme, ces points bleus disparaissent avec leurs points d'arrêt associés quand le programme a terminé son exécution (auquel cas on revient à C++ Builder pour continuer le développement) ou si vous réinitialisez de vous même le programme (Exécuter|Réinitialiser le programme) pendant l'exécution ou au moment d'un arrêt. Ce sont donc des points d'arrêt temporaires ou volatiles qui n'existent que le temps d'un test.

4. Ouverture d'un projet.

Si vous avez créé un raccourci de C++ Builder sur le bureau et que vous y accédiez en cliquant dessus, le logiciel vous affiche par défaut un projet vierge. Si vous voulez ouvrir un projet en cours, le mieux est de passer par Fichier|Réouvrir qui vous propose une liste de projets que vous avez ouverts dans vos précédentes utilisations de C++ Builder. Les projets étant proposés dans l'ordre, le premier qui apparaît est celui sur lequel on a travaillé la dernière fois, on l'ouvre aussi en appuyant sur la touche 0 (zéro). C'est assez pratique quand on travaille longtemps sur un même projet, on l'ouvre ainsi de façon automatique sans se poser de question. Sinon, autre possibilité, vous allez avec l'explorateur Windows dans le répertoire où se trouve votre projet (on le reconnaît par le fait qu'il est doté de la même icône que C++ Builder, il s'agit du .bpr unique pour un projet donné) et vous cliquez dessus, C++ Builder est alors chargé avec ce projet. Sinon passez par "Fichier|Ouvrir un projet" mais dans ce cas C++Builder part toujours du répertoire C:\Program Files\Borland\CBuilder5\Projects\ par défaut.

5. Conversion AnsiString en Char*

Dans un code classique en C++, on a tout intérêt à utiliser des chaînes de caractères du type AnsiString, vous avez ainsi accès à des syntaxes beaucoup plus simples qu'en C. Par exemple on utilise simplement le signe = et on met une chaîne entre quotes pour initialiser une variable, par exemple Var_AnsiStr="Bonjour" au lieu de strcpy(VarChar, "Bonjour") du C classique. Idem pour les concaténations qui se font simplement avec le signe + pour des AnsiString au lieu d'un strcat en C. Cela dit, si vous êtes amené à utiliser des fonctions de C pur, comme C ne connaît pas du tout les chaînes de type AnsiString, vous devez alors convertir un AnsiString en Char*. Imaginez par exemple qu'un OpenDialog vous donne le nom d'un fichier à ouvrir et que ce nom soit dans une variable AnsiString, vous ne pourrez pas ouvrir ce fichier par un fopen du langage C car la chaîne d'un fopen est du type Char*, donc il faut convertir l'AnsiString en Char*. Si donc NomFic est déclaré en AnsiString, il faudra écrire NomFic.c_str() pour ouvrir le fichier via un fopen. Vous appliquez ainsi la méthode c_str à la variable AnsiString la convertissant de cette manière en Char*. Il faudra donc écrire

fopen(NomFic.c_str(), etc.………);

pour que ça marche. Pour plus d'information sur cette question, mettez votre curseur juste avant la chaîne "c_str" et appuyez sur F1, l'aide associée à cette question apparaît. (il en va d'ailleurs de même pour tout autre mot clé). Idem si vous voulez connaître les statistiques concernant ce fichier (accessibilité, longueur etc.) on écrira une instruction du genre

stat(NonFic.c_str(), &statbuf) ;

à partir de laquelle vous avez accès à toutes les caractéristiques du fichier Voir "stat" dans le fichier d'aide et les exemples pour plus d'information. Idem pour les fonctions C de traitement de chaîne de caractères, strcpy, strcat etc. N'oubliez pas les parenthèses ouvrante et fermante quand vous ajouter c_str() sinon vous avez l'erreur E2034 en compilation. Ne pas oublier que VarAS.c_str() où VarAS est un AnsiString est considéré comme une constante, vous ne pouvez donc l'utiliser qu'en lecture mais vous ne pouvez pas y mettre une valeur. Ainsi par exemple un fputs du C pur avec VarAS.c_str() sera correct car vous écrivez une chaîne de caractères mais un fgets sera fautif car vous ne pouvez pas écrire dans VarAS.c_str() qui encore une fois est une constante. Il faudra donc pour lire un fichier via fgets utiliser un char* puis convertir en AnsiString par une affectation classique, VarAS = VarC avec par exemple char VarC[256].

6. Aide en ligne

Pour connaître la signification d'un paramètre dans l'inspecteur d'objet, il suffit de cliquer sur ce paramètre et de faire F1, l'aide concernant ce paramètre apparaît dans une fenêtre. Idem pour se remémorer la signification d'une instruction C ou C++ dans le source, on positionne le curseur juste avant le mot clé et on fait F1. Idem pour comprendre une option du menu, vous déployez le menu déroulant, vous mettez en reverse video l'option et vous faites F1, l'aide en ligne de cette fonction apparaîtra.

7. Erreur de compilation

Pour avoir une explication de l'erreur de compilation, appelez l'aide de C++ Builder en cliquant sur le petit livre du menu en haut de l'écran ou via Aide|C++ Builder puis entrez dans la zone de saisie Ennnn où nnnn est le numéro d'erreur annoncée par le compilateur (la compilation est automatique avant une demande d'exécution ou par la construction de l'exécutable via Projet|Construire_projet où "projet" est le nom de votre propre projet qui apparaît maintenant dans le menu déroulant) ou encore Projet|Make. La réponse du compilateur se trouve dans une petite fenêtre au bas de l'écran juste en dessous du code source. En entrant Ennnn (où nnnn est le numéro de l'erreur e.g. E2034 pour l'erreur 2034), l'aide vous donnera des explications sur cette erreur particulière. Ou encore (et c'est peut-être mieux), vous double-cliquez sur la ligne signalant l'erreur au bas de l'écran pour la sélectionner (elle se met donc en reverse video) et vous faites comme d'habitude F1, une fenêtre apparaît pour vous donner des explications sur cette erreur. Notez que si le compilateur vous indique une ligne d'erreur où vous ne trouvez de toute évidence rien d'anormal, c'est la ligne précédente qui est souvent en cause notamment parce que votre instruction ne se termine pas comme ils se doit par un point-virgule. Pour aller à une ligne précise (par exemple celle annoncée par le compilateur pour vérifier votre code), faites Alt G et entrez le numéro de la ligne. Normalement, le progiciel surligne la première ligne de code trouvée en erreur mais si vous voulez y retourner par la suite, il suffit de double-cliquer dessus, c'est assez pratique.

8. Construire et Make

Dans le menu déroulant Projet, vous avez deux options importantes, l'une est Construire et l'autre Make, elles sont presque identiques. Construire recompile tout et crée l'exécutable. Make (Ctrl F9) ne recompile que ce qui a été modifié depuis la dernière fois et construit l'exécutable. Cette seconde option est donc préférable, vous ne recompilez que les fichiers modifiés, ce qui est plus rapide que de tout recompiler à chaque fois. Notez qu'il n'y a pas de touche raccourci pour Construire alors qu'il y en a une pour Make (Construire ne s'utilise qu'à titre de vérification, cette option vous permet d'être sûr que tout le projet se compile normalement notamment quand vous avez modifié des options de compilation ou des directives). Quant à l'option Compiler l'unité, elle ne compile comme son nom l'indique que la fenêtre active, elle vous permet de vérifier qu'il n'y a pas d'erreur de syntaxe dans votre programme.

9. Appel d'une fonction à l'intérieur du code d'un événement

Quand vous programmez le code correspondant à un événement, par exemple

void __fastcall TForm1::Ouvrir1Click(TObject *Sender) ;

code qui s'exécute quand vous sélectionnez la fonction "ouvrir" d'un menu déroulant, vous avez accès directement aux objets de l'objet principal, en l'occurrence ici TForm1. Si cette fiche contient par exemple un mémo que vous voulez rendre visible à ce moment là, vous écrirez par exemple simplement

memo1->show();

dans le code de cet événement, le programme comprendra qu'il s'agit de la fiche mémo1 de Form1. Mais si cet événement fait appel à une fonction que vous écrivez, vous ne pourrez plus écrire

memo1->show();

le compilateur dira qu'il ne connaît pas mémo1. Il faut alors dans ce cas préciser qu'il s'agit de la fiche memo1 de Form1, il faut donc écrire à l'intérieur de la fonction appelée

Form1-> memo1->show();

Et ça marchera.

10. Arrêt d'exécution

Si, suite à une erreur de programmation votre exécutable ne vous rend pas la main durant son exécution, vous avez néanmoins toujours accès au menu de C++ Builder qui reste actif malgré ce problème, il suffit donc de faire Exécuter|Réinitialiser le programme ou encore Ctrl F2 qui fait la même chose. Vous avez ainsi repris la main sur votre développement.

11. Extraction à l'intérieur d'un AnsiString d'un ou plusieurs caractères

Si vous voulez extraire un seul caractère d'un AnsiString, il suffit d'écrire sa position entre crochets sachant que le premier caractère à le numéro 1. Par exemple CarUnique = Chaine[3] fera que la variable CarUnique contiendra le 3ème caractère de Chaine, CarUnique et Chaine étant tous deux des AnsiString. Mais si vous voulez extraire une suite de caractères, il faut utiliser la méthode SubString(P,L) où P est la position de départ et L la longueur à partir de cette position. Par exemple SousChaine=Chaine.Substring(3,2) extraira 2 caractères à partir du troisième caractère de la chaîne de caractères. Notons que SousChaine=Chaine[3]+Chaine[4] qui serait censé faire la même chose ne marche pas, le compilateur ne signale pas d'erreur mais le résultat est faux.

Quand vous saisissez le point dans une syntaxe du type Chaine.SubString (le point annonce la méthode), il suffit d'attendre une seconde ou deux, le progiciel vous propose d'office toutes les méthodes possibles dans le contexte. Si cela ne marche pas, appuyez sur le bouton droit de la souris dans une fenêtre de code C et choisissez tout en bas du menu déroulant l'option propriétés, là choisissez l'onglet "audit de code" et cochez la case "achèvement du code", c'est ce flag qui indique qu'on propose au développeur la liste des méthodes disponibles dans le contexte, cliquez sur aide et vous connaîtrez le détail de ces propriétés.

12. La fonction IntToHex

Cette fonction permet de convertir un integer en hexadécimal (base 16) sur un nombre de digits donné. On écrit par exemple VarAS = IntToHex(i,2) signifie que la chaîne AnsiString VarAS contiendra après exécution l'équivalent décimal du nombre entier i sur deux digits, i pouvant aussi être du type char (un char peut toujours être considéré comme un entier). Mais il faut préciser que i est considéré comme étant codifié en complément à 2 (ce qu'oublie de préciser la doc) et donc peut être négatif (bit le plus significatif au 1 logique). Si donc ce nombre entier est négatif, IntToHex l'écrit d'office sur huit chiffres hexadécimaux (i.e. quatre octets). Cela peut provoquer des erreurs. Imaginez un buffer de caractères par exemple char buffer[100] déclarant ainsi une chaîne de 100 caractères c'est-à-dire de 100 octets. Vous voulez afficher en hexadécimal chacune de ces cases mémoire. Chacune à un contenu allant en décimal de -128 à +127 c'est-à-dire en hexadécimal allant de 0x00 à 0XFF. Les valeurs positives peuvent être extraites normalement sur deux chiffres hexadécimaux. Mais, toutes les valeurs négatives c'est-à-dire celles dont le bit le plus significatif est au 1 logique seront transcrites sur 4 octets. Donc le mieux sera de tout extraire sur 4 octets et ensuite d'utiliser un SubString pour extraire de ce résultat les deux derniers caractères c'est-à-dire 2 caractères à partir de la 7ème position. On écrira donc pour traiter la ième case du buffer HexaDec = IntToHex(Buffer[i],8).SubString (7,2) où HexaDec est un AnsiString qui contiendra après exécution les deux digits hexadécimaux. Vous voyez qu'on applique directement la méthode SubString qui extraie 2 caractères à partir de la 7ème position de l'AnsiString donné par IntToHex sur 8 chiffres. La logique eût voulu qu'on puisse écrire dans ce cas HexaDec = IntToHex(Buffer[i],2) mais malheureusement ça sera faux pour toutes les valeurs négatives qui donneront non pas deux chiffres comme demandé mais huit. (Bug?).

13. Longueur de vos buffers

Attention à ne pas dépasser la longueur de vos buffers. Si vous écrivez au-delà du buffer, vous risquez d'écraser certaines données qui se trouvent par hasard juste après ce buffer mais l'exécution du programme peut très bien continuer encore un peu si ces variables ne sont pas vitales à ce moment-là de l'exécution. Cela dit, tôt ou tard le programme va planter et c'est un des cas où vous avez droit au message "l'application va s'arrêter car elle a exécuté une opération non conforme". Notez que même dans ce cas, C++ Builder garde toujours la main si vous testez sous cet environnement, vous appuyez sur OK et vous retournez à votre source que vous pouvez maintenant vérifier. Si votre programme fonctionnait au moment du dernier test, mettez un point d'arrêt juste avant les lignes rajoutées ou modifiées et faites du pas à pas (une fois arrêté, faites Maj F7 pour exécuter jusqu'à la ligne suivante et continuez ainsi le debug).

14. Explorateur de classes

À gauche des fenêtres source, vous avez un explorateur de classes que vous pouvez déployer en cliquant sur le petit +. Cela vous donne la structure de votre programme, en cliquant sur une fonction, le curseur se positionne au début de celle-ci dans le source. C'est donc très pratique pour s'y retrouver. Si vous supprimez cet explorateur en cliquant le petit x sur le bord haut de la fenêtre, ce qui vous donne une fenêtre plus grande au moment d'écrire le source, vous pouvez toujours le récupérer via Voir|Explorateur de classes. Pour que les fonctions soient accessibles à partir de cette liste, il faut déclarer leur prototype en début de listing, ce qui d'ailleurs est obligatoire si l'appel est antérieur dans le listing à la fonction elle-même (sinon le compilateur ne connaît pas le prototype de la fonction). Soit par exemple la fonction Essai qui revoie un AnsiString et qui a pour argument un integer et un AnsiString, vous déclarerez au début son prototype à savoir dans ce cas AnsiString Essai(int, AnsiString); le compilateur connaît alors le type du résultat et le type des arguments. La fonction étant dûment déclarée, elle sera accessible à partir de l'explorateur de classes (elle s'écrit d'ailleurs dans l'explorateur en même temps que vous l'écrivez dans le source). Sinon, si vous ne déclarez pas ainsi la fonction (ce qui est possible à condition que vous écriviez la fonction dans le listing avant que vous l'utilisiez par un appel), elle apparaîtra dans l'explorateur de classes mais en cliquant dessus, il ne se passera rien. À vous donc de savoir si vous voulez pouvoir accéder rapidement au code d'une fonction ou non. Pour plus d'informations sur les classes, voyez mon Étude autour des classes du C++.

15. Initialisations

Si votre programme doit procéder à des initialisations, créez-les au moment de la construction de la fiche principale dont la fonction (automatiquement créée par le logiciel) est facilement reconnaissable par sa syntaxe

__fastcall TForm1::TForm1(TComponent* Owner): TForm(Owner);

Cette fonction apparaît d'ailleurs dans l'explorateur de classes. Cliquez dessus et le progiciel positionnera le curseur au début du code de cette fonction qui n'est simplement que déclarée mais qui ne contient rien pour l'instant.

16. Déclaration des variables

Vous voulez savoir où une variable ou constante a été déclarée, pointez le curseur sur son nom et appuyez sur Ctrl (la touche contrôle). La variable devient une sorte de lien hypertexte, en cliquant sur elle, le curseur pointe exactement la ligne du source où elle a été déclarée. En faisant Ctrl Z (contrôle Z), vous revenez où vous en étiez dans le source. Pratique non?

17. Fenêtre d'exécution

Si, après avoir cliqué dans le source un point d'arrêt pour débuguer une partie de programme juste après avoir lancé un test par F9 qui exécute l'application en cours de développement, vous avez perdu la fenêtre de l'application en train de s'exécuter (la fenêtre où se trouve le source étant maintenant active puisque vous l'avez sollicitée pour y positionner un point d'arrêt), vous la retrouver facilement en appuyant sur Alt Tab (touche Alt puis Tab). Là, Windows vous propose d'exécuter une autre fenêtre dont il vous indique le nom. Si ce n'est pas celle-là, appuyez de nouveau sur la touche tabulation (Alt étant toujours appuyée) jusqu'à ce que vous tombiez sur le nom de votre application. Cela vous évite d'avoir à modifier l'emplacement des fenêtres qui cache le bas de l'écran où toutes les programmes en cours sont affichés dans de petits rectangles. Si toutefois ces petits rectangles sont visibles, vous cliquez alors directement votre application qui est venue s'ajouter au nombre des programmes en cours.

18. Grand fichier cpp

Pour une fiche donnée, le progiciel vous fournit un premier fichier point cpp (C++). Si ce fichier devient trop grand, le mieux est de diviser en deux ce grand fichier d'origine et donc de créer un autre fichier cpp qui contiendra des routines que vous ne toucherez plus ou pratiquement plus, fichier qu'il faut compiler à part. La marche à suivre est la suivante.

  1. faire Fichier|Nouveau et choisissez comme objet un fichier cpp parmi les choix possibles.
  2. par défaut, ce nouveau fichier s'appelle file1.cpp, le mieux est de faire maintenant Fichier|Enregistrer sous et de lui donner le nom que vous voulez. Comme vous allez essayer de regrouper des routines ayant une signification globale pertinente, donnez lui un nom en conséquence. Vous venez donc d'enregistrer un nouveau fichier vide cpp.
  3. Observez que dans le cpp portant le nom du projet, C++ a automatiquement rajouter le directive USEUNIT avec le nom de ce nouveau cpp source. D'ailleurs si vous faites maintenant Voir|Gestionnaire de projets, vous voyez que ce nouveau fichier fait maintenant partie du projet.
  4. Recopier toute l'en-tête du fichier cpp d'origine (il s'agit du fichier crée par C++ Builder au moment de la création de la fiche associée). Il faut recopier la première section contenant tous les fichiers d'include (ou alors si vous voulez peaufiner, ne sélectionnez que les includes dont vous avez besoin dans ce nouveau fichier). De toute façon, il y aura des erreurs à la compilations s'il en manque.
  5. Couper toutes les routines concernées par ce transfert et collez-les dans ce nouveau fichier. Normalement, leur prototype a été déclaré au début du fichier d'origine, donc si ce fichier d'origine fait appel à ces routines qui sont maintenant dans l'autre fichier, il n'y aura pas de problème de compilation.
  6. Faites maintenant Projet|compiler l'unité. Si vous appelez des routines situées dans l'autre fichier, le compilateur vous le signalera, il suffira alors de déclarer le prototype de ces fonctions. Par exemple vous appelez la routine "essai" de l'autre fichier (celui d'origine à scinder en deux) qui renvoie un entier et qui a deux arguments, un integer long et un pointeur de caractères, vous déclarerez ce prototype ainsi int essai(long int, char*);
  7. Ayant compilé cette unité à part sans erreur de compilation, le progiciel vous a créé le fichier point obj correspondant dont a besoin le lieur au moment de la construction de l'exécutable.
  8. Faites maintenant Projet|construire (c'est-à-dire créez l'exécutable complet du projet), vous voyez que ça marche parfaitement. Vous avez tout à y gagner car d'une part cela vous évite d'avoir à manipuler des fichiers trop grands et d'autre part, la compilation est plus rapide puisque le nouveau fichier est déjà compilé.
  9. Remarque. Les fonctions situées dans ce nouveau fichier ne seront accessibles par l'explorateur de classes qu'après sa première compilation. Cela signifie que si vous cliquez sur une des fonctions visibles dans l'explorateur de classes et appartenant à ce nouveau source C++ pour y positionner le curseur, cela ne fonctionnera qu'après une compilation de ce fichier (si le fichier n'est pas compilé, il ne se passe rien). Notez également que quand vous chargez un développement, C++ Builder ne charge que le fichier source de base i.e. celui qu'il a lui-même créé au moment de la création du projet. Mais si vous cliquez dans l'explorateur de classe sur une fonction qui est dans un autre fichier (celui que vous avez créé pour diviser le source en deux), C++ Builder chargera alors automatiquement ce fichier. C'est évidemment très pratique, vous n'avez pas besoin de savoir dans quel fichier source se trouve une fonction (si votre projet est complexe, il peut bien sûr y en avoir plusieurs et ça deviendrait vite compliqué), vous l'appelez via l'explorateur de classes qui se charge d'ouvrir le bon source mais encore une fois, cela ne fonctionne qu'après une première compilation de ce nouveau source. Il faut signaler que dans certains cas, l'explorateur de classes ne trouve pas la fonction bien qu'il soit dûment déclarée, il semble qu'il y ait un bug à ce niveau.
  10. Autre remarque. Pour tester certaines syntaxes C que vous maîtrisez mal, vous pouvez toujours vous créer une sorte de brouillon.cpp isolé que vous pouvez compiler à part juste pour vérifier une syntaxe. De toute façon, il est bon d'avoir un répertoire de travail qui contient une copie temporaire de votre développement pour des tests spécifiques sans risque de toucher votre original. Une fois votre algorithme éprouvé, vous allez dans votre répertoire officiel que vous mettez à jour de ces nouveautés par un copier-coller judicieux.

19. Gestionnaire de projet

Après avoir chargé votre projet (si vous travaillez toujours sur le même, utilisez Fichier|Réouvrir et une fois le menu déroulant actif, appuyez sur la touche 0), vous pouvez visualiser tous les sources liés à ce projet en faisant Voir|Gestionnaire de projet. C'est très pratique notamment quand vous avez différentes variantes pour savoir où vous en êtes. En effet, quand vous utilisez la fonction Enregistrer sous, c'est ce nouveau fichier qui devient actif, l'ancien ne fait plus partie du projet mais le fichier existe toujours. Si vous voulez revenir à l'ancienne version, il faut alors utiliser les fonctions Projet|retirer du projet (là vous supprimez le source dont vous ne voulez plus) puis Projet|ajouter au projet (là vous remettez en service l'ancien source). Si vous ne savez plus où vous en êtes suite à plusieurs de ces changements, utilisez la fonction Voir|Gestionnaire de projet qui vous indiquera clairement les fichiers impliqués à ce moment-là.

20. Conseil personnel

Pour aller vite, la meilleure façon est d'aller très lentement, de penser profondément les choses, de les structurer, de les maîtriser. Cioran disait "toute forme de hâte trahit quelque dérangement mental". Festina lente, hâte-toi lentement disaient les anciens. L'extrême lenteur est une sorte de vitesse absolue, vous vous en rendrez compte en en tentant l'expérience dans ce monde de suante précipitation. Ayez aussi cet adage en tête : suus cuique diei sufficit labor (à chaque jour suffit sa peine) c'est-à-dire n'essayez pas de tout faire en une seule fois. L'important est simplement d'avancer dans votre projet, non pas avancer vite mais simplement avancer. À ce sujet, il est très bon d'utiliser la fonction "ajouter un élément A faire" via Maj Ctrl T (vous appuyez simultanément sur Majuscule, Contrôle et la lettre T) ou encore, en cliquant sur le bouton droit de la souris, un menu déroulant apparaît où cette fonction est disponible. La principe est simple. Vous positionnez votre curseur à un endroit du listing où vous avez quelque chose à faire et à ne pas oublier. Vous appelez cette fonction et vous écrivez un petit mémo en style télégraphique rappelant brièvement ce qu'il y a à faire. Le texte ainsi saisi est écrit en commentaire dans votre source mais de plus son endroit dans le source est mémorisé par le progiciel. Ensuite, revenant à votre projet le lendemain, vous cliquez Voir|Liste à faire, toutes les occurrences de ce genre apparaissent, il suffit de cliquer sur l'une d'entre elles pour y accéder, C++ Builder charge le source cpp si ce n'est déjà fait et vous présente l'endroit du source où il y a ce commentaire et où il faut rajouter quelque chose. Vous pouvez aussi assigner un priorité, une catégorie (c'est une chaîne de caractères qui contient un code à vous) etc. Donc avant de fermer votre projet après une journée de travail, notez par un texte du type "A faire" l'idée que vous aviez, c'est très utile. Pour supprimer un élément de cette liste, vous avez le choix entre le supprimer dans le listing ou de visionner cette liste, de sélectionner l'occurrence à supprimer et d'appuyer sur la touche Suppr du clavier.

21. Remarques et exemple autour de malloc (memory allocation)

Je fais une légère déviation vers le C pur car les syntaxes de malloc sont assez difficiles et l'aide en ligne ne donne qu'un tout petit exemple.

Quand vous devez déclarer un tableau dont vous ignorez la longueur (la longueur va dépendre d'une donnée que vous ignorez au moment du développement soit par exemple une saisie de l'utilisateur au clavier soit suite au chargement d'un fichier etc.), il est préférable d'utiliser la fonction C malloc (memory allocation). Mais il faut se souvenir que malloc non seulement réserve l'emplacement mémoire demandé et d'autre part initialise un pointeur (c'est toujours un pointeur donc vous devez avoir le signe *) qui pointe le premier élément de cette structure. Par exemple vous ouvrez le fichier N (N étant un AnsiString qui contient le chemin d'accès et le nom du fichier, MES également qui contiendra un message d'erreur le cas échéant, Fichier est du type FILE*)

if ((Fichier = fopen(N.c_str(), "r+b"))== NULL)
{
MES="Impossible d'ouvrir le fichier "+N+".";
Application->MessageBox(MES.c_str(),NULL,MB_OK);
return;
}

Notez que le fichier a été ouvert ici en mode r+b, cela signifie d'une part en lecture écriture (r pour read et + pour possibilité d'ajout) et d'autre part en binaire, cela signifie qu'on considère que c'est une suite d'octets. Voyez la documentation pour plus d'information sur la manière d'ouvrir les fichiers (vous mettez votre curseur devant fopen et vous faites F1, c'est l'aide en ligne).

Ensuite vous interrogez les stats pour connaître la longueur de ce fichier N, par exemple :

stat(N.c_str(), &statbuf);
LongFic = statbuf.st_size;

Cette syntaxe suppose d'une part que vous ayez déclaré au début du source l'include sys\stat.h ainsi : #include <sys\stat.h> et d'autre part que vous ayez déclaré statbuf (nom de variable arbitraire que je choisis) qui résulte de la structure officielle des statistiques de fichiers, on le déclare ainsi : struct stat statbuf;

Dans ces conditions stat (N.c_str(), &statbuf) signifie que vous demandez de mettre à jour dans la structure statbuf les renseignements concernant le fichier N. Ces statistiques étant maintenant en mémoire, on peut les lire notamment la longueur du fichier (mais bien d'autres choses, voyez l'aide en ligne). On a donc écrit LongFic = statbuf.st_size, LongFic est donc maintenant égal à la longueur du fichier en nombre d'octets que l'on a ouvert. Si maintenant on veut lire ce fichier en mémoire, il faut réserver une place de LongFic octets que l'on ne peut pas connaître à l'avance. Donc on utilise malloc qui va allouer LongFic octets. Il est bon de tester LongFic qui ne doit pas être nul sinon cela signifie que le fichier est vide :

if(!LongFic)
{
MES="Le fichier "+N+" est complètement vide, zéro octet.";
Application->MessageBox(MES.c_str(),NULL,MB_OK);
return;
}

Remarquez que if(!a) équivaut à if(a==0), de même if(a) équivaut à if(a!=0). Un moyen mnémotechnique consiste à se souvenir que "non nul" c'est "quelque chose" et que "nul" c'est "rien". Ainsi while(a) signifiera "tant que a vaut quelque chose" et while(!a) "tant que a ne vaut pas quelque chose" et donc "tant que a est nul".

On suppose avoir préalablement déclaré un pointeur de caractères au début du programme (ou au début de la fonction) par exemple char* PtrFic; PtrFic est donc un pointeur qui pointe un élément ou une suite d'éléments du type char.

Il faut donc maintenant réserver une zone mémoire de LongFic octets à partir de ce pointeur. On utilise malloc et l'on écrit :

// allocation de cette mémoire à partir de PtrFic
if ((PtrFic = (char *) malloc(LongFic)) == NULL)
{
MES="Le fichier "+N+" est trop grand. Je n'arrive pas à "
"allouer la mémoire correspondante. "
"Sa longueur est de "+IntToStr(LongFic)+" octets. "
"Essayez de libérer un peu de mémoire et recommencez";
Application->MessageBox(MES.c_str(),NULL,MB_OK);
return;
}

Notez que si LongFic est nul, cela revient à demander 0 octet via malloc qui renvoie logiquement NULL. C'est pourquoi nous avons écarté ce cas précédemment, à ce stade du programme on sait que LongFic est non nul.

Remarquez aussi la façon de concaténer les chaînes de caractères. On écrit entre quotes une première chaîne puis une deuxième à la ligne suivante et ainsi de suite. Cela évite d'avoir des lignes trop grandes. Le compilateur comprend qu'il s'agit d'une seule et même chaîne.

La fonction C malloc renvoie NULL si la demande d'allocation mémoire a échoué. Cela n'arrive pratiquement jamais avec nos ordinateurs d'aujourd'hui dotés de 64 voire 128 MO mais c'est toujours possible si l'utilisateur a ouvert trop d'applications en même temps. De toute façon il faut toujours tester un code retour pour une bonne qualité de programme, c'est ce qu'on appelle la programmation paranoïaque (defensive programmation), c'est le seul cas où il est excellent d'être parano. Faites attention au nombre et à la place des parenthèses. Si l'allocation a réussi, PtrFic pointe le premier octet d'une zone mémoire de LongFic octets. Dans ces conditions on peut lire le fichier en une seule fois ainsi :

if((NbData=fread(PtrFic,1,LongFic,Fichier))!=LongFic)
{
MES="Impossible de lire le fichier "+N+
" dans sa totalité bien que l'allocation "
"mémoire vient d'être acceptée, "
"le fichier a une longueur de "+IntToStr(LongFic)+
" mais je n'ai pu en lire que "+IntToStr(NbData)+".";
Application->MessageBox(MES.c_str(),NULL,MB_OK);
fclose(Fichier);
return;
}

où NbData est un unsigned long int. Comme la fonction fread renvoie le nombre d'octets effectivement lus, il suffit de comparer le nombre d'octets à lire (ici LongFic) au nombre d'octets effectivement lus (ici NbData) pour savoir si la lecture s'est bien passée. Voyez l'aide en ligne pour le détail de fread. Vous accédez à chacun de ces octets en mettant entre crochets son numéro d'ordre qui ira de 0 à LongFic - 1, par exemple PtrFic[i] est le ième octet pointé par PtrFic, i étant un unsigned long int (codé sur 4 octets donc sur 32 bits).

Pour libérer cette mémoire allouée, il faudra écrire free(PtrFic);

22. Autres exemples de malloc

Il faut se souvenir que malloc renvoie toujours un pointeur qui pointe le premier élément d'un série d'éléments d'un certain type. Par exemple, vous voulez allouer à partir d'un pointeur d'unsigned long int Longueur (nom arbitraire de ce pointeur) une zone mémoire de NbULI unsigned long int, vous écrirez :

if ((Longueur = (unsigned long int*)
malloc((NbULI)*sizeof(long int))) == NULL) return;

Dans cet exemple si la mémoire n'a pu être allouée, le pointeur est égal à NULL et il y aura ici retour de la fonction sinon on continue. Notez que cette écriture suppose que vous ayez déclaré au début (ou quelque part avant l'appel à malloc) la variable Longueur comme pointeur sur un unsigned long int ainsi :

unsigned long int* Longueur;

Notez que malloc peut être utilisé au moment de la déclaration d'une variable, par exemple :

char* P=(char*)malloc(1000);

Le compilateur C de C++ Builder exige que l'on reprécise clairement au moment du malloc le type char* même dans ce cas bien qu'il y ait redondance. La réservation mémoire est tellement dérisoire que le code retour du malloc n'est pas testé dans ce cas. Cette instruction réserve une zone de mille octets à partir de l'adresse P et est équivalente à char P[1000] si ce n'est que cette dernière notation ne permettra pas par la suite la possibilité d'une réallocation via realloc.

23. Exemple de malloc pointant des structures

Supposons qu'on déclare la structure suivante :

struct StructureEssai

struct StructureEssai
{
unsigned char CAR;
int N1,N2;
} STE;

Cette structure contient trois éléments, un caractère CAR et deux entiers N1 et N2. StructureEssai est le nom de ce type de structure et STE une variable de ce type. Vous n'êtes pas tenu de déclarer ces deux éléments. Si vous ne voulez qu'une seule variable de ce genre, la structure n'a alors pas besoin de nom et il vous suffit d'écrire :

struct
{
unsigned char CAR;
int N1,N2;
} STE;

Si vous n'avez pas besoin de variable mais seulement de la déclaration de la structure, vous écrirez :

struct StructureEssai
{
unsigned char CAR;
int N1,N2;
};

auquel cas la structure n'existe qu'à titre de prototype.

Si vous déclarez la variable STE (nom arbitraire, structure essai), vous accédez au caractère de cette structure STE par la syntaxe STE.CAR et aux entiers via STE.N1 et STE.N2. Remarquez le point qui sert à désigner un des éléments de la structure.

On veut maintenant déclarer via malloc une zone mémoire d'une longueur de NBS fois cette structure (NBS étant un nombre arbitraire non connu à l'avance, NBS est le nom arbitraire de cette variable pour nombre de structures) avec un pointeur qui de pointera la première structure de la série. Dans ce cas, la variable STE ne vous sert à rien, vous n'avez besoin que du prototype de la structure.

On déclare le pointeur ainsi :

StructureEssai* PointeurStruc;

PointeurStruc est donc un pointeur qui pointe un élément de type StructureEssai.

On réserve via malloc la mémoire ainsi :

if ((PointeurStruc = (StructureEssai*)
malloc((NBS)*sizeof(StructureEssai))) == NULL) return;

D'une part vous allouez NBS fois une structure de longueur StructureEssai et d'autre part PointeurStruc pointe le premier élément de cette structure.

Soit i un entier non signé (unsigned int ou unsigned long int) compris entre 0 et NBS-1,

PointeurStruc[i].Car sera le caractère de la ième structure,

PointeurStruc[i].N1 sera le premier int de la ième structure,

PointeurStruc[i].N2 sera le deuxième int de la ième structure,

avec toujours ce fameux point pour accéder à un élément de la ième structure.

Notez que si vous déclarez la variable STE comme c'est possible (revoir plus haut dans ce même alinéa les différentes possibilités de déclaration), vous pouvez alors lire en une seule fois la ième structure ainsi : STE = PointeurStruc[i]; et dans ce cas vous accédez au caractère de la structure STE qui vient d'être lue par la syntaxe STE.CAR et aux entiers via STE.N1 et STE.N2 comme nous le disions déjà plus haut.

24. Malloc dans un cas de tableaux de pointeurs de séries de structures

Si vous déclarez un tableau de pointeurs vers ces structures par exemple StructureEssai* PointeurStruc[50]; qui déclare un tableau de 50 pointeurs vers une structure de type StructureEssai, vous déclarerez la kème allocation mémoire via malloc par :

if ((PointeurStruc[k] = (StructureEssai*)
malloc((NBS)*sizeof(StructureEssai))) == NULL) return;

k étant supposé compris entre 0 et 49 puisque dans notre exemple nous déclarons 50 pointeurs vers une structure (les autres variables ont la même signification que précédemment).

Dans ce cas, pour accéder au caractère de la ième structure de la kème série de structures il faudra écrire : PointeurStruc[k][i].CAR; où il faut bien sûr remarquer les deux doubles crochets, le premier invoque le kème pointeur, il pointe donc la kème série de structures de ce type et le second la ième structure de cette série numéro k, la bonne structure étant maintenant pointée, on complète par le point et le nom de l'élément à invoquer ici CAR.

25. À propos de la réallocation mémoire (realloc)

Quand une zone mémoire allouée par malloc s'avère insuffisante dans la contexte, on réalloue cette mémoire via l'instruction standard C realloc mais il ne faut pas oublier que realloc renvoie la nouvelle position du pointeur qu'il faut assigner au pointeur courant. Par exemple vous avez besoin d'une zone mémoire de bloc octets, bloc étant une constante arbitraire du programme initialisée ainsi : const int bloc = 1000; Vous avez déclaré un pointeur P sur une chaîne de caractères ainsi char* P; vous initialisez la longueur de la zone LZ à bloc (LZ = bloc; LZ étant un unsigned int) et vous déclarez une zone de LZ octets via malloc pointée par P :

P = (char*) malloc (LZ);

À ce stade du programme P pointe le premier octet d'une zone de LZ octets. Un offset o initialisé à 0 naviguera dans cette mémoire et y écrira via une syntaxe du type P[o] = aliquid; (le oème octet pointé par P prend la valeur aliquid, comme c'est un octet, aliquid ira de 0 à 255 ou de -128 à +127 si vous êtes en signé). Mais quand o, suite à une incrémentation sera égal à LZ, il sera hors zone (la zone ne va que de 0 à LZ-1), il faudra donc réallouer la mémoire pour l'agrandir et y ajouter par exemple un bloc à cette zone, on écrira donc :

LZ+=bloc;
P= (char*) realloc(P,LZ);

ou encore en une seule ligne :

P=(char*) realloc(P,LZ+=bloc);

Notez bien cette syntaxe du realloc qui a deux arguments, pointeur et longueur et qui renvoie la nouvelle position du pointeur après réallocation. La longueur de la zone LZ toujours pointée par P est rallongée de bloc octets (première instruction) et P pointe maintenant cette nouvelle zone mémoire plus longue de bloc octets. Si P==NULL après ce realloc, la réallocation a échoué, cela devrait ne jamais se produire mais s'il faut toujours tester un code retour par sécurité (programmation paranoïaque). Il se peut que P n'ait pas bougé de position s'il se trouve possible à ce moment-là du point de vue du système de rallonger la zone sans ce changement mais il se peut tout aussi bien que P ait complètement changé de position dans la mémoire. Cela ne vous regarde en rien, le programme continue comme si de rien n'était, toutes vos données sont bien sûr conservées mais la nouvelle zone est maintenant plus grande. On peut faire autant de realloc que l'on veut. Dans tous les cas, ne pas oublier de libérer la mémoire après utilisation via free(P);

La nouvelle réservation peut aussi être plus petite que l'ancienne (utilisation probablement rare mais possible), dans ce cas, seule la section commençante des données de l'ancienne zone est disponible dans la nouvelle.

La fonction realloc est très utile quand on ne sait pas à l'avance combien de mémoire sera nécessaire. On procède alors par blocs. Un premier malloc crée une première réservation. On surveille de près l'offset d'accès en lecture-écriture, dès que cet offset est hors zone, on agrandit alors l'allocation via realloc en testant le code retour (si le pointeur est égal à NULL, il y a échec de l'allocation et donc probablement arrêt pur et simple du programme qui ne peut pas continuer). Si vous y allez par incrémentation, l'offset est hors zone dès qu'il est exactement égal à la longueur de la zone, si o est cet offset et LZ la longueur réservée à un moment donné, o ne pourra aller que de 0 à LZ-1, dès que o = LZ il est en dehors de la zone, a fortiori si o > LZ, et c'est à ce moment-là qu'une réallocation peut s'avérer nécessaire dans le contexte.

26. À propos des unsigned

Quand vous ne voulez pas considérer le complément à deux d'une valeur binaire, il faut faire la précision unsigned au moment de sa déclaration. Par exemple un élément du type char est un octet. Par défaut il sera considéré comme signé (si toutefois vous considérez ce char comme un entier ce qui est toujours possible) si vous ne faites aucune précision c'est-à-dire qu'on ira de -128 à +127. Si vous ne voulez pas de ce signe il faut le préciser par le mot clé unsigned, par exemple :

unsigned char C;

C est donc un unsigned de type char (octet unique), on ira donc de 0 à 255 puisque le signe est ignoré.

Il en va de même pour les int (sur 2 octets) et les long int (sur 4 octets). Par exemple, dans un alinéa précédent, vous écrivions PointeurStruc[i]Car pour accéder au caractère de la ième structure, il est évident que i est un unsigned, il faut donc le déclarer ainsi :

unsigned int i;

Ainsi on ira sur 16 bits de 0 à 65535 (sinon, si cette précision n'avait pas été faite, le nombre aurait été considéré comme codifié en complément à 2 et l'on irait de -32768 à +32767). Si votre zone mémoire contient un nombre de structures inférieur ou égal à 32767, ça marcherait quand même puisque dans ce cas le nombre serait toujours positif même en considérant le complément à deux mais à partir de 32768 et en l'absence de la précision unsigned, le programme considérerait que i est négatif, vous pointeriez donc n'importe où dans la mémoire. Le mieux est donc de toujours préciser unsigned et même, pour les zones mémoire dont vous ignorez la longueur mais qui peuvent être assez grande, d'utiliser un unsigned long int, le pointeur d'éléments est alors codifié sur 4 octets donc sur 32 bits, vous êtes ainsi sûr de pointer toujours correctement la mémoire sans surprise par une possibilité de nombres négatifs.

27. À propos du signe * (étoile)

Ce signe s'utilise pour exprimer un pointeur qui pointe un élément d'un certain type, par exemple

char * P;

Cette notation signifie que P est un pointeur sur un élément de type caractère, on peut aussi considérer que P pointe le premier caractère d'une chaîne de caractères ou encore le premier octet d'une série d'octets, c'est le contexte du programme qui le dira. De façon assez curieuse dans cette notation, l'étoile "appartient" plus au pointeur P qu'au type char, c'est une convention arbitraire même si cela peut paraître inattendu. Cela se voit quand vous avez à déclarer deux pointeurs P1 et P2, il faut écrire char *P1, *P2; où l'étoile a dû être répétée car la notation char *P1, P2; déclarerait non pas deux pointeurs mais un pointeur P1 et un caractère P2. Avec la première déclaration, vous pourriez écrire dans le cours du programme P1 = P2 ce qui signifie que le pointeur P1 prend la position du pointeur P2 mais cette possibilité serait interdite avec la deuxième déclaration car alors le compilateur vous dira qu'il ne peut convertir un char* en char.

En revanche, quand vous déclarez le prototype d'une fonction, le type char* se met à exister. Imaginons une fonction ESSAI qui renvoie un entier et qui a pour argument un pointeur sur octets ou sur caractères. Le prototype de cette fonction se déclarera ainsi int ESSAI(char*); où l'étoile "appartient" cette fois-ci au type char et non à une variable qui d'ailleurs n'est pas mentionnée. L'étoile appartient donc à la variable au moment de la déclaration d'un pointeur mais au type au moment de la déclaration d'un prototype.

Notez également la quasi-équivalence qu'il y a entre les notations * et [ ], étoile et crochets. L'exemple précédent pourrait tout aussi bien être déclaré par int ESSAI(char[ ]); il en est de même au moment de l'appel de la fonction, vous pouvez aussi bien écrire nombre_entier = ESSAI(char* P) que nombre_entier = ESSAI(char P[ ]); ces deux écritures sont équivalentes du point de vue du compilateur et du fonctionnement du programme. En revanche le compilateur refusera les crochets au moment de la déclaration de la variable, la notation par étoile est alors nécessaire par exemple

char *P;

28. Polices de caractères du source C++

Si vous voulez changer de police de caractères pour l'affichage de votre code, cliquez sur le bouton droit de la souris dans un fenêtre de code C puis choisissez l'option propriétés tout en bas du menu déroulant. Là, sélectionnez l'onglet affichage. C'est dans ce menu que vous décidez de votre police et de sa taille pour l'affichage du source. Il semble que la police proposée par défaut (courrier New taille 9) soit excellente pour du code C, les lignes sont ainsi assez longues, les italiques des commentaires entre les signes /* et */ (ou après le signe // pour un commentaire monoligne) sont très lisibles et les caractères gras des mots clés bien prononcés mais sans excès.

29. Recherche dans le source

La recherche d'une chaîne de caractères dans le source se fait via Ctrl F (contrôle F, Find). Après avoir saisi le mot recherché et validé par OK, le curseur se positionne sur la première occurrence trouvée et la fenêtre appelée par Ctrl F disparaît. Pour trouver l'occurrence suivante, faites F3 (et F3 à nouveau pour la suivante etc.). Si vous recherchez un mot qui se trouve à l'écran, positionnez le curseur dessus (ou juste avant) puis faites Ctrl F, le mot visé est déjà écrit dans la zone de recherche du formulaire, ce qui vous en épargne la saisie.

30. Les pointeurs de pointeurs

On utilise deux fois le signe * (étoile) pour déclarer un pointeur de pointeurs. Imaginons que vous vouliez réserver NZM zones mémoire à chaque fois de longueur variable parce leur longueur dépend du contexte informatique (e.g. chargement de fichiers) ou humain (e.g. entrée de données au clavier). NZM n'est pas connu à l'avance, il faut donc, une fois ce nombre connu réserver une zone qui contiendra NZM pointeurs. En premier lieu on déclare un pointeur de pointeurs PP ainsi :

if((PP=(char**)malloc(NZM*sizeof(char*)))==NULL)
{
Application->MessageBox("Erreur d'allocation mémoire",NULL,MB_OK);
return;
}
if((PP[i]=(char*)malloc(bloc))==NULL)
Application->MessageBox("Erreur d'allocation mémoire",NULL,MB_OK) ;

À ce stade, vous disposez que NZM pointeurs, PP[0], PP[1] etc. jusqu'à PP[NZM-1].

Pour chacun de ces pointeurs PP[i], vous allez devoir réserver une zone mémoire de bloc octets (bloc étant soit une constante définie par vous soit un nombre positif quelconque même assez grand). La fonction test ESSAI ci-dessous procède à ces déclarations et remplit arbitrairement la mémoire allouée puis fait une réallocation. Ce type de remplissage n'a d'autre but que de montrer que la zone mémoire est bien accessible.

void ESSAI(void)
{
const int NZM=500,bloc=200000;
char** PP;
int i,j;

// Petit message avant test
Application->MessageBox("AVANT test","TEST",MB_OK);

// réservation pour NZM pointeurs
if((PP=(char**)malloc(NZM*sizeof(char*)))==NULL)
{
Application->MessageBox("Erreur d'allocation mémoire",NULL,MB_OK);
return;
}

// Réservation pour les NZM pointeurs d'une zone de bloc octets
for(i=0;i!=NZM;i++)
if((PP[i]=(char*)malloc(bloc))==NULL)
{
Application->MessageBox("Erreur d'allocation mémoire",NULL,MB_OK) ;
return;
}

// remplissage arbitraire des NZM zones de bloc octets
for(i=0;i!=NZM;i++)for(j=0;j!=bloc;j++) PP[i][j]='a';

// Réallocation de toutes les zones du double de la première réservation (bloc*2)
for(i=0;i!=NZM;i++)
if((PP[i]=(char*)realloc(PP[i],bloc*2))==NULL)
{
Application->MessageBox("Erreur d'allocation mémoire",NULL,MB_OK) ;
return;
}

// remplissage arbitraire des NZM zones maintenant toutes doublées
for(i=0;i!=NZM;i++)for(j=0;j!=bloc*2;j++) PP[i][j]='a';

// Libération des NZM zones mémoire
for(i=0;i!=NZM;i++) free(PP[i]);
// et de la zone des NZM pointeurs
free(PP);

/*Petit message une fois le test terminé qui prouve que tout est correct
sinon il y aurait un retour suite à une allocation fautive */
Application->MessageBox("Tout est correct","TEST",MB_OK);
}// Fin de cette petite fonction de TEST MÉMOIRE.

Si l’augmentation d’une zone mémoire via la fonction realloc se fait à l’intérieur d’une fonction, il faut alors déclarer le pointeur de la zone mémoire en référence grâce à l’opérateur & et ce, parce que realloc peut modifier la position du pointeur. Il faut donc que le programme appelant récupère cette nouvelle valeur en cas de changement. C'est un des avantages du C++ à savoir l'utilisation de ce signe qui, à lui seul, exprime la référence. En C pur, la référence était plus difficile à exprimer. Si par exemple on voulait qu'un entier i soit du type référence dans une fonction C, il fallait prendre trois précautions :

  1. Déclarer que l'argument de la fonction est int* (pointeur sur un entier)
  2. appeler la fonction en utilisant &i comme argument.
  3. Affecter (*i) dans la fonction elle-même.

C++ a apporté une grande simplification à cette problématique puisque vous n'aurez qu'à déclarer non plus i mais &i, l'opérateur & à lui seul exprime la référence. Notez qu'il y a donc en C++ un dual relativement à la référence puisque l'ancienne syntaxe est tout à fait possible (C++ intégrant la totalité des notations C).

Imaginons un programme qui déclare une zone mémoire de bloc octets, bloc étant une constante arbitraire, et qui va écrire très longtemps dans cette zone mémoire et l’allonger dès que nécessaire. On initialise un entier LZ (longueur de la zone) à bloc qui est bien la première longueur de cette zone. On écrit dans cette zone mémoire réservée en la pointant par incrémentation d’un offset. Dès que cet offset est égal à LZ, il pointe le premier octet hors zone, on décide alors d’augmenter la zone de bloc octets par la fonction booléenne ALLOC qui renvoie « true » si l’allocation a réussi, « false » en cas d’échec et qui a deux arguments, le pointeur envoyé par référence avec l’opérateur & et la nouvelle longueur de réallocation.

// prototype de la fonction ALLOC
bool ALLOC(char*&,long int);

void ESSAI(void)
{
const int bloc=1000;
char *Pointeur =(char*)malloc(bloc);
long int LZ=bloc;
int i=0,ok;
Application->MessageBox("AVANT test","TEST",MB_OK);
do
{
/* si i pointe le premier octet hors zone
on augmente cette zone de bloc octets*/
if(i==LZ)
{
// LZ augmente de bloc octets
if(!ALLOC(Pointeur,LZ+=bloc))
{
Application->MessageBox("Erreur d'allocation ","TEST",MB_OK);
// fin de la boucle si erreur
ok=0;
}
}
// i pointe bien dans la zone avant son incrémentation, remplissage arbitraire de la case pointée
Pointeur[i++]='a';

/*fin de la boucle si i est arbitrairement grand
cette instruction signifie, si i n’est pas égal à 500000 alors
ok=1 (non nul = vrai, true) sinon ok=0 (nul = faux, false). */
ok=(i!=500000);
}
while(ok) ;

// Libération de la mémoire allouée
free(Pointeur);
Application->MessageBox("APRES test","TEST",MB_OK);
}
//--------------------------------------
bool ALLOC(char *&P,long int L)
{
return (P=(char*) realloc(P,L))!=NULL;
}
//---------------------------------------

Remarquez bien sûr la concision de la syntaxe. P pointe la nouvelle adresse de la réallocation et est en même temps comparé à !=NULL. Si c’est vrai (true), le pointeur ne pointe pas NULL après le realloc ce qui signifie que l’allocation a réussi, sinon, si P pointe NULL, l’allocation a échoué. On peut donc considérer que la fonction booléenne ALLOC répond à la question : la réallocation mémoire a-t-elle réussi ? true oui, false non.

Si cette précaution n’avait pas été prise à savoir celle de déclarer le pointeur par référence avec la syntaxe char *&P, la nouvelle position en cas de changement de position mémoire du realloc ne serait pas transmise au programme appelant, par conséquent ça ne pourrait jamais marcher car le pointeur pointerait n’importe où après un changement de position de cette zone. Ça ne marcherait qu'aussi longtemps que la réallocation se situerait par hasard au même endroit. Faites-en l’expérience en supprimant le signe & aussi bien dans le prototype que dans la fonction. Durant l’exécution, vous aurez très vite le message « une erreur d’application s’est produite, violation d’accès etc. » et ce, parce que la zone mémoire allouée a changé d’adresse à un moment donné alors que Pointeur pointe toujours l’ancienne adresse, laquelle n’est plus d’aucune utilité. L'instruction d'écriture en mémoire Pointeur[i++]='a' est alors fautive car on pointe n'importe où.

Dans cet exemple, la variable Pointeur du programme appelant est transmise à P de la fonction par référence c’est-à-dire par l’adresse mémoire de ce pointeur. Comme ce contenu de la mémoire est modifié par le realloc, ce nouveau contenu est comme retransmis à la variable Pointeur du programme appelant.

Il est toujours très important de savoir si une variable, argument d’une fonction, doit être retournée ou non à l’appelant. Si non, on n’écrit que le nom de la variable, si oui, on préfixe le nom de cette variable par l’opérateur &. Dans le premier cas, la fonction travaille avec une copie de la variable, son contenu à l’issue de l’exécution n’est pas retransmis à l’appelant. Dans le second cas, on donne à la fonction non pas la variable mais son adresse mémoire (c’est la signification de l’opérateur &). Si donc la fonction écrit dans cette mémoire, ce contenu sera logiquement répercuté par la variable de l’appelant qui pointe précisément cette mémoire.


par Gilles Louise

Septembre 2000





Hit-Parade

Vos questions techniques :Newsgroups - Publiez vos avis, liens, cours & articles : Publication
et rejoignez-nous dans l'équipe de rédaction de l'Association d'entraide des développeurs Francophones
Copyright 2000 www.developpez.com