Dans le chapitre précédent, nous avons vu que la représentation des chaînes de caractères reposait le plus souvent sur l’encodage UTF-8, ce qui avait pour conséquence de potentiellement supprimer la correspondance entre un char
et un caractère. Dans ce chapitre, nous allons voir le concept de caractère large (et, conséquemment, de chaîne de caractères larges) qui a été introduit pour résoudre ce problème.
- Introduction
- Traduction en chaîne de caractères larges et vice versa
- L'en-tête <wchar.h>
- <wchar.h> : les fonctions de lecture/écriture
- <wchar.h> : les fonctions de manipulation des chaînes de caractères
- L'en-tête <wctype.h>
Introduction
Si la non correspondance entre un char
et un caractère n’est pas forcément gênante (elle n’empêche pas de lire ou d’écrire du texte par exemple), elle peut l’être dans d’autres cas, par exemple lorsque l’on souhaite compter le nombre de caractères composant une chaîne de caractères.
Pour ces derniers cas, il faudrait revenir à une correspondance une à une. Or, vous le savez, pour cela il faudrait un autre type que le type char
, ce dernier n’ayant pas une taille suffisante (d’où le recours aux encodages comme l’UTF-8).
Dans cette optique, le type wchar_t
(pour wide character, littéralement « caractère large »), défini dans l’en-tête <stddef.h>
, a été introduit. Celui-ci n’est rien d’autre qu’un type entier (signé ou non signé) capable de représenter la valeur la plus élevée d’une table de correspondance donnée. Ainsi, il devient par exemple possible de stocker toutes les valeurs de la table Unicode sur un système utilisant celle-ci.
Toutefois, disposer d’un type dédié n’est pas suffisant. En effet, il nous faut également « décoder » nos chaînes de caractères afin de récupérer les valeurs correspondantes à chaque caractère (qui, pour rappel, sont pour l’instant potentiellement réparties sur plusieurs char
). Cependant, pour effectuer ce décodage, il est nécessaire de connaître l’encodage utilisé pour représenter les chaînes de caractères. Or, vous le savez, cet encodage est susceptible de varier suivant qu’il s’agit d’une chaîne littérale, d’une entrée récupérée depuis le terminal ou de données provenant d’un fichier.
Dès lors, assez logiquement, la méthode pour convertir une chaîne de caractères en une chaîne de caractères larges varie suivant le même critère.
Traduction en chaîne de caractères larges et vice versa
Les chaînes de caractères littérales
Le cas des chaînes de caractères littérales est le plus simple : étant donné que la table et l’encodage sont déterminés par le compilateur, c’est lui qui se charge également de leur conversion en chaînes de caractères larges.
Pour demander au compilateur de convertir une chaîne de caractères littérale en une chaîne de caractères larges littérale, il suffit de précéder la définition d’une chaîne de caractères littérale de la lettre L
.
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
wchar_t ws[] = L"é";
printf("%u\n", (unsigned)ws[0]);
return 0;
}
233
Comme vous le voyez, nous obtenons 233, qui est bien la valeur du caractère é
dans la table Unicode. Techniquement, le compilateur a lu les deux char
composant le caractère é
et en a recalculé la valeur : 233, qu’il a ensuite assigné au premier élément du tableau ws
.
Notez qu’il est également possible de faire de même pour un caractère littéral seul, en le précédent également de la lettre L
. Le code suivant produit à nouveau le même résultat.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("%u\n", (unsigned)L'é');
return 0;
}
233
Les entrées récupérées depuis le terminal
Dans le cas des chaînes de caractères lues depuis le terminal, la conversion doit être effectuée par nos soins. Pour ce faire, la bibliothèque standard nous fournis deux fonctions déclarées dans l’en-tête <stdlib.h>
: mbstowcs()
et wcstombs()
.
La fonction mbstowcs
size_t mbstowcs(wchar_t *destination, char *source, size_t max);
La fonction mbstowcs()
traduit la chaîne de caractères source
en une chaîne caractères larges d’au maximum max
caractères qui sera stockée dans destination
. Elle retourne le nombre de caractères larges écrits, sans compter le caractère nul final. En cas d’erreur, la fonction renvoie (size_t)-1
(soit la valeur maximale du type size_t
).
Le code ci-dessous récupère une ligne depuis le terminal, la converti en une chaîne de caractères larges et affiche ensuite la valeur de chaque caractère la composant.
#include <locale.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char ligne[255];
if (fgets(ligne, sizeof ligne, stdin) == NULL)
{
perror("fgets");
return EXIT_FAILURE;
}
if (setlocale(LC_CTYPE, "") == NULL)
{
perror("setlocale");
return EXIT_FAILURE;
}
wchar_t ws[sizeof ligne];
if (mbstowcs(ws, ligne, sizeof ligne) == (size_t)-1)
{
perror("mbstowcs");
return EXIT_FAILURE;
}
for (unsigned i = 0; ws[i] != L'\n' && ws[i] != L'\0'; ++i)
printf("%d ", ws[i]);
printf("\n");
return 0;
}
Élégant
201 108 233 103 97 110 116
Comme vous le voyez, nous obtenons 201 et 233 pour « É » et « é », qui correspondent bien aux valeurs que leur attribue la table Unicode.
La catégorie LC_CTYPE
Vous l’aurez sûrement remarqué : la fonction setlocale()
est employée dans l’exemple précédent pour modifier la localisation de la catégorie LC_CTYPE
. Cette catégorie affecte le comportement des fonctions de traduction entre chaînes de caractères et chaînes de caractères larges.
En fait, l’appel à setlocale()
permet aux fonctions de traductions de connaître la table et l’encodage utilisés par notre système et ainsi de traduire correctement les entrées récupérées depuis le terminal.
Avec la localisation par défaut (C
), les fonctions de traductions se contentent d’affecter la valeur de chaque char
à un wchar_t
ou inversement.
La fonction wcstombs
size_t wcstombs(char *destination, wchar_t *source, size_t max);
La fonction wcstombs()
effectue l’opération inverse de la fonction mbstowcs()
: elle traduit la chaîne de caractères larges source
en une chaîne de caractères d’au maximum max
caractères qui sera stockée dans destination
.
La fonction retourne le nombre de multiplets écrits ou (size_t)-1
en cas d’erreur.
#include <locale.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char ligne[255];
if (fgets(ligne, sizeof ligne, stdin) == NULL)
{
perror("fgets");
return EXIT_FAILURE;
}
if (setlocale(LC_CTYPE, "") == NULL)
{
perror("setlocale");
return EXIT_FAILURE;
}
wchar_t ws[sizeof ligne];
if (mbstowcs(ws, ligne, sizeof ligne) == (size_t)-1)
{
perror("mbstowcs");
return EXIT_FAILURE;
}
size_t n = wcstombs(ligne, ws, sizeof ligne);
if (n == (size_t)-1)
{
perror("wcstombs");
return EXIT_FAILURE;
}
printf("%zu multiplet(s) écrit(s) : %s\n", n, ligne);
return 0;
}
Élégant
10 multiplet(s) écrit(s) : Élégant
Les données provenant d’un fichier
Dans le cas des fichiers, malheureusement, il n’y a pas de solution pour déterminer la table de correspondance ou l’encodage utilisés lors de l’écriture de ses données. La seule solution est de s’assurer d’une manière ou d’une autre que ces derniers sont les mêmes que ceux employés par le système.
Ceci étant posé, nous nous permettons d’attirer votre attention sur un point : étant donné que la table et l’encodage utilisé pour représenter les chaînes de caractères littérales et les entrées du terminal sont susceptibles d’être différents, il est parfaitement possible d’obtenir des chaînes de caractères larges différentes.
Ainsi, si le code suivant donnera le plus souvent le résultat attendu étant donné que l’emploie de l’UTF-8 se généralise, rien ne vous le garanti de manière générale. En effet, si, par exemple, les chaînes littérales sont encodées en UTF-8 et les entrées du terminal en IBM 850, alors la fonction nombre_de_e()
ne retournera pas le bon résultat.
#include <locale.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
static void chomp(char *s)
{
while (*s != '\n' && *s != '\0')
++s;
*s = '\0';
}
static unsigned nombre_de_e(wchar_t *ws)
{
unsigned n = 0;
while (*ws != L'\n' && *ws != L'\0')
{
switch(*ws)
{
case L'é':
case L'ê':
case L'è':
case L'e':
case L'É':
case L'Ê':
case L'È':
case L'E':
++n;
break;
}
++ws;
}
return n;
}
int main(void)
{
char ligne[255];
if (fgets(ligne, sizeof ligne, stdin) == NULL)
{
perror("fgets");
return EXIT_FAILURE;
}
if (setlocale(LC_CTYPE, "") == NULL)
{
perror("setlocale");
return EXIT_FAILURE;
}
chomp(ligne);
wchar_t ws[sizeof ligne];
if (mbstowcs(ws, ligne, sizeof ligne) == (size_t)-1)
{
perror("mbstowcs");
return EXIT_FAILURE;
}
printf("Il y a %zu \"e\" dans la phrase : \"%s\"\n", nombre_de_e(ws), ligne);
return 0;
}
Élégamment trouvé !
Il y a 4 "e" dans la phrase : "Élégamment trouvé !"
L'en-tête <wchar.h>
Nous venons de le voir, il nous est possible de convertir une chaîne de caractères en chaîne de caractères larges et vice versa à l’aide des fonctions mbstowcs()
et wcstombs()
. Toutefois, c’est d’une part assez fastidieux et, d’autre part, nous ne disposons d’aucune fonction pour manipuler ces chaînes de caractères larges (pensez à strlen()
, strcmp()
ou strchr()
).
Heureusement pour nous, la bibliothèque standard nous fournis un nouvel en-tête : <wchar.h>
pour nous aider.
Ce dernier fournit à peu de chose près un équivalent gérant les caractères larges de chaque fonction de lecture/écriture et de manipulation des chaînes de caractères.
<wchar.h> : les fonctions de lecture/écriture
Le tableau ci-dessous liste les fonctions de lecture/écriture classique et leur équivalent gérant les caractères larges.
<stdio.h> |
<wchar.h> |
---|---|
printf | wprintf |
fprintf | fwprintf |
scanf | wscanf |
fscanf | fwscanf |
getc | getwc |
fgetc | fgetwc |
getchar | getwchar |
fgets | fgetws |
putc | putwc |
fputc | fputwc |
putchar | putwchar |
fputs | fputws |
ungetc | ungetwc |
Les fonctions de lecture/écriture « larges » s’utilisent de la même manière que les autres, la seule différence est qu’elles effectuent les conversions nécessaires pour nous. Ainsi, l’exemple suivant est identique à celui de la section précédente, si ce n’est que la fonction fgetws()
se charge d’effectuer la conversion en chaîne de caractères larges pour nous.
#include <locale.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
int main(void)
{
wchar_t ligne[255];
if (setlocale(LC_CTYPE, "") == NULL)
{
perror("setlocale");
return EXIT_FAILURE;
}
if (fgetws(ligne, sizeof ligne / sizeof ligne[0], stdin) == NULL)
{
perror("fgetws");
return EXIT_FAILURE;
}
for (unsigned i = 0; ligne[i] != L'\n' && ligne[i] != L'\0'; ++i)
printf("%d ", ligne[i]);
printf("\n");
return 0;
}
Élégant
201 108 233 103 97 110 116
N’oubliez pas de faire appel à la fonction setlocale()
avant toute utilisation des fonctions de lecture/écriture larges !
L’orientation des flux
Toutefois, les fonctions de lecture/écriture larges amènent une subtilité supplémentaire. En effet, lorsque nous vous avons présenté les flux et les fonctions de lecture/écriture, nous avons omis de vous parler d’une caractéristique des flux : leur orientation.
L’orientation d’un flux détermine si ce dernier manipule des caractères larges ou des caractères simples. Lors de sa création, un flux n’a pas d’orientation, c’est la première fonction de lecture/écriture utilisée sur ce dernier qui la déterminera. Si c’est une fonction de lecture/écriture classique, le flux sera dit « orienté multiplets », si c’est une fonction de lecture/écriture large, le flux sera dit « orienté caractères larges ».
Cette orientation a son importance et amène quatre restrictions supplémentaires quant à l’usage des flux.
-
Une fois l’orientation d’un flux établie, elle ne peut plus être changée ;
-
Un flux orienté multiplets ne peut pas être utilisé par des fonctions de lecture/écriture larges et, inversement, un flux orienté caractères larges ne peut pas être utilisé par des fonctions de lecture/écriture classiques ;
fgets(s, sizeof s, stdin);
/* Faux, car stdin est déjà un flux orienté multiplets. */
fgetws(ws, sizeof ligne / sizeof ligne[0], stdin);
-
Un flux binaire orienté caractères larges voit ses déplacements possibles via la fonction
fseek()
davantage limités. Ainsi, seul le repèreSEEK_SET
avec une valeur nulle ou précédemment retournée par la fonctionftell
et le répèreSEEK_CUR
avec une valeur nulle peuvent être utilisés ; -
Si la position au sein d’un flux orienté caractères larges est modifée et est suivie d’une opération d’écriture, alors le contenu qui suit les données écrites devient indéterminé, sauf si le déplacement a lieu à la fin du fichier (c’est assez logique, puisque dans ce cas il n’y a rien après).
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
int main(void)
{
wchar_t ligne[255];
FILE *fp = fopen("fichier.txt", "w+");
if (setlocale(LC_CTYPE, "") == NULL)
{
perror("setlocale");
return EXIT_FAILURE;
}
if (fputws(L"Une ligne\n", fp) < 0)
{
perror("fputws");
return EXIT_FAILURE;
}
if (fputws(L"Une autre ligne\n", fp) < 0)
{
perror("fputws");
return EXIT_FAILURE;
}
rewind(fp);
/*
* On réécrit par dessus « Une ligne ».
*/
if (fputws(L"Une ligne\n", fp) < 0)
{
perror("fputws");
return EXIT_FAILURE;
}
/*
* Obligatoire entre une opération d'écriture et une opération de lecture, rappelez-vous.
*/
if (fseek(fp, 0L, SEEK_CUR) < 0)
{
perror("fseek");
return EXIT_FAILURE;
}
/*
* Rien ne garantit que l'on va lire « Une autre ligne »...
*/
if (fgetws(ligne, sizeof ligne / sizeof ligne[0], fp) == NULL)
{
perror("fgetws");
return EXIT_FAILURE;
}
fclose(fp);
return 0;
}
Notez que dans cet exemple nous passons des chaînes de caractères larges littérales comme arguments à la fonction fputws()
. Si cela ne pose le plus souvent pas de problèmes, n’oubliez pas que la table et l’encodage des chaînes de caractères larges littérales sont susceptibles d’être différents de ceux employés par votre système.
La fonction fwide
int fwide(FILE *flux, int mode);
La fonction fwide()
permet de connaître l’orientation du flux flux
ou d’en attribuer une s’il n’en a pas encore. Dans le cas où mode
est un entier positif, la fonction essaye de fixer une orientation « caractères larges », si c’est un nombre négatif, une orientation « multiplets ». Enfin, si mode
est nul, alors la fonction détermine simplement l’orientation du flux.
Elle retourne un nombre positif si le flux est orienté caractères larges, un nombre négatif s’il est orienté multiplets et zéro s’il n’a pas encore d’orientation.
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
static void affiche_orientation(FILE *fp, char *nom)
{
int orientation = fwide(fp, 0);
fprintf(stderr, "Le flux %s ", nom);
if (orientation < 0)
fprintf(stderr, "est orienté multiplets\n");
else if (orientation > 0)
fprintf(stderr, "est orienté caractères larges\n");
else
fprintf(stderr, "n'a pas encore d'orientation\n");
}
int main(void)
{
if (setlocale(LC_CTYPE, "") == NULL)
{
perror("setlocale");
return EXIT_FAILURE;
}
affiche_orientation(stdout, "stdout");
printf("Une ligne\n");
affiche_orientation(stdout, "stdout");
return 0;
}
Le flux stdout n'a pas encore d'orientation
Une ligne
Le flux stdout est orienté multiplets
L’indicateur de conversion ls
Enfin, sachez que les fonctions printf()
et scanf()
disposent d’un indicateur de conversion permettant d’afficher ou de récupérer des chaînes de caractères larges : l’indicateur ls
. Toutefois, il y a une subtilité : la conversion en caractères larges est effectuée avant l’écriture ou après la lecture, dès lors, le flux utilisé doit être orienté multiplets.
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
static void affiche_orientation(FILE *fp, char *nom)
{
int orientation = fwide(fp, 0);
fprintf(stderr, "Le flux %s ", nom);
if (orientation < 0)
fprintf(stderr, "est orienté multiplets\n");
else if (orientation > 0)
fprintf(stderr, "est orienté caractères larges\n");
else
fprintf(stderr, "n'a pas encore d'orientation\n");
}
int main(void)
{
wchar_t ws[255];
affiche_orientation(stdout, "stdout");
if (setlocale(LC_CTYPE, "") == NULL)
{
perror("setlocale");
return EXIT_FAILURE;
}
if (scanf("%254ls", ws) != 1)
{
perror("scanf");
return EXIT_FAILURE;
}
printf("%ls\n", ws);
affiche_orientation(stdout, "stdout");
return 0;
}
Le flux stdout n'a pas encore d'orientation
Élégant
Élégant
Le flux stdout est orienté multiplets
Comme vous le voyez, nous avons lu et écrit une chaîne de caractères larges, pourtant le flux stdout
est orienté multiplets. Cela tient au fait que scanf()
lit des caractères et effectue seulement ensuite leur conversions en caractères larges. Inversement printf()
converti les caractères larges en caractères avant de les écrire.
Notez que l’inverse est possible à l’aide des fonctions wscanf()
et wprintf()
.
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
static void affiche_orientation(FILE *fp, char *nom)
{
int orientation = fwide(fp, 0);
fprintf(stderr, "Le flux %s ", nom);
if (orientation < 0)
fprintf(stderr, "est orienté multiplets\n");
else if (orientation > 0)
fprintf(stderr, "est orienté caractères larges\n");
else
fprintf(stderr, "n'a pas encore d'orientation\n");
}
int main(void)
{
char s[255];
affiche_orientation(stdout, "stdout");
if (setlocale(LC_CTYPE, "") == NULL)
{
perror("setlocale");
return EXIT_FAILURE;
}
if (wscanf(L"%254s", s) != 1)
{
perror("wscanf");
return EXIT_FAILURE;
}
wprintf(L"%s\n", s);
affiche_orientation(stdout, "stdout");
return 0;
}
Le flux stdout n'a pas encore d'orientation
Élégant
Élégant
Le flux stdout est orienté caractères larges
<wchar.h> : les fonctions de manipulation des chaînes de caractères
Le tableau ci-dessous liste quelques fonctions et leur équivalent déclaré dans l’en-tête <wchar.h>
.
<string.h> |
<wchar.h> |
---|---|
strlen | wcslen |
strcpy | wcscpy |
strcat | wcscat |
strcmp | wcscmp |
strchr | wcschr |
strpbrk | wcspbrk |
strctr | wcsstr |
strtok | wcstok |
Ces fonctions se comportent de manière identique à leur équivalent, la seule différence est qu’elles manipulent des caractères larges ou des chaînes de caractères larges au lieu de caractères ou de chaînes de caractères.
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
int
main(void)
{
char ligne[255];
if (fgets(ligne, sizeof ligne, stdin) == NULL)
{
perror("fgets");
return EXIT_FAILURE;
}
if (setlocale(LC_CTYPE, "") == NULL)
{
perror("setlocale");
return EXIT_FAILURE;
}
wchar_t ws[sizeof ligne];
if (mbstowcs(ws, ligne, sizeof ligne) == (size_t)-1)
{
perror("mbstowcs");
return EXIT_FAILURE;
}
printf("strlen : %zu\n", strlen(ligne));
printf("wcslen : %zu\n", wcslen(ws));
return 0;
}
Élégant
strlen : 10
wcslen : 8
L'en-tête <wctype.h>
Pour terminer, sachez qu’il existe l’en-tête <wctype.h>
qui fournit les mêmes fonctions de classification que l’en-tête <ctype.h>
, mais pour les caractères larges.
<ctype.h> |
<wctype.h> |
---|---|
isupper | iswupper |
islower | iswlower |
isdigit | iswdigit |
isxdigit | iswxdigit |
isblank | iswblank |
isspace | iswspace |
iscntrl | iswcntrl |
ispunct | iswpunct |
isalpha | iswalpha |
isalnum | iswalnum |
isgraph | iswgraph |
isprint | iswprint |
En résumé
- Le type entier
wchar_t
permet de stocker la valeur la plus élevée d’une table de correspondance donnée ; - La conversion des chaînes de caractères littérales en chaînes de caractères littérales larges est assurée par le compilateur ;
- Pour les autres chaînes, les fonctions de conversions
mbstowcs()
etwcstombs()
doivent être utilisées ; - La table de correspondance et l’encodage utilisé par le système sont récupérés grâce à la fonction
setlocale()
; - Il n’y a pas de solution pour déterminer la table de correspondance et/ou l’encodage utilisés pour écrire les données d’un fichier ;
- Etant donné que la table de correspondance et l’encodage utilisés pour représenter les chaînes de caractères littérales et les entrées du terminal sont susceptibles d’être différents, il est parfaitement possible d’obtenir des chaînes de caractères larges différentes ;
- Les flux ont une orientation qui détermine s’ils manipulent des caractères ou des caractères larges. Une fois cette orientation fixée, elle ne peut plus être changée ;
- Un flux orienté multiplets ne peut pas être utilisé par des fonctions de lecture/écriture larges et, inversement, un flux orienté caractères larges ne peut pas être utilisé par des fonctions de lecture/écriture classiques ;
- La fonction
fwide()
permet de connaître et/ou fixer l’orientation d’un flux ; - L’en-tête
<wchar.h>
fournit à peu de chose près un équivalent gérant les caractères larges de chaque fonction de lecture/écriture et de manipulation des chaînes de caractères ; - L’en-tête
<wctype.h>
fournit les mêmes fonctions de classification que l’en-tête<ctype.h>
, mais pour les caractères larges.