Passer des arguments à une fonction en C++
Il est important de comprendre précisément comment les arguments sont transmis à une fonction. Cela affecte la manière dont vous écrivez des fonctions et finalement comment elles fonctionnent. Il y a aussi un certain nombre de pièges à éviter. En général, les arguments de la fonction doivent correspondre au type et à la séquence de la liste des paramètres de la définition de la fonction.
Si vous spécifiez un argument de fonction d'un type qui ne correspond pas au type du paramètre, le compilateur insère une conversion implicite de l'argument vers le type du paramètre lorsque cela est possible. Les règles pour les conversions automatiques de ce type sont les mêmes que celles pour les conversions automatiques dans une instruction d'affectation. Si une conversion automatique n'est pas possible, vous obtiendrez un message d'erreur du compilateur. Si ces conversions implicites entraînent une perte potentielle de précision, les compilateurs émettent généralement un avertissement. Des exemples de telles conversions sont les conversions de long en int, de double en float, ou de int en float.
Il existe trois mécanismes par lesquels les arguments sont passés aux fonctions, le passage par valeur, le passage par adresse et le passage par référence.
Passage par valeur
Avec le mécanisme de passage par valeur, les valeurs des variables ou des constantes que vous spécifiez comme arguments ne sont pas du tout transmises à une fonction. Au lieu de cela, des copies des arguments sont créées, et ces copies sont transférées à la fonction. Par conséquent, le fait de modifier le paramètre de quelque manière que ce soit n'affectera pas l'original, et le passage d'un objet volumineux sera lent.
Exemple 1
#include <iostream> using namespace std; int changer(int); // prototype int main(void){ int a {5}, res; res=changer(a); cout << "Val de a Après l'exécution de la fonction : " << a << '\n'; cout << "Valeur retournée par la fonction : " << res << '\n' ; return 0; } int changer(int a) // définition de la fonction { a+=4; cout << "Val de a dans la fonction : " << a << '\n'; return a; }
Val de a dans la fonction : 9 Val de a Après l'exécution de la fonction : 5 Valeur retournée par la fonction : 9
Le résultat montre que l'ajout de 4 à a dans la fonction changer() n'a aucun effet sur la variable a dans main(). La variable a dans changer() est locale à la fonction, et elle se réfère à une copie de n'importe quelle valeur d'argument passée quand la fonction est appelée. Bien sûr, quand la valeur de a qui est locale à changer() est retournée, une copie de sa valeur courante est faite, et c'est cette copie qui est retournée au programme appelant.
Le passage par valeur est le mécanisme par défaut par lequel les arguments sont passés à une fonction. Il offre une grande sécurité à la fonction appelante en empêchant la fonction de modifier les variables qui lui appartiennent. Cependant, il arrive que l'on souhaite modifier des valeurs dans la fonction appelante. Existe-t-il un moyen de le faire lorsque vous en avez besoin ? Bien sûr que oui ; l'un des moyens consiste à utiliser un pointeur (passage par adresse).
Passage par adresse
Lorsqu'un type de paramètre de fonction est un pointeur, le mécanisme de passage par valeur fonctionne comme auparavant. Cependant, un pointeur contient l'adresse d'une autre variable ; une copie du pointeur contient la même adresse et pointe donc vers la même variable.
Si vous modifiez la définition de la première fonction changer() pour accepter un argument de type int*, vous pouvez passer l'adresse de celui-ci comme argument. Bien sûr, vous devez également modifier le code dans le corps de changer() pour déréférencer le paramètre pointeur. Le code est maintenant comme ceci :
Exemple 2
#include <iostream> using namespace std; int changer(int&); // prototype int main(void){ int a {5}, res; res=changer(a); // l'adresse de a est passée cout << "Val de a Après l'exécution de la fonction : " << a << '\n'; cout << "Valeur retournée par la fonction : " << res << '\n' ; return 0; } int changer(int& a) // définition de la fonction { a+=4; cout << "Val de a dans la fonction : " << a << '\n'; return a; }
Val de a dans la fonction : 9 Val de a Après l'exécution de la fonction : 9 Valeur retournée par la fonction : 9
Passage par référence
Comme vous vous en souvenez peut-être, une référence est un alias pour une autre variable. Vous pouvez également spécifier un paramètre de fonction comme une référence, auquel cas la fonction utilise le mécanisme de passage par référence avec l'argument. Lorsque la fonction est appelée, un argument correspondant à un paramètre de référence n'est pas copié. Au lieu de cela, le paramètre de référence est initialisé avec l'argument. Ainsi, il devient un alias pour l'argument dans la fonction appelante. Chaque fois que le nom du paramètre est utilisé dans le corps de la fonction, c'est comme s'il accédait directement à la valeur de l'argument dans la fonction appelante.
Vous spécifiez un type de référence en ajoutant & après le nom du type. L'appel d'une fonction qui a un paramètre de référence n'est pas différent de l'appel d'une fonction où l'argument est passé par valeur. L'utilisation de références améliore toutefois les performances avec des objets tels que le type string. Le mécanisme de passage par valeur copie l'objet, ce qui prendrait beaucoup de temps avec une longue chaîne et consommerait également beaucoup de mémoire. Avec un paramètre de référence, il n'y a pas de copie.
Exemple 3
#include <iostream> using namespace std; int changer(int&); // prototype int main(void){ int a {5}, res; res=changer(a); // passage par référence cout << "Val de a Après l'exécution de la fonction : " << a << '\n'; cout << "Valeur retournée par la fonction : " << res << '\n' ; return 0; } int changer(int& a) // définition de la fonction { a+=4; cout << "Val de a dans la fonction : " << a << '\n'; return a; }
Val de a dans la fonction : 9 Val de a Après l'exécution de la fonction : 9 Valeur retournée par la fonction : 9
Lorsque les arguments sont transmis par référence, les types de données primitifs et objets peuvent être modifiés, et les changements affecteront la variable originale.
Références vs pointeurs
Les références sont similaires aux pointeurs. Pour voir cette similitude, prenons l'exemple d'une fonction qui augmente la valeur de la variable donnée avec deux fonctions : une qui accepte un pointeur comme argument et une qui accepte une référence à la place :
Exemple 4
#include <iostream> using namespace std; void changer_ptr(int *a){ *a += 5; } void changer_ref(int& a){ a += 5; } int main(void){ auto val{4}; changer_ptr(&val); cout << "Après le premier appel val = " << val << '\n'; changer_ref(val); cout << "Après le deuxième appel val = " << val << '\n'; return 0; }
Après le premier appel val = 9 Après le deuxième appel val = 14
La différence la plus évidente est que pour passer un pointeur, vous devez d'abord prendre l'adresse d'une valeur en utilisant l'opérateur d'adressage &. À l'intérieur de la fonction, vous devez ensuite, bien sûr, déréférencer à nouveau ce pointeur pour accéder à la valeur. Pour une fonction qui accepte ses arguments par référence, vous ne devez faire ni l'un ni l'autre. Mais notez que cette différence est purement syntaxique ; au final, les deux ont le même effet. En fait, les compilateurs compilent généralement les références de la même manière que les pointeurs.
La caractéristique la plus distinctive d'un pointeur est qu'il peut être nullptr, alors qu'une référence doit toujours faire référence à quelque chose. Donc, si vous voulez permettre la possibilité d'un argument nul, vous ne pouvez pas utiliser une référence. Bien sûr, précisément parce qu'un paramètre de pointeur peut être nul, vous êtes presque obligé de toujours tester la présence de nullptr avant de l'utiliser. Les références ont l'avantage de ne pas avoir à se soucier des nullptrs.
Comme le montre l'exemple ci-dessus, la syntaxe d'appel d'une fonction avec un paramètre de référence n'est en effet pas différente de celle d'une fonction où l'argument est passé par valeur. D'une part, parce que vous n'avez pas besoin des opérateurs d'adressage (&) et de déréférencement (*), les paramètres de référence permettent une syntaxe plus élégante. Mais d'un autre côté, le fait qu'il n'y ait pas de différence syntaxique signifie précisément que les références peuvent parfois causer des surprises.