Langage C (ANSI)

102
Langage procédural ANSI C Philippe Robillard

description

Syllabus C (programmation procédurale)

Transcript of Langage C (ANSI)

Page 1: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 1

Langage procédural ANSI C

Philippe Robillard

Page 2: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 2

Tables des matières 1. Conventions d'écriture .................................................................................................................................................... 6 2. Avertissements ................................................................................................................................................................ 7 

2.1. Origine du cours .................................................................................................................................................. 7 3. Références pour le langage ............................................................................................................................................. 7 

3.1. Sur internet .......................................................................................................................................................... 7 3.2. Bibliographie ....................................................................................................................................................... 7 

3.2.1. La bible ..................................................................................................................................................... 7 3.2.2. Autres ouvrages ........................................................................................................................................ 7 

4. Avant de commencer ...................................................................................................................................................... 8 4.1. Histoire de C ........................................................................................................................................................ 8 4.2. La compilation ..................................................................................................................................................... 8 4.3. Préalable visuel .................................................................................................................................................... 9 

4.3.1. Le schéma du traitement rationnel de l’information ................................................................................ 9 4.3.2. Application du schéma aux langages informatiques et au C en particulier .............................................. 9 

4.4. Préalable syntaxique .......................................................................................................................................... 10 4.4.1. Les identificateurs .................................................................................................................................. 10 4.4.2. Les commentaires ................................................................................................................................... 10 4.4.3. La marque de continuation ..................................................................................................................... 10 4.4.4. A propos de longueurs............................................................................................................................ 11 4.4.5. Les mots-clefs ........................................................................................................................................ 12 4.4.6. Expressions et instructions ..................................................................................................................... 12 4.4.7. Structure d’un programme ..................................................................................................................... 13 4.4.8. Conventions d'écriture d'un programme C ............................................................................................. 14 

5. Les composants du langage .......................................................................................................................................... 15 5.1. Les instructions d'entrées-sorties classiques ...................................................................................................... 15 

5.1.1. Les entrées-sorties de caractères ............................................................................................................ 15 5.1.2. Les entrées-sorties de chaînes de caractères ........................................................................................... 16 5.1.3. Les entrées-sorties formatées ................................................................................................................. 17 

5.1.3.1. Les sorties avec printf ................................................................................................................. 17 5.1.3.2. Les entrées avec scanf ................................................................................................................. 18 5.1.3.3. Exemples d’entrées-sorties formatées ......................................................................................... 19 

5.1.4. Le problème du retour chariot résiduel .................................................................................................. 20 5.1.5. Autres entrées-sorties formatées ............................................................................................................ 21 

5.1.5.1. Lecture et écriture formatées des fichiers ................................................................................... 21 5.1.5.2. Lecture et écriture formatées de la mémoire ............................................................................... 21 

5.2. Les types de données prédéfinis ........................................................................................................................ 22 5.2.1. Le type caractère .................................................................................................................................... 22 5.2.2. Les types entiers ..................................................................................................................................... 23 5.2.3. Les types réels ........................................................................................................................................ 24 

5.3. Les expressions littérales ................................................................................................................................... 25 5.3.1. Les expressions littérales d’entiers ......................................................................................................... 25 5.3.2. Les expressions littérales de réels .......................................................................................................... 25 5.3.3. Les expressions littérales de caractères .................................................................................................. 25 5.3.4. Les expressions littérales de chaînes de caractères ................................................................................ 26 

5.4. Les variables ...................................................................................................................................................... 26 5.4.1. Durée de vie des variables ...................................................................................................................... 26 

5.4.1.1. Les variables permanentes .......................................................................................................... 26 5.4.1.2. Les variables temporaires ............................................................................................................ 27 

5.4.2. Portée des variables ................................................................................................................................ 27 5.4.2.1. Variables globales ....................................................................................................................... 27 5.4.2.2. Variables locales ......................................................................................................................... 28 

5.4.2.2.a. La variable « static » ........................................................................................................ 28 5.4.3. Les variables « volatiles » ...................................................................................................................... 28 5.4.4. Les constantes ........................................................................................................................................ 29 

5.4.4.1. Ne pas confondre « macro » et « constante » .............................................................................. 29 5.4.4.2. Déclaration et initialisation ......................................................................................................... 29 

5.4.4.2.a. Pointeur sur une valeur constante .................................................................................... 29 5.4.4.2.b. Pointeur d’une valeur constante ....................................................................................... 29 

Page 3: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 3

5.4.4.2.c. Pointeur constant d’une valeur quelconque ..................................................................... 30 5.4.4.2.d. Pointeur constant d’une valeur constante ........................................................................ 30 5.4.4.2.e. Pointeur constant sur une valeur constante ...................................................................... 30 5.4.4.2.f. Tableaux et constantes ...................................................................................................... 30 

5.5. Les organisations de données ............................................................................................................................ 30 5.5.1. Types ou structures composées .............................................................................................................. 31 

5.5.1.1. Les tableaux ................................................................................................................................ 31 5.5.1.2. Les chaînes de caractères ............................................................................................................ 32 5.5.1.3. Les énumérations ........................................................................................................................ 32 

5.5.2. Types ou structures complexes .............................................................................................................. 33 5.5.2.1. Les structures .............................................................................................................................. 33 5.5.2.2. Les champs de bits ...................................................................................................................... 34 5.5.2.3. Les unions ................................................................................................................................... 35 

5.6. Les opérateurs .................................................................................................................................................... 36 5.6.1. L'affectation............................................................................................................................................ 36 5.6.2. La concaténation .................................................................................................................................... 36 5.6.3. Les opérateurs arithmétiques .................................................................................................................. 36 5.6.4. Les formes contractées de l’affectation .................................................................................................. 37 5.6.5. Les opérateurs relationnels ..................................................................................................................... 37 5.6.6. Les opérateurs logiques .......................................................................................................................... 38 5.6.7. Les opérateurs binaires (bit à bit) ........................................................................................................... 38 5.6.8. Les opérateurs d'incrémentation et de décrémentation ........................................................................... 39 5.6.9. La virgule, opérateur séparateur d’expression ....................................................................................... 39 5.6.10. L'alternative contractée ou l’opérateur conditionnel ternaire ............................................................... 39 5.6.11. L'opérateur de conversion de type ........................................................................................................ 40 5.6.12. Les opérateurs de référencement et de déréférencement ...................................................................... 40 5.6.13. Opérateur d’accès et opérateurs de traversée ....................................................................................... 40 5.6.14. Règles de priorité des opérateurs ......................................................................................................... 40 

5.7. Les structures de contrôle .................................................................................................................................. 41 5.7.1. Définitions préalables ............................................................................................................................. 41 

5.7.1.1. La séquence ................................................................................................................................. 41 5.7.1.2. La rupture de séquence ............................................................................................................... 41 5.7.1.3. L’alternative ................................................................................................................................ 41 5.7.1.4. Les itérations ............................................................................................................................... 41 

5.7.2. Les ruptures de séquences ...................................................................................................................... 42 5.7.2.1. L’instruction de branchement break ........................................................................................... 42 5.7.2.2. L’instruction de branchement continue ....................................................................................... 42 

5.7.3. L’alternative ........................................................................................................................................... 43 5.7.4. La sélection ............................................................................................................................................ 44 5.7.5. Les itérations .......................................................................................................................................... 45 

5.7.5.1. Les boucles logiques ................................................................................................................... 45 5.7.5.2. La boucle arithmétique ................................................................................................................ 45 

5.7.6. La récursivité .......................................................................................................................................... 46 6. Aspects avancés ............................................................................................................................................................ 48 

6.1. Les fonctions ..................................................................................................................................................... 48 6.1.1. Définition d’une fonction ....................................................................................................................... 48 

6.1.1.1. Exemples de définitions .............................................................................................................. 48 6.1.2. Déclaration ou prototypage .................................................................................................................... 49 

6.1.2.1. Exemples de déclarations ............................................................................................................ 49 6.1.3. Appel d'une fonction .............................................................................................................................. 49 

6.1.3.1. Exemples d’appels ...................................................................................................................... 49 6.1.4. Transmission des paramètres par valeurs et par pointeurs ..................................................................... 49 

6.1.4.1. Illustration du passage par valeur ................................................................................................ 50 6.1.4.2. Illustration du passage par pointeur ............................................................................................ 51 6.1.4.3. Passage d’un tableau à une fonction ........................................................................................... 52 6.1.4.4. Le qualificatif const et les arguments des fonctions ................................................................... 52 

6.1.5. La fonction main .................................................................................................................................... 53 6.1.6. Fonctions avec un nombre variable de paramètres ................................................................................ 55 

6.2. Les pointeurs ..................................................................................................................................................... 56 6.2.1. Notion de pointeur .................................................................................................................................. 56 

Page 4: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 4

6.2.2. Adresse et valeur d'un objet ................................................................................................................... 56 6.2.3. Arithmétique des pointeurs .................................................................................................................... 57 6.2.4. Allocation dynamique ............................................................................................................................ 59 

6.2.4.1. Allocation dynamique et non dynamique : comparaison importante .......................................... 60 6.2.4.2. Allocation dynamique avec mise à zéro ...................................................................................... 60 6.2.4.3. Réallocation dynamique .............................................................................................................. 61 6.2.4.4. Libération de la mémoire ............................................................................................................ 61 

6.2.5. Pointeurs et tableaux .............................................................................................................................. 62 6.2.5.1. Pointeurs et tableaux à plusieurs dimensions .............................................................................. 62 

6.2.6. Relations entre un tableau et un vecteur ................................................................................................. 64 6.2.6.1. Les relations en formules ............................................................................................................ 66 

6.2.7. Pointeurs et chaînes de caractères .......................................................................................................... 67 6.2.8. Pointeurs et structures ............................................................................................................................ 69 

6.2.8.1. Pointeur sur une structure ........................................................................................................... 69 6.2.8.2. Les structures dites auto-référencées .......................................................................................... 70 6.2.8.3. La liste chaînée ........................................................................................................................... 70 

6.2.8.3.a. Création et parcours de Pile et de File ............................................................................. 72 6.2.8.4. L’arbre binaire ............................................................................................................................ 73 

6.2.9. Les pointeurs sur fonctions .................................................................................................................... 77 6.2.9.1. Exemple d’utilisation des fonctions qsort et bsearch .................................................................. 79 

6.3. Les fichiers ........................................................................................................................................................ 81 6.3.1. Ouverture d’un fichier ............................................................................................................................ 81 6.3.2. Fermeture d’un fichier ............................................................................................................................ 81 6.3.3. Exemples d’ouverture et fermeture ........................................................................................................ 81 6.3.4. Contrôle de l’ouverture d’un fichier ....................................................................................................... 82 6.3.5. Les fichiers textes ................................................................................................................................... 83 

6.3.5.1. Lecture et écriture par caractères ................................................................................................ 83 6.3.5.1.a. Relecture d'un caractère ................................................................................................... 83 

6.3.5.2. Lecture et écriture par lignes ....................................................................................................... 84 6.3.5.3. Lecture et écriture formatées ....................................................................................................... 84 

6.3.6. Les fichiers binaires ............................................................................................................................... 85 6.3.6.1. L’accès direct .............................................................................................................................. 86 

6.3.6.1.a. Accès direct sur un fichier d’entiers ................................................................................. 86 6.3.6.1.b. Accès direct sur un fichier de personnes .......................................................................... 87 

6.3.7. Outils divers ........................................................................................................................................... 88 6.3.7.1. Forcer l’enregistrement des données ........................................................................................... 88 6.3.7.2. Taille d’un fichier........................................................................................................................ 88 6.3.7.3. Réinitialisation des indicateurs ................................................................................................... 88 6.3.7.4. Renommer et supprimer .............................................................................................................. 88 

7. Directives au préprocesseur .......................................................................................................................................... 89 7.1. L’inclusion ......................................................................................................................................................... 89 7.2. Les définitions ................................................................................................................................................... 89 

7.2.1. Définition de constantes symboliques .................................................................................................... 89 7.2.2. Définition de macros .............................................................................................................................. 89 

7.3. La compilation conditionnelle ........................................................................................................................... 90 7.3.1. Condition liée à la valeur d'une expression ............................................................................................ 90 7.3.2. Condition liée à l'existence d'un symbole .............................................................................................. 91 

8. Extraits des librairies standards .................................................................................................................................... 92 8.1. Entrées-sorties : <stdio.h> ................................................................................................................................. 92 

8.1.1. Les opérations sur les fichiers ................................................................................................................ 92 8.1.2. Les sorties mises en forme ..................................................................................................................... 94 8.1.3. Les entrées mises en forme .................................................................................................................... 94 8.1.4. Les fonctions d'entrées-sorties de caractères .......................................................................................... 94 8.1.5. Les fonctions d'entrées-sorties directes .................................................................................................. 95 8.1.6. Les fonctions de gestion des erreurs ...................................................................................................... 95 

8.2. Les tests de catégories de caractères : <ctype.h> .............................................................................................. 96 8.3. Les fonctions de traitement des chaînes : <string.h> ........................................................................................ 96 8.4. Les fonctions mathématiques : <math.h> .......................................................................................................... 98 8.5. Les fonctions utilitaires : <stdlib.h> .................................................................................................................. 98 8.6. Date et heure : <time.h> .................................................................................................................................. 100 

Page 5: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 5

9. Quelques énoncés d’exercices .................................................................................................................................... 101 9.1. Les types de données ....................................................................................................................................... 101 9.2. Les instructions de branchement ..................................................................................................................... 101 9.3. Les paramètres de la fonction main ................................................................................................................. 101 9.4. Les pointeurs ................................................................................................................................................... 101 9.5. Les fichiers ...................................................................................................................................................... 102 

Page 6: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 6

1. Conventions d'écriture Afin de permettre au lecteur de différencier rapidement diverses parties du texte, la rédaction et la mise en page de ce cours sont réalisées autant que possible dans le respect des règles suivantes. Mots du langage et mots du programmeur. Les mots des langages sont écrits dans le texte courant en Courier New gras. Il en est de même pour les mots utilisés par le programmeur dans les lignes de codes des exemples, mais ceux-ci sont francisés. Ex. :

… l’effet de masque est assuré car la variable globale Cinq et son homonyme locale … … les déclarations static ne sont …

Programme complet et extraits. Les lignes de codes des exemples sont cadrées à gauche lorsqu'elles constituent un programme complet, même si son code est entrecoupé de textes explicatifs, et elles sont décalées d'un retrait vers la droite lorsqu'elles constituent un extrait de programme. Exemple de programme complet : #include <stdio.h> main() { printf("Bonjour\n");

return 0; } Exemple d'extrait de programme :

main() { printf("Bonjour\n");

return 0; }

Codes et commentaires. Les lignes et fins de lignes qui sont des commentaires sont écrites en plus petit et en italique. Certaines d'entre elles présentent des résultats d'affichages : l'italique est supprimé pour les caractères effectivement affichés. Ex. : /* La ligne de code suivante a pour but d'afficher le mot Bonjour à l'écran */

printf("Bonjour\n"); /* Affichage : Bonjour */ Longues lignes de codes. Les lignes de codes trop longues pour tenir sur la largeur de la page sont continuées cadrées à droite à la ligne suivante. Cette typographie signifie donc que le code ainsi représenté doit être programmé sur une seule ligne. Ex. :

printf("Voici les 5 premiers éléments du tableau : %d %d %d %d %d\n", Tab[0], Tab[1], Tab[2], Tab[3], Tab[4]);

Divers, abus de langage, particularités. Les mots dont le sens premier est détourné, ceux représentant pour la première fois un objet ou un concept particulier, ceux dont la définition est donnée plus loin, ou simplement ceux qui méritent une attention spéciale dans le contexte, et qui ne sont pas des mots du langage, ni des extraits des lignes de codes des exemples, sont écrits en italique dans le texte courant. Ex. :

… la variable de type pointeur est bien traitée comme telle … … seule la valeur d'un membre de type valeur est conservée …

Page 7: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 7

2. Avertissements La programmation procédurale consiste essentiellement en une découpe structurée d’un programme en morceaux constituant des fonctions et des procédures. Seules les fonctions existent en C. Une fonction générale (nommée main en C ou program en Pascal) appelle des fonctions secondaires, elles-mêmes en appelant d’autres, et ainsi de suite jusqu’à l’exécution de la tâche la plus infime. Le présent document traite du langage C dans le contexte de la programmation procédurale et, sauf mention particulière, les exemples sont donnés en C normalisé (ANSI X3.159, 1989), excluant notamment les notations C++. Les programmes repris dans ce cours sont censés être illustratifs et non être à toute épreuve. Bien que généralement de bonne facture, ils n’ont pas subi de tests approfondis. Le programmeur souhaitant en intégrer dans un développement aura soin de pratiquer les tests nécessaires à la fiabilité de son application.

2.1. Origine du cours Le présent syllabus est inspiré du cours de langage C de Anne Canteaut, chargée de recherche à l’INRIA-Rocquencourt (Institut de recherche en informatique et en automatique, F-78153 Le Chesnay). Des adaptations ont été apportées au cours d’origine, et des exemples ajoutés, pour répondre aux spécificités des cours du « Bachelier en informatique de gestion » de l’enseignement de promotion sociale et en formation continuée.

3. Références pour le langage

3.1. Sur internet http://www-rocq.inria.fr/codes/Anne.Canteaut http://perso.efrei.fr/~charroux

3.2. Bibliographie 3.2.1. La bible Kernighan (B.W.) et Richie (D.M.). – Le langage C. – Manuels informatiques Masson – Prentice Hall, 1990, 2ème

éd., (traduction de « The C programming language », 2ème édition, incluant la norme ANSI C). 3.2.2. Autres ouvrages Delannoy (C.). – Programmer en langage C. – Eyrolles, 1992. Harbison (S.) et Steele (G.) – Langage C Manuel de référence – Manuels informatiques Masson – Paris – 1990.

Page 8: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 8

4. Avant de commencer

4.1. Histoire de C Dans les années 60, Martin Richards développe le BCPL, un langage de programmation spécialement conçu pour l'écriture de logiciels de base pour ordinateurs. En 1970, Ken Thompson, un chercheur des laboratoires Bell, s’inspire du BCPL pour définir un nouveau langage pour le premier système Unix. Il crée alors le langage B sur un ordinateur DEC PDP-7. Peu satisfait de ce langage non typé et interprété, K. Thompson redéfinit son langage pour produire le NB (pour "New B") toujours peu satisfaisant. En 1972, un autre chercheur des laboratoires Bell, Dennis M. Ritchie, reprend les travaux de K. Thompson et développe le langage C pour le système Unix sur DEC PDP-11. Ce langage C est retravaillé dans les années suivantes par Dennis M. Ritchie et Brian W. Kernighan avec pour objectif de créer un langage qui permette le développement d'un système d'exploitation Unix portable. En 1978, ils publient "The C programming language" et donnent ainsi une définition sommaire mais déjà importante du langage. Le C devenant de plus en plus populaire dans les années 80, plusieurs producteurs s’y intéressent, chacun à sa façon. De multiples compilateurs C comportant des extensions particulières font leur apparition. C’est ainsi qu’apparait la nécessité de donner une définition complète et stable du langage. En 1983, l' "American National Standards Institute" (ANSI) décide de la normalisation de ce langage; ce travail s'achève en 1989 par la définition de la norme ANSI X3.159 communément appelée l'ANSI C. La seconde version, intégrant la norme, du livre précédemment cité sort en 1990 et fait la part des choses entre le K&R C et l’ANSI C. Ce livre (dans sa seconde édition) fait aujourd'hui encore office de bible en la matière. En 1982, Bjarne Stroustrup crée sa première version du langage C++. Celui-ci est une extension du langage C afin de permettre la programmation orientée objet. L'ANSI C en cours d’élaboration a de toute évidence hérité de certains concepts de Bjarne Stroustrup. Plus tard, dans les années 95, Java prend place dans la lignée des langages dérivés du C. L'ANSI C reste à l'heure actuelle le langage incontournable dans bien des domaines. C’est, à peu d’exceptions près, le langage qui est décrit dans le présent document.

4.2. La compilation Le C est un langage compilé (par opposition aux langages interprétés). Cela signifie qu'un programme C est décrit par un fichier texte, appelé fichier source dont l’extension est idéalement C. Ce fichier n'étant évidemment pas exécutable par le microprocesseur, il faut le traduire en langage machine. Cette opération est effectuée par un programme appelé compilateur. La compilation se décompose en fait en 4 phases successives (les 3 premières étant enchaînées en une seule étape par la plupart des compilateurs) : La précompilation : le fichier source est analysé par le préprocesseur qui effectue des transformations purement textuelles (remplacement de chaînes de caractères, inclusion d'autres fichiers source ...). La compilation : la compilation proprement dite traduit le fichier généré par le préprocesseur en assembleur, c'est-à-dire en une suite d'instructions du microprocesseur qui utilisent des mnémoniques rendant la lecture possible. L'assemblage : cette opération transforme le code assembleur en un fichier binaire, c'est-à-dire en instructions directement compréhensibles par le processeur. Généralement, la compilation et l'assemblage se font dans la foulée, sauf si l'on spécifie explicitement que l'on veut le code assembleur. Le fichier produit par l'assemblage est appelé fichier objet. Son extension est OBJ. L'édition de liens : un programme est souvent séparé en plusieurs fichiers source, pour des raisons de clarté mais aussi parce qu'il fait généralement appel à des librairies de fonctions standard déjà écrites. Une fois chaque code source assemblé, il faut donc lier entre eux les différents fichiers objets. L'édition de liens produit alors un fichier dit exécutable. Son extension est EXE. Les environnements de développements actuels sont bien plus que de simples compilateurs. Ils intègrent éditeur de source, compilateur et assembleur, éditeur de liens, gestionnaire de projets, debugger, aide en ligne, … et font facilement oublier les différentes étapes, pourtant toujours présentes, qui conduisent du fichier source au fichier exécutable. Il reste au programmeur de s’assurer de la rigueur de son ide (Integrated Development Environment) eu égard à la norme ANSI et à effectuer les réglages éventuellement nécessaires, à moins d’appliquer lui-même les restrictions qui s’imposent. Outre le choix de la syntaxe (K&R, ANSI, C++, …) fixé dans les options des compilateurs, l’extension du fichier source (C ou CPP) influence aussi le travail de certains environnements de développements (Turbo C, Dev-Cpp, …). Le respect de la norme ANSI implique donc un réglage correct du compilateur et l’usage de l’extension C.

Page 9: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 9

4.3. Préalable visuel C’est une aide précieuse à l’utilisation d’un nouveau langage que d’y repérer les outils de base correspondant à chaque étape du cycle du traitement rationnel de l’information. 4.3.1. Le schéma du traitement rationnel de l’information (inspiré de von Neuman, mathématicien, 1903-1957) 4.3.2. Application du schéma aux langages informatiques et au C en particulier

UF

UT (processeur)

Mémoire de travail (RAM)

Périphériques

d’entrées

Périphériques

de sorties

Mémoire d’archivage

(Mém. de masse)

UC

UE US

char, int, float, typedef, struct, …

UC

UE

UT Instructions de contrôles et

moyens de traitements

Gestion des données. Types de données, variables,…

Instructions d’entrées

Instructions de sorties

Instructions « Fichiers »

US

UF + E/S

printf, putc, putchar, puts, … scanf, getc, getchar, gets, …

fscanf, fprintf, fgetc, fputc, fgets, fputs, fread, fwrite, fopen, fclose, …

if, switch, for, while, break, … Opérateurs : + - / * = ++ … Fonctions : sqrt, strlen, atoi, …

Page 10: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 10

4.4. Préalable syntaxique Un programme en langage C est constitué de composants élémentaires dont notamment : les identificateurs, les mots-clefs, les expressions littérales, les chaînes de caractères, les opérateurs, les signes de ponctuation.

On peut y ajouter les commentaires, mais ceux-ci sont enlevés par le préprocesseur. 4.4.1. Les identificateurs Le rôle d'un identificateur est de donner un nom à une entité du programme. Un identificateur peut désigner : un nom de variable, un nom de fonction, une constante, un type de données, une étiquette.

Un identificateur est une suite de caractères parmi : les lettres (minuscules ou majuscules, mais non accentuées), les chiffres, le ''blanc souligné'' (_).

Le premier caractère d'un identificateur ne peut pas être un chiffre. Par exemple, var1, tab_23 ou _deb sont des identificateurs valides tandis que 1i et i:j ne le sont pas. Un identificateur doit être aussi représentatif que possible de l’objet identifié. L’emploi de majuscules et minuscules dans leur orthographe permet une bonne lisibilité de l’identificateur (ex. TotalGeneral, TotGen, OldValue, …). Attention, les majuscules et minuscules étant différenciées, Total et total désignent deux variables différentes. Le compilateur peut tronquer les identificateurs au-delà d'une certaine longueur. Cette limite dépend des implémentations, mais tous les compilateurs modernes distinguent au moins 31 caractères. 4.4.2. Les commentaires Un commentaire, qui peut être constitué de nombreuses lignes de texte, débute par /* et se termine par */.

/* Ceci est un commentaire */

Il n’est pas permis d’imbriquer des commentaires. Aussi, lorsqu’on utilise la mise en commentaire pour isoler un morceau de programme, il faut veiller à ce que celui-ci ne contienne pas de commentaire. Attention, les commentaires désignés par // sont interdits en ANSI C. Cette notation est utilisée en C++, en php, … 4.4.3. La marque de continuation Bien que le compilateur C n’impose pas de limite en matière de longueur de ligne (il est possible d’écrire tout le code C d’un programme sur une seule ligne), la lisibilité des longues lignes de codes peut être améliorée par l’emploi du caractère de continuation. Le caractère de continuation est le \ seul en fin de ligne. Il permet l’écriture d’une longue ligne de code sur plusieurs lignes.

printf("Voici les 6 premiers éléments du tableau : %d %d %d %d %d %d\n", \ Tab[0], Tab[1], Tab[2], Tab[3], Tab[4], Tab[5]);

Attention, lorsque le caractère de continuation coupe une chaîne de caractères, celle-ci doit être continuée à la ligne suivante sans espaces, ni indentation. Ces espaces seraient incorporés dans la chaîne de caractères.

printf("Voici une longue, longue, très longue chaîne de caractères à afficher \ à l’écran");

Page 11: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 11

4.4.4. A propos de longueurs #include <stdio.h> int main() { int i; char TresLongNomDeVariableChaine[] = "C'est dingue de choisir des noms aussi longs !"; printf("\nVoici une phrase délibérément construite de façon anormalement longue afin de mettre en évidence le fait qu'un compilateur C n'impose pas, a priori, de limite en matière de longueur de ligne. Il faut savoir qu'à défaut de limite imposée, le bon sens programmeur doit lui dicter les limites qu'il convient de respecter. Il est par ailleurs possible que la paramétrisation de certains compilateurs limite la longueur d'une chaîne telle que celle-ci à, par exemple, 65.000 caractères. Les lignes printf ne sont pas des traitements de textes ! Cette ligne contient près de 600 caractères.");

printf("\nAfin de permettre une lisibilité confortable du source, \ on utilisera le\ncaractère de continuation de ligne \\. De plus, ces \ longues phrases s'affichant\nmal à l'écran, des \\n bien placés \ permettront un bon formatage de l'affichage."); printf("\nEn ce qui concerne les longues lignes de codes de quelque\n\ nature que ce soit, les \\ ne sont pas toujours nécessaires du fait que\n\ les espaces ne sont pris en compte lors de la compilation.\nPar exemple, \ la boucle suivante est écrite en dépit du bon sens.\nEt pourtant, elle \ tourne !"); for ( i=0; i < 5 ; i ++ ) printf( "\n%s", TresLongNomDeVariableChaine ) ; return 0; } La ligne de ±600 caractères (en clair) : printf("\nVoici une phrase délibérément construite de façon anormalement longue afin de mettre en évidence le fait qu'un compilateur C n'impose pas, a priori, de limite en matière de longueur de ligne. Il faut savoir qu'à défaut de limite imposée, le bon sens programmeur doit lui dicter les limites qu'il convient de respecter. Il est par ailleurs possible que la paramétrisation de certains compilateurs limite la longueur d'une chaîne telle que celle-ci à, par exemple, 65.000 caractères. Les lignes printf ne sont pas des traitements de textes ! Cette ligne contient près de 600 caractères."); Les résultats des deux autres printf, tels qu’ils se présentent à l’écran :

Phrase écrite en une seule ligne dans le code source. Elle contient près de 600 caractères et ne peut être reproduite sur cette page en une seule ligne. Elle est imprimée ici en Courier New de taille 2.

Page 12: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 12

4.4.5. Les mots-clefs Un certain nombre de mots, appelés mots-clefs, sont réservés pour le langage lui-même et ne peuvent pas être utilisés comme identificateurs. L'ANSI C compte 32 mots-clefs : auto, break, case, char, const, continue, default, do, double, else, enum, extern, float, for, goto, if, int, long, register, return, short, signed, sizeof, static, struct, switch, typedef, union, unsigned, void, volatile et while. Ces mots-clefs se regroupent en catégories telles que : les spécificateurs de stockage : auto, register, static, extern, typedef les identificateurs de type : char, double, enum, float, int, struct, union, void les modificateurs de type : long, short, signed, unsigned les qualificateurs de type : const, volatile les instructions de contrôle : break, case, continue, default, do, else, for, goto, if, switch, while les divers : return, sizeof

Il faut bien entendu aussi considérer comme réservé tout mot désignant une fonction, une constante ou une variable défini dans une librairie utilisée. Ainsi par exemple, printf qui est une des fonctions de la librairie stdio.h, est utilisée comme variable dans le programme suivant. #include <stdio.h> main() {

int printf = 0, i; for (i=0; i<10; i++) printf = printf + i; printf("%d", printf); /* Erreur, la fonction printf n’est plus connue ici */ return 0;

} Le compilateur génère une erreur lors du traitement de la ligne qui devrait produire l’affichage du résultat car il ne reconnaît pas la fonction printf. La déclaration locale d’une variable printf masque toute autre printf déclarée ailleurs, même dans une librairie ! 4.4.6. Expressions et instructions Un programme est essentiellement une suite d’instructions. Une instruction est constituée d’une ou de plusieurs expressions successives, éventuellement séparées par des virgules et/ou opérateurs, et dont l’écriture est terminée par un point-virgule. Une expression est une suite de composants élémentaires syntaxiquement correcte. Dans cette approche, les instructions de contrôle ne sont considérées que comme partie d’instruction, voire comme expression, puisque le point-virgule de clôture est celui qui termine la dernière instruction contrôlée. Dans la suite de ces pages, une instruction de contrôle est considérée comme instruction à part entière.

x = 0 /* expression */ (i >= 0) && (i < 10) && (p[i] != 0) /* expression */ (x != 0) /* expression */ z = y / x; /* instruction */ if (x != 0) /* instruction de contrôle */ if (x != 0) z = y / x; /* instruction */

Plusieurs instructions peuvent être rassemblées par des accolades {} pour former une instruction composée (ou bloc). C'est ainsi qu'on peut soumettre plusieurs instructions à un même contrôle.

if (x != 0) { z = y / x; t = y % x; }

Page 13: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 13

Une instruction composée d'un spécificateur de type et d'une liste d'identificateurs séparés par une virgule est une déclaration.

int a, b = 17, c; /* Trois entiers dont un initialisé à 17 */ double x = 2.38e4; /* Un réel double initialisé en notation scientifique */ char Message[80]; /* Une chaîne de 80 caractères */ int Carre(int); /* Une fonction qui reçoit un entier et retourne un entier */

4.4.7. Structure d’un programme La principale page de code d’un programme C est celle qui contient la fonction main(). Cette fonction, utilisée avec ou sans paramètres, est le point d’entrée de l’exécutable, le code de démarrage activé par le système d’exploitation aussitôt le programme installé en mémoire. C’est de cette fonction que sont appelées les principales fonctions secondaires, celles-ci en appelant d’autres à leur tour, et ainsi de suite jusqu’à l’exécution des tâches les plus élémentaires requises par l’application. Le programme peut être composé de nombreuses pages de codes, chacune contenant des outils spécifiques regroupés là en fonction de leur domaine d’activité ou selon la logique du programmeur. Des pages de codes sont incluses (#include) dans d’autres où elles sont utiles. Les programmations multi-fichiers ne sont pas nécessaires dans le cadre de ce cours, mais le mécanisme d’inclusion est quand même utilisé pour l’exploitation d’outils livrés dans des librairies du langage (stdio.h, stdlib.h, string.h, math.h, …). Les principaux outils des principales librairies sont énumérés, avec description et syntaxe d’usage, plus loin dans ce document sous le titre « Extraits des librairies standards ». La principale page de code d’un programme C se structure comme suit : Directives au préprocesseur (ou précompilateur) Déclaration des variables et constantes globales Déclaration des fonctions secondaires (prototypes) et/ou leurs définitions Définition de la fonction principale (avec ses variables locales) Définition des fonctions secondaires avec leurs variables locales (si ce n’est déjà fait) /* Directives au préprocesseur */ #include <stdio.h> /* Inclusion d’une librairie */ #include <stdlib.h> /* Déclaration des variables et constantes globales : Rien à déclarer pour cet exemple */ /* Déclaration d’une fonction secondaire */ void Bonjour(); /* Définition de la fonction principale (avec ses variables locales éventuelles) */ main() { int Choix; /* Déclaration d’une variable locale */

do /* Instruction de contrôle do … while */ { printf("- 1 – Afficher un message de bienvenue\n "); printf("- 0 – Quitter le programme \n "); printf("Votre choix ? "); scanf("%d", &Choix); if (Choix == 1)

Bonjour(); } while (Choix != 0);

system("Pause"); /* Appel d’une commande système */ return 0; /* Retour au système avec code de retour */ } /* Définition des fonctions secondaires avec leurs variables locales éventuelles */ void Bonjour() { printf("Bonjour\n "); /* Affichage : Bonjour */ }

Page 14: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 14

En ANSI C, la fonction main() retourne un entier au système d’exploitation. Cet entier, qui est appelé code de retour, peut être récupéré dans les scripts du système pour évaluer la bonne terminaison d’un programme. Conventionnellement, un code de retour nul exprime la fin normale d’un programme tandis qu’une autre valeur livre une information (généralement une erreur) dont la signification doit être intégrée à la documentation de ce programme. Certains compilateurs tolèrent l’écriture d’une fonction main() sans retour mais produisent alors un message d'avertissement. Les fonctions secondaires peuvent être placées indifféremment avant ou après la fonction principale. Toutefois, pour être placée après la fonction principale, une fonction secondaire doit obligatoirement avoir été prototypée. Le prototypage des fonctions permet au programmeur de les définir ensuite dans n’importe quel ordre, sans tenir compte de l’ordre des appels (une fonction doit être connue du compilateur avant de pourvoir être invoquée). Une fonction secondaire est déclarée (ou prototypée) par l’indication du type de sa valeur de retour (void si aucune), par son identificateur et par la liste des types de ses arguments (liste qui peut être vide ou mentionner void lorsqu’il n’y a aucun paramètre formel). Les lignes de codes suivantes présentent des prototypes valides.

void Bonjour(); /* Aucun paramètre formel */ void Bonjour(void); /* Aucun paramètre formel */ int Somme(int, int); /* Deux paramètres à nommer à la définition de la fonction */

4.4.8. Conventions d'écriture d'un programme C Il n’existe presqu’aucune contrainte dans l'écriture d'un programme C. Un programme peut être écrit sur une seule ligne de plusieurs milliers de caractères ou sur une seule colonne (ou presque) comme illustré précédemment sous le titre « A propos de longueurs ». Toutefois, ce genre de prouesse, qui n’apporte aucun gain de performance au compilateur ni à l’exécutable produit, ne sert qu’à rendre les programmes illisibles. Mieux vaut s’en tenir à quelques pratiques bien séantes : Une seule instruction par ligne. Sauf commentaire éventuel, le point-virgule d'une instruction ou d'une déclaration

est toujours le dernier caractère de la ligne. Idéalement, une instruction de contrôle est seule sur sa ligne mais lorsqu’elle contrôle plusieurs instructions, l’accolade ouvrante du bloc est tolérée.

Les instructions sont disposées de telle façon que la structure modulaire du programme soit mise en évidence. En particulier, une accolade ouvrante marquant le début d'un bloc est idéalement seule sur sa ligne mais elle peut aussi être placée sur la ligne de l’instruction de contrôle du bloc. Une accolade fermante est toujours seule sur sa ligne.

L’indentation des instructions doit mettre en évidence les structures des blocs. On laisse un espace :

o entre un mot-clef et la parenthèse ouvrante qui suit; o après une virgule; o de part et d'autre d'un opérateur binaire.

On ne met pas d’espace : o entre un opérateur unaire et son opérande; o de part et d'autre d'un opérateur de traversée; o entre l’identificateur d’une fonction et la parenthèse ouvrante qui suit; o entre les différents caractères composants certains opérateurs.

Page 15: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 15

5. Les composants du langage

5.1. Les instructions d'entrées-sorties classiques Il existe deux périphériques standards d’entrées-sorties, le clavier et l’écran, auxquels sont associés des pointeurs, dits aussi flux, nommés stdin, stdout et stderr :

stdin désigne le clavier; stdout désigne l'écran; stderr désigne aussi l'écran.

Les instructions d’entrées-sorties classiques sont les fonctions de la librairie standard stdio.h utilisées avec les unités périphériques standards d’entrées-sorties. Leur exploitation nécessite donc l’include de cette librairie. 5.1.1. Les entrées-sorties de caractères Les fonctions de base de lecture et d’écriture de caractères sont en réalité les fonctions d’entrées-sorties fichiers fgetc et fputc. Comme toutes les instructions fichiers, elles nécessitent la désignation du pointeur du fichier concerné, soit en entrées-sorties classiques, stdin et stdout.

UnCaractere = fgetc(stdin); /* Saisie d’un caractère */ fputc(UnCaractere, stdout); /* Affichage d’un caractère */

Les compilateurs implémentent aussi getc et putc comme fonctions strictement équivalentes à fgetc et fputc, ou comme macros dévirées de ces primitives.

UnCaractere = getc(stdin); /* Saisie d’un caractère */ putc(UnCaractere, stdout); /* Affichage d’un caractère */

La plupart des compilateurs intègrent aussi dans leur librairie stdio.h des macros qui définissent les instructions getchar et putchar.

#define getchar() getc(stdin) #define putchar(c) putc((c), stdout) … UnCaractere = getchar(); /* Saisie d'un caractère */ putchar(UnCaractere); /* Affichage d'un caractère */

La fonction fgetc(stdin) et ses cousines retournent un int correspondant au code ASCII du caractère lu. Le type char étant aussi un type entier, les deux exemples suivants sont équivalents.

int N; N = fgetc(stdin); /* Saisie d’un caractère */

char C; C = fgetc(stdin); /* Saisie d’un caractère */

Cependant, lorsqu’elle est employée pour la lecture d’un vrai fichier, la fonction fgetc retourne l'entier EOF (End Of File) qui est une constante définie dans la librairie stdio.h et dont la valeur est ordinairement fixée à -1. La détection correcte d’une fin de fichier impose donc que la variable soit d’un type entier signé. Bien entendu, le périphérique d’entrée standard stdin ne livre pas de fin de fichier. Lors de saisies en boucle, il faut prendre soin détecter l’arrivée du caractère \n (<Enter>). Ceci n’interdit pas d’effectuer ces saisies avec une variable de type entier signé.

main() /* Copie du clavier à l’écran */ { char c; /* int c eut été aussi bien */ while ((c = getc(stdin)) != ’\n’) /* Lecture du clavier jusqu’à \n */

putc(c, stdout); /* Affichage (après réception du \n) */ return 0; }

Page 16: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 16

Le code suivant ne recopie pas les caractères dactylographiés au clavier, mais bien ceux d’un fichier texte Test.TXT qui est passé par redirection au programme nommé ici AfficheFichier.exe. La ligne de commande est : AfficheFichier.exe < Test.TXT

main() /* Copie d’un fichier texte à l’écran */ { int c; while ((c = getc(stdin)) != EOF)

putc(c, stdout); return 0; }

Il faut noter que getc(stdin) retourne un entier signé et par conséquent, quel que soit le type de la variable c, dans le test ((c = getc(stdin)) != EOF), c’est un entier signé qui est comparé à EOF (cf. L’opérateur d’affectation). Au contraire, dans le programme suivant, ce n’est plus la valeur retournée par getc(stdin)) qui est évaluée dans le test, mais bien celle stockée dans la variable c et cette fois, le type de cette variable importe. Si le code du programme précédent AfficherFichier.exe est remplacé par le code suivant, il est impératif que le type char soit signed par défaut faute de quoi c ne vaudra jamais -1 et donc le test (c != EOF) sera toujours vrai.

main() /* Copie d’un fichier texte à l’écran */ { signed char c; /* signed int eu été plus approprié */ do { c = getc(stdin); if (c != EOF) putc(c, stdout); } while (c != EOF); return 0; }

5.1.2. Les entrées-sorties de chaînes de caractères Les fonctions de base de lecture et d’écriture de chaînes de caractères sont en réalité les fonctions d’entrées-sorties fichiers fgets et fputs. Comme toutes les instructions fichiers, elles nécessitent la désignation du pointeur du fichier concerné, soit en entrées-sorties classiques, stdin et stdout.

char UneChaine[TailleMax]; fgets(UneChaine, TailleMax, stdin); /* Saisie d’une chaîne */ fputs(UneChaine, stdout); /* Affichage d’une chaîne */

La fonction fgets lit les TailleMax-1 caractères et les stocke dans la chaîne UneChaine. Si un caractère de fin de ligne est rencontré, il est aussi stocké dans la chaîne et la lecture s’arrête. La chaîne est ensuite complétée du caractère terminateur '\0'. La fonction retourne un pointeur sur la chaîne, ou bien NULL en cas d’erreur ou si la fin de fichier est atteinte (lorsque la lecture s’effectue sur un vrai fichier).

printf("%s", fgets(UneChaine, TailleMax, stdin)); /* Affichage de la chaîne */

Les compilateurs implémentent aussi gets et puts comme fonctions équivalentes à fgets et fputs, sauf qu’elles sont destinées aux entrées-sorties classiques. La fonction gets lit stdin jusqu’au caractère de fin de ligne, lequel est aussi stocké dans la chaîne. La chaîne est ensuite complétée du caractère terminateur '\0'. Il est à noter que les espaces ne sont pas des séparateurs pour les fonctions fgets et gets, mais bien des caractères ordinaires.

char UneChaine[TailleMax]; gets(UneChaine); /* Saisie d’une chaîne */ puts(UneChaine); /* Affichage d’une chaîne */

La fonction gets retourne aussi un pointeur sur la chaîne saisie :

printf("%s", gets(UneChaine)); /* Affichage de la chaîne */

Page 17: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 17

5.1.3. Les entrées-sorties formatées Les fonctions scanf et printf adaptent les données traitées aux formats stipulés dans une chaîne de formatage. L’exemple suivant illustre l’affichage d’une même valeur selon des formats différents. Le format est toujours composé du caractère % et d’un code de formatage. Ce dernier peut éventuellement être accompagné de quelques vitamines qui servent le plus souvent à exprimer la longueur de l’information à traiter. Les codes de formatage des fonctions scanf et printf sont parfois un peu différents.

char C; int N; scanf("%c", &C); /* Encodage de la lettre A */ scanf("%d", &N); /* Encodage de l’entier 65 */ printf("%c", C); /* Affichage A */ printf("%d", C); /* Affichage 65 */ printf("%c", N); /* Affichage A */ printf("%d", N); /* Affichage 65 */

5.1.3.1. Les sorties avec printf La fonction printf permet l’affichage des données d’une liste de variables et/ou d’expressions dont les valeurs sont converties selon les formats spécifiés dans la chaîne de formatage. Il faut noter que la chaîne de formatage de printf peut contenir d’autres caractères que les codes de formatage. Elle peut notamment contenir des chaînes de caractères littéralement exprimées et des caractères spéciaux, tels des caractères de mise en page (\n et \t, par exemple). La chaîne de formatage est suivie de la liste des variables et/ou d’expressions à afficher, dans l’ordre de leur écriture, le premier code concernant la première valeur. La chaîne de formatage peut aussi ne contenir aucun format, mais seulement une valeur littérale à afficher. Le tableau suivant présente les codes de formatage de la fonction printf :

%d int décimale signée %ld long int décimale signée %u unsigned int décimale non signée %lu unsigned long int décimale non signée %o unsigned int octale non signée %lo unsigned long int octale non signée %x unsigned int hexadécimale non signée %lx unsigned long int hexadécimale non signée %f double décimale virgule fixe %lf long double décimale virgule fixe %e double décimale notation exponentielle %le long double décimale notation exponentielle %g double décimale, représentation la plus courte parmi %f et %e %lg long double décimale, représentation la plus courte parmi %lf et %le%c unsigned char caractère %s char* chaîne de caractères

Les vitamines additionnelles au code de formatage s’écrivent entre le caractère % et le(s) caractère(s) définissant le code proprement dit : %5s pour une chaîne de 5 caractères, par exemple. Pour l’essentiel, les vitamines de la chaîne de formatage de la fonction printf sont : largeur minimale du champ numérique : %10d spécifie qu'au moins 10 caractères seront réservés pour afficher

l'entier. Par défaut, la donnée est cadrée à droite du champ. Le signe - avant le format impose le cadrage à gauche de la donnée (%-10d).

précision des réels : %.12f signifie qu'un flottant sera imprimé avec 12 chiffres après la virgule. De même %12.2f signifie que l'on réserve 12 caractères (point décimal inclus) pour afficher le flottant et que 2 d'entre eux sont destinés aux chiffres après la virgule. A défaut de spécification, la précision d’un réel est de 6 décimales.

précision des chaînes de caractères : la précision correspond au nombre de caractères effectivement affichés %30.4s signifie que l'on réserve un champ de 30 caractères pour afficher les 4 premiers caractères de la chaîne, lesquels sont suivis de 26 blancs.

Page 18: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 18

5.1.3.2. Les entrées avec scanf La fonction scanf permet l’encodage de données qui sont converties selon les formats spécifiés et stockées ensuite aux adresses indiquées par les arguments de la fonction. Il faut noter que la chaîne de formatage de scanf ne peut contenir d’autres caractères que des codes de formatage. Elle ne peut notamment pas contenir d’espace inutile (sauf traitement du \n résiduel, cf. plus loin), ni de caractères spéciaux, ni de séquence d’échappement, ni de chaînes de caractères quelconques. La chaîne de formatage est suivie de la liste des adresses à affecter, dans l’ordre de leur écriture, le premier code concernant la première adresse. Lorsque la fonction scanf nécessite l’encodage de plusieurs données, celles-ci doivent être encodées séparées par des espaces ou des \n (retour chariot). Le tableau suivant présente les codes de formatage de la fonction scanf :

%d int décimale signée %hd short int décimale signée %ld long int décimale signée %u unsigned int décimale non signée %hu unsigned short int décimale non signée %lu unsigned long int décimale non signée %o int octale %ho short int octale %lo long int octale %x int hexadécimale %hx short int hexadécimale %lx long int hexadécimale %f float flottante virgule fixe %lf double flottante virgule fixe %Lf long double flottante virgule fixe %e float flottante notation exponentielle %le double flottante notation exponentielle %Le long double flottante notation exponentielle %g float flottante virgule fixe ou notation exponentielle %lg double flottante virgule fixe ou notation exponentielle %Lg long double flottante virgule fixe ou notation exponentielle %c char caractère %s char* chaîne de caractères

Les vitamines qui déterminent les longueurs des informations à traiter s’écrivent entre le caractère % et le(s) caractère(s) définissant le code proprement dit : %5s pour une chaîne de 5 caractères, par exemple. Dans la chaîne de formatage de la fonction scanf, des vitamines particulières permettent la désignation, selon la syntaxe employée, des seuls caractères autorisés ou interdits lors de la saisie d’une chaîne : chaîne de formatage pour des caractères autorisés : "%[caractères_autorisés]s" chaîne de formatage pour des caractères interdits : "%[^caractères_interdits]s"

La saisie semble accepter tous les caractères encodés au clavier mais la chaîne restituée est limitée au dernier caractère valide :

/* Seuls les chiffres sont autorisés */ scanf("%[0123456789]s",s);

Encodage Contenu de la variable Bonjour \0 /* Chaîne vide */2001 L’odyssée 2001\0 Janvier 2002 \0 /* Chaîne vide */31 Mars 2003 31\0 12345 12345\0

/* Seuls les chiffres sont interdits */ scanf("%[^0123456789]s",s);

Encodage Contenu de la variable Bonjour Bonjour\0 2001 L’odyssée \0 /* Chaîne vide */Janvier 2002 Janvier \0 31 Mars 2003 \0 /* Chaîne vide */12345 \0 /* Chaîne vide */

Page 19: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 19

5.1.3.3. Exemples d’entrées-sorties formatées

char C; char Ch[32]; int N; float F; scanf("%3s", Ch); /* Encodage : AZE */ printf("%s", Ch); /* Affichage : AZE */ scanf("%3s", Ch); /* Encodage : AZERTY */ printf("%s", Ch); /* Affichage : AZE L’excédent reste dans le buffer */ scanf("%2d", &N); /* Encodage : 5 */ printf("%d", N); /* Affichage : 5 */ scanf("%2d", &N); /* Encodage : 65 */ printf("%d", N); /* Affichage : 65 */ scanf("%2d", &N); /* Encodage : 165 */ printf("%d", N); /* Affichage : 16 L’excédent reste dans le buffer */

scanf("%3f", &F); /* Encodage : 12 */ printf("%f", F); /* Affichage : 12.000000 */ scanf("%3f", &F); /* Encodage : 123 */ printf("%f", F); /* Affichage : 123.000000 */ scanf("%3f", &F); /* Encodage : 1234 */ printf("%f", F); /* Affichage : 123.000000 L’excédent reste dans le buffer */ scanf("%3f", &F); /* Encodage : 123.4 */ printf("%f", F); /* Affichage : 123.000000 L’excédent reste dans le buffer */ scanf("%3f", &F); /* Encodage : 12.34 */ printf("%f", F); /* Affichage : 12.000000 L’excédent reste dans le buffer */ scanf("%3f", &F); /* Encodage : 1.234 */ printf("%f", F); /* Affichage : 1.200000 L’excédent reste dans le buffer */ scanf("%s", Ch); /* Encodage : Voici un mot … */ printf(">%8.5s<", Ch); /* Affichage : > Voici< */ printf(">-%8.5s<", Ch); /* Affichage : >Voici < */ printf(">%3.8s<", Ch); /* Affichage : >Voici< L’espace est séparateur */

scanf("%s", Ch); /* Encodage : VoiciUnMot … */ printf(">%3.8s<", Ch); /* Affichage : >VoiciUnM< */

scanf("%f", &F); /* Encodage : 12345.6789 */ printf(">%6.2f<", F); /* Affichage : >12345.68< */ scanf("%f", &F); /* Encodage : 12.6789 */ printf(">%6.2f<", F); /* Affichage : > 12.68< */

N = 1234; printf("%X", N); /* Affichage : 4D2 */ printf("%x", N); /* Affichage : 4d2 */ printf("%o", N); /* Affichage : 2322 */ printf("%O", N); /* Affichage : 0 Le code de formatage O n’existe pas */

scanf("%X",&N); /* Encodage : 4D3 */ printf("N = %d", N); /* Affichage : N = 1235 */

Page 20: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 20

5.1.4. Le problème du retour chariot résiduel Pour la fonction scanf, le retour chariot \n est un séparateur au même titre que l’espace. Il désigne aussi la fin de la saisie pour toute instruction d’entrée à partir de stdin, même pour scanf quand il arrive après la valeur destinée à la dernière variable passée à cette fonction. L’origine du problème réside dans le fait que toutes les instructions d’entrées ne débarrassent pas le buffer de ce \n. Le retour chariot est ôté du buffer après : n’importe quelle saisie par gets ou fgets; n’importe quelle saisie d’un seul caractère et que le seul caractère encodé fut le retour chariot.

Le retour chariot reste dans le buffer après : n’importe quel usage de scanf; n’importe quelle saisie d’un seul caractère et que le retour chariot fut précédé d’un caractère utile.

Le problème du retour chariot résiduel se révèle quand la saisie suivante est : une saisie d’un seul caractère; une saisie par gets ou fgets.

Le problème ressenti : la dernière saisie semble ne pas s’exécuter ! En effet, cette saisie rencontre le \n abandonné et le traite comme séparateur ou comme fin de saisie, selon le cas, mais sans attendre l’encodage d’une éventuelle valeur.

c = getc(stdin); /* Encodage de la lettre A */ putc(c, stdout); /* Affichage : A */ c = getc(stdin); /* Problème : pas d’attente de l’encodage */ putc(c, stdout); /* Envoi de \n à l’écran */

scanf("%c", &c); /* Encodage de la lettre B */ putc(c, stdout); /* Affichage : B */ c = getc(stdin); /* Problème : pas d’attente de l’encodage */ putc(c, stdout); /* Envoi de \n à l’écran */

c = getc(stdin); /* Encodage de \n */ putc(c, stdout); /* Envoi de \n à l’écran */ c = getc(stdin); /* Pas de problème. Encodage de la lettre X */ putc(c, stdout); /* Affichage : X Problème possible plus loin */

scanf("%c", &c); /* Encodage de \n */ putc(c, stdout); /* Envoi de \n à l’écran */ c = getc(stdin); /* Pas de problème. Encodage de la lettre Y */ putc(c, stdout); /* Affichage : Y Problème possible plus loin */

Une première solution consiste à précéder le format %c du scanf d’un espace. Les saisies successives par scanf fonctionnent alors parfaitement, mais pas une saisie ultérieure par une autre fonction (par getc ou gets par exemple).

scanf(" %c", &c); /* Encodage de la lettre A */ putc(c, stdout); /* Affichage : A */ scanf(" %c", &c); /* Encodage de la lettre B */ putc(c, stdout); /* Affichage : B */ c = getc(stdin); /* Problème : pas d’attente de l’encodage */ putc(c, stdout); /* Envoi de \n à l’écran */

Une autre solution consiste à faire suivre toute saisie autre que fgets ou que gets, par une autre apparemment inutile, qui débarrasse le buffer du retour chariot résiduel. Cela ne peut se faire que si le caractère encodé à la première saisie est différent de \n.

c = getc(stdin); /* Encodage de la lettre A */ if (c !='\n') getc(stdin); /* Saisie "inutile" conditionnelle */ putc(c, stdout); /* Affichage : A */ c = getc(stdin); /* Encodage de la lettre Z */ if (c !='\n') getc(stdin); /* Saisie "inutile" conditionnelle */ putc(c, stdout); /* Affichage : Z */

Page 21: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 21

void VideStdin(void) { while ((getc(stdin)) != '\n'); }

Cette dernière solution n’est toutefois applicable qu’après la saisie d’un seul caractère. Après la saisie d’une valeur numérique ou d’une chaîne par scanf, le nettoyage du buffer doit être inconditionnel.

scanf("%d", &N); /* Encodage d’un entier */ getc(stdin); /* Saisie "inutile" inconditionnelle */ c = getc(stdin); /* Encodage de la lettre A */ putc(c, stdout); /* Affichage : A */

scanf("%d", &N); /* Encodage d’un entier */ getc(stdin); /* Saisie "inutile" inconditionnelle */ gets(UneChaine); /* Encodage d’une chaîne */ puts(UneChaine); /* Affichage de la chaîne */

Bien que fréquemment rencontré, l’usage de l’instruction fflush(stdin) en réponse à ce problème est une solution peu fiable. En effet, cette instruction est destinée aux sorties vers les mémoires de masse et son usage sur le flux d’entrée stdin est indéterminé. Le code exploitant cette solution pourra présenter des comportements différents selon les implémentations (compilateur, environnement, …). La saisie correcte d’un seul caractère peut aussi être perturbée par un encodage antérieur de plusieurs caractères là où un seul était attendu, ou encore lorsque la longueur spécifiée dans une chaîne de formatage de scanf a été dépassée. Il n’y a plus seulement un \n résiduel à ôter du buffer mais toute une chaîne. L’écriture d’une fonction de nettoyage, telle que VideStdin proposée ci-contre, peut s’avérer très utile. Attention, la fonction VideStdin ne peut être employée que dans les mêmes conditions que getc(stdin) et donc, notamment pas après l’usage des fonctions fgets ou gets :

c = getc(stdin); /* Saisie d’un caractère */ if (c !='\n') VideStdin(); /* Nettoyage conditionnel */

scanf("%d", &N); /* Saisie d’un entier */ VideStdin(); /* Nettoyage inconditionnel */ gets(UneChaine); /* Saisie d’une chaîne par fgets ou gets */ /* … */ /* Pas de nettoyage */

5.1.5. Autres entrées-sorties formatées Les fonctions printf et scanf possèdent des cousines dont les rôles un peu différents, qui sortent du domaine des entrées-sorties classiques, sont esquissés ci-après. 5.1.5.1. Lecture et écriture formatées des fichiers Les fonctions fprintf et fscanf, mieux illustrées dans la partie « fichiers » de ce document, sont reprises ici pour comparaison. Ces fonctions s’utilisent exactement comme leurs cousines des entrées-sorties classiques à la seule différence qu’elles nécessitent la désignation du fichier à lire ou à écrire.

fprintf(Pointeur_sur_le_fichier_a_ecrire, "%d %f ", UnEntier, UnReel); fscanf(Pointeur_sur_le_fichier_a_lire, "%d%f", &UnEntier, &UnReel);

5.1.5.2. Lecture et écriture formatées de la mémoire Les fonctions sprintf et sscanf, écrivent et lisent directement des données stockées en mémoire vive. Elles ne possèdent donc pas de périphérique par défaut et elles ne nécessitent pas la désignation d’un fichier, mais bien celle de la zone mémoire à lire ou à écrire (le pointeur de fichier est remplacé par un pointeur de données).

char Chaine[100], UnMot[16], UnAutre[16]; sprintf(Chaine, "Voici trois mots"); sscanf(Chaine, "%s%s", UnMot, UnAutre); /* Saisie de "Voici" et de "trois" */ int T[3], N, M; sprintf(T, "%d %d %d ", 1, 2, 3); sscanf(T, "%d%d", &N, &M); /* Saisie des entiers 1 et 2 */

Page 22: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 22

5.2. Les types de données prédéfinis Le C est un langage typé. Avant d’être utilisée, une variable ou une constante doit faire l’objet d’une déclaration qui associe un type de donnée spécifié à l’identificateur de la variable. C’est le type de la variable qui détermine la façon dont elle est rangée en mémoire et le nombre d’octets à lui réserver. Bien qu’occupant toujours le même espace en mémoire, les pointeurs sont également typés. Ils permettent l’accès à des données en mémoire par l’usage de leur adresse et non d’un identificateur. Le type d’un pointeur détermine la façon dont la valeur pointée est rangée en mémoire et l’unité de calcul de l’arithmétique des pointeurs sur ces données. La taille mémoire correspondant aux différents types dépend des compilateurs et des systèmes. Il est à noter que la norme ANSI spécifie des règles à ce sujet mais elles ne sont pas détaillées dans ce document. Les types de base en C concernent les caractères, les entiers et les réels (nombres en virgule flottante). Ils sont désignés en ANSI C par les identificateurs de types char, int, float et double, et éventuellement précisés par les modificateurs short, long, signed et unsigned. 5.2.1. Le type caractère Une variable de type caractère est déclarée avec l’identificateur char. Un char peut contenir n'importe quel élément du jeu de caractères de la machine utilisée et occupe ordinairement un octet en mémoire. Le jeu de caractères utilisé correspond généralement aujourd’hui au codage ASCII étendu sur 8 bits. Les 128 premiers caractères (code 0 à 127) contiennent les caractères de contrôle (Beep, Tab, CR, LF, FF, …) et les caractères imprimables de base représentés dans le tableau ci-dessous. Les 128 derniers caractères (code 128 à 255) sont utilisés pour les caractères propres aux différentes langues. Par exemple, la version ISO-8859-1 (aussi appelée ISO-LATIN-1) contient les caractères accentués d'Europe occidentale.

déc. oct. hex. déc. oct. hex. déc. oct. hex. 32 40 20 @ 64 100 40 ` 96 140 60! 33 41 21 A 65 101 41 a 97 141 61" 34 42 22 B 66 102 42 b 98 142 62# 35 43 23 C 67 103 43 c 99 143 63$ 36 44 24 D 68 104 44 d 100 144 64% 37 45 25 E 69 105 45 e 101 145 65& 38 46 26 F 70 106 46 f 102 146 66' 39 47 27 G 71 107 47 g 103 147 67( 40 50 28 H 72 110 48 h 104 150 68) 41 51 29 I 73 111 49 i 105 151 69* 42 52 2a J 74 112 4a j 106 152 6a+ 43 53 2b K 75 113 4b k 107 153 6b, 44 54 2c L 76 114 4c l 108 154 6c- 45 55 2d M 77 115 4d m 109 155 6d. 46 56 2e N 78 116 4e n 110 156 6e/ 47 57 2f O 79 117 4f o 111 157 6f0 48 60 30 P 80 120 50 p 112 160 701 49 61 31 Q 81 121 51 q 113 161 712 50 62 32 R 82 122 52 r 114 162 723 51 63 33 S 83 123 53 s 115 163 734 52 64 34 T 84 124 54 t 116 164 745 53 65 35 U 85 125 55 u 117 165 756 54 66 36 V 86 126 56 v 118 166 767 55 67 37 W 87 127 57 w 119 167 778 56 70 38 X 88 130 58 x 120 170 789 57 71 39 Y 89 131 59 y 121 171 79: 58 72 3a Z 90 132 5a z 122 172 7a; 59 73 3b [ 91 133 5b { 123 173 7b< 60 74 3c \ 92 134 5c | 124 174 7c= 61 75 3d ] 93 135 5d } 125 175 7d> 62 76 3e ^ 94 136 5e ~ 126 176 7e? 63 77 3f _ 95 137 5f DEL 127 177 7f

Page 23: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 23

Le type char désigne aussi le type entier le plus petit, celui dont les valeurs peuvent être codées sur un seul octet. Toute variable de type char peut ainsi être utilisée dans une expression manipulant des entiers. Par exemple, si c est de type char, l'expression c + 1 est valide. En matière de caractère, elle désigne le caractère suivant dans la table des caractères et en matière d’entier, elle représente le code ASCII du caractère contenu dans c augmenté d’une unité.

char c = 'A'; /* Déclare c et l’initialise à A */ char n = 66; /* Déclare n et l’initialise à 66 */ printf("%c", c); /* Affichage au format caractère : A */ printf("%c", c + 1); /* Affichage au format caractère : B */ printf("%d", c); /* Affichage au format numérique : 65 */ printf("%d", c + 1); /* Affichage au format numérique : 66 */ printf("%c", n); /* Affichage au format caractère : B */ printf("%c", n + 1); /* Affichage au format caractère : C */ printf("%d", n); /* Affichage au format numérique : 66 */ printf("%d", n + 1); /* Affichage au format numérique : 67 */

Le type char est signé ou non suivant les implémentations. Il est généralement signed par défaut, mais en cas de doute, il vaut mieux préciser unsigned char ou signed char. Il faut noter que tous les caractères imprimables sont positifs et que pour obtenir les caractères du code ASCII étendu, il faut utiliser le type unsigned char.

signed char c; for (c = 32; c < 255; c++)

printf("%c", c); /* printf("%d ", c) montre mieux le passage de 127 à -128 */ La boucle précédente est infinie car la valeur de c passe de 127 à –128 et n’atteint donc jamais 254. Par contre, la boucle suivante affiche bel et bien tous les caractères imprimables et s’arrête ensuite. Dans un cas comme dans l’autre, la boucle for doit éviter la valeur 255 car elle se terminerait avec c = 256, ce qui ne peut se faire sur un seul octet.

unsigned char c; for (c = 32; c < 255; c++)

printf("%c", c); printf("%c", c); /* Affichage distinct de la valeur 255 */

5.2.2. Les types entiers Outre le type char dont il vient d’être question, le C ANSI reconnaît un seul entier et le désigne par l’identificateur int. Cet entier représente un mot naturel de la machine utilisée, à condition toutefois que le compilateur l’ait implémenté ainsi. Le C ANSI propose des modificateurs qui permettent de réduire ou agrandir l’espace accordé à un entier. Dans la mesure où ces modificateurs de taille ne peuvent s’appliquer qu’à l’entier int, la spécification de ce dernier peut être omise. Dans la suite de ce document, sauf stipulation expresse, un int représente un entier codé sur 4 octets. Comme pour le char, le int est généralement signed par défaut, mais en cas de doute, il vaut mieux préciser unsigned ou signed en plus de l’éventuel modificateur de taille. Soit, sur un ordinateur équipé d’un processeur Intel 32 bits :

Type Synonyme gcc sous Linux

TC 3.0 sous MsDos

Dev-Cpp sous Windows

Signification

char char 8 bits 8 bits 8 bits caractère short int short 16 bits 16 bits 16 bits entier court int int 32 bits 16 bits 32 bits entier long int long 32 bits 32 bits 32 bits entier long long long int long long 32 ou 64 bits* 32 bits 32 bits entier long

* Selon la version : à tester au cas par cas. Le bit de poids fort d'un entier est son signe. Un entier positif est donc représenté en mémoire par la suite de 32 bits dont le bit de poids fort vaut 0 et les 31 autres bits correspondent à la décomposition de l'entier en base 2. Un entier négatif est, lui, représenté par une suite de 32 bits dont le bit de poids fort vaut 1 et les 31 autres bits correspondent à la valeur absolue de l'entier représentée suivant la technique du complément à 2. Le calcul de cette valeur peut être fait selon les 3 étapes suivantes :

1. traduction de la valeur absolue en binaire; 2. inversion de chaque bit de la valeur binaire obtenue; 3. ajout d’une unité au résultat ainsi obtenu.

Page 24: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 24

Exemples (sur un octet) :

Valeur à représenter -1 -2 -12

Traduction de la valeur absolue 00 00 00 01 00 00 00 10 00 00 11 00

Inversion des bits 11 11 11 10 11 11 11 01 11 11 00 11

Ajout d’une unité 11 11 11 11 11 11 11 10 11 11 01 00

L’entier signed stocké sur 4 octets peut ainsi représenter les valeurs -2(32-1) à (2(32-1)-1) tandis que son cousin unsigned peut représenter un entier entre 0 et (232-1). Ordinairement, sur un ordinateur équipé d’un processeur Intel 32 bits :

Types Tailles Calcul des plages Limites des plages signed char 8 bits -2(8-1) à (2(8-1)-1) -128 à 127 unsigned char 8 bits 0 à (28-1) 0 à 255 signed short int 16 bits -2(16-1) à (2(16-1)-1) -32768 à 32767 unsigned short int 16 bits 0 à (216-1) 0 à 65535 signed int 32 bits -2(32-1) à (2(32-1)-1) -2147483648 à 2147483647unsigned int 32 bits 0 à (232-1) 0 à 4294967295 signed long long int 64 bits -2(64-1) à (2(64-1)-1) -9223372036854775808

à 9223372036854775807 unsigned long long int 64 bits 0 à (264-1) 0 à 18446744073709551615

L’opérateur sizeof() livre le nombre d’octets nécessaires au stockage de l’objet (type ou variable) qui lui est passé par paramètre.

unsigned short x; printf("%d", sizeof(unsigned short)); /* Affichage : 2 */ printf("%d", sizeof(x)); /* Affichage : 2 */

Afin de préserver la portabilité des programmes, il convient de ne jamais présumer de la taille d'un objet de type entier. L’utilisation de l'opérateur sizeof() permet la détermination dynamique des limites des nombres entiers. 5.2.3. Les types réels Les types float, double et long double servent à représenter des nombres en virgule flottante. Soit, sur un ordinateur équipé d’un processeur Intel 32 bits :

Type gcc sous Linux

TC 3.0 sous MsDos

Dev-Cpp sous Windows

float 32 bits 32 bits 32 bits double 64 bits 64 bits 64 bits long double 128 bits 80 bits 96 bits

Les flottants sont généralement stockés en mémoire sous la représentation de la virgule flottante normalisée. Le nombre est décomposé en signe_du_nombre / exposant / 1.mantisse, avec l’exposant calculé en base 2. Un flottant est donc représenté par une suite de bits dont le bit de poids fort représente le signe du nombre, le champ du milieu contient la représentation binaire de l'exposant et le reste des bits de poids faible représente la mantisse et donc, la précision. A titre illustratif, voici la décomposition d’un nombre réel en représentation normalisée IEEE 754 sur un ordinateur équipé d’un processeur Intel 32 bits : le bit de poids fort (bit 31) signe_du_nombre les 8 bits suivants (bits 30 à 23) exposant les 23 bits restants (bits 22 à 0) 1.mantisse Il faut noter que les modificateurs unsigned et signed ne s’appliquent pas aux réels.

Page 25: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 25

5.3. Les expressions littérales Une expression littérale est une valeur directement exprimée dans le code source du programme. Une expression littérale peut représenter un entier, un réel, un caractère, une chaîne de caractères ou encore une liste de valeurs littérales élémentaires. Une telle expression est essentiellement utilisée pour initialiser une variable ou une constante, mais aussi pour participer aux opérations dans le programme et pour fournir des informations en sortie à l’intention des utilisateurs (chaîne de caractères dans un printf(), par exemple). Lorsqu’une expression littérale est employée en initialisation d’une variable ou d’une constante, son type est celui de la variable ou de la constante. Dans une expression, son type est déterminé par la façon dont est écrite cette valeur. 5.3.1. Les expressions littérales d’entiers Une valeur entière peut être écrite en base décimale, en base octale ou en base hexadécimale. Une syntaxe appropriée fait la distinction. En base décimale :

L’écriture du nombre entier est sa représentation en base 10, soit son écriture ordinaire. Par exemple, 0 et 2437348 sont des nombres entiers littéralement exprimés en base 10.

En base octale : L’écriture du nombre entier est sa représentation en base 8. Elle doit être préfixée d’un zéro. Par exemple, 00 et 0377 sont les expressions octales littérales des nombres entiers 0 et 255.

En base hexadécimale : L’écriture du nombre entier est sa représentation en base 16. Elle doit être préfixée de 0x ou 0X et les caractères alphabétiques de cette base (A, B, C, D, E et F) peuvent être écrits en minuscules ou en majuscules. Par exemple, 0x0, 0xe et 0xFF sont les expressions octales littérales des nombres entiers 0, 14 et 255.

Par défaut, une valeur littérale entière occupe en mémoire le nombre d’octets d’un entier naturel, mais il est permis de forcer ce choix à entier long, signé ou non, par l’emploi de suffixes L (ou l) et U (ou u) :

valeurs types 1234 int02322 int /* octal */0x4D2 int /* hexadécimal */123456789L long int1234U unsigned int 123456789UL unsigned long int

5.3.2. Les expressions littérales de réels Une valeur littérale réelle peut être écrite en notation ordinaire ou en notation scientifique auquel cas l’exposant est désigné par la lettre E (ou e). La présence du point décimal est nécessaire lorsque la valeur littérale participe à un calcul (faute de quoi elle est considérée comme entière) et par ailleurs, il améliore la lisibilité. Par défaut, une valeur littérale réelle est du type double précision, mais il est permis de forcer ce choix à la simple précision ou à la plus grande précision par l’emploi de suffixes F (ou f) ou L (ou l) :

valeurs types 12. double12.0 double12.34 double12.3e-4 double12.34F float12.34L long double

5.3.3. Les expressions littérales de caractères L’expression littérale d’un caractère imprimable s’écrit entre apostrophes ('A', '$', '0', '"', '?', …). Le caractère backslash et l'apostrophe elle-même nécessitent la présence d’un backslash de contrôle et s’écrivent donc '\\' et '\''. A noter que le point d'interrogation et le guillemet peuvent aussi être précédés d’un backslash ('\?', '\"').

Page 26: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 26

C’est la précédence du guillemet par un backslash qui permet son insertion éventuelle dans l’expression littérale d’une chaîne de caractères, cette dernière étant délimitée par des guillemets. Tous les caractères, et les caractères non imprimables en particulier, peuvent être désignés par un backslach suivi de leur code ASCII exprimé en octal, ou par un backslach suivi d’un x (x en minuscule, indifférent pour les lettres hexadécimales) et de leur code ASCII exprimé en hexadécimal.

char Escape = '\33'; /* Caractère ASCII 27 (33 en base 8) = Escape */ char EncoreEscape = '\x1b'; /* Caractère ASCII 27 (1B en base 16) = Escape */ char ToujoursEscape = '\x1B'; printf("%c", '\101'); /* Affichage : A */ printf("%c", '\x41'); /* Affichage : A */

Certains caractères non-imprimables, parmi les plus fréquemment utiles, disposent d'une notation simplifiée :

\n nouvelle ligne \r retour chariot \b retour arrière (backspace) \a signal d'alerte (beep) \t tabulation horizontale (tab) \v tabulation verticale \f saut de page

5.3.4. Les expressions littérales de chaînes de caractères La chaîne de caractères est une suite de caractères. Son expression littérale s’écrit entre des guillemets. Elle peut contenir des caractères spéciaux, éventuellement non imprimables, qui sont écrits de façon littérale, comme expliqué ci-dessus, mais sans les apostrophes.

"Ceci est une chaîne de caractères littéralement exprimée." "Voici un guillemet \" et, au bout de cette chaîne, le passage à la ligne.\n"

Le caractère \ seul à la fin d’une ligne est la marque de continuation déjà expliquée précédemment et largement illustrée sous le titre « A propos de longueurs ».

5.4. Les variables Une variable est désignée par un identificateur et fait toujours l’objet d’une déclaration préalablement à son exploitation. Comme indiqué précédemment, la déclaration associe un type de donnée spécifié à l’identificateur de la variable. Ce type de donnée est un des types de base du langage (char, int, float, …), ou un pointeur sur un de ces types, ou une variable d’un type construit par le programmeur (une structure par exemple), ou un pointeur sur un type utilisateur. La déclaration peut être écrite en dehors de toute fonction auquel cas la variable est globale ou à l’intérieur d’une fonction auquel cas la variable est locale à cette fonction. Contrairement à ce qui se fait en C++, il n’est pas permis de déclarer une variable dans une instruction de contrôle ( par exemple, for (int i; … est interdit). La bonne pratique est de déclarer les variables globales au début de leur page de code, tout de suite après les directives au préprocesseur et les variables locales au début de leur fonction, toute de suite après l’accolade ouvrante. 5.4.1. Durée de vie des variables Les variables manipulées dans un programme C ne sont pas toutes traitées de la même manière et en particulier, elles n'ont pas toutes la même durée de vie. 5.4.1.1. Les variables permanentes Une variable permanente occupe un emplacement en mémoire qui reste le même durant toute l'exécution du programme. Cet emplacement est alloué (dans le data segment) une fois pour toutes lors de la compilation. Par défaut, les variables permanentes sont initialisées à leur valeur nulle par le compilateur. A l’intérieur des fonctions et des blocs d’instructions, elles sont caractérisées par le mot-clef static.

Page 27: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 27

5.4.1.2. Les variables temporaires Les variables temporaires reçoivent un emplacement en mémoire (généralement dans le stack segment) de façon dynamique lors de l'exécution du programme. Elles ne sont pas initialisées par défaut. Leur emplacement en mémoire est libéré à la fin de l'exécution de la portion de codes qui a nécessité leur allocation, généralement une fonction secondaire. Une variable temporaire stockée dans la pile est dite automatique. Le modificateur de type correspondant est auto, et il n’est pratiquement jamais utilisé puisqu'il ne s'applique qu'aux variables temporaires qui sont automatiques par défaut. Une variable temporaire peut également être stockée dans un registre du processeur. L’accès à une telle variable est évidemment beaucoup plus rapide que si elle était située quelque part dans la mémoire RAM. Le modificateur de type register réalise une demande au compilateur de ranger une variable très utilisée dans un registre. Toutefois, vu le nombre limité des registres, cette demande peut être ignorée et la variable est alors rangée dans la pile. 5.4.2. Portée des variables La durée de vie des variables est liée à leur portée, c'est-à-dire à la portion du programme dans laquelle elles sont définies. 5.4.2.1. Variables globales Comme déjà précisé, une variable globale est une variable déclarée en dehors de toute fonction. Elle est connue partout où elle ne subit pas d’effet de masque dans le code qui suit sa déclaration. Une telle variable est automatiquement permanente (spécification static superflue) et initialisée à sa valeur nulle. Toutefois, c’est une bonne pratique que d’initialiser toute variable lors de sa déclaration ou au démarrage du programme quand le premier usage fait de cette variable implique son évaluation avant son affectation. Par exemple, dans l’expression c = c + 1, il est clair que la valeur de c est évaluée afin que l’addition puisse s’effectuer, et ce n’est qu’après cette opération qu’à lieu l’affectation. Cette pratique peut valablement s’appliquer quel que soit le langage utilisé et sans préjuger des capacités d’initialisation de son compilateur. int N; void UneFonction() { N++; printf("%d ",N); /* Un affichage par appel, soit respectivement : 1 2 3 */ return; } void UneAutreFonction() { int N = 7; /* La variable N masque la variable globale de même nom */ printf("%d ", N); /* Un affichage par appel, soit respectivement : 7 7 7 */ return; } main() { int i; for (i = 0; i < 3; i++) /* Appel de chaque fonction quand i vaut 0, 1 et 2 */ { UneFonction();

UneAutreFonction(); }

printf("%d ", N); /* Affichage : 3 */ return 0; }

Page 28: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 28

5.4.2.2. Variables locales Comme déjà précisé, une variable locale est une variable déclarée à l’intérieur d’une fonction, voire d’un bloc de codes. Elle est ensuite connue dans toute la portion de code (fonction ou bloc) de sa déclaration. A défaut d’être spécifiée static, une variable locale est temporaire et elle est rangée dans la pile lors de l’appel de la fonction et abandonnée à la sortie de la fonction. Sa durée de vie est donc limitée à une seule exécution de la fonction ou du bloc d'instructions et sa valeur n’est pas conservée d'un appel à l’autre. Comme illustré ci-dessus dans la fonction UneAutreFonction(), les variables locales n'ont aucun lien avec des variables globales de même nom (effet de masque). 5.4.2.2.a. La variable « static » La variable locale stipulée static devient une variable permanente. Une telle variable reste locale à la fonction dans laquelle elle est déclarée, mais sa valeur est conservée d'un appel au suivant. Elle est également initialisée à sa valeur nulle à la compilation, mais elle peut être explicitement initialisée à une valeur souhaitée au moment de sa déclaration. Cette initialisation prend effet lors du premier appel de la fonction. int N = 17; void UneFonction() { static int N; /* La variable N masque la variable globale de même nom */ N++; printf("%d ",N); /* Un affichage par appel, soit respectivement : 1 2 3 */ return; } void UneAutreFonction() { static int N = 5; /* La variable N masque la variable globale de même nom */ N++; printf("%d ", N); /* Un affichage par appel, soit respectivement : 6 7 8 */ return; } main() { int i; for (i = 0; i < 3; i++) /* Appel de chaque fonction quand i vaut 0, 1 et 2 */ { UneFonction();

UneAutreFonction(); }

printf("%d ", N); /* Affichage : 17 */ return 0; } Dans UneFonction(), la variable N est initialisée à sa valeur nulle comme toute autre variable permanente. Dans UneAutreFonction(), la variable N est initialisée lors du premier appel par une valeur spécifique 5. Dans une fonction comme dans l’autre, la variable N est bien permanente puisque sa valeur est conservée d'un appel à l'autre. Néanmoins, ces variables N sont aussi locales, chacune à sa fonction, et n’ont aucun lien avec la variable globale du même nom. 5.4.3. Les variables « volatiles » Le qualificateur de type volatile est utilisé dans la programmation système. Une variable dont le type est qualifié par volatile peut être modifiée en arrière plan par un autre programme.

Page 29: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 29

5.4.4. Les constantes Le mot réservé const est utilisé pour définir une donnée qui ne peut être modifiée en cours d’exécution, ni même dans la suite du projet en cours de développement. 5.4.4.1. Ne pas confondre « macro » et « constante » Le programmeur C délaisse fréquemment le qualificatif const au profit de la directive #define qui produit le même effet, à savoir l’impossibilité de modifier la valeur ensuite. Il faut toutefois noter que le mécanisme de #define est différent et qu’il sert à l’écriture de macros. Par exemple, les lignes suivantes définissent les macros MAX et CARRE, ainsi qu’une constante Pi (plus communément appelée constante symbolique). #define MAX(a,b) (((a) > (b)) ? (a) : (b)) #define CARRE(a) ((a) * (a)) #define Pi 3.141592 Mais, que du point de vue du programmeur, il s’agisse d’une macro ou d’une constante, le mécanisme est dans tous les cas celui de la macro, c’est un mécanisme de substitution : tout appel de MAX, CARRE ou Pi dans le code est remplacé à la pré-compilation par le code de la macro elle-même. Une valeur telle que Pi dans cet exemple est stockée en mémoire parmi les codes du programme, dans le code segment qui lui est alloué. A la suite des #define ci-dessus, les lignes suivantes : sont compilées comme étant : M = MAX(x, y); Surface = CARRE(Cote); Perimetre = Diametre * Pi;

M = (((x) > (y)) ? (x) : (y)); Surface = ((Cote) * (Cote)); Perimetre = Diametre * 3.141592;

Le mécanisme de la véritable constante est différent : il s’agit bien d’une définition avec initialisation d’une variable non modifiable, mais variable quand même en ce sens qu’elle possède bien une place en mémoire, dans un des espaces réservés aux données du programme, dans un data segment ou dans un stack segment selon que sa portée est globale ou locale. 5.4.4.2. Déclaration et initialisation Les valeurs déclarées constantes doivent être initialisées. Les deux déclarations suivantes sont synonymes.

const int X = 17; int const X = 17;

5.4.4.2.a. Pointeur sur une valeur constante Un pointeur sur une valeur constante doit être déclaré comme tel, comme pointeur d’un type constant.

const int X = 7; /* Déclaration d’un entier constant */ int *pI = &X; /* Avertissement du compilateur que X est constant */ (*pI)++; /* Ignoré à l’exécution */

const int *pI = &X; /* Permis car pI est déclaré pointeur d’un entier constant */ pI++; /* Incrémente le pointeur pI qui cesse ensuite de pointer X */ (*pI)++; /* Interdit car la valeur pointée est réputée constante */

5.4.4.2.b. Pointeur d’une valeur constante Il est permis d’initialiser un pointeur de valeur constante avec une valeur non constante. La valeur pointée donne accès à une constante tandis que la variable donne accès à une valeur modifiable.

int X = 7; X++; /* Permis car X n'est pas constant */ const int *pI = &X; /* Permis même si X n’est pas une constante */ pI++; /* Incrémente le pointeur pI qui cesse ensuite de pointer X */ (*pI)++; /* Interdit car la valeur pointée est réputée constante */

Page 30: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 30

Il est bien entendu aussi permis d’initialiser un pointeur de valeur constante avec une valeur constante du type adéquat. La valeur pointée et la constante donnent accès à une valeur non modifiable.

const int X = 7; /* Une constante doit être initialisée */ const int *pI = &X; /* Permis car pI est déclaré pointeur d’un entier constant */ *pI++; /* Incrémente pI qui ne pointe donc plus X ensuite */ (*pI)++; /* Interdit car la valeur pointée est un entier constant */

5.4.4.2.c. Pointeur constant d’une valeur quelconque Un pointeur peut être déclaré constant. Il ne peut plus alors faire l’objet d’incrémentation ou décrémentation, ni de réaffectation.

int X = 7; int * const pI = &X; /* Permis même si X n’est pas une constante */ *pI++; /* Interdit car le pointeur est déclaré constant */ (*pI)++; /* Incrémente la valeur pointée qui n’est pas une constante */ X++; /* Permis car X n'est pas une constante */

5.4.4.2.d. Pointeur constant d’une valeur constante Un pointeur d’une valeur constante peut être déclaré constant. Le pointeur et sa valeur pointée sont alors non modifiables.

int X = 7; const int * const pI = &X; /* Permis, la valeur pointée est un entier constant */ *pI++; /* Interdit car le pointeur est déclaré constant */ (*pI)++; /* Interdit car la valeur pointée est un entier constant */ X++; /* Permis car X n'est pas une constante */

5.4.4.2.e. Pointeur constant sur une valeur constante Un pointeur peut être déclaré constant et être initialisé avec une valeur constante. Le pointeur et sa valeur pointée sont alors non modifiables.

const int X = 7; const int * const pI = &X; /* Permis, la valeur pointée est un entier constant */ *pI++; /* Interdit car le pointeur est déclaré constant */ (*pI)++; /* Interdit car la valeur pointée est un entier constant */ X++; /* Interdit car X est un entier constant */

5.4.4.2.f. Tableaux et constantes

const int Taille = 3; /* Usage d’une valeur constante pour réserver un tableau */ char Ch[Taille] ; /* Réservation d’un vecteur pour 3 caractères stockés aux */

/* indices 0, 1 et 2, l’indice 3 recevant le terminateur '\0' */ const int T[] = {10, 20, 30}; /* Déclaration d’un vecteur de 3 entiers constants */ T[2]++; /* Interdit car les valeurs du vecteur sont des constantes */

5.5. Les organisations de données L’assemblage de types de données élémentaires prédéfinis du C (caractères, entiers, flottants) crée des nouveaux types représentant des ensembles de données organisées. Ces structures de données sont appelées types composés (ou structures composées) lorsqu’elles rassemblent des éléments d’un même type et types complexes (ou structures complexes) lorsqu’elles rassemblent des éléments de types différents. Dans ce document, le type composé désigne les tableaux (et les chaînes de caractères) et les énumérations tandis que le type complexe désigne la structure et l’union, même quand tous leurs membres sont d’un même type. Les champs de bits, bien qu’uniquement composés d’entiers, ne peuvent être définis qu’en tant que membre d’une structure et leur description est donc associée à celle de cette dernière.

Page 31: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 31

5.5.1. Types ou structures composées 5.5.1.1. Les tableaux Un tableau est un ensemble délimité d'éléments de même type qui sont stockés en mémoire à des adresses contiguës. Le tableau est aussi nommé vecteur lorsqu’il comprend une seule dimension et matrice lorsqu’il comprend deux dimensions. Il faut noter qu’une chaîne de caractères est en fait, un vecteur de caractères.

int MesEntiers[10]; /* Déclaration d’un vecteur de 10 entiers */ float MesReel[10]; /* Déclaration d’un vecteur de 10 réels */ char UneChaine[50]; /* Déclaration d’une chaîne de 50 caractères */ int Sudoku[9][9]; /* Déclaration d’une matrice de 9 x 9 entiers */ int UnVolume[2][3][4]; /* Déclaration d’un tableau de 2 x 3 x 4 entiers */

Dans ces déclarations, chaque paire [] désigne une dimension et la valeur entre ces crochets est la taille du tableau pour cette dimension, c’est-à-dire le nombre d’éléments permis. L’indice minimal dans une dimension est toujours 0 et par conséquent, l’indice maximal est égal à la taille de la dimension moins une unité. Par exemple, les plus grandes valeurs d’indices du tableau à 3 dimensions UnVolume sont respectivement 1, 2 et 3. Hors déclarations, les crochets servent à encadrer les indices et sont les opérateurs d’accès aux valeurs du tableau.

int i; /* Remplissage de MesEntiers avec les valeurs de ses indices */ for (i = 0; i < 10; i++)

MesEntiers[i] = i; Un tableau est un pointeur vers le premier élément du tableau. Ce pointeur est constant et cela implique en particulier qu’un tableau ne peut pas figurer à gauche d'un opérateur d'affectation. Par exemple, pour copier le vecteur MesEntiers vers un autre vecteur UnVecteur, il faut effectuer l'affectation pour chacun des éléments.

int UnVecteur[10]; int i; for (i = 0; i < 10; i++)

UnVecteur[i] = MesEntiers[i]; Un tableau peut être initialisé lors de sa déclaration par une liste de valeurs littérales, voire par une liste de listes de valeurs dans les cas de tableaux à plusieurs dimensions.

int MesEntiers[10] = {7, 77, 777}; /* Initialisation aux indices 0, 1 et 2 */ int UneAire[2][3] = {{1, 11, 111}, {2, 22, 222}}; /* Initialisation totale */

Lors d’une initialisation partielle (nombre de données de la liste inférieur à la dimension du tableau), les premiers éléments reçoivent les valeurs de la liste et les autres reçoivent la valeur nulle de leur type. Pour rappel, à défaut d’initialisation explicite, tous les éléments d’un tableau de portée globale sont initialisés à leur valeur nulle. Lorsqu’un tableau est ainsi initialisé, sa dimension de gauche peut n’être pas précisée. Elle est alors définie par le nombre de valeurs de la liste d’initialisation correspondante.

int MesEntiers[] = {7, 77, 777}; /* Initialisation d’un vecteur de 3 entiers */ int UneAire[][3] = {{1, 11, 111}, {2, 22, 222}};/* Deux lignes de 3 entiers */ int UnVolume[][2][3] = {{1,11,111},{2,22,222}}; /* 1 page de 2 lignes de 3 entiers */ int T[]; /* Erreur */ int T[] = {}; /* Vecteur pour un seul entier non initialisé */

Un tableau à plusieurs dimensions est un tableau de tableaux. Par exemple, un tableau à 3 dimensions est un tableau de matrices, la matrice étant un vecteur de vecteurs d’éléments. La visualisation d’un tel tableau sous la forme d’un ensemble de pages composées de lignes, chacune composées de colonnes d’un seul élément facilite la conception algorithmique des traitements à exécuter.

int UnVolume[2][3][4], Page, Ligne, Colonne; for (Page = 0; Page < 2; Page++)

for (Ligne = 0; Ligne < 3; Ligne++) for (Colonne = 0; Colonne < 4; Colonne++)

UnVolume[Page][Ligne][Colonne] = (Page + 1)*100 + (Ligne + 1)*10 + Colonne + 1;

Page 32: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 32

5.5.1.2. Les chaînes de caractères Une chaîne de caractères peut être initialisée, comme un vecteur d’entiers, par une liste de valeurs littérales mais elle peut aussi être initialisée par une chaîne de caractères littéralement exprimée.

char UneChaine[7] = {'A', 'Z', 'E', 'R', 'T', 'Y'}; char UneAutre[7] = "AZERTY";

Lorsque la longueur est stipulée dans la déclaration comme ci-dessus, elle doit être d’une unité supérieure au nombre maximal de caractères utiles que peut contenir la chaîne d’initialisation. L’emplacement excédentaire est occupé à la compilation par la marque de fin de chaîne, le caractère '\0', indispensable à toutes les fonctions de traitement de chaînes. Si cette longueur n’est pas indiquée, le compilateur la fixera comme étant égale au nombre de caractères de la chaîne d’initialisation augmenté d’une unité. Quand il est possible de fournir la chaîne d’initialisation complète, mieux vaut laisser le compilateur calculer la taille lui-même.

char UneChaine[] = {'A', 'Z', 'E', 'R', 'T', 'Y'}; char UneAutre[] = "AZERTY"; /* La meilleure écriture */

Une chaîne est un tableau et comme tel, elle ne peut notamment pas faire l’objet d’affectation dans le code. Les opérations de base sur les chaînes sont exécutées par des fonctions de la librairie string.h.

#include <string.h> … char MaChaine[10]; … MaChaine = "Panzani"; /* Erreur */ strcpy(MaChaine, "Panzani"); /* strcpy() réalise l’affectation */

5.5.1.3. Les énumérations Une énumération définit un type par la liste des valeurs qu'il peut prendre. Par défaut, chacune de ces valeurs, qui sont des expressions littérales quelconques, a pour valeur effective un nombre entier égal à sa position dans la liste de 0 à N-1. Des variables de ce type peuvent faire l’objet d’affectations entre elles, comme toutes les variables de type int. Plutôt que de véritables types de données, les énumérations sont des aides au service du programmeur à qui elles présentent des valeurs entières constantes sous des noms représentatifs.

enum Jours { Dimanche, Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi }; enum Mois { Janvier = 1, Fevrier = 2, Mars = 3, \

Avril = 4, Mai = 5, Juin = 6, \ Juillet = 7, Aout = 8, Septembre = 9, \ Octobre = 10, Novembre = 11, Decembre = 12 };

Les types créés ci-dessus sont enum Jours et enum Mois. Les identificateurs Jours et Mois sont nommés modèles. Le mot-clef enum et son modèle associé constituent l’identificateur du type. Il faut noter que le modèle n’est pas toujours indispensable, notamment lorsque les variables sont déclarées dans la foulée de la création du type, mais le type créé n’a pas d’identificateur et cela handicape sa réutilisabilité dans d’autres portions du code. Dans l’exemple suivant, l’énumération ne possède pas de modèle, mais une variable UnBooleen est déclarée à la suite de sa définition. Cette écriture est à reproduire partout où une nouvelle variable de ce type est requise.

enum { false = 0, true } UnBooleen; Les déclarations des variables de ce type se composent donc en principe du mot-clef enum et de son modèle, suivis par la liste des variables. Il existe cependant un outil représenté par le mot-clef typedef qui permet au programmeur de créer ses propres types, dits types utilisateurs, c’est-à-dire de renommer les types à sa guise, de créer des alias d’identificateurs de types.

typedef int Entier; /* "Crée" un type Entier équivalent au type int */ typedef int Sudoku [9][9]; /* "Crée" un type Sudoku qui est une table de 9 x 9 int */ typedef enum { false = 0, true } Booleen; /* "Crée" un type nommé Booleen */ typedef enum Jours TypeJour; /* "Crée" un type nommé TypeJour */

Page 33: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 33

On peut épingler au passage que l’identificateur d’un type structure et celui d’un type union se construisent aussi par l’association d’un mot-clef et d’un modèle et que ces types aussi peuvent être renommés par l’usage de typedef. Quelques usages (en continuation des exemples précédents) :

Entier i = 7; Sudoku SDK = {{1,2,3},{7,8,9}}; /* Initialisation partielle */ Booleen UneValeurBooleenne = false; TypeJour UnJour, UnAutre; enum Mois UnMois; UnJour = XYZ; /* Erreur à la compilation : ce n’est ni une des valeurs

de l’énumération, ni un entier */ UnMois = 77; /* Pas d’erreur, mais un non-sens car 77 ne correspond à

aucune valeur de l’énumération */ printf("%d", i); /* Affichage : 7 */ printf("%d", SDK[0][2]); /* Affichage : 3 */ printf("%d", SDK[1][1]); /* Affichage : 8 */ UnJour = Mardi; UnAutre = UnJour; /* Affectation entre variables de type énumération */ UnMois = 3; printf("%d", UnJour); /* Affichage : 2 */ printf("%d", UnAutre); /* Affichage : 2 */ printf("%d", UnMois); /* Affichage : 3 */ UnMois = Avril; printf("%d", UnMois); /* Affichage : 4 */ printf("%d", UneValeurBooleenne); /* Affichage : 0 */ UneValeurBooleenne = true; printf("%d", UneValeurBooleenne); /* Affichage : 1 */ UneValeurBooleenne = 0; printf("%d", UneValeurBooleenne); /* Affichage : 0 */ UneValeurBooleenne = 5; /* Non-sens admissible car l’énumération est écrite de

sorte que toute valeur différente de 0 vaut true */ printf("%d", UneValeurBooleenne); /* Affichage : 5 */ if (UneValeurBooleenne) printf("OK"); /* Affichage : OK */

5.5.2. Types ou structures complexes 5.5.2.1. Les structures Une structure définit un type de données qui est un ensemble d’éléments de types éventuellement différents. Chacun de ces éléments, appelé membre ou champ, est désigné par un identificateur. Des variables de ce type peuvent faire l’objet d’affectations entre elles. L’identificateur d’un type structure se construit à l’instar de celui de l’énumération, par l’association d’un mot-clef et d’un modèle et un tel type peut également être renommé par l’usage de typedef. Le mot-clef est struct. Il faut noter que le modèle n’est pas toujours indispensable, notamment lorsque les variables sont déclarées dans la foulée de la création du type, mais le type créé n’a pas d’identificateur et cela handicape sa réutilisabilité dans d’autres portions du code. Dans l’exemple suivant, la structure ne possède pas de modèle, mais une variable DegresCeJour est déclarée à la suite de sa définition. Cette écriture est à reproduire partout où une nouvelle variable de ce type est requise.

struct { int Jour; float TMinimale; float TMaximale; } DegresCeJour; L’usage d’un modèle, DegresJours dans l’exemple suivant, permet la réutilisation du type sur simple désignation de son identificateur struct DegresJours ou mieux encore sur désignation d’un alias créé avec typedef, par exemple TemperatureJour.

struct DegresJours { int Jour; float TMinimale; float TMaximale; }; typedef struct DegresJours TemperatureJour;

Page 34: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 34

Il faut être attentif à la syntaxe employée pour les définitions des types et celles de leurs alias, ainsi que pour les déclarations des variables : il ne faut pas confondre définition de types et déclaration de variables. /* Déclaration de variables d’un type structure sans modèle */

struct { int Jour; float TMinimale; float TMaximale; } DegresCeJour; struct { int Jour; float TMinimale; float TMaximale; } DegresAutreJour;

/* Déclaration d’une variable DegresCeJour d’un type structure avec modèle DegresJours */ struct DegresJours { int Jour; float TMinimale; float TMaximale; } DegresCeJour;

/* Déclaration d’une variable DegresAutreJour du type structure avec modèle DegresJours */ struct DegresJours DegresAutreJour;

/* Déclaration d’un type TemperatureJour à partir d’une structure sans modèle */ typedef struct { int Jour; float TMinimale; float TMaximale; } TemperatureJour;

/* Déclaration d’un type TemperatureJour à partir d’une structure avec modèle */ typedef struct DegresJours { int Jour; float TMinimale; float TMaximale; }

TemperatureJour; /* Déclaration d’un type TemperatureJour à partir d’une structure avec modèle existant */

typedef struct DegresJours TemperatureJour; /* Déclaration des variables DegresCeJour et DegresAutreJour à partir d’un alias */

TemperatureJour DegresCeJour, DegresAutreJour; Les variables de type structure peuvent être initialisées lors de leur déclaration par une liste de valeurs présentée entre des accolades. Lors d’une initialisation partielle d’une variable de portée locale, les premiers éléments reçoivent les valeurs de la liste et les autres reçoivent la valeur nulle de leur type.

TemperatureJour DegresCeJour = {0, 0.0, 0.0}; L’exploitation d’une variable de type structure impose d’accéder distinctement à ses différents membres. Un opérateur d’accès particulier, nommé opérateur de traversée, existe pour cela : le point (.). Il est à épingler ici que lorsque la variable est un pointeur sur une donnée de ce type (et non une simple variable), l’opérateur de traversée change : le point est remplacé par la combinaison des caractères tiret et plus grand que (->). Cette forme de l’opérateur de traversée est notamment utilisée dans les opérations sur les structures auto-référencées dont il question plus loin dans ces pages.

TemperatureJour DegresCeJour; TemperatureJour * PointeurDeDegresJour; ... DegresCeJour.Jour = 1 ; DegresCeJour.TMinimale = 10.5; DegresCeJour.TMaximale = 19.3; printf("%.2f", DegresCeJour.TMinimale); /* Affichage : 10.50 */ PointeurDeDegresJour = &DegresCeJour; /* Initialisation du pointeur */ printf("%.2f", PointeurDeDegresJour->TMaximale);/* Affichage : 19.30 */

Les champs des structures peuvent être de n’importe quel type : types de base du langage (char, int, float, …), types composés, types complexes, et même de type entier partiel : les champs de bits. 5.5.2.2. Les champs de bits La taille d’un champ d'une structure peut être définie au bit près s’il est de type entier (char, int, short, unsigned int, …) en précisant le nombre de bits de ce champ. Ce nombre de bits ne peut excéder le nombre de bits du type employé. Les affectations de champs avec des valeurs excédentaires produisent des résultats erronés. Un tel champ n’a pas d’adresse et ne peut donc pas servir à l’initialisation d’un pointeur, il n’est pas permis de lui appliquer l’opérateur &.

struct Registre { unsigned int Actif : 1; /* VarReg.Actif = 2 donne 0 pour résultat */ unsigned int Valeur : 31; } VarReg;

Page 35: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 35

5.5.2.3. Les unions Une union définit un type de données qui est un ensemble d’éléments de types éventuellement différents susceptibles d'occuper alternativement une même zone mémoire. Une union permet donc de définir un objet comme pouvant être d'un type au choix parmi l’ensemble des types contenus dans l’union. L’espace réservé en mémoire pour contenir une donnée de ce type correspond à la taille nécessaire au stockage du plus grand type spécifié dans l’union. Chacun des éléments d’une union, appelé membre ou champ, est désigné par un identificateur. Des variables de ce type peuvent faire l’objet d’affectations entre elles. L’identificateur d’un type union se construit à l’instar de ceux de l’énumération et de la structure, par l’association d’un mot-clef et d’un modèle et un tel type peut également être renommé par l’usage de typedef. Le mot-clef est union. Il faut noter que le modèle n’est pas toujours indispensable, notamment lorsque les variables sont déclarées dans la foulée de la création du type, mais le type créé n’a pas d’identificateur et cela handicape sa réutilisabilité dans d’autres portions du code. Dans l’exemple suivant, l’union ne possède pas de modèle, mais une variable UnNombre est déclarée à la suite de sa définition. Cette écriture est à reproduire partout où une nouvelle variable de ce type est requise.

union { int Entier; double Reel; } UnNombre; L’usage d’un modèle, DesNombres dans l’exemple suivant, permet la réutilisation du type sur simple désignation de son identificateur union DesNombres ou mieux encore sur désignation d’un alias créé avec typedef, par exemple TypeNombre.

union DesNombres { int Entier; double Reel; }; typedef union DesNombres TypeNombre;

Comme c’est déjà le cas avec les énumérations et les structures, il faut être attentif à la syntaxe employée pour les définitions des types et celles de leurs alias, ainsi que pour les déclarations des variables : il ne faut pas confondre définition de types et déclaration de variables. Les variables de type union peuvent être initialisées lors de leur déclaration par une seule valeur présentée entre des accolades. Cette valeur initialise le premier champ de l’union. Si la variable est permanente, à défaut d’initialisation explicite, l’espace mémoire réservé à la variable est initialisé la valeur nulle du plus grand type de l’union.

TypeNombre UnNombre = {7}; L’exploitation d’une variable de type union impose d’accéder distinctement à ses différents membres. Un opérateur d’accès particulier, nommé opérateur de traversée, existe pour cela : le point (.). Il est à épingler ici que lorsque la variable est un pointeur sur une donnée de ce type (et non une simple variable), l’opérateur de traversée change : le point est remplacé par la combinaison des caractères tiret et plus grand que (->).

TypeNombre UnNombre; printf("\n%d", sizeof(UnNombre)); /* Affichage : 8 (taille d’un double) */

Le champ Entier occupe les 4 octets les moins significatifs et le champ Reel occupe l’entièreté des 8 octets. Dans l’exemple suivant, l’affectation du champ Reel écrase le champ Entier dont les octets sont remplacés par les 4 derniers octets de la mantisse du réel, ces octets ayant la valeur 0 pour le nombre réel 10.5.

UnNombre.Entier = 5 ; UnNombre.Reel = 10.5; printf("%d", UnNombre.Entier); /* Affichage : 0 */ printf("%.2f", UnNombre.Reel); /* Affichage : 10.50 */

Le champ Reel affecté d’abord ne semble pas souffrir de l’affection du champ Entier or, l’affectation du champ Entier écrase bien les 4 derniers octets de la mantisse du champ Reel. L’affichage du champ Reel avec un nombre de décimales suffisant le prouve. Dans une union, seul le dernier champ affecté est valide.

UnNombre.Reel = 10.5; UnNombre.Entier = 5; printf("%d", UnNombre.Entier); /* Affichage : 5 */ printf("%.2f", UnNombre.Reel); /* Affichage : 10.50 */ printf("%.16f", UnNombre.Reel); /* Affichage : 10.5000000000000090 */

Page 36: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 36

Les champs des unions peuvent être de n’importe quel type : types de base du langage (char, int, float, …), types composés, types complexes, et même de type entier partiel : les champs de bits.

5.6. Les opérateurs 5.6.1. L'affectation Cette opération, aussi appelée assignation, est celle par laquelle une valeur est attribuée à une variable. Son opérateur est =. Il convient sans doute de rappeler ici, que l’affectation entre tableaux n’est pas permise. Une variable pouvant faire l’objet d’une affectation est dite Lvalue (Left value, valeur qui peut être placée à gauche d’un opérateur d’affectation). Ainsi par exemple, une chaîne de caractères n’est pas une Lvalue. Certains auteurs utilisent aussi le mot Rvalue (Right value, valeur qui peut être placée à droite d’un opérateur d’affectation) pour caractériser une valeur ou une expression susceptible d’affecter une variable.

Pi = 3.141592; L'affectation a la faculté de réaliser si nécessaire, des conversions implicites entre les types numériques de base. Lorsque la Lvalue et la Rvalue sont de types différents, cette dernière est convertie dans le type de la Lvalue. A noter que ces conversions s’effectuent aussi lors du passage de paramètres aux fonctions : un paramètre effectif est converti dans le type du paramètre formel.

int N1, N2 = 2; float F = 2.5; N1 = N2 + F; /* Affecte 4 à N1 (et non 4.5) */ F = F + N1; /* Affecte 6.5 à F */ printf("%d %.2f", N1, F); /* Affichage : 4 6.50 */

Il faut noter qu’en C l’affectation dégage une valeur : celle qui passe dans le symbole =.

printf("%d", UnEntier = 25); /* Affichage : 25 */ A noter encore que, hors déclarations, les affectations en série sont permises :

int X = Y = 0; /* Interdit */ X = Y = 0; /* A l’issue de quoi X et Y valent chacune 0 */

5.6.2. La concaténation La concaténation, qui est la mise bout à bout de chaînes de caractères, ne possède pas d’opérateur en langage C. Lorsque cette opération est requise, il faut appeler la fonction strcat() de la libraire string.h.

char MaChaine[10]; strcpy(MaChaine, "Bon"); /* strcpy() affecte "Bon" à MaChaine */ strcat(MaChaine, "jour"); /* strcat() concatène "jour" à MaChaine */ printf("%s", MaChaine); /* Affichage : Bonjour */

5.6.3. Les opérateurs arithmétiques

* Multiplication V = X * 2 / Division réelle V = X / 2 % Modulo (Reste) V = X % 2 + Addition V = X + 2 – Soustraction V = X – 2

Le caractère – est aussi le symbole de l’opérateur unaire de changement de signe.

UnEntier = 25; printf("%d", -UnEntier); /* Affichage : -25 */

Page 37: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 37

Ces opérateurs agissent généralement de la façon attendue sur les entiers comme sur les réels. Toutefois, quelques remarques s’imposent : L'opérateur % ne s'applique qu'à des opérandes de type entier (char, short, int, …). Si l'un des deux opérandes

est négatif, le signe du reste dépend de l'implémentation, mais il est en général le même que celui du dividende. Contrairement à certains langages, le C ne dispose pas d’opérateur d’élévation à une puissance. Toutefois, la

fonction pow(x, y) de la librairie math.h effectue le calcul de x exposant y (xy). Le C ne dispose pas non plus d’opérateur de division entière, mais du fait de la conversion implicite, l’affectation

du résultat d’une division quelconque à une variable de type int livre un quotient entier. Lorsque ses opérandes sont d’un même type, la division produit un résultat du même type. Par contre, si un des

opérandes est réel, la division produit un résultat réel.

float F; F = 7 / 2; /* Les valeurs littérales sont entières */ printf("%.2f", F); /* Affichage : 3.00 */ F = 10. / 3.; /* Les valeurs littérales sont réelles */ printf("%.2f", F); /* Affichage : 3.33 */ F = 7. / 2; /* Dividende réel et diviseur entier */ printf("%.2f", F); /* Affichage : 3.50 */ F = 10 / 3.; /* Dividende entier et diviseur réel */ printf("%.2f", F); /* Affichage : 3.33 */

5.6.4. Les formes contractées de l’affectation Il s’agit de la combinaison d’un des opérateurs précédents avec l’opérateur d’affectation, avec pour effet une économie d’écriture. On retrouve là tous les opérateurs arithmétiques. La signification de Variable Opérateur = Valeur est la même que celle de Variable = Variable Opérateur Valeur.

*= V *= 3 est équivalent à V = V * 3 /= V /= 4 est équivalent à V = V / 4 %= V %= 2 est équivalent à V = V % 2 += V += 6 est équivalent à V = V + 6 -= V -= 7 est équivalent à V = V – 7

5.6.5. Les opérateurs relationnels Les opérateurs relationnels, dits aussi opérateurs de comparaisons, sont indispensables à la définition des critères de choix des alternatives et à la définition des conditions d’arrêt des itérations logiques. Des expressions utilisant ces opérateurs fournissent des résultats qui peuvent être affectés à des variables d’un type numérique. Ces résultats sont 1 ou 0 selon que l’expression évaluée est vraie ou fausse. Il convient de se rappeler qu’il n’existe pas de type booléen en ANSI C et que toute valeur différente de 0 est considérée comme vraie.

< Inférieur à X = V < 2 si V = 3, X reçoit la valeur 0 <= Inférieur ou égal à X = V <= 2 si V = 3, X reçoit la valeur 0 > Supérieur à X = V > 2 si V = 3, X reçoit la valeur 1 >= Supérieur ou égal à X = V >= 2 si V = 3, X reçoit la valeur 1 == Egal à X = V == 2 si V = 3, X reçoit la valeur 0 != Différent de X = V != 2 si V = 3, X reçoit la valeur 1

Il ne faut pas confondre l'opérateur relationnel d'égalité == et l'opérateur d'affection =. Par exemple, le code suivant produit une information erronée.

int a = 0, b = 1; if (a = b)

printf("a et b sont égales \n"); /* Affichage d’une idiotie */ else

printf("a et b sont différentes \n"); Comme indiqué précédemment, l’affectation génère une valeur égale à la valeur mouvementée. Or, dans le code ci-dessus, l’alternative n’évalue pas une expression relationnelle, mais l’affectation de a par b et la valeur mouvementée est 1 (donc logiquement vraie). C’est vrai qu’à l’issue de cette affectation, a et b sont égales …

Page 38: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 38

Si le code est récrit avec if (b = a), l’affichage semble pertinent car dans l’affectation de b par a, la valeur mouvementée est 0 (donc logiquement fausse). Mais à l’issue de cette affectation, a et b sont égales … 5.6.6. Les opérateurs logiques Lorsque les définitions des critères d’alternatives et des conditions d’arrêt d’itérations ne peuvent se suffire d’une seule expression relationnelle, les opérateurs logiques permettent d’en combiner plusieurs. Des expressions utilisant ces opérateurs fournissent des résultats qui peuvent être affectés à des variables d’un type numérique. Un tel résultat vaut 1 ou 0 selon que l’expression évaluée est vraie ou fausse.

! Négation (contraire) && Et (résultat vrai si les deux opérandes sont vrais) || Ou (résultat vrai si au moins un des opérandes est vrai)

L’opérateur XOR (ou exclusif) n’existe pas en C, mais il se programme aisément par l’usage des trois opérateurs disponibles. Ainsi, l’expression (A XOR B) se programme : ((A && !B) || (!A && B)). L’évaluation d’une expression logique s’effectue de gauche à droite et s’arrête dès que le résultat est déterminé. Ainsi par exemple, dans le code suivant, si dans la première expression relationnelle, n est différent de 0, les autres expressions relationnelles ne sont pas évaluées. En effet, si (n == 0) est faux, toute l’expression est fausse, quelle que soit la valeur de la variable i.

if ((n == 0) && (i >= 0) && (i <= 9)) Pour rappel, lorsque des expressions logiques sont combinées avec l’opérateur ||, il suffit qu’une seule des expressions soit vraie pour que le résultat global soit vrai. Inversement, si des expressions logiques sont combinées avec l’opérateur &&, il suffit qu’une seule des expressions soit fausse pour que le résultat global soit faux. 5.6.7. Les opérateurs binaires (bit à bit) Les opérateurs binaires permettent les opérations bit à bit sur les octets composant les nombres entiers, signés ou non.

~ Complément à 1 (tous les bits sont inversés) & Garde les bits à valeur 1 dont le vis-à-vis a la valeur 1 | Garde tous les bits à valeur 1 ^ Garde les bits à valeur 1 dont le vis-à-vis a la valeur 0 >> Décalage, d’un nombre donné de bits, de tous les bits vers la droite (les bits sortants sont perdus) << Décalage, d’un nombre donné de bits, de tous les bits vers la gauche (les bits sortants sont perdus)

A B ~ A A & B A | B A ^ B A >> 2 A << 2

Combinaison 00010111 00001010 11101000 00000010 00011111 00011101 00000101 01011100

Valeur 23 10 -24 2 31 29 5 92

A l’exception de l’opérateur ~, les opérateurs binaires disposent également d’affectations contractées :

&= V &= 4 est équivalent à V = V & 4 |= V |= 3 est équivalent à V = V | 3 ^= V ^= 4 est équivalent à V = V ^ 4 >>= V >>= 3 est équivalent à V = V >> 3 <<= V <<= 4 est équivalent à V = V << 4

A noter qu’un décalage d’un bit vers la gauche correspond à une multiplication par 2 et qu’un décalage d’un bit vers la droite correspond à une division par 2.

int N = 3; N <<= 1; /* Multiplication de N par 2 */ printf("%d", N); /* Affichage : 6 */

Ordinairement, en matière d’opérateurs, le mot binaire qualifie un opérateur traitant deux opérandes, de même que les mots unaire et ternaire qualifient respectivement les opérateurs traitant un seul opérande et ceux qui en traitent trois. Ici, l’emploi du mot binaire désigne le type de traitement assigné à ces opérateurs : le calcul binaire.

Page 39: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 39

5.6.8. Les opérateurs d'incrémentation et de décrémentation Les opérateurs d'incrémentation ++ et de décrémentation -- s'utilisent aussi bien en préfixe qu’en suffixe. Placés en préfixe (++i, --i), ils réalisent respectivement la pré-incrémentation et la pré-décrémentation. Placés en suffixe (i++, i--), ils réalisent respectivement la post-incrémentation et la post-décrémentation. L’opérande (i dans ces exemples) est mouvementé dans tous les cas, mais le résultat final de l’expression exploitant ces opérateurs est différent selon qu’ils sont positionnés en préfixe ou en suffixe. Lorsque l’opérateur est placé en préfixe d’une variable, l’incrémentation ou la décrémentation est effectuée avant la suite de l’expression. Au contraire, lorsque l’opérateur est placé en suffixe, l’incrémentation ou la décrémentation n’est effectuée qu’après achèvement de toute l’expression.

int X = 1, Y = 2, R; R = X++; printf("R = %d", R); /* Affichage : R = 1 */ printf("X = %d", X); /* Affichage : X = 2 */ R = ++Y; printf("R = %d", R); /* Affichage : R = 3 */ printf("Y = %d", Y); /* Affichage : Y = 3 */ R = X-- + --Y; printf("R = %d", R); /* Affichage : R = 4 */ printf("X = %d", X); /* Affichage : X = 1 */ printf("Y = %d", Y); /* Affichage : Y = 2 */

5.6.9. La virgule, opérateur séparateur d’expression Il est possible en C, d’écrire une expression composée d’une suite de sous-expressions séparées par une virgule. Cette expression est évaluée de gauche à droite, en évaluant et traitant chaque sous-expression. Si l’ensemble doit livrer un résultat, l’expression composée doit être placée entre parenthèses. C’est dans ce cas, la dernière sous-expression qui détermine le résultat.

X = (Y = 3, Y + 2); /* Y + 2 détermine le résultat */ printf("X = %d", X); /* Affichage : X = 5 */ printf("Y = %d", Y); /* Affichage : Y = 3 */ X = 7, Y = 9; /* Aucun résultat à livrer */ printf("X = %d", X); /* Affichage : X = 7 */ printf("Y = %d", Y); /* Affichage : Y = 9 */

La virgule séparant les arguments d'une fonction ou les déclarations de variables n'est pas l'opérateur virgule. En particulier l'évaluation de gauche à droite n'est pas garantie. Par exemple, avec Dev-Cpp sous Windows, sur un ordinateur équipé d’un processeur Intel 32 bits, les lignes suivantes produisent un affichage qui montre bien que l’expression a été évaluée de droite à gauche (dans le cas contraire, l’affichage aurait été : 2 2).

int X = 1; printf("%d %d", ++X, X); /* Affichage : 2 1 */

5.6.10. L'alternative contractée ou l’opérateur conditionnel ternaire L’alternative contractée s’écrit avec un opérateur ternaire représenté par deux symboles : le point d’interrogation (?) et le double-point (:). La condition peut être écrite comme toutes les conditions en C, c’est-à-dire entre parenthèses. Il faut lire une telle expression comme étant : Si (condition) Alors ValeurDeRetour Sinon AutreValeurDeRetour, le Si étant omis, le Alors remplacé par le ? et le Sinon par le :.

int X = -1, Y = 3; printf("Absolue de X = %d", (X >= 0) ? X : -X); /* Affichage : Absolue de X = 1 */ printf("Maximum = %d", (X > Y) ? X : Y); /* Affichage : Maximum = 3 */

Page 40: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 40

5.6.11. L'opérateur de conversion de type L'opérateur de conversion de type, appelé cast, permet de modifier explicitement le type d'une variable ou du résultat d’une expression. Le type souhaité est écrit entre parenthèses à gauche de l’objet à convertir.

int X = 3, Y = 2; float F; F = X / Y; printf("%.2f", F); /* Affichage : 1.00 */ F = (float)X / Y; printf("%.2f", F); /* Affichage : 1.50 */

5.6.12. Les opérateurs de référencement et de déréférencement Les deux opérations de base du travail avec les pointeurs sont réalisées par des opérateurs unaires représentés par les caractères esperluette (&) et astérisque (*). L’esperluette est l’opérateur de référencement, dit aussi opérateur d’adresse. Son rôle est de fournir l’adresse de son opérande. L’astérisque est l’opérateur de déréférencement, dit aussi opérateur d’accès à la valeur pointée. Son rôle est de fournir la valeur stockée à l’adresse mémoire désignée par le pointeur. Il n’est pas encore temps d’aborder l’étude des pointeurs, mais voici tout de même une illustration de l’emploi de ces opérateurs :

int *pI; /* Déclaration d’un pointeur d’entier pI */ int X = 3; /* Déclaration d’un entier X initialisé à 3 */ pI = &X; /* Initialisation du pointeur pI avec l’adresse de X */ printf("%X", &pI); /* Affichage : 22FF74 */ printf("%X", &X); /* Affichage : 22FF70 (adresse de X) */ printf("%X", pI); /* Affichage : 22FF70 */ printf("%d", *pI); /* Affichage : 3 (valeur pointée par pI) */

5.6.13. Opérateur d’accès et opérateurs de traversée L’opérateur d’accès (dit aussi d’indexation) permet l’accès à une donnée stockée dans un tableau par la désignation d’indice(s). L’opérateur d’accès est représenté par la paire de crochets []. L’opérateur de traversée permet l’accès à une donnée stockée dans une structure ou une union par la désignation de son identificateur. L’opérateur de traversée est représenté par le point (.) si la manipulation est faite par une variable et par la symbolisation d’une flèche (->) si la manipulation est faite par un pointeur. 5.6.14. Règles de priorité des opérateurs Le tableau ci-contre présente les opérateurs par ordre décroissant des priorités. Les opérateurs placés sur une même ligne ont même priorité. Si dans une expression figurent plusieurs opérateurs de même priorité, l'ordre d'évaluation est défini par la flèche de la seconde colonne du tableau. Il peut être utile de mettre des parenthèses en cas de doute ... Par exemple, les opérateurs bit à bit sont moins prioritaires que les opérateurs relationnels. Sous réserve du résultat escompté, il est généralement nécessaire d’écrire les tests sur les bits entre parenthèses.

int X = 0, Y = 3; printf("%d", X); /* Affichage : 0 */ printf("%d", Y); /* Affichage : 3 */ printf("%d", Y ^ X); /* Affichage : 3 (Y XOR X vaut 3) */ printf("%d", (Y ^ X) != 0); /* Affichage : 1 (l’expression est vraie) */ printf("%d", Y ^ X != 0 ); /* Affichage : 3 (l’expression X != 0 est fausse) */

Opérateurs Sens () [] -> . ! ~ ++ -- - (cast) * & (pointeurs) sizeof * / % + - << >> < <= > >= == != & ^ | && || ? : = += -= *= /= %= &= ^= |= <<= >>= ,

Page 41: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 41

5.7. Les structures de contrôle 5.7.1. Définitions préalables 5.7.1.1. La séquence C’est la structure de programmation la plus élémentaire. Toutes les instructions ainsi programmées s’exécutent l’une après l’autre, successivement, séquentiellement et sans aucun contrôle. Le code suivant constitue une séquence :

int X = 3, Y = 2; float F; F = X/Y; printf("%.2f", F); F = (float)X/Y; printf("%.2f", F);

5.7.1.2. La rupture de séquence Les langages offrent diverses solutions permettant le contrôle de l’exécution de certaines séquences. La plus élémentaire de ces solutions est la rupture de séquence. Le programmeur peut interrompre le déroulement d’une séquence en détournant son cours normal vers d’autres codes. La rupture de séquence permet aussi la réexécution de codes en détournant le déroulement de la séquence vers son début.

La rupture de séquence inconditionnelle. La rupture de séquence conditionnelle. Bien que présente sous l’une ou l’autre forme dans tous les langages, l’instruction "Aller à" doit toujours être remplacée par l’usage d’alternatives bien construites ou d’itérations judicieusement organisées. Cette instruction n'est tolérable que dans les langages pauvres en contrôles d’alternatives et d’itérations (script MS-DOS, Assembleur, … ). 5.7.1.3. L’alternative L’alternative est une instruction de contrôle de l’exécution du code qui permet ordinairement le choix entre deux séquences distinctes. Dans des formes imbriquées, elle permet le choix d’une séquence parmi plusieurs. 5.7.1.4. Les itérations Les instructions de contrôle d’itérations sont nombreuses. Elles permettent toutes l’organisation et le contrôle de répétitions de séquences. Certaines permettent un nombre déterminé de répétitions, d’autres autorisent la répétition jusqu’à la réalisation d’une condition préalablement définie.

Condition

Aller à Action(s) répétées

Action(s)

Aller à

Action(s)

Action(s) ignorées

Action(s) ignorées

Page 42: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 42

5.7.2. Les ruptures de séquences Bien que l’usage de branchements de type "Aller à" soit à proscrire, voici un exemple caricatural de son emploi en langage C. La rupture de séquence se programme par le mot-clef goto associé à l’identificateur d’une étiquette qui désigne la destination du branchement. L’étiquette est constituée d’un identificateur suivi d’un double point. Contrairement à d’autres langages, elle ne doit pas obligatoirement être collée à la marge, son indentation est permise.

void Branchement() { printf("Message 1"); goto Etiq1; /* Un branchement */ Etiq2: /* Une étiquette */ printf("Message 3"); goto Etiq3; Etiq1: printf("Message 2"); goto Etiq2; Etiq4: return; Etiq3: printf("Message 4"); goto Etiq4; }

Si l’usage d’alternatives bien construites et d’itérations judicieusement organisées permet de créer du code sans rupture de séquence, il est toutefois admis de briser certains déroulements de traitements par l’emploi des instructions break, continue, return. Les branchements tels que break et continue, de même que return lorsqu’il ne sert qu’à éviter une séquence de codes, doivent être utilisés avec modération. Il existe aussi des instructions particulières, exit et abort, qui provoquent l’arrêt inconditionnel du programme et le retour au système d’exploitation (voir leurs différences plus loin, sous le titre « Extraits des librairies standards »). 5.7.2.1. L’instruction de branchement break L'instruction break s’utilise à l’intérieur de n’importe quelle itération et à l’intérieur de l’instruction de branchement multiple, plus communément appelée sélection et décrite plus loin dans ces pages. Dans un cas comme dans l’autre elle provoque la sortie inconditionnelle de l’instruction de contrôle qui la contient.

int X = 11; while (X > 5) { X--; if (X == 7) /* Les valeurs 7, 6 et 5 de X ne sont pas affichées */ break; printf("%d ", X); /* Affichage : 10 9 8 */ }

5.7.2.2. L’instruction de branchement continue L'instruction continue s’utilise à l’intérieur de n’importe quelle itération. Elle provoque le passage au tour suivant de la boucle sans exécuter les autres instructions de la boucle.

int X = 11; while (X > 5) { X--; if (X == 7) /* Le valeur 7 de X n’est pas affichée */ continue; printf("%d ", X); /* Affichage : 10 9 8 6 5 */ }

Spaghetti ?

Page 43: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 43

5.7.3. L’alternative Le contrôle d’une alternative se réalise toujours par l’évaluation d’une condition. Les alternatives, comme toutes les structures de contrôles, peuvent être imbriquées. Cela signifie qu’une ou plusieurs actions contrôlées peuvent elles-mêmes être des structures de contrôle, et même des alternatives. Les alternatives peuvent également être assemblées en cascades. L’usage de plusieurs alternatives permet l’évaluation de plusieurs conditions. Mots-clefs : if et else. Dans sa forme la plus simple, l’alternative permet ou non l’exécution d’une instruction.

if (X != 0) Z = Y / X; /* Ce calcul se fait seulement si X n’est pas nul */

Comme déjà mentionné, plusieurs instructions peuvent être rassemblées par des accolades {} pour former une instruction composée (ou bloc). C'est ainsi qu'on peut soumettre plusieurs instructions à n’importe quel contrôle.

if (X != 0) {

Z = Y / X; printf("%d", Z);

} Dans une forme plus élaborée, l’alternative réalise le choix entre deux portions de code par l’emploi du mot-clef else.

if (X != 0) {

Z = Y / X; printf("%d", Z);

} else

printf("Erreur : diviseur nul !");

Les alternatives peuvent également être assemblées en cascades.

if (X < 0) printf("Le nombre est négatif");

else if (X > 0)

printf("Le nombre est positif"); else

printf("Le nombre est nul"); Vu que toute valeur non nulle est considérée comme vraie, les deux lignes suivantes sont équivalentes :

if (X != 0) if (X)

Vu que toute valeur nulle est considérée comme fausse, les deux lignes suivantes sont équivalentes :

if (X == 0) if (!X)

Les équivalences précédentes ne sont pas réservées aux conditions des alternatives : elles s’appliquent à toutes les expressions logiques.

Condition

Action(s) contrôlée(s)

Condition

Action(s) contrôlée(s)

Action(s) contrôlée(s)

1 2

Page 44: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 44

5.7.4. La sélection La sélection est une forme élaborée de l’alternative qui remplace avantageusement toute cascade d’alternatives, dont chacune évalue sa condition sur les valeurs possibles littéralement exprimées d’une seule et même variable commune d’un type entier ou d’une expression. Contrairement à certains langages, il n’est pas permis en C d’énumérer ces valeurs possibles par plages : chaque cas doit proposer une et une seule valeur. Les mots-clefs de ce mécanisme sont switch, case, default et break, encore que ce dernier ne soit pas uniquement réservé à la sélection. Soit par exemple, pour déterminer si une lettre majuscule ou minuscule est une voyelle :

switch(CodeAscii) /* Type entier */ { case 65 : /* Code ASCII de A */ case 65 + 32 : /* Code ASCII de a */ case 'E' : /* et les autres … */ case 'E' + 32 : case 'I' : case 'i' : case 'O' : case 'o' : case 'U' : case 'u' : case 'Y' : case 'y' :

printf ("La lettre est une voyelle") ; break ;

case 'B' : case 'C' : case 'D' : case 'F' : case 'G' : /* Les consonnes en */ case 'H' : case 'J' : case 'K' : case 'L' : case 'M' : /* majuscules et … */ case 'N' : case 'P' : case 'Q' : case 'R' : case 'S' : case 'T' : case 'V' : case 'W' : case 'X' : case 'Z' : case 'b' : case 'c' : case 'd' : case 'f' : case 'g' : /* … en minuscules */ case 'h' : case 'j' : case 'k' : case 'l' : case 'm' : case 'n' : case 'p' : case 'q' : case 'r' : case 's' : case 't' : case 'v' : case 'w' : case 'x' : case 'z' :

printf("La lettre est une consonne"); break ;

default : /* Pour d’autres codes */ printf("Le caractère proposé n’est pas une lettre");

} L’instruction switch() présente une variable ou une expression et l’ensemble de son corps s’écrit toujours entre des accolades. Ce corps contient un ou plusieurs case (généralement plusieurs), chacun suivi d’un entier ou d’une expression composée d’entiers (pas de variable) et d’un double-point. Chaque case est un point d’entrée dans l’ensemble des codes contrôlés par la sélection : le code qui suit le double-point est exécuté de même que toutes les autres lignes jusqu’à l’accolade fermante. L’instruction break sert à quitter la sélection après l’exécution des instructions présentes au point d’entrée. Il n’est en effet que rarement souhaitable d’exécuter aussi les instructions des autres case. Le mot-clef default est aussi un point d’entrée dans la sélection : c’est celui pour lequel aucune valeur n’est spécifiée. Son usage est facultatif. Dans l’exemple précédent, toutes les valeurs des voyelles sont proposées, chacune dans un case. Si CodeAscii vaut 69 (caractère E), l’entrée se fait sur case 'E' où il n’y a rien à exécuter. Le programme continue son activité au case 'E' + 32, puis au suivant, et ainsi de suite jusqu’au case 'y' où il y a enfin un traitement : printf("La lettre est une voyelle"). Ce traitement est suivi d’un break faute de quoi ce programme afficherait aussi que "la lettre est une consonne" et, toujours à défaut de break, finirait par afficher encore que "le caractère proposé n’est pas une lettre" !

Page 45: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 45

5.7.5. Les itérations Les itérations permettent de répéter une série d'instructions tant qu'une certaine condition est vérifiée (boucles logiques) ou encore un nombre de fois prédéterminé (boucle arithmétique). 5.7.5.1. Les boucles logiques Le langage C propose deux boucles "Tant que". L’une (mot-clef : while) réalise le contrôle des itérations avant la portion de code contrôlée. Cela implique que ce code peut n’être jamais exécuté. L’autre (mots-clefs : do et while) réalise le contrôle des itérations après la portion de code contrôlée. Cela implique que ce code est toujours exécuté au moins une fois.

int X = 5; while (X > 0) /* Tant que X > 0 */ { printf("%d ", X); /* Affichage : 5 4 3 2 1 */ X--; }

int X = 5; /* La boucle ne peut tourner avec cette valeur de X */ while (X < 5) /* Tant que X < 5 */ { printf("%d ", X); /* N’affiche rien. Le code contrôlé n’est pas exécuté */ X++; }

int X = 5; /* La boucle ne peut tourner avec cette valeur de X */ do { printf("%d ", X); /* Affichage : 5. Le code contrôlé est exécuté une fois */ X++; } while (X < 5); /* Tant que X < 5 */

Contrairement à d’autres langages, le C ne propose pas de boucle "Jusqu’à". Cette structure de contrôle se programme en C par une boucle "Tant que" dont la condition est modifiée en conséquence.

int X = 0; while (!(X > 5)) /* Tant que pas X > 5 ou jusqu’à ce que X > 5 */ { printf("%d ", X); /* Affichage : 0 1 2 3 4 5 */ X++; }

int X = 0; do { printf("%d ", X); /* Affichage : 0 1 2 3 4 5 */ X++; } while (!(X > 5)); /* Tant que pas X > 5 ou jusqu’à ce que X > 5 */

5.7.5.2. La boucle arithmétique La boucle arithmétique automatique est ordinairement basée sur une variable, dite variable de boucle, qui est un compteur dont la valeur initiale, la valeur limite et le pas d’incrémentation sont prédéfinis. Le code contrôlé est exécuté un nombre de fois prédéterminé. La variable de boucle doit être d’un type numérique. Mot-clef : for. Dans d’autres langages, tel le Pascal ou le VB, une telle boucle arrête ses itérations dès que la variable reçoit une valeur qui excède la valeur limite. En C, la boucle réalise ses itérations tant qu’une condition donnée est respectée, elle fonctionne comme une boucle "Tant que". Cette condition ne doit pas nécessairement concerner la variable de boucle.

int X; for (X = 0; X < 6; X++) /* En faisant varier X au pas de 1, tant que X < 6 */ printf("%d ", X); /* Affichage : 0 1 2 3 4 5 */

Page 46: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 46

La boucle précédente peut donc être lue comme une boucle while et même être programmée comme telle. La comparaison de cet exemple avec le suivant montre l’avantage du programmeur qui peut, avec la boucle for, régler l’initialisation de la variable de boucle, son incrémentation et la condition de fonctionnement de la boucle dans l’instruction de contrôle elle-même.

int X = 0; while (X < 6)) /* Tant que X < 6 */ { printf("%d ", X); /* Affichage : 0 1 2 3 4 5 */ X++; /* En faisant varier X au pas de 1 */ }

Comme signalé ci-dessus, la condition de fonctionnement des itérations ne doit pas nécessairement tenir compte de la variable de boucle.

int X, A = 5; for (X=1; A > 0; X++) printf("%d/%d ", X, A--); /* Affichage : 1/5 2/4 3/3 4/2 5/1 */

La syntaxe de cette instruction de contrôle intègre trois instructions, chacune pouvant être une instruction composée d’expressions séparées par une virgule (l’opérateur séparateur d’expressions). La première de ces instructions est destinée à l’initialisation de la variable de boucle, la seconde détermine la condition d’arrêt et la troisième définit essentiellement l’opération d’incrémentation de la variable de boucle, mais elle peut contenir aussi d’autres opérations. Par exemple, le calcul (non récursif) d’une factorielle se programme raisonnablement comme ceci :

int N, i, Fact; /* Calculer dans Fact, la factorielle de N. Variable de boucle i */ scanf("%d", &N); /* Saisie de N */ Fact = 1; /* Initialisation de la variable avec 1 (car produit) */

for (i = 1; i<=N; i++) Fact *= i;

printf("%d %d ", N, Fact); /* Affichage : 5 120 (si N vaut 5) */ Il est aussi raisonnable d’intégrer l’initialisation de Fact et son calcul Fact *= i dans l’instruction de contrôle :

int N, i, Fact; /* Calculer dans Fact, la factorielle de N. Variable de boucle i */ scanf("%d ", &N); /* Saisie de N */

for (i=1, Fact = 1; i<=N ; Fact *= i, i++); printf("%d %d ", N, Fact); /* Affichage : 5 120 (si N vaut 5) */

Il est encore raisonnable de regrouper le calcul Fact *= i et l’incrémentation de la variable de boucle i :

int N, i, Fact; /* Calculer dans Fact, la factorielle de N. Variable de boucle i */ scanf("%d", &N); /* Saisie de N */

for (i=1, Fact = 1; i<=N ; Fact *= i++); printf("%d %d ", N, Fact); /* Affichage : 5 120 (si N vaut 5) */

Il est même possible d’insérer aussi l’acquisition de la valeur de N dans l’instruction de contrôle :

int N, i, Fact; /* Calculer dans Fact, la factorielle de N. Variable de boucle i */ for (i=1, Fact = 1, scanf("%d", &N); i<=N ; Fact *= i++); printf("%d %d ", N, Fact); /* Affichage : 5 120 (si N vaut 5) */

Aussi spectaculaire que puisse paraître ce type d'acrobatie, il n'apporte rien d’autre qu’une diminution de la lisibilité et il est à rejeter d’une programmation soignée. 5.7.6. La récursivité La récursivité est un dispositif par lequel une fonction s’appelle elle-même (récursivité directe) ou par lequel une fonction en appelle une seconde qui à son tour, rappelle la première (récursivité croisée). Elle est davantage une organisation algorithmique d’une ou plusieurs fonctions qu’un moyen de contrôle de l’exécution d’une portion de code.

Page 47: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 47

Contrairement à l’approche itérative, qui possède plusieurs outils de programmation et de contrôle d’itérations, la récursivité ne dispose pas d’opérateur, ni de fonction, ni d’un quelconque moyen spécifique : elle utilise les moyens de contrôles de la programmation itérative. De plus, tous les algorithmes récursifs (ou presque) peuvent être remplacés par des algorithmes itératifs. Par exemple, le programme suivant exploite deux fonctions de calcul de la factorielle, l’une itérative et l’autre récursive. int FactorielleIterative(int N) /* Fonction itérative de calcul de la factorielle */ {

int i, F = 1; for (i = N; i > 0; i--)

F = F * i; return F;

} int FactorielleRecursive(int N) /* Fonction récursive de calcul de la factorielle */ {

if (N > 1) return (N * FactorielleRecursive(N – 1));

else return N;

} main() {

int A; printf("\Calculer la factorielle de : "); scanf("%d", &A); printf("\n%d", FactorielleIterative(A)); printf("\n%d", FactorielleRecursive(A)); printf("\n\n"); system("Pause"); return 0;

}

Page 48: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 48

6. Aspects avancés

6.1. Les fonctions La programmation procédurale consiste essentiellement en une découpe structurée d’un programme en fonctions. Une fonction générale, nommée main en C, appelle des fonctions secondaires, elles-mêmes en appelant d’autres, et ainsi de suite jusqu’à l’exécution de la tâche la plus infime. L’appel d’une fonction produit l’installation de ses variables locales dans la pile (stack segment). L’espace ainsi occupé est libéré aussitôt terminé le travail de la fonction. 6.1.1. Définition d’une fonction Une fonction doit obligatoirement être définie. La définition consiste en l’écriture de la ligne d’entête de la fonction suivie d’une paire d’accolades encadrant le corps de la fonction, c’est-à-dire le texte de son algorithme, son code. Elle mentionne l’identificateur de la fonction, le type de la valeur de retour et les types et les noms des arguments dans leur ordre d’utilisation. Au niveau de la définition de la fonction, les arguments sont nommés paramètres formels par opposition aux paramètres effectifs qui sont ceux effectivement utilisés lors de l’appel de la fonction. Lorsqu’une fonction n’a pas de paramètre, la liste de ses paramètres formels est remplacée par le mot-clef void (qui peut être omis). Certaines fonctions, réalisant des tâches qui seraient confiées à des procédures dans d’autres langages, ne retournent aucune valeur. Le type de leur valeur de retour est spécifié par le mot-clef void (qui peut être omis). La valeur de retour d’une fonction est transmise par l’instruction return suivie d’une valeur littérale, d’une variable ou d’une expression idéalement écrite entre parenthèses. Plusieurs instructions return peuvent apparaître dans une fonction. Le retour au programme appelant est provoqué par le premier return rencontré lors de l'exécution. Si la fonction ne retourne pas de valeur (fonction de type void), sa définition s'achève par return; (qui peut être omis). 6.1.1.1. Exemples de définitions

int MaxAB(int A, int B) /* Reçoit deux entiers et retourne un entier */ {

if (A > B) return A; /* return l’un */

return B; /* ou l’autre */ } int Produit(int A, int B) /* Reçoit deux entiers et retourne un entier */ {

return (A * B); /* Expression entre parenthèses */ } int Puissance(int A, int N) /* Reçoit deux entiers et retourne un entier */ {

if (N == 0) return 1;

return (A * Puissance(A, N-1));/* Appel récursif */ } void PasseALaLigne(void) /* Ne reçoit rien et ne retourne rien */ {

printf("\n"); return; /* return facultatif */

} void AfficheVecteur(int *V, int N) /* Reçoit un pointeur d’entiers et un entier */ {

int i; /* Déclaration d’une variable locale */ for (i = 0; i < N; i++)

printf("%d\t", V[i]); PasseALaLigne(); /* Appel d’une autre fonction secondaire */

}

Page 49: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 49

La définition d'une fonction secondaire peut être écrite n’importe où pour autant que ce ne soit pas à l’intérieur d’une autre fonction (le C n'autorise pas l’écriture de fonctions imbriquées). Toutefois, il est indispensable que le compilateur connaisse la fonction au moment où celle-ci est appelée. Si une fonction est définie après son premier appel (son code étant écrit après celui de la fonction appelante), elle doit impérativement avoir été déclarée. 6.1.2. Déclaration ou prototypage La déclaration (ou prototypage) des fonctions est donc facultative si celles-ci sont définies dans le bon ordre (chacune avant son premier appel). Cette condition peut devenir très contraignante pour le programmeur dont l’application comporte beaucoup de fonctions. La déclaration des fonctions permet au programmeur de les définir ensuite dans n’importe quel ordre. Elle contribue aussi à certaines conversions implicites entre paramètres effectifs et paramètres formels. Lorsqu’une fonction est déclarée, sa définition peut omettre l’indication du type de la valeur de retour. La déclaration consiste en l’écriture de la ligne d’entête de la fonction suivie d’un point-virgule. Elle mentionne l’identificateur de la fonction, le type de la valeur de retour et les types des arguments dans leur ordre d’utilisation. Il faut noter ici qu’il n’est pas nécessaire de nommer les arguments au niveau de la déclaration. 6.1.2.1. Exemples de déclarations

int MaxAB(int, int); /* Reçoit deux entiers et retourne un entier */ int Produit(int, int); /* Reçoit deux entiers et retourne un entier */ void PasseALaLigne(void); /* Ne reçoit rien et ne retourne rien */ void AfficheVecteur(int *, int); /* Reçoit un pointeur d’entiers et un entier */

6.1.3. Appel d'une fonction L'appel d'une fonction se fait par l’écriture de son identificateur suivi par une paire de parenthèses encadrant les paramètres effectifs éventuels. L'ordre et le type de ces paramètres effectifs doivent correspondre à ceux spécifiés dans l'entête de la fonction. Les paramètres effectifs peuvent être des expressions. La virgule qui sépare deux paramètres effectifs est un simple signe de ponctuation : il ne s'agit pas de l'opérateur virgule. Cela implique en particulier que l'ordre d'évaluation des paramètres effectifs n'est pas assuré et dépend du compilateur. C’est pourquoi il est déconseillé, pour une fonction à plusieurs paramètres, d’utiliser des opérateurs d'incrémentation ou de décrémentation (++ ou --) dans les expressions définissant les paramètres effectifs. Lorsqu’une valeur de retour est attendue, l’appel de la fonction doit être écrit dans une expression qui exploite la valeur retournée, à moins de n’avoir rien à faire de cette valeur … 6.1.3.1. Exemples d’appels

int N1, N2, P; printf("Encodez deux entiers séparés par un espace : "); scanf("%d%d", &N1, &N2); printf("Le plus grand des deux entiers est : %d", MaxAB(N1, N2)); PasseALaLigne(); P = Produit(N1, N2) ; printf("Leur produit vaut : %d", P);

6.1.4. Transmission des paramètres par valeurs et par pointeurs Les paramètres d'une fonction sont traités de la même manière que les variables locales de classe automatique. Lors de l'appel de la fonction, ce sont les copies des paramètres effectifs qui sont empilés dans le stack segment. La fonction travaille alors uniquement sur ces copies et celles-ci disparaissent lors du retour au programme appelant. Cela implique en particulier que si la fonction modifie la valeur d'un de ses paramètres, seule la copie s’en trouve modifiée et la variable du programme appelant reste inchangée. Il s’agit du passage de paramètres par valeurs. void Permut(int A, int B) {

int t; printf("A = %d\tB = %d\n", A, B); /* Affichage : A = 2 B = 5 */ t = A; A = B; B = t; printf("A = %d\tB = %d\n", A, B); /* Affichage : A = 5 B = 2 */

}

Page 50: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 50

main() {

int AA = 2, BB = 5; printf("AA = %d\tBB = %d\n", AA, BB); /* Affichage : AA = 2 BB = 5 */ Permut(AA, BB); /* cf. affichage dans la fonction */ printf("AA = %d\tBB = %d\n", AA, BB); /* Affichage : AA = 2 BB = 5 */ return 0;

} L’exécution des fonctions Permut et main ci-dessus, illustre bien l’isolation qui existe entre les paramètres effectifs et les arguments de la fonction appelée. En effet, alors que les affichages produits dans la fonction Permut prouvent la permutation des valeurs de ses variables A et B, les affichages produits dans la fonction main prouvent quant à eux, que les valeurs de ses variables AA et BB n’ont subi aucune modification. Pour qu'une fonction modifie effectivement la valeur d’une variable du programme appelant, il faut que le paramètre formel soit un pointeur et que le paramètre effectif soit l’adresse de la variable, et non sa valeur. Il s’agit du passage de paramètres par pointeurs. void Permut(int * A, int * B) {

int t; printf("A = %d\tB = %d\n", *A, *B); /* Affichage : A = 2 B = 5 */ t = *A; *A = *B; *B = t; printf("A = %d\tB = %d\n", *A, *B); /* Affichage : A = 5 B = 2 */

} main() {

int AA = 2, BB = 5; printf("AA = %d\tBB = %d\n", AA, BB); /* Affichage : AA = 2 BB = 5 */ Permut(&AA, &BB); /* cf. affichage dans la fonction */ printf("AA = %d\tBB = %d\n", AA, BB); /* Affichage : AA = 5 BB = 2 */ return 0;

} Les fonctions Permut et main précédentes sont adaptées ici pour produire la modification des variables du programme appelant. En passant les adresses des variables, l’isolation est brisée et la fonction Permut peut modifier les valeurs sises à ces adresses, les valeurs pointées. Il faut noter que le mécanisme du passage des paramètres effectifs par valeurs est toujours présent (c’est en effet le seul qui existe vraiment), mais il s’applique ici aux adresses des variables tandis que les modifications ont lieu sur les valeurs pointées (ceci implique donc une adaptation de l’algorithme de la fonction). 6.1.4.1. Illustration du passage par valeur void Somme(int x, int y, int z) {

x = y + z; return; } main() {

int s, a, b; s = 0, a = 1, b = 2; Somme(s, a, b); return 0;

} Malgré l’espoir de recevoir la somme des valeurs des variables a et b dans la variable s, la valeur de celle-ci reste inchangée au retour de la fonction. Lorsque la fonction main commence à s'exécuter, trois emplacements sont réservés sur la pile pour contenir les valeurs des variables s, a et b. Au moment de l'initialisation de ces variables par l'expression s = 0, a = 1, b = 2, ce sont ces emplacements qui sont référencés. Lorsque la fonction main

Page 51: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 51

appelle la fonction Somme, les valeurs des variables s, a et b passent en paramètres effectifs et leurs copies sont empilées à destination de la fonction appelée. Les variables x, y et z, qui sont les paramètres formels de la fonction Somme, sont les emplacements sur la pile des copies des variables s, a et b. La valeur modifiée par l'expression x = y + z est la variable x, qui se trouve à un emplacement différent de celui de la variable s. Au retour, les variables x, y et z sont dépilées et leurs valeurs sont perdues. La variable s n'a donc pas changé.

6.1.4.2. Illustration du passage par pointeur Pour que l’espoir de recevoir la somme des valeurs des variables a et b dans la variable s ne soit pas vain, il faut modifier les fonctions de sorte à passer l’adresse de s comme paramètre effectif à la fonction Somme. void Somme(int * x, int y, int z) {

*x = y + z; return; } main() {

int s, a, b; s = 0, a = 1, b = 2; Somme(&s, a, b); return 0;

}

La fonction Somme attend ici l'adresse d'un int, adresse à laquelle il faut stocker la somme de ses arguments y et z. Quant à la fonction main, c’est l’adresse de la variable s qu’elle transmet à la fonction Somme. Après l'appel à la fonction Somme, on retrouve donc bien la somme de a et de b dans la variable s.

Dans main, avant l’appel

à Somme

Empilement des paramètres par

main

Modification des valeurs par Somme

Dépilement des paramètres par

main

Dans main, après l’appel de Somme

x : 0xA100 x : 0xA100 y : 1 y : 1 z : 2 z : 2

s : 0 s : 0 s : 3 s : 3 s : 3a : 1 a : 1 a : 1 a : 1 a : 1b : 2 b : 2 b : 2 b : 2 b : 2

0xA100 adresse de s

sur la pile

saut dans Somme

retour de Somme

segment de pile

Dans main, avant l’appel

à Somme

Empilement des paramètres par

main

Modification des valeurs par Somme

Dépilement des paramètres par

main

Dans main, après l’appel de Somme

x : 0 x : 3 y : 1 y : 1 z : 2 z : 2

s : 0 s : 0 s : 0 s : 0 s : 0 a : 1 a : 1 a : 1 a : 1 a : 1 b : 2 b : 2 b : 2 b : 2 b : 2

0xA100 adresse de s

sur la pile

saut dans Somme

retour de Somme

segment de pile

Page 52: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 52

6.1.4.3. Passage d’un tableau à une fonction Un tableau est un pointeur (sur le premier élément du tableau) et lorsqu'il est transmis comme paramètre à une fonction secondaire, ses éléments sont modifiables par cette fonction. #include <stdlib.h> /* Librairie requise pour l’allocation dynamique */ void InitTab(int *, int); /* Déclaration d’une fonction d’initialisation */ main() {

int N = 5; int *T; T = malloc(N * sizeof(int)); /* Allocation dynamique pour N entiers */ InitTab(T, N); /* Appel de la fonction InitTab */ free(T); /* Libérer la mémoire dynamiquement allouée */ return 0;

} void InitTab(int *Tab, int N) /* Définition de la fonction d’initialisation */ {

int i; for (i = 0; i < N; i++) /* Initialisation des valeurs pointées par Tab */

Tab[i] = i; } Il faut noter l’existence d’une syntaxe équivalente dans l’écriture de l’entête des fonctions, tant à la déclaration qu’à la définition, qui remplace l’astérisque par une paire de crochets :

void InitTab(int [], int); est équivalent à void InitTab(int *, int); void InitTab(int Tab[], … … … est équivalent à void InitTab(int *Tab, … … …

6.1.4.4. Le qualificatif const et les arguments des fonctions Comme illustré ci-après, le mot const peut être employé pour protéger des données passées par pointeurs à des fonctions. Le mot const dans le prototype d’une fonction est la garantie pour le programmeur que cette fonction n’altèrera pas ses données. Il convient d’être attentif aux notations tellement semblables des paramètres formels mais dont les conséquences sont tellement différentes :

const char C; /* Valeur de type caractère non modifiable */

const char *pC; /* Pointeur d’une valeur de type caractère non modifiable */

char * const pC; /* Pointeur non modifiable d’une valeur de type caractère */ Les conséquences sur les valeurs des variables des codes appelants sont différentes selon que les paramètres sont passés aux fonctions par valeurs, ou par pointeurs. Dans le cas du passage par valeur, c’est une copie de la donnée qui est transmise à la fonction (cf. précédemment). Les modifications exercées sur cette copie n’affectent évidemment pas la variable d’origine. Il est toutefois permis de qualifier un argument de constant et alors, la copie de la variable transmise devient une constante locale.

int DeuxFois(int X) { X = X * 2; /* La fonction manipule une copie de la variable d’origine */

return X; /* et retourne la copie modifiée */ }

int DeuxFois(const int X) { return (X * 2); /* Impossible de modifier la copie de la variable d’origine */ }

Page 53: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 53

Dans le cas du passage par pointeur, la copie du pointeur qui est transmise à la fonction est l’adresse des données (cf. précédemment). Si les modifications du pointeur peuvent souvent être ignorées, les modifications des valeurs pointées quant à elles sont bien répercutées au niveau du code appelant.

int T[] = {10, 20, 30}; … FP(T, 1); …

void FP(int * X, int i) { *(X+i) += 10; /* La valeur 20 de T[1] est remplacée par la valeur 30 */ } void FP(const int * X, int i) { *(X+i) += 10; /* Interdit car X est un pointeur d’entiers constants */ } void FP(int * const X, int i) { *(X+i) += 10; /* La valeur 20 de T[1] est remplacée par la valeur 30 */ } /* C’est le pointeur qui est constant. X++ serait une erreur */

6.1.5. La fonction main La fonction principale main est une fonction comme les autres. Ses arguments et ses données locales sont empilés au chargement de la fonction et dépilés lorsque la fonction rend la main au programme appelant. Une différence la distingue toutefois des autres fonctions : alors que ces dernières sont appelées par la fonction main ou par des fonctions secondaires, la fonction main est le point d’entrée de l’application, c’est-à-dire qu’elle est chargée par le système d’exploitation au démarrage de l’application et c’est à lui qu’elle renvoie son éventuelle valeur de retour. Comme déjà signalé, en ANSI C, la fonction main() retourne un entier au système d’exploitation. Cet entier, qui est appelé code de retour, peut être récupéré dans les scripts du système pour évaluer la bonne terminaison d’un programme. Le type de cette valeur de retour est int, même s’il n’a pas été explicitement déclaré. Certains compilateurs, sans doute moins tatillons par rapport à la norme, autorisent l’écriture d’une fonction main avec d’autres types de retour explicitement spécifiés, dont void (déviation du C++).

main() /* ANSI C */ { …

return 0; } int main() /* ANSI C */ { …

return 0; } char main() /* N’importe quoi, mais char est quand même un entier */ { …

return '0'; /* Retourne l’entier 48 (code ASCII de '0') */ } float main() /* Vraiment n’importe quoi */ { …

return 1.5; } void main() /* C++ */ { …

return; }

Page 54: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 54

La valeur renvoyée en ANSI C est donc transmise à l'environnement d'exécution et elle sert ordinairement à signifier si le programme s'est terminé normalement ou suite à une erreur. Conventionnellement, un code de retour nul exprime la fin normale d’un programme tandis qu’une autre valeur livre une information dont la signification doit être intégrée à la documentation de ce programme. La librairie stdlib.h propose deux constantes symboliques EXIT_SUCCESS (égale à 0) et EXIT_FAILURE (égale à 1) qui peuvent être utilisées comme valeur de retour pour l’instruction return de la fonction main. L’instruction exit, qui peut être exécutée à partir de n’importe quelle fonction secondaire, peut également utiliser ces constantes comme valeurs de retour à l’environnement d’exécution. Outre de retourner une valeur au système d’exploitation, la fonction main peut également recevoir des données au moment de son chargement : il s’agit des arguments d’appel, dits aussi paramètres en ligne de commande. L'usage des arguments d'appel d'une application permet à l'utilisateur de lancer le programme en lui passant des paramètres effectifs. Par exemple, la commande Format du Dos nécessite le passage d'un paramètre désignant l'unité à formater. De même, la commande Copy a besoin du nom du fichier à copier et du nom du fichier de destination.

Format D: Copy MonFichier.Txt MonFichier.Sav

Ces commandes travaillent ainsi parce que des paramètres formels ont été déclarés dans l’entête de leur fonction main et que les paramètres effectifs passés en ligne de commande, sont récupérés et traités dans la fonction. En fait, la fonction main possède deux paramètres formels, conventionnellement nommés argc (argument count) et argv (argument vector). La fonction main possède donc deux prototypes en ANSI C :

int main(); int main(int argc, char *argv[]);

Le premier de ces paramètres, argc, est une variable de type int dont la valeur représente le nombre de mots composant la ligne de commande (y compris le nom de l'exécutable). Elle est donc égale au nombre de paramètres effectifs de la fonction augmenté d’une unité. Le second paramètre, argv, est un tableau de chaînes de caractères dont chaque position contient un mot de la ligne de commande. Le premier élément argv[0] contient donc le nom de la commande (du fichier exécutable), le second argv[1] contient le premier paramètre, ... Par exemple, le programme suivant, compilé sous le nom de MonProduit.exe, doit calculer et afficher le produit de deux entiers qui lui sont passés en ligne de commande : #include <stdio.h> #include <stdlib.h> /* Librairie requise pour les valeurs de retour constantes */ int main(int argc, char *argv[]) {

int N1, N2; if (argc != 3) {

printf("\nErreur : nombre invalide d'arguments"); printf("\n Usage : %s int int\n",argv[0]); return(EXIT_FAILURE); /* Retour d’un code de "Terminaison anormale" */

} N1 = atoi(argv[1]); /* Convertion d’une chaîne de chiffres en nombre entier */ N2 = atoi(argv[2]); printf("\nLe produit %d x %d vaut : %d\n", N1, N2, N1 * N2); return(EXIT_SUCCESS); /* Retour d’un code de "Terminaison sans erreur" */

}

Page 55: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 55

6.1.6. Fonctions avec un nombre variable de paramètres Il est possible de définir des fonctions dont le nombre de paramètres effectifs peut être différent d’un appel à l’autre. Bien que cette nécessité puisse souvent être évitée par la mise en place d’algorithmes bien pensés, il existe toutefois des cas où cette fonctionnalité est indispensable, notamment dans les fonctions d’entrées et de sorties formatées. Une fonction possédant un nombre variable de paramètres doit posséder au moins un paramètre formel fixe par lequel doit passer le nombre de paramètres effectifs d’un appel donné. C’est la notation ... (trois points) à la fin de la liste des paramètres formels de la fonction qui indique que ces paramètres peuvent être suivis par d’autres en nombre inconnu. Exemples de prototypes de fonctions avec un nombre variable de paramètres :

int F1(int NombreParametre, ...); int F2(int NombreParametre, char, ...); int F3(int NombreParametre, char, int *, ...);

int printf(char *format, ...); /* Retourne le nombre de caractères écrits */

Dans ce dernier exemple, c’est l’analyse de la chaîne de formatage qui permet à la fonction printf de déterminer son nombre de paramètres effectifs. Quatre outils sont nécessaires à l’exploitation de fonctions avec un nombre variable de paramètres. Ce sont des macros définies dans la librairie stdarg.h : va_list, va_start, va_arg et va_end. Ces outils sont tous indispensables dans la fonction concernée et s’utilisent comme suit : 1. déclaration d’une variable de type va_list : va_list MesParametres; 2. initialisation de cette variable avec le nombre de paramètres effectifs : va_start(MesParametres, Nombre); 3. accès au paramètre suivant (le premier, puis le second, …) : va_arg(MesParametres, type); 4. libération de la variable en vue du prochain usage : va_end(MesParametres); La définition et l’exploitation d’une fonction avec un nombre variable de paramètres effectifs sont illustrées ci-dessous avec la fonction Somme.

#include <stdarg.h> int Somme(int Nombre, ...) {

int S = 0; int i; va_list MesParametres; /* Déclaration de la liste */ va_start(MesParametres, Nombre); /* Initialisation de la liste */ for (i = 0; i < Nombre; i++)

S += va_arg(MesParametres, int); /* Accès aux éléments de la liste */ va_end(MesParametres); /* Libération de la mémoire */ return S;

} main() {

int x = 1, y = 2; printf("%d", Somme(3, x, y, 25)); /* Affichage : 28 */ ...

Les types de données spécifiés à la macro va_arg doivent être compatibles avec les données lues. Par exemple, la lecture des paramètres effectifs dans la fonction AfficheDesValeurs(int Nombre, ...) par les trois printf (cf. extrait ci-dessous) est possible pour autant que l’ordre des paramètres soit respecté lors de l’appel de cette fonction.

AfficheDesValeurs(3, 17, "Azerty", 2.5); /* Appel de la fonction */ printf("\n%d", va_arg(MesParametres, int)); /* Affichage par cet appel : 17 */ printf("\n%s", va_arg(MesParametres, char *)); /* Affichage par cet appel : Azerty */ printf("\n%f", va_arg(MesParametres, double)); /* Affichage par cet appel : 2.5 */

Page 56: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 56

6.2. Les pointeurs Toute variable manipulée dans un programme est stockée quelque part en mémoire centrale. Cette mémoire est constituée d'octets qui sont identifiés de manière univoque par un numéro qu'on appelle adresse. Pour retrouver une variable, il suffit donc de connaître l'adresse du premier octet de la donnée stockée. L’usage d’identificateurs aussi représentatifs que possible du contenu des variables offre la meilleure lisibilité du code. Cependant, il est parfois très utile de manipuler des données à partir de leur adresse plutôt qu’à partir d’une variable. Il faut toutefois noter que l’usage des adresses n’élimine pas les variables, ni la nécessité de leur coller des identificateurs pertinents : une variable ne contient plus sa donnée, mais bien l’adresse de sa donnée. Cette variable, qui est dite pointeur, doit être déclarée à l’aide d’une notation adéquate.

int I; /* Une variable élémentaire de type entier */ char C; /* Une variable élémentaire de type caractère */ float R; /* Une variable élémentaire de type réel */ int *pI; /* Une variable pointeur d’une donnée de type entier */ char *pC; /* Une variable pointeur d’une donnée de type caractère */ float *pR; /* Une variable pointeur d’une donnée de type réel */

6.2.1. Notion de pointeur Un pointeur est un objet Lvalue dont la valeur est égale à l'adresse d'un autre objet. Comme pour n'importe quelle Lvalue, cette valeur est modifiable. Chacune des trois dernières déclarations de l’exemple précédent associe un identificateur à un type de donnée. C’est l’usage d’un astérisque qui indique que cet identificateur ne désigne pas une donnée mais l’adresse d’une donnée. Il faut noter qu’à ce niveau de déclaration, l’astérisque n’est pas l’opérateur unaire d'indirection mais un symbole signifiant "pointeur de". L'adresse d'un objet étant un numéro d'octet en mémoire, il s'agit d'un entier quel que soit le type de l'objet considéré. Le format interne de cet entier (16 bits, 32 bits, …) dépend des architectures. Cependant, pour permettre l'interprétation de la valeur d'un pointeur, indispensable à l’arithmétique des pointeurs, le type du pointeur est celui de l’objet pointé : un pointeur sur un objet de type char contient l'adresse de l'octet du caractère pointé; un pointeur sur un objet de type int contient l'adresse du premier des 4 octets de l’entier pointé; … 6.2.2. Adresse et valeur d'un objet Une Lvalue est caractérisée par : son adresse; sa valeur, c'est-à-dire ce qui est stocké à cette adresse.

int I, J; I = 3; J = I;

Par exemple, si la compilation des trois lignes ci-dessus a placé la variable I à l'adresse 10000600 de la mémoire, et la variable J à l'adresse 10000604, le tableau ci-contre représente le plan d’occupation de la mémoire par les données de ce code. Deux variables différentes ont des adresses différentes. L'affectation J = I n'opère que sur les valeurs des variables. Les variables I et J étant de type int, elles sont stockées sur 4 octets. La valeur de I est stockée sur les octets des adresses 10000600 à 10000603 et la valeur de J est stockée sur les octets des adresses 10000604 à 10000607. L’accès à la valeur d’une variable est aisé et souvent suffisant mais l’usage des pointeurs impose dans bien des cas d’accéder aussi à l’adresse de la variable. Un opérateur d’adresse, dit aussi opérateur de référencement, existe à cet effet : c’est l’opérateur unaire &.

int I; int *pI; I = 3; pI = &I;

Objet Adresse ValeurI 10000600 3J 10000604 3

Objet Adresse Valeur I 10000600 3pI 10000604 10000600

Page 57: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 57

Mais l’usage des pointeurs nécessite aussi d’accéder aux données pointées. C’est l’opérateur unaire *, appelé opérateur d'indirection ou opérateur de déréférencement (selon les auteurs), qui permet l’accès direct à la valeur de l'objet pointé.

int I; int *pI; I = 3; pI = &I; printf("pI = %ld\t*pI = %d ", pI, *pI); /* Affichage : 10000600 3 */

Dans ce programme, les objets I et *pI sont identiques : ils ont la même adresse et la même valeur. Cela signifie en particulier que toute modification de *pI modifie I. Ainsi, si l'instruction *pI = 0; est ajoutée à la fin du programme précédent, la valeur de I devient nulle. L’usage d’un pointeur implique donc l’existence deux objets manipulables supplémentaires dans un programme : le pointeur et sa valeur pointée. Ces deux manipulations sont très différentes comme illustré ci-après. Code de départ :

int I = 3, J = 6; int *pI, *pJ; pI = &I; pJ = &J;

Ajout d’une ligne de code *pI = *pJ; ou pI = pJ; : Si la ligne ajoutée est *pI = *pJ; c’est la valeur située à l’adresse contenue dans pI (adresse de I) qui est affectée par celle située à l’adresse contenue dans pJ (adresse de J). Pour le dire simplement : la valeur de I est remplacée par la valeur de J. Si la ligne ajoutée est pI = pJ; c’est la valeur de pI (adresse de I) qui est affectée par celle de pJ (adresse de J). Pour le dire simplement : la valeur de pI est remplacée par la valeur de pJ, c’est-à-dire que les deux pointeurs ont maintenant la même valeur, ils pointent la même donnée. 6.2.3. Arithmétique des pointeurs La valeur d'un pointeur étant un entier, il est permis de lui appliquer certaines opérations arithmétiques. Ces opérations arithmétiques sont : l'addition d'un entier à un pointeur (le résultat est un pointeur de même type que le pointeur de départ); la soustraction d'un entier à un pointeur (le résultat est un pointeur de même type que le pointeur de départ); la différence entre pointeurs d’un même type (le résultat est la valeur de l’écart des pointeurs exprimée en objets).

Pour obtenir l’écart exprimé en octets, il suffit de multiplier le résultat de la soustraction par la taille d’un objet, par sizeof(objet). Il faut noter au passage que la somme de deux pointeurs n'est pas autorisée.

Si i est un entier et p est un pointeur sur un objet donné, l'expression p + i désigne l’adresse d’un autre objet de même type. Cette adresse est égale à celle contenue dans p incrémentée de i * sizeof(type_objet). Il en va de même pour la soustraction d'un entier à un pointeur, et pour les opérateurs d'incrémentation et de décrémentation ++ et --. Toute opération d’addition ou de soustraction d’une valeur à un pointeur procède à la multiplication de l’opérande de type entier par une valeur égale à sizeof(type_objet_pointé) et ce, de manière transparente pour le programmeur.

Objet Adresse Valeur I 10000600 3pI 10000604 10000600*pI 10000600 3

Objet Adresse Valeur I 10000600 3J 10000604 6pI 10000608 10000600pJ 10000612 10000604*pI 10000600 3*pJ 10000604 6

si *pI = *pJ; Objet Adresse Valeur I 10000600 6J 10000604 6pI 10000608 10000600pJ 10000612 10000604*pI 10000600 6*pJ 10000604 6

si pI = pJ; Objet Adresse Valeur I 10000600 3 J 10000604 6 pI 10000608 10000604 pJ 10000612 10000604 *pI 10000604 6 *pJ 10000604 6

Page 58: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 58

Exemples :

int I = 3, J = 6; int *pI, *pJ; pI = &I; pJ = pI + 1; printf("%ld", pI); /* Affichage : 10000600 */ printf("%ld", pJ); /* Affichage : 10000604 */ printf("%d", pJ - pI); /* Affichage : 1 */

double R = 3.0, S = 6.0;

double *pR, *pS; pR = &R; pS = pR + 1; printf("%ld", pR); /* Affichage : 10000600 */ printf("%ld", pS); /* Affichage : 10000608 */ printf("%d", pS - pR); /* Affichage : 1 */

Les opérations relationnelles sont également applicables aux pointeurs. Elles n’ont de sens que si leurs opérandes sont des pointeurs d’un même type. L'utilisation des opérations arithmétiques sur les pointeurs est particulièrement utile pour parcourir des tableaux. Ainsi, les codes suivants affichent les éléments du tableau Tab de différentes manières.

int N = 5, Tab[] = {0, 1, 2, 3, 4}; int *pI; int i; for (pI = &Tab[0]; pI <= &Tab[N-1]; pI++)

printf("%d ",*pI); /* Le pointeur est incrémenté dans la boucle */

for (pI = &Tab[0], i = 0; i <= N-1; i++) printf("%d ",*(pI + i)); /* Le pointeur n’est pas incrémenté */

for (i = 0; i <= N-1; i++) printf("%d ",*(Tab + i)); /* Un tableau est un pointeur */

for (i = 0; i <= N-1; i++) printf("%d ",Tab[i]); /* Un tableau est souvent indicé */

for (pI = &Tab[0], i = 0; i <= N-1; i++)

printf("%d ",pI[i]); /* L’indice peut aussi servir le pointeur */ Et pour continuer cette comparaison entre l’usage de l’arithmétique des pointeurs et celui des indices, voici les fonctions strcpy, strlen et strcmp de la librairie string.h reprogrammées d’une manière et de l’autre. La fonction strcpy(char *Cible, char *Source) réalise l’affectation de la chaîne Source à la chaîne Cible.

void STRCPY_a(char *Cible, char *Source){

char *pC = Cible, *pS = Source; while (*pC++ = *pS++); return;

}

void STRCPY_i(char *Cible, char *Source){

int i=0; while (Cible[i] = Source[i++]); return;

}

La fonction strlen(char *Chaine) livre le nombre de caractère(s) utile(s) (non compté le terminateur) de la chaîne Chaine. int STRLEN_a(char *Chaine) {

char *p = Chaine; while (*p++); return (p – Chaine – 1);

}

int STRLEN_i(char *Chaine) {

int i=0; while (Chaine[i++]); return (i – 1);

}

Objet Adresse Valeur I 10000600 3J 10000604 6pI 10000608 10000600pJ 10000612 10000604*pI 10000600 3*pJ 10000604 6

Objet Adresse Valeur R 10000600 3.0S 10000608 6.0pR 10000616 10000600pS 10000620 10000608*pR 10000600 3.0*pS 10000608 6.0

Page 59: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 59

La fonction strcmp(char *Ch1, char *Ch2) retourne un nombre positif, nul ou négatif selon que Ch1>Ch2, Ch1==Ch2 ou Ch1<Ch2. int STRCMP_a(char *Ch1, char *Ch2){

char *p1 = Ch1, *p2 = Ch2; for (; *p1==*p2; p1++, p2++) if (!*p1) return 0; return (*p1 - *p2);

}

int STRCMP_i(char *Ch1, char *Ch2){

int i; for (i=0; Ch1[i]==Ch2[i]; i++) if (!Ch1[i]) return 0; return (Ch1[i] - Ch2[i]);

} 6.2.4. Allocation dynamique Avant de manipuler un pointeur, et notamment de lui appliquer l'opérateur d'indirection *, il faut l'initialiser. Un pointeur est initialisé par défaut à une valeur nulle s’il est de portée globale et il n’est pas initialisé s’il est de portée locale. La valeur nulle d’un pointeur est exprimée par une constante notée NULL. Dans un cas comme dans l’autre, sa valeur ne peut être utilisée comme adresse d’un lieu stockage de données. Le pointeur doit impérativement être initialisé par l’adresse d’une zone mémoire autorisée pour le programme. L’initialisation d’un pointeur avec l’adresse d’une des variables du programme lui affecte l’adresse d’une zone mémoire autorisée puisqu’elle a été allouée au programme lors de la compilation.

int I = 3; int *pI; pI = &I; /* Affectation à pI d’une adresse autorisée */

Il est également possible d'affecter directement une valeur à *pI à condition de lui avoir d'abord réservé une zone mémoire autorisée de taille adéquate.

int *pI = 3; /* Erreur : tentative d’imposer une adresse illicite à pI */ int *pI; *pI = 3; /* Erreur : pI ne contient pas une adresse autorisée */

La gestion de zones mémoires autorisées autres que celles occupées par les variables du programme consiste à pratiquer l’initialisation dynamique des pointeurs. Cette gestion dynamique de la mémoire s’effectue par l’usage des fonctions malloc, calloc et realloc de la librairie standard stdlib.h. La fonction malloc(Nombre_octets) livre l’adresse d’un ensemble d’octets contigus sous la forme d’un pointeur de type void de sorte qu’il puisse être affecté à n’importe quel pointeur. En effet, bien que les conversions entre pointeurs doivent être explicitées et réalisées en parfaite connaissance algorithmique de l’effet attendu, il est permis d’attribuer la valeur de retour de malloc à n’importe quel type de pointeur parce que la norme ANSI impose qu’une conversion impliquant un pointeur void et un autre soit implicite. Il faut noter que de nombreux exemples d’utilisation de malloc présentent une conversion explicite de l’adresse renvoyée dans le type du pointeur à affecter. Ceci s’explique par le fait que dans les versions du langage C antérieures à sa normalisation, malloc renvoyait un pointeur de type char au lieu d’un pointeur void. La conversion implicite n’était pas permise. Cette conversion explicite est donc obsolète mais elle peut éventuellement être conservée pour la lisibilité du code qu’elle induit.

int *pI1, *pI2; pI1 = malloc(sizeof(int)); /* Réservation pour 1 entier */ pI2 = (int *) malloc(sizeof(int)); /* Idem, mais avec conversion explicite obsolète */

Comme illustré ci-dessus, le nombre d’octets souhaités est généralement calculé par l’opérateur sizeof. Bien que nullement obligatoire, l’usage de sizeof évite les erreurs de calculs du programmeur et rend le code indépendant des implémentations. Les deux allocations suivantes sont équivalentes si les entiers sont codés sur 4 octets.

int *pI1, *pI2; pI1 = malloc(100); /* Réservation de 100 octets pour 25 entiers */ pI2 = malloc(25 * sizeof(int)); /* Réservation pour 25 entiers */

Page 60: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 60

6.2.4.1. Allocation dynamique et non dynamique : comparaison importante Dans le programme suivant, l’allocation dynamique livre au pointeur pI une adresse différente de celle de la variable I du programme. Il en résulte que *pI et I sont deux objets distincts et la modification de l’un n’altère pas l’autre. #include <stdio.h> #include <stdlib.h> main() {

int I = 3; int *pI; printf("%ld", &I); /* Affichage : 10000600 (adresse de I) */ printf("%ld", pI); /* Affichage : 19283761 (valeur quelconque aléatoire) */ *pI = I; /* Erreur : pI ne contient pas une adresse autorisée */ pI = malloc(sizeof(int)); /* Affectation à pI de l’adresse d’une zone autorisée */ printf("%ld", pI); /* Affichage : 10005100 (adresse d’une zone reservée) */ *pI = I * 2; /* Affectation d’une zone autorisée */ printf("%d",I); /* Affichage : 3 */ printf("%d",*pI); /* Affichage : 6 */ return 0;

} Dans le programme suivant, l’initialisation du pointeur pI, qui n’est pas une allocation dynamique, lui affecte l’adresse de la variable I du programme. Il en résulte que pI et I désignent le même objet et la modification de *pI change la valeur de I et vice versa. #include <stdio.h> #include <stdlib.h> main() {

int I = 3; int *pI; printf("%ld", &I); /* Affichage : 10000600 (adresse de I) */ printf("%ld", pI); /* Affichage : 19283761 (valeur quelconque aléatoire) */ *pI = I; /* Erreur : pI ne contient pas une adresse autorisée */ pI = &I; /* Affectation à pI de l’adresse d’une zone autorisée */ printf("%ld", pI); /* Affichage : 10000600 (adresse d’une zone reservée) */ *pI = I * 2; /* Affectation d’une zone autorisée */ printf("%d",I); /* Affichage : 6 */ printf("%d",*pI); /* Affichage : 6 */ return 0;

} 6.2.4.2. Allocation dynamique avec mise à zéro La fonction calloc de la librairie stdlib.h a le même rôle que la fonction malloc mais outre d’initialiser le pointeur avec l’adresse d’une zone mémoire autorisée, elle initialise cette zone à zéro. Alors que malloc requiert, pour unique paramètre, le nombre d’octets à réserver, calloc quant à elle exige deux paramètres : le nombre d’objets et la taille d’un objet. Mis à part que l'emploi de calloc est plus simple et plus rapide, les deux extraits de codes ci-après sont équivalents.

int i, NombreDeInt; int *pI; … pI = malloc(NombreDeInt * sizeof(int)); for(i=0; i < NombreDeInt; i++) *(pI + i) = 0; int NombreDeInt; int *pI; … pI = calloc(NombreDeInt, sizeof(int));

Page 61: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 61

6.2.4.3. Réallocation dynamique Dès lors que l’allocation mémoire est dynamique, il peut être souhaitable de pouvoir modifier la taille d’une zone réservée. La fonction realloc existe à cet effet. Elle permet la modification de l’espace alloué à un pointeur en conservant la contiguïté des octets. Elle ne peut s’utiliser que sur des pointeurs initialisés avec malloc ou calloc, allocation dynamique oblige.

pI = realloc(pI, NouvelleTaille); Si la modification est un accroissement, les données déjà présentes dans la zone sont entièrement conservées. La zone ajoutée n’est pas initialisée. Si la modification est une réduction, la conservation des données n’est garantie que pour le début de la zone, jusqu’à la nouvelle taille.

int NombreDeInt, NouveauNombreDeInt; int *pI; … pI = calloc(NombreDeInt, sizeof(int)); … pI = realloc(pI, NouveauNombreDeInt * sizeof(int));

La valeur de retour de realloc peut être affectée à un autre pointeur de même type. Le pointeur d’origine est alors dupliqué : les deux pointeurs pointent la même zone mémoire.

AutrePointeurDeInt = realloc(pI, NouvelleTaille); 6.2.4.4. Libération de la mémoire La zone allouée dynamiquement n’est pas située dans une région de gestion automatique de la mémoire. Ceci signifie en particulier qu’une telle zone n’est jamais désallouée automatiquement pendant l’exécution du programme, tout au plus le sera-t-elle lorsqu’il rendra la main au système d’exploitation et c’est alors l’affaire de ce dernier. Certains programmes, tels des services de serveur ou du système d’exploitation, ne sont que rarement arrêtés et, sans précautions particulières, le mécanisme de l’allocation dynamique peut conduire à l’encombrement excessif de la mémoire. Quel que soit le rôle d’une application, il convient d’empêcher l’occupation redondante de la mémoire par ses allocations dynamiques successives. La fonction free libére la zone mémoire dont le pointeur, qui a été initialisé avec malloc, calloc ou realloc, ou encore par la valeur NULL, lui est passé en paramètre. Les tentatives redondantes de désallocation de la mémoire sont ignorées.

int NombreDeInt; int *pI1, *pI2; … pI1 = malloc(NombreDeInt * sizeof(int)); pI2 = calloc(NombreDeInt, sizeof(int)); … … free(pI1); /* Libère la zone pointée par pI1 */ free(pI2); /* Libère la zone pointée par pI2 */ … … free(pI1); /* Libèration redondante ignorée */ free(pI1); /* Libèration redondante ignorée */

A toute allocation dynamique doit correspondre une libération de la zone mémoire. L’affectation de la valeur NULL à un pointeur provoque l’abandon de la zone pointée mais pas sa mise à la disposition du système d’exploitation.

Page 62: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 62

6.2.5. Pointeurs et tableaux L'usage des pointeurs en C est essentiellement orienté vers la manipulation des tableaux. En effet, un tableau est un pointeur constant (donc non modifiable) dont la valeur est l'adresse du premier octet des données rangées sous son identificateur. En d’autres termes, un tableau T a pour valeur &T[0]. L’accès aux données se fait ordinairement par l’identificateur du tableau flanqué des indices (un indice par dimension) nécessaires exprimés par l’opérateur d’accès (ou d'indexation) []. Comme déjà illustré, cet opérateur d'accès peut s'appliquer à tout objet de type pointeur. Il est lié à l'opérateur d'indirection * par la formule : T[i] = *(T + i). Les pointeurs et les tableaux se manipulent donc exactement de même manière, comme déjà illustré dans "L’arithmétique des pointeurs" et encore ci-dessous.

int N = 5, T[] = {0, 1, 2, 3, 4}; int *pI = T; /* Initialisation de pI par T (T vaut &T[0]) */ int i; for (i = 0; i <= N-1; i++)

printf("%d ",*(pI + i)); /* Le pointeur n’est pas incrémenté */

for (i = 0; i <= N-1; i++) printf("%d ",*pI++); /* Le pointeur est incrémenté */

Deux différences entre tableau et pointeur sont toutefois à épingler : 1. un pointeur doit toujours être initialisé, soit par une allocation dynamique, soit par l’affectation d'une adresse

autorisée, celle d’une variable du programme, par exemple; 2. un tableau est un pointeur constant et n'est donc pas une Lvalue : il ne peut donc pas être affecté d’une valeur, ni

être modifié par l'arithmétique (on ne peut pas écrire T++;). La manipulation de tableaux, et non de pointeurs, présente certains inconvénients dus au fait qu'un tableau est un pointeur constant. Ainsi : on ne peut pas créer de tableaux dont la taille est une variable du programme; on ne peut pas créer de matrices dont les lignes n'ont pas toutes le même nombre de colonnes. Ces opérations deviennent possibles dès que l'on manipule des pointeurs alloués dynamiquement. Ainsi, par exemple, le programme suivant crée un vecteur d'entiers Tab dont la taille N est encodée par l’utilisateur. #include <stdlib.h> main() {

int N; int *Tab; … printf("Nombre d’entier à stocker ? "); scanf("%d", &N); Tab = malloc(N * sizeof(int)); … free(Tab); return 0;

} S’il fallait que tous les éléments de ce vecteur soient initialisés à zéro, il suffisait de remplacer l'usage de malloc par l’usage de calloc et écrire : Tab = calloc(N, sizeof(int)); . 6.2.5.1. Pointeurs et tableaux à plusieurs dimensions Un tableau à deux dimensions est, par définition, un tableau de tableaux. Il s'agit donc en fait d'un pointeur vers un pointeur. Ainsi, à la suite de l’expression int Tab[L][C];, Tab est un pointeur qui pointe vers un objet lui-même de type pointeur d'entier. Il a une valeur constante égale à l'adresse du premier élément du tableau, &Tab[0][0]. De même Tab[l], avec l valant de 0 à L-1, est un pointeur constant vers un objet de type entier, qui est le premier élément de la ligne d'indice l. Il a donc une valeur constante qui est égale à &Tab[l][0]. Déclaration équivalente pour un vecteur d’entiers : int *Tab; (tableau à une dimension) Déclaration équivalente pour une matrice d’entiers : int **Tab; (tableau à deux dimensions) Déclaration équivalente pour un espace d’entiers : int ***Tab; (tableau à trois dimensions)

Page 63: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 63

Par exemple, le code suivant crée une matrice d’entiers de MaxL lignes et MaxC colonnes par l’exploitation d’un pointeur de pointeurs d’entiers. main() {

int MaxL, MaxC, l, c; int **Tab; … Tab = malloc(MaxL * sizeof(int*)); /* Allocation de MaxL pointeurs d’entiers */ for (l = 0; l < MaxL; l++)

Tab[l] = malloc(MaxC * sizeof(int)); /* Allocation de MaxC entiers */ … /* Usage de Tab[l][c] … */ for (l = 0; l < MaxL; l++)

free(Tab[l]); /* Libération de chaque zone de MaxC entiers */ free(Tab); /* Libération de la zone de MaxL pointeurs */ return 0;

} Les pointeurs de pointeurs ont de nombreux avantages sur les tableaux multi-dimensionnés. Ils permettent notamment de créer des matrices dont les lignes ne comptent pas toutes le même nombre d’éléments. Le code suivant crée une matrice triangulaire d’entiers de MaxL lignes, chacune disposant d’un nombre de colonnes égal à son rang (1ère ligne : 1 colonne, 2ème ligne : 2 colonnes, …).

int MaxL, NbreC, l, c; int **Tab; … Tab = malloc(MaxL * sizeof(int*)); /* Allocation de MaxL pointeurs d’entiers */ for (l = 0; l < MaxL; l++) { NbreC = l + 1 ;

Tab[l] = malloc(NbreC * sizeof(int)); /* Allocation de MaxC entiers */ }

… /* Usage de Tab[l][c] … */ Le programme suivant permet la gestion d’un tableau d’entiers dont les lignes sont allouées dynamiquement, chacune possédant des colonnes également allouées dynamiquement. Afin de connaître la taille de chaque vecteur, ainsi que le nombre de vecteurs composant le tableau, une organisation particulière est mise en place. A l’instar de ce qui se fait en langage Pascal en matière de gestion de chaînes de caractères, la première position de chaque vecteur contient le nombre de données utiles du vecteur. En appliquant ce concept au tableau tout entier, la première position du tableau contient le nombre de vecteurs. Le premier vecteur du tableau devient ainsi un vecteur d’un seul entier, ce dernier étant une information de gestion, tout comme chaque premier entier de vecteur. Le parcours du tableau devient ainsi très aisé : pour l qui varie de 1 à T[0][0], et pour c qui varie de 1 à T[l][0], lire T[l][c]. C’est la fonction gets qui est utilisée ici pour la saisie des entiers parce qu’elle accepte les encodages vides. Ainsi, il suffit à l’utilisateur de presser <Enter>, sans avoir encodé de valeur numérique, pour signaler la fin de l’encodage d’un vecteur ou du tableau. La fonction atoi de la librairie ctype convertit une chaîne de chiffres en valeur numérique entière. #include<stdio.h> #include<stdlib.h> #include <ctype.h> int **Saisie(int MaxChiffres) /* La fonction Saisie réalise l’algorithme décrit */ { /* et retourne l’adresse du 1er élément du tableau */

int **T; int l=0, c, Nombre ; char *Chiffres = malloc(MaxChiffres+1); T = malloc(sizeof(int *)); T[0] = malloc(sizeof(int)); T[0][0] = 0;

cl

0 1 2 3 4 5 6 7

0 6 1 7 1 2 3 4 5 6 72 2 8 9 3 5 10 11 12 13 144 2 15 16 5 7 17 18 19 20 21 22 236 2 24 25

Page 64: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 64

do {

c = 0; l++; printf("\nLigne %d, colonne %d (<Enter> si fini) : ", l, c+1); gets(Chiffres); /* 1ère saisie pour cette ligne */ Chiffres[MaxChiffres] = '\0'; /* Tronquer la chaîne trop longue */ if (Chiffres[0] != '\0') /* Si pas chaîne vide */ {

T[0][0] = l; T = realloc(T, (l+1) * sizeof(int *)); T[l] = malloc(sizeof(int)); T[l] = realloc(T[l], (++c+1) * sizeof(int)); do {

Nombre = atoi(Chiffres); /* Conversion en valeur numérique */ T[l][c] = Nombre; T[l][0] = c; printf("\nLigne %d, colonne %d (<Enter> si fini) : ", l, c+1); gets(Chiffres); /* Saisies suivantes de cette ligne */ Chiffres[MaxChiffres] = '\0'; if (Chiffres[0] != '\0')

T[l] = realloc(T[l], (++c+1) * sizeof(int)); } while ((Chiffres[0] != '\0')); printf("\n");

} } while(c); free(Chiffres); return T;

} main() {

int **Tab, l, c, LMax; Tab = Saisie(5); /* Appel de saisie pour des nombres d’au plus 5 chiffres */ LMax = Tab[0][0]; for (l=1; l<=LMax; l++) /* Accès aux lignes de 1 à Tab[0][0] */ {

printf("\n"); for (c=1; c<= Tab [l][0]; c++) /* Accès aux colonnes de 1 à Tab[l][0] */

printf("%d\t", Tab [l][c]); } for (l=1; l<=LMax; l++)

free(Tab[l]); /* Libération de chaque vecteur entiers */ free(Tab); /* Libération du vecteur de pointeurs */ printf("\n\n"); system("Pause"); return 0;

} 6.2.6. Relations entre un tableau et un vecteur Les octets d’un tableau résultant d’une même allocation sont contigus en mémoire. Ainsi, il est naturel de visualiser un tableau à une dimension (par ailleurs nommé vecteur) sous la forme d’un vecteur d’octets en mémoire. Par exemple, le tableau T ci-dessous est un vecteur de 5 entiers de 4 octets.

0 1 2 3 4

Le premier élément du tableau occupe les quatre premiers octets de la mémoire allouée, lesquels sont suivis des quatre octets de l’élément suivant, et ainsi de suite jusqu’au dernier octet du dernier élément. Le nombre d’octets est fonction de la taille des éléments du tableau et importe peu ici.

int T[5]; /* Indice de 0 à 4 */

T[0] T[1] T[2] T[3] T[4]

T+0 T+1 T+2 T+3 T+4

20 octets en mémoire

Page 65: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 65

Il importe par contre de savoir comment les éléments d’un tableau sont répartis dans la mémoire de façon contiguë et quelles relations existent entre les indices d’une structure multidimensionnelle comme une matrice ou un volume, et l’indice unique requis par la structure vectorielle. Toutes les structures peuvent être représentées en tant que vecteur de données en mémoire : Une variable élémentaire est une structure de dimension nulle, son adresse est celle de sa donnée et elle ne

nécessite donc aucun indice pour accéder à la donnée. Soit D0, pour cette structure à zéro dimension. La longueur, exprimée en données, de son vecteur en mémoire est égal à 1.

Un vecteur est une structure à une dimension, son adresse est celle de sa première donnée et elle nécessite 1 indice pour accéder aux données. Soit D1, pour cette structure à une dimension. La longueur, exprimée en données, de son vecteur en mémoire est égale à son nombre de données.

Une matrice est une structure à deux dimensions, son adresse est celle de sa première donnée et elle nécessite 2 indices pour accéder aux données. Soit D2, pour cette structure à deux dimensions. La longueur, exprimée en données, de son vecteur en mémoire est égale au produit du nombre de données dans chacune de ses 2 dimensions.

Un espace est une structure à trois dimensions, son adresse est celle de sa première donnée et elle nécessite 3 indices pour accéder aux données. Soit D3, pour cette structure à trois dimensions. La longueur, exprimée en données, de son vecteur en mémoire est égale au produit du nombre de données dans chacune de ses 3 dimensions.

Une structure multidimensionnée possède N dimensions, son adresse est celle de sa première donnée et elle nécessite N indices pour accéder aux données. Soit DN, pour cette structure à N dimensions. La longueur, exprimée en données, de son vecteur en mémoire est égale au produit du nombre de données dans chacune de ses N dimensions.

Répartition en mémoire des éléments d’un tableau à trois dimensions : Les éléments sont alignés en mémoire, depuis celui de la première colonne de la première ligne de la première page, colonne par colonne, ligne par ligne et page par page, jusqu’à l’élément de la dernière colonne de la dernière ligne de la dernière page. Le tableau de 3 pages de 2 lignes de 3 colonnes devient en mémoire un vecteur de 3 * 2 * 3 éléments, soit 18 éléments. Le tableau à 3 dimensions peut être noté : T[NombrePages][NombreLignes][NombreColonnes], la valeur élémentaire est implicite, sa longueur en données vaut 1. L’accès aux données nécessite les 3 indices p, l et c qui peuvent prendre les valeurs :

p de 0 à NombrePages – 1 l de 0 à NombreLignes - 1 c de 0 à NombreColonnes - 1.

Le tableau à N dimensions peut être noté : T[DN][DN-1]…[D2][D1], les Dx exprimant la taille de la dimension x concernée et la valeur élémentaire D0 est implicite, sa longueur en données vaut 1. L’accès aux données nécessite les N indices de dn à d1 qui peuvent prendre les valeurs :

dn de 0 à DN – 1 dn-1 de 0 à DN-1 - 1 … d1 de 0 à D1 - 1.

C’est la lecture itérative du tableau, de sa plus petite dimension (D1) d’abord, de sa dimension immédiatement supérieure ensuite (D2), et ainsi de suite jusqu’au bout de sa plus grande dimension (DN), qui réalise sa transposition sous forme d’un vecteur en mémoire. Le mécanisme est transparent pour le programmeur qui ne doit donc généralement pas s’en soucier. Il existe cependant des relations entre l’indice du vecteur et les différents indices du tableau, et vice versa.

Indice i du vecteur en mémoire

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1711 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

0

1

2

23 24 25

26 27 28 17 18 19

20 21 22 11 12 13

14 15 16 1

1 0 2

0

Indice c des colonnes

Indice l des lignes

Indice p des pages

Page 66: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 66

L’illustration montre que la donnée située en T[1][0][2], qui vaut 19, se retrouve à l’indice 8 du vecteur. Donc, p = 1, l = 0 et c = 2 correspondent à i = 8. Inversement, la donnée située à l’indice 13 du vecteur, qui vaut 24, se retrouve en T[2][0][1]. Donc, i = 13 correspond à p = 2, l = 0 et c = 1. 6.2.6.1. Les relations en formules La valeur de i correspondant à un élément donné d’un tableau à N dimensions est égale à la somme des produits de chaque indice avec les tailles maximales des dimensions qui lui sont inférieures. Dans le tableau précédent T[NombrePages][NombreLignes][NombreColonnes], à chaque combinaison de valeurs p, l et c, correspond une seule valeur de i donnée par la formule suivante (D0 vaut toujours 1) :

i = p * (NombreLignes * NombreColonnes * D0) + l * (NombreColonnes * D0)+ c * (D0) Dans le tableau T[DN][DN-1]…[D2][D1], à tout jeu de valeurs dn, dn-1, …, d1, correspond aussi une seule valeur de i donnée par cette formule :

i = (dn * (DN-1 * … * D1 * D0)) + (dn-1 * (DN-2 * … * D1 * D0)) + … + (d1 * D0) La valeur de l’indice dans une dimension donnée dn d’un élément d’un tableau à N dimensions est égale à la division de l’indice i de cet élément dans le vecteur, par le produit des tailles maximales des dimensions qui lui sont inférieures, modulo la taille maximale de la dimension concernée par dn. Dans le tableau précédent T[NombrePages][NombreLignes][NombreColonnes], à chaque valeur de i correspond une seule combinaison de valeurs p, l et c données par la formule suivante (la division est entière et D0 vaut toujours 1) :

p = (i / (NombreLignes * NombreColonnes * D0)) Modulo NombrePages l = (i / (NombreColonnes * D0)) Modulo NombreLignes c = (i / (D0)) Modulo NombreColonnes

Dans le tableau T[DN][DN-1]…[D2][D1], à toute valeur de i correspond un jeu de valeurs dn, dn-1, …, d1, données par cette formule :

dn = (i / (DN-1 * … * D1 * D0)) Modulo DN dn-1 = (i / (DN-2 * … * D1 * D0)) Modulo DN-1 … d1 = (i / (D0)) Modulo D1

0

1

2

23 24 25

26 27 28 17 18 19

20 21 22 11 12 13

14 15 16 1

1 0 2

0

Indice c des colonnes

Indice l des lignes

Indice p des pages

Indice i du vecteur en mémoire

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1711 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

N

n

N

xxn Ddi

1

1

0

.

n

n

xxn DModuloDid

1

0

/

Page 67: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 67

6.2.7. Pointeurs et chaînes de caractères Une chaîne de caractères est un vecteur d’éléments de type char, terminé par la marque de fin de chaîne, le caractère nul '\0', faute de quoi la chaîne est quasi inexploitable, elle ne peut pas être manipulée par les fonctions de la librairie string.h. Toute chaîne de caractères peut être manipulée à l'aide d'un pointeur de type char qui peut être initialisé de manières différentes. L’attribution de la mémoire diffère selon la syntaxe employée : Chaine1[] ci-après est l’adresse de sa chaîne d’initialisation tandis que *Chaine2 contient l’adresse de sa chaîne d’initialisation, laquelle est située dans une autre zone de la mémoire.

char Chaine1[] = "Une chaine"; /* Initialisation à la déclaration */ printf(">%s< %ld %ld %ld\n", Chaine1, Chaine1, &Chaine1, &(*Chaine1)); /* Affiche :

>Une chaine< 2293584 2293584 2293584 */ char *Chaine2 = "Une chaine"; /* Initialisation à la déclaration */ printf(">%s< %ld %ld %ld\n", Chaine2, Chaine2, &Chaine2, &(*Chaine2)); /* Affiche :

>Une chaine< 4206592 2293580 4206592 */ char Chaine3[20]; Chaine3 = "Ceci est une chaine"; /* Erreur : Chaine3 n’est pas une Lvalue */

char *Chaine4; Chaine4 = "Ceci est une chaine"; /* Le pointeur Chaine4 est une Lvalue */ char *Chaine5; Chaine5 = &Chaine1[0]; /* Deux pointeurs pointent la même chaîne */ char *Chaine6; Chaine6 = &Chaine4[0]; /* Deux pointeurs pointent la même chaîne */ char *Chaine7; Chaine7 = calloc(20, 1); /* Allocation d’une zone mémoire */ printf(">%s<\n", Chaine7); /* Affiche : >< */ Chaine7 = "Ceci est une chaine"; printf(">%s<\n", Chaine7); /* Affiche : >Ceci est une chaine< */ char *Chaine8; Chaine8 = malloc(20); /* Allocation d’une zone mémoire */ printf(">%s<\n", Chaine8); /* Affiche n’importe quoi */

Comme largement illustré sous le titre « Arithmétique des pointeurs », où des fonctions de la librairie string.h ont été récrites, dont notamment strlen(), toute opération valide sur les pointeurs, comme l'instruction Chaine++;, est aussi valide sur les chaînes de caractères. Ainsi, le programme suivant affiche le nombre de caractères utiles d'une chaîne.

int i; char *Chaine; Chaine = "Ceci est une chaine"; for (i = 0; *Chaine != '\0'; i++) Chaine++; printf("Nombre de caracteres = %d\n",i); /* Affiche : 19 */

L'utilisation de pointeurs de caractères et non de tableaux permet par exemple de créer une chaîne correspondant à la concaténation de deux chaînes de caractères et ce, sans l’usage de la fonction strcat().

int i; char *Ch1, *Ch2, *ChResultat, *pC; Ch1 = "Ceci est "; Ch2 = "une chaine"; ChResultat = malloc((strlen(Ch1) + strlen(Ch2) + 1) * sizeof(char)); pC = ChResultat; for (i = 0; i < strlen(Ch1); i++) *pC++ = Ch1[i]; for (i = 0; i < strlen(Ch2); i++) *pC++ = Ch2[i]; *pC = '\0'; /* Ne pas oublier de placer un terminateur */ printf("%d\n", strlen(ChResultat)); /* Affiche : 19 */ printf(">%s<\n", ChResultat); /* Affiche : >Ceci est une chaine< */

Page 68: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 68

L'utilisation d'un pointeur de travail, comme pC ci-dessus, est indispensable lorsque des opérations d’incrémentation ou de décrémentation sont utilisées pour parcourir les données pointées. En effet, si une telle opération affecte directement le pointeur initial des données, une partie ou l’entièreté de celles-ci s’égare. Par exemple, dans le code précédent, il est possible de faire l’économie du pointeur pC en incrémentant directement le pointeur ChResultat, mais cela provoque la perte de la référence sur le premier caractère de la chaîne et le résultat n’a évidemment plus rien à voir avec celui escompté.

int i; char *Ch1, *Ch2, *ChResultat; Ch1 = "Ceci est "; Ch2 = "une chaine"; ChResultat = malloc((strlen(Ch1) + strlen(Ch2) + 1) * sizeof(char)); for (i = 0; i < strlen(Ch1); i++) *ChResultat++ = Ch1[i]; for (i = 0; i < strlen(Ch2); i++) *ChResultat++ = Ch2[i]; *ChResultat = '\0'; /* Ne pas oublier de placer un terminateur */ printf("%d\n", strlen(ChResultat)); /* Affiche : 0 */ printf(">%s<\n", ChResultat); /* Affiche : >< */

Le programme suivant permet la gestion d’un tableau de chaînes dont la longueur de chacune est allouée dynamiquement (jusqu’à un nombre maximal de caractères donné), le nombre de chaînes du tableau étant également alloué dynamiquement. La fonction gets fournit les chaînes qui sont stockées chacune avec son terminateur \0 par la fonction strcpy. La saisie, qui s'arrête à l'encodage d'une chaîne vide, est organisée de sorte que le tableau se termine par cette chaîne vide. #include<stdio.h> #include<stdlib.h> #include<string.h> char **Saisie(int MaxCaracteres) {

char **T; char *Chaine = malloc(MaxCaracteres + 1); int l=0; do {

printf("Ligne %d : ", l + 1); gets(Chaine); Chaine[MaxCaracteres] = '\0'; /* Tronquer la chaîne trop longue */ if (!l)

T = malloc(sizeof(char *)); else

T = realloc(T, (l+1) * sizeof(char *)); T[l] = malloc(strlen(Chaine)+1); strcpy(T[l++],Chaine);

} while(*Chaine); free(Chaine); T[l] = NULL; return T;

} main() {

char **Tab, **pTrav; Tab = Saisie(10); pTrav = Tab; /* Le tableau de chaînes est lu, chaîne */ while (*pTrav) /* par chaîne, par un pointeur de travail */ { /* Tant que *pTrav != '\0', lire (*pTrav) */

printf("%s\n", *pTrav++); /* et incrémenter pTrav : (pTrav++) */ }

cl

0 1 2 3 4 5 6 7

0 U N \0 1 E X E M P L E \02 D E \0 3 T A B L E \04 D E \0 5 C H A I N E S \06 \0

Page 69: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 69

while (*Tab) free(*Tab++);

free(Tab); printf("\n\n"); system("Pause"); return 0;

} 6.2.8. Pointeurs et structures 6.2.8.1. Pointeur sur une structure Contrairement au tableau, un objet d’un type structure est une Lvalue. Il possède une adresse qui est celle du premier octet du premier membre de la structure. Il est donc possible de manipuler un tel type de données par des pointeurs. Le programme suivant utilise un pointeur pour créer un tableau dynamique d'objets d’un type structure. #include <stdio.h> #include <stdlib.h> typedef struct Eleve {

char Nom[20]; int Cote;

} ELEVE; main() {

int N, i; ELEVE *Classe; printf("Nombre d'élèves ? "); scanf("%d",&N); Classe = malloc(N * sizeof(ELEVE)); for (i = 0; i < N; i++) {

printf("\nElève %d", i+1); printf("\n\tNom ? "); scanf("%s",Classe[i].Nom); printf("\n\tCote ? "); scanf("%d",&Classe[i].Cote);

} printf("\n\nNumero (1 à %d) : ", N); scanf("%d",&i); i--; printf("\nElève %d : ", i+1); printf("\n\tNom = %s", Classe[i].Nom); printf("\n\tCote = %d", Classe[i].Cote); free(Classe); printf("\n\n"); system("Pause"); return 0;

} Si p est un pointeur sur une structure, l’accès à un membre de la structure pointée se programme par l'expression (*p).UnMembre. L'usage de parenthèses est indispensable parce que l'opérateur d'indirection * a une priorité moins élevée que le point, l'opérateur de traversée. Cette notation se simplifie par l’usage de l'opérateur de traversée -> réservé aux pointeurs : p->UnMembre. C’est ainsi que dans le programme précédent, les expressions Classe[i]. peuvent être remplacées par (*(Classe + i)). et encore par (Classe + i)->. Par exemple, Classe[i].Nom est équivalent à (*(Classe + i)).Nom et encore à (Classe + i)->Nom.

Page 70: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 70

PBase PTrav

6.2.8.2. Les structures dites auto-référencées Une structure est dite auto-référencée lorsqu’un de ses membres est un pointeur vers une structure sœur, c’est-à-dire une structure de même modèle qui participe à la gestion du même ensemble de données. Cette organisation de données permet de construire des architectures dynamiques pour la mémoire vive telles que les listes chaînées et les arbres binaires. L’accès aux données dans ces architectures part toujours d’un pointeur de base qui contient l’adresse du premier octet de la première cellule d’une liste chaînée ou du premier nœud d’un arbre binaire, ce premier nœud étant aussi appelé racine. 6.2.8.3. La liste chaînée Définition d’un type de données ListeDEntiers pour l’illustration d’une liste chaînée :

typedef struct UneCellule { int UnEntier; struct UneCellule *UneCelluleSoeur; }ListeDEntiers;

Le type de donnée ListeDEntiers ci-dessus est une structure complexe composée d’une partie pour une donnée nommée UnEntier et de type int dans cet exemple, et d’une variable de type pointeur de cette structure elle-même et appelée UneCelluleSoeur. Il s’agit bien d’une structure conforme à celle illustrée par le schéma ci-dessus. Comme annoncé, un pointeur de base est indispensable, de même qu’un ou plusieurs pointeurs de travail. Soit PBase et PTrav pour l’illustration ci-après.

ListeDEntiers *PBase, *PTrav; Après les déclarations précédentes des variables PBase et PTrav, ces pointeurs existent et ne pointent encore aucun objet significatif bien qu’ils contiennent forcément une valeur. Il est indispensable d’initialiser PBase à NULL :

PBase = NULL; Le pointeur de travail est initialisé par la fonction malloc lors de la réservation d’un espace pour une cellule :

PTrav = malloc(sizeof(ListeDEntiers));

La partie de la cellule réservée aux données peut être affectée :

PTrav->UnEntier = Nbre1;

La partie de la cellule réservée au pointeur peut aussi être affectée. Cela se fait de différentes manières selon l’algorithme choisi. C’est la valeur de PBase qui est utilisée dans cet exemple.

PTrav->UneCelluleSoeur = PBase; /* UneCelluleSoeur pointe le même objet que PBase, soit NULL */

Données Données Données Données Données

Chaque cellule possède au moins un pointeur qui désigne une autre structure

Un pointeur de base désigne la liste

Le dernier pointeur désigne une adresse

particulière appelée NULLE

PTravPBase

Nbre1

PTrav PBase

?

PTravPBase

Nbre1

PTrav PBase

Page 71: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 71

Il ne reste plus qu’à accrocher le pointeur de base à cette première cellule :

PBase = PTrav; /* PBase pointe le même objet que PTrav */

Le pointeur PTrav n’est plus nécessaire sur Nbre1 et il peut être réaffecté à une deuxième cellule :

PTrav = malloc(sizeof(ListeDEntiers));

La partie de la cellule réservée aux données peut être affectée :

PTrav->UnEntier = Nbre2;

La partie de la cellule réservée au pointeur peut aussi être affectée. C’est encore la valeur de PBase qui est utilisée, comme pour la première cellule.

PTrav->UneCelluleSoeur = PBase;

Il ne reste plus qu’à accrocher le pointeur de base à cette deuxième cellule :

PBase = PTrav;

Le pointeur PTrav n’est plus nécessaire sur Nbre2 et il peut être réaffecté à une troisième cellule :

PTrav = ... ... ...

La liste créée ci-dessus est constituée d’une succession d’ajouts en pile. Il s’agit d’une liste LIFO (Last in, first out) parce que, comme dans le cas d’une pile d’assiettes, c’est le dernier élément empilé qui est le premier accessible (par PBase). Il est également possible de créer une liste par une succession d’ajouts en file. Il s’agit alors d’une liste FIFO (First in, first out) auquel cas, à l’instar des files d’attente, c’est le premier arrivé qui est le premier servi (accessible par PBase). Les codes suivants illustrent ces différents modes de création ainsi que des algorithmes de lecture de ces listes.

Nbre1

PTrav PBase

?

PTrav

Nbre1

PBase

Nbre2

PTrav

Nbre1

PBase

Nbre2

PTrav

Nbre1

PBase

Nbre2

PTrav

Nbre1

PBase

Nbre2

PBase

Nbre1

?

PTrav

Page 72: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 72

6.2.8.3.a. Création et parcours de Pile et de File #include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct UneCellule { int UnEntier; struct UneCellule *UneCelluleSoeur; }ListeDEntiers; ListeDEntiers *AjoutPile(int Valeur, ListeDEntiers *ptr) /* Création en Pile */ {

ListeDEntiers *PTrav; PTrav = malloc(sizeof(ListeDEntiers)); PTrav->UnEntier = Valeur; PTrav->UneCelluleSoeur = ptr; return PTrav;

} ListeDEntiers *AjoutFile(int Valeur, ListeDEntiers *ptr) /* Création en File */ {

ListeDEntiers *PTrav, *PTmp; PTrav = malloc(sizeof(ListeDEntiers)); PTrav->UnEntier = Valeur; PTrav->UneCelluleSoeur = ptr; if (ptr) {

while (PTrav->UneCelluleSoeur) {

PTmp = PTrav->UneCelluleSoeur; PTrav->UneCelluleSoeur= PTrav->UneCelluleSoeur->UneCelluleSoeur;

} PTmp->UneCelluleSoeur = PTrav;

} else

ptr = PTrav; return ptr;

} void LitPile(ListeDEntiers *ptr) /* Lecture en Pile (de Base à NULL) */ {

ListeDEntiers *PTrav; PTrav = ptr; while (PTrav) {

printf("Donnée = %10d\n",PTrav->UnEntier); PTrav = PTrav->UneCelluleSoeur;

} } void LitFile(ListeDEntiers *ptr) /* Lecture en File (de NULL à Base) */ {

if (ptr->UneCelluleSoeur) LitFile(ptr->UneCelluleSoeur);

printf("Donnée = %10d\n",ptr->UnEntier); } main() {

ListeDEntiers *PBase; int N; char n[10]; PBase = NULL; printf("Un entier ? "); gets(n);

Page 73: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 73

while (strlen(n) > 0) { N = atoi(n); /* Création. Voir aussi : PBase = AjoutFile(c, PBase); */ PBase = AjoutPile(N, PBase); /* Mise à jour de PBase avec sa nouvelle valeur */ printf("Un autre ? ");

gets(n); } LitPile(PBase); /* Lecture. Voir aussi : LitFile(PBase); */ printf("\n\n"); system("Pause"); return 0;

} Il faut noter que toute fonction susceptible de modifier la valeur du pointeur de base, telle AjoutPile qui insère chaque nouvelle cellule entre PBase et sa cellule pointée, doit impérativement retourner la valeur de ce pointeur de base, faute de quoi des cellules sont perdues, voire la liste entière. Comme illustré dans le tableau suivant, que la liste soit créée en pile ou en file, il est toujours possible de la lire du premier élément encodé jusqu’au dernier, et vice versa (les données ont été encodées dans l’ordre : 1, 2, 3, 4 et 5).

Liste créée par AjoutPile PBase 5 4 3 2 1 NULL

Affichage de cette liste avec LitPile 5 4 3 2 1 Affichage de cette liste avec LitFile 1 2 3 4 5

Liste créée par AjoutFile PBase 1 2 3 4 5 NULL

Affichage de cette liste avec LitPile 1 2 3 4 5 Affichage de cette liste avec LitFile 5 4 3 2 1

6.2.8.4. L’arbre binaire Les mécanismes de base appropriés aux arbres binaires sont évidemment identiques à ceux des listes chaînées sauf que, différence importante, les nœuds nécessitent au moins deux pointeurs tandis que la cellule n’en nécessite qu’un seul. Définition d’un type de données ArbreDEntiers pour l’illustration d’un arbre binaire :

typedef struct UnNoeud { int UnEntier; struct UnNoeud *UnNoeudGauche; struct UnNoeud *UnNoeudDroite; }ArbreDEntiers;

Le type de donnée ArbreDEntiers ci-dessus est une structure complexe composée d’une partie données nommée UnEntier et de type int dans cet exemple, et de deux variables de type pointeur de cette structure elle-même et

Un pointeur de base désigne la racine de l’arbre

Chaque nœud possède au moins deux pointeurs, chacun désignant un nœud ou l’adresse

particulière NULLE.

Données

Données

Données

Données Données Données

DonnéesDonnéesDonnées

Page 74: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 74

appelées UnNoeudGauche et UnNoeudDroite. Il s’agit bien d’une structure conforme à celle illustrée par le schéma précédent. Comme annoncé, un pointeur de base est indispensable, de même qu’un ou plusieurs pointeurs de travail. Soit PBase et PTrav pour l’illustration ci-après.

ArbreDEntiers *PBase, *PTrav; Comme pour la liste chaînée, le pointeur de base est initialisé avec la valeur NULL et le pointeur de travail est initialisé avec l’adresse réservée par malloc pour un nouveau nœud.

PBase = NULL; PTrav = malloc(sizeof(ArbreDEntiers));

Lors de l’affectation des données, c’est une règle logique qui détermine auquel des deux pointeurs doit s’accrocher la nouvelle structure. Par exemple, pour un algorithme qui construit l’arbre de sorte que les données s’y retrouvent ordonnées, la règle d’accrochage d’un nouveau nœud est simple : si sa donnée est plus petite que celle du nœud présent, alors accrochage à gauche sinon à droite. Soit, pour l’illustration, l’encodage dans l’ordre de leur présentation des entiers suivants : 69, 78, 67, 65, 75, 66, 82, 79 et 85. Dans la logique si plus petit à gauche sinon à droite, l’arbre se construit comme ci-contre et un nouveau nœud contenant la valeur 81 doit s’accrocher à droite du nœud contenant la valeur 79. L’arbre ainsi construit est nommé Arbre de recherche. Il offre un effet dichotomique permettant un accès très rapide à n’importe quelle donnée. Toutefois, son efficacité peut être déforcée selon l’ordre d’ajout des données. Ainsi, si les données de l’exemple précédent avaient préalablement été ordonnées de façon décroissante, l’arbre résultant ne présenterait aucun effet dichotomique, comme en témoigne l’illustration ci-dessous.

L’effet dichotomique d’un arbre est d’autant mieux conservé que l’arbre est mieux équilibré. Dans l’arbre présenté ci-dessus, les données du nœud dont la valeur est 65 sont accessibles dès le troisième nœud visité. Au contraire, dans l’arbre présenté ci-contre, les données du nœud dont la valeur est 65 ne sont accessibles qu’au neuvième nœud visité. Il peut être nécessaire de rééquilibrer un arbre de sorte à ramener son nœud central comme nœud de départ et bénéficier à nouveau de l’effet dichotomique. Ceci peut se faire par la lecture de l’existant et la constitution d’un nouveau ou par la mise en œuvre d’algorithmes de rotations d’arbres. Ces techniques ne sont pas abordées dans ces pages. Les codes suivants, dont la fonction PlaceNoeud, illustrent ce mode de création ainsi que les principaux algorithmes de lecture des arbres binaires. #include<stdio.h> #include<stdlib.h> #include<string.h>

PBase

79

85

65

82

81

PTrav

PBase

69

67

66

65 82

79

75

85

78

Page 75: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 75

typedef struct UnNoeud { int UnEntier; struct UnNoeud *UnNoeudGauche; struct UnNoeud *UnNoeudDroite; }ArbreDEntiers; ArbreDEntiers *PlaceNoeud(int Valeur, ArbreDEntiers *Ptr) {

ArbreDEntiers *PTrav; PTrav = Ptr; if (PTrav == NULL) {

PTrav = malloc(sizeof(ArbreDEntiers)); PTrav->UnNoeudGauche = NULL; PTrav->UnNoeudDroite = NULL; PTrav->UnEntier = Valeur;

} else

if ( Valeur < PTrav->UnEntier) PTrav->UnNoeudGauche = PlaceNoeud(Valeur, PTrav->UnNoeudGauche);

else PTrav->UnNoeudDroite = PlaceNoeud(Valeur, PTrav->UnNoeudDroite);

return PTrav; } void AfficheArbreIN(ArbreDEntiers *PtrIn) /* InOrdre ou infixé */ { /* Affiche les données de l’arbre */

if (PtrIn != NULL) /* créé avec PlaceNoeud dans leur */ { /* ordre croissant */

AfficheArbreIN(PtrIn->UnNoeudGauche); printf("%d ",PtrIn->UnEntier); AfficheArbreIN(PtrIn->UnNoeudDroite);

} } void AfficheArbrePR(ArbreDEntiers *PtrPr) /* PreOrdre ou préfixé */ {

if (PtrPr != NULL) {

printf("%d ",PtrPr->UnEntier); AfficheArbrePR(PtrPr->UnNoeudGauche); AfficheArbrePR(PtrPr->UnNoeudDroite);

} } void AfficheArbrePO(ArbreDEntiers *PtrPo) /* PostOrdre ou postfixé */ {

if (PtrPo != NULL) {

AfficheArbrePO(PtrPo->UnNoeudGauche); AfficheArbrePO(PtrPo->UnNoeudDroite); printf("%d ",PtrPo->UnEntier);

} } int AfficheNiv(ArbreDEntiers *PtrN, int N1, int N2) /* Par niveau (sous fonction) */ {

if (PtrN != NULL) {

if (N1 != N2) {

return (AfficheNiv(PtrN->UnNoeudGauche, N1, N2 + 1) | AfficheNiv(PtrN->UnNoeudDroite, N1, N2 + 1));

} else

Page 76: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 76

{ printf("%d ", PtrN->UnEntier); return 1;

} } return 0;

} void AfficheArbreNI(ArbreDEntiers *PtrNi) /* Par niveau */ {

int N = 1; while (AfficheNiv(PtrNi,N,1)) N++; return;

} main() {

ArbreDEntiers *PBase; int N; char n[10]; PBase = NULL; printf("Un entier ? "); gets(n); while (strlen(n) > 0) { N = atoi(n); PBase = PlaceNoeud(N, PBase); printf("Un autre ? "); gets(n); } printf("\nInOrdre\n"); AfficheArbreIN(PBase); printf("\nPreOrdre\n"); AfficheArbrePR(PBase); printf("\nPostOrdre\n"); AfficheArbrePO(PBase); printf("\nPar niveau\n"); AfficheArbreNI(PBase); printf("\n\n"); system("Pause"); return 0;

}

PBase

69

67

66

65 82

79

75

85

78

PBase

69

67

66

65 82

79

75

85

78

PBase

69

67

66

65 82

79

75

85

78

PBase

69

67

66

65 82

79

75

85

78 3

4

6

1 5 8

2 7 9

2 5

3 6 7

4 8 9

1

3 8

2 4 7

1 5 6

9

2 3

4 5 6

7 8 9

1

Ordre de visite InOrdre Ordre de visite PreOrdre Ordre de visite PostOrdre Ordre de visite par niveau

Page 77: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 77

6.2.9. Les pointeurs sur fonctions Le mécanisme du pointeur de fonction a pour objet de passer une fonction en paramètre à une autre fonction. Cela permet notamment d'utiliser une même fonction pour différents usages, mais aussi de passer à une fonction, l’outil nécessaire à la manipulation des données également transmises. Un pointeur sur une fonction correspond à l'adresse du début du code de la fonction (et non à l’adresse d’une donnée traitée par cette fonction). La différence se note déjà lors des déclarations des fonctions. La première déclaration ci-après, est celle d’une très ordinaire fonction Fonction1 qui doit recevoir deux entiers en paramètres effectifs et qui retourne un résultat de type entier. Ces paramètres effectifs peuvent être livrés par une autre fonction, directement écrite dans l’appel de la fonction, sans qu’il s’agisse d’un pointeur sur une autre fonction. La seconde déclaration est celle d’une fonction Fonction2 dont un des paramètres attendus doit être un pointeur sur une fonction. Il ne peut pas s’agir de n’importe quelle fonction mais bien d’une fonction habilitée à recevoir deux entiers en paramètres effectifs et à retourner un résultat de type entier, c’est ce qui exprimé par : int(*)(int, int).

int Fonction1(int, int); int Fonction2(int, int(*)(int, int));

La différence entre ces deux fonctions peut parfois être invisible dans l’écriture de l’appel des fonctions. Par exemple, dans les lignes de codes suivantes, alors que le deuxième paramètre effectif de la fonction Fonction1 est la valeur renvoyée par la fonction Max(), le deuxième paramètre effectif de la fonction Fonction2 est l’adresse de la fonction Max().

UnEntier = Fonction1(N1, Max(N2, N3)); UnEntier = Fonction2(N1, Max(N2, N3)); /* A noter qu’on n’utilise pas & */

Le programme suivant illustre l’usage d’un pointeur de fonction. float Difference(float x1, float x2) /* Reçoit 2 float et retourne 1 float */ {

return (x1 - x2); } float Somme(float x1, float x2) /* Reçoit 2 float et retourne 1 float */ {

return (x1 + x2); } float Multiplication(float x1, float x2) /* Reçoit 2 float et retourne 1 float */ {

return (x1 * x2); } float Division(float x1, float x2) /* Reçoit 2 float et retourne 1 float */ {

return (x1 / x2); } main() {

float X, Y; char Operateur; float (*pf)(float, float); /* Déclaration d’un pointeur sur une fonction */ printf("\n\nNombre A ? : "); /* qui reçoit 2 float et retourne 1 float */ scanf("%f", &X); printf("\nNombre B ? : "); scanf("%f", &Y); printf("\nOpérateur (+, -, /, *) : "); getc(stdin); /* Vide le buffer */ scanf("%c", &Operateur); switch(Operateur) { case '+' : pf = Somme; /* Pointer la fonction Somme */ break;

Page 78: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 78

case '-' : pf = Difference; /* Pointer la fonction Difference */ break; case '/' : if (Y != 0) pf = Division; /* Pointer la fonction Division */ else printf("\nDivision par zéro\n"); break; case '*' : pf = Multiplication; /* Pointer la fonction Multiplication */ break; default : printf("\nOpérateur erroné\n"); } printf("\n%6.2f", pf(X, Y)); /* Invoquer la fonction pointée */ printf("\n\n"); system("Pause"); return 0;

} Mais le programme précédent peut facilement se passer du pointeur de fonctions. Il n’illustre pas le meilleur usage des pointeurs sur fonctions. L’utilisation la plus pertinente des pointeurs sur fonctions se retrouve là où ils permettent de passer en même temps, des données et l’outil approprié à leur manipulation. Dans le programme suivant, une fonction Comparaison reçoit deux données et un pointeur sur la fonction apte à les comparer. int CompareInt(int * , int *); /* Il y a pas de différence entre CompareInt */ int CompareChar(char *, char *); /* et CompareChar (qui traitent toutes les deux */ int CompareChaine(char *, char *); /* des entiers) mais CompareChaine nécessite un */

/* traitement particulier */ int Comparaison(void *, void *, int(*)(void *, void *)); int CompareInt(int *N1 , int *N2) {

return(*N1 - *N2); } int CompareChar(char *C1, char *C2) {

return(*C1 - *C2); } int CompareChaine(char *CH1, char *CH2) {

return strcmp(CH1, CH2); } int Comparaison(void *Valeur1, void *Valeur2, int(*pF)(void *, void *)) {

return(pF(Valeur1, Valeur2)); } main() {

int A = 5, B = 7; char J = 'a', K = 'b'; char *Ch1 = "AZERTY", *Ch2 = "ALBERT"; printf("\n%d", Comparaison(&A, &B, CompareInt)); /* Affiche : -2 */ printf("\n%d", Comparaison(&J, &K, CompareChar)); /* Affiche : -1 */ printf("\n%d", Comparaison(Ch1, Ch2, CompareChaine)); /* Affiche : 1 */ printf("\n\n"); system("Pause"); return 0;

}

L’usage de pointeurs void permet d’exploiter ensuite la fonction avec n’importe quel type de pointeur du fait de la conversion implicite imposée par la norme ANSI.

Page 79: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 79

Ce mécanisme qui consiste à passer les données et un pointeur sur la fonction nécessaire à leur traitement, est exploité par les fonctions qsort et bsearch de la librairie stdlib.h. L’une comme l’autre reçoit les données à traiter sous la forme d’un tableau de valeurs et nécessite le passage de l’adresse d’une fonction habilitée à comparer ces valeurs. A l’instar des fonctions de comparaisons de l’exemple précédent, la fonction désignée à qsort et à bsearch doit fournir le résultat de son évaluation sous la forme d’un entier positif, nul ou négatif selon que de deux valeurs comparées, la première est supérieure, égale ou inférieure à la seconde.

void qsort(void *Tab, size_t NbreElem, size_t TailleElem, int(*Compare)(const void *, const void *));

La fonction qsort, dont le prototype est présenté ci-dessus, réalise le tri du tableau Tab, contenant NbreElem éléments de taille TailleElem, indicé de 0 à NbreElem-1. Le type d’entier size_t correspond au plus grand type entier non signé. La valeur de TailleElem est idéalement livrée par l’opérateur sizeof().

void *bsearch(const void *Clef, const void *Tab, size_t NbreElem, size_t TailleElem, int(*Compare)(const void *, const void *));

La fonction bsearch, dont le prototype est présenté ci-dessus, effectue la recherche de l’élément Clef dans le tableau Tab de NbreElem de taille TailleElem, préalablement ordonné sur les valeurs de Clef et indicé de 0 à NbreElem-1. A l’issue de la recherche, bsearch retourne un pointeur sur l’élément s’il est trouvé, ou NULL dans le cas contraire. 6.2.9.1. Exemple d’utilisation des fonctions qsort et bsearch #include<stdio.h> #include<stdlib.h> #include<string.h> int CompareEntier(int *, int *); int CompareChaine(char *, char *); int CompareEntier(int *a, int *b) {

return(*a - *b); } int CompareChaine(char *s1, char *s2) {

return(strcmp(s1, s2)); } main() {

int i, NbElem = 5; /* TN pour les tests sur des entiers */ int TN[] = {123, 654, 107, 357, 987}; int NombreClef; int *ResultatInt; /* TS pour les tests sur des chaînes */ char TS[][10] = {"Panzani", "Azerty", "Ranzani", "Qwerty", "Barolo"}; char ChaineClef[10]; char *ResultatChar; for (i=0; i < NbElem; i++) /* Affichage de TS non trié */

printf("%s\t", TS[i]); printf("\n"); qsort(TS, NbElem, 10, CompareChaine); for (i=0; i < NbElem; i++) /* Affichage de TS trié */

printf("%s\t", TS[i]); printf("\n");

Page 80: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 80

printf("\nChaine à chercher TS : "); /* Recherche d’une chaîne dans TS */ scanf("%s", ChaineClef); ResultatChar = bsearch(ChaineClef, TS, NbElem, 10, CompareChaine); if (ResultatChar)

printf ("%s existe dans TS.\n", ResultatChar); else

printf ("%s n'existe pas dans TS.\n", ChaineClef); for (i=0; i < NbElem; i++) /* Affichage de TN non trié */

printf("%d\t", TN[i]); printf("\n"); qsort(TN, NbElem, sizeof(int), CompareEntier); for (i=0; i < NbElem; i++) /* Affichage de TN trié */

printf("%d\t", TN[i]); printf("\n"); printf("\nNombre à cherher dans TN : "); /* Recherche d’un entier dans TN */ scanf ("%d", &NombreClef); ResultatInt = bsearch(&NombreClef, TN, NbElem, sizeof(int), CompareEntier); if (ResultatInt)

printf ("%d existe dans TN.\n", *ResultatInt); else

printf ("%d n'existe pas dans TN.\n", NombreClef); printf("\n\n"); system("Pause"); return 0;

}

Page 81: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 81

6.3. Les fichiers Alors que les instructions d’entrées-sorties classiques sont associées aux périphériques standards par des flux permanents désignés par les pointeurs stdin, stdout et stderr, les fichiers quant à eux doivent recevoir un flux le temps nécessaire à leur traitement. Ce flux est désigné par un pointeur sur une structure complexe définie dans stdio.h qui regroupe certaines informations dont notamment, le chemin d’accès au fichier, l'adresse de la zone mémoire (buffer) réservée aux transferts avec le fichier, la position de la tête de lecture-écriture, le mode d'accès au fichier (lecture ou écriture), ... Cette structure est initialisée par une instruction d’ouverture du fichier qui retourne un pointeur communément nommé pointeur de fichier. Ce pointeur est de type FILE * et il est nécessaire à toutes les opérations sur les enregistrements. La zone mémoire occupée pour le traitement du fichier doit être libérée après usage par une instruction de fermeture. 6.3.1. Ouverture d’un fichier L’ouverture d’un fichier est réalisée par l’instruction fopen qui requiert deux paramètres et retourne un pointeur de fichier ou la valeur NULL en cas d’échec. Le premier de ces deux paramètres est une chaîne de caractères désignant le nom et le chemin complet du fichier en mémoire de masse. Le second est une autre chaîne qui exprime le mode d’ouverture du fichier. Les différents modes d’ouvertures sont décrits dans le tableau suivant.

"r" Ouverture d'un fichier texte en lecture. "w" Ouverture d'un fichier texte en écriture. "a" Ouverture d'un fichier texte en écriture à la fin. "rb" Ouverture d'un fichier binaire en lecture. "wb" Ouverture d'un fichier binaire en écriture. "ab" Ouverture d'un fichier binaire en écriture à la fin. "r+" Ouverture d'un fichier texte en lecture/écriture. "w+" Ouverture d'un fichier texte en lecture/écriture. "a+" Ouverture d'un fichier texte en lecture/écriture à la fin. "r+b" Ouverture d'un fichier binaire en lecture/écriture. "w+b" Ouverture d'un fichier binaire en lecture/écriture. "a+b" Ouverture d'un fichier binaire en lecture/écriture à la fin.

Des significations particulières de caractères définissant le mode sont à épingler :

b Désigne une ouverture en mode binaire. Les caractères de contrôle contenus dans le fichier ne sont pas interprétés, tandis que si le fichier est ouvert en mode texte, les caractères de fins de lignes sont interprétés comme tels.

+ Indique une extension de l’opération initiale de lecture à écriture, ou l’inverse. r Le fichier doit exister, à défaut de quoi l’instruction fopen retourne un pointeur NULL. w Un fichier existant est écrasé par la création d’un nouveau. a Un fichier inexistant est créé. Toutes les écritures se font à la fin du fichier, même si un

repositionnement a été opéré par rewind ou fseek. 6.3.2. Fermeture d’un fichier La fermeture du fichier est réalisée par l’instruction fclose dont le paramètre effectif doit être le pointeur du fichier concerné. Outre la libération la mémoire réservée à la manipulation du fichier, la fermeture assure le transfert vers la mémoire de masse des dernières données encodées et éventuellement encore en transit dans le buffer. L’arrêt d’un programme sans fermeture de fichiers ouverts en mode d’écriture occasionne toujours (ou presque) la perte de données. L’instruction fflush préserve les données d’éventuelles coupures électriques en forçant leur transfert vers la mémoire de masse sans fermeture du fichier. 6.3.3. Exemples d’ouverture et fermeture

FILE *UnFichier; /* Déclaration d’un pointeur de fichier */ char *NomFichier = "NouveauFichier"; /* Obtention du nom du fichier */ … … … UnFichier = fopen(NomFichier, "w"); /* Ouverture du fichier en création */ … … …

Page 82: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 82

/* Traitement du fichier, encodage de données … … …*/ … … … fflush(UnFichier); /* Forcer l’enregistrement physique */ … … … /* Traitement du fichier, encodage de données … … …*/ … … … fclose(UnFichier); /* Fermeture */

6.3.4. Contrôle de l’ouverture d’un fichier Le mode d’ouverture en lecture impose que le fichier existe préalablement et le mode d’ouverture en écriture écrase le fichier qui existerait déjà. Par ailleurs, le programmeur ne peut laisser une application fonctionner sans s’assurer de la disponibilité des fichiers qui lui sont nécessaires : l’absence d’un fichier doit être gérée avant qu’ait eu lieu tout le travail d’encodage et de traitement des données qui lui sont destinées. Les fonctions suivantes permettent respectivement de tester sans altération des données, l’existence d’un fichier et la possibilité d’y écrire :

int EstCeQueLeFichierExiste(char *NomFichier) {

FILE *F; F = fopen(NomFichier, "r"); if (F) /* Le fichier existe */ { fclose(F); /* Refermer le fichier */

return 1; /* Retourner 1, logiquement vrai */ }

return 0; /* Sinon, retourner 0, logiquement faux */ }

int EstCeQuOnPeutEcrire(char *NomFichier) {

FILE *F; F = fopen(NomFichier, "a"); if (F) /* Le fichier peut être ouvert en ajout */ { fclose(F); /* Refermer le fichier */

return 1; /* Retourner 1, logiquement vrai */ }

return 0; /* Sinon, retourner 0, logiquement faux */ }

Il faut encore prévenir le programme d’une éventuelle impossibilité d’accès en écriture au fichier, par manque de droits par exemple, ou encore du fait d’une erreur au niveau du chemin, comme un dossier inexistant.

FILE *FichierSortie; /* Déclaration d’un pointeur de fichier */ char *NomFichier = "NouveauFichier"; /* Obtention du nom du fichier */ … … … if (EstCeQueLeFichierExiste(NomFichier)) {

if (EstCeQuOnPeutEcrire(NomFichier)) {

… … … /* A traiter : le fichier existe déjà et on peut y écrire */ } else {

… … … /* A traiter : le fichier existe déjà mais on ne peut y écire */ }

} else { if ((FichierSortie = fopen(NomFichier, "w")) == NULL)

{ … … … /* A traiter : le fichier n’existe pas mais son ouverture en création a échoué */

} else {

… … … /* A traiter : le fichier n’existait pas mais il a été créé */ }

}

Page 83: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 83

6.3.5. Les fichiers textes Les fichiers textes ne contiennent en principe que des caractères affichables (à partir de l’ASCII 32) et les caractères de fin de ligne (caractères 10 et 13 sur les systèmes Dos et Windows). 6.3.5.1. Lecture et écriture par caractères La lecture d’un caractère est réalisée par la fonction fgetc. Cette fonction retourne le caractère lu sous la forme de son code ASCII. Lorsque la fin du fichier est atteinte, fgetc retourne l'entier EOF (End Of File) qui est une constante définie dans la librairie stdio.h et dont la valeur est ordinairement fixée à -1. La détection correcte d’une fin de fichier impose donc que la variable soit d’un type entier signé. L’écriture d’un caractère est réalisée par la fonction fputc. Outre de transmettre le caractère à enregistrer, cette fonction retourne le code ASCII du caractère transmis ou EOF en cas d’erreur. Les compilateurs implémentent aussi les fonctions getc et putc comme strictement équivalentes à fgetc et fputc. Les prototypes de ces fonctions sont :

int fgetc(FILE *); /* int getc(FILE *); */ int fputc(int, FILE *); /* int putc(int, FILE *); */

Le programme suivant réalise la lecture d’un fichier Original.txt et sa copie, caractère par caractère, vers le fichier Duplicata.txt. main() {

FILE *FichierSource, *FichierCible; char *NomSource = "Original.txt"; char *NomCible = "Duplicata.txt"; int C;

FichierSource = fopen(NomSource, "r"); FichierCible = fopen(NomCible, "w");

while ((C = fgetc(FichierSource)) != EOF) fputc(C, FichierCible);

fclose(FichierCible); fclose(FichierSource);

printf("\n\n"); system("Pause"); return 0;

} 6.3.5.1.a. Relecture d'un caractère La fonction ungetc, dont le prototype est int ungetc(int C, FILE *);, remplace le dernier caractère lu dans le flux par le caractère C (converti en unsigned char). La fonction annule ainsi le déplacement provoqué par la lecture précédente. Elle peut être utilisée pour placer n'importe quel caractère (sauf EOF). Seul le buffer est modifié par ungetc et le fichier d'entrée reste inchangé. Cette fonction retourne le caractère C, ou bien EOF en cas d'erreur. main() {

FILE *FichierEntree; char *NomFichier = "MonFichier.txt"; /* Contenu du fichier : 097023 */ int C;

FichierEntree = fopen(NomFichier, "r"); while ((C = fgetc(FichierEntree)) != EOF) {

if (C == '0') ungetc('.', FichierEntree);

putc(C, stdout); /* Affichage : 0.970.23 */ }

fclose(FichierEntree); printf("\n\n"); system("Pause"); return 0;

}

Page 84: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 84

6.3.5.2. Lecture et écriture par lignes La lecture d’une ligne de texte est réalisée par la fonction fgets. Cette fonction lit un nombre maximal donné de caractères et les stocke dans une chaîne également donnée. La lecture s’arrête si le caractère de fin de ligne '\n' est rencontré. Ce caractère est aussi copié dans la chaîne qui est ensuite terminée par '\0'. La fonction retourne un pointeur sur cette chaîne ou bien NULL si la fin de fichier est atteinte ou en cas d’erreur. L’écriture d’une ligne de texte est réalisée par la fonction fputs. Outre de transmettre la chaîne de caractères à enregistrer, cette fonction retourne une valeur nulle, ou EOF en cas d’erreur. Les prototypes de ces fonctions sont :

char * fgets(char * Ch, int NbreMax, FILE *); /* Lecture NbreMax – 1 caractères */ int fputs(const char * Ch, FILE *);

Le programme suivant réalise la lecture d’un fichier Original.txt et sa copie, ligne par ligne, vers le fichier Duplicata.txt. main() {

FILE *FichierSource, *FichierCible; char *NomSource = "Original.txt"; char *NomCible = "Duplicata.txt"; char *Chaine = malloc(80); FichierSource = fopen(NomSource, "r"); FichierCible = fopen(NomCible, "w"); while ((Chaine = fgets(Chaine, 80, FichierSource)) != NULL) fputs(Chaine, FichierCible); fclose(FichierCible); fclose(FichierSource); free(Chaine); printf("\n\n"); system("Pause"); return 0;

} 6.3.5.3. Lecture et écriture formatées Les fonctions fprintf et fscanf, déjà illustrées précédemment sous le titre « Autres entrées-sorties formatées », s’utilisent exactement comme leurs cousines des entrées-sorties classiques à la seule différence qu’elles nécessitent la désignation du fichier à lire ou à écrire. En particulier, la fonction fscanf interprète les espaces et les '\n' comme séparateurs de données et pose, comme scanf, le problème du retour chariot résiduel (cf. précédemment) lors de la lecture d’un seul caractère (%c). La fonction fscanf retourne EOF si la fin de fichier est atteinte ou en cas d’erreur. main() {

FILE *UnFichier; char *NomFichier = "MonFichier.txt"; int A = 1; float X = 3.1; char Ch[] = "Azerty"; UnFichier = fopen(NomFichier, "w"); /* Ouverture du fichier en création */ fprintf(UnFichier, "%d %6.2f %s\n", A, X, Ch); fclose(UnFichier); /* Fermeture */ UnFichier = fopen(NomFichier, "r"); /* Ouverture du fichier en lecture */ fscanf(UnFichier, "%d%f%s", &A, &X, Ch); fclose(UnFichier); /* Fermeture */ printf("%d %6.2f %s\n", A, X, Ch); printf("\n\n"); system("Pause"); return 0;

}

Page 85: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 85

6.3.6. Les fichiers binaires Alors qu’un fichier texte, ou traité comme tel, ne contient en principe que les caractères affichables et éventuellement, des codes de fins de ligne, un fichier binaire peut contenir des octets de n’importe quelle valeur : ils ne sont pas interprétés en cours de lecture ou d’écriture. L’organisation d’un fichier binaire répond aux impératifs de l’application qui l’a créé et qui en manipule les données. Cela signifie notamment qu’un fichier binaire n’est généralement pas lisible par les outils ordinaires des systèmes. Bien entendu, la puissance des instructions de traitements des fichiers binaires peut être utilisée aussi pour des fichiers textes sans altération de leur lisibilité. Il est par exemple possible, comme illustré ci-dessous, de réaliser la copie d’un fichier texte par l’usage des instructions de lecture et d’écriture des fichiers binaires. Les instructions de lecture et d’écriture des fichiers binaires sont respectivement fread et fwrite. Chacune retourne le nombre d’éléments effectivement mouvementés. Elles sont inévitablement accompagnées d’une troisième fonction, feof, dont le rôle est de tester la rencontre de la fin de fichier. Elle retourne un entier différent de zéro, c’est-à-dire logiquement vrai, lorsque l’indicateur de fin de fichier est positionné. Les prototypes de ces fonctions sont :

size_t fread(void *Tab, size_t TailleElem, size_t NbreElem, FILE *); size_t fwrite(void *Tab, size_t TailleElem, size_t NbreElem, FILE *); int feof(FILE *);

Le programme suivant réalise la lecture d’un fichier Original.txt et sa copie, par lots de 1000 octets, vers le fichier Duplicata.txt. main() {

FILE *FichierSource, *FichierCible; char *NomSource = "Original.txt"; char *NomCible = "Duplicata.txt"; char *Buffer = malloc(1000); /* 1000 octets */ int N; FichierSource = fopen(NomSource, "rb"); FichierCible = fopen(NomCible, "wb"); while (!feof(FichierSource)) {

N = fread(Buffer, 1, 1000, FichierSource); fwrite(Buffer, 1, N, FichierCible);

} fclose(FichierCible); fclose(FichierSource); free(Buffer); printf("\n\n"); system("Pause"); return 0;

} Le programme suivant crée un fichier MesEntiers.dat et y enregistre 50 nombres entiers, de 0 à 49, contenus dans tableau Tab1. Un autre tableau, Tab2, est ensuite rempli par la lecture du fichier et son contenu est affiché à l’écran. main() {

FILE *FichierDEntiers; char *NomFichier = "MesEntiers.dat"; int *Tab1 = malloc(50 * sizeof(int)); /* 200 octets */ int *Tab2 = calloc(50, sizeof(int)); /* 200 octets initialisés à 0 */ int i, N = 50; for (i = 0; i < N; i++)

Tab1[i] = i; FichierDEntiers = fopen(NomFichier, "wb"); fwrite(Tab1, sizeof(int), N, FichierDEntiers); /* Ecriture de 50 entiers en 1 fois */ fclose(FichierDEntiers);

Page 86: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 86

FichierDEntiers = fopen(NomFichier, "rb"); fread(Tab2, sizeof(int), 50, FichierDEntiers); /* Lecture de 50 entiers en 1 fois */ fclose(FichierDEntiers); for (i = 0; i < N; i++) /* Affichage des 50 entiers de Tab2 */

printf("%4d ", Tab2[i]); free(Tab1); free(Tab2); printf("\n\n"); system("Pause"); return 0;

} 6.3.6.1. L’accès direct Le fichier à accès random (à la demande), aussi nommé fichier à accès direct, ou relatif, permet l’enregistrement de structures complexes. Les champs de chaque enregistrement, et donc les enregistrements eux-mêmes, doivent être tous de la même longueur. Un tel fichier peut être lu séquentiellement, mais aussi par accès direct à un enregistrement souhaité, notamment pour permettre le remplacement d’un enregistrement modifié. En C, ce mode d’accès est exploitable sur les fichiers binaires, essentiellement par l’usage des trois fonctions fseek, rewind et ftell. La fonction fseek permet de se positionner à un endroit précis. Elle retourne un entier différent de zéro, c’est-à-dire logiquement vrai, en cas d’erreur. Son prototype est :

int fseek(FILE *, long Deplacement, int Origine); La variable Deplacement, de type long, détermine la nouvelle position dans le fichier. Il s'agit d'un déplacement relatif, compté en octets, par rapport à la variable Origine. La variable Origine peut prendre une des trois valeurs suivantes : SEEK_SET

ou 0 Le déplacement se fait par rapport au début du fichier. Le décalage spécifié doit être positif ou nul, la valeur 0 correspondant au début du fichier.

SEEK_CUR ou 1

Le déplacement se fait relativement à la position courante. Le décalage spécifié peut donc être négatif, positif ou nul (auquel cas aucun déplacement n’est effectué).

SEEK_END ou 2

Le déplacement se fait relativement à la fin du fichier. Le décalage spécifié doit être négatif ou nul, la valeur 0 correspondant à la fin de fichier.

La fonction rewind positionne le pointeur tout au début du fichier. Elle est équivalente à fseek(FILE *, 0L, 0). Son prototype est :

void rewind(FILE *); La fonction ftell retourne la position courante dans le fichier exprimée en octets, c’est-à-dire la position du prochain octet à lire, ou -1L en cas d’erreur. Son prototype est :

long ftell(FILE *); 6.3.6.1.a. Accès direct sur un fichier d’entiers main() {

FILE *FichierDEntiers; char *NomFichier = "MesEntiers.dat"; int *Tab = malloc(50 * sizeof(int)); /* 50 entiers => 200 octets */ int i, N = 50; for (i = 0; i < N; i++)

Tab[i] = i; FichierDEntiers = fopen(NomFichier, "wb"); fwrite(Tab, sizeof(int), N, FichierDEntiers); fclose(FichierDEntiers);

Page 87: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 87

FichierDEntiers = fopen(NomFichier, "rb"); fseek(FichierDEntiers, 0L, SEEK_END); /* Pointer la fin du fichier */ printf("Pos. : %ld\n", ftell(FichierDEntiers)); /* Affiche : Pos. : 200 */ fseek(FichierDEntiers, -10L * sizeof(int), SEEK_END); /* Reculer de 10 entiers */ printf("Pos. : %ld\n", ftell(FichierDEntiers)); /* Affiche : Pos. : 160 */ fread(&i, sizeof(i), 1, FichierDEntiers); printf("Val. : %d\n", i); /* Affiche : Val. : 40 */ printf("Pos. : %ld\n", ftell(FichierDEntiers)); /* Affiche : Pos. : 164 */ rewind(FichierDEntiers); /* Retourner au début */ printf("Pos. : %ld\n", ftell(FichierDEntiers)); /* Affiche : Pos. : 0 */ fread(&i, sizeof(i), 1, FichierDEntiers); printf("Val. : %d\n", i); /* Affiche : Val. : 0 */ printf("Pos. : %ld\n", ftell(FichierDEntiers)); /* Affiche : Pos. : 4 */ fseek(FichierDEntiers, 5L * sizeof(int), SEEK_CUR); /* Avancer de 5 entiers */ printf("Pos. : %ld\n", ftell(FichierDEntiers)); /* Affiche : Pos. : 24 */ fread(&i, sizeof(i), 1, FichierDEntiers); printf("Val. : %d\n", i); /* Affiche : Val. : 6 */ printf("Pos. : %ld\n", ftell(FichierDEntiers)); /* Affiche : Pos. : 28 */ fclose(FichierDEntiers); free(Tab); printf("\n\n"); system("Pause"); return 0;

} 6.3.6.1.b. Accès direct sur un fichier de personnes typedef struct Personne {

char Nom[50]; float Salaire;

} PERS; main() {

FILE *FPersonnes; char *NomFichier = "MesPersonnes.dat"; int i, j, N = 5; PERS UnePers, *TPers = malloc(5 * sizeof(PERS)); strcpy(TPers[0].Nom, "Azerty"); /* 5 pers. pour l’exemple */ TPers[0].Salaire = 1123.0; strcpy(TPers[1].Nom, "Qwerto"); TPers[1].Salaire = 1234.0; strcpy(TPers[2].Nom, "Panzani"); TPers[2].Salaire = 1345.0; strcpy(TPers[3].Nom, "Rabata"); TPers[3].Salaire = 1456.0; strcpy(TPers[4].Nom, "Brazano"); TPers[4].Salaire = 1567.0; for (i = 0; i < 5; i++) /* Chaînes à longueur à 50 */ { /* caractères, '\0' compris */

for (j = strlen(TPers[i].Nom); j < 50-1; j++) /* Complèter les trop */ TPers[i].Nom[j] = '\0'; /* courtes et tronquer les */

TPers[i].Nom[49] = '\0'; /* trop longues */ } FPersonnes = fopen(NomFichier, "wb"); /* Enregistrer les 5 pers. */ fwrite(TPers, sizeof(PERS), N, FPersonnes); fclose(FPersonnes);

Page 88: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 88

FPersonnes = fopen(NomFichier, "rb"); fseek(FPersonnes, 0L, SEEK_END); /* Pointer la fin du fichier */ fseek(FPersonnes, -2L * sizeof(PERS), SEEK_END); /* Reculer de 2 pers. */ fread(&UnePers, sizeof(PERS), 1, FPersonnes); printf("%s\t%6.1f\n", UnePers.Nom, UnePers.Salaire); /* Affiche : Rabata 1456.0 */ rewind(FPersonnes); /* Retour au début */ fread(&UnePers, sizeof(PERS), 1, FPersonnes); printf("%s\t%6.1f\n", UnePers.Nom, UnePers.Salaire); /* Affiche : Azerty 1123.0 */ fseek(FPersonnes, 3L * sizeof(PERS), SEEK_CUR); /* Avancer de 3 pers. */ fread(&UnePers, sizeof(PERS), 1, FPersonnes); printf("%s\t%6.1f\n", UnePers.Nom, UnePers.Salaire); /* Affiche : Brazano 1567.0 */ fclose(FPersonnes); free(TPers); printf("\n\n"); system("Pause"); return 0;

} 6.3.7. Outils divers 6.3.7.1. Forcer l’enregistrement des données L’instruction fflush(LePointeurDuFichier) provoque l’enregistrement physique des données encore en mémoire. Il est impératif d’exécuter cette fonction après une écriture vers le fichier pour éviter la perte des données qui surviendrait par exemple, à la suite d’une coupure d’électricité. 6.3.7.2. Taille d’un fichier Les librairies standards de l’ANSI C ne proposent pas de fonction retournant la taille d’un fichier. En voici une qui retourne la taille du fichier exprimée en octets, ou -1L en cas d’erreur.

long LongueurFichier(char *NomFichier) {

FILE *F; signed long L = -1; if (F = fopen(NomFichier, "r")) {

fseek(F, 0L, SEEK_END); L = ftell(F); fclose(F);

} return L;

} 6.3.7.3. Réinitialisation des indicateurs La fonction clearerr réinitialise les indicateurs de fin de fichier et d’erreur positionnés sur un flux. Son prototype est :

void clearerr(FILE *); 6.3.7.4. Renommer et supprimer Les fonctions rename et remove ne peuvent s’exécuter que sur des fichiers fermés. Elles ont respectivement pour rôle de renommer et de supprimer un fichier existant. Ces fonctions retournent un entier différent de zéro, c’est-à-dire logiquement vrai, en cas d’erreur. Les prototypes imagés suivants dispensent de tout commentaire.

int rename(const char * VieuxNomDuFichier, const char * NouveauNomDuFichier);

int remove(const char * NomDuFichier);

Page 89: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 89

7. Directives au préprocesseur Le préprocesseur est un programme exécuté lors de la première phase de la compilation. Il effectue des modifications textuelles sur le fichier source à partir de directives. Les différentes directives au préprocesseur, introduites par le caractère #, ont pour but : l'inclusion de fichiers source (#include); la définition de constantes symboliques et de macros (#define); la compilation conditionnelle (#if, #ifdef, ...); d’autres contrôles et actions qui ne sont pas expliqués dans ces pages (#line, #error, #pragma, ...).

7.1. L’inclusion Elle permet d'incorporer dans le fichier source le texte figurant dans un autre fichier. Ce dernier peut être un fichier d’entête de la librairie standard (stdio.h, math.h, ...) ou n'importe quel autre fichier. La directive #include possède deux syntaxes très semblables : 1. le NomDuFichier est spécifié entre chevrons : #include <NomDuFichier> 2. le NomDuFichier est spécifié entre guillemets : #include "CheminCompletEtNomDuFichier" La première syntaxe est utilisée pour tous les fichiers stockés dans un ou plusieurs répertoires systèmes définis par l'implémentation (par exemple, \CompilateurC\Librairies\includes\). C’est la syntaxe généralement utilisée pour les fichiers d’entête de la librairie standard : #include <stdio.h>. La seconde est utilisée pour tous les fichiers stockés ailleurs, dont notamment les fichiers créés par le programmeur. Elle nécessite la désignation du chemin d’accès au fichier : #include "D:\MesLibrairies\NomDuFichier".

7.2. Les définitions La directive #define permet de définir : des constantes symboliques; des macros avec paramètres. 7.2.1. Définition de constantes symboliques La directive #define suivie d’un identificateur, lui-même suivi d’un texte (sur la même ligne), demande au préprocesseur de remplacer dans la suite du fichier source, toutes les occurrences de l’identificateur par la chaîne de caractères constituant le reste de la ligne. Sa principale utilité est de donner un nom représentatif à une valeur littérale. Puisqu’elle est située dans les premières lignes du code source, cette valeur littérale est aisément modifiable par le programmeur.

#define MaxLignes 10 #define MaxColonnes 33 #define Taille MaxLignes * MaxColonnes

7.2.2. Définition de macros La définition d’une macro est très semblable à celle d’une constante symbolique. L’identificateur qui suit la directive #define est flanqué d’une liste de paramètres séparés par une virgule. Le texte du reste de la ligne exprime le corps de la macro. Le travail du préprocesseur consiste ici aussi en une substitution dans la suite du fichier source, de toutes les occurrences de l’identificateur par la chaîne de caractères constituant le reste de la ligne. La distinction par le préprocesseur entre la définition de constante symbolique et celle d'une macro avec paramètres se fait sur le caractère qui suit immédiatement l’identificateur : si ce caractère est une parenthèse ouvrante, c'est une macro avec paramètres, sinon c'est une constante symbolique. Il ne peut donc pas exister un espace entre le nom de la macro et la parenthèse ouvrante. Par exemple, la directive #define Max(a,b) (((a) > (b)) ? (a) : (b)) provoque le remplacement par le préprocesseur de toutes les occurrences de Max(a,b) par (((a) > (b)) ? (a) : (b)). Les paramètres servent la définition du modèle à remplacer, mais ils ne sont pas eux-mêmes remplacés. Le modèle est valide pour Max(a,b), Max(x,y), Max(N1,N2), …

Page 90: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 90

Une macro s’utilise comme une fonction mais, vu que son exécution ne nécessite aucun branchement (appel de fonction), son emploi permet en général d'obtenir de meilleures performances d’exécution et ce, malgré un fichier exécutable plus long (reproduction du corps de la macro à chaque appel). Il faut toujours garder à l'esprit que le préprocesseur n'effectue que des remplacements de chaînes de caractères. En particulier, il convient de mettre entre parenthèses le corps de la macro ainsi que ses paramètres formels. Sans parenthèses, la macro suivante :

#define Carre(a) a * a génère les remplacements :

C = Carre(x + y); C = x + y * x + y; if (!Carre(x)) … if ( ! x * x) …

Mais avec les parenthèses, la macro suivante :

#define Carre(a) ((a) * (a)) génère les remplacements :

C = Carre(x + y) ; C = ((x + y) * (x + y)); if (!Carre(x)) … if ( ! ((x) * (x))) …

Enfin, il faut être attentif aux éventuels effets de bord que peut entraîner l'usage de macros. Par exemple, l’usage d’un opérateur unaire dans la macro précédente peut produire des effets indésirables. Dans l’exemple suivant, l’incrémentation est calculée deux fois.

C = Carre(x++); C = ((x++) * (x++));

7.3. La compilation conditionnelle La compilation conditionnelle a pour but d'incorporer ou d'exclure des parties du code source dans le texte généré par le préprocesseur. Elle permet d'adapter le programme au matériel ou à l'environnement sur lequel il s'exécute, ou d'introduire dans le programme des instructions de débogage. Les directives de compilation conditionnelle se répartissent en deux catégories, suivant le type de condition invoquée : la valeur d'une expression (Mots clés : #if, #elif, #else, #endif); l'existence ou l'inexistence de symboles (Mots clés : #ifdef, #ifndef, #else, #endif). 7.3.1. Condition liée à la valeur d'une expression Sa syntaxe la plus générale est :

#if Condition_1 Partie_1_Du_Programme #elif Condition_2 Partie_2_Du_Programme … … … #elif Condition_N Partie_N_Du_Programme #else Autre_Partie_Du_Programme #endif

Le nombre de #elif est quelconque et le #else est facultatif. Chaque Condition_x doit être une expression constante.

Exemple : #define PROCESSEUR ALPHA … … … #if PROCESSEUR == ALPHA TailleEntierLong = 64; #elif PROCESSEUR == PC TailleEntierLong = 32; #endif

Page 91: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 91

7.3.2. Condition liée à l'existence d'un symbole Sa syntaxe la plus générale est :

#ifdef UnSymbole Une_Partie_Du_Programme #else Une_Autre_Partie_Du_Programme #endif

Si UnSymbole est défini au moment où la directive #ifdef est rencontrée, alors c’est Une_Partie_Du_Programme qui est compilée. Dans le cas contraire, c’est Une_Autre_Partie_Du_Programme qui est compilée. La directive #else est facultative. Ce type de directive est utile pour ajouter des instructions destinées au débogage du programme, là où elles semblent pertinentes :

#define DEBUG … … … #ifdef DEBUG for (i = 0; i < N; i++) printf("%d ",i); #endif

Il suffit de supprimer la directive #define DEBUG pour que les instructions liées au débogage ne soient plus compilées. De façon similaire, il est possible de tester la non-existence d'un symbole. La syntaxe est alors :

#ifndef UnSymbole Une_Partie_Du_Programme #else Une_Autre_Partie_Du_Programme #endif

Cette directive est fréquemment utilisée pour éviter les inclusions redondantes. Plusieurs approches sont possibles, mais seule la meilleure est illustrée ici. La directive est écrite dans le fichier source de la librairie par le programmeur de cette librairie. Idéalement, le symbole rappelle le nom du fichier ciblé par l’inclusion, par exemple _MaLibrairie pour le fichier MaLibrairie.h. La meilleure méthode de contrôle d’inclusion :

#ifndef _UneLibrairie #define _UneLibrairie … … … Tout le corps de la librairie « UneLibrairie.h » … … … #endif

Pour empêcher l’inclusion redondante de libraires standards du compilateur, sans nécessiter la modification de leurs sources, il peut s’avérer utile de créer un fichier à cet effet. Il s’agit d’un fichier source, par exemple MesIncludes.h, dans lequel sont uniquement programmées toutes les inclusions standards requises par l’application. C’est ce fichier qui est ensuite inclus dans les autres sources de l’application : #include <MesIncludes.h>. Il faut toutefois noter que les librairies standards sont elles-mêmes protégées contre les inclusions redondantes.

#ifndef _MesIncludes #define _MesIncludes #include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> … … … #endif

Page 92: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 92

8. Extraits des librairies standards Les fonctions, les types et les macros de la bibliothèque standard définie par la norme ANSI sont déclarés dans les fichiers d'entête standards :

assert.h float.h math.h stdarg.h stdlib.hctype.h limits.h setjmp.h stddef.h string.herrno.h locale.h signal.h stdio.h time.h

Dans la mesure où ils sont protégés contre les inclusions redondantes, les fichiers d'entête peuvent être inclus plusieurs fois, et dans un ordre quelconque. Un fichier d'entête doit être inclus en dehors de toute déclaration ou définition externe et avant toute utilisation des objets qu'il déclare. Un fichier d'entête n'est pas nécessairement un fichier source. Une liste exhaustive de toutes les fonctions disponibles figure à l'annexe B de l'ouvrage de Kernighan et Richie. Ces pages, qui n’en reproduisent qu’un extrait, ne dispensent pas le lecteur de consulter le texte original de cet ouvrage (cf. bibliographie).

8.1. Entrées-sorties : <stdio.h> 8.1.1. Les opérations sur les fichiers Les fonctions suivantes concernent le traitement des fichiers. Le type size_t est le type entier non signé retourné par l'opérateur sizeof. FILE *fopen(const char *filename, const char *mode)

Ouverture ou création du fichier filename selon la valeur de mode. La valeur retournée par cette fonction est un pointeur sur le flot, ou NULL si échec. Les valeurs autorisées pour l'argument mode sont : "r" ouvre un fichier texte en lecture. "w" crée un fichier texte en écriture en écrasant le contenu précédent s’il existait. "a" ouvre ou crée un fichier texte et se positionne en écriture à la fin du fichier. "r+" ouvre un fichier texte en mode mise à jour. "w+" crée un fichier texte en mode mise à jour en écrasant le contenu précédent s’il existait. "a+" ouvre ou crée un fichier en mode mise à jour, et se positionne en écriture à la fin du fichier.

Le mode mise à jour permet de lire et d'écrire dans le même fichier. Il faut appeler fflush ou une fonction de positionnement dans les fichiers entre une lecture et une écriture, et inversement. Si en ajoute un b au mode, comme dans "rb" ou dans "w+b", cela indique un fichier binaire.

FILE *freopen(const char *filename, const char *mode, FILE *stream)

Ouverture ou création du fichier filename selon la valeur de mode en redirection de celui désigné par stream. La valeur retournée par cette fonction est un pointeur sur le flot, ou NULL si échec. Cette fonction est utilisée pour rediriger les flots standard stdin, stdout, ou stderr.

int fflush(FILE *stream)

Vidange du flot en sortie vers le fichier physique. Son effet sur un flot d’entrée est indéfini. Elle retourne une valeur non nulle (égale à EOF) en cas d'échec.

int fclose(FILE *stream)

Force l'écriture des données non écrites du flot ou efface le contenu du buffer d’entrée, et ferme le flot. Elle retourne une valeur non nulle (égale à EOF) en cas d'échec.

int remove(const char *filename)

Suppression du fichier filename. Elle retourne une valeur non nulle en cas d'échec.

Page 93: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 93

int rename(const char *oldname, const char *newname)

Renomme le oldname en newname. Elle retourne une valeur non nulle en cas d'échec. FILE *tmpfile(void)

Création d’un fichier temporaire dans le mode "wb+" qui sera automatiquement détruit lors de sa fermeture ou lors de la fin normale du programme. La valeur retournée par cette fonction est un pointeur sur le flot du fichier créé, ou NULL si échec.

char *tmpnam(char s[L_tmpnam])

Cette fonction crée un nom de fichier inexistant, mais pas le fichier. Elle génère un nom différent chaque fois qu’elle est appelée pendant l'exécution du programme. La constante TMP_MAX indique le nombre maximum de noms différents pouvant être ainsi produits. tmpnam(NULL) crée une chaîne de caractères et retourne un pointeur sur un tableau statique interne. tmpnam(s) crée une chaîne de caractères s et l'utilise comme valeur de retour. La longueur de cette chaîne est au moins égale à L_tmpnam.

int setvbuf (FILE *stream, char *buf, int mode, size_t size)

Cette fonction, qui doit être appelée avant la première lecture ou écriture sur ce flot désigné par stream, en contrôle la mise en mémoire tampon. Les valeurs de mode sont :

_IOFBF : bufferisation complète. _IOLBF : bufferisation par lignes de fichier texte. _IONBF : pas de bufferisation.

Si buf est NULL, un buffer de taille size est alloué, sinon c’est buf qui désigne le buffer. La fonction retourne une valeur non nulle en cas d'échec.

void setbuf(FILE *stream, char *buf)

Si buf est NULL, cette fonction stoppe la bufferisation pour le flot désigné par stream. Autrement, elle équivaut à (void) setvbuf (stream, buf , _IOFBF, BUFSIZ).

int fseek(FILE *stream, long offset, int origin)

Déplacement du pointeur sur flot stream. Pour un fichier binaire, la position est fixée à offset caractères de origin. Les valeurs possibles pour origin sont SEEK_SET (début), SEEK_CUR (position courante) ou SEEK_END (fin de fichier). Pour un flot de texte, offset doit valoir zéro, ou une valeur retournée par ftell (dans ce cas, origin doit valoir SEEK_SET). La fonction fseek retourne une valeur non nulle en cas d'erreur.

int ftell(FILE *stream)

Donne la position courante dans le flot stream, ou bien -1L en cas d'erreur. void rewind(FILE *stream)

Equivaut à fseek (stream, 0L, SEEK_SET); clearerr (stream). int fgetpos(FILE *stream, fpos_t *ptr)

Donne dans *ptr la position courante dans stream, en vue de son utilisation ultérieure par fsetpos. Le type fpos_t doit convenir à la mémorisation de telles valeurs. La fonction retourne une valeur non nulle en cas d'erreur.

Page 94: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 94

int fsetpos(FILE *stream, const fpos_t *ptr)

Positionne stream à la position mémorisée dans *ptr par fgetpos. La fonction retourne une valeur non nulle en cas d'erreur.

8.1.2. Les sorties mises en forme int fprintf(FILE *stream, const char *format, ... )

La fonction fprintf convertit ses arguments d'après les spécifications du format et écrit le résultat sur le flot stream. Elle retourne le nombre de caractères écrits ou, une valeur négative en cas d'erreur.

int printf(const char *format, ...

C’est la fonction fprintf dédicacée à l’unité de sortie par défaut. Elle équivaut à fprintf(stdout). int sprintf(char *s, const char *format, ...)

Fonction similaire à printf, sauf que le résultat est écrit dans la chaîne de caractères s terminée par '\0'. Cette fonction retourne le nombre de caractères écrits, non compris le '\0'.

vprintf(const char *format, va_list arg) vfprintf(FILE *stream, const char *format, va_list arg) vsprintf(char *s, const char *format, va_list arg)

Ces fonctions sont équivalentes aux fonctions printf correspondantes, mis à part que la liste variable d'arguments est remplacée par arg, qui a été initialisée par la macro va_start et éventuellement par des appels de va_arg (cf. <stdarg.h> dans le cours).

8.1.3. Les entrées mises en forme int fscanf(FILE *stream, const char *format, ...)

Fonction de lecture des données du flot stream d'après les spécifications incluses dans format, et affecte les valeurs converties aux arguments suivants, chacun de ceux-ci devant être un pointeur. Elle retourne EOF si la fin de fichier est atteinte ou en cas d’erreur. Sinon elle retourne le nombre d'objets convertis et affectés.

int scanf(const char *format, ...)

C’est la fonction fscanf dédicacée à l’unité d’entrée par défaut. Elle équivaut à fscanf(stdin).

int sscanf(char *format, ... )

Fonction similaire à scanf, sauf que le résultat est extrait de la chaîne de caractères s. 8.1.4. Les fonctions d'entrées-sorties de caractères int fgetc(FILE *stream)

Cette fonction retroune le caractère suivant du flot stream, lu comme un unsigned char (converti en int), ou EOF si la fin de fichier est atteinte ou en cas d’erreur.

char *fgets(char *s, int n, FILE *stream)

Lecture des n-1 caractères suivants du flot qui sont stockés dans la chaîne s. La lecture s’arrête si un caractère de fin de ligne est rencontré. Ce caractère de fin de ligne est aussi copié dans la chaîne qui est terminée ensuite par '\0'. La fonction retourne s, ou bien NULL si la fin de fichier est atteinte ou en cas d’erreur.

Page 95: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 95

int getc(FILE *stream)

Equivalente à fgetc, sauf que si c'est une macro, elle peut évaluer stream plus d'une fois. int getchar(void)

C’est la macro getc dédicacée à l’unité d’entrée par défaut. Elle équivaut à getc(stdin). char *gets(char *s)

C’est la fonction fgets dédicacée à l’unité d’entrée par défaut. Elle équivaut à fgets(stdin). int fputc(int c, FILE *stream)

Ecriture du caractère c (converti en unsigned char) sur le flot stream. Elle retourne le caractère écrit, ou EOF en cas d'erreur.

int fputs(const char *s, FILE *stream)

Ecriture de la chaîne s sur le flot stream. Elle retourne une valeur nulle ou EOF en cas d'erreur. int putc(int c, FILE *stream)

Equivalente à fputc, sauf que si c'est une macro, elle peut évaluer stream plus d'une fois. int putchar(int c)

C’est la macro putc dédicacée à l’unité de sortie par défaut. Elle équivaut à putc(stdout). int puts(const char *s)

C’est la fonction fputs dédicacée à l’unité de sortie par défaut. Elle équivaut à fputs(stdout). int ungetc(int c, FILE *stream)

Remet c sur le flot stream où il sera retrouvé à la prochaine lecture (on peut utiliser ungetc pour placer une valeur différente de c sans modifier le fichier d’origine, ungetc n’affectant que le buffer). On ne peut remettre qu'un seul caractère par flot de façon garantie. Cette fonction retourne le caractère remis, ou bien EOF en cas d'erreur.

8.1.5. Les fonctions d'entrées-sorties directes size_t fread(void *ptr, size_t size, size_t nobj, FILE *stream)

Lecture d’un maximum de nobj de taille size sur le flot stream. Le résultat est stocké dans le tableau désigné par ptr. La fonction retoune le nombre d'objets lus (qui peut être inférieur au nombre demandé). Il faut utiliser feof et ferror pour déterminer l'état du flot.

size_t fwrite(const void *ptr, size_t size, size_t nobj, FILE *stream)

Ecriture de nobj objets de taille size du tableau désigné par ptr sur le flot stream. Elle retourne le nombre d'objets écrits, qui est inférieur à nobj en cas d'erreur.

8.1.6. Les fonctions de gestion des erreurs De nombreuses fonctions positionnent les indicateurs d'état quand une erreur se produit ou quand une fin de fichier est atteinte. void clearerr(FILE *stream)

Remet à zéro les indicateurs de fin de fichier et d'erreur du flot stream.

Page 96: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 96

int feof(FILE *stream)

Retourne une valeur non nulle si l'indicateur de fin de fichier du flot stream est positionné. int ferror(FILE *stream)

Retourne une valeur non nulle si l'indicateur d'erreur du flot stream est positionné. void perror(const char *s)

Imprime s et un message d'erreur dépendant de l'implémentation et correspondant à l'entier contenu dans errno. L'expression entière errno (déclarée dans <errno.h>) peut contenir un numéro d'erreur qui fournit de plus amples informations au sujet de la dernière erreur survenue.

8.2. Les tests de catégories de caractères : <ctype.h> Toutes les fonctions suivantes retournent une valeur non nulle («vrai») si l'argument c remplit la condition indiquée, et zéro sinon.

isalnum(c) isalpha(c) ou isdigit(c) est vrai isalpha(c) isupper(c) ou islower(c) est vrai iscntrl(c) caractère de contrôle isdigit(c) chiffre décimal isgraph(c) caractère imprimable sauf l'espace islower(c) lettre minuscule isprint(c) caractère imprimable y compris l'espace ispunct(c) caractère imprimable différent de l'espace, des lettres et des chiffres isspace(c) espace, saut de page, fin de ligne, retour chariot, tabulation, tabulation verticale isupper(c) lettre majuscule isxdigit(c) chiffre hexadécimal

Deux fonctions permettent de convertir les majuscules en minuscules et vice versa. Ces fonctions retournent le résultat de la conversion de c, ou c lui-même en cas d’erreur.

int tolower(int c) convertit c en minuscules int toupper(int c) convertit c en majuscules

8.3. Les fonctions de traitement des chaînes : <string.h> Les fonctions sur les chaînes définies <string.h> se divisent en deux groupes. Les premières ont des noms qui commencent par str et les secondes ont des noms qui commencent par mem. A l'exception de memmove, leur comportement est indéterminé en cas de copie entre d’objets qui se chevauchent. char *strcpy(char *s, const char *ct)

Copie la chaîne ct, y compris '\0', dans la chaîne s et retourne s. char *strncpy(char *s, const char *ct, size_t n)

Copie au plus n caractères de la chaîne ct dans s et retourne s complèté par des '\0' si ct comporte moins de n caractères.

char *strcat(char *s, const char *ct)

Concatène la chaîne ct à la suite de la chaîne s et retourne s.

Page 97: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 97

char *strncat(char *s, const char *ct, size_t n)

Concatène au plus n caractères de la chaîne ct à la chaîne s terminée '\0' et retourne s.

int strcmp(const char *cs, const char *ct)

Compare les chaînes cs et ct, et retourne une valeur négative, nulle ou positive selon que cs<ct, cs==ct ou cs>ct.

int strncmp(const char *cs, const char *ct, size_t n)

Compare au plus n caractères des chaînes cs et ct, et retourne une valeur négative, nulle ou positive selon que cs<ct, cs==ct ou cs>ct.

char *strchr(const char *cs, int c)

Retourne un pointeur sur la première occurrence de c dans cs, ou NULL si c n’existe pas dans cs. char *strrchr(const char *cs, c)

Retourne un pointeur sur la dernière occurrence de c dans cs, ou NULL si c n’existe pas dans cs. size_t strspn(const char *cs, const char *ct)

Retourne le nombre de caractères du début de cs constitué de caractères appartenant à ct. size_t strcspn(const char *cs, const char *ct)

Retourne le nombre de caractères du début de cs constitué de caractères n'appartenant pas à ct. char *strpbrk(const char *cs, const char *ct)

Retourne un pointeur sur la première occurrence, dans la chaîne cs, de n'importe quel caractère de la chaîne ct, ou NULL si aucun n'y figure.

char *strstr(const char *cs, const char *ct)

Retourne un pointeur sur la première occurrence de la chaîne ct dans la chaîne cs, ou NULL si elle n'y figure pas.

size_t strlen(const char *cs)

Retourne le nombre de caractères utiles cs. char *strerror(size_t n)

Retourne un pointeur sur la chaîne correspondant à l'erreur n, définie par l'implémentation. char *strtok(char *s, const char *ct)

Recherche dans s des lexèmes (tokens) délimités par des caractères appartenant à ct. void *memcpy(void *s, const void *ct, size_t n)

Copie n caractères de ct dans s et retourne s. void *memmove(void *s, const void *ct, size_t n)

Même fonction que memcpy, mais fonctionne aussi si les objets se chevauchent.

Page 98: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 98

int memcmp(const void *cs, const void *ct, size_t n)

Compare les n premiers caractères de cs à ct. La valeur de retour se détermine comme pour strcmp. void *memchr(const void *cs, int c, size_t n)

Retourne un pointeur sur la première occurrence du caractère c dans cs, ou NULL si c n’existe pas dans les n premiers caractères.

void *memset(void *s, int c, size_t n)

Remplit les n premiers caractères de s par le caractère c et retourne s.

8.4. Les fonctions mathématiques : <math.h> Dans le tableau suivant, x et y sont de type double, n de type int, et toutes les fonctions retournent un double. Pour les fonctions trigonométriques, les angles sont exprimés en radians.

sin(x) sinus de x. cos(x) cosinus de x. tan(x) tangente de x. asin(x) arc sinus de x, dans l'intervalle [-/2, /2, x [-1, 1]. acos(x) arc cosinus de x, dans l'intervalle [0, ], x [-1, 1]. atan(x) arc tangente de x, dans l'intervalle [-/2, /2]. atan2(y, x) arc tangente de y/x, dans l'intervalle [-,]. sinh(x) sinus hyperbolique de x. cosh(x) cosinus hyperbolique de x. tanh(x) tangente hyperbolique de x. exp (x) fonction exponentielle ex log(x) logarithme népérien ln(x), x > 0. log10(x) logarithme à base 10 : log10(x), x > 0. pow(x, y) xy. Il y a erreur de domaine si x = 0 et y 0, ou si x < 0 et y n'est pas un

entier. sqrt(x) x, x 0. ceil(x) le plus petit entier supérieur ou égal à x, exprimé en double. floor(x) le plus grand entier inférieur ou égal à x, exprimé en double. fabs(x) valeur absolue |x|. ldexp(x, n) X * 2n. frexp(x, int *exp) sépare x en une fraction normalisée dans l'intervalle [1/2, 1[, qui est

retournée, et une puissance de 2, qui est placée dans *exp. Si x est nul, les deux parties du résultat sont nulles.

modf(x, double *ip) sépare x en ses parties entière et fractionnaire, toutes deux du même signe que x. Cette fonction place la partie entière dans *ip et retourne la partie fractionnaire.

fmod(x,y) reste de x/y, exprimé en virgule flottante, de même signe que x. Si y est nul, le résultat dépend de l'implémentation.

8.5. Les fonctions utilitaires : <stdlib.h> double atof(const char *s)

Conversion de la chaîne s en un double. int atoi(const char *s)

Conversion de la chaîne s en un int.

Page 99: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 99

long atol(const char *s)

Conversion de la chaîne s en un long. double strtod(const char *s, char **endp)

Conversion du début de s en un double, sans tenir compte des caractères d'espacement de tête. Elle place dans *endp un pointeur sur la partie non convertie de s, si elle existe et si endp est différent de NULL.

long strtol(const char *s, char **endp, int base)

Conversion du début de s en un long, sans tenir compte des caractères d'espacement de tête. Elle place dans *endp un pointeur sur la partie non convertie de s, si elle existe et si endp est différent de NULL.

unsigned long strtoul(const char *s char **endp, int base)

Conversion du début de s en un unsigned long, sans tenir compte des caractères d'espacement de tête. Elle place dans *endp un pointeur sur la partie non convertie de s, si elle existe et si endp est différent de NULL.

int rand(void)

Cette fonction donne un entier pseudo-aléatoire compris entre 0 et au moins 32767. void srand(unsigned int seed)

Génère une nouvelle séquence de nombres pseudo aléatoires sur base de l’amorce seed. L'amorce initiale vaut 1.

void *calloc(size_t nobj, size_t size)

Retourne un pointeur sur un espace mémoire réservé à un tableau de nobj objets de taille size, ou NULL en cas d’erreur. La mémoire allouée est initialisée par des zéros.

void *malloc(size_t size)

Même fonction que calloc, sauf que la mémoire allouée n'est pas initialisée. void *realloc(void *p, size_t size)

Change en size la taille de l'objet pointé par p. Le contenu de l'objet est conservé si la nouvelle taille est supérieure à la précédente. L’espace mémoire supplémentaire n'est pas initialisé. La fonction retourne un pointeur sur le nouvel espace mémoire, ou bien NULL en cas d’erreur.

void free(void *p)

Libère l'espace mémoire pointé par p. Cet espace doit avoir été alloué par ca1loc, malloc ou realloc. void abort(void)

Provoque un arrêt anormal du programme. void exit(int status)

Provoque l'arrêt normal du programme. int system(const char *s)

Lance l’exécution d’une commande système spécifiée par chaîne s.

Page 100: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 100

char *getenv(const char *name)

Retourne la chaîne d'environnement désignée par name, ou NULL si cette chaîne n'existe pas. int abs(int n)

Donne la valeur absolue de n (de type int) long labs(long n)

Donne la valeur absolue de n (de type long). div_t div(int num, int denom)

Donne le quotient et le reste de la division de num par denom. Le quotient et le reste sont placés respectivement dans les champs quot et rem, de type int, d'une structure de type div_t.

ldiv_t ldiv(long num, long denom)

Donne le quotient et le reste de la division de num par denom. Le quotient et le reste sont placés respectivement dans les champs quot et rem, de type long, d'une structure de type ldiv_t.

8.6. Date et heure : <time.h> Plusieurs fonctions permettent d'obtenir la date et l'heure. Le temps est représenté par des objets de type time_t ou clock_t, lesquels correspondent généralement à des int ou à des long int. Fonction Prototype Action

time time_t time(time_t *tp) Retourne le nombre de secondes écoulées depuis le 1er

janvier 1970 à 0 heures G.M.T. La valeur retournée est assignée à *tp.

difftime double difftime(time_t t1, time_t t2) Retourne la différence t1 - t2 en secondes.

ctime char *ctime(time_t *tp) Convertit le temps système *tp en une chaîne de caractères explicitant la date et l'heure sous un format prédéterminé.

clock clock_t clock(void) Retourne le temps CPU en microsecondes utilisé depuis le dernier appel à clock.

Page 101: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 101

9. Quelques énoncés d’exercices

9.1. Les types de données Par programmation, en vous inspirant des formules utilisées dans les exemples du cours sous le titre « Les types

entiers », et sans usage de la librairie limits.h, déterminez si les types char et int de votre implémentation sont signés ou non par défaut, et calculez leur plage de valeurs dans chaque combinaison permise de modificateurs.

9.2. Les instructions de branchement 1. Modifiez l’exemple proposé dans le cours sous le titre « L’instruction de branchement break » de sorte à produire

le même résultat sans utiliser l’instruction break. Mettez en œuvre une fonction main permettant l’expérimentation de l’exemple modifié.

2. Modifiez l’exemple proposé dans le cours sous le titre « L’instruction de branchement continue » de sorte à

produire le même résultat sans utiliser l’instruction continue. Mettez en œuvre une fonction main permettant l’expérimentation de l’exemple modifié.

9.3. Les paramètres de la fonction main Ecrivez le programme Calcul.exe à lancer en ligne de

commande avec un nombre indéterminé (au moins deux) de valeurs numériques entières en paramètres effectifs. Le programme doit calculer et afficher : 1. Le nombre de valeurs traitées; 2. La plus petite de ces valeurs (extrême inférieure); 3. La plus grande de ces valeurs (extrême supérieure); 4. La somme des valeurs; 5. Le produit des valeurs; 6. Le plus grand diviseur commun (PGCD) des extrêmes; 7. Le plus petit multiple commun (PPCM) des extrêmes.

9.4. Les pointeurs 1. Testez l’exemple du tri d’un tableau de chaînes par la fonction qsort, sous le titre « Les pointeurs sur fonctions »,

et tâchez de l’adapter ensuite au tri du tableau strictement dynamique de chaînes présenté sous le titre « Pointeurs et chaînes de caractères ».

2. Réalisez un algorithme explouitant la fonction qsort pour trier les entiers de chaque vecteur du tableau

dynamique créé par le programme décrit sous le titre « Pointeurs et tableaux à plusieurs dimensions ». 3. Réalisez un algorithme original pour classer les vecteurs du tableau dynamique créé par le programme décrit sous

le titre « Pointeurs et tableaux à plusieurs dimensions », par ordre croissant des sommes de leurs entiers. 4. Réalisez une application de gestion d’une liste chaînée d’entiers et permettant notamment :

1. Comptage des éléments de la liste. 2. Insertion après le Nième élément de la liste. 3. Insertion avant le Nième élément de la liste. 4. Suppression du Nième élément de la liste. 5. Recherche de l’élément contenant une donnée désignée. 6. Insertion après l’élément contenant une donnée désignée. 7. Insertion avant l’élément contenant une donnée désignée. 8. Suppression de l’élément contenant une donnée désignée. 9. Création d’une liste de structures contenant 2 pointeurs en contre sens. 10. Tri d’une liste chaînée.

Algorithme du PGCD de N et M : PGCD = |N|; X = |M|; Tant que X > 0

Y = PCGD Modulo X; PGCD = X; X = Y;

Fin Tant; Formule du PPCM de N et M : PPCM(N, M) = |N * M| / PGCD(N, M)

Page 102: Langage C (ANSI)

[ phr @ skynet . be ] 12/03/2015 PHR – Langage C 102

5. Effectuez la recherche d’un algorithme de rotation et/ou d’équilibrage d’arbre binaire et implémentez-le dans les codes de l’exemple du cours de sorte à pouvoir rééquilibrer l’arbre à la demande.

9.5. Les fichiers 1. Réalisez un programme qui effectue la copie d’un fichier texte, caractère par caractère, en effectuant au passage

l’une des tâches suivantes selon l’option indiquée par l’utilisateur :

Copie sans changement Changer les minuscules en majuscules Changer les majuscules en minuscules Changer les minuscules en majuscules et vice versa

Les noms des fichiers d’entrée et de sortie, ainsi que le choix du traitement, peuvent être passés par paramètres en ligne de commande ou demandés à l’utilisateur. Ces informations sont demandées si elles sont absentes de la ligne de commande.

2. Réalisez un programme qui effectue la copie d’un fichier texte, ligne par ligne, en effectuant au passage le

remplacement d’une chaîne donnée Rech par une autre Remp également donnée. Ces chaînes peuvent être de longueurs différentes. Certaines fonctions de la librairie <string.h> énumérées précédemment dans la partie « Extraits des librairies standards » peuvent s’avérer très utiles. Les noms des fichiers d’entrée et de sortie, ainsi que les chaînes Rech et Remp, doivent être passés par paramètres en ligne de commande.

3. Réalisez un programme permettant, par l’usage des entrées-sorties formatées, la gestion d’un fichier texte dont les

données proviennent de la variable P de type PERSONNE, ce dernier contenant les informations suivantes : Nom, Prenom, Age, Localite. La gestion doit permettre :

1. l’ajout d’enregistrements; 2. la lecture et l’affichage de tout le fichier enregistrement par enregistrement; 3. la recherche et l’affichage des informations d’une personne désignée par son Nom.

4. Réalisez un programme permettant la gestion d’un fichier à accès direct dont les données proviennent de la variable

P de type PERSONNE, ce dernier contenant les informations suivantes : Nom, Prenom, Age, Localite. La gestion doit permettre :

1. l’ajout d’enregistrements; 2. la modification des données du dernier enregistrement sélectionné; 3. la suppression du dernier enregistrement sélectionné (nécessite la mise au point d’un algorithme adéquat); 4. la recherche et l’affichage des informations d’une personne désignée par son Nom; 5. l’affichage des enregistrements par balayage, c’est-à-dire accès au premier, au précédent, au suivant ou au

dernier, selon la demande de l’utilisateur. 5. Réalisez un programme permettant la gestion d’un fichier à accès direct de Mots et Numeros. Ce programme

nécessite la mise en œuvre du tri fichier et de la recherche dichtomique : ces algorithmes doivent agir directement sur le fichier (et non sur une table en mémoire). La gestion doit permettre :

1. l’ajout d’enregistrements; 2. l’affichage séquentiel, un par un, du premier au dernier; 3. le tri sur les Mots (tri 3 bandes); 4. recherche du Numeros d’un Mots (recherche dichotomique sur les Mots).

6. Modifez l’exercice précédent (n°5), de sorte que le fichier des Mots et Numeros devienne le fichier index (sur le

champ Nom) d’un fichier de PERSONNEs, et complétez l’application de gestion des personnes (exercice n°4) en y ajoutant les fonctionnalités de tri et de recherche indexée.