web-dev-qa-db-fra.com

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

J'ai fait un programme assez simple de conversion d'une chaîne de caractères (en supposant que des nombres soient entrés) en entier.

Après avoir terminé, j'ai remarqué des "bogues" très particuliers auxquels je ne peux pas répondre, principalement en raison de ma connaissance limitée de la façon dont les scanf(), gets() et fgets() les fonctions fonctionnent. (J'ai cependant lu beaucoup de littérature.)

Donc sans trop écrire 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: %d\n", CharToInt(str));

    return 0;
}

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

    result = 0;
    i = 0;

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

    return result / 10;
}

Voici donc le problème que j'ai rencontré. Tout d'abord, lorsque vous utilisez la fonction gets(), le programme fonctionne parfaitement.

Deuxièmement, lorsque vous utilisez fgets(), le résultat est légèrement faux car, apparemment, la fonction fgets() lit le caractère de nouvelle ligne (valeur ASCII 10) en dernier, ce qui fausse le résultat.

Troisièmement, lorsque vous utilisez la fonction scanf(), le résultat est complètement faux car le premier caractère a apparemment une valeur -52 ASCII. Pour cela, je n'ai aucune explication.

Maintenant, je sais que gets() est déconseillé d'utiliser, donc je voudrais savoir si je peux utiliser fgets() ici pour qu'il ne lise pas (ou ignore) le caractère de nouvelle ligne. De plus, quel est le problème avec la fonction scanf() dans ce programme?

35
Marko
  • Ne jamais utiliser 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 la taille du tampon que vous lui passez, donc il ne peut pas empêcher un utilisateur d'entrer une ligne plus grande que le tampon et de la mémoire de clobber).

  • Évitez d'utiliser scanf. S'il n'est pas utilisé avec précaution, il peut avoir les mêmes problèmes de débordement de tampon que gets. Même en l'ignorant, il a d'autres problèmes qui rendent son utilisation difficile .

  • En règle générale, vous devez utiliser fgets à la place, bien que cela soit parfois gênant (vous devez supprimer la nouvelle ligne, vous devez déterminer une taille de mémoire tampon à l'avance, puis vous devez savoir quoi faire avec les lignes trop longues - conservez-vous la partie que vous lisez et jetez l'excédent , jetez le tout, augmentez dynamiquement le tampon et réessayez, etc.). Il existe certaines fonctions non standard qui font cette allocation dynamique pour vous (par exemple getline sur les systèmes POSIX, fonction domaine public de Chuck Falconer ggets fonction). Notez que ggets a une sémantique semblable à gets dans la mesure où elle supprime pour vous une nouvelle ligne de fin.

27
jamesdlin

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

Une façon simple (si étrange) de supprimer la nouvelle ligne de fin d'un tampon après avoir lu avec fgets est: strtok(buffer, "\n"); Ce n'est pas ainsi que strtok est censé être utilisé, mais je l'ai utilisé de cette façon plus souvent que de la manière prévue (ce que j'évite généralement).

19
Jerry Coffin

Il y a nombreux problèmes avec ce code. Nous corrigerons les variables et les fonctions mal nommées et étudierons les problèmes:

  • Tout d'abord, CharToInt() doit être renommé en StringToInt() approprié car il fonctionne sur une chaîne pas un seul caractère .

  • La fonction CharToInt() [sic.] N'est pas sûre. Il ne vérifie pas si l'utilisateur passe accidentellement un pointeur NULL.

  • Il ne valide pas l'entrée, ou plus correctement, ignore les entrées non valides. Si l'utilisateur entre un chiffre non numérique, le résultat contiendra une valeur fausse. c'est-à-dire si vous entrez dans N le code *(s+i) & 15 produira 14!?

  • Ensuite, le non descriptif temp dans CharToInt() [sic.] Devrait être appelé digit puisque c'est ce qu'il est vraiment.

  • De plus, le kludge return result / 10; N'est que cela - un mauvais hack pour contourner une implémentation de buggy.

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

  • La fonction verbeuse *(s+i) n'est pas aussi lisible que simplement *s. Il n'est pas nécessaire d'utiliser et d'encombrer le code avec un autre index temporaire i.

obtient ()

C'est mauvais car cela peut déborder le tampon de chaîne d'entrée. 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 car cela peut déborder le tampon de chaîne d'entrée.

Vous mentionnez " lorsque vous utilisez la fonction scanf (), le résultat est complètement faux car le premier caractère a apparemment une valeur -52 ASCII."

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

fgets ()

Ceci est sûr car vous pouvez garantir que vous ne dépasserez jamais le tampon de chaîne d'entrée en transmettant la taille du tampon (qui inclut de la place pour le NULL.)

getline ()

Quelques personnes ont suggéré le standard POSIX C getline() en remplacement. Malheureusement, ce n'est pas une solution portable pratique car Microsoft n'implémente pas de version C; seule la fonction de modèle de chaîne C++ standard comme ceci SO # 27755191 répond aux questions. C++ de Microsoft getline() était disponible au moins aussi loin que Visual Studio 6 mais puisque l'OP pose strictement des questions sur C et non sur C++ ce n'est pas une option.

Divers.

Enfin, cette implémentation est boguée car elle ne détecte pas le débordement d'entier. Si l'utilisateur entre un nombre trop grand, le nombre peut devenir négatif! c'est-à-dire que 9876543210 deviendra -18815698?! Corrigeons cela aussi.

C'est trivial à corriger pour un unsigned int. Si le numéro partiel précédent est inférieur au numéro partiel actuel, alors nous avons débordé et nous renvoyons le numéro partiel précédent.

Pour un signed int C'est un peu plus de travail. Dans Assembly, nous pourrions inspecter le carry-flag, mais en C il n'y a pas de méthode standard intégrée 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 sera aussi. Pour un débordement int 32 bits se produira lorsque x * 8 = 0x100000000 donc tout ce que nous devons faire est de détecter lorsque x> = 0x20000000. Puisque nous ne voulons pas supposer combien de bits un int a, nous devons seulement tester si les 3 premiers msb (bits les plus significatifs) sont définis.

De plus, un deuxième test de débordement est nécessaire. Si le msb est défini (bit de signe) après la concaténation des chiffres, alors nous connaissons également le nombre débordé.

Code

Voici une version sécurisée fixe avec du code avec lequel vous pouvez jouer pour détecter le débordement dans les versions non sécurisées. J'ai également inclus les versions signed et unsigned 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;
}
10
Michaelangel007

Vous avez raison de ne jamais utiliser gets. Si vous souhaitez utiliser fgets, vous pouvez simplement remplacer la nouvelle ligne.

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

Cela suppose qu'il n'y a pas de NULL incorporés. 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] = '\0';
}
else
{
  // Handle error
}

L'avantage de getline est qu'il fait l'allocation et la réallocation pour vous, il gère les NULLs incorporés possibles, et il retourne le nombre afin que vous n'ayez pas à perdre 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 rencontrez avec scanf.

4
Matthew Flaschen

n'utilisez jamais gets (), cela peut entraîner des débordements imprévisibles. Si votre tableau de chaînes est de taille 1000 et que j'entre 1001 caractères, je peux déborder le tampon de votre programme.

3
Peter Miehle

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) != '\0')
    {
        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 brut donc modifiez-le et salez au goût.

1
Amardeep AC9MF