Précédent Index Suivant

Chapitre 2   Les structures de base

2.1   Types de base et constantes

En C++, les types de base sont : Les constantes caractères s'écrivent entre ``quotes'' simples :
  'a'  'G'  '3'  '*' '['
Certains caractères de contrôle s'écrivent par des séquences prédéfinies ou par leur code octal ou hexadécimal, comme par exemple :
  \n  \t  \r  \135  \'  \x0FF
Les constantes entières peuvent s'écrire en notations décimale, hexadécimale (précédées de 0x2) ou octale (précédées de 03). Pour forcer la constante à être de type entier long, il faut ajouter un L à la fin, de même le suffixe u indique une constante non signée :

  12  -43  85  18642  54L 255u 38ul
  0xabfb  0x25D3a  0x3a
  0321  07215  01526
Les constantes réelles s'écrivent avec point décimal et éventuellement en notation exponentielle :

  532.652  -286.34  12.73
  52e+4  42.63E-12  -28.15e4
Les constantes de type chaînes de caractères (voir plus loin) s'écrivent entre guillemets :

  "Home sweet home"
  "Français, je vous ai compris."

2.2   Opérateurs et expressions

C++ offre un jeu très étendu d'opérateurs, ce qui permet l'écriture d'une grande variété d'expressions. Un principe général est que toute expression retourne une valeur. On peut donc utiliser le résultat de l'évaluation d'une expression comme partie d'une autre expression. De plus, le parenthésage permet de forcer l'ordre d'évaluation.

Les opérateurs disponibles sont les suivants :

2.2.1   Opérateurs arithmétiques

+
addition
-
soustraction
*
multiplication
/
division (entière ou réelle)
%
modulo (sur les entiers)

2.2.2   Opérateurs relationnels

> >= <= <
comparaisons
== !=
égalité et inégalité
!
négation (opérateur unaire)
&&
et relationnel
||
ou relationnel

2.2.3   L'affectation

=
affectation
Il faut bien noter que, comme en Java, le signe = est l'opérateur d'affectation et non de comparaison ; cela prête parfois à confusion, et entraîne des erreurs difficiles à discerner. À noter aussi que l'affectation est une expression comme une autre, c'est-à-dire qu'elle retourne une valeur. Il est donc possible d'écrire :
  a = b = c+2;
ceci revenant à affecter à b le résultat de l'évaluation de c+2, puis à a le résultat de l'affectation b = c+2, c'est-à-dire la valeur qu'on a donnée à b. Remarquez l'ordre d'évaluation de la droite vers la gauche.

2.2.4   Opérateurs d'incrémentation et de décrémentation

++
incrémentation
--
décrémentation
Ces opérateurs, qui ne peuvent être appliqués que sur les types scalaires, peuvent s'employer de deux manières : en principe, s'ils préfixent une variable, celle-ci sera incrémentée (ou décrémentée) avant utilisation dans le reste de l'expression ; s'ils la postfixent, elle ne sera modifiée qu'après utilisation. Ainsi :
  a = 5; b = 6;
  c = ++a - b;
donnera à c la valeur 0, alors que
  a = 5; b = 6;
  c = a++ - b;
lui donnera la valeur -1.

Faites cependant attention dans les expressions un peu complexes où l'on réutilise la même variable plusieurs fois : l'ordre d'évaluation n'est pas garanti, et l'expression peut donc avoir des résultats différents suivant la machine utilisée. Par exemple, le résultat de l'expression suivante est indéfini :
  t[++a] = a;

2.2.5   Opérateurs logiques

Ce sont les opérateurs permettant d'effectuer des opérations au niveau des bits (masquages).

&
and. Exemple : a & 0x000F extrait les 4 bits de poids faible de a.
|
or. Ainsi, b = b | 0x100 force à 1 le 9ème bit de b.
^
xor.
<<
shift à gauche. a = b << 2 met dans a la valeur de b où tous les bits ont été décalés de 2 positions vers la gauche.
>>
shift à droite.
~
complément à 1 (opérateur unaire).

2.2.6   Modifier la valeur d'une variable

Nous avons déjà vu l'affectation, l'incrémentation et la décrémentation. Il arrive très souvent qu'on calcule la nouvelle valeur d'une variable en fonction de son ancienne valeur. C++ fournit pour cela un jeu d'opérateurs combinés, de la forme

<variable> <op>= <expr>

où <op> est un opérateur. Une telle expression est équivalente à l'expression :

<variable= <variable> <op> <expr>

+=
a += b équivaut à a = a + b; --- À noter : a++a += 1a = a + 1
-=
idem, de même que *=, /=, %=, <<=, >>=, &=, |= et ^=.

2.2.7   Expressions conditionnelles

expr1 ? expr2 : expr3
est évaluée de la manière suivante :

si expr1 alors expr2
  sinon expr3
fsi

Cela est pratique par exemple pour calculer le maximum de 2 nombres sans passer par une fonction :
   z = (a > b) ? a : b;
Cette construction pourrait bien sûr s'exprimer avec une structure conditionnelle de la forme si--alors--sinon, mais l'écriture sous forme d'expression conditionnelle est plus compacte ; les ``vrais'' programmeurs C++ sont même convaincus qu'elle est plus lisible !

2.2.8   Conversions de types

On désire souvent changer le type du résultat retourné par une expression. Pour cela existe le mécanisme de cast. Celui-ci a été profondément modifié par la norme définitive de C++ ; comme beaucoup de casts ``ancien régime'' existent encore, nous expliquons ici les deux, en insistant sur la norme officielle. Nous vous recommandons d'ailleurs d'utiliser les constructions de cette dernière. Certaines explications font référence à des notions qui ne sont expliquées que dans les chapitres suivants...

L'ancien système

(<nom de type>) expression
retourne une valeur dont le type est celui qui est indiqué dans la première parenthèse, et qui est obtenue en convertissant le résultat de l'expression dans le type spécifié.

La norme officielle

Les opérateurs de conversion de types proposés par la norme sont particulièrement utiles dans des contextes tels que le polymorphisme, afin de convertir un objet d'une classe de base vers une classe dérivée. En effet, jusqu'à l'introduction de ces opérateurs, ce type de conversion délicate était entièrement à la charge du programmeur, qui devait vérifier la validité de la conversion avant de la réaliser. Du coup, cela pouvait engendrer quelques problèmes de sécurités, que ces nouveaux opérateurs sont censés résoudre.

Certains nouveaux casts se basent sur une fonctionnalité ajoutée il y a quelques années au langage C++ : la Run-Time Type Identification (RTTI)4. L'objectif de ces nouveaux opérateurs est de disposer d'une syntaxe améliorée, plus claire, d'une sémantique moins ambiguë et de réaliser des conversions de types en toute sécurité. Les opérateurs de cast sont au nombre de 4 :
  1. L'opérateur static_cast<T> (expr) : cet opérateur est utilisé pour effectuer des conversions qui sont résolues à la compilation. Il peut être utilisé pour convertir un pointeur (ou une référence) sur une classe de base vers un pointeur (ou une référence) sur une classe dérivée. L'opérateur n'effectue aucune vérification au cours de l'exécution (comme son nom l'indique) et doit donc être utilisé pour des conversions non-ambiguës. Mal utilisé, il renvoie un résultat indéfini. Il doit surtout être utilisé pour effectuer des conversions arithmétiques. Il est assez proche de la conversion de l'ancien système, mais permet de supprimer des trous de sécurité qui existaient.
  2. L'opérateur const_cast<T> (expr) : cet opérateur permet de supprimer la constance d'un objet. Ce n'est pas très naturel, mais utile dans certaines situations ; il doit être ainsi utilisé avec parcimonie. Un exemple d'utilisation se trouve ci-dessous :
    void f(Article &i)
    {
    }
    
    void g(const Article &j)
    {
      f(j);  // Erreur : j est constant et f n'attend pas un const
      f(const_cast<Article&> (j));  // Ok
    }
      
    De même, il peut être utilisé à l'intérieur d'une méthode de classe constante : en l'appliquant sur le pointeur this, on peut modifier par la suite l'objet courant (!)
  3. L'opérateur dynamic_cast<T> (expr) : c'est certainement l'un des nouveaux opérateurs les plus intéressants. Il peut uniquement être utilisé sur des pointeurs ou des références pour naviguer dans une hiérarchie de classes. Il peut être utilisé pour convertir un objet d'une classe dérivée vers un objet d'une classe de base ou inversement. Dans le premier cas, c'est une classique conversion statique qui est effectuée tandis que dans le second cas c'est une conversion dynamique qui est réalisée, en se basant sur le système RTTI. Dans ce cas, si la conversion est possible, l'opérateur de conversion renvoie un pointeur valide, ou un pointeur nul sinon. Cette fonctionnalité est très puissante comme le montre l'exemple suivant :
    #define MAXELTS 1000
    
    int main()
    {
      Article* lesArticle[MAXELTS];
    
      // Initialisation du tableau avec des articles hétérogènes
      // Deux cas différenciés : Alcools ou autres articles
    
      for (int i = 0; i < MAXELTS; i++) {
          BoissonAlcoolisee* ba;
    
          ba = dynamic_cast<BoissonAlcoolisee*> (lesArticle[i]);
    
          // Si l'article est un alcool, affichage du nom et du 
          // degré d'alcool. Affiche du nom uniquement sinon.
    
          if (ba != NULL)
            cout << ba->nom() << " (" << ba->degre() << ")" << endl;
          else
            cout << lesArticle[i]->nom() << endl;
      }
    }
      
  4. L'opérateur reinterpret_cast<T> (expr) : cet opérateur peut être utilisé pour convertir des objets dont les types ne sont pas en relation. Le résultat de la conversion est dépendante de l'implantation, et n'est ainsi pas portable. Il peut être utilisé dans certains contextes particuliers de conversion entre types de pointeurs de fonctions.
Conclusion : parmi les nouveaux opérateurs introduits, static_cast et le dynamic_cast sont à utiliser en priorité.

2.2.9   Récapitulatif

Pour finir ce long paragraphe, notons aussi que l'appel à une fonction est une expression comme une autre. Enfin, une expression peut dans certains cas être une suite de plusieurs expressions indépendantes séparées par des virgules ; voir à cet égard ce qui sera dit par la suite sur la structure itérative par exemple (cf. § 2.3.3).

Nous donnons ci-dessous un tableau récapitulatif des opérateurs de C++, classés dans l'ordre décroissant des priorités. Certains de ces opérateurs n'ont pas été mentionnés ci-dessus, mais sont décrits dans la suite du polycopié.



1 Fonction/Sélection/Portée () [] . -> ::
2 Unaire * & - ! ~ ++ -- typeid sizeof
    casts new delete
3 Multiplicatif * / %
4 Additif + -
5 Décalages << >>
6 Relationnels < > <= >=
7 Inégalité/Egalité == !=
8 et logique &
9 xor logique ^
10 ou logique |
11 et relationnel &&
12 ou relationnel ||
13 Affectation = <op>=
14 Conditionnel ? :
15 Exceptions throw
16 Virgule ,

2.3   Structuration d'un programme C++

Contrairement à Java, toutes les fonctions ne sont pas incluses dans une classe en C++. En ce sens, C++ hérite de son prédécesseur C une structure modulaire, et on peut très bien concevoir un programme C++ composé d'un grand nombre de modules compilés séparément. Chaque module est alors composé de fonctions, et éventuellement de déclarations de variables globales. Dans l'ensemble des modules, une fonction particulière, ayant pour nom main(), doit obligatoirement exister, et de manière unique. On l'appelle souvent le programme principal, par abus de langage. Il serait sûrement plus correct de dire que c'est le point d'entrée à l'exécution du programme.

C++ doit donc être considéré plutôt comme un langage ``multi-paradigmes'', permettant d'allier programmation object, programmation procédurale et programmation générique, et non comme un langage à objets ``pur et dur''.

Ceci étant dit, il est fortement conseillé de ne pas multiplier les fonctions hors classe ; dans bien des cas, seule la fonction main, et éventuellement quelques fonctions annexes à des fins utilitaires, ont vocation à être définies hors d'une structuration en classes. De même, nous déconseillons fortement l'emploi de variables globales ; comme en Java, il est beaucoup plus judicieux, lorsque cela est nécessaire, d'utiliser des variables de classe regroupées dans une classe ad hoc.

Chaque fonction a la syntaxe suivante :
typeRetour nomDeLaFonction(spécification des paramètres formels)

{
   suite de déclarations de variables locales et d'instructions
}
Les paramètres formels doivent être séparés par des virgules, et sont typés.

Précisons ces notions en voyant une petite fonction :
  int moyenne(int a, int b)
  {
    int c = (a+b)/2;
    return c;
  }
Remarque : comme en Java, on peut passer à la fonction main des paramètres correspondant aux paramètres d'appel du programme.

2.3.1   Instructions et blocs

Chaque instruction est terminée par un point-virgule. Un bloc est une suite d'instructions délimitées par une accolade ouvrante { et une accolade fermante }. À l'intérieur de tout bloc, on peut aussi définir des variables locales à ce bloc :
   if (n > 0) {
      int cumul = 0;
      for (int i=0 ; i < n ; i++) ....
      ....
      }
Il est conseillé de déclarer les variables locales le plus tard possible, seulement au moment où on en a effectivement besoin.

Attention à l'instruction vide ---; --- qui est source potentielle d'erreurs difficiles à détecter, comme dans :
  /* Exemple d'une instruction vide involontaire */
   for ( ... ) ;   // Ici le point-virgule indique une instruction vide
                   // à exécuter à chaque itération ; ce n'était pas
                   // forcément le souhait du programmeur
Vous avez peut-être remarqué que j'ai lâchement profité de l'occasion pour introduire les deux types de commentaires valides en C++. Les portions de code comprises entre /* et */ sont des commentaires, de même que celles comprises entre // et la fin de la ligne. Ceci étant dit, nous vous conseillons fortement de vous en tenir aux commentaires compris entre // et la fin de la ligne.

2.3.2   Structures conditionnelles

La condition s'exprime de la manière suivante :
   if (<expression>)
    <instruction-1>
 [ else
    <instruction-2>  ]
où l'exécution de l'une ou de l'autres des branches alors ou sinon va dépendre de l'évaluation de <expression> : si le résultat est vrai, on exécutera <instruction-1>, sinon on effectuera <instruction-2>. De manière tout à fait classique, s'il y a plusieurs instructions dans la partie alors ou la partie sinon, on mettra un bloc.

Quand il y a plusieurs conditions imbriquées et qu'il y a ambiguïté sur un else, on le rattache au if le plus proche.

Une autre instruction conditionnelle se comporte comme un branchement calculé. Par conséquent, il ne faut surtout pas oublier de mettre les break aux endroits nécessaires :
switch (<expression>) {
case <constante-1> : <suite d'instructions> break;
case <constante-2> : <suite d'instructions> break;
...
case <constante-n> : <suite d'instructions> break;
default : <suite d'instructions>
}

Si on ne met pas de break, l'exécution va continuer à la suite au lieu de sortir du switch, puisque les différentes constantes correspondent seulement à des étiquettes de branchement. Il y a parfois des cas où c'est l'effet souhaité ; mais il faut être très prudent et le documenter explicitement, le cas échéant !

2.3.3   Structures itératives

Plusieurs structures itératives existent en C++. Voici la première :
while (<expression>)
<instruction>
la partie <instruction> pouvant bien sûr être un bloc. C'est la structure tant-que classique.

Une autre structure itérative est la suivante :
for (<expr1> ; <expr2> ; <expr3>)
<instruction>
où <expr1>, <expr2> et <expr3> sont des expressions.

Souvenez-vous qu'une expression peut aussi être une suite d'expressions séparées par des virgules. C'est dans cette structure que cela est le plus utilisé. Cette construction est équivalente à :
<expr1>;
while (<expr2>) {
<instruction>;
<expr3>;
}

Résumons en disant que <expr1> indique l'initialisation avant entrée dans la boucle, <expr2> est la condition de poursuite de l'itération, et <expr3> est la partie exécutée à la fin de chaque itération.

Une ou plusieurs de ces expressions peuvent être vides ; en particulier :
   for ( ; ; )
est une boucle infinie !

Une dernière variante de la structure itérative est :
do
<instruction>
while (<expression>);
qui permet d'effectuer l'instruction (ou le bloc) une première fois avant le premier test sur la condition d'arrêt.

Nous avons déjà vu l'emploi de break dans les structures conditionnelles. En fait, break permet plus généralement de sortir prématurément et proprement d'une structure de contrôle. Ainsi, on peut l'utiliser dans une itération pour sortir sans passer par la condition d'arrêt. Donnons en exemple une boucle qui lit un caractère en entrée (par une fonction getchar()) et qui s'arrête sur la lecture du caractère '&' :
  for ( ; ; ) if ((c = getchar()) == '&') break;
Cette fonction peut bien sûr s'écrire plus simplement :
  while ((c = getchar()) != '&') ; // le point-virgule ici est
                                   // l'instruction vide !
     
Une autre instruction particulière qui peut être utile dans les itérations est continue, qui permet de se rebrancher prématurément en début d'itération.

Enfin, signalons que C++ permet aussi de faire goto ; mais comme nous sommes des informaticiens bien élevés qui ne disent jamais de gros mots, nous n'en parlerons pas...

2.4   Fonctions

Théoriquement, toute fonction retourne une valeur, qui peut être utilisée ou non. Toutefois, un mot clé particulier, void, permet d'indiquer qu'une fonction ne retourne pas de valeur (ce qui en fait stricto sensu une procédure et non une fonction !).

Le passage de paramètres peut se faire par valeur ou par référence. Le passage d'une référence se note par le caractère &. En voici un exemple avec une procédure qui échange les valeurs de deux variables :
    void swap(int& a, int& b)
    {
        int tmp = a; a = b; b = tmp;
    }
    ...
    int x, y;
    ...
    swap(x, y);
Il est conseillé de passer le plus systématiquement possible les objets (par opposition aux variables de type simple) par référence, et non par valeur.

Une référence peut également être déclarée constante, par exemple pour passer la référence d'un objet de grande taille, tout en interdisant l'accès en écriture dans la fonction ou la procédure. Avec un passage par valeur, l'objet serait dupliqué dans la pile d'exécution. Cette fonctionnalité très intéressante apporte un degré de contrôle supérieur à ce que permet Java sur les opérations permises sur l'objet qui est passé à une fonction. Nous vous conseillons de l'utiliser le plus possible.

En supposant l'existence d'un type Matrice décrivant une matrice, on peut par exemple écrire :

   void print(const Matrice& m)
   {
       // le compilateur interdit toute tentative
       // de modification de la variable m dans
       // le corps de la procédure print
   }
Une fonction peut être déclarée inline, comme dans l'exemple suivant :
   inline int max(int x, int y) { return (x > y ? x : y); }
La qualification inline indique au compilateur qu'il est préférable de remplacer chaque appel à la fonction par le code correspondant. Cette qualification n'est qu'indicative, et n'est en particulier pas prise en compte si elle est irréalisable, en particulier parce que le compilateur aurait besoin de connaître l'adresse de la fonction.

Comme en Java, une fonction peut être surchargée ; la discrimination est alors faite sur le nombre et le type des paramètres effectifs.

Notons aussi qu'il est possible de définir des valeurs par défaut pour certains paramètres de fonctions. Certaines fonctions sont appelées avec des paramètres qui changent rarement. Considérons par exemple une fonction ecranInit qui est chargée d'initialiser un écran d'ordinateur (en mode caractères). Dans 90% des cas, l'écran a les dimensions 24 lignes × 80 caractères et doit être initialisé dans 99% des cas avec le caractère ' ', qui provoque l'effacement de l'écran. Plutôt que de contraindre le programmeur à énumérer des paramètres qui sont généralement invariants, C++ offre la possibilité de donner des valeurs par défaut à certains paramètres lors de la déclaration de la fonction, comme ci-dessous :

void ecranInit(Ecran ecran, int lig = 24, int col = 80, char fond = ' ');

void ecranInit(Ecran ecran, int lig, int col, char fond)
{
  ...
}

int main()
{
  Ecran ec;

  ecranInit(ec);              // Éq. à : ecranInit(ec, 24, 80, ' ');
  ecranInit(ec, 26);          // Éq. à : ecranInit(ec, 26, 80, ' ');
  ecranInit(ec, 26, 92);      // Éq. à : ecranInit(ec, 26, 92, ' ');
  ecranInit(ec, 26, 92, '+');
}
Quelques remarques sur cette fonctionnalité :
  1. Une fonction peut définir des valeurs par défaut pour tous ses paramètres ou seulement pour une partie. Les paramètres acceptant des valeurs par défaut doivent se trouver après les paramètres sans valeur par défaut dans la liste des paramètres acceptés par une fonction.
  2. Les valeurs par défaut de chaque paramètre ne peuvent être mentionnées qu'une seule fois. Ainsi, par convention, ces valeurs sont généralement mentionnées dans la déclaration de la fonction et pas dans sa définition (donc dans le header et pas dans le fichier de suffixe .cpp ou .C).
  3. L'ordre de déclaration des paramètres est important : dans l'exemple ci-dessus il est en effet impossible de donner une valeur à col sans en donner une auparavant à lig. D'une façon générale, il faut donc positionner parmi les paramètres ayant des valeurs par défaut en premier ceux qui ont le plus de chances d'être modifiés.

2.5   Variables

Les variables d'un programme C++ peuvent avoir plusieurs classes de stockage :

automatiques
: c'est l'option par défaut pour toute variable interne d'une fonction. L'allocation se fait dans la pile d'exécution.
externes
ou globles : ce sont les variables définies à l'extérieur de toute fonction, et qui sont donc globales. Si on fait référence dans une fonction à une variable définie dans un autre module, on précisera qu'elle est externe par le mot-clé extern.

NB : Nous déconseillons fortement l'utilisation de variables externes.

statiques
: une variable globale statique (mot-clé static) est une variable dont le nom n'est pas exporté à l'édition de liens, et qui reste donc invisible hors du module où elle est définie.

Une variable interne à une fonction qui est déclarée statique est une variable rémanente : sa portée de visibilité est réduite à la fonction, mais elle n'est initialisée que la première fois où la fonction qui la déclare est appelée ; ensuite, sa valeur persiste d'un appel de la fonction à l'autre.

Le mot-clé static permet également de définir les variables et méthodes de classe (cf. § 3.8).

registres
: on peut demander qu'une variable de type entier, caractère ou pointeur soit implantée dans un registre, ce qui est souvent utile quand on veut aller vite. Les indices dans les tableaux et les pointeurs en mémoire sont souvent de bons candidats pour être déclarés comme registres.

Attention : seule une variable automatique peut être de type registre. De plus, le mot-clé register, à employer dans ce cas, ne donne qu'une indication au compilateur ; on ne garantit pas que la variable sera bien en registre, le compilateur n'ayant à sa disposition qu'un nombre limité de registres. Sauf cas très particuliers, comme en programmation système ou en micro-optimisation de code dans des boucles particulières, nous vous déconseillons de recourir à l'emploi de ce mot-clé.
Les déclarations de variables peuvent en plus être agrémentées de l'un des deux mots clés suivants :

const
: la variable désigne en fait une constante ; aucune modification n'est autorisée dans le programme.

volatile
: un objet déclaré volatile peut être modifié par un événement extérieur à ce qui est contrôlé par le compilateur (exemple : variable mise à jour par l'horloge système). Cette indication donnée au compilateur lui signale que toute optimisation sur l'emploi de cette variable serait hasardeuse.

2.6   Pointeurs

Les pointeurs sont des variables contenant des adresses. Ils permettent donc de faire de l'adressage indirect. Ainsi :
  int* px;
déclare une variable px qui est un pointeur sur un entier. La variable pointée par px est notée *px. Inversement, pour une variable
  int x;
on peut accéder à l'adresse de x par la notation &x. Ainsi, je peux écrire :
     px = &x;
ou
     x = *px;
Voici une autre manière d'écrire la fonction swap() qui échange deux entiers, cette fois-ci en passant par des pointeurs :
  void swap(int* px, int* py)
  {
     int temp;    // variable temporaire

     temp = *px;
     *px = *py;
     *py = temp;
  }
et pour échanger deux paramètres on appellera :
  int a,b;

  swap(&a,&b);
Attention : un des pièges les plus classiques en C++ est celui du pointeur non initialisé. Le fait d'avoir déclaré une variable de type pointeur ne suffit pas pour pouvoir déréférencer ce pointeur. Encore faut-il qu'il pointe sur une ``case'' mémoire valide. Pour reprendre l'exemple précédent, si j'écris
  int* px;
  *px = 3;
j'ai de très fortes chances d'avoir une erreur à l'exécution, puisque px ne désigne pas une adresse mémoire dans laquelle j'ai le droit d'écrire. Ce n'est qu'après avoir écrit par exemple px = &x; comme dans l'exemple ci-dessus que l'instruction *px = 3; devient valide.

2.6.1   Les tableaux

On déclare un tableau de la manière suivante :
     int a[10];
Il y a une très forte relation entre un pointeur et un tableau. Dans l'exemple précédent, a est en fait une constante de type adresse ; en effet, a est l'adresse du début du tableau. Par conséquent, on peut écrire les choses suivantes :
     int* pa, a[10];

     pa = &a[0];
ou
     pa = a;
Mais attention, il y a des différences dues au fait que a est une adresse constante alors que pa est une variable. Ainsi, on peut écrire
     pa = a;
mais il n'est pas valide d'écrire
     a = pa;
Quand on veut passer un tableau en paramètre formel d'une fonction, il est équivalent d'écrire :
     void funct(int tab[])
ou
     void funct(int* tab)
car on passe dans les deux cas une adresse.

Remarque : comme en Java, les indices, qui correspondent à des déplacements à partir du début du tableau, commencent toujours à 0.

Voyons maintenant comment on peut utiliser cette équivalence entre pointeurs et tableaux pour parcourir un tableau sans recalculer systématiquement l'adresse du point courant. Le problème est de calculer la moyenne d'une matrice 200 × 200 d'entiers.

     int tab[200][200];
     long int moyenne=0;
     int* p = tab;

     for (int i=0  ; i < 200 ; i++)
          for (int j=0 ; j < 200 ; j++ , p++)
              moyenne += *p;
     moyenne /= 40000;
Remarque : on peut écrire cela de manière encore plus efficace en profitant du fait qu'on utilise p pour l'incrémenter en même temps. Par ailleurs, une seule boucle suffit, et il est inutile d'utiliser des compteurs :

     int tab[200][200];
     long int moyenne=0;
     int* p = tab;
     int* stop = p + 200 * 200;
     for ( ; p < stop ; ) // on ne fait plus p++ ici
              moyenne += *p++; // on accède à la valeur pointée
                               // par p, puis on l'incrémente
     moyenne /= 40000;
Mais attention : le programme devient ainsi à peu près illisible, et je déconseille d'abuser de telles pratiques, qui ne sont justifiées que dans des cas extrêmes, où l'optimisation du code est un impératif.

Notez aussi qu'il est exclu de réaliser des ``affectations globales'' sur les tableaux, autrement que par le mécanisme des pointeurs (pas de recopie globale).

2.6.2   Allocation dynamique de mémoire

L'allocation et la libération dynamique de mémoire sont réalisées par les opérateurs new et delete. Une expression comprenant l'opération new retourne un pointeur sur l'objet alloué. On écrira donc par exemple :
  int* pi = new int;
Pour allouer un tableau dynamique, on indique la taille souhaitée comme suit :
  int* tab = new int[20];
Contrairement à Java, C++ n'a pas de mécanisme de ramasse-miettes ; c'est donc à vous de libérer la mémoire dynamique dont vous n'avez plus besoin (voir aussi la notion de destructeur pour les classes --- § 3.2) :
  delete pi;
  delete [] tab;
L'exemple ci-dessous reprend et illustre l'utilisation de new et de delete pour des variables et des tableaux :

// Allocation d'une variable et d'un tableau en C++

int main()
{
  int* pi = new int;
  int* tab = new int[10];

  if ((pi != NULL) && (tab != NULL)) {
    ...
    delete pi;
    delete [] tab;
  }
}

2.6.3   Arithmétique sur les pointeurs

Comme le montre l'exemple du § 2.6.1, un certain nombre d'opérations arithmétiques sont possibles sur les pointeurs, en particulier l'incrémentation.

Tout d'abord, on peut leur ajouter ou leur soustraire un entier n. Cela revient à ajouter à l'adresse courante n fois la taille d'un objet du type pointé. Ainsi, dans un tableau, comme nous l'avons vu, l'instruction p++ (qui est la même chose que p = p+1) fait pointer p sur la case suivante dans le tableau, c'est-à-dire que l'adresse est incrémentée de la taille (en octets) du type pointé.

On peut comparer deux pointeurs avec les opérateurs relationnels. Évidemment, cela n'a de sens que s'ils pointent dans une même zone (tableau par exemple).

Enfin, on peut soustraire deux pointeurs. Le résultat est un entier indiquant le nombre de ``cases'' de la taille du type pointé entre les deux pointeurs. Là encore, cela n'a de signification que si les deux pointeurs pointent dans la même zone contiguë.

2.6.4   Compléments sur les pointeurs

On pourrait encore dire beaucoup sur les pointeurs. Nous nous contentons ici de signaler quelques points que le lecteur intéressé par la poétique de C++ pourra approfondir dans la littérature appropriée :

2.7   La bibliothèque d'entrées-sorties

Nous ne prétendons pas couvrir dans ce polycopié les très nombreuses fonctionnalités couvertes par la bibliothèque standard C++. Cependant, il nous semble utile de donner quelques indications sur les entrées-sorties.

Pour utiliser la bibliothèque, il faut inclure son fichier de déclarations :
   #include <iostream>
Les opérations standards d'entrée et de sortie sont fournies par trois flots (streams), désignés par les variables suivantes :

Les opérateurs << et >> sont redéfinis pour permettre des écritures et lectures aisées :
   #include <iostream>
   #include <string>

   // ...

   cout << "Bonjour, comment vous appelez-vous ? ";
   string nom;
   cin >> nom;
   if (nom.string_empty()) {
      cerr << "erreur : nom vide" << endl;
   }
   else {
      cout << nom << ", donnez-moi maintenant votre âge : ";
      int age;
      cin >> age;
      if (age > 35) {
          cout << "Ouah, vous n'êtes plus tout jeune !" << endl;
      }
      else {
          cout << "Blanc bec !" << endl;
      }
   }
On notera au passage l'emploi de string, la bibliothèque de manipulation de chaînes de caractères C++ (cf. § 4.5.3) et de la constante endl, qui indique le passage à la ligne.

Bien entendu, la bibliothèque iostream fournit de nombreuses autres fonctionnalités d'entrée-sortie, et la bibliothèque fstream fournit les fonctionnalités de manipulation de fichiers. Nous nous sommes contentés ici de donner quelques rudiments vous permettant d'écrire vos tout premiers programmes... Vous trouverez en annexe A quelques indications supplémentaires sur les entrées-sorties.

2.8   Les namespaces

Les namespaces (espaces de noms) ont été introduits dans la norme définitive de C++. Dans des projets conséquents, il n'est en effet pas rare d'utiliser plusieurs bibliothèques C++ qui peuvent parfois définir les mêmes identificateurs, ce qui génère des conflits. Avec les espaces de noms, il ne doit plus y avoir de conflits de noms : les déclarations restent cachées dans un namespace jusqu'à ce qu'on fasse explicitement appel à lui. On rejoint à plusieurs égards la notion de package en Java...

Pour définir un namespace, il faut utiliser le mot-clé namespace comme cela est présenté :
namespace MonNameSpace
{
  // Toutes les déclarations sont regroupées
  // dans ce bloc

  int f();

  // D'autres déclarations...

  // Fin du namespace
};
La déclaration d'un même namespace peut être réalisée dans plusieurs fichiers d'interface, le namespace complet résultant alors de l'union des déclarations. Dans l'exemple présenté, le nom complet de la fonction f devient MonNameSpace::f, selon une syntaxe qui est similaire à celle des méthodes membres de classe. Cependant, afin de ne pas être contraint de désigner la fonction f par rapport à son namespace lorsqu'il n'y a pas de risque de conflit, des facilités d'utilisation sont disponibles grâce à la définition d'alias ou à l'utilisation du mot-clé using :
// Si on souhaite définir un alias sur le nom d'un
// namespace

namespace mon = MonNameSpace;

mon::f();   // Appelle MonNameSpace::f()

// Si la fonction f est la seule à être présente,
// on peut déclarer

using MonNameSpace::f;

f();   // Appelle MonNameSpace::f()

// Si on souhaite bénéficier de toutes les déclarations
// d'un namespace sans avoir à les préfixer du nom
// du namespace

using namespace MonNameSpace;
L'utilisation de ces directives using est cependant à bannir des fichiers d'interface, pour éviter des conflits qui pourraient apparaître dans les modules incluant ces interfaces.

Du coup, la plupart des déclarations de la bibliothèque standard C++ ont été regroupées dans un namespace, appelé std. Ainsi, un programme utilisant des fonctionnalités de la bibliothèque standard devra comporter la directive
  using namespace std;
pour éviter d'avoir à écrire explicitement std::cout, par exemple.


1
La présence d'un type booléen explicite est assez récente ; auparavant, les entiers étaient interprétés comme des booléens suivant leur valeur nulle ou non-nulle et par compatibilité, un certain nombre de compilateurs C++ continuent à accepter des valeurs entières à la place de valeurs booléennes.
2
zéro-X.
3
zéro.
4
Identification des types au cours de l'exécution.

Précédent Index Suivant