Les caractères larges

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

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;
}
Résultat
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;
}
Résultat
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;
}
Résultat
É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.

Localisation par défaut

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;
}
Résultat
É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.

Des chaînes, des encodages

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;
}
Résultat
É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;
}
Résultat
É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.

  1. Une fois l’orientation d’un flux établie, elle ne peut plus être changée ;

  2. 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);
  1. Un flux binaire orienté caractères larges voit ses déplacements possibles via la fonction fseek() davantage limités. Ainsi, seul le repère SEEK_SET avec une valeur nulle ou précédemment retournée par la fonction ftell et le répère SEEK_CUR avec une valeur nulle peuvent être utilisés ;

  2. 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;
}
Résultat
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;
}
Résultat
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;
}
Résultat
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;
}
Résultat
É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é
  1. Le type entier wchar_t permet de stocker la valeur la plus élevée d’une table de correspondance donnée ;
  2. 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 ;
  3. Pour les autres chaînes, les fonctions de conversions mbstowcs() et wcstombs() doivent être utilisées ;
  4. La table de correspondance et l’encodage utilisé par le système sont récupérés grâce à la fonction setlocale() ;
  5. 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 ;
  6. 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 ;
  7. 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 ;
  8. 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 ;
  9. La fonction fwide() permet de connaître et/ou fixer l’orientation d’un flux ;
  10. 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 ;
  11. 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.