C - le scanf() vs gets() et fgets()

j'ai fait un programme assez facile de convertir une chaîne de caractères (en supposant que les nombres sont entrés) à un entier.

après avoir été fait, j'ai remarqué quelques" bogues "très particuliers que je ne peux pas répondre, principalement en raison de ma connaissance limitée de la façon dont les fonctions scanf() , gets() et fgets() fonctionnent. (Je n'ai lu beaucoup de littérature.)

sans écriture trop de texte, voici le code du programme:

#include <stdio.h>

#define MAX 100

int CharToInt(const char *);

int main()
{
    char str[MAX];

    printf(" Enter some numbers (no spaces): ");
    gets(str);
//  fgets(str, sizeof(str), stdin);
//  scanf("%s", str);

    printf(" Entered number is: %dn", CharToInt(str));

    return 0;
}

int CharToInt(const char *s)
{
    int i, result, temp;

    result = 0;
    i = 0;

    while(*(s+i) != '"151900920"')
    {
        temp = *(s+i) & 15;
        result = (temp + result) * 10;
        i++;
    }

    return result / 10;
}

voilà le problème que j'ai eu. Tout d'abord, en utilisant la fonction gets() , le programme fonctionne parfaitement.

Second, en utilisant fgets() , le résultat est légèrement erroné parce que apparemment fgets() fonction lit newline (ASCII valeur 10) caractère dernier qui bousille le résultat.

troisièmement, en utilisant la fonction scanf() , le résultat est complètement erroné parce que le premier caractère apparemment, il a une valeur ASCII de -52. Pour cela, je n'ai pas d'explication.

maintenant je sais que gets() est déconseillé d'utiliser, donc je voudrais savoir si je peux utiliser fgets() ici donc il ne lit pas (ou ignore) le caractère newline. En outre, Quel est le problème avec la fonction scanf() dans ce programme?

30
demandé sur Jonathan Leffler 2010-07-21 21:54:59

7 réponses

  • jamais utilisez gets . Il n'offre aucune protection contre une vulnérabilité de débordement de tampon (c'est-à-dire que vous ne pouvez pas lui dire quelle est la taille du tampon que vous lui passez, de sorte qu'il ne peut pas empêcher un utilisateur d'entrer dans une ligne plus grande que le tampon et la mémoire de cloquage).

  • éviter d'utiliser scanf . S'il n'est pas utilisé avec soin, il peut avoir les mêmes problèmes de débordement de tampon que gets . Même ignorant cela, il a d'autres problèmes qui le rendent difficile à utiliser correctement .

  • généralement, vous devez utiliser fgets à la place, bien que ce soit parfois gênant (vous devez démonter la nouvelle ligne, vous devez déterminer une taille de tampon à l'avance, et ensuite vous devez comprendre ce qu'il faut faire avec les lignes qui sont trop longues–est-ce que vous gardez la partie que vous lisez et jeter l'excès , jeter le tout, faire pousser dynamiquement le tampon et réessayer, etc.). Il existe certaines fonctions non standard disponibles qui font cette allocation dynamique pour vous (par exemple getline sur les systèmes POSIX, Domaine public de Chuck Falconer ggets fonction). Notez que ggets a la sémantique gets en ce sens qu'il vous ouvre une nouvelle ligne de départ.

25
répondu jamesdlin 2015-01-28 08:24:31

Oui, vous voulez éviter gets . fgets Lira toujours la nouvelle ligne si le buffer était assez grand pour la tenir (ce qui vous permet de savoir quand le buffer était trop petit et il y a plus de la ligne qui attend d'être lue). Si vous voulez quelque chose comme fgets qui ne lira pas la nouvelle ligne (en perdant l'indication d'un tampon trop petit), vous pouvez utiliser fscanf avec une conversion scan-set comme: "%N[^\n]" , où le " N " est remplacé par la taille du tampon-1.

une façon facile (si étrange) d'enlever la nouvelle ligne traînante d'un tampon après avoir lu avec fgets est: strtok(buffer, "\n"); ce n'est pas comme cela que strtok est destiné à être utilisé, mais je l'ai utilisé de cette façon plus souvent que dans la mode prévue (que j'évite généralement).

19
répondu Jerry Coffin 2010-07-21 18:01:17

il y a nombreux problèmes avec ce code. Nous allons corriger les variables et les fonctions mal nommées et enquêter sur les problèmes:

  • D'abord, CharToInt() doit être renommé au bon StringToInt() car il fonctionne sur une chaîne de caractères pas un seul caractère.

  • la fonction CharToInt() [sic.] est dangereux. Il ne vérifie pas si l'utilisateur passe accidentellement dans un pointeur NULL.

  • il ne valide pas les entrées, ou plus correctement, sauter les entrées invalides. Si l'utilisateur entre dans un non-chiffres le résultat contiendra une fausse valeur. i.e. si vous entrez dans N le code *(s+i) & 15 produira 14 !?

  • suivant, l'indescriptible temp dans CharToInt() [sic.] devrait être appelé digit car c'est ce qu'il est vraiment être.

  • aussi, le kludge return result / 10; est juste que -- un mauvais hack pour travailler autour d'une implémentation buggy.

  • de même MAX est mal nommé car il peut sembler en conflit avec l'usage standard. c'est-à-dire #define MAX(X,y) ((x)>(y))?(x):(y)

  • le verbeux *(s+i) n'est pas aussi lisible que simplement *s . Il n'y a pas de besoin pour utiliser et encombrer le code avec un index temporaire i .

gets ()

c'est mauvais parce qu'il peut déborder le tampon de chaîne de caractères. Par exemple, si la taille du tampon est 2, et que vous entrez 16 caractères, vous déborderez str .

scanf ()

c'est tout aussi mauvais parce qu'il peut déborder le tampon de la chaîne de caractères.

vous mentionnez " en utilisant la fonction scanf (), le résultat est complètement erroné parce que le premier caractère a apparemment une valeur ASCII -52.

qui est dû à une utilisation incorrecte de scanf(). Je n'ai pas pu dupliquer ce bug.

fgets ()

c'est sûr parce que vous pouvez garantir que vous ne débordez jamais le tampon de chaîne de caractères en passant dans la taille du tampon (qui comprend la place pour le NULL.)

getline ()

quelques personnes ont suggéré le C norme POSIX getline() comme un remplacement. Malheureusement, ce N'est pas une solution pratique portable que Microsoft ne met pas en œuvre une version C; seulement la norme C++ string template function comme ceci SO #27755191 questions réponses. Le C++ de Microsoft getline() était disponible au moins depuis longtemps Visual Studio 6 mais puisque L'OP pose strictement des questions sur C et non C++, ce n'est pas une option.

Misc.

enfin, cette implémentation est boguée en ce qu'elle ne détecte pas de dépassement d'entier. Si l'utilisateur entre un trop grand nombre le nombre peut devenir négatif! c'est à dire 9876543210 deviendra -18815698 ?! Nous allons résoudre ce problème aussi.

c'est trivial de fixer pour un unsigned int . Si la précédente partiel nombre est inférieur à l'actuel numéro partiel, puis nous avons débordé et nous retourner le précédent numéro partiel.

Pour un signed int c'est un peu plus de travail. Dans l'assemblage, nous pourrions inspecter le carry-flag, mais en C il n'y a pas de moyen standard intégré pour détecter le débordement avec des mathématiques int signées. Heureusement, puisque nous multiplions par une constante, * 10 , nous pouvons facilement le détecter si nous utilisons une équation équivalente:

n = x*10 = x*8 + x*2

Si x*8 déborde alors logiquement x*10 le fera aussi. Pour un débordement int de 32 bits se produira quand x*8 = 0x100000000 donc tout ce que nous avons à faire est de détecter quand x >= 0x20000000. Puisque nous ne voulons pas supposer combien de bits un int a, nous n'avons qu'à tester si les 3 premiers msb (Bits les plus significatifs) sont positionnés.

en outre, un deuxième essai de débordement est nécessaire. Si l'ESM est réglé (bit de signe) après la concaténation de chiffre alors nous savons aussi le nombre déborder.

Code

Voici une version sécurisée fixe avec du code que vous pouvez jouer pour détecter le débordement dans les versions dangereuses. J'ai également inclus à la fois un signed et unsigned versions via #define SIGNED 1

#include <stdio.h>
#include <ctype.h> // isdigit()

// 1 fgets
// 2 gets
// 3 scanf
#define INPUT 1

#define SIGNED 1

// re-implementation of atoi()
// Test Case: 2147483647 -- valid    32-bit
// Test Case: 2147483648 -- overflow 32-bit
int StringToInt( const char * s )
{
    int result = 0, prev, msb = (sizeof(int)*8)-1, overflow;

    if( !s )
        return result;

    while( *s )
    {
        if( isdigit( *s ) ) // Alt.: if ((*s >= '0') && (*s <= '9'))
        {
            prev     = result;
            overflow = result >> (msb-2); // test if top 3 MSBs will overflow on x*8
            result  *= 10;
            result  += *s++ & 0xF;// OPTIMIZATION: *s - '0'

            if( (result < prev) || overflow ) // check if would overflow
                return prev;
        }
        else
            break; // you decide SKIP or BREAK on invalid digits
    }

    return result;
}

// Test case: 4294967295 -- valid    32-bit
// Test case: 4294967296 -- overflow 32-bit
unsigned int StringToUnsignedInt( const char * s )
{
    unsigned int result = 0, prev;

    if( !s )
        return result;

    while( *s )
    {
        if( isdigit( *s ) ) // Alt.: if (*s >= '0' && *s <= '9')
        {
            prev    = result;
            result *= 10;
            result += *s++ & 0xF; // OPTIMIZATION: += (*s - '0')

            if( result < prev ) // check if would overflow
                return prev;
        }
        else
            break; // you decide SKIP or BREAK on invalid digits
    }

    return result;
}

int main()
{
    int  detect_buffer_overrun = 0;

    #define   BUFFER_SIZE 2    // set to small size to easily test overflow
    char str[ BUFFER_SIZE+1 ]; // C idiom is to reserve space for the NULL terminator

    printf(" Enter some numbers (no spaces): ");

#if   INPUT == 1
    fgets(str, sizeof(str), stdin);
#elif INPUT == 2
    gets(str); // can overflows
#elif INPUT == 3
    scanf("%s", str); // can also overflow
#endif

#if SIGNED
    printf(" Entered number is: %d\n", StringToInt(str));
#else
    printf(" Entered number is: %u\n", StringToUnsignedInt(str) );
#endif
    if( detect_buffer_overrun )
        printf( "Input buffer overflow!\n" );

    return 0;
}
9
répondu Michaelangel007 2017-05-23 12:09:56

vous avez raison de ne jamais utiliser gets . Si vous voulez utiliser fgets , vous pouvez simplement écraser la nouvelle ligne.

char *result = fgets(str, sizeof(str), stdin);
char len = strlen(str);
if(result != NULL && str[len - 1] == '\n')
{
  str[len - 1] = '"151900920"';
}
else
{
  // handle error
}

cela suppose qu'il n'y a aucun null intégré. Une autre option est POSIX getline :

char *line = NULL;
size_t len = 0;
ssize_t count = getline(&line, &len, stdin);
if(count >= 1 && line[count - 1] == '\n')
{
  line[count - 1] = '"151910920"';
}
else
{
  // Handle error
}

l'avantage de getline est qu'il fait l'allocation et la réallocation pour vous, il gère des NULLs intégrés possibles, et il retourne le compte de sorte que vous ne on perd du temps avec strlen . Notez que vous ne pouvez pas utiliser un tableau avec getline . Le pointeur doit être NULL ou libre.

Je ne sais pas quel problème vous avez avec scanf .

4
répondu Matthew Flaschen 2014-09-10 04:46:54

n'utilisez jamais gets(), il peut conduire à des débordements impraticables. Si votre tableau de chaînes de caractères est de taille 1000 et que j'entre 1001 caractères, je peux surcharger votre programme.

3
répondu Peter Miehle 2010-07-21 18:05:41

essayez d'utiliser fgets () avec cette version modifiée de votre CharToInt ():

int CharToInt(const char *s)
{
    int i, result, temp;

    result = 0;
    i = 0;

    while(*(s+i) != '"151900920"')
    {
        if (isdigit(*(s+i)))
        {
            temp = *(s+i) & 15;
            result = (temp + result) * 10;
        }
        i++;
    }

    return result / 10;
}

il valide essentiellement les chiffres d'entrée et ignore toute autre chose. C'est très rudimentaire donc modifiez-le et salez au goût.

1
répondu Amardeep AC9MF 2010-07-21 18:22:07

donc je ne suis pas vraiment un programmeur mais laissez-moi essayer de répondre à votre question sur le scanf(); . Je pense que le scanf est assez bien et l'utiliser pour la plupart de tout sans avoir aucun problème. Mais vous avez pris une structure pas tout à fait correcte. Il devrait être:

char str[MAX];
printf("Enter some text: ");
scanf("%s", &str);
fflush(stdin);

Le "&" devant la variable est importante. Il indique au programme où (dans quelle variable) sauvegarder la valeur scannée. le fflush(stdin); efface le tampon de l'entrée standard (clavier) donc vous êtes moins susceptible d'obtenir un dépassement de tampon.

et la différence entre gets/scanf et fgets est que gets(); et scanf(); ne numérisent que jusqu'au premier espace ' ' tandis que fgets(); scanne l'entrée entière. (mais assurez-vous de nettoyer le tampon par la suite afin que vous n'obtiendrez pas un débordement plus tard)

-2
répondu nicolas gasser 2014-12-02 09:09:03