III. Construire et utiliser les objets▲
Ce chapitre va introduire suffisamment de syntaxe C++ et de concepts de programmation pour vous permettre d'écrire et de lancer des programmes simples orientés objet. Dans le chapitre suivant nous verrons en détail la syntaxe de base du C et du C++.
En lisant ce chapitre en premier, vous acquerrez une idée générale sur ce qu'est la programmation avec les objets en C++, et vous découvrirez également quelques-unes des raisons de l'enthousiasme entourant ce langage. Cela devrait être suffisant pour que vous puissiez aborder le chapitre 3, qui est un peu plus consistant du fait qu'il contient beaucoup de détails sur le langage C.
Le type de données personnalisé, ou classe, est ce qui distingue le C++ des langages de programmation procéduraux traditionnels. Une classe est un nouveau type de données que vous ou un tiers créez pour résoudre un type particulier de problème. Une fois qu'une classe est créée, n'importe qui peut l'employer sans connaitre les détails de son fonctionnement, ou comment les classes sont construites. Ce chapitre traite des classes comme s'il s'agissait simplement d'autres types de données intégrés, disponibles à l'usage dans les programmes.
Les classes qu'un tiers a créées sont typiquement empaquetées dans une bibliothèque. Ce chapitre utilise plusieurs des bibliothèques de classes disponibles avec toutes les implémentations C++. Une bibliothèque standard particulièrement importante, iostreams, vous permet (entre autres choses) de lire dans des fichiers et au clavier, et d'écrire dans des fichiers ou sur l'affichage. Vous verrez également la très utile classe string, et le conteneur vector de la bibliothèque standard du C++. Vers la fin de ce chapitre, vous verrez à quel point il est facile d'utiliser une bibliothèque prédéfinie de classes.
Afin de créer votre premier programme, vous devez comprendre les outils utilisés pour construire des applications.
III-A. Le processus de traduction du langage▲
Tous les langages informatiques sont traduits à partir de quelque chose d'aisé à comprendre pour un humain (le code source) en quelque chose qui peut être exécuté sur un ordinateur (les instructions machine). Traditionnellement, les traducteurs se scindent en deux classes : les interpréteurs et les compilateurs.
III-A-1. Les interpréteurs▲
Un interpréteur traduit le code source en activités (lesquelles peuvent être constituées de groupes d'instructions machine) et exécute immédiatement ces activités. Le BASIC, par exemple, a été un langage interprété très populaire. Traditionnellement, les interpréteurs BASIC traduisent et exécutent une ligne à la fois, puis oublient que la ligne a été traduite. Cela fait qu'ils sont lents, puisqu'ils doivent retraduire tout le code répété. Le BASIC a été également compilé, pour la rapidité. Des interpréteurs plus modernes, comme ceux du langage Python, traduisent le programme entier dans un langage intermédiaire qui est alors exécuté par un interpréteur beaucoup plus rapide (23)
Les interpréteurs ont beaucoup d'avantages. La transition entre l'écriture du code et son exécution est presque immédiate, et le code source est toujours disponible ainsi l'interpréteur peut être beaucoup plus spécifique quand une erreur se produit. Les avantages souvent cités pour les interpréteurs sont la facilité d'interaction et la vitesse de développement (mais pas nécessairement d'exécution) des programmes.
Les langages interprétés ont souvent de graves limitations lors de la réalisation de grands projets (Python semble être une exception en cela). L'interpréteur (ou une version réduite) doit toujours être en mémoire pour exécuter le code, et même l'interpréteur le plus rapide introduira d'inacceptables restrictions de vitesse. La plupart des interpréteurs requièrent que la totalité du code source soit passée à l'interpréteur en une fois. Non seulement cela introduit une limitation spatiale, mais cela entraine également plus de bogues difficiles à résoudre si le langage ne fournit pas de facilités pour localiser les effets des différentes parties du code.
III-A-2. Les compilateurs▲
Un compilateur traduit le code source directement en langage assembleur ou en instructions machine. L'éventuel produit fini est un fichier ou des fichiers contenant le code machine. C'est un processus complexe, nécessitant généralement plusieurs étapes. La transition entre l'écriture du code et son exécution est significativement plus longue avec un compilateur.
En fonction de la perspicacité du créateur du compilateur, les programmes générés par un compilateur tendent à utiliser beaucoup moins d'espace pour s'exécuter, et s'exécutent beaucoup plus rapidement. Bien que la taille et la vitesse soient probablement les raisons les plus citées d'utilisation des compilateurs, dans nombre de situations ce ne sont pas les raisons les plus importantes. Certains langages (comme le C) sont conçus pour autoriser la compilation séparée de certaines parties du programme. Ces parties sont éventuellement combinées en un programme exécutable final par un outil appelé éditeur de liens( linker). Ce processus est appelé compilation séparée.
La compilation séparée a moult avantages. Un programme qui, en prenant tout en une fois, excède les limites du compilateur ou de l'environnement de compilation peut être compilé par morceaux. Les programmes peuvent être construits et testés morceau par morceau. Une fois qu'un morceau fonctionne, il peut être sauvegardé et traité comme un module. Les collections de morceaux testés et validés peuvent être combinées en bibliothèques pour être utilisées par d'autres programmeurs. Pendant que chaque morceau est créé, la complexité des autres morceaux est cachée. Tous ces dispositifs permettent la création de programmes volumineux (24).
Les dispositifs de débogage des compilateurs se sont sensiblement améliorés ces derniers temps. Les compilateurs de première génération généraient seulement du code machine, et le programmeur insérait des rapports d'impression pour voir ce qui se passait. Ce n'est pas toujours efficace. Les compilateurs modernes peuvent insérer des informations à propos du code source dans le programme exécutable. Ces informations sont utilisées par de puissants débogueurs de haut niveau pour montrer exactement ce qui se passe en traçant la progression dans le code source.
Quelques compilateurs abordent le problème de la vitesse de compilation en exécutant une compilation en mémoire. La plupart des compilateurs fonctionnent avec des fichiers, les lisant et les écrivant à chaque étape du processus de compilation. Les compilateurs résidents gardent le programme de compilation dans la RAM. Pour les petits programmes, cela peut sembler aussi réactif qu'un interpréteur.
III-A-3. Le processus de compilation▲
Pour programmer en C et C++ vous avez besoin de comprendre les étapes et les outils du processus de compilation. Certains langages (le C et le C++, en particulier) commencent la compilation en exécutant un préprocesseur sur le code source. Le préprocesseur est un programme simple qui remplace les modèles dans le code source par d'autres modèles que le programmeur a définis (en utilisant les directives du préprocesseur). Les directives du préprocesseur sont utilisées pour limiter les frappes et augmenter la lisibilité du code. (Plus loin dans le livre vous apprendrez comment la conception du C++ est faite pour décourager une grande partie de l'utilisation du préprocesseur, puisqu'elle peut causer les bogues subtils.) Le code prétraité est souvent écrit dans un fichier intermédiaire.
Les compilateurs travaillent souvent en deux temps. La première passe décompose le code prétraité. Le compilateur sépare le code source en petites unités et l'organise dans une structure appelée arbre. Dans l'expression « A + B » les éléments « A », « + », et « B » sont des feuilles de l'arbre de décomposition.
Un optimisateur global est parfois utilisé entre la première et la deuxième passe pour produire un code plus petit et plus rapide.
Dans la seconde passe, le générateur de code parcourt l'arbre de décomposition et génère soit du code en langage assembleur soit du code machine pour les nœuds de l'arbre. Si le générateur de code produit du code assembleur, l'assembleur doit être exécuté. Le résultat final dans les deux cas est un module objet (un fichier dont l'extension est typiquement .o ou .obj). Un optimiseur à lucarne( peephole optimizer) est parfois utilisé dans la deuxième passe pour rechercher des morceaux de code contenant des instructions de langage assembleur redondantes.
L'utilisation du mot « objet » pour décrire les morceaux du code machine est un artefact regrettable. Le mot fût employé avant que la programmation orientée objet ne se soit généralisée. « Objet » est utilisé dans le même sens que « but » lorsqu'on parle de compilation, alors qu'en programmation orientée objet cela désigne « une chose avec une frontière ».
L' éditeur de liens combine une liste de modules objets en un programme exécutable qui peut être chargé et lancé par le système d'exploitation. Quand une fonction d'un module objet fait référence à une fonction ou une variable d'un autre module objet, l'éditeur de liens résout ces références ; cela assure que toutes les fonctions et les données externes dont vous déclarez l'existence pendant la compilation existent. L'éditeur de liens ajoute également un module objet spécial pour accomplir les activités du démarrage.
L'éditeur de liens peut faire des recherches dans des fichiers spéciaux appelés bibliothèques afin de résoudre toutes les références. Une bibliothèque contient une collection de modules objets dans un fichier unique. Une bibliothèque est créée et maintenue par un programme appelé bibliothécaire( librarian).
Vérification statique du type
Le compilateur exécute la vérification de type pendant la première passe. La vérification de type teste l'utilisation appropriée des arguments dans les fonctions et empêche beaucoup de sortes d'erreurs de programmation. Puisque la vérification de type se produit pendant la compilation et non à l'exécution du programme, elle est appelée vérification statique du type.
Certains langages orientés objet (notamment Java) font des vérifications de type pendant l'exécution ( vérification dynamique du type). Si elle est combinée à la vérification statique du type, la vérification dynamique du type est plus puissante que la vérification statique seule. Cependant, cela ajoute également un cout supplémentaire à l'exécution du programme.
Le C++ utilise la vérification statique de type, car le langage ne peut assumer aucun support d'exécution particulier en cas de mauvaises opérations. La vérification statique du type notifie au programmeur les mauvaises utilisations de types pendant la compilation, et ainsi maximise la vitesse d'exécution. En apprenant le C++, vous verrez que la plupart des décisions de conception du langage favorisent ce genre de rapidité, programmation axée sur la production pour laquelle le langage C est célèbre.
Vous pouvez désactiver la vérification statique du type en C++. Vous pouvez également mettre en œuvre votre propre vérification dynamique de type - vous avez seulement besoin d'écrire le code.
III-B. Outils de compilation séparée▲
La compilation séparée est particulièrement importante dans le développement de grands projets. En C et C++, un programme peut être créé par petits morceaux maniables, testés indépendamment. L'outil primordial pour séparer un programme en morceaux est la capacité de créer des sous-routines ou des sous-programmes nommés. En C et C++, un sous-programme est appelé fonction, et les fonctions sont les parties du code qui peuvent être placées dans différents fichiers, permettant une compilation séparée. Autrement dit, la fonction est l'unité atomique du code, puisque vous ne pouvez pas avoir une partie d'une fonction dans un fichier et une autre partie dans un fichier différent ; la fonction entière doit être placée dans un fichier unique (mais les fichiers peuvent contenir plus d'une fonction).
Quand vous appelez une fonction, vous lui passez typiquement des arguments, qui sont des valeurs que la fonction utilise pendant son exécution. Quand une fonction se termine, vous récupérez typiquement une valeur de retour, une valeur que la fonction vous retourne comme résultat. Il est aussi possible d'écrire des fonctions qui ne prennent aucun argument et qui ne retournent aucune valeur.
Pour créer un programme avec plusieurs fichiers, les fonctions d'un fichier doivent accéder à des fonctions et à des données d'autres fichiers. Lorsqu'il compile un fichier, le compilateur C ou C++ doit connaitre les fonctions et données des autres fichiers, en particulier leurs noms et leur emploi correct. Le compilateur s'assure que les fonctions et les données sont employées correctement. Ce processus « d'indiquer au compilateur » les noms des fonctions et des données externes et ce à quoi elles ressemblent est appelé déclaration. Une fois que vous avez déclaré une fonction ou variable, le compilateur sait comment vérifier le code pour s'assurer qu'elle est employée correctement.
III-B-1. Déclarations vs. définitions▲
Il est important de comprendre la différence entre déclarations et définitions, parce que ces termes seront utilisés précisément partout dans le livre. Par essence, tous les programmes C et C++ exigent des déclarations. Avant que vous puissiez écrire votre premier programme, vous devez comprendre la manière convenable d'écrire une déclaration.
Une déclaration introduit un nom - un identifiant - pour le compilateur. Elle indique au compilateur « Cette fonction ou cette variable existe quelque part, et voici à quoi elle devrait ressembler. » Une définition, d'un autre côté, dit : « Faire cette variable ici » ou « Faire cette fonction ici ». Elle alloue de l'espace pour le nom. Ce principe s'applique aux variables comme aux fonctions ; dans tous les cas, le compilateur alloue de l'espace au moment de la définition. Pour une variable, le compilateur détermine sa taille et entraine la réservation de l'espace en mémoire pour contenir les données de cette variable. Pour une fonction, le compilateur génère le code, qui finit par occuper de l'espace en mémoire.
Vous pouvez déclarer une variable ou une fonction dans beaucoup d'endroits différents, mais il doit y avoir seulement une définition en C et C++ (ceci s'appelle parfois l'ODR : one-definition rule). Quand l'éditeur de liens unit tous les modules objets, il se plaindra généralement s'il trouve plus d'une définition pour la même fonction ou variable.
Une définition peut également être une déclaration. Si le compilateur n'a pas vu le nom x avant, et que vous définissez int x;, le compilateur voit le nom comme une déclaration et alloue son espace de stockage en une seule fois.
Syntaxe de la déclaration de fonction
Une déclaration de fonction en C et C++ donne le nom de fonction, le type des paramètres passés à la fonction, et la valeur de retour de la fonction. Par exemple, voici une déclaration pour une fonction appelée func1( ) qui prend deux arguments entiers (les nombres entiers sont annoncés en C/C++ avec le mot-clé int) et retourne un entier :
int
func1(int
,int
);
Le premier mot-clé que vous voyez est la valeur de retour elle-même int. Les paramètres sont entourés de parenthèses après le nom de la fonction, dans l'ordre dans lequel ils sont utilisés. Le point-virgule indique la fin d'une instruction; dans l'exemple, il indique au compilateur « c'est tout - il n'y a pas de définition de fonction ici ! »
Les déclarations du C et du C++ essaient d'imiter la forme d'utilisation de l'élément. Par exemple, si a est un autre entier la fonction ci-dessus peut être utilisée de cette façon :
a =
func1(2
,3
);
Puisque func1( ) retourne un entier, le compilateur C ou C++ vérifiera l'utilisation de func1( ) pour s'assurer que a peut accepter la valeur de retour de la fonction et que les arguments sont appropriés.
Les arguments dans les déclarations de fonction peuvent avoir des noms. Le compilateur ignore les noms, mais ils peuvent être utiles en tant que dispositifs mnémoniques pour l'utilisateur. Par exemple, nous pouvons déclarer func1( ) d'une façon différente qui a la même signification :
int
func1(int
taille, int
largeur);
Un piège
Il y a une différence significative entre le C et le C++ pour les fonctions dont la liste d'arguments est vide. En C, la déclaration :
int
func2();
signifie « une fonction avec n'importe quels nombre et type d'arguments. » Cela empêche la vérification du type, alors qu'en C++ cela signifie « une fonction sans argument. »
Définitions de fonction
Les définitions de fonction ressemblent aux déclarations de fonction sauf qu'elles ont un corps. Un corps est un ensemble d'instructions entouré d'accolades. Les accolades annoncent le début et la fin d'un bloc de code. Pour donner une définition à func1( ) qui soit un corps vide (un corps ne contenant aucun code), écrivez :
int
func1(int
taille, int
largeur) {
}
Notez que dans la définition de fonction, les accolades remplacent le point-virgule. Puisque les accolades entourent une instruction ou un groupe d'instructions, vous n'avez pas besoin d'un point-virgule. Notez aussi que les paramètres dans la définition de fonction doivent avoir des noms si vous voulez les utiliser dans le corps de la fonction (comme ils ne sont jamais utilisés dans l'exemple, c'est optionnel).
Syntaxe de la déclaration de variable
La signification attribuée à l'expression « déclaration de variable » a historiquement été déroutante et contradictoire, et il est important que vous compreniez la définition correcte, ainsi vous pouvez lire le code correctement. Une déclaration de variable indique au compilateur à quoi une variable ressemble. Elle dit, « Je sais que tu n'as pas vu ce nom avant, mais je promets qu'il existe quelque part, et que c'est une variable du type X. »
Dans une déclaration de fonction, vous donnez un type (la valeur de retour), le nom de la fonction, la liste des arguments, et un point-virgule. C'est suffisant pour que le compilateur comprenne que c'est une déclaration et ce à quoi la fonction devrait ressembler. De la même manière, une déclaration de variable pourrait être un type suivi d'un nom. Par exemple :
int
a;
pourrait déclarer la variable a comme un entier, en utilisant la logique ci-dessus. Voilà le conflit : il y a assez d'information dans le code ci-dessus pour que le compilateur crée l'espace pour un entier appelé a, et c'est ce qui se produit. Pour résoudre ce dilemme, un mot-clé était nécessaire en C et C++ pour dire « ceci est seulement une déclaration ; elle a été définie ailleurs. » Le mot-clé est extern. Il peut signifier que la définition est externe au fichier, ou que la définition a lieu plus tard dans le fichier.
Déclarer une variable sans la définir signifie utiliser le mot-clé extern avant une description de la variable, comme ceci :
extern
int
a;
extern peut aussi s'appliquer aux déclarations de fonctions. Pour func1( ), ça ressemble à :
extern
int
func1(int
taille, int
largeur);
Cette instruction est équivalente aux précédentes déclarations de func1( ). Puisqu'il n'y a pas de corps de fonction, le compilateur doit la traiter comme une déclaration de fonction plutôt qu'une définition de fonction. Le mot-clé extern est donc superflu et optionnel pour les déclarations de fonctions. Il est vraisemblablement regrettable que les concepteurs du C n'aient pas requis l'utilisation d' extern pour les déclarations de fonctions ; cela aurait été plus cohérent et moins déroutant (mais cela aurait nécessité plus de frappes, ce qui explique probablement la décision).
Voici quelques exemples de déclarations :
//: C02:Declare.cpp
// Exemples de déclaration & définition
extern
int
i; // Déclaration sans définition
extern
float
f(float
); // Déclaration de fonction
float
b; // Déclaration & définition
float
f(float
a) {
// Définition
return
a +
1.0
;
}
int
i; // Définition
int
h(int
x) {
// Déclaration & définition
return
x +
1
;
}
int
main() {
b =
1.0
;
i =
2
;
f(b);
h(i);
}
///
:~
Dans les déclarations de fonctions, les identifiants d'argument sont optionnels. Dans les définitions, ils sont requis (les identifiants sont requis uniquement en C, pas en C++).
Inclusion d'en-têtes
La plupart des bibliothèques contiennent un nombre significatif de fonctions et de variables. Pour économiser le travail et assurer la cohérence quand sont faites des déclarations externes pour ces éléments, le C et le C++ utilisent un dispositif appelé le fichier d'en-tête. Un fichier d'en-tête est un fichier contenant les déclarations externes d'une bibliothèque; il a par convention une extension de nom de fichier « h », comme headerfile.h. (Vous pourrez également voir certains codes plus anciens utilisant des extensions différentes, comme .hxx ou .hpp, mais cela devient rare.)
Le programmeur qui crée la bibliothèque fournit le fichier d'en-tête. Pour déclarer les fonctions et variables externes de la bibliothèque, l'utilisateur inclut simplement le fichier d'en-tête. Pour inclure un fichier d'en-tête, utilisez la directive du préprocesseur #include. Elle demande au préprocesseur d'ouvrir le fichier d'en-tête cité et d'insérer son contenu là où l'instruction #include apparaît. Un #include peut citer un fichier de deux façons : entre équerres ( < >) ou entre guillemets doubles.
Les noms de fichiers entre équerres, comme :
#include
<entete>
font rechercher le fichier par le préprocesseur d'une manière propre à votre implémentation, mais typiquement il y a une sorte de « chemin de recherche des inclusions » que vous spécifiez dans votre environnement ou en ligne de commandes du compilateur. Le mécanisme de définition du chemin de recherche varie selon les machines, les systèmes d'exploitation, et les implémentations du C++, et peut nécessiter quelque investigation de votre part.
Les noms de fichiers entre guillemets doubles, comme :
#include
"local.h"
indiquent au préprocesseur de chercher le fichier (selon les spécifications) « d'une manière définie par l'implémentation. » Ceci signifie typiquement de chercher le fichier relativement au répertoire courant. Si le fichier n'est pas trouvé, alors la directive d'inclusion est retraitée comme s'il s'agissait d'équerres et non de guillemets.
Pour inclure le fichier d'en-tête iostream, vous écrivez :
#include
<iostream>
Le préprocesseur trouvera le fichier d'en-tête iostream (souvent dans un sous-répertoire appelé « include ») et l'insérera.
Format d'inclusion du Standard C++
Alors que le C++ évoluait, les différents fournisseurs de compilateurs ont choisi différentes extensions pour les noms de fichiers. En outre, les divers systèmes d'exploitation ont différentes contraintes sur les noms de fichiers, en particulier la taille du nom. Ces sujets ont entrainé des problèmes de portabilité du code source. Pour arrondir les angles, le standard utilise un format qui permet des noms de fichiers plus longs que les huit caractères notoires et élimine l'extension. Par exemple, au lieu du vieux style d'inclusion iostream.h, qui ressemble à :
#include
<iostream.h>
vous pouvez maintenant écrire :
#include
<iostream>
L'interprète peut mettre en application les instructions d'inclusion d'une façon qui convient aux besoins de ces compilateur et système d'exploitation particuliers, si nécessaire en tronquant le nom et en ajoutant une extension. Bien sûr, vous pouvez aussi copier les en-têtes donnés par le fournisseur de votre compilateur dans ceux sans extensions si vous voulez utiliser ce style avant que le fournisseur ne le supporte.
Les bibliothèques héritées du C sont toujours disponibles avec l'extension traditionnelle « .h ». Cependant, vous pouvez aussi les utiliser avec le style d'inclusion plus moderne du C++ en ajoutant un « c » devant le nom. Ainsi :
#include
<stdio.h>
;
#include
<stdlib.h>
devient :
#include
<cstdio>
#include
<cstdlib>
Et ainsi de suite, pour tous les en-têtes standards du C. Cela apporte une distinction agréable au lecteur, indiquant quand vous employez des bibliothèques C et non C++.
L'effet du nouveau format d'inclusion n'est pas identique à l'ancien : utiliser le .h vous donne la version plus ancienne, sans modèles, et omettre le .h vous donne la nouvelle version, avec modèles. Vous aurez généralement des problèmes si vous essayez d'entremêler les deux formes dans un même programme.
III-B-2. Édition des liens▲
L'éditeur de liens rassemble les modules objets (qui utilisent souvent des extensions de nom de fichier comme .o ou .obj), générés par le compilateur, dans un programme exécutable que le système d'exploitation peut charger et démarrer. C'est la dernière phase du processus de compilation.
Les caractéristiques de l'éditeur de liens changent d'un système à l'autre. Généralement, vous donnez simplement à l'éditeur de liens les noms des modules objets et des bibliothèques que vous voulez lier ensemble, et le nom de l'exécutable, et il va fonctionner. Certains systèmes nécessitent d'appeler l'éditeur de liens vous-même. Avec la plupart des éditeurs C++, vous appelez l'éditeur de liens à travers le compilateur C++. Dans beaucoup de situations, l'éditeur de liens est appelé sans que vous le voyez.
Certains éditeurs de liens plus anciens ne chercheront pas les fichiers objet et les bibliothèques plus d'une fois, et ils cherchent dans la liste que vous leur donnez de gauche à droite. Ceci signifie que l'ordre des fichiers objet et des bibliothèques peut être important. Si vous avez un problème mystérieux qui n'apparaît pas avant l'édition des liens, une cause possible est l'ordre dans lequel les fichiers sont donnés à l'éditeur de liens.
III-B-3. Utilisation des bibliothèques▲
Maintenant que vous connaissez la terminologie de base, vous pouvez comprendre comment utiliser une bibliothèque :
- Incluez le fichier d'en-tête de la bibliothèque.
- Utilisez les fonctions et variables de la bibliothèque.
- Liez la bibliothèque dans le programme exécutable.
Ces étapes s'appliquent également quand les modules objets ne sont pas combinés dans une bibliothèque. Inclure un fichier d'en-tête et lier les modules objets sont les étapes de base de la compilation séparée en C et C++.
Comment l'éditeur de liens cherche-t-il une bibliothèque ?
Quand vous faites une référence externe à une fonction ou variable en C ou C++, l'éditeur de liens, lorsqu'il rencontre cette référence, peut faire deux choses. S'il n'a pas encore rencontré la définition de la fonction ou variable, il ajoute l'identifiant à sa liste des « références non résolues ». Si l'éditeur de liens a déjà rencontré la définition, la référence est résolue.
Si l'éditeur de liens ne peut pas trouver la définition dans la liste des modules objets, il cherche dans les bibliothèques. Les bibliothèques ont une sorte d'index de telle sorte que l'éditeur de liens n'a pas besoin de parcourir tous les modules objets de la bibliothèque - il regarde juste l'index. Quand l'éditeur de liens trouve une définition dans une bibliothèque, le module objet complet, et non seulement la définition de fonction, est lié dans le programme exécutable. Notez que la bibliothèque n'est pas liée dans son ensemble, seulement le module objet de la bibliothèque qui contient la définition que vous voulez (sinon les programmes seraient inutilement volumineux). Si vous voulez minimiser la taille du programme exécutable, vous pouvez imaginer mettre une seule fonction par fichier du code source quand vous construisez vos propres bibliothèques. Cela nécessite plus de rédaction. (25)Mais ça peut être utile aux utilisateurs.
Comme l'éditeur de liens cherche les fichiers dans l'ordre dans lequel vous les listez, vous pouvez préempter l'utilisation d'une fonction de bibliothèque en insérant un fichier avec votre propre fonction, utilisant le même nom de fonction, dans la liste avant l'apparition du nom de la bibliothèque. Puisque l'éditeur de liens résoudra toutes les références à cette fonction en utilisant votre fonction avant de chercher dans la bibliothèque, votre fonction sera utilisée à la place de la fonction de la bibliothèque. Notez que cela peut aussi être un bogue, et que les espaces de nommage du C++ préviennent ce genre de choses.
Ajouts cachés
Quand un programme exécutable C ou C++ est créé, certains éléments sont secrètement liés. L'un d'eux est le module de démarrage, qui contient les routines d'initialisation qui doivent être lancées à chaque fois qu'un programme C ou C++ commence à s'exécuter. Ces routines mettent en place la pile et initialisent certaines variables du programme.
L'éditeur de liens cherche toujours dans la bibliothèque standard les versions compilées de toutes les fonctions « standard » appelées dans le programme. Puisque la librairie standard est toujours recherchée, vous pouvez utiliser tout ce qu'elle contient en incluant simplement le fichier d'en-tête approprié dans votre programme ; vous n'avez pas à indiquer de rechercher dans la bibliothèque standard. Les fonctions iostream, par exemple, sont dans la bibliothèque Standard du C++. Pour les utiliser, vous incluez juste le fichier d'en-tête <iostream>.
Si vous utilisez une bibliothèque complémentaire, vous devez explicitement ajouter le nom de la bibliothèque à la liste des fichiers donnés à l'éditeur de liens.
Utilisation des bibliothèques C
Bien que vous écriviez du code en C++, on ne vous empêchera jamais d'utiliser des fonctions d'une bibliothèque C. En fait, la bibliothèque C complète est incluse par défaut dans le Standard C++. Une quantité énorme de travail a été effectuée pour vous dans ces fonctions, elles peuvent donc vous faire gagner beaucoup de temps.
Ce livre utilisera les fonctions de la bibliothèque Standard C++ (et donc aussi le standard C) par commodité, mais seules les fonctions de la bibliothèque standard seront utilisées, pour assurer la portabilité des programmes. Dans les quelques cas pour lesquels des fonctions de bibliothèques qui ne sont pas dans le standard C++ doivent être utilisées, nous ferons tous les efforts possibles pour utiliser des fonctions conformes POSIX. POSIX est une norme basée sur un effort de standardisation d'Unix qui inclut des fonctions qui dépassent la portée de la bibliothèque C++. Vous pouvez généralement espérer trouver des fonctions POSIX sur les plateformes Unix (en particulier, Linux), et souvent sous DOS/Windows. Par exemple, si vous utilisez le multithreading, vous partez d'autant mieux que vous utilisez la bibliothèque de thread POSIX parce que votre code sera alors plus facile à comprendre, porter et maintenir (et la bibliothèque de thread POSIX utilisera généralement simplement les possibilités de thread sous-jacentes du système d'exploitation, si elles existent).
III-C. Votre premier programme C++▲
Vous en savez maintenant presque suffisamment sur les bases pour créer et compiler un programme. Le programme utilisera les classes iostream (flux d'entrée/sortie) du Standard C++. Elles lisent et écrivent dans des fichiers et l'entrée et la sortie « standard » (qui normalement reposent sur la console, mais peuvent être redirigés vers des fichiers ou des périphériques). Dans ce programme simple, un objet flux sera utilisé pour écrire un message à l'écran.
III-C-1. Utilisation de la classe iostream▲
Pour déclarer les fonctions et données externes de la classe iostream, incluez le fichier d'en-tête avec l'instruction
#include
<iostream>
Le premier programme utilise le concept de sortie standard, qui signifie « un endroit universel pour envoyer la sortie ». Vous verrez d'autres exemples utilisant la sortie standard de différentes façons, mais ici elle ira simplement sur la console. La bibliothèque iostream définit automatiquement une variable (un objet) appelée cout qui accepte toutes les données liées à la sortie standard.
Pour envoyer des données à la sortie standard, vous utilisez l'opérateur <<. Les programmeurs C connaissent cet opérateur comme celui du « décalage des bits à gauche », ce qui sera décrit dans le prochain chapitre. Ça suffit pour dire qu'un décalage des bits à gauche n'a rien à voir avec la sortie. Cependant, le C++ permet la surcharge des opérateurs. Quand vous surchargez un opérateur, vous donnez un nouveau sens à cet opérateur quand il est utilisé avec un objet d'un type donné. Avec les objets iostream, l'opérateur << signifie « envoyer à ». Par exemple :
cout <<
"salut !"
;
envoie la chaine de caractères « salut ! » à l'objet appelé cout(qui est le raccourci de « console output » - sortie de la console)
Ca fait assez de surcharge d'opérateur pour commencer. Le chapitre 12 couvre la surcharge des opérateurs en détail.
III-C-2. Espaces de noms▲
Comme mentionné dans le chapitre 1, un des problèmes rencontrés dans le langage C est que vous « épuisez les noms » pour les fonctions et les identifiants quand vos programmes atteignent une certaine taille. Bien sûr, vous n'épuisez pas vraiment les noms ; cependant, il devient difficile d'en trouver de nouveaux après un certain temps. Plus important, quand un programme atteint une certaine taille il est généralement coupé en morceaux, chacun étant construit et maintenu par une personne ou un groupe différent. Puisque le C n'a en réalité qu'un seul domaine où tous les identifiants et noms de fonction existent, cela signifie que tous les développeurs doivent faire attention à ne pas utiliser accidentellement les mêmes noms dans des situations où ils peuvent entrer en conflit. Cela devient rapidement fastidieux, chronophage, et, en fin de compte, cher.
Le Standard C++ contient un mécanisme pour éviter ces heurts : le mot-clé namespace. Chaque ensemble de définitions C++ d'une bibliothèque ou d'un programme est « enveloppé » dans un espace de nom, et si une autre définition a un nom identique, mais dans un espace de nom différent, alors il n'y a pas de conflit.
Les espaces de noms sont un outil pratique et utile, mais leur présence signifie que vous devez vous rendre compte de leur présence avant que vous ne puissiez écrire le moindre programme. Si vous incluez simplement un fichier d'en-tête et que vous utilisez des fonctions ou objets de cet en-tête, vous aurez probablement des erreurs bizarres quand vous essaierez de compiler le programme, dues au fait que le compilateur ne peut trouver aucune des déclarations des éléments dont vous avez justement inclus le fichier d'en-tête ! Après avoir vu ce message plusieurs fois, vous deviendrez familier avec sa signification (qui est « Vous avez inclus le fichier d'en-tête, mais toutes les déclarations sont dans un espace de nom et vous n'avez pas signalé au compilateur que vous vouliez utiliser les déclarations de cet espace de nom »).
Il y a un mot-clé qui vous permet de dire « Je veux utiliser les déclarations et/ou définitions de cet espace de nom ». Ce mot-clé, de façon assez appropriée, est using(utiliser). Toutes les bibliothèques du Standard C++ sont enveloppées dans un espace de nom unique, std(pour « standard »). Puisque ce livre utilise presque exclusivement les bibliothèques standards, vous verrez la directive using dans presque tous les programmes :
using
namespace
std;
Cela signifie que vous voulez exposer tous les éléments de l'espace de nom appelé std. Après cette instruction, vous n'avez plus à vous préoccuper de l'appartenance à un espace de nom de votre composant particulier de bibliothèque, puisque la directive using rend cet espace de nom disponible tout au long du fichier où la directive using a été écrite.
Exposer tous les éléments d'un espace de nom après que quelqu'un ait pris la peine de les masquer peut paraître un peu contre-productif, et en fait vous devez faire attention à moins penser le faire (comme vous allez l'apprendre plus tard dans ce livre). Cependant, la directive using expose seulement ces noms pour le fichier en cours, donc ce n'est pas si drastique qu'on peut le croire à première vue. (Mais pensez-y à deux fois avant de le faire dans un fichier d'en-tête - c'est risqué.)
Il y a un rapport entre les espaces de nom et la façon dont les fichiers d'en-tête sont inclus. Avant que l'inclusion moderne des fichiers d'en-tête soit standardisée (sans la fin « .h », comme dans <iostream>), la méthode classique d'inclusion d'un fichier d'en-tête était avec le « .h », comme <iostream.h>. À ce moment-là, les espaces de nom ne faisaient pas non plus partie du langage. Donc pour assurer la compatibilité ascendante avec le code existant, si vous dites
#include
<iostream.h>
cela signifie
#include
<iostream>
using
namespace
std;
Cependant, dans ce livre, le format d'inclusion standard sera utilisé (sans le « .h ») et donc la directive using doit être explicite.
Pour l'instant, c'est tout ce que vous avez besoin de savoir sur les espaces de nom, mais dans le chapitre 10 le sujet est couvert plus en détail.
III-C-3. Principes fondamentaux de structure de programme▲
Un programme C ou C++ est une collection de variables, de définitions de fonctions, et d'appels de fonctions. Quand le programme démarre, il exécute un code d'initialisation et appelle une fonction spéciale, « main( ) ». Vous mettez le code basique du programme dedans.
Comme mentionné plus tôt, une définition de fonction consiste en un type de valeur de retour (qui doit être spécifié en C++), un nom de fonction, une liste d'arguments entre parenthèses, et le code de la fonction contenu dans des accolades. Voici un échantillon de définition de fonction :
int
fonction() {
// Code de la fonction (ceci est un commentaire)
}
La fonction ci-dessus a une liste d'arguments vide et son corps contient uniquement un commentaire.
Il peut y avoir de nombreuses paires d'accolades dans une définition de fonction, mais il doit toujours y en avoir au moins une paire entourant le corps de la fonction. Puisque main( ) est une fonction, elle doit respecter ces règles. En C++, main( ) a toujours le type de valeur de retour int.
Le C et le C++ sont des langages de forme libre. À de rares exceptions près, le compilateur ignore les retours à la ligne et les espaces, il doit donc avoir une certaine manière de déterminer la fin d'une instruction. Les instructions sont délimitées par les points-virgules.
Les commentaires C commencent avec /* et finissent avec */. Ils peuvent comprendre des retours à la ligne. Le C++ utilise les commentaires du style C et a un type de commentaire supplémentaire : //. // commence un commentaire qui se termine avec un retour à la ligne. C'est plus pratique que /* */ pour les commentaires d'une seule ligne, et c'est beaucoup utilisé dans ce livre.
III-C-4. « Bonjour tout le monde ! »▲
Et maintenant, enfin, le premier programme :
//: C02:Hello.cpp
// Dire bonjour en C++
#include
<iostream>
// Déclaration des flux
using
namespace
std;
int
main() {
cout <<
"Bonjour tout le monde ! J'ai "
<<
8
<<
" ans aujourd'hui !"
<<
endl;
}
///
:~
L'objet cout reçoit une série d'arguments à travers les opérateurs « << ». Il écrit ces arguments dans l'ordre gauche à droite. La fonction de flux spéciale endl restitue la ligne et en crée une nouvelle. Avec les flux d'entrée/sortie, vous pouvez enchaîner une série d'arguments comme indiqué, ce qui rend la classe facile à utiliser.
En C, le texte entre guillemets doubles est classiquement appelé une « string » (chaine de caractères). Cependant, la bibliothèque du Standard C++ contient une classe puissante appelée string pour manipuler le texte, et donc j'utiliserai le terme plus précis tableau de caractères pour le texte entre guillemets doubles.
Le compilateur crée un espace de stockage pour les tableaux de caractères et stocke l'équivalent ASCII de chaque caractère dans cet espace. Le compilateur termine automatiquement ce tableau de caractères par un espace supplémentaire contenant la valeur 0 pour indiquer la fin du tableau de caractères.
Dans un tableau de caractères, vous pouvez insérer des caractères spéciaux en utilisant des séquences d'échappement. Elles consistent en un antislash ( \) suivi par un code spécial. Par exemple, \n signifie « nouvelle ligne ». Le manuel de votre compilateur ou un guide C local donne l'ensemble complet des séquences d'échappement ; d'autres incluent \t(tabulation), \\(antislash), et \b(retour arrière).
Notez que l'instruction peut continuer sur plusieurs lignes, et que l'instruction complète se termine avec un point-virgule.
Les arguments tableau de caractère et entier constant sont mêlés ensemble dans l'instruction cout ci-dessus. Comme l'opérateur << est surchargé avec diverses significations quand il est utilisé avec cout, vous pouvez envoyer à cout une gamme d'arguments différents et il « comprendra quoi faire avec le message ».
Tout au long du livre vous noterez que la première ligne de chaque fichier sera un commentaire qui commence par les caractères qui annoncent un commentaire (classiquement //), suivis de deux points, et la dernière ligne du listing se terminera avec un commentaire suivi par « /:~ ». C'est une technique que j'utilise pour permettre une extraction simple de l'information des fichiers de code (le programme pour le faire peut être trouvé dans le Volume 2 de ce livre, sur www.BruceEckel.com). La première ligne contient aussi le nom et l'emplacement du fichier, il peut donc être cité dans le texte ou dans d'autres fichiers, et ainsi vous pouvez facilement le trouver dans le code source pour ce livre (qui est téléchargeable sur www.BruceEckel.com).
III-C-5. Lancer le compilateur▲
Après avoir téléchargé et décompressé le code source du livre, trouvez le programme dans le sous-répertoire CO2. Lancez le compilateur avec Hello.cpp en argument. Pour de simples programmes d'un fichier comme celui-ci, la plupart des compilateurs mèneront le processus à terme. Par exemple, pour utiliser le compilateur C++ GNU (qui est disponible gratuitement sur Internet), vous écrivez :
g++
Hello.cpp
Les autres compilateurs auront une syntaxe similaire ; consultez la documentation de votre compilateur pour les détails.
III-D. Plus sur les flux d'entrée-sortie▲
Jusqu'ici vous avez seulement vu l'aspect le plus rudimentaire de la classe iostream. Le formatage de la sortie disponible avec les flux inclut également des fonctionnalités comme le formatage des nombres en notation décimale, octale et hexadécimale. Voici un autre exemple d'utilisation des flux :
//: C02:Stream2.cpp
// Plus de fonctionnalités des flux
#include
<iostream>
using
namespace
std;
int
main() {
// Spécifier des formats avec des manipulateurs :
cout <<
"un nombre en notation décimale : "
<<
dec <<
15
<<
endl;
cout <<
"en octale : "
<<
oct <<
15
<<
endl;
cout <<
"en hexadécimale : "
<<
hex <<
15
<<
endl;
cout <<
"un nombre à virgule flottante : "
<<
3.14159
<<
endl;
cout <<
"un caractère non imprimable (échap) : "
<<
char
(27
) <<
endl;
}
///
:~
Cet exemple montre la classe iostream imprimant des nombres en notation décimale, octale, et hexadécimale en utilisant des manipulateurs(qui n'écrivent rien, mais modifient l'état du flux en sortie). Le formatage des nombres à virgule flottante est automatiquement déterminé par le compilateur. En plus, chaque caractère peut être envoyé à un objet flux en utilisant un cast vers un char(un char est un type de donnée qui contient un caractère unique). Ce cast ressemble à un appel de fonction : char( ), avec le code ASCII du caractère. Dans le programme ci-dessus, le char(27) envoie un « échap » à cout.
III-D-1. Concaténation de tableaux de caractères▲
Une fonctionnalité importante du processeur C est la concaténation de tableaux de caractères. Cette fonctionnalité est utilisée dans certains exemples de ce livre. Si deux tableaux de caractères entre guillemets sont adjacents, et qu'aucune ponctuation ne les sépare, le compilateur regroupera les tableaux de caractères ensemble dans un unique tableau de caractères. C'est particulièrement utile quand les listes de code ont des restrictions de largeur :
//: C02:Concat.cpp
// Concaténation de tableaux de caractères
#include
<iostream>
;
using
namespace
std;
int
main() {
cout <<
"C'est vraiment trop long pour être mis "
"sur une seule ligne, mais ça peut être séparé sans "
"effet indésirable
\n
tant qu'il n'y a pas "
"de ponctuation pour séparer les tableaux de caractères "
"adjacents.
\n
"
;
}
///
:~
À première vue, le code ci-dessus peut ressembler à une erreur puisqu'il n'y a pas le point-virgule familier à la fin de chaque ligne. Souvenez-vous que le C et le C++ sont des langages de forme libre, et bien que vous verrez habituellement un point-virgule à la fin de chaque ligne, le besoin actuel est d'un point virgule à la fin de chaque instruction, et il est possible qu'une instruction s'étende sur plusieurs lignes.
III-D-2. Lire les entrées▲
Les classes de flux d'entrée-sortie offrent la possibilité de lire des entrées. L'objet utilisé pour l'entrée standard est cin(pour « console input » - entrée de la console). cin attend normalement une entrée sur la console, mais cette entrée peut être redirigée à partir d'autres sources. Un exemple de redirection est montré plus loin dans ce chapitre.
L'opérateur de flux d'entrée-sortie utilisé avec cin est >>. Cet opérateur attend le même type d'entrée que son argument. Par exemple, si vous donnez un argument entier, il attend un entier de la console. Voici un exemple :
//: C02:Numconv.cpp
// Convertit une notation décimale en octale et hexadécimale
#include
<iostream>
using
namespace
std;
int
main() {
int
number;
cout <<
"Entrez un nombre décimal : "
;
cin >>
number;
cout <<
"Valeur en octal = 0"
<<
oct <<
number <<
endl;
cout <<
"Valeur en hexadécimal = 0x"
<<
hex <<
number <<
endl;
}
///
:~
Ce programme convertit un nombre tapé par l'utilisateur dans ses représentations octales et hexadécimales.
III-D-3. Appeler d'autres programmes▲
Alors que la manière classique d'appeler un programme qui lit l'entrée standard et écrit sur la sortie standard est un script dans un shell Unix ou un fichier batch du DOS, tout programme peut être appelé à partir d'un programme C ou C++ en utilisant la fonction du Standard C system( ), qui est déclarée dans le fichier d'en-tête <cstdlib>:
//: C02:CallHello.cpp
// Appeler un autre programme
#include
<cstdlib>
// Déclare "system()"
using
namespace
std;
int
main() {
system("Hello"
);
}
///
:~
Pour utiliser la fonction system( ), vous lui donnez un tableau de caractères que vous pouvez normalement taper en ligne de commandes du système d'exploitation. Il peut aussi comprendre des arguments de ligne de commande, et le tableau de caractères peut être construit à l'exécution (au lieu de simplement utiliser un tableau de caractères statique comme montré ci- dessus). La commande exécute et contrôle les retours du programme.
Ce programme vous montre à quel point il est facile d'utiliser des fonctions de la bibliothèque C ordinaire en C++ ; incluez simplement le fichier d'en-tête et appelez la fonction. Cette compatibilité ascendante du C au C++ est un grand avantage si vous apprenez le langage en commençant avec une expérience en C.
III-E. Introduction aux chaines de caractères▲
Bien qu'un tableau de caractères puisse être assez utile, il est assez limité. C'est simplement un groupe de caractères en mémoire, mais si vous voulez faire quelque chose avec vous devez gérer tous les moindres détails. Par exemple, la taille d'un tableau de caractères donné est fixe au moment de la compilation. Si vous avez un tableau de caractères et que vous voulez y ajouter quelques caractères supplémentaires, vous devrez comprendre énormément de fonctionnalités (incluant la gestion dynamique de la mémoire, la copie de tableau de caractères, et la concaténation) avant de pouvoir réaliser votre souhait. C'est exactement le genre de chose que l'on aime qu'un objet fasse pour nous.
La classe string du Standard C++ est conçue pour prendre en charge (et masquer) toutes les manipulations de bas niveau des tableaux de caractères qui étaient à la charge du developpeur C. Ces manipulations étaient à l'origine de perte de temps et source d'erreurs depuis les débuts du langage C. Ainsi, bien qu'un chapitre entier soit consacré à la classe string dans le Volume 2 de ce livre, les chaines de caractères sont si importantes et elles rendent la vie tellement plus simple qu'elles seront introduites ici et utilisées régulièrement dans la première partie du livre.
Pour utiliser les chaines de caractères, vous incluez le fichier d'en-tête C++ <string>. La classe string est dans l'espace de nom std donc une directive using est nécessaire. Du fait de la surcharge des opérateurs, la syntaxe d'utilisation des chaines de caractères est assez intuitive :
//: C02:HelloStrings.cpp
// Les bases de la classe string du Standard C++
#include
<string>
#include
<iostream>
using
namespace
std;
int
main() {
string s1, s2; // Chaines de caractères vides
string s3 =
"Bonjour, Monde !"
; // Initialisation
string s4("J'ai"
); // Egalement une initialisation
s2 =
"ans aujourd'hui"
; // Affectation à une chaine de caractères
s1 =
s3 +
" "
+
s4; // Combinaison de chaines de caractères
s1 +=
" 8 "
; // Ajout à une chaine de caractères
cout <<
s1 +
s2 +
" !"
<<
endl;
}
///
:~
Les deux premières chaines de caractères, s1 et s2, commencent vides, alors que s3 et s4 montrent deux méthodes équivalentes d'initialisation des objets string à partir de tableaux de caractères (vous pouvez aussi simplement initialiser des objets string à partir d'autres objets string).
Vous pouvez assigner une valeur à n'importe quel objet string en utilisant « = ». Cela remplace le précédent contenu de la chaine de caractères avec ce qui est du côté droit de l'opérateur, et vous n'avez pas à vous inquiéter de ce qui arrive au contenu précédent - c'est géré automatiquement pour vous. Pour combiner des chaines de caractères, vous utilisez simplement l'opérateur « + », qui permet aussi de combiner des tableaux de caractères avec des chaines de caractères. Si vous voulez ajouter soit une chaine de caractère soit un tableau de caractère à une autre chaine de caractères, vous pouvez utiliser l'opérateur +=. Enfin, notez que les flux d'entrée/sortie savent déjà quoi faire avec les chaines de caractères, ainsi vous pouvez simplement envoyer une chaine de caractères (ou une expression qui produit une chaine de caractères, comme s1 + s2 + « ! ») directement à cout pour l'afficher.
III-F. Lire et écrire des fichiers▲
En C, le processus d'ouverture et de manipulation des fichiers nécessite beaucoup d'expérience dans le langage pour vous préparer à la complexité des opérations. Cependant, la bibliothèque de flux d'entrée-sortie du C++ fournit un moyen simple pour manipuler des fichiers, et donc cette fonctionnalité peut être introduite beaucoup plus tôt qu'elle ne le serait en C.
Pour ouvrir des fichiers en lecture et en écriture, vous devez inclure la bibliothèque <fstream>. Bien qu'elle inclue automatiquement <iostream>, il est généralement prudent d'inclure explicitement <iostream> si vous prévoyez d'utiliser cin, cout, etc.
Pour ouvrir un fichier en lecture, vous créez un objet ifstream, qui se comporte ensuite comme cin. Pour ouvrir un fichier en écriture, vous créez un objet ofstream, qui se comporte ensuite comme cout. Une fois le fichier ouvert, vous pouvez y lire ou écrire comme vous le feriez avec n'importe quel autre objet de flux d'entrée-sortie. C'est aussi simple que ça (c'est, bien entendu, son point fort).
Une des fonctions les plus utiles de la bibliothèque de flux d'entrée-sortie est getline( ), qui vous permet de lire une ligne (terminée par un retour chariot) dans un objet string(26). Le premier argument est l'objet ifstream que vous lisez et le second l'objet string. Quand l'appel de fonction est terminé, l'objet string contiendra la ligne.
Voici un exemple simple qui copie le contenu d'un fichier dans un autre :
//: C02:Scopy.cpp
// Copie un fichier dans un autre, une ligne à la fois
#include
<string>
#include
<fstream>
using
namespace
std;
int
main() {
ifstream in("Scopy.cpp"
); // Ouvre en lecture
ofstream out("Scopy2.cpp"
); // Ouvre en écriture
string s;
while
(getline(in, s)) // Ecarte le caractère nouvelle ligne...
out <<
s <<
"
\n
"
; // ... on doit donc l'ajouter
}
///
:~
Pour ouvrir les fichiers, vous passez juste aux objets ifstream et ofstream les noms de fichiers que vous voulez créer, comme vu ci-dessus.
Un nouveau concept est introduit ici, la boucle while. Bien que cela sera expliqué en détail dans le chapitre suivant, l'idée de base est que l'expression entre les parenthèses qui suivent le while contrôle l'exécution de l'instruction suivante (qui peut être aussi des instructions multiples, en les enveloppant dans des accolades). Tant que l'expression entre parenthèses (dans l'exemple, getline(in, s)) produit un résultat « vrai », l'instruction contrôlée par le while continuera à s'exécuter. Il s'avère que getline( ) retournera une valeur qui peut être interprétée comme « vrai » si une autre ligne a été lue avec succès, et « faux » lorsque la fin de l'entrée est atteinte. Ainsi, la boucle while ci-dessus lit chaque ligne du fichier d'entrée et envoie chaque ligne au fichier de sortie.
getline( ) lit les caractères de chaque ligne jusqu'à rencontrer une nouvelle ligne (le caractère de fin peut être changé, mais ce cas ne sera pas traité avant le chapitre sur les flux d'entrée-sortie du Volume 2). Cependant, elle ne prend pas en compte le retour chariot et ne stocke pas ce caractère dans l'objet string résultant. Ainsi, si nous voulons que le fichier de destination ressemble au fichier source, nous devons remettre le retour chariot dedans, comme montré précédemment.
Un autre exemple intéressant est de copier le fichier entier dans un unique objet string:
//: C02:FillString.cpp
// Lit un fichier en entier dans une seule chaine de caractères
#include
<string>
#include
<iostream>
#include
<fstream>
using
namespace
std;
int
main() {
ifstream in("FillString.cpp"
);
string s, line;
while
(getline(in, line))
s +=
line +
"
\n
"
;
cout <<
s;
}
///
:~
Du fait de la nature dynamique des chaines de caractères, vous n'avez pas à vous inquiéter de la quantité de mémoire à allouer pour une string; vous pouvez simplement continuer à ajouter des choses et la chaine de caractères continuera à s'étendre pour retenir tout ce que vous mettez dedans.
Une des choses agréables dans le fait de mettre un fichier en entier dans une chaine de caractères est que la classe string a beaucoup de fonctions de recherche et de manipulation qui peuvent alors vous permettre de modifier le fichier comme une simple chaine de caractères. Cependant, ceci a ses limites. Pour une chose, il est souvent pratique de traiter un fichier comme un ensemble de lignes plutôt que simplement comme un gros blob (27)de texte. Par exemple, si vous voulez ajouter une numérotation des lignes, c'est plus simple si vous avez chaque ligne dans un objet string différent. Pour accomplir cela, vous aurez besoin d'une autre approche.
III-G. Introduction à la classe vector▲
Avec les string nous avons pu remplir une chaine de caractères sans savoir de quelle taille nous allions avoir besoin. Le problème avec la lecture de lignes dans des objets string individuels est que vous ne savez pas à l'avance de combien d'objets vous allez avoir besoin - vous ne le savez qu'après avoir lu le fichier en entier. Pour résoudre ce problème, nous avons besoin d'une sorte de support qui va automatiquement s'agrandir pour contenir autant d'objets string que nous aurons besoin d'y mettre.
En fait, pourquoi se limiter à des objets string? Il s'avère que ce genre de problème - ne pas connaitre le nombre d'objets que vous allez avoir lorsque vous écrivez le programme - est fréquent. Et il semble que ce “conteneur” serait beaucoup plus utile s'il pouvait contenir toute sorte d'objets ! Heureusement, la bibliothèque standard a une solution toute faite : les classes de conteneurs standards. Les classes de conteneur sont une des forces du standard C++.
Il y a souvent une petite confusion entre les conteneurs et les algorithmes dans la bibliothèque standard du C++, et l'entité connue sous le nom de STL. Standard Template Library (Bibliothèque de modèle standard) est le nom qu'Alex Stepanov (qui travaillait alors pour Hewlett-Packard) utilisa lorsqu'il présenta sa bibliothèque au Comité de Normalisation du C++ à la conférence de San Diego, Californie au printemps 1994. Le nom est resté, surtout après que HP a décidé de la rendre disponible au libre téléchargement. Entre-temps,le comité l'a intégré dans la bibliothèque standard du C++, en y apportant un grand nombre de changements. Le développement de la STL continue au sein de Silicon Graphics (SGI; voir http://www.sgi.com/Technology/STL). Le STL de SGI diverge de la bibliothèque strandard du C++ sur un certain nombre de points subtils. Ainsi, bien que cela soit une idée faussement répandue, la bibliothèque standard du C++ n'inclut pas la STL. Cela peut prêter à confusion du fait que les conteneurs et les algorithmes de la bibliothèque standard du C++ ont la même racine (et souvent les mêmes noms) que la STL de SGI. Dans ce livre, je dirai “bibliothèque standard du C++” ou “conteneurs de la bibliothèque standard,” ou quelque chose de similaire et éviterai le terme “STL.”
Même si l'implémentation des conteneurs et des algorithmes de la bibliothèque standard du C++ utilisent des concepts avancés et que la couverture de cette dernière prenne deux grands chapitres dans le volume 2 de ce livre, cette bibliothèque peut également être efficace sans en savoir beaucoup à son sujet. Elle est si utile que le plus basique des conteneurs standards, le vector, est introduit dès ce chapitre et utilisé tout au long de ce livre. Vous constaterez que vous pouvez faire une quantité de choses énorme juste en employant les fonctionnalités de base du vector et sans vous inquiéter pour l'implémentation (encore une fois, un but important de la POO). Puisque vous apprendrez beaucoup plus à ce sujet et sur d'autres conteneurs quand vous atteindrez les chapitres sur la bibliothèque standard dans le volume 2, on pardonnera le fait que les programmes utilisant le vector dans la première partie de ce livre ne soient pas exactement ce qu'un programmeur expérimenté de C++ ferait. Vous vous rendrez compte que dans la plupart des cas, l'utilisation montrée ici est adéquate.
La classe vector est une classe générique, ce qui signifie qu'elle peut être appliquée efficacement à différents types. Ainsi, on peut créer un vecteur de formes, un vecteur de chats, un vecteur de chaines de caractères, etc. Fondamentalement, avec une classe générique vous pouvez créer une “classe de n'importe quoi”. Pour dire au compilateur ce avec quoi la classe va travailler (dans ce cas, ce que le vector va contenir), vous mettez le nom du type désiré entre les caractères \x{0091}<' et \x{0091}>'. Ainsi un vecteur de chaines de caractères se note vector<string>. Lorsque vous faites cela, vous obtenez un vecteur personnalisé pouvant contenir uniquement des objets string, et vous aurez un message d'erreur de la part du compilateur si vous essayez d'y mettre autre chose.
Puisque le vecteur exprime le concept de “conteneur,” il doit y avoir une manière d'y mettre des objets et d'en enlever. Pour ajouter un élément nouveau à la fin d'un vector, vous utilisez la fonction membre push_back( ).(Rappelez vous que, lorsqu'il s'agit d'une fonction membre, vous utilisez le \x{0091} .' pour l'appeler à partir d'un objet particulier.) La raison du nom de cette fonction membre qui peut sembler verbeux \x{0096} push_back( ) au lieu de quelque chose de plus simple comme “put”\x{0096} est qu'il y d'autres conteneurs et fonctions membres pour mettre des éléments dans les conteneurs. Par exemple, il y a une fonction membre insert( ) pour mettre quelque chose au milieu d'un conteneur. Le vector supporte cela, mais son utilisation est plus compliquée et nous n'avons pas besoin de la découvrir avant le volume 2 de ce livre. Il y a aussi push_front( )(qui n'est pas une fonction membre de la classe vector) pour mettre des objets en tête. Il y a pas mal d'autres fonctions membres de vector et pas mal de conteneurs standards dans le bibliothèque standard du C++, mais vous seriez surpris de tout ce que vous pouvez faire en ne connaissant que quelques fonctionnalités simples.
Donc vous pouvez mettre des éléments dans un vector avec push_back( ), mais comment les récupérer par la suite ? Cette solution est plus intelligente et et élégante \x{0096} la surcharge d'opérateur est utilisée pour faire ressembler le vector à un tableau. Le tableau (qui sera décrit plus en détail dans le prochain chapitre) est un type de données qui est disponible dans pratiquement tout langage de programmation il devait déjà vous être familier. Les tableaux sont des agrégats, ce qui signifie qu'ils consistent en un nombre d'éléments groupés entre eux. La caractéristique distinctive d'un tableau est que les éléments sont de même taille et sont arrangés pour être l'un après l'autre. Plus important, ces éléments peuvent être sélectionnés par “indexation”, ce qui veut dire que vous pouvez dire “je veux l'élément numéro n” et cet élément sera accessible, en général très rapidement. Bien qu'il y ait des exceptions dans les langages de programmation, l'indexation est normalement réalisée en utilisant les crochets, de cette façon si vous avez un tableau a et que vous voulez accéder au cinquième élément, vous écrivez a[4](notez que l'indexation commence à zéro).
Cette notation d'indexation très compacte et puissante est incorporée dans la classe vector en utilisant la surcharge d'opérateur, tout comme pour \x{0091} <<' et \x{0091} >>' sont incorporés dans iostreams. Encore une fois, nous n'avons pas besoin de connaitre les détails de l'implémentation de la surcharge \x{0096} cela fera l'objet d'un prochain chapitre \x{0096}, mais c'est utile si vous avez l'impression qu'il y a de la magie dans l'air dans l'utilisation de [ ] avec le vecteur.
Avec cela à l'esprit, vous pouvez maintenant voir un programme utilisant la classe vector. Pour utiliser un vector, vous incluez le fichier d'en-tête <vector>:
//: C02:Fillvector.cpp
// Copie un fichier entier dans un vecteur de chaines de caractères
#include
<string>
#include
<iostream>
#include
<fstream>
#include
<vector>
using
namespace
std;
int
main() {
vector<
string>
v;
ifstream in("Fillvector.cpp"
);
string line;
while
(getline(in, line))
v.push_back(line); // Ajoute la ligne à la fin
// Ajoute les numéros de lignes:
for
(int
i =
0
; i <
v.size(); i++
)
cout <<
i <<
": "
<<
v[i] <<
endl;
}
///
:~
Une grande partie de ce programme est similaire au précédent; un fichier est ouvert et les lignes sont lues une par une dans des objets string. Cependant, ces objets string sont poussés à la fin du vecteur v. Une fois la boucle while terminée, le fichier entier réside en mémoire, dans v.
L'étape suivante du programme est ce que l'on appelle une boucle for. Elle est similaire à la boucle while à l'exception du fait qu'elle ajoute des possibilités de contrôle supplémentaires. Après le for, il y a une “expression de contrôle” entre parenthèses, tout comme pour la boucle while. Cependant, cette expression de contrôle est en trois parties : une partie qui initialise, une qui teste si l'on doit sortir de la boucle, et une qui change quelque chose, typiquement pour itérer sur une séquence d'éléments. Ce programme exhibe une boucle for telle qu'elle est communément utilisée : l'initialisation int i = 0 crée un entier i utilisé comme un compteur de boucle et de valeur initiale zéro. La portion de test dit que pour rester dans la boucle, i doit être inférieur au nombre d'éléments du vecteur v. (Cela est déterminé en utilisant la fonction membre size( ), que j'ai glissé ici, mais vous admettrez que sa signification est assez évidente.) La portion finale emploie une notation du C et du C++, l'opérateur d'“autoincrémentation”, pour ajouter une unité à la valeur de i. En effet, i++ signifie “prend la valeur de i, ajoutes-y un, et mets le résultat dans i”. Ainsi, l'effet global de la boucle for est de prendre une variable i et de l'incrémenter par pas de un jusqu'à la taille du vecteur moins un. Pour chaque valeur de i, le cout est exécuté et cela construit une ligne contenant la valeur de i(magiquement convertie en tableau de caractères par cout), deux-points et un espace, la ligne du fichier, et un retour à la ligne amené par endl. Lorsque vous compilerez et exécuterez ce programme, vous verrez que l'effet est d'ajouter une numérotation de ligne au fichier.
Du fait que l'opérateur \x{0091} >>' fonctionne avec iostreams, vous pouvez facilement modifier le programme afin qu'il découpe l'entrée en mots au lieu de lignes :
//: C02:GetWords.cpp
// Break a file into whitespace-separated words
#include
<string>
#include
<iostream>
#include
<fstream>
#include
<vector>
using
namespace
std;
int
main() {
vector<
string>
words;
ifstream in("GetWords.cpp"
);
string word;
while
(in >>
word)
words.push_back(word);
for
(int
i =
0
; i <
words.size(); i++
)
cout <<
words[i] <<
endl;
}
///
:~
L'expression
while
(in >>
word)
permet d'obtenir une entrée “mot” à “mot”, et quand cette expression est évaluée à “faux” cela signifie que la fin du fichier a été atteinte. Naturellement, délimiter des mots par un espace est assez brut, mais cela est un exemple simple. Plus tard dans ce livre vous verrez des exemples plus sophistiqués qui vous permettront de découper l'entrée comme bon vous semble.
Pour démontrer la facilité d'utilisation d'un vecteur de n'importe quel type, voici un exemple qui crée un vector<int>:
//: C02:Intvector.cpp
// Creating a vector that holds integers
#include
<iostream>
#include
<vector>
using
namespace
std;
int
main() {
vector<
int
>
v;
for
(int
i =
0
; i <
10
; i++
)
v.push_back(i);
for
(int
i =
0
; i <
v.size(); i++
)
cout <<
v[i] <<
", "
;
cout <<
endl;
for
(int
i =
0
; i <
v.size(); i++
)
v[i] =
v[i] *
10
; // Assignment
for
(int
i =
0
; i <
v.size(); i++
)
cout <<
v[i] <<
", "
;
cout <<
endl;
}
///
:~
Pour créer un vector pour contenir un certain type, vous avez juste à mettre ce type en temps que paramètre de template (entre les caractères < et >). Les templates et les bibliothèques de templates optimisées sont prévus pour être aussi faciles à employer.
Cet exemple va vous montrer un autre aspect essentiel de la classe vector. Dans l'expression
v[i] =
v[i] *
10
;
vous pouvez voir que le vector n'est pas limité seulement à y mettre des choses et à les récupérer. Vous êtes aussi habilité à affecter à(et donc à modifier) n'importe quel élément du vecteur, ceci en utilisant l'opérateur d'indexation. Cela signifie que la classe vector est un outil d'usage universel et flexible pour travailler avec une collection d'objets, et nous en ferons usage dans les chapitres à venir.
III-H. Résumé▲
Le but de ce chapitre est de vous montrer à quel point la programmation orientée objet peut être facile si un tiers a fait pour vous le travail de définition des objets. Dans ce cas, incluez le fichier d'en-tête, créez des objets, et envoyez-leur des messages. Si les types que vous utilisez sont performants et bien conçus, vous n'avez pas beaucoup plus de travail à faire et votre programme sera également performant.
Dans l'optique de montrer la facilité de la POO lorsqu'on utilise des bibliothèques de classes, ce chapitre a également introduit quelques-uns des types les plus basiques et utiles de la bibliothèque standard du C++: la famille des iostreams (en particulier ceux qui lisent et écrivent sur la console et dans les fichiers), la classe string, et le template vector. Vous avez vu comme il est très simple de les utiliser et pouvez probablement imaginer ce que vous pouvez accomplir avec, mais il y a encore beaucoup plus à faire (28). Même si nous n'emploierons seulement qu'un sous-ensemble limité des fonctionnalités de ces outils dans la première partie de ce livre, ils fourniront néanmoins les bases de l'apprentissage d'un langage de bas niveau comme le C. Et tout en étant éducatif, l'apprentissage des aspects bas niveau du C prend du temps. En fin de compte, vous serez beaucoup plus productif si vous avez des objets pour contrôler les aspects bas niveau. Après tout, l' objectif de la POO est de cacher les détails ainsi vous pouvez peindre avec une plus grande brosse.
Cependant, pour autant que la POO essaye d'être de haut niveau, il y a quelques aspects fondamentaux du C que vous ne pouvez pas ignorer, et ceux-ci seront couverts par le prochain chapitre.
III-I. Exercices▲
Les solutions des exercices suivants se trouvent dans le document électronique The Thinking in C++ Annotated Solution Guide, disponible pour un prix modique sur http://www.BruceEckel.com
- Modifier Hello.cpp pour afficher vos nom et âge (ou pointure de chaussure, ou l'âge de votre chien, si cela vous convient mieux). Compilez et exécutez le programme.
- En se basant sur Stream2.cpp et Numconv.cpp, créez un programme qui demande le rayon d'un cercle et affiche l'aire de ce dernier. Vous ne pouvez utiliser que l'opérateur * pour élever le rayon au carré. N'essayez pas d'afficher la valeur octal ou hexadécimal (cela n'est possible qu'avec les nombres entiers).
- Écrivez un programme qui ouvre un fichier et compte les mots séparés par un espace dans ce fichier.
- Écrivez un programme qui compte le nombre d'occurrences d'un mot particulier dans un fichier (utiliser la classe string et l'opérateur == pour trouver le mot).
- Changez FillVector.cpp pour qu'il imprime les lignes (inversées) de la dernière à la première.
- Changez FillVector.cpp pour qu'il concatène tous les éléments du vector dans une simple chaine avant de les afficher, mais n'essayez pas d'ajouter la numérotation.
- Affichez un fichier ligne par ligne, qui attend que l'utilisateur appuie sur la touche “Entrée« après chaque ligne.
- Créez un vector<float> et y mettre 25 nombres flottants en utilisant une boucle for. Affichez le vecteur.
- Créez trois objets vector<float> et remplissez les deux premiers comme dans l'exercice précédent. Écrivez une boucle for qui additionne les éléments correspondants des deux premiers vecteurs et met le résultat dans l'élément correspondant du troisième vecteur. Affichez les trois vecteurs.
- Créez un vector<float> et mettez-y 25 nombres flottants comme dans les exercices précédents. Maintenant élevez au carré chaque nombre et mettez le résultat au même emplacement dans le vector. Affichez le vector avant et après la multiplication.