Guide Complet C
Guide Complet C
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 ».
Du même auteur
Autres ouvrages
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
En fait, tant que l’on ne cherche pas à utiliser les « arguments de la ligne de
commande », les deux formes :
int main ()
main()
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
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.
*adi désigne la valeur de l’objet pointé par adi, tandis que dans :
*adi = 12 ;
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 = ‘ç' ;
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.
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.
Par exemple, vous devrez impérativement écrire (avec au moins un espace blanc
entre int et x) :
int x,y ;
et non :
intx,y ;
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 */
et non à :
int quantite ;
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 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 *
============================================ */
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.
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.
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
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.
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
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.
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.
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.
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.
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.
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.
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.
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 !
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.
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.
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"
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.
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.
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).
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
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
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.
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.
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.
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 */
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…
• 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).
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
FLT_MAX_EXP
DBL_MAX_EXP Plus grand nombre n tel que FLT_RADIX soit
n-1
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.
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é.
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.
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 */
• 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.
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.
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
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)
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.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 / */
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 */
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
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 )
#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.
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.
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.
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 ;
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…
Exemple
Avec :
int n = 12 ;
long p = 50000 ;
De même, avec :
int n = 5 ;
double x = 3.25 ;
Exemple
Avec :
unsigned int n = 12 ;
unsigned long p = 50000 ;
De même, avec :
unsigned int n = 5 ;
double x = 3.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.
Types
Conversions prévues
opérandes
int et unsigned Conversion de l’opérande de type int dans le type
int unsigned int
#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.
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) ‘é'.
• dans le type int si le type int peut recevoir toutes les valeurs du type unsigned
short ;
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.
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.
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
• 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.
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.
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 */
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.
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
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
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…
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) */
pourra apparaître comme plus concise (mais pas forcément plus lisible) que :
if ( n == 0 )
En effet, l’expression :
! 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
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).
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' ) )
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
Bit opérande 0 1
~ (complément à un) 1 0
l’instruction
n = p & q ;
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) ;
}
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.
l’instruction :
n = p >> 3 ;
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.
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 ; */
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 !).
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
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 :
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.
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.
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
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;
en :
lvalue operateur= expression
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 )
(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
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.
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.
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 */
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)
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 */
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 */
→
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é) :
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 ;
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 *.
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 * */
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
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.
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 */
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 */
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.
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 *) */
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é.
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
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 */
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
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.
[ 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.
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") ;
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
{ [ 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 ;
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.
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 */
…..
}
la valeur de k ne sera pas définie (pas plus d’ailleurs que celle de i).
4. 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.
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") ;
}
max = b ;
if (a > b) max = a ;
printf ("maximum : %d", max) ;
else max = b ;
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
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) …..
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.
ou même, dans :
if (a<=b) { if (b<=c) { printf ("ordonne") ;
}
else { printf ("non ordonne") ;
}
}
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
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
#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.
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.
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 : …..
……
}
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.
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.
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 …..
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
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') ;
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.
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.
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") ;
}
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 ) ;
est équivalente à :
do c = getchar() ; while ( c != ‘$' ) ;
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 !
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) ;
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.
par :
instruction
while (expression) do instruction
par :
if (expression) do instruction while (expression) ;
Remarques
1. Lorsque le corps de boucle est vide, do … while et while sont équivalentes :
do {} while (expression) ;
while (expression) {}
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.
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) ;
}
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
}
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
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.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
ou encore ainsi :
for (i=0 ; i<10 ; i++ )
{ x = i * 0.1 ;
…..
}
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") ;
}
c’est-à-dire en fait à :
expression_1 ;
while (1)
{ exp_2a ;
if (!exp_2b) break ;
instructions
expression_3 ;
}
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.
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
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.
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++) …..
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.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.
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.
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) ;
}
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 : ;
}
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.
à :
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 */
}
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.
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.
Remarque
Dans beaucoup d’autres langages que le C, les constructions précédentes sont formellement interdites.
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.
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.
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.
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
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 */