Programmation C++/Les fonctions

Un livre de Wikilivres.
Aller à : navigation, rechercher

Les fonctions[modifier | modifier le wikicode]

Le C++ est un langage procédural, on peut définir des fonctions qui vont effectuer une certaine tâche. On peut paramétrer des fonctions qui vont permettre de paramétrer cette tâche et rendre ainsi les fonctions réutilisables dans d'autres contextes. Une fonction pourra appeler d'autres fonctions et ainsi de suite. Une fonction peut même s'appeler elle-même : on parle alors de fonctions récursives.

Prototype d'une fonction[modifier | modifier le wikicode]

Le prototype va préciser le nom de la fonction, donner le type de la valeur de retour de la fonction (void quand il n'y a pas de retour), et donner les types des paramètres éventuels.

syntaxe[modifier | modifier le wikicode]
type identificateur(paramètres);
Exemple[modifier | modifier le wikicode]
// prototype de la fonction f :
double f(double x,double y);
Rôle[modifier | modifier le wikicode]

Le rôle d'un prototype n'est pas de définir les instructions de la fonction, mais donner sa signature. Il est utilisé pour spécifier que la fonction existe, et est implémentée ailleurs (dans un autre fichier, une librairie, ou à la fin du fichier source).

On les trouve principalement dans les fichiers d'en-tête (extension .h).

Définition d'une fonction[modifier | modifier le wikicode]

La définition va reprendre le prototype mais va préciser cette fois le contenu de la fonction (le corps).

Syntaxe[modifier | modifier le wikicode]
type identificateur(paramètres)
{
    ... Liste d'instructions ...
}
Exemple[modifier | modifier le wikicode]
#include <iostream>
using namespace std;
 
// définition de la fonction f :
double f(double x, double y)
{
    double a;
    a = x*x + y*y;
    return a;
}
 
int main()
{
    double u, v, w;
    cout << "Tapez la valeur de u : "; cin >> u;
    cout << "Tapez la valeur de v : "; cin >> v;
 
    w = f (u, v); //appel de notre fonction
 
    cout << "Le résultat est " << w << endl;
    return 0;
}
Exemple avec prototype[modifier | modifier le wikicode]
#include <iostream>
using namespace std;
 
// prototype de la fonction f :
double f(double x, double y);
 
int main()
{
    double u, v, w;
    cout << "Tapez la valeur de u : "; cin >> u;
    cout << "Tapez la valeur de v : "; cin >> v;
 
    w = f (u, v); //appel de notre fonction
 
    cout << "Le résultat est " << w << endl;
    return 0;
}
 
// définition de la fonction f :
double f(double x, double y)
{
    double a;
    a = x*x + y*y;
    return a;
}

Dans cet exemple, le prototype est nécessaire, car la fonction est définie après la fonction main qui l'utilise. Si le prototype est omis, le compilateur signale une erreur.

Portée des variables[modifier | modifier le wikicode]

Présentation[modifier | modifier le wikicode]

Une fonction peut accéder :

  • à ses différents paramètres,
  • à ses variables définies localement,
  • aux variables globales (on évitera au maximum d'utiliser de telles variables).

On appelle environnement d'une fonction l'ensemble des variables auxquelles elle peut accéder. Les différents environnements sont donc largement séparés et indépendants les uns des autres. Cette séparation permet de mieux structurer les programmes.

Exemple[modifier | modifier le wikicode]
#include <iostream>
using namespace std;
 
int b;  // variable globale
 
double f(double x, double y)
{
    double a;  // variable locale à la fonction f
    a = x*x + y*y;
    return a;
}
 
double g(double x)
{
    int r, s, t;  // variables locales à la fonction g
    /* ... */
}
 
int main()
{
    double u, v, w;  // variables locales à la fonction main
    /* ... */
    return 0;
}
  • b est une variable globale (à éviter)
  • f peut accéder
    • à ses paramètres x et y.
    • à sa variable locale a.
    • à la variable globale b.
  • g peut accéder
    • à son paramètre x.
    • à ses variables locales r,s et t.
    • à la variable globale b.
  • la fonction main peut accéder
    • à ses variables locales u,v et w.
    • à la variable globale b.

Passage de paramètres par pointeur[modifier | modifier le wikicode]

Passer un paramètre par pointeur permet de modifier la valeur pointée en utilisant l'opérateur de déréférencement *.

Pour passer un pointeur comme argument de fonction, il faut en spécifier le type dans la définition de la fonction et (si pertinent), dans le prototype de la fonction, comme ceci :

void passagePointeur(int *);  // Prototype d'une fonction renvoyant void
// et prenant comme argument un pointeur vers un int
 
void passagePointeur(int * ptr)  // Définition d'une fonction renvoyant void
// et prenant comme argument un pointeur vers un int appelé ptr
{
    /* ... */
}

De même, il faut, lors de l'appel de fonction, non pas spécifier le nom de la variable comme lors d'un passage par valeur, mais son adresse. Pour ceci, il suffit de placer le signe & devant la variable.

int a = 5;  // Initialisation d'une variable a, de valeur 5
passageValeur( a  );  // Appel d'une fonction par valeur
passagePointeur( &a );  // Appel d'une fonction par pointeur

Notez donc le & devant la variable, ceci a pour effet de passer l'adresse mémoire de la variable.

Examinons le programme simple suivant :

#include <iostream>
using std::cout;
 
void passagePointeur(int *);
void passageValeur(int);
 
int main()
{
   int a = 5;
   int b = 7;
 
   cout << "a : " << a << "\nb : " << b;  // Affiche les deux variables
 
   passageValeur (a);  // Appel de la fonction en passant la variable a par valeur
   // Une copie de la valeur est transmise à la fonction
 
   passagePointeur (&b);  // Appel de la fonction en passant l'adresse de la variable b par pointeur
   // Une copie de l'adresse est transmise à la fonction
 
   cout << "\na : " << a << "\nb : " << b;  // Réaffiche les deux variables
 
   system ("PAUSE");
   return 0;
}
 
void passagePointeur(int * ptr)
{
   int num = 100;
   cout << "\n*ptr : " << *ptr;  // Affiche la valeur pointée
 
   * ptr = 9;  // Change la valeur pointée;
 
   ptr = &num;  // <-- modification de l'adresse ignorée par la fonction appelante
}
 
void passageValeur(int val)
{
   cout << "\nval : " << val;
 
   val = 12;  // <-- modification de la valeur ignorée par la fonction appelante
}

Le début du code n'est pas compliqué. On affiche les deux variables puis on appelle les deux fonctions, une par valeur et une par pointeur. Puis on affiche dans ces deux fonctions la valeur et on modifie la valeur. Mais vous remarquerez quelque chose ! De retour dans main, on réaffiche les deux variables a et b. a, qui a été passé par valeur, n'a pas été modifiée et a toujours sa valeur initiale (5), spécifiée à l'initialisation. Or, b n'a plus la même valeur que lors de son initialisation, 7, mais la valeur de 9. En effet, lors d'un appel par valeur, une copie de cette valeur est créée, donc lorsqu'un appel de fonction par valeur est effectué, on ne modifie pas la valeur d'origine mais une copie, ce qui fait que lorsqu'on retourne dans la fonction d'origine, les modifications effectuées sur la variable dans la fonction appelée ne sont pas prises en comptes. En revanche, lors d'un appel par pointeur, il s'agit de la variable elle même qui est modifiée (puisqu'on passe son adresse). Donc si elle est modifiée dans la fonction appelée, elle le sera également dans la fonction appelante.

Les avantages sont que cet appel nécessite moins de charge de travail pour la machine. En effet, par valeur, il faut faire une copie de l'objet, alors que par pointeur, seule l'adresse de l'objet est copiée. L'inconvénient, c'est qu'on peut accidentellement modifier dans la fonction appelée la valeur de la variable, ce qui se répercutera également dans la fonction appelante. La solution est simple : il suffit d'ajouter const dans le prototype de fonction de cette manière :

void passagePointeur(const int *);
// En d'autres termes, un pointeur vers un int constant

ainsi que la définition de fonction comme ceci :

void passagePointeur(const int * ptr)
{
    // * ptr = 9;  // <- maintenant interdit
}

Essayez donc de recompiler le programme mis plus haut avec ces deux choses, et vous verrez que vous aurez droit à un beau message d'erreur vous indiquant que vous n'avez pas le droit de modifier une valeur constante ;)

Passage de paramètres par référence[modifier | modifier le wikicode]

Passer un paramètre par référence a les mêmes avantages que le passage d'un paramètre par pointeur. Celui-ci est également modifiable. La différence est que l'opérateur de déréférencement n'est pas utilisé, car il s'agit déjà d'une référence.

Le passage de paramètre par référence utilise une syntaxe similaire au passage par pointeur dans la déclaration de la fonction, en utilisant & au lieu de *.

Par exemple :

 void incrementer(int& value)
 // value : référence initialisée quand la fonction est appelée
 {
     value++;
     // la référence est un alias de la variable passée en paramètre
 }
 
 void test()
 {
     int a = 5;
     cout << "a = " << a << endl;  // a = 5
 
     incrementer(a);
 
     cout << "a = " << a << endl;  // a = 6
 }

Le paramètre ainsi passé ne peut être qu'une variable. Sa valeur peut être modifiée dans la fonction appelée, à moins d'utiliser le mot const :

void incrementer(const int& value)
{
    value++;  // <- erreur générée par le compilateur
}
 
void test()
{
    int a = 5;
    cout << "a = " << a << endl;
    incrementer(a);
    cout << "a = " << a << endl;
}

La question que l'on peut se poser est puisque le passage par référence permet de modifier le paramètre, pourquoi l'en empêcher, et ne pas utiliser un simple passage par valeur ? La réponse se trouve lorsqu'on utilise des objets ou des structures. Le passage par valeur d'un objet ou d'une structure demande une recopie de la valeur de ses membres (utilisant un contructeur de recopie pour les objets). L'avantage de passer une référence est d'éviter la recopie. Le mot const permet de garantir qu'aucun membre de l'objet ou de la structure n'est modifié par la fonction.

Pointeur de fonction[modifier | modifier le wikicode]

Un pointeur de fonction stocke l'adresse d'une fonction, qui peut être appelée en utilisant ce pointeur.

La syntaxe de la déclaration d'un tel pointeur peut paraître compliquée, mais il suffit de savoir que cette déclaration est identique à celle de la fonction pointée, excepté que le nom de la fonction est remplacé par (* pointeur).

Obtenir l'adresse d'une fonction ne nécessite pas l'opérateur &. Il suffit de donner le nom de la fonction seul.

Exemple:

#include <iostream>
#include <iomanip>
 
using namespace std;
 
int (* pf_comparateur)(int a, int b);
// peut pointer les fonctions prenant 2 entiers en arguments et retournant un entier
 
int compare_prix(int premier, int second)
{
    return premier - second;
}
 
int main()
{
    pf_comparateur = compare_prix;
    cout << "compare 1 et 2 : " << pf_comparateur(1, 2) << endl;
    return 0;
}

La syntaxe d'appel à une fonction par un pointeur est identique à l'appel d'une fonction classique.

Passage de fonctions en paramètre[modifier | modifier le wikicode]

Il s'agit en fait de passer un pointeur de fonction.

Exemple:

void sort_array( int[] data, int count, int (* comparateur_tri)(int a, int b) );

Pour clarifier la lecture du code, il est préférable d'utiliser l'instruction typedef :

typedef int (* pf_comparateur)(int a, int b);
 
void sort_array( int[] data, int count, pf_comparateur comparateur_tri );