Comment autoriser la saisie d'espaces à l'aide de scanf?


129

En utilisant le code suivant:

char *name = malloc(sizeof(char) + 256); 

printf("What is your name? ");
scanf("%s", name);

printf("Hello %s. Nice to meet you.\n", name);

Un utilisateur peut entrer son nom, mais lorsqu'il entre un nom avec un espace comme Lucas Aardvark, scanf()il coupe simplement tout après Lucas. Comment scanf()autoriser les espaces


9
Notez que plus idiomatique est 'malloc (sizeof (char) * 256 + 1)', ou 'malloc (256 + 1)', ou encore mieux (en supposant que 'nom' sera utilisé strictement localement) 'char name [256 + 1 ] '. Le '+1' peut agir comme une mneumonique pour le terminateur nul, qui doit être inclus dans l'allocation.
Barry Kelly

@Barry - Je soupçonne que sizeof(char) + 256c'était une faute de frappe.
Chris Lutz

Réponses:


186

Les gens (et en particulier les débutants) ne devraient jamais utiliser scanf("%s")ou gets()ou toute autre fonction qui n'a pas de protection contre le dépassement de la mémoire tampon, à moins que vous ne sachiez avec certitude que l'entrée sera toujours d'un format spécifique (et peut-être même pas dans ce cas).

N'oubliez pas que scanfsignifie «numérisation formatée» et il y a un peu moins formaté que les données saisies par l'utilisateur. C'est l'idéal si vous avez un contrôle total sur le format des données d'entrée mais généralement inadapté à la saisie utilisateur.

Utilisez fgets()(qui a une protection contre le débordement de tampon) pour obtenir votre entrée dans une chaîne et sscanf()pour l'évaluer. Puisque vous voulez juste ce que l'utilisateur a entré sans analyse, vous n'avez pas vraiment besoin sscanf()dans ce cas de toute façon:

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

/* Maximum name size + 1. */

#define MAX_NAME_SZ 256

int main(int argC, char *argV[]) {
    /* Allocate memory and check if okay. */

    char *name = malloc(MAX_NAME_SZ);
    if (name == NULL) {
        printf("No memory\n");
        return 1;
    }

    /* Ask user for name. */

    printf("What is your name? ");

    /* Get the name, with size limit. */

    fgets(name, MAX_NAME_SZ, stdin);

    /* Remove trailing newline, if there. */

    if ((strlen(name) > 0) && (name[strlen (name) - 1] == '\n'))
        name[strlen (name) - 1] = '\0';

    /* Say hello. */

    printf("Hello %s. Nice to meet you.\n", name);

    /* Free memory and exit. */

    free (name);
    return 0;
}

1
Je ne savais pas fgets(). Il semble en fait plus facile à utiliser alors scanf(). +1
Kredns

7
Si vous souhaitez simplement obtenir une ligne de l'utilisateur, c'est plus facile. C'est également plus sûr car vous pouvez éviter les débordements de tampon. La famille scanf est vraiment utile pour transformer une chaîne en différentes choses (comme quatre caractères et un int par exemple avec "% c% c% c% c% d") mais, même dans ce cas, vous devriez utiliser fgets et sscanf, pas scanf, pour éviter la possibilité d'un dépassement de la mémoire tampon.
paxdiablo

4
Vous pouvez mettre la taille maximale du tampon au format scanf, vous ne pouvez tout simplement pas en mettre une calculée à l'exécution sans construire le format au moment de l'exécution (il n'y a pas l'équivalent de * pour printf, * est un modificateur valide pour scanf avec un autre comportement: supprimer l'assignation ).
AProgrammer

Notez également que le scanfcomportement est indéfini si la conversion numérique déborde ( N1570 7.21.6.2p10 , dernière phrase, libellé inchangé depuis C89), ce qui signifie qu'aucune des scanffonctions ne peut être utilisée en toute sécurité pour la conversion numérique d'une entrée non approuvée.
zwol

@JonathanKomar et quiconque lira ceci à l'avenir: si votre professeur vous a dit que vous deviez utiliser scanfdans un devoir, ils ont eu tort de le faire, et vous pouvez leur dire que je l'ai dit, et s'ils veulent discuter avec moi à ce sujet , mon adresse e-mail se trouve facilement dans mon profil.
zwol

124

Essayer

char str[11];
scanf("%10[0-9a-zA-Z ]", str);

J'espère que cela pourra aider.


10
(1) Évidemment, pour accepter les espaces, vous devez mettre un espace dans la classe de caractères. (2) Notez que le 10 est le nombre maximum de caractères qui seront lus, donc str doit pointer vers un tampon de taille 11 au moins. (3) Le s final ici n'est pas une directive de format mais scanf essaiera ici de le faire correspondre exactement. L'effet sera visible sur une entrée comme 1234567890s où les s seront consommés mais mis nulle part. Une autre lettre ne sera pas consommée. Si vous mettez un autre format après le s, il ne sera en lecture que s'il y a un s à mettre en correspondance.
AProgrammer

Un autre problème potentiel, l'utilisation de - à un autre endroit que le premier ou le dernier est la mise en œuvre définie. Habituellement, il est utilisé pour les plages, mais ce que la plage désigne dépend du jeu de caractères. EBCDIC a des trous dans les plages de lettres et même en supposant un jeu de caractères dérivé ASCII, il est naïf de penser que toutes les lettres minuscules sont dans la plage az ...
AProgrammer

1
"% [^ \ n]" a le même problème que gets (), dépassement de la mémoire tampon. Avec la capture supplémentaire que le \ n final n'est pas lu; cela sera masqué par le fait que la plupart des formats commencent par sauter des espaces blancs, mais [n'en fait pas partie. Je ne comprends pas l'instance lors de l'utilisation de scanf pour lire des chaînes.
AProgrammer

1
Suppression du sà la fin de la chaîne d'entrée car il est à la fois superflu et incorrect dans certains cas (comme indiqué dans les commentaires précédents). [est son propre spécificateur de format plutôt qu'une variation de celui- sci.
paxdiablo

54

Cet exemple utilise un scanset inversé, donc scanf continue à prendre des valeurs jusqu'à ce qu'il rencontre un '\ n' - nouvelle ligne, donc les espaces sont également enregistrés

#include <stdio.h>

int main (int argc, char const *argv[])
{
    char name[20];
    scanf("%[^\n]s",name);
    printf("%s\n", name);
    return 0;
}

1
Attention aux débordements de tampon. Si l'utilisateur écrit un "nom" avec 50 caractères, le programme plantera probablement.
brunoais

3
Comme vous connaissez la taille de la mémoire tampon, vous pouvez l'utiliser %20[^\n]spour éviter les débordements de mémoire tampon
osvein

45 points et personne n'a souligné le culte évident de la cargaison d'avoir slà-bas!
Antti Haapala

22

Vous pouvez utiliser ceci

char name[20];
scanf("%20[^\n]", name);

Ou ca

void getText(char *message, char *variable, int size){
    printf("\n %s: ", message);
    fgets(variable, sizeof(char) * size, stdin);
    sscanf(variable, "%[^\n]", variable);
}

char name[20];
getText("Your name", name, 20);

DEMO


1
Je n'ai pas testé, mais sur la base d'autres réponses dans cette même page, je pense que la taille de tampon correcte pour scanf dans votre exemple serait: scanf("%19[^\n]", name);(toujours +1 pour la réponse concise)
Dr Beco

1
Tout comme une note latérale, sizeof(char)est par définition toujours 1, il n'est donc pas nécessaire de multiplier par elle.
paxdiablo

8

N'utilisez pas scanf()pour lire des chaînes sans spécifier une largeur de champ. Vous devez également vérifier les valeurs de retour pour les erreurs:

#include <stdio.h>

#define NAME_MAX    80
#define NAME_MAX_S "80"

int main(void)
{
    static char name[NAME_MAX + 1]; // + 1 because of null
    if(scanf("%" NAME_MAX_S "[^\n]", name) != 1)
    {
        fputs("io error or premature end of line\n", stderr);
        return 1;
    }

    printf("Hello %s. Nice to meet you.\n", name);
}

Sinon, utilisez fgets():

#include <stdio.h>

#define NAME_MAX 80

int main(void)
{
    static char name[NAME_MAX + 2]; // + 2 because of newline and null
    if(!fgets(name, sizeof(name), stdin))
    {
        fputs("io error\n", stderr);
        return 1;
    }

    // don't print newline
    printf("Hello %.*s. Nice to meet you.\n", strlen(name) - 1, name);
}

6

Vous pouvez utiliser la fgets()fonction pour lire une chaîne ou l'utiliser scanf("%[^\n]s",name);pour que la lecture de la chaîne se termine lors de la rencontre d'un caractère de nouvelle ligne.


Attention, cela n'empêche pas les débordements de tampon
brunoais

sn'y appartient pas
Antti Haapala

5

getline()

Fait désormais partie de POSIX, néanmoins.

Il prend également en charge le problème d'allocation de tampon que vous avez posé précédemment, bien que vous deviez vous occuper de freela mémoire.


La norme? Dans la référence que vous citez: "getline () et getdelim () sont des extensions GNU."
AProgrammer

1
POSIX 2008 ajoute getline. Donc GNU a pris de l'avance et a changé ses en-têtes pour la glibc autour de la version 2.9, et cela pose des problèmes pour de nombreux projets. Ce n'est pas un lien définitif, mais regardez ici: bugzilla.redhat.com/show_bug.cgi?id=493941 . Quant à la page de manuel en ligne, j'ai attrapé la première que google a trouvée.
dmckee --- ex-moderator chaton

3

Si quelqu'un cherche toujours, voici ce qui a fonctionné pour moi: lire une longueur arbitraire de chaîne, y compris des espaces.

Merci à de nombreuses affiches sur le web pour partager cette solution simple et élégante. Si cela fonctionne, le mérite leur revient, mais toutes les erreurs sont les miennes.

char *name;
scanf ("%m[^\n]s",&name);
printf ("%s\n",name);

2
Il est à noter qu'il s'agit d'une extension POSIX et n'existe pas dans la norme ISO. Par souci d'exhaustivité, vous devriez probablement également vérifier errnoet nettoyer la mémoire allouée.
paxdiablo

sn'appartient pas là après le scanset
Antti Haapala

1

Vous pouvez utiliser scanfà cette fin avec une petite astuce. En fait, vous devez autoriser l'entrée de l'utilisateur jusqu'à ce que l'utilisateur clique sur Entrée ( \n). Cela prendra en compte tous les caractères, y compris l' espace . Voici un exemple:

int main()
{
  char string[100], c;
  int i;
  printf("Enter the string: ");
  scanf("%s", string);
  i = strlen(string);      // length of user input till first space
  do
  {
    scanf("%c", &c);
    string[i++] = c;       // reading characters after first space (including it)
  } while (c != '\n');     // until user hits Enter
  string[i - 1] = 0;       // string terminating
return 0;
}

Comment ça marche? Lorsque l'utilisateur saisit des caractères à partir de l'entrée standard, ils seront stockés dans une variable chaîne jusqu'au premier espace vide. Après cela, le reste de l'entrée restera dans le flux d'entrée et attendra le prochain scanf. Ensuite, nous avons une forboucle qui prend char par char du flux d'entrée (till \n) et les ajoute à la fin de la variable de chaîne , formant ainsi une chaîne complète identique à l'entrée utilisateur depuis le clavier.

J'espère que cela aidera quelqu'un!


Sous réserve de dépassement de la mémoire tampon.
paxdiablo

0

Bien que vous ne devriez vraiment pas utiliser scanf()pour ce genre de chose, car il existe de bien meilleurs appels tels que gets()ou getline(), cela peut être fait:

#include <stdio.h>

char* scan_line(char* buffer, int buffer_size);

char* scan_line(char* buffer, int buffer_size) {
   char* p = buffer;
   int count = 0;
   do {
       char c;
       scanf("%c", &c); // scan a single character
       // break on end of line, string terminating NUL, or end of file
       if (c == '\r' || c == '\n' || c == 0 || c == EOF) {
           *p = 0;
           break;
       }
       *p++ = c; // add the valid character into the buffer
   } while (count < buffer_size - 1);  // don't overrun the buffer
   // ensure the string is null terminated
   buffer[buffer_size - 1] = 0;
   return buffer;
}

#define MAX_SCAN_LENGTH 1024

int main()
{
   char s[MAX_SCAN_LENGTH];
   printf("Enter a string: ");
   scan_line(s, MAX_SCAN_LENGTH);
   printf("got: \"%s\"\n\n", s);
   return 0;
}

2
Il y a une raison pour laquelle a getsété déconseillé et supprimé ( stackoverflow.com/questions/30890696/why-gets-is-deprecated ) de la norme. Il est encore pire que scanfparce qu'au moins ce dernier a des moyens de le rendre sûr.
paxdiablo

-1
/*reading string which contains spaces*/
#include<stdio.h>
int main()
{
   char *c,*p;
   scanf("%[^\n]s",c);
   p=c;                /*since after reading then pointer points to another 
                       location iam using a second pointer to store the base 
                       address*/ 
   printf("%s",p);
   return 0;
 }

4
Pouvez-vous expliquer pourquoi c'est la bonne réponse? Veuillez ne pas publier de réponses basées uniquement sur le code.
Theo

sn'appartient pas là après le scanset
Antti Haapala
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.