SlideShare une entreprise Scribd logo
INTRODUCTION
  q   1 SYMBOLES
  q   2 C, la naissance d'un langage de programmation
      portable
        r  2.1 Historique
        r  2.2 Avantages
        r  2.3 Désavantages
             s  2.3.1 efficience et compréhensibilité :
             s  2.3.2 portabilité et bibliothèques de fonctions
             s  2.3.3 discipline de programmation


1. SYMBOLES

Explication des symboles utilisés dans le texte:




Conseil !




Mauvaise solution !
Bonne solution !




Attention !

Piège dangereux !




Attention ! Piège fréquent ! ou Remarque importante !




Solution incorrecte ! Peut mener à des erreurs.




Solution correcte !




Fonction ou opération non portable selon le standard ANSI-
C.

2. C, la naissance d'un langage de programmation portable
2.1 Historique

Dans les dernières années, aucun langage de programmation
n'a pu se vanter d'une croissance en popularité comparable à
celle de C et de son jeune frère C++. L'étonnant dans ce fait
est que le langage C n'est pas un nouveau-né dans le monde
informatique, mais qu'il trouve ses sources en 1972 dans les
'Bell Laboratories': Pour développer une version portable du
système d'exploitation UNIX, Dennis M. Ritchie a conçu ce
langage de programmation structuré, mais très 'près' de la
machine.

K&R-C

En 1978, le duo Brian W. Kernighan / Dennis M. Ritchie a
publié la définition classique du langage C (connue sous le
nom de standard K&R-C ) dans un livre intitulé 'The C
Programming Language'.

ANSI-C

Le succès des années qui suivaient et le développement de
compilateurs C par d'autres maisons ont rendu nécessaire la
définition d'un standard actualisé et plus précis. En 1983, le
'American National Standards Institute' (ANSI) chargeait une
commission de mettre au point 'une définition explicite et
indépendante de la machine pour le langage C', qui devrait
quand même conserver l'esprit du langage. Le résultat était le
standard ANSI-C. La seconde édition du livre 'The C
Programming Language', parue en 1988, respecte tout à fait
le standard ANSI-C et elle est devenue par la suite, la 'bible'
des programmeurs en C.

2.2 Avantages
Le grand succès du langage C s'explique par les avantages
suivants; C est un langage:

(1) universel :

C n'est pas orienté vers un domaine d'applications spéciales,
comme par exemple FORTRAN (applications scientifiques et
techniques) ou COBOL (applications commerciales ou
traitant de grandes quantités de données).

(2) compact :

C est basé sur un noyau de fonctions et d'opérateurs limité,
qui permet la formulation d'expressions simples, mais
efficaces.

(3) moderne :

C est un langage structuré, déclaratif et récursif; il offre des
structures de contrôle et de déclaration comparables à celles
des autres grands langages de ce temps (FORTRAN,
ALGOL68, PASCAL).

(4) près de la machine :

comme C a été développé en premier lieu pour programmer
le système d'exploitation UNIX, il offre des opérateurs qui
sont très proches de ceux du langage machine et des
fonctions qui permettent un accès simple et direct aux
fonctions internes de l'ordinateur (p.ex: la gestion de la
mémoire).

(5) rapide :
comme C permet d'utiliser des expressions et des opérateurs
qui sont très proches du langage machine, il est possible de
développer des programmes efficients et rapides.

(6) indépendant de la machine :

bien que C soit un langage près de la machine, il peut être
utilisé sur n'importe quel système en possession d'un
compilateur C. Au début C était surtout le langage des
systèmes travaillant sous UNIX, aujourd'hui C est devenu le
langage de programmation standard dans le domaine des
micro-ordinateurs.

(7) portable :

en respectant le standard ANSI-C, il est possible d'utiliser le
même programme sur tout autre système (autre hardware,
autre système d'exploitation), simplement en le recompilant.

(8) extensible :

C ne se compose pas seulement des fonctions standard; le
langage est animé par des bibliothèques de fonctions privées
ou livrées par de nombreuses maisons de développement.

2.3 Désavantages

Évidemment, rien n'est parfait. Jetons un petit coup d'oeil sur
le revers de la médaille:

 2.3.1 efficience et compréhensibilité :

En C, nous avons la possibilité d'utiliser des expressions
compactes et efficientes. D'autre part, nos programmes
doivent rester compréhensibles pour nous-mêmes et pour
d'autres. Comme nous allons le constater sur les exemples
suivants, ces deux exigences peuvent se contredire
réciproquement.

Exemple 1

Les deux lignes suivantes impriment les N premiers éléments
d'un tableau A[ ], en insérant un espace entre les éléments et
en commençant une nouvelle ligne après chaque dixième
élément:



for (i=0; i<n; i++)
    printf("%6d%c", A[i], (i%10==9)?'n':' ');

Cette notation est très pratique, mais plutôt intimidante pour
un débutant. L'autre variante est plus lisible, mais elle ne
profite pas des avantages du langage C:

for(I=0;I<N;I=I+1)
   {
      printf("%6d", A[I]);
      if ((I%10)==9)
          printf("n");
      else
          printf(" ");
    }


Exemple 2

La fonction copietab() copie les éléments d'une chaîne de
caractères T[ ] dans une autre chaîne de caractères S[ ]. Voici
d'abord la version 'simili-Pascal' :



void copietab(char S[], char T[])
 { int I;
   I=0;
   while (T[I] != '0')
     {
       S[I] = T[I];
       I = I+1;
     }
   S[I] = '0';
 }

Cette définition de la fonction est valable en C, mais en
pratique elle ne serait jamais programmée ainsi. En utilisant
les possibilités de C, un programmeur expérimenté préfère la
solution suivante:



void copietab(char *S, char *T)
 {
   while (*S++ = *T++);
 }

La deuxième formulation de cette fonction est élégante,
compacte, efficace et la traduction en langage machine
fournit un code très rapide...; mais bien que cette manière de
résoudre les problèmes soit le cas normal en C, il n'est pas si
évident de suivre le raisonnement.
Conclusions

Bien entendu, dans les deux exemples ci-dessus, les
formulations 'courtes' représentent le bon style dans C et
sont de loin préférables aux deux autres. Nous constatons
donc que:

la programmation efficiente en C nécessite beaucoup
d'expérience et n'est pas facilement accessible à des
débutants.

sans commentaires ou explications, les programmes peuvent
devenir incompréhensibles, donc inutilisables.

 2.3.2 portabilité et bibliothèques de fonctions

Les limites de la portabilité

La portabilité est l'un des avantages les plus importants de
C: en écrivant des programmes qui respectent le standard
ANSI-C, nous pouvons les utiliser sur n'importe quelle
machine possédant un compilateur ANSI-C. D'autre part, le
répertoire des fonctions ANSI-C est assez limité. Si un
programmeur désire faire appel à une fonction spécifique de
la machine (p.ex: utiliser une carte graphique spéciale), il est
assisté par une foule de fonctions 'préfabriquées', mais il doit
être conscient qu'il risque de perdre la portabilité. Ainsi, il
devient évident que les avantages d'un programme portable
doivent être payés par la restriction des moyens de
programmation.

2.3.3 discipline de programmation

Les dangers de C
Nous voici arrivés à un point crucial: C est un langage près
de la machine, donc dangereux et bien que C soit un langage
de programmation structuré, il ne nous force pas à adopter
un certain style de programmation (comme p.ex. Pascal).
Dans un certain sens, tout est permis et la tentation de
programmer du 'code spaghetti' est grande. (Même la
commande 'goto', si redoutée par les puristes ne manque pas
en C). Le programmeur a donc beaucoup de libertés, mais
aussi des responsabilités: il doit veiller lui-même à adopter
un style de programmation propre, solide et compréhensible.

Remarque

Au fil de l'introduction du langage C, ce manuel contiendra
quelques recommandations au sujet de l'utilisation des
différents moyens de programmation. Il est impossible de
donner des règles universelles à ce sujet, mais le conseil
suivant est valable pour tous les langages de
programmation:




    Si, après avoir lu uniquement les commentaires d'un
          programme, vous n'en comprenez pas le
         fonctionnement, alors jetez le tout de suite !
Chapitre 1: L'ENVIRONNEMENT ET LES
                         BIBLIOTHEQUES

Pour le travail pratique en C, il faut utiliser un compilateur et un
éditeur facile à utiliser. A titre d'exemple je décris ici l'utilisation de
l'environnement Borland C++ (Version 3.1). Ce programme nous
offre une surface de programmation confortable et rapide. Tout
autre compilateur qui permet la programmation selon le standard
ANSI-C fait aussi bien l'affaire.

   q   1.1. Description de l'environnement Borland C++
         r  1.1.1. Les menus
         r  1.1.2. Les nouveautés
   q   1.2. Sélectionner le compilateur ANSI-C
   q   1.3. Les bibliothèques de fonctions prédéfinies
         r  Schéma: Bibliothèques de fonctions et compilation



1.1. Description de l'environnement Borland C++

Borland C++ est une implémentation complète du standard C++
AT&T (Version 2.1). Le compilateur Borland C++ est capable de
produire du code C 'pur' selon la définition Kernighan & Ritchie ou
selon le standard ANSI-C. D'autre part, Borland C++ nous offre toute
une série de bibliothèques, qui (parfois aux dépens de la portabilité)
nous permettent d'exploiter les capacités du PC.

Ce chapitre se limite à une brève description des menus de Borland
C++.
1.1.1. Les menus

FILE gestion des fichiers, retour au DOS

EDIT commandes d'édition du texte

SEARCH recherche d'un texte, d'une déclaration de fonction, de la
position d'une erreur dans le programme

RUN exécution d'un programme en entier ou par parties

COMPILE traduction et/ou enchaînement (link) des programmes

DEBUG détection d'erreurs en inspectant les données, en insérant
des points d'observation (watch) ou en insérant des points d'arrêt
(breakpoints)

PROJECT gestion de projets

OPTIONS changement et sauvetage des réglages par défaut :

* des menus
* des options du compilateur
* de l'environnement
WINDOW visualisation et disposition des différentes fenêtres
(Message, Output, Watch, User, Project) sur l'écran

HELP système d'aide



1.1.2. Les nouveautés

a) L'interface utilisateur :
L'environnement Borland C++ nous permet l'utilisation confortable
de plusieurs fenêtres sur l'écran. L'interaction des données des
différentes fenêtres (Message, Output, Watch, User Screen) a rendu
la recherche d'erreurs très efficace.

b) La gestion de projets multi-fichiers :

En pratique, les programmes sont souvent subdivisés en plusieurs
sous-programmes ou modules, qui peuvent être répartis dans
plusieurs fichiers. L'environnement Borland C++ nous offre un
gestionnaire 'Project Manager' qui détecte automatiquement les
fichiers qui doivent être recompilés et enchaînés après une
modification. Dans nos applications, nous allons uniquement
utiliser des fonctions définies dans le même fichier que notre
programme principal ou prédéfinies dans une bibliothèque
standard; ainsi, nous n'aurons pas besoin de l'option 'Project'.

c) Le système d'aide :

Borland C++ est accompagné d'un programme d'aide puissant qui
est appelé directement à partir de l'éditeur. Le système d'aide peut
donner des informations ponctuelles sur un sujet sélectionné ou
nous laisser choisir un sujet à partir d'une liste alphabétique :

F1 Aide sur l'environnement Borland C++

Shift-F1 Index alphabétique des mots-clefs

Ctrl-F1 Aide sur le mot actif (à la position du curseur)



1.2. Sélectionner le compilateur ANSI-C

Comme nous utilisons l'environnement Borland C++ pour compiler
des programmes correspondant au standard ANSI-C, nous devons
d'abord changer le mode de compilation par défaut :

- Choisir le menu 'compilateur' par : Alt-Options|Compiler
- Choisir le modèle ANSI-C par : Source|ANSI|OK




1.3. Les bibliothèques de fonctions prédéfinies

Utilisation des bibliothèques de fonctions

La pratique en C exige l'utilisation de bibliothèques de fonctions.
Ces bibliothèques sont disponibles dans leur forme précompilée
(extension: .LIB). Pour pouvoir les utiliser, il faut inclure des fichiers
en-tête (header files - extension .H) dans nos programmes. Ces
fichiers contiennent des 'prototypes' des fonctions définies dans les
bibliothèques et créent un lien entre les fonctions précompilées et
nos programmes.

#include

L'instruction #include insère les fichiers en-tête indiqués comme
arguments dans le texte du programme au moment de la
compilation.

Identification des fichiers

Lors de la programmation en Borland C, nous travaillons donc avec
différents types de fichiers qui sont identifiés par leurs extensions:

*.C        fichiers source
*.OBJ      fichiers compilés (versions objet)
*.EXE      fichiers compilés et liés (versions exécutables)
*.LIB      bibliothèques de fonctions précompilées
*.H        fichiers en-tête (header files)

Exemple
Nous avons écrit un programme qui fait appel à des fonctions
mathématiques et des fonctions graphiques prédéfinies. Pour
pouvoir utiliser ces fonctions, le programme a besoin des
bibliothèques:

MATHS.LIB
GRAPHICS.LIB

Nous devons donc inclure les fichiers en-tête correspondants dans
le code source de notre programme à l'aide des instructions:

     #include <math.h>
     #include <graphics.h>

Après la compilation, les fonctions precompilées des bibliothèques
seront ajoutées à notre programme pour former une version
exécutable du programme (voir schéma).

Remarque: La bibliothèque de fonctions graphics.h est spécifique
aux fonctionnalités du PC et n'est pas incluse dans le standard
ANSI-C.

Schéma: Bibliothèques de fonctions et compilation
Cours langage-c
Cours langage-c
Chapitre 2: NOTIONS DE BASE


avant de pouvoir comprendre ou même écrire des programmes, il faut connaître la
composition des programmes dans le langage de programmation. Dans ce chapitre,
nous allons discuter un petit programme en mettant en évidence les structures
fondamentales d'un programme en C.

  q   2.1. Hello C !
  q   2.2. Les composantes d'un programme en C
        r  2.2.1. Les fonctions
        r  2.2.2. La fonction main
        r  2.2.3. Les variables
        r  2.2.4. Les identificateurs
        r  2.2.5. Les commentaires
  q   2.3. Discussion de l'exemple 'Hello_World'



2.1. Hello C !

Suivons la tradition et commençons la découverte de C par l'inévitable programme
'hello world'. Ce programme ne fait rien d'autre qu'imprimer les mots suivants sur
l'écran:




      Comparons d'abord la définition du programme en C avec celle en langage
                                    algorithmique.

HELLO_WORLD en langage algorithmique



      programme HELLO_WORLD
        (* Notre premier programme en C *)
        écrire "hello, world"
      fprogramme


HELLO_WORLD       en   C
#include <stdio.h>
     main()
      { /* Notre premier programme en C */
        printf("hello, worldn");
        return 0;
      }


Dans la suite du chapitre, nous allons discuter les détails de cette implémentation.



2.2. Les composantes d'un programme en C

Les programmes en C sont composés essentiellement de fonctions et de variables.
Pour la pratique, il est donc indispensable de se familiariser avec les
caractéristiques fondamentales de ces éléments.



2.2.1. Les fonctions

En C, le programme principal et les sous-programmes sont définis comme
fonctions. Il n'existe pas de structures spéciales pour le programme principal ni les
procédures (comme en langage algorithmique).

Le programme principal étant aussi une 'fonction', nous devons nous intéresser dès
le début à la définition et aux caractéristiques des fonctions en C. Commençons par
comparer la syntaxe de la définition d'une fonction en C avec celle d'une fonction
en langage algorithmique:

Définition d'une fonction en langage algorithmique



     fonction <NomFonct> (<NomPar1>, <NomPar2>,
                             ..........) :<TypeRés>
        | <déclarations des paramètres>
        | <déclarations locales>
        | <instructions>
     ffonction



Définition d'une fonction en C
<TypeRés><NomFonct> (<TypePar1><NomPar1>,
                              <TypePar2> <NomPar2>, ... )
     {
       <déclarations locales>
       <instructions>
     }




En C, une fonction est définie par:

une ligne déclarative qui contient :

<TypeRés> le type du résultat de la fonction
<NomFonct> le nom de la fonction
<TypePar1> <NomPar1>, <TypePar2> <NomPar2>, ...
les types et les noms des paramètres de la fonction

un bloc d'instructions délimité par des accolades { }, contenant:

<déclarations locales> - les déclarations des données locales (c.-à-d.: des données
qui sont uniquement connues à l'intérieur de la fonction)
<instructions> - la liste des instructions qui définit l'action qui doit être exécutée

Résultat d'une fonction

Par définition, toute fonction en C fournit un résultat dont le type doit être défini. Si
aucun type n'est défini explicitement, C suppose par défaut que le type du résultat
est int (integer).

Le retour du résultat se fait en général à la fin de la fonction par l'instruction return.

Le type d'une fonction qui ne fournit pas de résultat (comme les procédures en
langage algorithmique ), est déclaré comme void (vide).

Paramètres d'une fonction

La définition des paramètres (arguments) d'une fonction est placée entre
parenthèses ( ) derrière le nom de la fonction. Si une fonction n'a pas besoin de
paramètres, les parenthèses restent vides ou contiennent le mot void. La fonction
minimale qui ne fait rien et qui ne fournit aucun résultat est alors:

     void dummy()
     {}
Instructions

En C, toute instruction simple est terminée par un point-virgule ; (même si elle se
trouve en dernière position dans un bloc d'instructions). Par exemple:

     printf("hello, world
     n");



2.2.2. La fonction main

La fonction main est la fonction principale des programmes en C. Elle se trouve
obligatoirement dans tous les programmes. L'exécution d'un programme entraîne
automatiquement l'appel de la fonction main.

Dans les premiers chapitres, nous allons simplement 'traduire' la structure
programme du langage algorithmique par une définition équivalente de la fonction
main :

Définition du programme principal en langage algorithmique



     programme <NomProgramme>
        <déclarations>
        <instructions>
     fprogramme




Définition de la fonction main enC

     main()
     { <déclarations>
       <instructions>
       return 0;
     }

Résultat de main

En principe tout programme devrait retourner une valeur comme code d'erreur à
son environnement. Par conséquent, le type résultat demain est toujours int. En
général, le type de main n'est pas déclaré explicitement, puisque c'est le type par
défaut. Nous allons terminer nos programmes par l'instruction:
return 0;



qui indique à l'environnement que le programme s'est terminé avec succès, sans
anomalies ou erreurs fatales.

Paramètres de main

- Si la liste des paramètres de la fonction main est vide, il est d'usage de la déclarer
par ().

- Si nous utilisons des fonctions prédéfinies (par exemple: printf), il faut faire
précéder la définition de main par les instructions #include correspondantes.

Remarque avancée:
Il est possible de faire passer des arguments de la ligne de commande à un
programme. Dans ce cas, la liste des paramètres doit contenir les déclarations
correspondantes. Dans notre cours, nous n'allons pas utiliser des arguments de la
ligne de commande. Ainsi la liste des paramètres de la fonction main sera vide
(void) dans tous nos exemples et nous pourrons employer la déclaration suivante
qui fait usage des valeurs par défaut:

     main()
     { ... }



2.2.3. Les variables

Les variables contiennent les valeurs qui sont utilisées pendant l'exécution du
programme. Les noms des variables sont des identificateurs quelconques (voir
2.2.4 ). Les différents types de variables simples et les opérateurs admissibles sont
discutés au chapitre 3.



2.2.4. Les identificateurs

Les noms des fonctions et des variables en C sont composés d'une suite de lettres
et de chiffres. Le premier caractère doit être une lettre. Le symbole '_' est aussi
considéré comme une lettre.

* L'ensemble des symboles utilisables est donc:
{0,1,2,...,9,A,B,...,Z,_,a,b,...,z}

* Le premier caractère doit être une lettre (ou le symbole '_') - voir aussi Remarque -.

* C distingue les majuscules et les minuscules, ainsi:

     ' Nom_de_variable' est différent de

     ' nom_de_variable'



* La longueur des identificateurs n'est pas limitée, mais C distingue 'seulement' les
31 premiers caractères. - voir aussi Remarque -

Remarque:

Il est déconseillé d'utiliser le symbole '_' comme premier caractere pour un
identificateur, car il est souvent employé pour définir les variables globales de
l'environment C.



Exemples



Identificateurs corrects:             Identificateurs incorrects:
nom1                                  1nom
nom_2                                 nom.2
_nom_3                                -nom-3
Nom_de_variable                       Nom de variable
deuxieme_choix                        deuxième_choix
mot_francais                          mot_français




2.2.5. Les commentaires

Un commentaire commence toujours par les deux symboles '/*' et se termine par les
symboles '*/'. Il est interdit d'utiliser des commentaires imbriqués.
Exemples



     /* Ceci est un commentaire correct */
     /* Ceci est /* évidemment */ défendu */



2.3. Discussion de l'exemple 'Hello_World'

Reprenons le programme 'Hello_World' et retrouvons les particularités d'un
programme en C.

HELLO_WORLD en C



     #include <stdio.h>
     main()
        /* Notre premier programme en C
     */
      {
         printf("hello, worldn");
         return 0;
       }

Discussion

- La fonction main ne reçoit pas de données, donc la liste des paramètres est vide.

- La fonction main fournit un code d'erreur numérique à l'environnement, donc le
type du résultat est int et n'a pas besoin d'être déclaré explicitement.

- Le programme ne contient pas de variables, donc le bloc de déclarations est vide.

- La fonction main contient deux instructions:

* l'appel de la fonction printf avec l'argument "hello, worldn";

Effet: Afficher la chaîne de caractères "hello worldn".

* la commande return avec l'argument 0;

Effet: Retourner la valeur 0 comme code d'erreur à l'environnement.

- L'argument de la fonction printf est une chaîne de caractères indiquée entre les
guillements. Une telle suite de caractères est appelée chaîne de caractères
constante (string constant).
- La suite de symboles 'n' à la fin de la chaîne de caractères "hello, worldn" est la
notation C pour 'passage à la ligne' (angl: new line). En C, il existe plusieurs
couples de symboles qui contrôlent l'affichage ou l'impression de texte. Ces
séquences d'échappement sont toujours précédées par le caractère d'échappement
''.

printf et la bibliothèque <stdio>

La fonction printf fait partie de la bibliothèque de fonctions standard <stdio> qui
gère les entrées et les sorties de données. La première ligne du programme:

     #include <stdio.h>



instruit le compilateur d'inclure le fichier en-tête 'STDIO.H' dans le texte du
programme. Le fichier 'STDIO.H' contient les informations nécessaires pour pouvoir
utiliser les fonctions de la bibliothèque standard <stdio>(voir chapitre 1.3).
Chapitre 3: TYPES DE BASE, OPÉRATEURS ET EXPRESSIONS
                            Récapitulation du vocabulaire

Les variables et les constantes sont les données principales qui peuvent être
manipulées par un programme. Les déclarations introduisent les variables qui sont
utilisées, fixent leur type et parfois aussi leur valeur de départ. Les opérateurs
contrôlent les actions que subissent les valeurs des données. Pour produire de
nouvelles valeurs, les variables et les constantes peuvent être combinées à l'aide
des opérateurs dans des expressions. Le type d'une donnée détermine l'ensemble
des valeurs admissibles, le nombre d'octets à réserver en mémoire et l'ensemble
des opérateurs qui peuvent y être appliqués.

                                      Motivation

La grande flexibilité de C nous permet d'utiliser des opérandes de différents types
dans un même calcul. Cet avantage peut se transformer dans un terrible piège si
nous ne prévoyons pas correctement les effets secondaires d'une telle opération
(conversions de type automatiques, arrondissements, etc.). Une étude minutieuse
de ce chapitre peut donc aider à éviter des phénomènes parfois 'inexplicables' ...

  q   3.1. Les types simples
        r  3.1.1. Les types entiers
        r  3.1.2. Les types rationnels
  q   3.2. La déclaration des variables simples
        r  3.2.1. Les constantes numériques
             s  - Les constantes entières
             s  - Les constantes rationnelles
             s  - Les caractères constants
        r  3.2.2. Initialisation des variables
  q   3.3. Les opérateurs standard
        r  3.3.1. Les opérateurs connus
        r  3.3.2. Les opérateurs particuliers de C
             s  - Les opérateurs d'affectation
             s  - Opérateurs d'incrémentation et de décrémentation
  q   3.4. Les expressions et les instructions
  q   3.5. Les priorités des opérateurs
  q   3.6. Les fonctions arithmétiques standard
  q   3.7. Les conversions de type
        r  3.7.1. Les conversions de type automatiques
r   3.7.2. Les conversions de type forcées (casting)



3.1. Les types simples

Ensembles de nombres et leur représentation

En mathématiques, nous distinguons divers ensembles de nombres:

* l'ensemble des entiers naturels IN,

* l'ensemble des entiers relatifs ZZ,

*   l'ensemble des rationnels Q,

*   l'ensemble des réels IR.

En mathématiques l'ordre de grandeur des nombres est illimité et les rationnels
peuvent être exprimés sans perte de précision.

Un ordinateur ne peut traiter aisément que des nombres entiers d'une taille limitée.
Il utilise le système binaire pour calculer et sauvegarder ces nombres. Ce n'est que
par des astuces de calcul et de représentation que l'ordinateur obtient des valeurs
correctement approchées des entiers très grands, des réels ou des rationnels à
partie décimale infinie.

Les charges du programmeur

Même un programmeur utilisant C ne doit pas connaître tous les détails des
méthodes de codage et de calcul, il doit quand même être capable de:

- choisir un type numérique approprié à un problème donné;
c.-à-d.: trouver un optimum de précision, de temps de calcul et d'espace à réserver
en mémoire

- choisir un type approprié pour la représentation sur l'écran

- prévoir le type résultant d'une opération entre différents types numériques;
c.-à-d.: connaître les transformations automatiques de type que C accomplit lors
des calculs (voir 3.7.1).

- prévoir et optimiser la précision des résultats intermédiaires au cours d'un calcul
complexe;
c.-à-d.: changer si nécessaire l'ordre des opérations ou forcer l'ordinateur à utiliser
un type numérique mieux adapté (casting: voir 3.7.2).

3.1.1. Les types entiers
Avant de pouvoir utiliser une variable, nous devons nous intéresser à deux
caractéristiques de son type numérique:

(1) le domaine des valeurs admissibles

(2) le nombre d'octets qui est réservé pour une variable

Le tableau suivant résume les caractéristiques des types numériques entiers de C :

 définition      description              min                 max    nombre
                                                                     d'octets

char          caractère          -128                 127                   1

short         entier court       -32768               32767                 2

int           entier standard    -32768               32767                 2

long          entier long        -2147483648          2147483647            4


- char : caractère

Une variable du type char peut contenir une valeur entre -128 et 127 et elle peut
subir les mêmes opérations que les variables du type short, int ou long.

- int : entier standard

Sur chaque machine, le type int est le type de base pour les calculs avec les
entiers. Le codage des variables du type int est donc dépendant de la machine. Sur
les IBM-PC sous MS-DOS, une variable du type int est codée dans deux octets.

- short : entier court

Le type short est en général codé dans 2 octets. Comme une variable int occupe
aussi 2 octets sur notre système, le type short devient seulement nécessaire, si on
veut utiliser le même programme sur d'autres machines, sur lesquelles le type
standard des entiers n'est pas forcément 2 octets.

- Les modificateurs signed/unsigned:

Si on ajoute le préfixe unsigned à la définition d'un type de variables entières, les
domaines des variables sont déplacés comme suit:

                                                                      nombre
        définition             description      min         max
                                                                      d'octets
unsigned char              caractère         0      255                     1


unsigned short             entier court      0      65535                   2


unsigned int               entier standard 0        65535                   2


unsigned long              entier long       0      4294967295              4



Remarques

1. Le calcul avec des entiers définis comme unsigned correspond à l'arithmétique
modulo 2n. Ainsi, en utilisant une variable X du type unsigned short, nous
obtenons le résultat suivant:



Affectation :     X = 65500 + 100

Résultat :        X = 64                       /* [+216] */




2. Par défaut, les types entiers short, int, long sont munis d'un signe. Le
type par défaut de char est dépendant du compilateur et peut être signed ou
unsigned. Ainsi, l'attribut signed a seulement un sens en liaison avec char et peut
forcer la machine à utiliser la représentation des caractères avec signe (qui n'est
cependant pas très usuelle).

3. Les valeurs limites des differents types sont indiquées dans le fichier header
<limits.h>.

4. En principe, on peut dire que sizeof(short) <= sizeof(int) <= sizeof(long).Ainsi sur
certaine architecture on peut avoir

short = 2 octets, int = 2 octets, long = 4 octets

et sur d'autre

short = 2 octets, int = 4 octets, long = 4 octets

( Il sera intéressant de voir l'implementation dans un environnement 64 bits! )
3.1.2. Les types rationnels

En informatique, les rationnels sont souvent appelés des 'flottants'. Ce terme vient
de 'en virgule flottante' et trouve sa racine dans la notation traditionnelle des
rationnels:



                                  <+|-> <mantisse> * 10<exposant>
<+|->            est le signe positif ou négatif du nombre
                 est un décimal positif avec un seul chiffre devant la
<mantisse>
                 virgule.
<exposant> est un entier relatif

En C, nous avons le choix entre trois types de rationnels: float, double et long
double. Dans le tableau ci-dessous, vous trouverez leurs caractéristiques:

                      représentent les valeurs minimales et
min et max            maximalespositives. Les valeurs négatives peuvent
                      varier dans les mêmes domaines.

mantisse              indique le nombre de chiffres significatifs de la mantisse.




                                                                                    nombre
                      précision     mantisse           min               max
    définition                                                                      d'octets

                                       6                                               4
float               simple                     3.4 *10-38      3.4 * 1038
                                      15                                               8
double              double                     1.7 *10-308     1.7 *10308

                                      19                                              10
long double         suppl.                     3.4 *10-4932    1.1 *104932



Remarque avancée

Les détails de l'implémentation sont indiqués dans le fichier header <float.h>.

3.2. La déclaration des variables simples

Maintenant que nous connaissons les principaux types de variables, il nous faut
encore la syntaxe pour leur déclaration:

Déclaration de variables en langage algorithmique



     <Type> <NomVar1>,<NomVar2>,...,<NomVarN>

Déclaration de variables en C



     <Type> <NomVar1>,<NomVar2>,...,<NomVarN>;

Prenons quelques déclarations du langage descriptif,

     entier    COMPTEUR,X,Y
     réel      HAUTEUR,LARGEUR,MASSE_ATOMIQUE
     caractère TOUCHE
     booléen T_PRESSEE

et traduisons-les en des déclarations du langage C :

     int      compteur, X, Y ;
     float    hauteur, largeur ;
     double   masse_atomique ;
     char     touche;
     int      t_pressee;

Langage algorithmique --> C

En général, nous avons le choix entre plusieurs types et nous devons trouver celui
qui correspond le mieux au domaine et aux valeurs à traiter. Voici quelques règles
générales qui concernent la traduction des déclarations de variables numériques
du langage algorithmique en C :

- La syntaxe des déclarations en C ressemble à celle du langage algorithmique.
Remarquez quand même les points-virgules à la fin des déclarations en C.

entier : Nous avons le choix entre tous les types entiers (inclusivement char) dans
leurs formes signed ou unsigned. Si les nombres deviennent trop grands pour
unsigned long, il faut utiliser un type rationnel (p.ex: double)

réel : Nous pouvons choisir entre les trois types rationnels en observant non
seulement la grandeur maximale de l'exposant, mais plus encore le nombre de
chiffres significatifs de la mantisse.
caractère : Toute variable du type char peut contenir un (seul) caractère. En C, il
faut toujours être conscient que ce 'caractère' n'est autre chose qu'un nombre
correspondant à un code (ici: code ASCII). Ce nombre peut être intégré dans toute
sorte d'opérations algébriques ou logiques ...

chaîne : En C il n'existe pas de type spécial pour chaînes de caractères. Les
moyens de traiter les chaînes de caractères seront décrits au chapitre 8.

booléen : En C il n'existe pas de type spécial pour variables booléennes. Tous les
types de variables numériques peuvent être utilisés pour exprimer des opérations
logiques:

valeur logique faux           <=> valeur numérique zéro
valeur logique vrai           <=> toute valeur différente de zéro



Si l'utilisation d'une variable booléenne est indispensable, le plus naturel
sera d'utiliser une variable du type int.

Les opérations logiques en C retournent toujours des résultats du type int:

0 pour faux
1 pour vrai

3.2.1. Les constantes numériques

En pratique, nous utilisons souvent des valeurs constantes pour calculer, pour
initialiser des variables, pour les comparer aux variables, etc. Dans ces cas,
l'ordinateur doit attribuer un type numérique aux valeurs constantes. Pour pouvoir
prévoir le résultat et le type exact des calculs, il est important pour le programmeur
de connaître les règles selon lesquelles l'ordinateur choisit les types pour les
constantes.

- Les constantes entières

Type automatique

Lors de l'attribution d'un type à une constante entière, C choisit en général la
solution la plus économique:

Le type des constantes entières

* Si possible, les constantes entières obtiennent le type int.

* Si le nombre est trop grand pour int (p.ex: -40000 ou +40000) il aura
automatiquement le type long.
* Si le nombre est trop grand pour long, il aura le type unsigned long.

* Si le nombre est trop grand pour unsigned long, la réaction du programme est
imprévisible.

Type forcé

Si nous voulons forcer l'ordinateur à utiliser un type de notre choix, nous pouvons
employer les suffixes suivants:

     suffixe                   type                     Exemple

u ou U            unsigned (int ou long)         550u
l ou L            long                           123456789L
ul ou UL          unsigned long                  12092UL

Exemples

12345        type int
52000        type long
-2           type int
0            type int
1u           type unsigned int
52000u       type unsigned long
22lu         Erreur !



Base octale et hexadécimale

Il est possible de déclarer des constantes entières en utilisant la base octale ou
hexadécimale:

* Si une constante entière commence par 0 (zéro), alors elle est interprétée en base
octale.

* Si une constante entière commence par 0x ou 0X , alors elle est interprétée en
base hexadécimale.

Exemples



   base                               base
                base octale                             représ. binaire
  décimale                        hexadécimale
100            0144             0X64                1100100
255            0377             0xff                11111111
65536          0200000          0X10000             10000000000000000
12             014              0XC                 1100
4040           07710            0xFC8               111111001000

- Les constantes rationnelles

Les constantes rationnelles peuvent être indiquées:

* en notation décimale, c.-à-d. à l'aide d'un point décimal:

Exemples


123.4 -0.001 1.0

* en notation exponentielle, c.-à-d. à l'aide d'un exposant séparé du nombre
décimal par les caractères 'e' ou 'E':

Exemples

1234e-1 -1E-3 0.01E2

L'ordinateur reconnaît les constantes rationnelles au point décimal ou au
séparateur de l'exposant ('e' ou 'E'). Par défaut, les constantes rationnelles sont du
type double.

Le type des constantes rationnelles

* Sans suffixe, les constantes rationnelles sont du type double.

* Le suffixe f ou F force l'utilisation du type float.

* Le suffixe l ou L force l'utilisation du type long double.

- Les caractères constants

Les constantes qui désignent un (seul) caractère sont toujours indiquées entre des
apostrophes: par exemple 'x'. La valeur d'un caractère constant est le code interne
de ce caractère. Ce code (ici: le code ASCII) est dépendant de la machine.

Les caractères constants peuvent apparaître dans des opérations arithmétiques ou
logiques, mais en général ils sont utilisés pour être comparés à des variables.

Séquences d'échappement
Comme nous l'avons vu au chapitre 2, l'impression et l'affichage de texte peut être
contrôlé à l'aide de séquences d'échappement. Une séquence d'échappement est
un couple de symboles dont le premier est le signe d'échappement ''. Au moment
de la compilation, chaque séquence d'échappement est traduite en un caractère de
contrôle dans le code de la machine. Comme les séquences d'échappement sont
identiques sur toutes les machines, elles nous permettent d'écrire des programmes
portables, c.-à-d.: des programmes qui ont le même effet sur toutes les machines,
indépendemment du code de caractères utilisé.

Séquences d'échappement



a   sonnerie                             trait oblique
b   curseur arrière                   ?   point d'interrogation
t   tabulation                        '   apostrophe
n   nouvelle ligne                    "   guillemets
r   retour au début de ligne          f   saut de page (imprimante)
0   NUL                               v   tabulateur vertical

Le caractère NUL

La constante '0' qui a la valeur numérique zéro (ne pas à confondre avec le
caractère '0' !!) a une signification spéciale dans le traitement et la mémorisation
des chaînes de caractères: En C le caractère '0' définit la fin d'une chaîne de
caractères.

3.2.2. Initialisation des variables

Initialisation

En C, il est possible d'initialiser les variables lors de leur déclaration:

     int MAX = 1023;
     char TAB = 't';
     float X = 1.05e-4;

En utilisant l'attribut const, nous pouvons indiquer que la valeur d'une variable ne
change pas au cours d'un programme:



     const int    MAX = 767;
     const double e = 2.71828182845905;
     const char   NEWLINE = 'n';
3.3. Les opérateurs standard

Affectation en langage algorithmique



     en <NomVariable> ranger <Expression>

Affectation en C



     <NomVariable> = <Expression>;

Exemples d'affectations


- L'affectation avec des valeurs constantes

      Langage algorithmique                     C           Type de la constante

en LONG ranger 141                 LONG = 141;             entière
en PI ranger 3.1415926             PI = 3.1415926;         réelle
en NATION ranger "L"               NATION = 'L';           caractère

- L'affectation avec des valeurs de variables



           Langage algorithmique                                C

en VALEUR ranger X1A                             VALEUR = X1A;
en LETTRE ranger COURRIER                        LETTRE = COURRIER;

- L'affectation avec des valeurs d'expressions

           Langage algorithmique                                 C

en AIRE ranger PI*R2                            AIRE = PI*pow(R,2);
en MOYENNE ranger (A+B)/2                       MOYENNE = (A+B)/2;
en UN ranger sin2(X)+cos2(X)                    UN=pow(sin(X),2)+pow(cos(X),2);
en RES ranger 45+5*X                            RES = 45+5*X;
en PLUSGRAND ranger (X>Y)                       PLUSGRAND = (X>Y);
en CORRECT ranger ('a'='a')                    CORRECT = ('a' == 'a');

Observations

* il n'existe pas de fonction standard en C pour calculer le carré d'une valeur; on
peut se référer à la fonction plus générale pow(x,y) qui calcule xy (voir 3.5.).

* le test d'égalité en C se formule avec deux signes d'égalité== , l'affectation avec
un seul = .

3.3.1. Les opérateurs connus

Avant de nous lancer dans les 'spécialités' du langage C, retrouvons d'abord les
opérateurs correspondant à ceux que nous connaissons déjà en langage
descriptif .

Opérateurs arithmétiques



+    addition
-    soustraction
*    multiplication
/    division (entière et rationnelle!)
% modulo (reste d'une div. entière)

Opérateurs logiques



&&        et logique (and)
||        ou logique (or)
!         négation logique (not)

Opérateurs de comparaison



==                    égal à
!=                    différent de
<, <=, >, >=          plus petit que, ...

Opérations logiques

Les résultats des opérations de comparaison et des opérateurs logiques sont du
type int:

- la valeur 1 correspond à la valeur booléenne vrai

- la valeur 0 correspond à la valeur booléenne faux


Les opérateurs logiques considèrent toute valeur différente de zéro comme vrai et
zéro comme faux:

32 && 2.3                    1
!65.34                       0
0||!(32 > 12)                0

3.3.2. Les opérateurs particuliers de C

- Les opérateurs d'affectation

En pratique, nous retrouvons souvent des affectations comme:

i=i+2

En C, nous utiliserons plutôt la formulation plus compacte:

i += 2

L'opérateur += est un opérateur d'affectation.

Pour la plupart des expressions de la forme:

expr1 = (expr1) op (expr2)

il existe une formulation équivalente qui utilise un opérateur d'affectation:

expr1 op= expr2

Opérateurs d'affectation



+=          ajouter à
-=          diminuer de
*=          multiplier par
/=          diviser par
%=          modulo

Avantages
La formulation avec un opérateur d'affectation est souvent plus près de la logique
humaine:

Un homme dirait <<Ajoute 2 à I>>

plutôt que <<Ajoute 2 à I et écris le résultat dans I>>


Les opérateurs d'affectation peuvent aider le compilateur à générer un code plus
efficient parce que expr1 n'est évalué qu'une seule fois.

Les opérateurs d'affectation deviennent le plus intéressant si expr1 est une
expression complexe. Ceci peut être le cas si nous calculons avec des tableaux.
L'expression:

Element[n*i+j] = Element[n*i+j] * x[j]

peut être formulée de manière plus efficiente et plus claire:

Element[n*i+j] *= x[j]

- Opérateurs d'incrémentation et de décrémentation

Les affectations les plus fréquentes sont du type:

I = I + 1 et I = I - 1

En C, nous disposons de deux opérateurs inhabituels pour ces affectations:

I++ ou ++I           pour l'incrémentation              (augmentation d'une unité)
I-- ou --I           pour la décrémentation             (diminution d'une unité)

Les opérateurs ++ et -- sont employés dans les cas suivants:

incrémenter/décrémenter une variable (p.ex: dans une boucle). Dans ce cas il n'y a
pas de différence entre la notation préfixe (++I --I) et la notation postfixe (I++ I-- ).

incrémenter/décrémenter une variable et en même temps affecter sa valeur à une
autre variable. Dans ce cas, nous devons choisir entre la notation préfixe et
postfixe:



X = I++                  passe d'abord la valeur de I à X et incrémente après
X = I--                  passe d'abord la valeur de I à X et décrémente après
X = ++I                  incrémente d'abord et passe la valeur incrémentée à X
X = --I           décrémente d'abord et passe la valeur décrémentée à X

Exemple

Supposons que la valeur de N est égal à 5:



Incrém. postfixe:      X = N++;       Résultat: N=6 et X=5
Incrém. préfixe:       X = ++N;       Résultat: N=6 et X=6

3.4. Les expressions et les instructions

Expressions


Les constantes et les variables sont des expressions.

Les expressions peuvent être combinées entre elles par des opérateurs et former
ainsi des expressions plus complexes.

Les expressions peuvent contenir des appels de fonctions et elles peuvent
apparaître comme paramètres dans des appels de fonctions.

Exemples



     i=0
     i++
     X=pow(A,4)
     printf(" Bonjour !n")
     a=(5*x+10*y)*2
     (a+b)>=100
     position!=limite

Instructions

Une expression comme I=0 ou I++ ou printf(...) devient une instruction, si elle est
suivie d'un point-virgule.

Exemples
i=0;
     i++;
     X=pow(A,4);
     printf(" Bonjour !n");
     a=(5*x+10*y)*2;


Évaluation et résultats

En C toutes les expressions sont évaluées et retournent une valeur comme
résultat:

(3+2==5)             retourne la valeur 1 (vrai)
A=5+3                retourne la valeur 8

Comme les affectations sont aussi interprétées comme des expressions, il est
possible de profiter de la valeur rendue par l'affectation: ((A=sin(X)) == 0.5)

3.5. Les priorités des opérateurs

L'ordre de l'évaluation des différentes parties d'une expression correspond en
principe à celle que nous connaissons des mathématiques.

Exemple



     Supposons pour l'instruction suivante: A=5, B=10, C=1



                 X = 2*A+3*B+4*C;

     L'ordinateur évalue d'abord les multiplications:

                 2*A ==> 10 , 3*B ==> 30 , 4*C ==> 4

     Ensuite, il fait l'addition des trois résultats obtenus:

                 10+30+4 ==> 44

     A la fin, il affecte le résultat général à la variable:

                 X = 44
Priorité d'un opérateur

 On dit alors que la multiplication a la priorité sur l'addition et que la multiplication
et l'addition ont la priorité sur l'affectation.



Si nous voulons forcer l'ordinateur à commencer par un opérateur avec une priorité
plus faible, nous devons (comme en mathématiques) entourer le terme en question
par des parenthèses.



Exemple

Dans l'instruction:



     X = 2*(A+3)*B+4*C;



l'ordinateur évalue d'abord l'expression entre parenthèses, ensuite les
multiplications, ensuite l'addition et enfin l'affectation. (En reprenant les valeurs de
l'exemple ci-dessus, le résultat sera 164)

Entre les opérateurs que nous connaissons jusqu'ici, nous pouvons distinguer les
classes de priorités suivantes:



Classes de priorités



Priorité 1 (la plus forte):                  ()
Priorité 2:                                  ! ++ --
Priorité 3:                                  */%
Priorité 4:                                  +-
Priorité 5:                                  < <= > >=
Priorité 6:                                  == !=
Priorité 7:                                  &&
Priorité 8:                                  ||
Priorité 9 (la plus faible):                 = += -= *= /= %=
Evaluation d'opérateurs de la même classe

 --> Dans chaque classe de priorité, les opérateurs ont la même priorité. Si nous
avons une suite d'opérateurs binaires de la même classe, l'évaluation se fait en
passant de la gauche vers la droite dans l'expression.

--> Pour les opérateurs unaires ( ! , ++ , -- ) et pour les opérateurs d'affectation (=,
+= , -= , *= , /= , %= ), l'évaluation se fait de droite à gauche dans l'expression.

Exemples

L'expression 10+20+30-40+50-60 sera évaluée comme suit:



     10+20 ==> 30
               30+30 ==> 60
                         60-40 ==> 20
                                   20+50 ==> 70
                                             70-60 ==> 10



Pour A=3 et B=4, l'expression A *= B += 5 sera évaluée comme suit:




Pour A=1 et B=4, l'expression !--A==++!B sera évaluée comme suit:
Les parenthèses

Les parenthèses sont seulement nécessaires si nous devons forcer la priorité, mais
elles sont aussi permises si elles ne changent rien à la priorité. En cas de
parenthèses imbriquées, l'évaluation se fait de l'intérieur vers l'extérieur.

Exemple

 En supposant à nouveau que A=5, B=10, C=1 l'expression suivante s'évaluera à
134: X = ((2*A+3)*B+4)*C

Observez la priorité des opérateurs d'affectation :

    X *= Y + 1       <=>       X = X * (Y + 1)
    X *= Y + 1    n'équivaut PAS à X = X * Y + 1

3.6. Les fonctions arithmétiques standard

Les fonctions suivantes sont prédéfinies dans la bibliothèque standard <maths.
lib>. Pour pouvoir les utiliser, le programme doit contenir la ligne:
#include <math.h>

Type des données

Les arguments et les résultats des fonctions arithmétiques sont du type double.

Fonctions arithmétiques


COMMANDE C          EXPLICATION             LANG. ALGORITHMIQUE
exp(X)              fonction exponentielle eX
log(X)              logarithme naturel      ln(X), X>0
log10(X)            logarithme à base 10    log10(X), X>0
pow(X,Y)            X exposant Y            XY
sqrt(X)             racine carrée de X      pour X>0
fabs(X)             valeur absolue de X     |X|
floor(X)            arrondir en moins       int(X)
ceil(X)             arrondir en plus
                    reste rationnel de X/Y
fmod(X,Y)                                  pour X différent de 0
                    (même signe que X)
sin(X) cos(X) tan(X)                    sinus, cosinus, tangente de X
asin(X) acos(X) atan(X)                 arcsin(X), arccos(X), arctan(X)
                                        sinus, cosinus, tangente hyperboliques de
sinh(X) cosh(X) tanh(X)
                                        X

3.7. Les conversions de type

La grande souplesse du langage C permet de mélanger des données de différents
types dans une expression. Avant de pouvoir calculer, les données doivent être
converties dans un même type. La plupart de ces conversions se passent
automatiquement, sans l'intervention du programmeur, qui doit quand même
prévoir leur effet. Parfois il est nécessaire de convertir une donnée dans un type
différent de celui que choisirait la conversion automatique; dans ce cas, nous
devons forcer la conversion à l'aide d'un opérateur spécial ("cast").

3.7.1. Les conversions de type automatiques

Calculs et affectations

Si un opérateur a des opérandes de différents types, les valeurs des opérandes
sont converties automatiquement dans un type commun.

Ces manipulations implicites convertissent en général des types plus 'petits' en
des types plus 'larges'; de cette façon on ne perd pas en précision.



Lors d'une affectation, la donnée à droite du signe d'égalité est convertie dans
le type à gauche du signe d'égalité. Dans ce cas, il peut y avoir perte de
précision si le type de la destination est plus faible que celui de la source.

Exemple

Considérons le calcul suivant:



     int I = 8;
     float X = 12.5;
     double Y;
     Y = I * X;



Pour pouvoir être multiplié avec X, la valeur de I est convertie en float (le type le
plus large des deux). Le résultat de la multiplication est du type float, mais avant
d'être affecté a Y, il est converti en double. Nous obtenons comme résultat: Y =
100.00

Appels de fonctions

Lors de l'appel d'une fonction, les paramètres sont automatiquement convertis
dans les types déclarés dans la définition de la fonction.

Exemple

Au cours des expressions suivantes, nous assistons à trois conversions
automatiques:



     int A = 200;
     int RES;
     RES = pow(A, 2);



A l'appel de la fonction pow, la valeur de A et la constante 2 sont converties en
double, parce que pow est définie pour des données de ce type. Le résultat (type
double) retourné par pow doit être converti en int avant d'être affecté à RES.

Règles de conversion automatique

Conversions automatiques lors d'une opération avec,

(1) deux entiers:

D'abord, les types char et short sont convertis en int. Ensuite, l'ordinateur choisit le
plus large des deux types dans l'échelle suivante:

int , unsigned int , long , unsigned long


(2) un entier et un rationnel:

Le type entier est converti dans le type du rationnel.

(3) deux rationnels:

L'ordinateur choisit le plus large des deux types selon l'échelle suivante:

float , double , long double


(4) affectations et opérateurs d'affectation:
Lors d'une affectation, le résultat est toujours converti dans le type de la
destination. Si ce type est plus faible, il peut y avoir une perte de précision.

Exemple

Observons les conversions nécessaires lors d'une simple division:

     int X;
     float A=12.48;
     char B=4; X=A/
     B;


B est converti en float (règle 2). Le résultat de la division est du type float (valeur
3.12) et sera converti en int avant d'être affecté à X (règle 4), ce qui conduit au
résultat X=3 .

Phénomènes imprévus ...

Le mélange de différents types numériques dans un calcul peut inciter à ne pas
tenir compte des phénomènes de conversion et conduit parfois à des résultats
imprévus ...

Exemple

Dans cet exemple, nous divisons 3 par 4 à trois reprises et nous observons que le
résultat ne dépend pas seulement du type de la destination, mais aussi du type des
opérandes.



     char A=3;
     int B=4;
     float C=4;
     float D,E;
     char F;
     D = A/C;
     E = A/B;
     F = A/C;



* Pour le calcul de D, A est converti en float (règle 2) et divisé par C. Le résultat
(0.75) est affecté à D qui est aussi du type float. On obtient donc: D=0.75

* Pour le calcul de E, A est converti en int (règle 1) et divisé par B. Le résultat de la
division (type int, valeur 0) est converti en float (règle 4). On obtient donc: E=0.000
* Pour le calcul de F, A est converti en float (règle 2) et divisé par C. Le résultat
(0.75) est retraduit en char (règle 4). On obtient donc: F=0

Perte de précision

Lorsque nous convertissons une valeur en un type qui n'est pas assez précis ou
pas assez grand, la valeur est coupée sans arrondir et sans nous avertir ...

Exemple



unsigned int A = 70000;
/* la valeur de A sera: 70000 mod 65536 = 4464 */



3.7.2. Les conversions de type forcées (casting)

Il est possible de convertir explicitement une valeur en un type quelconque en
forçant la transformation à l'aide de la syntaxe:

Casting (conversion de type forcée)


(<Type>) <Expression>

Exemple

Nous divisons deux variables du type entier. Pour avoir plus de précision, nous
voulons avoir un résultat de type rationnel. Pour ce faire, nous convertissons l'une
des deux opérandes en float. Automatiquement C convertira l'autre opérande en
float et effectuera une division rationnelle:



     char A=3;
     int B=4;
     float C;
     C = (float)A/B;



La valeur de A est explicitement convertie en float. La valeur de B est
automatiquement convertie en float (règle 2). Le résultat de la division (type
rationnel, valeur 0.75) est affecté à C.

Résultat: C=0.75
Attention !

Les contenus de A et de B restent inchangés; seulement les valeurs utilisées dans
les calculs sont converties !
Chapitre 4: LIRE ET ÉCRIRE DES DONNÉES


  q   4.1. Écriture formatée de données
  q   4.2. Lecture formatée de données
  q   4.3. Écriture d'un caractère
  q   4.4. Lecture d'un caractère

La bibliothèque standard <stdio> contient un ensemble de fonctions qui
assurent la communication de la machine avec le monde extérieur. Dans ce
chapitre, nous allons en discuter les plus importantes:



      printf()      écriture formatée de données

      scanf()       lecture formatée de données

      putchar()     écriture d'un caractère

      getchar()     lecture d'un caractère



4.1. Écriture formatée de données

      printf()

La fonction printf est utilisée pour transférer du texte, des valeurs de
variables ou des résultats d'expressions vers le fichier de sortie standard
stdout (par défaut l'écran).

Ecriture formatée en langage algorithmique
écrire <Expression1>,
     <Expression2>, ...



Ecriture formatée en C



     printf("<format>",<Expr1>,<Expr2>, ... )



"<format>"    :    format de représentation

                   variables et expressions dont les valeurs sont à
<Expr1>,...   :
                   représenter


La partie " <format>" est en fait une chaîne de caractères qui peut contenir:

* du texte

* des séquences d'échappement

* des spécificateurs de format

* Les spécificateurs de format indiquent la manière dont les valeurs des
expressions <Expr1..N> sont imprimées.

* La partie "<format>" contient exactement un spécificateur de format pour
chaque expression <Expr1..N>.

* Les spécificateurs de format commencent toujours par le symbole % et se
terminent par un ou deux caractères qui indiquent le format d'impression.

* Les spécificateurs de format impliquent une conversion d'un nombre en
chaîne de caractères. Ils sont encore appelés symboles de conversion.

Exemple 1

La suite d'instructions:
int A = 1234;
     int B = 567;
     printf("%i fois %i est %lin", A, B, (long)A*B);



va afficher sur l'écran:



     1234 fois 567 est 699678



Les arguments de printf sont



-   la partie format              "%i fois %i est %li"
-   la variable                   A
-   la variable                   B
-   l'expression                  (long)A*B



Le1er spécificateur (%i) indique que la valeur de A sera    ==>1234
imprimée comme entier relatif.

Le 2e spécificateur (%i) indique que la valeur de B sera    ==> 567
imprimée comme entier relatif

Le 3e spécificateur (%li) indique que la valeur de (long)   ==> 699678
A*B sera imprimée comme entier relatif long

Exemple 2

La suite d'instructions:



     char B = 'A';
     printf("Le caractère %c a le code %i !n", B, B);
va afficher sur l'écran:



Le caractère A a le code 65 !



La valeur de B est donc affichée sous deux formats différents:

%c comme caractère:                     A
%i comme entier relatif:                65

Spécificateurs de format pour printf



   SYMBOLE         TYPE                IMPRESSION COMME


%d ou %i         int       entier relatif
%u               int       entier naturel (unsigned)
%o               int       entier exprimé en octal
%x               int       entier exprimé en hexadécimal
%c               int       caractère
%f               double    rationnel en notation décimale
%e               double    rationnel en notation scientifique
%s               char*     chaîne de caractères

1. Arguments du type long

Les spécificateurs %d, %i, %u, %o, %x peuvent seulement représenter des
valeurs du type int ou unsigned int. Une valeur trop grande pour être codée
dans deux octets est coupée sans avertissement si nous utilisons %d.



Pour pouvoir traiter correctement les arguments du type long, il faut
utiliser les spécificateurs %ld, %li, %lu, %lo, %lx.
Exemple



long N = 1500000;
printf("%ld, %lx", N, N);              ==> 1500000, 16e360
printf("%x, %x" , N);                  ==> e360, 16
printf("%d, %d" , N);                  ==> -7328, 22

2. Arguments rationnels

Les spécificateurs %f et %e peuvent être utilisés pour représenter des
arguments du type float ou double. La mantisse des nombres représentés
par %e contient exactement un chiffre (non nul) devant le point décimal.
Cette représentation s'appelle la notation scientifique des rationnels.

Pour pouvoir traiter correctement les arguments du type long double, il faut
utiliser les spécificateurs %Lf et %Le.

Exemple

float N = 12.1234;
double M = 12.123456789;
long double P = 15.5;
printf("%f", N);                         ==> 12.123400
printf("%f", M);                         ==> 12.123457
printf("%e", N);                         ==> 1.212340e+01
printf("%e", M);                         ==> 1.212346e+01
printf("%Le", P);                        ==> 1.550000e+01

3. Largeur minimale pour les entiers

Pour les entiers, nous pouvons indiquer la largeur minimale de la valeur à
afficher. Dans le champ ainsi réservé, les nombres sont justifiés à droite.

Exemples

( _ <=> position libre)
printf("%4d", 123);             ==>      _123
printf("%4d", 1234);            ==>      1234
printf("%4d", 12345);           ==>      12345
printf("%4u", 0);               ==>      ___0
printf("%4X", 123);             ==>      __7B
printf("%4x", 123);             ==>      __7b

4. Largeur minimale et précision pour les rationnels

Pour les rationnels, nous pouvons indiquer la largeur minimale de la valeur
à afficher et la précision du nombre à afficher. La précision par défaut est
fixée à six décimales. Les positions décimales sont arrondies à la valeur la
plus proche.

Exemples



printf("%f", 100.123);                 ==>       100.123000
printf("%12f", 100.123);               ==>       __100.123000
printf("%.2f", 100.123);               ==>       100.12
printf("%5.0f", 100.123);              ==>       __100
printf("%10.3f", 100.123);             ==>       ___100.123
printf("%.4f", 1.23456);               ==>       1.2346

4.2. Lecture formatée de données

scanf()

La fonction scanf est la fonction symétrique à printf; elle nous offre
pratiquement les mêmes conversions que printf, mais en sens inverse.

Lecture formatée en langage algorithmique



     lire <NomVariable1>,<NomVariable2>, ...

Lecture formatée en C
scanf("<format>",<AdrVar1>,<AdrVar2>, ...)



"<format>"        :   format de lecture des données


                      adresses des variables auxquelles les données seront
<AdrVar1>,...     :
                      attribuées


* La fonction scanf reçoit ses données à partir du fichier d'entrée standard
stdin (par défaut le clavier).

* La chaîne de format détermine comment les données reçues doivent être
interprétées.

* Les données reçues correctement sont mémorisées successivement aux
adresses indiquées par <AdrVar1>,... .


* L'adresse d'une variable est indiquée par le nom de la variable précédé du
signe &.

Exemple

La suite d'instructions:



int JOUR, MOIS, ANNEE;
scanf("%i %i %i", &JOUR, &MOIS, &ANNEE);



lit trois entiers relatifs, séparés par des espaces, tabulations ou interlignes.
Les valeurs sont attribuées respectivement aux trois variables JOUR, MOIS et
ANNEE.

* scanf retourne comme résultat le nombre de données correctement
reçues (type int).

Spécificateurs de format pour scanf
SYMBOLE                        LECTURE D'UN(E)                        TYPE

%d ou %i          entier relatif                                       int*
%u                entier naturel (unsigned)                            int*
%o                entier exprimé en octal                              int*
%b                entier exprimé en hexadécimal                        int*
%c                caractère                                            char*
%s                chaîne de caractères                                 char*
                  rationnel en notation décimale ou exponentielle
%f ou %e                                                               float*
                  (scientifique)

Le symbole * indique que l'argument n'est pas une variable, mais l'adresse
d'une variable de ce type (c.-à-d.: un pointeur sur une variable - voir chapitre
9 'Les pointeurs' ).

1. Le type long

Si nous voulons lire une donnée du typelong, nous devons utiliser les
spécificateurs %ld, %li, %lu, %lo, %lx. (Sinon, le nombre est simplement
coupé à la taille de int).

2. Le type double



Si nous voulons lire une donnée du type double, nous devons utiliser les
spécificateurs %le ou %lf.

3. Le type long double

Si nous voulons lire une donnée du typelong double, nous devons utiliser
les spécificateurs %Le ou %Lf.

4. Indication de la largeur maximale

Pour tous les spécificateurs, nous pouvons indiquer la largeur maximale du
champ à évaluer pour une donnée. Les chiffres qui passent au-delà du
champ défini sont attribués à la prochaine variable qui sera lue !
Exemple

Soient les instructions:



int A,B;
scanf("%4d %2d", &A,
&B);




Si nous entrons le nombre 1234567, nous obtiendrons les affectations
suivantes:


A=1234
B=56
le chiffre 7 sera gardé pour la prochaine instruction de lecture.

5. Les signes d'espacement

Lors de l'entrée des données, une suite de signes d'espacement (espaces,
tabulateurs, interlignes) est évaluée comme un seul espace. Dans la chaîne
de format, les symboles t, n, r ont le même effet qu'un simple espace.

Exemple

Pour la suite d'instructions



int JOUR, MOIS, ANNEE;
scanf("%i %i %i", &JOUR, &MOIS,
&ANNEE);



les entrées suivantes sont correctes et équivalentes:

12 4 1980
        ou
   12      004         1980
ou
    12
    4
    1980

6. Formats 'spéciaux'

Si la chaîne de format contient aussi d'autres caractères que des signes
d'espacement, alors ces symboles doivent être introduits exactement dans
l'ordre indiqué.

Exemple

La suite d'instructions

int JOUR, MOIS, ANNEE;
scanf("%i/%i/%i", &JOUR, &MOIS, &ANNEE);




    accepte les entrées:           rejette les entrées:

12/4/1980                      12 4 1980
12/04/01980                    12 /4 /1980

7. Nombre de valeurs lues

Lors de l'évaluation des données,scanf s'arrête si la chaîne de format a été
travaillée jusqu'à la fin ou si une donnée ne correspond pas au format
indiqué. scanf retourne comme résultat le nombre d'arguments
correctement reçus et affectés.

Exemple

La suite d'instructions

int JOUR, MOIS, ANNEE, RECU;
RECU = scanf("%i %i %i", &JOUR, &MOIS, &ANNEE);




réagit de la façon suivante ( - valeur indéfinie):
RECU   JOUR       MOIS    ANNEE
  Introduit:
12 4 1980        ==>      3          12     4           1980
12/4/1980        ==>      1          12     -           -
12.4 1980        ==>      1          12     -           -
12 4 19.80       ==>      3          12     4           19



4.3. Écriture d'un caractère

La commande, putchar('a'); transfère le caractère a vers le fichier standard
de sortie stdout. Les arguments de la fonction putchar sont ou bien des
caractères (c.-à-d. des nombres entiers entre 0 et 255) ou bien le symbole
EOF (End Of File).

EOF est une constante définie dans <stdio> qui marque la fin d'un fichier. La
commande putchar(EOF); est utilisée dans le cas où stdout est dévié vers
un fichier.

Type de l'argument

Pour ne pas être confondue avec un caractère, la constante EOF doit
nécessairement avoir une valeur qui sort du domaine des caractères (en
général EOF a la valeur -1). Ainsi, les arguments de putchar sont par
définition du type int et toutes les valeurs traitées par putchar (même celles
du type char) sont d'abord converties en int.

Exemples



char A = 225;
char B = 'a';
int C = 'a';
putchar('x');           /* afficher la lettre x */
putchar('?');          /* afficher le symbole ? */
putchar('n');         /* retour à la ligne */
putchar(65);           /* afficher le symbole avec */
                       /* le code 65 (ASCII: 'A') */
putchar(A);             /* afficher la lettre avec */
                       /* le code 225 (ASCII: 'ß') */
putchar(B);         /* beep sonore */
putchar(C);         /* beep sonore */
putchar(EOF);       /* marquer la fin du fichier */



4.4. Lecture d'un caractère

Une fonction plus souvent utilisée que putchar est la fonction getchar, qui lit
le prochain caractère du fichier d'entrée standard stdin.

Type du résultat

Les valeurs retournées par getchar sont ou bien des caractères
(0 - 255) ou bien le symbole EOF. Comme la valeur du symbole EOF sort du
domaine des caractères, le type résultat de getchar est int. En général,
getchar est utilisé dans une affectation:

int C;
C = getchar());
Chapitre 5: LA STRUCTURE ALTERNATIVE


   q   5.1. if - else
   q   5.2. if sans else
   q   5.3. if - else if - ... - else
   q   5.4. Instructions switch case break default
   q   5.5. Les opérateurs conditionnels

A priori, dans un programme, les instructions sont exécutées
séquentiellement, c'est à dire dans l'ordre où elles apparaissent. Or la
puissance et le comportement intelligent d'un programme proviennent
essentiellemnt de la possibilité d'effectuer des choix, de se comporter
différemment suivant les circonstances ( celle-ci pouvant être, par
exemple, une réponse de l'utilisateur, un résultat de calcul...)

Tous les langages disposent d'instructions, nommées " instructions de
contrôles ", permettant de réaliser ces choix.

Constatons déjà que la particularité la plus importante des instructions de
contrôle en C est le fait que les 'conditions' en C peuvent être des
expressions quelconques qui fournissent un résultat numérique. La
valeur zéro correspond à la valeur logique faux et toute valeur différente
de zéro est considérée comme vrai.

5.1. if - else

La structure alternative en langage algorithmique



       si (<expression logique>)
       alors <bloc d'instructions 1>
       sinon <bloc d'instructions 2>
       fsi
* Si l'<expression logique> a la valeur logique vrai,
alors le <bloc d'instructions 1> est exécuté

* Si l'<expression logique> a la valeur logique faux,
alors le <bloc d'instructions 2> est exécuté

La structure alternative en C



     if ( <expression> )
        <bloc d'instructions 1>
     else <bloc d'instructions 2>



* Si l'<expression> fournit une valeur différente de zéro,
alors le <bloc d'instructions 1> est exécuté

* Si l'<expression> fournit la valeur zéro,
alors le <bloc d'instructions 2> est exécuté

La partie <expression> peut désigner :

une variable d'un type numérique,
une expression fournissant un résultat numérique.

La partie <bloc d'instructions> peut désigner :

un (vrai) bloc d'instructions compris entre accolades,
une seule instruction terminée par un point-virgule.

Exemple 1

     if (a > b)
         max = a;
     else
         max = b;



Exemple 2
if (EGAL)
        printf("A est égal à Bn");
     else printf("A est différent de Bn");



Exemple 3

     if (A-B)
         printf("A est différent de Bn");
     else
         printf("A est égal à Bn");



Exemple 4

     if (A > B)
        {
          AIDE = A;
          A = C;
          C = AIDE;
         }
     else
         {
          AIDE = B;
          B = C;
          C = AIDE;
         }




5.2. if sans else

La partie else est facultative. On peut donc utiliser if de la façon suivante:

if sans else
if ( <expression> )
        <bloc d'instructions>

Attention !

Comme la partie else est optionnelle, les expressions contenant plusieurs
structures if et if - else peuvent mener à des confusions.

Exemple

L'expression suivante peut être interprétée de deux façons:

     if (N>0)
        if (A>B)
           MAX=A;
        else
           MAX=B;

     if (N>0)
        if (A>B)
            MAX=A;
     else
        MAX=B;

Pour N=0, A=1 et B=2,

* dans la première interprétation, MAX reste inchangé,

* dans la deuxième interprétation, MAX obtiendrait la valeur de B.

Sans règle supplémentaire, le résultat de cette expression serait donc
imprévisible.

Convention



En C une partie else est toujours liée au dernier if qui ne possède pas de
partie else.
Dans notre exemple, C utiliserait donc la première interprétation.

Solution

Pour éviter des confusions et pour forcer une certaine interprétation
d'une expression, il est recommandé d'utiliser des accolades { } .

Exemple

Pour forcer la deuxième interprétation de l'expression ci-dessus, nous
pouvons écrire:



      if (N>0)
         {
           if (A>B)
              MAX=A;
         }
      else
         MAX=B;

5.3. if - else if - ... - else

En combinant plusieurs structures if - else en une expression nous
obtenons une structure qui est très courante pour prendre des décisions
entre plusieurs alternatives:

if - else - ... - else



      if ( <expr1> )
         <bloc1>
      else if (<expr2>)
              <bloc2>
      else if (<expr3>)
              <bloc3>
      else if (<exprN>)
              <blocN>
      else
         <blocN+1>
Les expressions <expr1> ... <exprN> sont évaluées du haut vers le bas
jusqu'à ce que l'une d'elles soit différente de zéro. Le bloc d'instructions y
lié est alors exécuté et le traitement de la commande est terminé.

Exemple



     #include <stdio.h>
     main()
       {
         int A,B;
         printf("Entrez deux nombres entiers :");
         scanf("%i %i", &A, &B);
         if (A > B)
            printf("%i est plus grand que %in", A, B);
         else if (A < B)
                  printf("%i est plus petit que %in", A, B);
         else
            printf("%i est égal à %in", A, B);
         return 0;
       }

La dernière partie else traite le cas où aucune des conditions n'a été
remplie. Elle est optionnelle, mais elle peut être utilisée très
confortablement pour détecter des erreurs.

Exemple



     ...
     printf("Continuer (O)ui / (N)on ?");
     getchar(C);
     if (C=='O')
         {
           ...
         }
     else if (C=='N')
               printf("Au revoir ...n");
else
        printf("aErreur d'entrée !n"); ...



5.4. Instructions switch case break default

L'instruction switch compare une valeur à une série de constantes et
permet d'exécuter des instructions dans les cas où la valeur est égale à
une des constantes:


     switch (valeur) {
      case constante1:
                              instructions1
                              break;
         case constante2:
                              instructions2
                              break;
         default:
                              instructions3
     }



Peut se lire comme:


     if (valeur == constante1)
        {
          instructions1
        }
     else if (valeur == constante2)
              {
                instructions2
              }
     else
        {
          instructions3
        }
L'instruction break sert à interrompre le flux d'exécution. Si on l'omet, les
instructions suivant les case sont exécutées en séquence:


      switch (valeur) {
        case constante1:
         instructions1
        case constante2:
         instructions2
        default:
         instructions3
      }



se lit:


      if (valeur == constante1)
         {
           instructions1
           instructions2
           instructions3
         }
      else if (valeur == constante2)
               {
                 instructions2
                 instructions3
                }
      else
         {
           instructions3
          }



Syntaxe de l'instruction switch

En langage algorithmique
au cas où expression vaut :

     - valeur1 : <bloc_d_instructions1>

     - valeur2 : <bloc_d_instructions2>

     ......................................................

     ......................................................

     -valeurN : <bloc_d_instructionN>

     aucune des valeurs précédentes : <bloc_d_instructions>


En langage C

     switch ( expression) {
         case constante_1 : [<suite_d_instructions_1>]
                                          break;
         case constante_2 : [< suite_d_instructions_2>]
                                          break;
           ...........................................
           ...........................................
         case constante_N : [<suite_d_instructions_N>]
                                         break;
         [defaut : <suite_d_instructions> ]
      }




expression : expression entière quelconque

constante_1 constante_2 .... constante_N : expressions constantes d'un
type entier quelconque ( char est accepté car il sera converti en int )

< suite_d_instructions_1> ...<suite_d_instructions_N> : séquence
d'instructions quelconques.

Les crochets ( [ et ] ) signifient que ce qu'ils renferment est facultatif.
Exemple : l'instruction switch est commode pour fabriquer des "menus":


     char choix ;
     printf("Liste par groupe taper 1 n");
     printf("Liste alphabetique taper 2n");
     printf("Pour sortir taper Sn");
     printf("n Votre choix ");
     choix = getchar();
     switch (choix) {
            case '1' : ............ ;
                         ............ ;
                         break ;
            case '2' : ............ ;
                        ............. ;
                         break;
             case 'S' : printf("n Fin du programme");
                        break;
             default : printf("n Ce choix n'est pas prevu ");
     }




5.5. Les opérateurs conditionnels

Le langage C possède une paire d'opérateurs un peu exotiques qui peut
être utilisée comme alternative à if - else et qui a l'avantage de pouvoir
être intégrée dans une expression:

Les opérateurs conditionnels

<expr1> ? <expr2> : <expr3>

* Si <expr1> fournit une valeur différente de zéro,
alors la valeur de <expr2> est fournie comme résultat

* Si <expr1> fournit la valeur zéro,
alors la valeur de <expr3> est fournie comme résultat

Exemple
La suite d'instructions



     if (A>B)
        MAX=A;
     else
        MAX=B;



peut être remplacée par:



     MAX = (A > B) ? A : B;

Employés de façon irréfléchis, les opérateurs conditionnels peuvent
nuire à la lisibilité d'un programme, mais si on les utilise avec
précaution, ils fournissent des solutions très élégantes.

Exemple

     printf("Vous avez %i carte%c n", N, (N==1) ? ' ' : 's');

Les règles de conversion de types s'appliquent aussi aux
opérateurs conditionnels ? : Ainsi, pour un entier N du type int
et un rationnel F du type float, l'expression

     (N>0) ? N : F



va toujours fournir un résultat du type float, n'importe si N est plus grand
ou plus petit que zéro!
Chapitre 6: LA STRUCTURE REPETITIVE


En C, nous disposons de trois structures qui nous permettent la définition de boucles
conditionnelles:

1) la structure : while

2) la structure : do - while

3) la structure : for

Théoriquement, ces structures sont interchangeables, c.-à-d. il serait possible de
programmer toutes sortes de boucles conditionnelles en n'utilisant qu'une seule des
trois structures. Il est quand même absolument recommandé de choisir toujours la
structure la mieux adaptée au cas actuel (voir 6.4.).

   q   6.1. while
   q   6.2. do - while
   q   6.3. for
   q   6.4. Choix de la structure répétitive
6.1. while

La structure while correspond tout à fait à la structure tant que du langage algorithmique.
(Si on néglige le fait qu'en C les conditions peuvent être formulées à l'aide d'expressions
numériques.)

La structure tant que en langage algorithmique



     tant que (<expression logique>) faire
              <bloc d'instructions>
     ftant



* Tant que l'<expression logique> fournit la valeur vrai,
le <bloc d'instructions> est exécuté.

* Si l'<expression logique> fournit la valeur faux,
l'exécution continue avec l'instruction qui suit ftant.

* Le <bloc d'instructions> est exécuté zéro ou plusieurs fois.

La structure while en C
while ( <expression> )
             <bloc d'instructions>



* Tant que l'<expression> fournit une valeur différente de zéro, le <bloc d'instructions>
est exécuté.

* Si l'<expression> fournit la valeur zéro,
l'exécution continue avec l'instruction qui suit le bloc d'instructions.

* Le <bloc d'instructions> est exécuté zéro ou plusieurs fois.

La partie <expression> peut désigner :

q   une variable d'un type numérique,
q   une expression fournissant un résultat numérique.

La partie <bloc d'instructions> peut désigner :
qun (vrai) bloc d'instructions compris entre accolades,
une seule instruction terminée par un point-virgule.

Exemple 1
/* Afficher les nombres de 0 à 9 */
    int I = 0;
    while (I<10)
            {
              printf("%i n", I);
              I++;
            }



Exemple 2



    int I;
    /* Afficher les nombres de 0 à 9 */
    I = 0;
    while (I<10)
           printf("%i n", I++);

    /* Afficher les nombres de 1 à 10 */
    I = 0;
    while (I<10)
           printf("%i n", ++I);
Exemple 3



     /* Afficher les nombres de 10 à 1 */
     int I=10;
     while (I)
            printf("%i n", I--);

Remarque

Parfois nous voulons seulement attendre un certain événement, sans avoir besoin d'un
traitement de données. Dans ce cas, la partie <bloc d'instructions> peut être vide
(notation: ; ou {} ).
La ligne suivante ignore tous les espaces entrés au clavier et peut être utilisée avant de
lire le premier caractère significatif:
q




     while (getch()==' ') ;



6.2. do - while

La structure do - while est semblable à la structure while, avec la différence suivante :

* while évalue la condition avant d'exécuter le bloc d'instructions,
* do - while évalue la condition après avoir exécuté le bloc d'instructions. Ainsi le bloc
d'instructions est exécuté au moins une fois.

La structure do - while en C



     do
       <bloc d'instructions>
     while ( <expression> );



Le <bloc d'instructions> est exécuté au moins une fois
et aussi longtemps que l'<expression> fournit une valeur différente de zéro.

En pratique, la structure do - while n'est pas si fréquente que while; mais dans certains
cas, elle fournit une solution plus élégante. Une application typique de do - while est la
saisie de données qui doivent remplir une certaine condition:

Exemple 1
float N;
    do
       {
         printf("Introduisez un nombre entre 1 et 10 :");
         scanf("%f", &N);
       }
    while (N<1 || N>10);



Exemple 2



    int n, div;
    printf("Entrez le nombre à diviser : ");
    scanf("%i", &n);
    do
       {
         printf("Entrez le diviseur : ");
         scanf("%i", &div);
       }
    while (!div);
    printf("%i / %i = %fn", n, div, (float)n/div);
do - while est comparable à la structure répéter du langage algorithmique si la
condition finale est inversée logiquement.

Exemple 3

Le programme de calcul de la racine carrée :



     programme RACINE_CARREE
      réel N
      répéter
        écrire "Entrer un nombre (>=0) : "
        lire N
      jusqu'à (N >= 0)
      écrire "La racine carrée de ",N ,"vaut ", N
     fprogramme (* fin RACINE_CARRE *)



se traduit en C par :
#include <stdio.h>
     #include <math.h>
     main()
       {
         float N;
         do
           {
             printf("Entrer un nombre (>= 0) : ");
             scanf("%f", &N)
           }
        while (N < 0);
        printf("La racine carrée de %.2f est %.2fn", N, sqrt(N)); return 0;
     }



6.3. for

La structure pour en langage algorithmique est utilisée pour faciliter la programmation de
boucles de comptage. La structure for en C est plus générale et beaucoup plus
puissante.

La structure for en C
for ( <expr1> ; <expr2> ; <expr3> )
         <bloc d'instructions>



est équivalent à :



     <expr1>;
     while ( <expr2> )
           {
             <bloc d'instructions>
             <expr3>;
           }



<expr1>est évaluée une fois avant le passage de la boucle.
Elle est utilisée pour initialiser les données de la boucle.

<expr2> est évaluée avant chaque passage de la boucle.
Elle est utilisée pour décider si la boucle est répétée ou non.

<expr3> est évaluée à la fin de chaque passage de la boucle.
Elle est utilisée pour réinitialiser les données de la boucle.
Le plus souvent, for est utilisé comme boucle de comptage :

     for ( <init.> ; <cond. répétition> ; <compteur> )
         <bloc d'instructions>



Exemple



     int I;
     for (I=0 ; I<=20 ; I++)
          printf("Le carré de %d est %d n", I, I*I);



En pratique, les parties <expr1> et <expr2> contiennent souvent plusieurs initialisations
ou réinitialisations, séparées par des virgules.

Exemple
int n, tot;
    for (tot=0, n=1 ; n<101 ; n++)
        tot+=n;
    printf("La somme des nombres de 1 à 100 est %dn", tot);



Exemple

Cet exemple nous présente différentes variations pour réaliser le même traitement et
nous montre la puissance de la structure for. Les expressions suivantes lisent un
caractère au clavier et affichent son code numérique en notation binaire :

    /* a */
    /* notation utilisant la structure while */
    int C, I;
    C=getchar();
    I=128;
    while (I>=1)
            {
              printf("%i ", C/I);
              C%=I;
              I/=2;
            }
    /* b */
    /* notation utilisant for - très lisible - */
/* préférée par les débutants en C */
     int C, I;
     C=getchar();
     for (I=128 ; I>=1 ; I/=2)
        {
         printf("%i ", C/I);
         C%=I;
        }

     /* c */
     /* notation utilisant for - plus compacte - */
     /* préférée par les experts en C */
     int C, I;
     C=getchar();
     for (I=128 ; I>=1 ; C%=I, I/=2)
         printf("%i ", C/I);
     /* d */
     /* notation utilisant for - à déconseiller - */
     /* surcharge et mauvais emploi de la structure */
     int C, I;
     for(C=getchar(),I=128 ; I>=1 ;
        printf("%i ",C/I),C%=I,I/=2);



6.4. Choix de la structure répétitive
Dans ce chapitre, nous avons vu trois façons différentes de programmer des boucles
(while, do - while, for ). Utilisez la structure qui reflète le mieux l'idée du programme que
vous voulez réaliser, en respectant toutefois les directives suivantes :

* Si le bloc d'instructions ne doit pas être exécuté si la condition est fausse, alors
utilisez while ou for.

* Si le bloc d'instructions doit être exécuté au moins une fois, alors utilisez do - while.

* Si le nombre d'exécutions du bloc d'instructions dépend d'une ou de plusieurs
variables qui sont modifiées à la fin de chaque répétition, alors utilisez for.

* Si le bloc d'instructions doit être exécuté aussi longtemps qu'une condition extérieure
est vraie (p.ex aussi longtemps qu'il y a des données dans le fichier d'entrée), alors
utilisez while.

Le choix entre for et while n'est souvent qu'une question de préférence ou d'habitudes:

* for nous permet de réunir avantageusement les instructions qui influencent le nombre
de répétitions au début de la structure.

* while a l'avantage de correspondre plus exactement aux structures d'autres langages
de programmation (while, tant que).

* for a le désavantage de favoriser la programmation de structures surchargées et par la
suite illisibles.

* while a le désavantage de mener parfois à de longues structures, dans lesquelles il faut
chercher pour trouver les instructions qui influencent la condition de répétition.
Chapitre 7: LES TABLEAUX
Les tableaux sont certainement les variables structurées les plus populaires.
Ils sont disponibles dans tous les langages de programmation et servent à
résoudre une multitude de problèmes. Dans une première approche, le
traitement des tableaux en C ne diffère pas de celui des autres langages de
programmation.

  q   7.1. Les tableaux à une dimension
        r  7.1.1. Déclaration et mémorisation
        r  7.1.2. Initialisation et réservation automatique
        r  7.1.3. Accès aux composantes
        r  7.1.4. Affichage et affectation
  q   7.2. Les tableaux à deux dimensions
        r  7.2.1. Déclaration et mémorisation
        r  7.2.2. Initialisation et réservation automatique
        r  7.2.3. Accès aux composantes
        r  7.2.4. Affichage et affectation




7.1. Les tableaux à une dimension

Définitions

Un tableau (uni-dimensionnel) A est une variable structurée formée d'un
nombre entier N de variables simples du même type, qui sont appelées les
composantes du tableau. Le nombre de composantes N est alors la
dimension du tableau.
En faisant le rapprochement avec les mathématiques, on dit encore que "A
                       est un vecteur de dimension N"

Exemple

La déclaration



int JOURS[12]={31,28,31,30,31,30,31,31,30,31,30,31};




définit un tableau du type int de dimension 12. Les 12 composantes sont
initialisées par les valeurs respectives 31, 28, 31, ... , 31.

On peut accéder à la première composante du tableau par JOURS[0] , à la
deuxième composante par JOURS[1], . . . , à la dernière composante par
JOURS[11].

7.1.1. Déclaration et mémorisation

Déclaration

Déclaration de tableaux en langage algorithmique



    <TypeSimple> tableau <NomTableau>[<Dimension>]



Déclaration de tableaux en C



    <TypeSimple> <NomTableau>[<Dimension>];

Les noms des tableaux sont des identificateurs qui doivent correspondre aux
restrictions définies au chapitre 2.2.4.
Exemples

Les déclarations suivantes en langage algorithmique,



     entier tableau      A[25]
     réel tableau        B[100]
     booléen tableau     C[10]
     caractère tableau   D[30]



se laissent traduire en C par:



     int A[25]; ou bien long A[25];
     float B[100]; ou bien double B[100];
     int C[10];
     char D[30];

Mémorisation

En C, le nom d'un tableau est le représentant de l'adresse du premier élément
du tableau. Les adresses des autres composantes sont calculées
(automatiquement) relativement à cette adresse.

Exemple:



short A[5] = {1200, 2300, 3400, 4500, 5600};




Si un tableau est formé de N composantes et si une composante a besoin de
        M octets en mémoire, alors le tableau occupera de N*M octets.
Exemple :

En supposant qu'une variable du type long occupe 4 octets (c.-à-d: sizeof
(long)=4), pour le tableau T déclaré par: long T[15];
C réservera N*M = 15*4 = 60 octets en mémoire.



7.1.2. Initialisation et réservation automatique

Initialisation

Lors de la déclaration d'un tableau, on peut initialiser les composantes du
tableau, en indiquant la liste des valeurs respectives entre accolades.

Exemples :



int A[5] = {10, 20, 30, 40, 50};
float B[4] = {-1.05, 3.33, 87e-5, -12.3E4};
int C[10] = {1, 0, 0, 1, 1, 1, 0, 1, 0, 1};

Il faut évidemment veiller à ce que le nombre de valeurs dans la liste
corresponde à la dimension du tableau. Si la liste ne contient pas assez de
valeurs pour toutes les composantes, les composantes restantes sont
initialisées par zéro.

Réservation automatique

Si la dimension n'est pas indiquée explicitement lors de l'initialisation, alors
l'ordinateur réserve automatiquement le nombre d'octets nécessaires.

Exemples :

int A[ ] = {10, 20, 30, 40, 50};



==> réservation de 5*sizeof(int) octets (dans notre cas: 10 octets)

float B[ ] = {-1.05, 3.33, 87e-5, -12.3E4};
==> réservation de 4*sizeof(float) octets (dans notre cas: 16 octets)

int C[ ] = {1, 0, 0, 1, 1, 1, 0, 1, 0, 1};



==> réservation de 10*sizeof(int) octets (dans notre cas: 20 octets)

Exemples :




7.1.3. Accès aux composantes

En déclarant un tableau par:

int A[5];

nous avons défini un tableau A avec cinq composantes, auxquelles on peut
accéder par: A[0], A[1], ... , A[4]



Exemples :
MAX = (A[0]>A[1]) ? A[0] : A[1]; A[4] *= 2;



Attention !

Considérons un tableau T de dimension N :
En C,

- l'accès au premier élément du tableau se fait par T[0]

- l'accès au dernier élément du tableau se fait par T[N-1]

En langage algorithmique,

- l'accès au premier élément du tableau se fait par T[1]

- l'accès au dernier élément du tableau se fait par T[N]



7.1.4. Affichage et affectation

La structure for se prête particulièrement bien au travail avec les tableaux. La
plupart des applications se laissent implémenter par simple modification des
exemples-types de l'affichage et de l'affectation.

- Affichage du contenu d'un tableau

Traduisons le programme AFFICHER du langage algorithmique en C:

     programme AFFICHER
       | entier tableau A[5]
       | entier
       I (* Compteur *)
       | pour I variant de 1 à 5 faire
         | écrire A[I]
       | fpour
     fprogramme
#include <stdio.h>
     main()
       {
         int A[5];
         int I; /* Compteur */
         for (I=0; I<5; I++)
              printf("%d ", A[I]);
         printf("n");
         return 0;
       }


Remarques

* Avant de pouvoir afficher les composantes d'un tableau, il faut évidemment
leur affecter des valeurs.

* Rappelez-vous que la deuxième condition dans la structure for n'est pas
une condition d'arrêt, mais une condition de répétition! Ainsi la commande
d'affichage sera répétée aussi longtemps que I est inférieur à 5. La boucle
sera donc bien exécutée pour les indices 0,1,2,3 et 4 !

* Par opposition à la commande simplifiée écrire A[I] du langage
algorithmique, la commande printf doit être informée du type exact des
données à afficher. (Ici: %d ou %i pour des valeurs du type int)

*Pour être sûr que les valeurs sont bien séparées lors de l'affichage, il faut
inclure au moins un espace dans la chaîne de format. Autres possibilités:



printf("%dt", A[I]); /* tabulateur */
printf("%7d", A[I]); /* format d'affichage */



Affectation

- Affectation avec des valeurs provenant de l'extérieur

Traduisons le programme REMPLIR du langage algorithmique en C:
programme REMPLIR
      | entier tableau A[5]
      | entier I (* Compteur *)
      | pour I variant de 1 à 5 faire
          | lire A[I]
      | fpour
     fprogramme



     #include <stdio.h>
     main()
       {
         int A[5];
         int I; /* Compteur */
         for (I=0; I<5; I++)
              scanf("%d", &A[I]);
         return 0;

       }


Remarques

* Comme scanf a besoin des adresses des différentes composantes du
tableau, il faut faire précéder le terme A[I] par l'opérateur adresse '&'.

* La commande de lecture scanf doit être informée du type exact des données
à lire. (Ici: %d ou %i pour lire des valeurs du type int)

7.2. Les tableaux à deux dimensions

Définitions

En C, un tableau à deux dimensions A est à interpréter comme un tableau
(uni-dimensionnel) de dimension L dont chaque composante est un tableau
(uni-dimensionnel) de dimension C.

On appelle L le nombre de lignes du tableau et C le nombre de colonnes du
tableau. L et C sont alors les deux dimensions du tableau. Un tableau à deux
dimensions contient donc L*C composantes.
On dit qu'un tableau à deux dimensions est carré, si L est égal à C.

En faisant le rapprochement avec les mathématiques, on peut dire que "A est
un vecteur de L vecteurs de dimension C", ou mieux:

"A est une matrice de dimensions L et C".

Exemple :

Considérons un tableau NOTES à une dimension pour mémoriser les notes
de 20 élèves d'une classe dans un devoir:

int NOTE[20] = {45, 34, ... , 50, 48};




Pour mémoriser les notes des élèves dans les 10 devoirs d'un trimestre, nous
pouvons rassembler plusieurs de ces tableaux uni-dimensionnels dans un
tableau NOTES à deux dimensions :



int NOTE[10][20] = { {45, 34, ... , 50, 48},
                    {39, 24, ... , 49, 45},
                      ...
                      ...
                      ...
                    {40, 40, ... , 54, 44} };
Dans une ligne nous retrouvons les notes de tous les élèves dans un devoir.
Dans une colonne, nous retrouvons toutes les notes d'un élève.

7.2.1. Déclaration et mémorisation

Déclarations

Déclaration de tableaux à deux dimensions en lang. algorithmique



     <TypeSimple> tableau <NomTabl>[<DimLigne>,<DimCol>]



Déclaration de tableaux à deux dimensions en C



     <TypeSimple> <NomTabl>[<DimLigne>][<DimCol>];

Exemples :

Les déclarations suivantes en langage algorithmique,



     entier tableau    A[10,10]
     réel tableau     B[2,20]
     booléen tableau C[3,3]
     caractère tableau D[15,40]



se laissent traduire en C par:
int A[10][10];ou bien long A[10][10];
     float B[2][20]; ou bien double B[2][20];
     int C[3][3];
     char D[15][40];



Mémorisation

Comme pour les tableaux à une dimension, le nom d'un tableau est le
représentant de l'adresse du premier élément du tableau (c.-à-d. l'adresse de
la première ligne du tableau). Les composantes d'un tableau à deux
dimensions sont stockées ligne par ligne dans la mémoire.

Exemple: Mémorisation d'un tableau à deux dimensions



short A[3][2] = {{1, 2 }, {10, 20 }, {100, 200}};




   Un tableau de dimensions L et C, formé de composantes dont chacune a
           besoin de M octets, occupera L*C*M octets en mémoire.

Exemple :

En supposant qu'une variable du type double occupe 8 octets (c.-à-d: sizeof
(double)=8), pour le tableau T déclaré par: double T[10][15];
C réservera L*C*M = 10*15*8 = 1200 octets en mémoire.



7.2.2. Initialisation et réservation automatique

Initialisation

Lors de la déclaration d'un tableau, on peut initialiser les composantes du
tableau, en indiquant la liste des valeurs respectives entre accolades. A
l'intérieur de la liste, les composantes de chaque ligne du tableau sont encore
une fois comprises entre accolades. Pour améliorer la lisibilité des
programmes, on peut indiquer les composantes dans plusieurs lignes.

Exemples :



int A[3][10] ={{ 0,10,20,30,40,50,60,70,80,90},
               {10,11,12,13,14,15,16,17,18,19},
               { 1,12,23,34,45,56,67,78,89,90} };

float B[3][2] = {{-1.05 , -1.10 }, {86e-5, 87e-5 },
                 {-12.5E4, -12.3E4} };

Lors de l'initialisation, les valeurs sont affectées ligne par ligne en passant de
gauche à droite. Nous ne devons pas nécessairement indiquer toutes les
valeurs: Les valeurs manquantes seront initialisées par zéro. Il est cependant
défendu d'indiquer trop de valeurs pour un tableau.

Exemples :
Réservation automatique

Si le nombre de lignes L n'est pas indiqué explicitement lors de l'initialisation,
l'ordinateur réserve automatiquement le nombre d'octets nécessaires.



int A[ ][10] = { { 0,10,20,30,40,50,60,70,80,90},
                {10,11,12,13,14,15,16,17,18,19},
                { 1,12,23,34,45,56,67,78,89,90} };



réservation de 3*10*2 = 60 octets

float B[ ][2] = { { -1.05 , -1.10 },
                 { 86e-5 , 87e-5 },
                 { -12.5E4, -12.3E4} };



réservation de 3*2*4 = 24 octets

Exemple :
7.2.3. Accès aux composantes

Accès à un tableau à deux dimensions en lang. algorithmique



     <NomTableau>[<Ligne>, <Colonne>]



Accès à un tableau à deux dimensions en C



     <NomTableau>[<Ligne>][<Colonne>]

Les éléments d'un tableau de dimensions L et C se présentent de la façon
suivante:



A[0][0] A[0][1] A[0][2] . . . . . . . . . . .A[0][C-1]
A[1][0] A[1][1] A[1][2] . . . . . . . . . . .A[1][C-1]
A[2][0] A[2][1] A[2][2] . . . . . . . . . . .A[2][C-1]
.........................................

A[L-1][0] A[L-1][1] A[L-1][2] . . . . . . . . . A[L-1][C-1]




Attention !
Considérons un tableau A de dimensions L et C.

En C,

- les indices du tableau varient de 0 à L-1, respectivement de 0 à C-1.

                          ième              ième
- la composante de la N           ligne et M     colonne est notée: A[N-1]
[M-1]

En langage algorithmique,

- les indices du tableau varient de 1 à L, respectivement de 1 à C.

                          ième              ième
- la composante de la N           ligne et M     colonne est notée: A[N,M]




7.2.4. Affichage et affectation

Lors du travail avec les tableaux à deux dimensions, nous utiliserons
deux indices (p.ex: I et J), et la structure for, souvent imbriquée, pour
parcourir les lignes et les colonnes des tableaux.

- Affichage du contenu d'un tableau à deux dimensions

Traduisons le programme AFFICHER du langage algorithmique en C:



     programme AFFICHER
      | entier tableau A[5,10]
      | entier I,J
      | (* Pour chaque ligne ... *)
      | pour I variant de 1 à 5 faire
         | (* ... considérer chaque composante *)
         | pour J variant de 1 à 10 faire
            | écrire A[I,J]
         | fpour
         | (* Retour à la ligne *)
         | écrire
| fpour
     fprogramme



     main()
       {
         int A[5][10];
         int I,J;
         /* Pour chaque ligne ... */
         for (I=0; I<5; I++)
             {
               /* ... considérer chaque composante
     */
               for (J=0; J<10; J++)
                     printf("%7d", A[I][J]);
               /* Retour à la ligne */
               printf("n");
             }
         return 0;
       }

Remarques

* Avant de pouvoir afficher les composantes d'un tableau, il faut leur affecter
des valeurs.

* Pour obtenir des colonnes bien alignées lors de l'affichage, il est pratique
d'indiquer la largeur minimale de l'affichage dans la chaîne de format. Pour
afficher des matrices du type int (valeur la plus 'longue': -32768), nous
pouvons utiliser la chaîne de format "%7d" :



printf("%7d", A[I][J]);



- Affectation avec des valeurs provenant de l'extérieur

Traduisons le programme REMPLIR du langage algorithmique en C:
programme REMPLIR
 | entier tableau A[5,10]
 | entier I,J
 | (* Pour chaque ligne ... *)
 | pour I variant de 1 à 5 faire
    | (* ... considérer chaque composante *)
    | pour J variant de 1 à 10 faire
       | lire A[I,J]
    | fpour
 | fpour
fprogramme



main()
  {
    int A[5][10];
    int I,J;
    /* Pour chaque ligne ... */
    for (I=0; I<5; I++)
        /* ... considérer chaque composante
*/
        for (J=0; J<10; J++)
              scanf("%d", &A[I][J]);
    return 0;
  }
Chapitre 8: LES CHAÎNES DE CARACTÈRES


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.



  q   8.1. Déclaration et mémorisation
  q   8.2. Les chaînes de caractères constantes
  q   8.3. Initialisation de chaînes de caractères
  q   8.4. Accès aux éléments d'une chaîne
  q   8.5. Précédence alphabétique et lexicographique
  q   8.6. Travailler avec des chaînes de caractères
        r  8.6.1. Les fonctions de <stdio.h>
        r  8.6.2. Les fonctions de <string>
        r  8.6.3. Les fonctions de <stdlib>
        r  8.6.4. Les fonctions de <ctype>
  q   8.7. Tableaux de chaînes de caractères
        r  8.7.1. Déclaration, initialisation et mémorisation
        r  8.7.2. Accès aux différentes composantes



8.1. Déclaration et mémorisation

Déclaration de chaînes de caractères en langage algorithmique



      chaîne <NomVariable>

Déclaration de chaînes de caractères en C
char <NomVariable> [<Longueur>];

Exemples :



char NOM [20];
char PRENOM [20];
char PHRASE [300];



Espace à réserver

Lors de la déclaration, nous devons indiquer l'espace à réserver en mémoire
pour le stockage de la chaîne.

La représentation interne d'une chaîne de caractères est terminée par le
symbole '0' (NUL). 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 ...

Mémorisation

Le nom d'une chaîne est le représentant de l'adresse du premier caractère de
la chaîne. Pour mémoriser une variable qui doit être capable de contenir un
texte de N caractères, nous avons besoin de N+1 octets en mémoire:

Exemple: Mémorisation d'un tableau

char TXT[10] = "BONJOUR !";




8.2. Les chaînes de caractères constantes

* 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 " :

"Affichage de "guillemets" n"

* 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'}

* Plusieurs chaînes de caractères constantes qui sont séparées par des
signes d'espacement (espaces, tabulateurs ou interlignes) dans le texte du
programme seront réunies en une seule chaîne constante lors de la
compilation:



"un " "deux"
  " trois"



sera évalué à



"un deux trois"



Ainsi il est possible de définir de très longues chaînes de caractères
constantes en utilisant plusieurs lignes dans le texte du programme.

Observation

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: P.ex: 'x' a 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

8.3. Initialisation de chaînes 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 CHAINE[ ] = {'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ère constante:



char CHAINE[ ] = "Hello";


Lors de l'initialisation par [] , l'ordinateur réserve automatiquement le nombre
d'octets nécessaires pour la chaîne, c.-à-d.: le nombre de caractères + 1 (ici: 6
octets). 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.

Exemples :
8.4. Accès aux éléments d'une chaîne

L'accès à un élément d'une chaîne de caractères peut se faire de la même
façon que l'accès à un élément d'un tableau. En déclarant une chaîne par:




char A[6];




nous avons défini un tableau A avec six éléments, auxquels on peut accéder
par:

A[0], A[1], ... , A[5]

Exemple




8.5. Précédence alphabétique et lexicographique

Précédence alphabétique des caractères
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.

Relation de précédence

De la précédence alphabétique des caractères, on peut déduire une relation
de précédence 'est inférieur à' sur l'ensemble des caractères. Ainsi, on peut
dire que

'0' est inférieur à 'Z'

et noter '0' < 'Z'

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).

Précédence lexicographique des chaînes de caractères

En nous basant sur cette relation de précédence alphabétique des caractères,
nous pouvons définir une précédence lexicographique pour les chaînes de
caractères. Cette relation de précédence suit l'<<ordre du dictionnaire>> et
est définie de façon récurrente:

a) La chaîne vide "" précède lexicographiquement toutes les autres chaînes.

b) La chaîne A = "a1a2a ... ap" (p caractères) précède lexicographiquement la
chaîne B = "b1b2 ... bm" (m caractères) si l'une des deux conditions suivantes
est remplie:

1) 'a1' < 'b1'

2) 'a1' = 'b1' et

"a2a3 ... ap" précède lexicographiquement "b2b3 ... bm"

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'



(le code ASCII de ' ' est 32, et le code ASCII de 'a' est 97)



Remarque

Malheureusement, il existe différents codes de caractères (p.ex. ASCII,
EBCDIC, ISO) et l'ordre lexicographique est dépendant de la machine. Même
la fonction strcmp qui indique la précédence lexicographique de deux chaînes
de caractères (voir 8.6.2. ) dépend du code de caractères utilisé.

Conversions et tests

En tenant compte de l'ordre alphabétique des caractères, on peut contrôler le
type du caractère (chiffre, majuscule, minuscule).

Exemples :



if (C>='0' && C<='9')
   printf("Chiffren", C);
if (C>='A' && C<='Z')
   printf("Majusculen", C);
if (C>='a' && C<='z')
   printf("Minusculen", 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';



8.6. Travailler avec des chaînes de caractères

Les bibliothèques de fonctions de C contiennent une série de fonctions
spéciales pour le traitement de chaînes de caractères. Sauf indication
contraire, les fonctions décrites dans ce chapitre sont portables
conformément au standard ANSI-C.



8.6.1. Les fonctions de <stdio.h>

Comme nous l'avons déjà vu au chapitre 4 , la bibliothèque <stdio> nous offre
des fonctions qui effectuent l'entrée et la sortie des données. A côté des
fonctions printf et scanf que nous connaissons déjà, nous y trouvons les
deux fonctions puts et gets, spécialement conçues pour l'écriture et la lecture
de chaînes de caractères.

- Affichage de chaînes de caractères

printf avec le spécificateur de format %s permet d'intégrer une chaîne de
caractères dans une phrase.

En plus, le spécificateur %s permet l'indication de la largeur minimale du
champ d'affichage. Dans ce champ, les données sont justifiées à droite. Si on
indique une largeur minimale négative, la chaîne sera justifiée à gauche. Un
nombre suivant un point indique la largeur maximale pour l'affichage.

Exemples :
char NOM[] = "hello, world";
printf(":%s:", NOM);                            ->   :hello, world:
printf(":%5s:", NOM);                           ->   :hello, world:
printf(":%15s:", NOM);                          ->   : hello, world:
printf(":%-15s:", NOM);                         ->   :hello, world :
printf(":%.5s:", NOM);                          ->   :hello:



puts est idéale pour écrire une chaîne constante ou le contenu d'une variable
dans une ligne isolée.




Syntaxe:          puts( <Chaîne> )

                  puts écrit puts la chaîne de caractères désignée par
                  <Chaîne> sur stdout et provoque un retour à la ligne.
Effet:            En pratique,puts(TXT); est équivalent à
                  printf("%sn",TXT);


Exemples :



char TEXTE[] = "Voici une première ligne.";
puts(TEXTE);
puts("Voici une deuxième ligne.");



- Lecture de chaînes de caractères

scanf avec le spécificateur %s permet de lire un mot isolé à l'intérieur d'une
suite de données du même ou d'un autre type.

         scanf avec le spécificateur %s lit un mot du fichier d'entrée standard
Effet:
         stdin et le mémorise à l'adresse qui est associée à %s.

Exemple :
char LIEU[25];
int JOUR, MOIS, ANNEE;
printf("Entrez lieu et date de naissance : n");
scanf("%s %d %d %d", LIEU, &JOUR, &MOIS, &ANNEE);

Remarques importantes

* La fonction scanf a besoin des adresses de ses arguments:

* Les noms des variables numériques (int, char, long, float, ...) doivent être
marqués par le symbole '&' (voir chap 4.4.).

* Comme le nom d'une chaîne de caractères est le représentant de l'adresse
du premier caractère de la chaîne, il ne doit pas être précédé de l'opérateur
adresse '&' !

* La fonction scanf avec plusieurs arguments présuppose que l'utilisateur
connaisse exactement le nombre et l'ordre des données à introduire! Ainsi,
l'utilisation de scanf pour la lecture de chaînes de caractères est seulement
conseillée si on est forcé de lire un nombre fixé de mots en une fois.

gets est idéal pour lire une ou plusieurs lignes de texte (p.ex. des phrases)
terminées par un retour à la ligne.



Syntaxe:           gets( <Chaîne> )
                   gets lit une ligne de de caractères de stdin et la
                   copie à l'adresse indiquée par <Chaîne>. Le retour à
Effet:
                   la ligne final est remplacé par le symbole de fin de
                   chaîne '0'.



Exemple :



int MAXI = 1000;
char LIGNE
[MAXI];
gets(LIGNE);
8.6.2. Les fonctions de <string>

La bibliothèque <string> fournit une multitude de fonctions pratiques pour le
traitement de chaînes de caractères. Voici une brève description des
fonctions les plus fréquemment utilisées.

Dans le tableau suivant, <n> représente un nombre du type int. Les symboles
<s> et <t> peuvent être remplacés par :

* une chaîne de caractères constante

* le nom d'une variable déclarée comme tableau de char

* un pointeur sur char (voir chapitre 9)

              Fonctions pour le traitement de chaînes de caractères

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




Remarques

- Comme le nom d'une chaîne de caractères représente une adresse fixe en
mémoire, on ne peut pas 'affecter' une autre chaîne au nom d'un tableau:
Il faut bien copier la chaîne caractère par caractère ou utiliser la fonction
strcpy respectivement strncpy:

strcpy(A, "Hello");

- La concaténation de chaînes de caractères en C ne se fait pas par le
symbole '+' comme en langage algorithmique ou en Pascal. Il faut ou bien
copier la deuxième chaîne caractère par caractère ou bien utiliser la fonction
strcat ou strncat.

- La fonction strcmp est dépendante du code de caractères et peut fournir
différents résultats sur différentes machines (voir 8.5.).



8.6.3. Les fonctions de <stdlib>

La bibliothèque <stdlib> contient des déclarations de fonctions pour la
conversion de nombres en chaînes de caractères et vice-versa.

Chaîne --> Nombre

Les trois fonctions définies ci-dessous correspondent au standard ANSI-C et
sont portables. Le symbole <s> peut être remplacé par :

  q   une chaîne de caractères constante
  q   le nom d'une variable déclarée comme tableau de char
  q   un pointeur sur char (voir chapitre 9 )

Conversion de chaînes de caractères en nombres

                retourne la valeur numérique représentée par <s>
atoi(<s>)
                comme int
                retourne la valeur numérique représentée par <s>
atol(<s>)
                comme long
                retourne la valeur numérique représentée par <s>
atof(<s>)
                comme double (!)

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

  q   Les espaces au début d'une chaîne sont ignorés
  q   Il n'y a pas de contrôle du domaine de la cible
  q   La conversion s'arrête au premier caractère non convertible
  q   Pour une chaîne non convertible, les fonctions retournent zéro
8.6.4. Les fonctions de <ctype>

Les fonctions de <ctype> servent à classifier et à convertir des caractères.
Les symboles nationaux (é, è, ä, ü, ß, ç, ...) ne sont pas considérés. Les
fonctions de <ctype> sont indépendantes du code de caractères de la
machine et favorisent la portabilité des programmes. Dans la suite, <c>
représente une valeur du type int qui peut être représentée comme caractère.

Fonctions de classification et de conversion

Les fonctions de classification suivantes fournissent un résultat du type int
différent de zéro, si la condition respective est remplie, sinon zéro.

La fonction       retourne une valeur différente de zéro,
isupper(<c>)      si <c> est une majuscule ('A'...'Z')
islower(<c>)      si <c> est une minuscule ('a'...'z')
isdigit(<c>)      si <c> est un chiffre décimal ('0'...'9')
isalpha(<c>)      si islower(<c>) ou isupper(<c>)
isalnum(<c>)      si isalpha(<c>) ou isdigit(<c>)
                  si <c> est un chiffre hexadécimal ('0'...'9' ou 'A'...'F' ou
isxdigit(<c>)     'a'...'f')

isspace(<c>)      si <c> est un signe d'espacement (' ', 't', 'n', 'r', 'f')


Les fonctions de conversion suivantes fournissent une valeur du type int qui
peut être représentée comme caractère; la valeur originale de <c> reste
inchangée:

                    retourne <c> converti en minuscule si <c> est
tolower(<c>)
                    une majuscule
                    retourne <c> converti en majuscule si <c> est
toupper(<c>)
                    une minuscule



8.7. Tableaux de chaînes de caractères

Souvent, il est nécessaire de mémoriser une suite de mots ou de phrases
dans des variables. Il est alors pratique de créer un tableau de chaînes de
caractères, ce qui allégera les déclarations des variables et simplifiera l'accès
aux différents mots (ou phrases).

8.7.1. Déclaration, initialisation et mémorisation

Un tableau de chaînes de caractères correspond à un tableau à deux
dimensions du type char, où chaque ligne contient une chaîne de caractères.

Déclaration

La déclaration char JOUR[7][9];

réserve l'espace en mémoire pour 7 mots contenant 9 caractères (dont 8
caractères significatifs).




Initialisation

Lors de la déclaration il est possible d'initialiser toutes les composantes du
tableau par des chaînes de caractères constantes:

char JOUR[7][9]= {"lundi", "mardi", "mercredi",
      "jeudi", "vendredi", "samedi", "dimanche"};




Mémorisation

Les tableaux de chaînes sont mémorisés ligne par ligne. La variable JOUR
aura donc besoin de 7*9*1 = 63 octets en mémoire.
8.7.2. Accès aux différentes composantes

Accès aux chaînes

Il est possible d'accéder aux différentes chaînes de caractères d'un tableau,
en indiquant simplement la ligne correspondante.

Exemple :

L'exécution des trois instructions suivantes:

char JOUR[7][9]= {"lundi", "mardi", "mercredi",
   "jeudi", "vendredi", "samedi", "dimanche"};
int I = 2;
printf("Aujourd'hui, c'est %s !n", JOUR[I]);

affichera la phrase:


Aujourd'hui, c'est mercredi !


Affectation

Des expressions comme JOUR[I] représentent l'adresse du premier élément
d'une chaîne de caractères. N'essayez donc pas de 'modifier' une telle
adresse par une affectation directe !




L'attribution d'une chaîne de caractères à une composante d'un tableau de
chaînes se fait en général à l'aide de la fonction strcpy:

Exemple :

La commande

strcpy(JOUR[4], "Friday");
changera le contenu de la 5e composante du tableau JOUR de "vendredi" en
"Friday" .

Accès aux caractères

Evidemment, il existe toujours la possibilité d'accéder directement aux
différents caractères qui composent les mots du tableau.

Exemple :

L'instruction


for(I=0; I<7; I++)
   printf("%c ", JOUR[I][0]);


va afficher les premières lettres des jours de la semaine:




lmmjvsd
Chapitre 9: LES POINTEURS


                      L'importance des pointeurs en C

La plupart des langages de programmation offrent la possibilité d'accéder
aux données dans la mémoire de l'ordinateur à l'aide de pointeurs, c.-à-d. à
l'aide de variables auxquelles on peut attribuer les adresses d'autres
variables.

En C, les pointeurs jouent un rôle primordial dans la définition de fonctions:
Comme le passage des paramètres en C se fait toujours par la valeur, les
pointeurs sont le seul moyen de changer le contenu de variables déclarées
dans d'autres fonctions. Ainsi le traitement de tableaux et de chaînes de
caractères dans des fonctions serait impossible sans l'utilisation de
pointeurs (voir Chapitre 10 ).

En outre, les pointeurs nous permettent d'écrire des programmes plus
compacts et plus efficients et fournissent souvent la seule solution
raisonnable à un problème. Ainsi, la majorité des applications écrites en C
profitent extensivement des pointeurs.

Le revers de la médaille est très bien formulé par Kernighan & Ritchie dans
leur livre 'Programming in C':

" ... Les pointeurs étaient mis dans le même sac que l'instruction goto
comme une excellente technique de formuler des programmes
incompréhensibles. Ceci est certainement vrai si les pointeurs sont
employés négligemment, et on peut facilement créer des pointeurs qui
pointent 'n'importe où'. Avec une certaine discipline, les pointeurs peuvent
aussi être utilisés pour programmer de façon claire et simple. C'est
précisément cet aspect que nous voulons faire ressortir dans la suite. ..."

Cette constatation a ensuite motivé les créateurs du standard ANSI-C à
prescrire des règles explicites pour la manipulation des pointeurs.
q   9.1. Adressage de variables
        r  9.1.1. Adressage direct
        r  9.1.2. Adressage indirect
  q   9.2. Les pointeurs
        r  9.2.1. Les opérateurs de base
        r  9.2.2. Les opérations élémentaires sur pointeurs
  q   9.3. Pointeurs et tableaux
        r  9.3.1. Adressage des composantes d'un tableau
        r  9.3.2. Arithmétique des pointeurs
        r  9.3.3. Pointeurs et chaînes de caractères
        r  9.3.4. Pointeurs et tableaux à deux dimensions
  q   9.4. Tableaux de pointeurs
  q   9.5. Allocation dynamique de mémoire
        r  9.5.1. Déclaration statique de données
        r  9.5.2. Allocation dynamique
        r  9.5.3. La fonction malloc et l'opérateur sizeof
        r  9.5.4. La fonction free




9.1. Adressage de variables

Avant de parler de pointeurs, il est indiqué de brièvement passer en revue
les deux modes d'adressage principaux, qui vont d'ailleurs nous
accompagner tout au long des chapitres suivants.

9.1.1. Adressage direct

Dans la programmation, nous utilisons des variables pour stocker des
informations. La valeur d'une variable se trouve à un endroit spécifique
dans la mémoire interne de l'ordinateur. Le nom de la variable nous permet
alors d'accéder directement à cette valeur.

Adressage direct: Accès au contenu d'une variable par le nom de la variable.

Exemple :
9.1.2. Adressage indirect

  Si nous ne voulons ou ne pouvons pas utiliser le nom d'une variable A,
nous pouvons copier l'adresse de cette variable dans une variable spéciale
 P, appelée pointeur. Ensuite, nous pouvons retrouver l'information de la
                  variable A en passant par le pointeur P.

Adressage indirect: Accès au contenu d'une variable, en passant par un
pointeur qui contient l'adresse de la variable.

Exemple :

Soit A une variable contenant la valeur 10 et P un pointeur qui contient
l'adresse de A . En mémoire, A et P peuvent se présenter comme suit:




9.2. Les pointeurs

Définition: Pointeur

Un pointeur est une variable spéciale qui peut contenir
l'adresse d'une autre variable.

   En C, chaque pointeur est limité à un type de données. Il peut contenir
 l'adresse d'une variable simple de ce type ou l'adresse d'une composante
                           d'un tableau de ce type.

       Si un pointeur P contient l'adresse d'une variable A, on dit que

                               'P pointe sur A '.

Remarque
Les pointeurs et les noms de variables ont le même rôle: Ils donnent accès à
un emplacement dans la mémoire interne de l'ordinateur. Il faut quand
même bien faire la différence:

* Un pointeur est une variable qui peut 'pointer' sur différentes
adresses.

* Le nom d'une variable reste toujours lié à la même adresse.

9.2.1. Les opérateurs de base

Lors du travail avec des pointeurs, nous avons besoin

- d'un opérateur 'adresse de': &
pour obtenir l'adresse d'une variable.

- d'un opérateur 'contenu de': *
pour accéder au contenu d'une adresse.

- d'une syntaxe de déclaration
pour pouvoir déclarer un pointeur.

L'opérateur 'adresse de' : &

&<NomVariable> fournit l'adresse de la variable <NomVariable>

L'opérateur & nous est déjà familier par la fonction scanf, qui a besoin de
l'adresse de ses arguments pour pouvoir leur attribuer de nouvelles valeurs.

Exemple :



int N;
printf("Entrez un nombre entier : ");
scanf("%d", &N);

Attention !

L'opérateur & peut seulement être appliqué à des objets qui se trouvent
dans la mémoire interne, c.-à-d. à des variables et des tableaux. Il ne peut
pas être appliqué à des constantes ou des expressions.

Représentation schématique
Soit P un pointeur non initialisé




et A une variable (du même type) contenant la valeur 10 :




Alors l'instruction

P = &A;

affecte l'adresse de la variable A à la variable P. En mémoire, A et P se
présentent comme dans le graphique à la fin du chapitre 9.1.2. Dans notre
représentation schématique, nous pouvons illustrer le fait que 'P pointe sur
A' par une flèche:




L'opérateur 'contenu de' : *

*<NomPointeur> désigne le contenu de l'adresse référencée par le pointeur
<NomPointeur>

Exemple :

Soit A une variable contenant la valeur 10, B une variable contenant la
valeur 50 et P un pointeur non initialisé:




Après les instructions,
P = &A;
B = *P;
*P = 20;



- P pointe sur A ,

- le contenu de A (référencé par *P) est affecté à B, et

- le contenu de A (référencé par *P ) est mis à 20.




Déclaration d'un pointeur

<Type> *<NomPointeur>déclare un pointeur <NomPointeur> qui peut
recevoir des adresses de variables du type <Type>

Une déclaration comme int *PNUM; peut être interprétée comme suit:

         "*PNUM est du type int "
ou
         "PNUM est un pointeur sur int "
ou
         "PNUM peut contenir l'adresse d'une variable du type int "

Exemple :

Le programme complet effectuant les transformations de l'exemple ci-
dessus peut se présenter comme suit:

main()                               main()
{                                    {
/* déclarations */                   /* déclarations */
    short A = 10;                        short A, B, *P;
    short B = 50;                        /* traitement */
    short *P;            ou bien         A = 10;
    /* traitement */                     B = 50;
    P = &A;                              P = &A;
    B = *P;                              B = *P;
    *P = 20;                             *P = 20;
    return 0;                            return 0;
}                                    }

Remarque

Lors de la déclaration d'un pointeur en C, ce pointeur est lié explicitement à
un type de données. Ainsi, la variable PNUM déclarée comme pointeur sur
int ne peut pas recevoir l'adresse d'une variable d'un autre type que int.

Nous allons voir que la limitation d'un pointeur à un type de variables
n'élimine pas seulement un grand nombre de sources d'erreurs très
désagréables, mais permet une série d'opérations très pratiques sur les
pointeurs (voir 9.3.2.).

9.2.2. Les opérations élémentaires sur pointeurs

En travaillant avec des pointeurs, nous devons observer les règles
suivantes:

Priorité de * et &

*) Les opérateurs * et & ont la même priorité que les autres opérateurs
unaires (la négation !, l'incrémentation ++, la décrémentation --). Dans une
même expression, les opérateurs unaires *, &, !, ++, -- sont évalués de droite
à gauche.

*) Si un pointeur P pointe sur une variable X, alors *P peut être utilisé
partout où on peut écrire X.

Exemple

Après l'instruction

P = &X;
les expressions suivantes, sont équivalentes:

Y = *P+1             Y = X+1
*P = *P+10           X = X+10
*P += 2              X += 2
++*P                 ++X
(*P)++               X++

Dans le dernier cas, les parenthèses sont nécessaires:

Comme les opérateurs unaires * et ++ sont évalués de droite à gauche,
sans les parenthèses le pointeur P serait incrémenté, non pas l'objet
sur lequel P pointe.

On peut uniquement affecter des adresses à un pointeur.

Le pointeur NUL

Seule exception: La valeur numérique 0 (zéro) est utilisée pour indiquer
qu'un pointeur ne pointe 'nulle part'.




int *P;
P = 0;

Finalement, les pointeurs sont aussi des variables et peuvent être utilisés
comme telles. Soit P1 et P2 deux pointeurs sur int, alors l'affectation

P1 = P2;

copie le contenu de P2 vers P1. P1 pointe alors sur le même objet que P2.

Résumons:
Après les instructions:

int A;
int *P;
P = &A;



A       désigne le contenu de A
&A      désigne l'adresse de A
P       désigne l'adresse de A
*P      désigne le contenu de A

En outre:

&P     désigne l'adresse du pointeur P
*A     est illégal (puisque A n'est pas un pointeur)



9.3. Pointeurs et tableaux

En C, il existe une relation très étroite entre tableaux et pointeurs. Ainsi,
chaque opération avec des indices de tableaux peut aussi être exprimée à
l'aide de pointeurs. En général, les versions formulées avec des pointeurs
sont plus compactes et plus efficientes, surtout à l'intérieur de fonctions.
Mais, du moins pour des débutants, le 'formalisme pointeur' est un peu
inhabituel.

9.3.1. Adressage des composantes d'un tableau

Comme nous l'avons déjà constaté au chapitre 7, le nom d'un tableau
représente l'adresse de son premier élément. En d'autre termes:

&tableau[0] et tableau

sont une seule et même adresse.

En simplifiant, nous pouvons retenir que le nom d'un tableau est un
pointeur constant sur le premier élément du tableau.

Exemple :
En déclarant un tableau A de type int et un pointeur P sur int,



int A[10];
int *P;



l'instruction:

P = A; est équivalente à P = &A[0];




 Si P pointe sur une composante quelconque d'un tableau, alors P+1 pointe
               sur la composante suivante. Plus généralement,

P+i      pointe sur la i-ième composante derrière P et
P-i      pointe sur la i-ième composante devant P.

Ainsi, après l'instruction, P = A;

le pointeur P pointe sur A[0], et

*(P+1)       désigne le contenu de A[1]
*(P+2)       désigne le contenu de A[2]
...          ...
*(P+i)       désigne le contenu de A[i]

Remarque

Au premier coup d'oeil, il est bien surprenant que P+i n'adresse pas le i-
ième octet derrière P, mais la i-ième composante derrière P ...
Ceci s'explique par la stratégie de
programmation 'défensive' des créateurs du
langage C:

Si on travaille avec des pointeurs, les erreurs
les plus perfides sont causées par des pointeurs malplacés et des adresses
mal calculées. En C, le compilateur peut calculer automatiquement l'adresse
de l'élément P+i en ajoutant à P la grandeur d'une composante multipliée
par i. Ceci est possible, parce que:

- chaque pointeur est limité à un seul type de données, et

- le compilateur connaît le nombre d'octets des différents types.

Exemple :

Soit A un tableau contenant des éléments du type float et P un pointeur sur
float:



float A[20], X;
float *P;



Après les instructions,



P = A;
X = *(P+9);



Xcontient la valeur du 10-ième élément de A, (c.-à-d. celle de A[9]). Une
donnée du type float ayant besoin de 4 octets, le compilateur obtient
l'adresse P+9 en ajoutant 9 * 4 = 36 octets à l'adresse dans P.

Rassemblons les constatations ci dessus :

Comme A représente l'adresse de A[0],
*(A+1)        désigne le contenu de A[1]
*(A+2)        désigne le contenu de A[2]
...
*(A+i)        désigne le contenu de A[i]

Attention !

Il existe toujours une différence essentielle entre un pointeur et le nom d'un
tableau:

- Un pointeur est une variable,
donc des opérations comme P = A ou P++ sont permises.

- Le nom d'un tableau est une constante,
donc des opérations comme A = P ou A++ sont impossibles.

Lors de la première phase de la compilation,
toutes les expressions de la forme A[i] sont
traduites en *(A+i). En multipliant l'indice i par
la grandeur d'une composante, on obtient un
indice en octets:

         <indice en octets> = <indice élément> * <grandeur élément>

Cet indice est ajouté à l'adresse du premier élément du tableau pour obtenir
l'adresse de la composante i du tableau. Pour le calcul d'une adresse
donnée par une adresse plus un indice en octets, on utilise un mode
d'adressage spécial connu sous le nom 'adressage indexé':

              <adresse indexée> = <adresse> + <indice en octets>

Presque tous les processeurs disposent de plusieurs registres spéciaux
(registres index) à l'aide desquels on peut effectuer l'adressage indexé de
façon très efficace.

Résumons

Soit un tableau A d'un type quelconque et i un indice pour les composantes
de A, alors
A          désigne l'adresse de                      A[0]
A+i        désigne l'adresse de                      A[i]
*(A+i)     désigne le contenu de                     A[i]

Si P = A, alors



P           pointe sur l'élément                 A[0]
P+i         pointe sur l'élément                 A[i]
*(P+i)      désigne le contenu de                A[i]



Formalisme tableau et formalisme pointeur

A l'aide de ce bagage, il nous est facile de 'traduire' un programme écrit à
l'aide du 'formalisme tableau' dans un programme employant le 'formalisme
pointeur'.

Exemple :

Les deux programmes suivants copient les éléments positifs d'un tableauT
dans un deuxième tableau POS.

Formalisme tableau



      main()
      {
        int T[10] = {-3, 4, 0, -7, 3, 8, 0, -1, 4, -9};
        int POS[10];
        int I,J;
        /* indices courants dans T et POS */
        for (J=0,I=0 ; I<10 ; I++)
             if (T[I]>0)
                {
                  POS[J] = T[I];
                  J++;
                }
        return 0;
}

Nous pouvons remplacer systématiquement la notation tableau[I] par *
(tableau + I), ce qui conduit à ce programme:

Formalisme pointeur



     main()
      {
        int T[10] = {-3, 4, 0, -7, 3, 8, 0, -1, 4, -9};
        int POS[10];
        int I,J;
        /* indices courants dans T et POS */
        for (J=0,I=0 ; I<10 ; I++)
             if (*(T+I)>0)
                {
                  *(POS+J) = *(T+I);
                   J++;
                }
        return 0;
      }



Sources d'erreurs

Un bon nombre d'erreurs lors de l'utilisation de C provient de la confusion
entre soit contenu et adresse, soit pointeur et variable. Revoyons donc les
trois types de déclarations que nous connaissons jusqu'ici et résumons les
possibilités d'accès aux données qui se présentent.

Les variables et leur utilisation

int A;déclare une variable simple du type int



*A        désigne le contenu de A
&A        désigne l'adresse de A
int B[ ];déclare un tableau d'éléments du type int



         désigne l'adresse de la première composante de B (Cette
B        adresse est toujours constante).

B[i]     désigne le contenu de la composante i du tableau
&B[i]    désigne l'adresse de la composante i du tableau



en utilisant le formalisme pointeur:



B+i       désigne l'adresse de la composante i du tableau
*(B+i)    désigne le contenu de la composante i du tableau

int *P;déclare un pointeur sur des éléments du type int.

         Peut pointersur des variables simples du type int ou sur les
P
         composantes d'un tableau du type int.
         désigne l'adresse contenue dans P (Cette adresse est
P        variable)

*P       désigne le contenu de l'adresse dans P

Si P pointe dans un tableau, alors



P        désigne l'adresse de la première composante
P+i      désigne l'adresse de la i-ième composante derrière P
*(P+i)   désigne le contenu de la i-ième composante derrière P



9.3.2. Arithmétique des pointeurs

Comme les pointeurs jouent un rôle si important, le langage C soutient une
série d'opérations arithmétiques sur les pointeurs que l'on ne rencontre en
général que dans les langages machines. Le confort de ces opérations en C
est basé sur le principe suivant:

Toutes les opérations avec les pointeurs tiennent compte automatiquement
               du type et de la grandeur des objets pointés.

- Affectation par un pointeur sur le même type

Soient P1 et P2 deux pointeurs sur le même type de données, alors
l'instruction

P1 = P2;

fait pointer P1 sur le même objet que P2

- Addition et soustraction d'un nombre entier

Si P pointe sur l'élément A[i] d'un tableau, alors



P+n        pointe sur A[i+n]
P-n        pointe sur A[i-n]

- Incrémentation et décrémentation d'un pointeur

Si P pointe sur l'élément A[i] d'un tableau, alors après l'instruction



P++;          P pointe sur A[i+1]
P+=n;         P pointe sur A[i+n]
P--;          P pointe sur A[i-1]
P-=n;         P pointe sur A[i-n]

Domaine des opérations

L'addition, la soustraction, l'incrémentation et la décrémentation sur les
pointeurs sont seulement définies à l'intérieur d'un tableau. Si l'adresse
formée par le pointeur et l'indice sort du domaine du tableau, alors le
résultat n'est pas défini.

Seule exception: Il est permis de 'pointer' sur le premier octet derrière un
tableau (à condition que cet octet se trouve dans le même segment de
mémoire que le tableau). Cette règle, introduite avec le standard ANSI-C,
légalise la définition de boucles qui incrémentent le pointeur avant
l'évaluation de la condition d'arrêt.

Exemples :



int A
[10];
int *P;
P = A+9;   /* dernier élément -> légal */
P=A
           /* dernier élément + 1 -> légal */
+10;
P=A        /* dernier élément + 2 -> illégal
+11;       */
           /* premier élément - 1 -> illégal
P = A-1;
           */

- Soustraction de deux pointeurs

Soient P1 et P2 deux pointeurs qui pointent dans le même tableau:



           fournit le nombre de composantes comprises entre P1 et
P1-P2
           P2.

Le résultat de la soustraction P1-P2 est



négatif      si P1 précède P2
zéro         si P1 = P2
positif      si P2 precède P1
indéfini     si P1 et P2 ne pointent pas dans le même tableau

Plus généralement, la soustraction de deux pointeurs qui pointent dans le
même tableau est équivalente à la soustraction des indices correspondants.
- Comparaison de deux pointeurs

On peut comparer deux pointeurs par <, >, <=, >=, ==, !=.

La comparaison de deux pointeurs qui pointent dans le même tableau est
équivalente à la comparaison des indices correspondants. (Si les pointeurs
ne pointent pas dans le même tableau, alors le résultat est donné par leurs
positions relatives dans la mémoire).

9.3.3. Pointeurs et chaînes de caractères

De la même façon qu'un pointeur sur int peut contenir l'adresse d'un
nombre isolé ou d'une composante d'un tableau, un pointeur sur char peut
pointer sur un caractère isolé ou sur les éléments d'un tableau de
caractères. Un pointeur sur char peut en plus contenir l'adresse d'une
chaîne de caractères constante et il peut même être initialisé avec une telle
adresse.

A la fin de ce chapitre, nous allons anticiper avec un exemple et montrer que
les pointeurs sont les éléments indispensables mais effectifs des fonctions
en C.

- Pointeurs sur char et chaînes de caractères constantes

a) Affectation

On peut attribuer l'adresse d'une chaîne de caractères constante à un
pointeur sur char:

Exemple :



char *C;
C = "Ceci est une chaîne de caractères constante";
Nous pouvons lire cette chaîne constante (p.ex: pour l'afficher),
mais il n'est pas recommandé de la modifier, parce que le résultat
d'un programme qui essaie de modifier une chaîne de caractères
constante n'est pas prévisible en ANSI-C.

b) Initialisation

Un pointeur sur char peut être initialisé lors de la déclaration si on
lui affecte l'adresse d'une chaîne de caractères constante:



char *B = "Bonjour !";

Attention !

Il existe une différence importante entre les deux déclarations:



char A[ ] = "Bonjour !"; /* un tableau */
char *B = "Bonjour !"; /* un pointeur */

A est un tableau qui a exactement la grandeur pour contenir la chaîne de
caractères et la terminaison '0'. Les caractères de la chaîne peuvent être
changés, mais le nom A va toujours pointer sur la même adresse en
mémoire.

B est un pointeur qui est initialisé de façon à ce qu'il pointe sur une chaîne
de caractères constante stockée quelque part en mémoire. Le pointeur peut
être modifié et pointer sur autre chose. La chaîne constante peut être lue,
copiée ou affichée, mais pas modifiée.




c) Modification

Si nous affectons une nouvelle valeur à un pointeur sur une chaîne de
caractères constante, nous risquons de perdre la chaîne constante. D'autre
part, un pointeur sur char a l'avantage de pouvoir pointer sur des chaînes de
n'importe quelle longueur:

Exemple :



char *A = "Petite chaîne";
char *B = "Deuxième chaîne un peu plus longue";
A = B;

Maintenant A et B pointent sur la même chaîne; la "Petite chaîne" est
perdue:




Attention !

Les affectations discutées ci-dessus ne peuvent pas être effectuées avec
des tableaux de caractères:

Exemple :



char A[45] = "Petite chaîne";
char B[45] = "Deuxième chaîne un peu plus longue";
char C[30];
A = B; /* IMPOSSIBLE -> ERREUR !!! */
C = "Bonjour !"; /* IMPOSSIBLE -> ERREUR !!! */
Dans cet exemple, nous essayons de copier l'adresse de B dans A,
  respectivement l'adresse de la chaîne constante dans C. Ces opérations
  sont impossibles et illégales parce que l'adresse représentée par le nom
                   d'un tableau reste toujours constante.

Pour changer le contenu d'un tableau, nous devons changer les
composantes du tableau l'une après l'autre (p.ex. dans une boucle) ou
déléguer cette charge à une fonction de <stdio> ou <string>.

Conclusions:

  q   Utilisons des tableaux de caractères pour déclarer les chaînes de
      caractères que nous voulons modifier.
  q   Utilisons des pointeurs sur char pour manipuler des chaînes de
      caractères constantes (dont le contenu ne change pas).
  q   Utilisons de préférence des pointeurs pour effectuer les manipulations
      à l'intérieur des tableaux de caractères. (voir aussi les remarques ci-
      dessous).

Perspectives et motivation

- Avantages des pointeurs sur char

Comme la fin des chaînes de caractères est
marquée par un symbole spécial, nous n'avons
pas besoin de connaître la longueur des
chaînes de caractères; nous pouvons même
laisser de côté les indices d'aide et parcourir
les chaînes à l'aide de pointeurs.

Cette façon de procéder est indispensable pour traiter de chaînes de
caractères dans des fonctions. En anticipant sur la matière du chapitre 10,
nous pouvons ouvrir une petite parenthèse pour illustrer les avantages des
pointeurs dans la définition de fonctions traitant des chaînes de caractères:

Pour fournir un tableau comme paramètre à une fonction, il faut passer
l'adresse du tableau à la fonction. Or, les paramètres des fonctions sont des
variables locales, que nous pouvons utiliser comme variables d'aide. Bref,
une fonction obtenant une chaîne de caractères comme paramètre, dispose
d'une copie locale de l'adresse de la chaîne. Cette copie peut remplacer les
indices ou les variables d'aide du formalisme tableau.

Discussion d'un exemple

Reprenons l'exemple de la fonction strcpy, qui copie la chaîne CH2 vers
CH1. Les deux chaînes sont les arguments de la fonction et elles sont
déclarées comme pointeurs sur char. La première version de strcpy est
écrite entièrement à l'aide du formalisme tableau:

void strcpy(char *CH1, char *CH2)
 { int I;
   I=0;
   while ((CH1[I]=CH2[I]) != '0')
          I++;
  }

Dans une première approche, nous pourrions remplacer simplement la
notation tableau[I] par *(tableau + I), ce qui conduirait au programme:

void strcpy(char *CH1, char *CH2)
 { int I;
   I=0;
   while ((*(CH1+I)=*(CH2+I)) != '0')
          I++;
 }


Cette transformation ne nous avance guère, nous avons tout au plus gagné
quelques millièmes de secondes lors de la compilation. Un 'véritable'
avantage se laisse gagner en calculant directement avec les pointeurs CH1
et CH2 :

void strcpy(char *CH1, char *CH2)
 {
   while ((*CH1=*CH2) != '0')
        {
          CH1++;
          CH2++;
        }
 }
Comme nous l'avons déjà constaté dans l'introduction de ce manuel, un vrai
professionnel en C escaladerait les 'simplifications' jusqu'à obtenir:

void strcpy(char *CH1, char *CH2)
 {
   while (*CH1++ = *CH2++) ;
 }

Assez 'optimisé' - fermons la parenthèse et familiarisons-nous avec les
notations et les manipulations du 'formalisme pointeur' ...

9.3.4. Pointeurs et tableaux à deux dimensions

L'arithmétique des pointeurs se laisse élargir avec toutes ses conséquences
sur les tableaux à deux dimensions. Voyons cela sur un exemple:

Exemple :

Le tableauM à deux dimensions est défini comme suit:

int M[4][10] = {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
                {10,11,12,13,14,15,16,17,18,19},
                {20,21,22,23,24,25,26,27,28,29},
                {30,31,32,33,34,35,36,37,38,39}};

Le nom du tableau M représente l'adresse du premier élément du tableau et
pointe (oh, surprise...) sur le tableau M[0] qui a la valeur: {0,1,2,3,4,5,6,7,8,9}.

L'expression (M+1) est l'adresse du deuxième élément du tableau et pointe
sur M[1] qui a la valeur: {10,11,12,13,14,15,16,17,18,19}.

Explication

Au sens strict du terme, un tableau à deux dimensions est un tableau
unidimensionnel dont chaque composante est un tableau unidimensionnel.
Ainsi, le premier élément de la matrice M est le vecteur {0,1,2,3,4,5,6,7,8,9},
le deuxième élément est {10,11,12,13,14,15,16,17,18,19} et ainsi de suite.

L'arithmétique des pointeurs qui respecte automatiquement les dimensions
des éléments conclut logiquement que:

M+I désigne l'adresse du tableau M[I]
Problème

Comment pouvons-nous accéder à l'aide de pointeurs aux éléments de
chaque composante du tableau, c.à-d.: aux éléments M[0][0], M[0][1], ... , M[3]
[9] ?

Discussion

Une solution consiste à convertir la valeur de M (qui est un pointeur sur un
tableau du type int) en un pointeur sur int. On pourrait se contenter de
procéder ainsi:

int M[4][10] = {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
                {10,11,12,13,14,15,16,17,18,19},
                {20,21,22,23,24,25,26,27,28,29},
                {30,31,32,33,34,35,36,37,38,39}};
int *P;
P = M; /* conversion automatique */



Cette dernière affectation entraîne une conversion automatique de
l'adresse &M[0] dans l'adresse &M[0][0]. (Remarquez bien que
l'adresse transmise reste la même, seule la nature du pointeur a changé).

Cette solution n'est pas satisfaisante à cent pour-cent: Généralement, on
gagne en lisibilité en explicitant la conversion mise en oeuvre par
l'opérateur de conversion forcée ("cast"), qui évite en plus des messages
d'avertissement de la part du compilateur.

Solution

Voici finalement la version que nous utiliserons:
int M[4][10] = {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
                {10,11,12,13,14,15,16,17,18,19},
                {20,21,22,23,24,25,26,27,28,29},
                {30,31,32,33,34,35,36,37,38,39}};
int *P;
P = (int *)M; /* conversion forcée */



Dû à la mémorisation ligne par ligne des tableaux à deux dimensions, il
nous est maintenant possible de traiter M à l'aide du pointeur P comme un
tableau unidimensionnel de dimension 4*10.

Exemple :

Les instructions suivantes calculent la somme de tous les éléments du
tableau M:

int M[4][10] = {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
                {10,11,12,13,14,15,16,17,18,19},
                {20,21,22,23,24,25,26,27,28,29},
                {30,31,32,33,34,35,36,37,38,39}};
int *P;
int I, SOM;
P = (int*)M;
SOM = 0;
for (I=0; I<40; I++)
     SOM += *(P+I);

Attention !

Lors de l'interprétation d'un tableau à deux dimensions comme
tableau unidimensionnel il faut calculer avec le nombre de colonnes
indiqué dans la déclaration du tableau.

Exemple :

Pour la matrice A, nous réservons de la mémoire pour 3 lignes et 4
colonnes, mais nous utilisons seulement 2 lignes et 2 colonnes:
int A[3][4];
A[0][0]=1;
A[0][1]=2;
A[1][0]=10;
A[1][1]=20;

Dans la mémoire, ces composantes sont stockées comme suit :




L'adresse de l'élément A[I][J] se calcule alors par:

A + I*4 + J

Conclusion

Pour pouvoir travailler à l'aide de pointeurs dans un tableau à deux
dimensions, nous avons besoin de quatre données:

a) l'adresse du premier élément du tableau converti dans le type simple des
éléments du tableau

b) la longueur d'une ligne réservée en mémoire
(- voir déclaration - ici: 4 colonnes)

c) le nombre d'éléments effectivement utilisés dans une ligne
(- p.ex: lu au clavier - ici: 2 colonnes)

d) le nombre de lignes effectivement utilisées
(- p.ex: lu au clavier - ici: 2 lignes)

9.4. Tableaux de pointeurs

Si nous avons besoin d'un ensemble de pointeurs du même type, nous
pouvons les réunir dans un tableau de pointeurs.

Déclaration d'un tableau de pointeurs
<Type> *<NomTableau>[<N>]déclare un tableau <NomTableau> de <N>
pointeurs sur des données du type <Type>.

Exemple :



double *A[10];



déclare un tableau de 10 pointeurs sur des rationnels du type double dont
les adresses et les valeurs ne sont pas encore définies.

Remarque

Le plus souvent, les tableaux de pointeurs sont utilisés pour mémoriser de
façon économique des chaînes de caractères de différentes longueurs.
Dans la suite, nous allons surtout considérer les tableaux de pointeurs sur
des chaînes de caractères.

Initialisation

Nous pouvons initialiser les pointeurs d'un tableau sur char par les
adresses de chaînes de caractères constantes.

Exemple



char *JOUR[] = {"dimanche", "lundi", "mardi",
               "mercredi", "jeudi", "vendredi",
               "samedi"};



déclare un tableau JOUR[] de 7 pointeurs sur char. Chacun des pointeurs
est initialisé avec l'adresse de l'une des 7 chaînes de caractères.
On peut afficher les 7 chaînes de caractères en fournissant les adresses
contenues dans le tableau JOUR à printf (ou puts) :

int I;
for (I=0; I<7; I++)
     printf("%sn", JOUR[I]);

Comme JOUR[I] est un pointeur sur char, on peut afficher les premières
lettres des jours de la semaine en utilisant l'opérateur 'contenu de' :

int I;
for (I=0; I<7; I++)
     printf("%cn", *JOUR[I]);

L'expression JOUR[I]+J désigne la J-ième lettre de la I-ième chaîne. On peut
afficher la troisième lettre de chaque jour de la semaine par:

int I;
for (I=0; i<7; I++)
     printf("%cn",*(JOUR[I]+2));

Résumons : Les tableaux de pointeurs

int *D[ ];
déclare un tableau de pointeurs sur des éléments du type int



        peut pointer sur des variables simples ou sur les
D[i]    composantes d'un tableau.
désigne l'adresse contenue dans l'élément i de D (Les
D[i]        adresses dans D[i] sont variables)

*D[i]       désigne le contenu de l'adresse dans D[i]

Si D[i] pointe dans un tableau,



D[i]             désigne l'adresse de la première composante
D[i]+j           désigne l'adresse de la j-ième composante
*(D[i]+j)        désigne le contenu de la j-ième composante



9.5. Allocation dynamique de mémoire

Nous avons vu que l'utilisation de pointeurs nous permet de mémoriser
économiquement des données de différentes grandeurs. Si nous générons
ces données pendant l'exécution du programme, il nous faut des moyens
pour réserver et libérer de la mémoire au fur et à mesure que nous en avons
besoin. Nous parlons alors de l'allocation dynamique de la mémoire.

Revoyons d'abord de quelle façon la mémoire a été réservée dans les
programmes que nous avons écrits jusqu'ici.

9.5.1. Déclaration statique de données

Chaque variable dans un programme a besoin d'un certain nombre d'octets
en mémoire. Jusqu'ici, la réservation de la mémoire s'est déroulée
automatiquement par l'emploi des déclarations des données. Dans tous ces
cas, le nombre d'octets à réserver était déjà connu pendant la compilation.
Nous parlons alors de la déclaration statique des variables.

Exemples :
float A, B, C;             /* réservation de 12 octets */
short D[10][20];           /* réservation de 400 octets */
char E[ ] = {"Bonjour !"}; /* réservation de 10 octets */ char F[ ][10] =
{"un", "deux", "trois", "quatre"};
                           /* réservation de 40 octets */

Pointeurs

Le nombre d'octets à réserver pour un pointeur dépend de la machine et du
'modèle' de mémoire choisi, mais il est déjà connu lors de la compilation. Un
pointeur est donc aussi déclaré statiquement. Supposons dans la suite
qu'un pointeur ait besoin de p octets en mémoire. (En DOS: p =2 ou p = 4)

Exemples :



double *G;         /* réservation de p octets */
char *H;            /* réservation de p octets */
float *K[10];        /* réservation de 10*p octets */

Chaînes de caractères constantes

L'espace pour les chaînes de caractères constantes qui sont affectées à des
pointeurs ou utilisées pour initialiser des pointeurs sur char est aussi
réservé automatiquement:

Exemples



char *J = "Bonjour !";       /* réservation de p+10 octets */
float *K[ ] = {"un", "deux", "trois", "quatre"};
                    /* réservation de 4*p+3+5+6+7 octets */

9.5.2. Allocation dynamique

Problème

Souvent, nous devons travailler avec des données dont nous ne pouvons
pas prévoir le nombre et la grandeur lors de la programmation. Ce serait
alors un gaspillage de réserver toujours l'espace maximal prévisible. Il nous
faut donc un moyen de gérer la mémoire lors de l'exécution du programme.

Exemple :

Nous voulons lire 10 phrases au clavier et mémoriser les phrases en
utilisant un tableau de pointeurs sur char. Nous déclarons ce tableau de
pointeurs par:

char *TEXTE[10];

Pour les 10 pointeurs, nous avons besoin de 10*p octets. Ce nombre est
connu dès le départ et les octets sont réservés automatiquement. Il nous est
cependant impossible de prévoir à l'avance le nombre d'octets à réserver
pour les phrases elles-mêmes qui seront introduites lors de l'exécution du
programme ...

Allocation dynamique

La réservation de la mémoire pour les 10 phrases peut donc seulement se
faire pendant l'exécution du programme. Nous parlons dans ce cas de
l'allocation dynamique de la mémoire.

9.5.3. La fonction malloc et l'opérateur sizeof

La fonction malloc de la bibliothèque <stdlib> nous aide à localiser et à
réserver de la mémoire au cours d'un programme. Elle nous donne accès au
tas (heap); c.-à-d. à l'espace en mémoire laissé libre une fois mis en place le
DOS, les gestionnaires, les programmes résidents, le programme lui-même
et la pile (stack).

La fonction malloc



malloc( <N> ) fournit l'adresse d'un bloc en mémoire de <N> octets libres ou
la valeur zéro s'il n'y a pas assez de mémoire.

Attention !

Sur notre système, le paramètre <N> est du type unsigned int. A l'aide de
malloc, nous ne pouvons donc pas réserver plus de 65535 octets à la fois!

Exemple :
Supposons que nous ayons besoin d'un bloc en mémoire pour un texte de
4000 caractères. Nous disposons d'un pointeur T sur char (char *T). Alors
l'instruction:



T = malloc(4000);



fournit l'adresse d'un bloc de 4000 octets libres et l'affecte àT. S'il n'y a plus
assez de mémoire, T obtient la valeur zéro.

Si nous voulons réserver de la mémoire pour des données d'un type dont la
grandeur varie d'une machine à l'autre, nous avons besoin de la grandeur
effective d'une donnée de ce type. L'opérateur sizeof nous aide alors à
préserver la portabilité du programme.

L'opérateur unaire sizeof sizeof <var> fournit la grandeur de la variable
<var> sizeof <const> fournit la grandeur de
la constante <const>
sizeof (<type>) fournit la grandeur pour un objet du type <type>

Exemple :

Après la déclaration,



short A[10];
char B[5][10];



nous obtenons les résultats suivants sur un IBM-PC (ou compatible):

sizeof A                  s'évalue à 20
sizeof B                  s'évalue à 50
sizeof 4.25               s'évalue à 8
sizeof "Bonjour !"        s'évalue à 10
sizeof(float)             s'évalue à 4
sizeof(double)          s'évalue à 8

Exemple :

Nous voulons réserver de la mémoire pourX valeurs du type int; la valeur de
X est lue au clavier:

int X;
int *PNum;
printf("Introduire le nombre de valeurs :");
scanf("%d", &X);
PNum = malloc(X*sizeof(int));

exit

S'il n'y a pas assez de mémoire pour effectuer
une action avec succès, il est conseillé
d'interrompre l'exécution du programme à
l'aide de la commande exit (de <stdlib>) et de
renvoyer une valeur différente de zéro comme
code d'erreur du programme (voir aussi chapitre 10.4).

Exemple :

Le programme à la page suivante lit 10 phrases au clavier, recherche des
blocs de mémoire libres assez grands pour la mémorisation et passe les
adresses aux composantes du tableauTEXTE[ ]. S'il n'y a pas assez de
mémoire pour une chaîne, le programme affiche un message d'erreur et
interrompt le programme avec le code d'erreur -1.

Nous devons utiliser une variable d'aide INTRO comme zone intermédiaire
(non dynamique). Pour cette raison, la longueur maximale d'une phrase est
fixée à 500 caractères.
#include <stdio.h>
     #include <stdlib.h>
     #include <string.h>
     main()
      {
         /* Déclarations */
         char INTRO[500];
         char *TEXTE[10];
         int I;
         /* Traitement */
         for (I=0; I<10; I++)
             {
                gets(INTRO);
                /* Réservation de la mémoire */
                TEXTE[I] = malloc(strlen(INTRO)+1);
                /* S'il y a assez de mémoire, ... */
                if (TEXTE[I]) /* copier la phrase à l'adresse */
                                 /* fournie par malloc, ... */
                    strcpy(TEXTE[I], INTRO);
                else
                    {
                       /* sinon quitter le programme */
                       /* après un message d'erreur. */
                       printf("ERREUR: Pas assez de mémoire n");
                       exit(-1);
                     }
             }
        return 0;
      }



9.5.4. La fonction free

Si nous n'avons plus besoin d'un bloc de mémoire que nous avons réservé
à l'aide de malloc, alors nous pouvons le libérer à l'aide de la fonction free
de la bibliothèque <stdlib>.

free( <Pointeur> )libère le bloc de mémoire désigné par le <Pointeur>; n'a
pas d'effet si le pointeur a la valeur zéro.

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.

* La fonction free ne change pas le contenu du pointeur; il est conseillé
d'affecter la valeur zéro au pointeur immédiatement après avoir libéré le bloc
de mémoire qui y était attaché.

* Si nous ne libérons pas explicitement la mémoire à l'aide free, alors elle est
libérée automatiquement à la fin du programme.
Chapitre 10: LES FONCTIONS
La structuration de programmes en sous-programmes se fait en C à l'aide de
fonctions. Les fonctions en C correspondent aux fonctions et procédures en
Pascal et en langage algorithmique. Nous avons déjà utilisé des fonctions
prédéfinies dans des bibliothèques standard (printf de <stdio>, strlen de
<string>, pow de <math>, etc.). Dans ce chapitre, nous allons découvrir comment
nous pouvons définir et utiliser nos propres fonctions.

  q   10.1. Modularisation de programmes
        r 10.1.1. La modularité et ses avantages
        r 10.1.2. Exemples de modularisation en C
  q   10.2. La notion de blocs et la portée des identificateurs
        r 10.2.1. Variables locales
        r 10.2.2. Variables globales
  q   10.3. Déclaration et définition de fonctions
        r 10.3.1. Définition d'une fonction
        r 10.3.2. Déclaration d'une fonction
        r 10.3.3. Discussion d'un exemple
  q   10.4. Renvoyer un résultat
  q   10.5. Paramètres d'une fonction
        r 10.5.1. Généralités
        r 10.5.2. Passage des paramètres par valeur
        r 10.5.3. Passage de l'adresse d'une variable
        r 10.5.4. Passage de l'adresse d'un tableau à une dimension
        r 10.5.5. Passage de l'adresse d'un tableau à deux dimensions
  q   10.6. Les modules en lang. algorithmique, en Pascal et en C
  q   10.7. Discussion de deux problèmes



10.1. Modularisation de programmes

Jusqu'ici, nous avons résolu nos problèmes à l'aide de fonctions prédéfinies et
d'une seule fonction nouvelle: la fonction principale main(). Pour des problèmes
plus complexes, nous obtenons ainsi de longues listes d'instructions, peu
structurées et par conséquent peu compréhensibles. En plus, il faut souvent
répéter les mêmes suites de commandes dans le texte du programme, ce qui
entraîne un gaspillage de mémoire interne et externe.
10.1.1. La modularité et ses avantages

La plupart des langages de programmation nous permettent de subdiviser nos
programmes en sous-programmes, fonctions ou procédures plus simples et plus
compacts. A l'aide de ces structures nous pouvons modulariser nos
programmes pour obtenir des solutions plus élégantes et plus efficientes.

Modules

Dans ce contexte, un module désigne une entité de données et d'instructions qui
fournissent une solution à une (petite) partie bien définie d'un problème plus
complexe. Un module peut faire appel à d'autres modules, leur transmettre des
données et recevoir des données en retour. L'ensemble des modules ainsi reliés
doit alors être capable de résoudre le problème global.

Avantages

Voici quelques avantages d'un programme modulaire:

* Meilleure lisibilité

* Diminution du risque d'erreurs

* Possibilité de tests sélectifs

* Dissimulation des méthodes
Lors de l'utilisation d'un module il faut seulement connaître son effet, sans
devoir s'occuper des détails de sa réalisation.

* Réutilisation de modules déjà existants
Il est facile d'utiliser des modules qu'on a écris soi-même ou qui ont été
développés par d'autres personnes.

* Simplicité de l'entretien
Un module peut être changé ou remplacé sans devoir toucher aux autres
modules du programme.

* Favorisation du travail en équipe
Un programme peut être développé en équipe par délégation de la
programmation des modules à différentes personnes ou groupes de personnes.
Une fois développés, les modules peuvent constituer une base de travail
commune.

10.1.2. Exemples de modularisation en C

Les deux programmes présentés ci-dessous vous donnent un petit aperçu sur
les propriétés principales des fonctions en C. Les détails seront discutés plus
loin dans ce chapitre.

a) Exemple 1: Afficher un rectangle d'étoiles

Commençons par un petit programme que nous vous proposons d'examiner
vous-mêmes sans autres explications:

Le programme suivant permet d'afficher à l'écran un rectangle de longueur L et
de hauteur H, formé d'astérisques '*' :




Implémentation en C

     #include <stdio.h>
     main()
      {
        /* Prototypes des fonctions appelées par main */
        void RECTANGLE(int L, int H);
        /* Déclaration des variables locales de main */
        int L, H;
        /* Traitements */
        printf("Entrer la longueur (>= 1): ");
        scanf("%d", &L);
        printf("Entrer la hauteur (>= 1): ");
        scanf("%d", &H);
        /* Afficher un rectangle d'étoiles */
        RECTANGLE(L,H);
        return 0;
      }

Pour que la fonction soit exécutable par la machine, il faut encore spécifier la
fonction RECTANGLE:
void RECTANGLE(int L, int H)
     {
       /* Prototypes des fonctions appelées */
       void LIGNE(int L);
       /* Déclaration des variables locales */
       int I;
       /* Traitements */
       /* Afficher H lignes avec L étoiles */
       for (I=0; I<H; I++)
            LIGNE(L);
     }

Pour que la fonction RECTANGLE soit exécutable par la machine, il faut spécifier
la fonction LIGNE:

    void LIGNE(int L)
     {
       /* Affiche à l'écran une ligne avec L étoiles */
       /* Déclaration des variables locales */
       int I;
       /* Traitements */
       for (I=0; I<L; I++)
          printf("*");
       printf("n");
     }

Schématiquement, nous pouvons représenter la hiérarchie des fonctions du
programme comme suit:




b) Exemple 2: Tableau de valeurs d'une fonction
Soit F la fonction numérique définie par F(X) = X3-2X+1. On désire construire un
tableau de valeurs de cette fonction. Le nombre N de valeurs ainsi que les
valeurs de X sont entrés au clavier par l'utilisateur.

Exemple

Entrez un entier entre 1 et 100 : 9
Entrez 9 nombres réels : -4 -3 -2 -1 0 1 2 3 4
X      -4.0 -3.0 -2.0 -1.0 0.0 1.0 2.0 3.0 4.0
F(X) -55.0 -20.0 -3.0 2.0 1.0 0.0 5.0 22.0 57.0

En modularisant ce problème, nous obtenons un programme principal très court
et bien 'lisible'. La fonction main joue le rôle du programme principal:

     main()
      {
        float X[100]; /* valeurs de X */
        float V[100]; /* valeurs de F(X) */
        int N;
        ACQUERIR(&N); /* 1 <= N <= 100 */
        LIRE_VECTEUR(X, N);
        CALCULER_VALEURS(X, V, N);
        AFFICHER_TABLE(X, V, N);
        return 0;
      }

Pour que la machine puisse exécuter ce programme, il faut encore implémenter
les modules ACQUERIR, LIRE_VECTEUR, CALCULER_VALEURS et
AFFICHER_TABLE. Ces spécifications se font en C sous forme de fonctions qui
remplacent les fonctions et les procédures que nous connaissons en langage
algorithmique. Une 'procédure' est réalisée en C par une fonction qui fournit le
résultat void (vide). Les fonctions sont ajoutées dans le texte du programme au-
dessus ou en-dessous de la fonction main.

Si dans le texte du programme une fonction est défine après la fonction
appelante, il faut la déclarer ou bien localement à l'intérieur de la fonction
appelante ou bien globalement au début du programme. La déclaration d'une
fonction se fait à l'aide d'un 'prototype' de la fonction qui correspond en
général à la première ligne (la ligne déclarative) de la fonction.

Par convention, nous allons définir la fonction main en premier lieu. Ainsi nous
obtenons le programme suivant:
Implémentation en C



    #include <stdio.h>
    main()
     {
       /* Prototypes des fonctions appelées par main */
       void ACQUERIR(int *N);
       void LIRE_VECTEUR(float T[ ], int N);
       void CALCULER_VALEURS(float X[ ], float V[ ], int N);
       void AFFICHER_TABLE(float X[ ], float V[ ], int N);
       /* Déclaration des variables locales de main */
       float X[100]; /* valeurs de X */
       float V[100]; /* valeurs de F(X) */
       int N;
       /* Traitements */
       ACQUERIR(&N); /* 1 <= N <= 100 */
       LIRE_VECTEUR(X, N);
       CALCULER_VALEURS(X, V, N);
       AFFICHER_TABLE(X, V, N);
       return 0;
     }

    void ACQUERIR(int *N)
     {
       do
        {
          printf("Entrez un entier entre 1 et 100 : ");
          scanf("%d", N);
        }
       while (*N<1 || *N>100);
     }

    void LIRE_VECTEUR(float T[ ], int N)
     {
       /* Remplit un tableau T d'ordre N avec des nombres
          réels entrés au clavier */
       /* Déclaration des variables locales */
       int I;
       /* Remplir le tableau */
       printf("Entrez %d nombres réels :n", N);
       for (I=0; I<N; I++)
       scanf("%f", &T[I]);
     }
void CALCULER_VALEURS(float X[ ], float V[ ], int N)
      {
        /* Remplit le tableau V avec les valeurs de */
        /* F(X[I]) pour les N premières composantes */
        /* X[I] du tableau X */
        /* Prototype de la fonction F */
        float F(float X);
        /* Déclaration des variables locales */
        int I;
        /* Calculer les N valeurs */
        for (I=0; I<N; I++)
             V[I] = F(X[I]);
      }

     float F(float X)
       {
         /* Retourne la valeur numérique du polynôme défini
            par F(X) = X^3-2X+1 */
         return (X*X*X - 2*X + 1);
       }

     void AFFICHER_TABLE(float X[ ], float V[ ], int N)
      {
        /* Affiche une table de N valeurs : X contient les
           valeurs données et V contient les valeurs
           calculées. */
        /* Déclaration des variables locales */
        int I;
        /* Afficher le tableau */
        printf("n X : ");
        for (I=0; I<N; I++)
             printf("%.1f", X[I]);
        printf("n F(X): ");
        for (I=0; I<N; I++)
             printf("%.1f", V[I]);
        printf("n"); }

Le programme est composé de six fonctions dont quatre ne fournissent pas de
résultat. La fonction F retourne la valeur de F(X) comme résultat. Le résultat de F
est donc du type float; nous disons alors que 'F est du type float' ou 'F a le type
float'.
Les fonctions fournissent leurs résultats à l'aide de la commande return. La
valeur rendue à l'aide de return doit correspondre au type de la fonction, sinon
elle est automatiquement convertie dans ce type.

A la fin de l'exécution du programme, la fonction main fournit par défaut une
valeur comme code d'erreur à l'environnement. Le retour de la valeur zéro veut
dire que le programme s'est terminé normalement et sans erreurs fatales.

Le passage des paramètres en C se fait toujours par la valeur. Pour pouvoir
modifier une variable déclarée dans la procédure appelante, la fonction
appelée a besoin de l'adresse de cette variable. Le paramètre correspondant
doit donc être un pointeur et lors d'un appel de la fonction, il faut veiller à
envoyer l'adresse et non la valeur de la variable.

Dans notre exemple, la fonction ACQUERIR a besoin de l'adresse de la variable N
pour pouvoir affecter une nouvelle valeur à N. Le paramètre N doit donc être
défini comme pointeur (sur int). Lors de l'appel, il faut transmettre l'adresse de N
par &N. A l'intérieur de la fonction il faut utiliser l'opérateur 'contenu de' pour
accéder à la valeur de N. Les autres fonctions ne changent pas le contenu de N
et ont seulement besoin de sa valeur. Dans les en-têtes de ces fonctions, N est
simplement déclaré comme int.

Lorsque nous passons un tableau comme paramètre à une fonction, il ne faut
pas utiliser l'opérateur adresse & lors de l'appel, parce que le nom du tableau
représente déjà l'adresse du tableau.

Dans notre exemple, la fonction LIRE_VECTEUR modifie le contenu de la
variable X, mais lors de l'appel, il suffit d'envoyer le nom du tableau comme
paramètre.

Schématiquement, nous pouvons représenter la hiérarchie des fonctions comme
suit:
10.2. La notion de blocs et la portée des identificateurs

Les fonctions en C sont définies à l'aide de blocs d'instructions. Un bloc
d'instructions est encadré d'accolades et composé de deux parties:

Blocs d'instructions en C



{
    <déclarations locales>
    <instructions>
}

Par opposition à d'autres langages de programmation, ceci est vrai pour tous les
blocs d'instructions, non seulement pour les blocs qui renferment une fonction.
Ainsi, le bloc d'instructions d'une commande if, while ou for peut théoriquement
contenir des déclarations locales de variables et même de fonctions.

Exemple :

La variable d'aide I est déclarée à l'intérieur d'un bloc conditionnel. Si la
condition (N>0) n'est pas remplie, I n'est pas défini. A la fin du bloc conditionnel,
I disparaît.
if (N>0)
   {
     int I;
     for (I=0; I<N; I++)
     ...
   }



10.2.1. Variables locales

Les variables déclarées dans un bloc d'instructions sont uniquement visibles à
l'intérieur de ce bloc. On dit que ce sont des variables locales à ce bloc.

Exemple :

La variableNOM est définie localement dans le bloc intérieur de la fonction
HELLO. Ainsi, aucune autre fonction n'a accès à la variable NOM:



void HELLO(void);
 {
   char NOM[20];
   printf("Introduisez votre nom : ");
   gets(NOM);
   printf("Bonjour %s !n", NOM);
 }


Exemple :

La déclaration de la variable I se trouve à l'intérieur d'un bloc d'instructions
conditionnel. Elle n'est pas visible à l'extérieur de ce bloc, ni même dans la
fonction qui l'entoure.



if (N>0)
   {
     int I;
     for (I=0; I<N; I++)
          ...
   }
Attention !

Une variable déclarée à l'intérieur d'un bloc cache toutes les variables du même
nom des blocs qui l'entourent.

Exemple :

Dans la fonction suivante,




int FONCTION(int A)
 {
   int X;
   ...
   X = 100;
   ...
   while (A>10)
          {
            double X;
            ...
            X *= A;
            ...
          }
 }
la première instruction X=100 se rapporte à la variable du type int déclarée dans
le bloc extérieur de la fonction; l'instruction X*=A agit sur la variable du type
double déclarée dans la boucle while. A l'intérieur de la boucle, il est impossible
d'accéder à la variable X du bloc extérieur.




Ce n'est pas du bon style d'utiliser des noms de variables qui cachent des
variables déclarées dans des blocs extérieurs; ceci peut facilement mener à
des malentendus et à des erreurs.

La plupart des programmes C ne profitent pas de la possibilité de déclarer des
variables ou des fonctions à l'intérieur d'une boucle ou d'un bloc conditionnel.
Dans la suite, nous allons faire toutes nos déclarations locales au début des
fonctions.

10.2.2. Variables globales

Les variables déclarées au début du fichier, à l'extérieur de toutes les fonctions
sont disponibles à toutes les fonctions du programme. Ce sont alors des
variables globales. En général, les variables globales sont déclarées
immédiatement derrière les instructions #include au début du programme.

Attention !

           Les variables déclarées au début de la fonction principale main

        ne sont pasdes variables globales, mais elles sont locales à main !


Exemple :

La variable STATUS est déclarée globalement pour pouvoir être utilisée dans les
procédures A et B.

#include <stdio.h>
int STATUS;
void A(...)
 {
   ...
   if (STATUS>0)
       STATUS--;
   else
        ...
    ...
 }

void B(...)
 {
   ...
   STATUS++;
   ...
 }

Conseils

* Les variables globales sont à utiliser avec précaution, puisqu'elles créent des
liens invisibles entre les fonctions. La modularité d'un programme peut en
souffrir et le programmeur risque de perdre la vue d'ensemble.
* Il faut faire attention à ne pas cacher involontairement des variables
globales par des variables locales du même nom.
* Le codex de la programmation défensive nous conseille d'écrire nos
programmes aussi 'localement' que possible.

L'utilisation de variables globales devient inévitable, si

* plusieurs fonctions qui ne s'appellent pas ont besoin des mêmes variables, ou
* plusieurs fonctions d'un programme ont besoin du même ensemble de
variables. Ce serait alors trop encombrant de passer toutes les variables comme
paramètres d'une fonction à l'autre.

10.3. Déclaration et définition de fonctions

En général, le nom d'une fonction apparaît à trois endroits dans un programme:

1) lors de la déclaration

2) lors de la définition

3) lors de l'appel

Exemple :

Avant de parler des détails, penchons-nous sur un exemple. Dans le programme
suivant, la fonction main utilise les deux fonctions:

- ENTREE qui lit un nombre entier entré au clavier et le fournit comme résultat.
La fonction ENTREE n'a pas de paramètres.

- MAX qui renvoie comme résultat le maximum de deux entiers fournis comme
paramètres.

     #include <stdio.h>
     main()
      {
        /* Prototypes des fonctions appelées */
        int ENTREE(void);
        int MAX(int N1, int N2);
        /* Déclaration des variables */
        int A, B;
        /* Traitement avec appel des fonctions */
        A = ENTREE();
        B = ENTREE();
printf("Le maximum est %dn", MAX(A,B));
      }

     /* Définition de la fonction ENTREE */
     int ENTREE(void)
       {
         int NOMBRE;
         printf("Entrez un nombre entier : ");
         scanf("%d", &NOMBRE);
         return NOMBRE;
       }

     /* Définition de la fonction MAX */
     int MAX(int N1, int N2)
       {
         if (N1>N2)
            return N1;
         else
             return N2;
       }



10.3.1. Définition d'une fonction

Dans la définition d'une fonction, nous indiquons:

- le nom de la fonction
- le type, le nombre et les noms des paramètres de la fonction
- le type du résultat fourni par la fonction
- les données locales à la fonction
- les instructions à exécuter

Définition d'une fonction en langage algorithmique



     fonction <NomFonct> (<NomPar1>, <NomPar2>,..):<TypeRés>
      | <déclarations des paramètres>
      | <déclarations locales>
      | <instructions>
     ffonction

Définition d'une fonction en C
<TypeRés> <NomFonct> (<TypePar1> <NomPar1>, <TypePar2> <NomPar2>,... )
      {
       <déclarations locales>
       <instructions>
      }

Remarquez qu'il n'y a pas de point-virgule derrière la définition des
paramètres de la fonction.

Les identificateurs

Les noms des paramètres et de la fonction sont des identificateurs qui doivent
correspondre aux restrictions définies dans chapitre 2.2.4. Des noms bien
choisis peuvent fournir une information utile sur leur rôle. Ainsi, les
identificateurs font aussi partie de la documentation d'un programme. (La
définition et le rôle des différents types de paramètres dans une fonction seront
discutés au chapitre 10.5. "Paramètres d'une fonction".)

Attention !



Si nous choisissons un nom de fonction qui existe déjà dans une bibliothèque,
notre fonction cache la fonction prédéfinie.

Type d'une fonction

Si une fonction F fournit un résultat du type T, on dit que 'la fonction F est du
type T' ou que 'la fonction F a le type T'.

Exemple :

La fonction MAX est du type int et elle a besoin de deux paramètres du type int.
Le résultat de la fonction MAX peut être intégré dans d'autres expressions.
int MAX(int N1, int N2)
 {
   if (N1>N2)
      return N1;
   else
        return N2;
 }

Exemple :

La fonction PI fournit un résultat rationnel du type float. La liste des paramètres
de PI est déclarée comme void (vide), c.-à-d. PI n'a pas besoin de paramètres et il
faut l'appeler par: PI()



float PI(void)
  {
    return 3.1415927;
  }

Remarques

  q   Une fonction peut fournir comme résultat:
        r un type arithmétique,
        r une structure (définie par struct - pas traité dans ce cours),
        r une réunion (définie par union - pas traité dans ce cours),
        r un pointeur,
        r void(la fonction correspond alors à une 'procédure').
      Une fonction ne peut pas fournir comme résultat des tableaux, des chaînes
      de caractères ou des fonctions


      Attention: Il est cependant possible de renvoyer un pointeur sur le premier
      élément d'un tableau ou d'une chaîne de caractères.)



  q   Si une fonction ne fournit pas de résultat, il faut indiquer void (vide) comme
      type du résultat.

  q   Si une fonction n'a pas de paramètres, on peut déclarer la liste des
      paramètres comme (void) ou simplement comme () .

  q   Le type par défaut est int; autrement dit: si le type d'une fonction n'est pas
déclaré explicitement, elle est automatiquement du type int.

  q   Il est interdit de définir des fonctions à l'intérieur d'une autre fonction
      (comme en Pascal).

  q   En principe, l'ordre des définitions dans le texte du programme ne joue pas
      de rôle, mais chaque fonction doit être déclarée ou définie avant d'être
      appelée (voir aussi 10.3.3.)

Rappel: main

La fonction principale main est du type int. Elle est exécutée automatiquement
lors de l'appel du programme. A la place de la définiton:

int main(void)

on peut écrire simplement:

main()



10.3.2. Déclaration d'une fonction

En C, il faut déclarer chaque fonction avant de pouvoir l'utiliser. La déclaration
informe le compilateur du type des paramètres et du résultat de la fonction. A
l'aide de ces données, le compilateur peut contrôler si le nombre et le type des
paramètres d'une fonction sont corrects. Si dans le texte du programme la
fonction est définie avant son premier appel, elle n'a pas besoin d'être déclarée.

Prototype d'une fonction

La déclaration d'une fonction se fait par un prototype de la fonction qui indique
uniquement le type des données transmises et reçues par la fonction.

Déclaration : Prototype d'une fonction
<TypeRés> <NomFonct> (<TypePar1>, <TypePar2>, ...);
ou bien
<TypeRés> <NomFonct> (<TypePar1> <NomPar1>,
                     <TypePar2> <NomPar2>, ... );

Attention !



Lors de la déclaration, le nombre et le type des paramètres doivent
nécessairement correspondre à ceux de la définition de la fonction.
Noms des paramètres

On peut facultativement inclure les noms des paramètres dans la déclaration,
mais ils ne sont pas considérés par le compilateur. Les noms fournissent
pourtant une information intéressante pour le programmeur qui peut en déduire
le rôle des différents paramètres.

Conseil pratique

Il est d'usage de copier (à l'aide de Edit - Copy & Paste) la première ligne de la
définition de la fonction comme déclaration. (N'oubliez pas d'ajouter un point-
virgule à la fin de la déclaration !)

Règles pour la déclaration des fonctions

De façon analogue aux déclarations de variables, nous pouvons déclarer une
fonction localement ou globalement. La définition des fonctions joue un rôle
spécial pour la déclaration. En résumé, nous allons considérer les règles
suivantes:

Déclaration locale:


Une fonction peut être déclarée localement dans la fonction qui l'appelle (avant
la déclaration des variables). Elle est alors disponible à cette fonction.

Déclaration globale:
Une fonction peut être déclarée globalement au début du programme (derrière
les instructions #include). Elle est alors disponible à toutes les fonctions du
programme.

Déclaration implicite par la définition:
La fonction est automatiquement disponible à toutes les fonctions qui suivent sa
définition.

Déclaration multiple:
Une fonction peut être déclarée plusieurs fois dans le texte d'un programme,
mais les indications doivent concorder.

main
La fonction principale main n'a pas besoin d'être déclarée.

10.3.3. Discussion d'un exemple

Considérons la situation suivante:
* La fonction main appelle la fonction FA.

* La fonction FA appelle la fonction FB.

Nous obtenons donc la hiérarchie suivante:




   Il y a beaucoup de possibilités de déclarer et de définir ces fonctions. Nous
         allons retenir trois variantes qui suivent une logique conséquente:

a) Déclarations locales des fonctions et définition 'top-down'

La définition 'top-down' suit la hiérarchie des fonctions:

Nous commençons par la définition de la fonction principale main, suivie des
sous-programmes FA et FB. Nous devons déclarer explicitement FA et FB car
leurs définitions suivent leurs appels.

     /* Définition de main */
     main()
       {
         /* Déclaration locale de FA */
         int FA (int X, int Y);
         ...
         /* Appel de FA */
         I = FA(2, 3);
         ...
       }

     /* Définition de FA */
     int FA(int X, int Y)
       {
         /* Déclaration locale de FB */
         int FB (int N, int M);
         ...
         /* Appel de FB */
         J = FB(20, 30);
         ...
       }

     /* Définition de FB */
int FB(int N, int M)
      {
        ...
      }

Cet ordre de définition a l'avantage de refléter la hiérarchie des fonctions: Ainsi
l'utilisateur qui ne s'intéresse qu'à la solution globale du problème n'a qu'à lire le
début du fichier. Pour retrouver les détails d'une implémentation, il peut passer
du haut vers le bas dans le fichier. Sur ce chemin, il retrouve toutes les
dépendances des fonctions simplement en se référant aux déclarations locales.
S'il existe beaucoup de dépendances dans un programme, le nombre des
déclarations locales peut quand même s'accroître dans des dimensions
insoutenables.

b) Définition 'bottom-up' sans déclarations

La définition 'bottom-up' commence en bas de la hiérarchie:

La fonction main se trouve à la fin du fichier. Les fonctions qui traitent les détails
du problème sont définies en premier lieu.

     /* Définition de FB */
     int FB(int N, int M)
       {
         ...
       }

     /* Définition de FA */
     int FA(int X, int Y)
       {
         ...
         /* Appel de FB */
         J = FB(20, 30);
         ...
       }

     /* Définition de main */
     main()
       {
         ...
         /* Appel de FA */
         I = FA(2, 3);
         ...
       }
Comme les fonctions sont définies avant leur appel, les déclarations peuvent
être laissées de côté. Ceci allège un peu le texte du programme, mais il est
beaucoup plus difficile de retrouver les dépendances entre les fonctions.

c) Déclaration globale des fonctions et définition 'top-down'

En déclarant toutes les fonctions globalement au début du texte du programme,
nous ne sommes pas forcés de nous occuper de la dépendance entre les
fonctions. Cette solution est la plus simple et la plus sûre pour des programmes
complexes contenant une grande quantité de dépendances. Il est quand même
recommandé de définir les fonctions selon l'ordre de leur hiérarchie:

     /* Déclaration globale de FA et FB */
     int FA (int X, int Y);
     int FB (int N, int M);
     /* Définition de main */
     main()
       {
         ...
         /* Appel de FA */
         I = FA(2, 3);
         ...
       }

     /* Définition de FA */
     int FA(int X, int Y)
       {
         ...
         /* Appel de FB */
         J = FB(20, 30);
         ...
       }

     /* Définition de FB */
     int FB(int N, int M)
       {
         ...
       }

d) Conclusions

Dans la suite, nous allons utiliser l'ordre de définition 'top-down' qui reflète le
mieux la structure d'un programme. Comme nos programmes ne contiennent
pas beaucoup de dépendances, nous allons déclarer les fonctions localement
dans les fonctions appelantes.
10.4. Renvoyer un résultat

Par définition, toutes les fonctions fournissent un résultat d'un type que nous
devons déclarer. Une fonction peut renvoyer une valeur d'un type simple ou
l'adresse d'une variable ou d'un tableau.

Pour fournir un résultat en quittant une fonction, nous disposons de la
commande return:

La commande return

L'instruction return <expression>;a les effets suivants:

-   évaluation de l'<expression>
-   conversion automatique du résultat de l'expression dans le type de la fonction
-   renvoi du résultat
-   terminaison de la fonction

Exemples :

La fonction CARRE du type double calcule et fournit comme résultat le carré
d'un réel fourni comme paramètre.

double CARRE(double X)
 {
   return X*X;
 }

Nous pouvons définir nous-mêmes une fonction TAN qui calcule la tangente d'un
réel X à l'aide des fonctions sin et de cos de la bibliothèque <math>. En langage
algorithmique cette fonction se présente comme suit:

       fonction TAN(X): réel
         | donnée: réel X
         | si (cos(X) <> 0)
         | alors en TAN ranger sin(X)/cos(X)
         | sinon écrire "Erreur !"
         | fsi
       ffonction (* fin TAN *)

En C, il faut d'abord inclure le fichier en-tête de la bibliothèque <math> pour
pouvoir utiliser les fonctions prédéfinies sin et cos.
#include <math.h>
       double TAN(double X)
        {
          if (cos(X) != 0)
             return sin(X)/cos(X);
          else
               printf("Erreur !n");
        }

Si nous supposons les déclarations suivantes, double X, COT;les appels des
fonctions CARRE et TAN peuvent être intégrés dans des calculs ou des
expressions:

printf("Le carre de %f est %f n", X, CARRE(X));
printf("La tangente de %f est %f n", X, TAN(X));
COT = 1/TAN(X);

void

En C, il n'existe pas de structure spéciale pour la définition de procédures
comme en langage algorithmique. Nous pouvons cependant employer une
fonction du type void partout où nous utiliserions une procédure en langage
algorithmique.

Exemple :

La procédure LIGNE affiche L étoiles dans une ligne:

       procédure LIGNE(L)
        | donnée L
        | (* Déclarations des variables locales *)
        | entier I
        | (* Traitements *)
        | en I ranger 0
        |tant que I<>L faire
           | écrire "*"
           | en I ranger I+1
        | ftant (* I=L *)
        | écrire (* passage à la ligne *)
       fprocédure


Pour la traduction en C, nous utilisons une fonction du type void:
void LIGNE(int L)
        {
          /* Déclarations des variables locales */
          int I;
          /* Traitements */
          for (I=0; I<L; I++)
               printf("*");
          printf("n");
        }

main

Dans nos exemples, la fonction main n'a pas de paramètres et est toujours du
type int (Voir aussi Chap 2.2.2. Remarque avancée)
Typiquement, les programmes renvoient la valeur zéro comme code d'erreur s'ils
se terminent avec succès. Des valeurs différentes de zéro indiquent un arrêt
fautif ou anormal.

Remarque

Si nous quittons une fonction (d'un type différent
de void) sans renvoyer de résultat à l'aide de
return, la valeur transmise à la fonction appelante
est indéfinie. Le résultat d'une telle action est
imprévisible. Si une erreur fatale s'est produite à
l'intérieur d'une fonction, il est conseillé
d'interrompre l'exécution de tout le programme et
de renvoyer un code erreur différent de zéro à
l'environnement pour indiquer que le programme
ne s'est pas terminé normalement.

Vu sous cet angle, il est dangereux de déclarer la
fonction TAN comme nous l'avons fait plus haut: Le
cas d'une division par zéro, est bien intercepté et
reporté par un message d'erreur, mais l'exécution
du programme continue 'normalement' avec des
valeurs incorrectes.

exit

Pour remédier à ce dilemme, nous pouvons utiliser
la fonction exit qui est définie dans la bibliothèque <stdlib>. exit nous permet
d'interrompre l'exécution du programme en fournissant un code d'erreur à
l'environnement. Pour pouvoir localiser l'erreur à l'intérieur du programme, il est
avantageux d'afficher un message d'erreur qui indique la nature de l'erreur et la
fonction dans laquelle elle s'est produite.

Une version plus solide de TAN se présenterait comme suit:

     #include <math.h>
     double TAN(double X)
      {
        if (cos(X) != 0)
           return sin(X)/cos(X);
        else
            {
              printf("aFonction TAN:n"
                 "Erreur: Division par zéro !n");
              exit(-1);
              /* Code erreur -1 */
            }
      }

Ignorer le résultat

Lors de l'appel d'une fonction, l'utilisateur est libre d'accepter le résultat d'une
fonction ou de l'ignorer.

Exemple :

La fonction scanf renvoie le nombre de données correctement reçues comme
résultat. En général, nous avons ignoré ce fait:



int JOUR, MOIS, ANNEE;
printf("Entrez la date actuelle : ");
scanf("%d %d %d", &JOUR, &MOIS, &ANNEE);



Nous pouvons utiliser le résultat de scanf comme contrôle:
int JOUR, MOIS, ANNEE;
int RES;
do
  {
    printf("Entrez la date actuelle : ");
    RES = scanf("%d %d %d", &JOUR,&MOIS,&ANNEE);
  }
while (RES != 3);



10.5. Paramètres d'une fonction

Les paramètres ou arguments sont les 'boîtes aux lettres' d'une fonction. Elles
acceptent les données de l'extérieur et déterminent les actions et le résultat de la
fonction. Techniquement, nous pouvons résumer le rôle des paramètres en C de
la façon suivante:

 Les paramètres d'une fonction sont simplement des variables locales qui sont
             initialisées par les valeurs obtenues lors de l'appel.

10.5.1. Généralités

Conversion automatique

Lors d'un appel, le nombre et l'ordre des paramètres doivent nécessairement
correspondre aux indications de la déclaration de la fonction. Les paramètres
sont automatiquement convertis dans les types de la déclaration avant d'être
passés à la fonction.

Exemple

Le prototype de la fonction pow (bibliothèque <math>) est déclaré comme suit:

double pow (double, double);

Au cours des instructions,



int A, B;
...
A = pow (B, 2);
nous assistons à trois conversions automatiques:

Avant d'être transmis à la fonction, la valeur de B est convertie en double; la
valeur 2 est convertie en 2.0 . Comme pow est du type double, le résultat de la
fonction doit être converti en int avant d'être affecté à A.

void

Evidemment, il existe aussi des fonctions qui fournissent leurs résultats ou
exécutent une action sans avoir besoin de données. La liste des paramètres
contient alors la déclaration void ou elle reste vide (P.ex.: double PI(void) ou int
ENTREE() ).

10.5.2. Passage des paramètres par valeur

En C, le passage des paramètres se fait toujours par la valeur,
c.-à-d. les fonctions n'obtiennent que les valeurs de leurs paramètres et n'ont
pas d'accès aux variables elles-mêmes.

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.

A l'intérieur de la fonction, nous pouvons donc changer les valeurs des
paramètres sans influencer les valeurs originales dans les fonctions appelantes.

Exemple :

La fonction ETOILES dessine une ligne de N étoiles. Le paramètre N est modifié
à l'intérieur de la fonction:

void ETOILES(int N)
 {
   while (N>0)
     {
       printf("*");
       N--;
      }
   printf("n");
 }

En utilisant N comme compteur, nous n'avons pas besoin de l'indice d'aide I
comme dans la fonction LIGNES définie plus haut.

La fonction TRIANGLE, appelle la fonction ETOILES en utilisant la variable L
comme paramètre:
void TRIANGLE(void)
 {
   int L;
   for (L=1; L<10; L++)
       ETOILES(L);
 }


Au moment de l'appel, la valeur de L est copiée dans N. La variable N peut donc
être décrémentée à l'intérieur de ETOILES, sans influencer la valeur originale de
L.

Schématiquement, le passage des paramètres peut être représenté dans une
'grille' des valeurs:




Avantages

Le passage par valeur a l'avantage que nous pouvons utiliser les paramètres
comme des variables locales bien initialisées. De cette façon, nous avons besoin
de moins de variables d'aide.

10.5.3. Passage de l'adresse d'une variable

Comme nous l'avons constaté ci-dessus, une fonction n'obtient que les valeurs
de ses paramètres.
Pour changer la valeur d'une variable de la fonction appelante, nous
allons procéder comme suit:

- la fonction appelante doit fournir l'adresse de la variable et

- la fonction appelée doit déclarer le paramètre comme pointeur.

On peut alors atteindre la variable à l'aide du pointeur.

Discussion d'un exemple

Nous voulons écrire une fonction PERMUTER qui échange le contenu
de deux variables du type int. En première approche, nous écrivons la
fonction suivante:

void PERMUTER (int A, int B)
 {
   int AIDE;
   AIDE = A;
   A = B;
   B = AIDE;
 }

Nous appelons la fonction pour deux variables X et Y par:

PERMUTER(X, Y);

Résultat: X et Y restent inchangés !

Explication: Lors de l'appel, les valeurs de X et de Y sont copiées dans les
paramètres A et B. PERMUTER échange bien le contenu des variables locales A
et B, mais les valeurs de X et Y restent les mêmes.




Pour pouvoir modifier le contenu de X et de Y , la fonction PERMUTER a besoin
des adresses de X et Y . En utilisant des pointeurs, nous écrivons une deuxième
                                     fonction:
void PERMUTER (int *A, int *B)
 {
   int AIDE;
   AIDE = *A;
   *A = *B;
   *B = AIDE;
 }

Nous appelons la fonction par:

PERMUTER(&X, &Y);

Résultat: Le contenu des variables X et Y est échangé !

Explication: Lors de l'appel, les adresses de X et de Y sont copiées dans les
pointeurs A et B. PERMUTER échange ensuite le contenu des adresses
indiquées par les pointeurs A et B.




10.5.4. Passage de l'adresse d'un tableau à une dimension

Méthode

Comme il est impossible de passer 'la valeur' de tout un tableau à une fonction,
on fournit l'adresse d'un élément du tableau.

En général, on fournit l'adresse du premier élément du tableau, qui est donnée
par le nom du tableau.

Déclaration

Dans la liste des paramètres d'une fonction, on peut déclarer un tableau par le
nom suivi de crochets,

<type> <nom>[ ]



ou simplement par un pointeur sur le type des éléments du tableau:
<type> *<nom>

Exemple :

La fonction strlen calcule et retourne la longueur d'une chaîne de caractères
fournie comme paramètre:



int strlen(char *S)
 {
   int N;
   for (N=0; *S != '0'; S++)
       N++;
   return N;
 }

A la place de la déclaration de la chaîne comme char *Son aurait aussi pu
indiquer char S[ ]comme nous l'avons fait dans l'exemple d'introduction
(chapitre 10.1.2). Dans la suite, nous allons utiliser la première notation pour
mettre en évidence que le paramètre est un pointeur variable que nous pouvons
modifier à l'intérieur de la fonction.

Appel

Lors d'un appel, l'adresse d'un tableau peut être donnée par le nom du tableau,
par un pointeur ou par l'adresse d'un élément quelconque du tableau.

Exemple :

Après les instructions,

char CH[ ] = "Bonjour !";
char *P;
P = CH;

nous pouvons appeler la fonction strlen définie ci-dessus par:

strlen(CH)                /* résultat: 9 */
strlen(P)                 /* résultat: 9 */
strlen(&CH[4])            /* résultat: 5 */
strlen(P+2)               /* résultat: 7 */
strlen(CH+2)              /* résultat: 7 */
Dans les trois dernièrs appels, nous voyons qu'il est possible de
fournir une partie d'un tableau à une fonction, en utilisant l'adresse
d'un élément à l'intérieur de tableau comme paramètre.

Remarque pratique

Pour qu'une fonction puisse travailler correctement avec un tableau
qui n'est pas du type char, il faut aussi fournir la dimension du tableau ou le
nombre d'éléments à traiter comme paramètre, sinon la fonction risque de sortir
du domaine du tableau.

Exemple :

La fonction LIRETAB lit N données pour un tableau (unidimensionnel) du type int
et les mémorise à partir de l'adresse indiquée par le pointeur PTAB. PTAB et N
sont fournis comme paramètres.

void LIRE_TAB(int N, int *PTAB)
 {
   printf("Entrez %d valeurs : n", N);
   while(N)
      {
        scanf("%d", PTAB++);
        N-- ;
      }
 }

Dans l'appel de la fonction nous utilisons en général le nom du tableau:

LIRE_TAB(4, T);

Nous obtenons alors les grilles suivantes:




10.5.5. Passage de l'adresse d'un tableau à deux dimensions

Exemple :

Imaginons que nous voulons écrire une fonction qui calcule la somme de tous
les éléments d'une matrice de réels A dont nous fournissons les deux
dimensions N et M comme paramètres.

Problème

Comment pouvons-nous passer l'adresse de la matrice à la fonction ?

Par analogie avec ce que nous avons vu au chapitre précédent, nous pourrions
envisager de déclarer le tableau concerné dans l'en-tête de la fonction sous la
forme A[ ][ ]. Dans le cas d'un tableau à deux dimensions, cette méthode ne
fournit pas assez de données, parce que le compilateur a besoin de la deuxième
dimension du tableau pour déterminer l'adresse d'un élément       A[i][j].

Une solution praticable consiste à faire en sorte que la fonction reçoive un
pointeur (de type float*) sur le début de la matrice et de parcourir tous les
éléments comme s'il s'agissait d'un tableau à une dimension N*M.

Cela nous conduit à cette fonction:

float SOMME(float *A, int N, int M)
  {
    int I;
    float S;
    for (I=0; I<N*M; I++)
         S += A[I];
    return S;
  }


Lors d'un appel de cette fonction, la seule difficulté consiste à transmettre
l'adresse du début du tableau sous forme d'un pointeur sur float. Prenons par
exemple un tableau déclaré par float A[3][4];

Le nom A correspond à la bonne adresse, mais cette adresse est du type
"pointeur sur un tableau de 4 éléments du type float". Si notre fonction est
correctement déclarée, le compilateur la convertira automatiquement dans une
adresse du type 'pointeur sur float'.

Toutefois, comme nous l'avons déjà remarqué au chapitre 9.3.4, on gagne en
lisibilité et on évite d'éventuels messages d'avertissement si on utilise
l'opérateur de conversion forcée ("cast").

Solution

Voici finalement un programme faisant appel à notre fonction SOMME:
#include <stdio.h>
     main()
      {
        /* Prototype de la fonction SOMME */
        float SOMME(float *A, int N, int M);
        /* Déclaration de la matrice */
        float T[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9,10,11,12}};
        /* Appel de la fonction SOMME */
        printf("Somme des éléments : %f "
               "n", SOMME((float*)T, 3, 4) );
        return 0;
      }

Rappel

Rappelons encore une fois que lors de l'interprétation d'un tableau à deux
dimensions comme tableau unidimensionnel, il faut calculer les adresses des
composantes à l'aide du nombre de colonnes maximal réservé lors de la
déclaration.

Remarque

Une méthode plus propre pour éviter le cast



SOMME((float*)T, 3, 4) );

est de renvoyer explicitement l'adresse du premier élément du tableau:

SOMME(&T[0][0], 3, 4) );



10.6. Les modules en lang. algorithmique, et en C

Ce chapitre résume les différences principales entre les modules (programme
principal, fonctions) dans les différents langages de programmation que nous
connaissons.

Modules

En langage algorithmique, nous distinguons programme principal, procédures et
fonctions.

En C, il existe uniquement des fonctions. La fonction principale main se
distingue des autres fonctions par deux qualités:

a) Elle est exécutée lors de l'appel du programme.

b) Les types du résultat (int) et des paramètres (void) sont fixés.

Définition des modules

En langage algorithmique, le programme principal, les fonctions et les
procédures sont déclarés dans des blocs distincts. Il est interdit d'imbriquer
leurs définitions. La définition du programme principal précède celle des
fonctions et des procédures.

En C, il est interdit de définir des fonctions à l'intérieur d'autres fonctions, mais
nous pouvons déclarer des fonctions localement.




Variables locales

En langage algorithmique, nous pouvons déclarer

des variables locales au début des fonctions et des procédures.

En C, il est permis (mais déconseillé) de déclarer des variables locales au début
de chaque bloc d'instructions.

Variables globales

En langage algorithmique, les variables globales sont définies au début du
programme principal.

En C, les variables globales sont définies au début du fichier, à l'extérieur de
toutes les fonctions. (Les variables de la fonction principale main sont locales à
main.)
Passage des paramètres

En langage algorithmique, nous distinguons entre passage des paramètres par
valeur et passage des paramètres par référence.

En C, le passage des paramètres se fait toujours par la valeur. Pour pouvoir
changer le contenu d'une variable déclarée dans une autre fonction, il faut
utiliser un pointeur comme paramètre de passage et transmettre l'adresse de la
variable lors de l'appel.

Exemple comparatif

La fonction DIVI divise son premier paramètre A par son deuxième paramètre B
et fournit le reste de la division entière comme résultat. Le contenu du paramètre
A est modifié à l'intérieur de la fonction, le paramètre B reste inchangé. Le
programme principal appelle la fonction DIVI avec deux entiers lus au clavier et
affiche les résultats.

- Solution du problème en langage algorithmique

     programme TEST_DIVI
     entier N,D,R
     écrire "Entrer nominateur et dénominateur : "
     lire N
     lire D
     en R ranger DIVI(N,D)
     écrire "Résultat: ",N," Reste: ",R
     fprogramme

     fonction DIVI (A, B): entier
     résultat: entier A
     donnée: entier B entier C
     en C ranger A modulo B
     en A ranger A divent B
     en DIVI ranger C
     ffonction (* fin DIVI *)



* Le paramètre A est transféré par référence: Il est déclaré par le mot-clef résultat
au début de la fonction.

* Le paramètre B est transféré par valeur: Il est déclaré par le mot-clef donnée au
début de la fonction.

* Le résultat de la fonction est affecté au nom de la fonction. Cette affectation
doit se trouver à la fin la fonction.

* Dans un appel, il n'y a pas de différence entre la notation des paramètres
passés par référence et ceux passés par valeur.

- Solution du problème en C

     #include <stdio.h>
     main()
      {
        int DIVI(int *A, int B);
        int N, D, R;
        printf("Entrer nominateur et dénominateur : ");
        scanf("%d %d", &N, &D);
        R = DIVI (&N, D);
        printf("Résultat: %d Reste: %dn", N, R);
        return 0;
      }

     int DIVI (int *A, int B)
      {
        int C;
        C = *A % B;
        *A /= B;
        return C;
       }

* Le paramètre A reçoit l'adresse d'une variable: Il est déclaré comme pointeur
sur int.

* Le paramètre B reçoit la valeur d'une variable: Il est déclaré comme int.

* Le résultat de la fonction est retourné à l'aide de la commande return. Comme
l'exécution de la fonction s'arrête après la commande return, celle-ci doit se
trouver à la fin de la fonction.

* Dans un appel, le premier paramètre est une adresse. Le nom de la variable N
est donc précédé par l'opérateur adresse &.
Le deuxième paramètre est passé par valeur. Le nom de la variable est indiqué
sans désignation spéciale.
Vu de plus près, les trois langages offrent les
mêmes mécanismes pour le passage des
paramètres, mais:

>> en C nous devons veiller nous-mêmes à opérer
avec les adresses et les pointeurs respectifs si nous voulons changer le contenu
d'une variable déclarée dans une autre fonction;

>> en langage algorithmique, les mêmes opérations se déroulent derrière les
rideaux, sous l'étiquette 'passage par référence' ('call-by-reference').



10.7. Discussion de deux problèmes

"fonction" ou "procédure" ?

Problème 1: Ecrire une fonction qui lit un nombre entier au clavier en affichant
un petit texte d'invite.

Réflexion: Avant d'attaquer le problème, nous devons nous poser la question, de
quelle façon nous allons transférer la valeur lue dans la variable de la fonction
appelante. Il se présente alors deux possibilités:

a) Nous pouvons fournir la valeur comme résultat de la fonction.

b) Nous pouvons affecter la valeur à une adresse que nous obtenons comme
paramètre. Dans ce cas, le résultat de la fonction est void. Cette fonction est en
fait une "procédure" au sens du du langage algorithmique.

Résultat int ==> "fonction"

Reprenons d'abord la fonction ENTREE que nous avons définie au chapitre 10.3.:



int ENTREE(void)
 {
   int NOMBRE;
   printf("Entrez un nombre entier : ");
   scanf("%d", &NOMBRE);
   return NOMBRE;
 }


La fonction ENTREE fournit un résultat du type int qui est typiquement affecté à
une variable,

int A;
A = ENTREE();



ou intégré dans un calcul:



long SOMME;
int I;
for (I=0; I<10; I++)
     SOMME += ENTREE();



Résultat void ==> "procédure"

Nous pouvons obtenir le même effet que ENTREE en définissant une fonction
ENTRER du type void, qui affecte la valeur lue au clavier immédiatement à une
adresse fournie comme paramètre. Pour accepter cette adresse, le paramètre de
la fonction doit être déclaré comme pointeur:



void ENTRER(int *NOMBRE)
 {
   printf("Entrez un nombre entier : ");
   scanf("%d", NOMBRE);
 }



Remarque : Comme le paramètre NOMBRE est un pointeur, il n'a pas besoin
d'être précédé du symbole & dans l'instruction scanf.

Lors de l'appel, nous devons transférer l'adresse de la variable cible comme
paramètre:



int A;
ENTRER(&A);
Jusqu'ici, la définition et l'emploi de la fonction ENTRER peuvent sembler plus
simples que ceux de la fonction ENTREE. Si nous essayons d'intégrer les
valeurs lues par ENTRER dans un calcul, nous allons quand même constater que
ce n'est pas toujours le cas:



long SOMME;
int I;
int A;
for (I=0; I<10; I++)
    {
      ENTRER(&A);
      SOMME += A;
    }

Conclusions

Dans la plupart des cas, nous pouvons remplacer une fonction qui fournit un
résultat par une fonction du type void qui modifie le contenu d'une variable de la
fonction appelante. En général, la préférence pour l'une ou l'autre variante
dépend le l'utilisation de la fonction:

* Si le résultat de la fonction est typiquement intégré dans un calcul ou une
expression, alors nous employons une fonction qui fournit un résultat. En
fait, personne ne remplacerait une fonction comme double sin(double X)

par une fonction void sin(double *Y, double X)

* Si la charge principale d'une fonction est de modifier des données ou l'état
de l'environnement, sans que l'on ait besoin d'un résultat, alors il vaut mieux
utiliser une fonction du type void.

* Si une fonction doit fournir plusieurs valeurs comme résultat, il s'impose
d'utiliser une procédure du type void. Ce ne serait pas une bonne solution de
fournir une valeur comme résultat et de transmettre les autres valeurs comme
paramètres.

Exemple :

La fonction MAXMIN fournit le maximum et le minimum des valeurs d'un tableau
T de N d'entiers. Comme nous ne pouvons pas fournir les deux valeurs comme
résultat, il vaut mieux utiliser deux paramètres pointeurs MAX et MIN qui
obtiendront les adresses cibles pour les deux valeurs. Ainsi la fonction MAXMIN
est définie avec quatre paramètres:
void MAXMIN(int N, int *T, int *MAX, int *MIN)
 {
   int I;
   *MAX=*T;
   *MIN=*T;
   for (I=1; I<N; I++)
       {
         if (*(T+I)>*MAX)
            *MAX = *(T+I);
         if (*(T+I)<*MIN)
            *MIN = *(T+I);
       }
 }

Lors d'un appel de MAXMIN, il ne faut pas oublier d'envoyer les adresses des
paramètres pour MAX et MIN.

int TAB[8] = {2,5,-1,0,6,9,-4,6};
int N = 8;
int MAXVAL, MINVAL;
MAXMIN(N, TAB, &MAXVAL, &MINVAL);

Pointeur ou indice numérique ?

Problème 2: Ecrire une fonction qui fournit la position du prochain signe
d'espacement dans une chaîne de caractères ou la position de la fin de la chaîne
si elle ne contient pas de signe d'espacement. Utiliser la fonction isspace de la
bibliothèque <ctype> pour la recherche.

Réflexion: Il y a plusieurs possibilités de résoudre ce problème: Une "indication"
d'une position dans une chaîne de caractères peut être fournie par un pointeur
ou par un indice numérique. Dans la suite, nous allons développer les deux
variations.

Dans les deux cas, nous avons besoin de l'adresse du tableau qui est passée
comme paramètre char *CH à la fonction.

Résultat int

La fonction CHERCHE1 fournit l'indice de l'élément recherché comme résultat du
type int.
int CHERCHE1(char *CH)
 {
   int INDEX=0;
   while (*CH && !isspace(*CH))
       {
         CH++;
         INDEX++;
       }
   return INDEX;
 }




Cette information peut être affectée à une variable:



int I;
char TXT[40];
...
I = CHERCHE1(TXT);

ou être intégrée dans une expression en profitant même de l'arithmétique des
pointeurs:



     main()
      {
        /* Prototype de la fonction appelée */
        int CHERCHE1(char *CH);
        char TXT[40];
        printf("Entrer une phrase : ");
        gets(TXT);
        /* Affichage de la phrase sans le premier mot */
        puts(TXT + CHERCHE1(TXT));
        return 0;
      }

Résultat char*

La fonction CHERCHE2 fournit un pointeur sur l'élément recherché. Remarquez
la déclaration du résutat de la fonction CHERCHE2 comme pointeur sur char:

char *CHERCHE2(char *CH)
 {
   while (*CH && !isspace(*CH))
       CH++;
   return CH;
 }


Il se montre à nouveau que l'utilisation de pointeurs permet une solution très
compacte et élégante. Dans cette version, nous n'avons plus besoin de variables
d'aide et nous pouvons renvoyer la valeur modifiée du paramètre local CH
comme résultat. L'utilisation de la fonction peut se présenter de façon aussi
élégante:



     main()
      {
        /* Prototype de la fonction appelée */
        char *CHERCHE2(char *CH);
        char TXT[40];
        printf("Entrer une phrase : ");
        gets(TXT);
        /* Affichage de la phrase sans le premier mot */
        puts(CHERCHE2(TXT));
        return 0;
      }

Conclusion

Lors du travail avec des tableaux et surtout avec des chaînes de caractères, il est
toujours avantageux d'utiliser des pointeurs et de profiter de l'arithmétique des
pointeurs. Les fonctions employant des pointeurs lors du traitement de tableaux
permettent souvent des solutions très naturelles, d'autant plus qu'elles
disposent des adresses des tableaux dans des paramètres locaux.
Chapitre 11: LES FICHIERS SEQUENTIELS


      En C, les communications d'un programme avec son
 environnement se font par l'intermédiaire de fichiers. Pour le
    programmeur, tous les périphériques, même le clavier et
  l'écran, sont des fichiers. Jusqu'ici, nos programmes ont lu
   leurs données dans le fichier d'entrée standard, (c.-à-d.: le
 clavier) et ils ont écrit leurs résultats dans le fichier de sortie
 standard (c.-à-d.: l'écran). Nous allons voir dans ce chapitre,
comment nous pouvons créer, lire et modifier nous-mêmes des
            fichiers sur les périphériques disponibles.
 q   11.1. Définitions et propriétés

 q   11.2. La mémoire tampon

 q   11.3. Accès aux fichiers séquentiels

       r   11.3.1. Le type FILE*

       r   11.3.2. Exemple: Créer et afficher un fichier séquentiel

 q   11.4. Ouvrir et fermer des fichiers séquentiels

       r   11.4.1. Ouvrir un fichier séquentiel

       r   11.4.2. Fermer un fichier séquentiel

       r   11.4.3. Exemples: Ouvrir et fermer des fichiers en
           pratique

 q   11.5. Lire et écrire dans des fichiers séquentiels

       r   11.5.1. Traitement par enregistrements
r   11.5.2. Traitement par caractères

        r   11.5.3. Détection de la fin d'un fichier séquentiel

  q   11.6. Résumé sur les fichiers

  q   11.7. Mise à jour d'un fichier séquentiel en C

        r   11.7.1. Ajouter un enregistrement à un fichier

        r   11.7.2. Supprimer un enregistrement dans un fichier

        r   11.7.3. Modifier un enregistrement dans un fichier

11.1. Définitions et propriétés
Fichier
Un fichier (angl.: file) est un ensemble structuré de données
stocké en général sur un support externe (disquette, disque
dur, disque optique, bande magnétique, ...). Un fichier structuré
contient une suite d'enregistrements homogènes .
Fichier séquentiel
Dans des fichiers séquentiels, les enregistrements sont
mémorisés consécutivement dans l'ordre de leur entrée et
peuvent seulement être lus dans cet ordre. Si on a besoin d'un
enregistrement précis dans un fichier séquentiel, il faut lire tous
les enregistrements qui le précèdent, en commençant par le
premier.
En simplifiant, nous pouvons nous imaginer qu'un fichier
séquentiel est enregistré sur une bande magnétique:
Propriétés
Les fichiers séquentiels que nous allons considérer dans ce
cours auront les propriétés suivantes:
*) Les fichiers se trouvent ou bien en état d'écriture ou bien en
état de lecture; nous ne pouvons pas simultanément lire et
écrire dans le même fichier.
*) A un moment donné, on peut uniquement accéder à un seul
enregistrement; celui qui se trouve en face de la tête de lecture/
écriture.
*) Après chaque accès, la tête de lecture/écriture est déplacée
derrière la donnée lue en dernier lieu.
Fichiers standards
Il existe deux fichiers spéciaux qui sont définis par défaut pour
tous les programmes:
- stdin le fichier d'entrée standard
- stdout le fichier de sortie standard
En général, stdin est lié au clavier et stdout est lié à l'écran, c.-à-
d. les programmes lisent leurs données au clavier et écrivent
les résultats sur l'écran.
En UNIX et en MS-DOS, il est possible de
dévier l'entrée et la sortie standard vers
d'autres fichiers ou périphériques à l'aide
des symboles < (pour stdin ) et > (pour
stdout) :
Exemple
L'appel suivant du programme PROG lit les données dans le
fichier C:TEST.TXT au lieu du clavier et écrit les résultats sur
l'imprimante au lieu de l'écran.
PROG <C:TEST.TXT >PRN:
En fait, l'affectation de stdin et stdout est gérée par le système
d'exploitation; ainsi le programme ne 'sait' pas d'où viennent
les données et où elles vont.
11.2. La mémoire tampon
Pour des raisons d'efficacité, les accès à un fichier se font par
l'intermédiaire d'une mémoire tampon (angl.: buffer ). La
mémoire tampon est une zone de la mémoire centrale de la
machine réservée à un ou plusieurs enregistrements du fichier.
L'utilisation de la mémoire tampon a l'effet de réduire le nombre
d'accès à la périphérie d'une part et le nombre des mouvements
de la tête de lecture/écriture d'autre part.




11.3. Accès aux fichiers séquentiels
Les problèmes traitant des fichiers ont généralement la forme
suivante: un fichier donné par son nom (et en cas de besoin le
chemin d'accès sur le médium de stockage) doit être créé, lu ou
          modifié. La question qui se pose est alors:
    Comment pouvons-nous relier le nom d'un fichier sur un
   support externe avec les instructions qui donnent accès au
                      contenu du fichier ?
En résumé, la méthode employée sera la suivante:
Avant de lire ou d'écrire un fichier, l'accès est notifié par la
commande fopen. fopen accepte le nom du fichier (p.ex: "A:
ADRESSES.DAT"), négocie avec le système d'exploitation et
fournit un pointeur spécial qui sera utilisé ensuite lors de
l'écriture ou la lecture du fichier. Après les traitements, il faut
annuler la liaison entre le nom du fichier et le pointeur à l'aide
de la commande fclose.
On peut dire aussi qu'entre les événements fopen() et fclose() le
fichier est ouvert.
11.3.1. Le type FILE*

Pour pouvoir travailler avec un fichier, un programme a besoin
d'un certain nombre d'informations au sujet du fichier:
- adresse de la mémoire tampon,
- position actuelle de la tête de lecture/écriture,
- type d'accès au fichier: écriture, lecture, ...
- état d'erreur,
-...
Ces informations (dont nous n'aurons pas à nous occuper),
sont rassemblées dans une structure du type spécial FILE.
Lorsque nous ouvrons un fichier avec la commande fopen, le
système génère automatiquement un bloc du type FILE et nous
fournit son adresse.
Tout ce que nous avons à faire dans notre programme est:
*) déclarer un pointeur du type FILE* pour chaque fichier dont
nous avons besoin,
*) affecter l'adresse retournée par fopen à ce pointeur,
*) employer le pointeur à la place du nom du fichier dans toutes
les instructions de lecture ou d'écriture,
*) libérer le pointeur à la fin du traitement à l'aide de fclose.
11.3.2. Exemple: Créer et afficher un fichier séquentiel

Avant de discuter les détails du traitement des fichiers, nous
vous présentons un petit exemple comparatif qui réunit les
opérations les plus importantes sur les fichiers.
Problème
On se propose de créer un fichier qui est formé
d'enregistrements contenant comme information le nom d'une
personne. Chaque enregistrement est donc constitué d'une
seule rubrique, à savoir, le nom de la personne.




 L'utilisateur doit entrer au clavier le nom du fichier, le nombre
  de personnes et les noms des personnes. Le programme se
 chargera de créer le fichier correspondant sur disque dur ou
                            sur disquette.
Après avoir écrit et fermé le fichier, le programme va rouvrir le
même fichier en lecture et afficher son contenu, sans utiliser le
nombre d'enregistrements introduit dans la première partie.
Solution en langage algorithmique

   programme PERSONNEL
     chaîne NOM_FICHIER, NOM_PERS
     entier C,_NB_ENREG

     (* Première partie :
        Créer et remplir le fichier *)
     écrire "Entrez le nom du fichier à créer : "
     lire NOM_FICHIER
     ouvrir NOM_FICHIER en écriture
      écrire "Nombre d'enregistrements à créer : "
     lire NB_ENREG
     en C ranger 0
     tant que (C<NB_ENREG) faire
     | écrire "Entrez le nom de la personne : "
     | lire NOM_PERS
     | écrire NOM_FICHIER:NOM_PERS
     | en C ranger C+1
     ftant (* C=NB_ENREG *)
     fermer NOM_FICHIER
   (* Deuxième partie :
        Lire et afficher le contenu du fichier *)
     ouvrir NOM_FICHIER en lecture
     en C ranger 0
     tant que non(finfichier(NOM_FICHIER)) faire
     | lire NOM_FICHIER:NOM_PERS
     | écrire "NOM : ",NOM_PERS
     | en C ranger C+1
     ftant
     fermer NOM_FICHIER
   fprogramme (* fin PERSONNEL *)
Solution en langage C

   #include <stdio.h>

   main()
   {
     FILE *P_FICHIER; /* pointeur sur FILE */
     char NOM_FICHIER[30], NOM_PERS[30];
     int C,NB_ENREG;

     /* Première partie :
        Créer et remplir le fichier */
     printf("Entrez le nom du fichier à créer : ");
     scanf("%s", NOM_FICHIER);
     P_FICHIER = fopen(NOM_FICHIER, "w"); /*
   write */
     printf("Nombre d'enregistrements à créer : ");
     scanf("%d", &NB_ENREG);
     C = 0;
     while (C<NB_ENREG)
        {
          printf("Entrez le nom de la personne : ");
          scanf("%s", NOM_PERS);
          fprintf(P_FICHIER, "%sn", NOM_PERS);
          C++;
        }
     fclose(P_FICHIER);

      /* Deuxième partie :
         Lire et afficher le contenu du fichier */
      P_FICHIER = fopen(NOM_FICHIER, "r"); /* read
   */
      C = 0;
      while (!feof(P_FICHIER))
{
               fscanf(P_FICHIER, "%sn", NOM_PERS);
               printf("NOM : %sn", NOM_PERS);
               C++;
            }
         fclose(P_FICHIER);
         return 0;
     }

>> Voir aussi: Chapitre 11.4.3. Exemples: Ouvrir et fermer des
fichiers en pratique
11.4. Ouvrir et fermer des fichiers séquentiels
Avant de créer ou de lire un fichier, nous devons informer le
système de cette intention pour qu'il puisse réserver la
mémoire pour la zone d'échange et initialiser les informations
nécessaires à l'accès du fichier. Nous parlons alors de
l'ouverture d'un fichier.
Après avoir terminé la manipulation du fichier, nous devons
vider la mémoire tampon et libérer l'espace en mémoire que
nous avons occupé pendant le traitement. Nous parlons alors
de la fermeture du fichier.
L'ouverture et la fermeture de fichiers se font à l'aide des
fonctions fopen et fclose définies dans la bibliothèque standard
<stdio>.
11.4.1. Ouvrir un fichier séquentiel

Ouvrir un fichier en langage algorithmique
          ouvrir <Nom> en écriture
ou bien
          ouvrir <Nom> en lecture
<Nom> est une chaîne de caractères constante ou une variable
de type chaîne qui représente le nom du fichier sur le médium
de stockage.
Ouvrir un fichier en C - fopen
Lors de l'ouverture d'un fichier avec fopen, le système s'occupe
de la réservation de la mémoire tampon dans la mémoire
centrale et génère les informations pour un nouvel élément du
type FILE. L'adresse de ce bloc est retournée comme résultat si
l'ouverture s'est déroulée avec succès. La commande fopen
peut ouvrir des fichiers en écriture ou en lecture en dépendance
de son deuxième paramètre ("r" ou "w") :
              <FP> = fopen ( <Nom> , "w" );
ou bien
              <FP> = fopen ( <Nom> , "r" );
*) <Nom> est une chaîne de caractères constante ou une
variable de type chaîne qui représente le nom du fichier sur le
médium de stockage,
*) le deuxième argument détermine le mode d'accès au fichier:
       pour 'ouverture en        - write
 "w"
       écriture'                 -
                                 - read
 "r" pour 'ouverture en lecture'
                                 -


*) <FP> est un pointeur du type FILE* qui sera relié au fichier
sur le médium de stockage. Dans la suite du programme, il faut
utiliser <FP> au lieu de <Nom> pour référencer le fichier.
*) <FP> doit être déclaré comme:
                                  FILE *FP;
Le résultat de fopen
Si le fichier a pu être ouvert avec succès, fopen fournit
l'adresse d'un nouveau bloc du type FILE. En général, la valeur
de cette adresse ne nous intéresse pas; elle est simplement
affectée à un pointeur <FP> du type FILE* que nous utiliserons
ensuite pour accéder au fichier.
A l'apparition d'une erreur lors de l'ouverture du fichier, fopen
fournit la valeur numérique zéro qui est souvent utilisée dans
une expression conditionnelle pour assurer que le traitement ne
continue pas avec un fichier non ouvert (voir 11.4.3. Exemples).
Ouverture en écriture
Dans le cas de la création d'un nouveau fichier, le nom du
fichier est ajouté au répertoire du médium de stockage et la tête
de lecture/écriture est positionnée sur un espace libre du
médium.
Si un fichier existant est ouvert en écriture, alors son contenu
est perdu.
Si un fichier non existant est ouvert en écriture, alors il est créé
automatiquement. Si la création du fichier est impossible alors
fopen indique une erreur en retournant la valeur zéro.
Autres possibilités d'erreurs signalées par un résultat nul:
- chemin d'accès non valide,
- pas de disque/bande dans le lecteur,
- essai d'écrire sur un médium protégé contre l'écriture,
-...
Ouverture en lecture
Dans le cas de la lecture d'un fichier existant, le nom du fichier
doit être retrouvé dans le répertoire du médium et la tête de
lecture/écriture est placée sur le premier enregistrement de ce
fichier.
Possibilités d'erreurs signalées par un résultat nul:
- essai d'ouvrir un fichier non existant,
- essai d'ouvrir un fichier sans autorisation d'accès,
- essai d'ouvrir un fichier protégé contre la lecture,
-...
Remarque avancée:
Si un fichier n'a pas pu être ouvert avec
succès, (résultat NUL), un code d'erreur
est placé dans la variable errno. Ce code
désigne plus exactement la nature de l'erreur survenue. Les
codes d'erreurs sont définis dans <errno.h>.
  q   L'appel de la fonction strerror(errno) retourne un pointeur
      sur la chaîne de caractères qui décrit l'erreur dans errno.
  q   L'appel de la fonction perror(s) affiche la chaîne s et le
      message d'erreur qui est défini pour l'erreur dans errno.
11.4.2. Fermer un fichier séquentiel

Fermer un fichier en langage algorithmique
                        fermer <Nom>
<Nom> est une chaîne de caractères constante ou une variable
de type chaîne qui représente le nom du fichier que l'on désire
fermer.
Fermer un fichier en langage C
                        fclose( <FP> );
<FP> est un pointeur du type FILE* relié au nom du fichier que
l'on désire fermer.
La fonction fclose provoque le contraire de fopen:
Si le fichier a été ouvert en écriture, alors les données non
écrites de la mémoire tampon sont écrites et les données
supplémentaires (longueur du fichier, date et heure de sa
création) sont ajoutées dans le répertoire du médium de
stockage.
Si le fichier a été ouvert en lecture, alors les données non lues
de la mémoire tampon sont simplement 'jetées'.
La mémoire tampon est ensuite libérée et la liaison entre le
pointeur sur FILE et le nom du fichier correspondant est
annulée.
Après fclose() le pointeur <FP> est invalide. Des erreurs graves
pourraient donc survenir si ce pointeur est utilisé par la suite!
11.4.3. Exemples: Ouvrir et fermer des fichiers en pratique

En langage algorithmique, il suffit de simplement ouvrir et
fermer un fichier par les commandes respectives:

    programme PERSONNEL
      chaîne NOM_FICHIER
      . . .

        écrire "Entrez le nom du fichier : "
        lire NOM_FICHIER
        ouvrir NOM_FICHIER en écriture
        (* ou bien *)
        (* ouvrir NOM_FICHIER en lecture *)

        . . .

        fermer NOM_FICHIER
fprogramme (* fin PERSONNEL *)

En pratique, il faut contrôler si l'ouverture d'un fichier a été
accomplie avec succès avant de continuer les traitements. Pour
le cas d'une erreur, nous allons envisager deux réactions
différentes:
a) Répéter l'essai jusqu'à l'ouverture correcte du fichier

    #include <stdio.h>
    main()
    {
      FILE *P_FICHIER;      /* pointeur sur FILE */
      char NOM_FICHIER[30]; /* nom du fichier */
      . . .

        do
          {
          printf("Entrez le nom du fichier : ");
          scanf("%s", NOM_FICHIER);
          P_FICHIER = fopen(NOM_FICHIER, "w");
          /* ou bien */
          /* P_FICHIER = fopen(NOM_FICHIER, "r"); */
          if (!P_FICHIER)
             printf("aERREUR: Impossible d'ouvrir "
                    "le fichier: %s.n",
    NOM_FICHIER);
        }
      while (!P_FICHIER);

        . . .

        fclose(P_FICHIER);
        return 0;
    }
b) Abandonner le programme en retournant un code d'erreur
non nul - exit

   #include <stdio.h>
   #include <stdlib.h>
   main()
   {
     FILE *P_FICHIER;      /* pointeur sur FILE */
     char NOM_FICHIER[30]; /* nom du fichier */
     . . .

        printf("Entrez le nom du fichier : ");
        scanf("%s", NOM_FICHIER);
        P_FICHIER = fopen(NOM_FICHIER, "w");
        /* ou bien */
        /* P_FICHIER = fopen(NOM_FICHIER, "r"); */
        if (!P_FICHIER)
           {
             printf("aERREUR: Impossible d'ouvrir "
                    "le fichier: %s.n", NOM_FICHIER);
             exit(-1); /* Abandonner le programme en
   */
           }          /* retournant le code d'erreur -1
   */

       . . .

       fclose(P_FICHIER);
       return 0;
   }

11.5. Lire et écrire dans des fichiers séquentiels
Fichiers texte
Les fichiers que nous employons dans ce manuel sont des
fichiers texte, c.-à-d. toutes les informations dans les fichiers
sont mémorisées sous forme de chaînes de caractères et sont
organisées en lignes. Même les valeurs numériques (types int,
float, double, ...) sont stockées comme chaînes de caractères.
Pour l'écriture et la lecture des fichiers, nous allons utiliser les
fonctions standard fprintf, fscanf, fputc et fgetc qui
correspondent à printf, scanf, putchar et getchar si nous
indiquons stdout respectivement stdin comme fichiers de sortie
ou d'entrée.
11.5.1. Traitement par enregistrements

Les fichiers texte sont généralement organisés en lignes, c.-à-d.
la fin d'une information dans le fichier est marquée par le
symbole 'n':




                                  Attention !
Pour pouvoir lire correctement les enregistrements dans un
fichier séquentiel, le programmeur doit connaître l'ordre des
différentes rubriques (champs) à l'intérieur des enregistrements.
a) Ecrire une information dans un fichier séquentiel
Ecrire dans un fichier séquentiel en langage algorithmique
écrire <Nom>:<Expr1>
           écrire <Nom>:<Expr2>
           ...
           écrire <Nom>:<ExprN>

*) <Nom> est une chaîne de caractères constante ou une
variable de type chaîne qui représente le nom du fichier dans
lequel on veut écrire.
*) <Expr1>, <Expr2>, ... , <ExprN> représentent les rubriques qui
forment un enregistrement et dont les valeurs respectives sont
écrites dans le fichier.
Ecrire dans un fichier séquentiel en langage C - fprintf

           fprintf( <FP>, "<Form1>n", <Expr1>);
           fprintf( <FP>, "<Form2>n", <Expr2>);
           ...
           fprintf( <FP>, "<FormN>n", <ExprN>);

ou bien

      fprintf(<FP>,"<Form1>n<Form2>n...n<FormN>
   n", <Expr1>, <Expr2>, ... , <ExprN>);

*) <FP> est un pointeur du type FILE* qui est relié au nom du
fichier cible.
*) <Expr1>, <Expr2>, ... , <ExprN> représentent les rubriques qui
forment un enregistrement et dont les valeurs respectives sont
écrites dans le fichier.
*) <Form1>, <Form2>, ... , <FormN> représentent les
spécificateurs de format pour l'écriture des différentes
rubriques (voir chapitre 4.3.).
Remarque
L'instruction
fprintf(stdout, "Bonjourn");
est identique à
printf("Bonjourn");
Attention !
Notez que fprintf (et printf) écrit toutes les chaînes de
caractères sans le symbole de fin de chaîne '0'. Dans les
fichiers texte, il faut ajouter le symbole de fin de ligne 'n' pour
séparer les données.
b) Lire une information dans un fichier séquentiel
Lire dans un fichier séquentiel en langage algorithmique

              lire <Nom>:<Var1>
              lire <Nom>:<Var2>
              ...
              lire <Nom>:<VarN>

*) <Nom> est une chaîne de caractères constante ou une
variable de type chaîne qui représente le nom du fichier duquel
on veut lire.
*) <Var1>, <Var2>, ... , <VarN> représentent les variables qui
vont recevoir les valeurs des différentes rubriques d'un
enregistrement lu dans le fichier.
Lire dans un fichier séquentiel en langage C - fscanf
fscanf( <FP>, "<Form1>n", <Adr1>);
              fscanf( <FP>, "<Form2>n", <Adr2>);
              ...
              fscanf( <FP>, "<FormN>n", <AdrN>);

ou bien

         fscanf(<FP>,"<Form1>n<Form2>n...
   n<FormN>n", <Adr1>, <Adr2>, ... , <AdrN>);

*) <FP> est un pointeur du type FILE* qui est relié au nom du
fichier à lire.
*) <Adr1>, <Adr2>, ... , <AdrN> représentent les adresses des
variables qui vont recevoir les valeurs des différentes rubriques
d'un enregistrement lu dans le fichier.
* <Form1>, <Form2>, ... , <FormN> représentent les
spécificateurs de format pour la lecture des différentes
rubriques (voir chapitre 4.4.).
Remarque
L'instruction
fscanf(stdin, "%dn", &N);
est identique à
scanf("%dn", &N);
Attention !
Pour les fonctions scanf et fscanf tous les signes d'espacement
sont équivalents comme séparateurs. En conséquence, à l'aide
de fscanf, il nous sera impossible de lire toute une phrase dans
laquelle les mots sont séparés par des espaces.
11.5.2. Traitement par caractères

La manipulation de fichiers avec les instructions fprintf et
fscanf n'est pas assez flexible pour manipuler de façon
confortable des textes écrits. Il est alors avantageux de traiter le
fichier séquentiellement caractère par caractère.
a) Ecrire un caractère dans un fichier séquentiel - fputc
fputc( <C> , <FP> );
fputc transfère le caractère indiqué par <C> dans le fichier
référencé par <FP> et avance la position de la tête de lecture/
écriture au caractère suivant.
*) représente un caractère (valeur numérique de 0 à 255) ou le
symbole de fin de fichier EOF (voir 11.5.3.).
*) <FP> est un pointeur du type FILE* qui est relié au nom du
fichier cible.
Remarque
L'instruction
fputc('a', stdout);
est identique à
putchar('a');
b) Lire un caractère dans un fichier séquentiel - fgetc
              <C> = fgetc( <FP> );
fgetc fournit comme résultat le prochain caractère du fichier
référencé par <FP> et avance la position de la tête de lecture/
écriture au caractère suivant. A la fin du fichier, fgets retourne
EOF (voir 11.5.3.).
<C> représente une variable du type int qui peut accepter une
valeur numérique de 0 à 255 ou le symbole de fin de fichier EOF.
<FP> est un pointeur du type FILE* qui est relié au nom du
fichier à lire.
Remarque
L'instruction
C = fgetc(stdin);
est identique à
C = getchar();
11.5.3. Détection de la fin d'un fichier séquentiel

Lors de la fermeture d'un fichier ouvert en écriture, la fin du
fichier est marquée automatiquement par le symbole de fin de
fichier EOF (End Of File). Lors de la lecture d'un fichier, les
fonctions finfichier(<Nom>) respectivement feof(<FP>)
nous permettent de détecter la fin du fichier:
Détection de la fin d'un fichier en langage algorithmique

             finfichier( <Nom> )

finfichier retourne la valeur logique vrai, si la tête de
lecture du fichier référencé par <Nom> est arrivée à la fin du
fichier; sinon la valeur logique du résultat est faux.
<Nom> est une chaîne de caractères constante ou une variable
de type chaîne qui représente le nom du fichier duquel on veut
lire.
Détection de la fin d'un fichier en langage C - feof
feof( <FP> );

feof retourne une valeur différente de zéro, si la tête de lecture
du fichier référencé par <FP> est arrivée à la fin du fichier;
sinon la valeur du résultat est zéro.
<FP> est un pointeur du type FILE* qui est relié au nom du
fichier à lire.
Pour que la fonction feof détecte correctement la fin du
                                                                Atten
fichier, il faut qu'après la lecture de la dernière donnée du
fichier, la tête de lecture arrive jusqu'à la position de la
marque EOF. Nous obtenons cet effet seulement si nous
terminons aussi la chaîne de format de fscanf par un retour à la
ligne 'n' (ou par un autre signe d'espacement).
Exemple
Une boucle de lecture typique pour lire les enregistrements
d'un fichier séquentiel référencé par un pointeur FP peut avoir
la forme suivante:
  while (!feof(FP))
     {
       fscanf(FP, "%sn ... n", NOM, ... );
       . . .
     }
Exemple
Le programme suivant lit et affiche le fichier "C:AUTOEXEC.
BAT" en le parcourant caractère par caractère:
#include <stdio.h>
      #include <stdlib.h>
      main()
      {
        FILE *FP;
        FP = fopen("C:AUTOEXEC.BAT", "r");
        if (!FP)
           {
             printf("Impossible d'ouvrir le fichiern");
             exit(-1);
           }
        while (!feof(FP))
             putchar(fgetc(FP));
        fclose(FP);
        return 0;
      }

Dans une chaîne de caractères constante, il faut indiquer le
symbole '' (back-slash) par '', pour qu'il ne soit pas
confondu avec le début d'une séquence d'échappement (p.ex:
n, t, a, ...).
11.6. Résumé sur les fichiers

                      Langage algorithmique                 C
Ouverture en      ouvrir <Nom> en
                                               <FP> = fopen(<Nom>,"w");
écriture          écriture
Ouverture en lecture ouvrir <Nom> en lecture   <FP> = fopen(<Nom>,"r");
Fermeture         fermer <Nom>                 fclose(<FP>);
Fonction fin de
                  finfichier(<Nom>)            feof(<FP>)
fichier
                                               fprintf(<FP>,"...",<Adr>);
Ecriture          écrire <Nom>:<Exp>
                                               fputc(<C>, <FP>);
                                               fscanf(<FP>,"...",<Adr>);
Lecture           lire <Nom>:<Var>
                                               <C> = fgetc(<FP>);


11.7. Mise à jour d'un fichier séquentiel en C
Dans ce chapitre, nous allons résoudre les problèmes
standards sur les fichiers, à savoir:
*) l'ajoute d'un enregistrement à un fichier
*) la suppression d'un enregistrement dans un fichier
*) la modification d'un enregistrement dans un fichier
Comme il est impossible de lire et d'écrire en même temps dans
un fichier séquentiel, les modifications doivent se faire à l'aide
d'un fichier supplémentaire. Nous travaillons donc typiquement
avec au moins deux fichiers: l'ancien fichier ouvert en lecture et
le nouveau fichier ouvert en écriture:




11.7.1. Ajouter un enregistrement à un fichier

  Nous pouvons ajouter le nouvel enregistrement à différentes
                  positions dans le fichier:
                         a) Ajoute à la fin du fichier
 L'ancien fichier est entièrement copié dans le nouveau fichier,
                 suivi du nouvel enregistrement.
b) Ajoute au début du fichier
L'ancien fichier est copié derrière le nouvel enregistrement qui
                     est écrit en premier lieu.




  c) Insertion dans un fichier trié relativement à une rubrique
                 commune des enregistrements
Le nouveau fichier est créé en trois étapes:
-) copier les enregistrements de l'ancien fichier qui précèdent le
nouvel enregistrement,
-) écrire le nouvel enregistrement,
-) copier le reste des enregistrements de l'ancien fichier.
Le programme suivant effectue l'insertion d'un enregistrement à
introduire au clavier dans un fichier trié selon la seule rubrique
de ses enregistrements: le nom d'une personne. Le programme
    inclut en même temps les solutions aux deux problèmes
  précédents. La comparaison lexicographique des noms des
         personnes se fait à l'aide de la fonction strcmp.
Solution en C

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

   main()
   {
     /* Déclarations : */
     /* Noms des fichiers et pointeurs de
   référence */
     char ANCIEN[30], NOUVEAU[30];
     FILE *INFILE, *OUTFILE;
     /* Autres variables */
     char NOM_PERS[30], NOM_AJOUT[30];
     int TROUVE;

      /* Ouverture de l'ancien fichier en lecture */
      do
         {
           printf("Nom de l'ancien fichier : ");
           scanf("%s", ANCIEN);
INFILE = fopen(ANCIEN, "r");
    if (!INFILE)
        printf("aERREUR: Impossible d'ouvrir "
               "le fichier: %s.n", ANCIEN);
    }
 while (!INFILE);
 /* Ouverture du nouveau fichier en écriture */
 do
    {
      printf("Nom du nouveau fichier : ");
      scanf("%s", NOUVEAU);
      OUTFILE = fopen(NOUVEAU, "w");
      if (!OUTFILE)
          printf("aERREUR: Impossible d'ouvrir "
                 "le fichier: %s.n", NOUVEAU);
    }
 while (!OUTFILE);
 /* Saisie de l'enregistrement à insérer */
 printf("Enregistrement à insérer : ");
 scanf("%s",NOM_AJOUT);

/* Traitement */
 TROUVE = 0;
 /* Copie des enregistrements dont le nom */
 /* précéde lexicogr. celui à insérer.*/
 while (!feof(INFILE) && !TROUVE)
   {
     fscanf(INFILE, "%sn", NOM_PERS);
     if (strcmp(NOM_PERS, NOM_AJOUT) > 0)
          TROUVE = 1;
     else
          fprintf(OUTFILE, "%sn", NOM_PERS);
   }
 /* Ecriture du nouvel enregistrement, */
 fprintf(OUTFILE, "%sn", NOM_AJOUT);
/* suivi du dernier enregistrement lu. */
      if (TROUVE) fprintf(OUTFILE, "%sn",
    NOM_PERS);
      /* Copie du reste des enregistrements */
      while (!feof(INFILE))
        {
          fscanf(INFILE, "%sn", NOM_PERS);
          fprintf(OUTFILE, "%sn", NOM_PERS);
        }
      /* Fermeture des fichiers */
      fclose(OUTFILE);
      fclose(INFILE);
      return 0;
    }

11.7.2. Supprimer un enregistrement dans un fichier

Le nouveau fichier est créé en copiant tous les enregistrements
de l'ancien fichier qui précèdent l'enregistrement à supprimer et
tous ceux qui le suivent:




                                Solution en C
#include <stdio.h>
#include <string.h>

main()
{
  /* Déclarations : */
  /* Noms des fichiers et pointeurs de
référence */
  char ANCIEN[30], NOUVEAU[30];
  FILE *INFILE, *OUTFILE;
  /* Autres variables */
  char NOM_PERS[30], NOM_SUPPR[30];
  /* Ouverture de l'ancien fichier en lecture */
  do
     {
       printf("Nom de l'ancien fichier : ");
       scanf("%s", ANCIEN);
       INFILE = fopen(ANCIEN, "r");
       if (!INFILE)
           printf("aERREUR: Impossible d'ouvrir "
                  "le fichier: %s.n", ANCIEN);
     }
  while (!INFILE);
  /* Ouverture du nouveau fichier en écriture */
  do
     {
       printf("Nom du nouveau fichier : ");
       scanf("%s", NOUVEAU);
       OUTFILE = fopen(NOUVEAU, "w");
       if (!OUTFILE)
           printf("aERREUR: Impossible d'ouvrir "
                  "le fichier: %s.n", NOUVEAU);
     }
  while (!OUTFILE);
  /* Saisie de l'enregistrement à supprimer */
printf("Enregistrement à supprimer : ");
        scanf("%s",NOM_SUPPR);
        /* Traitement */
        /* Copie de tous les enregistrements à */
        /* l'exception de celui à supprimer.    */
        while (!feof(INFILE))
          {
            fscanf(INFILE, "%sn", NOM_PERS);
            if (strcmp(NOM_PERS, NOM_SUPPR) != 0)
                 fprintf(OUTFILE, "%sn", NOM_PERS);
          }
        /* Fermeture des fichiers */
        fclose(OUTFILE);
        fclose(INFILE);
        return 0;
    }

11.7.3. Modifier un enregistrement dans un fichier

Le nouveau fichier est créé de tous les enregistrements de
l'ancien fichier qui précèdent l'enregistrement à modifier, de
l'enregistrement modifié et de tous les enregistrements qui
suivent l'enregistrement à modifier dans l'ancien fichier:




                                Solution en C
#include <stdio.h>
#include <string.h>

main()
{
  /* Déclarations : */
  /* Noms des fichiers et pointeurs de
référence */
  char ANCIEN[30], NOUVEAU[30];
  FILE *INFILE, *OUTFILE;
  /* Autres variables */
  char NOM_PERS[30], NOM_MODIF[30], NOM_NOUV
[30];
  /* Ouverture de l'ancien fichier en lecture */
  do
     {
       printf("Nom de l'ancien fichier : ");
       scanf("%s", ANCIEN);
       INFILE = fopen(ANCIEN, "r");
       if (!INFILE)
           printf("aERREUR: Impossible d'ouvrir "
                  "le fichier: %s.n", ANCIEN);
     }
  while (!INFILE);

  /* Ouverture du nouveau fichier en écriture */
  do
     {
       printf("Nom du nouveau fichier : ");
       scanf("%s", NOUVEAU);
       OUTFILE = fopen(NOUVEAU, "w");
       if (!OUTFILE)
           printf("aERREUR: Impossible d'ouvrir "
                  "le fichier: %s.n", NOUVEAU);
     }
while (!OUTFILE);
    /* Saisie de l'enregistrement à modifier, */
    printf("Enregistrement à modifier : ");
    scanf("%s",NOM_MODIF);
    /* et de sa nouvelle valeur. */
    printf("Enregistrement nouveau : ");
    scanf("%s",NOM_NOUV);
    /* Traitement */
    /* Copie de tous les enregistrements en    */
    /* remplaçant l'enregistrement à modifier */
    /* par sa nouvelle valeur. */
    while (!feof(INFILE))
      {
        fscanf(INFILE, "%sn", NOM_PERS);
        if (strcmp(NOM_PERS, NOM_MODIF) = 0)
             fprintf(OUTFILE, "%sn", NOM_NOUV);
        else
             fprintf(OUTFILE, "%sn", NOM_PERS);
      }
    /* Fermeture des fichiers */
    fclose(OUTFILE);
    fclose(INFILE);
    return 0;
}

Contenu connexe

PDF
Cours de c
PDF
Cours de programmation en c
PDF
Ch2-Notions de base & actions élémentaires.pdf
PDF
Chapitre 3 tableaux et pointeurs en C
PDF
Seance 1 - Programmation en langage C
PDF
Fascicule tp programmation c
PDF
Cours+sql++ +base+de+données+-+bac+informatique+(2009-2010)++elève++khmiri+zied
PDF
Ch1-Généralités.pdf
Cours de c
Cours de programmation en c
Ch2-Notions de base & actions élémentaires.pdf
Chapitre 3 tableaux et pointeurs en C
Seance 1 - Programmation en langage C
Fascicule tp programmation c
Cours+sql++ +base+de+données+-+bac+informatique+(2009-2010)++elève++khmiri+zied
Ch1-Généralités.pdf

Tendances (20)

PDF
Correction examen-java-avancé-1
PDF
Cours Programmation Orientée Objet en C++
PDF
Ch5-les tableaux et les pointeurs.pdf
PDF
Chap1: Cours en C++
PPSX
Algorithme & structures de données Chap III
PDF
Travaux dirigés 1: algorithme & structures de données (corrigés)
PDF
PDF
Cours structures des données (langage c)
PDF
Cours algorithmique et complexite complet
PDF
Pour Écrire un Bon Rapport en Informatique
PDF
Qcm en-informatique-avec-correction
PDF
Cours linux complet
PDF
Systeme embarque
PDF
Cours en maintenance pc
PPTX
cour PIC16F877.pptx
PDF
exercices Corrigées du merise
PPTX
Initiation à l'algorithmique
PPT
Cours langage c
PDF
Introduction àJava
Correction examen-java-avancé-1
Cours Programmation Orientée Objet en C++
Ch5-les tableaux et les pointeurs.pdf
Chap1: Cours en C++
Algorithme & structures de données Chap III
Travaux dirigés 1: algorithme & structures de données (corrigés)
Cours structures des données (langage c)
Cours algorithmique et complexite complet
Pour Écrire un Bon Rapport en Informatique
Qcm en-informatique-avec-correction
Cours linux complet
Systeme embarque
Cours en maintenance pc
cour PIC16F877.pptx
exercices Corrigées du merise
Initiation à l'algorithmique
Cours langage c
Introduction àJava
Publicité

Similaire à Cours langage-c (20)

PPTX
Programmation-en-C-ESIITECH-2023-2024.pptx
PPTX
Programmation-en-C-ESIITECH-2023-2024-bis.pptx
PDF
Cours programmation en langage C.pdf
PDF
Langage C
PDF
Cours.langage c
PDF
Cours_C_for_Etudiant.pdf
PDF
Le langage C.pdf
PDF
cour formation c Langage C_ Licence QL.pdf
PDF
Langage c
PPTX
programmationcv1-181029122934language.pptx
PDF
SLIDES COURS ARCHITECTURE DES ORDINATEURS.pdf
PDF
Chapitre 01 - Notions de base
PDF
Chapitre Eléments de Bases du Langage C
PPT
introC.pptxgfggggggggggffffffffffffffffffffffffffffffffffff
PPT
introC.ppteeeeeeeeeeeeeeeeeeevvvvvvvvvvvvvvvvvvvvvvvvvvvveeeee
PPT
C language presentation course and examples
PPT
Cours de programmation en langage C. idéal pour les apprenants du langage C
PDF
Programmation en C
PDF
Langage C
PPT
Langage_C.azadaddzazazazaazeazeazeazeazeazeaze"e"&"e""rfrff"r"e"&e"e"
Programmation-en-C-ESIITECH-2023-2024.pptx
Programmation-en-C-ESIITECH-2023-2024-bis.pptx
Cours programmation en langage C.pdf
Langage C
Cours.langage c
Cours_C_for_Etudiant.pdf
Le langage C.pdf
cour formation c Langage C_ Licence QL.pdf
Langage c
programmationcv1-181029122934language.pptx
SLIDES COURS ARCHITECTURE DES ORDINATEURS.pdf
Chapitre 01 - Notions de base
Chapitre Eléments de Bases du Langage C
introC.pptxgfggggggggggffffffffffffffffffffffffffffffffffff
introC.ppteeeeeeeeeeeeeeeeeeevvvvvvvvvvvvvvvvvvvvvvvvvvvveeeee
C language presentation course and examples
Cours de programmation en langage C. idéal pour les apprenants du langage C
Programmation en C
Langage C
Langage_C.azadaddzazazazaazeazeazeazeazeazeaze"e"&"e""rfrff"r"e"&e"e"
Publicité

Dernier (20)

DOCX
ENDODONTIE CONSERVATRICE.docx faculté de médecine dentaire
PPTX
Présentation Projet Entreprise Minimaliste Moderne Sobre Blanc Noir.pptx
PPTX
Copie de Présentation Personal Branding J2025.pptx_20250610_120558_0000.pptx
PPTX
SESSION1-SUPPORT-DE-COURS-FLEC-(Future leader en énergie au Cameroun)-CECOSDA...
PPTX
plus que ´parfait--.-.-.-..-.--.-..-.-.-.-.
DOC
Le verbe avoir- Free francais lesson-free
PPT
Les moyens de transport-2023.ppt french language teaching ppt
PDF
Bac 2026 - Livre - 🎤 Le Grand Oral - ✍️ Exemples rédigés - 🌿 SVT - Extrait
PPTX
Le rendez-vous de l'été.pptx Film français
PPTX
Bienvenido slides about the first steps in spanish.pptx
PPTX
Le rendez-vous de l'été.pptx Film français
PPTX
Présentation Personal Branding J2025.pptx_20250218_132749_0000.pptx_20250610_...
PPT
calcul---électrique--et--chutes de tension.ppt
PDF
Bac 2026 - Livre - 🎤 Le Grand Oral - ✍️ Exemples rédigés - Physique ⚡ - Chimi...
PPTX
Devenir Inspecteur HSE _ Chp1_ L1....pptx
PDF
Consignes générales sécurité et environnement.pdf
PPT
le-subjonctif-présent, Grammaire, français
PPTX
SESSION4-SUPPORT-DE-COURS-FLEC-(Future leader en énergie au Cameroun)-CECOSDA...
PPTX
SESSION5-SUPPORT-DE-COURS-FLEC-(Future leader en énergie au Cameroun)-CECOSDA...
PPTX
le subjonctif présent, Conjugaison français
ENDODONTIE CONSERVATRICE.docx faculté de médecine dentaire
Présentation Projet Entreprise Minimaliste Moderne Sobre Blanc Noir.pptx
Copie de Présentation Personal Branding J2025.pptx_20250610_120558_0000.pptx
SESSION1-SUPPORT-DE-COURS-FLEC-(Future leader en énergie au Cameroun)-CECOSDA...
plus que ´parfait--.-.-.-..-.--.-..-.-.-.-.
Le verbe avoir- Free francais lesson-free
Les moyens de transport-2023.ppt french language teaching ppt
Bac 2026 - Livre - 🎤 Le Grand Oral - ✍️ Exemples rédigés - 🌿 SVT - Extrait
Le rendez-vous de l'été.pptx Film français
Bienvenido slides about the first steps in spanish.pptx
Le rendez-vous de l'été.pptx Film français
Présentation Personal Branding J2025.pptx_20250218_132749_0000.pptx_20250610_...
calcul---électrique--et--chutes de tension.ppt
Bac 2026 - Livre - 🎤 Le Grand Oral - ✍️ Exemples rédigés - Physique ⚡ - Chimi...
Devenir Inspecteur HSE _ Chp1_ L1....pptx
Consignes générales sécurité et environnement.pdf
le-subjonctif-présent, Grammaire, français
SESSION4-SUPPORT-DE-COURS-FLEC-(Future leader en énergie au Cameroun)-CECOSDA...
SESSION5-SUPPORT-DE-COURS-FLEC-(Future leader en énergie au Cameroun)-CECOSDA...
le subjonctif présent, Conjugaison français

Cours langage-c

  • 1. INTRODUCTION q 1 SYMBOLES q 2 C, la naissance d'un langage de programmation portable r 2.1 Historique r 2.2 Avantages r 2.3 Désavantages s 2.3.1 efficience et compréhensibilité : s 2.3.2 portabilité et bibliothèques de fonctions s 2.3.3 discipline de programmation 1. SYMBOLES Explication des symboles utilisés dans le texte: Conseil ! Mauvaise solution !
  • 2. Bonne solution ! Attention ! Piège dangereux ! Attention ! Piège fréquent ! ou Remarque importante ! Solution incorrecte ! Peut mener à des erreurs. Solution correcte ! Fonction ou opération non portable selon le standard ANSI- C. 2. C, la naissance d'un langage de programmation portable
  • 3. 2.1 Historique Dans les dernières années, aucun langage de programmation n'a pu se vanter d'une croissance en popularité comparable à celle de C et de son jeune frère C++. L'étonnant dans ce fait est que le langage C n'est pas un nouveau-né dans le monde informatique, mais qu'il trouve ses sources en 1972 dans les 'Bell Laboratories': Pour développer une version portable du système d'exploitation UNIX, Dennis M. Ritchie a conçu ce langage de programmation structuré, mais très 'près' de la machine. K&R-C En 1978, le duo Brian W. Kernighan / Dennis M. Ritchie a publié la définition classique du langage C (connue sous le nom de standard K&R-C ) dans un livre intitulé 'The C Programming Language'. ANSI-C Le succès des années qui suivaient et le développement de compilateurs C par d'autres maisons ont rendu nécessaire la définition d'un standard actualisé et plus précis. En 1983, le 'American National Standards Institute' (ANSI) chargeait une commission de mettre au point 'une définition explicite et indépendante de la machine pour le langage C', qui devrait quand même conserver l'esprit du langage. Le résultat était le standard ANSI-C. La seconde édition du livre 'The C Programming Language', parue en 1988, respecte tout à fait le standard ANSI-C et elle est devenue par la suite, la 'bible' des programmeurs en C. 2.2 Avantages
  • 4. Le grand succès du langage C s'explique par les avantages suivants; C est un langage: (1) universel : C n'est pas orienté vers un domaine d'applications spéciales, comme par exemple FORTRAN (applications scientifiques et techniques) ou COBOL (applications commerciales ou traitant de grandes quantités de données). (2) compact : C est basé sur un noyau de fonctions et d'opérateurs limité, qui permet la formulation d'expressions simples, mais efficaces. (3) moderne : C est un langage structuré, déclaratif et récursif; il offre des structures de contrôle et de déclaration comparables à celles des autres grands langages de ce temps (FORTRAN, ALGOL68, PASCAL). (4) près de la machine : comme C a été développé en premier lieu pour programmer le système d'exploitation UNIX, il offre des opérateurs qui sont très proches de ceux du langage machine et des fonctions qui permettent un accès simple et direct aux fonctions internes de l'ordinateur (p.ex: la gestion de la mémoire). (5) rapide :
  • 5. comme C permet d'utiliser des expressions et des opérateurs qui sont très proches du langage machine, il est possible de développer des programmes efficients et rapides. (6) indépendant de la machine : bien que C soit un langage près de la machine, il peut être utilisé sur n'importe quel système en possession d'un compilateur C. Au début C était surtout le langage des systèmes travaillant sous UNIX, aujourd'hui C est devenu le langage de programmation standard dans le domaine des micro-ordinateurs. (7) portable : en respectant le standard ANSI-C, il est possible d'utiliser le même programme sur tout autre système (autre hardware, autre système d'exploitation), simplement en le recompilant. (8) extensible : C ne se compose pas seulement des fonctions standard; le langage est animé par des bibliothèques de fonctions privées ou livrées par de nombreuses maisons de développement. 2.3 Désavantages Évidemment, rien n'est parfait. Jetons un petit coup d'oeil sur le revers de la médaille: 2.3.1 efficience et compréhensibilité : En C, nous avons la possibilité d'utiliser des expressions compactes et efficientes. D'autre part, nos programmes doivent rester compréhensibles pour nous-mêmes et pour
  • 6. d'autres. Comme nous allons le constater sur les exemples suivants, ces deux exigences peuvent se contredire réciproquement. Exemple 1 Les deux lignes suivantes impriment les N premiers éléments d'un tableau A[ ], en insérant un espace entre les éléments et en commençant une nouvelle ligne après chaque dixième élément: for (i=0; i<n; i++) printf("%6d%c", A[i], (i%10==9)?'n':' '); Cette notation est très pratique, mais plutôt intimidante pour un débutant. L'autre variante est plus lisible, mais elle ne profite pas des avantages du langage C: for(I=0;I<N;I=I+1) { printf("%6d", A[I]); if ((I%10)==9) printf("n"); else printf(" "); } Exemple 2 La fonction copietab() copie les éléments d'une chaîne de caractères T[ ] dans une autre chaîne de caractères S[ ]. Voici
  • 7. d'abord la version 'simili-Pascal' : void copietab(char S[], char T[]) { int I; I=0; while (T[I] != '0') { S[I] = T[I]; I = I+1; } S[I] = '0'; } Cette définition de la fonction est valable en C, mais en pratique elle ne serait jamais programmée ainsi. En utilisant les possibilités de C, un programmeur expérimenté préfère la solution suivante: void copietab(char *S, char *T) { while (*S++ = *T++); } La deuxième formulation de cette fonction est élégante, compacte, efficace et la traduction en langage machine fournit un code très rapide...; mais bien que cette manière de résoudre les problèmes soit le cas normal en C, il n'est pas si évident de suivre le raisonnement.
  • 8. Conclusions Bien entendu, dans les deux exemples ci-dessus, les formulations 'courtes' représentent le bon style dans C et sont de loin préférables aux deux autres. Nous constatons donc que: la programmation efficiente en C nécessite beaucoup d'expérience et n'est pas facilement accessible à des débutants. sans commentaires ou explications, les programmes peuvent devenir incompréhensibles, donc inutilisables. 2.3.2 portabilité et bibliothèques de fonctions Les limites de la portabilité La portabilité est l'un des avantages les plus importants de C: en écrivant des programmes qui respectent le standard ANSI-C, nous pouvons les utiliser sur n'importe quelle machine possédant un compilateur ANSI-C. D'autre part, le répertoire des fonctions ANSI-C est assez limité. Si un programmeur désire faire appel à une fonction spécifique de la machine (p.ex: utiliser une carte graphique spéciale), il est assisté par une foule de fonctions 'préfabriquées', mais il doit être conscient qu'il risque de perdre la portabilité. Ainsi, il devient évident que les avantages d'un programme portable doivent être payés par la restriction des moyens de programmation. 2.3.3 discipline de programmation Les dangers de C
  • 9. Nous voici arrivés à un point crucial: C est un langage près de la machine, donc dangereux et bien que C soit un langage de programmation structuré, il ne nous force pas à adopter un certain style de programmation (comme p.ex. Pascal). Dans un certain sens, tout est permis et la tentation de programmer du 'code spaghetti' est grande. (Même la commande 'goto', si redoutée par les puristes ne manque pas en C). Le programmeur a donc beaucoup de libertés, mais aussi des responsabilités: il doit veiller lui-même à adopter un style de programmation propre, solide et compréhensible. Remarque Au fil de l'introduction du langage C, ce manuel contiendra quelques recommandations au sujet de l'utilisation des différents moyens de programmation. Il est impossible de donner des règles universelles à ce sujet, mais le conseil suivant est valable pour tous les langages de programmation: Si, après avoir lu uniquement les commentaires d'un programme, vous n'en comprenez pas le fonctionnement, alors jetez le tout de suite !
  • 10. Chapitre 1: L'ENVIRONNEMENT ET LES BIBLIOTHEQUES Pour le travail pratique en C, il faut utiliser un compilateur et un éditeur facile à utiliser. A titre d'exemple je décris ici l'utilisation de l'environnement Borland C++ (Version 3.1). Ce programme nous offre une surface de programmation confortable et rapide. Tout autre compilateur qui permet la programmation selon le standard ANSI-C fait aussi bien l'affaire. q 1.1. Description de l'environnement Borland C++ r 1.1.1. Les menus r 1.1.2. Les nouveautés q 1.2. Sélectionner le compilateur ANSI-C q 1.3. Les bibliothèques de fonctions prédéfinies r Schéma: Bibliothèques de fonctions et compilation 1.1. Description de l'environnement Borland C++ Borland C++ est une implémentation complète du standard C++ AT&T (Version 2.1). Le compilateur Borland C++ est capable de produire du code C 'pur' selon la définition Kernighan & Ritchie ou selon le standard ANSI-C. D'autre part, Borland C++ nous offre toute une série de bibliothèques, qui (parfois aux dépens de la portabilité) nous permettent d'exploiter les capacités du PC. Ce chapitre se limite à une brève description des menus de Borland C++.
  • 11. 1.1.1. Les menus FILE gestion des fichiers, retour au DOS EDIT commandes d'édition du texte SEARCH recherche d'un texte, d'une déclaration de fonction, de la position d'une erreur dans le programme RUN exécution d'un programme en entier ou par parties COMPILE traduction et/ou enchaînement (link) des programmes DEBUG détection d'erreurs en inspectant les données, en insérant des points d'observation (watch) ou en insérant des points d'arrêt (breakpoints) PROJECT gestion de projets OPTIONS changement et sauvetage des réglages par défaut : * des menus * des options du compilateur * de l'environnement WINDOW visualisation et disposition des différentes fenêtres (Message, Output, Watch, User, Project) sur l'écran HELP système d'aide 1.1.2. Les nouveautés a) L'interface utilisateur :
  • 12. L'environnement Borland C++ nous permet l'utilisation confortable de plusieurs fenêtres sur l'écran. L'interaction des données des différentes fenêtres (Message, Output, Watch, User Screen) a rendu la recherche d'erreurs très efficace. b) La gestion de projets multi-fichiers : En pratique, les programmes sont souvent subdivisés en plusieurs sous-programmes ou modules, qui peuvent être répartis dans plusieurs fichiers. L'environnement Borland C++ nous offre un gestionnaire 'Project Manager' qui détecte automatiquement les fichiers qui doivent être recompilés et enchaînés après une modification. Dans nos applications, nous allons uniquement utiliser des fonctions définies dans le même fichier que notre programme principal ou prédéfinies dans une bibliothèque standard; ainsi, nous n'aurons pas besoin de l'option 'Project'. c) Le système d'aide : Borland C++ est accompagné d'un programme d'aide puissant qui est appelé directement à partir de l'éditeur. Le système d'aide peut donner des informations ponctuelles sur un sujet sélectionné ou nous laisser choisir un sujet à partir d'une liste alphabétique : F1 Aide sur l'environnement Borland C++ Shift-F1 Index alphabétique des mots-clefs Ctrl-F1 Aide sur le mot actif (à la position du curseur) 1.2. Sélectionner le compilateur ANSI-C Comme nous utilisons l'environnement Borland C++ pour compiler des programmes correspondant au standard ANSI-C, nous devons d'abord changer le mode de compilation par défaut : - Choisir le menu 'compilateur' par : Alt-Options|Compiler
  • 13. - Choisir le modèle ANSI-C par : Source|ANSI|OK 1.3. Les bibliothèques de fonctions prédéfinies Utilisation des bibliothèques de fonctions La pratique en C exige l'utilisation de bibliothèques de fonctions. Ces bibliothèques sont disponibles dans leur forme précompilée (extension: .LIB). Pour pouvoir les utiliser, il faut inclure des fichiers en-tête (header files - extension .H) dans nos programmes. Ces fichiers contiennent des 'prototypes' des fonctions définies dans les bibliothèques et créent un lien entre les fonctions précompilées et nos programmes. #include L'instruction #include insère les fichiers en-tête indiqués comme arguments dans le texte du programme au moment de la compilation. Identification des fichiers Lors de la programmation en Borland C, nous travaillons donc avec différents types de fichiers qui sont identifiés par leurs extensions: *.C fichiers source *.OBJ fichiers compilés (versions objet) *.EXE fichiers compilés et liés (versions exécutables) *.LIB bibliothèques de fonctions précompilées *.H fichiers en-tête (header files) Exemple
  • 14. Nous avons écrit un programme qui fait appel à des fonctions mathématiques et des fonctions graphiques prédéfinies. Pour pouvoir utiliser ces fonctions, le programme a besoin des bibliothèques: MATHS.LIB GRAPHICS.LIB Nous devons donc inclure les fichiers en-tête correspondants dans le code source de notre programme à l'aide des instructions: #include <math.h> #include <graphics.h> Après la compilation, les fonctions precompilées des bibliothèques seront ajoutées à notre programme pour former une version exécutable du programme (voir schéma). Remarque: La bibliothèque de fonctions graphics.h est spécifique aux fonctionnalités du PC et n'est pas incluse dans le standard ANSI-C. Schéma: Bibliothèques de fonctions et compilation
  • 17. Chapitre 2: NOTIONS DE BASE avant de pouvoir comprendre ou même écrire des programmes, il faut connaître la composition des programmes dans le langage de programmation. Dans ce chapitre, nous allons discuter un petit programme en mettant en évidence les structures fondamentales d'un programme en C. q 2.1. Hello C ! q 2.2. Les composantes d'un programme en C r 2.2.1. Les fonctions r 2.2.2. La fonction main r 2.2.3. Les variables r 2.2.4. Les identificateurs r 2.2.5. Les commentaires q 2.3. Discussion de l'exemple 'Hello_World' 2.1. Hello C ! Suivons la tradition et commençons la découverte de C par l'inévitable programme 'hello world'. Ce programme ne fait rien d'autre qu'imprimer les mots suivants sur l'écran: Comparons d'abord la définition du programme en C avec celle en langage algorithmique. HELLO_WORLD en langage algorithmique programme HELLO_WORLD (* Notre premier programme en C *) écrire "hello, world" fprogramme HELLO_WORLD en C
  • 18. #include <stdio.h> main() { /* Notre premier programme en C */ printf("hello, worldn"); return 0; } Dans la suite du chapitre, nous allons discuter les détails de cette implémentation. 2.2. Les composantes d'un programme en C Les programmes en C sont composés essentiellement de fonctions et de variables. Pour la pratique, il est donc indispensable de se familiariser avec les caractéristiques fondamentales de ces éléments. 2.2.1. Les fonctions En C, le programme principal et les sous-programmes sont définis comme fonctions. Il n'existe pas de structures spéciales pour le programme principal ni les procédures (comme en langage algorithmique). Le programme principal étant aussi une 'fonction', nous devons nous intéresser dès le début à la définition et aux caractéristiques des fonctions en C. Commençons par comparer la syntaxe de la définition d'une fonction en C avec celle d'une fonction en langage algorithmique: Définition d'une fonction en langage algorithmique fonction <NomFonct> (<NomPar1>, <NomPar2>, ..........) :<TypeRés> | <déclarations des paramètres> | <déclarations locales> | <instructions> ffonction Définition d'une fonction en C
  • 19. <TypeRés><NomFonct> (<TypePar1><NomPar1>, <TypePar2> <NomPar2>, ... ) { <déclarations locales> <instructions> } En C, une fonction est définie par: une ligne déclarative qui contient : <TypeRés> le type du résultat de la fonction <NomFonct> le nom de la fonction <TypePar1> <NomPar1>, <TypePar2> <NomPar2>, ... les types et les noms des paramètres de la fonction un bloc d'instructions délimité par des accolades { }, contenant: <déclarations locales> - les déclarations des données locales (c.-à-d.: des données qui sont uniquement connues à l'intérieur de la fonction) <instructions> - la liste des instructions qui définit l'action qui doit être exécutée Résultat d'une fonction Par définition, toute fonction en C fournit un résultat dont le type doit être défini. Si aucun type n'est défini explicitement, C suppose par défaut que le type du résultat est int (integer). Le retour du résultat se fait en général à la fin de la fonction par l'instruction return. Le type d'une fonction qui ne fournit pas de résultat (comme les procédures en langage algorithmique ), est déclaré comme void (vide). Paramètres d'une fonction La définition des paramètres (arguments) d'une fonction est placée entre parenthèses ( ) derrière le nom de la fonction. Si une fonction n'a pas besoin de paramètres, les parenthèses restent vides ou contiennent le mot void. La fonction minimale qui ne fait rien et qui ne fournit aucun résultat est alors: void dummy() {}
  • 20. Instructions En C, toute instruction simple est terminée par un point-virgule ; (même si elle se trouve en dernière position dans un bloc d'instructions). Par exemple: printf("hello, world n"); 2.2.2. La fonction main La fonction main est la fonction principale des programmes en C. Elle se trouve obligatoirement dans tous les programmes. L'exécution d'un programme entraîne automatiquement l'appel de la fonction main. Dans les premiers chapitres, nous allons simplement 'traduire' la structure programme du langage algorithmique par une définition équivalente de la fonction main : Définition du programme principal en langage algorithmique programme <NomProgramme> <déclarations> <instructions> fprogramme Définition de la fonction main enC main() { <déclarations> <instructions> return 0; } Résultat de main En principe tout programme devrait retourner une valeur comme code d'erreur à son environnement. Par conséquent, le type résultat demain est toujours int. En général, le type de main n'est pas déclaré explicitement, puisque c'est le type par défaut. Nous allons terminer nos programmes par l'instruction:
  • 21. return 0; qui indique à l'environnement que le programme s'est terminé avec succès, sans anomalies ou erreurs fatales. Paramètres de main - Si la liste des paramètres de la fonction main est vide, il est d'usage de la déclarer par (). - Si nous utilisons des fonctions prédéfinies (par exemple: printf), il faut faire précéder la définition de main par les instructions #include correspondantes. Remarque avancée: Il est possible de faire passer des arguments de la ligne de commande à un programme. Dans ce cas, la liste des paramètres doit contenir les déclarations correspondantes. Dans notre cours, nous n'allons pas utiliser des arguments de la ligne de commande. Ainsi la liste des paramètres de la fonction main sera vide (void) dans tous nos exemples et nous pourrons employer la déclaration suivante qui fait usage des valeurs par défaut: main() { ... } 2.2.3. Les variables Les variables contiennent les valeurs qui sont utilisées pendant l'exécution du programme. Les noms des variables sont des identificateurs quelconques (voir 2.2.4 ). Les différents types de variables simples et les opérateurs admissibles sont discutés au chapitre 3. 2.2.4. Les identificateurs Les noms des fonctions et des variables en C sont composés d'une suite de lettres et de chiffres. Le premier caractère doit être une lettre. Le symbole '_' est aussi considéré comme une lettre. * L'ensemble des symboles utilisables est donc:
  • 22. {0,1,2,...,9,A,B,...,Z,_,a,b,...,z} * Le premier caractère doit être une lettre (ou le symbole '_') - voir aussi Remarque -. * C distingue les majuscules et les minuscules, ainsi: ' Nom_de_variable' est différent de ' nom_de_variable' * La longueur des identificateurs n'est pas limitée, mais C distingue 'seulement' les 31 premiers caractères. - voir aussi Remarque - Remarque: Il est déconseillé d'utiliser le symbole '_' comme premier caractere pour un identificateur, car il est souvent employé pour définir les variables globales de l'environment C. Exemples Identificateurs corrects: Identificateurs incorrects: nom1 1nom nom_2 nom.2 _nom_3 -nom-3 Nom_de_variable Nom de variable deuxieme_choix deuxième_choix mot_francais mot_français 2.2.5. Les commentaires Un commentaire commence toujours par les deux symboles '/*' et se termine par les symboles '*/'. Il est interdit d'utiliser des commentaires imbriqués.
  • 23. Exemples /* Ceci est un commentaire correct */ /* Ceci est /* évidemment */ défendu */ 2.3. Discussion de l'exemple 'Hello_World' Reprenons le programme 'Hello_World' et retrouvons les particularités d'un programme en C. HELLO_WORLD en C #include <stdio.h> main() /* Notre premier programme en C */ { printf("hello, worldn"); return 0; } Discussion - La fonction main ne reçoit pas de données, donc la liste des paramètres est vide. - La fonction main fournit un code d'erreur numérique à l'environnement, donc le type du résultat est int et n'a pas besoin d'être déclaré explicitement. - Le programme ne contient pas de variables, donc le bloc de déclarations est vide. - La fonction main contient deux instructions: * l'appel de la fonction printf avec l'argument "hello, worldn"; Effet: Afficher la chaîne de caractères "hello worldn". * la commande return avec l'argument 0; Effet: Retourner la valeur 0 comme code d'erreur à l'environnement. - L'argument de la fonction printf est une chaîne de caractères indiquée entre les guillements. Une telle suite de caractères est appelée chaîne de caractères constante (string constant).
  • 24. - La suite de symboles 'n' à la fin de la chaîne de caractères "hello, worldn" est la notation C pour 'passage à la ligne' (angl: new line). En C, il existe plusieurs couples de symboles qui contrôlent l'affichage ou l'impression de texte. Ces séquences d'échappement sont toujours précédées par le caractère d'échappement ''. printf et la bibliothèque <stdio> La fonction printf fait partie de la bibliothèque de fonctions standard <stdio> qui gère les entrées et les sorties de données. La première ligne du programme: #include <stdio.h> instruit le compilateur d'inclure le fichier en-tête 'STDIO.H' dans le texte du programme. Le fichier 'STDIO.H' contient les informations nécessaires pour pouvoir utiliser les fonctions de la bibliothèque standard <stdio>(voir chapitre 1.3).
  • 25. Chapitre 3: TYPES DE BASE, OPÉRATEURS ET EXPRESSIONS Récapitulation du vocabulaire Les variables et les constantes sont les données principales qui peuvent être manipulées par un programme. Les déclarations introduisent les variables qui sont utilisées, fixent leur type et parfois aussi leur valeur de départ. Les opérateurs contrôlent les actions que subissent les valeurs des données. Pour produire de nouvelles valeurs, les variables et les constantes peuvent être combinées à l'aide des opérateurs dans des expressions. Le type d'une donnée détermine l'ensemble des valeurs admissibles, le nombre d'octets à réserver en mémoire et l'ensemble des opérateurs qui peuvent y être appliqués. Motivation La grande flexibilité de C nous permet d'utiliser des opérandes de différents types dans un même calcul. Cet avantage peut se transformer dans un terrible piège si nous ne prévoyons pas correctement les effets secondaires d'une telle opération (conversions de type automatiques, arrondissements, etc.). Une étude minutieuse de ce chapitre peut donc aider à éviter des phénomènes parfois 'inexplicables' ... q 3.1. Les types simples r 3.1.1. Les types entiers r 3.1.2. Les types rationnels q 3.2. La déclaration des variables simples r 3.2.1. Les constantes numériques s - Les constantes entières s - Les constantes rationnelles s - Les caractères constants r 3.2.2. Initialisation des variables q 3.3. Les opérateurs standard r 3.3.1. Les opérateurs connus r 3.3.2. Les opérateurs particuliers de C s - Les opérateurs d'affectation s - Opérateurs d'incrémentation et de décrémentation q 3.4. Les expressions et les instructions q 3.5. Les priorités des opérateurs q 3.6. Les fonctions arithmétiques standard q 3.7. Les conversions de type r 3.7.1. Les conversions de type automatiques
  • 26. r 3.7.2. Les conversions de type forcées (casting) 3.1. Les types simples Ensembles de nombres et leur représentation En mathématiques, nous distinguons divers ensembles de nombres: * l'ensemble des entiers naturels IN, * l'ensemble des entiers relatifs ZZ, * l'ensemble des rationnels Q, * l'ensemble des réels IR. En mathématiques l'ordre de grandeur des nombres est illimité et les rationnels peuvent être exprimés sans perte de précision. Un ordinateur ne peut traiter aisément que des nombres entiers d'une taille limitée. Il utilise le système binaire pour calculer et sauvegarder ces nombres. Ce n'est que par des astuces de calcul et de représentation que l'ordinateur obtient des valeurs correctement approchées des entiers très grands, des réels ou des rationnels à partie décimale infinie. Les charges du programmeur Même un programmeur utilisant C ne doit pas connaître tous les détails des méthodes de codage et de calcul, il doit quand même être capable de: - choisir un type numérique approprié à un problème donné; c.-à-d.: trouver un optimum de précision, de temps de calcul et d'espace à réserver en mémoire - choisir un type approprié pour la représentation sur l'écran - prévoir le type résultant d'une opération entre différents types numériques; c.-à-d.: connaître les transformations automatiques de type que C accomplit lors des calculs (voir 3.7.1). - prévoir et optimiser la précision des résultats intermédiaires au cours d'un calcul complexe; c.-à-d.: changer si nécessaire l'ordre des opérations ou forcer l'ordinateur à utiliser un type numérique mieux adapté (casting: voir 3.7.2). 3.1.1. Les types entiers
  • 27. Avant de pouvoir utiliser une variable, nous devons nous intéresser à deux caractéristiques de son type numérique: (1) le domaine des valeurs admissibles (2) le nombre d'octets qui est réservé pour une variable Le tableau suivant résume les caractéristiques des types numériques entiers de C : définition description min max nombre d'octets char caractère -128 127 1 short entier court -32768 32767 2 int entier standard -32768 32767 2 long entier long -2147483648 2147483647 4 - char : caractère Une variable du type char peut contenir une valeur entre -128 et 127 et elle peut subir les mêmes opérations que les variables du type short, int ou long. - int : entier standard Sur chaque machine, le type int est le type de base pour les calculs avec les entiers. Le codage des variables du type int est donc dépendant de la machine. Sur les IBM-PC sous MS-DOS, une variable du type int est codée dans deux octets. - short : entier court Le type short est en général codé dans 2 octets. Comme une variable int occupe aussi 2 octets sur notre système, le type short devient seulement nécessaire, si on veut utiliser le même programme sur d'autres machines, sur lesquelles le type standard des entiers n'est pas forcément 2 octets. - Les modificateurs signed/unsigned: Si on ajoute le préfixe unsigned à la définition d'un type de variables entières, les domaines des variables sont déplacés comme suit: nombre définition description min max d'octets
  • 28. unsigned char caractère 0 255 1 unsigned short entier court 0 65535 2 unsigned int entier standard 0 65535 2 unsigned long entier long 0 4294967295 4 Remarques 1. Le calcul avec des entiers définis comme unsigned correspond à l'arithmétique modulo 2n. Ainsi, en utilisant une variable X du type unsigned short, nous obtenons le résultat suivant: Affectation : X = 65500 + 100 Résultat : X = 64 /* [+216] */ 2. Par défaut, les types entiers short, int, long sont munis d'un signe. Le type par défaut de char est dépendant du compilateur et peut être signed ou unsigned. Ainsi, l'attribut signed a seulement un sens en liaison avec char et peut forcer la machine à utiliser la représentation des caractères avec signe (qui n'est cependant pas très usuelle). 3. Les valeurs limites des differents types sont indiquées dans le fichier header <limits.h>. 4. En principe, on peut dire que sizeof(short) <= sizeof(int) <= sizeof(long).Ainsi sur certaine architecture on peut avoir short = 2 octets, int = 2 octets, long = 4 octets et sur d'autre short = 2 octets, int = 4 octets, long = 4 octets ( Il sera intéressant de voir l'implementation dans un environnement 64 bits! )
  • 29. 3.1.2. Les types rationnels En informatique, les rationnels sont souvent appelés des 'flottants'. Ce terme vient de 'en virgule flottante' et trouve sa racine dans la notation traditionnelle des rationnels: <+|-> <mantisse> * 10<exposant> <+|-> est le signe positif ou négatif du nombre est un décimal positif avec un seul chiffre devant la <mantisse> virgule. <exposant> est un entier relatif En C, nous avons le choix entre trois types de rationnels: float, double et long double. Dans le tableau ci-dessous, vous trouverez leurs caractéristiques: représentent les valeurs minimales et min et max maximalespositives. Les valeurs négatives peuvent varier dans les mêmes domaines. mantisse indique le nombre de chiffres significatifs de la mantisse. nombre précision mantisse min max définition d'octets 6 4 float simple 3.4 *10-38 3.4 * 1038 15 8 double double 1.7 *10-308 1.7 *10308 19 10 long double suppl. 3.4 *10-4932 1.1 *104932 Remarque avancée Les détails de l'implémentation sont indiqués dans le fichier header <float.h>. 3.2. La déclaration des variables simples Maintenant que nous connaissons les principaux types de variables, il nous faut
  • 30. encore la syntaxe pour leur déclaration: Déclaration de variables en langage algorithmique <Type> <NomVar1>,<NomVar2>,...,<NomVarN> Déclaration de variables en C <Type> <NomVar1>,<NomVar2>,...,<NomVarN>; Prenons quelques déclarations du langage descriptif, entier COMPTEUR,X,Y réel HAUTEUR,LARGEUR,MASSE_ATOMIQUE caractère TOUCHE booléen T_PRESSEE et traduisons-les en des déclarations du langage C : int compteur, X, Y ; float hauteur, largeur ; double masse_atomique ; char touche; int t_pressee; Langage algorithmique --> C En général, nous avons le choix entre plusieurs types et nous devons trouver celui qui correspond le mieux au domaine et aux valeurs à traiter. Voici quelques règles générales qui concernent la traduction des déclarations de variables numériques du langage algorithmique en C : - La syntaxe des déclarations en C ressemble à celle du langage algorithmique. Remarquez quand même les points-virgules à la fin des déclarations en C. entier : Nous avons le choix entre tous les types entiers (inclusivement char) dans leurs formes signed ou unsigned. Si les nombres deviennent trop grands pour unsigned long, il faut utiliser un type rationnel (p.ex: double) réel : Nous pouvons choisir entre les trois types rationnels en observant non seulement la grandeur maximale de l'exposant, mais plus encore le nombre de chiffres significatifs de la mantisse.
  • 31. caractère : Toute variable du type char peut contenir un (seul) caractère. En C, il faut toujours être conscient que ce 'caractère' n'est autre chose qu'un nombre correspondant à un code (ici: code ASCII). Ce nombre peut être intégré dans toute sorte d'opérations algébriques ou logiques ... chaîne : En C il n'existe pas de type spécial pour chaînes de caractères. Les moyens de traiter les chaînes de caractères seront décrits au chapitre 8. booléen : En C il n'existe pas de type spécial pour variables booléennes. Tous les types de variables numériques peuvent être utilisés pour exprimer des opérations logiques: valeur logique faux <=> valeur numérique zéro valeur logique vrai <=> toute valeur différente de zéro Si l'utilisation d'une variable booléenne est indispensable, le plus naturel sera d'utiliser une variable du type int. Les opérations logiques en C retournent toujours des résultats du type int: 0 pour faux 1 pour vrai 3.2.1. Les constantes numériques En pratique, nous utilisons souvent des valeurs constantes pour calculer, pour initialiser des variables, pour les comparer aux variables, etc. Dans ces cas, l'ordinateur doit attribuer un type numérique aux valeurs constantes. Pour pouvoir prévoir le résultat et le type exact des calculs, il est important pour le programmeur de connaître les règles selon lesquelles l'ordinateur choisit les types pour les constantes. - Les constantes entières Type automatique Lors de l'attribution d'un type à une constante entière, C choisit en général la solution la plus économique: Le type des constantes entières * Si possible, les constantes entières obtiennent le type int. * Si le nombre est trop grand pour int (p.ex: -40000 ou +40000) il aura automatiquement le type long.
  • 32. * Si le nombre est trop grand pour long, il aura le type unsigned long. * Si le nombre est trop grand pour unsigned long, la réaction du programme est imprévisible. Type forcé Si nous voulons forcer l'ordinateur à utiliser un type de notre choix, nous pouvons employer les suffixes suivants: suffixe type Exemple u ou U unsigned (int ou long) 550u l ou L long 123456789L ul ou UL unsigned long 12092UL Exemples 12345 type int 52000 type long -2 type int 0 type int 1u type unsigned int 52000u type unsigned long 22lu Erreur ! Base octale et hexadécimale Il est possible de déclarer des constantes entières en utilisant la base octale ou hexadécimale: * Si une constante entière commence par 0 (zéro), alors elle est interprétée en base octale. * Si une constante entière commence par 0x ou 0X , alors elle est interprétée en base hexadécimale. Exemples base base base octale représ. binaire décimale hexadécimale
  • 33. 100 0144 0X64 1100100 255 0377 0xff 11111111 65536 0200000 0X10000 10000000000000000 12 014 0XC 1100 4040 07710 0xFC8 111111001000 - Les constantes rationnelles Les constantes rationnelles peuvent être indiquées: * en notation décimale, c.-à-d. à l'aide d'un point décimal: Exemples 123.4 -0.001 1.0 * en notation exponentielle, c.-à-d. à l'aide d'un exposant séparé du nombre décimal par les caractères 'e' ou 'E': Exemples 1234e-1 -1E-3 0.01E2 L'ordinateur reconnaît les constantes rationnelles au point décimal ou au séparateur de l'exposant ('e' ou 'E'). Par défaut, les constantes rationnelles sont du type double. Le type des constantes rationnelles * Sans suffixe, les constantes rationnelles sont du type double. * Le suffixe f ou F force l'utilisation du type float. * Le suffixe l ou L force l'utilisation du type long double. - Les caractères constants Les constantes qui désignent un (seul) caractère sont toujours indiquées entre des apostrophes: par exemple 'x'. La valeur d'un caractère constant est le code interne de ce caractère. Ce code (ici: le code ASCII) est dépendant de la machine. Les caractères constants peuvent apparaître dans des opérations arithmétiques ou logiques, mais en général ils sont utilisés pour être comparés à des variables. Séquences d'échappement
  • 34. Comme nous l'avons vu au chapitre 2, l'impression et l'affichage de texte peut être contrôlé à l'aide de séquences d'échappement. Une séquence d'échappement est un couple de symboles dont le premier est le signe d'échappement ''. Au moment de la compilation, chaque séquence d'échappement est traduite en un caractère de contrôle dans le code de la machine. Comme les séquences d'échappement sont identiques sur toutes les machines, elles nous permettent d'écrire des programmes portables, c.-à-d.: des programmes qui ont le même effet sur toutes les machines, indépendemment du code de caractères utilisé. Séquences d'échappement a sonnerie trait oblique b curseur arrière ? point d'interrogation t tabulation ' apostrophe n nouvelle ligne " guillemets r retour au début de ligne f saut de page (imprimante) 0 NUL v tabulateur vertical Le caractère NUL La constante '0' qui a la valeur numérique zéro (ne pas à confondre avec le caractère '0' !!) a une signification spéciale dans le traitement et la mémorisation des chaînes de caractères: En C le caractère '0' définit la fin d'une chaîne de caractères. 3.2.2. Initialisation des variables Initialisation En C, il est possible d'initialiser les variables lors de leur déclaration: int MAX = 1023; char TAB = 't'; float X = 1.05e-4; En utilisant l'attribut const, nous pouvons indiquer que la valeur d'une variable ne change pas au cours d'un programme: const int MAX = 767; const double e = 2.71828182845905; const char NEWLINE = 'n';
  • 35. 3.3. Les opérateurs standard Affectation en langage algorithmique en <NomVariable> ranger <Expression> Affectation en C <NomVariable> = <Expression>; Exemples d'affectations - L'affectation avec des valeurs constantes Langage algorithmique C Type de la constante en LONG ranger 141 LONG = 141; entière en PI ranger 3.1415926 PI = 3.1415926; réelle en NATION ranger "L" NATION = 'L'; caractère - L'affectation avec des valeurs de variables Langage algorithmique C en VALEUR ranger X1A VALEUR = X1A; en LETTRE ranger COURRIER LETTRE = COURRIER; - L'affectation avec des valeurs d'expressions Langage algorithmique C en AIRE ranger PI*R2 AIRE = PI*pow(R,2); en MOYENNE ranger (A+B)/2 MOYENNE = (A+B)/2; en UN ranger sin2(X)+cos2(X) UN=pow(sin(X),2)+pow(cos(X),2); en RES ranger 45+5*X RES = 45+5*X; en PLUSGRAND ranger (X>Y) PLUSGRAND = (X>Y);
  • 36. en CORRECT ranger ('a'='a') CORRECT = ('a' == 'a'); Observations * il n'existe pas de fonction standard en C pour calculer le carré d'une valeur; on peut se référer à la fonction plus générale pow(x,y) qui calcule xy (voir 3.5.). * le test d'égalité en C se formule avec deux signes d'égalité== , l'affectation avec un seul = . 3.3.1. Les opérateurs connus Avant de nous lancer dans les 'spécialités' du langage C, retrouvons d'abord les opérateurs correspondant à ceux que nous connaissons déjà en langage descriptif . Opérateurs arithmétiques + addition - soustraction * multiplication / division (entière et rationnelle!) % modulo (reste d'une div. entière) Opérateurs logiques && et logique (and) || ou logique (or) ! négation logique (not) Opérateurs de comparaison == égal à != différent de <, <=, >, >= plus petit que, ... Opérations logiques Les résultats des opérations de comparaison et des opérateurs logiques sont du
  • 37. type int: - la valeur 1 correspond à la valeur booléenne vrai - la valeur 0 correspond à la valeur booléenne faux Les opérateurs logiques considèrent toute valeur différente de zéro comme vrai et zéro comme faux: 32 && 2.3 1 !65.34 0 0||!(32 > 12) 0 3.3.2. Les opérateurs particuliers de C - Les opérateurs d'affectation En pratique, nous retrouvons souvent des affectations comme: i=i+2 En C, nous utiliserons plutôt la formulation plus compacte: i += 2 L'opérateur += est un opérateur d'affectation. Pour la plupart des expressions de la forme: expr1 = (expr1) op (expr2) il existe une formulation équivalente qui utilise un opérateur d'affectation: expr1 op= expr2 Opérateurs d'affectation += ajouter à -= diminuer de *= multiplier par /= diviser par %= modulo Avantages
  • 38. La formulation avec un opérateur d'affectation est souvent plus près de la logique humaine: Un homme dirait <<Ajoute 2 à I>> plutôt que <<Ajoute 2 à I et écris le résultat dans I>> Les opérateurs d'affectation peuvent aider le compilateur à générer un code plus efficient parce que expr1 n'est évalué qu'une seule fois. Les opérateurs d'affectation deviennent le plus intéressant si expr1 est une expression complexe. Ceci peut être le cas si nous calculons avec des tableaux. L'expression: Element[n*i+j] = Element[n*i+j] * x[j] peut être formulée de manière plus efficiente et plus claire: Element[n*i+j] *= x[j] - Opérateurs d'incrémentation et de décrémentation Les affectations les plus fréquentes sont du type: I = I + 1 et I = I - 1 En C, nous disposons de deux opérateurs inhabituels pour ces affectations: I++ ou ++I pour l'incrémentation (augmentation d'une unité) I-- ou --I pour la décrémentation (diminution d'une unité) Les opérateurs ++ et -- sont employés dans les cas suivants: incrémenter/décrémenter une variable (p.ex: dans une boucle). Dans ce cas il n'y a pas de différence entre la notation préfixe (++I --I) et la notation postfixe (I++ I-- ). incrémenter/décrémenter une variable et en même temps affecter sa valeur à une autre variable. Dans ce cas, nous devons choisir entre la notation préfixe et postfixe: X = I++ passe d'abord la valeur de I à X et incrémente après X = I-- passe d'abord la valeur de I à X et décrémente après X = ++I incrémente d'abord et passe la valeur incrémentée à X
  • 39. X = --I décrémente d'abord et passe la valeur décrémentée à X Exemple Supposons que la valeur de N est égal à 5: Incrém. postfixe: X = N++; Résultat: N=6 et X=5 Incrém. préfixe: X = ++N; Résultat: N=6 et X=6 3.4. Les expressions et les instructions Expressions Les constantes et les variables sont des expressions. Les expressions peuvent être combinées entre elles par des opérateurs et former ainsi des expressions plus complexes. Les expressions peuvent contenir des appels de fonctions et elles peuvent apparaître comme paramètres dans des appels de fonctions. Exemples i=0 i++ X=pow(A,4) printf(" Bonjour !n") a=(5*x+10*y)*2 (a+b)>=100 position!=limite Instructions Une expression comme I=0 ou I++ ou printf(...) devient une instruction, si elle est suivie d'un point-virgule. Exemples
  • 40. i=0; i++; X=pow(A,4); printf(" Bonjour !n"); a=(5*x+10*y)*2; Évaluation et résultats En C toutes les expressions sont évaluées et retournent une valeur comme résultat: (3+2==5) retourne la valeur 1 (vrai) A=5+3 retourne la valeur 8 Comme les affectations sont aussi interprétées comme des expressions, il est possible de profiter de la valeur rendue par l'affectation: ((A=sin(X)) == 0.5) 3.5. Les priorités des opérateurs L'ordre de l'évaluation des différentes parties d'une expression correspond en principe à celle que nous connaissons des mathématiques. Exemple Supposons pour l'instruction suivante: A=5, B=10, C=1 X = 2*A+3*B+4*C; L'ordinateur évalue d'abord les multiplications: 2*A ==> 10 , 3*B ==> 30 , 4*C ==> 4 Ensuite, il fait l'addition des trois résultats obtenus: 10+30+4 ==> 44 A la fin, il affecte le résultat général à la variable: X = 44
  • 41. Priorité d'un opérateur On dit alors que la multiplication a la priorité sur l'addition et que la multiplication et l'addition ont la priorité sur l'affectation. Si nous voulons forcer l'ordinateur à commencer par un opérateur avec une priorité plus faible, nous devons (comme en mathématiques) entourer le terme en question par des parenthèses. Exemple Dans l'instruction: X = 2*(A+3)*B+4*C; l'ordinateur évalue d'abord l'expression entre parenthèses, ensuite les multiplications, ensuite l'addition et enfin l'affectation. (En reprenant les valeurs de l'exemple ci-dessus, le résultat sera 164) Entre les opérateurs que nous connaissons jusqu'ici, nous pouvons distinguer les classes de priorités suivantes: Classes de priorités Priorité 1 (la plus forte): () Priorité 2: ! ++ -- Priorité 3: */% Priorité 4: +- Priorité 5: < <= > >= Priorité 6: == != Priorité 7: && Priorité 8: || Priorité 9 (la plus faible): = += -= *= /= %=
  • 42. Evaluation d'opérateurs de la même classe --> Dans chaque classe de priorité, les opérateurs ont la même priorité. Si nous avons une suite d'opérateurs binaires de la même classe, l'évaluation se fait en passant de la gauche vers la droite dans l'expression. --> Pour les opérateurs unaires ( ! , ++ , -- ) et pour les opérateurs d'affectation (=, += , -= , *= , /= , %= ), l'évaluation se fait de droite à gauche dans l'expression. Exemples L'expression 10+20+30-40+50-60 sera évaluée comme suit: 10+20 ==> 30 30+30 ==> 60 60-40 ==> 20 20+50 ==> 70 70-60 ==> 10 Pour A=3 et B=4, l'expression A *= B += 5 sera évaluée comme suit: Pour A=1 et B=4, l'expression !--A==++!B sera évaluée comme suit:
  • 43. Les parenthèses Les parenthèses sont seulement nécessaires si nous devons forcer la priorité, mais elles sont aussi permises si elles ne changent rien à la priorité. En cas de parenthèses imbriquées, l'évaluation se fait de l'intérieur vers l'extérieur. Exemple En supposant à nouveau que A=5, B=10, C=1 l'expression suivante s'évaluera à 134: X = ((2*A+3)*B+4)*C Observez la priorité des opérateurs d'affectation : X *= Y + 1 <=> X = X * (Y + 1) X *= Y + 1 n'équivaut PAS à X = X * Y + 1 3.6. Les fonctions arithmétiques standard Les fonctions suivantes sont prédéfinies dans la bibliothèque standard <maths. lib>. Pour pouvoir les utiliser, le programme doit contenir la ligne: #include <math.h> Type des données Les arguments et les résultats des fonctions arithmétiques sont du type double. Fonctions arithmétiques COMMANDE C EXPLICATION LANG. ALGORITHMIQUE exp(X) fonction exponentielle eX log(X) logarithme naturel ln(X), X>0 log10(X) logarithme à base 10 log10(X), X>0 pow(X,Y) X exposant Y XY sqrt(X) racine carrée de X pour X>0 fabs(X) valeur absolue de X |X| floor(X) arrondir en moins int(X) ceil(X) arrondir en plus reste rationnel de X/Y fmod(X,Y) pour X différent de 0 (même signe que X)
  • 44. sin(X) cos(X) tan(X) sinus, cosinus, tangente de X asin(X) acos(X) atan(X) arcsin(X), arccos(X), arctan(X) sinus, cosinus, tangente hyperboliques de sinh(X) cosh(X) tanh(X) X 3.7. Les conversions de type La grande souplesse du langage C permet de mélanger des données de différents types dans une expression. Avant de pouvoir calculer, les données doivent être converties dans un même type. La plupart de ces conversions se passent automatiquement, sans l'intervention du programmeur, qui doit quand même prévoir leur effet. Parfois il est nécessaire de convertir une donnée dans un type différent de celui que choisirait la conversion automatique; dans ce cas, nous devons forcer la conversion à l'aide d'un opérateur spécial ("cast"). 3.7.1. Les conversions de type automatiques Calculs et affectations Si un opérateur a des opérandes de différents types, les valeurs des opérandes sont converties automatiquement dans un type commun. Ces manipulations implicites convertissent en général des types plus 'petits' en des types plus 'larges'; de cette façon on ne perd pas en précision. Lors d'une affectation, la donnée à droite du signe d'égalité est convertie dans le type à gauche du signe d'égalité. Dans ce cas, il peut y avoir perte de précision si le type de la destination est plus faible que celui de la source. Exemple Considérons le calcul suivant: int I = 8; float X = 12.5; double Y; Y = I * X; Pour pouvoir être multiplié avec X, la valeur de I est convertie en float (le type le plus large des deux). Le résultat de la multiplication est du type float, mais avant d'être affecté a Y, il est converti en double. Nous obtenons comme résultat: Y =
  • 45. 100.00 Appels de fonctions Lors de l'appel d'une fonction, les paramètres sont automatiquement convertis dans les types déclarés dans la définition de la fonction. Exemple Au cours des expressions suivantes, nous assistons à trois conversions automatiques: int A = 200; int RES; RES = pow(A, 2); A l'appel de la fonction pow, la valeur de A et la constante 2 sont converties en double, parce que pow est définie pour des données de ce type. Le résultat (type double) retourné par pow doit être converti en int avant d'être affecté à RES. Règles de conversion automatique Conversions automatiques lors d'une opération avec, (1) deux entiers: D'abord, les types char et short sont convertis en int. Ensuite, l'ordinateur choisit le plus large des deux types dans l'échelle suivante: int , unsigned int , long , unsigned long (2) un entier et un rationnel: Le type entier est converti dans le type du rationnel. (3) deux rationnels: L'ordinateur choisit le plus large des deux types selon l'échelle suivante: float , double , long double (4) affectations et opérateurs d'affectation:
  • 46. Lors d'une affectation, le résultat est toujours converti dans le type de la destination. Si ce type est plus faible, il peut y avoir une perte de précision. Exemple Observons les conversions nécessaires lors d'une simple division: int X; float A=12.48; char B=4; X=A/ B; B est converti en float (règle 2). Le résultat de la division est du type float (valeur 3.12) et sera converti en int avant d'être affecté à X (règle 4), ce qui conduit au résultat X=3 . Phénomènes imprévus ... Le mélange de différents types numériques dans un calcul peut inciter à ne pas tenir compte des phénomènes de conversion et conduit parfois à des résultats imprévus ... Exemple Dans cet exemple, nous divisons 3 par 4 à trois reprises et nous observons que le résultat ne dépend pas seulement du type de la destination, mais aussi du type des opérandes. char A=3; int B=4; float C=4; float D,E; char F; D = A/C; E = A/B; F = A/C; * Pour le calcul de D, A est converti en float (règle 2) et divisé par C. Le résultat (0.75) est affecté à D qui est aussi du type float. On obtient donc: D=0.75 * Pour le calcul de E, A est converti en int (règle 1) et divisé par B. Le résultat de la division (type int, valeur 0) est converti en float (règle 4). On obtient donc: E=0.000
  • 47. * Pour le calcul de F, A est converti en float (règle 2) et divisé par C. Le résultat (0.75) est retraduit en char (règle 4). On obtient donc: F=0 Perte de précision Lorsque nous convertissons une valeur en un type qui n'est pas assez précis ou pas assez grand, la valeur est coupée sans arrondir et sans nous avertir ... Exemple unsigned int A = 70000; /* la valeur de A sera: 70000 mod 65536 = 4464 */ 3.7.2. Les conversions de type forcées (casting) Il est possible de convertir explicitement une valeur en un type quelconque en forçant la transformation à l'aide de la syntaxe: Casting (conversion de type forcée) (<Type>) <Expression> Exemple Nous divisons deux variables du type entier. Pour avoir plus de précision, nous voulons avoir un résultat de type rationnel. Pour ce faire, nous convertissons l'une des deux opérandes en float. Automatiquement C convertira l'autre opérande en float et effectuera une division rationnelle: char A=3; int B=4; float C; C = (float)A/B; La valeur de A est explicitement convertie en float. La valeur de B est automatiquement convertie en float (règle 2). Le résultat de la division (type rationnel, valeur 0.75) est affecté à C. Résultat: C=0.75
  • 48. Attention ! Les contenus de A et de B restent inchangés; seulement les valeurs utilisées dans les calculs sont converties !
  • 49. Chapitre 4: LIRE ET ÉCRIRE DES DONNÉES q 4.1. Écriture formatée de données q 4.2. Lecture formatée de données q 4.3. Écriture d'un caractère q 4.4. Lecture d'un caractère La bibliothèque standard <stdio> contient un ensemble de fonctions qui assurent la communication de la machine avec le monde extérieur. Dans ce chapitre, nous allons en discuter les plus importantes: printf() écriture formatée de données scanf() lecture formatée de données putchar() écriture d'un caractère getchar() lecture d'un caractère 4.1. Écriture formatée de données printf() La fonction printf est utilisée pour transférer du texte, des valeurs de variables ou des résultats d'expressions vers le fichier de sortie standard stdout (par défaut l'écran). Ecriture formatée en langage algorithmique
  • 50. écrire <Expression1>, <Expression2>, ... Ecriture formatée en C printf("<format>",<Expr1>,<Expr2>, ... ) "<format>" : format de représentation variables et expressions dont les valeurs sont à <Expr1>,... : représenter La partie " <format>" est en fait une chaîne de caractères qui peut contenir: * du texte * des séquences d'échappement * des spécificateurs de format * Les spécificateurs de format indiquent la manière dont les valeurs des expressions <Expr1..N> sont imprimées. * La partie "<format>" contient exactement un spécificateur de format pour chaque expression <Expr1..N>. * Les spécificateurs de format commencent toujours par le symbole % et se terminent par un ou deux caractères qui indiquent le format d'impression. * Les spécificateurs de format impliquent une conversion d'un nombre en chaîne de caractères. Ils sont encore appelés symboles de conversion. Exemple 1 La suite d'instructions:
  • 51. int A = 1234; int B = 567; printf("%i fois %i est %lin", A, B, (long)A*B); va afficher sur l'écran: 1234 fois 567 est 699678 Les arguments de printf sont - la partie format "%i fois %i est %li" - la variable A - la variable B - l'expression (long)A*B Le1er spécificateur (%i) indique que la valeur de A sera ==>1234 imprimée comme entier relatif. Le 2e spécificateur (%i) indique que la valeur de B sera ==> 567 imprimée comme entier relatif Le 3e spécificateur (%li) indique que la valeur de (long) ==> 699678 A*B sera imprimée comme entier relatif long Exemple 2 La suite d'instructions: char B = 'A'; printf("Le caractère %c a le code %i !n", B, B);
  • 52. va afficher sur l'écran: Le caractère A a le code 65 ! La valeur de B est donc affichée sous deux formats différents: %c comme caractère: A %i comme entier relatif: 65 Spécificateurs de format pour printf SYMBOLE TYPE IMPRESSION COMME %d ou %i int entier relatif %u int entier naturel (unsigned) %o int entier exprimé en octal %x int entier exprimé en hexadécimal %c int caractère %f double rationnel en notation décimale %e double rationnel en notation scientifique %s char* chaîne de caractères 1. Arguments du type long Les spécificateurs %d, %i, %u, %o, %x peuvent seulement représenter des valeurs du type int ou unsigned int. Une valeur trop grande pour être codée dans deux octets est coupée sans avertissement si nous utilisons %d. Pour pouvoir traiter correctement les arguments du type long, il faut utiliser les spécificateurs %ld, %li, %lu, %lo, %lx.
  • 53. Exemple long N = 1500000; printf("%ld, %lx", N, N); ==> 1500000, 16e360 printf("%x, %x" , N); ==> e360, 16 printf("%d, %d" , N); ==> -7328, 22 2. Arguments rationnels Les spécificateurs %f et %e peuvent être utilisés pour représenter des arguments du type float ou double. La mantisse des nombres représentés par %e contient exactement un chiffre (non nul) devant le point décimal. Cette représentation s'appelle la notation scientifique des rationnels. Pour pouvoir traiter correctement les arguments du type long double, il faut utiliser les spécificateurs %Lf et %Le. Exemple float N = 12.1234; double M = 12.123456789; long double P = 15.5; printf("%f", N); ==> 12.123400 printf("%f", M); ==> 12.123457 printf("%e", N); ==> 1.212340e+01 printf("%e", M); ==> 1.212346e+01 printf("%Le", P); ==> 1.550000e+01 3. Largeur minimale pour les entiers Pour les entiers, nous pouvons indiquer la largeur minimale de la valeur à afficher. Dans le champ ainsi réservé, les nombres sont justifiés à droite. Exemples ( _ <=> position libre)
  • 54. printf("%4d", 123); ==> _123 printf("%4d", 1234); ==> 1234 printf("%4d", 12345); ==> 12345 printf("%4u", 0); ==> ___0 printf("%4X", 123); ==> __7B printf("%4x", 123); ==> __7b 4. Largeur minimale et précision pour les rationnels Pour les rationnels, nous pouvons indiquer la largeur minimale de la valeur à afficher et la précision du nombre à afficher. La précision par défaut est fixée à six décimales. Les positions décimales sont arrondies à la valeur la plus proche. Exemples printf("%f", 100.123); ==> 100.123000 printf("%12f", 100.123); ==> __100.123000 printf("%.2f", 100.123); ==> 100.12 printf("%5.0f", 100.123); ==> __100 printf("%10.3f", 100.123); ==> ___100.123 printf("%.4f", 1.23456); ==> 1.2346 4.2. Lecture formatée de données scanf() La fonction scanf est la fonction symétrique à printf; elle nous offre pratiquement les mêmes conversions que printf, mais en sens inverse. Lecture formatée en langage algorithmique lire <NomVariable1>,<NomVariable2>, ... Lecture formatée en C
  • 55. scanf("<format>",<AdrVar1>,<AdrVar2>, ...) "<format>" : format de lecture des données adresses des variables auxquelles les données seront <AdrVar1>,... : attribuées * La fonction scanf reçoit ses données à partir du fichier d'entrée standard stdin (par défaut le clavier). * La chaîne de format détermine comment les données reçues doivent être interprétées. * Les données reçues correctement sont mémorisées successivement aux adresses indiquées par <AdrVar1>,... . * L'adresse d'une variable est indiquée par le nom de la variable précédé du signe &. Exemple La suite d'instructions: int JOUR, MOIS, ANNEE; scanf("%i %i %i", &JOUR, &MOIS, &ANNEE); lit trois entiers relatifs, séparés par des espaces, tabulations ou interlignes. Les valeurs sont attribuées respectivement aux trois variables JOUR, MOIS et ANNEE. * scanf retourne comme résultat le nombre de données correctement reçues (type int). Spécificateurs de format pour scanf
  • 56. SYMBOLE LECTURE D'UN(E) TYPE %d ou %i entier relatif int* %u entier naturel (unsigned) int* %o entier exprimé en octal int* %b entier exprimé en hexadécimal int* %c caractère char* %s chaîne de caractères char* rationnel en notation décimale ou exponentielle %f ou %e float* (scientifique) Le symbole * indique que l'argument n'est pas une variable, mais l'adresse d'une variable de ce type (c.-à-d.: un pointeur sur une variable - voir chapitre 9 'Les pointeurs' ). 1. Le type long Si nous voulons lire une donnée du typelong, nous devons utiliser les spécificateurs %ld, %li, %lu, %lo, %lx. (Sinon, le nombre est simplement coupé à la taille de int). 2. Le type double Si nous voulons lire une donnée du type double, nous devons utiliser les spécificateurs %le ou %lf. 3. Le type long double Si nous voulons lire une donnée du typelong double, nous devons utiliser les spécificateurs %Le ou %Lf. 4. Indication de la largeur maximale Pour tous les spécificateurs, nous pouvons indiquer la largeur maximale du champ à évaluer pour une donnée. Les chiffres qui passent au-delà du champ défini sont attribués à la prochaine variable qui sera lue !
  • 57. Exemple Soient les instructions: int A,B; scanf("%4d %2d", &A, &B); Si nous entrons le nombre 1234567, nous obtiendrons les affectations suivantes: A=1234 B=56 le chiffre 7 sera gardé pour la prochaine instruction de lecture. 5. Les signes d'espacement Lors de l'entrée des données, une suite de signes d'espacement (espaces, tabulateurs, interlignes) est évaluée comme un seul espace. Dans la chaîne de format, les symboles t, n, r ont le même effet qu'un simple espace. Exemple Pour la suite d'instructions int JOUR, MOIS, ANNEE; scanf("%i %i %i", &JOUR, &MOIS, &ANNEE); les entrées suivantes sont correctes et équivalentes: 12 4 1980 ou 12 004 1980
  • 58. ou 12 4 1980 6. Formats 'spéciaux' Si la chaîne de format contient aussi d'autres caractères que des signes d'espacement, alors ces symboles doivent être introduits exactement dans l'ordre indiqué. Exemple La suite d'instructions int JOUR, MOIS, ANNEE; scanf("%i/%i/%i", &JOUR, &MOIS, &ANNEE); accepte les entrées: rejette les entrées: 12/4/1980 12 4 1980 12/04/01980 12 /4 /1980 7. Nombre de valeurs lues Lors de l'évaluation des données,scanf s'arrête si la chaîne de format a été travaillée jusqu'à la fin ou si une donnée ne correspond pas au format indiqué. scanf retourne comme résultat le nombre d'arguments correctement reçus et affectés. Exemple La suite d'instructions int JOUR, MOIS, ANNEE, RECU; RECU = scanf("%i %i %i", &JOUR, &MOIS, &ANNEE); réagit de la façon suivante ( - valeur indéfinie):
  • 59. RECU JOUR MOIS ANNEE Introduit: 12 4 1980 ==> 3 12 4 1980 12/4/1980 ==> 1 12 - - 12.4 1980 ==> 1 12 - - 12 4 19.80 ==> 3 12 4 19 4.3. Écriture d'un caractère La commande, putchar('a'); transfère le caractère a vers le fichier standard de sortie stdout. Les arguments de la fonction putchar sont ou bien des caractères (c.-à-d. des nombres entiers entre 0 et 255) ou bien le symbole EOF (End Of File). EOF est une constante définie dans <stdio> qui marque la fin d'un fichier. La commande putchar(EOF); est utilisée dans le cas où stdout est dévié vers un fichier. Type de l'argument Pour ne pas être confondue avec un caractère, la constante EOF doit nécessairement avoir une valeur qui sort du domaine des caractères (en général EOF a la valeur -1). Ainsi, les arguments de putchar sont par définition du type int et toutes les valeurs traitées par putchar (même celles du type char) sont d'abord converties en int. Exemples char A = 225; char B = 'a'; int C = 'a'; putchar('x'); /* afficher la lettre x */ putchar('?'); /* afficher le symbole ? */ putchar('n'); /* retour à la ligne */ putchar(65); /* afficher le symbole avec */ /* le code 65 (ASCII: 'A') */ putchar(A); /* afficher la lettre avec */ /* le code 225 (ASCII: 'ß') */
  • 60. putchar(B); /* beep sonore */ putchar(C); /* beep sonore */ putchar(EOF); /* marquer la fin du fichier */ 4.4. Lecture d'un caractère Une fonction plus souvent utilisée que putchar est la fonction getchar, qui lit le prochain caractère du fichier d'entrée standard stdin. Type du résultat Les valeurs retournées par getchar sont ou bien des caractères (0 - 255) ou bien le symbole EOF. Comme la valeur du symbole EOF sort du domaine des caractères, le type résultat de getchar est int. En général, getchar est utilisé dans une affectation: int C; C = getchar());
  • 61. Chapitre 5: LA STRUCTURE ALTERNATIVE q 5.1. if - else q 5.2. if sans else q 5.3. if - else if - ... - else q 5.4. Instructions switch case break default q 5.5. Les opérateurs conditionnels A priori, dans un programme, les instructions sont exécutées séquentiellement, c'est à dire dans l'ordre où elles apparaissent. Or la puissance et le comportement intelligent d'un programme proviennent essentiellemnt de la possibilité d'effectuer des choix, de se comporter différemment suivant les circonstances ( celle-ci pouvant être, par exemple, une réponse de l'utilisateur, un résultat de calcul...) Tous les langages disposent d'instructions, nommées " instructions de contrôles ", permettant de réaliser ces choix. Constatons déjà que la particularité la plus importante des instructions de contrôle en C est le fait que les 'conditions' en C peuvent être des expressions quelconques qui fournissent un résultat numérique. La valeur zéro correspond à la valeur logique faux et toute valeur différente de zéro est considérée comme vrai. 5.1. if - else La structure alternative en langage algorithmique si (<expression logique>) alors <bloc d'instructions 1> sinon <bloc d'instructions 2> fsi
  • 62. * Si l'<expression logique> a la valeur logique vrai, alors le <bloc d'instructions 1> est exécuté * Si l'<expression logique> a la valeur logique faux, alors le <bloc d'instructions 2> est exécuté La structure alternative en C if ( <expression> ) <bloc d'instructions 1> else <bloc d'instructions 2> * Si l'<expression> fournit une valeur différente de zéro, alors le <bloc d'instructions 1> est exécuté * Si l'<expression> fournit la valeur zéro, alors le <bloc d'instructions 2> est exécuté La partie <expression> peut désigner : une variable d'un type numérique, une expression fournissant un résultat numérique. La partie <bloc d'instructions> peut désigner : un (vrai) bloc d'instructions compris entre accolades, une seule instruction terminée par un point-virgule. Exemple 1 if (a > b) max = a; else max = b; Exemple 2
  • 63. if (EGAL) printf("A est égal à Bn"); else printf("A est différent de Bn"); Exemple 3 if (A-B) printf("A est différent de Bn"); else printf("A est égal à Bn"); Exemple 4 if (A > B) { AIDE = A; A = C; C = AIDE; } else { AIDE = B; B = C; C = AIDE; } 5.2. if sans else La partie else est facultative. On peut donc utiliser if de la façon suivante: if sans else
  • 64. if ( <expression> ) <bloc d'instructions> Attention ! Comme la partie else est optionnelle, les expressions contenant plusieurs structures if et if - else peuvent mener à des confusions. Exemple L'expression suivante peut être interprétée de deux façons: if (N>0) if (A>B) MAX=A; else MAX=B; if (N>0) if (A>B) MAX=A; else MAX=B; Pour N=0, A=1 et B=2, * dans la première interprétation, MAX reste inchangé, * dans la deuxième interprétation, MAX obtiendrait la valeur de B. Sans règle supplémentaire, le résultat de cette expression serait donc imprévisible. Convention En C une partie else est toujours liée au dernier if qui ne possède pas de partie else.
  • 65. Dans notre exemple, C utiliserait donc la première interprétation. Solution Pour éviter des confusions et pour forcer une certaine interprétation d'une expression, il est recommandé d'utiliser des accolades { } . Exemple Pour forcer la deuxième interprétation de l'expression ci-dessus, nous pouvons écrire: if (N>0) { if (A>B) MAX=A; } else MAX=B; 5.3. if - else if - ... - else En combinant plusieurs structures if - else en une expression nous obtenons une structure qui est très courante pour prendre des décisions entre plusieurs alternatives: if - else - ... - else if ( <expr1> ) <bloc1> else if (<expr2>) <bloc2> else if (<expr3>) <bloc3> else if (<exprN>) <blocN> else <blocN+1>
  • 66. Les expressions <expr1> ... <exprN> sont évaluées du haut vers le bas jusqu'à ce que l'une d'elles soit différente de zéro. Le bloc d'instructions y lié est alors exécuté et le traitement de la commande est terminé. Exemple #include <stdio.h> main() { int A,B; printf("Entrez deux nombres entiers :"); scanf("%i %i", &A, &B); if (A > B) printf("%i est plus grand que %in", A, B); else if (A < B) printf("%i est plus petit que %in", A, B); else printf("%i est égal à %in", A, B); return 0; } La dernière partie else traite le cas où aucune des conditions n'a été remplie. Elle est optionnelle, mais elle peut être utilisée très confortablement pour détecter des erreurs. Exemple ... printf("Continuer (O)ui / (N)on ?"); getchar(C); if (C=='O') { ... } else if (C=='N') printf("Au revoir ...n");
  • 67. else printf("aErreur d'entrée !n"); ... 5.4. Instructions switch case break default L'instruction switch compare une valeur à une série de constantes et permet d'exécuter des instructions dans les cas où la valeur est égale à une des constantes: switch (valeur) { case constante1: instructions1 break; case constante2: instructions2 break; default: instructions3 } Peut se lire comme: if (valeur == constante1) { instructions1 } else if (valeur == constante2) { instructions2 } else { instructions3 }
  • 68. L'instruction break sert à interrompre le flux d'exécution. Si on l'omet, les instructions suivant les case sont exécutées en séquence: switch (valeur) { case constante1: instructions1 case constante2: instructions2 default: instructions3 } se lit: if (valeur == constante1) { instructions1 instructions2 instructions3 } else if (valeur == constante2) { instructions2 instructions3 } else { instructions3 } Syntaxe de l'instruction switch En langage algorithmique
  • 69. au cas où expression vaut : - valeur1 : <bloc_d_instructions1> - valeur2 : <bloc_d_instructions2> ...................................................... ...................................................... -valeurN : <bloc_d_instructionN> aucune des valeurs précédentes : <bloc_d_instructions> En langage C switch ( expression) { case constante_1 : [<suite_d_instructions_1>] break; case constante_2 : [< suite_d_instructions_2>] break; ........................................... ........................................... case constante_N : [<suite_d_instructions_N>] break; [defaut : <suite_d_instructions> ] } expression : expression entière quelconque constante_1 constante_2 .... constante_N : expressions constantes d'un type entier quelconque ( char est accepté car il sera converti en int ) < suite_d_instructions_1> ...<suite_d_instructions_N> : séquence d'instructions quelconques. Les crochets ( [ et ] ) signifient que ce qu'ils renferment est facultatif.
  • 70. Exemple : l'instruction switch est commode pour fabriquer des "menus": char choix ; printf("Liste par groupe taper 1 n"); printf("Liste alphabetique taper 2n"); printf("Pour sortir taper Sn"); printf("n Votre choix "); choix = getchar(); switch (choix) { case '1' : ............ ; ............ ; break ; case '2' : ............ ; ............. ; break; case 'S' : printf("n Fin du programme"); break; default : printf("n Ce choix n'est pas prevu "); } 5.5. Les opérateurs conditionnels Le langage C possède une paire d'opérateurs un peu exotiques qui peut être utilisée comme alternative à if - else et qui a l'avantage de pouvoir être intégrée dans une expression: Les opérateurs conditionnels <expr1> ? <expr2> : <expr3> * Si <expr1> fournit une valeur différente de zéro, alors la valeur de <expr2> est fournie comme résultat * Si <expr1> fournit la valeur zéro, alors la valeur de <expr3> est fournie comme résultat Exemple
  • 71. La suite d'instructions if (A>B) MAX=A; else MAX=B; peut être remplacée par: MAX = (A > B) ? A : B; Employés de façon irréfléchis, les opérateurs conditionnels peuvent nuire à la lisibilité d'un programme, mais si on les utilise avec précaution, ils fournissent des solutions très élégantes. Exemple printf("Vous avez %i carte%c n", N, (N==1) ? ' ' : 's'); Les règles de conversion de types s'appliquent aussi aux opérateurs conditionnels ? : Ainsi, pour un entier N du type int et un rationnel F du type float, l'expression (N>0) ? N : F va toujours fournir un résultat du type float, n'importe si N est plus grand ou plus petit que zéro!
  • 72. Chapitre 6: LA STRUCTURE REPETITIVE En C, nous disposons de trois structures qui nous permettent la définition de boucles conditionnelles: 1) la structure : while 2) la structure : do - while 3) la structure : for Théoriquement, ces structures sont interchangeables, c.-à-d. il serait possible de programmer toutes sortes de boucles conditionnelles en n'utilisant qu'une seule des trois structures. Il est quand même absolument recommandé de choisir toujours la structure la mieux adaptée au cas actuel (voir 6.4.). q 6.1. while q 6.2. do - while q 6.3. for q 6.4. Choix de la structure répétitive
  • 73. 6.1. while La structure while correspond tout à fait à la structure tant que du langage algorithmique. (Si on néglige le fait qu'en C les conditions peuvent être formulées à l'aide d'expressions numériques.) La structure tant que en langage algorithmique tant que (<expression logique>) faire <bloc d'instructions> ftant * Tant que l'<expression logique> fournit la valeur vrai, le <bloc d'instructions> est exécuté. * Si l'<expression logique> fournit la valeur faux, l'exécution continue avec l'instruction qui suit ftant. * Le <bloc d'instructions> est exécuté zéro ou plusieurs fois. La structure while en C
  • 74. while ( <expression> ) <bloc d'instructions> * Tant que l'<expression> fournit une valeur différente de zéro, le <bloc d'instructions> est exécuté. * Si l'<expression> fournit la valeur zéro, l'exécution continue avec l'instruction qui suit le bloc d'instructions. * Le <bloc d'instructions> est exécuté zéro ou plusieurs fois. La partie <expression> peut désigner : q une variable d'un type numérique, q une expression fournissant un résultat numérique. La partie <bloc d'instructions> peut désigner : qun (vrai) bloc d'instructions compris entre accolades, une seule instruction terminée par un point-virgule. Exemple 1
  • 75. /* Afficher les nombres de 0 à 9 */ int I = 0; while (I<10) { printf("%i n", I); I++; } Exemple 2 int I; /* Afficher les nombres de 0 à 9 */ I = 0; while (I<10) printf("%i n", I++); /* Afficher les nombres de 1 à 10 */ I = 0; while (I<10) printf("%i n", ++I);
  • 76. Exemple 3 /* Afficher les nombres de 10 à 1 */ int I=10; while (I) printf("%i n", I--); Remarque Parfois nous voulons seulement attendre un certain événement, sans avoir besoin d'un traitement de données. Dans ce cas, la partie <bloc d'instructions> peut être vide (notation: ; ou {} ). La ligne suivante ignore tous les espaces entrés au clavier et peut être utilisée avant de lire le premier caractère significatif: q while (getch()==' ') ; 6.2. do - while La structure do - while est semblable à la structure while, avec la différence suivante : * while évalue la condition avant d'exécuter le bloc d'instructions,
  • 77. * do - while évalue la condition après avoir exécuté le bloc d'instructions. Ainsi le bloc d'instructions est exécuté au moins une fois. La structure do - while en C do <bloc d'instructions> while ( <expression> ); Le <bloc d'instructions> est exécuté au moins une fois et aussi longtemps que l'<expression> fournit une valeur différente de zéro. En pratique, la structure do - while n'est pas si fréquente que while; mais dans certains cas, elle fournit une solution plus élégante. Une application typique de do - while est la saisie de données qui doivent remplir une certaine condition: Exemple 1
  • 78. float N; do { printf("Introduisez un nombre entre 1 et 10 :"); scanf("%f", &N); } while (N<1 || N>10); Exemple 2 int n, div; printf("Entrez le nombre à diviser : "); scanf("%i", &n); do { printf("Entrez le diviseur : "); scanf("%i", &div); } while (!div); printf("%i / %i = %fn", n, div, (float)n/div);
  • 79. do - while est comparable à la structure répéter du langage algorithmique si la condition finale est inversée logiquement. Exemple 3 Le programme de calcul de la racine carrée : programme RACINE_CARREE réel N répéter écrire "Entrer un nombre (>=0) : " lire N jusqu'à (N >= 0) écrire "La racine carrée de ",N ,"vaut ", N fprogramme (* fin RACINE_CARRE *) se traduit en C par :
  • 80. #include <stdio.h> #include <math.h> main() { float N; do { printf("Entrer un nombre (>= 0) : "); scanf("%f", &N) } while (N < 0); printf("La racine carrée de %.2f est %.2fn", N, sqrt(N)); return 0; } 6.3. for La structure pour en langage algorithmique est utilisée pour faciliter la programmation de boucles de comptage. La structure for en C est plus générale et beaucoup plus puissante. La structure for en C
  • 81. for ( <expr1> ; <expr2> ; <expr3> ) <bloc d'instructions> est équivalent à : <expr1>; while ( <expr2> ) { <bloc d'instructions> <expr3>; } <expr1>est évaluée une fois avant le passage de la boucle. Elle est utilisée pour initialiser les données de la boucle. <expr2> est évaluée avant chaque passage de la boucle. Elle est utilisée pour décider si la boucle est répétée ou non. <expr3> est évaluée à la fin de chaque passage de la boucle. Elle est utilisée pour réinitialiser les données de la boucle.
  • 82. Le plus souvent, for est utilisé comme boucle de comptage : for ( <init.> ; <cond. répétition> ; <compteur> ) <bloc d'instructions> Exemple int I; for (I=0 ; I<=20 ; I++) printf("Le carré de %d est %d n", I, I*I); En pratique, les parties <expr1> et <expr2> contiennent souvent plusieurs initialisations ou réinitialisations, séparées par des virgules. Exemple
  • 83. int n, tot; for (tot=0, n=1 ; n<101 ; n++) tot+=n; printf("La somme des nombres de 1 à 100 est %dn", tot); Exemple Cet exemple nous présente différentes variations pour réaliser le même traitement et nous montre la puissance de la structure for. Les expressions suivantes lisent un caractère au clavier et affichent son code numérique en notation binaire : /* a */ /* notation utilisant la structure while */ int C, I; C=getchar(); I=128; while (I>=1) { printf("%i ", C/I); C%=I; I/=2; } /* b */ /* notation utilisant for - très lisible - */
  • 84. /* préférée par les débutants en C */ int C, I; C=getchar(); for (I=128 ; I>=1 ; I/=2) { printf("%i ", C/I); C%=I; } /* c */ /* notation utilisant for - plus compacte - */ /* préférée par les experts en C */ int C, I; C=getchar(); for (I=128 ; I>=1 ; C%=I, I/=2) printf("%i ", C/I); /* d */ /* notation utilisant for - à déconseiller - */ /* surcharge et mauvais emploi de la structure */ int C, I; for(C=getchar(),I=128 ; I>=1 ; printf("%i ",C/I),C%=I,I/=2); 6.4. Choix de la structure répétitive
  • 85. Dans ce chapitre, nous avons vu trois façons différentes de programmer des boucles (while, do - while, for ). Utilisez la structure qui reflète le mieux l'idée du programme que vous voulez réaliser, en respectant toutefois les directives suivantes : * Si le bloc d'instructions ne doit pas être exécuté si la condition est fausse, alors utilisez while ou for. * Si le bloc d'instructions doit être exécuté au moins une fois, alors utilisez do - while. * Si le nombre d'exécutions du bloc d'instructions dépend d'une ou de plusieurs variables qui sont modifiées à la fin de chaque répétition, alors utilisez for. * Si le bloc d'instructions doit être exécuté aussi longtemps qu'une condition extérieure est vraie (p.ex aussi longtemps qu'il y a des données dans le fichier d'entrée), alors utilisez while. Le choix entre for et while n'est souvent qu'une question de préférence ou d'habitudes: * for nous permet de réunir avantageusement les instructions qui influencent le nombre de répétitions au début de la structure. * while a l'avantage de correspondre plus exactement aux structures d'autres langages de programmation (while, tant que). * for a le désavantage de favoriser la programmation de structures surchargées et par la suite illisibles. * while a le désavantage de mener parfois à de longues structures, dans lesquelles il faut
  • 86. chercher pour trouver les instructions qui influencent la condition de répétition.
  • 87. Chapitre 7: LES TABLEAUX Les tableaux sont certainement les variables structurées les plus populaires. Ils sont disponibles dans tous les langages de programmation et servent à résoudre une multitude de problèmes. Dans une première approche, le traitement des tableaux en C ne diffère pas de celui des autres langages de programmation. q 7.1. Les tableaux à une dimension r 7.1.1. Déclaration et mémorisation r 7.1.2. Initialisation et réservation automatique r 7.1.3. Accès aux composantes r 7.1.4. Affichage et affectation q 7.2. Les tableaux à deux dimensions r 7.2.1. Déclaration et mémorisation r 7.2.2. Initialisation et réservation automatique r 7.2.3. Accès aux composantes r 7.2.4. Affichage et affectation 7.1. Les tableaux à une dimension Définitions Un tableau (uni-dimensionnel) A est une variable structurée formée d'un nombre entier N de variables simples du même type, qui sont appelées les composantes du tableau. Le nombre de composantes N est alors la dimension du tableau.
  • 88. En faisant le rapprochement avec les mathématiques, on dit encore que "A est un vecteur de dimension N" Exemple La déclaration int JOURS[12]={31,28,31,30,31,30,31,31,30,31,30,31}; définit un tableau du type int de dimension 12. Les 12 composantes sont initialisées par les valeurs respectives 31, 28, 31, ... , 31. On peut accéder à la première composante du tableau par JOURS[0] , à la deuxième composante par JOURS[1], . . . , à la dernière composante par JOURS[11]. 7.1.1. Déclaration et mémorisation Déclaration Déclaration de tableaux en langage algorithmique <TypeSimple> tableau <NomTableau>[<Dimension>] Déclaration de tableaux en C <TypeSimple> <NomTableau>[<Dimension>]; Les noms des tableaux sont des identificateurs qui doivent correspondre aux restrictions définies au chapitre 2.2.4.
  • 89. Exemples Les déclarations suivantes en langage algorithmique, entier tableau A[25] réel tableau B[100] booléen tableau C[10] caractère tableau D[30] se laissent traduire en C par: int A[25]; ou bien long A[25]; float B[100]; ou bien double B[100]; int C[10]; char D[30]; Mémorisation En C, le nom d'un tableau est le représentant de l'adresse du premier élément du tableau. Les adresses des autres composantes sont calculées (automatiquement) relativement à cette adresse. Exemple: short A[5] = {1200, 2300, 3400, 4500, 5600}; Si un tableau est formé de N composantes et si une composante a besoin de M octets en mémoire, alors le tableau occupera de N*M octets.
  • 90. Exemple : En supposant qu'une variable du type long occupe 4 octets (c.-à-d: sizeof (long)=4), pour le tableau T déclaré par: long T[15]; C réservera N*M = 15*4 = 60 octets en mémoire. 7.1.2. Initialisation et réservation automatique Initialisation Lors de la déclaration d'un tableau, on peut initialiser les composantes du tableau, en indiquant la liste des valeurs respectives entre accolades. Exemples : int A[5] = {10, 20, 30, 40, 50}; float B[4] = {-1.05, 3.33, 87e-5, -12.3E4}; int C[10] = {1, 0, 0, 1, 1, 1, 0, 1, 0, 1}; Il faut évidemment veiller à ce que le nombre de valeurs dans la liste corresponde à la dimension du tableau. Si la liste ne contient pas assez de valeurs pour toutes les composantes, les composantes restantes sont initialisées par zéro. Réservation automatique Si la dimension n'est pas indiquée explicitement lors de l'initialisation, alors l'ordinateur réserve automatiquement le nombre d'octets nécessaires. Exemples : int A[ ] = {10, 20, 30, 40, 50}; ==> réservation de 5*sizeof(int) octets (dans notre cas: 10 octets) float B[ ] = {-1.05, 3.33, 87e-5, -12.3E4};
  • 91. ==> réservation de 4*sizeof(float) octets (dans notre cas: 16 octets) int C[ ] = {1, 0, 0, 1, 1, 1, 0, 1, 0, 1}; ==> réservation de 10*sizeof(int) octets (dans notre cas: 20 octets) Exemples : 7.1.3. Accès aux composantes En déclarant un tableau par: int A[5]; nous avons défini un tableau A avec cinq composantes, auxquelles on peut accéder par: A[0], A[1], ... , A[4] Exemples :
  • 92. MAX = (A[0]>A[1]) ? A[0] : A[1]; A[4] *= 2; Attention ! Considérons un tableau T de dimension N : En C, - l'accès au premier élément du tableau se fait par T[0] - l'accès au dernier élément du tableau se fait par T[N-1] En langage algorithmique, - l'accès au premier élément du tableau se fait par T[1] - l'accès au dernier élément du tableau se fait par T[N] 7.1.4. Affichage et affectation La structure for se prête particulièrement bien au travail avec les tableaux. La plupart des applications se laissent implémenter par simple modification des exemples-types de l'affichage et de l'affectation. - Affichage du contenu d'un tableau Traduisons le programme AFFICHER du langage algorithmique en C: programme AFFICHER | entier tableau A[5] | entier I (* Compteur *) | pour I variant de 1 à 5 faire | écrire A[I] | fpour fprogramme
  • 93. #include <stdio.h> main() { int A[5]; int I; /* Compteur */ for (I=0; I<5; I++) printf("%d ", A[I]); printf("n"); return 0; } Remarques * Avant de pouvoir afficher les composantes d'un tableau, il faut évidemment leur affecter des valeurs. * Rappelez-vous que la deuxième condition dans la structure for n'est pas une condition d'arrêt, mais une condition de répétition! Ainsi la commande d'affichage sera répétée aussi longtemps que I est inférieur à 5. La boucle sera donc bien exécutée pour les indices 0,1,2,3 et 4 ! * Par opposition à la commande simplifiée écrire A[I] du langage algorithmique, la commande printf doit être informée du type exact des données à afficher. (Ici: %d ou %i pour des valeurs du type int) *Pour être sûr que les valeurs sont bien séparées lors de l'affichage, il faut inclure au moins un espace dans la chaîne de format. Autres possibilités: printf("%dt", A[I]); /* tabulateur */ printf("%7d", A[I]); /* format d'affichage */ Affectation - Affectation avec des valeurs provenant de l'extérieur Traduisons le programme REMPLIR du langage algorithmique en C:
  • 94. programme REMPLIR | entier tableau A[5] | entier I (* Compteur *) | pour I variant de 1 à 5 faire | lire A[I] | fpour fprogramme #include <stdio.h> main() { int A[5]; int I; /* Compteur */ for (I=0; I<5; I++) scanf("%d", &A[I]); return 0; } Remarques * Comme scanf a besoin des adresses des différentes composantes du tableau, il faut faire précéder le terme A[I] par l'opérateur adresse '&'. * La commande de lecture scanf doit être informée du type exact des données à lire. (Ici: %d ou %i pour lire des valeurs du type int) 7.2. Les tableaux à deux dimensions Définitions En C, un tableau à deux dimensions A est à interpréter comme un tableau (uni-dimensionnel) de dimension L dont chaque composante est un tableau (uni-dimensionnel) de dimension C. On appelle L le nombre de lignes du tableau et C le nombre de colonnes du tableau. L et C sont alors les deux dimensions du tableau. Un tableau à deux dimensions contient donc L*C composantes.
  • 95. On dit qu'un tableau à deux dimensions est carré, si L est égal à C. En faisant le rapprochement avec les mathématiques, on peut dire que "A est un vecteur de L vecteurs de dimension C", ou mieux: "A est une matrice de dimensions L et C". Exemple : Considérons un tableau NOTES à une dimension pour mémoriser les notes de 20 élèves d'une classe dans un devoir: int NOTE[20] = {45, 34, ... , 50, 48}; Pour mémoriser les notes des élèves dans les 10 devoirs d'un trimestre, nous pouvons rassembler plusieurs de ces tableaux uni-dimensionnels dans un tableau NOTES à deux dimensions : int NOTE[10][20] = { {45, 34, ... , 50, 48}, {39, 24, ... , 49, 45}, ... ... ... {40, 40, ... , 54, 44} };
  • 96. Dans une ligne nous retrouvons les notes de tous les élèves dans un devoir. Dans une colonne, nous retrouvons toutes les notes d'un élève. 7.2.1. Déclaration et mémorisation Déclarations Déclaration de tableaux à deux dimensions en lang. algorithmique <TypeSimple> tableau <NomTabl>[<DimLigne>,<DimCol>] Déclaration de tableaux à deux dimensions en C <TypeSimple> <NomTabl>[<DimLigne>][<DimCol>]; Exemples : Les déclarations suivantes en langage algorithmique, entier tableau A[10,10] réel tableau B[2,20] booléen tableau C[3,3] caractère tableau D[15,40] se laissent traduire en C par:
  • 97. int A[10][10];ou bien long A[10][10]; float B[2][20]; ou bien double B[2][20]; int C[3][3]; char D[15][40]; Mémorisation Comme pour les tableaux à une dimension, le nom d'un tableau est le représentant de l'adresse du premier élément du tableau (c.-à-d. l'adresse de la première ligne du tableau). Les composantes d'un tableau à deux dimensions sont stockées ligne par ligne dans la mémoire. Exemple: Mémorisation d'un tableau à deux dimensions short A[3][2] = {{1, 2 }, {10, 20 }, {100, 200}}; Un tableau de dimensions L et C, formé de composantes dont chacune a besoin de M octets, occupera L*C*M octets en mémoire. Exemple : En supposant qu'une variable du type double occupe 8 octets (c.-à-d: sizeof (double)=8), pour le tableau T déclaré par: double T[10][15]; C réservera L*C*M = 10*15*8 = 1200 octets en mémoire. 7.2.2. Initialisation et réservation automatique Initialisation Lors de la déclaration d'un tableau, on peut initialiser les composantes du
  • 98. tableau, en indiquant la liste des valeurs respectives entre accolades. A l'intérieur de la liste, les composantes de chaque ligne du tableau sont encore une fois comprises entre accolades. Pour améliorer la lisibilité des programmes, on peut indiquer les composantes dans plusieurs lignes. Exemples : int A[3][10] ={{ 0,10,20,30,40,50,60,70,80,90}, {10,11,12,13,14,15,16,17,18,19}, { 1,12,23,34,45,56,67,78,89,90} }; float B[3][2] = {{-1.05 , -1.10 }, {86e-5, 87e-5 }, {-12.5E4, -12.3E4} }; Lors de l'initialisation, les valeurs sont affectées ligne par ligne en passant de gauche à droite. Nous ne devons pas nécessairement indiquer toutes les valeurs: Les valeurs manquantes seront initialisées par zéro. Il est cependant défendu d'indiquer trop de valeurs pour un tableau. Exemples :
  • 99. Réservation automatique Si le nombre de lignes L n'est pas indiqué explicitement lors de l'initialisation, l'ordinateur réserve automatiquement le nombre d'octets nécessaires. int A[ ][10] = { { 0,10,20,30,40,50,60,70,80,90}, {10,11,12,13,14,15,16,17,18,19}, { 1,12,23,34,45,56,67,78,89,90} }; réservation de 3*10*2 = 60 octets float B[ ][2] = { { -1.05 , -1.10 }, { 86e-5 , 87e-5 }, { -12.5E4, -12.3E4} }; réservation de 3*2*4 = 24 octets Exemple :
  • 100. 7.2.3. Accès aux composantes Accès à un tableau à deux dimensions en lang. algorithmique <NomTableau>[<Ligne>, <Colonne>] Accès à un tableau à deux dimensions en C <NomTableau>[<Ligne>][<Colonne>] Les éléments d'un tableau de dimensions L et C se présentent de la façon suivante: A[0][0] A[0][1] A[0][2] . . . . . . . . . . .A[0][C-1] A[1][0] A[1][1] A[1][2] . . . . . . . . . . .A[1][C-1] A[2][0] A[2][1] A[2][2] . . . . . . . . . . .A[2][C-1] ......................................... A[L-1][0] A[L-1][1] A[L-1][2] . . . . . . . . . A[L-1][C-1] Attention !
  • 101. Considérons un tableau A de dimensions L et C. En C, - les indices du tableau varient de 0 à L-1, respectivement de 0 à C-1. ième ième - la composante de la N ligne et M colonne est notée: A[N-1] [M-1] En langage algorithmique, - les indices du tableau varient de 1 à L, respectivement de 1 à C. ième ième - la composante de la N ligne et M colonne est notée: A[N,M] 7.2.4. Affichage et affectation Lors du travail avec les tableaux à deux dimensions, nous utiliserons deux indices (p.ex: I et J), et la structure for, souvent imbriquée, pour parcourir les lignes et les colonnes des tableaux. - Affichage du contenu d'un tableau à deux dimensions Traduisons le programme AFFICHER du langage algorithmique en C: programme AFFICHER | entier tableau A[5,10] | entier I,J | (* Pour chaque ligne ... *) | pour I variant de 1 à 5 faire | (* ... considérer chaque composante *) | pour J variant de 1 à 10 faire | écrire A[I,J] | fpour | (* Retour à la ligne *) | écrire
  • 102. | fpour fprogramme main() { int A[5][10]; int I,J; /* Pour chaque ligne ... */ for (I=0; I<5; I++) { /* ... considérer chaque composante */ for (J=0; J<10; J++) printf("%7d", A[I][J]); /* Retour à la ligne */ printf("n"); } return 0; } Remarques * Avant de pouvoir afficher les composantes d'un tableau, il faut leur affecter des valeurs. * Pour obtenir des colonnes bien alignées lors de l'affichage, il est pratique d'indiquer la largeur minimale de l'affichage dans la chaîne de format. Pour afficher des matrices du type int (valeur la plus 'longue': -32768), nous pouvons utiliser la chaîne de format "%7d" : printf("%7d", A[I][J]); - Affectation avec des valeurs provenant de l'extérieur Traduisons le programme REMPLIR du langage algorithmique en C:
  • 103. programme REMPLIR | entier tableau A[5,10] | entier I,J | (* Pour chaque ligne ... *) | pour I variant de 1 à 5 faire | (* ... considérer chaque composante *) | pour J variant de 1 à 10 faire | lire A[I,J] | fpour | fpour fprogramme main() { int A[5][10]; int I,J; /* Pour chaque ligne ... */ for (I=0; I<5; I++) /* ... considérer chaque composante */ for (J=0; J<10; J++) scanf("%d", &A[I][J]); return 0; }
  • 104. Chapitre 8: LES CHAÎNES DE CARACTÈRES 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. q 8.1. Déclaration et mémorisation q 8.2. Les chaînes de caractères constantes q 8.3. Initialisation de chaînes de caractères q 8.4. Accès aux éléments d'une chaîne q 8.5. Précédence alphabétique et lexicographique q 8.6. Travailler avec des chaînes de caractères r 8.6.1. Les fonctions de <stdio.h> r 8.6.2. Les fonctions de <string> r 8.6.3. Les fonctions de <stdlib> r 8.6.4. Les fonctions de <ctype> q 8.7. Tableaux de chaînes de caractères r 8.7.1. Déclaration, initialisation et mémorisation r 8.7.2. Accès aux différentes composantes 8.1. Déclaration et mémorisation Déclaration de chaînes de caractères en langage algorithmique chaîne <NomVariable> Déclaration de chaînes de caractères en C
  • 105. char <NomVariable> [<Longueur>]; Exemples : char NOM [20]; char PRENOM [20]; char PHRASE [300]; Espace à réserver Lors de la déclaration, nous devons indiquer l'espace à réserver en mémoire pour le stockage de la chaîne. La représentation interne d'une chaîne de caractères est terminée par le symbole '0' (NUL). 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 ... Mémorisation Le nom d'une chaîne est le représentant de l'adresse du premier caractère de la chaîne. Pour mémoriser une variable qui doit être capable de contenir un texte de N caractères, nous avons besoin de N+1 octets en mémoire: Exemple: Mémorisation d'un tableau char TXT[10] = "BONJOUR !"; 8.2. Les chaînes de caractères constantes * Les chaînes de caractères constantes (string literals) sont indiquées entre
  • 106. 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 " : "Affichage de "guillemets" n" * 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'} * Plusieurs chaînes de caractères constantes qui sont séparées par des signes d'espacement (espaces, tabulateurs ou interlignes) dans le texte du programme seront réunies en une seule chaîne constante lors de la compilation: "un " "deux" " trois" sera évalué à "un deux trois" Ainsi il est possible de définir de très longues chaînes de caractères constantes en utilisant plusieurs lignes dans le texte du programme. Observation 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: P.ex: 'x' a la valeur
  • 107. 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 8.3. Initialisation de chaînes 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 CHAINE[ ] = {'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ère constante: char CHAINE[ ] = "Hello"; Lors de l'initialisation par [] , l'ordinateur réserve automatiquement le nombre d'octets nécessaires pour la chaîne, c.-à-d.: le nombre de caractères + 1 (ici: 6 octets). 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. Exemples :
  • 108. 8.4. Accès aux éléments d'une chaîne L'accès à un élément d'une chaîne de caractères peut se faire de la même façon que l'accès à un élément d'un tableau. En déclarant une chaîne par: char A[6]; nous avons défini un tableau A avec six éléments, auxquels on peut accéder par: A[0], A[1], ... , A[5] Exemple 8.5. Précédence alphabétique et lexicographique Précédence alphabétique des caractères
  • 109. 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. Relation de précédence De la précédence alphabétique des caractères, on peut déduire une relation de précédence 'est inférieur à' sur l'ensemble des caractères. Ainsi, on peut dire que '0' est inférieur à 'Z' et noter '0' < 'Z' 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). Précédence lexicographique des chaînes de caractères En nous basant sur cette relation de précédence alphabétique des caractères, nous pouvons définir une précédence lexicographique pour les chaînes de caractères. Cette relation de précédence suit l'<<ordre du dictionnaire>> et est définie de façon récurrente: a) La chaîne vide "" précède lexicographiquement toutes les autres chaînes. b) La chaîne A = "a1a2a ... ap" (p caractères) précède lexicographiquement la chaîne B = "b1b2 ... bm" (m caractères) si l'une des deux conditions suivantes est remplie: 1) 'a1' < 'b1' 2) 'a1' = 'b1' et "a2a3 ... ap" précède lexicographiquement "b2b3 ... bm" Exemples :
  • 110. "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' (le code ASCII de ' ' est 32, et le code ASCII de 'a' est 97) Remarque Malheureusement, il existe différents codes de caractères (p.ex. ASCII, EBCDIC, ISO) et l'ordre lexicographique est dépendant de la machine. Même la fonction strcmp qui indique la précédence lexicographique de deux chaînes de caractères (voir 8.6.2. ) dépend du code de caractères utilisé. Conversions et tests En tenant compte de l'ordre alphabétique des caractères, on peut contrôler le type du caractère (chiffre, majuscule, minuscule). Exemples : if (C>='0' && C<='9') printf("Chiffren", C); if (C>='A' && C<='Z') printf("Majusculen", C); if (C>='a' && C<='z') printf("Minusculen", C); Il est facile, de convertir des lettres majuscules dans des minuscules:
  • 111. if (C>='A' && C<='Z') C = C-'A'+'a'; ou vice-versa: if (C>='a' && C<='z') C = C-'a'+'A'; 8.6. Travailler avec des chaînes de caractères Les bibliothèques de fonctions de C contiennent une série de fonctions spéciales pour le traitement de chaînes de caractères. Sauf indication contraire, les fonctions décrites dans ce chapitre sont portables conformément au standard ANSI-C. 8.6.1. Les fonctions de <stdio.h> Comme nous l'avons déjà vu au chapitre 4 , la bibliothèque <stdio> nous offre des fonctions qui effectuent l'entrée et la sortie des données. A côté des fonctions printf et scanf que nous connaissons déjà, nous y trouvons les deux fonctions puts et gets, spécialement conçues pour l'écriture et la lecture de chaînes de caractères. - Affichage de chaînes de caractères printf avec le spécificateur de format %s permet d'intégrer une chaîne de caractères dans une phrase. En plus, le spécificateur %s permet l'indication de la largeur minimale du champ d'affichage. Dans ce champ, les données sont justifiées à droite. Si on indique une largeur minimale négative, la chaîne sera justifiée à gauche. Un nombre suivant un point indique la largeur maximale pour l'affichage. Exemples :
  • 112. char NOM[] = "hello, world"; printf(":%s:", NOM); -> :hello, world: printf(":%5s:", NOM); -> :hello, world: printf(":%15s:", NOM); -> : hello, world: printf(":%-15s:", NOM); -> :hello, world : printf(":%.5s:", NOM); -> :hello: puts est idéale pour écrire une chaîne constante ou le contenu d'une variable dans une ligne isolée. Syntaxe: puts( <Chaîne> ) puts écrit puts la chaîne de caractères désignée par <Chaîne> sur stdout et provoque un retour à la ligne. Effet: En pratique,puts(TXT); est équivalent à printf("%sn",TXT); Exemples : char TEXTE[] = "Voici une première ligne."; puts(TEXTE); puts("Voici une deuxième ligne."); - Lecture de chaînes de caractères scanf avec le spécificateur %s permet de lire un mot isolé à l'intérieur d'une suite de données du même ou d'un autre type. scanf avec le spécificateur %s lit un mot du fichier d'entrée standard Effet: stdin et le mémorise à l'adresse qui est associée à %s. Exemple :
  • 113. char LIEU[25]; int JOUR, MOIS, ANNEE; printf("Entrez lieu et date de naissance : n"); scanf("%s %d %d %d", LIEU, &JOUR, &MOIS, &ANNEE); Remarques importantes * La fonction scanf a besoin des adresses de ses arguments: * Les noms des variables numériques (int, char, long, float, ...) doivent être marqués par le symbole '&' (voir chap 4.4.). * Comme le nom d'une chaîne de caractères est le représentant de l'adresse du premier caractère de la chaîne, il ne doit pas être précédé de l'opérateur adresse '&' ! * La fonction scanf avec plusieurs arguments présuppose que l'utilisateur connaisse exactement le nombre et l'ordre des données à introduire! Ainsi, l'utilisation de scanf pour la lecture de chaînes de caractères est seulement conseillée si on est forcé de lire un nombre fixé de mots en une fois. gets est idéal pour lire une ou plusieurs lignes de texte (p.ex. des phrases) terminées par un retour à la ligne. Syntaxe: gets( <Chaîne> ) gets lit une ligne de de caractères de stdin et la copie à l'adresse indiquée par <Chaîne>. Le retour à Effet: la ligne final est remplacé par le symbole de fin de chaîne '0'. Exemple : int MAXI = 1000; char LIGNE [MAXI]; gets(LIGNE);
  • 114. 8.6.2. Les fonctions de <string> La bibliothèque <string> fournit une multitude de fonctions pratiques pour le traitement de chaînes de caractères. Voici une brève description des fonctions les plus fréquemment utilisées. Dans le tableau suivant, <n> représente un nombre du type int. Les symboles <s> et <t> peuvent être remplacés par : * une chaîne de caractères constante * le nom d'une variable déclarée comme tableau de char * un pointeur sur char (voir chapitre 9) Fonctions pour le traitement de chaînes de caractères fournit la longueur de la chaîne sans strlen(<s>) compter le '0' final strcpy(<s>, <t>) copie <t> vers <s> strcat(<s>, <t>) ajoute <t> à la fin de <s> strncpy(<s>, <t>, <n>) copie au plus <n> caractères de <t> vers <s> ajoute au plus <n> caractères de <t> à la fin strncat(<s>, <t>, <n>) de <s> compare <s> et <t> lexicographiquement et fournit un résultat:négatif si <s> précède strcmp(<s>, <t>) <t> , zéro si <s> est égal à <t>, positif si <s> suit <t>. Remarques - Comme le nom d'une chaîne de caractères représente une adresse fixe en mémoire, on ne peut pas 'affecter' une autre chaîne au nom d'un tableau:
  • 115. Il faut bien copier la chaîne caractère par caractère ou utiliser la fonction strcpy respectivement strncpy: strcpy(A, "Hello"); - La concaténation de chaînes de caractères en C ne se fait pas par le symbole '+' comme en langage algorithmique ou en Pascal. Il faut ou bien copier la deuxième chaîne caractère par caractère ou bien utiliser la fonction strcat ou strncat. - La fonction strcmp est dépendante du code de caractères et peut fournir différents résultats sur différentes machines (voir 8.5.). 8.6.3. Les fonctions de <stdlib> La bibliothèque <stdlib> contient des déclarations de fonctions pour la conversion de nombres en chaînes de caractères et vice-versa. Chaîne --> Nombre Les trois fonctions définies ci-dessous correspondent au standard ANSI-C et sont portables. Le symbole <s> peut être remplacé par : q une chaîne de caractères constante q le nom d'une variable déclarée comme tableau de char q un pointeur sur char (voir chapitre 9 ) Conversion de chaînes de caractères en nombres retourne la valeur numérique représentée par <s> atoi(<s>) comme int retourne la valeur numérique représentée par <s> atol(<s>) comme long retourne la valeur numérique représentée par <s> atof(<s>) comme double (!) Règles générales pour la conversion: q Les espaces au début d'une chaîne sont ignorés q Il n'y a pas de contrôle du domaine de la cible q La conversion s'arrête au premier caractère non convertible q Pour une chaîne non convertible, les fonctions retournent zéro
  • 116. 8.6.4. Les fonctions de <ctype> Les fonctions de <ctype> servent à classifier et à convertir des caractères. Les symboles nationaux (é, è, ä, ü, ß, ç, ...) ne sont pas considérés. Les fonctions de <ctype> sont indépendantes du code de caractères de la machine et favorisent la portabilité des programmes. Dans la suite, <c> représente une valeur du type int qui peut être représentée comme caractère. Fonctions de classification et de conversion Les fonctions de classification suivantes fournissent un résultat du type int différent de zéro, si la condition respective est remplie, sinon zéro. La fonction retourne une valeur différente de zéro, isupper(<c>) si <c> est une majuscule ('A'...'Z') islower(<c>) si <c> est une minuscule ('a'...'z') isdigit(<c>) si <c> est un chiffre décimal ('0'...'9') isalpha(<c>) si islower(<c>) ou isupper(<c>) isalnum(<c>) si isalpha(<c>) ou isdigit(<c>) si <c> est un chiffre hexadécimal ('0'...'9' ou 'A'...'F' ou isxdigit(<c>) 'a'...'f') isspace(<c>) si <c> est un signe d'espacement (' ', 't', 'n', 'r', 'f') Les fonctions de conversion suivantes fournissent une valeur du type int qui peut être représentée comme caractère; la valeur originale de <c> reste inchangée: retourne <c> converti en minuscule si <c> est tolower(<c>) une majuscule retourne <c> converti en majuscule si <c> est toupper(<c>) une minuscule 8.7. Tableaux de chaînes de caractères Souvent, il est nécessaire de mémoriser une suite de mots ou de phrases dans des variables. Il est alors pratique de créer un tableau de chaînes de caractères, ce qui allégera les déclarations des variables et simplifiera l'accès
  • 117. aux différents mots (ou phrases). 8.7.1. Déclaration, initialisation et mémorisation Un tableau de chaînes de caractères correspond à un tableau à deux dimensions du type char, où chaque ligne contient une chaîne de caractères. Déclaration La déclaration char JOUR[7][9]; réserve l'espace en mémoire pour 7 mots contenant 9 caractères (dont 8 caractères significatifs). Initialisation Lors de la déclaration il est possible d'initialiser toutes les composantes du tableau par des chaînes de caractères constantes: char JOUR[7][9]= {"lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"}; Mémorisation Les tableaux de chaînes sont mémorisés ligne par ligne. La variable JOUR aura donc besoin de 7*9*1 = 63 octets en mémoire.
  • 118. 8.7.2. Accès aux différentes composantes Accès aux chaînes Il est possible d'accéder aux différentes chaînes de caractères d'un tableau, en indiquant simplement la ligne correspondante. Exemple : L'exécution des trois instructions suivantes: char JOUR[7][9]= {"lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"}; int I = 2; printf("Aujourd'hui, c'est %s !n", JOUR[I]); affichera la phrase: Aujourd'hui, c'est mercredi ! Affectation Des expressions comme JOUR[I] représentent l'adresse du premier élément d'une chaîne de caractères. N'essayez donc pas de 'modifier' une telle adresse par une affectation directe ! L'attribution d'une chaîne de caractères à une composante d'un tableau de chaînes se fait en général à l'aide de la fonction strcpy: Exemple : La commande strcpy(JOUR[4], "Friday");
  • 119. changera le contenu de la 5e composante du tableau JOUR de "vendredi" en "Friday" . Accès aux caractères Evidemment, il existe toujours la possibilité d'accéder directement aux différents caractères qui composent les mots du tableau. Exemple : L'instruction for(I=0; I<7; I++) printf("%c ", JOUR[I][0]); va afficher les premières lettres des jours de la semaine: lmmjvsd
  • 120. Chapitre 9: LES POINTEURS L'importance des pointeurs en C La plupart des langages de programmation offrent la possibilité d'accéder aux données dans la mémoire de l'ordinateur à l'aide de pointeurs, c.-à-d. à l'aide de variables auxquelles on peut attribuer les adresses d'autres variables. En C, les pointeurs jouent un rôle primordial dans la définition de fonctions: Comme le passage des paramètres en C se fait toujours par la valeur, les pointeurs sont le seul moyen de changer le contenu de variables déclarées dans d'autres fonctions. Ainsi le traitement de tableaux et de chaînes de caractères dans des fonctions serait impossible sans l'utilisation de pointeurs (voir Chapitre 10 ). En outre, les pointeurs nous permettent d'écrire des programmes plus compacts et plus efficients et fournissent souvent la seule solution raisonnable à un problème. Ainsi, la majorité des applications écrites en C profitent extensivement des pointeurs. Le revers de la médaille est très bien formulé par Kernighan & Ritchie dans leur livre 'Programming in C': " ... Les pointeurs étaient mis dans le même sac que l'instruction goto comme une excellente technique de formuler des programmes incompréhensibles. Ceci est certainement vrai si les pointeurs sont employés négligemment, et on peut facilement créer des pointeurs qui pointent 'n'importe où'. Avec une certaine discipline, les pointeurs peuvent aussi être utilisés pour programmer de façon claire et simple. C'est précisément cet aspect que nous voulons faire ressortir dans la suite. ..." Cette constatation a ensuite motivé les créateurs du standard ANSI-C à prescrire des règles explicites pour la manipulation des pointeurs.
  • 121. q 9.1. Adressage de variables r 9.1.1. Adressage direct r 9.1.2. Adressage indirect q 9.2. Les pointeurs r 9.2.1. Les opérateurs de base r 9.2.2. Les opérations élémentaires sur pointeurs q 9.3. Pointeurs et tableaux r 9.3.1. Adressage des composantes d'un tableau r 9.3.2. Arithmétique des pointeurs r 9.3.3. Pointeurs et chaînes de caractères r 9.3.4. Pointeurs et tableaux à deux dimensions q 9.4. Tableaux de pointeurs q 9.5. Allocation dynamique de mémoire r 9.5.1. Déclaration statique de données r 9.5.2. Allocation dynamique r 9.5.3. La fonction malloc et l'opérateur sizeof r 9.5.4. La fonction free 9.1. Adressage de variables Avant de parler de pointeurs, il est indiqué de brièvement passer en revue les deux modes d'adressage principaux, qui vont d'ailleurs nous accompagner tout au long des chapitres suivants. 9.1.1. Adressage direct Dans la programmation, nous utilisons des variables pour stocker des informations. La valeur d'une variable se trouve à un endroit spécifique dans la mémoire interne de l'ordinateur. Le nom de la variable nous permet alors d'accéder directement à cette valeur. Adressage direct: Accès au contenu d'une variable par le nom de la variable. Exemple :
  • 122. 9.1.2. Adressage indirect Si nous ne voulons ou ne pouvons pas utiliser le nom d'une variable A, nous pouvons copier l'adresse de cette variable dans une variable spéciale P, appelée pointeur. Ensuite, nous pouvons retrouver l'information de la variable A en passant par le pointeur P. Adressage indirect: Accès au contenu d'une variable, en passant par un pointeur qui contient l'adresse de la variable. Exemple : Soit A une variable contenant la valeur 10 et P un pointeur qui contient l'adresse de A . En mémoire, A et P peuvent se présenter comme suit: 9.2. Les pointeurs Définition: Pointeur Un pointeur est une variable spéciale qui peut contenir l'adresse d'une autre variable. En C, chaque pointeur est limité à un type de données. Il peut contenir l'adresse d'une variable simple de ce type ou l'adresse d'une composante d'un tableau de ce type. Si un pointeur P contient l'adresse d'une variable A, on dit que 'P pointe sur A '. Remarque
  • 123. Les pointeurs et les noms de variables ont le même rôle: Ils donnent accès à un emplacement dans la mémoire interne de l'ordinateur. Il faut quand même bien faire la différence: * Un pointeur est une variable qui peut 'pointer' sur différentes adresses. * Le nom d'une variable reste toujours lié à la même adresse. 9.2.1. Les opérateurs de base Lors du travail avec des pointeurs, nous avons besoin - d'un opérateur 'adresse de': & pour obtenir l'adresse d'une variable. - d'un opérateur 'contenu de': * pour accéder au contenu d'une adresse. - d'une syntaxe de déclaration pour pouvoir déclarer un pointeur. L'opérateur 'adresse de' : & &<NomVariable> fournit l'adresse de la variable <NomVariable> L'opérateur & nous est déjà familier par la fonction scanf, qui a besoin de l'adresse de ses arguments pour pouvoir leur attribuer de nouvelles valeurs. Exemple : int N; printf("Entrez un nombre entier : "); scanf("%d", &N); Attention ! L'opérateur & peut seulement être appliqué à des objets qui se trouvent dans la mémoire interne, c.-à-d. à des variables et des tableaux. Il ne peut pas être appliqué à des constantes ou des expressions. Représentation schématique
  • 124. Soit P un pointeur non initialisé et A une variable (du même type) contenant la valeur 10 : Alors l'instruction P = &A; affecte l'adresse de la variable A à la variable P. En mémoire, A et P se présentent comme dans le graphique à la fin du chapitre 9.1.2. Dans notre représentation schématique, nous pouvons illustrer le fait que 'P pointe sur A' par une flèche: L'opérateur 'contenu de' : * *<NomPointeur> désigne le contenu de l'adresse référencée par le pointeur <NomPointeur> Exemple : Soit A une variable contenant la valeur 10, B une variable contenant la valeur 50 et P un pointeur non initialisé: Après les instructions,
  • 125. P = &A; B = *P; *P = 20; - P pointe sur A , - le contenu de A (référencé par *P) est affecté à B, et - le contenu de A (référencé par *P ) est mis à 20. Déclaration d'un pointeur <Type> *<NomPointeur>déclare un pointeur <NomPointeur> qui peut recevoir des adresses de variables du type <Type> Une déclaration comme int *PNUM; peut être interprétée comme suit: "*PNUM est du type int " ou "PNUM est un pointeur sur int " ou "PNUM peut contenir l'adresse d'une variable du type int " Exemple : Le programme complet effectuant les transformations de l'exemple ci- dessus peut se présenter comme suit: main() main() { {
  • 126. /* déclarations */ /* déclarations */ short A = 10; short A, B, *P; short B = 50; /* traitement */ short *P; ou bien A = 10; /* traitement */ B = 50; P = &A; P = &A; B = *P; B = *P; *P = 20; *P = 20; return 0; return 0; } } Remarque Lors de la déclaration d'un pointeur en C, ce pointeur est lié explicitement à un type de données. Ainsi, la variable PNUM déclarée comme pointeur sur int ne peut pas recevoir l'adresse d'une variable d'un autre type que int. Nous allons voir que la limitation d'un pointeur à un type de variables n'élimine pas seulement un grand nombre de sources d'erreurs très désagréables, mais permet une série d'opérations très pratiques sur les pointeurs (voir 9.3.2.). 9.2.2. Les opérations élémentaires sur pointeurs En travaillant avec des pointeurs, nous devons observer les règles suivantes: Priorité de * et & *) Les opérateurs * et & ont la même priorité que les autres opérateurs unaires (la négation !, l'incrémentation ++, la décrémentation --). Dans une même expression, les opérateurs unaires *, &, !, ++, -- sont évalués de droite à gauche. *) Si un pointeur P pointe sur une variable X, alors *P peut être utilisé partout où on peut écrire X. Exemple Après l'instruction P = &X;
  • 127. les expressions suivantes, sont équivalentes: Y = *P+1 Y = X+1 *P = *P+10 X = X+10 *P += 2 X += 2 ++*P ++X (*P)++ X++ Dans le dernier cas, les parenthèses sont nécessaires: Comme les opérateurs unaires * et ++ sont évalués de droite à gauche, sans les parenthèses le pointeur P serait incrémenté, non pas l'objet sur lequel P pointe. On peut uniquement affecter des adresses à un pointeur. Le pointeur NUL Seule exception: La valeur numérique 0 (zéro) est utilisée pour indiquer qu'un pointeur ne pointe 'nulle part'. int *P; P = 0; Finalement, les pointeurs sont aussi des variables et peuvent être utilisés comme telles. Soit P1 et P2 deux pointeurs sur int, alors l'affectation P1 = P2; copie le contenu de P2 vers P1. P1 pointe alors sur le même objet que P2. Résumons:
  • 128. Après les instructions: int A; int *P; P = &A; A désigne le contenu de A &A désigne l'adresse de A P désigne l'adresse de A *P désigne le contenu de A En outre: &P désigne l'adresse du pointeur P *A est illégal (puisque A n'est pas un pointeur) 9.3. Pointeurs et tableaux En C, il existe une relation très étroite entre tableaux et pointeurs. Ainsi, chaque opération avec des indices de tableaux peut aussi être exprimée à l'aide de pointeurs. En général, les versions formulées avec des pointeurs sont plus compactes et plus efficientes, surtout à l'intérieur de fonctions. Mais, du moins pour des débutants, le 'formalisme pointeur' est un peu inhabituel. 9.3.1. Adressage des composantes d'un tableau Comme nous l'avons déjà constaté au chapitre 7, le nom d'un tableau représente l'adresse de son premier élément. En d'autre termes: &tableau[0] et tableau sont une seule et même adresse. En simplifiant, nous pouvons retenir que le nom d'un tableau est un pointeur constant sur le premier élément du tableau. Exemple :
  • 129. En déclarant un tableau A de type int et un pointeur P sur int, int A[10]; int *P; l'instruction: P = A; est équivalente à P = &A[0]; Si P pointe sur une composante quelconque d'un tableau, alors P+1 pointe sur la composante suivante. Plus généralement, P+i pointe sur la i-ième composante derrière P et P-i pointe sur la i-ième composante devant P. Ainsi, après l'instruction, P = A; le pointeur P pointe sur A[0], et *(P+1) désigne le contenu de A[1] *(P+2) désigne le contenu de A[2] ... ... *(P+i) désigne le contenu de A[i] Remarque Au premier coup d'oeil, il est bien surprenant que P+i n'adresse pas le i- ième octet derrière P, mais la i-ième composante derrière P ...
  • 130. Ceci s'explique par la stratégie de programmation 'défensive' des créateurs du langage C: Si on travaille avec des pointeurs, les erreurs les plus perfides sont causées par des pointeurs malplacés et des adresses mal calculées. En C, le compilateur peut calculer automatiquement l'adresse de l'élément P+i en ajoutant à P la grandeur d'une composante multipliée par i. Ceci est possible, parce que: - chaque pointeur est limité à un seul type de données, et - le compilateur connaît le nombre d'octets des différents types. Exemple : Soit A un tableau contenant des éléments du type float et P un pointeur sur float: float A[20], X; float *P; Après les instructions, P = A; X = *(P+9); Xcontient la valeur du 10-ième élément de A, (c.-à-d. celle de A[9]). Une donnée du type float ayant besoin de 4 octets, le compilateur obtient l'adresse P+9 en ajoutant 9 * 4 = 36 octets à l'adresse dans P. Rassemblons les constatations ci dessus : Comme A représente l'adresse de A[0],
  • 131. *(A+1) désigne le contenu de A[1] *(A+2) désigne le contenu de A[2] ... *(A+i) désigne le contenu de A[i] Attention ! Il existe toujours une différence essentielle entre un pointeur et le nom d'un tableau: - Un pointeur est une variable, donc des opérations comme P = A ou P++ sont permises. - Le nom d'un tableau est une constante, donc des opérations comme A = P ou A++ sont impossibles. Lors de la première phase de la compilation, toutes les expressions de la forme A[i] sont traduites en *(A+i). En multipliant l'indice i par la grandeur d'une composante, on obtient un indice en octets: <indice en octets> = <indice élément> * <grandeur élément> Cet indice est ajouté à l'adresse du premier élément du tableau pour obtenir l'adresse de la composante i du tableau. Pour le calcul d'une adresse donnée par une adresse plus un indice en octets, on utilise un mode d'adressage spécial connu sous le nom 'adressage indexé': <adresse indexée> = <adresse> + <indice en octets> Presque tous les processeurs disposent de plusieurs registres spéciaux (registres index) à l'aide desquels on peut effectuer l'adressage indexé de façon très efficace. Résumons Soit un tableau A d'un type quelconque et i un indice pour les composantes de A, alors
  • 132. A désigne l'adresse de A[0] A+i désigne l'adresse de A[i] *(A+i) désigne le contenu de A[i] Si P = A, alors P pointe sur l'élément A[0] P+i pointe sur l'élément A[i] *(P+i) désigne le contenu de A[i] Formalisme tableau et formalisme pointeur A l'aide de ce bagage, il nous est facile de 'traduire' un programme écrit à l'aide du 'formalisme tableau' dans un programme employant le 'formalisme pointeur'. Exemple : Les deux programmes suivants copient les éléments positifs d'un tableauT dans un deuxième tableau POS. Formalisme tableau main() { int T[10] = {-3, 4, 0, -7, 3, 8, 0, -1, 4, -9}; int POS[10]; int I,J; /* indices courants dans T et POS */ for (J=0,I=0 ; I<10 ; I++) if (T[I]>0) { POS[J] = T[I]; J++; } return 0;
  • 133. } Nous pouvons remplacer systématiquement la notation tableau[I] par * (tableau + I), ce qui conduit à ce programme: Formalisme pointeur main() { int T[10] = {-3, 4, 0, -7, 3, 8, 0, -1, 4, -9}; int POS[10]; int I,J; /* indices courants dans T et POS */ for (J=0,I=0 ; I<10 ; I++) if (*(T+I)>0) { *(POS+J) = *(T+I); J++; } return 0; } Sources d'erreurs Un bon nombre d'erreurs lors de l'utilisation de C provient de la confusion entre soit contenu et adresse, soit pointeur et variable. Revoyons donc les trois types de déclarations que nous connaissons jusqu'ici et résumons les possibilités d'accès aux données qui se présentent. Les variables et leur utilisation int A;déclare une variable simple du type int *A désigne le contenu de A &A désigne l'adresse de A
  • 134. int B[ ];déclare un tableau d'éléments du type int désigne l'adresse de la première composante de B (Cette B adresse est toujours constante). B[i] désigne le contenu de la composante i du tableau &B[i] désigne l'adresse de la composante i du tableau en utilisant le formalisme pointeur: B+i désigne l'adresse de la composante i du tableau *(B+i) désigne le contenu de la composante i du tableau int *P;déclare un pointeur sur des éléments du type int. Peut pointersur des variables simples du type int ou sur les P composantes d'un tableau du type int. désigne l'adresse contenue dans P (Cette adresse est P variable) *P désigne le contenu de l'adresse dans P Si P pointe dans un tableau, alors P désigne l'adresse de la première composante P+i désigne l'adresse de la i-ième composante derrière P *(P+i) désigne le contenu de la i-ième composante derrière P 9.3.2. Arithmétique des pointeurs Comme les pointeurs jouent un rôle si important, le langage C soutient une série d'opérations arithmétiques sur les pointeurs que l'on ne rencontre en
  • 135. général que dans les langages machines. Le confort de ces opérations en C est basé sur le principe suivant: Toutes les opérations avec les pointeurs tiennent compte automatiquement du type et de la grandeur des objets pointés. - Affectation par un pointeur sur le même type Soient P1 et P2 deux pointeurs sur le même type de données, alors l'instruction P1 = P2; fait pointer P1 sur le même objet que P2 - Addition et soustraction d'un nombre entier Si P pointe sur l'élément A[i] d'un tableau, alors P+n pointe sur A[i+n] P-n pointe sur A[i-n] - Incrémentation et décrémentation d'un pointeur Si P pointe sur l'élément A[i] d'un tableau, alors après l'instruction P++; P pointe sur A[i+1] P+=n; P pointe sur A[i+n] P--; P pointe sur A[i-1] P-=n; P pointe sur A[i-n] Domaine des opérations L'addition, la soustraction, l'incrémentation et la décrémentation sur les pointeurs sont seulement définies à l'intérieur d'un tableau. Si l'adresse formée par le pointeur et l'indice sort du domaine du tableau, alors le résultat n'est pas défini. Seule exception: Il est permis de 'pointer' sur le premier octet derrière un
  • 136. tableau (à condition que cet octet se trouve dans le même segment de mémoire que le tableau). Cette règle, introduite avec le standard ANSI-C, légalise la définition de boucles qui incrémentent le pointeur avant l'évaluation de la condition d'arrêt. Exemples : int A [10]; int *P; P = A+9; /* dernier élément -> légal */ P=A /* dernier élément + 1 -> légal */ +10; P=A /* dernier élément + 2 -> illégal +11; */ /* premier élément - 1 -> illégal P = A-1; */ - Soustraction de deux pointeurs Soient P1 et P2 deux pointeurs qui pointent dans le même tableau: fournit le nombre de composantes comprises entre P1 et P1-P2 P2. Le résultat de la soustraction P1-P2 est négatif si P1 précède P2 zéro si P1 = P2 positif si P2 precède P1 indéfini si P1 et P2 ne pointent pas dans le même tableau Plus généralement, la soustraction de deux pointeurs qui pointent dans le même tableau est équivalente à la soustraction des indices correspondants.
  • 137. - Comparaison de deux pointeurs On peut comparer deux pointeurs par <, >, <=, >=, ==, !=. La comparaison de deux pointeurs qui pointent dans le même tableau est équivalente à la comparaison des indices correspondants. (Si les pointeurs ne pointent pas dans le même tableau, alors le résultat est donné par leurs positions relatives dans la mémoire). 9.3.3. Pointeurs et chaînes de caractères De la même façon qu'un pointeur sur int peut contenir l'adresse d'un nombre isolé ou d'une composante d'un tableau, un pointeur sur char peut pointer sur un caractère isolé ou sur les éléments d'un tableau de caractères. Un pointeur sur char peut en plus contenir l'adresse d'une chaîne de caractères constante et il peut même être initialisé avec une telle adresse. A la fin de ce chapitre, nous allons anticiper avec un exemple et montrer que les pointeurs sont les éléments indispensables mais effectifs des fonctions en C. - Pointeurs sur char et chaînes de caractères constantes a) Affectation On peut attribuer l'adresse d'une chaîne de caractères constante à un pointeur sur char: Exemple : char *C; C = "Ceci est une chaîne de caractères constante";
  • 138. Nous pouvons lire cette chaîne constante (p.ex: pour l'afficher), mais il n'est pas recommandé de la modifier, parce que le résultat d'un programme qui essaie de modifier une chaîne de caractères constante n'est pas prévisible en ANSI-C. b) Initialisation Un pointeur sur char peut être initialisé lors de la déclaration si on lui affecte l'adresse d'une chaîne de caractères constante: char *B = "Bonjour !"; Attention ! Il existe une différence importante entre les deux déclarations: char A[ ] = "Bonjour !"; /* un tableau */ char *B = "Bonjour !"; /* un pointeur */ A est un tableau qui a exactement la grandeur pour contenir la chaîne de caractères et la terminaison '0'. Les caractères de la chaîne peuvent être changés, mais le nom A va toujours pointer sur la même adresse en mémoire. B est un pointeur qui est initialisé de façon à ce qu'il pointe sur une chaîne de caractères constante stockée quelque part en mémoire. Le pointeur peut être modifié et pointer sur autre chose. La chaîne constante peut être lue, copiée ou affichée, mais pas modifiée. c) Modification Si nous affectons une nouvelle valeur à un pointeur sur une chaîne de caractères constante, nous risquons de perdre la chaîne constante. D'autre part, un pointeur sur char a l'avantage de pouvoir pointer sur des chaînes de
  • 139. n'importe quelle longueur: Exemple : char *A = "Petite chaîne"; char *B = "Deuxième chaîne un peu plus longue"; A = B; Maintenant A et B pointent sur la même chaîne; la "Petite chaîne" est perdue: Attention ! Les affectations discutées ci-dessus ne peuvent pas être effectuées avec des tableaux de caractères: Exemple : char A[45] = "Petite chaîne"; char B[45] = "Deuxième chaîne un peu plus longue"; char C[30]; A = B; /* IMPOSSIBLE -> ERREUR !!! */ C = "Bonjour !"; /* IMPOSSIBLE -> ERREUR !!! */
  • 140. Dans cet exemple, nous essayons de copier l'adresse de B dans A, respectivement l'adresse de la chaîne constante dans C. Ces opérations sont impossibles et illégales parce que l'adresse représentée par le nom d'un tableau reste toujours constante. Pour changer le contenu d'un tableau, nous devons changer les composantes du tableau l'une après l'autre (p.ex. dans une boucle) ou déléguer cette charge à une fonction de <stdio> ou <string>. Conclusions: q Utilisons des tableaux de caractères pour déclarer les chaînes de caractères que nous voulons modifier. q Utilisons des pointeurs sur char pour manipuler des chaînes de caractères constantes (dont le contenu ne change pas). q Utilisons de préférence des pointeurs pour effectuer les manipulations à l'intérieur des tableaux de caractères. (voir aussi les remarques ci- dessous). Perspectives et motivation - Avantages des pointeurs sur char Comme la fin des chaînes de caractères est marquée par un symbole spécial, nous n'avons pas besoin de connaître la longueur des chaînes de caractères; nous pouvons même laisser de côté les indices d'aide et parcourir les chaînes à l'aide de pointeurs. Cette façon de procéder est indispensable pour traiter de chaînes de caractères dans des fonctions. En anticipant sur la matière du chapitre 10, nous pouvons ouvrir une petite parenthèse pour illustrer les avantages des pointeurs dans la définition de fonctions traitant des chaînes de caractères: Pour fournir un tableau comme paramètre à une fonction, il faut passer l'adresse du tableau à la fonction. Or, les paramètres des fonctions sont des variables locales, que nous pouvons utiliser comme variables d'aide. Bref, une fonction obtenant une chaîne de caractères comme paramètre, dispose
  • 141. d'une copie locale de l'adresse de la chaîne. Cette copie peut remplacer les indices ou les variables d'aide du formalisme tableau. Discussion d'un exemple Reprenons l'exemple de la fonction strcpy, qui copie la chaîne CH2 vers CH1. Les deux chaînes sont les arguments de la fonction et elles sont déclarées comme pointeurs sur char. La première version de strcpy est écrite entièrement à l'aide du formalisme tableau: void strcpy(char *CH1, char *CH2) { int I; I=0; while ((CH1[I]=CH2[I]) != '0') I++; } Dans une première approche, nous pourrions remplacer simplement la notation tableau[I] par *(tableau + I), ce qui conduirait au programme: void strcpy(char *CH1, char *CH2) { int I; I=0; while ((*(CH1+I)=*(CH2+I)) != '0') I++; } Cette transformation ne nous avance guère, nous avons tout au plus gagné quelques millièmes de secondes lors de la compilation. Un 'véritable' avantage se laisse gagner en calculant directement avec les pointeurs CH1 et CH2 : void strcpy(char *CH1, char *CH2) { while ((*CH1=*CH2) != '0') { CH1++; CH2++; } }
  • 142. Comme nous l'avons déjà constaté dans l'introduction de ce manuel, un vrai professionnel en C escaladerait les 'simplifications' jusqu'à obtenir: void strcpy(char *CH1, char *CH2) { while (*CH1++ = *CH2++) ; } Assez 'optimisé' - fermons la parenthèse et familiarisons-nous avec les notations et les manipulations du 'formalisme pointeur' ... 9.3.4. Pointeurs et tableaux à deux dimensions L'arithmétique des pointeurs se laisse élargir avec toutes ses conséquences sur les tableaux à deux dimensions. Voyons cela sur un exemple: Exemple : Le tableauM à deux dimensions est défini comme suit: int M[4][10] = {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, {10,11,12,13,14,15,16,17,18,19}, {20,21,22,23,24,25,26,27,28,29}, {30,31,32,33,34,35,36,37,38,39}}; Le nom du tableau M représente l'adresse du premier élément du tableau et pointe (oh, surprise...) sur le tableau M[0] qui a la valeur: {0,1,2,3,4,5,6,7,8,9}. L'expression (M+1) est l'adresse du deuxième élément du tableau et pointe sur M[1] qui a la valeur: {10,11,12,13,14,15,16,17,18,19}. Explication Au sens strict du terme, un tableau à deux dimensions est un tableau unidimensionnel dont chaque composante est un tableau unidimensionnel. Ainsi, le premier élément de la matrice M est le vecteur {0,1,2,3,4,5,6,7,8,9}, le deuxième élément est {10,11,12,13,14,15,16,17,18,19} et ainsi de suite. L'arithmétique des pointeurs qui respecte automatiquement les dimensions des éléments conclut logiquement que: M+I désigne l'adresse du tableau M[I]
  • 143. Problème Comment pouvons-nous accéder à l'aide de pointeurs aux éléments de chaque composante du tableau, c.à-d.: aux éléments M[0][0], M[0][1], ... , M[3] [9] ? Discussion Une solution consiste à convertir la valeur de M (qui est un pointeur sur un tableau du type int) en un pointeur sur int. On pourrait se contenter de procéder ainsi: int M[4][10] = {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, {10,11,12,13,14,15,16,17,18,19}, {20,21,22,23,24,25,26,27,28,29}, {30,31,32,33,34,35,36,37,38,39}}; int *P; P = M; /* conversion automatique */ Cette dernière affectation entraîne une conversion automatique de l'adresse &M[0] dans l'adresse &M[0][0]. (Remarquez bien que l'adresse transmise reste la même, seule la nature du pointeur a changé). Cette solution n'est pas satisfaisante à cent pour-cent: Généralement, on gagne en lisibilité en explicitant la conversion mise en oeuvre par l'opérateur de conversion forcée ("cast"), qui évite en plus des messages d'avertissement de la part du compilateur. Solution Voici finalement la version que nous utiliserons:
  • 144. int M[4][10] = {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, {10,11,12,13,14,15,16,17,18,19}, {20,21,22,23,24,25,26,27,28,29}, {30,31,32,33,34,35,36,37,38,39}}; int *P; P = (int *)M; /* conversion forcée */ Dû à la mémorisation ligne par ligne des tableaux à deux dimensions, il nous est maintenant possible de traiter M à l'aide du pointeur P comme un tableau unidimensionnel de dimension 4*10. Exemple : Les instructions suivantes calculent la somme de tous les éléments du tableau M: int M[4][10] = {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, {10,11,12,13,14,15,16,17,18,19}, {20,21,22,23,24,25,26,27,28,29}, {30,31,32,33,34,35,36,37,38,39}}; int *P; int I, SOM; P = (int*)M; SOM = 0; for (I=0; I<40; I++) SOM += *(P+I); Attention ! Lors de l'interprétation d'un tableau à deux dimensions comme tableau unidimensionnel il faut calculer avec le nombre de colonnes indiqué dans la déclaration du tableau. Exemple : Pour la matrice A, nous réservons de la mémoire pour 3 lignes et 4 colonnes, mais nous utilisons seulement 2 lignes et 2 colonnes:
  • 145. int A[3][4]; A[0][0]=1; A[0][1]=2; A[1][0]=10; A[1][1]=20; Dans la mémoire, ces composantes sont stockées comme suit : L'adresse de l'élément A[I][J] se calcule alors par: A + I*4 + J Conclusion Pour pouvoir travailler à l'aide de pointeurs dans un tableau à deux dimensions, nous avons besoin de quatre données: a) l'adresse du premier élément du tableau converti dans le type simple des éléments du tableau b) la longueur d'une ligne réservée en mémoire (- voir déclaration - ici: 4 colonnes) c) le nombre d'éléments effectivement utilisés dans une ligne (- p.ex: lu au clavier - ici: 2 colonnes) d) le nombre de lignes effectivement utilisées (- p.ex: lu au clavier - ici: 2 lignes) 9.4. Tableaux de pointeurs Si nous avons besoin d'un ensemble de pointeurs du même type, nous pouvons les réunir dans un tableau de pointeurs. Déclaration d'un tableau de pointeurs
  • 146. <Type> *<NomTableau>[<N>]déclare un tableau <NomTableau> de <N> pointeurs sur des données du type <Type>. Exemple : double *A[10]; déclare un tableau de 10 pointeurs sur des rationnels du type double dont les adresses et les valeurs ne sont pas encore définies. Remarque Le plus souvent, les tableaux de pointeurs sont utilisés pour mémoriser de façon économique des chaînes de caractères de différentes longueurs. Dans la suite, nous allons surtout considérer les tableaux de pointeurs sur des chaînes de caractères. Initialisation Nous pouvons initialiser les pointeurs d'un tableau sur char par les adresses de chaînes de caractères constantes. Exemple char *JOUR[] = {"dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"}; déclare un tableau JOUR[] de 7 pointeurs sur char. Chacun des pointeurs est initialisé avec l'adresse de l'une des 7 chaînes de caractères.
  • 147. On peut afficher les 7 chaînes de caractères en fournissant les adresses contenues dans le tableau JOUR à printf (ou puts) : int I; for (I=0; I<7; I++) printf("%sn", JOUR[I]); Comme JOUR[I] est un pointeur sur char, on peut afficher les premières lettres des jours de la semaine en utilisant l'opérateur 'contenu de' : int I; for (I=0; I<7; I++) printf("%cn", *JOUR[I]); L'expression JOUR[I]+J désigne la J-ième lettre de la I-ième chaîne. On peut afficher la troisième lettre de chaque jour de la semaine par: int I; for (I=0; i<7; I++) printf("%cn",*(JOUR[I]+2)); Résumons : Les tableaux de pointeurs int *D[ ]; déclare un tableau de pointeurs sur des éléments du type int peut pointer sur des variables simples ou sur les D[i] composantes d'un tableau.
  • 148. désigne l'adresse contenue dans l'élément i de D (Les D[i] adresses dans D[i] sont variables) *D[i] désigne le contenu de l'adresse dans D[i] Si D[i] pointe dans un tableau, D[i] désigne l'adresse de la première composante D[i]+j désigne l'adresse de la j-ième composante *(D[i]+j) désigne le contenu de la j-ième composante 9.5. Allocation dynamique de mémoire Nous avons vu que l'utilisation de pointeurs nous permet de mémoriser économiquement des données de différentes grandeurs. Si nous générons ces données pendant l'exécution du programme, il nous faut des moyens pour réserver et libérer de la mémoire au fur et à mesure que nous en avons besoin. Nous parlons alors de l'allocation dynamique de la mémoire. Revoyons d'abord de quelle façon la mémoire a été réservée dans les programmes que nous avons écrits jusqu'ici. 9.5.1. Déclaration statique de données Chaque variable dans un programme a besoin d'un certain nombre d'octets en mémoire. Jusqu'ici, la réservation de la mémoire s'est déroulée automatiquement par l'emploi des déclarations des données. Dans tous ces cas, le nombre d'octets à réserver était déjà connu pendant la compilation. Nous parlons alors de la déclaration statique des variables. Exemples :
  • 149. float A, B, C; /* réservation de 12 octets */ short D[10][20]; /* réservation de 400 octets */ char E[ ] = {"Bonjour !"}; /* réservation de 10 octets */ char F[ ][10] = {"un", "deux", "trois", "quatre"}; /* réservation de 40 octets */ Pointeurs Le nombre d'octets à réserver pour un pointeur dépend de la machine et du 'modèle' de mémoire choisi, mais il est déjà connu lors de la compilation. Un pointeur est donc aussi déclaré statiquement. Supposons dans la suite qu'un pointeur ait besoin de p octets en mémoire. (En DOS: p =2 ou p = 4) Exemples : double *G; /* réservation de p octets */ char *H; /* réservation de p octets */ float *K[10]; /* réservation de 10*p octets */ Chaînes de caractères constantes L'espace pour les chaînes de caractères constantes qui sont affectées à des pointeurs ou utilisées pour initialiser des pointeurs sur char est aussi réservé automatiquement: Exemples char *J = "Bonjour !"; /* réservation de p+10 octets */ float *K[ ] = {"un", "deux", "trois", "quatre"}; /* réservation de 4*p+3+5+6+7 octets */ 9.5.2. Allocation dynamique Problème Souvent, nous devons travailler avec des données dont nous ne pouvons pas prévoir le nombre et la grandeur lors de la programmation. Ce serait alors un gaspillage de réserver toujours l'espace maximal prévisible. Il nous
  • 150. faut donc un moyen de gérer la mémoire lors de l'exécution du programme. Exemple : Nous voulons lire 10 phrases au clavier et mémoriser les phrases en utilisant un tableau de pointeurs sur char. Nous déclarons ce tableau de pointeurs par: char *TEXTE[10]; Pour les 10 pointeurs, nous avons besoin de 10*p octets. Ce nombre est connu dès le départ et les octets sont réservés automatiquement. Il nous est cependant impossible de prévoir à l'avance le nombre d'octets à réserver pour les phrases elles-mêmes qui seront introduites lors de l'exécution du programme ... Allocation dynamique La réservation de la mémoire pour les 10 phrases peut donc seulement se faire pendant l'exécution du programme. Nous parlons dans ce cas de l'allocation dynamique de la mémoire. 9.5.3. La fonction malloc et l'opérateur sizeof La fonction malloc de la bibliothèque <stdlib> nous aide à localiser et à réserver de la mémoire au cours d'un programme. Elle nous donne accès au tas (heap); c.-à-d. à l'espace en mémoire laissé libre une fois mis en place le DOS, les gestionnaires, les programmes résidents, le programme lui-même et la pile (stack). La fonction malloc malloc( <N> ) fournit l'adresse d'un bloc en mémoire de <N> octets libres ou la valeur zéro s'il n'y a pas assez de mémoire. Attention ! Sur notre système, le paramètre <N> est du type unsigned int. A l'aide de malloc, nous ne pouvons donc pas réserver plus de 65535 octets à la fois! Exemple :
  • 151. Supposons que nous ayons besoin d'un bloc en mémoire pour un texte de 4000 caractères. Nous disposons d'un pointeur T sur char (char *T). Alors l'instruction: T = malloc(4000); fournit l'adresse d'un bloc de 4000 octets libres et l'affecte àT. S'il n'y a plus assez de mémoire, T obtient la valeur zéro. Si nous voulons réserver de la mémoire pour des données d'un type dont la grandeur varie d'une machine à l'autre, nous avons besoin de la grandeur effective d'une donnée de ce type. L'opérateur sizeof nous aide alors à préserver la portabilité du programme. L'opérateur unaire sizeof sizeof <var> fournit la grandeur de la variable <var> sizeof <const> fournit la grandeur de la constante <const> sizeof (<type>) fournit la grandeur pour un objet du type <type> Exemple : Après la déclaration, short A[10]; char B[5][10]; nous obtenons les résultats suivants sur un IBM-PC (ou compatible): sizeof A s'évalue à 20 sizeof B s'évalue à 50 sizeof 4.25 s'évalue à 8 sizeof "Bonjour !" s'évalue à 10 sizeof(float) s'évalue à 4
  • 152. sizeof(double) s'évalue à 8 Exemple : Nous voulons réserver de la mémoire pourX valeurs du type int; la valeur de X est lue au clavier: int X; int *PNum; printf("Introduire le nombre de valeurs :"); scanf("%d", &X); PNum = malloc(X*sizeof(int)); exit S'il n'y a pas assez de mémoire pour effectuer une action avec succès, il est conseillé d'interrompre l'exécution du programme à l'aide de la commande exit (de <stdlib>) et de renvoyer une valeur différente de zéro comme code d'erreur du programme (voir aussi chapitre 10.4). Exemple : Le programme à la page suivante lit 10 phrases au clavier, recherche des blocs de mémoire libres assez grands pour la mémorisation et passe les adresses aux composantes du tableauTEXTE[ ]. S'il n'y a pas assez de mémoire pour une chaîne, le programme affiche un message d'erreur et interrompt le programme avec le code d'erreur -1. Nous devons utiliser une variable d'aide INTRO comme zone intermédiaire (non dynamique). Pour cette raison, la longueur maximale d'une phrase est fixée à 500 caractères.
  • 153. #include <stdio.h> #include <stdlib.h> #include <string.h> main() { /* Déclarations */ char INTRO[500]; char *TEXTE[10]; int I; /* Traitement */ for (I=0; I<10; I++) { gets(INTRO); /* Réservation de la mémoire */ TEXTE[I] = malloc(strlen(INTRO)+1); /* S'il y a assez de mémoire, ... */ if (TEXTE[I]) /* copier la phrase à l'adresse */ /* fournie par malloc, ... */ strcpy(TEXTE[I], INTRO); else { /* sinon quitter le programme */ /* après un message d'erreur. */ printf("ERREUR: Pas assez de mémoire n"); exit(-1); } } return 0; } 9.5.4. La fonction free Si nous n'avons plus besoin d'un bloc de mémoire que nous avons réservé à l'aide de malloc, alors nous pouvons le libérer à l'aide de la fonction free de la bibliothèque <stdlib>. free( <Pointeur> )libère le bloc de mémoire désigné par le <Pointeur>; n'a pas d'effet si le pointeur a la valeur zéro. Attention !
  • 154. * 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. * La fonction free ne change pas le contenu du pointeur; il est conseillé d'affecter la valeur zéro au pointeur immédiatement après avoir libéré le bloc de mémoire qui y était attaché. * Si nous ne libérons pas explicitement la mémoire à l'aide free, alors elle est libérée automatiquement à la fin du programme.
  • 155. Chapitre 10: LES FONCTIONS La structuration de programmes en sous-programmes se fait en C à l'aide de fonctions. Les fonctions en C correspondent aux fonctions et procédures en Pascal et en langage algorithmique. Nous avons déjà utilisé des fonctions prédéfinies dans des bibliothèques standard (printf de <stdio>, strlen de <string>, pow de <math>, etc.). Dans ce chapitre, nous allons découvrir comment nous pouvons définir et utiliser nos propres fonctions. q 10.1. Modularisation de programmes r 10.1.1. La modularité et ses avantages r 10.1.2. Exemples de modularisation en C q 10.2. La notion de blocs et la portée des identificateurs r 10.2.1. Variables locales r 10.2.2. Variables globales q 10.3. Déclaration et définition de fonctions r 10.3.1. Définition d'une fonction r 10.3.2. Déclaration d'une fonction r 10.3.3. Discussion d'un exemple q 10.4. Renvoyer un résultat q 10.5. Paramètres d'une fonction r 10.5.1. Généralités r 10.5.2. Passage des paramètres par valeur r 10.5.3. Passage de l'adresse d'une variable r 10.5.4. Passage de l'adresse d'un tableau à une dimension r 10.5.5. Passage de l'adresse d'un tableau à deux dimensions q 10.6. Les modules en lang. algorithmique, en Pascal et en C q 10.7. Discussion de deux problèmes 10.1. Modularisation de programmes Jusqu'ici, nous avons résolu nos problèmes à l'aide de fonctions prédéfinies et d'une seule fonction nouvelle: la fonction principale main(). Pour des problèmes plus complexes, nous obtenons ainsi de longues listes d'instructions, peu structurées et par conséquent peu compréhensibles. En plus, il faut souvent répéter les mêmes suites de commandes dans le texte du programme, ce qui entraîne un gaspillage de mémoire interne et externe.
  • 156. 10.1.1. La modularité et ses avantages La plupart des langages de programmation nous permettent de subdiviser nos programmes en sous-programmes, fonctions ou procédures plus simples et plus compacts. A l'aide de ces structures nous pouvons modulariser nos programmes pour obtenir des solutions plus élégantes et plus efficientes. Modules Dans ce contexte, un module désigne une entité de données et d'instructions qui fournissent une solution à une (petite) partie bien définie d'un problème plus complexe. Un module peut faire appel à d'autres modules, leur transmettre des données et recevoir des données en retour. L'ensemble des modules ainsi reliés doit alors être capable de résoudre le problème global. Avantages Voici quelques avantages d'un programme modulaire: * Meilleure lisibilité * Diminution du risque d'erreurs * Possibilité de tests sélectifs * Dissimulation des méthodes Lors de l'utilisation d'un module il faut seulement connaître son effet, sans devoir s'occuper des détails de sa réalisation. * Réutilisation de modules déjà existants Il est facile d'utiliser des modules qu'on a écris soi-même ou qui ont été développés par d'autres personnes. * Simplicité de l'entretien Un module peut être changé ou remplacé sans devoir toucher aux autres modules du programme. * Favorisation du travail en équipe Un programme peut être développé en équipe par délégation de la programmation des modules à différentes personnes ou groupes de personnes. Une fois développés, les modules peuvent constituer une base de travail commune. 10.1.2. Exemples de modularisation en C Les deux programmes présentés ci-dessous vous donnent un petit aperçu sur les propriétés principales des fonctions en C. Les détails seront discutés plus
  • 157. loin dans ce chapitre. a) Exemple 1: Afficher un rectangle d'étoiles Commençons par un petit programme que nous vous proposons d'examiner vous-mêmes sans autres explications: Le programme suivant permet d'afficher à l'écran un rectangle de longueur L et de hauteur H, formé d'astérisques '*' : Implémentation en C #include <stdio.h> main() { /* Prototypes des fonctions appelées par main */ void RECTANGLE(int L, int H); /* Déclaration des variables locales de main */ int L, H; /* Traitements */ printf("Entrer la longueur (>= 1): "); scanf("%d", &L); printf("Entrer la hauteur (>= 1): "); scanf("%d", &H); /* Afficher un rectangle d'étoiles */ RECTANGLE(L,H); return 0; } Pour que la fonction soit exécutable par la machine, il faut encore spécifier la fonction RECTANGLE:
  • 158. void RECTANGLE(int L, int H) { /* Prototypes des fonctions appelées */ void LIGNE(int L); /* Déclaration des variables locales */ int I; /* Traitements */ /* Afficher H lignes avec L étoiles */ for (I=0; I<H; I++) LIGNE(L); } Pour que la fonction RECTANGLE soit exécutable par la machine, il faut spécifier la fonction LIGNE: void LIGNE(int L) { /* Affiche à l'écran une ligne avec L étoiles */ /* Déclaration des variables locales */ int I; /* Traitements */ for (I=0; I<L; I++) printf("*"); printf("n"); } Schématiquement, nous pouvons représenter la hiérarchie des fonctions du programme comme suit: b) Exemple 2: Tableau de valeurs d'une fonction
  • 159. Soit F la fonction numérique définie par F(X) = X3-2X+1. On désire construire un tableau de valeurs de cette fonction. Le nombre N de valeurs ainsi que les valeurs de X sont entrés au clavier par l'utilisateur. Exemple Entrez un entier entre 1 et 100 : 9 Entrez 9 nombres réels : -4 -3 -2 -1 0 1 2 3 4 X -4.0 -3.0 -2.0 -1.0 0.0 1.0 2.0 3.0 4.0 F(X) -55.0 -20.0 -3.0 2.0 1.0 0.0 5.0 22.0 57.0 En modularisant ce problème, nous obtenons un programme principal très court et bien 'lisible'. La fonction main joue le rôle du programme principal: main() { float X[100]; /* valeurs de X */ float V[100]; /* valeurs de F(X) */ int N; ACQUERIR(&N); /* 1 <= N <= 100 */ LIRE_VECTEUR(X, N); CALCULER_VALEURS(X, V, N); AFFICHER_TABLE(X, V, N); return 0; } Pour que la machine puisse exécuter ce programme, il faut encore implémenter les modules ACQUERIR, LIRE_VECTEUR, CALCULER_VALEURS et AFFICHER_TABLE. Ces spécifications se font en C sous forme de fonctions qui remplacent les fonctions et les procédures que nous connaissons en langage algorithmique. Une 'procédure' est réalisée en C par une fonction qui fournit le résultat void (vide). Les fonctions sont ajoutées dans le texte du programme au- dessus ou en-dessous de la fonction main. Si dans le texte du programme une fonction est défine après la fonction appelante, il faut la déclarer ou bien localement à l'intérieur de la fonction appelante ou bien globalement au début du programme. La déclaration d'une fonction se fait à l'aide d'un 'prototype' de la fonction qui correspond en général à la première ligne (la ligne déclarative) de la fonction. Par convention, nous allons définir la fonction main en premier lieu. Ainsi nous obtenons le programme suivant:
  • 160. Implémentation en C #include <stdio.h> main() { /* Prototypes des fonctions appelées par main */ void ACQUERIR(int *N); void LIRE_VECTEUR(float T[ ], int N); void CALCULER_VALEURS(float X[ ], float V[ ], int N); void AFFICHER_TABLE(float X[ ], float V[ ], int N); /* Déclaration des variables locales de main */ float X[100]; /* valeurs de X */ float V[100]; /* valeurs de F(X) */ int N; /* Traitements */ ACQUERIR(&N); /* 1 <= N <= 100 */ LIRE_VECTEUR(X, N); CALCULER_VALEURS(X, V, N); AFFICHER_TABLE(X, V, N); return 0; } void ACQUERIR(int *N) { do { printf("Entrez un entier entre 1 et 100 : "); scanf("%d", N); } while (*N<1 || *N>100); } void LIRE_VECTEUR(float T[ ], int N) { /* Remplit un tableau T d'ordre N avec des nombres réels entrés au clavier */ /* Déclaration des variables locales */ int I; /* Remplir le tableau */ printf("Entrez %d nombres réels :n", N); for (I=0; I<N; I++) scanf("%f", &T[I]); }
  • 161. void CALCULER_VALEURS(float X[ ], float V[ ], int N) { /* Remplit le tableau V avec les valeurs de */ /* F(X[I]) pour les N premières composantes */ /* X[I] du tableau X */ /* Prototype de la fonction F */ float F(float X); /* Déclaration des variables locales */ int I; /* Calculer les N valeurs */ for (I=0; I<N; I++) V[I] = F(X[I]); } float F(float X) { /* Retourne la valeur numérique du polynôme défini par F(X) = X^3-2X+1 */ return (X*X*X - 2*X + 1); } void AFFICHER_TABLE(float X[ ], float V[ ], int N) { /* Affiche une table de N valeurs : X contient les valeurs données et V contient les valeurs calculées. */ /* Déclaration des variables locales */ int I; /* Afficher le tableau */ printf("n X : "); for (I=0; I<N; I++) printf("%.1f", X[I]); printf("n F(X): "); for (I=0; I<N; I++) printf("%.1f", V[I]); printf("n"); } Le programme est composé de six fonctions dont quatre ne fournissent pas de résultat. La fonction F retourne la valeur de F(X) comme résultat. Le résultat de F est donc du type float; nous disons alors que 'F est du type float' ou 'F a le type float'.
  • 162. Les fonctions fournissent leurs résultats à l'aide de la commande return. La valeur rendue à l'aide de return doit correspondre au type de la fonction, sinon elle est automatiquement convertie dans ce type. A la fin de l'exécution du programme, la fonction main fournit par défaut une valeur comme code d'erreur à l'environnement. Le retour de la valeur zéro veut dire que le programme s'est terminé normalement et sans erreurs fatales. Le passage des paramètres en C se fait toujours par la valeur. Pour pouvoir modifier une variable déclarée dans la procédure appelante, la fonction appelée a besoin de l'adresse de cette variable. Le paramètre correspondant doit donc être un pointeur et lors d'un appel de la fonction, il faut veiller à envoyer l'adresse et non la valeur de la variable. Dans notre exemple, la fonction ACQUERIR a besoin de l'adresse de la variable N pour pouvoir affecter une nouvelle valeur à N. Le paramètre N doit donc être défini comme pointeur (sur int). Lors de l'appel, il faut transmettre l'adresse de N par &N. A l'intérieur de la fonction il faut utiliser l'opérateur 'contenu de' pour accéder à la valeur de N. Les autres fonctions ne changent pas le contenu de N et ont seulement besoin de sa valeur. Dans les en-têtes de ces fonctions, N est simplement déclaré comme int. Lorsque nous passons un tableau comme paramètre à une fonction, il ne faut pas utiliser l'opérateur adresse & lors de l'appel, parce que le nom du tableau représente déjà l'adresse du tableau. Dans notre exemple, la fonction LIRE_VECTEUR modifie le contenu de la variable X, mais lors de l'appel, il suffit d'envoyer le nom du tableau comme paramètre. Schématiquement, nous pouvons représenter la hiérarchie des fonctions comme suit:
  • 163. 10.2. La notion de blocs et la portée des identificateurs Les fonctions en C sont définies à l'aide de blocs d'instructions. Un bloc d'instructions est encadré d'accolades et composé de deux parties: Blocs d'instructions en C { <déclarations locales> <instructions> } Par opposition à d'autres langages de programmation, ceci est vrai pour tous les blocs d'instructions, non seulement pour les blocs qui renferment une fonction. Ainsi, le bloc d'instructions d'une commande if, while ou for peut théoriquement contenir des déclarations locales de variables et même de fonctions. Exemple : La variable d'aide I est déclarée à l'intérieur d'un bloc conditionnel. Si la condition (N>0) n'est pas remplie, I n'est pas défini. A la fin du bloc conditionnel, I disparaît.
  • 164. if (N>0) { int I; for (I=0; I<N; I++) ... } 10.2.1. Variables locales Les variables déclarées dans un bloc d'instructions sont uniquement visibles à l'intérieur de ce bloc. On dit que ce sont des variables locales à ce bloc. Exemple : La variableNOM est définie localement dans le bloc intérieur de la fonction HELLO. Ainsi, aucune autre fonction n'a accès à la variable NOM: void HELLO(void); { char NOM[20]; printf("Introduisez votre nom : "); gets(NOM); printf("Bonjour %s !n", NOM); } Exemple : La déclaration de la variable I se trouve à l'intérieur d'un bloc d'instructions conditionnel. Elle n'est pas visible à l'extérieur de ce bloc, ni même dans la fonction qui l'entoure. if (N>0) { int I; for (I=0; I<N; I++) ... }
  • 165. Attention ! Une variable déclarée à l'intérieur d'un bloc cache toutes les variables du même nom des blocs qui l'entourent. Exemple : Dans la fonction suivante, int FONCTION(int A) { int X; ... X = 100; ... while (A>10) { double X; ... X *= A; ... } } la première instruction X=100 se rapporte à la variable du type int déclarée dans le bloc extérieur de la fonction; l'instruction X*=A agit sur la variable du type double déclarée dans la boucle while. A l'intérieur de la boucle, il est impossible d'accéder à la variable X du bloc extérieur. Ce n'est pas du bon style d'utiliser des noms de variables qui cachent des variables déclarées dans des blocs extérieurs; ceci peut facilement mener à des malentendus et à des erreurs. La plupart des programmes C ne profitent pas de la possibilité de déclarer des
  • 166. variables ou des fonctions à l'intérieur d'une boucle ou d'un bloc conditionnel. Dans la suite, nous allons faire toutes nos déclarations locales au début des fonctions. 10.2.2. Variables globales Les variables déclarées au début du fichier, à l'extérieur de toutes les fonctions sont disponibles à toutes les fonctions du programme. Ce sont alors des variables globales. En général, les variables globales sont déclarées immédiatement derrière les instructions #include au début du programme. Attention ! Les variables déclarées au début de la fonction principale main ne sont pasdes variables globales, mais elles sont locales à main ! Exemple : La variable STATUS est déclarée globalement pour pouvoir être utilisée dans les procédures A et B. #include <stdio.h> int STATUS; void A(...) { ... if (STATUS>0) STATUS--; else ... ... } void B(...) { ... STATUS++; ... } Conseils * Les variables globales sont à utiliser avec précaution, puisqu'elles créent des
  • 167. liens invisibles entre les fonctions. La modularité d'un programme peut en souffrir et le programmeur risque de perdre la vue d'ensemble. * Il faut faire attention à ne pas cacher involontairement des variables globales par des variables locales du même nom. * Le codex de la programmation défensive nous conseille d'écrire nos programmes aussi 'localement' que possible. L'utilisation de variables globales devient inévitable, si * plusieurs fonctions qui ne s'appellent pas ont besoin des mêmes variables, ou * plusieurs fonctions d'un programme ont besoin du même ensemble de variables. Ce serait alors trop encombrant de passer toutes les variables comme paramètres d'une fonction à l'autre. 10.3. Déclaration et définition de fonctions En général, le nom d'une fonction apparaît à trois endroits dans un programme: 1) lors de la déclaration 2) lors de la définition 3) lors de l'appel Exemple : Avant de parler des détails, penchons-nous sur un exemple. Dans le programme suivant, la fonction main utilise les deux fonctions: - ENTREE qui lit un nombre entier entré au clavier et le fournit comme résultat. La fonction ENTREE n'a pas de paramètres. - MAX qui renvoie comme résultat le maximum de deux entiers fournis comme paramètres. #include <stdio.h> main() { /* Prototypes des fonctions appelées */ int ENTREE(void); int MAX(int N1, int N2); /* Déclaration des variables */ int A, B; /* Traitement avec appel des fonctions */ A = ENTREE(); B = ENTREE();
  • 168. printf("Le maximum est %dn", MAX(A,B)); } /* Définition de la fonction ENTREE */ int ENTREE(void) { int NOMBRE; printf("Entrez un nombre entier : "); scanf("%d", &NOMBRE); return NOMBRE; } /* Définition de la fonction MAX */ int MAX(int N1, int N2) { if (N1>N2) return N1; else return N2; } 10.3.1. Définition d'une fonction Dans la définition d'une fonction, nous indiquons: - le nom de la fonction - le type, le nombre et les noms des paramètres de la fonction - le type du résultat fourni par la fonction - les données locales à la fonction - les instructions à exécuter Définition d'une fonction en langage algorithmique fonction <NomFonct> (<NomPar1>, <NomPar2>,..):<TypeRés> | <déclarations des paramètres> | <déclarations locales> | <instructions> ffonction Définition d'une fonction en C
  • 169. <TypeRés> <NomFonct> (<TypePar1> <NomPar1>, <TypePar2> <NomPar2>,... ) { <déclarations locales> <instructions> } Remarquez qu'il n'y a pas de point-virgule derrière la définition des paramètres de la fonction. Les identificateurs Les noms des paramètres et de la fonction sont des identificateurs qui doivent correspondre aux restrictions définies dans chapitre 2.2.4. Des noms bien choisis peuvent fournir une information utile sur leur rôle. Ainsi, les identificateurs font aussi partie de la documentation d'un programme. (La définition et le rôle des différents types de paramètres dans une fonction seront discutés au chapitre 10.5. "Paramètres d'une fonction".) Attention ! Si nous choisissons un nom de fonction qui existe déjà dans une bibliothèque, notre fonction cache la fonction prédéfinie. Type d'une fonction Si une fonction F fournit un résultat du type T, on dit que 'la fonction F est du type T' ou que 'la fonction F a le type T'. Exemple : La fonction MAX est du type int et elle a besoin de deux paramètres du type int. Le résultat de la fonction MAX peut être intégré dans d'autres expressions.
  • 170. int MAX(int N1, int N2) { if (N1>N2) return N1; else return N2; } Exemple : La fonction PI fournit un résultat rationnel du type float. La liste des paramètres de PI est déclarée comme void (vide), c.-à-d. PI n'a pas besoin de paramètres et il faut l'appeler par: PI() float PI(void) { return 3.1415927; } Remarques q Une fonction peut fournir comme résultat: r un type arithmétique, r une structure (définie par struct - pas traité dans ce cours), r une réunion (définie par union - pas traité dans ce cours), r un pointeur, r void(la fonction correspond alors à une 'procédure'). Une fonction ne peut pas fournir comme résultat des tableaux, des chaînes de caractères ou des fonctions Attention: Il est cependant possible de renvoyer un pointeur sur le premier élément d'un tableau ou d'une chaîne de caractères.) q Si une fonction ne fournit pas de résultat, il faut indiquer void (vide) comme type du résultat. q Si une fonction n'a pas de paramètres, on peut déclarer la liste des paramètres comme (void) ou simplement comme () . q Le type par défaut est int; autrement dit: si le type d'une fonction n'est pas
  • 171. déclaré explicitement, elle est automatiquement du type int. q Il est interdit de définir des fonctions à l'intérieur d'une autre fonction (comme en Pascal). q En principe, l'ordre des définitions dans le texte du programme ne joue pas de rôle, mais chaque fonction doit être déclarée ou définie avant d'être appelée (voir aussi 10.3.3.) Rappel: main La fonction principale main est du type int. Elle est exécutée automatiquement lors de l'appel du programme. A la place de la définiton: int main(void) on peut écrire simplement: main() 10.3.2. Déclaration d'une fonction En C, il faut déclarer chaque fonction avant de pouvoir l'utiliser. La déclaration informe le compilateur du type des paramètres et du résultat de la fonction. A l'aide de ces données, le compilateur peut contrôler si le nombre et le type des paramètres d'une fonction sont corrects. Si dans le texte du programme la fonction est définie avant son premier appel, elle n'a pas besoin d'être déclarée. Prototype d'une fonction La déclaration d'une fonction se fait par un prototype de la fonction qui indique uniquement le type des données transmises et reçues par la fonction. Déclaration : Prototype d'une fonction <TypeRés> <NomFonct> (<TypePar1>, <TypePar2>, ...); ou bien <TypeRés> <NomFonct> (<TypePar1> <NomPar1>, <TypePar2> <NomPar2>, ... ); Attention ! Lors de la déclaration, le nombre et le type des paramètres doivent nécessairement correspondre à ceux de la définition de la fonction.
  • 172. Noms des paramètres On peut facultativement inclure les noms des paramètres dans la déclaration, mais ils ne sont pas considérés par le compilateur. Les noms fournissent pourtant une information intéressante pour le programmeur qui peut en déduire le rôle des différents paramètres. Conseil pratique Il est d'usage de copier (à l'aide de Edit - Copy & Paste) la première ligne de la définition de la fonction comme déclaration. (N'oubliez pas d'ajouter un point- virgule à la fin de la déclaration !) Règles pour la déclaration des fonctions De façon analogue aux déclarations de variables, nous pouvons déclarer une fonction localement ou globalement. La définition des fonctions joue un rôle spécial pour la déclaration. En résumé, nous allons considérer les règles suivantes: Déclaration locale: Une fonction peut être déclarée localement dans la fonction qui l'appelle (avant la déclaration des variables). Elle est alors disponible à cette fonction. Déclaration globale: Une fonction peut être déclarée globalement au début du programme (derrière les instructions #include). Elle est alors disponible à toutes les fonctions du programme. Déclaration implicite par la définition: La fonction est automatiquement disponible à toutes les fonctions qui suivent sa définition. Déclaration multiple: Une fonction peut être déclarée plusieurs fois dans le texte d'un programme, mais les indications doivent concorder. main La fonction principale main n'a pas besoin d'être déclarée. 10.3.3. Discussion d'un exemple Considérons la situation suivante:
  • 173. * La fonction main appelle la fonction FA. * La fonction FA appelle la fonction FB. Nous obtenons donc la hiérarchie suivante: Il y a beaucoup de possibilités de déclarer et de définir ces fonctions. Nous allons retenir trois variantes qui suivent une logique conséquente: a) Déclarations locales des fonctions et définition 'top-down' La définition 'top-down' suit la hiérarchie des fonctions: Nous commençons par la définition de la fonction principale main, suivie des sous-programmes FA et FB. Nous devons déclarer explicitement FA et FB car leurs définitions suivent leurs appels. /* Définition de main */ main() { /* Déclaration locale de FA */ int FA (int X, int Y); ... /* Appel de FA */ I = FA(2, 3); ... } /* Définition de FA */ int FA(int X, int Y) { /* Déclaration locale de FB */ int FB (int N, int M); ... /* Appel de FB */ J = FB(20, 30); ... } /* Définition de FB */
  • 174. int FB(int N, int M) { ... } Cet ordre de définition a l'avantage de refléter la hiérarchie des fonctions: Ainsi l'utilisateur qui ne s'intéresse qu'à la solution globale du problème n'a qu'à lire le début du fichier. Pour retrouver les détails d'une implémentation, il peut passer du haut vers le bas dans le fichier. Sur ce chemin, il retrouve toutes les dépendances des fonctions simplement en se référant aux déclarations locales. S'il existe beaucoup de dépendances dans un programme, le nombre des déclarations locales peut quand même s'accroître dans des dimensions insoutenables. b) Définition 'bottom-up' sans déclarations La définition 'bottom-up' commence en bas de la hiérarchie: La fonction main se trouve à la fin du fichier. Les fonctions qui traitent les détails du problème sont définies en premier lieu. /* Définition de FB */ int FB(int N, int M) { ... } /* Définition de FA */ int FA(int X, int Y) { ... /* Appel de FB */ J = FB(20, 30); ... } /* Définition de main */ main() { ... /* Appel de FA */ I = FA(2, 3); ... }
  • 175. Comme les fonctions sont définies avant leur appel, les déclarations peuvent être laissées de côté. Ceci allège un peu le texte du programme, mais il est beaucoup plus difficile de retrouver les dépendances entre les fonctions. c) Déclaration globale des fonctions et définition 'top-down' En déclarant toutes les fonctions globalement au début du texte du programme, nous ne sommes pas forcés de nous occuper de la dépendance entre les fonctions. Cette solution est la plus simple et la plus sûre pour des programmes complexes contenant une grande quantité de dépendances. Il est quand même recommandé de définir les fonctions selon l'ordre de leur hiérarchie: /* Déclaration globale de FA et FB */ int FA (int X, int Y); int FB (int N, int M); /* Définition de main */ main() { ... /* Appel de FA */ I = FA(2, 3); ... } /* Définition de FA */ int FA(int X, int Y) { ... /* Appel de FB */ J = FB(20, 30); ... } /* Définition de FB */ int FB(int N, int M) { ... } d) Conclusions Dans la suite, nous allons utiliser l'ordre de définition 'top-down' qui reflète le mieux la structure d'un programme. Comme nos programmes ne contiennent pas beaucoup de dépendances, nous allons déclarer les fonctions localement dans les fonctions appelantes.
  • 176. 10.4. Renvoyer un résultat Par définition, toutes les fonctions fournissent un résultat d'un type que nous devons déclarer. Une fonction peut renvoyer une valeur d'un type simple ou l'adresse d'une variable ou d'un tableau. Pour fournir un résultat en quittant une fonction, nous disposons de la commande return: La commande return L'instruction return <expression>;a les effets suivants: - évaluation de l'<expression> - conversion automatique du résultat de l'expression dans le type de la fonction - renvoi du résultat - terminaison de la fonction Exemples : La fonction CARRE du type double calcule et fournit comme résultat le carré d'un réel fourni comme paramètre. double CARRE(double X) { return X*X; } Nous pouvons définir nous-mêmes une fonction TAN qui calcule la tangente d'un réel X à l'aide des fonctions sin et de cos de la bibliothèque <math>. En langage algorithmique cette fonction se présente comme suit: fonction TAN(X): réel | donnée: réel X | si (cos(X) <> 0) | alors en TAN ranger sin(X)/cos(X) | sinon écrire "Erreur !" | fsi ffonction (* fin TAN *) En C, il faut d'abord inclure le fichier en-tête de la bibliothèque <math> pour pouvoir utiliser les fonctions prédéfinies sin et cos.
  • 177. #include <math.h> double TAN(double X) { if (cos(X) != 0) return sin(X)/cos(X); else printf("Erreur !n"); } Si nous supposons les déclarations suivantes, double X, COT;les appels des fonctions CARRE et TAN peuvent être intégrés dans des calculs ou des expressions: printf("Le carre de %f est %f n", X, CARRE(X)); printf("La tangente de %f est %f n", X, TAN(X)); COT = 1/TAN(X); void En C, il n'existe pas de structure spéciale pour la définition de procédures comme en langage algorithmique. Nous pouvons cependant employer une fonction du type void partout où nous utiliserions une procédure en langage algorithmique. Exemple : La procédure LIGNE affiche L étoiles dans une ligne: procédure LIGNE(L) | donnée L | (* Déclarations des variables locales *) | entier I | (* Traitements *) | en I ranger 0 |tant que I<>L faire | écrire "*" | en I ranger I+1 | ftant (* I=L *) | écrire (* passage à la ligne *) fprocédure Pour la traduction en C, nous utilisons une fonction du type void:
  • 178. void LIGNE(int L) { /* Déclarations des variables locales */ int I; /* Traitements */ for (I=0; I<L; I++) printf("*"); printf("n"); } main Dans nos exemples, la fonction main n'a pas de paramètres et est toujours du type int (Voir aussi Chap 2.2.2. Remarque avancée) Typiquement, les programmes renvoient la valeur zéro comme code d'erreur s'ils se terminent avec succès. Des valeurs différentes de zéro indiquent un arrêt fautif ou anormal. Remarque Si nous quittons une fonction (d'un type différent de void) sans renvoyer de résultat à l'aide de return, la valeur transmise à la fonction appelante est indéfinie. Le résultat d'une telle action est imprévisible. Si une erreur fatale s'est produite à l'intérieur d'une fonction, il est conseillé d'interrompre l'exécution de tout le programme et de renvoyer un code erreur différent de zéro à l'environnement pour indiquer que le programme ne s'est pas terminé normalement. Vu sous cet angle, il est dangereux de déclarer la fonction TAN comme nous l'avons fait plus haut: Le cas d'une division par zéro, est bien intercepté et reporté par un message d'erreur, mais l'exécution du programme continue 'normalement' avec des valeurs incorrectes. exit Pour remédier à ce dilemme, nous pouvons utiliser la fonction exit qui est définie dans la bibliothèque <stdlib>. exit nous permet d'interrompre l'exécution du programme en fournissant un code d'erreur à l'environnement. Pour pouvoir localiser l'erreur à l'intérieur du programme, il est
  • 179. avantageux d'afficher un message d'erreur qui indique la nature de l'erreur et la fonction dans laquelle elle s'est produite. Une version plus solide de TAN se présenterait comme suit: #include <math.h> double TAN(double X) { if (cos(X) != 0) return sin(X)/cos(X); else { printf("aFonction TAN:n" "Erreur: Division par zéro !n"); exit(-1); /* Code erreur -1 */ } } Ignorer le résultat Lors de l'appel d'une fonction, l'utilisateur est libre d'accepter le résultat d'une fonction ou de l'ignorer. Exemple : La fonction scanf renvoie le nombre de données correctement reçues comme résultat. En général, nous avons ignoré ce fait: int JOUR, MOIS, ANNEE; printf("Entrez la date actuelle : "); scanf("%d %d %d", &JOUR, &MOIS, &ANNEE); Nous pouvons utiliser le résultat de scanf comme contrôle:
  • 180. int JOUR, MOIS, ANNEE; int RES; do { printf("Entrez la date actuelle : "); RES = scanf("%d %d %d", &JOUR,&MOIS,&ANNEE); } while (RES != 3); 10.5. Paramètres d'une fonction Les paramètres ou arguments sont les 'boîtes aux lettres' d'une fonction. Elles acceptent les données de l'extérieur et déterminent les actions et le résultat de la fonction. Techniquement, nous pouvons résumer le rôle des paramètres en C de la façon suivante: Les paramètres d'une fonction sont simplement des variables locales qui sont initialisées par les valeurs obtenues lors de l'appel. 10.5.1. Généralités Conversion automatique Lors d'un appel, le nombre et l'ordre des paramètres doivent nécessairement correspondre aux indications de la déclaration de la fonction. Les paramètres sont automatiquement convertis dans les types de la déclaration avant d'être passés à la fonction. Exemple Le prototype de la fonction pow (bibliothèque <math>) est déclaré comme suit: double pow (double, double); Au cours des instructions, int A, B; ... A = pow (B, 2);
  • 181. nous assistons à trois conversions automatiques: Avant d'être transmis à la fonction, la valeur de B est convertie en double; la valeur 2 est convertie en 2.0 . Comme pow est du type double, le résultat de la fonction doit être converti en int avant d'être affecté à A. void Evidemment, il existe aussi des fonctions qui fournissent leurs résultats ou exécutent une action sans avoir besoin de données. La liste des paramètres contient alors la déclaration void ou elle reste vide (P.ex.: double PI(void) ou int ENTREE() ). 10.5.2. Passage des paramètres par valeur En C, le passage des paramètres se fait toujours par la valeur, c.-à-d. les fonctions n'obtiennent que les valeurs de leurs paramètres et n'ont pas d'accès aux variables elles-mêmes. 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. A l'intérieur de la fonction, nous pouvons donc changer les valeurs des paramètres sans influencer les valeurs originales dans les fonctions appelantes. Exemple : La fonction ETOILES dessine une ligne de N étoiles. Le paramètre N est modifié à l'intérieur de la fonction: void ETOILES(int N) { while (N>0) { printf("*"); N--; } printf("n"); } En utilisant N comme compteur, nous n'avons pas besoin de l'indice d'aide I comme dans la fonction LIGNES définie plus haut. La fonction TRIANGLE, appelle la fonction ETOILES en utilisant la variable L comme paramètre:
  • 182. void TRIANGLE(void) { int L; for (L=1; L<10; L++) ETOILES(L); } Au moment de l'appel, la valeur de L est copiée dans N. La variable N peut donc être décrémentée à l'intérieur de ETOILES, sans influencer la valeur originale de L. Schématiquement, le passage des paramètres peut être représenté dans une 'grille' des valeurs: Avantages Le passage par valeur a l'avantage que nous pouvons utiliser les paramètres comme des variables locales bien initialisées. De cette façon, nous avons besoin de moins de variables d'aide. 10.5.3. Passage de l'adresse d'une variable Comme nous l'avons constaté ci-dessus, une fonction n'obtient que les valeurs de ses paramètres.
  • 183. Pour changer la valeur d'une variable de la fonction appelante, nous allons procéder comme suit: - la fonction appelante doit fournir l'adresse de la variable et - la fonction appelée doit déclarer le paramètre comme pointeur. On peut alors atteindre la variable à l'aide du pointeur. Discussion d'un exemple Nous voulons écrire une fonction PERMUTER qui échange le contenu de deux variables du type int. En première approche, nous écrivons la fonction suivante: void PERMUTER (int A, int B) { int AIDE; AIDE = A; A = B; B = AIDE; } Nous appelons la fonction pour deux variables X et Y par: PERMUTER(X, Y); Résultat: X et Y restent inchangés ! Explication: Lors de l'appel, les valeurs de X et de Y sont copiées dans les paramètres A et B. PERMUTER échange bien le contenu des variables locales A et B, mais les valeurs de X et Y restent les mêmes. Pour pouvoir modifier le contenu de X et de Y , la fonction PERMUTER a besoin des adresses de X et Y . En utilisant des pointeurs, nous écrivons une deuxième fonction:
  • 184. void PERMUTER (int *A, int *B) { int AIDE; AIDE = *A; *A = *B; *B = AIDE; } Nous appelons la fonction par: PERMUTER(&X, &Y); Résultat: Le contenu des variables X et Y est échangé ! Explication: Lors de l'appel, les adresses de X et de Y sont copiées dans les pointeurs A et B. PERMUTER échange ensuite le contenu des adresses indiquées par les pointeurs A et B. 10.5.4. Passage de l'adresse d'un tableau à une dimension Méthode Comme il est impossible de passer 'la valeur' de tout un tableau à une fonction, on fournit l'adresse d'un élément du tableau. En général, on fournit l'adresse du premier élément du tableau, qui est donnée par le nom du tableau. Déclaration Dans la liste des paramètres d'une fonction, on peut déclarer un tableau par le nom suivi de crochets, <type> <nom>[ ] ou simplement par un pointeur sur le type des éléments du tableau:
  • 185. <type> *<nom> Exemple : La fonction strlen calcule et retourne la longueur d'une chaîne de caractères fournie comme paramètre: int strlen(char *S) { int N; for (N=0; *S != '0'; S++) N++; return N; } A la place de la déclaration de la chaîne comme char *Son aurait aussi pu indiquer char S[ ]comme nous l'avons fait dans l'exemple d'introduction (chapitre 10.1.2). Dans la suite, nous allons utiliser la première notation pour mettre en évidence que le paramètre est un pointeur variable que nous pouvons modifier à l'intérieur de la fonction. Appel Lors d'un appel, l'adresse d'un tableau peut être donnée par le nom du tableau, par un pointeur ou par l'adresse d'un élément quelconque du tableau. Exemple : Après les instructions, char CH[ ] = "Bonjour !"; char *P; P = CH; nous pouvons appeler la fonction strlen définie ci-dessus par: strlen(CH) /* résultat: 9 */ strlen(P) /* résultat: 9 */ strlen(&CH[4]) /* résultat: 5 */ strlen(P+2) /* résultat: 7 */ strlen(CH+2) /* résultat: 7 */
  • 186. Dans les trois dernièrs appels, nous voyons qu'il est possible de fournir une partie d'un tableau à une fonction, en utilisant l'adresse d'un élément à l'intérieur de tableau comme paramètre. Remarque pratique Pour qu'une fonction puisse travailler correctement avec un tableau qui n'est pas du type char, il faut aussi fournir la dimension du tableau ou le nombre d'éléments à traiter comme paramètre, sinon la fonction risque de sortir du domaine du tableau. Exemple : La fonction LIRETAB lit N données pour un tableau (unidimensionnel) du type int et les mémorise à partir de l'adresse indiquée par le pointeur PTAB. PTAB et N sont fournis comme paramètres. void LIRE_TAB(int N, int *PTAB) { printf("Entrez %d valeurs : n", N); while(N) { scanf("%d", PTAB++); N-- ; } } Dans l'appel de la fonction nous utilisons en général le nom du tableau: LIRE_TAB(4, T); Nous obtenons alors les grilles suivantes: 10.5.5. Passage de l'adresse d'un tableau à deux dimensions Exemple : Imaginons que nous voulons écrire une fonction qui calcule la somme de tous
  • 187. les éléments d'une matrice de réels A dont nous fournissons les deux dimensions N et M comme paramètres. Problème Comment pouvons-nous passer l'adresse de la matrice à la fonction ? Par analogie avec ce que nous avons vu au chapitre précédent, nous pourrions envisager de déclarer le tableau concerné dans l'en-tête de la fonction sous la forme A[ ][ ]. Dans le cas d'un tableau à deux dimensions, cette méthode ne fournit pas assez de données, parce que le compilateur a besoin de la deuxième dimension du tableau pour déterminer l'adresse d'un élément A[i][j]. Une solution praticable consiste à faire en sorte que la fonction reçoive un pointeur (de type float*) sur le début de la matrice et de parcourir tous les éléments comme s'il s'agissait d'un tableau à une dimension N*M. Cela nous conduit à cette fonction: float SOMME(float *A, int N, int M) { int I; float S; for (I=0; I<N*M; I++) S += A[I]; return S; } Lors d'un appel de cette fonction, la seule difficulté consiste à transmettre l'adresse du début du tableau sous forme d'un pointeur sur float. Prenons par exemple un tableau déclaré par float A[3][4]; Le nom A correspond à la bonne adresse, mais cette adresse est du type "pointeur sur un tableau de 4 éléments du type float". Si notre fonction est correctement déclarée, le compilateur la convertira automatiquement dans une adresse du type 'pointeur sur float'. Toutefois, comme nous l'avons déjà remarqué au chapitre 9.3.4, on gagne en lisibilité et on évite d'éventuels messages d'avertissement si on utilise l'opérateur de conversion forcée ("cast"). Solution Voici finalement un programme faisant appel à notre fonction SOMME:
  • 188. #include <stdio.h> main() { /* Prototype de la fonction SOMME */ float SOMME(float *A, int N, int M); /* Déclaration de la matrice */ float T[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9,10,11,12}}; /* Appel de la fonction SOMME */ printf("Somme des éléments : %f " "n", SOMME((float*)T, 3, 4) ); return 0; } Rappel Rappelons encore une fois que lors de l'interprétation d'un tableau à deux dimensions comme tableau unidimensionnel, il faut calculer les adresses des composantes à l'aide du nombre de colonnes maximal réservé lors de la déclaration. Remarque Une méthode plus propre pour éviter le cast SOMME((float*)T, 3, 4) ); est de renvoyer explicitement l'adresse du premier élément du tableau: SOMME(&T[0][0], 3, 4) ); 10.6. Les modules en lang. algorithmique, et en C Ce chapitre résume les différences principales entre les modules (programme principal, fonctions) dans les différents langages de programmation que nous connaissons. Modules En langage algorithmique, nous distinguons programme principal, procédures et fonctions. En C, il existe uniquement des fonctions. La fonction principale main se
  • 189. distingue des autres fonctions par deux qualités: a) Elle est exécutée lors de l'appel du programme. b) Les types du résultat (int) et des paramètres (void) sont fixés. Définition des modules En langage algorithmique, le programme principal, les fonctions et les procédures sont déclarés dans des blocs distincts. Il est interdit d'imbriquer leurs définitions. La définition du programme principal précède celle des fonctions et des procédures. En C, il est interdit de définir des fonctions à l'intérieur d'autres fonctions, mais nous pouvons déclarer des fonctions localement. Variables locales En langage algorithmique, nous pouvons déclarer des variables locales au début des fonctions et des procédures. En C, il est permis (mais déconseillé) de déclarer des variables locales au début de chaque bloc d'instructions. Variables globales En langage algorithmique, les variables globales sont définies au début du programme principal. En C, les variables globales sont définies au début du fichier, à l'extérieur de toutes les fonctions. (Les variables de la fonction principale main sont locales à main.)
  • 190. Passage des paramètres En langage algorithmique, nous distinguons entre passage des paramètres par valeur et passage des paramètres par référence. En C, le passage des paramètres se fait toujours par la valeur. Pour pouvoir changer le contenu d'une variable déclarée dans une autre fonction, il faut utiliser un pointeur comme paramètre de passage et transmettre l'adresse de la variable lors de l'appel. Exemple comparatif La fonction DIVI divise son premier paramètre A par son deuxième paramètre B et fournit le reste de la division entière comme résultat. Le contenu du paramètre A est modifié à l'intérieur de la fonction, le paramètre B reste inchangé. Le programme principal appelle la fonction DIVI avec deux entiers lus au clavier et affiche les résultats. - Solution du problème en langage algorithmique programme TEST_DIVI entier N,D,R écrire "Entrer nominateur et dénominateur : " lire N lire D en R ranger DIVI(N,D) écrire "Résultat: ",N," Reste: ",R fprogramme fonction DIVI (A, B): entier résultat: entier A donnée: entier B entier C en C ranger A modulo B en A ranger A divent B en DIVI ranger C ffonction (* fin DIVI *) * Le paramètre A est transféré par référence: Il est déclaré par le mot-clef résultat au début de la fonction. * Le paramètre B est transféré par valeur: Il est déclaré par le mot-clef donnée au début de la fonction. * Le résultat de la fonction est affecté au nom de la fonction. Cette affectation
  • 191. doit se trouver à la fin la fonction. * Dans un appel, il n'y a pas de différence entre la notation des paramètres passés par référence et ceux passés par valeur. - Solution du problème en C #include <stdio.h> main() { int DIVI(int *A, int B); int N, D, R; printf("Entrer nominateur et dénominateur : "); scanf("%d %d", &N, &D); R = DIVI (&N, D); printf("Résultat: %d Reste: %dn", N, R); return 0; } int DIVI (int *A, int B) { int C; C = *A % B; *A /= B; return C; } * Le paramètre A reçoit l'adresse d'une variable: Il est déclaré comme pointeur sur int. * Le paramètre B reçoit la valeur d'une variable: Il est déclaré comme int. * Le résultat de la fonction est retourné à l'aide de la commande return. Comme l'exécution de la fonction s'arrête après la commande return, celle-ci doit se trouver à la fin de la fonction. * Dans un appel, le premier paramètre est une adresse. Le nom de la variable N est donc précédé par l'opérateur adresse &. Le deuxième paramètre est passé par valeur. Le nom de la variable est indiqué sans désignation spéciale.
  • 192. Vu de plus près, les trois langages offrent les mêmes mécanismes pour le passage des paramètres, mais: >> en C nous devons veiller nous-mêmes à opérer avec les adresses et les pointeurs respectifs si nous voulons changer le contenu d'une variable déclarée dans une autre fonction; >> en langage algorithmique, les mêmes opérations se déroulent derrière les rideaux, sous l'étiquette 'passage par référence' ('call-by-reference'). 10.7. Discussion de deux problèmes "fonction" ou "procédure" ? Problème 1: Ecrire une fonction qui lit un nombre entier au clavier en affichant un petit texte d'invite. Réflexion: Avant d'attaquer le problème, nous devons nous poser la question, de quelle façon nous allons transférer la valeur lue dans la variable de la fonction appelante. Il se présente alors deux possibilités: a) Nous pouvons fournir la valeur comme résultat de la fonction. b) Nous pouvons affecter la valeur à une adresse que nous obtenons comme paramètre. Dans ce cas, le résultat de la fonction est void. Cette fonction est en fait une "procédure" au sens du du langage algorithmique. Résultat int ==> "fonction" Reprenons d'abord la fonction ENTREE que nous avons définie au chapitre 10.3.: int ENTREE(void) { int NOMBRE; printf("Entrez un nombre entier : "); scanf("%d", &NOMBRE); return NOMBRE; } La fonction ENTREE fournit un résultat du type int qui est typiquement affecté à
  • 193. une variable, int A; A = ENTREE(); ou intégré dans un calcul: long SOMME; int I; for (I=0; I<10; I++) SOMME += ENTREE(); Résultat void ==> "procédure" Nous pouvons obtenir le même effet que ENTREE en définissant une fonction ENTRER du type void, qui affecte la valeur lue au clavier immédiatement à une adresse fournie comme paramètre. Pour accepter cette adresse, le paramètre de la fonction doit être déclaré comme pointeur: void ENTRER(int *NOMBRE) { printf("Entrez un nombre entier : "); scanf("%d", NOMBRE); } Remarque : Comme le paramètre NOMBRE est un pointeur, il n'a pas besoin d'être précédé du symbole & dans l'instruction scanf. Lors de l'appel, nous devons transférer l'adresse de la variable cible comme paramètre: int A; ENTRER(&A);
  • 194. Jusqu'ici, la définition et l'emploi de la fonction ENTRER peuvent sembler plus simples que ceux de la fonction ENTREE. Si nous essayons d'intégrer les valeurs lues par ENTRER dans un calcul, nous allons quand même constater que ce n'est pas toujours le cas: long SOMME; int I; int A; for (I=0; I<10; I++) { ENTRER(&A); SOMME += A; } Conclusions Dans la plupart des cas, nous pouvons remplacer une fonction qui fournit un résultat par une fonction du type void qui modifie le contenu d'une variable de la fonction appelante. En général, la préférence pour l'une ou l'autre variante dépend le l'utilisation de la fonction: * Si le résultat de la fonction est typiquement intégré dans un calcul ou une expression, alors nous employons une fonction qui fournit un résultat. En fait, personne ne remplacerait une fonction comme double sin(double X) par une fonction void sin(double *Y, double X) * Si la charge principale d'une fonction est de modifier des données ou l'état de l'environnement, sans que l'on ait besoin d'un résultat, alors il vaut mieux utiliser une fonction du type void. * Si une fonction doit fournir plusieurs valeurs comme résultat, il s'impose d'utiliser une procédure du type void. Ce ne serait pas une bonne solution de fournir une valeur comme résultat et de transmettre les autres valeurs comme paramètres. Exemple : La fonction MAXMIN fournit le maximum et le minimum des valeurs d'un tableau T de N d'entiers. Comme nous ne pouvons pas fournir les deux valeurs comme résultat, il vaut mieux utiliser deux paramètres pointeurs MAX et MIN qui obtiendront les adresses cibles pour les deux valeurs. Ainsi la fonction MAXMIN est définie avec quatre paramètres:
  • 195. void MAXMIN(int N, int *T, int *MAX, int *MIN) { int I; *MAX=*T; *MIN=*T; for (I=1; I<N; I++) { if (*(T+I)>*MAX) *MAX = *(T+I); if (*(T+I)<*MIN) *MIN = *(T+I); } } Lors d'un appel de MAXMIN, il ne faut pas oublier d'envoyer les adresses des paramètres pour MAX et MIN. int TAB[8] = {2,5,-1,0,6,9,-4,6}; int N = 8; int MAXVAL, MINVAL; MAXMIN(N, TAB, &MAXVAL, &MINVAL); Pointeur ou indice numérique ? Problème 2: Ecrire une fonction qui fournit la position du prochain signe d'espacement dans une chaîne de caractères ou la position de la fin de la chaîne si elle ne contient pas de signe d'espacement. Utiliser la fonction isspace de la bibliothèque <ctype> pour la recherche. Réflexion: Il y a plusieurs possibilités de résoudre ce problème: Une "indication" d'une position dans une chaîne de caractères peut être fournie par un pointeur ou par un indice numérique. Dans la suite, nous allons développer les deux variations. Dans les deux cas, nous avons besoin de l'adresse du tableau qui est passée comme paramètre char *CH à la fonction. Résultat int La fonction CHERCHE1 fournit l'indice de l'élément recherché comme résultat du type int.
  • 196. int CHERCHE1(char *CH) { int INDEX=0; while (*CH && !isspace(*CH)) { CH++; INDEX++; } return INDEX; } Cette information peut être affectée à une variable: int I; char TXT[40]; ... I = CHERCHE1(TXT); ou être intégrée dans une expression en profitant même de l'arithmétique des pointeurs: main() { /* Prototype de la fonction appelée */ int CHERCHE1(char *CH); char TXT[40]; printf("Entrer une phrase : "); gets(TXT); /* Affichage de la phrase sans le premier mot */ puts(TXT + CHERCHE1(TXT)); return 0; } Résultat char* La fonction CHERCHE2 fournit un pointeur sur l'élément recherché. Remarquez
  • 197. la déclaration du résutat de la fonction CHERCHE2 comme pointeur sur char: char *CHERCHE2(char *CH) { while (*CH && !isspace(*CH)) CH++; return CH; } Il se montre à nouveau que l'utilisation de pointeurs permet une solution très compacte et élégante. Dans cette version, nous n'avons plus besoin de variables d'aide et nous pouvons renvoyer la valeur modifiée du paramètre local CH comme résultat. L'utilisation de la fonction peut se présenter de façon aussi élégante: main() { /* Prototype de la fonction appelée */ char *CHERCHE2(char *CH); char TXT[40]; printf("Entrer une phrase : "); gets(TXT); /* Affichage de la phrase sans le premier mot */ puts(CHERCHE2(TXT)); return 0; } Conclusion Lors du travail avec des tableaux et surtout avec des chaînes de caractères, il est toujours avantageux d'utiliser des pointeurs et de profiter de l'arithmétique des pointeurs. Les fonctions employant des pointeurs lors du traitement de tableaux permettent souvent des solutions très naturelles, d'autant plus qu'elles disposent des adresses des tableaux dans des paramètres locaux.
  • 198. Chapitre 11: LES FICHIERS SEQUENTIELS En C, les communications d'un programme avec son environnement se font par l'intermédiaire de fichiers. Pour le programmeur, tous les périphériques, même le clavier et l'écran, sont des fichiers. Jusqu'ici, nos programmes ont lu leurs données dans le fichier d'entrée standard, (c.-à-d.: le clavier) et ils ont écrit leurs résultats dans le fichier de sortie standard (c.-à-d.: l'écran). Nous allons voir dans ce chapitre, comment nous pouvons créer, lire et modifier nous-mêmes des fichiers sur les périphériques disponibles. q 11.1. Définitions et propriétés q 11.2. La mémoire tampon q 11.3. Accès aux fichiers séquentiels r 11.3.1. Le type FILE* r 11.3.2. Exemple: Créer et afficher un fichier séquentiel q 11.4. Ouvrir et fermer des fichiers séquentiels r 11.4.1. Ouvrir un fichier séquentiel r 11.4.2. Fermer un fichier séquentiel r 11.4.3. Exemples: Ouvrir et fermer des fichiers en pratique q 11.5. Lire et écrire dans des fichiers séquentiels r 11.5.1. Traitement par enregistrements
  • 199. r 11.5.2. Traitement par caractères r 11.5.3. Détection de la fin d'un fichier séquentiel q 11.6. Résumé sur les fichiers q 11.7. Mise à jour d'un fichier séquentiel en C r 11.7.1. Ajouter un enregistrement à un fichier r 11.7.2. Supprimer un enregistrement dans un fichier r 11.7.3. Modifier un enregistrement dans un fichier 11.1. Définitions et propriétés Fichier Un fichier (angl.: file) est un ensemble structuré de données stocké en général sur un support externe (disquette, disque dur, disque optique, bande magnétique, ...). Un fichier structuré contient une suite d'enregistrements homogènes . Fichier séquentiel Dans des fichiers séquentiels, les enregistrements sont mémorisés consécutivement dans l'ordre de leur entrée et peuvent seulement être lus dans cet ordre. Si on a besoin d'un enregistrement précis dans un fichier séquentiel, il faut lire tous les enregistrements qui le précèdent, en commençant par le premier. En simplifiant, nous pouvons nous imaginer qu'un fichier séquentiel est enregistré sur une bande magnétique:
  • 200. Propriétés Les fichiers séquentiels que nous allons considérer dans ce cours auront les propriétés suivantes: *) Les fichiers se trouvent ou bien en état d'écriture ou bien en état de lecture; nous ne pouvons pas simultanément lire et écrire dans le même fichier. *) A un moment donné, on peut uniquement accéder à un seul enregistrement; celui qui se trouve en face de la tête de lecture/ écriture. *) Après chaque accès, la tête de lecture/écriture est déplacée derrière la donnée lue en dernier lieu. Fichiers standards Il existe deux fichiers spéciaux qui sont définis par défaut pour tous les programmes: - stdin le fichier d'entrée standard - stdout le fichier de sortie standard En général, stdin est lié au clavier et stdout est lié à l'écran, c.-à- d. les programmes lisent leurs données au clavier et écrivent les résultats sur l'écran. En UNIX et en MS-DOS, il est possible de dévier l'entrée et la sortie standard vers d'autres fichiers ou périphériques à l'aide des symboles < (pour stdin ) et > (pour
  • 201. stdout) : Exemple L'appel suivant du programme PROG lit les données dans le fichier C:TEST.TXT au lieu du clavier et écrit les résultats sur l'imprimante au lieu de l'écran. PROG <C:TEST.TXT >PRN: En fait, l'affectation de stdin et stdout est gérée par le système d'exploitation; ainsi le programme ne 'sait' pas d'où viennent les données et où elles vont. 11.2. La mémoire tampon Pour des raisons d'efficacité, les accès à un fichier se font par l'intermédiaire d'une mémoire tampon (angl.: buffer ). La mémoire tampon est une zone de la mémoire centrale de la machine réservée à un ou plusieurs enregistrements du fichier. L'utilisation de la mémoire tampon a l'effet de réduire le nombre d'accès à la périphérie d'une part et le nombre des mouvements de la tête de lecture/écriture d'autre part. 11.3. Accès aux fichiers séquentiels Les problèmes traitant des fichiers ont généralement la forme suivante: un fichier donné par son nom (et en cas de besoin le
  • 202. chemin d'accès sur le médium de stockage) doit être créé, lu ou modifié. La question qui se pose est alors: Comment pouvons-nous relier le nom d'un fichier sur un support externe avec les instructions qui donnent accès au contenu du fichier ? En résumé, la méthode employée sera la suivante: Avant de lire ou d'écrire un fichier, l'accès est notifié par la commande fopen. fopen accepte le nom du fichier (p.ex: "A: ADRESSES.DAT"), négocie avec le système d'exploitation et fournit un pointeur spécial qui sera utilisé ensuite lors de l'écriture ou la lecture du fichier. Après les traitements, il faut annuler la liaison entre le nom du fichier et le pointeur à l'aide de la commande fclose. On peut dire aussi qu'entre les événements fopen() et fclose() le fichier est ouvert. 11.3.1. Le type FILE* Pour pouvoir travailler avec un fichier, un programme a besoin d'un certain nombre d'informations au sujet du fichier: - adresse de la mémoire tampon, - position actuelle de la tête de lecture/écriture, - type d'accès au fichier: écriture, lecture, ... - état d'erreur, -... Ces informations (dont nous n'aurons pas à nous occuper), sont rassemblées dans une structure du type spécial FILE. Lorsque nous ouvrons un fichier avec la commande fopen, le système génère automatiquement un bloc du type FILE et nous
  • 203. fournit son adresse. Tout ce que nous avons à faire dans notre programme est: *) déclarer un pointeur du type FILE* pour chaque fichier dont nous avons besoin, *) affecter l'adresse retournée par fopen à ce pointeur, *) employer le pointeur à la place du nom du fichier dans toutes les instructions de lecture ou d'écriture, *) libérer le pointeur à la fin du traitement à l'aide de fclose. 11.3.2. Exemple: Créer et afficher un fichier séquentiel Avant de discuter les détails du traitement des fichiers, nous vous présentons un petit exemple comparatif qui réunit les opérations les plus importantes sur les fichiers. Problème On se propose de créer un fichier qui est formé d'enregistrements contenant comme information le nom d'une personne. Chaque enregistrement est donc constitué d'une seule rubrique, à savoir, le nom de la personne. L'utilisateur doit entrer au clavier le nom du fichier, le nombre de personnes et les noms des personnes. Le programme se chargera de créer le fichier correspondant sur disque dur ou sur disquette. Après avoir écrit et fermé le fichier, le programme va rouvrir le
  • 204. même fichier en lecture et afficher son contenu, sans utiliser le nombre d'enregistrements introduit dans la première partie. Solution en langage algorithmique programme PERSONNEL chaîne NOM_FICHIER, NOM_PERS entier C,_NB_ENREG (* Première partie : Créer et remplir le fichier *) écrire "Entrez le nom du fichier à créer : " lire NOM_FICHIER ouvrir NOM_FICHIER en écriture écrire "Nombre d'enregistrements à créer : " lire NB_ENREG en C ranger 0 tant que (C<NB_ENREG) faire | écrire "Entrez le nom de la personne : " | lire NOM_PERS | écrire NOM_FICHIER:NOM_PERS | en C ranger C+1 ftant (* C=NB_ENREG *) fermer NOM_FICHIER (* Deuxième partie : Lire et afficher le contenu du fichier *) ouvrir NOM_FICHIER en lecture en C ranger 0 tant que non(finfichier(NOM_FICHIER)) faire | lire NOM_FICHIER:NOM_PERS | écrire "NOM : ",NOM_PERS | en C ranger C+1 ftant fermer NOM_FICHIER fprogramme (* fin PERSONNEL *)
  • 205. Solution en langage C #include <stdio.h> main() { FILE *P_FICHIER; /* pointeur sur FILE */ char NOM_FICHIER[30], NOM_PERS[30]; int C,NB_ENREG; /* Première partie : Créer et remplir le fichier */ printf("Entrez le nom du fichier à créer : "); scanf("%s", NOM_FICHIER); P_FICHIER = fopen(NOM_FICHIER, "w"); /* write */ printf("Nombre d'enregistrements à créer : "); scanf("%d", &NB_ENREG); C = 0; while (C<NB_ENREG) { printf("Entrez le nom de la personne : "); scanf("%s", NOM_PERS); fprintf(P_FICHIER, "%sn", NOM_PERS); C++; } fclose(P_FICHIER); /* Deuxième partie : Lire et afficher le contenu du fichier */ P_FICHIER = fopen(NOM_FICHIER, "r"); /* read */ C = 0; while (!feof(P_FICHIER))
  • 206. { fscanf(P_FICHIER, "%sn", NOM_PERS); printf("NOM : %sn", NOM_PERS); C++; } fclose(P_FICHIER); return 0; } >> Voir aussi: Chapitre 11.4.3. Exemples: Ouvrir et fermer des fichiers en pratique 11.4. Ouvrir et fermer des fichiers séquentiels Avant de créer ou de lire un fichier, nous devons informer le système de cette intention pour qu'il puisse réserver la mémoire pour la zone d'échange et initialiser les informations nécessaires à l'accès du fichier. Nous parlons alors de l'ouverture d'un fichier. Après avoir terminé la manipulation du fichier, nous devons vider la mémoire tampon et libérer l'espace en mémoire que nous avons occupé pendant le traitement. Nous parlons alors de la fermeture du fichier. L'ouverture et la fermeture de fichiers se font à l'aide des fonctions fopen et fclose définies dans la bibliothèque standard <stdio>. 11.4.1. Ouvrir un fichier séquentiel Ouvrir un fichier en langage algorithmique ouvrir <Nom> en écriture ou bien ouvrir <Nom> en lecture
  • 207. <Nom> est une chaîne de caractères constante ou une variable de type chaîne qui représente le nom du fichier sur le médium de stockage. Ouvrir un fichier en C - fopen Lors de l'ouverture d'un fichier avec fopen, le système s'occupe de la réservation de la mémoire tampon dans la mémoire centrale et génère les informations pour un nouvel élément du type FILE. L'adresse de ce bloc est retournée comme résultat si l'ouverture s'est déroulée avec succès. La commande fopen peut ouvrir des fichiers en écriture ou en lecture en dépendance de son deuxième paramètre ("r" ou "w") : <FP> = fopen ( <Nom> , "w" ); ou bien <FP> = fopen ( <Nom> , "r" ); *) <Nom> est une chaîne de caractères constante ou une variable de type chaîne qui représente le nom du fichier sur le médium de stockage, *) le deuxième argument détermine le mode d'accès au fichier: pour 'ouverture en - write "w" écriture' - - read "r" pour 'ouverture en lecture' - *) <FP> est un pointeur du type FILE* qui sera relié au fichier sur le médium de stockage. Dans la suite du programme, il faut utiliser <FP> au lieu de <Nom> pour référencer le fichier. *) <FP> doit être déclaré comme: FILE *FP; Le résultat de fopen
  • 208. Si le fichier a pu être ouvert avec succès, fopen fournit l'adresse d'un nouveau bloc du type FILE. En général, la valeur de cette adresse ne nous intéresse pas; elle est simplement affectée à un pointeur <FP> du type FILE* que nous utiliserons ensuite pour accéder au fichier. A l'apparition d'une erreur lors de l'ouverture du fichier, fopen fournit la valeur numérique zéro qui est souvent utilisée dans une expression conditionnelle pour assurer que le traitement ne continue pas avec un fichier non ouvert (voir 11.4.3. Exemples). Ouverture en écriture Dans le cas de la création d'un nouveau fichier, le nom du fichier est ajouté au répertoire du médium de stockage et la tête de lecture/écriture est positionnée sur un espace libre du médium. Si un fichier existant est ouvert en écriture, alors son contenu est perdu. Si un fichier non existant est ouvert en écriture, alors il est créé automatiquement. Si la création du fichier est impossible alors fopen indique une erreur en retournant la valeur zéro. Autres possibilités d'erreurs signalées par un résultat nul: - chemin d'accès non valide, - pas de disque/bande dans le lecteur, - essai d'écrire sur un médium protégé contre l'écriture, -... Ouverture en lecture Dans le cas de la lecture d'un fichier existant, le nom du fichier doit être retrouvé dans le répertoire du médium et la tête de
  • 209. lecture/écriture est placée sur le premier enregistrement de ce fichier. Possibilités d'erreurs signalées par un résultat nul: - essai d'ouvrir un fichier non existant, - essai d'ouvrir un fichier sans autorisation d'accès, - essai d'ouvrir un fichier protégé contre la lecture, -... Remarque avancée: Si un fichier n'a pas pu être ouvert avec succès, (résultat NUL), un code d'erreur est placé dans la variable errno. Ce code désigne plus exactement la nature de l'erreur survenue. Les codes d'erreurs sont définis dans <errno.h>. q L'appel de la fonction strerror(errno) retourne un pointeur sur la chaîne de caractères qui décrit l'erreur dans errno. q L'appel de la fonction perror(s) affiche la chaîne s et le message d'erreur qui est défini pour l'erreur dans errno. 11.4.2. Fermer un fichier séquentiel Fermer un fichier en langage algorithmique fermer <Nom> <Nom> est une chaîne de caractères constante ou une variable de type chaîne qui représente le nom du fichier que l'on désire fermer. Fermer un fichier en langage C fclose( <FP> ); <FP> est un pointeur du type FILE* relié au nom du fichier que
  • 210. l'on désire fermer. La fonction fclose provoque le contraire de fopen: Si le fichier a été ouvert en écriture, alors les données non écrites de la mémoire tampon sont écrites et les données supplémentaires (longueur du fichier, date et heure de sa création) sont ajoutées dans le répertoire du médium de stockage. Si le fichier a été ouvert en lecture, alors les données non lues de la mémoire tampon sont simplement 'jetées'. La mémoire tampon est ensuite libérée et la liaison entre le pointeur sur FILE et le nom du fichier correspondant est annulée. Après fclose() le pointeur <FP> est invalide. Des erreurs graves pourraient donc survenir si ce pointeur est utilisé par la suite! 11.4.3. Exemples: Ouvrir et fermer des fichiers en pratique En langage algorithmique, il suffit de simplement ouvrir et fermer un fichier par les commandes respectives: programme PERSONNEL chaîne NOM_FICHIER . . . écrire "Entrez le nom du fichier : " lire NOM_FICHIER ouvrir NOM_FICHIER en écriture (* ou bien *) (* ouvrir NOM_FICHIER en lecture *) . . . fermer NOM_FICHIER
  • 211. fprogramme (* fin PERSONNEL *) En pratique, il faut contrôler si l'ouverture d'un fichier a été accomplie avec succès avant de continuer les traitements. Pour le cas d'une erreur, nous allons envisager deux réactions différentes: a) Répéter l'essai jusqu'à l'ouverture correcte du fichier #include <stdio.h> main() { FILE *P_FICHIER; /* pointeur sur FILE */ char NOM_FICHIER[30]; /* nom du fichier */ . . . do { printf("Entrez le nom du fichier : "); scanf("%s", NOM_FICHIER); P_FICHIER = fopen(NOM_FICHIER, "w"); /* ou bien */ /* P_FICHIER = fopen(NOM_FICHIER, "r"); */ if (!P_FICHIER) printf("aERREUR: Impossible d'ouvrir " "le fichier: %s.n", NOM_FICHIER); } while (!P_FICHIER); . . . fclose(P_FICHIER); return 0; }
  • 212. b) Abandonner le programme en retournant un code d'erreur non nul - exit #include <stdio.h> #include <stdlib.h> main() { FILE *P_FICHIER; /* pointeur sur FILE */ char NOM_FICHIER[30]; /* nom du fichier */ . . . printf("Entrez le nom du fichier : "); scanf("%s", NOM_FICHIER); P_FICHIER = fopen(NOM_FICHIER, "w"); /* ou bien */ /* P_FICHIER = fopen(NOM_FICHIER, "r"); */ if (!P_FICHIER) { printf("aERREUR: Impossible d'ouvrir " "le fichier: %s.n", NOM_FICHIER); exit(-1); /* Abandonner le programme en */ } /* retournant le code d'erreur -1 */ . . . fclose(P_FICHIER); return 0; } 11.5. Lire et écrire dans des fichiers séquentiels Fichiers texte
  • 213. Les fichiers que nous employons dans ce manuel sont des fichiers texte, c.-à-d. toutes les informations dans les fichiers sont mémorisées sous forme de chaînes de caractères et sont organisées en lignes. Même les valeurs numériques (types int, float, double, ...) sont stockées comme chaînes de caractères. Pour l'écriture et la lecture des fichiers, nous allons utiliser les fonctions standard fprintf, fscanf, fputc et fgetc qui correspondent à printf, scanf, putchar et getchar si nous indiquons stdout respectivement stdin comme fichiers de sortie ou d'entrée. 11.5.1. Traitement par enregistrements Les fichiers texte sont généralement organisés en lignes, c.-à-d. la fin d'une information dans le fichier est marquée par le symbole 'n': Attention ! Pour pouvoir lire correctement les enregistrements dans un fichier séquentiel, le programmeur doit connaître l'ordre des différentes rubriques (champs) à l'intérieur des enregistrements. a) Ecrire une information dans un fichier séquentiel Ecrire dans un fichier séquentiel en langage algorithmique
  • 214. écrire <Nom>:<Expr1> écrire <Nom>:<Expr2> ... écrire <Nom>:<ExprN> *) <Nom> est une chaîne de caractères constante ou une variable de type chaîne qui représente le nom du fichier dans lequel on veut écrire. *) <Expr1>, <Expr2>, ... , <ExprN> représentent les rubriques qui forment un enregistrement et dont les valeurs respectives sont écrites dans le fichier. Ecrire dans un fichier séquentiel en langage C - fprintf fprintf( <FP>, "<Form1>n", <Expr1>); fprintf( <FP>, "<Form2>n", <Expr2>); ... fprintf( <FP>, "<FormN>n", <ExprN>); ou bien fprintf(<FP>,"<Form1>n<Form2>n...n<FormN> n", <Expr1>, <Expr2>, ... , <ExprN>); *) <FP> est un pointeur du type FILE* qui est relié au nom du fichier cible. *) <Expr1>, <Expr2>, ... , <ExprN> représentent les rubriques qui forment un enregistrement et dont les valeurs respectives sont écrites dans le fichier. *) <Form1>, <Form2>, ... , <FormN> représentent les spécificateurs de format pour l'écriture des différentes rubriques (voir chapitre 4.3.).
  • 215. Remarque L'instruction fprintf(stdout, "Bonjourn"); est identique à printf("Bonjourn"); Attention ! Notez que fprintf (et printf) écrit toutes les chaînes de caractères sans le symbole de fin de chaîne '0'. Dans les fichiers texte, il faut ajouter le symbole de fin de ligne 'n' pour séparer les données. b) Lire une information dans un fichier séquentiel Lire dans un fichier séquentiel en langage algorithmique lire <Nom>:<Var1> lire <Nom>:<Var2> ... lire <Nom>:<VarN> *) <Nom> est une chaîne de caractères constante ou une variable de type chaîne qui représente le nom du fichier duquel on veut lire. *) <Var1>, <Var2>, ... , <VarN> représentent les variables qui vont recevoir les valeurs des différentes rubriques d'un enregistrement lu dans le fichier. Lire dans un fichier séquentiel en langage C - fscanf
  • 216. fscanf( <FP>, "<Form1>n", <Adr1>); fscanf( <FP>, "<Form2>n", <Adr2>); ... fscanf( <FP>, "<FormN>n", <AdrN>); ou bien fscanf(<FP>,"<Form1>n<Form2>n... n<FormN>n", <Adr1>, <Adr2>, ... , <AdrN>); *) <FP> est un pointeur du type FILE* qui est relié au nom du fichier à lire. *) <Adr1>, <Adr2>, ... , <AdrN> représentent les adresses des variables qui vont recevoir les valeurs des différentes rubriques d'un enregistrement lu dans le fichier. * <Form1>, <Form2>, ... , <FormN> représentent les spécificateurs de format pour la lecture des différentes rubriques (voir chapitre 4.4.). Remarque L'instruction fscanf(stdin, "%dn", &N); est identique à scanf("%dn", &N); Attention ! Pour les fonctions scanf et fscanf tous les signes d'espacement sont équivalents comme séparateurs. En conséquence, à l'aide de fscanf, il nous sera impossible de lire toute une phrase dans laquelle les mots sont séparés par des espaces.
  • 217. 11.5.2. Traitement par caractères La manipulation de fichiers avec les instructions fprintf et fscanf n'est pas assez flexible pour manipuler de façon confortable des textes écrits. Il est alors avantageux de traiter le fichier séquentiellement caractère par caractère. a) Ecrire un caractère dans un fichier séquentiel - fputc fputc( <C> , <FP> ); fputc transfère le caractère indiqué par <C> dans le fichier référencé par <FP> et avance la position de la tête de lecture/ écriture au caractère suivant. *) représente un caractère (valeur numérique de 0 à 255) ou le symbole de fin de fichier EOF (voir 11.5.3.). *) <FP> est un pointeur du type FILE* qui est relié au nom du fichier cible. Remarque L'instruction fputc('a', stdout); est identique à putchar('a'); b) Lire un caractère dans un fichier séquentiel - fgetc <C> = fgetc( <FP> ); fgetc fournit comme résultat le prochain caractère du fichier référencé par <FP> et avance la position de la tête de lecture/ écriture au caractère suivant. A la fin du fichier, fgets retourne EOF (voir 11.5.3.).
  • 218. <C> représente une variable du type int qui peut accepter une valeur numérique de 0 à 255 ou le symbole de fin de fichier EOF. <FP> est un pointeur du type FILE* qui est relié au nom du fichier à lire. Remarque L'instruction C = fgetc(stdin); est identique à C = getchar(); 11.5.3. Détection de la fin d'un fichier séquentiel Lors de la fermeture d'un fichier ouvert en écriture, la fin du fichier est marquée automatiquement par le symbole de fin de fichier EOF (End Of File). Lors de la lecture d'un fichier, les fonctions finfichier(<Nom>) respectivement feof(<FP>) nous permettent de détecter la fin du fichier: Détection de la fin d'un fichier en langage algorithmique finfichier( <Nom> ) finfichier retourne la valeur logique vrai, si la tête de lecture du fichier référencé par <Nom> est arrivée à la fin du fichier; sinon la valeur logique du résultat est faux. <Nom> est une chaîne de caractères constante ou une variable de type chaîne qui représente le nom du fichier duquel on veut lire. Détection de la fin d'un fichier en langage C - feof
  • 219. feof( <FP> ); feof retourne une valeur différente de zéro, si la tête de lecture du fichier référencé par <FP> est arrivée à la fin du fichier; sinon la valeur du résultat est zéro. <FP> est un pointeur du type FILE* qui est relié au nom du fichier à lire. Pour que la fonction feof détecte correctement la fin du Atten fichier, il faut qu'après la lecture de la dernière donnée du fichier, la tête de lecture arrive jusqu'à la position de la marque EOF. Nous obtenons cet effet seulement si nous terminons aussi la chaîne de format de fscanf par un retour à la ligne 'n' (ou par un autre signe d'espacement). Exemple Une boucle de lecture typique pour lire les enregistrements d'un fichier séquentiel référencé par un pointeur FP peut avoir la forme suivante: while (!feof(FP)) { fscanf(FP, "%sn ... n", NOM, ... ); . . . } Exemple Le programme suivant lit et affiche le fichier "C:AUTOEXEC. BAT" en le parcourant caractère par caractère:
  • 220. #include <stdio.h> #include <stdlib.h> main() { FILE *FP; FP = fopen("C:AUTOEXEC.BAT", "r"); if (!FP) { printf("Impossible d'ouvrir le fichiern"); exit(-1); } while (!feof(FP)) putchar(fgetc(FP)); fclose(FP); return 0; } Dans une chaîne de caractères constante, il faut indiquer le symbole '' (back-slash) par '', pour qu'il ne soit pas confondu avec le début d'une séquence d'échappement (p.ex: n, t, a, ...). 11.6. Résumé sur les fichiers Langage algorithmique C Ouverture en ouvrir <Nom> en <FP> = fopen(<Nom>,"w"); écriture écriture Ouverture en lecture ouvrir <Nom> en lecture <FP> = fopen(<Nom>,"r"); Fermeture fermer <Nom> fclose(<FP>); Fonction fin de finfichier(<Nom>) feof(<FP>) fichier fprintf(<FP>,"...",<Adr>); Ecriture écrire <Nom>:<Exp> fputc(<C>, <FP>); fscanf(<FP>,"...",<Adr>); Lecture lire <Nom>:<Var> <C> = fgetc(<FP>); 11.7. Mise à jour d'un fichier séquentiel en C
  • 221. Dans ce chapitre, nous allons résoudre les problèmes standards sur les fichiers, à savoir: *) l'ajoute d'un enregistrement à un fichier *) la suppression d'un enregistrement dans un fichier *) la modification d'un enregistrement dans un fichier Comme il est impossible de lire et d'écrire en même temps dans un fichier séquentiel, les modifications doivent se faire à l'aide d'un fichier supplémentaire. Nous travaillons donc typiquement avec au moins deux fichiers: l'ancien fichier ouvert en lecture et le nouveau fichier ouvert en écriture: 11.7.1. Ajouter un enregistrement à un fichier Nous pouvons ajouter le nouvel enregistrement à différentes positions dans le fichier: a) Ajoute à la fin du fichier L'ancien fichier est entièrement copié dans le nouveau fichier, suivi du nouvel enregistrement.
  • 222. b) Ajoute au début du fichier L'ancien fichier est copié derrière le nouvel enregistrement qui est écrit en premier lieu. c) Insertion dans un fichier trié relativement à une rubrique commune des enregistrements Le nouveau fichier est créé en trois étapes: -) copier les enregistrements de l'ancien fichier qui précèdent le nouvel enregistrement, -) écrire le nouvel enregistrement, -) copier le reste des enregistrements de l'ancien fichier.
  • 223. Le programme suivant effectue l'insertion d'un enregistrement à introduire au clavier dans un fichier trié selon la seule rubrique de ses enregistrements: le nom d'une personne. Le programme inclut en même temps les solutions aux deux problèmes précédents. La comparaison lexicographique des noms des personnes se fait à l'aide de la fonction strcmp. Solution en C #include <stdio.h> #include <string.h> main() { /* Déclarations : */ /* Noms des fichiers et pointeurs de référence */ char ANCIEN[30], NOUVEAU[30]; FILE *INFILE, *OUTFILE; /* Autres variables */ char NOM_PERS[30], NOM_AJOUT[30]; int TROUVE; /* Ouverture de l'ancien fichier en lecture */ do { printf("Nom de l'ancien fichier : "); scanf("%s", ANCIEN);
  • 224. INFILE = fopen(ANCIEN, "r"); if (!INFILE) printf("aERREUR: Impossible d'ouvrir " "le fichier: %s.n", ANCIEN); } while (!INFILE); /* Ouverture du nouveau fichier en écriture */ do { printf("Nom du nouveau fichier : "); scanf("%s", NOUVEAU); OUTFILE = fopen(NOUVEAU, "w"); if (!OUTFILE) printf("aERREUR: Impossible d'ouvrir " "le fichier: %s.n", NOUVEAU); } while (!OUTFILE); /* Saisie de l'enregistrement à insérer */ printf("Enregistrement à insérer : "); scanf("%s",NOM_AJOUT); /* Traitement */ TROUVE = 0; /* Copie des enregistrements dont le nom */ /* précéde lexicogr. celui à insérer.*/ while (!feof(INFILE) && !TROUVE) { fscanf(INFILE, "%sn", NOM_PERS); if (strcmp(NOM_PERS, NOM_AJOUT) > 0) TROUVE = 1; else fprintf(OUTFILE, "%sn", NOM_PERS); } /* Ecriture du nouvel enregistrement, */ fprintf(OUTFILE, "%sn", NOM_AJOUT);
  • 225. /* suivi du dernier enregistrement lu. */ if (TROUVE) fprintf(OUTFILE, "%sn", NOM_PERS); /* Copie du reste des enregistrements */ while (!feof(INFILE)) { fscanf(INFILE, "%sn", NOM_PERS); fprintf(OUTFILE, "%sn", NOM_PERS); } /* Fermeture des fichiers */ fclose(OUTFILE); fclose(INFILE); return 0; } 11.7.2. Supprimer un enregistrement dans un fichier Le nouveau fichier est créé en copiant tous les enregistrements de l'ancien fichier qui précèdent l'enregistrement à supprimer et tous ceux qui le suivent: Solution en C
  • 226. #include <stdio.h> #include <string.h> main() { /* Déclarations : */ /* Noms des fichiers et pointeurs de référence */ char ANCIEN[30], NOUVEAU[30]; FILE *INFILE, *OUTFILE; /* Autres variables */ char NOM_PERS[30], NOM_SUPPR[30]; /* Ouverture de l'ancien fichier en lecture */ do { printf("Nom de l'ancien fichier : "); scanf("%s", ANCIEN); INFILE = fopen(ANCIEN, "r"); if (!INFILE) printf("aERREUR: Impossible d'ouvrir " "le fichier: %s.n", ANCIEN); } while (!INFILE); /* Ouverture du nouveau fichier en écriture */ do { printf("Nom du nouveau fichier : "); scanf("%s", NOUVEAU); OUTFILE = fopen(NOUVEAU, "w"); if (!OUTFILE) printf("aERREUR: Impossible d'ouvrir " "le fichier: %s.n", NOUVEAU); } while (!OUTFILE); /* Saisie de l'enregistrement à supprimer */
  • 227. printf("Enregistrement à supprimer : "); scanf("%s",NOM_SUPPR); /* Traitement */ /* Copie de tous les enregistrements à */ /* l'exception de celui à supprimer. */ while (!feof(INFILE)) { fscanf(INFILE, "%sn", NOM_PERS); if (strcmp(NOM_PERS, NOM_SUPPR) != 0) fprintf(OUTFILE, "%sn", NOM_PERS); } /* Fermeture des fichiers */ fclose(OUTFILE); fclose(INFILE); return 0; } 11.7.3. Modifier un enregistrement dans un fichier Le nouveau fichier est créé de tous les enregistrements de l'ancien fichier qui précèdent l'enregistrement à modifier, de l'enregistrement modifié et de tous les enregistrements qui suivent l'enregistrement à modifier dans l'ancien fichier: Solution en C
  • 228. #include <stdio.h> #include <string.h> main() { /* Déclarations : */ /* Noms des fichiers et pointeurs de référence */ char ANCIEN[30], NOUVEAU[30]; FILE *INFILE, *OUTFILE; /* Autres variables */ char NOM_PERS[30], NOM_MODIF[30], NOM_NOUV [30]; /* Ouverture de l'ancien fichier en lecture */ do { printf("Nom de l'ancien fichier : "); scanf("%s", ANCIEN); INFILE = fopen(ANCIEN, "r"); if (!INFILE) printf("aERREUR: Impossible d'ouvrir " "le fichier: %s.n", ANCIEN); } while (!INFILE); /* Ouverture du nouveau fichier en écriture */ do { printf("Nom du nouveau fichier : "); scanf("%s", NOUVEAU); OUTFILE = fopen(NOUVEAU, "w"); if (!OUTFILE) printf("aERREUR: Impossible d'ouvrir " "le fichier: %s.n", NOUVEAU); }
  • 229. while (!OUTFILE); /* Saisie de l'enregistrement à modifier, */ printf("Enregistrement à modifier : "); scanf("%s",NOM_MODIF); /* et de sa nouvelle valeur. */ printf("Enregistrement nouveau : "); scanf("%s",NOM_NOUV); /* Traitement */ /* Copie de tous les enregistrements en */ /* remplaçant l'enregistrement à modifier */ /* par sa nouvelle valeur. */ while (!feof(INFILE)) { fscanf(INFILE, "%sn", NOM_PERS); if (strcmp(NOM_PERS, NOM_MODIF) = 0) fprintf(OUTFILE, "%sn", NOM_NOUV); else fprintf(OUTFILE, "%sn", NOM_PERS); } /* Fermeture des fichiers */ fclose(OUTFILE); fclose(INFILE); return 0; }