Kurz jazyka C - 5. dílPavel Čížek
Vážení přátelé, je to tady. Začínáme se skutečným programováním v jazyce C -
pomalu, opatrně, aby všichni stačili; ale začínáme. Dovolte mi přivítat všechny,
kteří se rozhodli poprat se s Céčkem. Jak jsem již dříve předeslal, mění se styl výkladu. V předchozích částech
jsme si "suše" vyložili základy programovacího jazyka C - teoreticky. Zvláště
méně zkušeným se to může zdát příliš komplikované. Ale nebojte se, už začíná
praktická část. Slíbil jsem, že se pustíme do nějakého malého projektu, který
poroste s našimi zkušenostmi (nebo naopak). Situace je následující: budeme
vytvářet program - kalkulačku - který bude schopen především vyhodnocovat v
podstatě jakékoliv výrazy. Nejprve bude zpracovávat vstup z příkazové řádky, až
se to naučí, může dostat vzhled opravdové kalkulačky, a také něco navíc. Naučíme
náš program vytvářet tabulky funkcí a kreslit grafy a pokud budete chtít, tak je
i budeme umět ukládat na disk v IFF formátu. Pokud by měl někdo připomínky,
návrhy apod., může je všem sdělit prostřednictvím redakce.
To vše je budoucnost, doufám, že světlá. Pokud nebude mít nikdo nic proti, budou
se kompletní zdrojové texty k našemu kurzu objevovat na Coverdiscích Amiga
Review (stejně by se všechno nevešlo na stránky časopisu). A bude-li místo,
budou se objevovat obrázky prezentující činnost a schopnosti programu ty budou
zpočátku sice malé, ale porostou.
Dovolte mi ještě pár slov v úvodu našeho programování v Céčku: Céčko je znám
jako jazyk velice "volný"; teoreticky můžete vzít program v Céčku a zmáčknout ho
do jediné řádky bez ohledu na jeho délku. Program se může (v nevhodném zápisu)
stát velmi těžko čitelným (omluvte kvalitu výpisů v časopise, ale možnosti jsou
omezené zdrojové soubory na Cover disku by to měly napravit). Pokud má ovšem
spolupracovat několik lidí na tomtéž kódu, je třeba se domluvit na několika
základních pravidlech při psaní, aby si mohli dobře rozumět. Přitom není
podstatné, na čem se kdo s kým dohodne, ale musí se daná pravidla dodržovat. Pár pravidel hned na začátku
Při formátování funkce je většinou snaha, aby bylo dobře vidět jméno funkce
a aby všechen kód funkce byl odsazen. Původní styl dle Kernighan & Ritchie
vypadá takto:
void Nesmysl(a, b)
int a; char b;
{
... odsazený kód funkce ...
}
My budeme používat novější styl podle ANSI normy jazyka C, v němž jsou deklarace
typů proměnných přímo v hlavičce funkce:
void Nesmysl(int a, char b)
{
... odsazený kód funkce ...
}
Poznamenejme, že někdo používá ANSI zápis s tím, že typ návratové hodnoty je
uveden na samostatném řádku.
Další důležitou věcí je pojmenovávání proměnných v programech. Jména globálních
proměnných mají obvykle velká písmena na počátku každého slova svého jména, jako
např. "DocasnySoubor", "VeIkyNesmysl". Totéž se týká statických proměnných
(obzvláště, jsou-li deklarovány uvnitř funkcí). Původně se používala pro
zčitelnění podtržítka "_", ale pomalu se od toho upouští. Automatické proměnné
(ty, které jsou lokální v jednotlivých funkcích) se vždy píší malými písmeny.
Naopak makra a konstanty se vždy píší velkými písmeny, aby se odlišily od
proměnných. Jména procedur se obvykle píší s velkými počátečními písmeny,
jedná-li se o hlavní/důležité funkce, malými písmeny se píší názvy pomocných
funkcí. A zde je příklad:
char VyslednyRetezec[256];
int NactiData(char * jmeno)
{ int i, j;
static char TempBuf[20];
...
}
Důležitou součástí zdrojového kódu je zápis bloku začíná { a končí }.
Kdykoliv se objeví nový blok, kód v něm obsažený je vždy o několik mezer odsazen
- zpravidla o délku tabelátoru (viz dále). Jedná se především o bloky = těla
podmínek a cyklů. Otevírací závorka je zpravidla na tomtéž řádku jako podmínka
nebo samostatně na řádku následujícím. Uzavírací závorka je samostatně na konci
bloku a je odsazena od levého okraje stejně jako příslušná podmínka / cyklus.
Příkladem může být následující cyklus:
while (! konec)
{
... něco dělej ...
}
Mezery a tabelátory hrají při zápisu zdrojového kódu důležitou roli, neboť mohou
výrazně zlepšit čitelnost. Porovnejte např. tyto dva výrazy:
i=j*(k+4)/-23-(i+j)
i=j * (k + 4) / -23 - (i + j)
Pravidlo je jednoduché - kolem binárních operátorů umisťujeme mezery z obou
stran a mezeru umístíme i před unární operátor. Mezery se obvykle vynechávají za
levou a před pravou závorkou. Další výjimka jsou zápisy polí a struktur:
i = uk->mi_Hodnota + nesmysl[j + 2];
U volání procedur a funkcí se většinou před čárkami mezery nepíší, za nimi ano:
MojeFunkce(a, retezec, 2);.
Jak bylo zmíněno, míra odsazování a tedy i úprava závisí na používaných
tabelátorech. Používají se tabelátory o velikostí 3, 4, 5 či 8 mezer - většinou
to závisí na osobním vkusu. Dříve se často používal tabelátor o velikostí 8
mezer, ale v nových, hodně strukturovaných programech by docházelo k velikému
odsazení a byla by vidět jen malá část kódu. Používat by se nemělo ani odsazení
menší než 3 - činí to strukturu programu nepřehlednou. Začínáme s kalkulačkou
Podívejme se konečně na náš program - jeho první verzi. Dnes je uveden v
plném znění, abychom si mohli ukázat řadu základních věcí. Když se na výpis
podíváme, hned v úvodu spatříme komentář obsahující jméno souboru a krátkou
poznámku; nemusím snad už připomínat, že komentáře jsou uzavřeny dvojicemi znaků
/* a */. Je zvykem (zvláště u rozsáhlejších projektů) na začátku souboru
uvést stručnou informaci o tom, co obsahuje a k čemu slouží (existují pomocné
utilitky, které Vám tuto informaci vypíší např. u všech souborů v daném adresáři
- pak je tímto komentářem usnadněno hledání). Prohlédněme si další řádky, které
nepatří do těla žádné funkce.
Následují řádky začínající příkazem preprocesoru "#include" , který zajistí
vložení příslušného souboru do programu. Jak bylo již několikrát vidět, zde se
jedná o standardní soubory obsahující deklarace funkcí dodávaných ke kompilátoru
(protože Céčko samo o sobě neumí skoro nic); "stdio.h" obsahuje deklarace funkcí
sloužících pro vstup/výstup a "string.h" nám definuje funkce pro práci s
řetězci.
A následují další příkazy pro preprocesor - "#define". Slouží k definici
symbolických výrazů či maker. Preprocesor (jak jistě víte) před kompilací
nahradí všechny výskyty definovaného symbolu tím, co za ním následuje (zde např.
všechny výskyty MAX_DELKA budou v textu nahrazeny číslem 1000). Symbolické
konstanty slouží mimo jiné ke zpřehlednění programu. Umožňují ale také snadné
změny příslušného výrazu - pokud bychom chtěli aby maximální délka (označená
MAX_DELKA) nebyla 1000, ale 500, nemusíme procházet cely program a zkoumat,
která tisícovka se má nahradit číslem 500 - stačí přepsat hodnotu v příkazu
"#define".
Kromě symbolu MAX_DELKA je zde definován i symbol ZNAME_OPERATORY - to souvisí s
tím, co zatím naše kalkulačka bude umět. Zmíním se o tom právě nyní. Program
"Kalkulacka" bude zatím spustitelný pouze z příkazové řádky. Parametrem bude
nějaký číselný výraz obsahující pouze operace sčítání + a odčítání - (např.
5 + 3.14 - 10), čísla jsou bez exponentu. Program tento výraz vyhodnotí a vypíše
výsledek - jednoduché, ale s něčím se začít musí. No, a je vidět, že
ZNAME_OPERATORY je řetězec obsahující jediné povolené operátory: + a -.
MAX_DELKA udává maximální délku zpracovávaného výrazu.
Mezi dalšími definicemi najdete řetězec obsahující jméno našeho programu jeho
verzi a datum. Navíc je zde definována globální proměnná - ukazatel na řetězec -
která ukazuje na zvláštní pole znaků: toto pole obsahuje nulový byte, pak text
"$VER:" a poté název a verzi programu. Jedná se o identifikační řetězec, který
umožňuje zjištění verze programu stačí pak třeba v CLI napsat příkaz "version
Kalkulacka" (viz obrázek). A hle, vypsalo se jméno programu a jeho verze.
Posledním příkazem napsaným v úvodu programu (je hned za #include příkazy) je
typedef. Jak již víte, můžete si s jeho pomocí nadefinovat svoje vlastní typy
dat. My zde definujeme typ pro logickou hodnotu měl by obsahovat hodnoty typu
pravda/nepravda. Zároveň jsou zde definovány symboly pro pravdu (TRUE = 1) a
nepravdu (FALSE = 0). Je to samozřejmě symbolický zápis, který však zlepšuje
čitelnost programu. Je jistě zřejmé, že je ve shodě s Céčkovským pojetím
logických hodnot (nenulová hodnota = pravda, nulová hodnota = nepravda). Tento
typ spolu s mnoha dalšími je definován v systémovém hlavičkovém souboru
<exec/types.h>. Jak to vlastně pracuje?
Podívejme se nyní na výkonnou část programu - skládá se ze dvou částí. První
z nich je funkce ProjdiRetezec(), která slouží k vyhodnocení zadaného výrazu. O
ní si povíme za chvíli. Druhá část je tvořena funkcí main(), která musí být
obsažena v každém Céčkovském programu. Když svůj program spustíte (po
kompilaci), pak po nezbytných počátečních inicializacích (které zařídí kód
připojený během kompilace, nemusíte se o to starat) je zavolána právě funkce
main(); je to jakýsi vstupní bod vašeho programu. Povězme si o ní tedy něco
více.
Deklarace funkce main() má následující tvar:
int main(int argc, char *argv[]).
První parametr je číslo, které udává počet položek v příkazové řádce, jíž byl
program spuštěn (argc = argument count = počet argumentů). V tomto čísle je
obsažen i vlastní název programu. Druhý parametr je pole řetězců. Připomeňme si,
že v Céčku je řetězec pole po sobě jdoucích znaků ukončené nulovým bytem. Pokud
se s řetězci manipuluje stačí předávat pouze adresu prvního znaku řetězce
ostatní následují za ním a konec poznáme. Jestliže tedy "argv" je pole ukazatelů
na char (= znak), lze ho chápat jako pole řetězců (ty musí samozřejmě být
uloženy někde v paměti, o to se zde ale nemusíme starat). Nezapomeňte, že pole
jsou číslována od nuly a jméno pole zároveň reprezentuje adresu prvního prvku!
Pokud tedy bude mít příkazová řádka tvar
Delete soubor1 soubor2 ALL,
pak "argc" bude rovno 4 a pole "argv" bude mít tvar { "Delete", "soubor1,
"soubor2", "ALL" }.
Jistě Vás napadlo, jak to vypadá když se program spouští z Workbenche tam žádná
příkazová řádka neexistuje. Většinu potřebných věcí zařídí opět kód přidaný
kompilátorem. Některé kompilátory (nejen Céčka, ale i Pascalu) při spuštění z
Workbenche zavolají funkci main() a počet parametrů "argc" položí roven nule.
Jiné implementace definují pro spouštění z Workbenche dnou vstupní funkci (např.
wbmain()) - pokud není definována, pak se při spuštění z Workbenche prostě nic
nestane. Pokud Vám vrtá hlavou, k čemu je návratová hodnota u funkce main(), pak
vězte, že je to chybový kód vrácený tomu, kdo program spustil většinou
AmigaDOSu.
Nyní je snad jasné, jak se program dozví o zadaných parametrech. Podívejme se na
vlastní kód funkce main(). Na počátku jsou deklarované reálné proměnná
"vysledek" a pomocná proměnná "i" typu přirozené číslo (význam vyplyne dále).
Dále je zde definováno pole znaků o délce MAX_DELKA. Toto pole bude obsahovat
řetězec reprezentující vyhodnocovaný výraz, tedy posloupnost znaků ukončených
nulovým bytem. Na počátku toto pole obsadíme prázdným řetězcem - "" (tj. jedním
nulovým bytem).
Příkazy následující deklarace proměnných jsou více než jasné. Testujeme hodnotu
argc na nulu (ošetření spouštění z WB) a na hodnotu 1 (pak příkazová řádka
obsahovala pouze jméno programu, žádné parametry). K tomu nám slouží podmíněný
příkaz "if". Tělo prvního podmíněného příkazu (if (argc == 0)) je tvořeno
jediným příkazem, tělo druhého podmíněného příkazu je složený příkaz blok, který
vytiskne hlášení a ukončí program. Jak už jistě víte návrat z funkce je
realizován příkazem "return". Navíc zde používáme jednu z funkcí deklarovaných v
souboru <stdio.h> printf() - pro formátovaný výstup; zde slouží pouze k výpisu
řetězce (připomínám, že znak "
" představuje konec řádku).
V další části musíme spojit jednotlivé parametry do jednoho řetězce protože
celou příkazovou řádku považujeme za příkaz k vyhodnocení. Výsledek si dáme do
připraveného pole "vyraz" . Využíváme zde for-cyklus - for(výraz1; výraz2;
výraz3). Jak jsme si řekl minule, výraz1 se provede jen poprvé (přiřadí do i
číslo 1), výraz2 je podmínka cyklu (dokud i < argc, tj. dokud jsme neprošli
všechny parametry), výraz3 se provede po každém provedení těla cyklu (zde
zvyšuje hodnotu i o 1). Tělo cyklu je tvořeno podmíněným příkazem. Podmínka
testuje, zda je v poli vyraz dost místa na připojení dalšího řetězce (délka
toho, co tam už je + délka připojovaného + 1 (na nulový byte)). K tomu využíváme
funkci "strlen()", která je deklarována v souboru "string.h" a určuje počet
znaků řetězce (bez koncového nulového bytu). Jak je vidět, je-li podmínka
splněna (je dost místa), provede se připojení opět pomocí knihovní funkce
"strcat()" - první parametr určuje, kam se bude připojovat, druhý parametr
určuje, co se bude připojovat. Pokud podmínka splněna není, přeruší se provádění
cyklu příkazem "break" (viď minule).
Pak už stačí zavolat jen vyhodnocovací funkci ProjdiRetezec() a vytisknout
výsledek. Funkce ProjdiRetezec() vrací hodnotu typu BOOL - úspěch (= TRUE) nebo
neúspěch (= FALSE, něco se pokazilo). Má dva parametry -ukazatel na
vyhodnocovaný výraz a adresu proměnné, do níž má uložit výsledek. Všimněte si
předávání hodnoty výsledku - protože se parametry funkcí předávají hodnotou,
vytvoří se při volání funkce proměnná potřebného typu, do níž se uloží předávaný
parametr a která po skončení volané funkce zmizí. Pokud tedy chceme aby nám
funkce ProjdiRetezec() dosadila do "vysledek" nějakou hodnotu, musíme jí předat
adresu proměnné (aby věděla, KAM má psát). K získání adresy objektu lze použít
operátor "&" (jak víme z předchozích dílů). Když víme, jak získat výsledek,
stačí už jen vytisknout - zde poznamenám pouze tolik, že kombinace "%s" ve
formátovacím řetězci pro funkci printf() vytiskne příslušný řetězec uvedený jako
parametr a " %g" vytiskne reálné číslo.
A na závěr si povíme něco o funkci pro vlastní vyhodnocení výrazu:
BOOL ProjdiRetezec(char *vyraz, double *vysledek).
Parametry této funkce jsou (jak jsme již řekli) řetězec = vyhodnocovaný výraz a
adresa, kam se má zapsat určený výsledek. Lokální proměnné jsou čtyři - vysl (do
ní si průběžně budeme zapisovat mezivýsledek součtu), operand (tam budeme dávat
hodnotu právě načteného čísla), operator (obsahuje znak naposledy přečteného
operátoru - zatím jen + nebo-) a end (booleovská proměnná určující, zda jsme
již prošli celý výraz).
Tělo funkce je tvořeno především while-cyklem (dělej, dokud platí podmínka) -
zde je podmínka "negace end" (! je negace), tj. dokud nedojdeme na konec.
Vyhodnocení je založeno na tom, že nejprve musí být ve výrazu číslo, pak
operátor, a tak stále dokola (na konci je opět číslo).
V cyklu se proto vždy pokusíme nejprve načíst číslo. To udělá funkce "sscanf()",
opět z <stdio.h>. Tato funkce se chová stejně jako standardní funkce pro
formátovaný vstup "scanf()" s tím rozdílem, že data čte ze zadaného řetězce (a
to se nám hodí). Parametrem je tedy vstupní řetězec, formátovací řetězec
(obsahuje kódy stejné jako printf() - když se pomocí %g vypíše reálné číslo, pak
se tím i načte) a adresy proměnných, kam se mají výsledky načíst (je to stejný
mechanismus jako při předávání výsledku z této funkce do main()). Funkce
sscanf() přeskočí i případné počáteční mezery. Návratová hodnota určuje, do
kolika proměnných se něco načetlo, resp. je -1, pokud nešlo nic načíst. Pokud
tedy vrácené číslo ze sscanf() není 1, nepodařilo se číslo načíst (byla tam
např. písmena nebo konec řetězce) a považujeme to za chybu. Samozřejmě to
oznámíme pomocí printf() a ukončíme funkci pomocí příkazu "return" - vracíme
hodnotu FALSE, neboť se nepovedlo rozebrat korektně celý řetězec. Pozn.: k
funkcím jako scanf() a printf() se někdy vrátíme a popíšeme je podrobněji.
Jestliže jsme načetli do "operand" další číslo, pokusíme se najít další operátor
(pokud už tam žádný není, jsme na konci). K tomu nám opět poslouží knihovní
funkce "strpbrk()", která v prvním řetězci (vyraz) vyhledá první výskyt
libovolného znaku obsaženého v druhém řetězci (ZNAME_OPERATORY) - najde tedy
další plus nebo minus. strpbrk() vrací ukazatel na nalezený znak; pokud nic
nenajde, vrací NULL, což je v jazyce C symbolická hodnota pro ukazatel, který
nikam neukazuje (nemá přiřazenu adresu).
Vše zbývající je už jednoduché - podle toho, jaký operátor předcházel, přičteme
resp. odečteme načtený operand. Poté buď nastavíme end na TRUE (vyraz == NULL->
chceme ukončit cyklus, protože už nic nenásleduje), nebo si zapamatujeme nový
operátor (je na adrese určené ukazatelem "vyraz" a obsah adresy získáváme pomocí
operátoru "*") a posuneme se na další znak (pomocí vyraz++).
Všimněte si, jak se v cyklu manipuluje s ukazatelem vyraz - na počátku je v něm
adresa prvního znaku řetězce; můžeme si jej představit jako nějaké ukazovátko na
řadu políček (pole znaků) za sebou, přičemž toto ukazovátko lze libovolně
posunovat (a to taky děláme). Funkce strpbrk() nám řekne, kam ho posunout, aby
ukazovalo na další "+" nebo "-". Když si pomocí příkazu (operator=vyraz)
vyzvedneme znak, na který vyraz ukazuje, posuneme ho za tento znak příkazem
vyraz++ - tj. posuneme ukazovátko o jednu pozici dále, na další znak v řetězci.
Funkce sscanf() pak už nedostane ukazatel na počátek řetězce (jako poprvé), ale
ukazatel na počátek zatím nezpracované části řetězce.
A abychom nezapomněli - na závěr (po ukončení cyklu) se zapíše na dodanou adresu
výsledek (aby ho mohla použít funkce main()) - všimněte si, že operátor "*"
aplikovaný na ukazatel reprezentuje věrně obsah dané adresy, lze na ní takto i
přiřazovat. /* Calculator.c projekt pro kurs C v Amiga Review */
/* potřebné hlavičkové soubory */
#include <stdio.h>
#include <string.h>
/* definice booleovského typu i s hodnotami; standardně je definován v
<exec/types.h> */
typedef short BOOL;
#define TRUE 1
#define FALSE 0
/* definice používaných symbolů */
#define MAX_DELKA 1000
#define ZNAME_OPERATORY "+-"
/* řetězec identifikující program a jeho verzi */
#define VERZE_KALKULACKY "Kalkulačka 1.00 (5. 9. 1995)"
char *VERSION = " |