Bibliothèques standard¤
Aux débuts de l’informatique (années 1950 et 1960), chaque programme était intimement lié à la machine qui l’exécutait. Les codeuses et codeurs travaillaient quasi exclusivement en assembleur et devaient implémenter eux-mêmes la moindre fonctionnalité : lecture depuis le clavier, écriture sur bande magnétique, calculs numériques de base. L’idée de partager des briques logicielles réutilisables n’était pas encore ancrée dans les pratiques, et la portabilité d’un système à l’autre relevait de l’exploit.
Le langage C a profondément changé ce paysage en proposant très tôt une bibliothèque standard, la C standard library, communément appelée libc. Son objectif était double : fournir un socle commun d’outils fiables et encourager l’écriture de programmes portables. Y figurent, entre autres, la manipulation des chaînes de caractères, la gestion des fichiers, l’allocation dynamique ou encore les opérations mathématiques usuelles. Cette démarche a évité aux équipes de réécrire continuellement les mêmes routines et a contribué à la diffusion rapide du langage.
À partir des années 1990, d’autres langages ont poussé encore plus loin cette approche. Java a été livré dès sa première version avec un ensemble très riche de bibliothèques standardisé, couvrant l’interface graphique, le réseau, les collections ou la sécurité. Python a popularisé le slogan « batteries incluses », pour souligner qu’une grande variété d’outils est disponible directement après l’installation. Rust, Go, Swift ou Kotlin perpétuent cette tradition en associant à leurs bibliothèques standard des gestionnaires de paquets qui facilitent l’accès aux extensions tierces.
En C, la situation demeure plus frugale : la bibliothèque standard reste volontairement compacte afin de préserver la portabilité et la simplicité du langage. Ce minimalisme a un coût — certaines fonctionnalités modernes sont absentes — mais il garantit que le même code peut se compiler sur une grande diversité de plateformes. Pour combler ces lacunes, le monde du C s’appuie sur une multitude de bibliothèques supplémentaires, parfois spécifiques à un système, parfois normalisées par POSIX.
La libc n’en reste pas moins indispensable. Les sections qui suivent résument ses principaux en-têtes, détaillent les points d’attention pour les utiliser correctement et proposent des exemples concrets montrant comment s’appuyer sur ces outils dans vos propres projets.
| En-tête | Description | Standard |
|---|---|---|
<assert.h> |
Validation des prérequis | C89 |
<complex.h> |
Nombres complexes | C99 |
<ctype.h> |
Tests | C89 |
<errno.h> |
Gestion des erreurs | C89 |
<fenv.h> |
Environnement de calcul flottant | C99 |
<float.h> |
Constantes de précision des types flottants | C89 |
<inttypes.h> |
Types entiers formatés | C99 |
<iso646.h> |
Alternative aux opérateurs (and, or) | C95 |
<limits.h> |
Limites des types entiers | C89 |
<locale.h> |
Gestion des locales | C89 |
<math.h> |
Fonctions mathématiques | C89 |
<setjmp.h> |
Gestion des sauts | C89 |
<signal.h> |
Gestion des signaux | C89 |
<stdalign.h> |
Alignement des types | C11 |
<stdarg.h> |
Arguments variables | C89 |
<stdatomic.h> |
Opérations atomiques | C11 |
<stdbit.h> |
Macros pour les bits | C23 |
<stdbool.h> |
Type booléen | C99 |
<stdckdint.h> |
Macros de tests pour les entiers | C23 |
<stddef.h> |
Macros standard | C89 |
<stdint.h> |
Types entiers standard | C99 |
<stdio.h> |
Entrées/sorties standard | C89 |
<stdlib.h> |
Allocation dynamique | C89 |
<stdnoreturn.h> |
Fonctions sans retour | C11 |
<string.h> |
Manipulation des chaînes de caractères | C89 |
<tgmath.h> |
Fonctions mathématiques génériques | C99 |
<threads.h> |
Gestion des threads | C11 |
<time.h> |
Date et heure | C89 |
<uchar.h> |
Caractères Unicode | C11 |
<wchar.h> |
Caractères larges | C95 |
<wctype.h> |
Tests larges | C95 |
<assert.h>¤
Même si <assert.h> semble modeste, il introduit des mécanismes précieux pour valider les hypothèses de votre code. La macro assert vérifie qu’une condition est vraie ; si ce n’est pas le cas, elle interrompt le programme et affiche un message descriptif. static_assert réalise une vérification similaire mais à la compilation, ce qui permet de bloquer un programme avant même qu’il soit exécuté lorsque la configuration est incohérente.
Ces prototypes illustrent l’intention, mais rappelons qu’il s’agit en pratique de macros. Leur emploi systématique pendant le développement aide à repérer très tôt les erreurs de logique, comme l’utilisation d’un pointeur nul ou une division par zéro, et offre un message clair pour comprendre la situation.
La grande force d’assert est qu’il peut être désactivé dans un environnement de production en définissant la macro NDEBUG. Lorsque NDEBUG est présent, toutes les assertions deviennent inactives, supprimant le coût de ces vérifications. On peut schématiser ce fonctionnement de la manière suivante :
#ifdef NDEBUG
#define assert(ignore) ((void)0)
#else
#define assert(expr) \
((expr) ? (void)0 : __assert_fail(#expr, __FILE__, __LINE__, __func__))
#endif
Pour désactiver les assertions, il suffit d’ajouter -DNDEBUG à la ligne de commande du compilateur, comme dans l’exemple suivant :
Avant l'en-tête
Définissez NDEBUG avant d’inclure <assert.h>. Une définition tardive laisserait la macro assert active, car l’en-tête aura déjà généré sa version instrumentée.
<errno.h>¤
L’en-tête <errno.h> propose un mécanisme simple pour transmettre des informations d’erreur entre une fonction et son appelant. Il fournit une variable globale errno, qui stocke le dernier code d’erreur défini par certaines fonctions de la bibliothèque standard ou par des extensions POSIX.
Le standard POSIX complète ce dispositif avec de nombreuses macros nommées (EACCES, ENOENT, ENOMEM, etc.) décrivant des situations précises. La liste complète est disponible sur la documentation officielle. Ci-dessous figurent les codes les plus fréquents :
| Code | Description |
|---|---|
EACCES |
Permission refusée (p.ex. sur un fichier) |
EADDRINUSE |
Adresse déjà utilisée (socket) |
ECONNREFUSED |
Connexion refusée (socket) |
EDOM |
Erreur de domaine mathématique |
EEXIST |
Fichier existe déjà |
EINVAL |
Argument invalide |
ENOENT |
Fichier ou répertoire inexistant |
ENOMEM |
Pas assez de mémoire disponible |
ENOSPC |
Plus d'espace disponible sur le périphérique |
Prenons un exemple : calculer le logarithme d’un nombre négatif déclenche une erreur de domaine (EDOM). Avant d’appeler log, on réinitialise errno à zéro pour éviter de confondre une erreur actuelle avec un ancien état. Après l’appel, on peut afficher le message associé à l’aide de strerror.
#include <stdio.h>
#include <math.h>
#include <errno.h>
#include <string.h>
int main(void)
{
errno = 0;
printf("log(-1.0) = %f\n", log(-1.0)); // Domain error
printf("%s\n\n",strerror(errno));
errno = 0;
printf("log(0.0) = %f\n", log(0.0));
printf("%s\n",strerror(errno)); // Numerical result out of range
}
<math.h>¤
La bibliothèque mathématique est l’une des plus sollicitées. Elle regroupe les fonctions d’analyse numérique de base (exponentielles, logarithmes, racines, trigonométrie) et décline la plupart d’entre elles pour float, double et long double via les suffixes f et l. Sur de nombreux systèmes Unix, il faut ajouter l’option -lm lors de l’édition de liens pour profiter de ces symboles.
| Constantes | Description |
|---|---|
M_PI |
Valeur de \(\pi\) |
M_E |
Valeur de \(e\) |
M_SQRT1_2 |
Valeur de \(1/\sqrt(2)\) |
Windows
Ces constantes ne font pas partie du standard ISO C, mais de POSIX. Elles peuvent donc manquer sur certaines plates-formes, notamment sous Windows ; dans ce cas, il faut les définir soi-même ou s’appuyer sur une bibliothèque alternative.
| Fonction | Description |
|---|---|
exp(x) |
Exponentielle \(e^x\) |
ldexp(x,n) |
Exposant d'un nombre flottant \(x\cdot2^n\) |
log(x) |
Logarithme binaire \(\log_{2}(x)\) |
log10(x) |
Logarithme décimal \(\log_{10}(x)\) |
pow(x,y) |
Puissance \(x^y\) |
sqrt(x) |
Racine carrée \(\sqrt(x)\) |
cbrt(x) |
Racine cubique \(\sqrt[3](x)\) |
hypot(x,y) |
Hypoténuse optimisé \(\sqrt(x^2 + y^2)\) |
ceil |
Arrondi à l'entier supérieur |
floor |
Arrondi à l'entier inférieur |
Notons par exemple que la fonction hypot peut très bien être émulée facilement en utilisant la fonction sqrt. Néanmoins elle existe pour deux raisons élémentaires :
- Éviter les dépassements (overflow).
- Une meilleure optimisation du code.
Souvent, les processeurs sont équipés de coprocesseurs arithmétiques capables de calculer certaines fonctions plus rapidement.
Le standard C99 a introduit l'en-tête <tgmath.h> qui donne accès à des fonctions génériques. Par exemple, sin peut être utilisé pour des float, double et long double sans avoir à choisir le nom de la fonction (sinf, sin, sinl), en outre les types complexes sont également supportés comme csin pour les complexes.
<fenv.h>¤
La bibliothèque <fenv.h> est étroitement liée aux calculs mathématique et permet de manipuler l'environnement de calcul flottant. Elle permet de contrôler les modes de calculs, les exceptions et les arrondis. Les fonctions sont définies pour les types float, double et long double avec les préfixes f, l et sans préfixe respectivement.
La structure fenv_t contient l'état de l'environnement de calcul flottant et la structure fexcept_t contient les exceptions de calcul flottant. Ces structures sont opaques et ne doivent pas être manipulées directement, elles dépendent de l'implémentation et peuvent varier d'un système à l'autre. Néanmoins pour X86, voici à quoi elles pourraient ressembler pour les curieux.
typedef struct {
// Mot de contrôle de la FPU.
union {
unsigned int __control_word;
struct {
// Masque des exceptions de la FPU
unsigned int IM : 1; // Invalid Operation
unsigned int DM : 1; // Denormalized Operand
unsigned int ZM : 1; // Zero-Divide
unsigned int OM : 1; // Overflow
unsigned int UM : 1; // Underflow
unsigned int PM : 1; // Precision
unsigned int _Reserved : 2;
// Gestion de l'arrondi et de la précision
unsigned int PC : 2; // Precision Control
unsigned int RC : 2; // Rounding Control mode
unsigned int _FPUReserved : 3;
unsigned int IC : 1; // Plus utilisé
};
};
// Word de statut de la FPU (indicateurs d'état et d'exception)
union {
unsigned int __status_word;
struct {
// Indicateurs d'exception
unsigned int IE : 1; // Invalid Operation Exception
unsigned int DE : 1; // Denormalized Operand Exception
unsigned int ZE : 1; // Zero-Divide Exception
unsigned int OE : 1; // Overflow Exception
unsigned int UE : 1; // Underflow Exception
unsigned int PE : 1; // Precision Exception
unsigned int SF : 1; // Stack Fault
unsigned int ES : 1; // Exception Summary Status
// Indicateurs d'état pour le stockage de valeurs
// intermédiaires durant les calculs
unsigned int C0 : 1;
unsigned int C1 : 1;
unsigned int C2 : 1;
unsigned int Top : 3; // Position du sommet de la pile
unsigned int C3 : 1;
unsigned int Busy : 1; // FPU occupée
};
};
unsigned int __tag_word;
unsigned int __fpu_ip; // Instruction pointer
unsigned int __fpu_cs; // Code segment
unsigned int __opcode; // Opcode de l'opération en cours
unsigned int __fpu_dp; // Data pointer
unsigned int __mxcsr; // Registre MXCSR (contrôle des exceptions SSE)
unsigned int __mxcsr_mask; // Masque de contrôle MXCSR
} fenv_t;
Contrôle des exceptions¤
Il est possible de gérer les exceptions de calculs flottants comme :
- la division par zéro,
- le dépassement de capacité (overflow),
- un résultat non numérique (NaN),
- un sous-dépassement de capacité (underflow),
- une perte de précision.
Dans certains programmes, notamment ceux impliquant des calculs numériques intensifs ou critiques (comme en science ou en ingénierie), il est important de savoir si une opération en virgule flottante a échoué ou produit un résultat incorrect.
#include <stdio.h>
#include <fenv.h>
#pragma STDC FENV_ACCESS ON // NECESSAIRE
int main() {
feclearexcept(FE_ALL_EXCEPT); // Efface anciennes exceptions
double result = 1.0 / 0.0; // Division par zéro
if (fetestexcept(FE_DIVBYZERO)) {
printf("Erreur : division par zéro détectée\n");
}
}
Contrôle de l'arrondi¤
Il est aussi possible contrôler la manière dont les résultats des opérations en virgule flottante sont arrondis. Par défaut, les opérations en virgule flottante arrondissent au plus proche, mais vous pouvez modifier ce comportement pour arrondir vers zéro, vers l'infini, ou vers moins l'infini.
Nous avions vu précédemment que l'arrondi d'un nombre est compliqué. La norme IEEE 754 définit plusieurs modes d'arrondis. La fonction fesetround permet de définir le mode d'arrondi. Les modes possibles sont donnés par la table suivante :
| Mode | Description | \(-3.5\) | $-2.5 | $2.5 | 3.5$ |
|---|---|---|---|---|---|
FE_TONEAREST |
Arrondi bancaire | \(-4\) | \(-2\) | \(2\) | \(4\) |
FE_DOWNWARD |
Arrondi vers zéro | \(-4\) | \(-3\) | \(2\) | \(3\) |
FE_UPWARD |
Arrondi vers l'infini | \(-3\) | \(-2\) | \(3\) | \(4\) |
FE_TOWARDZERO |
Arrondi vers moins l'infini | \(-3\) | \(-2\) | \(2\) | \(3\) |
round() |
Comparaison avec round |
\(-4\) | \(-3\) | \(3\) | \(4\) |
L'arrondi bancaire minimise les biais d'arrondi lorsqu'on fait des calculs sur de grandes quantités de données. En arrondissant vers l'entier pair dans les cas où un nombre tombe exactement à mi-chemin entre deux entiers, cette méthode réduit l'accumulation d'erreurs statistiques qui peuvent survenir avec d'autres méthodes d'arrondi. En effet les valeurs sont arrondies vers l'entier pair le plus proche. Voici quelques exemples :
round
La configuration du mode d'arrondi avec fesetround affecte les fonctions nearbyint, rint mais pas round.
La fonction round utilise un arrondi spécifique appelé round half away from zero qui arrondit les valeurs à l'entier le plus proche en s'éloignant de zéro.
Notez que la différence entre rint et nearbyint est que nearbyint ne génère pas d'exception en cas de dépassement de capacité (overflow).
<float.h>¤
La bibliothèque <float.h> contient des constantes qui définissent la précision des types flottants sur l'architecture cible. Les constantes sont définies pour les types float, double et long double.
On y retrouve FLT_ROUNDS qui indique le mode d'arrondi par défaut utilisé à la compilation.
Dans IEEE 754, l'exposant est de base 2, c'est ce qu'on appelle le radix. Il peut être contrôlé avec la macro FLT_RADIX. Les constantes DBL_DIG et LDBL_DIG indiquent le nombre de chiffres significatifs que l'on peut stocker dans un double et un long double respectivement.
Autre base ?
Aujourd'hui quasiment 100 pour cent des ordinateurs utilisent le radix 2. Néanmoins, à une certaine époque le radix 10 était utilisé sur certaines architectures comme le l'IBM 650 (1953).
La norme IEEE 754-2008 permet d'utiliser le radix 16, 10 ou 2. Elle défini notament la repséentation DFP (Decimal Floating Point) qui permet de représenter les nombres décimaux de manière exacte. Cependant l'implémentation physique d'une FPU en radix 10 est plus complexe et moins performante c'est pour cela que la vaste majorité des processeurs utilisent le radix 2 suffisant pour la plupart des applications.
<complex.h>¤
La bibliothèque <complex.h> permet de manipuler les nombres complexes. Les fonctions sont définies pour les types float, double et long double avec les préfixes f, l et sans préfixe respectivement.
Un nombre complexe est défini par une partie réelle et une partie imaginaire. Le type _Complex est un type de base, mais il peut être utilisé de manière plus simple avec la macro complex. Pour définir un nombre complexe, on utilise la notation a + bi où a est la partie réelle et b la partie imaginaire.
#include <complex.h>
int main() {
double complex z = 1.0 + 2.0 * I;
double complex w = 3.0 + 4.0 * I;
double complex sum = z + w;
printf("Somme : %f + %fi\n", creal(sum), cimag(sum));
// Nombre imaginaire pur
double imaginary = -2.0 * I;
}
La constante I est bien entendue définie comme 1.0i. Toutes les fonctions complexes se déclines en trois versions :
| Type | Exemple de fonction |
|---|---|
float |
cabsf, csinf |
double |
cabs, csin |
long double |
cabsl, csinl |
| Fonction | Description |
|---|---|
cabs(z) |
Module |
cacos(z) |
Arc cosinus |
cacosh(z) |
Arc cosinus hyperbolique |
carg(z) |
Argument \(\arg(z)\) |
casin(z) |
Arc sinus |
casinh(z) |
Arc sinus hyperbolique |
catan(z) |
Arc tangente |
catanh(z) |
Arc tangente hyperbolique |
ccos(z) |
Cosinus |
ccosh(z) |
Cosinus hyperbolique |
cexp(z) |
Exponentielle \(e^z\) |
cimag(z) |
Partie imaginaire \(\Im z\) |
clog(z) |
Logarithme \(\log(z)\) |
conj(z) |
Conjugaison \(\bar(z)\) |
cpow(z,w) |
Puissance \(z^w\) |
creal(z) |
Partie réelle \(\Re z\) |
csin(z) |
Sinus |
csinh(z) |
Sinus hyperbolique |
csqrt(z) |
Racine carrée \(\sqrt{z}\) |
ctan(z) |
Tangente |
ctanh(z) |
Tangente hyperbolique |
Certaines extensions prévue possiblement avec C23 amènerait des fonctionnalités supplémentaires telles que cexp2, clog2, cexp10, clog10, crootn ...
<iso646.h>¤
L'en-tête <iso646.h> est une extension du standard C95 qui définit des alternatives aux opérateurs logiques. Les opérateurs logiques sont définis avec des symbols (&&, ||, !) mais pour des raisons de lisibilité, il est possible de les définir en anglais (and, or, not).
| Opérateur | Macro | Description |
|---|---|---|
&& |
and |
ET logique |
\|\| |
or |
OU logique |
! |
not |
NON logique |
!= |
not_eq |
Différent de |
&= |
and_eq |
ET binaire |
\|= |
or_eq |
OU binaire |
^= |
xor_eq |
OU exclusif binaire |
^ |
xor |
OU exclusif binaire |
~ |
compl |
Complément binaire |
Ces macros sont utiles pour les personnes qui ne peuvent pas taper certains caractères spéciaux sur leur clavier. Elles sont également utiles pour les personnes qui veulent rendre leur code plus lisible. Néanmoins, elles ne sont pas très utilisées en pratique.
Je vous recommande personnellement de ne pas utiliser ces macros. Elles ne sont pas très utilisées et peuvent rendre le code moins lisible pour les autres développeurs.
<limits.h>¤
La bibliothèque <limits.h> contient des constantes qui définissent les limites des types entiers de base. Les constantes sont définies pour les types char, short, int, long, long long et float, double, long double.
| Constante | Description | LP64 |
|---|---|---|
CHAR_BIT |
Nombre de bits dans un char |
8 |
CHAR_MAX |
Valeur maximale d'un char |
127 |
CHAR_MIN |
Valeur minimale d'un char |
-128 |
SCHAR_MAX |
Valeur maximale d'un signed char |
127 |
SCHAR_MIN |
Valeur minimale d'un signed char |
-128 |
UCHAR_MAX |
Valeur maximale d'un unsigned char |
255 |
SHRT_MAX |
Valeur maximale d'un short |
32767 |
SHRT_MIN |
Valeur minimale d'un short |
-32768 |
USHRT_MAX |
Valeur maximale d'un unsigned short |
65535 |
INT_MAX |
Valeur maximale d'un int |
2147483647 |
INT_MIN |
Valeur minimale d'un int |
-2147483648 |
UINT_MAX |
Valeur maximale d'un unsigned int |
4294967295 |
LONG_MAX |
Valeur maximale d'un long |
9223372036854775807 |
LONG_MIN |
Valeur minimale d'un long |
-9223372036854775808 |
ULONG_MAX |
Valeur maximale d'un unsigned long |
18446744073709551615 |
LLONG_MAX |
Valeur maximale d'un long long |
9223372036854775807 |
LLONG_MIN |
Valeur minimale d'un long long |
-9223372036854775808 |
ULLONG_MAX |
Valeur maximale d'un unsigned long long |
18446744073709551615 |
<locale.h>¤
En jargon informatique, la locale est un ensemble de paramètres qui définissent les conventions culturelles d'une région. Cela inclut la langue, le format de date, le format de nombre, etc. La bibliothèque <locale.h> permet de manipuler ces paramètres.
Un système d'exploitation défini généralement une locale par défaut. Par exemple, un système en français utilisera la locale fr_FR. Le premier paramètre est la langue de la locale et le second et le pays. Il existe en effet des différences entre le français suisse fr_CH et le français canadien fr_CA.
Ces conventions sont définie par la norme ISO 15897 et font de surcroît partie du standard POSIX.
L'en-tête <locale.h> contient donc des fonctions pour manipuler les locales.
| Fonction | Description |
|---|---|
setlocale |
Définit la locale |
localeconv |
Récupère les paramètres de la locale |
lconv |
Structure retournée par localeconv |
Voici un exemple :
#include <locale.h>
#include <stdio.h>
int main() {
setlocale(LC_ALL, "fr_FR");
struct lconv *locale = localeconv();
printf("Séparateur de milliers : %s\n", locale->thousands_sep);
printf("Séparateur décimal : %s\n", locale->decimal_point);
}
La structure lconv est définie comme suit :
struct lconv {
char *decimal_point; // Séparateur décimal
char *thousands_sep; // Séparateur de milliers
char *grouping; // Taille des groupes de milliers
char *int_curr_symbol; // Symbole de la monnaie
char *currency_symbol; // Symbole de la monnaie
char *mon_decimal_point; // Séparateur décimal de la monnaie
char *mon_thousands_sep; // Séparateur de milliers de la monnaie
char *mon_grouping; // Taille des groupes de milliers de la monnaie
char *positive_sign; // Signe positif
char *negative_sign; // Signe négatif
char int_frac_digits; // Décimales pour la monnaie
char frac_digits; // Décimales
char p_cs_precedes; // Symbole avant la monnaie
char p_sep_by_space; // Espace entre la monnaie et le montant
char n_cs_precedes; // Symbole avant la monnaie négative
char n_sep_by_space; // Espace entre la monnaie négative et le montant
char p_sign_posn; // Position du signe positif
char n_sign_posn; // Position du signe négatif
};
Voici un exemple plus complet :
#include <locale.h>
int main() {
char country[] = "EN";
printf("Vous parlez français, tant mieux. Quel est votre pays ? ");
if (scanf("%2s", &country) != 1) {
printf("Erreur de saisie\n");
return 1;
}
for (int i = 0; country[i]; i++) country[i] = toupper(country[i]);
char locale[6];
snprintf(locale, 6, "fr_%s", country);
setlocale(LC_ALL, locale);
float apple_unit_price;
printf("Quel est le prix d'une pomme ? ");
if (scanf("%f", &apple_unit_price) != 1) {
printf("Erreur de saisie\n");
return 1;
}
const int quantity = 10;
const float ten_apples_price = apple_unit_price * quantity;
printf("Le prix de %d pommes est de %.2f %s\n",
quantity, ten_apples_price, localeconv()->currency_symbol);
}
Avec la fonction setlocale, il est possible de ne définir qu'une partie des conventions à utiliser. Par exemple, si on ne veut changer que le format de date, on peut utiliser setlocale(LC_TIME, "fr_FR").
| Catégorie | Description |
|---|---|
LC_ALL |
Toutes les catégories |
LC_COLLATE |
Comparaison de chaînes |
LC_CTYPE |
Caractères et conversions |
LC_MONETARY |
Format monétaire |
LC_NUMERIC |
Format numérique |
LC_TIME |
Format de date et heure |
<setjmp.h>¤
La bibliothèque <setjmp.h> permet de gérer les exceptions en C. Elle fournit deux fonctions setjmp et longjmp qui permettent de sauvegarder l'état du programme et de le restaurer à un point donné.
En pratique il est très rare d'utiliser ces fonctions, elles sont aussi dangereuses que les goto et peuvent rendre le code difficile à lire et à maintenir. Néanmoins dans des cas très spécifiques, elles peuvent s'avérer très utiles, notament pour simuler des exceptions avec des directives préprocesseur.
Nous avons vu que le compilateur utilise la pile pour stocker les variables locales et le contexte d'appel des fonctions. Dans chaque frame de la pile, on trouve l'adresse de retour permettant de continuer l'exécution d'une fonction dans la fonction appelante une fois la fonction courrante terminée. Ceci permet de communiquer hiérarchiquement entre les fonctions. Il n'est pas possible par exemple de remonter à la fonction main depuis une fonction baz appelée par bar appelée par foo, appelée par main.
Ce n'est pas possible... sauf si on triche un peu. La fonction setjmp permet de sauvegarder l'état du programme à un point donné. C'est-à-dire que si on sauve le contexte de main dans un espace mémoire séparé avant la chaîne d'appel de fonctions enfants, on pourrait manipuler le stack pour revenir à main depuis baz. C'est très exactement ce que fait setjmp.
La fonction setjmp prend un seul argument qui est une structure de type jmp_buf. Cette structure est opaque car elle dépend du compilateur et de la manière dont le stack est implémenté. Néanmoins sur une architecture x86, elle pourrait ressembler à ceci :
typedef struct {
unsigned int __eip; // Instruction pointer
unsigned int __esp; // Stack pointer
unsigned int __ebp; // Base pointer
unsigned int __ebx; // Base register
unsigned int __esi; // Source index
unsigned int __edi; // Destination index
} jmp_buf[1];
Il s'agit des registres du processeur concernés par la gestion du stack et le déroulement du programme. Le registre eip par exemple est le pointeur d'instruction. Il contient l'adresse de la prochaine instruction à exécuter. Si ce registre est modifié, le programme saute à une autre adresse. C'est ce que fait entre autre le goto, il modifie eip pour sauter à une autre adresse.
Pour marquer un point de retour, on utilise setjmp qui sauvegarde l'état du programme dans la structure jmp_buf. On peut ensuite revenir à ce point avec longjmp. La fonction longjmp prend deux arguments, la structure jmp_buf et une valeur de retour qui peut être utilisée pour savoir pourquoi on est revenu à ce point. Voici un exemple :
#include <setjmp.h>
jmp_buf env; // Doit être transverse à toutes les fonctions, donc globale
void foo() { longjmp(env, 42); }
void bar() { foo(); }
void baz() { bar(); }
int main() {
int ret = setjmp(env); // Sauvegarde l'état du programme
if (ret == 0) {
printf("Première exécution\n");
baz();
} else {
printf("Retour à setjmp avec %d\n", ret);
}
}
Lors de l'appel de setjmp, la fonction retourne 0. Cette valeur peut être utilisée pour tester si c'est la première fois que la fonction est appelée ou si c'est un retour de longjmp. Dans ce cas, la fonction retourne la valeur passée à longjmp.
<signal.h>¤
Les signaux sont des mécanismes spécifiques aux systèmes d'exploitations qui permettent de communiquer entre les processus (programmes) et le noyau. Un signal ne véhicule pas de données, il permet simplement de réveiller un processus pour lui indiquer qu'un événement s'est produit. Alternativement un signal peut être émis par un processus pour demander au noyau de réaliser une action.
Les signaux sont fondamentaux au sein d'un OS et de ce fait ils sont standardisés par POSIX. Windows utilise également des signaux mais ils diffèrent un peu. En C, la bibliothèque <signal.h> permet de manipuler les signaux de manière portable avec quelques nuances. Voici les types de signaux les plus courants :
| Signal | Description |
|---|---|
SIGABRT |
Abandon du processus |
SIGALRM |
Alarme horloge |
SIGFPE |
Erreur de calcul flottant |
SIGILL |
Instruction illégale |
SIGINT |
Interruption depuis le clavier |
SIGKILL |
Arrêt forcé du processus |
SIGPIPE |
Écriture dans un tube sans lecteur |
SIGSEGV |
Violation de segmentation |
SIGTERM |
Demande d'arrêt du processus |
SIGUSR1 |
Signal utilisateur 1 |
SIGUSR2 |
Signal utilisateur 2 |
La fonction abort() disponible dans <stdlib.h> envoie un signal SIGABRT pour arrêter le programme. Une violation d'accès à un espace mémoire non alloué envoie un signal SIGSEGV (la fameuse erreur de segmentation). Un dépassement de capacité en virgule flottante envoie un signal SIGFPE.
Pour envoyer un signal à un processus depuis le terminal, on utilise la commande kill. Par exemple, pour envoyer un signal SIGUSR1 au processus 1234, on utilise la commande kill -SIGUSR1 1234. Le 1234 est le PID (Process ID) du processus car chaque programme qui s'exécute a un identifiant unique attribué par le système d'exploitation. Le nom de la commande kill peut être trompeur car elle n'arrête pas le processus, elle lui envoie un signal. Le nom vient historiquement du fait que le signal par défaut est SIGTERM qui demande au processus de s'arrêter.
Dans un programme C il est possible de capturer les signaux reçus en installant des handlers. Il s'agit d'une fonction qui sera appelée de manière évènementielle lorsqu'un signal est reçu. Ces handlers court-circuitent donc le comportement normal du programme en interrompant l'action en cours. Voici un exemple pour capturer le signal SIGINT qui est envoyé lorsqu'on appuie sur Ctrl+C pour interrompre un programme depuis le terminal :
#include <signal.h>
#include <stdio.h>
void sigint_handler(int signum) {
printf("Vous partez déjà ? :(\n");
exit(0);
}
void jeudredi() {
char fruits[] = {"banane", "kiwi", "ananas", "mangue", "cerise"};
int i = 0;
while(1) {
printf("Il est très bon ce coktail à la %s !\n", fruits[i++ % 5]);
sleep(1); // Pause d'une seconde
}
}
int main() {
signal(SIGINT, sigint_handler);
jeudredi();
}
<stdalign.h>¤
La bibliothèque <stdalign.h> fournit des fonctions pour manipuler l'alignement des données en mémoire. L'alignement est une notion importante en informatique car les processeurs sont plus efficaces lorsqu'ils accèdent à des données alignées. Imaginez un camion qui transporte des palettes de marchandises. La logistique est faite de manière à ce que les palettes soient facile à charger et décharger du camion avec un minimum de manutention. Imaginez maintenant que vous voulez prendre un élément d'une palette. Cela demande plus de travail parce que vous devez extraire l'élément et trouver un autre outil pour le transporter. Un ordinateur 64-bits sur une architecture x86 a beaucoup de faciliter à véhiculer des mots de 8 octets et il s'arrangera en mémoire à disposer les données de la taille d'une palette (64-bits) de façon à ce que son accès soit le plus rapide possible.
La fonction alignof retourne l'alignement d'un type donné. Par exemple, alignof(int) retourne 4 sur une architecture x86 32-bits et 8 sur une architecture x86 64-bits. La fonction alignas permet de spécifier l'alignement d'une variable ou d'une structure. Dans cet exemple, on demande à ce que la variable a soit alignée sur 16 octets. L'adresse de a sera donc un multiple de 16. On peut le vérifier avec la fonction alignof.
#include <stdio.h>
#include <stdalign.h>
int main() {
alignas(16) int a;
printf("Alignement de int : %zu\n", alignof(int));
printf("Adresse de 'a' : %p\n", (void*)&a);
}
L'utilité de cette fonction est limitée en pratique. Elle est principalement utilisée pour manipuler des données SIMD (Single Instruction Multiple Data) ou pour optimiser les performances de certaines structures de données. On peut l'utiliser égalemenr pour accroître l'interopérabilité entre le C et d'autres langages de programmation.
Une utilisation courante est avec des structures. L'exemple suivant montre une structure de 13 bytes mais le processeur, selon l'architecture pourrait décider de stocker le char sur 4 bytes et la structure serait donc de 16 bytes. On peut forcer l'alignement de la structure manuellement avec alignas(16).
<stdarg.h>¤
Ne vous êtes-vous jamais demandé quel est le prototype de printf ? Comment se fait-il que cette fonction puisse prendre un nombre variable d'arguments ? La réponse est la bibliothèque <stdarg.h> qui permet de manipuler les arguments d'une fonction variable. Observons le prototype de printf :
Notez les points de suspension ... après le format. Il s'agit d'une fonction variadic, c'est-à-dire qu'elle peut prendre un nombre variable d'arguments. Rappelez-vous en relisant le chapitre sur la pile, que lorsqu'une fonction est appelée, les différents arguments sont empilés sur la pile. Il est techniquement possible d'empiler autant d'arguments que l'on veut sans changer le comportement de la fonction. Cette dernière lira simplements les arguments à partir de la base du frame pointer.
Le fichier d'en-tête déclare 4 macros et un type :
| Macro | Description |
|---|---|
va_list |
Type de la liste d'arguments |
va_start |
Initialise la liste d'arguments |
va_arg |
Récupère un argument de la liste |
va_copy |
Copie une liste d'arguments |
va_end |
Termine la liste d'arguments |
Imaginons le cas de figure suivant. Nous souhaitons écrire une fonction qui affiche la somme des valeurs passées en arguments et nous ne savons pas le nombre de valeurs qui seront passées. Voici comment procéder :
#include <stdarg.h>
#include <stdio.h>
int sum(int count, ...) {
va_list args;
va_start(args, count);
int sum = 0;
for (int i = 0; i < count; i++) sum += va_arg(args, int);
va_end(args);
return sum;
}
int main() {
printf("Somme : %d\n", sum(3, 1, 2, 3));
printf("Somme : %d\n", sum(5, 1, 2, 3, 4, 5));
}
Dans une fonction dont le prototype autorise un nombre variable d'arguments, l'ellipse ... est utilisée après au moins un argument fixe qui pourrait représenter le nombre d'arguments à suivre. Dans la fonction printf ce nombres est caché dans le format, en comptant le nombre de %. Dans cette fonction, une liste d'arguments args est déclarée. Lors de l'appel de va_start, la liste est initialisée à partir de l'argument suivant count. En pratique va_list est un pointeur sur la pile qui pointe sur l'argument suivant count. La fonction va_arg permet de récupérer les arguments un par un de façon portable. En pratique elle déréférence le pointeur et retourne le type spécifié comme deuxième argument. Enfin, va_end termine la liste d'arguments.
Voici ci-dessous comment elle pourrait être implémentée, néanmoins ces macros utilisent des fonctions précompilées (p. ex : __builtin_va_start) qui sont spécifiques à chaque compilateur.
typedef struct {
unsigned int gp_offset; // Offset dans les registres généraux
unsigned int fp_offset; // Offset dans les registres flottants
void* overflow_arg_area; // Pointeur vers les arguments passés sur la pile
void* reg_save_area; // Pointeur vers les registres sauvegardés
} va_list;
// Macro générique pour récupérer la taille d'un type
#define type_size(type) _Generic((type), \
int: sizeof(int), \
double: sizeof(double), \
float: sizeof(float), \
char: sizeof(char), \
default: sizeof(void*))
#define va_start(ap, last) __va_start(ap, &last, type_size(last))
#define va_arg(ap, type) \
*((type*)(ap.overflow_arg_area)); \
ap.overflow_arg_area += type_size(type)
#define va_end(ap) (void)0
void __va_start(va_list_hack* ap, void* last, size_t last_size) {
void *stack_pointer;
ap->overflow_arg_area = (void*)((char*)(&last) + last_size);
ap->gp_offset = 0;
ap->fp_offset = 0;
ap->reg_save_area = NULL;
}
<stdatomic.h>¤
Cet en-tête concerne la notion d'atomicité en programmation concurrente, et il pourrait s'agir d'un cours à part entière. L'atomicité est la propriété d'une opération qui est exécutée en une seule étape sans être interrompue. En d'autres termes, une opération atomique est une opération qui est soit complètement exécutée, soit pas du tout. Lorsqu'un programme utilise des threads (sous-programmes exécutés en parallèle), il est possible que deux exécutions parallèles tentent de modifier la même variable en même temps. Cela peut poser de gros problèmes de corruption de données. Vous savez par exemple qu'un entier est stocké sur 4 octets. On peut néanmoins imaginer une fonction d'échange de deux variables un peu naive qui traite chaque octet séparément.
union Int { int i; char c[4]; };
void swap(int *a, int *b) {
union Int tmp;
tmp.i = *a;
for (int i = 0; i < 4; i++) {
char t = ((union Int)*a).c[i];
((union Int)*a).c[i] = ((union Int)*b).c[i];
((union Int)*b).c[i] = t;
}
}
Dans le cas ou deux processus séparés essaient de traiter la valeur de a envoyée, l'un à swap et l'autre à printf. Le résultat dépendra et l'ordre d'exécution des instructions, et il se peut que printf affiche déjà quelque chose alors que swap n'a pas encore terminé. C'est ce qu'on appelle une condition de course. Bien entendu en pratique personne n'écrirait une fonction d'échange de variables de cette manière. Toutefois, pour résoudre ce type de problème on utilise des fonctions atomiques qui ajoutent une couche de protection à des variables partagées entre plusieurs threads.
Par conséquent, il est nécessaire d'utiliser un accesseur atomique pour lire et écrire la variable :
#include <stdatomic.h>
int main() {
atomic_int a = 42;
atomic_store(&a, 42); // Écriture atomique
int b = atomic_load(&a); // Lecture atomique
atomic_fetch_add(&a, 1); // Incrémentation atomique de 1
atomic_fetch_sub(&a, 1); // Soustraction atomique de 1
atomic_fetch_or(&a, 1); // ...
atomic_fetch_and(&a, 1);
atomic_fetch_xor(&a, 1);
atomic_exchange(&a, 42); // Notre fonction swap atomic
atomic_compare_exchange_strong(&a, &b, 42);
atomic_compare_exchange_weak(&a, &b, 42);
}
Pour de plus emples informations sur la programmation concurrente, je vous redirige sur un cours dédié à ce sujet.
<stdbit.h>¤
Cette bibliothèque a été introduite avec le standard C23 et elle permet de manipuler les bits de manière portable en fournissant des macros pour les opérations bit à bit. Les macro suivantes sont disponibles :
| Macro | Description |
|---|---|
stdc_popcount |
Compte le nombre de bits à 1 |
stdc_clz |
Compte le nombre de bits à 0 avant le premier bit à 1 |
stdc_ctz |
Compte le nombre de bits à 0 après le dernier bit à 1 |
stdc_rotl |
Rotation à gauche |
stdc_rotr |
Rotation à droite |
stdc_bswap |
Inversion des octets |
Bien entendu pour ces opérations, il est nécessaire de connaître la taille du type utilisé. C'est pourquoi ces macros sont génériques et utilisent _Generic pour déterminer la taille du type. Une rotation à droite pourrait être implémentée naïvement avec une macro :
#define stdc_rotl(x, n) \
((x << (n % (sizeof(x) * CHAR_BIT))) | (x >> ((sizeof(x) * CHAR_BIT) -
(n % (sizeof(x) * CHAR_BIT)))))
Néanmoins ces fonctions sont faites pour profiter des instructions spécifiques des processeurs modernes qui permettent de réaliser ces opérations de manière plus efficace. En effet dans l'architecture X86 par exemple il existe la directive assembleur ror pour la rotation à droite et rol pour la rotation à gauche. Ces instructions sont plus rapides que la méthode naïve ci-dessus mais elles n'existent pas nécessairement dans toutes les architectures. Du reste, si on essaye de compiler cette macro avec gcc et observons l'assembler généré, on constate que le compilateur utilise bien l'instruction ror pour la rotation à droite. Il est donc capable de comprendre le code et de l'optimiser en conséquence.
<stdbool.h>¤
Cette bibliothèque est apparue en C99 et après 20 ans d'attente, elle introduit enfin le type booléen bool et les valeurs true et false. Cet en-tête est par conséquent l'un des plus simple de la bibliothèque standard, car il ne contient que trois lignes :
Quelle belle mascarade ! On définit bool comme _Bool... En réalité ce dernier est un type natif du langage alors que bool n'est qu'une macro. C'est-à-dire que sans include <stdbool.h> vous pouvez tout de même définir un booléen en utilisant _Bool. Néanmoins, l'intérêt de cette bibliothèque est de standardiser le type booléen et les valeurs true et false pour une meilleure portabilité du code.
On peut s'interroger pourquoi le standard à décidé, plutôt que d'ajouter un nouveau type bool natif, que de le définir dans un en-tête supplémentaire et de définir le type avec un _ en préfixe. Le langage C a une longue histoire et de nombreux programmes n'ont pas attendu la sortie de cette bibliothèque pour définir leur propre type booléen. Il était donc nécessaire de ne pas casser la compatibilité avec les anciens programmes. C'est pourquoi le type _Bool a été introduit sous cette nomenclature. L'histoire est similaire avec _Complex et _Imaginary de la bibliothèque <complex.h>.
Notez que le _Bool est un type très particulier car il est en réalité souvent implémenté comme un entier 8-bit. Rappelez-vous que le processeur aime manipuler des mots de la taille de son bus de données. C'est pourquoi un booléen est souvent stocké sur 32-bits même si un seul bit suffirait. C'est pourquoi on peut stocker des valeurs autres que true et false dans un booléen. Par exemple, bool b = 42 est tout à fait valide en C. En réalité, true et false sont des macros qui valent respectivement 1 et 0. C'est pourquoi on peut les utiliser pour initialiser des booléens.
_Bool b = 1;
printf("%ld\n", sizeof(b)); // Affiche 1
*((int*)&b) += 10; // Bypass le système de typage
printf("%hhd\n", (int)b); // Affiche 11
Il est nécessaire de gruger pour ajouter 10 car naïvement le compilateur implémente b += 10 avec :
Dans le cas des tableaux, ne perdez pas à l'esprit qu'un tableau de booléens est un tableau d'octets :
bool bool_array[8] = {true, false, true, true, false, false, true, true};
assert(sizeof(bool_array) == 8);
<stdckdint.h>¤
Cette bibliothèque est apparue en C23 et propose des fonctions arithmétiques pour les opérations de base comme l'addition, la soustraction, et la multiplication, mais avec une détection explicite de l'overflow. L'abbréviation ckd signifie checked. Les fonctions introduites par cet en-tête sont :
| Fonction | Description |
|---|---|
ckd_add |
Addition avec détection de l'overflow |
ckd_sub |
Soustraction avec détection de l'overflow |
ckd_mul |
Multiplication avec détection de l'overflow |
Lorsque deux entiers sont additionnés, il est possible que le résultat dépasse la capacité de stockage de l'entier. Par exemple, si on additionne \(2^31 - 1\) à 1, on obtient \(-2^31\). C'est ce qu'on appelle un overflow. En C, l'overflow est un comportement indéfini, c'est-à-dire que le compilateur est libre de décider du comportement du programme. En pratique, la plupart des compilateurs vont simplement ignorer l'overflow et le résultat sera tronqué. C'est pourquoi il est important de vérifier les overflows lorsqu'on travaille avec des entiers.
Avant l'introduction de cette bibliothèque, un dépassement de capacité devrait être manuellement implémenté :
Avec la bibliothèque <stdckdint.h>, il est possible de détecter l'overflow de manière plus élégante :
#include <stdckdint.h>
int add(int a, int b) {
int result;
if (ckd_add(a, b, &result)) {
// Overflow détecté
} else
return result;
}
<stddef.h>¤
La bibliothèque <stddef.h> fournit quelques définitions utiles tel que donné par la table suivante :
| Macro | Description |
|---|---|
NULL |
Pointeur nul |
offsetof |
Offset d'un membre d'une structure |
ptrdiff_t |
Type pour les différences de pointeurs |
size_t |
Type pour les tailles d'objets |
wchar_t |
Type pour les caractères larges |
nullptr_t |
Pointeur null (C23) |
max_align_t |
Type pour l'alignement maximal |
L'implémentation probable de ces macros est la suivante :
#define NULL ((void*)0)
#define offsetof(type, member) ((size_t)&((type*)0)->member)
typedef unsigned long size_t
typedef ptrdiff_t long
typedef int wchar_t`
Concernant les pointeurs, s'il est parfaitement correct de tester si un pointeur vaut 0 (if (ptr == 0)), il est recommandé d'utiliser NULL pour plus de clarté. Le standard C23 à introduit le type nullptr_t pour les pointeurs nuls. Il est recommandé de l'utiliser à la place de NULL.
max_align_t-
Il s'agit d'un type un type introduit en C11 pour représenter l'alignement maximal possible pour n'importe quel type. Il est utilisé pour définir des types alignés de manière optimale. Par exemple, si on veut définir une structure alignée sur 16 octets, on peut utiliser
max_align_tpour définir le type de la structure. size_t-
Il s'agit d'un type non signé qui est utilisé pour représenter la taille d'un objet en mémoire. Il est utilisé pour les fonctions qui retournent la taille d'un objet, comme
strlenousizeof. Il est défini tel que sa taille soit suffisante, en pratique 64-bits sur une architecture 64-bits. ptrdiff_t-
Il s'agit d'un type signé qui est utilisé pour représenter la différence entre deux pointeurs. Lorsque l'on veut calculer
ptr_p - ptr_qon obtient un entier dont la valeur maximale dépend de la taille de la mémoire adressable.
<inttypes.h> et <stdint.h>¤
Ces deux bibliothèques répondent au besoin d'avoir des types entiers d'une taille contrôlée et surtout portable. En effet, nous avons vu que les types standards (int, short, long...) dépendent du modèle de données de l'architecture cible. Un long n'aura pas la même taille sur Linux ou Windows par exemple.
L'en-tête <stint.h> fourni trois types de base :
| Exemple | Description |
|---|---|
int8_t |
Entier signé sur 8 bits |
int8_fast8_t |
Entier signé d'au moins 8 bits le plus rapide |
int8_least8_t |
Entier signé d'au moins 8 bits, le plus petit |
Ces catégories sont disponibles our les longueurs 8, 16, 32, 64 bits. Les types sont définis pour les entiers signés et non signés. Par exemple, int8_t est un entier signé sur 8 bits, uint8_t est un entier non signé sur 8 bits.
Dans le cas ou on aurait besoin d'une variable pouvant contenir les valeurs de 0 à 255 mais que la taille de l'entier importe peu pour autant que le processeur n'ait pas de coût supplémentaire à manipuler la variable, on peut utiliser uint_fast8_t.
À l'inverse, si le besoin est d'avoir une variable qui peut contenir les valeurs de 0 à 255 avec la taille la plus petite possible (idéalement 8 bits), on utilisera uint_least8_t.
Enfin, dans le cas (le plus rare) ou on aurait besoin exactement d'un entier non signé de 8 bits, on utilisera uint8_t. Néanmoins ce type présente une contrainte importante car toutes les architectures ne sont pas nécessairement prévues pour manipuler des entiers de 8 bits. Par exemple le SHARC d'Analog Devices est un processeur 32 bits qui n'a pas de support natif pour les entiers de 8 bits. L'utilisation de uint8_t résulterait en une erreur de compilation.
L'en-tête <stdint.h> fournit également des macros utiles pour connaître le choix de l'implémentation. Par exemple, INT_FAST8_WIDTH donne la largeur de l'entier le plus rapide selon la machine cible.
On aura également les valeurs minimum et maximum que peut contenir chacun des types entiers. Par exemple, INT8_MIN et INT8_MAX pour les entiers signés sur 8 bits.
Dans une boucle for opérant sur un tableau de 100 éléments, il serait correct d'utiliser le type uint_fast8_t pour l'index de la boucle. Néanmoins pour des raisons de lisibilités, il est souvent préférable d'utiliser simplement int qui, selon le standard, garanti d'être capable de contenir la taille du tableau.
En outre, pour des raisons de cohérence, certaines normes pour l'avionique ou le médical imposent que les constantes littérales soient explicitement typées. On connaît déjà les suffixes u, ull pour les entiers de base, mais on peut également utiliser les macros de <stdint.h> pour les constantes littérales.
L'utilisation de ces types spécifiques dans des fonctions d'entrées sortie (p. ex. printf) doit aussi être faite cohérence. Un int32_t n'est pas compatible avec %d sur toutes les architectures. Il est préférable d'utiliser les macros de <inttypes.h> pour les spécifier.
<stdio.h>¤
La bibliothèque <stdio.h> est l'une des bibliothèques les plus importantes en C. Elle fournit des fonctions pour l'entrée et les sorties, c'est-à-dire pour lire et écrire des données depuis et vers la console. Elle fournit également des fonctions pour lire et écrire des fichiers.
La plupart des fonctions de cette bibliothèque ont déjà été abordées dans les chapitres précédents. Voici ci-dessous un résumé des fonctions qu'elle contient. Le (f) indique que la fonction peut être utilisée pour lire ou écrire depuis un fichier, elle prend un pointeur de type FILE en premier argument. Le (w) indique que la fonction est prévue pour les caractères larges (wchar_t).
| Fonction | Description |
|---|---|
(f)get(w)c |
Lit un caractère depuis l'entrée standard ou un fichier |
(f)get(w)s |
Lit une ligne depuis l'entrée standard ou un fichier |
(f)put(w)c |
Écrit un caractère vers l'entrée standard ou un fichier |
(f)put(w)s |
Écrit une chaîne de caractères vers la sortie standard ou un fichier |
clearerr |
Réinitialise l'état d'erreur d'un flux |
fclose |
Ferme un fichier ouvert |
feof |
Vérifie si la fin du fichier est atteinte |
ferror |
Vérifie si une erreur est survenue dans le flux |
fflush |
Vide le tampon de sortie d'un flux |
fgetpos |
Obtient la position actuelle dans un fichier sous forme de fpos_t |
fileno |
Obtient le descripteur de fichier associé à un flux |
flockfile |
Verrouille un flux pour les opérations multithreadées |
fopen |
Ouvre un fichier pour la lecture, l'écriture ou l'ajout |
fread |
Lit des blocs d'octets depuis un flux |
freopen |
Ouvre à nouveau un fichier sur un flux de fichier existant |
fscanf |
Lit des données formatées depuis un fichier |
fseek |
Positionne le curseur de lecture/écriture dans un fichier |
fsetpos |
Définit la position actuelle dans un fichier selon un objet fpos_t |
ftell |
Renvoie la position actuelle dans un fichier |
ftrylockfile |
Tente de verrouiller un flux pour les opérations multithreadées |
funlockfile |
Déverrouille un flux verrouillé |
fwrite |
Écrit des blocs d'octets vers un flux |
perror |
Affiche un message d'erreur basé sur la dernière erreur rencontrée |
remove |
Supprime un fichier |
rename |
Renomme un fichier |
rewind |
Remet le curseur au début d'un fichier |
scanf |
Lit des données formatées depuis l'entrée standard |
setbuf |
Définit un tampon pour un flux |
setvbuf |
Définit le mode de tampon pour un flux |
sscanf |
Lit des données formatées depuis une chaîne |
tmpfile |
Crée et ouvre un fichier temporaire qui est supprimé à la fermeture |
tmpnam |
Génère un nom de fichier temporaire unique |
ungetc |
Remet un caractère dans le flux pour qu'il soit lu à nouveau |
ungetwc |
Remet un caractère large dans le flux |
| Fonction | Description |
|---|---|
vfprintf |
Écrit une sortie formatée sur un flux avec une liste variadiques |
vprintf |
Écrit une sortie formatée sur stdout avec une liste variadiques |
vsprintf |
Écrit une sortie formatée dans une chaîne avec une liste variadiques |
vfwprintf |
Version large de vfprintf pour les caractères larges (wchar_t) |
vwprintf |
Version large de vprintf pour les caractères larges (wchar_t) |
vswprintf |
Version large de vsprintf pour les caractères larges (wchar_t) |
fprintf |
Écrit une sortie formatée dans un fichier |
printf |
Écrit une sortie formatée sur stdout |
sprintf |
Écrit une sortie formatée dans une chaîne |
snprintf |
Écrit une sortie formatée dans une chaîne avec une taille limitée |
fwprintf |
Version large de fprintf pour les caractères larges (wchar_t) |
wprintf |
Version large de printf pour les caractères larges (wchar_t) |
swprintf |
Version large de sprintf pour les caractères larges (wchar_t) |
| Constante/Type | Description |
|---|---|
EOF |
Constante retournée par les fonctions de lecture lorsqu'une fin de fichier ou une erreur est rencontrée |
NULL |
Pointeur nul utilisé pour représenter l'absence d'objet |
FILENAME_MAX |
Longueur maximale d'un nom de fichier |
FOPEN_MAX |
Nombre maximal de fichiers pouvant être ouverts simultanément |
L_tmpnam |
Longueur minimale d'un tampon pour tmpnam |
BUFSIZ |
Taille du tampon par défaut pour les opérations de lecture/écriture |
TMP_MAX |
Nombre maximal de noms uniques générés par tmpnam |
SEEK_SET |
Indique le début du fichier pour fseek et fseeko |
SEEK_CUR |
Indique la position actuelle dans le fichier pour fseek et fseeko |
SEEK_END |
Indique la fin du fichier pour fseek et fseeko |
stderr |
Flux de sortie d'erreur standard |
stdin |
Flux d'entrée standard |
stdout |
Flux de sortie standard |
FILE |
Type opaque représentant un flux de fichier |
fpos_t |
Type utilisé pour stocker la position dans un fichier |
<stdlib.h>¤
Cette bibliothèque contient des fonctions éparses qui ne sont pas assez importantes pour être regroupées dans une bibliothèque dédiée. Contrairement aux langages plus récents (comme C++ ou Java), C n'a pas été conçu avec une philosophie de modularité stricte pour les bibliothèques. Les fonctions étaient rassemblées par utilité pratique plutôt que par sujet spécifique, et les bibliothèques étaient assez limitées en nombre pour garder le langage simple et portable. On y retrouve les catégories suivantes :
- Gestion de la mémoire
- Conversion de chaînes en types numériques
- Gestion du programme
- Nombres aléatoires
- Algorithmes de recherche et de tri
| Fonction | Description |
|---|---|
abort |
Arrête le programme de manière anormale sans nettoyage des ressources |
exit |
Arrête le programme de manière normale avec nettoyage des ressources |
quick_exit |
Arrête le programme de manière normale sans nettoyage complet des ressources (C11) |
_Exit |
Arrête le programme de manière normale sans nettoyage des ressources (C99) |
atexit |
Enregistre une fonction à appeler lors de l'appel à exit |
at_quick_exit |
Enregistre une fonction à appeler lors de l'appel à quick_exit (C11) |
getenv |
Récupère la valeur d'une variable d'environnement |
setenv |
Ajoute ou modifie une variable d'environnement (POSIX, non standard) |
putenv |
Ajoute ou modifie une variable d'environnement |
unsetenv |
Supprime une variable d'environnement (POSIX, non standard) |
system |
Exécute une commande système dans un shell |
rand |
Génère un nombre pseudo-aléatoire |
srand |
Initialise le générateur de nombres pseudo-aléatoires |
mblen |
Retourne le nombre d'octets d'un caractère multioctet dans une chaîne |
| Fonction | Description |
|---|---|
malloc |
Alloue un bloc de mémoire |
calloc |
Alloue et initialise un bloc de mémoire |
realloc |
Redimensionne un bloc de mémoire précédemment alloué |
free |
Libère un bloc de mémoire précédemment alloué |
aligned_alloc |
Alloue un bloc de mémoire aligné (C11) |
free_sized |
Libère un bloc de mémoire de taille spécifiée (C23) |
free_aligned_sized |
Libère un bloc de mémoire aligné de taille spécifiée (C23) |
| Fonction | Description |
|---|---|
bsearch |
Recherche un élément dans un tableau trié en utilisant une fonction de comparaison |
qsort |
Trie un tableau en utilisant un algorithme de tri rapide (quick sort) |
| Fonction | Description |
|---|---|
abs |
Calcule la valeur absolue d'un entier (int) |
labs |
Calcule la valeur absolue d'un entier long (long) |
llabs |
Calcule la valeur absolue d'un long long (long long) (C99) |
div |
Effectue une division entière et retourne le quotient et le reste pour les int |
ldiv |
Effectue une division entière pour les long et retourne quotient et reste |
lldiv |
Effectue une division entière pour les long long et retourne quotient et reste (C99) |
| Fonction | Description |
|---|---|
atof |
Convertit une chaîne de caractères en double (double) |
atoi |
Convertit une chaîne de caractères en entier (int) |
atol |
Convertit une chaîne de caractères en long (long) |
atoll |
Convertit une chaîne de caractères en long long (long long) (C99) |
mbstowcs |
Convertit une chaîne multioctet en chaîne de caractères larges (wchar_t) |
mbtowc |
Convertit un caractère multioctet en caractère large (wchar_t) |
strtod |
Convertit une chaîne en double (double) |
strtof |
Convertit une chaîne en float (float) (C99) |
strtol |
Convertit une chaîne en long (long), avec une base personnalisable |
strtold |
Convertit une chaîne en long double (long double) (C99) |
strtoll |
Convertit une chaîne en long long (long long) (C99) |
strtoul |
Convertit une chaîne en unsigned long (unsigned long) |
strtoull |
Convertit une chaîne en unsigned long long (unsigned long long) (C99) |
wcstombs |
Convertit une chaîne de caractères larges en chaîne multioctet |
wctomb |
Convertit un caractère large (wchar_t) en multioctet |
| Constante/Type | Description |
|---|---|
EXIT_SUCCESS |
Indique une terminaison réussie du programme (valeur utilisée avec exit) |
EXIT_FAILURE |
Indique une terminaison échouée du programme (valeur utilisée avec exit) |
NULL |
Pointeur nul, utilisé pour initialiser ou tester des pointeurs |
RAND_MAX |
Valeur maximale que peut retourner rand |
MB_CUR_MAX |
Taille maximale d'un caractère multioctet pour la locale courante |
size_t |
Type pour représenter des tailles et des dimensions |
div_t |
Structure retournée par div contenant le quotient et le reste |
ldiv_t |
Structure retournée par ldiv contenant le quotient et le reste |
lldiv_t |
Structure retournée par lldiv (C99) contenant le quotient et le reste |
wchar_t |
Type pour représenter un caractère large |
mbstate_t |
Type utilisé pour conserver l'état entre conversions de caractères multioctets et caractères larges |
<stdnoreturn.h>¤
Cette bibliothèque est apparue en C11 et elle introduit le type noreturn qui est utilisé pour indiquer qu'une fonction ne retourne jamais. Cela permet au compilateur d'optimiser le code en supprimant les instructions de retour de la fonction. En pratique, cela permet de gagner quelques cycles d'horloge. Voici un exemple d'utilisation :
#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>
noreturn void exit_now(int i)
{
if (i > 0)
exit(i);
// Si i <= 0, le comportement est indéfini
}
int main(void)
{
puts("Se prépare à terminer le programme...");
exit_now(2);
puts("On sait que ce code n'est jamais exécuté.");
}
Avant C23, il fallait utiliser _Noreturn.
<string.h>¤
La bibliothèque <string.h> contient des fonctions pour manipuler les chaînes de caractères. Les fonctions sont définies pour les chaînes de caractères ASCII uniquement. On distingue deux famille de fonctions, les mem qui manipulent des régions mémoires et les str qui manipulent des chaînes de caractères.
La table suivante résume les fonctions les plus utilisées. On notera que les lettres entre parenthèses indiquent les variantes des fonctions. La fonction strcpy existe en version strncpy qui permet de copier une chaîne en spécifiant la taille maximale à copier. On notera n pour les fonctions dont la taille maximum de la chaîne peut être spécifiée, r pour reverse et c pour not in.
| Fonction | Description |
|---|---|
memset |
Remplissage d'une région mémoire |
memcpy |
Copie d'une région mémoire |
memmove |
Copie d'une région mémoire avec superposition |
memcmp |
Comparaison de deux régions mémoire |
memchr |
Recherche d'un caractère dans une région mémoire |
strlen |
Longueur de la chaîne |
str(n)cpy |
Copie d'une chaîne |
str(n)cat |
Concaténation de deux chaînes |
str(n)cmp |
Comparaison de deux chaînes |
str(r)chr |
Recherche d'un caractère dans une chaîne |
strstr |
Recherche d'une sous-chaîne dans une chaîne |
strtok |
Découpe une chaîne en morceaux |
strspn |
Longueur du préfixe d'une chaîne |
strcspn |
Longueur du préfixe qui ne contient pas certains caractères |
strpbrk |
Recherche d'un caractère dans une liste |
strcoll |
Comparaison de chaînes selon la locale |
strxfrm |
Transformation de chaînes selon la locale |
strerror |
Message d'erreur associé à un code d'erreur |
memset¤
La fonction memset permet de remplir une région mémoire avec une valeur donnée. Son prototype est :
Elle est utilisée principalement pour initialiser ou réinitialiser un tableau en une seule instruction qui sera plus performante qu'une boucle. L'exemple suivant initialise un tableau de 100 éléments avec la valeur 42 :
Notez que la valeur est un byte. Memset ne peut pas être utilisé pour initialiser un tableau avec une valeur de type int par exemple.
memcpy et memmove¤
Les deux fonctions permettent de copier des régions mémoires d'une adresse à une autre. Leur prototype est le suivant :
void *memmove(void *dest, const void *src, size_t n)
void *memcpy(void *dest, const void *src, size_t n)
La différence principale est que memcpy ne traite pas les cas où les deux régions mémoire se superposent. memmove est plus sûr et plus lent que memcpy. En effet, memmove doit vérifier si les deux régions mémoire se superposent et dans ce cas, elle doit copier les données dans un buffer temporaire avant de les copier dans la région de destination. memcpy ne fait pas cette vérification et copie directement les données.
Pour comprendre ce problème de superposition prenons l'exemple ci-dessous. La fonction memcpy est réimplémentée pour avoir une résultat prévisible. Un pointeur p est déclaré et un pointeur q correspond à l'adresse de p mais décalé de deux éléments. Les deux espaces mémoire se superposent donc. Après copie on devrait obtenir 12345 dans q mais après la copie mais on obtient 12121. Il y a donc un problème.
int mymemcpy(void *dest, const void *src, size_t n) {
char *d = dest;
const char *s = src;
for (size_t i = 0; i < n; i++) d[i] = s[i];
return 0;
}
int main() {
char s[] = "12345..";
char *p = s;
char *q = p + 2;
mymemcpy(q, p, 5);
for (int i = 0; i < 5; i++) printf("%c", q[i]);
printf("\n");
}
Pour vous en convaincre, vous pouvez vous aider de la figure suivante.
En réalité, le fonctionnement de memcpy n'est pas si simple. En effet, le compilateur peut optimiser le code et utiliser des instructions SIMD pour copier les données. Cela va également dépendre du niveau d'optimisation du compilateur. En exécutant le même code avec memcpy, je n'obtiens pas le même résultat, en observant le code assembleur généré pour memcpy, on observe que les premiers 4 octets sont copiés en une seule instruction, le cinquième octet est copié en une instruction séparée. Ce qui donne comme résultat 12343.
mov rcx, qword ptr [rbp - 128] ; Charge l'adresse de destination (q) dans rcx
mov rdx, qword ptr [rbp - 120] ; Charge l'adresse source (p) dans rdx
mov esi, dword ptr [rdx] ; Charge les 4 premiers octets dans esi
mov dword ptr [rcx], esi ; Copie ces 4 octets dans la destination
mov r8b, byte ptr [rdx + 4] ; Charge le 5e octet de la source dans r8b
mov byte ptr [rcx + 4], r8b ; Copie le 5e octet dans la destination
Pour s'affranchir de ce type de problème, il est préférable d'utiliser memmove lorsque vous n'êtes pas sûr que les deux régions mémoire ne se superposent pas.
memcmp¤
La fonction memcmp permet de comparer deux régions mémoires. Son prototype est :
On utilise typiquement memcmp pour comparer deux structures. Il n'est en effet pas possible de comparer deux structures directement avec l'opérateur ==. On utilisera plutôt :
struct Person {
char firstname[64];
char lastname[64];
Genre genre;
int age;
};
Person p, q;
// Do something with p and q
if (memcmp(&p, &p, sizeof(struct Person)) == 0)
printf("Les deux personnes sont identiques\n");
Comparaison de chaînes de caractères
Attention néanmoins au cas de figure donné. Les champs firstname et lastname sont des buffers de 64 caractères. Si les deux structures contiennent les mêmes prénoms et noms il n'y a aucune garantie que les deux structures soient égales. En effet, après le caractère '\0', rien n'oblige l'utilisateur à remplir le reste du buffer avec des '\0'.
Il serait ici préférable de tester individuellement les champs de la structure.
memchr et str(r)chr¤
Les fonctions memchr, strchr et strrchr permettent de rechercher un caractère dans une région mémoire. Leurs prototypes sont :
void *memchr(const void *s, int c, size_t n);
char *strchr(const char *s, int c);
char *strrchr(const char *s, int c);
Une utilisaiton typique de strchr ou strrchr est de rechercher l'occurence d'un caractère dans une chaîne de caractères. La fonction s'arrête dès qu'elle trouve le caractère recherché ou qu'elle atteint la fin de la chaîne.
char *s = "Anticonsitutionnellement";
char *p = strchr(s, 'i');
assert(p != NULL); // Caractère trouvé
assert(*p == 'i'); // Caractère trouvé est 'i'
assert(p - s == 3); // Position de 'i' dans la chaîne
// Recherche depuis la fin
p = strrchr(s, 'i');
assert(p - s == 12); // Dernière position de 'i' dans la chaîne
Dans le cas de memchr, il est possible de chercher n'importe quelle valeur de byte, y compris '\0'. En revanche, il est nécessaire de spécifier la taille de la région mémoire à parcourir.
strlen¤
La fonction strlen permet de calculer la longueur d'une chaîne de caractères. Son prototype est :
Son implémentation pourrait être la suivante :
str(n)cpy¤
Les fonctions strcpy et strncpy permettent de copier une chaîne de caractères. Leur prototype sont :
La fonction strcpy copie la chaîne de caractères src dans dest. La fonction strncpy copie au maximum n caractères de src dans dest. Si la chaîne src est plus longue que n, la chaîne dest ne sera pas terminée par '\0'.
str(n)cat¤
Les fonctions strcat et strncat permettent de concaténer deux chaînes de caractères. Leur prototype sont :
La fonction strcat concatène la chaîne de caractères src à la fin de dest. La fonction strncat concatène au maximum n caractères de src à la fin de dest. Si la chaîne src est plus longue que n, la chaîne dest ne sera pas terminée par '\0'.
char dest[20] = "Hello, "; // dest est plus grand que src !
char src[] = "World!";
strcat(dest, src);
printf("%s\n", dest); // Affiche "Hello, World!"
L'implémentation de strcat pourrait être la suivante :
char *strcat(char *dest, const char *src) {
while (*dest != '\0') ++dest;
while ((*dest++ = *src++) != '\0');
return dest;
}
strcmp et strncmp¤
La fonction strcmp permet de comparer deux chaînes de caractères. Son prototype est :
Elle sera utilisée principalement pour comparer deux chaînes de caractères. Une utlisation typique est de tester si deux chaînes sont égales. Par exemple :
char *owner = "John";
char user[64];
if (scanf("%63s", user) == 1 && memcmp(owner, user) == 0) {
printf("Welcome John!\n");
}
Le strncmp permet de forcer la comparaison à s'arrêter après un certain nombre de caractères. C'est particulièrement utile pour le traitement des arguments de ligne de commande. Par exemple :
if (strncmp(argv[1], "--filename=", 11) == 0) {
printf("Ouverture du fichier %s\n", argv[1] + 11);
}
strtok¤
La fonction strtok permet de découper une chaîne de caractères en morceaux. Il s'agit de l'abbréviation de string token. Son prototype est :
La première fois que la fonction est appelée, elle prend en paramètre la chaîne à découper. Les appels suivants doivent passer NULL en premier paramètre. La fonction retourne un pointeur sur le début du morceau suivant. La fonction modifie la chaîne passée en paramètre en insérant des '\0' à la place des délimiteurs.
char s[] = "mais,ou,est,donc,or,ni,car";
char *token = strtok(s, ",");
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, ",");
}
strspn et strcspn¤
Les fonctions strspn et strcspn permettent de calculer la longueur du préfixe d'une chaîne qui contient ou ne contient pas certains caractères. Leur prototype est :
size_t strspn(const char *s, const char *accept);
size_t strcspn(const char *s, const char *reject);
Une utilisation typique de strspn est de valider une chaîne de caractères. Par exemple, pour valider un nombre entier :
char *s = "12345";
if (strspn(s, "0123456789") == strlen(s)) {
printf("La chaîne %s est un nombre entier\n", s);
}
Inversement, strcspn permet de valider une chaîne de caractères qui ne contient pas certains caractères. Par exemple pour aider Georges Perec à écrire un lipogramme sans la lettre e :
char *s = "La disparition";
if (strcspn(s, "e") == strlen(s)) {
printf("La chaîne %s ne contient pas la lettre 'e'\n", s);
}
strpbrk¤
La fonction strpbrk permet de rechercher un caractère dans une liste de caractères. Son prototype est :
Elle retourne un pointeur sur le premier caractère de s qui appartient à accept. Par exemple, pour chercher les opérateurs utilisés dans une chaîne de caractères :
char *s = "1 + 2 * 4 + 8";
while (s = strpbrk(s, "+-*/%%")) {
printf("Opérateur trouvé : %c\n", *s);
s++;
}
strcoll et strxfrm¤
Les fonctions strcoll et strxfrm permettent de comparer des chaînes de caractères selon la locale. Elles sont l'abbréviation de string collate où collate fait référence au tri ou à l'ordre de classement des chaînes de caractères en fonction des conventions locales. strxfrm est l'abbréviation de string transform et permet de transformer une chaîne de caractères en une chaîne de caractères qui peut être comparée avec strcmp. Leur prototype est :
Ces deux fonctions sont utilisées pour comparer des chaînes de caractères en tenant compte des conventions de tri locales, qui peuvent varier d'une langue ou d'un jeu de caractères à un autre. Elles sont principalement utilisées pour des opérations de tri ou de comparaison dans des contextes où il est important de respecter l'ordre défini par les paramètres régionaux (locales). Ces fonction utlisent donc les paramètres régionaux définis par la fonction setlocale de la bibliothèque <locale.h>.
Pourquoi ces deux fonctions étranges ? Comparer deux chaînes n'est pas facile surtout s'il y a des diacritiques. Selon la langue, les chaînes de caractères ne sont pas forcément comparées de la même manière. En français on considère alphabétiquement que é est juste après e dans l'alphabet, en anglais les deux lettres sont équivalentes. En suédois, les lettres å, ä sont placées après le z, et les règles sont nombreuses.
strxfrm permet de transformer une chaîne de caractères en une chaîne de caractères dite « collationnée » qui peut être comparée ensuite rapidement avec strcmp. Cela peut être utile lors de comparaison répétées.
Notons que ces fonctions ne sont plus vraiment utilisées car elles se limitent au jeux de caractères ISO-8859, et le support Unicode est limité. Pour une gestion correcte il vaut mieux faire appel à des bibliothèques plus spécialisées comme ICU qui offre la fonction ucol_strcoll pour comparer des chaînes de caractères Unicode.
strerror¤
La fonction strerror permet de récupérer un message d'erreur associé à un code d'erreur. Son prototype est :
Elle est utilisée principalement pour afficher des messages d'erreur associés à des codes d'erreur. Par exemple :
FILE *f = fopen("file.txt", "r");
if (f == NULL) {
fprintf(stderr, "Erreur lors de l'ouverture du fichier : %s\n",
strerror(errno));
}
<tgmath.h>¤
La bibliothèque <tgmath.h> est une bibliothèque de type générique qui permet de définir des fonctions mathématiques qui acceptent des arguments de différents types. Par exemple, la fonction sqrt peut accepter un argument de type float, double ou long double.
Il est courant de ne pas utiliser la bonne fonction mathématique pour un type donné. Par exemple, on peut appeler sqrt avec un argument de type float alors que la fonction sqrtf est plus adaptée peut entraîner une perte de performance, l'inverse peut entraîner une perte de précision. La bibliothèque <tgmath.h> permet de résoudre ce problème en définissant des fonctions mathématiques génériques qui acceptent des arguments de différents types.
Cette généricité est permise à l'aide du mot clé _Generic introduit en C11.
La bibliothèque redéfini les fonctions mathématiques de la bibliothèque <math.h>, pour l'utiliser il suffit d'inclure l'en-tête <tgmath.h> à la place de <math.h>. Par exemple, pour calculer la racine carrée d'un nombre, on peut utiliser la fonction sqrt de la bibliothèque <tgmath.h> :
<threads.h>¤
La bibliothèque <threads.h> contient des fonctions pour créer et gérer des threads. Les threads sont aussi nommés des processus légers qui partagent le même espace mémoire. Un thread peut être vu comme un sous-programme parallèle tournant dans le même programme. Les fonctions offertes par le standard sont les suivantes :
| Fonction | Description |
|---|---|
thrd_create |
Crée un nouveau thread |
thrd_exit |
Termine le thread |
thrd_join |
Attend la fin d'un thread |
thrd_sleep |
Met le thread en sommeil |
thrd_yield |
Passe la main à un autre thread |
mtx_init |
Initialise un mutex |
mtx_lock |
Verrouille un mutex |
mtx_trylock |
Tente de verrouiller un mutex |
mtx_unlock |
Déverrouille un mutex |
mtx_destroy |
Détruit un mutex |
cnd_init |
Initialise une variable de condition |
cnd_signal |
Signale une variable de condition |
cnd_broadcast |
Signale toutes les variables de condition |
cnd_wait |
Attend une variable de condition |
cnd_destroy |
Détruit une variable de condition |
Pour plus de détails sur le fonctionnement des threads, vous pouvez consulter un cours spécialisé sur la programmation concurrente.
<time.h>¤
La bibliothèque <time.h> contient des fonctions pour lire et convertir des dates et heures. Les fonctions sont définies pour les dates et heures en secondes depuis le 1er janvier 1970.
| Fonction | Description |
|---|---|
time |
Temps écoulé depuis le 1er janvier 1970 |
localtime |
Convertit le temps en heure locale |
gmtime |
Convertit le temps en heure UTC |
asctime |
Convertit le temps en chaîne de caractères |
ctime |
Convertit le temps en chaîne de caractères |
strftime |
Convertit le temps en chaîne de caractères |
mktime |
Convertit une structure en temps |
difftime |
Différence entre deux temps |
clock |
Temps CPU utilisé par le programme |
Stucture tm¤
Les fonctions de date et d'heure utilisent la structure tm pour représenter les dates et heures. La structure est définie comme suit :
struct tm {
int tm_sec; // Secondes (0-59)
int tm_min; // Minutes (0-59)
int tm_hour; // Heures (0-23)
int tm_mday; // Jour du mois (1-31)
int tm_mon; // Mois (0-11)
int tm_year; // Année - 1900
int tm_wday; // Jour de la semaine (0-6, dimanche = 0)
int tm_yday; // Jour de l'année (0-365)
int tm_isdst; // Heure d'été (0, 1, -1)
};
time¤
La fonction time permet de récupérer le temps écoulé depuis le 1er janvier 1970. Elle prend en paramètre un pointeur sur un time_t qui contiendra le temps écoulé. Ce dernier peut être NULL si on ne souhaite pas récupérer le temps. Le prototype de la fonction est le suivant :
Un exemple d'utilisation est le suivant :
time_t t;
time(&t);
printf("Time since 1st January 1970 : %ld seconds\n", t);
// Ou sans récupérer le temps
printf("Time since 1st January 1970 : %ld seconds\n", time(NULL));
Pourquoi le 1er janvier 1970 ? C'est une convention qui remonte aux premiers systèmes Unix. Le temps est stocké en secondes depuis cette date. C'est ce qu'on appelle le temps Unix ou temps POSIX.
Problème de l'an 2038
Sur de nombreux systèmes 32 bits historiques, le type time_t est codé sur 32 bits signés. Dans ce cas, la valeur maximale représente le 19 janvier 2038 à 03:14:07 UTC : au-delà, l'entier déborde et les dates rebasculent en 1901. C'est ce qu'on appelle le bug de l'an 2038. Les plates-formes modernes utilisent désormais un time_t sur 64 bits (par exemple un long sur les systèmes Linux 64 bits ou un long long sur Windows récents), ce qui repousse l'échéance à des horizons astronomiquement lointains.
Pourquoi avoir deux moyen de retourner le temps ? C'est une question de style. Certains préfèrent récupérer le temps dans une variable, d'autres préfèrent le récupérer directement sans variable intermédiaire.
localtime et gmtime¤
Ces deux fonctions permettent de convertir un temps en heure locale ou en heure UTC. Leur prototype est le suivant :
localtime se base sur les paramètres régionaux fixés dans le système pour déterminer le fuseau horaire. Elle tient compte de l'ajustement pour l'heure d'été. gmtime en revanche se base sur le fuseau horaire UTC et ne tient pas compte de l'heure d'été.
Un exemple d'utilisation est le suivant :
time_t t;
time(&t);
struct tm *tm = localtime(&t);
printf("Heure locale : %d:%d:%d\n", tm->tm_hour, tm->tm_min, tm->tm_sec);
asctime et ctime¤
Les fonctions asctime et ctime permettent de convertir un temps en chaîne de caractères. Leur prototype est le suivant :
L'une prend en paramètre une structure tm et l'autre un temps. Elles retournent une chaîne de caractères représentant le temps. Par exemple :
time_t current_time = time(NULL);
struct tm *local_tm = localtime(¤t_time);
printf("Heure locale : %s", asctime(local_tm));
// Affiche par exemple "Sun Sep 16 01:03:52 1973\n" (locale en anglais)
// "Dimanche 16 Septembre 01:03:52 1973\n" (locale en français)
Pour afficher l'heure actuelle, on peut également utiliser ctime :
strftime¤
La fonction strftime permet de convertir un temps en chaîne de caractères en utilisant un format spécifique. Son prototype est le suivant :
Elle prend en paramètre un pointeur sur une chaîne de caractères, la taille de la chaîne, un format et une structure tm. Elle retourne le nombre de caractères écrits dans la chaîne.
| Format | Description | Exemple de sortie |
|---|---|---|
%A |
Nom complet du jour de la semaine | "Sunday" |
%a |
Nom abrégé du jour de la semaine | "Sun" |
%B |
Nom complet du mois | "January" |
%b |
Nom abrégé du mois | "Jan" |
%C |
Siècle (les deux premiers chiffres de l'année) | "20" pour 2024 |
%d |
Jour du mois (01-31) | "17" |
%D |
Date au format MM/DD/YY |
"09/17/24" |
%e |
Jour du mois (1-31, avec espace si un chiffre) | "17" ou " 7" |
%F |
Date au format YYYY-MM-DD |
"2024-09-17" |
%H |
Heure (00-23, format 24 heures) | "14" |
%I |
Heure (01-12, format 12 heures) | "02" |
%j |
Jour de l'année (001-366) | "260" |
%k |
Heure (0-23, avec espace si chiffre) | " 2" |
%l |
Heure (1-12, avec espace si chiffre, 12 heures) | " 2" |
%M |
Minutes (00-59) | "05" |
%m |
Mois (01-12) | "09" |
%n |
Saut de ligne | "\n" |
%p |
Indicateur AM ou PM | "PM" |
%P |
Indicateur am ou pm (minuscule) | "pm" |
%r |
Heure au format 12 heures (hh:mm:ss AM/PM) |
"02:05:45 PM" |
%R |
Heure au format 24 heures (hh:mm) |
"14:05" |
%S |
Secondes (00-60) | "45" |
%T |
Heure au format 24 heures (hh:mm:ss) |
"14:05:45" |
%u |
Numéro du jour de la semaine (1-7, lundi = 1) | "2" pour mardi |
%U |
Numéro de la semaine (00-53, dimanche) | "37" |
%W |
Numéro de la semaine (00-53, lundi) | "37" |
%V |
Numéro de la semaine ISO 8601 (01-53, lundi) | "38" |
%w |
Numéro du jour de la semaine (0-6, dimanche = 0) | "0" |
%x |
Représentation locale de la date | "09/17/24" |
%X |
Représentation locale de l'heure | "14:05:45" |
%y |
Année (00-99, deux derniers chiffres) | "24" |
%Y |
Année (tous les chiffres) | "2024" |
%z |
Décalage UTC (format +hhmm) | "+0200" (UTC+2) |
%Z |
Nom du fuseau horaire | "CEST" |
%% |
Symbole % |
"%" |
Queqlues notes sur les formats :
- Le
%Speut retourner 60 lorsqu'une seconde intercalaire est insérée. Une second intercalaire est une seconde ajoutée à la fin d'une minute pour compenser la rotation de la Terre. - La différence entre
%Uet%West que%Ucommence la semaine le dimanche alors que%Wcommence la semaine le lundi. Les américains utilisent%Ualors que les européens utilisent%V.
Voici un exemple d'utilisation :
#include <stdio.h>
#include <time.h>
int main() {
// Obtenir l'heure locale
time_t current_time = time(NULL);
struct tm *local_tm = localtime(¤t_time);
// Formater la date et l'heure
char buffer[100];
strftime(buffer, sizeof(buffer),
"Aujourd'hui, c'est %A, %d %B %Y, et il est %T.", local_tm);
printf("%s\n", buffer);
}
Il pourrait afficher :
<uchar.h>¤
Apparue avec la norme C11, cette bibliothèque contient des fonctions pour gérer les caractères Unicode. Elle contient des fonctions pour convertir des caractères en minuscules ou majuscules, pour tester si un caractère est un chiffre, une lettre, etc.
Un caractère multi-octets (multibyte) est un caractère qui nécessite plus d'un octet pour être stocké. Nous avons que la norme Unicode définit un jeu de caractères universel qui peut être représenté en binaire avec des caractères de 8-bit (UTF-8). Cela permet de stocker théoriquement jusqu'à 4 294 967 295 caractères différents.
Le C étant un langage ancien, il a été conçu à une époque où seul la table ASCII existait. Néanmoins, certaines langues comme le chinois nécessitaient plus de 256 caractères. Pour cela, le C a introduit le concept de caractères larges (wide characters) qui étaient initialement stockés sur 16-bits (short). Néanmoins, avec l'arrivée de l'Unicode, il n'est pas rare de trouver des caractères qui nécessitent 32-bits. Or, les wide-chars historiques du C ne sont que sur 16-bits (sous Windows) et 32-bits (sous Unix). Pour palier à ce problème de portabilité, la norme C11 a introduit la bibliothèque <uchar.h> qui permet de gérer les caractères Unicode convenablement.
La bibliothèque définit deux types supplémentaires :
Contrairement à UTF-8 qui est un encodage variable : de 1 à 4 bytes, l'UTF-16 et l'UTF-32 sont des encodages fixes (à moins d'utiliser des surrogatges). Comme la plupart des systèmes utilisent massivement l'UTF-8, la bibliothèque offre des fonctions de conversion entre les différents encodages.
| Fonction | Description |
|---|---|
c16rtomb |
Convertit un caractère 16-bit en UTF-8 |
c32rtomb |
Convertit un caractère 32-bit en UTF-8 |
mbrtoc16 |
Convertit un caractère UTF-8 en 16-bit |
mbrtoc32 |
Convertit un caractère UTF-8 en 32-bit |
c16rtowc |
Convertit un caractère 16-bit en wide char |
c32rtowc |
Convertit un caractère 32-bit en wide char |
wctoc16 |
Convertit un wide char en 16-bit |
wctoc32 |
Convertit un wide char en 32-bit |
Le standard C nomme mb (multibyte) pour se référer à UTF-8.
L'inconvénient majeur d'UTF-8 c'est qu'il est impossible d'éditer un caractère à un endroit précis sans devoir possiblement décaler tous les caractères suivants. Remplacer un e (stocké sur 1 byte) par un émoji (stocké sur 4 bytes), nécessite de décaler tout le texte de 3 bytes. Suivant la taille de la chaîne cela peut être fastidieux. C'est pourquoi l'UTF-32 est souvent utilisé pour les traitements internes. On perd de la place mémoire car un texte en UTF-32 est jusqu'à 4 fois plus gros qu'en UTF-8, mais on gagne en temps de traitement car aucun déclage n'est nécessaire. En outre, le processeur étant plus à l'aise avec les données alignées sur 32-bits, les traitements sont plus rapides.
Prenons l'exemple d'un algorithme qui inverse une chaîne de caractères UTF-8 et affiche le résultat. Sans cette bibliothèque, il n'est pas trivial de le faire car les caractères unicode peuvent être stockés sur plusieurs bytes. Ici on commence par convertir la chaîne UTF-8 en UTF-32 pour avoir une chaîne simple à traiter, on inverse ensuite la chaîne UTF-32, puis on la reconvertit en UTF-8 pour l'affichage. Une implémentation est donnée dans la section algorithmes.
<wchar.h>¤
Cette bibliothèque supplémente d'autres bibliothèques pour gérer les caractères larges (wide characters). Ci-dessous la table d'équilvalence :
| Fonction | Description | Équivalent |
|---|---|---|
wcstol |
Convertit une chaîne en long | strtol |
wcstoul |
Convertit une chaîne en unsigned long | strtoul |
wcstoll |
Convertit une chaîne en long long | strtoll |
wcstoull |
Convertit une chaîne en unsigned long long | strtoull |
wcstof |
Convertit une chaîne en float | strtof |
wcstod |
Convertit une chaîne en double | strtod |
wcstold |
Convertit une chaîne en long double | strtold |
wcscpy |
Copie une chaîne | strcpy |
wcsncpy |
Copie une chaîne | strncpy |
wcscat |
Concatène deux chaînes | strcat |
wcsncat |
Concatène deux chaînes | strncat |
wcscmp |
Compare deux chaînes | strcmp |
wcsncmp |
Compare deux chaînes | strncmp |
wcschr |
Recherche un caractère dans une chaîne | strchr |
wcsrchr |
Recherche un caractère dans une chaîne | strrchr |
wcscoll |
Compare deux chaînes | strcoll |
wcslen |
Calcule la longueur d'une chaîne | strlen |
wcsxfrm |
Transforme une chaîne | strxfrm |
wmemcmp |
Compare deux régions mémoire | memcmp |
wmemchr |
Recherche un caractère dans une région mémoire | memchr |
wmemcpy |
Copie une région mémoire | memcpy |
wmemmove |
Copie une région mémoire | memmove |
wmemset |
Remplit une région mémoire | memset |
<(w)ctype.h>¤
La bibliothèque <ctype.h> contient des fonctions pour tester et convertir des caractères. Les fonctions sont définies pour les caractères ASCII uniquement, elle ne s'applique pas aux caractères Unicode, ni aux caractères étendus (au-delà de 127). La bibliothèque <wctype.h> est similaire mais pour les caractères larges (wide characters).
| ctype | wctype | Description |
|---|---|---|
isalnum |
iswalnum |
une lettre ou un chiffre |
isalpha |
iswalpha |
une lettre |
iscntrl |
iswcntrl |
un caractère de commande |
isdigit |
iswdigit |
un chiffre décimal |
isgraph |
iswgraph |
un caractère imprimable ou le blanc |
islower |
iswlower |
une lettre minuscule |
isprint |
iswprint |
un caractère imprimable (pas le blanc) |
ispunct |
iswpunct |
un caractère imprimable pas isalnum |
isspace |
iswspace |
un caractère d'espace blanc |
isupper |
iswupper |
une lettre majuscule |
isxdigit |
iswxdigit |
un chiffre hexadécimal |
En plus de ces fonctions de test, il existe des fonctions de conversion de casse définies dans <wctype.h> :
| Fonction | Description |
|---|---|
towlower |
Convertit une lettre en minuscule |
towupper |
Convertit une lettre en majuscule |
towctrans |
Convertit un caractère selon la locale LC_CTYPE |
Exercices de révision¤
Exercice 1 : Arc-cosinus
La fonction Arc-Cosinus acos est-elle définie par le standard et dans quel fichier d'en-tête est-elle déclarée ? Un fichier d'en-tête se termine avec l'extension .h.
Solution
En cherchant man acos header dans Google, on trouve que la fonction acos est définie dans le header <math.h>.
Une autre solution est d'utiliser sous Linux la commande apropos:
$ apropos acos
acos (3) - arc cosine function
acosf (3) - arc cosine function
acosh (3) - inverse hyperbolic cosine function
acoshf (3) - inverse hyperbolic cosine function
acoshl (3) - inverse hyperbolic cosine function
acosl (3) - arc cosine function
cacos (3) - complex arc cosine
cacosf (3) - complex arc cosine
cacosh (3) - complex arc hyperbolic cosine
cacoshf (3) - complex arc hyperbolic cosine
cacoshl (3) - complex arc hyperbolic cosine
cacosl (3) - complex arc cosine
Le premier résultat permet ensuite de voir :
$ man acos | head -10
ACOS(3) Linux Programmer's Manual ACOS(3)
NAME
acos, acosf, acosl - arc cosine function
SYNOPSIS
#include <math.h>
double acos(double x);
float acosf(float x);
La réponse est donc <math.h>.
Sous Windows avec Visual Studio, il suffit d'écrire acos dans un fichier source et d'appuyer sur F1. L'IDE redirige l'utilisateur sur l'aide Microsoft acos-acosf-acosl qui indique que le header source est <math.h>.
Exercice 2 : Date
Lors du formatage d'une date, on y peut y lire %w, par quoi sera remplacé ce token ?