Kurs programování "C" část 5.

©=37,/=92,}=123,(=91,)=93,"=39

i definice znaku procenta, obráceného lomítka, levé složené závorky, levé a pravé lomené závorky, apostrofu.

Hned v úvodu musím upozornit na závažnou chybu ve vysvětlení činnosti příkazu "while" v minulém pokračování.

Závislost průběhu cyklu na podmínce je přesně opačná, než jsem ji popsal. Opakování příkazů uvedených za WHILE se provádí, dokud platí řídící podmínka. Program se "zacyklí" pokud platit nepřestane.

Nevím, jak jsem mohl napsat špatné vysvětlení, přestože jsem si nejdříve v několika příručkách přečetl, že právě tím se liší "C" od PASCALu, pak jsem zkoušel kontrolní prográmky, no a pak jsem to stejně zvrtal. Mohl bych sice tvrdit, že jsem to udělal záměrně špatně, aby si na to programátoři přišli sami, ale to bych kecal.

Opakování a prohlubování poznatků I.

Nejprve shrnu příkazy pro násilné změny průběhu programu:

"break" - opouští cyklus, jen ten, v němž byl použitý.

"continue" - skáče na konec cyklu, v němž je použitý, způsobí tedy jeho nový průběh (pokud nejsou jiné podmínky pro jeho ukončení) bez vykonání následujících příkazů.

"return" - ukončuje funkci, ve které je použitý a může vracet nějakou hodnotu. Její typ je závislý na typu funkce.

"exit" - ukončí celý program. V našem "C" asi vůbec není a podle učebnice se používá málo.

"goto" - je to tady, i Céčko má příkaz skoku. Doposud jsem to tajil, protože je to při strukturovaném programování fuj. Skáče na návěští, které se nemusí předem definovat, ale jen v rámci jedné funkce. V seznamu příkazů pro naše C chybí.

Výpis programu pro zopakování dřívějších poznatků obsahuje 3 příklady z mé nové učebnice jazyka "C", které se mi zalíbily. Program má číst zadávané znaky z klávesnice, vypisovat je na obrazovku, mezeru a tabulátor nahradit "ohrádkou" a ukončit činnost po stisku písmene Q.

První řešení je odstrašující. Příkaz continue je umístěný do příkazu switch, což může znesnadnit pochopení programu. Způsobuje totiž skok na konec smyčky while, protože switch není cyklem. Také by zde bylo lepší použití do while, aby se první čtení vstupu nemuselo provádět zvlášť před cyklem.

Druhé řešení má zapracované testování ukončovacího znaku hned v podmínce cyklu. Třetí řešení je zkrácení druhého, ale po úspoře již není program tak přehledný.

poznámka: v našem systému zadávání dat z klávesnice není zřejmě možné předat holou jednu mezeru. K záměně dojde až po napsání mezery následované nějakým jiným znakem, který však již není přečten správně. Jak jsme si již všimli dříve, funkce getchar() bere správně jen jeden znak následovaný stiskem RETURN, který se chápe jako druhý zadaný znak. Znak tabulátoru se napíše stiskem kláves ESC a TAB. Použitá proměnná je typu int, ale úplně shodně program pracuje i s typem char. Pokud je jen jednoznaková, rozdíly se nějak zvlášť neprojevují. Nedávno mě to hrozně potrápilo na ST, než mi program začal chodit. (s pomocí Jirky)

Řešení úlohy výpočtu faktoriálu I.

Jako obvykle jsem se snažil vyřešit zadanou úlohu z rubriky "Hlavolamy" pomocí jazyka "C". Vzdal jsem původní úmysl vytvořit algoritmicky shodný program s Honzovým originálem, protože obsahuje funkci logaritmu, která v našem celočíselném Céčku chybí. Zkusil jsem sice i matematicku knihovnu, která umožňuje výpočty s desetinnou tečkou, ale zavrhl jsem její styl práce. Myslím si, že je natolik odlišný od používání funkcí "C" na velkých počítačích, že snad nemá smysl se jej učit.

Připomínám, že naše Céčko nepovažuji za prostředek pro tvorbu prima programů, ale přesto nemůže být naškodu se v něm pocvičit. Základní příkazy jsou totiž nejen shodné s velkými Céčky, ale podobné jsou i těm v jiných strukturovaných jazyků, jako je PASCAL, nějaká databáse a pro nás TURBOBASIC. I v něm je možné psát programy bez jediné instrukce GOTO (!) a mít je tak nejen programátorsky "čisté", ale i naprosto nezávislé na číslování řádků.

Protože píšu tento kurs spíše pro programování než jako výuku jazyka, rozeberu úlohu s faktoriálem nejprve principielně. To pro ty, kteří stejně jako já moc nepochopili Honzovy záhadné výpočetní postupy. Zejména mě zaráželo použití algoritmu pro určení délky čísla. Asi mi středoškolská matematika vypadla úplně z hlavy, pamatuji si jen, že sčítáním algoritmů čísel se provádí jejich násobení ale jinak nic. Později mi to Honza vysvětlil a nic na tom vlastně není. Pomocí triku s logaritmy odhadl i vhodnou velikost dimenzování paměťového prostoru na nejvyšší možný výsledek. Funguje to ovšem u logaritmu desítkového, nikoliv přirozeného.

Honzou zadaná úloha předepisuje výpočet faktoriálu čísla s úplnou přesností - na všechny platné číslice. (Žádné zaokrouhlování.) První překážkou je používaná přesnost daného počítače. Na nějakých sto platných čísel asi nebude běžně počítat žádný. Jde ovšem o přesnost již naprogramovaných matematických rutin zabudovaných v operačním systému. Počítač v principu nemá přesnost omezenou nijak - vždyť jeho procesor umí sečíst nebo odečíst dva bajty, popřípadě rotováním bitů násobit a dělit dvěma.

Pro vyřešení úlohy je nutné napsat vlastní násobicí rutinu. Je lhostejné, jaký programovací prostředek se použije protože je potřeba skutečně málo funkcí.

Algoritmus násobení víceciferného čísla znají jistě všichni čtenáři ze základní školy. Jednoduše se násobí jednotlivé číslice od nejmenšího řádu po nejvyšší. Při každém úkonu se výsledek rozdělí na přenos do vyššího řádu a zůstatek, který se dosadí na místo právě násobené číslice. Zůstatkem je číslice nejnižšího řádu, všechno ostatní je přenos do vyššího řádu, který se před dalším násobením přičte k následující číslici v pořadí.

Při ručním násobení na papíře se u víceciferného násobitele provádí postupné násobení jeho jednotlivými číslicemi a vzniklé výsledky se nakonec sečtou. Protože zadaná úloha připouští největší zadanou hodnotu 100, nebude při použití počítače takový rozklad nutný, protože přesnost matematických rutin v OS postačí. Počítač si hravě zapamatuje i dvojciferné přenosy do vyšších řádů.

Přesnost výpočtu, čili počet platných číslic je teoreticky neomezená. Praktickým omezením je velikost operační paměti, který silná povaha může obejít používáním paměti vnější, například na disketě. Totiž na kazetě by virtuální pamět asi vytvořit nešlo. Jsem rád, že si Honza na nás nevymyslel počítání s takovými maxičísli, protože by to byla děsná dřina. Uvažujme také, že při větších hodnotách násobitelů by se musely i tyto rozkládat jako při výpočtech ručních z důvodu limitované přesnosti matematických rutin. (Brr !)

Všechno co potřebujeme pro převedení algoritmu do programovacího jazyka je vědět, co je to faktoriál - posloupnost činitelů, postupně je násobit mezi sebou rozloženým způsobem po jednotlivých číslicích, při tom správně rozdělovat mezivýsledky na přenos a zbytek. To při násobení z hlavy děláme prakticky bez počítání oddělením poslední číslice. Programově se mezivýsledek rozdělí jednoduše pomocí celočíselného dělení deseti a funkcí dělení modulo, která dává zbytek operace. Ten samozřejmě při podělení deseti je rovný nejnižší platné číslici.

Ve zdrojovém textu řešení úlohy je použito několik novinek. Pro vstup čísla poslouží funkce gets(), která má povinný argument jméno řetězcové proměnné, do níž načte text zadaný z klávesnice. Je skutečně dobré, jak se píše v příručce, proměnnou nepřeplnit. Jinak se programu zbortí za současného vydávání nepříjemných zvuků. (tento průvodní efekt nebývá častý)

Obsah řetězce lze převést na číslo poněkud zamlženě popsanou funkcí val() pokud je vzhledem k obsahu převod možný. Funguje ale asi jen do trojciferného čísla, což je divné, ale v naší úloze postačující.

Poslední novou funkcí je clear(), opět nejasně popsaná, zřejmě ale skutečně nuluje řetězec, jinak by uvedený chybný algoritmus musel vypočítávat nesprávné výsledky. Matematická funkce dělení modulo má jako operátor znak procenta, ale to není nic zváštního.

Pro uložení výsledku je stejně jako v řešení od Honzy použitý znakový řetězec. Je mnohem úspornější než číselné pole, protože na každou číslici je třeba jen jeden bajt. Proto by měl být i rychlejší. A nakonec je snad i práce s ním jednodušší, pokud programátor ví jak na to . V učebnicích jazyka se ve spojitosti s používáním řetězců hovoří o ukazatelích, speciálních funkcích pro plnění řetězců a jejich porovnávání a dalších hrůzách, které jeden může zkoušet dva dny a ono stejně nic nechodí. Naštěstí jsem se po poradě s kolegou Jirkou dozvěděl jednoduchou pravdu o nakládání s chary. Například "string" může být jméno proměnné typu char deklarované třeba na délku 100 znaků a pod ním se s ní pracuje skutečně jako s řetězcem. Ale výraz string(n) adresuje přímo prvek řetězce "n" a nakládá se s ním jako s jednobajtovým číslem, popřípadě znakovou konstantou.

Obsahuje-li například proměnná txt posloupnost znaků 123, potom:

  printf("©s",txt); vytiskne  123
  printf("©c",txt(1)); vytiskne  2
  printf("©d",txt(1)); vytiskne  50

Index 1 ukazuje na druhý prvek pole kterým je znak 2 vytištěný jako znaková konstanta c, nebo jako číslo 50, které je ASCII vyjádřením tohoto znaku a je to skutečný obsah daného bajtu řetězce.

Poznámka:
Zápis printf("©s",txt(1)); je chybný, pokud jej použitý překladač vůbec zkompiluje, způsobí tisk nesmyslů

Celý výpočet faktoriálu je v programu oddělen do samostatné funkce faktorial(), hlavní main() slouží jen pro možnost opětného vyvolání výpočtu nebo ukončení celého programu. Nijak jsem se s ní nepáral, je sešitá narychlo a nic zajímavého na ní není.

Vlastní výpočet začíná vstupem čísla pro výpočet faktoriálu. Je řešen cyklem do while, který je vhodnější než while. Pokud zadané číslo neleží v intervalu 1 - 100 vrací se zpět na zadání čísla. Pro zadávání se použije ta sama proměnná jako pro pozdější výpočet, není důvod, aby tomu bylo jinak. Číslo je převedeno do proměnné typu int a řetězec je připraven pro výpočet. Nejprve je celý vynulován a potom je na nultou pozici nachystána jednička jako výsledek 1! a základ pro další násobení. Do proměnné i je zadána také jednička jako aktuální délka výsledku a současně slouží jako ukazatel na první volnou pozici řetězce.

Používání proměnné i mě pěkně vypeklo. Jednodušší by byl algoritmus, který by postupně projížděl všechny pozice řetězce, přečetl bajt, připočítal přenos, rozdělil výsledek, uložil bajt a tak dál. Jenže tak by se zbytečně počítalo i s neobsazenými byty a proto používám hlídání délky výsledku pro každý průběh.

Průběhy se samozřejmě opakují až do hodnoty zadaného čísla včetně, přičemž se násobitel zvětšuje o jedničku.

Vlastní násobení je rozděleno do dvou částí. V první se provádí násobení předchozího výsledku číslo po čísle beze změny proměnné i, v druhé se případný vzniklý přenos rozpočítá na nové nejvyšší pozice výsledku za současné inkrementace i. Protože přenos může být logicky nejvýše dvoumístný, proto závěrečný cyklus proběhne nejvíce dvakrát.

Zdá se mi to být jednoduché, ale někde mám chybu. Pro vyšší hodnoty n! algoritmus počítal chybné výsledky, na což jsem přišel srovnáním s Honzovým programem. Po "umělém" zvětšení délky výsledku, což samo o sobě nemůže výpočet změnit jsou-li byty řetězce skutečně vynulované, počítá výsledky dobře. Zato do velikosti 17! přidává jednu až dvě nuly do nejvyšších řádů, což je vada spíše estetická.

Protože po posledním výpočtu se přidá jedna pozice navíc, je po skončení cyklu odečtena. Předtím je ale podmínkou testováno, jestli k přidání skutečně došlo, jinak při zadání 1! by vyšel nulový výsledek.

Po dvou dnech zkoumání programu, kdy jsem již nebyl schopný číst řádky a jen si horečně opakoval: "To musí chodit !" jsem další práci vzdal. Možná někdo z vás na chybu přijde a pošle upravené řešení, třeba v Turbobasicu, to je už jedno. Nemám na mysli prosté odmazání nejvyšších neplatných nul, ale zabránít vůbec jejich vzniku.

Tisk výsledku je uskutečněn číslo po čísle. Bylo by možné tisknout přímo řetězec, ale až po úpravě. Nyní obsahuje čísla v kódu BCD jak píše Honza, které se musí převést na ASCII hodnoty. Jinak by se tiskly nesmysly až do prvního výskytu čísla nula. Protože v této tabulce jsou číselné znaky kódované ve vzestupné řadě, provede se konverze jednoduše přičítáním kódu číslice nula. Ani nemusíme znát její kód zpaměti nebo pracně hledat tabulku, (mám doma chaos) nýbrž použijeme znakovou konstantu, asi takto:

  v(i) = v(i) + "0" 

Pak by se ještě musel výsledek v řetězci zrcadlově otočit, jinak by byl vlastně psaný zprava doleva.

Ve svém programu jsem závěrečnou úpravu vynechal, protože v zadání úlohy byl jen tisk výsledku na monitor a s vlastním řešením faktoriálu nesouvisí. Chystám ale na příště jako příklad práce s řetězci pěknou úložku z příručky uváděné v rubrice RECENZE.

Zmíněnou možnost odečítání znakové konstanty nula bych použil v případě požadavku vstupu čísla z klávesnice delšího než tři místa. Přijal bych je jako řetězec opět funkcí gets() a provedl konverzi jednotlivých číslic. Jinou možnost v příručce nevidím.

Při odlaďování mi dělal program zásadní chybu při vnoření funkcí gets() a val() do sebe. Podle pouček to má libovolně jít, ale nešlo to.

Další moje chyby byly nezávislé na použitém jazyce - pokusy o práci s nedeklarovanými proměnnými. Ze začátku jsem také psal algoritmus počítající úplně něco jiného, to mi připomělo běžnou poučku z programátorských příruček : "Dobře si promyslet řešení úlohy". Samozřejmě ji při čtení jinak považuji za zbytečnou a přehlížím ji.

-ZB-