Indicateurs de conversion de scanf : problème dans le cours de C ?

Une conversion suggérée par le cours ne fonctionne pas...

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Voici le passage du cours auquel se réfère ma question : chapitre "manipulations basiques des entrée sorties".

Pour l’utilisation de scanf, il est dit qu’on utilise l’indicateur %hhd pour récupérer un signed char. Je fais donc ce test basique :

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

int main()
{
	char x, y; 				//j'ai aussi tenté avec signed char et unsigned char

	scanf("%hhd %hhd", &x, &y); 		//j'entre 45 46
	printf("x = %d | y = %d\n", x, y); 	//affiche 0 | 46 (pas logique ?)

	return 0;
}

Le cours commet-il une erreur ? (je suis bien au fait de la complexité de scanf et de l’impossibilité de donner un aperçu exhaustif de son fonctionnement au début d’un cours pour débutants, mais on sait jamais).

+0 -0

Salut @AScriabine,

Chez moi ça marche™.
Je ne vois a priori aucune erreur dans le code ni aucune raison pour que tu obtiennes ce résultat.
As-tu vérifié le retour de scanf(), à tout hasard ?

Edit : en fait, le comportement fait penser à un buffer overflow, comme si scanf() écrivait plus qu’un byte. Vu que les variables x et y se suivent dans la pile et que ta machine est très probablement petit boutiste, si scanf() tente d’écrire 46 représenté sur plus d’un byte (par exemple deux avec 0010 1110 0000 0000), alors la valeur de x est remplacée par zéro. Reste à savoir pourquoi ton implémentation de scanf() se comporte ainsi. Sur quel OS compiles-tu et avec quel compilateur ?

+4 -0

Merci pour la réponse,

Oula ça a l’air compliqué tout ça haha ! :euh: Je suis sur Windows 64bits, processeur x64, je compile avec gcc en ligne de commande. Voici la ligne de compilation : gcc *.c -o exe (oui pas de warning et tout je sais c’est pas bien du tout ! mais c’est juste pour faire des petits tests). Sinon, je ne sais pas si mon gcc est en 32 ou 64 bits…

Que te donne le test suivant ?

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char x = 'Z';
    printf("known as character = %c\n", x); // Z
    printf("stored as integer = %d\n", x); // 90
    return 0;
}

Ensuite, que te donne le test ci ?

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char x = '-', y = '.';
    printf("x = %d | y = %d\n", x, y); // 45 46
    return 0;
}

Si jusque là tout marche comme espéré, alors c’est que le souci est avec scanf()

Pour la version de GCC, que te renvoi

gcc --version
+0 -0

J’ai fait le même test sans problème sur Windows 10 sous cmd.

#include <stdio.h>
int main(void) {
    char x, y;
    scanf("%hhd %hhd", &x, &y);
    printf("%d %d\n", x, y);
}

Mon compilateur est:

gcc (MinGW-W64 x86_64-posix-seh, built by Brecht Sanders) 10.2.0
Copyright © 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

J’ai compilé avec gcc -o af af.c -Wall -Wextra

et je n’ai pas de warning.

Le programme fonctionne comme attendu. Je crois qu’il n’y a pas d’erreur dans le cours à ce niveau.

Si on entre un nombre supérieur à 255, il est masquéà 8 bits.

Entre 128 et 255, on aura un nombre négatif.

+0 -0

Quel est le terminal que tu utilises pour exécuter ton programme ?
Je suspecte le caractère espace que tu entres de ne pas en être vraiment un.

Si j’essaie d’exécuter le programme avec 45 46 en entrée tout fonctionne bien, mais si je remplace l’espace par une espace insécable j’obtiens le même comportement que toi par exemple (x vaut 45 mais y vaut 0).

@Gil Cot : quel est le but des deux tests exactement ? Le problème vient visiblement de scanf(), pas de printf() ou de l’encodage des caractères.

@entwanne : dans l’exemple donné c’est x qui est à zéro et y qui est à 46. Ce serait toujours intéressant d’avoir la valeur de retour de scanf(), cela dit c’est quasiment certain qu’elle sera de 2 et qu’il lit bien les deux nombres vu qu'y vaut 46.

@AScriabine : comme demandé par @Gil Got, ce serait intéressant d’avoir ta version de gcc et aussi de savoir comment tu l’as installé (via Msys2, via Code::Blocks ou un autre IDE ou une installation directe de MinGW ?) car cela détermine quelle bibliothèque C tu utilises.

+2 -0

Il faut bien se rendre compte qu’il n’y a pas de souci d’encodage (cf. le commentaire « j’ai aussi tenté avec signed char et unsigned char » dans le code initial) ; Et je n’accuse point printf() pas plus que scanf() qui n’a certainement pas de souci… (mais ça je comptais le constater par deux petits tests aussi.)

+0 -0

@Gil Cot : je ne saisis pas ton explication. L’entrée est composée de deux nombres, or les chiffres ne posent pas de problèmes d’encodage : ils ont le même point de code (0x30 à 0x39) dans toutes les tables : ASCII, IMB850, UNICODE, etc. La seule exception serait une entrée en UTF-16 ou en UTF-32, mais la présence de bytes nuls ne produirait pas la sortie présentée, peu importe le boutisme.

+1 -0

Et si on faisait à la place un fgets d’une chaîne assez longue et qu’on affichait les caractères en hexadécimal?

On saurait si ce sont de vrais espaces ou des espaces insécables.

D’après mes tests sur un copier-coller à partir d’un autre site, les espaces insécables ont deux codes: C2 A0

+0 -0

@Gil Cot Premier test que tu suggères :

known as character = Z
stored as integer = 90

Deuxième test :

x = 45 | y = 46

Ma version de gcc : gcc (MinGW.org GCC-6.3.0–1) 6.3.0 Copyright © 2016 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

@entwanne j’utilise le terminal "classique" de windows… Enfin celui qui s’ouvre tout seul quand tu fais un printf ;)

@Taurre j’ai installé gcc directement avec minGW en suivant bêtement ce tuto https://fr.wikihow.com/compiler-un-programme-en-C-avec-le-compilateur-GNU-GCC#:~:text=Installez%20GNU%20GCC.&text=Tapez%20la%20commande%20sudo%20apt,le%20gestionnaire%20de%20compilation%20Make.

Merci pour votre aide à tous

Je ne sais pas sur quelle version de Windows tu es: 7, 8, 8.1, 10, … ?

Je trouve que ta version du compilateur est assez vieille. En voici une plus récente:

mingw-v10.1.0 free download - SourceForge.

Cependant, je ne crois pas que ce soit le vrai problème. J’ai déjà été sur Windows 8.1 avec MinGw-W64 à la version 8.3 et je n’ai jamais eu ce genre de problème.

Je te suggère encore de faire des tests avec fgets.

+1 -0

@PierrotLeFou : comme dit précédemment, ce n’est pas lié à l’entrée, elle est correcte.

@Taurre j’ai installé gcc directement avec minGW en suivant bêtement ce tuto https://fr.wikihow.com/compiler-un-programme-en-C-avec-le-compilateur-GNU-GCC#:~:text=Installez%20GNU%20GCC.&text=Tapez%20la%20commande%20sudo%20apt,le%20gestionnaire%20de%20compilation%20Make.

AScriabine

Ok, le problème est alors probablement effectivement là.
Je n’ai pas les détails techniques sous-jacent ne connaissant pas suffisamment Windows, mais pour faire bref, ton programme utilise actuellement une implémentation de la bibliothèque C qui n’est pas conforme à la norme C99 (l’indicateur de conversion hhd ayant été introduit par celle-ci).

Tu as 2 solutions sous Windows (en plus de la machine virtuelle ou de WSL, mais dans ce cas ce n’est pas de la compilation sous Windows) pour disposer d’une bibliothèque standard à jour :

  • Utiliser Msys2 qui compense ce qui manque de base sous Windows en liant ton programme à une bibliothèque spécifique, libmsys2 ;
  • Installer Visual Studio Community 2022 et utiliser son compilateur C à la place de MinGW.

Edit : comme le suggère @PierrotLeFou, installer une version plus récente de MinGW corrige peut-être également le problème.

A priori, la version actuelle de scanf() que tu emploies interprète l’indicateur hhd comme hd. On peur vérifié ça aisément avec le code ci-dessous.

#include <stdio.h>

int
main(void) {
	int x = 0x11111111, y = 0x11111111;

	scanf("%hhd %hhd", &x, &y);
        printf("%08x, %08x\n", x, y);
	return 0;
}

Si tu fournis en entrée 45 46 et obtiens en sortie 1111002d 1111002e alors c’est bien que scanf() utilises des short int et non des signed char. Tu peux aussi simplement tester ton code initial en changeant le type des variables x et y par short.

(Oui, ce code ne respecte techniquement pas la norme, mais chut. :p )

+1 -0

@Taurre Voici le retour du code que tu suggères :

45 46
0000002d, 0000002e

AScriabine

Comme l’a dit @dalfab, l’exécution du code montre que l’indicateur hhd est utilisé comme d (donc scanf() stocke la valeur dans un int).

Et par curiosité, en quoi ton code ne respectait pas la norme ?

AScriabine

Concernant le respect de la norme1, celle-ci impose qu’un objet ne peut être modifié que via une lvalue (la norme défini une lvalue comme une expression désignant un objet) qui :

  • a le même type ;
  • a une version qualifiée du même type (genre const int) ;
  • a une version non signée ou signée du même type ;
  • est une union ou une structure dont un des membres respecte un des trois critères précédents ;
  • est de type char, signed char ou unsigned char.

En très gros, dans l’exemple que je donnais, je partais du principe que scanf() allait interprété l’indicateur hhd comme hd. Ce qui veux dire que scanf() allait interpréter les deux pointeurs que je lui passe comme des pointeurs sur short alors que ce sont des pointeurs sur int. Foncièrement cela revient au même que d’écrire ceci.

int x = 0x11111111, y = 0x11111111;
int *p = (short *)&x, *q = (short *)&y;
*p = 45; /* Interdit */
*q = 46; /* Interdit */

Ce code est invalide car j’accède à un objet de type int via une lvalue (ici *p et *q) de type short. Note que je parle bien d’accès, même si je lisais simplement le contenu ce serait également invalide.

En revanche, si hhd est interprété comme d c’est correct car le type est identique et si hhd est interprété comme char (signé ou non signé) c’est correct également (dernier point de la liste).


  1. ISO/IEC 9899:2017, N2310, § 6.5 Expressions, al. 7, pp. 55–56.
+2 -0

Sorry, j’ai été absent par ici (sur d’autres fronts)

@Gil Cot : je ne saisis pas ton explication. L’entrée est composée de deux nombres, or les chiffres ne posent pas de problèmes d’encodage : ils ont le même point de code (0x30 à 0x39) dans toutes les tables : ASCII, IMB850, UNICODE, etc. La seule exception serait une entrée en UTF-16 ou en UTF-32, mais la présence de bytes nuls ne produirait pas la sortie présentée, peu importe le boutisme.

Taurre

Alors, outre le problème explicitement posé (auquel je ne réponds qu’à la fin en m’inquiétant de la version de GCC), j’ai noté qu’il y a un point à éclaircir (et puis c’est toujours mieux de pouvoir se concentrer sur un bogue sans plusieurs trucs qui parasitent.)

Il faut bien se rendre compte qu’il n’y a pas de souci d’encodage (cf. le commentaire « j’ai aussi tenté avec signed char et unsigned char » dans le code initial)

Gil Cot

Je fais référence au passage suivant…

	char x, y; 				//j'ai aussi tenté avec signed char et unsigned char

AScriabine

…mais effectivement, mes tests sont un peu mal choisis (point de code inférieur à 126) Un truc que beaucoup ne saisissent pas en se mettant au C, c’est que le type char n’est pas un vrai type au même sens que int ou short mais juste l’indication qu’on stocke un octet représentant normalement (mais pas obligatoirement) le code d’un caractère… ^^ L’autre point, une fois ceci compris, est que par défaut les types entiers sont signés par défaut (quand je mets juste int c’est synonyme de signed int) alors que le pseudo-type caractère c’est souvent1 l’inverse et cela fait sens (puisqu’on utilise tout l’octet… en fait quand on ne prend pas en compte le premier bit, comme on le ferait si c’est un bit de signe, on se retrouve en train d’utiliser explicitement de l’ASCII i.e. 7 bits) :p

1 Je veux dire que quand je mets juste char c’est souvent synonyme de unsigned char …Mais attention que c’est en réalité implementation-defined sauf dans deux cas : quand char et sort ont la même taille, alors char tout court doit être signé ; quand le jeu de caractères requis par C sur la plateforme cible map avec des codes dépassant la plus grande valeur de signed char alors char tout court doit être non signé.

En relisant l’échange, je crois comprendre que j’ai laissé comprendre que le souci pourrait être l’encodage ou au niveau de printf() ? En terminant par

Si jusque là tout marche comme espéré, alors c’est que le souci est avec scanf()

Gil Cot

L’idée était qu’il fallait être bien au fait des données qu’on manipule, d’où le clin d’œil

    printf("known as character = %c\n", x); // Z
    printf("stored as integer = %d\n", x); // 90

Gil Cot

…et une fois que ceci n’est plus un sujet, on pouvait en revenir à nos moutons

Et je n’accuse point printf() pas plus que scanf() qui n’a certainement pas de souci… (mais ça je comptais le constater par deux petits tests aussi.)

Gil Cot

Ce point a été traité, par toi, entre temps : bien comprendre la différence entre %d et %hd et %hhd Il se trouve, accessoirement, que du fait la vieille version de compilateur utilisée, %hhd était compris comme %d

les 2 entiers ont été entièrement mis à jour. Donc le "%hhd" est compris comme si c’était "%d".

Le format %hhd existe depuis le C99, ton compilateur semble être sur une version datant d’avant 1999. Il faudrait vérifier sa version.

dalfab
+0 -2

…mais effectivement, mes tests sont un peu mal choisis (point de code inférieur à 126) Un truc que beaucoup ne saisissent pas en se mettant au C, c’est que le type char n’est pas un vrai type au même sens que int ou short mais juste l’indication qu’on stocke un octet représentant normalement (mais pas obligatoirement) le code d’un caractère… ^^ L’autre point, une fois ceci compris, est que par défaut les types entiers sont signés par défaut (quand je mets juste int c’est synonyme de signed int) alors que le pseudo-type caractère c’est souvent1 l’inverse et cela fait sens (puisqu’on utilise tout l’octet… en fait quand on ne prend pas en compte le premier bit, comme on le ferait si c’est un bit de signe, on se retrouve en train d’utiliser explicitement de l’ASCII i.e. 7 bits) :p

1: Je veux dire que quand je mets juste char c’est souvent synonyme de unsigned char …Mais attention que c’est en réalité implementation-defined sauf dans deux cas : quand char et sort ont la même taille, alors char tout court doit être signé ; quand le jeu de caractères requis par C sur la plateforme cible map avec des codes dépassant la plus grande valeur de signed char alors char tout court doit être non signé.

Gil Cot

Ils ne sont pas « un peu mal choisis », ils sont inutiles, ils n’apportent rien au problème posé. Tu n’as d’ailleurs toujours pas expliqué leur utilité en trois messages successifs (celui-ci inclus).

Sinon, je ne sais pas où tu veux en venir avec tes histoires de « le type char n’est pas un vrai type », ce que je sais en revanche c’est que la norme le défini bien comme tel.

Par ailleurs, en ce qui me concerne, je n’ai jamais vu un code où le type char est un synonyme de unsigned char. En un sens on s’en fout, vu que comme tu le précises, le choix du caractère signé ou non du type char est laissé au soin du compilateur, mais dire que « c’est souvent synonyme de unsigned char » est un abus de langage.

En relisant l’échange, je crois comprendre que j’ai laissé comprendre que le souci pourrait être l’encodage ou au niveau de printf() ? En terminant par […] L’idée était qu’il fallait être bien au fait des données qu’on manipule, d’où le clin d’œil […] …et une fois que ceci n’est plus un sujet, on pouvait en revenir à nos moutons.

Gil Cot

Sauf que, une fois encore, tu n’expliques pas en quoi cela serait un problème d’encodage (au sens : d’où déduis-tu un tel problème du message initial ?), ni en quoi tes tests l’aurait démontré.

+1 -0

Oui, « ils n’apportent rien au problème posé. » Mais comme je l’ai déjà écrit : « outre le problème explicitement posé, j’ai noté qu’il y a un point à éclaircir » Et avant cela : « Il faut bien se rendre compte qu’il n’y a pas de souci d’encodage (cf. le commentaire “ j’ai aussi tenté avec signed char et unsigned char ” dans le code initial) »

Bon, « tu n’expliques pas en quoi cela serait un problème d’encodage » Comme marqué, « je crois comprendre que j’ai laissé comprendre » précise que ce que tu as cru comprendre n’est pas mon propos. J’invitais à approfondir les types de base et ne pas penser qu’il y a quelque lien avec le comportement constaté. Autrement dit, « // j'ai aussi tenté avec signed char et unsigned char » n’a pas de lien avec le problème exposé (et on pourrait même utiliser des types entiers pour ce code) ; mais je ne sais pas pourquoi tu tiens à me faire dire le contraire…


Quand à l’abus de langage, c’est lié à mon expérience et raccord avec la norme : j’ai travaillé trop longtemps-et-souvent sur des architectures où short est l’octet. (ceci trahi ma jeunesse.)
Hormis, les cas définis, le fait que le comportement par défaut soit laissé au compilateur fait que les deux cas peuvent exister ou qu’on puisse choisir : option /J chez MSVC et option -funsigned-char pour GCC qui était le comportement par défaut quand j’ai essayé Android NDK :euh: (j’avais lu que c’est pour des raisons de perf sur ARM…)

+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