Préface
PREFACE
C est un langage de programmation algorithmique très utile
et très utilisé dans de nombreuses applications. Développé
initialement (1972 déjà) pour le système Unix1, lequel est écrit en
C, il a vite été adopté par une large communauté d'utilisateurs
aussi bien sous Unix que dans d'autres environnements.
C se caractérise par une concision d'écriture, ses structures
de contrôles très élaborées, ses possibilités en structure de
données et un ensemble riche d'opérateurs. Au premier abord, C
se manifeste par sa syntaxe quelque peu décourageante, mais a
laquelle on s'habitue assez vite du fait de l'orthogonalité des
concepts mis en oeuvre dans le langage. Selon Brad J. COX, et
en paraphrasant, le langage C est comme un gros couteau bien
aiguisé. Pour un novice, il est dangereux, mais placé en des mains
expertes, il peut se révéler d'une redoutable efficacité.
Par certains aspects, C peut être jugé comme un langage de
"haut niveau" et par d'autres (dus à son origine historique), il
peut l'être moins, dans le sens où C traite des objets jusqu'au
niveau machine. Ce fut d'ailleurs un jeu chez des informaticiens
que d'écrire les programmes C les plus courts ou les plus illisibles
possibles. Peuvent être considérées comme "bas niveau" des
écritures telles que:
z = (a>b) ? a : b;
++p->c;
les détails des entrées/sorties avec formats,
printf("%d %-6.2f ...);
ou les manipulations proches des caractéristiques machines...
Mais, les fonctionnalités offertes dans le langage, en font un outil
de programmation très puissant aussi bien pour les applications
systèmes (accès aux mécanismes d'un système d'exploitation) que
pour les applications conventionnels. Un simple exemple, comme
1 Le mot UNIX est une marque déposée.
i
celui de l'écriture d'un programme qui a pour résultat lui-même
(i.e. son source) atteste de cette puissance (cf. exemples en fin de
chapitre III).
Le langage C utilisé aujourd'hui est le produit de plusieurs
années d'évolution. Il a été conçu en 1970 par Ken Thompson
pour l'écriture sur PDP-11 du premier Unix.
A propos de ce livre
Ce livre est une introduction progressive à la programmation avec
le langage C. Il vise deux objectifs:
- En s'appuyant sur des exemple, introduire de façon simple
et concise les mécanismes du langages.
- Rappeler au fur et à mesure et autant que possible,
quelques règles importantes dans la programmation,
illustrées aussi par les exemples.
Des exercices progressifs accompagnent la présentation des
différentes notions ainsi que des exemples complets de
programmes ayant aussi un caractère pédagogique. Le lien
http://www.emi.ac.ma/~ntounsi/C/ contiendra les corrections des
exercices.
Le livre se présente en deux parties: une partie composée de trois
chapitres pour les mécanismes de base du langage, et une partie
composée de deux autres chapitres pour les concepts plus avancés
de structures de données et de fonctions.
ii
Préface
AVERTISSEMENT
Dans la présentation de certaines phrases du langage C, nous
emploierons la notation suivante:
<nom> est une catégorie syntaxique en italique. Les crochets [ ]
entourent une clause facultative.
struct [<nom>] {<liste declarations>}
signifie qu'il y a un nom, c'est à dire un identificateur C qui vient
après struct et qui est facultatif, ensuite une liste de déclaration
entre accolades. Exemple:
struct tuple {int numero; char[20] nom;}
struct {float x, y; }
Les exemples sont donnés et vérifiés sous compilateur C ANSI
des systèmes à base de Unix (e.g. Mac OS, Linux, etc. dits Unix-
like). Parfois le script complet de vérification est montré tel quel.
Prompt %
% cat prgme.c pour afficher le source
% cc prgme.c pour le compiler
% a.out pour la trace d'exécution
Si certains exemples sont donnés utilisant un script Unix avec
des commandes cc, cat, a.out... cela n'est pas lié au langage C
lui-même. Les mêmes commandes ou utilisations peuvent être
faites sous d'autres OS (Windows en mode Command-line), ou
avec des outils environnements intégrés de développement,
comme DevCpp, ou autre.
iii
Table des matières
Table des matières
A propos de ce livre ii
1. Ce qu'est un Programme C 1
2. Exemples 2
2.1. Exemple 1 2
2.2. Exemple 2 3
3. Structure d'un Programme C 5
3.1. Directives de Compilation 6
3.2. Déclarations de Noms Externes 7
3.3. Textes de Fonctions 7
4. Compilation d'un Programme C 9
5. Compilation Séparée 11
1. Introduction 13
1. Objets de Base de C 14
1.1. Caractère: char 14
1.2. Entiers: int, short int, long int, unsigned 15
1.3. Réels: short float , long float 18
1.4. Initialisations 18
2. Opérateurs Arithmétiques 19
3. Les Tableaux 21
4. Les Chaînes de Caractères char* idf ou char idf[n] 23
5. Des Opérateurs de C 27
6. Règles de Conversion 28
6.1. Conversion Implicite 28
6.2. Conversion Explicite 31
7. Expressions Conditionnelles 32
7.1. Opérateurs Logiques sur les Bits. 34
8. Récapitulatif des Opérateurs de C 36
1. Blocs d'Instructions 39
2. La Conditionnelle if-else 40
3. Choix Multiple switch 43
4. Itérations while, for, do-while 44
4.1. Boucle while 45
4.2. Boucle for 45
4.3. Boucle do...while 47
5. Les Ruptures de Contrôle 48
5.1. Break 48
5.2. Continue 49
5.3. Return 50
iv
Table des matières
5.4. Goto goto <label> 50
6. Exemples de Programmes 51
1. Les Pointeurs 77
1.1. Opérateurs * et & 77
1.1.1. Notion d'Objet et d'Adresse 77
1.1.2. Notion de Pointeur 79
1.2. Arithmétique des Pointeurs 80
1.3. Valeur NULL 83
1.4. Initialisation d'un Pointeur 83
1.5. Pointeurs ** 86
2. Pointeurs et Tableaux 88
3. Tableaux et Chaînes de Caractères 91
4. Tableaux et Pointeurs 93
4.1. Tableau de Chaînes de Caractères 95
5. Types Structurés 98
5.1. La Structure struct 98
5.2. Tableaux de Structures 101
5.3. Pointeurs vers les Structures 102
5.4. Combinaison de pointeurs tableaux et structures 103
5.5. Structures Récursives 104
5.6. Unions 105
5.7. Typedef 106
5.8. Exemples de Programmes 109
6. Types Enumérés 112
1. Les Fonctions 117
1.1. Déclaration de Fonction 117
1.2. Définition de Fonction 119
1.3. Appel de Fonction 119
1.4. C ANSI vs C Classique 120
1.5. Mode de Passage des Paramètres 122
1.6. Fonctions à Nombre Variable de Paramètres 124
1.7. Passage de Tableaux en Paramètres 130
1.8. La Récursivité 136
2. Statut des Variables et Classes d'Allocation 138
2.1. Portée vs Durée de Vie 138
2.1.1. Portée 138
2.1.2. Durée de Vie 139
2.2. Variables extern et Variables registre 142
3. Les Fonctions en Paramètres 145
4. Les Fonctions d'Entrées/Sorties de Base 151
4.1. Getchar() et Putchar() 152
4.2. Getc() et Putc() 153
4.3. Gets() et Puts() 154
4.4. scanf() et printf() 154
4.4.1. scanf() 155
4.4.2. printf() 157
v
Table des matières
4.5. sscanf() et sprintf() 159
vi
Structure Générale d'un Programme C
CHAPITRE I
STRUCTURE GENERALE
D'UN PROGRAMME C
The only way to learn a new programming language is by writing programs
in it.
La seule façon d'apprendre un nouveau langage de programmation, c'est
d'écrire des programmes avec.
-- Brian Kernighan
1. Ce qu'est un Programme C
Un programme C se présente comme un ensemble de
fonctions réparties sur un ou plusieurs fichiers nom.c. Pour
compiler un programme, il suffit d'indiquer au compilateur C
(commande Unix-like2 cc, C Compiler) cette liste de fichiers:
cc pgme1.c pgme2.c etc...
Le compilateur génère alors un fichier exécutable a.out
correspondant au programme. On peut changer ce nom a.out en
un autre, comme monProgramme, avec l'option -o de la commande
cc:
cc -o monProgramme pgme1.c pgme2.c
Limitons-nous pour l'instant à un seul fichier source C.
Parmi les fonctions d'un programme C, une est obligatoirement
présente et doit s'appeler main(). Sans arguments pour l'instant.
Elle correspond au programme principal (d'où son nom), i.e. c'est
la première exécutée et le nom main est alors obligatoire3. Donc
un programme C contient au moins cette fonction. L'exécution
2 Voir l'avertissement à ce sujet
3 On peut bien sûr compiler séparément un fichier de fonction C sans vouloir en
constituer un programme. Voir §4 ci-après.
1
Langage C
d'autres fonctions se fait lors de leurs appels comme il est
courant.
Certaines fonctions, très utilisées et déjà écrites, peuvent
être rajoutées à un programme C par une directive spéciale:
#include, qui insère le texte de ces fonctions dans celui d'un
programme.
2. Exemples
2.1. Exemple 1
Un premier exemple, classique, de programme C est:
main()
{
printf("hello, world\n");
}
qui imprime le texte:
hello, world
La première ligne est la déclaration de l'entête d'une
fonction, ici main(). Les accolades {} délimitent le corps de la
fonction (cf. begin ... end). Celui-ci est constitué de l'instruction
printf("hello, world\n");
qui est un appel à une fonction printf() de bibliothèque, avec
l'argument "hello,world\n". Cette fonction a pour effet
d'imprimer ses arguments sur le terminal (ou la sortie standard
du programme). Le seul argument ici est la chaîne de caractères
entre guillemets. La séquence \n à une signification spéciale: \
sert à indiquer un caractère de contrôle, comme n, qui représente
newline.
Si le programme main() ci-dessus se trouve dans un fichier
hello.c on le compile par (% étant le caractère invite ici):
% cc hello.c
et on l'exécute par:
2
Structure Générale d'un Programme C
% a.out
hello, world
% _
a.out étant le fichier exécutable résultat de la compilation.
Remarque: En réalité la fonction printf() existe dans un fichier
stdio.h (pour STandarD Input Output) faisant partie de la
bibliothèque du compilateur C et qui contient des utilitaires
d'Entrées/Sorties. L'extension .h signifie Header (entête).
2.2. Exemple 2
Un deuxième exemple est un programme qui lit deux
entiers et imprime leur somme. Il fait appel à une fonction
somme(a,b):
#include <stdio.h>
/* inclusion en tête du programme
du fichier bibliothèque
C stdio.h
*/
int somme(int, int);
/* Déclaration d'une fonction somme */
main() {
int a,b,s;
scanf("%d %d", &a,&b); /* lecture des deux
entiers */
s = somme (a,b); /* appel de la
fonction somme */
printf(" Voici leur somme : %d\n", s);
}
/* La fonction somme avec deux
paramètres formels x et y */
int somme (int x, int y) {
return (x+y);
}
La première ligne de la fonction main() est une déclaration
d'entiers a b et s. le type est int pour integer, placé avant. Ce
sont des variables locales dont la portée est limitée au bloc de la
fonction. La ligne suivante est l'appel de la fonction scanf() pour
3
Langage C
la lecture des variables a et b. Le symbole & sera justifié plus bas
(§ 3.3). Entre guillemets dans scanf(), il est explicité ce qu'on
appelle un format, introduit par le symbole %. Comme il y a deux
variables, a et b, il y a deux formats %d, ou d signifie décimal (on
utilise %f pour les réels, %c pour caractère et %s pour les chaînes
string etc... voir plus loin les détails au chapitre V). Ainsi il faut
présenter en entrée pour scanf deux entiers (séparés par un
"blanc": espace, tabulation tab ou entrée newline, qui jouent un
rôle de séparateurs de champs).
La ligne suivante du programme est l'appel de la fonction
somme avec les arguments (paramètres effectifs) a et b. Fonction
dont on connaît l'interface,
int somme(int, int);
composé du type du résultat (int) et des paramètres (int aussi),
et déclaré en début de programme.
Ensuite printf() imprime la valeur de la variable s, avec
un format décimal %d, précédée du texte Voici leur somme :.
La fonction somme consiste à renvoyer à la fonction
appelante, main() ici, la somme x+y. Voyons comment elle se
présente: la ligne
int somme(int x, int y)
est l'entête de la fonction qui spécifie son nom, le type de son
résultat et de ses paramètres formels x et y. Cette entête est
suivie immédiatement du corps de la fonction entre {}. Une autre
écriture de l'entête de la fonction est
int somme (x, y) int x, y;
où on déclare les paramètres après leur énumération entre ().
La première écriture est la syntaxe C ANSI (et C++), plus
intéressante ( cf. Chapitre V).
Les symboles /* et */ encadrent un commentaire en C.
/* Ceci est un commentaire */
/* Ceci est /* n'est vraiment pas */
un autre commentaire */
4
Structure Générale d'un Programme C
On peut remarquer déjà que chaque déclaration ou instruction en
C se termine par point-virgule ";" . On verra que contrairement à
PASCAL par exemple, un ";" ne sépare pas deux instructions ou
déclarations, mais termine une instruction ou déclaration.
Voici la compilation et l'exécution de ce deuxième
programme:
% cc somme.c
% a.out
2
3 <--- entrée des données 2 pour
a et 3 pour b
Voici leur somme : 5 <--- Résultat
Une deuxième exécution:
% a.out
2 3 <--- { mêmes données sur même ligne
cette fois-ci }
Voici leur somme : 5
On peut imprimer un texte pour inviter l'utilisateur à
rentrer des données: on doit sortir un message par printf avant
de faire scanf
printf(" Renter a ensuite b > ");
scanf("%d%d", &a,&b);
ou mieux
printf(" Renter a > ");
scanf("%d", &a);
printf(" Renter b > ");
scanf("%d", &b);
Ce qui donne dans ce dernier cas
% a.out
Renter a > 2
Renter b > 3
Voici leur somme : 5
%
3. Structure d'un Programme C
Un programme C se présente sous la forme générale
suivante:
5
Langage C
<directives de compilation>
<déclarations de noms externes>
<textes de fonctions>
Seule la partie textes de fonctions est obligatoire avec au
minimum la fonction main() pour un programme complet4.
3.1. Directives de Compilation
Classiquement on y retrouve ce dont le compilateur a besoin
pour compiler correctement un programme. A savoir les #include
pour les fichiers fonctions de bibliothèque à inclure dans le
programme, des définitions de symboles de types ou de con-
stantes, ou des macros. Cette partie est traitée par un
préprocesseur: programme invoqué pour un premier passage sur
un texte source. C'est une autre caractéristique de C.
Exemples:
#define begin {
#define end }
#define then
#define MAX 1000
#include <math.h>
#include <ctype.h>
#include <string.h>
begin est défini comme symbole synonyme de {. Ainsi, si begin
apparaît dans un programme, le préprocesseur le remplacera par
{. end est défini comme synonyme de } et then comme
synonyme de rien (il sera ignoré car il n'y a pas le mot then en C).
MAX est défini comme synonyme de la constante entière 1000.
Il est ensuite demandé d'inclure dans le texte du
programme les fichiers de bibliothèque math.h, ctype.h et
string.h. Respectivement, des fonctions mathématiques, des
utilitaires sur les types de donnés et la manipulation de chaînes.
4Se rappeler néanmoins qu'un fichier C peut être seulement compilé avec l'une des
parties citées
6
Structure Générale d'un Programme C
3.2. Déclarations de Noms Externes
Dans cette partie, on peut déclarer des noms globaux dont la
portée peut être l'ensemble d'un programme C. Ces noms
correspondent à des variables aussi bien qu'à des fonctions. Pour
ces dernières, seule l'interface (l'entête sans le corps) peut être
fournie. On l'appelle aussi prototype de fonction (voir Chapitre V).
Exemple:
int i;
float f( int, char*, float);
3.3. Textes de Fonctions
Viennent enfin les définitions de fonctions, entête et corps,
qui constituent le programme proprement dit, i.e. la partie calcul.
Leur structure est la suivante:
[<type résultat>] <nom de fonction> ([<liste arguments
typés>])
{
<texte des instructions>
}
Exemple:
float f (int i, char* s, float x)
{ float y;
...
return y;
}
Noter que la notion de ligne de texte n'existe pas en C. Mais
il est de bonne habitude d'écrire le corps d'une fonction sous
forme d'une instruction par ligne, indentée selon le besoin. La
plupart des éditeurs font l'indentation automatique. Il y a aussi
les commandes Unix-like cb ou indent qui indentent un source C
après écriture.
Une fonction rend un résultat et son type est celui
précédant le nom de la fonction. Ce type est pris par défaut
comme int pour integer. Ainsi on aurait pu définir la fonction
somme du § précédent comme
7
Langage C
somme(int x, int y) {
...
}
sans avoir besoin de la déclarer en début de programme.
Noter aussi qu'en C les arguments sont passés par valeurs,
comme il convient pour une fonction. (C++ a introduit, entre
autre, le passage des paramètres par référence, voir remarque
plus bas). Plusieurs questions se posent alors.
- Comment faire pour avoir une procédure qui n'a pas de
résultat?
On dit alors que le type de la fonction est void , c'est à dire
neutre. La fonction est alors une simple routine et ne doit pas
être appelée comme une fonction à résultat dans une expression
de calcul.
- Comment faire si on veut une procédure qui modifie la
valeur de ses paramètres, ou procédure à plusieurs résultats,
comme par exemple permut(x, y) qui permute les valeurs x et y
(et modifie donc x et y)?
On passe alors comme paramètres à l'appel non pas les variables
mais l'adresse de ces variables,
permut(&a, &b);
Le programmeur dispose d'un opérateur unaire & qui permet
d'obtenir l'adresse d'une variable. Ainsi, c'est la variable elle-
même qui est manipulée, à travers son adresse, et non pas une
copie. (On doit dans ce cas déréférencer x et y dans la fonction
avec la notation *x et *y.)
C'est pourquoi les paramètres, a et b, de la fonction scanf()
du programme précédent sont précédés de &. Ce sont des résultats
de la fonction. Cette possibilité sert aussi si la taille d'un objet est
suffisamment grande pour être passée en paramètre par valeur.
Remarque: C++ a introduit le passage par référence, pour une
commodité de notation. On n'a pas besoin de déréférencer
explicitement les paramètres dans le corps de la fonction.
8
Structure Générale d'un Programme C
4. Compilation d'un Programme C
Nous avons vu que l'appel au compilateur C se fait par la
commande cc. Nous l'avons utilisée telle que pour compiler un
seul fichier et pour générer directement un module exécutable
a.out. En fait, cette commande cc enchaîne le préprocesseur, la
compilation, l'assemblage et l'édition de lien d'un programme.
Chacune de ces phases peut être faite individuellement bien sûr
mais, dans les cas simples, on fait le tout d'un coup. Sinon on
sépare la phase d'édition de lien, au cas où un programme se
compose de plusieurs fichiers, pour ne pas avoir à recompiler des
morceaux déjà compilés et corrects. C'est ce qu'on appelle la
compilation séparée.
La forme générale de cette commande cc est:
% cc [<options>] <nomFichiers> ... [-l<Librairie>] ...
Principalement, les options sont -c et -o et les fichiers se ter-
minent par .c et .o Les fichiers .c sont des sources C et les
fichiers .o sont des modules objets résultats de la compilation de
fichiers .c. Ce sont des fichiers codes binaires non encore édités
ou liés, donc non prêts à l'exécution.
Remarque: Ces fichiers .o sont toujours créés mais ne sont pas
toujours sauvegardés. Ils peuvent l'être à la demande ou à la
compilation de plusieurs sources C. On verra plus bas l'utilité de
les garder.
La partie -l est nécessaire (lors de l'édition de lien) pour
faire appel à des modules objets de bibliothèque, quand on utilise
des fonctions qui s'y trouvent. Exemple, -lm pour la bibliothèque
mathématique, -lX11 pour celle X11, etc ... Nous en ferons
abstraction ici. Ainsi la commande cc se présente:
[1] Sans options et avec des fichiers .c
% cc pgme.c
Le fichier pgme.c est compilé et un exécutable a.out
correspondant est généré
(Si toutefois il existe une fonction main() sinon il y a erreur
d'édition empêchant de générer un exécutable).
9
Langage C
% cc pgme1.c pgme2.c ...
Les fichiers sources pgme1.c, pgme2.c... sont compilés et,
pour chacun, un module objet pgme1.o, pgme2.o ... est
généré en plus de l'exécutable (général) a.out. Ces fichiers
sont générés dès qu'il y a plusieurs sources .c .
Soit, sans options, la commande cc avec un seul fichier
source .c crée un fichier exécutable a.out, et avec plusieurs
fichiers sources .c, elle crée en plus les fichiers modules objets
pgme1.o, pgme2.o ... correspondants.
[2] Sans option, avec fichiers .o
% cc pgme1.o pgme2.o ...
Les fichiers objets pgme1.o, pgme2.o ... sont édités pour
(re)créer un exécutable a.out.
[3] Avec option -c
% cc -c pgme.c ...
Le (ou les) fichier(s) source(s) est (sont) compilé(s) pour
générer un (ou des) module(s) objet(s) pgme.o Il n'y a pas de
a.out en sortie.
[4] Avec option -o
% cc -o nomExec ...
Le fichier exécutable a.out est nommé par nomExec. Cette
option est ignorée si l'option -c est présente (il n'y a pas
d'exécutable à générer). Se rappeler qu'il faut une fonction
main() pour former un exécutable.
On peut constater que le (1) correspond à la partie
enchaînement de l'ensemble: préprocesseur,
compilation/assemblage et édition de lien le (2) correspond à la
partie édition de lien uniquement et le(3) correspond à la partie
préprocesseur et compilation/assemblage uniquement.
On peut aussi noter au passage l'existence de la commande
cpp (C PreProcessor) pour effectuer uniquement le passage du
10
Structure Générale d'un Programme C
préprocesseur, et de la commande ld (Link eDitor) pour l'édition
de lien des fichiers .o la génération d'un exécutable.
En Résumé:
COMMANDE RESULTAT
cc pgme.c a.out
cc pgme1.c pgme2.c a.out, pgme1.o, pgme2.o
cc pgme1.o pgme2.o a.out
cc pgme1.c pgme2.o a.out, pgme1.o (parfois)
cc -c pgme.c pgme.o
cc -o exec pgme.o exec
cc -o exec pgme.c exec
Remarque: Il y a parfois un effet de bord, quand un fichier
exécutable est généré et quand on compile simultanément des
fichiers .o et .c. Cet effet est celui de créer, s'ils n'existent pas,
ou de supprimer s'ils existent, les fichiers .o correspondant aux
fichiers .c.
5. Compilation Séparée
Il est très utile —et très conseillé— quand un programme
est assez long, de l'écrire en morceaux compilés séparément en
fichiers .o par la commande cc -c. Fichiers à éditer plus tard par
cc (ou cc -o) pour générer un programme exécutable, si une
fonction main() est présente. D'ailleurs, celle-ci peut se limiter
aux appels nécessaires pour lancer et contrôler l'application, et
être ainsi la dernière écrite.
Les différents morceaux du programme peuvent aussi être
écrits en plusieurs versions chacun, laissant le choix au
programmeur d'éditer les morceaux voulus et de composer
l'application lors de la dernière compilation qui crée l'exécutable
final. Nous y reviendrons plus loin quand on verra l'utilitaire
MAKE, qui permet la recompilation (entre autre) automatique
des programmes et la composition d'une application.
11
Langage C
La compilation séparée répond au besoin de la
programmation modulaire: l'écriture de programmes en plusieurs
morceaux ou modules. C'est un domaine à part entière qui
déborde du cadre de ce cours. Disons simplement La
décomposition modulaire la plus simple c'est d'écrire des sous-
fonctions qui réalisent des tâches plus élémentaires qui
concourent à la réalisation de la fonction globale d'un programme.
Les modules qui en résultent doivent être les plus indépendants
et les plus autonomes possibles, (leurs relations ou
interconnections dans le cadre du programme globale est réduite
au strict nécessaire) et par conséquent pouvant servir pour
d'autres programmes. La tâche de chaque module doit être alors
très bien spécifiée, i.e., définie avec précision. Mis à part qu'il doit
être fortement commenté aussi. Le talent d'un(e)
programmeur(se) se reconnaît à la conception modulaire de ses
programmes.
Remarque: Un module réalisant une certaine tâche ne se limite
pas forcément à une fonction au sens langage de programmation.
Au contraire, On peut envisager des modules à la Parnas (du nom
de D.L. Parnas, pionnier du génie logiciel, qui développa le
principe de la programmation modulaire), c'est à dire constitués
d'un ensemble de fonctions logiquement reliées et manipulant un
ensemble de données (dites ressources) communes déclarées au
sein du même module, et accessibles uniquement en son sein. Ce
qui s'appelle "Information Hiding", ou protections des informa-
tions contre les accès et modifications imprudents et
involontaires. Cette caractéristique, dite aussi encapsulation,
avec d'autres comme l'héritage, est à la base de la programmation
orientée objets, et a été retenue dans le langage C++ sous la
notion de classe.
12
Expressions De Base
CHAPITRE II
EXPRESSIONS DE BASE
A person who is more than casually interested in computers should be
wellschooled in machine language, since it is a fundamental part of a
computer.
-- Donald Knuth
1. Introduction
En C une instruction est une expression suivie de point-
virgule ";"
<expression> ;
";" qui a, en quelque sorte, le sens "évaluer cette expression".
Une expression est une combinaison bien formée5
d'opérandes et d'opérateurs. Les opérandes sont des variables,
constantes ou des appels de fonctions. Les opérateurs sont ceux
classiquement connus. Cependant, on remarquera un opérateur
spécial qui est l'affectation, symbole =. Pour bien comprendre cela
il faut imaginer qu'en C une expression a non seulement une
valeur, celle résultant de son évaluation, et un type, celui hérité
de ses opérandes, mais aussi un effet —de bord en quelque sorte.
Voici des exemples d'expressions/instructions: soit la
déclaration int i;
2 + 3 ; est une expression de type entier évaluée à 5 qui
est son résultat
i + 5 ; idem, expression entière évaluée à i+ 5
5 Sans donner une syntaxe formelle, bien formée veut dire écrite correctement:
2 * (a - b) est bien formée, /2 + )a - b par exemple ne l'est pas.
13
Langage C
i++ ; est une expression qui a pour type et valeur
résultat ceux de i, et pour effet
d'incrémenter i de 1.
i = 2+3; est une expression entière de valeur 5 (2+3) et
qui a pour effet d'affecter le résultat 5 à i.
C'est l'usage le plus fréquent de l'opérateur =,
i.e., instruction d'affectation.
L'expression précédente ayant comme valeur 5 de type int,
l'expression
x = (i = 2 +3);
a pour effet d'affecter 5 à x (en plus de l'affecter à i).
On aura noté que les deux premières écritures sont sans intérêt
(résultat perdu) quoique correctes.
1. Objets de Base de C
1.1. Caractère: char
char est le type d'un caractère quelconque US-ASCII codé
sur un octet. C'est l'objet C le plus élémentaire.
\0 pour caractère "null" 0 (le premier du code ascii)
\a pour son bip BELL code ascii 7 ('\007')
\b pour espace arrière BACKSPACE code ascii 8 ('\010')
\t pour tabulation TAB code ascii 9 ('\011')
\n pour nouvelle ligne LINEFEED code ascii 10 ('\012')
\f pour nouvelle page FORMFEED code ascii 12 ('\014')
\r pour retour chariot RETURN code ascii 13 ('\015')
\" pour double quote ou guillemets "
code ascii 34 ('\042')
\' pour quote ou apostrophe ' code ascii 39 ('\047')
\\ pour le \ lui-même code ascii 92 ('\134')
\ddd pour une constante octale ddd désignant
une valeur ASCII ( 0 =< d <= 7 )
Fig-II-1 Caractères Spéciaux de C
14
Expressions De Base
Exemple:
char c,b;
c='b' ; b='+'; c=b; c='\'';
Les constantes sont entre apostrophe '' comme 'a' ou en
code octal '\134' (valeur ascii en base 8 de \). Le caractère
apostrophe lui-même est introduit par le symbole \ (considéré
méta caractère) comme dans '\''. En quelque sorte, \x signifie
traiter le caractère x spécialement s'il est normal, ou
normalement s'il est spécial. Ainsi '\\' est le caractère normal
barre oblique (backslash) \ et '\n' est le caractère spécial
nouvelle ligne (newline). La figure II-1 donne la liste des
caractères spéciaux (avec leur code ascii en décimal et en octal).
Exercice: Expérimenter printf de \x où x est de votre choix.
Des fonctions utiles sur le type char sont données par la
figure II-2. Ces fonctions retournent 0 pour faux et une valeur
différente de 0 pour vrai. Elle servent essentiellement à connaître
le type de caractère: chiffre, lettre, majuscule etc...
1.2. Entiers: int, short int, long int, unsigned
Les entiers sont du type int pour l'usage courant, short
int (ou short tout court, sans jeu de mots) pour les entiers
codés sur deux octets (16 bits) appartenant à l'intervalle:
[-32768 , 32767], i.e. [-215 , 215 - 1]
et long int (ou long ) pour des entiers sur 4 octets (32 bits)
appartenant à l'intervalle:
[-2147483648 , 2147483647], i.e. [-231 , 231 - 1]
En général int équivaut à long, dans les ordinateurs
récents. On peut utiliser aussi unsigned pour des entiers non
signés, i.e., positifs. Par exemple unsigned short appartient à [0,
65535].
15
Langage C
#include <ctype.h> /* l'entête à mettre */
isalpha(c) c est une lettre
isupper(c) c est une lettre majuscule
islower(c) c est minuscule
isdigit(c) c est chiffre
isalnum(c) c est alphanumérique (lettre ou
chiffre)
isspace(c) c est blanc, tab, retour-chariot,
newline ou formfeed
ispunct(c) c est caractère de ponctuation
isprint(c) c est caractère imprimable
iscntrl(c) c est caractère de contrôle ou delete
isascii(c) c est caractère ASCII (0 <= c <128
Fig-II-2 Fonctions sur le Type char
Exemples de déclarations:
int i; long int j; (ou long j;)
short int j; (ou short j;)
unsigned int k; unsigned short l, m, n;
Exemples de constantes:
1, 25, 189 en notation décimale.
0123, 0717 en notation octale (0 suivi de chiffres à base
8 ).
0xfF 0XfaC en notation hexadécimale (0 suivi de
x ou X suivi de chiffres dans
[0 , 9] ∪ [A , F] ∪ [a , f]).
Exemple de programme:
Le programme de la figure II-3 calcule le pgcd de deux
entiers a et b. Voici les résultats du programme sur les couples
d'entiers (a, b): (18,12) (72,81) (233,144)
16
Expressions De Base
a | b a | b a | b
----------- ----------- -----------
18 | 12 72 | 81 233 | 144
12 | 6 81 | 72 144 | 89
6 | 0 72 | 9 89 | 55
PGCD = 6 9 | 0 55 | 34
PGCD = 9 34 | 21
21 | 13
13 | 8
8 | 5
5 | 3
2 | 1
1 | 0
PGCD = 1
Le dernier jeu d'essai, (233,144) a donné l'itération la
plus longue. En effet ce sont deux termes successifs de la suite
de Fibonacci, comme le montrent les différentes valeurs de a
(exercice: montrer pourquoi).
main (){
int pgcd (int, int);
printf(" PGCD = %4d\n",pgcd(18,12));
printf(" PGCD = %4d\n",pgcd(72,81));
printf(" PGCD = %4d\n",pgcd(233,144));
}
int pgcd(int a, int b) {
/* PGCD de a b par algorithme d'Euclide.
Version itérative.*/
int reste;
printf(" a | b \n"); printf("-----------\n");
printf("%4d | %4d\n",a,b);
reste = b;
while (reste != 0){
/* si a < b, le premier tour de la boucle
permute a et b */
reste = a % b; /* % est le modulo */
a = b;
b = reste;
printf("%4d | %4d\n",a,b);
}
return a;
}
Fig-II-3 Programme PGCD de Deux Entiers.
17
Langage C
1.3. Réels: short float , long float
Ils sont short float (ou float), long float (ou
double). La précision est de 6 ou de 16 chiffres décimaux selon le
cas. Soit 4 ou 8 octets en général.
Exemples de déclarations:
short float delta; (ou float delta;)
long float pi; (ou double pi;)
Exemples de constantes:
3.14 -5.0 5e17 -3E-12 etc...
Remarque: On peut utiliser la fonction sizeof(<type>) ou
sizeof (<expr>) pour connaître la taille d'un objet. Par exemple
sizeof(short) est 2 et sizeof(3.14) est 4.
Exercice: tester sizeof() pour certains types ou expressions.
1.4. Initialisations
On peut initialiser une variable dès sa déclaration comme dans:
int i = 3;
qui déclare i entier et lui donne 3 comme valeur initiale; ou
encore
double pi = 3.1415926535897932384626433;
(16 chiffres seront retenus)
char c = 'c';
18
Expressions De Base
#include <math.h>
int abs (int i); valeur absolue de i
double fabs (double x); valeur absolue de x
réel double
double floor(double x); plus grand entier ≤ x
(partie entière)
double ceil (double x); plus petit entier ≥ x
double exp (double x); e puissance x
double log (double x); le logarithme de x
double log10 (double x); log à base 10 de x
double pow (double x,double y); x puissance y
(power)
double sqrt (double x); racine carré de x
(Square Root)
double sin (double x); sinus de x
/* les autres fonctions trigonométriques sont
* cos, tan, acos, asin, atan, sinh, cosh, tanh
*/
void srand (unsigned germe); initie la fonction
aléatoire rand()
int rand (void) génère une valeur aléatoire (random)
dans [0 , 2^31 - 1]
Le générateur est initialisé par srand(1),
ensuite réinitialisé par srand(i) i quelconque.
Fig-II-4 Fonctions Mathématiques de C.
2. Opérateurs Arithmétiques
C offre les opérateurs arithmétiques classiques à savoir:
+ et - addition et soustraction binaires (ou - unaire)
* et / pour la multiplication et la division. Dans la division
d'entiers la partie décimale est tronquée! Ainsi 5.0/2.0 donne 2.5
et 5/2 donne 2, i.e. le quotient entier.
% est l'opérateur modulo (reste de la division entière) sur les
entiers.
5%2 donne 1.
L'élévation à la puissance n'existe pas. Il faut utiliser la
fonction pow(x, y) (power) pour avoir xy. La figure II-4 liste des
19
Langage C
fonctions mathématiques de C. On doit se rappeler que les
fonctions retournent par défaut un entier (e.g. abs(i)). Il est très
utile de jeter un coup d'œil sur le fichier Unix
/usr/include/math.h pour ces fonctions et d'autres.
L'exemple suivant, est un programme sur des réels. C'est le calcul
d'un nombre réel y par fractions continues.
y = x1 + 1/( x2 + 1/(x3 + 1/(x4 + ... + 1/(xn))))
Il a pour données: n et xi, pour i de n à 1. Le résultat est y, le réel
cherché. Nous l'expérimenterons sur des réels connus. Il donne
pour les valeurs de xi suivantes:
[1; 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
résultat = 1.61805556 = nombre d'or k6
[1; 2, 2, 2, 2, 2, 2, 2]
résultat = 1.41421569 = racine de 2
[2; 1, 2, 1, 1, 4, 1, 1, 6, 1, 1, 8]
résultat = 2.71828184 = nombre e
La notation [x1; x2, ..., xn] représente les termes successifs du
calcul, qu'on commence à l'envers de xn à x1 (rentrer xn, xn-1, ...,
x1).
6 k = (L+l)/L = L/l. L et l sont les dimensions d'un rectangle parfait.
20
Expressions De Base
main()
{ float x;
double y;
/* y est le résultat de chaque itération,
* i.e. le cumul intermédiaire et final
*/
int i,n;
printf("combien de termes?");
scanf("%d",&n);printf("\n");
printf("dernier terme >");
scanf("%f",&x);printf("\n");
y = x;
for (i=1; i < n; i++){
y = 1.0 / y;
printf("nouveau terme >");
scanf("%f",&x);printf("\n");
y = x + y ;
}
printf("résultat = %12.8f\n",y);
}
Fig-II-5 Programme de Calcul de Réels par Fractions Continues.
Exercices:
II.2.1 Faire un programme qui lit et écrit des entiers short
et long, des réels float et double. Utiliser les formats %ld %lf
pour les long int et long float .
II.2.3 Vérifier que
[3; 7, 15, 292, 1, 1, 6, 1, 3] donne π et que
[1; 1, 2, 1, 2, 1, 2, 1, 2] donne √3.
Chercher l'algorithme inverse qui, pour un réel donné,
donne les termes successif de la suite xn, xn-1, ..., x1.
3. Les Tableaux
Les tableaux sont des variables indicées, à une ou plusieurs
dimensions. La notation [] sert à introduire l'indexation.
int t[20]; déclare un tableau de 20 entiers.
21
Langage C
float m[3][5]; déclare une matrice de réels ayant 3
lignes et 5 colonnes.
L'accès à un élément se fait par la notation x[entier],
x[entier][entier]...
t[i], t[5] etc... pour une dimension, avec i dans 0..19
! (t déclaré t[20])
m[2][3] en deux dimension, pour l'élément
de la 3ième ligne 4ième colonne !
En effet les indices de tableau commencent à 0 en C. Un
tableau t[5] contient 5 éléments et il est indicé de 0 à 4. De
même pour toute dimension. On se rendra compte vite de cette
praticité. On verra pourquoi plus loin quand on étudiera les
pointeurs.
remarque importante: Les tableaux en C ne supportent pas
l'opération d'affectation. On ne peut pas affecter en bloc un
tableau à un autre. Le compilateur vérifie cela et n'acceptera pas
que le nom d'un tableau figure en partie gauche d'une affectation.
Si on a
int r[5], s[5];
on ne peut pas faire
r = s;
Il faut le faire dans une boucle par exemple:
for (i=0; i<5; i++)
r[i] = s[i];
Cela est dû au fait qu'un nom de tableau n'est pas une
variable comme les autres qui contient une valeur —qui serait
dans ce cas tout le tableau. Elle contient en fait l'adresse du
tableau et pour cela elle ne doit pas changer sinon on ne peut plus
accéder au tableau. Nous en reparlerons au chapitre IV.
On peut initialiser un tableau, totalement ou partiellement,
à la déclaration.
int t[4] = {1, 2, 3, 0};
On énumère entre {} la liste_virgule des éléments.
float u[5] = {0., 3.1415};
Les deux premiers éléments sont initialisés, les autres étant nuls.
22
Expressions De Base
float mat[3][2] = { {0, 1}, {1, 0}, {2, 5}};
Liste de liste d'initialisations, i.e. listes des lignes.
char s[5] = {'t', 'o', 't', 'o'}; qu'on peut écrire en
bref comme
char s[5] = "toto";
N.B. L'initialisation de tableau n'est valable que si ce dernier est
déclaré static, par exemple:
static char s[5] = {'t', 'o', 't', 'o'};
static char s[5]="toto"; en plus bref.
ou en dehors de toute fonction, dans la partie déclarations de
noms externes (voir §3 Chapitre I).7
4. Les Chaînes de Caractères char* idf ou char
idf[n]
Les chaînes sont des tableaux de caractères, qu'on peut
manipuler par indexation comme un tableau ou à travers un
pointeur (qui pointe vers le premier caractère). Une discussion
plus détaillée sur les pointeurs et les tableaux se trouve plus loin
avec les opérateurs * et & (Voir Chapitre IV). Contentons nous dès
à présent de donner quelques cas simples qui illustrent l'usage
d'un tableau de type char:
char s[20];
Déclare s un tableau de 20 caractères maximum8.
char* ps ;
Déclare ps un pointeur vers (le premier caractère d') une chaîne.
char x[10] = "toto\0";
Déclare une chaîne d'au maximum 10 caractères et l'initialise
avec le mot toto (ou plus exactement, avec une adresse
contenant le mot toto).
7 Contrainte levée en C ANSI
8 En C le débordement de tableaux n'est pas toujours vérifié. C'est au
programmeur(se), généralement averti(e), de faire attention.
23
Langage C
On a x[0]='t' x[1]='o' x[2]='t' x[3]='o' et
x[4]='\0'. Le caractère \0 est, par convention, le terminateur
d'une chaîne. Penser à ne pas l'omettre. Parfois il est rajouté par
le compilateur. En tout cas il faut prévoir sa place dans la taille
du tableau lors de l'initialisation.
Il est plus pratique d'utiliser les chaînes de caractères avec
les pointeurs que comme tableaux. L'affectation
ps = "toto\0";
marche si ps est déclaré char* ps; mais elle n'est pas permise si
ps est déclaré char ps[n]; Un tableau ne supporte pas
d'affectation globale. Il faut procéder élément par élément. Mais
la notation est permise pour initialiser une déclaration tableau de
caractères.
#include <strings.h>
char *strcat (s1, s2) char *s1, *s2;
Concatène s2 au bout de s1. Le résultat est
identique à s1.
char *strncat(s1, s2, n) char *s1, *s2; int n;
idem avec une limite totale égale à n.
char *strcpy(s1, s2) char *s1, *s2;
Copie s2 dans s1 jusqu'au caractère null '\0'
compris.
Résultat identique dans s1.
char *strncpy(s1, s2, n) char *s1, *s2; int n;
idem avec copie de n caractères.
Il y a troncature ou remplissage avec \0 (null).
char *index(s, c) char *s, c;
Retourne la chaîne à partir de 1ère occurrence
de c dans s.
char *rindex(s, c) char *s, c;
idem mais dernière occurrence de c.
int strcmp (s1, s2) char *s1, *s2;
Compare s1 et s2.
résultat = 0 (s1 = s2), <0 (s1 < s2),
>0 (s1 > s2).
int strncmp (s1, s2, n) char *s1, *s2; int n;
compare les n premiers caractères.
int strlen(s) char *s;
Retourne la longueur de s.
Fig-II-6 Fonctions sur les Chaînes de Caractères
24
Expressions De Base
Remarque: Une différence (à ne jamais oublier) existe toutefois
entre une chaîne considérée comme tableau de caractères
(char[]) ou comme pointeur vers caractère (char*): dans le cas
d'un tableau, la chaîne de caractères se trouve dans l'espace
d'adressage normal du programme, et dans le cas d'un pointeur,
la chaîne se trouve dans un espace dynamique, appelé tas (Heap).
Par conséquent, le pointeur en question doit être initialisé pour
lui allouer de la place. Plus exactement, allouer la place à l'objet
pointé et affecter l'adresse de cet objet au pointeur. On utilise
pour cela la fonction C malloc() définie dans #include
<malloc.h>. (Nous détaillons cela dans le Chapitre IV).
Exemple: soit char* ps;
ps = malloc (80); alloue 80 octets pour ps. On peut alors
écrire:
ps = .../* expression chaîne */
Noter au passage que "chaine", constante tableau de
caractères, est une expression à valeur d'adresse . L'écriture:
PointeurChar = "chaine";
alloue un espace pour la chaîne chaine et affecte son adresse
(celle du premier caractère) au pointeur. Ce dernier peut, ainsi, se
passer d'une initialisation préalable par malloc().
L'écriture, inhabituelle:
"chaine"[3]
quand à elle, est le 4e caractère 'i' de la chaîne chaine.
Remarquer aussi que l'expression
"chaine"[3] = 'î' est correcte (que fait-elle et quel est
son type?)
Les fonctions sur chaînes de caractères sont données dans la
figure II-6. La plupart rendent un pointeur sur une chaîne. Les
trois dernières fonctions retournent, on l'aura compris, un entier.
Pour les autres fonctions les paramètres, ainsi que le résultat,
sont des pointeurs sur (le 1er caractère de) la chaîne. Les
paramètres effectifs peuvent être des tableaux de char ou des
pointeurs char*.
25
Langage C
Exemple:
char *x,*y ,*z ;
x="toto\0"; y="titi\0";
z= strcat (x,y);
donnera pour z (et x!) "tototiti\0". Donc on peut se contenter
d'écrire
strcat(x,y);
et récupérer le résultat dans x.
Remarques:
1) On ne peut pas comparer (<, >, ==, <=, >=) deux
chaînes de caractères (les opérateurs de comparaison ne
marchent que pour des objets scalaires). Il faut utiliser la
fonction strcmp().
2) Comme les tableaux en C ne sont pas affectables,
strcpy() peut servir pour affecter un tableau de caractères à
un autre.
3) Toutefois, quand x et y sont des chaînes, x = y; n'est pas
la même chose que strcpy(x, y). Dans un cas, (figure II-7),
x et y designeront le même objet chaîne, celui pointé par y
auparavant, dans l'autre, il y a copie de l'objet. x et y
désigneront deux objets différents —mais ayant la même
valeur. (Exercice le vérifier).
x toto x toto x titi
y titi y titi y titi
(a) (b) (c)
Fig-II-7 Affectation vs Copie
(a) est la situation avant; (b) est la situation après x = y; (c)
est la situation après strcpy(x,y). Noter que dans le cas (b),
le premier espace alloué à x est inutilisable9.
9En termes plus modernes, le cas (c) est appelé deep copy (copie en profondeur).
Le cas (b) est alors une simple affectation.
26
Expressions De Base
5. Des Opérateurs de C
Dans la figure II-8 on peut voir les opérateurs qu'on peut
retenir dès à présent. Ils sont listés par ordre de priorité
décroissante et par groupe de même priorité. L'associativité
signifie qu'en l'absence de parenthèses et pour des opérateurs de
même priorité, l'évaluation d'une expression se fait dans le sens
droite-gauche ( <--- ) ou gauche-droite ( ---> ).
Opérateur Sens Exemple Associativité
& Adresse de &a
* Indirection *p
- - unaire -4, -(b*b)
++ ++i, j++ <---
Incrémentation
-- --i, j--
sizeof() Decrémentation sizeof(i)
Taille de sizeof(float)
* Multiplication 2 * 4
/ Division (réelle) 4.0 / 5 --->
/ Quotient (entier) 9 / 3
% Modulo (reste) 9 % 7
+ Addition 2 + 4 --->
- Soustraction 3 - 5
= Affectation x = 8, x=y=0
op= Changement x += 5 <---
y /= 2
Fig-II-8 Quelques Opérateurs de C
Opérateur =
Comme il a déjà été mentionné, l'affectation est considérée
en C comme un opérateur normal. Quoiqu'à l'usage il sert à écrire
l'instruction d'affectation classique comme dans
delta = b*b - 4*a*c;
Mais il n'est pas rare de voir des écritures comme
x = y = 1;
qui affecte 1 à x et à y ou
27
Langage C
while ( s[i] = s[j] ) ...
qui affecte s[j] à s[i] tout en testant si s[j], valeur de
l'expression, devient nulle... Cette écriture condensée s'est révélée
très pratique, mais il faut s'en méfier (ne pas lire tant que s[i]
égale s[j] ! ).
Opérateur ++
i++ (resp. i--) incrémente (resp. décrémente) i de 1. Mais
la valeur de l'expression i++ est celle de i avant
l'incrémentation. De même pour i--. On parle de post
incrémentation ou post décrémentation.
++i (resp. --i) incrémente (resp. décrémente) i de 1 aussi.
Mais la valeur de l'expression est celle après incrémentation
(resp. décrémentation). Ainsi
i=5;
x=i++; donne 5 pour x et 6 pour i.
x=++i; donne 6 pour x et pour i.
On notera que les deux instructions suivantes sont identiques
(pourquoi?).
i++;
++i;
6. Règles de Conversion
Le résultat d'une expression est en générale de même type
que ses opérandes. Mais il arrive que des fois on combine
plusieurs types dans une même expression. Quel type doit
l'emporter alors?
6.1. Conversion Implicite
Il existe des règles de conversions implicites10 faites par le
compilateur. L'entier l'emporte sur le caractère et le réel
l'emporte sur l'entier.
10 Y faire très attention, c'est parfois source d'erreurs.
28
Expressions De Base
De façon générale, pour chaque opérateur arithmétique
binaire, si deux opérandes sont de type différents, alors il y a
conversion. Dans le sens caractère vers entier vers réel. Plus
exactement:
- S'il existe un opérande de type double ou float alors le
calcul s'effectue en double, et le résultat est de type double.
- Sinon (on n'a pas de réels) s'il existe un opérande de type
long alors calcul en long.
- Sinon s'il existe un opérande unsigned alors calcul en
unsigned.
- Autrement le calcul est en int.
Mais il faut noter aussi qu'avant un calcul, short et char
sont toujours convertis en int, et float est toujours converti en
double.
La conversion se fait aussi lors de l'affectation. la valeur de
la partie droite est convertie vers le type de la partie gauche qui
est aussi celui du résultat final.
Exemples:
float x, y;
x = 7; x devient 7.0, conversion d'affectation
y = x / 2; y devient 3.5 ( 7.0 / 2.0, calcul
effectué en double)
int x, y;
y = 5;
x = y / 2; x devient 2
float x, y;
y = 5 / (2 + 1); y devient 1.0 car (division entière
de 5 par 3 = 1)
x = y + 1; x devient 2.0
float x, y; int i;
y = 3 / 2; y devient 1.0
i = y + 1.5; i devient 2 (troncature de 2.5)
x = i + y; x devient 3.0
29
Langage C
La conversion la plus importante à noter, du point de vue
réversibilité, est celle de char (8 bits) vers int (16 ou 32 bits).
En général c'est une extension du signe. Mais cela dépend de
l'architecture de la machine. Si le caractère a une valeur (ASCII)
< 128, (dernier bit = 0), alors l'entier a la même valeur. Sinon
(valeur ASCII >= 128) le bit de gauche est à 1, et il est recopié à
gauche pour faire un entier négatif ! Valeur ASCII perdue.
La conversion inverse, de int vers char est une simple
troncature des bits d'ordre > 7, qui ne fait pas perdre la valeur
ASCII (entier positif).
int i; char c;
c = 'd';
i = c; extension de signe (positif)
c = i; troncature
La valeur de c n'a pas changé, c = 'd' toujours.
Par contre l'inverse, char vers int, n'est pas toujours juste,
comme le montre l'exemple suivant :
main(){
int i;
char c;
i = 200;
c = i; /* c = ascii(200) */
i = c; /* extension de signe pour i */
printf("%c %d\n",c,i);
i = 100;
c = i;
i = c;
printf("%c %d\n",c,i);
}
qui imprime
H -56 <--- H ou un autre caractère
d 100
Dans la première partie de cet exemple on a
i= 200 = 128 + 64 + 8 = 00000000 11001000 et
30
Expressions De Base
c=11001000 = 100 1000 = 72 = 'H' (si la conversion ne
considère que 7 bits)
i = 11111111 11001000 = -56 (extension de signe)
et dans la deuxième partie
i = 100 = 64 + 32 + 4 =00000000 01100100 et
c = 01100100 = 110 0100 = 'd'
i redevient 00000000 01100100 (extension de signe) donc
100
Sachez aussi que si x est float et i est int, alors x = i
et i = x causent des conversions. float vers int est alors une
troncature de la partie décimale. De même double est converti
vers float par arrondi et troncature de la mantisse, et float est
converti vers double par allongement avec des 0 de la mantisse.
Long int est converti vers short ou char par troncature des
bits de poids fort.
Remarque: Les arguments des fonctions étant des expressions, la
conversion se fait et les valeurs passées à une fonction sont
toujours double ou int selon le cas.
En programmation, il est de bonne discipline de ne pas
laisser trop s'opérer les conversions implicites. Avec float x,
écrire plutôt x = x + 5.0 que x = x + 5 . La compréhension
des programmes est meilleure et on contrôle soi-même ses
instructions.
6.2. Conversion Explicite
C offre aussi un moyen de forcer une conversion en cas de
nécessité. La ligne suivante convertit l'entier 2 en réel float.
(float ) 2
et
int n;
sqrt ( (double) n )
fait passer un flottant double à la fonction de bibliothèque
sqrt() dont l'argument est double. De façon générale on écrit:
(type) expression
31
Langage C
pour convertir le résultat de l'expression vers le type nommé. Par
exemple:
(float ) (i+5)
convertit le résultat de i+5 vers float et
pt = (int*) malloc(sizeof(int));
convertit un pointeur char* , retour de malloc(), vers un
pointeur int* sur entier.
Mais ce type de conversion, appelé cast, ne touche pas à la
valeur de son argument:
(double) n
ne modifie pas la valeur de n. Elle produit la même valeur dans le
type double.
7. Expressions Conditionnelles
On aura remarqué que le type booléen est absent de C.
Qu'importe, dans les tests on emploie des expressions numériques
avec la signification FAUX si le résultat est 0, VRAI sinon (i.e.
différent de 0).
C permet l'utilisation des opérateurs relationnels et logiques
pour former des expressions conditionnelles. Les voici par groupe
de priorité décroissante. Ils s'associent tous de gauche à droite (--
>). Pour certains, la notation n'est pas très classique... Faire
attention surtout au test d'égalité. Il n'est par rare d'écrire par
erreur
if (a=b)
au lieu de
if (a==b)
32
Expressions De Base
Symbole Signification
== égal à (deux
signes =)
!= différent de
> supérieur à
< inférieur à
>= supérieur ou égal à
<= inférieur ou égal à
&& "et" logique
|| "ou" logique
! "non" logique
Exemples:
x == y x égale y
(x + 4) != 3 (x + 4) différent de 3
(a < b) && (b < c) b supérieur à a et inférieur
à c
a < b && b < c même chose car < est
prioritaire sur &&
!b < c (non b) est inférieur à c
!(b < c) b n'est pas inférieur à c
a < b < c a un sens! (quel est-il?)
Une syntaxe particulièrement utile, quoiqu'illisible parfois
est
exp_c ? exp_1 : exp_0
qui signifie: évaluer exp_c et si elle est non nulle alors évaluer
exp_1, qui est alors le résultat, sinon évaluer exp_0 et c'est le
résultat. Exemples:
max = (a > b) ? a : b ;
signifie:
max = si a > b alors a sinon b
On peut imbriquer ces expressions
z = (a > b) ? (a > c) ? a : c : b;
Une expression conditionnelle de ce genre peut être utilisée
partout à la place d'une expression. Si les types de exp_1 et exp_0
33
Langage C
sont différents il y a conversion selon les règles précitées
indifféremment de la valeur du test.
L'exemple suivant imprime les 100 premiers entiers à
raison de dix par ligne, séparés par un espace.
for (i=1; i <= 100 ; i++)
printf("%3d%c", i, (i%10) == 0 ? '\n' : ' ');
Résultat:
1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40
41 42 43 44 45 46 47 48 49 50
51 52 53 54 55 56 57 58 59 60
61 62 63 64 65 66 67 68 69 70
71 72 73 74 75 76 77 78 79 80
81 82 83 84 85 86 87 88 89 90
91 92 93 94 95 96 97 98 99 100
7.1. Opérateurs Logiques sur les Bits.
En C on peut manipuler la configuration binaire d'un objet.
On a les opérateurs:
& ET bit à bit (& binaire)
| OU logique bit à bit
^ OU exclusif bit à bit
<< décalage à gauche
>> décalage à droite
~ complément à un (les 1 deviennent 0 et inversement).
~ est un opérateur Unaire.
Ces opérateurs doivent s'appliquer quand ils ont un sens. Ils n'en
ont pas sur float ou double par exemple.
On peut utiliser & pour masquer des bits comme par
exemple
c = i & 0177 (0177 étant 01 111 111)
qui met à 0 tous les bits de i sauf les 7 bits de poids faible. Ainsi c
devient ASCII
internationalement valide (entre 0 et 127).
34
Expressions De Base
x = x | MASK
met à 1 les bit de x situés vis à vis des 1 de MASK, une constante
donnée.
x & ~077
met à 0 les 6 bits de droite de x (~077 = 11 000 000).
Les opérateurs << et >> servent à faire des décalages de n
bits où n est le deuxième argument.
x << 5 décale x de 5 bits à gauche
x >> 2 décale x de 2 bits à droite
Dans le premier cas, x reçoit des 0 à droite et dans le second cas,
des 0 ou des 1 selon son signe. Mais cela dépend des architectures
machine.
Pour illustrer un usage de ces opérateurs, la figure II-9, est
le code d'une fonction bin(n) qui retournent la configuration
binaire de son argument entier n. Le résultat est une chaîne de
caractères 0 ou 1.
char b[16]; /* tableau résultat */
char *bin(short n) {
/* routine qui convertit n en sa représentation
* binaire b tableau de '0' ou '1' .
*/
short i,j,k;
k = 16;
i = n; /* i variable à décaler */
for(j=0; j<=15; j++){
if ((i % 2) == 0) /* si i est pair */
b[k--]='0'; /* son 1er bit est 0 */
else
b[k--]='1'; /* sinon il est 1 */
/* decaler i de 1 bit à droite */
i >>= 1;
}
return (b);
}
Fig-II-9, Configuration Binaire d'un Entier Court
La séquence:
35
Langage C
int i
char *s;
s = bin(i);
printf("%s\n",s);
donne:
0000000000000000 i = 0
0000000000000100 i = 4
1111111111111111 i = -1
8. Récapitulatif des Opérateurs de C
La table de la figure II-10, extraite du manuel C original
(KERNIGHAN & RITCHIE), récapitule l'ensemble des opérateurs
de C. Elle est donnée dans l'ordre décroissant de priorité des
opérateurs avec leur associativité. Les opérateurs de même
priorité sont sur une même ligne, comme *, / ou % qui sont
prioritaires sur + et -. En l'absence de parenthèses et à priorité
égale, l'associativité indique l'ordre d'évaluation des opérateurs.
Nous y avons inclus aussi l'arité, qui est 1 pour unaire, 2 pour
binaire et 3 pour ternaire.
Certains opérateurs ne sont pas encore étudiés comme -> ou
(sélection de champs de structure, voir Chapitre IV). Les
parenthèses () de la première ligne désignent l'appel de fonction
f(...) aussi bien que le parenthésage des expressions, et les []
de la première ligne, l'indexation t[n]. (<type>) est l'opérateur
de conversion forcée comme (float)3. Le contexte d'apparition
de certains symboles identiques, *, - et &, implique leur
signification:
unaires, * est l'indirection, - est le moins et & est l'adresse;
binaires, * et - désignent respectivement la multiplication et la
soustraction, et & est le ET sur les bits.
36
Expressions De Base
Arité Opérateur Associativité
2 () [] . -> --->
1 ! ~ ++ -- - (<type>) * & <---
sizeof()
2 * / % --->
2 + - --->
2 << >> --->
2 < <= > >= --->
2 == != --->
2 & --->
2 ^ --->
2 | --->
2 && --->
2 || --->
3 ? : <---
2 = += -= etc. <---
2 , --->
Fig-II-10, Table des Opérateurs de C.
Le dernier opérateur , (virgule) sert pour une liste_virgule
d'expressions.
x=3, p++, f(x,p)
qui est une expression évaluée de gauche à droite et dont le
résultat est celui de l'expression la plus à droite, ici f(x,p). Dans
un appel de fonction, il faut parenthéser une telle expression et
écrire par exemple
g (a, (x=3,p++), b)
pour ne pas confondre avec la virgule qui sépare les arguments.
37
Structures De Contrôle
CHAPITRE III
STRUCTURES DE CONTROLES
Testing can show the presense of bugs, but not their absence.
"Tester un programme peut montrer la présence d'erreurs, mais pas leur
absence"
-- Edger W. Dijkstra
Les structures de contrôle permettent de spécifier l'ordre
d'exécution d'un calcul. C offre les structures de contrôle
traditionnelles des langages, à savoir
- le bloc ou la composition d'instructions,
- la conditionnelle qui est if-else,
- le choix entre cas multiples qui est switch-case,
- les itérations qui sont for, while et do-while,
- les ruptures de séquence break, continue et return,
- et le branchement goto (ne polémiquons pas).
1. Blocs d'Instructions
Une expression telle que x = 0, i++ ou f(x, y), devient
instruction si elle est suivie de ";"
x = 0;
i++;
f(x, y);
Les { } sont utilisées pour former un bloc qui groupe
ensembles des instructions afin de créer une instruction
composée.
{ w = a;
a = b;
b = w;
}
39
Langage C
Il n'y a donc pas ; après }. On peut rajouter éventuellement des
déclarations locales au bloc comme
{ int w; w = a; a = b; ...},
où w n'a qu'une portée locale au bloc. Ces déclarations locales
doivent se trouver en début de bloc.
L'intérêt de l'instruction composée est évident : corps de
fonctions, bloc if, while, for etc., car il n'y a pas ENDIF,
ENDWHILE ou ENDFOR ...
Dans tout ce qui suit, <instruction> sera l'une des formes
suivantes (les crochets [] entourent une clause optionnelle) :
- <expressions> ;
- { /* bloc */
[<liste_declararations>]
<liste_instructions>
}
- if ...
- switch ...
- while ...
- for ...
- do... while
- break ;
- continue ;
- goto label ;
- return ;
- return ( <expression> );
2. La Conditionnelle if-else
C'est la signification classique. Attention Il n'y a pas le mot
THEN!11
11 Les inconditionnel(le)s du then, peuvent le mettre à condition toutefois de placer
en tête du programme la macro "#define then ", qui remplace then par rien. Il
sera donc ignoré.
40
Structures De Contrôle
if ( <expression> )
<instruction_1>
[ else
<instruction_2> ]
La partie else est bien sûr optionnelle. Remarquer la présence
obligatoire des parenthèses de l'expression de test.
L'expression est évaluée et si elle est différente de zéro (c'est
la condition vraie) la partie <instruction_1> est exécutée.
Sinon, l'expression donne zéro (condition fausse), la partie
<instruction_2> est exécutée si elle est présente.
Exemples:
- if ( n != 0)
moyenne = somme / n;
- if ( n )
moyenne = somme / n;
else
printf("Je refuse de diviser par zero\n");
- if ( delta ) {
x1 = ... ;
x2 = ... ;
}
else
printf("... ");
Remarquer l'usage d'un bloc {} dans la partie vraie de if qui
contient plusieurs instructions.
Attention à l'ambiguïté du else (source d'erreur!)
if (n > 0)
if (a > b)
z = a;
else
z = b;
Ici le else se rapporte au deuxième if, le plus interne, comme
l'illustre l'indentation. Pour le rapporter au premier if, il faut
utiliser les accolades {} qui mettent le deuxième if dans un bloc.
41
Langage C
if (n > 0) {
if (a > b)
z = a;
}
else
z = b;
Une autre bonne habitude consisterait à mettre systémati-
quement un else, suivi de ";" s'il est vide.
if (n > 0)
if (a > b)
z = a;
else;
else
z = b;
Par ailleurs, pour séparer la partie else qui contient plusieurs
instructions de la suite du programme, on utilisera aussi un bloc.
if (...)
...
else {
...
}
if en cascade:
C'est une série de tests. Il est commode de les écrire comme
if (...)
...
else if (...)
...
else if (...)
...
else if (...)
.
.
.
else
...
Cela permet de ne pas atteindre des niveaux d'indentation
impossibles. Chaque else se rapporte au dernier if mais n'est
pas aligné sur lui.
42
Structures De Contrôle
3. Choix Multiple switch
switch (i) {
case 1 : printf("...");
break;
case 3 : printf("...");
break;
case 0 : printf("...");
break;
case 23 : printf("...");
break;
default : printf("...");
break;
}
Fig III-1 Exemple de switch avec tous les cas et break
Le switch, correspondant de CASE de PASCAL, permet
d'évaluer une expression entière (ou caractère convertie en entier)
et selon la valeur du résultat exécuter une partie donnée du
programme.
switch ( <expresionEntiere> ) {
case <constanteEntiere_1> : <choix_1>
case <constanteEntiere_2> : <choix_2>
...
case <constanteEntiere_n> : <choix_n>
[ default : <choix_autre> ]
}
Chaque choix est vide, ou est de la forme <instruction> suivie
éventuellement de break; pour arrêter l'exécution des autres cas.
Les <constanteEntiere_i> peuvent être constante ou
expression constante entière ou caractère. En tout cas elles
doivent être différentes.
L'expression est évaluée et est comparée à toutes les
constantes. Dès que sa valeur est égale à une de ces constantes,
l'exécution reprend au choix correspondant, et se poursuit sur les
choix suivants s'il y' en a. Sinon c'est le choix autre , default, qui
est sélectionné quand il est présent.
43
Langage C
La figure III-1 est un exemple général de l'usage de switch
qui, pour chaque valeur de i dans {0, 1, 3, 23}, fait un traitement,
e.g. printf("...") ici, et quitte switch avec break. Si i n'est
pas dans l'ensemble énuméré la partie default est exécutée.
Attention, les choix case sont comme des labels (étiquettes)
qui montrent le début d'exécution. Le break n'est pas toujours
nécessaire mais il permet de terminer le choix en cours. La figure
III-2 montre un exemple où un choix case est considéré comme
une étiquette. Si le choix sélectionné est vide c'est le premier
rencontré qui est exécuté. Ici par exemple, c'est le cas de
printf("mention Bien\n") qui est exécuté si note est égale à
17; de même pour 16 ou 15. Les case ne sont donc pas comme en
PASCAL. En outre ils ne permettent pas les cas intervalle.
switch (note) {
case 20 :
case 19 :
case 18 : printf("Mention Très Bien\n");
break;
case 17 :
case 16 :
case 15 : printf("Mention Bien\n");
break;
case 14 :
case 13 :
case 12 : printf("Mention Assez Bien\n");
break;
default : printf(" Mention Passable\n");
break;
}
Fig III-2 Exemple de switch avec des cas vides.
4. Itérations while, for, do-while
Il y a trois formes d'itération et nous en avons déjà
rencontrées dans nos exemples. while pour répéter tant que une
expression est vrai, for pour la répétition, un nombre connu de
fois, avec variable de contrôle et do ...while pour répéter Jusqu'à
ce qu'une expression devienne fausse.
44
Structures De Contrôle
4.1. Boucle while
C'est l'instruction d'itération while classique.
while ( <expression > )
< instruction >
Tant que l'expression <expression> est évaluée à vrai,
l'exécution de <instruction> est répétée. La figure III-3 est un
exemple de recherche dichotomique dans un tableau trié, entre
les bornes bas et haut. La variable rang est le résultat de la
recherche: indice de l'élément sinon -1. Remarquer l'usage du
break pour quitter prématurément la boucle quand on a trouvé
l'élément.
4.2. Boucle for
L'instruction for de C ne ressemble pas aux habitudes des
autres langages. Elle s'écrit
for ( [<expr_1>]; [<expr_2>]; [<expr_3>])
<instruction>
et signifie (exprimé en while)
<expr_1>;
while ( <expr_2> ) {
<instruction>
<expr_3>;
}
Généralement, <expr_1> est l'initialisation de la variable de
contrôle, <expr_2> une expression logique qui est la condition
d'arrêt de la boucle (en devenant fausse) et <expr_3> une
incrémentation de la variable de contrôle («finalisation»). C'est
assez fort (sic) comme moyen d'expression d'itération .
45
Langage C
/* trouver x dans t[bas .. haut] trié*/
rang = -1;
while (bas <= haut) {
milieu = (bas + haut) / 2;
if (x < t[milieu])
haut = milieu - 1;
else if (x > t[milieu])
bas = milieu + 1;
else { /* trouvé */
rang = milieu;
break;
}
}
Fig III-3 Recherche Dichotomique dans un tableau trié
Exemple:
/* imprimer les éléments d'un tableau t[bas , haut]
*/
for (i = bas; i <= haut; i++)
printf("%d\n", t[i]);
Pour les mordu(e)s des programmes C condensés, c'est équi-
valent à
for (i = bas; i <= haut; printf("%d\n", t[i++])) ;
où la dernière expression groupe l'incrémentation de i avec
l'instruction printf..., corps de la boucle. Remarquer la
présence de ; en fin de for ici. C'est pour dire qu'il n'y a pas de
corps de boucle car sinon, c'est la mauvaise instruction (la
prochaine du programme) qui sera répétée..! Encore une source
d'erreur pour les amateurs(trices) de programmes condensés.
Une des expressions <expr_1> ou <expr_3> peut manquer.
La même itération peut s'écrire sans <expr_3>:
for (i = bas; i <= haut;)
printf("%d\n", t[i++]);
46
Structures De Contrôle
Si <expr_1> et <expr_3> manquent, c'est une boucle sans
variable de contrôle qui ressemble à while. Sa condition d'arrêt
est <expr_2> (fausse) .
Si <expr_2> manque, elle est considérée toujours vrai. Ainsi
for ( ; ; )
est une boucle sans fin, qu'on ne peut quitter qu'avec break,
return, goto ou ... une bogue.
4.3. Boucle do...while
Ce type d'itération se distingue du WHILE normal par le fait
que le test d'arrêt se fait après un passage dans le corps de la
boucle.
do
<instruction>
while ( <expression> );
veut dire répéter l'instruction tant que l'expression est vrai (ou, en
réalité, jusqu'à ce que l'expression devienne fausse). C'est le
même comportement, mais plus commode à écrire, que
<instruction>
while (<expression>)
<instruction>
où <instruction> est la même dans les deux lignes.
Remarque: do ... while en C est comme REPEAT ... UNTIL de
certains langages avec la condition inversée.
Exemple: Faire la somme des N premiers entiers naturels
s = i = 0;
do {
s += i++;
} while ( i <= N );
Noter le ; après while. Quoique cela ne soit pas nécessaire , on
peut utiliser les {} pour la lisibilité et afin de ne pas prendre
while pour un début de boucle TANTQUE.
47
Langage C
A propos de l'usage des accolades pour la lisibilité, une
bonne habitude veut qu'elles soient alignées verticalement pour
délimiter leur bloc.
{
...
}
Mais dans les cas des structures de contrôles, il est peut être
préférable12 d'aligner l'accolade fermante } sur le mot clé qui
débute la structure de contrôle nécessitant un bloc { }.
L'accolade ouvrante { venant sur la même ligne que ce mot clé.
Ecrire
if() { while () { switch () {
... ... ...
} } }
plutôt que
if() while () switch ()
{ { {
... ... ...
} } }
L'accolade } jouant alors le rôle d'un ENDIF, ENDWHILE etc... et
on gagne une ligne.
Exercice: Ecrire des blocs d'instructions syntaxiquement correctes
(comprenant des if, while, for, switch ...) et leur faire une
indentation par l'utilitaire Unix indent.
5. Les Ruptures de Contrôle
5.1. Break
L'instruction break est un moyen commode pour quitter un
switch ou une boucle for, while, do while de façon prématurée.
Il évite d'avoir recours à un booléen comme trouve dans
TANTQUE ( <condition> ET NON trouve) ) ou à goto pour
arrêter une boucle ou quitter un switch. Mais break ne quitte
que la boucle (ou le switch) le plus interne.
12 Oui c'est subjectif. Question de style.
48
Structures De Contrôle
while (...){
...
while (...) {
...
break;
}
/* suite premier while */
}
Après break le contrôle se poursuit à la ligne marquée /* suite
premier while */.
Exemple:
/* ajouter les n premiers entiers jusqu'à atteindre ou
dépasser 1000 */
s = 0;
for (i = 1; ; i++) {
s += i;
if (s >= 1000)
break;
}
Exercice: Vérifier, pour les fanatiques encore du code condensé,
que les trois instructions suivantes font la même chose que ce
dernier exemple.
for (s = i = 0; ; i++)
if ((s += i) > 1000) break;
for (s = i = 0; s < 1000; ) s += i++;
for (s = i = 0; s < 1000; s += i++) ;
Quelle est la valeur de s et de i dans chaque cas?13
5.2. Continue
Comme break, l'instruction continue est utilisée dans une
boucle. Seulement elle sert à arrêter l'itération courante pour
continuer la même boucle à la prochaine itération.
13 Réponse: s = 1035 dans les 4 cas, i = 45 dans les deux premiers cas et 46
(pourquoi?) dans les deux derniers cas. (1035 = (45*46) / 2)
49
Langage C
Exemple:
/* Compter les entiers positifs d'un tableau
(sauter les négatifs) */
positif = 0;
for (i = 0; i <= max; i++) {
if ( t[i] < 0 )
continue;
positif++;
}
Si t[i] est inférieur à 0, on passe à l'itération suivante, sinon on
incrémente positif de 1. continue est surtout utile pour la
présentation de texte de programmes pour économiser des
indentations dans les niveaux d'imbrications. En inversant le test
ci-dessus, la partie positif++ aurait été indentée d'un pas.
5.3. Return
return; ou return (<expression>); est l'instruction qui
arrête la fonction et renvoie à la fonction appelante. Le cas
échéant, la valeur de l'expression <expression> est la résultat
de la fonction. Exemple:
float f(){
float x;
/* ...x= ...*/
return x;
}
5.4. Goto goto <label>
L'instruction goto n'est en fait jamais nécessaire. Elle a été
jugée mauvaise (harmfull) par Edger W. Dijkstra, un des
fondateurs des concepts de la programmation structurée. Mais, et
comme l'a souligné Donald E. Knuth, on peut faire de la
programmation structurée avec goto qui est utile dans certains
algorithmes pour des cas, e.g. cas d'exception, où on aimerait
arrêter le processus en cours. Surtout dans les boucles. break ne
peut être utilisé puisqu'il ne quitte que la boucle la plus interne.
D'où l'usage de goto qui s'utilise comme
goto <label>;
50
Structures De Contrôle
où <label> est un identificateur. Le contrôle est transféré vers
l'instruction étiquetée par le label <label>
<label> : <instruction>
Cette dernière écriture joue aussi le rôle de déclarateur du label.
Exemple:
/* Recherche x dans une matrice t[m][n] */
for ( i = 0; i < m; i++)
for ( j = 0; j < n; j++)
if (t[i][j] == x)
goto trouve;
/* partie non trouvé */
trouve:
/* c'est t[i][j] ... si toutefois i < m */
Voici l'équivalent sans goto mais avec une variable logique
trouve
trouve = 0;
for ( i = 0; i < m && !trouve; i++)
for ( j = 0; j < n && !trouve; j++)
if (t[i][j] == x)
trouve = 1;
if (trouve)
/* c'est t[i-1][j-1] ! */
else
/* cas non trouvé */
On peut débattre longtemps sur la meilleure écriture.
L'instruction goto figure dans la plupart des langages et souvent
pour des raisons de complétude que pour autre chose.
Exercice: Que fait le premier programme ci-dessus (celui avec
goto) si on a utilisé break au lieu de goto?
6. Exemples de Programmes
Pour terminer ce chapitre, on propose un assortiment de
programmes C, assez classiques du reste, pour se familiariser
avec la syntaxe C. Il est très utile de refaire ces programmes sur
machine, et d'essayer de les varier, les augmenter etc...
51
Langage C
- Un premier programme (dans KERNIGHAN & RITCHIE)
est celui qui convertit une température Fahrenait en degré
Celsius.
- Un second, est un programme (adapté de Richard
MOORE), qui calcule les amortissements d'un prêt bancaire avec
taux d'intérêt. Il calcule et imprime le montant des mensualités
et l'échéancier des sommes restant à rembourser: Principal et
Intérêts.
- Un troisième programme est le triangle de Pascal, avec
une variante pour visualiser les emplacements des nombres pairs
et impairs du triangle (structure scalante ou fractale)
- Deux autres programmes font le tri d'un tableau selon les
méthodes: tri par insertion, tri par sélection. Algorithmes en
O(n2). Il y a de meilleurs algorithmes de tri ( O(n.log(n)) ), mais là
n'est pas notre souci.
- Les deux programmes qui viennent ensuite, parcours de
tableau aussi, représentent des algorithmes très intéressants. Le
premier d'entre eux est une solution du problème du Drapeau
Hollandais de E. W. Dijkstra: un tableau contient n éléments
ayant chacun une couleur, valeur de l'élément. Il y a en tout trois
couleurs: Rouge (ou 1), Bleu (ou 2), Blanc (ou 3). D'où le nom de
drapeau hollandais. Il s'agit, en balayant une seule fois le
tableau, de mettre les couleurs dans l'ordre: Rouge d'abord, Bleu
ensuite et Blanc enfin. Le problème peut se réduire à trier un
tableau ne contenant que trois valeurs différentes. L'autre
problème, dit problème du plateau (dû à Michael Griffith),
consiste à chercher dans un tableau d'entiers rangés en ordre
croissant, l'élément le plus souvent répété. Le tableau est
considéré comme une montée en montagne (valeurs croissantes),
entrecoupée par des espaces plats (valeurs répétées). D'où le nom.
Une variante un peu compliquée de ce problème (due à Jacques
Arsac) consiste à chercher dans un tableau d'entiers quelconques,
les indices i et j tels que la sommes des éléments entre i et j soit
maximum, quelque soit i et j. Balayer une seule fois le tableau
évidemment.
52
Structures De Contrôle
- Enfin un dernier programme «last but not least», un peu
insolite, est un programme qui a pour résultat lui-même (qui
imprime son texte source).
53
Langage C
Programme 1:
Conversion de températures en degrés Fahrenait vers
degrés Celcius.
#include <stdio.h>
float celcius(float);
main(){
/* Programme qui imprime la conversion des degrés
* Fahrenait (de 0 a 300 par pas de 20) en Celcius
*/
int bas, haut, pas;
float fahr,c;
bas = 0;
haut = 200;
pas = 20;
fahr = bas;
printf("Fahrenait Celcius\n\n");
while(fahr <= haut){
c = celcius (fahr);
printf("%8.0f %8.1f \n",fahr,c);
fahr += pas;
}
}
float celcius(float x) {
return (5.0/9.0)*(x-32.0);
}
Résultat:
Fahrenait Celcius
0 -17.8
20 -6.7
40 4.4
60 15.6
80 26.7
100 37.8
120 48.9
140 60.0
160 71.1
180 82.2
200 93.3
54
Structures De Contrôle
Programme 2:
Calcul des amortissements d'un prêt bancaire. La banque
vous prête un montant de M en dh, au taux t% annuel et pour
une durée d en mois. Vous rembourser mensuellement une
certaine somme fixe v, calculée comme étant composée d'un
principal: la partie affectée au montant M, et d'un intérêt: partie
couvrant les intérêts.
La formule (un banquier vous la fournira —s'il la connaît)
de calcul du montant d'une mensualité est:
v = tm . M / (1 - (1 + tm) -d) (tm ≠ 0)
où tm est le taux d'intérêt mensuel, i.e. t /12.
#include <math.h>
#include <stdio.h>
main(){
/* programme qui calcule les amortissements d'un prêt.
* Entrees: Montant du Prêt, Taux Interet annuel et
* Duree du prêt (en mois).
* Sortie: Montant Versements mensuels,
* Tableau des amortissements:
* Mois, Solde(Debit), Cumul,
* Verse(Credit), Cumul Interet et Cumul Principal
* ( CumulVerse = CumulInteret + CumulPrincipal)
*/
int i, duree;
double mtPret,tauxMensuel,tauxAnnuel;
double solde,credit,cumulInteret,cumulPrincipal;
double mensualite,principal,interet;
printf("montant du prêt? \n");
scanf("%lf",&mtPret);
printf("taux interet annuel en pourcentage? \n");
scanf("%lf",&tauxAnnuel);
printf("nombre de mois? \n");
scanf("%d",&duree);
tauxMensuel = (tauxAnnuel / 100.)/ 12.;
mensualite = tauxMensuel *
mtPret / (1. - pow(1. + tauxMensuel , (double) -duree ));
printf("\nMontant du prêt = %12.2f DH\n",mtPret);
printf("taux annuel = %4.2f pour cent (%0.3f mensuel)\n",
tauxAnnuel,tauxMensuel);
printf("\n Soit: %d MENSUALITE DE %12.2f DH\n",duree,mensualite);
cumulPrincipal = cumulInteret = interet = 0.0;
solde = mtPret;
55
Langage C
credit = cumulPrincipal + cumulInteret;
printf("\n");
printf("| ECHEANCE | SOLDE | CREDIT | INTERET | PRINCIPAL |\n");
printf("|==========|=========|===========|==========|===========|\n");
/* Calcul et impression des échéances */
for (i=0 ; i <= duree; i++){
printf("| [%2d] |",i);
printf("%12.2f |",solde);
printf("%12.2f |",credit);
printf("%12.2f |",cumulInteret);
printf("%12.2f |\n",cumulPrincipal);
interet = tauxMensuel * solde ;
principal = mensualite - interet;
cumulInteret += interet; cumulPrincipal += principal;
credit = cumulPrincipal + cumulInteret; /*total verse */
solde += interet; /* On rajoute l'intérêt */
solde -= mensualite; /* et on enlève une mensualite */
}
}
Résultat:
% a.out
montant du prêt?
12000
taux interet annuel en pourcentage?
10
nombre de mois?
12
Montant du prêt = 12000.00 DH
taux annuel = 10.00 pour cent (0.008 mensuel)
Soit: 12 MENSUALITE DE 1054.99 DH
| ECHEANCE | SOLDE | CREDIT | INTERET | PRINCIPAL |
|==========|=============|=============|=============|=============|
| [ 0] | 12000.00 | 0.00 | 0.00 | 0.00 |
| [ 1] | 11045.01 | 1054.99 | 100.00 | 954.99 |
| [ 2] | 10082.06 | 2109.98 | 192.04 | 1917.94 |
| [ 3] | 9111.09 | 3164.97 | 276.06 | 2888.91 |
| [ 4] | 8132.02 | 4219.96 | 351.98 | 3867.98 |
| [ 5] | 7144.80 | 5274.95 | 419.75 | 4855.20 |
| [ 6] | 6149.35 | 6329.94 | 479.29 | 5850.65 |
| [ 7] | 5145.60 | 7384.93 | 530.54 | 6854.40 |
| [ 8] | 4133.49 | 8439.93 | 573.42 | 7866.51 |
| [ 9] | 3112.95 | 9494.92 | 607.86 | 8887.05 |
| [10] | 2083.90 | 10549.91 | 633.80 | 9916.10 |
| [11] | 1046.27 | 11604.90 | 651.17 | 10953.73 |
| [12] | 0.00 | 12659.89 | 659.89 | 12000.00 |
56
Structures De Contrôle
Les 3 dernières colonnes représentent des cumuls. Faire la
différence avec les valeurs précédentes dans la même colonnes
pour avoir la décomposition (interet, principal) de chaque men-
sualité.
Constater que le montant des intérêts (100, 92.04, ...) diminue au
fur et à mesure, et que celui du principal (954.99, 962.95, ...)
augmente de façon à ce que leur somme soit constante (c'est la
mensualité à verser).
Exercice: Variante du programme. Le client dispose d'un délai de
3 mois (ou n mois) avant de commencer à effectuer les
remboursements. On compte donc les intérêts afférents à ce délai.
Adapter le programme précédent pour cela (Il n'y a pas
d'amortissements durant cette période).
57
Langage C
Programme 3: Le triangle de Pascal.
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
1 8 28 56 70 56 28 8 1
1 9 36 84 126 126 84 36 9 1
1 10 45 120 210 252 210 120 45 10 1
1 11 55 165 330 462 462 330 165 55 11 1
Le triangle de Pascal
La ligne i est obtenue à partir de la ligne i-1 par:
Ligne i ... a b c ...
Ligne i+1 ... a+b b+c ...
La méthode consiste à utiliser 2 tableaux t et s de taille MAXC
impair: un tableau représente la dernière ligne calculée (et
imprimée), et l'autre, la nouvelle ligne à calculer et imprimer.
Chaque tableau contient les nombres de Pascal (coefficients d'un
polynôme de Newton) centrés, séparés et complétés par des 0, qui
deviendront des espaces à l'impression (c'est l'astuce pour avoir
une triangle isocèle). Exemple:
t = 0 0 0 0 1 0 3 0 3 0 1 0 0 0 0
Le calcul de la prochaine ligne est facile:
SI t[i] = 0 ALORS
s[i] = t[i-1] + t[i+1]
Soit:
58
Structures De Contrôle
t = 0 0 0 0 1 0 3 0 3 0 1 0 0 0 0
s = 0 0 0 1 0 4 0 6 0 4 0 1 0 0 0
Les deux premières lignes sont initialisées telles que:
t = 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0
s = 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0
A l'impression d'une ligne, on n'imprime pas les 0. La boucle
du programme consiste donc à répéter:
- Imprimer t
- Calculer s
- Imprimer s
- Calculer t
MAXL/2 fois (2 impressions par boucle), où MAXL est le nombre de
lignes à sortir.
Les deux premières lignes sont calculées et imprimées, en
dehors de la boucle. Soit le programme:
#define MAXL 10
#define MAXC 31
int t[MAXC], s[MAXC];
void newtab( int [], int []);
void impr( int []);
main()
{
/* On initialise les deux premières lignes
* (une valeur toutes les deux cases)
* et on les imprime
*/
int i,j;
j = MAXC / 2;
t[j]=1;
impr(t);
t[j-1]=1;
t[j]=0;
t[j+1]=1;
impr(t)
/* On commence le calcul */
for(i=0; i<(MAXL/2); i++){
newtab(t,s);
impr (s);
newtab(s,t);
impr (t);
59
Langage C
}
}
void newtab( int x[],int y[]) {
/* Copy x sur y en effectuant les additions qu'il faut
*/
int i;
for (i=0;i<MAXC;i++)
if (x[i]==0) y[i] = x[i-1] + x[i+1];
}
void impr(int x[]) {
/* imprime tableau x (3 espaces pour les 0)*/
int i;
for (i=0; i<MAXC;i++)
(x[i]==0)? printf(" ") :
printf("%3d",x[i]) ;
printf("\n");
}
Remarque: Voici le même triangle, en faisant apparaître la
répartitions des nombres pairs (*) et impairs (espace) (d'après
Ian Stewart, revue Pour la Science, Mars 93). On obtient ce beau
dessin bien structuré comme une image fractale (le tout
ressemble à la partie).
60
Structures De Contrôle
*
* *
* *
* * * *
* *
* * * *
* * * *
* * * * * * * *
* *
* * * *
* * * *
* * * * * * * *
* * * *
* * * * * * * *
* * * * * * * *
* * * * * * * * * * * * * * * *
* *
* * * *
* * * *
* * * * * * * *
* * * *
* * * * * * * *
* * * * * * * *
* * * * * * * * * * * * * * * *
* * * *
* * * * * * * *
* * * * * * * *
* * * * * * * * * * * * * * * *
* * * * * * * *
* * * * * * * * * * * * * * * *
* * * * * * * * * * * * * * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
61
Langage C
Programme 3 et 4: Tri
Les programmes qui suivent illustrent, entre autre, la
notion d'invariant de boucle. Une boucle fait passer un pro-
gramme d'un état initial I à un état final F, par répétition d'un
traitement, qui le fait passer par différents états intermédiaires
Ei. Chaque itération fait passer d'un état intermédiaire à un
autre, en partant de l'état initial I jusqu'à l'état final F. Dans un
programme itératif, la règle d'or est
Pour écrire le corps d'une boucle, on suppose le travail
fait à moitié (on est à l'état Ei) et on se demande comment
passer à l'état Ei+1.
Par exemple, pour trier un tableau t de taille N, on suppose
que sa première partie, jusqu'à l'indice i non compris, est déjà
triée. On examine le ième élément. Ou bien on l'insère à sa bonne
place dans la partie triée (tri par insertion), ou bien on le
remplace par le plus petit élément de la partie non triée restante
(tri par sélection). En tout cas, on passe d'un état (itération) à un
autre en respectant la propriété:
P = "t est trié jusqu'à i"
Quand i atteindra la fin du tableau (i=N), le travail est fait.
Une propriété comme P ci-dessus, s'appelle un invariant de
boucle. C'est une propriété invariante durant la boucle. Ainsi,
cette propriété P est vérifiée
A l'état initial I, pour i = 0 tableau non trié
A l'état final F, pour i = N tableau trié
Aux états intermédiaires Ei, pour 0 < i < N tableau à
moitié trié
Un fois trouvé l'invariant (pas toujours facile), il suffit de
trouver l'initialisation de i, et la condition d'arrêt de la boucle.
62
Structures De Contrôle
#include <stdio.h>
#define N 10
int t[N] = { 2, -44, 1, 0, -5, 9, 12, 4, 1, 3};
main() {
/* Algorithme Tri Par Sélection.
* Invariant de boucle:
* t [ <-- trie --> | <-- Vrac --> ]
* i k
* sélectionner t[k] = min(Vrac);
* Permuter t[i] et t[k] et avancer dans i
* i dans [0..N-1], k dans [i+1.. N-1]
* pout tout i et tout k
*/
int i, j, k;
int min, w;
for (i=0; i<N; i++){
k = i;
/* On suppose que t[k] EST t[i]
* et on cherche plus petit
*/
for(j=i+1; j<N; j++)
if ( t[j]<t[k])
k = j;
/* On permute t[k] et t[i] */
w=t[k];
t[k]=t[i];
t[i]=w;
}
for (i=0; i<N; i++)
printf("%2d\n", t[i]);
}
Tri d'un Tableau par Sélection
Résultat: (Sans les sauts à la ligne)
-44 -5 0 1 1 2 3 4 9 12
63
Langage C
#include <stdio.h>
#define N 10
int t[N] = { 2, -4, 1, 0, -5, 9, 12, 4, 1, 3};
main() {
/* Algorithme Tri Par Insertion
* Invariant de boucle:
* t [ <-- trie --> | <-- Vrac --> ]
* k i
*
* On insère t[i] a la bonne place k dans t[<trie>]
* i dans [1..N-1], k dans [0..i-1]
*/
int i, k;
int w, aCaser;
for (i=1; i<N; i++){
aCaser = t[i];
k = i-1;
/* On permute t[k] et t[k+1] (aCaser)
* jusqu'à k<0 ou t[k] <= aCaser
*/
while (t[k]>aCaser && k>=0){
w=t[k];
t[k]=t[k+1];
t[k-- +1]=w;
}
}
for (i=0; i<N; i++)
printf("%2d\n", t[i]);
}
Tri d'un Tableau par Insertion .
Résultat: (Sans les sauts à la ligne)
-5 -4 0 1 1 2 3 4 9 12
64
Structures De Contrôle
Programme 5: Le problème du drapeau Hollandais:
#define N 12
int t[N] = { 2, 2, 2, 3, 3, 2, 2, 1, 3, 3, 1, 1};
main(){
/* Programme Drapeau Hollandais:
* Dans t, mettre d'abord les 1, ensuite les 2 enfin les 3
*
* Invariant de boucle:
* t[ 111112222???????333333 ]
* i j k
* Jusqu'à i c'est les 1. Ensuite les 2 (jusqu'à j-1).
* Élément t[j] non encore examine.
* De k a la fin c'est les 3.
* Arrêt j=k i.e. partie ??? vide.
*/
int i, j, k;
int w;
i= -1; /* Avant t */
j=0; /* 1er élément */
k=N; /* Au delà de t */
/* j = k Il ne reste plus de ? donc fini CQFD */
while ( j<k ) {
/* On examine t[j] */
switch (t[j]){
case 1: /* on le met avec les 1
i.e. on permute avec t[i+1] */
w=t[i+1];
t[i+1]=t[j];
t[j]=w;
i++; j++;/* On avance i et j */
break;
case 2: /* t[j] a la bonne place */
j++;
break;
case 3: /* on le met avec les 3
* i.e. on permute avec t[k-1]
* Mais on n'incremente pas j ! */
w=t[k-1];
t[k-1]=t[j];
t[j]=w;
k--; /* On recule k */
break;
} /* switch */
}
for (w=0; w<N; w++)
printf("%d ",t[w]);
65
Langage C
printf("\n");
}
Résultat:
1 1 1 2 2 2 2 2 3 3 3 3
66
Structures De Contrôle
Programme 6 et 7: Le problème du plateau
C'est un problème assez classique qui illustre bien la notion
d'invariant de boucle et de manière générale comment écrire un
programme de façon raisonnée.
Programme 6.
Chercher dans un tableau t (ou vecteur) l'élément le plus souvent
répété. On supposera le tableau trié pour que les éléments égaux
soient consécutifs.
Pour trouver l'algorithme, imaginons une situation générale
possible en faisant l'hypothèse que le travail est fait à moitié: en
l'occurrence, on a parcouru une partie du tableau jusqu'à i non
compris. Dans cette partie, c'est x l'élément répété nb fois et c'est
un maximum de fois. Si i devient supérieur à taille du tableau, le
travail est fait et on arrête. Si i = 0, alors nb est nul et cela
répond à la question (Il n'y a pas d'éléments dans cette partie
vide). Maintenant, sinon, examinons si l'élément t[i] est de
nature à changer la situation. C'est le cas s'il est répété nb+1 fois,
c'est a dire si t[i] = t[i-nb]. Auquel cas x devient t[i], nb est
augmenté de 1 et on passe à l'itération suivante. D'où le
programme
#define N 15
int t[N]={
1,1,3,3,3,4,4,4,5,5,6,6,6,6,7};
main(){
/*
* Problème du Plateau:
* Chercher l'élément le plus souvent répété
* dans un tableau
*/
int i;
int nb, x; /* nb est le nombre d'occurrences de
l'élément x le plus souvent répété */
nb = 0;
for (i=0; i<N; i++) {
/* On est a l'étape i,
* on examine si le plateau courant est
* plus long que celui déjà calcule
* i.e. t[i] est égale a t[i-nb], auquel cas
* t[i] est un nouveau x avec nb augmente de 1
67
Langage C
*/
if (t[i] == t[i-nb]) {
x = t[i];
nb++;
}
}
printf("%d est répété un maximum de fois = %d\n", x, nb);
}
Résultat:
% cc plateau.c
% a.out
6 est répété un maximum de fois = 4
68
Structures De Contrôle
Programme 7:
Une variante du programme ci-dessus (Jacques ARSAC,
Pour la Science No 85, Nov. 84) est de chercher dans le tableau t,
quelconque cette fois-ci, les indices d et f tels que la somme des
éléments entre d et f soit maximale. Par exemple, dans le tableau
2 -5 3 -1 8 -2
d f
la plus grande somme (10) est entre le 3e et le 5e élément, i.e.
indices d = 2 et f = 4.
Comme pour les programmes précédents, on va supposer qu'on a
fait la moitié du travail, c'est à dire:
-qu'on possède déjà les indices d et f (d<=f) de la plus
grande somme som calculée jusqu'à maintenant
-qu'on est à l'indice i (i>=f) et en train de chercher une
meilleure somme m, laquelle commence à un nouvel indice k
(k>=d), autrement dit, m est la somme des éléments de k à i-
1.
On examine t[i] donc, qui, rajoutée à m, peut contribuer à
améliorer la somme som déjà connue. Auquel cas som deviendra
m, d et f devenant k et i respectivement. Mais on ne rajoute t[i] à
m que si m est positif (m<0 ==> t[i] > t[i]+m), sinon m se réduit à
t[i] et k devient i. D'où le programme de la page suivante.
Les commentaires en début de programmes indiquent les
trois jeux d'essai testés avec leur résultats. Par exemple, pour le
premier:
% cc plateau2.c
% a.out
Meilleur somme 10 entre 2 et 4
Exercice: Dans le même ordre d'idées, dans un tableau t
quelconque, chercher la plus longue sous suite croissante.
69
Langage C
Dans la suite:
0, 4, -1, 2, 6, 4, 3, 10
on a
-1, 2, 6
qui est la plus longue sous suite croissante.
70
Structures De Contrôle
#define N 6
/*int t[N] = {2, -5, 3, -1, 8, -2} ;
somme maximale 10 entre 2 et 4 */
/* int t[N] = {2, -5, 3, -4, 8, -2};
somme maximale 8 entre 4 et 4*/
/*int t[N] = {-10, -12, -2, -3, -4, -1};
somme maximale -1 entre 5 et 5*/
main(){
/* Variante du problème du plateau.
* Il s'agit de trouver deux indices d et f
* tel que la somme des éléments de t entre d et f soit
* maximale
*/
int d,f, i,k;
int som, m;
d = k = f = 1;
som = m = t[0];
/* On a parcouru t jusqu'à i (exclu)
* som est la meilleure somme rencontrée
* jusqu'à présent et elle est entre d et f.
* On est en train d'examiner si une somme actuelle (m)
* entre k et i n'est pas meilleure que som,
* auquel cas d devient k et f devient i.
*/
for (i=1; i<N; i++){
/* On regarde une nouvelle somme m + t[i]
(ou t[i], si m négatif) */
if (m < 0) {
m = t[i] ;
k = i;
}
else m += t[i] ;
/* On compare avec som */
if ( m > som) {
d = k;
f = i;
som = m;
}
else ;
}
printf(" Meilleur somme %d entre %d et %d 0,som,d,f);
}
71
Langage C
Programme 8:
Un dernier programme (last but not least), un peu insolite,
est un programme qui a pour résultat lui-même. C'est à dire un
programme qui imprime son texte source. Il va de soi que le
programme n'a pas de données, sinon, un programme qui
imprime un fichier texte fait l'affaire: on lui donne en entrée son
fichier source à lire et à imprimer.
Un tel programme n'est pas facile, car il y a un phénomène
d'auto référence, source de paradoxe, comme un serpent qui se
mord la queue. On pourrait imaginer un programme qui contient
pour seule instruction printf("Chaîne"); avec la chaîne égale
au texte du programme. Mais alors si N est la longueur de cette
chaîne, N est aussi la longueur du programme, puisque cette
chaîne est le texte du programme lui-même. Or ce programme
contient au moins 11 autres caractères qui sont: p r i n t f (
" " ) ; Sa longueur est au moins N+11. D'où la contradiction N
> N+11.
Une solution imaginée par Kenneth THOMPSON,
l'inventeur de UNIX, est donnée dans la page suivante (c'est en
réalité un programme qui a pour résultat un programme qui a
pour résultat lui-même).
Ce programme a l'allure suivante:
(0) char s[]= {
(1) …
…
(n-1) …
(n) };
(n+1) main(){
(n+2) for (i=0;s[i];i++) printf("%d,\n",s[i]);
(n+3) printf("%s",s);
(n+4) }
La chaîne s est initialisée et sa valeur est une partie
seulement du texte total du programme (partie allant de (n) à la
fin). En l'occurrence, la partie code qui doit reproduire le texte
total justement, imprimé en deux fois. L'instruction (n+2)
reproduit les lignes (1) à (n-1), et l'instruction (n+3) reproduit le
72
Structures De Contrôle
texte allant de (n) à la fin. Il ne reste plus qu'à reproduire la ligne
(0) et déclarer i.
D'autres solutions, imaginées par les programmeurs
chevronnés (hackers) sont beaucoup plus courtes, mais pas plus
simples, que celle donnée ici.
' ',
char s[]={ '=',
'0', ' ',
'\n', '{',
'}', '\\',
';', 'n',
'\n', '"',
' ', ')',
'\n', ';',
'/', '\n',
'*', 'f',
' ', 'o',
'm', 'r',
'e', ' ',
'm', '(',
'e', 'i',
' ', '=',
'l', '0',
'e', ';',
's', 's',
' ', '[',
'c', 'i',
'o', ']',
'm', ';',
'm', 'i',
'e', '+',
'n', '+',
't', ')',
'a', '\n',
'i', ' ',
'r', ' ',
'e', ' ',
's', ' ',
' ', ' ',
's', 'p',
'\'', 'r',
'y', 'i',
' ', 'n',
't', 't',
'r', 'f',
'o', '(',
'u', '"',
'v', '%',
'e', 'd',
73
Langage C
'n', ',',
't', '\\',
' ', 'n',
'*', '"',
'/', ',',
'\n', 's',
'\n', '[',
'm', 'i',
'a', ']',
'i', ')',
'n', ';',
'(', '\n',
')', 'p',
'{', 'r',
'\n', 'i',
'i', 'n',
'n', 't',
't', 'f',
' ', '(',
'i', '"',
';', '%',
'\n', 's',
'p', '"',
'r', ',',
'i', 's',
'n', ')',
't', ';',
'f', '\n',
'(', '}',
'"', '\n'
'c', };
'h', /* meme les commentaires s'y trouvent
'a', */
'r', main(){
' ', int i;
's', printf("char s[] = {\n");
'[', for (i=0;s[i];i++)
']', printf("%d,\n",s[i],s[i],i);
printf("%s",s);
}
74
2ème
PARTIE
Concepts Avancés
Structures De Données
CHAPITRE IV
STRUCTURES DE DONNEES
Pointers are like jumps, leading wildly from one part of the data
structure to another. Their introduction into high-level languages has
been a step backwards from which we may never recover.14
-- C.A.R. Hoare
1. Les Pointeurs
Le langage C permet de manipuler non seulement les
variables, représentant des données en mémoire, mais aussi leur
adresse, emplacement mémoire où elles se trouvent.
1.1. Opérateurs * et &
Ces deux opérateurs * et &, inverse l'un de l'autre, sont liés
au mécanisme de pointeurs, adressage indirect des objets: & est
l'adresse d'un objet, * est le contenu d'une adresse.
1.1.1. Notion d'Objet et d'Adresse
Un objet est une information, donnée ou programme,
manipulable se trouvant en mémoire. Il n'a d'intérêt que s'il est
accessible et donc adressable. Cela se fait à travers un
identificateur qui est un nom associé à un objet. La valeur de
l'objet est l'information qui y est représentée. L'adresse d'un objet
est alors la designation de l'emplacement où il se trouve.
Exemple:
int V;
14C.A.R.Hoare "Hints on Programming Language Design", 1973, Prentice-
Hall, collection of essays and papers by Tony Hoare
77
Langage C
déclare un nom (identificateur V) associé à un objet variable (dont
la valeur peut changer), de type entier. V désigne une case
mémoire contenant la valeur de cet objet.
... 5 ...
Cette valeur peut être 5 à un moment donné comme sur la figure.
Durant l'exécution du programme, V sera associé à cet objet en
désignant cette case. On dira, par abus de langage, l'objet V.
Remarques:
1) Ceci est vraiment un abus de langage: d'abord un
identificateur disparaît après la compilation, sa trace à
l'exécution reste justement comme adresse. Ensuite, selon
qu'un identificateur est en partie gauche d'une affectation
ou en partie droite, il s'évalue différemment. Dans x = V,
V s'évalue comme la valeur à affecter à x, et dans V = 5, V
s'évalue comme l'adresse de la case mémoire devant contenir
la valeur 5. En C on parle de «l_value» (Left) pour V dans
cette dernière écriture. On peut dire «r_value» pour la
première écriture, x = V.
2) Il convient de bien distinguer entre objet et valeur: une
valeur est une des caractéristiques, en plus de l'adresse (et
du type), d'un objet15.
3) Un objet peut aussi bien être une fonction. Sa valeur est
constituée de son code et son interface constitue son type.
On verra dans le chapitre suivant comment on peut
manipuler l'adresse d'une fonction.
En C l'expression
&V
15 Plus exactement, un objet peut être défini comme un triplet <adresse, valeur,
type>. L'adresse servant pour identifier l'objet et designer son emplacement. Cet
emplacement contient la valeur, d'un certain type, de l'objet.
78
Structures De Données
représente l'adresse de l'objet V, c'est à dire l'emplacement
mémoire (numéro de case, entier positif) où il se trouve, alors que
l'expression
V
représente le contenu de cet emplacement, c'est à dire la valeur
de l'objet. C'est la notation traditionnelle pour manipuler des
objets dans la plupart des langages de programmation
procéduraux. Du moins quand V est une r_value, ce qui est le cas
dans une expression. Voir remarque 1) précédente
(Exercice: A ce propos, &V peut-elle être une l_value? Regarder le
message du compilateur dans ce cas).
1.1.2. Notion de Pointeur
Un pointeur est un objet dont la valeur est l'adresse d'un
autre objet. Ce dernier pouvant à l'occasion être lui même une
autre adresse (mais d'un usage en général limité aux tableaux de
pointeurs que nous présenterons plus loin.)
La déclaration
int* p;
déclare un nom p associé à un objet (variable!, voir plus bas
arithmétique des pointeurs) qui est une adresse d'un entier. p est
en quelque sorte un identificateur de type adresse.
Si l'expression p désigne une valeur de type adresse donc,
l'expression
*p
désigne, l'objet dont l'adresse est dans p i.e. l'entier supposé. p est
déréférencé par *. Voir figure IV-1. (Le symbole * est utilisé dans
une expression comme opérateur d'indirection aussi bien que
dans la déclaration. On dit indirection à cause de "contenu du
contenu de p"). Il s'en suit que l'instruction
p = &v;
79
Langage C
met dans p, objet de type adresse, l'adresse de l'objet v (Noter le
blanc entre = et &). On dira "p pointe vers v". On schématise cela
par une figure comme IV-1.
5
v
v
p
Fig-IV-1, Un Objet p Contenant l'Adresse
d'un Autre Objet v.
Si on fait ensuite
v = 5;
l'expression
*p
donnera la valeur 5 et
*p + 3
donnera la valeur 8 etc....
Aussi, si p est initialisé à &v
*p = 5;
produira le même effet que v = 5, car elle affectera 5 à l'objet v
pointé par p.
1.2. Arithmétique des Pointeurs
Les pointeurs supportent une arithmétique mais de façon
restreinte. Si p est un pointeur, ++p ou p = p+1 etc.... sont
valides. p++ déplace le pointeur une «position plus loin», i.e. un
objet plus loin. (Déplacement d'autant d'octets que la longueur de
l'objet pointé; sizeof() permet de connaître cette longueur le cas
échéant). De même que p = p+i, où i est un entier. p pointera
i objets plus loin (dans le sens de i).
Exemple: Soit la déclaration
80
Structures De Données
#define MAX 10
int t[MAX]={0,1,2,3,4,5,6,7,8,9};
La séquence suivante
{ int *p;
int *q;
int i;
p = &t[1];
printf("%d %d ", *p, *(p+2));
p++;
printf("%d %d", *p, *(p+2)) ;
}
imprime
1 3 2 4
p étant initialisé à l'adresse de t[1], p+2 est alors l'adresse de
t[3]. *p et *(p+2) correspondent donc aux valeurs t[1] et
t[3]. Ensuite, p++ fait pointer p vers t[2] et p+2 sera alors
l'adresse de t[4]. D'où le résultat.
En général on rajoute, ou retranche, un entier à un
pointeur. C'est un des moyens pour parcourir un tableau comme
l'illustre l'exemple suivant:
for (i=0, p= &t[0]; i<10; i++)
printf("%d ", *p++);
p est en quelque sorte un curseur sur le tableau. Avec p++, on
peut le faire glisser sur les éléments successifs de t. Avec p+i, on
peut le faire pointer vers un élément i arbitraire dans le tableau.
L'expression *p++, fait avancer (++) le pointeur et le déréférence
ensuite (*), i.e. prend la valeur pointée. ++ est évalué avant *.
Par contre ajouter par exemple, ou retrancher deux
pointeurs, donne un entier. La séquence suivante donne à i la
valeur 4, distance entre l'adresse de t[7] et celle de t[3] (l'unité
étant la taille des objets pointés).
p = &t[3];
q = &t[7];
i = q - p; /* i reçoit 4 */
Exemple: Inversion d'un tableau.
81
Langage C
for(p= &t[0], q= &t[MAX-1]; abs(p-q)>1; p++,q--){
i= *q; *q= *p; *p=i;
}
Exercices
IV.1.1 Soit {int v=1; int* p; p=&v;}. Que veut dire
*(&v), &(*p)? Expérimenter sur machine en imprimant ces
valeurs.
IV.1.2 Soit un tableau de caractères. Ecrire un programme
qui teste si le tableau est un palindrome (identique à son inverse,
e.g. «ici», «radar», «rever», «un roc cornu», «a man a plan a canal
panama» si on ignore les blancs. «Esope reste ici et se repose» est
un autre palindrome célèbre).
N.B. Parcourir une seule fois le tableau. La taille du tableau est
une donnée.
VI.1.3 Pour s'habituer aux expressions de type *p++, ++*p,
etc... Soit int t[2]; avec t[0]=1; et t[1]=10; et la séquence
p = &t[0];
x = *++p;
printf("%d %d\n", x, *p);
qui imprime le résultat de l'expression *++p (valeur de x) et la
nouvelle valeur (*p) pointée par p. Parmi les expressions
suivantes:
*p++, *++p (celle de l'exemple), ++*p, *p++, *(p++),
*(++p), (*p)++ et ++(*p)
a) Quelles sont celles qui incrémentent x et celles qui
incrémentent p (autrement dit dans quels cas l'indirection
est évaluée avant l'incrémentation)?
b) Quand est-ce que l'incrémentation prend effet: avant
l'affectation à x ou après l'affectation à x?
c) Quelles expressions sont équivalentes?
82
Structures De Données
Indication: Les deux opérateurs sont de même priorité, et
s'associent de droite à gauche.
1.3. Valeur NULL
NULL est une constante symbolique de type pointeur à qui on
peut faire jouer le rôle d'indéfini à la façon du NIL de Pascal. Cela
sert pour tester si un pointeur pointe vers un objet, comme tester
la fin d'une liste dans une structure de données liste chaînée ou
tester la valeur retour des fonctions rendant un pointeur (e.g.
fopen(), gets()). NULL est macro-définie égale à 0 dans
<stdio.h>.
1.4. Initialisation d'un Pointeur
Un pointeur doit —faut-il le dire— toujours être initialisé
avant d'être utilisé. Sinon, comme il peut contenir n'importe
quelle valeur au début, le programme se comportera bizarrement
ou générera une violation mémoire ( le fameux Segmentation
fault core dumped) etc. ...dès le premier accès avec *.
Dans tous les exemples précédents, nous avons écrit
p = & <variable>;
pour initialiser un pointeur: on lui affecte une valeur d'adresse
dans l'espace des données du programme. Cela sert pour
manipuler un tableau par exemple, variable indicée, comme on
vient de le voir sur les exemples précédents.
Il y a une autre façon d'initialiser un pointeur. On demande
une allocation d'espace mémoire pour contenir le ou les objets
(non encore initialisés!) à pointer. L'adresse de cet espace sera
alors valeur initiale du pointeur considéré. La fonction C pour
cela est malloc() de profile:
char *malloc(unsigned taille);
qui a pour effet d'allouer un bloc mémoire de taille cases (au
moins) d'un octet et de retourner son adresse (bloc qui
initialement contient n'importe quelle valeur). Le résultat est un
pointeur vers char dans le sens octet (qui peut être converti vers
le type de pointeur approprié).
83
Langage C
Exemple: Le programme suivant lit une chaîne de caractères et
l'imprime.
#include <malloc.h>
#include <stdio.h>
main(){
char *s, *p;
s = malloc(1000);
gets(s);
puts(s);
}
Il commence par allouer 1000 octets dans un bloc dont l'adresse
est affectée à s pointeur char, lit une chaîne qui sera mise dans
cet espace (pointé par s donc) et l'imprime. Le bloc alloué par
malloc() se trouve dans un espace mémoire réservé à cet effet
appelé tas «heap» ou espace dynamique.
Avec s pointeur entier, s = (int*) malloc(1000)
allouerait pour s un tableau pour 1000 entiers (non initialisés!).
Remarque: Il convient de distiguer entre valeur initiale d'un
pointeur et celle de l'espace alloué (objets pointés). malloc()
initialise le pointeur et non l'espace alloué. Dans l'exemple ci-
dessus, c'est gets(s) qui initialise la chaîne pointée s.
On peut noter au passage l'autre façon d'initialiser un
pointeur: par la valeur retour d'une fonction. En l'occurrence la
fonction gets() ici qui lit une chaîne, la place en mémoire à
l'adresse fournie en paramètre et retourne cette même adresse,
laquelle peut servir pour initialiser p par
p = gets(s);
p et s designeront ainsi la même chaîne lue. Il y a beaucoup de
fonctions C qui retournent des adresses.
En PASCAL normal, l'opérateur adresse & n'existe pas.
C'est pour cela qu'on utilise toujours la procédure new avec les
pointeurs. En C malloc() est en quelque sorte l'équivalent de
new de Pascal16.
16 En C++, il existe une instruction new, semblable à malloc(), qui est d'usage plus
facile.
84
Structures De Données
L'inverse de malloc() est free()
void free(char* ptr);
qui restitue au système l'espace, désigné par ptr, précédemment
alloué par malloc(). A charge pour lui de le récupérer. (Par un
ramasse-miettes, garbage collector.) L'adresse dans ptr devient
virtuellement inexistante quoique physiquement elle contient
peut être toujours la même valeur. Mais l'espace désigné peut
contenir plus tard un autre objet et ptr devient donc obsolète. Le
pointeur ptr est dit «pendant» (dangling).
Autres Fonctions sur Pointeurs
La fonction
char *realloc(ptr, nouvelleTaille)
char *ptr; unsigned nouvelleTaille;
change la taille de l'espace mémoire référencé par ptr pour le
ramener à nouvelleTaille octets, et retourne l'adresse du
nouvel espace (éventuellement déplacé). Le contenu de l'espace
est inchangé, sauf peut être s'il a subi une réduction passant à
une nouvelle taille plus petite.
La fonction
char *calloc(nelem, tailleElem)
unsigned nelem, tailleElem;
utilise malloc() pour allouer de l'espace pour un tableau de
nelem éléments chacun de taille tailleElem, l'initialise à zéro et
retourne son adresse.
char *s;
s = calloc (20, 1)
alloue 20 octets pour s. Cette fonction est particulièrement utile
pour créer des tableaux dynamiques, dont on ne connaît la taille
qu'à l'exécution.
85
Langage C
int *t;
int nelem;
t = (int *) calloc (nelem, sizeof(int));
alloue nelem entiers (chacun occupant 4 octets en principe) au
tableau t. Remarquer la conversion du retour de la fonction vers
un pointeur entier.
Voir la documentation malloc(3) par «man 3 malloc» pour
d'autres fonctions de ce type. L'exemple qui suit est un
programme qui fait 20 tentatives d'allocation mémoire pour
contenir 1000 réels doubles. Retour NULL en cas d'échec de
malloc().
#define TAILLE 1000
...
double t[TAILLE];
...
for (i=0; i<20; i++)
if ( (t = (double * ) calloc( TAILLE,
sizeof(double))) != NULL) break;
if (i = 20)
printf("Que vous êtes gourmand!\n");
...
Dans la plupart des exemples qui suivent nous n'utiliserons pas
malloc() (on travaillera sur des tableau) pour nous concentrer
sur le phénomène des pointeurs.
1.5. Pointeurs **
p
pp p v
Fig-IV-2, Double Indirection.
Il est possible en C de déclarer un pointeur sur un pointeur
(double indirection, cf. fig-IV-2). Soit les déclarations:
86
Structures De Données
int v=5;
int *p, *q;
int **pp;
pp est un pointeur sur un pointeur d'entier. Si on fait
p = &v;
alors
pp = &p;
est une écriture valide et fait pointer pp sur p. pp contient
l'adresse de p qui contient lui-même l'adresse de v. Si donc
l'expression *p est de valeur 5,
**pp
est un expression valide de même valeur. On a ici deux niveaux
d'indirection. Par ailleurs, l'écriture
q = *pp;
est valide: q reçoit la valeur pointée par pp, i.e. l'adresse de v, et
l'expression *q est de valeur 5.
Mais une écriture comme q = pp ou **q est bien sûr
incorrecte: combinaison illégale de pointeurs et indirection
illégale (en paraphrasant le compilateur). C'est tout simplement
des erreurs de type de données avec des opérations incompatibles.
Exercice IV.1.4: p étant un pointeur, faire le schéma (semblable
à la figure IV-2) correspondant à p=&p; Sous quelle condition
cette écriture est correcte?.
* vs **
En général un seul niveau d'indirection suffit. Un pointeur
sert à manipuler des objets dynamiques. Un objet dynamique est
un objet dont l'emplacement par exemple est changeant:
caractéristique très importante quand on ne peut pas prévoir à
l'avance l'existence par exemple ou la taille d'un objet. Pour ne
pas lier un programme à des contraintes de ce types, et pour
rendre les manipulations faites sur un objet indépendantes de son
emplacement (ou sa taille), le mécanisme d'indirection est le
moyen adéquat: le programme utilise un emplacement fixe qui
87
Langage C
contient comme valeur l'adresse de l'emplacement actuel de
l'objet manipulé. Adresse pouvant varier au gré des objets
auxquels il faut accéder (ou des déplacements de ces mêmes
objets à cause d'un tassage par exemple), mais l'emplacement qui
la contient est fixe.
Un deuxième niveau d'indirection ne se justifie que si
l'emplacement fixe voulu par le programme est en fait un ensem-
ble d'emplacements fixes, c'est à dire, un tableau de pointeurs. Il
serait alors très intéressant de manipuler le dit tableau comme
un premier niveau d'indirection, donc via un pointeur, selon le
schéma:
t
p Objet1
Objet2
Objet3
.
.
.
Fig-IV-2-bis Pointeur sur Tableau de Pointeurs
Nous reprendrons cette discussion plus loin en § 4. Pour
l'instant intéressons-nous au lien que fait le langage C justement,
entre un tableau habituel et un pointeur.
2. Pointeurs et Tableaux
Les exemples cités précédemment lors de la discussion sur
l'arithmétique des pointeurs, illustrent l'intérêt des pointeurs
pour manipuler des tableaux. En programmation C, on peut
manipuler les tableaux aussi bien par des pointeurs que par
indexation. Le compilateur le fait d'ailleurs à votre place dans ce
dernier cas (Il ne faut pas bouleverser les habitudes du program-
meur).
88
Structures De Données
t ...
0 1 9
Fig-IV-3, Pointeur p (= &t[0]) Vers le Premier
Elément d'un Tableau.
Soit les déclarations:
int t[10];
int *p,x;
et l'instruction
p = &t[0];
qui correspondent au diagramme de la figure IV-3. On a alors
x = *p;
affecte à x la valeur t[0]. C'est comme l'écriture x = t[0];
x = *(p + i);
affecte à x la valeur t[i]. C'est comme l'écriture x = t[i];
Inversement
*(p+i) = 5; est équivalent à t[i] = 5;
Donc
*(p + i) et t[i]
désignent le même objet ( si i est toutefois non négatif). C'est
pour cela que les indices des tableaux commencent par 0 en C. Si
on a p = &t[0] alors
t[0] t[1] t[2] ... t[i] ...
*p *(p+1) *(p+2) *(p+i)
Règle générale
Un nom de tableau en C est en réalité un pointeur. Dans
notre cas ici, t se comporte comme p, i.e. t contient une adresse
89
Langage C
(comme l'illustre fig-IV-3). Adresse devant rester constante,
rappelons-nous.
p = &t[0]; peut s'écrire p = t; et
t[i] peut donc s'écrire (ce que fait un compilateur) *(t+i)
de même et inversement
*(p+i) peut s'écrire p[i], pointeur indicé (intéressant), et
l'expression (r_value)
&t[i] peut s'écrire t + i.
Attention!
La différence entre un pointeur et un tableau, est que ce
dernier ne supporte pas d'arithmétique. t est un nom (associé à
l'adresse) de tableau qui doit rester constant pour que t[i] ait
toujours le même sens, classique encore, dans tout le programme.
Donc
si p++ a un sens, t++ n'en a pas
si p = t; a un sens, t = p; n'en a pas ( modification du
contenu de t).
Pas plus que &t (t est déjà une adresse). Ne pas confondre
avec &t[i].
Autrement dit t est une adresse constante durant l'exécution.
Voilà pourquoi les tableaux ne sont pas affectables en C. A ce
propos l'expression (déjà vue).
"abcdefgh" [4]
est juste et désigne la lettre e, 5ième élément d'une constante
tableau de caractères. Exemple qui montre, si besoin est, le
privilège dont jouit le cas particulier d'un tableau de caractères
en C, c'est à dire une chaîne.
En conséquence, le passage d'un tableau en paramètre à
une fonction, ne se fait pas par valeur (copie du tableau) mais par
adresse (copie du contenu de t, i.e. l'adresse du 1er élément du
tableau). Nous parlerons de cet aspect dans le chapitre V avec le
passage de tableaux en paramètres.
Une autre différence entre un tableau et un pointeur, c'est
qu'un pointeur doit être initialisé. Par malloc() ou par
90
Structures De Données
affectation d'une adresse (p = &...). En effet, un tableau est
alloué dans l'espace d'adressage d'un programme (son nom pointe
vers une zone accessible et n'a donc pas besoin d'être initialisé),
alors qu'un pointeur reste «pendant», sa valeur est indéfinie, tant
qu'il n'a pas reçu une valeur d'adresse. Mais ladite valeur, elle, se
trouve dans l'espace d'adressage du programme.
3. Tableaux et Chaînes de Caractères
Une chaîne de caractères étant un tableau de type char, on
la manipule par un pointeur char. Chose relativement aisée. Il
suffit de déclarer un identificateur tableau de char
char TaChaine[26];
ou encore mieux, un identificateur de type char*
char *MaChaine;
pour lui faire subir des affectations comme
MaChaine = "Gai Luron";
TaChaine = MaChaine + 4; /* TaChaine devient
"Luron"
ou des déplacements vers un autre caractère comme
MaChaine++;
MaChaine += 10;
Exemple: La fonction suivante calcule la longueur d'une chaîne de
caractères.
strlen (char *s) {
int n;
for (n=0; *s++ != '\0'; n++);
/* boucle sans corps */
return (n);
}
On peut l'utiliser, si on déclare
char* MaChaine = "toto";
par
strlen (MaChaine) qui retourne 4,
strlen (&MaChaine[2]) qui retourne 2, la longueur du
deuxième "to" ou
91
Langage C
strlen (MaChaine + 2) qui retourne 2 pour les mêmes
raisons.
En effet, MaChaine étant déclarée pointeur sur char et
initialisée "toto", l'expression MaChaine est l'adresse du premier
caractère 't' de "toto", &MaChaine[2] est l'adresse du
troisième caractère 't', ainsi que MaChaine + 2.
Ainsi une chaîne de caractères se manipule directement par
son nom et il n'y a pas besoin de l'opérateur * d'indirection qui
est transparent:
MaChaine = TaChaine;
strlen(MaChaine);
strcmp(Machaine, TaChaine);
etc.... L'usage de typedef (renommage d'un type en C) peut
renforcer cette transparence:
typedef char* STRING;
STRING MaChaine;
C'est quand on veut accéder à un caractère de la chaîne
qu'on a besoin de l'opérateur * d'indirection comme *s dans
l'exemple ci-dessus.
Exercices
IV.3.1 Constater que la fonction suivante qui calcule la
longueur d'une chaîne en utilisant un tableau, est équivalente à
la précédente (exemple ci-dessus).
strlen (char s[]){
/* s[] sans bornes, pour dire que c'est un tableau
*/
int n = 0;
while (s[n++]) ;
return (--n);
}
Cette fonction est-elle vraiment équivalente à la précédente?
IV.3.2 Ecrire la fonction strcat(s1,s2) qui rend une
chaîne égale à la concaténation de s1 et s2:
a) sans changer s1
92
Structures De Données
b) s1 devient (aussi) le résultat de la concaténation.
c) quels sont les effets de bord de votre fonction?
4. Tableaux et Pointeurs
Reprenons la discussion entamée en fin du § 1 de ce
chapitre. Un tableau de pointeurs (d'entiers par exemple) se
déclare par:
int* t[10];
qui déclare t comme un tableau de 10 pointeurs sur entiers.
Exemple:
% cat tableauPointeur.c
main(){
int *t[9];
int i=2;
t[2]= &i; /* t[2] est une adresse! */
printf("%u \t%d \t%d\n", t[2], *t[2],
**(t+2));
}
% cc tableauPointeur.c
% a.out
4160748720 2 2
Un élément de tableau (e.g. t[2]) contient une adresse d'entier
(celle de i) comme l'illustre la valeur 41607... du premier argument
t[2] de printf(). Pour accéder à cet entier on dispose de deux
écritures:
*t[2], valeur pointée par cette adresse t[2] et qui est 2
ici, ou bien
**(t+2), valeur pointée par le contenu de l'adresse t+2
(double
indirection).
C'est une simple extension des notations déjà connues pour un
tableau E d'entiers: E[indice] et son équivalent *(E+indice) qui
sont l'entier de rang indice. L'extension étant l'usage de
l'opérateur * pour une première indirection. (Exercice: A ce
propos, quelle est la valeur de l'expression **t ?)
93
Langage C
Donc là il y a une double indirection, matérialisée par
**(t+2). Si maintenant on rajoute les instructions suivantes:
{ int **p;
int j;
t[3]= &j; /* chaque élément de t doit être
initialise pour son propre compte!
*/
*t[3]=3; /* *t[indice] en partie gauche */
p= &t[2];
printf("%d \n", **p);
p++;
printf("%d \n", **p);
}
on obtient en plus les valeurs 2 et 3
% a.out
4160748720 2 2
2
3
Quelle est alors la différence entre p et t? p est un pointeur vers
un pointeur d'entier, en l'occurrence ici le 2e élément de t et qui
donc peut supporter l'arithmétique (p++) ou l'affectation (p=...).
la variable t est un nom de tableau qui est pointeur en C, mais
constant. Si **t est possible (désigne la valeur pointée par t[0],
réponse à l'exercice juste proposé), t++ ou t = … n'est pas
possible.
Remarque: Dans un tableau de pointeurs, un élément qui ne
pointe nulle part doit par convention valoir la constante NULL, à
affecter explicitement.
Exercice:
IV.4.1 Un tableau int *t[3] contient les adresses de trois
variables entières i, j et k. Sans modifier les valeurs de ces
variables, ranger ce tableau selon les valeurs croissantes de i, j et
k.
94
Structures De Données
Comme on a utilisé un tableau de pointeurs d'entiers, on
peut utiliser un tableau de pointeurs vers des objets quelconques
en particulier char.
4.1. Tableau de Chaînes de Caractères
C'est une conséquence directe de ce qui précède. On va se
contenter d'un exemple pour illustrer cela:
% cat tabString.c
main(){
char *s;
char **ps;
char *t[10];
t[0]="toto\0";
t[1]="titi\0";
t[2]="tintin\0";
t[3]="tutu\0";
s = t[1];
ps = &t[2];
printf("%s %s %s %s\n", t[0], *t, s, *ps);
printf("%c %c %c %c\n", *t[0], **t, *s, **ps);
printf("%s %s\n", ++s, *++ps);
}
% cc tabString.c
% a.out
toto toto titi tintin
t t t t
iti tutu
Dans ce programme, char *t[10] déclare t comme tableau
de 10 chaînes de caractères (char*). Les quatre premiers
éléments du tableau sont donc des chaînes comme le montrent les
initialisations. Or une chaîne de caractères se manipule
directement et sans indirection par son identificateur ( qu'on sait
être un pointeur vers un caractère). Le résultat de cet exemple le
montre bien:
t[0] est la chaîne "toto" (il faut un indice ici) et
s est la chaîne "titi" dans t[1] (cela on le sait déjà).
De plus, t étant tableau donc pointeur vers son premier élément
et ps étant double pointeur vers char, on a:
*t est aussi la chaîne "toto"
95
Langage C
*ps est la chaîne "tintin" (ps valant &t[2])
D'où la présence de * pour la première indirection dans ces deux
derniers cas. Cependant *t[0], **t, *s et **ps sont des doubles
indirections vers le premier caractère 't' de chaque chaîne (Ce
qui revient aux cas de l'exemple de tableau de pointeurs d'entiers
avec char au lieu de int).
Remarquer que ++s fait avancer la chaîne s vers son prochain
caractère alors que ++ps fait avancer ps (pointeur vers une
chaîne élément de tableau) vers le prochain élément(chaîne) du
dit tableau! C'est la différence entre char* et char** . Se
rappeler enfin aussi que t++ ou t=... sont interdits. C'est la
différence entre les déclarations char* t[10] et char **ps.
Notations syntaxiques
L'opérateur d'indexation [] est de priorité supérieure à
l'opérateur d'indirection *. Reprenons int comme exemple de
type.
1) int *t[10]; déclare t tableau de 10 pointeurs entiers.
2) int (*t)[10]; déclare (*t) comme un tableau de 10
entiers et donc t est un pointeur vers un tel tableau. Ici, t
supportera l'affectation et l'arithmétique.
3) int *(*t)[10]; déclare t comme pointeur vers tableau
de 10 pointeurs d'entiers.
Aspects sémantiques
Le deuxième cas ici, pointeur vers un tableau de «quelque
chose», mérite l'attention (dans les cas 1 et 3, ce quelque chose
n'étant autre qu'un entier et une adresse d'entier respective-
ment). Un pointeur vers un tableau d'entier, ne se comporte pas
comme un pointeur d'entier initialisé vers un tableau d'entier.
Soit
96
Structures De Données
int tab[10],*p;
p= &tab[0]
Ici, p est déclaré devant pointer vers un entier, donc
l'initialisation est correcte, et en outre p++ fait avancer p de un
entier.
Par contre dans
int (*p)[10];
p est déclaré devant pointer vers un tableau de 10 entiers, et
comme tel, il doit être initialisé par quelque chose d'horrible
comme:
p = (int (*)[10]) &tab[0];
pour convertir l'adresse &tab[0] vers un type compatible avec p.
Cela a pour conséquence que p++ fait avancer p de la longueur
d'un tableau de 10 entiers. (Quelle est l'initialisation par
malloc() dans ce cas?)
Exemple: Soit int tab[50] un tableau de 50 entiers, et int
(*p)[10] un pointeur vers un tableau de 10 entiers. On initialise
p à &t[0] et ensuite on le fait avancer de deux crans par p +=2.
Dans les deux cas on imprime les 10 entiers pointés.
% cat pointToTab.c
main(){
int tab[50];
int (*p)[10];
int i;
/* tab initialisé aux 50 premiers entiers */
for(i=0; i<50; i++) tab[i]=i;
p= (int (*)[10]) &tab[0];
for(i=0; i<10; i++)
printf("%d ", (*p)[i]); printf("\n");
p +=2;
for(i=0; i<10; i++)
printf("%d ", (*p)[i]);
}
97
Langage C
% cc pointToTab.c
% a.out
0 1 2 3 4 5 6 7 8 9
20 21 22 23 24 25 26 27 28 29
Le pointeur p a bien avancé vers la troisième tranche de 10
éléments de tab. Noter dans ce cas que la taille 10 dans
l'initialisation de p doit être la même que celle dans sa
déclaration. Une autre initialisation est:
p = (int (*)[10]) malloc (1000);
pour allouer un espace pour 100 tranches de 10 entiers (réponse à
la question juste proposée).
Exercices:
IV.4.1 Soit t un tableau de 6 chaînes non vides de longueurs
croissantes. Ecrire un programme qui a pour résultat la chaîne
obtenue par concaténation des chaînes Si (i=0..5), Si = les i
premiers caractères de chaque chaîne t[i].
Exemple:
t[0] = "do";
t[1] = "il";
t[2] = "magasin";
t[3] = "gingembre";
t[4] = "airelle";
t[5] = NULL;
doit donner
NULL
imaginaire
IV.4.2 Reprendre le même exercice pour un tableau de N
chaînes.
IV.4.3 Idem, avec Si débutant au caractère i.
5. Types Structurés
5.1. La Structure struct
98
Structures De Données
Jusqu'à présent la seule structure d'objet complexe qu'on a
vu est la structure tableau à une ou plusieurs dimensions. C'est
une structure d'objet complexe, dans le sens composé d'objets plus
élémentaires. Dans le cas du tableau ces objets élémentaires
sont des objets de même type. C'est pour cela qu'on peut les
ranger dans des cases consécutives de même taille et donc
numérotées.
Une autre structure d'objet complexe est celle
d'enregistrement (cf. RECORD de Pascal). C'est une structure
composée d'une juxtaposition d'objets quelconques appelés
champs. En C une structure est déclarée par la forme générale:
struct [<nom>] { <ListeDeclarations> };
Le nom <nom> est celui de la structure et il va jouer un rôle de
type pour déclarer des objets de cette structure.
<ListeDeclarations> donne les déclarations des différents
champs de la structure.
Exemple:
struct date {
int jour;
char mois[3]; (1)
int annee;
};
est une structure d'objet d'un nouveau type date, composé d'un
champ entier jour, un champ mois de 3 caractères, e.g. "jan"
"fev" etc…, et d'un champ annee qui est entier aussi. Une
écriture comme struct {… …} s'appelle un constructeur de type.
jour, mois et annee sont des variables, mais date non. C'est le
nom de la structure.
Maintenant, pour déclarer des variables de type date et
ayant cette structure on peut écrire:
struct date d; (2)
où d est la variable structurée composée de trois champs: jour,
mois et année. C'est toujours l'écriture
<type> <variable>;
99
Langage C
où le type est struct date. On se réfère aux champs individuels
de d par l'opérateur point (.) Ainsi:
d.jour est l'entier jour.
d.mois[1] est le deuxième caractère du mois.
d.annee est l'entier année.
La définition d'une variable structurée est faite donc en
deux phases: la construction du type par la description de la
structure avec ses champs comme en (1), et la déclaration de la
variable structurée comme en (2). C permet néanmoins de
combiner ces deux écritures comme suit:
struct date {
int jour;
char mois[3]; (3)
int annee;
} d ;
La structure date est définie et une instance, variable d, en est
déclarée. D'autres variables qui ont la même structure, peuvent
être déclarées ultérieurement sans redéfinir la structure par
struct date d1, d2; (4)
où d1 et d2 sont deux nouvelles variables ayant cette structure.
Néanmoins il peut paraître plus discipliné de définir d'abord
la structure comme en (1) et ensuite déclarer les variables
correspondantes séparément comme en (2). Une écriture comme
(3) risque en effet de cacher la déclaration de la variable d, pour ne
voir que celle des champs, alors que (4) est plus explicite.
On peut imbriquer les structures les unes dans les autres:
struct DatePrecise {
int jour;
char mois[3];
int annee;
struct moment {
int heure, minute,sec;
} m;
} dp ;
où m est un champ structuré faisant partie de la structure dp.
Noter que l'apparition de m dans la description de la structure
100
Structures De Données
moment est nécessaire ici, ce qui contrarie ce qui vient d'être dit.
Mais la structure moment peut être décrite par ailleurs et utilisée
ensuite dans la structure qui la requiert.
Autre exemple:
struct personne {
char nom[10],prenom[10];
float salaire;
struct date DateNaissance;
};
Utilisation:
struct personne p;
p.nom = "toto\0";
p.DateNaissance.jour = 10;
notation doublement pointée dans le deuxième cas.
Remarque: On peut déclarer des variables structure sans nommer
celle-ci.
struct {int x,y} z;
où z est une variable structurée composée de deux entiers x et y.
La structure n'étant pas nommée, elle est inutilisable ailleurs.
Cela fait quand même assez de liberté dans C. (Exercice: à ce
propos que pensez-vous de struct { int x,y}; qui est correcte
en C? )
5.2. Tableaux de Structures
On peut déclarer des tableaux de structures
<type> <variable> [<dimension>] …
Par exemple:
struct personne employes[100];
pour avoir un tableau (variable employes) constitué de 100
personnes (structure personne). La dimension [100] suit donc le
nom de la variable structure comme dans int x[100];. On
utilise cette variable simplement comme tout autre tableau avec
le point (.) pour chaque champ.
101
Langage C
s = employes[i].salaire;
c = employes[i].nom[0]
sont ainsi le salaire et la première lettre du nom du ième
employé. Mais il faut faire attention à l'affectation du genre:
employes[2].nom = employes[1].nom
qui copie le nom du deuxième employé sur le troisième. Se
rappeler que les tableaux ne sont pas affectables (il faut utiliser
un pointeur char* nom dans ce cas ou la fonction strcpy()).
Mais les structures, elles, sont affectables même au niveau sous-
structure. Tout l'objet est copié.
employes[100].DateNaissance = d;
Ce qui nous ramène aux pointeurs vers une structure.
5.3. Pointeurs vers les Structures
C étant un langage orthogonal, pour avoir un pointeur vers
une structure il suffit de le déclarer.
struct personne *pp;
déclare pp comme pointeur vers une structure personne. On peut
écrire
(*pp).salaire
pour accéder au champ salaire. L'opérateur . étant prioritaire sur
l'indirection *, il faut parenthéser celle-ci. Mais il y a une autre
notation d'utilisation. Au lieu de (*pp).salaire, on utilise
l'opérateur -> pour écrire plus simplement
pp->salaire
pour accéder au champ salaire de la structure pointée par pp.
Le signe -> est là pour le refléter. Dans (*pp).salaire les
parenthèses sont obligatoires à cause de la précédence de . sur *.
Auparavant on aura initialisé (ne pas l'oublier) pp par
pp = &employes[0] ou pp = employes; par exemple.
Une variable pointeur vers une structure peut aussi subir
de l'arithmétique. Comme les autres pointeurs. L'incrémenter par
exemple, la fait pointer une structure plus loin.
102
Structures De Données
x = (pp + 1)->salaire;
accède au salaire de l'employé suivant du tableau et l'affecte à x;
(++pp)->salaire;
fait pointer pp vers l'employé suivant et accède à son salaire.
Dans le cas précédent pp n'a pas bougé.
L'opérateur -> a la même priorité que le point (.). Dans ces
deux dernières expressions, il faut parenthéser (pp+1) et (++p).
Voir plus bas.
5.4. Combinaison de pointeurs tableaux et
structures
struct {
x y
int x;
int *y; p 1 2
} *p, q[10];
p pointe vers un couple: entier x et un pointeur d'entiers y.
q est un tableau de 10 couples x et y, ainsi définis.
Soit i=2 et j=4 deux entiers, et les initialisations
q[0].x = 1;
q[0].y = &i;
q[1].x = 3;
q[1].y = &j;
p = q;
correspondant à la figure:
q
x y i
p q[0] 1 2
j
q[1] 3
4
... ...
On a:
p->x est 1 et
103
Langage C
*p->y est 2 (-> étant prioritaire, l'indirection concerne y),
tout comme
q[0].x est 1 et
*q[0]->y est 2.
(p+1)->x est 3 et *(p+1)->y est 4. (ce serait q[1].x et
*q[1]->y ici)
(++p)->x est 3 et *(++p)->y est 4. ( p aura été incrémenté
de 1 avant d'accéder à x et ce à quoi y pointe)
Attention à la syntaxe:
++p->x aurait été considéré comme ++(p->x), i.e. incrémenter x,
car -> est prioritaire sur ++ (ainsi que sur l'indirection * comme
déjà signalé). Par contre p++->x est pareil que (p++)->x
(pourquoi?): dans les deux cas on incrémente p après avoir accédé
à x (post-incrémentation).
De même, *p->y prend ce à quoi y pointe comme *(p->y) (la
valeur 2 ci-dessus). *(++p)->y incrémente p avant de prendre ce
à quoi y pointe (la valeur 4 ci-dessus). *p++->y, comme *(p++)-
>y prend ce à quoi y pointe et incrémente p après.
Mais l'écriture *p->y++ prend ce à quoi y pointe et ensuite
(post)incrémente y (syntaxiquement, c'est comme *t++, où t
serait ici (p->y)). Ce serait utile si y était un tableaux ou une
chaîne dans la structure. (Exercice: que serait *++p->y? Réponse:
(Pre)incrémenter y et aller vers quoi il pointe. *p->++y est
syntaxiquement incorrecte).
5.5. Structures Récursives
Une structure peut s'utiliser elle-même, mais par pointeur.
104
Structures De Données
Struct arbre {
int valeur;
struct arbre *FilsGauche;
struct arbre *FilsDroit;
};
Les champs FilsDroit et FilsGauche ne sont pas une structure
arbre, mais un pointeur vers structure arbre. Nous y
reviendrons (§ 5.7).
5.6. Unions
L'union est une structure qui contient une donnée dont le
type varie selon les circonstances de son utilisation. Sa syntaxe
est semblable à celle de struct mais avec le mot clé union.
union ent_car {
int i;
char c;
} x;
Ici on a deux variables x.i et x.c, qui occupent la même place en
mémoire donc une seule donnée. Avec struct au lieu de union, il
y aurait eu deux emplacements différents. La taille de x ici est
celle du plus grand composant de l'union, donc 4 octets (de int).
x.c = 'A';
printf("i = %d c = %c\n", x.i, x.c);
donne i = 65 c = 'A' (i=65 si le compilateur est bien gentil de
remplir les 3 octets restants par 0). C'est en fait i considéré
caractère dans le second cas, i.e. x.i et x.c désignent un même
objet. &x.i est donc égale à &x.c.
Se pose alors le problème de comment savoir à tout moment
si on doit utiliser un entier ou un caractère? Quel type est
significatif? En associant une autre donnée qui pourrait être
utilisée pour indiquer que la valeur est significative mais pour tel
type, comme dans la figure IV-5.
Cette structure StrNoeud, et la variable associée x,
contient deux éléments : le champ type et le champ valeur. Le
champ type servira, si égale à 'C' par exemple, pour indiquer que
c'est valeur.opérateur qui est significative et, si égale à 'I', ce
105
Langage C
serait valeur.opérande. Plus précisément x.valeur.operande
et x.valeur.opérateur. On peut reconnaître ici un arbre binaire
représentant une expression arithmétique où un noeud est
parfois opérande parfois opérateur.
struct StrNoeud {
char type;
union {
int operande;
char operateur;
} valeur;
} x ;
Fig-IV-5, Une structure avec Union, pour le Nœud
d'un Arbre Expression Arithmétique.
On peut utiliser cette structure par
switch (x.type) {
case 'I' :
printf (" Operande %d \n", x.valeur.operande);
break;
case 'C' : printf (" Operateur %c \n",
x.valeur.operateur);
break;
default : printf (" Pardon? \n");
}
Moralité: l'union n'est utile que si elle est associée à une
structure.
Elle peut aussi être combinée avec des pointeurs et des
tableaux comme pour les structures. La même notation est
appliquée
<NomUnion>.<Champs> ou <NomUnion> -> <Champs>
En d'autres termes, l'union est une structure où tous les champs
sont situés au même endroit et se chevauchent.
5.7. Typedef
C permet de définir de nouveaux noms de types. Ce n'est
pas de nouveaux types, mais juste un nom pour se référer à un
106
Structures De Données
type défini par combinaison de types existants, ou pour avoir un
type mnémonique.
typedef int NOMBRE;
rend NOMBRE synonyme de int. On peut l'utiliser pour déclarer
NOMBRE effectif, taille;
NOMBRE dimensions [4];
On peut aussi créer un type STRING
typedef char* STRING;
et déclarer
STRING nom; STRING classe[30];
C'est peut être mieux que char* classe[30];
typedef est très utile avec les structures. En voici deux exemples.
typedef struct {
float re;
float im;
} COMPLEXE, *PCOMPLEXE;
De struct à } c'est la définition du type (une structure), et
COMPLEXE c'est son nom. PCOMPLEX est un type pointeur vers cette
structure. On peut donc déclarer
COMPLEXE x, y, z;
PCOMPLEXE px, py, pz;
Cette dernières écriture est du reste la même que COMPLEXE *px,
*py, *pz.
Mais attention aux opérations permises pour les nouveaux types.
z = x + y; est bien sûr incorrecte. Voici une fonction qui ajoute
deux complexes:
107
Langage C
PCOMPLEXE cxPlus (PCOMPLEXE x, PCOMPLEXE y){
COMPLEXE z;
z.re = x->re + y->re;
z.im = x->im + y->im;
return (&z);
}
et qu'on peut utiliser par
pz = cxPlus (px, py); ou par
pz = cxPlus (&x, &y);
Autrement dit, les complexes étant de nouveaux objets, il faut en
fournir les opérateurs de manipulations (cf. C++).
Un deuxième exemple de typedef est donné par la figure
IV-6.
typedef struct StrArbre {
struct StrNoeud nd;
struct StrArbre *fg;
struct StrArbre *fd;
} ARBRE, *PTRARBRE;
Fig-IV-6, Structure Arbre Binaire (définition
des types arbre et pointeur vers arbre)
Ici la structure est nommée par StrArbre car il y est fait
référence. StrNoeud est la structure déjà définie plus haut (figure
IV-5). Voici un exemple d'utilisation
PTRARBRE ArbreAllouer(){
/* allocation d'un arbre */
return ( (PTRARBRE) malloc(sizeof(ARBRE)));
}
qui alloue l'espace pour un arbre (sa racine plus exactement).
Pour les fils il doit y avoir la même allocation. Noter la conversion
vers le type PTRARBRE du résultat char* de malloc(), et l'usage
de sizeof() pour calculer la taille du type ARBRE. D'où l'usage de
deux noms: ARBRE et PTRARBRE dans typedef.
Utiliser typedef a donc certains avantages. Il permet
d'utiliser des nom courts est mnémoniques (COMPLEXE, ARBRE)
108
Structures De Données
pour une bonne compréhension de programme, et surtout il peut
aider à assurer une meilleur portabilité pour les types
dépendants des machines (ce qui le rend très utilisé dans les
fichiers include). Le changement sera fait une seule fois dans la
ligne typedef. Imaginer qu'il faut partout remplacer int par
short, si int est prévu sur 2 octets et il se trouve qu'il est sur 4.
Si on avait
typedef int ENTIERCOURT;, on n'aura qu'à réécrire
typedef short ENTIERCOURT;.
5.8. Exemples de Programmes
Nous allons illustrer des structures de données sur deux
exemples. L'exemple de la figure IV-7 est une fonction qui évalue
le résultat d'une expression donnée sous forme d'arbre
(paramètre e de type PTRARBRE). Les déclarations utilisées sont
celles des figures IV-5 et IV-6.
109
Langage C
EvalExp( PTRARBRE e ){
if (e == NULL) /* arbre vide au premier appel
*/
return;
else if (e->nd.type == 'I') /* c'est une
feuille */
return (e->nd.valeur.operande);
else /* c'est un noeud operateur */
switch (e->nd.valeur.operateur) {
case '+' : return ( EvalExp(e->fg) +
EvalExp(e->fd)); break;
case '-' : return ( EvalExp(e->fg) -
EvalExp(e->fd)); break;
case '/' : return ( EvalExp(e->fg) /
EvalExp(e->fd)); break;
case '*' : return ( EvalExp(e->fg) *
EvalExp(e->fd)); break;
default : printf("Operateur bizarre! %c\n",
e->nd.valeur.operateur);
};
}
Fig-IV-7, Evaluation d'une Expression Arithmétique
(représentée sous forme d'arbre).
L'algorithme commence par tester si e est NULL, auquel cas
l'arbre est vide. Ensuite si l'information du nœud (racine de e) est
du type entier alors c'est une feuille (un arbre réduit à la racine,
i.e. expression réduite à un opérande), et la valeur de e est celle
de ce nœud. Sinon, le nœud est un opérateur et le résultat de
l'évaluation de e est celui de l'application de cet opérateur aux 2
sous-expressions: sous-arbre gauche et sous-arbre droit, dans cet
ordre. Ces sous-expressions sont bien sûr évaluées par appel
récursif à la même fonction. Noter que ces appels récursifs ne se
font jamais sur une expression NULL. Du moins si l'arbre est bien
formé.
Voici un exemple d'arbre bien formé:
110
Structures De Données
- *
3 1 3 /
6 2
Dont l'évaluation donne la valeur 11, i.e. (3-1) + 3*(6/2).
Un deuxième exemple, beaucoup plus simple et pour rester
au milieu des arbres, est bien sûr le parcours pré/post/infixé d'un
arbre. Voir figure IV-8 pour un programme réalisant le parcours
préfixé. La spécification de l'algorithme est
PreFixe (A) = Racine (A) ; PreFixe(A.FG) ; PreFixe(A.FD)
où A est un arbre et Racine(A) un traitement quelconque sur le
nœud racine de A, comme l'impression de sa valeur dans cet
exemple. Le point virgule signifie l'enchaînement. Les autres
parcours s'en déduisent simplement en mettant Racine(A) au
bon endroit:
PostFixe (A) = PostFixe(A.FG) ; PostFixe(A.FD) ; Racine(A)
InFixe (A) = InFixe(A.FG) ; Racine(A) ; InFixe(A.FD)
PreFixe(PTRARBRE e) {
if (e != NULL) {
if (e->nd.type == 'I')
printf("%d ",
e->nd.valeur.operande);
else
printf("%c",
e->nd.valeur.operateur);
pre(e->fg);
pre(e->fd);
}
}
Fig-IV-8, Parcours Prefixé d'Arbre Binaire.
Exercice: Vérifier par programme les parcours (post/in)fixés.
111
Langage C
Voici le résultat donné pour le parcours préfixé du même arbre
que ci-dessus.
+-3 1 *3 /6 2
Un programme principal pour ces fonctions est donné par la
figure IV-9. On y a initialisé un arbre par affectations dans les
différents nœuds pour illustrer l'usage des structures avec des
pointeurs. Les variables a, b,..., i sont des pointeurs pour les
différents nœuds. L'arbre construit est (a, (b, (d, e), c, (f, (g, (h,
i))))) en notation LISP. Soit:
b c
d e f g
h i
Attention, ne pas commettre l'erreur facile de vouloir
condenser et écrire a=b=c=...=i=ArbreAllouer(); (Pourquoi
c'est faux?)
6. Types Enumérés
Autre caractéristique de C, un type énuméré définit un
ensemble d'objets dont les valeurs possibles sont données en
extension, i.e. énumérées. Ce sont donc des objets élémentaires.
La syntaxe est identique à celle de la structure avec le mot clé
enum
enum [<nom>] { <listeEnum> } [<variable>];
Le rôle de nom est le même que pour struct. Il sert à déclarer
des variables enum. Par exemple
enum color {blue, green, red};
enum color coul, *pcoul;
coul est une variable de type énuméré pouvant recevoir les
valeurs blue, green ou red.
112
Structures De Données
coul = blue;
pcoul est un pointeur vers une couleur pouvant en recevoir
l'adresse.
pcoul = &coul.
Les types énumérés se comportent comme les types
scalaires, on peut les affecter, les comparer (l'ordre d'énumération
est l'ordre croissant: blue < red < green ici), les passer en
paramètres etc....
Par ailleurs, les valeurs (énumérées) d'un type enum, sont
équivalentes à des constantes entières 0, 1, 2, ...(dans l'ordre
d'énumération) et peuvent être traitées (ce que fait le
compilateur) comme telles. Par exemple blue est équivalent à 0,
green à 1 et red à 2. Ces constantes peuvent être redéfinies dans
l'énumération.
enum color {blue, green=3, red=20};
Ici green est 3, red est 20 et blue reste 0 (l'ordre est croissant).
Si on fait par exemple
switch (coul) on peut écrire
case 3 : ... ou
case green : ...
113
Langage C
#include <stdio.h>
#include <malloc.h>
struct StrNoeud {
char type;
union {
int operande;
char operateur;
} valeur;
} ;
typedef struct StrArb {
struct StrNoeud nd;
struct StrArb *fg;
struct StrArb *fd;
} ARBRE, *PTRARBRE;
main() {
PTRARBRE a,b,c,d,e,f,g,h,i, ArbreAllouer();
/* allocations des noeuds */
a = ArbreAllouer(); b = ArbreAllouer(); c =
ArbreAllouer();
d = ArbreAllouer(); e = ArbreAllouer(); f =
ArbreAllouer();
g = ArbreAllouer(); h = ArbreAllouer(); i =
ArbreAllouer();
/* initialisations des feuilles (fils = NULL) */
d->fg = NULL; d->fd = NULL;
e->fg = NULL; e->fd = NULL;
f->fg = NULL; f->fd = NULL;
h->fg = NULL; h->fd = NULL;
i->fg = NULL; i->fd = NULL;
/* initialisations des feuilles Opérateurs/
a->nd.type ='C'; a->nd.valeur.operateur ='+';
b->nd.type ='C'; b->nd.valeur.operateur ='-';
c->nd.type ='C'; c->nd.valeur.operateur ='*';
g->nd.type ='C'; g->nd.valeur.operateur ='/';
/* initialisations des feuilles Opérandes/
d->nd.type ='I'; d->nd.valeur.operande = 3;
e->nd.type ='I'; e->nd.valeur.operande = 1;
f->nd.type ='I'; f->nd.valeur.operande = 3;
h->nd.type ='I'; h->nd.valeur.operande = 6;
i->nd.type ='I'; i->nd.valeur.operande = 2;
114
Structures De Données
/* Et des noeud restants jusqu'à la racine */
b->fg = d; b->fd = e;
g->fg = h; g->fd = i;
c->fg = f; c->fd = g;
a->fg = b; a->fd = c;
/* les appels */
printf(" RESULTAT %d \n",EvalExp(a));
PreFixe(a);
}
PTRARBRE ArbreAllouer(){
/* allocation d'un arbre */
return ( (PTRARBRE) malloc(sizeof(ARBRE)));
}
Fig-IV-9, Programme Principale pour les Figures IV-7 et IV-8.
115
Les Fonctions et Les Noms Externes
CHAPITRE V
LES FONCTIONS ET LES NOMS EXTERNES
I may not be totally perfect, but parts of me are excellent.
-- Ashleigh Brilliant
Rappelons qu'un programme C est un ensemble de fonctions
et qu'en programmation une fonction est un code (bloc
d'instructions) qui a un nom qui permet de l'utiliser comme un
tout à plusieurs endroits. Si en plus on paramètre ce code, on
peut l'utiliser sur différents objets, appelés alors paramètres, et
par là même lui faire retourner une valeur résultat. Rappelons
aussi que dans un programme C une des fonctions doit s'appeler
main.
1. Les Fonctions
En C on peut compiler un programme utilisant une fonction
même si cette dernière n'est pas encore écrite. (On dit référence
en avant). Cette possibilité est très intéressante dans la mesure
où, au moment de l'écriture d'un programme, on n'a pas encore
envie de réfléchir à comment programmer une fonction, pour se
concentrer uniquement sur l'algorithme en cours. Il suffit d'avoir
une idée de ce qu'elle fait —le quoi ou sa spécification17. D'où les
notions de déclaration et de définition de fonction. Définition qui
du reste peut être remplacée par une autre sans remettre en
cause le programme, si la spécification est respectée. En outre si
cette définition se trouve sur un fichier séparé, elle est la seule à
être recompilée (en un fichier .o).
1.1. Déclaration de Fonction
Une déclaration de fonction est la donnée de son entête ou
17 Spécification qui inclut en réalité la description de ce que fait la fonction, e.g.
sous forme de commentaire.
117
Langage C
interface — appelé prototype de fonction par la norme ANSI —
qui est constituée du nom de la fonction, du type de son résultat
(par défaut int) et du type de ses paramètres éventuels.
[<Type>] <Nom> ( [<ListeTypeParametres>] );
Elle est utile donc car on peut utiliser son nom pour y faire appel
avec toute la cohérence nécessaire en ce qui concerne les
paramètres.
Exemple:
main() {
...
float x;
float cube(float);
/* dcl d'une fonction cube */
x = cube (3.0);
...
}
A la rencontre de cube(3.0), le compilateur saura que
l'identificateur cube est une fonction qui retourne un objet float
et qui a un argument de type float.
La présence des parenthèses dans cube(3.0) suffit en fait
pour indiquer que c'est une fonction. Mais elle sera prise comme
retournant un entier int, type par défaut, si la déclaration est
absente ou si le type du résultat n'est pas précisé.
Si le type de la fonction est int donc, la déclaration n'est
pas nécessaire (cela par conformité avec les premiers
compilateurs C qui tolèrent ce fait). Mais il vaut mieux la
mentionner quand même car c'est plus discipliné, ne serait ce que
dans un esprit de clarté.
Le type des paramètres dans la déclaration de fonction n'est
pas nécessaire non plus. On aurait pu déclarer (forme classique
encore de la déclaration d'une fonction)
float cube();
Mais il vaut mieux encore le mettre —sinon on a des surprise—
pour pouvoir par exemple convertir les paramètres effectifs avant
l'appel. Dans un appel comme cube(3), 3 est converti en float.
118
Les Fonctions et Les Noms Externes
(Voir §1.4. plus bas). Noter cependant qu'il s'agit bien de types
sans variables.
1.2. Définition de Fonction
La définition d'une fonction est la donnée du texte de son
algorithme (qu'on appelle aussi le corps de la fonction). Elle a la
forme
<type> <Nom> ([<DéclarationParametresFormels>])
{ <Corps> }
Attention: il n'y a pas le terminateur ; après la parenthèse
fermante de la liste des paramètres.
Exemple:
float cube (float x) {
return (x*x*x);
}
Dans cette définition on rappelle bien sûr l'entête de la fonction et
on y rajoute le nom des paramètres, x ici qui joue le rôle de
paramètre formel. Si la définition d'une fonction précède son
utilisation, il n'y a par conséquent pas besoin de déclaration.
Remarque: On peut écrire
float cube (x) float x; {...}
par conformité avec l'ancienne forme de C où on énumère les
paramètres formels dans la parenthèse et spécifie leur type
après. Là, il y a ; car c'est la déclaration des paramètres
formels.
1.3. Appel de Fonction
L'écriture
<NomFonction> ( [<ListeParametresEffectifs>] );
est un appel de fonction. C'est un nom suivi de parenthèses. Si la
fonction est définie avec des paramètres formels, il faut fournir,
dans l'ordre respectif, les paramètres effectifs correspondants,
avec si possible la concordance des types. N'importe quelle
expression peut être paramètre effectif en principe. (L'ordre
119
Langage C
d'évaluation des paramètres à l'appel n'est pas défini, et il vaut
mieux éviter des expressions à effet de bord type f(…, i=3, i,
k++, k,…). Voir exercice V.1.2 plus bas).
Exemple: Pour la déclaration
float f (float, int, char);
nous pouvons avoir les appels suivants:
3.14 * f(x, i, c);
f(0.5, i+j, "abc"[2]);
f(cube(3.5), t[2], c);
Un appel de fonction est une expression (r_value) dont la
valeur est le résultat de la fonction, en l'occurrence l'objet
retourné, et le type celui de la fonction (sauf si celle-ci est de type
void. Voir plus bas). Un appel de fonction peut donc se trouver
dans n'importe quelle expression. Il peut constituer lui même une
instruction, s'il est suivi de ;. Le résultat est alors perdu18, i.e.
non utilisé, comme dans
scanf("%d", &i);
1.4. C ANSI vs C Classique
Le langage C classique est très libéral sur l'usage des
fonctions. Dans sa définition initiale, C ne permet pas de
mentionner (sous peine d'erreur compilation) le type des
paramètres dans la déclaration d'une fonction et ainsi aucun
contrôle n'est (ne peut être) fait lors d'un appel. C'est le
programmeur lui-même qui doit gérer la concordance des
paramètres en nombre, rang et type (au risque d'avoir des
surprises, voir exercices V.1.1). La politique c'est que ce qui est
transmis à une fonction est un double ou un int. Lors de l'appel,
un argument float est converti en double sinon en int. Cette
liberté, si elle est intéressante surtout pour utiliser une fonction
avec des arguments variant en nombre, peut se révéler
dangereuse si pratiquée à outrance. De plus cette liberté laisse
beaucoup de choix au compilateur. Or une règle d'or en
18 Certains compilateurs sortent néanmoins un avertissement (Warning) type
"résultat de fonction non utilisé".
120
Les Fonctions et Les Noms Externes
programmation est d'éviter tout ce qui dépend d'un compilateur
donné.
La norme ANSI a repris cela à son compte en introduisant
la notion de prototype de fonction. C'est la forme complète, type
du résultat et des paramètres, de sa déclaration. Elle permet au
compilateur de vérifier la cohérence des arguments (en type et en
nombre) à l'appel et d'avertir par un message d'erreur (ou du
moins un "Warning") en cas de non conformité19. Néanmoins, des
conversions peuvent s'opérer à l'appel, comme dans les
expressions, quand c'est possible.
Cette notion de prototype permet aussi de déclarer une
fonction avec un nombre variable de paramètres (voir plus loin §
1.6.). Dans la suite, et dans la définition d'une fonction, nous
utiliserons parfois le schéma ANSI
f (int x, int y, float z) {...}
parfois le schéma
f (x, y, z) int x,y; float y; {...}
Ce dernier ayant l'avantage de factoriser les variables d'un même
type (x, y ici) et d'être conforme avec le langage C classique.
Exercices
V.1.1 Concordance des paramètres.
Soit la fonction:
f (x, y) double x;int y; {
printf("x = %lf y = %d\n", x,y);
}
de paramètres x double, y int, et qui les imprime.
a) Exécuter les appels suivants et interpréter le résultat:
f(1.0, 2); f(1, 2); f(1.0, 'A'); f(1.0, "toto");
f('A', 2);
19 C++ a renforcé cet aspect.
121
Langage C
b) De même pour les appels suivants (plusieurs arguments
à l'appel):
f(1.0, 2, 13, 14); f(1, 2, 13, 14);
Indication: Dans f, x occupe deux mots de 4 octets.
c) Refaire (a) et (b) pour f définie selon le schéma ANSI:
f(double x, int y){...} (contrôle des arguments) et regarder
le résultat de la compilation.
V.1.2 Ordre d'évaluation des paramètres et effets de bord.
Soit la fonction
f(x, y, z) int x, y, z; {
printf("%d %d %d \n", x, y, z);
}
Exécuter les appels suivants: (avec int i=1;)
f(i, i++, i++); f(i++, i, i++); f(i++, i++, i);
Conclure.
1.5. Mode de Passage des Paramètres
En C, comme nous l'avons déjà signalé, il n'y a pas la notion
de «procédure», au sens classique, avec aucun ou plusieurs
paramètres résultats. Les fonctions retournent en principe un
résultat, celui de leur calcul, est nécessitent donc un passage des
paramètres par valeur.
Pour des "routines" sans paramètres résultats, on peut
déclarer une fonction de type void i.e. neutre. Par exemple une
routine qui trace une ligne pourra être déclarée
void ligne();
est définie comme suit
void ligne(){
printf("_______________________________\n");
}
Et avec un paramètre donné, longueur de la ligne
122
Les Fonctions et Les Noms Externes
void ligne(int n){
int i;
for (i=0; i<n; i++) printf("_");
printf("\n");
}
void sert quand on n'attend pas de résultat d'une fonction.
Si on veut définir une «procédure» avec données et résultats,
par exemple la résolution d'une équation de second degré
(décidément on ne se débarrassera jamais de cet exemple) à
coefficients entiers, on déclarera une fonction Equa2 comme
void equa2 (int, int, int, *double, *double)
et on l'utilisera par l'appel
equa2 (a, b, c, &x1, &x2)
en lui passant, pour les paramètres (effectifs) résultats attendus
x1 et x2, les adresses ou références de ces variables. A travers ces
références, c'est les objets désignés par x1 et x2 qui seront
manipulés dans la fonction et qui seront donc touchés pour
recevoir, par effet de bord, les valeurs solutions attendues.
La définition de la fonction sera en conséquence:
void equa2 (a, b, c, s1, s2 )
int a, b, c; double *s1, *s2;
{int delta ...
...
if (delta > 0)
*s1 = (-b - sqrt((double)delta)) / (2. * a)
*s2 = (-b + sqrt((double)delta)) / (2. * a)
...
}
Les valeurs de x1 et x2 de l'appelante sont manipulées par les
pointeurs associés s1 et s2.
Remarque:
Le qualificatif void n'est pas toujours nécessaire. On peut
l'omettre, la fonction sera alors prise comme rendant un entier
qui ne servira peut être à rien. Mais il est de bon usage de le
123
Langage C
mettre, surtout en C ANSI, car cela favorise la documentation
pour la compréhension et la réutilisation du programme.
Exercices
V.1.3 Ecrire une fonction qui permute deux variables x et y, et
une autre qui classe trois variables a, b et c dans l'ordre croissant
(si a = 2, b = 1, c = 3 on voudrait a = 1, b = 2, c = 3).
V.1.4 Que peut on dire des appels de fonctions à effet de bord (e.g.
g(&x)) comme paramètres effectifs (e.g. f( g(&x), ..., g(&x),
...))?
Indication: Prendre g(x)= x*x et pour f une fonction qui imprime
ses paramètres et se rappeler l'exercice V.1.2 précédent.
déclaration: f(<type> x, <type> y, int z, ...);
appel: f(x, y ,3 , a, b, c);
pile d'exécution:
...
c
b
a Pointeur liste_param
y
x Bas de pile d'exécution
Fig-V-1 Mécanisme de Fonction avec Nombre Variable de
Paramètres. (Ici 6 paramètres: x, y et z déjà signalés, plus a,
b, c fournis à l'appel. La valeur 3 de z est là pour en indiquer
le nombre).
1.6. Fonctions à Nombre Variable de Paramètres
C'est une innovation très intéressante introduite en C
ANSI. Même du point de vue du choix de la syntaxe. On met trois
points de suspension ... avant la parenthèse fermante de la liste
des paramètres:
124
Les Fonctions et Les Noms Externes
f(x, y, z, ...)
On appelle cela une ellipse. Cette caractéristique est très utile,
quand on voudrait faire un traitement sur un nombre quelconque
d'objets même de type différents. Par exemple, l'opérateur de
projection d'une relation dans le modèle relationnel de CODD,
opère sur une liste quelconque d'attributs. (C'est la liste SELECT
dans le langage SQL). Sans aller aussi loin, printf() ou scanf()
de C, sont des exemples de telles fonctions avec ellipse:
printf/scanf( char *, ...) où char* est le paramètre format
d'impression/lecture.
Plus concrètement, dans la définition d'une fonction, on a la
possibilité de traiter un nombre de paramètres quelconque
dépendant de l'occurrence d'un appel. Un moyen pour faire cela
est de disposer dans la fonction d'une liste qui contient la valeur
des paramètres effectifs, et d'en connaître le nombre. Un
compilateur C standard gère une telle liste et la met à disposition
en cours d'exécution (Figure V-1). Pour bien utiliser ce
mécanisme, le programmeur doit aussi choisir un moyen
d'indiquer lors de chaque appel d'une fonction avec ellipse,
combien de paramètres sont actuellement transmis. On pourra
décider que le dernier paramètre avant l'ellipse (z dans la forme
f(<type> x, <type> y, int z, ...)) )
est ce nombre. (Dans printf() c'est le nombre de symboles %
dans la spécification de format qui indique combien de
paramètres effectifs sont transmis).
Voici à présent les outils (type d'objet et fonctions)
bibliothèques, macro-définis, qui permettent d'accéder au
mécanisme de liste de paramètres en la parcourant. Ils se
trouvent dans le fichier include <stdarg.h>
Déclaration de la Liste des Paramètres: Type va_list
Il faut d'abord déclarer la structure de l'objet qui contient
cette liste (qu'on peut considérer comme une pile par cohérence
avec la pile d'exécution). En fait il suffit juste de déclarer un
pointeur vers cette liste, laquelle liste existe déjà du fait de
l'ellipse.
va_list liste_param;
125
Langage C
déclare une variable liste_param du type va_list, et qui est un
pointeur qui va servir à parcourir la liste en pointant à chaque
fois vers un paramètre.
Exemple:
void f(float x, char y, int nb_param /* z */, ...)
{
va_list liste_param;
...
}
Fonctions d'Accès au Paramètres: va_start(), va_arg(),
va_end().
Les deux fonctions va_start() et va_end() sont juste une
initialisation et une finalisation (respectivement) de la liste des
paramètres.
a) va_start (liste_param, nb_param);
est l'appel à une routine qui initialise l'accès à la liste des
paramètres: liste_param est le pointeur déclaré va_list, et
nb_param est la taille de cette liste (taille qui est par convention
passée explicitement en paramètre à l'appel comme nous l'avons
dit)
b) va_end (liste_param);
est l'appel à une routine qui ferme en quelque sorte la liste
liste_param. Elle est sans effet en général, mais son usage est
recommandé avant la fin de la fonction pour une meilleure
documentation.
Exemple:
void f(float x, char y, int nb_param /* z */, ...) {
va_list liste_param;
...
va_start (liste_param, nb_param);
...
va_end (liste_param);
}
c) va_arg (liste_param, <type>);
126
Les Fonctions et Les Noms Externes
est l'appel à une fonction qui rend le paramètre suivant de la liste
liste_param (ou le premier paramètre après appel à
va_start()). Elle rend donc le paramètre pointé par
liste_param. <type> est le type supposé de ce paramètre (int
ou double en principe, voir plus bas). On l'utilise donc (si le
prochain paramètre est du type entier) comme suit:
param = va_arg (liste_param, int);
où param est une variable int locale qui recevra à chaque fois un
paramètre. Cela se fera par exemple dans une boucle avec un
appel à va_arg() par itération.
Exemple:
void f(float x, char y, int nb_param /* z */, ...) {
va_list liste_param;
int i, param;
...
va_start (liste_param, nb_param);
for(i=0; i<nb_param; i++){
param = va_arg(liste_param, int);
... /* traitement de param */
}
...
va_end (liste_param);
}
Voici un exemple de programme principal avec des appels à
cette fonction f, dans laquelle on imprime simplement la valeur
des paramètres. On a rajouté les lignes:
printf("\n%d paramètres: \n", 2+nb_param);
printf("x = %f y = %c plus \n", x, y);
avant l'appel va_start, et la ligne:
printf (" %d \n", param);
dans la boucle for.
% cat ellipseEssai.c
#include <stdarg.h>
void f(float x, char y, int nb_param, ...) {
/*... voir texte ci dessus */
}
main(){
f(0.1, 'a', 4, 1,2,3,4);
127
Langage C
f(0.2, 'b', 0);
f(0.3, 'c', 1,1);
}
% cc ellipseEssai.c
% a.out
6 paramètres:
x=0.100000 y=a plus
1
2
3
4
2 paramètres:
x=0.200000 y=b plus
3 paramètres:
x=0.300000 y=c plus
1
Remarquer l'appel (le deuxième) avec aucun paramètre dans
l'ellipse. Noter aussi le résultat des appels suivants:
f(0.4, 'd', 3, 9); /* nb_param=3 > nombre des
arguments */
5 paramètres:
x = 0.400000 y = d plus
9
0 <-- on récupère des miettes
0
f(0.5, 'e', 2, 1,2,3,4,5,6); /* nb_param=2 < ce
nombre*/
4 paramètres:
x = 0.500000 y = e plus
1 <-- on ne récupère que les premiers
2
Le nombre de paramètres doit par conséquent être égal au
total des arguments fournis, pour une bonne utilisation.
Variation sur un Thème
128
Les Fonctions et Les Noms Externes
L'usage des fonctions avec un nombre de paramètres
variable est donc relativement simple. Dans l'exemple nous avons
utilisé des entiers int pour ces paramètres. Peut-on utiliser
d'autres types et lesquels?
1) Si on se rappelle que lors d'un appel de fonction, les
paramètres de type char et short sont promus int, et ceux de
type float promus double, on conclut qu'on n'a pas intérêt à
utiliser ces types (char, short, float) dans la macro va_arg(),
et que celle-ci doit avoir int ou double comme deuxième
argument.
2) Par contre si on sait par avance que les paramètres dans
l'ellipse sont tous de même type (soit T), on peut utiliser
va_arg() par:
T param;
...
param = va_arg (liste_param, T);
Exemple: T est STRING. Dans l'exemple précédent on remplace la
déclaration de param par
typedef char * STRING;
STRING param;
et écrit
param = va_arg(liste_param, STRING);
Avec l'appel
f(0.1, 'a', 4, "toto", "tintin");
on obtient
6 paramètres:
x = 0.100000 y = a plus
toto
tintin
(null) <--- On a 4 (au lieu de 2) à
l'appel
(null)
3) Si maintenant le type des paramètres est variable aussi à
l'appel, on doit choisir une politique pour pouvoir l'indiquer lors
de l'appel et le récupérer ensuite dans la fonction. Une solution
canonique serait de transmettre chaque paramètre précédé d'un
129
Langage C
indicateur entier valant 0 pour char* (ou STRING), 1 pour int et
2 pour double etc... comme de toute façon on a droit à autant
d'arguments qu'on veut. On double par conséquent le nombre
variable de paramètres.
Dans la fonction f on doit alors distinguer les différents cas
comme ainsi par exemple
switch ( va_arg(liste_param, int)) {
case 0: param0 = va_arg(liste_param, char*);
... /* c'etait un char * */
case 1: param1 = va_arg(liste_param, int);
... /* c'etait un int */
case 2: param2 = va_arg(liste_param, double);
... /* c'etait un double*/
}
Exercices
V.1.5 Ecrire une fonction f qui imprime ses arguments, sachant
qu'ils sont en nombre et en type quelconque.
V.1.6 Simuler la fonction printf() avec une fonction output()
dont le premier argument est une chaîne qui contient autant de
caractères 'x' que d'arguments entiers qui viennent ensuite (et
qui sont à imprimer). Par exemple
output ("abcx efghxx", 1,2,3)
doit imprimer les trois valeurs 1 2 3 fournies (il y a 3 'x').
1.7. Passage de Tableaux en Paramètres
Le passage de tableaux en paramètres se fait toujours par
référence. Le choix n'est pas laissé au programmeur quant au
mode de transmission pour les tableaux, le passage par valeur
nécessitant la copie, à chaque fois, de tout le tableau. Pour passer
un tableau t en paramètre à une fonction f, on doit donc toujours
écrire
f(t)
Le compilateur envoie l'adresse de t à la fonction. En
conséquence f(&t) est une écriture, déjà signalée, incorrecte. Ne
pas confondre, encore, &t et &t[i].
130
Les Fonctions et Les Noms Externes
Pour les paramètres formels dans la définition de f, on peut
déclarer un tableau t comme dans sa déclaration originale avec
dimension, ou comme
t[] dimension vide pour indiquer que c'est un tableau, ( [] dans
le prototype)
*t (* dans le prototype), ce qui est plus commode et plus
général.
**t (** dans le prototype), si c'est un tableau de pointeurs.
Aussi, toute modification des valeurs d'un tableau dans une
fonction, affecte le vrai tableau.
int r[2] = {1,2};
int s[2] = {3,4};
int ProdScal(int *, int []);
main(){
int p;
p = ProdScal(r, s);
printf("Produit Scalaire = %d\n", p);
}
int ProdScal(int *x, int y[]){
/* Retourne le produit scalaire z de x,y */
int i, z=0;
for( i=0; i<2; i++)
z += *x++ * *y++;
/* z += *(x+i) * *(y+i)
z += x[i] * y[i]
marchent aussi */
return z;
}
Fig-V-2, Produit Scalaire de Deux Vecteurs r et s.
Exemple 1:
L'exemple de la figure V-2, est un programme qui calcule le
produit scalaire de deux vecteurs. La fonction int
ProdScal(x,y) où x et y sont des vecteurs, est réservée à cet
effet. Le calcule consiste en la somme des produits deux à deux
des composants de x et de y. Ici le résultat est 11. Cet exemple,
produit scalaire, illustre entre autre 4 choses:
131
Langage C
• Le passage de tableaux en paramètre, en l'occurrence ici r
et s.
• Leurs déclarations en paramètres formels indifféremment
comme pointeurs ou tableaux: x[] ou *x, y[] ou *y.
• Leurs utilisations indifféremment comme pointeur, avec *x,
ou comme tableau, avec indexation x[i]. (Cependant *x++,
incrémentation du pointeur local, n'est pas identique à
*(x+i)).
• Même si y est déclaré y[], i.e. tableau, on peut écrire *y++
(à méditer).
Moralité: Un tableau en paramètre se manipule exactement
comme une chaîne d'objets.
Exemple 2:
L'exemple de la figure V-3, est une somme de deux matrices.
(Le produit de deux matrices est laissé comme exercice!). Nous y
avons utilisé l'ancienne forme d'une fonction (voir plus bas
comment déclarer le prototype correspondant) pour illustrer une
chose importante: un tableau T[L][C] à deux dimensions en
paramètre, est considéré dans une fonction comme tableau T'[] à
une dimension, de taille L*C, rangé ligne par ligne.
132
Les Fonctions et Les Noms Externes
#define L 3
#define C 2
int r[L][C] = { {1,2}, {3,4}, {5,6}};
int s[L][C] = { {0,1}, {2,3}, {4,5}};
void SommeMat();
main(){
int t[L][C];
int i,j;
SommeMat(r,s,t); /* t = r + s */
for(i=0; i<L; i++){
for(j=0; j<C; j++)
printf("%4d", t[i][j]);
printf("\n");
}
}
void SommeMat()
int* x, int* y, int* z;
/* Somme z de deux matrices x et y */
{
int i,j;
for (i=0; i<L; i++)
for (j=0; j<C; j++)
*z++ = *x++ + *y++ ;
}
Fig-V-3, Somme t de Deux Matrices r et s.
T[0][0] devient T'[0]
T[0][1] devient T'[1]
T[0][2] devient T'[2]
...
T[1][0] devient T'[C + 0]
T[1][1] devient T'[C + 1]
...
T[i][j] devient T'[i*C + j]
...
le dernier élément T[L-1][C-1] devient T'[(L-1)*C + C-1]
soit T'[L*C -1]. Et en vertu de la moralité ci-dessus:
T'[m] = *(T' + m) soit, en remplaçant m par i*C + j,
133
Langage C
T'[i*C + j] = *(T' + i*C + j) notation pour accéder au
composant i,j d'une matrice T en paramètre.
A propos de cet exemple (Fig-V-3), on peut noter que
• le passage des paramètres est donc fait par adresse ( avec z
ici résultat)
• On pourrait déclarer int x[], int y[] ou int z[] dans la
définition de la fonction.
• La déclaration x[][] est une erreur de compilation : Null
Dimension. De même que l'accès par x[i][j] (on ne peut
deviner la valeur de C pour le calcul de i*C+j. Déclarer
x[][C] plutôt).
• On peut accéder à l'élément T[i][j] par *(T' + i*C +j)
ou bien par T[i*C+j].
En effet, on peut remplacer la ligne
*z++ = *x++ + *y++;
par
z[i*C + j] = x[i*C + j] + y[i*C + j] ;
ou par
*(z + i*C +j) = *(x + i*C +j) + *(y + i*C +j)
;
(Exercice: le vérifier). On peut préférer la première écriture. Se
rappeler seulement qu'elle n'est pas valable pour un tableau non
en paramètre. Le Résultat du programme est
% cc sommematrice.c
% a.out
1 3
5 7
9 11
Remarques:
Dans la déclaration du prototype d'une fonction (soit f) qui
doit avoir une matrice (soit T[L][C] déclarée par ailleurs) en
paramètre, il faut écrire:
134
Les Fonctions et Les Noms Externes
(a) f(int *) ou f(int []) ou
(b) f(int **) ou
(c) f(int [][C])
Dans la définition de la fonction f, les déclarations des
paramètres doivent alors être identiques (correspondre) au
prototype, i.e. si on a déclaré f(int *) on doit définir f(int
*x){...}
L'écriture (a) est peut-être à préférer, car elle autorise dans
f toutes les notations d'accès: T'[i*C +j], *(T' + i*C +j) et
*T'++. T'[i][j] n'est pas possible dans ce cas . A l'appel de f, il
faut néanmoins écrire f((int*) T) ou f(&T[0][0]) qui marche
dans tous les cas. f(T) est une erreur de compilation. L'écriture
(b) est possible et marche comme dans le cas a- (à l'appel écrire
f((int **)T)). Par contre, l'écriture (c) autorise l'accès par
indexation dans f (notation T'[i][j]) et l'appel simple par f(T).
Elle est à utiliser si pour des raisons de lisibilité on tient à
utiliser la notation matricielle classique dans une fonction.
Seulement attention aux accès de type pointeur (*T'++ etc... ) qui,
si combinés, peuvent donner des résultats incorrects.
Exercices
V.1.7 Refaire le programme somme de deux matrices en
remplaçant la ligne
*z++ = *x++ + *y++;
avec des notations équivalentes (exercice donné dans le texte).
135
Langage C
int factorielle(int n) {
return ( n <= 1 ? 1 : n *
factorielle(n-1) );
}
int fibon(int n){
if ( n<2 )
return (1);
else return
( fibon(n-1) + fibon(n-2));
}
Figs V-4 et V-5, Fonctions Récursives Factorielle
et Suite de Fibonacci.
V.1.8 Ecrire un programme qui fait le produit de deux matrices.
Utiliser d'abord une fonction déclarée
void ProdMat();
ensuite la déclarer
void ProdMat(int *, int *, int*); ou
void ProdMat(int[], int[], int[]);
1.8. La Récursivité
En C une fonction f peut être appelée par elle même,
récursivité directe, ou faire appel à une autre fonction g qui
appelle h qui ... appelle f, récursivité indirecte. Les figures V-4 et
V-5 sont deux exemples de fonctions récursives classiques. Un
exemple d'appel est:
printf("%f\n", (float)fibon(20) / fibon(19));
qui donne 1.618304, le nombre d'or k, déjà rencontré (tel que:
(a+b)/a = a/b. k est la valeur d'un tel rapport. Les valeurs a et b
sont alors les proportions d'un rectangle parfait).
«Procédures» récursives
Une procédure récursive hanoi est donnée dans la figure V-
6. C'est un exemple classique dit «la tour de Hanoi». C'est la
donnée de trois piliers numérotés a b et c, sur l'un d'eux viennent
s'empiler n disques (ou plateaux) de taille décroissante. Le jeu
consiste à déplacer ces n plateaux du pilier a vers le pilier b. On
136
Les Fonctions et Les Noms Externes
prend les plateaux l'un après l'autre, on utilise le pilier c comme
moyen temporaire de stockage de plateaux et on évite de poser un
grand plateau sur un plateau plus petit.
Nous avons défini une fonction hanoi(n,a,b); qui déplace
n plateaux de a vers b, le plateau intermédiaire c étant calculé.
La routine move (a,b); affiche un message "je déplace un
plateau de a vers b". Voici le résultat de l'appel hanoi
(n,1,3);, i.e. déplacer n plateaux du pilier 1 vers le pilier 3 (avec
n = 3 ici).
Je déplace un plateau de 1 vers 3
Je déplace un plateau de 1 vers 2
Je déplace un plateau de 3 vers 2
Je déplace un plateau de 1 vers 3
Je déplace un plateau de 2 vers 1
Je déplace un plateau de 2 vers 3
Je déplace un plateau de 1 vers 3
Le passage des paramètres par valeur est donc favorable à la
récursivité.
Exercice
V.1.9 Supposer que les piliers de la tour de Hanoi sont de
hauteur 5 plateaux, et que ces derniers sont les valeurs 5, 4, 3, 2,
1 dans cet ordre.
a) Ecrire une procédure PrintHanoi() qui affiche
verticalement les 3 piliers de la tour de Hanoi, en utilisant 3
tableaux.
b) Ecrire la procédure move(a,b) qui déplace un plateau,
un entier donc d'un tableau à un autre.
c) Utiliser la fonction hanoi pour simuler le déplacement
des plateaux. A la quatrième étape par exemple, on pourra avoir
sur les piliers a b et c
4 1
5 2 3
-a- -b- -c-
137
Langage C
void hanoi(int n, int a, int b){
/* résout le problème de la tour de hanoi
* déplacer n plateaux du piler n° a vers b.
* a b différents et égaux à 1, 2 ou 3
*/
int c ; /* c n° pilier intermédiaire */
switch (a+b){
case 3 : c = 3; break;
case 4 : c = 2; break;
case 5 : c = 1; break;
}
if (n == 1) move(a,b);
/* déplace un plateau (du haut) de a
vers b */
else {
hanoi(n-1, a, c);
move (a, b);
hanoi(n-1, c, b);
}
}
Fig-V-6, Problème de la Tour de Hanoi.
2. Statut des Variables et Classes d'Allocation
2.1. Portée vs Durée de Vie
2.1.1. Portée
On appelle portée d'une variable la portion de programme où
elle est définie et donc utilisable (connue du compilateur comme
on dit). La portée est ainsi liée à la phase de compilation d'un
programme.
Exemple: Soit le fichier C contenant le texte suivant:
138
Les Fonctions et Les Noms Externes
int x,y;
f(int a, int b){
int i,j;
... (1)
{ int k,l;
... (2)
}
... (3)
}
g(){
int a;
char b;
f(a, a);
... (4)
}
x et y sont connues partout, i.e. toute la suite du fichier
source à partir de la déclaration.
i et j ainsi que a et b (paramètres de f) sont accessible en (1)
en (2) et en (3), c'est à dire dans le bloc de f.
k et l ne sont accessibles que dans le bloc (2).
Ainsi, une référence à k (e.g. k = 3;) en (1) ou en (3) est une
erreur de compilation: "symbole indéfini". Noter que les
variables a et b du bloc de g désignent des objets différents des a
et b de f, et qu'une redéfinition de i par exemple dans (2),
masquerait le i du (1).
En bref, les déclarations faites en début de bloc, instructions
(cas de k,l) ou fonction (cas de i,j), sont locales à ce bloc
(qualificatif auto par défaut). Il en est de même des paramètres
(cas de a,b) pour un bloc fonction. Les déclarations faites en
dehors de tout bloc, sont globales à tous les blocs qui les suivent,
même dans d'autres fichiers d'un même programme (si toutefois
1) elles ne sont pas qualifiées static, voir ci-après, auquel cas
elles ne sont valables que dans la suite du même fichier, 2) elles
sont qualifiées extern dans les autres fichiers).
2.1.2. Durée de Vie
139
Langage C
On appelle durée de vie d'une variable la durée d'existence,
à l'exécution, de l'objet qu'elle désigne. On parle alors de variable
statique ou automatique.
Statique qualifie l'objet qui existe durant toute l'exécution
d'un programme. Automatique qualifie l'objet qui n'existe que
momentanément, e.g. le temps d'exécution d'un appel de fonction
ou, de façon générale, d'un bloc {...}, instruction composée ou
fonction. Pour cela l'allocation mémoire des variables est
différente. Virtuellement on peut imaginer la mémoire comme
dans la figure V-7. C'est l'état de la mémoire en cours d'exécution
d'un programme ( état qui, soit dit en passant, constitue avec les
registres et les descripteurs de fichiers l'image d'un processus
sous UNIX).
Les variables statiques sont déclarées avec le mot clé static
static <type> <variable>;
pour leur allocation dans la zone statique. Elles gardent la valeur
qu'elles ont durant toute l'exécution du programme. Leur zone est
de taille fixée à la compilation.
code
zone statique
zone dynamique
(pile)
libre
tas
Fig-V-7, Etat de la Mémoire en Cours
d'Exécution.
Les variables automatiques, allouées durant l'exécution
dans la pile, sont empilées / dépilées continuellement dans la zone
dynamique. Ces variables ne conservent donc pas leur valeur, et
140
Les Fonctions et Les Noms Externes
pour cause, d'une pile à une autre et celle-ci peut être vide à un
moment donné.
Remarque: Le tas sert en principe aux données, dynamiques,
repérées par des pointeurs. Ne pas confondre avec les données,
dynamiques aussi, de la pile (dynamisme plus «régulier»).
Classes d'allocation par défaut
Les variables globales, déclarées en dehors de toute
fonction, sont systématiquement de classe d'allocation statique et
initialisé à 0 par le compilateur (par défaut). L'attribut static
est donc implicite et c'est la cas de x et y dans l'exemple ci-dessus.
C'est normal car elles sont utilisées dans toutes les fonctions avec
leur bonne valeur. Elles ne doivent donc pas être de classe
automatique et n'ont donc pas besoin d'être déclarées statique.
Néanmoins et pour une meilleur protection, on peut les qualifier
par static pour en limiter la portée au seule fichier où elles sont
déclarées.
Les variables locales à un bloc sont de classe d'allocation
automatique et non initialisées par défaut. C'est la cas dans le
même exemple de toutes les autres variables. Elles sont allouées
(empilées) à l'entrée du bloc (ou appel de fonction) et
disparaissent (dépilées) à la sortie du bloc (ou retour de fonction).
Tout cela en cours d'exécution. Elle n'ont donc pas d'initialisation
par défaut (l'overhead en serait grandement affecté).
Toutefois, on peut déclarer des variables statiques à
l'intérieur d'un bloc de fonction (par le mot static toujours)
comme dans l'exemple
main(){
f();f();f();
}
141
Langage C
f(){
static int y = 5;
printf("%d\n",y);
y++;
}
qui imprime 5 6 7, montrant ainsi que y est une variable locale
(de portée limitée à f) mais qui conserve sa valeur durant toute
l'exécution du programme. Ici y est alloué, en phase de
compilation, dans la zone statique en mémoire. La valeur initiale
5 est affectée par le compilateur (défaut 0) à l'allocation. Ainsi
donc on voit que, d'un appel à l'autre, y conserve sa valeur.
(Enlever static donnera 5 5 5).
Initialisations Utilisateurs
Pour les automatiques, seule les variables scalaires peuvent
être initialisées par le programme à la déclaration, comme dans
float pi = 3.14159;
Les tableaux par exemple ne peuvent être initialisés que si ils
sont déclarés static, ou déclarés à l'extérieur de tout bloc, donc
statiques. (Restriction enlevée dans ANSI C).
char *t = "Ma valeur initiale\0";
static int t[10] = {1,2,3}; /* Le reste est 0 */
2.2. Variables extern et Variables registre
Variable extern
En C l'écriture
extern int x;
dit que x est une variable globale (donc statique) déclarée
ailleurs, i.e. «plus bas» dans le même fichier ou dans un autre
fichier, par
int x;
Plus précisément, en C int x; est dite une définition de
variable. Comme pour une fonction. Alors que extern int x; est
142
Les Fonctions et Les Noms Externes
une déclaration. Elle ne donne pas lieu à réservation d'espace
mémoire, et déclare juste qu'on va utiliser l'entier x.
Exemple:
Fichier F1.c Fichier F2.c Fichier F3.c
extern int x; (1) h() extern int x; (3)
f() {extern int x; (2) p()
{...} ... {...}
g() } int x; (4)
{...} ...
La variable x est définie entière dans F3.c en (4) et est utilisée,
dans les deux fichiers F1.c en (1) et F2.c en (2) par la déclaration
extern int x, et dans F3.c en (3) par la même déclaration parce
qu'elle est définie plus bas. Les fonctions des trois fichiers
utilisent donc le même objet statique x. Remarquer que x est
globale à f et g dans F1.c, locale à h dans F2.c.
Les concepts de global/local et statique/automatique sont
donc orthogonaux. Néanmoins global et automatique n'a pas de
sens en C (cela en a-t-il?).
Variable Registre
L'écriture
register int x;
définit x comme devant être alloué dans un registre si possible.
Cela sert pour accélérer certains calculs devant s'effectuer dans
un registre (si la variable est lourdement utilisée). Toutefois, le
compilateur C peut ignorer cette allocation et se réserver lui-
même le droit de le faire selon ses possibilités. Dans ce cas la
variable est considérée automatique. Les variables registres
n'acceptent pas l'opérateur adresse & (pourquoi à votre avis?).
Qualificatif const
ANSI C a introduit le qualificatif const pour définir des
objets constants dont la valeur n'est jamais modifiée. Le
compilateur empêche de telles tentatives (du moins si elles sont
directes).
143
Langage C
const float pi = 3.14259;
const short kelvin = -273;
Une instruction comme
pi = 3.14;
ne peut donc figurer dans le programme. Par contre, on peut
modifier un objet constant à travers un pointeur. Une telle
modification ne peut de toute façon être détectée qu'à l'exécution:
en allouant la constante dans un espace d'adressage en lecture
seulement (e.g. ROM). Ce qui n'est pas forcement garanti sur
toute installation.
Les objets constants servent principalement à se protéger
contre des accès indésirables, ou bien pour nommer des
constantes pour que leur littéral n'apparaisse qu'à un seul endroit
dans le programme. Détournés de ce but ils sont inutiles.
Attention, ne pas confondre
#define pi 3.14159
et const float pi = 3.14159;
define est une macro et sert à la précompilation. Dit autrement,
le nom pi disparaît avant la compilation. Par contre const est un
qualificatif qui fait partie d'une déclaration et comme tel il est
compilable. Dit autrement, si dans un programme apparaît
l'instruction
x = 2*pi;
avec define le compilateur lira
x = 2 * 3.14159; et avec const il lira
x = 2 * pi;
144
Les Fonctions et Les Noms Externes
main(){
int OpBin();
/* déclaration de fonction OpBin, */
int somme(), produit();
/* somme et produit */
int s,p;
s = OpBin (3, 4, somme);
/* somme est paramètre de Opbin*/
printf("%d\n",s);
p = OpBin ( 3, 4, produit);
/* idem pour produit */
printf("%d\n",p);
}
Fig-V-8 Passage de Fonctions en Paramètres.
3. Les Fonctions en Paramètres
En C une fonction (f disons) peut constituer une «donnée» et
être passée en paramètre à une autre fonction (g disons). Cela se
fait par un mécanisme de pointeur: ce qui est passé à g c'est
l'adresse du point d'entrée de f. Pointeur vers le début de son
code. Ainsi on permet à g de pouvoir appeler f, paramètre formel,
pour réaliser à l'exécution des algorithmes différents selon la
«valeur» de f, paramètre effectif, fournie à g lors de l'appel. On
appelle générique ce genre —d'où le mot— de fonction g, bien que
le terme soit utilisé en programmation (objet surtout) dans un
sens plus global.
Exemple: Le programme de la figure V-8 appelle une fonction
générique OpBin (x, y, f) qui applique un calcul ( e.g. somme ,
produit, pgcd, ...) aux arguments x et y. Ce calcul constitue,
justement, le troisième paramètre f qui est par conséquent une
fonction. En d'autres termes, OpBin(x, y, f) est une fonction
qui a pour résultat f(x,y) où f est variable. Ce programme
imprime: 7 (3+4) et 12 (3*4).
Dans main() on utilise trois symboles de fonction: OpBin(),
somme() et produit(), déclarée rendant un entier. La ligne
OpBin (3,4,somme);
145
Langage C
est l'appel à la fonction générique OpBin() qui applique le
paramètre (déclaré fonction) somme au entiers 3 et 4. Il en est de
même pour la ligne OpBin(3, 4, produit); en ce qui concerne
le produit cette fois-ci. Noter qu'il n'est pas fait usage de l'écriture
&somme ou &produit dans l'appel. Le compilateur comprendra, vu
que ce sont
des fonctions, qu'il doit passer leur adresse en paramètre. Voilà
pour ce qui est appel et transmission des paramètres effectifs.
Pour les paramètres formels de OpBin() (voir figure V-9) et
qui sont x y et f, on déclare x et y comme int, et f comme
int (*f)(int,int);
pour indiquer que le symbole f est un pointeur (*f) vers une
fonction à deux paramètres (int, int) rendant un entier int.
Le signe * est nécessaire pour indiquer que f est pointeur.
Remarquer la présence des parenthèses (*f) qui sont
nécessaires, car l'écriture int *f() est la déclaration d'une
fonction rendant un pointeur sur un entier.
OpBin(x,y,f)
int x,y;
int (*f)(int, int); /* f est un pointeur
vers fonction */
{
int z;
z= (*f)(x,y); /* appel de f sur les
argument x et y */
return z;
}
somme(a,b) int a,b; { return (a+b); }
produit(a,b) int a,b; { return (a*b);}
Fig-V-9 Fonction en Paramètre Formel.
Pour l'appel ou la référence à ce paramètre formel f, on écrit
(voir même figure)
(*f)(x, y);
146
Les Fonctions et Les Noms Externes
i.e. appel de la fonction pointée par f sur les paramètres x et y .
Là aussi on utilise le signe * et les parenthèses.
Exercice
V.1.10 Ecrire un programme calculette qui lit
<nombre> <signe> <nombre>,
par exemple 12 / 4, et qui sort le résultat correspondant à
l'opération ( 3 ici). <signe> est dans { +, -, *, /, % }. On utilisera un
tableau d'opérateurs, char op[5] tel que op[0] = '+', op[1] =
'-' etc... et un tableau de (pointeurs vers) fonctions rendant un
entier, déclaré
int (*f[5])();.
(*f[i])(x,y); est alors l'appel de la ième fonction du tableau
sur les paramètres x et y. On initialisera le tableau par
int (*f[5])() = { somme, diff, prod, div, mod};
Intérêt des fonctions en paramètres
Les fonctions en paramètres, comme signalé auparavant,
permettent d'écrire des procédures (fonctions C) dites génériques,
dans le sens où on écrit un code qui peut être utilisable plusieurs
fois, non seulement en ce qui concerne l'exécution d'un même
algorithme (abstraction procédurale classique), mais ce code peut
donner lieu à des calculs différents.
En effet on peut définir une fois pour toute des unités de
programmes de fonctionnalités génériques telles qu'on puisse les
utiliser dans des applications spécifiques. Considérons
l'algorithme de recherche d'un élément a dans un tableau T de
taille n et qui retourne un entier égale à l'indice de l'élément, et -
1 sinon. Pour cela on doit comparer (fonction comp en paramètre)
a avec les éléments de T pris un par un. En voici l'algorithme:
147
Langage C
typedef int ELEMENT; /* cette ligne vient
avant la suivante */
#include "rechgen.c"
int tabent[10] = {
10,20,30,40,50,60,70,80,90,100};
main(){
/* cas de recherche dans un tableau
d'entiers */
ELEMENT a; /* a est de type ELEMENT */
int EqualInt(int, int);
/* fonction qui teste l'égalité
de deux entiers */
{ int res;
a = 20;
res = recherche (tabent, a, 10, EqualInt);
printf("pour %d on trouve %d \n", a, res);
a = 45;
res = recherche (tabent, a, 10, EqualInt);
printf("pour %d on trouve %d \n", a, res);
}
}
EqualInt(int i, int j) { return (i==j); }
Fig-V-10, Cas_1 Recherche d'Eléments dans un Tableau
d'Entiers.
recherche (t, n, a, comp ) retourne entier
pour i = 0 tantque i < n faire
si comp (a, t[i])
sortir de boucle ;
si i < n
retourner (i)
sinon retourner (-1)
fin
Cet algorithme est général et marche pour n'importe quel tableau
pourvu qu'on lui fournisse une fonction, comp ici, qui compare
deux éléments entre eux et qui doit donc dépendre du type des
éléments du tableau. Voici son implantation en C:
recherche (t, a, n, comp)
ELEMENT t[], a;
int (*comp)();
int n;
148
Les Fonctions et Les Noms Externes
/* ELEMENT est le type de a et des membres de t,
* supposé défini dans l'application
*/
{
int i;
for (i=0; i<n; i++)
if ( (*comp)(a,t[i]) )
break;
if ( i<n)
return (i);
else return(-1);
}
Les figures V-10 et V-11 montrent deux cas d'utilisation, sur
un tableau d'entiers (Fig-V-10) et sur un tableau de caractères
(Fig-V-11). On a utilisé typedef pour définir le type ELEMENT
comme synonyme de int ou char selon le cas. On a aussi supposé
la fonction recherche dans un fichier "rechgen.c".
149
Langage C
typedef char ELEMENT;
#include "rechgen.c"
main(){
/* cas de recherche dans un
tableau de caractères */
char *tabcar = "abcDe*Ghi\0";
ELEMENT a;
int EqualChar(char, char);
/* Teste si deux caractères sont égaux */
{ int res;
a = 'c';
res = recherche (tabcar, a, 10, EqualChar);
printf("pour %c on trouve %d \n", a, res);
a = 'd';
res = recherche (tabcar, a, 10, EqualChar);
printf("pour %c on trouve %d \n", a, res);
a = '*';
res = recherche (tabcar, a, 10, EqualChar);
printf("pour %c on trouve %d \n", a, res);
a = '#';
res = recherche (tabcar, a, 10, EqualChar);
printf("pour %c on trouve %d \n", a, res);
}
}
EqualChar (char a, char b){
if( isalpha(a) && isalpha(b) )
return ( a==b || a==(b+32) || b==(a+32) );
else
return (a==b);
}
Fig-V-11 Cas_2 Recherche de Caractères
dans une Chaîne.
Le programme de la figure V-10 donne comme résultat attendu
pour 20 on trouve 1
pour 45 on trouve -1
Le programme de la figure V-11 donne comme résultat attendu
150
Les Fonctions et Les Noms Externes
pour c on trouve 2
pour d on trouve 3
pour * on trouve 5
pour # on trouve -1
Le lecteur peut trouver que tout cela est compliqué pour une
simple recherche dans un tableau, mais là n'est pas la question. Il
faut comprendre deux chose:
• L'exemple de recherche peut s'étendre à un algorithme plus
complexe.
• La réutilisabilité, (cet aspect est très caractéristique de la
programmation orientée objets) surtout, est une chose
fondamentale dans l'écriture de logiciel car, et l'expérience le
montre, on trouve souvent des «bouts« d'algorithmes qui ont une
certaine ressemblance donnant l'impression du déjà vu. Il serait
alors très intéressant et très productif d'avoir des modèles
d'algorithmes et les utiliser en cas de besoin.
4. Les Fonctions d'Entrées/Sorties de Base
Dans un programme on a très souvent besoin de lire (entrer)
des données ou d'écrire (sortir) des résultats. Nous entendons par
Entrées/Sorties (E/S) de base, les opérations pour lire les données
d'un programme et écrire ses résultats. On dira aussi parfois
afficher les résultats. Abstraction faite des opérations générales
de traitement des fichiers. (Voir Chapitre VII). Nous allons
présenter les opérations d'E/S
getchar()/putchar(), getc()/putc()
pour lire/écrire un caractère
gets()/puts()
pour lire/écrire une chaîne de caractères
scanf()/printf()
pour lire/écrire des données élémentaires quelconques formatées.
Opérations qui nous
sont familières maintenant.
Ces opérations sont toutes définies dans le fichier include
#include <stdio.h>
151
Langage C
Ces opérations permettent de lire ou d'afficher des données sur
les périphériques standard d'E/S appelés stdin et stdout
(STanDard INput, STanDard OUTput) et qui sont assignés par
défaut au clavier et à l'écran.
La notion de périphérique standard, lié au système
d'exploitation, est logique et permet au programme d'être
indépendant du dispositif physique d'E/S. Dans UNIX par
exemple, il y a le choix, au moment de l'exécution d'un
programme d'indiquer l'unité physique d'entrée (clavier ou fichier
texte) ou de sortie (écran, imprimante ou fichier texte). De même,
ce choix peut être fait à l'intérieur d'un programme (voir
freopen() chapitre VII). Par défaut c'est donc le clavier et
l'écran. Nous y reviendrons au chapitre suivant et nous
supposerons tout au long de cette section que les opérations d'E/S
concernent le clavier et l'écran, assignés par défaut à stdin et
stdout.
4.1. Getchar() et Putchar()
Ce sont les opérations les plus simples. Elles lisent ou
écrivent un caractère. Elles ont pour interface
int getchar() /* retourne un caractère lu sur
stdin */
int putchar (char) /* écrit son argument sur
stdout */
Exemple:
Le programme suivant imprime tous ce qu'il lit, i.e. copie le
fichier stdin sur stdout. La séquence d'entrée doit se terminer
par ^D, indicateur de fin de fichier.
#include <stdio.h>
main(){
int c;
while (( c = getchar()) != EOF)
putchar(c);
}
EOF est une variable définie dans stdio.h par
#define EOF -1
152
Les Fonctions et Les Noms Externes
La valeur -1 étant retourné par getchar() en fin de fichier.
Remarquer que c est de type int au lieu de char, ce qui est plus
générale (pour le -1 de EOF).
Ce programme lit un caractère c qui, tant qu'il est différent
de EOF, est ensuite écrit. Voici son exécution:
% cc getchar.c
% a.out
abcdef <-- saisies (y compris newline)
abcdef <-- imprimés
xyz <-- saisies
xyz <-- imprimés
^D <-- fin fichier entrée
%
4.2. Getc() et Putc()
int getc(flot) /* lit un caractère dans un flot */
FILE *flot;
int putc(c,flot) /* écrit un caractère sur un flot
*/
char c;
FILE *flot;
getc() et putc() lisent ou écrivent un caractère sur un
fichier quelconque. Un fichier est considéré en UNIX comme un
flot (stream) d'octets d'après des considérations en dehors de
l'objet de ce cours. FILE est un type de donnée associé aux
fichiers et défini dans stdio.h. La variable flot, utilisée par
getc() et putc(), est un pointeur vers un fichier UNIX.
En réalité, ce sont getc() et putc() qui lisent ou écrivent
un caractère, mais dans un fichier quelconque. getchar() et
putchar() sont les macros
#define getchar() getc(stdin)
#define putchar(x) putc(x, stdout)
qui lisent ou écrivent un caractère sur les fichiers spéciaux, stdin
et stdout, d'E/S standards. Ainsi le programme précédent pourra
s'écrire avec
while ((c= getc(stdin)) != EOF)
putc(c, stdout);
153
Langage C
4.3. Gets() et Puts()
char *gets(str) /* lit une ligne dans stdin */
char *str;
puts (str) /* écrit une ligne dans stdout */
char *str;
gets() et puts() lisent et écrivent une chaîne de
caractères. gets() lit une chaîne jusqu'à newline (donc une
ligne), la place dans la zone réceptrice str en remplaçant
newline par \0 et retourne son argument. A l'appel str doit être
char[] ou char* initialisée.
Exemple:
char t[4],u[5], *ligne;
gets(u);
ligne = malloc(80);
gets(ligne);
ligne = gets(t);
Si la chaîne lue déborde de la zone réceptrice, des données en
mémoire risquent d'être silencieusement écrasées.
Puts(arg) écrit son argument char* jusqu'à \0 remplacé
par newline.
Exemple:
ligne = "toto";
puts(ligne);
4.4. scanf() et printf()
scanf( format [, <ListePointeurs>]) /* entrée
formatée de stdin */
char* format;
printf( format [, <ListeArguments>]) /*sortie
formatée vers stdout */
char* format;
scanf() et printf() sont les opérations que nous avons
jusqu'ici utilisées pour rentrer ou sortir des données. Ces données
sont des nombres, des chaînes ou des caractères. Ces opérations
convertissent respectivement leurs données de (vers) caractères
154
Les Fonctions et Les Noms Externes
imprimables, e.g. 123e-1, vers (de) données en mémoire, e.g. le
réel 12.3. D'où le mot formaté. On se rappellera que ces deux
fonctions utilisent une ellipse ... pour leur nombre variable
d'arguments.
4.4.1. scanf()
scanf() lits des caractères sur stdin, les interprète selon le
format donné et met le résultat dans l'argument correspondant
qui est un pointeur vers une donnée en mémoire. Le format de
lecture est donné par la chaîne en paramètre format qui contient
généralement les spécifications des conversions pour interpréter
les entrées saisies. Cette chaîne peut se composer de
• Caractères blancs (espace tab ou newline) qui doivent
coïncider avec des caractères blancs optionnels dans la
saisie. Autrement dit la lecture commence au prochain
caractère non blanc en entrée. Sauf s'il s'agit de lire un char
(spécification %c) ou énuméré (spécification %[])
• Tout autre caractère, sauf %, qui doit coïncider avec le
même caractère en entrée.
• Une spécification de conversion qui consiste au caractère %
suivi du caractère optionnel * qui fait sauter l'assignation
du champ suivant en entrée, ou d'un nombre optionnel
spécifiant la taille maximale de la zone d'un champ et d'un
caractère de conversion parmi { d, o, x, c, e, f, %}.
Le texte présenté en entrée pour la saisie est constitué de
champs à convertir selon les spécifications fournies et à assigner
aux arguments correspondants (sauf celui spécifié par *). Les
spécifications de format sont:
% Un simple % est attendu mais non assigné
d Un entier décimale est attendu; l'argument correspondant
est pointeur vers entier (int*).
h idem mais pour entier court (short*).
155
Langage C
o Un entier octal est attendu et assigné à un pointeur d'entier.
x Un entier hexadécimal est attendu et assigné à un pointeur
d'entier.
s Une chaîne de caractères est attendue et assignée à un
tableau ou pointeur char*. \0 est ajouté au bout. La chaîne
en entrée commence au premier caractère différent d'espace
et se termine par newline ou espace.
c Un simple caractère est attendu et assigné à un pointeur
vers char. Le saut normal des blancs ne se fait pas dans ce
cas. Pour lire le prochain caractère non blanc il faut faire
%1s ou faire précéder %c par un blanc.
e ou f Un réel flottant est attendu pour être assigné à un
pointeur vers float. le format d'entrée est [+|-]
[entier][.][entier] [E|e][+|-][entier] où un des
trois entier est présent, sinon on récupère la valeur 0.
les formats o d x ou e f peuvent être précédés de la lettre
l (%lf) pour avoir un entier long ou un réel double. Mais %lf et
%f ne doivent pas correspondre respectivement à des arguments
float et double (inverses). De même pour int. Le résultat n'est
pas garanti.
La fonction scanf() retourne le nombre d'arguments
remplis avec succès, en particulier 0 si aucun argument n'est
rempli. Elle s'arrête si le format est épuisé ou si un mauvais
caractère s'est glissé dans l'entrée. Le prochain scanf()
commence là où le précédent s'est arrêté. EOF alias -1 aussi est
retourné s'il est rencontré (^D en entrée). Dans ce cas scanf()
s'arrête, même s'il reste des arguments à remplir.
Exemples:
int i; float x; char nom[10];
scanf("%d %f %s", &i, &x, nom);
avec la ligne en entrée
12 34.5E-1 Thompson
156
Les Fonctions et Les Noms Externes
assignera 12 à i, 3.45 à x et Thompson\0 à nom.
La ligne
12 34.5E-1 Ken Thompson
aurait mis Ken\0 dans nom.
scanf("%2d %f %*d %2s", &i, &x, nom);
avec l'entrée
12345 0123 45ici72
mettra 12 dans i, 345.0 dans x, sautera 0123 et placera "45\0"
dans nom. La prochaine lecture débutera à i. En effet ce qui est
saisie et non assigné à des arguments reste pour le prochain
scanf().
scanf ("%d%f", &i,&x);
scanf ("%d%f", &j,&y);
avec la ligne
1 2 3 4
assignera 1 à i, 2.0 à x, 3 à j et 4.0 à y!
4.4.2. printf()
printf() affiche sa liste d'arguments en les convertissant
selon le format spécifié. Celui-ci contient deux sortes de formats:
des caractères ordinaires quelconques (sauf %) qui sont imprimés,
et des spécifications de conversion introduits par % pour imprimer
les arguments fournis. Après % on peut trouver dans l'ordre:
1) quelque chose optionnelle de la forme
-m.n m.n m -m .n etc ...
m et n étant des entiers. m est la taille minimum de la zone
réservée pour afficher une donnée (justifiée à droite, complétée
par des blancs si besoin), n est alors le nombre de chiffres (la
précision) après la virgule pour les réels, ou bien le nombre
maximum de caractères à imprimer pour une chaîne. Le signe -
sert pour rendre la justification vers la gauche. Si m commence
par 0 alors la zone est complétée par des 0 au lieu des blancs.
157
Langage C
2) la lettre l optionnelle pour les entiers longs ou les réels
doubles.
3) le caractère de conversion
d, u, o, x
pour les entiers à afficher en (d)écimal , sans signe —
(u)nsigned, en (o)ctal sans signe, ou en he(x)adécimal sans
signe. Ainsi -1 (la valeur maximum 11...1 sur 32 bits)
donnera -1 en format d, 4294967295 (232) en format u,
ffffffff en format x et 37777777777 en format o .
e, f, g
pour les réels à afficher en format décimal flottant avec
(e)xposant
[+-]ddd.nnnnnnE[+-]xx, ou en format décimal format (f)ixe
[-]ddd.nnnnnn. La longueur des n est donnée par la
précision sinon elle est 6 par défaut. Le format g est pour
l'affichage le plus précis dans le minimum d'espace.
c
Le caractère est imprimé.
s
La chaîne est imprimée jusqu'à la rencontre de \0 (!) ou la
limite de la zone réceptrice.
%
le caractère est imprimé, i.e. % est imprimé par %%.
A la rencontre de \x dans le format, le caractère spécial x, en
particulier \n, est imprimé.
Exemple:
float x; double y;
x = y = acos((double) -1);
printf("Simples flottants %f %e %g\n", x, x, x);
printf("Flottants doubles %f %e %g\n", y, y, y);
printf("Simples flottants+ %18.12f %18.12e %18.12g\n",
x, x, x);
printf("Flottants doubles+ %18.12f %18.12e %18.12g\n",
y, y, y);
158
Les Fonctions et Les Noms Externes
donne:
Simples flottants 3.141593 3.141593e+00 3.14159
Flottants doubles 3.141593 3.141593e+00 3.14159
Simples flottants+ 3.141592741013
3.141592741013e+00 3.14159274101
Flottants doubles+ 3.141592653590
3.141592653590e+00 3.14159265359
On voit ainsi que le format g, s'il est moins précis, il occupe moins
de place pour un meilleur compromis, et que float est précis
jusqu'à 6 chiffres après la virgule. (double offre 16 chiffres
significatifs. Il faut utiliser dans ce cas le format %22.15e. Une
position pour le point décimal, une pour le signe et 4 positions
pour "e+nn". Les 16 positions restantes sont pour les chiffres
significatifs, dont 15 après la virgule.)
Pour conclure la dessus, nous suggérons au lecteur / lectrice
d'aller visiter le manuel scanf() et printf() de UNIX/LINUX,
avec la commande
man scanf ( man printf)
pour plus de détails, et surtout pour les caractéristiques propres à
une installation donnée (LINUX, UNIX à base de SystemV
d'ATT ou à base de BSD de Berkeley).
4.5. sscanf() et sprintf()
sscanf() et sprintf() lisent et écrivent comme scanf() et
printf(), mais sur une variable chaîne de caractère. Celle-ci est
le premier argument de sscanf() ou sprintf().
Exemple:
char *s; int i; floatx;
s="12 34";
sscanf(s, "%d %f", &i,&x);
donne:
12 34.000000
Ces fonctions sont utiles pour lire des données formatées dans des
arguments passés par un autre programme.
159
Langage C
Remarque: fscanf() et fprintf() sont des fonctions analogues,
mais qui opèrent sur des fichiers textes. Le fichier constituant le
premier argument.
fscanf(f, "%d %f", &i,&x);
lit dans le fichier f (type FILE*) un entier i et un réel x. Ces
fonctions sont utiles pour lire des données formatées provenant
d'un fichier.
Les fichiers font l'objet d'un prochain ouvrage.
160
INDEX a
caractère spécial, 15
/usr/include/math.h, 20 case, 39, 43
%c, 4, 155 cast, 32
%d, 4, 157 ceil, 19
%e, 158 chaîne d'objets, 132
%f, 4, 156 chaîne de caractères, 91
%g, 158 chaînes, 23
%ld, 21 Chaînes de Caractères, 23
%lf, 21, 156 champs, 99
%s, 4, 156 char, 14
16 bits, 15 char*, 23
32 bits, 15 Classes d'allocation, 141
abs, 19 Compilation Séparée, 11
acos, 19 complément à un, 34
addition, 19 Concatène, 24
adresse, 8, 22, 23, 25, 77, 79, 83, Concordance des paramètres, 121
87, 96, 123, 143, 145, 146 const, 143
adresse d'un objet, 77 constante, 6, 15, 16, 18, 25
affectation, 13, 22, 27, 78, 91, 94, constante octale, 14
96, 102 continue, 39, 40, 49
allouer la place à l'objet, 25 conversion, 28, 30, 31, 34, 36, 86,
amortissements d'un prêt 108, 121, 155, 157
bancaire, 52, 55 Conversion Explicite, 31
ANSI, 120, 121, 124, 143 Conversion Implicite, 28
appel de fonction, 119 copie de l'objet, 26
Arithmétique des Pointeurs, 80, 88 cos, 19
ARSAC, 52, 69 cosh, 19
ASCII, 14 ctype.h, 16
asin, 19 décalage, 35
associativité, 27, 37 Déclaration, 7, 18, 40, 117
atan, 19 déclaration de fonction, 117
auto, 139 default, 43
automatique, 140, 141 define, 6, 144
BACKSPACE, 14 définition d'une fonction, 119
BELL, 14 déréférence, 8, 81
bloc, 3, 39, 41, 42, 117, 139 DIJKSTRA, 50, 52
bloc mémoire, 83 dimension, 22
Boucle, 47 dimension vide, 131
branchement, 39 Directives de Compilation, 6
break, 39, 40, 44, 45, 48 division, 19
calloc, 85 division entière, 19
Caractère, 14 do, 40, 47
b
do, 44, 47 %lf, 21, 156
double, 18 %s, 4, 156
double indirection, 86 FORMFEED, 14
Drapeau Hollandais, 52, 65 fprintf, 160
Durée de Vie, 139 fractions continues, 20
effet —de bord, 13 free, 85
effet de bord, 11 fscanf, 160
effets de bord, 122 générique, 145
ellipse, 125 getc, 151, 153
else, 39, 40, 41 getchar, 151, 152
else if, 42 gets, 83, 84, 151, 154
enregistrement, 99 goto, 39, 40, 50
entiers, 15 GRIFFITH, 52, 67
Entrées/Sorties, 3, 151 hackers, 73
enum, 112 Ian STEWART, 60
EOF, 152, 156 if, 39, 40, 48
évaluation d'une expression, 27 if, 41, 42
évaluation des paramètres, 122 include, 2, 6, 109
exp, 19 index, 24
Expressions, 13 indexation, 21
Expressions Conditionnelle, 32 indices de tableau, 22
extern, 139, 142 initialisation, 24, 25
externe, 6 Initialisation d'un Pointeur, 83
fabs, 19 initialisation de tableau, 23
float, 18 Initialisations, 18
floor, 19 initialiser un tableau, 22
fonction, 1, 4, 7, 78, 117, 145 instruction, 2, 7, 13, 39
fonction aléatoire, 19 instruction d'affectation, 14
Fonctions en Paramètres, 145 instruction d'affectation, 27
fonctions mathématiques, 20 int, 15
Fonctions Récursives, 136 interface, 7, 118
fonctions sur chaînes de invariant de boucle, 62
caractères, 25 isalnum, 16
fonctions isalpha, 16
trigonométriques, 19 isascii, 16
for, 22, 44, 45 iscntrl, 16
format, 4, 21 isdigit, 16
%c, 4, 155 islower, 16
%d, 4, 157 isprint, 16
%e, 158 ispunct, 16
%f, 4, 156 isspace, 16
%g, 158 isupper, 16
%ld, 21 itération, 39, 47
INDEX c
for, 22, 44, 45 opérateurs * et &, 23, 77
do, 40, 47, 44, 47 opérateurs arithmétiques, 19
while, 39, 40, 44, 45, 48 Opérateurs de C, 27, 36
KERNIGHAN, 36, 52 Opérateurs Logiques, 34
KNUTH, 50 opérateurs relationnels, 32
l_value, 78 paramètre, 118, 124
LINEFEED, 14 paramètre formel, 119
log, 19 paramètres effectifs, 4, 119
log10, 19 paramètres formels, 3
logarithme, 19 passage de tableaux en
long, 15 paramètre, 130, 132
long float, 18 passage des paramètres, 8, 122
long int, 15 PARNAS, 12
main, 1, 10, 11 partie entière, 19
malloc, 25, 32, 83 passage de tableaux en paramètre,
malloc.h, 25, 84 130, 132
masque, 34 passage des paramètres, 8, 122
math.h, 19 pgcd, 16
matrice, 22, 51, 132 pointeur, 23, 25, 32, 77, 79, 80, 81,
module, 12 83, 85, 86, 88, 89, 93, 96, 102,
modules objets, 9 107, 112, 125, 132, 141, 145,
modulo, 19 146, 153, 155
multiplication, 19 nom de tableau, 89
nom de tableau, 89 pointeur (*f) vers une fonction,
nombre d'or, 20 146
nombre variable de paramètres, pointeur vers (le premier
121, 124 caractère d') une chaîne, 23
Noms Externes, 7, 117 pointeur vers fonction, 146
notation décimale, 16 Pointeur vers le début de son
notation hexadécimale, 16 code, 145
notation octale, 16 pointeur vers structure, 105,
notation x[entier], 22 102
null, 14, 24, 83, 86, 94 pointeur vers tableau, 96
objet, 14, 77 pointeur vers un pointeur, 94
objet complexe, 99 Portée, 138
objet dynamique, 87 pow, 19
objet variable, 78 power, 19
opérateur, 13, 27, 37, 100, 102 précision, 18
Opérateur ++, 28 printf, 2, 125, 151, 154, 157, 159
Opérateur =, 27 priorité, 27, 32, 36, 96, 103
opérateur arithmétique, 29 problème du plateau, 52, 67
opérateur d'indexation, 96 procédure récursive, 136
opérateur d'indirection, 79 produit scalaire, 131
d
programmation structurée, 50 strcpy, 24
prototype de fonction, 7, 118, 121 STRING, 92, 107
puissance, 19 strings.h, 24
putc, 151, 153 strlen, 24
putchar, 151, 152 strncat, 24
puts, 84, 151, 154 strncmp, 24
r_value, 78 strncpy, 24
racine carré, 19 struct, 99
ramasse‐miettes, 85 Structure, 98
rand, 19 structure arbre, 105
realloc, 85 structures de contrôle, 39
Recherche Dichotomique, 46 STRUCTURES DE DONNEES, 77
RECORD, 99 structures de données, 109
récursivité, 136 Structures Récursives, 104
Réels, 18 suite de Fibonacci, 17
register, 143 switch, 39, 40, 43, 48
Règles de Conversion, 28 TAB, 14
return, 3, 14, 39, 40, 50, 119 tableau, 22, 25, 81, 84, 88, 89, 90,
rindex, 24 91, 93, 95, 96, 101
RITCHIE, 36, 52 tableau d'opérateurs, 147
ruptures de séquence, 39 tableau de (pointeurs vers)
scanf, 3, 125, 151, 154, 159 fonctions, 147
short, 15 tableau de pointeurs, 93, 131
short float, 18 tableau en paramètre, 90
short int, 15 tableaux, 21
sin, 19 tableaux de caractères, 23
sinh, 19 Tableaux de Structures, 101
sizeof, 18, 32, 86 tableaux et structures, 103
soustraction, 19 taille d'un objet, 18
sprintf, 159 tan, 19
SQL, 125 tanh, 19
sqrt, 19 THOMPSON, 72
Square Root, 19 tour de Hanoi, 136
srand, 19 tri d'un tableau, 52
sscanf, 159 tri par insertion, 52, 62
static, 23, 139, 140 tri par sélection, 52, 62
statique, 140, 141, 143 triangle de Pascal, 52, 58
stdarg.h, 125 troncature, 24
stdin, 152, 153 type énuméré, 112
stdio.h, 3, 83, 151, 152, 153 typedef, 92, 107, 108
stdout, 152, 153 union, 105
strcat, 24 unsigned, 15
strcmp, 24 va_arg, 126
INDEX e
va_end, 126 variable de contrôle, 45
va_list, 125 variable structurée, 100
va_start, 126 vecteur, 131
valeur absolue, 19 void, 8, 120, 122
valeur de l'objet, 79 while, 39, 40, 44, 45, 48