IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Penser en C++

Volume 1

Penser en C++

Volume 1


précédentsommairesuivant

3. Le C de C++

Puisque le C++ est basé sur le C, vous devez être familier avec la syntaxe du C pour programmer en C++, tout comme vous devez raisonnablement être à l'aise en algèbre pour entreprendre des calculs.

Si vous n'avez jamais vu de C avant, ce chapitre va vous donner une expérience convenable du style de C utilisé en C++. Si vous êtes familier avec le style de C décrit dans la première édition de Kernighan et Ritchie (souvent appelé K&R C), vous trouverez quelques nouvelles fonctionnalités différentes en C++ comme en C standard. Si vous êtes familier avec le C standard, vous devriez parcourir ce chapitre en recherchant les fonctionnalités particulières au C++. Notez qu'il y a des fonctionnalités fondamentales du C++ introduites ici qui sont des idées de base voisines des fonctionnalités du C ou souvent des modifications de la façon dont le C fait les choses. Les fonctionnalités plus sophistiquées du C++ ne seront pas introduites avant les chapitres suivants.

Ce chapitre est un passage en revue assez rapide des constructions du C et une introduction à quelques constructions de base du C++, en considérant que vous avez quelque expérience de programmation dans un autre langage. Une introduction plus douce au C se trouve dans le CD ROM relié au dos du livre, appelée Penser en C : Bases pour Java et C++ par Chuck Allison (publiée par MindView, Inc., et également disponible sur www.MindView.net). C'est une conférence sur CD ROM avec pour objectif de vous emmener prudemment à travers les principes fondamentaux du langage C. Elle se concentre sur les connaissances nécessaires pour vous permettre de passer aux langages C++ ou Java, au lieu d'essayer de faire de vous un expert de tous les points d'ombre du C (une des raisons d'utiliser un langage de haut niveau comme le C++ ou le Java est justement d'éviter plusieurs de ces points sombres). Elle contient aussi des exercices et des réponses guidées. Gardez à l'esprit que parce que ce chapitre va au-delà du CD Penser en C, le CD ne se substitue pas à ce chapitre, mais devrait plutôt être utilisé comme une préparation pour ce chapitre et ce livre.

3.1. Création de fonctions

En C d'avant la standardisation, vous pouviez appeler une fonction avec un nombre quelconque d'arguments sans que le compilateur ne se plaigne. Tout semblait bien se passer jusqu'à l'exécution du programme. Vous obteniez des résultats mystérieux (ou pire, un plantage du programme) sans raison. Le manque d'aide par rapport au passage d'arguments et les bugs énigmatiques qui en résultaient est probablement une des raisons pour laquelle le C a été appelé un « langage assembleur de haut niveau ». Les programmeurs en C pré-standard s'y adaptaient.

Le C et le C++ standards utilisent une fonctionnalité appelée prototypage de fonction. Avec le prototypage de fonction, vous devez utiliser une description des types des arguments lors de la déclaration et de la définition d'une fonction. Cette description est le « prototype ». Lorsque cette fonction est appelée, le compilateur se sert de ce prototype pour s'assurer que les bons arguments sont passés et que la valeur de retour est traitée correctement. Si le programmeur fait une erreur en appelant la fonction, le compilateur remarque cette erreur.

Dans ses grandes lignes, vous avez appris le prototypage de fonction (sans lui donner ce nom) au chapitre précédent, puisque la forme des déclaration de fonction en C++ nécessite un prototypage correct. Dans un prototype de fonction, la liste des arguments contient le type des arguments qui doivent être passés à la fonction et (de façon optionnelle pour la déclaration) les identifiants des arguments. L'ordre et le type des arguments doit correspondre dans la déclaration, la définition et l'appel de la fonction. Voici un exemple de prototype de fonction dans une déclaration :

 
Sélectionnez
int translate(float x, float y, float z);

Vous ne pouvez pas utiliser pas la même forme pour déclarer des variables dans les prototypes de fonction que pour les définitions de variables ordinaires. Vous ne pouvez donc pas écrire : float x, y, z. Vous devez indiquer le type de chaque argument. Dans une déclaration de fonction, la forme suivante est également acceptable :

 
Sélectionnez
int translate(float, float, float);

Comme le compilateur ne fait rien d'autre que vérifier les types lorsqu'une fonction est appelée, les identifiants sont seulement mentionnés pour des raisons de clarté lorsque quelqu'un lit le code.

Dans une définition de fonction, les noms sont obligatoires car les arguments sont référencés dans la fonction :

 
Sélectionnez
int translate(float x, float y, float z) {
  x = y = z;
  // ...
}

Il s'avère que cette règle ne s'applique qu'en C. En C++, un argument peut être anonyme dans la liste des arguments d'une définition de fonction. Comme il est anonyme, vous ne pouvez bien sûr pas l'utiliser dans le corps de la fonction. Les arguments anonymes sont autorisés pour donner au programmeur un moyen de « réserver de la place dans la liste des arguments ». Quiconque utilise la fonction doit alors l'appeler avec les bons arguments. Cependant, le créateur de la fonction peut utiliser l'argument par la suite sans forcer de modification du code qui utilise cette fonction. Cette possibilité d'ignorer un argument dans la liste est aussi possible en laissant le nom, mais vous obtiendrez un message d'avertissement énervant à propos de la valeur non utilisée à chaque compilation de la fonction. Cet avertissement est éliminé en supprimant le nom.

Le C et le C++ ont deux autres moyens de déclarer une liste d'arguments. Une liste d'arguments vide peut être déclarée en C++ par fonc( ), ce qui indique au compilateur qu'il y a exactement zero argument. Notez bien que ceci indique une liste d'argument vide en C++ seulement. En C, cela indique « un nombre indéfini d'arguments » (ce qui est un « trou » en C, car dans ce cas le contrôle des types est impossible). En C et en C++, la déclaration fonc(void); signifie une liste d'arguments vide. Le mot clé void signifie, dans ce cas, « rien » (il peut aussi signifier « pas de type » dans le cas des pointeurs, comme il sera montré plus tard dans ce chapitre).

L'autre option pour la liste d'arguments est utilisée lorsque vous ne connaissez pas le nombre ou le type des arguments ; cela s'appelle une liste variable d'arguments. Cette « liste d'argument incertaine » est représentée par des points de suspension ( ...). Définir une fonction avec une liste variable d'arguments est bien plus compliqué que pour une fonction normale. Vous pouvez utiliser une liste d'argument variable pour une fonction avec un nombre fixe d'argument si (pour une raison) vous souhaitez désactiver la vérification d'erreur du prototype. Pour cette raison, vous devriez restreindre l'utilisation des liste variables d'arguments au C et les éviter en C++ (lequel, comme vous allez l'apprendre, propose de bien meilleurs alternatives). L'utilisation des listes variables d'arguments est décrite dans la section concernant la bibliothèque de votre guide sur le C.

3.1.1. Valeurs de retour des fonctions

Un prototype de fonction en C++ doit spécifier le type de la valeur de retour de cette fonction (en C, si vous omettez le type de la valeur de retour, il vaut implicitement int). La spécification du type de retour précède le nom de la fonction. Pour spécifier qu'aucune valeur n'est retournée, il faut utiliser le mot clé void. Une erreur sera alors générée si vous essayez de retourner une valeur de cette fonction. Voici quelques prototypes de fonctions complets :

 
Sélectionnez
int f1(void); // Retourne un int, ne prend pas d'argument
int f2(); // Comme f1() en C++ mais pas en C standard !
float f3(float, int, char, double); // Retourne un float
void f4(void); // Ne prend pas d'argument, ne retourne rien

Pour retourner une valeur depuis une fonction, utilisez l'instruction return. return sort de la fonction et revient juste après l'appel de cette fonction. Si return a un argument, cet argument devient la valeur de retour de la fonction. Si une fonction mentionne qu'elle renvoie un type particulier, chaque instruction return doit renvoyer ce type. Plusieurs instructions return peuvent figurer dans la définition d'une fonction :

 
Sélectionnez
//: C03:Return.cpp
// Utilisation de "return"
#include <iostream>
using namespace std;
 
char cfonc(int i) {
  if(i == 0)
    return 'a';
  if(i == 1)
    return 'g';
  if(i == 5)
    return 'z';
  return 'c';
}
 
int main() {
  cout << "Entrez un entier : ";
  int val;
  cin >> val;
  cout << cfonc(val) << endl;
} ///:~

Dans cfonc( ), le premier if qui est évalué à true sort de la fonction par l'instruction return. Notez que la déclaration de la fonction n'est pas nécessaire, car sa définition apparaît avec son utilisation dans main( ), et le compilateur connaît donc la fonction depuis cette définition.

3.1.2. Utilisation de la bibliothèque de fonctions du C

Toutes les fonctions de la bibliothèque de fonctions du C sont disponibles lorsque vous programmez en C++. Étudiez attentivement la bibliothèque de fonctions avant de définir vos propres fonctions – il y a de grandes chances que quelqu'un ait déjà résolu votre problème, et y ait consacré plus de réflexion et de débogage.

Cependant, soyez attentifs : beaucoup de compilateurs proposent de grandes quantités de fonctions supplémentaires qui facilitent la vie et dont l'utilisation est tentante, mais qui ne font pas partie de la bibliothèque du C standard. Si vous êtes certains que vous n'aurez jamais à déplacer votre application vers une autre plateforme (et qui peut être certain de cela ?), allez-y – utilisez ces fonctions et simplifiez vous la vie. Si vous souhaitez que votre application soit portable, vous devez vous restreindre aux fonctions de la bibliothèque standard. Si des activités spéficiques à la plateforme sont nécessaires, essayez d'isoler ce code en un seul endroit afin qu'il puisse être changé facilement lors du portage sur une autre plateforme. En C++, les activités spécifiques à la plateforme sont souvent encapsulées dans une classe, ce qui est la solution idéale.

La recette pour utiliser une fonction d'une bibliothèque est la suivante : d'abord, trouvez la fonction dans votre référence de programmation (beaucoup de références de programmation ont un index des fonctions aussi bien par catégories qu'alphabétique). La description de la fonction devrait inclure une section qui montre la syntaxe du code. Le début de la section comporte en général au moins une ligne #include, vous montrant le fichier d'en-tête contenant le prototype de la fonction. Recopiez cette ligne #include dans votre fichier pour que la fonction y soit correctement déclarée. Vous pouvez maintenant appeler cette fonction de la manière qui est montrée dans la section présentant la syntaxe. Si vous faites une erreur, le compilateur la découvrira en comparant votre appel de fonction au prototype de la fonction dans l'en-tête et vous signifiera votre erreur. L'éditeur de liens parcourt implicitement la bibliothèque standard afin que la seule chose que vous ayez à faire soit d'inclure le fichier d'en-tête et d'appeler la fonction.

3.1.3. Créer vos propres bibliothèques avec le bibliothécaire

Vous pouvez rassembler vos propres fonctions dans une bibliothèque. La plupart des environnements de programmation sont fournis avec un bibliothécaire qui gère des groupes de modules objets. Chaque bibliothécaire a ses propres commandes, mais le principe général est le suivant : si vous souhaitez créer une bibliothèque, fabriquez un fichier d'en-tête contenant les prototypes de toutes les fonctions de votre bibliothèque. Mettez ce fichier d'en-tête quelque part dans le chemin de recheche du pré-processeur, soit dans le répertoire local (qui pourra alors être trouvé par #include "en_tete") soit dans le répertoire d'inclusion (qui pourra alors être trouvé par #include <en_tete>). Prenez ensuite tous les modules objets et passez-les au bibliothécaire, en même temps qu'un nom pour la bibliothèque (la plupart des bibliothécaires attendent une extension habituelle, comme .lib ou .a). Placez la bibliothèque terminée avec les autres bibliothèques, afin que l'éditeur de liens puisse la trouver. Pour utiliser votre bibliothèque, vous aurez à ajouter quelque chose à la ligne de commande pour que l'éditeur de liens sache où chercher la bibliothèque contenant les fonctions que vous appelez. Vous trouverez tous les détails dans votre manuel local, car ils varient d'un système à l'autre.

3.2. Contrôle de l'exécution

Cette section traite du contrôle de l'exécution en C++. Vous devez d'abord vous familiariser avec ces instructions avant de pouvoir lire et écrire en C ou C++.

Le C++ utilise toutes les structures de contrôle du C. Ces instructions comprennent if-else, while, do-while, for, et une instruction de sélection nommée switch. Le C++ autorise également l'infâme goto, qui sera proscrit dans cet ouvrage.

3.2.1. Vrai et faux

Toutes les structures de contrôle se basent sur la vérité ou la non vérité d'une expression conditionnelle pour déterminer le chemin d'exécution. Un exemple d'expression conditionnelle est A == B. Elle utilise l'opérateur == pour voir si la variable A est équivalente à la variable B. L'expression génère un Booléen true ou false(ce sont des mots clé du C++ uniquement ; en C une expression est “vraie” si elle est évaluée comme étant différente de zéro). D'autres opérateurs conditionnels sont >, <, >=, etc. Les instructions conditionnelles seront abordées plus en détail plus loin dans ce chapitre.

3.2.2. if-else

La structure de contrôle if-else peut exister sous deux formes : avec ou sans le else. Les deux formes sont :

 
Sélectionnez
if(expression)
    instruction

or

 
Sélectionnez
if(expression)
    instruction
else
    instruction

l'“expression” est évaluée à true ou false. Le terme “instruction” désigne soit une instruction seule terminée par un point virgule soit une instruction composée qui est un groupe d'instructions simples entourées d'accolades. Chaque fois que le terme “instruction” est utilisé, cela implique toujours qu'il s'agisse d'une instruction simple ou composée. Notez qu'une telle instruction peut être également un autre if, de façon qu'elles puissent être cascadées.

 
Sélectionnez
//: C03:Ifthen.cpp
// Demonstration des structures conditionnelles if et if-else
#include <iostream>
using namespace std;
 
int main() {
  int i;
  cout << "tapez un nombre puis 'Entrée" << endl;
  cin >> i;
  if(i > 5)
    cout << "Il est plus grand que 5" << endl;
  else
    if(i < 5)
      cout << "Il est plus petit que 5 " << endl;
    else
      cout << "Il est égal à " << endl;
 
  cout << "tapez un nombre puis 'Entrée" << endl;
  cin >> i;
  if(i < 10)
    if(i > 5)  // "if" est juste une autre instruction
      cout << "5 < i < 10" << endl;
    else
      cout << "i <= 5" << endl;
  else // Se réfère au "if(i < 10)"
    cout << "i >= 10" << endl;
} ///:~

Par convention le corps d'une structure de contrôle est indenté pour que le lecteur puisse déterminer aisément où elle commence et où elle se termine (30).

3.2.3. while

Les boucles while, do-while, et for. une instruction se répète jusqu'à ce que l'expression de contrôle soit évaluée à false. La forme d'une boucle while est

 
Sélectionnez
while(expression)
    instruction

L'expression est évaluée une fois à l'entrée dans la boucle puis réévaluée avant chaque itération sur l'instruction.

L'exemple suivant reste dans la boucle while jusqu'à ce que vous entriez le nombre secret ou faites un appui sur control-C.

 
Sélectionnez
//: C03:Guess.cpp
// Devinez un nombre (demontre le "while")
#include <iostream>
using namespace std;
 
int main() {
  int secret = 15;
  int guess = 0;
  // "!=" est l'opérateur conditionnel "différent de" :
  while(guess != secret) { // Instruction composée
    cout << "Devinez le nombre : ";
    cin >> guess;
  }
  cout << "Vous l'avez trouvé !" << endl;
} ///:~

L'expression conditionnelle du while n'est pas restreinte à un simple test comme dans l'exemple ci-dessus ; il peut être aussi compliqué que vous le désirez tant qu'il produit un résutat true ou false. Vous verrez même du code dans lequel la boucle n'a aucun corps, juste un point virgule dénudé de tout effet :

 
Sélectionnez
while(/* Plein de choses ici */)
 ;

Dans un tel cas, le programmeur a écrit l'expression conditionnelle pour non seulement réaliser le test mais aussi pour faire le boulot.

3.2.4. do-while

La construction d'un boucle do-while est

 
Sélectionnez
do
    instruction
 while(expression);

la boucle do-while est différente du while parce que l'instruction est exécutée au moins une fois, même si l'expression est évaluée à fausse dès la première fois. Dans une boucle while ordinaire, si l'expression conditionnelle est fausse à la première évaluation, l'instruction n'est jamais exécutée.

Si on utilise un do-while dans notre Guess.cpp, la variable guess n'a pas besoin d'une valeur initiale factice, puisqu'elle est initialisée par l'instruction cin avant le test :

 
Sélectionnez
//: C03:Guess2.cpp
// Le programme de devinette avec un do-while
#include <iostream>
using namespace std;
 
int main() {
  int secret = 15;
  int guess; // Pas besoin d'initialisation
  do {
    cout << "Devinez le nombre : ";
    cin >> guess; // L'initialisation s'effectue
  }   while(guess != secret);
  cout << "Vous l'avez trouvé!" << endl;
} ///:~

Pour des raisons diverses, la plupart des programmeurs tend à éviter l'utilisation du do-while et travaille simplement avec un while.

3.2.5. for

Une boucle for permet de faire une initialisation avant la première itération. Ensuite elle effectue un test conditionnel et, à la fin de chaque itération, une forme de “saut”. La construction de la boucle for est :

 
Sélectionnez
for(initialisation; condition; saut)
    instruction

chacune des expressions initialisation, condition, ou saut peut être laissée vide. l' initialisation est exécutée une seule fois au tout début. La condition est testée avant chaque itération (si elle est évaluée à fausse au début, l'instruction ne s'exécutera jamais). A la fin de chaque boucle, le saut s'exécute.

Une boucle for est généralement utilisée pour “compter” des tâches:

 
Sélectionnez
//: C03:Charlist.cpp
// Affiche tous les caractères ASCII
// Demontre "for"
#include <iostream>
using namespace std;
 
int main() {
  for(int i = 0; i < 128; i = i + 1)
    if (i != 26)  // Caractère ANSI d'effacement de l'écran
      cout << " valeur : " << i 
           << " caractère : " 
           << char(i) // Conversion de type
           << endl;
} ///:~

Vous pouvez noter que la variable i n'est définie qu'a partir de là où elle est utilisée, plutôt qu'au début du block dénoté par l'accolade ouvrante ‘ {'. Cela change des langages procéduraux traditionnels (incluant le C), qui requièrent que toutes les variables soient définies au début du bloc. Ceci sera discuté plus loin dans ce chapitre.

3.2.6. Les mots clé break et continue

Dans le corps de toutes les boucles while, do-while, ou for, il est possible de contrôler le déroulement de l'exécution en utilisant break et continue. break force la sortie de la boucle sans exécuter le reste des instructions de la boucle. continue arrête l'exécution de l'itération en cours et retourne au début de la boucle pour démarrer une nouvelle itération.

Pour illustrer break et continue, le programme suivant est un menu système tres simple :

 
Sélectionnez
//: C03:Menu.cpp
// Démonstration d'un simple menu système
// the use of "break" and "continue"
#include <iostream>
using namespace std;
 
int main() {
  char c; // Pour capturer la réponse
  while(true) {
    cout << "MENU PRINCIPAL :" << endl;
    cout << "g : gauche, d : droite, q : quitter -> ";
    cin >> c;
    if(c == 'q')
      break; // Out of "while(1)"
    if(c == 'g') {
      cout << "MENU DE GAUCHE :" << endl;
      cout << "sélectionnez a ou b : ";
      cin >> c;
      if(c == 'a') {
        cout << "vous avez choisi 'a'" << endl;
        continue; // Retour au menu principal
      }
      if(c == 'b') {
        cout << "vous avez choisi 'b'" << endl;
        continue; // Retour au menu principal
      }
      else {
        cout << "vous n'avez choisi ni a ni b !"
             << endl;
        continue; // Retour au menu principal
      }
    }
    if(c == 'd') {
      cout << "MENU DE DROITE:" << endl;
      cout << "sélectionnez c ou d : ";
      cin >> c;
      if(c == 'c') {
        cout << "vous avez choisi 'c'" << endl;
        continue; // Retour au menu principal
      }
      if(c == 'd') {
        cout << "vous avez choisi 'd'" << endl;
        continue; // Retour au menu principal
      }
      else {
        cout << "vous n'avez choisi ni c ni d !" 
             << endl;
        continue; // Retour au menu principal
      }
    }
    cout << "vous devez saisir g, d ou q !" << endl;
  }
  cout << "quitte le menu..." << endl;
} ///:~

Si l'utilisateur sélectionne ‘q' dans le menu principal, le mot clé break est utilisé pour quitter, sinon, le programme continue normalement son exécution indéfiniment. Après chaque sélection dans un sous-menu, le mot clé continue est utilisé pour remonter au début de la boucle while.

l'instruction while(true) est équivalente à dire “exécute cette boucle infiniment”. L'instruction break vous autorise à casser cette boucle sans fin quant l'utilisateur saisi un ‘q'.

3.2.7. switch

Une instruction switch effectue un choix parmi une sélection de blocs de code basé sur la valeur d'une expression intégrale. Sa construction est de la forme :

 
Sélectionnez
switch(sélecteur) {
    case valeur-intégrale1 : instruction; break;
    case valeur-intégrale2 : instruction; break;
    case valeur-intégrale3 : instruction; break;
    case valeur-intégrale4 : instruction; break;
    case valeur-intégrale5 : instruction; break;
    (...)
    default: instruction;
}

Le sélecteur est une expression qui produit une valeur entière. Le switch compare le résultat du sélecteur avec chaque valeur entière. si il trouve une valeur identique, l'instruction correspondante (simple ou composée) est exécutée. Si aucune correspondance n'est trouvée, l'instruction default est exécutée.

Vous remarquerez dans la définition ci-dessus que chaque case se termine avec un break, ce qui entraine l'exécution à sauter à la fin du corps du switch(l'accolade fermante qui complete le switch). Ceci est la manière conventionnelle de construire un switch, mais le break est facultatif. S'il est omis, votre case“s'étend” au suivant. Ainsi, le code du prochain case s'exécute jusqu'à ce qu'un break soit rencontré. Bien qu'un tel comportement ne soit généralement pas désiré, il peut être très utile à un programmeur expérimenté.

L'instruction switch est un moyen clair pour implémenter un aiguillage (i.e., sélectionner parmi un nombre de chemins d'exécution différents), mais elle requiert un sélecteur qui s'évalue en une valeur intégrale au moment de la compilation. Si vous voulez utiliser, par exemple, un objet string comme sélecteur, cela ne marchera pas dans une instruction switch. Pour un sélecteur de type string, vous devez utiliser à la place une série d'instructions if et le comparer à la string de l'expression conditionnelle.

L'exemple du menu précédent un particulièrement bon exemple pour utiliser un switch:

 
Sélectionnez
//: C03:Menu2.cpp
// Un menu utilisant un switch
#include <iostream>
using namespace std;
 
int main() {
  bool quit = false;  // Flag pour quitter
  while(quit == false) {
    cout << "Sélectionnez a, b, c ou q pour quitter: ";
    char reponse;
    cin >> response;
    switch(reponse) {
      case 'a' : cout << "vous avez choisi 'a'" << endl;
                 break;
      case 'b' : cout << "vous avez choisi 'b'" << endl;
                 break;
      case 'c' : cout << "vous avez choisi 'c'" << endl;
                 break;
      case 'q' : cout << "quittte le menu" << endl;
                 quit = true;
                 break;
      default  : cout << "sélectionnez a,b,c ou q !"
                 << endl;
    }
  }
} ///:~

Le flag quit est un bool, raccourci pour “Booléen,” qui est un type que vous ne trouverez qu'en C++. Il ne peut prendre que les valeurs des mots clé true ou false. Sélectionner ‘q' met le flag quit à true. A la prochaine évaluation du sélecteur, quit == false retourne false donc le corps de la boucle while ne s'exécute pas.

3.2.8. Du bon et du mauvais usage du goto

Le mot-clé goto est supporté en C++, puisqu'il existe en C. Utiliser goto dénote souvent un style de programmation pauvre, et ca l'est réellement la plupart du temps. Chaque fois que vous utilisez goto, regardez votre code, et regardez s'il n'y a pas une autre manière de le faire. A de rares occasions, vous pouvez découvrir que le goto peut résoudre un problème qui ne peut être résolu autrement, mais encore, pensez y à deux fois. Voici un exemple qui pourrait faire un candidat plausible :

 
Sélectionnez
//: C03:gotoKeyword.cpp
// L'infâme goto est supporté en C++
#include <iostream>
using namespace std;
 
int main() {
  long val = 0;
  for(int i = 1; i < 1000; i++) {
    for(int j = 1; j < 100; j += 10) {
      val = i * j;
      if(val > 47000)
        goto bas; 
        // Break serait remonté uniquement au 'for' extérieur
    }
  }
  bas: // une étiquette
  cout << val << endl;
} ///:~

Une alternative serait de définir un booléen qui serait testé dans la boucle for extérieure, qui le cas échéant exécuterait un break. Cependant, si vous avez plusieurs boucles for ou while imbriquées, cela pourrait devenir maladroit.

3.2.9. Récursion

La récursion une technique de programmation interressante et quelque fois utile par laquelle vous appelez la fonction dans laquelle vous êtes. Bien sûr, si vous ne faites que cela, vous allez appeler la fonction jusqu'à ce qu'il n'y ait plus de mémoire, donc vous devez fournir une “issue de secours” aux appels récursifs. Dans l'exemple suivant, cette “issue de secours” est réalisée en disant simplement que la récursion ira jusqu'à ce que cat dépasse ‘Z' : (31)

 
Sélectionnez
//: C03:CatsInHats.cpp
// Simple demonstration de récursion
#include <iostream>
using namespace std;
 
void retirerChapeau(char cat) {
  for(char c = 'A'; c < cat; c++)
    cout << "  ";
  if(cat <= 'Z') {
    cout << "cat " << cat << endl;
    retirerChapeau(cat + 1); // appel récursif
  } else
    cout << "VOOM !!!" << endl;
}
 
int main() {
  retirerChapeau('A');
} ///:~

Dans retirerChapeau( ), vous pouvez voir que tant que cat est plus petit que ‘Z', retirerChapeau( ) sera appelé depuis l'intérieur de retirerChapeau( ), d'où la récursion. Chaque fois que retirerChapeau( ) est appelé, sont paramètre est plus grand de un par rapport à la valeur actuelle de cat donc le paramètre continue d'augmenter.

La récursion est souvent utilisée pour résoudre des problèmes d'une complexité arbitraire, comme il n'y a pas de limite particulière de “taille” pour la solution – la fonction peut continuer sa récursion jusqu'à résolution du problème.

3.3. Introduction aux operateurs

Vous pouvez penser aux opérateurs comme un type spécial de fonction (vous allez apprendre que le C++ traite la surchage d'opérateurs exactement de cette façon). Un opérateur prend un ou plusieurs arguments et retourne une nouvelle valeur. Les arguments sont sous une forme différente des appels de fonction ordinaires, mais le résultat est identique.

De part votre expérience de programmation précédente, vous devriez être habitué aux opérateurs qui ont été employés jusqu'ici. Les concepts de l'addition ( +), de la soustraction et du moins unaire ( -), de la multiplication (*), de la division (/), et de l'affectation (=) ont tous essentiellement la même signification dans n'importe quel langage de programmation. L'ensemble complet des opérateurs est détaillé plus tard dans ce chapitre.

3.3.1. Priorité

La priorité d'opérateur définit l'ordre dans lequel une expression est évaluée quand plusieurs opérateurs différents sont présents. Le C et le C++ ont des règles spécifiques pour déterminer l'ordre d'évaluation. Le plus facile à retenir est que la multiplication et la division se produisent avant l'addition et soustraction. Si, après cela, une expression n'est pas claire pour vous, elle ne le sera probablement pas pour n'importe qui d'autre lisant le code, aussi, vous devriez utiliser des parenthèses pour rendre l'ordre d'évaluation explicite. Par exemple :

 
Sélectionnez
A = X + Y - 2/2 + Z;

a une signification très différente de le même instruction avec un groupe particulier de parenthèses

 
Sélectionnez
A = X + (Y - 2)/(2 + Z);

(Essayez d'évaluer le résultat avec X = 1, Y = 2, and Z = 3.)

3.3.2. Auto incrémentation et décrémentation

Le C, et donc le C++, sont pleins des raccourcis. Les raccourcis peuvent rendre le code beaucoup plus facile à écrire et parfois plus difficile à lire. Peut-être les concepteurs du langage C ont-ils pensé qu'il serait plus facile de comprendre un morceau de code astucieux si vos yeux ne devaient pas balayer une large zone d'affichage.

L'un des raccourcis les plus intéressants sont les opérateurs d'auto-incrémentation et d'auto-décrementation. On emploie souvent ces derniers pour modifier les variables de boucle, qui commandent le nombre d'exécution d'une boucle.

L'opérateur d'auto-décrementation est ' --' et veut dire “ diminuer d'une unité. ” l'opérateur d'auto-incrémentation est le ' ++' et veut dire “augmentation d'une unité.” Si A est un int, par exemple, l'expression ++A est équivalente à ( A = A + 1). Les opérateurs Auto-incrémentation et auto-décrementation produisent comme résultat la valeur de la variable. Si l'opérateur apparaît avant la variable, (c.-à-d., ++A), l'opération est effectuée d'abord puis la valeur résultante est produite. Si l'opérateur apparaît après la variable (c.-à-d. A++), la valeur courante est produite, puis l'opération est effectuée. Par exemple :

 
Sélectionnez
//: C03:AutoIncrement.cpp
// montre l'utilisation des operateurs d'auto-incrémentation
// et auto-décrementation .
#include <iostream>
using namespace std;
 
int main() {
  int i = 0;
  int j = 0;
  cout << ++i << endl; // Pre-incrementation
  cout << j++ << endl; // Post-incrementation
  cout << --i << endl; // Pre-décrementation
  cout << j-- << endl; // Post décrementation
} ///:~

Si vous vous êtes déjà interrogés sur le mot “C++,“ maintenant vous comprenez. Il signifie “une étape au delà de C. “

3.4. Introduction aux types de données

Les types de données définissent la façon dont vous utilisez le stockage (mémoire) dans les programmes que vous écrivez. En spécifiant un type de données, vous donnez au compilateur la manière de créer un espace de stockage particulier ainsi que la façon de manipuler cet espace.

Les types de données peuvent être intégrés ou abstraits. Un type de données intégré est compris intrinsèquement par le compilateur, et codé directement dans le compilateur. Les types de données intégrés sont quasiment identiques en C et en C++. À l'opposé, un type défini par l'utilisateur correspond à une classe créée par vous ou par un autre programmeur. On les appelle en général des types de données abstraits. Le compilateur sait comment gérer les types intégrés lorsqu'il démarre ; il « apprend » à gérer les types de données abstraits en lisant les fichiers d'en-tête contenant les déclarations des classes (vous étudierez ce sujet dans les chapitres suivants).

3.4.1. Types intégrés de base

La spécification du C standard (dont hérite le C++) pour les types intégrés ne mentionne pas le nombre de bits que chaque type intégré doit pouvoir contenir. À la place, elle stipule les valeurs minimales et maximales que le type intégré peut prendre. Lorsqu'une machine fonctionne en binaire, cette valeur maximale peut être directement traduite en un nombre minimal de bits requis pour stocker cette valeur. Cependant, si une machine utilise, par exemple, le système décimal codé en binaire (BCD) pour représenter les nombres, la quantité d'espace nécessaire pour stocker les nombres maximum de chaque type sera différente. Les valeurs minimales et maximales pouvant être stockées dans les différents types de données sont définis dans les fichiers d'en-tête du système limits.h et float.h(en C++ vous incluerez généralement climits et cfloat à la place).

Le C et le C++ ont quatre types intégrés de base, décrits ici pour les machines fonctionnant en binaire. Un char est fait pour le stockage des caractères et utilise au minimum 8 bits (un octet) de stockage, mais peut être plus grand. Un int stocke un nombre entier et utilise au minumum deux octets de stockage. Les types float et double stockent des nombres à virgule flottante, habituellement dans le format IEEE. float est prévu pour les flottants simple précision et double est prévu pour les flottants double précision.

Comme mentionné précédemment, vous pouvez définir des variables partout dans une portée, et vous pouvez les définir et les initialiser en même temps. Voici comment définir des variables utilisant les quatre types de données de base :

 
Sélectionnez
//: C03:Basic.cpp
// Utilisation des quatre 
// types de données de base en C en C++
 
int main() {
  // Définition sans initialisation :
  char proteine;
  int carbohydrates;
  float fibre;
  double graisse;
  // Définition & initialisation simultannées :
  char pizza = 'A', soda = 'Z';
  int machin = 100, truc = 150, 
    chose = 200;
  float chocolat = 3.14159;
  // Notation exponentielle :
  double ration_de_creme = 6e-4; 
} ///:~

La première partie du programme définit des variables des quatre types de données de base sans les initialiser. Si vous n'initialisez pas une variable, le standard indique que son contenu n'est pas défini (ce qui signifie en général qu'elle contient n'importe quoi). La seconde partie du programme définit et initialise en même temps des variables (c'est toujours mieux, si possible, de donner une valeur initiale au moment de la définition). Notez l'utilisation de la notation exponentielle dans la constant 6e-4, signifiant « 6 fois 10 puissance -4 »

3.4.2. bool, true, & false

Avant que bool fasse partie du C++ standard, tout le monde avait tendance à utiliser des techniques différentes pour obtenir un comportement booléen. Ces techniques causaient des problèmes de portabilité et pouvait introduire des erreurs subtiles.

Le type bool du C++ standard possède deux états, exprimés par les constantes intégrées true(qui est convertie en l'entier 1) et false(qui est convertie en l'entier 0). De plus, certains éléments du langage ont été adaptés :

Élément Utilisation avec bool
&& || ! Prend des arguments bool et retourn un résultat bool.
< > <= >= == != Produit des résultats en bool.
if, for, while, do Les expressions conditionnelles sont converties en valeurs bool.
? : La première opérande est convertie en valeur bool.

Comme il existe une grande quantité de code qui utilise un int pour représenter un marqueur, le compilateur convertira implicitement un int en bool(les valeurs non nulles produisent true tandis que les valeurs nulles produisent false). Idéalement, le compilateur vous avertira pour vous suggérer de corriger cette situation.

Un idiome, considéré comme un « mauvais style de programmation », est d'utiliser ++ pour mettre la valeur d'un marqueur à vrai. Cet idiome est encore autorisé, mais déprécié, ce qui signifie qu'il deviendra illégal dans le futur. Le problème vient du fait que vous réalisez une conversion implicite de bool vers int en incrémentant la valeur (potentiellement au-delà de l'intervalle normal des valeurs de bool, 0 et 1), puis la convertissez implicitement dans l'autre sens.

Les pointeurs (qui seront introduits plus tard dans ce chapitre) sont également convertis en bool lorsque c'est nécessaire.

3.4.3. Spécificateurs

Les spécificateurs modifient la signification des types intégrés de base et les étendent pour former un ensemble plus grand. Il existe quatre spécificateurs : long, short, signed et unsigned.

long et short modifient les valeurs maximales et minimales qu'un type de données peut stocker. Un int simple doit être au moins de la taille d'un short. La hiérarchie des tailles des types entier est la suivante : short int, int, long int. Toutes les tailles peuvent être les mêmes, tant qu'elles respectent les conditions sur les valeurs minimales et maximales. Sur une machine avec des mots de 64 bits, par exemple, tous les types de données peuvent être longs de 64 bits.

La hiérarchie des tailles pour les nombres à virgule flottante est : float, double et long double. « long float » n'est pas un type légal. Il n'y a pas de flottants short.

Les spécificateurs signed et unsigned donnent au compilateur la manière de traiter le bit de signe des types entiers et des caractères (les nombres à virgule flottante ont toujours un signe). Un nombre unsigned n'a pas de signe et a donc un bit en plus de disponible ; il peut ainsi stocker des nombres positifs deux fois plus grands que les nombres positifs qui peuvent être stockés dans un nombre signed. signed est implicite, sauf pour char; char peut être implicitement signé ou non. En spécifiant signed char, vous forcez l'utilisation du bit de signe.

L'exemple suivant montre les tailles en octet des types de données en utilisant l'opérateur sizeof, introduit plus tard dans ce chapitre :

 
Sélectionnez
//: C03:Specify.cpp
// Montre l'utilisation des spécificateurs
#include <iostream>
using namespace std;
 
int main() {
  char c;
  unsigned char cu;
  int i;
  unsigned int iu;
  short int is;
  short iis; // Même chose que short int
  unsigned short int isu;
  unsigned short iisu;
  long int il;
  long iil;  // Même chose que long int
  unsigned long int ilu;
  unsigned long iilu;
  float f;
  double d;
  long double ld;
  cout 
    << "\n char= " << sizeof(c)
    << "\n unsigned char = " << sizeof(cu)
    << "\n int = " << sizeof(i)
    << "\n unsigned int = " << sizeof(iu)
    << "\n short = " << sizeof(is)
    << "\n unsigned short = " << sizeof(isu)
    << "\n long = " << sizeof(il) 
    << "\n unsigned long = " << sizeof(ilu)
    << "\n float = " << sizeof(f)
    << "\n double = " << sizeof(d)
    << "\n long double = " << sizeof(ld) 
    << endl;
} ///:~

Notez que les résultats donnés par ce programme seront probablement différents d'une machine à l'autre, car (comme mentionné précédemment), la seule condition qui doit être respectée est que chaque type puisse stocker les valeurs minimales et maximales spécifiées dans le standard.

Lorsque vous modifiez un int par short ou par long, le mot-clé int est facultatif, comme montré ci-dessus.

3.4.4. Introduction aux pointeurs

À chaque fois que vous lancez un programme, il est chargé dans la mémoire de l'ordinateur (en général depuis le disque). Ainsi, tous les éléments du programme sont situés quelque part dans la mémoire. La mémoire est généralement arrangée comme une suite séquentielle d'emplacements mémoire ; nous faisons d'habitude référence à ces emplacements par des octets de huit bits, mais la taille de chaque espace dépend en fait de l'architecture particulière d'une machine et est en général appelée la taille du mot de cette machine. Chaque espace peut être distingué de façon unique de tous les autres espaces par son adresse. Au cours de cette discussion, nous considérerons que toutes les machines utilisent des octets qui ont des adresses séquentielles commençant à zéro et s'étendant jusqu'à la fin de la mémoire disponible dans l'ordinateur.

Puisque votre programme réside en mémoire au cours de son exécution, chaque élément du programme a une adresse. Supposons que l'on démarre avec un programme simple :

 
Sélectionnez
//: C03:YourPets1.cpp
#include <iostream>
using namespace std;
 
int chien, chat, oiseau, poisson;
 
void f(int animal) {
  cout << "identifiant de l'animal : " << animal << endl;
}
 
int main() {
  int i, j, k;
} ///:~

Chaque élément de ce programme se voit attribuer un emplacement mémoire à l'exécution du programme. Même la fonction occupe de la place mémoire. Comme vous le verrez, il s'avère que la nature d'un élément et la façon dont vous le définissez détermine en général la zone de mémoire dans laquelle cet élément est placé.

Il existe un opérateur en C et et C++ qui vous donne l'adresse d'un élément. Il s'agit de l'opérateur ‘ &'. Tout ce que vous avez à faire est de faire précéder le nom de l'identifiant par ‘ &' et cela produira l'adresse de cet identifiant. YourPets.cpp peut être modifié pour afficher l'adresse de tous ses éléments, de cette façon :

 
Sélectionnez
//: C03:YourPets2.cpp
#include <iostream>
using namespace std;
 
int chien, chat, oiseau, poisson;
 
void f(int pet) {
  cout << "identifiant de l'animal : " << pet << endl;
}
 
int main() {
  int i, j, k;
  cout << "f() : " << (long)&f << endl;
  cout << "chien : " << (long)&chien << endl;
  cout << "chat : " << (long)&chat << endl;
  cout << "oiseau : " << (long)&oiseau << endl;
  cout << "poisson : " << (long)&poisson << endl;
  cout << "i : " << (long)&i << endl;
  cout << "j : " << (long)&j << endl;
  cout << "k : " << (long)&k << endl;
} ///:~

L'expression (long) est une conversion. Cela signifie « ne considère pas ceci comme son type d'origine, considère le comme un long». La conversion n'est pas obligatoire, mais si elle n'était pas présente, les adresses auraient été affichées en hexadécimal, et la conversion en long rend les choses un peu plus lisibles.

Les résultats de ce programme varient en fonction de votre ordinateur, de votre système et d'autres facteurs, mais ils vous donneront toujours des informations intéressantes. Pour une exécution donnée sur mon ordinateur, les résultats étaient les suivants :

 
Sélectionnez
f(): 4198736
chien  : 4323632
chat : 4323636
oiseau : 4323640
poisson : 4323644
i: 6684160
j: 6684156
k: 6684152

Vous pouvez remarquer que les variables définies dans main( ) sont dans une zone différente des variables définies en dehors de main( ); vous comprendrez la raison en apprenant plus sur ce langage. De plus, f( ) semble être dans sa propre zone ; en mémoire, le code est généralement séparé des données.

Notez également que les variables définies l'une après l'autre semblent être placées séquentiellement en mémoire. Elles sont séparées par le nombre d'octets dicté par leur type de donnée. Ici, le seul type utilisé est int, et chat est à quatre octets de chien, oiseau est à quatre octets de chat, etc. Il semble donc que, sur cette machine, un int est long de quatre octets.

En plus de cette expérience intéressante montrant l'agencement de la mémoire, que pouvez-vous faire avec une adresse ? La chose la plus importante que vous pouvez faire est de la stocker dans une autre variable pour vous en servir plus tard. Le C et le C++ ont un type spécial de variable pour contenir une adresse. Cette variable est appelée un pointeur.

L'opérateur qui définit un pointeur est le même que celui utilisé pour la multiplication, ‘ *'. Le compilateur sait que ce n'est pas une multiplication grace au contexte dans lequel il est utilisé, comme vous allez le voir.

Lorsque vous définissez un pointeur, vous devez spécifier le type de variable sur lequel il pointe. Vous donnez d'abord le nom du type, puis, au lieu de donner immédiatement un identifiant pour la variable, vous dites « Attention, c'est un pointeur » en insérant une étoile entre le type et l'identifiant. Un pointeur sur un int ressemble donc à ceci :

 
Sélectionnez
int* ip; // ip pointe sur une variable de type int

L'association de l'opérateur ‘ *' a l'air raisonnable et se lit facilement mais peut induire en erreur. Vous pourriez être enclins à penser à « pointeurSurInt » comme un type de données distinct. Cependant, avec un int ou un autre type de données de base, il est possible d'écrire :

 
Sélectionnez
int a, b, c;

tandis qu'avec un pointeur, vous aimeriez écrire :

 
Sélectionnez
int* ipa, ipb, ipc;

La syntaxe du C (et par héritage celle du C++) ne permet pas ce genre d'expressions intuitives. Dans les définitions ci-dessus, seul ipa est un pointeur, tandis que ipb et ipc sont des int ordinaires (on peut dire que « * est lié plus fortement à l'identifiant »). Par conséquent, les meilleurs résultats sont obtenus en ne mettant qu'une définition par ligne ; vous obtiendrez ainsi la syntaxe intuitive sans la confusion :

 
Sélectionnez
int* ipa;
int* ipb;
int* ipc;

Comme une recommendation générale pour la programmation en C++ est de toujours initialiser une variable au moment de sa définition, cette forme fonctionne mieux. Par exemple, les variables ci-dessus ne sont pas initialisées à une valeur particulière ; elles contiennent n'importe quoi. Il est plus correct d'écrire quelque chose du genre :

 
Sélectionnez
int a = 47;
int* ipa = &a;

De cette façon, a et ipa ont été initialisés, et ipa contient l'adresse de a.

Une fois que vous avez un pointeur initialisé, son utilisation la plus élémentaire est de modifier la valeur sur laquelle il pointe. Pour accéder à une variable par un pointeur, on déréférence le pointeur en utilisant le même opérateur que pour le définir, de la façon suivante :

 
Sélectionnez
*ipa = 100;

Maintenant, a contient la valeur 100 à la place de 47.

Vous venez de découvrir les bases des pointeurs : vous pouvez stocker une adresse et utiliser cette adresse pour modifier la variable d'origine. Une question reste en suspens : pourquoi vouloir modifier une variable en utilisant une autre variable comme intermédiaire ?

Dans le cadre de cette introduction aux pointeurs, on peut classer la réponse dans deux grandes catégories :

  1. Pour changer des « objets extérieurs » depuis une fonction. Ceci est probablement l'usage le plus courant des pointeurs et va être présenté maintenant.
  2. Pour d'autres techniques de programmation avancées, qui seront présentées en partie dans le reste de ce livre.

3.4.5. Modification d'objets extérieurs

Habituellement, lorsque vous passez un argument à une fonction, une copie de cet argument est faite à l'intérieur de la fonction. Ceci est appelé le passage par valeur. Vous pouvez en voir les effets dans le programme suivant :

 
Sélectionnez
//: C03:PassByValue.cpp
#include <iostream>
using namespace std;
 
void f(int a) {
  cout << "a = " << a << endl;
  a = 5;
  cout << "a = " << a << endl;
}
 
int main() {
  int x = 47;
  cout << "x = " << x << endl;
  f(x);
  cout << "x = " << x << endl;
} ///:~

Dans f( ), a est une variable locale, elle n'existe donc que durant l'appel à la fonction f( ). Comme c'est un argument de fonction, la valeur de a est initialisée par les arguments qui sont passés lorsque la fonction est appelée ; dans main( ) l'argument est x, qui a une valeur de 47, et cette valeur est copiée dans a lorsque f( ) est appelée.

En exécutant ce programme, vous verrez :

 
Sélectionnez
x = 47
a = 47
a = 5
x = 47

La valeur initiale de x est bien sûr 47. Lorsque f() est appelée, un espace temporaire est créé pour stocker la variable a pour la durée de l'appel de fonction, et a est initialisée en copiant la valeur de x, ce qui est vérifié par l'affichage. Bien sûr, vous pouvez changer la valeur de a et montrer que cette valeur a changé. Mais lorsque f( ) se termine, l'espace temporaire qui a été créé pour a disparait, et on s'aperçoit que la seule connexion qui existait entre a et x avait lieu lorsque la valeur de x était copiée dans a.

À l'intérieur de f( ), x est l'objet extérieur (dans ma terminologie) et, naturellement, une modification de la variable locale n'affecte pas l'objet extérieur, puisqu'ils sont à deux emplacements différents du stockage. Que faire si vous voulez modifier un objet extérieur ? C'est là que les pointeurs se révèlent utiles. D'une certaine manière, un pointeur est un synonyme pour une autre variable. En passant un pointeur à une fonction à la place d'une valeur ordinaire, nous lui passons un synonyme de l'objet extérieur, permettant à la fonction de modifier cet objet, de la façon suivante :

 
Sélectionnez
//: C03:PassAddress.cpp
#include <iostream>
using namespace std;
 
void f(int* p) {
  cout << "p = " << p << endl;
  cout << "*p = " << *p << endl;
  *p = 5;
  cout << "p = " << p << endl;
}
 
int main() {
  int x = 47;
  cout << "x = " << x << endl;
  cout << "&x = " << &x << endl;
  f(&x);
  cout << "x = " << x << endl;
} ///:~

De cette façon, f( ) prend un pointeur en argument, et déréférence ce pointeur pendant l'affectation, ce qui cause la modification de l'objet extérieur x. Le résultat est :

 
Sélectionnez
x = 47
&ax = 0065FE00
p = 0065FE00
*p = 47
p = 0065FE00
x = 5

Notez que la valeur contenue dans p est la même que l'adresse de x– le pointeur p pointe en effet sur x. Si cela n'est pas suffisamment convaincant, lorsque p est déréférencé pour lui affecter la valeur 5, nous voyons que la valeur de x est également changée en 5.

Par conséquent, passer un pointeur à une fonction permet à cette fonction de modifier l'objet extérieur. Vous découvrirez beaucoup d'autres utilisations pour les pointeurs par la suite, mais ceci est sans doute la plus simple et la plus utilisée.

3.4.6. Introduction aux références en C++

Les pointeurs fonctionnent globalement de la même façon en C et en C++, mais le C++ ajoute une autre manière de passer une adresse à une fonction. Il s'agit du passage par référence, qui existe dans plusieurs autres langages, et n'est donc pas une invention du C++.

Votre première impression sur les références peut être qu'elles sont inutiles, et que vous pourriez écrire tous vos programmes sans références. En général ceci est vrai, à l'exception de quelques cas importants présentés dans la suite de ce livre. Vous en apprendrez également plus sur les références plus tard, mais l'idée de base est la même que pour la démonstration sur l'utilisation des pointeurs ci-dessus : vous pouvez passer l'adresse d'un argument en utilisant une référence. La différence entre les références et les pointeurs est que l' appel d'une fonction qui prend des références est plus propre au niveau de la syntaxe que celui d'une fonction qui prend des pointeurs (et c'est cette même différence syntaxique qui rend les références indispensables dans certaines situations). Si PassAddress.cpp est modifié pour utiliser des références, vous pouvez voir la différence d'appel de fonction dans main( ):

 
Sélectionnez
//: C03:PassReference.cpp
#include <iostream>
using namespace std;
 
void f(int& r) {
  cout << "r = " << r << endl;
  cout << "&r = " << &r << endl;
  r = 5;
  cout << "r = " << r << endl;
}
 
int main() {
  int x = 47;
  cout << "x = " << x << endl;
  cout << "&x = " << &x << endl;
  f(x); // Ressemble à un passage par valeur
        // c'est en fait un passage par référence
  cout << "x = " << x << endl;
} ///:~

Dans la liste d'arguments de f( ), à la place d'écrire int* pour passer un pointeur, on écrit int& pour passer une référence. À l'intérieur de f( ), en écrivant simplement ‘ r' (ce qui donnerait l'adresse si r était un pointeur), vous récupérez la valeur de la variable que r référence . En affectant quelque chose à r, vous affectez cette chose à la variable que r référence. La seule manière de récupérer l'adresse contenue dans r est d'utiliser l'opérateur ‘ &'.

Dans main( ), vous pouvez voir l'effet principal de l'utilisation des références dans la syntaxe de l'appel à f( ), qui se ramène à f(x). Bien que cela ressemble à un passage par valeur ordinaire, la référence fait que l'appel prend l'adresse et la transmet, plutôt que de simplement copier la valeur. La sortie est :

 
Sélectionnez
x = 47
&x = 0065FE00
r = 47
&r = 0065FE00
r = 5
x = 5

Vous pouvez ainsi voir que le passage par référence permet à une fonction de modifier l'objet extérieur à l'instar d'un pointeur (vous pouvez aussi voir que la référence cache le passage de l'adresse, ceci sera examiné plus tard dans ce livre). Pour les besoins de cette introduction simple, vous pouvez considérer que les références ne sont qu'une autre syntaxe (ceci est parfois appelé « sucre syntactique ») pour réaliser ce que font les pointeurs : permettre aux fonctions de changer des objets extérieurs.

3.4.7. Pointeurs et références comme modificateurs

Jusqu'à maintenant, vous avez découvert les types de données de base char, int, float et double, ainsi que les spécificateurs signed, unsigned, short et long qui peuvent être utilisés avec les types de données de base dans de nombreuses combinaisons. Nous venons d'ajouter les pointeurs et les références, qui sont orthogonaux aux types de données de base et aux spécificateurs et qui donnent donc un nombre de combinaisons triplé :

 
Sélectionnez
//: C03:AllDefinitions.cpp
// Toutes les définitions possibles des types de données 
// de base, des spécificateurs, pointeurs et références
#include <iostream>
using namespace std;
 
void f1(char c, int i, float f, double d);
void f2(short int si, long int li, long double ld);
void f3(unsigned char uc, unsigned int ui, 
  unsigned short int usi, unsigned long int uli);
void f4(char* cp, int* ip, float* fp, double* dp);
void f5(short int* sip, long int* lip, 
  long double* ldp);
void f6(unsigned char* ucp, unsigned int* uip, 
  unsigned short int* usip, 
  unsigned long int* ulip);
void f7(char& cr, int& ir, float& fr, double& dr);
void f8(short int& sir, long int& lir, 
  long double& ldr);
void f9(unsigned char& ucr, unsigned int& uir, 
  unsigned short int& usir, 
  unsigned long int& ulir);
 
int main() {} ///:~

Les pointeurs et les références peuvent aussi être utilisés pour passer des objets dans une fonction et retourner des objets depuis une fonction ; ceci sera abordé dans un chapitre suivant.

Il existe un autre type fonctionnant avec les pointeurs : void. En écrivant qu'un pointeur est un void*, cela signifie que n'importe quel type d'adresse peut être affecté à ce pointeur (tandis que si avez un int*, vous ne pouvez affecter que l'adresse d'une variable int à ce pointeur). Par exemple :

 
Sélectionnez
//: C03:VoidPointer.cpp
int main() {
  void* vp;
  char c;
  int i;
  float f;
  double d;
  // L'adresse de n'importe quel type
  // peut être affectée à un pointeur void
  vp = &c;
  vp = &i;
  vp = &f;
  vp = &d;
} ///:~

Une fois que vous affectez une adresse à un void*, vous perdez l'information du type de l'adresse. Par conséquent, avant d'utiliser le pointeur, vous devez le convertir dans le type correct :

 
Sélectionnez
//: C03:CastFromVoidPointer.cpp
int main() {
  int i = 99;
  void* vp = &i;
  // On ne peut pas déréférencer un pointeur void
  // *vp = 3; // Erreur de compilation
  // Il faut le convertir en int avant de le déréférencer
  *((int*)vp) = 3;
} ///:~

La conversion (int*)vp dit au compilateur de traiter le void* comme un int*, de façon à ce qu'il puisse être déréférencé. Vous pouvez considérer que cette syntaxe est laide, et elle l'est, mais il y a pire – le void* crée un trou dans le système de types du langage. En effet, il permet, et même promeut, le traitement d'un type comme un autre type. Dans l'exemple ci-dessus, je traite un int comme un int en convertissant vp en int*, mais rien ne m'empèche de le convertir en char* ou en double*, ce qui modifierait une zone de stockage d'une taille différente que celle qui a été allouée pour l' int, faisant potientiellement planter le programme. En général, les pointeurs sur void devraient être évités et n'être utilisés que dans des cas bien précis que vous ne rencontrerez que bien plus tard dans ce livre.

Vous ne pouvez pas créer de références sur void, pour des raisons qui seront expliquées au chapitre 11.

3.5. Portée des variables

Les règles de portée d'une variable nous expliquent la durée de validité d'une variable, quand elle est créée, et quand elle est détruite (i.e.: lorsqu'elle sort de la portée). La portée d'une variable s'étend du point où elle est définie jusqu'à la première accolade "fermante" qui correspond à la plus proche accolade "ouvrante" précédant la définition de la variable. En d'autres termes, la portée est définie par le plus proche couple d'accolades entourant la variable. L'exemple qui suit, illustre ce sujet:

 
Sélectionnez
//: C03:Scope.cpp
// Portée des variables
int main() {
  int scp1;
  // scp1 est utilisable ici
  {
    // scp1 est encore utilisable ici
    //.....
    int scp2;
    // scp2 est utilisable ici
    //.....
    {
      // scp1 & scp2 sont toujours utilisables ici
      //..
      int scp3;
      // scp1, scp2 & scp3 sont utilisables ici
      // ...
    } // <-- scp3 est détruite ici
    // scp3 n'est plus utilisable ici
    // scp1 & scp2 sont toujours utilisables ici
    // ...
  } // <-- scp2 est détruite ici
  // scp3 & scp2 ne sont plus utilisables ici
  // scp1 est toujours utilisable ici
  //..
} // <-- scp1 est détruite ici
///:~

L'exemple ci-dessus montre quand les variables sont utilisables (on parle aussi de visibilité) et quand elles ne sont plus utilisables (quand elles sortent de la portée). Une variable ne peut être utilisée qu'à l'intérieur de sa portée. Les portées peuvent être imbriquées, indiquées par une paire d'accolades à l'intérieur d'autres paires d'accolades. Imbriqué veut dire que vous pouvez accéder à une variable se trouvant dans la portée qui englobe la portée dans laquelle vous vous trouvez. Dans l'exemple ci-dessus, la variable scp1 est utilisable dans toutes les portées alors que la variable scp3 n'est utilisable que dans la portée la plus imbriquée.

3.5-bis. Définir des variables "à la volée"

Comme expliqué plus tôt dans ce chapitre, il y a une différence significative entre C et C++ dans la définition des variables. Les deux langages requièrent que les variables soient définies avant qu'elles ne soient utilisées, mais C (et beaucoup d'autres langages procéduraux) vous oblige à définir toutes les variables en début de portée, ainsi lorsque le compilateur crée un bloc, il peut allouer la mémoire pour ces variables.

Quand on lit du code C, un bloc de définition de variables est habituellement la première chose que vous voyez quand vous entrez dans une portée. Déclarer toutes les variables au début du bloc demande, de la part du programmeur, d'écrire d'une façon particulière, à cause des détails d'implémentation du langage. La plupart des programmeurs ne savent pas quelles variables vont être utilisées avant d'écrire le code, ainsi ils doivent remonter au début du bloc pour insérer de nouvelles variables ce qui est maladroit et source d'erreurs. Ces définitions de variables en amont ne sont pas très utiles pour le lecteur, et elles créent la confusion parce qu'elles apparaissent loin du contexte où elles sont utilisées.

C++ (mais pas C) vous autorise à définir une variable n'importe où dans la portée, ainsi vous pouvez définir une variable juste avant de l'utiliser. De plus, vous pouvez initialiser la variable lors de sa définition, ce qui évite un certain type d'erreur. Définir les variables de cette façon, rend le code plus facile à écrire et réduit les erreurs que vous obtenez quand vous effectuez des allers-retours dans la portée. Le code est plus facile à comprendre car vous voyez la définition d'une variable dans son contexte d'utilisation. Ceci est particulièrement important quand vous définissez et initialisez une variable en même temps - vous pouvez comprendre la raison de cette initialisation grâce à la façon dont cette variable est utilisée.

Vous pouvez aussi définir les variables à l'intérieur des expressions de contrôle de boucle for ou de boucle while, à l'intérieur d'un segment conditionnel if et à l'intérieur d'une sélection switch.Voici un exemple de définition de variables "à la volée":

 
Sélectionnez
//: C03:OnTheFly.cpp
// Définitions de variables à la volée
#include <iostream>
using namespace std;
 
int main() {
  //..
  { // Commence une nouvelle portée
    int q = 0; // C demande les définitions de variables ici
    //..
    // Définition à l'endroit de l'utilisation
    for(int i = 0; i < 100; i++) { 
      q++; // q provient d'une portée plus grande
      // Définition à la fin d'une portée
      int p = 12; 
    }
    int p = 1;  // Un p différent
  } // Fin de la portée contenant q et le p extérieur
  cout << "Tapez un caractère:" << endl;
  while(char c = cin.get() != 'q') {
    cout << c << " n'est ce pas ?" << endl;
    if(char x = c == 'a' || c == 'b')
      cout << "Vous avez tapé a ou b" << endl;
    else
      cout << "Vous avez tapé" << x << endl;
  }
  cout << "Tapez A, B, ou C" << endl;
  switch(int i = cin.get()) {
    case 'A': cout << "Snap" << endl; break;
    case 'B': cout << "Crackle" << endl; break;
    case 'C': cout << "Pop" << endl; break;
    default: cout << "Ni A, B ou C!" << endl;
  }
} ///:~

Dans la portée la plus intérieure, p est défini juste avant la fin de la portée, c'est réellement sans intérêt ( mais cela montre que vous pouvez définir une variable n'importe où). Le p de la portée extérieure est dans la même situation.

La définition de i dans l'expression de contrôle de la boucle for est un exemple de la possibilité de définir une variable exactement à l'endroit où vous en avez besoin (ceci n'est possible qu'en C++). La portée de i est la portée de l'expression contrôlée par la boucle for, ainsi vous pouvez réutiliser i dans une prochaine boucle for. Ceci est pratique et communément utilisé en C++ : i est un nom de variable classique pour les compteurs de boucle et vous n'avez pas besoin d'inventer de nouveaux noms.

Bien que l'exemple montre également la définition de variables dans les expressions while, if et switch, ce type de définition est moins courant, probablement parce que la syntaxe est contraignante. Par exemple, vous ne pouvez pas mettre de parenthèses. Autrement dit, vous ne pouvez pas écrire :

 
Sélectionnez
while((char c = cin.get()) != 'q')

L'addition de parenthèses supplémentaires peut sembler innocent et efficace, mais vous ne pouvez pas les utiliser car les résultats ne sont pas ceux escomptés. Le problème vient du fait que ' !=' a une priorité supérieure à ' =', ainsi le char c renvoie un bool convertit en char. A l'écran, sur de nombreux terminaux, vous obtiendrez un caractère de type "smiley".

En général, vous pouvez considérer que cette possibilité de définir des variables dans les expressions while, if et switch n'est là que pour la beauté du geste mais vous utiliserez ce type de définition dans une boucle for(où vous l'utiliserez très souvent).

3.6. Définir l'allocation mémoire

Quand vous créez une variable, vous disposez de plusieurs options pour préciser sa durée de vie, comment la mémoire est allouée pour cette variable, et comment la variable est traitée par le compilateur.

3.6.1. Variables globales

Les variables globales sont définies hors de tout corps de fonction et sont disponibles pour tous les éléments du programme (même le code d'autres fichiers). Les variables globales ne sont pas affectées par les portées et sont toujours disponibles (autrement dit, une variable globale dure jusqu'à la fin du programme). Si une variable globale est déclarée dans un fichier au moyen du mot-clé extern et définie dans un autre fichier, la donnée peut être utilisée par le second fichier. Voici un exemple d'utilisation de variables globales :

 
Sélectionnez
//: C03:Global.cpp
//{L} Global2
// Exemple de variables globales
#include <iostream>
using namespace std;
 
int globe;
void func();
int main() {
  globe = 12;
  cout << globe << endl;
  func(); // Modifies globe
  cout << globe << endl;
} ///:~

Ici un fichier qui accède à globe comme un extern:

 
Sélectionnez
//: C03:Global2.cpp {O}
// Accès aux variables globales externes
extern int globe;  
// (The linker resolves the reference)
void func() {
  globe = 47;
} ///:~

Le stockage de la variable globe est créé par la définition dans Global.cpp, et le code dans Global2.cpp accède à cette même variable. Comme le code de Global2.cpp est compilé séparément du code de Global.cpp, le compilateur doit être informé que la variable existe ailleurs par la déclaration

 
Sélectionnez
extern int globe;

A l'exécution du programme, vous verrez que, de fait, l'appel à func( ) affecte l'unique instance globale de globe.

Dans Global.cpp, vous pouvez voir la balise de commentaire spéciale (qui est de ma propre conception):

 
Sélectionnez
//{L} Global2

Cela dit que pour créer le programme final, le fichier objet Global2 doit être lié (il n'y a pas d'extension parce que l'extension des fichiers objets diffère d'un système à l'autre). Dans Global2.cpp, la première ligne contient aussi une autre balise de commentaire spéciale {O}, qui dit "n'essayez pas de créer un exécutable à partir de ce fichier, il est en train d'être compilé afin de pouvoir être lié dans un autre exécutable." Le programme ExtractCode.cpp dans le deuxième volume de ce livre (téléchargeable à www.BruceEckel.com) lit ces balises et crée le makefile approprié afin que tout se compile proprement (vous étudierez les makefile s à la fin de ce chapitre).

3.6.2. Variables locales

Les variables locales existent dans un champ limité ; elles sont "locales" à une fonction. Elle sont souvent appelées variables automatiques parce qu'elles sont créés automatiquement quand on entre dans le champ et disparaissent automatiquement quand le champ est fermé. Le mot clef auto rend la chose explicite, mais les variables locales sont par défaut auto afin qu'il ne soit jamais nécessaire de déclarer quelque chose auto.

Variables de registre

Une variable de registre est un type de variable locale. Le mot clef register dit au compilateur "rend l'accès à cette donnée aussi rapide que possible". L'accroissement de la vitesse d'accès aux données dépend de l'implémentation, mais, comme le suggère le nom, c'est souvent fait en plaçant la variable dans un registre. Il n'y a aucune garantie que la variable sera placée dans un registre ou même que la vitesse d'accès sera augmentée. C'est une suggestion au compilateur.

Il y a des restrictions à l'usage des variables de registre. Vous ne pouvez pas prendre ou calculer leur adresse. Elles ne peuvent être déclarées que dans un bloc (vous ne pouvez pas avoir de variables de registre globales ou static). Toutefois, vous pouvez utiliser une variable de registre comme un argument formel dans une fonction (i.e., dans la liste des arguments).

En général, vous ne devriez pas essayer de contrôler l'optimiseur du compilateur, étant donné qu'il fera probablement un meilleur travail que vous. Ainsi, il vaut mieux éviter le mot-clef register.

3.6.3. static

Le mot-clef static a différentes significations. Normalement, les variables définies dans une fonction disparaissent à la fin de la fonction. Quand vous appelez une fonction à nouveau, l'espace de stockage pour la variable est recréé et les valeurs ré-initialisées. Si vous voulez qu'une valeur soit étendue à toute la durée de vie d'un programme, vous pouvez définir la variable locale d'une fonction static et lui donner une valeur initiale. L'initialisation est effectuée uniquement la première fois que la fonction est appelée, et la donnée conserve sa valeur entre les appels à la fonction. Ainsi, une fonction peut "se souvenir" de morceaux d'information entre les appels.

Vous pouvez vous demander pourquoi une variable globale n'est pas utilisée à la place ? La beauté d'une variable static est qu'elle est indisponible en dehors du champ de la fonction et ne peut donc être modifiée par inadvertance. Ceci localise les erreurs.

Voici un exemple d'utilisation des variables static:

 
Sélectionnez
//: C03:Static.cpp
// Utiliser une variable static dans une fonction
#include <iostream>
using namespace std;
 
void func() {
  static int i = 0;
  cout << "i = " << ++i << endl;
}
 
int main() {
  for(int x = 0; x < 10; x++)
    func();
} ///:~

A chaque fois que func( ) est appelée dans la boucle for, elle imprime une valeur différente. Si le mot-clef static n'est pas utilisé, la valeur utilisée sera toujours ‘1'.

Le deuxième sens de static est relié au premier dans le sens “indisponible en dehors d'un certain champ”. Quand static est appliqué au nom d'une fonction ou à une variable en dehors de toute fonction, cela signifie “Ce nom est indisponible en dehors de ce fichier.” Le nom de la focntion ou de la variable est local au fichier ; nous disons qu'il a la portée d'un fichier. Par exemple, compiler et lier les deux fichiers suivants causera une erreur d'édition de liens :

 
Sélectionnez
//: C03:FileStatic.cpp
// Démonstration de la portée à un fichier. Compiler et
// lier ce fichier avec FileStatic2.cpp
// causera une erreur d'éditeur de liens
 
// Portée d'un fichier signifie disponible seulement dans ce fichier :
static int fs; 
 
int main() {
  fs = 1;
} ///:~

Même si la variable fs est déclaré exister comme une extern dan le fichier suivant, l'éditeur de liens ne la trouvera pas parce qu'elle a été déclarée static dans FileStatic.cpp.

 
Sélectionnez
//: C03:FileStatic2.cpp {O}
// Tentative de référencer fs
extern int fs;
void func() {
  fs = 100;
} ///:~

Le mot-clef static peut aussi être utilisé dans une classe. Ceci sera expliqué plus loin, quand vous aurez appris à créer des classes.

3.6.4. extern

Le mot-clef extern a déjà été brièvement décrit et illustré. Il dit au compilateur qu'une variable ou une fonction existe, même si le compilateur ne l'a pas encore vu dans le fichier en train d'être compilé. Cette variable ou cette fonction peut être définie dans un autre fichier ou plus loin dans le même fichier. Comme exemple du dernier cas :

 
Sélectionnez
//: C03:Forward.cpp
// Fonction forward & déclaration de données
#include <iostream>
using namespace std;
 
// Ce n'est pas vraiment le cas externe, mais il
// faut dire au compilateur qu'elle existe quelque part :
extern int i; 
extern void func();
int main() {
  i = 0;
  func();
}
int i; // Création de la donnée
void func() {
  i++;
  cout << i;
} ///:~

Quand le compilateur rencontre la déclaration ‘ extern int i', il sait que la définition de i doit exister quelque part comme variable globale. Quand le compilateur atteint la définition de i, il n'y a pas d'autre déclaration visible, alors il sait qu'il a trouvé le même i déclaré plus tôt dans le fichier. Si vous définissiez istatic, vous diriez au compilateur que i est défini globalement (via extern), mais qu'il a aussi une portée de fichier (via static), et le compilateur génèrera une erreur.

Edition de lien

Pour comprendre le comportement des programmes en C et C++, vous devez connaître l'édition de liens ( linkage). Dans un programme exécutable un identifiant est représenté par un espace mémoire qui contient une variable ou le corps d'une fonction compilée. L'édition de liens décrit cet espace comme il est vu par l'éditeur de liens ( linker). Il y a deux types d'édition de liens : l'édition de liens interne et externe.

L'édition de liens interne signifie que l'espace mémoire est créé pour représenter l'identifiant seulement pour le fichier en cours de compilation. D'autres fichiers peuvent utiliser le même nom d'identifiant avec l'édition interne de liens, ou pour une variable globale, et aucun conflit ne sera détecté par l'éditeur de liens – un espace différent est créé pour chaque identifiant. L'édition de liens interne est spécifiée par le mot-clef static en C et C++.

L'édition de liens externe signifie qu'un seul espace de stockage est créé pour représenter l'identifiant pour tous les fichiers compilés. L'espace est créé une fois, et l'éditeur de liens doit assigner toutes les autres références à cet espace. Les variables globales et les noms de fonctions ont une édition de liens externe. Ceux-ci sont atteints à partir des autres fichiers en les déclarant avec le mot-clef extern. Les variables définies en dehors de toute fonction (à l'exception de cont en C++) et les définitions de fonctions relèvent par défaut de l'édition de liens externe. Vous pouvez les forcer spécifiquement à avoir une édition interne de liens en utilisant le mot-clef static. Vous pouvez déclarer explicitement qu'un identifiant a une édition de liens externe en le définissant avec le mot-clef extern. Définir une variable ou une fonction avec extern n'est pas nécessaire en C, mais c'est parfois nécessaire pour const en C++.

Les variables (locales) automatiques existent seulement temporairement, sur la pile, quand une fonction est appelée. L'éditeur de liens ne connaît pas les variables automatiques, et celles-ci n'ont donc pas d'édition de liens.

3.6.5. Constantes

Dans l'ancien C (pré-standard), si vous vouliez créer une constante, vous deviez utiliser le préprocesseur :

 
Sélectionnez
#define PI 3.14159

Partout où vous utilisiez PI, la valeur 3.14159 était substitué par le préprocesseur (vous pouvez toujours utiliser cette méthode en C et C++).

Quand vous utilisez le préprocesseur pour créer des constantes, vous placez le contrôle de ces constantes hors de la portée du compilateur. Aucune vérification de type n'est effectuée sur le nom PI et vous ne pouvez prendre l'adresse de PI(donc vous ne pouvez pas passer un pointeur ou une référence à PI). PI ne peut pas être une variable d'un type défini par l'utlisateur. Le sens de PI dure depuis son point de définition jusqu'à la fin du fichier ; le préprocesseur ne sait pas gérer la portée.

C++ introduit le concept de constante nommée comme une variable, sauf que sa valeur ne peut pas être changée. Le modificateur const dit au compilateur qu'un nom représente une constante. N'importe quel type de données, prédéfini ou défini par l'utilisateur, peut être défini const. Si vous définissez quelque chose const et essayez ensuite de le modifier, le compilateur génère une erreur.

Vous devez définir le type de const, ainsi :

 
Sélectionnez
const int x = 10;

En C et C++ standard, vous pouvez utiliser une constante nommée dans une liste d'arguments, même si l'argument auquel il correspond est un pointeur ou un référence (i.e., vous pouvez prendre l'adresse d'une const). Une const a une portée, exactement comme une variable normale, vous pouvez donc "cacher" une const dans une fonction et être sûr que le nom n'affectera pas le reste du programme.

const a été emprunté au C++ et incorporé en C standard, mais de façon relativement différente. En C, le compilateur traite une const comme une variable qui a une étiquette attachée disant "Ne me changez pas". Quand vous définissez une const en C, le compilateur crée un espace pour celle-ci, donc si vous définissez plus d'une const avec le même nom dans deux fichiers différents (ou mettez la définition dans un fichier d'en-tête ( header)), l'éditeur de liens générera des messages d'erreur de conflits. L'usage voulu de const en C est assez différent de celui voulu en C++ (pour faire court, c'est plus agréable en C++).

Valeurs constantes

En C++, une const doit toujours avoir une valeur d'initialisation (ce n'est pas vrai en C). Les valeurs constantes pour les types prédéfinis sont les types décimal, octal, hexadécimal, nombres à virgule flottante ( floating-point numbers) (malheureusement, les nombres binaires n'ont pas été considérés importants), ou caractère.

En l'absence d'autre indication, le compilateur suppose qu'une valeur constante est un nombre décimal. Les nombres 47, 0 et 1101 sont tous traités comme des nombres décimaux.

Une valeur constante avec 0 comme premier chiffre est traitée comme un nombre octal (base 8). Les nombres en base 8 peuvent contenir uniquement les chiffres 0-7 ; le compilateur signale les autres chiffres comme des erreurs. Un nombre octal valide est 017 (15 en base 10).

Une valeur constante commençant par 0x est traitée comme un nombre hexadécimal (base 16). Les nombres en base 16 contiennent les chiffres 0 à 9 et les lettres A à F. Un nombre hexadécimal valide peut être 0x1fe (510 en base 10).

Les nombres à virgule flottante peuvent contenir un point décimal et une puissance exponentielle (représentée par e, ce qui veut dire "10 à la puissance"). Le point décimal et le e sont tous deux optionnels. Si vous assignez une constante à une variable en virgule flottante, le compilateur prendra la valeur constante et la convertira en un nombre à virgule flottante (ce procédé est une forme de ce que l'on appelle la conversion de type implicite). Toutefois, c'est une bonne idée d'utiliser soit un point décimal ou un e pour rappeler au lecteur que vous utilisez un nombre à virgule flottante ; des compilateurs plus anciens ont également besoin de cette indication.

Les valeurs constantes à virgule flottante valides sont : 1e4, 1.0001, 47.0, 0.0, et -1.159e-77. Vous pouvez ajouter des suffixes pour forcer le type de nombre à virgule flottante : f ou F force le type float, L ou l force le type longdouble; autrement le nombre sera un double.

Les caractères constants sont des caractères entourés par des apostrophes, comme : ‘ A', ‘ 0', ‘ ‘. Remarquer qu'il y a une grande différence entre le caractère ‘ 0' (ASCII 96) et la valeur 0. Des caractères spéciaux sont représentés avec un échappement avec “backslash”: ‘ \n' (nouvelle ligne), ‘ \t' (tabulation), ‘ \\' (backslash), ‘ \r' (retour chariot), ‘ "' (guillemets), ‘ '' (apostrophe), etc. Vous pouvez aussi exprimer les caractères constants en octal : ‘ \17' ou hexadecimal : ‘ \xff'.

3.6.6. volatile

Alors que la déclaration const dit au compilateur “Cela ne change jamais” (ce qui permet au compilateur d'effectuer des optimisations supplémentaires), la déclaration volatile dit au compilateur “On ne peut pas savoir quand cela va changer” et empêche le compilateur d'effectuer des optimisations basée sur la stabilité de cette variable. Utilisez ce mot-clef quand vous lisez une valeur en dehors du contrôle de votre code, comme un registre dans une partie de communication avec le hardware. Une variable volatile est toujours lue quand sa valeur est requise, même si elle a été lue à la ligne précédente.

Un cas spécial d'espace mémoire étant “en dehors du contrôle de votre code” est dans un programme multithreadé. Si vous utilisez un flag modifié par une autre thread ou process, ce flag devrait être volatile afin que le compilateur ne suppose pas qu'il peut optimiser en négligeant plusieurs lectures des flags.

Notez que volatile peut ne pas avoir d'effet quand un compilateur n'optimise pas, mais peut éviter des bugs critiques quand vous commencez à optimiser le code (c'est alors que le compilateur commencera à chercher des lectures redondantes).

Les mots-clefs const et volatile seront examinés davantage dans un prochain chapitre.

3.7. Operateurs et leurs usages

Cette section traite de tous les opérateurs en C et en C++.

Tous les opérateurs produisent une valeur à partir de leurs opérandes. Cette valeur est produite sans modifier les opérandes, excepté avec les opérateurs d'affectation, d'incrémentation et de décrémentation. Modifier un opérande est appelé effet secondaire. L'usage le plus commun pour les opérateurs est de modifier ses opérandes pour générer l'effet secondaire, mais vous devez garder à l'esprit que la valeur produite est disponible seulement pour votre usage comme dans les opérateurs sans effets secondaires.

3.7.1. L'affectation

L'affectation est éxécutée par l'opérateur =. Il signifie “Prendre le côté droit (souvent appelé la valeur droite ou rvalue) et la copier dans le côté gauche (souvent appelé la valeur gauche ou lvalue).” Une valeur droite est une constante, une variable, ou une expression produisant une valeur, mais une valeur gauche doit être distinguée par un nom de variable (c'est-à-dire, qu'il doit y avoir un espace physique dans lequel on stocke les données). Par exemple, vous pouvez donner une valeur constante à une variable ( A = 4;), mais vous ne pouvez rien affecter à une valeur constante – Elle ne peut pas être une valeur l (vous ne pouvez pas écrire 4 = A;).

3.7.2. Opérateurs mathématiques

Les opérateurs mathématiques de base sont les mêmes que dans la plupart des langages de programmation: addition ( +), soustraction ( -), division ( /), multiplication ( *), et modulo ( %; qui retourne le reste de la division entière). La division de entière tronque le résultat (ne provoque pas un arrondi). L'opérateur modulo ne peut pas être utilisé avec des nombres à virgule.

C et C++ utilisent également une notation condensée pour exécuter une opération et une affectation en même temps. On le note au moyen d'un opérateur suivi par le signe égal, et est applicable avec tous les opérateurs du langage (lorsque ceci à du sens). Par exemple, pour ajouter 4 à une variable x et affecter x au résultat, vous écrivez: x += 4;.

Cet exemple montre l'utilisation des opérateurs mathématiques:

 
Sélectionnez
//: C03:Mathops.cpp
// Opérateurs mathématiques
#include <iostream>
using namespace std;
 
// Une macro pour montrer une chaîne de caractères et une valeur.
#define PRINT(STR, VAR) \
  cout << STR " = " << VAR << endl
 
int main() {
  int i, j, k;
  float u, v, w;  // Appliquable aussi aux doubles
  cout << "saisir un entier: ";
  cin >> j;
  cout << "saisissez un autre entier: ";
  cin >> k;
  PRINT("j",j);  PRINT("k",k);
  i = j + k; PRINT("j + k",i);
  i = j - k; PRINT("j - k",i);
  i = k / j; PRINT("k / j",i);
  i = k * j; PRINT("k * j",i);
  i = k % j; PRINT("k % j",i);
  // La suite ne fonctionne qu'avec des entiers:
  j %= k; PRINT("j %= k", j);
  cout << "saisissez un nombre à virgule: ";
  cin >> v;
  cout << "saisissez un autre nombre à virgule:";
  cin >> w;
  PRINT("v",v); PRINT("w",w);
  u = v + w; PRINT("v + w", u);
  u = v - w; PRINT("v - w", u);
  u = v * w; PRINT("v * w", u);
  u = v / w; PRINT("v / w", u);
  // La suite fonctionne pour les entiers, les caractères,
  // et les types double aussi:
  PRINT("u", u); PRINT("v", v);
  u += v; PRINT("u += v", u);
  u -= v; PRINT("u -= v", u);
  u *= v; PRINT("u *= v", u);
  u /= v; PRINT("u /= v", u);
} ///:~

La valeur droite de toutes les affectations peut, bien sûr, être beaucoup plus complexe.

Introduction aux macros du préprocesseur

Remarquez l'usage de la macro PRINT( ) pour économiser de la frappe(et les erreurs de frappe!). Les macros pour le préprocesseur sont traditionnellement nommées avec toutes les lettres en majuscules pour faire la différence – vous apprendrez plus tard que les macros peuvent vite devenir dangereuses (et elles peuvent aussi être très utiles).

Les arguments dans les parenthèses suivant le nom de la macro sont substitués dans tout le code suivant la parenthèse fermante. Le préprocesseur supprime le nom PRINT et substitue le code partout où la macro est appelée, donc le compilateur ne peut générer aucun message d'erreur utilisant le nom de la macro, et il ne peut vérifier les arguments (ce dernier peut être bénéfique, comme dans la macro de débogage à la fin du chapitre).

3.7.3. Opérateurs relationnels

Les opérateurs relationnels établissent des relations entre les valeurs des opérandes. Ils produisent un booléen (spécifié avec le mot-clé bool en C++) true si la relation est vraie, et false si la relation est fausse. Les opérateurs relationnels sont: inférieur à ( <), supérieur à ( >), inférieur ou égal à ( <=), supérieur ou égal à ( >=), équivalent ( ==), et non équivalent ( !=). Ils peuvent être utilisés avec tous les types de données de base en C et en C++. Ils peuvent avoir des définitions spéciales pour les types de données utilisateurs en C++ (vous l'apprendrez dans le chapitre 12, dans la section surcharge des opérateurs).

3.7.4. Opérateurs logiques

Les opérateurs logiques et( &&) et ou( ||) produisent vrai ou faux selon les relations logiques de ses arguments. Souvenez vous qu'en C et en C++, une expression est true si elle a une valeur non-égale à zéro, et false si elle a une valeur à zéro. Si vous imprimez un bool, vous verrez le plus souvent un ' 1' pour true et ‘ 0' pour false.

Cet exemple utilise les opérateurs relationnels et les opérateurs logiques:

 
Sélectionnez
//: C03:Boolean.cpp
// Opérateurs relationnels et logiques.
#include <iostream>
using namespace std;
 
int main() {
  int i,j;
  cout << "Tapez un entier: ";
  cin >> i;
  cout << "Tapez un autre entier: ";
  cin >> j;
  cout << "i > j is " << (i > j) << endl;
  cout << "i < j is " << (i < j) << endl;
  cout << "i >= j is " << (i >= j) << endl;
  cout << "i <= j is " << (i <= j) << endl;
  cout << "i == j is " << (i == j) << endl;
  cout << "i != j is " << (i != j) << endl;
  cout << "i && j is " << (i && j) << endl;
  cout << "i || j is " << (i || j) << endl;
  cout << " (i < 10) && (j < 10) is "
       << ((i < 10) && (j < 10))  << endl;
} ///:~

Vous pouvez remplacer la définition de int avec float ou double dans le programme précédent. Soyez vigilant cependant, sur le fait que la comparaison d'un nombre à virgule avec zéro est stricte; Un nombre, aussi près soit-il d'un autre, est toujours ”non égal.” Un nombre à virgule qui est le plus petit possible est toujours vrai.

3.7.5. Opérateurs bit à bit

Les opérateurs bit à bit vous permettent de manipuler individuellement les bits dans un nombre (comme les valeurs à virgule flottante utilisent un format interne spécifique, les opérateurs de bits travaillent seulement avec des types entiers: char, int et long). Les opérateurs bit à bit exécutent l'algèbre booléene sur les bits correspondant dans les arguments pour produire le résultat.

L'opérateur bit à bit et( &) donne 1 pour bit se sortie si les deux bits d'entrée valent 1; autrement il produit un zéro. L'opérateur bit à bit ou ( |) produit un Un sur la sortie si l'un ou l'autre bit est un Un et produit un zéro seulement si les deux bits d'entrés sont à zéro. L'opérateur bit à bit ou exclusif, ou xor( ^) produit un Un dans le bit de sortie si l'un ou l'autre bit d'entré est à Un, mais pas les deux. L'opérateur bit à bit non( ~, aussi appelé le complément de Un) est un opérateur unaire – Il prend seulement un argument (tous les autres opérateurs bit à bit sont des opérateurs binaires). L'opérateur bit à bit non produit l'opposé du bit d'entrée – un Un si le bit d'entré est zéro, un zéro si le bit d'entré est Un.

Les opérateurs bit à bit peuvent être combinés avec le signe = pour regrouper l'opération et l'affectation: &=, |=, et ^= sont tous des opérations légitimes (comme ~ est un opérateur unitaire, il ne peut pas être combiné avec le signe =).

3.7.6. Opérateurs de décalage

Les opérateurs de décalages manipulent aussi les bits. L'opérateur de décalage à gauche ( <<) retourne l'opérande situé à gauche de l'opérateur décalé vers la gauche du nombre de bits spécifié après l'opérateur. L'opérateur de décalage à droite ( >>) retourne l'opérande situé à gauche de l'opérateur décalé vers la droite du nombre de bits spécifié après l'opérateur. Si la valeur après l'opérateur de décalage est supérieure au nombre de bits de l'opérande de gauche, le résultat est indéfini. Si l'opérande de gauche est non signée, le décalage à droite est un décalage logique donc les bits supérieurs seront remplis avec des zéros. Si l'opérande de gauche est signé, le décalage à droite peut être ou non un décalage logique ( c'est-à-dire, le comportement est non défini).

Les décalages peuvent être combinés avec le signe ( <<= et >>=). La valeur gauche est remplacée par la valeur gauche décalé par la valeur droite.

Ce qui suit est un exemple qui démontre l'utilisation de tous les opérateurs impliquant les bits. D'abord, voici une fonction d'usage universel qui imprime un octet dans le format binaire, créée séparément de sorte qu'elle puisse être facilement réutilisée. Le fichier d'en-tête déclare la fonction :

 
Sélectionnez
//: C03:printBinary.h
// imprime un bit au format binaire
void printBinary(const unsigned char val);
///:~

Ici; ce trouve l'implémentation de la fonction:

 
Sélectionnez
//: C03:printBinary.cpp {O}
#include <iostream>
void printBinary(const unsigned char val) {
  for(int i = 7; i >= 0; i--)
    if(val & (1 << i))
      std::cout << "1";
    else
      std::cout << "0";
} ///:~

La fonction printBinary( ) prend un simple octet et l'affiche bit par bit. L'expression

 
Sélectionnez
(1 &lt;&lt; i)

produit un Un successivement dans chaque position; en binaire: 00000001, 00000010, etc. Si on fait un et bit à bit avec val et que le résultat est non nul, cela signifie qu'il y avait un Un dans cette position en val.

Finalement, la fonction est utilisée dans l'exemple qui montre la manipulation des opérateurs de bits:

 
Sélectionnez
//: C03:Bitwise.cpp
//{L} printBinary
// Démonstration de la manipulation de bit
#include "printBinary.h"
#include <iostream>
using namespace std;
 
// Une macro pour éviter de la frappe
#define PR(STR, EXPR) \
  cout << STR; printBinary(EXPR); cout << endl;  
 
int main() {
  unsigned int getval;
  unsigned char a, b;
  cout << "Entrer un nombre compris entre 0 et 255: ";
  cin >> getval; a = getval;
  PR("a in binary: ", a);
  cout << "Entrer un nombre compris entre 0 et 255: ";
  cin >> getval; b = getval;
  PR("b en binaire: ", b);
  PR("a | b = ", a | b);
  PR("a & b = ", a & b);
  PR("a ^ b = ", a ^ b);
  PR("~a = ", ~a);
  PR("~b = ", ~b);
  // Une configuration binaire intéressante:
  unsigned char c = 0x5A; 
  PR("c en binaire: ", c);
  a |= c;
  PR("a |= c; a = ", a);
  b &= c;
  PR("b &= c; b = ", b);
  b ^= a;
  PR("b ^= a; b = ", b);
} ///:~

Une fois encore, une macro préprocesseur est utilisée pour économiser de la frappe. Elle imprime la chaîne de caractère de votre choix, puis la représentation binaire d'une expression, puis une nouvelle ligne.

Dans main( ), les variables sont unsigned. Parce que, en général, vous ne voulez pas de signe quand vous travaillez avec des octets. Un int doit être utilisé au lieu d'un char pour getval parce que l'instruction “ cin >>” va sinon traiter le premier chiffre comme un caractère. En affectant getval à a et b, la valeur est convertie en un simple octet(en le tronquant).

Les << et >> permettent d'effectuer des décalages de bits, mais quand ils décalent les bits en dehors de la fin du nombre, ces bits sont perdus. Il est commun de dire qu'ils sont tombés dans le seau des bits perdus, un endroit où les bits abandonnés finissent, vraisemblablement ainsi ils peuvent être réutilisés...). Quand vous manipulez des bits vous pouvez également exécuter une rotation, ce qui signifie que les bits qui sont éjectés d'une extrémité sont réinsérés à l'autre extrémité, comme s'ils faisait une rotation autour d'une boucle. Quoique la plupart des processeurs d'ordinateur produise une commande de rotation au niveau machine (donc vous pouvez voir cela dans un langage d'assembleur pour ce processeur), Il n'y a pas de support direct pour les “rotations” en C et C++. Vraisemblablement les concepteurs du C percevaient comme justifié de laisser les “rotations” en dehors (visant, d'après eux, un langage minimal) parce que vous pouvez construire votre propre commande de rotation. Par exemple, voici les fonctions pour effectuer des rotations gauches et droites:

 
Sélectionnez
//: C03:Rotation.cpp {O}
// effectuer des rotations gauches et droites
 
unsigned char rol(unsigned char val) {
  int highbit;
  if(val & 0x80) // 0x80 est le bit de poids fort seulement
    highbit = 1;
  else
    highbit = 0;
  // décalage à gauche (le bit de poids faible deviens 0):
  val <<= 1;
  // Rotation du bit de poids fort sur le bit de poids faible:
  val |= highbit;
  return val;
}
 
unsigned char ror(unsigned char val) {
  int lowbit;
  if(val & 1) // vérifie le bit de poids faible
    lowbit = 1;
  else
    lowbit = 0;
  val >>= 1; // décalage à droite par une position
  // Rotation du bit de poids faible sur le bit de poids fort:
  val |= (lowbit << 7);
  return val;
} ///:~

Essayez d'utiliser ces fonctions dans Bitwise.cpp. Noter que les définitions (ou au moins les déclarations) de rol( ) et ror( ) doivent être vues par le compilateur dans Bitwise.cpp avant que les fonctions ne soit utilisées.

Les fonctions bit à bit sont généralement extrêmement efficaces à utiliser parce qu'elles sont directement traduites en langage d'assembleur. Parfois un simple traitement en C ou C++ peut être généré par une simple ligne de code d'assembleur.

3.7.7. Opérateurs unaires

L'opérateur bit à bit not n'est pas le seul opérateur qui prend un argument unique. Son compagnon, le non logique( !), va prendre une valeur true et produire une valeur false. L'unaire moins ( -) et l'unaire plus ( +) sont les même opérateurs que les binaires moins et plus; le compilateur trouve quelle utilisation est demandée en fonction de la façon dout vous écrivez l'expression.Par exemple, le traitement

 
Sélectionnez
x = -a;

a une signification évidente. Le compilateur peut comprendre:

 
Sélectionnez
x = a * -b;

mais le lecteur peut être troublé, donc il est préférable d'écrire:

 
Sélectionnez
x = a * (-b);

L'unaire moins produit l'opposé de la valeur. L'unaire plus produit la symétrie avec l'unaire moins, bien qu'il ne fasse actuellement rien.

Les opérateurs d'incrémentation et de décrémentation ( ++ et --) ont été introduits plus tôt dans ce chapitre. Ils sont les seuls autres opérateurs hormis ceux impliquant des affectations qui ont des effets de bord. Ces opérateurs incrémentent et décrémentent la variable d'une unité, bien qu'une “unité” puisse avoir différentes significations selon le type de la donnée, c'est particulièrement vrai avec les pointeurs.

Les derniers opérateurs unaires sont adresse-de ( &),déréférence ( * et ->), et les opérateurs de transtypage en C et C++, et new et delete en C++. L'adresse-de et la déréférence sont utilisés avec les pointeurs, décrit dans ce chapitre. Le transtypage est décrit plus tard dans ce chapitre, et new et delete sont introduits dans ce chapitre 4.

3.7.8. L'opérateur ternaire

Le ternaire if-else est inhabituel parce qu'il a trois opérandes. C'est un vrai opérateur parce qu'il produit une valeur, à la difference de l'instruction ordinaire if-else. Il est composé de trois expressions: si la première expression (suivie par ?) est évaluée à vrai, l'expression suivant le ? est évaluée et son résultat devient la valeur produite par l'opérateur. Si la première expression est fausse, la troisième expression (suivant le :) est évaluée et le résultat devient la valeur produite par l'opérateur.

L'opérateur conditionnel peut être utilise pour son effet de bord ou pour la valeur qu'il produit. Voici un fragment de code qui démontre cela :

 
Sélectionnez
a = --b ? b : (b = -99);

Ici, la condition produit la valeur droite. a est affectée à la valeur de b si le résultat de la décrémentation de b n'est pas zéro. Si b devient zéro, a et b sont tous les deux assignés à -99. b est toujours décrémenté, mais il est assigné à -99 seulement si la décrémentation fait que b deviens 0. Un traitement similaire peut être utilisé sans le “ a =” juste pour l'effet de bord:

 
Sélectionnez
--b ? b : (b = -99);

Ici le second B est superflu, car la valeur produite par l'opérateur n'est pas utilisée. Une expression est requise entre le ? et le :. Dans ce cas, l'expression peut simplement être une constante qui va produire un code un peu plus rapide.

3.7.9. L'opérateur virgule

La virgule n'est pas restreinte à séparer les noms de variables dans les définitions multiples, comme dans

 
Sélectionnez
int i, j, k;

Bien sûr, c'est aussi utilisé dans les listes d'arguments de fonctions. Pourtant, il peut aussi être utilisé comme un opérateur pour séparer les expressions – dans ce cas cela produit seulement la valeur de la dernière expression. Toutes les autres expressions dans une liste séparée par des virgules sont évaluées seulement pour leur effet seondaire. Cet exemple incrémente une liste de variables et utilise la dernière comme la valeur droite:

 
Sélectionnez
//: C03:CommaOperator.cpp
#include <iostream>
using namespace std;
int main() {
  int a = 0, b = 1, c = 2, d = 3, e = 4;
  a = (b++, c++, d++, e++);
  cout << "a = " << a << endl;
  // Les parentheses sont obligatoires ici.
  // Sans celle ci, le traitement sera évalué par:
  (a = b++), c++, d++, e++;
  cout << "a = " << a << endl;
} ///:~

En général, il est préférable d'éviter d'utiliser la virgule comme autre chose qu'un séparateur, car personne n'a l'habitude de le voir comme un opérateur.

3.7.10. Piège classique quand on utilise les opérateurs

Comme illustré précédemment, un des pièges quand on utilise les opérateurs est d'essayer de se passer de parenthèses alors que vous n'êtes pas sûr de comment une expression va être évaluée (consulter votre manuel C pour l'ordre d'évaluation des expressions).

Une autre erreur extrêmement commune ressemble à ceci:

 
Sélectionnez
//: C03:Pitfall.cpp
// Erreur d'opérateur
 
int main() {
  int a = 1, b = 1;
  while(a = b) {
    // ....
  }
} ///:~

Le traitement a = b sera toujours évalué à vrai quand b n'est pas nul. La variable a est assignée à la valeur de b, et la valeur de b est aussi produite par l'opérateur =. En général, vous voulez utiliser l'opérateur d'équivalence == à l'intérieur du traitement conditionnel, et non l'affectation. Cette erreur est produite par un grand nombre de programmeurs (pourtant, certains compilateurs peuvent vous montrer le problème, ce qui est utile).

Un problème similaire est l'utilisation des opérateurs bit à bit and et or à la place des opérateurs logique associés. Les opérateurs bit à bit and et or utilise un des caractère ( & ou |), alors and et or logique utilisent deux ( && et ||). Tout comme = et ==, il est facile de taper un caractère à la place de deux. Un moyen mnémotechnique est d'observer que les “ bits sont plus petits, donc ils n'ont pas besoin de beaucoup de caractères dans leurs opérateurs.”

3.7.11. Opérateurs de transtypage

Le mot transtypage( cast en anglais, ndt) est utilisé dans le sens de “fondre dans un moule.” Le compilateur pourra automatiquement changer un type de donnée en un autre si cela a un sens. Par exemple, si vous affectez une valeur entière à une valeur à virgule, le compilateur fera secrètement appel a une fonction (ou plus probablement, insérera du code) pour convertir le int en un float. Transtyper vous permet de faire ce type de conversion explicitement, ou de le forcer quand cela ne se ferait pas normalement.

Pour accomplir un transtypage, mettez le type de donnée désiré (incluant tout les modifieurs) à l'intérieur de parenthèses à la gauche de la valeur. Cette valeur peut être une variable, une constante, la valeur produite par une expression, ou la valeur de retour d'une fonction. Voici un exemple :

 
Sélectionnez
//: C03:SimpleCast.cpp
int main() {
  int b = 200;
  unsigned long a = (unsigned long int)b;
} ///:~

Le transtypage est puissant, mais il peut causer des maux de tête parce que dans certaine situations il peut forcer le compilateur à traiter les données comme si elles étaient (par exemple) plus larges qu'elles ne le sont en réalité, donc cela peut occuper plus d'espace en mémoire ; et peut écraser d'autres données. Cela arrive habituellement quand un pointeur est transtypé, et non quand un simple transtypage est fait comme celui montré plus tôt.

C++ a une syntaxe de transtypage supplémentaire, qui suit la syntaxe d'appel de fonction. Cette syntaxe met des parenthèses autour de l'argument, comme un appel de fonction, plutôt qu'autour du type de la donnée :

 
Sélectionnez
//: C03:FunctionCallCast.cpp
int main() {
  float a = float(200);
  // Ceci est équivalent à:
  float b = (float)200;
} ///:~

Bien sûr dans le cas précédent vous ne pouvez pas réellement avoir besoin de transtypage; vous pouvez juste dire 200 . f ou 200.0f(en effet, c'est ce que le compilateur fera normalement pour l'expression précédente). Le transtypage est habituellement utilisé avec des variables, plutôt qu' avec les constantes.

3.7.12. Transtypage C++ explicite

Le transtypage doit être utilisé avec précaution, parce que ce que vous faites est de dire au compilateur “oublie le contrôle des types – traite le comme cet autre type à la place.” C'est à dire, vous introduisez une faille dans le système de types du C++ et empechez le compilateur de vous dire que vous êtes en train de faire quelque chose de mal avec ce type. Ce qui est pire, le compilateur vous croit implicitement et ne peut exécuter aucun autre contrôle pour détecter les erreurs. Une fois que vous commencez à transtyper, vous vous ouvrez à toutes sortes de problèmes. En fait, tout programme qui utilise beaucoup de transtypages doit être abordé avec suspicion, peut importe le nombre de fois où on vous dit que ça “doit” être fait ainsi.En général, les transtypages devraient être peu nombreux et réduits au traitement de problèmes spécifiques.

Une fois que vous avez compris cela et que vous êtes en leur présence dans un programme bogué, votre premier réflexe peut être de regarder les transtypages comme pouvant être les coupables. Mais comment localiser les transtypages du style C ? Ils ont simplement un nom de type entre parenthèses, et si vous commencez à chercher ces choses vous découvrirez que c'est souvent difficile de les distinguer du reste du code.

Le standard C++ inclut une syntaxe de transtypage explicite qui peut être utilisée pour remplacer complètement l'ancien style C de transtypage (bien sûr, les transtypages de style C ne peuvent pas être declarés hors la loi sans briser la compatibilité avec du code existant, mais les compilateurs peuvent facilement vous signaler un transtypage de l'ancien style). La syntaxe de transtypage explicite est ainsi faite que vous pouvez facilement la trouver, comme vous pouvez la voir par son nom :

static_cast Pour le “ comportement correct” du transtypage et “ le comportement raisonnable ” du transtypage, y compris les choses que vous devriez maintenant faire sans le transtypage (tel qu'un type de conversion automatique).
const_cast Pour transtyper les const et/ou les volatile.
reinterpret_cast Pour transtyper en quelque chose de complètement différent. La clé est que vous avez besoin de transtyper pour revenir dans le type original sans risques. Le type que vous transtypez est typiquement utilisé seulement pour manipuler des bits ou quelque autres buts mystérieux. C'est le plus dangereux de tout les transtypages.
dynamic_cast Pour un transtypage de type sûr vers le bas (ce transtypage sera décrit dans le chapitre 15).

Le trois premiers transtypages explicites seront décrits dans la prochaine section, alors que le dernier sera expliqué seulement après que vous en ayez appris plus, dans le chapitre 15.

static_cast

Un static_cast est utilisé pour toutes les conversions qui sont bien définies. Ceci inclut les conversions “sûres” que le compilateur peut vous autoriser de faire sans un transtypage et les conversions moins sûres qui sont néanmoins bien définies. Les types de conversions couverts par static_cast incluent typiquement les conversions de type sans danger (implicites), les conversions limitantes (pertes d'information), le forçage d'une conversion d'un void*, conversions implicite du type, et la navigation statique dans la hiérarchie des classes (comme vous n'avez pas vu les classes et l'héritage actuellement, ce dernier est repoussé au chapitre 15):

 
Sélectionnez
//: C03:static_cast.cpp
void func(int) {}
 
int main() {
  int i = 0x7fff; // Max pos value = 32767
  long l;
  float f;
  // (1) Conversion typique sans transtypage:
  l = i;
  f = i;
  // fonctionne aussi:
  l = static_cast<long>(i);
  f = static_cast<float>(i);
 
  // (2) conversion limitante:
  i = l; // Peut perdre des chiffres
  i = f; // Peut perdre des informations
  // Dis &#8220;Je sais,&#148; elimine les avertissements:
  i = static_cast<int>(l);
  i = static_cast<int>(f);
  char c = static_cast<char>(i);
 
  // (3) Forcer une conversion depuis un void* :
  void* vp = &i;
  // Ancienne forme: produit une conversion dangereuse:
  float* fp = (float*)vp;
  // La nouvelle façon est également dangereuse:
  fp = static_cast<float*>(vp);
 
  // (4) Conversion de type implicite, normalement
  // exécutée par le compilateur:
  double d = 0.0;
  int x = d; // Conversion de type automatique 
  x = static_cast<int>(d); // Plus explicite
  func(d); // Conversion de type automatique
  func(static_cast<int>(d)); // Plus explicite
} ///:~

Dans la section (1), vous pouvez voir le genre de conversion que vous utilisiez en C, avec ou sans transtypage. Promouvoir un int en un long ou float n'est pas un problème parce que ces derniers peuvent toujours contenir que qu'un int peut contenir. Bien que ce ne soit pas nécessaire, vous pouvez utiliser static_cast pour mettre en valeur cette promotion.

La conversion inverse est montrée dans (2). Ici, vous pouvez perdre des données parce que un int n'est pas “large” comme un long ou un float; ce ne sont pas des nombres de même taille. Ainsi ces convertions sont appelées conversions limitantes. Le compilateur peut toujours l'effectuer, mais peut aussi vous retourner un avertissement. Vous pouvez éliminer le warning et indiquer que vous voulez vraiment utiliser un transtypage.

L'affectation à partir d'un void* n'est pas permise sans un transtypage en C++ (à la différence du C), comme vu dans (3). C'est dangereux et ça requiert que le programmeur sache ce qu'il fait. Le static_cast, est plus facile à localiser que l'ancien standard de transtypage quand vous chassez les bugs.

La section (4) du programme montre le genre de conversions implicites qui sont normalement effectuées automatiquement par le compilateur. Celles-ci sont automatiques et ne requièrent aucun transtypage, mais à nuoveau un static_cast met en évidence l'action dans le cas où vous voudriez le faire apparaitre clairement ou le reperer plus tard.

const_cast

Si vous voulez convertir d'un const en un non const ou d'un volatile en un non volatile, vous utilisez const_cast. C'est la seule conversion autorisée avec const_cast; si une autre conversion est impliquée, il faut utiliser une expression séparée ou vous aurez une erreur de compilation.

 
Sélectionnez
//: C03:const_cast.cpp
int main() {
  const int i = 0;
  int* j = (int*)&i; // Obsolete
  j  = const_cast<int*>(&i); // A privilegier
  // Ne peut faire simultanément de transtypage additionnel:
//! long* l = const_cast<long*>(&i); // Erreur
  volatile int k = 0;
  int* u = const_cast<int*>(&k);
} ///:~

Si vous prenez l'adresse d'un objet const, vous produisez un pointeur sur un const, et il ne peut être assigné à un pointeur non const sans un transtypage. L'ancien style de transtypage peut l'accomplir, mais le const_cast est approprié pour cela. Ceci est vrai aussi pour un volatile.

reinterpret_cast

Ceci est le moins sûr des mécanismes de transtypage, et le plus apprecié pour faire des bugs. Un reinterpret_cast prétend qu'un objet est juste un ensemble de bit qui peut être traité (pour quelques obscures raisons) comme si c'était un objet d'un type entièrement différent. C'est le genre de bricolage de bas niveau qui a fait mauvaise réputation au C. Vous pouvez toujours virtuellement avoir besoin d'un reinterpret_cast pour retourner dans le type original de la donnée(ou autrement traiter la variable comme son type original) avant de faire quoi que ce soit avec elle.

 
Sélectionnez
//: C03:reinterpret_cast.cpp
#include <iostream>
using namespace std;
const int sz = 100;
 
struct X { int a[sz]; };
 
void print(X* x) {
  for(int i = 0; i < sz; i++)
    cout << x->a[i] << ' ';
  cout << endl << "--------------------" << endl;
}
 
int main() {
  X x;
  print(&x);
  int* xp = reinterpret_cast<int*>(&x);
  for(int* i = xp; i < xp + sz; i++)
    *i = 0;
  // Ne pas utiliser xp comme un X* à ce point
  // à moins de le retranstyper dans son état d'origine:
  print(reinterpret_cast<X*>(xp));
  // Dans cette exemple, vous pouvez aussi juste utiliser
  // l'identifiant original:
  print(&x);
} ///:~

Dans cet exemple simple, struct X contiens seulement un tableau de int, mais quand vous en créez un sur la pile comme dans X x, la valeur de chacun des int s est n'importe quoi (ceci est montré en utilisant la fonction print( ) pour afficher le contenu de la struct). Pour les initialiser , l'adresse de X est prise et transtypée en un pointeur de type int, le tableau est alors parcouru pour mettre chaque int à zéro. Notez comment la limite haute pour i est calculée par “l'addition” de sz avec xp; le compilateur sait que vous voulez actuellement sz positions au départ de xp et utilise l'arithmétique de pointeur pour vous.

L'idée du reinterpret_cast est que quand vous l'utilisez, ce que vous obtenez est à ce point différent du type original que vous ne pouvez l'utiliser comme tel à moins de le transtyper à nouveau. Ici, nous voyons le transtypage précédent pour un X* dans l'appel de print, mais bien sûr dès le début vous avez l'identifiant original que vous pouvez toujours utiliser comme tel. Mais le xp est seulement utile comme un int*, qui est vraiment une “réinterpretation” du X original.

Un reinterpret_cast peut aussi indiquer une imprudence et/ou un programme non portable, mais est disponible quand vous décidez que vous devez l'utiliser.

3.7.13. sizeof – Un opérateur par lui même

L'opérateur sizeof reste seul parce qu'il satisfait un besoin non usuel. sizeof vous donne des informations à propos de la quantité de mémoire allouée pour une donnée. Comme décrit plus tôt dans ce chapitre, sizeof vous dit le nombre d'octets utilisés par n'importe quelle variable. Il peut aussi donner la taille du type de la donnée (sans nom de variable):

 
Sélectionnez
//: C03:sizeof.cpp
#include <iostream>
using namespace std;
int main() {
  cout << "sizeof(double) = " << sizeof(double);
  cout << ", sizeof(char) = " << sizeof(char);
} ///:~

Avec la définition de sizeof tout type de char( signed, unsigned ou simple) est toujours un, sans se soucier du fait que le stockage sous-jacent pour un char est actuellement un octet. Pour tous les autres types, le résultat est la taille en octets.

Notez que sizeof est un opérateur, et non une fonction. Si vous l'appliquez à un type, il doit être utilisé avec les parenthèses comme vu précédemment, mais si vous l'appliquez à une variable vous pouvez l'utiliser sans les parenthèses:

 
Sélectionnez
//: C03:sizeofOperator.cpp
int main() {
  int x;
  int i = sizeof x;
} ///:~

sizeof peut aussi vous donnez la taille des données d'un type défini par l'utilisateur. C'est utilisé plus tard dans le livre.

3.7.14. Le mot clef asm

Ceci est un mécanisme d'échappement qui vous permet d'écrire du code assembleur pour votre matériel dans un programme C++. Souvent vous êtes capable de faire référence à des variables C++ dans le code assembleur, ceci signifie que vous pouvez facilement communiquer avec votre code C++ et limiter les instructions en code assembleur pour optimiser des performances ou pour faire appel à des instructions microprocesseur précises. La syntaxe exacte que vous devez utiliser quand vous écrivez en langage assembleur est dépendante du compilateur et peut être découverte dans la documentation de votre compilateur.

3.7.15. Opérateurs explicites

Ces mots clefs sont pour les opérateurs de bit et les opérateurs logiques. Les programmeurs non américains sans les caractères du clavier comme &, |, ^, et ainsi de suite, sont forcés d'utiliser les horribles trigraphes C, ce qui n'est pas seulement pénible, mais obscur à lire. Cela a été arrangé en C++ avec l'ajout des mots clefs :

Keyword Meaning
and &&(logical and)
or ||(logical or)
not !(logical NOT)
not_eq !=(logical not-equivalent)
bitand &(bitwise and)
and_eq &=(bitwise and-assignment)
bitor |(bitwise or)
or_eq |=(bitwise or-assignment)
xor ^(bitwise exclusive-or)
xor_eq ^=(bitwise exclusive-or-assignment)
compl ~(ones complement)

Si votre compilateur se conforme au standard C++, il supportera ces mots clefs.

3.8. Création de type composite

Les types de données fondamentaux et leurs variantes sont essentiels, bien que primitifs. C et C++ fournissent des outils qui vous autorisent à composer des types de données plus sophistiqués à partir des types fondamentaux. Comme vous le verrez, le plus important de ces types est struct, qui est le fondement des class es du C++. Cependant, la façon la plus simple de créer des types plus sophistiqués est de simplement créer un alias d'un nom vers un autre nom grâce à typedef.

3.8.1. Alias de noms avec typedef

Ce mot clé promet plus qu'il n'agit : typedef suggère “une définition de type” alors qu'“alias” serait probablement une description plus juste, puisque c'est ce qu'il fait réellement. Sa syntaxe est :

typedef description-type-existant nom-alias;

Le typedef est fréquemment utilisé quand les noms des types de données deviennent quelque peu compliqués, simplement pour économiser quelques frappes. Voici un exemple d'utilisation commune du typedef:

 
Sélectionnez
typedef unsigned long ulong;

Maintenant, si vous dites ulong le compilateur sait que vous voulez dire unsigned long. Vous pensez peut-être que cela pourrait être si facilement résolu avec une substitution pré processeur, mais il existe des situations pour lesquelles le compilateur doit savoir que vous traitez un nom comme s'il était un type, donc typedef est essentiel.

Un endroit pour lequel typedef est pratique est pour les types pointeurs. Comme mentionné précédemment, si vous dites :

 
Sélectionnez
int* x, y;

Le code va en fait créer un int* qui est x et un int(pas un int*) qui est y. Cela vient du fait que le ‘*' s'associe par la droite, et non par la gauche. Cependant si vous utilisez un ‘ typedef' :

 
Sélectionnez
typedef int* IntPtr;
IntPtr x, y;

Alors x et y sont tous les deux du type int*.

Vous pourriez argumenter qu'il est plus explicite et donc plus lisible d'éviter les typedef sur les types primitifs, et que les programmes deviendraient rapidement difficiles à lire quand beaucoup de typedef sont utilisés. Cependant, les typedef deviennent particulièrement importants en C quand ils sont utilisés avec des struct ures.

3.8.2. Combiner des variables avec des struct

Une struct est une manière de rassembler un groupe de variables dans une structure. Une fois que vous créez une struct, vous pouvez alors créer plusieurs instances de ce “nouveau” type de variable que vous venez d'inventer. Par exemple :

 
Sélectionnez
//: C03:SimpleStruct.cpp
struct Structure1 {
  char c;
  int i;
  float f;
  double d;
};
 
int main() {
  struct Structure1 s1, s2;
  s1.c = 'a'; // Sélectionnez un élément en utilisant un '.'
  s1.i = 1;
  s1.f = 3.14;
  s1.d = 0.00093;
  s2.c = 'a';
  s2.i = 1;
  s2.f = 3.14;
  s2.d = 0.00093;
} ///:~

La déclaration d'une struct doit être terminée par un point-virgule. Dans notre main( ), deux instances de Structure1 sont créées : s1 et s2. Chacune de ces instances dispose de ses propres versions distinctes de c, i, f et d. ainsi s1 et s2 représentent des blocs de variables totalement indépendants. Pour sélectionner un des éléments encapsulé dans s1 ou s2, vous utilisez un ‘ .', syntaxe que vous avez rencontré dans le précédent chapitre en utilisant des objets de class es C++ – comme les class es sont des struct ures évoluées, voici donc d'où vient la syntaxe.

Une chose que vous noterez est la maladresse d'utilisation de Structure1(comme cela ressort, c'est requis en C uniquement, pas en C++). En C, vous ne pouvez pas juste dire Structure1 quand vous définissez des variables, vous devez dire struct Structure1. C'est ici que le typedef devient particulièrement pratique en C :

 
Sélectionnez
//: C03:SimpleStruct2.cpp
// Utilisation de typedef avec des struct
typedef struct {
  char c;
  int i;
  float f;
  double d;
} Structure2;
 
int main() {
  Structure2 s1, s2;
  s1.c = 'a';
  s1.i = 1;
  s1.f = 3.14;
  s1.d = 0.00093;
  s2.c = 'a';
  s2.i = 1;
  s2.f = 3.14;
  s2.d = 0.00093;
} ///:~

En utilisant typedef de cette façon, vous pouvez prétendre (en C tout du moins ; essayez de retirer le typedef pour C++) que Structure2 est un type natif, au même titre que int ou float, quand vous définissez s1 et s2(mais notez qu'il a uniquement des –caractéristiques– de données mais sans inclure de comportement particulier, comportements que l'on peut définir avec de vrais objets en C++). Vous noterez que l'identifiant struct a été abandonné au début, parce que le but était de créer un type via le typedef. Cependant, il y a des fois où vous pourriez avoir besoin de vous référer au struct pendant sa définition. Dans ces cas, vous pouvez en fait répéter le struct dans le nom de la struct ure à travers le typedef:

 
Sélectionnez
//: C03:SelfReferential.cpp
// Autoriser une struct à faire référence à elle-même
 
typedef struct SelfReferential {
  int i;
  SelfReferential* sr; // Déjà mal à la tête ?
} SelfReferential;
 
int main() {
  SelfReferential sr1, sr2;
  sr1.sr = &sr2;
  sr2.sr = &sr1;
  sr1.i = 47;
  sr2.i = 1024;
} ///:~

Si vous regardez ceci de plus près, vous remarquerez que sr1 et sr2 pointent tous les deux l'un vers l'autre, comme s'ils contenaient une donnée quelconque.

En fait, le nom de la struct n'est pas obligatoirement le même que celui du typedef, mais on procède généralement de cette façon pour préserver la simplicité du procédé.

Pointeurs et structs

Dans les exemples ci-dessus, toutes les struct ures sont manipulées comme des objets. Cependant, comme tout élément de stockage, vous pouvez prendre l'adresse d'une struct(comme montré dans l'exemple SelfReferential.cpp ci-dessous). Pour sélectionner les éléments d'un objet struct particulier, On utilise le ‘ .', comme déjà vu plus haut. Cela dit, si vous disposez d'un pointeur sur une struct, vous devez sélectionner ses éléments en utilisant un opérateur différent : le ‘ ->'. Voici un exemple :

 
Sélectionnez
//: C03:SimpleStruct3.cpp
// Utilisation de pointeurs de struct
typedef struct Structure3 {
  char c;
  int i;
  float f;
  double d;
} Structure3;
 
int main() {
  Structure3 s1, s2;
  Structure3* sp = &s1;
  sp->c = 'a';
  sp->i = 1;
  sp->f = 3.14;
  sp->d = 0.00093;
  sp = &s2; // Pointe vers une struct différent
  sp->c = 'a';
  sp->i = 1;
  sp->f = 3.14;
  sp->d = 0.00093;
} ///:~

Dans main( ), le pointeur de structsp pointe initialement sur s1, et les membres de s1 sont initialisés en les sélectionnant grâce au ‘ ->' (et vous utilisez ce même opérateur pour lire ces membres). Mais par la suite sp pointe vers s2, et ses variables sont initialisées de la même façon. Aussi vous pouvez voir qu'un autre avantage des pointeurs est qu'ils peuvent être redirigés dynamiquement pour pointer vers différents objets ; ceci apporte d'avantage de flexibilité à votre programmation, comme vous allez le découvrir.

Pour l'instant, c'est tout ce que vous avez besoin de savoir à propos des struct, mais vous allez devenir très familiers (et particulièrement avec leurs plus puissants successeurs, les class es) au fur et à mesure de la lecture de ce livre.

3.8.3. Eclaircir les programmes avec des enum

Un type de données énuméré est une manière d'attacher un nom a des nombres, et ainsi d'y donner plus de sens pour quiconque qui lit le code. Le mot-clé enum(du C) énumère automatiquement une liste d'identifiants que vous lui donnez en affectant des valeurs 0, 1, 2, etc. On peut déclarer des variables enum(qui sont toujours représentées comme des valeurs intégrales). La déclaration d'une enum ération est très proche à celle d'une struct ure.

Un type de données énuméré est utile quand on veut garder une trace de certaines fonctionnalités :

 
Sélectionnez
//: C03:Enum.cpp
// Suivi des formes
 
enum ShapeType {
  circle,
  square,
  rectangle
};  // Se termine par un point-virgule
 
int main() {
  ShapeType shape = circle;
  // Activités ici....
  // Faire quelque chose en fonction de la forme :
  switch(shape) {
    case circle:  /* c'est un cercle */ break;
    case square:  /* C'est un carré */ break;
    case rectangle:  /* Et voici le rectangle */ break;
  }
} ///:~

shape est une variable du type de données énuméré ShapeType, et sa valeur est comparée à la valeur de l'énumération. Puisque shape est réellement juste un int, il est possible de lui affecter n'importe quelle valeur qu'un int peut prendre (y compris les valeurs négatives). Vous pouvez aussi comparer une variable de type int à une valeur de l'énumération.

Vous devez savoir que l'exemple de renommage ci-dessus se révèle une manière de programmer problématique. Le C++ propose une façon bien meilleure pour faire ce genre de choses, l'explication de ceci sera donnée plus loin dans le livre.

Si vous n'aimez pas la façon dont le compilateur affecte les valeurs, vous pouvez le faire vous-même, comme ceci :

 
Sélectionnez
enum ShapeType { 
  circle = 10, square = 20, rectangle = 50
};

Si vous donnez des valeurs à certains noms mais pas à tous, le compilateur utilisera la valeur entière suivante. Par exemple,

 
Sélectionnez
enum snap { crackle = 25, pop };

Le compilateur donne la valeur 26 à pop.

Vous pouvez voir alors combien vous gagnez en lisibilité du code en utilisant des types de données énumérés. Cependant, d'une certaine façon, cela reste une tentative (en C) d'accomplir des choses que l'on peut faire avec une class en C++, c'est ainsi que vous verrez que les enum sont moins utilisées en C++.

Vérification de type pour les énumérations

Les énumérations du C sont très primitives, en associant simplement des valeurs intégrales à des noms, mais elles ne fournissent aucune vérification de type. En C++, comme vous pouvez vous y attendre désormais, le concept de type est fondamental, et c'est aussi vrai avec les énumérations. Quand vous créez une énumération nommée, vous créez effectivement un nouveau type tout comme vous le faites avec une classe : le nom de votre énumération devient un mot réservé pour la durée de l'unité de traduction.

De plus, la vérification de type est plus stricte pour les énumérations en C++ qu'en C. Vous noterez cela, en particulier, dans le cas d'une énumération color appelée a. En C, vous pouvez écrire a++, chose que vous ne pouvez pas faire en C++. Ceci parce que l?incrémentation d?une énumération effectue en réalité deux conversions de type, l'une d'elle légale en C++, mais l'autre illégale. D'abord, la valeur de l'énumération est implicitement convertie de color vers un int, puis la valeur est incrémentée, et reconvertie en color. En C++, ce n'est pas autorisé, parce que color est un type distinct et n'est pas équivalent à un int. Cela a du sens, parce que comment saurait-on si le résultat de l'incrémentation de blue sera dans la liste de couleurs? Si vous souhaitez incrémenter un color, alors vous devez utiliser une classe (avec une opération d'incrémentation) et non pas une enum, parce que la classe peut être rendue plus sûre. Chaque fois que vous écrirez du code qui nécessite une conversion implicite vers un type enum, Le compilateur vous avertira du danger inhérent à cette opération.

Les unions (décrites dans la prochaine section) possèdent la même vérification additionnelle de type en C++.

3.8.4. Economiser de la mémoire avec union

Parfois, un programme va manipuler différents types de donnée en utilisant la même variable. Dans de tels cas, deux possibilités: soit vous créez une struct qui contient tous les types possibles que vous auriez besoin d'enregistrer, soit vous utilisez une union. Une union empile toutes les données dans un même espace; cela signifie que la quantité de mémoire nécessaire sera celle de l'élément le plus grand que vous avez placé dans l' union. Utilisez une union pour économiser de la mémoire.

Chaque fois que vous écrivez une valeur dans une union, cette valeur commence toujours à l'adresse de début de l' union, mais utilise seulement la mémoire nécessaire. Ainsi, vous créez une “super-variable” capable d'utiliser chacune des variables de l' union. Toutes les adresses des variables de l' union sont les mêmes (alors que dans une classe ou une struct, les adresses diffèrent).

Voici un simple usage d'une union. Essayez de supprimer quelques éléments pour voir quel effet cela a sur la taille de l' union. Notez que cela n'a pas de sens de déclarer plus d'une instance d'un simple type de données dans une union(à moins que vous ne fassiez que pour utiliser des noms différents).

 
Sélectionnez
//: C03:Union.cpp
// Simple usage d'une union
#include <iostream>
using namespace std;
 
union Packed { // Déclaration similaire à une classe
  char i;
  short j;
  int k;
  long l;
  float f;
  double d;  
  // L'union sera de la taille d'un
  // double, puisque c'est l?élément le plus grand
};  // Un point-virgule termine une union, comme une struct
 
int main() {
  cout << "sizeof(Packed) = " 
       << sizeof(Packed) << endl;
  Packed x;
  x.i = 'c';
  cout << x.i << endl;
  x.d = 3.14159;
  cout << x.d << endl;
} ///:~

Le compilateur effectuera l'assignation correctement selon le type du membre de l'union que vous sélectionnez.

Une fois que vous avez effectué une affectation, le compilateur se moque de ce que vous ferez par la suite de l'union. Dans l'exemple précédent, on aurait pu assigner une valeur flottante à x:

 
Sélectionnez
x.f = 2.222;

Et l'envoyer sur la sortie comme si c'était un int:

 
Sélectionnez
cout << x.i;

Ceci aurait produit une valeur sans aucun sens.

3.8.5. Tableaux

Les tableaux sont une espèce de type composite car ils vous autorisent à agréger plusieurs variables ensemble, les unes à la suite des autres, sous le même nom. Si vous dites :

 
Sélectionnez
int a[10];

Vous créez un emplacement mémoire pour 10 variables int empilées les unes sur les autres, mais sans un nom unique pour chacune de ces variables. A la place, elles sont toutes réunies sous le nom a.

Pour accéder à l'un des éléments du tableau, on utilise la même syntaxe utilisant les crochets que celle utilisée pour définir un tableau :

 
Sélectionnez
a[5] = 47;

Cependant, vous devez retenir que bien que la taille de a soit 10, on sélectionne les éléments d'un tableau en commençant à zero (ceci est parfois appelé indexation basée sur zero), donc vous ne pouvez sélectionner que les éléments 0-9 du tableau, comme ceci :

 
Sélectionnez
//: C03:Arrays.cpp
#include <iostream>
using namespace std;
 
int main() {
  int a[10];
  for(int i = 0; i < 10; i++) {
    a[i] = i * 10;
    cout << "a[" << i << "] = " << a[i] << endl;
  }
} ///:~

L'accès aux tableaux est extrêmement rapide. Cependant, si votre index dépasse la taille du tableau, il n'y a aucun filet de sécurité – vous pointerez sur d'autres variables. L'autre inconvénient est que vous devez spécifier la taille du tableau au moment de la compilation ; si vous désirez changer la taille lors de l'exécution, vous ne pouvez pas le faire avec la syntaxe précédente (le C propose une façon de créer des tableaux dynamiquement, mais c'est assurément plus sale). Le type vector fournit par C++, présenté au chapitre précédent, nous apporte un type semblable à un tableau qui redéfinit sa taille automatiquement, donc c'est généralement une meilleure solution si la taille de votre tableau ne peut pas être connue lors de la compilation.

Vous pouvez créer un tableau de n'importe quel type, y compris de struct ures :

 
Sélectionnez
//: C03:StructArray.cpp
// Un tableau de struct
 
typedef struct {
  int i, j, k;
} ThreeDpoint;
 
int main() {
  ThreeDpoint p[10];
  for(int i = 0; i < 10; i++) {
    p[i].i = i + 1;
    p[i].j = i + 2;
    p[i].k = i + 3;
  }
} ///:~

Remarquez comment l'identifiant i de la struct ure est indépendant de celui de la boucle for.

Pour vérifier que tous les éléments d'un tableau se suivent, on peut afficher les adresses comme ceci :

 
Sélectionnez
//: C03:ArrayAddresses.cpp
#include <iostream>
using namespace std;
 
int main() {
  int a[10];
  cout << "sizeof(int) = "<< sizeof(int) << endl;
  for(int i = 0; i < 10; i++)
    cout << "&a[" << i << "] = " 
         << (long)&a[i] << endl;
} ///:~

Quand vous exécutez ce programme, vous verrez que chaque élément est éloigné de son précédent de la taille d'un int. Ils sont donc bien empilés les uns sur les autres.

Pointeurs et tableaux

L'identifiant d'un tableau n'est pas comme celui d'une variable ordinaire. Le nom d'un tableau n'est pas une lvalue ; vous ne pouvez pas lui affecter de valeur. C'est seulement un point d'ancrage pour la syntaxe utilisant les crochets ‘[]', et quand vous utilisez le nom d'un tableau, sans les crochets, vous obtenez l'adresse du début du tableau:

 
Sélectionnez
//: C03:ArrayIdentifier.cpp
#include <iostream>
using namespace std;
 
int main() {
  int a[10];
  cout << "a = " << a << endl;
  cout << "&a[0] =" << &a[0] << endl;
} ///:~

En exécutant ce programme, vous constaterez que les deux adresses (affichées en hexadécimal, puisque aucun cast en long n'est fait) sont identiques.

Nous pouvons considérer que le nom d?un tableau est un pointeur en lecture seule sur le début du tableau. Et bien que nous ne puissions pas changer le nom du tableau pour qu'il pointe ailleurs, nous pouvons, en revanche, créer un autre pointeur et l'utiliser pour se déplacer dans le tableau. En fait, la syntaxe avec les crochets marche aussi avec les pointeurs normaux également :

 
Sélectionnez
//: C03:PointersAndBrackets.cpp
int main() {
  int a[10];
  int* ip = a;
  for(int i = 0; i < 10; i++)
    ip[i] = i * 10;
} ///:~

Le fait que nommer un tableau produise en fait l'adresse de départ du tableau est un point assez important quand on s'intéresse au passage des tableaux en paramètres de fonctions. Si vous déclarez un tableau comme un argument de fonction, vous déclarez en fait un pointeur. Dans l'exemple suivant, func1( ) et func2( ) ont au final la même liste de paramètres :

 
Sélectionnez
//: C03:ArrayArguments.cpp
#include <iostream>
#include <string>
using namespace std;
 
void func1(int a[], int size) {
  for(int i = 0; i < size; i++)
    a[i] = i * i - i;
}
 
void func2(int* a, int size) {
  for(int i = 0; i < size; i++)
    a[i] = i * i + i;
}
 
void print(int a[], string name, int size) {
  for(int i = 0; i < size; i++)
    cout << name << "[" << i << "] = " 
         << a[i] << endl;
}
 
int main() {
  int a[5], b[5];
  // Probablement des valeurs sans signification:
  print(a, "a", 5);
  print(b, "b", 5);
  // Initialisation des tableaux:
  func1(a, 5);
  func1(b, 5);
  print(a, "a", 5);
  print(b, "b", 5);
  // Les tableaux sont toujours modifiés :
  func2(a, 5);
  func2(b, 5);
  print(a, "a", 5);
  print(b, "b", 5);
} ///:~

Même si func1( ) et func2( ) déclarent leurs paramètres différemment, leur utilisation à l'intérieur de la fonction sera la même. Il existe quelques autres problèmes que l'exemple suivant nous révèle : les tableaux ne peuvent pas être passés par valeur (32), car vous ne récupérez jamais de copie locale du tableau que vous passez à une fonction. Ainsi, quand vous modifiez un tableau, vous modifiez toujours l'objet extérieur. Cela peut dérouter au début, si vous espériez un comportement de passage par valeur tel que fourni avec les arguments ordinaires.

Remarquez que print( ) utilise la syntaxe avec les crochets pour les paramètres tableaux. Même si les syntaxes de pointeurs et avec les crochets sont effectivement identiques quand il s'agit de passer des tableaux en paramètres, les crochets facilitent la lisibilité pour le lecteur en lui explicitant que le paramètre utilisé est bien un tableau.

Notez également que la taille du tableau est passée en paramètre dans tous les cas. Passer simplement l'adresse d'un tableau n'est pas une information suffisante; vous devez toujours savoir connaître la taille du tableau à l'intérieur de votre fonction, pour ne pas dépasser ses limites.

Les tableaux peuvent être de n'importe quel type, y compris des tableaux de pointeurs. En fait, lorsque vous désirez passer à votre programme des paramètres en ligne de commande, le C et le C++ ont une liste d'arguments spéciale pour main( ), qui ressemble à ceci :

 
Sélectionnez
int main(int argc, char* argv[]) { // ...

Le premier paramètre est le nombre d'éléments du tableau, lequel tableau est le deuxième paramètre. Le second paramètre est toujours un tableau de char*, car les arguments sont passés depuis la ligne de commande comme des tableaux de caractères (et souvenez vous, un tableau ne peut être passé qu'en tant que pointeur). Chaque portion de caractères délimitée par des espaces est placée dans une chaîne de caractères séparée dans le tableau. Le programme suivant affiche tous ses paramètres reçus en ligne de commande en parcourant le tableau :

 
Sélectionnez
//: C03:CommandLineArgs.cpp
#include <iostream>
using namespace std;
 
int main(int argc, char* argv[]) {
  cout << "argc = " << argc << endl;
  for(int i = 0; i < argc; i++)
    cout << "argv[" << i << "] = " 
         << argv[i] << endl;
} ///:~

Notez que argv[0] est en fait le chemin et le nom de l'application elle-même. Cela permet au programme de récupérer des informations sur lui. Puisque que cela rajoute un élément de plus au tableau des paramètres du programme, une erreur souvent rencontrée lors du parcours du tableau est d'utiliser argv[0] alors qu'on veut en fait argv[1].

Vous n'êtes pas obligés d'utiliser argc et argv comme identifiants dans main( ); ils sont utilisés par pure convention (mais ils risqueraient de perturber un autre lecteur si vous ne les utilisiez pas). Aussi, il existe une manière alternative de déclarer argv:

 
Sélectionnez
int main(int argc, char** argv) { // ...

Les deux formes sont équivalentes, mais je trouve la version utilisée dans ce livre la plus intuitive pour relire le code, puisqu'elle dit directement “Ceci est un tableau de pointeurs de caractères”.

Tout ce que vous récupérez de la ligne de commande n'est que tableaux de caractères; si vous voulez traiter un argument comment étant d'un autre type, vous avez la responsabilité de le convertir depuis votre programme. Pour faciliter la conversion en nombres, il existe des fonctions utilitaires de la librairie C standard, déclarées dans <cstdlib>. Les plus simples à utiliser sont atoi( ), atol( ), et atof( ) pour convertir un tableau de caractères ASCII en valeurs int, long, et double, respectivement. Voici un exemple d'utilisation de atoi( )(les deux autres fonctions sont appelées de la même manière) :

 
Sélectionnez
//: C03:ArgsToInts.cpp
// Convertir les paramètres de la ligne de commande en int
#include <iostream>
#include <cstdlib>
using namespace std;
 
int main(int argc, char* argv[]) {
  for(int i = 1; i < argc; i++)
    cout << atoi(argv[i]) << endl;
} ///:~

Dans ce programme, vous pouvez saisir n'importe quel nombre de paramètres en ligne de commande. Vous noterez que la boucle for commence à la valeur 1 pour ignorer le nom du programme en argv[0]. Mais, si vous saisissez un nombre flottant contenant le point des décimales sur la ligne de commande, atoi( ) ne prendra que les chiffres jusqu'au point. Si vous saisissez des caractères non numériques, atoi( ) les retournera comme des zéros.

Exploration du format flottant

La fonction printBinary( ) présentée précédemment dans ce chapitre est pratique pour scruter la structure interne de types de données divers. Le plus intéressant de ceux-ci est le format flottant qui permet au C et au C++ d'enregistrer des nombres très grands et très petits dans un espace mémoire limité. Bien que tous les détails ne puissent être exposés ici, les bits à l'intérieur d'un float et d'un double sont divisées en trois régions : l'exposant, la mantisse et le bit de signe; le nombre est stocké en utilisant la notation scientifique. Le programme suivant permet de jouer avec les modèles binaires de plusieurs flottants et de les imprimer à l'écran pour que vous vous rendiez compte par vous même du schéma utilisé par votre compilateur (généralement c'est le standard IEEE, mais votre compilateur peut ne pas respecter cela) :

 
Sélectionnez
//: C03:FloatingAsBinary.cpp
//{L} printBinary
//{T} 3.14159
#include "printBinary.h"
#include <cstdlib>
#include <iostream>
using namespace std;
 
int main(int argc, char* argv[]) {
  if(argc != 2) {
    cout << "Vous devez fournir un nombre" << endl;
    exit(1);
  }
  double d = atof(argv[1]);
  unsigned char* cp = 
    reinterpret_cast<unsigned char*>(&d);
  for(int i = sizeof(double)-1; i >= 0 ; i -= 2) {
    printBinary(cp[i-1]);
    printBinary(cp[i]);
  }
} ///:~

Tout d'abord, le programme garantit que le bon nombre de paramètres est fourni en vérifiant argc, qui vaut deux si un seul argument est fourni (il vaut un si aucun argument n'est fourni, puisque le nom du programme est toujours le premier élément de argv). Si ce test échoue, un message est affiché et la fonction de la bibliothèque standard du C exit( ) est appelée pour terminer le programme.

Puis le programme récupère le paramètre de la ligne de commande et convertit les caractères en double grâce à atof( ). Ensuite le double est utilisé comme un tableau d'octets en prenant l'adresse et en la convertissant en unsigned char*. Chacun de ces octets est passé à printBinary( ) pour affichage.

Cet exemple a été réalisé de façon à ce que que le bit de signe apparaisse d'abord sur ma machine. La vôtre peut être différente, donc vous pourriez avoir envie de réorganiser la manière dont sont affichées les données. Vous devriez savoir également que le format des nombres flottants n'est pas simple à comprendre ; par exemple, l'exposant et la mantisse ne sont généralement pas arrangés sur l'alignement des octets, mais au contraire un nombre de bits est réservé pour chacun d'eux, et ils sont empaquetés en mémoire de la façon la plus serrée possible. Pour vraiment voir ce qui se passe, vous devrez trouver la taille de chacune des parties (le bit de signe est toujours un seul bit, mais l'exposant et la mantisse ont des tailles différentes) et afficher les bits de chaque partie séparément.

Arithmétique des pointeurs

Si tout ce que l'on pouvait faire avec un pointeur qui pointe sur un tableau était de l'utiliser comme un alias pour le nom du tableau, les pointeurs ne seraient pas très intéressants. Cependant, ce sont des outils plus flexibles que cela, puisqu'ils peuvent être modifiés pour pointer n'importe où ailleurs (mais rappelez vous que l'identifiant d'un tableau ne peut jamais être modifié pour pointer ailleurs).

Arithmétique des pointeurs fait référence à l'application de quelques opérateurs arithmétiques aux pointeurs. La raison pour laquelle l'arithmétique des pointeurs est un sujet séparé de l'arithmétique ordinaire est que les pointeurs doivent se conformer à des contraintes spéciales pour qu'ils se comportent correctement. Par exemple, un opérateur communément utilisé avec des pointeurs est le ++, qui “ajoute un au pointeur”. Cela veut dire en fait que le pointeur est changé pour se déplacer à “la valeur suivante”, quoi que cela signifie. Voici un exemple :

 
Sélectionnez
//: C03:PointerIncrement.cpp
#include <iostream>
using namespace std;
 
int main() {
  int i[10];
  double d[10];
  int* ip = i;
  double* dp = d;
  cout << "ip = " << (long)ip << endl;
  ip++;
  cout << "ip = " << (long)ip << endl;
  cout << "dp = " << (long)dp << endl;
  dp++;
  cout << "dp = " << (long)dp << endl;
} ///:~

Pour une exécution sur ma machine, voici le résultat obtenu :

 
Sélectionnez
ip = 6684124
ip = 6684128
dp = 6684044
dp = 6684052

Ce qui est intéressant ici est que bien que l'opérateur ++ paraisse être la même opération à la fois pour un int* et un double*, vous remarquerez que le pointeur a avancé de seulement 4 octets pour l' int* mais de 8 octets pour le double*. Ce n'est pas par coïncidence si ce sont les tailles de ces types sur ma machine. Et tout est là dans l'arithmétique des pointeurs : le compilateur détermine le bon déplacement à appliquer au pointeur pour qu'il pointe sur l'élément suivant dans le tableau (l'arithmétique des pointeurs n'a de sens qu'avec des tableaux). Cela fonctionne même avec des tableaux de struct s:

 
Sélectionnez
//: C03:PointerIncrement2.cpp
#include <iostream>
using namespace std;
 
typedef struct {
  char c;
  short s;
  int i;
  long l;
  float f;
  double d;
  long double ld;
} Primitives;
 
int main() {
  Primitives p[10];
  Primitives* pp = p;
  cout << "sizeof(Primitives) = " 
       << sizeof(Primitives) << endl;
  cout << "pp = " << (long)pp << endl;
  pp++;
  cout << "pp = " << (long)pp << endl;
} ///:~

L'affichage sur ma machine a donné :

 
Sélectionnez
sizeof(Primitives) = 40
pp = 6683764
pp = 6683804

Vous voyez ainsi que le compilateur fait aussi les choses correctement en ce qui concerne les pointeurs de struct ures (et de class es et d' union s).

L'arithmétique des pointeurs marche également avec les opérateurs --, +, et -, mais les deux derniers opérateurs sont limités : Vous ne pouvez pas additionner deux pointeurs, et si vous retranchez des pointeurs, le résultat est le nombre d'élément entre les deux pointeurs. Cependant, vous pouvez ajouter ou soustraire une valeur entière et un pointeur. Voici un exemple qui démontre l'utilisation d'une telle arithmétique :

 
Sélectionnez
//: C03:PointerArithmetic.cpp
#include <iostream>
using namespace std;
 
#define P(EX) cout << #EX << ": " << EX << endl;
 
int main() {
  int a[10];
  for(int i = 0; i < 10; i++)
    a[i] = i; // Attribue les valeurs de l?index
  int* ip = a;
  P(*ip);
  P(*++ip);
  P(*(ip + 5));
  int* ip2 = ip + 5;
  P(*ip2);
  P(*(ip2 - 4));
  P(*--ip2);
  P(ip2 - ip); // Renvoie le nombre d?éléments
} ///:~

Il commence avec une nouvelle macro, mais celle-ci utilise une fonctionnalité du pré processeur appelée stringizing(transformation en chaîne de caractères - implémentée grâce au symbole ‘ #' devant une expression) qui prend n'importe quelle expression et la transforme en tableau de caractères. C'est très pratique puisqu'elle permet d'imprimer une expression, suivie de deux-points, suivie par la valeur de l'expression. Dans main( ) vous pouvez voir l'avantage que cela produit.

De même, les version pré et post fixées des opérateurs ++ et -- sont valides avec les pointeurs, même si seule la version préfixée est utilisée dans cet exemple parce qu'elle est appliquée avant que le pointeur ne soit déréférencé dans les expressions ci-dessus, on peut donc voir les effets des opérations. Notez que seules les valeurs entières sont ajoutées et retranchées ; si deux pointeurs étaient combinés de cette façon, le compilateur ne l'aurait pas accepté.

Voici la sortie du programme précédent :

 
Sélectionnez
*ip: 0
*++ip: 1
*(ip + 5): 6
*ip2: 6
*(ip2 - 4): 2
*--ip2: 5

Dans tous les cas, l'arithmétique des pointeurs résulte en un pointeur ajusté pour pointer au “bon endroit”, basé sur la taille des éléments pointés.

Si l'arithmétique des pointeurs peut paraître accablante de prime abord, pas de panique. La plupart du temps, vous aurez simplement besoin de créer des tableaux et des index avec [ ], et l'arithmétique la plus sophistiquée dont vous aurez généralement besoin est ++ et --. L'arithmétique des pointeurs est en général réservée aux programmes plus complexes, et la plupart des conteneurs standards du C++ cachent tous ces détails intelligents pour que vous n'ayez pas à vous en préoccuper.

3.9. Conseils de déboguage

Dans un environnement idéal, vous disposez d'un excellent débogueur qui rend aisément le comportement de votre programme transparent et qui vous permet de découvrir les erreurs rapidement. Cependant, la plupart des débogueurs ont des "angles morts" qui vont vous forcer à insérer des fragments de code dans votre programme afin de vous aider à comprendre ce qu'il s'y passe. De plus, vous pouvez être en train de développer dans un environnement (par exemple un système embarqué, comme moi durant mes années de formation) qui ne dispose pas d'un débogueur, et fournit même peut-être des indications très limitées (comme une ligne d'affichage à DEL). Dans ces cas, vous devenez créatif dans votre manière de découvrir et d'afficher les informations à propos de l'exécution de votre code. Cette section suggère quelques techniques de cet ordre.

3.9.1. Drapeaux de déboguage

Si vous branchez du code de déboguage en dur dans un programme, vous risquez de rencontrer certains problèmes. Vous commencez à être inondé d'informations, ce qui rend le bogue difficile à isoler. Lorsque vous pensez avoir trouvé le bogue, vous commencez à retirer le code, juste pour vous apercevoir que vous devez le remettre. Vous pouvez éviter ces problèmes avec deux types de drapeaux de déboguage : les drapeaux de précompilation et ceux d'exécution.

Drapeaux de déboguage de précompilation

En utilisant le préprocesseur pour définir (instruction #define) un ou plusieurs drapeaux (de préférence dans un fichier d'en-tête), vous pouvez tester le drapeau (à l'aide de l'instruction #ifdef) et inclure conditionnellement du code de déboguage. Lorsque vous pensez avoir terminé, vous pouvez simplement désactiver (instruction #undef) le(s) drapeau(x) et le code sera automatiquement exclu de la compilation (et vous réduirez la taille et le coût d'exécution de votre fichier).

Il est préférable de choisir les noms de vos drapeaux de déboguage avant de commencer la construction de votre projet afin qu'ils présentent une certaine cohérence. Les drapeaux de préprocesseur sont distingués traditionnellement des variables en les écrivant entièrement en majuscules. Un nom de drapeau classique est simplement DEBUG(mais évitez d'utiliser NDEBUG, qui, lui, est reservé en C). La séquence des instructions pourrait être :

 
Sélectionnez
#define DEBUG // Probablement dans un fichier d'en-tête
//...
#ifdef DEBUG // Teste l'état du drapeau
/* code de déboguage */
#endif // DEBUG

La plupart des implémentations C et C++ vous laissent aussi utiliser #define et #undef pour contrôler des drapeaux à partir de la ligne de commande du compilateur, de sorte que vous pouvez re-compiler du code et insèrer des informations de deboguage en une seule commande (de préférence via le makefile, un outil que nous allons décrire sous peu). Consultez votre documentation préferée pour plus de détails.

Drapeau de déboguage d'exécution

Dans certaines situations, il est plus adapté de lever et baisser des drapeaux de déboguage durant l'exécution du programme, particulièrement en les contrôlant au lancement du programme depuis la ligne de commande. Les gros programmes sont pénibles à recompiler juste pour insérer du code de déboguage.

Pour activer et désactiver du code de déboguage dynamiquement, créez des drapeaux booléens ( bool) ::

 
Sélectionnez
//: C03:DynamicDebugFlags.cpp
#include <iostream>
#include <string>
using namespace std;
// Les drapeaux de deboguage ne sont pas nécéssairement globaux :
bool debug = false;
 
int main(int argc, char* argv[]) {
  for(int i = 0; i < argc; i++)
    if(string(argv[i]) == "--debug=on")
      debug = true;
  bool go = true;
  while(go) {
    if(debug) {
      // Code de déboguage ici
      cout << "Le débogueur est activé!" << endl;
    } else {
      cout << " Le débogueur est désactivé." << endl;
    }  
    cout << "Activer le débogueur [oui/non/fin]: ";
    string reply;
    cin >> reply;
    if(reply == "oui") debug = true; // Activé
    if(reply == "non") debug = false; // Désactivé
    if(reply == "fin") break; // Sortie du 'while'
  }
} ///:~

Ce programme vous permet d'activer et de désactiver la drapeau de deboguage jusqu'à ce que vous tapiez “fin” pour lui indiquer que vous voulez sortir. Notez qu'il vous faut taper les mots en entier, pas juste une lettre (vous pouvez bien sûr changer ça, si vous le souhaitez). Vous pouvez aussi fournir un argument de commande optionnel qui active le déboguage au démarrage; cet argument peut être placé à n'importe quel endroit de la ligne de commande, puisque le code de démarrage dans la fonction main( ) examine tous les arguments. Le test est vraiment simple grâce à l'expression:

 
Sélectionnez
string(argv[i])

Elle transforme le tableau de caractères argv[i] en une chaîne de caractères ( string), qui peut en suite être aisément comparée à la partie droite du ==. Le programme ci-dessus recherche la chaîne --debug=on en entier. Vous pourriez aussi chercher --debug= et regarder ce qui se trouve après pour offrir plus d'options. Le Volume 2 (disponible depuis www.BruceEckel.com) dédie un chapitre à la classe string du Standard C++.

Bien qu'un drapeau de déboguage soit un des rares exemples pour lesquels il est acceptable d'utiliser une variable globale, rien n'y oblige. Notez que la variable est en minuscules pour rappeler au lecteur que ce n'est pas un drapeau du préprocesseur.

3.9.2. Transformer des variables et des expressions en chaînes de caractère

Lorsqu'on ecrit du code de déboguage, il devient vite lassant d'écrire des expressions d'affichage formées d'un tableau de caractères contenant le nom d'une variable suivi de la variable elle même. Heureusement, le Standard C inclut l'opérateur de transformation en chaîne de caractères ' #', qui a déjà été utilisé dans ce chapitre. Lorsque vous placez un # devant un argument dans une macro du préprocesseur, il transforme cet argument en un tableau de caractères. Cela, combiné avec le fait que des tableaux de caractères mis bout à bout sans ponctuation sont concaténés en un tableau unique, vous permet de créer une macro très pratique pour afficher la valeur de variables durant le déboguage :

 
Sélectionnez
#define PR(x) cout << #x " = " << x << "\n";

Si vous affichez la variable a en utilisant la macro PR(a), cela aura le même effet que le code suivant:

 
Sélectionnez
cout << "a = " << a << "\n";

Le même processus peut s'appliquer a des expressions entières. Le programme suivant utilise une macro pour créer un raccourcis qui affiche le texte d'une expression puis évalue l'expression et affiche le résultat:

 
Sélectionnez
//: C03:StringizingExpressions.cpp
#include <iostream>
using namespace std;
 
#define P(A) cout << #A << ": " << (A) << endl;
 
int main() {
  int a = 1, b = 2, c = 3;
  P(a); P(b); P(c);
  P(a + b);
  P((c - a)/b);
} ///:~

Vous pouvez voir comment une telle technique peut rapidement devenir indispensable, particulièrement si vous êtes sans debogueur (ou devez utiliser des environnements de développement multiples). Vous pouvez aussi insérer un #ifdef pour redéfinir P(A) à “rien” lorsque vous voulez retirer le déboguage.

3.9.3. la macro C assert( )

Dans le fichier d'en-tête standard <cassert> vous trouverez assert( ), qui est une macro de déboguage très utile. Pour utiliser assert( ), vous lui donnez un argument qui est une expression que vous “considérez comme vraie.” Le préprocesseur génère du code qui va tester l'assertion. Si l'assertion est fausse, le programme va s'interrompre après avoir émis un message d'erreur indiquant le contenu de l'assertion et le fait qu'elle a échoué. Voici un exemple trivial:

 
Sélectionnez
//: C03:Assert.cpp
// Utilisation de la macro assert()
#include <cassert>  // Contient la macro
using namespace std;
 
int main() {
  int i = 100;
  assert(i != 100); // Échec
} ///:~

La macro vient du C standard, elle est donc disponible également dans le fichier assert.h.

Lorsque vous en avez fini avec le déboguage, vous pouvez retirer le code géneré par la macro en ajoutant la ligne :

 
Sélectionnez
#define NDEBUG

dans le programme avant d'inclure <cassert>, ou bien en définissant NDEBUG dans la ligne de commande du compilateur. NDEBUG est un drapeau utilisé dans <cassert> pour changer la façon dont le code est géneré par les macros.

Plus loin dans ce livre, vous trouverez des alternatives plus sophistiquées à assert( ).

3.10. Adresses de fonctions

Une fois qu'une fonction est compilée et chargée dans l'ordinateur pour être exécutée, elle occupe un morceau de mémoire. Cette mémoire, et donc la fonction, a une adresse.

Le C n'a jamais été un langage qui bloquait le passage là où d'autres craignent de passer. Vous pouvez utiliser les adresses de fonctions avec des pointeurs simplement comme vous utiliseriez des adresses de variable. La déclaration et l'utilisation de pointeurs de fonctions semblent un peu plus opaque de prime abord, mais suit la logique du reste du langage.

3.10.1. Définir un pointeur de fonction

Pour définir un pointeur sur une fonction qui ne comporte pas d'argument et ne retourne pas de valeur, vous écrivez:

 
Sélectionnez
void (*funcPtr)();

Quand vous regardez une définition complexe comme celle-ci, la meilleure manière de l'attaquer est de commencer par le centre et d'aller vers les bords. “Commencer par le centre” signifie commencer par le nom de la variable qui est funcPtr. “Aller vers les bords” signifie regarder à droite l'élément le plus proche (rien dans notre cas; la parenthèse droite nous arrête), puis à gauche (un pointeur révélé par l'astérisque), puis à droite (une liste d'argument vide indiquant une fonction ne prenant aucun argument), puis regarder à gauche ( void, qui indique que la fonction ne retourne pas de valeur). Ce mouvement droite-gauche-droite fonctionne avec la plupart des déclarations.

Comme modèle, “commencer par le centre” (“ funcPtr est un ...”), aller à droite (rien ici – vous êtes arrêté par la parenthèse de droite), aller à gauche et trouver le ‘ *' (“... pointeur sur ...”), aller à droite et trouver la liste d'argument vide (“... fonction qui ne prend pas d'argument ... ”), aller à gauche et trouver le void(“ funcPtr est un pointeur sur une fonction qui ne prend aucun argument et renvoie void”).

Vous pouvez vous demander pourquoi *funcPtr requiert des parenthèses. Si vous ne les utilisez pas, le compilateur verra:

 
Sélectionnez
void *funcPtr();

Vous déclareriez une fonction (qui retourne void*) comme on définit une variable. Vous pouvez imaginer que le compilateur passe par le même processus que vous quand il se figure ce qu'une déclaration ou une définition est censée être. Les parenthèses sont nécessaires pour que le compilateur aille vers la gauche et trouve le ‘ *', au lieu de continuer vers la droite et de trouver la liste d'argument vide.

3.10.2. Déclarations complexes & définitions

Cela mis à part, une fois que vous savez comment la syntaxe déclarative du C et du C++ fonctionne, vous pouvez créer des déclarations beaucoup plus compliquées. Par exemple:

 
Sélectionnez
//: C03:ComplicatedDefinitions.cpp
 
/* 1. */     void * (*(*fp1)(int))[10];
 
/* 2. */     float (*(*fp2)(int,int,float))(int);
 
/* 3. */     typedef double (*(*(*fp3)())[10])();
             fp3 a;
 
/* 4. */     int (*(*f4())[10])();
 
int main() {} ///:~

Les prendre une par une et employer la méthode de droite à gauche pour les résoudre. Le premier dit que “ fp1 est un pointeur sur une fonction qui prend un entier en argument et retourne un pointeur sur un tableau de 10 pointeurs void.”

Le second dit que “ fp2 est un pointeur sur une fonction qui prend trois arguments ( int, int, et float) et retourne un float.”

Si vous créez beaucoup de définitions complexes, vous voudrez sans doute utiliser un typedef. Le troisième exemple montre comment un typedef enregistre à chaque fois les descriptions complexes. Cet exemple nous dit que “ fp3 est un pointeur sur une fonction ne prenant aucun argument et retourne un pointeur sur un tableau de 10 pointeurs de fonctions qui ne prennent aucun argument et retournent des double.” Il nous dit aussi que “ a est du type fp3.” typedef est en général très utile pour établir des descriptions simples à partir de descriptions complexes.

Le numéro 4 est une déclaration de fonction plutôt qu'une définition de variable. “ f4 est une fonction qui retourne un pointeur sur un tableau de 10 pointeurs de fonctions qui retournent des entiers.”

Vous aurez rarement besoin de déclarations et définitions aussi compliquées que ces dernières. Cependant, si vous vous entraînez à ce genre d'exercice vous ne serez pas dérangé avec les déclarations légèrement compliquées que vous pourrez rencontrer dans la réalité.

3.10.3. Utiliser un pointeur de fonction

Une fois que vous avez défini un pointeur de fonction, vous devez l'assigner à une adresse de fonction avant de l'utiliser. Tout comme l'adresse du tableau arr[10] est produite par le nom du tableau sans les crochets, l'adresse de la fonction func() est produite par le nom de la fonction sans la liste d'argument ( func). Vous pouvez également utiliser la syntaxe suivante, plus explicite, &func(). Pour appeler une fonction, vous déférencez le pointeur de la même façon que vous l'avez défini (rappelez-vous que le C et le C++ essaient de produire des définitions qui restent semblables lors de leur utilisation). L'exemple suivant montre comment un pointeur sur une fonction est défini et utilisé:

 
Sélectionnez
//: C03:PointerToFunction.cpp
// Définir et utiliser un pointeur de fonction
#include <iostream>
using namespace std;
 
void func() {
  cout << "func() called..." << endl;
}
 
int main() {
  void (*fp)();  // Définir un pointeur de fonction
  fp = func;  // L'initialiser
  (*fp)();    // Le déférencement appelle la fonction
  void (*fp2)() = func;  // Définir et initialiser
  (*fp2)();
} ///:~

Après que le pointeur de fonction fp soit défini, il est assigné à l'adresse de la fonction func() avec fp = func(notez que la liste d'arguments manque au nom de la fonction). Le second cas montre une déclaration et une initialisation simultanées.

3.10.4. Tableau de pointeurs de fonction

L'une des constructions les plus intéressantes que vous puissiez créer est le tableau de pointeurs de fonctions. Pour sélectionner une fonction, il vous suffit d'indexer dans le tableau et de référencer le pointeur. Cela amène le concept de code piloté par table; plutôt que d'utiliser un cas ou une condition, vous sélectionnez les fonctions à exécuter en vous basant sur une variable d'état (ou une combinaison de variables d'état). Ce type de design peut être très utile si vous ajoutez ou supprimez souvent des fonctions à la table (ou si vous voulez créer ou changer de table dynamiquement).

L'exemple suivant crée des fonctions factices en utilisant un macro du préprocesseur, et crée un tableau de pointeurs sur ces fonctions en utilisant une initialisation globale automatique. Comme vous pouvez le constater, il est facile d'ajouter ou supprimer des fonctions de la table (et de cette façon, les fonctionnalités du programme) en changeant une petite portion du code:

 
Sélectionnez
//: C03:FunctionTable.cpp
// Utilisation d'un tableau de pointeurs de fonctions
#include <iostream>
using namespace std;
 
// Une macro qui définit des fonctions factices:
#define DF(N) void N() { \
   cout << "la fonction " #N " est appelee..." << endl; }
 
DF(a); DF(b); DF(c); DF(d); DF(e); DF(f); DF(g);
 
void (*func_table[])() = { a, b, c, d, e, f, g };
 
int main() {
  while(1) {
    cout << "pressez une touche de 'a' a 'g' "
      "or q to quit" << endl;
    char c, cr;
    cin.get(c); cin.get(cr); // le second pour CR
    if ( c == 'q' ) 
      break; // ... sortie du while(1)
    if ( c < 'a' || c > 'g' ) 
      continue;
    (*func_table[c - 'a'])();
  }
} ///:~

A ce stade, vous êtes à même d'imaginer combien cette technique peut être utile lorsqu'on crée une espèce d'interpréteur ou un traitement de liste.

3.11. Make: gestion de la compilation séparée

Lorsque vous utilisez la compilation séparée(découpage du code en plusieurs unités de compilation), il vous faut une manière de compiler automatiquement chaque fichier et de dire à l'éditeur de liens de joindre tous les morceaux - ainsi que les bibliothèques nécessaires et le code de lancement - pour en faire un fichier exécutable. La plupart des compilateurs vous permettent de faire ça avec une seule ligne de commande. Par exemple, pour le compilateur GNU C++, vous pourriez dire

 
Sélectionnez
g++ SourceFile1.cpp SourceFile2.cpp

Le problème de cette approche est que le compilateur va commencer par compiler chaque fichier, indépendamment du fait que ce fichier ait besoin d'être recompilé ou pas. Pour un projet avec de nombreux fichiers, il peut devenir prohibitif de recompiler tout si vous avez juste changé un seul fichier.

La solution de ce problème, développée sous Unix mais disponible partout sous une forme ou une autre, est un programme nommé make. L'utilitaire make gère tous les fichiers d'un projet en suivant les instructions contenues dans un fichier texte nommé un makefile. Lorsque vous éditez des fichiers dans un projet puis tapez make, le programme make suis les indications du makefile pour comparer les dates de fichiers source aux dates de fichiers cible correspondants, et si un fichier source est plus récent que son fichier cible, make déclanche le compilateur sur le fichier source. make recompile uniquement les fichiers source qui ont été changés, ainsi que tout autre fichier source affecté par un fichier modifié. En utilisant make vous évitez de recompiler tous les fichiers de votre projet à chaque changement, et de vérifier que tout à été construit correctement. Le makefile contient toutes les commandes pour construire votre projet. Apprendre make vous fera gagner beaucoup de temps et éviter beaucoup de frustration. Vous allez aussi découvrir que make est le moyen typique d'installer un nouveau logiciel sous Linux/Unix (bien que, le makefile pour cela ait tendance à être largement plus complexe que ceux présentés dans ce livre, et que vous générerez souvent le makefile pour votre machine particulière pendant le processus d'installation).

Étant donné que make est disponible sous une forme ou une autre pour virtuellement tous les compilateurs C++ (et même si ce n'est pas le cas, vous pouvez utiliser un make disponible gratuitement avec n'importe quel compilateur), c'est l'outil que nous utiliserons à travers tout ce livre. Cependant, les fournisseurs de compilateur ont aussi créé leur propre outil de construction de projet. Ces outils vous demandent quels fichiers font partie de votre projet et déterminent toutes les relations entre eux par eux-mêmes. Ces outils utilisent quelque chose de similaire à un fichier makefile, généralement nommé un fichier de projet, mais l'environnement de développement maintient ce fichier de sorte que vous n'avez pas à vous en soucier. La configuration et l'utilisation de fichiers de projets varie d'un environnement de développement à un autre, c'est pourquoi vous devez trouver la documentation appropriée pour les utiliser (bien que les outils de fichier de projet fournis par les vendeurs de compilateur sont en général si simples à utiliser que vous pouvez apprendre juste en jouant avec - ma façon préférée d'apprendre).

Les fichiers makefile utilisés à travers ce livre devraient fonctionner même si vous utilisez aussi un outil de construction spécifique.

3.11.1. Les actions du Make

Lorsque vous tapez make(ou le nom porté par votre incarnation de "make"), le programme make cherche un fichier nommé makefile dans le répertoire en cours, que vous aurez créé si c'est votre projet. Ce fichier liste les dépendances de vos fichiers source. make examine la date des fichiers. Si un dépendant est plus ancien qu'un fichier dont il dépend, make exécute la règle donnée juste après la définition de dépendance.

Tous les commentaires d'un makefile commencent par un # et continuent jusqu'à la fin de la ligne.

Un exemple simple de makefile pour un programme nommé "bonjour" pourrait être :

 
Sélectionnez
# Un commentaire
bonjour.exe: bonjour.cpp
        moncompilateur bonjour.cpp

Cela signifie que bonjour.exe(la cible) dépend de bonjour.cpp. Quand bonjour.cpp a une date plus récente que bonjour.exe, make applique la "règle" moncompilateur hello.cpp. Il peut y avoir de multiples dépendances et de multiples règles. De nombreux programmes de make exigent que les règles débutent par un caractère de tabulation. Ceci mis à part, les espaces sont ignorés ce qui vous permet de formater librement pour une meilleure lisibilité.

Les règles ne sont pas limitées à des appels au compilateur; vous pouvez appeler n'importe quel programme depuis make. En créant des groupes interdépendants de jeux de règles de dépendance, vous pouvez modifier votre code source, taper make et être certain que tous les fichiers concernés seront reconstruits correctement.

Macros

Un makefile peut contenir des macros(notez bien qu'elle n'ont rien à voir avec celles du préprocesseur C/C++). Les macros permettent le remplacement de chaînes de caractères. Les makefile de ce livre utilisent une macro pour invoquer le compilateur C++. Par exemple,

 
Sélectionnez
CPP = moncompilateur
hello.exe: hello.cpp
        $(CPP) hello.cpp

Le signe = est utilisé pour identifier CPP comme une macro, et le $ avec les parenthèses permettent d'utiliser la macro. Dans ce cas, l'utilisation signifie que l'appel de macro $(CPP) sera remplacé par la chaîne de caractères moncompilateur. Avec la macro ci-dessus, si vous voulez passer à un autre compilateur nommé cpp, vous avez simplement à modifier la macro comme cela:

 
Sélectionnez
CPP = cpp

Vous pouvez aussi ajouter des drapeaux de compilation, etc., à la macro, ou bien encore utiliser d'autres macros pour ajouter ces drapeaux.

Règles de suffixes

Il devient vite lassant d'invoquer make pour chaque fichier cpp de votre projet, alors que vous savez que c'est le même processus basique à chaque fois. Puisque make a été inventé pour gagner du temps, il possède aussi un moyen d'abréger les actions, tant qu'elles dépendent du suffixe des noms de fichiers. Ces abréviations se nomment des règles de suffixes. Une telle règle permet d'apprendre à make comment convertir un fichier d'un type d'extension ( .cpp, par exemple) en un fichier d'un autre type d'extension ( .obj ou .exe). Une fois que make connaît les règles pour produire un type de fichier à partir d'un autre, tout ce qu'il vous reste à lui dire est qui dépend de qui. Lorsque make trouve un fichier avec une date plus ancienne que le fichier dont il dépend, il utilise la règle pour créer un nouveau fichier.

La règle de suffixes dit à make qu'il n'a pas besoin de règle explicite pour construire tout, mais qu'il peut trouver comment le faire simplement avec les extensions de fichier. Dans ce cas, elle dit "pour construire un fichier qui se termine par exe à partir d'un qui finit en cpp, activer la commande suivante". Voilà à quoi cela ressemble pour cette règle:

 
Sélectionnez
CPP = moncompilateur
.SUFFIXES: .exe .cpp
.cpp.exe:
        $(CPP) $&lt;

La directive .SUFFIXES dit à make qu'il devra faire attention aux extensions de fichier suivantes parce qu'elles ont une signification spéciale pour ce makefile particulier. Ensuite, vous voyez la règle de suffixe .cpp.exe, qui dit "voilà comment convertir un fichier avec l'extension cpp en un avec l'extension exe" (si le fichier cpp est plus récent que le fichier exe). Comme auparavant, la macro $(CPP) est utilisée, mais ensuite vous apercevez quelque chose de nouveau: $<. Comme ça commence avec un " $", c'est une macro, mais c'est une des macros spéciales intégrées à make. Le $< ne peut être utilisé que dans les règles de suffixe, et il signifie "le dépendant qui a déclenché la règle", ce qui, dans ce cas, se traduit par "le fichier cpp qui a besoin d'être compilé".

Une fois les règles des suffixes en place, vous pouvez simplement dire, par exemple, " make Union.exe", et la règle de suffixes s'activera bien qu'il n'y ait pas la moindre mention de "Union" dans le makefile.

Cibles par défaut

Après les macros et les règles de suffixes, make examine la première "cible" dans un fichier, et la construit, si vous n'avez pas spécifié autrement. Ainsi pour le makefile suivant:

 
Sélectionnez
CPP = moncompilateur
.SUFFIXES: .exe .cpp
.cpp.exe:
        $(CPP) $<
cible1.exe:
cible2.exe:

si vous tapez juste " make", ce sera cible1.exe qui sera construit (à l'aide de la règle de suffixe par défaut) parce que c'est la première cible que make rencontre. Pour construire cible2.exe vous devrez explicitement dire " make cible2.exe". Cela devient vite lassant, et pour y remédier, on créé normalement une cible "fictive" qui dépend de toutes les autres cibles de la manière suivante :

 
Sélectionnez
CPP = moncompilateur
.SUFFIXES: .exe .cpp
.cpp.exe:
        $(CPP) $<
all: cible1.exe cible2.exe

Ici, " all" n'existe pas et il n'y a pas de fichier nommé " all", du coup, à chaque fois que vous tapez make, le programme voit " all" comme première cible de la liste (et donc comme cible par défaut), ensuite il voit que " all" n'existe pas et qu'il doit donc le construire en vérifiant toutes les dépendances. Alors, il examine cible1.exe et (à l'aide de la règle de suffixe) regarde (1) si cible1.exe existe et (2) si cible1.cpp est plus récent que cible1.exe, et si c'est le cas, exécute la règle de suffixe (si vous fournissez une règle explicite pour une cible particulière, c'est cette règle qui sera utilisée à la place). Ensuite, il passe au fichier suivant dans la liste de la cible par défaut. Ainsi, en créant une liste de cible par défaut (typiquement nommée all par convention, mais vous pouvez choisir n'importe quel nom) vous pouvez déclancher la construction de tous les exécutables de votre projet en tapant simplement " make". De plus, vous pouvez avoir d'autres listes de cibles non-défaut qui font d'autres choses - par exemple vous pouvez arranger les choses de sorte qu'en tapant " make debug" vous reconstruisiez tous vos fichiers avec le déboguage branché.

3.11.2. Les makefiles de ce livre

En utilisant le programme ExtractCode.cpp du Volume 2 de ce livre, tous les listings sont automatiquement extraits de la version en texte ASCII de ce livre et placé dans des sous-réertoires selon leur chapitre. De plus, ExtractCode.cpp crée plusieurs makefiles dans chaque sous-répertoire (avec des noms distincts) pour que vous puissiez simplement vous placer dans ce sous-répertoire et taper make -f moncompilateur.makefile(en substituant le nom de votre compilateur à "moncompilateur", le drapeau " -f" signifie "utilise ce qui suit comme makefile"). Finalement, ExtractCode.cpp crée un makefile maître dans le répertoire racine où les fichiers du livre ont été décompressés, et ce makefile descend dans chaque sous-répertoire et appelle make avec le makefile approprié. De cette manière, vous pouvez compiler tout le code du livre en invoquant une seule commande make, et le processus s'arrêtera dès que votre compilateur rencontrera un problème avec un fichier particulier (notez qu'un compilateur compatible avec le Standard C++ devrait pouvoir compiler tous les fichiers de ce livre). Du fait que les implémentations de make varient d'un système à l'autre, seules les fonctionnalités communes de base sont utilisées dans les makefile s générés.

3.11.3. Un exemple de makefile

Comme indiqué, l'outil d'extraction de code ExtractCode.cpp génère automatiquement des makefiles pour chaque chapitre. De ce fait, ils ne seront pas inclus dans le livre (il sont tous joints au code source que vous pouvez télécharger depuis www.BruceEckel.com). Cependant il est utile de voir un exemple. Ce qui suit est une version raccourcie d'un makefile qui a été automatiquement généré pour ce chapitre par l'outil d'extraction du livre. Vous trouverez plus d'un makefile dans chaque sous-répertoire (ils ont des noms différents ; vous invoquez chacun d'entre eux avec " make -f"). Celui-ci est pour GNU C++:

 
Sélectionnez
CPP = g++
OFLAG = -o
.SUFFIXES : .o .cpp .c
.cpp.o :
  $(CPP) $(CPPFLAGS) -c $<
.c.o :
  $(CPP) $(CPPFLAGS) -c $<
 
all: \
  Return \
  Declare \
  Ifthen \
  Guess \
  Guess2
# Le reste des fichiers de ce chapitre est omis
 
Return: Return.o 
  $(CPP) $(OFLAG)Return Return.o 
 
Declare: Declare.o 
  $(CPP) $(OFLAG)Declare Declare.o 
 
Ifthen: Ifthen.o 
  $(CPP) $(OFLAG)Ifthen Ifthen.o 
 
Guess: Guess.o 
  $(CPP) $(OFLAG)Guess Guess.o 
 
Guess2: Guess2.o 
  $(CPP) $(OFLAG)Guess2 Guess2.o 
 
Return.o: Return.cpp 
Declare.o: Declare.cpp 
Ifthen.o: Ifthen.cpp 
Guess.o: Guess.cpp 
Guess2.o: Guess2.cpp

La macro CPP affectée avec le nom du compilateur. Pour utiliser un autre compilateur, vous pouvez soit éditer le makefile, soit changer la valeur de la macro sur la ligne de commande de la manière suivante:

 
Sélectionnez
make CPP=cpp

Notez cependant, que ExtractCode.cpp utilise un système automatique pour construire les fichiers makefile des autres compilateurs.

La seconde macro OFLAG est le drapeau qui est uilisé pour indiquer le nom de fichier en sortie. Bien que de nombreux compilateurs supposent automatiquement que le fichier de sortie aura le même nom de base que le fichier d'entrée, d'autre ne le font pas (comme les compilateurs Linux/Unix, qui créent un fichier nommé a.out par défaut).

Vous pouvez voir deux règles de suffixes, une pour les fichiers cpp et une pour les fichiers c(au cas où il y aurait du code source C à compiler). La cible par défaut est all, et chaque ligne de cette cible est continuée en utilisant le caractère \, jusqu'à Guess2 qui est le dernier de la ligne et qui n'en a pas besoin. Il y a beaucoup plus de fichiers dans ce chapitre, mais seulement ceux là sont présents ici par soucis de brièveté.

Les règles de suffixes s'occupent de créer les fichiers objets (avec une extension .o) à partir des fichiers cpp, mais en général, vous devez spécifier des règles pour créer les executables, parce que, normalement, un exécutable est créé en liant de nombreux fichiers objets différents et make ne peut pas deviner lesquels. De plus, dans ce cas (Linux/Unix) il n'y a pas d'extension standard pour les exécutables, ce qui fait qu'une règle de suffixe ne pourrait pas s'appliquer dans ces situations simples. C'est pourquoi vous voyez toutes les règles pour construire les exécutables finaux énoncées explicitement.

Ce makefile choisit la voie la plus absolument sure possible; il n'utilise que les concepts basiques de cible et dépendance, ainsi que des macros. De cette manière il est virtuellement garanti de fonctionner avec autant de programmes make que possible. Cela a tendance à produire un makefile plus gros, mais ce n'est pas si grave puisqu'il est géneré automatiquement par ExtractCode.cpp.

Il existe de nombreuses autres fonctions de make que ce livre n'utilisera pas, ainsi que de nouvelles et plus intelligentes versions et variations de make avec des raccourcis avancés qui peuvent faire gagner beaucoup de temps. Votre documentation favorite décrit probablement les fonctions avancées de votre make, et vous pouvez en apprendre plus sur make grâce à Managing Projects with Make de Oram et Talbott (O'Reilly, 1993). D'autre part, si votre votre vendeur de compilateur ne fournit pas de make ou utilise un make non-standard, vous pouvez trouver le make de GNU pour virtuellement n'importe quel système existant en recherchant les archives de GNU (qui sont nombreuses) sur internet.

3.12. Résumé

Ce chapitre était une excursion assez intense parmi les notions fondamentales de la syntaxe du C++, dont la plupart sont héritées du C et en commun avec ce dernier (et résulte de la volonté du C++ d'avoir une compatibilité arrière avec le C). Bien que certaines notions de C++ soient introduites ici, cette excursion est principalement prévue pour les personnes qui sont familières avec la programmation, et doit simplement donner une introduction aux bases de la syntaxe du C et du C++. Si vous êtes déjà un programmeur C, vous pouvez avoir déjà vu un ou deux choses ici au sujet du C qui vous était peu familières, hormis les dispositifs de C++ qui étaient très probablement nouveaux pour vous. Cependant, si ce chapitre vous a semblé un peu accablant, vous devriez passer par le cours du cédérom Thinking in C: Foundations for C++ and Java(qui contient des cours, des exercices et des solutions guidées), qui est livré avec ce livre, et également disponible sur www.BruceEckel.com.

3.13. Exercices

Les solutions de exercices suivants peuvent être trouvés dans le document électronique The Thinking in C++ Annotated Solution Guide, disponible à petit prix sur www.BruceEckel.com.

  1. Créer un fichier d'en-tête (avec une extension ‘ .h'). Dans ce fichier, déclarez un groupe de fonctions qui varient par leur liste d'arguments et qui retournent des valeurs parmi les types suivants : void, char, int, and float. A présent créez un fichier .cpp qui inclut votre fichier d'en-tête et crée les définitions pour toutes ces fonctions. Chaque fonction doit simplement imprimer à l'écran son nom, la liste des paramètres, et son type de retour de telle façon que l'on sâche qu'elle a été appelée. Créez un second fichier .cpp qui inclut votre en-tête et définit int main( ), qui contient des appels à toutes vos fonctions. Compilez et lancez votre programme.
  2. Ecrivez un programme qui utilise deux boucles for imbriquées et l'opérateur modulo ( %) pour détecter et afficher des nombres premiers (nombres entiers qui ne sont divisibles que pas eux-même ou 1).
  3. Ecrivez un programme qui utilise une boucle while pour lire des mots sur l'entrée standard ( cin) dans une string. C'est une boucle while“infinie”, de laquelle vous sortirez (et quitterez le programme) grâce une instruction break. Pour chaque mot lu, évaluez le dans un premier temps grâce à une série de if pour “associer” une valeur intégrale à ce mot, puis en utilisant une instruction switch sur cet entier comme sélecteur (cette séquence d'événements n'est pas présentée comme étant un bon style de programmation ; elle est juste supposée vous fournir une source d'entraînement pour vous exercer au contrôle de l'exécution). A l'intérieur de chaque case, imprimez quelque chose qui a du sens. Vous devez choisir quels sont les mots “intéressants” et quelle est leur signification. Vous devez également décider quel mot signalera la fin du programme. Testez le programme en redirigeant un fichier vers l'entrée standard de votre programme (Pour économiser de la saisie, ce fichier peut être le fichier source de votre programme).
  4. Modifiez Menu.cpp pour utiliser des instructions switch au lieu d'instructions if.
  5. Ecrivez un programme qui évalue les deux expressions dans la section “précédence.”
  6. Modifiez YourPets2.cpp pour qu'il utilise différents types de données ( char, int, float, double, et leurs variantes). Lancez le programme et créez une carte de l'arrangement de mémoire résultant. Si vous avez accès à plus d'un type de machine, système d'exploitation, ou compilateur, essayez cette expérience avec autant de variations que you pouvez.
  7. Créez deux fonctions, l'une qui accepte un string* et une autre qui prend un string&. Chacune de ces fonctions devraient modifier l'objet string externe de sa propre façon. Dans main( ), créez et iniatilisez un objet string, affichez le, puis passez le à chacune des deux fonctions en affichant les résultats.
  8. Ecrivez un programme qui utilise tous les trigraphes pour vérifier que votre compilateur les supporte.
  9. Compilez et lancez Static.cpp. Supprimez le mot clé static du code, recompilez et relancez le, en expliquant ce qui s'est passé.
  10. Essayez de compiler et de lier FileStatic.cpp avec FileStatic2.cpp. Qu'est-il indiqué par le message d'erreur ? Que signifie-t-il ?
  11. Modifiez Boolean.cpp pour qu'il travaille sur des valeurs double plutot que des int s.
  12. Modifiez Boolean.cpp et Bitwise.cpp pour qu'ils utilisent des opérateurs explicites (si votre compilateur est conforme au standard C++ il les supportera).
  13. Modifiez Bitwise.cpp pour utiliser les fonctions définies dans Rotation.cpp. Assurez vous d'afficher les résultats de façon suffisamment claire pour être représentative de ce qui se passe pendant les rotations.
  14. Modifiez Ifthen.cpp pour utiliser l'opérateur ternaire if-else( ?:).
  15. Créez une struct ure qui manipule deux objets string et un int. Utilisez un typedef pour le nom de la struct ure. Créez une instance de cette struct, initialisez ses trois membres de votre instance, et affichez les. Récupérez l'adresse de votre instance, et affichez la. Affectez ensuite cette adresse dans un pointeur sur le type de votre structure. Changez les trois valeurs dans votre instance et affichez les, tout en utilisant le pointeur.
  16. Créez un programme qui utilise une énumération de couleurs. Créez une variable du type de cette enum et affichez les numéros qui correspondent aux noms des couleurs, en utilisant une boucle for.
  17. Amusez vous à supprimer quelques union de Union.cpp et regardez comment évolue la taille des objets résultants. Essayez d'affecter un des éléments d'une union et de l'afficher via un autre type pour voir ce qui se passe.
  18. Créez un programme qui définit deux tableaux d' int, l'un juste derrière l'autre. Indexez la fin du premier tableau dans le second, et faites une affectation. Affichez le second tableau pour voir les changements que ceci a causé. Maintenant essayez de définir une variable char entre la définition des deux tableaux, et refaites un test. Vous pourrez créer une fonction d'impression pour vous simplifier la tâche d'affichage.
  19. Modifiez ArrayAddresses.cpp pour travailler sur des types de données char, long int, float, et double.
  20. Appliquez la technique présentée dans ArrayAddresses.cpp pour afficher la taille de la struct et les adresses des éléments du tableau dans StructArray.cpp.
  21. Créez un tableau de string et affectez une string à chaque élément. Affichez le tableau grâce à une boucle for.
  22. Créez deux nouveaux programmes basés sur ArgsToInts.cpp pour qu'ils utilisent respectivement atol( ) et atof( ).
  23. Modifiez PointerIncrement2.cpp pour qu'il utilise une union au lieu d'une struct.
  24. Modifiez PointerArithmetic.cpp pour travailler avec des long et des long double.
  25. Definissez une variable du type float. Récupérez son adresse, transtypez la en unsigned char, et affectez la à un pointeur d' unsigned char. A l'aide de ce pointeur et de [ ], indexez la variable float et utilisez la fonction printBinary( ) définie dans ce chapitre pour afficher un plan de la mémoire du float(allez de 0 à sizeof(float)). Changez la valeur du float et voyez si vous pouvez expliquer ce qui se passe (le float contient des données encodées).
  26. Définissez un tableau d' int s Prenez l'adresse du premier élément du tableau et utilisez l'opérateur static_cast pour la convertir en void*. Ecrivez une fonction qui accepte un void*, un nombre (qui indiquera un nombre d'octets), et une valeur (qui indiquera la valeur avec laquelle chaque octet sera affecté) en paramètres. La fonction devra affecter chaque octet dans le domaine spécifié à la valeur reçue. Essayez votre fonction sur votre tableau d' int s.
  27. Créez un tableau constant ( const) de double s et un tableau volatile de double s. Indexez chaque tableau et utilisez const_cast pour convertir chaque élément en non- const et non- volatile, respectivement, et affectez une valeur à chaque element.
  28. Créez une fonction qui prend un pointeur sur un tableau de double et une valeur indiquant la taille du tableau. La fonction devrait afficher chaque élément du tableau. Maintenant créez un tableau de double et initialisez chaque élément à zero, puis utilisez votre fonction pour afficher le tableau. Ensuite, utilisez reinterpret_cast pour convertir l'adresse de début du tableau en unsigned char*, et valuer chaque octet du tableau à 1 (astuce : vous aurez besoin de sizeof pour calculer le nombre d'octets d'un double). A présent utilisez votre fonction pour afficher les nouveaux résultats. Pourquoi, d'après vous, chaque élément n'est pas égal à la valeur 1.0 ?
  29. (Challenge) Modifez FloatingAsBinary.cpp pour afficher chaque partie du double comme un groupe de bits séparé. Il vous faudra remplacer les appels à printBinary( ) avec votre propre code spécialisé (que vous pouvez dériver de printBinary( )), et vous aurez besoin de comprendre le format des nombres flottants en parallèle avec l'ordre de rangement des octets par votre compilateur (c'est la partie challenge).
  30. Créez un makefile qui compile non seulement YourPets1.cpp et YourPets2.cpp(pour votre compilateur en particulier) mais également qui exécute les deux programmes comme cible par défaut. Assurez vous d'utiliser la règle suffixe.
  31. Modifiez StringizingExpressions.cpp pour que P(A) soit conditionné par #ifdef pour autoriser le code en déboggage à être automatiquement démarré grâce à un flag sur la ligne de commande. Vous aurez besoin de consulter la documentation de votre compilateur pour savoir comment définir des valeurs du préprocesseur en ligne de commande.
  32. Définissez une fonction qui prend en paramètre un double et retourne un int. Créez et initialisez un pointeur sur cette fonction, et appelez là à travers ce pointeur.
  33. Déclarez un pointeur de fonction recevant un paramètre int et retournant un pointeur de fonction qui reçoit un char et retourne un float.
  34. Modifiez FunctionTable.cpp pour que chaque fonction retourne une string(au lieu d'afficher un message) de telle façon que cette valeur soit affichée directement depuis main( ).
  35. Créez un makefile pour l'un des exercices précédents (de votre choix) qui vous permettra de saisir make pour un build de production du programme, et make debug pour un build de l'application comprenant les informations de déboggage.

précédentsommairesuivant
Remarquez que toutes les conventions ne s'accorde pas à indenter le code en ce sens. La guerre de religion entre les styles de formattage est incessante. Conférez l'appendice A pour une description du style utilisé dans ce livre.
Merci à Kris C. Matson d'avoir suggéré ce sujet d'exercice.
A moins que vous ne considériez l'approche stricte selon laquelle “ tous les paramètres en C/C++ sont passés par valeur, et que la ‘valeur' d'un tableau est ce qui est effectivement dans l'identifiant du tableau : son adresse.” Ceci peut être considéré comme vrai d'un point de vue du langage assembleur, mais je ne pense pas que cela aide vraiment quand on travaille avec des concepts de plus haut niveau. L'ajout des références en C++ ne fait qu'accentuer d'avantage la confusion du paradigme “tous les passages sont par valeur”, au point que je ressente plus le besoin de penser en terme de “passage par valeur” opposé à “passage par adresse”

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005 Bruce Eckel. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.