Jusqu'à présent, la plupart des programmes que nous avons réalisés n'utilisaient que la fonction principale main. Mais alors que vos programmes deviennent progressivement plus complexes, vous pouvez simplifier votre tâche et améliorer la lisibilité en les subdivisant en sous-ensembles dénommés fonctions.
Une fonction désigne une entité de données et d'instructions qui fournie une solution à une (petite) partie bien définie d'un problème plus complexe. Elle peut faire appel à d'autres fonctions, leur transmettre des données et en recevoir en retour. L'ensemble des fonctions ainsi reliées doit alors être capable de résoudre le problème global.
Il arrive que nos programmes utilisent très fréquemment un même groupe d'instructions. Il devient très avantageux alors d'en faire une ou plusieurs fonctions particulières, que l'on peut même regrouper dans une bibliothèque (en dehors du fichier du programme principal). Ainsi, avec cette démarche, nous réalisons une étude une fois pour toute, et ensuite chaque programme profite de cet effort avec une utilisation beaucoup plus simplifiée. Cette démarche est fondamentale. Le programme principal devient alors une boîte à outils, chaque outil étant représenté par une fonction.
Un programme est très souvent développé en équipe. L'identification des fonctions du programme permet de répartir de travail au sein de cette équipe. Chaque programmeur aura ensuite la charge du développement d'une ou de plusieurs fonctions. La tâche du programmeur sera alors de les coder et de les tester, afin de garantir leur fonctionnement.
Une fonction peut s'apparenter à une opération par l'utilisateur. Une fonction est représentée par un nom. Les opérandes d'une fonction, appelée paramètres, sont spécifiés dans une liste entourée de parenthèses, les paramètres étant séparés par des virgules. Le résultat d'une fonction se nomme valeur de retour et le type de la valeur de retour s'appelle type de retour. Une fonction ne renvoyant pas de valeur a un type de retour void, ce qui veut dire qu'elle ne renvoie rien. Dans ce cas particulier, cette fonction s'appelle une procédure. Les actions qu'exécute une fonction sont spécifiées dans le corps de la fonction. Celui-ci, entouré d'accolades, est parfois appelé bloc de la fonction. Le type de retour de la fonction suivi du nom de la fonction, la liste des paramètres et le corps de la fonction, composent la définition de la fonction.

Nous allons mettre en œuvre une fonction qui calcule les puissances entières de la forme : z = yx = y.y….y avec y0=1 Nous appellerons la fonction du nom de puissance à laquelle nous associerons deux paramètres relatifs à x et y. Cette fonction doit renvoyer une valeur pour z puisse la récupérer.

<Type de retour> <nom de la fonction> (<liste des paramètres>) // Signature d'une fonction
Avant de pouvoir être utilisée (être appelée), il est nécessaire pour le compilateur, de connaître la signature d'une fonction. Cette signature informe le compilateur du type des paramètres et du résultat attendu de la fonction. A l'aide de ces données, le compilateur peut contrôler si le nombre et le type des paramètres d'une fonction sont corrects.
Pour que cette signature soit connue avant l'utilisation de la fonction, il est nécessaire qu'elle soit placée avant l'appel de cette fonction. Il existe deux façons de procéder.
Une déclaration de fonction consiste à placer la signature de la fonction suivie d'un point virgule sans le bloc de définition. Par ailleurs, il est possible d'omettre le nom des paramètres, le compilateur vérifie uniquement leurs types pour permettre la vérification de compatibilité lors de l'appel. Il peut être toutefois judicieux de nommer quand même ces paramètres afin d'informer l'utilisateur de se que l'on attend à priori comme valeurs possibles. Pour la définition de la fonction, par contre, il sera impératif de nommer les paramètres, sinon il serait impossible de les atteindre.

| Quelques déclarations de fonction | Commentaires associés |
| int longueurChaine( char * ) ; | longueurChaine attend comme paramètre une chaîne de caractères et renvoi la longueur sous forme d'un entier. Le nom du paramètre n'est pas spécifié. |
| void copieChaine( char * source, char * destination) ; | copieChaine est une procédure puisque, à priori, elle ne renvoie pas de valeur, si ce n'est au travers de son dernier paramètre puisque c'est un pointeur. Le nom des paramètres devient indispensable pour mieux maîtriser la fonction et surtout pour savoir dans quel ordre on doit placer les arguments. |
| void affiche (void) ; ou void affiche () ; | C'est une simple procédure sans paramètre, le but étant de morceler le programme en plusieurs parties pour avoir une meilleure lecture et une meilleure fiabilité. |
| int saisieClavier () ; | Cette fonction ne prend pas de paramètres, mais retourne une valeur entière qui peut être récupérée par une variable de même type. |
| int puissance( int ) ; int factoriel (int) ; |
Deux fonctions classiques. Le nom des paramètres n'est pas indispensable. Nous comprenons parfaitement le rôle de chacune d'entre elle. |
Lors de l'appel d'une fonction il y a suspension de l'exécution de la fonction en cours. On « saute » à l'exécution de la fonction appelée. Quand l'évaluation de la fonction appelée est terminée, la fonction suspendue reprend son exécution à l'endroit qui suit immédiatement l'appel. L'exécution d'une fonction se termine une fois exécutée la dernière instruction du corps de la fonction ou quand une instruction return est rencontrée dans le corps de la fonction.

Les fonctions utilisent un espace d'allocation de mémoire située sur la pile d'exécution du programme. Cet espace d'allocation reste associé à la fonction jusqu'à ce que celle-ci se termine. Dès lors, l'espace devient automatiquement disponible pour être réutilisé.
Chaque paramètre de fonction, ainsi que les variables internes, sont stockés sur cet espace d'allocation. Ces deux valeurs sont alors appelés, variables locales. Cette pile est différente de l'allocation mémoire statique, ce qui sous-entends que les valeurs des arguments passées à la fonction vont être copiées dans les paramètres et se retrouvent donc sur la pile (si c'est un passage par valeur – voir plus loin).
Les changements effectués sur ces variables locales (donc sur la pile), ne sont pas répercutées sur les valeurs des arguments. Chaque entité possède son propre espace mémoire. Une fois la fonction terminée, l'espace d'allocation de la pile est supprimée pour cette fonction, et donc, les valeurs locales sont définitivement perdues. Les valeurs locales sont donc des variables dynamiques qui possèdent, malgré tout, une identité, c'est-à-dire un nom.








Nous venons de voir, que le passage de paramètres permet à une fonction de pouvoir traiter des données qui ne sont pas définies dans son corps. Ces données sont passées à la fonction lors de son appel. Il existe globalement deux techniques de passage de paramètres :
C'est le type de transmission qui est le plus couramment utilisé. Avec ce système, la fonction manipule les copies locales des arguments. Ainsi, les fonctions n'obtiennent que les valeurs de leurs paramètres passés et elles n'ont pas accès au contenu des variables elles-mêmes. Les paramètres d'une fonction sont des variables locales qui sont initialisées automatiquement par les valeurs indiquées par les arguments lors de l'appel.
A l'intérieur de la fonction, nous pouvons donc changer les valeurs des paramètres sans influencer les valeurs originales dans les fonctions appelantes , ce qui procure une protection maximale pour les arguments. Dans certain cas Toutefois, il peut être nécessaire d'atteindre l'argument lui-même pour permettre le changement de sa valeur. C'est là qu'intervient la transmission par variable.

Dans cet exemple, nous proposons de fabriquer une fonction qui permet d'échanger le contenu des variables.
En prenant le passage par valeurs comme c'est le cas ici, les seuls échanges proposés se situent au niveau des variables locales à la fonction, sans qu'il y ait de répercutions sur les arguments n et p . Dans la plupart des cas, c'est très bien, puisque les arguments sont protégés de toute mauvaise utilisation. Dans le cas qui nous préoccupe, cela ne correspond spécialement pas à notre attente, puisque nous désirons échanger les valeurs entre les deux arguments.
Le passage par variable permet à la fonction appelée de pouvoir modifier le contenu de la variable passée en paramètre. Il existe deux techniques pour résoudre ce problème :
Envisageons les deux cas de figure sur notre problème pour arriver à réaliser l'échange proposé et commençons par la technique des pointeurs.

Avec des pointeurs comme paramètres
Avec des références comme paramètres
A l'aide de ces différents scénarii, nous pouvons presque conclure que :
Lorsque nous devons récupérer une valeur sans changer le contenu de l'argument, il faut alors proposer une transmission par valeur, et il suffit alors de faire une déclaration classique des paramètres.
Lorsque nous devons modifier directement le contenu de l'argument, il faut cette fois-ci proposer une transmission par variable en prenant si possible une référence pour que l'argument soit directement connecté.
La plupart du temps, ces principes demeurent vrais. Toutefois, il existe des situations où nous devons réaliser une analyse plus fine pour répondre parfaitement à l'attente de l'utilisateur. En fait, tout dépend du type d'argument.
Jusqu'à présent, nous nous sommes contenté de prendre des types simples comme paramètres de fonction. Dans ce chapitre, nous allons nous intéresser à des types définis par l'utilisateur ainsi que les tableaux et les chaînes de caractères.
Nous pouvons utiliser les principes évoqués plus haut au sujet des types de transmission. La déclaration des paramètres est alors identique à la déclaration que nous réalisons lorsque nous avons besoin de variables classiques. Prenons l'exemple de la structure sur les nombres complexe pour visualiser le comportement au sein de la pile.

La fonction affiche a juste besoin de récupéré la valeur de la structure. C'est pour cela, qu'à priori, nous utilisons le passage par valeur. Le résultat du programme est d'ailleurs le comportement attendu. Toutefois, avec ce système, nous copions tout le contenu de la structure sur la pile. Imaginons que nous ayons une structure volumineuse avec pas moins d'une dizaine de champs. Dans ce cas de figure, nous prenons alors une bonne partie de la pile pour récupérer tous ces champs. Par ailleurs, la copie demande un temps considérable par rapport à l'utilisation que nous en faisons. Il est alors judicieux de proposer une autre solution plus adaptée.
Plutôt que de copier tout le contenu de la structure, il serait préférable de se connecter directement à l'argument x en utilisant le principe des références. Cette démarche est judicieuse puisque nous obtenons un gain de temps considérable. Nous avons toutefois un problème. En effet, lorsque nous utilisons une référence sur un argument, cela veut normalement dire que nous désirons modifier son contenu, et l'utilisateur s'attend justement à ce comportement. Dans cet exemple, ce n'est pas ce que nous voulons.
Il faut alors impérativement avertir l'utilisateur que nous désirons juste lire l'argument sans modifier son contenu et que malgré tout nous proposons une connexion directe par une référence par soucis de performance. Nous avons déjà traité ce genre de principe. Une lecture sans modification du contenu correspond à une constante. Il faudra donc proposer une référence constante.

Encore une fois, n'oubliez pas qu'un tableau est en fait un pointeur, et que du coup, pour ce type de variable, la transmission par valeur n'existe pas. C'est donc une transmission par variable qui est proposée et qui s'effectue à l'aide de pointeur et non plus au travers de référence.

Dans l'exemple proposé, nous avons une fonction qui permet d'afficher un tableau d'entier. Nous passons donc un tableau en paramètre, seulement, nous récupérons uniquement l'adresse de l'argument (puisqu'il s'agit d'un pointeur). Du coup, la dimension d'un tableau n'est plus significative puisque seule l'adresse est passée. Les trois déclarations suivantes sont alors équivalentes :
void affiche (int tableau [10], int taille) ;
void affiche (int tableau [], int taille) ;
void affiche (int * tableau, int taille) ;
Comme la dimension n'est pas connue lorsque nous passons un tableau en paramètre, il est alors nécessaire de rajouter un paramètre supplémentaire pour réellement connaître la taille effective du tableau.
Pour résumer, lorsque nous passons un tableau en paramètre d'une fonction, il s'agit d'une transmission par variable de type pointeur ce qui amène des conséquences. Comme toutes les transmissions par variable, les changements effectués sur un tableau dans la fonction appelée sont faits sur l'argument lui-même, non sur la copie locale (puisqu'il n'y a pas de copie locale de l'ensemble du tableau).
Comme pour les structures, si vous n'avez pas l'intension de modifier les éléments du tableau, il faut avertir l'utilisateur de la fonction en proposant l'attribut const sur le paramètre de type tableau. Avec cette nouvelle signature, toute tentative de modification d'un des éléments du tableau se traduit par une erreur de compilation.
Comme vous le savez, une chaîne de caractères est un cas particulier du tableau. Toute la discussion sur les tableaux s'applique intégralement sur les chaînes de caractères et voici juste un exemple pour illustrer ces propos.
Il est assez fréquent qu'un des paramètres d'une fonction prenne souvent la même valeur. Une fonction peut gérer ce genre de situation en spécifiant un argument par défaut pour un de ses paramètres, ou plus, en utilisant la syntaxe d'initialisation à l'intérieur de la liste des paramètres. Une fonction donnant un argument par défaut pour un paramètre peut être invoqué avec ou sans argument pour ce paramètre. Si un argument est fourni, il remplace la valeur par défaut ; sinon, l'argument par défaut est utilisé.
A titre d'exemple, créons une fonction qui affiche un caractère quelconque à l'écran. Sans précision particulière, elle doit afficher le caractère ‘espace'.

Lorsqu'une déclaration prévoit des valeurs par défaut, les arguments concernés doivent obligatoirement être les derniers de la liste. La déclaration suivante est interdite :
double exemple (int = 5 , long, int = 3 ) ; // notez qu'il s'agit d'une déclaration de fonction et non d'une définition.
En fait, une telle interdiction relève du pur bon sens. En effet, si cette déclaration était acceptée, l'appel suivant :
exemple(10, 20) ;
pourrait être interprété aussi bien comme :
exemple(5, 10, 20) ;
que comme :
exemple(10, 20, 3) ;
Une partie de la conception d'une fonction avec des arguments par défaut consiste à trier les paramètres à l'intérieur de leur liste afin que ceux, amenés le plus souvent à recevoir une valeur spécifiée par l'utilisateur, soient définis en premier et que ceux devant fréquemment utiliser les arguments par défaut le soient en dernier.
Attention : L'argument par défaut d'un paramètre ne peut être spécifié qu'une seule fois dans un fichier. En conséquence, si nous avons à la fois une déclaration et une définition de fonction, la spécification de l'argument par défaut doit plutôt être placé sur la déclaration puisque c'est elle qui représente la fonction.
|
![]() |
Imaginons que nous ayons besoin de fabriquer une fonction qui détermine la valeur minimale entre deux entités. Il vient donc tout de suite à l'esprit qu'il s'agit de déclarer une fonction que nous pouvons simplement appeler minimum. Une question se pose toutefois quant au choix des types des paramètres de cette fonction. Il faudrait que cette fonction puisse accepter aussi bien des réels que des entiers. Le meilleurs choix semble être des paramètres de type réels, puisque les réels englobent les entiers.
Toutefois, cette situation n'est pas satisfaisante. En effet, si nous proposons à une telle fonction des entiers comme arguments, le système devra faire une conversion pour passer des entiers vers les réels et effectuer ensuite les calculs nécessaires, ce qui représente une perte de temps considérable vue la taille de cette fonction.
Du coup, il peut être judicieux de proposer une fonction par type d'arguments ; une fonction minimum pour les entiers et une fonction minimum pour les réels. Maintenant se pose le problème de la désignation de chacune de ces fonctions. On pourrait par exemple appeler la première fonction minimumInt et la suivante minimumDouble. L'écriture est très lourde, et de plus nous ne sommes par sûr de nous en souvenir.
Tout ce que nous désirons, c'est avoir le minimum de deux valeurs, et qu'à l'utilisation, le système soit capable de faire la différence entre les entiers et les réels sans se soucier de donner un nom spécifique à chacune des fonctions. Heureusement, le langage C++ permet d'avoir cette approche. C'est ce qui s'appelle la surdéfinition (ou alors la surcharge) de fonctions. Deux fonctions sont surdéfinies (ou surchargées) si elles ont le même nom, mais disposent d'une liste de paramètres différente.

La résolution de surcharge est le mécanisme par lequel une fonction surdéfinie est choisie plutôt qu'une autre. En effet, comme les fonctions surdéfinies portent, par définition, le même nom, le compilateur doit être suffisamment compétent pour sélectionner la bonne fonction au moment de l'appel. Si les arguments proposés correspondent parfaitement à l'une des signatures d'une fonction, l'appel ne posera aucun problème. Par contre, si les arguments ne correspondent à aucune des signatures proposées dans l'ensemble des fonctions surdéfinies, il faut que le compilateur sélectionne la fonction qui correspond le mieux à l'appel. S'il n'arrive pas à choisir, le compilateur provoque alors une erreur de compilation.
Une fonction qui s'appelle elle-même, directement ou indirectement, est appelée une fonction récursive. Pour illustrer l'intérêt de la récursivité, nous allons nous intéresser à la fonction factorielle. Toutefois, commençons par une étude plus classique en utilisant une itérative pour résoudre le problème.
Rappelons que la fonction factorielle de n est :
n ! = n (n-1) (n-2) …... 3.2.1 avec 0 ! = 1 et n € N
Par exemple, la factorielle de 5 est :
5 ! = 5.4.3.2.1 = 120.
Reprenons l'exemple précédent : 5 ! = 5.4.3.2.1 = 120. peut s'écrire aussi :
5 ! = 5. (4.3.2.1) = 5. 4 !
Autrement dit, la factorielle de 5, c'est cinq fois la factorielle de 4. Finalement, pour définir une factorielle, je fais référence à une autre factorielle. C'est une écriture récursive. D'une façon générale, nous avons :
n ! = n. (n-1) ! avec 0 ! = 1
Une fonction récursive doit toujours définir une condition d'arrêt ; sinon la fonction s'appelle à l'infini. On parle alors d'erreur de récursivité infinie.
Une fonction récursive s'exécute souvent plus lentement que la version itérative équivalente à cause du surcoût associé à l'appel d'une fonction. Cependant, la version récursive est toujours beaucoup plus concise et donc plus facilement compréhensible.
Il est très fréquent d'écrire de petites fonctions de quelques instructions seulement, qui sont essentiellement des commodités d'écriture. Il est en effet plus facile d'écrire, par exemple, minimum(a, b), plutôt que x<=y ? x : y. Par ailleurs, si un jour nous changeons l'implémentation de cette fonction, cela n'aura aucune répercussion pour les utilisateurs. Ce serait plus problématique si cette fonction n'existait pas, il faudrait en effet modifier plusieurs fois dans l'application les lignes équivalente à cet appel de fonction. Si jamais vous avez 300 occurrences de ce type, il serait fastidieux de tout remettre en place.
Il existe cependant un sérieux inconvénient à utiliser tel quel des fonctions aussi petites.
On se rend compte du coup que l'appel d'une fonction prend du temps rien que pour se connecter. Si dans le corps, nous avons beaucoup d'instructions, ou bien si nous avons des itératives, ce temps de connexion est infime par rapport au temps passé pour traiter le contenu. Par contre, pour de toutes petites fonction, le système passe pratiquement tout son temps à se connecter et se déconnecter.
Une fonction « en ligne » permet de palier à tous ces problèmes. Une fonction « en ligne » est développée au moment de la compilation à chaque endroit du programme ou elle est invoquée. Une fonction « en ligne » utilise le préfixe inline devant la déclaration ou la définition normale de la fonction.

Par ce système, nous n'avons plus de mécanisme d'appel de fonction, il s'agit tout simplement du changement d'un texte par un autre. Toutefois, ces fonctions « en ligne » ne doivent être utilisée que pour de toute petites fonctions, sinon, la taille de l'exécutable pourrait devenir relativement conséquente.
Lorsque nous demandons un nouveau projet, vous remarquez que le système nous propose couramment, pour la fonction principale main, une signature étendue avec deux paramètres passés à la fonction.
Ces paramètres sont utiles lorsque dans un programme, on doit passer des options sur la ligne de commande comme, par exemple :
ls –al *.cpp
Ici, la commande ls (un « programme » qui permet de lister le contenu du répertoire courant) comporte deux arguments, « -al » et « *.cpp ». Ces arguments sont récupérés par les paramètres de la fonction main correspondant à l'application « ls ».

Le paramètre argc récupère le nombre d'options sur la ligne de commande, alors que argv est le tableau de chaînes de caractères représentant les options de commande séparées par des espaces.
