Concepts avancés du langage C


précédentsommairesuivant

II. Fonctions

II-A. Pointer sur une fonction

Un pointeur rappelons-le est une variable destinée à contenir une adresse. Une adresse référence un objet en mémoire. Une fonction (pendant l'exécution) est tout simplement une suite d'instructions en mémoire. On peut donc pointer sur une fonction tout comme on peut pointer sur n'importe quelle variable.

Une fonction possède un type. Par exemple, considérons les fonctions f et g suivantes :

 
Sélectionnez
int f()
{
    printf("Fonction : f.\n");
    return 0;
}

int g()
{
    printf("Fonction : g.\n");
    printf("-------------\n");
    return 0;
}

Ces fonctions ont le même type et leur type est fonction retournant un int et ne prenant aucun argument.

Mais attention ! La taille occupée en mémoire par une fonction ne peut pas se déduire de son type. Par exemple, f occupe moins de place que g, cela est plus qu'évident. En conséquence : on ne peut pas affecter une « fonction » à une variable. Cependant, rien ne nous empêche de pointer sur une fonction. En se basant toujours sur l'exemple précédent, on peut déclarer le type de f et g comme suit :

 
Sélectionnez
typedef int F(void);

Et on peut donc déclarer un pointeur sur F de la manière suivante :

 
Sélectionnez
F * p;

Ensuite, il suffit de savoir que lorsqu'elle n'est pas utilisée en unique argument de sizeof ou de l'opérateur & ("adresse de"), une expression de type fonction est toujours convertie par le compilateur en l'adresse de cette fonction. Cela signifie que les expressions :

 
Sélectionnez
&f     f    *f   **f  ***f   ...

sont toutes équivalentes. Elles vallent toutes &f.

Donc une fois p correctement initialisée, il y a plus d'une manière d'appeler la fonction pointée :

 
Sélectionnez
p()   (p)()  (*p)()   ...

II-B. Point d'entrée d'un programme C

Au niveau global, un programme C est constitué d'une ou plusieurs fonctions, chacune ayant éventuellement une tâche bien précise. Quel que soit le nombre de fonctions constituant le programme, une doit être désignée comme étant le point d'entrée du programme, c'est-à-dire celle qui sera automatiquement appelée par l'environnement d'exécution lorsqu'on exécute le programme. Cette fonction est bien entendu dépendante de la plateforme. Par exemple sous Windows le nom de cette fonction est main pour une application console, WinMain pour une application fenêtrée, NtProcessStartup pour un driver, etc.

Cependant, pour permettre aux programmeurs d'écrire des applications totalement portables, le comité chargé de normalisation du langage C a désigné une fonction appelée main (héritée d'UNIX) comme point d'entrée d'un programme C standard. C'est ensuite au compilateur et au linkeur de faire en sorte que le programme puisse être exécutable sur la plateforme cible.

main est une fonction qui doit retourner un int. Les deux prototypes possibles pour cette fonction sont :

 
Sélectionnez
>> int main(void)
>> int main(int argc, char * argv[])

Ces arguments (argc et argv), lorsqu'ils sont utilisés, seront évidemment initialisés par l'environnement d'exécution (sinon par qui alors ?). Il est donc clair que les valeurs placées dans ces variables sont dépendantes de l'environnement. Néanmoins :

argv[0] n'est jamais NULL et pointe sur une chaîne qui représente le nom du programme. Si argc est supérieur à 1, argv[1] à argv[argc-1] représentent les paramètres du programme. Dans tous les cas, argv[argc] vaut toujours NULL. Dans la plupart des implémentations, les « paramètres du programme » sont les arguments de la ligne de commande. Par exemple sous Windows, en exécutant le programme prog.exe avec la ligne de commande :

 
Sélectionnez
prog abc xy

On aura :

  • argc = 3
  • argv[0] = "prog"
  • argv[1] = "abc"
  • argv[2] = "xy"
  • argv[3] = NULL

II-C. Quelques fonctions utiles

La fonction exit

 
Sélectionnez
void exit(int status);

déclarée dans stdlib.h, permet de provoquer la terminaison normale du programme. Cette fonction reçoit en argument un entier qui sera retourné comme étant le code d'erreur de l'application. Les valeurs qu'on peut utiliser sont EXIT_SUCCESS (qui est généralement défini à 0), pour indiquer un succès et EXIT_FAILURE, pour indiquer un échec. L'utilisation de toute valeur autre que 0, EXIT_SUCCESS ou EXIT_FAILURE entraîne un comportement dépendant de l'implémentation. Attention ! Il faut avoir appelé free suite à un malloc avant d'appeler cette fonction, sinon la mémoire ne sera pas libérée ! En règle général : libérer toutes les ressources avant d'appeler exit.

La fonction system

 
Sélectionnez
int system(char const * string);

déclarée dans stdlib.h, permet de passer la chaîne string à l'interpréteur de commandes en vue d'être exécutée. L'utilisation de cette fonction rend donc un programme dépendant d'une certaine plateforme (les commandes Windows par exemple ne sont pas à priori les mêmes que les commandes UNIX ...).

La fonction sprintf

 
Sélectionnez
int sprintf(char * buffer, const char * format, ...);

déclarée dans stdio.h, est équivalente à fprintf sauf que la sortie est placée dans buffer plutôt que d'être écrite sur un fichier. Le programmeur doit être sûr que buffer est assez large pour pouvoir contenir la chaîne formatée avant d'utiliser cette fonction.

II-D. La récursivité

Un algorithme récursif est un algorithme dont la définition fait référence à lui-même. Par exemple, étudions l'algorithme qui permet de calculer la puissance d'un nombre c'est-à-dire an. Nous-nous limiterons au cas où a est de type réel et n de type entier relatif. Voici l'algorithme :

 
Sélectionnez
Algorithme « calcul de a p (n) »

/* Cet algorithme donne une définition de a p (n) (a puissance n) */

Debut
    Si n > 0
        a p (n) = a * (a p (n - 1))
    Sinon
        Si n < 0
            a p (n) = (1/a) p (-n)
        Sinon
            /* n == 0 */
            a p (n) = 1
        FinSi
    FinSi
Fin

Et voici un exemple d'implémentation en langage C :

 
Sélectionnez
double a_puissance_n(double a, int n)
{
    if (n > 0)
        return a * a_puissance_n(a, n - 1);
    else
        if (n < 0)
            return a_puissance_n(1/a, -n);
        else
            return 1.0;
}

Ainsi, en calculant a_puissance_n(1.5, 3) par exemple, on obtiendra successivement

  • 1.5 * a_puissance_n(1.5, 2)
  • 1.5 * 1.5 * a_puissance_n(1.5, 1)
  • 1.5 * 1.5 * 1.5 * a_puissance_n(1.5, 0)
  • 1.5 * 1.5 * 1.5 * 1.0

Nous pouvons bien constater que les appels de fonction récursive s'empilent. Une fonction reste toujours « en vie » tant que la suivante n'a pas retourné car elle doit justement attendre la valeur de retour de cette dernière avant de pouvoir à son tour retourner la sienne. Donc, derrière leur simplicité enfantine, les fonctions récursives consomment généralement énormément de mémoire. Heureusement, il n'y a pas d'algorithme récursif qu'on ne puisse écrire sous forme d'itération ...


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Melem. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.