Kurz jazyka C - 6. dílPavel Číž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);
} 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
|