Scanf problémák

Czirkos Zoltán · 2019.02.27.

A karakterek és a sorvége jel kezelése a scanf() függvénynél.

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. Vagyis egyetlen egy \n újsor karaktert.

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.

Ha egy valaki elkezdi...

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 tévképzet 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. Érdemes ezt a getchar() hívást közvetlenül a scanf %d után tenni. 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 */

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:

#include <stdio.h>

int main(void) {
    char c;
    
    printf("igen(i) vagy nem(n)? ");
    scanf(" %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.