100% ont trouvé ce document utile (6 votes)
7K vues1 117 pages

Guide Complet C

Programmation C

Transféré par

Chehira Doghmen
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
100% ont trouvé ce document utile (6 votes)
7K vues1 117 pages

Guide Complet C

Programmation C

Transféré par

Chehira Doghmen
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd

Résumé

La référence des étudiants et des développeurs professionnels


Cet ouvrage de référence a été conçu pour les étudiants de niveau avancé en
programmation et pour les développeurs souhaitant approfondir leur
connaissance du C ou trouver une réponse précise aux problèmes techniques
rencontrés lors du développement d’applications professionnelles.
Exhaustif et précis, l’ouvrage explore le langage C dans ses moindres recoins. Il
clarifie les points délicats et les ambiguïtés du langage, analyse le comportement
qu’on peut attendre d’un code ne respectant pas la norme ou confronté à une
situation d’exception. Tout au long de l’ouvrage, des notes soulignent les
principales différences syntaxiques entre le C et le C++, de manière à établir des
passerelles entre les deux langages.
Une annexe présente les spécificités des deux dernières moutures de la norme
ISO du langage, connues sous les noms C99 et C11.

Au sommaire
Les bases du langage C. Historique, programmes et compilation, variables et objets. Structure d’un
programme source. Jeux de caractères, identificateurs, mots-clés, séparateurs, format libre,
commentaires, tokens. Types de base. Types entiers, types caractère, types flottants, fichiers limits.h
et float.h. Opérateurs et expressions. Opérateurs arithmétiques, relationnels, logiques, de
manipulation de bits, d’affectation et d’incrémentation, de cast ; conversions numériques ; opérateur
conditionnel, séquentiel, sizeof ; priorité et associativité ; expressions constantes. Instructions
exécutables. Expressions, blocs ; instructions if, switch, do… while, while, for, break, continue ;
schémas de boucles utiles ; goto et les étiquettes. Tableaux. Déclaration, utilisation, débordement
d’indice, tableau de tableaux, initialisation d’un tableau. Pointeurs. Variable de type pointeur et
opérateur *, déclaration, propriétés arithmétiques ; opérateurs +, -, &, * et [] ; pointeurs et tableaux,
pointeur NULL, pointeurs et affectation, pointeurs génériques, comparaisons, conversions par cast.
Fonctions. Définition, déclaration et appel d’une fonction, transmission d’arguments, tableaux
transmis en arguments, variables globales et variables locales, pointeurs sur des fonctions. Entrées-
sorties standard. printf, putchar, scanf, getchar. Chaînes de caractères. Création, utilisation et
modification ; écriture et lecture avec puts, printf, gets, gets_s, scanf ; fonctions de manipulation de
chaînes (strcpy, strcat…) et de suites d’octets (memcpy, memmove…) ; fonctions de conversion en
numérique (strtod…). Structures, unions, énumérations et champs de bits. Déclaration,
représentation en mémoire, utilisation. Instruction typedef et synonymes. Fichiers. Fichiers binaires
et formatés ; fwrite et fread ; fprintf, fscanf, fputs et fgets ; fputc et fgetc ; accès direct avec fseek,
ftell… ; fopen et les modes d’ouverture ; flux prédéfinis stdin, stdout et stderr. Gestion dynamique
de la mémoire. Principes, fonctions malloc, free, calloc, realloc ; exemples d’utilisation : tableaux
dynamiques et listes chaînées. Préprocesseur. Directives et caractère #, définition de symboles et de
macros, directives de compilation conditionnelle, d’inclusion de fichier source, etc. Déclarations.
Syntaxe générale, spécificateurs, définition de fonction, interprétation de déclarations, écriture de
déclarateurs. Fiabilisation des lectures au clavier. Utilisation de scanf, gets, gets_s, fgets.
Catégories de caractères et fonctions associées. Gestion des programmes de grande taille.
Avantages et inconvénients des variables globales, partage d’identificateurs entre plusieurs fichiers
source. Fonctions à arguments variables. Règles d’écriture, macros va_start, va_arg, va_end,
fonctions vprintf, vfprintf et vsprintf. Communication avec l’environnement. Arguments reçus par la
fonction main, terminaison d’un programme, fonctions getenv et system, signaux. Caractères
étendus. Type wchar_t, fonctions mblen, mbtowc et wctomb, chaînes de caractères étendus.
Localisation. Mécanisme, fonctions setlocale et localeconv. Récursivité. Principes et exemples,
empilement des appels. Branchements non locaux. Macros setjmp et longjmp. Incompatibilités
entre C et C++. Bibliothèque standard du C. assert.h, ctype.h, errno.h, locale.h, math.h, setjmp.h,
signal.h, stdarg.h, stddef.h, stdio.h, stdlib.h, string.h, time.h. Nouveautés des normes ISO C99 et
C11. Contraintes supplémentaires, division d’entiers, tableaux de dimension variable, nouveaux
types, caractères étendus et Unicode, pointeurs restreints, structures anonymes, expressions
génériques, fonctions de vérification du débordement mémoire, threads, etc.
Biographie auteur

C. Delannoy
Ingénieur informaticien au CNRS, Claude Delannoy possède une grande pratique de la formation
continue et de l’enseignement supérieur. Réputés pour la qualité de leur démarche pédagogique, ses
ouvrages sur les langages et la programmation totalisent plus de 300 000 exemplaires vendus.

[Link]
Le guide complet
du langage
C
Claude Delannoy
ÉDITIONS EYROLLES
61, bd Saint-Germain
75240 Paris Cedex 05
[Link]

Le présent ouvrage est une nouvelle édition du livre publié à l’origine sous le titre
« La référence du C norme ANSI/ISO », puis au format semi-poche sous le titre « Langage C ».

En application de la loi du 11 mars 1957, il est interdit de reproduire intégralement ou partiellement le


présent ouvrage, sur quelque support que ce soit, sans l’autorisation de l’Éditeur ou du Centre Français
d’exploitation du droit de copie, 20, rue des Grands Augustins, 75006 Paris.
© Groupe Eyrolles, 1999, 2008, 2014, ISBN : 978-2-212-14012-5
AUX EDITIONS EYROLLES

Du même auteur

C. DELANNOY. – Programmer en langage C. Avec exercices corrigés.


N°14010, 5e édition, 2009, 276 pages (réédition avec nouvelle présentation,
2014).

C. DELANNOY. – Exercices en langage C.


N°11105, 2002, 2010 pages.

C. DELANNOY. – S’initier à la programmation et à l’orienté objet.


Avec des exemples en C, C++, C#, Python, Java et PHP.
N°14011, 2e édition, 2014, 382 pages.

C. DELANNOY. – Programmer en langage C++.


N°14008, 8e édition, 2011, 820 pages (réédition avec nouvelle présentation,
2014).

C. DELANNOY. – Exercices en langage C++.


N°12201, 3e édition, 2007, 336 pages.

C. Delannoy. – Programmer en Java. Java 8.


N°14007, 9e édition, 2014, 940 pages.

C. DELANNOY. – Exercices en Java.


N°14009, 4e édition, 2014, 360 pages.

Autres ouvrages

B. MEYER. – Conception et programmation orientées objet.


N°12270, 2008, 1222 pages.

P. ROQUES. – UML 2 par la pratique


N°12565, 7e édition, 2009, 396 pages.

G. SWINNEN. – Apprendre à programmer avec Python 3.


N°13434, 3e édition, 2012, 435 pages.

C. BLAESS. – Développement système sous Linux.


Ordonnancement multitâches, gestion mémoire, communications,
programmation réseau.
N°12881, 3e édition, 2011, 1004 pages.

C. BLAESS. – Solutions temps réel sous Linux. Avec 50 exercices corrigés.


N°13382, 2012, 294 pages.
Table des matières

Avant-propos
À qui s’adresse ce livre ?
Structure de l’ouvrage
À propos des normes ANSI/ISO
À propos de la fonction main
Remerciements
CHAPITRE 1
Généralités
1. Historique du langage C
2. Programme source, module objet et programme exécutable
3. Compilation en C : existence d’un préprocesseur
4. Variable et objet
4.1 Définition d’une variable et d’un objet
4.2 Utilisation d’un objet
5. Lien entre objet, octets et caractères
6. Classe d’allocation des variables
CHAPITRE 2
Les éléments constitutifs d’un programme source
1. Jeu de caractères source et jeu de caractères d’exécution
1.1 Généralités
1.2 Commentaires à propos du jeu de caractères source
1.3 Commentaires à propos du jeu minimal de caractères d’exécution
2. Les identificateurs
3. Les mots-clés
4. Les séparateurs et les espaces blancs
5. Le format libre
6. Les commentaires
7. Notion de token
7.1 Les différentes catégories de tokens
7.2 Décomposition en tokens
CHAPITRE 3
Les types de base
1. Les types entiers
1.1 Les six types entiers
1.2 Représentation mémoire des entiers et limitations
1.3 Critères de choix d’un type entier
1.4 Écriture des constantes entières
1.5 Le type attribué par le compilateur aux constantes entières
1.6 Exemple d’utilisation déraisonnable de constantes hexadécimales
1.7 Pour imposer un type aux constantes entières
1.8 En cas de dépassement de capacité dans l’écriture des constantes
entières
2. Les types caractère
2.1 Les deux types caractère
2.2 Caractéristiques des types caractère
2.3 Écriture des constantes caractère
2.4 Le type des constantes caractère
3. Le fichier limits.h
3.1 Son contenu
3.2 Précautions d’utilisation
4. Les types flottants
4.1 Rappels concernant le codage des nombres en flottant
4.2 Le modèle proposé par la norme
4.3 Les caractéristiques du codage en flottant
4.4 Représentation mémoire et limitations
4.5 Écriture des constantes flottantes
4.6 Le type des constantes flottantes
4.7 En cas de dépassement de capacité dans l’écriture des constantes
5. Le fichier float.h
6. Déclarations des variables d’un type de base
6.1 Rôle d’une déclaration
6.2 Initialisation lors de la déclaration
6.3 Les qualifieurs const et volatile
CHAPITRE 4
Les opérateurs et les expressions
1. Généralités
1.1 Les particularités des opérateurs et des expressions en C
1.2 Priorité et associativité
1.3 Pluralité
1.4 Conversions implicites
1.5 Les différentes catégories d’opérateurs
2. Les opérateurs arithmétiques
2.1 Les différents opérateurs numériques
2.2 Comportement en cas d’exception
3. Les conversions numériques implicites
3.1 Introduction
3.2 Les conversions numériques d’ajustement de type
3.3 Les promotions numériques
3.4 Combinaisons de conversions
3.5 Cas particulier des arguments d’une fonction
4. Les opérateurs relationnels
4.1 Généralités
4.2 Les six opérateurs relationnels du langage C
4.3 Leur priorité et leur associativité
5. Les opérateurs logiques
5.1 Généralités
5.2 Les trois opérateurs logiques du langage C
5.3 Leur priorité et leur associativité
5.4 Les opérandes de && et de || ne sont évalués que si nécessaire
6. Les opérateurs de manipulation de bits
6.1 Présentation des opérateurs de manipulation de bits
6.2 Les opérateurs « bit à bit »
6.3 Les opérateurs de décalage
6.4 Applications usuelles des opérateurs de manipulation de bits
7. Les opérateurs d’affectation et d’incrémentation
7.1 Généralités
7.2 La lvalue
7.3 L’opérateur d’affectation simple
7.4 Tableau récapitulatif : l’opérateur d’affectation simple
7.5 Les opérateurs d’affectation élargie
8. Les opérateurs de cast
8.1 Généralités
8.2 Les opérateurs de cast
9. Le rôle des conversions numériques
9.1 Conversion d’un type flottant vers un autre type flottant
9.2 Conversion d’un type flottant vers un type entier
9.3 Conversion d’un type entier vers un type flottant
9.4 Conversion d’un type entier vers un autre type entier
9.5 Cas particuliers des conversions d’entier vers caractère
9.6 Tableau récapitulatif des conversions numériques
10. L’opérateur conditionnel
10.1 Introduction
10.2 Rôle de l’opérateur conditionnel
10.3 Contraintes et conversions
10.4 La priorité de l’opérateur conditionnel
11. L’opérateur séquentiel
12. L’opérateur sizeof
12.1 L’opérateur sizeof appliqué à un nom de type
12.2 L’opérateur sizeof appliqué à une expression
13. Tableau récapitulatif : priorités et associativité des opérateurs
14. Les expressions constantes
14.1 Introduction
14.2 Les expressions constantes d’une manière générale
CHAPITRE 5
Les instructions exécutables
1. Généralités
1.1 Rappels sur les instructions de contrôle
1.2 Classification des instructions exécutables du langage C
2. L’instruction expression
2.1 Syntaxe et rôle
2.2 Commentaires
3. L’instruction composée ou bloc
3.1 Syntaxe d’un bloc
3.2 Commentaires
3.3 Déclarations dans un bloc
3.4 Cas des branchements à l’intérieur d’un bloc
4. L’instruction if
4.1 Syntaxe et rôle de l’instruction if
4.2 Exemples d’utilisation
4.3 Cas des if imbriqués
4.4 Traduction de choix en cascade
5. L’instruction switch
5.1 Exemple introductif
5.2 Syntaxe usuelle et rôle de switch
5.3 Commentaires
5.4 Quelques curiosités de l’instruction switch
6. Choix entre if et switch
7. Les particularités des boucles en C
7.1 Rappels concernant la programmation structurée
7.2 Les boucles en C
8. L’instruction do … while
8.1 Syntaxe
8.2 Rôle
8.3 Exemples d’utilisation
9. L’instruction while
9.1 Syntaxe
9.2 Rôle
9.3 Lien entre while et do … while
9.4 Exemples d’utilisation
10. L’instruction for
10.1 Introduction
10.2 Syntaxe
10.3 Rôle
10.4 Lien entre for et while
10.5 Commentaires
10.6 Exemples d’utilisation
11. Conseils d’utilisation des différents types de boucles
11.1 Boucle définie
11.2 Boucle indéfinie
12. L’instruction break
12.1 syntaxe et rôle
12.2 Exemple d’utilisation
12.3 Commentaires
13. L’instruction continue
13.1 Syntaxe et rôle
13.2 Exemples d’utilisation
13.3 Commentaires
14. Quelques schémas de boucles utiles
14.1 Boucle à sortie intermédiaire
14.2 Boucles à sorties multiples
15. L’instruction goto et les étiquettes
15.1 Les étiquettes
15.2 Syntaxe et rôle
15.3 Exemples et commentaires
CHAPITRE 6
Les tableaux
1. Exemple introductif d’utilisation d’un tableau
2. Déclaration des tableaux
2.1 Généralités
2.2 Le type des éléments d’un tableau
2.3 Déclarateur de tableau
2.4 La dimension d’un tableau
2.5 Classe de mémorisation associée à la déclaration d’un tableau
2.6 Les qualifieurs const et volatile
2.7 Nom de type correspondant à un tableau
3. Utilisation d’un tableau
3.1 Les indices
3.2 Un identificateur de tableau n’est pas une lvalue
3.3 Utilisation d’un élément d’un tableau
3.4 L’opérateur sizeof et les tableaux
4. Arrangement d’un tableau et débordement d’indice
4.1 Les éléments d’un tableau sont alloués de manière consécutive
4.2 Aucun contrôle n’est effectué sur la valeur de l’indice
5. Cas des tableaux de tableaux
5.1 Déclaration des tableaux à deux indices
5.2 Utilisation d’un tableau à deux indices
5.3 Peut-on parler de lignes et de colonnes d’un tableau à deux indices ?
5.4 Arrangement en mémoire d’un tableau à deux indices
5.5 Cas des tableaux à plus de deux indices
6. Initialisation de tableaux
6.1 Initialisation par défaut des tableaux
6.2 Initialisation explicite des tableaux
CHAPITRE 7
Les pointeurs
1. Introduction à la notion de pointeur
1.1 Attribuer une valeur à une variable de type pointeur
1.2 L’opérateur * pour manipuler un objet pointé
2. Déclaration des variables de type pointeur
2.1 Généralités
2.2 Le type des objets désignés par un pointeur
2.3 Déclarateur de pointeur
2.4 Classe de mémorisation associée à la déclaration d’un pointeur
2.5 Les qualifieurs const et volatile
2.6 Nom de type correspondant à un pointeur
3. Les propriétés des pointeurs
3.1 Les propriétés arithmétiques des pointeurs
3.2 Lien entre pointeurs et tableaux
3.3 Ordre des pointeurs et ordre des adresses
3.4 Les restrictions imposées à l’arithmétique des pointeurs
4. Tableaux récapitulatifs : les opérateurs +, -, &, * et []
5. Le pointeur NULL
6. Pointeurs et affectation
6.1 Prise en compte des qualifieurs des objets pointés
6.2 Les autres possibilités d’affectation
6.3 Tableau récapitulatif
6.4 Les affectations élargies += et -= et les incrémentations ++ et --
7. Les pointeurs génériques
7.1 Généralités
7.2 Déclaration du type void *
7.3 Interdictions propres au type void *
7.4 Possibilités propres au type void *
8. Comparaisons de pointeurs
8.1 Comparaisons basées sur un ordre
8.2 Comparaisons d’égalité ou d’inégalité
8.3 Récapitulatif : les comparaisons dans un contexte pointeur
9. Conversions de pointeurs par cast
9.1 Conversion d’un pointeur en un pointeur d’un autre type
9.2 Conversions entre entiers et pointeurs
9.3 Récapitulatif concernant l’opérateur de cast dans un contexte pointeur
CHAPITRE 8
Les fonctions
1. Les fonctions en C
1.1 Une seule sorte de module en C : la fonction
1.2 Fonction et transmission des arguments par valeur
1.3 Les variables globales
1.4 Les possibilités de compilation séparée
2. Exemple introductif de la notion de fonction en langage C
3. Définition d’une fonction
3.1 Les deux formes de l’en-tête
3.2 Les arguments apparaissant dans l’en-tête
3.3 La valeur de retour
3.4 Classe de mémorisation d’une fonction : extern et static
3.5 L’instruction return
4. Déclaration et appel d’une fonction
4.1 Déclaration sous forme de prototype
4.2 Déclaration partielle (déconseillée)
4.3 Portée d’une déclaration de fonction
4.4 Redéclaration d’une fonction
4.5 Une définition de fonction tient lieu de déclaration
4.6 En cas d’absence de déclaration
4.7 Utilisation de la déclaration dans la traduction d’un appel
4.8 En cas de non-concordance entre arguments muets et arguments
effectifs
4.9 Les fichiers en-tête standards
4.10 Nom de type correspondant à une fonction
5. Le mécanisme de transmission d’arguments
5.1 Cas où la transmission par valeur est satisfaisante
5.2 Cas où la transmission par valeur n’est plus satisfaisante
5.3 Comment simuler une transmission par adresse avec des pointeurs
6. Cas des tableaux transmis en arguments
6.1 Règles générales
6.2 Exemples d’applications
6.3 Pour qu’une fonction dispose de la dimension d’un tableau
6.4 Quelques conseils de style à propos des tableaux en argument
7. Cas particulier des tableaux de tableaux transmis en arguments
7.1 Application des règles générales
7.2 Artifices facilitant la manipulation de tableaux de dimensions
variables
8. Les variables globales
8.1 Exemples introductifs d’utilisation de variables globales
8.2 Les déclarations des variables globales
8.3 Portée des variables globales
8.4 Variables globales et édition de liens
8.5 Les variables globales sont de classe statique
8.6 Initialisation des variables globales
9. Les variables locales
9.1 La portée des variables locales
9.2 Classe d’allocation et initialisation des variables locales
10. Tableau récapitulatif : portée, accès et classe d’allocation des
variables
11. Pointeurs sur des fonctions
11.1 Déclaration d’une variable pointeur sur une fonction
11.2 Affectation de valeurs à une variable pointeur sur une fonction
11.3 Appel d’une fonction par le biais d’un pointeur
11.4 Exemple de paramétrage d’appel de fonctions
11.5 Transmission de fonction en argument
11.6 Comparaisons de pointeurs sur des fonctions
11.7 Conversions par cast de pointeurs sur des fonctions
CHAPITRE 9
Les entrées-sorties standards
1. Caractéristiques générales des entrées-sorties standards
1.1 Mode d’interaction avec l’utilisateur
1.2 Formatage des informations échangées
1.3 Généralisation aux fichiers de type texte
2. Présentation générale de printf
2.1 Notions de format d’entrée, de code de format et de code de
conversion
2.2 L’appel de printf
2.3 Les risques d’erreurs dans la rédaction du format
3. Les principales possibilités de formatage de printf
3.1 Le gabarit d’affichage
3.2 Précision des informations flottantes
3.3 Justification des informations
3.4 Gabarit ou précision variable
3.5 Le code de format g
3.6 Le drapeau + force la présence d’un signe « plus »
3.7 Le drapeau espace force la présence d’un espace
3.8 Le drapeau 0 permet d’afficher des zéros de remplissage
3.9 Le paramètre de précision permet de limiter l’affichage des chaînes
3.10 Cas particulier du type unsigned short int : le modificateur h
4. Description des codes de format des fonctions de la famille printf
4.1 Structure générale d’un code de format
4.2 Le paramètre drapeaux
4.3 Le paramètre de gabarit
4.4 Le paramètre de précision
4.5 Le paramètre modificateur h/l/L
4.6 Les codes de conversion
4.7 Les codes utilisables avec un type donné
5. La fonction putchar
5.1 Prototype
5.2 L’argument de putchar est de type int
5.3 La valeur de retour de putchar
6. Présentation générale de scanf
6.1 Format de sortie, code de format et code de conversion
6.2 L’appel de scanf
6.3 Les risques d’erreurs dans la rédaction du format
6.4 La fonction scanf utilise un tampon
6.5 Notion de caractère invalide et d’arrêt prématuré
6.6 La valeur de retour de scanf
6.7 Exemples de rencontre de caractères invalides
7. Les principales possibilités de scanf
7.1 La présentation des informations lues en données
7.2 Limitation du gabarit
7.3 La fin de ligne joue un rôle ambigu : séparateur ou caractère
7.4 Lorsque le format impose certains caractères dans les données
7.5 Attention au faux gabarit du code C
7.6 Les codes de format de la forme %[…]
8. Description des codes de format des fonctions de la famille de scanf
8.1 Récapitulatif des règles utilisées par ces fonctions
8.2 Structure générale d’un code de format
8.3 Les paramètres * et gabarit
8.4 Le paramètre modificateur h/l/L
8.5 Les codes de conversion
8.6 Les codes utilisables avec un type donné
8.7 Les différences entre les codes de format en entrée et en sortie
9. La fonction getchar
9.1 Prototype et valeur de retour
9.2 Précautions
CHAPITRE 10
Les chaînes de caractères
1. Règles générales d’écriture des constantes chaîne
1.1 Notation des constantes chaîne
1.2 Concaténation des constantes chaîne adjacentes
2. Propriétés des constantes chaîne
2.1 Conventions de représentation
2.2 Emplacement mémoire
2.3 Cas des chaînes identiques
2.4 Les risques de modification des constantes chaîne
2.5 Simulation d’un tableau de constantes chaîne
3. Créer, utiliser ou modifier une chaîne
3.1 Comment disposer d’un emplacement pour y ranger une chaîne
3.2 Comment agir sur le contenu d’une chaîne
3.3 Comment utiliser une chaîne existante
4. Entrées-sorties standards de chaînes
4.1 Généralités
4.2 Écriture de chaînes avec puts
4.3 Écriture de chaînes avec le code de format %s de printf ou fprintf
4.4 Lecture de chaînes avec gets
4.5 Lecture de chaînes avec le code de format %s dans scanf ou fscanf
4.6 Comparaison entre gets et scanf dans les lectures de chaînes
4.7 Limitation de la longueur des chaînes lues sur l’entrée standard
5. Généralités concernant les fonctions de manipulation de chaînes
5.1 Ces fonctions travaillent toujours sur des adresses
5.2 Les adresses sont toujours de type char *
5.3 Certains arguments sont déclarés const, d’autres pas
5.4 Attention aux valeurs des arguments de limitation de longueur
5.5 La fonction strlen
6. Les fonctions de copie de chaînes
6.1 Généralités
6.2 La fonction strcpy
6.3 La fonction strncpy
7. Les fonctions de concaténation de chaînes
7.1 Généralités
7.2 La fonction strcat
7.3 La fonction strncat
8. Les fonctions de comparaison de chaînes
8.1 Généralités
8.2 La fonction strcmp
8.3 La fonction strncmp
9. Les fonctions de recherche dans une chaîne
9.1 Les fonctions de recherche d’un caractère : strchr et strrchr
9.2 La fonction de recherche d’une sous-chaîne : strstr
9.3 La fonction de recherche d’un caractère parmi plusieurs : strpbrk
9.4 Les fonctions de recherche d’un préfixe
9.5 La fonction d’éclatement d’une chaîne : strtok
10. Les fonctions de conversion d’une chaîne en un nombre
10.1 Généralités
10.2 La fonction de conversion d’une chaîne en un double : strtod
10.3 Les fonctions de conversion d’une chaîne en entier : strtol et strtoul
10.4 Cas particulier des fonctions atof, atoi et atol
11. Les fonctions de manipulation de suites d’octets
11.1 Généralités
11.2 Les fonctions de recopie de suites d’octets
11.3 La fonction memcmp de comparaison de deux suites d’octets
11.4 La fonction memset d’initialisation d’une suite d’octets
11.5 La fonction memchr de recherche d’une valeur dans une suite
d’octets
CHAPITRE 11
Les types structure, union et énumération
1. Exemples introductifs
1.1 Exemple d’utilisation d’une structure
1.2 Exemple d’utilisation d’une union
2. La déclaration des structures et des unions
2.1 Définition conseillée d’un type structure ou union
2.2 Déclaration de variables utilisant des types structure ou union
2.3 Déclaration partielle ou déclaration anticipée
2.4 Mixage entre définition et déclaration
2.5 L’espace de noms des identificateurs de champs
2.6 L’espace de noms des identificateurs de types
3. Représentation en mémoire d’une structure ou d’une union
3.1 Contraintes générales
3.2 Cas des structures
3.3 Cas des unions
3.4 L’opérateur sizeof appliqué aux structures ou aux unions
4. Utilisation d’objets de type structure ou union
4.1 Manipulation individuelle des différents champs d’une structure ou
d’une union
4.2 Affectation globale entre structures ou unions de même type
4.3 L’opérateur & appliqué aux structures ou aux unions
4.4 Comparaison entre pointeurs sur des champs
4.5 Comparaison des structures ou des unions par == ou != impossible
4.6 L’opérateur ->
4.7 Structure ou union transmise en argument ou en valeur de retour
5. Exemples d’objets utilisant des structures
5.1 Structures comportant des tableaux
5.2 Structures comportant d’autres structures
5.3 Tableaux de structures
5.4 Structure comportant des pointeurs sur des structures de son propre
type
6. Initialisation de structures ou d’unions
6.1 Initialisation par défaut des structures ou des unions
6.2 Initialisation explicite des structures
6.3 L’initialisation explicite d’une union
7. Les champs de bits
7.1 Introduction
7.2 Exemples introductifs
7.3 Les champs de bits d’une manière générale
7.4 Exemple d’utilisation d’une structure de champs de bits dans une
union
8. Les énumérations
8.1 Exemples introductifs
8.2 Déclarations associées aux énumérations
CHAPITRE 12
La définition de synonymes avec typedef
1. Exemples introductifs
1.1 Définition d’un synonyme de int
1.2 Définition d’un synonyme de int *
1.3 Définition d’un synonyme de int[3]
1.4 Définition d’un synonyme d’un type structure
2. L’instruction typedef d’une manière générale
2.1 Syntaxe
2.2 Définition de plusieurs synonymes
2.3 Imbrication des définitions de synonyme
3. Utilisation de synonymes
3.1 Un synonyme peut s’utiliser comme spécificateur de type
3.2 Un synonyme n’est pas un nouveau type
3.3 Un synonyme peut s’utiliser à la place d’un nom de type
4. Les limitations de l’instruction typedef
4.1 Limitations liées à la syntaxe de typedef
4.2 Cas des tableaux sans dimension
4.3 Cas des synonymes de type fonction
CHAPITRE 13
Les fichiers
1. Généralités concernant le traitement des fichiers
1.1 Notion d’enregistrement
1.2 Archivage de l’information sous forme binaire ou formatée
1.3 Accès séquentiel ou accès direct
1.4 Fichiers et implémentation
2. Le traitement des fichiers en C
2.1 L’absence de la notion d’enregistrement en C
2.2 Notion de flux
2.3 Distinction entre fichier binaire et fichier formaté
2.4 Opérations applicables à un fichier et choix du mode d’ouverture
2.5 Accès séquentiel et accès direct
2.6 Le tampon et sa gestion
3. Le traitement des erreurs de gestion de fichier
3.1 Introduction
3.2 La détection des erreurs en C
4. Les entrées-sorties binaires : fwrite et fread
4.1 Exemple introductif de création séquentielle d’un fichier binaire
4.2 Exemple introductif de liste séquentielle d’un fichier binaire
4.3 La fonction fwrite
4.4 La fonction fread
5. Les opérations formatées avec fprintf, fscanf, fputs et fgets
5.1 Exemple introductif de création séquentielle d’un fichier formaté
5.2 Exemple introductif de liste séquentielle d’un fichier formaté
5.3 La fonction fprintf
5.4 La fonction fscanf
5.5 La fonction fputs
5.6 La fonction fgets
6. Les opérations mixtes portant sur des caractères
6.1 La fonction fputc et la macro putc
6.2 La fonction fgetc et la macro getc
7. L’accès direct
7.1 Exemple introductif d’accès direct à un fichier binaire existant
7.2 La fonction fseek
7.3 La fonction ftell
7.4 Les possibilités de l’accès direct
7.5 Détection des erreurs supplémentaires liées à l’accès direct
7.6 Exemple d’accès indexé à un fichier formaté
7.7 Les fonctions fsetpos et fgetpos
8. La fonction fopen et les différents modes d’ouverture d’un fichier
8.1 Généralités
8.2 La fonction fopen
9. Les flux prédéfinis
CHAPITRE 14
La gestion dynamique
1. Intérêt de la gestion dynamique
2. Exemples introductifs
2.1 Allocation et utilisation d’un objet de type double
2.2 Cas particulier d’un tableau
3. Caractéristiques générales de la gestion dynamique
3.1 Absence de typage des objets
3.2 Notation des objets
3.3 Risques et limitations
3.4 Limitations
4. La fonction malloc
4.1 Prototype
4.2 La valeur de retour et la gestion des erreurs
5. La fonction free
6. La fonction calloc
6.1 Prototype
6.2 Rôle
6.3 Valeur de retour et gestion des erreurs
6.4 Précautions
7. La fonction realloc
7.1 Exemples introductifs
7.2 Prototype
7.3 Rôle
7.4 Valeur de retour
7.5 Précautions
8. Techniques utilisant la gestion dynamique
8.1 Gestion de tableaux dont la taille n’est connue qu’au moment de
l’exécution
8.2 Gestion de tableaux dont la taille varie pendant l’exécution
8.3 Gestion de listes chaînées
CHAPITRE 15
Le préprocesseur
1. Généralités
1.1 Les directives tiennent compte de la notion de ligne
1.2 Les directives et le caractère #
1.3 La notion de token pour le préprocesseur
1.4 Classification des différentes directives du préprocesseur
2. La directive de définition de symboles et de macros
2.1 Exemples introductifs
2.2 La syntaxe de la directive #define
2.3 Règles d’expansion d’un symbole ou d’une macro
2.4 L’opérateur de conversion en chaîne : #
2.5 L’opérateur de concaténation de tokens : ##
2.6 Exemple faisant intervenir les deux opérateurs # et ##
2.7 La directive #undef
2.8 Précautions à prendre
2.9 Les symboles prédéfinis
3. Les directives de compilation conditionnelle
3.1 Compilation conditionnelle fondée sur l’existence de symboles
3.2 Compilation conditionnelle fondée sur des expressions
3.3 Imbrication des directives de compilation conditionnelle
3.4 Exemples d’utilisation des directives de compilation conditionnelle
4. La directive d’inclusion de fichier source
4.1 Généralités
4.2 Syntaxe
4.3 Précautions à prendre
5. Directives diverses
5.1 La directive vide
5.2 La directive #line
5.3 La directive #error
5.4 La directive #pragma
CHAPITRE 16
Les déclarations
1. Généralités
1.1 Les principaux éléments : déclarateur et spécificateur de type
1.2 Les autres éléments
2. Syntaxe générale d’une déclaration
2.1 Forme générale d’une déclaration
2.2 Spécificateur de type structure
2.3 Spécificateur de type union
2.4 Spécificateur de type énumération
2.5 Déclarateur
3. Définition de fonction
3.1 Forme moderne de la définition d’une fonction
3.2 Forme ancienne de la définition d’une fonction
4. Interprétation de déclarations
4.1 Les règles
4.2 Exemples
5. Écriture de déclarateurs
5.1 Les règles
5.2 Exemples
CHAPITRE 17
Fiabilisation des lectures au clavier
1. Généralités
2. Utilisation de scanf
3. Utilisation de gets
4. Utilisation de fgets
4.1 Pour éviter le risque de débordement en mémoire
4.2 Pour ignorer les caractères excédentaires
4.3 Pour traiter l’éventuelle fin de fichier et paramétrer la taille des
chaînes lues
CHAPITRE 18
Les catégories de caractères et les fonctions associées
1. Généralités
1.1 Dépendance de l’implémentation et de la localisation
1.2 Les fonctions de test
2. Les catégories de caractères
3. Exemples
3.1 Pour obtenir la liste de tous les caractères imprimables et leur code
3.2 Pour connaître les catégories des caractères d’une implémentation
4. Les fonctions de transformation de caractères
CHAPITRE 19
Gestion des gros programmes
1. Utilisation de variables globales
1.1 Avantages des variables globales
1.2 Inconvénients des variables globales
1.3 Conseils en forme de compromis
2. Partage d’identificateurs entre plusieurs fichiers source
2.1 Cas des identificateurs de fonctions
2.2 Cas des identificateurs de types ou de synonymes
2.3 Cas des variables globales
CHAPITRE 20
Les arguments variables
1. Écriture de fonctions à arguments variables
1.1 Exemple introductif
1.2 Arguments variables, forme d’en-tête et déclaration
1.3 Contraintes imposées par la norme
1.4 Syntaxe et rôle des macros va_start, va_arg et va_end
2. Transmission d’une liste variable
3. Les fonctions vprintf, vfprintf et vsprintf
CHAPITRE 21
Communication avec l’environnement
1. Cas particulier des programmes autonomes
2. Les arguments reçus par la fonction main
2.1 L’en-tête de la fonction main
2.2 Récupération des arguments reçus par la fonction main
3. Terminaison d’un programme
3.1 Les fonctions exit et atexit
3.2 L’instruction return dans la fonction main
4. Communication avec l’environnement
4.1 La fonction getenv
4.2 La fonction system
5. Les signaux
5.1 Généralités
5.2 Exemple introductif
5.3 La fonction signal
5.4 La fonction raise
CHAPITRE 22
Les caractères étendus
1. Le type wchar_t et les caractères multioctets
2. Notation des constantes du type wchar_t
3. Les fonctions liées aux caractères étendus mblen, mbtowc et wctomb
3.1 Généralités
3.2 La fonction mblen
3.3 La fonction mbtowc
3.4 La fonction wctomb
4. Les chaînes de caractères étendus
5. Représentation des constantes chaînes de caractères étendus
6. Les fonctions liées aux chaînes de caractères étendus : mbstowcs et
wcstombs
6.1 La fonction mbstowcs
6.2 La fonction wcstombs
CHAPITRE 23
Les adaptations locales
1. Le mécanisme de localisation
2. La fonction setlocale
3. La fonction localeconv
CHAPITRE 24
La récursivité
1. Notion de récursivité
2. Exemple de fonction récursive
3. L’empilement des appels
4. Autre exemple de récursivité
CHAPITRE 25
Les branchements non locaux
1. Exemple introductif
2. La macro setjmp et la fonction longjmp
2.1 Prototypes et rôles
2.2 Contraintes d’utilisation
CHAPITRE 26
Les incompatibilités entre C et C++
1. Les incompatibilités raisonnables
1.1 Définition d’une fonction
1.2 Les prototypes en C++
1.3 Fonctions sans valeur de retour
1.4 Compatibilité entre le type void * et les autres pointeurs
1.5 Les déclarations multiples
1.6 L’instruction goto
1.7 Initialisation de tableaux de caractères
2. Les incompatibilités incontournables
2.1 Fonctions sans arguments
2.2 Le qualifieur const
2.3 Les constantes de type caractère
ANNEXE A
La bibliothèque standard C90
1. Généralités
1.1 Les différents fichiers en-tête
1.2 Redéfinition d’une macro standard par une fonction
2. Assert.h : macro de mise au point
3. Ctype.h : tests de caractères et conversions majuscules - minuscules
3.1 Les fonctions de test d’appartenance d’un caractère à une catégorie
3.2 Les fonctions de transformation de caractères
4. Errno.h : gestion des erreurs
4.1 Constantes prédéfinies
4.2 Macros
5. Locale.h : caractéristiques locales
5.1 Types prédéfinis
5.2 Constantes prédéfinies
5.3 Fonctions
6. Math.h : fonctions mathématiques
6.1 Constantes prédéfinies
6.2 Traitement des conditions d’erreur
6.3 Fonctions trigonométriques
6.4 Fonctions hyperboliques
6.5 Fonctions exponentielle et logarithme
6.6 Fonctions puissance
6.7 Autres fonctions
7. Setjmp.h : branchements non locaux
7.1 Types prédéfinis
7.2 Fonctions et macros
8. Signal.h : traitement de signaux
8.1 Types prédéfinis
8.2 Constantes prédéfinies
8.3 Fonctions de traitement de signaux
9. Stdarg.h : gestion d’arguments variables
9.1 Types prédéfinis
9.2 Macros
10. Stddef.h : définitions communes
10.1 Types prédéfinis
10.2 Constantes prédéfinies
10.3 Macros prédéfinies
11. Stdio.h : entrées-sorties
11.1 Types prédéfinis
11.2 Constantes prédéfinies
11.3 Fonctions d’opérations sur les fichiers
11.4 Fonctions d’accès aux fichiers
11.5 Fonctions d’écriture formatée
11.6 Fonctions de lecture formatée
11.7 Fonctions d’entrées-sorties de caractères
11.8 Fonctions d’entrées-sorties sans formatage
11.9 Fonctions agissant sur le pointeur de fichier
11.10 Fonctions de gestion des erreurs d’entrée-sortie
12. Stdlib.h : utilitaires
12.1 Types prédéfinis
12.2 Constantes prédéfinies
12.3 Fonctions de conversion de chaîne
12.4 Fonctions de génération de séquences de nombres pseudo aléatoires
12.5 Fonctions de gestion de la mémoire
12.6 Fonctions de communication avec l’environnement
12.7 Fonctions de tri et de recherche
12.8 Fonctions liées à l’arithmétique entière
12.9 Fonctions liées aux caractères étendus
12.10 Fonctions liées aux chaînes de caractères étendus
13. String.h : manipulations de suites de caractères
13.1 Types prédéfinis
13.2 Constantes prédéfinies
13.3 Fonctions de copie
13.4 Fonctions de concaténation
13.5 Fonctions de comparaison
13.6 Fonctions de recherche
13.7 Fonctions diverses
14. Time.h : gestion de l’heure et de la date
14.1 Types prédéfinis
14.2 Constantes prédéfinies
14.3 Fonctions de manipulation de temps
14.4 Fonctions de conversion
ANNEXE B
Les normes C99 et C11
1. Contraintes supplémentaires (C99)
1.1 Type de retour d’une fonction
1.2 Déclaration implicite d’une fonction
1.3 Instruction return
2. Division d’entiers (C99)
3. Emplacement des déclarations (C99)
4. Commentaires de fin de ligne (C99)
5. Tableaux de dimension variable (C99, facultatif en C11)
5.1 Dans les déclarations
5.2 Dans les en-têtes de fonctions et leurs prototypes
6. Nouveaux types (C99)
6.1 Nouveau type entier long long (C99)
6.2 Types entiers étendus (C99)
6.3 Nouveaux types flottants (C99)
6.4 Le type booléen (C99)
6.5 Les types complexes (C99, facultatif en C11)
7. Nouvelles fonctions mathématiques (C99)
7.1 Généralisation aux trois types flottants (C99)
7.2 Nouvelles fonctions (C99)
7.3 Fonctions mathématiques génériques (C99)
8. Les fonctions en ligne (C99)
9. Les caractères étendus (C99) et Unicode (C11)
10. Les pointeurs restreints (C99)
11. La directive #pragma (C99)
12. Les calculs flottants (C99)
12.1 La norme IEEE 754
12.2 Choix du mode d’arrondi
12.3 Gestion des situations d’exception
12.4 Manipulation de l’ensemble de l’environnement de calcul flottant
13. Structures incomplètes (C99)
14. Structures anonymes (C11)
15. Expressions fonctionnelles génériques (C11)
16. Gestion des contraintes d’alignement (C11)
17. Fonctions vérifiant le débordement mémoire (C11 facultatif)
18. Les threads (C11 facultatif)
19. Autres extensions de C99
20. Autres extensions de C11

Index
Avant-propos

À qui s’adresse ce livre ?


L’objectif de ce livre est d’offrir au développeur un outil de référence clair et
précis sur le langage C tel qu’il est défini par la norme ANSI/ISO. Il s’adresse à
un lecteur possédant déjà de bonnes notions de programmation, qu’elles aient été
acquises à travers la pratique du C ou de tout autre langage. La vocation
première de l’ouvrage n’est donc pas de servir de manuel d’initiation, mais
plutôt de répondre aux besoins des étudiants avancés, des enseignants et des
développeurs qui souhaitent approfondir leur maîtrise du langage ou trouver des
réponses précises aux problèmes techniques rencontrés dans le développement
d’applications professionnelles.
L’ouvrage a été conçu de façon que le lecteur puisse accéder efficacement à
l’information recherchée sans avoir à procéder à une lecture linéaire. La tâche lui
sera facilitée par un index très détaillé, mais aussi par la présence de nombreuses
références croisées et de tableaux de synthèse servant à fois de résumé du
contenu d’une section et d’aiguillage vers ses différentes parties. Il y trouvera
également de nombreux encadrés décrivant la syntaxe des différentes
instructions ou fonctions du langage, ainsi que des tableaux récapitulatifs et des
canevas types.
Comme il se doit, nous couvrons l’intégralité du langage jusque dans ses aspects
les plus marginaux ou les moins usités. Pour qu’une telle exhaustivité reste
exploitable en pratique, nous l’avons largement assortie de commentaires,
conseils ou jugements de valeur ; le lecteur pourra ainsi choisir en toute
connaissance de cause la solution la plus adaptée à son objectif. Notamment, il
sera en mesure de développer des programmes fiables et lisibles en évitant
certaines situations à risque dont la connaissance reste malgré tout indispensable
pour adapter d’anciens programmes. Il peut s’agir là de quelques rares cas où la
norme reste elle-même ambiguë, ou encore d’usages syntaxiques conformes à la
norme mais suffisamment « limites » pour être mal interprétés par certains
compilateurs. Mais, plus souvent, il s’agira de possibilités désuètes, remontant à
l’origine du langage, et dont la norme n’a pas osé se débarrasser, dans le souci de
préserver l’existant. La plupart d’entre elles seront précisément absentes du
langage C++ ; bon nombre de remarques (titrées En C++) visent d’ailleurs à
préparer le lecteur à une éventuelle migration vers ce langage.
Une norme n’a d’intérêt que par la manière dont elle est appliquée. C’est
pourquoi, au-delà de la norme elle-même, nous apportons un certain nombre
d’informations pratiques. Ainsi, nous précisons le comportement qu’on peut
attendre des différents compilateurs existants en cas de non-respect de la
syntaxe. Nous faisons de même pour les différentes situations d’exception qui
risquent d’apparaître lors de l’exécution du programme. Il va de soi que ces
connaissances se révéleront précieuses lors de la phase de mise au point d’un
programme.
Malgré le caractère de référence de l’ouvrage, nous lui avons conservé une
structure comparable à celle d’un cours. Il pourra ainsi être utilisé soit
parallèlement à la phase d’apprentissage, soit ultérieurement, le lecteur
retrouvant ses repères habituels. Accessoirement, il pourra servir de support à un
cours de langage C avancé.
Toujours dans ce même souci pédagogique, nous avons doté l’ouvrage de
nombreux exemples de programmes complets, accompagnés du résultat fourni
par leur exécution. La plupart d’entre eux viennent illustrer une notion après
qu’elle a été exposée. Mais quelques-uns jouent un rôle d’introduction pour les
points que nous avons jugés les plus délicats.
Structure de l’ouvrage
La première partie de l’ouvrage est formée de 15 chapitres qui traitent des
grandes composantes du langage suivant un déroulement classique.
Le chapitre 1 expose quelques notions de base spécifiques au C qui peuvent faire
défaut au programmeur habitué à un autre langage : historique, préprocesseur,
compilation séparée, différence entre variable et objet, classe d’allocation.
Le chapitre 2 présente les principaux constituants élémentaires d’un programme
source : jeu de caractères, identificateurs, mots clés, séparateurs, espaces blancs,
commentaires.
Le chapitre 3 est consacré aux types de base, c’est-à-dire ceux à partir desquels
peuvent être construits tous les autres : entiers, flottants, caractères. Il définit
également de façon précise l’importante notion d’expression constante.
Le chapitre 4 passe en revue les différents opérateurs, à l’exception de quelques
opérateurs dits de référence ([ ], ( ), -> et «.») qui trouvent tout naturellement
leur place dans d’autres chapitres. Il étudie la manière dont sont conçues les
expressions en C, ainsi que les différentes conversions qui peuvent y apparaître :
implicites, explicites par cast, forcées par affectation.
Le chapitre 5 étudie l’ensemble des instructions exécutables du langage, après en
avoir proposé une classification.
Le chapitre 6 traite de l’utilisation naturelle des tableaux à une ou à plusieurs
dimensions. Leur manipulation, particulière au C, par le biais de pointeurs, n’est
examinée que dans le chapitre suivant. De même, le cas des tableaux transmis en
argument d’une fonction n’est traité qu’au chapitre 8.
Le chapitre 7 porte sur les pointeurs : déclarations, propriétés arithmétiques, lien
entre tableau et pointeur, pointeur NULL, affectation, pointeurs génériques,
comparaisons, conversions. Toutefois les pointeurs sur des fonctions ne sont
abordés qu’au chapitre suivant.
Le chapitre 8 est consacré aux fonctions, tant sur le plan de leur définition que
des différentes façons de les déclarer. Il examine notamment le cas des tableaux
transmis en argument, en distinguant les tableaux à une dimension des tableaux à
plusieurs dimensions. En outre, il fait le point sur les variables globales et les
variables locales, aussi bien en ce qui concerne leur déclaration que leur portée,
leur classe d’allocation ou leur initialisation.
Le chapitre 9 étudie les entrées-sorties standard, que nous avons préféré séparer
des fichiers, pour des questions de clarté, malgré le lien étroit qui existe entre les
deux. Ce chapitre décrit en détail les fonctions printf, scanf, puts, gets, putchar et
getchar.

Le chapitre 10 montre comment le C permet de manipuler des chaînes de


caractères. Il passe en revue les différentes fonctions standard correspondantes :
recopie, concaténation, comparaison, recherche. Il traite également des fonctions
de conversion d’une chaîne en un nombre, ainsi que des fonctions de
manipulation de suites d’octets.
Le chapitre 11 examine les types définis par l’utilisateur que sont les structures,
les unions et les énumérations.
Le chapitre 12 est consacré à l’instruction typedef qui permet de définir des types
synonymes.
Le chapitre 13 fait le point sur le traitement des fichiers : aspects spécifiques au
langage C, traitement des erreurs, distinction entre opérations binaires et
formatées… Puis il passe en revue les différentes fonctions standard
correspondantes.
Le chapitre 14 montre comment mettre en œuvre ce que l’on nomme la gestion
dynamique de la mémoire et en fournit quelques exemples d’application.
Le chapitre 15 étudie les différentes directives du préprocesseur.
La seconde partie de l’ouvrage, plus originale dans sa thématique, est
composée de 11 chapitres qui traitent de sujets transversaux comme la
récursivité ou les déclarations, des modalités d’application de certains éléments
de syntaxe, ou encore de possibilités peu usitées du langage.
Le chapitre 16 propose un récapitulatif sur les déclarations aussi bien sur le plan
de leur syntaxe, que sur la manière de les écrire ou de les interpréter. Sa présence
se justifie surtout par la complexité et l’interdépendance des règles de
déclaration en C : il aurait été impossible de traiter ce thème de manière
exhaustive dans chacun des chapitres concernés.
Le chapitre 17 fait le point sur la manière de pallier les problèmes de manque de
fiabilité que posent les lectures au clavier.
Le chapitre 18 montre comment le langage C distingue différentes catégories de
caractères (caractères de contrôle, graphiques, alphanumériques, de
ponctuation…) et examine les fonctions standard correspondantes.
Le chapitre 19 fournit quelques informations indispensables dans la gestion de
gros programmes nécessitant un découpage en plusieurs fichiers source.
Le chapitre 20 explique comment, à l’image de fonctions standard telles que
printf, écrire des fonctions à arguments variables en nombre et en type.

Le chapitre 21 examine les différentes façons dont un programme peut recevoir


une information de l’environnement ou lui en transmettre, ainsi que des
possibilités dites de traitement de signaux.
Le chapitre 22 montre comment, par le biais de ce que l’on nomme les
caractères étendus, le langage C offre un cadre de gestion d’un jeu de caractères
plus riche que celui offert par le codage sur un octet.
Le chapitre 23 traite du mécanisme général de localisation, qui offre à une
implémentation la possibilité d’adapter le comportement de quelques fonctions
standard à des particularités nationales ou locales.
Le chapitre 24 illustre les possibilités de récursivité du langage.
Le chapitre 25 traite d’un mécanisme dit de branchements non locaux, qui
permet de s’affranchir de l’enchaînement classique : appel de fonction, retour.
Le chapitre 26 recense les incompatibilités qui ont subsisté entre le C ANSI et le
C++, c’est-à-dire tout ce qui fait que le C++ n’est pas tout à fait un surensemble
du C.
Une importante annexe fournit la syntaxe et le rôle de l’ensemble des fonctions
de la bibliothèque standard. La plupart du temps, il s’agit d’un résumé
d’informations figurant déjà dans le reste de l’ouvrage, à l’exception de quelques
fonctions qui, compte tenu de leur usage extrêmement restreint, se trouvent
présentées là pour la première fois.
Enfin, cette nouvelle édition tient compte des deux extensions de la norme
publiées en 1999 et 2011 et connues sous les acronymes C99 et C11 :
• une importante annexe en présente la plupart des fonctionnalités ; sa situation
tardive dans l’ouvrage se justifie par le fait que ces « nouveautés » ne sont pas
intégralement appliquées par tous les compilateurs ;
• certains ajouts sont mentionnés au fil du texte, lorsque cela nous a paru utile.
À propos des normes ANSI/ISO
On parle souvent, par habitude, du C ANSI (American National Standard
Institute) alors que la première norme américaine, publiée en 1989, a été rendue
internationale en 1990 par l’ISO (International Standardization Organisation),
avant d’être reprise par les différents comités de normalisation continentaux ou
nationaux sous la référence ISO/IEC 9899:1990. En fait, l’abus de langage se
justifie par l’identité des deux documents, même si, en toute rigueur, le texte ISO
est structuré différemment du texte ANSI d’origine.
Cette norme a continué d’évoluer. Certains « additifis » publiés séparemment ont
été intégrés dans une nouvelle norme ISO/IEC 9899:1999 (nommée brièvement
C99). Une nouvelle définition est apparue avec ISO/IEC 9899:2011 (C11). La
première norme reste désignée par C ANSI ou par C90.
À propos de la fonction main
En théorie, selon la norme (C90, C99 ou C11), la fonction main qui, contrairement
aux autres fonctions, ne dispose pas de prototype, devrait disposer de l’un des
deux en-têtes suivants :
int main (void)
int main (int arg, char *argv)

En fait, tant que l’on ne cherche pas à utiliser les « arguments de la ligne de
commande », les deux formes :
int main ()
main()

sont acceptées par toutes les implémentations, la seconde s’accompagnant


toutefois fréquemment d’un message d’avertissement.
La première est la plus répandue et c’est celle qu’imposera la norme de C++.
Nous l’utiliserons généralement.
Remerciements
Je tiens à remercier tout particulièrement Jean-Yves Brochot pour sa relecture
extrêmement minutieuse de l’ouvrage, ainsi que pour les discussions nombreuses
et enrichissantes qu’il a suscitées.
1
Généralités

A priori, cet ouvrage s’adresse à des personnes ayant déjà une expérience de la
programmation, éventuellement dans un langage autre que le C. Bien qu’il
puisse être étudié de manière séquentielle, il a été conçu pour permettre l’accès
direct à n’importe quelle partie, à condition de disposer d’un certain nombre de
notions générales, plutôt spécifiques au C, et qui sont examinées ici.
Nous commencerons par un bref historique du langage, qui permettra souvent
d’éclairer certains points particuliers ou redondants. Puis nous verrons comment
se présente la traduction d’un programme, à la fois par les possibilités de
compilation séparée et par l’existence, originale, d’un préprocesseur. Nous
expliquerons ensuite en quoi la classique notion de variable est insuffisante en C
et pourquoi il est nécessaire de la compléter par celle, plus générale, d’objet.
Enfin, nous verrons qu’il existe plusieurs façons de gérer l’emplacement
mémoire alloué à une variable, ce qui se traduira par la notion de classe
d’allocation.
1. Historique du langage C
Le langage C a été créé en 1972 par Denis Ritchie avec un objectif relativement
limité : écrire un système d’exploitation (Unix). Mais ses qualités
opérationnelles ont fait qu’il a très vite été adopté par une large communauté de
programmeurs.
Une première définition rigoureuse du langage a été réalisée en 1978 par
Kernighan et Ritchie avec la publication de l’ouvrage The C Programming
Language. De nombreux compilateurs ont alors vu le jour en se fondant sur cette
définition, quitte à l’assortir parfois de quelques extensions. Ce succès
international du langage a conduit à sa normalisation, d’abord par l’ANSI
(American National Standard Institute), puis par l’ISO (International
Standardization Organisation), en 1993 par le CEN (Comité européen de
normalisation) et enfin, en 1994, par l’AFNOR. En fait, et fort heureusement,
toutes ces normes sont identiques, et l’usage veut qu’on parle de C90 (autrefois
de « C ANSI » ou de « C norme ANSI »).
La norme ANSI élargit, sans la contredire, la première définition de Kernighan
et Ritchie. Pour la comprendre et pour l’accepter, il faut savoir qu’elle a cherché
à concilier deux intérêts divergents :
• d’une part, améliorer et sécuriser le langage ;
• d’autre part, préserver l’existant, c’est-à-dire faire en sorte que les programmes
créés avant la norme soient acceptés par la norme.
Dans ces conditions, certaines formes désuètes ou redondantes ont dû être
conservées. L’exemple le plus typique réside dans les déclarations de fonctions :
la première définition prévoyait de déclarer une fonction en ne fournissant que le
type de son résultat ; la norme ANSI a prévu d’y ajouter le type des arguments,
mais sans interdire l’usage de l’ancienne forme. Il en résulte que la maîtrise des
différentes situations possibles nécessite des connaissances qui seraient devenues
inutiles si la norme avait osé interdire l’ancienne possibilité. On notera, à ce
propos, que la norme du C++, langage basé très fortement sur le C, supprime
bon nombre de redondances pour lesquelles la norme du C n’a pas osé trancher ;
c’est notamment le cas des déclarations de fonctions qui doivent obligatoirement
utiliser la seconde forme.
Après cette première normalisation, des extensions ont été apportées, tout
d’abord sous forme de simples additifs en 1994 (ISO/IEC 9899/COR1:1994) et
en 1995 (ISO/IEC 9899/COR2 :1995), lesquels se sont trouvés intégrés dans la
nouvelle norme ISO/IEC 9899:1999, désignée sous l’acronyme C99. Enfin, une
dernière norme ISO/IEC 9899:2011 est apparue, plus connue sous l’acronyme
C11.
Compte tenu du fait que tous les compilateurs ne respectent pas intégralement
les dernières normes C99 et C11 (cette dernière comportant d’ailleurs des
fonctionnalités « facultatives »), notre discours se fonde plutôt sur la norme C90.
Mais, dans la suite de l’ouvrage, il nous arrivera souvent de préciser :
• ce qui constitue un apport de la norme C90 par rapport à la première définition
du C ;
• ce qui dans la première définition est devenu désuet ou déconseillé, bien
qu’accepté par la norme ; en particulier, nous ferons souvent référence à ce qui
disparaîtra ou qui changera en C++ ;
• les points concernés par les apports des normes C99 et C11.
L’annexe en fin d’ouvrage récapitule les principaux apports de C99 et C11.
2. Programme source, module objet et programme
exécutable
Tout programme écrit en langage évolué forme un texte qu’on nomme un
« programme source ». En langage C, ce programme source peut être découpé en
un ou plusieurs fichiers source. Notez qu’on parle de fichier même si,
exceptionnellement, le texte correspondant, saisi en mémoire, n’a pas été
véritablement recopié dans un fichier permanent.
Chaque fichier source est traduit en langage machine, indépendamment des
autres, par une opération dite de « compilation », réalisée par un logiciel ou une
partie de logiciel nommée « compilateur ». Le résultat de cette opération porte le
nom de « module objet ». Bien que formé d’instructions machine, un tel module
objet n’est pas exécutable tel quel car :
• il peut lui manquer d’autres modules objet ;
• il lui manque, de toute façon, les instructions exécutables des fonctions
standards appelées dans le fichier source (par exemple printf, scanf, strcat…).
Le rôle de l’éditeur de liens est précisément de réunir les différents modules
objet et les fonctions de la bibliothèque standard afin de constituer un
programme exécutable. Ce n’est d’ailleurs que lors de cette édition de liens
qu’on pourra s’apercevoir de l’absence d’une fonction utilisée par le programme.
3. Compilation en C : existence d’un préprocesseur
En C, la traduction d’un fichier source se déroule en deux étapes totalement
indépendantes :
• un prétraitement ;
• une compilation proprement dite.
La plupart du temps, ces deux étapes sont enchaînées automatiquement, de sorte
qu’on a l’impression d’avoir affaire à un seul traitement. Généralement, on parle
du préprocesseur pour désigner le programme réalisant le prétraitement. En
revanche, les termes de « compilateur » ou de « compilation » restent ambigus
puisqu’ils désignent tantôt l’ensemble des deux étapes, tantôt la seconde.
L’étape de prétraitement correspond à une modification du texte d’un fichier
source, basée essentiellement sur l’interprétation d’instructions très particulières
dites « directives à destination du préprocesseur » ; ces dernières sont
reconnaissables par le fait qu’elles commencent par le signe #.
Les deux directives les plus importantes sont :
• la directive d’inclusion d’autres fichiers source : #include ;
• la directive de définition de macros ou de symboles : #define.
La première est surtout utilisée pour incorporer le contenu de fichiers prédéfinis,
dits « fichiers en-tête », indispensables à la bonne utilisation des fonctions de la
bibliothèque standard, la plus connue étant :
#include <stdio.h>

La seconde est très utilisée dans les fichiers en-tête prédéfinis. Elle est également
souvent exploitée par le programmeur dans des définitions de symboles telles
que :
#define NB_COUPS_MAX 100
#define TAILLE 25
4. Variable et objet

4.1 Définition d’une variable et d’un objet


Dans beaucoup de langages, les informations sont manipulées par le biais de
variables, c’est-à-dire d’emplacements mémoire portant un nom et dont le
contenu est susceptible d’évoluer. En C, il existe bien entendu des variables
répondant à une telle définition mais on peut également manipuler des
informations qui ne sont plus vraiment contenues dans des variables ; le cas le
plus typique est celui d’une information manipulée par l’intermédiaire d’un
pointeur :
int *adi ; /* adi est une variable destinée à contenir une adresse d'entier */

*adi = 5 ; /* place la valeur 5 dans l'entier pointé par adi */

L’entier pointé par adi ne porte pas vraiment de nom ; d’ailleurs, au fil de
l’exécution, adi peut pointer sur des entiers différents.
Pour tenir compte de cette particularité, il est donc nécessaire de définir un
nouveau mot. On utilise généralement celui d’objet1. On dira donc qu’un objet
est un emplacement mémoire parfaitement défini qu’on utilise pour représenter
une information à laquelle on peut accéder à volonté (autant de fois qu’on le
souhaite) au sein du programme.
Bien entendu, une variable constitue un cas particulier d’objet. Mais, dans notre
précédent exemple, l’emplacement pointé à un instant donné par adi est lui-
même un objet. En revanche, une expression telle n+5 n’est pas un objet dans la
mesure où l’emplacement mémoire correspondant n’est pas parfaitement défini
et où, de plus, il a un caractère relativement fugitif ; on y accédera véritablement
qu’une seule fois : au moment de l’utilisation de l’expression en question.
La question de savoir si des constantes telles que 34, ‘d' ou "bonjour" sont ou non
des objets est relativement ambiguë : une constante utilise un emplacement
mémoire mais peut-on dire qu’on y accède à volonté ? En effet, on n’est pas sûr
qu’une même constante se réfère toujours au même emplacement, ce point
pouvant dépendre de l’implémentation. La norme n’est d’ailleurs pas totalement
explicite sur ce sujet qui n’a guère d’importance en pratique. Dans la suite, nous
conviendrons qu’une constante n’est pas un objet.

4.2 Utilisation d’un objet


4.2.1 Accès par une expression désignant l’objet
Lorsqu’un objet est une variable, son utilisation ne pose guère de problème, qu’il
s’agisse d’en utiliser ou d’en modifier la valeur, même si, au bout du compte, le
nom même de la variable possède une signification dépendant du contexte dans
lequel il est employé. Par exemple, si p est une variable, dans l’expression :
p + 5

p désigne tout simplement la valeur de la variable p, tandis que dans :


p = 5 ;

p désigne la variable elle-même.


Dans le cas des objets pointés, en revanche, on ne pourra pas recourir à un
simple identificateur ; il faudra faire appel à des expressions plus complexes
telles que *adi ou *(adi+3). Ici encore, cette expression pourra intervenir soit pour
utiliser la valeur de l’objet, soit pour en modifier la valeur. Par exemple, dans
l’expression :
*adi + 5

*adi désigne la valeur de l’objet pointé par adi, tandis que dans :
*adi = 12 ;

*adi désigne l’objet lui-même.

4.2.2 Type d’un objet


Comme dans la plupart des langages, le type d’un objet n’est pas défini de façon
intrinsèque : en examinant une suite d’octets de la mémoire, on est incapable de
savoir comment l’information qui s’y trouve a été codée, et donc de donner une
valeur à l’objet correspondant. En fait, le type d’un objet n’est défini que par la
nature de l’expression qu’on utilise, à un instant donné, pour y accéder ou pour
le modifier. Certes, dans un langage où tout objet est contenu dans une variable,
cette distinction est peu importante puisque le type est alors défini par le nom de
la variable, lequel constitue le seul et unique moyen d’accéder à l’objet2. Dans
un langage comme le C, en revanche, on peut accéder à un objet par le biais d’un
pointeur. Son type se déduira, là encore, de la nature du pointeur mais avec cette
différence fondamentale par rapport à la variable qu’il est alors possible,
volontairement ou par erreur, d’accéder à un même objet avec des pointeurs de
types différents.
Certes, lorsque l’on est amené à utiliser plusieurs expressions pour accéder à un
même objet, elles sont généralement de même type, de sorte qu’on a tendance à
considérer que le type de l’objet fait partie de l’objet lui-même. Par souci de
simplicité, d’ailleurs, il nous arrivera souvent de parler du « type de l’objet ». Il
ne s’agira cependant que d’un abus de langage qui se réfère au type ayant servi à
créer l’objet, en faisant l’hypothèse que c’est celui qu’on utilisera toujours pour
accéder à l’objet.

Remarque
Parmi les différentes expressions permettant d’accéder à un objet, certaines ne permettent pas sa
modification. C’est par exemple le cas d’un nom d’une variable ayant reçu l’attribut const ou d’un
nom de tableau. Comme on le verra à la section 7.2 du chapitre 4, on parle généralement de lvalue
pour désigner les expressions utilisables pour modifier un objet.
5. Lien entre objet, octets et caractères
En langage C, l’octet correspond à la plus petite partie adressable de la mémoire,
mais il n’est pas nécessaire, comme pourrait le faire croire la traduction française
du terme anglais byte, qu’il soit effectivement constitué de 8 bits, même si cela
est très souvent le cas.
Tout objet est formé d’un nombre entier d’octets et par conséquent, il possède
une adresse, celle de son premier octet. La réciproque n’est théoriquement pas
vraie : toute adresse (d’octet) n’est pas nécessairement l’adresse d’un objet ;
voici deux contre-exemples :
• l’octet en question n’est pas le premier d’un objet ;
• l’octet en question est le premier octet d’un objet mais l’expression utilisée
pour y accéder correspond à un type occupant un nombre d’octets différent.
Malgré tout, il sera souvent possible de considérer une adresse quelconque
comme celle d’un objet de type quelconque. Bien entendu, cela ne préjugera
nullement des conséquences plus ou moins catastrophiques qui pourront en
découler.
Par ailleurs, la notion de caractère en C coïncide totalement avec celle d’octet.
Dans ces conditions, on pourra toujours considérer n’importe quelle adresse
comme celle d’un objet de type caractère. C’est d’ailleurs ainsi que l’on
procédera lorsqu’on voudra traiter individuellement chacun des octets
constituant un objet.
6. Classe d’allocation des variables
Comme on le verra, notamment au chapitre 8, l’emplacement mémoire attribué à
une variable peut être géré de deux façons différentes, suivant la manière dont
elle a été déclarée. On parle de « classe d’allocation statique » ou de « classe
d’allocation automatique ».
Les variables de classe statique voient leur emplacement alloué une fois pour
toutes avant le début de l’exécution du programme ; il existe jusqu’à la fin du
programme. Une variable statique est rémanente, ce qui signifie qu’elle conserve
sa dernière valeur jusqu’à une nouvelle éventuelle modification.
Les variables de classe automatique voient leur emplacement alloué au moment
de l’entrée dans un bloc ou dans une fonction ; il est supprimé lors de la sortie de
ce bloc ou de cette fonction. Une variable automatique n’est donc pas rémanente
puisqu’on n’est pas sûr d’y trouver, lors d’une nouvelle entrée dans le bloc, la
valeur qu’elle possédait à la sortie précédente de ce bloc.
On verra qu’une variable déclarée à un niveau global est toujours de classe
d’allocation statique, tandis qu’une variable déclarée à un niveau local (à un bloc
ou à une fonction) est, par défaut, seulement de classe d’allocation automatique.
On pourra agir en partie sur la classe d’allocation d’une variable en utilisant, lors
de sa déclaration, un mot-clé dit « classe de mémorisation ». Par exemple, une
variable locale déclarée avec l’attribut static sera de classe d’allocation statique.
On veillera cependant à distinguer la classe d’allocation de la classe de
mémorisation, malgré le lien étroit qui existe entre les deux ; d’une part la classe
de mémorisation est facultative et d’autre part, quand elle est présente, elle ne
correspond pas toujours à la classe d’allocation.
Par ailleurs, par le biais de pointeurs, le langage C permet d’allouer
dynamiquement des emplacements pour des objets. Il est clair qu’un tel
emplacement est géré d’une manière différente de celles évoquées
précédemment. On parle souvent de gestion dynamique (ou programmée). La
responsabilité de l’allocation et de la libération incombant, cette fois, au
programmeur. Comme on peut le constater, les notions de classe statique ou
automatique ne concernent donc que les objets contenus dans des variables.

Remarque
En toute rigueur, il existe une troisième classe d’allocation, à savoir la classe registre. Il ne s’agit
cependant que d’un cas particulier de la classe d’allocation automatique.
1. Attention, ce terme n’a ici aucun lien avec la programmation orientée objet.
2. Bien que certains langages autorisent, exceptionnellement, d’accéder à un même emplacement par le
biais de deux variables différentes, de types éventuellement différents.
2
Les éléments constitutifs
d’un programme source

Un programme peut être considéré, à son niveau le plus élémentaire, comme une
suite de caractères appartenant à un ensemble qu’on nomme le « jeu de
caractères source ». Bien entendu, pour donner une signification au texte du
programme, il est nécessaire d’effectuer des regroupements de ces caractères en
vue de constituer ce que l’on pourrait nommer les « éléments constitutifs » du
programme ; une telle opération est similaire à celle qui permet de déchiffrer un
texte usuel en le découpant en mots et en signes de ponctuation.
Après avoir décrit le jeu de caractères source, en montrant en quoi il se distingue
du jeu de caractères d’exécution, nous examinerons certains des éléments
constitutifs d’un programme, à savoir : les identificateurs, les mots-clés, les
séparateurs et les commentaires ; puis nous parlerons du format libre dans lequel
peut être rédigé le texte d’un programme.
Nous terminerons en introduisant la notion de « token ». Peu importante en
pratique, elle ne fait que formaliser la façon dont, le préprocesseur d’abord, le
compilateur ensuite, effectuent le découpage du programme en ses différents
éléments constitutifs.
Notez que certains des éléments constitutifs d’un programme source seront
simplement cités ici, dans la mesure où ils se trouvent tout naturellement étudiés
en détail dans d’autres chapitres ; cette remarque concerne les constantes
numériques, les constantes chaîne ou les opérateurs.
1. Jeu de caractères source et jeu de caractères
d’exécution

1.1 Généralités
On appelle « jeu de caractères source » l’ensemble des caractères utilisables pour
écrire un programme source. Il est imposé par la norme ANSI ; mais cette
dernière permet à des environnements possédant un jeu de caractères plus
restreint de représenter certains caractères par le biais de « séquences
trigraphes ».
Ce jeu doit être distingué du « jeu de caractères d’exécution » qui correspond
aux caractères susceptibles d’être traités par le programme lors de son exécution,
que ce soit par le biais d’informations de type char ou par le biais de chaînes de
caractères. Bien entendu, compte tenu de l’équivalence existant en C entre octet
et caractère, on voit qu’on disposera toujours de 256 caractères différents (du
moins lorsque l’octet occupe 8 bits, comme c’est généralement le cas). Mais ces
caractères, ainsi que leurs codes, pourront varier d’une implémentation à une
autre. Cependant, la norme prévoit un jeu minimal qui doit exister dans toute
implémentation. Là encore, elle ne précise que les caractères concernés, et en
aucun cas leur code.
On pourrait penser que, par sa nature même, le jeu de caractères d’exécution n’a
aucune incidence sur l’écriture des instructions d’un programme. En fait, il
intervient indirectement lorsqu’on introduit des constantes (caractère ou chaîne)
ou des commentaires à l’intérieur d’un programme. Par exemple, dans certaines
implémentations françaises, vous pourrez écrire :
char * ch = "résultats" ;
char c_cedille = ‘ç' ;

et ceci, bien que les caractères é et ç n’appartiennent pas au jeu de caractères


source.
Le tableau 2.1 fournit la liste complète du jeu de caractères source et du jeu
minimal des caractères d’exécution, tels qu’ils sont imposés par la norme. On
trouvera quelques commentaires dans les deux paragraphes suivants.

Tableau 2.1 : les jeux de caractères du langage C


Types de Jeu de caractères Jeu minimal de
caractères source caractères d’exécution
Lettres majuscules A B C D E F G H I J K A B C D E F G H I J K
L M N O P Q R S T U L M N O P Q R S T U
V W X Y Z V W X Y Z
Lettres minuscules a b c d e f g h i j k l m n a b c d e f g h i j k l m n
o p q r s t u v w x y z o p q r s t u v w x y z
Chiffres décimaux 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
Espace espace espace
Autres caractères ! « % & ‘ ( ) * + , - . / : ; ! « % & ‘ ( ) * + , - . / : ;
disposant d’un < = > ? _ < = > ? _
graphisme # [ \ ] ^ { | } ~ # [ \ ] ^ { | } ~
Caractères dits de tabulation horizontale tabulation horizontale1
« contrôle » car non tabulation verticale (\t)
imprimables saut de page tabulation verticale (\v)
« indication » de fin de saut de page (\f)
ligne alerte (\a)
retour arrière (\b)
retour chariot (\r)
nouvelle ligne (\n)
1. Entre parenthèses, on trouvera la séquence d’échappement correspondante, telle qu’elle sera présentée à
la section 2.3.2 du chapitre 3.

1.2 Commentaires à propos du jeu de caractères


source
On note la présence de 29 caractères disposant d’un graphisme imprimable. Pour
les environnements disposant d’un jeu de caractères restreint (tel que celui
correspondant à la norme ISO 646-10831), la norme ANSI permet que certains
des 9 derniers caractères (# [ \ ] ^ { | } ~) soient remplacés par ce que l’on
nomme des séquences trigraphes (suites de trois caractères commençant par ??).

Tableau 2.2 : les séquences trigraphes

Séquence trigraphe Caractère équivalent


??= #
??( [
??/ \
??) ]
??’ ^
??< {
??! |
??> }
??- ~

Assez curieusement, la norme semble rendre obligatoire la présence des trois


caractères de contrôle que sont les tabulations et le saut de page alors que, de
toute façon, ils joueront pour le compilateur le même rôle que des espaces. Le
premier présente l’intérêt de faciliter les « indentations » d’instructions. On ne
perdra cependant pas de vue qu’il peut poser des problèmes dès lors qu’on
cherche à manipuler un même texte source à l’aide d’éditeurs différents ou à
l’aide d’un logiciel de traitement de texte : remplacement de tabulations par un
certain nombre (variable !) d’espaces, pose de taquets de tabulation à des
endroits différents2, etc.
Par ailleurs, on notera que la norme prévoit qu’il doit exister une « manière »
d’indiquer les fins de ligne, sans imposer un caractère précis. Cette tolérance
vise simplement à reconnaître l’existence de certains environnements (cas des
PC notamment) qui utilisent une succession de deux caractères pour représenter
la fin de ligne. Mais, en théorie, la norme est très large puisqu’elle autorise
n’importe quelle « technique » permettant de reconnaître convenablement les
changements de ligne dans un programme source.

Remarque
Les éventuelles substitutions de séquences trigraphes ont lieu (une seule fois) avant passage au
préprocesseur, quel que soit l’environnement considéré. Par exemple, l’instruction :
printf ("bonjour??/n") ;
sera toujours équivalente à :
printf ("bonjour\n") ;
En revanche, toute suite de caractères de la forme ??x, où x n’est pas l’un des 9 caractères prévus
précédemment, ne sera pas modifiée. Par exemple :
printf ("bonjour??+") ;
affichera bien : bonjour??+
1.3 Commentaires à propos du jeu minimal de
caractères d’exécution
Le jeu de caractères d’exécution dépend de l’implémentation. Il s’agit donc ici
des caractères qui pourront être manipulés par le programme, c’est-à-dire ceux
qui pourront :
• être placés dans une variable de type caractère, par affectation ou par lecture ;
• apparaître dans une constante de type caractère (‘a', ‘+', etc.) ;
• apparaître dans une chaîne constante ("bonjour", "salut\n", etc.).
La norme impose un jeu minimal assez réduit puisqu’il comporte, outre tous les
caractères du jeu de caractères source, quelques caractères de contrôle
supplémentaires :
• alerte ;
• retour arrière ;
• retour chariot ;
• nouvelle ligne.
Cette fois, le caractère de fin de ligne est bien présent en tant que tel. On notera
cependant qu’il s’agit simplement de sa représentation en mémoire. Cela ne
préjuge nullement de la manière dont il sera représenté dans un fichier de type
texte (par un ou plusieurs caractères, voire suivant une autre convention).
Comme nous le verrons au chapitre 13, il existe effectivement un mécanisme
permettant à un programme de « voir » les fins de ligne d’un fichier texte
comme s’il s’agissait d’un caractère de fin de ligne.
Pour pouvoir faire apparaître dans une constante caractère ou une constante
chaîne l’un des caractères de contrôle imposés par la norme, une notation
particulière est prévue sous la forme de ce qu’on nomme souvent une « séquence
d’échappement », qui utilise le caractère \. La plus connue est \n pour la fin de
ligne (ou encore ??/n si l’on utilise les caractères trigraphes) ; les autres seront
présentées à la section 2.3.2 du chapitre 3, mais on notera dès maintenant que la
norme offre l’avantage de fournir des notations portables pour ces caractères.

Remarque
Si la norme impose le jeu de caractères minimal, elle n’impose aucunement le code utilisé pour les
représenter, lequel va dépendre de l’implémentation. En pratique, on ne rencontre qu’un nombre limité
de codes, parmi lesquels on peut citer l’EBCDIC et l’ASCII. On notera bien que, en toute rigueur,
l’ASCII est un code qui n’exploite que 7 des 8 bits d’un octet et dans lequel les 32 premiers codes (0 à
31) sont réservés à des caractères de contrôle. Beaucoup d’autres codes (comme Latin-1) sont des
surensembles de l’ASCII, les 128 premières valeurs ayant la même signification dans toutes les
variantes. À noter qu’il existe un abus de langage qui consiste à nommer ASCII ces variantes à 8 bits.
Parfois, on parle alors d’US-ASCII pour désigner le code initial à 7 bits.
Par ailleurs, comme nous le verrons au chapitre 22, le langage C permet de gérer des jeux de
caractères beaucoup plus riches que ceux offerts par un simple octet, par le biais des « caractères
étendus » et des caractères multi-octets.
2. Les identificateurs
Dans un programme, beaucoup d’éléments (variables, fonctions, types…) sont
désignés par un nom qu’on appelle « identificateur ». Comme dans la plupart des
langages, un tel identificateur est formé d’une suite de caractères choisis parmi
les lettres, les chiffres ou le caractère souligné (_), le premier d’entre eux étant
nécessairement différent d’un chiffre. Voici quelques identificateurs corrects :
lg_lig valeur_5 _total _89

Par ailleurs, les majuscules et les minuscules sont autorisées mais ne sont pas
équivalentes (contrairement, par exemple, à ce qui se produit en Turbo Pascal).
Ainsi, en C, les identificateurs « ligne » et « Ligne » désignent deux objets
différents.
En ce qui concerne la longueur des identificateurs, la norme ANSI prévoit (sauf
exception, mentionnée dans la remarque ci-après) que, au moins les 31 premiers
caractères (63 en C11), soient « significatifs » ; autrement dit, deux
identificateurs qui diffèrent par leurs 31 (63) premières lettres désigneront deux
choses différentes.
En langage C, il est donc assez facile de choisir des noms de variables
suffisamment évocateurs de leur rôle, voire de leur type ou de leur classe de
mémorisation. En voici quelques exemples :
Prix_TTC Taxe_a_la_valeur_ajoutee TaxeValeurAjoutee
PtrInt_Adresse

Remarque
En théorie, la norme (C90, aussi bien que C99 et même C11) autorise une implémentation à limiter à 6
la longueur des identificateurs dits « externes », c’est-à-dire qui subsistent après compilation : noms de
fonctions, variables globales. En pratique, ces restrictions tendent à disparaître.
3. Les mots-clés
Certains mots sont réservés par le langage à un usage bien défini. On les nomme
des « mots-clés ». Un mot-clé ne peut pas être employé comme identificateur.
Le tableau 2.3 liste les mots-clés par ordre alphabétique.

Tableau 2.3 : les mots-clés du langage C

Remarque
Contrairement à ce qui se passe dans un langage comme le Pascal, en C il n’y a théoriquement pas de
notion d’identificateur prédéfini dont la signification pourrait, le cas échéant, être modifiée par
l’utilisateur. Cependant, un tel résultat peut quand même être obtenu en faisant appel à la directive
#define du préprocesseur, comme on le verra au chapitre 15.
4. Les séparateurs et les espaces blancs
Dans notre langue écrite, les mots sont séparés par un espace, un signe de
ponctuation ou une fin de ligne. Il en va presque de même pour le langage C
dont les règles vont donc paraître naturelles. Ainsi, dans un programme, deux
identificateurs successifs entre lesquels la syntaxe n’impose aucun caractère ou
groupe de caractères particulier dit « séparateur », doivent impérativement être
séparés par ce que l’on nomme un « espace blanc » (le plus usité étant l’espace) :

On nomme espace blanc une suite de un ou plusieurs caractères choisis parmi : espace, tabulation
horizontale, tabulation verticale, changement de ligne ou changement de page.

En revanche, dès que la syntaxe impose un séparateur quelconque, il n’est pas


nécessaire de prévoir d’espaces blancs supplémentaires. Cependant, en pratique,
des espaces amélioreront généralement la lisibilité du programme.
Les caractères séparateurs comprennent tous les opérateurs (+, –, *, =, +=, ==…)
ainsi que les caractères dits de « ponctuation »3 :
( ) [ ] { } , ; : …

Par exemple, vous devrez impérativement écrire (avec au moins un espace blanc
entre int et x) :
int x,y ;

et non :
intx,y ;

En revanche, vous pourrez écrire indifféremment (le caractère virgule étant un


séparateur) :
int n,compte,total,p ;

ou plus lisiblement :
int n, compte, total, p ;
5. Le format libre
Comme le langage Pascal, le langage C autorise une « mise en page »
parfaitement libre. En particulier, une instruction peut s’étendre sur un nombre
quelconque de lignes et une même ligne peut comporter autant d’instructions
que voulu.
Les fins de ligne ne jouent pas de rôle particulier, si ce n’est celui de séparateur,
au même titre qu’un simple espace, ce qui signifie donc qu’un identificateur ne
peut être « coupé en deux par une fin de ligne. Par exemple :
int quant
ite ; /* incorrect */

est incorrect car équivalent à :


int quant ite ;

et non à :
int quantite ;

ce qui correspond à l’intuition.


En revanche, les fins de ligne ne sont pas autorisées dans les constantes chaîne.
Par exemple :
const char message = "salut, cher ami,
comment allez-vous?" ;

conduira à une erreur de compilation.


Il ne faut pas en conclure qu’il est nécessaire d’écrire une constante chaîne sur
une seule ligne car on pourra faire appel aux possibilités dites de « concaténation
des chaînes adjacentes », décrites à la section 1.2 du chapitre 10, en écrivant, par
exemple :
const char messsage = "salut, cher ami, "
"comment allez-vous?" ;

Remarques
1. Les directives à destination du préprocesseur (comme #include ou #define) ne bénéficient pas du
format libre dont nous venons de parler. De plus, la fin de ligne y joue un rôle important de
terminateur.
2. Le fait que le langage C autorise un format libre présente des contreparties. Notamment, le risque
existe, si l’on n’y prend pas garde, d’aboutir à des programmes peu lisibles. Voici, à titre
d’illustration, un programme accepté par le compilateur :
Exemple de programme mal présenté
int main() { int i,n;printf("bonjour\n") ; printf(
"je vais vous calculer 3 carrés\n") ; for (i=1;i<=
3; i++){printf("donnez un nombre entier : ") ; scanf("%d",
&n) ; printf ("son carré est: %d\n", n*n) ; } printf (
"au revoir"); }
6. Les commentaires
Comme tout langage évolué, le langage C autorise la présence de commentaires
dans les programmes source. Il s’agit de textes explicatifs destinés aux lecteurs
du programme et qui n’ont aucune incidence sur sa compilation.
Ces commentaires sont formés de caractères quelconques placés entre les
symboles /* et */. Ils peuvent apparaître à tout endroit du programme où un
espace est autorisé, y compris dans les directives à destination du préprocesseur.
En général, cependant, on se limitera à des emplacements propices à une bonne
lisibilité du programme.
Voici quelques exemples de commentaires :
/* programme de calcul de racines carrées */

/* commentaire fantaisiste &ç§{<>} ?%!!!!!! */

/* commentaire s'étendant
sur plusieurs lignes
de programme source */

/* ============================================
* commentaire quelque peu esthétique *
* et mis en boîte, pouvant servir, *
* par exemple, d'en-tête de programme *
============================================ */

Voici d’autres exemples de commentaires qui, situés au sein d’une instruction de


déclaration, permettent de définir le rôle des différentes variables :
int nb_points ; /* nombre de valeurs à calculer */
float debut, /* abscisse de début */
fin, /* abscisse de fin */
pas ; /* pas choisi */

La norme ANSI ne prévoit pas de possibilités dites de « commentaires


imbriqués ». Ainsi, une construction telle que :
/* début commentaire 1 /* commentaire 2 */ fin commentaire 1 */

conduit à considérer la dernière partie (fin commentaire 1) comme des instructions


C, et donc à une erreur de syntaxe4.
Voici, enfin, un exemple de commentaire déraisonnable mais accepté par le
compilateur :
int /*type entier*/ n /*une variable*/,/*---*/p/*une autre variable*/ ;
n /*on affecte a n*/= /*la valeur*/p/*de p*/ + /*augmentée de*/2 /* deux */ ;

Remarque
La norme C99 autorise une seconde forme de commentaire, dit « de fin de ligne » que l’on retrouve
également en C++. Un tel commentaire est introduit par // et tout ce qui suit ces deux caractères
jusqu’à la fin de ligne est considéré comme un commentaire. En voici un exemple :
printf("bonjour\n") ; // formule de politesse
7. Notion de token
Ce chapitre a étudié certains éléments constituant un programme. Les autres, tels
que les constantes, les chaînes constantes, les opérateurs, seront abordés dans
d’autres chapitres. D’une manière générale, on peut dire qu’un programme
source peut se décomposer en une succession d’éléments que l’on nomme
« tokens »5. Cette notion est bien entendu indispensable au compilateur chargé
de cette décomposition. En revanche, elle l’est beaucoup moins pour l’auteur ou
le lecteur d’un programme, excepté dans de très rares cas. Cette section présente
donc surtout un intérêt anecdotique, hormis pour ceux qui souhaiteraient réaliser
un compilateur ou, éventuellement, être en mesure d’interpréter des
constructions un peu sophistiquées.

7.1 Les différentes catégories de tokens


Si l’on souhaite être exhaustif, il faut distinguer les tokens reconnus par le
préprocesseur de ceux qui seront reconnus par le compilateur proprement dit,
même si, comme on s’y attend, la plupart d’entre eux sont communs. Le tableau
2.4 liste les différentes catégories de tokens existant dans les deux cas, ainsi que
le chapitre de l’ouvrage dans lequel ils sont décrits (certains l’étant dans le
présent chapitre).

Tableau 2.4 : les différents tokens du langage C


Comme on peut le constater, les différences de tokens entre le préprocesseur et le
compilateur sont minimes et logiques :
• les mots-clés ne sont pas reconnus par le préprocesseur qui n’y voit qu’un
identificateur ordinaire ;
• les symboles [ ] ( ) { } sont encore de simples ponctuations pour le
préprocesseur, tandis qu’ils posséderont une signification particulière pour le
compilateur, ce qui leur impose alors d’être appariés correctement.

Remarques
1. La norme ne considère pas un commentaire comme un token, dans la mesure où il est assimilé aux
caractères dits « espaces blancs », utilisés précisément pour séparer deux tokens. D’ailleurs, ces
commentaires ne sont vus que du préprocesseur et non du compilateur. En pratique, ce point est de
faible importance.
2. Les opérateurs forment une catégorie de tokens, les caractères de ponctuation en forment une autre.
Ce point est de moindre importance. En effet, on pourrait considérer qu’un séparateur n’est plus un
token puisqu’il sert à séparer des tokens. En fait, la norme a dû apporter cette précision, tout
simplement pour que les tokens identifiés par le préprocesseur soient effectivement retransmis au
compilateur et non supprimés comme le sont, par exemple, les espaces blancs ou les commentaires.

7.2 Décomposition en tokens


A priori, la démarche adoptée par le préprocesseur ou le compilateur est assez
intuitive. Cependant, la connaissance de l’algorithme utilisé peut s’avérer
nécessaire pour interpréter correctement certains cas limites. Ce dernier se
résume ainsi :

Le préprocesseur et le compilateur recherchent toujours la plus longue séquence possible de caractères


qui soit un token.

Voici quelques exemples :


x1 + 2 /* conduit aux tokens : x1, + et 2 */
x++2 /* conduit aux tokens : x, ++ et 2, comme si on avait écrit : x++ 2 */
/* et non, par exemple, aux tokens : x, +, + et 2 */
x+++3 /* conduit aux tokens : x, ++, + et 3, comme si on avait écrit : x ++ +3 */
x++++3 /* conduit aux tokens : x, ++, ++ et 3, comme si on avait écrit : */
/* x ++ ++ 3, ou encore x++ ++3, ce qui sera rejeté par le compilateur */

D’une manière générale, il est possible d’éviter au lecteur d’avoir à s’interroger


sur l’algorithme utilisé en adoptant un style de programmation évitant toute
ambiguïté. Le troisième exemple pourrait notamment être écrit ainsi (le
deuxième et le quatrième exemple étant, de toute façon, incorrects) :
x++ + 3 /* plus lisible que : x+++3 */

1. Il s’agit d’un sous-ensemble du code ASCII restreint dont nous parlerons un peu plus loin.
2. Certains environnements peuvent également poser des problèmes opposés en remplaçant
automatiquement des suites d’espaces par des tabulations…
3. Certains de ces caractères comme [ et ] apparaissent dans un opérateur ([]), lequel est lui-même un
séparateur. Néanmoins, chacun d’entre eux forme également un séparateur : on peut trouver « quelque
chose » entre [ et ].
4. Certains compilateurs acceptent les commentaires imbriqués.
5. Nous n’avons pas cherché à traduire le mot anglais.
3
Les types de base

La manipulation d’une information fait généralement intervenir la notion de


type, c’est-à-dire la manière dont elle a été codée en mémoire. La connaissance
de ce type est nécessaire pour la plupart des opérations qu’on souhaite lui faire
subir. Traditionnellement, on distingue les types simples dans lesquels une
information est, à un instant donné, caractérisée par une seule valeur, et les types
agrégés dans lesquels une information est caractérisée par un ensemble de
valeurs.
Ce chapitre étudie tout d’abord les caractéristiques des différents types de base
du langage, c’est-à-dire ceux à partir desquels peuvent être construits tous les
autres, qu’il s’agisse de types simples comme les pointeurs ou de types agrégés
comme les tableaux, les structures ou les unions. Nous les avons classés en trois
catégories : entiers, caractères et flottants, même si, comme on le verra, les
caractères apparaissent, dans une certaine mesure, comme des cas particuliers
d’entiers. Nous présenterons ensuite l’importante notion d’expression constante,
c’est-à-dire calculable par le compilateur. Enfin, nous terminerons sur la
déclaration et l’initialisation des variables d’un type de base.
1. Les types entiers
Pour les différents types entiers prévus par la norme, nous étudions ici les
différentes façons de les nommer et la manière dont les informations
correspondantes sont codées en mémoire. Nous fournissons quelques éléments
permettant de choisir le type entier le plus approprié à un objectif donné. Enfin,
nous indiquons les différentes façons d’écrire des constantes entières dans un
programme source.

1.1 Les six types entiers


En théorie, la norme ANSI prévoit qu’il puisse exister six types entiers différents
caractérisés par deux paramètres :
• la taille de l’emplacement mémoire utilisé pour les représenter ;
• un attribut précisant si l’on représente des nombres signés, c’est-à-dire des
entiers relatifs, ou des nombres non signés, c’est-à-dire des entiers naturels.
Le premier paramètre est assez classique et, comme dans la plupart des langages,
il se traduit par l’existence de différents noms de type : à chaque nom correspond
une taille qui pourra cependant dépendre de l’implémentation. Le second
paramètre, quant à lui, est beaucoup moins classique et on verra qu’il est
conseillé de ne recourir aux entiers non signés que dans des circonstances
particulières telles que la manipulation de motifs binaires.
Pour chacun des six types, il existe plusieurs façons de les nommer, compte tenu
de ce que :
• le paramètre de taille s’exprime par un attribut facultatif : short ou long ;
• le paramètre de signe s’exprime, lui aussi, par un attribut facultatif : signed ou
unsigned ;

• le mot-clé int, correspondant à un entier de taille intermédiaire, peut être omis,


dès lors qu’au moins un des qualificatifs précédents est présent.
Le tableau 3.1 récapitule les différentes manières de spécifier chacun des six
types (on parle de « spécificateur de type »), ainsi que la taille minimale et le
domaine minimal que leur impose la norme, quelle que soit l’implémentation
concernée.

Tableau 3.1 : les six types entiers prévus par la norme


Remarques
1. La norme impose qu’un type signé et un type non signé de même nom possèdent la même taille ; ce
sera par exemple le cas pour long int et unsigned long int.
2. La norme impose seulement à une implémentation de disposer de ces six types ; en particulier, rien
n’interdit que deux types différents possèdent la même taille. Fréquemment, d’ailleurs, soit les
entiers courts et les entiers seront de même taille, soit ce seront les entiers et les entiers longs. Les
seules choses dont on soit sûr sont que, dans une implémentation donnée, la taille des entiers courts
est inférieure ou égale à celle des entiers et que celle des entiers est inférieure ou égale à celle des
entiers longs.
3. C99 introduit les types supplémentaires : long long int et unsigned long long int.

1.2 Représentation mémoire des entiers et limitations


Comme le montre le tableau 3.2, la norme prévoit totalement la manière dont
une implémentation doit représenter les entiers non signés ainsi que les valeurs
positives des entiers signés. En revanche, elle laisse une toute petite latitude en
ce qui concerne la représentation des valeurs négatives.

Tableau 3.2 : contraintes imposées à la représentation des entiers

Contrainte imposée
Nature de l’entier Remarque
par la norme
Non signé Codage binaire pur Un entier non signé de
taille donnée est donc
totalement portable.
Signé Un entier positif doit La plupart des
avoir la même implémentations
représentation que utilisent, pour les
l’entier non signé de nombres négatifs, la
même valeur. représentation dite du
« complément à deux ».

Voyons cela plus en détail en raisonnant, pour fixer les idées, sur des nombres
entiers représentés sur 16 bits. Il va de soi qu’il serait facile de généraliser notre
propos à une taille quelconque.

1.2.1 Représentation des nombres non signés


La norme leur impose une notation binaire pure, de sorte que le codage de ces
nombres est totalement défini : il s’agit simplement de la représentation du
nombre en base 2. Voici des exemples de représentation de quelques nombres sur
16 bits : la dernière colonne reprend, sous forme hexadécimale classique, le
codage binaire exprimé dans la colonne précédente :

Codage, exprimé en
Valeur décimale Codage en binaire
hexadécimal
1 0000000000000001 0001
2 0000000000000010 0002
3 0000000000000011 0003
16 0000000000010000 0010
127 0000000001111111 007F
255 0000000011111111 00FF
1 025 0000010000000001 0401
32 767 0111111111111111 7FFF
32 768 1000000000000000 8000
32 769 1000000000000001 8001
64 512 1111110000000000 FC00
65 534 1111111111111110 FFFE
65 535 1111111111111111 FFFF

1.2.2 Représentation des nombres entiers signés


Lorsqu’il s’agit d’un nombre positif, la norme impose que sa représentation soit
identique à celle du même nombre sous forme non signée, ce qui revient à dire
qu’on y trouve le nombre exprimé en base 2. Dans ces conditions, comme
l’implémentation doit pouvoir distinguer entre nombres positifs et nombres
négatifs, la seule possibilité qui lui reste consiste à se baser sur le premier bit, en
considérant que 0 correspond à un nombre positif, tandis que 1 correspond à un
nombre négatif1.
Une latitude subsiste néanmoins dans la manière de coder la valeur absolue du
nombre. Actuellement, la représentation dite « en complément à deux » tend à
devenir universelle. Elle procède ainsi :
• on exprime la valeur en question en base 2 ;
• tous les bits sont « inversés » : 1 devient 0 et 0 devient 1 ;
• enfin, on ajoute une unité au résultat.
Voici des exemples de représentation de quelques nombres négatifs sur 16 bits,
lorsqu’on utilise cette technique : la dernière colonne reprend, sous forme
hexadécimale classique, le codage binaire exprimé dans la colonne précédente.

Codage, exprimé en
Valeur décimale Codage en binaire
hexadécimal
-1 1111111111111111 FFFF
-2 1111111111111110 FFFE
-3 1111111111111101 FFFD
-4 1111111111111100 FFFC
-16 1111111111110000 FFF0
-256 1111111100000000 FF00
-1 024 1111110000000000 FC00
-32 768 1000000000000000 8000

Remarques
1. Dans la représentation en complément à deux, le nombre 0 est codé d’une seule manière, à savoir
0000000000000000.
2. Si l’on ajoute 1 au plus grand nombre positif (ici 0111111111111111, soit 7FFF en hexadécimal ou
32 767 en décimal) et que l’on ne tient pas compte du dépassement de capacité qui se produit, on
obtient… le plus petit nombre négatif possible (ici 1000000000000000, soit 8 000 en hexadécimal
ou -32 768 en décimal). C’est ce qui explique le phénomène de « modulo » bien connu de
l’arithmétique entière dans le cas (fréquent) où les dépassements de capacité ne sont pas détectés.
3. Par sa nature, la représentation en complément à deux conduit à une différence d’une unité entre la
valeur absolue du plus petit négatif et celle du plus grand positif. Dans les autres représentations
(rares), on a souvent deux façons de coder le nombre 0 : avec bit de signe à 0 ou avec bit de signe à
1. Dans ce cas, on dispose d’autant de combinaisons possibles pour les positifs que pour les
négatifs, la valeur absolue de la plus petite valeur négative étant alors égale à celle de la plus grande
valeur positive qui se trouve être la même que dans la représentation en complément à deux.
Autrement dit, la manière exacte dont on représente les nombres négatifs a une incidence
extrêmement faible sur le domaine couvert par un nombre de bits donnés puisqu’elle ne joue que
sur une unité.

1.2.3 Limitations
Le nombre de bits utilisés pour représenter un entier et le codage employé
dépendent, bien sûr, de l’implémentation considérée. Il en va donc de même des
limitations qui en découlent. Le tableau 3.3 indique ce que sont ces limitations
relatives aux entiers dans le cas où ils sont codés sur 16 ou 32 bits, en utilisant la
représentation en complément à deux.

Tableau 3.3 : limitations relatives aux entiers codés en complément à deux

Dans les implémentations respectant la norme ANSI, sans utiliser la


représentation en complément à deux, la seule différence pouvant apparaître
dans ces limitations concerne uniquement la valeur absolue des valeurs
négatives. Ces dernières, comme expliqué dans la troisième remarque, peuvent
se trouver diminuées de 1 unité. D’ailleurs, la norme tient compte de cette
remarque pour imposer le domaine minimal des différents types entiers, comme
nous l’avons présenté dans le tableau 3.1.
D’une manière générale, les limites spécifiques à une implémentation donnée
peuvent être connues en utilisant le fichier en-tête limits.h qui sera décrit à la
section 3 de ce chapitre. Ce fichier contient également d’autres informations
concernant les caractéristiques des entiers et des caractères.

1.3 Critères de choix d’un type entier


Compte tenu du grand nombre de types entiers différents dont on dispose, voici
quelques indications permettant d’effectuer son choix.

Utiliser les entiers non signés uniquement lorsque cela est indispensable
À taille égale, un type entier non signé permet de représenter des nombres deux
fois plus grands (environ) qu’un type signé. Dans ces conditions, certains
programmeurs sont tentés de recourir aux types non signés pour profiter de ce
gain. En fait, il faut être prudent pour au moins deux raisons :
• dès qu’on est amené à effectuer des calculs, il est généralement difficile
d’affirmer qu’on ne sera pas conduit, à un moment ou à un autre, à un résultat
négatif non représentable dans un type non signé ;
• même s’il est permis, le mélange de types signés et non signés dans une même
expression est fortement déconseillé, compte tenu du peu de signification que
possèdent les conversions qui se trouvent alors mises en place (elles sont
décrites à la section 3 du chapitre 4).
En définitive, les entiers non signés ne devraient pratiquement jamais être
utilisés pour réaliser des calculs. On peut considérer que leur principale vocation
est la manipulation de motifs binaires, indépendamment des valeurs numériques
correspondantes. On les utilisera souvent comme opérandes des opérateurs de
manipulation de bits ou comme membres d’unions ou de champs de bits. Dans
certains cas, ils pourront également être utilisés pour échanger des informations
numériques entre différentes machines, compte tenu du caractère entièrement
portable de leur représentation. Toutefois, il sera alors généralement nécessaire
de transformer artificiellement des valeurs négatives en valeurs positives (par
exemple, en introduisant un décalage donné).

Remarque
Le mélange entre flottants et entiers non signés présente les mêmes risques que le mélange entre
entiers non signés et entiers signés. En revanche, le mélange entre entiers signés et flottants ne pose
pas de problèmes particuliers. Cela montre que l’arithmétique non signée constitue un cas bien à part.

Efficacité
En général, le type int correspond au type standard de la machine, de sorte que
l’on est quasiment assuré que c’est dans ce type que les opérations seront les
plus rapides. On pourra l’utiliser pour réaliser des programmes portables
efficaces, pour peu qu’on accepte les limitations correspondantes.
Signalons que l’on rencontre actuellement des machines à 64 bits, dans
lesquelles la taille du type int reste limitée à 32 bits, probablement par souci de
compatibilité avec des machines antérieures. Le type int reste cependant le plus
efficace car, généralement, des mécanismes spécifiques à la machine évitent
alors la dégradation des performances.

Occupation mémoire
Le type short est naturellement celui qui occupera le moins de place, sauf si l’on
peut se contenter du type char, qui peut jouer le rôle d’un petit entier (voir section
2 de ce chapitre). Toutefois, l’existence de contraintes d’alignement et le fait que
ce type peut être plus petit que la taille des entiers manipulés naturellement par
la machine, peuvent conduire à un résultat opposé à celui escompté. Par
exemple, sur une machine où le type short occupe deux octets et où le type int
occupe 4 octets, on peut très bien aboutir à la situation suivante :
• les informations de 2 octets sont alignées sur des adresses multiples de 4, ce qui
peut annuler le gain de place escompté ;
• l’accès à 2 octets peut impliquer l’accès à 4 octets avec sélection des 2 octets
utiles d’où une perte de temps.
Dans tous les cas, dans l’évaluation d’une expression, toute valeur de type short
est convertie systématiquement en int (voir éventuellement le chapitre 4), ce qui
peut entraîner une perte de temps.
En pratique, le type short pourra être utilisé pour des tableaux car la norme
impose la contiguïté de leurs éléments : on sera donc assuré d’un gain de place
au détriment éventuel d’une perte de temps.

Remarque
Tout ce qui vient d’être dit à propos du type short se transposera au petit type entier qu’est le type
char.

Portabilité des programmes


Ici, le problème est délicat dans la mesure où le terme même de portabilité est
quelque peu ambigu. En effet, s’il s’agit d’écrire un programme qui compile
correctement dans toute implémentation, on peut effectivement utiliser n’importe
quel type. En revanche, s’il s’agit d’écrire un programme qui fonctionne de la
même manière dans toute implémentation, il n’en va plus de même étant donné
qu’un spécificateur de type donné correspond à un domaine différent d’une
implémentation à une autre. Par exemple, int pourra correspondre à 2 octets sur
certaines machines, à 4 octets sur d’autres… Dans certains cas, on souhaitera
disposer d’un type entier ayant une taille bien déterminée ; on pourra y parvenir
en utilisant des possibilités de compilation conditionnelle (voir section 3.4.2 du
chapitre 15).

1.4 Écriture des constantes entières


Lorsque vous devez introduire une constante entière dans un programme, le
langage C vous laisse le choix entre trois formes d’écriture présentées dans le
tableau 3.4 :

Tableau 3.4 : les trois formes d’écriture des constantes entières

1.5 Le type attribué par le compilateur aux constantes


entières
Tant que l’on se limite à l’écriture de constantes sous forme décimale, au sein
d’expressions ne faisant pas intervenir d’entiers non signés, on peut
tranquillement ignorer la nature exacte du type que leur attribue le compilateur.
Mais cette connaissance s’avère indispensable dans les situations suivantes :
• utilisation de constantes écrites en notation décimale dans une expression où
apparaissent des quantités non signées ;
• utilisation de constantes écrites en notation hexadécimale ou octale.
Nous allons ici examiner les règles employées par le compilateur pour définir ce
type.

1.5.1 Cas usuel de la notation décimale

Une constante entière écrite sous forme décimale est du premier des types suivants dont la taille suffit
à la représenter correctement : int, long int, unsigned long int.

Ainsi, dans toute implémentation, la constante 12 sera de type int ; la constante 3


000 000 sera représentée en long si le type int est de capacité insuffisante et dans
le type int dans le cas contraire. Il faut cependant noter que les valeurs positives
non représentables dans le type long seront représentées dans le type unsigned long
si ce dernier a une capacité suffisante. Cela va à l’encontre du conseil prodigué à
la section 1.3 de ce chapitre, puisqu’on risque de se retrouver sans le vouloir en
présence d’une expression mixte. Toutefois, cette anomalie ne se produit qu’avec
des constantes qui, de toute façon, ne sont pas représentables dans
l’implémentation.

1.5.2 Cas de la notation octale ou hexadécimale

Une constante entière écrite sous forme octale ou hexadécimale est du premier des types suivants dont
la taille suffit à la représenter correctement : int, unsigned int, long int, unsigned long int.

Alors qu’une constante décimale n’est jamais non signée (excepté lorsqu’elle
n’est pas représentable en long), un constante octale ou hexadécimale peut l’être,
alors même qu’elle aurait été représentable en long. En outre, elle peut se voir
représenter avec un attribut de signe différent suivant l’implémentation. Par
exemple :
• OxFF sera toujours considérée comme un int (donc signée) car, dans toutes les
implémentations, la capacité du type int est supérieure à 255.
• OxFFFF sera considérée comme un unsigned int dans les implémentations où le
type int utilise 16 bits et comme un int dans celles où le type int utilise une
taille supérieure.
Cette remarque trouvera sa pleine justification avec les règles utilisées dans
l’évaluation d’expressions mixtes (mélangeant des entiers signés et non signés)
puisque, comme l’explique le chapitre 4, ces dernières ont alors tendance à
privilégier la conservation du motif binaire plutôt que la valeur. Quoi qu’il en
soit, on ne perdra pas de vue que l’utilisation de constantes hexadécimales ou
octales nécessite souvent la connaissance de la taille exacte du type concerné
dans l’implémentation employée et qu’elle est donc, par essence, peu portable.

1.6 Exemple d’utilisation déraisonnable de constantes


hexadécimales
La section 6 du chapitre 4, vous présentera des exemples d’utilisation
raisonnable de constantes hexadécimales, notamment pour réaliser des
« masques binaires ». Ici, nous nous contentons d’un programme qui illustre les
risques que présente un usage non justifié de telles constantes. Il a été exécuté
dans une implémentation où le type int est représenté sur 16 bits, les nombres
négatifs utilisant la représentation en complément à deux :
Utilisation déraisonnable de constantes hexadécimales

int main()
{ int n ;
n = 10 + 0xFF ; premiere valeur : 265
printf ("premiere valeur : %d\n", n) ; seconde valeur : 9
n = 10 + 0xFFFF ;
printf ("seconde valeur : %d\n", n) ;
}

À première vue, tout se passe comme si 0xFF était interprétée comme valant 255,
tandis que 0xFFFF serait interprétée comme valant -1, ce qui correspond
effectivement à la représentation sur 16 bits de la constante -1 dans le type int.
Or, comme indiqué précédement à la section 1.5.2, la norme prévoit que 0xFFFF
soit de type unsigned int, ce qui correspond à la valeur 65 565. En fait, comme on
le verra au chapitre suivant, le calcul de l’expression 10 + 0xFFFF se fait en
convertissant 10 en unsigned int. Dans ces conditions, le résultat (65 545) dépasse
la capacité de ce type ; mais la norme prévoit exactement le résultat (par une
formule de modulo), à savoir 65 545 modulo 65 536, c’est-à-dire 9. La
conversion en int qu’entraîne son affectation à n ne pose ensuite aucun
problème.
Ainsi, dans cette implémentation, tout se passe effectivement comme si les
constantes hexadécimales étaient signées : avec un type int représenté sur 16
bits, 0xFF apparaît comme positive, tandis que xFFFF apparaît comme négative. En
revanche, avec un type int représenté sur 32 bits, 0xFFFF apparaîtrait comme
positive, alors que 0xFFFFFFFF apparaîtrait comme négative.

1.7 Pour imposer un type aux constantes entières


Il est toujours possible, quelle que soit l’écriture employée (décimale, octale,
hexadécimale), de forcer le compilateur :
• à utiliser un type long en ajoutant la lettre L (ou l) à la suite de l’écriture de la
constante. Par exemple :
1L 045L 0x7F8L 25l 0xFFl

Bien entendu, la constante correspondante sera de type signed long ou unsigned


long suivant les règles habituelles présentées précédemment ;
• à utiliser un attribut unsigned en ajoutant la lettre U (ou u) à la suite de l’écriture
de la constante. Par exemple :
1U 035U 0xFFFFu

Là encore, la constante correspondante sera de type unsigned int ou unsigned long


suivant les règles présentées précédemment ;
• à combiner les deux possibilités précédentes. Par exemple :
1LU 045LU 0xFFFFlu

Cette fois, la constante correspondante est obligatoirement du type unsigned


long.

Remarque
À simple titre indicatif, sachez que le programme précédent (utilisation de constantes hexadécimales),
exécuté dans la même implémentation, fournit toujours les mêmes résultats, quelle que soit la façon
d’écrire la constante utilisée dans la sixième ligne, à savoir : 0xFFFF, 0xFFFFu, 0xFFFFl ou 0xFFFFlu.

1.8 En cas de dépassement de capacité dans l’écriture


des constantes entières
Comme le compilateur choisit d’office le type approprié, les seuls cas qui posent
vraiment problème sont ceux où vous écrivez une constante positive supérieure à
la capacité du type unsigned long int ou une constante négative inférieure au plus
petit nombre représentable dans le type long int.
On notera que, dans ce cas, la norme du langage C, comme celle des autres
langages, ne prévoit nullement comment doit réagir le compilateur2. Certains
compilateurs fournissent alors un diagnostic de compilation, ce qui est fort
satisfaisant. D’autres se contentent de fabriquer une constante fantaisiste
(généralement par perte des bits les plus significatifs !). Ainsi, sur une machine
où le type longint occupe 32 bits, une constante telle que 4 294 967 297 pourra
être acceptée à la compilation et interprétée comme valant 2 !
Rappelons qu’il est déconseillé d’utiliser des constantes décimales dont la valeur
est supérieure à la capacité du type long, tout en restant inférieure à la capacité du
type unsigned long car on serait alors amené à créer une valeur de type non signé,
sans nécessairement s’en apercevoir ; si cette constante apparaît dans une
expression utilisant des types signés, le résultat peut être différent de celui
escompté.
2. Les types caractère
Le langage C dispose non pas d’un seul, mais de deux types caractère, l’un
signé, l’autre non signé. Cette curiosité est essentiellement liée à la forte
connotation numérique de ces deux types. Ici, nous examinerons les différentes
façons de nommer ces types, leurs caractéristiques et la façon d’écrire des
constantes dans un programme.

2.1 Les deux types caractère


Les types caractère correspondent au mot-clé char. La norme ANSI prévoit en
fait deux types caractère différents obtenus en introduisant dans le spécificateur
de type, de façon facultative, un qualificatif de signe, à savoir signed ou unsigned.
Cet attribut intervient essentiellement lorsqu’on utilise un type caractère pour
représenter de petits entiers. C’est la raison pour laquelle la norme définit,
comme pour les types entiers, le domaine (numérique) minimal des types
caractère.
Contrairement à ce qui se passe pour les types entiers, l’absence de qualificatif
de signe pour les caractères ne correspond pas systématiquement au type signed
char mais plus précisément à l’un des deux types signed char ou unsigned char, ceci
suivant l’implémentation et même, parfois, dans une implémentation donnée,
suivant certaines options de compilation.

Tableau 3.5 : les deux types caractère du langage C

En C++
Alors que C dispose de deux types caractère, C++ en disposera de trois : char (malgré son ambiguïté,
ce sera un type à part entière), unsigned char et signed char.

2.2 Caractéristiques des types caractère


Le tableau 3.6 récapitule les caractéristiques des types caractère. Ces derniers
seront ensuite détaillés dans les sections suivantes de ce chapitre.

Tableau 3.6 : les caractéristiques des types caractère

Code associé – indépendant de l’attribut de signe ; Voir


à un section
caractère – dépend de l’implémentation. 2.2.1 de ce
chapitre
Caractères – au moins le jeu minimal d’exécution ; Voir
existants section
– ne pas oublier que certains caractères ne 2.2.2 de ce
sont pas imprimables. chapitre
Influence de – en pratique, aucune, dans les simples Voir
l’attribut de manipulations de variables (type section
signe conseillé : char ou unsigned char) ; 2.2.3 de ce
chapitre
– importante si l’on utilise ce type pour
représenter de petits entiers (type
conseillé signed char).
Manipulation Possible par le biais de ce type, compte Voir
d’octets tenu de l’équivalence entre octet et section
caractère (type conseillé unsigned char). 2.2.4 de ce
chapitre

2.2.1 Code associé à un caractère


Les valeurs de type caractère sont représentées sur un octet, au sens large de ce
terme, c’est-à-dire correspondant à la plus petite partie adressable de la mémoire.
En pratique, le code associé à un caractère est indépendant de l’attribut de signe.
Par exemple, on obtiendra exactement le même motif binaire dans c1 et c2 avec :
unsigned char c1 ;
signed char c2 ;
c1 = ‘a' ;
c2 = ‘a' ;

Cependant, on verra à la section 2.4, que les constantes caractère sont en fait de
type int. Les instructions précédentes font donc intervenir des conversions
d’entier en caractère dont les règles exactes sont étudiées à la section 9 du
chapitre 4. Leur examen attentif montrera que, en théorie, cette conservation du
motif binaire ne devrait être assurée que dans certains cas : caractères
appartenant au jeu minimal d’exécution, variables caractère non signées (cas de
c1 dans notre exemple) dans les implémentations utilisant la représentation en
complément à deux. En pratique, cette unicité se vérifie dans toutes les
implémentations que nous avons rencontrées et d’ailleurs, beaucoup de
programmes se basent sur elle.
Bien entendu, le code associé à un caractère donné dépend de l’implémentation.
Certes, le code dit ASCII tend à se répandre, mais comme indiqué à la section
1.3 du chapitre 2, seul le code ASCII restreint a un caractère universel ; et nos
caractères nationaux n’y figurent pas !

2.2.2 Caractères existants


La norme précise le jeu minimal de caractères d’exécution dont on doit
disposer ; il est présenté à la section 1.1 du chapitre 2. Mais d’autres caractères
peuvent apparaître dans une implémentation donnée. Ainsi, lorsque l’octet
occupe 8 bits (comme c’est presque toujours le cas), on est sûr de disposer d’un
jeu de 256 caractères parmi lesquels figurent ceux du jeu minimal. Les
caractères supplémentaires peuvent être imprimables ou de contrôle. À ce
propos, signalons qu’il existe des fonctions standards permettant de connaître la
nature (imprimable, alphabétique, numérique, de contrôle…) d’un caractère de
code donné ; elles sont présentées au chapitre 18.

2.2.3 Influence de l’attribut de signe


Dans les manipulations de variables de type caractère
En pratique, tant que l’on se contente de manipuler des caractères en tant que
tels, l’attribut de signe n’a pas d’importance. C’est le cas dans des situations
telles que :
signed char c1 ;
unsigned char c2 ;
…..
c2 = c1 ;
c1 = c2 ;

Le motif binaire est conservé par affectation. Cependant, là encore, si l’on


examine la norme à la lettre, on constate que ces situations font intervenir des
conversions (étudiées à la section 9 du chapitre 4) qui, en théorie, n’assurent
cette conservation que dans certains cas : caractères appartenant au jeu minimal
d’exécution, conversions de signé en non signé dans les implémentations
utilisant la représentation en complément à deux. En pratique, ces conversions
conservent le motif binaire dans toutes les implémentations que nous avons
rencontrées.

Remarque
Si l’on vise une portabilité absolue, on pourra toujours éviter les conversions en évitant les mélanges
d’attribut de signe. Cependant, dans ce cas, il faudra tenir compte du fait qu’une constante caractère
est de type int (voir section suivante « Le type des constantes caractère »), ce qui pourra influer sur
l’initialisation ou sur l’affectation d’une constante à une variable caractère. Si l’on suit la norme à la
lettre, on verra que le seul cas de conservation théorique sera celui où l’on utilise le type char. En
définitive, il faudra choisir entre :
• un type char qui assure la portabilité absolue du motif binaire mais qui présente une ambiguïté de
signe (soit quand on s’intéresse à sa valeur numérique, soit lorsqu’on compare deux caractères, voir
section 3.3.2 du chapitre 4) ;
• le type unsigned char qui, en théorie, n’assure la portabilité que dans les implémentations utilisant
la représentation en complément à deux) mais qui ne présente plus l’ambiguïté précédente.

Dans les expressions entières


Comme on le verra à la section 9 du chapitre 4, il existe une conversion implicite
de char en int qui pourra intervenir dans :
• des expressions dans lesquelles figurent des variables de type caractère ;
• des affectations du type caractère vers un type entier.
Ces conversions permettent aux types caractère d’être utilisés pour représenter
des petits entiers. Dans ce cas, comme on peut s’y attendre, l’attribut de signe
intervient pour définir le résultat de la conversion. On verra par exemple que,
avec :
signed char c1 ;
unsigned char c2 ;

l’expression c2+1 aura toujours une valeur positive, tandis que c1+1 pourra, suivant
la valeur de c1, être négative, positive ou nulle. De même, si n1 et n2 sont de type
int, avec ces affectations :

n1 = c1 ;
n2 = c2 ;

la valeur de n2 sera toujours positive ou nulle, tandis que celle de n1 pourra être
négative, positive ou nulle.
Les conseils fournis à la section 1.3 de ce chapitre, à propos des types entiers
s’appliquent encore ici : si l’objectif est effectivement de réduire la taille de
variables destinées à des calculs numériques classiques, il est conseillé d’utiliser
systématiquement le type signed char.
2.2.4 Manipulations d’octets
Un des atouts du langage C est de permettre des manipulations dites « proches
de la machine ». Parmi celles-ci, on trouve notamment les manipulations du
contenu binaire d’un objet, indépendamment de son type. A priori, tout accès à
un objet requiert un type précis défini par l’expression utilisée, de sorte que les
manipulations évoquées semblent irréalisables. En fait, un objet est toujours
formé d’une succession d’un nombre entier d’octets et un octet peut toujours être
manipulé par le biais du type char.
Dans ces conditions, il est bien possible de manipuler les différents octets d’un
objet quelconque, pour peu qu’on soit en mesure d’assurer la conservation du
motif binaire. Cet aspect a été exposé à la section précédente. On a vu que cette
conservation avait toujours lieu en pratique, même si en théorie, elle n’était
absolue qu’avec le type char. Par ailleurs, les manipulations d’octets sont souvent
associées à des manipulations au niveau du bit (masque, décalages…) pour
lesquelles le type unsigned char sera plus approprié (voir section 6 du chapitre 4).
Le type unsigned char constituera donc généralement le meilleur choix possible.

2.3 Écriture des constantes caractère


Il existe plusieurs façons d’écrire les constantes caractère dans un programme.
Elles ne sont pas totalement équivalentes.

2.3.1 Les caractères « imprimables »


Les constantes caractère correspondant à des caractères imprimables peuvent se
noter de façon classique, en écrivant entre apostrophes (ou quotes) le caractère
voulu, comme dans ces exemples :
‘a' ‘Y' ‘+' ‘$' ‘0' ‘<' /* caractères du jeu minimal d'exécution */
‘é' ‘à' ‘ç' /* n'existent que dans certaines implémentations */

On notera bien que l’utilisation de caractères n’appartenant pas au jeu minimal


conduit à des programmes qu’on pourrait qualifier de « semi-portables ». En
effet, une telle démarche présente les caractéristiques suivantes :
• elle est plus portable que celle qui consisterait à fournir directement le code du
caractère voulu car on dépendrait alors de l’implémentation elle-même ;
• elle n’est cependant portable que sur les implémentations qui possèdent le
graphisme en question (quel que soit son codage).
Par exemple, la notation ‘é' représente bien le caractère é dans toute
implémentation où il existe, quel que soit son codage ; mais cette notation n’est
pas utilisable dans les autres implémentations. À ce propos, il faut bien voir que
cette notation du caractère imprimable n’est visible qu’à celui qui saisit ou qui lit
un programme. Dès qu’on travaille sur des fichiers source, on a affaire à des
suites d’octets représentant chacun un caractère. Par exemple, il n’est pas rare de
saisir un caractère é dans une implémentation et de le voir apparaître
différemment lorsqu’on exploite le même programme source dans une autre
implémentation.

2.3.2 Les caractères disposant d’une « séquence d’échappement »


Certains caractères non imprimables possèdent une représentation
conventionnelle dite « séquence d’échappement », utilisant le caractère \
(antislash)3. Dans cette catégorie, on trouve également quelques caractères qui,
bien que disposant d’un graphisme, jouent un rôle particulier de délimiteurs, ce
qui les empêche d’être notés de manière classique entre deux apostrophes.

Tableau 3.7 : les caractères disposant d’une séquence d’échappement

Si le caractère \ apparaît suivi d’un caractère différent de ceux qui sont


mentionnés ici, le comportement du programme est indéterminé. Par ailleurs,
une implémentation peut introduire d’autres séquences d’échappement ; il lui est
cependant conseillé d’éviter les minuscules qui sont réservées pour une future
extension de la norme. Par essence, l’emploi de cette notation est totalement
portable, quel que soit le code correspondant dans l’implémentation. En
revanche, tout recours à un caractère de contrôle n’appartenant pas à cette liste
nécessite l’introduction directe de son code, ce qui n’assure la portabilité
qu’entre implémentations utilisant le même codage.

Remarque
La notation sous forme d’une séquence d’échappement ne dispense nullement de l’utilisation des
apostrophes dans l’écriture d’une constante caractère. Ainsi, il faudra bien écrire ‘\n' et non
simplement \n. Bien entendu, quand cette même séquence d’échappement apparaîtra dans une chaîne
constante, ces apostrophes n’auront plus aucune raison d’être. Par exemple, on écrira bien :
"bonjour\nmonsieur"

2.3.3 Écriture d’un caractère par son code


Il est possible d’utiliser directement le code du caractère, en l’exprimant,
toujours à la suite du caractère \ :
• soit sous forme octale ;
• soit sous forme hexadécimale précédée de x.
Voici quelques exemples de notations équivalentes (sur une même ligne), dans le
code ASCII restreint :

La notation octale doit comporter de 1 à 3 chiffres ; la notation hexadécimale


n’est pas soumise à des limites. Ainsi, ‘\4321' est incorrect, tandis que ‘x4321' est
correct. Toutefois, dans le dernier cas, le code obtenu en cas de dépassement de
la capacité d’un octet, n’est pas précisé par la norme. Il est donc recommandé de
ne pas utiliser cette tolérance4.
D’une manière générale, ces notations, manifestement non portables, doivent
être réservées à des situations particulières telles que :
• besoin d’un caractère de contrôle non prévu dans le jeu d’exécution, par
exemple ACK, NACK… Dans ce cas, on minimisera le travail de portage d’une
machine à une autre en prenant bien soin de définir une seule fois, au sein d’un
fichier en-tête, chacun de ces caractères par une instruction de la forme :
#define ACK 0x5

• besoin de décrire le motif binaire contenu dans un octet ; c’est notamment le


cas lorsqu’on doit recourir à un masque binaire.

Remarques
1. Le caractère \ suivi d’un caractère autre que ceux du tableau 3.7 ou d’un chiffre de 0 à 7, est
simplement ignoré. Ainsi, dans le cas du code ASCII, \9 correspond au caractère 9 (de code ASCII
57), tandis que \7 correspond au caractère de code ASCII 7, c’est-à-dire la « cloche ».
2. Avec la notation hexadécimale ou octale, comme avec la notation sous forme d’une séquence
d’échappement présentée à la section précédente 2.3.2, il ne faut pas oublier les apostrophes
délimitant une constante caractère. Bien entendu, cette remarque ne s’appliquera plus au cas des
constantes chaînes.

2.4 Le type des constantes caractère


Assez curieusement, la norme prévoit que :

Toute constante caractère est de type int et la valeur correspondante est obtenue comme si l’on
convertissait une valeur de type char (dont l’attribut de signe dépend de l’implémentation) en int.

L’explication réside probablement dans le lien étroit existant en C entre


caractères et entiers. Si l’on admet que les types caractère correspondent à des
types entiers de petite taille, il n’est alors pas plus choquant de dire qu’une
notation comme ‘a' est de type int que de dire qu’une « petite constante
numérique » comme +43 était de type int (et non short !).
Dans ces conditions, on peut s’interroger sur le fait que, suivant les
implémentations, le type char peut être signé ou non, de sorte que, suivant les
règles de conversion étudiées dans le chapitre suivant, le résultat peut être
parfois négatif. Nous allons examiner deux situations :
• on utilise les constantes caractère de façon naturelle, c’est-à-dire pour
représenter de vrais caractères ;
• on utilise les constantes caractère pour leur valeur numérique.

2.4.1 Utilisation naturelle des constantes caractère


En pratique, les trois instructions suivantes placeront le même motif dans c1, c2 et
c3 (la notation α désignant un caractère quelconque) :

char c1 = ‘ ' ;α
unsigned char c2 = ' ' ; α
signed char c3 = ' ' ; α
Il en ira de même si la constante caractère est exprimée sous forme octale ou
hexadécimale.
Cependant, si l’on examine la norme à la lettre, on constate que ces situations,
hormis la première, font intervenir une suite de deux conversions de char en int,
puis en char. En théorie (voir section 9 du chapitre 4), elles n’assurent la
conservation du motif binaire que dans certains cas : caractères appartenant au
jeu minimal d’exécution, conversions de signé en non signé dans les
implémentations utilisant le complément à deux. En pratique, ces conversions
conservent le motif binaire dans toutes les implémentations que nous avons
rencontrées. Si toutefois on cherche une portabilité absolue, on pourra se limiter
à l’utilisation du type char, à condition que l’ambiguïté correspondante ne s’avère
pas gênante (voir section précédente 2.2.3).

2.4.2 Utilisation des constantes caractère pour leur valeur


numérique
Il s’agit du cas où une telle constante apparaît dans une expression numérique ou
dans une affectation à une variable entière, ce qui peut se produire lorsqu’on
s’intéresse à la valeur du code du caractère correspondant. Dans ce cas,
l’implémentation intervient, non seulement sur la valeur obtenue, mais
éventuellement sur son signe. Voici un exemple dans lequel on suppose que le
code du caractère é est supérieur à 127, dans une implémentation codant les
caractères sur 8 bits et utilisant la représentation en complément à deux :
int n = ‘é' ; /* la valeur de n sera >0 si char est signé */
/* et >0 si char n'est pas signé */

On peut simplement affirmer que l’effet de cette déclaration sera équivalent à :


char c = ‘é' ; /* char est signé ou non suivant l'implémentation */
int n ;
…..
n = c ;

D’une manière comparable, dans la même implémentation (octets de 8 bits,


représentation en complément à deux) :
int n ='\xFE' ; /* -2 si le type char est signé par défaut */
/* 254 si le type char n'est pas signée par défaut */

On notera bien que, alors qu’on pouvait choisir l’attribut de signe d’une variable
de type char, il n’en va plus de même pour une constante. On peut toutefois faire
appel à l’opérateur de cast comme dans :
int n = (signed char) ‘\xfE' ; /* conv char -> signed char -> int */
/* -2 dans le cas du complément à deux */
int n = (unsigned char) ‘\xfE' ; /* conv char -> unsigned signed char -> int */
/* 254 dans le cas du complément à deux */

Remarques
Ici, il est important de ne pas confondre valeur et motif binaire. Le motif binaire associé à une
constante caractère est bien constant après conversion dans un type char de type quelconque (du
moins, en pratique) ; en revanche, la valeur numérique correspondante de type int, ne l’est pas.
La norme autorise des constantes caractère de la forme ‘xy' voire ‘xyz', contenant plusieurs
caractères imprimables. Certes, une telle particularité peut se justifier par le fait que la constante
produite est effectivement de type int et non de type char ; elle reste cependant d’un emploi malaisé
et, de toute façon, la valeur ainsi obtenue dépend de l’implémentation.

En C++
En C++, les constantes caractère seront effectivement de type char, et non plus de type int.
3. Le fichier limits.h

3.1 Son contenu


Ce fichier en-tête contient, sous forme de constantes ou macros (définies par la
directive #define), de nombreuses informations concernant le codage des entiers
et les limitations qu’elles imposent dans une implémentation donnée.
En voici la liste, accompagnée de la valeur minimale qu’on est assuré d’obtenir
pour chaque type d’entier quelle que soit l’implémentation. On notera la
présence d’une constante MB_LEN_MAX relative aux caractères dits « multi-octets »,
d’usage assez peu répandu, et dont nous parlerons au chapitre 22.

Tableau 3.8 : les valeurs définies dans le fichier limits.h

Valeur
Symbole Signification
minimale
CHAR_BIT 8 Nombre de bits dans un caractère
SCHAR_MIN -127 Plus petite valeur (négative) du type
signed char

SCHAR_MAX +127 Plus grande valeur du type signed char


UCHAR_MAX 255 Plus grande valeur du type unsigned char
CHAR_MIN Plus petite valeur du type char (signed
char ou unsigned char suivant
l’implémentation, ou même suivant les
options de compilation)
CHAR_MAX Plus grande valeur du type char (signed
char ou unsigned char suivant
l’implémentation, ou même suivant les
options de compilation)
MB_LEN_MAX 1 Nombre maximal d’octets dans un
caractère multi-octets (quel que soit le
choix éventuel de localisation)
SHRT_MIN - 32 767 Plus petite valeur du type short int
SHRT_MAX + 32 767 Plus grande valeur du type short int
USHRT_MAX 65 535 Plus grande valeur du type unsigned short
int

INT_MIN - 32 767 Plus petite valeur du type int


INT_MAX + 32 767 Plus grande valeur du type int
UINT_MAX 65 535 Plus grande valeur du type unsigned int
LONG_MIN - 2 147 483 Plus petite valeur du type long int
647
LONG_MAX + 2 147 Plus grande valeur du type long int
483 647
ULONG_MAX 4 294 967 Plus grande valeur du type unsigned long
int
295

3.2 Précautions d’utilisation


On notera bien que la norme impose peu de contraintes au type des constantes
définies dans limits.h. En général, on trouvera des définitions de ce genre :
#define INT_MAX +32767

Le symbole INT_MAX sera donc remplacé par le préprocesseur par la constante +32
767, laquelle sera de type int. Dans ces conditions, on évitera certains calculs
arithmétiques risquant de conduire à des dépassements de capacité dans le type
int. Le programme suivant montre ce qu’il ne faut pas faire, puis ce qu’il faut
faire, pour calculer la valeur de INT_MAX+5 :
Exemple de mauvais et de bon usage de la constante INT_MAX

#include <stdio.h>
#include <limits.h>
int main()
{
int n ;
long q ;
q = INT_MAX + 5 ; /* calcul incorrect */
printf ("INT_MAX+5 = %ld\n", q) ;
q = (long)INT_MAX + 5 ; /* calcul correct */
printf ("INT_MAX+5 = %ld\n", q) ;
}
INT_MAX+5 = -32764
INT_MAX+5 = 32772
4. Les types flottants
Nous commencerons par rappeler brièvement en quoi consiste le codage en
flottant d’un nombre réel et quels sont les éléments caractéristiques d’un codage
donné : précision, limitations, epsilon machine… Puis nous examinerons les
trois types de flottants prévus par la norme, leur nom et leurs caractéristiques
respectives. Nous terminerons par les différentes façons d’écrire des constantes
flottantes dans un programme source.

4.1 Rappels concernant le codage des nombres en


flottant
Les types flottants (appelés parfois, un peu à tort, réels) servent à représenter de
manière approchée une partie des nombres réels. Ils s’inspirent de la notation
scientifique des calculettes, dans laquelle on exprime un nombre sous forme
d’une mantisse et d’un exposant correspondant à une puissance de 10, comme
dans 0.453 E 15 (mantisse 0,453, exposant 15) ou dans 45.3 E 13 (mantisse 45,3,
exposant 13). Le codage en flottant se distinguera cependant de cette notation
scientifique sur deux points :
• le codage de la mantisse : il est généralement fait en binaire et non plus en base
10 ;
• la base utilisée pour l’exposant : on n’a guère de raison d’employer une base
décimale. En général, des bases de 2 ou de 16 seront utilisées car elles
facilitent grandement les calculs de la machine (attention, l’utilisation d’une
base 16 n’est pas incompatible avec le codage en binaire des valeurs de la
mantisse et de l’exposant) .
D’une manière générale, on peut dire que la représentation d’un nombre réel en
flottant revient à l’approcher par une quantité de la forme
s. m . be
dans laquelle :
• s représente un signe, ce qui revient à dire que s vaut soit -1, soit +1 ;
• m représente la mantisse ;
• e représente l’exposant, tel que : emin <= e <= emax ;
• b représente la base.
La base b (en pratique 2 ou 16) est fixée pour une implémentation donnée5. Il
n’en reste pas moins qu’un même nombre réel peut, pour une valeur b donnée,
être approché de plusieurs façons différentes par la formule précédente. Par
exemple, si un nombre est représentable par le couple de valeurs (m, e), il reste
représentable par le couple de valeurs (b/m, e+1) ou (mb, e-1)…
Pour assurer l’unicité de la représentation, on fait donc appel à une contrainte
dite de « normalisation ». En général, il s’agit de :
1/b <= m< 1
Dans le cas de la notation scientifique des calculettes, l’application de cette
contrainte conduirait à une mantisse commençant toujours par 0 et dont le
premier chiffre après le point décimal est non nul. Par exemple, 0.2345 et 0.124
seraient des mantisses normalisées, tandis que 3.45 ou 0.034 ne le seraient pas6.

4.2 Le modèle proposé par la norme


Contrairement à ce qui se passe, en partie du moins, pour les nombres entiers, la
norme ANSI n’impose pas de contraintes précises quant à la manière dont une
implémentation représente les types flottants. Elle se contente de proposer un
modèle théorique possible, dont le principal avantage est de donner une
définition formelle d’un certain nombre de paramètres caractéristiques tels que la
précision ou l’epsilon machine. Ce modèle correspond à la formule précédente
(voir section 4.1), dans laquelle on explicite la mantisse de la façon suivante, le
coefficient f1 étant non nul si le nombre est non nul :

Certes, cette formule définit la mantisse de façon unique, dès lors que π, b et les
limites emin et emax sont fixées. Comme on s’en doute, elles dépendront de
l’implémentation et du type de flottant utilisé (float, double ou long double). Mais il
ne s’agit que d’un modèle théorique de comportement et, même si la plupart des
implémentations s’en inspirent, quelques petits détails de codage peuvent
apparaître :
• élimination de certains bits superflus de la mantisse ; par exemple, lorsque la
base est égale à 2, le premier bit est toujours à 1 et certaines implémentations
ne le conservent pas, ce qui double la capacité ;
• réservation, comme le propose la norme IEEE 754, de certains motifs binaires
pour représenter des nombres infinis ou des quantités non représentables.

4.3 Les caractéristiques du codage en flottant


Le codage en entier n’a guère d’autres conséquences que de limiter le domaine
des valeurs utilisables. Dans le cas du codage en flottant, les conséquences sont
moins triviales et nous allons les examiner ici, en utilisant parfois le modèle
théorique présenté précédemment.

4.3.1 Représentation approchée


Le codage en flottant permet de représenter un nombre réel de façon approchée,
comme on le fait dans la vie courante en approchant le nombre réel pi par 3.14
ou 3.14159… La notion de représentation approchée paraît alors relativement
naturelle. En revanche, lorsqu’on a affaire à un nombre décimal tel que 0.1, qui
s’exprime de manière exacte dans notre système décimal, on peut être surpris de
ce qu’il ne s’exprime plus toujours de façon exacte une fois codé en flottant7.
Voici un petit programme illustrant ce phénomène, dans une implémentation où
la base b de l’exposant est égale à 2 :
Conséquences de la représentation approchée des nombres flottants

include <stdio.h>
int main()
{ float x = 0.1 ;
printf ("x avec 1 decimale : %.1e\n", x) ;
printf ("x avec 10 decimales : %.10e\n", x) ;
}
x avec 1 decimale : 1.0e-01
x avec 10 decimales : 1.0000000149e-01

Cependant, la norme impose aux entiers dont la valeur absolue est inférieure à
une certaine limite d’être représentés de façon exacte en flottant, de façon à ce
qu’un cycle de conversion entier → flottant → entier permette de retrouver la
valeur d’origine. Ces limites dépendent à la fois du type de flottant concerné et
de l’implémentation ; elles sont précisées dans le tableau 3.8, qui montre qu’elles
sont toujours au moins égales à 1E6.

4.3.2 Notion de précision


On peut définir la précision d’une représentation flottante :
• soit en considérant le nombre de chiffres en base b, c’est-à-dire finalement la
valeur de p dans le modèle défini par la norme et présenté à la section 4.2 ;
cette valeur est définie de façon exacte ;
• soit en cherchant à exprimer cette précision en termes de chiffres décimaux
significatifs ; en théorie, on peut montrer que p chiffres exacts en base b
conduisent toujours à au moins q chiffres décimaux exacts, avec q = (p-
1).log10 b ; autrement dit, tout nombre entier d’au plus q chiffres s’exprime
sans erreur en flottant.
Ces différentes valeurs sont fournies dans le fichier float.h décrit à la section 5.

4.3.3 Limitations des valeurs représentables


Dans le cas du type entier, les valeurs représentables appartenaient simplement à
un intervalle de l’ensemble des entiers relatifs. Dans le cas des flottants, on a
affaire à une limitation des valeurs de l’exposant emin et emax, lesquelles
conduisent en fait à une limitation de l’amplitude de la valeur absolue du
nombre. Les valeurs réelles représentables appartiennent donc à deux intervalles
disjoints, de la forme :
[-xmax, -xmin] [xmin, xmax] avec xmin = bemin et xmax = bemax
En outre, la valeur 0 (qui n’appartient à aucun de ces deux intervalles) est
toujours représentable de façon exacte ; l’unicité de sa représentation nécessite
l’introduction d’une contrainte conventionnelle, par exemple, mantisse nulle,
exposant 1.

4.3.4 Non-respect de certaines règles de l’algèbre


La représentation approchée des types flottants induit des différences de
comportement par rapport à l’algèbre traditionnelle.
Certes, la commutativité des opérations est toujours respectée ; ainsi les
opérations a+b ou b+a donneront-elles toujours le même résultat (même si
celui-ci n’est qu’une approximation de la somme).
En revanche, si a et b désignent des valeurs réelles et si x’ désigne
l’approximation en flottant de l’expression x, on n’est pas assuré que les
conditions suivantes soient vérifiées :
(a + b)’ = a’ + b’
(a’ + b’)’ = a’ + b’
Par exemple :
float x = 0.1, y = 0.1
…..
if (x + y == 0.2) /* peut être vrai ou faux */

De façon comparable, on n’est pas assuré que ces conditions soient vérifiées8 :
(3 * a)’ = 3 * a’
(3 * a’) ‘ = 3 * a’
Par exemple :
float x = 0.1 ;
if (3*x == 0.3) /* peut être vrai ou faux */

Par ailleurs, l’associativité de certaines opérations n’est plus nécessairement


respectée. On n’est plus assuré que :
a’ + (b’+c’)’ = (a’ + b’)’ + c’
Tout ceci se compliquera encore un peu plus avec l’incertitude qui règne en C
sur l’ordre d’évaluation des opérateurs commutatifs, comme nous le verrons à la
section 2.1.4 du chapitre 4.

4.3.5 Notion d’epsilon machine


Compte tenu de la représentation approchée du type flottant, on peut aisément
trouver des nombres eps tels que la représentation de la somme de eps+1 soit
identique à celle de 1, autrement dit que la condition suivante soit vraie :
1 + eps == 1

La plus grande de ces valeurs se nomme souvent « l’espilon machine ». On peut


montrer qu’elle est définie par :
eps = b1-p
où b et π sont définis par le modèle de comportement ANSI présenté à la section
4.2.
On en trouvera la valeur pour chacun des types flottants dans le fichier float.h
décrit à la section 5.

4.4 Représentation mémoire et limitations


La norme ANSI prévoit les trois types de flottants suivants :
Tableau 3.9 : les trois types flottants prévus par la norme

Bien entendu, les caractéristiques exactes de chacun de ces types dépendent à la


fois de l’implémentation et du type concerné. Un certain nombre d’éléments sont
cependant généralement communs, dans une implémentation donnée, aux trois
types de flottants :
• la technique d’approximation (arrondi par défaut, par excès, au plus
proche…) ;
• la valeur de la base de l’exposant b ;
• la manière dont la mantisse est normalisée.
D’autres éléments, en revanche, dépendent effectivement du type de flottant (et
aussi de l’implémentation) à savoir :
• le nombre de bits utilisés pour coder la mantisse m ;
• le nombre de bits utilisés pour coder l’exposant e.
D’une manière générale, le fichier float.h contient bon nombre d’informations
concernant les caractéristiques des flottants.

Remarques
1. La première définition du langage C (Kernighan et Ritchie) ne comportait pas le type long double.
En outre, long float y apparaissait comme un synonyme de double ; cette possibilité a disparu de
la norme.
2. Certaines implémentations acceptent des valeurs flottantes non normalisées, c’est-à-dire des valeurs
dans lesquelles la mantisse comporte un ou plusieurs de ses premiers chiffres (en base b) nuls. Dans
ce cas, il devient possible de manipuler des valeurs inférieures en valeur absolue au minimum
imparti au type, moyennant, alors une perte de précision…

4.5 Écriture des constantes flottantes


Comme dans la plupart des langages, les constantes réelles peuvent s’écrire
indifféremment suivant l’une des deux notations :
• décimale ;
• exponentielle.
La notation décimale doit obligatoirement comporter un point (correspondant à
notre virgule). La partie entière ou la partie décimale peuvent être omises (mais
bien sûr pas toutes les deux en même temps !). En voici quelques exemples
corrects :
12.43 -0.38 -.38 4. .27

En revanche, la constante 47 serait considérée comme entière et non comme


flottante. Dans la pratique, ce fait aura peu d’importance9, compte tenu des
conversions automatiques qui seront mises en place par le compilateur (et dont
nous parlerons au chapitre suivant).
La notation exponentielle utilise la lettre e (ou E) pour introduire un exposant
entier (puissance de 10), avec ou sans signe. La mantisse peut être n’importe
quel nombre décimal ou entier (le point peut être absent dès qu’on utilise un
exposant). Voici quelques exemples corrects (les exemples d’une même ligne
étant équivalents) :
4.25E4 4.25e+4 42.5E3
54.27E-32 542.7E-33 5427e-34
48e13 48.e13 48.0E13

4.6 Le type des constantes flottantes


Par défaut, toutes les constantes sont créées par le compilateur dans le type
double. Il est cependant possible d’imposer à une constante flottante :

• d’être du type float, en faisant suivre son écriture de la lettre F (ou f), comme
dans 1.25E+03f ; cela permet de gagner un peu de place mémoire, en
contrepartie d’une éventuelle perte de précision ;
• d’être du type long double, en faisant suivre son écriture de la lettre L (ou l),
comme dans 1.0L ; cela permet de gagner en précision, en contrepartie d’une
perte de place mémoire ; c’est aussi le seul moyen de représenter les valeurs
très grandes ou très petites (bien qu’un tel besoin soit rare en pratique).

4.7 En cas de dépassement de capacité dans l’écriture


des constantes
Contrairement à ce qui se produit pour les entiers, la manière dont est écrite une
constante flottante impose son type, de manière unique : float, double ou long
double. Dans chacun de ces trois cas, vous avez affaire à des limitations propres, à
la fois :
• vers « le bas » : une constante de valeur absolue trop petite ne peut être
représentée (avec une erreur relative d’approximation raisonnable) ; on parle
alors de sous-dépassement de capacité (en anglais underflow) ;
• vers « le haut » : une constante de valeur absolue trop grande ne peut être
représentée (avec une erreur relative d’approximation raisonnable) : on parle
alors de dépassement de capacité (en anglais overflow).
Là encore, suivant les compilateurs, on pourra obtenir : un diagnostic de
compilation, une valeur fantaisiste ou une utilisation des conventions IEEE 754
(présentées à la section 2.2.2) en cas de dépassement de capacité, une valeur
fantaisiste ou une valeur nulle en cas de sous-dépassement de capacité.
5. Le fichier float.h
Ce fichier contient, sous forme de constantes ou de macros (définies par la
directive #define), de nombreuses informations concernant :
• les caractéristiques du codage des flottants tel qu’il est défini par le modèle
théorique de comportement proposé par la norme et présenté à la section 4.2 :
base b, précision p en base b ou précision en base 10, epsilon machine…
• les limitations correspondantes : emin, emax, xmin, xmax…
En voici la liste. On notera que, à l’exception des symboles FLT_ROUNDS et FLT_RADIX
qui concernent les trois types flottants (float, double et long double), les autres
symboles sont définis pour les trois types avec le même suffixe et un préfixe
indiquant le type concerné :
• FLT : le symbole correspondant (par exemple, FLT_MIN_EXP) concerne le type float ;
• DBL : le symbole correspondant (par exemple, DBL_MIN_EXP) concerne le type
double ;

• LDBL : le symbole correspondant (par exemple, LDBL_MIN_EXP) concerne le type long


double.

Tableau 3.10 : le contenu du fichier float.h

Valeur
Symbole Signification
minimale
FLT_RADIX
2 Base b telle que définie à la section 4.2
FLT_ROUNDS
Méthode utilisée pour déterminer la
représentation d’un nombre réel donné :
-1 : indéterminée
0 : arrondi vers zéro
1 : arrondi au plus proche
2 : arrondi vers plus l’infini
3 : arrondi vers moins l’infini
autre : méthode définie par
l’implémentation
FLT_MANT_DIG
DBL_MANT_DIG Précision (p dans la formule de la section
LDBL_MANT_DIG 4.2)
FLT_DIG
DBL_DIG 6 Valeur q, telle que tout nombre décimal de
LDBL_DIG 10 q chiffres puisse être exprimé sans erreur
10 en notation flottante ; on peut montrer
que :
q = (p-1).log10b (+1 si b est puissance de
10)
FLT_MIN_EXP
DBL_MIN_EXP Plus petit nombre négatif n tel que
LDBL_MIN_EXP FLT_RADIX soit un nombre flottant
n-1

normalisé ; il s’agit de emin tel qu’il est


défini à la section 4.2
FLT_MIN_10_EXP
DBL_MIN_10_EXP -37 Plus petit nombre négatif n tel que 10n soit
LDBL_MIN_10_EXP -37 dans l’intervalle des nombres flottants
-37 normalisés ; on peut montrer que :
n = log 10 be min-1

FLT_MAX_EXP
DBL_MAX_EXP Plus grand nombre n tel que FLT_RADIX soit
n-1

LDBL_MAX_EXP un nombre flottant fini représentable ; il


s’agit de emax tel qu’il est défini à la section
4.2
FLT_MAX_10_EXP
DBL_MAX_10_EXP
+37 Plus grand entier n tel que 10n soit dans
LDBL_MAX_10_EXP +37 l’intervalle des nombres flottants finis
+37 représentables ; on peut montrer que :
n = log10((1-b-p)be ) max

FLT_MAX
DBL_MAX 1e37 Plus grande valeur finie représentable ; on
LDBL_MAX 1e37 peut montrer qu’il s’agit de :
1e37 (1-b-p)be max

FLT_EPSILON
DBL_EPSILON 1e-5 Écart entre 1 et la plus petite valeur
LDBL_EPSILON 1e-9 supérieure à 1 qui soit représentable ; il
1e-9 s’agit de ce que l’on nomme généralement
l’epsilon machine dont on montre qu’il est
égal à : b1-p
FLT_MIN
DBL_MIN
1e-37 Plus petit nombre flottant positif
LDBL_MIN 1e-37 normalisé. On montre qu’il est égal à : be min-

1
1e-37
6. Déclarations des variables d’un type de base
Le tableau 3.11 récapitule les différents éléments pouvant intervenir dans la
déclaration des variables d’un type de base. Ils seront détaillés dans les sections
indiquées.

Tableau 3.11 : déclaration de variables d’un type de base

Rôle d’une – associe un spécificateur de type voir section


déclaration (éventuellement complété de qualifieurs 6.1
et d’une classe de mémorisation), à un
déclarateur ;
– dans le cas des types de base, le
déclarateur se limite au nom de la
variable.
Initialisation – aucune initialisation par défaut ; voir section
variables 6.2
classe – initialisation explicite par expressions
automatique quelconques.
(ou registre)
Initialisation – initialisation par défaut à zéro ; voir section
variables 6.2
classe – initialisation explicite par des
statique expressions constantes.
Qualifieurs – const : la variable ne peut pas voir sa voir section
(const, valeur modifiée ; 6.3
volatile)
– volatile : la valeur de la variable peut
changer, indépendamment des
instructions du programme ;
– une variable constante doit être
initialisée (il existe deux rares
exceptions – voir remarque 3 de la
section 6.3.2).
Classe de – extern : pour les redéclarations de voir
mémorisation variables globales ; chapitre 8
– auto : pour les variables locales
(superflu) ;
– static : variable rémanente ;
– register : demande de maintien dans un
registre.

6.1 Rôle d’une déclaration


6.1.1 Quelques exemples simples
Comme on s’y attend, la déclaration d’une variable d’un type de base permet de
préciser son nom et son type, par exemple :
unsigned int n ; /* n est de type unsigned int */

On peut déclarer plusieurs variables dans une seule instruction. Par exemple :
unsigned int n, p ;

est équivalente à :
unsigned int n ;
unsigned int p ;

Un même type peut être défini par des spécificateurs de type différents. Par
exemple, ces quatre instructions sont équivalentes :
short int p
signed short p
signed short int p ;
short p ;

Les tableaux 3.1, 3.5 et 3.9 fournissent les différents spécificateurs de type qu’il
est possible d’utiliser pour un type donné.

6.1.2 Les déclarations de variables en général


Tant qu’on se limite à des variables d’un type de base, les déclarations restent
relativement simples puisque, comme dans les précédents exemples, elles
associent un simple identificateur à un spécificateur de type. On peut cependant
y trouver quelques informations supplémentaires, parmi les suivantes :
• une valeur initiale de la variable, comme dans :
int n, p=5, q ; /* n, p et q sont des int; p est initialisée à 5 */

• un ou plusieurs qualifieurs (const ou volatile) associés au spécificateur de type


et qui concernent donc l’ensemble des variables de la déclaration, par
exemple :
const float x, y ; /* x et y sont des float constants */

• une classe de mémorisation associée, elle aussi, à l’ensemble des variables de


la déclaration, par exemple :
static int n, q ; /* n et q sont déclarés avec la classe de mémorisation static */

Les deux premiers points (valeur initiale et qualifieurs) sont examinés ici, dans
le seul cas cependant des variables d’un type de base. Pour les autres types, on
trouvera des compléments d’information dans les chapitres correspondants
(tableaux, pointeurs, structures, unions, énumérations). Quant à la classe de
mémorisation, dont on a dit au chapitre 1 qu’elle pouvait influer sur la classe
d’allocation des variables, elle est étudiée en détail au chapitre 8.

Remarque
D’une manière générale, les déclarations en C sont complexes et parfois peu naturelles. Ainsi, une
même instruction peut déclarer des variables de types différents, par exemple un entier, un pointeur sur
un entier et un tableau d’entiers, comme dans :
unsigned int n, *adi, t[10] ;

Pour connaître le type correspondant à un identificateur donné, on considère qu’une telle déclaration
associe un spécificateur de type (ici unsigned int) non pas simplement à des identificateurs, mais à
des déclarateurs (ici n, *adi et t[10]). Il existe trois formes de déclarateurs (tableaux, pointeurs,
fonctions) qui peuvent se composer à volonté. Chacun de ces déclarateurs sera étudié dans le chapitre
correspondant, tandis que le chapitre 16 récapitulera tout ce qui concerne les déclarations.

6.2 Initialisation lors de la déclaration


Une variable peut être initialisée lors de sa déclaration comme dans :
int n = 5 ;

Cela signifie que la valeur 5 sera placée dans l’emplacement correspondant à n,


avant le début de l’exécution de la fonction ou du bloc contenant cette
déclaration (pour les variables de classe automatique) ou avant le début de
l’exécution du programme (pour les variables de classe statique).
On notera bien qu’une variable ainsi initialisée reste une « vraie variable », c’est-
à-dire que son contenu peut tout à fait être modifié lors de l’exécution du
programme ou de la fonction correspondante.
Dans une même déclaration, on peut initialiser certaines variables et pas
d’autres :
int n=5, p, q=3 ;

L’expression utilisée pour initialiser une variable porte le nom d’initialiseur. Le


chapitre8 fait le point sur les différents initialiseurs qu’il est possible d’utiliser
pour tous les types de variables. Pour résumer ce qui concerne les variables d’un
type de base, disons qu’un initialiseur peut être :
• une expression quelconque pour les variables de classe automatique ;
• une expression constante, c’est-à-dire calculable par le compilateur, pour les
variables de classe statique ; la notion d’expression constante est étudiée en
détail à la section 14 du chapitre 4.
Le type de l’expression servant d’initialiseur n’est pas obligatoirement du type
de la variable à initialiser ; il suffit qu’il soit d’un type autorisé par affectation
(voir section 7 du chapitre 4).
Voici quelques exemples :
float x = 5 ; /* la valeur entière 5 sera convertie en float */
/* comme elle le serait dans une affectation */
int n = 8.23 ; /* la valeur flottante (environ 8,23) sera convertie */
/* en int comme elle serait dans une affectation */
/* ici, il serait plus raisonnable d'écrire : */
/* int n = 8 ; */
float x = 40.73 ;
int n = x/2.3 ; /* l'expression x/2.3 est évaluée en flottant ; */
/* son résultat est converti en entier */

6.3 Les qualifieurs const et volatile


La norme ANSI a introduit la possibilité d’ajouter dans une déclaration des
qualifieurs choisis parmi les mots-clés const et volatile. Le premier est de loin le
plus utilisé, et son rapprochement avec le second n’est qu’une pure affaire de
syntaxe. Ici, nous étudions la signification de ces qualifieurs lorsqu’ils sont
appliqués à une variable d’un type de base.

6.3.1 Le qualifieur const


Considérons la déclaration :
const int n = 5, p = 12 ;

Elle précise que n et p sont de type int et que, de plus, leur valeur ne devra pas
varier au fil de l’exécution du programme. Cependant, la norme ne précise pas
de façon exhaustive les situations que le compilateur devrait interdire. Dans la
plupart des implémentations, il rejettera alors une instruction telle que les
suivantes, dès lors qu’elles figurent dans la portée de la déclaration de n (la
fonction ou le bloc pour une variable locale, la partie du fichier source suivant sa
déclaration pour une variable globale) :
n = 6 ; /* généralement rejeté puisque n est qualifié de constant */
n++ ; /* généralement rejeté puisque n est qualifié de constant */

Cependant, quelle que soit la bonne volonté du compilateur, des modifications


indirectes de ces variables restent possibles, notamment :
• par appel d’une fonction de lecture, par exemple :
scanf ("%d", &n) ; /* toujours accepté car le compilateur n'a aucune */
/* connaissance du rôle de scanf */

• par l’utilisation d’un pointeur sur ces variables (pour peu que le qualifieur const
n’ait pas été attribué à l’objet pointé, comme on le verra au chapitre 7…).
Il n’en reste pas moins que l’usage systématique de const améliore la lisibilité des
programmes.

Remarques
1. Comme on le verra au chapitre 4, une variable déclarée d’un type qualifié par const n’est pas une
expression constante ; il s’agit là d’une lacune importante de la norme ANSI du C, à laquelle le
langage C++ a d’ailleurs remédié.
2. La norme ANSI laisse l’implémentation libre d’allouer les emplacements destinés à des objets
constants dans une mémoire protégée contre toute modification pendant l’exécution du programme.
Dans ce cas, les tentatives de modification de tels objets, lorsqu’elles ne sont pas rejetées à la
compilation, provoqueront obligatoirement une erreur d’exécution.

6.3.2 Le qualifieur volatile


Il s’emploie de la même manière que const ; il sert à préciser au compilateur
qu’une variable (ou un objet pointé) peut voir sa valeur évoluer,
indépendamment des instructions du programme. Un tel changement peut par
exemple être provoqué :
• par une interruption ;
• par un périphérique qui agit sur des emplacements particuliers de la mémoire ;
dans ce cas, volatile s’appliquera généralement à un objet pointé plutôt qu’à
une variable sauf si l’on dispose dans l’implémentation concernée d’un moyen
permettant d’imposer une adresse à une variable.
L’intérêt de ce qualifieur volatile est d’interdire au compilateur d’effectuer
certaines optimisations. Par exemple, avec :
volatile int etat ;
int n ;
…..
while (…)
{ n = etat + 1 ;
…..
}

le compilateur ne sortira jamais l’instruction n = etat + 1 de la boucle, comme il


pourrait le faire si etat n’avait pas été déclarée avec le qualifieur volatile et
qu’elle n’était pas modifiée à l’intérieur de la boucle.

Remarques
1. Les qualifieurs const ou volatile s’appliquent à toutes les variables mentionnées dans l’instruction
de déclaration : il n’est pas possible, dans une même instruction, de déclarer une variable ayant le
qualifieur const et une autre ne l’ayant pas. Cette remarque ne s’appliquera cependant pas aux
variables de type pointeur, compte tenu de la manière dont ces qualifieurs sont alors utilisés.
2. En théorie, il est possible d’utiliser conjointement les deux qualifieurs const et volatile (l’ordre
est alors indifférent) :
const volatile int n ; /* la valeur de n ne peut pas être modifiée par le */
/* programme mais elle peut l'être "de l'extérieur" */
/* c'est pourquoi elle peut ne pas être initialisée */
volatile const int p = 5 ; /* même chose mais ici, p a été initialisée */
/* lors du déroulement du programme, sa valeur */
/* pourra devenir différente de 5 */

3. Une variable ayant reçu l’attribut const doit être initialisée lors de sa déclaration, à deux exceptions
près :
• elle possède en plus le qualifieur volatile : elle pourra donc être modifiée indépendamment du
programme ; son initialisation n’est donc pas indispensable mais elle reste possible ;
• il s’agit de la redéclaration d’une variable globale (par extern) ; l’initialisation a dû être faite par
ailleurs ; qui plus est, l’initialisation est alors interdite à ce niveau.

1. Même si la norme n’impose pas formellement l’existence d’un bit de signe.


2. La norme ne dit jamais comment doit réagir le compilateur ou le programme en cas de situation
d’exception, c’est-à-dire de non-respect de la norme.
3. Aussi appelé « barre inverse » ou « contre-slash » (back-slash en anglais).
4. D’autant plus que certaines implémentations se permettent de limiter d’office (souvent à 3) le nombre de
caractères pris effectivement en compte dans la notation hexadécimale.
5. En toute rigueur, rien n’interdirait à une implémentation d’utiliser une base (par exemple, 2) pour un type
(par exemple, float) et une autre base (par exemple, 16) pour un autre type (par exemple, long double).
Cela conduirait toutefois à complexifier inutilement l’unité centrale, de sorte qu’en pratique, cette
situation ne se rencontre pas !
6. Attention à ne pas confondre la contrainte de normalisation utilisée pour coder un nombre flottant en
mémoire avec celle qu’utilise printf pour afficher un tel nombre.
7. Pour s’en convaincre, il suffit d’exprimer la valeur 0,1 dans le modèle de comportement proposé par la
norme : on s’aperçoit que la conversion en binaire (exprimée en puissances négatives de b) conduit dans
les cas usuels (b=2 ou b=16) à une mantisse m ayant un nombre infini de décimales, et donc
obligatoirement à une approximation, quel que soit le nombre de bits réservés à m.
8. Ici, il n’est pas nécessaire de considérer 3’ car, dans toute implémentation, 3’ = 3 puisque tout nombre
entier d’au plus q chiffres s’exprime exactement en flottant (voir section 4.3.2) et que la valeur de q est
toujours supérieure ou égale à 6 (voir section 5).
9. Si ce n’est au niveau du temps d’exécution.
4
Les opérateurs
et les expressions

Dans tous les langages, les notions d’opérateur et d’expression sont étroitement
liées : une expression se forme à partir de variables, de constantes et
d’opérateurs. Il en va de même en langage C, qui se trouve d’ailleurs être l’un
des plus fournis en matière d’opérateurs. Cette richesse se manifeste par
l’existence d’opérateurs spécialisés de manipulation de bits et surtout par
l’existence d’opérateurs d’affectation et d’incrémentation qui modifient
notablement la notion habituelle d’expression. De surcroît, le langage C introduit
des opérateurs dits « de référence », là où d’autres langages se contentent d’une
notation syntaxique : accès aux éléments d’un agrégat ou appel de fonction. Ce
dernier aspect reste cependant mineur, dans la mesure où il n’intervient guère
que pour régler des problèmes de priorité ou d’associativité.
Ce chapitre étudie les différents opérateurs du langage C, les règles de priorité et
d’associativité correspondantes, ainsi que les éventuelles conversions implicites
qui interviennent dans l’évaluation de leurs opérandes. Toutefois, les opérateurs
dits « de référence » trouveront tout naturellement leur place dans les chapitres
correspondants : tableaux pour [], fonctions pour (), structures pour -> et .…
Pour préserver à l’ouvrage son caractère de référence, ces opérateurs seront
malgré tout cités dans certains tableaux récapitulatifs.
Après quelques généralités concernant les notions de priorité, associativité,
pluralité et conversion implicite, nous ferons une étude systématique de ces
opérateurs, en les regroupant par catégorie : arithmétiques, relationnels,
logiques, manipulation de bits, affectation, incrémentation, cast et divers. Au
passage, nous décrirons les différentes conversions implicites numériques. Enfin,
après un tableau récapitulatif des priorités et associativité de tous les opérateurs
du langage, nous présenterons ce que l’on nomme des « expressions
constantes », c’est-à-dire des expressions calculables par le compilateur.
1. Généralités

1.1 Les particularités des opérateurs et des


expressions en C
Dans la plupart des langages, une expression se définit comme l’indication d’une
suite de calculs à effecteur et dont le résultat constitue la valeur ; par ailleurs, il
existe des instructions (affectation, écriture…) pouvant faire intervenir des
expressions. Certes, cet aspect classique se retrouve en C. Par exemple, dans
l’affectation :
y = a * x + b ;

apparaît l’expression a * x + b ; de même, dans l’instruction d’affichage :


printf ("valeur %d", n + 2*p) ;

apparaît l’expression n + 2 * p.
Mais dans la plupart des langages autres que C, l’expression se contente de
posséder une valeur ; elle ne réalise aucune action, en particulier son calcul ne
modifie la valeur d’aucun objet ; autrement dit, on n’y confond pas affectation et
expression. D’autre part, dans ces langages, l’affectation est une instruction dont
le rôle est d’affecter la valeur d’une expression à un objet ; parler de la valeur
d’une affectation n’aurait pas de sens !
En C, il en va tout autrement. D’une part, certains opérateurs peuvent non
seulement intervenir au sein d’une expression, mais également agir sur le
contenu d’objets. Par exemple, l’expression ++i réalisera une action : augmenter
la valeur de i de 1 ; en même temps, elle aura une valeur, à savoir celle de i
après incrémentation. D’autre part, l’affectation y est réalisée par des opérateurs.
La notation :
i = 5

n’est pas une instruction, mais une expression formée d’un opérateur (=) recevant
deux opérandes (i et 5).
Certes, il n’y a qu’un pas entre cette dernière expression et une instruction telle
que :
i = 5 ;

Mais cette expression peut apparaître à son tour au sein d’expressions plus
complexes comme :
k = i = 5
a = b * (i = 5)

Ces dernières formes sont généralement absentes de la plupart des autres


langages.
En outre, une expression peut ne posséder aucune valeur ; ce sera le cas de
l’appel d’une fonction ne renvoyant aucune valeur.

Remarque
La principale instruction du langage C est ce qu’on nomme « l’instruction expression », c’est-à-dire
une expression terminée par un point virgule. Toute expression peut devenir une instruction en la
faisant suivre d’un point virgule : sa valeur, si elle existe, est simplement inutilisée à ce moment-là.
Par exemple, dans :
k = i = 5 ;

on a bien deux affectations : la valeur de la première affectation (ici, 5) est utilisée pour être affectée à
son tour à k ; la valeur de la seconde (en l’occurrence, ici, toujours 5) est inutilisée.
En revanche, une expression peut être utilisée ailleurs que dans une instruction expression ; dans ce
cas sa valeur est bien utilisée.

1.2 Priorité et associativité


1.2.1 Priorités et parenthèses
Lorsque plusieurs opérateurs apparaissent dans une même expression, il est
nécessaire de savoir dans quel ordre ils sont mis en jeu. En C comme dans les
autres langages, on utilise des règles de priorité qui permettent de définir
exactement l’ordre dans lequel doivent être évalués les opérateurs. Par exemple :
a + b * c /* * est prioritaire sur + : on fera donc le produit de b par c */
/* avant d'ajouter a au résultat */

En outre, des parenthèses permettent d’outrepasser ces règles de priorité, en


forçant le calcul préalable de l’expression qu’elles contiennent. Notez que ces
parenthèses peuvent également être employées pour assurer une meilleure
lisibilité d’une expression.
Ces règles de priorité sont totalement naturelles dans le cas des opérateurs
arithmétiques ; elles rejoignent alors les règles de l’algèbre traditionnelle. Quant
aux priorités des autres opérateurs, elles ont été manifestement définies de
manière à éviter au maximum le recours aux parenthèses : ces dernières pourront
toutefois toujours être utilisées pour éviter toute ambiguïté au lecteur du
programme.
On notera que, comme en algèbre, les priorités relatives ne sont pas définies
opérateur par opérateur, mais par groupe d’opérateurs. Par exemple, * et / ont
même priorité. Le choix entre deux opérateurs de même priorité se fera suivant
la règle d’associativité évoquée ci-après.

1.2.2 Associativité
En cas de priorité identique entre deux opérateurs (ou, donc, en cas d’opérateurs
identiques), les calculs s’effectuent très souvent de gauche à droite, comme
dans :
a + b - c /* + et - ont la même priorité : + est évalué avant - */
a * b / c /* * et / ont la même priorité : * est évalué avant / */

On dit qu’on a affaire à une « associativité de gauche à droite ». Quelques rares


opérateurs possèdent une « associativité de droite à gauche » ; c’est notamment
le cas de l’opérateur d’affectation :
a = b = c /* la seconde affectation est réalisée avant la première */

1.3 Pluralité
Comme dans tous les langages, on distingue :
• les opérateurs unaires, c’est-à-dire ne portant que sur un seul opérande ;
• les opérateurs binaires, c’est-à-dire portant sur deux opérandes.
En outre, de façon assez singulière, C dispose d’un opérateur ternaire, c’est-à-
dire à trois opérandes. Il s’agit de l’opérateur conditionnel qui s’exprime avec
deux symboles disjoints servant en quelque sorte à délimiter les différents
opérandes.
On notera que, comme en algèbre, certains symboles d’opérateur sont à la fois
unaires et binaires, par exemple :
a - b /* - binaire */
- c /* - unaire */
a * b /* * binaire : produit de deux nombres */
*adr /* * unaire : déréférenciation de pointeur */

Dans un tel cas, la priorité de l’opérateur unaire est différente de celle de


l’opérateur binaire correspondant. Notez qu’il en va exactement ainsi en algèbre
dans -a - b où le moins unaire est bien évalué avant le moins binaire.
En revanche, lorsqu’un même symbole opérateur binaire dispose de plusieurs
significations, en fonction du contexte, sa priorité reste exactement la même
dans les deux cas. C’est le cas de l’opérateur somme (+), qu’il soit appliqué à
deux nombres ou à un nombre et un pointeur. Cela ne concerne en fait qu’un tout
petit nombre d’opérateurs et, au demeurant, reste conforme au bon sens.

1.4 Conversions implicites


La plupart des opérateurs imposent des contraintes sur le type de leurs
opérandes. L’exemple le plus flagrant est celui des opérateurs arithmétiques
binaires qui ne sont définis que pour deux opérandes d’un même type de base
autre que char ou short. En fait, beaucoup d’opérateurs prévoient des possibilités
de conversions implicites de leurs opérandes, ce qui permet d’en élargir les
possibilités. Ainsi, les opérandes des opérateurs arithmétiques binaires seront
soumis à la fois à ce que l’on nomme des « promotions numériques » et à des
« conversions d’ajustement de type ». Les premières leur donneront un sens avec
des opérandes d’un type char ou short ; les secondes leur donneront un sens
lorsque leurs opérandes seront de type différent. De même, les opérateurs unaires
ne sont pas définis pour les types char et short. Là encore, le compilateur saura
leur donner un sens dans ce cas en soumettant leur unique opérande à des
promotions numériques.
D’une manière générale, la plupart des opérateurs, et pas seulement les
opérateurs arithmétiques, soumettent leurs opérandes à de telles conversions,
certains se limitant à des promotions numériques (les opérateurs unaires sont
obligatoirement dans ce cas). C’est pourquoi l’étude des ces conversions
implicites fait l’objet d’un paragraphe séparé. Leur existence sera cependant
toujours mentionnée pour chacun des opérateurs concernés.

1.5 Les différentes catégories d’opérateurs


Le tableau 4.1 décrit les différentes catégories d’opérateurs qui sont étudiées ici.

Tableau 4.1 : les différentes catégories d’opérateurs étudiées dans ce


chapitre

Catégorie Description Voir


Opérateurs Ils effectuent les calculs arithmétiques Section
arithmétiques classiques. 2
Opérateurs Ils permettent de comparer des valeurs Section
relationnels numériques et fournissent un résultat de 4
type « logique », c’est-à-dire ne possédant
que deux valeurs « vrai » ou « faux »,
valeurs qui seront représentées en C par
des valeurs entières particulières 0 et 1.
Opérateurs Ils relient plusieurs expressions logiques Section
logiques par et, ou ou non. En fait, là encore, ils 5
s’appliquent non seulement à une valeur
logique (c’est-à-dire en C, entier 0 ou 1),
mais également à n’importe quelle valeur
numérique, toute valeur non nulle étant
interprétée comme « vrai ».
Opérateurs de Ils travaillent directement sur des motifs Section
manipulation de binaires et permettent d’effectuer des 6
bits combinaisons logiques bit à bit et des
décalages.
Opérateurs Ils servent à affecter à un objet, la valeur Section
d’affectation d’une expression. Ils font intervenir 7
l’importante notion de lvalue, c’est-à-dire
de référence à un objet dont on peut
modifier la valeur. S’il existe plusieurs
opérateurs d’affectation, c’est parce que le
C permet de condenser certaines écritures
faisant intervenir une affectation et une
expression arithmétique simple en ce que
nous nommons un opérateur d’affectation
élargie.
Opérateurs Ce sont des cas particuliers d’affectation Section
d’incrémentation élargie ; ils permettent d’en condenser 7
encore un peu plus l’écriture.
Opérateurs cast Ils effectuent, de façon explicite, des Section
conversions de type. 8
Opérateurs Il s’agit de l’opérateur conditionnel, de Sections
divers l’opérateur séquentiel et de l’opérateur 10, 11
sizeof. et 12
2. Les opérateurs arithmétiques
On nomme « opérateurs arithmétiques », les opérateurs permettant d’effectuer
les opérations arithmétiques classiques. A priori, ils portent sur des opérandes de
type numérique, mais l’addition et la soustraction pourront posséder un ou deux
opérandes de type pointeur (ce point sera étudié au chapitre 7).
Nous présenterons d’abord les différents opérateurs existants, leur rôle et leur
priorité en citant les éventuelles conversions implicites qu’ils peuvent mettre en
œuvre. Puis nous examinerons les situations dites « d’exception », c’est-à-dire
celles dans lesquelles le résultat d’une opération n’est plus défini.

2.1 Les différents opérateurs numériques


2.1.1 Présentation générale
Le tableau 4.2 présente, classés en trois catégories, les différents opérateurs
numériques, en précisant les contraintes portant sur les opérandes et les
conversions implicites qui peuvent leur être appliquées (elles seront étudiées à la
section 3). Les opérateurs d’une même catégorie ont même priorité et les
différentes catégories sont classées par priorité décroissante. Ils ont tous une
associativité naturelle de gauche à droite.

Tableau 4.2 : les opérateurs numériques dans un contexte numérique


Remarque
Il n’existe pas d’opérateur d’élévation à la puissance. Il est nécessaire de faire appel :
• soit à des produits successifs pour des puissances entières pas trop grandes (par exemple, on
calculera x3 comme x*x*x) ;
• soit à la fonction pow de la bibliothèque standard.

2.1.2 Cas particulier de l’opérateur /


Comme pour tous les autre opérateurs, le type du résultat fourni par l’opérateur /
est défini par le type commun (après éventuelles conversions implicites) à ses
deux opérandes. Cela paraît naturel dans le cas du quotient de deux flottants,
lequel correspond tout naturellement à une valeur approchée du quotient exact.
En revanche, dans le cas du quotient de deux entiers, on obtient l’entier
correspondant à ce qu’on nomme souvent le « quotient entier », c’est-à-dire une
valeur entière approchée du quotient exact.
Par exemple, 9%5 vaut 1 (valeur approchée du quotient exact de 9 par 5, c’est-à-
dire 1,8) ; de même, 11/4 vaut 2 (valeur approchée du quotient exact de 11 par 4,
c’est-à-dire 2,75).
Le résultat de cet opérateur n’est entièrement défini que lorsque les deux valeurs
de ces deux opérandes sont non négatives : la norme prévoit alors que le résultat
soit la valeur entière approchée par défaut du quotient exact, comme dans nos
précédents exemples. En revanche, dans les autres cas, c’est-à-dire si l’un au
moins des deux opérandes est négatif, la norme laisse une latitude à
l’implémentation, celle de choisir entre valeur entière approchée par défaut ou
valeur entière approchée par excès. Par exemple :
• -9/5 vaut, suivant l’implémentation, -1 ou -2 (valeurs approchées par excès ou
par défaut du quotient exact -1,8) ;
• -11/-5 vaut, suivant l’implémentation, 2 ou 3 (valeurs approchées par défaut ou
par excès du quotient exact 2,2).
Cependant, le choix effectué par une implémentation pour l’opérateur / doit être
compatible avec un choix analogue effectué pour l’opérateur %, comme nous
allons le voir.

2.1.3 L’opérateur %
Il ne peut porter que sur des opérandes entiers et il fournit le reste de la division
entière des deux opérandes. Là encore, quand les deux opérandes sont non
négatifs, le résultat est parfaitement défini par la norme. Par exemple, 9%5 vaut
4 (reste de la division entière de 9 par 5) tandis que 11%4 vaut 3 (reste de la
division entière de 11 par 4).
En revanche, si l’un au moins des opérandes est négatif, le résultat va, comme
celui de l’opérateur /, dépendre de l’implémentation. Cependant, il faut savoir
que, dans une implémentation donnée, les résultats des opérateurs / et % sont
cohérents, ce qui signifie que si a et b désignent deux valeur entières, on a
toujours :
a = b * (a/b) + a%b

Par exemple, dans une implémentation donnée :


• si -9/5 vaut -1, alors -9%5 vaut -4 ;
• si -9/5 vaut -2, alors -9%5 vaut 1 ;
• si -11/-5 vaut 2, alors -11%-5 vaut -1 ;
• si -11/-5 vaut 3, alors -11%-5 vaut 4.

2.1.4 Priorités relatives et associativité ; cas particulier des


opérateurs commutatifs
Comme indiqué à la section 1.2, en ce qui concerne les opérateurs arithmétiques,
les choses sont naturelles et rejoignent les habitudes de l’algèbre traditionnelle.
Voici quelques exemples dans lesquels l’expression de droite, où ont été
introduites des parenthèses superflues, montre dans quel ordre s’effectuent les
calculs (les deux expressions proposées conduisent donc aux mêmes résultats) :
a + b * c a + ( b * c )
a * b + c % d ( a * b ) + ( c % d )
- c % d ( - c ) % d
- a + c % d ( - a ) + ( c % d )
- a / - b + c ( ( - a ) / ( - b ) ) + c
- a / - ( b + c ) ( - a ) / ( - ( b + c ) )

Cependant, le C fait preuve d’une légère originalité ; en effet, lorsque deux


opérateurs de même priorité sont commutatifs, la norme n’impose pas un ordre
précis des calculs, et ce malgré la règle d’associativité. Ainsi, par exemple,
l’expression :
a + b + c

pourra, suivant les cas, être évaluée comme :


a + ( b + c )

ou comme :
( a + b ) + c

Cela reste vrai même si vous placez des parenthèses dans votre expression.
Si, mathématiquement, les expressions mentionnées sont identiques, il n’en reste
pas moins que cet ordre d’évaluation a une influence sur le résultat numérique
pour au moins deux raisons :
• la précision limitée des calculs ;
• les risques de dépassement de capacité qui, suivant les valeurs concernées,
peuvent apparaître dans une des évaluations et pas nécessairement dans l’autre.
Si vous souhaitez absolument maîtriser l’ordre des calculs, il vous est possible
d’utiliser à cet effet l’opérateur unaire +. Grâce à sa priorité élevée, il supprime
certains risques de réorganisation des calculs. Ainsi, une expression telle que1 :
a + + ( b + c )

conduira au calcul de la somme b + c, avant d’ajouter la valeur de a au résultat.


Notez que :
a + ( b + c )

n’aurait pas conduit à cette certitude concernant l’ordre des calculs.

2.2 Comportement en cas d’exception


Lors de la détermination du résultat d’un opérateur arithmétique, on peut se
trouver devant ce que l’on nomme une situation d’exception, c’est-à-dire l’une
des trois possibilités suivantes :
• dépassement de capacité : résultat de calcul trop grand (en valeur absolue) pour
la capacité du type utilisé ou encore résultat négatif dans le cas d’entiers non
signés ;
• sous-dépassement de capacité : résultat de calcul trop petit (en valeur absolue)
pour la capacité du type utilisé ; cette situation ne peut se produire que pour les
types flottants ;
• tentative de division par zéro.
Comme mentionné à la section 2.1, les opérateurs arithmétiques binaires ne sont
définis que pour deux opérandes de même type, quitte à aboutir à une telle
situation par la mise en place de conversions implicites. Dans ces conditions, un
dépassement de capacité se produit dès qu’une opération possède un résultat
théorique dépassant la capacité du type commun aux deux opérandes, même si,
finalement, il existe un type plus large pouvant l’accueillir. Par exemple, la
somme de deux int pourra conduire à un dépassement de capacité, même si le
résultat peut être contenu dans un long. La même remarque s’applique au cas des
sous-dépassements de capacité. Par exemple, la somme de deux float pourra
conduire à un sous-dépassement de capacité, même si le résultat est
représentable en double ou en long double.
La norme ANSI se contente de dire que, dans ces situations, le comportement du
programme est indéterminé, excepté, comme nous le verrons, pour certaines
opérations sur des entiers non signés. En théorie, on peut donc aboutir :
• à un résultat faux ;
• à une valeur particulière servant conventionnellement à indiquer qu’un résultat
n’est plus un nombre, ou encore qu’il est infini ; c’est ce qui se produit pour
les flottants dans les implémentations qui respectent les conventions dites
« IEEE »2 ;
• à un arrêt du programme, accompagné (peut-être) d’un diagnostic d’erreur ;
• à l’exécution d’un traitement particulier fourni par le programme.
En pratique cependant, pour un type d’exception donné, bon nombre
d’environnements ont tendance à adopter des comportements semblables. Nous
examinons ici les comportements les plus répandus, en distinguant les types
entiers des types flottants ; rappelons que le cas des caractères n’a pas à être
considéré compte tenu des conversions implicites en entier qui seront mises en
place (voir section 3.3).

2.2.1 Calculs sur des entiers


Dépassement de capacité sur des entiers signés
Ce dépassement ne peut apparaître qu’en cas d’addition, de soustraction ou de
multiplication3. Le comportement du programme n’est pas imposé par la norme.
Dans la plupart des implémentations, aucun test de dépassement n’est effectué4
et, généralement, on perd les bits les plus significatifs du résultat, ce qui conduit
à un phénomène de « modulo » assez connu. D’ailleurs, bien que non portable,
cette démarche paraît naturelle à bon nombre de programmeurs et elle est même
exploitée par certains algorithmes de génération de nombres aléatoires.
Exemple
Voici un exemple obtenu dans la plupart des implémentations qui codent les
entiers sur 16 bits, suivant la représentation en complément à deux et où le
dépassement de capacité n’est pas détecté :
Exemple (théoriquement non portable) de dépassement de capacité sur des
entiers signés

#include <stdio.h>
int main()
{
int n = 32000, p =32000, q ; -1536
q = n + p;
printf ("%d", q) ;
}

Remarque
Ne confondez pas cette situation de dépassement de capacité apparaissant pendant le calcul d’une
expression, avec la tentative d’affecter à une variable une valeur non représentable dans son type (voir
section 9), même si, souvent, le comportement de l’implémentation est le même.

Dépassement de capacité sur des entiers non signés


Ce dépassement peut naturellement apparaître en cas d’addition ou de
multiplication conduisant à un résultat trop grand pour le type imparti. Mais un
problème semblable se pose dès que le premier opérande de l’opérateur - est
inférieur au second. Par simplicité, nous parlerons encore de dépassement de
capacité dans ce cas5. Contrairement à ce qui se passe pour des entiers signés, la
norme prévoit toujours le résultat d’une telle opération : il s’agit de la valeur
congrue modulo N+1 (N étant le plus grand nombre représentable) au résultat
théorique (négatif) de l’opération6.

Exemple
Voici un exemple obtenu dans une implémentation où le type unsigned int est
représenté sur 16 bits. Il est cette fois portable (en théorie, comme en pratique)
dans toutes les implémentations où le type unsigned int possède cette taille.
Exemple (portable) de dépassement de capacité sur des entiers non signés (16
bits)

#include <stdio.h>
int main()
{
unsigned int n = 64000, p = 64000, q ; 62464
q = n + p ;
printf (""%u, q) ;
}

Remarque
Même si cela n’est pas raisonnable, la norme n’interdit pas d’appliquer l’opérateur unaire - à un
nombre non signé. Dans ce cas, sauf lorsque le nombre est nul, on aboutit aussi à un dépassement de
capacité et le résultat reste défini par le même algorithme que précédemment.

Division par zéro


Le comportement du programme n’est pas imposé par la norme. En pratique, il
est rare qu’une implémentation se contente de fournir un résultat faux. En
général, on aboutit à un arrêt du programme, avec un diagnostic d’erreur. On
notera que, dans le cas de la division entière par 0, il n’existe pas de conventions
IEEE analogues à celles qui sont prévues pour les flottants.

2.2.2 Calculs sur des flottants


Dépassement de capacité
Une telle situation peut apparaître avec n’importe laquelle des opérations
arithmétiques. Là encore, le comportement du programme n’est pas imposé
par la norme. En pratique, il est rare qu’une implémentation fournisse un
résultat faux ; on rencontre l’une des deux situations suivantes :
• arrêt de l’exécution du programme, accompagné d’un message d’erreur ;
• utilisation des conventions dites IEEE consistant, ici, à utiliser des valeurs
conventionnelles pour représenter un résultat infini. Cette démarche présente
plusieurs avantages : le programme ne s’interrompt pas ; le résultat peut être
affiché par printf (on obtient généralement quelque chose ressemblant au
libellé INF ou -INF) ; le résultat peut, à son tour, intervenir dans des expressions
arithmétiques suivant des règles mathématiquement logiques. Cependant, ces
conventions ont des limites, dans la mesure où, par exemple, il est impossible
de donner une signification à +INF/+INF ; il existe d’ailleurs une notation
appropriée à ce cas : NaN (Not A Number).

Exemple
Voici un exemple exécuté dans deux environnements différents, disposant tous
les deux d’un type float ayant une capacité de l’ordre de 1038 :
Exemple de dépassement de capacité dans une multiplication flottante

#include <stdio.h>
int main() /* exécution premier environnement */
{ inf
float x ;
float y ;
x = 1e30 ; /* exécution second environnement */
y = x * x ; Floating point error : Overflow
printf ("%e", ; Abnormal program termination
}

Notez bien que c’est l’évaluation de l’expression x*x (dont le résultat est de type
float) qui a provoqué le dépassement de capacité. Si y était de type double, ce
dépassement de capacité aurait bien lieu (avec toutefois quelques exceptions,
comme indiqué dans la remarque 2 suivante).

Remarques
1. Ne confondez pas les dépassements de capacité qui apparaissent pendant le calcul d’une expression
avec la tentative d’affecter à une variable une valeur non représentable dans son type, même si,
dans certaines implémentations, les messages correspondants sont les mêmes (voir section 9).
2. En toute rigueur, la norme n’interdit pas à une implémentation d’effectuer certains calculs avec plus
de précision que nécessaire. Ainsi, dans certains cas, l’évaluation de l’expression x*x du précédent
exemple fournira un résultat de type double (et non de type float) ; c’est alors l’affectation de ce
résultat à y qui provoquera une situation d’exception. Parfois, le message sera le même qu’en cas de
dépassement de capacité, parfois il sera différent (on peut obtenir quelque chose comme « erreur de
domaine »). Qui plus est, si y est de type double, l’affectation pourra fonctionner dans certaines
implémentations et fournir un résultat correct (1.e60), contre toute attente. Cependant, on ne peut
cependant pas dire que le programme ne respecte alors pas la norme, sous prétexte qu’il fournit un
résultat juste, là où la norme dit simplement que son comportement est indéterminé !

Sous-dépassement de capacité
Une telle situation peut apparaître avec n’importe laquelle des opérations
arithmétiques. Là encore, le comportement du programme n’est pas imposé par
la norme. En pratique, on rencontre l’une des situations suivantes :
• résultat égal à 0 (n’oubliez pas que la valeur 0 est toujours représentable, de
façon exacte, en flottant) ;
• arrêt de l’exécution, accompagné d’un message d’erreur.
Comme on l’a dit dans le paragraphe précédent à propos du dépassement de
capacité, ce sous-dépassement peut se produire dans une situation telle que :
float x=1E-30 ; /* on suppose le type float limité à 1E-38 */
double y ;
y = x * x ; /* x * x est trop petit pour le type float */

Remarque
La remarque 2 précédente s’applique encore ici. Dans certaines implémentations, l’évaluation de
l’expression x*x fournira un résultat de type double (et non de type float) et l’affectation à y, de type
double, pourra fonctionner et fournir un résultat correct (1.e-60), contre toute attente.

Division par zéro


Le comportement du programme n’est pas imposé par la norme. En pratique, il
est rare qu’une implémentation se contente de fournir un résultat faux. En
général, on aboutit à l’une des situations suivantes :
• arrêt de l’exécution du programme, accompagné d’un message d’erreur ;
• utilisation des conventions IEEE consistant, ici, à utiliser des valeurs
conventionnelles pour représenter un résultat infini (dans le cas où le
numérateur n’est pas nul) ou un résultat non défini (dans le cas où le
numérateur est nul). Comme signalé précédemment à propos du dépassement
de capacité, cette démarche possède plusieurs avantages : le programme ne
s’interrompt pas ; le résultat peut être affiché par printf (ce qui conduit
généralement au libellé INF ou -INF dans le premier cas, au libellé NaN dans le
second). Le résultat peut, à son tour, intervenir dans des expressions
arithmétiques suivant des règles mathématiquement logiques.

2.2.3 Tableau récapitulatif


Le tableau 4.3 récapitule les comportements des différents opérateurs
arithmétiques, en cas d’exception. Il précise, lorsqu’il existe, le comportement
prévu par la norme et, dans le cas contraire, celui ou ceux que l’on rencontre en
pratique. Notez qu’il se limite au contexte numérique : le cas des opérateurs + et
- utilisés dans un contexte pointeur est étudié dans le chapitre relatif aux
pointeurs.

Tableau 4.3 : comportement des opérateurs arithmétiques, en cas


d’exception
3. Les conversions numériques implicites

3.1 Introduction
Aucun opérateur n’accepte d’opérandes de type char ou short. En outre, la plupart
des opérateurs binaires, en particulier les opérateurs arithmétiques étudiés dans
la section 2, ne sont définis que pour des opérandes de même type (autre que char
ou short). Cependant, le langage C leur donne une signification dans tous les cas,
en prévoyant la mise en place par le compilateur de conversions implicites.
Celles-ci sont généralement « intègres », c’est-à-dire qu’elles modifient peu la
valeur d’origine (avec, cependant, une exception dans le cas déconseillé de
mélange d’attribut de signes).
Ces conversions se classent en deux catégories :
• les conversions numériques d’ajustement de type ;
• les promotions numériques.
Les conversions numériques d’ajustement de type sont destinées à donner une
signification à un opérateur binaire lorsque ses deux opérandes sont de types
différents. Par exemple, avec :
int n ;
long q ;

l’expression n + p sera évaluée en convertissant n en long et le résultat sera de type


long.

Les promotions numériques (en anglais integral promotions7) sont destinées à


donner un sens à un opérateur (cette fois, unaire ou binaire) lorsqu’il porte sur un
ou des opérandes de type char ou short. Par exemple, avec :
short p1, p2 ;

une expression telle que p1+ p2 sera évaluée en convertissant les valeurs de p1 et
de p2 dans le type int et l’on aboutira à un résultat de type int.
Ces deux sortes de conversions pourront, naturellement, être mêlées : un même
opérande pourra être soumis à la fois à une promotion numérique et à une
conversion numérique d’ajustement de type.
D’autre part, ces conversions pourront intervenir à différentes reprises dans des
expressions comportant plusieurs opérateurs.
Ce paragraphe traite des conversions implicites les plus importantes que sont les
conversions numériques implicites. Il existe quelques autres conversions
implicites non numériques concernant les pointeurs (voir chapitre 7), les appels
de fonctions (voir chapitre 8) et la conversion d’un nom de tableau en un
pointeur (voir section 3.2 du chapitre 7)8. Par ailleurs, toutes ces conversions
implicites s’opposent aux conversions explicites, que ce soit par l’opérateur de
cast (voir section 8) ou par affectation (voir section 7). Comme nous le verrons,
contrairement aux conversions implicites, ces autres sortes de conversions ne
seront plus nécessairement intègres, c’est-à-dire qu’elles pourront modifier
notablement la valeur d’origine.
Enfin, signalons que, dans ce paragraphe, nous utilisons des exemples ne faisant
intervenir que des opérateurs arithmétiques. Il n’en reste pas moins que ces
conversions numériques implicites pourront également intervenir pour d’autres
opérateurs disposant d’un ou plusieurs opérandes numériques ; dans certains cas,
d’ailleurs, seule interviendra la promotion numérique. D’une manière générale,
lors de la présentation de chacun des opérateurs, nous préciserons, le cas
échéant, à quelles conversions peuvent être soumis ses opérandes (comme nous
l’avons déjà fait dans le tableau récapitulatif concernant les opérateurs
arithmétiques).

Remarque
On commet généralement un abus de langage en se contentant de dire, par exemple, que le
compilateur met en place telle ou telle conversion. En effet, pour être tout à fait précis, il faudrait dire
que le compilateur met en place des instructions appropriées qui, lors de leur exécution, provoqueront
telle ou telle conversion. Le compilateur ne peut procéder directement à la conversion d’une valeur
située dans une variable, valeur qui, par essence, est susceptible de varier lors de l’exécution.
On peut dire que le compilateur décide des conversions à mettre en œuvre, mais que leur déroulement
se fait pendant l’exécution du programme. Cela montre, au passage, que les conversions occupent de
la place dans le code et qu’elles prennent du temps de calcul ; éviter une conversion inutile permet de
gagner sur les deux tableaux…

3.2 Les conversions numériques d’ajustement de type


Ce sont donc des conversions que le compilateur met en place pour permettre à
un opérateur binaire de fonctionner lorsque ses deux opérandes sont de types
différents. D’une manière générale, l’idée consiste à effectuer des conversions
suivant une certaine hiérarchie préservant la valeur d’origine. C’est bien ce qui
se passe lorsque aucun opérande entier n’est non signé. En revanche, si ce n’est
pas le cas, les choses sont un peu moins satisfaisantes ; plus précisément, le cas
où aucun entier n’est signé a été traité de façon à préserver un motif binaire,
plutôt qu’une valeur. Quant au cas de mélange d’attributs de signe, on verra que
les conversions mises en œuvre sont relativement peu naturelles. Nous
étudierons donc séparément ces trois situations, résumées dans le tableau 4.4 :
• les opérandes entiers sont tous signés (il peut y avoir un opérande flottant) ; il
s’agit de la situation la plus usuelle ;
• les opérandes entiers sont tous non signés (il peut y avoir un opérande flottant,
mais en pratique ce sera rarement le cas) ;
• il y a un opérande entier signé et un opérande entier non signé (il n’y a donc
plus d’opérande flottant).

Tableau 4.4 : conversions d’ajustement de type

3.2.1 Cas d’opérandes entiers tous signés


Lorsqu’aucun opérande n’est de type entier non signé, les conversions
d’ajustement de type se font suivant la hiérarchie :
int → long → float → double → long double
Plus précisément, on peut convertir un type de cette liste en n’importe quel autre
type situé plus à droite dans la liste. Les conversions correspondantes sont
intègres ou non dégradantes, c’est-à-dire qu’elles sont censées ne pas dénaturer
la valeur d’origine. Cependant, il n’est pas certain qu’une conversion d’entier
(int ou long) vers flottant conserve exactement la valeur d’origine. Par exemple,
dans une implémentation où le type long est codé sur 32 bits et où le type float est
lui aussi codé sur 32 bits, il est certain que les grandes valeurs entières seront,
après conversion en float, légèrement erronées. On peut cependant considérer
que l’intégrité des données est vérifiée, dans la mesure où la valeur convertie est
bien celle d’origine, à la précision près inhérente au type float…
Dans le cas où les deux opérandes sont entiers, on peut montrer que, dans des
implémentations utilisant la représentation en complément à deux, ces
conversions conservent le motif binaire, à l’exception, bien entendu, d’éventuels
bits supplémentaires (à 0 ou à 1) apparaissant du côté des bits de poids forts.

Exemple
Avec :
int n = 12 ;
long p = 50000 ;

l’expression n+p sera évaluée selon ce schéma :


n + p
| |
long | conversion de n en long
| |
|__ + __| addition a p
|
long le resultat est de type long ; il vaut 50012

De même, avec :
int n = 5 ;
double x = 3.25 ;

l’expression n + x sera évaluée selon ce schéma :


n + x
| |
double | conversion de n en double
| |
|__ + __| addition a x
|
double le resultat est de type double : il vaut environ 8,25

3.2.2 Cas d’opérandes entiers tous non signés


Là encore, même si les choses sont moins usuelles, elles restent relativement
naturelles ; les conversions sont cette fois mises en place suivant la hiérarchie :
unsigned int → unsigned long → float → double → long double
Les conversions d’un entier non signé en un flottant (par nature toujours signé)
ne posent aucun problème particulier, hormis celui de la précision du résultat de
la conversion (déjà évoqué à la section 3.2.1 à propos des entiers signés).
Dans le cas où les deux opérandes sont des entiers non signés, on peut montrer
que, quelle que soit l’implémentation9, ces conversions présentent la particularité
de conserver le motif binaire, à l’exception d’éventuels bits à zéro
supplémentaires apparaissant du côté des poids forts. Ce sera d’ailleurs souvent
cette propriété qui justifiera le recours à l’utilisation d’entiers non signés. Elle
s’avérera particulièrement intéressante avec les manipulations de champs de bits.

Exemple
Avec :
unsigned int n = 12 ;
unsigned long p = 50000 ;

l’expression n+p sera évaluée selon ce shéma :


n + p
| |
unsigned long | conversion de n en unsigned long
| |
|____ + ____| addition a p
|
unsigned long le resultat est de type unsigned long ; il vaut 50012

De même, avec :
unsigned int n = 5 ;
double x = 3.25 ;

l’expression n + x sera évaluée selon ce schéma :


n + x
| |
double | conversion de n en double
| |
|__ + __| addition a x
|
double le resultat est de type double ; il vaut environ 8,25

3.2.3 Cas d’un opérande entier signé et d’un opérande entier non
signé
Rappelons que ces situations mixtes sont fortement déconseillées. En effet,
contre toute attente, la plupart des conversions prévues alors par la norme se font
de signé vers non signé. Cela signifie qu’une valeur entière signée telle que -7
devra, au bout du compte, être transformée en une valeur sans signe !
En fait, l’examen attentif des règles de conversion (au demeurant, non triviales !)
vous montrera que ce choix privilégie la conservation d’un motif binaire plutôt
que l’intégrité de la valeur ; cette dernière sera cependant respectée pour toutes
les valeurs positives ou nulles. Dans un seul but d’exhaustivité, nous allons
examiner en détail ces diverses conversions. Nous procéderons en deux étapes :
présentation des types concernés par ces conversions, algorithmes utilisés pour
définir la valeur résultante.

Les types concernés par les conversions « mixtes »


Il existe quatre situations faisant intervenir un entier signé et un entier non
signé :

Tableau 4.5 : les conversions « mixtes »

Types
Conversions prévues
opérandes
int et unsigned Conversion de l’opérande de type int dans le type
int unsigned int

long et unsigned Conversion de l’opérande de type long dans le type


long unsigned long

int et unsigned Conversion de l’opérande de type int dans le type


long unsigned long

unsigned int et Les conversions vont dépendre de l’implémentation : si


long
le type long est de taille supérieure au type int, on est sûr
qu’une valeur de type unsigned int est toujours
représentable dans le type long et l’opérande de type
unsigned int est converti en long ; le résultat est de type
long. Si, en revanche, le type long et le type int sont de
même taille, cela signifie qu’un entier non signé n’est
pas toujours représentable dans le type long ; les deux
opérandes sont convertis en unsigned long ; le résultat est
de type unsigned long. On notera bien
qu’exceptionnellement, dans ce deuxième cas, le type
du résultat ne correspond à aucun des types des deux
opérandes.

L’algorithme de conversion d’un type signé vers un type non signé


Les quatre situations citées ci-dessus correspondent en fait à cinq conversions
possibles, dont quatre se font d’un type signé vers un type non signé. La
dernière, de unsigned int en unsigned long, fait partie des conversions étudiées à la
section précédente 3.2.2, lesquelles préservent à la fois la valeur et le motif
binaire (aux bits excédentaires près).
En ce qui concerne les conversions d’un type signé en un type non signé, la
norme considère que toutes les conversions sont légales10. Dans le cas des
conversions implicites étudiées ici, il s’agit obligatoirement de conversions vers
un type d’une taille au moins égale à celle du type d’origine : si la valeur initiale
est positive ou nulle, elle est donc conservée par la conversion, ainsi d’ailleurs
que le motif binaire. En revanche, si la valeur initiale est négative, donc
manifestement non représentable dans le type d’arrivée, la conversion se fait en
ajoutant à la valeur initiale la valeur N+1, N correspondant à plus grande valeur
représentable dans le type d’arrivée.
Par exemple, dans une implémentation représentant le type int sur 16 bits, la
conversion d’un int de valeur -3 en unsigned int conduira à la valeur 65 533 (N =
65 535).
D’une manière générale, dans les implémentations, fort répandues, utilisant la
représentation en complément à deux, cette démarche revient à conserver le
motif binaire, aux bits excédentaires près (qui peuvent apparaître du côté des
poids forts lorsqu’il s’agit d’une conversion vers un type de plus grande taille).
À titre indicatif, voici un exemple de programme complet, exécuté dans une telle
implémentation (type int sur 16 bits), qui montre le caractère relativement fictif
de cette conversion signed → unsigned :
Le caractère fictif de la conversion signed → unsigned (ici, type int sur 16 bits,
en complément à deux)

#include <stdio.h>
int main()
{ int n = - 3000 ;
unsigned int p = 50000, q ; 47000
q = p + n ;
printf ("%u", q) ;
}

Remarques
1. L’algorithme de conversion signed → unsigned a été exposé ici dans le cas des conversions
implicites qui se font sans diminution de la taille. En fait, il s’agit d’un cas particulier de toutes les
conversions possibles, y compris les conversions explicites dans lesquelles il peut y avoir alors
diminution de la taille.
2. La conversion d’un type non signé vers un type signé de même taille n’est jamais mise en place de
façon implicite. En revanche, elle pourra l’être par affectation ou par l’opérateur de cast.
3.3 Les promotions numériques
Ce sont donc des conversions que le compilateur met en place de manière à
permettre à certains opérateurs de s’appliquer à des opérandes de type char ou
short. L’idée générale consiste à chercher à effectuer une conversion dans un type
de plus grande taille, tout en préservant la valeur. Là encore, les choses seront
naturelles et transparentes pour les types signés (signed char et signed short)
puisqu’on se contentera tout simplement d’une conversion dans le type int. Elles
le resteront encore pour le type unsigned char qui sera, lui aussi, converti en int. En
revanche, le cas du type unsigned short sera plus délicat car la norme a prévu deux
possibilités suivant qu’il possède ou non la même taille que le type int.
Nous examinerons donc séparément les trois situations suivantes, résumées dans
le tableau 4.6 :
• type short (équivalent de signed short) ;
• type caractère (char, signed char ou unsigned char) ;
• type unsigned short.

Tableau 4.6 : les promotions numériques

3.3.1 Cas du type short


La promotion numérique d’un opérande de type short consiste à le convertir dans
le type int. Une telle conversion est réalisable dans tous les cas en conservant la
valeur d’origine. Par exemple, avec ces déclarations :
short p = -4 ;
int n = 15 ;

l’expression p + n sera évaluée suivant ce schéma :


p + n
| |
int | promotion numerique short -> int de p ; on obtient -4
|_______+________| addition de -4 et 15 en int
|
int le resultat, de type int, vaut 11

3.3.2 Cas du type char (signé ou non)


La promotion numérique d’un opérande de type signed char, unsigned char ou char
(qui, selon l’implémentation, correspond à signed char ou unsigned char) consiste à
le convertir dans le type int ; on notera bien qu’on aboutit toujours au type int et
jamais au type unsigned int.
Une telle conversion de char en int possède des propriétés plus ou moins
satisfaisantes suivant qu’on utilise le type char pour représenter des entiers de
petite taille ou de véritables caractères.

Utilisation du type char pour représenter des entiers de petite taille


La conversion évoquée reste alors relativement naturelle :
• dans le cas de caractères signés, on obtient un nombre qui peut être positif,
négatif ou nul ; dans le cas presque universel de caractères codés sur 8 bits et
d’utilisation de la représentation en complément à deux, ce résultat est compris
entre -127 et 128 ;
• dans le cas de caractères non signés, on obtient un nombre toujours positif ou
nul ; dans le cas de caractères codés sur 8 bits, il est compris entre 0 et 255.
Malgré tout, on aura quand même intérêt à utiliser le type signed char, pour les
mêmes raisons qu’il est conseillé d’utiliser des entiers signés ; on évitera ainsi
les problèmes inhérents aux mélanges d’attribut de signe d’une même
expression.

Utilisation naturelle du type char


Si l’on utilise les types char pour représenter des caractères, une telle conversion
en int peut surprendre, mais il faut cependant remarquer que :
• la conversion n’apparaîtra que lorsqu’on cherchera à effectuer des calculs
comme dans l’expression c1 + 1 (si c1 est de type caractère) ; elle n’apparaîtra
pas dans une affectation telle que c1 = c2 (c2 étant également de type
caractère)11 ;
• le résultat d’une telle conversion est toujours positif pour les caractères
appartenant au jeu minimal d’exécution (présenté à la section 1 du chapitre 2) ;
rien d’autre ne peut être assuré pour les caractères dépendant de
l’implémentation. Cela a manifestement une incidence sur les comparaisons de
caractères ; en effet, dans le cas fréquent de la représentation en complément à
deux, tout caractère codé avec un premier bit à un apparaîtra comme négatif,
alors que les autres apparaîtront comme positifs ;
• quel que soit le code utilisé pour représenter les caractères du jeu d’exécution,
la norme impose que les codes des caractères représentant les chiffres 0 à 9
possèdent des codes consécutifs. Ainsi, avec :
char c1 = ‘2', c2 = ‘5' ;

• l’expression c2 - c1 vaudra toujours 3. En revanche, aucune autre contrainte de


ce type n’est imposée, en particulier pour les lettres de l’alphabet ! En pratique
cependant, tous les codes gardent l’ordre alphabétique pour les majuscules
d’une part, pour les minuscules (sans accent) d’autre part, même si l’on n’est
pas toujours assuré que l’écart entre les codes de deux lettres consécutives soit
égal à 1.

Exemple 1
L’expression c + 1 (c1 étant d’un type char) est évaluée ainsi :
c + 1
| |
int | promotion numerique char -> int
|___ + ___|
|
int le resultat est de type int

Voici deux exemples de programmes utilisant une telle expression, l’un dans
lequel c est signé, l’autre dans lequel il ne l’est pas ; l’implémentation code les
caractères sur 8 bits et utilise la représentation en complément à deux.
Exemple de promotion numérique signed char → int

#include <stdio.h>
int main()
{
int n ;
signed char c ;
c = ‘\xfe' ; /* ici, l'attribut de signe par défaut du type char */
/* n'intervient pas en pratique - voir section 2.4 du chapitre
3 */
n = c + 1 ;
printf ("%d", n) ;
}
-1
Exemple de promotion numérique unsigned char → int

#include <stdio.h>
int main()
{
int n;
unsigned char c ;
c = ‘\xfe' ; /* ici, l'attribut de signe par défaut du type char */
/* n'intervient pas en pratique */
n = c + 1 ;
printf ("%d", n) ;
}

255

Remarques
1. L’attribut de signe par défaut du type char peut, en théorie, influer sur la valeur de la constante
entière ‘\xfe' et donc sur le motif binaire obtenu dans c ; mais, en pratique, ce n’est pas le cas
(voir section 2.4 du chapitre 3). En revanche, si l’on écrivait directement :
n = ‘\xfe' + 1

la valeur affectée à n dépendrait de cet attribut. Par exemple, dans une implémentation utilisant la
représentation du complément à deux, on obtiendra la valeur -1 si char est signé par défaut et la
valeur 255 si char ne l’est pas.
2. Ici, nous avons affecté l’expression c+1, de type int, à une variable de type int. Mais il sera
fréquent de rencontrer des affectations à une variable de type caractère, par exemple :
c1 = c + 1 ;
c = c + 1 ; /* ou c++ */

Elles feront intervenir l’affectation d’un int à une variable de type char et, par suite, une
conversion de int en char. On peut alors montrer (voir section 9.5) que, si c est signé, tant que la
valeur de c+1 ne dépasse pas la capacité du type char, tout se passe comme si l’on avait incrémenté
de 1 le petit entier contenu dans c1.

Exemple 2
Voici un exemple exécuté dans une implémentation fort classique utilisant un
code ASCII francisé, dans lequel le caractère é possède un code supérieur à 127
et exécuté, dans une implémentation où le type char est, par défaut, signé :
Quand certains caractères (nationaux) paraissent mal ordonnés

#include <stdio.h>
int main()
{ char c1 = ‘a', c2 = ‘é' ;
if (c1 < c2) printf ("%c arrive avant %c", c1, c2) ;
else printf ("%c arrive apres %c", c1, c2) ;
}
a arrive apres é

Dans une implémentation utilisant le même codage des caractères, mais dans
laquelle le type char serait, par défaut, non signé, on aboutirait au résultat
inverse : a arrive avant é. Cette dépendance de l’attribut de signe du type char
disparaîtrait si l’on imposait le même attribut de signe (en général, non signé)
aux variables c1 et c2. Certes, une ambiguïté subsisterait encore dans une
comparaison telle que c1<'é' ; on pourrait cependant la lever en la remplaçant par
c1 < (unsigned char) ‘é'.

3.3.3 Cas du type unsigned short


La promotion numérique du type short en int ne pose pas de problème particulier
puisque toute valeur de type short peut s’exprimer dans le type int. En revanche,
le cas du type unsigned short est moins simple ; en effet, dans les implémentations
où les types short et int ont la même taille, les grandes valeurs du type unsigned
13
short ne peuvent pas s’exprimer dans le type int .

Dans ces conditions, la norme prévoit que la promotion numérique du type


unsigned short se fasse :

• dans le type int si le type int peut recevoir toutes les valeurs du type unsigned
short ;

• dans le type unsigned int dans le cas contraire.


Par exemple, avec ces déclarations :
int n ;
unsigned short q ;

l’expression n + q sera, suivant l’implémentation concernée, évaluée d’après l’un


de ces deux schémas :
n + q
| |
| int promotion numerique de unsigned short en int
|________+________|
|

int le resultat est de type int


n + q
| |
| unsigned int promotion num de unsigned short en unsigned int
unsigned int | puis conv ajust de type de int en unsigned int
|________+________|
|
unsigned int le resultat est de type unsigned int

Dans le premier cas, seule une promotion numérique intervient, alors que dans le
second cas, on trouve en plus une conversion d’ajustement de type.
Si les choses sont satisfaisantes sur le plan de la préservation de la valeur
d’origine, on constate qu’elles introduisent manifestement des aspects non
portables. En effet, le type du résultat peut être, suivant l’implémentation, signé
ou non, ce qui signifie qu’on ne maîtrise plus nécessairement le mélange
d’attributs de signe au sein d’une même expression.
Ce point peut avoir des conséquences dans une simple instruction telle que :
printf ("…", n+q) ;

puisque l’expression n+q (entière) peut être signée ou non, de sorte que le code de
format à utiliser est, selon l’implémentation, tantôt %d, tantôt %u. Il en va de même
dans les opérations de manipulation de bits dont nous parlerons à la section 6.

Remarque
On peut montrer que, quelle que soit l’implémentation, la promotion numérique d’un unsigned short
(en unsigned int ou en int) préserve toujours le motif binaire, à l’exception d’éventuels bits
supplémentaires à zéro. D’une manière générale, nous conseillons fortement de réserver le type
unsigned short aux situations où il est absolument impossible de s’en passer et, dans tous les cas,
d’éviter d’effectuer des calculs arithmétiques avec des opérandes de ce type.

3.4 Combinaisons de conversions


Jusqu’ici, nous n’avons considéré qu’un seul opérateur à la fois et chacun de ses
opérandes était soumis soit aux conversions d’ajustement de type, soit aux
promotions numériques14. D’une manière générale, ces deux sortes de
conversions peuvent être mêlées et une expression peut comporter plusieurs
opérateurs.

3.4.1 Combinaisons de conversions au niveau d’un opérateur


Les promotions numériques et les conversions d’ajustement de type peuvent être
combinées au niveau d’un même opérateur binaire. Dans ce cas, cependant, la
norme prévoit le regroupement de certaines conversions. Considérons, par
exemple :
short p ;
long q ;

A priori, on pourrait penser que l’expression p + q se trouve évaluée selon le


schéma ci-après :
p + q
| |
int | promotion numerique de short en int
| |
long | puis conversion d'ajustement de type d'int en long
|_____+_____|
|
long

En fait, la norme prévoit que la conversion de short en long se fasse directement,


sans passer par l’intermédiaire de la conversion en long, suivant ce schéma :
p + q
| |
long | conversion directe de short en long
|_____+_____|
|
long

On notera que cette démarche ne change rien au résultat ; il ne s’agit en fait que
de regrouper deux conversions consécutives en une seule.

3.4.2 Conversions d’ajustement de type avec plusieurs opérateurs


Lorsque plusieurs opérateurs apparaissent dans une expression, le choix des
conversions d’ajustement de type à mettre en œuvre est effectué en considérant
un à un les opérateurs concernés et non pas l’expression de façon globale. Par
exemple, avec ces déclarations :
int n ;
long p ;
float x ;

l’expression n * p + x sera évaluée non pas en convertissant d’abord n et p en


float, mais bel et bien selon ce schéma :

n * p + x
| | |
long | | conversion de n en long
| | |
|__ * __| | multiplication par p
| |
long | le resultat de * est de type long
| |
float | il est converti en float
| |
|____ + ____| pour etre additionne a x
|
float ce qui fournit un resultat de type float

3.4.3 Combinaisons de conversions au niveau de plusieurs


opérateurs
Les promotions numériques et les conversions d’ajustement de type peuvent être
combinées au sein d’une expression numérique. Par exemple, avec ces
déclarations :
short p1=5, p2=-2 ;
float x = 5.25 :

l’expression p1 * p2 + x est évaluée comme l’indique ce schéma :


p1 * p2 + x
| | |
int int | promotions numeriques short -> int
|____ * ____| | multiplication dans type int
| |
int |
| |
float | conversion d'ajustement de type int->float
|________+________| addition dans le type float
|
float resultat de type float (environ -4,75)

On notera bien qu’ici aucun regroupement de conversions n’est possible,


contrairement à ce qui se produisait avec l’exemple de la section 3.4.1.

3.4.4 Règles générales de conversions numériques implicites


Voici, à titre indicatif, la manière dont la norme exprime la mise en place des
conversions numériques implicites, dans le cas d’un opérateur possédant au
moins deux opérandes15 et lorsque ceux-ci sont soumis aux conversions
implicites numériques : promotions numériques et conversions d’ajustement de
type16. On y retrouve les règles présentées dans les paragraphes précédents. On
constate que les promotions numériques ne sont mentionnées qu’après les
conversions d’ajustement de type relatives aux différents types flottants ; c’est ce
qui permet de remplacer deux conversions successives par une seule conversion
comme nous l’avons vu précédemment.
• si un opérande est de type long double, l’autre est converti en long double ;
• sinon, si un opérande est de type double, l’autre est converti en double ;
• sinon, si un opérande est de type float, l’autre est converti en float ;
• sinon, si un opérande est de type unsigned long int, l’autre est converti en unsigned
long int ;

• sinon, on réalise les promotions numériques puis,


• si un des opérandes est de type unsigned long int, l’autre est converti en unsigned
long int ;

• sinon, si un opérande est de type long int et l’autre de type unsigned int, le
second est converti en long int (sauf si le type long int n’est pas « assez grand »
pour accueillir le type unsigned int, auquel cas les deux opérandes sont convertis
en unsigned long int) ;
• sinon, si un opérande est de type long int, l’autre est converti en long int ;
• sinon, si un opérande est de type unsigned int, l’autre est converti en unsigned int ;
• sinon, les deux opérandes sont de type int.

3.5 Cas particulier des arguments d’une fonction


Dans un appel de fonction, les arguments qui sont fournis sous forme
d’expression sont d’abord évalués suivant les règles habituelles relatives aux
opérateurs utilisés. Les opérandes sont soumis à d’éventuelles promotions
numériques et conversions d’ajustement de type. Ensuite, il faut distinguer deux
cas selon que le compilateur connaît ou non le type des arguments muets
correspondants.
1. Le compilateur connaît le type des arguments muets. Cela revient à dire,
comme on le verra au chapitre 8, que la fonction a fait l’objet d’une déclaration
sous la forme d’un prototype. Les valeurs des arguments sont converties dans le
type voulu, comme s’il s’agissait d’une affectation, à condition que la conversion
soit légale. Ces conversions sont présentées à la section 7 et on verra notamment
que toutes les conversions numériques sont légales, quitte à être dégradantes.
2. Le compilateur ne connaît pas le type des arguments muets. Cela peut se
produire soit lorsqu’aucun prototype n’a été mentionné (ce qui constitue un style
de programmation désuet et déconseillé), soit lorsqu’on a affaire à une fonction à
nombre d’arguments variables (comme printf). Dans ce cas, le compilateur met
en place un certain nombre de conversions implicites, à savoir :
• les promotions numériques étudiées précédemment : char et short en int, unsigned
short en int ou unsigned int ;

• une éventuelle promotion numérique supplémentaire de float en double ; cette


dernière conversion n’apparaît bien sûr que pour les expressions de type float ;
sa présence dans la norme ANSI ne se justifie que pour des raisons historiques.
Exemple 1
Considérons :
float x1, x2 ;
…..
printf ("%f", x1 + x2) ;

L’expression x1+ x2 est d’abord évaluée suivant les règles usuelles, ce qui conduit
à un résultat de type float. Comme le type de l’argument correspondant attendu
par printf n’est pas connu, ce résultat est alors soumis à la promotion numérique
de float en double, avant d’être transmis à printf.

Exemple 2
Considérons maintenant :
void f(int) ;
float x1, x2 ;
…..
f(x1+x2) ;

Ici encore, l’expression x1+ x2 est d’abord évaluée suivant les règles usuelles, ce
qui conduit à un résultat de type float. Mais cette fois, compte tenu du prototype
de f, elle est convertie dans le type int, avant d’être transmise à la fonction f.

Remarque
On voit qu’il est impossible à une fonction d’arguments de type non connu de recevoir un argument de
type char, short ou float. C’est d’ailleurs ce qui se produit pour la fonction printf dans laquelle,
par exemple :
• le code de format %c correspond à un entier et non à un caractère ;
• le code de format %f correspond à un double et non à un float.
4. Les opérateurs relationnels

4.1 Généralités
Comme tout langage, C permet de comparer des expressions à l’aide
d’opérateurs relationnels (ou « de comparaison »), comme dans :
2 * a > b + 5

Toutefois, en langage C, il n’existe pas de véritable type logique (on dit aussi
« booléen »), c’est-à-dire ne prenant que les valeurs vrai ou faux ; en fait, le
résultat d’une comparaison est un entier valant :
• 0 si le résultat de la comparaison est faux ;
• 1 si le résultat de la comparaison est vrai.
Ainsi, le résultat d’une comparaison peut, assez curieusement, intervenir dans
des calculs arithmétiques comme :
3 * (2 * a > b + 5) /* vaut soit 0, soit 3 */
(n < p) + (p < q) /* vaut 2 si n<p<q, 1 ou 0 sinon */

Par ailleurs, les expressions comparées pourront être d’un type de base
quelconque et elles seront soumises aux règles de conversions présentées dans
les paragraphes précédents. Cela signifie que la comparaison des caractères
s’effectuera après promotion numérique en int, c’est-à-dire, en définitive, suivant
la valeur du code utilisé pour les représenter ; on n’oubliera pas dans ce cas que,
comme l’indique le paragraphe 3.3.2 :
• la valeur obtenue pourra dépendre de l’attribut de signe du type caractère ;
• l’ordre alphabétique ne sera que partiellement respecté.
Enfin, le langage C permettra de comparer des pointeurs. La signification exacte
de telles comparaisons est présentée au chapitre 7.

4.2 Les six opérateurs relationnels du langage C


Le tableau 4.7 liste les six opérateurs relationnels existant en C, avec leur
signification dans un contexte numérique, c’est-à-dire avec des opérandes d’un
type de base (caractère, entier ou flottant). On notera que quatre d’entre eux sont
formés de l’association de deux caractères ; ceux-ci ne doivent, en aucun cas,
être séparés par un espace.
Tableau 4.7 : les opérateurs relationnels dans un contexte numérique

Exemple
Soit ces déclarations :
int n = 5, p = 15 ;
long q = 25 ;
float x = 5.43 ;
char ca = ‘a', cf = ‘f', cA= ‘A' ;
char cff = ‘\xFF' ; /* char signé ou non suivant l'implémentation */
/* ‘xFF' est un int ; voir section 2.4 du chapitre 3 */

Voici quelques exemples d’expressions de comparaison et la valeur


correspondante (bien entendu, ces expressions n’auront d’intérêt que si elles sont
utilisées, soit dans une expression plus complète, soit dans une instruction, en
particulier une instruction conditionnelle telle que if ou while) :
n == p /* résultat : l'entier 0 (faux) */
n <= q /* conversion d'ajustement de type de n en long */
/* résultat : l'entier 1 (vrai) */
x >= n /* conversion d'ajustement de type de n en float */
/* résultat : l'entier 1 (vrai) */
ca < cf /* promotion numérique de ca et cf en int */
/* résultat : en général l'entier 1 (vrai) car, la plupart du */
/* temps, les codes des minuscules sont ordonnés */
cA < cf /* promotion numérique de cA et cf en int */
/* résultat : entier 0 ou 1, suivant l'implémentation17 car, bien que */
/* A et f appartiennent au jeu standard d'exécution */
/* l'ordre des valeurs de leur code n'est pas imposé */
cff > 1 /* promotion num de cff en int : la valeur obtenue sera souvent */
/* -1 ou 255 suivant que char est signé ou non */
/* résultat : entier 0 ou 1, suivant l'implémentation */

Notez bien que les deux derniers exemples ne sont pas portables.

Remarque
La notation (==) de l’opérateur d’égalité est souvent la source d’erreurs plus ou moins faciles à déceler.
En effet, l’emploi par mégarde du symbole = (réservé aux affectations) à la place de == ne conduit pas
toujours à un diagnostic de compilation. Cela provient d’une part de la manière dont l’opérateur
d’affection est traité en C (il fournit une valeur), et d’autre part du fait que toute valeur numérique peut
jouer le rôle d’une valeur logique (voir section 6). Par exemple, l’instruction suivante sera toujours
légale :
if (a=b) ….

Elle exécutera la partie relative au « cas vrai » de l’instruction if, si la valeur affectée à a (donc, celle
de b) est non nulle ! Qui plus est, même l’instruction suivante aura un sens :
if (a=b & c<d) …

Elle exécutera la partie relative au cas vrai si à la fois la valeur affectée à a est non nulle et si la valeur
de c est inférieure à celle de d.

4.3 Leur priorité et leur associativité


Le tableau 4.18 de la section 13 liste les priorités et l’associativité de tous les
opérateurs. Il appelle ici quelques commentaires.
Ces six opérateurs relationnels sont moins prioritaires que n’importe quel
opérateur arithmétique. Cela permet d’éviter certaines parenthèses dans des
expressions. Ainsi :
x + y < a + 2

est équivalent à :
( x + y ) < ( a + 2 )

Les quatre premiers opérateurs (<, <=, > et >=) ont la même priorité. Les deux
derniers (== et !=) ont également la même priorité, mais celle-ci est inférieure à
celle des précédents. Ainsi, une expression telle que :
a < b == c < d

est interprétée comme :


( a < b) == (c < d)

ce qui, en C, a effectivement une signification, car les expressions a < b et c < d


sont, finalement, des quantités entières. En fait, cette expression prendra la
valeur 1 lorsque les relations a < b et c < d auront toutes les deux la même valeur,
c’est-à-dire soit lorsqu’elles seront toutes les deux vraies, soit lorsqu’elles seront
toutes les deux fausses. Elle prendra la valeur 0 dans le cas contraire. Voici
quelques exemples de valeur de cette expression pour différentes valeurs de a, b,
c et d (ici supposées entières) :

Tous ces opérateurs sont, comme la plupart des opérateurs, associatifs de gauche
à droite. Cela signifie que, si les règles de priorité et d’emploi de parenthèses ne
suffisent pas à décider de l’ordre d’application de deux opérateurs, on fait
d’abord intervenir celui de gauche (comme on le fait avec les opérateurs de
l’algèbre traditionnelle). Ainsi, une expression telle que :
a < b < c

est évaluée comme :


(a < b) < c

Autrement dit, elle vaut 1 :


• soit si a est inférieur à b (a < b vaut alors 1) et si 1 < c ;
• soit si a n’est pas inférieur à b (a < b vaut alors 0) et si 0 < c.
Quoi qu’il en soit, si l’on souhaite exprimer le fait que la valeur de b est
comprise entre a et b, on fera appel à un opérateur logique (présenté dans la
section suivante) en utilisant l’expression :
(a < b) && (c < d)

De même, une expression telle que :


a <= b > c

serait évaluée comme :


(a <= b) > c

En revanche, on notera bien que dans l’expression :


a == b < c

ce sont les priorités relatives des opérateurs < et == qui permettent de trancher et
de dire qu’elle correspond à :
a == (b < c)

Remarque
D’une manière générale, il est préférable de faire appel à des parenthèses pour rendre plus lisibles
certaines expressions douteuses. Ce conseil est d’autant plus justifié que les priorités relatives des
opérateurs relationnels et des opérateurs arithmétiques ne sont pas les mêmes dans tous les langages.
Dans cet esprit, les formes avec parenthèses de tous les exemples précédents sont préférables aux
formes équivalentes sans parenthèses.
5. Les opérateurs logiques

5.1 Généralités
Dans la plupart des langages, les opérateurs logiques servent à combiner des
expressions logiques (dont la valeur est soit vrai, soit faux) par des opérations
logiques usuelles de type et, ou et de négation. Ces possibilités se retrouvent en
langage C, mais sous une forme assez particulière.
Tout d’abord, C ne dispose pas de type logique ; en particulier, les opérateurs de
comparaison fournissent une valeur entière, ce qui impose aux opérandes des
opérateurs logiques d’être de type entier. Dans ces conditions, plutôt que de
restreindre leur valeur à 0 et 1, les concepteurs du langage ont préféré donner
une signification aux opérateurs logiques dans tous les cas, en considérant
simplement que seul 0 correspond à « faux », tandis que toute autre valeur
correspond à « vrai ».
Ils ont même souhaité élargir ce point de vue à toute valeur scalaire : une valeur
nulle étant considérée comme faux, et toute autre valeur comme vrai. Ainsi, ces
opérateurs logiques acceptent non seulement des opérandes entiers, mais aussi
des opérandes de type caractère, des opérandes flottants, ainsi que des opérandes
pointeurs. On notera que la notion de valeur nulle varie suivant le type
concerné :
• caractère de code nul pour un type caractère ; il s’agit donc ici d’un octet ayant
tous ses bits à zéro ;
• entier nul pour un type entier ; ici encore, il s’agit d’une suite d’octets nuls ;
• flottant nul : il n’y a aucune raison pour que tous les octets soient nuls ;
• pointeur nul, c’est-à-dire ayant la valeur conventionnelle NULL, laquelle ne
correspond pas obligatoirement à des octets tous nuls.
Contrairement à la plupart des autres opérateurs, les opérateurs logiques ne
prévoient aucune conversion de leurs opérandes. En revanche, il est tout a fait
légal de combiner des opérandes de types totalement différents : caractère et
flottant, entier et pointeur…

5.2 Les trois opérateurs logiques du langage C


C dispose de trois opérateurs logiques : && (et), || (ou inclusif) et ! (négation).
Comme dans beaucoup d’autres langages, il n’existe pas d’opérateur
correspondant au « ou exclusif ». On ne confondra pas && avec l’opérateur de
manipulation de bits (&), ni || avec l’opérateur de manipulation de bits (|).

Tableau 4.8 : les opérateurs logiques du langage C

Comme tout opérande nul (caractère de code 0, entier 0, flottant 0 ou pointeur


nul) est considéré comme faux et que toute autre valeur est considérée comme
vrai, le résultat de ces opérateurs peut être défini par la table suivante :

Tableau 4.9 : table de vérité des opérateurs logiques du C

Exemples usuels
Voici des exemples usuels où les opérandes de ces opérateurs sont le résultat de
comparaison (lequel est, par définition, un int de valeur 0 ou 1) :
(a<b) && (c<d)

prend la valeur 1 (vrai) si les deux expressions a<b et c<d sont toutes deux vraies
(de valeur 1), la valeur 0 (faux) dans le cas contraire.
(a<b) || (c<d)

prend la valeur 1 (vrai) si l’une au moins des deux conditions a<b et c<d est vraie
(de valeur 1), la valeur 0 (faux) dans le cas contraire.
! (a<b)

prend la valeur 1 (vrai) si la condition a<b est fausse (de valeur 0) et la valeur 0
(faux) dans le cas contraire. Cette expression est équivalente à a>=b.
Exemples moins usuels
Si n et p sont des entiers et si adr est un pointeur, voici quelques expressions
correctes, accompagnées d’une écriture équivalente plus explicite18 :
n && p /* (n != 0) && (p != 0) */
n || p /* (n != 0) || (p != 0) */
! n /* (n == 0 ) */
! adr /* (adr == NULL) */
adr && n /* (adr != NULL) && (n != 0) */

On rencontrera souvent ce genre d’écriture dans une instruction if. Ainsi :


if (!n) …

pourra apparaître comme plus concise (mais pas forcément plus lisible) que :
if ( n == 0 )

5.3 Leur priorité et leur associativité


Le tableau 4.18 de la section 13 liste les priorités et l’associativité de tous les
opérateurs. Il appelle ici quelques commentaires.
L’opérateur ! a une priorité supérieure à celle de tous les opérateurs
arithmétiques binaires et aux opérateurs relationnels. Ainsi, pour écrire la
condition contraire de :
a == b

il est nécessaire d’utiliser des parenthèses en écrivant :


! ( a == b )

En effet, l’expression :
! a == b

serait interprétée comme :


( ! a ) == b

L’opérateur || est moins prioritaire que &&. Tous deux sont de priorité inférieure
aux opérateurs arithmétiques ou relationnels. Ainsi, les expressions utilisées
comme exemples en début de section auraient pu être écrites sans parenthèses :
a<b && c<d /* équivaut à (a<b) && (c<d) */
a<b || c<d /* équivaut à (a<b) || (c<d) */

Par ailleurs, tous ces opérateurs sont, comme la plupart des opérateurs,
associatifs de gauche à droite, ce qui signifie que si les règles de priorité et de
parenthèses ne suffisent pas à décider de l’ordre d’application de deux
opérateurs, on fait d’abord intervenir celui de gauche (comme on le fait avec les
opérateurs de l’algèbre traditionnelle). Ainsi, une expression (d’usage peu
conseillé) telle que :
n && p && q

est évaluée comme :


(n && p) && q

Remarque
Ici, encore, nous vous recommandons de ne pas hésiter à faire appel à des parenthèses superflues pour
rendre plus lisibles certaines expressions douteuses. Dans les exemples précédents, les formes avec
parenthèses sont préférables aux autres formes équivalentes, avec cependant une exception (mais elle
concerne une forme de toute façon déconseillée) : l’expression n && p && q reste aussi lisible que (n
&& p) && q, d’ailleurs équivalente, ici, à n && (p && q).

5.4 Les opérandes de && et de || ne sont évalués que si


nécessaire
Les deux opérateurs && et || jouissent en C d’une propriété intéressante : leur
second opérande (celui qui figure à droite de l’opérateur) n’est évalué que si la
connaissance de sa valeur est indispensable pour décider si l’expression
correspondante est vraie ou fausse. Par exemple, dans une expression telle que :
a<b && c<d

on commence par évaluer a<b. Si le résultat est faux (0), il est inutile d’évaluer c<d
puisque, de toute façon, l’expression complète aura la valeur faux (0).
La connaissance de cette propriété est indispensable pour maîtriser des
« constructions » telles que :
if ( i<max && ( c=getchar() != ‘\n' ) )

En effet, le second opérande de l’opérateur && :


c = getchar() != ‘\n'

fait appel à la lecture d’un caractère au clavier. Celle-ci n’aura donc lieu que si la
première condition (i<max) est vraie.

Remarque
Peu d’opérateurs imposent un ordre d’évaluation à leurs opérandes. En dehors de && et de ||, on ne
trouve en effet que l’opérateur ?: étudié à la section 10.
6. Les opérateurs de manipulation de bits

6.1 Présentation des opérateurs de manipulation de


bits
Le langage C dispose d’opérateurs qui travaillent directement sur le « motif
binaire » d’une valeur et qui lui procurent ainsi des possibilités
traditionnellement réservées à la programmation en langage assembleur. Ces
opérateurs permettent de réaliser des combinaisons logiques, bit à bit (et, ou
inclusif, ou exclusif, complément à 1) ainsi que des décalages de bits.
A priori, il aurait été agréable de disposer d’un type particulier, non numérique,
permettant d’effectuer ce genre de manipulations binaires, indépendamment
d’une quelconque valeur numérique. Ce n’est pas le cas en C où ces
manipulations porteront en fait sur des opérandes de type entier. Il va de soi que
l’utilisation de types flottants n’aurait guère de signification dans ce cas, dans la
mesure où le motif binaire manipulé serait sans rapport avec la valeur
correspondante.

Tableau 4.10 : les opérateurs de manipulation de bits (opérandes entiers)


Mais, même avec des types entiers, les choses ne sont qu’à demi satisfaisantes
compte tenu des conversions implicites qui pourront modifier plus ou moins le
motif binaire ; on verra toutefois que l’utilisation systématique de types non
signés permettra de bien maîtriser la situation puisque les seules risques de
modifications se limiteront à l’éventuelle apparition de bits supplémentaires à
zéro.
Le tableau 4.10 ci-avant récapitule le rôle de ces opérateurs, qui sont ensuite
décrits en détail.

6.2 Les opérateurs « bit à bit »


6.2.1 Leur fonctionnement
Les trois opérateurs &, | et ^ appliquent en fait la même opération à chacun des
bits des deux opérandes qui seront de même taille compte tenu des éventuelles
conversions numériques mises en jeu. Leur résultat peut ainsi être défini à partir
de la table suivante qui fournit le résultat de cette opération lorsqu’on la fait
porter sur deux bits de même rang de chacun des deux opérandes :

Tableau 4.11 : table de vérité des opérateurs « bit à bit » binaires

L’opérateur unaire ~, dit de « complément à un », est également du type « bit à


bit ». Il se contente d’inverser chacun des bits de son unique opérande (0 donne
1 et 1 donne 0) :

Tableau 4.12 : table de vérité de l’opérateur « bit à bit » ~ (unaire)

Bit opérande 0 1
~ (complément à un) 1 0

6.2.2 Contraintes et conversions implicites des opérandes


Les opérandes de ces opérateurs doivent obligatoirement être d’un type entier ou
caractère, c’est-à-dire char, short, int, long avec ou sans signe, mais les
conversions numériques implicites leurs sont appliquées :
• promotions numériques seulement pour l’opérateur unaire ~ ;
• promotions numériques et conversions d’ajustement de type pour les trois
opérateurs binaires &, | et ^.
Ainsi, en définitive, les opérandes effectivement reçus par ces opérateurs seront
de l’un des types int ou long, signé ou non. Dans tous les cas, le résultat est du
type commun aux opérandes après conversion ; cela signifie notamment que si c1
et c2 sont de type char, une expression telle que c1&c2 a un sens, mais qu’elle est
de type int.
D’une manière générale, compte tenu des règles des conversions implicites, il est
préférable, lorsque l’on a aucune raison de faire autrement, de n’appliquer ces
opérateurs qu’à des opérandes non signés. Dans ce cas, en effet, on est sûr que le
motif binaire est respecté, quitte à être complété par des zéros du côté des bits les
plus significatifs. Dans le cas contraire, seules les implémentations où les entiers
sont codés suivant la représentation du complément à deux assurent cette
conservation du motif binaire (malgré tout, les bits supplémentaires peuvent,
suivant les cas, être à 0 ou à 1).
On objectera que, même en se limitant ainsi à des opérandes non signés, on peut
encore rencontrer des promotions numériques (unsigned char en int et,
éventuellement unsigned short en int) qui perturbent quelque peu le souhait de ne
travailler que sur des quantités non signées. Par exemple, avec :
unsigned short n, p, q ;

l’instruction
n = p & q ;

pourra introduire les conversions de unsigned short en int de p et de q, suivies


d’une conversion de int en unsigned short du résultat. En fait, on peut montrer que,
dans ce cas précis, cette succession de conversions ne change rien au résultat
escompté.

Exemple 1
Nous supposons que les variables n et p sont de type unsigned int et que celui-ci
est codé sur 16 bits ; nous indiquons leur valeur, à la fois sous forme binaire et
sous forme hexadécimale, ainsi que celles d’expressions simples utilisant les
opérateurs précédents.
n 0000010101101110 056E
p 0000001110110011 03B3
_________________________________________
n & p 0000000100100010 0122
n | p 0000011111111111 07FF
n ^ p 0000011011011101 06DD
~ n 1111101010010001 FA91

Exemple 2
On suppose que le type int est codé sur 16 bits et le type unsigned char sur 8 bits.

int main()
{ unsigned char c1 = ‘\x5C', c2 ; /* c1 contient le motif 01011100 */
c2 = c1 | 0x80 ; /* le second opérande est, après conversion en int */
/* 0000000010000000 */
printf ("%4x %4x", c1, c2) ;
}

5c dc /* correspond à 01011100 et 11011100 */

Le premier opérande de | est converti en int, ce qui conduit (quelle que soit
l’implémentation où le type int est codé sur 16 bits) au motif binaire
0000000001011100 ; le second opérande est déjà de type int ; le résultat fournit
par l’opérateur | est l’entier signé 0000000011011100, lequel, après conversion
en unsigned char, conduit (quelle que soit l’implémentation où les caractères sont
codés sur 8 bits) à 11011100. Autrement dit, tout se passe bien comme si
l’opérateur | avait fonctionné directement sur deux motifs binaires de 8 bits.

Remarque
De nombreux autres exemples d’utilisation des opérateurs de manipulation de bits se trouvent à la
section 6.4.

6.3 Les opérateurs de décalage


6.3.1 Leur rôle
Ils permettent de réaliser des « décalages à droite ou à gauche » sur le motif
binaire correspondant à leur premier opérande. L’amplitude du décalage,
exprimée en nombre de bits, est fournie par le second opérande. Par exemple :
n << 2

fournit comme résultat la valeur obtenue en décalant le motif binaire de n de 2


bits vers la gauche ; les 2 bits de gauche sont perdus et 2 bits à zéro apparaissent
du côté des poids faibles (à droite).
De même :
n >> 3

fournit comme résultat la valeur obtenue en décalant le motif binaire de n de 3


bits vers la droite. Cette fois, les bits de droite sont perdus, tandis que 3 bits
apparaissent du côté des poids forts (à gauche). Ces derniers dépendent du
qualificatif signed/unsigned du premier opérande. S’il s’agit de unsigned, les bits
ainsi créés à gauche sont à zéro. S’il s’agit de signed, les bits ainsi créés
dépendent de l’implémentation : sur certaines machines, ils sont identiques au bit
de signe du premier opérande (décalage arithmétique) ; sur d’autres, il s’agit de
zéros (décalage logique).
Cette dernière remarque plaide, une fois de plus, pour l’usage systématique de
types non signés comme opérandes de ces opérateurs.

6.3.2 Contraintes et conversions implicites des opérandes


D’une manière générale, les opérandes de ces deux opérateurs doivent être de
type entier (char, short, int, long avec ou sans signe). Seules les promotions
numériques sont appliquées à chacun des opérandes. En effet, contrairement à ce
qui passe avec la plupart des autres opérateurs, il n’est pas nécessaire ici que les
deux opérandes soient de même type. Par exemple, si q est de type long,
l’expression q>>3 a bien un sens. Le motif binaire à soumettre au décalage
correspond donc toujours à un int ou à un long, signé ou non.
Le résultat sera du type du premier opérande, après promotion numérique ; il
s’agit donc de int ou long, signé ou non. Par exemple, l’expression précédente q
>> 3 est bien de type long. Si c est de type char, l’expression c>>2 sera de type int,
puisque c’est dans ce type que le premier opérande c aura été converti.
On pourrait objecter que, comme dans le cas des opérateurs de manipulation de
bits, même en se limitant à un premier opérande non signé, on peut rencontrer
des promotions numériques (unsigned char en int et éventuellement unsigned short
en int) qui perturbent le souhait initial de ne travailler qu’avec des valeurs non
signées. Par exemple, avec :
unsigned short n, p ;

l’instruction :
n = p >> 3 ;

pourra introduire la conversion de p en int, suivie de la conversion du résultat en


unsigned short. Là encore, on peut montrer que cette succession de conversions
(implicites d’abord, forcées par affectation ensuite) ne change rien au résultat
escompté.

Remarque
Ces opérateurs ne modifient en aucun cas la valeur de leur premier opérande. Par exemple, pour
décaler le motif binaire d’une variable p, on utilisera une affectation de la forme :
p = p >> 3 ; /* ou p >>= 3 ; */

Exemple 1
Voici quelques exemples de résultats obtenus à l’aide de ces opérateurs de
décalage. La variable n est supposée signed int (situation déconseillée, utilisée
uniquement ici comme illustration), tandis que p est supposée unsigned int.
(signed) n 0000010101101110 1010110111011110
(unsigned) p 0000010101101110 1010110111011110
________________________________________________________________
n << 2 0001010110111000 1011011101111000
n >> 3 0000000010101101 1111010110111011
ou 000101011011101119
p >> 3 0000000010101101 0001010110111011

Exemple 2
On suppose que le type int est codé sur 16 bits et le type unsigned char sur 8 bits.

int main()
{ unsigned char c1 = ‘\x5C', c2 ; /* c1 contient le motif 01011100 */
c2 = c1 >> 2 ;
printf ("%4x %4x", c1, c2) ;
}

5c 17

Le premier opérande de >> est converti en int, ce qui conduit (quelle que soit
l’implémentation où le type int est codé sur 16 bits) au motif binaire
0000000001011100. Le résultat fournit par l’opérateur >> est l’entier
0000000000010111, lequel, après conversion en unsigned char, conduit (quelle que
soit l’implémentation où les caractères sont codés sur 8 bits) à 00010111.
Autrement dit, tout se passe comme si le décalage s’était fait directement sur un
motif de 8 bits.

6.4 Applications usuelles des opérateurs de


manipulation de bits
Les opérateurs de manipulation de bits sont très souvent utilisés pour réaliser
l’une des opérations suivantes sur le motif binaire contenu dans un entier de
taille quelconque :
• forcer à 1 ou à 0 un ou plusieurs bits de position donnée ;
• forcer à 1 ou à 0 un bit dont la position est fournie par la valeur d’une variable ;
• connaître la valeur de un ou plusieurs bits de position donnée ;
• connaître la valeur d’un bit dont la position est fournie par la valeur d’une
variable.
Dans ces différents cas, comme il a été dit précédemment, l’utilisation d’un type
non signé permet d’éviter les risques liés à d’éventuelles conversions implicites.

6.4.1 Forcer la valeur d’un ou de plusieurs bits de positions


données
On y parvient facilement en utilisant ce que l’on nomme un masque binaire,
c’est-à-dire un entier non signé dans lequel les bits ayant la même position que
les bits à forcer ont la valeur 1, les autres ayant la valeur 0.
Pour forcer à un les bits correspondant à ce masque, on combinera ce dernier par
l’opérateur | à l’entier concerné. Pour forcer à zéro les mêmes bits, on combinera
le complément à un (opérateur ~) du masque par l’opérateur & à l’entier concerné.
Exemple 1
Pour forcer à 1 les 9 bits de poids faible contenu dans n, supposée de type unsigned
int, on procédera ainsi :

n = n | 0x1FFu ; /* u pour que le masque soit non signé */

ce qui peut éventuellement s’abréger en :


n |= 0x1FFu ;

Pour forcer à 0 ces mêmes bits, on procédera ainsi :


n = n & ~0x1FFu ; /* ou de façon abrégée : n &= ~0x1FFu ; */

Les notations 0x1FFu et ~0x1FFu ont le mérite de fournir une valeur de type unsigned
int, sans qu’il soit nécessaire de connaître la taille exacte de ce type dans
l’implémentation concernée. En revanche, il n’en irait plus de même si l’on
forçait à 0 les bits voulus en fournissant directement le masque voulu, comme
dans :
n = n &FE00u ; /* valable si le type unsigned int occupe 16 bits */
Exemple 2
Pour forcer à un les trois premiers bits de poids fort et le dernier bit de poids
faible d’une variable n, de type unsigned int dans une implémentation où ce type
occupe 16 bits, on procédera ainsi :
n = n | 0xE001u ; /* ou de façon abrégée : n |= 0xE001u ; */

Pour forcer à zéro ces mêmes bits, on procédera ainsi :


n = n & ~0xE001u ; /* ou de façon abrégée : n &= ~0xE001u ; */
/* ou de façon équivalente : n &= 0x1FFEu ; */

On notera que, contrairement à ce qui produisait dans l’exemple précédent, il est


nécessaire ici de connaître la taille du type unsigned int. On peut éventuellement
contourner la difficulté en créant le masque voulu dans une variable, de cette
manière :
unsigned int masque, taille ;
…..
taille = sizeof(unsigned int) * CHAR_BIT /* inclure <limits.h> pour CHAR_BIT */
masque = 0x7u << (taille - 3) ; /* 3 premiers bits de poids fort */
masque |= 0x1u ; /* dernier bit de poids faible */

6.4.2 Forcer la valeur d’un bit de position variable


Supposons que i, de type unsigned int, contienne la position d’un bit d’un entier n,
de type unsigned int, dont on souhaite forcer la valeur à 0 ou à 1. On peut créer le
masque correspondant de cette manière :
1u<<i /* bit de rang 0 décalé i fois à gauche */

Pour forcer le bit de rang i de n à un, on pourra procéder ainsi :


n = n | (1u<<i) ; /* ou n |= (1u<<i) ; */

Pour forcer ce même bit à un, on pourra procéder ainsi :


n = n & ~(1u<<i) ; /* ou n &= ~(1u<<i) ; */

Remarque
Dans les expressions précédentes, les parenthèses ne sont théoriquement pas nécessaires, compte tenu
des priorités relatives des opérateurs concernés. Toutefois, en l’absence de parenthèses, certains
compilateurs fournissent un message d’avertissement, considérant que l’écriture est ambiguë (pour le
lecteur du programme, non pas pour le compilateur !).

6.4.3 Connaître la valeur d’un ou de plusieurs bits de positions


données
On utilise cette fois un masque dans lequel les bits ayant la position des bits à
extraire ont la valeur 1, les autres ayant la valeur 0 ; on combine ce masque par &
avec l’entier concerné. Par exemple, pour extraire dans la variable p, supposée de
type unsigned int, les trois premiers bits de poids fort et le dernier bit de poids
faible d’une variable n de type unsigned int, on procédera ainsi :
p = n & 0xE001u ;

6.4.4 Connaître la valeur d’un bit de position variable


Pour ne conserver d’une variable n, de type unsigned int, que le bit de rang i, on
pourra procéder ainsi :
p = n & (1u<<i) ;

Si l’on s’intéresse à la valeur de ce bit, sous la forme 0 ou 1, on pourra décaler


de façon appropriée le motif binaire obtenu en procédant ainsi :
p = (n & (1u<<i)) >> i ; /* (n & 1u>>i) >> i est correct mais conduit parfois */
/* à un message d'avertissement - voir section 6.4.2 */

ou encore ainsi :
taille = sizeof (unsigned int)*CHAR_BIT ; /* inclure <limits.h> pour CHAR_BIT */
p = (n << (taille-i-1)) >> (taille-1) ;
7. Les opérateurs d’affectation et d’incrémentation

7.1 Généralités
Le langage C possède la particularité de traiter l’affectation comme un opérateur.
Cela signifie qu’une notation telle que :
i = 5

est une expression qui :


• d’une part réalise une action : l’affectation de la valeur 5 à i ;
• d’autre part possède une valeur : celle de i après affectation, c’est-à-dire 5.
Manifestement, le premier opérande de cet opérateur devra être modifiable ; les
expressions :
5 = n
n + 5 = 3

n’auraient aucun sens. La notion de lvalue, présentée à la section 7.2, sert


précisément à désigner des opérandes modifiables.
Par ailleurs, il existe d’autres opérateurs d’affectation qui permettent de
condenser des écritures telles que :
n = n + 3 /* se condensera en n += 3 */
p = p *5 /* se condensera en p *= 5 */

Ces opérateurs se nommeront « opérateurs d’affectation élargie », tandis que = se


nommera « affectation simple ». Bien entendu, ces opérateurs d’affectation
élargie nécessiteront, eux aussi, une lvalue en premier opérande.
Enfin, des opérateurs unaires (à un opérande), ++ et --, dits « opérateurs
d’incrémentation », permettent, dans certains cas, de condenser encore plus
l’écriture :
n = n + 1 /* déjà condensable en n += 1 peut se condenser en n++ */
n = n - 1 /* déjà condensable en n -= 1 peut se condenser en n-- */

Ce paragraphe commencera par étudier en détail la notion de lvalue, avant


d’examiner successivement l’opérateur d’affectation simple, les opérateurs
d’affectation élargie et les opérateurs d’incrémentation.

7.2 La lvalue
Beaucoup d’opérateurs du langage C imposent à certains de leurs opérandes
d’être modifiables ; c’est notamment le cas de l’opérateur d’affectation simple
(=).
Certes, dans tout langage évolué, ce genre de contrainte existe pour l’affectation,
même lorsque cette dernière n’est pas traitée comme un opérateur, c’est-à-dire
lorsqu’elle apparaît comme une instruction à part entière. En général, on se
contente alors de dire que la partie gauche d’une telle affectation doit être une
variable. En C, ce terme de variable est insuffisant puisque :
• il est déjà employé pour un objet désigné par un identificateur figurant dans
une déclaration ;
• certaines variables ne sont pas modifiables, notamment celles ayant reçu le
qualifieur const ;
• il n’est pas adapté à des notations telles que *adr ou *(adr) dans lesquelles adr
désigne un pointeur ou une expression de type pointeur ;
• un nom de structure (variable) pourra apparaître en premier opérande d’une
affectation, un nom de tableau (variable) ne le pourra pas.
Il faut donc disposer d’un nouveau mot désignant la référence à un objet dont on
peut modifier la valeur. Nous utiliserons celui employé par la norme ANSI, à
savoir lvalue : ce mot est l’abréviation de left value, c’est-à-dire valeur à gauche,
sous-entendu à gauche d’un opérateur d’affectation (on pourrait éventuellement
franciser ce mot, en utilisant gvaleur ou encore g-valeur). En définitive :

Une lvalue est une expression désignant un objet modifiable.

On notera que, par définition, à une lvalue sont toujours associés une adresse et
un type. Par ailleurs, ce terme de lvalue s’applique à une expression désignant un
objet, non à l’objet lui-même. On ne peut pas dire si un objet est en soi, une
lvalue ou non. Il n’est d’ailleurs pas rare de pouvoir accéder à un même objet
avec des expressions différentes, l’une étant une lvalue, l’autre ne l’étant pas.
Le tableau 4.13 récapitule les différentes sortes d’expressions en précisant dans
quel cas ce sont des lvalue ; il tient compte de tous les types existants, y compris
ceux qui seront étudiés dans des chapitres ultérieurs.

Tableau 4.13 : les expressions qui sont des lvalue

Expression Conditions
Identificateur de – n’ayant pas reçu le qualifieur const ;
variable – autre que tableau ;
– dans le cas d’une variable d’un type structure ou
union, celle-ci ne doit pas comporter de champs
constants.
Expression de la p étant une variable de type pointeur sur un objet
forme *p ou *(adr) non constant, adr étant une expression d’un type
pointeur sur un objet non constant.
élément de tableau – autre que tableau (cas des tableaux à plusieurs
indices) ;
– autre que structure ou union comportant des
champs constants.
Champ de structure – autre que tableau ;
ou d’union
– autre que structure ou union comportant des
champs constants.

Remarques
1. En toute rigueur, la norme ANSI donne au mot lvalue un sens légèrement différent de celui que
nous utilisons ici, à savoir celui d’une expression désignant un objet (modifiable ou non) ; elle parle
alors de « lvalue modifiable » pour désigner ce que nous nommons, comme la plupart des ouvrages
sur le C, lvalue.
2. Contrairement à l’attribut const, l’attribut volatile n’empêche nullement à une variable d’être une
lvalue. On pourrait même dire qu’une telle variable est encore plus qu’une lvalue puisqu’elle peut
être modifiée par le programme, même à son insu !
3. Un nom de tableau n’est pas une lvalue. Il s’agit certes d’une grande originalité du langage
puisque, par convention, un nom de tableau désigne un pointeur constant sur son premier élément.
Mais il s’agit également d’une grande lacune puisqu’on ne peut pas réaliser d’affectations entre
tableaux ou transmettre les valeurs d’un tableau à une fonction. Qui plus est, ces opérations seront
réalisables avec les autres agrégats que sont les structures.

7.3 L’opérateur d’affectation simple


7.3.1 Rôle, priorité et associativité
Cet opérateur binaire se présente sous la forme :
lvalue = expression
Son rôle est double : d’une part, il affecte à son premier opérande qui doit, bien
sûr, être une lvalue, la valeur de son deuxième opérande ; d’autre part, il fournit
comme résultat, la valeur de son premier opérande après modification (ou, ce qui
revient au même, la valeur de son deuxième opérande).
Sa faible priorité (elle est inférieure à celle de tous les opérateurs arithmétiques
et de comparaison) fait qu’on peut l’utiliser de façon naturelle, sans recourir à
des parenthèses. Par exemple, dans :
c = b + 3

il y a d’abord évaluation de l’expression b + 3. La valeur ainsi obtenue est ensuite


affectée à c. Il n’est pas nécessaire d’écrire (mais ce serait correct) :
c = (b + 3)

Contrairement à la plupart des autres, cet opérateur d’affectation possède une


associativité de droite à gauche. C’est ce qui permet à une expression telle que :
i = j = 5

d’évaluer d’abord l’expression j = 5 avant d’en affecter la valeur (5) à la variable


j. Bien entendu, la valeur finale de cette expression est celle de i après
affectation, c’est-à-dire 5.

7.3.2 Conversions liées aux affectations


Contrairement à ce qui se passe avec la plupart des autres opérateurs, l’opérateur
d’affectation utilise son premier opérande, non pas pour sa valeur, mais pour son
aspect lvalue : il désigne un emplacement dont on doit modifier la valeur. Une
conversion d’un tel opérande n’aurait donc aucun sens.
En ce qui concerne l’expression apparaissant en deuxième opérande, son type est
en partie conditionné par celui du premier opérande. En effet, comme on peut s’y
attendre, il n’est pas possible d’affecter n’importe quoi à n’importe quoi : il
n’aurait aucun sens d’affecter la valeur d’un entier à un pointeur ou encore la
valeur d’une structure à un flottant. Néanmoins, fidèle à ses habitudes, le
langage C n’impose pas pour autant une concordance absolue de type. Plus
précisément, certaines conversions seront possibles au moment de l’affectation :
• dans le cas où les deux opérandes sont de type numérique ;
• dans le cas où au moins le premier opérande est de type pointeur (ces
possibilités, au demeurant, assez restreintes, seront étudiées au chapitre 7).
Dans le cas des opérandes numériques, la valeur de l’expression est bien sûr
évaluée suivant les règles habituelles, avec d’éventuelles conversions implicites
(promotions numériques et conversions d’ajustement de type). Si le type du
résultat ainsi obtenu ne correspond pas à la lvalue, il y a conversion systématique
dans le type de la lvalue, avant affectation. Une telle conversion imposée ne
respecte plus nécessairement la hiérarchie des types qui est de rigueur dans le
cas des conversions implicites. Elle peut donc conduire, selon les cas, à une
dégradation plus ou moins importante de l’information : par exemple lorsque
l’on convertit un double en int, on perd la « partie décimale » du nombre.
De telles conversions interviennent dans des affectations aussi banales que les
suivantes :
int n ;
long p ;
char c1, c2 ;
n = p + 5 ; /* valeur de n correcte si p+5 est représentable en int */
p = n - 3 ; /* n-3 évalué en int, le résultat est converti en long */
c1 = c2 + 1 ; /* c2+1 évalué en int, le résultat est converti en char */

D’une manière générale, le rôle exact de ces différentes conversions numériques


est étudié à la section 9.

7.3.3 Affectation et qualifieurs


Tout naturellement, le premier opérande d’une affectation ne peut pas posséder
le qualifieur const, lequel, par définition, interdit toute tentative de modification.
Par exemple :
const int n = 5 ;
int p ;
…..
p = n + 5 ; /* OK */
n = p + 3 ; /* interdit : n n'est pas une lvalue */

En revanche, le qualifieur volatile, par sa nature même, n’entraîne aucune


interdiction de ce genre.
La même remarque s’appliquera à des variables de type pointeur. Il faudra
cependant bien distinguer les qualifieurs appliqués à la variable elle-même de
ceux appliqués à l’objet pointé. Nous y reviendrons en détail au chapitre 7.

7.4 Tableau récapitulatif : l’opérateur d’affectation


simple
Le tableau 4.14 tient compte de tous les types d’opérandes qu’est susceptible de
recevoir l’opérateur d’affectation simple. Cependant, en dehors de l’aspect
numérique traité dans ce chapitre, il faudra se reporter aux chapitres
correspondants (pointeurs, structures, unions, énumérations) pour trouver la
signification exacte des autres affectations et des éventuelles conversions
correspondantes.

Tableau 4.14 : les opérandes de l’opérateur d’affectation simple

Opérande
Opérande de droite Remarques
de gauche
de
lvalue Expression d’un type numérique Possibilités
type quelconque de
numérique conversions
dégradantes
de
lvalue Expression du même type structure Voir
type chapitre 11
structure
de
lvalue Expression du même type union Voir
type union chapitre 11
de
lvalue
Une des deux possibilités suivantes : Voir section
type 6 du
pointeur – expression du même type pointeur ou de chapitre 7
sur un type void *, la lvalue concernée devant,
objet, autre dans tous les cas, posséder au moins les
que void * mêmes qualifieurs const ou volatile que le
type des objets pointés ;
– NULL ou entier 0.
de
lvalue
Une des deux possibilités suivantes :
type void *
– expression d’un type pointeur quelconque,
y compris void *, la lvalue concernée
devant, dans tous les cas, posséder au moins
les mêmes qualifieurs const ou volatile que
le type des objets pointés ;
– NULL ou entier 0.
de
lvalue Valeur d’un type compatible au sens de la Voir section
type redéclaration des fonctions 11.2 du
pointeur chapitre 8
sur une
fonction

7.5 Les opérateurs d’affectation élargie


L’intérêt de ces différents opérateurs réside uniquement dans la possibilité qu’ils
offrent de condenser certaines affectations. En voici quelques exemples montrant
en parallèle l’affectation simple et l’affectation élargie :

Utilisation équivalente de
Utilisation de l’affectation simple
l’affectation élargie
i = i + k; i += k;
i = i + 2*n; i += 2*n;
a = a * b; a *= b;
a = a * (b+3); a *= b+3;
n = n << 3; n <<= 3;

D’une manière générale, C permet de condenser des affectations de la forme


suivante, dans laquelle la mention lvalue désigne obligatoirement la même lvalue :
lvalue = lvalue operateur expression

en :
lvalue operateur= expression

Cette possibilité concerne tous les opérateurs binaires arithmétiques et de


manipulation de bits, ce qui conduit aux opérateurs +=, -=, *=, /=, %=, |=, ^=, &=, <<= et
>>=. Tous ces opérateurs disposent de la même priorité et de la même
associativité que l’opérateur d’affectation simple. Les contraintes pesant sur
leurs opérandes découlent directement des contraintes affectant à la fois
l’opérateur d’affectation simple et l’opérateur binaire correspondant (par
exemple, + pour +=). De la même manière, tous ces opérateurs peuvent provoquer
une conversion (éventuellement dégradante), au même titre que l’opérateur
d’affectation.

Tableau 4.15 : les opérateurs d’affectation élargie

Opérateurs Opérande de gauche Opérande de droite


+= -=
lvalue de type Expression numérique
numérique Expression numérique
lvalue de type pointeur entière
*= /=
lvalue de type Expression numérique
numérique
%=
lvalue de type entier Expression entière
&= |= ~=
lvalue de type entier Expression entière
<<= >>=
lvalue de type entier Expression entière

Remarque
Si n est de type int et x de type float, une expression telle que :
n += x

est légale, même si un tel mélange de types est rarement utilisé ; elle est rigoureusement équivalente
à :
n = n + x

ce qui signifie que la valeur de l’expression n + x est d’abord évaluée : n est converti en float et le
résultat de l’addition est de type float. Il est ensuite converti dans le type de la lvalue réceptrice,
c’est-à-dire ici en int.
Le même genre de commentaires s’appliquerait à des expressions telles que c %= 5 ou c += 5 (c étant
de type char).
8. Les opérateurs de cast

8.1 Généralités
Il existe deux circonstances dans lesquelles le compilateur met automatiquement
en place des conversions, sans que le programme ne l’ait explicitement
demandé :
• Dans les expressions numériques : il s’agit de promotions numériques et de
conversions d’ajustement de type. C es conversions (voir section 3) sont
généralement intègres, c’est-à-dire qu’elles préservent la valeur d’origine, les
quelques rares exceptions concernant uniquement des situations de mélange
d’attributs de signe.
• Dans certaines affectations (voir section 7) : cette fois, toutes les conversions
numériques sont autorisées, ce qui signifie qu’elles peuvent, éventuellement,
être dégradantes. Il existe quelques autres conversions concernant les
pointeurs.
En outre, il est possible de provoquer explicitement une conversion en utilisant
un opérateur dit de « cast » dont le nom est formé à l’aide de ce que l’on nomme
le « nom » du type voulu. Par exemple, si n et p sont des variables entières,
l’expression :
(double) ( n/p )

aura comme valeur celle de l’expression entière n/p convertie en double. La


notation :
(double)

correspond en fait à un opérateur unaire dont le rôle est d’effectuer la conversion


dans le type double de l’expression le suivant. Notez bien que cet opérateur
(unaire) force la conversion du résultat de l’expression et non celle des
différentes valeurs qui concourent à son évaluation. Autrement dit, ici, il y a
d’abord calcul, dans le type int, du quotient de n par p ; c’est seulement ensuite
que le résultat sera converti en double. Si n vaut 10 et que p vaut 3, cette
expression aura comme valeur 3.
Comme on peut s’y attendre, il n’est pas possible de convertir n’importe quoi en
n’importe quoi. Par exemple, il serait absurde de vouloir convertir un pointeur
sur une fonction en un pointeur sur un entier ou encore un entier en une
structure.
Les seules conversions possibles se limitent en fait aux conversions d’un type
scalaire (numérique ou pointeur) vers un autre type scalaire. Bien entendu, qui
peut le plus peut le moins et toutes les conversions autorisées lors d’une
affectation sont réalisables par cast. La réciproque n’est pas vraie et certaines
conversions réalisables par cast ne sont pas autorisées par affectation.

8.2 Les opérateurs de cast


8.2.1 Syntaxe et rôle
Les opérateurs de cast

(nom_de_type)
nom_de_type
Nom d’un type – nom de type de base = spécificateur de
scalaire (type type éventuellement précédé de
de base ou qualifieurs (voir commentaires à la
pointeur), avec section 8.2.3) ;
d’éventuels
qualifieurs – nom de type pointeur présenté à la
dans le cas section 2.6 du chapitre 7.
d’objets
pointés.

On voit qu’un opérateur de cast se note sous la forme du nom du type placé entre
parenthèses. La notion de nom de type est différente de celle de spécificateur de
type. Dans le cas d’un type de base, elle correspond effectivement à ce
spécificateur de type, éventuellement précédé de qualifieurs, comme nous le
verrons à la section 8.2.3. En revanche, dans le cas des pointeurs, elle fait
également intervenir le déclarateur correspondant, comme nous le verrons à la
section 2.6 du chapitre 7.
Le rôle de l’opérateur de cast dans le cas des types de base est présenté à la
section 9 qui expose le déroulement exact des différentes conversions qu’elles
soient ou non dégradantes. Pour ce qui concerne les pointeurs, on se reportera à
la section 9 du chapitre 7.
Exemples
Voici quelques notations équivalentes, avec des types de base :
(long int) n (long) n (signed long) n
(int) c (signed int) c (signed) c
(short) n (signed short) n (short int) n (signed short int) n
(unsigned) c (unsigned int) c

Voici un exemple de cast servant à expliciter le rôle d’une affectation :


int n ;
float x ;

n = (int) x ; /* équivaut à n = x; */
n = (int) (x + 2.3) ; /* équivaut à n = x + 2.3; */

Remarque
D’une manière générale, le nom d’un type n’est utilisé que dans quelques circonstances : opérateur de
cast, opérateur sizeof et prototypes de fonctions. Les deux derniers cas pourront alors faire intervenir
des noms de types non scalaires qui seront présentés dans les chapitres correspondants.

8.2.2 Priorité des opérateurs de cast


La priorité élevée de l’opérateur de cast (voir tableau 4.18, section 13) fait qu’il
est généralement nécessaire de placer entre parenthèses l’expression concernée.
Ainsi, l’expression :
(double) n/p

conduirait d’abord à convertir n en double ; les règles de conversions implicites


amèneraient alors à convertir p en double avant que n’ait lieu la division (en
double). Le résultat serait alors différent de celui obtenu par l’expression proposée
au début de ce paragraphe (avec les mêmes valeurs de n et de p, on obtiendrait
une valeur de l’ordre de 3.33333….).

8.2.3 Opérateurs de cast et qualifieurs


Comme nous le verrons à la section 9du chapitre 7, le rôle d’un qualifieur est
parfaitement clair dans le cas des objets pointés et il fait, en quelque sorte, partie
intégrante du type, donc du nom de type. En revanche, la norme ne précise pas si
un tel qualifieur fait ou non partie du nom de type dans le cas où il porte sur une
variable d’un type de base ou sur la variable pointeur elle-même. En fait, ce
point est de peu d’importance, dans la mesure où, de toute façon, ce qualifieur ne
sert à rien dans le nom de type et en particulier dans le cas de l’opérateur de cast.
Par exemple, avec :
volatile int p ;
int n ;

la plupart des implémentations acceptent :


p = (volatile int) n ; /* accepté dans la plupart des implémentations */
mais ceci peut être avantageusement remplacé par :
p = n ;

puisque la notion de volatilité d’une expression n’a, en soi, aucun sens.


D’une manière comparable, mais plus criante, avec :
const int p = 5 ;
int n ;

la plupart des implémentations acceptent une expression de la forme :


(const int) n

mais celle-ci n’a aucune signification puisque la notion de constance d’une


expression n’a pas de sens. De toute façon, une tentative d’affectation de la
forme suivante sera rejetée :
p = (const int) n ; /* rejeté : p n'est pas une lvalue */

En effet, étant constante, p ne peut être modifiée, même en cherchant


sournoisement à lui affecter quelque chose de constant…

Remarque
En fait, cette tolérance des qualifieurs au premier niveau de l’opérateur de cast pourra s’avérer utile
en cas de définition de types synonymes par typedef. En voici un exemple :
typedef volatile int t_vi ;
t_vi n, q ;
…..
q = (t_vi) n ; /* la présence de volatile dans le type t_vi ne gêne pas */
9. Le rôle des conversions numériques
On appelle conversion numérique la conversion d’un type de base en un autre
type de base. Une telle conversion peut être :
• implicite, dans l’évaluation d’une expression : promotion numérique ou
conversion d’ajustement de type (voir section 3) ;
• forcée par une affectation, comme indiqué dans la section 7 ou par l’opérateur
de cast (voir section 8).
Les conversions implicites sont intègres, c’est-à-dire qu’elles préservent la
valeur initiale, excepté en cas de mélange d’attribut de signe. Elles constituent
un cas particulier de conversions forcées qui, quant à elles, ne sont plus
nécessairement intègres. Notamment, on y trouvera des conversions d’un type
entier/flottant vers un type entier/flottant plus petit ou des conversions de flottant
vers entier20. Qui plus est, certaines de ces conversions, bien qu’acceptées par le
compilateur, pourront, lors de l’exécution, conduire à des situations d’exception
pour lesquelles la norme n’a rien prévu !
Nous allons examiner en détail ces différentes situations de conversion. Elles
feront ensuite l’objet d’un tableau récapitulatif.

9.1 Conversion d’un type flottant vers un autre type


flottant
C’est le cas lorsque les deux types concernés sont choisis parmi les types long
double, double ou float.

Si le type destination est de taille supérieure au type d’origine, aucun problème


particulier ne se pose ; la norme dit simplement que la valeur obtenue doit être
identique21 à la valeur initiale. Cette situation correspond à toutes les conversions
d’ajustement de type.
Si le type destination est de taille inférieure au type d’origine, deux situations
doivent être distinguées :
• si la valeur initiale appartient au domaine correspondant au nouveau type, le
résultat sera la valeur la plus proche de la valeur d’origine, par défaut ou par
excès suivant l’implémentation. Là encore, aucun problème ne se pose, hormis
une perte (normale !) de précision ;
• dans le cas contraire, la norme prévoit que le comportement du programme
est indéterminé. Dans certaines implémentations, on obtiendra alors un
message d’erreur lors de l’exécution : dépassement de capacité, sous-
dépassement de capacité, erreur de domaine…. Dans d’autres, on obtiendra
simplement un résultat sans signification. Dans les implémentations utilisant
les conventions IEEE 754, on obtiendra l’une des deux valeurs +INF ou -INF.

9.2 Conversion d’un type flottant vers un type entier


C’est le cas des conversions de float, double ou long double vers long int, int, short
int ou char (d’attribut de signe quelconque). Rappelons qu’aucune de ces
conversions ne peut apparaître de façon implicite.
Si la valeur initiale ne sort pas des limites correspondant au nouveau type, la
norme prévoit que le résultat de la conversion s’obtient en privant le nombre
flottant de sa partie décimale.
Dans le cas contraire, la norme se contente de dire que le résultat n’est pas
défini. Là encore, seules certaines implémentations fourniront un message
d’erreur à l’exécution. Notez bien que la conversion d’un flottant négatif
(comme -3.36) en un entier non signé est, en principe, un cas d’erreur. Les
implémentations utilisant les conventions IEEE n’apportent rien de particulier à
ce niveau puisque ces conventions ne concernent que les représentations des
nombres flottants.

Remarque
Dans les conversions légales de flottant en entier, on ne perdra pas de vue la précision limitée de la
représentation des flottants. Considérez, par exemple, ces instructions :
float x ;
long q ;
float eps = 0.1 /* valeur généralement représentée en float avec une erreur */
x = eps * 1E10 ;
q = x ; /* q ne vaut peut-être pas exactement 1000000000 */

9.3 Conversion d’un type entier vers un type flottant


Ces conversions font déjà partie des conversions d’ajustement de type étudiées à
la section 3.2.1, dont nous avons vu qu’elles sont intègres. Rappelons
simplement que :
• si la valeur d’origine est représentable dans le type d’arrivée, aucun problème
ne se pose ;
• si la valeur d’origine n’est pas représentable de façon exacte, elle l’est
obligatoirement d’une manière approchée (car, dans tous les cas, elle
appartient au domaine du type d’arrivée) et le résultat sera la valeur la plus
proche (par excès ou par défaut, suivant l’implémentation).

9.4 Conversion d’un type entier vers un autre type


entier
C’est le cas lorsque les deux types concernés sont choisis parmi les types long,
int, short et char, avec leurs attributs éventuels de signe (ne pas oublier que char
est, selon l’implémentation, équivalent à signed char ou à unsigned char !). Il faut
distinguer le cas où la valeur est représentable dans le type destination de celui
où elle ne l’est pas.

Cas A : la valeur initiale est représentable dans le type destination


On notera bien qu’il n’est pas toujours suffisant que le type destination soit de
taille supérieure ou égale au type initial pour que cette condition soit réalisée. En
effet, lorsque le type destination est non signé, il faut de surcroît que la valeur
initiale soit non négative. De même, avec un type destination signé et une valeur
initiale de type non signé de même taille, il est nécessaire que la valeur initiale
ne soit pas trop grande.
Si la condition évoquée est vérifiée, aucun problème particulier se ne pose pour
la valeur qui est, tout naturellement, conservée. Le motif binaire est conservé
pour ce qui est des bits communs aux deux types. Des bits supplémentaires
peuvent apparaître du côté des poids forts si le type destination est plus grand
que le type d’origine. Avec la représentation en complément à deux, ces bits sont
à 0 pour des valeurs positives et à 1 pour des valeurs négatives. Dans les autres
(rares) cas, on ne peut rien affirmer.

Cas B : la valeur initiale n’est pas représentable dans le type destination


Dans ce cas, la norme distingue à nouveau deux cas :

Cas B1
Si le type destination est signé, le résultat22 n’est théoriquement pas défini. La
plupart des implémentations se contentent cependant de fournir un résultat faux,
obtenu en conservant le motif binaire, en ignorant éventuellement les bits les
plus significatifs ou en introduisant des bits supplémentaires. Ces bits sont à 0
pour des valeurs positives et à 1 pour des valeurs négatives dans le cas de la
représentation en complément à deux.
Par exemple, si n est de type int, dans une implémentation où ce type est
représenté sur 16 bits, l’instruction :
n = 0xFFFFu ;

affecte à n une valeur de type unsigned int, manifestement trop grande pour son
type. Dans la plupart des implémentations utilisant la représentation en
complément à deux, on obtiendra en fait la valeur -1 dans n (même si cela n’est
nullement imposé par la norme dans ce cas).

Cas B2
Si le type destination est non signé, le résultat de la conversion est défini par une
formule de modulo généralisant celle déjà rencontrée à la section 3.2.3 pour les
conversions d’ajustement de type. Plus précisément, si n représente la valeur
initiale et nmax le plus grand nombre représentable dans le type destination (mod
désignant l’opérateur mathématique modulo), le résultat de la conversion sera :
n mod (nmax + 1)

Par exemple, la conversion d’un int de valeur -3 en unsigned int conduira


toujours, dans une implémentation où ces types sont représentés sur 16 bits, à la
valeur 65 533 (nmax = 65535, et -3 mod 65536 = 65533).
On peut montrer que, dans les implémentations utilisant la représentation en
complément à deux, cette démarche revient à conserver le motif binaire, en
ignorant les bits les plus significatifs.

9.5 Cas particuliers des conversions d’entier vers


caractère
Le type caractère n’étant qu’un type entier particulier, ces possibilités sont déjà
décrites dans les paragraphes précédents. Toutefois, compte tenu de la dualité
(numérique, caractère) des types char et de l’usage important qui est fait de ces
conversions, nous examinons ici quelques exemples.
Exemple 1
int n = 25, q ;
char c ;
…..
c = n ; /* conversion de n en char et affectation a c */
q = c ; /* on retrouve bien la valeur 25 dans q */
Bien entendu, la conversion de char en int induite par l’affectation c=n est
acceptée par le compilateur. À l’exécution, la valeur de n (ici, 25) est
représentable dans le type char (qu’il soit ou non signé) ; aucun problème ne se
pose. L’affectation q=c entraîne la conversion non dégradante de la valeur 25 dans
le type int. En définitive, q reçoit bien la valeur 25.

Exemple 2
Considérons maintenant ces instructions analogues aux précédentes, avec cette
différence que n est initialisé à 200 :
int n = 200, q ;
char c ;
…..
c = n ; /* conversion de n en char : */
/* si char signé -> 200 non représentable */
/* si char non signé -> 200 est représentable */
q = c ; /* valeur dépendant de l'attribut de signe de char */
/* si char non signé : toujours 200 */
/* si char signé : souvent -56 */

Là encore, la première affectation est acceptée par le compilateur. Lors de


l’exécution, il faut distinguer deux cas :
• si le type char est par défaut signé, la valeur 200 n’est alors pas représentable
dans ce type ; le résultat de la conversion n’est pas défini par la norme mais, en
pratique, on conservera le motif binaire, ce qui conduira la plupart du temps à
un bit de gauche (de l’octet représentant le char) à un. Lors de l’affectation à q,
ce bit de gauche sera (généralement) interprété comme un bit de signe et le
résultat de la conversion de char en int sera négatif. La valeur de q sera donc
négative ;
• si, en revanche, le type char est par défaut non signé, la valeur 200 sera
représentable dans ce type ; aucun problème ne se pose et q recevra bien la
valeur 200.
Exemple 3
signed char c1 = ‘a' ;
unsigned char c2 ;
…..
c2 = c1 ; /* a appartient au jeu minimum d'exécution ; il est représentable */
/* en unsigned int ; le motif binaire et la valeur sont conservés */

Exemple 4
unsigned char c1 = 200 ;
signed char c2 ;
…..
c2 = c1 ; /* 200 n'est pas représentable en signed char ; en théorie, le */
/* résultat est indéterminé ; en pratique, le motif binaire */
/* est conservé */

Exemple 5
signed char c1 = -20 ;
signed char c2 ;
…..
c2 = c1 + 1 ; /* c1 est converti en int, avant d'être ajouté à 1 ; le résultat */
/* -19, de type int, est converti en signed char, ce qui ne */
/* change pas sa valeur */

D’une manière générale, le type signed char est utilisable comme un petit entier,
exactement au même titre que signed short. En revanche, les mélanges d’attributs
de signe posent les mêmes problèmes :
signed char c1 = -6 ;
unsigned char c2 ;
…..
c2 = c1 + 1 ; /* c1 est converti en int, avant d'être ajouté à 1 ; le résultat */
/* -5 est converti en unsigned char, ce qui change sa valeur */

9.6 Tableau récapitulatif des conversions numériques


Le tableau 4.16 récapitule le rôle des différentes conversions légales d’un type
numérique en un autre type numérique. On notera que les types caractère ne sont
rien d’autre que des cas particuliers de types entier. Ainsi, ce que nous nommons
conversion flottant → entier correspond aussi bien à une conversion float → int
qu’à une conversion double → char. De même, ce que nous nommons conversion
entier → entier non signé peut correspondre aussi bien à une conversion int →
unsigned char qu’à une conversion signed char → unsigned long.

Tableau 4.16 : les conversions numériques légales (dégradantes ou non)


Voici ce que deviennent ces règles appliquées à des conversions d’un type
caractère vers un autre type caractère. Ce tableau n’est fourni qu’à titre indicatif
dans la mesure où, comme nous l’avons dit à plusieurs reprises, en pratique, le
motif binaire est toujours conservé dans ce cas.


signed char
– valeur conservée pour les codes positifs donc, en
unsigned char
particulier, pour les caractères du jeu standard ; valeur
définie par modulo pour les codes négatifs ;
– motif binaire : d’après la norme, conservé avec la
représentation en complément à deux ; en pratique,
toujours conservé.

unsigned char
– d’après la norme : valeur et motif binaire conservés
signed char
uniquement pour les codes pas trop grands et, en
particulier, pour les caractères du jeu standard ;
indéfinis dans les autres cas ;
– en pratique, le motif binaire est toujours conservé.
Toujours à titre indicatif, voici l’effet théorique d’une suite de trois conversions,
partant d’un type char, pour aboutir à un type char, par l’intermédiaire d’un type
int (signé) :

→ int → signed char


signed char Valeur et motif binaire conservés
unsigned char → int → unsigned char Valeur et motif binaire conservés
→ int → unsigned char
signed char Équivaut à signed char → unsigned
char (voir précédemment)

unsigned char → int → signed char Équivaut à unsigned char → signed


char (voir précédemment)

Notez que la troisième suite de conversions intervient dans des situations aussi
banales que :
printf ("%c", c) ; /* c étant de type signed char ou de type char */
/* dans une implémentation ou ce type est signé par défaut */
10. L’opérateur conditionnel

10.1 Introduction
Considérons cette instruction :
if ( a>b )
max = a ;
else
max = b ;

Elle attribue à la variable max la plus grande des deux valeurs de a et de b. La


valeur de max pourrait être définie par : si a>b alors a sinon b. En langage C,
l’opérateur conditionnel permet de traduire presque littéralement cette condition
de la manière suivante :
max = a>b ? a : b

L’expression figurant ici à droite de l’opérateur d’affectation est en fait


constituée de trois expressions (a>b, a et b) qui sont les trois opérandes de
l’opérateur conditionnel, lequel se matérialise par les deux symboles séparés ? et
:.

Voici un autre exemple d’une expression utilisant l’opérateur conditionnel pour


placer dans la variable va la valeur absolue de l’expression 3*a + 1 :
va = 3*a+1 > 0 ? 3*a+1 : -3*a-1

10.2 Rôle de l’opérateur conditionnel


Cet opérateur évalue la première expression qui joue le rôle d’une condition.
Comme toujours en C, celle-ci peut être en fait de n’importe quel type scalaire
(type de base ou pointeur). Si sa valeur est différente de zéro, il y a évaluation du
second opérande, ce qui fournit le résultat. En revanche, si sa valeur est nulle, il
y a évaluation du troisième opérande, ce qui fournit le résultat.
Rien n’empêche que l’expression conditionnelle soit évaluée sans que sa valeur
ne soit utilisée, comme dans cette instruction :
a>b ? i++ : i-- ;

Ici, suivant que la condition a>b est vraie ou fausse, on incrémentera ou on


décrémentera la variable i.
Bien entendu, une expression conditionnelle peut, comme toute expression,
apparaître à son tour dans une expression plus complexe :
z = z * ( a>b ? a : b ) ;

10.3 Contraintes et conversions


Le premier opérande doit être d’un type scalaire (il faut bien pouvoir
l’interpréter comme une condition) ; il est évalué, sans qu’aucune conversion ne
soit nécessaire. En ce qui concerne les deux derniers opérandes et le type du
résultat, ils sont définis par le tableau 4.17 :

Tableau 4.17 : l’opérateur conditionnel

Remarque
Les deux derniers opérandes ne peuvent jamais être tous les deux nuls. En revanche, ils peuvent être
tous les deux de type void * (ce cas correspond simplement au cas « pointeurs de même type ») et le
résultat est alors, lui aussi, de type void *.

10.3.1 Opérandes d’un type de base


Lorsque les deux derniers opérandes sont d’un type de base, ils sont soumis aux
conversions numériques habituelles (promotions numériques et conversions
d’ajustement de type) et le résultat fournit par l’opérateur sera du type commun.
Voici un exemple (n étant de type int) :
n ? 2 * n : 1.5 * n ;
Le second opérande est de type int, tandis que le troisième est de type float. Le
résultat de l’expression précédente sera toujours de type float. Bien entendu, si
les instructions de conversion de 2*n en float sont effectivement prévues dès la
compilation, leur exécution n’aura lieu que dans les cas où n est effectivement
non nul.

10.3.2 Opérandes de type pointeur


Les opérandes de type pointeur n’entraînent de conversions que lorsque l’un des
deux est de type void * (il y a alors conversion en void *) ou de valeur nulle (qui
est alors convertie dans le type de l’autre pointeur). La signification de ces
conversions sera étudiée en détail dans le chapitre consacré aux pointeurs, mais
nous les avons mentionnés dans le tableau précédent de façon à être exhaustif.
Signalons cependant dès maintenant que les qualifieurs const et volatile n’ont pas
besoin d’être les mêmes pour chacun des deux opérandes et que le résultat se
verra attribuer tout qualificatif concernant au moins l’un des opérandes.
Par exemple, avec ces déclarations :
const void * c_vptr ;
void * vptr ;
volatile int * v_iptr ;

voici ce que seraient les types de deux expressions (le type du premier opérande
n’ayant ici aucune importance) :
n ? c_vptr : vptr ; /* type : const void * */
n ? c_vptr : v_iptr /* type : const volatile void * */

10.3.3 Opérandes de type structure ou union


La possibilité de disposer d’opérandes de type structure ou union ne pose pas de
problème particulier, si ce n’est qu’il faut assurer l’identité des types des deux
opérandes. Cette notion d’identité est définie à la section 4.2 du chapitre 11. Elle
correspond en général à des structures ou unions définies suivant un type de
même nom. Par exemple :
int n ;
struct enreg { …. } ; /* définition d'un type structure enreg */
struct enreg e1, e2, e3 ; /* e1, e2 et e3 sont du type struct enreg */
…..
e1 = n ? e2 : e3; /* affecté à e1 : e2 si n non nul, e3 si n nul */

10.3.4 Opérandes de type void


La possibilité de disposer d’opérandes de type void signifie qu’il est possible que
ces opérandes n’aient pas de valeur ; il peut donc s’agir d’appel de fonctions ne
renvoyant pas de résultat, ce qui signifie qu’on utilise alors uniquement
l’opérande pour son « effet de bord » et qu’on ne cherche pas à utiliser sa valeur.
Par exemple :
n ? f1(…) : f2 (…) ;

pourrait remplacer (tout en étant nettement moins lisible) :


if (n != 0) f1 (…) ;
else f2 (…) ;

10.4 La priorité de l’opérateur conditionnel


L’opérateur conditionnel jouit d’une faible priorité (il arrive juste avant
l’affectation), de sorte qu’il est rarement nécessaire d’employer des parenthèses
pour en délimiter les différents opérandes (bien que cela puisse parfois améliorer
la lisibilité du programme). Voici, toutefois, un cas où les parenthèses sont
indispensables :
z = (x=y) ? a : b

Le calcul de cette expression amène tout d’abord à affecter la valeur de y à x.


Puis, si cette valeur est non nulle, on affecte la valeur de a à z. Si, au contraire,
cette valeur est nulle, on affecte la valeur de b à z.
Il est clair que cette expression est différente de :
z = x = y ? a : b

laquelle serait évaluée comme :


z = x = ( y ? a : b )
11. L’opérateur séquentiel
Comme nous l’avons déjà évoqué en introduction de ce chapitre, la notion
d’expression est beaucoup plus générale en C que dans la plupart des autres
langages car elle peut à la fois réaliser une action et posséder une valeur.
L’opérateur dit « séquentiel » va élargir encore un peu plus cette notion
d’expression. En effet, il permet en quelque sorte d’exprimer plusieurs calculs
successifs au sein d’une même expression. Par exemple :
a * b , i + j

est une expression qui évalue d’abord a*b, puis i+j et qui prend comme valeur la
dernière calculée (donc ici celle de i+j). Certes, dans cet exemple « d’école », le
calcul préalable de a*b est inutile puisqu’il n’intervient pas dans la valeur de
l’expression globale et qu’il ne réalise aucune action. En revanche, une
expression telle que :
i++, a + b

peut présenter un intérêt puisque la première expression (dont la valeur ne sera


pas utilisée) réalise en fait une incrémentation de la variable i.
Il en va de même pour l’expression suivante :
i++, j = i + k

dans laquelle, il y a :
• évaluation de l’expression i++, ;
• évaluation de l’affectation j = i + k. Notez qu’alors, on utilise la valeur de i
après incrémentation par l’expression précédente.
Cet opérateur séquentiel, qui jouit d’une associativité de gauche à droite, peut
facilement faire intervenir plusieurs expressions et sa faible priorité évite l’usage
de parenthèses :
i++, j = i+k, j--

Certes, un tel opérateur pourrait théoriquement être utilisé pour réunir plusieurs
instructions en une seule. Ainsi, ces deux formulations sont équivalentes :
i++, j = i+k, j-- ;
i++ ; j = i+k ; j-- ;

Dans la pratique, ce n’est cependant pas là le principal usage que l’on fera de cet
opérateur séquentiel. En revanche, il interviendra fréquemment dans les
instructions de choix ou dans les boucles : là où la syntaxe n’a prévu qu’une
seule expression, l’opérateur séquentiel permet d’en placer plusieurs et, partant,
d’y réaliser plusieurs calculs ou plusieurs actions.
Voici deux exemples fournissant, sur une même ligne, deux formulations
équivalentes, la première avec l’opérateur séquentiel, la seconde sans :
if (i++, k>0) …… i++ ; if (k>0) ……
for (i=1, k=0 ; … ; … ) ……. i=1; for (k=0; … ; … ) ……

Comme l’appel d’une fonction n’est en fait rien d’autre qu’une expression, la
construction suivante est parfaitement valide en C :
for (i=1, k=0, printf("on commence") ; … ; …) ……

Dans le chapitre suivant, nous verrons que dans le cas des boucles
conditionnelles, cet opérateur permet de réaliser des constructions ne possédant
pas d’équivalent simple.
Par sa nature même, cet opérateur n’impose aucune contrainte à ses opérandes et
il n’induit aucune conversion de type.

Remarque
Il ne faut pas confondre cet opérateur séquentiel (,) avec la virgule utilisée (avec d’ailleurs la même
priorité) pour séparer les différents arguments d’une liste dans un appel de fonction, comme dans
l’instruction :
printf ("%d %d", n+2, p) ;
Si l’on souhaite utiliser cet opérateur séquentiel dans une telle liste, il est nécessaire d’en placer le
résultat entre parenthèses. Par exemple :
printf ("%d %d", a, (b=5, 3*b)) ;
• imprime la valeur de a ;
• évalue l’expression : b=5, 3*b, ce qui conduit à affecter 5 à b et à calculer ensuite la valeur de 3*b ;
• affiche la valeur de cette expression, c’est-à-dire finalement la valeur de 3*b.
12. L’opérateur sizeof
L’opérateur sizeof permet de connaître la taille en octets d’un objet ; plus
précisément, il possède un unique opérande qui peut être :
• soit un nom de type ;
• soit une expression.

12.1 L’opérateur sizeof appliqué à un nom de type


L’opérateur sizeof s’emploie simplement sous la forme suivante :

L’opérateur sizeof appliqué à un nom de type

sizeof (nom_de_type)
nom_de_type Nom de type quelconque, – nom d’un type de base =
y compris pointeur sur une spécificateur de type,
fonction, mais pas type éventuellement précédé
fonction. de qualifieurs (voir
commentaires à la
section 12.2.3) ;
– autres noms de types
présentés dans les
chapitres correspondants.
résultat Taille des objets du type, Ce résultat est de type
en octets (tient compte size_t (voir section 12.2.4).
pour les structures ou les
unions, d’octets
d’alignement ou de
remplissage).

La notion de nom de type n’intervient que dans quelques cas : opérateur de cast,
prototype de fonctions, opérateur sizeof. Dans le cas d’une variable d’un type de
base, ce nom de type n’est rien d’autre que le spécificateur de type utilisé pour sa
déclaration. Les autres noms de type (tableaux, pointeurs, structures, unions)
seront présentés dans les chapitres correspondants.
Exemples
sizeof (unsigned long int) /* même valeur que sizeof (long int) */
sizeof (char) /* vaut toujours 1 par définition */
sizeof (struct enreg) /* taille des objets de type struct enreg */
sizeof (int *) /* taille d'un pointeur sur un int */
sizeof (struct enreg *) /* taille d'un pointeur sur une structure de type enreg */
sizeof (int (*)[4]) /* taille d'un pointeur sur un tableau de 4 int */

On notera que, dans une implémentation donnée, différents types pointeurs


auront la même taille ; dans certains cas même, tous les pointeurs auront la
même taille. Aussi, les dernières expressions auront rarement un intérêt.

12.2 L’opérateur sizeof appliqué à une expression


12.2.1 Syntaxe
Dans le cas où l’opérande de sizeof est une expression, il est possible de faire
appel à l’une des deux notations ci-après :

Les deux notations de l’opérateur sizeof appliqué à une expression

sizeof (expression)
sizeof expression

Exemples
int n = 12 ; /* on suppose que dans l'implémentation */
/* concernée, le type int occupe 2 octets */
long q = 25 ; /* et le type long occupe 4 octets */
sizeof (n) /* a pour valeur 2 */
sizeof (n+q) /* a pour valeur 4 */
sizeof n /* a pour valeur 2 */
sizeof n + q /* a pour valeur 27, compte tenu de la */
/* priorité de l'opérateur sizeof */

On trouvera également des exemples de détermination du nombre d’éléments


d’un tableau à la section 3.4 du chapitre 6.

12.2.2 L’opérande de sizeof n’est pas évalué


L’expression utilisée comme opérande de sizeof n’est pas évaluée ; cela peut
avoir plusieurs conséquences :
• L’action éventuelle correspondant à une expression ne sera pas réalisée. Par
exemple :
sizeof (i++) /* i n'est pas incrémenté */
• L’expression en question peut apparaître dans une expression constante (les
expressions constantes sont définies à la section 14). Ce sera le cas, par
exemple, d’une expression comme sizeof(x+1) dont on notera qu’elle n’est pas
équivalente à sizeof(x) lorsque x est de type short ou char, en vertu des règles de
conversion.
short x ;
int t [sizeof (x+1) ] ; /* attention, équivalent, ici, à sizeof(int) */
char tc [sizeof(x)] ; /* plus portable que char tc [sizeof(short)] */

Remarques
1. Manifestement, la notation de sizeof sans parenthèses n’apporte aucun avantage. En revanche, elle
oblige à s’interroger sur la priorité relative de cet opérateur ; d’une manière générale, nous la
déconseillons.
2. Il peut s’avérer judicieux d’appliquer sizeof à une expression plutôt qu’à un type lorsqu’une
adaptation ultérieure d’un programme risque de modifier le type d’une variable. Par exemple, si à
un instant donné, on a déclaré x de type float, l’expression sizeof(x) représentera toujours la
taille de x, même après une modification de l’instruction de déclaration.

12.2.3 L’opérateur sizeof et les qualifieurs


Les éventuels qualifieurs ne modifient pas la taille d’un type. Par exemple, des
objets de type const int et des objets de type int auront toujours la même taille.
Cependant, comme l’indique la section 8.2.3, la norme ne précise pas si les
qualifieurs font ou non partie du nom de type. Ils sont acceptés dans un opérande
de sizeof (lorsqu’il s’agit d’un nom de type) dans la plupart des
implémentations :
sizeof (const int) /* généralement accepté, mais identique à sizeof (int) */

Bien entendu, ce problème ne se pose plus si sizeof porte sur une expression
puisque le type de cette dernière ne comporte plus de qualifieurs.
Dans le cas des objets pointés, leurs qualifieurs sont également inutiles bien que,
comme on le verra à la section 2.5 du chapitre 7, ils fassent partie intégrante du
type et qu’ils soient donc acceptés par la norme :
sizeof (const int *) /* correct, mais identique à sizeof (int *) */

12.2.4 Le type du résultat de sizeof


Manifestement, sizeof fourni un résultat entier ; mais la norme ne précise pas s’il
s’agit de long, unsigned long, int, unsigned int… Plus exactement, chaque
implémentation doit définir, par typedef, dans les fichiers en-tête en ayant
besoin23, le nom size_t correspondant au type entier du résultat fourni par sizeof.
Dans ces conditions, la seule chose qu’on puisse assurer est que le type unsigned
long est toujours suffisant pour accueillir le résultat de sizeof.

Notez qu’il est préférable d’éviter ce genre d’instruction :


printf ("taille flottant : %d\n", sizeof (float)) ;

qui ne fonctionnera que lorsque size_t sera identique à int. Il est préférable de
procéder ainsi :
int taille ; /* ou encore unsined int */
… /* ou unsigned long …. */
taille = sizeof (float) ;
printf ("taille flottant : %d\n", sizeof (float)) ; /* avec %u ou %lu … */
13. Tableau récapitulatif : priorités et associativité des
opérateurs
Le tableau 4.18 fournit la liste complète des opérateurs du langage C, classés par
ordre de priorité décroissante, accompagnés de leur associativité. Lorsque
plusieurs opérateurs figurent (même sur des lignes différentes) dans une même
cellule du tableau, ils ont même priorité.

Tableau 4.18 : priorités et associativités des différents opérateurs


14. Les expressions constantes

14.1 Introduction
Une expression constante est une expression dont la valeur peut être évaluée lors
de la compilation. Certes, il peut s’agir d’expressions telles que :
5 + 2
3 * 8 - 2

mais cela ne présente guère d’intérêt pour le programmeur puisqu’il lui est alors
possible de faire le calcul lui-même.
Les choses deviennent déjà plus intéressantes si l’on tient compte de l’existence
du préprocesseur, lequel traite toutes les directives (commençant par #) avant de
donner le résultat à compiler. Par exemple, avec :
#define LIMITE 20

les expressions :
LIMITE + 1
2 * LIMITE - 3

seront en fait fournies au compilateur sous la forme (le préprocesseur se


contentant de faire des substitutions, mais pas de calculs) :
20 + 1
2 * 20 - 3

En fait, la norme ANSI demande que le compilateur soit en mesure de calculer


des expressions constantes plus générales que celles que nous venons de citer.
On peut en effet y faire intervenir bon nombre d’opérateurs portant eux-mêmes
sur des expressions constantes. Par exemple, avec ces déclarations :
#define N 40
#define P 80
#define MOTIF 0xFFFF
#define DELTA 1.24

les expressions suivantes seront des expressions constantes :


N < P /* entier 0 ou 1 (ici 1) */
N == P /* entier 0 ou 1 (ici 0) */
MOTIF && (N == P) /* entier (ici 0) */
N < P ? 2 : 5 /* entier 2 ou 5 (ici 2) */
(N < 30) & (P < 100) /* entier 0, 1 ou 2 (ici 1 ) */
MOTIF >> 8 /* entier (ici 255) */
~(MOTIF >> 8) /* entier (ici 0) */
2*DELTA /* flottant, valant environ 2,48 */
14.2 Les expressions constantes d’une manière
générale
14.2.1 Les opérateurs utilisables
La plupart des opérateurs peuvent intervenir dans une expression constante. Les
seules exceptions sont parfaitement logiques puisqu’elles concernent des
opérateurs qui peuvent réaliser une action ou qui ne peuvent être évalués qu’au
moment de l’exécution du programme et non plus lors de sa compilation.

Dans une expression constante, tous les opérateurs sont autorisés, sauf les opérateurs d’affectation,
d’incrémentation, d’appel de fonction et l’opérateur séquentiel, à moins que ces opérateurs
n’apparaissent dans un opérande de sizeof.

Exemple
#define N 10
int p ;
int fct (int) ;
…..
p = N /* n'est pas une expression constante car = interdit ; */
/* mais, de toute façon, p n'est déjà pas une constante ! */
sizeof (p=N) /* expression constante, mais identique à sizeof (p) */
fct (N) + 1 /* n'est pas une expression constante, car appel de fonction */

14.2.2 Les trois catégories d’expressions constantes


La norme définit trois catégories d’expressions constantes :
• les expressions constantes entières ;
• les expressions constantes numériques, cas plus général englobant à la fois les
constantes entières précédentes et les constantes flottantes ;
• les expressions constantes correspondant à une adresse.
Selon leur nature, elles pourront intervenir dans des contextes différents. Le
tableau 4.19 en donne les principales caractéristiques. Elles feront ensuite l’objet
de quelques commentaires.

Tableau 4.19 : les trois catégories d’expressions constantes et leur


utilisation24

Catégorie Définition Utilisation


Expressions – résultat de type entier (y compris – dimension
constantes caractère) ; d’un tableau ;
entières – tous les opérandes entiers constants, – taille d’un
sauf pour sizeof (opérande champ de
quelconque) et pour cast (opérande bits ;
flottant, mais résultat entier). – constantes
d’énumération
24
;
– étiquettes de la
forme case xxx.
Expressions – résultat de type numérique (y – initialisation
constantes compris caractère) ; de variables
numériques statiques de
– tous les opérandes numériques
constants, sauf pour sizeof (opérande type
quelconque) et cast (opérandes numérique ;
obligatoirement numériques). – initialisation
de tableaux
numériques
(statiques ou
automatiques).
Expressions L’une des possibilités suivantes : – initialisation
constantes de variables
adresses – pointeur nul (NULL) ;
statiques de
– adresse constante (objet statique ou type pointeur.
fonction) ; on peut y utiliser les
opérateurs [], ., ->, * et cast de
pointeurs ;
– expression obtenue par addition ou
soustraction d’une constante entière à
une adresse constante.

La contrainte imposant aux opérandes apparaissant dans des expressions


constantes entières d’être entiers est manifestement restrictive puisque certaines
opérations sur des flottants conduisent à des résultats entiers. C’est le cas d’une
simple comparaison.
En pratique, bon nombre d’implémentations n’appliquent pas la norme à la lettre
dans ce cas :
#define DELTA 1.24
…..
2*(DELTA<1.5) /* expression entière valant 0 ou 2, théoriquement incorrecte
*/
/* mais acceptée dans beaucoup
d'implémentations */
int t[2*(DELTA<1.5)+5] ; /*généralement accepté : t sera de dimension 5 ou
7 */

Remarques
1. Les constantes de type énumération définies par une instruction enum sont des constantes entières.
C’est le cas de jaune, rouge et vert définies par :
enum couleur { jaune=5, rouge, vert=jaune+4 } ;

2. La notion d’expression constante existe également pour le préprocesseur. On verra à la section 3.2.4
du chapitre 15 qu’elle est légèrement plus restrictive qu’ici.
3. Les variables déclarées avec le qualifieur const ne sont pas considérées comme des constantes.
Elles le seront, en revanche, en C++ :
const int n = 5 ;
…..
int t[n] ; /* incorrect en C ; accepté en C++ *

1. N’oubliez pas de séparer les deux opérateurs + par au moins un espace ; dans le cas contraire, ces deux +
consécutifs seraient interprétés comme l’opérateur d’incrémentation ++ (dont nous parlerons un peu plus
loin).
2. La référence exacte est ANSI/IEEE 754-1985.
3. Et parfois avec l’opérateur -. Lorsqu’il est appliqué à la plus petite valeur (c’est-à-dire à l’entier négatif
de plus grande valeur absolue), il peut conduire, dans les implémentations utilisant la technique du
complément à deux, à un entier opposé non représentable (par exemple, avec 16 bits, -32 768 est
représentable, 32 768 ne l’est pas).
4. Alors même que bon nombre de machines disposent d’un mécanisme permettant de détecter la perte d’un
bit de retenue.
5. Certes, on pourrait parler de sous-dépassement de capacité, mais ce terme est conventionnellement
réservé à l’arithmétique flottante.
6. Généralement N+1 est égal à 2n, n étant le nombre de bits utilisés. Dans les machines utilisant la
technique du complément à deux pour les entiers signés, on retrouve le comportement habituel (mais non
imposé par la norme) du dépassement de capacité des entiers signés.
7. La norme actuelle emploie l’expression « promotions entières », mais « promotions numériques » reste
fort répandue (probablement parce que la description initiale du langage C, effectuée par Kernighan et
Ritchie comportait également une promotion numérique non entière de float en double).
8. En toute rigueur, la norme mentionne également une conversion triviale : conversion d’une lvalue en sa
valeur lorsqu’elle apparaît ailleurs qu’en premier opérande d’une affectation (ce qui va de soi !).
9. Et pas seulement lorsqu’on emploie la représentation en complément à deux !
10. Y compris certaines conversions vers un type de taille inférieure à celui d’origine qui ne concernent pas
les conversions implicites examinées ici, mais qui pourront apparaître dans les conversions explicites par
cast ou par affectation.
11. Cependant, si c1 et c2 n’ont pas le même attribut de signe, il apparaîtra une conversion d’un type
caractère dans un autre. Comme expliqué à la section 9, celle-ci conserve en pratique le motif binaire.
12. D’ailleurs, dans ce cas, la norme ANSI autorise le compilateur à mettre directement en place des
instructions portant sur un octet (incrémentation directe de un) sans passer par les transformations
intermédiaires en entier…
13. Pas plus que ne le pourraient les grandes valeurs du type unsigned int, totalement équivalent dans ces
implémentations à unsigned short.
14. À la section 3.3.3, nous avons rencontré un exemple dans lequel apparaissaient une promotion
numérique pour un opérande et une conversion d’ajustement de type pour l’autre. Mais nous n’avons
rencontré aucune situation dans laquelle un même opérande était soumis aux deux sortes de conversions.
15. Les conversions d’ajustement de type n’ont aucun sens pour un opérateur unaire !
16. Il existe quelques opérateurs binaires qui n’appliquent pas les conversions d’ajustement de type
(opérateur logiques, etc.).
17. 1 dans le cas du code ASCII.
18. Laquelle serait d’ailleurs la seule utilisable dans un langage tel que Pascal.
19. Suivant l’implémentation.
20. N’oubliez pas que les types caractère sont considérés comme des cas particuliers de types entiers.
21. La norme impose bien une identité, et pas seulement une valeur voisine à la précision du type près !
22. Notez bien qu’il ne s’agit que d’un résultat indéfini, non d’un comportement indéterminé ; il ne peut
donc pas y avoir d’erreur d’exécution dans ce cas.
23. Il s’agit de stddef.h, stdio.h, stdlib.h et string.h.
24. Les constantes d’énumération sont les expressions constantes entières qui servent à fixer la valeur d’une
constante de type énumération.
5
Les instructions
exécutables

En C, comme dans bon nombre de langages, il est d’usage de distinguer les


instructions de déclaration des instructions exécutables. Dans cet ouvrage, les
instructions de déclaration, qu’on nomme souvent déclarations tout simplement,
sont étudiées dans différents chapitres : types de base, tableaux, pointeurs,
structures, unions et énumérations. Elles font l’objet d’une récapitulation au
chapitre 16.
Ce chapitre est consacré aux instructions exécutables, qu’on nomme souvent en
C instructions. Nous commencerons par proposer deux classifications de ces
instructions, la première fondée sur la notion très répandue d’instruction de
contrôle, l’autre plus appropriée au langage C. Puis, nous passerons en revue les
différentes instructions du langage. Après l’instruction expression et l’instruction
composée (ou bloc), nous présenterons les instructions de choix if et switch.
Après une présentation générale des particularités des boucles en C, nous
examinerons les instructions do … while, while et for et nous vous prodiguerons
quelques conseils d’utilisation. Puis, après avoir étudié les instructions de
rupture de séquence que sont break et goto, nous vous proposerons quelques
schémas de boucles supplémentaires. Nous terminerons par l’instruction goto.
1. Généralités
Les façons de classifier les instructions du langage C sont nombreuses. Nous
commencerons par rappeler ce que l’on nomme instruction de contrôle dans un
langage évolué, ce qui nous amènera à constater qu’en C, la plupart des
instructions entrent dans cette catégorie. Nous proposerons ensuite une
classification relativement naturelle des différentes instructions exécutables.

1.1 Rappels sur les instructions de contrôle


, dans un programme, les instructions sont exécutées séquentiellement,
A priori
c’est-à-dire dans l’ordre où elles apparaissent. Or la puissance et le
« comportement intelligent » d’un programme proviennent essentiellement :
• de la possibilité d’effectuer des choix (ou sélections ou encore alternatives),
c’est-à-dire de se comporter différemment suivant les circonstances, par
exemple en fonction d’une réponse de l’utilisateur, d’un résultat de calcul… ;
• de la possibilité d’effectuer des boucles (ou itérations), autrement dit de répéter
plusieurs fois un ensemble donné d’instructions.
Pour réaliser ces choix ou ces boucles, tous les langages disposent d’instructions,
nommées « instructions de contrôle ». Autrefois, ces dernières étaient basées
essentiellement sur la notion de « branchement » conditionnel ou inconditionnel
(cas des premiers Basic ou Fortran). Elles ont ensuite reproduit tout ou partie des
structures fondamentales de la programmation structurée (cas de Pascal, puis de
la plupart des langages récents tels que Java, Python, C#, PHP…), tout en
conservant en parallèle quelques possibilités de branchement, utilisés alors plutôt
à titre exceptionnel.
Le langage C dispose d’un riche éventail d’instructions structurées permettant de
réaliser :
• des choix : instructions if et switch ;
• des boucles : instructions do … while, while et for.
Il dispose par ailleurs de quelques instructions de branchement inconditionnel :
goto, break, continue et return.

En C, la plupart des instructions sont des instructions de contrôle. Cela tient à la


richesse de la notion d’expression qui, en incluant les appels de fonction (donc
les entrées-sorties) rend toute action réalisable avec la seule instruction
expression. En effet, en dehors de cette dernière instruction, il n’en existe qu’une
seule autre qui ne soit pas une instruction de contrôle, à savoir l’instruction
composée (ou bloc). Celle-ci est formée d’autres instructions qui, en définitive,
seront soit des instructions expressions, soit des instructions de contrôle.

1.2 Classification des instructions exécutables du


langage C
Hormis la distinction classique entre les instructions de contrôle et les autres
instructions exécutables, présentée dans la section précédente, il existe plusieurs
autres classifications des instructions exécutables du langage C. Nous vous en
proposons ici une dont l’expérience montre qu’elle est relativement naturelle.
On notera que toutes les instructions simples se terminent par un point-virgule.
En revanche, il existe une instruction n’entrant pas dans cette catégorie et se
terminant aussi par un point-virgule, à savoir do … while ! C’est pourquoi la
terminaison par un point-virgule n’est pas utilisable pour caractériser
l’instruction simple dont la véritable caractéristique est, en fait, de ne renfermer
aucune autre instruction.

Tableau 5.1 : classification des instructions exécutables du langage C

Il y a en C, comme dans beaucoup de langages, une sorte de récursivité de la


notion d’instruction. Un bloc ou une instruction structurée peuvent renfermer à
leur tour d’autres instructions de l’une des trois catégories. Seules les
instructions simples échappent à cette règle (d’où leur nom). D’une manière
générale, dans la description de la syntaxe des différentes instructions, nous
utiliserons souvent le terme d’instruction. Celui-ci désignera toujours n’importe
quelle instruction exécutable : simple, structurée ou bloc.
Toute instruction peut comporter une étiquette. Nous verrons qu’il existe deux
sortes d’étiquettes :
• celles de la forme case xxx, décrites à la section 5 : elles ne sont utilisables que
dans une instruction switch ;
• celles formées d’un simple identificateur et décrites à la section 15 : elles
peuvent apparaître devant n’importe quelle instruction.

Remarques
1. La norme ANSI classe les instructions en six catégories : instruction étiquetée, instruction composée
(bloc), instruction expression, instructions de sélection (choix), instructions d’itération (boucles) et
instructions de saut (branchements).
2. Rappelons que dans une instruction expression peuvent apparaître beaucoup d’actions telles qu’une
affectation, un appel de fonction, donc en particulier une entrée-sortie. C’est la raison pour laquelle
on ne trouve pas en C, comme dans beaucoup d’autres langages, d’instructions spécifiques pour
l’affectation, l’appel de procédure, l’écriture ou la lecture.
2. L’instruction expression
Cette instruction est la plus utilisée car elle permet de réaliser la plupart des
actions, en particulier les affectations, les appels de fonctions et donc les entrées-
sorties puisqu’elles sont mise en œuvre en C par un appel d’une fonction
standard.

2.1 Syntaxe et rôle


L’instruction expression

[ expression ] ;
expression
– expression d’un type quelconque – les expressions
(y compris structure ou union) ou numériques sont
de type void, dans le cas d’un étudiées au
appel de fonction sans résultat ; chapitre 4 ;
– si absente, on a affaire à une – les expressions de
instruction vide. type pointeur sont
étudiées au
chapitre 7.
N.B. : les crochets ([ et ]) signifient que leur contenu est facultatif vis-à-vis de la syntaxe.

Cette instruction évalue l’expression mentionnée (si elle est présente).

2.2 Commentaires
La différence entre une expression et une instruction expression n’apparaît que
dans le point-virgule qui termine la seconde. On peut considérer, en quelque
sorte, que la présence de ce point-virgule indique qu’on accepte de perdre la
valeur de l’expression considérée. Par exemple, dans :
printf ("bonjour") ;

on perd effectivement la valeur de retour de printf. De même, dans :


a = 5 ;

on perd finalement la valeur de l’expression, à savoir, valeur de a après


affectation, ce qui n’est nullement gênant !
Cette curiosité peut permettre d’écrire des instructions expression qui ne font
rien, si ce n’est peut-être de prendre un petit peu de temps d’exécution. Il suffit
pour cela de transformer en instruction une expression qui ne réalise aucune
action, autrement dit qui se contente de posséder une valeur. En voici des
exemples :
i ;
i+5 ; /* la valeur i+5 est calculée mais non utilisée */
i, j, k ;

En revanche, une simple instruction telle que :


i++ ;

possède bien un intérêt, celui d’incrémenter i de un, alors qu’on n’utilise pas sa
valeur (celle de i avant incrémentation).
Par ailleurs, on notera qu’un appel de fonction est une expression, même lorsque
la fonction ne possède pas de valeur de retour. D’ailleurs, dans ce dernier cas,
l’instruction expression constitue le seul et unique moyen d’appeler la fonction.
Si la fonction possède une valeur de retour, on peut l’utiliser dans une expression
mais ce n’est pas obligatoire ; si on ne le fait pas, on retrouve un appel de la
forme instruction expression. Considérons ces exemples :
void f1(int) ;
int f2(float) ;
…..
f1(5); /* appel de f1, fonction sans valeur */
y = f2(x) + 5; /* appel de f2 dont on utilise la valeur */
f2(x); /* appel de f2 dont on n'utilise pas la valeur */

L’expression est facultative. Si elle est absente, on dit qu’on a affaire à une
instruction vide. Une telle instruction peut apparaître là où une instruction simple
est permise. Par exemple, on peut écrire :
i++ ; ; /* autorisé, mais sans intérêt */

Ici, l’instruction vide est inutile. En revanche, elle pourra présenter un intérêt à
l’intérieur de certaines instructions structurées. Nous en verrons des exemples
dans ce chapitre.
3. L’instruction composée ou bloc

3.1 Syntaxe d’un bloc


Un bloc est une suite d’instructions exécutables placées entre accolades ({ et }).
Ces instructions sont absolument quelconques. Il peut donc s’agir d’instructions
simples, structurées ou même d’autres blocs, un bloc pouvant donc apparaître à
son tour dans un autre bloc, soit tel quel, soit au sein d’une instruction structurée.
De plus, un bloc peut comporter des déclarations – depuis C99, leur
emplacement est libre ; en C90, elles doivent précéder toute instruction
exécutable.

Instruction composée ou bloc

{ [ declarations ]
[ 0, 1 ou plusieurs instructions executables quelconques ]
}
N.B. : les crochets ([…]) signifient que leur contenu est facultatif vis-à-vis de la syntaxe.

3.2 Commentaires
Le corps d’une fonction, donc en particulier celui de la fonction main, n’est rien
d’autre qu’un bloc.
Un bloc peut se réduire à une seule instruction exécutable, comme :
{ i = 1 ; }

On peut objecter que cela présente peu d’intérêt puisque ce bloc peut toujours
être remplacé par l’instruction qu’il contient :
i = 1 ;

Toutefois, cette façon de procéder peut faciliter l’adaptation ultérieure d’un


programme dans le cas où cette unique instruction risque, par la suite, d’être
complétée par d’autres. C’est ce qui se passe lorsqu’elle correspond au cas vrai
ou au cas faux d’une instruction if ou au corps d’une boucle. Un exemple en est
présenté à la section 4.3.2, dans laquelle nous verrons également que le recours à
des blocs permet de mettre en évidence la règle relative aux imbrications des
instructions if, voire à l’outrepasser.
Un bloc peut être vide :
{ }

Il joue théoriquement le même rôle qu’une instruction simple vide :


;

Il est souvent employé pour améliorer la lisibilité des boucles à corps vide. Par
exemple, on utilisera souvent :
do {} while (condition) ;

plutôt que :
do ; while (condition) ;

Notez que :
{ ; }

est un bloc constitué d’une seule instruction vide, ce qui est « syntaxiquement »
correct mais plutôt inutile.
Toutes les instructions structurées sauf do … while nécessitent l’emploi d’un bloc,
à partir du moment où elles contiennent plus d’une instruction. Mais un bloc
peut toujours être utilisé en dehors de ces instructions et autrement que comme
bloc principal d’une fonction. En général, on procédera ainsi en vue de
bénéficier des possibilités de déclarations spécifiques à un bloc étudiées à la
section 3.3.

Remarque
Toute instruction simple est toujours terminée par un point-virgule. Ainsi le bloc suivant est incorrect
car il manque un point-virgule à la fin de la seconde instruction qu’il contient :
{ i = 5 ; k = 3 } /* incorrect */
Par ailleurs, un bloc joue le même rôle syntaxique qu’une instruction simple (point-virgule compris). Il
faut donc éviter d’ajouter des points-virgules intempestifs à la suite d’un bloc. Dans le meilleur des
cas, cela s’avère inutile. Ainsi, dans l’instruction suivante, le point-virgule revient à ajouter une
instruction vide :
while (…) {…} ; /* ; superflu mais ne nuit pas */
En revanche, dans d’autres cas, cela peut conduire à une erreur de syntaxe. Par exemple, avec :
if (a < b) { min = a ;
max = b ;
} ; /* ; superflu et conduisant à une erreur de syntaxe */
else …..
le point-virgule placé à la fin du bloc met fin à l’instruction if, et le else qui suit conduit à une erreur.
Bien entendu, ce point-virgule n’aurait pas été gênant si l’instruction if n’avait pas contenu de else.

3.3 Déclarations dans un bloc


Lorsqu’un bloc comporte des déclarations, ces dernières sont soumises aux
règles habituelles de portée, de classe d’allocation et d’initialisation qui
s’appliquent à n’importe quel bloc, qu’il s’agisse d’un bloc interne à une
fonction ou du bloc servant de définition de la fonction. Ces règles sont décrites
en détail à la section 9 du chapitre 8. Ici, nous n’en donnerons qu’un bref extrait
assorti de quelques exemples.

3.3.1 Classe d’allocation


Par défaut, la classe d’allocation des variables déclarées dans un bloc est la
classe automatique. Ces variables voient donc leur emplacement alloué à l’entrée
dans le bloc et libéré lors de la sortie du bloc. Il est possible d’utiliser cette
particularité pour économiser certains emplacements qu’on s’arrange pour
allouer le temps nécessaire. Par exemple, si pour effectuer le traitement noté
instructions, on a besoin d’un tableau de 100 entiers, on pourra très bien procéder
ainsi :
{ int t[100] ; /* tableau alloué pour ce bloc */
/* instructions */ /* ici t est connu et utilisable */
} /* l'emplacement alloué à t est libéré à la sortie du bloc */
….. /* ici t n'est plus connu */

Notez bien que cette démarche s’applique même si le bloc en question ne fait pas
partie d’une instruction structurée : il peut très bien être précédé et/ou suivi
d’instructions simples… D’ailleurs, il est tout à fait envisageable de créer ainsi
artificiellement un bloc, uniquement en vue d’allouer un emplacement pour le
seul temps où il est nécessaire.

3.3.2 Initialisation
Les variables automatiques sont initialisées à chaque entrée dans le bloc, comme
dans :
for (i=0 ; i<20 ; i++)
{ int k = 2*i + 3 ; /* k est initialisé à chaque entrée dans le bloc */
…..
}

Au premier tour de boucle, k sera initialisée à 3, au deuxième tour à 5, etc.


On peut déclarer dans un bloc des variables de classe statique : elles ne sont
initialisées qu’une seule fois, avant l’exécution de l’ensemble du programme, et
elles conservent leur valeur d’une exécution du bloc à la suivante. Voici un
exemple dans lequel on comptabilise le nombre de passages dans un bloc :
{ static int ctr = 0 ; /* initialisation à 0 en début de programme */
ctr++ ; /* +1 sur ctr, à chaque exécution du bloc */
…..
}

3.4 Cas des branchements à l’intérieur d’un bloc


Comme nous le verrons à la section 15 consacrée à l’instruction goto, il est
théoriquement permis de se brancher depuis l’extérieur d’un bloc vers une
instruction située à l’intérieur du bloc et ce, soit par un goto, soit par le biais
d’une étiquette case xxx d’un switch. Il s’agit d’une situation fortement
déconseillée pour les risques qu’elle comporte. En effet, dans ce cas, les
initialisations des variables automatiques ne sont pas effectuées ! Considérez, par
exemple :
for (i=0 ; i<20 ; i++)
{ int k = 2*i + 3 ;
…..
suite :
…..
}

Si l’on exécute, depuis l’extérieur de ce bloc, une instruction :


goto suite ;

la valeur de k ne sera pas définie (pas plus d’ailleurs que celle de i).
4. L’instruction if

4.1 Syntaxe et rôle de l’instruction if


L’instruction if permet de programmer une structure dite de « choix » (ou
sélection ou encore alternative), permettant de choisir entre deux instructions,
suivant la valeur d’une expression numérique jouant le rôle de condition. La
seconde partie, introduite par le mot-clé else, est facultative, de sorte que
l’instruction if présente deux formes.

Les deux formes de l’instruction if

if (expression) if (expression)
instruction_1 instruction_1
else
instruction_2
expression
Expression quelconque de type scalaire (numérique
ou pointeur)
instruction_1 ou Instruction exécutable quelconque, c’est-à-dire
instruction_2
simple, structurée ou bloc

Cette instruction évalue l’expression mentionnée à la suite de if. Si elle est non
nulle, on exécute instruction_1 ; si elle est nulle, on exécute instruction_2 si cette
dernière est présente. Puis, dans tous les cas, on passe à l’instruction suivant
cette instruction if.

Remarques
1. Les parenthèses entourant l’expression jouant le rôle de condition font partie de la syntaxe de
l’instruction et sont donc obligatoires.
2. La syntaxe de cette instruction n’impose en soi aucun point-virgule, si ce n’est ceux qui terminent
naturellement les instructions simples (ou do … while) qui peuvent y figurer.

4.2 Exemples d’utilisation


4.2.1 Exemples liés à la généralité de la notion d’instruction
Les instructions_1 et instruction_2 figurant dans la syntaxe de if sont quelconques.
Voici une première instruction if dans laquelle les deux instructions concernées
sont des instructions simples :
if (a > b) max = a ;
else max = b ;

Voici une autre instruction if dans laquelle les deux instructions concernées sont
des blocs :
if (a > b) { max = a ;
printf ("maximum en a\n") ;
}
else { max = b ;
printf ("maximum en b\n") ;
}

Voici la même instruction présentée avec des indentations moins importantes,


plus adaptées à des programmes un peu complexes :
if (a > b)
{ max = a ;
printf ("maximum en a\n") ;
}
else
{ max = b ;
printf ("maximum en b\n") ;
}

On trouvera à la section 4.3 des exemples dans lesquels l’instruction if contient


elle-même d’autres instructions if.

4.2.2 Exemples sans else


max = b ;
if (a > b) max = a ;
printf ("maximum : %d", max) ;

On notera bien qu’après l’instruction :


max = a ;

le compilateur acceptera indifféremment le mot-clé else ou une autre instruction.


Dans ce dernier cas, il conclut à la seconde forme d’instruction if. Il n’est alors
plus possible (hormis les situations de if imbriqués décrits à la section 4.3) qu’un
else apparaisse plus tard comme dans :

max = b ;
if (a > b) max = a ;
printf ("maximum : %d", max) ;
else max = b ;

4.2.3 Attention aux adaptations de programmes


La remarque précédente est particulièrement sensible en cas d’adaptation de
programmes existants. Si, par exemple, on part de la forme suivante dans
laquelle instruction_1 et instructions_2 sont des instructions simples (donc
terminées par un point-virgule) :
if (…) instruction_1
else instruction_2

et qu’on souhaite par la suite introduire une seconde instruction dans le cas
« vrai », il ne faudra pas procéder ainsi :
if (…) instruction_1
instruction /* cette instruction force la seconde forme de if */
else instruction_2

Une manière d’éviter ce type de problème consiste à placer systématiquement


des blocs dans l’instruction if, quitte à ce que certains d’entre eux soient réduits
à des instructions simples, ce qui peut amener à écrire des choses telles que :
if (a > b) { max = a ; }
else { max = b ; }

4.2.4 Conséquences de la généralité de la notion d’expression en C


L’expression conditionnant le choix est quelconque. La richesse de la notion
d’expression en C fait que celle-ci peut elle-même réaliser certaines actions.
Ainsi :
if ( ++i < limite) printf ("OK") ;

est équivalent à :
i = i + 1 ;
if ( i < limite ) printf ("OK") ;

Par ailleurs :
if ( i++ < limite ) ……

est équivalent à :
i = i + 1 ;
if ( i-1 < limite ) ……

De même :
if ( ( c=getchar() ) != ‘\n' ) ……

peut remplacer :
c = getchar() ;
if ( c != ‘\n' ) ……

En revanche :
if ( ++i<max && ( (c=getchar()) != ‘\n') ) ……
n’est pas équivalent à :
++i ;
c = getchar() ;
if ( i<max && ( c!= ‘\n' ) ) ……

car l’opérateur && n’évalue son second opérande que lorsque cela est nécessaire.
Autrement dit, dans la première formulation, l’expression :
c = getchar()

n’est pas évaluée lorsque la condition ++i<max est fausse. En revanche,, elle l’est
dans la deuxième formulation.
Enfin, l’expression gouvernant le choix pourra être d’un type pointeur. Par
exemple, si p est un pointeur de type quelconque :
if (p) …..

est équivalent à :
if (p != NULL) …..

4.3 Cas des if imbriqués


4.3.1 La règle
Les instructions figurant dans chaque partie du choix d’une instruction if sont
absolument quelconques. En particulier, elles peuvent à leur tour renfermer
d’autres instructions if. Or, comme cette instruction peut comporter ou ne pas
comporter de else, il existe certaines situations qui peuvent paraître ambiguës.
C’est le cas dans cet exemple :
if (a<=b) if (b<=c) printf ("ordonne") ;
else printf ("non ordonne") ;

Est-il interprété comme le suggère cette présentation ?


if (a<=b) if (b<=c) printf ("ordonne") ;
else printf ("non ordonne") ;

Ou bien comme le suggère celle-ci ?


if (a<=b) if (b<=c) printf ("ordonne") ;
else printf ("non ordonne") ;

La première interprétation conduirait à afficher « non ordonné » lorsque la


condition a<=b est fausse, tandis que la seconde n’afficherait rien dans ce cas. La
règle adoptée par le langage C pour lever une telle ambiguïté est la suivante :

Un else se rapporte toujours au dernier if rencontré (dans le même bloc) auquel un else n’a pas
encore été attribué.

Ainsi, dans notre exemple, c’est la seconde présentation qui suggère le mieux ce
qui se passe.

4.3.2 Pour outrepasser la règle


Signalons tout d’abord que l’utilisation systématique de blocs (éventuellement
réduits à une instruction simple) évite d’avoir à se poser la question du
rattachement d’un else, comme dans :
if (a<=b) { if (b<=c) printf ("ordonne") ;
else printf ("non ordonne") ;
}

ou même, dans :
if (a<=b) { if (b<=c) { printf ("ordonne") ;
}
else { printf ("non ordonne") ;
}
}

Cette même technique, employée de façon adéquate, permet d’outrepasser


facilement la règle précédente, par exemple :
if (a<=b) { if (b<=c) printf ("ordonne") ;
}
else printf ("non ordonne") ;

Cette formulation est plus agréable que la formulation équivalente suivante :


if (a<=b) if (b<=c) printf ("ordonne") ;
else ; /* else suivi d'une instruction vide : correct */
else printf ("non ordonne") ;

4.3.3 Attention à certaines erreurs cachées


Considérez la construction suivante :
if (…) while (…)
if (…) instruction_1
else instruction_2

Quelle que soit la nature de instruction_1 (simple, structurée ou bloc), le else est
rattaché au if situé à l’intérieur de while. Une présentation plus suggestive de la
réalité serait :
if (…) while (…)
if (…) instruction_1
else instruction_2

Pour forcer le rattachement au premier if, on peut procéder ainsi :


if (…) while (…)
{ if (…) instruction_1 /* bloc réduit à une seule instruction */
} /* structurée */
else instruction_2

4.4 Traduction de choix en cascade


Il est fréquent d’avoir à exprimer ce que l’on nomme souvent des choix en
cascade, c’est-à-dire des schémas tels que celui de la figure 5-1, qui comporte
trois sélections (il va de soi que ce nombre pourrait être plus élevé) :
Figure 5-1
Choix en cascade

Un tel schéma se traduit presque littéralement en langage C de la manière


suivante, les instructions concernées pouvant être des instructions simples (donc
suivies d’un point-virgule) ou des blocs :
if (cond_1) instruction_1
else if (cond_2) instruction_2
else if (cond_3) instruction_3
else instruction_4

Exemple
Voici un exemple d’utilisation de choix en cascade. Il s’agit d’un programme de
facturation avec remise. Il lit en donnée un simple prix hors taxes et calcule le
prix TTC correspondant (avec un taux de TVA constant de 19,6 %). Il établit
ensuite une remise dont le taux dépend de la valeur ainsi obtenue, à savoir :
• 0 % pour un montant inférieur à 1 000 € ;
• 1 % pour un montant supérieur ou égal à 1 000 € et inférieur à 2 000 € ;
• 3 % pour un montant supérieur ou égal à 2 000 € et inférieur à 5 000 € ;
• 5 % pour un montant supérieur ou égal à 5 000 €.
Exemple de choix en cascade : facturation avec remise

#define TAUX_TVA 19.6


int main()
{
double ht, ttc, net, tauxr, remise ;
printf("donnez le prix hors taxes : ") ;
scanf ("%lf", &ht) ;
ttc = ht * ( 1. + TAUX_TVA/100.) ;
if ( ttc < 1000.) tauxr = 0 ;
else if ( ttc < 2000 ) tauxr = 1. ;
else if ( ttc < 5000 ) tauxr = 3. ;
else tauxr = 5. ;
remise = ttc * tauxr / 100. ;
net = ttc - remise ;
printf ("prix ttc %10.2lf\n", ttc) ;
printf ("remise %10.2lf\n", remise) ;
printf ("net a payer %10.2lf\n", net) ;
}
5. L’instruction switch
Compte tenu de l’aspect peu structuré de l’instruction switch, ainsi que de la
nature très particulière de sa syntaxe, nous commencerons par l’introduire grâce
à un exemple simple. Nous présenterons ensuite ce que nous nommons sa
syntaxe usuelle qui correspond à la manière courante d’utiliser cette instruction.
Alors seulement nous présenterons la forme théorique de switch telle qu’elle est
prévue par la norme.

5.1 Exemple introductif


La principale vocation de l’instruction switch est de permettre de programmer ce
que l’on nomme usuellement une structure de choix multiple (ou de sélection
multiple), c’est-à-dire un choix entre plusieurs possibilités, chaque possibilité
s’exprimant par une ou plusieurs instructions.
Voici un exemple d’école accompagné de trois exemples d’exécution ; il a pour
but d’illustrer le fonctionnement de switch et il ne faut pas chercher à lui attribuer
une signification pratique :
Exemple d’utilisation de l’instruction switch

#include <stdio.h>
int main()
{ int n ;
printf ("donnez un entier : ") ;
scanf ("%d", &n) ;
switch (n)
{ case 0 : printf ("nul\n") ;
case 1 :
case 2 : printf ("petit\n") ;
break ;
case 3 : printf ("moyen\n") ;
break ;
case 4 :
case 5 : printf ("grand\n") ;
break ;
default : printf ("hors norme\n") ;
break ; /* facultatif mais bonne précaution */
}
printf ("fin programme\n") ;
}
donnez un entier : 0
nul
petit
fin programme
donnez un entier : 4
grand
fin programme

donnez un entier : 10
hors norme
fin programme

Si n vaut 0, on se branche à l’étiquette case 0, si elle existe (ce qui est le cas ici) et
on exécute les instructions en séquence à partir de là. On affiche donc le texte nul
puis le texte petit. C’est seulement la rencontre de l’instruction break qui met fin à
l’exécution de l’instruction switch en passant à l’instruction suivante laquelle, ici,
affiche le texte fin programme.
Si n vaut 4, on se branche à l’étiquette case 4, ce qui amène à l’affichage du texte
grand. L’instruction break suivante met fin à l’instruction switch.

Lorsque la valeur de n n’est pas trouvée dans les différentes étiquettes de la


forme case xxx, on se branche à l’étiquette default (si elle n’existait pas, on serait
tout simplement sorti du switch sans rien faire).

5.2 Syntaxe usuelle et rôle de switch


La syntaxe théorique de l’instruction switch est présentée à la section 5.4.2. Nous
en donnons ici une forme un peu plus particulière mais beaucoup plus
significative quant à la manière dont on emploie cette instruction en pratique,
c’est-à-dire, en définitive, pour programmer une structure de choix multiple.

L’instruction switch (forme usuelle)

switch (expression)
{ case constante_1 : [ suite_d_instructions_1 ]
case constante_2 : [ suite_d_instructions_2 ]
…………..
case constante_n : [ suite_d_instructions_n ]
[ default : suite_d_instructions ]
}
expression
Expression de type
entier (char, short, int ou
1
long ) signé ou non

constante_i
Expression constante Les expressions
entière constantes sont définies
à la section 14 du
chapitre 4
suite_d_instructions_i
Séquence d’instructions
Attention, il ne s’agit
quelconques pas nécessairement
d’un bloc
1. Certaines anciennes implémentations peuvent ne pas accepter d’entiers longs.
N.B. : les crochets ([ et ]) signifient que leur contenu est facultatif.

Le compilateur prévoit l’évaluation de l’expression selon les règles habituelles et


la soumet éventuellement à une promotion numérique. Au final, le résultat est de
type int ou long (signé ou non). Par ailleurs, il convertit systématiquement les
expressions constantes dans le type final de l’expression et s’assure qu’aucune
valeur n’apparaît en double.
Lors de l’exécution, l’expression est évaluée et il y a branchement à l’étiquette
case xxx correspondante si elle existe. Dans le cas contraire, il y a branchement à
l’étiquette default si elle existe, à la suite de l’instruction switch sinon.

Remarques
1. Les parenthèses entourant l’expression font partie de la syntaxe de l’instruction et sont donc
obligatoires.
2. Les étiquettes de la forme case xxx doivent obligatoirement comporter un ou plusieurs espaces
entre case et la valeur entière xxx. En revanche, cela n’est pas nécessaire entre xxx et les deux-
points suivants, même si, en pratique, cela améliore la lisibilité.
3. En théorie, rien n’interdit que certaines étiquettes de la forme case xxx apparaissent à l’intérieur de
blocs englobés dans le bloc gouverné par switch. Cette possibilité est vivement déconseillée mais
elle sera néanmoins étudiée à la section 5.4.
4. La syntaxe de l’instruction switch justifie qu’on la classe traditionnellement dans les instructions
structurées. Elle n’en reste pas moins une instruction relativement hybride, dans la mesure où :
– elle se contente de mettre en place un « aiguillage » basé sur la valeur d’une expression ;
– elle laisse le programmeur décider de mettre fin quand bon lui semble au traitement d’un cas
donné. En général, il utilise pour ce faire des instructions break mais celles-ci ne font pas
vraiment partie de la syntaxe de l’instruction switch elle-même. On pourrait à la limite écrire une
instruction switch ne comportant aucune instruction break…

5.3 Commentaires
5.3.1 Type des constantes suivant case
Les constantes suivant case ne peuvent pas être d’un type flottant. Une telle
contrainte est parfaitement justifiée. En effet, il ne faut pas oublier que la
comparaison d’égalité entre flottants est relativement aléatoire, compte tenu de la
précision limitée des calculs. De surcroît, la nature même des étiquettes rendrait
assez difficile la prise en compte de valeurs flottantes…
En revanche, ces constantes peuvent être d’un type caractère puisqu’elles seront,
de toute façon, converties dans le type de l’expression, laquelle sera de l’un des
types int ou long. On voit que la construction suivante, dans laquelle c est de type
char, est parfaitement légale :

switch(c)
{ case ‘a' : ……
case 132 : …..
……
}

Il en va de même pour celle-ci, où n est supposée de type int :


switch (n)
{ case ‘A' : …..
case 559 : …..
case 4023 : …..
…….
}

Notez que si n était de type char, l’instruction précédente serait acceptée, mais il
est fort probable que certaines des étiquettes ne pourraient jamais être atteintes.

5.3.2 Après case, on peut trouver une expression constante


Rappelons qu’on nomme « expression constante », une expression qui peut être
évaluée lors de la compilation. Les différentes formes possibles pour les
expressions constantes entières ont été définies à la section 14 du chapitre 4.
Bien entendu, il ne sert à rien d’écrire case 5+ 2 plutôt que case 7. En revanche,
l’utilisation de symboles définis par la directive #define présente un intérêt. En
voici un exemple :
#define LIMITE 20
…..
switch (n)
{ …..
case LIMITE-1 : ……
case LIMITE : ……
case LIMITE+1 : ……
}

À la compilation, les expressions LIMITE-1, LIMITE et LIMITE+1 seront effectivement


remplacées par les valeurs 19, 20 et 21.
Cette façon de procéder permet un certain « paramétrage » des programmes.
Ainsi, dans cet exemple, une modification de la valeur de LIMITE se résume à une
seule intervention au niveau de la directive #define. Notez bien qu’une variable
initialisée à 20 au sein du programme ne pourrait pas être utilisée puisque alors
les étiquettes de l’instruction switch ne seraient plus des expressions constantes. Il
en va de même pour une constante symbolique déclarée par1 :
const int N = 20 ; /* N n'est pas une expression constante en C */

5.4 Quelques curiosités de l’instruction switch


Les sections précédentes ont décrit la manière usuelle d’utiliser l’instruction
switch. La norme autorise des possibilités théoriquement plus larges, mais en
réalité fort dangereuses. En effet :
• les étiquettes concernées peuvent se trouver à l’intérieur de blocs eux-mêmes
inclus dans le bloc gouverné par cette instruction ;
• l’instruction concernée par le switch peut être une instruction quelconque et non
seulement un bloc, comme nous l’avons laissé entendre à la section 5.2.

5.4.1 Étiquettes enfouies dans des blocs


La seule contrainte pesant sur les étiquettes case xxx concernées par switch est que
ces étiquettes sont recherchées à l’intérieur du bloc régi par cette instruction.
Cela revient à dire qu’on autorise tout branchement ayant lieu vers l’intérieur du
bloc, quelle qu’en soit la profondeur. En particulier, on peut ainsi provoquer des
branchements vers l’intérieur d’un bloc avec les conséquences décrites à la
section 15.
Par exemple, cette construction serait admise :
switch (n)
{ case 1 : …..
if (…) { …..
case 2 : …..
break ;
}
else { …..
case 5 :
default :
}
for (i=0 ; i<5 ; i++)
{ case 6 : …..
}
case 4 :
}

Quant n vaut -6, cette instruction provoque un branchement à l’intérieur du bloc


régit par l’instruction for, la valeur de i étant alors indéfinie !
Il existe toutefois une limitation à un branchement vers l’intérieur d’un bloc régit
par switch, à savoir que la recherche d’étiquette n’est pas effectuée dans les blocs
régis par d’éventuelles instructions switch englobées dans l’instruction switch
concernée. Par exemple :
switch(n)
{ case 1 : …..
switch(p)
{ case 5 : …..
…..
}
…..
}

Si, lors de l’entrée sur le premier switch, n vaut 5, il n’y aura pas branchement sur
le case 5 du deuxième switch, même si aucune étiquette case 5 n’apparaît pas dans
le premier switch ; dans ce dernier cas, il y aura simplement branchement à
l’étiquette default si elle existe ou passage à la suite du switch.

5.4.2 La forme théorique de switch


La syntaxe usuelle présentée à la section 5.2 suppose que l’instruction switch fait
intervenir un bloc apparaissant à la suite de la condition. En fait, la norme
prévoit une syntaxe beaucoup plus simple :

La syntaxe théorique de switch

switch (expression)
instruction
Cela autorise des constructions aussi stupides que (le if est ici légal mais
inutile) :
switch (n)
if (…) { case 1 : …..
case 2 : …..
}
else …..

En combinant cette syntaxe avec la possibilité précédente, on peut aboutir à des


constructions légales telles que :
switch (n)
default : if ( a<b ) { case 1 : ….. /* effectué si n=1 ou si a<b */
case 2 : ….. /* effectué si n=2 ou si a<b */
}
else { case 0 : ….. /* effectué si n=0 ou si a>=b */
}
6. Choix entre if et switch
On démontre qu’il suffit théoriquement de disposer de deux structures de base
(choix et répétition) pour traduire tout programme. Dans ces conditions, le switch
peut apparaître comme une structure redondante par rapport à if. Il n’en reste pas
moins que, dès qu’on a affaire à une énumération de cas, switch peut s’avérer plus
pratique que if et conduire à des programmes plus lisibles. Cependant,
l’instruction switch semble souffrir de limitations sévères, dans la mesure où,
pour l’utiliser, il faut :
• que les conditions de sélection puissent, au bout du compte, porter sur des
valeurs entières ;
• que chaque partie de la sélection soit associée à un nombre pas trop grand de
valeurs pour qu’on puisse les énumérer facilement sous la forme case xxx.
En fait, on peut toujours « préparer le terrain » en initialisant préalablement, par
des instructions de choix, une variable entière avec un nombre restreint de
valeurs entières.
Par exemple, supposons qu’on souhaite effectuer un traitement sur des pièces
cylindriques en fonction de leur diamètre (nombre flottant) :
float diametre ;
…..
if (diametre < 1.5) printf ("hors norme\n" ) ;
if (diametre >= 1.5) && (diametre < 1.7) printf ("second choix\n) ;
if (diametre >= 1.7) && (diametre < 1.8) printf ("premier choix\n) ;
if (diametre >= 1.8) && (diametre < 2.0) printf ("second choix\n) ;
if (diametre >2.0) printf ("hors norme\n") ;

Voici une façon (parmi d’autres) d’utiliser un switch :


float diametre ;
int choix ;
…..
choix = 0 ;
if (diametre >= 1.5) && (diametre < 1.7) choix = 2 ;
if (diametre >= 1.7) && (diametre < 1.8) choix = 1 ;
if (diametre >= 1.8) && (diametre < 2.0) choix = 2 ;
switch (choix)
{ case 1 : printf ("premier choix\n") ;
case 2 : printf ("second choix\n") ;
default : printf ("hors norme\n") ;
}

D’une manière générale, le choix entre ces deux formes d’expression n’est pas
évident. Il dépend étroitement de l’habileté du programmeur et de la nature du
problème à résoudre : plus les valeurs associées aux différentes constantes case
xxx auront un caractère artificiel et moins l’utilisation du switch sera justifiée.
7. Les particularités des boucles en C

7.1 Rappels concernant la programmation structurée


Traditionnellement, en programmation structurée, on distingue trois sortes de
boucles :
• Les boucles dites « tant que » : on y répète des instructions tant qu’une certaine
condition est réalisé[Link] condition, dite « de poursuite », est examinée avant
chaque nouveau tour de boucle.
• Les boucles dites « jusqu’à » : on y répète des instructions jusqu’à ce qu’une
certaine condition soit réalisée. Cette condition, dite « condition d’arrêt » est
examinée après chaque tour de boucle.
• Les boucles dites « avec compteur » : on y répète des instructions en faisant
évoluer une variable particulière nommée compteur depuis une valeur initiale
jusqu’à une valeur finale.
On parle souvent de boucles indéfinies dans les deux premiers cas car le nombre
de tours n’est pas nécessairement connu au moment de l’entrée dans la boucle.
On parle de boucle définie dans le dernier cas, car le nombre de tours de boucle
est alors parfaitement déterminé lors de l’entrée dans la boucle par la valeur
initiale et la valeur finale.
La figure 5-2 présente les organigrammes correspondant à ces trois types de
boucles.
Figure 5-2
Les trois sortes de boucles en programmation structurée
7.2 Les boucles en C
Théoriquement, le langage C dispose de trois instructions évoquant plus ou
moins les trois structures précédentes :
• L’instruction while : elle correspond exactement à une boucle de type « tant
que ».En particulier, la condition est bien examinée avant chaque tour de
boucle.
• L’instruction do … while : elle correspond à une boucle de type « jusqu’à » dans
laquelle on exprime simplement une condition de poursuite au lieu d’une
condition d’arrêt (il s’agit de la condition inverse). La condition est bien
examinée après chaque tour de boucle.
• L’instruction for : malgré son nom, elle apparaît en fait comme un canevas de
type « tant que », à compléter en précisant :
– les actions à réaliser avant d’entrer dans la boucle ;
– les actions à réaliser à la fin de chaque tour ;
– la condition de poursuite.
Certes, elle est souvent utilisée pour programmer une boucle avec compteur
(en complétant le canevas comme il faut), mais il ne s’agit là que d’une
utilisation particulière.
Ces trois instructions pourront s’écarter plus ou moins de l’aspect structuré pour
deux raisons.
La première de ces raisons est que la condition régissant la poursuite d’une
boucle est en fait, comme toute condition en C, une expression quelconque. Elle
peut donc éventuellement réaliser une action après le test de sortie de boucle (cas
de for et while) ou après le test de poursuite (cas de do … while). On aboutira ainsi à
ce qu’on nomme parfois des « boucles à sortie intermédiaire ». Bien comprise,
cette possibilité pourra simplifier les choses ; mal utilisée, elle pourra conduire à
des programmes peu lisibles et peu adaptables.
La seconde des raisons qui fait échapper ces trois instructions à l’aspect structuré
est que, à l’intérieur du corps de la boucle (instructions à répéter), on pourra
trouver des instructions de rupture de séquence, en vue :
• soit de forcer le passage au tour de boucle suivant (instruction continue) ; on
verra que cette possibilité pourra constituer une formulation concise et lisible à
un problème usuel alors même que le pur respect de la programmation
structurée conduirait à une formulation plus lourde ;
• soit de mettre fin prématurément à la boucle ; deux sortes d’instructions
peuvent intervenir :
– break : on met fin à la boucle pour passer simplement à l’instruction
suivante. Là encore, malgré son aspect non structuré, cette possibilité reste
intéressante pour réaliser des boucles à sorties multiples ou pour gérer des
situations extraordinaires qui, en programmation structurée pure,
conduiraient à des formulations plus lourdes.
– goto ou return : on met fin à la boucle en se branchant ailleurs qu’à
l’instruction suivante. Cette fois cette possibilité ne sera conseillée que dans
des circonstances très particulières.
Ici, nous étudierons tout d’abord chacune des trois instructions do … while, while et
for, en montrant leurs ressemblances et leurs différences avec les trois structures
de boucle proposées par la programmation structurée. Puis, après avoir présenté
l’instruction break, nous verrons comment l’exploiter pour créer de nouveaux
schémas de boucle.
8. L’instruction do … while
Comme l’indique la section 7, l’instruction do … while permet de réaliser des
boucles de type « jusqu’à », mais la richesse de la notion d’expression en C peut
amener à la dénaturer quelque peu.

8.1 Syntaxe
L’instruction do … while

do instruction
while (expression) ;
instruction
instruction quelconque :
simple, structurée ou
bloc
expression
expression quelconque Nommée parfois
de type scalaire « expression de
(numérique ou contrôle de la boucle »
pointeur) ou condition de
poursuite

Remarques
1. Notez bien, d’une part la présence de parenthèses autour de l’expression de contrôle de la boucle et
d’autre part, la présence d’un point-virgule à la fin de cette instruction (qui se trouve être la seule ne
faisant pas partie des instructions simples à posséder un point-virgule).
2. Lorsque l’instruction à répéter se limite à une seule instruction simple, n’omettez pas le point-
virgule qui la termine. Ainsi, la syntaxe :
do c = getchar() while ( c != ‘x') ;

est incorrecte. Il faut absolument écrire :


do c = getchar() ; while ( c != ‘x') ;

8.2 Rôle
Cette instruction exécute l’instruction, puis elle évalue l’expression de contrôle
suivant les règles habituelles. Si cette dernière est nulle, elle passe à l’instruction
suivant do … while et l’exécution de la boucle est terminée. Dans le cas contraire
(expression non nulle), on reprend le processus d’exécution de instruction et ainsi
de suite.
L’exécution d’une telle boucle peut prendre fin :
• de manière naturelle : la valeur de l’expression de contrôle est devenue nulle ;
• de manière prématurée : une instruction de rupture de séquence (break, goto ou
return) a été exécutée de l’intérieur vers l’extérieur du corps de boucle.

Il est fréquent de traduire le rôle de do … while par l’organigramme suivant :


Figure 5-3
L’instruction do … while (en l’absence de branchements dans instruction)

On peut alors dire qu’au sens de la programmation structurée, elle réalise une
boucle de type « jusqu’à » (voir § 7.1) dans laquelle la condition de poursuite
serait ! expression. Autrement dit, instruction est répétée jusqu’à ce que l’expression
de contrôle soit fausse. Cette affirmation doit cependant être nuancée pour au
moins deux raisons :
• l’instruction correspondant au corps de boucle peut très bien contenir des
branchements non exprimés par cet organigramme (break, continue, goto) ;
• la notion d’expression, en C, dépasse largement celle de simple condition.

8.3 Exemples d’utilisation


8.3.1 Utilisation naturelle de do … while
Comme indiqué à la section 7, do … while peut servir à programmer une boucle
« jusqu’à », pour peu qu’on se limite à l’arrêt naturel et qu’on n’introduise pas
d’actions dans la condition de poursuite. Voici un exemple naturel d’emploi de do
… while :

Exemple d’utilisation naturelle d’instruction do… while

int main()
{ int n ; donnez un nb >0 : -3
do vous avez fourni -3
{ printf ("donnez un nb >0 : ") ; donnez un nb >0 : -9
scanf ("%d", &n) ; vous avez fourni -9
printf ("vous avez fourni %d\n", n) ; donnez un nb >0 : 12
} vous avez fourni 12
while (n <= 0) ; reponse correcte
printf ("reponse correcte\n") ;
}

8.3.2 Utilisation artificielle d’actions dans la condition


Il est théoriquement possible d’utiliser l’opérateur séquentiel au sein de
l’expression de contrôle pour juxtaposer, au bout du compte, plusieurs
expressions. Ainsi, l’exemple précédent peut également s’écrire :
do { printf ("donnez un nb > 0 : ") ;
scanf ("%d", &n) ;
}
while ( printf("vous avez fourni %d\n", n), n <= 0 ) ;

ou encore :
do printf ("donnez un nb >0 : ") ;
while ( scanf("%d", &n), printf ("vous avez fourni %d\n", n), n <= 0 ) ;

ou même :
do { }
while ( printf ("donnez un nb > 0 : "), scanf ("%d", &n),
printf ("vous avez fourni %d\n", n), n <= 0 ) ;

La dernière formulation utilise un corps vide, ce qui n’empêche nullement la


boucle de « réaliser quelque chose » !
De la même manière, la formulation suivante :
do { } while ( (c=getchar()) != ‘$' ) ;

est équivalente à :
do c = getchar() ; while ( c != ‘$' ) ;

D’une manière générale, on peut dire que le schéma suivant :


do instruction
while (expression_1, expression_2) ;

est équivalent à :
do
{ instruction
expression_1 ;
}
while (expression_2) ;

Cela montre bien que cette possibilité de déplacement d’une expression du corps
de boucle vers la condition ne présente guère d’intérêt en pratique ; elle a même
tendance à obscurcir le programme !

8.3.3 Boucles d’apparence infinie


La construction :
do { } (1) ;

représente une « boucle infinie ». Elle est syntaxiquement correcte, bien qu’elle
ne présente en pratique aucun intérêt. En revanche :
do instruction while (1) ;

pourra présenter un intérêt dans la mesure où il est possible d’en sortir


éventuellement par une instruction break figurant dans instruction. Cette
formulation sera d’ailleurs équivalente à :
while (1) instruction

C’est cette dernière forme que nous exploiterons à la section 14 pour créer des
schémas de boucles à sortie intermédiaire ou à sorties multiples.
9. L’instruction while
Comme vu à la section 7, l’instruction while permet de réaliser des boucles de
type « tant que », mais la richesse de la notion d’expression en C peut la
dénaturer quelque peu.

9.1 Syntaxe
L’instruction while

while (expression)
instruction
instruction
Instruction quelconque :
simple, structurée ou bloc
expression
Expression quelconque de Nommée parfois
type scalaire (numérique ou « expression de contrôle de
pointeur) la boucle » ou condition de
poursuite

Remarque
Notez bien la présence de parenthèses autour de l’expression de contrôle de la boucle. En revanche,
contrairement à ce qui se passe pour do … while, la syntaxe n’impose ici aucun point-virgule de fin.

9.2 Rôle
Cette instruction évalue l’expression de contrôle suivant les règles habituelles. Si
cette dernière est nulle, elle passe à l’instruction suivant while et l’exécution de la
boucle est terminée. Dans le cas contraire (expression non nulle), on exécute
l’instruction gouvernée par while et on reprend le processus d’évaluation de
l’expression et ainsi de suite.
On notera bien qu’une telle boucle peut prendre fin :
• de manière naturelle : la valeur de l’expression de contrôle est devenue nulle ;
• de manière prématurée : une instruction de rupture de séquence (break, goto ou
return) a été exécutée de l’intérieur vers l’extérieur du corps de boucle.
Il est fréquent de traduire le rôle de while par l’organigramme de la figure 5-4
Figure 5-4
L’instruction while (en l’absence de branchements dans instruction)

On peut alors dire qu’au sens de la programmation structurée, elle réalise une
vraie boucle de type « tant que » (voir section 7.1), la condition de poursuite
étant simplement mentionnée dans expression (alors que pour do … while, il fallait
inverser la condition pour aboutir à une vraie boucle de type « jusqu’à »). Cette
affirmation doit cependant être nuancée pour au moins deux raisons :
• l’instruction correspondant au corps de boucle peut très bien contenir des
branchements non exprimés par cet organigramme ;
• la notion d’expression, en C, dépasse largement celle de simple condition.

9.3 Lien entre while et do … while


En théorie de la programmation, on montre qu’une seule structure de boucle
(tant que ou jusqu’à) permet d’écrire n’importe quel programme. Effectivement,
on peut toujours remplacer :
do instruction while (expression) ;

par :
instruction
while (expression) do instruction

mais au prix de la duplication de instruction.


De même, on peut toujours remplacer :
while (expression) do instruction

par :
if (expression) do instruction while (expression) ;

au prix d’un test supplémentaire.

Remarques
1. Lorsque le corps de boucle est vide, do … while et while sont équivalentes :
do {} while (expression) ;
while (expression) {}

Par exemple, ces deux instructions sont équivalentes :


do {} while ( (c=getchar()) != ‘*') ;
while ( (c=getchar()) != ‘*') do {}

2. Contrairement à ce qui se produit pour do … while, si l’expression de contrôle est nulle lors de
l’entrée dans la boucle, l’instruction constituant le corps de boucle n’est pas exécutée.
De même, la valeur de l’expression de contrôle doit être définie avant l’entrée dans la boucle.
Dans le cas de do … while, elle peut ne pas l’être pour peu que les instructions gouvernées par la
boucle lui donnent effectivement une valeur.

9.4 Exemples d’utilisation


9.4.1 Utilisation naturelle de while
Voici un programme qui demande à l’utilisateur de fournir des nombres entiers
jusqu’à ce que la somme de ces nombres atteigne ou dépasse la valeur 100 :
Exemple d’instruction while

int main()
{
int n, som ; donnez un nombre : 15
som = 0 ; donnez un nombre : 25
while (som<100) donnez un nombre : 12
{ printf ("donnez un nombre : ") ; donnez un nombre : 60
somme obtenue : 112
scanf ("%d", &n) ;
som += n ;
}
printf ("somme obtenue : %d", som) ;
}

9.4.2 Utilisation artificielle d’actions dans la condition


Comme dans le cas de do … while, il est possible d’utiliser pour while une
expression de contrôle faisant appel à l’opérateur séquentiel pour « juxtaposer »
au bout du compte plusieurs expressions. Par exemple :
while (exp1, exp2, exp3) instruction

Dans ce cas, il faut bien voir que toutes les expressions (exp1, exp2 et exp3) sont
évaluées avant d’effectuer le test de poursuite qui, quant à lui, ne porte que sur la
valeur de la dernière expression (ici exp3). Les autres expressions n’interviennent
en fait que par les actions qu’elles provoquent.
Ainsi, l’instruction précédente n’est équivalente à aucune de ces formulations :
while (exp1) exp1 ; while(exp3)
{ exp2 ; exp2 ; { exp1 ;
exp3 ; while (exp3) exp2 ;
instruction instruction instruction
} }

ni même à celle-ci :
do instruction
while (exp1, exp2, exp3) ;

En fait, elle correspond à une boucle à sortie intermédiaire dont nous reparlerons
à la section 14.1 :
while (1)
{ exp1 ;
exp2 ;
if (!exp3) break ;
instruction
}

En ce sens, et contrairement à ce qui passait pour do … while (voir section 8.3.2),


une telle construction présente un intérêt manifeste.

9.4.3 Boucles d’apparence infinie


La construction :
while (1) { } /* ou encore while (1) ; */

représente une boucle infinie. Elle est syntaxiquement correcte, bien qu’elle ne
présente en pratique aucun intérêt. En revanche :
while (1) instruction

pourra présenter un intérêt dans la mesure où il est éventuellement possible d’en


sortir par une instruction break. Nous l’exploiterons à la section 14 pour créer de
nouveaux schémas de boucle à sortie intermédiaire ou à sorties multiples.
Rappelons que, comme indiqué à la section 8.3.3, cette forme est équivalente à :
do {} while (1) ;
10. L’instruction for
Nous avons vu à la section 7 que l’instruction for permet de réaliser des boucles
avec compteur mais sa structure même, jointe à la richesse de la notion
d’expression en C peut la dénaturer très profondément.

10.1 Introduction
La principale vocation de l’instruction for est de permettre de programmer ce que
l’on appelle des boucles avec compteur (voir section 7.1) dans lesquelles une
variable particulière dite variable de contrôle ou compteur sert à compter les
tours de boucle. En voici un exemple, dans lequel on place la valeur 0 dans les
différents éléments d’un tableau de 10 entiers, le compteur i évoluant ici de 0 à
9 :
int t[10] ;
int i ;
…..
for (i=0 ; i<10 ; i++) t[i]= 0 ;

Cependant, par sa nature même, for est en réalité une boucle de type « tant que »
(analogue à while), dans laquelle on peut préciser, par le biais d’expressions
appropriées :
• les actions à réaliser avant l’entrée dans la boucle ; dans notre exemple, nous
n’y trouvons qu’une seule initialisation (i=0) mais rien n’interdit à l’instruction
for d’en introduire plusieurs, voire d’introduire d’autres actions que des
affectations ;
• les actions à réaliser à la fin de chaque tour ; dans notre exemple, il s’agit de i++
mais, là encore, rien n’interdit d’introduire plusieurs actions ;
• la condition de poursuite ; dans notre exemple, il s’agit de i<10.
Notez que nous parlons d’actions bien que celles-ci soient provoquées par des
expressions. Mais une expression qui ne réaliserait aucune action n’aurait aucun
intérêt à ce niveau.

10.2 Syntaxe
L’instruction for
for ( [ expression_1 ] ; [ expression_2 ] ; [ expression_3 ] )
instruction
instruction
Instruction
quelconque : simple,
structurée ou bloc
expresion_1 et Expressions de type Pas nécessairement
expression_3
quelconque scalaire ici
expression_2
Expression de type Si cette expression est
scalaire (numérique ou omise, tout se passe
pointeur) comme si elle avait
pour valeur 1 (vrai)
N.B : les crochets ([ et ]) signifient ici que leur contenu est facultatif.

10.3 Rôle
Cette instruction réalise les étapes suivantes :
1. On évalue expression_1 si elle est présente.
2. On évalue expression_2 (si elle est absente, cela revient à lui attribuer la valeur
1). Si sa valeur est nulle, l’exécution de la boucle for est terminée. Dans le cas
contraire, on exécute l’instruction et on évalue expression_3 si cette dernière est
présente.
3. On recommence le processus à l’étape 2.
On notera bien qu’une telle boucle peut prendre fin :
• de manière naturelle : la valeur de expression_2 est devenue nulle ;
• de manière prématurée : une instruction de rupture de séquence (break, goto ou
return) a été exécutée de l’intérieur vers l’extérieur du corps de boucle formé
par instruction.
Il est fréquent de traduire le rôle de for par l’organigramme de la figure 5-5 :
Figure 5-5
L’instruction for (en l’absence de branchements dans instruction)
Toutefois, on n’oubliera pas qu’en langage C, la notion d’expression dépasse
celle de simple condition. Aussi, l’instruction correspondant au corps de boucle
peut très bien contenir des branchements non exprimés par cet organigramme !
Et même en l’absence de branchements, on n’aboutira à une véritable « boucle
avec compteur » (au sens de la programmation structurée) que lorsque :
• expression_1 se réduit à une affectation d’une valeur initiale à une variable
compteur ;
• expression_2 se réduit à une comparaison entre ce compteur et une valeur limite ;
• expression_3 se réduit à une incrémentation du compteur.

10.4 Lien entre for et while


L’instruction for est une instruction while qui s’ignore. D’ailleurs, comme le
précise formellement la norme ANSI, l’instruction :
for (expression_1 ; expression_2 ; expression_3) instruction

est équivalente, en l’absence de branchements dans instruction, à :


expression_1 ;
while (expression-2)
{ instruction
expression-3 ;
}
Avec notre exemple d’introduction, cela conduirait à :
i = 0 ;
while (i < 10)
{ t[i] = 0 ;
i++ ;
}

10.5 Commentaires
Les expressions 1 et 3 n’ont d’intérêt que pour leur action
Manifestement, les valeurs de expression_1 ou de expression_3 ne jouent aucun rôle
dans le déroulement de l’instruction for. Ainsi, cette construction (artificielle) :
for (2*i ; i<10 ; i>5) instruction

n’a pas plus d’intérêt que :


for ( ; i<10 ; ) instruction

Chacune des trois expressions est facultative


Ainsi, ces trois constructions sont équivalentes :
for (i=1 ; i<=5 ; i++) /* construction la plus usuelle et recommandée */
{ instructions
}
i = 1 ;
for ( ; i<=5 ; i++) /* initialisation absente de for */
{ instructions
}

i = 1 ; /* incrémentation de fin de boucle absente */


for ( ; i<=5 ;)
{ instructions
i++ ; /* attention à l'emplacement de cette incrémentation */
} /* dans le cas où i est utilisé dans instructions */

Attention aux compteurs à valeurs réelles


L’instruction for est souvent utilisée pour programmer une boucle avec compteur.
Certains langages imposent à de telles boucles d’utiliser des compteurs de type
entier, ce qui évite tout risque lié aux approximations de valeurs flottantes. En C,
aucune limitation de ce genre n’existe et pour cause, la notion de compteur n’y
apparaît même pas ! Il est tout à fait envisageable d’y écrire des choses telles
que :
float x ;
…..
for (x=0 ; x<1.0 ; x+=0.1) /* on pourrait, par exemple, ici, calculer la valeur */
{ ….. } /* d'une fonction, pour les différentes valeurs de x */
Or cette construction n’est pas portable car, suivant les implémentations, elle
conduira :
• à 11 tours de boucle, x prenant les valeurs : 0 ; 0,1 (environ) ; 0,2 (environ)…
0,9 (environ) et environ 1, si le résultat des 10 premières incrémentations de x
conduit à une valeur (voisine de 1) légèrement inférieure à 1 ;
• à 10 tours de boucle seulement, x prenant les valeurs 0 ; 0,1 (environ) ; 0,2
(environ)… et 0,9 (environ) si le résultat des 10 premières incrémentations de x
conduit à une valeur (voisine de 1) légèrement supérieure à 1.
En fait, il est plus judicieux de procéder ainsi :
float x ; int i ;
…..
for (i=0, x=0 ; i<10 ; i++, x+=0.1 )
{ ….. }

ou encore ainsi :
for (i=0 ; i<10 ; i++ )
{ x = i * 0.1 ;
…..
}

10.6 Exemples d’utilisation


10.6.1 Exemples liés à la généralité de la notion d’expression en C
Comme expression_1 n’intervient que par son action, elle peut être indifféremment
placée dans for ou avant (voir le schéma section 10.4). Là encore, il est possible
d’utiliser l’opérateur pour juxtaposer, au bout du compte, plusieurs expressions.
Dans ce cas, une instruction telle que :
for (j=1, k=5, i=0 ; … ; … )

est équivalente à :
j=1 ; k=5 ;
for ( i=0 … ; … )

ou encore à :
j=1 ; k=5 ; i=0 ;
for ( ; … ; …)

D’une manière générale, il ne faut user de cette liberté que pour placer
éventuellement plusieurs initialisations dans for, à condition que ces dernières
soient logiquement liées entre elles.
Une remarque similaire s’applique à expression_3, qui n’intervient que par son
action et qui peut donc être éventuellement glissée à la fin des instructions à
répéter. Ainsi :
for ( i=1 ; i <= 5 ; printf("fin de tour"), i++ ) { instructions }

est équivalent à :
for ( i=1 ; i<=5 ; i++ )
{ instructions
printf ("fin de tour") ;
}

En revanche, une telle remarque ne s’applique pas à expression_2. En effet,


comme le montre le schéma de la section 10.4, lorsque expression_2 est formée de
deux expressions exp_2a et exp_2b :
for (expression_1 ; exp_2a, exp_2b ; expression_3 ) { instructions }

elle est alors équivalente à :


expression_1 ;
while (exp_2a, exp_2b)
{ instructions
expression_3 ;
}

c’est-à-dire en fait à :
expression_1 ;
while (1)
{ exp_2a ;
if (!exp_2b) break ;
instructions
expression_3 ;
}

Cette construction correspond à une boucle à sortie intermédiaire, exposée en


détail à la section 14.1. Cependant, il n’y a aucun intérêt à utiliser for dans ce
cas, while convenant beaucoup mieux.
Notez bien, à ce propos, que :
for ( i=1, printf("on commence") ; printf("debut de tour"), i<=5 ; i++)
{ instructions }

n’est pas équivalent à :


printf ("on commence") ;
for ( i=1 ; i<=5 ; i++ )
{ printf ("debut de tour") ;
instructions
}

En effet, dans la première construction, le message debut de tour est affiché après
le dernier tour tandis qu’il ne l’est pas dans la seconde construction.

10.6.2 Boucles d’apparence infinie


La construction :
for ( ; ; ;) {} /* ou éventuellement for (; ; ;) ; */

représente une boucle infinie. Elle est syntaxiquement correcte, bien qu’elle ne
présente en pratique aucun intérêt. En revanche, la construction suivante :
for ( ; ; ;) instruction

pourra présenter un intérêt dans la mesure où il est possible d’en sortir


éventuellement par une instruction break. Néanmoins, la forme équivalente
suivante :
while (1) instruction

est préférable car nettement plus explicite. C’est d’ailleurs cette dernière que
nous exploiterons à la section 14 pour créer de nouveaux schémas de boucle à
sortie intermédiaire ou à sorties multiples.
11. Conseils d’utilisation des différents types de
boucles
Comme nous l’avons déjà mentionné, en théorie de la programmation, on
montre qu’il suffit d’un seul type de boucle pour écrire n’importe quel
programme. Dans ces conditions, on pourrait se contenter de n’utiliser qu’une
seule des trois instructions while, do … while et for. Néanmoins, suivant la nature du
problème à résoudre, telle ou telle forme de boucle s’avérera plus adaptée en
conduisant à des formulations plus simples et plus naturelles. Ici, nous
examinons quelques critères de choix de la bonne instruction, ainsi que quelques
règles de bon usage.
Notez qu’en C, comme nous le verrons à la section 14, il sera possible, si on le
désire, de compléter les trois schémas de boucle de la programmation structurée
par des schémas mieux adaptés aux situations dites de boucle à sortie
intermédiaire ou de boucle à sorties multiples. Leur usage ne contredira pas ce
qui exposé ici, il le complétera simplement.

11.1 Boucle définie


Tout d’abord, si le problème conduit à une boucle définie, c’est-à-dire si le
nombre de tours de boucle est connu au moment de l’entrée dans la boucle, il est
plus naturel d’utiliser une boucle avec compteur, et donc de faire appel à for sous
la forme :
for (i=debut ; i<=fin ; i++) …..

Les valeurs de debut et de fin pourront tout à fait résulter d’un calcul préalable,
autrement dit être des variables et non obligatoirement des constantes. On notera
que si la valeur de debut est strictement supérieure à celle de fin, on n’effectuera
simplement aucun tour de boucle…
On évitera la modification de la valeur du compteur à l’intérieur de la boucle. En
effet, une telle action revient à contredire le nombre de tours annoncé par
l’instruction for. Outre le fait qu’elle rende difficile la lecture du programme, elle
présente l’énorme risque de conduire à des boucles infinies… Quoi qu’il en soit,
si vraiment le problème semble insoluble sans ce genre d’action sur le compteur,
c’est que probablement il ne doit pas s’exprimer sous la forme d’une boucle
définie, mais d’une boucle indéfinie (while ou do … while). Signalons que cette
contrainte relative à l’absence de modification du compteur n’est
malheureusement pas imposable en C, pour la bonne raison que la notion de
compteur est absente de l’instruction for. Une telle contrainte existe dans la
plupart des autres langages.
De manière similaire, si fin est une variable, on évitera d’en modifier la valeur à
l’intérieur de la boucle. S’il s’agit d’une expression, on évitera de modifier la
valeur des variables qui y interviennent. Signalons que, dans la plupart des autres
langages, la valeur de l’expression fin n’est calculée qu’une seule fois, avant le
premier tour (éventuel) de boucle, ce qui évite les risques évoqués.
Dans la mesure du possible, on limitera expression_1 à la seule initialisation du
compteur ou, à la rigueur, à d’éventuelles initialisations fortement liées à celles
du compteur. La même remarque vaut pour expression_3 qu’on limitera à
l’incrémentation du compteur ou, à la rigueur, à l’incrémentation de variables
devant s’incrémenter en parallèle avec lui. Dans cet esprit, la construction :
for (i=1, j=1 ; i<=10 ; i++, j++) …..

est acceptable à condition que la valeur de j ne soit pas modifiée à l’intérieur de


la boucle (pas plus que ne doit l’être celle de i !).
On évitera l’exploitation abusive de l’opérateur séquentiel à l’intérieur des trois
expressions régissant la boucle. Par exemple, plutôt que :

for (printf ("on commence\n"), i=1 ; i<10 ; i++) …..

on préférera tout simplement :


printf ("on commence\n") ;
for (i=1 ; i<10 ; i++) …..

11.2 Boucle indéfinie


S’il n’est pas possible de connaître le nombre de tours avant l’entrée dans la
boucle, on fera appel à l’une des instructions while ou do … while. Le choix entre
les deux pourra être guidé par les considérations suivantes :
• l’instruction while peut ne faire aucun tour de boucle, tandis que do … while en
fait toujours au moins un ;
• la condition de poursuite de la boucle doit être définie avant l’entrée dans while,
alors qu’elle peut l’être au cours du premier tour dans le cas de do … while ; ce
point est crucial si la condition de poursuite découle d’une valeur dépendant
des informations lues en cours de boucle…
Si le problème se prête indifféremment à l’utilisation de while ou de do … while, il
semble préférable d’employer while, qui a le mérite de faciliter la relecture du
programme, en présentant d’emblée la condition de poursuite.
12. L’instruction break
Couramment utilisée avec l’instruction switch, l’instruction permet
break
également de provoquer la fin prématurée d’une boucle.

12.1 syntaxe et rôle


L’instruction break

break ;
Cette instruction peut être utilisée dans deux contextes différents, avec des rôles
semblables.
• Dans une instruction switch, elle met fin à l’exécution de l’instruction. Elle est
quasiment indispensable pour faire du simple aiguillage induit par switch un
véritable choix multiple (voir section 5).
• Dans une instruction de boucle (for, while ou do … while), elle provoque la sortie
(prématurée) de la boucle.
L’usage de break est naturellement bien plus répandu dans la première situation
que dans la seconde.
En cas d’imbrication d’instructions de boucles ou d’instructions switch, break ne
met fin qu’à l’instruction la plus interne le contenant. On notera que l’instruction
structurée if n’est pas concernée par break.

12.2 Exemple d’utilisation


Voici un exemple d’école montrant le fonctionnement de break à l’intérieur d’une
boucle :
Exemple d’instruction break dans une boucle for

int main() debut tour 1


{ bonjour
int i ; fin tour 1
for ( i=1 ; i<=10 ; i++ ) debut tour 2
{ printf ("debut tour %d\n", i) ; bonjour
printf ("bonjour\n") ; fin tour 2
if ( i==3 ) break ; debut tour 3
printf ("fin tour %d\n", i) ; bonjour
} apres la boucle
printf ("apres la boucle") ;
}

On trouvera d’autres exemples d’utilisations, plus réalistes, à la section 14, où


break est utilisé pour réaliser des boucles à sortie intermédiaire ou des boucles à
sorties multiples.

12.3 Commentaires
12.3.1 L’instruction structurée if n’est pas concernée par break
Toute utilisation de break en dehors de switch, for, while ou do … while conduit à une
erreur de compilation. On notera bien cependant que les constructions suivantes
sont syntaxiquement correctes :
for (…..)
{ …..
if (…) { …..
break ; /* met fin à for */
}
…..
}
switch( …..)
{ …..
if (…..) { …..
case xxx : break ; /* met fin à switch */
…..
}
}

Cependant, l’instruction concernée par break n’est pas l’instruction if mais bel et
bien l’instruction for englobante dans le premier cas, l’instruction switch
englobante dans le second.

12.3.2 break ne fait sortir que du niveau le plus interne


Considérons cet exemple :
for (…..)
{ …..
while (…)
{ …..
if (…) break ; /* on met fin au while */
…..
} /* fin while */
….. /* pour venir ici */
} /* fin for */
….. /* et non là */

Si on souhaite provoquer la sortie du for depuis l’intérieur du while, on voit qu’il


est impossible d’y parvenir directement. Il faut :
• soit passer par l’intermédiaire d’indicateurs booléens (de type vrai/faux) qu’on
positionne dans while et qu’il faudra tester, peut-être plusieurs fois, dans for ;
• soit utiliser l’instruction goto (voir section 15), en procédant ainsi :
for (…..)
{ …..
while (…)
{ …..
if (…) goto fin ; /* on met fin au while */
…..
} /* fin while */
…..
} /* fin for */
fin : ….. /* pour venir là */
13. L’instruction continue
Alors que break permet de mettre fin prématurément à une boucle, continue permet
de forcer le passage au tour suivant.

13.1 Syntaxe et rôle


L’instruction continue

continue ;
Cette instruction ne s’utilise que dans une boucle et son exécution force
simplement le passage au tour de boucle suivant, en ignorant les instructions
situées entre continue et la fin de la boucle. Elle ne concerne que la boucle de
niveau le plus interne la contenant.

13.2 Exemples d’utilisation


Voici deux exemples d’utilisation de continue, l’un avec do … while, l’autre avec
for :

Exemple d'instruction continue dans une boucle do … while

int main()
{ donnez un nb>0 : 4
int n ; son carre est : 16
do donnez un nb>0 : -5
{ printf ("donnez un nb>0 : ") ; svp >0
scanf ("%d", &n) ; donnez un nb>0 : 2
if (n<0) { printf ("svp >0\n") ; son carre est : 4
continue ; donnez un nb>0 : 0
} son carre est : 0
printf ("son carre est : %d\n", n*n) ;
}
while(n) ;
}

Exemple d’instruction continue dans une boucle for

int main()
{ debut tour 1
int i ; debut tour 2
for ( i=1 ; i<=5 ; i++ ) debut tour 3
{ printf ("debut tour %d\n", i) ; debut tour 4
if (i<4) continue ; bonjour
printf ("bonjour\n") ; debut tour 5
} bonjour
}

13.3 Commentaires
13.3.1 S’il fallait remplacer continue par un goto
Pour être plus précis, on peut dire (comme l’exprime d’ailleurs formellement la
norme ANSI) que, dans chacun des trois cas suivants, le rôle de continue est
parfaitement équivalent à un branchement à l’étiquette suite, située à la suite de
la dernière instruction régie par la boucle, c’est-à-dire, en fait, devant une
instruction vide :
while (…)
{ …..
suite : ;
}
do
{ …..
suite : ;
}
while (…) ;
for (…)
{ …..
suite : ;
}

On notera que, dans le cas de :


for (expression_1 ; expression_2 ; expression_3) instruction

l’instruction continue provoque bien un branchement sur l’évaluation de


expression_3 et non après. Ainsi, si l’on remplaçait for par sa forme équivalente
avec while, il faudrait placer l’étiquette suite ainsi :
expression_1 ;
while (expression_2)
{ instruction
suite : expression_3 ;
}

C’est ce que l’on constate en examinant le deuxième exemple de la section 13.2.


Après exécution de continue, il y a bien incrémentation de la valeur de i.

13.3.2 Seules les boucles sont concernées par continue


Toute utilisation de continue en dehors d’une boucle conduit à une erreur de
compilation. On notera bien cependant que la construction suivante est
syntaxiquement correcte :
for (…)
{ …..
if (…) { …..
continue ; /* passe au tour suivant du for */
…..
}
…..
}

13.3.3 L’instruction continue ne concerne que la boucle la plus


interne
Considérons cet exemple :
for (…)
{ …..
while (…)
{ …..
if (…) continue ; /* on passe au tour suivant du while */
….. /* et non au tour suivant du for */
}
…..
}

Si on souhaite provoquer le passage au tour suivant du for, on voit qu’il est


impossible d’y parvenir directement. On peut :
• utiliser break pour sortie de while. Il faudra alors disposer d’un mécanisme
approprié pour sauter à la suite de la dernière instruction du for. Que ce soit par
continue ou par goto, il faudra déclencher ce mécanisme de façon conditionnelle,
par exemple en testant un booléen qu’on aura positionné juste avant le break…
• utiliser goto (voir plus loin, section 15) en procédant ainsi :
for (…)
{ …..
while (…)
{ …..
if (…) goto fin ; /* on passe au tour suivant du for */
…..
}
…..
fin : ; /* en se branchant à une instruction vide */
} /* qui est la dernière du bloc régi par for */
14. Quelques schémas de boucles utiles
On éprouve parfois le besoin de disposer de schémas plus élaborés que les trois
schémas de base offerts par la programmation structurée. Face à un tel besoin, on
peut adopter deux attitudes opposées :
• chercher à tout prix à se limiter aux schémas de base ; c’est toujours possible
puisqu’en théorie, un seul type de boucle est suffisant pour traiter tout
problème. On notera cependant qu’en C, certaines utilisations des instructions
de boucle conduisent déjà à s’écarter des schémas de base (voir sections 8.3.2,
8.3.3, 9.4.2, 9.4.3 et 10.6) ;
• essayer de profiter de la liberté offerte par le langage C, notamment par
l’utilisation d’instructions de rupture de séquence à l’intérieur des boucles.
Aucune de ces deux attitudes n’est vraiment meilleure que l’autre. La première
peut conduire à une complexification artificielle des programmes, notamment
par un recours à des booléens qu’il est nécessaire de tester fréquemment. En
abusant de la seconde, on peut retrouver les structures (ou plutôt les absences de
structures) inextricables évoquant les « plats de spaghettis » et dont la
programmation structurée nous avait heureusement débarrassés.
Ici, nous examinerons quelques situations usuelles et nous verrons comment
faire un usage modéré des possibilités offertes par le C, en particulier de
l’instruction break.

14.1 Boucle à sortie intermédiaire


Il est très fréquent de se trouver face à un besoin de ce genre :
Figure 5-6
Boucle à sortie intermédiaire (1)
En programmation structurée pure, il est nécessaire d’utiliser une variable
auxiliaire booléenne, ce qui conduit au schéma de la figure 5-7 :
Figure 5-7
Boucle à sortie intermédiaire (2)
Voyons comment, d’une manière générale, se programment ces schémas en C,
avant d’en examiner des cas particuliers.

14.1.1 Cas général


En C, on peut indifféremment programmer les deux schémas précédents, par
exemple :
Les deux façons de programmer une boucle à sortie intermédiaire

while (1) sortie = 0 ;


{ Instructions_1 do
if (Condition) break ; { Instructions_1
Instructions_2 if (Condition) sortie = 1 ;
} else { Instructions_2
}
}
while (sortie)

Avec des commentaires appropriés, la première solution peut être nettement plus
lisible :
while (1) /* sortie en cours de boucle quand Condition sera réalisée */
{ Instructions_1
if (Condition) break ;
Instructions_2
} /* fin répétition jusqu'à condition */

Remarques
1. On pourrait penser à do … while aussi bien qu’à while :
do
{ Instructions_1
if (Condition) break ;
Instructions_2
}
while (1) ;

Les deux formulations sont ici équivalentes. Cependant, while paraît plus lisible, dans la mesure où
la répétition infinie apparaît immédiatement à la relecture du programme. Toutefois, là encore, des
commentaires appropriés peuvent améliorer les choses. Quoi qu’il en soit, il est bon de s’astreindre
à utiliser toujours la même formulation dans l’ensemble d’un même programme.
2. On pourrait remplacer while (1) par :
for ( ; ;)

Mais cela n’améliore en rien la lisibilité du programme et nous conseillons de limiter l’usage de
for aux vraies boucles avec compteur.

14.1.2 Cas particuliers


Lorsque, dans les schémas précédents, instructions_1 représente une instruction
simple, c’est-à-dire une instruction de la forme :
expression-1 ;

on peut envisager de l’associer à la condition du while :


while (expression_1, !condition)
{ instructions_2
}

Par exemple, on pourra préférer :


while (c=getchar(), c != ‘\n') { instruction } /* qui peut se condenser en : */
/* while ((c=getchar())!= ‘\n') */

à :
while (1)
{ c = getchar () ;
if (c == ‘\n') break ;
instruction
}

En revanche, dès que instructions représente plus d’une instruction simple, il est
préférable de conserver le schéma général. Ainsi, à cette construction :
while (printf ("donnez un nombre\n"), scanf ("%d", &n), n>0)
{ /* instructions de traitement de la valeur n */
}

on préférera celle-ci :
while (1)
{ printf ("donnez un nombre\n") ;
scanf ("%d", &n) ;
if (n <= 0) break ;
/* instructions de traitement de la valeur n */
}

14.2 Boucles à sorties multiples


Voici une autre situation fréquente dans laquelle le nombre de conditions (ici 3)
peut être quelconque :
Figure 5-8
Boucle à sortie multiple
14.2.1 Cas général
Si on ne se préoccupe pas de l’aspect non (totalement) structuré de ce schéma,
on peut le programmer textuellement en C :
while (1) /* trois sorties intermédiaires : Cond_1, Cond_2 et Cond_3 */
{ Instructions_1
if (Cond_1) break ;
Instructions_2
if (Cond_2) break ;
Instructions_3
if (Cond_3) break ;
Instructions_4
}

Une solution parfaitement structurée telle que la suivante serait manifestement


moins lisible :
sortie = 0 ;
do
{ Instructions_1
if (Cond_1) sortie = 1 ;
if (! sortie) { Instructions_2 }
if (Cond_2) sortie = 1 ;
if (! sortie) { Instructions_3 }
if (Cond_3) sortie = 1 ;
if (! sortie) { Instructions_4 }
}
while (! sortie)

14.2.2 Quand il existe une condition principale et des conditions


secondaires
Le schéma précédent et les diverses façons de le programmer en C placent les
différentes sorties de boucle sur un même plan. Dans certains problèmes, on
pourra avoir affaire à une sortie principale correspondant à un déroulement
relativement naturel du programme et à une ou plusieurs sorties à caractère plus
exceptionnel.
Dans ce cas, on pourra avoir intérêt, ne serait-ce que pour des questions de
lisibilité, à mettre en évidence cette condition principale, en procédant ainsi :
while (Condition_principale) /* boucle tant que Condition_principale avec */
/* sorties intermédiaires Cond_sec1 et Cond_sec2 */
{ Instructions_1
if (Cond_sec1) break ;
Instructions_2
if (Cond_sec2) break ;
Instructions_3
}
15. L’instruction goto et les étiquettes

15.1 Les étiquettes


Toute instruction exécutable peut être précédée d’une étiquette, c’est-à-dire d’un
identificateur suivi de deux-points (avec ou sans espaces de part et d’autre de ces
deux-points).
Voici un exemple :
trait : if (…) { …..
suite : b = a ;
}
boucle : for (…) { …..
ici : while (…) { ….. }
}

Ici, trait est une étiquette pour une instruction if, suite est une étiquette pour une
instruction expression, boucle est une étiquette pour une instruction for et, enfin,
ici est une étiquette pour une instruction while.

Remarques
1. Il existe une deuxième sorte d’étiquette, de la forme case xxx ; elle peut, elle aussi, précéder
n’importe quelle instruction exécutable mais elle n’est utilisée qu’à l’intérieur d’une instruction
switch.

2. Les étiquettes décrites ici ne peuvent être utilisées que par goto. Il est possible d’introduire dans un
programme une étiquette qui n’est pas utilisée. La norme ne précise pas si le compilateur doit
fournir un diagnostic dans ce cas : en général, on obtient un message d’avertissement. Cette
possibilité reste déconseillée, dans la mesure où certains compilateurs peuvent en tenir compte pour
supprimer certaines optimisations de boucles…
3. À l’intérieur d’une même portée, un identificateur d’étiquette peut être identique à un identificateur
d’une autre sorte (variable, type, fonction…) comme dans :
int fin ; /* variable entière nommée fin */
…..
fin : ….. /* étiquette nommée fin : correct */

On traduit souvent cela en disant que les étiquettes disposent d’un espace de noms séparé des
espaces de noms des autres identificateurs.

15.2 Syntaxe et rôle


L’instruction goto
goto etiquette ;
etiquette
Identificateur quelconque devant exister à l’intérieur de la
fonction où apparaît l’instruction goto

L’exécution de l’instruction goto entraîne le branchement à l’instruction portant


l’étiquette indiquée.
Contrairement aux étiquettes relatives à une instruction switch, la norme n’impose
aucune contrainte sur les emplacements relatifs de goto et de l’instruction portant
l’étiquette, hormis le fait qu’ils doivent figurer dans le corps de la même
fonction. Cette liberté peut avoir parfois des conséquences fâcheuses (voir
section 15.3.3).

15.3 Exemples et commentaires


Nous vous proposons deux exemples fort différents d’utilisation de goto : le
premier, peu usité, où l’on reste à l’intérieur d’un même bloc ; le second, plus
répandu, où l’on réalise un branchement vers l’extérieur d’un bloc afin de traiter
une circonstance exceptionnelle. Nous examinerons ensuite la situation, fort
déconseillée, de branchement de l’extérieur d’un bloc vers l’intérieur. Nous
terminerons par quelques conseils.

15.3.1 goto à l’intérieur d’un même bloc


Voici une autre façon de programmer le premier exemple section 13.2 :
do
{ printf ("donnez un nb>0 : ") ;
scanf ("%d", &n) ;
if (n<0) { printf ("svp >0\n") ;
goto suite ;
}
printf ("son carre est : %d\n", n*n) ;
suite : ;
}
while(n)

Bien entendu, il s’agit là d’un exemple d’école, dans la mesure où :


• l’usage de continue, comme dans l’exemple initial, est nettement plus adapté à la
situation ;
• tant qu’il s’agit d’un branchement à l’intérieur d’une même boucle, l’usage de
goto est rarement justifié ; la plupart du temps en effet, une instruction if
appropriée conviendra mieux.
15.3.2 Pour traiter une circonstance exceptionnelle
Considérons ces deux exemples :
for (…)
{ …..
if (…) goto erreur ;
…..
}
…..
erreur : …..
exit (-1) ;

for (…)
{ …..
while (…)
{ …..
if (…) goto erreur ;
…..
}
…..
}
…..
erreur : …..

Ici, l’usage de goto peut se justifier si le branchement à erreur n’a lieu que dans
des circonstances très particulières qui compromettent le bon déroulement de la
suite.
On notera qu’en général break ne serait pas facilement utilisable dans ce type de
situation.

15.3.3 Branchement de l’extérieur d’un bloc vers l’intérieur


Voici des exemples de constructions admises, bien que fortement déconseillées :
goto suite ;
…..
if (…) { …..
suite : …..
…..
}
else …..
…..
goto suite ;
…..
do
{ …..
scanf ("…", &n) ;
suite : …..
}
while (n != 0) ;
goto suite ;
…..
for (i=0 ; i<10 ; i++)
{ int n = 15 ;
…..
suite : …..
…..
}

La première construction est certainement la moins risquée des trois. Dans la


deuxième, on voit qu’on entre dans la boucle après la lecture de n ; le test de
poursuite portera donc, au mieux, sur une précédente valeur de n, au pire sur une
valeur imprévisible. Enfin, dans la troisième construction, le même problème se
pose pour la valeur de i (ancienne valeur ou valeur imprévisible). Mais de plus,
la variable n n’aura pas été initialisée : en effet, la norme insiste largement sur le
fait que l’initialisation des variables d’un bloc n’a lieu que lors de l’entrée
normale… Malgré tout, l’allocation mémoire aura bien eu lieu, mais le contenu
de n sera tout simplement imprévisible.

Remarque
Dans beaucoup d’autres langages que le C, les constructions précédentes sont formellement interdites.

15.3.4 Intérêt de goto


En général, on recommande d’utiliser goto pour faciliter la programmation des
traitements de situations exceptionnelles dont la prise en compte au niveau de
l’algorithmique de base, risquerait d’obscurcir le programme : anomalie
d’entrée-sortie, échec d’une allocation mémoire… La section 15.3.2, expose
cette situation.
Cependant, même dans ce cas, il est fréquent qu’après un traitement succinct, on
aboutisse à un arrêt du programme. L’utilisation de goto entre alors en
concurrence avec l’appel d’une fonction de terminaison. Ainsi, le premier
exemple de la section 15.3.2 peut être avantageusement remplacé par :
for (…)
{ …..
if (…) erreur (…) ;
…..
}
…..
void erreur (…)
{ …..
exit (-1) ;
}

Il est même possible de paramétrer le traitement à l’aide d’arguments transmis à


erreur.

15.3.5 Limitations de goto


Par sa nature même, l’instruction goto ne peut provoquer de branchement qu’à
l’intérieur du bloc régi par une fonction. Or, de même qu’on peut avoir besoin de
sortir d’une imbrication d’instructions structurées, on peut avoir besoin de sortir
d’une imbrication d’appels de fonctions. Plus généralement, de même qu’on peut
se brancher d’un point à un autre d’une même fonction, on peut souhaiter se
brancher d’un point d’un programme à un autre point situé dans une portée
différente (dans une autre fonction, voire dans un autre fichier source).
Les fonctions standards setjmp et longjmp, étudiées au chapitre 25, offriront une
solution à ce problème.

1. En revanche, N sera bien une expression constante en C++.


6
Les tableaux

Comme tous les langages évolués, C permet de définir et d’utiliser des tableaux.
Rappelons qu’un tableau est un ensemble d’éléments de même type désignés par
un identificateur unique ; chaque élément y est repéré par une valeur entière
nommée indice indiquant sa position au sein de l’ensemble, de façon analogue à
ce que l’on fait pour un vecteur en mathématique.
Comme dans la plupart des langages récents, la notion de tableau à plusieurs
indices est mise en œuvre par « composition » de la notion de tableau : on parle
de tableaux dont les éléments sont eux-mêmes des tableaux, ou encore de
tableaux de tableaux.
Le type des éléments d’un tableau pourra être aussi varié qu’on le désire, de
sorte qu’on pourra aboutir à des structures de données relativement complexes :
tableaux dont les éléments sont eux-mêmes des structures, structures dont
certains champs sont des tableaux dont les éléments sont eux-mêmes des
unions…
Une fois de plus, le langage C fait preuve d’originalité en introduisant une très
forte corrélation entre la notion de tableau et celle de pointeur : d’une part, un
identificateur de tableau est un pointeur constant ; d’autre part, l’opérateur []
possède comme premier opérande un pointeur, lequel peut éventuellement être
un identificateur de tableau.
Ici, nous étudions essentiellement ce que l’on pourrait nommer la manière
usuelle d’exploiter des tableaux, en dehors de tout contexte pointeur. Après un
exemple de programme utilisant un tableau, nous ferons le point sur la
déclaration d’un tableau. Nous verrons ensuite comment utiliser les différents
éléments d’un tableau. Sur ce plan, C sera similaire aux autres langages. Puis,
après avoir précisé comment les tableaux sont organisés en mémoire, nous
examinerons les problèmes induits par les éventuels débordements d’indice.
Nous étudierons en détail le cas des tableaux de tableaux et nous terminerons sur
la façon d’initialiser un tableau au moment de sa déclaration.
Le lien entre tableau et pointeur sera quant à lui examiné à la section 7 du
chapitre 7. Nous y ferons également le point sur l’opérateur []. Par ailleurs, la
transmission de tableaux en argument d’une fonction sera étudiée au chapitre 8.
Enfin, les possibilités dites parfois de « tableaux dynamiques » – c’est-à-dire de
tableaux dont l’emplacement est alloué dynamiquement (par une fonction telle
que malloc) – seront étudiées au chapitre 14.
1. Exemple introductif d’utilisation d’un tableau
Voici un exemple simple montrant comment s’utilise un tableau en langage C. Il
s’agit d’un programme déterminant, à partir de 20 notes (entières) d’élèves
fournies en données, combien sont supérieures à la moyenne de la classe. On
notera qu’un tel problème nécessite le recours à un tableau, dès lors qu’on
souhaite éviter de lire deux fois les mêmes notes.
Exemple d’utilisation d’un tableau

#include <stdio.h>
int main()
{
int i, som, nbm ;
float moy ;
int t[20] ; /* déclaration d'un tableau t de 20 int */
for (i=0 ; i<20 ; i++)
{ printf ("donnez la note numero %d : ", i+1) ;
scanf ("%d", &t[i]) ; /* lecture de l'élément de rang i de t */
}
for (i=0, som=0 ; i<20 ; i++) som += t[i] ;
moy = (float)som / 20 ;
printf ("\n\n moyenne de la classe : %f\n", moy) ;
for (i=0, nbm=0 ; i<20 ; i++ )
if (t[i] > moy) nbm++ ; /* comparaison de l'élément de rang i avec moy */
printf ("%d eleves ont plus de cette moyenne", nbm) ;
}

La déclaration :
int t[20] ;

réserve l’emplacement pour 20 éléments de type int. Chaque élément est repéré
par sa position dans le tableau, nommée « indice ». Conventionnellement, en
langage C, la première position porte le numéro 0. Ici, nos indices vont donc de
0 à 19. Le premier élément du tableau sera désigné par t[0], le troisième par t[2],
le dernier par t[19].
Plus généralement, une notation telle que t[i] désigne un élément dont la
position dans le tableau est fournie par la valeur de i. Elle joue le même rôle
qu’une variable scalaire de type int. La notation &t[i] désigne l’adresse de cet
élément t[i].
On remarquera que, dans ce programme, on a été amené à plusieurs reprises à
appliquer la même opération aux différents éléments du tableau, en utilisant une
instruction for. En effet, il n’existe en C aucune opération susceptible de porter
globalement sur l’ensemble des éléments d’un tableau, qu’il s’agisse de lecture,
d’écriture ou d’affectation.
2. Déclaration des tableaux
Le tableau 6.1 récapitule les différents éléments intervenant dans la déclaration
des tableaux. Ils seront ensuite détaillés dans les sections mentionnées.

Tableau 6.1 : déclaration des tableaux

Type des Type quelconque, hormis fonction, Voir section 2.2


éléments mais on peut définir des tableaux de
d’un tableau pointeurs sur des fonctions
De la forme : – rappels sur
declarateur [ dimension ]
les
déclarations
en C à la
Déclarateur section 2.1 ;
de tableau – description
du
déclarateur et
exemples à la
section 2.3.

– extern : pour les redéclarations de – étude


tableaux globaux ; détaillée de
– auto : pour les tableaux locaux la classe de
(superflu) ; mémorisation
dans les
– static : tableau rémanent ; sections 8, 9
– register : peu d’intérêt en général. et 10 du
Classe de chapitre 8 ;
mémorisation
– exemple
static à la
section
2.5.1 ;
– discussion
register à la
section 2.5.2.
– s’appliquent à tous les éléments du – définition des
tableau ; qualifieurs à
– un tableau constant doit en général la section 6.3
Qualifieurs être initialisé, sauf s’il est volatile du chapitre
(const, volatile) ou s’il s’agit de la redéclaration 3 ;
d’un tableau global. – discussion à
la section
2.6.
Utile pour sizeof et pour le prototype Voir section 2.7
Nom de type
d’une fonction ayant un argument de
d’un tableau
type tableau.

2.1 Généralités
La déclaration d’un tableau permet de préciser tout ou partie des informations
suivantes :
• le nom donné au tableau : il s’agit d’un identificateur usuel ;
• le type de ses éléments ;
• éventuellement, un ou deux qualifieurs (const, volatile) ;
• le nombre de ses éléments, lorsque cette information est utile au compilateur ;
• éventuellement, une classe de mémorisation.
Cependant, la nature même des déclarations en C disperse ces différentes
informations au sein d’une même instruction de déclaration. Par exemple, dans :
static const unsigned int *ad, x, t[10] ;

t est un tableau de 10 éléments constants de type unsigned int. On peut dire aussi
que t est un tableau de 10 éléments de type const unsigned int ou encore que t est
un tableau constant de 10 éléments de type unsigned int.
D’une manière générale, on peut dire qu’une déclaration en C associe un ou
plusieurs déclarateurs (ici *ad, x et t[10]) à une première partie commune à tous
ces déclarateurs et comportant effectivement :
• un spécificateur de type (ici, il s’agit de unsigned int) ;
• un éventuel qualifieur (ici const) ;
• une éventuelle classe de mémorisation (ici static).
Les déclarations en C peuvent devenir complexes compte tenu de ce que :
• un même spécificateur de type peut être associé à des déclarateurs de nature
différente ;
• les déclarateurs peuvent se « composer » : il existe des déclarateurs de
tableaux, de pointeurs et de fonctions ;
• la présence d’un déclarateur de type donné ne renseigne pas précisément sur la
nature de l’objet déclaré. Par exemple, un pointeur sur un tableau comportera,
entre autres, un déclarateur de tableau ; ce ne sera pas un tableau pour autant.
Pour tenir compte de cette complexité et de ces dépendances mutuelles, le
chapitre 16 fait le point sur la syntaxe des déclarations, la manière de les
interpréter et de les rédiger. Ici, nous examinerons de manière moins formelle les
déclarations correspondant aux situations les plus usuelles.

2.2 Le type des éléments d’un tableau


Le langage C est très souple puisqu’en fait, les éléments d’un tableau peuvent
être d’un type quelconque. La seule restriction est qu’ils doivent être des objets,
ce qui exclut simplement les tableaux de fonctions, tout en autorisant les
tableaux de pointeurs sur des fonctions. Comme certains types sont eux-mêmes
construits à partir d’autres types, et ce d’une façon éventuellement récursive, on
voit qu’on peut créer des types relativement complexes, même si ces derniers ne
sont pas toujours indispensables !
En dehors des tableaux d’objets d’un type de base, on sera amené à recourir à
des tableaux dont les éléments sont eux-mêmes des tableaux, simplement pour
disposer de ce que l’on nomme souvent, dans d’autres langages, des tableaux à
plusieurs indices. Les tableaux de structures pourront remplacer
avantageusement plusieurs tableaux de types différents, comme on le verra au
chapitre 11. Les tableaux de pointeurs, quant à eux, pourront faciliter certaines
manipulations d’objets volumineux, notamment des structures, en remplaçant
leur copie par celle de simples pointeurs, plus économiques en temps. Nous en
verrons des exemples au chapitre 14.

2.3 Déclarateur de tableau


Comme indiqué à la section 2.1, dans une déclaration, le type des éléments d’un
tableau est défini par l’association d’un déclarateur à un spécificateur de type,
éventuellement complété par des qualifieurs, l’éventuelle classe de mémorisation
n’ayant pas d’incidence sur le type même du tableau.
La déclaration d’un tableau fait toujours intervenir un déclarateur de la forme
suivante (attention, les crochets en gras font partie de la syntaxe, tandis que ceux
en romain précisent, comme d’habitude, que leur contenu est facultatif) :

Déclarateur de forme tableau

declarateur [ [ dimension ] ]
declarateur
Déclarateur quelconque
dimension
– expression constante – voir discussion à la
entière positive, sans section 2.4 ;
signe ; – définition expression
– peut être omise dans constante à la section
certains cas. 14 du chapitre 4.

Notez que nous parlons de « déclarateur de forme tableau » plutôt que


« déclarateur de tableau » car la présence d’un tel déclarateur de tableau ne
signifie pas que l’identificateur correspondant soit un tableau. Elle prouve
simplement que la définition de son type fait intervenir un tableau. Par exemple,
il pourra s’agir d’un pointeur sur un tableau, comme nous le verrons dans les
exemples ci-après.
Par ailleurs, les possibilités de composition des déclarateurs font que le
déclarateur mentionné dans cette définition peut être éventuellement complexe,
même si, dans les situations les plus simples, il se réduit à un identificateur.

Exemples
Voici des exemples de déclarateurs que, par souci de clarté, nous avons introduit
dans des déclarations complètes. Lorsque cela est utile, nous indiquons en regard
les règles utilisées pour l’interprétation de la déclaration, telles que vous les
retrouverez à la section 4 du chapitre 16. La dernière partie constitue un contre-
exemple montrant que la présence d’un déclarateur de tableau ne correspond pas
nécessairement à la déclaration d’un tableau.

Cas simples : éléments d’un type de base, structure, union ou défini par
typedef
unsigned int t[5];
est un tableau de 5 éléments de type int
t

struct point { char nom;


int x; courbe est un tableau de 10 éléments de type
int y; struct point
};
struct point courbe [10];
union u { float x;
char z[4]; est un tableau de 25 éléments de type union u
t
};
union u t[25];
typedef int * ptr;
ptr tab1[4], tab2 [8]; ptr est un synonyme de int *
tab1 est un tableau de 4 pointeurs sur int
tab2 est un tableau de 8 pointeurs sur int
typedef int vecteur [3];
vecteur mat [5]; vecteur est synonyme de int [3]
mat est un tableau de 5 éléments, eux-mêmes
tableaux de 3 int

Éléments de type pointeur


int *chose [10];
est un int
*chose[10]
→ chose [10] est un pointeur sur un int
→ chose est un tableau de 10 pointeurs sur un int

Éléments de type tableau


float mat [10] [5];
est un float
mat [10][5]
→ mat [10] est un tableau de 5 éléments de type
float
→ mat est un tableau de 10 éléments qui sont
eux-mêmes des tableaux de 5 float

Un déclarateur de forme tableau ne correspond pas toujours à un tableau

int (*chose) [10]


est un int
(*chose) [10]
→ (*chose) est un tableau de 10 int
→ *chose est un tableau de 10 int (on a
simplement enlevé les parenthèses)
→ chose est un pointeur sur un tableau de 10 int
int chose * [10];
syntaxiquement incorrect
2.4 La dimension d’un tableau
Comme l’indique la syntaxe d’un déclarateur de forme tableau, la dimension
possède deux propriétés particulières :
• il doit s’agir d’une expression constante (sauf en C99 ou en C11, comme
expliqué dans l’annexe B consacrée aux normes C99 et C11) ;
• elle peut être omise dans certaines conditions.

2.4.1 Il s’agit d’une expression entière constante


Cette condition est à la fois large et restrictive. Elle est large puisqu’elle ne
limite pas la dimension à une (vraie) constante, mais autorise des déclarations
telles que :
#define N_LIG 30
#define N_COL 25
…..
float somme_col [N_COL] ;
float somme_lig [N_LIG] ;
float mat [N_COL] [N_LIG] ;
float coef [N_COL * N_LIG] ;
float truc [2 * N_COL + 3 * N_LIG -2 ] ;

En revanche, elle est restrictive puisqu’il ne peut pas s’agir d’une expression
variable. Or, dans le cas de tableaux de classe automatique – c’est-à-dire dont
l’allocation mémoire est réalisée lors de chaque entrée dans une fonction ou dans
un bloc –, il aurait été techniquement possible que cette dimension puisse varier
d’un appel à un autre. Cela n’a pas été prévu par les concepteurs du C, ni par la
norme ANSI. Les instructions suivantes seraient rejetées en compilation :
void f(int n) ;
{ float t[n] ; /* incorrect */
…..
}

On notera que la forme autorisée pour la dimension d’un tableau est la même,
que l’on ait affaire à des déclarations globales ou locales.
Par ailleurs, on n’oubliera pas qu’en C, un objet ayant reçu le qualifieur const ne
peut pas apparaître dans une expression constante (il le pourra en C++) :
const int n = 5 ;
…..
int t[n] ; /* incorrect, quelle que soit la classe d'allocation de t */

2.4.2 Elle peut parfois être omise


La dimension d’un tableau peut ne pas figurer dans un déclarateur lorsqu’elle
n’est pas utile au compilateur, c’est-à-dire dans l’un des deux cas suivants.

a) Le compilateur peut en définir la valeur


C’est ce