La fonction setlocale() en C

Bizarreries constatées

a marqué ce sujet comme résolu.

Bonjour,

J’introduis un petit contexte à ma question : je crée un fichier nommé "monFichier.txt", encodé en utf-8, dans lequel j’écris le caractère 'Ä’. Il a la particularité d’être stocké sur 2 caractères en utf-8 :)

Je crée le petit code suivant (je ne fais aucun test de sécurité genre vérifier que le fichier est bien ouvert etc. pour ne pas nuire à la lisibilité du post) :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <locale.h>

#define TAILLE 100

int main()
{
	char* encodage = setlocale(LC_ALL, "");
	if (encodage != NULL) printf("encodage terminal : %s\n", encodage);

	FILE* fichier = fopen("monFichier.txt", "r");

	char chaine[TAILLE];
	fgets(chaine, TAILLE, fichier);

	printf(	"\nstrlen(chaine) : %zu\n", strlen(chaine));
	printf(	"chaine         : %s\n", chaine);

	int k;
	for (k = 0 ; k < strlen(chaine) ; k++)
		printf("chaine[%d]      : %u, %c\n", k, (unsigned char)chaine[k], chaine[k]);

	fclose(fichier);
	return 0;
}

Il me semble bien que la ligne char* encodage = setlocale(LC_ALL, ""); renvoie la localisation actuelle utilisée par le terminal si j’en crois cette phrase du cours du site "En fait, l’appel à setlocale() permet aux fonctions de traductions de connaître la table et l’encodage utilisés par notre système" (dans le chapitre sur les chaînes de caractères larges). Donc pour moi, cette ligne permet de signaler aux fonctions l’encodage du terminal de la sortie standard sur Windows ?

et voici le retour de mon code :

encodage terminal : French_France.1252

strlen(chaine) : 2
chaine         : Ã"
chaine[0]      : 195, Ã
chaine[1]      : 132, "

En allant voir sur le net la table indiquée par l’encodage du terminal (French_France.1252) ce n’est pas cohérent ! Comme montré ci-dessus, le caractère 'Ä' est encodé en utf-8 par les octets 195 et 132, ce qui dans la table de French_France.1252 correspond à 'Ã' et '„' (et non pas '"’, la différence n’est pas esthétique, ce sont bien deux caractères différents).

Si maintenant on refait le test en changeant la localisation par char* encodage = setlocale(LC_ALL, "C"); pour remettre en anglais par défaut, on a ce retour :

encodage terminal : C

strlen(chaine) : 2
chaine         : Ä
chaine[0]      : 195, ├
chaine[1]      : 132, ä

En me perdant sur internet pour mettre l’encodage de mon terminal en utf-8, j’ai finalement essayé d’entrer cette commande dans mon script de compilation : CHCP 65001>nul (si j’ai bien compris ça force le terminal à utiliser de l’utf-8). Et là miracle, le terminal renvoie, pour les deux précédents codes :

encodage terminal : French_France.1252

strlen(chaine) : 2
chaine         : Ä
chaine[0]      : 195, Ã
chaine[1]      : 132, „

Souvenez-vous, c’est bien un guillemet vers le bas qu’il fallait afficher, la table French_France.1252 a cette fois bien été respectée ! Mais aucune idée de pourquoi…

encodage terminal : C

strlen(chaine) : 2
chaine         : Ä
chaine[0]      : 195,
chaine[1]      : 132,

Cette fois encore c’est logique car on a mis le terminal en utf-8 donc 'Ä' est interprété comme un seul caractère par ce-dernier.

Enfin, ma question : puisque manifestement la ligne char* encodage = setlocale(LC_ALL, ""); ne renseigne pas sur l’encodage utilisé dans le terminal, à quoi sert-elle ? Elle change bien quelque chose mais elle ne fait pas ce qu’il faudrait… Et pourquoi le fait de passer le terminal en utf-8 a fait fonctionner mon bazar (surtout dans le premier cas) ?

Merci pour votre aide

+0 -0

Rapidement. La locale du programme (au moment de l’exécution) n’a aucun effet sur les bytes que l’on va envoyer dans un fichier. Il n’y a pas de réencodage à la volée. Au mieux cela va être utilisé pour choisir comment on représente le séparateur décimal (virgule ou point) ou par des fonctions dédiées qui vont produire des dates différemment. Si on veut des messages dans une langue ou une autre, si on veut qu’une glyphe soit encodée en UTF-8 ou latin-15 ou OEM-850 ou en Windows1252 ben… il faut nous même réaliser l’encodage à la main. Entre les standards et certaines bibliothèques (standard de facto) il y a déjà de quoi faire des choses.

Aussi. Tu vas jongler entre 3 encodages:

  • celui de ton source — cela se règle au niveau de l’éditeur — où l’on décide quels bytes vont où
  • celui du fichier que tu produis — qui sera affiché d’une façon ou d’une autre selon l’éditeur, le visualisateur, la console où il est dumpé, etc.
  • celui du terminal — qui fait l’association séquence d’octets -> glyphe affiché dans la police courante du terminal

Sous Linux, c’est facile. C’est UTF-8 partout maintenant.

Sous Windows, c’est le zouk. Et ma conclusion personnelle est qu’avoir des petits programmes jouets qui vont balancer du texte en français n’en vaut pas la peine.

AMHA,
char* encodage = setlocale(LC_ALL, ""); force le terminal à utiliser la page de code du système. Visiblement, c’est dans ton cas la page 1252.

char* encodage = setlocale(LC_ALL, "C"); forve le terminal à utiliser la page de code 850.

Reste à savoir pourquoi dans la page de code 1252, tu vois s’afficher x22 pour la valeur 132.

+0 -0

Bonjour,

Comme précisé par lmghs, un appel à setlocale n’a aucune influence sur l’encodage des caractères stockés ou affichés. Elle permet surtout de connaître la langue, le pays et l’encodage de caractères par défaut du système, quand elle est appelée avec une chaîne vide en paramètre.

Elle a comme effet de bord de redéfinir, entres autres, les formats de nombres et de dates par défaut. Par exemple, en France, le séparateur décimal est ",", alors qu’il est "." en locale "C", ce qui a comme conséquence que sscanf("3.14", "%g", &valeurDouble) va échouer avec la locale française de définie alors que ça marchera avec la locale "C". Ca peut d’ailleurs poser plein de problèmes inattendus, entres autres lors de la lecture de fichiers de configuration censés être normalisés et indépendants de la locale.

Par ailleurs, la chaîne retournée par setlocale n’est pas normalisée entre les systèmes. Sous windows, tu obtiens "French_France.1252", alors que sous linux tu obtiendrais "fr_FR.UTF-8". En plus sauf erreur le pays et l’encodage sont facultatifs.

Pour ton problème d’encodage, ça s’explique assez facilement.

  • L’encodage par défaut de la console sous windows est généralement CP850 en Europe de l’ouest, pour des raisons historiques qui datent de MS-DOS.
  • L’encodage UTF-8 dans la console windows est réputé buggé (en tout cas jusqu’à récemment), mieux vaut utiliser CP1252
  • Quand tu affiches ta chaîne octet par octet, tu ne fais aucune traduction. Si tu essaies d’afficher un point de code invalide, ce qui est le cas pour 132 en UTF-8 qui n’a pas de sens seul, le comportement est plus ou moins indéfini, et en général soit ça affiche un '?’, soit ça va piocher dans une autre table 8 bits comme CP850 ou CP1252. Ici tu as de la chance, mais c’est pas garanti.
  • Utiliser des caractères larges (wchar_t) ne fait que rajouter du bordel, et en plus potentiellement incompatible hors windows, donc à éviter, sauf si tu vises du windows only

Mon conseil est de switcher systématiquement l’encodage de la console en CP1252, ou UTF-8 si les réputés bugs ne te posent pas de problème. Sinon tu peux aussi utiliser des consoles alternatives qui fonctionneront peut-être mieux, par exemple commander, ou windows terminal sous windows 11.

+2 -0

Merci pour vos réponses,

@QuentinC Si je comprends bien ta réponse, en substance, tu me dis que le terminal ne recombine pas les caractères qui lui sont envoyés ? Cela est en contradiction avec le tout dernier test de mon post il me semble, où on voit que le terminal parvient tout à fait à afficher le caractère recombiné ? A moins que j’ai mal compris

Aussi, comment fait-on pour savoir la page de code utilisée par le terminal Windows ? Mes tests avancent, je pense que je suis sur une bonne piste

+0 -0

Ce n’est pas CHCP seul pour savoir ce qui est couramment employé?

Et si tu veux le changer par programmation, il y a une commande dédiée de l’API Win32 (?) SetConsoleOutputCP()

Mais… quel sera l’effet si le programme est lancé dans une autre console que celle couramment employée, ou meme sans console (en mode filtre p.ex.), je n’en sais rien.

Si je comprends bien ta réponse, en substance, tu me dis que le terminal ne recombine pas les caractères qui lui sont envoyés ? Cela est en contradiction avec le tout dernier test de mon post il me semble, où on voit que le terminal parvient tout à fait à afficher le caractère recombiné ?

Je me suis peut-être mal exprimé. Ca fonctionne très bien quand les octets sont bien envoyés à la suite.

Cependant, dans ta boucle, les octets 195 et 132 sont isolés l’un de l’autre, ils ne sont pas écrits à la suite. Tout seuls, ils sont invalides en UTF-8. Comme ils sont invalides en UTF-8, windows suppose que c’est du CP1252 et du coup affiche le caractère que tu voulais. Par contre, sous linux, l’UTF-8 invalide fera peut-être bugger ta console ou affichera un vilain carré à la place.

La commande chcp permet effectivement d’afficher l’encodage actuel ou de le changer.

La fonction SetConsoleOutputCP fait effectivement ce qu’elle dit, mais en vérité il vaudrait mieux ne pas l’utiliser. En plus de te fermer à un portage linux, un utilisateur qui a son windows en cyrillique et qui est sauf erreur en CP1251 risque de ne pas être très content en utilisant ton programme.

+2 -0

@QuentinC il est tout à fait normal en effet que dans ma boucle les octets soient affichés les uns après les autres, c’est justement ce que je cherche à faire. C’est juste histoire de visualiser ce qu’il se passe. Ma question n’est pas de savoir pourquoi ma boucle n’affiche pas un unique caractère, ce qui n’aurait aucun sens en effet, ma question est de savoir pourquoi ma boucle n’affiche pas les caractères que j’attends compte tenu des pages de codes/locales que je définis

Bonsoir,

Ce que tu ne sembles pas comprendre, c’est que l’octet 195 (0xC3) envoyé seul est invalide en UTF-8. Si tu veux afficher le point de code 195, en UTF-8, tu dois en réalité envoyer 0xC3 0x83. Le point de code 132 quant à lui n’existe tout simplement pas.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte