Kurz jazyka C - 6. díl

Pavel Čížek

Přátelé jazyka C a výpočetních prostředků ("Kalkulaček"), vítejte u další části seriálu. Dnes si podrobně povíme o práci s ukazateli a řetězci.

Hned v úvodu musím objasnit jeden drobný problém. Minulá část kurzu byla poněkud delší a došlo k tomu, že nám někdo konec minulého povídání "odříznul". V tomto odříznutém konci byly mimo jiné 2 otázky pro Vás, čtenáře, na které jsem chtěl dnes navázat. Uvádím je tedy ještě jednou nyní (ovšem s tím, že si nebudete muset nic rozmýšlet sami doma, ale odpovíme si hned zde). A nyní slíbené otázky:
1) Proč nelze ve výrazech zatím používat unární plus a minus (funkce scanf() ho umí načíst)?
2) Lze někam do výrazu umístit chybné znaky tak, aby je program ignoroval? Proč je ignoruje?
Jak to obvykle bývá, nic není dokonalé hned od počátku - ani náš program. Odpověď na první otázku je jednoduchá - ve funkci ProjdiRetezec hledáme další začátek dalšího sčítance tak, že hledáme následující + nebo -. Pokud by ve výrazu bylo unární znaménko, pak by program při vyhodnocení výrazu nepřešel na následující sčítanec, ale zpracoval by ještě jednou tentýž. Pokud by programu bylo zadáno např. -10, pak určí, že výsledek je -20.
Druhá otázka není o nic složitější. Když jsme hledali další výskyt + nebo - použili jsme funkci strpbr(), která (pokud se nic nenašlo), vrátila místo adresy znaku NULL. Následovala podmínka která ukončila procházení výrazu, když vrácená hodnota byla NULL. To ovšem znamená, že pokud za posledním číslem bude jakýkoli nesmysl který nebude obsahovat + ani -, bude přeskočen a ignorován - např. 5 + 6 ble ble ble... projde jako správný výraz s výsledkem 11.
A než se pustíme do práce, upozorňuji, že novou podobu zdrojového kódu ke Kalkulačce (tak jak ji dnes upravíme) naleznete opět na coverdisku u časopisu.

Co s tím?
Ano co s tím? Jestliže budeme chtít vyhodnocovat nějaký složitější výraz, v němž budou navíc funkce (např. sinus) a závorky a leccos dalšího, nebudeme se moci bezvýhradně spolehnout na pomoc funkcí ze standardních knihoven Céčka. Budeme muset rozebírat ručně, včetně přeskakování čísel (to si dnes vyzkoušíme) atd. Určitě lze říci, že v takovém případě se bude procházet mnohem snáze výraz, který neobsahuje žádné mezery, konce řádků, tabelátory a mnoho podobných nevýznamných znaků. První, co tedy uděláme, je nasnadě zkusíme už při spojování řetězce (výrazu) všechny tyto zbytečné znaky zlikvidovat.
Jestli si vzpomínáte (stačí se podívat do zdrojového kódu z minulo), tak v hlavní funkci main() jsme po ošetření situací s nesprávným počtem parametrů prováděli jistý for-cyklus. Jeho účelem bylo spojit jednotlivé parametry z příkazové řádky do jediného řetězce s výrazem. Toto připojování bylo prováděno funkcí strcat(). Prvním parametrem této funkce je řetězec, ke kterému se má připojit řetězec daný parametrem druhým. Funkce vrací ukazatel na začátek spojeného řetězce. K čemu je nám dobré toto vědět? No to je jasné - napíšeme svoji vlastní funkci NasStrCat() (viz výpis Funkce dále), která bude dělat to samé jako původní funkce strcat(), ale bude připojovat z druhého řetězce jen části, které nejsou tzv. bílými znaky (mezery, konce řádků, tabelátory, posun stránky apod.).
Funkce NasStrCat() je velmi jednoduchá. Má dva parametry - ukazatele na znak. Připomínám, že ukazatel je proměnná, která obsahuje nějakou adresu ukazující do paměti. Pomocí ukazatele lze získat obsah paměti na příslušné adrese. Pokud máme tedy ukazatel na znak, je to adresa, na níž se nachází nějaké písmenko. No, a protože v Céčku se řetězec chápe jako posloupnost znaků, která je ukončena speciálním znakem (= nulovým bytem), děje se manipulace s řetězci pomocí ukazatelů na znak. Někdo vám prostě předá ukazatel na první znak řetězce a vy víte, že na adrese obsažené v proměnné typu ukazatel je první písmeno řetězce, na adrese o 1 vyšší je druhé písmeno a tak dále (dokud není nulový znak - tam je konec). Proto běžně pak používáme místo ukazatel na znak výrazy jako ukazatel na počátek řetězce nebo první parametr je řetězec (přičemž to chápeme tak, že dostáváme ukazatel na znak).
Když už jsme u těch ukazatelů - řekli jsme, že obsahují adresu. Samozřejmě, že ji potřebujeme měnit. To je v Céčku velice jednoduché, stačí k ukazateli přičíst celé číslo, které udává, o kolik "buněk paměti" se má ukazatel posunout, tj. jak se změní adresa v ukazateli obsažená. Pokud máme tedy za sebou nějaké znaky, tak přístup k dalšímu znaku získáme tak, že do ukazatele přičteme jedničku (ukazatel += 1 nebo ukazatel++). Pozor! Pokud budete mít pole reálných čísel a ukazatel na první z nich, tak posun ukazatele na číslo následující se provede stejně (i přesto, že reálné číslo zabírá v paměti jiný počet bytů než znak). Céčko prostě chápe přičítání / odčítání od ukazatelů tak, že se chcete posunout na některý jiný objekt; když ukazatel deklarujete, tak udáváte, na co ukazuje. Céčko si zjistí jak je daný objekt (znak, reálné číslo) velký a když pak přičítáte do ukazatele 1, změní adresu o tolik bytů, aby ukazovala na následující prvek pole apod. Například u znaku dojde při operaci ukazatel++ (odpovídající deklarace je char *ukazatel) ke zvětšení adresy v ukazateli o 1 byte, u reálného čísla v jednoduché přesnosti při téže operací (ukazatel++, deklarace vypadala float *ukazatel) se skutečná adresa u ukazateli zvýší o 4 byty.
Takže naše funkce dostane dva ukazatele - jeden na řetězec, ke kterému se má něco připojit, druhý na řetězec, který se připojuje. My budeme připojovat příslušný řetězec znak po znaku. Proto se na počátku funkce definují dvě proměnné; kam určuje, kam se právě znak zapisuje. Tuto proměnnou na počátku nastavíme za poslední znak prvního řetězce, tj. ukazatel retezec posuneme o počet písmen řetězce = strlen(retezec), přesněji adresu v ukazateli zvýšíme o tuto hodnotu. strlen() je funkce, která určí počet znaků řetězce bez posledního nulového znaku, je deklarována v hlavičkovém souboru <string.h>. odkud nastavíme na první znak připojovaného řetězce. Pak už stačí jenom procházet připojovaný řetězec znak po znaku a případně je kopírovat. Ono "procházení" není nic jiného než zvyšování adresy v ukazateli odkud o 1 (příkazem odkud++), tj. posunování pomyslného ukazovátka do paměti vždy o 1 políčko dále.
Podobně posunujeme i ukazatel kam na další a další pozice - ten posunujeme ovšem pouze v případě, že jsme něco zapsali. A jak poznáme, že máme znak připojit? Používáme funkci "isspace()" (deklarována v <ctype.h>), která vrátí TRUE (pravda), je-li znak bílá mezera. My samozřejmě máme v podmínce negaci (znak !), protože chceme připojit právě nemezerové znaky. Vlastní zapsání se provádí příkazem *kam = *odkud. Jak víme, lze z ukazatele získat obsah paměti na adrese v něm obsažené. K tomu slouží operátor reference * - pokud ho umístíme před ukazatel, pak nám celý výraz (*kam) reprezentuje přímo onu buňku v paměti - lze z ní hodnotu číst, ale také do ní hodnotu přiřazovat.
Po skončení "opisování" z jednoho řetězce na konec druhého musíme samozřejmě zapsat do nově vzniklého řetězce značku konec řetězce - znak . A na závěr vrátíme ukazatel na nový řetězec, což je totéž jako ukazatel na počátek prvního řetězce (na jeho konec jsme jen cosi připojili). Když už máme k dispozici naši funkci NasStrCat(), stačí nyní ve funkci main() nahradit již zmiňovaný výskyt funkce strcat() (je tam pouze jeden) naší funkcí.

/* pomocná funkce pro přeskočení reálného čísla; vstup: ukazatel na znak (= na řetězec), tj. ukazuje na 1. písmeno přeskakovaného čísla; výstup ukazatel na znak (= na řetězec) - bude ukazovat na znak následující za přeskakovaným číslem */
char *PreskokCisla(char *cislo)
{
/* číslo má tvar <znaménko><číslice>.<číslice>E<znaménko><číslice>; je-li na počátku znaménko, posuneme se na další znak čísla */
if ((*cislo == +) || (*cislo == -)) cislo++;
/* přeskočení úvodních číslic; isdigit je funkce, která vrací TRUE (pravda), je-li daný znak číslice */
/* děláme: dokud je (*cislo) číslice, posunujeme se na další znak */
for( ; isdigit(*cislo); cislo++);
/* pokud následuje tečka -> číslo má i desetinnou část; to přeskočíme stejným způsobem */
if (*cislo ==.)
{
cislo++;
/* posuneme se za tečku */
for( ; isdigit(*cislo); cislo++);
}
/* pokud následuje e nebo E, má číslo exponent; přeskočíme ho; toupper je funkce, která malé písmeno převede na velké */
if (toupper(*cislo) ==E)
{
cislo++;
/* posun na znak za E; je-li na počátku exponentu znaménko, posuneme se na další znak exponentu */
if(/*cislo==+) || (*cislo==-))
cislo++;
/* přeskočíme všechny číslice exponentu */
for( ; isdigit(*cislo); cislo++);
}
/* vrátíme ukazatel na znak za číslem ... je v cislo */
return(cislo);
}

Další úpravy
Když konečně umíme zadaný výraz transformovat do vhodné podoby pro další zpracování, můžeme se pustit dále. Jak jsem se zmínil výše, tak problém při hledání dalšího operátoru je v tom, že nepřeskakujeme číslo, ale hledáme další znaménko, které ovšem může být v čísle obsaženo. Musíme si proto vytvořit funkci pro přeskočení čísla - PreskokCisla (viz výpis). Jak ji pak použijeme, si povíme za chvilku.
Tato funkce bude mít na vstupu jediný parametr - ukazatel na řetězec (přesněji na počátek přeskakovaného čísla). Potom zjistí, kde je konec tohoto čísla a vrátí ukazatel na znak bezprostředně následující za číslem. Přeskočení samo o sobě je jednoduché - stačí si uvědomit, že posun na další znak dosáhneme zvýšením adresy v proměnné typu ukazatel o 1 (např. cislo++). Navíc nezapomeňte, že máme výraz bez mezer a podobných znaků!
Protože číslo může začínat znaménkem, nejprve přeskočíme znaménko (je-li třeba); přitom opět využíváme toho, že zápis *cislo nám reprezentuje obsah adresy, tj. znak. Připomínám - podmíněný příkaz začíná klíčovým slovem if následuje podmínka uvedená v kulatých závorkách. Test na rovnost se provádí pomocí ==, test na nerovnost pomocí != . Logické spojky v Céčku mají tento tvar - a (&&), nebo (||).
Hlavní část čísla se skládá z mantisy (<posloupnost číslic> . <posloupnost číslic> přičemž posloupnost číslic může být i prázdná). Naše funkce tedy pomocí for-cyklu přeskočí úvodní číslice a následuje-li tečka, přeskočí pomocí téhož for-cyklu také zbylou část číslic. Využíváme přitom funkce isdigit() (deklarována v <ctype.h>), která vrací TRUE (pravda), je-li zadaný znak číslice. No, a když je začátek čísla přeskočen, stačí (v případě potřeby) přeskočit exponent. Pokud je exponent uveden pak musí být uvozen písmenem e nebo E, za nímž následuje celé číslo. Postup je totožný jako při přeskakování mantisy. Vlastní otestování, zda je přítomen exponent (tj. test na e) jsme si zjednodušili tím, že jsme opět využili funkci deklarovanou v <ctype.h> toupper(), která zadaný znak převede na velké písmeno. Jakmile máme číslo přeskočeno (ukazatel výraz ukazuje za poslední znak čísla), stačí už jen vrátit tento ukazatel - jeho hodnotu - příkazem return().
Jak nyní využijeme toho, že jsme vyrobili přeskakovací funkci? Jednoduše. Pokud nahlédnete do našeho minulého programu, zjistíte, že v hlavním cyklu funkce ProjdiRetezec() používáme nejprve funkci sscanf() pro načtení čísla a hned za ní následuje hledání dalšího operátoru pomocí strpbrk() - o něm ovšem nyní víme, že následuje hned za načteným číslem (mezery jsou vynechány!). Tuto funkci strpbrk() proto stačí nahradit naší funkcí PreskokCisla. Nový řádek dostane tento tvar:

vyraz = PreskokCisla(vyraz);

Bude třeba udělat ještě nějaké další úpravy? Samozřejmě, že ano. Pokyn k ukončení byl zatím dán podmínkou (vyraz == NULL) - naše funkce ale vždy vrací ukazatel na něco. Podmínka se tedy změní; víme, že má následovat operátor (zatím bereme jen + a -). Pokud nenásleduje, považujeme to za ukončení procházení. Výhoda nynějšího postupu ovšem spočívá v tom, že můžeme detekovat "chyby za poslední číslicí". Když skončíme procházení, měl by následovat konec řetězce, nulový znak . Pokud tomu tak není, vrátíme chybu (FALSE). Jak tedy bude nahrazena podmínka pro ukončení procházení? Nový tvar je následující:

if ((*vyraz != +) && (*vyraz != -))
{
if (*vyraz != )
return(FALSE);
end = TRUE;
}
else /* dál už je to stejné... */

Zdá se, že je vše v pořádku. Ale není když se pozorně podíváte, zjistíte, že v předchozí verzi programu jsme chybu hlásili uvnitř funkce ProjdiRetezec() bylo to hned za funkcí sscanf(). Aby byla ohlášena chyba, je nutno nějaké chybové hlášení doplnit. Máte dvě možnosti - buď přímo před příkaz return(FALSE) (viz výše), který ukončuje funkci (pak jsou všechna hlášení prováděna funkcí ProjdiRetezec()), nebo v hlavní funkci main() můžete při neúspěchu funkce ProjdiRetezec() ohlásit chybu (tato varianta je provedena i v programu na coverdisku. V druhá varianta bude vypadat následovně (první dva řádky už tam byly od minula, třetí byl přidán):

if (ProjdiRetezec(vyraz, &vysledek))
printf("Výsledek: %s = %G ", vyraz, vysledek);
else printf("Chyba v zadaném výrazu! ");

Jste již vyčerpáni? Nebojte se, už je to téměř vše. Nezapomeňte pouze přidat na začátek k příkazům #include
příkaz pro vložení nově použitých funkcí:

#include <ctype.h>

Opravte si také řetězec popisující aktuální verzi kalkulačky na novou verzi. Dostane pak tvar

#define VERZE_KALKULACKY "Kalkulačka 1.01 (9.10.1995)"

Tak, a to je opravdu pro dnešek všechno. Můžete vyzkoušet, že nyní náš program může sčítat a odčítat čísla v libovolném tvaru. doufám, že vám v paměti utkvělo něco z našeho ukazatelo-řetězcového guláše. Příště nás čeká seznámení s rekurzí, která nám umožní poměrně jednoduše zpracovat i podstatně složitější výrazy, a začnou se nám objevovat také jednoduché struktury a jejich seznamy.

/* naše varianta funkce strcat ... připojí řetězec, ale vynechá všechny nezajímavé znaky = tzv. bílé znaky (mezery apod.); funkce strcat připojí k prvnímu řetězci druhý řetězec, vrátí ukazatel na první řetězec */
char *NasStrCat(char *retezec, char *pripojovany)
{
/* ukazatelé na znak: kam určuje pozici, kam se bude kopírovat; odkud určuje pozici, odkud se kopíruje */
char *kam, *odkud;
/* pomocí for--cyklu provedeme připojení; počáteční inicializace - kam ukazuje za poslední znak retezec; odkud na první znak připojovaného řetězce; podmínka = dělej, dokud nejsme na konci pripojovany (řetezce jsou ukončovány znakem - ten se bude brát jako logická hodnota FALSE; po okopírování se provede odkud++ = posun na další znak */
for( kam = retezec + strlen(retezec), odkud = pripojovany; *odkud;odkud++)
/* isspace vrací TRUE (pravda), Ie-li daný znak mezera, konec řádku apod. pokud je to nějaký rozumný znak -> okopírujeme ho na pozici kam pokud okopírujeme, posuneme kam na další zapisovací políčko - toto je tělo forcyklu! */
if (!isspace(*odkud))
{
*kam = *odkud; kam++;
}
/* ukončení vytvářeného řetězce ... umístíme na jeho konec znak */
*kam =;
/* vrátíme ukazatel na počátek řetězce */
return(retezec);
}



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