Developpez.com - C
X

Choisissez d'abord la catégorieensuite la rubrique :


Cours C

Date de publication : 2008

Par la rédaction C (Accueil)
 


I. Initiation
I-A. Présentation de la fonction main
I-B. Le classique "Bienvenue sur developpez" (Hello world)
I-C. Mécanisme Source, Compilation, Edition de liens, Exécutable
XVII. Pointeur sur fonctions
XVII-C. Avancé
XVII-C-1. Introduction
XVII-C-2. Définition
XVII-C-3. Initialisation
XVII-C-4. Appel de la fonction pointée
XVII-C-5. Utilisation des pointeurs de fonction


I. Initiation


I-A. Présentation de la fonction main

Un programme informatique est une suite d'instructions destinées à être exécutées par un ordinateur. En langage C, une telle suite est matérialisée par ce qu'on appelle une fonction. Une fonction contient donc une ou plusieurs instructions voire aucune ! Une fonction possède un nom. Pour exécuter une fonction, c'est-à-dire exécuter les instructions qu'elle contient, il suffit de l'appeler (nous verrons un peu plus bas comment). Un programme écrit en langage C est donc composé de définitions et d'appels de fonctions. Chaque programme doit cependant comporter une fonction dont le nom main est imposé, c'est celle qui sera automatiquement appelée lorsqu'on exécutera le programme. C'est donc la fonction principale (main function). On l'appelle également le point d'entrée du programme (program entry point).

Il est temps à présent d'écrire un peu de code. Voici le programme que nous allons réaliser : un programme qui ne fait rien ! Voici un début de code (ce code n'est pas ecnore complet) :
La fonction main
int main(void)
{

}
Ce que nous venons de faire, c'est une définition (création) de fonction. Nous venons de définir (créer) la fonction main. C'est une fonction qui renvoit un int (int signifie "entier" - integer en anglais) et qui ne prend rien en entrée (void signifie vide). C'est ainsi qu'il faut définir la fonction main (nous verrons plus tard que ce n'est pas la seule forme qu'on peut utiliser). Les accolades servent à délimiter le corps de la fonction. C'est là-dedans qu'on mettra toutes les instructions qu'on veut faire exécuter à l'ordinateur quand la fonction est appelée. Pour l'instant, nous n'y mettrons pas grand-chose.

L'entier retourné par main est appelé le code de retour (ou code d'erreur) du programme. Généralement, quand un programme retourne 0, l'appelant (celui qui a appelé main) conclut le programme s'est terminé normalement.

Nous avons dit tout à l'heure que notre programme ne fera rien. En fait, il doit au moins retourner son code d'erreur. Nous allons dans notre cas retourner 0.
La fonction main
int main(void)
{
    return 0;

}
La ligne que nous venons d'ajouter est une instruction. L'instruction en question est une instruction return. Elle sert à terminer (quitter) une fonction. Le 0 qui suit le mot-clé return indique la valeur retournée par la fonction, donc ici 0. Et enfin, une instruction élémentaire doit se terminer par un point-virgule.

Et voilà. Nous venons de créer notre tout premier programme. Enregistrez le fichier sous le nom de monprog.c (l'extension .c sert à indiquer que le fichier est un fichier source C). Compilez. Vous devriez obtenir alors en sortie le fichier exécutable (monprog, monprog.exe ou quelque chose de ce genre selon votre système). Lancez-le. Le programme se lance mais ne fait rien, il se termine aussitôt.



I-B. Le classique "Bienvenue sur developpez" (Hello world)

Nous allons cette fois-ci créer un programme qui fait quelque chose. Ce programme écrira tout simplement "Bienvenue sur developpez" (sans les guillemets) sur la sortie standard. La sortie standard, c'est tout simplement, si on peut le dire, le périphérique d'affichage par défaut. Autrement dit, le programme que nous allons créer affichera le message "Bienvenue sur developpez". Pour faire cela, nous utiliserons la fonction puts (put string - afficher chaîne).
Dans main, nous devons donc écrire l'instruction suivante :
Exemple d'utilisation d'une fonction (puts)
puts("Bienvenue sur developpez");
Cette instruction est une instruction d'appel à une fonction. Dans cette instruction, nous appelons la fonction puts en lui passant en entrée (on dit aussi paramètre ou encore argument) la chaîne "Bienvenue sur developpez". Voici donc notre fonction main actuelle :
Exemple d'utilisation d'une fonction (puts)
int main(void)
{
    puts("Bienvenue sur developpez");
    
    return 0;
}
Mais il manque encore une chose : pour utiliser puts, il faut tout d'abord inclure le fichier stdio.h. En effet, puts n'est pas un élément du langage C mais une fonction de sa bibliothèque standard. La bibliothèque standard du C, c'est une collection de fonctions (une "bibliothèque") qui accompagne tout compilateur C. stdio.h est un fichier de la bibliothèque standard. Il contient entre autres la déclaration de la fonction puts (nous verrons un peu plus loin comment déclarer une fonction). Cette déclaration permet au compilateur de savoir que puts est une fonction, qu'elle nécessite un et un seul paramètre, que ce paramètre doit être une chaîne, etc. Sans ces informations, le compilateur ne pourra pas détecter les erreurs dues à une mauvaise utilisation de la fonction. Ajoutons donc, avant d'écrire la fonction main, la ligne :
Comment inclure le fichier stdio.h
#include <stdio.h>
Les lignes commencant par # sont destinées au préprocesseur, un programme qui traite les fichiers sources avant qu'ils ne soient effectivement soumis au compilateur. include est une directive qui indique au préprcesseur d'inclure (insérer) à partir de cet emplacement le contenu du fichier indiqué.
Le préprocesseur ne passera le fichier source au compilateur (pour être enfin compilé) que lorsqu'il ne lui reste plus une seule directive (en effet, le fichier stdio.h inclut lui-même d'autres fichiers, etc.). Nous étudierons bien sûr le préprocesseur de plus près dans les chapitres suivants mais pour le moment, contentons-nous de ce que nous avons appris jusqu'ici. Voici donc la version définitive de notre programme :
Le programme 'Bienvenue sur developpez'
#include <stdio.h>

int main(void)
{
puts("Bienvenue sur developpez");

return 0;

}
Enregistrez-le sous le nom de hello.c et compilez puis testez.

Comme nous pouvons le constater, puts ne fait pas qu'afficher la chaîne qu'on lui a fournie, elle fait également passer à la ligne suivante. Cela est mis en évidence par le programme suivant :
#include <stdio.h>

int main(void)
{
    puts("1. Bienvenue sur developpez");
    puts("2. Bienvenue sur developpez");
    puts("3. Bienvenue sur developpez");
    
    return 0;
}
Qui donne à la sortie :
1. Bienvenue sur developpez
2. Bienvenue sur developpez
3. Bienvenue sur developpez
On voit donc que puts, malgré sa simplicité d'utilisation, est quand même très limitée dans ses possibilités, c'est pourquoi il est généralement préférable d'utiliser printf, qui est beaucoup plus souple, à la place de cette fonction. printf est également déclarée dans stdio.h. Voici ce que devient le programme précédent en remplacant puts par printf :
#include <stdio.h>

int main(void)
{
    printf("1. Bienvenue sur developpez\n");
    printf("2. Bienvenue sur developpez\n");
    printf("3. Bienvenue sur developpez\n");
    
    return 0;
}
\n est une séquence spéciale qui permet d'indiquer la fin d'une ligne. Il s'agit en fait d'un caractère spécial, c'est-à-dire avant tout un caractère ('\n'), au même titre que 'a', 'b', 'c', '1', '2', '3', point, espace, etc. Il n'est pas lié à la fonction printf. Seulement, il n'était pas nécessaire avec puts. Il n'est pas non plus "obligatoire" dans printf, tout dépend uniquement de ce qu'on veut faire.
Par exemple, le programme suivant :
#include <stdio.h>

int main(void)
{
    printf("Bien");
    printf("venue ");
    printf("sur ");
    printf("developpez\n");

    return 0;
}
Donne à la sortie :
Bienvenue sur developpez

I-C. Mécanisme Source, Compilation, Edition de liens, Exécutable

La compilation désigne usuellement le processus de génération du programme exécutable à partir des fichiers sources de ce programme en les soumettant à un programme appelé compilateur (en effet, comme nous le verrons un peu plus loin, il est possible de découper un programme en plusieurs fichiers sources). En réalité, le compilateur se charge simplement de traduire chaque fichier source dans le langage compris par l'ordinateur. Le résultat n'est donc plus un fichier texte mais un fichier contenant des instructions réellement compréhensibles par l'ordinateur, appellé fichier objet.

Un fichier objet n'est cependant pas exécutable, déjà simplement parce qu'il est possible qu'il ne contienne qu'une partie du code du programme complet (cas d'un programme organisé en plusieurs fichiers sources) ou qu'il ne contienne pas le code des fonctions auxquelles il fait référence par exemple. Prenons par exemple le fichier hello.c. Si on compile ce fichier, on obtiendra en sortie le fichier objet correspondant (ça peut être hello.o ou hello.obj selon votre compilateur, système, etc.). Ce fichier ne contient pas cependant le code de la fonction puts utilisée dans la fonction main. Le rôle de l'éditeur de liens sera de chercher ce code et de l'ajouter (de le "lier") au code contenu dans notre fichier objet pour produire un fichier complet, enfin exécutable. Cette phase est appelée l'édition des liens.

En résumé, les grandes étapes de la réalisation d'un exécutable sont : l'écriture du code, la compilation et l'édition des liens.


XVII. Pointeur sur fonctions


XVII-C. Avancé


XVII-C-1. Introduction

Les pointeurs de fonctions sont avant tout des pointeurs, c'est-à-dire des variables qui contiennent une adresse. À la différence des pointeurs (sur objets), les pointeurs de fonctions contiennent l'adresse d'une fonction.
Pour initialiser correctement un pointeur de fonction, il n'y a que 3 moyens :

Il n'y a aucune autre façon. On ne peut notamment pas faire de calculs (arithmétique des pointeurs) sur un pointeur de fonction. Cela n'aurait évidemment aucun sens.


XVII-C-2. Définition

Il existe une syntaxe de base assez tordue qui consiste à utiliser le prototype de la fonction et à y adjoindre adroitement une * et de () selon un logique qui m'échappera toujours :

Soit une fonction
int f(char *);
On définit un pointeur sur cette fonction comme ceci :
int (*pf)(char *);
Je déconseille cette syntaxe qui deviens lourde, surtout si la fonction retourne un pointeur de fonction !

Je recommande une pratique beaucoup plus claire qui consiste à définir un type de fonction. Pour cela, c'est extrêmement simple.

on part du prototype :
int f(char *);
et on ajoute le mot 'typedef' au début :
typedef int f(char *);
Le nom de la fonction devenant un 'type fonction' (en fait, un 'profil' ou 'prototype'), je recommande l'usage du suffixe _f .
typedef int f_f(char *);
Il devient maintenant extrêmement simple de définir un pointeur sur cette fonction :
f_f *pf;

XVII-C-3. Initialisation

Rien de plus simple. Il suffit de donner le nom d'une fonction ayant le même prototype. La fonction doit avaoir été déclarée avant, soit sous la forme d'un prptotype séparé, soit sour la forme d'un définition de fonction. Ces deux formes sont valides :
typedef int f_f (char *);

int f (char *);

/* ou tout simplement
 
f_f f;

*/

int main (void)
{
  f_f *pf = f;

   return 0;
}
et :
#include <stdio.h>
typedef int f_f (char *);

int f (char *s)
{
   puts(s);
   return 0;
}

int main (void)
{
  f_f *pf = f;

   return 0;
}
On peut aussi récupérer la valeur d'un pointeur valide :
<...>
int main (void)
{
  f_f *pf_a = f;
  f_f *pf_b = pf_a;

   return 0;
}
enfin, on peut mettre la valeur NULL et tester celle-ci :
int main (void)
{
   f_f *pf_a = f;
   f_f *pf_b = pf_a;
   f_f *pf_c = NULL;

   if (pf_a != NULL)
   {
   }

   if (pf_b != NULL)
   {
   }
   
   if (pf_c != NULL)
   {
   }

   return 0;
}
Comme pour les pointeurs de données, je recommande que la valeur d'un pointeur de fonctions ne soit jamais indéterminée. Soit elle est valide (adresse d'une fonction), soit elle est invalide (NULL). L'usage d'un pointeur de fonction ayant une valeur indéterminé provoque bien sûr un comportement indéfini, dont les conséquences peuvent être redoutables, puis qu'il s'agit d'exécution de code indéfini...


XVII-C-4. Appel de la fonction pointée

Là encore, c'est extrêmement simple. Il suffit d'utiliser le pointeur de fonction exactement comme une fonction :
#include <stdio.h>
typedef int f_f (char const *);

int f (char const *s)
{
   puts (s);
   return 0;
}

int main (void)
{
   f_f *pf_a = f;
   f_f *pf_b = pf_a;
   f_f *pf_c = NULL;

   if (pf_a != NULL)
   {
      pf_a ("hello");
   }

   if (pf_b != NULL)
   {
      pf_b ("wild");
   }

   if (pf_c != NULL)
   {
      pf_c ("world");
   }

   return 0;
}

XVII-C-5. Utilisation des pointeurs de fonction

Les pointeurs de fonction, c'est bien joli, mais à quoi ça sert ?

Cette question légitime hante les débutants. En fait, l'utilisation principale des pointeurs de fonction est l'implémentation du concept de 'callback'.

Qu'est-ce qu'un callback?

C'est un principe de programmation qui consiste à ce qu'une fonction de niveau inférieur (système, bibliothèque) appelle une fonction de niveau supérieur (application).

Il existe quelques exemples dans la bibliothèque C. Par exemple, atexit() permet d'installer une fonction utlisateur qui sera appelée à chaque fois que le programme appelle la fonction exit(), c'est-à-dire :

Exemple
#include <stdio.h>
#include <stdlib.h>

static void cb_exit (void)
{
   printf ("at exit\n");
}

int main (void)
{
   atexit (cb_exit);

   printf ("end of main()\n");
   return 0;
}
donne
end of main()
at exit

Process returned 0 (0x0)   execution time : 0.044 s
Press any key to continue.
On voit bien que le callback 'cb_exit()' a été appelé après que main() soit terminé.

Je laisse le lecteur faire quelques essais avec exit() et return pour voir à quel moment cb_exit() est appelé.

Si on étudie de près la fonction atexit(), on constate que son interface est :
int atexit	(void (*)(void));
C'est donc une fonction qui attend un paramètre de type
void (*)(void)
C'est pour ça que le prototype de la fonction que j'ai passée en paramètre (cb_exit) est :
static void cb_exit (void)
info static n'a pas de rôle fonctionnel. Il sert simplement à dire au compilateur 'cette fonction n'a pas de lien externe', ce qui est le cas, en principe, des fonctions définies dans main.c (à part main(), bien sûr).
Ce paramètre est un donc pointeur sur une fonction dont le prototype est void f(void).

Le fait d'appeler cette fonction avec l'argument cb_exit se traduit par l'enregistrement quelque part de cette valeur dans un pointeur (global, forcément) de type void (*)(void), par exemple :
void (*G_pf_user_exit)(void);
On peut alors imaginer que dans l'implémentation de exit() je rappelle que exit est appelée quand main() se termine), on trouve :
if (G_pf_user_exit != NULL)
{
   G_pf_user_exit();
}
Rien n'empêche un utilisateur d'utiliser ce principe pour d'autres applications.

Exemple d'utilisation : la réalisation d'un composant logiciel : ici

Evidemment, et comme indiqué dans l'exemple ci-dessus, il est préférable de rester 'contextuel' et donc d'éviter les globales, au profit de structures de contextes.

De même, toujours pour éviter l'usage des globales, j'ai montré dans l'exemple comment passer un pointeur de données générique au callback afin de personnaliser le traitement avec des données utilisateurs.

Ce principe est largement utilisé :



Valid XHTML 1.1!Valid CSS!

Copyright © 2008 Developpez LLC Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique C