4.7. Références et pointeurs constants et volatiles

L'utilisation des mots clés const et volatile avec les pointeurs et les références est un peu plus compliquée qu'avec les types simples. En effet, il est possible de déclarer des pointeurs sur des variables, des pointeurs constants sur des variables, des pointeurs sur des variables constantes et des pointeurs constants sur des variables constantes (bien entendu, il en est de même avec les références). La position des mots clés const et volatile dans les déclarations des types complexes est donc extrêmement importante. En général, les mots clés const et volatile caractérisent ce qui les précède dans la déclaration, si l'on adopte comme règle de toujours les placer après les types de base. Par exemple, l'expression suivante :

const int *pi;

peut être réécrite de la manière suivante :

int const *pi;

puisque le mot clé const est interchangeable avec le type le plus simple dans une déclaration. Ce mot clé caractérise donc le type int, et pi est un pointeur sur un entier constant. En revanche, dans l'exemple suivant :

int j;
int * const pi=&j;

pi est déclaré comme étant constant, et de type pointeur d'entier. Il s'agit donc d'un pointeur constant sur un entier non constant, que l'on initialise pour référencer la variable j.

Note : Les déclarations C++ peuvent devenir très compliquées et difficiles à lire. Il existe une astuce qui permet de les interpréter facilement. Lors de l'analyse de la déclaration d'un identificateur X, il faut toujours commencer par une phrase du type « X est un ... ». Pour trouver la suite de la phrase, il suffit de lire la déclaration en partant de l'identificateur et de suivre l'ordre imposé par les priorités des opérateurs. Cet ordre peut être modifié par la présence de parenthèses. L'annexe B donne les priorités de tous les opérateurs du C++.

Ainsi, dans l'exemple suivant :

const int *pi[12];
void (*pf)(int * const pi);

la première déclaration se lit de la manière suivante : « pi (pi) est un tableau ([]) de 12 (12) pointeurs (*) d'entiers (int) constants (const) ». La deuxième déclaration se lit : « pf (pf) est un pointeur (*) de fonction (()) de pi (pi), qui est lui-même une constante (const) de type pointeur (*) d'entier (int). Cette fonction ne renvoie rien (void) ».

Le C et le C++ n'autorisent que les écritures qui conservent ou augmentent les propriétés de constance et de volatilité. Par exemple, le code suivant est correct :

char *pc;
const char *cpc;

cpc=pc;   /* Le passage de pc à cpc augmente la constance. */

parce qu'elle signifie que si l'on peut écrire dans une variable par l'intermédiaire du pointeur pc, on peut s'interdire de le faire en utilisant cpc à la place de pc. En revanche, si l'on n'a pas le droit d'écrire dans une variable, on ne peut en aucun cas se le donner.

Cependant, les règles du langage relatives à la modification des variables peuvent parfois paraître étranges. Par exemple, le langage interdit une écriture telle que celle-ci :

char *pc;
const char **ppc;

ppc = &pc;   /* Interdit ! */

Pourtant, cet exemple ressemble beaucoup à l'exemple précédent. On pourrait penser que le fait d'affecter un pointeur de pointeur de variable à un pointeur de pointeur de variable constante revient à s'interdire d'écrire dans une variable qu'on a le droit de modifier. Mais en réalité, cette écriture va contre les règles de constances, parce qu'elle permettrait de modifier une variable constante. Pour s'en convaincre, il faut regarder l'exemple suivant :

const char c='a';      /* La variable constante. */
char *pc;              /* Pointeur par l'intermédiaire duquel
                          nous allons modifier c. */
const char **ppc=&pc;  /* Interdit, mais supposons que ce ne le
                          soit pas. */
*ppc=&c;               /* Parfaitement légal. */
*pc='b';               /* Modifie la variable c. */

Que s'est-il passé ? Nous avons, par l'intermédiaire de ppc, affecté l'adresse de la constante c au pointeur pc. Malheureusement, pc n'est pas un pointeur de constante, et cela nous a permis de modifier la constante c.

Afin de gérer correctement cette situation (et les situations plus complexes qui utilisent des triples pointeurs ou encore plus d'indirection), le C et le C++ interdisent l'affectation de tout pointeur dont les propriétés de constance et de volatilité sont moindres que celles du pointeur cible. La règle exacte est la suivante :

  1. On note cv les différentes qualifications de constance et de volatilité possibles (à savoir : const volatile, const, volatile ou aucune classe de stockage).

  2. Si le pointeur source est un pointeur cvs,0 de pointeur cvs,1 de pointeur ... de pointeur cvs,n-1 de type Ts cvs,n, et que le pointeur destination est un pointeur cvd,0 de pointeur cvd,1 de pointeur ... de pointeur cvd,n-1 de type Td cvs,n, alors l'affectation de la source à la destination n'est légale que si :

    • les types source Ts et destination Td sont compatibles ;

    • il existe un nombre entier strictement positif N tel que, quel que soit j supérieur ou égal à N, on ait :

      • si const apparaît dans cvs,j, alors const apparaît dans cvd,j ;

      • si volatile apparaît dans cvs,j, alors volatile apparaît dans cvd,j ;

      • et tel que, quel que soit 0<k<N, const apparaisse dans cvd,k.

Ces règles sont suffisamment compliquées pour ne pas être apprises. Les compilateurs se chargeront de signaler les erreurs s'il y en a en pratique. Par exemple :

const char c='a';
const char *pc;
const char **ppc=&pc;  /* Légal à présent. */
*ppc=&c;
*pc='b';               /* Illégal (pc a changé de type). */

L'affectation de double pointeur est à présent légale, parce que le pointeur source a changé de type (on ne peut cependant toujours pas modifier le caractère c).

Il existe une exception notable à ces règles : l'initialisation des chaînes de caractères. Les chaînes de caractères telles que :

"Bonjour tout le monde !"

sont des chaînes de caractères constantes. Par conséquent, on ne peut théoriquement affecter leur adresse qu'à des pointeurs de caractères constants :

const char *pc="Coucou !"; /* Code correct. */

Cependant, il a toujours été d'usage de réaliser l'initialisation des chaînes de caractères de la manière suivante :

char *pc="Coucou !";       /* Théoriquement illégal, mais toléré
                              par compatibilité avec le C. */

Par compatibilité, le langage fournit donc une conversion implicite entre « const char * » et « char * ». Cette facilité ne doit pas pour autant vous inciter à transgresser les règles de constance : utilisez les pointeurs sur les chaînes de caractères constants autant que vous le pourrez (quitte à réaliser quelques copies de chaînes lorsqu'un pointeur de caractère simple doit être utilisé). Sur certains systèmes, l'écriture dans une chaîne de caractères constante peut provoquer un plantage immédiat du programme.