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...



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