C: prevod reťazcov na čísla cez atoi() a strtol()

Krátka verzia

Na prevod reťazcov na čísla nepoužívajte atoi(). Namiesto toho použite strtol() [aj atoi() to robí tak]. Nahrubo môžete naparsovať cez:

long parsuj_long(char * vstup, int default) {
    char * zvysok;
    long cislo;
    errno = 0;

    cislo = strtol(vstup, &zvysok, 10);
    if(errno != 0             /* pretiekol rozsah long-u */ 
        || *p == vstup        /* mame prazdny retazec */
    {
        return default;
    }
}

Toto požuje "-3" (-3), "12 opic" (12), "\t\t\t 1643 orkov" (1643) a pre ostatné prípady vráti implicitnú hodnotu z premennej default.

Ale teraz rewind!

Atolica

Kto by nechcel prevádzať reťazce na čísla? Najjednoduchšia možnosť je atoi(), lebo:

Prevedie reťazec na celé číslo.

Ak si urobíme

atoi("12")

Uvidíme 12 a všetko je v poriadku. Lenže skúsme toto a hádajme výstup:

printf("%d\n", atoi(""));

Odpovede: Debian, Commodore 64 [ick!] a MingW-nuté Windowsy svorne 0.

A teraz?

printf("%d\n", atoi("hello"));

Opäť: všetci svorne 0. Ale toto:

printf("%d\n", atoi("12 opic"));

Zase: jednotne 12. A grand finále!

printf("%d\n", atoi("99999999999999"));

Debian: 2147483647!. Commodore: 32764 a Windowsy: -1530494977!

Ako to funguje?

Lebo dokumentácia vraví:

Ak hodnotu nemožno reprezentovať, správanie funkcie atoi() je nedefinované.

A ešte:

Volanie atoi(str) je ekvivalentné volaniu:

(int) strtol(str, (char **)NULL, 10)

Ak poštudujeme strtol(), čo je veselé čítanie, zistíme, že:

  • Ak sa prevod čísla nevie uskutočniť, vráti sa nula. Odtiaľ nuly pre prázdny reťazec, resp. pre krásne okrúhle číslo hello.

  • Ak je hodnota mimo rozsahu reprezentovateľných čísiel, dostaneme, podľa znamienka, buď najväčšie kladné celé číslo alebo najmenšie záporné číslo príslušneho typu… a ako bonus sa nám nastaví premenná errno na hodnotu ERANGE.

A áno, z prvého bodu vyplýva napríklad, že atoi("hura") a atoi("0") vrátia stále nulu. Čistá radosť.

Druhý bod vidieť pri atoi("99999999999999"): navrátená hodnota je naozaj LONG_MAX.

Úplne podivná fáza je pri dvanástich opiciach. Ako to, že atoi("12 opic") je 12?

To preto, že strtol() funguje takto:

  • popreskakuje všetky úvodné biele miesta.
  • pokúsi sa interpretovať všetky znaky v príslušnej sústave ako cifry, a prezieravo zahrnie aj úvodné plus či mínus.
  • ak už nevie žuvať znaky do čísla, zastane v parsovaní a zvyšok reťazca nechá tak.

A preto "12 opic" vedie k 12ke. Úvodné biele miesto neexistuje a zvyšok po medzere sa jednoducho neprečíta.

Stále je to na nič: atoi() teda vráti buď nulu (“akože neviem parsovať” ale aj “zadal si nulu”) alebo kladné či záporné nekonečno (“akože si dal pridlhé číslo” ale aj “zadal si kladné, či záporné nekonečno”).

Strtoliny poriadne

Ak chceme mať robustné parsovanie, neostáva nám nič iné, než sa rukami chopiť strtol()u.

long strtol(const char *restrict str, char **restrict endptr, int base);

POSIXová špecifikácia je napísaná ako právnický text, ale našťastie máme klasického Kernighana a Ritchieho, ktorí to uvádzajú ako pre ľudí. Je to približne to, čo bolo spomenuté vyššie, akurát obohatené o viaceré sústavy:

strtol prevedie predponu reťazca str na long, pričom ignoruje úvodné biele miesta. Pointer na neprevedený zvyšok uloží do *endptr (okrem prípadu, že endptr je NULL). Ak je sústava base medzi 2 a 36, prevod sa uskutoční podľa tejto sústavy. Ak je sústava 0, využije sa osmičková, desiatková, resp. šestnástková sústava: úvodná nula indikuje osmičkovú, úvodná 0x a 0X uvádza šestnástkovú spstavu. Písmená v oboch prípadoch reprezentujú cifry medzi 10 až base - 1. V šestnástkovej sústave je povolená úvodná 0x, resp. 0X. Ak výsledok pretečie, podľa znamienka výsledku sa vráti buď LONG_MAX alebo LONG_MIN a premenná errno sa nastaví na ERANGE.

Inými slovami:

  • Prvý parameter je jasný: obsahuje vstupný string.
  • Druhý parameter môže obsahovať pointer na reťazec (teda pointer na pointer na char), ktorý sa nastaví na takú adresu, od ktorej už nemožno vstupný reťazec ďalej parsovať. O tomto o chvíľu!
  • Tretí parameter udáva sústavu: ak je 2, ideme binárne, ale môžeme ísť dokonca aj v 36kovej sústave (hell yeah!, lebo cifry 0..10 a 26 písmen A..Z).

Dva funďamentálne stavy sú:

  • máme chybu v errno? Pretiekli sme z rozsahu longu a vrátime buď LONG_MAX, či LONG_MIN.
  • je * endptr rovnaký ako vstup? Nevieme nič naparsovať (buď máme neporiadok alebo prázdny reťazec).

Tretí bonusový:

  • ukazuje * endptr na niečo iné než na \0? Na konci ostal nenaparsovaný neporiadok!

Zistiť nahrubo, či je reťazec číslom, môžeme teda:

int je_cislo(char * vstup) {
    char * zvysok;
    long cislo;
    errno = 0;

    cislo = strtol(vstup, &zvysok, 10);

    return errno == 0        /* nepretiekli sme rozsah longu */
            && *p == '\0'    /* neostal nenaparsovany zvysok */
            && *p != vstup;  /* nemame prazdny retazec */
}

Všetky stavy sú toť:

  • optimistický: vráti sa korektné číslo.

    • a pointer na pointer na char v druhom parametri sa nastaví na prvé miesto, odkiaľ už nemožno parsovať: v tomto optimistickom prípade je to koncové \0: na základe klasickej konvencie je každý C-reťazec ukončený práve týmto znakom. (Áno, hackity hack.)
  • prehnane optimistický: načíta sa priveľké, či primalé číslo

    • premenná errno sa nastaví na ERANGE
    • vráti sa LONG_MAX, či LONG_MIN
  • skoro optimistický: načíta sa číslo, za ktorým ostane neporiadok ("12 opíc")
    • vráti sa najnaparsovanejšie číslo
    • a pointer na pointer na char v druhom parametri sa nastaví na prvé miesto, odkiaľ už nemožno parsovať: v prípade opíc na medzeru za dvojkou.
  • pesimistický: načíta sa blbosť ("hurá").
    • vráti sa nula
    • pointer na pointer na char bude ukazovať na nultý znak vstupu.
  • nihilistický: načíta sa prázdny vstup
    • vráti sa nula
    • pointer na pointer na char bude ukazovať na nultý znak vstupu, ktorým je \0 (prázdny reťazec "" je totiž pole { '\0' };

Najlepší prípad, krásne rýdze číslo, nastáva, ak druhý parameter ukazuje na znak \0, neukazuje na začiatok reťazca (lebo \0 na začiatku reťazca indikuje prázdny reťazec) a premenná errno nie je ERANGE.

A na záver, ak sme sa ešte nezdiveli, si môžeme napísať funkciu, ktorá naparsuje int medzi nulou a ak sa číslo nedá naparsovať, vráti implicitnú hodnotu:

int parsuj_kladne(char * vstup, int default) {
    char * zvysok;
    long cislo;
    errno = 0;

    cislo = strtol(vstup, &zvysok, 10);
    if(errno != 0             /* pretiekol rozsah long-u */ 
        || *p != '\0'         /* ostal nenaparsovany zvysok */
        || *p == vstup        /* mame prazdny retazec */
        || cislo < 0          /* mame zaporne cislo */
        || cislo > INT_MAX    /* vypadli sme z rozsahu int-u */
    {
        return default;
    }
}

Pramene

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *