I
NTRODUCTION
Qu'est-ce que C++ ?
Le langage C, inventé à la fin des années 1980, s’est imposé comme le langage de programmation phare sur les micro- et mini-ordinateurs, grâce à sa facilité d’utilisation, son adéquation au langage machine qui permet une compilation rapide et un code performant, et surtout grâce aux systèmes
Unix, dont le noyau est écrit en C.
Cependant, ce langage dû à Kernighan et Ritchie, malgré plusieurs améliorations successives (dont une normalisation par l’ANSI), souffrait encore d’un certain nombre de défauts qui semblaient difficilement évitables, et notamment d’une sécurité de programmation assez faible.
Il revient à Bjarne Stroustrup, des laboratoires d’ATT, d’avoir amélioré considérablement le C en augmentant notablement ses capacités, sa sécurité, et en lui donnant la possibilité de programmer par objets, mode de programmation qui s’est beaucoup répandu ces dernières années ; ceci fut fait en respectant l’esprit du C, si bien que la nouvelle mouture, C++, semble — trompeusement — simple.
Mais si C++ est d’un abord aisé, sa maîtrise n’est pas si évidente, car il s’agit d’un langage extrêmement puissant. Passer de C à C++ équivaut sensiblement à changer une vieille deux-chevaux contre une voiture équipée d’un moteur V6 ; si l’accélération est foudroyante, un apprentissage soigné s’impose.
Ce site décrit le langage conformément aux normes d’ATT. Les programmes peuvent donc être utilisés avec n’importe quel compilateur conforme aux spécifications 2.0 et suivantes. Lorsque le comportement dépend explicitement du compilateur, nous l’avons indiqué.
Note aux programmeurs connaissant le
C
Les programmeurs qui connaissent le C n’ont pas été oubliés. Pour apprendre plus rapidement C++, ils pourront sauter les paragraphes marqués d'un bandeau de couleur, indiquant des spécifications héritées du C sans modifications, comme ceci:
Section décrivant un comportement hérité
du C
Par contre, il leur est vivement recommandé de lire soigneusement les paragraphes non marqués, qui donnent les nouveautés ou les modifications intervenues entre C et C++ ; bien que C++ soit très fortement compatible avec C, certains points de détail sont des pièges inattendus pour les programmeurs habitués au C. Ces points sont mis en relief dans le site.
Plan du site
On trouvera dans ce site une description complète des spécifications du langage C++. Cette description est répartie sur dix chapitres. Vous pouvez à tout moment vous référer au sommaire complet en cliquant sur le lien en bas de chaque page.
Le chapitre 1 donne une introduction aux éléments de base de C++, il est d’un abord très facile. Le chapitre 2 le prolonge avec une description des types prédéfinis, et des opérateurs, très importants en C++ ; ce chapitre est un peu moins simple, certains points comme la précédence des opérateurs étant assez délicats. Le chapitre 3 décrit les types composés comme les pointeurs et les tableaux, et introduit une nouveauté importante de C++, les références. Le chapitre 4 complète une description qui est encore essentiellement celle du C avec des capacités complémentaires.
Le chapitre 5 est important car il décrit l’usage des fonctions, objets de base de C et C++ ; les programmeurs connaissant le C se pencheront avec attention sur les passages d’arguments par référence et sur les fonctions en ligne, une nouveauté toute simple mais très puissante de C++, qui remplace en grande partie les macros.
La programmation orientée objet (POO) est étudiée ensuite aux chapitres 6 à 9. La définition des classes et les éléments de base sont donnés au chapitre 6 ; le chapitre 7 introduit le concept essentiel de redéfinition des opérateurs, qui donne à C++ une puissance inégalée même parmi les langages orientés objet. Le chapitre 8 introduit la notion fondamentale d’héritage qui fait une grande partie de l’efficacité de la POO. Quant au chapitre 9, il décrit les flots d’entrées-sorties, une application très pratique de la POO aux entrées-sorties, fournissant un système bien plus souple et bien plus agréable que le vieux printf. Ces chapitres sont assez difficiles, car les notions qu’ils introduisent sont, malgré leur apparente simplicité, d’une profondeur rare en programmation. Les programmeurs C ne pourront guère sauter que le premier paragraphe du chapitre 6, qui traite des structures.
Enfin le chapitre 10 explique les différentes phases de la compilation d’un programme C++ et indique comment programmer avec plusieurs fichiers. Les programmeurs C liront le paragraphe sur les macros, afin d’apprendre comment on peut les éviter en C++, et au contraire quand elles se révèlent encore utiles.
Des annexes donnent quelques points importants de références, et notamment le tableau de précédence des opérateurs.
Des exercices
Nous avons parsemé le livre de petits exercices destinés en grande partie à permettre au lecteur de vérifier sa compréhension des notions introduites. Nous ne pouvons qu’insister sur la quasi-nécessité de chercher à les résoudre, au moins mentalement, et mieux encore en programmant, car la programmation est un art qui s’apprend en pratiquant. Le langage C++
cache sa puissance sous des dehors simples et bonhommes ; de même, ces exercices paraissent souvent très simples : la solution (qui est donnée en fin d’ouvrage) ne sera pourtant peut-être pas celle que vous imaginiez.
1/
É
LEMENTS DE BASE
Le C++ est un langage structuré, typé et modulaire, autorisant la programmation orientée objet. Nous verrons dans le cours de ce livre ce que signifie exactement chacun de ces termes. Les trois premières caractéristiques, héritées du C, en font un langage de programmation « classique » (par opposition aux langages « exotiques » comme Lisp, Prolog, ou SmallTalk) ; la programmation orientée objet (POO) permet des développements intéressants, difficiles à réaliser en C. En outre, C++ contient des facilités, non liées à la POO, que le C ne possède pas.
Compilation et structure d’un texte
source
Le langage C, et a fortiori C++, est un langage compilé. Cela signifie que les instructions que l’on écrit ne sont pas directement lues par la machine : celle-ci attend qu’on lui donne l’ordre de compiler le programme. Ce dernier est alors lu en bloc, du début à la fin, et transformé en code machine. C’est ce code qui est ensuite exécuté en tant que programme. Les avantages de la compilation, par rapport à l'interprétation, sont importants : le code est beaucoup plus rapide, et les erreurs d’écriture plus faciles à repérer. En outre, elle oblige à une certaine organisation du programme (sur laquelle nous reviendrons) qui limite les risques d’erreurs. Cependant, elle exige d’écrire directement des programmes complets.
Messages d’erreur
Le texte que le programmeur tape est appelé texte source du programme; le code compilé obtenu est l’exécutable. Pour que la
compilation ne soit pas trop lente, il faut aussi qu’elle ne soit pas trop complexe. Par conséquent, le programmeur est tenu de respecter un certain nombre de conventions qui ont pour but de faciliter cette compilation, et aussi d’éviter des erreurs.
Lorsque ces conventions ne sont pas respectées, le compilateur suppose que le programmeur s’est trompé. Il affiche alors un message d’erreur, qui peut être de trois types.
Un message d’attention Warning signale simplement une bizarrerie dans le programme qui ne l’empêche pas cependant d’être compilé et exécuté; c’est le cas par exemple lorsqu’on déclare une variable sans l’utiliser : le compilateur affiche Warning : xxx declared but never used (xxx
déclarée mais jamais utilisée), où xxx désigne le nom de la variable en question (en fait le message est un peu plus long, car le compilateur précise le numéro de ligne et le nom de la fonction courante). De tels messages sont peut-être des erreurs du programmeur, ou peut-être pas. Ils n’empêchent nullement le programme de fonctionner, et peuvent donc être ignorés, bien qu’il soit généralement préférable d’y prêter attention.
Un message Error signale une erreur qui empêche le programme de fonctionner. C++ continue cependant la compilation (si les options par défaut sont choisies) jusqu’à la fin du fichier, afin de signaler par la même occasion les autres erreurs. Cependant, la première erreur peut en provoquer toute une série d’autres. Ainsi un point-virgule mal placé (voir exemple ci-après) peut engendrer des dizaines de messages d’erreur successifs, car il ôte au compilateur ses points de repère, lui faisant ainsi « perdre les pédales ». De telles erreurs de syntaxe sont fréquentes, surtout pour un débutant, ainsi que les erreurs en frappant un nom de variable. Elles doivent être corrigées impérativement pour que le programme puisse fonctionner.
Dans certains cas rares, il arrive qu’une erreur fatale (Fatal error) se produise en cours de compilation ; celle-ci s’arrête alors immédiatement. C’est le cas notamment des débordements de mémoire (Fatal error : out of memory).
Il est important de savoir que les messages d’erreur ne sont pas toujours ceux attendus. En effet, le compilateur signale le premier terme qui l’empêche d’accepter le texte, mais il se peut que l’erreur soit avant. Par exemple, le code suivant :
int fonction(void); // erreur ici
{
// texte de la fonction...
provoque l’arrêt du compilateur sur la ligne contenant l’accolade ouvrante {,
avec le message Error : declaration was expected, c’est-à-dire une
déclaration était attendue. En effet, à cette étape c’est ce qui devrait suivre du point de vue du compilateur. Mais en réalité, il ne fallait pas mettre de point-virgule à la fin de la ligne précédente (voir la différence entre déclaration et définition de fonction plus loin).
Symboles et identificateurs
Le texte source est composé d’un certain nombre de termes séparés par des espaces blancs (ou par rien dans certain cas) ; les tabulations et les sauts de lignes sont considérés comme des blancs en général.
Les termes sont constitués d’identificateurs et de symboles. Les
identificateurs servent à donner des noms aux variables, types, fonctions, etc. Tout nom ne peut cependant pas être utilisé. Les identificateurs ne peuvent contenir que des lettres (minuscules ou majuscules mais pas de lettre accentuée), des chiffres (0 à 9), ou le caractère spécial de soulignement (_) ; ils ne doivent pas commencer par un chiffre. En particulier, les espaces ne sont pas admis, ils coupent l’identificateur en plusieurs.
Voici quelques exemples d’identificateurs :
Salut carre_entier _9 uneChaine var3
Les mots suivants ne sont pas des identificateurs :
Hello! Premier? 7val 9_
De plus, certains identificateurs sont réservés pour des opérations spéciales ; on les appelle des mots réservés, ou mots clés. Leur liste figure
en annexe.
En C++, on distingue les mots en majuscules de ceux en minuscules. En conséquence, A et a sont deux identificateurs différents. Les mots réservés
sont toujours en minuscules.
Les symboles sont les groupes de caractères qui ne sont pas des identificateurs. On y trouve notamment un grand nombre de caractères spéciaux d’opérations, qui se lisent individuellement, ou par paire, voire par triple, comme par exemple :
+ * ++ && <<= /* */
Commentaires
Il est très utile de placer à l’intérieur d’un programme des commentaires, indiquant en quelques mots ce que l’on fait à cet endroit. Cela permet de relire plus facilement le programme ultérieurement, surtout pour ceux qui ne l’ont pas écrit.
Il existe deux sortes de commentaires en C++. L’ancien type (hérité du C) commence par le doublon /* et se finit par */ comme dans cet exemple :
void fonction(int i)
/* Cette fonction fait ceci, cela ... (suit une description de la fonction). */ {
...
Le deuxième type, nouveau en C++, commence par le doublon // et se
void fonction(int i)
// Cette fonction fait ceci, cela ...
// (suit une description de la fonction).
{
... // ici commence la
fonction
Tous ces commentaires sont considérés comme des blancs par le compilateur.
On ne peut pas en général imbriquer des commentaires du premier type (bien qu’une option le permette parfois). Le code suivant:
/*
void fonction(int i)
/*Cette fonction fait ceci, cela ... */ {
... }
*/
est erroné car le commentaire s’arrête à la fin de la troisième ligne, et non en dernière ligne. Il faudrait écrire :
/*
void fonction(int i)
*//* Cette fonction fait ceci, cela ... *//* {
... }
*/
Il est donc préférable d’utiliser toujours le second type de commentaires, sauf pour « débrancher » momentanément une partie du code, car on peut alors croiser les deux types de commentaires :
/*
void fonction(int i)
// Cette fonction fait ceci, cela ...
{
... }
Ce groupe est ici entièrement ignoré; si l’on retire /* et */, la fonction
sera de nouveau compilée.
Types de données et variables
Un langage informatique manipule de l’information, comme le terme l’indique. Celle-ci est stockée dans les cases mémoire de l’ordinateur sous forme de bits. Cependant, il est rare que l’on ait besoin de manipuler ces bits en tant que tels. En général, on souhaite plutôt utiliser des entités plus sophistiquées, comme des entiers, des réels, des caractères, etc. Chacune de ces entités va elle-même être codée sur un certain nombre de bits dans la mémoire, mais ce détail n’est pas très intéressant pour le programmeur, bien qu’il ait des conséquences importantes sur lesquelles nous reviendrons. Un langage évolué comme C++ permet d’utiliser des données de haut niveau, comme des nombres entiers par exemple, en se chargeant lui-même de la « basse besogne » consistant à convertir les bits de mémoire en ce type de donnée, ou inversement.
Caractérisation des types
Le grand mot est lancé : les données ont donc un type qui indique deux choses importantes :
• l’ensemble de valeurs dont elles font partie,
• l’ensemble des propriétés qui les caractérisent.
Donnons tout de suite un exemple simple : le type entier int (abréviation de l’anglais integer), qui est le plus utilisé en C++, a habituellement pour ensemble de valeurs tous les nombres de -32767 à 32768 compris ; parmi les propriétés (très nombreuses) qui le caractérisent, on trouve un grand nombre d’opérations possibles, comme l’addition (+), la soustraction (-), la multiplication (notée * et non avec un point ou une croix comme en mathématiques), la division entière (/) et modulo (%), etc.
Le type décimal double a un ensemble de valeurs différent, et certaines opérations comme la division modulo n’ont pas de sens sur ce type : ses
propriétés sont donc différentes de celles de int.
Déclarations de variables
Une donnée est une brique élémentaire dans un programme que l’on caractérise par son type, d’une part, et par sa valeur actuelle d’autre part. Nous venons de voir ce qu’est un type ; la valeur actuelle de la donnée (par exemple 12 pour un entier) est sujette à modification en général, sous la
contrainte qu’elle reste dans l’ensemble de valeurs du type. Par contre, le type de la donnée reste toujours le même ; en conséquence, les propriétés d’une donnée, qui sont celles de son type, sont constantes.
Lorsque le programmeur veut utiliser une donnée, il doit :
• préciser son type en écrivant le nom de celui-ci,
• donner un nom particulier à la donnée (afin de pouvoir y faire référence),
• éventuellement préciser des éléments supplémentaires lorsqu’il souhaite que la donnée ait des propriétés spécifiques en plus.
Voici donc comment on indique que l’on va utiliser une donnée entière nommée nombre :
int nombre;
Par défaut, on crée ainsi une variable, c’est-à-dire une donnée que l’on peut modifier librement.
Une telle écriture s’appelle une déclaration; elle indique à C++ que l’on souhaite utiliser une telle donnée. La déclaration est obligatoire: si on l’oublie, et que dans la suite on utilise le nom nombre, le compilateur refusera
ce terme non déclaré, puisqu’il ne peut deviner de quel type de variable il s’agit. Ceci permet d’éviter de nombreuses erreurs ; en particulier, si l’on fait une faute de frappe, en tapant par exemple nmbre au lieu de nombre, le compilateur signalera l’erreur (par le message Error : Undefined symbol
'nmbre'), alors que d’autres langages créeraient automatiquement une
variable de ce nom, ce qui serait tout à fait erroné.
La syntaxe d’une déclaration de variable est donc élémentaire : nom_de_type nom_de_variable ;
Une telle déclaration a deux utilités. Primo, elle indique à C++ que l’on a besoin d’une variable ; celui-ci va donc prendre un petit morceau de la mémoire et le réserver spécialement à cet usage. Secundo, elle indique au compilateur les propriétés de la variable, qui sont celles de son type ; de la sorte, si l’on écrit par erreur une opération interdite sur cette variable, le compilateur le signalera par un message.
Définitions de variables
En pratique, il est fréquent que l’on souhaite initialiser cette variable, c’est-à-dire lui donner une valeur. En effet, il est important de savoir que lorsqu’on déclare une variable, sa valeur est la plupart du temps indéfinie; en particulier, ce n’est pas zéro en général ! (La plupart du temps, c'est simplement le contenu de la mémoire réservée à l'usage de la donnée.) Ce point est essentiel, il est source de nombreuses erreurs. On initialisera les variables le plus souvent possible, afin d’éviter tout problème, en écrivant par exemple :
int nombre = 1;
Un tel couple déclaration + initialisation sera nommé définition, car il définit entièrement la variable. Sa syntaxe est aussi très simple:
nom_de_type nom_de_variable = valeur_initiale ;
Modification d’une variable par affectation
Lorsqu’on souhaite modifier une variable, c’est-à-dire changer sa valeur actuelle, le moyen le plus simple est d’effectuer une affectation. Cette opération s’exprime avec le symbole =, comme ceci (les variables nommées
sont entières sauf indication du contraire ; elles sont supposées déclarées précédemment) :
nombre = 75;
A droite du signe =, on doit trouver un nom de variable ; à gauche, on doit avoir une expression du même type que cette variable. Une telle expression peut être composée de nombreuses façons, comme on le verra, et notamment en utilisant une ou plusieurs variables de type adéquat séparées par des opérateurs autorisés (nous y reviendrons). Voici quelques exemples:
nombre = 12 + 7; nombre = i + j;
nombre = 1 + 2 * nombre;
On notera que l’on peut aussi utiliser la valeur actuelle de la variable pour créer la nouvelle, comme dans le cas de la troisième ligne ; une telle écriture n’est pas une équation demandant à l’ordinateur une résolution (et dont la solution serait ici -1), mais bien une affectation : la machine prend l’ancienne valeur de la variable nombre (par exemple 12), la multiplie par 2 (rappelons que * signifie multiplier), ajoute 1, et place le résultat obtenu (25) dans la case mémoire associée à nombre, modifiant ainsi sa valeur actuelle.
Exercice 1.1 Pourquoi ne peut-on écrire :
i + j = 2;
Quelle est l’erreur produite, en supposant les deux variables i et j déclarées de type int ?
Voir solution
Solution de l’exercice 1.1
C’est que i+j n’est pas un nom de variable, mais une expression. L’erreur
Error : Lvalue required est générée; le terme lvalue, abréviation de left
d’affectation =, c’est-à-dire essentiellement les noms de variables (plus d’autres écritures étudiées plus tard).
Nous verrons plus tard d’autres moyens de modifier la valeur d’une variable; l’affectation est cependant de loin la plus fréquemment utilisée.
Des fonctions et des programmes
Nous avons dit au début de ce chapitre que C++ était un langage modulaire ; cela signifie que les actions élémentaires ayant une certaine cohérence peuvent être regroupées en unités plus importantes, créant ainsi de nouveaux types d’action, utilisables simplement par leur nom.
Une fonction élémentaire
Ainsi, supposons que l’on écrive un programme de calcul sur des nombres entiers, dans lequel on calcule fréquemment le cube de nombre ; un tel cube peut être écrit nombre * nombre * nombre mais c’est assez lourd.
Pour abréger nos notations, nous allons introduire dans le programme une fonction qui aura pour tâche de calculer le cube de nombre :
int cube_nombre(void) // exemple un peu simplet !
{
return nombre * nombre * nombre; }
On a indiqué en premier le type du résultat de la fonction (int, c’est-à-dire entier), puis le nom de la fonction, choisi arbitrairement (ici
cube_nombre) ; viennent ensuite une parenthèse ouvrante qui indique au compilateur que l’on déclare une fonction (et non une variable), puis la liste des arguments (ici aucun, ce qu’on exprime par le terme void) et une parenthèse fermante. Cette première ligne déclare la fonction. Elle est ici
suivie de la définition de la fonction, constituée par une séquence d’instructions élémentaires enclose entre accolades {}. Ici, la seule
instruction est la déclaration du résultat de la fonction par le mot clé return.
La fonction principale
Tout programme C++ doit comprendre au moins une fonction, nommée main (adjectif anglais signifiant « principale »), qui est le point d’entrée du programme en ce sens que le programme commence au début de main et s’arrête à la fin de celle-ci.
Voici donc un exemple de programme élémentaire :
#include <iostream.h> int nombre = 15; main() {
cout << nombre * nombre * nombre; return 0;
}
Pour bien marquer la différence entre cette fonction et les autres, nous n’écrivons rien entre les parenthèses, et nous omettons la déclaration de son type résultat, qui est int. Cette écriture est autorisée car int est le type
résultat par défaut des fonctions; il est toutefois préférable de toujours l’indiquer pour les fonctions autres que main, pour d’évidentes raisons de
clarté. Nous verrons ultérieurement que main peut en fait avoir des
arguments.
Ce programme écrit simplement le cube de nombre. La directive #include
demande au préprocesseur d’inclure les en-têtes des fonctions d’entrées-sorties. Le flot de sortie cout est celui de l’écran, et l’écriture cout << x
D’une façon générale, les instructions comprises entre les accolades correspondant à main sont exécutées par le programme de la première à la
dernière dans l’ordre où elles sont écrites, et le programme s’arrête lorsqu’il rencontre l’instruction return ou la fin de la fonction main (c’est-à-dire
l’accolade fermante). Nous verrons cependant qu’il est possible de modifier cet ordre d’exécution par des instructions adéquates.
La fonction main doit renvoyer un résultat, qui est le numéro retourné au
système d'exploitation ; la valeur 0 indique un fonctionnement normal, les
autres une erreur.
Appel d’une fonction
Nous aurions pu utiliser notre fonction cube_nombre ainsi : #include <iostream.h> int nombre = 15; int cube_nombre(void) {
return nombre * nombre * nombre; } main() { cout << cube_nombre(); return 0; }
Observons bien la notation cube_nombre() qui a remplacé le produit
précédent. Elle indique au programme d’appeler la fonction cube_nombre, sans arguments, et de remplacer cube_nombre() par le résultat renvoyé par
cette fonction.
Lorsque le programme arrive sur un appel de fonction comme celui-ci, qui se distingue d’une variable par les parenthèses qui le suivent, il se « déroute », et continue son exécution à l’intérieur de la fonction, jusqu’à ce qu’il y rencontre une instruction return, qui lui fournit la quantité recherchée ; le programme revient alors à l'instruction suivant l'appel de
fonction, remplaçant cet appel par la quantité renvoyée, et continue l’exécution normalement. On peut représenter ce fonctionnement par un petit schéma ; les flèches indiquent dans quel sens l’exécution du programme se déroule.
Une fonction peut aussi être appelée seule dans une instruction. Dans ce cas, sa valeur de retour est ignorée.
Fonctions avec arguments
Notre fonction cube_nombre n’est guère pratique, car elle ne permet de calculer que le cube de la variable nombre ; on pourrait bien sûr écrire une fonction comme celle-là pour chaque variable, mais ce serait fastidieux. Il est préférable d’écrire une fonction générale calculant le cube d’un nombre quelconque.
Voici une telle fonction :
int cube(int x) {
return x * x * x; }
Ici nous avons précisé un argument entre parenthèses, alors que cube_nombre n’en avait pas, ce qui s’exprimait par le terme void. L’argument est un entier, ce qui est exprimé par l’indication de type int. On lui donne le nom x pour la durée de la fonction, afin de pouvoir le désigner à l’intérieur de celle-ci ; cela n’est pas obligatoire mais, si on ne le
faisait pas, on ne pourrait calculer le cube de cet argument, puisqu’on ne pourrait y faire référence.
A présent, dans le programme principal, on devra indiquer la valeur de l’argument (nommée le paramètre effectif) de cube avant d’appeler celle-ci, comme ceci :
cout << cube(nombre);
L’oubli du paramètre entraînerait une erreur de compilation (Error : Too few parameters in call to 'cube(int)', c’est-à-dire trop peu de paramètres
dans l’appel de ‘cube(int)’).
Notons qu’une expression entière peut être passée en paramètre à cube, si son résultat est du type int:
cout << cube(nombre+1);
On obtient ainsi d’agréables raccourcis d’écriture.
Exercice 1.2 Comment écrirait-on
cube(cube(nombre))
sans utiliser de fonctions ?
Voir solution
Solution de l’exercice 1.2
nombre * nombre * nombre * nombre * nombre * nombre * nombre * nombre *
nombre; (eh oui!)
Une fonction peut avoir autant d’arguments que souhaité, avec des types différents éventuellement. Par exemple, la fonction mathématique suivante, tirée de la librairie <math.h>, admet deux arguments, le premier de type
double, le second de type int, et renvoie une valeur de type double : double ldexp(double x, int exp);
D’autre part, il arrive que certaines fonctions ne renvoient aucun résultat (). Dans ce cas, elles sont déclarées avec pour résultat void ; ce mot réservé sert donc de « remplissage » soit pour une liste d’arguments vide, soit pour un résultat
inexistant. Voici deux exemples tirés de la librairie <conio.h> :
void gotoxy(int x, int y) ; void clrscr(void);
Lorsqu’on désire arrêter le déroulement d’une telle fonction, on peut écrire une instruction return sans rien derrière ; en l’absence d’une telle instruction, la fonction se termine sur l’accolade fermante. Toutes les autres fonctions doivent avoir une instruction return, sinon elles renvoient un résultat aléatoire (un message d’erreur prévient de l’oubli dans certains cas :
Error : Function should return a value, c’est-à-dire la fonction doit
renvoyer une valeur).
Le concept de fonction est central en C++, et nous en verrons de nombreux exemples ultérieurement.
Boucles et branchements
Nous avons dit plus haut que les instructions d’un programme étaient exécutées, par défaut, de la première à la dernière l’une après l’autre. Il est rare en pratique qu’un tel comportement soit souhaitable. Dans la plupart des cas, on souhaite au contraire pouvoir choisir entre plusieurs actions possibles, en fonction d’un critère quelconque. On souhaite souvent aussi recommencer une action plusieurs fois, le propre des ordinateurs étant d’effectuer des tâches répétitives sans ennui ni fatigue. Pour toutes ces actions, C++ fournit un certain nombre d’instructions spéciales introduites par des mots clés particuliers.
Lorsqu’on a le choix d’une alternative, ce qui est extrêmement fréquent, on utilise l’instruction de branchement simple if, dont la syntaxe est la suivante :
if (condition) action1; else action2;
Ici, condition désigne n’importe quelle expression donnant un résultat numérique (entier ou à virgule flottante). Cette quantité est calculée ; si elle est non nulle, l’instruction action1 est exécutée ; dans le cas contraire, l’instruction action2 est exécutée. Dans les deux cas, le déroulement du programme se poursuit normalement sur l’instruction suivante.
Par exemple, l’instruction suivante donne un message différent selon que la variable nombre est nulle ou non :
if (nombre) cout << "Nombre non nul"; else cout << "Nombre nul";
Notons bien que les parenthèses entourant l’expression de condition, même lorsqu’elle se réduit à la valeur d’une seule variable comme ici, sont absolument obligatoires.
Dans la pratique, on utilisera plutôt des opérateurs spéciaux permettant de comparer deux quantités. Ces opérateurs sont < (inférieur à), > (supérieur à), <= (inférieur ou égal), >= (supérieur ou égal), == (égal à), != (différent). Ils renvoient 1 si l’inégalité ou l’égalité est vérifiée, 0 sinon.
Ainsi, l’instruction suivante place dans y la valeur absolue de x :
if (x < 0) y = -x; else y = x;
Exercice 1.3 Écrire une fonction abs qui renvoie la valeur
absolue d’un nombre.
Voir solution
Solution de l’exercice 1.3
Pas besoin de y:int abs(int x) {
if (x < 0) return -x else return x; }
Il arrive parfois que l’on n’ait pas d’alternative, simplement une action à faire si la condition est réalisée, rien sinon. Dans ce cas, on peut omettre la clause else. Voici une instruction qui remplace x par sa valeur absolue :
if (x < 0) x = -x;
On peut aussi ne rien écrire à la place de action1, comme ceci :
if (erreur) ; else cout << "Aucune erreur";
Notons le point-virgule obligatoire entre la parenthèse fermante et le mot else. On préférera toutefois une écriture plus explicite :
if (erreur == 0) cout << "Aucune erreur";
On peut aussi utiliser l’opérateur ! qui change les valeurs nulles en 1 et
les autres en 0, inversant ainsi les conditions:
if (!erreur) cout << "Aucune erreur";
Il arrive souvent que l’on souhaite faire plusieurs actions ensemble dans le cas où une condition est vérifiée. Dans ce cas, on doit les grouper avec des accolades, comme ceci :
if (x + y < 0) { x = 0; y =0; } else { x = x + y; y = x -y; }
ou de manière plus aérée :
if (x + y < 0) { x = 0;
y = 0; } else { x = x + y; y = x -y; }
Observons l’absence de point-virgule devant else et à la fin de l’ensemble: les accolades fermantes les rendent inutiles (devant else, un point-virgule est ici fautif, et produit une erreur Error : Misplaced else, else
mal placé ; à la fin de l’ensemble, il ajoute simplement une instruction vide). Par contre, les points-virgules devant ces accolades fermantes sont obligatoires (message d’erreur Error : Statement missing ;, ; manquant).
Boucles simples
while
et
do
Il est fréquent que l’on doive répéter une action un grand nombre de fois dans un programme ; en fait, la plupart des algorithmes sont basés sur de telles répétitions. On dispose pour cela de trois instructions spéciales que l’on appelle instructions de boucle.
Les deux premières se ressemblent beaucoup. Il s’agit de la boucle while, dont la syntaxe est la suivante:
while (condition) action;
et de la boucle do...while de syntaxe :
do action while (condition);
La première demande à la machine de calculer d’abord la condition (qui doit être une expression entière comme dans le cas de l’instruction de branchement) ; si elle est nulle, on passe directement à l’instruction suivante. Si elle est non nulle au contraire, l’instruction action est exécutée, et le processus recommence (c’est-à-dire qu’on ne passe pas à l’instruction suivante, mais on réévalue la condition, etc.). Une telle boucle ne cesse donc que lorsque l’expression condition devient nulle. En conséquence, la séquence suivante :
while (1) cout << "Hello !";
a l’intéressant effet d’écrire indéfiniment le mot « Hello ! » à l’écran, puisque 1 n’est jamais nul.
La seconde boucle est à peine différente. Elle demande à la machine d’exécuter l’action, puis d’évaluer la condition, et de recommencer ainsi jusqu’à ce que la condition devienne nulle. La différence réside dans le fait que l’instruction action est exécutée au moins une fois, quelle que soit la condition. Une telle boucle est plus adaptée que la première lorsque les variables dont dépend la condition sont calculées par l’instruction action, et n’ont pas de sens à l’entrée de la boucle. Nous en verrons des exemples plus tard.
Il arrive souvent que l’on souhaite exécuter plusieurs instructions dans une telle boucle. Dans ce cas, comme pour l’instruction de branchement, on les enclôt dans des accolades, et l’on ne place pas de point-virgule après l’accolade fermante. Voici par exemple un programme qui affiche tous les nombres multiples de 7 compris entre 0 et 1000.
#include <iostream.h> main() { int nombre = 0; while (nombre <= 1000) { cout << nombre << '\t'; nombre += 7; } return 0; }
On a écrit une tabulation derrière chaque nombre. L’écriture nombre += 7
est un raccourci d’écriture pour ajouter 7 à nombre (voir plus loin). Dans ce cas particulier, on aurait pu aussi écrire :
int nombre = 0; do {
cout << nombre << '\t'; nombre += 7;
}
while (nombre <= 1000);
à la place de l’autre boucle, puisqu’on souhaite écrire au moins un nombre de toute façon. On notera que les accolades sont absolument obligatoires, bien qu’ici les instructions soient encloses par les termes do et while.
Exercice 1.4 Écrire un programme qui additionne tous les
nombres impairs inférieurs à 100, et qui affiche le résultat obtenu. Voir solution
Solution de l’exercice 1.4
Le résultat est 2 500 : #include <iostream.h> main() { int somme = 0, i = 1; while (i < 100) { somme += i; // augmente la somme i += 2; // nombre impair suivant }cout << "La somme est : " << somme; return 0;
}
Boucle complexe
for
La troisième instruction de boucle est bien plus sophistiquée que les autres, bien qu’elle ne soit nullement nécessaire. Elle a la syntaxe suivante :
for (initialisation; condition; itération)
Elle demande la réalisation des actions suivantes : exécuter l’instruction d’initialisation, puis tester la condition (qui doit ici encore être une expression numérique). Si elle est non nulle, exécuter l’instruction action, puis l’instruction itération, et tester à nouveau la condition, et ainsi de suite jusqu’à ce que la condition devienne nulle.
On notera que l’initialisation n’est réalisée qu’une fois, avant toute autre action. Le schéma est donc identique à celui produit par la boucle while suivante: initialisation; while (condition) { action; itération; }
Le programme de la section précédente aurait donc pu être écrit ainsi (compte tenu de la possibilité d’une déclaration interne, décrite à la section suivante) :
#include <iostream.h>
main() {
for (int nombre = 0; nombre <= 1000; nombre += 7)
cout << nombre << '\t'; return 0;
}
Cette boucle permet donc essentiellement des écritures plus compactes, au prix parfois d’une certaine perte de lisibilité, surtout si elle est utilisée sans discernement.
On notera que l’instruction action peut être remplacée par un groupe entre accolades, comme pour les autres boucles, mais que initialisation et itération doivent rester des instructions uniques (on peut toutefois en mettre plusieurs par des astuces d’écriture décrites au chapitre 2).
Déclarations internes
En C++, les déclarations de variables sont autorisées à peu près n’importe où dans un programme. On les utilise surtout dans des boucles for :
for (int i = 0; i < 100; i++) // ...
et dans des blocs internes:
if (ok) { int i = 0; // ... } else { long i = 1; // ... }
Dans ce dernier cas, la variable n’existe que le temps d’exécuter son bloc (c’est-à-dire de l’accolade ouvrante à l’accolade fermante) ; cela permet de déclarer deux variables différentes de même nom dans deux blocs différents et séparés, comme dans notre exemple ci-dessus. On reviendra sur ces notions de déclarations locales et de « visibilité » au chapitre 5.
Nous avons vu les principales capacités élémentaires de C++. Nous allons à présent réaliser une approche plus systématique, en étudiant plus à fond les éléments entrevus ci-dessus, et en en développant de nouveaux.
2/
T
YPES PREDEFINIS ET
OPERATEURS
Il existe en C++ un certain nombre de types dits prédéfinis, c’est-à-dire automatiquement connus du compilateur; nous connaissons déjà les types int et double. Ces types sont dotés d’un certain nombre d’opérations de base exprimées par des opérateurs, comme l’addition, etc. Ces opérateurs se retrouvent sur d’autres types, et nous verrons aussi plus tard qu’il est possible de les redéfinir. On retiendra pour le moment que les opérateurs, très nombreux en C++, y jouent un rôle central.
Types entiers
Nous connaissons déjà le type int, qui désigne un entier avec signe codé (sur PC) sur deux octets (seize bits, dont un de signe); son ensemble de valeurs varie donc de -215 à 215-1, c’est-à-dire de -32 768 à 32 767. Il arrive
parfois que ces quantités soient insuffisantes. On dispose alors du type « entier long » long int, codé sur quatre octets. Son ensemble de valeurs varie donc de -231 = -2147483648 à 231-1 = 2147483647.
Il existe en outre un type « entier court » short int, mais son ensemble de valeurs est le même que int sur PC. Il n’est donné que pour des raisons de compatibilité avec les systèmes UNIX, sur lesquels les tailles de codage des entiers ne sont pas, en général, les mêmes. On prendra garde en général d’éviter de faire des hypothèses sur les valeurs maximales et minimales des types entiers, qui peuvent varier d’une machine à l’autre, parfois d’un compilateur à l’autre. Pour ppalier ces inconvénients, il est parfoi utile d'utiliser une instruction typedef pour créer un nom de type que l'on ajustera selon les circonstances. Il est possible aussi de calculer le nombre d'octets occupés par un type, en utilisant l'opérateur sizeof.
Chacun de ces trois types peut être utilisé en version sans signe. On obtient alors les types unsigned int, dont les valeurs sont de 0 à 216-1 =
65535, ainsi que unsigned short int, et unsigned long int qui varie de 0 à 232-1 = 4 294 967 295. On peut aussi écrire signed int, etc.,
mais c’est sans intérêt en général puisque c’est la valeur par défaut.
En pratique, il est assez lourd d’écrire unsigned int par exemple. Il est donc permis d’omettre le mot int dans ces écritures (sauf quand il est seul). On écrira donc unsigned pour unsigned int, long pour long int, et unsigned long pour unsigned long int :
unsigned u, v; long l1, l2; int i, j, k = 0;
unsigned long ll = -1; // attention ! valeur
0xFFFFFFFF en fait
Dans cet exemple, on a déclaré deux unsigned int, deux long int, trois int, dont un initialisé à 0, et un unsigned long int initialisé... à une valeur qui ne fait pas partie de son ensemble de valeurs ! Cette écriture est cependant permise.
En effet, lorsque dans un programme un débordement
quelconque se produit dans un calcul sur des entiers, aucune erreur n’est signalée. Les calculs se poursuivent simplement en
tronquant les bits excédentaires. Il en résulte que les calculs sur les entiers à deux octets (int et short, signés ou non) se font modulo 216 = 65536, et
sur les entiers à quatre octets (long et unsigned long) modulo 232 =
4 294 967 296. En conséquence, la variable ll ci-dessus est en fait initialisée à 4294967296 -1 = 4294967295, (ou 0xFFFFFFFF en hexadécimal), soit la plus grande valeur possible pour un unsigned long.
Par ailleurs, lorsque une opération est effectué sur deux types entiers, celle-ci est réalisée avec les arrondis correspondants au plus grand de ces types, indépendamment du type résultat. Par exemple les instructions suivantes:
short i = 256; int j = 512;
fournissent la valeur 0 (supposant toujours que int contient deux octets), parce que le produit est effectué dans le type int. Pour contourner ce problème , il faut d'abord convertir l'un des entiers en long, en écrivant par exemple l = i* (long)j. Voir aussi plus loin sur ce point délicat.
Exercice 2.1 Après les initialisations suivantes, combien vaut x ?
int u = 10000, v = 10000; unsigned long x = u *v;
Voir solution
Solution de l’exercice 2.1
Cela dépend du compilateur, mais il est peu probable que x vale 100 000 000 comme on devrait l’attendre. En Turbo C++, par exemple, sur PC, x vaut 4 294 959 360, soit encore 232 - 7 936. En effet, le produit u*v est
trop grand pour tenir dans un entier à deux octets; il est donc tronqué, ce qui donne -7 936, d’où le résultat final. Une telle écriture est donc erronée et doit être proscrite. On peut l’améliorer par un changement de type (voir plus loin dans ce chapitre), mais il est bien plus sûr d’écrire :
unsigned long x = u; x *= v;
Constantes entières
On utilise souvent dans les programmes des constantes entières, comme 0, 12, -2000, etc. Lorsqu’elles sont écrites en décimal, ce qui est le cas général, elles ne doivent pas commencer par un zéro: écrire 12 et non 012
(voir paragraphe suivant). Il ne faut pas non plus placer d’espace à l’intérieur: écrire 2000 et non 2 000 qui serait interprété comme 2, suivi de 000, et provoquerait une erreur.
La constante peut être suivie des caractères U et/ou L, écrits en
majuscules ou en minuscules, qui indiquent au compilateur de prendre la valeur sans signe (U pour unsigned) ou longue correspondante. Par
int i = 10000;
long l = 10000L * i;
unsigned long ul = 30000UL * i;
placera bien la valeur 100 000 000 dans l, alors que si l’on avait écrit 10000 * i, un débordement se serait produit, plaçant la valeur erronée -7936 dans
l. De même, la valeur correcte de 300 000 000 sera placée dans ul, au lieu de la valeur erronée 4 294 943 488.
Exercice 2.2 Dans cet exemple, peut-on écrire 30000L au lieu
de 30000UL ? Et si la valeur était 300000, que faudrait-il écrire ?
Voir solution
Solution de l’exercice 2.2
Oui, car les valeurs signées et non-signées prennent la même place: aucun débordement ne se produit, seule la signification des bits change; il est donc inutile de placer le suffixe U. Dans le cas de 300000, il s’agit obligatoirement d’un entier long, il est donc inutile en plus de placer le suffixe L.
Constantes hexadécimales et octales
Les valeurs constantes entières sont généralement écrites en décimal, mais il peut parfois être utile de les écrire en octal ou en hexadécimal. Pour cela, on place un préfixe qui est le chiffre 0 pour les valeurs octales, et 0x
pour les hexadécimales. Par exemple, on a les égalités suivantes:
119 = 0167 = 0x77;
118783 = 0347777 = 0x1CFFF;
On notera que le zéro initial est le signe d’un nombre octal : c’est pourquoi il est interdit pour les valeurs décimales, et une écriture comme 078 provoquera une erreur de compilation (Error : Illegal octal digit,
chiffre octal incorrect); cela ne pose un problème que pour la valeur zéro, qui est de toute façon identique en décimal ou en octal.
Les chiffres hexadécimaux A à F peuvent être écrits en majuscules ou en minuscules
Opérateurs sur les entiers
On dispose d’un grand nombre d’opérateurs sur les entiers. Nous en donnons ci-après la liste (on n’a pas indiqué les opérateurs de comparaison, voir plus loin) avec des exemples dans lesquels on a supposé les initialisations suivantes :
int i = 3, j = -12; unsigned u = 35000; long l = -100000;
unsigned long ul = 3000000000;
Pour comprendre ces exemples, il faut savoir que lorsqu’on fait agir
dans une même opération deux entiers de même type, le résultat est de ce même type, avec troncature éventuelle, c'est-à-dire perte des bits excédentaires; par exemple, sur des entiers codés sur 32
bits, tous les calculs sont effectués modulo 232.
Si l’on fait agir deux entiers de types différents, les entiers signés sont convertis en non-signés si nécessaire, et les courts en plus long afin que les deux types soient les mêmes. Par exemple, si j et u interviennent ensemble, j est transformé en entier non-signé (ce qui donne 65524). Si j et l
interviennent ensemble, j est transformé en long (et reste donc -12). Si j et ul interviennent ensemble, j est transformé en unsigned long (ce qui donne
4 294 967 284). Enfin si u et l interviennent ensemble, u est converti en long.
Toutes ces conversions sont indépendantes du type du résultat.
Les opérateurs sur les entiers agissent de gauche à droite, c'est-à-dire que l’opérande de gauche est évalué en premier. Ils ont en outre une priorité correspondant à l’habitude : les multiplications ou divisions avant les additions et soustractions, elles-mêmes avant les décalages qui sont encore avant les opérations sur les bits. Par exemple, comme * précède +, une
multiplication est prioritaire et réalisée en premier. Le tableau de précédence complet des opérateurs est donné en annexe.
Opérateur Description Exemples
+ Opérateur unaire sans effet. +i donne 3
+j donne -12
-Opérateur unaire de changement de signe. Si x est une
variable unsigned, on ajoute 216 ou 232 à -x pour
conserver un signe positif.
-i donne -3 -l donne 100000 -u donne 30536 -ul donne 1294967296¤
~
Opérateur unaire inversant tous les bits (1 changé en 0, 0 en 1). L’effet obtenu est que ~x = -x-1 si x est
une variable signée, ~x = M-x si x est non signé, où M
désigne la plus grande valeur possible de x, soit
65535 pour une variable à deux octets, et 4294967295 pour une à quatre octets.
~i donne -4 ~j donne 11 ~l donne 99999 ~u donne 30535 ~ul donne 1294967695 *
Opérateur binaire symétrique de multiplication. i*j donne -36 l*i donne -300000
u*i donne 39464
(troncature)
/
Opérateur binaire de division sans reste. On obtient le quotient de la division euclidienne des deux
opérandes, s’ils sont positifs; sinon le résultat dépend de la machine et il est préférable de se méfier. Les résultats suivants sont obtenus avec Turbo C++ sur PC.
l/i donne -33333 u/i donne 11666
l/j donne 8333 (-l)/j donne -8333 u/j donne 0 (ici j est
transformé en unsigned
de valeur 65524)
%
Opérateur binaire de division modulo. Renvoie le reste de la division euclidienne des opérandes s’ils sont positifs. On a toujours la relation x == (x/y)*y + (x%y).
l%i donne -1 u%i donne 2 l%j donne 4 (-l)%j donne -4 u%j donne 35000 (idem) +
Opérateur binaire symétrique d’addition. i+j donne -9 u+i donne 35003 ul+l donne 2999900000
l+u donne -65000
-Opérateur binaire de soustraction. i-j donne 15 u-i donne 34997 ul-l donne 3000100000 l-ul donne 1294867296
(débordement)
<<
Opérateur binaire de décalage des bits à gauche: x << y vaut x décalé de y bits à gauche; les bits de poids
fort sortants sont perdus, les entrants valent 0, si y est
i << 2 donne 12 l << i donne -80000
positif (si y est négatif, le résultat est indéfini). Cette opération donne donc x * 2y, avec troncature.
(troncature)
>>
Opérateur binaire de décalage des bits à droite.
Comme précédemment, mais les bits sont décalés vers la droite. L’opération x >> y est donc égale à x / 2y.
i >> 2 donne 0 l >> i donne -12500
u >> 4 donne 2187 &
Opérateur binaire symétrique de « et » logique. Les bits des deux opérandes sont conjugués en « et » logique (multiplication logique).
i&6 donne 2 i&j donne 0 l&u donne 2080 ^
Opérateur binaire symétrique de « ou exclusif » logique. Les bits des opérandes sont conjugués en « ou exclusif » logique (différence logique).
i^6 donne 5 i^j donne -9 l^u donne -69160 |
Opérateur binaire symétrique de « ou » logique. Les bits des opérandes sont conjugués en « ou » logique (addition logique).
i|6 donne 7 i|j donne -9 l|u donne -67080
Ceci constitue une liste respectable d’opérateurs, mais en pratique, ce sont surtout les opérateurs arithmétiques +, -, *, /, % qui sont utilisés, les
autres opérant au niveau des bits sont plutôt destinés à des opérations de masquage assez pointues, et rares.
Caractères et chaînes
Un type entier particulier, nommé char, est utilisé pour les caractères. Il
comprend toutes les valeurs ASCII de 0 à 255. Il y a trois moyens de donner
une valeur constante à un caractère. La première consiste à écrire le caractère entre apostrophes :
char c = 'E';
On ne peut écrire une apostrophe seule de cette manière. Il faut utiliser la deuxième ou la troisième méthode. La deuxième est un ensemble de caractères spéciaux signalés par une barre oblique inverse \, dont voici la liste :
Caractère Description Code ASCII
'\a' signal sonore 7
'\f' saut de page 12
'\n' nouvelle ligne 10
'\r' retour chariot 13
'\t' tabulation 9
'\v' tabulation verticale 11
'\\' barre oblique inverse \ 92
'\'' apostrophe ' 39
'\"' guillemet " 34
'\?' point d’interrogation ? 63
Les deux derniers peuvent être écrits sans barre oblique inverse: '"' et '?'.
La troisième méthode consiste à donner directement la valeur ASCII du caractère soit en octal soit la forme '\nnn' où nnn désigne un nombre octal
(sans 0 initial obligatoire), soit en hexadécimal sous la forme '\xhh' où hh
désigne un nombre en hexadécimal. On a donc par exemple les identités :
'?' == '\?' == '\77' == '\x3F'
car il s'agit du 63ème caractère de la table ASCII, et que 63 == 077 = 0x3F. Il n’y a pas d’opération spécifique sur les caractères, qui sont considérés comme des entiers par le compilateur. Dans ces opérations, un octet de poids fort est ajouté au caractère pour obtenir un entier sur deux octets. Donc (char)63 donne encore '?'.
Notons qu’il existe des types unsigned char et signed char. Par défaut, le
type char signifie signed char, c’est-à-dire que l’octet de poids fort, quand il
existe, est égal à -1 lorsque le caractère est supérieur ou égal à 128, mais on dispose d’une option de compilation demandant de le considérer comme
unsigned; dans ce cas, l’octet de poids fort est toujours nul.
Chaînes de caractères constantes
Une chaîne de caractères est constituée d’un certain nombre de caractères suivis du caractère spécial nul '\0', placé par le compilateur, qui
entre guillemets. Il est possible d’y placer des caractères spéciaux en les écrivant comme indiqué dans la section précédente. Par exemple la chaîne suivante :
"\tLes sanglots longs\n\tDes violons\n"
sera écrite ainsi :
Les sanglots longs Des violons
avec passage à la ligne à la fin de chaque vers (caractère \n) et tabulation au
début (caractère \t). Dans tous les cas, le zéro final est ajouté
automatiquement; ce caractère n’est jamais affiché.
Exercice 2.3 Laquelle des deux chaînes suivantes est incorrecte ?
Pourquoi? Combien y a-t-il de caractères dans l’autre chaîne, et que vaut-elle ?
"\xffFolie !\"\n"
"\xFF\"Farceur\a\\\t !\"\n"
Voir solution
Il n’existe aucun opérateur sur les chaînes de caractères. On dispose cependant dans la librairie <string.h> d’un grand nombre de fonctions
adaptées, dont nous verrons quelques exemples ultérieurement.
Solution de l’exercice 2.3
La première chaîne est incorrecte. En effet, comme elle commence par
\x, on cherche un nombre hexadécimal derrière. On trouve alors ffF (o n'est
pas un chiffre hexadécimal), soit un nombre trop grand pour convenir à un caractère (0 à 255 = 0xFF seulement). Le compilateur affiche alors Error : Numeric constant too large(constante numérique trop grande).
La seconde chaîne est correcte. Elle correspond au caractère 255 = 0xFF
(\xFF), suivi d’un guillemet " (\"), puis des lettres F, a, r, c, e, u, r, du
caractère 7 = '\a' (signal sonore), d’une barre oblique inverse \ (\\), d’une
" (\") et enfin d’un saut de ligne (\n). En comptant le zéro final, cela fait
seize caractères, et l’on obtient à l'affichage, avec un signal sonore :
"Farceur\(tabulation) !"
le caractère 255 étant affiché comme un espace au début.
Nombres à virgule flottante
On a parfois besoin de faire des calculs sur des nombres décimaux (ou
nombres à virgule flottante), et non seulement entiers. Pour cela, on dispose de trois types à virgule flottante, nommés float, double et long double. Ils se distinguent par la taille qu’ils occupent et la précision qu’ils
autorisent.
Une variable de type float occupe quatre octets, peut varier de ±3.4 10-38
à ±3.4 1038, et a une précision d’environ sept chiffres décimaux. Une
variable de type double occupe huit octets, peut varier de ±1.7 10-308 à
±1.7 10308, et a une précision d’environ quinze chiffres décimaux. Enfin une
variable de type long double occupe dix octets, peut varier de ±3.4 10-4932 à
3.4 104932 et a une précision d’environ dix-neuf chiffres décimaux.
À ces intervalles de valeurs il faut ajouter quatre valeurs spéciales, 0,
+infini, -infini et NaN (not a number, pas un nombre). Les trois dernières
sont générées en cas de débordement de capacité, ou d’opération interdite (0/0 par exemple). En principe, de telles valeurs ne sont jamais vues car
elles provoquent l’arrêt du programme avec un message. Toutefois, ce comportement par défaut peut être modifié par les routines signal et matherr. Par défaut, les dépassements de capacité par le bas ne sont pas
interceptés : ils renvoient simplement la valeur 0.
Les entiers sont automatiquement convertis en décimaux lorsqu’une opération fait intervenir ces deux types de variables. Inversement, des affectations comme celle-ci :
float f = 2.6; int i = f;
sont tout à fait autorisées. Elles ont pour conséquence une troncature de la valeur à virgule flottante, par suppression des décimales. En conséquence, i
vaudra 2 après l’initialisation ci-dessus. De plus, lorsqu’il y a débordement de capacité des entiers, seuls les bits les moins significatifs sont conservés, comme pour les autres opérations. Donc, si f était initialisé à 65 789.9 par
exemple, i serait initialisé à 253 = 65 789 modulo 65 536. Toutefois, si le
nombre décimal est plus grand que la plus grande valeur entière possible (232 -1), le programme s’interrompt en indiquant une erreur (overflow,
c’est-à-dire débordement).
Opérateurs sur les décimaux
Les opérateurs sur les décimaux sont les suivants, dans l’ordre de priorité:
Priorité forte
+ Opérateur unaire sans effet.
- Opérateur unaire de changement de signe.
Priorité moyenne
* Opérateur binaire symétrique de multiplication. / Opérateur binaire de division;
il s’agit de la division de deux décimaux au sens usuel.
Priorité faible
+ Opérateur binaire symétrique d’addition. - Opérateur binaire symétrique de soustraction.
Ces opérateurs ont leur sens usuel, identique à celui des entiers, sauf pour la division / qui est normale ici, alors qu’elle n’est faite que de manière
euclidienne sur les entiers.
Opérateurs et raccourcis
Nous avons déjà examiné les opérateurs agissant sur les nombres entiers et décimaux. Il s’agit d’opérateurs assez usuels, qui se retrouvent dans beaucoup de langages de programmation. Ceux que nous allons étudier à présent sont très spécifiques de C++, et servent essentiellement de raccourcis d’écriture.
Incrémentation et décrémentation
Deux opérateurs méritent un paragraphe spécial. Il s’agit de l’opérateur d’incrémentation ++ et de celui de décrémentation --. Ces opérateurs
unaires peuvent agir sur une variable entière (y compris caractère) ou décimale, ainsi que sur les pointeurs comme on le verra, et ont deux effets groupés. La variable est en effet augmentée de 1 (incrémentation) ou diminuée (décrémentation) de 1, tandis que la valeur renvoyée est soit l’ancienne valeur de la variable (incrémentation ou post-décrémentation), soit la nouvelle (pré-incrémentation et pré-décrémentation).
Étudions cela sur un exemple:
int i = 4; int u = i++; int v = i--; int w = --u; int t = ++v;
À l’issue de ces initialisations, que valent les différentes variables ? Il suffit de regarder pas à pas. Au départ, i vaut 4. L’écriture u = i++ est une
post-incrémentation (++ est derrière la variable i à laquelle il s’applique) qui
a deux effets. D’abord, la valeur de i, soit 4, est recopiée dans u ; puis la
variable i est incrémentée. À l’issue de cette seconde ligne, u vaut donc 4 et i
5. La troisième ligne est semblable, mais cette fois i est décrémenté après
avoir été copié dans v; donc i vaut alors 4 et v 5.
Dans la quatrième ligne, on a une pré-décrémentation. Par conséquent, u
est d’abord décrémenté, prenant ainsi la valeur 3, puis cette valeur est recopiée dans w. Dans la dernière ligne, on a une pré-incrémentation ; v est
donc incrémenté, prenant la valeur 6, et recopié dans t.
À l’issue de l’ensemble, i vaut 4, u 3, v 6, w 3, t 6. Si cela ne vous
paraissait pas évident au premier abord, c’est normal !
Ces deux opérateurs sont parfois très pratiques comme raccourcis d’écriture. En outre, ils correspondent à des opérations très naturelles et
donc très rapides du processeur. Cependant, il est évident qu’ils diminuent la lisibilité des programmes.
On prendra garde de ne pas tenter de les appliquer à des expressions qui ne représentent pas des variables. Ainsi, l’expression :
u = (i + j)++;
est refusée par le compilateur (Error : Lvalue required), tandis que celle-ci :
u = i + j++;
est correcte, car l’opérateur d’incrémentation est prioritaire. Ici, u prend la
valeur de la somme i+j, puis j est incrémenté.
Exercice 2.4 Après la séquence d’instructions suivantes,
combien valent les variables?
int i = 10, j = 3; float f = i/j; j = 10-f++*--i;
Voir solution
Solution de l’exercice 2.4
>code>f vaut 4, i vaut 9 et j vaut -17. En effet, i/j vaut 3 (et non 3.333...), car il y a
troncature tant que l’on reste avec des opérandes entiers. Ensuite, la dernière ligne équivaut, vu la priorité et l’ordre des opérateurs (cf. annexe), aux suivantes:
i = i -1; // pré-décrémentation
j = 10 - (f*i); // * prioritaire par rapport à
-f = -f + 1; // post-incrémentation
Opérateurs logiques
Il n’existe pas en C++ de type logique, comme par exemple le type
Boolean du Pascal. Les nombres, entiers ou décimaux, sont utilisés à la place
avec la convention suivante : une valeur nulle correspond à la valeur logique faux, une autre valeur à vrai.
Il existe des opérateurs qui fournissent automatiquement des valeurs logiques, c’est-à-dire en l’occurrence un entier égal à 0 ou 1 ; ils s’appliquent
à un ou deux nombres entiers ou décimaux (une conversion est faite pour rendre les types des deux opérandes égaux). Ce sont les suivants :
!
Opérateur unaire de négation logique. !x vaut 0 si x est non nul, 1 sinon. Cet
opérateur ne peut être appliqué à un décimal, car il donne un résultat erroné; écrire dans ce cas x == 0.
== Opérateur binaire symétrique d’égalité. x == y vaut 1 si les opérandes sont égaux, 0
sinon.
!= Opérateur binaire symétrique d’inégalité. Contraire de ==.
< Opérateur binaire d’inégalité. x < y vaut 1 si x est strictement inférieur à y, 0 sinon. > Opérateur binaire d’inégalité. x > y vaut 1 si x est strictement supérieur à y, 0 sinon. <= Opérateur binaire d’inégalité. Contraire de >.
>= Opérateur binaire d’inégalité. Contraire de <.
Ces opérateurs sont redondants, puisque par exemple x<y équivaut à !(x>=y), que !x équivaut à x == 0, etc.
Exercice 2.5 Il n’existe pas d’opérateur logique unaire qui
renvoie 1 si son opérande est non-nul, 0 sinon. Comment le simuler ? Est-ce très utile ?
Voir solution
Solution de l’exercice 2.5
Il suffit d’écrire !!x (si x n’est pas décimal) ou encore x != 0. C’est
toutefois d’une utilité assez faible, puisque la valeur logique obtenue est équivalente à celle de x seul. On pourra ainsi aussi bien écrire if (x)... que if (!!x)...
Lorsqu’on a deux valeurs logiques, on peut effectuer des opérations dessus à l’aide de deux opérateurs particuliers :
&& Opérateur binaire symétrique logique « et » . Renvoie 1 si ses deux opérandes sont
non nuls, 0 sinon.
|| Opérateur binaire symétrique logique « ou » . Renvoie 1 si l’un au moins de ses deux