Scanf és hibakezelés

Csendes Dávid · 2019.09.26.

Hogyan jelzi a scanf a hibát beolvasáskor? Hogyan tudjuk a hibák típusait megkülönböztetni?

Amikor inputról olvasunk, ennek a sikerességét néha ellenőriznünk kell. Azonban gyakran félreértések övezik ezt a területet, és a scanf visszatérési értékét. Itt tisztázunk pár dolgot a következő kódon keresztül:

#include <stdio.h>
int main(void) {
    char betu = '$';
    int a = -23, b = -23, c = -23, d = -23;
    printf("scanf eredmenye: %d\n", scanf("%d%d%d%d", &a, &b, &c, &d));
    printf("a: %d , b: %d , c: %d , d: %d\n", a, b, c, d);
    if (scanf("%c", &betu) == EOF)
        printf("EOF erkezett!");
    else printf("betu: %c\n", betu);
    return 0;
}

Először is: a scanf nem adja vissza, milyen értéket olvasott be. Azt a megadott változókba tárolta el. Ez tulajdonképpen a legfontosabb, amit tudni kell, mert nagyon abszurd kódot tud eredményezni az ilyen feltételvizsgálat.

Mégis mi a visszatérési értéke a scanf függvénynek? A fent lévő kódot különböző inputokkal kipróbálva elég hamar rá lehet jönni: hány darab értéket olvasott be sikeresen.

A helyzetet egy dolog bonyolítja: ha valahol a beolvasás sikertelen lesz (pl. a lenti példában a c változó esetén), akkor az adott scanf befejezi a feldolgozást. Ezáltal ahol a beolvasás sikertelen lett, az az input még ott marad feldolgozásra, és egy későbbi scanf fel is tudja dolgozni. Erre egy jó példa a következő bemenet, ha begépeljük:

100 200 a 300

Mi is történik ekkor? A 100-at és a 200-at sikeresen beolvassa az a és a b változóba. Ekkor azonban egy karakterbe ütközik, amit nem tud feldolgozni – a scanf emiatt ezen a ponton visszatér. Két értéket tudott beolvasni sikeresen, ezért 2 lesz a visszatérési értéke. Mi a helyzet akkor az ott maradt 'a' betűvel? Az utána lévő scanf fel fogja dolgozni, és a betu változóba az 'a' fog kerülni (és annak a visszatérési értéke 1 lesz). A 300 ott maradt az inputon, nem került már feldolgozásra, a c és a d változóban pedig az eredeti érték maradt bent.

scanf eredmenye: 2
a: 100 , b: 200 , c: -23 , d: -23
betu: a

Mi történik end-of-file (EOF) jel kiadásakor? Adjunk ki egy EOF-ot a program legelején! (Windowson Ctrl+Z + Enter, Linuxon Ctrl-D.) A scanf eredménye egy negatív szám lesz. Adjunk ki még egyet, amikor a karaktert olvasná be! Kiírja, hogy EOF érkezett. Azaz az EOF értéke úgy van definiálva, hogy valamilyen negatív érték legyen, ami nyilván nem lehet azonos a beolvasott elemek számával. Az EOF konstans értelme az, hogy ezzel a kódban kifejezően jelezhetjük, mit vizsgálunk: end-of-file jelzést.

Mihez lehet kezdeni ezzel a sok háttértudással? Írjunk egy olyan függvényt, ami garantáltan beolvas egy olyan számot, aminek az értéke a megadott intervallumon belül van (addig nem hagyja a felhasználót tovább lépni a program futásában).

Először is nyilván könnyű elérni azt, hogy addig olvassunk, míg a beolvasott szám nem megfelelő tartománybeli:

// int min és max, amik a legkisebb és legnagyobb elfogadható értéket tárolják
int olvasott;
scanf("%d", &olvasott);
while (olvasott > max || olvasott < min) {
    printf("A megadott szam nem volt megfelelo tartomanybeli!");
    scanf("%d", &olvasott);
}

Ez megfelelően működik... mindaddig, míg csak számot adunk meg. Ha akár csak egy betűt is írunk, akkor végtelenszer elvégzi a kiírást. A fent leírtakból már tudjuk miért: a helytelen inputot, a karaktert a scanf ott hagyja az inputon, ami miatt a következő scanf ismét elhasal, majd ott hagyja az inputot... végtelen ciklusba kerültünk.

Hogyan kerülhetjük ezt el? A cél az lenne, hogy ha helytelen volt az input, akkor az eldobásra kerüljön. Egy másik írásban található erre egy jó módszer.

int sikerult, olvasott;
sikerult = scanf("%d", &olvasott);
while (sikerult != 1) {
    printf("Amit megadtal, nem volt szam!");
    scanf("%*[^\n]"); // enterig mindent eldob, lásd a linkelt írást
    sikerult = scanf("%d", &olvasott);
}

Ezt a kettőt már csak ötvözni kell, ki kell szervezni egy függvénybe, és kész is a hiper-szuper számbeolvasó függvényünk. Mivel a kétféle hibára másképp szeretnénk reagálni, ezért érdemesebb átszervezni kicsit a kódunkat ehhez.

int szam_beolvas(int min, int max) {
    // bár végtelen ciklusnak tűnik így, van benne return, ami megszakítja
    while (true) {
        int olvasott;
        int sikerult = scanf("%d", &olvasott);
        if (sikerult != 1) {
            printf("Amit megadtal, nem szam volt!\n");
            scanf("%*[^\n]");
        } else if (olvasott > max || olvasott < min) {
            printf("A megadott szam nem volt megfelelo! (min: %d, max: %d)\n", min, max);
        } else {
            return olvasott;
        }
    }
}