Introduction au langage C
Deug STPI 1ère année
2002-2003
Yannick Chevalier1

Chapitre 1 : Présentation du langage C



Mots-clefs : langage (de programmation), compilateur, bibliothèque, algorithme, code source, utilisateur, application, fichier, fonction, affectation, terminal, shell.




1.1 Qu'est-ce qu'un algorithme ?





Un algorithme, c'est essentiellement une recette. Une personne qui écrit un algorithme garantit qu'en faisant toutes les étapes pas à pas, on aboutira au résultat de l'algorithme. Par exemple :

  1. prendre 2 oeufs ;
  2. les casser dans un récipient ;
  3. ajouter du sel et du poivre ;
  4. mélanger le tout ;
  5. faire cuire à la poêle.
Alg. 1.0 : Recette de l'omelette


Pour que ça marche, on remarque qu'il faut écrire l'algorithme dans une langue qui est compréhensible par la personne qui doit suivre l'algorithme.



1.2 Qu'est-ce qu'un ordinateur comprend ?






1.2.1 Le langage machine




Le langage utilisé par le processeur est le langage machine. Il est difficile de comprendre et d'écrire des programmes dans ce langage. Par exemple, une ligne ressemble à :




Figure 1.1: Une ligne en langage machine...


Ce langage correspond mot pour mot (on peut faire des traductions littérales) à un autre langage, l'assembleur, qui est plus simple à comprendre. Il existe des traducteurs simples de l'assembleur vers la langage machine. On appelle ces traducteurs des compilateurs.


1.2.2 L'assembleur




En assembleur, il faut écrire toutes les étapes que doit suivre l'oridnateur pour faire un algorithme. Ce langage est utilisé lorsqu'on veut des programmes vraiment rapides : on écrit en assembleur les parties qui prennent le plus de temps.

        .file   "ex.c"
        .version        "01.01"
gcc2_compiled.:
.section        .rodata
.LC0:
        .string "Bonjour\n"
.text
        .align 4
.globl main
        .type    main,@function
main:
        pushl %ebp
        movl %esp,%ebp
        pushl $.LC0
        call printf
        addl $4,%esp
.L1:
        leave
        ret
.Lfe1:
        .size    main,.Lfe1-main
        .ident  "GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)"

Exemple de programme en assembleur.



1.3 Langages de haut niveau





L'assembleur n'est pas très pratique pour écrire de longs programmes. On s'y perd rapidement, et il est très facile de faire des erreurs. Aujourdh'ui, lorsqu'on programme, on préfére utiliser des langages de haut niveau, c'est-à-dire qui peuvent être compris par des humains. Les avantages sont :
  1. c'est plus facile à apprendre ;
  2. il ne faut pas tout changer quand on change d'ordinateur ;
  3. cela permet d'écrire des programmes plus courts et plus clairs.
Pour la plupart des ordinateurs, plusieurs langages sont disponibles : le pascal, ocaml, prolog, ..., C. On fait la différence entre les langages de haut niveau et ceux de bas niveau. Si on reprend la recette de l'omelette, la description dans un langage de haut niveau pourrait être :

  1. battre des oeufs, du sel et du poivre ;
  2. les cuire dans une poêle.
Alg. 1.0 : Recette de l'omelette, haut niveau


À chaque fois qu'on utilise un langage de programmation, il faut avoir un traducteur entre ce langage et le langage machine (ou l'assembleur). Ce traducteur, c'est le compilateur.



1.4 Utilisation du langage C





Le langage C est un langage de plutôt bas niveau. Cela a des avantages et des inconvénients.

Les principaux avantages sont qu'il n'y a pas beaucoup de vocabulaire à apprendre, et que le programmeur peut s'arranger pour que le programme soit très rapide.

Les inconvénients sont que souvent un programme ne fait pas ce qu'il a l'air de faire, il est très facile d'écrire un programme qui est illisible, et rien n'est défini par défaut. En C, il n'y a pas de mot du vocabulaire (qui a 32 mots) pour afficher un résultat.

Cette limitation du vocabulaire oblige à utiliser des boites à outils (des bibliothèques de fonctions, ou library en anglais). Il existe une bibliothèque qui contient une fonction qui permet d'afficher un résultat, par exemple.

Le compilateur qui traduit un programme du C vers le langage machine est gcc.



1.5 Création d'une application






1.5.1 Programme, programmeur et utilisateur




Le mot programme est utilisé pour deux choses :
  1. il peut s'agir d'un texte, le code source ---écrit en C, par exemple--- décrivant ce qui doit être fait ;
  2. ou il peut s'agir de l'application qui contient les instructions que l'ordinateur doit suivre. Cette application est la traduction (compilation) du code source en langage machine.
Pour le deuxième cas, par exemple, xemacs ou le serveur de fenêtres est un programme. D'habitude, on parle aussi de programme pour le code source d'une application. Le programmeur est celui qui écrit le code source, et l'utilisateur est celui qui utilise l'application. On va maintenant voir comment passer du code source d'un programme à une application.


1.5.2 Processus de compilation




rappel : il s'agit de la traduction dans le langage de l'ordinateur d'un programme écrit dans le langage C.
Un programme écrit en C a toujours besoin d'utiliser des fonctions qui ont déjà été définies dans des bibliothèques de fonctions. Lors de la traduction, le compilateur (gcc) regarde dans le fichier où est défini le programme les bibliothèques qui sont utilisées. Ces bibliothèques sont ensuite utilisées pour créer l'application.




Figure 1.2: Processus de compilation



1.5.3 édition d'un programme




Pour écrire un programme, on peut utiliser n'importe quel éditeur de textes (et pas un logiciel de formattage comme Word !). Parmi ceux disponibles (vi,emacs,kedit,...), on utilisera en TP l'éditeur de textes XEmacs, parce qu'il a un bon mode d'édition de programmes C.


1.5.4 Compilation




Le programmeur est aussi utilisateur de programmes. Sous Unix, il est possible de donner des instructions de haut niveau à l'ordinateur en utilisant un shell de commande. Il faut lancer une application, appelée un terminal, qui donne accès à un shell. Sous KDE, l'icône correspondant à cette application est reconnaissable à son coquillage (shell, en anglais). Il est aussi possible de lancer un shell dans XEmacs.

Pour compiler un programme, le plus simple est d'utiliser un shell. Pour compiler le programme toto.c, il faut utiliser la commande :



dendron % gcc toto.c




Le plus souvent, gcc donne une liste d'erreurs dans le programme. Il faut les corriger dans l'éditeur de texte, et réessayer de compiler. Une fois que le programme source est correct, gcc crée une nouvelle application dont le nom par défaut est a.out. Si on veut que l'application s'appelle toto, on utilise :



dendron % gcc -o toto toto.c






1.6 Format d'un programme C





Deviner ce que fait le programme suivant :

#include <stdio.h>
int
main(int argc,char * argv[]){
/* fonction principale */
printf("Bonjour !\n");
}
Alg. 1.0 :  Un premier programme


Dans ce programme, #include <stdio.h> indique qu'on va utiliser des fonctions de la bibliothèque stdio (STandarD Input/Output).

Ensuite, on déclare la fonction main. Elle ne prend pas d'arguments (il n'y a rien dans les parenthèses), et rend toujours le résultat 0 (son résultat est de type int ). Il doit y avoir une fonction main dans tous les programmes C. C'est la fonction qui démarre le programme. Ici, cette fonction ne fait qu'une chose : elle appelle la fonction printf, qui est définie dans la bibliothèque stdio, et qui affiche une chaîne de caractères.
Les chaînes de caractères sont entourées par des guillemets « " ». Le « \n» est un caractère spécial qui fait aller à la ligne. Il existe aussi « \t», qui permet d'afficher une tabulation.

Dans ce programme, il y a enfin des commentaires. Ce sont les parties entre « /* » et « */ ». Les commentaires sont des parties du programme qui sont ignorées par le compilateur. Ils servent à rendre un programme compréhensible. Leur utilité n'est pas évidente pour un programme aussi simple, mais ils sont obligatoires dès qu'un programme contient plusieurs fonctions.

Les fonctions de C se comportent comme les fonctions mathématiques : elles prennent en entrée une valeur, et rendent comme résultat une autre valeur.

Plus généralement, un programme C contient deux parties :
  1. une partie où on indique les bibliothèques qu'on va utiliser. Cette partie contient presque toujours:
    #include <stdio.h>
  2. une partie où sont définies des fonctions. Dans cette deuxième partie, il faut toujours définir au moins la fonction main.


1.7 Types de données de base






1.7.1 Qu'est-ce qu'un type de donnée ?




Lorsqu'on écrit un algorithme (par exemple une recette de cuisine), il est évident que certains objets ne peuvent pas être mis à la place d'autres objets. Par exemple, la phrase « cuire une poêle dans les oeufs » n'a pas de sens. Les types servent à détecter certaines de ces phrases qui n'ont pas de sens. Par exemple, on peut dire que pour les recettes de cuisine, il y a deux types :
  1. le type ustensile ;
  2. le type comestible.
La poêle est de type ustensile, et les oeufs sont de type comestible (on espère). Maintenant, on peut définir une fonction cuire, qui a deux arguments a et b. cuire(a,b) signifie alors « cuire a dans b ». On peut alors imposer que a doit être un ustensile, et b doit être comestible.

En faisant cela, et avant même d'essayer, on peut alors dire que la phrase « cuire une poêle dans les oeufs », c'est-à-dire cuire(poêle,oeuf), n'a pas de sens pour quelqu'un lisant la recette : il remarque que poêle n'est pas comestible et que oeuf n'est pas un ustensile !

Autrement dit, les types de données servent à dire qu'il est possible de faire certaines opérations, mais pas d'autres (on peut cuire des oeufs, pas des poêles.)


1.7.2 Les types de données en C




Dans le langage C, on ne va voir que 4 types de données:
  1. le type char : pour un caractère;
  2. le type int : pour un entier;
  3. le type float : pour les nombres à virgule;
  4. le type void : pour les données qui n'ont pas de type.
Par exemple, dans un programme, on peut avoir :

1.8 Affectation






1.8.1 La mémoire




Lorsque le processeur effectue des calculs, il a souvent besoin de stocker des résultats intermédiaires. Pour cela, il utilise la mémoire vive de l'ordinateur. On peut voir cette mémoire comme un gigantesque placard avec plein de tiroirs, et où chaque tiroir ne peut contenir qu'une seule valeur. En C, avant de donner des instructions au processeur, le programmeur doit déclarer les cases qu'il compte utiliser pour exprimer les instructions.


1.8.2 Déclaration des variables




Une variable est le nom que l'utilisateur donne à une case. C'est la compilateur qui décide de l'endroit de la mémoire où se trouve la case. Le programmeur peut retrouver cet endroit avec le symbole & : la variable de nom i se trouve à l'adresse &i.

Le programmeur, en même temps qu'il déclare une variable, doit lui donner un type.




Figure 1.3: Dans cette figure, la variable i vaut 8 et & i vaut 5



1.8.3 Affectation




Ces variables sont utilisées pour stocker des valeurs. On change la valeur stockée dans une variable en affectant une valeur (qui est à droite du signe =) à une variable qui est à gauche du signe =. L'affectation se fait avec le symbole =.

  • x = 5;
  • x = 5+12;
  • x = x+1;
  • a= 'c ';/* on stocke le caractère 'c' dans la case correspondant à la variable a */
Alg. 1.0 : Exemples d'affectation


à droite du signe =, on peut faire des opérations. Sur les nombres entiers et les nombres à virgule, les opérations possibles sont :

  1. +,- : addition, soustraction ;
  2. * : multiplication ;
  3. / : division. Il faut noter que si x=5 et y=3, x/y vaut 1.6666... si x ou y est de type float , et vaut 1 si les deux sont de type entier (division entière) ;
  4. % : reste de la division entière (pour les nombres entiers). Par exemple, 5%3 vaut 2. Pour deux variables x et y de type int , on a toujours x est égal à (x/y)*y+(x%y) ;


Figure 1.4: un simulateur de vol écrit en C


Chapitre 2 : Boucles



Mots-clefs : itération, while , do ...while , for




2.1 Motivation pour les boucles





L'intérêt de l'informatique est de pouvoir effectuer automatiquement des tâches répétitives. On peut exprimer ces tâches répétitives de deux manières :
  1. soit on dit : « Fais dix fois » :
    1. prendre une brique ;
    2. la mettre dans la brouette.
  2. soit on dit : « Fais jusqu'à ce que (la brouette soit pleine) » :
    1. prendre une brique ;
    2. la mettre dans la brouette.
Dans le premier cas, on sait avant le début de la boucle combien de fois il faut suivre les instructions. C'est aussi le cas lorsqu'on veut faire des instructions pour chaque jour de la semaine, ou pour tous les mois, etc. Dans le second cas, on n'est pas sûr d'arrêter un jour :

Faire tant que la brouette n'est pas pleine :
  1. prendre une brique ;
  2. la mettre dans la brouette.
Alg. 2.0 : Boucle qui ne s'arrête pas


Dans ce deuxième cas, si on comprend qu'il faut commencer à prendre la brique dans la brouette, on ne termine jamais la tâche.

Dans ce chapitre, on va voir les différentes manières d'exprimer une répétition, ainsi que les différentes manières de traiter les cas.



2.2 Tester si une condition est vraie






2.2.1 Codage des valeurs vrai/faux




Dans certains langages, il y a un type de données booléen. Les variables qui ont ce type ne peuvent prendre que les valeurs vrai ou faux. Dans ces langages, on peut exprimer qu'une réponse est vraie ou fausse en disant qu'elle a le type booléen.

En C, il n'y a pas ce type de données. On utilise des entiers (type int ) pour savoir si la réponse à une question est vraie ou fausse. Pour cela, on utilise toujours :

2.2.2 Opérations booléennes






Ce sont des opérations dont le résultat est un entier !





Le tableau suivant recense les opérations courantes ainsi que leur résultat :


Expression valeur
(x == y) 1 si x est égal à y, et 0 sinon
(x != y) 1 si x est différent de y, et 0 sinon
(x < y) 1 si x est strictement plus petit que y, et 0 sinon
(x > y) 1 si x est strictement plus grand que y, et 0 sinon
(x <= y) 1 si x est plus petit ou égal à y, et 0 sinon
(x >= y) 1 si x est plus grand ou égal à y, et 0 sinon
Opérateurs booléens

x peut être de n'importe quel type, mais il faut que y ait le même type.



L'instruction d'affectation, x=y, signifie que la variable x prend la valeur de la variable y. Une erreur très courante est de confondre x=y et x==y dans les tests. Pour C, les deux opérations sont possibles, donc il ne signale pas d'erreur.






2.2.3 Composition d'opérations booléennes




Les opérations suivantes servent à composer des tests booléens :


Expression valeur
! x 1 si x vaut 0, et 0 sinon
x && y 1 si x et y sont différents de 0, et 0 sinon
x | | y 1 si x ou y sont différents de 0, et 0 sinon
Opérateurs booléens

Dans le Tableau ,
L'expression vaut 1 si ...
(x = 5)&& (x <7) (erreur ?)
(x == 5)&& (y < 'c ') (erreur ?)
!(x == 5) | | (y==5) si x est égal à 5, alors y est égal à 5
( x != 5) | | (y==5) (pareil)
(x == 17) && (y==5) | | (x != 17) =((x == 17) && (y==5))| | (x != 17)
Alg. 2.0 : Tests




2.3 Boucles while





Ces premières boucles servent à effectuer une action jusqu'à ce qu'une condition soit vraie. Par exemple :

#include <stdio.h>
/* On calcule le reste de la division entière d'un nombre i par le nombre j */
int
main(int argc, char * argv[]) {
int i,j;
/* On prend deux valeurs quelconques pour initialiser */
i=30;
j=12;
/* On enlève j de i jusqu'à trouver un nombre inférieur à j */
while (i >= j) {
i = i - j;
}
printf("%d\n",i);
return 0;
}
Alg. 2.0 :  Utilisation de la boucle while


Le Programme  permet de calculer le reste de la division entière de deux nombres. Pour cela, on soustrait j à i :

i = i - j;

tant que i est plus grand que j :

while (i >= j)

On peut regarder l'exécution pas-à-pas du Programme , en notant les valeurs des variables (voir Tableau ).

On peut réécrire toutes les instructions qu'on va voir d'ici à la fin du chapitre en utilisant uniquement des boucles while . Les instructions suivantes sont néanmoins importantes, car elle permettent d'écrire un programme plus lisible.


instruction valeur de i valeur de j valeur de (i >=j) Notes
début       On a juste déclaré les variables i et j
i=30 ; 30      
j=12 ; 30 12 1  
while cond 30 12 1 la condition est différente de 0, donc on entre dans la boucle
i = i - j; 18 12 1  
while cond 18 12 1 la condition est différente de 0, donc on entre dans la boucle
i = i - j; 6 12 1  
while cond 6 12 0 la condition est égale à 0, donc on passe à l'instruction suivant la boucle
printf("%d\n",i); 6 12 0 On affiche la valeur entière de i (%d), et on passe à la ligne (\n)
return 0; 6 12 0 La fonction main doit rendre un entier, on retourne l'entier 0, qui signifie une exécution correcte
Exécution d'une boucle while simple



2.4 Boucles for





Dans les exemples précédents, on ne donne pas, dès le départ, combien de fois on doit exécuter la boucle. Il y a un cas où le nombre d'itérations est fixé dès le départ, c'est lorsqu'on cherche à calculer un élément d'une suite définie par récurrence. Ce cas semble a priori très limité (suites arithmétiques et géométriques). Ce n'est pas le cas, et ce type de calcul se retrouve chaque fois qu'on cherche un élément dans un ensemble (le plus petit, le plus grand,...)


2.4.1 Calcul d'un élément d'une suite récurrente.




On donne la suite :

u0  =  3
un+1  =  un+4   (n³ 0)

et on veut calculer u10. Il faut a priori utiliser 11 variables, pour stocker les 11 valeurs de la suite jusqu'à ce qu'on arrive à u10. Mais on peut faire beaucoup mieux !

Le calcul de base peut se voir à la manière de la Figure 2.1. On aligne tous les éléments de la suite, et on calcule les valeurs les unes après les autres.




Figure 2.1: Calcul benêt du dixième élément d'une suite.


Ce n'est pas efficace, car on se souvient de tous les éléments qu'on a calculé, alors qu'on a juste besoin du dixième.

On peut calculer cette suite avec seulement deux variables. Pour voir comment, on reprend le calcul des éléments (voir Figure 2.2)




Figure 2.2: Calcul du dixième élément d'une suite.


Sur la Figure 2.2, on remarque qu'il n'est pas utile d'avoir toutes les cases. On peut effectuer le calcul du dixième élément de la suite avec l'algorithme suivant :
  1. mettre la valeur initiale de la suite dans la case de gauche de la fenêtre ;
  2. faire 9 fois :
    1. calculer la valeur de la case de droite de la fenêtre ;
    2. déplacer la fenêtre vers la droite ;
  3. la dixième valeur est dans la case de droite de la fenêtre.
Pour déplacer la fenêtre vers la droite, on va juste mettre la valeur qui est dans le case de droite dans la case de gauche. On obtient le Programme  page ??.

#include <stdio.h>
/* Calcul du dixième élément de la suite u donnée par récurrence */
/* Méthode de la fenêtre coulissante */
int
main(int argc, char * argv[]) {
float case_droite,case_gauche;
int i;
/* On initialise la valeur de la case de gauche */
case_gauche=3;
/* On effectue dix fois le déplacement de la fenêtre */
for (i=0; i< 10; i++) {
/* calcul de la nouvelle valeur */
case_droite = case_gauche + 4;
/* déplacement de la fenêtre */
case_gauche = case_droite;
}
/* affichage du résultat */
printf("la dixième valeur est %d\n", case_gauche);
return 0;
}
Utilisation de la boucle for


Pour utiliser une boucle for , il faut déclarer une variable de type entier (c'est ce qu'on a fait avec la variable i en déclarant int i;). Ensuite, pour faire N fois les instructions de la boucle, on utilise:
for (i=0; i<N ; i++)
Les instructions seront exécutées une première fois avec dans i la valeur 0, puis une deuxième fois avec dans i la valeur 1,... et une dernière fois avec dans i la valeur N-1. Ensuite, la condition i < N n'est plus vraie, et on sort de la boucle.


2.4.2 Amélioration du code




On remarque que dans la boucle du Programme  page ??, il y a les deux instructions :

/* calcul de la nouvelle valeur */
case_droite = case_gauche + 4;
/* déplacement de la fenêtre */
case_gauche = case_droite;

Regardons comment les valeurs de case_droite et case_gauche évoluent au cours de l'exécution (voir Tableau ).


case_gauche case_droite instruction
3   case_gauche = 3 ;
3 7 case_droite = case_gauche + 4;
7 7 case_gauche = case_droite ;
7 11 case_droite = case_gauche + 4;
11 11 case_gauche = case_droite ;
11 15 case_droite = case_gauche + 4;
15 15 case_gauche = case_droite ;
15 19 case_droite = case_gauche + 4;
19 19 case_gauche = case_droite ;
19 23 case_droite = case_gauche + 4;
23 23 case_gauche = case_droite ;
...
Évolution des valeurs de case_droite et case_gauche.

On remarque qu'on n'a pas besoin de la variable case_droite ! Si on remplace les deux affectations de la boucle du Programme  par :

case_gauche = case_gauche +4

on calcule le même résultat.



2.5 Boucle do ...while





Il y a un dernier cas de boucle, qui est utilisé le plus souvent lorsque des informations sont demandées à un utilisateur. Il s'agit d'une boucle while , mais qui est toujours exécutée au moins une fois.

int nombre=0;
char reponse;
do {
printf("Oui ou non ? répondre (o/n)\n");

/* On demande à l'utilisateur de donner sa réponse */
  nombre=scanf("%c",reponse);
/* On teste si on a bien lu une réponse (nombre contient le nombre de variables lues) */
/* par scanf) et on vérifie la réponse. On repose la question si le nombre est 0, ou */
  /* si la réponse est différente de 'o' ou 'n'. */
}while ((nombre == 0)||((reponse !='o')&&(reponse !='n')));
Alg. 2.0 : Demander à un utilisateur de répondre o ou n


Ce type de boucles peut être utilisé chaque fois qu'on a besoin de faire une boucle while au moins une fois.

Chapitre 3 : Exécution conditionnelle





3.1 Motivations





Un autre intérêt de l'informatique est de pouvoir traiter différents cas. Par exemple, un programme qui compose des lettres doit écrire :
  1. « Cher monsieur, » si le destinataire est masculin, et
  2. « Chère madame, » si le destinataire est féminin.
Il devrait aussi faire deux cas si le destinataire est féminin, un pour le cas d'une personne mariée, et un pour le cas d'une personne célibataire. On remarque que dans cet exemple, on peut toujours savoir dans quel cas on est en répondant à une question par vrai ou faux :

Est-ce que le destinataire est masculin ?
  • oui : afficher « Cher monsieur, »
  • non : Est-ce que le destinataire est marié ?
    • oui : afficher « Chère madame, »
    • non : afficher « Chère mademoiselle, »
...
Alg. 3.0 : Programme écrivant des lettres


Mais il y a aussi des cas où il est plus simple de pouvoir avoir plusieurs réponses possibles à une question.

Si le feu est :
  • vert : ne pas ralentir ;
  • orange : ralentir et s'arrêter ;
  • rouge : rester arrêter.
Alg. 3.0 : Cas où il y a plus de 2 réponses possibles




3.2 Structure if





Le if correspond aux cas où on fait deux traitements différents selon que la réponse à une question est oui ou non.


3.2.1 if ...else




Sur des exemples :

/* Est-ce que x est supérieur ou égal à 0 ? */
if (x>=0) {
/* oui : cas où x est supérieur ou égal à 0 */
valeur_absolue= x;
}
else {
/* non : cas où x est strictement plus petit que 0 */
valeur_absolue= -x;
}
Alg. 3.0 : Calcul de la valeur absolue


/* Est-ce que score_A est strictement supérieur à score_B ? */
if (score_A > score_B) {
/* oui : cas où score_A est strictement supérieur à score_B */
printf("L'équipe A a gagné");
}
else {
/* non : cas où score_A est inférieur ou égal à score_B */
printf("L'équipe B a gagné, ou les deux équipes sont à égalité");
}
Alg. 3.0 : Affichage d'un résultat


/* Est-ce que score_A est strictement supérieur à score_B ? */
if (score_A>score_B) {
/* oui : cas où score_A est strictement supérieur à score_B */
printf("L'équipe A a gagné");
}
else {
/* non : cas où score_A est inférieur ou égal à score_B */
if (score_A==score_B) {
/* cas où les 2 score sont égaux */
printf("les deux équipes sont à égalité");
}
else {
/* Si les deux scores sont différents, comme score_A est inférieur ou égal */
/* à score_B, c'est que score_A est strictement plus petit que score_B */
printf("L'équipe B a gagné");
}
}
Alg. 3.0 : Affichage d'un résultat (amélioré)



3.2.2 if sans la partie else




La partie else est optionnelle. Cela signifie qu'on n'est pas obligé de la mettre. Par exemple :

/* rappel: la fonction scanf rend comme résultat le nombre de variables lues ! */
if (scanf("%d\n",&i)){
printf("On a lu l'entier %d.\n",i);
}
Alg. 3.0 : Lecture d'un entier


/* On teste si la variable resultat_trouve, un entier, est égale à 0 ou à 1 */
if (resultat_trouve){
printf("Le résultat a été trouvé !\n");
}
Alg. 3.0 : Test d'une condition




3.3 Traitement par cas, structure switch ...case





Le traitement par cas est plus limité que le traitement par une suite de if ...else , car on pose une seule question, et il n'y a qu'un nombre fixé de réponses possibles. Les valeurs de ces réponses possibles doivent être connues lors de l'écriture du programme.

/* Affichage de l'en-tête d'une lettre */
switch (i){
case 1 :
printf("un\n");
break ;
case 2 :
printf("deux\n");
break ;
case 3 :
printf("trois\n");
break ;
case 4 :
printf("quatre\n");
break ;
default:
printf("le nombre n'est pas entre 1 et 4");
break ;
}/* fin du switch */
Alg. 3.0 : Utilisation de switch ...case


break signifie qu'il ne faut pas regarder les résultats suivants. Par exemple, on peut aussi écrire :

/* Affichage de l'en-tête d'une lettre */
switch (i){
case 1 :
printf("un\n");
break ;
case 2 :
printf("deux\n");
break ;
case 3 :
printf("trois\n");
break ;
case 4 :
printf("quatre\n");
break ;
default:
printf("le nombre n'est pas entre 1 et 4");
break ;
}/* fin du switch */
Alg. 3.0 : break et conditions imbriquées


Cette nouvelle formulation fait exactement la même chose que la première. Maintenant, si on enlève les break  :

/* Affichage de l'en-tête d'une lettre */
switch (i){
case 1 :
printf("un\n");
case 2 :
printf("deux\n");
case 3 :
printf("trois\n");
case 4 :
printf("quatre\n");
default:
printf("le nombre n'est pas entre 1 et 4");
}/* fin du switch */
Alg. 3.0 : Cas où on veut pouvoir faire plusieurs actions


Le résultat, si i=3, sera :

  trois  
  quatre  
  le nombre n'est pas entre 1 et 4  

ce qui n'est pas forcément ce qu'on voulait...

Chapitre 4 : Fonctions



Mots-clefs : fonctions en C, arguments, valeur de retour, variable locale, en-tête.




4.1 Introduction





On a vu pour l'instant des programmes qui se réduisaient à la fonction main. Ces programmes effectuent des calculs, et retournent, lorsque tout s'est bien passé, la valeur entière 0. En C, comme dans la plupart des langages de programmation, on peut définir des fonctions. Les fonctions sont des petits programmes qui coopèrent pour arriver à un résultat. Chaque fonction doit exécuter une tâche bien précise, et le programmeur les relie entre elles pour obtenir un programme complet.

Pour relier des fonctions entre elles, il faut qu'elles puissent communiquer entre elles. Cette année, on ne verra qu'un seul mode de communication, l'échange de données. Dans ce cadre, une fonction se comporte comme une boite noire (voir Figure 4.1).




Figure 4.1: Schéma d'utilisation de fonctions


On donne à la fonction des entrées, ou paramètres, pour effectuer le calcul. En sortie, la fonction rend un résultat.

Chaque fonction doit être déclarée avant d'être utilisée, donc toutes fonctions sont déclarées avant la fonction main. On a déjà commencé à utiliser des fonctions qui étaient déjà définies. Par exemple :
  1. printf("Bonjour tout le monde !\n");
  2. scanf("%d",&i);
Ces fonctions sont définies dans le fichier stdio.h1. Et on a aussi déjà défini des fonctions, comme la fonction main. On va voir dans ce chapitre comment définir de nouvelles fonctions.

L'utilité des fonctions est évidente :
  1. vous n'avez pas besoin de savoir comment marchent printf() et scanf. Il suffit de savoir comment les utiliser ;
  2. en plus, on peut utiliser plusieurs fois printf() et scanf dans le programme, sans avoir besoin de dire, à chaque fois, comment faire pour écrire une chaîne de caractères, ou comment lire une donnée ;
  3. c'est plus simple et plus lisible d'écrire printf("bonjour !\n") que d'écrire ce que fait la fonction printf() : la définition de cette fonction prend une dizaine de pages, et utilise d'autres fonctions ;
  4. lors de l'écriture d'un programme, les fonctions permettent de tester le programme sans devoir l'écrire entièrement d'abord (voir TD/TP).


4.2 Déclaration des fonctions






4.2.1 Exemple canonique




On a bien sûr, la classique déclaration d'une fonction main :

int
main(int argc,char * argv[]){
/* fonction principale */
printf("Bonjour !% \% n");
return 0;
}
Alg. 4.0 : Une fonction main


Dans cette déclaration, il y a un en-tête (en dehors des accolades), un corps (dans les accolades), et des variables. Toutes les fonctions, en C, s'écrivent sur le même modèle. On va voir comment faire sur l'exemple d'une fonction plus qui calcule la somme de deux nombres.


4.2.2 En-tête d'une fonction




L'en-tête d'une fonction définit comment la fonction peut être utilisée. Il ne dit rien sur la valeur du résultat, ni sur la manière par laquelle le résultat est calculé. Il définit juste le profil de la fonction, c'est-à-dire : Par exemple, pour une fonction plus de profil :

plus : int ×int int

l'en-tête sera :

int
plus(int a,int b)
Alg. 4.0 : En-tête de la fonction plus



4.2.3 Corps d'une fonction




Le corps d'une fonction détermine comment le résultat est calculé. Tout d'abord, le résultat d'une fonction est la valeur qui suit le premier return exécuté. Par exemple, depuis le début du cours, la fonction main a toujours eu le résultat 0.

Il est possible de déclarer de nouvelles variables dans le corps d'une fonction. Ces nouvelles variables n'ont pas de valeur par défaut définie. Par exemple :

int
plus(int a,int b){
int resultat;
...
return resultat;
}
Alg. 4.0 : Déclaration d'une variable resultat dans la fonction plus


Dans le corps de la fonction, on peut aussi utiliser les variables données en argument (dans l'exemple, il s'agit de a et de b.) Ces variables ont des valeurs qui sont nouvelles à chaque fois qu'on utilise la fonction plus. Par exemple : On obtient finalement la fonction plus :

int
plus(int a,int b){
int resultat;
resultat = a+b;
return resultat;
}
Alg. 4.0 : Exemple complet de la fonction plus


Il est possible d'utiliser cela dans le langage C. On peut commencer par déclarer l'en-tête de la fonction (type du résultat et des arguments, nom) avant de préciser comment la calculer (déclaration du corps de la fonction). On verra au chapitre sur la compilation comment utiliser cela pour la création de programmes.


4.2.4 Exemples




int
plus(int a,int b){
/* fonction plus */
return (a+b);
}
Alg. 4.0 : Fonction plus sans variables locales


On peut aussi définir des fonctions spécialisées à partir de fonctions plus générales :
int
affiche_float(float nombre){
/* cette fonction affiche un nombre à virgule avec toujours */
/* deux chiffres après la virgule */
return printf("%.2f",nombre);
}
Alg. 4.0 : Spécialisation de la fonction printf




4.3 Communication entre les fonctions








4.3.1 Arguments et valeur de retour




Le meilleur moyen de faire communiquer deux fonctions est d'utiliser des arguments et un résultat. On donne des valeurs aux arguments de la fonction, et la fonction retourne un résultat.




Figure 4.2: Communication entre la fonction main et la fonction printf


Pour voir exactement comment marche cette communication, il faut se souvenir que la mémoire est comme un gigantesque placard avec plein de cases. Les fonctions main et plus utilisent des variables qui désignent des cases qui sont séparées les unes des autres :


Regardons ce qui se passe dans le cas suivant :

int
plus(int a,int b){
int c;
c = a+b;
return c;
}
int
main(int argc,char *argv[]){
int a;
a=8;
a=plus(a+1,a)-a;
printf("a vaut %d% \% n",a);
return 0;
}
Alg. 4.0 : Fonctions main et plus


On commence toujours l'exécution par la fonction main. On commence par mettre 8 dans la case désignée par la variable a de la fonction main :


Ensuite, on calcule les valeurs des arguments de la fonction plus. La premier vaut 9 (a+1), et le second vaut 8 (a). On passe ensuite à l'exécution de la fonction plus avec ces valeurs. L'état de la mémoire est alors :


Dans la fonction plus, on effectue ensuite l'assignation c=a+b;. L'état de la mémoire est alors :


La valeur rendue par : return c; est donc 17. On revient à la fonction main. Dans l'assignation :
a=plus(a+1,a)-a;

on remplace plus(a+1,a) par 17. La valeur de a, pour la fonction main, est toujours 8. Donc l'expression à droite du signe = vaut 17-8=9. On peut alors changer la valeur dans la case a :



4.3.2 Cas de la fonction scanf




D'après ce qui vient d'être dit, les fonctions ne modifient pas les valeurs des arguments. Cela semble un peu bizarre si on pense à la fonction scanf, qui change le contenu d'une variable. Pourtant, ce cas n'est pas une exception si on se souvient de ce que signifie le & !

On se souvient que si i est une variable de type int , i est en fait une case qui contient des 0 et des 1 interprétés comme des entiers. Pour la fonction scanf, on ne donne pas, comme argument, la valeur contenue dans la case i, qui vaut le contenu de ce morceau de mémoire, mais l'adresse &i de cette case. Cette adresse n'est pas modifiée, mais peut être utilisée pour modifier le contenu de la case.

int i; /* on choisit une case pour contenir les valeurs de i */
scanf("%d",&i);
/* l'adresse (5) de la case de i est stockée par une variable a de la fonction scanf */
/* Ensuite, connaissant l'adresse où écrire, la fonction scanf écrit dans la case i */
/* Enfin, la fonction scanf termine, et on continue la suite du programme, avec la bonne valeur pour i */
printf("La valeur lue est %d% \% n",i);
...
Alg. 4.0 : Lecture d'un entier dans la variable i




Il est très simple de faire un programme incompréhensible en utilisant les adresses. Il convient donc de les utiliser avec précaution. Par exemple, essayez de deviner ce que fait le programme de l'Exemple  (avant de le taper et de l'exécuter).





#include <stdio.h>
int main() {
int j;
int i;
j=17;
i=(int )&i+4;
scanf("%d",i);
printf("i=%d, j=%d\n",i,j);
return 0;
}
Alg. 4.0 : Confusion, mais il n'y a pas d'erreurs...




4.4 Variables locales à une fonction





Les variables déclarées dans le corps d'une fonction ne sont utilisables que dans le corps de cette fonction. Donc les variables déclarées dans le corps de la fonction main et dans le corps de la fonction fois suivante sont différentes, même si elles ont le même nom. Elles sont différentes parce qu'elles désignent des cases (dans la mémoire) différentes (voir Exemple ).

int
fois(int a,int b){
/* fonction fois */
int resultat;
resultat = a*b;
return resultat;
}
Alg. 4.0 : Une fonction fois


Dans cet exemple, on utilise trois variables :
  1. a et b, qui sont des arguments de la fonction ;
  2. resultat, qui est déclaré à l'intérieur de la fonction fois.
Dans l'introduction, on a dit que les fonctions se comportaient comme des boites noires. Cela signifie que ces trois variables n'ont rien à voir avec d'autres variables déclarées dans d'autres fonctions. Par exemple, on peut déclarer deux variables de type int a et b dans la fonction main, et faire :

a = 3;
b = 4;
a = fois(b,5);
Dans ce cas, on calcule fois(4,5) (on remplace les arguments par leur valeur). à l'intérieur de la fonction fois, on commence par remplacer les arguments par leur valeur (on remplace a par 4 et b par 5, puis on fait le calcul. à ce moment, les variables a et b de la fonction main continuent de valoir 3 et 4 ! Le remplacement fait à l'intérieur de la fonction ne change rien à l'extérieur.

La dernière instruction est :

return resultat;
donc la fonction rend la valeur 20. On peut alors finir la dernière assignation de la fonction main. à droite, la valeur calculée est 20, donc on change à 20 la valeur de a. Finalement, les variables a et b de la fonction main valent 20 et 4.



4.5 Exemples de fonctions





char
LireCaractere (){
char c,reste;
/* On lit un caractère */
scanf("%c",&c);
/* On lit ensuite tous les caractères jusqu'à la fin de la ligne */
do {
scanf("%c",&reste);
}while (reste != '\n');
return c;
}
Alg. 4.0 : fonction lisant un caractère


int
LireEntierPositif (){
char reste;
int entier, retour;
/* On lit un entier */
do {
retour=scanf("%d",&entier);
/* On lit ensuite tous les caractères jusqu'à la fin de la ligne */
do {
scanf("%c",&reste);
}while (reste != '\n');
}while ((retour == 0)||(entier <=0));
return entier;
}
Alg. 4.0 : fonction lisant un entier positif


float LireDecimal (){
char reste;
int retour;
float decimal;
do {
/* On lit un nombre decimal */
retour=scanf("%f",&decimal);
/* On lit ensuite tous les caractères jusqu'à la fin de la ligne */
do {
scanf("%c",&reste);
}while (reste != '\n');
/* on recommence en cas d'erreur de lecture */
}while ((retour == 0));
return decimal;
}
Alg. 4.0 : fonction lisant un nombre décimal


float
Maximum(int N){
int i;
float max,entree;
for (i=0;i<N;i++){
/* On lit le i+1ème nombre */
entree= LireDecimal ();
if (i==0) {
/* Si c'est la premier, c'est aussi le plus grand pour l'instant */
max = entree;
}
else {
/* Sinon, si il est plus grand que le maximum des nombres précédents, il devient le nouveau maximum */
if (entree > max){
max= entree;
}
}
}
/* À la fin de la boucle, max contient le plus grand des N nombres lus */
return max;
}
Alg. 4.0 : fonction lisant N nombres flottants et rendant leur maximum


Chapitre 5 : Variables, pointeurs et tableaux



Mots-clefs : pointeur, adresse, *, &, malloc, sizeof.




5.1 Qu'est-ce que la mémoire ?





Un programme, lors de l'exécution, a besoin d'un endroit où stocker des résultats intermédiaires. C'est la mémoire. Cette mémoire est organisée en cases, et chaque case correspond a un résultat qu'il est possible de stocker. Dans certains langages, ces cases sont réservées et libérées au fur et à mesure des besoins du programme. La difficulté, en C, est que l'une des tâches du programmeur est de définir explicitement les emplacements en mémoire que le programme utilisera.


5.1.1 Description logique




Pour simplifier, on peut se représenter la mémoire d'un ordinateur comme une armoire. Les cases correspondent à des tiroirs, et ces cases sont numérotées. Une première difficulté est que toutes les cases n'ont pas la même taille.

Il y a deux longueurs possibles pour les cases : On appelle un bit un chiffre qui ne peut valoir que 0 ou 1.




Figure 5.1: Schéma simplifié de la mémoire



5.1.2 Déclaration des variables




Apparemment, la déclaration d'une variable, par exemple :

int i;


n'a aucun effet. En pratique, lors de la compilation, le compilateur va faire plusieurs choses lorsqu'il verra cette déclaration :

  1. pour commencer, il va réserver une case, dans la mémoire, qui servira à stocker les valeurs de la variable i. Cette case a une taille définie en fonction du type de la variable déclarée (ici, c'est le type int , et il va réserver une grande case). Il se souvient aussi du numéro de la case réservée (par exemple, la case a le numéro 8) ;
  2. ensuite, il va se souvenir qu'il peut trouver la valeur de la variable i en la cherchant dans la case 8 ;
  3. enfin, il va se souvenir que si il doit changer la valeur de la variable i, il doit ranger la nouvelle valeur dans la case 8.
La taille de la case réservée dépend du type :
Regardons ce qu'il se passe lors de l'exécution d'une assignation i=i*2 :

  1. on commence par calculer l'expression à droite du signe =. Pour cela,
    1. l'ordinateur prend la valeur de i dans la case qui lui a été indiquée par le compilateur (case 8). Par exemple, la case 8 contient des bits qui représentent le nombre 17 ;
    2. il multiplie le nombre obtenu (17) par deux, et obtient une nouvelle valeur (34).
    Maintenant, le calcul de l'expression à droite du signe = est terminée ;
  2. ensuite, l'ordinateur recherche la case dans laquelle il doit ranger ce résultat. Le compilateur lui a indiqué que la variable i correspond à la case 8, donc l'ordinateur range la valeur 34 dans la case 8.
Maintenant, la prochaine fois que la valeur de la case i sera utilisée, l'ordinateur trouvera la valeur 34.
Alg. 5.0 : Déroulement d'une assignation


On peut retenir qu'à gauche du signe =, i représente l'adresse de la case où est stocké la valeur de la variable i, et à droite du signe =, i représente la valeur de la variable i.



5.2 Les pointeurs





Pour l'instant, la mémoire ne pose pas de problèmes. Tant qu'on utilise des variables simples, les cases en mémoire ont été réservées, et il n'y a pas d'erreurs dues au programmeur.

En C, en plus des variables simples, il y a des variables dont la valeur est l'adresse d'une case en mémoire. On appelle ces variables, qui contiennent des adresses, des pointeurs. Toute la difficulté, dans l'utilisation des pointeurs, vient du fait qu'un pointeur peut contenir n'importe quelle adresse, y compris des adresses non valides, ou mal alignées.

Ces erreurs sont souvent difficiles à retrouver, car elles ne se produisent pas toujours : des fois, le programme marche bien, des fois, il rend un mauvais résultat, et des fois, il s'arrête sans rien rendre du tout. Il faudra donc bien faire attention, lors de l'écriture d'un programme, à l'utilisation des pointeurs, afin de ne pas perdre beaucoup de temps, ensuite, à retrouver les erreurs commises.


5.2.1 Syntaxe




Les pointeurs sont déclarés en même temps que les autres variables. Par exemple, les déclarations suivantes :

char * pchar;
int * pentier;
void * pVoid;


définissent des pointeurs sur un caractère et un entier. pVoid est un pointeur sur une case qui n'a pas de type spécial. Il n'est donc pas possible d'utiliser un pointeur de type void * tel quel, car on ne sait pas si la case désignée est une petite ou une grande case. De plus, si c'est une grande case, on ne sait pas si le nombre désigné est un entier ou un nombre décimal.



Les entiers et les nombres décimaux sont représentés de deux manières totalement différentes. Par exemple, les 32 bits qui représente 1 dans le type int n'ont rien à voir avec les 32 bits qui représentent 1 dans le type float .





Les pointeurs sont des variables qui sont stockées dans des cases de 32 bits. On peut obtenir la valeur de la case désignée par le pointeur en utilisant *. Par exemple :




Figure 5.2: Retrouver la valeur d'une case désignée par un pointeur



5.2.2 Obtenir des adresses valides




Il y a deux moyens d'obtenir une adresse valide. Le premier est de reprendre l'adresse d'une variable déjà définie. Cette adresse est valide car elle correspond à un morceau de mémoire qui a été réservé au moment de la compilation. On peut obtenir l'adresse de la variable v en utilisant &v.



retour sur scanf : Quelle que soit la variable x, & x est l'adresse de cette variable. Donc la fonction scanf ne modifie pas ses arguments (l'adresse des variables lues). Elle modifie les emplacements désignés par ces adresses, et donc les variables !





Le second moyen d'obtenir une adresse valide est de réserver un nouvel emplacement en mémoire. On peut réserver de la place en utilisant les fonctions malloc et sizeof. La fonction sizeof permet de trouver la place occupée en fonction du type donné en argument. La fonction malloc est de type :

void * malloc(int taille);

Donc il faut changer le type du pointeur avant de faire l'affectation. Par exemple, si on veut réserver un nouvel emplacement pour un caractère, on peut faire :

char *c;
c=(char *)malloc(sizeof(char ));


Pour comprendre comment fonctionne cette instruction, il faut la décomposer. On se souvient qu'en C, on évalue d'abord les arguments d'une fonction avant d'appeler cette fonction.
  1. sizeof(char ) permet de trouver la place que doit occuper une variable de type char ;
  2. malloc(sizeof(char )) va réserver de la place pour une variable de type char. La valeur que rend la fonction malloc est l'adresse de la case créée ;
  3. (char *)malloc(sizeof(char )) permet de préciser que la zone réservée est une zone contenant des caractères ;
  4. c=(char *)malloc(sizeof(char )) permet de donner comme valeur à la variable c l'adresse de la zone qu'on vient de réserver.
Après cette instruction, le pointeur c aura pour valeur une adresse valide pouvant contenir une valeur de type char .



5.3 Les tableaux






5.3.1 Qu'est-ce qu'un tableau ?




Un tableau, c'est une partie de la mémoire pouvant contenir plusieurs valeurs de même type. Cette partie de la mémoire est définie par :


5.3.2 Déclaration




On peut déclarer les tableaux de deux manières différentes, de manière statique ou dynamique.

5.3.2.1 Cas statique.



C'est le cas où le programmeur connaît, au moment de l'écriture du programme, combien de cases seront utilisées. Dans ce cas, les tableaux sont déclarés en même temps que les variables et les pointeurs. Pour déclarer un tableau notes pouvant contenir 30 nombres décimaux, on écrit :
float notes[30];


Les cases disponibles d'un tableau de N cases sont numérotées de 0 à N-1





Ensuite, on accède à la 17-ème note en utilisant notes[16]. notes[16] se comporte exactement comme une variable.

5.3.2.2 Cas dynamique.



Lorsque le programmeur ne sait pas, au moment d'écrire le programme, combien de cases devra contenir le tableau, il faut utiliser un pointeur ! Pour reprendre le même exemple, si on ne connaît pas le nombre d'élèves, il faut déclarer :
float * notes;
Il faut faire attention, car pour l'instant, aucune place n'a été réservée en mémoire. Donc on ne peut pas utiliser le tableau tout de suite. Il faut commencer par réserver de la place. Pour cela, on utilise encore la fonction malloc. Cette fois ci, au lieu de réserver de la place pour un float , on va lui demander de réserver de la place pour 30 valeurs float , c'est-à-dire 30 fois la place d'une valeur de type float :

notes = (float *)malloc(30*sizeof(float ));


On peut ensuite utiliser notes comme un tableau normal. Par exemple, on accède à la 17-ème valeur de ce tableau en utilisant notes[16].



différence entre t[] et *p : On peut changer l'adresse désignée par un pointeur, mais l'adresse désignée par un tableau est constante. Elle doit être fixée à la compilation du programme.






5.3.3 Exemples d'utilisation




  1. faire une fonction qui prend en entrée un entier N, et qui rend un tableau de N nombres flottants demandés à l'utilisateur. (On peut utiliser la fonction LireDecimal : Ø |® float )
  2. faire une fonction qui prend en entrée un tableau d'entiers de taille 20, et qui rend le maximum de ce tableau
  3. faire une fonction qui prend en entrée un tableau de flottants de taille inconnue, et qui rend le maximum de ce tableau. Que manque-t'il ? (taille du tableau)
  4. faire une fonction qui rend le produit scalaire de 2 vecteurs de taille N.


5.4 Les chaînes de caractères





Une chaîne de caractères est un tableau de caractères (statique ou dynamique) qui se termine par le caractère ' \ 0'. Les bibliothèques standard du langage C contiennent de nombreuses fonctions permettant de lire ou d'écrire des chaînes de caractères. Pour les utiliser, il faut inclure, en même temps que les définitions de stdio.h, les définitions contenues dans string.h :

#include <stdio.h>
#include <string.h>


Parmi ces fonctions, il y a entre autres : On peut lire ou écrire une chaîne de caractères en utilisant les fonctions scanf et printf. Il faut utiliser "%s" pour désigner une chaîne de caractères. La lecture avec scanf peut être facilitée en utilisant "%as" au lieu de "%s". Lorsqu'on fait cela, la place pour la chaîne de caractères est réservée en même temps que la lecture. Ainsi, pour lire un mot, on peut faire :

char *chaine;
scanf("%as",&chaine);




scanf arrête la lecture d'une chaîne de caractères dès qu'un caractère blanc (espace, tabulation ou passage à la ligne) est rencontré.





Chapitre 6 : La compilation



Mots-clefs : pré-processeur, compilation de code objet, édition de lien, module, bibliothèques (libraries), # define, # include.




6.1 Processus détaillé de compilation





La compilation d'un programme avec gcc se fait en plusieurs étapes, dont les principales sont données dans la Figure 6.1.




Figure 6.1: étapes de la compilation.




6.2 Le pré-processeur





C'est une phase durant laquelle le fichier C est changé. Cette phase permet de changer le fichier C au moment de la compilation. Lors de la phase de pré-processing, le fichier C qu'on désire compiler est changer en un autre fichier C suivant des directives qu'on écrit dans le fichier C original. Ces directives permettent d'inclure un fichier, de définir des constantes et bien d'autres choses. Dans le fichier C généré, il n'y a plus ces directives, elles ont été remplacées par leurs effets.

On peut arrêter le processus de compilation après le pré-processeur en utilisant l'option -E.


6.2.1 La directive # define




On peut définir des constantes, qui seront remplacées textuellement lors de la phase de pré-processing. Ces constantes peuvent êtres (presque) n'importe quoi.

Pour éviter de confondre ces constantes avec les instructions C normales, on met ces constantes toutes en majuscules. Par exemple, pour rendre le programme plus lisible, on peut définir des constantes représentant les valeurs vrai et faux :

# define VRAI 1
# define FAUX 0
Alg. 6.0 : Définition de constantes


Partout, ensuite, quand on met VRAI, VRAI sera remplacé par 1. Le remplacement obéit à quelques règles :
  1. il n'y a pas de remplacement à l'intérieur d'une chaîne de caractères ;
  2. la valeur de la constante doit contenir autant de parenthèses ouvrantes que de parenthèses fermantes ;
  3. lors du remplacement, des espaces sont ajoutés autour de la valeur, donc il ne faut pas couper un nom de variable ou de fonction ;
  4. la constante à remplacer doit être bien séparée de ce qui l'entoure.
Mais ces constantes peuvent être n'importe quel morceau de programme C.
# define FORMAT "Le nombre est %d\n"
int
main(){
int i;
i = 5;
printf(FORMAT,i);
return 0;
}
Alg. 6.0 : Utilisation de constantes (avant remplacement)
devient :

int
main(){
int i;
i = 5;
printf("Le nombre est %d\n",i);
return 0;
}
Alg. 6.0 : Utilisation de constantes (après remplacement)


Il est aussi possible de définir des macros. Ce sont des petites fonctions qui sont remplacées textuellement. Par exemple, on peut définir les macros suivantes :

# define EST_POSITIF(x) ( (x) >= 0)
# define NOUVEAU_TABLEAU_INT(n) (int *)malloc(sizeof(int )*(n))
Alg. 6.0 : Définitions de macros


Il faut faire attention, car le remplacement de la variable peut donner des résultats faux. Par exemple :

# define NOUVEAU_TABLEAU_INT(n) (int *)malloc(sizeof(int )* n)
...
t = NOUVEAU_TABLEAU_INT( 5 + 3 );
Alg. 6.0 : Mauvaise utilisation de macros


devient :
# define NOUVEAU_TABLEAU_INT(n) (int *)malloc(sizeof(int )* n)
...
t = (int *)malloc(sizeof(int )* 5 + 3 );
Alg. 6.0 : Mauvaise utilisation de macros (suite)


Ce qui n'est pas ce qu'on veut ! Il faut donc ne pas hésiter à mettre des parenthèses pour bien isolé la macro et ses arguments.


6.2.2 La directive #include




La directive #include permet d'inclure des fichiers dans le fichier C courant. On peut spécifier ces fichiers de deux manières différentes :

  1. #include <stdio.h>
  2. #include "Cours.h"
Dans le premier cas, on recherche le fichier stdio.h dans des répertoires où sont les fichiers du système. On peut ajouter des répertoires à la liste des répertoires recherchés en utilisant l'option -I de gcc.

Dans le deuxième cas, on cherche le fichier Cours.h dans le répertoire courant. Cela permet de créer soi-même les fichiers qu'on désire inclure.


6.2.3 Les fichiers .h




Lorsqu'on définit une fonction, en C, on défini deux choses :
  1. on définit la manière de calculer la fonction. Par exemple, on peut définir une manière de demander un entier positif à l'utilisateur ;
  2. on définit la manière d'utiliser la fonction. Pour reprendre l'exemple de la fonction LireEntierPositif, on définit cette fonction comme ne prenant pas de paramètres, et comme rendant une valeur de type int .
Le point intéressant est que pour savoir comment utiliser une fonction, il n'est pas nécessaire de savoir comment le calcul est fait. Tant qu'on ne créé pas d'application, le compilateur a juste besoin de connaître la manière d'utiliser la fonction.

Les fichiers .h sont des fichiers qui ne contiennent que des en-têtes de fonctions, c'est-à-dire la manière d'utiliser ces fonctions. Par exemple, un fichier Cours.h peut contenir :

int LireEntierPositif();
char LireCaractere();
char LireNombreDecimal();
Alg. 6.0 : Exemple de fichier .h


En incluant ce fichier, le compilateur saura comment utiliser les fonctions LireEntierPositif LireCaractere et LireNombreDecimal, mais ne saura pas comment les calculer.



6.3 Création de code objet





Le code objet, c'est une suite d'instructions exécutables directement par le processeur. Lors de la phase de création de code objet, il n'est pas nécessaire de relier les noms des fonctions utilisées au code qui permet de réaliser ces fonctions. Cela permet de compiler des fonctions d'un côté, afin d'avoir un fichier contenant les manières de calculer des fonctions. On appelle ces fichiers des bibliothèques ou des modules. La différence est qu'une bibliothèque est construite en rassemblant plusieurs modules.

Les fichiers obtenus se terminent par .o. L'intérêt est de pouvoir compiler des fonctions utilisées dans plusieurs programmes une fois et une seule. à chaque fois qu'on voudra les utiliser, il faudra juste préciser comment les utiliser. On parle de compilation séparée, car on compile des fonctions indépendamment les unes des autres.

Les bibliothèques sont aussi des fichiers contenant les codes objets de fonctions. Quand on compile un fichier en s'arrêtant à la génération de code objet, on construit en fait une mini-bibliothèque.

Pour arrêter la compilation à la création du code objet, il faut utiliser l'option -c de gcc.



6.4 édition de liens





La dernière phase de la compilation consiste à relier les manières de calculer des fonctions, contenues dans des fichiers .o et les bibliothèques. On donne les fichiers contenant le code objet en même temps que le fichier contenant le programme à compiler. Le compilateur gcc se charge ensuite de relier les définitions de fonctions aux manières de calculer contenues dans les fichiers objets.

C'est au dernier moment que gcc vérifie qu'il y a une fonction main qui a été définie. Le programme produit commence par cette fonction main.



6.5 Exemple complet





avec Cours.c, Cours.h, Utilisation.c.

#include "Cours.h"
int
LireEntierPositif () {
int entier_lu, retour_scanf;
char caractere;
do {
retour_scanf = scanf("%d",&entier_lu);
do {
scanf("%c",&caractere);
}while (c != '\n');
}while ((retour)&&NEGATIF(entier_lu)) ;
return entier_lu;
}
Alg. 6.0 : Exemple de compilation séparée --- fichier Cours.c


#include <stdio.h>
# define VRAI 1
# define FAUX 0
# define POSITIF(n) ( (n) >= 0)
# define NEGATIF(n) ( (n) < 0)
int LireEntierPositif();
Alg. 6.0 : Exemple de compilation séparée --- fichier Cours.h


#include "Cours.h"
int
main() {
int i;
i = LireEntierPositif();
printf("L'entier lu est %d\n",i);
return 0;
}
Alg. 6.0 : Exemple de compilation séparée --- fichier Utilisation.c


étapes:
  1. compilation de Cours.c :
    gcc -c Cours.c
  2. compilation de Utilisation.c
    gcc -o final Utilisation.c Cours.o

*
Yannick.Chevalier@loria.fr, bureau A201
1
Yannick.Chevalier@loria.fr, bureau A201
1
Pour le vérifier, vous pouvez ouvrir le fichier /usr/include/stdio.h en lecture et les rechercher.

This document was translated from LATEX by HEVEA.