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 hodnotuERANGE
.
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ťazcastr
nalong
, pričom ignoruje úvodné biele miesta. Pointer na neprevedený zvyšok uloží do*endptr
(okrem prípadu, žeendptr
jeNULL
). Ak je sústavabase
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
a0X
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
aleboLONG_MIN
a premennáerrno
sa nastaví naERANGE
.
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 rozsahulong
u a vrátime buďLONG_MAX
, čiLONG_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.)
- a pointer na pointer na
prehnane optimistický: načíta sa priveľké, či primalé číslo
- premenná
errno
sa nastaví naERANGE
- vráti sa
LONG_MAX
, čiLONG_MIN
- premenná
- 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
- Secure Coding: INT06: Use
strtol()
or a related function to convert a string token to an integer - POSIX:
strtol()
- POSIX:
atoi()
- GNU C Library: Parsing of Integers