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
http://www.mescours.ma/C/c1.html
ntounsi@emi.ac.ma
Version: Sept. 2000, Dernière MàJ: Dec. 2021
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.
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).
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");
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].
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.
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);
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.
On dit alors que la fonction est void
, c'est à dire
sans type. La fonction est alors une simple routine et ne doit pas
être appelée comme une fonction à résultat dans une expression de
calcul. Exemple, une fonction qui fait bonjour.
void hello(){ printf("Bonjour\n"); }
appel par: hello();
Comment faire si on veut une «procédure» qui modifie la valeur de ses paramètres, ou procédure à plusieurs résultats, comme par exemple permut(x, y) qui permute les valeurs de deux variables x et y ?
On passe alors comme paramètres à l'appel non pas les variables mais l'adresse de ces variables,
permut(&a, &b);
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.
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:
.c
% 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.
.o
% cc pgme1.o pgme2.o ...
Les fichiers objets pgme1.o, pgme2.o ...
sont édités pour
(re)créer un exécutable a.out
.
-c
% cc -c pgme.c ...
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.
-o
% cc -o nomExec ...
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 |
|
|
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
.
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.
[2] Se rappeler néanmoins qu'un fichier C peut être compilé avec seulement l'une des parties citées