Participer au site avec un Tip
Rechercher
 

Améliorations / Corrections

Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.

Emplacement :

Description des améliorations :

Définition de fonctions en C

Les instructions Utilisation de pointeurs


Accès rapide :
   Quelques points de terminologie
   Définitions de fonctions
   Les différents styles de formatage
   Le cas de la fonction main
   Définitions de fonctions récursives
   Fonctions à nombre variable d'arguments
   Mise en oeuvre d'une librairie de fonctions réutilisables

Quelques points de terminologie

Une fonction permet de factoriser un certain nombre de lignes de code en un endroit du programme. Une fois la fonction définie, elle peut être utilisée autant de fois que nécessaire à partir de n'importe quel autre point du programme (ou presque, nous y reviendrons plus tard). Une fonction accepte un certain nombre de paramètres afin d'appliquer son traitement sur ces valeurs.

Dans d'autres langages de programmation (Visual Basic, Pascal, ...), on distingue les fonctions des procédures. En effet, une fonction calcule une valeur, on dit aussi qu'elle retourne une valeur. Au contraire, la procédure effectue un traitement, éventuellement en tenant compte des valeurs de paramètres, mais ne retourne pas de résultat. En C, et peut-être par abus de langage, on n'utilise quasiment jamais le terme de procédure et l'on utilise uniquement le terme de fonction (qu'elle retourne, ou non, un résultat).

Définitions de fonctions

Pour définir une fonction, vous devez spécifiez en premier le type de retour de la fonction (void, s'il n'y pas de retour), puis spécifier le nom de la fonction, puis lister entre parenthèses les paramètres de la fonction. Pour chaque paramètre, il faut d'abord mentionner son type puis son nom. Les paramètres seront séparés les uns des autres par des virgules. Le corps de la fonction suit les paramètres et il est placé entre accolades.

Pour rappel, en mathématique, la fonction factorielle d'un entier naturel n est le produit des nombres entiers strictement positifs inférieurs ou égaux à n. Traditionnellement, cette fonction mathématique est notée via un point d'exclamation. Par exemple 5! sera équivalent à 5*4*3*2*1, soit 120. Nous allons dans le programme suivant coder une fonction C qui calcule la factorielle d'un entier.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
#include <stdio.h>
#include <stdlib.h>

// Définition de la fonction fact
unsigned int fact( unsigned int value ) {
    unsigned int result = 1;
    while ( value > 1 ) {
        result *= value;
        value --;
    }
    return result;
}


// Utilisation de la fonction fact
int main() {

    printf( "3! = %d\n", fact( 3 ) );
    printf( "5! = %d\n", fact( 5 ) );
    printf( "6! = %d\n", fact( 6 ) );

    return EXIT_SUCCESS;
}
Exemple de définition d'une fonction factorielle

La fonction se nomme fact et elle accepte un seul paramètre, l'entier pour lequel calculer la factorielle. Ce paramètre est typé unsigned int et se nomme value. La fonction renvoie un entier non signé. Dans la suite du programme (dans la fonction main), la fonction est utilisée trois fois d'affilé sur trois entier distincts. Voici ce que produit ce programme.

$> gcc -Wall -o Sample Sample.c
$> ./Sample
3! = 6
5! = 120
6! = 720
$>

Voici un autre exemple de déclaration de fonction C. Cette fonction permet d'enregistrer des messages dans un fichier de journalisation (un fichier de log). Nous prendrons aussi soin de stocker la date et l'heure auxquelles le message sera produit. Si vous regardez bien le code de cette fonction, vous noterez l'utilisation du mot clé void comme type de retour : cela signifie que la fonction ne renvoie rien. En conséquence, et si l'on veut être précis, il s'agit d'une procédure plus que d'une fonction.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define BUFFER_SIZE 20
#define LOG_FILENAME "./logs.txt"


// Définition de la fonction logMessage
void logMessage( const char * message ) {
    time_t timestamp = time( NULL );
    struct tm * pTime = localtime( & timestamp );
    char timeBuffer[ BUFFER_SIZE ];
    strftime( timeBuffer, BUFFER_SIZE, "%d/%m/%Y %H:%M:%S", pTime );

    FILE * file = fopen( LOG_FILENAME, "a" );
    if ( file == NULL ) {
        fprintf( stderr, "Cannot write log\n" );
        return;
    }
    fprintf( file, "%s - %s\n", timeBuffer, message );
    fclose( file );
}


// Utilisation de la fonction logMessage
int main() {

    logMessage( "Begin program" );
    // Do something
    logMessage( "End program" );

    return EXIT_SUCCESS;
}
Exemple de définition d'une fonction de journalisation

Testons cette fonction (notez, que nous lançons plusieurs fois le programme afin d'avoir des temps différents).

$> gcc -Wall -o Sample Sample.c
$> ./Sample
$> cat logs.txt
21/11/2015 10:42:50 - Begin program
21/11/2015 10:42:50 - End program
$> ./Sample
$> cat logs.txt
21/11/2015 10:42:50 - Begin program
21/11/2015 10:42:50 - End program
21/11/2015 10:43:04 - Begin program
21/11/2015 10:43:04 - End program
$>

Les différents styles de formatage

Il est à noter que selon les codes que vous pourrez trouver, deux grandes variantes sur le style de formatage pourront être observées.

Voici un petit exemple montrant deux définitions de fonctions strictement équivalentes, mais utilisant chacune d'elles un style de formatage particulier. Quel que soit la fonction que vous invoquerez, le résultat sera strictement le même.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
#include <stdio.h>
#include <stdlib.h>

// Style de formatage avec accolades ouvrantes en fin de ligne
unsigned int fact1( unsigned int value ) {
    unsigned int result = 1;
    while ( value > 1 ) {
        result *= value;
        value --;
    }
    return result;
}

// Style de formatage avec accolades ouvrantes en début de ligne suivante
unsigned int fact2( unsigned int value )
{
    unsigned int result = 1;
    while ( value > 1 )
    {
        result *= value;
        value --;
    }
    return result;
}



// Utilisation de la fonction fact
int main() {

    printf( "3! = %d\n", fact1( 3 ) );
    printf( "5! = %d\n", fact1( 5 ) );
    printf( "6! = %d\n", fact1( 6 ) );

    return EXIT_SUCCESS;
}
Les deux grandes manières de formater le code de vos fonctions.

Personnellement, j'ai une préférence pour la première manière de formater le code : vous avez pu vous en rendre compte en consultant les exemples de codes précédents. Je trouve que le début de construction est néanmoins bien matérialisé par la première ligne de définition de votre fonction et que cela permet de réduire la taille, en hauteur, de vos blocs de codes. Il sera ainsi moins souvent nécessaire de jouer avec l'ascenseur vertical pour lire vos codes. Mais cela reste un avis personnel et si vous préférez formater votre code différemment, n'hésitez pas à le faire.

Par contre, un conseil : dans un même projet optez pour un seul et unique style de formatage : ne mélangez pas les deux styles. Ça peut, pour certains, être perturbant, même s'il ne s'agit que de styles de codages qui ne change en rien le fond du code.

Le cas de la fonction main

Parmi toutes les fonctions qui vont constituer votre programme, une d'entre elle est particulière : je parle, bien entendu, de la fonction main. Elle constitue le point d'entrée de votre programme. La fonction main à un second objectif : il permet aussi, dans une certaine mesure, la communication entre le système d'exploitation et votre programme. Sur ce dernier point, il faut noter, que le main peut être écrit de quatre manières différentes en fonction de ce que vous voulez échanger avec le système d'exploitation. Voici les quatre façons d'écrire la fonction main.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
/*
 * Première manière :
 *     - ce main ne permettra pas de manipuler les arguments passés sur la ligne de commande.
 *     - le type de retour void, implique que votre programme n'indique pas au système
 *       d'exploitation comment il se termine (en succès ou en échec).
 */
void main() {

    // TODO

}


/*
 * Seconde manière :
 *     - ce main ne permettra pas de manipuler les arguments passés sur la ligne de commande.
 *     - le type de retour int, permet de renvoyer un code de retour au système d'exploitation.
 *          0 (ou constante EXIT_SUCCESS) : indique que le programme se termine correctement.
 *          Toute autre valeur (ou constante EXIT_FAILURE) : le programme se termine suite à une erreur.
 *     - Les constantes (EXIT_SUCCESS et EXIT_FAILURE) sont définies dans l'entête <stdlib.h>.
 */
int main() {

    // TODO

    return EXIT_SUCCESS;
}


/*
 * Troisème manière :
 *     - ce main permet de manipuler les arguments passés sur la ligne de commande.
 *       argc (argument counter) : indique combien de paramètres sont passés au main.
 *       argv (argument values) : un tableau de chaînes de caractères contenant les
 *                                différents arguments.
 *     - le type de retour void, implique que votre programme n'indique pas au système
 *       d'exploitation comment il se termine (en succès ou en échec).
 */
void main( int argc, char * argv [] ) {

    // TODO

}


/*
 * Quatrième manière :
 *     - ce main permet de manipuler les arguments passés sur la ligne de commande.
 *       argc (argument counter) : indique combien de paramètres sont passés au main.
 *       argv (argument values) : un tableau de chaînes de caractères contenant les
 *                                différents arguments.
 *     - le type de retour int, permet de renvoyer un code de retour au système d'exploitation.
 *          0 (ou constante EXIT_SUCCESS) : indique que le programme se termine correctement.
 *          Toute autre valeur (ou constante EXIT_FAILURE) : le programme se termine suite à une erreur.
 *     - Les constantes (EXIT_SUCCESS et EXIT_FAILURE) sont définies dans l'entête <stdlib.h>.
 */
int main( int argc, char * argv [] ) {

    // TODO

    return EXIT_SUCCESS;
}
Les différentes manières de code la fonction main.

Attention : dans le cas où vous cherchez à manipuler les arguments passés à votre programme, le nom de l'exécutable fait partie du tableau de chaînes de caractères. Pour mieux comprendre ce point, analysez cet exemple de code.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
#include <stdio.h>
#include <stdlib.h>


int main( int argc, char * argv [] ) {

    int i;

    // Affichage de l'ensemble de arguments sur la console
    for( i=0; i<argc; i++ ) {
        printf( "Argument at position %2d == %s\n", i, argv[i] );
    }

    return EXIT_SUCCESS;
}
Exemple d'utilisation des arguments passés à la fonction main.

Et voici deux exemples de démarrage de ce programme. Le premier lancement, aucun argument n'est spécifié (si ce n'est le nom de l'exécutable). Le second lancement est effectué avec trois arguments supplémentaires.

$> gcc -Wall -o Sample Sample.c
$> ./Sample
Argument at position  0 == ./Sample
$> ./Sample first second third
Argument at position  0 == ./Sample
Argument at position  1 == first
Argument at position  2 == second
Argument at position  3 == third
$>

Si vous le souhaitez, vous pouvez esquiver le nom de l'exécutable dans le tableau d'argument. Il suffit d'attaquer le parcourt des arguments à partir de l'indice 1. Une autre solution consiste à modifier les valeurs des paramètres argc et argv : en voici un petit exemple.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
#include <stdio.h>
#include <stdlib.h>


int main( int argc, char * argv [] ) {

    // Pour esquiver le nom de l'exécutable dans le tableau d'arguments
    argc --;
    argv ++;


    // Affichage de l'ensemble de arguments sur la console
    int i;
    for( i=0; i<argc; i++ ) {
        printf( "Argument at position %2d == %s\n", i, argv[i] );
    }

    return EXIT_SUCCESS;
}
On retire le nom de l'exécutable dans argv

Définitions de fonctions récursives

Une fonction récursive est une fonction qui s'appuie sur elle-même pour effectuer son traitement. Bien entendu, on utilise la récursivité qu'à la condition de la problématique à laquelle on cherche à répondre le soit intrinsèquement. C'est par exemple le cas de la fonction factorielle. Nous avons vu (quelques exemples plus haut) qu'il était possible de coder une factorielle de manière itérative (basée sur une boucle). Pour autant, mathématiquement parlant, la fonction factorielle peut aussi se définir ainsi :

Les deux premier cas sont appelés cas terminaux : une valeur constante est associée à ces deux cas. Par contre le troisième cas nous intéresse plus : la factorielle de n est basée sur la factorielle de n-1. C'est ici qu'on peut voir que cette fonction est récursive (elle se rappelle elle-même).

La bonne nouvelle pour nous c'est que le langage C supporte la récursivité : il est donc possible de réécrire notre fonction C de calcul de factorielle autrement : voici une proposition de codage.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
#include <stdio.h>
#include <stdlib.h>

/*
 * Définition de la fonction factorielle en version itérative
 * On la garde dans l'exemple à titre de comparaison

unsigned int fact( unsigned int value ) {
    unsigned int result = 1;
    while ( value > 1 ) {
        result *= value;
        value --;
    }
    return result;
}

*/


/*
 * Définition de la fonction factorielle en version récursive
 */
unsigned int fact( unsigned int value ) {
    if ( value <= 1 ) return 1;              // Cas terminaux
    return value * fact( value - 1 );      // Appels récursifs
}


// Utilisation de la fonction fact
int main() {

    printf( "3! = %d\n", fact( 3 ) );
    printf( "5! = %d\n", fact( 5 ) );
    printf( "6! = %d\n", fact( 6 ) );

    return EXIT_SUCCESS;
}
Exemple de définition d'une fonction factorielle récursive

Souvent une fonction récursive est plus simple à comprendre que son équivalent en version itérative. Par contre c'est un peu moins performant en temps d'exécution car un plus grand nombre d'appels de fonctions doivent être effectués (et ce n'est pas complétement gratuit).

Fonctions à nombre variable d'arguments

Le langage C permet de définir des fonctions à nombre variable d'arguments. Vous en connaissez déjà au moins une : la fonction printf. Effectivement, le nombre d'arguments (de paramètres, c'est la même chose) est dépendant du nombre de valeurs à injecter dans le format. Voici deux exemples d'utilisation de cette fonction printf : le premier exemple utilise 3 paramètres alors que le second en utilise 4.

Pour définir une fonction à nombre variable de paramètres (arguments), il vous faut :

Voici un petit exemple de code montrant comment définir une fonction à nombre variable d'arguments.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>

int addition( int counter, ... ) {

    int sum = 0;
    va_list parametersInfos;
    /* Initialize the va_list structure */
    va_start( parametersInfos, counter );

    /* for all unnamed integer, do an addition */
    while( counter > 0 ) {

        /* Extraction of the next integer */
        sum += (int) va_arg( parametersInfos, int );

        counter --;
    }

    /* Release va_list resources */
    va_end( parametersInfos );

    return sum;
}

int main( int argc, char * argv[] ) {

    int result = addition( 2 /* counter */, 3, 11 );
    printf( "addition( 2, 3, 11 ) == %d\n", result );     /* 14 */


    result = addition( 3 /* counter */, 4, 5, 6 );
    printf( "addition( 3, 4, 5, 6 ) == %d\n", result );   /* 15 */

    return EXIT_SUCCESS;

}
Exemple de définition d'une fonction à nombre variable d'arguments (de paramètres)

Pour de plus amples informations sur ce sujet, veuillez consulter notre section dédiée à la présentation de la librairie <stdarg.h>

Mise en oeuvre d'une librairie de fonctions réutilisables

Le but premier d'une fonction est de factoriser du code pour être réutiliser un grand nombre de fois au travers de votre (vos) programme(s). Il est donc nécessaire de ne pas devoir recoder une même fonction dans les différents fichiers qui constituent votre programme. Pour ce faire, il vous faut définir une librairie de fonctions réutilisable. Une librairie de fonction est, à minima, constituée de deux fichiers : un fichier de déclaration (d'entête) ayant pour extension .h et un fichier d'implémentation ayant pour extension .c. Le fichier de déclaration sera inclus (comme les fichiers .h de la librairie standard) dans vos différents fichiers de code ayant besoin de ces fonctions.

Voici un exemple de fichier de déclaration (d'entête) permettant de définir une petite librairie de fonction mathématiques (il est bien clair que ces fonctions existe déjà dans la librairie standard, mais ces fonctions sont proposés à titre d'exemples). Quelques explications suivront.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
#ifndef __MY_MATH_LIB_H
#define __MY_MATH_LIB_H

// Définition d'une fonction de calcul de factorielle.
unsigned int fact( unsigned int value );

// Définition d'une fonction d'élévation à une puissance données.
int power( int value, unsigned int pow );


#endif
Exemple de définition d'un fichier .h de déclaration de fonctions : SampleLib.h

Comme vous pouvez le remarquer, les fonctions sont uniquement déclarées, mais elles seront implémentées dans un autre fichier. C'est déclarations permettront au compilateur, lors du traitement d'un fichier utilisant cette librairie, de vérifier que les fonctions de la librairie sont correctement utilisées (bon nombre de paramètres, bonne utilisation des types, bonne récupération de la valeur de retour de chaque fonction). Ensuite, après la compilation, la phase d'édition de liens permettra de lier l'appel de la fonction à son implémentation.

Notez, en début de fichier, la présence des instructions du préprocesseur #ifndef et #define. Elles permettent de sécuriser des éventuelles inclusions multiples de ce fichier. Effectivement, si vous incluez plusieurs fois ce fichier, les déclarations de fonctions pourraient être vues plusieurs fois par le compilateur : cela produirait obligatoirement une erreur. Pour éviter cela, on sécurise l'inclusion en indiquant que si la macro __MY_MATH_LIB_H n'est pas définie (IF Not DEFined) alors on traite le contenu du #ifndef mais on définit aussi la macro __MY_MATH_LIB_H. Lors d'un éventuel second #include, la macro étant existante, le contenu du #ifndef sera purement et simplement ignoré.

Voici maintenant le fichier d'implémentation de la librairie. Quelques explications suivront.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
#include "SampleLib.h"

// Définition d'une fonction de calcul de factorielle.
unsigned int fact( unsigned int value ) {
    unsigned int result = 1;
    while ( value > 1 ) {
        result *= value;
        value --;
    }
    return result;
}

// Définition d'une fonction d'élévation à une puissance données.
int power( int value, unsigned int pow ) {
    if ( pow == 0 ) return 1;
    if ( pow == 1 ) return value;

    int accumulator = 1;
    while( pow > 0 ) {
        accumulator *= value;
        pow--;
    }

    return accumulator;
}
Exemple de fichier .c d'implémentation des fonctions précédente : SampleLib.c

La première ligne permet d'inclure la déclaration de votre librairie. Notez que cette librairie est liée à votre développement et donc n'est pas fournie par le compilateur. On n'utilise donc pas les caractères < > pour spécifier le fichier à inclure, mais plutôt une paire de guillemets. Si vous ne faites pas comme ça, l'inclusion ne fonctionnera pas. Le fait d'inclure le .h dans le fichier d'implémentation est souvent nécessaire (même si dans notre exemple, il ne serait pas requis). Effectivement cela permet de pouvoir coder vos fonctions dans un ordre quelconque (même si elles dépendent les unes des autres) étant donné qu'elles seront toutes pré-déclarées dans le fichier d'entête. De plus, certaines fois, les fichiers d'entête définissent des structures de données qui seront requises pour vos implémentations.

Il ne reste plus qu'à utiliser notre librairie pour voir si tout fonctionne bien. Voici un exemple d'utilisation.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
#include <stdio.h>
#include <stdlib.h>

#include "SampleLib.h"


int main( void ) {

    int result = power( 2, 3 );
    printf( "2³ == %d\n", result );

    result = fact( 6 );
    printf( "6! == %d\n", result );

    return EXIT_SUCCESS;
}
Exemple d'utilisation de notre librairie : SampleUseLib.c


<- Les instructions du langage C Utilisation de pointeurs ->