I. Historique▲
4 novembre 2006
Clarification de la signification des pragma.
Précision sur l'environnement en virgule flottante.
Ajout des nouveaux fichiers d'en-tête.
Ajout de références.
II. Introduction▲
À l'heure actuelle, deux normes cohabitent : il y a celle publiée en 1989 par l'ANSI(1), reprise en 1990 par l'ISO(2) connue respectivement sous le nom de C89 et C90 (ce sont les mêmes) et une seconde norme qui regroupe les différents drafts apparus depuis la dernière norme, publiée en 1999 par ISO connue sous le nom de C99 (et C2k pour la version ANSI). Cette dernière a pour but de minimiser les incompatibilités entre le C et le C++ (sans pour autant en faire un seul et même langage). Voici donc la liste des nouveautés et des modifications apportées par la norme C99 par rapport au C90.
III. Préprocesseur▲
III-A. La macro __STDC_VERSION__▲
Cette macro permet de connaître la norme utilisée :
- en C89 elle n'est pas définie ;
- en C94 elle vaut 199409L ;
- et maintenant, en C99, 199901L.
III-B. Macros à nombre variable d'arguments▲
Cette fonctionnalité permet de définir des macros dont le nombre des arguments peut être différent d'un appel à l'autre. La notation utilisée est la même que pour les fonctions ('…') et la liste est récupérée grâce à la macro __VA_ARGS__ :
#define print_error(s, ...) fprintf (stderr, (s), __VA_ARGS__)
III-C. Les commentaires monoligne▲
Les commentaires « à la C++ », c'est-à-dire commençant par // et se terminant à la fin de la ligne viennent s'ajouter aux commentaires classiques (/* */).
III-D. La directive #line▲
Cette directive du préprocesseur permet de modifier le numéro de la ligne courante (et optionnellement le nom du fichier). La valeur utilisée était limitée à 215-1 en C90 et passe maintenant à 231-1.
III-E. La directive #pragma▲
La directive #pragma permet de contrôler le comportement du compilateur. La norme C99 définit trois pragma standards :
- FP_CONTRACT : optimisation des expressions flottant ;
- FENV_ACCESS : notre programme accède aux paramètres du FPU (Floating Point Unit ou unité de calcul flottant), ceci se fait à l'aide des fonctions déclarées dans fenv.h. Le compilateur doit donc respecter la norme pour les opérations sur les nombres flottant ;
- CX_LIMITED_RANGE : dans le cas d'opérations sûres avec les nombres complexes (c'est-à-dire sans risque de débordement de capacités), vous pouvez le signaler afin d'accélérer le traitement en activant ce pragma.
Chacune de ces fonctionnalités peut être activée (ON), désactivée (OFF) ou remise à son état par défaut (DEFAULT). Par exemple, voici comment activer les optimisations des expressions flottant :
#pragma STDC FP_CONTRACT ON
Chaque compilateur propose ces propres directives pragma. Si le compilateur rencontre une directive inconnue, la norme prévoit que celle-ci soit ignorée, cependant il est déconseillé d'utiliser des directives non standard, car selon les compilateurs leur rôle peut être différent.
À la place de la directive #pragma, il est possible d'utiliser l'opérateur unaire _Pragma.
IV. Syntaxe▲
IV-A. Les nouveaux mots réservés▲
Le C99 ajoute cinq mots réservés au langage :
- restrict
- inline
- _Complex
- _Imaginary
- _Bool
IV-B. Séquences d'échappement à deux caractères▲
Les séquences d'échappement à deux caractères permettent de remplacer certains caractères spéciaux qui peuvent être absents du clavier (comme les séquences d'échappement à trois caractères). Avant tout traitement, les lexèmes <:, :>, <%, %>, %: et %:%: sont respectivement remplacés par [, ], {, }, # et ##.
IV-C. Mélange déclaration/code▲
En C90, les déclarations doivent être faites au début d'un bloc, alors qu'en C99 il est tout à fait possible d'écrire :
int
main (
void
)
{
printf (
"
Debut du code
\n
"
);
int
i =
10
;
printf (
"
i vaut %d
\n
"
, i);
return
0
;
}
Cependant ce genre de mélange n'est pas recommandé afin de préserver la lisibilité du code.
Cette fonctionnalité entraîne la possibilité de créer une variable de boucle temporaire pour l'instruction for :
for
(
int
i =
0
; i <
10
; i++
)
{
/* variable i utilisable */
}
/* variable i inexistante */
Est équivalent à :
{
int
i;
for
(
i =
0
; i <
10
; i++
)
{
/* variable i utilisable */
}
}
/* variable i inexistante */
Par conséquent, la variable i n'existera plus à la sortie de la boucle.
IV-D. Initialisation des membres d'une structure▲
Il est possible d'initialiser les membres d'une structure, lors de sa déclaration, en utilisant leurs noms :
struct
T
{
int
a;
int
b;
}
;
struct
T s1 =
{
.a =
0
, .b =
1
}
;
/* ou dans le cas d'un tableau de structure : */
struct
T s2[] =
{
[0
].a =
0
, [0
].b =
0
, [1
].a =
1
, [1
].b =
1
}
;
De plus, pour les structures de type auto, il est possible d'initialiser les membres avec des valeurs non connues au moment de la compilation :
int
i =
0
;
struct
T s1 =
{
.a =
i, .b =
i+
2
}
;
V. Sémantique▲
V-A. Les fonctions en ligne▲
Le mot-clé inline est un nouvel attribut pour les fonctions qui ont pour but de remplacer le système de macro défini grâce à la commande du préprocesseur #define. Pour illustrer l'avantage de cette méthode par rapport aux macros, voici l'exemple classique du calcul du carré d'un nombre :
#define carre(a) ((a)*(a))
Si par malheur vous oubliez les parenthèses autour de la variable a, la macro peut produire un résultat inattendu, il faut aussi faire attention aux effets de bord puisque la variable a est évaluée deux fois. Maintenant avec le mécanisme de fonction en ligne :
inline
int
carre (
int
a)
{
return
a*
a;
}
int
main (
void
)
{
int
b =
carre (
5
);
printf (
"
Le carre de 5 est %d
\n
"
, b);
return
0
;
}
Ceci supprime les dangers liés à l'utilisation de l'instruction #define. Les fonctions en ligne doivent être définies aux mêmes endroits que les macros classiques, ce qui peut alourdir les fichiers d'en-tête et surtout recopie la fonction dans plusieurs fichiers sources : pour éviter de se retrouver avec plusieurs fonctions globales portant le même nom, il est conseillé de toujours utiliser le mot-clé inline conjointement avec le mot-clé static (même dans un fichier d'en-tête).
L'attribut inline est une information d'optimisation pour le compilateur, si la mise en ligne n'est pas possible, le compilateur peut ignorer cet attribut.
V-B. long long▲
Le nouveau type d'entier long long (signé ou non) est représenté sur au moins 64 bits. Il s'accompagne des notations LL (ou ll) et ULL (ou ull).
V-C. __func__▲
La variable __func__(3) contient le nom de la fonction courante. Elle est déclarée ainsi :
static
const
char
__func__[];
V-D. Type de retour implicite▲
En C90, en l'absence de type de retour, le compilateur considère que cette fonction retourne un int, en C99 ce n'est plus possible : le type de retour doit être spécifié.
V-E. Les pointeurs restreints▲
Le mot-clé restrict permet de spécifier qu'un pointeur est le seul à pointer sur une zone mémoire. Cela permet au compilateur d'opérer certaines optimisations. Par exemple, la fonction memcpy qui est utilisée pour copier deux zones de mémoires qui ne se chevauchent pas peut avoir comme prototype, en C99 :
void
*
memcpy (
void
*
restrict dst, const
void
*
restrict src, size_t size);
Le mot-clé se place entre le caractère * et le nom du pointeur.
Comme pour les fonctions en ligne, cette directive peut être ignorée par le compilateur.
V-F. Les tableaux de taille variable▲
Voilà une fonctionnalité qui va réjouir les réfractaires à l'utilisation des fonctions d'allocation dynamique. En effet, en C90 pour déclarer un tableau, sa taille doit être connue à la compilation. Le C99 propose un mécanisme nommé VLA (Variable-Length Arrays), qui permet de créer des tableaux dont la taille n'est connue qu'à l'exécution. Voici un exemple :
void
foo (
int
n)
{
int
tab[n];
...
}
Comme toutes variables automatiques, elles sont détruites à la fin du bloc dans lequel elles sont déclarées (plus de fuites mémoires avec ce système), en contrepartie l'allocation dynamique reste indispensable pour retourner l'adresse d'une zone mémoire.
On peut aussi déclarer un tableau de taille variable dans le prototype d'une fonction :
void
foo (
int
n, int
a[n]) {
... }
Il faut que la variable n soit définie avant, ce qui interdit d'écrire ceci :
void
foo (
int
a[n], int
n) {
... }
La déclaration d'une telle fonction peut s'écrire :
/* Declaration complete */
void
foo (
int
n, int
a[n]);
/* Declaration partielle */
void
foo (
int
n, int
a[*
]);
Notez l'utilisation du caractère '*' pour signaler un tableau de taille variable.
Malgré le warning de gcc, le code fonctionne comme prévu.
Ce mécanisme très puissant a tout de même certaines limitations.
-
Les tableaux de tailles variables sont limités au fichier, il n'est pas possible d'utiliser le mot-clé extern :
Sélectionnezextern
int
n;int
a[n];/* n est declaree extern : utilisation interdite */
extern
int
b[n];/* utilisation interdite */
-
Il n'est pas possible de créer de tel tableau static. Par contre il est possible de déclarer static un pointeur sur VLA :
Sélectionnezint
b[10
];int
foo(
int
n){
static
int
a[n];/* Interdit */
static
(*
p_b)[n]=
&
b;/* Authorise si n == 10 */
}
Déclarer un pointeur dur un VLA d'une taille différente de celle autorisée ne stoppe pas l'exécution du programme (on pourrait s'attendre à la levée d'une exception ou quelque chose approchant), mais bien sûr ce genre de manipulation est très risquée et est à classer dans les comportements indéfinis.
-
Il n'est pas possible de déclarer un tableau de taille variable dans une structure ou une union.
- Il n'est pas possible d'effectuer un saut dans un bloc qui contient une déclaration de ce type de tableau, par contre sortir de ce bloc est autorisé :
goto
start; /* Interdit */
{
int
n =
10
;
int
a[n];
start :
/* ... */
goto
end; /* Authorise */
}
end :
/* ... */
V-G. Type de tableau incomplet▲
Lors de la déclaration d'une structure, il est possible de ne pas spécifier la taille d'un tableau, s'il s'agit du dernier membre de la structure :
struct
incomplet
{
int
n;
int
tab[];
}
;
Cela permet de faire de belles choses comme ceci :
#include <stdio.h>
#include <stdlib.h>
struct
incomplet
{
int
n;
int
tab[];
}
;
int
main (
void
)
{
struct
incomplet *
a =
NULL
;
a =
malloc (
sizeof
(*
a) +
3
*
sizeof
(
int
));
if
(
a)
{
a->
n =
3
;
for
(
int
i =
0
; i <
a->
n; i++
)
{
a->
tab[i] =
i;
}
printf (
"
a->tab = {
"
);
for
(
int
i =
0
; i <
a->
n; i++
)
{
printf (
"
%d,
"
, a->
tab[i]);
}
printf (
"
};
\n
"
);
free (
a), a =
NULL
;
}
struct
incomplet *
b =
NULL
;
b =
malloc (
sizeof
(*
b) +
5
*
sizeof
(
int
));
if
(
b)
{
b->
n =
5
;
for
(
int
i =
0
; i <
b->
n; i++
)
{
b->
tab[i] =
i;
}
printf (
"
b->tab = {
"
);
for
(
int
i =
0
; i <
b->
n; i++
)
{
printf (
"
%d,
"
, b->
tab[i]);
}
printf (
"
};
\n
"
);
free (
b), b =
NULL
;
}
return
0
;
}
gege2061@debian:~/c99$
gcc -Wall -std
=
c99 main.c &&
./a.out
a->
tab =
{ 0
, 1
, 2
, };
b->
tab =
{ 0
, 1
, 2
, 3
, 4
, };
Cela évite d'utiliser un pointeur pour tab et d'avoir une allocation supplémentaire à faire. Hélas il n'est pas possible de faire :
struct
incomplet a =
{
3
, 0
, 1
, 2
}
;
struct
incomplet b =
{
5
, 0
, 1
, 2
, 3
, 4
}
;
VI. La bibliothèque standard▲
VI-A. Les nouveaux fichiers d'en-tête▲
Un enrichissement de la bibliothèque standard, s'accompagne logiquement de nouveaux fichiers d'en-tête :
- complex.h ;
- fenv.h ;
- inttypes.h ;
- iso646.h ;
- stdbool.h ;
- stdint.h ;
- tgmath.h ;
- wchar.h ;
- wctype.h.
VI-B. Les types entiers étendus▲
En C, la taille des types de variables n'est pas définie par la norme, seule une taille minimale est garantie. Le C99, dans le fichier d'en-tête stdint.h propose des types dont la taille est connue :
Types |
Description |
---|---|
intN_t |
Entier signé de N (8, 16, 32 ou 64(4)) bits |
uintN_t |
Entier non signé de N (8, 16, 32 ou 64) bits |
intmax_t |
Entier signé de taille maximale |
uintmax_t |
Entier non signé de taille maximale |
int_leastN_t |
Entier signé d'au moins N (8, 16, 32 ou 64)bits |
uint_leastN_t |
Entier non signé d'au moins N (8, 16, 32 ou 64) bits |
int_fastN_t |
Entier rapide (5) signé d'au moins N (8, 16, 32 ou 64) bits |
uint_fastN_t |
Entier rapide non signé d'au moins N (8, 16, 32 ou 64) bits |
Ces types sont notés comme optionnels dans la norme, par conséquent leur portabilité est réduite.
VI-C. Les caractères étendus▲
En C90 il n'était pas possible d'utiliser de caractères spécifiques d'une langue (par exemple, les caractères accentués pour le français), et ce, même dans les commentaires. Pour pallier ce problème, la norme C99 propose un nouveau type de caractères étendus nommés wchar_t (pour wide char) défini dans wchar.h. Pour convertir un caractère étendu en entier, le type int peut ne pas être suffisant, à la place, il faut utiliser le type wint_t.
Toutes les fonctions de manipulation des chaînes de caractères (de la forme str*) sont reprises sous la forme wcs* (par exemple wcslen, wcscmp ou encore wcscpy) ainsi que les fonctions wprintf et wscanf pour l'affichage et la saisie formatée de chaînes de caractères étendues.
VI-D. va_copy▲
Le fichier d'en-tête stdarg.h contient une nouvelle fonction va_copy qui permet de copier un objet de type va_list.
VI-E. Le type booléen▲
Un booléen est un nouveau type de variable qui peut prendre uniquement la valeur vrai ou faux. La définition du type bool est faite dans le fichier d'en-tête stdbool.h et contient les définitions suivantes :
- bool : le type booléen ;
- true : la valeur vrai ;
- false : la valeur faux ;
- __bool_true_false_are_defined : si cette macro est définie, les valeurs bool, true et false le sont aussi.
La définition du type bool a été placée dans un fichier d'en-tête pour garantir la compatibilité avec les anciens codes (il n'est pas rare de voir un code contenant déjà une définition de bool). Si, pour une raison quelconque, vous ne souhaitez pas inclure stdbool.h, le langage contient le type _Bool qui peut prendre les valeurs 0 (faux) et 1 (vrai).
VI-F. Les nombres complexes▲
Les nombres complexes font leurs entrées dans le langage C grâce au type complex défini dans complex.h. Comme il existe trois représentations pour les nombres réels, il existe trois types de nombres complexes :
float
complex
double
complex
long
double
complex
Pour construire un nombre complexe, il suffit d'additionner sa partie réelle et sa partie imaginaire :
double
partie_reelle =
2
.0
;
double
partie_imaginaire =
3
.0
;
double
complex z =
partie_reelle +
partie_imaginaire *
I;
La macro I permet de construire la partie imaginaire.
Pour retrouver les parties réelles et imaginaires d'un complexe, on peut utiliser, respectivement les fonctions creal et cimag définies pour les trois types de complexes :
float
crealf (
float
complex z);
double
creal (
double
complex z);
long
double
creall (
long
double
complex z);
float
cimagf (
float
complex z);
double
cimag (
double
complex z);
long
double
cimagl (
long
double
complex z);
La bibliothèque standard du C99 propose aussi des fonctions pour calculer le module (cabs*), l'argument (carg*) et la représentation selon la projection sur la sphère de Riemann (cproj*) d'un nombre complexe.
VI-G. Les nombres à virgule flottante▲
C'est sûrement dans ce domaine que les nouveautés sont les plus nombreuses, puisque le C99 introduit la notion d'environnement virgule flottante qui permet une gestion améliorée des erreurs du système de calcul en nombres flottant.
VI-G-1. Dépassement de capacité de la valeur de retour▲
En plus de la valeur HUGE_VAL, retournée lorsque le type de retour n'a pas la capacité nécessaire pour contenir le résultat, la norme C99 ajoute les valeurs HUGE_VALF (de type float) et HUGE_VALL (de type long double).
VI-G-2. Le contrôle des arrondis▲
Le type d'arrondi effectué sur les nombres à virgule flottante est représenté par les constantes définies dans fenv.h :
- FE_TONEARREST : au plus proche (valeur par défaut) ;
- FE_UPWARD : vers le haut ;
- FE_DOWNWARD : vers le bas ;
- FE_TOWARDZERO : vers zéro (vers le bas pour les nombres positifs, vers le haut pour les nombres négatifs).
Il est possible de connaître le mode utilisé grâce à la fonction :
int
fegetround (
void
);
Et de le modifier grâce à :
int
fesetround (
int
mode);
En cas de succès, cette fonction retourne 0.
VI-G-3. Les exceptions▲
En cas d'erreur lors d'une opération sur les nombres en virgule flottante, une exception est levée, elle peut être traitée par le système (par exemple en envoyant un signal SIGFPE pour les systèmes POSIX). Cependant, un signal n'est pas toujours envoyé, dans ce cas c'est au programmeur de les tester.
Il existe cinq exceptions, représentées par des macros :
- FE_DIVBYZERO : division par zéro ;
- FE_OVERFLOW : valeur trop grande ;
- FE_UNDERFLOW : valeur trop petite ;
- FE_INEXACT : résultat inexact ;
- FE_INVALID : opération indéfinie ;
- FE_ALL_EXCEPT : représente l'ensemble des exceptions.
Vous pouvez tester la levée d'une exception avec la fonction :
int
fetestexcept (
int
exceptions);
Où exceptions peut être une combinaison des macros précédentes (grâce à un ou binaire |) et la fonction retourne les exceptions réellement levées parmi celles passées en argument (ou zéro si aucune n'est levée).
Vous pouvez lever une ou plusieurs exceptions grâce à :
void
feraiseexcept (
int
exceptions);
Et inversement, baisser les drapeaux d'exceptions avec :
void
feclearexcept (
int
exceptions);
Et enfin, vous pouvez sauvegarder et restaurer l'état d'un ou plusieurs drapeaux :
void
fegetexceptflag (
fexcept_t *
svg, int
execptions);
void
fesetexceptflag (
const
fexcept_t *
svg, int
execptions);
VI-G-4. Manipulation de l'environnement▲
Vous pouvez enregistrer et sauvegarder la configuration de l'environnement avec la fonction :
void
fegetenv (
fenv_t *
ecf);
void
fesetenv (
const
fenv_t *
ecf);
La fonction fesetenv peut prendre en argument la valeur FE_DFL_ENV qui permet de restaurer l'état initial de l'environnement.
void
feupdateenv (
const
fenv_t *
ecf);
Cette fonction restaure l'environnement en préservant les drapeaux d'exception.
void
feholdexcept (
fenv_t *
ecf);
feholdexcept désactive les exceptions en sauvegardant les paramètres actuels.
VII. Conclusion▲
Voilà, notre petit tour des nouveautés du C99 est terminé ! Je n'ai fait que vous présenter les nouveautés les plus importantes et sans approfondir, volontairement, certains points. Par exemple les nombres flottant ou les caractères étendus nécessiteraient un article entier, si le cœur vous en dit : https://club.developpez.com/redaction/ ;).
Cependant à l'heure actuelle, il n'est pas recommandé d'utiliser les spécificités de cette nouvelle norme puisqu'elle n'est pas encore intégralement implémentée dans tous les compilateurs (par exemple pour gcc : Status of C99 features in GCC). Il existe tout de même certains compilateurs conformes à la norme C99(6) :
- Dinkum Unabridged Library (Dinkumware, Ltd) ;
- EDG C/C++ Compiler (Edison Design Group) ;
- Visual Age Compiler (IBM) ;
- LMPCC C99 Compiler (Lund Multiprocessor Compiler Company, AB) ;
- Sun Studio 9 ( Sun Microsystems).
VIII. Références▲
Si vous souhaitez en savoir plus sur ce sujet, vous pouvez télécharger gratuitement la dernière version de la norme au format pdf : n1124.pdf.
Je vous conseille aussi la lecture de deux très bons livres qui abordent le sujet : méthodologie de la programmation en C et C précis et concis.
IX. Remerciements▲
Merci à fearyourself, neguib et à Yogui pour leur relecture attentive de cet article.