Cours de Programmation S4

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 176

Algorithmique II.

Programmation C Avancée.

Pr. Abdessamad JARRAR SMI S4


Algorithmique II : Programmation C Avancée.

Objectifs de ce module

• Maitrise des concepts avancés de


programmation:
– Pointeurs.
– Gestion de Mémoire.
– Chaines de Caractères.
– Fichiers.
– Types Composés.
– Compilation Séparée.
– Directives du Préprocesseur.

Pr. Abdessamad JARRAR SMI S4


Algorithmique II : Programmation C Avancée.

Plan de ce module

• Chapitre 1 : Pointeur et Allocation Dynamique


• Chapitre 2 : Chaines de Caractères
• Chapitre 3 : Fonctions
• Chapitre 4 : Types Composés (Structures, Unions,
Synonymes)
• Chapitre 5 : Fichiers
• Chapitre 6 –Complément- : Compilation séparée,
Directives du préprocesseur
Pr. Abdessamad JARRAR SMI S4
Chapitre 1.

Pointeur et Allocation Dynamique.

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• Toute variable manipulée dans un programme


est stockée quelque part en mémoire centrale.

• La mémoire centrale est constituée d’octets qui


sont identifiés de maniére univoque par un
numéro qu’on appelle adresse.

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• Lorsqu’on déclare une variable var de type T,


l’ordinateur réserve un espace mémoire (de
sizeof(T) octets) pour y stocker les valeurs de
var.
• Pour retrouver cette variable, il suffit donc de
connaître l’adresse du premier octet où elle est
stockée.
UN POINTEUR EST UNE VARIABLE DE TYPE ADRESSE

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• Les pointeurs présentent de nombreux


avantages :
– Ils permettent de manipuler de façon simple des
données de taille importante.
– Les tableaux ne permettent de stocker qu’un
nombre fixé d’éléments de même type. Si les
composantes du tableau sont des pointeurs, il sera
possible de stocker des éléments de tailles diverses.
– Il est possible de créer des structures chaînées qui
sont utilisées pour définir des listes chainées.

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• On déclare un pointeur par l’instruction :


type *nom-du-pointeur ;

• où type est le type de l’objet pointé.

• Exemple :
int *pi; // pi est un pointeur vers un int
short int *psi; // psi est un pointeur vers un short int
char *pc; // pc pointeur vers un char

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• le type d’un pointeur dépend du type de l’objet


pointé.

• A noter aussi l’existence de pointeurs


génériques, c’est à dire capable de pointer vers
n’importe quel type d’objet.
– On utilise pour cela le type void *.

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• Lors du travail avec des pointeurs, nous avons


besoin :
– d’un opérateur ’adresse de’ & pour obtenir l’adresse
d’une variable. L’opérateur & peut seulement être
appliqué à des objets qui se trouvent dans la
mémoire interne, c’est à dire à des variables et des
tableaux. Il ne peut pas être appliqué à des
constantes ou des expressions.
– d’un opérateur ’contenu de’ * pour accéder au
contenu d’une adresse.

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• Dans l’exemple suivant, on définit un pointeur p


qui pointe vers un entier i :
int * p; //étape (1)
int i = 14; //étape (2)
p=&i; //étape (3)

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• L’opérateur * permet d’accéder directement à


la valeur de l’objet pointé.
– La syntaxe est la suivante :
*nom-pointeur

• si p est un pointeur vers un entier i, *p désigne


la valeur de i. Exemple :
int i = 14;
int *p;
p = &i; //p contient l’adresse de i (0x352C)
printf("*p = %d \n",*p); //affiche "*p = 14"

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Exercices.
Petites devinettes : Soient i et j deux pointeurs vers des
entiers.

1. A quoi correspond l’expression *i**j ?


2. Et *i/*j ?

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• Lorsqu’on déclare un pointeur p sur un objet de


type T, on ne sait pas sur quoi il pointe.
• La case mémoire qu’il occupe contient une
certaine valeur qui risque de le faire pointer
vers une zone hasardeuse de la mémoire.
Comme toute variable, un pointeur doit être initialisé !

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• Initialisation peut s’effectuer de trois façons :


1. Affectation à l’adresse d’une autre variable.
– Si la variable est un pointeur, on peut faire
l’affectation directement, sinon on doit utiliser
l’opérateur &.
– Exemple :
int *p1, *p2; //déclaration de 2 pointeurs vers des entiers
int i = 14; //supposons que i se trouve à l’adresse 0x352C
p1 = &i; //affectation à l’adresse de i de p1 , soit 0x352C
p2 = p1; //affectation de p2 à p1:
//p2 contient aussi l’adresse de i

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

2. Affectation de p à la valeur NULL : on peut


dire qu’un pointeur ne pointe sur rien en lui
affectant la valeur NULL.
– Exemple : int * p = NULL;

3. Affectation directe de *p. Pour cela, il faut


d’abord réserver à *p un espace-mémoire de
taille adéquate. L’adresse de cet espace-mémoire
sera la valeur de p. Cette opération consistant à
réserver un espace-mémoire pour stocker l’objet
pointé s’appelle une allocation dynamique.
Pr. Abdessamad JARRAR SMI S4
Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• Pour bien montrer l’intérêt de l’initialisation de


tout pointeur, reprenons l’exemple précédent
dans lequel on remplace l’affectation p2 = p1
par *p2 = *p1 :
• L’exemple devient:
int * p1, *p2;
int i = 14;
p1 = &i;
*p2 = *p1;

• Que va t il se passer ?
Pr. Abdessamad JARRAR SMI S4
Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

1. p1 est bien initialisé et pointe sur i (*p1=14)


2. mais p2 n’a pas été initialisé :
– *p2 désigne une adresse mémoire a priori
inconnue.
– L’instruction *p2 = *p1 force l’écriture de la valeur
*p1=14 dans la case mémoire pointée par p2 ce
qui pourrait avoir des conséquences désastreuses !
– Cette écriture est en général sanctionné par le
message d’erreur ”Segmentation fault” à
l’exécution.
Pr. Abdessamad JARRAR SMI S4
Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

Les opérations arithmétiques sur les pointeurs:


– L’addition d’un entier à un pointeur → p + i :
• Le résultat est un pointeur de même type que le pointeur
de départ.
– La soustraction d’un entier à un pointeur → p – i :
• Le résultat est un pointeur de même type que le pointeur
de départ.
– La différence de deux pointeurs → p1 – p2 :
• Il faut absolument que les deux pointeurs pointent vers
des objets de même type T. Le résultat est un entier dont
la valeur est égale à (p1 - p2)/sizeof(T).
Pr. Abdessamad JARRAR SMI S4
Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• Exemple :

int i = 2;
int *p1, *p2;
p1 = &i;
p2 = p1 + 1;
printf("Adresse p1 = Ox%lx \t Adresse p2 = 0x%lx\n", (unsigned long)p1,
(unsigned long)p2);
printf("Diff´erence des adresses: %lu \t sizeof(int)=%u\n", (unsigned
long)p2-(unsigned long)p1, sizeof(int));
printf("MAIS p2-p1 = %i !\n",p2-p1);

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Pointeurs

• Ce programme renverra :
Adresse p1 = Oxbffffb24 Adresse p2 = 0xbffffb28
Différence des adresses: 4 sizeof(int)=4
MAIS p2-p1 = 1 !

• On peut faire la même expérience avec le type


double. On obtiendrait alors :
Adresse p1 = Oxbffffb20 Adresse p2 = 0xbffffb28
Différence des adresses: 8 sizeof(double)=8
MAIS p2-p1 = 1 !

• A noter qu’on peut également utiliser les


opérateurs ++ et -- avec des pointeurs.
Pr. Abdessamad JARRAR SMI S4
Chapitre 1 : Pointeur et Allocation Dynamique.

Allocation dynamique de mémoire

• Nous avons vu que l’utilisation de pointeurs


permet de mémoriser économiquement des
données de différentes grandeurs. Pour
permettre une utilisation efficace de la
mémoire, il est primordial de disposer de
moyens pour réserver et libérer de la mémoire
dynamiquement au fur et à mesure de
l’exécution.
– C’est ce qu’on appelle une allocation dynamique
de la mémoire.
Pr. Abdessamad JARRAR SMI S4
Chapitre 1 : Pointeur et Allocation Dynamique.

Allocation dynamique de mémoire

• Les fonctions pour la gestion dynamique de la


mémoire sont déclarées dans le fichier stdlib.h.
• L’opération consistant à réserver un espace-
mémoire est réalisé par la fonction malloc.
– Sa syntaxe est :
malloc(nb_octets)

• Cette fonction retourne un pointeur de type


char * pointant vers une zone mémoire de
nb_octets octets.
Pr. Abdessamad JARRAR SMI S4
Chapitre 1 : Pointeur et Allocation Dynamique.

Allocation dynamique de mémoire

• Pour initialiser des pointeurs vers des objets qui


ne sont pas de type char, il faut convertir le
type de la sortie de la fonction malloc à l’aide
d’un cast.
• A noter enfin qu’en pratique, on utilise la
fonction sizeof() pour déterminer la valeur
nb_octets. Ainsi, pour initialiser un pointeur
vers un entier, on écrit :
p = (int*)malloc(sizeof(int));

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Allocation dynamique de mémoire

• Il est primordial de bien comprendre qu’avant


l’allocation dynamique (et plus généralement
avant toute initialisation de p), *p n’a aucun
sens !
• En particulier, toute manipulation de la variable
*p générerait en général une violation
mémoire, détectable à l’exécution par le
message d’erreur « Segmentation fault ».

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Allocation dynamique de mémoire

• La fonction malloc permet également d’allouer


un espace pour plusieurs objets contigus en
mémoire.
– Exemple:
int *p;
p = (int*)malloc(2 * sizeof(int)); //allocation pour 2 int
*p = 14;
*(p + 1) = 10;
printf("p = Ox%lx \t *p = %i \t p+1 = 0x%lx \t *(p+1)=%i\n",
(unsigned long)p, *p, (unsigned long)(p+1), *(p+1));

– Resultat: p = Ox8049718 *p = 14 p+1 = 0x804971c *(p+1)=10

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Allocation dynamique de mémoire

• La fonction calloc a le même rôle que la


fonction malloc mais elle permet de réserver
nb_objets objets de nb_octets octets et de les
initialiser à zéro.
– Sa syntaxe est : calloc(nb_objets,nb_octets)

• Ainsi, si p est de type int*, l’instruction :


p = (int*)calloc(N,sizeof(int));

• Est équivalente à : p = (int*)malloc(N * sizeof(int));


for (i = 0; i < N; i++)
*(p + i) = 0;

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Allocation dynamique de mémoire

• La fonction : realloc (pointeur , taille )

permet de modifier la taille d’une zone préalablement


allouée (par malloc, calloc ou realloc).
• Le pointeur doit être l’adresse de début de la
zone dont on veut modifier la taille. Quant à
taille, elle représente la nouvelle taille
souhaitée. Cette fonction restitue l’adresse de
la nouvelle zone ou un pointeur nul dans le cas
où l’allocation a échoué.

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Allocation dynamique de mémoire

• Lorsque la nouvelle taille demandée est


supérieure à l’ancienne, le contenu de
l’ancienne zone est conservé (quitte à le
recopier si la nouvelle adresse est différente de
l’ancienne).

• Dans le cas où la nouvelle taille est inférieure à


l’ancienne, le début de l’ancienne zone (c’est-à-
dire taille octets) verra son contenu inchangé
Pr. Abdessamad JARRAR SMI S4
Chapitre 1 : Pointeur et Allocation Dynamique.

Allocation dynamique de mémoire

• En C, il n’est pas possible de déclarer un tableau


dont le nombre d’éléments n’est pas connu lors
de la compilation.

• En revanche, les possibilités de gestion


dynamique du langage C nous permettent
d’envisager d’allouer des emplacements aux
différents éléments du tableau au fur et à
mesure des besoins.
Pr. Abdessamad JARRAR SMI S4
Chapitre 1 : Pointeur et Allocation Dynamique.

Allocation dynamique de mémoire

• Exemple:
int *T;
int N, i;
printf("Veuillez saisir le nombre d'éléments du tableau :");
scanf("%d",&N);
T = (int*)calloc(N,sizeof(int));
for(i=0;i<N;i++)
printf("T[%d]=%d \n",i,T[i]);

• NB: Il est possible aussi d’utiliser *(T+i) au lieu


de T[i]. C’est-à-dire, pour afficher les éléments
du tableau on utilise: printf( "T[%d]=%d \n", i, *(T+i) );

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Allocation dynamique de mémoire


• Lorsque l’on n’a plus besoin de l’espace-
mémoire alloué dynamiquement, il faut libérer
ce bloc de mémoire.
• Ceci se fait à l’aide de l’instruction free qui a
pour syntaxe : free(nom-du-pointeur);

• Cette instruction libère le bloc de mémoire


désigné par nom-du-pointeur mais n’a pas
d’effet si le pointeur a la valeur NULL.
• A toute allocation doit être associée une
instruction de type free.
Pr. Abdessamad JARRAR SMI S4
Chapitre 1 : Pointeur et Allocation Dynamique.

Allocation dynamique de mémoire

• Attention :
• La fonction free peut aboutir à un désastre si on essaie
de libérer de la mémoire qui n’a pas été allouée par
malloc ou calloc.
• free ne change pas le contenu du pointeur.
• Si la mémoire n’est pas libérée à l’aide de free, alors elle
l’est automatiquement à la fin du programme.
Cependant, cela ne doit pas dispenser de l’utilisation de
cette fonction. Si la mémoire n’est pas libérée à chaque
suppression d’un élément de la liste, on aboutira
rapidement à une violation de mémoire et au
traditionnel message d’erreur ”Segmentation fault”.

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Exercices.
Exercice 1 : Ecrire un programme en C qui permet de
d’inverser un tableau d’entiers de taille N (choisit par
l’utilisateur) en utilisant les pointeurs.

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Exercices.
Solution :

int *T;
int N, i, aux; for(i=0;i<N/2;i++)
printf("Veuillez saisir le nombre {
d'éléments du tableau :"); aux = *(T+i);
scanf("%d",&N); *(T+i) = *(T+N-i-1);
T = (int*)malloc(N*sizeof(int)); *(T+N-i-1) = aux;
for(i=0;i<N;i++) }
{
printf("T[%d]=",i); for(i=0;i<N;i++)
scanf("%d",T+i); printf("T[%d]=%d \n",i,T[i]);
}

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Exercices.
Exercice 2 : Ecrire un programme en C qui permet de remplir
un tableau de trois élément de différents types (Le premier
est un entier, le deuxième est un réel et le troisième est de
type char).

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Exercices.
Solution :

void** T; printf("T[2] = %c\n",*((char*)(T+2)));


int i, N=3;
T = (void**)malloc(N*sizeof(void*));
*T = (int*)malloc(sizeof(int));
*(T+1) = (float*)malloc(sizeof(float));
*(T+2) = (char*)malloc(sizeof(char));
*((int*)T) = 5;
*((float*)(T+1)) = 1.5;
*((char*)(T+2)) = 'a';
printf("T[0] = %d\n",*((int*)T));
printf("T[1] = %f\n",*((float*)(T+1)));

Pr. Abdessamad JARRAR SMI S4


Chapitre 1 : Pointeur et Allocation Dynamique.

Fin de ce chapitre

Avez – vous des questions ?

Pr. Abdessamad JARRAR SMI S4


Chapitre 2.

Chaines de Caractères.

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Introduction
• Dans un programme informatique, les chaînes de
caractères servent à stocker les informations non
numériques comme par exemple une liste de nom de
personne ou des adresses.

• Il n'existe pas de type spécial chaîne ou string en C. Une


chaîne de caractères est traitée comme un tableau à une
dimension de caractères (vecteur de caractères). Il existe
quand même des notations particulières et une bonne
quantité de fonctions spéciales pour le traitement de
tableaux de caractères.

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Déclaration d’une chaîne


• Une chaîne de caractères est un tableau de type char. La
déclaration est identique à un tableau normal:
char <nom_chaine> [<dimension>]

• La représentation interne d'une chaîne de caractères est


terminée par le symbole '\0' (NULL). Ainsi, pour un texte
de n caractères, nous devons prévoir n+1 octets.
• Malheureusement, le compilateur C ne contrôle pas si
nous avons réservé un octet pour le symbole de fin de
chaîne; l'erreur se fera seulement remarquer lors de
l'exécution du programme ...

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Initialiser une chaîne de caractères


• En général, les tableaux sont initialisés par l'indication de
la liste des éléments du tableau entre accolades:
char MACHAINE[ ] = {'H','e','l','l','o','\0'};

• Pour le cas spécial des tableaux de caractères, nous


pouvons utiliser une initialisation plus confortable en
indiquant simplement une chaîne de caractères constante:
char MACHAINE[ ] = "Hello";

• Lors de l'initialisation par [ ], l'ordinateur réserve


automatiquement le nombre d'octets nécessaires pour la
chaîne. Nous pouvons aussi indiquer explicitement le
nombre d'octets à réserver, si celui-ci est supérieur ou égal
à la longueur de la chaîne d'initialisation.
Pr. Abdessamad JARRAR SMI S4
Chapitre 2 : Chaines de Caractères.

Exemples d’initialisation

char MACHAINE[ ] = “Hello”;


char MACHAINE[6] = “Hello”;
char MACHAINE[ ] = {‘H’,’e’,’l’,’l’,’o’,’\0’};
char MACHAINE[8] = “Hello”;
// on peut utiliser les pointeurs
Char *MACHAINE;
MACHINE = “Hello”;
// par contre:
char MACHAINE[5] = “Hello”; //donnera une erreur à l’exécution
char MACHAINE[4] = “Hello”; //donnera une erreur à la compilation.

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Chaînes constantes et caractères

• Attention à la déclaration:
– Pour la mémorisation de la chaîne de caractères "Hello", C a
besoin de six (!!) octets.
– 'x’ est un caractère constant, qui a une valeur numérique:
• Par exemple: 'x' à la valeur 120 dans le code ASCII.
– "x” est un tableau de caractères qui contient deux caractères: la
lettre 'x' et le caractère NUL: '\0'
– 'x’ est codé dans un octet
– "x” est codé dans deux octets

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

A savoir (1)
• Les chaînes de caractères constantes (string literals) sont
indiquées entre guillemets. La chaîne de caractères vide
est alors: ""
• Dans les chaînes de caractères, nous pouvons utiliser
toutes les séquences d'échappement définies comme
caractères constants: "Ce \ntexte \nsera réparti sur 3
lignes."
• Le symbole " peut être représenté à l'intérieur d'une
chaîne par la séquence d'échappement \”.
• Le symbole ' peut être représenté à l'intérieur d'une liste
de caractères par la séquence d'échappement \' :
{'L','\'','a','s','t','u','c','e','\0'}
Pr. Abdessamad JARRAR SMI S4
Chapitre 2 : Chaines de Caractères.

Accéder à une chaîne et à ses éléments


• Une chaîne de caractères est une variable comme une
autre pour un programme: on y accède en l’appelant par
son nom de variable.
• Une chaîne est un tableau de caractères: pour accéder a
ses éléments on suit la logique d’un tableau.
– Exemple: char A[6] = “Hello”;
– A[0] contient ‘H’, A[1] contient ‘e’ … A[5] contient ‘\0’.

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Précédence et opérations
• Une chaîne de caractères est une variable : on peut utiliser
des opérations logiques et mathématiques.
• La précédence des caractères dans l'alphabet d'une
machine est dépendante du code de caractères utilisé.
Pour le code ASCII, nous pouvons constater l'ordre suivant:
– . . . ,0,1,2, ... ,9, . . . ,A,B,C, ... ,Z, . . . ,a,b,c, ... ,z, . . .
• Les symboles spéciaux (' ,+ ,- ,/ ,{ ,] , ...) et les lettres
accentuées (é ,è ,à ,û , ...) se trouvent répartis autour des
trois grands groupes de caractères (chiffres, majuscules,
minuscules). Leur précédence ne correspond à aucune
règle d'ordre spécifique.

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Tableau ASCII

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Précédence et opérations
• La précédence alphabétique des caractères induit une
relation de précédence 'est inférieur à' sur l'ensemble des
caractères. (idem pour les autres relations logiques)
• Ainsi, on peut dire que '0' est inférieur à 'Z‘ et noter '0' < 'Z’
• Ceci est possible car dans l'alphabet de la machine, le code
du caractère '0' (ASCII: 48) est inférieur au code du
caractère 'Z' (ASCII: 90).
– Exemples
• "ABC" précède "BCD” car 'A'<'B'
• "ABC" précède "B” car 'A'<'B'
• "Abc" précède "abc” car 'A'<'a'
• "ab" précède "abcd” car "" précède "cd"
• " ab" précède "ab” car ' '<'a'
Pr. Abdessamad JARRAR SMI S4
Chapitre 2 : Chaines de Caractères.

Tests logiques
• En tenant compte de l'ordre alphabétique des caractères,
on peut contrôler le type du caractère (chiffre, majuscule,
minuscule).
– Exemple: if (C>='0' && C<='9') printf("Chiffre\n", C);
if (C>='A' && C<='Z') printf("Majuscule\n", C);
if (C>='a' && C<='z') printf("Minuscule\n", C);

• Il est facile, de convertir des lettres majuscules dans des


minuscules:
if (C>='A' && C<='Z') C = C-'A'+'a';

• ou vice-versa:
if (C>='a' && C<='z') C = C-'a'+'A';

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Tableaux de chaînes
• Une chaîne de caractères est un tableau à 1 dimension de
caractères.
• On peut également définir des tableaux à plusieurs
dimensions qui peuvent contenir des mots:
char JOUR[7][9] = {“lundi” , ”mardi” , ”mercredi” , ”jeudi” ,
”vendredi”,”samedi”,”dimanche”};

• et on peut accéder à ces mots en utilisant la syntaxe


suivante: printf(“Aujourd’hui nous sommes %s”,JOUR[2]);

• qui affichera “Aujourd’hui nous sommes mercredi”.


• pour accéder à une lettre dans un mot, on utilise:
JOUR[I][j] avec %c pour l’affichage.
Pr. Abdessamad JARRAR SMI S4
Chapitre 2 : Chaines de Caractères.

Fonctions de bibliothèque

• Des fonctions de traitement des chaînes de


caractères des bibliothèques standards:
– <stdio.h> :
• scanf, printf : en utilisant %s dans le format
– attention: scanf prend une adresse en argument (&x), une
chaîne de caractères étant un tableau (ie, l’adresse du premier
élément), il n’y a pas de &.
• puts: puts(MACHAINE); est équivalent à
printf("%s\n",TXT);
• gets: gets(MACHAINE); lit une ligne jusqu’au retour
chariot et remplace le ‘\n’ par ‘\0’ dans l’affectation de la
chaîne.
Pr. Abdessamad JARRAR SMI S4
Chapitre 2 : Chaines de Caractères.

Fonctions de bibliothèque

– <string.h> :
• strlen(<s>): fournit la longueur de la chaîne sans compter le '\0' final
• strcpy(<s>, <t>): copie <t> vers <s>
• strcat(<s>, <t>): ajoute <t> à la fin de <s>
• strcmp(<s>, <t>): compare <s> et <t> lexicographiquement et fournit
un résultat:
– négatif si <s> précède <t>
– zéro si <s> est égal à <t>
– positif si <s> suit <t>
• strncpy(<s>, <t>, <n>): copie au plus <n> caractères de <t> vers <s>
• strncat(<s>, <t>, <n>): ajoute au plus <n> caractères de <t> à la fin
de <s>

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Fonctions de bibliothèque
• Attention:
• La nature de tableau d’une chaîne de caractères (ie,
l’adresse en mémoire du premier élément) interdit des
affections du type A= “hello” en dehors de la phase
d’initialisation.
– char A[ ]=“Hello”; est correct mais
– char A[6]; A= “Hello”; ne l’est pas.
• Il faut bien copier la chaîne caractère par caractère ou
utiliser la fonction strcpy respectivement strncpy:
– strcpy(A, "Hello");

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Fonctions de bibliothèque
• <stdlib>: conversion chaîne -> nombre
– atoi(<s>) retourne la valeur numérique représentée par <s>
comme int
– atol(<s>) retourne la valeur numérique représentée par <s>
comme long
– atof(<s>) retourne la valeur numérique représentée par <s>
comme double (!)

• Règles générales pour la conversion:


– Les espaces au début d'une chaîne sont ignorés
– La conversion s'arrête au premier caractère non convertible
– Pour une chaîne non convertible, les fonctions retournent zéro

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Exercices.
Exercice 1 : Ecrire un programme C qui lit une chaîne de
caractères et vérifie si elle est palindrome ou non. On
rappelle qu'une chaîne de caractères est dite palindrome, si
elle se lit de la même manière dans les deux sens.
Exemple: non, touot et 1234321 sont toutes
des chaînes de caractères palindromes.

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Exercices.
Solution :

#include<stdio.h> {
#include<stdlib.h> if(s[i]!=s[j])
#include<string.h> {
ok=0;
int main() break;
{ }
char s[100]; }
int i, j, ok; if(ok==1) printf("%s est
printf("Donnez une chaine de palindrome.\n",s);
caracteres:\n"); else printf("%s n'est pas
scanf("%s",s); palindrome.\n",s);
ok=1; return 0;
for(i=0,j=strlen(s)-1;i<j;i++,j--) }

Pr. Abdessamad JARRAR SMI S4


Chapitre 2 : Chaines de Caractères.

Fin de ce chapitre

Avez – vous des questions ?

Pr. Abdessamad JARRAR SMI S4


Chapitre 3.

Fonctions.

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Programmation modulaire
• Un programme dépassant une ou deux pages est difficile à
comprendre

• Une écriture modulaire permet de scinder le programme


en plusieurs parties et sous‐parties

• En C, le module se nomme la « fonction ».

• Le programme principal décrit essentiellement les


enchaînements des fonctions.

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Programmation modulaire
Bien différencier :
• Le texte (ou code) d’un programme qui est donc une suite
de fonctions non emboîtées (on ne définit pas une
fonction dans une autre fonction)
– Une fonction appelée dans une autre fonction a son code propre
séparé de la fonction appelante

• L’exécution d’un programme qui va enchaîner instructions,


appels de fonctions (appelant elles‐mêmes des fonctions)
etc.

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Les fonctions
• Dès qu’un groupe de lignes revient plusieurs fois on les
regroupe dans une fonction

• Une fonction se reconnaît à ses ()

• Une fonction en C est assez proche de la notion


mathématique de fonction:
• Exemples :
– y = sqrt(x) ;
– Z = pgcd(A,B) ;

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Intérêt des fonctions


 Lisibilité du code
 Réutilisation de la fonction
 Tests facilités
 Évolutivité du code
 Plus tard : les fonctions dans des fichiers séparés du main.c
(chapitre 6: Compilation séparée, Directives du
préprocesseur)

• Nb : une fonction peut faire appel à d’autres fonctions


dans son code ou dans ses arguments.

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Les types de fonctions


• Des fonctions qui s’exécutent sans retourner de valeurs
– nommées procédures dans certains langages
– Seront typées void
– Ex : une fonction qui affiche « bonjour »
void affiche_bonjour()
{
printf(« bonjour »);
}
• Des fonctions qui s’exécutent et retournent une valeur
– Exemples : sin(x) ; z = sqrt(x) ;
– Auront le type de la valeur à retourner

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions
Définition, déclaration, et appel des
fonctions
• On rencontre le nom des fonctions dans 3 cas :

• Déclaration : le type de la fonction et de ses arguments


– 1 seule fois

• Définition : codage de la fonction


– 1 seule fois

• Appels (= utilisations) : de la fonction


– n fois

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Paramètres réels – paramètres formels

• Un paramètre ou argument réel, est une valeur ou une


variable qui est mis entre parenthèses lors de l’appel de la
fonction.
– Il existe vraiment en mémoire.

• Un paramètre ou argument formel est un nom de variable


utilisé lors de la déclaration de la fonction.
– Le nom peut être omis (pas conseillé)
– Ne correspond pas à un emplacement mémoire

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Déclaration d’une fonction


• Permet au compilateur de vérifier l’adéquation des types
et de réserver l’espace mémoire pour la valeur de retour à
l’aide d’un prototype de fonction utilisant des paramètres
formels typés de la forme :
Type‐retourné NOM‐FONCTION (type1 paramètre1, type2 paramètre2, …) ;
//Exemple
double CalculePrixNet(double prix, double tauxTVA) ;

• NB : on peut définir une fonction avec autant de


paramètres formels qu’on veut. Dans l’exemple, il y a deux
paramètres formels.
Pr. Abdessamad JARRAR SMI S4
Chapitre 3 : Fonctions

Définition d’une fonction


• C’est le code de la fonction, de la forme :
Type‐retourné NOM‐FONCTION (type1 paramètre1, type2 paramètre2, …)
{
Déclaration des autres variables de la fonction;
Code de la fonction;
return (valeur‐de‐la‐fonction) ;
}

• Attention:
• En C, une fonction ne peut retourner qu’une valeur (au plus) grâce à la
commande return
• Le type de la fonction doit être le même que celui de la valeur retournée
• Quand une fonction ne retourne pas de valeur elle est typée void
– Exemples : void main() ; void AfficheBonjour();

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Le return
Il permet de:
• Retourne la valeur au programme appelant
• Et interrompt immédiatement l’exécution de la fonction
– On peut avoir plusieurs return.
– Mais un seul return pris en compte à chaque exécution.
• Exemple:
int est_positif(int val)
{
if( val >= 0)
return 1;
else
return 0;
}

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Variables locales – variables globales


• Définition : Un bloc est la partie de code compris entre {}
• Une variable créee dans un bloc n’existe que dans ce bloc
– C’est une variable locale au bloc
– Elle ne sera pas connue en dehors
– Sa valeur est perdue à la sortie du bloc
– « Sa durée de vie est celle du bloc »
• Le code suivant génère un message d’erreur du type
« error: 'a' undeclared (first use in this function) »:
int i = 1;
if(i==1)
{ int a=0; }
printf("la valeur de %d",a);

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Variables locales – variables globales


• Une variable globale existe en dehors de tout bloc.
• Elle a sa mémoire réservée pour toute l’exécution du
programme.
• « Sa durée de vie est celle du programme ».
• Exemple :
int i ;
main()
{
i=2;
printf(‘‘%d’’,i);
}

• Conseil : Soyez le plus local possible.

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions
Déclaration de variables dans les
fonctions
• Exemple: int triple (int x )
{
int y ;
y=3*x;
return y ;
}

 y est locale à la fonction  x est locale à la fonction


 Sa valeur sera perdue à  Elle est initialisée lors de
la sortie de la fonction l’appel à la valeur fournie
par le programme
appelant.
 Sa valeur sera perdue à la
sortie de la fonction
Pr. Abdessamad JARRAR SMI S4
Chapitre 3 : Fonctions

Appels de fonctions : exemples


• Utilisations :
Int triple(int x); // déclaration de la function triple
int main()
{
int a=2 ;
int b ;
triple(2) ; // appel de la function en passant la valeur 2
triple(a) ; // appel de la function en passant la valeur de a
b = triple(a) ; // affectation de la valeur du return de triple à b
a = triple(a) ; // affectation de la valeur du return de triple à a
return 0;
}
Int triple(int x) { // definition de la function triple
return 3*x ;
}

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Appel d’une fonction

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Fonctions et tableau
• Un tableau peut être un argument d’entrée d’une fonction.
• La syntaxe est : int tab[22] ;
int N = 22 ;
… // bout de code
m = Moyenne(tab,N) ;
… // bout de code
x = Maximum(tab,N)) ;
• Il peut être retourné sous forme de pointeur.
• On transmet donc le nom du tableau sans crochets
• Très souvent, le nombre d’éléments du tableau sur lequel
on souhaite travaillé est aussi un argument de la fonction
pour donner un caractère générique à la fonction.

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Les bonnes pratiques de programmation


• Une fonction ne fait en général qu’une chose.
• Le nom de la fonction décrit cette chose.
• Prendre le temps de bien choisir les fonctions, leur nom,
leurs paramètres.
• Bien choisir un nom explicite … et l’utiliser par
copier‐coller avec son jeu de paramètres.
• Une fonction reçoit un nombre limité de paramètre (2‐3
dans la plupart des cas ) ;
• Une fonction ne compte pas trop de lignes.
• Tester chaque fonction avant de passer à l’écriture de la
suivante.
Pr. Abdessamad JARRAR SMI S4
Chapitre 3 : Fonctions

Les erreurs courantes avec les fonctions

– Une fonction est déclarée mais non définie.


– Une fonction est appelée et n’existe pas.
– Le type de la fonction ne correspond pas au type de la valeur
retournée.
– La valeur retournée n’est pas stockée dans une variable du bon
type.
– Entre la déclaration, la définition et l’appel, le nombre de
paramètres n’est pas le même.
– Au moins un paramètre n’a pas le bon type.
– Ne confondez pas valeur retournée par la fonction (qui peut être
stockée dans une variable en mémoire) et affichage à l’écran d’un
résultat (qui n’est pas automatiquement stocké en mémoire).

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Conseils
• Si vous utilisez beaucoup de fonctions, tenez leur liste à
jour (Tableur, texte, …).
• Lorsque vous écrivez une fonction : testez‐la et
assurez‐vous de son bon fonctionnement avant de passer à
l’écriture de la suivante !!
• Ce qu’on ne doit jamais faire : écrire toutes les fonctions
et tester ensuite tout d’un bloc.
• Evitez les printf dans une fonction qui n’est pas dédiée à
l’affichage. Vous pouvez utiliser des affichages avec printf
dans vos fonctions pour les débugger, mais retirez‐les dès
que la fonction marche correctement.

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Conseils
• Au niveau du texte :
– Un programme en C est un ensemble disjoint de fonctions dont
une seule porte le nom de main (programme principal) et
constitue le point d’entrée du programme.
– On verra qu’on peut répartir les fonctions dans plusieurs fichiers
textes

• Au niveau de l’exécution :
– Un programme en C est une succession d’appels d’instructions et
de fonctions pouvant utiliser comme paramètres des résultats de
fonctions (et ainsi de suite).

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Programmation récursive
• Définition : la programmation récursive est une technique
de programmation qui remplace les instructions de boucle
(while, for, etc.) par des appels de fonction.
– (Ne pas confondre avec la notion de récursivité en
mathématiques)
• Exemple:
long fac (int n)
{
if (n>1)
return fac(n-1)*n ;
else
return 1 ;
}

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Passage des paramètres par valeur


• En C, le passage des paramètres se fait toujours par valeur,
autrement dit les fonctions n’obtiennent que les valeurs
de leurs paramètres et n’ont pas d’accès aux variables
elles-mêmes.
• A l’intérieur de la fonction, On peut donc changer les
valeurs des paramètres sans influencer les valeurs
originales dans les fonctions appelantes.
• Rappel : Les paramètres d’une fonction sont à considérer
comme des variables locales qui sont initialisées
automatiquement par les valeurs indiquées lors d’un
appel.

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Passage des paramètres par valeur

void triple(int);
int main() void triple(int a)
{ {
int a = 1; a = 3*a;
triple(a); }
printf("%d",a);
return 0;
}

• Ce programme affiche la valeur 1 puisque la variable a de


la fonction main ne sera pas affecté par le changement
dans la fonction triple.
• NB: la variable a de main et la variable a de triple sont
deux variable différents.
Pr. Abdessamad JARRAR SMI S4
Chapitre 3 : Fonctions

Passage des paramètres par adresse


• Comme on vient de le voir, tout paramètre est passé par
valeur, et cette règle ne souffre aucune exception.
• Cela pose le problème de réaliser un passage de
paramètre par adresse lorsque le programmeur en a
besoin.

• Pour changer la valeur d’une variable de la fonction


appelante, on procède comme suit :
1. la fonction appelante doit fournir l’adresse de la variable ;
2. la fonction appelée doit déclarer le paramètre comme pointeur.
3. On peut alors atteindre la variable à l’aide du pointeur.

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Passage des paramètres par adresse


• Prenons l’exemple précédent, l’utilisation d’un pointeur p
dans la fonction triple permet d’accéder à la variable a de
la fonction main à travers son adresse.
• La méthode est la suivante:
void triple(int*);
int main() void triple(int* p)
{ {
int a = 1; *p = 3*(*p);
triple(&a); }
printf("%d",a);
return 0;
}

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Pointeurs sur une fonction


• Il est parfois utile de passer une fonction comme
paramètre d’une autre fonction.
• Cette procédure permet en particulier d’utiliser une même
fonction pour différents usages. Pour cela, on utilise un
mécanisme de pointeur.
• Un pointeur sur une fonction correspond à l’adresse du
début du code de la fonction.
• Un pointeur p_fonction sur une fonction ayant pour
prototype: type fonction(type_1 arg_1,...,type_n arg_n);

• se déclare par :
type (*p_fonction)(type_1,...,type_n);

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Pointeurs sur une fonction


• Exemple :

int main() return 0;


{ }
int douuble(int);
int triple(int); int douuble(int val)
int (*pf)(int); {
int choix, x = 5; return val*2;
printf("Veuillez tapez 2 ou 3 : "); }
scanf("%d",&choix);
if(choix == 2) int triple(int val)
pf = douuble; {
else return val*3;
pf = triple; }
printf("le resultat est %d",(*pf)(x));

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Exercices.
Exercice 1 : écrire un programme qui permet de calculer le
nombre de combinaisons de p éléments parmi n éléments
définit par:

Définir et utiliser les deux fonctions :


• une fonction récursive factoriel.
• une fonction combinaison qui fait
appel à la fonction factoriel.

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Exercices.
Solution :
int main() else
{ return factoriel(n-1)*n;
printf("%d",combinaison(2,10)); }
return 0;
} int combinaison(int p, int n)
{
int factoriel(int n) return factoriel(n) /
{ (factoriel(p)*factoriel(n-p));
if(n < 2) }
return 1;

Pr. Abdessamad JARRAR SMI S4


Chapitre 3 : Fonctions

Fin de ce chapitre

Avez – vous des questions ?

Pr. Abdessamad JARRAR SMI S4


Chapitre 4.

Types Composés (Structures,


Unions, Synonymes).

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Les énumérations
• Les énumérations permettent de définir un type pour des
variables qui ne sont affectées qu’a un nombre fini de
valeurs.
• Un objet de type énumération est défini par le mot-clef
enum et un identificateur de modèle, suivi de la liste des
valeurs que peut prendre cet objet :
enum modele {constante1, constante2,. . . ,constanten} ;

• En réalité, les objets de type enum sont représentés


comme des int. Les valeurs possibles constante1,
constante2,. . . ,constanten sont codées par des entiers de
0 à n-1.

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Les énumérations
• Dans l’exemple suivant, le type enum boolean associe
l’entier 0 à la valeur FALSE et l’entier 1 à la valeur TRUE.
#include <stdio.h>
enum boolean {FALSE, TRUE};
int main () {
enum boolean b1 = TRUE; //declaration
printf("b = %d\n",b1);
return 0;
}

• On peut modifier le codage par défaut des valeurs de la


liste lors de la déclaration du type énuméré, par exemple :
enum boolean {FALSE = 12, TRUE = 23};

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Les structures
• Une structure est une suite finie d'objets de types
différents.
• Contrairement aux tableaux, les différents éléments d'une
structure n'occupent pas nécessairement des zones
contiguës en mémoire.
• Chaque élément de la structure, appelé membre ou
champ, est désigné par un identificateur.
• Ce mécanisme permet de grouper un certain nombre de
variables de types différents au sein d’une même entité.

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Utilisation des structures


• L’utilisation pratique d’une structure se déroule de la façon
suivante :
1. On commence par déclarer la structure elle-même. Le modèle
général de cette déclaration est le suivant :

struct nom_structure {
type_1 membre1 ;
type_2 membre2 ;
...
type_n membren ;
};

2. Pour déclarer un objet de type structure correspondant au


modèle précédent, on utilise la syntaxe :
struct nom_structure identificateur-objet ;

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Utilisation des structures


3. On accède aux différents membres d’une structure grâce à
l’opérateur membre de structure, noté « . ». Le i-ème membre
de objet est désigné par l’expression :
identificateur-objet.membrei
• On peut effectuer sur le i-ème membre de la structure
toutes les opérations valides sur des données de type
type_i.
• Les règles d’initialisation d’une structure lors de sa
déclaration sont les mêmes que pour les tableaux.
• On écrit par exemple :
struct complexe i = {0. , 1.};

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Utilisation des structures


• A noter qu’a la différence des tableaux, on peut appliquer
l’opérateur d’affectation aux structures.
• On peut écrire :
struct complexe { int main() {
double reelle; struct complexe z1, z2;
double imaginaire; ...
}; // ne pas oublier le ";« z2 = z1;
}

• NB: Aucune comparaison n’est possible sur les structures


(en particulier, on ne peut appliquer les opérateurs == ou
!=).

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Utilisation des structures


• On déclare un tableau dont les composantes sont des
structures de la même façon qu’on déclarerait un tableau
d’éléments de type simple.
• Exemple:
struct personne {
char nom[20];
char prenom[20];
};
...
struct personne t[100]; //tableau de 100 personnes

• Pour référencer le nom de la personne qui a l’index i, on


écrira : t[i].nom

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Pointeur sur une structure


• En reprenant la structure personne précédentes, on
déclarera une variable de type pointeur vers cette
structure de la manière suivante :
struct personne *p;

• On peut affecter à ce pointeur des adresses sur des struct


personne. Exemple :
struct personne {...};
int main() {
struct personne pers; // pers = variable de type struct personne
struct personne * p; // p = pointeur vers une struct personne
p = &pers; // p pointe maintenant vers pers
}

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Pointeur sur une structure


Pour accéder aux membres de la structure pointée :

• il faudrait écrire normalement (*p).nom ; et non *p.nom


qui sera interprété compte tenu des priorités entre
opérateurs par *(p.nom) ce qui n’a de sens que si le champ
nom est un pointeur !

• mais on utilise en pratique l’écriture simplifiée p->nom.

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Structures auto-référées
• Il s’agit de cas particulier de structures dont un des
membres pointe vers une structure du même type.
• Cette représentation permet en particulier de construire
des listes chaînées.
• il est possible de représenter une liste d’éléments de
même type par un tableau (ou un pointeur).
• Toutefois, cette représentation, dite contiguë, impose que
la taille maximale de la liste soit connue a priori.
• Pour résoudre ce problème, on utilise une représentation
chaînée : l’élément de base de la chaîne est une structure
appelée cellule qui contient la valeur d’un élément de la
liste et un pointeur sur l’élément suivant.
Pr. Abdessamad JARRAR SMI S4
Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Structures auto-référées
• Le dernier élément pointe sur le pointeur NULL (défini
dans stddef.h).
• La liste est définie comme un pointeur sur le premier
élément de la chaîne.

• Considérons par exemple la structure toto possédant 2


champs :
1. un champ data de type int ;
2. un champ next de type pointeur vers une struct toto.

• La liste est alors un objet de type pointeur sur une struct


toto.
Pr. Abdessamad JARRAR SMI S4
Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Structures auto-référées
• Grâce au mot-clef typedef, on peut définir le type liste,
synonyme du type pointeur sur une struct toto.
• On peut alors définir un objet liste l qu’il convient
d’initialiser à NULL.

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Structures auto-référées
• Un des avantages de la représentation chaînée est qu’il est
très facile d’insérer un élément à un endroit quelconque
de la liste.
• Ainsi, pour insérer un élément en tête de liste, on utilise la
fonction suivante :
liste insere(int element, liste Q) {
liste L;
L = (liste)malloc(sizeof(struct toto));
L->data = element;
L->next = Q;
return L;
}

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Structures auto-référées
De façon général, l’ajout d’un nouvel élément dans une liste
chaînée s’effectue en trois étapes :
1. recherche de l’endroit où la nouvelle cellule devra être
insérée ;
2. Création de la nouvelle cellule (par malloc) et mise à jour
de ses champs. Au niveau de l’allocation en mémoire, on
prendra garde :
1. de réserver le bon nombre d’octets (en reprenant l’exemple
précédent, sizeof(struct toto) et non sizeof(struct toto *))
2. de convertir le résultat du malloc vers le bon type (pointeur vers
la structure).
3. mise à jour des champs des cellules voisines.
Pr. Abdessamad JARRAR SMI S4
Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Structures auto-référées
• Pour supprimer le premier élément de la liste par exemple,
il est important de libérer l’espace mémoire alloué :
liste supprime_tete(liste L) {
liste suivant = L;
if (L != NULL) { // pour etre sûr que L->next existe
suivant= L->next;
free(L); //libération de l’espace alloué pour une cellule
}
return suivant;
}

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Structures auto-référées
• Il est primordial d’utiliser free dans le cas des listes
chaînées.
• Sinon, la mémoire risque d’être rapidement saturée (par
exemple lors d’ajouts et de suppressions successives qui ne
libèrent pas l’espace alloué).

• Ainsi, de façon similaire à l’ajout, la suppression d’une


cellule dans une liste chaînée s’effectue en trois étapes :
1. Recherche de la cellule à supprimer ;
2. mise à jour des champs des cellules voisines ;
3. libération de la cellule avec free.

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Les unions
• Il est parfois nécessaire de manipuler des variables
auxquelles on désire affecter des valeurs de types
différents.
• Une union désigne un ensemble de variables de types
différents susceptibles d’occuper alternativement une
même zone mémoire.
• Une union permet donc de définir un objet comme
pouvant être d’un type au choix parmi un ensemble fini de
types.
• Si les membres d’une union sont de longueurs différentes,
la place réservée en mémoire pour la représenter
correspond à la taille du membre le plus grand.
Pr. Abdessamad JARRAR SMI S4
Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Déclaration d’une union


• La déclaration et la définition d’une union ne diffèrent de
celles d’une structure que par l’utilisation du mot-clé union
(qui remplace le mot-clé struct).
• Dans l’exemple suivant, la variable hier de type union jour
peut être soit un caractère, soit un entier (mais pas les
deux à la fois) :
union jour { hier.lettre = ’J’; //jeudi
char lettre; printf("hier = %c\n",hier.lettre);
int numero; hier.numero = 4;
}; demain.numero = (hier.numero + 2) % 7;
printf("demain = %d\n",demain.numero);
int main() { return 0;
union jour hier, demain; }

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Utilisation pratique des unions


• Lorsqu’il manipule des unions, le programmeur n’a
malheureusement aucun moyen de savoir à un instant
donné quel est le membre de l’union qui possède une
valeur.

• Pour être utilisable, une union doit donc toujours être


associée à une variable dont le but sera d’indiquer le
membre de l’union qui est valide.

• En pratique, une union et son indicateur d’état sont


généralement englobés à l’intérieur d’une structure.

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Utilisation pratique des unions

enum etat {CARAC, ENTIER}; else printf("Erreur!\n");


struct jour { }
enum etat indicateur;
union { int main() {
char lettre; struct jour ex1, ex2;
int numero; ex1.indicateur = CARAC;
} day; ex1.day.lettre = ’j’;
}; print_jour(ex1, "ex1");
ex2.indicateur = ENTIER;
void print_jour( struct jour d, ex2.day.numero = 4;
char * name) { print_jour(ex2, "ex2");
if (d.indicateur == CARAC) return 0;
printf("%s = %c\n",name,d.day.lettre); }
else if (d.indicateur == ENTIER)
printf("%s = %i\n",name,d.day.numero);

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Une méthode pour alléger l’accès aux


membres
• Quand une union est dans une structure (comme c’est la
cas dans l’exemple précédent), on aura noter que l’accès
aux membres de l’union est relativement lourd (ex :
d.day.numero pour accéder au membre numero).
• On peut alléger cette écriture en utilisant les facilités du
préprocesseur :
#define L day.lettre
#define N day.numero
...
ex1.indicateur = CARAC;
ex1.L = ’j’; //initialisation de ex1 `a jeudi

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Définition de synonymes de types


• Pour alléger l’écriture des programmes, on peut affecter
un nouvel identificateur à un type à l’aide du mot-clé
typedef : typedef type synonyme ;

• Exemple :
typedef unsigned char UCHAR;
typedef struct { double x, y } POINT;
typedef POINT *P_POINT; //pointeur vers un POINT

• A partir de ce moment, l’identificateur UCHAR peut être


utilisé comme une abréviation pour le type unsigned char,
l’identificateur POINT peut être utilisé pour spécifier le
type de structure associé et P_POINT permet de
caractériser un pointeur vers un POINT.

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Exercices.
Exercice 1 : Définir un type rationnel comportant les deux
champs num (pour numérateur) et den (pour dénominateur)
de types entier puis des fonctions permettant la saisie d’un
rationnel, son affichage, la somme, la multiplication, la
soustraction et la division de deux rationnels.

Pr. Abdessamad JARRAR SMI S4


Chapitre 4 : Types Composés (Structures, Unions, Synonymes).

Fin de ce chapitre

Avez – vous des questions ?

Pr. Abdessamad JARRAR SMI S4


Chapitre 5.

Fichiers.

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Gestion des fichiers


• Pour des raisons d’efficacité, les accès à un fichier se font
par l’intermédiaire d’une mémoire-tampon (on parle de
buffer), ce qui permet de réduire le nombre d’accès aux
périphériques (disque...).
• Pour pouvoir manipuler un fichier, un programme a besoin
d’un certain nombre d’informations : l’adresse de l’endroit
de la mémoire-tampon où se trouve le fichier, la position
de la tête de lecture, le mode d’accès au fichier (lecture ou
écriture)... Ces informations sont rassemblées dans une
structure, dont le type, FILE *, est défini dans <stdio.h>. Un
objet de type FILE * est appelé flot de données (stream en
anglais).
Pr. Abdessamad JARRAR SMI S4
Chapitre 5 : Fichiers.

Gestion des fichiers


• Avant de lire ou d’écrire dans un fichier, on notifie son
accès par la commande fopen.

• Cette fonction prend comme argument:


– le nom du fichier, défini avec le système d’exploitation
– le mode d’ouverture du fichier
• et initialise un flot de données, qui sera ensuite utilisé lors
de l’écriture ou de la lecture.

• Après les traitements, on annule la liaison entre le fichier


et le flot de données grâce à la fonction fclose.
Pr. Abdessamad JARRAR SMI S4
Chapitre 5 : Fichiers.

Ouverture et fermeture de fichiers


• Lorsqu’on désire accéder à un fichier, il est nécessaire
avant tout accès d’ouvrir le fichier à l’aide de la fonction
fopen.

• Cette fonction, de type FILE * ouvre un fichier et lui associe


un flot de données. Sa syntaxe est :
fopen("nom-de-fichier","mode")

• La valeur retournée par fopen est :


– soit un flot de données ;
– soit NULL si l’exécution de cette fonction ne se déroule pas
normalement.

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Ouverture et fermeture de fichiers


• Il faut toujours tester si la valeur renvoyée par la fonction
fopen est égale à NULL afin de détecter les erreurs
d’ouverture de fichier (lecture d’un fichier inexistant...).
– Le premier argument de fopen fournit donc le nom du fichier.
– Le second argument, mode, est une chaîne de caractères qui
spécifie le mode d’accès au fichier.
• Les spécificateurs de mode d’accès diffèrent suivant le type
de fichier considéré. On distingue:
– les fichiers textes, pour lesquels les caractères de contrôle (retour
à la ligne ...) seront interprétés en tant que tels lors de la lecture
et de l’écriture ;
– les fichiers binaires, pour lesquels les caractères de contrôle se
sont pas interprétés.
Pr. Abdessamad JARRAR SMI S4
Chapitre 5 : Fichiers.

Ouverture et fermeture de fichiers


• Les différents modes d’accès sont:
– "r" ouverture d’un fichier texte en lecture.
– "w" ouverture d’un fichier texte en écriture.
– "a" ouverture d’un fichier texte en écriture à la fin.
– "rb" ouverture d’un fichier binaire en lecture.
– "wb" ouverture d’un fichier binaire en écriture.
– "ab" ouverture d’un fichier binaire en écriture à la fin.
– "r+" ouverture d’un fichier texte en lecture/écriture.
– "w+" ouverture d’un fichier texte en lecture/écriture.
– "a+" ouverture d’un fichier texte en lecture/écriture à la fin.
– "r+b" ouverture d’un fichier binaire en lecture/écriture.
– "w+b" ouverture d’un fichier binaire en lecture/écriture.
– "a+b" ouverture d’un fichier binaire en lecture/écriture à la fin.
Pr. Abdessamad JARRAR SMI S4
Chapitre 5 : Fichiers.

Ouverture et fermeture de fichiers


• Conditions particulières et cas d’erreur:
– Si le mode contient la lettre r, le fichier doit exister, sinon c’est
une erreur.
– Si le mode contient la lettre w, le fichier peut ne pas exister. Dans
ce cas, il sera créé, et si le fichier existait déjà, son ancien contenu
est perdu.
– Si le mode contient la lettre a, le fichier peut ne pas exister.
Comme pour le cas précédent, si le fichier n’existe pas, il est créé ;
si le fichier existe déjà, son ancien contenu est conservé.
– Si un fichier est ouvert en mode ”écriture à la fin”, toutes les
écritures se font à l’endroit qui est était la fin du fichier lors de
l’exécution de l’ordre d’écriture. Cela signifie que si plusieurs
processus partagent le même FILE*, résultat de l’ouverture d’un
fichier en écriture à la fin, leurs écritures ne s’écraseront pas
mutuellement.
Pr. Abdessamad JARRAR SMI S4
Chapitre 5 : Fichiers.

Ouverture et fermeture de fichiers


• Les modes de communication standard:
• Quand un programme est lancé par le système, celui-ci
ouvre trois fichiers correspondant aux trois modes de
communication standard : standard input, standard output
et standard error.
• Il y a trois constantes prédéfinies dans stdio.h qui
correspondent aux trois fichiers :
– stdin (standard input) : flot d’entrée (par défaut, le clavier) ;
– stdout (standard output) : flot de sortie (par défaut, l’écran) ;
– stderr (standard error) : flot d’affichage des messages d’erreur
(par défaut, l’écran).

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Ouverture et fermeture de fichiers


• Il est fortement conseillé d’afficher systématiquement les
messages d’erreur sur stderr afin que ces messages
apparaissent à l’écran même lorsque la sortie standard est
redirigée.
• Utilisation typique de fopen:
#include <stdio.h>
FILE *fp;
...
if ((fp = fopen("donnees.txt","r")) == NULL) {
fprintf(stderr,"Impossible d’ouvrir le fichier données en lecture\n");
exit(1);
}

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Ouverture et fermeture de fichiers


• La fonction fclose permet de fermer le flot qui a ´et´e
associé à un fichier par la fonction fopen.
• Sa syntaxe est :
fclose(flot)

• où flot est le flot de type FILE* retourné par la fonction


fopen correspondante.

• La fonction fclose retourne 0 si l’opération s’est déroulée


normalement et EOF si il y a eu une erreur.

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Les entrées-sorties formatées


• La fonction fprintf, analogue à printf, permet d’écrire des
données dans un flot.
• Sa syntaxe est :
fprintf(flot,"chaîne de contrôle", expression1, . . . , expressionn);

• où flot est le flot de données retourné par la fonction


fopen.
• Les spécifications de format utilisées pour la fonction
fprintf sont les mêmes que pour printf puisque :
printf(...) ⇐⇒ fprintf(stdout,...)

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Les entrées-sorties formatées


• La fonction fscanf, analogue à scanf, permet de lire des
données dans un fichier.

• Sa syntaxe est semblable à celle de scanf :


fscanf(flot,"chaîne de contrôle", arg1, . . . , argn);

• où flot est le flot de données retourné par fopen.

• Les spécifications de format sont ici les mêmes que celles


de la fonction scanf.

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Impression et lecture de caractères


• Similaires aux fonctions getchar et putchar, les fonctions
fgetc et fputc permettent respectivement de lire et d’écrire
un caractère dans un fichier.
• La fonction fgetc retourne le caractère lu dans le fichier et
la constante EOF lorsqu’elle détecte la fin du fichier.
• Son prototype est : int fgetc(FILE* flot);

• La fonction fputc écrit un caractère dans le flot de données


int fputc(int caractere, FILE *flot)

• Elle retourne l’entier correspondant au caractère lu (ou la


constante EOF en cas d’erreur).

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Impression et lecture de caractères


• Il existe également deux versions optimisées des fonctions
fgetc et fputc qui sont implémentées par des macros. Il
s’agit respectivement de getc et putc.
• Leur syntaxe est similaire à celle de fgetc et fputc :
int getc(FILE* flot);
int putc(int caractere, FILE *flot)

• le programme suivant lit le contenu du fichier texte


entree.txt, et le recopie caractère par caractère dans le
fichier sortie.txt (voir la page suivante):

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Impression et lecture de caractères


#include <stdio.h> return(EXIT_FAILURE);
#include <stdlib.h> }

int main() { while ((c = fgetc(f_in)) != EOF)


FILE *f_in, *f_out; fputc(c, f_out);
int c;
if ((f_in = fopen("entree.txt", "r")) == fclose(f_in);
NULL) { fclose(f_out);
fprintf(stderr, "\nErreur: Impossible de return(EXIT_SUCCESS);
lire %s\n", "entree.txt"); }
return(EXIT_FAILURE); /* On notera l’utilisation des constantes
} EXIT_SUCCESS (valant 0) et
if ((f_out = fopen("sortie.txt","w")) == EXIT_FAILURE (valant 1), définies dans la
NULL) { librairie <stdlib.h> et qui sont les
fprintf(stderr, "\nErreur: Impossible paramètres privilégiés de la fonction exit
d’ecrire dans %s\n", "sortie.txt"); */

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Relecture d’un caractère


• Il est possible de replacer un caract`ere dans un flot au
moyen de la fonction ungetc :
int ungetc(int carac, FILE *f);

• Cette fonction place le caractère carac (converti en


unsigned char) dans le flot f.
• si carac est égal au dernier caractère lu dans le flot, elle
annule le déplacement provoqué par la lecture
précédente.
• Toutefois, ungetc peut être utilisée avec n’importe quel
caractère (sauf EOF).

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Relecture d’un caractère


• L’exemple suivant permet d’illustrer le comportement de
ungetc :
#include <stdio.h> return(EXIT_FAILURE);
#include <stdlib.h> }
while ((c = fgetc(f_in)) != EOF) {
int main(void) { if (c == ’0’)
FILE *f_in; ungetc(’.’,f_in);
int c; putchar(c);
if ((f_in = fopen("entree.txt", "r")) == }
NULL) { fclose(f_in);
fprintf(stderr, "\nErreur: Impossible de return(EXIT_SUCCESS);
lire le fichier %s\n", "entree.txt"); }

• Sur le fichier entree.txt dont le contenu est 097023, ce


programme affiche à l’écran 0.970.23.
Pr. Abdessamad JARRAR SMI S4
Chapitre 5 : Fichiers.

Les entrées-sorties binaires


• Les fonctions d’entrées-sorties binaires permettent de
transférer des données dans un fichier sans transcodage.
En ce sens, elles sont plus portables car elles écrivent ce
qu’on leur dit. Elles sont ainsi plus efficaces que les
fonctions d’entrée-sortie standard.
• Elles sont notamment utiles pour manipuler des données
de grande taille ou ayant un type composé.
• Leurs prototypes sont :
size_t fread(void *pointeur, size_t taille, size_t nbre, FILE *f);
size_t fwrite(void *pointeur, size_t taille, size_t nbre, FILE *f);

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Les entrées-sorties binaires


• où pointeur est l’adresse du début des données à
transférer, taille la taille des objets à transférer et nbre leur
nombre.
• Rappelons que le type size_t, défini dans <stddef.h>,
correspond au type du résultat de l’évaluation de sizeof.
• La fonction fread lit les données sur le flot f et la fonction
fwrite les écrit.Ces deux fonctions retournent le nombre
de données transférées.
• Par exemple, le programme suivant écrit un tableau
d’entiers (contenant les 50 premiers entiers) avec fwrite
dans le fichier sortie.txt, puis lit ce fichier avec fread et
imprime les éléments du tableau.
Pr. Abdessamad JARRAR SMI S4
Chapitre 5 : Fichiers.

Les entrées-sorties binaires

int main(void) { fclose(f_out);


FILE *f_in, *f_out; if ((f_in = fopen(F_SORTIE, "r")) ==
int *tab1, *tab2; NULL) {
int I, NB = 50; fprintf(stderr, "\nImpossible de lire
tab1 = (int*)malloc(NB * sizeof(int)); dans %s\n", "sortie.txt“ );
tab2 = (int*)malloc(NB * sizeof(int)); return(EXIT_FAILURE);
for (i = 0 ; i < NB; i++) }
tab1[i] = i; fread(tab2, NB * sizeof(int), 1, f_in);
if ((f_out = fopen("sortie.txt", "w")) == fclose(f_in);
NULL) { for (i = 0 ; i < NB; i++)
fprintf(stderr, "\nImpossible d’ecrire printf("%d\t",tab2[i]);
dans %s\n", "sortie.txt"); printf("\n");
return(EXIT_FAILURE); return(EXIT_SUCCESS);
} }
fwrite(tab1, NB * sizeof(int), 1, f_out);

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Positionnement dans un fichier


• Les différentes fonctions d’entrées-sorties permettent
d’accéder à un fichier en mode séquentiel :
– les données du fichier sont lues ou écrites les unes à la suite des
autres.
• Il est également possible d’accéder à un fichier en mode
direct, c’est-à-dire que l’on peut se positionner à n’importe
quel endroit du fichier. La fonction fseek permet de se
positionner à un endroit précis et a pour prototype :
int fseek(FILE *flot, long deplacement, int origine);

• La variable deplacement détermine la nouvelle position


dans le fichier. Il s’agit d’un déplacement relatif par rapport
à origine, compté en nombre d’octets.
Pr. Abdessamad JARRAR SMI S4
Chapitre 5 : Fichiers.

Positionnement dans un fichier


• La variable origine peut prendre trois valeurs :
1. SEEK_SET (égale à 0) : début du fichier ;
2. SEEK_CUR (égale à 1) : position courante ;
3. SEEK_END (égale à 2) : fin du fichier.
• La fonction int rewind(FILE *flot);

permet de se positionner au début du fichier. Elle est


équivalente à fseek(flot, 0, SEEK_SET) ;
• La fonction long ftell(FILE *flot);

retourne la position courante dans le fichier (en nombre


d’octets depuis l’origine).

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.
Positionnement dans un fichier
(exemple)
int main(void) { 1, f_out); fread(&i, sizeof(i), 1, f_in);
FILE *f_in, *f_out; fclose(f_out); printf("\t i = %d", i);
int *tab; if ((f_in = fopen("sortie.txt" rewind(f_in);
int I, NB=50; , "r")) == NULL) { printf("\n position %ld",
tab = (int*)malloc(NB * fprintf(stderr, "\nImpossible ftell(f_in));
sizeof(int)); de lire dans %s\n", fread(&i, sizeof(i), 1, f_in);
for (i = 0 ; i < NB; i++) "sortie.txt"); printf("\t i = %d", i);
tab[i] = i; return(EXIT_FAILURE); fseek(f_in, 5 * sizeof(int),
if ((f_out = fopen("sortie.txt"} SEEK_CUR);
, "w")) == NULL){ fseek(f_in, 0, SEEK_END); printf("\n position %ld",
fprintf(stderr, "\nImpossible printf("\n position %ld", ftell(f_in));
d’ecrire dans %s\n", ftell(f_in)); fread(&i, sizeof(i), 1, f_in);
"sortie.txt"); fseek(f_in, -10 * sizeof(int), printf("\t i = %d\n", i);
return(EXIT_FAILURE); SEEK_END); fclose(f_in);
} printf("\n position %ld", return(EXIT_SUCCESS);
fwrite(tab, NB * sizeof(int), ftell(f_in)); }

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.
Positionnement dans un fichier
(exemple)
• L’exécution de ce programme affiche à l’écran :
position 200
position 160 i = 40
position 0 i=0
position 24 i=6

• On constate en particulier que l’emploi de la fonction fread


provoque un déplacement correspondant à la taille de
l’objet lu à partir de la position courante.

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Exercices.
Exercice 1 :
1. Ecrivez une fonction compte_c(FILE * f) qui renvoie le
nombre de caractères d’un fichier.
2. Ecrivez une fonction compte_m(FILE * F) qui renvoie le
nombre de mots d’un fichier. Les mots sont
séparés par des espaces ou des retours à la
ligne.
3. Ecrivez une fonction compte_l qui renvoie
le nombre de lignes.

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Exercices.
Solution :
int compte_c(FILE ∗ f ){ on_est_sur_un_mot =1;
int cp_t =0; cp_t++; }
while ( fgetc( f )!=EOF) cp_t++; }
return cp_t ; return cp_t ;
} }

int compte_m(FILE ∗ f ){ int compte_l(FILE ∗ f ){


int c, cp_t =0; int c ;
int on_est_sur_un_mot =0; int cp_t =0;
while ( ( c=fgetc( f ) )!=EOF) while ( ( c=fgetc( f ) )!=EOF)
if ( c==’ ’ | | c==’ \n ’ ) if ( c==’ \n ’ )
on_est_sur_un_mot =0; cp_t++;
else { return cp_t ;
if ( ! on_est_sur_un_mot ) { }

Pr. Abdessamad JARRAR SMI S4


Chapitre 5 : Fichiers.

Fin de ce chapitre

Avez – vous des questions ?

Pr. Abdessamad JARRAR SMI S4


Chapitre 6. - Complément -

Compilation séparée et Directives


du préprocesseur

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Les directives du préprocesseur


• Le préprocesseur est un programme exécuté lors de la
première phase de la compilation.
• Il effectue des modifications textuelles sur le fichier source
à partir de directives.
• Les différentes directives au préprocesseur, introduites par
le caractère #, ont pour but :
– l’incorporation de fichiers source (#include),
– la définition de constantes symboliques et de macros (#define),
– la compilation conditionnelle (#if, #ifdef,...).
• Dans ce chapitre, nous détaillerons l’ensemble des
directives du préprocesseur.

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La directive #include
• #include permet d’incorporer dans le fichier source le
texte figurant dans un autre fichier.
• La directive #include possède deux syntaxes voisines :
– #include <nom-de-fichier>: recherche le fichier mentionné dans
un ou plusieurs répertoires systèmes définis par l’implémentation
(typiquement /usr/include/) :
– #include "nom-de-fichier": recherche le fichier dans le répertoire
courant (celui où se trouve le fichier source). On peut spécifier
d’autres répertoires à l’aide de l’option -I du compilateur.
• La première syntaxe est généralement utilisée pour les
fichiers en-tête de la librairie standard, tandis que la
seconde est destinée aux fichiers créés par l’utilisateur.

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La directive #define
• La directive #define permet de définir des constantes
symboliques (on parle aussi de macros sans paramètres)
ou des macros avec paramètre.
• Lorsque le préprocesseur lit une ligne du type :
#define nom reste-de-la-ligne

• il remplace dans toute la suite du source toute nouvelle


occurrence de nom par reste-de-la-ligne. Il n’y a aucune
contrainte quand à ce qui peut se trouver dans reste-de-la-
ligne : on pourra ainsi écrire
#define BEGIN {
#define END }

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La directive #define
• L’utilité principale des macros sans paramètre est de
donner un nom parlant à une constante. Les avantages à
toujours donner un nom aux constantes sont les suivants :
– un nom bien choisi permet d’expliciter la sémantique de la
constante. Ex : #define NB_COLONNES 100
– la constante chiffrée se trouve à un seul endroit, ce qui facilite la
modification du programme quand on veut changer la valeur de la
constante (cas de la taille d’un tableau, par exemple).
– on peut expliciter facilement les relations entre constantes. Ex :
#define NB_LIGNES 24
#define NB_COLONNES 80
#define TAILLE_MATRICE NB_LIGNES * NB_COLONNES

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Définition de constantes symboliques à


l’invocation du compilateur
• Certains compilateurs permettent de définir des
constantes symboliques à l’invocation du compilateur. Il
est alors possible d’écrire un programme utilisant une
macro qui n’est nulle part définie dans le source.
• Ceci est très pratique pour que certaines constantes
critiques d’un programme aient une valeur qui soit
attribuée à l’extérieur du programme (comme lors d’une
une phase de configuration par exemple).
• Ci-dessous, un exemple pour gcc : la compilation du fichier
prog.c en définissant la constante symbolique NB_LIGNES
de valeur 24 :
gcc -c -DNB_LIGNES=24 prog.c

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Constantes symboliques prédéfinies


• Il y a un certain nombre de macros prédéfinies par le
préprocesseur, récapitulées dans le tableau suivant.
Nom Valeur de la macro Type
__LINE__ Numéro de la ligne courante du programme Entier
source
__FILE__ Nom du fichier source en cours de Chaîne
compilation
__DATE__ La date de la compilation Chaîne
__TIME__ L’heure de la compilation Chaîne
__STDC__ 1 si le compilateur est ISO, 0 sinon Entier

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Les macros avec paramètres


• Une macro avec paramètres se définit de la manière
suivante : #define nom(liste-de-paramètres) corps-de-la-macro

• où liste-de-paramètres est une liste d’identificateurs


séparés par des virgules.
• Par exemple, avec la directive #define MAX(a,b) (a > b ? a : b)

• le processeur remplacera dans la suite du code toutes les


occurrences du type MAX(x,y) où x et y sont des symboles
quelconques par (x > y ? x : y).
• Une macro a donc une syntaxe similaire à celle d’une
fonction, mais son emploi permet en général d’obtenir de
meilleures performances en temps d’exécution.

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Les macros avec paramètres


• La distinction entre une définition de constante
symbolique et celle d’une macro avec paramètres se fait
sur le caractère qui suit immédiatement le nom de la
macro :
– si ce caractère est une parenthèse ouvrante, c’est une macro avec
paramètres,
– sinon c’est une constante symbolique.
• Il ne faut donc jamais mettre d’espace entre le nom de la
macro et la parenthèse ouvrante. L’erreur classique étant
d’écrire par erreur : #define CARRE (a) a * a

• la chaîne de caractères CARRE(2) sera alors remplacée par:


(a) a * a (2)
Pr. Abdessamad JARRAR SMI S4
Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Les macros avec paramètres


• Il faut toujours garder à l’esprit que le préprocesseur
n’effectue que des remplacements de chaînes de
caractères. En particulier, il est conseillé de toujours
mettre entre parenthèses le corps de la macro et les
paramètres formels qui y sont utilisés.
• Par exemple, si l’on écrit sans parenthèses :
#define CARRE(a) a * a

• le préprocesseur remplacera CARRE(a + b) par a + b * a + b


et non par (a + b) * (a + b). De même, !CARRE(x) sera
remplacé par ! x * x et non par !(x * x).

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Les directives du préprocesseur


• Il faut être attentif aux éventuels effets de bord que peut
entraîner l’usage de macros.

• Par exemple, CARRE(x++) aura pour expansion (x++) *


(x++).
• L’opérateur d’incrémentation sera donc appliqué deux fois
au lieu d’une.

• En conclusion, les macros avec paramètres sont à utiliser


avec précaution et en cas de doutes, il faut mieux s’en
passer.
Pr. Abdessamad JARRAR SMI S4
Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La compilation conditionnelle
• La compilation conditionnelle a pour but d’incorporer ou
d’exclure des parties du code source dans le texte qui sera
généré par le préprocesseur, le choix étant basé sur un test
exécuté à la compilation.
• Elle permet d’adapter le programme au matériel ou à
l’environnement sur lequel il s’exécute, ou d’introduire
dans le programme des instructions de débuggage.
• Les directives de compilation conditionnelle se
répartissent en deux catégories, suivant le type de la
condition invoquée qui est testée :
– la valeur d’une expression
– l’existence ou l’inexistence de symboles
Pr. Abdessamad JARRAR SMI S4
Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La compilation conditionnelle
• La syntaxe de la compilation à condition liée à la valeur
d’une expression est :
#if condition-1
partie-du-programme-1
#elif condition-2
partie-du-programme-2
...
#elif condition-n
partie-du-programme-n
#else
partie-du-programme-else
#endif

• Le nombre de #elif est quelconque et le #else est facultatif.


Chaque condition-i doit être une expression constante.
Pr. Abdessamad JARRAR SMI S4
Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La compilation conditionnelle
• Lors de la compilation, une seule partie-du-programme-i
sera compilée : celle qui correspond à la première
condition-i non nulle, ou bien la partie-du-programme-else
si toutes les conditions sont nulles.
• Par exemple, on peut écrire :
#define PROCESSEUR ALPHA
...
#if PROCESSEUR == ALPHA
taille_long = 64;
#elif PROCESSEUR == PC
taille_long = 32;
#endif

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La compilation conditionnelle
• La syntaxe de la compilation à condition liée à l’existence
d’un symbole est :
#ifdef symbole
partie-du-programme-1
#else condition-2
partie-du-programme-2
#endif

• Si symbole est défini au moment où l’on rencontre la


directive #ifdef, alors partie-du-programme-1 sera
compilée et partie-du-programme-2 sera ignorée.
• Dans le cas contraire, c’est partie-du-programme-2 qui
sera compilée.
Pr. Abdessamad JARRAR SMI S4
Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La compilation conditionnelle
• La directive #else est comme précédemment facultative.
• Ce type de directive est utile pour rajouter des instructions
destinées au débuggage du programme :
#define DEBUG
....
#ifdef DEBUG
for (i = 0; i < N; i++)
printf("%d\n",i);
#endif /* DEBUG */

• Il suffit alors de supprimer la directive #define DEBUG pour


que les instructions liées au debuggage ne soient pas
compilées.

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La compilation conditionnelle
• De façon similaire, on peut tester la non-existence d’un
symbole à l’aide de la directive #ifndef :
#ifndef symbole
partie-du-programme-1
#else condition-2
partie-du-programme-2
#endif

• On rencontre beaucoup cette directive dans le cadre de la


compilation modulaire puisqu’elle permet de s’assurer que
les définitions d’instructions dans un fichier d’en-tête ne
seront effectuées qu’une seule fois.

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

L’opérateur defined
• L’opérateur defined est un opérateur spécial : il ne peut
être utilisé que dans le contexte d’une commande #if ou
#elif.
• Il peut être utilisé sous l’une des deux formes suivantes :
– defined nom
– defined ( nom )
• Il délivre la valeur 1 si nom est une macro définie, et la
valeur 0 sinon. L’intérêt de cet opérateur est de permettre
d’écrire des tests portant sur la définition de plusieurs
macros, alors que #ifdef ne peut en tester qu’une :
#if defined(SOLARIS) || defined(SYSV)

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La commande #error
• La commande #error a la syntaxe suivante :
#error chaine

• La rencontre de cette commande provoquera l’émission


d’un message d’erreur comprenant la chaine. Cette
commande a pour utilité de capturer à la compilation des
conditions qui font que le programme ne peut pas
s’exécuter sur cette plate-forme. Voici un exemple où on
teste que la taille des entiers est suffisante :
#include <limits.h>
#if INT_MAX < 1000000
#error "Entiers trop petits sur cette machine"
#endif

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La programmation modulaire
• Dès que l’on écrit un programme de taille importante ou
destiné à être utilisé et maintenu par d’autres personnes, il
est indispensable de se fixer un certain nombre de règles
d’écriture.

• En particulier, il est nécessaire de fractionner le


programme en plusieurs fichiers sources, que l’on compile
séparément.

• Ces règles d’écriture ont pour objectifs de rendre un


programme lisible, portable, réutilisable mais surtout facile
à maintenir et à modifier.
Pr. Abdessamad JARRAR SMI S4
Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La programmation modulaire
• L’idée est alors de regrouper dans un même fichier les
instructions implémentant des fonctionnalités similaires.

• par exemple, tab_management.c contiendra une


bibliothèque de fonctions gérant les tableaux,
file_management.c fournira une bibliothèque de fonctions
gérant les fichiers dans le contexte du projet effectué et
main.c contiendra la définition de la fonction main.

• La programmation modulaire consiste alors à opérer les


manipulations nécessaires permettant de lier ces fichiers.

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Principes élémentaires
• Trois principes essentiels doivent guider l’écriture d’un
programme C. Ces principes s’appliquent en fait dans le cas
général du génie logiciel.
1. L’abstraction des constantes littérales
L’utilisation explicite de constantes littérales dans le corps d’une
fonction rend les modifications et la maintenance difficiles. Des
instructions comme : fopen("mon_fichier", "r");
perimetre = 2 * 3.14 * rayon;
sont à proscrire (il faudrait définir des constantes fournissant le nom
du fichier ou la valeur de Pi). Sauf cas très particuliers, les constantes
doivent être définies comme des constantes symboliques au moyen
de la directive #define.

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Principes élémentaires
2. La factorisation du code
Le but est d’éviter les duplications de code. La présence d’une même
portion de code à plusieurs endroits du programme est un obstacle à
d’éventuelles modifications. Les fonctions doivent donc être
systématiquement utilisées pour éviter la duplication de code. Il ne
faut pas craindre de définir une multitude de fonctions de petite
taille.
3. La fragmentation du code
Pour des raisons de lisibilité, il est pertinent de découper un
programme en plusieurs fichiers. En plus, cette règle permet de
réutiliser facilement une partie du code pour d’autres applications.
On sépare alors le programme en modules, chaque module
implémentant des fonctions sur un thème similaire et qui se
traduiront physiquement par deux fichiers :
Pr. Abdessamad JARRAR SMI S4
Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Principes élémentaires
a. un fichier en-tête (on dit aussi de header) ayant l’extension .h et
contenant le prototype des fonctions principales implémentées dans
ce module.
b. un fichier source ayant l’extension .c contenant non seulement le
corps des fonctions déclarées dans le fichier en-tête mais également
celui des fonctions intermédiaires éventuellement utilisées. Ce fichier inclut
évidemment le fichier en-tête par le biais de la directive #include.
• L’exemple suivant illustre ce principe sur un programme
qui saisit deux entiers au clavier et affiche leur produit.
Sur cet exemple, il a été choisi de définir un module
arithmétique qui fournit la fonction effectuant le produit.
Ce module est donc implémenté dans les fichiers
arithmétique.h et arithmétique.c.

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Principes élémentaires (Exemple)


• fichier main.c qui permet de saisir 2 entiers et affiche leur
produit:
#include <stdlib.h>
#include <stdio.h>
#include "arithmetique.h" // Le module d’arithmetique

int main(void) {
int a, b, c;
scanf("%d",&a);
scanf("%d",&b);
c = produit(a,b); // appel de la fonction d´efinie dans arithmetique.h
printf("\nle produit vaut %d\n",c);
return EXIT_SUCCESS;
}

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Principes élémentaires (Exemple)


• fichier arithmetique.h qui gère les opérations
arithmétiques sur les entiers (Ici, seul le produit est
implémenté): int produit(int a, int b);

• Fichier arithmetique.c, corps des fonctions définies dans


arithmetique.h: #include "arithmetique.h"
int produit(int a, int b) {
return(a * b);
}

• Remarquer que c’est exactement la procédure utilisée


pour les fonctions de la librairie standard : les fichiers en-
tête .h de la librairie standard sont constitués de
déclarations de fonctions et de définitions de constantes
symboliques et le corps des fonctions en lui-même est
séparé.
Pr. Abdessamad JARRAR SMI S4
Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Erreur d’inclusions multiples


• Une dernière règle, très importante, consiste è éviter les
erreurs de double définition de fonctions qui se traduise à
l’édition de lien par le message suivant (avec gcc) :
toto.o:toto.c multiple definition of ’...’
toto.o: first defined here

• Cette erreur se produit en général lors de l’inclusion


multiple d’un même fichier en-tête.
• Pour éviter cela, la méthode consiste à définir une
constante à la première inclusion du fichier en-tête et
d’ignorer le contenu de celui-ci si la constante a déjà été
définie (donc si on a déjà fait une inclusion).

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Erreur d’inclusions multiples


• Pour cela, on utilise la directive #ifndef. Il est recommandé
d’appeler la constante __TOTO_H pour le fichier toto.h En
appliquant cette règle, le fichier arithmetique.h de
l’exemple précédent devient :
#ifndef __ARITHMETIQUE_H
#define __ARITHMETIQUE_H
int produit(int a, int b);
#endif

• Il est FORTEMENT recommandé de TOUJOURS appliquer


cette dernière règle dès lors que vous créez un fichier de
header !

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

La compilation séparée
• Ce n’est pas le tout de bien fragmenter son code en
plusieurs fichiers, encore faut-il les compiler pour obtenir
un exécutable.
• La méthode consiste à générer un fichier objet par module
(option -c de gcc) :
gcc -O3 -Wall -I. -c module_1.c
gcc -O3 -Wall -I. -c module_2.c
...
gcc -O3 -Wall -I. -c module_n.c

• Ces commandes génèrent n fichiers objets module_i.o.

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Les directives du préprocesseur


• L’option -I de gcc permet d’ajouter un répertoire en
première position de la liste des répertoires où sont
cherchés les fichiers en-tête. Cette option est utile lorsque,
pour des raisons de lisibilité dans l’architecture des fichiers
sources, un répertoire Include est créé pour contenir tous
les fichiers en-tête du programme. La compilation avec gcc
comportera alors l’option -IInclude.

• Une passe d’édition de lien entre ces fichiers objets en


ensuite nécessaire pour générer l’exécutable final toto.exe:
gcc -o toto.exe module_1.o module_2.o ... module_n.o

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Résumé des règles de programmation


modulaire
• Découper le programme en modules implémentant des
fonctions similaires.
• Chaque module se traduit en 2 fichiers sources :
– un fichier en-tête module.h qui définit son interface, c’est à dire le
prototype des fonctions du module qui sont exportées. Plus
précisément, ce fichier se compose :
• Des déclarations des fonctions d’interface (celles qui sont exportées et donc
utilisées dans d’autres fichiers sources)
• D’éventuelles définitions de constantes symboliques et de macros.
• D’éventuelles directives au préprocesseur (inclusion d’autres fichiers,
compilation conditionnelle).
– Ce fichier doit respecter également les conventions d’écritures
proposées au précédemment pour éviter les erreurs de
définitions multiples.
Pr. Abdessamad JARRAR SMI S4
Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Résumé des règles de programmation


modulaire
– un fichier module.c contenant le corps des fonctions
implémentées dans ce module. Ce fichier se compose :
• De variables globales qui ne sont utilisées que dans le fichier module.c ;
• Du corps des fonctions d’interface dont la déclaration se trouve dans
module.h ;
• D’éventuelles fonctions locales à module.c.
• Le fichier module.h est inclus dans le fichier module.c (via
la directive #include) et dans tous les autres fichiers qui
font appel aux fonctions exportées par ce module.
• Chaque module est compilé pour générer l’exécutable
final.
• Ces règles s’ajoutent aux règles générales d’écriture de
programmes C abordées précédemment.
Pr. Abdessamad JARRAR SMI S4
Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Exercices.
Exercice 1 : Ecrire un programme qui affiche un menu puis
demande à l’utilisateur de choisir une opération
arithmétique élémentaire (+, -, x, /) ou une fonction
mathématique (cos, sin, tan, exp, ln). Ensuite, le programme
demande la saisie des valeurs nécessaire
pour le calcul et affiche le résultat.
Vous devez créer les fichiers suivant:
Main.c, operation_arith.c, operation_arith.h,
Operation_math.c, operation_math.h

Pr. Abdessamad JARRAR SMI S4


Chapitre 6 : Compilation séparée et Directives du Préprocesseur.

Fin de ce chapitre

Avez – vous des questions ?

Pr. Abdessamad JARRAR SMI S4

Vous aimerez peut-être aussi