Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Chap-3_Delannoy.pdf

Claude Delannoy Exercices en langage C++ 3e édition © Groupe Eyrolles, 1997, 1999, 2007, ISBN : 978-2-212-12201-5 Chapitre 3 Les fonctions Rappels Généralités Une fonction est un bloc d’instructions éventuellement paramétré par un ou plusieurs arguments et pouvant fournir un résultat nommé souvent « valeur de retour ». On distingue la définition d’une fonction de son utilisation, cette dernière nécessitant une déclaration. La définition d’une fonction se présente comme dans cet exemple : float fexple (float x, int b, int c) { // corps de la fonction } // en-tête de la fonction L’en-tête précise le nom de la fonction (fexple) ainsi que le type et le nom (muet) de ses différents arguments (x, b et c). Le corps est un bloc d’instructions qui définit le rôle de la fonction. Au sein d’une autre fonction (y compris main), on utilise cette fonction de cette façon : float fepxle (float, int, int) ; // déclaration de fexple ("prototype") ..... fexple (z, n, p) ; //appel fexple avec les arguments effectifs z, n et p © Éditions Eyrolles 33 Exercices en langage C++ La déclaration d’une fonction peut être omise lorsqu’elle est connue du compilateur, c’est-àdire que sa définition a déjà été rencontrée dans le même fichier source. Mode de transmission des arguments Par défaut, les arguments sont transmis par valeur. Dans ce cas, les arguments effectifs peuvent se présenter sous la forme d’une expression quelconque. En faisant suivre du symbole & le type d’un argument dans l’en-tête d’une fonction (et dans sa déclaration), on réalise une transmission par référence. Cela signifie que les éventuelles modifications effectuées au sein de la fonction porteront sur l’argument effectif de l’appel et non plus sur une copie. On notera qu’alors l’argument effectif doit obligatoirement être une lvalue du même type que l’argument muet correspondant. Toutefois, si l’argument muet est, de surcroît, déclaré avec l’attribut const, la fonction reçoit quand même une copie de l’argument effectif correspondant, lequel peut alors être une constante ou une expression d’un type susceptible d’être converti dans le type attendu. Ces possibilités de transmission par référence s’appliquent également à une valeur de retour (dans ce cas, la notion de constance n’a plus de signification). La notion de référence est théoriquement indépendante de celle de transmission d’argument ; en pratique, elle est rarement utilisée en dehors de ce contexte. L’instruction return L’instruction return sert à la fois à fournir une valeur de retour et à mettre fin à l’exécution de la fonction. Elle peut mentionner une expression ; elle peut apparaître à plusieurs reprises dans une même fonction ; si aucune instruction return n’est mentionnée, le retour est mis en place à la fin de la fonction. Lorsqu’une fonction ne fournit aucun résultat, son en-tête et sa déclaration comportent le mot void à la place du type de la valeur de retour, comme dans : void fSansValRetour (...) Lorsqu’une fonction ne reçoit aucun argument, l’en-tête et la déclaration comportent une liste vide, comme dans : int fSansArguments () Les arguments par défaut Dans la déclaration d’une fonction, il est possible de prévoir pour un ou plusieurs arguments (obligatoirement les derniers de la liste) des valeurs par défaut ; elles sont indiquées par le signe =, à la suite du type de l’argument, comme dans cet exemple : 34 © Éditions Eyrolles chapitre n° 3 Les fonctions float fct (char, int = 10, float = 0.0) ; Ces valeurs par défaut seront alors utilisées lorsqu’on appellera ladite fonction avec un nombre d’arguments inférieur à celui prévu. Par exemple, avec la précédente déclaration, l’appel fct ('a') sera équivalent à fct ('a', 10, 0.0) ; de même, l’appel fct ('x', 12) sera équivalent à fct ('x', 12, 0.0). En revanche, l’appel fct () sera illégal. Conversion des arguments Lorsqu’un argument est transmis par valeur, il est éventuellement converti dans le type mentionné dans la déclaration (la conversion peut être dégradante). Ces possibilités de conversion disparaissent en cas de transmission par référence : l’argument effectif doit alors être une lvalue du type prévu ; ctte dernière ne peut posséder l’attribut const si celui-ci n’est pas prévu dans l’argument muet ; en revanche, si l’agument muet mentionne l’attribut const, l’argument effectif pourra être non seulement une constante, mais également une expression d’un type quelconque dont la valeur sera alors convertie dans une variable temporaire dont l’adresse sera fournie à la fonction. Variables globales et locales Une variable déclarée en dehors de toute fonction (y compris du main) est dite globale. La portée d’une variable globale est limitée à la partie du programme source suivant sa déclaration. Les variable globales ont une classe d’allocation statique, ce qui signifie que leurs emplacements en mémoire restent fixes pendant l’exécution du programme. Une variable déclarée dans une fonction est dite locale. La portée d’une variable locale est limitée à la fonction dans laquelle elle est définie. Les variables locales ont une classe d’allocation automatique, ce qui signifie que leurs emplacements sont alloués à l’entrée dans la fonction, et libérés à la sortie. Il est possible de déclarer des variables locales à un bloc. On peut demander qu’une variable locale soit de classe d’allocation statique, en la déclarant à l’aide du mot-clé static, comme dans : static int i ; Les variables de classe statique sont, par défaut, initialisées à zéro. On peut les initialiser explicitement à l’aide d’expressions constantes d’un type compatible par affectation avec celui de la variable. Celles de classe automatique ne sont pas initialisées par défaut. Elles doivent être initialisées à l’aide d’une expression quelconque, d’un type compatible par affectation avec celui de la variable. © Éditions Eyrolles 35 Exercices en langage C++ Surdéfinition de fonctions En C++, il est possible, au sein d’un même programme, que plusieurs fonctions possèdent le même nom. Dans ce cas, lorsque le compilateur rencontre l’appel d’une telle fonction, il effectue le choix de la « bonne » fonction en tenant compte de la nature des arguments effectifs. D’une manière générale, si les règles utilisées par le compilateur pour sa recherche sont assez intuitives, leur énoncé précis est assez complexe, et nous ne le rappellerons pas ici. On trouvera de nombreux exemples de surdéfinition et un récapitulatif complet des règles dans nos ouvrages consacrés au C++ (publiés également aux éditions Eyrolles). Signalons simplement que la recherche d’une fonction surdéfinie peut faire intervenir toutes les conversions usuelles (promotions numériques et conversions standards, ces dernières pouvant être dégradantes), ainsi que les conversions définies par l’utilisateur en cas d’argument de type classe, à condition qu’aucune ambiguïté n’apparaisse. Les fonctions en ligne Une fonction en ligne (on dit aussi « développée ») est une fonction dont les instructions sont incorporées par le compilateur (dans le module objet correspondant) à chaque appel. Cela évite la perte de temps nécessaire à un appel usuel (changement de contexte, copie des valeurs des arguments sur la « pile »...) ; en revanche, les instructions en question sont générées plusieurs fois. Une fonction en ligne est nécessairement définie en même temps qu’elle est déclarée (elle ne peut plus être compilée séparément) et son en-tête est précédée du mot-clé inline, comme dans : inline fct ( ...) { 36 ..... } © Éditions Eyrolles chapitre n° 3 Les fonctions Exercice 24 Énoncé Quelle modification faut-il apporter au programme suivant pour qu’il devienne correct : #include <iostream> using namespace std ; main() { int n, p=5 ; n = fct (p) ; cout << "p = " << p << " n = " << n ; } int fct (int r) { return 2*r ; } La fonction fct est utilisée dans la fonction main, sans être encore « connue » du compilateur, ce qui provoque une erreur de compilation. Pour y rémédier, on dispose de deux possibilités : ■ déclarer la fonction avant son utilisation dans main, de préférence au début : #include <iostream> using namespace std ; main() { int fct (int) ; // déclaration de fct ; on pourrait écrire int fct (int x) int n, p=5 ; n = fct (p) ; cout << "p = " << p << " n = " << n ; } int fct (int r) { return 2*r ; } ■ placer la définition de la fonction avant celle de la fonction main : #include <iostream> using namespace std ; int fct (int r) { return 2*r ; } main() { int n, p=5 ; n = fct (p) ; cout << "p = " << p << " n = " << n ; } © Éditions Eyrolles 37 Exercices en langage C++ Il est conseillé d’utiliser la première démarche qui a le mérite de permettre de modifier les emplacements des définitions de fonctions dans un fichier source ou, même, de le scinder en plusieurs parties (compilation séparée). Exercice 25 Énoncé Écrire : • une fonction, nommée f1, se contentant d’afficher « bonjour » (elle ne possédera aucun argument, ni valeur de retour) ; • une fonction, nommée f2, qui affiche « bonjour » un nombre de fois égal à la valeur reçue en argument (int) et qui ne renvoie aucune valeur ; • une fonction, nommée f3, qui fait la même chose que f2, mais qui, de plus, renvoie la valeur (int) 0. Écrire un petit programme appelant successivement chacune de ces 3 fonctions, après les avoir convenablement déclarées (on ne fera aucune hypothèse sur les emplacements relatifs des différentes fonctions composant le fichier source). L’énoncé ne précisant rien, nous utiliserons une transmission d’arguments par valeur. Comme on n’impose pas d’ordre aux définitions des différentes fonctions dans le fichier source, on déclarera systématiquement toutes les fonctions utilisées. #include <iostream> using namespace std ; void f1 (void) { cout << "bonjour\n" ; } void f2 (int n) { int i ; for (i=0 ; i<n ; i++) cout << "bonjour\n" ; } int f3 (int n) { int i ; for (i=0 ; i<n ; i++) cout << "bonjour\n" ; return 0 ; } 38 © Éditions Eyrolles chapitre n° 3 Les fonctions main() { void f1 (void) ; void f2 (int) ; int f3 (int) ; f1 () ; f2 (3) ; f3 (3) ; } Exercice 26 Énoncé Quels résultats fournira ce programme : #include <iostream> using namespace std ; int n=10, q=2 ; main() { int fct (int) ; void f (void) ; int n=0, p=5 ; n = fct(p) ; cout << "A : dans main, n = " << n << " p = " << p << " q = " << q << "\n" ; f() ; } int fct (int p) { int q ; q = 2 * p + n ; cout << "B : dans fct, n = " << n << " p = " << p << " q = " << q << "\n" ; return q ; } void f (void) { int p = q * n ; cout << "C : dans f, n = " << n << " p = " << p << " q = " << q << "\n" ; } B : dans fct, n = 10, p = 5, q = 20 A : dans main, n = 20, p = 5, q = 2 C : dans f, n = 10, p = 20, q = 2 © Éditions Eyrolles 39 Exercices en langage C++ Exercice 27 Énoncé Écrire une fonction qui reçoit en arguments 2 nombres flottants et un caractère, et qui fournit un résultat correspondant à l’une des 4 opérations appliquées à ses deux premiers arguments, en fonction de la valeur du dernier, à savoir : addition pour le caractère +, soustraction pour -, multiplication pour * et division pour / (tout autre caractère que l’un des 4 cités sera interprété comme une addition). On ne tiendra pas compte des risques de division par zéro. Écrire un petit programme (main) utilisant cette fonction pour effectuer les 4 opérations sur les 2 nombres fournis en donnée. #include <iostream> using namespace std ; float oper (float v1, float { float res ; switch (op) { case '+' : res = v1 + break ; case '-' : res = v1 break ; case '*' : res = v1 * break ; case '/' : res = v1 / break ; default : res = v1 + } return res ; } v2, char op) v2 ; v2 ; v2 ; v2 ; v2 ; main() { float oper (float, float, char) ; float x, y ; // déclaration de oper cout << "donnez deux nombres réels : " ; cin >> x >> y ; cout cout cout cout << << << << "leur "leur "leur "leur somme est : " << différence est : " << produit est : " << quotient est : " << oper oper oper oper (x, (x, (x, (x, y, y, y, y, '+') '-') '*') '/') << << << << "\n" "\n" "\n" "\n" ; ; ; ; } 40 © Éditions Eyrolles chapitre n° 3 Les fonctions Exercice 28 Énoncé Transformer le programme (fonction + main) écrit dans l’exercice précédent de manière que la fonction ne dispose plus que de 2 arguments, le caractère indiquant la nature de l’opération à effectuer étant précisé, cette fois, à l’aide d’une variable globale. #include <iostream> using namespace std ; char op ; // variable globale pour la nature de l'opération // attention : doit être déclarée avant d'être utilisée float oper (float v1, float { float res ; switch (op) { case '+' : res = v1 + break ; case '-' : res = v1 break ; case '*' : res = v1 * break ; case '/' : res = v1 / break ; default : res = v1 + } return res ; } v2) v2 ; v2 ; v2 ; v2 ; v2 ; main() { float oper (float, float) ; float x, y ; /* prototype de oper */ cout << "donnez deux nombres réels : " ; cin >> x >> y ; op = cout op = cout op = cout op = cout '+' ; << "leur '-' ; << "leur '*' ; << "leur '/' ; << "leur somme est : " << oper (x, y) << "\n" ; différence est : " << oper (x, y) << "\n" ; produit est : " << oper (x, y) << "\n" ; quotient est : " << oper (x, y) << "\n" ; } © Éditions Eyrolles 41 Exercices en langage C++ Il s’agissait ici d’un exercice d’« école » destiné à forcer l’utilisation d’une variable globale. Dans la pratique, on évitera autant que possible ce genre de programmation qui favorise trop les risques d’« effets de bord ». Exercice 29 Énoncé Écrire une fonction, sans argument ni valeur de retour, qui se contente d’afficher, à chaque appel, le nombre total de fois où elle a été appelée sous la forme : appel numéro 3 La meilleure solution consiste à prévoir, au sein de la fonction en question, une variable de classe statique. Elle sera initialisée une seule fois à zéro (ou à toute autre valeur éventuellement explicitée) au début de l’exécution du programme. Ici, nous avons, de plus, prévu un petit programme d’essai. #include <iostream> using namespace std ; void fcompte (void) { static int i ; // il est inutile, mais pas interdit, d'écrire i=0 i++ ; cout << "appel numéro " << i << "\n" ; } /* petit programme d'essai de fcompte */ main() { void fcompte (void) ; int i ; for (i=0 ; i<3 ; i++) fcompte () ; } Là encore, la démarche consistant à utiliser comme compteur d’appels une variable globale (qui devrait alors être connue du programme utilisateur) est à proscrire. 42 © Éditions Eyrolles chapitre n° 3 Les fonctions Exercice 30 Énoncé Écrire 2 fonctions à un argument entier et une valeur de retour entière permettant de préciser si l’argument reçu est multiple de 2 (pour la première fonction) ou multiple de 3 (pour la seconde fonction). Utiliser ces deux fonctions dans un petit programme qui lit un nombre entier et qui précise s’il est pair, multiple de 3 et/ou multiple de 6, comme dans cet exemple (il y a deux exécutions) : donnez un entier : 9 il est multiple de 3 --------------donnez un entier : 12 il est pair il est multiple de 3 il est divisible par 6 #include <iostream> using namespace std ; int mul2 (int n) { if (n%2) return 0 ; else return 1 ; } int mul3 (int n) { if (n%3) return 0 ; else return 1 ; } main() { int mul2 (int) ; int mul3 (int) ; int n ; cout << "donnez un entier : " ; cin >> n ; if (mul2(n)) cout << "il est pair\n" ; if (mul3(n)) cout << "il est multiple de 3\n" ; if (mul2(n) && mul3(n)) cout << "il est divisible par 6\n" ; } © Éditions Eyrolles 43 Exercices en langage C++ Exercice 31 Énoncé Écrire une fonction permettant d’ajouter une valeur fournie en argument à une variable fournie également en argument. Par exemple, l’appel (n et p étant entiers) : ajouter (2*p+1, n) ; ajoutera la valeur de l’expression 2*p+1 à la variable n. Écrire un petit programme de test de la fonction. Étant donné que la fonction doit être en mesure de modifier la valeur de son second argument, il est nécessaire que ce dernier soit transmis par référence. #include <iostream> using namespace std ; void ajoute (int exp, int & var) { var += exp ; return ; } main() { void ajoute (int, int &) ; int n = 12 ; int p = 3 ; cout << "Avant, n = " << n << "\n" ; ajoute (2*p+1, n) ; cout << "Après, n = " << n << "\n" ; } 44 © Éditions Eyrolles chapitre n° 3 Les fonctions Exercice 32 Énoncé Soient les déclarations suivantes : int fct (int) ; int fct (float) ; void fct (int, float) ; void fct (float, int) ; int n, p ; float x, y ; char c ; double z ; // // // // fonction fonction fonction fonction I II III IV Les appels suivants sont-ils corrects et, si oui, quelles seront les fonctions effectivement appelées et les conversions éventuellement mises en place ? a. fct (n) ; b. fct (x) ; c. fct (n, x) ; d. fct (x, n) ; e. fct (c) ; f. fct (n, p) ; g. fct (n, c) ; h. fct (n, z) ; i. fct (z, z) ; Les cas a, b, c et d ne posent aucun problème. Il y a respectivement appel des fonctions I, II, III et IV, sans qu’aucune conversion d’argument ne soit nécessaire. e. Appel de la fonction I, après conversion de la valeur de c en int. f. Appel incorrect, compte tenu de son ambiguïté ; deux possibilités existent en effet : conserver n, convertir p en float et appeler la fonction III ou, au contraire, convertir n en float, conserver p et appeler la fonction IV. g. Appel de la fonction III, après conversion de c en float. h. Appel de la fonction III, après conversion (dégradante) de z en float. i. Appel incorrect, compte tenu de son ambiguïté ; deux possibilités existent en effet : convertir le premier argument en float et le second en int et appeler la fonction III ou, au contraire, convertir le premier argument en int et le second en float et appeler la fonction IV. © Éditions Eyrolles 45 Exercices en langage C++ Exercice 33 Énoncé a. Transformer le programme suivant pour que la fonction fct devienne une fonction en ligne. #include <iostream> using namespace std ; main() { int fct (char, int) ; int n = 150, p ; char c = 's' ; p = fct ( c , n) ; cout << "fct (\'" << c } int fct (char c, int n) { int res ; if (c == 'a') res else if (c == 's') res else res return res ; } // déclaration (prototype) de fct << "\', " << n << ") vaut : " << p ; // définition de fct = n + c ; = n - c ; = n * c ; b. Comment faudrait-il procéder si l’on souhaitait que la fonction fct séparément ? soit compilée a. Nous devons donc d’abord déclarer (et définir en même temps) la fonction fct comme une fonction en ligne. Le programme main s’écrit de la même manière, si ce n’est que la déclaration de fct n’y est plus nécessaire puisqu’elle apparaît auparavant (il reste permis de la déclarer, à condition de ne pas utiliser le qualificatif inline). #include <iostream> using namespace std ; inline int fct (char c, int n) { int res ; if (c == 'a') res = n + c else if (c == 's') res = n - c else res = n * c return res ; } main () { int n = 150, p ; char c = 's' ; p = fct (c, n) ; cout << "fct (\'" << c << "\', } 46 ; ; ; " << n << ") vaut : " << p ; © Éditions Eyrolles chapitre n° 3 Les fonctions b. Il s’agit en fait d’une question piège. En effet, la fonction fct étant en ligne, elle ne peut plus être compilée séparément. Il est cependant possible de la conserver dans un fichier d’extension h et d’incorporer simplement ce fichier par #include pour compiler le main. Cette démarche se rencontrera d’ailleurs fréquemment dans le cas de classes comportant des fonctions en ligne. Alors, dans un fichier d’extension h, on trouvera la déclaration de la classe en question, à l’intérieur de laquelle apparaîtront les « déclarations-définitions » des fonctions en ligne. © Éditions Eyrolles 47