Scanf problémák

Feladatunk írni egy programot, amelyik kér egy egész számot; utána pedig beolvas egy sornyi szöveget.

#include <stdio.h>

int main(void) {
    int i;
    char s[200];
    
    printf("Szam: ");
    scanf("%d", &i);
    printf("Szoveg: ");
    fgets(s, 200, stdin);
    
    printf("A szamod: %d, a szoveged: [%s]\n", i, s);
    
    return 0;
}

Érdemes kipróbálni: a program nem olvassa be a szöveget. Helyette a fgets() híváson látszólag átugrik, de igazából üres sort rak s-be.

Szam: 5
Szoveg: A szamod: 5, a szoveged: [
]

Vajon miért, és mit lehet tenni, hogy ez ne így legyen?

1. Mi történik?

Tegyük fel, hogy beírjuk: 5 <enter> hello <enter>. Ekkor a program a szabványos bemenetén a következő karaktereket kapja:

5hello

Logikus is: a scanf %d beolvassa az 5-ös számot, beírja i-be. A következő karakternél megáll, mert az az enter. Nem számjegy, ezért nem tartozhat egy egész számhoz. Így kerül az 5 a változóba, és tér vissza a scanf() függvény 1-gyel. Ez az enter ott fog maradni a bemeneten. A következő fgets() hívás enterig olvassa a sort. Mivel az első karakter, amit ez meglát, pont az enter, ezért kerül az a sztringbe. A bemeneten egyébként a hello megmarad, ezt mondjuk egy következő fgets() hívás beolvasná. Vagy ha utána megint egy scanf %d jönne, az meg hibakóddal térne vissza.

Erre szokták azt javasolni, hogy fflush(stdin)-t kell írni. Nem, nem kell azt írni. A C szabvány egy olvasásra megnyitott fájlnál az fflush()-tól nem vár el semmit; vagyis a szabvány szerint az fflush(stdin) utasítás hatása definiálatlan. Windowson, az MSDN szerint az fflush() input streamre „kiüríti a bemeneti puffert”, ami elég érdekesen hangzik, ugyanis nem egyértelmű, honnan kellene tudni, hogy meddig tart a bemeneti puffer. Ez a hülyeség annyira elterjedt, hogy könyvekben is megjelent; és annyira, hogy más operációs rendszereken és függvénykönyvtárakon, pl. az újabb Linuxokon is elkezdték leutánozni az fflush() ilyen jellegű funkcionalitását – tisztán kompatibilitási okokból, hogy a rosszul megírt programok működjenek. Azért csak jegyezzük meg: az fflush(stdin) a szabvány szerint értelmetlen.

Hogy lehet akkor javítani a programot? Legegyszerűbben úgy, hogy a scanf %d hívás után beteszünk egy üres getchar()-t. Ott kell lennie egy enternek, azt az entert beolvassuk, eldobjuk. A fgets() pedig majd már a h betűt látja elsőnek. Ezt a getchar() hívást én közvetlenül a scanf %d után tenném. Akkor az a scanf()+getchar() kombó beolvas egy számot, és nem hagy maga után semmit a bemeneten. Nem a következő beolvasás leprogramozásánál kell emlékeznünk, hogy az előzőnél még maradt valamit a bemeneten. A javított rész:

printf("Szam: "); scanf("%d", &i); getchar();
printf("Szoveg: "); fgets(s, 200, stdin);
Szam: 5
Szoveg: hello
A szamod: 5, a szoveged: [hello]

2. Hogyan tovább? – Sor beolvasása

Most már akkor fejezzük be, amit elkezdtünk. Mi történik akkor, ha a felhasználó azt írja be, hogy 5 szóköz enter hello enter? A bemeneten ez lesz:

5hello

Vagyis a getchar() hívásunk a szóközt fogja beolvasni, és az előző probléma újból előáll.

Pontosan mit is kellene csinálnunk? A bemeneten van egy szám, utána lehetnek egyéb dolgok, amik minket nem érdekelnek, az enterig; végül pedig egy enter (ami úgyszint nem érdekel bennünket). Nagyon egyszerű, olvassuk be ezeket is, és dobjuk el. Egyik megoldás lehet erre, hogy egészen addig olvasunk, amíg entert nem kapunk. Enternek biztosan lennie kell előbb-utóbb.

while (getchar() != '\n')
    ; /* üres */

A scanf()-et is használhatjuk erre. A scanf %[] hasonló a %s-hez; egy szót olvas be egy sztringbe, csak itt a szögletes zárójelek között megadhatjuk azokat a karaktereket, amelyek a szóban szerepelhetnek. Meg lehet adni tartományt is, pl. sscanf("hello123", "%[a-z]", s) az s sztringbe „hello”-t ír. Meg lehet adni azt is, hogy egy adott karakter ne szerepeljen a beolvasott szóban (vagyis annál megálljon a feldolgozás). Ezt a ^ kalap karakterrel tehetjük meg. Emiatt a következő scanf() hívás egy egész sort beolvas a sztringbe (az entert a bemeneten hagyja):

char s[200];
scanf("%[^\n]", s);

Most, amit beolvastunk, azt el szeretnénk dobni. Utána az entert is be szeretnénk olvasni, és azt is el szeretnénk dobni, ezért a bűvös formátumsztring a szám beolvasása után:

int i;
scanf("%d", &i);
scanf("%*[^\n]%*c"); // nem is kap változót

Ez jó kell legyen, hiszen azért áll meg a %[], mert entert talált, a következő, a %c által beolvasott karakter az maga az enter lesz.

3. Ugyanez karakterre

A scanf %c-nek van egy érdekes tulajdonsága. Az összes többi konverzió (pl. %d, %s stb.) a beolvasott whitespace karaktereket eldobja, csak amikor nem whitespace karaktert talál, akkor kezdi meg a konverziót. Emiatt mindegy, ha egy számra várunk, hogy a felhasználó "5"-öt, vagy "␣␣5"-öt ír be. A scanf %c viszont nem teszi ezt. Direkt, hogy egy whitespace karaktert is be lehessen vele olvasni.

Írjunk egy programot, amelyik megkérdezi, hogy „igen(i) vagy nem(n)”, és utána kiírja, melyiket választottuk! Ha a scanf %c-nek azt írjuk, hogy "␣␣␣i", akkor a szóközt fogja a c-be tenni, és nem működik rendesen. A scanf()-et a formátumsztringjében egy szóköz karakterrel kérhetjük arra, hogy a szokásos whitespace karakterek eldobását elvégezze. Szóval a scanf() hívás, amelyik beolvas egy karaktert, de nem zavarja, ha szóköz van előtte; illetve a többi karaktert eldobja, és az entert sem hagyja ott a bemeneten:

#include <stdio.h>

int main(void) {
    char c;
    
    printf("igen(i) vagy nem(n)? ");
    scanf(" %c%*[^\n]%*c", &c); // omg
    switch (c) {
        case 'i': printf("igen\n"); break;
        case 'n': printf("nem\n"); break;
        default:  printf("???\n"); break;
    }
    
    return 0;
}

4. Fájlkezelésben

Fájlok kezelése esetén minden pontosan ugyanúgy történik, mintha billentyűzetről olvasnánk. Tehát a fent bemutatott problémák ott is előjöhetnek, és a megoldási módszer is ugyanaz. Nem véletlen, hogy ezek miatt gyakran a szövegfájlokból egész sorokat olvasunk be egyszerre, és utána a beolvasott sztringgel dolgozunk tovább. Akkor biztosan tudjuk, hogy mennyit haladtunk előre a fájlban. A beolvasott sort pedig egyben látjuk, onnan tudjuk esetleg tovább bontani az adatokat egy sscanf() vagy egy strtok() függvénnyel.