CHAPITRE I

Structure Générale d'Un Programme C

The only way to learn a new programming language is by writing programs in it.
La seule façon d'apprendre un nouveau langage de programmation, c'est d'écrire des programmes avec.
-- Brian Kernighan


Najib TOUNSI,   Original: http://www.mescours.ma/C/c1.html

ntounsi@emi.ac.ma
Version: Sept. 2000, Dernière MàJ: Dec. 2021



Sommaire Suivant >

1. Ce qu'est un Programme C

Un programme C se présente comme un ensemble de fonctions réparties sur un ou plusieurs fichiers nom.c. Pour compiler un programme, il suffit d'indiquer au compilateur C (commande UNIX cc, pour C Compiler) cette liste de fichiers:

cc pgme1.c pgme2.c etc...

(Sur PC commande gcc pour Gnu-C.)

Le compilateur génère alors un fichier exécutable a.out correspondant au programme (pour PC, un fichier .exe de même nom). On peut changer ce nom a.out en un autre, comme monProgramme, avec l'option -o de la commande cc:

cc -o monProgramme pgme1.c pgme2.c

Limitons-nous pour l'instant à un seul fichier source C. Parmi les fonctions d'un programme C, une est obligatoirement présente et doit s'appeler main(). Sans arguments pour l'instant. Elle correspond au programme principal (d'où son nom), i.e. c'est la première exécutée et le nom main est alors obligatoire[1]. Donc un programme C contient au moins cette fonction. L'exécution d'autres fonctions se fait lors de leurs appels comme il est courant.

Certaines fonctions, très utilisées et déjà écrites, peuvent être rajoutées à un programme C par une directive spéciale: #include, qui insère le texte de ces fonctions dans celui d'un programme.

2. Exemples

2.1. Exemple 1

Un premier exemple de programme C, classique, est le programme qui dit bonjour:

main() 
{
      printf("hello, world\n"); 
}

qui imprime le texte:

hello, world

La première ligne est la déclaration de l'entête d'une fonction, ici main(). Les accolades {} délimitent le corps de la fonction (cf. begin ... end). Celui-ci est constitué de l'instruction

printf("hello, world\n");

qui est un appel à une fonction printf() de bibliothèque, avec l'argument "hello,world\n". Cette fonction a pour effet d'imprimer ses arguments sur le terminal (ou la sortie standard du programme). Le seul argument ici est la chaîne de caractères entre double quottes. La séquence \n à une signification spéciale: \ sert à indiquer un caractère de contrôle, n, qui représente ici newline.

Si le programme main() ci-dessus se trouve dans un fichier hello.c on le compile par (% étant le prompt UNIX):

% cc hello.c

et on l'exécute par:

% a.out
hello, world
% _

a.out étant le fichier excutable résultat de la compilation.

Remarque: En réalité la fonction printf() existe dans un fichier stdio.h (pour STandarD Input Output) faisant partie de la bibliothèque du compilateur C et qui contient des utilitaires d'Entrées/Sorties. L'extension .h signifie Header ou Heading (entête).

2.2. Exemple 2

Un deuxième exemple est un programme qui lit deux entiers et imprime leur somme. Il fait appel à une fonction somme(a,b):

#include <stdio.h> 
/* inclusion en tête du programme du fichier bibliothèque 
C/UNIX stdio.h 
*/ 
int somme(int, int);          /* Déclaration d'une fonction somme */ 
main() { 
       int a,b,s; 
       scanf("%d%d", &a,&b);  /* lecture des deux entiers */ 
       s = somme (a,b);       /* appel de la fonction somme */ 
       printf(" Voici leur somme : %d\n", s); 
} 

/* La fonction somme avec deux paramètres formels x et y */ 
int somme (int x, int y) { 
       return (x+y); 
}

La première ligne de la fonction main() est une déclaration d'entiers a b et s. le type est int pour integer, placé avant. Ce sont des variables locales dont la portée est limitée au bloc de la fonction. La ligne suivante est l'appel de la fonction scanf() pour la lecture des variables a et b. Le symbole & sera justifié plus bas (§ 3.3). Entre quottes dans scanf(), il est explicité ce qu'on appelle un format, introduit par le symbole %. Comme il y a deux variables, a et b, il y a deux formats %d, ou d signifie entier en décimal . On utilise %f pour les réels, %c pour caractère et %s pour les chaînes (string)etc... voir plus loin. Ainsi il faut présenter en entrée pour scanf deux entiers (séparés par un espace, une tabulation tab ou entrée newline, qui jouent un rôle de séparateurs de champs).

La ligne suivante du programme est l'appel de la fonction somme avec les arguments (paramètres effectifs) a et b. Fonction dont on connaît l'interface,

int somme(int, int);

composée du nom somme, du type du résultat (int) et des paramètres (int aussi), et déclarée en début de programme.

Ensuite printf() imprime la valeur de la variable s, avec un format décimal %d, précédée du texte Voici leur somme :.

La fonction somme consiste à renvoyer à la fonction appelante, main() ici, la somme x+y. Voyons comment elle se présente: la ligne

int somme(int x, int y)

est l'entête de la fonction qui spécifie son nom, le type de son résultat et de ses paramètres formels x et y. Cette entête est suivie immédiatement du corps de la fonction entre {}. Une autre écriture de l'entête de la fonction est

int somme (x, y) int x, y;

où on déclare les paramètres après leur énumération entre (). La première écriture est la syntaxe C ANSI (et C++), plus intéressante ( cf. Chapitre. V).

Les symboles /* et */ encadrent un commentaire en C.

/* Ceci est un commentaire */
/* Ceci est /* n'est vraiment pas */ un autre commentaire */

On peut remarquer déjà que chaque déclaration ou instruction en C se termine par point virgule «;» . On verra que contrairement à PASCAL par exemple, en C «;» ne sépare pas deux instructions ou déclarations, mais termine une instruction ou déclaration.

Voici la compilation et l'exécution de ce deuxième programme:

% cc somme.c 
% a.out 
2 
3                     <--- entrée des données: 2 pour a et 3 pour b 
Voici leur somme : 5  <--- Résultat affiché

Une deuxième exécution:

% a.out 
2 3                   <--- { mêmes données sur même ligne cette fois-ci } 
Voici leur somme : 5

On peut imprimer un texte pour inviter l'utilisateur à rentrer des données: on doit sortir un message par printf() avant de faire scanf()

printf(" Renter a ensuite b > "); 
scanf("%d%d", &a,&b);

ou mieux

printf(" Renter a > "); 
scanf("%d", &a); 
printf(" Renter b > "); 
scanf("%d", &b);

Ce qui donne dans ce dernier cas

% a.out 
Renter a > 2 
Renter b > 3 
Voici leur somme : 5 
%

Remarque: Dans scanf(), il est préférable pour l'instant de ne rien mettre d'autre que les formats %d %c ... C'est une des sources d'erreurs difficile à apercevoir. Ne pas mettre par exemple scanf("%d\n");

3. Structure d'un Programme C

Un programme C se présente sous la forme générale suivante:

<directives de compilation> 
<déclarations de noms externes> 
<textes de fonctions>

Seule la partie textes de fonctions est obligatoire avec au minimum la fonction main() pour un programme complet[2].

3.1. Directives de Compilation

Classiquement on y retrouve ce dont le compilateur a besoin pour compiler correctement un programme. A savoir les #include pour les fichiers fonctions de bibliothèque à inclure dans le programme, des définitions de symboles de types ou de constantes, ou des macros. Cette partie est traitée par un préprocesseur: programme invoqué pour un premier passage sur un texte source ( Annexe A). C'est une autre caractéristique de C.

Exemples:

#define begin { 
#define end } 
#define then 
#define MAX 1000 
#include <math.h> 
#include <ctype.h> 
#include <string.h>

begin est défini comme symbole synonyme de {. Ainsi, si begin apparaît dans un programme, le préprocesseur le remplacera par {. end est défini comme synonyme de } et then comme synonyme de rien (il sera ignoré car il n'y a pas le mot then en C). MAX est défini comme synonyme de la constante entière 1000.

Il est ensuite demandé d'inclure dans le texte du programme les fichiers de bibliothèque math.h, ctype.h et string.h. Respectivement, des fonctions mathématiques, des utilitaires sur les types de donnés et la manipulation de chaînes.

3.2. Déclarations de Noms Externes

Dans cette partie, on peut déclarer des noms globaux dont la portée peut être l'ensemble d'un programme C. Ces noms correspondent à des variables aussi bien qu'à des fonctions. Pour ces dernières, seule l'interface (l'entête sans le corps) peut être fournie. On l'appelle aussi prototype de fonction (voir Chapitre. V).

Exemple:

int i;
float f( int, char*, float);

3.3. Textes de Fonctions

Viennent enfin les définitions de fonctions, entête et corps, qui constituent le programme proprement dit, i.e. la partie calcul. Leur structure est la suivante:

[<type_résultat>] <nom_de_fonction> ([<liste arguments typés>]) 
{ 
<texte des déclarations et instructions> 
}

Exemple:

float fahr2celc (int f)
{     /* Convertit une température fahrenait f en celius c */
      float c;
      c = (5.0/9)*(f-32);
      return c;
}

Noter que la notion de ligne de texte n'existe pas en C. Mais il est de bonne habitude d'écrire le corps d'une fonction sous forme d'une instruction par ligne, indentée selon le besoin. (A ce propos, les utilitaires UNIX cb, ou indent sont très intéressants). Noter qu'une fonction a besoin d'un commentaire aussi qui explique ce qu'elle fait.

Une fonction rend un résultat et son type est celui précédant le nom de la fonction. Ce type est pris par défaut comme int pour integer. Ainsi on aurait pu définir la fonction somme du § précédent comme

somme (int x, int y) {
...
}

sans avoir besoin de la déclarer en début de programme.

Noter aussi qu'en C les arguments sont passés par valeurs, comme il convient pour une fonction. (C++ a introduit, entre autre, le passage des paramètres par référence, voir remarque plus bas). Plusieurs questions se posent alors.

Le programmeur dispose d'un opérateur unaire & qui permet d'obtenir l'adresse d'une variable. Ainsi, c'est la variable elle-même qui est manipulée, à travers son adresse, et non pas une copie. (On doit dans ce cas de-référencer x et y dans la fonction avec la notation *x et *y. La fonction est déclarée alors:

void permut(int *x, int *y))

C'est pourquoi les paramètres, a et b, de la fonction scanf() du programme précédent sont précédés de &. Ce sont des résultats de la fonction.

Cette possibilité sert aussi si la taille d'un objet est suffisamment grande pour être passée en paramètre par valeur.

Remarque: C++ a introduit le passage par référence, pour une commodité de notation. On n'a pas besoin de de-référencer les paramètres dans la fonction.

4. Compilation d'un Programme C

Nous avons vu que l'appel au compilateur C se fait par la commande UNIX cc. Nous l'avons utilisée telle que pour compiler un seul fichier et pour générer directement un module exécutable a.out. En fait, cette commande cc enchaîne le préprocesseur, la compilation, l'assemblage et l'édition de lien d'un programme. Chacune de ces phases peut être faite individuellement bien sûr mais, dans les cas simples, on fait le tout d'un coup. Sinon on sépare la phase d'édition de lien, au cas où un programme se compose de plusieurs fichiers, pour ne pas avoir à recompiler des morceaux déjà compilés et corrects. C'est ce qu'on appelle la compilation séparée.

La forme générale de cette commande cc est:

% cc [<options>] <nomFichiers> ... [-l<Librairie>] ...

Principalement, les options sont -c et -o et les fichiers se terminent par .c et .o Les fichiers .c sont des sources C et les fichiers .o sont des modules objets résultats de la compilation de fichiers .c. Ce sont des fichiers codes binaires non encore édités ou liés, donc non prêts à l'exécution.

Remarque: Ces fichiers .o sont toujours créés mais ne sont pas toujours sauvegardés. Ils peuvent l'être à la demande ou à la compilation de plusieurs sources C. On verra plus bas l'utilité de les garder.

La partie -l est nécessaire (lors de l'édition de lien) pour faire appel à des modules objets de bibliothèque, quand on utilise des fonctions qui s'y trouvent. Exemple, -lm pour la bibliothèque mathématique, -lX11 pour celle X11, etc ... Nous en ferons abstraction ici. Ainsi la commande cc se présente:

% cc pgme.c

Le fichier pgme.c est compilé et un exécutable a.out correspondant est généré (Si toutefois il existe une fonction main() sinon il y a erreur d'édition empêchant de générer un exécutable).

% cc pgme1.c pgme2.c ...

Les fichiers sources pgme1.c, pgme2.c... sont compilés et, pour chacun, un module objet pgme1.o, pgme2.o ... est généré en plus de l'exécutable (général) a.out. Ces fichiers sont générés dès qu'il y a plusieurs sources .c .

Soit, sans options, la commande cc avec un seul fichier source .c crée un fichier exécutable a.out, et avec plusieurs fichiers sources .c, elle crée en plus les fichiers modules objets pgme1.o, pgme2.o ... correspondants.

Les fichiers objets pgme1.o, pgme2.o ... sont édités pour (re)créer un exécutable a.out.

Le (ou les) fichier(s) source(s) est (sont) compilé(s) pour générer un (ou des) module(s) objet(s) pgme.o Il n'y a pas de a.out en sortie.

Le fichier exécutable a.out est nommé par nomExec. Cette option est ignorée si l'option -c est présente (il n'y a pas d'exécutable à générer). Se rappeler qu'il faut une fonction main() pour former un exécutable.

On peut constater que le (1) correspond à la partie enchaînement de l'ensemble: préprocesseur, compilation/assemblage et édition de lien le (2) correspond à la partie édition de lien uniquement et le(3) correspond à la partie préprocesseur et compilation/assemblage uniquement.

On peut aussi noter l'existence de la commande cpp (C PreProcessor) pour effectuer uniquement le passage du préprocesseur, et de la commande ld (Link eDitor) pour l'édition de lien des fichiers .o la génération d'un exécutable.

En Résumé:

COMMANDE RESULTAT

cc pgme.c

cc pgme1.c pgme2.c

cc pgme1.o pgme2.o

cc pgme1.c pgme2.o

cc -c pgme.c

cc -o exec pgme.o

cc -o exec pgme.c

a.out

a.out, pgme1.o, pgme2.o

a.out

a.out, pgme1.o (parfois)

pgme.o

exec

exec

Remarque: Il y a parfois un effet de bord, quand un fichier exécutable est généré et quand on compile simultanément des fichiers .o et .c. Cet effet est celui de créer, s'ils n'existent pas, ou de supprimer s'ils existent, les fichiers .o correspondant aux fichiers .c.

5. Compilation Séparée

Il est très utile --et très conseillé-- quand un programme est assez long, de l'écrire en morceaux compilés séparément en fichiers.o par la commande UNIX cc-c. Fichiers à éditer plus tard par cc (ou cc -o) pour générer un programme exécutable, si une fonction main() est présente. D'ailleurs, celle-ci peut se limiter aux appels nécessaires pour lancer et contrôler l'application, et être ainsi la dernière écrite.

Les différents morceaux du programme peuvent aussi être écrits en plusieurs versions chacun, laissant le choix au programmeur d'éditer les morceaux voulus et de composer l'application lors de la dernière compilation qui crée l'exécutable final. Nous y reviendrons plus loin quand on verra l'utilitaire MAKE, qui permet la re-compilation (entre autre) automatique des programmes et la composition d'une application.

La compilation séparée répond au besoin de la programmation modulaire: l'écriture de programmes en plusieurs morceaux ou modules. C'est un domaine à part entière qui déborde du cadre de ce cours. Disons simplement que la décomposition modulaire la plus simple c'est d'écrire des «sous-fonctions» qui réalisent des tâches plus élémentaires qui concourent à la réalisation de la fonction globale d'un programme. Les modules qui en résultent doivent être les plus indépendants et les plus autonomes possibles, (leurs relations ou interconnections dans le cadre du programme globale est réduite au strict nécessaire) et par conséquent pouvant servir pour d'autres programmes. La tâche de chaque module doit être alors très bien spécifiée, i.e. définie avec précision. Mis à part qu'il doit être fortement commenté aussi.

Le talent d'un(e) programmeur(se) se reconnaît à la conception modulaire de ses programmes.

Remarque: Un module réalisant une certaine tâche ne se limite pas forcément à une fonction au sens langage de programmation. Au contraire, On peut envisager des modules à la PARNAS (du nom de D.L. PARNAS qui a longtemps étudié la question), c'est à dire constitués d'un ensemble de fonctions logiquement reliées et manipulant un ensemble de données (ressources) communes déclarées au sein du même module, et accessibles uniquement en son sein. Ce qui s'appelle Information Hiding, ou protections des informations contre les accès (modifications) imprudents et involontaires. Cette caractéristique, avec d'autres comme l'héritage, est à la base de la programmation orientée objets, et a été retenue dans le langage C++ sous la notion de classe.
 


[1] On peut bien sûr compiler séparément un fichier de fonctions C sans vouloir en constituer un programme. Voir §4.

[2] Se rappeler néanmoins qu'un fichier C peut être compilé avec seulement l'une des parties citées 


Sommaire Suivant >


Copyleft © Najib TOUNSI
ntounsi@emi.ac.ma
Version : Sept 2000 Dernière MàJ: Dec 2021