Académique Documents
Professionnel Documents
Culture Documents
Programmation C
Programmation C
Avant-propos
Nous vous proposons, par ce livre, d'apprendre la programmation en langage C. Dans ce
livre, nous supposerons que le lecteur a des notions de base en programmation,
algorithmique et structures de donnes; si vous n'en avez pas, ou si vous voulez rviser des
notions pendant l'apprentissage du C, vous pouvez consulter les wikilivres associs:
Programmation, Algorithmique et Structures de donnes.
Le langage C permet d'apprhender des principes informatiques de bas niveau, lis
l'architecture d'un ordinateur, comme la gestion de la mmoire. Si la connaissance de ces
principes peut aider comprendre certains cts du langage C, ils ne sont cependant pas
ncessaires pour lire ce wikilivre, les concepts tant expliqus chaque fois que
ncessaire.
Ce livre, en constante volution, est issu d'un projet collaboratif. Si vous trouvez des
erreurs, vous pouvez le corriger directement l'aide des liens Modifier sur chaque
page. Vous pouvez poser des questions et apporter vos remarques en laissant un nouveau
message [1] sur la page de discussion de ce livre. Toute question ou participation est la
bienvenue !
Bonne lecture !
Rfrences
[1] http:/ / fr. wikibooks. org/ w/ wiki. phtml?title=Discuter:Programmation_C& amp;action=edit&
amp;section=new
Sommaire
Voici une description rapide des diffrents chapitres qui constituent ce livre:
Introduction
Le C a une longue histoire... Cr il y a plus de 30 ans, il a beaucoup volu depuis.
Nous expliquerons ici son histoire, dcrirons ses principales caractristiques et
domaines d'utilisation, et fournirons des raisons de l'apprendre (ou de ne pas
l'apprendre).
Bases du langage
Dans cette partie, nous tudierons les concepts de base du langage C, sa syntaxe ainsi
que la manire dont un programme est structur.
Types de base
Ensuite, nous prsenterons les diffrents types de base que fournit le langage C. Ces
types tant pour l'essentiel des nombres (entiers, rels et complexes), ce chapitre
traitera aussi de l'arithmtique associe ces types. Les types caractres, qui sont la
base de nombreux traitements, et ont un rle particulier dans ce langage, seront aussi
dfinis.
Classe de stockage
Le C permet de construire des constantes, par le biais de classes de stockages.
Celles-ci permettent de dfinir plus gnralement des types drivs des types de base
Sommaire
et seront tudis dans ce chapitre.
Oprateurs
Nous tudierons ensuite les oprateurs permettant de travailler sur les types de base,
ainsi que les rgles gnrales qui structurent les expressions en C.
Structures de contrle - tests
La notion d'expression dfinie, nous aborderons les moyens de contrler le flux
d'excution d'un programme. Tout d'abord, les tests de type Si...Alors...Fin Si seront
tudis.
Structures de contrle - itrations
Dans la suite du chapitre prcdent, nous tudierons les structure d'itrations, ou
boucles, de type Pour, Tant que ou drivs.
Fonctions et procdures
Comment le C prend en charge les fonctions et procdures.
Tableaux
Comment le C gre les tables de donnes.
Pointeurs
Les pointeurs sont une spcificit du C, souvent mal comprise. Ce chapitre dfinira et
montrera comment bien utiliser les pointeurs, ainsi que les erreurs viter dans leur
manipulation.
Types avancs - structures, unions, numrations
Explications sur les types avancs du C.
Prprocesseur
Le prprocesseur est un langage au-dessus du C, que l'utilisateur rencontre pour la
premire fois dans l'utilisation des directives d'inclusion d'en-ttes de la bibliothque
standard. Cependant, il est capable de bien plus, et fournit des outils comme les
macros ou l'inclusion conditionnnelle, qui sont l'objet de ce chapitre.
Bibliothque standard
Cette partie a pour but de familiariser le lecteur avec la bibliothque standard du
C. Celle-ci propose un ensemble de fonctions, mais aussi de types ou de variables
permettant d'effectuer des oprations spcifiques, comme les traitements de fichiers,
les calculs mathmatiques, ou la gestion des chanes des caractres.
Chanes de caractres
Ce chapitre traite de toutes les fonctions permettant d'effectuer des traitements sur
les chanes de caractres : concatnation, copie, recherche de caractres, etc.
Entres/sorties
Ce chapitre traite des interactions avec l'utilisateur.
Erreurs
La bibliothque standard utilise un mcanisme unique pour la gestion d'erreur, qui
permet un programme de dterminer prcisment la cause de l'chec d'une fonction
de la bibliothque standard.
Mathmatiques
Ce chapitre dtaille les fonctions mathmatiques fournies par la bibliothque standard.
Sommaire
Gestion de la mmoire
La bibliothque standard fournit un moyen d'allouer et de librer dynamiquement de la
mmoire au cours de l'excution du programme. Cette fonctionnalit est surtout
utilise pour rsoudre des problmes dont le volume des donnes traiter varie d'une
excution l'autre.
Gestion des signaux
Un signal est une interruption logicielle reue par un processus. Ces interruptions
fournissent un moyen de communication entre le systme et un programme, pour
informer le programme de situations d'erreur (utilisation d'une zone de mmoire
invalide, par exemple), ou informer un processus d'vnements asynchrones (fin d'une
opration d'entre/sortie). Le C permet d'envoyer et de traiter la rception de ces
signaux.
Conclusion
Introduction
Historique
Naissance
Le langage C a t invent aux Bells Labs en 1972 par Dennis Ritchie pour permettre
l'criture du systme d'exploitation UNIX, alors dvelopp par Ken Thompson et Dennis
Ritchie.
Le systme d'exploitation UNIX, n la fin des annes 1960 - dbut des annes 1970, a t
crit directement en langage assembleur pour les machines auxquelles il tait destin. Si le
langage assembleur permettait d'crire un tel systme, il n'en tait pas moins peu ais
utiliser. Un tel langage est en effet particulier un type de processeur, ce qui fait que tout
le systme devait tre rcrit pour le faire fonctionner sur une nouvelle architecture. Cela
fait que son principal crateur, Ken Thompson, souhaita rapidement utiliser un langage
plus volu pour rcrire UNIX.
Parmi les langages disponibles l'poque, BCPL (pour Basic Combined Programming
Language, qui est une simplification de CPL), cr par Martin Richards en 1966, tait
intressant. Sans entrer dans des descriptions dtailles, BCPL est un langage simple,
procdural, et non typ. Sa simplicit permettait de crer facilement des compilateurs
BCPL sur les machines de l'poque, o les ressources taient trs limites (le premier
ordinateur utilis par Keith Thompson pour lancer Unix tait un PDP-7, qui disposait d'une
mmoire de 4000 mots de 18 bits, soit moins de 9 Ko). Ken Thompson l'a fait voluer pour
concevoir le langage B, qu'il a implment sur les premires machines UNIX. Cependant,
certaines limitations du langage B ont fait qu'UNIX n'a pu tre rcrit dans ce langage.
partir de 1971, Dennis Ritchie fit voluer B, pour rpondre ces problmes. l'image des
programmeurs qui incrmentent les versions de leurs programmes, Ritchie incrmenta
la lettre B pour appeler le nouveau langage C. Cette volution se stabilisa vers 1973,
anne partir de laquelle UNIX et les utilitaires systmes d'UNIX ont t rcrits avec
succs en C.
Introduction
Dveloppement
Par la suite en 1978, Brian W. Kernighan documenta trs activement le langage, pour
finalement publier avec Ritchie le livre de rfrence The C Programming Language. On
appelle souvent C K&R le langage tel que spcifi dans la premire dition de ce livre.
Dans les annes qui suivirent, le langage C fut port sur de nombreuses autres machines.
Ces portages ont souvent t faits, au dbut, partir du compilateur pcc de Steve Johnson,
mais par la suite des compilateurs originaux furent dvelopps indpendamment. Durant
ces annes, chaque compilateur C fut crit en suivant les spcifications du K&R, mais
certains ajoutaient des extensions, comme des types de donnes ou des fonctions
supplmentaires, ou interprtaient diffremment certaines parties du livre (qui n'tait pas
forcment trs prcis). cause de cela, il fut de moins en moins facile d'crire des
programmes en C qui puissent fonctionner tels quels sur un grand nombre d'architectures.
Normalisation
Pour rsoudre ce problme, et ajouter au C des possibilits dont le besoin se faisait sentir
(et dj existantes dans certains compilateurs), en 1989, l'organisme national de
normalisation des USA (ANSI) normalisa le C. Le nom exact de cette norme est ANSI
X3.159-1989 Programming Language C, mais elle fut (et est toujours) connue sous les
dnominations ANSI C ou C89. Puis l'ANSI soumit cette norme l'Organisation
internationale de normalisation (ou ISO), qui l'accepta telle quelle et la publia l'anne
suivante sous le numro ISO/IEC 9899:1990 (le document tant connu sous le nom ISO
C90, ou C90). Le besoin de normalisation de l'poque tant extrmement pressant,
pratiquement tous les diteurs de compilateurs C de l'poque se sont mis modifier leurs
compilateurs pour supporter la norme (et mme n'ont pas attendu la publication officielle
pour lancer ces modifications), et de fait la quasi totalit des implmentations existant ce
jour comprennent ces normes. L'ISO publia dans les annes qui suivirent deux ensembles
de correctifs, et un amendement pour la gestion des caractres internationaux.
tant donne la grande diversit des implmentations existants au moment de l'laboration
de la norme, et le principe de ngociation qui base les processus de normalisation, la norme
C est un compromis. Son but tait principalement double : d'une part assurer le plus
possible la portabilit du code C, pour simplifier le portage de programmes C d'une
implmentation une autre (ce qui tait souvent un cauchemar raliser), d'autre part
donner une certaine libert aux diteurs pour proposer des extensions spcifiques. Pour ce
faire, la norme a dfini des niveaux de conformit pour les programmes C et les
implmentations, allant du programme strictement conforme, que toute implmentation
doit accepter, et qui fonctionnera exactement de la mme manire, aux programmes
dpendant d'extensions.
On peut noter que le livre de Kernighan et Ritchie a t republi dans une seconde dition,
pour reflter les changements du C89.
Enfin, en 1999, l'organisme ISO proposa une nouvelle version de la norme, qui reprenait
quelques bonnes ides du langage C++ (voir plus bas). Il ajouta aussi le type long long
d'une taille minimale de 64 bits, les types complexes, l'initialisation des structures avec des
champs nomms, parmi les modifications les plus visibles. Le nouveau document, qui au
niveau de l'ISO est celui ayant autorit aujourd'hui, est ISO/IEC 9899:1999, connu sous le
sigle C99. Deux ensembles de correctifs cette version ont t publis jusqu' prsent. Au
sens strict, la dnomination ISO C correspond donc actuellement la norme de 1999
Introduction
corrige, mais l'usage est tel qu'il est prfrable de toujours prciser de quelle version on
parle (89/90 ou 99), et c'est ce qui sera fait dans cet ouvrage lorsque la distinction sera
ncessaire. Il est noter que, si la norme C90 a t largement adopte par les diteurs de
compilateurs C, trs peu implmentent la version C99. En effet, le besoin des utilisateurs
s'est fait moins pressant, et l'effort ncessaire pour rendre les compilateurs conformes a
rarement t men terme.
L'activit du groupe de travail de la norme C est, depuis, porte plus sur la maintenance
du langage qu' de vritables volutions . La prochaine version, dnomme
officieusement C1X pour l'instant, n'est pas encore publie et ne sera pas tudie dans ce
livre[1] .
La norme C spcifie la syntaxe du langage ainsi qu'une bibliothque de fonctions simple.
Cette bibliothque est moins fournie que dans d'autres langages, mais cela est d au
domaine d'utilisation trs vari de ce langage. En particulier, pour assurer une portabilit
maximale du langage entre les implmentations embarques, les ordinateurs de type PC et
les supercalculateurs (par exemple), certaines fonctions n'ont pas t acceptes dans la
norme. On peut citer, entre autres, la programmation parallle, les communications entre
processus, la communication rseau ou les interfaces graphiques...
Cela fait qu'en parallle de cette normalisation du langage C, certaines extensions ont elles
aussi t standardises, voire normalises. Ainsi, par exemple, des fonctions spcifiques
aux systmes UNIX, sur lesquels ce langage est toujours trs populaire, et qui n'ont pas t
intgres dans la norme du langage C, ont servi dfinir une partie de la norme POSIX.
Dans ce livre, nous tudierons surtout le langage C tel que dfini dans les normes ISO
cites ci-dessus. La norme C99 n'tant pas implmente par tous les compilateurs, et
beaucoup de programmes existants tant dvelopps en C90, les diffrences entre les deux
normes seront indiques chaque fois que ncessaire. Toutefois, nous illustrerons certains
chapitres avec des exemples d'extensions courantes qu'un dveloppeur C peut rencontrer,
en particulier concernant les fonctions qui ne sont pas fournies par la norme C elle-mme.
Mais ces exemples d'extensions seront mineurs. D'autres Wikilivres tudient plus en dtails
de telles extensions, vous pouvez les trouver en regardant les livres lists sur la page
Catgorie:C.
Introduction
le faire sur le site officiel de l'ISO, par exemple.
Les corrections, ou questions, concernant la norme sont diffuses individuellement par le
WG14 sur leur site sous le nom de Defect Report (DR), et rassembles pour publication
offcielle dans des rectificatifs techniques ou Technical Corrigenda (TC). Deux ont t
publis pour le C99, en 2001 et 2004.
Par ailleurs, les brouillons (ou drafts) de la norme appartiennent au groupe de travail, qui
peut dcider de les publier. C'est le cas des documents n869, qui est le dernier brouillon
disponible avant la publication de C99, et n1124, correspondant au C99 auquel ont t
ajouts les TC1 et TC2 (le TC3 ayant t publi en 2007).
On pourra noter que, tout comme l'ANSI l'a fait pour le C89, le WG14 a publi un Rationale,
qui est un commentaire de la norme C99. Ces deux textes (en anglais, toujours) peuvent
tre intressants pour comprendre les motivations de certains choix dans l'volution du
langage.
Tous ces documents sont disponibles sur le site officiel du groupe de travail (les URLs et
rfrences se trouvent dans la bibliographie la fin de ce livre).
Toutefois, il faut bien noter qu'une norme est un texte trs technique, qui sert de rfrence,
et est trs loin d'tre un texte pdagogique. Un dbutant fera mieux de lire un ouvrage
comme ce wikilivre pour apprendre le C que lire directement la norme, au risque d'tre
rapidement dcourag. Le prsent ouvrage est construit de manire ne pas avoir
recourir au texte de la norme, et n'y fera rfrence que rarement, mais le lecteur intress
et averti pourra y vrifier des points complexes qui ne seront pas dtaills ici.
C et C++
Ce livre ne porte pas sur le C++, mais ces deux langages tant d'apparence trs proches. Il
convient, pour viter la confusion au lecteur dsireux d'apprendre l'un ou l'autre, de
prciser qu'ils sont trs diffrents et que, si la connaissance de l'un peut aider
l'apprentissage de l'autre, ils ne doivent pas tre confondus. L'ide du langage qui est
devenu C++ a t lance par Bjarne Stroustrup en 1979, dans un souci de faire voluer le
C de manire le rendre plus robuste . L'apport le plus visible de cette volution est que
le C++ est orient objet, alors que le C est procdural, mais ce n'est pas le seul.
Le langage C++ s'est cr partir du C puis, rapidement, en parallle lui, et en 1998 est
devenu une norme ISO au mme titre que le C. Les groupes de travail respectifs du C et du
C++ communiquent rgulirement dans le but d'assurer une certaine compatibilit entre
les deux langages. De fait, un certain nombre d'ides issues des rflexions autour du C++
se sont vues intgres dans la norme C99[3] . Cependant, la croyance populaire selon
laquelle le C est un sous-ensemble du C++, ou qu'utiliser un compilateur C++ pour un
programme C ne pose pas de problme, est fausse, et plus d'un titre :
un source strictement conforme du point de vue du C peut tre invalide au sens du
C++[4] ;
un source qui est la fois strictement conforme pour le C et le C++ peut avoir un
comportement diffrent dans ces deux langages[5] .
Durant l'apprentissage du C, le lecteur devra donc faire attention. Un certain nombre
d'diteurs, mettant profit la grande proximit des deux langages, distribuent ensemble un
compilateur C et un compilateur C++, en appelant parfois l'ensemble C/C++, ce qui
entrane une grande confusion chez les utilisateurs, qui ont parfois du mal savoir quel est
Introduction
le langage dans lequel ils travaillent. L'utilisateur doit donc apprendre comment fonctionne
son implmentation, pour dterminer quel(s) langage(s), norme(s) et extension(s) elle
supporte, et comment le faire fonctionner dans l'un ou l'autre mode.
Une liste de compilateurs, avec les supports des normes, est rfrence en Bibliographie.
Introduction
Est-ce vraiment si merveilleux que le langage C soit un langage si rpandu ? Par effet
domino, la gnration suivante de programmes suit la tendance de ses anctres. Les
systmes d'exploitation crits en C ont toujours des librairies crites en C. Ces
bibliothques systme sont leur tour utilises pour crire des bibliothques de plus haut
niveau (comme OpenGL ou GTK) et le programmeur de ces bibliothques dcide souvent
d'utiliser le mme langage que celui utilis par ces bibliothques systme. Les
dveloppeurs d'applications utilisent ces bibliothques de haut niveau pour crire des
traitements de texte, des jeux, les lecteurs multimdia, etc... La plupart d'entre eux
choisiront d'utiliser pour leur programme le mme langage que les bibliothques de haut
niveau. Et le schma se reproduit l'infini...
Rfrences
[1] Le lecteur intress peut rcuprer la dernire version de travail sur la page "projects" du site du WG.
[2] Certaines normes sont rendues disponibles gratuitement et en libre tlchargement sur internet, mais leur
nombre est relativement faible par rapport l'ensemble des normes existantes.
[3] On peut citer comme exemple les commentaires de fin de ligne, les macros nombre variable d'arguments, les
dclarations de symboles n'importe quel endroit et le mot cl inline pour pallier la prcarit des macros
[4] L'exemple le plus simple serait la dfinition d'une variable ayant pour identifieur un mot rserv du C++,
comme int class;.
[5] Le lecteur curieux pourra se rfrer cet article (http:/ / david. tribble. com/ text/ cdiffs. htm) de David R.
Tribble, en anglais, qui numre les diffrences entre C99 et C++98.
Bases du langage
Bonjour!
L'un des plus petits programmes possible en langage C est :
#include <stdio.h>
int main(void)
{
printf("Bonjour !\n");
return 0;
}
(Les numros de lignes ont t ajouts pour la lisibilit, mais ne font pas partie du
programme.)
Ce programme a pour seule fonction d'afficher le texte Bonjour ! suivi d'un retour la
ligne. Voici la description de chaque ligne qui le compose :
#include <stdio.h>
Inclusion de l'en-tte nomm <stdio.h>. Il est li la gestion des entres et sorties
(STanDard Input Output, entres/sorties standards) et contient, entre autres, la dclaration
de la fonction printf permettant d'afficher du texte format.
int main(void)
Bases du langage
Dfinition de la fonction principale du programme, main (principal en anglais). Il s'agit de
la fonction appele par le systme d'exploitation au dmarrage du programme, tout
programme en C doit donc la dfinir. La partie entre parenthses spcifie les paramtres
que reoit le programme: ici, le programme n'utilisera aucun paramtre, ce qui est spcifi
en C avec le mot-cl void (vide). La partie gauche du nom de la fonction main spcifie le
type renvoy par la fonction: la fonction main est dfinie par la norme C comme renvoyant
une valeur entire, de type int (pour integer, entier), au systme d'exploitation.
{
Compilation
Pour excuter un programme C, il faut pralablement le compiler?.
Tapez le code source du programme Bonjour ! dans un diteur de texte? et sauvegardez-le
sous le nom bonjour.c (l'extension .c n'est pas obligatoire, mais est usuelle pour un
fichier source en C). Ouvrez une fentre de commandes (terminal sous Unix, commandes
MS-DOS sous Windows), et placez-vous dans le rpertoire o est sauvegard votre fichier.
Pour compiler avec le compilateur GNU CC, il faut taper la commande :
cc bonjour.c
D'autres compilateurs pour le langage C peuvent tre utiliss. La compilation du
programme produira un fichier excutable (a.out sous Unix, a.exe sous Windows) qui
peut tre excut (en tapant ./a.out sous Unix, a sous Windows).
Bases du langage
10
lments de syntaxe
Identificateurs
Les identificateurs commencent par une lettre ou le caractre soulign ("_") et peuvent
contenir des lettres, des chiffres et le caractre soulign (cependant, les identificateurs
commenant par deux caractres souligns, ou un caractre soulign suivi d'une majuscule
sont rservs au compilateur, et ne doivent pas tre utiliss dans un programme
ordinaire). Tous les mots-cls ainsi que les symboles (variables, fonctions, champs, etc.)
sont sensibles la casse des lettres. Quelques exemples d'identificateurs valides :
toto
_coin_coin76
break
case
char
const
continue
default
do
double
else
enum
extern
float
for
goto
if
inline (C99)
int
long
register
restrict (C99)
return
short
signed
sizeof
static
struct
switch
typedef
union
unsigned
void
volatile
while
_Bool (C99)
_Complex
(C99)
_Imaginary (C99)
Commentaires
Les commentaires commencent par /* et se terminent par */, ils ne peuvent pas tre
imbriques :
/* ceci est un commentaire */
/*
ceci est aussi un commentaire
*/
/* /* ceci est encore un commentaire */
Bases du langage
Ce code contient une erreur volontaire !
/* /* */ ceci n<font name="DejaVuSans"></font>est pas un commentaire */
Inclure des commentaires pertinents dans un programme est un art subtil. Cela ncessite
un peu de pratique pour savoir guider les lecteurs et attirer leur attention sur certaines
parties dlicates du code. Pour faire court, on ne saurait trop que rappeler ce clbre vers
de Nicolas Boileau, qui disait que ce que l'on conoit bien, s'nonce clairement et les mots
pour le dire arrivent aisment (Art Potique, 1674). Ou encore, pour adopter une
approche plus pragmatique : si un programmeur a du mal commenter (expliquer,
spcifier) le fonctionnement d'un passage de code, c'est que le code est mal conu et qu'il
serait bnfique de le rcrire plus lisiblement.
Attention! Les commentaires ne devraient pas tre employs pour dsactiver certaines
parties du code. Cette technique dissuade un programmeur d'en inclure puisque le langage
C ne permet pas d'imbriquer les commentaires. Pour cela, le prprocesseur dispose
d'instructions ddies, qui permettent heureusement de dsactiver du code en laissant les
commentaires.
Le // ajoute la possiblit de placer un commentaire d'une seule ligne, partir du //
jusqu' la fin de la ligne :
// commentaire
instruction; // autre commentaire
Cependant, ces commentaires la C++ sont apparus dans C99, et ne sont pas permis
en C90.
Instructions
Les instructions se terminent par un point-virgule (;), on peut placer autant d'instructions
que l'on veut sur une mme ligne (mme si ce n'est pas conseill pour la lisibilit du code).
Les blocs d'instructions commencent par une accolade ouvrante ({) et se terminent par une
accolade fermante (}). Les instructions doivent obligatoirement tre dclares dans
une fonction : il est impossible d'appeler une fonction pour initialiser une variable globale
par exemple (contrairement au C++).
/* une instruction */
i = 1;
/* plusieurs instructions sur la mme ligne */
i = 1; j = 2; printf("bonjour\n");
/* un bloc */
{
int i;
i = 5;
}
/* l'instruction vide */
;
11
Bases du langage
Dans la mesure o le compilateur ne se soucie pas des blancs (espaces et retours la
ligne), vous pouvez formater votre code comme vous l'entendez. Il y a beaucoup de religion
concernant les styles d'indentation, mais pour faire court et viter les guerres saintes, on
ne saurait trop conseiller que d'utiliser le mme nombre de blancs par niveau d'imbrication
de bloc.
noter que l'instruction vide tant valide en C, on peut donc pratiquement rajouter autant
de point-virgules que l'on veut. Le point-virgule ne sert pas uniquement marquer la fin
des instructions, les compilateurs l'utilisent gnralement comme caractre de
synchronisation, suite une erreur dans un programme source. En fait, en gnral,
lorsqu'une erreur est dtecte, les compilateurs ignorent tout jusqu'au prochain
point-virgule. Ce qui peut avoir des consquences assez dramatiques, comme dans
l'exemple suivant :
Ce code contient une erreur volontaire !
int traite_arguments( int nb, char * argv[] )
{
/* ... */
return 0
}
int main( int nb, char * argv[] )
{
int retour;
retour = traite_arguments( nb, argv );
/* ... */
}
On notera l'absence de point-virgule la fin de l'instruction return la fin de la fonction
traite_arguments. Ce que la plupart des compilateurs feront dans ce cas sera d'ignorer
tout jusqu'au prochain point-virgule. On se rend compte du problme : on a saut une
dclaration de fonction (avec ses deux paramtres) et une dclaration de variable. Ce qui
veut dire qu'une cascade d'erreurs va suivre suite l'oubli... d'un seul caractre (;) !
Dclarations de variables
T var1, var2, ..., varN;
Cette ligne dclare les variables var1, var2, ..., varN de type T. Une variable est dite locale,
si elle est dfinie l'intrieur d'une fonction et globale si dfinie en-dehors.
Par exemple:
int jour; /* 'jour' est une variable globale, de type entier */
int main(void)
{
double prix; /* 'prix' est une variable locale la fonction
'main', de type rel */
return 0;
12
Bases du langage
}
Variables locales
Les variables locales (aussi appeles automatiques) ne sont visibles que dans le bloc dans
lequel elles sont dfinies, et n'existent que durant ce bloc. Par exemple:
Ce code contient une erreur volontaire !
int fonction(int n)
{
int i; /* i est visible dans toute la fonction */
i = n + 1;
{ /* dbut d'un nouveau bloc */
int j; /* j est visible dans le nouveau bloc *seulement* */
j = 2 * i; /* i est accessible ici */
}
i = j; /* ERREUR : j n'est plus accessible */
return i;
}
Le code prcdent accde i depuis un bloc contenu dans le bloc o i a t dfini, ce qui
est normal. Puis il essaye d'accder la valeur de la variable j en-dehors du bloc o elle
t dfinie, ce qui est une erreur.
Les variables locales ne sont pas initialises automatiquement et contiennent
donc, aprs leur dclaration, une valeur alatoire. Utiliser une telle valeur peut causer
un comportement alatoire du programme. Le programme suivant essaye d'afficher la
valeur d'une telle variable, et on ne peut savoir l'avance ce qu'il va afficher. Si on le lance
plusieurs fois, il peut afficher plusieurs fois la mme valeur aussi bien qu'il peut afficher
une valeur diffrente chaque excution:
Ce code contient une erreur volontaire !
#include <stdio.h>
int main(void)
{
int n;
printf("La variable n vaut %d\n", n);
return 0;
}
Avant la normalisation ISO C99, les dclarations de variables locales devaient
obligatoirement tre places juste au dbut d'un bloc et s'arrtaient la premire
instruction rencontre. Si une dclaration est faite au-del, une erreur sera retourne.
Suivant la norme C99, les dclarations peuvent se trouver n'importe o dans un bloc. Par
exemple, le code suivant est correct suivant la norme C99, mais pas suivant la norme C90,
car la variable a est dfinie aprs l'instruction puts("Bonjour!");:
int ma_fonction(int n)
{
puts("Bonjour !");
13
Bases du langage
int a; /* Valide en C99, invalide en C90 */
/* autre chose... */
return a;
}
Variables globales
Une attention particulire doit tre porte aux variables globales, souvent source de
confusion et d'erreur. Utilisez des noms explicites, longs si besoin et limitez leur usage au
seul fichier o elles sont dclares, en les dclarants statiques.
Les variables globales sont initialises avant que la fonction main s'excute. Si le
programmeur ne fournit pas de valeur initiale explicitement, chaque variable reoit une
valeur par dfaut suivant son type:
un nombre (entier, rel ou complexe) est initialis 0;
les pointeurs sont initialiss NULL;
pour une structure, l'initialisation se fait rcursivement, chaque membre tant initalis
suivant les mmes rgles;
pour une union, le premier membre est initialis suivant ces rgles.
Pour initialiser explicitement une variable globale, on ne peut utiliser que des constantes;
en particulier, on ne peut appeler de fonction, contrairement d'autres langages.
Ce code contient une erreur volontaire !
int jour_courant(void)
{
/* retourne le n du jour courant */
}
int jour = jour_courant(); /* ERREUR ! */
int main(void)
{
/* ... */
return 0;
}
Le code prcdent ne pourra pas compiler, car on ne peut appeler directement
jour_courant() pour initialiser jour. Si vous avez besoin d'un tel comportement, une
solution est de faire l'initalisation explicite dans main:
int jour_courant(void)
{
/* retourne le n du jour courant */
}
int jour; /* 'jour' est initialis 0 *avant* le dbut de 'main' */
int main(void)
{
jour = jour_courant();
14
Bases du langage
/* ... */
return 0;
}
Types de base
Le C est un langage typ statiquement : chaque variable, chaque constante et chaque
expression, a un type dfini la compilation. Le langage lui-mme fournit des types
permettant de manipuler des nombres (entiers, rels ou complexes) ou des caractres
(eux-mmes tant manipuls comme des entiers spciaux), et permet de construire des
types plus complexes partir de ces premiers, par exemple en groupant des donnes de
mme type en tableaux, ou des donnes de types diffrents dans des structures. Dans ce
chapitre, nous tudierons les types de base fournis par le C, l'tude des types complexes
tant faite dans la suite du livre.
Entiers
Il y a cinq types de variables entires (integer en anglais) :
char ;
short int, ou plus simplement short ;
int ;
long int, ou long ;
long long int, ou long long (ce type a t ajout depuis la norme C99).
Comme voqu en introduction, le type caractre char est particulier, et sera tudi en
dtail plus bas.
Les types entiers peuvent prendre les modificateurs signed et unsigned qui permettent
respectivement d'obtenir un type sign ou non sign. Ces modificateurs ne changent pas la
taille des types. Le langage ne dfinit pas exactement leur tailles, mais dfinit un domaine
de valeurs minimal pour chacun.
La norme C tient compte des anciennes reprsentations des nombres signs telles que le
signe+valeur absolue et le complment 1. Ces deux reprsentations sont brivement
15
Types de base
16
expliques ci-dessous :
Signe+valeur absolue (SVA)
Un bit contient le signe du nombre (par ex: 0 pour +, 1 pour -), les autres bits sont
utiliss pour la valeur absolue. On peut donc reprsenter 0 de deux manires : +0
(000...000) ou -0 (100..000). Sur N bits on peut donc reprsenter tout nombre entre
-(2N-1) (111...111) et +(2N-1) (011...111).
Complment 1 (CPL1)
Les bits des nombres ngatifs sont inverss. On peut donc reprsenter 0 de deux
manires : +0 (000...000) ou -0 (111..111). Sur N bits on peut donc reprsenter tout
nombre entre -(2N-1) (100...000) et +(2N-1) (011...111).
Ces deux reprsentations peuvent reprsenter la valeur nulle de deux manires diffrentes.
La reprsentation moderne des nombres ngatifs utilise le complment 2 (CPL2) qui
consiste reprsenter les nombres ngatifs comme le complment 1 et en ajoutant 1. Sur
un nombre fixe de bits, la valeur 0 n'a qu'une seule reprsentation : +0 (000...000) et -0
(111...111 + 1 = (1)000...000) ont deux reprsentations identiques. Sur N bits on peut donc
reprsenter tout nombre entre -(2N) (100...000) et +(2N-1) (011...111). Cette reprsentation
possde donc un domaine plus large.
Le tableau ci-dessous donne le domaine des valeurs quel que soit la reprsentation utilise
(SVA, CPL1 ou CPL2) :
Borne
infrieure
(formule)
Borne infrieure
Borne suprieure
Borne
suprieure
(formule)
signed char
-127
-(27-1)
+127
27-1
unsigned char
+255
28-1
short
-32767
-(215-1)
+32767
215-1
unsigned short
+65535
216-1
int
-32767
-(215-1)
+32767
215-1
unsigned int
+65535
216-1
long
-2147483647
-(231-1)
+2147483647
231-1
unsigned long
+4294967295
232-1
-9223372036854775807
-(263-1)
+9223372036854775807
263-1
unsignedlonglong
(C99)
+18446744073709551615
264-1
Cette table signifie qu'un programme peut utiliser sans problme une variable de type int
pour stocker la valeur 215-1, quelque soit le compilateur ou la machine sur laquelle va
tourner le programme.
Par contre, une implmentation C peut fournir des domaines de valeurs plus larges que
ceux indiqus au-dessus :
Types de base
17
Les domaines indiqus pour les nombres signs dans le tableau prcdent sont ceux
d'une implmentation par complment 1, ou par signe et valeur absolue. Pour le
complment 2, la borne infrieure est de la forme -2N, ce qui autorise une valeur
supplmentaire (ex: int de -215 +215-1, soit de -32768 +32767),
un int implment sur 32 bits pourrait aller de -(231-1) 231-1, par exemple[1] , et un
programme tournant sur une telle implmentation peut alors utiliser ces valeurs, mais il
perdrait en portabilit.
Borne
infrieure
(formule)
Borne infrieure
Borne suprieure
Borne
suprieure
(formule)
signed char
-128
-(27)
+127
27-1
unsigned char
+255
28-1
short
-32768
-(215)
+32767
215-1
unsigned short
+65535
216-1
int
-32768
-(215)
+32767
215-1
unsigned int
+65535
216-1
long
-2147483648
-(231)
+2147483647
231-1
unsigned long
+4294967295
232-1
-9223372036854775808
-(263)
+9223372036854775807
263-1
unsignedlonglong
(C99)
+18446744073709551615
264-1
Par ailleurs, une relation d'ordre entre ces domaines de valeurs est garantie ; qui peut tre
exprime ainsi :
domaine(char) domaine(short) domaine(int) domaine(long)
domaine(long long)
Cela signifie que toutes les valeurs possibles pour une variable du type char sont aussi
utilisables pour les autres types; mais aussi que, par exemple, une valeur valide pour le
type int peut ne pas tre reprsentable dans une variable de type short.
Si vous ne savez pas quel type donner une variable de type entier, le type int est par
dfaut le meilleur choix ( condition que votre donne ne dpasse pas 215-1) : ce type est la
plupart du temps reprsent au niveau matriel par un mot machine , c'est--dire qu'il
est adapt la taille que la machine peut traiter directement (il fait usuellement 32 bits sur
un PC 32 bits, par exemple). Cela permet un traitement plus rapide par le matriel. De plus,
beaucoup de bibliothques (que ce soit celle fournie par le langage C ou d'autres) utilisent
ce type pour passer des entiers, ce qui fait que l'utilisation de ces bibliothques sera plus
aise.
Par ailleurs, un utilisateur peut connatre les domaines de valeurs exacts de sa machine en
utilisant l'en-tte <limits.h>.
Types de base
18
Types de base
U peut tre combin L et LL pour obtenir les types unsigned long et unsigned long
long, respectivement. Lorsqu'une constante est suffixe, mais que sa valeur est trop grande
pour le type demand, le mme processus de recherche de type assez grand est
utilis[2] .
Dbordement
Sur une machine donne, un type entier a un domaine de valeurs fixe. Considrons qu'on
travaille sur un PC en 32 bits, en complment 2 : sur un tel ordinateur, le type int varie
souvent de -231 231-1. Cela permet de manipuler sans problme des valeurs dans ce
domaine. Par contre, si on utilise des valeurs hors du domaine, par exemple 232, et qu'on
essaye de la stocker dans une variable de type int sur une telle machine, que se passe-t-il
?
La rponse dpend du type:
Si on essaye d'enregistrer une valeur hors domaine dans une variable de type sign
(comme dans l'exemple), la conversion n'est pas dfinie par le langage. Cela signifie que
tout peut arriver.
Si on essaye d'enregistrer une valeur hors domaine dans une variable de type non sign
(unsigned long, par exemple), la conversion se fait modulo la valeur maximale
reprsentable par ce type + 1.
Exemples : On suppose que le type unsigned char est cod sur 8 bits. Alors une valeur de
ce type peut aller de 0 28-1, soit 255. Par la rgle prcdente, les conversions se feront
donc modulo 255 + 1, soit 256. On considre le programme suivant :
#include <stdio.h>
int main(void)
{
unsigned char c = 300;
/* %hhu sert dire printf() qu'on veut afficher un unsigned char
*/
printf("La variable c vaut %hhu.\n", c);
c = -5;
printf("La variable c vaut %hhu.\n", c);
return 0;
}
Le rsultat du programme est :
La variable c vaut 44.
La variable c vaut 251.
En effet, on a 300 - 256 = 44 et -5 + 256 = 251.
De mme :
#include <stdio.h>
int main(void)
{
signed char c = 300;
19
Types de base
/* %hhd sert dire printf() qu'on veut afficher un signed char */
printf("La variable c vaut %hhd.\n", c);
c = -300;
printf("La variable c vaut %hhd.\n", c);
return 0;
}
Le rsultat du programme peut alors tre :
La variable c vaut 44.
La variable c vaut -44.
Sur une telle machine, les types signs sont traits de la mme manire que les types non
signs.
Si vous essayez de compiler ces deux programmes, votre compilateur pourra dtecter les
dbordements et vous en avertir (GCC le fait).
Rels
Les rels sont approxims par des nombres virgule flottante. Comme dans le cas des
entiers, il existe plusieurs types de nombre virgule flottante. En voici la liste trie par
prcision croissante :
float ;
double ;
long double.
La norme C90 tait assez floue concernant les nombres virgule flottante, leurs
reprsentations, la prcision des oprations, etc., ce qui fait que c'tait un des domaines o
la conception de programmes utilisant les nombres flottants tait chose peu aise. Le C99 a
clarifi les choses en prcisant qu'une implmentation C devait respecter la norme IEC
60559:1989 Arithmtique binaire en virgule flottante pour systmes
microprocesseur. Cette norme (drive de IEEE 754) dfinit des formats de donnes pour
les nombres virgule flottante, ainsi que des oprations et fonctions sur ces nombres. Elle
garantit, entre autres :
que certains types de donnes auront toujours le mme format ;
et que les calculs effectus sur un type de donne donneront toujours le mme rsultat.
Elle dfinit de plus comment sont grs les cas exceptionnels, comme les infinis, les NaN
(pour Not a Number, rsultant par exemple de la division 0/0), etc.
Les types flottants du C correspondent aux type IEC 60559 de la manire suivante :
float : simple prcision
double : double prcision
long double : suivant l'implmentation, soit la double prcision tendue, soit un type
non-IEC 60559 (mais de prcision au moins gale double), soit double prcision.
20
Types de base
Constantes relles
Une suite de caractres reprsente une constante virgule flottante si :
c'est une suite de chiffres spare par un caractre point , cette sparation pouvant
s'effectuer n'importe quel endroit de la suite (0.0, .0 et 0. reprsentent tous les trois
la valeur 0 de type double) ;
un nombre suivi d'un caractre e suivi d'un entier.
Dans le deuxime cas, le nombre peut tre soit un entier, soit un rel du premier cas.
Les constantes sont de type double par dfaut. Pour demander le type float, il faut la
suffixer par f ou F, et pour le type long double par l ou L.
Arithmtique
Une attention particulire doit tre porte sur la prcision des types rels. Ces diffrents
types ne font qu'approximer l'ensemble des nombres rels, avec une prcision finie. Des
erreurs d'arrondis sont prvoir, ce qui est trs problmatique pour des domaines qui n'en
tolrent pas (notamment pour les applications financires, il est conseill de ne pas utiliser
de calcul en virgule flottante).
Le type float en particulier a une prcision minimale, qui est bien souvent insuffisante.
Voici un exemple classique d'erreur ne pas faire qui illustre les problmes lis la
prcision des types flottants :
#include <stdio.h>
int main(void)
{
float i = 0;
int j;
for (j = 0; j < 1000; j ++)
{
i += 0.1;
}
printf("i = %f\n", i);
return 0;
}
Le rsultat est 99,999046, ce qui montre que la prcision du type float est en gnral
mauvaise, d'autant plus que le nombre 0,1 n'est pas reprsentable en binaire. Il est ainsi
conseill d'utiliser le type double la place de float autant que possible. Dans ce cas de
figure, il est prfrable d'viter les accumulations d'erreurs infinitsimales, en rcrivant le
code de la manire suivante :
#include <stdio.h>
int main(void)
{
int j;
for (j = 0; j < 1000; j++)
{
21
Types de base
float i = j * 0.1;
}
return 0;
}
C'est probablement plus coteux, car on effectue un millier de multiplications au lieu d'un
millier d'additions, mais cela permet d'avoir une prcision nettement meilleure que le code
prcdent.
Pour plus d'informations sur ce domaine, un wikilivre Arithmtique flottante est disponible.
Caractres
Le type permettant de reprsenter un caractre est char. Ce type est un peu plus
particulier que les autres, d'une part parce que sa taille dfinit l'unit de calcul pour les
quantits de mmoire (et donc pour les tailles des autres types du langage) et d'autre part
son domaine de valeur peut grandement varier de manire relativement inattendue.
Par dfinition, la taille du type char, note sizeof(char), vaut toujours 1. Cependant, il
faut faire attention : contrairement ce qu'on pense souvent, un char au sens du C ne vaut
pas toujours un octet. Il occupera au minimum 8 bits, mais il existe des architectures,
relativement spcialises il est vrai, ayant des char de 9 bits, de 16 bits, voire plus. Mme
si, dans une large majorit des cas, les compilateurs utilisent des char de 8 bits, la fois
par simplicit (les machines modernes fonctionnent gnralement en 8, 16, 32 ou 64 bits)
et pour viter des problmes de portabilit de code (beaucoup de codes C existants
reposent sur l'hypothse que les char font 8 bits, et risqueraient de ne pas marcher sur
une telle architecture)[3] . Par simplification, nous utiliserons donc le terme octet la plupart
du temps dans la suite de ce wikilivre.
Un autre pige de ce type est qu'il peut tre de base signed ou unsigned, au choix du
compilateur, ce qui peut s'avrer dangereux. Considrez le code suivant:
/* Ce code peut ne pas fonctionner avec certains compilateurs */
char i;
for (i = 100; i >= 0; i --)
{
/* ... */
}
Dans cet exemple, l'instruction for permet de faire itrer les valeurs entires de i de 100
0, incluses. On pourrait navement penser optimiser en utilisant un type char. Sauf que
si ce type est implicitement unsigned, la condition i >= 0 sera toujours vraie, et tout ce
que vous obtiendrez est une boucle infinie. Normallement tout bon compilateur devrait
vous avertir que la condition est toujours vraie et donc vous permettre de corriger en
consquence, plutt que perdre des heures en dbugage.
22
Types de base
23
Caractres spciaux
Il existe certaines constantes caractres aux significations particulires:
Constante
Caractre
'\''
une apostrophe
'\"'
un guillemet
'\?'
un point d'interrogation
'\\'
un backslash
'\a'
'\b'
un espace arrire
Types de base
24
'\f'
'\n'
saut de ligne
'\r'
un retour chariot
'\t'
une tabulation
'\v'
De plus, on peut crire n'importe quelle valeur de caractre avec les expressions suivantes:
'\xHH', o chaque H reprsente un chiffre hexadcimal correspondant au code du
caractre. Par exemple, '\x61' reprsente le caractre 'a' (minuscule) en ASCII (car
97 = 6 * 16 + 1);
'\oOO', o chaque O reprsente un chiffre octal correspondant au code du caractre.
Trigraphe
Cette fonctionnalit obscure et peu employe est en gnral viter, car trs souvent
inutile, et peut causer des bugs incomprhensibles au programmeur non averti. Nanmoins,
de trs rares programmes peuvent utiliser ce genre de fonctionnalits. Un trigraphe est
simplement une suite de trois caractres dans le code source qui sera remplace par un
seul ; cette fonctionnalit a t ajoute au C pour supporter les architectures dont
l'alphabet ne dispose pas de certains caracres qui sont ncessaires dans la syntaxe du C,
comme les dises ou les accolades.
Les substitutions suivantes sont faites partout dans le code source (y compris les chanes de
caractres):
Trigraphe
Caractre
??=
??(
??)
??<
??>
??/
??'
??!
??-
Types de base
Chane de caractres
Une chane de caractre, comme son nom l'indique, est une suite de caractres avec la
particularit d'avoir un caractre nul (0) la fin. Une chane de caractre est en fait
implmente en C avec un tableau de type char.
Sous sa forme la plus simple, on dclare une chane comme une suite de caractres entre
guillemets (double quote) :
"Ceci est une chane de caractre";
""; /* Chane vide */
Si la chane est trop longue, on peut aussi la couper sur plusieurs lignes :
"Ceci est une chane de caractre, " /* pas de ; */
"dclare sur plusieurs lignes.";
Deux chanes ctes ctes (modulo les espaces et les commentaires) seront concatnes
par le compilateur. De plus on peut utiliser le caractre barre oblique inverse (\) pour
annuler la signification spciale de certains caractres ou utiliser des caractres spciaux
(C.f liste ci-dessus).
"Une chane avec des \"guillemets\" et une barre oblique (\\)\n";
Il est aussi possible d'utiliser la notation hexadcimale et octale pour dcrire des caractres
dans la chane, il faut nanmoins faire attention avec la notation hexadcimale, car la
dfinition peut s'tendre sur plus de 2 caractres. En fait, tous les caractres suivant le \x
et appartenant l'ensemble "0123456789abcdefABCDEF" seront utiliss pour dterminer la
valeur du caractre (du fait que les caractres d'une chane peuvent faire plus qu'un octet).
Cela peut produire certains effets de bords comme:
/* Ce code contient un effet de bord inattendu */
"\x00abcdefghijklmnopqrstuvzxyz"
Ce code n'insrera pas un caractre 0 au dbut de la chaine, mais toute la squence
"\x00abcdef" servira calculer la valeur du caractre (mme si le rsultat sera tronqu
pour tenir dans un type char). On peut viter cela en utilisant la concatnation de chanes
constantes :
"\x00" "abcdefghijklmnopqrstuvzxyz"
Enfin, les chanes de caractres faisant appel aux concepts de pointeur, tableau et de zone
mmoire statique, leur utilisation plus pousse sera dcrite dans la section ddie aux
tableaux.
noter la reprsentation obsolte des chanes de caractres multilignes. viter dans la
mesure du possible :
/* Ce code est obsolete et a eviter */
"Ceci est une chane de caractre,\
dclare sur plusieurs lignes";
25
Types de base
Boolens
Le langage (jusqu' la norme C99) ne fournit pas de type boolen. La valeur entire 0 prend
la valeur de vrit faux et toutes les autres valeurs entires prennent la valeur de vrit
vrai.
La norme C99 a introduit le type _Bool, qui peut contenir les valeurs 0 et 1. Elle a aussi
ajout l'en-tte <stdbool.h>, qui dfinit le type bool qui est un raccourci pour _Bool, et
les valeurs true et false[5] . Nanmoins, ces nouveauts du C99 ne sont pas trs utilises,
les habitudes ayant t prises d'utiliser 0 et diffrent de zro pour les boolens en C.
Nota : toute expression utilisant des oprateurs boolens (voir oprateurs), retourne 1 si
l'expression est vraie et 0 si elle est fausse, ce qui rend quasiment inutile l'usage du type
boolen.
Vide
En plus de ces types, le langage C fournit un autre type, void qui reprsente rien, le vide.
Il n'est pas possible de dclarer une variable de type void. Nous verrons l'utilit de ce type
lorsque nous parlerons de fonctions et de pointeurs.
Rfrences
[1] La norme C contraint les domaines de valeurs des types signs entre -(2N-1) et 2N-1, o N est un entier
quelconque, et les types non signs entre 0 et 2N-1.
[2] La liste des types successifs utiliss pour dterminer le type d'une constante entire a chang entre C90 et
C99. Cela n'a, quasiment tout le temps, aucune incidence, sauf pour le cas d'une constante de valeur trop
grande pour le type long : en C90, le type unsigned long est utilis, alors qu'en C99 le type long long sera
utilis. Une telle valeur sera alors signe dans un cas, et non signe dans l'autre. Dans ce cas, utiliser un suffixe
adapt (UL, LL ou ULL) permet de s'assurer du type de la constante.
[3] La confusion est surtout ne de l'utilisation du mot byte pour dsigner la taille du type char dans la norme,
alors que ce mot est utilis dans presque tous les autres domaines de l'informatique pour dsigner un octet.
[4] En-dehors des encodages compatibles avec ASCII, on ne rencontre gure en pratique que la famille de jeux de
caractres EBCDIC, utilise sur des systmes IBM et quelques mainframes.
[5] Ces trois expressions sont dfinies sous formes de macro pour le prprocesseur.
26
Classe de stockage
Classe de stockage
Il est possible de construire des types drivs des types de base du langage en utilisant
plusieurs combinaisons, deux tant illustres dans ce chapitre: les classes de stockage et
les qualificateurs.
Classe de stockage
Le langage C permet de spcifier, avant le type d'une variable, un certain nombre de
classes de stockage :
auto : pour les variables locales ;
extern : dclare une variable sans la dfinir ;
register : demande au compilateur de faire tout son possible pour utiliser un registre
processeur pour cette variable ;
static : rend une dfinition de variable persistante.
Les classes static et extern sont, de loin, les plus utilises. register est d'une utilit
limite, et auto est maintenant obsolte.
Une variable, ou un paramtre de fonction, ne peut avoir qu'au plus une classe de stockage.
Classe 'static'
L'effet de la classe 'static' dpend de l'endroit o l'objet est dclar :
Objet local une fonction : la valeur de la variable sera persistante entre les diffrents
appels de la fonction. La variable ne sera visible que dans la fonction, mais ne sera pas
rinitialise chaque appel de la fonction. L'intrt est de garantir une certaine
encapsulation, afin d'viter des usages multiples d'une variable globale. Qui plus est, cela
permet d'avoir plusieurs fois le mme nom, dans des fonctions diffrentes.
Exemple :
#include <stdio.h>
void f(void)
{
static int i = 0; /* i ne sera initialis 0 qu'au premier appel
de f */
int j = 0; /* j sera initialis chaque fois */;
i++;
j++;
printf("i vaut %d et j vaut %d.\n", i, j);
}
int main(void)
{
f();
f();
f();
return 0;
27
Classe de stockage
}
Objet global et fonction : comme une variable globale est dj persistante, le mot-cl
static aura pour effet de limiter la porte de la variable ou de la fonction au seul fichier
o elle est dclare, toujours dans le but de garantir un certain niveau d'encapsulation.
faire...
utilisation de static en C99 pour les tableaux en paramtres de fonctions
Classe 'extern'
extern permet de dclarer une variable sans la dfinir. C'est utile pour la compilation
spare, pour dfinir une variable ou une fonction dans un fichier, en permettant des
fonctions contenues dans d'autres fichiers d'y accder.
Toutes les variables globales et fonctions qui ne sont pas dclares (ou dfinies) static
sont externes par dfaut.
static et extern sont employs pour distinguer, dans un fichier C, les objets et fonctions
publics , qui pourront tre accessibles depuis d'autres fichiers, de ceux qui sont privs
et ne doivent tre utiliss que depuis l'intrieur du fichier.
Classe 'register'
Indique que la variable devrait tre stocke dans un registre du processeur. Cela permet de
gagner en performance par rapport des variables qui seraient stockes dans un espace
mmoire beaucoup moins rapide, comme une pile place en mmoire vive.
Ce mot-cl a deux limitations principales :
Les registres du processeur sont forcment limits, aussi bien en nombre qu'en taille. Il
est donc inutile de dclarer une structure entire ou un tableau avec le mot cl register.
Qui plus est, les variables places dans des registres sont forcment locales des
fonctions ; on ne peut pas dfinir une variable globale en tant que registre.
Aujourd'hui, ce mot-cl est dconseill sauf pour des cas particuliers, les compilateurs
modernes sachant gnralement mieux que le programmeur comment optimiser et quelles
variables placer dans les registres.
#include <stdio.h>
int main(void)
{
register short i, j;
for (i = 1; i < 1000; ++i)
{
for(j = 1; j < 1000; ++j)
{
printf("\n %d %d", i, j);
}
}
return 0;
28
Classe de stockage
}
Classe 'auto'
Cette classe est un hritage du langage B. En C, ce mot-cl sert pour les variables locales
une fonction non-statiques, dites aussi automatiques. Mais une variable dclare
localement une fonction sans qualificateur static tant implicitement automatique, ce
mot-cl est inutile en C.
Qualificateurs
Le C dfinit trois qualificateurs pouvant influer sur une variable :
const : pour dfinir une variable dont la valeur ne devrait jamais changer ;
restrict : permet une optimisation pour la gestion des pointeurs ;
volatile : dsigne une variable pouvant tre modifie notamment par une source
externe indpendante du programme.
Une variable, ou un paramtre de fonction, peut avoir aucun, un, deux, ou les trois
qualificateurs (certaines combinaisons n'auraient que peu de sens, mais sont autorises).
Qualificateur 'const'
La classe const ne dclare pas une vraie constante, mais indique au compilateur que la
valeur de la variable ne doit pas changer. Il est donc impratif d'assigner une valeur la
dclaration de la variable, sans quoi toute tentative de modification ultrieure entrainera
une erreur de la part du compilateur :
Ce code contient une erreur volontaire !
const int i = 0;
i = 1; /* erreur*/
En fait, le mot-cl const est beaucoup plus utilis avec des pointeurs. Pour indiquer qu'on
ne modifie pas l'objet point, il est bon de spcificier le mot-cl const :
void fonction( const char * pointeur )
{
pointeur[0] = 0;
/* erreur*/
pointeur
= "Nouvelle chaine de caractres";
}
Dans cet exemple, on indique que l'objet point ne sera pas modifi. Pour indiquer que la
valeur elle-mme du pointeur est constante, il faut dclarer la variable de la sorte :
Ce code contient une erreur volontaire !
char * const pointeur = "Salut tout le monde !";
pointeur = "Hello world !"; /* erreur*/
Encore plus subtil, on peut mlanger les deux :
Ce code contient plusieurs erreurs volontaires !
const char * const pointeur = "Salut tout le monde !";
pointeur = "Hello world !"; /* erreur*/
pointeur[0] = 0; /* erreur*/
29
Classe de stockage
Cette dernire forme est nanmoins rarement usite. En outre ce dernier exemple prsente
un autre problme qui est la modification d'une chaine de caractres en dur , qui sont la
plupart du temps places dans la section lecture seule du programme et donc inaltrables.
Qualificateur 'volatile'
Ce mot-cl sert spcifier au compilateur que la variable peut tre modifie son insu.
Cela annule toute optimisation que le compilateur pourrait faire, et l'oblige procder
chaque lecture ou criture dans une telle variable tel que le programmeur l'a crit dans le
code. Ceci a de multiples utilisations :
pour les coordonnes d'un pointeur de souris qui seraient modifies par un autre
programme ;
pour la gestion des signaux (voir Gestion des signaux) ;
pour de la programmation avec de multiples fils d'excution qui doivent communiquer
entre eux ;
pour dsigner des registres matriels qui peuvent tre accds depuis un programme C
(une horloge, par exemple), mais dont la valeur peut changer indpendamment du
programme ;
etc.
On peut combiner const et volatile dans certaines situations. Par exemple :
extern const volatile int horloge_temps_reel;
dclare une variable entire, qu'on ne peut modifier partir du programme, mais dont la
valeur peut changer quand mme. Elle pourrait dsigner une valeur incrmente
rgulirement par une horloge interne.
Qualificateur 'restrict'
Introduit par C99, ce mot-cl s'applique aux dclarations de pointeurs uniquement. Avec
restrict, le programmeur certifie au compilateur que le pointeur dclar sera le seul
pointer sur une zone mmoire. Cela permettra au compilateur d'effectuer des optimisations
qu'il n'aurait pas pu deviner autrement. Le programmeur ne doit pas mentir sous peine de
problmes...
Par exemple :
int* restrict pZone;
30
Oprateurs
31
Oprateurs
Les oprateurs du C
Les oprateurs du C permettent de former des expressions, expressions qui diront quoi
faire votre programme. On peut voir un programme C comme tant compos de trois
catgories d'instructions:
Les dclarations et dfinitions (variables, fonctions, types) : dclarent et dfinissent
les objets que pourront manipuler le programme.
Les expressions : manipulent les dclarations, via les oprateurs.
Les instructions : manipulent les expressions pour leur donner une signification
particulire (test, boucle, saut, ...).
Les dclarations de variables ont en partie t dcrites dans les chapitres prcdents. Les
expressions seront en partie dcrites dans celui-ci (les oprateurs lis aux pointeurs,
tableaux, structures et fonctions seront dcrit dans des chapitres ddis), et les instructions
seront dcrites au cours des chapitres suivants.
Les expressions en C sont en fait trs gnriques, elles peuvent inclure des oprations
arithmtiques classiques (addition, soustraction, division, modulo, ...), des expressions
boolennes (OU logique, ET logique, OU exclusif, ...), des comparaisons (galit, ingalit,
diffrence, ...) et mme des affectations (copie, auto-incrment, ...). Toutes ces oprations
peuvent s'effectuer en une seule expression, il suffit d'appliquer les bons oprateurs sur les
bons oprandes.
Les oprateurs binaires et ternaires utilisent une notation infixe (l'oprateur se trouve
entre les 2 ou 3 oprandes). Les oprateurs unaires s'crivent de manire prfix (avant
l'oprande), l'exception des oprateurs ++ et -- qui peuvent s'crire de manire prfixe
ou postfixe (avec une diffrence subtile, dcrite ci-aprs).
La priorit (quel oprateur est appliqu avant, en l'absence de parenthses explicite) et
l'associativit (dans quel ordre sont traits les arguments des oprateurs ayant la mme
priorit) sont rsumes dans la table suivante (par ordre dcroissant de priorit - les
oprateurs dcrits dans un autre chapitre ont un lien ddi):
oprateur
arit
associativit
description
( )
parenthsage
() [] . ->
GD
unaire
ngation boolenne
unaire
DG
ngation binaire
++ --
unaire
DG
incrmentation et dcrmentation
unaire
DG
oppos
(type)
unaire
DG
unaire
DG
oprateur de drfrenage
&
unaire
DG
oprateur de rfrenage
Oprateurs
32
sizeof
unaire
DG
* / %
binaire
GD
+ -
binaire
GD
addition, soustraction
>> <<
binaire
GD
dcalages de bits
binaire
GD
comparaisons
== !=
binaire
GD
galit/diffrence
&
binaire
GD
et binaire
binaire
GD
ou exclusif binaire
binaire
GD
ou inclusif binaire
&&
binaire
GD
||
binaire
GD
? :
ternaire
DG
si...alors...sinon
= += -= *= /= %= ^= &= |=
>>= <<=
binaire
DG
affectation
binaire
GD
squencement
Post/pr incrmentation/dcrmentation
C'est un concept quelque peu sibyllin du C. Incrmenter ou dcrmenter de un est une
opration extrmement courante. crire chaque fois 'variable = variable + 1'
peut-tre trs pnible longue. Le langage C a donc introduit des oprateurs raccourcis
pour dcrmenter ou incrmenter n'importe quel type atomique (grable directement par le
processeur : c'est dire pas un tableau, ni une structure ou une union). Il s'agit des
oprateurs '++' et '--', qui peuvent tre utiliss de manire prfixe ou postfixe (avant ou
aprs la variable).
Utilis de manire prfixe, l'oprateur incrmente/dcrmente la variable, puis retourne la
valeur de celle-ci. En fait, les expressions (++E) et (--E) sont quivalentes respectivement
aux expressions (E+=1) et (E-=1).
Par contre, utilis de manire postfixe, l'oprateur retourne la valeur originale avant de
modifier la valeur de la variable.
int i = 0, j;
j = i++; /* j vaut 0 et i vaut 1 */
j = --i; /* j vaut -1 et i vaut -1 */
Il est important de noter que le langage ne fait que garantir qu'une variable
post-incrmente ou post-dcrmente acquiert sa nouvelle valeur entre le rsultat de
l'oprateur et le prochain point de squencement atteint (gnralement la fin d'une
instruction). Mais on ne sais pas vraiment quand. Ainsi, si l'objet sur lequel s'applique un
tel oprateur apparat plusieurs fois avant un point de squencement, alors le rsultat est
imprvisible et son comportement peut changer simplement en changeant les options d'un
Oprateurs
mme compilateur:
Ce code contient plusieurs erreurs volontaires !
/* Le code qui suit est imprvisible */
i = i++;
/* Ou de faon moins vidente */
j = f(++i, 22, i);
Promotion entire
La promotion entire, ne pas confondre avec la conversion automatique de type, fait que
tous les types plus petits ou gaux int (char, short, champs de bits, type numr) sont
convertis (promus) en int ou unsigned avant toute opration. Ainsi, dans l'exemple
suivant, a + b est calcul avec des int et le rsultat est de type int :
short sum(short a, short b) {
return a + b; /* quivaut : return (int)a + (int)b; */
}
Le compilateur peut mettre un avertissement du fait que le rsultat int est converti en
short pour tre retourn, et cette conversion peut causer une perte de prcision (par
exemple si int a une largeur de 32 bits et short de 16 bits).
La promotion se fait vers unsigned lorsqu'un int ne peut pas reprsenter le type promu.
Conversion automatique
La conversion est un mcanisme qui permet de convertir implicitement les nombres dans le
format le plus grand utilis dans l'expression.
L'exemple classique est le mlange des nombres rels avec des nombres entiers. Par
exemple l'expression '2 / 3.' est de type double et vaudra effectivement deux tiers
(0,666666...). L'expression '2 * a / 3' calculera les deux tiers de la variable 'a', et
arrondira automatiquement l'entier par dfaut, les calculs ne faisant intervenir que des
instructions sur les entiers (en supposant que 'a' soit un entier). noter que l'expression '2
/ 3 * a' vaudra... toujours zro !
Plus subtile, l'expression '2 * a / 3.', en supposant toujours que a soit un entier,
effectuera une multiplication entire entre 2 et a, puis promouvra le rsultat en rel
(double) puis effectuera la division relle avec trois, pour obtenir un rsultat rel lui-aussi.
Enfin un cas o les promotions peuvent surprendre, c'est lorsqu'on mlange des entiers
signs et non-signs, plus particulirement dans les comparaisons. Considrez le code
suivant :
/* Ce code contient un effet de bord sibyllin */
unsigned long a = 23;
signed char b = -23;
printf( "a %c b\n", a < b ? '<' : (a == b ? '=' : '>') );
De toute vidence a est suprieur b dans cet exemple. Et pourtant, si ce code est excut
sur certaines architectures, il affichera a < b, justement cause de cette conversion.
33
Oprateurs
Tout d'abord dans la comparaison a < b, le type de a est le plus grand , donc b est
promu en unsigned long. C'est en fait a le problme: -23 est une valeur ngative, et la
conversion d'une valeur ngative en un type non sign se fait modulo la valeur maximale
reprsentable par le type non sign + 1. Si la valeur maximale pour le type unsigned long
32
32
32
est 2 -1, alors -23 est converti modulo 2 , et devient 4294967273 (soit 2 -23). Dans ce
cas l effectivement 23 < 4294967273, d'o ce rsultat surprenant.
noter qu'on aurait eu le mme problme si le type de b tait signed long. En fait les
types signs sont promus en non-signs, s'il y a au moins une variable non-signes dans
l'expression, et que le type non-sign est "plus grand" que le type sign.
Dans ce cas prsent, il faut effectuer soi-mme le transtypage :
printf( "a %c b\n", (long) a < b ? '<' : ((long)a == b ? '=' : '>') );
La norme dfinit prcisment les conversions arithmtiques et les promotions entires, se
rfrer elle pour les dtails.
34
Oprateurs
}
Dans cet exemple, il y a un seul caractre = entre a et b, donc on ne teste pas si a est gal
b, mais on affecte la valeur de la variable b la variable a. Le rsultat de l'expression
tant 2, elle est donc toujours considre vraie. Ce genre de raccourci est en gnral
viter. En effet, il est facile de faire une faute de frappe et d'oublier de taper un caratre =.
Comme le code rsultant est toujours valide au sens du C, cette erreur peut ne pas tre vue
immdiatement. Pour aider les dveloppeurs dtecter ce genre d'erreurs, de nombreux
compilateurs mettent un avertissement quand un tel code leur est prsent. Une manire
de faire taire ces avertissements, une fois qu'on s'est assur qu'il ne s'agit pas d'une erreur,
est de parenthser l'affectation:
/* On veut faire une affectation ici. */
/* double parenthsage pour supprimer l'avertissement du compilateur*/
if ((a = b))
{
/* ... */
}
Comme un dveloppeur devant maintenir un programme contenant un test if (a = b) {
/* ... */ } se demandera si le dveloppeur prcdent n'a pas fait une faute de frappe, il
est prfrable d'viter autant que possible ce genre de situations et, s'il est ncessaire (ce
qui a trs peu de chances d'arriver), de la commenter. En effet, mme un code if ((a =
b)) { /* ... */ } non comment doit tre lu avec attention, car le dveloppeur
prcdent peut avoir ajout les parenthses juste pour faire taire le compilateur, sans se
rendre compte d'une erreur. Dans tous les cas, la manire la plus sre est de dcomposer
ainsi:
a = b;
if (a != 0)
{
/* ... */
}
Une autre technique classique, lorsqu'une comparaison fait intervenir une constante, est de
mettre la constante gauche. De cette manire, si la comparaison se transforme par
mgarde en affectation, cela provoquera une erreur la compilation:
if (0 == b)
{
/* Une instruction "0 = b" ne passerait pas */
}
Les oprateurs logiques de comparaisons (&& et ||, similaires smantiquement leur
quivalent binaire & et |) valuent leurs oprandes en circuit court. Dans le cas du ET
logique (&&), si l'oprande gauche s'value faux, on sait dj que le rsultat du ET sera
faux et donc ce n'est pas la peine d'valuer l'oprande droite. De la mme manire si
l'oprande gauche d'un OU logique (||) est valu vrai, le rsultat sera aussi vrai et donc
l'valuation de l'oprande droite est inutile. Ceci permet d'crire des expressions de ce
genre, sans gnrer d'exception de l'unit arithmtique du processeur :
35
Oprateurs
if (z != 0 && a / z < 10)
{
printf("Tout va bien\n");
}
Rfrences
[1] La norme C99 a ajout le type _Bool, mais celui-ci est peu utilis, l'usage des types entiers pour reprsenter
les boolens en C tant trs rpandu (voir Boolens).
Test if
Syntaxe
if (condition)
instructions
else
autre instructions
Si la condition est vrifie, alors on excute le bloc d'instructions, sinon on excute lautre
bloc. La clause else est facultative, qui plus est, une clause else' se rfre toujours
la dernire instruction if rencontre. Considrez le code, mal indent, suivant :
/* Ce code est volontairement indent de manire ambigu */
if (condition)
if (autre_condition) /* ... */ ;
else
/* ... */ ;
Dans ce dernier exemple la clause else se rapportait bien-sr au second if selon la rgle
nonce ci-dessus. Un bon compilateur devrait vous avertir que cette construction est
ambigu, et que vous devriez la rcrire :
if (condition)
{
if (autre_condition) /* ... */ ;
}
else /* ... */ ;
D'ailleurs, lorsqu'un if contient une instruction (for, do, while, switch, goto, return, etc.)
il est conseill de la mettre entre accolades.
36
Test switch
Cette instruction permet de tester si une expression concide avec un certain nombre de
constantes, et d'excuter une action par dfaut dans le cas o aucune valeur ne correspond
celle de l'expression. Cela permet un traitement beaucoup plus efficace et lisible qu'une
succession de if/else imbriqus. Insistons sur le terme constante: il est en effet
impossible d'utiliser des expressions dont la valeur n'est pas connue la compilation (c'est
dire de variable dans les instructions case).
Syntaxe
switch (expression)
{
case valeur1:
bloc1
case valeur2:
bloc2
/*...*/
case valeurN:
blocN
default:
blocD
}
Compare la valeur de l'expression celles de valeur1, valeur2, ..., valeurN. En cas d'galit
entre expression et valeurI les blocs sont excuts squentiellement partir de blocI, et ce
jusqu' la fin de l'instruction switch. Si expression est gale valeur2, dans cet exemple,
les blocs bloc2 blocN et mme blocD seront excuts. Pour empcher ce comportement
on utilise l'instruction break, que l'on peut placer n'importe quel endroit pour sortir (aller
la fin) de l'instruction switch. En gnral, on retrouve plus frquemment l'instruction
switch crite de la sorte :
switch (expression)
{
case valeur1:
bloc1
break;
case valeur2:
bloc2
break;
/*...*/
case valeurN:
blocN
/* pas de break; */
default:
blocD
}
C'est en fait tellement rare de ne pas mettre de break entre les diffrents cas, qu'il est
conseill de mettre un commentaire pour les cas o cette instruction est dlibrment
37
38
omise, a permet de bien signaler au lecteur qu'il ne s'agit pas d'un oubli.
Expression conditionnelle
test ? expression_si_vrai : expression_si_faux
Exemple
#include <stdio.h>
Exemples
Exemple 1
int i;
for (i = 0 ; i < 10 ; i++)
{
printf("%d\n", i);
Boucle while
Syntaxe
while (condition)
bloc
Une boucle while excute le bloc d'instructions tant que la condition est vrifie.
Exemple
int i = 0;
while (i < 10)
{
printf("%d\n", i);
i = i + 1;
}
Cet exemple est le mme que celui vu prcdement pour la boucle for, il affiche les dix
premiers entiers partir de 0.
39
Boucle do-while
Syntaxe
do
bloc
while (condition);
Cette boucle est une lgre variante de la prcdente. Elle excute donc le bloc au moins
une fois, et ce jusqu' ce que la condition soit fausse (ou tant que la condition est vraie).
Exemple 1
#include <stdio.h>
int main(void)
{
int i = 0;
do
{
printf("%d\n", i);
i = i + 1;
} while (i < 10);
return 0;
}
Exemple identique celui de la boucle while au dessus, on affiche les nombres de 0 jusqu'
9.
Exemple 2
#include <stdio.h>
int main(void)
{
char c;
do
{
printf("Veuillez taper la lettre 'o'\n");
scanf("%c", &c);
} while (c != 'o');
return 0;
}
Cette fois on voit l'utilit de la boucle do-while, la variable est initialise dans la boucle. On
doit tester sa valeur la fin de l'itration. Tant que l'utilisateur n'aura pas tap la lettre o,
le programme redemandera de taper la lettre.
40
41
Rfrences
[1] http:/ / www. acm. org/ classics/ oct95/
Fonctions et procdures
Dfinition
Le code suivant dfinit une fonction fonction renvoyant une valeur de type type_retour
et prenant N arguments, par1 de type type1, par2 de type type2, etc.
type_retour fonction(type1 par1, type2 par2, /* ..., */ typeN parN) {
/* Dclarations de variables ... */
/* Instructions ... */
}
L'excution d'une fonction se termine soit lorsque l'accolade fermante est atteinte, soit
lorsque le mot clef return est rencontr. La valeur renvoye par une fonction est donne
comme paramtre return. Une procdure est une fonction renvoyant void, dans ce cas
return est appel sans paramtre.
Les passages des arguments aux fonctions se font toujours par valeur. Si on veut modifier
la valeur d'un argument pass en paramtre une fonction, en dehors de cette mme
fonction, il faut utiliser des pointeurs.
42
Fonctions et procdures
noter que les noms des paramtres peuvent tre omis et que la dclaration doit se
terminer par un point-virgule (;), sans quoi vous pourrez vous attendre une cascade
d'erreurs.
43
Fonctions et procdures
{
/* 3 appels de fonction en 1 instruction */
int b = fonction(5, g(), h(6));
}
Dans cet exemple, g() peut tre appele avant h(6), ou l'inverse. Si ces fonctions ont des
effets de bord (affichage de messages, modification de variables globales...), alors le
comportement du programme dpendra de l'ordre que le systme choisit. Dans ce cas, la
seule manire d'tre sr de l'ordre est de l'imposer, de la manire suivante:
int fonction(int, int, int);
int g(void);
int h(int);
void test(void)
{
/* 1 instruction par appel de fonction */
int a = g();
int b = h(6);
int c = fonction(5, a, b);
}
Dclaration
#include <stdarg.h>
void ma_fonction(type1 arg1, type2 arg2, ...)
{
}
Dans l'exemple ci-dessus, les points de suspension ne sont pas un abus d'criture, mais bel
et bien une notation C pour indiquer que la fonction accepte d'autres arguments. L'exemple
est limit deux arguments, mais il est bien sr possible d'en spcifier autant qu'on veut.
C'est dans l'unique but de ne pas rendre ambige la dclaration, qu'aucun abus d'criture
n'a t employ.
L'inclusion de l'en-tte <stdarg.h> n'est ncessaire que pour traiter les arguments
l'intrieur de la fonction. La premire remarque que l'on peut faire est qu'une fonction
nombre variable d'arguments contient au moins un paramtre fixe. En effet la dclaration
suivante est invalide :
Ce code contient une erreur volontaire !
void ma_fonction(...);
44
Fonctions et procdures
Exemple de convention
Un bon exemple de convention est la fonction printf() elle mme. Elle utilise un
spcificateur de format qui renseigne la fois le nombre d'arguments qu'on s'attend
trouver mais aussi le type de chacun. D'un autre cot, analyser un spcificateur de format
est relativement rbarbatif, et on n'a pas toujours besoin d'une artillerie aussi lourde.
Une autre faon de faire, relativement rpandue, est de ne passer que des couples (type,
objet), o type correspond un code reprsentant un type (une numration par exemple)
et objet le contenu de l'objet lui-mme (int, pointeur, double, etc.). On utilise alors un
code spcial (gnralement 0) pour indiquer la fin des arguments, ou alors un des
paramtres pour indiquer combien il y en a. Un petit exemple complet :
#include <stdio.h>
#include <stdarg.h>
enum {
TYPE_FIN, TYPE_ENTIER, TYPE_REEL, TYPE_CHAINE
};
void affiche(FILE * out, ...)
{
45
Fonctions et procdures
va_list list;
int
type;
va_start(list, out);
while ((type = va_arg(list, int)))
{
switch (type)
{
case TYPE_ENTIER: fprintf(out, "%d", va_arg(list,
int)); break;
case TYPE_REEL:
fprintf(out, "%g", va_arg(list,
double)); break;
case TYPE_CHAINE: fprintf(out, "%s", va_arg(list, char
*)); break;
}
}
fprintf(out, "\n");
va_end(list);
}
int main(int nb, char * argv[])
{
affiche(stdout, TYPE_CHAINE, "Le code ascii de 'a' est ",
TYPE_ENTIER, 'a', 0);
affiche(stderr, TYPE_CHAINE, "2 * 3 / 5 = ", TYPE_REEL, 2. * 3
/ 5, 0);
return 0;
}
L'inconvnient de ce genre d'approche est de ne pas oublier le marqueur de fin. Dans les
deux cas, il faut tre vigilant avec les conversions implicites, notamment dans le second
cas. noter que la conversion (transtypage) explicite des types en une taille infrieure
celle par dfaut (int pour les entiers ou double pour les rels) ne permet pas de
contourner la promotion implicite. Mme crit de la sorte:
affiche(stderr, TYPE_CHAINE, "2 * 3 / 5 = ", TYPE_REEL, (float) (2. * 3
/ 5), 0);
Le rsultat transmis au cinquime paramtre sera quand mme promu implicitement en
type double.
46
Fonctions et procdures
Fonction inline
Il s'agit d'une extension ISO C99, qui l'origine vient du C++. Ce mot cl doit se placer
avant le type de retour de la fonction. Il ne s'agit que d'une indication, le compilateur peut
ne pas honorer la demande, notamment si la fonction est rcursive. Dans une certaine
mesure, les fonctionnalits proposes par ce mot cl sont dj prises en charge par les
instructions du prprocesseur. Beaucoup prfreront passer par une macro,
essentiellement pour des raisons de compatibilit avec d'anciens compilateurs ne
supportant pas ce mot cl, et quand bien mme l'utilisation de macro est souvent trs
dlicat.
Le mot cl inline permet de s'affranchir des nombreux dfauts des macros, et de
rellement les utiliser comme une fonction normale, c'est dire surtout sans effets de bord.
noter qu'il est prfrable de classifier les fonctions inline de manire statique. Dans le
cas contraire, la fonction sera aussi dclare comme tant accessible de l'extrieur, et donc
dfinie comme une fonction normale.
En la dclarant static inline, un bon compilateur devrait supprimer toute trace de la
fonction et seulement la mettre in extenso aux endroits o elle est utilise. Ceci permettrait
la limite de dclarer la fonction dans un fichier en-tte, bien qu'il s'agisse d'une pratique
assez rare et donc viter. Exemple de dclaration d'une fonction inline statique :
static inline int max(int a, int b)
{
return (a > b) ? a : b;
}
La fonction main
Nous allons revenir ici sur la fonction main, prsente dans chaque programme. Cette
fonction est le point d'entre du programme. La norme dfinit deux prototypes, qui sont
donc portables:
int main(int argc, char * argv[]) { /* ... */ }
int main(void) { /* ... */ }
Le premier prototype est plus "gnral": il permet de rcuprer des paramtres au
programme. Le deuxime existe pour des raisons de simplicit, quand on ne veut pas traiter
ces arguments.
Si ces deux prototypes sont portables, une implmentation peut nanmoins dfinir un autre
prototype pour main, ou spcifier une autre fonction pour le dmarrage du programme.
Cependant ces cas sont plus rares (et souvent spcifiques du C embarqu).
47
Fonctions et procdures
La premire chane de caractres, dont l'adresse est dans argv[0], contient le nom du
programme. Le premier paramtre est donc argv[1]. Le dernier lment du tableau,
argv[argc], est un pointeur nul.
Valeur de retour
La fonction main retourne toujours une valeur de type entier. L'usage veut qu'on retourne
0 (ou EXIT_SUCCESS) si le programme s'est droul correctement, ou EXIT_FAILURE pour
indiquer qu'il y a eu une erreur (Les macros EXIT_SUCCESS et EXIT_FAILURE tant dfinies
dans l'en-tte <stdlib.h>). Il est possible par le programme appelant de rcuprer ce code
de retour, et de l'interprter comme bon lui semble.
Exemple
Voici un petit programme trs simple qui affiche la liste des paramtres passs au
programme lorsqu'il est appel:
#include <stdio.h>
int main(int argc, char * argv[])
{
int i;
for (i = 0; i < argc; i++)
printf("paramtre %i : %s\n", i, argv[i]);
return 0;
}
On effectue une boucle sur argv l'aide de argc. Enregistrez-le sous le nom params.c puis
compilez-le (cc params.c -o params). Vous pouvez ensuite l'appeler ainsi:
./params hello world ! # sous Unix
params.exe hello world ! # sous MS-DOS ou Windows
Vous devriez voir en sortie quelque chose comme ceci (paramtre 0 varie selon le systme
d'exploitation):
paramtre
paramtre
paramtre
paramtre
0
1
2
3
:
:
:
:
./params
hello
world
!
48
Fonctions et procdures
Fonctions en C pr-ANSI
Absence de prototype lors de l'appel d'une fonction
Avant la normalisation C89, on pouvait appeler une fonction sans disposer ni de sa
dfinition, ni de sa dclaration. Dans ce cas, la fonction tait implicitement dclare comme
retournant le type int, et prenant un nombre indtermin de paramtres.
/* Aucune dclaration de g() n'est visible. */
void f(void)
{
g(); /* Dclaration implicite: extern int g() */
}
cause de la grande quantit de code existant l'poque qui reposait sur ce
comportement, le C90 a conserv cette possibilit. Cependant, elle a t retire de la
norme C99, et est viter mme lorsqu'on travaille en C90.
En effet, c'est plus qu'une bonne habitude de programmation de s'assurer que chaque
fonction utilise dans un programme ait son prototype dclar avant qu'elle ne soit dfinie
ou utilise. C'est d'autant plus indispensable lorsque les fonctions sont dfinies et utilises
dans des fichiers diffrents.
Ancienne notation
titre anecdotique, ceci est la faon historique de dfinir une fonction, avant que le
prototypage ne fut utilis. Cette notation est interdite depuis C90.
type_retour fonction(par1, par2, ..., parN)
type1 par1;
type2 par2;
...
typeN parN;
{
/* Dclarations de variables ... */
/* Instructions ... */
}
Au lieu de dclarer les types l'intrieur mme de la fonction, ils sont simplement dcrits
aprs la fonction et avant la premire accolade ouvrante. noter que type_retour pouvait
tre omis, et dans ce cas valait par dfaut int.
49
Tableaux
Tableaux
Il existe deux types de tableaux : les tableaux statiques, dont la taille est connue la
compilation, et les tableaux dynamiques, dont la taille est connue l'excution. Nous ne
nous intresserons pour l'instant qu'aux tableaux statiques, les tableaux dynamiques seront
prsents avec les pointeurs.
Syntaxe
Dclaration
T tableau[N];
Dclare un tableau de N lments, les lments tant de type T. N doit tre un nombre
connu la compilation, N ne peut pas tre une variable. Pour avoir des tableaux
dynamiques, il faut passer par l'allocation mmoire.
Cette dernire restriction a t leve par la norme C99 qui a apport le type tableau
longueur variable appels Variable Length Array (VLA). il est maintenant possible de
dclarer un tableau de type VLA par :
T tableau[expr];
50
Tableaux
51
Exemples
int i;
int tableau[10]; /* dclare un tableau de 10 entiers */
for (i = 0; i < 10; i++) /* boucle << classique >> pour le parcours d'un tableau */
{
tableau[i] = i; /* chaque case du tableau reoit son indice comme
valeur */
}
Ce code contient une erreur volontaire !
int tableau[1]; /* dclare un tableau d'un entier */
tableau[10] = 5; /* accde l'lment d'indice 10 (qui ne devrait pas
exister) */
printf("%d\n", tableau[10]);
Ce deuxime exemple, non seulement peut compiler (le compilateur peut ne pas dtecter le
dpassement de capacit), mais peut aussi s'excuter et afficher le bon rsultat. Le
langage C n'impose pas une implmentation de vrifier les accs, en criture
comme en lecture, hors des limites d'un tableau; il prcise explicitement qu'un tel
code a un comportement indfini, donc que n'importe quoi peut se passer. En l'occurence,
ce code peut trs bien marcher comme on pourrait l'attendre, i.e. afficher 5, ou causer un
arrt du programme avec erreur (si la zone qui correspondrait au 11me lment du tableau
est hors de la mmoire alloue au processus, le systme d'exploitation peut dtecter une
tentative d'accs invalide une zone mmoire, ce qui peut se traduire par une erreur de
segmentation qui termine le programme), ou encore corrompre une autre partie de la
mmoire du processus (dans le cas o le pseudo-11me lment correspondrait une partie
de la mmoire du processus), ce qui peut modifier son comportement ultrieur de manire
trs difficile prvoir.
Ce code est donc un exemple d'un type de bogue trs courant en langage C. S'il est facile
dtecter dans ce code trs court, il peut tre trs difficile identifier dans du code plus
complexe, o on peut par exemple essayer d'accder un indice i d'un tableau, la valeur
de i pouvant varier suivant un flot d'excution complexe, ou alors essayer de copier le
contenu d'un tableau dans un autre, sans vrifier la taille du tableau de destination (ce
dernier cas est connu sous le nom de dbordement de tampon, dpasement de capacit, ou
encore buffer overflow en anglais). Il est donc trs important de s'assurer que tous les
accs au contenu de tableaux, en lecture comme en criture, se font dans les
limites de ses bornes.
Tableaux
52
Tableaux
Ce qui peut aussi s'crire :
int matrice[2][3] = { 1 , 2 , 3 , 4 , 5 , 6 };
noter que si on veut aussi utiliser l'adaptation dynamique, il faut quand mme spcifier
une dimension :
int identite[][3] = { { 1 , 0 , 0 } , { 0 , 1 , 0 } , { 0 , 0 , 1 } };
/* Ou plus "simplement" */
int identite[][3] = { { 1 } , { 0 , 1 } , { 0 , 0 , 1 } };
tab[10];
*p, *q;
tab;
&(tab[0]);
La dclaration int tab[10] rserve une zone mmoire suffisante pour stocker 10 variables
de type int, dsignes par tab[0], tab[1]..., tab[9]. Dans l'instruction p = tab,
l'expression tab est de type pointeur vers int, et la valeur affecte p est l'adresse de la
premire case du tableau. Cette adresse est aussi l'adresse de la variable tab[0] : dans la
seconde instruction, l'adresse de tab[0] est affecte q via l'oprateur de prise d'adresse
&. Aprs la seconde instruction, p et q ont la mme valeur, et les instructions *p = 1, *q =
1 ou tab[0] = 1 deviennent opratoirement quivalentes.
Dans chaque cas, le nom du tableau gardera le sens d'une variable de type tableau,
c'est--dire conservera son type initial :
dans l'expression &tab, la sous-expression tab conserve son type "tableau d'entiers 10
lments", et l'expression elle-mme est de type "pointeur vers tableaux d'entiers 10
lments". Le code suivant est par exemple valide :
53
Tableaux
54
*/
dans sizeof tab la taille calcule sera celle de tab considr encore comme une
variable de type "tableau d'entiers 10 lments", soit 10 fois la taille d'un int.
dans tab++ , tab-- , --tab ou ++tab , la sous-expression tab n'est ni une expression
numrique ni un pointeur, mais bien une variable de type tableau : elle n'est donc ni
incrmentable, ni dcrmentable, et l'expression globale ne sera jamais compilable,
dans l'instruction tab = expr o expr est une expression quelconque, tab est toujours
considr comme une variable de type "tableau d'entiers 10 lments"... la rgle de
conversion des noms de tableaux et les restrictions sur les conversion explicites font qu'il
est impossible que expr soit de mme type : cette affectation gnrera toujours une
erreur de typage, et ne sera jamais compilable,
dans l'expression tab.champ o champ est un nom de champ quelconque, tab est de
type tableau, i.e. ne possde aucun champ, donc cette expression ne sera jamais
compilable.
Le cas o tab est membre gauche d'une affectation montre que les valeurs de type tableau
ne sont jamais manipulables de manire directe : il est par exemple impossible d'affecter un
tableau un autre tableau - l'instruction tab_1 = tab_2 est toujours incompilable, cause
de la conversion en pointeur de tab_2 - ou de comparer structurellement deux tableaux par
une comparaison simple - dans tab_1 == tab_2 , les deux noms seront convertis en
pointeur, et la comparaison effectue sera celle des adresses.
Tableaux
55
#include <stdio.h>
int tab[] = {42};
void f(void)
{
printf("fichier1 : tab = %p\n",
tab);
}
fichier2.c
#include <stdio.h>
extern int *tab;
void f(void)
int main(int nb, char *argv[] )
{
f();
printf("fichier2 : tab = %p\n",
tab);
return 0;
}
Les valeurs affiches pour tab dans main et dans f seront en gnral distinctes. L'erreur
se situe dans la dclaration externe de tab situe dans le fichier fichier2.c. Il aurait fallu
crire la dclaration suivante, spcifiant correctement le type de tab dans fichier2.c
comme tant celui d'un tableau et non d'un pointeur (mais ne spcifiant pas sa taille, le
compilateur n'effectuant de toute manire aucun test de dpassement) :
extern int tab[];
L'absence de message d'erreur dans la compilation de la premire version de ce
programme n'est due qu' l'insuffisance de la vrification de la cohrence globale du typage
lors de la phase de liaison : il est plus gnralement possible de dclarer une variable
externe de n'importe quel autre type que son type rel, sans que l'incohrence globale du
typage soit ncessairement signale par le compilateur. Aucune norme ne spcifiant le
comportement du programme rsultant, ce comportement est, de fait, indfini.
Tableaux
for (int j=0; j< taille2; j++)
{
(void)printf("table[%d][%d] = %d\n", i, j,
table[i][j]);
}
}
}
Chanes de caractres
Comme il a t dit tout au long de cet ouvrage, les chanes de caractres sont des tableaux
particuliers. En dclarant une chane de caractres on peut soit la manipuler en tant que
pointeur soit en tant que tableau. Considrez les dclarations suivantes :
char * chaine1 = "Ceci est une chaine";
char chaine2[] = "Ceci est une autre chaine";
Bien que se manipulant exactement de la mme faon, les oprations permises sur les deux
variables ne sont pas tout fait les mmes. Dans le premier cas on dclare un pointeur sur
une chane de caractres statique, dans le second cas, un tableau (allou soit sur la pile si
la variable est dclare dans une fonction ou soit dans le segment global si la variable est
globale) de taille suffisante pour contenir tous les caractres de la chane affecte (incluant
le caractre nul).
Le second cas est donc une notation abrge pour char chaine2[] = { 'C', 'e', 'c',
'i', ' ', ..., 'e', 0 };. Dans tous les autres cas (ceux o une chane de caractres ne
sert pas initialiser un tableau), la chane dclare est statique (les donnes sont
persistantes entre les diffrents appels de fonctions), et sur certaines architectures, pour
ne pas dire toutes, elle est mme en lecture seule. En effet l'instruction chaine1[0] =
'c'; peut provoquer un accs illgal la mmoire. En fait le compilateur peut optimiser la
gestion des chanes en regroupant celles qui sont identiques. C'est pourquoi il est
prfrable de classifier les pointeurs sur chane de caractres avec le mot cl const.
On comprends aisment que si chaine2 est allou sur la pile (dclar dans une fonction), la
valeur ne pourra pas tre utilise comme valeur de retour. Tandis que la valeur de
chaine1 pourra tre retourne mme si c'est une variable locale.
56
Pointeurs
Pointeurs
Dans cette section, nous allons prsenter un mcanisme permettant de manipuler les
adresses, les pointeurs. Un pointeur a pour valeur l'adresse d'un objet C d'un type donn
(un pointeur est typ). Ainsi, un pointeur contenant l'adresse d'un entier sera de type
pointeur vers entier.
Usage et dclaration
L'oprateur & permet de connaitre l'adresse d'une variable, on dira aussi la rfrence.
Toute dclaration de variable occupe un certain espace dans la mmoire de l'ordinateur. La
rfrence permet de savoir o cet emplacement se trouve. En simplifiant l'extrme, on
peut considrer la mmoire d'un ordinateur comme une gigantesque table d'octets. Quand
on dclare une variable de type int, elle sera alloue un certain emplacement (ou dit
autrement : un indice, une adresse ou une rfrence) dans cette table. Un pointeur permet
simplement de stocker une rfrence, il peut donc tre vu comme un nombre allant de 0
la quantit maximale de mmoire dont dispose votre ordinateur (moins un, pour tre exact).
Un pointeur occupera habituellement toujours la mme taille (occupera la mme place en
mmoire), quelque soit l'objet se trouvant cet emplacement. Il s'agit en gnral de la plus
grande taille directement grable par le processeur : sur une architecture 32bits, elle sera
de 4 octets, sur une architecture 64bits, 8 octets, etc. Le type du pointeur ne sert qu'
renseigner comment sont organises les donnes suivant l'adresse rfrence par le
pointeur. Ce code, par exemple, affiche la rfrence d'une variable au format hexadcimal:
int i;
printf("%p\n", &i);
Pouvoir rcuprer l'adresse n'a d'intrt que si on peut manipuler l'objet point. Pour cela,
il est ncessaire de pouvoir dclarer des pointeurs, ou dit autrement un objet pouvant
contenir des rfrences. Pour cela on utilise l'toile (*) entre le type et le nom de la variable
pour indiquer qu'il s'agit d'un pointeur:
T * pointeur, * pointeur2, /* ..., */ * pointeurN;
Dclare les variables pointeur, pointeur2, ..., pointeurN de type pointeur vers le type T.
noter la bizarrerie du langage vouloir associer l'toile la variable et non au type, qui
oblige rpter l'toile pour chaque variable.
/* Ce code contient une dclaration volontairement confuse */
int * pointeur, variable;
Cet exemple de code dclare un pointeur sur un entier de type int et une variable de type
int. Dans un vrai programme, il est rarement possible d'utiliser des noms aussi triviaux,
aussi il est recommand de sparer la dclaration des variables de celles des pointeurs (ou
d'utiliser l'instruction typedef, qui, elle, permet d'associer l'toile au type), la lisibilit du
programme sera lgrement amliore.
Il est essentiel de bien comprendre ce qui a t dclar dans ces exemples. Chaque
pointeur peut contenir une rfrence sur un emplacement de la mmoire (un indice dans
notre fameuse table). On peut obtenir une rfrence (ou un indice) avec l'oprateur & (ou
57
Pointeurs
allouer une rfrence soit-mme avec des fonctions ddies, c.f la section suivante). Cet
oprateur transforme donc une variable de type T en un pointeur de type T *. Insistons sur
le terme variable, car videmment des expressions telles que '&23435' ou '&(2 * a / 3.)'
n'ont aucun sens, dans la mesure o les constantes et expressions du langage n'occupent
aucun emplacement susceptible d'intresser votre programme.
Ce code, par exemple, affiche la rfrence d'une variable dans un format dfini par
l'implmentation (qui peut tre hexadcimal, ou une combinaison "segment:offset", par
exemple):
int i;
printf("%p\n", &i);
Il ne faut pas oublier que, comme toutes les variables locales en C, un pointeur est
l'origine non initialis. Une bonne attitude de programmation est de s'assurer que lorsqu'il
ne pointe pas vers un objet valide, sa valeur est mise zro (ou NULL, qui est dclar entre
autre dans stdio.h).
Drfrencement
Il s'agit de l'opration la plus simple sur les pointeurs. Comme son nom l'indique, il s'agit de
l'opration rciproque au rfrencement (&). L'oprateur associ est l'toile (*), qui est
aussi utilis pour dclarer un type pointeur. Cet oprateur permet donc de transformer un
pointeur de type T *, en un objet de type T, les oprations affectant l'objet point :
int variable = 10;
int * pointeur = &variable;
*pointeur = 20; /* Positionne 'variable' 20 */
Ici, pointeur contient une adresse valide, celle de variable; son drfrencement est
donc possible. Par contre, si pointeur tait une variable locale non initialise, son
drfrencement provoquerait coup sr un arrt brutal de votre programme.
Vous obtiendrez le mme rsultat, si pointeur est initialis NULL. Cette adresse est
invalide et toute tentative de drfrencement se soldera par un arrt du programme.
58
Pointeurs
59
Arithmtique de base
L'arithmtique des pointeurs s'apparente celle des entiers, mais il est important de
comprendre la distinction entre ces deux concepts.
Les oprations arithmtiques permises avec les pointeurs sont:
Addition / soustraction d'une valeur entire un pointeur (on avance / recule d'un
nombre de cases mmoires gal la taille du type T) : le rsultat est donc un pointeur, de
mme type que le pointeur de dpart.
Il faut bien faire attention avec ce genre d'opration ne pas sortir du bloc mmoire, car le
C n'effectuera aucun test pour vous. Considrez l'exemple suivant:
/* Parcours les lments d'un tableau */
int tableau[N];
int * p;
for (p = tableau; p < &tableau[N]; p ++)
{
/* ... */
}
Normalement un tableau de N cases permet d'tre itr sur les indices allant de 0 N - 1,
inclusivement. L'expression &tableau[N] fait rfrence la case mmoire non alloue
immdiatement aprs le plus grand indice, donc potentiellement source de problme.
Toutefois, par exception pour le premier indice aprs le plus grand, C garantit que le
rsultat de l'expression soit bien dfini. Bien sr, il ne faut pas drfrencer ce pointeur.
noter qu' l'issue de la boucle, p pointera sur la N+1me case du tableau, donc hors de
l'espace allou. Le C autorise tout fait ce genre de pratique, il faut juste faire attention
ne pas dfrencer le pointeur cet endroit.
Soustraction de deux pointeurs de mme type (combien d'objet de type T y a t-il entre les
deux pointeurs) : le rsultat est donc un entier, de type ptrdiff_t.
Ce code contient
une erreur volontaire !
int
autre_tableau[3];
int
tableau[10];
int *
p
= &tableau[5];
tableau */
int *
q
= &tableau[3];
tableau */
ptrdiff_t diff1 = p - q;
ptrdiff_t diff2 = q - p;
q = &autre_tableau[2];
ptrdiff_t dif3 = p - q; /* Erreur ! */
Dans cet exemple, les deux premires soustractions sont dfinies, car p et q pointent sur
des lments du mme tableau. La troisime soustraction est indfinie, car on utilise des
adresses d'lments de tableaux diffrents.
Notons que l'oprateur [] s'applique toujours une oprande de type entier et une autre
de type pointeur. Lorsqu'on crit tableau[i], il y a en fait une conversion de tableau
Pointeurs
pointeur avec l'application de l'oprateur []. On peut donc bien sr utiliser l'oprateur []
avec un pointeur pour oprande:
int
a;
int b;
int * p = &a; /* On peut accder la valeur de 'a' via 'p[0]' ou '*p'
*/
/* p[1] est indfini - n'esprez pas accder la valeur de b depuis
l'adresse de a */
60
Pointeurs
Tableaux dynamiques
Un des intrts des pointeurs et de l'allocation dynamique est de permettre de dcider de la
taille d'une variable au moment de l'excution, comme par exemple pour les tableaux. Ainsi
pour allouer un tableau de n entiers (n tant connu l'excution), on dclare une variable
de type pointeur sur entier laquelle on alloue une zone mmoire correspondant n
entiers :
int * alloue_tableau(int n, size_t taille)
{
return malloc(n * taille);
}
/* Ailleurs dans le programme */
int * tableau = alloue_tableau(256, sizeof *tableau);
if (tableau != NULL)
{
/* oprations sur le tableau */
/* ... */
free( tableau );
}
Cet exemple alloue un tableau de 256 cases. Bien que la variable soit un pointeur, il est
dans ce cas permis d'accder aux cases de 0 255, soit entre les adresses &tableau[0] et
&tableau[255], incluses.
61
Pointeurs
for(i = 0; i < LIGNES; i++)
{
free(matrice[i]);
}
free(matrice);
62
Pointeurs
adresse, juste pour viter la copie implicite des variables autres que les tableaux. Ceci est
particulirement intressant avec les structures, puisque celles-ci ont tendance tre assez
imposantes, et cela ne nuit pas trop la lisibilit du programme.
63
Initialisation
Il y a plusieurs faons d'initialiser une variable de type structure :
En initialisant les champs un un :
struct t_mastruct {
char ch;
int nb;
double pi;
};
t_mastruct variable;
variable.ch = 'a';
variable.nb = 12345;
variable.pi = 3.141592;
Cette faon est nanmoins pnible lorsqu'il y a beaucoup de champs.
la dclaration de la variable :
struct t_mastruct {
char ch;
int nb;
float pi;
64
Manipulation
La seule opration prise en charge par le langage est la copie, lors des affectations ou des
passages de paramtres des fontions. Toutes les autres oprations sont la charge du
programmeur, notamment la comparaison d'galit (cf. section suivante).
/* 8 bits */
/* 32 bits */
/* 8 bits */
On pourrait penser que cette structure occupera 6 octets en mmoire, et pourtant, sur une
bonne partie des compilateurs, on obtiendrait une taille plus proche des 12 octets.
En fait, les compilateurs insrent quasiment toujours des octets entre les champs pour
pouvoir les aligner sur des adresses qui correspondent des mots machines. Cela est d
une limitation de la plupart des processeurs, qui ne peuvent lire des mots de plus d'un
octet que s'ils sont aligns sur un certain adressage (alors qu'il n'y a pas de contrainte
particulire pour lire un seul octet, si le type char est dfini sur 8bit).
En se reprsentant la mmoire comme un tableau continu, on peut tracer le dessin suivant:
| bloc N
| bloc N + 1
| bloc N + 2
|
---+---------------+---------------+---------------+--| a | b | c | d | e | f | g | h | i | j | k | l |
65
66
Unions
Une union et un enregistrement se dclarent de manire identique :
union type_union
{
type1 champ1;
type2 champ2;
/* ... */
typeN champN;
};
/* Dclaration de variables */
union type_union var1, var2, /* ... */ varM;
67
68
Quelques exemples
typedef
typedef
typedef
typedef
/* Utilisation */
octet nombre = 255;
matrice4_4 identite = { {1,0,0,0}, {0,1,0,0}, {0,0,1,0}, {0,0,0,1} };
ma_struct pointeur = NULL;
gestionnaire_t fonction = NULL;
Ce mot cl est souvent utilis conjointement avec la dclaration des structures, pour ne pas
devoir crire chaque fois le mot cl struct. Elle permet aussi de grandement simplifier
les prototypes de fonctions qui prennent des pointeurs sur des fonctions en argument, ou
retournent de tels types. Il est conseill dans de tels cas de dfinir un type synonyme, plutt
que de l'crire in extenso dans la dclaration du prototype. Considrez les deux
dclarations :
/* Dclaration confuse */
void (*fonction(int, void (*)(int)))(int);
/* Dclaration claire avec typedef */
typedef void (*handler_t)(int);
handler_t fonction(int, handler_t);
Les vtrans auront reconnu le prototype imbitable de l'appel systme signal(), qui
permet de rediriger les signaux (interruption, alarme priodique, erreur de segmentation,
division par zro, ...).
69
numrations
enum nom_enum { val1, val2, ..., valN };
Les symboles val1, val2, ..., valN pourront tre utiliss littralement dans la suite du
programme. Ces symboles sont en fait remplacs par des entiers lors de la compilation. La
numrotation commenant par dfaut 0, s'incrmentant chaque dclaration. Dans
l'exemple ci-dessus, val1 vaudrait 0, val2 1 et valN N-1.
On peut changer tout moment la valeur d'un symbole, en affectant au symbole, la valeur
constante voulue (la numrotation recommenant ce nouvel indice). Par exemple :
enum Booleen { Vrai = 1, Faux = 0 };
/* Pour l'utiliser */
enum Booleen variable = Faux;
Ce qui est assez pnible en fait, puisqu'il faut chaque fois se souvenir que le type Booleen
est driv d'une numration. Il est prfrable de simplifier les dclarations, grce
l'instruction typedef :
typedef enum { Faux, Vrai } Booleen;
/* Pour l'utiliser */
Booleen variable = Faux;
Type incomplet
Pour garantir un certain degr d'encapsulation, il peut tre intressant de masquer le
contenu d'un type complexe, pour viter les usages trop optimiss de ce type. Pour cela,
le langage C permet de dclarer un type sans indiquer explicitement son contenu.
struct ma_structure;
/* Plus loin dans le code */
struct ma_structure * nouvelle = alloue_objet();
Les diffrents champs de la structure n'tant pas connus, le compilateur ne saura donc pas
combien de mmoire allouer. On ne peut donc utiliser les types incomplets qu'en tant que
pointeur. C'est pourquoi, il est pratique d'utiliser l'instruction typedef pour allger les
critures :
typedef struct ma_structure * ma_struct;
/* Plus loin dans le code */
ma_struct nouvelle = alloue_objet();
Cette construction est relativement simple comprendre, et proche d'une conception objet.
noter que le nouveau type dfini par l'instruction typedef peut trs bien avoir le mme
nom que la structure. C'est juste pour viter les ambiguits, qu'un nom diffrent a t
choisi dans l'exemple.
Un autre cas de figure relativement classique, o les types incomplets sont trs pratiques,
ce sont les structures s'auto-rfrenant, comme les listes chaines, les arbres, etc.
70
71
Prprocesseur
Prprocesseur
Le prprocesseur est un langage de macro qui est analys, comme son nom l'indique, avant
la compilation. En fait, c'est un langage compltement indpendant, il est mme
thoriquement possible de l'utiliser par dessus un autre langage que le C. Cette
indpendance fait que le prprocesseur ignore totalement la structure de votre programme,
les directives seront toujours values de haut en bas.
Ces directives commencent toutes par le symbole dise (#), suivi d'un nombre quelconque
de blancs (espace ou tabulation), suivi du nom de la directive en minuscule. Les directives
doivent tre dclares sur une ligne ddie. Les noms standards de directives sont :
permet de substituer presque partout dans le code source qui suit cette ligne la suite de
caractres CONSTANTE par la valeur. Plus prcisment, la substitution se fait partout,
l'exception des caractres et des chaines de caractres. Par exemple, dans le code suivant
:
#define TAILLE 100
printf("La constante TAILLE vaut %d\n", TAILLE);
La substitution se fera sur la deuxime occurrence de TAILLE, mais pas la premire. Le
prprocesseur transformera ainsi l'appel printf :
printf("La constante TAILLE vaut %d\n", 100);
Le prprocesseur procde des traitements sur le code source, sans avoir de connaissance
sur la structure de votre programme. Dans le cas des variables de substitution, il ne sait
faire qu'un remplacement de texte, comme le ferait un traitement de texte. On peut ainsi
les utiliser pour n'importe quoi, des constantes, des expressions, voire du code plus
complexe ; le prprocesseur n'ira pas vrifier si la valeur de remplacement est une
expression C ou non. Dans l'exemple prcdent, nous avons utilis TAILLE pour remplacer
la constante 100, mais toute suite de caractres peut tre dfinie. Par exemples:
72
Prprocesseur
#define PREFIXE "erreur:"
#define FORMULE (1 + 1)
printf(PREFIXE "%d\n", FORMULE);
Ici, l'appel printf sera transform ainsi :
printf("erreur:" "%d\n", (1 + 1));
Une dfinition de constantes peut s'tendre sur plusieurs lignes. Pour cela, il faut que le
dernier caractre de la ligne soit une barre oblique inverse ('\'). On peut ainsi dfinir une
macro qui est remplace par un bout de code plus complexe :
#define BLOC_DEFAUT
\
default:
\
puts("Cas interdit."); \
break;
Cette variable de substitution permet ainsi d'ajouter le cas par dfaut un switch.
Historiquement, les programmeurs avaient pour habitude d'utiliser des capitales pour
distinguer les dclarations du prprocesseur et les minuscules pour les noms de symboles
(fonctions, variables, champs, etc.) du compilateur. Ce n'est pas une rgle suivre
imprativement, mais elle amliore la lisibilit des programmes.
Il est possible de dfinir plusieurs fois la mme CONSTANTE . Le compilateur n'mettra
un avertissement que si les deux valeurs ne concordent pas.
En fait, dans la section prcdente, il tait fait mention d'un mcanisme relativement
similaire : les numrations. On peut lgitimement se demander ce que ces numrations
apportent en plus par rapport aux directives du prprocesseur. En fait, on peut
essentiellement souligner que :
Lors des cas multiples (switch), le compilateur peut vrifier que l'ensemble des cas
couvre l'intervalle de valeur du type, et mettre un avertissement si ce n'est pas le cas.
Ce qui est videmment impossible faire avec des #define.
L o l'utilit est plus flagrante, c'est lors du dbogage. Un bon dbogueur peut afficher
le nom de l'lment numr, au lieu de simplement une valeur numrique, ce qui rends
un peu moins pnible ce processus dj trs rbarbatif la base, surtout lorsque le type
en question est une structure avec des dizaines, pour ne pas dire une centaine, de
champs.
Certains compilateurs (gcc pour ne citer que le plus connu) n'incluent pas par dfaut les
symboles du prprocesseur avec l'option standard de dbogage (-g), principalement pour
viter de faire exploser la taille de l'excutable. Si bien que dans un dbogueur il est
souvent impossible d'afficher la valeur associe une constante du prprocesseur
autrement qu'en fouillant dans les sources. moins d'avoir un environnement plutt
volu, cette limitation peut s'avrer trs pnible.
73
Prprocesseur
Dclarations automatiques
Le langage C impose que le compilateur dfinisse un certain nombre de constantes. Sans
numrer toutes celles spcifiques chaque compilateur, on peut nanmoins compter sur :
__FILE__ (char *) : une chane de caractres reprsentant le nom de fichier dans lequel
on se trouve. Pratique pour diagnostiquer les erreurs.
__LINE__ (int) : le numro de la ligne en cours dans le fichier.
__DATE__ (char *) : la date en cours (incluant le jour, le mois et l'anne).
__TIME__ (char *) : l'heure courante (HH:MM:SS).
__STDC__ (int) : cette constante est en gnral dfinie si le compilateur suit les rgles du
C ANSI (sans les spcificits du compilateur). Cela permet d'encadrer des portions de
code non portables et fournir une implmentation moins optimise, mais ayant plus de
chance de compiler sur d'autres systmes.
Extensions
Chaque compilateur peut dfinir d'autres macro, pourvu qu'elles restent dans les
conventions de nommage que la norme leur rserve. Les lister toutes ici est impossible et
hors-sujet, mais on peut citer quelques unes, titre d'exemple, que le lecteur pourra
rencontrer rgulirement dans du code. Il va de soi que le fait qu'elles soient dfinies ou
non, et leur ventuelle valeur, est entirement dpendant du compilateur.
Dtection du systme d'exploitation :
_WIN32 ou __WIN32__ (Windows)
linux ou __linux__ (Linux)
__APPLE__ ou __MACH__ (Apple Darwin)
__FreeBSD__ (FreeBSD)
__NetBSD__ (NetBSD)
sun ou __SVR4 (Solaris)
Visual C++ : _MSC_VER
Compilateur gcc :
Version : __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__.
Compilateurs Borland :
__TURBOC__ (version de Turbo C ou Borland C)
__BORLANDC__ (version de Borland C++ Builder)
Divers : __MINGW32__ (MinGW), __CYGWIN__ ou __CYGWIN32__ (Cygwin),
Dclaration de macros
Une macro est en fait une constante qui peut prendre un certain nombre d'arguments. Les
arguments sont placs entre parenthses aprs le nom de la macro sans espaces, par
exemple :
#define MAX(x,y) x > y ? x : y
#define SWAP(x,y) x ^= y, y ^= x, x ^= y
La premire macro prend deux arguments et retourne le maximum entre les deux. La
deuxime est plus subtile, elle change la valeur des deux arguments (qui doivent tre des
variables entires), sans passer par une variable temporaire, et ce avec le mme nombre
d'oprations.
74
Prprocesseur
La macro va en fait remplacer toutes les occurrences de la chane MAX et de ses
arguments par x > y ? x : y . Ainsi, si on appelle la macro de cette faon :
printf("%d\n", MAX(4,6));
Elle sera remplace par :
printf("%d\n", 4 > 6 ? 4 : 6);
Il faut bien comprendre qu'il ne s'agit que d'une substitution de texte, qui ne tient pas
compte de la structure du programme. Considrez l'exemple suivant, qui illustre une erreur
trs classique dans l'utilisation des macros :
#define MAX(x,y) x > y ? x : y
i = 2 * MAX(4,6); /* Sera remplac par : i = 2 * 4 > 6 ? 4 : 6; */
L'effet n'est pas du tout ce quoi on s'attendait. Il est donc important de bien parenthser
les expressions, justement pour viter ce genre d'effet de bord. Il aurait mieux fallu crire
la macro MAX de la manire suivante :
#define MAX(x,y) ((x) > (y) ? (x) : (y))
En fait ds qu'une macro est compose d'autre chose qu'un lment atomique (un lexme,
ou un token) il est bon de le mettre entre parenthses, notamment les arguments qui
peuvent tre des expressions utilisant des oprateurs ayant des priorits arbitraires.
Il est a noter que l'emploi systmatique de parenthses ne protge pas contre tous les
effets de bord. En effet :
MAX(x++,y); /* Sera remplac par : ((x++) > (y) ? (x++) : (y)) */
Du coup, x est incrment de 2 et non pas de 1. La qualit et la performance des
compilateurs C modernes fait que l'utilisation de fonctions inline est le plus souvent
prfrable.
Cette directive supprime le symbole spcifi. noter que mme pour les macros avec
arguments, il suffit juste de spcifier le nom de cette macro. Qui plus est, supprimer une
variable de substitution inexistante ne provoquera aucune erreur.
75
Prprocesseur
noter qu'il n'existe pas de mcanisme simple pour transformer la valeur de la macro en
chane de caractres. C'est le cas classique des constantes numriques qu'on aimerait
souvent transformer en chane de caractres : le prprocesseur C n'offre malheureusement
rien de vraiment pratique pour effectuer ce genre d'opration.
Concatnation d'arguments
Il s'agit d'une facilit d'criture pour simplifier les tches rptitives. En utilisant
l'oprateur ##, on peut concatner deux expressions :
#define version(symbole) symbole ## _v123
int version(variable); /* Dclare "int variable_v123;" */
76
Prprocesseur
Exemples
Les macro du prprocesseur sont souvent utilises dans des situations o une fonction,
ventuellement inline, aurait le mme effet, avec une meilleure robustesse. Cependant, il
y a des usages pour lesquels les macros ne peuvent tre remplacs par des fonctions.
Par exemple, pour afficher la taille des types C, on crirait un programme comme le suivant
:
#include <stdio.h>
int main(void)
{
printf("sizeof(char) = %zd.\n", sizeof(char));
printf("sizeof(short) = %zd.\n", sizeof(short));
printf("sizeof(int) = %zd.\n", sizeof(int));
/* ...*/
return 0;
}
crire un tel programme peut tre fatiguant, et il est trs facile de faire des erreurs en
recopiant les lignes. Ici, une fonction ne pourrait simplifier l'criture. En effet, il faudrait
passer la fonction le nom du type, pour l'afficher dans la chane "sizeof(XXX) = ", et la
taille. On devrait donc donner deux arguments la fonction, pour l'appeler ainsi:
print_taille("char", sizeof(char));
Ce qui ne serait pas beaucoup plus simple, surtout que la fonction elle-mme serait plus
complexe (utilisation de fonctions str* pour inclure le premier paramtre dans la chane
de format). Ici, le prprocesseur fournit une solution beaucoup plus simple, l'aide de
l'oprateur # :
#include <stdio.h>
#define PRINTF_SIZEOF(type) printf("sizeof(" #type ") = %zd.\n",
sizeof(type))
int main(void)
{
PRINTF_SIZEOF(char);
PRINTF_SIZEOF(short);
PRINTF_SIZEOF(int);
return 0;
}
Une technique similaire peut tre utilise pour conserver l'affichage le nom de constantes
ou d'numrations :
#include <stdio.h>
enum etat_t { Arret, Demarrage, Marche, ArretEnCours };
#define CASE_ETAT(etat) case etat: printf("Dans l'tat " #etat "\n");
77
Prprocesseur
78
break;
void affiche_etat(etat_t etat)
{
switch (etat)
{
CASE_ETAT(Arret)
CASE_ETAT(Demarrage)
CASE_ETAT(Marche)
CASE_ETAT(ArretEnCours)
default:
printf("Etat inconnu (%d).\n, etat);
break;
}
}
Ces exemples sont tirs d'un message
[2]
Compilation conditionnelle
Les tests permettent d'effectuer de la compilation conditionnelle. La directive #ifdef
permet de savoir si une constante ou une macro est dfinie. Chaque dclaration #ifdef
doit obligatoirement se terminer par un #endif, avec ventuellement une clause #else
entre. Un petit exemple classique :
#ifdef DEBUG
/* S'utilise : debug( ("Variable x = %d\n", x) ); (double parenthsage)
*/
#define debug(x) printf x
#else
#define debug(x)
#endif
L'argument de la directive #ifdef doit obligatoirement tre un symbole du prprocesseur.
Pour utiliser des expressions un peu plus complexes, il y a la directive #if (et #elif,
contraction de else if). Cette directive utilise des expressions semblable l'instruction
if() : si l'expression value est diffrente de zro, elle sera considr comme vraie, et
comme fausse si l'expression s'value 0.
On peut utiliser l'addition, la soustraction, la multiplication, la division, les oprateurs
binaires (&, |, ^, ~, <<, >>), les comparaisons et les connecteurs logiques (&& et ||). Ces
derniers sont valus en circuit court comme leur quivalent C. Les oprandes possibles
sont les nombres entiers et les caractres, ainsi que les macros elle-mmes, qui seront
remplacs par leur valeur.
noter, l'oprateur spcial defined qui permet de tester si une macro est dfinie (et qui
renvoit donc un boolen). Exemple :
#if defined(DEBUG) && defined(NDEBUG)
#error DEBUG et NDEBUG sont dfinis !
#endif
Prprocesseur
Cette directive est trs pratique pour dsactiver des pans entier de code sans rien y
modifier. Il suffit de mettre une expression qui vaudra toujours zro, comme :
#if 0
/* Vous pouvez mettre ce que vous voulez ici, tout sera ignor, mme du
code invalide */
:-)
#endif
Un autre avantage de cette technique, est que les directives de compilations peuvent tre
imbriques, contrairement aux commentaires. Dans ce cas, s'il y avait eu d'autres directives
#if / #endif (correctement balances), la clause #if 0 ne s'arrterait pas au premier
#endif rencontr, tandis que cela aurait t le cas avec des commentaires.
noter qu'au niveau du prprocesseur il est impossible d'utiliser les oprateurs du C.
Notamment l'oprateur sizeof, dont le manque aura fait grincer les dents des
gnrations de programmeurs, sera en fait interprt comme tant une macro. Il faut bien
garder l'esprit que le prprocesseur C est totalement indpendant du langage C.
Inclusion de fichiers
Il s'agit d'une autre fonctionnalit massivement employe dans toute application C qui se
respecte. Comme son nom l'indique, la directive #include permet d'inclure in extenso le
contenu d'un autre fichier, comme s'il avait t crit en lieu et place de la directive. On
peut donc voir a comme de la factorisation de code, ou plutt de dclarations. On appelle
de tels fichiers, des fichiers en-ttes (header files) et on leur donne en gnral l'extension
.h.
Il est rare d'inclure du code dans ces fichiers (dfinitions de variables ou de fonctions),
principalement parce que ces fichiers sont destins tre inclus dans plusieurs endroits du
programme. Ces dfinitions seraient donc dfinies plusieurs fois, ce qui, dans le meilleur
des cas, seraient du code superflu, et dans le pire, pourraient poser des problmes
l'dition des liens.
On y place surtout des dclarations de type, macros, prototypes de fonctions relatif un
module. On en profite aussi pour documenter toutes les fonctions publiques, leurs
paramtres, les valeurs renvoyes, les effets de bords, la signification des champs de
structures et les pr/post conditions (quel tat doit respecter la fonction avant/aprs son
appel). Dans l'idal on devrait pouvoir comprendre le fonctionnement d'un module,
simplement en lisant son fichier en-tte.
La directive #include prend en fait un argument : le nom du fichier que vous voulez
inclure. Ce nom doit tre soit mis entre guillemets doubles ou entre balises (< >). Cette
diffrence affecte simplement l'ordre de recherche du fichier. Dans le premier cas, le
fichier est recherch dans le rpertoire o le fichier contenant la directive se trouve, puis le
prprocesseur regarde des endroits prconfigurs. Dans le second cas, il regardera
seulement dans les endroits prconfigurs. Les endroits prconfigurs sont le rpertoire
include par dfaut (par exemple /usr/include/, sous Unix) et ceux passs explicitement en
paramtre au compilateur.
En fait l'argument de la directive #include peut aussi tre une macro, dont la valeur est un
argument valide (soit une chane de caractres, soit un nom entre balises < et >) :
79
Prprocesseur
#define FICHIER_A_INCLURE <stdio.h>
#include FICHIER_A_INCLURE
Exemple
fichier.h
void affiche( void );
fichier.c
#include "fichier.h"
int main( void )
{
affiche();
return 0;
}
C'est comme si fichier.c avait t crit :
void affiche( void );
int main( void )
{
affiche();
return;
}
80
Prprocesseur
Cette directive affichera en fait le message tel quel, qui peut videmment contenir
n'importe quoi, y compris des caractres rservs, sans qu'on ait besoin de le mettre entre
guillement ("). Aprs mission du message, la compilation s'arrtera.
noter que certains compilateurs comme GCC proposent aussi la directive:
#warning message
Rfrences
[1] http:/ / www. redhat. com/ docs/ manuals/ enterprise/ RHEL-3-Manual/ cpp/ variadic-macros. html
[2] http:/ / groups. google. fr/ group/ comp. lang. c/ msg/ 19f07545672ba9e7
Bibliothque standard
La bibliothque standard du langage C peut paratre relativement pauvre par rapport
d'autres langages tout en un plus rcents comme Python, Ruby, Perl, C# ou Java. Conue
avant tout avec un souci de portabilit, et en ayant en tte les contraintes matrielles
limites de certaines plate-formes auxquelles le C est aussi destin, vous obtenez avec cela
le plus petit dnominateur commun qui puisse tre port sur le plus grand nombre de
plateformes. Les contraintes qu'ont subies l'ANSI, et le WG14 aprs lui, portant
principalement sur les contraintes de portabilit sur certaines architectures parfois
exotiques , et la trs forte contrainte de ne pas casser du code existant reposant sur un
comportement dj tabli, voire normalis, font que certains points connus pour tre
complexes, voire peu souhaitables, sont rests dans le langage et la bibliothque standard.
Concevoir une application avec les seules fonctions prsentes dans cette bibliothque
ncessite une trs grande rigueur. Le WG14 ne s'tant pas fix pour but d'tendre la
bibliothque standard de manire importante, il est plus que conseill de se tourner vers
des bibliothques de plus haut niveau, afin d'viter de rinventer inutilement la roue. Il en
existe heureusement beaucoup, mais dcrire ne serait-ce que ce qui existe est hors de la
porte de cet ouvrage.
La bibliothque standard permet toutefois de faire des traitements complexes avec peu
d'efforts, pour peu qu'on ait conscience des dangers et des piges qui sont parfois tendus.
Les sections qui suivent permettront de voir un peu plus clair dans les mandres parfois
trs sombres o s'aventure le C.
81
Bibliothque standard
Voir aussi
Cours sur Wikiversity sur l'utilisation des fonctions standards du langage C.
Chanes de caractres
Le langage C offre quelques facilits d'critures pour simuler les chanes de caractres
l'aide de tableaux. En plus de cela, certaines fonctions de la bibliothque standard (et les
autres) permettent de faciliter leur gestion. la diffrence d'un tableau, les chanes de
caractres respectent une convention : se terminer par le caractre nul, '\0'
(antislash-zero). Ainsi, pour construire une chane de caractres la main , il ne faut pas
oublier ce caractre final.
On peut noter que, par rapport ce qui est disponible dans d'autres langages, les fonctions
de la bibliothque standard sont peu pratiques utiliser. On peut avancer sans trop de
risque qu'une des plus grandes lacunes du C provient de sa gestion prcaire des chanes de
caractres, pourtant massivement employes dans tout programme digne de ce nom. Pour
faire court, on ne saurait trop conseiller de soit reprogrammer les fonctions ci-dessous,
d'utiliser une bibliothque externe ou de faire preuve de paranoa avant leur utilisation.
Les fonctions permettant de manipuler les chanes de caractres se trouvent dans l'entte
string.h, ainsi pour les utiliser il faut ajouter la commande prprocesseur :
#include <string.h>
Comparaison de chanes
int strcmp(const char * chaine1, const char * chaine2);
int strncmp(const char * chaine1, const char * chaine2, size_t
longueur);
Compare les chanes chaine1 et chaine2 et renvoie un entier :
ngatif, si chaine1 est infrieure chaine2 ;
nul, si chaine1 est gale chaine2 ;
positif, si chaine1 est suprieur chaine2.
premire remarque : lorsque les deux chanes sont gales, strcmp renvoie 0, qui a la
valeur de vrit faux. Pour tester l'galit entre deux chanes, il faut donc crire soit if
(strcmp(chaine1, chaine2) == 0) ..., soit if (!strcmp(chaine1, chaine2)) ...
mais surtout pas if (strcmp(chaine1, chaine2)) ... qui teste si deux chanes sont
diffrentes !
seconde remarque : l'oprateur ==, dans le cas de pointeurs, teste si les adresses sont
gales. Noter chaine1 == chaine2, si chaine1 et chaine2 sont des char * revient tester
si les deux chanes pointent sur la mme zone mmoire et non pas tester l'galit de leur
contenu.
noter l'existence de deux fonctions de comparaisons de chane, insensibles la casse des
caractres et s'adaptant la localisation en cours, fonctionnant sur le mme principe que
strcmp() et strncmp(), mais dont l'origine provient des systmes BSD :
int strcasecmp(const char * chaine1, const char * chaine2);
int strncasecmp(const char * chaine1, const char * chaine2, size_t
82
Chanes de caractres
longueur);
*
*
*
*
strcpy copie le contenu de source l'adresse mmoire point par destination, incluant le
caractre nul final. strcat concatne la chane source la fin de la chane destination, en
y rajoutant aussi le caractre nul final. Toutes ces fonctions renvoient le pointeur sur la
chane destination.
Pour strncpy, on notera que:
si la chane source a moins de n caractres non nuls, strncpy rajoutera n strlen(source) caractres nuls la suite, pour complter;
si la chane source fait au moins n caractres, la fonction n'insrera pas de caractre
nul en fin de chaine (i.e. destination ne sera pas une chane de caractres valide).
Il faut tre trs prudent lors des copies ou des concatnations de chanes, car les problmes
pouvant survenir peuvent tre trs pnibles diagnostiquer. L'erreur classique est de
faire une copie dans une zone mmoire non rserve ou trop petite, comme dans l'exemple
suivant, a priori anodin:
Ce code contient une erreur volontaire !
char *copie_chaine(const char *source)
{
char *chaine = malloc(strlen(source));
if (chaine != NULL)
{
strcpy(chaine, source);
}
return chaine;
}
Ce code a priori correct provoque pourtant l'criture d'un caractre dans une zone non
alloue. Les consquences de ce genre d'action sont totalement imprvisibles, pouvant au
mieux passer inaperue, ou au pire craser des structures de donnes critiques dans la
gestion des blocs mmoires et engendrer des accs mmoire illgaux lors des prochaines
tentatives d'allocation ou de libration de bloc, i.e. le cauchemar de tout programmeur.
Dans cet exemple il aurait bien videmment fallu crire :
char * chaine = malloc( strlen(source) + 1 );
83
Chanes de caractres
Une autre erreur, beaucoup plus frquente hlas, est de copier une chane dans un tableau
de caractres local, sans se soucier de savoir si ce tableau est capable d'accueillir la
nouvelle chane. Les consquences peuvent ici tre beaucoup plus graves qu'un simple
accs illgal une zone mmoire globale.
Un crasement mmoire (buffer overflow) est considr comme un dfaut de scurit
relativement grave, puisque, sur un grand nombre d'architectures, un attaquant bien
renseign sur la structure du programme peut effectivement lui faire excuter du code
arbitraire. Ce problme vient de la manire dont les variables locales aux fonctions et
certaines donnes internes sont stockes en mmoire. Comme le C n'interdit pas d'accder
un tableau en dehors de ses limites, on pourrait donc, suivant la qualit de
l'implmentation, accder aux valeurs stockes au-del des dclarations de variables
locales. En fait, sur un grand nombre d'architectures, les variables locales sont places
dans un espace mmoire appel pile, avec d'autres informations internes au systme,
comme l'adresse de retour de la fonction, c'est--dire l'adresse de la prochaine instruction
excuter aprs la fin de la fonction. En s'y prenant bien, on peut donc craser cette valeur
pour la remplacer par l'adresse d'un autre bout de code, qui donnerait l'ordre d'effacer le
disque dur, par exemple ! Si en plus le programme possde des privilges, les rsultats
peuvent tre assez catastrophiques. Ainsi dcrit, le problme semble complexe : il faudrait
que l'attaquant puisse insrer dans une zone mmoire de l'application un bout de code qu'il
a lui-mme crit, et qu'il arrive crire l'adresse de ce bout de code l o le systme
s'attend retrouver l'adresse de retour de la fonction. Cependant, ce genre d'attaque est
aujourd'hui trs courant, et les applications prsentant ce genre d'erreur deviennent trs
rapidement la cible d'attaques.
Voici un exemple trs classique, o ce genre d' exploit peut arriver :
Ce code contient une erreur volontaire !
int traite_chaine(const char *ma_chaine)
{
char tmp[512];
strcpy(tmp, ma_chaine);
/* ... */
}
Ce code, hlas plus frquent qu'on ne le pense, est bannir. Parmi diffrentes mthodes,
on peut viter ce problme en ne copiant qu'un certain nombre des premiers caractres de
la chane, avec la fonction strncpy :
char tmp[512];
strncpy(tmp, ma_chaine, sizeof(tmp));
tmp[sizeof(tmp) - 1] = '\0';
On notera l'ajout explicite du caractre nul, si ma_chaine est plus grande que la zone
mmoire tmp. La fonction strncpy ne rajoutant hlas pas, dans ce cas, de caractre nul.
C'est un problme tellement classique que toute application C reprogramme en gnral la
fonction strncpy pour prendre en compte ce cas de figure (voir la fonction POSIX strdup,
ou strlcopy utilise par le systme d'exploitation OpenBSD, par exemple)
84
Chanes de caractres
Le langage n'offrant que trs peu d'aide, la gestion correcte des chanes de caractres est
un problme ne pas sous-estimer en C.
85
Chanes de caractres
Compare les 'longueur' premiers octets des blocs 'mem1' et 'mem2'. Renvoie les codes
suivants :
< 0 : mem1 < mem2
= 0 : mem1 == mem2
> 0 : mem1 > mem2
void * memchr( const void * memoire, int caractere, size_t longueur );
Recherche dans les 'longueur' premiers octets du bloc 'memoire', la valeur convertie en
type char de 'caractere'. Renvoie un pointeur sur l'emplacement o le caractre a t
trouv, ou NULL si rien n'a t trouv.
void * memrchr( const void * memoire, int caractere, size_t longueur );
Pareil que memchr(), mais commence par la fin du bloc 'memoire'.
Entres/sorties
Les fonctions d'entres/sorties sont celles qui vous permettent de communiquer avec
l'extrieur, c'est--dire, la console, les fichiers, tubes de communication, socket IP, etc ...
Pour utiliser ces fonctions, il faut inclure l'en-tte stdio.h, avec la directive d'inclusion :
#include <stdio.h>
Manipulation de fichiers
En C, les fichiers ouverts sont reprsents par le type FILE, qui est un type opaque : on ne
connat pas la nature relle du type, mais seulement des fonctions pour le manipuler. Ainsi,
on ne peut crer directement de variables de type FILE, seules les fonctions de la
bibliothque standard peuvent crer une variable de ce type, lors de l'ouverture d'un
fichier. Ces donnes sont donc uniquement manipules par des pointeurs de type FILE *.
Ce type est un flux de donnes, qui reprsente des fichiers, mais peut aussi reprsenter
toute autre source ou destination de donnes. L'en-tte stdio.h fournit trois flux que l'on
peut utiliser directement :
stdin, l'entre standard ;
stdout, la sortie standard ;
stderr, la sortie standard des erreurs.
Souvent, l'entre standard envoie au programme les donnes issues du clavier, et les
sorties standard envoient les donnes que le programme gnre l'cran. Mais d'o
viennent et o vont ces donnes dpend troitement du contexte et de l'implmentation ; la
bibliothque standard fournit le type FILE comme une abstraction pour les manipuler tous
de la mme manire, ce qui libre le programmeur de certains dtails d'implmentations, et
permet l'utilisateur d'un programme d'employer (suivant son implmentation) aussi bien
son clavier qu'un fichier comme entre standard.
86
Entres/ sorties
87
Ouverture
FILE * fopen(const char * restrict chemin, const char * restrict mode)
Ouvre le fichier dsign par le chemin et renvoie un nouveau flux de donnes pointant sur
ce fichier. L'argument mode est une chane de caractres dsignant la manire dont on
veut ouvrir le fichier :
r : ouvre le fichier en lecture, le flux est positionn au dbut du fichier ;
r+ : ouvre le fichier en lecture/criture, le flux est positionn au dbut du fichier ;
w : ouvre le fichier en criture, supprime toutes les donnes si le fichier existe et le cre
sinon, le flux est positionn au dbut du fichier ;
w+ : ouvre le fichier en lecture/criture, supprime toutes les donnes si le fichier existe
et le cre sinon, le flux est positionn au dbut du fichier ;
a : ouvre le fichier en criture, cre le fichier s'il n'existe pas, le flux est positionn la
fin du fichier ;
a+ : ouvre le fichier en lecture/criture, cre le fichier s'il n'existe pas, le flux est
positionn la fin du fichier.
lecture
r+
w
w+
a
a+
criture
cre le fichier
vide le fichier
position du flux
dbut
dbut
dbut
dbut
fin
fin
Lorsqu'un fichier est ouvert en criture, les donnes qui sont envoyes dans le flux ne sont
pas directement crites sur le disque. Elles sont stockes dans un tampon, une zone
mmoire de taille finie. Lorsque le tampon est plein, les donnes sont purges (flush), elles
sont crites dans le fichier. Ce mcanisme permet de limiter les accs au systme de
fichiers et donc d'acclerer les oprations sur les fichiers.
noter une particularit des systmes Microsoft Windows, est de traiter diffremment les
fichiers textes, des fichiers binaires. Sur ces systmes, le caractre de saut de ligne est en
fait compos de deux caractres (CR, puis LF, de code ASCII respectif 13 et 10, ou '\r' et
'\n' crit sous forme de caractre C). Lorsqu'un fichier est ouvert en mode texte (mode par
dfaut), toute squence CRLF lue depuis le fichier sera convertie en LF, et tout caractre
LF crit sera en fait prcd d'un caractre CR supplmentaire. Si le fichier est ouvert en
mode binaire, aucune conversion n'aura lieu.
Ce genre de comportement issu d'un autre ge, est en fait bien plus agaant que rellement
utile. Le premier rflexe est en gnral de dsactiver ce parasitage des entres/sorties, tant
il est pnible. Pour cela deux cas de figure :
1. Vous avez accs au nom du fichier : donc vous pouvez utiliser la fonction
fopen(). Dans ce cas de figure, il suffit de rajouter la lettre
b au mode d'ouverture:
Entres/ sorties
fopen("fichier.txt", "rb"); /* Ouverture sans conversion */
fopen("fichier.txt", "wb"); /* L'criture d'un '\n' n'entrainera pas
l'ajout d'un '\r' */
2. Vous n'avez pas accs au nom du fichier (par exemple stdout ou
stdin). Il existe une fonction spcifique Microsoft Windows, non
portable sur d'autres systmes, mais qui peut s'avrer ncessaire:
/* Ce code qui suit n'est pas portable */
#include <fcntl.h>
setmode(descripteur, O_BINARY);
Histoire de compliquer encore un petit peu, cette fonction travaille avec les descripteurs de
fichier, plutt que les flux stdio. On peut tout de mme s'en sortir avec cette fonction
faisant partie de la bibliothque standard:
int fileno(FILE *);
Par exemple pour mettre la sortie standard en mode binaire:
setmode(fileno(stdout), O_BINARY);
Fermeture
int fclose(FILE * flux);
Dissocie le flux du fichier auquel il avait t associ par fopen. Si le fichier tait ouvert en
criture, le tampon est vid. Cette fonction renvoie 0 si la fermeture s'est bien passe
(notamment la purge des zones en criture), ou EOF en cas d'erreur (voir le paragraphe sur
la gestion d'erreurs).
Suppression
int remove(const char * path);
Supprime le fichier ou le rpertoire nomm 'path'. La fonction renvoie 0 en cas de russite
et une valeur non nulle en cas d'erreur, ce qui peut inclure :
88
Entres/ sorties
89
Entres/ sorties
Synchronisation
int fflush ( FILE *flux );
Cette fonction purge toutes les zones mmoires en attente d'criture et renvoie 0 si tout
c'est bien pass, ou EOF en cas d'erreur. Si NULL est pass comme argument, tous les flux
ouverts en criture seront purgs.
noter que cette fonction ne permet pas de purger les flux ouverts en lecture (Pour
rpondre une question du genre Voulez-vous effacer ce fichier (o/n) ? ). Une
instruction de ce genre sera au mieux ignore, et au pire provoquera un comportement
indtermin :
Ce code contient une erreur volontaire !
fflush( stdin );
Pour effectuer une purge des flux ouverts en lecture, il faut passer par des appels systmes
normaliss dans d'autres documents (POSIX), mais dont la disponibilit est en gnral
dpendante du systme d'exploitation.
Sorties formates
int printf(const char * restrict format, ...);
int fprintf(FILE * restrict flux, const char * restrict format, ...);
int sprintf(char * restrict chaine, const char * restrict format, ...);
int snprintf(char * restrict chaine, size_t taille, const char *
restrict format, ...);
Ces fonctions permettent d'crire des donnes formates dans :
la sortie standard pour printf ;
un flux pour fprintf ;
une chane de caractres pour sprintf.
En retour elle indique le nombre de caractres qui a t crit l'cran, dans le flux ou la
zone mmoire (caractre nul non compris pour sprintf).
Bien que cela dj t trait dans la section ddie aux chaines de caractres, il faut faire
trs attention avec la fonction sprintf(). Dans la mesure o la fonction n'a aucune ide de
la taille de la zone mmoire transmise, il faut s'assurer qu'il n'y aura pas de dbordements.
Mieux vaut donc utiliser la fonction snprintf(), qui permet de limiter explicitement le
nombre de caractre crire.
noter que snprintf() devrait toujours retourner la taille de la chaine crire,
indpendamment de la limite fixe par le paramtre taille. Le conditionnel reste de mise,
car beaucoup d'implmentations de cette fonction se limitent retourner le nombre de
caractres crit, c'est dire en s'arrtant la limite le cas chant.
90
Entres/ sorties
91
Type de conversion
Mis part l' endroit o crivent les fonctions, elles fonctionnent exactement de la mme
manire, nous allons donc dcrire leur fonctionnement en prenant l'exemple de printf.
L'argument format est une chane de caractres qui dtermine ce qui sera affich par
printf et sous quelle forme. Cette chane est compose de texte normal et de
squences de contrle permettant d'inclure des variables dans la sortie. Les squences de
contrle commencent par le caractre % suivi d'un caractre parmi :
d ou i pour afficher un entier sign au format dcimal (int) ;
u pour un entier non sign au format dcimal ;
x ou X pour afficher un entier au format hexadcimal (avec les lettres "abcdef" pour le
format 'x' et "ABCDEF" avec le format 'X') ;
f pour afficher un rel (double) avec une prcision fixe ;
e pour afficher un rel (double) en notation scientifique ;
g effectue un mixe de 'f' et de 'e' suivant le format le plus appropri ;
c pour afficher en tant que caractre ;
s pour afficher une chaine de caractre C standard ;
p pour afficher la valeur d'un pointeur, gnralement sous forme hexadcimale. Suivant
le compilateur, c'est l'quivalent soit "%08x", ou alors "0x%08x". ;
n ce n'est pas un format d'affichage et l'argument associ doit tre de type int * et tre
une rfrence valide. La fonction stockera dans l'entier point par l'argument le nombre
de caractres crit jusqu' maintenant ;
% pour afficher le caractre '%'.
"Salut");
"Salut");
"Salut tout le monde");
"Salut tout le monde");
/*
/*
/*
/*
" Salut" */
"Salut " */
"Salut tout le monde" */
"Salut tout" */
Entres/ sorties
92
543);
543);
1234567890);
1234567890);
543);
/*
/*
/*
/*
/*
"+000000543"
"+543 "
"+1234567890"
"+1234567890"
"0000021f"
*/
*/
*/
*/
*/
Entres/ sorties
93
Format
Autres attributs
(rarement utiliss)
Attributs de taille
aucun
hh
l (elle)
ll
(elle-elle)
int *
signed
char *
short *
long *
d, i, o, x, X
int
signed
char
short
unsigned
int
unsigned
char
unsigned
short
char *
wchar_t *
int
wint_t
void *
a, A, e, E, f,
F, g, G
double
long long
*
intmax_t *
size_t
*
ptrdiff_t
*
long
long long
intmax_t
size_t
ptrdiff_t
unsigned
long
unsigned
long long
uintmax_t
size_t
ptrdiff_t
long
double
Arguments positionnels
Il s'agit d'une fonctionnalit relativement peu utilise, mais qui peut s'avrer trs utile dans
le cadre d'une application internationnalise. Considrez le code suivant (tir du manuel de
gettext) :
printf( gettext("La chaine '%s' a %zu caractres\n"), s, strlen(s) );
gettext est un ensemble de fonctions permettant de manipuler des catalogues de langues.
La principale fonction de cette bibliothque est justement gettext(), qui en fonction d'une
chaine de caractre retourne la chaine traduite selon la locale en cours (o celle passe en
argument si rien n'a t trouv).
Une traduction en allemand du message prcdant, pourrait donner : "%d Zeichen lang
ist die Zeichenkette '%s'"
On remarque d'emble que les spcificateurs de format sont inverss par rapport la
chaine originale. Or l'ordre des arguments passs la fonction printf() sera toujours le
mme. Il est quand mme possible de s'en sortir avec les arguments positionnels. Pour cela,
il suffit d'ajouter la suite du caractre % un nombre, suivi d'un signe $. Ce nombre
reprsente le numro de l'argument utiliser pour le spcificateur, en commenant partir
de 1. Un petit exemple :
Entres/ sorties
char * s = "Bla bla";
printf("La chaine %2$s a %1$zu caractres\n", strlen(s), s); /* "La
chaine Bla bla a 7 caractres" */
noter que si un des arguments utilise la rfrence positionnelle, tous les autres
arguments devront faire videmment de mme, sous peine d'avoir un comportement
imprvisible.
94
Entres/ sorties
La fonction setvbuf() renvoie 0 si elle russi, et une valeur diffrente de zro dans le cas
contraire (en gnral le paramtre mode est invalide).
Cette fonctionnalit peut tre intressante pour les programmes gnrant des messages
sporadiques. Il peut effectivement s'couler un temps arbitrairement long avant que le bloc
mmoire soit plein, si cette commande est redirige vers un autre programme, ce qui peut
s'avrer assez dramatique pour des messages signalant une avarie grave. Dans ce cas, il est
prfrable de forcer l'criture par ligne (ou immdiate), plutt que de faire suivre
systmatiquement chaque criture de ligne par un appel fflush(), avec tous les risques
d'oubli que cela comporte.
Entres formates
La bibliothque stdio propose quelques fonctions trs puissantes pour saisir des donnes
depuis un flux quelconque. Le comportement de certaines fonctions (scanf notamment)
peut paraitre surprenant de prime abord, mais s'claircira la lumire des explications
suivantes.
int scanf(const char * restrict format, ...);
int fscanf(FILE * restrict flux, const char * restrict format, ...);
int sscanf(const char * restrict chaine, const char * restrict format,
...);
Ces trois fonctions permettent de lire des donnes formates provenant de :
l'entre standard pour scanf ;
un flux pour fscanf ;
une chane de caractres pour sscanf.
L'argument format ressemble aux rgles d'criture de la famille de fonction printf,
cependant les arguments qui suivent ne sont plus des variables d'entre mais des variables
de sortie (ie : l'appel scanf va modifier leur valeur, il faut donc passer une rfrence).
Ces fonctions retournent le nombre d'arguments correctement lus depuis le format, qui
peut tre infrieur ou gal au nombre de spcificateurs de format, et mme nul.
95
Entres/ sorties
Format de conversion
Les fonctions scanf() analysent le spcificateur de format et les donnes d'entre, en les
comparant caractre caractre et s'arrtant lorsqu'il y en a un qui ne correspond pas.
noter que les blancs (espaces, tabulations et retour la ligne) dans le spcificateur de
format ont une signification spciale : un blanc de la chaine format peut correspondre un
nombre quelconque de blanc dans les donnes d'entre, y compris aucun. D'autres part, il
est possible d'insrer des squences spciales, commenant par le caractre '%' et
l'image de printf(), pour indiquer qu'on aimerait rcuprer la valeur sous la forme dcrite
par le caractre suivant le '%' :
s : extrait la chane de caractres, en ignorant les blancs initiaux et ce jusqu'au
prochain blanc. L'argument correspondant doit tre de type char * et pointer vers un
bloc mmoire suffisamment grand pour contenir la chane et son caractre terminal.
d : extrait un nombre dcimal sign de type int, ignorant les espaces se trouvant
ventuellement avant le nombre.
i : extrait un nombre (de type int) hexadcimal, si la chaine commence par "0x", octal si
la chaine commence par "0" et dcimal sinon. Les ventuels espaces initiaux seront
ignors.
f : extrait un nombre rel, en sautant les blancs, de type float.
u : lit un nombre dcimal non-sign, sans les blancs, de type int.
c : lit un caractre (de type char), y compris un blanc.
[] : lit une chane de caractres qui doit faire partie de l'ensemble entre crochets. Cet
ensemble est une numration de caractre. On peut utiliser le tiret ('-') pour grouper les
dclarations (comme "0-9" ou "a-z"). Pour utiliser le caractre spcial ']' dans
l'ensemble, il doit tre plac en premire position et, pour utiliser le tiret comme un
caractre normal, il doit tre mis la fin. Pour indiquer que l'on veut tous les caractres
sauf ceux de l'ensemble, on peut utiliser le caractre '^' en premire position. noter
que scanf terminera toujours la chane par 0 et que, contrairement au spcificateur %s,
les blancs ne seront pas ignors.
n : Comme pour la fonction printf(), ce spcificateur de format permet de stocker dans
l'entier correspondand de type int, le nombre de caractres lus jusqu' prsent.
Contraindre la largeur
Comme pour la fonction printf(), il est possible de contraindre le nombre de caractres
lire, en ajoutant ce nombre juste avant le caractre de conversion. Dans le cas des chanes,
c'est mme une obligation, dans la mesure o scanf() ne pourra pas ajuster l'espace la
vole.
Exemple :
/* Lit une chane de caractres entre guillemets d'au plus 127
caractres */
char tmp[128];
if (fscanf(fichier, "Config = \"%127[^\"]\"", tmp ) == 1)
{
printf("L'argument associ au mot cl 'Config' est '%s'\n", tmp);
}
96
Entres/ sorties
97
Cet exemple est plus subtil qu'il ne parat. Il montre comment analyser une structure
relativement classique de ce qui pourrait tre un fichier de configuration de type
"MotCl=Valeur". Ce format spcifie donc qu'on s'attend trouver le mot cl "Config", en
ignorant ventuellement les blancs initiaux, puis le caratre '=', entour d'un nombre
quelconque de blancs, eventuellement aucun. la suite de cela, on doit avoir un guillemet
('"'), puis au plus 127 caractres autres que que les guillemets, qui seront stocks dans la
zone mmoire tmp (qui sera termine par 0, d'o l'allocation d'un caractre
supplmentaire). Le guillemet final est l pour s'assurer, d'une part, que la longueur de la
chane est bien infrieure 127 caractre et, d'autre part, que le guillemet n'a pas t
oubli dans le fichier.
En cas d'erreur, on peut par exemple ignorer tous les caractres jusqu' la ligne suivante.
Attributs de taille
aucun
hh
l (elle)
ll (elle-elle)
d, i, n
int *
char *
short *
long *
long long *
unsigned int *
unsigned char *
s, c, [ ]
char *
float *
double *
long double *
Ainsi pour lire la valeur d'un entier sur l'entre standard, on utilisera un code tel que celui
ci :
#include <stdio.h>
int main(void)
{
int i;
printf("Entrez un entier : ");
scanf("%d", &i);
printf("la variable i vaut maintenant %d\n", i);
return 0;
}
Les appels printf ne sont pas indispensables l'excution du scanf, mais permettent
l'utilisateur de comprendre ce qu'attend le programme (et ce qu'il fait aussi).
Entres/ sorties
Conversions muettes
La fonction scanf reconnait encore un autre attribut qui permet d'effectuer la conversion,
mais sans retourner la valeur dans une variable. Il s'agit du caractre toile '*', qui
remplace l'ventuel attribut de taille.
En thorie, la valeur de retour ne devrait pas tenir compte des conversions muettes.
Exemple:
int i, j;
sscanf("1 2.434e-308 2", "%d %*f %d", &i, &j); /* i vaut 1 et j vaut 2
*/
98
Entres/ sorties
Entres/sorties brutes
Les fonctions suivantes permettent d'crire ou de lire des quantits arbitraires de donnes
depuis un flux. Il faut faire attention la portabilit de ces oprations, notamment lorsque
le flux est un fichier. Dans la mesure o lire et crire des structures binaires depuis un
fichier ncessite de grer l'alignement, le bourrage, l'ordre des octets pour les entiers (big
endian, little endian) et le format pour les rels, il est souvent infiniment plus simple de
passer par un format texte.
Sortie
size_t fwrite(const void * buffer, size_t taille, size_t nombre, FILE *
flux);
Entre
size_t fread(void * buffer, size_t taille, size_t nombre, FILE * flux);
Lit nombre lments, chacun de taille taille, partir du flux et stocke le rsultat dans le
buffer. Renvoie le nombre d'lments correctement lus.
99
Entres/ sorties
Erreurs
Le langage C fournit un en-tte spcialis pour la gestion des erreurs : errno.h. Cet en-tte
dclare notamment une variable globale errno, et un certain nombre de codes d'erreur, qui
permettent aux fonctions de la bibliothque standard de reporter prcisment la cause
d'une erreur.
Utilisation
Pour inclure l'en-tte de son fichier source, il faut ajouter la ligne :
#include <errno.h>
On peut alors utiliser la variable errno, de type int, pour traiter les erreurs[1] .
Lorsqu'on veut utiliser errno pour dterminer la cause d'un chec, il faut d'abord s'assurer
que la fonction a bel et bien chou. Le C laissant une certaine libert dans la manire de
signaler un chec, il n'y a pratiquement aucun mcanisme universel qui permette de
dtecter une telle situation, chaque fonction tant presque un cas particulier. Cependant,
une pratique relativement rpandue est de retourner un code spcifique, en gnral en
dehors de l'intervalle de ce qu'on attend. Par exemple, lorsque la fonction est cense
allouer un objet et retourne un pointeur, une erreur est souvent signale en retournant un
pointeur nul, et la variable errno dcrit plus en dtail la nature de l'erreur.
Il est ncessaire de placer errno 0 avant d'utiliser la fonction qui peut chouer, car les
fonctions de la bibliothque standard ne sont pas obliges de la mettre zro en cas de
succs. Si on ne la rinitialisait pas manuellement , on pourrait voir le rsultat d'une
erreur cause par un appel de fonction antrieur. La procdure suivre est donc la
suivante :
Mettre errno 0 ;
Appeler la fonction (de la bibliothque standard) que l'on souhaite ;
Vrifier si elle a chou ;
Si c'est le cas : la valeur de errno est disponible pour traiter l'erreur ;
Sinon : procder la suite du traitement.
100
Erreurs
101
Exemple
Pour illustrer cela, voici un exemple, qui tente de convertir une chaine de caractres en un
nombre. Le nombre donn, 264, peut ne pas tre dans le domaine de valeur du type
unsigned long (qui peut se limiter 232 - 1), suivant l'architecture. Dans ce cas, on utilise
errno pour afficher un message d'erreur appropri l'utilisateur.
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<limits.h>
<string.h>
<errno.h>
/*
/*
/*
/*
/*
Erreurs
102
/* On rinitialise errno */
errno = 0;
/* Appel de strtoul : conversion d'une chaine en un entier unsigned
long*/
res = strtoul(nombre, NULL, 10);
/* On dtecte une erreur de strtoul quand elle renvoie ULONG_MAX
_et_
* que errno est non nul. En effet, si on lui passe en entre la
* reprsentation de ULONG_MAX, la conversion se fait sans erreur
* et errno reste 0.
*/
if (res == ULONG_MAX && errno != 0)
{
/* Il y a eu une erreur ! */
(void)fprintf(stderr, "Impossible de convertir le nombre '%s':
%s.\n",
nombre, strerror(errno));
}
else
{
(void)printf("La conversion a t effectue, et la valeur est:
%lu\n", res);
}
}
int main(void)
{
/* 2 puissance 8 : sera toujours accepte */
teste("256");
/* 2^64 - 1 : peut echouer suivant la machine */
teste("18446744073709551615");
return EXIT_SUCCESS;
}
Si le type unsigned long est cod sur 32 bits, on peut obtenir:
La conversion a t effecte, et la valeur est: 256
Impossible de convertir le nombre '18446744073709551615': la valeur est
en dehors de l'intervalle du type spcifi.
Notons bien qu'il est ncessaire de placer errno 0 avant d'utiliser strtoul, car elle
pourrait contenir une valeur non nulle, mme au lancement du programme.
Erreurs
Rfrences
[1] errno peut tre dfinie par une macro ou un identificateur. N'essayez donc pas de rcuprer son adresse.
Mathmatiques
Pour pouvoir utiliser les fonctions mathmatiques, il faut utiliser l'en-tte math, ainsi que
errno.h pour grer les erreurs :
#include <math.h>
#include <errno.h>
Comme pour un certain nombre de fonctions de la biliothques standard, il est en effet
ncessaire d'utiliser errno pour dtecter l'erreur d'une fonction mathmatique (voir le
chapitre sur la gestion d'erreurs pour voir comment utiliser errno).
Note : Sur certains compilateurs comme GCC, il est ncessaire d'ajouter durant l'dition
des liens une option pour que la librairie mathmatique soit lie au programme. Pour
GCC, cette option est -lm. Sans cette option, le programme pourra compiler, mais le
rsultat l'excution sera surprenant...
Exponentiations
double exp ( double x );
double pow ( double x, double y );
exp calcule e lev la puissance de x (
) o e est la base des logarithmes naturels (ln(e)
= 1). pow calcule la valeur de x lv la puissance y (
).
Erreurs
La fonction pow peut dclencher l'erreur suivante :
EDOM : x est ngatif, et y nest pas un entier.
Logarithmes
double log ( double x );
double log10 ( double x );
log calcule le logarithme nperien de x (not gnralement ln(x) en mathmatiques). log10
calcule le logarithme base 10 de x.
Erreurs
EDOM : x est ngatif ;
ERANGE : x est nul.
103
Mathmatiques
Racine carre
double sqrt ( double x );
Renvoie la racine carre de x.
Erreurs
EDOM : x est ngatif.
Erreurs
EDOM : x est infrieur -1 ou suprieur 1.
Arc tangente
double atan ( double x );
double atan2 ( double y, double x );
104
Gestion de la mmoire
Gestion de la mmoire
La gestion dynamique de la mmoire en C se fait l'aide de principalement deux fonctions
de la bibliothque standard :
malloc, pour l'allocation dynamique de mmoire;
free, pour la libration de mmoire pralablement alloue avec malloc.
Deux autres fonctions permettent de grer plus finement la mmoire :
calloc, pour allouer dynamiquement de la mmoire, comme malloc, qui a pralablement
t initialise 0;
realloc, pour modifier la taille d'une zone mmoire dj alloue.
Ces fonctions sont dclares dans l'en-tte stdlib.h.
105
Gestion de la mmoire
Au niveau des inconvnients, on citera essentiellement un problme de performances.
Plusieurs niveaux d'indirection impliquent de multiple lectures en mmoire, extrmement
pnalisant en terme de temps d'excution, au point o des caches sont ncessaires pour
garantir des vitesses acceptables. Mme si RAM veut dire mmoire accs alatoire, il faut
bien garder l'esprit qu'un accs purement squentiel (adresse mmoire croissante) peut
tre jusqu' cent fois plus rapide qu'une mthode d'accs qui met sans cesse dfaut ce
systme de cache.
Un processus peut donc demander au sytme de rserver pour son usage exclusif un
secteur mmoire de taille dtermine. Il peut galement demander au sytme de modifier
la taille d'une zone prcdemment rserve ou de la librer s'il n'en a plus l'utilit.
106
Gestion de la mmoire
/* dcider du traitement en cas d'erreur */
}
Si on souhaite allouer de l'espace pour une structure de donnes plus complexe :
typedef struct{
int a;
double b;
MaStructure *suivant;
} MaStructure;
/* Dclaration et initialisation */
MaStructure *ptr = NULL;
/* Allocation */
ptr = (MaStructure *)malloc(sizeof(MaStructure));
if (ptr != NULL)
{
/* Initialisation de la structure nouvellement cre */
ptr->a = 10;
ptr->b = 3.1415;
ptr->suivant = NULL;
}
107
Gestion de la mmoire
108
/* premier entier : 3
*/
Le pointeur peut tre utilis comme un tableau classique pour accder aux lments qu'il
contient :
ptr[0] = 3; /* quivaut
ptr[1] = 1; /* quivaut
ptr[2] = 4; /* quivaut
*ptr = 3;
*(ptr+1) = 1;
*(ptr+2) = 4;
*/
*/
*/
Notez que calloc place tous les bits zro, mais que ce n'est pas ncessairement une
reprsentation valide pour un pointeur nul ni pour le nombre zro en reprsentation
flottante. Ainsi, pour initialiser zro un tableau de double de manire portable, par
exemple, il est ncessaire d'assigner la valeur 0.0 chaque lment du tableau. tant
donn qu'on initialise chaque lment manuellement , on peut dans ce cas utiliser
malloc plutt que calloc (la premire tant normalement beaucoup plus rapide que la
seconde).
Gestion de la mmoire
109
realloc
Il arrive frquemment qu'un bloc allou n'ait pas la taille suffisante pour accueillir de
nouvelles donnes. La fonction realloc est utilise pour changer (agrandir ou rduire) la
taille d'une zone alloue par malloc, calloc, ou realloc.
Syntaxe :
void * realloc(void * ancien_bloc, size_t nouvelle_taille);
realloc tentera de rajuster la taille du bloc point par ancien_bloc la nouvelle taille
spcifie. noter :
si nouvelle_taille vaut zro, l'appel est quivalent free(ancien_bloc).
si ancien_bloc est nul, l'appel est quivalent malloc(nouvelle_taille).
En cas de succs, realloc alloue un espace mmoire de taille nouvelle_taille, copie le
contenu point par le paramtre pointeur dans ce nouvel espace (en tronquant
ventuellement si la nouvelle taille est infrieure la prcdente), puis libre l'espace
point et retourne un pointeur vers la nouvelle zone mmoire.
En cas d'chec, cette fonction ne libre pas l'espace mmoire actuel, et retourne une
adresse nulle.
Notez bien que realloc ne peut que modifier des espaces mmoires qui ont t allous par
malloc, calloc, ou realloc. En effet, autoriser realloc manipuler des espaces
mmoires qui ne sont pas issus des fonctions de la bibliothque standard pourrait causer
des erreurs, ou des incohrences graves de l'tat du processus. En particulier, les tableaux,
automatiques comme statiques, ne peuvent tre passs realloc, comme illustr par le
code suivant :
Ce code contient une erreur volontaire !
void f(void)
{
int tab[10];
/* ... */
int *ptr = realloc(tab, 20 * sizeof(int));
/* ... */
}
Lorsque realloc reoit la valeur de tab, qui est un pointeur sur le premier lment (i.e.
&tab[0]), il ne peut la traiter, et le comportement est indfini. Sur cet exemple, il est facile
de voir l'erreur, mais dans l'exemple suivant, la situation est plus dlicate :
#include <stdint.h> /* pour SIZE_MAX */
#include <stdlib.h>
/* 'double' essaye de doubler l'espace mmoire point par ptr.
*
* En cas de succs, la valeur renvoye est un pointeur vers le nouvel
espace mmoire, et l'ancienne
* valeur de ptr est invalide.
* En cas d'chec, l'espace point par ptr n'est pas modifi, et la
valeur NULL est renvoye.
Gestion de la mmoire
*/
void *double(void *ptr, size_t n)
{
void *tmp = NULL;
if ((ptr != NULL) && (n != 0) && (n <= SIZE_MAX / 2))
{
tmp = realloc(ptr, 2 * n);
}
return tmp;
}
La fonction double en elle-mme ne comporte pas d'erreur, mais elle peut causer des
plantages suivant la valeur de ptr qui lui est passe. Pour viter des erreurs, il faudrait
que la documentation de la fonction prcise les contraintes sur la valeur de ptr... et que les
programmeurs qui l'utilisent y fassent attention.
On peut aussi noter que, quand realloc russit, le pointeur renvoy peut trs bien tre
gal au pointeur initial, ou lui tre diffrent. En particulier, il n'y a aucune garantie que,
quand on diminue la taille de la zone mmoire, il le fasse sur place . C'est trs probable,
car c'est ce qui est le plus facile et rapide faire du point de vue de l'implmentation, mais
rien ne l'empche par exemple de chercher un autre espace mmoire disponible qui aurait
exactement la taille voulue, au lieu de garder la zone mmoire initiale.
On peut noter le test (n <= SIZE_MAX / 2). Il permet d'viter un dbordement entier : si n
tait suprieur cette valeur, le produit n * 2 devrait avoir une valeur suprieure
SIZE_MAX, qui est la plus grande valeur reprsentable par le type size_t. Lorsque cette
valeur est passe realloc, la conversion en size_t, qui est un type entier non sign, se
fera modulo SIZE_MAX + 1, et donc la fonction recevra une valeur diffrente de celle
voulue. Si le test n'tait pas fait, double pourrait ainsi retourner l'appelant une zone
mmoire de taille infrieure celle demande, ce qui causerait des bogues. Ce genre de
bogue (non spcifique realloc) est trs difficile dtecter, car n'apparat que lorsque
l'on atteint des valeurs limites, ce qui est assez rare, et le problme peut n'tre visible que
bien longtemps aprs que l'erreur de calcul soit faite.
Gestion d'erreur
Pour grer correctement le cas d'chec, on ne peut faire ainsi:
Ce code contient une erreur volontaire !
int *ptr = malloc(10 * sizeof(int));
if (ptr != NULL)
{
/* ... */
/* On se rend compte qu'on a besoin d'un peu plus de place. */
ptr = realloc(ptr, 20 * sizeof(int));
/* ... */
}
En effet, si realloc choue, la valeur de ptr est alors nulle, et on aura perdu la rfrence
vers l'espace de taille 10 * sizeof(int) qu'on a dj allou. Ce type d'erreur s'appelle
110
Gestion de la mmoire
111
Gestion de la mmoire
112
*erreur = 0;
while (lire_entree(&valeur) != 0)
{
if (i >= max)
{
/* Il n'y a plus de place pour stocker 'valeur' dans
'ptr[i]' */
max = max + 10;
int *tmp = realloc(ptr, max * sizeof(int));
if (tmp == NULL)
{
/* realloc a chou : on sort de la boucle */
*erreur = 1;
break;
}
else
{
ptr = tmp;
}
}
ptr[i] = valeur;
i++;
}
*taille = i;
return ptr;
}
Ici, on utilise max pour se souvenir du nombre d'lments que contient la zone de mmoire
alloue, et i pour le nombre d'lments effectivement utiliss. Quand on a pu lire un entier
depuis l'entre, et que i vaut max, on sait qu'il n'y a plus de place disponible et qu'il faut
augmenter la taille de la zone de mmoire. Ici, on incrmente la taille max de 10 chaque
fois, mais il est aussi possible de la multiplier par 2, ou d'utiliser toute autre formule. On
utilise par ailleurs le fait que, quand le pointeur envoy realloc est nul, la fonction se
comporte comme malloc.
Le choix de la formule de calcul de max utiliser chaque fois que le tableau est rempli
rsulte d'un compromis :
augmenter max peu peu permet de ne pas gaspiller trop de mmoire, mais on appellera
realloc trs souvent.
augmenter trs vite max gnre relativement peu d'appels realloc, mais une grande
partie de la zone mmoire peut tre perdue.
Une allocation mmoire est une opration qui peut tre coteuse en terme de temps, et un
grand nombre d'allocations mmoire peut fractionner l'espace mmoire disponible, ce qui
alourdit la tche de l'allocateur de mmoire, et au final peut causer des pertes de
performance de l'application. Aucune formule n'est universelle, chaque situation doit tre
tudie en fonction de diffrents paramtres (systme d'exploitation, capacit mmoire,
Gestion de la mmoire
vitesse du matriel, taille habituelle/maximale de l'entre...). Toutefois, l'essentiel est bien
souvent d'avoir un algorithme qui marche, l'optimisation tant une question secondaire.
Dans une telle situation, utilisez d'abord une mthode simple (incrmentation ou
multiplication par une constante), et n'en changez que si le comportement du programme
devient gnant.
113
Gestion de la mmoire
/* libration */
free( ptr );
ptr = NULL;
*ptr2 = 10; /* <- rsultat imprvisible (plantage de l'application, ...) */
En rgle gnrale, il faut viter que plusieurs variables pointent la mme zone mmoire
alloue.
L'utilisation d'un outil de vrification statique permet de dtecter ce genre d'erreur.
Fuite mmoire
La perte du pointeur associ un secteur mmoire rend impossible la libration du secteur
l'aide de free. On qualifie cette situation de fuite mmoire, car des secteurs demeurent
rservs sans avoir t dsallous. Cette situation perdure jusqu' la fin du programme
principal. Voyons l'exemple suivant:
Ce code contient une erreur volontaire !
int i = 0;
// un compteur
int* ptr = NULL; // un pointeur
while (i < 1001) {
ptr = malloc(sizeof(int)); // on crase la valeur prcdente de ptr
par une nouvelle
if (ptr != NULL)
{
/* traitement ....*/
}
}
la sortie de cette boucle on a allou un millier d'entiers soit environ 2000 octets, que l'on
ne peut pas librer car le pointeur ptr est cras chaque itration (sauf la dernire). La
mmoire disponible est donc peu peu grignote jusqu'au dpassement de sa capacit. Ce
genre d'erreurs est donc proscrire pour des processus tournant en boucle pendant des
heures, voire indfiniment. Si son caractre cumulatif la rend difficile dtecter, il existe
de nombreux utilitaires destins traquer la moindre fuite.
114
115
116
Rfrences
[1] http:/ / en. wikipedia. org/ wiki/ Programmation
117
Conclusion
Conclusion
Le C est un langage plein de paradoxes. Pouvant aussi bien s'accommoder d'applications
bas niveau que proposer des interfaces relativement proches des mthodes orientes
objets, ce langage a de quoi sduire un large public. Pourtant, force est de constater qu'il a
fait souffrir beaucoup de personnes et pas seulement les programmeurs. Issu de l'poque
o la mmoire et les capacits de calcul taient encore des denres rares et chres, les
pionniers de la programmation ont trop souvent privilgi le ct bas niveau de ce langage.
Il en rsulta des applications difficilement maintenables, abusant des aspects les plus
complexes ou non-portables qu'offre le C pour conomiser le moindre cycle processeur.
C'est de cette poque que sa rputation d'assembleur plus compliqu et plus lent que
l'assembleur s'est forge, clipsant ses aspects de plus haut niveau, qui pourtant mritent
une meilleure estime.
Les fonctions de la bibliothque standard et surtout le langage lui-mme contiennent
beaucoup de piges , notamment par les comportements indfinis, ou dpendants de
l'implmentation, qui rendent des bogues parfois trs difficiles dcouvrir. Le
programmeur doit comprendre que le C lui fournit de grandes possibilits, mais qu'en
retour il lui demande une rigueur d'autant plus grande. Apprivoiser ce langage peut
ncessiter une longue phase d'apprentissage. Nous esprons que cet ouvrage vous aura
permis d'apprhender le C avec plus de srnit et de casser certains mythes qui ont
dcidment la vie bien dure.
Il existe beaucoup de bibliothques qui proposent au programmeur C des outils pour
tendre la bibliothque standard, et permettent ainsi la gestion d'interfaces graphiques, la
programmation rseau, etc. Celles-ci sont trs riches, et bien trop nombreuses pour tre
abordes dans ce livre. Wikilivres propose des livres tudiant certaines de ces
bibliothques. Pour les autres, Internet et ses moteurs de recherche restent de loin les
meilleurs outils pour trouver ce dont vous aurez ncessairement besoin.
118
Bibliographie
119
Bibliographie
Vous trouverez ici des rfrences d'ouvrages ou de sites internet ayant servi l'laboration
de ce wikilivre, ou pouvant tre d'intrt pour une connaissance plus approfondie du C.
Livres
Le K&R :
Brian W. Kernighan et Dennis M. Ritchie. Le Langage C: Norme ANSI, 2e d., Dunod,
(ISBN 2-100-05116-4) http://c.developpez.com/livres/#L2100487345 prsentation en ligne
[6]
Compilateurs C
Le wiki de comp.lang.c liste ici [7] un certain nombre de compilateurs en prcisant leur
support des normes C90 et C99 (en).
Autres ressources
Tutoriels
Les bases du langage C
[8]
Histoire du C
The Development of the C Language [9] par Dennis Ritchie (en).
Historique de C [10] par Marc Mongenet.
Bibliographie
Rfrences
[1] http:/ / www. open-std. org/ jtc1/ sc22/ wg14/
[2] http:/ / www. iso. org/ iso/ iso_catalogue/ catalogue_tc/ catalogue_detail. htm?csnumber=50510
[3] http:/ / www. lysator. liu. se/ c/ rat/ title. html
[4] http:/ / clc-wiki. net/ wiki/ C_standardisation:ISO
[5] http:/ / www. developpez. com
[6] http:/ / c. developpez. com/ livres
[7] http:/ / clc-wiki. net/ wiki/ C_resources:Compilers
[8] http:/ / neofutur. net/ langage_c/ examples_language_C/ langage_C_les_bases/ langage_C_bases. html
[9] http:/ / cm. bell-labs. com/ cm/ cs/ who/ dmr/ chist. html
[10] http:/ / marc. mongenet. ch/ Articles/ C/ index. html
120
121
122
Licence
Licence
Creative Commons Attribution-Share Alike 3.0 Unported
http:/ / creativecommons. org/ licenses/ by-sa/ 3. 0/
123