4.6. Passage de paramètres par variable ou par valeur
Il y a deux méthodes pour passer des variables en paramètre dans une fonction : le passage par valeur et le passage par variable. Ces méthodes sont décrites ci-dessous.
4.6.1. Passage par valeur
La valeur de l'expression passée en paramètre est copiée dans une variable locale. C'est cette variable qui est utilisée pour faire les calculs dans la fonction appelée.
Si l'expression passée en paramètre est une variable, son contenu est copié dans la variable locale. Aucune modification de la variable locale dans la fonction appelée ne modifie la variable passée en paramètre, parce que ces modifications ne s'appliquent qu'à une copie de cette dernière.
Le C ne permet de faire que des passages par valeur.
Exemple 4-4. Passage de paramètre par valeur
void test(int j) /* j est la copie de la valeur passée en
paramètre */
{
j=3; /* Modifie j, mais pas la variable fournie
par l'appelant. */
return;
}
int main(void)
{
int i=2;
test(i); /* Le contenu de i est copié dans j.
i n'est pas modifié. Il vaut toujours 2. */
test(2); /* La valeur 2 est copiée dans j. */
return 0;
}
4.6.2. Passage par variable
La deuxième technique consiste à passer non plus la valeur des variables comme paramètre, mais à passer les variables elles-mêmes. Il n'y a donc plus de copie, plus de variable locale. Toute modification du paramètre dans la fonction appelée entraîne la modification de la variable passée en paramètre.
Le C ne permet pas de faire ce type de passage de paramètres (le C++ le permet en revanche).
Exemple 4-5. Passage de paramètre par variable en Pascal
Var i : integer;
Procedure test(Var j : integer)
Begin
{La variable j est strictement égale
à la variable passée en paramètre.}
j:=2; {Ici, cette variable est modifiée.}
End;
Begin
i:=3; {Initialise i à 3}
test(i); {Appelle la fonction. La variable i est passée en
paramètres, pas sa valeur. Elle est modifiée par
la fonction test.}
{Ici, i vaut 2.}
End.
Puisque la fonction attend une variable en paramètre, on ne peut plus appeler test avec une valeur (test(3) est maintenant interdit, car 3 n'est pas une variable : on ne peut pas le modifier).
4.6.3. Avantages et inconvénients des deux méthodes
Les passages par variables sont plus rapides et plus économes en mémoire que les passages par valeur, puisque les étapes de la création de la variable locale et la copie de la valeur ne sont pas faites. Il faut donc éviter les passages par valeur dans les cas d'appels récursifs de fonction ou de fonctions travaillant avec des grandes structures de données (matrices par exemple).
Les passages par valeurs permettent d'éviter de détruire par mégarde les variables passées en paramètre. Si l'on veut se prévenir de la destruction accidentelle des paramètres passés par variable, il faut utiliser le mot clé const. Le compilateur interdira alors toute modification de la variable dans la fonction appelée, ce qui peut parfois obliger cette fonction à réaliser des copies de travail en local.
4.6.4. Comment passer les paramètres par variable en C ?
Il n'y a qu'une solution : passer l'adresse de la variable. Cela constitue donc une application des pointeurs.
Voici comment l'Exemple 4-5 serait programmé en C :
Exemple 4-6. Passage de paramètre par variable en C
void test(int *pj) /* test attend l'adresse d'un entier... */
{
*pj=2; /* ... pour le modifier. */
return;
}
int main(void)
{
int i=3;
test(&i); /* On passe l'adresse de i en paramètre. */
/* Ici, i vaut 2. */
return 0;
}
ہ présent, il est facile de comprendre la signification de & dans l'appel de scanf : les variables à entrer sont passées par variable.
4.6.5. Passage de paramètres par référence
La solution du C est exactement la même que celle du Pascal du point de vue sémantique. En fait, le Pascal procède exactement de la même manière en interne, mais la manipulation des pointeurs est masquée par le langage. Cependant, plusieurs problèmes se posent au niveau syntaxique :
• la syntaxe est lourde dans la fonction, à cause de l'emploi de l'opérateur * devant les paramètres ;
• la syntaxe est dangereuse lors de l'appel de la fonction, puisqu'il faut systématiquement penser à utiliser l'opérateur & devant les paramètres. Un oubli devant une variable de type entier et la valeur de l'entier est utilisée à la place de son adresse dans la fonction appelée (plantage assuré, essayez avec scanf).
Le C++ permet de résoudre tous ces problèmes à l'aide des références. Au lieu de passer les adresses des variables, il suffit de passer les variables elles-mêmes en utilisant des paramètres sous la forme de références. La syntaxe des paramètres devient alors :
type &identificateur [, type &identificateur [...]]
Exemple 4-7. Passage de paramètre par référence en C++
void test(int &i) // i est une référence du paramètre constant.
{
i = 2; // Modifie le paramètre passé en référence.
return;
}
int main(void)
{
int i=3;
test(i);
// Après l'appel de test, i vaut 2.
// L'opérateur & n'est pas nécessaire pour appeler
// test.
return 0;
}
Il est recommandé, pour des raisons de performances, de passer par référence tous les paramètres dont la copie peut prendre beaucoup de temps (en pratique, seuls les types de base du langage pourront être passés par valeur). Bien entendu, il faut utiliser des références constantes au maximum afin d'éviter les modifications accidentelles des variables de la fonction appelante dans la fonction appelée. En revanche, les paramètres de retour des fonctions ne devront pas être déclarés comme des références constantes, car on ne pourrait pas les écrire si c'était le cas.
Exemple 4-8. Passage de paramètres constant par référence
typedef struct
{
...
} structure;
void ma_fonction(const structure & s)
{
...
return ;
}
Dans cet exemple, s est une référence sur une structure constante. Le code se trouvant à l'intérieur de la fonction ne peut donc pas utiliser la référence s pour modifier la structure (on notera cependant que c'est la fonction elle-même qui s'interdit l'écriture dans la variable s. const est donc un mot clé « coopératif ». Il n'est pas possible à un programmeur d'empêcher ses collègues d'écrire dans ses variables avec le mot clé const. Nous verrons dans le Chapitre 8 que le C++ permet de pallier ce problème grâce à une technique appelée l'encapsulation.).
Un autre avantage des références constantes pour les passages par variables est que si le paramètre n'est pas une variable ou, s'il n'est pas du bon type, une variable locale du type du paramètre est créée et initialisée avec la valeur du paramètre transtypé.
Exemple 4-9. Création d'un objet temporaire lors d'un passage par référence
void test(const int &i)
{
... // Utilisation de la variable i
// dans la fonction test. La variable
// i est créée si nécessaire.
return ;
}
int main(void)
{
test(3); // Appel de test avec une constante.
return 0;
}
Au cours de cet appel, une variable locale est créée (la variable i de la fonction test), et 3 lui est affectée.
tent de faire du code beaucoup plus sûr.
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 :
o les types source Ts et destination Td sont compatibles ;
o 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.