KURS jazyka C - 8.část

Celou náplň tohoto pokračování kursu tvoří popis řešení zadané úlohy od začátku až po funkční program. Řešení vychází převážně z dosud probíraných znalostí s minimem uvedených novinek.

Co jsou to "NERVY"?

Je to netypická početní úloha uvedená v novém časopise "Křížovkářský TV magazín" vyšlém v prosinci 1995. Upozornila mě na ni moje spolupracovnice z práce Jiřinka, která ráda luští křížovky.

Po prozkoumání pravidel jsem došel k závěru, že se nejedná o žádnou hádanku či hlavolam, ale o pracnou početní dřinu se zjišťováním výsledku. Vůbec si nemyslím, že by bylo možné odvodit si nějaké rovnice, či jiné urychlující matematické řešení.

Zato se mi úloha zdá velmi vhodná ke strojovému řešení, vlastně bych řekl, že přímo křičí po naprogramování. Když jsem si uvědomil příležitost prakticky se pocvičit v Céčku a možná získat i funkční program na malé ATARI, pustil jsem se do toho.

Pravidla hry NERVY

Jako výchozí data pro výpočet jsou zadané dvě tabulky hodnot. První představuje matici čísel 10*10, obsahující hodnoty v rozsahu 50 až 150. Každá položka je graficky ztvárněna jako měšec s korunami. Druhá tabulka obsahuje 200 desetimístných čísel a 200 devítimístných. Tyto se nazývají rodná čísla.

K výpočtu každý použije své vlastní rodné číslo (RČ) nebo členů své rodiny. U devítimístných čísel se doplní jako desátá číslice nula, která se může umístit na začátek, na sedmou pozici, nebo na konec.

V matici měšců se vybírají hodnoty tak, že umístění číslice v čísle uvažováno od levého okraje odpovídá řádek podle číslování shora dolů, vlastní hodnota číslice udává sloupec, čili polohu měšce na řádku. Tímto způsobem je určeno celkem deset měšců, jejichž obsah se prostě sečte.

Jako druhé RČ pro výpočet se vybere z tabulky připravených doplňkových podle jednoduchého pravidla kdo má vlastní číslo devítimístné, vybírá z desetimístných a naopak. Číslo je třeba vybrat tak, aby byl součet získaných měšců co největší, ale nesmí se vzít stejný měšec, jako pro vlastní RČ. To je trochu matoucí, proto vyjádřím podmínku matematicky. Dvojice rodných čísel - vlastní a doplňkové z tabulky - nesmí mít na stejné pozici stejnou číslici.

Možná výhra (po vylosování) je dána součtem měšců ze svého RČ a doplňkového. Podle hodnot uvedených v měšcích může být nejméně 1000 Kč a nejvíce 3000 Kč. Nic moc za 600 matematických operací.

Příprava řešení úlohy.

Vstupní data jsou uložena ve dvou zvláštních souborech. To umožňuje nezávisle aktualizovat obsah měšců nebo RČ. Jako potřebné funkce programu jsem uvažoval mimo vlastního výpočtu možnost vytvořit oba datové soubory a editovat jejich obsah.

Rodná čísla je vhodné uložit jako řetězce, protože operace probíhají s jednotlivými ciframi, mimo-to desetimístné číslo je trochu velké pro zpracování, alespoň pro malý počítač.

Vlastní částky v měšcích sice dávají malou výhru, ale jdou v pohodě zpracovat jako integer.

Pro ukládání a čtení dat jsem vyzkoušel formátované čtení a zápis funkcemi fprinf() a fscanf(). Liší se od zatím známé funkce tisku na obrazovku printf() tím, že před formátovací řetězec je přidán ukazatel na soubor oddělený od řetězce čárkou. Hodnota ukazatele se získá otevřením souboru fopen() či copen().

Funkce mají zajímavou vlastnost, že číslo převádí do souboru na řetězec. Dále u řetězce neuloží jeho koncový znak, takže data se "nalepí na sebe" a funkce čtení se v nich pak ani nevyzná. Proto je vhodné za každým uloženým řetězcem nebo číslem poslat ještě znak konce řádku. V učebnici není na toto výrazně upozorněno, ale praktický příklad v ní uvedené obsahoval.

Celý trik vypadá takto:
fprintf(pt,"©sĽn",string);

Zpětné načtení:
fscanf(pt,"©s",string);

Data jsou pak v souborech uložena jako řetězce ukončované RETURN, čili každé číslo je samostatný odstavec. Proto odpadla nutnost vymýšlet nějaký pořizovač dat na RČ, protože je lepší použít textový editor v režimu ASCII. To znamená u Čapka nepsat do textu žádné řídící kódy, u TEXTWRITERu provést export jako ASCII.

Ono opsat 400 desetimístných čísel je práce na 2.5 až 3 hodiny, proto je vhodné provádět po kratších časových úsecích uládání na nějakou disketu. Taky je na místě možnost editace překlepů a vůbec chyb, prostě to, co umí i nejjednodušší textové editory.

V programu jsem ponechal pořizovač dat matice, protože opsat 100 čísel na jeden zátah a bez chyb není taková hrůza.

Popis 16-ti bitové verze

Program jsem začal vytvářet na ATARI MEGA STE protože je to pohodlnější práce, to vám nebudu nic nalhávat. Originální výpis je v textu NERVYST.C, u něhož byl proveden při přenosu převod odlišných kódů konce řádku a tabulátoru. Po zpětné konverzi by měl být opět schopný kompilace na ST nebo i PC. Prohlédnout si jej můžete Hypertextem, kde bude trošičku zlobit čeština Kamenických, nebo lépe TEXTWRITEREM, který má kromě Kameníků i 80 znaků na řádek.

Na začátku jsou provedeny deklarace globálních proměnných, které jsou využívány různými funkcemi. Jsou to dvojrozměrná pole na datové proměnné a pointery na soubory. Tyto jsou připraveny dva, jeden na soubor MATICE.DAT s měšci, druhý na CISLA.DAT s rodnými čísly. Protože soubory uzavírám ihned po přečtení (zápisu), stačil by vlastně jen jeden.

Globální jsou také definované textové konstanty sloužící jako tištěné zprávy. Původní větší množství bylo zmenšeno na pouhé dvě.

Dále následují vlastní funkce programu poskládané opačně. Hlavní funkce main() je tedy až na konci. Kompilátor (preprocesor) je totiž jednoprůchodový a proto je dobré, aby v okamžiku kdy narazí na volání nějaké funkce, již z předchozího textu znal její definici, a tak mohl zkontrolovat správnost předávaných a přebíraných parametrů.

První a nejkratší funkce pauza() čeká prostě na stisk mezerníku. Před vlastní kontrolou stisknuté klávesy proběhne vyčištění zásobníku klávesnice, který může na ST být značně protivný nežádoucím obsahem.

Použita je funkce kbhit() a getch(). První zjišťuje, jestli je možné převzít nějaký znak, druhá jej převezme. Na rozdíl od již známé getchar() nepotřebuje nežádoucí potvrzení stiskem RETURN. Celý proces je uzavřen do nekonečné smyčky while(1), u níž výraz 1 znamená nepřetržitě splněnou podmínku. K opuštění funkce násilným způsobem poslouží říkaz break.

Tento způsob čtení z klávesnice je využíván i v jiných funkcích.

Vlastní funkce main() je také velmi krátká. Slouží vlastně jen pro spuštění programu a pro poslední potvrzení před jeho opuštěním.

Funkce edit() provede na svém začátku načtení dat ze souborů MATICE.DAT a CISLA.DAT, v případě nějakého problému je vypsáno varovné hlášení. Neúspěch nezpůsobí ukončení programu. Pokud příčinou byla chybějící disketa v mechanice, je možné vyvolat nové čtení návratem do main() (volba Q) a opětovným voláním edit(). Povšiměte si způsobu předávání zprávy o stavu čtení funkci edit() z volaných prováděcích funkcí. Tyto jsou definovány jako funkce vracející integer, vlastní hodnota je předána příkazem return.

Dále následuje výběr z menu koncipovaného hodně primitivně. Je to z důvodu rychlého vytvoření programu, který je vlastně jen pomocnou utilitou, a také pro možnost přenosu programu na jiné systémy.

V nabídce jsou 4 prováděcí možnosti a dále návratu do main(). Nejdůležitější je samozřejmě první volba k výpočtu výsledku. Druhá volba slouží k vytvoření matice měšců, přičemž způsobí přepsání již existujícího souboru. Samozřejmě před touto destruktivní akcí vydá varovné hlášení. (Můžete hádat,co se stalo s mojí opsanou maticí v době, kdy varování s možností návratu nebylo v programu ještě zapracováno.)

Třetí nabídka nebude chtít číst z klávesnice 400 rodných čísel, nýbrž vypíše zprávu, aby si uživatel posloužil textovým editorem.

Čvtrtá volba vypíše obsah měšců na obrazovku. Nečekejte ale, že bude graficky měšce vykreslovat, jako originální pramen. V textu naleznete příslušnou volanou funkci pod názvem oprav(), který pochází z doby, kdy jsem si myslel, že budu muset nějakou možnost editace dat do programu zapracovat.

Kontrolní výpis RČ zde chybí. Vzhledem k počtu čísel je to asi zbytečné. Malá kontrola se provádí hned po jejich načtení vypsáním poslední položky pro srovnání s originálním pramenem.

Vstupní data jsou organizována ve vícerozměrných polích. Matice hodnot měšců je dvojrozměrná integer, pole rodných čísel je třírozměrné pole charů. První rozměr udává 200 čísel, druhý rozměr rozlišuje část devítimístných čísel od desetimístných, čili má hodnotu 2, třetí rozměr uskladňuje vlastní délku RČ a má tedy rozměr 11. To proto, že se jedná o maximálně deseti prvkové řetězce, k nimž je potřeba přidat jako jedenáctý prvek nulový bajt. (Jím se při řetězcových operacích poznává konec řetězce) Z hlediska našeho chápání jsou čísla (převedená na řetězce) uložena ve dvojrozměrném poli, z něhož si vybíráme právě žádané položky. Třetí délkový index je trvale nastaven na nulu, čímž ukazuje na první (nultý) prvek řetězce. Jeho konec si řetězcové funkce již sami poznají podle nulového bajtu. Ten je v textu reprezentován výrazem "/0".

K vlastnímu výběru prvků z matic nemám mnoho co říci. Prohlédnutím výpisu zjistíte, že je použita kombinace adresového operátoru B a indexů v hranatých závorkách. Rád bych problematiku jasně vysvětlil, ale sám to zatím nechápu. Můj funkční zápis není výsledkem studia učebnice, ale nesčetných pokusů.

Zmíním se alespoň o práci s vlastními indexy. Je dobré si uvědomit, který je který a matice plnit stejnou konvencí, jako je číst. Jinak je možné dospět při výpočtech k pozoruhodným výsledkům.

Poté, co jsem se tvrdě do indexů zamotal, přepsal jsem zaužívané názvy i a y na matematické x a y. X představuje vodorovnou osu, čili index sloupce, Y svislou osu, nebo-li index řádku. Takové sjednocení výrazně ulehčí práci. Je také dobré při plnění nebo čtení matice po řádcích použít X-sovou proměnnou do vnitřní smyčky for(), která je vložená do smyčky se souřadnicí Y.

Při zcela bezmyšlenkovitém postupu napsání nejprve x, a do druhé, tedy vnořené smyčky y, jak se stalo mě, dojde k nesmyslnému zpřeházení hodnot v matici. Navenek se to neprojeví, protože nedojde k opuštění oblasti paměti vyhražené pro matici.

Hodně by se toho dalo změnit na funkci kalk(), v níž dochází k vlastnímu výpočtu. Podle zadaných podmínek jde z každého devítimístného čísla vytvořit tři desetimístná, a to jak z vlastních, nebo doplňkových. Tuto skutečnost řeším v programu dvojím zápisem, myslím dosti křečovitým. Proto berte prosím uvedený příklad jako užití příkazů jazyka C, nikoliv správné algoritmizace problému.

Popis 8-mi bitové verze

Převod na malý počítač mě stál několik dní další dřiny, ale to hlavně pro moje dosavadní malé zkušenosti s tímto jazykem. Ostatně, myslíte si někdy také, že Céčko vymyslel nějaký zatrpklý programátor jako trest na své kolegy ?

Rozhodně můžu říci, že prováděcí část funguje naprosto beze změny. Hlavně díky kompilátoru XCC, který umožňuje i vícerozměrné pole. Tyto by šlo zase přepočítat na jednorozměrné, ale to by bylo děsné!

V zásadě jsem musel některé funkce nahradit a některé přejmenovat. Pointery na soubory se změnily na obyčejné integer proměnné. U definicí funkcí bylo nutné vymazat slovíčko void, které znamená prázdno, čili bez parametrů. U funkcí vracejících číslo kopilátor výraz int uznával, ale nevyžadoval.

Chybějící funkce kbhit(), getch() a scanf() jsem nahradil čtením řetězce gets(). Všechny vstupy z menu a návrat z funkce pauza() se musí sice nyní potvrzovat stiskem RETURN, ale mám alespoň jistotu, že buffer klávesnice je vyprázdněný úplně dočista.

Zrušil jsem sled příkazů pro zjišťování délky souboru. Myslím, že něco takového není na malém systému možné, protože nejde nastavit ukazovátko na konec souboru (nebo kamkoliv jinam) bez nutnosti postupně načítat všechny předchozí sektory. Adresářová tabulka (FAT) vyšších systémů bude asi hodně dokonalejší.

Formátované čtení řetězce ze souboru jsem nahradil narychlo spíchnutou funkcí int cgets(pt,s). Předávají se jí dva parametry. Prvním je ukazatel na soubor, prostě číslo vrácené funkcí copen(). Druhým je pointer na adresu pole charů, (řetězce) které funkce naplní přečteným řetězcem. Vlastní čtení se provádí po jednotlivých bajtech, je tedy hrozně pomalé. Nic lepšího ale nemám momentálně po ruce.

Konec čteného řetězce se testuje přečtením znaku s nulovou ASCII hodnotou, místo něj je ale nutné uložit jako poslední prvek znak "/n", čili EOL. To byla jedna z mých výrazných chyb při tvorbě této funkce. Po úspěšném načtení řetězce vrací funkce pomocí return jedničku, při chybě nulu.

Druhý kopanec jsem dělal při volání funkce. Z pomatení mysli jsem připsal před pointer na soubor adresový operátor, tedy jsem psal cgets(Bmat,s); místo cgets(mat,s);, jak mělo být správně. Předával jsem tak adresu ukazatele a ne jeho hodnotu. Kupodivu matice měšců šla z neznámých důvodů tak načíst, zatímco soubor RČ správně odmítal spolupráci. Inu, to jsou zvláštní věci.

Po úpravě textu zůstal ještě jeden problém, a to v jeho nadměrné délce. Nejsem už naštěstí tak hloupý, abych si myslel jako dřív, že delší program nejde zkompilovat. Prostě se text musí rozdělit na více částí, samostatně zkompilovat a poté slinkovat dohromady.

Problémem zůstaly globální proměnné. Při nepřítomnosti jejich deklarace v některých modulech kompilátor odmítal spolupráci, při jejich uvedení ve všech se děly úžasné věci. Program záludně čtená data zapisoval do oblastí vlastního DOSu, takže tento se v lepším případě úplně zhroutil, nebo v horším vykazoval necivilizované chování. Z adresáře vypisoval náhodně jen některé položky, u souborů hlásil chybu 164, při pokusu zápisu matice měšců totálně zlikvidoval celý disk s rozepsanou nezálohovanou 8-mi bitovou verzí programu NERVY. (příznačný název)

Jako poslední možnost zůstala učebnice a ejhle! Ke jménům proměnných deklarovaných ve více modulech se kromě prvního výskytu připíše slovíčko extern. Chvilka napětí a na monitoru se objevují výsledky podobné těm z 16-ti bitové verze. Jen co jsem přestal řvát:"Už to chodí!!", věnoval jsem se srovnání s MEGOU STE na 16 MHz. Mrňousek příjemně překvapil. Výsledky sice z MEGy vypadnou hned po stisku RETURN, ale tisk alespoň prvního výpočtu na malém ATARI drží krok.

Rozdělení programu do jednotlivých částí, které mi nejprve řádně zkomplikovalo život, jsem ocenil při dodatečných úpravách tisků na obrazovku. Stačilo totiž kompilovat jen právě upravovanou část programu, čímž se něco uspořilo.

Vlastní výsledek programu

Tisk konečného výsledku není dotažený až do konce. Program neprovádí součet výtěžku ze svého RČ a z doplňkového. V případě zadání svého devítimístného čísla se vytisknou všechny tři možné výpočty a uživatel si sám musí vybrat ten nejvýhodnější.

Užitečnost programu

V tomto případě se jedná o poměrně diskutabilní námět. Jako příklad do kursu programování se hodí dobře, protože provádí jednoduché operace. S praktickým využitím to bude horší.

Odhaduji, že šikovný programátor by napsal funkční program možná i za hodinu. (nikoliv 2 týdny) Opsání dat se dá zvládnout za 2.5 hodiny. Vzhledem k velmi nejisté výhře průměrně 2100 korun jde o dost velkou dřinu, alespoň ve srovnání s jinými soutěžemi. Ovšem ve srovnání s blázny, kteří úlohu řeší ručně, musí jít nutně o veliké plus.

K zadání bylo poznamenáno, že se úloha bude opakovat s jinými vstupními daty. Pokud budou mít stejný tvar, bude možné použít shodný program a tím zůstane jen práce s jejich opisováním.

Poznámka: Při přenosu datových souborů z ST na 800XE se provedla pouze známá konverze CR+LF (13 a 10) na RETURN (155).