Ceci est une ancienne révision du document !
Chapitre précédent | Sommaire principal | Chapitre suivant |
---|
Les variables locales a une fonction ne sont donc pas partageable avec le code qui appelle une fonction, avec d'autres fonctions ou entre plusieurs appels d'une même fonction. Il est donc nécessaire de pouvoir partager des informations entre une fonction et le reste du code. Cela est réalisé via les paramètres de fonction. Pour rappel, la syntaxe générale d'une fonction est la suivante :
PARAMETRE_SORTIE NOM_FONCTION (PARAMETRES_ENTREE) { INSTRUCTIONS }
Une fonction va donc pouvoir recevoir zéro, un ou plusieurs paramètres en entrées et zéro ou un paramètre en sortie. Pour commencer, vous allez voir les fonctions qui ne prennent que des paramètres en entrée, les paramètres de sortie seront vu ensuite.
La syntaxe pour déclarer un paramètre de fonction est assez simple : il est identique à une déclaration de variable, avec un type et un identifiant (et une valeur par défaut, qui sera vu ensuite. Considérez pour le moment que le paramètre n'a pas de valeur par défaut).
TYPE IDENTIFIANT
Par exemple, pour une fonction qui prend un entier et une autre qui prend une chaîne de caractères.
void f(int i) { ... } void g(std::string s) { ... }
Pour la fonction f
, le paramètre est int i
, qui est déclaré avec un type int
et une identifiant i
. Même chose pour g
, qui contient un paramètre de type std::string
et qui s'appelle s
.
En pratique, un paramètre s'utilise de la même façon qu'une variable locale (en particulier, un paramètre suit les mêmes règles concernant la portée et la durée de vie : vous pouvez utiliser un paramètre dès qu'il est déclaré et jusqu'à la fin de la fonction).
Par exemple, pour afficher les valeurs des paramètres des fonctions f
et g
.
void f(int i) { std::cout << i << std::endl; } void g(std::string s) { std::cout << s << std::endl; }
Le dernier point à voir est comment appeler une fonction qui prend un paramètre. Pour cela, vous devez tout simplement donner une valeur entre les parenthèses lors de l'appel de la fonction.
Une valeur pourra être une variable constante ou non et/ou une littérale, selon les types de paramètres. Pour le moment, les paramètres sont “passés par valeur”, ce qui autorise l'utilisation de littérales et de variables. Les différents modes de passage de paramètres seront vu dans la suite de ce chapitre.
NOM_FONCTION(VALEUR)
Avec le code d'exemple précédent, vous pouvez donc écrire :
#include <iostream> void f(int i) { std::cout << i << std::endl; } void g(std::string s) { std::cout << s << std::endl; } int main() { f(123); // appel avec une littérale entière g("hello"); // appel avec une littérale chaîne const int i { 456 }; f(i); // appel avec une variable const std::string w { "world "}; g(w); // appel avec une variable }
affiche :
123 hello 456 world
Notez bien que même si dans cet exemple les variables dans la fonction main
et la fonctions f
ont le même nom i
, ce sont bien deux variables différentes (elles ne sont pas dans le même contexte). Comme vous le voyez avec la fonction g
, il n'y a aucune obligation d'utiliser le même nom entre l'appel de la fonction et le paramètre. (Heureusement ! Comme vous pouvez appeler une fonction plusieurs fois avec des valeurs différentes, cela serait très limitant de devoir utiliser le même nom).
Il existe deux termes qui sont proches, mais qui ont un sens légèrement différent : “paramètre” et “argument”. Dans la déclaration de la fonction, vous trouver des “paramètres de fonction”. Dans l'appel de fonction, ce sont des “arguments de fonction”.
void f(paramètres...) { } int main() { f(arguments...); }
Beaucoup de personne confondent les deux notions et cela n'est généralement pas problématique. Mais c'est souvent à ce genre de petits détails que l'on reconnait un développeur qui connait son sujet.
Pour déclarer une fonction qui prend plusieurs paramètres, vous devez donner la liste des paramètres séparés par une virgule.
TYPE IDENTIFIANT, TYPE IDENTIFIANT, TYPE IDENTIFIANT...
Il n'y a pas de limite théorique dans la norme C++ sur le nombre de paramètres que vous pouvez écrire (A vérifier). Par contre, les compilateurs imposent en général une limite. Et de toute façon, une fonction qui prend trop de paramètres deviendra difficilement lisible et c'est généralement le signe qu'il y a un problème d'organisation dans le code.
Par exemple, pour écrire une fonction qui prendre en paramètre un entier et une chaîne :
void f(int i, std::string s) { std::cout << i << std::endl; std::cout << s << std::endl; }
Une fonction qui prend plusieurs paramètres s'appelle en donnant la liste des arguments, séparés aussi par des virgules.
f(123, "hello");
Même si un paramètre correspond à un type, rien n'interdit que ce soit un type complexe, comme par exemple une std::pair
, un std::tuple
, ou une structure de données. Chacun de ces types peut contenir plusieurs autres types, ce qui permet en pratique de passer en paramètres de fonction autant de types que vous souhaitez.
Il faut donc éviter de passer trop d'information à une fonction et faire en sorte que les paramètres aient une cohérence entre eux. Par exemple, std::string
est une structure de données complexe, qui contient d'autres information en interne. Mais cela ne pose pas de problème, puisque ce type est manipulé comme un tout cohérent, sans avoir besoin de connaître ce qu'il contient exactement.
Avec std::pair
et std::tuple
, la situation est différente. En utilisant ces types, vous pouvez passer n'importe quelles informations, même des informations qui n'ont pas de lien logique entre elles. Même si cela semble être une plus grande liberté, cela nuit en fait à la compréhension du code. Il est donc assez rare de trouver des fonctions utilisant std::pair
et std::tuple
en paramètres. A la place, il est préférable de créer une structure de données, qui aura un nom explicite et facilitera la compréhension du code.
La liste des arguments doit correspondre à la liste des paramètres :
#include <iostream> void f() { std::cout << "f()" << std::endl; } void g(int i) { std::cout << "g() avec i=" << i << std::endl; } int main() { f(); // ok f(123); // erreur, trop d'argument g(); // erreur, pas assez d'argument g(123); // ok }
Exemple de message d'erreur pour f
(clang)
main.cpp:13:5: error: no matching function for call to 'f' f(123); ^ main.cpp:3:6: note: candidate function not viable: requires 0 arguments, but 1 was provided void f() { ^
Le compilateur indique qu'il ne trouve pas de fonction qui s'appelle f
et dont les paramètres sont compatible avec l'appel f(123)
(”no matching function”). Il indique à la ligne suivante qu'il connait une fonction f
qui pourrait être candidate (”candidate function”), mais qui prend zéro argument (”requires 0 arguments”), alors que l'appel utilise un argument (”but 1 was provided”).
Pour l'appel de la fonction g
, le message est le suivant :
main.cpp:15:5: error: no matching function for call to 'g' g(); ^ main.cpp:7:6: note: candidate function not viable: requires single argument 'i', but no arguments were provided void g(int i) { ^
De la même manière, le compilateur indique qu'il ne trouve pas de fonction correspondant à l'appel g()
, mais qu'il trouve une fonction qui se nomme g
et qui prend un argument.
En plus d'avoir le nombre d'arguments dans un appel de fonction qui correspond au nombre de paramètres déclarés dans une fonction, il faut aussi que les types soient les mêmes ou être implicitement convertible par le compilateur. Par exemple, le compilateur saura convertir sans problème un argument de type entier int
en paramètre de type réel double
ou une littérale chaîne (de type const char*
) en paramètre de type std::string
.
void f(double d) { } void g(std::string s) { } int main() { f(123); // conversion de int entre double g("hello"); // conversion de const char* en string }
En revanche, la conversion (par exemple) d'un argument réel en paramètre entier produira une erreur d'arrondi (comme vous avez vu lors de l'initialisation d'une variable, avec le narrowing).
#include <iostream> void f(int i) { std::cout << i << std::endl; } int main() { f(12.34); const double x = 12.34; f(x); }
affiche :
main.cpp:8:7: warning: implicit conversion from 'double' to 'int' changes value from 12.34 to 12 [-Wliteral-conversion] f(12.34); ~ ^~~~~ main.cpp:10:7: warning: implicit conversion turns floating-point number into integer: 'const double' to 'int' [-Wfloat-conversion] f(x); ~ ^ 2 warnings generated. 12 56
Notez que le compilateur fait la différence entre la conversion d'une littérale (-Wliteral-conversion) et la conversion d'une valeur réelle (-Wfloat-conversion). Le résultat de ces arrondis est visible dans le résultat affiché, puisque seules les parties entières sont conservées.
Pour terminer, lorsque les types des arguments et des paramètres ne sont pas du tout compatible, le compilateur indique une erreur spécifique (no known conversion).
#include <iostream> void f(int i) { std::cout << i << std::endl; } int main() { f("hello"); }
affiche :
main.cpp:8:5: error: no matching function for call to 'f' f("hello"); ^ main.cpp:3:6: note: candidate function not viable: no known conversion from 'const char [6]' to 'int' for 1st argument void f(int i) { ^ 1 error generated.
On peut souhaiter pouvoir appeler une fonction avec et sans un argument. Par exemple f qui prend un entier ou 0 si on en donne aucune valeur
Première solution, surcharger la fonction :
void f() { std::cout << "f()" << std::endl; } void f(int i) { std::cout << "f(int) avec i=" << i << std::endl; } int main() { f(); // ok, appel de f() f(123); // ok, appel de f(int i) }
Le compilateur trouve à chaque fois deux fonctions avec le même nom, mais pas d’ambiguïté pour savoir laquelle appeler.
Possibilité de simplifier en donnant une valeur par défaut à un paramètre :
void f(int i = 0) { std::cout << "f(int) avec i=" << i << std::endl; }
Dans ce cas, on indique que f peut prendre un entier. Si on ne donne pas de valeur, le compilateur peut utiliser la valeur par défaut :
int main() { f(); // ok, appel de f(int i) avec i = 0 f(123); // ok, appel de f(int i) avec i = 123 }
Bien sûr, il ne faut pas laisser les 2 fonctions, pour éviter les ambiguité :
void f() { std::cout << "f()" << std::endl; } void f(int i = 0) { std::cout << "f(int) avec i=" << i << std::endl; } int main() { f(); // erreur, appel de f() ou de f(int i) avec i = 0 ? }
Idem dans l'autre sens :
void f() { int i {}; } int main() { f(); // i existe dans f() std::cout << i << std::endl; // erreur, i n'existe pas dans ce bloc }
Une variable déclarée localement dans une fonction ne sera pas accessible dans le code qui appelle cette fonction. Utilisation de retour de fonction, permet de retourner 1 seule valeur. Utilisation du mot-clé return pour indiquer la valeur que la fonction doit retourner et remplacer void par le type de la valeur que l'on veut retourner.
int f() { int const i { 123 }; return i; } int main() { int const j = f(); std::cout << j << std::endl; }
Lorsque l'on appelle f, la variable i dans f est créée et initialisée avec la littérale 123. après le return, la valeur de i est retournée au code appelant, la variable j est créée et initialisée en copiant la valeur retournée par la fonction f (elle copie i) tandis que la variable i est détruite.
retourner directement une valeur :
int f() { return 123; } int main() { int const j = f(); std::cout << j << std::endl; }
Portée de variable fait que l'on peut utiliser 2 variables de même noms si portée différentes. Par exemple :
int f() { int const i { 123 }; return i; } int main() { int const i = f(); std::cout << i << std::endl; }
Il faut bien comprendre ici que même si les 2 variables dans la fonction f et dans main s'appellent toutes les 2 “i”, ce sont 2 variables différentes.
Mot clé return retourne immédiatement de la fonction. Si on écrit :
int f() { int const i { 123 }; return i; std::cout << "on est après le return" << std::endl; // n'est jamais exécuté } int main() { int const i = f(); std::cout << i << std::endl; }
le cout après le return n'est pas exécuté.
(ou overloading ou polymorphisme ad-hoc)
Polymorphisme : plusieurs fonctions de même nom. Des fonctions peuvent avoir le même nom, tant que les paramètres sont différents :
void f(int i) { std::cout << "f(int) avec i=" << i << std::endl; } void f(string s) { std::cout << "f(string) avec s=" << s << std::endl; }
Le compilateur choisit la fonction correspondante, selon le type que l'on donne en argument :
void f(int i) { std::cout << "f(int) avec i=" << i << std::endl; } void f(long int i) { std::cout << "f(long int) avec i=" << i << std::endl; } int main() { f(1); // 1 est une littérale de type int f(2L); // 2L est une littérale de type long int int i { 1 }; f(i); long int l { 2 }; f(l); }
affiche :
f(int) avec i=1 f(long int) avec i=2 f(int) avec i=1 f(long int) avec i=2
Le compilateur commence par rechercher s'il connait une fonction avec le nom correspondant. Par exemple pour f(1), il trouve 2 fonctions : f(int) et f(long int). Ensuite il regarde si l'un des types en paramètre correspondant au type en argument. Ici, c'est le cas, il appelle donc f(int).
Si on écrit :
#include <iostream> void f(long int i) { std::cout << "f(long int) avec i=" << i << std::endl; } int main() { f(1); // 1 est une littérale de type int }
Le compilateur trouve la fonction f, mais le paramètre ne correspond pas. Il regarde s'il peut faire une conversion. Ici, oui, on peut convertir implicitement un int en long int. il convertie donc 1 en 1L et appelle f(long int).
S'il ne trouve pas de conversion possible, il lance un message d'erreur. Par exemple, si on appelle f(“du texte”), le compilateur donne :
main.cpp:19:5: error: no matching function for call to 'f' f("une chaine"); ^ main.cpp:3:6: note: candidate function not viable: no known conversion from 'const char [11]' to 'int' for 1st argument void f(int i) { ^ main.cpp:7:6: note: candidate function not viable: no known conversion from 'const char [11]' to 'long' for 1st argument void f(long int i) { ^ 1 error generated.
Ce qui signifie qu'il ne trouve aucune fonction correspond à l'appel de f(“une chaine”), mais qu'il a 2 candidat (2 fonction qui ont le même nom) mais sans conversion possible (“no known conversion”).
Au contraire, dans certain cas, il aura plusieurs possible possible, soit parce que vous déclarez par erreur 2 fonctions avec les mêmes paramètres, soit parce que le compilateur peut faire 2 conversions pour 2 types. Dans le premier cas :
void f() { std::cout << "première fonction f" << std::endl; } void f() { std::cout << "seconde fonction f" << std::endl; }
produit le message :
main.cpp:7:6: error: redefinition of 'f' void f(int i) { ^ main.cpp:3:6: note: previous definition is here void f(int i) { ^
Quand le compilateur arrive à la ligne 7 et rencontre la seconde fonction f (qu'il connait déjà), il prévient qu'il connait déjà (“redefinition of 'f'”) et que la première version (“previous definition is here”) se trouve à la ligne 3.
L'autre cas est si plusieurs fonctions peuvent correspondent, l'appel est ambigu. Par exemple :
#include <iostream> void f(int i) { std::cout << "f(int) avec i=" << i << std::endl; } void f(long int i) { std::cout << "f(long int) avec i=" << i << std::endl; } int main() { f(1u); // 1 est une littérale de type unsigned int }
affiche le message d'erreur :
main.cpp:12:5: error: call to 'f' is ambiguous f(1u); // 1 est une littérale de type int ^ main.cpp:3:6: note: candidate function void f(int i) { ^ main.cpp:7:6: note: candidate function void f(long int i) { ^
Il existe une conversion de unsigned int vers int et vers long int. Il n'y a pas de priorité dans les conversions, le compilateur ne sait pas quelle conversion choisir et donc quelle fonction appeler. L'appel est ambuigu (“call to 'f' is ambiguous”), il trouve deux fonctions candidate (“candidate function”).
La méthode qui permet au compilateur de trouver la fonction correspondant à une appel s'appelle la résolution des noms (name lookup)
Comme cela a déjà été expliqué, certains types, dont les littérales chaînes (et plus généralement les pointeurs), sont convertissable automatiquement en booléen. Si on écrit la surcharge suivante :
void foo(bool) { std::cout << "f(bool)" << std::endl; } void foo(string const&) { std::cout << "f(string)" << std::endl; } foo("abc");
Ce code ne va pas afficher f(string)
, mais f(bool)
. Si on ajoute une fonction f(const char*)
et sera appelée en premier. La raison est que la littérale chaîne est de type const char*
, les fonctions seront appelée dans l'ordre suivant :
Donc attention lorsque vous écrivez une fonction qui prend bool, elle peut prendre aussi n'importe quel pointeur.
Solution C++14 : écrire “abc”s pour créer une littérale de type string.
Détailler le name lookup
Chapitre précédent | Sommaire principal | Chapitre suivant |
---|