Zajímavé úlohy pro programátory v C a C++ #7

Od Petr Zemek, 2009-09-06

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(), nebo int 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é jako char *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).
  • Podmínka if (!argv[1]) je ekvivalentní podmínce if (argc < 2), protože standard nám zaručuje, že argv[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 verzi if (argc != 2).
  • Podmínka if (!fd) je ekvivaletní podmínce if (fd != 0) (to už jsem tady probíral).
  • Zdánlivě nadbytečný fseek(fd, 0, SEEK_CUR);. Podle normy nesmí za fwrite() ihned následovat fread() či naopak, niž by byl volán fseek(). Toto volání je zde tedy nezbytné (upraví stav proudu po fwrite() 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)

Obsah tohoto pole je soukromý a nebude veřejně zobrazen.

Filtrované HTML (využíváno)

  • Povolené HTML značky: <a href hreflang> <em> <strong> <cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <table>
  • Zvýraznění syntaxe kódu lze povolit přes následující značky: <code>, <blockcode>, <bash>, <c>, <cpp>, <haskell>, <html>, <java>, <javascript>, <latex>, <perl>, <php>, <python>, <ruby>, <rust>, <sql>, <text>, <vim>, <xml>, <yaml>.
  • Řádky a odstavce se zalomí automaticky.
  • Webové a e-mailové adresy jsou automaticky převedeny na odkazy.
CAPTCHA
2 + 4 =
Vyřešte tento jednoduchý matematický příklad a vložte výsledek. Např. pro 1+3 vložte 4.
Nějak se mi tady rozmohl spam, takže poprosím o ověření.