Fonctions en C++ : L’element cle pour ecrire un code clair et reutilisable
As-tu remarque que, a mesure qu’un programme grandit, son code devient plus difficile a comprendre et a maintenir ? Si tu as deja eu l’impression que ton code ressemble a un labyrinthe enchevetre, c’est parce que tu n’exploites pas encore pleinement les fonctions en C++. Celles-ci agissent comme des blocs de construction permettant de diviser un programme en parties gerables, facilitant ainsi sa lecture, son entretien et son optimisation. Dans ce cours, tu apprendras a les utiliser efficacement pour ameliorer l’organisation de ton code, ecrire des programmes plus structures et rendre ton developpement en C++ plus professionnel et efficace.
Objectifs d’Apprentissage
A la fin de ce cours, tu auras appris a :
- Comprendre l’objectif des fonctions et pourquoi elles sont essentielles en C++.
- Creer des fonctions correctement, en assurant un code structure.
- Invoquer des fonctions dans un programme et comprendre leur execution.
- Distinguer entre les fonctions qui retournent des valeurs et celles qui executent simplement des instructions.
- Comparer differentes manieres de definir des fonctions et choisir la meilleure selon la situation.
TABLE DES MATIERES
Declaration, Invocation et Definition de Fonctions
Approche : Declarer – Invoquer – Definir
Approche : Declarer et Implementer avant d’Invoquer
Propagation de la Valeur de Retour
Recursivite : Fonctions qui s’appellent elles-memes
Retour Multiple dans les Fonctions
Surcharge de fonctions (overloading)
Fonctions inline en C++
Reflexion Finale sur les Fonctions en C++
Declaration, Invocation et Definition de Fonctions
En C++, les fonctions sont des blocs de code reutilisables qui permettent de structurer un programme de maniere modulaire et organisee. Chaque fonction encapsule une tache specifique, ce qui aide a ameliorer la clarte et la maintenabilite du code. Pour pouvoir utiliser une fonction dans un programme, nous devons suivre trois etapes fondamentales : declaration, invocation et definition.
Ces trois concepts sont essentiels, et chacun remplit un role particulier dans la structure du code. Examinons-les en detail.
- Declaration d’une fonction
Avant qu’une fonction puisse etre utilisee dans le code, le compilateur doit etre informe de son existence. Cela se fait par le biais d’une declaration de fonction ou prototype.
La declaration d’une fonction indique au compilateur trois elements fondamentaux :
- Le type de donnees que la fonction retournera (ou void si elle ne retourne rien).
- Le nom de la fonction.
- Les parametres qu’elle accepte (s’il y en a), ainsi que leurs types.
La syntaxe generale d’une declaration de fonction est :
type_de_retour nom_de_fonction (liste_de_parametres);
La declaration de la fonction est generalement placee avant
main()ou dans un fichier d’en-tete .h lorsque nous travaillons avec plusieurs fichiers. - Invocation d’une fonction
Apres avoir declare une fonction, nous pouvons l’invoquer, c’est-a-dire l’appeler dans le code pour qu’elle s’execute.
Lorsqu’une fonction est invoquee :
- Le code contenu dans sa definition est execute.
- Si la fonction retourne une valeur, celle-ci peut etre stockee dans une variable ou utilisee directement dans une expression.
- Si la fonction est de type
void, elle execute simplement ses instructions sans retourner de valeur.
La syntaxe pour invoquer une fonction consiste simplement a ecrire son nom suivi de parentheses avec les arguments (si necessaire) :
nom_de_fonction(arguments);
- Definition de la fonction :
Enfin, la definition de la fonction est la partie ou son comportement est implemente. C’est ici que sont precisees les instructions a executer lorsque la fonction est invoquee.
La syntaxe generale d’une definition de fonction est :
type_de_retour nom_de_fonction (liste_de_parametres) { // Corps de la fonction : instructions a executer return valeur; // (si la fonction retourne une valeur) }Chaque definition de fonction doit respecter les regles suivantes :
- Elle doit correspondre a la declaration (si elle a ete declaree auparavant).
- Si la fonction retourne une valeur (par exemple,
int), elle doit inclure une instructionreturnavec la valeur a retourner. - Si la fonction ne retourne rien (
void), elle execute simplement ses instructions et n’a pas besoin dereturn.
Flux d’execution
Lors de l’execution du programme, les fonctions sont appelees dans l’ordre ou elles apparaissent dans main(). Le flux d’execution est le suivant :
- Le compilateur reconnait la declaration de la fonction.
- Dans
main(), lorsqu’une invocation de la fonction est trouvee, le controle du programme est transfere a la definition de la fonction. - La fonction execute ses instructions.
- Si la fonction a une valeur de retour, celle-ci est retournee a la ligne ou elle a ete invoquee.
- Le flux du programme retourne a
main()ou a la fonction qui a effectue l’invocation.
Importance de la declaration prealable
La declaration des fonctions avant leur utilisation est cruciale car le compilateur C++ traite le code de haut en bas. Si nous essayons d’appeler une fonction avant qu’elle n’ait ete definie ou declaree, nous obtiendrons une erreur.
Il existe deux principales facons d’aborder ce probleme :
- Declarer la fonction avant
main()et la definir apres (comme nous l’avons vu jusqu’a present). - Definir la fonction avant
main(), evitant ainsi le besoin d’une declaration prealable.
Les deux approches sont valables, mais la premiere est plus utile dans les grands programmes ou les fonctions sont reparties sur plusieurs fichiers.
Approche : Declarer – Invoquer – Definir
Une des approches les plus utilisees pour structurer les fonctions en C++ est declarer – invoquer – definir. En suivant cette logique, nous organisons notre code en trois etapes fondamentales :
- Declaration : Informer le compilateur de l’existence de la fonction avant son utilisation, en precisant son nom, son type de retour et ses parametres (s’il y en a).
- Invocation : Appeler la fonction dans le code principal (
main()dans la plupart des cas), executant ainsi son contenu. - Definition : Detailing l’implementation de la fonction, precisant les instructions a executer lorsqu’elle est invoquee.
Cette structure permet d’ameliorer l’organisation du code, facilitant ainsi sa maintenance et son evolutivite. Examinons un exemple ou nous appliquons cette approche :
Exemple : Fonction consoladice()
Dans le code suivant, nous suivons la sequence declarer – invoquer – definir :
#include <iostream>
using namespace std;
// D'abord, nous declarons la fonction
void consoladice();
int main() {
// Nous invoquons la fonction
consoladice();
return 0;
}
// Nous definissons la fonction precedemment declaree en detaillant son fonctionnement interne
void consoladice() {
cout << "Ceci est une simple chaine de caracteres ou des litteraux." << endl;
cout << "Maintenant, je te montre le nombre cinq. Le voici : " << 5 << endl;
cout << "Voyons quel resultat nous obtenons si nous faisons 10/5. Le resultat est : " << 10/5 << endl;
cout << "Une facon typique d'approcher le nombre Pi est de faire 22/7. Le resultat est : " << 22/7 << endl;
cout << "En C++, ecrire 22/7 n'est pas la meme chose que 22.0/7, le traitement est different." << endl;
cout << "Avec ce simple changement, nous pouvons voir que 22.0/7 est egal a " << 22.0/7 << endl;
cout << "Ne trouves-tu pas que c'est une meilleure approximation ?" << endl;
}
Le resultat attendu de ce code est
Ceci est une simple chaine de caracteres ou des litteraux.
Maintenant, je te montre le nombre cinq. Le voici : 5
Voyons quel resultat nous obtenons si nous faisons 10/5. Le resultat est : 2
Une facon typique d'approcher le nombre Pi est de faire 22/7. Le resultat est : 3
En C++, ecrire 22/7 n'est pas la meme chose que 22.0/7, le traitement est different.
Avec ce simple changement, nous pouvons voir que 22.0/7 est egal a 3.14286
Ne trouves-tu pas que c'est une meilleure approximation ?
Pour mieux comprendre l’approche declarer – invoquer – definir, concentrons-nous sur trois parties cles du code :
- Ligne 5 : Declaration de la fonction
void consoladice();indique au compilateur qu’a un certain point du code, il y aura une fonction appeleeconsoladice().- Il est precise que son type de retour est
void, ce qui signifie qu’elle ne retourne aucune valeur. - Bien que l’implementation de
consoladice()ne soit pas encore connue, cette declaration permet au compilateur de la reconnaitre lorsqu’elle sera utilisee plus tard.
- Ligne 9 : Invocation de la fonction
- Dans la fonction
main(), la ligneconsoladice();execute la fonction. - A ce stade, le compilateur reconnait deja l’existence de
consoladice()grace a sa declaration precedente. - Lorsque la fonction est invoquee, le controle du programme est transfere a sa definition, ou son contenu est execute.
- Dans la fonction
- Lignes 14 a 22 : Definition de la fonction
- Ici se trouve l’implementation detaillee de
consoladice(), precisant ses instructions. - Dans ce cas, la fonction affiche plusieurs messages sur la console, y compris des nombres et des operations mathematiques.
- Un point important est la difference entre
22/7et22.0/7. Lorsque nous utilisons22/7, les deux nombres sont entiers, ce qui donne3en raison de la division entiere. Cependant, en ecrivant22.0/7, nous forcons l’operation a etre effectuee en virgule flottante, obtenant3.14286.
- Ici se trouve l’implementation detaillee de
Approche : Declarer et Implementer avant d’Invoquer
En C++, en plus de l’approche declarer – invoquer – definir, il existe une autre facon valide de structurer nos fonctions : declarer et implementer avant d’invoquer. Cette methode combine la declaration et la definition en une seule etape, avant que la fonction ne soit utilisee dans main().
Dans cette approche, au lieu de faire une declaration prealable de la fonction et de la definir apres main(), nous la declarons et la definissons directement au meme endroit avant de l’invoquer. Cela presente l’avantage d’eviter une declaration separee et de rendre le code plus compact et plus facile a lire dans les petits programmes.
La structure generale de cette approche est la suivante :
// Nous definissons la fonction avant main()
type_de_retour nom_de_fonction(liste_de_parametres) {
// Corps de la fonction
return valeur; // si necessaire
}
int main() {
// Invocation de la fonction
nom_de_fonction(arguments);
}
Etant definie avant main(), le compilateur la connait deja lorsqu’elle est invoquee, il n’est donc pas necessaire de faire une declaration prealable.
Exemple : Fonction consoladice() sans declaration prealable
Voyons maintenant un exemple pratique appliquant cette approche :
#include <iostream>
using namespace std;
// Nous definissons la fonction avant de l'invoquer
void consoladice() {
cout << "Ceci est une simple chaine de caracteres ou des litteraux." << endl;
cout << "Maintenant, je te montre le nombre cinq. Le voici : " << 5 << endl;
cout << "Voyons quel resultat nous obtenons si nous faisons 10/5. Le resultat est : " << 10/5 << endl;
cout << "Une facon typique d'approcher le nombre Pi est de faire 22/7. Le resultat est : " << 22/7 << endl;
cout << "En C++, ecrire 22/7 n'est pas la meme chose que 22.0/7, le traitement est different." << endl;
cout << "Avec ce simple changement, nous pouvons voir que 22.0/7 est egal a " << 22.0/7 << endl;
cout << "Ne trouves-tu pas que c'est une meilleure approximation ?" << endl;
}
int main() {
// Nous invoquons la fonction
consoladice();
return 0;
}
Dans ce code, nous pouvons voir que :
- Entre les lignes 5 et 13, la declaration et la definition sont combinees
La fonction
consoladice()est definie directement avantmain(), sans necessite d’une declaration separee. - A la ligne 17, la fonction est invoquee dans
main()Puisque la fonction a deja ete definie auparavant, le compilateur la reconnait et permet son execution sans probleme.
- Le resultat est exactement le meme
En termes fonctionnels, cette approche produit le meme comportement que l’approche declarer – invoquer – definir, mais avec une structure plus compacte.
✅ Avantages :
- Code plus direct et plus compact pour les petits programmes.
- Ne necessite pas de declaration prealable, reduisant ainsi le nombre de lignes de code.
- Facilite la lecture dans les scripts courts ou toutes les fonctions se trouvent dans un meme fichier.
❌ Inconvenients :
- Dans les grands programmes, cela peut compliquer l’organisation si de nombreuses fonctions sont definies avant
main(). - Moins utile lorsqu’on travaille avec plusieurs fichiers (.h et .cpp), car il est preferable de garder la declaration dans un fichier separe.
Propagation de la Valeur de Retour
Jusqu’a present, nous avons travaille avec des fonctions qui executent simplement des instructions sans retourner de resultat. Cependant, dans de nombreux cas, il est necessaire qu’une fonction retourne une valeur afin qu’elle puisse etre utilisee dans d’autres calculs ou stockee dans des variables. Ce processus est connu sous le nom de propagation de la valeur de retour.
Dans cette section, nous apprendrons comment fonctionnent les fonctions qui retournent des valeurs, en quoi elles se distinguent des fonctions de type void, et comment nous pouvons exploiter ce mecanisme en C++.
Exemple Pratique : Fonction qui calcule l’aire d’un rectangle
Pour illustrer la propagation de la valeur de retour, nous allons implementer une fonction qui recoit la base et la hauteur d’un rectangle et qui calcule son aire.
#include <iostream>
using namespace std;
// Fonction qui calcule l'aire d'un rectangle et retourne le resultat
double calculerAire(double base, double hauteur) {
return base * hauteur;
}
int main() {
double base, hauteur;
// Nous demandons a l'utilisateur d'entrer les valeurs
cout << "Entrez la base du rectangle : ";
cin >> base;
cout << "Entrez la hauteur du rectangle : ";
cin >> hauteur;
// Nous appelons la fonction et stockons son resultat
double aire = calculerAire(base, hauteur);
// Nous affichons le resultat
cout << "L'aire du rectangle est : " << aire << endl;
return 0;
}
- La fonction
calculerAire()retourne une valeur- Elle recoit deux valeurs (
baseethauteur) en tant que parametres. - Elle calcule l’aire via la multiplication
base * hauteur. - Elle utilise
returnpour envoyer le resultat de l’operation a la partie du programme qui l’a invoquee.
- Elle recoit deux valeurs (
- Utilisation de la valeur retournee dans
main()- Les valeurs de
baseethauteursont saisies par l’utilisateur. - La fonction
calculerAire()est appelee, et son resultat est stocke dans la variableaire. - Enfin, le resultat est affiche sur la console.
- Les valeurs de
- Difference cle avec une fonction
voidSi
calculerAire()etait de typevoid, nous devrions afficher le resultat directement a l’interieur de la fonction au lieu de le retourner amain()pour une utilisation ulterieure.
Exemple : Fonction qui determine si un nombre est pair ou impair
#include <iostream>
using namespace std;
bool estPair(int nombre) {
return nombre % 2 == 0;
}
int main() {
int nombre;
cout << "Entrez un nombre : "; cin >> nombre;
if (estPair(nombre)) {
cout << "Le nombre est pair." << endl;
} else {
cout << "Le nombre est impair." << endl;
}
return 0;
}
Ici, la fonction estPair() retourne true si le nombre est pair et false s’il est impair, permettant ainsi a main() d’utiliser le resultat pour decider quel message afficher.
Recursivite : Fonctions qui s’appellent elles-memes
La recursivite est une technique dans laquelle une fonction s’invoque elle-meme pour resoudre des problemes en les divisant en versions plus petites d’eux-memes. Elle est particulierement utile dans les algorithmes tels que le calcul du factoriel, la suite de Fibonacci et les parcours de structures de donnees comme les arbres.
Exemple : Factoriel d’un Nombre
Le factoriel d’un nombre n est n!=n\cdot(n-1)\cdot(n-2) \cdots 3 \cdot2 \cdot 1. Cette formulation suit une structure recursive que nous pouvons representer mathematiquement de la maniere suivante :
\begin{array}{rl} 0! &=1\\ n! &= n\cdot(n-1)!\\ \end{array}
En tenant compte de cela, nous pouvons programmer cette fonction en C++ comme suit :
#include <iostream>
using namespace std;
int factoriel(int n) {
if (n == 0 || n == 1) {
return 1;
}
return n * factoriel(n - 1);
}
int main() {
int nombre;
cout << "Entrez un nombre : "; cin >> nombre;
cout << "Le factoriel de " << nombre << " est " << factoriel(nombre) << endl;
return 0;
}Dans ce code :
- La fonction
factoriel(n)s’appelle elle-meme avecn-1jusqu’a atteindre le cas de base (n == 0oun == 1). - La fonction se resout de maniere recursive en multipliant les valeurs jusqu’a obtenir le resultat.
Exemple : Nombres de Fibonacci
Les nombres de Fibonacci sont ceux qui forment la sequence 1, 1, 2, 3, 5, 8, 13, \cdots. Cette sequence est caracterisee par le fait que chaque nombre est egal a la somme des deux precedents.
Mathematiquement, si fibo(n) est la fonction dont les resultats correspondent aux nombres de Fibonacci, alors elle possede la structure mathematique suivante :
\begin{array}{rl} fibo(0) &= 1\\ fibo(1) &= 1 \\ fibo(n) &= fibo(n-1) + fibo(n-2) \end{array}
Un exemple de code en C++ qui affiche les nombres de Fibonacci est le suivant :
#include<iostream>
using namespace std;
int fibo(int nombre){
if (nombre == 0 || nombre == 1){
return 1;
}
return fibo(nombre - 1) + fibo(nombre - 2);
}
int main(){
int x = 0, i = 0;
cout << "Entrez un nombre : "; cin >> x;
while (i < x){
cout << "Le nombre de Fibonacci a la position " << i+1 << " est : " << fibo(i) << endl;
i = i + 1;
}
}
Retour Multiple dans les Fonctions
En C++, une fonction peut retourner plusieurs valeurs en utilisant des structures telles que std::pair, std::tuple ou des references a des variables.
Exemple : Fonction qui retourne deux valeurs avec std::pair
#include <iostream>
#include <utility> // Pour utiliser std::pair
using namespace std;
pair<int, int> diviser(int a, int b) {
return make_pair(a / b, a % b);
}
int main() {
int numerateur = 0, denominateur = 1;
cout << "Entrez le numerateur : "; cin >> numerateur;
cout << "Entrez le denominateur : "; cin >> denominateur;
pair<int, int> resultat = diviser(numerateur, denominateur);
cout << "Quotient : " << resultat.first << endl;
cout << "Reste : " << resultat.second << endl;
return 0;
}
Ici, la fonction diviser() retourne deux valeurs : le quotient et le reste d’une division entiere.
Exemple : Fonction qui retourne plusieurs valeurs avec std::tuple
#include <iostream>
#include <tuple>
using namespace std;
tuple<int, int, int> operations(int a, int b) {
return make_tuple(a + b, a - b, a * b);
}
int main() {
int somme = 0, difference = 0, produit = 0;
int a = 0, b = 0;
cout << "Entrez un nombre : "; cin >> a;
cout << "Entrez un autre nombre : "; cin >> b;
std::tie(somme, difference, produit) = operations(a, b);
cout << "Somme : " << somme << ", Difference : " << difference << ", Produit : " << produit << endl;
return 0;
}
Surcharge de Fonctions (Overloading)
La surcharge de fonctions permet de definir plusieurs fonctions avec le meme nom mais differents types ou nombres de parametres. Cela ameliore la lisibilite et la reutilisation du code.
#include <iostream>
#include <string> // Necessaire pour utiliser std::string
#include <cmath>
using namespace std;
// Aire d'un carre ou d'un cercle (figures avec une seule donnee)
double aire(double cote) {
return cote * cote;
}
// Aire d'un rectangle (ou figures avec deux donnees)
double aire(double base, double hauteur) {
return base * hauteur;
}
// Aire d'un triangle (ou figures avec trois donnees)
double aire(double a, double b, double c){
return 0.25 * sqrt((a+b+c)*(a+b-c)*(a-b+c)*(-a+b+c));
}
int main() {
string figure;
double resultat = 0;
double l1=0, l2=0, l3=0;
// Demander la figure
cout << "Quelle est la figure ? (carre, cercle, rectangle ou triangle) : ";
cin >> figure;
// Evaluer la figure avec if-else
if (figure == "carre") {
cout << "Quelle est la longueur de son cote ? "; cin >> l1;
resultat = aire(l1);
cout << "L'aire du carre est : " << resultat << endl;
}
else if (figure == "rectangle"){
cout << "Quelle est la base ? "; cin >> l1;
cout << "Quelle est la hauteur ? "; cin >> l2;
resultat = aire(l1, l2);
cout << "L'aire du rectangle est : " << resultat << endl;
}
else if (figure == "cercle") {
l1 = 3.141592653;
cout << "Quel est le rayon ? "; cin >> l2;
resultat = aire(l1, l2);
cout << "L'aire du cercle est : " << resultat << endl;
}
else if (figure == "triangle"){
cout << "Quelles sont les longueurs de ses cotes ?" << endl;
cout << "Cote 1 : "; cin >> l1;
cout << "Cote 2 : "; cin >> l2;
cout << "Cote 3 : "; cin >> l3;
if ((l1+l2+l3)*(l1+l2-l3)*(l1-l2+l3)*(-l1+l2+l3) < 0){
cout << "Le triangle est impossible.";
}
else {
resultat = aire(l1, l2, l3);
cout << "L'aire du triangle est : " << resultat << endl;
}
}
else {
cout << "Figure non valide." << endl;
}
return 0;
}
Fonctions inline en C++
Les fonctions inline en C++ offrent un mecanisme pour optimiser les performances du programme en reduisant la surcharge des appels de fonction. Au lieu d’executer un appel conventionnel, le compilateur tente d’etendre directement le code de la fonction a chaque endroit ou elle est invoquee.
Syntaxe d’une fonction inline
inline type_de_retour nom_de_fonction(liste_de_parametres) {
// Corps de la fonction
return valeur; // Si necessaire
}En utilisant inline, nous eliminons le besoin de sauter vers une autre adresse memoire pour executer la fonction, ce qui peut reduire le temps d’execution.
Differences entre une fonction inline et une fonction conventionnelle
| Caracteristique | Fonction Conventionnelle | Fonction inline |
|---|---|---|
| Appel de fonction | Effectue un appel avec saut d’execution. | Le code est copie directement la ou il est utilise. |
| Temps d’execution | Peut etre plus lent en raison de la surcharge de l’appel. | Peut etre plus rapide pour les petites fonctions. |
| Utilisation de la memoire | Une seule copie de la fonction est stockee en memoire. | Peut augmenter la taille du code binaire si la fonction est utilisee de nombreuses fois. |
Exemple de fonction inline
#include <iostream>
using namespace std;
inline int carre(int x) {
return x * x;
}
int main() {
cout << "Le carre de 5 est : " << carre(5) << endl;
return 0;
}
🔍 Processus du compilateur :
- Le compilateur remplace l’appel
carre(5)directement par5 * 5. - Aucun saut d’execution n’est effectue.
- Le calcul est realise sur la meme ligne ou la fonction a ete invoquee.
Avantages et inconvenients de inline
✅ Avantages
- Elimine la surcharge des appels de fonction : Reduit le temps d’execution pour les fonctions courtes et frequemment appelees.
- Facilite l’optimisation par le compilateur : Peut ameliorer les performances en evitant l’utilisation des registres CPU et de la pile.
- Garantit que le code de la fonction est disponible a la compilation.
❌ Inconvenients
- Augmente la taille du binaire : Si la fonction
inlineest utilisee plusieurs fois dans un programme volumineux, le code sera duplique a chaque point d’appel. Cela se produit lorsqu’elle est appliquee a des fonctions longues ou trop repetees dans le code. - Ne garantit pas toujours l’expansion inline : Le compilateur peut ignorer la demande
inlines’il estime que cela n’est pas optimal.
