Kurz jazyka C - dokončeníPavel Čížek
Vítám vás u dnešního pokračování našeho povídání o jazyce C, které náš seriál o
Céčku ukončí. Jak jsem minule slíbil, povíme si, jak doplnit do vyhodnocení výrazu možnost
zpracování funkcí a proměnných. Tím jednak značně rozšíříme funkčnost naší
kalkulačky a dodáme smysl vícenásobnému výpočtu výrazu, pro který jsme
algoritmus navrhovali (můžeme počítat např. hodnoty pro X = 0 .. 1 s krokem
0.1). Nemusím snad připomínat, že opět celý zdrojový kód programu (nyní již
vlastně jeho konečnou podobu) naleznete na coverdisku. Začněme nyní tím
jednodušším. Zpracování proměnných
Proměnné se ve výrazech vyskytují zcela běžně - ať už chceme spočítat
tabulku hodnot, graf funkce nebo pracovat v tabulkovém kalkulátoru.
Předpokládejme prozatím, že potřebujeme pouze 1 proměnnou - X Co je potřeba
pozměnit v našem prográmku, aby výraz vzal v úvahu i proměnné?
Předně musíme zavést typ operace PROMENNA (stejně jako máme definovány operace
sčítání či odečítání). Tato operace bude mít jednoduchou funkci - dosadit do
výrazu aktuální hodnotu proměnné. Definice výčtového typu TypElementu pak bude
vypadat následovně: enum TypyElementu {TE_NEZNAMY = 0, TE_ClSLO = 1, TE_SCITANI, TE_NASOBENI,
TE_ZAVORKY, TE_PROMENNA}; Je zřejmé, že musíme také provést úpravy ve funkcích pro rozbor, vyhodnocení
a uvolnění výrazu. Uzel = element typu proměnná bude mít mnoho vlastností s
elementem typu TE_CISLO - rozdíl bude pouze v tom, že jeho hodnota bude určena
nějakou externě danou proměnnou místo zápisu v textové podobě přímo v řetězci.
Ve funkci VytvorElement nám tedy přibude ještě jeden parametr = ukazatel na
proměnnou typu BOOL, který bude říkat, zda se ve vytvářeném elementu vyskytla
proměnná X (zatím jsem měli pouze proměnné určující výskyt sčítání, odčítání,
závorek a čísla). Hned v úvodu funkce přibude ještě jeden podmíněný příkaz - za
podmínky, že se ve výrazu objevila proměnná, bude mít nový výraz typ
TE_PROMENNA. Navíc je třeba na konci této funkce provést nastavení všech
booleovských parametrů na FALSE. Celou funkci VytvorElement, kterou jsme si
popsali minule (její výpis však nebyl uveden), můžete nalézt někde na této
dvoustraně. Budou otištěny také zkrácené výpisy všech dalších funkcí, které
budeme měnit.
Vlastní funkce RozeberVyraz bude také pozměněna - přibude proměnná
byla_promenna, která nám bude sdělovat, zda se v aktuálně zpracovávaném
elementu vyskytla proměnná X. Tato booleovská proměnná pak bude (společně s
ostatními) předána výše popsané funkci VytvorElement. Dále se ve for cyklu,
pomocí kterého procházíme zadaný výraz, v příkazu switch (*uk) objeví další
varianta odpovídající výskytu proměnné x. Tato varianta bude obsahovat jediný
příkaz: case x: case X:
byla_promenna = TRUE;
break; Řekli jsme si totiž, že zpracování výskytu proměnné bude velice podobné
zpracování čísla ve výrazu. Při zpracování čísla však nastavíme příslušnou
booleovskou proměnnou (bylo cislo) a číslo přeskočíme. Přeskok na konec elementu
však u proměnné není nutný, nebol je tvořena pouze jedním znakem. Další změny ve
funkci RozeberVyraz nejsou nutné, nebol rekurentní rozbor nemá u elementu typu
TE_PROMENNA smysl.
Další funkcí v pořadí je UvolniVyraz. Změna bude opět nepatrná. V této funkci
jsme postupovali při uvolňování v závislosti na tom, zda element má ještě další
parametry (týkalo se to sčítání, násobení a závorek) nebo ne (případ číslice a
nyní také proměnné). Stačí tedy ve for-cyklu, který v podstatě tvoří celé tělo
této funkce, změnit podmínku if (pomelem->typ == TE_CISLO) na podmínku if ((pomelem->typ == TE_CISLO) || (pomelem->typ -= TE PROMENNA)) V případě funkce VypoctiVyraz je nutno provést změn více. Především musíme
přidat ještě jeden parametr, který bude určovat, pro jakou hodnotu proměnné x
provádíme výpočet výrazu. Hlavička této funkce bude tedy vypadat následovně: double VypoctiVyraz(struct Element *vyraz, double x) Další změna nastane o několik řádek níže. Při výpočtu výrazu jsme si
rozdělili elementy do tří skupin: čísla (a nyní i proměnná), tj. elementy bez
parametrů; sčítání a násobení, tj. operace s libovolným počtem parametrů; no a
konečně ostatní elementy (zatím to byly pouze závorky), které mají právě jeden
parametr. Do první skupiny nám nyní přibyl další typ elementu - musíme tedy
změnit podmínku ve funkci z jejího původního tvaru if (vyraz->typ == TE_CISLO)
vysledek = vyraz->cislo;
else ... do tvaru if ((vyraz->typ == TE_CISLO) ||
(vyraz->typ == TE_PROMENNA))
vysledek = (vyraz->typ == TE_CISLO)
? vyraz->cislo : x;
else ... Abychom zachovali popsanou koncepci, využili jsme ternární podmíněný operátor
- jedná-li se o číslo, dosadíme do proměnné výsledek hodnotu tohoto čísla,
jedná-li se o proměnnou, dosadíme aktuální hodnotu pro proměnnou x. Jediná další
změna v této funkci je doplnění nového parametru x (aktuální hodnota proměnné
x) do rekurentních volání funkce VypoctiVyraz.
Jak je vidět, ani to moc nebolelo. Nyní stačí doplnit zpracování případných
dalších parametrů našeho programu ale to už nechám na vás. Jedna z možností je
například považovat první parametr programu za minimální hodnotu x, druha za
maximální hodnotu x atd. Nebo se může program až po spuštění zeptat, co
vlastně chcete s výrazem provést (výpočet, výpis tabulky s funkčními hodnotami
apod.). Zpracování funkcí
Nyní si ukážeme, jak zabudovat do naší "Kalkulačky" rozbor výrazu, který
obsahuje nějaké funkce. Budeme pro jednoduchost předpokládat, že máme pouze
funkce s jedním argumentem, jako například sinus, logaritmus apod. Zatím budeme
předpokládat, že chceme přidat funkce sin(x), cos(x), tg(x), arcsin(x),
arctg(x), log(x) a root3(x) (= 3. odmocnina z x). Způsobem, který si ukážeme,
lze počet funkcí libovolně rozšiřovat dále (a to nejenom o klasické funkce jako
sgn(x), ale třeba o funkci PI(), která bude vracet Ludolfovo číslo...).
Stejně jako v předchozím případě musíme nejprve definovat jednotlivé operace,
tj. rozšířit dále výčtový typ TypyElementu. Příkazem #define FUNKCE 100 si zavedeme konstantu FUNKCE, která nám bude určovat číslo = identifikátor
první funkce z našeho seznamu (uvidíte, že se nám to bude hodit). Nová definice
typu elementu pak může vypadat například takto: enum TypyElementu { TE_NEZNAMY = 0, TE_CISLO = 1, TE_SCITANI, TE_NASOBENI,
TE_ZAVORKY, TE_PROMENNA, TE_SIN = FUNKCE, TE_COS, TE_TG, TE_ARCSIN, TE_ARCTG,
TE_LOG, TE_ROOT3 }; Dále budeme určitě potřebovat nějaký seznam, v němž budou obsaženy jména
známých funkcí v textové podobě, tj. zapsané tak, jak budou uváděny ve výrazech.
Za tímto účelem si můžeme nadefinovat pole ukazatelů (vlastně řetězců), každá
položka bude obsahovat jméno jedné z funkcí. Aby se dalo později jednoduše
realizovat přidávání /* pomocná funkce pro vytvoření elementu;*/
/* vstup je vyplněná struktura vzor (kromě typu), vyraz je výraz, jehož
parametry rozebíráme, poslední je naposledy vytvořený parametr (NULL znamená
první vytvářený parametr) a 4 booleovské proměnné plus, krat, zavorka a
cislo, která určují, co vše se v elementu vyskytlo; parametrem jsou ukazatele
na tyto proměnné, aby bylo možno je rovnou vyčistit (nastavit všechny na FALSE);
*/
/* výstup je ukazatel na nově vytvořenou strukturu nebo NULL (neúspěch) */
struct Element *VytvorElement(
struct Element *vzor
struct Element *vyraz,
struct Element *posledni,
BOOL *plus, BOOL *krat,
BOOL *zavorka, BOOL *cislo,
BOOL *promenna, BOOL *funkce,
enum TypyElementu typ)
{
/* ukazatel na nově vytvořenou strukturu */
struct Element *novy;
/* určení typu nového elementu - podle toho, co vše se v něm vyskytlo */
if (*krat)
vzor->typ = TE_NASOBENI;
else if (*zavorka)
vzor->typ = TE_ZAVORKY;
else if (*funkce)
vzor->typ = typ;
else if (*promenna)
vzor->typ = TE_PROMENNA;
else vzor->typ = TE_CISLO;
/* vytvoříme nový výraz... */
novy = malloc(sizeof(struct Element));
/* a pokud se to povedlo, okopírujeme do něj příslušné údaje */
if (novy)
*novy = *vzor;
/* zapojíme ho do seznamu parametrů */
if (poslední == NULL)
vyraz->parametr = novy;
else
poslední->dalsi = novy;
/* vymazání hodnot v booleovských proměnných */
*plus = FALSE; *krat = FALSE;
*zavorka = FALSE; *cislo = FALSE;
*promenna = FALSE; *funkce = FALSE;
/* návrat hodnoty */
return(novy);
}
/* funkce pro rozbor výrazu - úpravy */
BOOL RozeberVyraz(struct Element *vyraz, int uroven)
{
/* původní deklarace ... vynecháno */
BOOL byla_promenna = FALSE, byla_funkce = FALSE;
enum TypyElementu typ_funkce;
/* počáteční inicializace
/* vlastní prochazení výrazem */
for( ... vynecháno ...)
{
switch (*uk)
{
/* varianty pro sčítání, násobení, číslice a závorky ... vynecháno */
case x:
case X: /* proměnná */
byla_promenna = TRUE;
break:
default: /* test funkce */
byla funkce = TRUE;
pomuk = PreskokFunkce(uk, &typ funkce);
if (pomuk -= uk ) return(FALSE);
else uk = pomuk - 1;
break;
}
}
/* vytvoření posl. výrazu ... vynecháno */
/* rozbor jednotlivých elementů */
/* projdeme postupně všechny vytvořené parametry */
for(aktualni = vyraz->parametr; aktualni != NULL;
aktualni = aktualni->dalsi )
/* rozebíráme rekurentně výraz = parametr */
switch (aktualni->typ)
{
/* číslo, *, závorky ... vynecháno */
default:
while (*aktualni->zacatek != () aktualni->zacatek++; aktualni->zacatek++;
aktualni->konec--,
if (!RozeberVyraz(aktualni, UROVEN_1))
return(FALSE);
break;
}
return(TRUE);
} dalších funkcí, budeme předpokládat, že za jménem poslední Funkce bude
položka = ukazatel na NULL. A aby bylo možné jednoduše rozpoznat typ funkce (tj.
převést nějaké zadané jméno funkce na naši konstantu), domluvíme se, že jména
funkcí v poli řetězců budou ve stejném pořadí, jako identifikátory jednotlivých
funkcí ve výše uvedené definici výčtového typu. Není to ještě vše, zbytek si ale
ukážeme později. Definice pole JmenaFunkci bude tedy vypadat následovně: char *JmenaFunkci[ ] = { "Sin", "Cos", "Tg", "ArcSin", "ArcTg", "Log",
"Root3", NULL }; Pokud to ještě nevíte (nebo pokud jsme se o tom nezmínili), tak při
inicializaci pole přímo v deklaraci je možno provést výše použitou věc.
Uvedeme-li výpis položek, které má pole obsahovat, nemusíme uvádět za jméno pole
jeho délku, ale můžeme za jménem ponechat prázdné hranaté závorky. V tomto
případě si kompilátor sám spočítá, kolik položek má pole mít, aby se do něj
vešly všechny položky výčtu (u nás jsou to ta jména funkcí) a tuto délku
použije.
Podívejme se opět na to, co je třeba upravit v našich funkcích pro zpracování
výrazu, aby vše pracovalo jak má. I zde nám pomůže podobnost s již existujícím
kódem. Zápis funkce je totiž vlastně jakýsi text následovaný závorkou. Tato
závorka se má vyhodnotit (stejně jako normální závorka, kterou jsme již
naprogramovali). Ten text před závorkou nám pouze sděluje, že obsah závorky se
má po vyčíslení ještě nějak zpracovat - například odmocnit.
U funkce VytvorElement opět přidáme další parametr (dokonce dva). Jeden bude
booleovská proměnná udávající, zda se v právě rozebrané části řetězce vyskytla
funkce - ten nám opět umožní rozlišit při vytváření další typ elementu (podobně
jako tomu bylo, když jsme přidávali proměnnou x) a na konci bude opět
vynulován (nastaven na FALSE). Druhý parametr bude typu enum Element a bude
nám udávat, o jakou funkci se vlastně jednalo. Jeho hodnotu pak okopírujeme před
vytvořením do položky typ struktury Element.
Stejně jako před chvílí bude nutné funkci ve `RozeberVyraz vytvořit další dvě
proměnné - právě ty, které budou předávány funkci VytvorElement jako ony
dodatečně přidané parametry. Jedná se tedy o booleovskou proměnnou byla funkce
a proměnnou typu enum TypyElementu, kterou nazveme např. typ_funkce. Volání
funkce VytvorElement bude tedy v konečné podobě vypadat takto: aktualni = VytvorElement(&pomelem vyraz, posledni, &bylo_plus, &bylo_krat,
&byla_zavorka, &bylo_cislo, &byla_promenna, &byla_funkce, typ_funkce); Vypadá to divoce, což? Samozřejmě, že bychom vše mohli provést mnohem
elegantněji, ale na to nám již v našem kurzu nezbývá čas.
Jak je vám již určitě jasné z první části článku, je třeba doplnit do této
funkce nějakou část příkazu switch() v hlavním for-cyklu (= procházení
řetězce), která ošetří přeskočení funkce a určí její typ. Na to si za chvíli
vyrobíme funkci PreskokFunkce, která nám obě tyto funkce zajistí. Její volání
umístíme do části default - je nesmysl pokoušet se vyjmenovat všechna možná
počáteční písmena jako varianty příkazu switch(). Funkce PreskokFunkce
(stejně, jako ostatní funkce pro přeskakování něčeho) bude vracet ukazatel na
novou pozici = poslední znak přeskakované funkce. Protože nápis funkce musí mít
více než tři znaky (alespoň 1 znak jako jméno funkce a 2 znaky tvoří závorky),
měl by se ukazatel na rozebíraný řetězec posunout. Pokud se neposune, bude nám
to signalizovat, že tam není funkce, ale nějaké nesmysly - v tom případě
provedeme původní operaci, která byla u větve default uvedené - tj. vrátíme
chybu (return(FALSE)).
Dále je třeba zajistit také správné chování v druhém for-cyklu (rekurentní
rozbor jednotlivých již oddělených elementů). Vzhledem k podobnosti s
vyhodnocením závorky je snad zřejmé, že stačí přeskočit jméno funkce (tj. získat
ukazatel na závorku následující hned za jménem funkce). To provedeme
následujícím while-cyklem (aktualni je zde ukazatel na zpracovávaný výraz,
položky zacatek a konec určují počátek a konec řetězce, v němž je element
zapsán): while (aktualni->zacatek != () aktualni->zacatek++; Pak už máme totiž položkami zacatek a konec určenu závorku, v níž je
obsažen argument funkce a stačí tedy provést tytéž operace jako při rekurentním
rozboru závorky.
A co další dvě funkce? Funkci UvolniVyraz není třeba měnit - tam je vše v
pořádku. Ve funkci VypoctiVyraz je však třeba doplnit kód pro vyčíslení
jednotlivých funkcí. Již jsme se zmiňovali o tom, že máme při vyhodnocení typy
elementů rozděleny na tři skupiny; funkce patří do té poslední - mají jeden
parametr. V části odpovídající operátorům s jedním parametrem máme již z minula
uveden příkaz switch(), který nám umožní pro každý typ provést příslušnou
akci. V proměnné parametr je přitom již uvedena hodnota parametru funkce. Pokud
tedy budeme chtít přidat zpracování funkce sin(x), přidáme do onoho příkazu
switch() následující řádky: case TE_SIN:
vysledek = sin(parametr);
break; A kde vezmeme funkci sinus? Tuto a řadu dalších matematických funkcí lze
nalézt v matematické knihovně. Vám stačí přidat na počátek souboru příkaz pro
preprocesor #include <math.h> V souboru math.h jsou uvedeny deklarace všech možných matematických funkcí,
ale také mnoha konstant (Ludolfovo číslo, jeho polovina a čtvrtina, Eulerovo
číslo a mnoho dalších). Naleznete zde také funkce fxxxx(), kde xxxx
představuje jméno matematické funkce - to jsou vlastně tytéž operace pro práci v
jednoduché přesnosti (float). Ještě bych chtěl připomenout, že u některých
funkcí (logaritmus)je situace trochu složitější - nejprve by bylo dobré ošetřit
případy, kdy parametr není z definičního oboru funkce. Přeskok funkce
Nyní si popíšeme již zmíněnou funkci pro přeskok a identifikaci funkce.
Funkce PreskokFunkce bude mít dva parametry: ukazatel na počátek jména funkce
(funkce pak vrátí ukazatel na znak následující za celým zápisem funkce); dalším
parametrem bude ukazatel na proměnnou typu enum TypyElementu - ten nám umožní
vrátit identifikátor funkce (přesněji pomocí ukazatele zapsat hodnotu
identifikátoru do proměnné, jejíž adresu jako parametr funkce dostane). Pokud
bude tedy v textu zápis sin(2), bude vrácen identifikátor TE_SIN.
Jak ten typ zjistíme? To je prosté - v poli JmenaFunkci máme seznam jmen všech
funkcí, které umí náš program zpracovat. Pomocí for-cyklu stačí tato jména
projít a porovnat s tím, co je v zadaném výrazu. Proměnná i určuje, kolikáté
jméno právě testujeme; ukončovací podmínka cyklu je JmenaFunkci[i] - víme
totiž, že jsme za poslední jméno seznamu zapsali NULL (což je chápáno jako
nepravda). Porovnání provádíme pomocí strnicmp(). Obecně funkce str...cmp()
provádí porovnání dvou řetězců. Písmeno i znamená, že porovnání se má provádět
s bez ohledu na velikost písmen (tj. A == a). Písmeno n ve jméně značí, že
se má porovnat pouze prvních n znaků zadaných řetězců (toto číslo je posledním
parametrem funkce). Funkce str...cmp() vrací 3 možné hodnoty: -1 znamená, že
první řetězec je lexikograficky menší; 0 označuje rovnost obou řetězců; 1
znamená, že první řetězec je lexikograficky větší.
V našem případě porovnáme text v zadaném výrazu a i-té jméno v seznamu. Pokud
jsou totožné, procházení ukončíme. Po ukončení cyklu je v proměnné i index
jména funkce, která byla ve výrazu.
Způsob vyhledávání nám dává další omezení na seřazení seznamu jmen funkcí (o
některých jsme mluvili dříve). Pokud jméno jedné funkce začíná stejně jako jméno
funkce jiné (např. srn a sinh), je nutno zařadit funkci s delším jménem do
seznamu před funkci s kratším jménem. Proč? To si rozmyslete sami.
Po té, co jsme prošli jména funkcí, mohou následovat dvě akce. Buď nastala
chyba, nebo ne. Chyba nastane, pokud není funkce v seznamu programem
podporovaných operací (v tom případě nebyl for-cyklus ukončen příkazem break,
ale regulérní podmínkou; pak ale platí JmenaFunkci[i] == NULL), nebo za jménem
funkce nenásleduje otevírací závorka. Pokud je vše v pořádku, posuneme ukazatel
uk na otevírací závorku (tj. přičteme k němu počet znaků jména funkce
strlen(JmenaFunkci[i])) a využijeme již vytvořenou funkci pro přeskok závorky.
Potom zbývá již jen vrátit typ funkce (ten je i + FUNKCE, nebol identifikátor
první funkce má hodnotu FUNKCE...) a ukazatel na znak následující za zápisem
funkce. /* následující funkce vypočte hodnotu zadaného výrazu */
double VypoctiVyraz(struct Element *vyraz, double x)
{
struct Element *pomelem; double vysledek, parametr; enum TypyElementu pomtyp;
/* číslo nebo proměnná */
if ((vyraz->typ == TE_CISLO) || (vyraz->typ == TE_PROMENNA)) vysledek =
(vyraz->typ == TE_CISLO) ? vyraz->cislo: x;
else if (..operace + a *... vynecháno)
else /* ostatní operátory */
{
/* typ dat v závorce (který je obecně TE_ZAVORKA nebo identifikátor funkce)
změníme na TE_SCITANI posčítáme data v závorce; po vyhodnocení závorky obnovíme
původní typ */
pomtyp = vyraz->typ;
vyraz->typ = TE_SCITANI;
parametr= VypoctiVyraz(vyraz, x);
vyraz->typ = pomtyp;
/* zde jsou vyhodnoceny jednotlivé funkce; není doplněno žádné ošetření
(ne)správnosti funkčních hodnot; využíváme funkcí matematické knihovny */
switch (vyraz->typ)
{
case TE_SIN:
vysledek = sin(parametr);
break;
case TE_ARCTG:
vysledek = atan(parametr);
break;
/* následuje realizace řady další funkcí */
}
/* vrácení získané hodnoty */
}
return(vysledek);
}
/* pomocná funkce pro přeskočení funkce */
/* vstup: ukazatel na znak (= na řetězec), na počátek jména funkce a ukazatel na
proměnnou, kam se má uložit typ funkce;
výstup: ukazatel na znak (= na řetězec) - bude ukazovat na znak následující za
koncovou závorkou */
char *PreskokFunkce(char *uk, enum TypyElementu *typ)
{
int i;
/* projdeme jména nám známých funkcí; pokud je některé z nich totožné se jménem
v řetězci, přerušíme procházení; strinmp porovná prvních n znaků řetězců bez
ohledu na velikost písmen */
for(i = 0; JmenaFunkci[i]; i++)
if (0 == strnicmp(uk, JmenaFunkci[i], strlen(JmenaFunkci[i])))
break;
/* ošetříme případnou chybu */
/* bud se nenašla žádná funkce zadaného jména - došli jsme až k závěrečnému NULL
*/
if (JmenaFunkci[i] == NULL)
return(uk);
/* či za jménem nenásleduje závorka */
if (uk[strlen(JmenaFunkci[i])] != ()
return(uk);
/* nyní stačí přesunout ukazatel na závorku za jménem */
uk += strlen(JmenaFunkci[i]);
/* a závorku přeskočit */
uk = PreskokZavorky(uk);
/* vrátíme ukazatel na znak za závorkou a typ funkce */
*typ = i + FUNKCE;
return(uk);
} Závěr
Toť vše vážení přátelé. Ptáte se, k čemu nám takovýto prográmek může být
dobrý, že ano. Jak jsem již naznačil, je tu především možnost rozšířit ho o
další funkce (i s více parametry). Můžete se také pokusit o zpracování více
proměnných. Dalším tipem může být možnost definovat vlastní funkce přímo
uživatelem. Ptáte se, co s takovýmto prográmkem dál? Takto zdokonalené jádro pak
lze použít v rámci programu pro kreslení grafů, práci s matematickými výrazy
nebo také jako základ pro vyhodnocení obsahu buněk tabulkového kalkulátoru.
Nezapomínejte, že i ty nejsložitější programy se skládají z menších a malých
celků - a "Kalkulačka" může být,jedním z nich. S přáním hezkých chvil s jazykem
C končím tento seriál, aby vzniklo místo pro potřebnější a žádanější články... Vytlačiť článok
Pozn.: články boli naskenované ako text a preto obsahujú aj zopár chýb. Taktiež neručíme za zdrojové kódy (Asm, C, Arexx, AmigaGuide, Html) a odkazy na web. Dúfame, že napriek tomu vám táto databáza dobre poslúži.
Žiadna časť nesmie byť reprodukovaná alebo inak šírená bez písomného povolenia vydavatela © ATLANTIDA Publishing
none
|