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 chaqueargument. 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é voidsignifie, 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. returnsort de la fonction et revient juste après l'appel de cette fonction. Si returna un argument, cet argument devient la valeur de retour de la fonction. Si une fonction mentionne qu'elle renvoie un type particulier, chaque instruction returndoit renvoyer ce type. Plusieurs instructions returnpeuvent 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 ifqui est évalué à truesort 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 #includedans 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 .libou .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 Aest équivalente à la variable B. L'expression génère un Booléen trueou 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-elsepeut 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 à trueou 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 whileest

 
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 whilejusqu'à 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 whilen'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 trueou 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-whileest

 
Sélectionnez
do
    instruction
 while(expression);

la boucle do-whileest 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 whileordinaire, si l'expression conditionnelle est fausse à la première évaluation, l'instruction n'est jamais exécutée.

Si on utilise un do-whiledans notre Guess.cpp, la variable guessn'a pas besoin d'une valeur initiale factice, puisqu'elle est initialisée par l'instruction cinavant 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-whileet travaille simplement avec un while.

3.2.5. for

Une boucle forpermet 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 forest :

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

chacune des expressions initialisation, condition,ou sautpeut être laissée vide. l' initialisationest exécutée une seule fois au tout début. La conditionest 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 sauts'exécute.

Une boucle forest 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 in'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 breaket continue. breakforce la sortie de la boucle sans exécuter le reste des instructions de la boucle. continuearrê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 breaket 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é breakest 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é continueest utilisé pour remonter au début de la boucle while.

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

3.2.7. switch

Une instruction switcheffectue 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électeurest une expression qui produit une valeur entière. Le switchcompare le résultat du sélecteuravec 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 defaultest exécutée.

Vous remarquerez dans la définition ci-dessus que chaque casese 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 breakest facultatif. S'il est omis, votre case“s'étend” au suivant. Ainsi, le code du prochain cases'exécute jusqu'à ce qu'un breaksoit 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 switchest 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 stringcomme 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 ifet le comparer à la stringde 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 quitest 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é trueou false. Sélectionner ‘q' met le flag quità true. A la prochaine évaluation du sélecteur, quit == falseretourne falsedonc le corps de la boucle whilene s'exécute pas.

3.2.8. Du bon et du mauvais usage du goto

Le mot-clé gotoest supporté en C++, puisqu'il existe en C. Utiliser gotodé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 gotopeut 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 forextérieure, qui le cas échéant exécuterait un break. Cependant, si vous avez plusieurs boucles forou whileimbriqué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 catdépasse ‘Z' : (31)

 
Sélectionnez
//: C03:CatsInHats.cpp
// Simple demonstration de récursion
#include <iostream></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 catest plus petit que ‘Z', retirerChapeau( )sera appelé depuis l'intérieurde 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 catdonc 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 Aest un int, par exemple, l'expression ++Aest é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.het float.h(en C++ vous incluerez généralement climitset 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 charest fait pour le stockage des caractères et utilise au minimum 8 bits (un octet) de stockage, mais peut être plus grand. Un intstocke un nombre entier et utilise au minumum deux octets de stockage. Les types floatet doublestockent des nombres à virgule flottante, habituellement dans le format IEEE. floatest prévu pour les flottants simple précision et doubleest 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 boolfasse 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 booldu 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 boolet 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 intpour représenter un marqueur, le compilateur convertira implicitement un inten bool(les valeurs non nulles produisent truetandis 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 boolvers inten 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 boollorsque 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, signedet unsigned.

longet shortmodifient les valeurs maximales et minimales qu'un type de données peut stocker. Un intsimple 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, doubleet long double. « long float » n'est pas un type légal. Il n'y a pas de flottants short.

Les spécificateurs signedet unsigneddonnent 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 unsignedn'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. signedest implicite, sauf pour char; charpeut ê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 intpar shortou par long, le mot-clé intest 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 octetsde 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 motde 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.cpppeut ê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 longrend 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 chatest à quatre octets de chien, oiseauest à quatre octets de chat, etc. Il semble donc que, sur cette machine, un intest 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 intressemble 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 intou 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 ipaest un pointeur, tandis que ipbet ipcsont des intordinaires (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, aet ipaont été initialisés, et ipacontient 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érencele pointeur en utilisant le même opérateur que pour le définir, de la façon suivante :

 
Sélectionnez
*ipa = 100;

Maintenant, acontient 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( ), aest une variable locale, elle n'existe donc que durant l'appel à la fonction f( ). Comme c'est un argument de fonction, la valeur de aest 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 alorsque 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 xest bien sûr 47. Lorsque f()est appelée, un espace temporaire est créé pour stocker la variable apour la durée de l'appel de fonction, et aest 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 aet montrer que cette valeur a changé. Mais lorsque f( )se termine, l'espace temporaire qui a été créé pour adisparait, et on s'aperçoit que la seule connexion qui existait entre aet xavait lieu lorsque la valeur de xétait copiée dans a.

À l'intérieur de f( ), xest 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 voulezmodifier 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 pest la même que l'adresse de x– le pointeur ppointe en effet sur x. Si cela n'est pas suffisamment convaincant, lorsque pest déréférencé pour lui affecter la valeur 5, nous voyons que la valeur de xest é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' appeld'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.cppest 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 rréférence . En affectant quelque chose à r, vous affectez cette chose à la variable que rréférence. La seule manière de récupérer l'adresse contenue dans rest 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, floatet double, ainsi que les spécificateurs signed, unsigned, shortet longqui 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*)vpdit 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 intcomme un inten convertissant vpen 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 voiddevraient ê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 scp1est utilisable dans toutes les portées alors que la variable scp3n'est utilisable que dans la portée la plus imbriquée.

3.5.1. 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 forou de boucle while, à l'intérieur d'un segment conditionnel ifet à 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, pest 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 pde la portée extérieure est dans la même situation.

La définition de idans l'expression de contrôle de la boucle forest 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 iest la portée de l'expression contrôlée par la boucle for, ainsi vous pouvez réutiliser idans une prochaine boucle for. Ceci est pratique et communément utilisé en C++ : iest 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, ifet 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 crenvoie un boolconvertit 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, ifet switchn'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é externet 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 à globecomme 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 globeest créé par la définition dans Global.cpp, et le code dans Global2.cppaccède à cette même variable. Comme le code de Global2.cppest 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 Global2doit ê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.cppdans le deuxième volume de ce livre (téléchargeable à www.BruceEckel.com) lit ces balises et crée le makefileapproprié afin que tout se compile proprement (vous étudierez les makefiles à 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 automatiquesparce qu'elles sont créés automatiquement quand on entre dans le champ et disparaissent automatiquement quand le champ est fermé. Le mot clef autorend la chose explicite, mais les variables locales sont par défaut autoafin 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 registerdit 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 registreglobales ou static). Toutefois, vous pouvez utiliser une variable de registrecomme 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 statica 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 staticet 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 staticest 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 staticn'est pas utilisé, la valeur utilisée sera toujours ‘1'.

Le deuxième sens de staticest relié au premier dans le sens “indisponible en dehors d'un certain champ”. Quand staticest 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 fsest déclaré exister comme une externdan le fichier suivant, l'éditeur de liens ne la trouvera pas parce qu'elle a été déclarée staticdans FileStatic.cpp.

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

Le mot-clef staticpeut 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 externa 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 idoit 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 idéclaré plus tôt dans le fichier. Si vous définissiez istatic, vous diriez au compilateur que iest 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 interneet 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 staticen 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 conten 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 externn'est pas nécessaire en C, mais c'est parfois nécessaire pour consten 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 PIet vous ne pouvez prendre l'adresse de PI(donc vous ne pouvez pas passer un pointeur ou une référence à PI). PIne peut pas être une variable d'un type défini par l'utlisateur. Le sens de PIdure 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 constdit 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 constet 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 consta une portée, exactement comme une variable normale, vous pouvez donc "cacher" une constdans une fonction et être sûr que le nom n'affectera pas le reste du programme.

consta été emprunté au C++ et incorporé en C standard, mais de façon relativement différente. En C, le compilateur traite une constcomme une variable qui a une étiquette attachée disant "Ne me changez pas". Quand vous définissez une consten C, le compilateur crée un espace pour celle-ci, donc si vous définissez plus d'une constavec 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 consten 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 constdoit 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 esont 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 epour 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 : fou Fforce le type float, Lou lforce 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 constdit au compilateur “Cela ne change jamais” (ce qui permet au compilateur d'effectuer des optimisations supplémentaires), la déclaration volatiledit 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 volatileest 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 volatileafin que le compilateur ne suppose pas qu'il peut optimiser en négligeant plusieurs lectures des flags.

Notez que volatilepeut 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 constet volatileseront 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 xet affecter xau 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 PRINTet 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é boolen C++) truesi la relation est vraie, et falsesi 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 vraiou fauxselon les relations logiques de ses arguments. Souvenez vous qu'en C et en C++, une expression est truesi elle a une valeur non-égale à zéro, et falsesi elle a une valeur à zéro. Si vous imprimez un bool, vous verrez le plus souvent un ' 1' pour trueet ‘ 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 intavec floatou doubledans 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, intet 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 nonproduit 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 etbit à bit avec valet 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 intdoit être utilisé au lieu d'un charpour getvalparce que l'instruction “ cin >>” va sinon traiter le premier chiffre comme un caractère. En affectant getvalà aet 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.cppavant 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 notn'est pas le seul opérateur qui prend un argument unique. Son compagnon, le non logique( !), va prendre une valeur trueet 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 newet deleteen 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 newet deletesont introduits dans ce chapitre 4.

3.7.8. L'opérateur ternaire

Le ternaire if-elseest 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. aest affectée à la valeur de bsi le résultat de la décrémentation de bn'est pas zéro. Si bdevient zéro, aet bsont tous les deux assignés à -99. best toujours décrémenté, mais il est assigné à -99 seulement si la décrémentation fait que bdeviens 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 = bsera toujours évalué à vrai quand bn'est pas nul. La variable aest assignée à la valeur de b, et la valeur de best 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 andet orà la place des opérateurs logique associés. Les opérateurs bit à bit andet orutilise un des caractère ( &ou |), alors andet orlogique 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( casten 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 inten 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 habituellementutilisé avec des variables, plutôt qu' avecles 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 constet/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_castest 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_castincluent 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 ?Je sais,? 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 inten un longou floatn'est pas un problème parce que ces derniers peuvent toujours contenir que qu'un intpeut contenir. Bien que ce ne soit pas nécessaire, vous pouvez utiliser static_castpour mettre en valeur cette promotion.

La conversion inverse est montrée dans (2). Ici, vous pouvez perdre des données parce que un intn'est pas “large” comme un longou 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_castmet 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 consten un non constou d'un volatileen un non volatile, vous utilisez const_cast. C'est la seuleconversion 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 constsans un transtypage. L'ancien style de transtypage peut l'accomplir, mais le const_castest 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_castpré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_castpour 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 Xcontiens seulement un tableau de int, mais quand vous en créez un sur la pile comme dans X x, la valeur de chacun des ints 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 Xest 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 iest calculée par “l'addition” de szavec xp; le compilateur sait que vous voulez actuellement szpositions au départ de xpet utilise l'arithmétique de pointeur pour vous.

L'idée du reinterpret_castest 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 xpest seulement utile comme un int*, qui est vraiment une “réinterpretation” du Xoriginal.

Un reinterpret_castpeut 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 sizeofreste seul parce qu'il satisfait un besoin non usuel. sizeofvous 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, sizeofvous 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 sizeoftout type de char( signed, unsignedou simple) est toujours un, sans se soucier du fait que le stockage sous-jacent pour un charest actuellement un octet. Pour tous les autres types, le résultat est la taille en octets.

Notez que sizeofest 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;
} ///:~

sizeofpeut 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 trigraphesC, 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 classes 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 : typedefsuggè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 typedefest 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 ulongle 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 typedefest essentiel.

Un endroit pour lequel typedefest 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 xet 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 xet ysont tous les deux du type int*.

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

3.8.2. Combiner des variables avec des struct

Une structest 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 structdoit être terminée par un point-virgule. Dans notre main( ), deux instances de Structure1sont créées : s1et s2. Chacune de ces instances dispose de ses propres versions distinctes de c, i, fet d. ainsi s1et s2représentent des blocs de variables totalement indépendants. Pour sélectionner un des éléments encapsulé dans s1ou s2, vous utilisez un ‘ .', syntaxe que vous avez rencontré dans le précédent chapitre en utilisant des objets de classes C++ – comme les classes sont des structures é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 Structure1quand vous définissez des variables, vous devez dire struct Structure1. C'est ici que le typedefdevient 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 typedefde cette façon, vous pouvez prétendre (en C tout du moins ; essayez de retirer le typedefpour C++) que Structure2est un type natif, au même titre que intou float, quand vous définissez s1et 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 structa é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 structpendant sa définition. Dans ces cas, vous pouvez en fait répéter le structdans le nom de la structure à 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 sr1et sr2pointent tous les deux l'un vers l'autre, comme s'ils contenaient une donnée quelconque.

En fait, le nom de la structn'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 structures 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.cppci-dessous). Pour sélectionner les éléments d'un objet structparticulier, 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 structsppointe initialement sur s1, et les membres de s1sont 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 sppointe 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 classes) 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 structure.

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;
  }
} ///:~

shapeest une variable du type de données énuméré ShapeType, et sa valeur est comparée à la valeur de l'énumération. Puisque shapeest réellement juste un int, il est possible de lui affecter n'importe quelle valeur qu'un intpeut 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 classen C++, c'est ainsi que vous verrez que les enumsont 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 colorappelé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 colorvers un int, puis la valeur est incrémentée, et reconvertie en color. En C++, ce n'est pas autorisé, parce que colorest 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 bluesera 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 structqui contient tous les types possibles que vous auriez besoin d'enregistrer, soit vous utilisez une union. Une unionempile 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 unionpour é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' unionsont 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 intempilé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émentsdu 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 taillede asoit 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 vectorfournit 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 structures :

 
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 ide la structure 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 longn'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 tailledu 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 argcet argvcomme 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 forcommence à la valeur 1pour 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 floatet d'un doublesont 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 doublegrâ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 pointeursfait 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 structs:

 
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 structures (et de classes et d' unions).

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 #defineet #undefpour 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=onen 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 stringdu 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 aen 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 #ifdefpour 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” (“ funcPtrest 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(“ funcPtrest un pointeur sur une fonction qui ne prend aucun argument et renvoie void”).

Vous pouvez vous demander pourquoi *funcPtrrequiert 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 “ fp1est 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 “ fp2est 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 typedefenregistre à chaque fois les descriptions complexes. Cet exemple nous dit que “ fp3est 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 “ aest du type fp3.” typedefest 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. “ f4est 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 fpsoit 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 besoind'ê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 makegè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 makesuis les indications du makefilepour 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, makedéclanche le compilateur sur le fichier source. makerecompile uniquement les fichiers source qui ont été changés, ainsi que tout autre fichier source affecté par un fichier modifié. En utilisant makevous évitez de recompiler tous les fichiers de votre projet à chaque changement, et de vérifier que tout à été construit correctement. Le makefilecontient toutes les commandes pour construire votre projet. Apprendre makevous fera gagner beaucoup de temps et éviter beaucoup de frustration. Vous allez aussi découvrir que makeest le moyen typique d'installer un nouveau logiciel sous Linux/Unix (bien que, le makefilepour cela ait tendance à être largement plus complexe que ceux présentés dans ce livre, et que vous générerez souvent le makefilepour votre machine particulière pendant le processus d'installation).

Étant donné que makeest 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 makedisponible 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 makefileutilisé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 makecherche un fichier nommé makefiledans le répertoire en cours, que vous aurez créé si c'est votre projet. Ce fichier liste les dépendances de vos fichiers source. makeexamine la date des fichiers. Si un dépendant est plus ancien qu'un fichier dont il dépend, makeexécute la règledonnée juste après la définition de dépendance.

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

Un exemple simple de makefilepour 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.cppa une date plus récente que bonjour.exe, makeapplique la "règle" moncompilateur hello.cpp. Il peut y avoir de multiples dépendances et de multiples règles. De nombreux programmes de makeexigent 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 makeet être certain que tous les fichiers concernés seront reconstruits correctement.

Macros

Un makefilepeut 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 makefilede 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 CPPcomme 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 makepour chaque fichier cppde votre projet, alors que vous savez que c'est le même processus basique à chaque fois. Puisque makea é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 à makecomment convertir un fichier d'un type d'extension ( .cpp, par exemple) en un fichier d'un autre type d'extension ( .objou .exe). Une fois que makeconnaî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 maketrouve 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 à makequ'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 .SUFFIXESdit à makequ'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 cppen un avec l'extension exe" (si le fichier cppest 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 cppqui 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, makeexamine la première "cible" dans un fichier, et la construit, si vous n'avez pas spécifié autrement. Ainsi pour le makefilesuivant:

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

si vous tapez juste " make", ce sera cible1.exequi sera construit (à l'aide de la règle de suffixe par défaut) parce que c'est la première cible que makerencontre. Pour construire cible2.exevous 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.exeet (à l'aide de la règle de suffixe) regarde (1) si cible1.exeexiste et (2) si cible1.cppest 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 allpar 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.cppdu 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.cppcrée plusieurs makefilesdans 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.cppcrée un makefilemaître dans le répertoire racine où les fichiers du livre ont été décompressés, et ce makefiledescend dans chaque sous-répertoire et appelle makeavec le makefileapproprié. 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 makevarient d'un système à l'autre, seules les fonctionnalités communes de base sont utilisées dans les makefiles générés.

3.11.3. Un exemple de makefile

Comme indiqué, l'outil d'extraction de code ExtractCode.cppgénère automatiquement des makefilespour 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 makefilequi a été automatiquement généré pour ce chapitre par l'outil d'extraction du livre. Vous trouverez plus d'un makefiledans 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.cpputilise un système automatique pour construire les fichiers makefiledes autres compilateurs.

La seconde macro OFLAGest 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.outpar défaut).

Vous pouvez voir deux règles de suffixes, une pour les fichiers cppet 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'à Guess2qui 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 makene 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 makefilechoisit 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 makeque possible. Cela a tendance à produire un makefileplus gros, mais ce n'est pas si grave puisqu'il est géneré automatiquement par ExtractCode.cpp.

Il existe de nombreuses autres fonctions de makeque ce livre n'utilisera pas, ainsi que de nouvelles et plus intelligentes versions et variations de makeavec 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 makegrâce à Managing Projects with Makede Oram et Talbott (O'Reilly, 1993). D'autre part, si votre votre vendeur de compilateur ne fournit pas de makeou utilise un makenon-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 .cppqui 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 .cppqui 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 forimbriqué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 whilepour 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 ifpour “associer” une valeur intégrale à ce mot, puis en utilisant une instruction switchsur 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.cpppour utiliser des instructions switchau lieu d'instructions if.
  5. Ecrivez un programme qui évalue les deux expressions dans la section “précédence.”
  6. Modifiez YourPets2.cpppour 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 stringexterne 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é staticdu code, recompilez et relancez le, en expliquant ce qui s'est passé.
  10. Essayez de compiler et de lier FileStatic.cppavec FileStatic2.cpp. Qu'est-il indiqué par le message d'erreur ? Que signifie-t-il ?
  11. Modifiez Boolean.cpppour qu'il travaille sur des valeurs doubleplutot que des ints.
  12. Modifiez Boolean.cppet Bitwise.cpppour qu'ils utilisent des opérateurs explicites (si votre compilateur est conforme au standard C++ il les supportera).
  13. Modifiez Bitwise.cpppour 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.cpppour utiliser l'opérateur ternaire if-else( ?:).
  15. Créez une structure qui manipule deux objets stringet un int. Utilisez un typedefpour le nom de la structure. 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 enumet affichez les numéros qui correspondent aux noms des couleurs, en utilisant une boucle for.
  17. Amusez vous à supprimer quelques unionde Union.cppet regardez comment évolue la taille des objets résultants. Essayez d'affecter un des éléments d'une unionet 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 charentre 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.cpppour travailler sur des types de données char, long int, float,et double.
  20. Appliquez la technique présentée dans ArrayAddresses.cpppour afficher la taille de la structet les adresses des éléments du tableau dans StructArray.cpp.
  21. Créez un tableau de stringet affectez une string à chaque élément. Affichez le tableau grâce à une boucle for.
  22. Créez deux nouveaux programmes basés sur ArgsToInts.cpppour qu'ils utilisent respectivement atol( )et atof( ).
  23. Modifiez PointerIncrement2.cpppour qu'il utilise une unionau lieu d'une struct.
  24. Modifiez PointerArithmetic.cpppour travailler avec des longet 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 floatet 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 floatet voyez si vous pouvez expliquer ce qui se passe (le floatcontient des données encodées).
  26. Définissez un tableau d' ints Prenez l'adresse du premier élément du tableau et utilisez l'opérateur static_castpour 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' ints.
  27. Créez un tableau constant ( const) de doubles et un tableau volatilede doubles. Indexez chaque tableau et utilisez const_castpour convertir chaque élément en non- constet non- volatile, respectivement, et affectez une valeur à chaque element.
  28. Créez une fonction qui prend un pointeur sur un tableau de doubleet une valeur indiquant la taille du tableau. La fonction devrait afficher chaque élément du tableau. Maintenant créez un tableau de doubleet initialisez chaque élément à zero, puis utilisez votre fonction pour afficher le tableau. Ensuite, utilisez reinterpret_castpour convertir l'adresse de début du tableau en unsigned char*, et valuer chaque octet du tableau à 1 (astuce : vous aurez besoin de sizeofpour 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.cpppour afficher chaque partie du doublecomme 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.cppet 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.cpppour que P(A)soit conditionné par #ifdefpour 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 doubleet 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 intet retournant un pointeur de fonction qui reçoit un charet retourne un float.
  34. Modifiez FunctionTable.cpppour 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 makefilepour l'un des exercices précédents (de votre choix) qui vous permettra de saisir makepour un build de production du programme, et make debugpour 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”

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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.