Minule se mně sice nikdo s řešením neozval (nevím, zda to bylo náročností úlohy nebo prostě tím, že jsou prázdniny), ale nevadí, zkusíme jinou úlohu :). Tentokrát jsem si hádanku vypůjčil z jednoho zdroje, který a priori uvést nemohu, protože pak byste se místo samotného řešení pokoušeli hledat řešení v onom zdroji :), ale po zveřejnění řešení ho samozřejmě uvedu. Kód jsem ale mírně upravil, aby to bylo zajímavější, takže není úplně shodný. Tentokrát se budou moct zapojit i ti, kteří znají "jen" Jazyk C.
Zadání
Následující program (napsaný v ISO C++98, ale po upravení hlavičkových souborů lze přeložit i pod ISO C99) otevře binární soubor obsahující čísla typu int
(v rozsahu daném platformou) a každé z nich zvětší o 1. Neuvažujme přetečení. Kvůli přehlednosti nejsou vypisovány žádné chybové hlášky. Ve které části kódu se nachází potenciální chyba tohoto řešení (místo + zdůvodnění)?
#include <cstdio> #include <cstdlib> int main(int argc, const char **argv) { if (!argv[1]) { // Nedostatečný počet argumentů return EXIT_FAILURE; } FILE *fd = fopen(argv[1], "rb+"); if (!fd) { // Nepodařilo se otevřít soubor return EXIT_FAILURE; } int i; while (1 == fread(&i, sizeof(i), 1, fd)) { i++; // Posuň se v souboru zpátky fseek(fd, -sizeof(i), SEEK_CUR); // Přepiš starou hodnotu novou hodnotou fwrite(&i, sizeof(i), 1, fd); fseek(fd, 0, SEEK_CUR); } fclose(fd); return EXIT_SUCCESS; }
Řešení
Než uvedu, co bylo skutečně potenciální chybou, tak bych chtěl zmínít, co chybou nebylo:
- Signatura funkce
main()
- standardní signatura je buďint main()
, neboint main(int argc, char *argv[])
, ale použitá signatura je funkčně ekvivalentní s druhou uvedenou:- Předávané pole do funkce se konvertuje na ukazatel na první prvek, čili
char **argv
je to stejné jakochar *argv[]
). - Dodatečný modifikátor
const
pouze značí, že se jedná o pole ukazatelů na konstatníchar
(předávané argumenty bychom neměli měnit).
- Předávané pole do funkce se konvertuje na ukazatel na první prvek, čili
- Podmínka
if (!argv[1])
je ekvivalentní podmínceif (argc < 2)
, protože standard nám zaručuje, žeargv[argc] == 0
a prvním argumentem je vždy spuštěný program. Každopádně jsem tento způsob detekce počtu parametrů uvedl jen pro zmatení a nikdo by jej neměl v produkčním kódu použít! Místo něj použijte nezatemněnou a preferovanou verziif (argc != 2)
. - Podmínka
if (!fd)
je ekvivaletní podmínceif (fd != 0)
(to už jsem tady probíral). - Zdánlivě nadbytečný
fseek(fd, 0, SEEK_CUR);
. Podle normy nesmí zafwrite()
ihned následovatfread()
či naopak, niž by byl volánfseek()
. Toto volání je zde tedy nezbytné (upraví stav proudu pofwrite()
tak, aby mohl následovat dalšífread()
) [1].
A kde byla tedy chyba?
Potenciální chyba se nalézá na tomto řádku [1]:
fseek(fd, -sizeof(i), SEEK_CUR);
Přesněji, jedná se o:
-sizeof(i)
Jak všichni ví (doufám), tak operátor sizeof()
vrací výsledek typu size_t
(vždy celočíselný typ bez znaménka). Ovšem, pokud uděláte operaci (unární -) bezznaménkový typ
, tak výsledkem není znaménkový typ, ale opět size_t
, protože unární minus nemění datový typ (pouze se provede příslušná bitová konverze).
Funkce fseek()
tam ovšem očekává typ long.
Problém nastane na 16b (případně jiných architekturách), kde sizeof(int) < sizeof(long)
. Proběhne znaménkové rozšíření (z 2 na 4B) a výsledkem bude 65534 (-2 bezznaménkově), tudíž se posuneme úplně jinam, než jsme chtěli... Na 32b architekturách tento problém být nemusí (můžete si zkusit spočítat, případně mrkněte do [1]).
Správné musí být:
-static_cast<long>(sizeof(i))
Asi to bylo trošku složitější, ale určitě jste se přiučili něčemu novému.
Poznámka: Pokud by mně někdo napsal, že potenciální chyba se nachází v tom, že se netestují návratové hodnoty funkcí fread()
, fwrite()
a fseek()
, tak bych mu to asi uznal, protože testování návratových hodnot by mělo být samozřejmostí, ale v tomto příkladu šlo o něco jiného.
Zdroj: [1] Miroslav Virius: Pasti a propasti jazyka C++, 2. aktualizované a rozšířené vydání, Computer Press, 2005, ISBN 80-251-0509-1 (příklad byl ale mnou mírně upraven)
Zveřejněno řešení #7
Zveřejnil jsem řešení sedmého úkolu včetně zdroje. Komentáře jsou, jako vždy, vítány.