Programovací jazyk C++ Jiří Vogel 1994
Úvodní poznámka Tato skripta vznikla po odpřednášení několika jednosemestrálních doporučených kurzů: Programovací jazyk C++. Při sestavování těchto přednášek jsem vycházel hlavně z publikace [3], jejíž autoři jsou zároveň tvůrci jazyka C. Jsou to zřejmě ty výjimečné typy lidí, kteří jsou skvělými programátory a zároveň výbornými pedagogy. Takže v prakticky celé 1. a 2. kapitole jsem se nechal vést jejich vzorem. Další publikace, do kterých jsem by» jen nahlédl, jsou uvedeny v seznamu literatury. Publikace [2] a [6] pojednávají o standardu ANSI C. Publikace [1] a [3] o původní K&R verzi jazyka C, skriptum [4] popisuje jazyk ANSI C a zároveň seznamuje čtenáře s verzí K&R. Jazyka C++ se týkají knihy [5], [7] a kniha [8], která je navíc věnována hlavně objektově orientovanému programování, protože C++ je jedním z mnoha jazyků, které tento programátorský styl práce umožňují. Skripta jsem se snažil psát maximálně stručně a důraz jsem kladl na příklady, které jsou všechny odladěny (většinou jsem použil produktu firmy Borland Turbo C++), takže syntaktické chyby ani chyby v algoritmizaci (snad s výjimkou volby optimálního algoritmu a chyb, které vznikly při přenosu programů do skript) by se v nich neměly vyskytovat. V prvé kapitole jsou základní struktury jazyka C++ vysvětleny pouze na příkladech. Tuto kapitolu by si měl každý čtenář důkladně promyslet, a odzkoušet jak odladěné příklady, tak zadaná cvičení. Ve druhé kapitole jsou uvedeny podrobnosti typu: Jak se zadávají konstanty? Kolik a jakých znaků smí mít jméno nějakého objektu? Jaké jsou základní typy hodnot, jaké jsou standardní operace v jazyku atd. Ve třetí kapitole se čtenář dozví další důležité podrobnosti o jazyku C++, kterými jsem nechtěl z pedagogických důvodů zatěžovat v prvních dvou kapitolách. Čtvrtá kapitola pojednává podrobně o polích a funkcích, o kterých čtenář skript, který došel až sem, již dost ví, a o ukazatelích (jiný český termín je spoj), které byly čtenáři prakticky zamlčeny. Pátá kapitola pojednává o strukturách, třídách (i když s nejdůležitějšími vlastnostmi tříd se čtenář seznámí až v kapitole šesté), unionech a takových podrobnostech jako jsou tzv. bitová pole. Šestá kapitola je věnována objektově orientovanému programování, které je vysvětleno na různě složitých příkladech, kdy jsem se vždy snažil vycházet od jednoduchých příkladů, ale závěrem jsem se nevyhnul ani příkladům dosti složitým, aby vynikl důvod, pro který tento programátorský styl vznikl. Sedmá kapitola je věnována předpřekladači (nebo, chcete-li, preprocesoru), který usnadňuje programování jak v jazyku C, tak v jazyku C++. Závěrem bych chtěl upozornit, že ten, kdo se chce seznámit se speciálními možnostmi konkrétní implementace jazyka C++, tj. např. grafickými a matematickými knihovnami, možnostmi optimalizace programu apod., ten nech» tato skripta odloží. Ten kdo se chce obecně, ale relativně podrobně seznámit s jazykem C++, ten by snad mohl toto skriptum použít jako užitečnou pomůcku.
Závěrem bych chtěl poděkovat studentu Jiřímu Vávrovi ze Strojní fakulty ČVUT a doktorandu Ing. Luďku Šárovi z Ústavu termomechaniky AV ČR za pečlivé přečtení rukopisu a oponentu Ing. Svenu Ubikovi, systémovému programátorovi z Informačního centra FS ČVUT za cenné připomínky k úpravě textu.
Autor
Poznámka k ťelektronickéť podobě skript Skripta jsem přepsal do jazyka HTML, aby se studentům usnadnil přístup a zároveň aby byla demonstrována užitečnost práce s tímto tvarem informací. Dnešním dnem ruším informaci: Contents under Construction , ale za jakékoliv připomínky na své e-mailové adrese:
[email protected] nebo
[email protected]
předem děkuji
Autor Praha 11. července 1998
Obsah 1. Úvod do jazyka C++
1. Vstup a výstup 2. Příkazy cyklu for a do-while 3. Několik jednoduchých programů 1. Vstup a výstup po znacích 2. Kopírování souboru 3. Počítání znaků 4. Počítání řádků 5. Počítání slov 4. Pole 5. Funkce 1. Automatické a externí proměnné 2. Rekurzívní funkce 6. Závěrečné poznámky k 1. Kapitole 7. Cvičení 2. Typy, operátory, výrazy
1. Jména, typy, konstanty 1. Jména 2. Typy 3. Konstanty 2. Operátory 1. Aritmetické operátory 2. Přiřazovací operátory 3. Podmíněný výraz 4. Relační a logické operátory 3. Typová konverze 1. Priority operací 3. Podrobnosti k předchozím kapitolám
1. Příkazy a bloky 2. Operátor :: 3. Operátor čárka 4. Příkaz break 5. Příkaz continue 6. Registrové proměnné 7. Cvičení 4. Ukazatelé, pole, funkce
1. Ukazatelé a adresy 1. Ukazatelé a argumenty funkcí 2. Ukazatelé a pole 3. Ukazatelé a konstanty 4. Argumenty povelového řádku 5. Inicializace polí a polí ukazatelů 6. Ukazatelé a vícerozměrná pole 7. Ještě několik podrobností o funkcích 1. Vnitřní funkce 2. Dosazené parametry 3. Přetížené funkce 4. Funkce s nespecifikovaným počtem parametrů 8. Cvičení 5. Struktury, třídy, uniony, bitová pole
1. Struktury a třídy 1. Pole struktur 2. Typ union 1. Anonymní typ union 3. Příklady dynamických objektů 4. Bitová pole 5. Cvičení 6. Základy objektově orientovaného programování
1. Úvod 2. Příklady objektově orientovaného programování v C++ 3. Spřátelené funkce a třídy 4. Přetížené operátory 5. Závěrečná poznámka k OOP 6. Cvičení 7. Poznámky k předpřekladači
1. Direktiva #define 1. Direktiva #define bez parametrů 2. Direktiva #define s parametry 2. Operátory # a ## 3. Podmíněný překlad Literatura
Kapitola 1 Úvod do jazyka C++ V této kapitole se seznámíme s podstatnými prvky jazyka, aniž bychom zabíhali do podrobností. Jazyk budeme vykládat na příkladech. Tento způsob výkladu má svá úskalí, zejména pokud se jedná o přesnosti popisu jazyka, ale jediným způsobem jak se naučit programovací jazyk, je programovat, a proto zvolíme tuto strategii. Budeme postupovat tak, že nejprve zadáme úlohu, napíšeme program v jazyku C++ a program si vysvětlíme.
1. Vstup a výstup Příklad 1.1. Sestavme program, který vytiskne text Vítejte v kurzu jazyka C++
Řešení: 1. varianta: 2. 3. 4. 5. 6.
#include
main() { cout << ťVitejte v kurzu jazyka C++ť << '\n'; }
Vlastní program začíná slovem main(), které v tomto případě dává kompilátoru jazyka informaci, že se jedná o tzv. hlavní program. Program v jazyku C++ může sestávat z jednotlivých programových jednotek, kterým říkáme funkce. Každá funkce, může mít argumenty uvedené v okrouhlých závorkách za jménem funkce. Protože hlavní program je považován také za funkci, jsou za jménem main okrouhlé závorky. Později uvidíme, že i hlavní program smí mít argumenty. Složené závorky označují začátek a konec hlavního programu a příkaz cout je tzv. proudová funkce pro výstup. Zajistí (s pomocí operátorů proudového výstupu <<) výstup řetězce znaků, které jsou zapsány mezi uvozovkami a znaku \n , který je zapsán mezi apostrofy. Tento znak znamená přechod na nový řádek. Každý příkaz v jazyku C++ končí středníkem. Přesvědčte se, že lze též napsat: cout << ťVitejte v kurzu jazyka C++ \nť;
První řádek programu je příkazem pro tzv. předpřekladač, který před překladem programu v tomto případě zajistí, aby byl zahrnut (anglický termín include) tzv. hlavičkový soubor iostream.h, který v jazyku C++ (obvykle) umožňuje proudové funkce pro vstup a výstup používat. 7. varianta: 8. 9. 10. 11. 12.
#include <stdio.h> main() { printf(ťVitejte v kurzu jazyka C++\nť); }
Tento program využívá hlavičkového souboru stdio.h (standardní input/output), který se používá i v jazyku C a jednu jeho funkci pro výstup ( printf) na standardní výstupní zařízení. Příklad 1.2. Zapišme tentýž text jako v příkladu do souboru, který např. pojmenujeme out.dat.
Řešení: 1. varianta: 2. 3. 4. 5. 6. 7. 8. 9. 10. 11.
#include main() // Zapis do souboru out.dat { // Nejprve otevřeni souboru out.dat pro zápis ofstream fout(ťout.datť); // a vlastni zápis cout << ťVitejte v kurzu jazyka C++\nť; // Uzavřeni souboru cout.close(); }
První řádek informuje předpřekladač, že má zavést soubor fstream.h, který umožňuje (obvykle) definovat libovolně pojmenované proudové funkce. V našem případě cout. Text za znaky // je v jazyku C++ považován až do konce řádku za poznámku. Je vhodné nestandardní soubory uzavřít (i když v tomto případě po ukončení programu se uzavře automaticky). Chceme-li soubor z nějakého důvodu uzavřít před ukončením programu, je tento příkaz povinný. 12. varianta: 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27.
#include <stdio.h> main() /* Tentýž přiklad prostředky, které jsou obvykle v jazyku C i C++ */ { /* Definice ukazatele typu FILE*/ FILE *fp; /* Otevření souboru out.dat pro zápis*/ fp=fopen(ťout.datť,ťwť); /* Zapis do souboru funkci fprint ze souboru stdio.h*/ fprint(fp,ťVitejte v kurzu jazyka C++\nť); /* Uzavrení souboru*/ fclose(fp); }
V této variantě zápisu jsme použili poznámek uzavřených mezi znaky /* a */ . Oba typy poznámek jsou v C++ použitelné a mohou se libovolně střídat. Je vhodné jednořádkové poznámky uvozovat znaky // a víceřádkové poznámky mezi znaky /* a */. Ukazatel zmíněný v poznámce budeme probírat podrobně v kapitole 4. Důležité upozornění: jazyk C i C++ rozlišují mezi malými a velkými písmeny: tedy FILE, File a file jsou tři různá jména.
Příklad 1.3. Sestavme program pro přečtení dvou hodnot ze standardního vstupního zařízení a jejich přiřazení proměnným x a i.
Řešení: 1. varianta: 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13.
#include main() { float x; // Definice reálné promenne x int i; // Definice celočíselné proměnné i cout << ťCti x, i\nť; // Čteni hodnoty a x a i (zleva doprava) cin >> x >> i; // Tisk hodnoty proměnné x a i (zleva doprava) cout << ť\nx = ť << x << ť i = ť << i << '\n'; }
Tento zápis je dostatečně jasný z poznámek. 14. varianta: 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25.
#include <stdio.h> main() { float x;int i; printf(ťCti x, i\nť); // Čtení hodnoty x a i scanf(ť%f %dť,&x,&i); // Tisk hodnoty x a i printf(ťx = %f i = %d \nť,x,i); }
Zde je nová funkce scanf, která zajišťuje vstup hodnot, zdánlivě složitějším způsobem než cin. Právě tak jako funkce printf využívá tzv. formátu. Zde konverze f zajistí vstup reálné hodnoty a konverze i zajistí vstup celočíselné hodnoty. U proměnných x a i je povinný symbol &. Pamatujme si prozatím, že tento znak ve funkci scanf musí být u jednoduchých proměnných a indexovaných proměnných. Vysvětlení bude možné, až se naučíme pracovat s ukazateli. Příklad 1.4. Sestavme program pro přepočet palců na centimetry a obráceně. Budeme číst číselnou hodnotu a znak: p (převod palců na centimetry), c (převod centimetrů na palce). V případě chybného zadání znaku se vytiskne výstražná zpráva.
Řešení: // Prepocet palcu na centimetry a obracene #include main() {
typedef float real; /*Definice noveho typu (zde v C++ jsme standardní float zamenili nestandardnim, ale v jinych jazycich obvyklym slovnim symbolem real*/ const real PREPOCET=2.54; // Definice konstanty real x,palec,cm; char ch=0; //Definice promenne typu char s pocatecnim prirazenim cout << ť\nCti délku a co je zadano: p -- palce c -- cm\nť; cin >> x >> ch; if(ch=='p') // Prepocet palcu na cm { palec=x;cm=PREPOCET*x; } else if(ch=='c') // Prepocet centimetru { cm=x;palec=x/PREPOCET; } if(ch=='p'|| ch=='c') cout << palec << ť\ť = ť << cm << ťcm\nť; else cout << ť\n Chybne zadani!!!\nť; }
V programu jsme použili definici nového typu, počáteční přiřazení (které se okamžitě realizuje při překladu zdrojového programu), definici symbolické konstanty (na rozdíl od proměnné se symbolické konstantě nesmí v průběhu plnění přiřadit nová hodnota), přiřazovacího příkazu (kdy v nejjednodušším případě se proměnné na levé straně přiřadí hodnota výrazu na straně pravé) a podmíněného příkazu: if (pravda) příkaz; else jiný_příkaz; Je-li hodnota pravda různá od nuly, provede se příkaz, je-li rovna nule provede se jiný_příkaz. Příkaz i jiný_příkaz musí končit středníkem a příkaz i jiný_příkaz může být jeden příkaz, nebo více příkazů - tzv. složený příkaz - uzavřených do složených závorek. (Ty v jazyku C a C++ nahrazují slovní symboly begin a end známé z jiných programovacích jazyků.) V podmínce if(ch=='p' || ch=='c') se zjiš»uje, je-li ch rovno () znaku p, nebo ( || disjunkce nebo-li logický součet v jazyku C i C++) je-li rovno znaku c. Pozor na operátor rovná se () a operátor přiřaď () začátečníkům (a nejenom jim) se někdy pletou. Je-li větev za else prázdná, může se vynechat (včetně slovního symbolu else). V příkazu cout << palec << ť\ť =ť << cm << ťcm\nť; jsme potřebovali zapsat v řetězci znak uvozovka ( ť). To jazyk C a C++ umožní předřazením znaku zpětné lomítko (\) před znak uvozovka. Příklad 1.5. Sestavme program pro tutéž úlohu, ale použijme tzv. přepínače. Řešení: // Piseme jen vlastni algoritmus bez deklaraci, ktere se nemeni //... switch(ch) { case 'p': palec=x;
cm=PREPOCET*x; cout << palec << ť\ť = ť << cm << ťcm\nť; break; case 'c': cm=x; palec=x/PREPOCET; cout << palec << ť\ť = ť << cm << ťcm\nť; break; default: cout << ť\n Chybne zadani!!!\nť; break; } // ...
Přepínač pracuje tak, že se procházejí jednotlivé alternativy case a nalezne-li se shoda mezi výrazem za slovním symbolem switch a výrazem za case, provedou se příkazy za tímto case. Pak by se prováděly další příkazy bez ohledu na další case; abychom tomuto nežádoucímu jevu zabránili, musíme na konci každé alternativy přepínače umístit příkaz break, který přepínač ukončí. Alternativa uvedená slovním symbolem default se provede v případě, že nevyhovuje žádná jiná alternativa. Alternativa default je nepovinná a příkaz break je v ní zbytečný, ale je zvykem jej na toto místo psát.. Příklad 1.6. Sestavme program pro přepočet palců na centimetry a obráceně. Budeme číst číselnou hodnotu a znak ( p - převod palců na centimetry, c - převod centimetrů na palce, k konec čtení) a budeme je postupně zapisovat do souboru muj_s.dat. V případě, že čteme chybný znak, program bude požadovat opravu. Po přečtení hodnot soubor uzavřeme pro zápis, otevřeme pro čtení, budeme z něj číst a výsledky tisknout na standardním výstupním zařízení.
Řešení: // Vstup a vystup do souboru v C++ /* Program ocekava vstup do promenne ch a x, zapise vysledky do souboru muj_s.dat, ktery predtim otevre. Po ukonceni zapisu (predpoklad cte se pismeno k a x je libovolne) se soubor zavre pro vystup a otevre pro vstup. Vypoctene hodnoty se prectou a vypisi na obrazovce. Prepocitavaji se palce na cm a obracene */ #include #include main() { const float prepocet=2.54; float x=1,palec,cm; char ch=0; // Vstup a zapis do souboru muj_s.dat ofstream fout(ťmuj_s.datť); while(1) { ZNOVA: cout << ť\nZadejte, prosim, co se ma provadet:\nť << ťp - zadana hodnota v palcich\nť << ťc - zadana hodnota v cm\nť << ťk - konec vypoctu\nť << ť zadana hodnota\nť; cin >> ch >> x; switch (ch){
case 'p': palec=x; cm=x*prepocet; break; case 'c': cm=x; palec=x/prepocet; break; case 'k': goto KONEC; default: cout << ť\n Chybny udaj zadava se c, p nebo k. Opakujte cteni\nť; x=0; ch='\0'; goto ZNOVA; } // Zapis do souboru fout << palec << '\n'; fout << cm << '\n'; } // Uzavreni souboru muj_s.dat pro zapis KONEC: fout.close(); // Ctou se data ze souboru muj_s.dat cout << ť\n A nyni hodnoty: palec, cm vytisknu\nť; ifstream fin(ťmuj_s.datť); while(!fin.eof()) // ! je negace logickeho vyrazu { fin >> palec; if(!fin.eof()){ fin >> cm; cout << setw(6) << setprecision(2) << palec << ť\ť = ť << cm << ť cm\nť; } } // Uzavreni souboru muj_s.dat fin.close(); cout << ťKonec programu.\nť; }
U příkazu cout jsme použili tzv. manipulátorů (v systému, kde byly laděny programy pro tato skripta, jsou manipulátory definovány v souboru iomanip.h. Např. manipulátor setw(6) použitý v cout nastaví šířku slova na výstupu na hodnotu 6. Manipulátor setprecision(2) nastaví přesnost pro reálné hodnoty na 2 desetinná místa. Hlavičkový soubor iostream.h je součástí souboru fstream.h, takže nemusí být uveden. V programu jsme použili cyklus while. Jeho význam je jednoduchý. Příkaz uvedený za příkazem (eventuálně příkazy uzavřené v příkazových závorkách) while se provádí tak dlouho, dokud je výraz v závorkách za slovem while různý od nuly. V našem případě jsme použili tedy nekonečný cyklus a k ukončení jsme použili příkaz skoku a návěští, které se odděluje od příkazu, ke kterému se má provést skok, dvojtečkou. Je dobrou praxí, používat příkazů skoku co nejméně, protože někdy znepřehledňují program. V našem případě je tento příkaz vhodný v místě, kde kontrolujeme správnost vstupu. V druhém případě, kdy pomocí tohoto příkazu ukončujeme program, je lepší využít příkazu while.
Příklad 1.7. Sestavme program pro tisk tabulky Fahrenheit - Celsius. Pro přepočet použijte vzorce 5 C=
(F-32) 9
. Tabulku tiskněme ve tvaru: Stupne Fahrenheita dddd dddd ...
Stupne Celsia ddd.dd ddd.dd ...
kde d je číslice, znaménko minus nebo prázdný znak.
Řešení: /*Tisk tabulky prevodu teplot ze stupnu Fahrenheit na stupne Celsius pro fahr = 0, 20,..., 300 */ #include #include main() { // Definice konstant pro dolni a horni mez a krok zmeny const int DOLNI=0,HORNI=300,KROK=20; float fahr=DOLNI; // Definice promenne fahr s pocatecnim prirazenim; float cels; /* Definice promenne, ktera bude nabyvat hodnot stupnu Celsia */ // Tisk zahlavi tabulky cout << ť Stupne Fahrenheita Stupne Celsia\nť; /*Prikaz while, ktery se bude provadet dokud fahr bude mensi nebo rovno 300*/ while(fahr<=HORNI) { cels=5./9.*(fahr-32); cout << ť ť << setw(4) << fahr << ť ť << setw(5) << setprecision(2) << cels << '\n'; fahr=fahr+KROK; } }
Program je snad opět z poznámek srozumitelný. V příkazu while se provádí více příkazů, a proto jsou uzavřeny do složených (příkazových) závorek. Je nutno upozornit na zlomek 5/9, který jsme zapsali v přiřazovacím příkazu 5./9. a mohli jsme ho zapsat i ve tvarech: 5.0/9.0 nebo 5./9 popř. 5/9. apod. Nesmíme jej však zapsat ve tvaru: 5/9 To je v jazyku C i C++ tzv. celočíselné dělení. Výsledkem 5/9 je celočíselný výsledek dělení (po odříznutí zlomkové části) a to je nula. Právě tak výsledek dělení 3/2 je hodnota 1, výsledkem 11/5 je hodnota 2 atd. Fortranisté jsou na tuto skutečnost zvyklí a vědí, že se celočíselné dělení dá v mnoha případech s výhodou použít, ale v mnoha úlohách může být jeho přehlédnutí zdrojem těžko objevitelných chyb.
Na tomto místě znovu opakujeme, že v jazycích C++ a C je pravdivostní hodnota pravda realizována hodnotou různou od nuly a hodnota nepravda hodnotou nula. Hodnoty true a false známé z jiných programovacích jazyků tedy v C++ i C explicitně neexistují. 2. Příkazy cyklu for a do-while Příkazy cyklu jsou velmi důležitou a naštěstí nijak komplikovanou částí každého programovacího jazyka. V minulém odstavci jsme již probrali příkaz while a nyní se zmíníme o příkazu for. Význam ukážeme opět na příkladu. Příklad 1.7 naprogramujeme pomocí tohoto příkazu. /*Tisk tabulky prevodu teplot ze stupnu Fahrenheit na stupne Celsius pro fahr = 0, 20,..., 300 */ #include #include main() { // Definice konstant pro dolni a horni mez a krok zmeny const int DOLNI=0,HORNI=300,KROK=20; // Tisk zahlavi tabulky cout << ť Stupne Fahrenheita Stupne Celsia\nť; // Prikaz for for(int fahr=DOLNI;fahr<=HORNI;fahr=fahr+KROK) { cout << ť ť << setw(4) << fahr << ť ť << setw(6) << setprecision(2) << 5./9.*(fahr-32) << '\n'; } }
Význam příkazu for lze popsat pomocí příkazu while následovně: //... fahr=DOLNI; while(fahr<=HORNI) { cout ...; fahr=fahr+KROK; } //...
V příkazu jsme využili možnosti definice typu parametru cyklu přímo v příkazu for; to je v jazyku C++ možné. Definice proměnné pak platí až do konce funkce nebo bloku ve funkci (podrobněji v kap. 3 v čl. 3.1 a 3.2). Samozřejmě, že jsme mohli psát //... int fahr; //... for(fahr=DOLNI;fahr<=HORNI;fahr=fahr+KROK){//...
ale i //... int fahr=DOLNI; //... for(;fahr<=HORNI;fahr=fahr+KROK)
Z ukázek je vidět, že v příkazu for lze jednotlivé složky příkazu vynechávat, podle toho, jak se nám hodí. Dalším příkazem cyklu v jazyku C++ je cyklus do-while. Syntaxe příkazu je do příkaz while(výraz) Nejprve se provede příkaz a pak se vyhodnotí výraz. Je-li pravdivý (tedy přesně řečeno různý od nuly) příkaz se provede znovu atd. Je-li nepravdivý (roven nule), cyklus se ukončí. Program z příkladu 1.7 můžeme pomocí příkazu do-while napsat takto (Je samozřejmé, že příkaz může být složený): /*Tisk tabulky prevodu teplot ze stupnu Fahrenheit na stupne Celsius pro fahr = 0, 20,..., 300 */ #include #include main() { // Definice konstant pro dolni a horni mez a krok zmeny const int DOLNI=0,HORNI=300,KROK=20; int fahr=DOLNI; //Definice promenne fahr s pocatecnim prirazenim // Tisk zahlavi tabulky cout << ť Stupne Fahrenheita Stupne Celsia\nť; // Prikaz do-while do { cout << ť ť << setw(4) << fahr << ť ť << setw(6) << setprecision(2) << 5./9.*(fahr-32) << '\n'; fahr=fahr+KROK; }while(fahr<=HORNI); }
V příkazu do-while se provede příkaz alespoň jednou, protože tzv. zkouška konce se provádí po provedení příkazu.
3. Několik jednoduchých programů V této části si ukážeme několik jednoduchých programů pro práci se znaky.
3.1 Vstup a výstup po znacích V knihovnách jazyka C++ existují funkce, které po vyvolání přečtou nebo zapíší jeden znak. Uveďme některé: •
Funkce getchar() přečte při vyvolání vstupní znak z klávesnice a vrátí jej jako svoji hodnotu. Tedy po provedení příkazu c=getchar() se proměnné c přiřadí znak ze vstupu.
• • •
Funkce putchar(c) zapíše hodnotu c na obrazovku. Příkaz getc(fp) čte znak ze souboru fp, tedy c=getc(fp) přiřadí proměnné c znak ze souboru fp. Příkaz putc(c,fp) zapíše hodnotu c do souboru fp.
Řekli jsme si, že funkce getchar čte z klávesnice (přesněji ze standardního vstupu) a putchar vypisuje znakové hodnoty na obrazovku (přesněji na standardní výstup). To ovšem můžeme snadno v moderních operačních systémech změnit tzv. přesměrováním, kdy vyvoláním pracovního programu, který se např. jmenuje mujprog, ve tvaru mujprog < mujvst > mujvys zajistíme, že standardním vstupem pro něj bude soubor mujvst a výstupem mujvys. Totéž samozřejmě docílíme, budeme-li ve zdrojovém programu mujprog.cpp psát příkazy getc(mujvst) a putc(nějaká znaková veličina, mujvys).
3.2 Kopírování souboru Příklad 1.8. Napišme program který znak po znaku kopíruje vstup na výstup.
Řešení: 1. varianta: 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14.
#include<stdio.h> /* Kopie ze standardniho vstupniho souboru na standardni vystupni soubor*/ main() { int c; c=getchar(); while(c!=EOF) { putchar(c); c=getchar(); } }
Relační operátor != má význam . Cyklus while tedy probíhá, dokud se nepřečte znak konec souboru, pak se cyklus ukončí. Znak konec souboru pro konkrétní systém je uložen v souboru stdio.h. Je označen jménem EOF. Funkce getchar vrací celočíselnou hodnotu, a proto i proměnnou c jsme definovali jako celočíselnou ( typ int) a ne typu char. Je to stručně řečeno proto, že některé znaky nejsou typu char (např. znak konce souboru EOF). 15. varianta: Programátor v jazyku C nebo C++ ovšem napíše program stručněji: #include <stdio.h> main() // Kopirovani vstupu na vystup strucneji {
int c; while((c=getchar())!=EOF) { putchar(c); } }
Program nejprve v příkazu while přiřadí proměnné c hodnotu a pak se testuje, je-li tato hodnota různá od EOF. Je-li různá, provede se příkaz putchar, není-li různá, cyklus a program se ukončí. Protože operátor přiřazení (=) má nižší prioritu než operátor (!=), musí být přiřazovací příkaz uzávorkován. V jazyku C a C++ je mnoho možností, jak psát stručné programy. Necháme-li se však touto vlastností , může se nám stát, že budeme psát zcela nesrozumitelné programy, kterým, po čase, nebudeme rozumět ani my sami.
3.3 Počítání znaků Příklad 1.9. Napišme program, který spočítá znaky ve vstupním textu.
Řešení: #include<stdio.h> main()// Pocitani znaku ve vstupnim textu { long nc=0; while(getchar()!=EOF) ++nc; printf(ťPocet znaku ve vstupnim textu = %ld\nť,nc); }
V programu se objevil nový operátor ++, který znamená zvětšení o jednotku. Je možné psát nc=nc+1, ale ++nc je stručnější a mnohdy efektivnější. Operátory inkrementace mohou být prefixové ++nc nebo postfixové nc++.Rozdíl si vysvětlíme později. Vedle operátorů inkrementace máme i operátory dekrementace -, které se obdobně využívají při odečítání jedničky. V programu jsem využili nového typu long, který je stručnějším zápisem long int; u typu long je zaručeno, že že maximální hodnota tohoto typu nebude menší než maximální hodnota typu int. Konverze v printf a scanf pro tento typ je %ld .
3.4 Počítání řádků Příklad 1.10. Sestavme program, který spočítá řádky zapsané v souboru muj_sou . Předpokládáme, že každý řádek je ukončený znakem nový řádek ( \n ). Řešení: #include <stdio.h> main() // Pocitani radku { int zn,nr=0; // zn - cteny znak, nr - pocet radku FILE *ms; // Definice ukazatele typu FILE ms=fopen(ťmuj_souť,ťrť); // Otevreni souboru while((zn=getc(ms))!=EOF) if(zn=='\n') ++nr; printf(ťPocet radku= %d\nť,nr); fclose(ms); /* Zavreni souboru. V tomto pripade nepovinne. Po ukonceni programu se vsechny otevrene soubory automaticky uzavrou */ }
3.5 Počítání slov Příklad 1.11. Sestavme program, který spočítá slova, řádky a znaky zapsané v souboru msbr. Předpokládáme, že slovo je posloupnost znaků, která neobsahuje znaky mezera, tabelátor (\t), nebo nový řádek (\n). Řešení: #include<stdio.h> main() // Pocitani slov, radek a znaku v souboru msbr { typedef int BOOL; FILE *sb=fopen(ťmsbrť,ťrť); // Otevreni souboru msbr const BOOL ANO=1,NE=0; // Definice logickych konstant int zn,nz,nr,ns; /* zn - cteny znak, nz - pocet znaku, nr - pocet radku, ns - pocet slov */ BOOL ve_slove; /* ve_slove je logicka (Booleovska) promenna, nabyvajici logickych hodnot ANO a NE */ ve_slove=NE; // A priorni predpoklad: nejsem uvnitr slova; nz=nr=ns=0; // Vynulovani hodnot promennych while((zn=getc(sb))!=EOF) { ++nz; // Opakovani: ++nz je totozne s nz=nz+1; if(zn=='\n')++nr; if(zn==' ' || zn=='\t' || zn=='\n') ve_slove=NE; else if(ve_slove==NE) { ve_slove=ANO; ++ns; } } printf(ť\nV souboru je:\n \n pocet znaku = %d,\n pocet radku = %d,\nť, ť pocet slov = %d\nť,nz,nr,ns);
}
Čtenář při troše přemýšlení jistě porozumí popsanému algoritmu. Všimněme si snad jen pro úplnost zřetězeného příkazu nz = nr = ns = 0;
který se realizuje takto nz = (nr = (ns = 0));
a pro zopakování toho, že v jazycích C a C++ neexistují tzv. logické (Booleovské) proměnné: chceme-li je explicitně použít, musíme to realizovat např. tak, jako v tomto programu.
4. Pole Příklad 1.12. Sestavme program, který spočítá výskyty jednotlivých číslic, mezer, tabelátorů, nových řádek a ostatních znaků. Řešení: #include<stdio.h> #include// Soubor pro ovládání obrazovky v systému TURBO C++ main() // Pocitani cislic a ostatnich znaku { int cis[10],bily_zn=0,ost_zn=0,zn; FILE *ms; ms=fopen(ťvzorek.datť,ťrť); clrscr(); // Mazani obrazovky v systému TURBO C++ for(int i=0;i<10;++i) cis[i]=0; // Nulovani prvku pole // A vlastni realizace algoritmu while((zn=getc(ms))!=EOF) /* Je-li zn >= '0'a zaroven jeli zn <= '9' (operator &&), vypocte se hodnota indexu: c - '0' a k prislusnemu prvku se pricte jednicka */ if(zn>='0' && zn<='9') ++cis[zn-'0']; else if(zn==' ' || zn=='\t' || zn=='\n') ++bily_zn; else ++ost_zn; // Tisk vysledku printf(ť\nCislice Pocet cislic\n\nť); for(i=0;i<10;++i) printf(ť %d %d\nť,i, cis[i]); printf(ť\n\nPocet: bilych znaku: %d a ostatnich %dť,bily_zn, ost_zn); }
V programu je použito jednorozměrné pole tj. vektor cis, jež má deset prvků o indexech měnících se od nuly do devíti. Typické pro pole v jazyku C i C++ je právě indexování od nuly. Mez udaná v definici tedy znamená pocet prvků pole a ne horní mez indexu. To umožňuje v cyklu for psát ostrou nerovnost. V programu jsme použili nový hlavičkový soubor conio.h, který v systému, ve kterém byly programy laděny slouží k ovládání obrazovky; např. funkce clrscr() maže obrazovku.
Příklad 1.13. Sestavme program, který vypočítá součet matic typu (m,n), kde m 5 a n 5 podle vzorce cij = aij+bij, i = 1,2,...m; j = 1,2,...,n. Matice budeme číst ze souboru vzorek.dat a výsledek bude zapsán do téhož souboru.
Řešení: #include<stdio.h> main() { FILE *vz=fopen(ťvzorek.datť,ťrť); //Otevreni souboru pro cteni const int m=5,n=5; int a[m][n],b[m][n],c[m][n];// Definice matic typu(m,n) int ms,ns; // Definice skutecneho poctu radku (ms) a sloupcu (ns) fscanf(vz,ť %d %dť,&ms,&ns); // Cteni hodnot ms, ns for(int i=0;i<ms;++i) // Cyklus i s definici parametru cyklu i for(int j=0;j<ns;++j) // Cyklus j s definici parametru cyklu j fscanf(vz,ť%d ť,&a[i][j]); // Cteni prvku matice A; for(i=0;i<ms;++i) for(j=0;j<ns;++j) fscanf(vz,ť%d ť,&b[i][j]); // Cteni prvku matice B for(i=0;i<ms;++i) for(j=0;j<ns;++j) c[i][j]=a[i][j]+b[i][j]; // Vypocet souctu (matice C) fclose(vz); vz=fopen(ťvzorek.datť,ťwť); fprintf(vz,ťTisk souctu matic: C = A + B\n\nMatice A:ť ť\n=========\nť); for(i=0;i<ms;++i) // Tisk matice A { fprintf(vz,ť\nť); for(j=0;j<ns;++j) fprintf(vz,ť%d ť,a[i][j]); }// Konec Tisku matice A fprintf(vz,ť\n\nMatice B:\n=========\nť); for(i=0;i<ms;++i) // Tisk matice B { fprintf(vz,ť\nť); for(j=0;j<ns;++j) fprintf(vz,ť%d ť,b[i][j]); } // Konec tisku matice B fprintf(vz,ť\n\nMatice C:\n=========\nť); for(i=0;i<ms;++i) // Tisk matice C { fprintf(vz,ť\nť); for(j=0;j<ns;++j) fprintf(vz,ť%d ť,c[i][j]); } // Konec tisku matice C }
Program doplněný poznámkami je snad dostatečně čitelný. Upozorňujeme jen na zápis dvojrozměrných polí, tj. matic, který je oproti většině programovacích jazyků poněkud atypický, ale později uvidíme, že má své výhody. Též je nutno dát pozor na meze indexů. Parametry cyklů i a j jsme definovali při prvém použití způsobem povoleným v C++; standardní způsob zápisu je samozřejmě přípustný.
5. Funkce Funkce v jazyku C i C++ tvoří samostatné programové jednotky, pravě tak jako v jazyku Fortran. Příklad 1.14. Zapišme funkci, která umo'ní celočíselné umocňování celočíselné hodnoty. Umocňování není standardní operací jazyka a bývá součástí matematické knihovny příslušných systémů. Obvykle se jmenuje pow.
Řešení: #include<stdio.h> main() //Testovani funkce umocneni { int umoc(int,int); /* Toto je tzv. prototyp, ktery je v C++ povinny nepredchazi-li nahodou popis funkce jednotce, ve ktere je pouzivana. V prototypu je receno, jakeho je funkce typu a kolik a jakeho typu jsou parametry. */ for(int i=0;i<=10;++i) printf(ť%d na %d = %d\nť,2,i,umoc(2,i)); } int umoc(int m,int n) // Funkce umocnovani m na n { int p=1; //Pocatecni prirazeni hodnoty pomocne (!!!) promenne p for(int i=1;i<=n;++i)p=p*m; //Primitivni zpusob vypoctu mocniny return p; // Vraceni hodnoty funkce (mozny je i zapis return(p)) }
Funkci nebo skupinu funkcí lze překládat samostatně a propojení závisí na konkrétním systému. Program z příkladu 1.14 jsme zapsali jako jeden soubor. Pracujeme-li v operačním systému UNIX a jmenuje-li se soubor ukazka.cc, lze obvykle přelo'it program příkazem c++ ukazka.cc -o uk
kdy pracovní (spustitelný) program se bude jmenovat uk. Budeme-li program z příkladu 1.14 psát jako dva soubory - hlavni.cc a funkce.cc, pak budeme postupovat např. takto c++ -c hlavni.cc funkce.cc
Příkaz zajistí překlad souborů do sestavitelných modulů (majících v unixu postfixovou koncovku o a příkazem c++ hlavni.o funkce.o -o uk
se realizuje sestavení obou modulů do spustitelné verze uk. Kombinací je samozřejmě mnoho, ale práce se soubory a funkcemi je přehledná a praktická. Překlad a sestavení mů'eme však provést najednou zápisem c++ hlavni.cc funkce.cc -o uk
Napíšeme-li jen c++ program.cc funkce.cc bude mít výsledný produkt standardní jméno a.out. V produktech firmy Borland je první případ (překlad souboru ukazka.cpp - koncovka cpp povinná) jasný z menu borlandského prostředí. Druhý případ se realizuje v okně project takto: • • •
otevřeme project např. pod názvem uk.prj (postfixová koncovka prj), ulo'íme do něj (viz menu) soubory hlavni.cpp a funkce.cpp, otevřeme compile a přelo'íme (menu make).
Parametry funkce, tzv. formální parametry, které stojí na místě jednoduché proměnné nebo indexované proměnné (prvku pole), jsou, není-li řečeno jinak,(viz) volány hodnotou; to znamená, 'e se jim po zavolání funkce přiřadí hodnota skutečných parametrů. Hodnota formálních parametrů se uvnitř funkce mů'e jakkoliv měnit, ale hodnota skutečných parametrů zůstane po ukončení funkce nezměněna. Ve funkci mocnina jsme toho vyu'ili a ušetřili jsme jednu proměnnou. // Vyuziti vlastnosti volani hodnotou float mocnina (float x,int n) { for (float p=1;n>0;n--) /* parametr n se meni, ale jemu odpovidajici skutecny parametr zůstane beze změny */ p=p*x; return p; }
Funkce v tomto případě vrací reálnou hodnotu, a proto jsme ji museli definovat jako reálnou (float). Příklad 1.15. Sestavme program pro čtení řádků znakového souboru a tisku nejdelšího z nich. Řešení: Algoritmus mů'eme stručně slovně popsat takto: pokud (existuje další řádek) Jestli'e (je delší ne' poslední nejdelší ) ulo' řádek a jeho délku; Tiskni nejdelší řádek; /* hrad.cpp - Cteni radku ze souboru ms a tisk nejdelsiho z nich. Program testuje soubor funkci frad.cpp */ #include <stdio.h>; int getline(char[],int); /* Prototyp funkce getline. Informace v nem obsazena rika, ze funkce vraci hodnotu typu int a ma dva parametry: jeden parametr je vektor - do nej se ukladaji znaky cteneho radku, druhy parametr je celociselna hodnota urcujici
maximalni pripustnou delku retezce. Funkce mohla byt tez zapsana: getline(char[],int); protoze v takovem pripade se automaticky rozumi, ze funkce vraci hodnotu typu int. Jsme-li priznivci uplne informace, lze vsak maximalisticky psat int getline(char s[],int m_del_r) */ void copy(char s1[],char s2[]); /* Prototyp funkce copy. Funkce kopiruje hodnoty prvku vektoru s1 do prislusnych prvku vektoru s2. Mohli jsme tez psat void copy(chr[],char[]).Slovni symbol void sdeluje kompilatoru, ze se jedna o funkci, ktera nevraci hodnotu. V jinych programovacich jazycich se takove programove jednotce rika podprogram nebo vlastni procedura. Slovni symbol je povinny, nenapiseme-li ho, jsme kompilatorem varovani, ze funkce ma vratit hodnotu typu int a nevraci nic */ FILE *ms; /* Definice tzv. externi promenne, ktera se uziva v ruznych funkcich jednoho nebo vice programovych souboru */ main() { const int MAXDEL=100;// Maximalni delka radku int delka, // bezneho radku max; // delka nejdelsiho radku char radek[MAXDEL], // bezny radek mradek[MAXDEL]; // nejdelsi radek; ms=fopen(ťmsť,ťrť); // otevreni souboru ms pro cteni a zapis max=0; while ((delka=getline(radek,MAXDEL))>0) /* getline vraci delku radku a vektoru radek priradi retezec znaku radku */ if(delka>max) // Je-li cteni radek zatim nejdelsi z ctenych, pak max=delka; copy(radek,mradek); // Zapis radek do mradek
}
fclose(ms); // Uzavreni souboru pro cteni ms=fopen(ťmsť,ťwť); // Otevreni souboru pro zapis if(max>0) //Cetlo se vubec neco? // Tisk radku do souboru ms fprintf(ms,ťRadek %s\nje nejdelsi a ma delku %d\nť,mradek,max);
// Soubor frad.cpp #include<stdio.h>; int i; // Externi promenna, ktera se smi uzivat v celem souboru frad.cpp getline(char s[],int lim) // Cteni radku s maximalni delkou lim { extern FILE *ms; /* Deklarace (ne definice) externi promenne definovane v souboru hrad.cpp (viz pozn. na konci odst. 1.5.1) */ char c; /* V prikazu for je uzita operace konjunkce (tzv. logickeho soucinu) znama z vyrokove logiky. (V C++ se znaci &&) */ for(i=0;i
i=0; while((s2[i]=s1[i])!='\0') i++; }
Poznámky: Pro úsporu místa bylo snahou vše vysvětlit poznámkami přímo v programu. Zde jen tři vysvětlení: •
Řetězec znaků je v jazyku C a C++ automaticky doplňován znakem '\0'. To znamená, 'e napíšeme-li text ťNazdarť kompilátor jej automaticky rozšíří takto: Nazdar\0V případě, 'e vytváříme řetězec sami, nesmíme zapomenout znak '\0'. doplnit. Zde také je nutno upozornit na rozdíl mezi zápisem 'x' a ťxť; 'x' je znak, kde'to ťxť je řetězec o dvou znacích. Sestává ze znaku x a tzv. prázdného znaku \0.
•
•
Není-li u identifikátoru funkce napsán explicitně typ funkce, předpokládá se, 'e funkce je typu int, tedy 'e vrací hodnotu typu int. Nevrací-li např. 'ádnou hodnotu, píšeme typ void. Toto platí i pro hlavní program, který obvykle nevrací 'ádnou hodnotu, a proto má být před identifikátorem main slovní symbol void. Nenapíšeme-li tento slovní symbol, hlásí některé systémy varování, 'e funkce main nevrací hodnotu. Zápisu void před main se pak mů'eme vyhnout tak, 'e zapíšeme na konci hlavního programu příkaz return a vracíme nějakou hodnotu: např. skončil-li program správně, vracíme hodnotu 1, jinak 0 apod. Formální parametry jsme pou'ili ve funkci copy. U formálních parametrů v tomto případě nesmíme uvést rozměr pole. To je u jednorozměrného pole (vektoru) charakteristické. U dvojrozměrného pole je situace trochu slo'itější a v čl. se k této problematice vrátíme.
Zvláštní zmínku si ovšem zaslou'í tzv. externí proměnné, pou'ité v příkladu 1.15.
5.1 Automatické a externí proměnné Proměnné ve funkci main, tj. např. delka, max, radek, mradek jsou lokální v main. ®ádná funkce k nim nemá přístup. Totéž platí o jakékoliv programové jednotce v níž je nějaká lokální proměnná definovaná. Lokální proměnná začne existovat až po vyvolání funkce a její hodnota se ztrácí po ukončení činnosti příslušné funkce. V jazycích C a C++ se těmto proměnným říká automatické proměnné. Při vstupu do programové jednotky jim musí být vždy znovu přiřazena hodnota, protože jinak by obsahovaly hodnotu náhodnou. Mohou se inicializovat, tj. může se jim přiřadit počáteční hodnota ihned při definici. Pak se při každém vstupu do funkce inicializují znovu. V jazyku C se automatická pole inicializovat nesmějí (mají při vstupu do funkce vždy hodnotu nula), avšak v jazyku C++ toto omezení neplatí. Proměnné, které chceme používat ve více funkcích, a nechceme-li nebo nemůžeme-li použít mechanismu formální - skutečný parametr, zavedeme jako tzv. externí proměnné. Tyto proměnné jsou definovány globálně pro celý soubor funkcí a jsou dostupné pro všechny funkce souboru, . Externí proměnné se musí definovat mimo funkce a deklarovat buď explicitně deklarací extern, nebo implicitně svým použitím v příslušné funkci. Externí proměnné a pole se inicializují jen jednou v procesu plnění programu. Neprovede-li se inicializace mají napočátku externí proměnné hodnotu nulovou.
Sestává-li program z více souborů, pak je externí proměnná definována v jednom souboru a lze ji tam použít shora popsaným způsobem; ve druhém souboru se externí proměnná musí deklarovat ve funkci, deklarací extern. V našem programu jsme museli definovat proměnnou ms jako externí. V souboru hrad jsme s ní pracovali bez jakékoliv deklarace, v souboru frad jsme ji museli ve funkci getline explicitně deklarovat. Proměnnou i v souboru frad jsme definovali na začátku souboru a implicitně použili v obou funkcích getline a copy. V druhém případě by bylo vhodnější definovat proměnnou v obou funkcích zvláš», náš záměr zde byl čistě pedagogický. Na tomto místě je třeba upozornit, že extern i a extern int i znamená totéž. Externí proměnné je vhodné užívat uvážlivě, protože mohou být (právě tak jako proměnné v popisu common v jazyku fortran) zdrojem nepříjemných chyb a mohou znepřehledňovat program. Závěrem si uveďme schéma na vysvětlenou: // soubor1.cpp int sp=0; // Definice externi promenne s inicializaci float val[1000]; // Definice externiho pole bez inicializace void main() { extern sp; // Nepovinna deklarace // ... } funkce() { extern float val[]; // Nepovinna deklarace // soubor2.cpp extern sp; // Povinna deklarace extern float val[]; //Povinna deklarace /* Toto byly povinne deklarace externich promennych. Pak se mohou pouzivat ve vsech funkcich souboru. Mohli jsem je ale tez deklarovat jen ve funkcich, ktere dane promenne pouzivaji */ // ...
Poznámka: Pozorný čtenář si jistě všiml, že používáme dva pojmy: definice a deklarace proměnné nebo pole. V jazyku C a C++ se těchto pojmů užívá takto: • •
definice se vztahuje k místu, kde se proměnné skutečně přiděluje pamě», deklarace se vztahuje k místu, kde se popisuje druh proměnné bez přidělení paměti.
5.2 Rekurzívní funkce O funkci říkáme, že je rekurzívní, volá-li sama sebe ještě před dokončením svého algoritmu. Známým a jednoduchým příkladem rekurzívní funkce je výpočet faktoriálu. long fakt(int n) { if(n==0||n==1) return(1); else return (n*fakt(n-1)); }
nebo ještě stručněji long fakt(int n) { return (n==0||n==1)?1:n*fakt(n-1);// }
Na tomto velmi jednoduchém příkladu rekurzívní funkce si můžeme snadno vysvětlit princip výpočtu. Představme si, že funkci fakt zavoláme s parametrem 4. Formálnímu parametru se tedy přiřadí hodnota 4. Provede se zkouška, zda se n rovná nule nebo jedné. To se ale nerovná, protože se rovná čtyřem. Vyvolá se tedy znovu funkce fakt, ale tentokrát s novým parametrem, který má hodnotu n-1, tedy hodnotou tři. V tomto dalším bloku se provede další zkouška, a protože stále není hodnota parametru rovna nule nebo jedničce vyvolá se opět funkce fakt s hodnotou dvě, a pak ještě jednou s hodnotou jedna. Teprve nyní se vrací hodnota jedna, ta se násobí v funkci (podle algoritmu) dvěma, výsledek se opět v funkci násobí třemi a v původně volané funkci čtyřmi. Konečný výsledek je tedy 24. Z tohoto příkladu vidíme, že jsme sice rekurzívním zápisem zápis zjednodušili, ale místo jednoho parametru jsme potřebovali (aniž bychom to explicitně zadali) parametry čtyři. Obecně platí, že rekurzívní zápisy spotřebují více paměti než zápisy nerekurzívní a také více strojového času. Přesto mnohdy je zjednodušení a zpřehlednění zápisu tak významné, že rekurzivitu využíváme.
6. Závěrečné poznámky k 1. kapitole Po ukončení 1. kapitoly jsme schopni psát i dosti komplikované programy. Nejdůležitější skutečnosti, které vyžaduje zápis programu v C++, byly v této kapitole uvedeny; zde uvedeme jeden nový poznatek, jeden upřesňující poznatek a jedno důležité opakování. 1. Proměnné v jazyku C++ se nemusí definovat na začátku funkce, ale před prvním použitím ve funkci. Definice veličin mohou být tedy rozptýleny v celé operační části funkce nebo chcete-li v celém funkce. 2. V příkazu printf, fprintf, scanf a fscanf se používá tzv. formátová specifikace. Částečně jsme se s ní seznámili. Nyní se zmíníme o konverzích, o kterých jsme ještě nemluvili, popř. mluvili, ale příliš stručně. o %o Argument se konvertuje na hodnotu v osmičkové soustavě (printf) nebo se očekává hodnota v osmičkové soustavě ( scanf). o %x Totéž jako v předchozím případě, ale nyní se jedná o hodnotu v šestnáctkové soustavě. o %i Má stejný význam jako konverze %d, tj. očekává se nebo se tiskne celočíselná hodnota. o %u Argument se konvertuje do desítkové soustavy bez znaménka, nebo se očekává hodnota bez znaménka. o %e Argument je reálný a tiskne se desítkově ve tvaru [-]m.mmmmmmE±nn, kde hodnota za desetinnou tečkou je specifikovaná přesnost. Implicitní přesnost je 6. Pro funkci scanf má konverze %e tentýž význam jako konverze %f. o %g Použije %e nebo %f podle toho, co je kratší. Nevýznamné nuly se netisknou. Pro funkci scanf má konverze tentýž význam jako konverze %f. o %c Zajistí čtení nebo tisk jednoho znaku.
Písmeno l uvedené před d, o a x označuje, že v seznamu argumentů funkcí odpovídá této konverzi forma (long) a u konverzi f nebo e označuje, že odpovídající argument není typu float, ale double, tj., že nabývá reálné hodnoty s dvojnásobnou přesností. Písmeno L označuje v tomto případě, že odpovídající argument je typu long double. Zastavme se ještě u tisku řetězce. Uvedeme nejzajímavější příklady aplikace konverze %s pro tisk řetězce: Nejneobhospodarovavatelnejsi %s Nejneobhospodarovavatelnejsi 10%s Nejneobhospodarovavatelnejsi 35%s Nejneobhospodarovavatelnejsi .9%s Nejneobho 3. Každá funkce volaná v jiné funkci musí mít před místem volání tzv. prototyp. Lze obejít hlavičkovým souborem, ve kterém uvedeme všechny prototypy funkcí a k souboru připojíme příkazem #include o o o o
7. Cvičení 1. [1.] Sestavte programy z příkladů 1.4. a 1.5. tak, že příkazy (funkce) cin a cout nahradíte voláním funkcí scanf a printf. 2. [2.] Pokuste se u všech u programů v příkladech 1.3 až 1.5 definovat konverzi pro výstup hodnot na výstupu (nevyužívat tedy standardní výstupní konverze). Návod: U příkazu printf např. %3d zajistí tisk nejméně tří znaků celočíselné hodnoty, %6.1f zajistí tisk nejméně šesti znaků reálné hodnoty s jednou číslicí za desetinnou tečkou. Mnohé lze vynechat - např.%6f zajistí tisk nejméně šesti znaků reálné hodnoty %.4f požaduje dvě čísla za desetinnou tečkou, ale bez omezení šířky. %% znamená vytištění znaku % . o U příkazu cout je možno použít manipulátorů. 3. [3.] Sestavte program podle příkladu 1.6 tak, že použijete soubor stdio.h, funkci fprintf, fscanf a místo návěští KONEC a příslušného příkazu skoku zajistěte ukončení programu příkazem while. 4. [4.] Sestavte program pro převod teplot tak, aby tiskl tabulku v opačném pořadí, tj. od 300 do 0 stupňů Fahrenheita. 5. [5.] Sestavte program pro výpočet částečného součtu řady o
Sn = 1+x+[(x2)/ 2!]+…+[(xn)/( n!)], kde n je nejmenší číslo, pro které platí |[(xn)/( n!)]| < ε Čísla x a ε jsou dána vstupními daty. Vypočet realizujte: o
příkazem do-while,
o o
příkazem while, příkazem for.
Návod: Pro sestavení algoritmu je nutné si uvědomit, že každý příspěvek k částečném součtu řady lze vypočítat z předchozího příspěvku ze vztahu členi = x/ičleni-1 a není tedy nutné počítat ani mocninu ani faktoriál. 6. [6.] Sestavte program pro realizaci Euklidova algoritmu výpočtu největšího společného dělitele (NSD) dvou přirozených čísel. Výpočet realizujte: o příkazem while, o příkazem for, o příkazem do-while. Návod: Vyjděte z vlastností NSD: pro x > y
platí
NSD(x,y) = NSD(x-y,y),
pro x < y
platí
NSD(x,y) = NSD(x,y-x),
pro x = y
platí
NSD(x,y) = x = y.
7. [7.] Sestavte program na počítání znaků mezera, tabelátor a nový řádek v souboru m_sbr 8. [8.] Sestavte program na kopírování souboru m_vst do souboru m_vys tak, že každý řetězec znaků mezera v souboru m_vst zamění za jediný znak mezera a znak tabelátor zamění šesti mezerami. 9. [9.] Sestavte program, který na každý řádek výstupního souboru zapíše maximálně deset slov ze vstupního souboru. 10. [10.] Upravte program z příkladu 1.11 tak, aby slovo zpracovával podle definice: Slovo je posloupnost písmen, číslic, znaků '_' a '.' začínající písmenem. 11. [11.] Sestavte program pro transponování čtvercové matice A stupně n, kde n 10. 12. [12.] Sestavte program pro výpočet stopy matice stupně n, kde n 20. Návod: Použijte vzorce s = ∑i = 1naii. 13. [13.] Sestavte program pro násobení matic C(m,n) = A(m,l) ·B(l,n). Návod: cij = cij+∑k=1laikbkj, i = 1,2,.…,m; j = 1,2,…,n. 14. [14] Sestavte program, který zamění znaky tabelátor (\t) ze vstupu za správný počet mezer tak, aby vyplnil prostor po následující zarážku tabelátoru. Předpokládejte, že zarážky tabelátoru jsou pevné, např. na každé šesté pozici. 15. [15.] Sestavte program, který zamění řetězce znaků mezera za nejmenší počet znaků tabelátor a mezera tak, aby se zachovala grafická úprava textu.
16. [16.] Sestavte program, který zalomí dlouhé vstupní řádky za posledním znakem, který se vyskytne před n-tou pozicí vstupního řádku. n je parametr. 17. [17.] Napište předchozí program tak, aby řádky byly zarovnány. 18. [18.] Sestavte program, který vynechá z C++ programu všechny poznámky. 19. [19.] Sestavte funkci malep(chr z), která vrátí hodnotu z, není-li z písmeno, a malé písmeno, je-li z písmeno. Použijte funkci malep v programu, který konvertuje text na malá písmena. Návod: Využijte vlastnosti kódu ascii, kde číselné kódy malých a velkých písmen mají konstantní rozdíl; obě abecedy jsou spojité. 20. [20.] Sestavte funkci obrat(char s[]), která obrátí znakový řetězec s. Funkci použijte v programu. 21. [21.] Sestavte funkci sks(int x[],int y[],n) a void sks1(int x[],int y[],int s,n) pro výpočet skalárního součinu ∑i = 1nxiyi a použijte je v programu. 22. [22.] Sestavte rekurzívní funkci pro operaci umocňování podle následujícího efektivního rekurzívního algoritmu. 23. mocnina(int x,int n) 24. { 25. int v=1; 26. while (n) 27. { 28. while(n/2*2==n) 29. { 30. n /= 2; x *= x; 31. } 32. n--;v *= x; 33. } 34. return v; 35. }
Kapitola 2 Typy, operátory, výrazy V této kapitole nebudeme opakovat již probrané objekty, jen si řekneme některé podrobnosti.
1. Jména, typy, konstanty 1.1 Jména Jméno, nebo v odborné terminologii synonymum identifikátor, je v jazyku C++ posloupnost maximálně 31 písmen, číslic a znaků podtržení ( _) začínající písmenem. Znak _ se považuje za písmeno. Lépe je používat jména kratší než delší (maximálně osm, spíše šest). Slovní symboly (např. for, else, int, while apod. jsou tzv. vyhrazená slova, která se nesmějí používat jako jména. Malá a velká písmena se v C++ rozlišují: haha, Haha a HaHa jsou tři různá jména a For není slovní symbol.
1.2 Typy V jazyku C++ jsou následující základní standardní typy: char, int, unsigned, float a double, kterými definujeme proměnné, které mohou po řadě nabývat hodnoty: znak, celočíselná hodnota, celočíselná hodnota bez znaménka, reálná hodnota a reálná hodnota s dvojnásobnou přesností. Celočíselná proměnná může být označena kvalifikátorem short nebo long. Definice nebo deklarace: short int i a short i jsou totožné; právě tak long j a long int j. Dlouhé a krátké hodnoty můžeme definovat i pro typ unsigned: např. short unsigned u a long unsigned u (nebo unsigned short, unsigned long). Je možné také psát unsigned char nebo char unsigned. Dlouhá forma je možná i u proměnných typu double: např. long double ld. Můžeme odvozovat další typy např. struktury, unionya třídy. Na tomto místě se zmíníme o tzv. výčtovém typu. Dovoluje zpřehlednit program. Výčtový typ lze definovat několika způsoby. Uvedeme ten, který je v C++ nejjednodušší. Příklad 2.1. Sestavme program , který přečte hodnotu proměnné barva a bude-li barva červená, resp. žlutá, resp. zelená, bude se tisknout text STUJ, resp. PRIPRAV SE, resp. VOLNO. Řešení: #include main() { enum BARVA{cervena,zluta,zelena}; // Deklarace typu BARVA BARVA semafor; // Definice promenne semafor typu BARVA cin >> semafor;/* Cteni hodnoty. Neni-li receno jinak, plati cervena == 0, zluta == 1 a zelena == 2 */ switch(semafor) { case cervena: cout << ťSTUJ\nť;break; case zluta: cout << ťPRIPRAV SE\nť;break;
case zelena: } }
cout << ťVOLNO\nť;break;
Mohli bychom samozřejmě pracovat s čísly 0, 1, 2, ale shora uvedeným způsobem se zápis algoritmu zpřehlední. Implicitně zadané hodnoty položkám typu se mohou změnit, např. zápis enum DEN{po=1,ut,st,ct,pa,so,ne} zajistí, že položka po == 1, ut == 2, st == 3 atd. Zápis enum D{po,ut,st,ct,pa=1,so=12,ne} zajistí, že ut == pa == 1, so == 12, ne == 13 .
1.3 Konstanty 4444444444444 C++ Matematický zápis Zápis v C++ -123 -123 10-4 1e-4 2,345 2.345 x 'x' -0,5 -.5 (77)8 077 1,25 1.2e5 (1A)16 0x1a
¯ 4444444444444 Matematický zápis Zápis v
Zápisy číselných konstant jsou jasné z předchozí tabulky. Celočíselné konstanty se píší tak jako v matematice. U reálných konstant píšeme desetinnou tečku a exponent označujeme písmenem e nebo E (jako v jiných programovacích jazycích) a znaky píšeme mezi apostrofy. V C++ můžeme zapisovat i osmičkové nebo hexadecimální konstanty, tak jak je v tabulce uvedeno. Některé nezobrazitelné znaky je možné zapsat pomocí změnových posloupností, např. \n (nový řádek), \t (tabelátor), \0 (prázdný znak),\\ (zpětné lomítko), \' (apostrof) apod. Mimoto se může zápisem '\ddd' realizovat libovolný bitový řetězec, kde (d je osmičková číslice), popř.'\xdd', kde d je hexadecimální číslice. Napíšeme-li v programu konstantu desítkovou konstantu 48 nebo '\60' nebo '\x30', znamená to v kódu ascii znak nula; napíšemeli desítkově 95 nebo '\137' nebo '\x5f', je to v kódu ascii znak _. Definujeme-li např. v programu symbolickou konstantu: const char nova_str='\14'; definovali jsme novou stránku (pracujeme-li v kódu ASCII).
2. Operátory V tomto odstavci probereme podrobnosti o aritmetických, logických a dalších operátorech a zmíníme se o prioritách operací v C++
2.1 Aritmetické operátory 1. Operátor o kterém jsme ještě nehovořili je operátor operace . Např. 5%4 - > 1, 3%4 - > 3, 10%2 - > 0. Příklad 2.2. Sestavme část programu, která zjistí, je-li rok přestupný.
Řešení: Rok je přestupný, je-li letopočet dělitelný čtyřmi a zároveň není dělitelný stem. Rok je však vždy přestupný, je-li letopočet dělitelný čtyřmi sty. //... if(rok%4 == 0 && rok%100 != 0 || rok%400 == 0) cout << ťRok je prestupny \nť; else cout << ťRok neni prestupnyť; //... \
V programu jsme použili operace konjunkce a disjunkce, známých z výrokové logiky. 2.Operátory inkrementace a dekrementace Operátory jsme již používali v 1. kapitole a nyní naše znalosti doplníme o velmi důležitou podrobnost. • •
[a)] Prefixový operátor se aplikuje před použitím proměnné, u které je zapsán. [b)] Postfixový operátor se aplikuje po použití proměnné, u které je zapsán.
Příklad 2.3. Napišme funkci, která z řetězce r vymaže znaky zn.
Řešení: // Funkce vymaze znaky zn z retezce r void vymaz(char r[],char zn) { int i,j; for(i=j=0;r[i]!='\0';i++) if(r[i]!=zn) r[j++]=r[i]; // !!!! r[j]='\0'; }
V programu je naznačena velká modifikovatelnost příkazu for. Prosím čtenáře, aby se zamyslel, proč není možné v tomto případě napsat r[++j]=r[i];
2.2 Přiřazovací operátory V jazyku C++ samozřejmě můžeme, právě tak jako v jiných programovacích jazycích psát přiřazovací příkazy typu: i = i + 2; x = x * 2; z = z / y;
tyto příkazy však v C++ můžeme psát stručněji, takto: i += 2; x *= 2; z /= y; Těchto operátorů je celá řada, kromě uvedených např. ještě %=. Zbylé najde čtenář v článku , který pojednává o prioritách operací v C++. Důležité je ovšem vědět, že tyto operace mají velmi nízkou prioritu (stejnou jako přiřazovací příkaz), a proto např. x *= y + 1;
znamená totéž jako příkaz x = x * (y + 1) Těmito příkazy nejen zkracujeme zápis, ale můžeme i optimalizovat výpočet, protože výraz_1 výraz_1 op výraz_2;
je totožný se zápisem výraz_1 op= výraz_2;
ale výraz_1 se vyhodnocuje jen jednou. Pak ovšem příkaz yypro[yypv[p3 + p4]+yypg[p1 + p2]] += 2;
vyskytuje-li se v cyklu, je nejen stručný, ale též šetří strojový čas.
2.3 Podmíněný výraz V jazycích jako Algol 60 nebo Algol 68 existuje konstrukce zvaná podmíněný výraz. Taková konstrukce existuje i v C++. Příkaz if(a > b) z = a; else z = b; můžeme zapsat stručněji z = a > b ? a : b; nebo (jak je zvykem) méně stručně, ale přehledněji z = (a > b) ? a : b; Příklad 2.4. Sestavme program, který tiskne n prvků pole tak, že na řádku je 10 prvků. Každý prvek je oddělen od druhého mezerou a každý řádek (i poslední) je ukončen právě jedním znakem . Pole a jeho velikost definujme jako externí proměnné. Řešení: #include<stdio.h> int a[15]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},n=15; main() { for (int i=0;i
nebo #include int a[15]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},n=15; main() { for (int i=0;i
Napsané varianty programu jsou trochu školními případy. Chtěl jsem na nich opět nejen zopakovat použití externích proměnných a to, že polím je možné přiřadit počáteční hodnoty, ale i jak se to dělá.(Podrobněji v čl. .) Proměnná n je ovšem definována jako externí celkem zbytečně. V druhém případě musel být výraz (i%10==9||i==n-1) ? '\n' : ' '
uzavřen do okrouhlých závorek, protože jinak systém reaguje chybně (viz).
2.4 Relační a logické operátory Relační operátory jsou > >= < <= Všechny mají tutéž prioritu. O stupeň nižší prioritu mají operátory rovnosti a nerovnosti == !=jejichž priority se také rovnají. Výrazy spojené logickými operátory konjunkce && a disjunkce || se vyhodnocují zleva doprava a vyhodnocování se zastaví v okamžiku, kdy je známa pravdivost nebo nepravdivost výsledku. Priorita operátoru && je vyšší než priorita operátoru || a oba mají nižší prioritu než operátory rovnosti. Operátor negace ! jako unární operátor má jednu z nejvyšších priorit. Logické operátory po bitech není možné aplikovat na typy float nebo double. Mají vyšší prioritu než operace disjunkce a konjunkce. Jsou to operátory: &
operátor logického součinu po bitech
|
operátor logického součtu po bitech
^
operátor nonekvivalence po bitech
<<
operátor posunu vlevo
>>
operátor posunu vpravo
~
unární operátor jednotkového doplňku
•
[a)] c = n & 0177 Nuluje se vše s výjimkou posledních sedmi bitů hodnot proměnné c
•
[b)] x = x | MASKA Tzv. .Slouží k nastavení binárních jedniček do míst, kde jsou jedničky v proměnné nebo konstantě MASKA
•
[c)] n << i Bity výrazu (proměnné) n se posunou o i míst vlevo. Zprava se doplní nulami.
•
[d)] n >> i Totéž co v předchozím případě, ale posun se provádí vpravo. Nulami se doplní zleva v případě, že proměnná n je typu unsigned. Není-li proměnná typu unsigned mohou se bity v některých systémech doplnit nulami (tzv. logický posuv, ale v jiných hodnotou znaménkového bitu (aritmetický posuv).
•
[e)] x = x & ~077 Nuluje se šest posledních bitů veličiny x. Čtenář jistě při troše přemýšlení odpoví správně na otázku, proč je tento zápis vhodnější než např. zápis x = x & 0177700.
3. Typová konverze Pod pojmem typová konverze se míní převod proměnné určitého typu na typ jiný např. int na float. Většina typových konverzí probíhá implicitně, bez potřeby zásahu programátora. Platí: 1. [a)]Před vykonáním operace se samotné operandy konvertují takto: o Kdykoli se objeví typ char nebo short int, konvertuje se na typ int. o Všechny operandy typu unsigned char a unsigned short se konvertují na typ int; pouze tehdy, když typ int svojí velikostí nestačí, konvertují se na typ unsigned int. 2. [b)] Mají-li dva operandy jedné operace různý typ, pak je typ s nižší mírou informace konvertován na typ s vyšší mírou informace takto: unsigned int ¯ na ¯ unsigned int na unsigned int unsigned int na long long na unsigned long unsigned long na float float na double double na long double Toto pravidlo platí i pro podmíněný výraz. Je-li např. proměnná i typu int a proměnná f typu float, je výraz (n > 0) ? i : f typu float. 3. V přiřazovacím příkazu je typ na pravé straně konvertován na typ z levé strany; tato skutečnost, právě tak jako ve Fortranu, může vést ke ztrátě přesnosti a nepříjemným chybám. Příklady: •
[a)] Je-li definováno char zn; pak zn += 65; ¯ 65 je konvertováno na char, tj. zn je přiřazen znak A 65 je konvertováno na char, tzn. zn je přiřazen znak A zn += 32; hodnota zn se zvětší o 32, tj. na 97, což je znak a zn -= '2'; hodnota zn se zmenší o 50, tj. na 47, což je znak /
•
[b)] Je-li definováno int k; pak
k = 'Z' + 32; ¯ k je přiřazena hodnota 90 k je přiřazena hodnota 90 k = 'Z' + 32; k se přiřadí hodnota 122 k = 7.9876 k se přiřadí hodnota 7 (!!!) •
[c)] Je-li definováno double d; pak k = 'Z' + 32; k = d * zn;
¯ k je přiřazena hodnota 90 d se přiřadí hodnota 10.0
V posledním případě se zn konvertuje na int a pak na double. Výsledek součinu je double a je to hodnota 470., která se opět zkonvertuje na int a přiřadí proměnné k. Je nutno upozornit, že hodnoty v příkladech uvedené platí v případě, že systém pracuje s kódem ASCII. Předchozí popsané případy se provádějí automaticky bez našeho přispění. Můžeme však provádět typové konverze explicitně, které se říká přetypování. Má tvar:
( typ)výraz a význam: výraz je konvertován na typ
Příklady a význam některých používaných konverzí: (double) float_výraz převod celého čísla na jeho znakový ekvivalent převod znaku na jeho celočíselný ekvivalent (char) int_výraz převod celého čísla na jeho znakový ekvivalent (int) float_výraz odříznutí desetinné části (double) int_výraz převod celého čísla na reálné (double) float_výraz zvětšení přesnosti Přetypování je nutné v různých případech, např. při použití ukazatelů. V původně definovaném jazyku C (viz např. [3]), bylo nutné při nesouladu formálních a skutečných parametrů provést přetypování skutečného parametru (tak je tomu ostatně ve většině programovacích jazyků). Tedy např. //... int i; double d; //... d=exp((double)i); //...
V jazyku C++ to není nutné, protože jsou zavedeny povinné prototypy, ale přesto je vhodné přetypování uvést, protože zpřehledňuje program. V jazyku C++ se více než přetypování používá tzv. explicitní typová konverze. Spíše než dříve uvedený zápis píšeme v C++: //... int i; double d;
//... d=exp(double(i)); // double (i) je explicitni typova konverze //...
nemáme-li ovšem odvahu napsat //... int i; double d; //... d=exp(i); //...
3.1 Priority operací Závěrem této kapitoly si uvedeme priority operací. Uvedeme všechny operátory s tím, že nám ještě neznámé probereme později. Tabulka priorit operací pro jazyk C++: Operátor :: () [ ] -> . sizeof ! ~ ++ -- + - ( typ) * & new delete */% +<< >> < <= >= > ==!= & ^ | && || ?: = + = - = * = / = % = >>= <<= & = / = ^= ,
Poznamky: • • • • •
•
• •
[a)] Operátory zapsané v témže řádku mají tutéž prioritu. Operátory zapsané ve vyšším řádku mají vyšší prioritu než operátory zapsané v řádku nižším. [b)] Unární operátory, tj. operátory s jedním operandem a přiřazovací operátory jsou asociativní zprava doleva; všechny ostatní zleva doprava. [c)] V tabulce jsou uvedeny všechny operátory mnohé z nich např. ::, ->, ., new a delete probereme později. [d)] Operátory + a - ve třetím řádku jsou tzv. unární operátory narozdíl od operátorů + a - v pátém řádku, které jsou binární. [e)] Právě tak operátory * a & (reference nebo též operátor adresy ) ve třetím řádku jsou unární operátory, které nesmíme zaměňovat s operátorem násobení ve čtvrtém řádku a operátorem bitového součinu v devátém řádku. [f)] Operátor sizeof určí počet slabik výrazu (sizeof(i)) nebo typu (sizeof(int)) u výrazu není třeba závorek, ale je lepší, děláme-li je. hodnota sizeof('x') se rovné jedničce, kdežto např. sizeof(ťxť) se rovná hodnotě dva. Čtenář jistě ví proč. [g)] Operátory ++ a -- mohou být prefixové a postfixové. [h)] Vyhodnocení asociativních a komutativních operací záleží na kompilátoru. To platí i pro argumenty funkcí. Např. nesprávný (nebo spíše riskantní) zápis printf(ť%d %d\nť,++n,power(z,n));Správný (nebo spíše bezpečný) zápis ++n; printf(ť%d %d\nť,n,power(z,n));
Samozřejmě, že se vyhneme i zápisům typu: a[i] = ++i; // !!! Být či nebýt, to je oč tu běží!!!
Kapitola 3 Podrobnosti k předchozím kapitolám Nyní již dokážeme realizovat i dosti složité programy v jazyku C++. Budeme muset probrat ještě velmi důležité pojmy jako struktury a uniony a hlavně ukazatele a třídy, ale v této kapitole zatím probereme jen některé podrobnosti, o kterých jsme z pedagogických důvodů nechtěli hovořit v kapitolách předchozích, ale které nemůžeme opominout.
1. Příkazy a bloky Výraz jako x = 0 nebo i++ nebo printf(...) se stane příkazem, následuje-li za ním středník: x = 0; nebo i++; nebo printf(...); V jazyku C a C++ je středník koncovým znakem příkazu a ne oddělovačem, jako v jazycích typu Algol nebo Pascal. Proto se také musí např. psát ...;
if(a>b) max=a; else max=b;
i když středník před slovním symbolem else je v programovacích jazycích obvykle chybný. Složené závorky se používají pro sdružení příkazů v tzv. složený příkaz, který jsme již mnohokrát použili. Jsou-li ve složeném příkazu uvedeny definice proměnných, jedná se o tzv. blok. Bloky mohou být do sebe vřazeny. Příklad 3.1. Sestavme školní program, kterým si osvětlíme princip blokové struktury.
Řešení: #include<stdio.h> main() { int a,b,c; // Lokalizovane promenne v programu a=10; b1:{int i,j; // Lokalizovane promenne v bloku b1 i=j=5; c=i*j*a; printf(ťTisk v b1: c = %3d\nť,c); } b=c*a; b2:{float b,c,d; // Lokalizovane promenne v bloku b2 d=c=2.5; b=c+4; b3:{int p; // Lokalizovana promenna v bloku b3 p=5; d=p+b; b=10*p+b; printf(ťTisk v b3: d = %4.1f b = %4.1f\nť,d,b); }
}
a=100+a; printf(ťTisk v b2: a = %3d\nť,a); } printf(ťTisk v programu: a = %3d b = %4d c = %3d\nť,a,b,c);
Bloky b1, b2, b3 jsou podřazené hlavnímu programu. Hlavní program a blok b2 jsou nadřazené bloku b3. Definice, které jsou v bloku uvedeny, platí pouze uvnitř tohoto bloku. Vně bloku může být každý z definovaných identifikátorů užíván pro jiné účely. Tak každý blok zavádí novou úroveň označování. Každý identifikátor, který je v bloku definován, je v něm lokální, tj. má tyto vlastnosti: 1. Objekt označený tímto identifikátorem v bloku neexistuje vně tohoto bloku. 2. Každý objekt označený tímto identifikátorem vně bloku je uvnitř tohoto bloku zcela nepřístupný. V příkladu 3.1 jsou automatické proměnné a, b, c z hlavního programu přístupné v bloku b1. V bloku b2 a b3 je přístupná pouze proměnná a, protože identifikátory b, c jsou užity jinak. Proměnné i, j z bloku b1 jsou přístupné jen v tomto bloku a po jeho ukončení jejich hodnoty přestávají být definovány. Proměnné b, c, d definované v b2 jsou přístupné i v bloku b3 a přestávají být definovány po ukončení bloku b2. Pro pečlivého čtenáře je třeba uvést, že v příkladu 3.1 se budou tisknout postupně hodnoty: Tisk v b1: c = 250 Tisk v b3: d = 11.5 b = 56.5 Tisk v b2: a = 110 Tisk v programu: a = 110 b = 2500 c = 250 Za koncovými složenými závorkami složených příkazů a bloků se nemusí dělat středníky. Závěrem tohoto odstavce se letmo zmíníme o tom, že definice a deklarace nemusí být umístěny na začátku programové jednotky nebo bloku, ale mohou být zapsány na kterémkoli místě před prvým použitím definovaného nebo deklarovaného objektu. Původně definovaný jazyk C toto zakazoval (viz např. [3]).
2. Operátor :: Operátor :: se v jazyku C++ užívá k řešení konfliktů v přístupových právech k objektům. Máme-li např. automatickou (lokální) proměnnou vekt_sum v nějaké funkci a potřebujeme-li užít externí (globální) proměnnou, která se jmenuje též vekt_sum, zapíšeme: ::vekt_sum, čímž jsme programu umožnili tuto externí proměnnou použít. Napíšeme-li např. program z příkladu 3.1 takto: #include<stdio.h> int c; // Externi promenna main() { int a,b,c; // Lokalizovane promenne v programu a=10; ::c=1000; // Pracuje se s externi promennou b1:{int i,j; // Lokalizovane promenne v bloku b1 i=j=5; c=i*j*a; // Pracuje se s automatickou promennou printf(ťTisk v b1: c = %3d\nť,c);
}
} b=c*a; b2:{float b,c,d; // Lokalizovane promenne v bloku b2 d=c=2.5; b= c+4; b3:{int p; // Lokalizovana promenna v bloku b3 p=5; d=p+b; b=10*p+b; printf(ťTisk v b3: d = %4.1f b = %4.1f\nť,d,b); printf(ťTisk zdanlive nepristupne promenne c = %d\nť,::c); } a=100+a; printf(ťTisk v b2: a = %3d\nť,a); } printf(ťTisk v programu: a = %3d b = %4d c = %3d\nť,a,b,c);
bude se v bloku tisknout: Tisk v b1: c = 250 Tisk v b3: d = 11.5 b = 56.5 Tisk zdanlive nepristupne promenne c = 1000 Tisk v b2: a = 110 Tisk v programu: a = 110 b = 2500 c = 250 Je nutno upozornit, že operátorem :: lze zpřístupnit jen globální proměnné, které jsou externí a statické, nikoliv automatické proměnné, které jsou globální z hlediska blokové struktury. Nedůvěřivý čtenář se může v původním programu z příkladu 3.1 pokusit zpřístupnit proměnnou c ze záhlaví hlavního programu v bloku b3. Uvidí, že neuspěje. Toto nebývá v učebnicích jazyka C++ příliš zdůrazňováno. Operátor :: se užívá i ve spojitosti s tzv. třídami.
3. Operátor čárka Dvojice výrazů oddělených čárkou se vyhodnocuje zleva doprava a výsledek má hodnotu pravého operandu. Čárky mezi parametry nemají význam operátoru. Příklad 3.2. Sestavme funkci pro obracení řetězce znaků.
Řešení: // Funkce obrati retezec: tedy napr. ťabcdť zmeni na ťdcbať void obrret(char s[]) { int c,i,j; for(i=0,j=strlen(s)-1;i<j;i++,j--) { c=s[i];s[i]=s[j];s[j]=c;} }
Funkce strlen je obvykle standardní funkcí jazyka C++ a bývá součástí souboru string.h. Zjiš»uje délku řetězce bez koncového znaku '\0'. Protože indexace v jazyku C++ začíná od nuly a znak '\0' nepřehazujeme, museli jsme napsat strlen(s)-1. Příklad je pro tuto operaci typický, protože se operátor čárka v jazycích C a C++ užívá hlavně v příkazech for a while.
4. Příkaz break Příkaz break jsme již použili ve spojitosti s příkazem switch (přepínač), kdy jsme jednotlivé alternativy ukončovali právě příkazem break. Další využití tohoto příkazu je v ukončení příkazu cyklu, ve kterém je zapsán. Příklad 3.3. Sestavme program, který vynechá v textu koncové mezery, tabelátory a nové řádky. Řešení: V programu využijeme funkci getline, kterou jsme již sestavili v příkladu 1.15. main() // Vynechani koncovych mezer, tabelatoru a novych radku { const int MAXLINE = 1000; int n; char radka[MAXLINE]; while((n=getline(radka,MAXLINE))>0) { while(--n > 0) if (radka[n] != ' ' && radka[n] != '\t' && radka[n] != '\n') break; radka[n+1]='\0'; cout << radka; } }
Příkaz break v tomto případě ukončí práci vnitřního cyklu, nerovná-li se poslední kontrolovaný znak ani mezeře, ani tabelátoru a ani znaku nový řádek.
5. Příkaz continue Tento příkaz zajistí ukončení jednoho průchodu cyklem a narozdíl od příkazu break se používá pouze v cyklech. //... tato část programu vynechá záporné prvky for(i=0;i
V příkladu se zpracovávají v cyklu i pouze kladné prvky podle nějakého algoritmu. Narazí-li na záporný prvek příkaz continue zajistí, že se algoritmus neprovede i se (v tomto příkladu) zvětší o jedničku a provádí se další krok cyklu.
6. Registrové proměnné Jazyk C i jazyk C++ umožňuje pracovat s tzv. registrovými proměnnými. Počítač je umístí v registrech počítače, takže přístup k nim se značně zrychlí. V následujícím naznačení funkce f jsou jako registrové proměnné použity oba formální parametry a automatická proměnná m. f(register int c, register int n} { register int m; // ... }
Příklad 3.4. Sestavme funkci pro výpis násobků libovolného čísla hodnotami 11 až 20.
Řešení: #include <stdio.h> void nasob(register float k) { for (register j=11;j<=20;j++) printf(ť%d * %f = %f\nť,j,k,j*k); }
V příkladu je použit formální parametr typu register float a proměnná j, která je typu register int. Pro celočíselnou proměnnou je možné slovní symbol int vynechat. V praxi se na registrové proměnné kladou jistá omezení, které odrážejí možnosti použitého počítače. Registrové proměnné musí být vždy automatické (lokální) . V každé funkci můžeme v registrech držet jen několik proměnných a tyto mohou být jen určitých typů. Pro nadbytečné nebo nepřípustné definice se označení register ignoruje.
7. Cvičení 1. [23.] Sestavte funkci, která připojí řetězec t na konec řetězce s. Funkci použijte v programu. 2. [24.] Sestavte funkci, která z řetězce s1 odstraní všechny znaky nacházející se v řetězci s2. Funkci použijte v programu. 3. [25.] Napište funkci, která vrátí pozici prvního výskytu libovolného znaku z řetězce s2 v řetězci s1. Když s1 neobsahuje žádný znak z řetězce s2, vrátí funkce hodnotu -1. 4. [26.] Sestavte funkci poc_bit(long unsigned n), která zjistí počet bitů v proměnné typu long usigned. Zkuste modifikovat na int, long int a short int. 5. [27.] Sestavte funkci itoa(int n,char s[]), která konvertuje číslo na znakový řetězec. Použijte příkaz do-while, modulo a \= (jako celočíselné dělení). Návod: Seřaďte číslice odzadu a pak použijte funkci obrat ze cvičení č. 20.
Kapitola 4 Ukazatelé, pole, funkce Ukazatel (též se užívá termínu spoj) je proměnná (nebo symbolická konstanta) jejíž hodnotou je adresa jiného objektu (proměnné, pole apod.) Pro zavedení těchto lze např. uvést tyto důvody: • •
Někdy lze algoritmus realizovat lépe pomocí ukazatelů. Jindy se získá efektivnější strojový kód.
1. Ukazatelé a adresy Předpokládejme, že v programu jsou zapsány následující definice a příkazy //... int *px,x,y,z,*py; //... px=&x; //... y=*px; z=*px+1; printf(ťx = %d odmocnina z x = %lfť,sqrt((double)*px); py=px; //...
V této ne příliš smysluplné části programu jsme demonstrovali základní práci s ukazateli. Ukazatel se v deklaraci nebo definici pozná podle znaku *, který předchází jménu ukazatele. V našem příkladu je ukazatelem veličina px. Příkaz px=&x; zajistí, že se ukazateli px přiřadí hodnota adresy proměnné x. Operátor & se nazývá operátor adresy. Od tohoto okamžiku je v programu ekvivalentní zápis *px a x do doby, než je ukazateli px přiřazena jiná hodnota. Tedy příkazy y=*px; z=*px+1; jsou ekvivalentní příkazům y=x; z=x+1;. Též v příkazu printf jsme školně napsali *px, i když jsme mohli psát x. Příkazem px=py jsme zajistili, že px a py ukazují na totéž místo v paměti, tedy v našem případě na místo, které je přiděleno proměnné x. Závěrem tohoto odstavce by bylo vhodné, aby se čtenář laskavě přesvědčil, že zápis *px++ není totožný se zápisem (*px)++, kdežto zápis *++px je totožný se zápisem *(++px). V těchto případech je vhodné, aby se priorita operací vždy vyznačila závorkami. Vyhneme se tak nepříjemným omylům a program zpřehledníme.
1.1 Ukazatelé a argumenty funkcí Jednoduché proměnné, jak jsme si řekli v čl. 5, jsou volány hodnotou, není-li řečeno jinak. Jeli řečeno jinak, volají se odkazem. Jinak může být v jazyku C++ řečeno dvěma způsoby. První způsob je obvyklý i v jazyku C a provádí se takto:
1. Formální parametry se deklarují jako ukazatele. 2. Odpovídající skutečné parametry se zapisují s operátorem adresy. Druhý způsob je typický pro jazyk C++ a provádí se takto: 1. Formální parametry se označí operátorem adresy. 2. Odpovídající skutečné parametry se zapisují bez jakéhokoliv označení. Čtenář se může přesvědčit, že následující dva úseky programu jsou algoritmicky správně; kdežto třetí je algoritmicky (nikoliv ovšem syntakticky) chybný. • • • • • • • • • • • • • • • • • •
[1.] úsek void zamen(int *x,int *y) { int t=*x;*x=*y;*y=t; } main() {/*...*/zamen(&a,&b);/*...*/}
[2.] úsek void zamen(int &x,int &y) { int t=x;x=y;y=t; } main() {/*...*/zamen(a,b);/*...*/}
[3.] (chybný) úsek void zamen(int x,int y) { int t=x;x=y;y=t; } main() {/*...*/zamen(a,b);/*...*/}
2. Ukazatelé a pole Z čl. 4 víme, že např. při definici int a[10] se v paměti vyhradí deset pamě»ových míst pro prvky pole indexované od 0 do 9. Napíšeme-li v programu int *pa = &a[0], pak pa ukazuje na místo, kde je umístěno a[0]. (Pozor! Toto počáteční přiřazení je poněkud matoucí; srovnejte se zápisem pa = &a[0], které má tentýž význam a které je v intencích mnemotechniky: bez hvězdičky adresa a s hvězdičkou hodnota, která je na příslušné adrese uložena. Tato mnemotechnika většinou platí.) Platí-li že pa ukazuje na a[0], pak pa+1 ukazuje na a[1], pa+i na a[i]. Napíšeme-li *(pa+i), pak je to totéž jako bychom napsali a[i]. Místo pa = &a[0] smíme zapsat pa = a. Tyto zápisy vypadají zpočátku velmi nezvykle, ale v programu se často využívají. Příklady: •
[a)] Určeme délku řetězce znaků: o [a1)] Nejprve jak bychom to naprogramovali se znalostí předchozích kapitol.
o o o o o o o o o o o
delret(char s[]) { for(int n=0;s[n]!='\0';n++); return(n); }
[a2)] S využitím ukazatele. delret(char *s) { for(int n=0;*s!='\0';s++,n++); return(n); }
Funkci delret jsme již použili dříve jako knihovní funkci strlen. Nyní vidíme, že její naprogramování je velmi prosté. •
[b)] Zkopírujme řetězec t do řetězce s. o [1.] varianta: o o o o o o o o o o o o o o o o o o o o
void zkoret(char t[],char s[]) { int i=0; while((s[i]=t[i])!='\0')i++; }
[2.] varianta: void zkoret(char *t,char *s) { while((*s=*t)!='\0'){s++;t++;} }
[3.] varianta: void zkoret(char *t,char *s) { while((*s++=*t++)!='\0'); }
[4.] varianta: void zkoret(char *t, char *s) { while(*s++=*t++); }
V poslední variantě se využilo skutečnosti, že poslední znak v řetězci je vždy nula a je-li výraz v závorkách za příkazem while nulový příkaz while se ukončí. Vyniká zde stručnost zápisu v tomto jazyku, která je však mnohdy na účet přehlednosti.
3. Ukazatelé a konstanty Použije-li se slovní symbol const s ukazatelem, jsou konstantní data spojená s ukazatelem, kdežto ukazatel konstantní není. Při deklaraci const char *name =ťPROKOPť; Je přípustný příkaz name=ťBUBENť; ale nikoliv příkaz name[0]='B';
Chceme-li konstantní ukazatel a nikoliv údaj, na který ukazuje, píšeme char *const JMENO=ťhanať; Pak JMENO[0]='d'; je správně, kdežto JMENO=ťdanať; je chybně. Chceme-li mít konstantní ukazatel i údaj, na který ukazuje, píšeme const char *const KONST=ťNAZDARť; Tato poněkud méně přehledná pravidla jsou zachycena v ukázkách v příkladu 4.1. Příklad 4.1. #include<stdio.h> main() { // Definice ukazatele, ktery ukazuje na konstantu const char *name =ťPROKOPť; // name[0]='B'; // Chybny prikaz name=ťBUBENť; // Spravny prikaz // Definice konstantniho ukazatele char *const JMENO=ťhanať; JMENO[0]='d'; // Spravny prikaz // JMENO=dana; //Chybny prikaz // Definice konstantniho ukazatele ukazujiciho na konstantu const char *const KONST=ťNAZDARť; // KONST[0]='A'; // Chybne // KONST=ťAHOJ; // Take chybne printf(ť%s %s %s\nť,name,JMENO,KONST); }
4. Argumenty povelového řádku Funkce main se může volat se dvěma argumenty. Prvý - označme ho argc, což bývá zvykem, ale není to podmínkou - obsahuje počet argumentů tzv. povelového řádku, tj. řádku, kterým vyvoláváme příslušný program. Druhý - označme jej, jak je zvykem, ale nikoliv povinností, argv je ukazatel na pole znakových řetězců, z nichž každý označuje jeden argument. Příklad 4.2. Sestavme program, který bude realizovat známý příkaz echo (ozvěna) operačního systému UNIX. Řešení: • • • • • •
[1.] varianta: #include<stdio.h> // Nazev programu echo.cpp main(int argc, char *argv[]) { for(int i=1;i<argc;i++)
• • • • • • • • • •
printf(ť%s%cť,argv[i],(i<argc-1)?' ':'\n'); }
[2.] varianta: #include<stdio.h> // Nazev programu ozvena.cpp main(int argc, char *argv[]) // Ozvena jinak { while(--argc>0) //Staci while(--argc) printf(ť%s%cť,*++argv,(argc>1)?' ':'\n'); }
Nechávám na čtenáři, aby si vyzkoušel správnost zápisu variant. Je třeba jen upozornit, argc udává počet všech argumentů včetně názvu programu, kterým přeložený program vyvoláváme. Prvky pole ukazatelů argv ukazují vždy na začátek příslušného argumentu. Má-li tedy povelový řádek např. tvar: echo Jak se do lesa vola, tak se z lesa ozyvamá automaticky argc hodnotu 11 a argv[0] ukazuje na začátek řetězce echo, argv[1] ukazuje na začátek řetězce Jak a např. argv[10] ukazuje na začátek řetězce ozyva. Příklad 4.3. Sestavme program pro výpočet integrálu Simpsonovou metodou. Simpsonovu metodu i integrovanou funkci zapišme jako samostatné funkce. Integraci proveďme tak, že zadáme v příkazovém řádku vedle jména programu ještě počet výpočtů a počáteční dělení. V případě, že zadáme chybný počet argumentů, program nás bude instruovat, jak povelový řádek zadat. V tomto příkladu integrujeme funkci ∫01,5[(sinx)/( x)] dx
Řešení: #include <stdio.h> #include <stdlib.h> #include <math.h> #include ťintegral.cppť #include ťint.cppť // Jméno programu hlavint.cpp // Program pracuje s externi funkci integral.cpp // (Simpsonova metoda) a integrovanou funkci int.cpp main(int pocet_parametru, char *pole_ukazatelu_na_parametry[]) { int i,m,j;double g(double),integral(double,double,int,FUK); if(pocet_parametru==3){ j=atoi(*++pole_ukazatelu_na_parametry); /* Funkce prevadejici retezec znaku ulozenych v pameti na celociselnou hodnotu (soubor stdlib.h)*/ m=atoi(*++pole_ukazatelu_na_parametry); for(i=1;i<=j;i++) printf(ťDeleni intervalu: %d integral = %f\nť,m*i, integral(0.,1.5,m*i,g)); } else printf(ťZadej: hlavint pocet_vypoctu zakladni_deleniť); return 0; } // Integrovaná funkce double g(double x)
{ if (x==0) return(1); else return(sin(x)/x); } // Simpsonova metoda typedef double(*FUK)(double); double integral(double a, double b, int n, FUK f) // FUK f je totez jako bychom psali: double(*f)(double)) { double s; double h=(b-a)/n; int k,i; k=1;s=f(a)+f(b); /*Toto je strucnejsi,hezci a rozumnejsi zapis nez s=(*f)(a)+(*f)(b)*/ for(i=1;i
V příkladu jsme si ukázali vedle použití povelového řádku (kdy zbytečně dlouhé názvy argumentů jsem zvolil proto, abych explicitně upozornil na možnost volby jmen argumentů; argc a *argv[] se používají proto, že to tak zavedli tvůrci jazyka C pánové Kerninghan a Ritchie [3], není to však dogma a lze využívat různých mnemotechnických názvů) i využití jazyka C++ v numerické matematice, kdy je často vhodné použít jako skutečný parametr funkci. Tento způsob jsme si mohli ukázat až v okamžiku, kdy jsme si vysvětlili mechanismus ukazatele. Formální parametr, který stojí na místě funkce, musí být totiž deklarován jako ukazatel na funkci vracející hodnotu. Jak jsme již napsali v poznámce v proceduře, mohli jsme zapsat tento formální parametr jako double(*f)(double), my jsme však použili příkazu typedef v zápisu typedef double(*FUK)(double) čímž jsme definovali typ FUK jako ukazatel na funkci mající jeden argument typu double a vracející hodnotu typu double. Zápis formálního parametru má pak jednodušší formu: FUK f. Funkci f můžeme zapisovat běžným způsobem jako v jiných programovacích jazycích a ne, jak se traduje (a je ovšem také správné) a je uvedeno v poznámce ve funkci integral. Doplňme si nyní naše znalosti o příkazu typedef, přestože jsme se o něm zmínili již např. v čl. 1. Příkaz typedef int CEL, *CELUK; zajistí, že místo int ahoj, *metr; lze psát CEL ahoj; CELUK metr;což se může jevit jako zbytečné ale v případech jako • • • •
[a)] int *id1[3]; [b)] int (*id2)[3]; [c)] int *id3 (int); [d)] int (*id4)(int);
(kdy v případě a je deklarováno pole tří ukazatelů na celá čísla; v případě b je deklarován ukazatel na pole tří celých čísel; v případě c je deklarován identifikátor funkce vracející ukazatel na celé číslo a mající jeden argument typu int; a konečně v případě d je deklarován
ukazatel na funkci vracející celé číslo a mající jeden celočíselný argument) nám příkaz typedef pomůže zpřehlednit zápis programu, protože shora uvedené zápisy přehledností nevynikají. Napíšeme-li ale v příkazu typedef některou z výše uvedených konstrukci, pak jméno stojící na místě id1 až id4 označuje nový typ. Takže typedef int (*UNP3C)[3]; UNP3C ukp;//...
zajistí, že proměnná ukp je ukazatel na pole tří celých čísel. Zápis typedef int *FVUNC(int);FVUNC f1;
sděluje kompilátoru, že f1 je identifikátor funkce s jedním celočíselným argumentem, vracející ukazatel na celé číslo atd. Zápis se zjednodušší a toho jsme právě využili v příkl. 4.3.
5. Inicializace polí a polí ukazatelů Jak jsme si již řekli v 5.1 v jazyku C++ můžeme inicializovat i automatická pole, což je v jazyku C zakázáno. Platí, právě tak jako u jednoduchých proměnných, že externím polím se přiřazuje hodnota právě jednou při plnění programu, kdežto automatickým polím se přiřazuje hodnota vždy při vstupu do funkce nebo bloku. Zde si proto na příkladech ukážeme techniku počátečního přiřazení, aniž bychom rozlišovali mezi externími a automatickými poli. Příklady: •
[a)] Definice jednorozměrného pole se šesti prvky: int x[] = {1,3,5,7,9,11};
• • • • •
[b)] Definice matice (dvojrozměrného pole) typu (4;3) (Rozmezí řádkových, resp. sloupcových indexů je ovšem od 0 do 3, resp. od 0 do 2): int y[4][3] = {{1,3,5}, {2,4,6}, {8,7,9}}, };
Pak y[0][0] = 1; y[0][2] = 5;, čímž naznačujeme, že se hodnoty přiřazují po řádcích. Čtvrtý řádek se v našem případě případě inicializuje nulami. Téhož přiřazení dosáhneme zápisem: int x[4][3] = {1,3,5,2,4,6,8,7,9}; •
[c)] Chceme-li přiřadit prvnímu sloupci pole y hodnoty 10, 9, 8, 7 a ostatním prvkům pole nuly, postupujeme takto: int y[4][3] = {{10},{9},{8},{7}};
•
[d)] V následujícím příkladu jsme v hlavním programu definovali řetězec t, který používáme v příkazu printf jako formát výpisu. V hlavním programu voláme funkci jmeno_mesice, která vrací ukazatel na znak a má jeden celočíselný parametr (viz zapsaný prototyp před jednotkou main). Tato funkce vrací jméno měsíce podle skutečné hodnoty parametru n. To je zajištěno proměnnou typu static, která je v tomto případě ukazatelem na pole třinácti řetězců. Index nula je využit pro hlášení chybného zadání. Veličiny typu static mají tytéž vlastnosti jako veličiny typu extern s tou výjimkou, že mohou být dostupné jen v tom souboru, ve kterém jsou definovány před zápisem programových jednotek (funkcí) nebo jen v té funkci, ve které jsou definovány.
• • • • • • • • • • • • • •
#include<stdio.h> #include char *jmeno_mesice(int); main() { char t[]=ť Jmeno mesice je %s\nť; clrscr(); for(int i=0;i<=13;i++) printf(t,jmeno_mesice(i)); } char *jmeno_mesice(int n) { static char *jmeno[]={ťNespravne zadaniť,ťledenť,ťunorť,ťbrezenť,
• • •
ťdubenť,ťkvetenť,ťcervenť,ťcervenecť,ťsrpenť, ťzariť,ťrijenť,ťlistopadť,ťprosinecť}; return ( n < 1 || n > 12 ? jmeno[0] : jmeno[n]); }
6. Ukazatelé a vícerozměrná pole Vysvětleme si rozdíl mezi dvojrozměrným polem a polem ukazatelů. Budeme-li mít definice int a[20][20], *b[20];
pak použití a i b může být podobné tím, že a[10][10] i b[10][10] je správně zapsaný odkaz na celé číslo. a je však skutečné pole (přidělí se mu 400 pamě»ových míst) a při výpočtu se používá obvyklý výpočet indexů pravoúhlého pole (matice) pomocí mapovací funkce. Pro b se však přidělí 20 pamě»ových míst pro ukazatele. Každý z těchto ukazatelů je třeba nejprve inicializovat, má-li ukazovat na nějaké pole celých čísel. Budeme-li předpokládat, že každý z těchto ukazatelů bude ukazovat na pole o dvaceti prvcích, budeme v takovém případě potřebovat 420 pamě»ových míst. Pole ukazatelů tedy potřebuje více místa v paměti a vyžaduje počáteční inicializaci. Má to však dvě výhody: • •
[1.] Prvek pole je přístupný pomocí ukazatele a ne pomocí mapovací funkce (tj. násobením a sčítáním indexů). Nalezení prvku je tedy rychlejší. [2.] Řádky pole mohou mít různou délku. Jsme tedy např. schopni uložit trojúhelníkovou matici, aniž bychom si pamatovali i nulové prvky, takže v některých případech jsme schopni ušetřit i pamě».
•
[3.] S výhodou lze použít při aplikaci spojení formální-skutečný parametr, stojí-li formální a skutečný parametr na místě dvojrozměrného pole, tj. matice.
V následující ukázce programu se tiskne vždy totéž a sice hodnota prvku a[5][9] tj. v tomto případě hodnota 45. #include #include main() { int a[10][10],*b[10],i,j; clrscr(); for(i=0;i<10;i++) for(j=0;j<10;j++) a[i][j]=i*j; /* Ted se muze cyklem priradit &a[i][0] nebo (a+i)[0] popr. strucneji a mene prehledne a[i] nebo *(a+i) */ for(i=0;i<10;i++)b[i]=&a[i][0] // nebo b[i]=(a+i)[0] protoze obe je pripustne cout<< << cout<< << }
ť Tisk a[5,9] = ť<< a[5][9] << ť Tisk b[5,9] = ť b[5][9] << '\n'; ť Tisk a[5,9] = ť<< a[5][9] << ť Tisk b[5,9] = ť (*(b+5))[9] << '\n';
Příklad 4.4. Sestavme funkce, které umožní přečíst, vytisknout a sečíst matice libovolného typu. Využijme ukazatelů na vícerozměrná pole. Funkce použijme v programu.
Řešení: /*********************************************************************** * Ukazka prace s maticemi v jazyku C++, bez uvazovani dynamickych ob- * * jektu, ale prakticky s dynamickou alokaci dvojrozmernych poli na uro-* * vni (zhruba) jazyka Fortran * ***********************************************************************/ #include<stdio.h> void scmat(int *a[],int *b[],int *c[],int m,int n) { for(int i=0;i<m;i++) for (int j=0;j
for(int j=0;j
} main() { const m=10,n=20; int a[m][n],b[m][n],c[m][n]; int ns,ms; int *pa[m],*pb[m],*pc[m]; // Prirazeni adres prvku prvniho sloupce matic ukazatelum pa, pb, pc for(int i=0;i<m;i++){pa[i]=&a[i][0];pb[i]=&b[i][0];pc[i]=&c[i][0];} printf(ť\nCti pocet radku matice: ť);scanf(ť%dť,&ms); printf(ť\nCti pocet sloupcu matice: ť);scanf(ť%dť,&ns); cti(pa,ms,ns); tisk(pa,ms,ns); cti(pb,ms,ns); tisk(pb,ms,ns); scmat(pa,pb,pc,ms,ns); tisk(pc,ms,ns); }
7. Ještě několik podrobností o funkcích 7.1 Vnitřní funkce Před funkcí v jazyku C++ smí být uveden slovní symbol inline. Takovéto funkce jsou tzv. vnitřní funkce, jejichž operační část se zapisuje přímo na místo, kde je funkce volána. Tím, že odpadá odskok na podprogram, který běžnou funkci realizuje, zrychluje se výpočet, ale prodlužuje se strojový kód, zvláště, opakuje-li se volání funkce často na různých místech a operační část funkce je delší. Doporučujeme proto užívat jako vnitřní funkce jen funkce krátké. Navíc příkazy (a slovní symboly) do, for, while, goto, switch, break, continue a case se ve vnitřních funkcích vyskytovat nesmějí. Použijí-li se, není to chyba, ale funkce se implementuje jako normální funkce a na slovní symbol inline se nebere zřetel. Typická vnitřní funkce je uvedena v následujícím příkladu verbatim #include<stdio.h> inline void pr_int(int a,int b) printf(ť=
7.2 Dosazené parametry V jazyku C++ se smějí používat tzv. dosazené parametry. Kdekoliv se tak učiní, musí být dosazené parametry uvedeny jako poslední. Zápis void default_f(int prvni,float druhy=2.345,char treti='t', char *ctvrty=ťnapravať)
kdežto zápis void default_f(int prvni=1,float druhy,char treti='t', char *ctvrty=ťnapravať)
je chybný. Aplikace dosazených parametrů je v příkladu 4.5. Příklad 4.5. // Priklad na dosazene parametry #include<stdio.h> void default_f(int prvni,float druhy=2.345,char treti='t', char *ctvrty=ťnapravať) { printf(ťprvni = %d , druhy = %f , treti = %c , ctvrty = %s\nť,prvni, druhy,treti,ctvrty); } main() { default_f(32123); }
Z uvedeného příkladu vidíme, že musíme uvádět jen nedosazené parametry. Neuvedeme-li dosazené parametry, dosadí se za tyto parametry hodnota uvedená v záhlaví funkce nebo prototypu.
7.3 Přetížené funkce Přetížené funkce v jazyku C++ jsou takové funkce, které mají stejné jméno v daném programu, ale různou operační část (tj. rozdílný algoritmus). Systém rozlišuje takové funkce při volání podle typu skutečných parametrů, které se musí u různých stejně pojmenovaných funkcí lišit. Dříve než jsou přetížené funkce definovány, musí být uvedeny slovním symbolem overload (viz příklad 4.6). Příklad 4.6. // Pretizene funkce #include<stdio.h> overload pr; /* Musi byt podle normy uvedeno, nektere systemy vsak tuto deklaraci nevyzaduji*/ void pr(int ); //Prototyp void pr(char *); //Prototyp void pr(int *); //Prototyp int cislice[10]={0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; main() { // a jeste jeden prototyp void zobraz(const char *jmeno1,const char *jmeno2=ť dosazenť); zobraz(ťParametr1ť); zobraz(ťParametr2ť,ť na obrazovceť); pr(7);pr(ťMa uctať);pr(cislice); } void pr(int i) { printf(ť\nHodnota cisla je %dť,i); } void zobraz(const char *jmeno1,const char *jmeno2) {
printf(ť\n%s%sť,jmeno1,jmeno2); } void pr(char *retez) { printf(ť\nHodnota retezce je %sť,retez); } void pr(int *c) { printf(ť\nť); for(int i;i<10;i++) printf(ť%dť,c[i]); }
Čtenář se může přesvědčit, že program vytiskne: Parametr1 dosazen Prametr2 na obrazovce Hodnota cisla je 7 Hodnota retezce je Ma ucta 0123456789
7.4 Funkce s nespecifikovaným počtem parametrů U'itím tří teček v závorkách v záhlaví funkce deklarujeme v C++, 'e funkce je definována pro nespecifikovaný počet parametrů. Výklad této vlastnosti, která umo'ní potlačit kontrolu typu parametru, ale hlavně umo'ní větší flexibilitu při pou'ití funkce se vymyká těmto stručným skriptům. Podrobnější výklad lze nalézt např. ve [8] na str. 242, 246 a 247 nebo ve [4]
8. Cvičení 1. [28.] Sestavte funkci getin, která čte celé číslo ze vstupu. Vrací EOF, narazí-li na konec souboru, jinak vrací první neprázdný nečíselný znak. Číselná hodnota je vrácena ukazatelem (např. formální parametr int *p). Návod: Cyklem se přeskočí mezery, nové řádky a tabelátory. Znaky čteme do automatické (lokalizované) proměnné (např. int c). Je-li c == '+', resp. c == '-', přiřadí se např. sgn = 1, resp. sgn = -1. Je-li c ∈ < '0','9' > připočte se k *p řádná hodnota. Při prvé neprázdné a nenulové hodnotě se provede *p *= sgn a je-li c != EOF, vrátí se c na vstup funkcí ungetch. Návrat: return c. 2. [29.] Sestavte funkci getfloat, která čte ze vstupu reálné číslo. 3. [30.] Napište funkci strcat(char *s,char *t), pro kopírování řetězce t na konec řetězce s.
Kapitola 5 Struktury, třídy, uniony, bitová pole V této kapitole se budeme zabývat strukturami, což jsou množiny více prvků i různého typu (narozdíl od polí, kde pole bylo množina více prvků téhož typu), dále třídami, což je zobecněný pojem struktury v jazyku C++, které vedle toho, že mohou být množinami prvků různých typů, mohou k sobě vázat různé algoritmy, kterým se říká metody, jež pracují s prvky třídy, z nichž některé jsou přístupné jen těmto metodám. To je velice zjednodušeně popsaná podstata pojmu třída, jejich důležitá role je v tzv. objektově orientovaném programování, které sice přesahuje rozsah těchto stručných skript, ale kterému věnujeme poslední kapitolu, abychom se alespoň rámcově s tímto způsobem programování seznámili. Typ union byl zřejmě převzat do jazyka C z jazyka Algol 68, kde se poprvé objevil. Proměnné typu union mohou nejen nabývat různých hodnot (což není překvapivé), ale mohou nabývat i hodnot různých typů (což již překvapivé je). Při nedostatku paměti může být někdy výhodné uložit několik objektů do jednoho pamě»ového místa, obvykle je to množina jednobitových příznaků v aplikacích jako jsou tabulky symbolů v kompilátorech. Vzhledem k tomu, že jazyk C byl vytvořen zkušenými systémovými programátory pro vlastní ulehčení práce při tvorbě tak složitého operačního systému jako je UNIX, je jasné, že takovouto konstrukci museli zavést. V kapitole se také zmíníme o práci s tzv. dynamickými objekty, které jsou definovány a je jim vytvářeno místo v paměti (v tzv. hromadě) v průběhu procesu plnění algoritmu. Budeme využívat ukazatelů, tříd a unionů. Vynechal jsem záměrně struktury, abych naznačil, že se stavbou struktury se seznámíme pouze na začátku a pak místo se strukturami, budeme pracovat s třídami. Je to sice poněkud atypické, ale třída může strukturu plně nahradit. Tím ušetříme místo na důležitější konstrukce.
1. Struktury a třídy Jak jsme se již zmínili, je struktura množinou prvků i různého typu, které jsou z důvodu jednodušší manipulace sdružené pod jedním jménem. Strukturám se někdy říká strukturované proměnné, nebo, což je obvyklejší, záznamy, popř. věty. Prvkům struktury se často říká položky. Příklad 5.1. Zapišme deklaraci typu datum, který bude zahrnovat položky den, mesic, rok, cislo_dne_roce typu int a jméno_měsíce typu char[10]. Daný typ použijme v programu tak, že definujeme proměnnou typu datum, které přiřadíme počáteční hodnoty a pak tyto hodnoty vytiskneme. Řešení: Typ datum jsme použili v hlavním programu k popisu proměnné d1, které jsme hned přiřadili počáteční hodnoty. Po vymazání obrazovky (příkazem clrscr() v systému, kde byl program laděn) tiskneme jednotlivé položky tak, že píšeme jméno_struktury.položka
Tečka v konstrukci je v tomto případě tzv. operátor kvalifikace. // Priklad na strukturu #include #include struct datum { int den; int mesic; int rok; int cis_dne_v_roce; char jmeno_mesice[10]; }; main() { datum d1={4,7,1621,185,ťcervenecť}; clrscr(); cout << ťAnno Domini ť << d1.den << ť. ť << d1.mesic << ť. ť<< d1.rok <<ť byl 4. den v ť << d1.jmeno_mesice << ť a ť << d1.cis_dne_v_roce << ť. den v roceť; }
Existují další způsoby jak zajistit deklaraci shora uvedené struktury např. struct { int den; int mesic; int rok; int cis_dne_v_roce; char jmeno_mesice[10]; }d1;
kdy můžeme definovat přímo proměnnou (nebo proměnné) požadované struktury. Zápis uvedený v programu se mi však zdá pro C++ typický. Nyní si zapíšeme tentýž příklad s tím, že místo struktury použijeme třídu. Odlišuje se od struktury na první pohled tím že místo slovního symbolu struct je zapsán slovní symbol class a u položek uvedených ve třídě musíme zapsat slovní symbol public, kterým dáváme kompilátoru informaci, že položky jsou přístupné i příkazům mimo metody (procedury) svázané s danou třídou. // Priklad na trídu #include #include class datum { public: int den; int mesic; int rok; int cis_dne_v_roce; char jmeno_mesice[10]; };
main() { datum d1={4,7,1621,185,ťcervenecť}; clrscr(); cout << ťAnno Domini ť << d1.den << ť. ť << d1.mesic << ť. ť<< d1.rok <<ť byl 4. den v ť << d1.jmeno_mesice << ť a ť << d1.cis_dne_v_roce << ť. den v roceť; }
Od tohoto okamžiku budeme používat pouze objekty typu class (viz. kap. 6) s tím, že čtenář si může vždy za třídu dosadit strukturu a dostane tentýž výsledek. Na okamžik, kdy objekty typu class začneme používat v širším smyslu než objekty typu struct, upozorníme. Ve strukturách a třídách mohou být položky, které jsou opět strukturami nebo třídami. Následující příklad to demonstruje snad dostatečně jasně. Jen je třeba si všimnout násobného použití operátoru kvalifikace. Příklad 5.2. Definujme typ osoba, která bude obsahovat položky jmeno, adresa typu char[], PSC, cislo_pojistky typu long int, mzda typu double a narozeni, zamestnan typu datum. V hlavním programu definujme proměnnou zaměstnanec typu osoba, přiřaďme jí hodnoty a ty ihned vytiskněme.
Řešení: // Priklad na vnorene tridy (struktury) #include #include const int DELKA_JMENA=20, DELKA_ADRESY=20; class datum // deklarace typu datum { public: int den, mesic, rok; }; class osoba // deklarace typu osoba (vyuziva vnorene tridy) { public: char jmeno[DELKA_JMENA]; char adresa[DELKA_ADRESY]; long PSC; long cislo_pojistky; double mzda; datum narozeni, zamestnan; // Vnorene tridy !! }; main() { osoba zamestnanec={ťVorel Karelť, ťJecna 2, Praha 2ť, 12000, 924856787659, 75848, {26,11,1936}, {2,1,1962}}; cout << ťJmeno zamestnance:ť<< zamestnanec.jmeno << '\n' << ťZamestnan od: ť << zamestnanec.zamestnan.den << ť. ť << zamestnanec.zamestnan.mesic << ť. ť << zamestnanec.zamestnan.rok << '\n'; }
Příklad 5.3. Vypočtěme z údaje, který získáme z konkrétního data pořadové číslo dne v roce. Řešení: Úloha je příkladem použití proměnné typu class nebo struct jako parametru ve funkci. V C++ lze skutečné a formální parametry propojovat třemi způsoby, které jsme si již ukázali dříve. V této úloze bychom mohli použít volání hodnotou a výsledek (tj. ze zadaného data vypočítat pořadové číslo dne v roce) by byl správný. Vhodnější je však použít volání odkazem (pomocí operátoru * nebo &), protože se vyhneme kopírování objektu, který může být rozsáhlý a složitě členěný. #include #include <stdio.h> static int tab_dnu[2][13]= { {0,31,28,31,30,31,30,31,31,30,31,30,31}, {0,31,29,31,30,31,30,31,31,30,31,30,31} }; class datum{ public: int den; int mesic; int rok; } ; den_v_roce(datum *d) { int i, den, prestupny; den=d->den; prestupny=d->rok%4 == 0 && d->rok%100 !=0 || d->rok%400 == 0; for(i=1;imesic;i++) den+=tab_dnu[prestupny][i]; return(den); } main() { datum k; cout << ťCti den mesic rok\nť; scanf(ť%d %d %dť,&k.den,&k.mesic,&k.rok); cout << '\n' << k.den <<' ' << k.mesic << ' ' << k.rok << '\n' ; cout << '\n'; cout << ťPoradove cislo dne v roce: ť << den_v_roce(&k) << '\n'; }
Zde jsme v programu použili operátoru -> (spojením znaků minus a větší než); např. zápis d>den je ekvivalentní zápisu (*d).den (jsou-li čtenáři nejasné závorky, nech» se laskavě podívá na priority operací dříve uvedené), ale je stručnější. Jsou-li dva operátory -> zapsány ťvedle sebeť, vyhodnocují se zleva doprava. Tedy zápis p>q->r je totožný se zápisem (p->q)->r právě tak, jako zápis zamestnanec.zamestnan.mesic je totožný se zápisem (zamestnanec.zamestnan).mesic.
Máme-li definovánu třídu T{int x, *int y} a definujeme-li ukazatel na objekt typu T definicí T *p, (ukazatel musíme v průběhu procesu samozřejmě inicializovat) pak zápis ++p->x; zvětší o jedničku x a ne p, protože implicitní závorky jsou v tomto případě ++(p->x);. Chceme-li zvětšit hodnotu p ještě před přístupem k položce x, musíme psát (++p)->x;. Chceme-li však zvětšit hodnotu p po přístupu k x, píšeme (p++)->x;. Zde jsou však závorky zbytečné. Hloubavý čtenář jistě zjistí proč. Přestože autor těchto skript není přívržencem zbytečných závorek, v podobných konstrukcích jazyka C a C++ je jejich horlivým zastáncem. Z dřívějších kapitol víme, že v jazyku C++ je možné volání formálních parametrů adresou. Pak shora uvedený program má tvar #include static int tab_dnu[2][13]= { {0,31,28,31,30,31,30,31,31,30,31,30,31}, {0,31,29,31,30,31,30,31,31,30,31,30,31} }; class datum{ public: int den; int mesic; int rok; } ; den_v_roce(datum &d) { int i, den, prestupny; den=d.den; prestupny=d.rok%4 == 0 && d.rok%100 !=0 || d.rok%400 == 0; for(i=1;i> k.den >> k.mesic >> k.rok; cout << '\n' << k.den <<' ' << k.mesic << ' ' << k.rok << '\n' ; cout << '\n'; cout << ťPoradove cislo dne v roce: ť << den_v_roce(k) << '\n'; }
1.1 Pole struktur Struktury jsou zvláště vhodné, pracujeme-li s poli proměnných, které jsou svázány určitým vztahem. Uva'ujme např. program, který počítá výskyt slovních symbolů jazyka C++ v nějakém programu. Potřebujeme pole znakových řetězců jako vzory slovních symbolů a pole celých čísel jako počítadla výskytu. Ke konkrétnímu slovnímu symbolu musí patřit konkrétní počítadlo. Taková konstrukce by šla realizovat dvěma poli, ale máme-li v jazyku k dispozici objekty jako jsou struktury (a třídy), mů'eme obě pole realizovat např. typem class sl_symb{ public: char *slovo; int poc; };
a vlastní proměnnou mů'eme definovat (přesně viz program) jako static sl_symb tab[]={ {ťautoť,0},{ťbreakť,0},{ťcaseť,0},{ťcdeclť,0},{ťcharť,0}, {ťcontinueť,0},{ťdefaultť,0}, // ... {ťunsignedť,0},{ťvoidť,0},{ťvolatileť,0},{ťwhileť,0} };
Příklad 5.4. Sestavme program, který vypočítá četnost výskytu jednotlivých slovních symbolů v libovolném zdrojovém programu psaném v jazyku C++. Řešení: Program by zasluhoval podrobnější vysvětlení, které by v těchto stručných skriptech zabralo hodně místa. Předpokládám, 'e čtenář, který aktivně přečetl skriptum a' do těchto míst, algoritmu porozumí. Upozorním jen na několik skutečností: 1. Později uvidíme, 'e procedury zde pou'ité mů'eme zahrnout do třídy, která je daleko robustnější konstrukcí ne' struktura. 2. funkce getword vrátí ze vstupu, přičem' rozumíme řetězec písmen a číslic nebo samotný znak. Hodnota, kterou funkce vrací, charakterizuje typ přečteného objektu: mů'e to být LETTER, je-li objektem slovo (slovní symbol, jak napovídá jeho název, je pova'ován v kompilátorech za jeden nedílný znak), EOF pro konec souboru, nebo samotný znak, není-li písmenem. Konstanty LETTER a DIGIT mohou být libovolné hodnoty, zvolili jsme a a 0 3. Funkce typ určuje typ jednotlivých vstupních znaků. Pou'itá verze je vhodná jen pro abecedu v kódu ASCII 4. Funkce binary zajiš»uje efektivní vyhledávání. Prvky souboru, v kterém se prohledávání realizuje, musí být setříděny (v tomto případě podle abecedy). Algoritmus je obdobný numerické metodě, které se říká půlení intervalu. Funkce strcmp je funkce pro zjiš»ování, jsou-li dva řetězce identické. V systému, v kterém jsme program ladili, byla tato funkce v souboru string.h. 5. Konstantu NSLS, která udává počet slovních symbolů jsme mohli vypočítat ručně, ale vhodnější je svěřit výpočet počítači. Počet slovních symbolů se dá vypočíst ze vzorce velikost tab / velikost sl_symb S výhodou se vyu'ije operátor sizeof, který při kompilaci určí velikost jakéhokoliv objektu. // Jmeno programu: poc_sl_s.cpp const int LETTER = 'a'; const int DIGIT = '0'; #include <stdio.h> #include <string.h> #include #include const int MAXWORD = 10; class sl_symb{ public: char *slovo; int poc; };
static sl_symb tab[]={ {ťautoť,0},{ťbreakť,0},{ťcaseť,0},{ťcdeclť,0},{ťcharť,0}, {ťcontinueť,0},{ťdefaultť,0}, {ťdoť,0},{ťdoubleť,0},{ťelseť,0},{ťenumť,0},{ťentryť,0}, {ťexternť,0},{ťfarť,0}, {ťfloatť,0},{ťforť,0},{ťfortranť,0},{ťgotoť,0}, {ťhugeť,0},{ťifť,0},{ťintť,0},{ťlongť,0},{ťnearť,0}, {ťpascalť,0},{ťregisterť,0},{ťreturnť,0},{ťshortť,0}, {ťsignedť,0},{ťsizeofť,0}, {ťstaticť,0},{ťstructť,},{ťswitchť,0},{ťtypedefť,0}, {ťunionť,0}, {ťunsignedť,0},{ťvoidť,0},{ťvolatileť,0},{ťwhileť,0} }; const int NSLS = (sizeof(tab) / sizeof(sl_symb)); char getword(char*,int);int binary(char *, sl_symb [],int); int type(int); FILE *fd; void main(int fg,char *nz[]) { int a,b,pv=0; char slovo[MAXWORD],ch[]; if(fg!=2) { cout << ť\n Zadej: poc_sl_s jmeno_zkoumaneho_souboru\nť; return; } fd=fopen(*(++nz),ťrť); clrscr(); if (fd==NULL) cout<<ť\n soubor ť<<*nz<<ť nelze otevritť; else { cout<<ť\n Zacinam pracovatť<<'\n'; cout<<'\n'<<'\t'<<ťSL.SYMBOLť<< ť ť<<ťCETNOST VYSKYTUť<<'\n'; while ((a=getword(slovo,MAXWORD))!=EOF) if(a==LETTER){ if((b=binary(slovo,tab,NSLS))>=0) tab[b].poc++; } for(b=0;b0){ if(++pv==20){ cout<<'\n'<<ť\n ----- pokracovani -----ť; gets( ch ); pv=0; } cout<<'\n'<<'\t'<
else if (shoda>0) dolni=stred+1; else return(stred);
} return(-1);
} char getword (char *w,int lim) /*cteni dalsiho slova*/ { int c,t; if (type(c=*w++=getc(fd))!=LETTER){ *w='\0'; return (c); } while(--lim>0){ t=type(c=*w++=getc(fd)); if(t!=LETTER && t!=DIGIT){ ungetch(c); break ; } } *(w-1)='\0'; return (LETTER); } type (int c) // vraceni typu znaku v ASCII { int q; if(c>='a' && c<='z'|| c>='A'&& c<='Z'){ return (LETTER); } else if(c>='0' && c<='9') return (DIGIT); else return (c); }
2. Typ union Proměnná typu union mů'e v různých okam'icích nabývat různých typů hodnot. Příklad: union{ int ihod; float fhod; char *phod; }r_hod
union r_tri{ int ihod; float fhod; char *phod; }; r_tri r_hod;
V příkladu jsme pro úplnost uvedli opět dva způsoby definic. V levé části jsme definovali proměnou typu union, která mů'e nabývat hodnot typu int, float a char *, kde'to v pravé části jsme nejprve deklarovali nový typ r_tri a pak jsme definovali proměnnou r_hod typu r_tri, která mů'e nabývat hodnot typu int, float a char*. Nic nám nebrání v jazyku C++, abychom napsali
union r_tri{ int ihod; float fhod; char *phod; }r_hod;
pakli'e se nám to z nějakého důvodu hodí. Toté' platí pro typy struct a class, u kterých jsme to pro stručnost nezdůraznili. Přístup ke členům (polo'kám) je obdobný jako u typů struct a class, tedy jméno_proměnné_typu_union . polo'ka Příklad 5.5. Definujme typ proměnné, která mů'e nabývat hodnoty typu int nebo float nebo typu ukazatel na char. Určeme, jaký typ hodnoty chceme přečíst, přečtěme ji a vytiskněme ji. Řešení: Po zavedení souborů pro proudový vstup (iostream.h), standardní vstup (stdio.h) a ovládání obrazovky ( conio.h), které vyu'ívá systém, pod kterým byl program laděn, deklarujeme typ r_tri pro tři různé typy, proměnnou r_hod typu r_tri a definujeme tři konstanty pro rozpoznání čteného typu. V hlavním programu definujeme ukazatel typu char, kterému, co' je zcela nové, pomocí mechanismu (jen' je typický pro C++) new, umo'níme, aby ukazoval na místo pro jeden znak v tzv. hromadě pro dynamické objekty. Zatím nám stačí vědět, 'e objekty v hromadě jsou přístupné jen prostřednictvím nějakého ukazatele. Podrobněji se těmito objekty budeme zabývat v čl. . Pak ma'eme obrazovku funkcí ze souboru conio.h, čteme typ proměnné a podle přečteného typu čteme hodnotu proměnné. Čtení hodnoty typu int a float je snad zcela jasné a při troše přemýšlení čtenář rozkóduje i čtení řetězce. Výstup hodnoty ji' neřeší nic nového. Programátorsky čistší by snad bylo pro tisk řetězce pou'ít funkce putchar, ale proudová funkce cout vykoná v tomto případě stejnou slu'bu. Toté' nelze říci o nahrazení funkce getchar funkcí scanf nebo cin. Čtenář se mů'e přesvědčit sám. #include // Proudovy vstup #include // Ovladani obrazovky #include<stdio.h> // Standardni vstup union r_tri{ // Deklarace typu promenne tri hodnot int ihod; float fhod; char *phod; }; r_tri r_hod; // Definice promenne pro tri typy hodnot int const INT=1; int const FLOAT=2; int const STRING=3; main() { int r_typ; char *zr=new char[1]; clrscr(); cout << ťCti typ promenne (int=1 nebo float=2 nebo string=3)\nť; cin>>r_typ; cout << ťCti hodnotu\nť;
// Cteni jedne z hodnot
switch(r_typ){ // Cteni jedne z hodnot case INT: cin >> r_hod.ihod;break; case FLOAT: cin >> r_hod.fhod;break; case STRING:r_hod.phod=zr; while((*r_hod.phod=getchar())!='\n') r_hod.phod++; *r_hod.phod='\0'; r_hod.phod=zr;break; default: cout << ťChybne zadaniť;return(0); } switch(r_typ){ // Tisk jedne z hodnot case INT: cout << ťcelociselna hodnota: ť << r_hod.ihod;break; case FLOAT: cout <<ťrealna hodnota: ť << r_hod.fhod;break; case STRING: cout << ťretezec: ť; for(;*r_hod.phod!='\0';++r_hod.phod) cout << *r_hod.phod; } }
2.1 Anonymní typ union Položku typu union můžeme použít ve struktuře běžným způsobem např. takto: //... struct ruzne{ // Deklarace typu ruzne char *jmeno; int *praporky; int c_typ; union{ int c_hod; float r_hod; char *u_hod; }r_hodnota; }; ruzne syn_tab[NSYN]; // Definice promenne typu ruzne //... // Polozka c_hod promenne tab_syn[i] je dostupna zapisem // syn_tab[i].r_hodnota.c_hod // ...
Využijeme-li však tzv. anonymního typu union, lze shora uvedené zápisy psát takto: //... struct ruzne{ // Deklarace typu ruzne char *jmeno; int *praporky; int c_typ; union{ int c_hod; float r_hod; char *u_hod; }; // !!!!!!!!!!!!! }; ruzne syn_tab[NSYN]; // Definice promenne typu ruzne //... /* Polozka c_hod promenne tab_syn[i] je pak dostupna jednodussím zapisem: */ // syn_tab[i].c_hod
3. Příklady dynamických objektů V jazyku C a C++ lze pracovat s tzv. dynamickými objekty. Jsou to objekty, je' se vytvářejí v průběhu procesu. Jsou umís»ovány v paměti, která se nazývá hromada a ka'dý objekt je definován tak, 'e na něj ukazuje nějaký ukazatel. Pomocí dynamických objektů, lze realizovat slo'ité algoritmy prací se symboly, symbolické derivování, integrování apod. My zde uká'eme jen několik jednoduchých programů pro pochopení principu. Příklad 5.6. Napišme program, který uvolní v hromadě místo pro pole padesáti celočíselných prvků, pole naplníme a vytiskneme několik vybraných prvků.
Řešení: #include<stdio.h> void main() { int *data;// int index; data = new int[50]; // Vyhrazeno 50 celociselnych mist v hromade /* Tim jsme definovali pole a s ukazatelem muzeme pracovat jako s polem (coz bychom mohli - ctenar se muze presvedcit - i kdybychom definovali misto jen pro jeden prvek. To ale neni vhodne a ve slozitejsich pripadech i chybne). Kazdemu prvku o indexu i priradime hodnotu 10*i */ for (int index=0;index<50;index++) data[index]=10*index; // Tisk vybranych dat printf(ť\n++*data = %dť,++*data); printf(ť\n*++data = %dť,*++data); printf(ť\n*data++ = %dť,*data++); printf(ť\n(*data)++ = %dť,(*data)++); printf(ť\n*data = %dť,*data); delete(data); // Zruseni objektu }
Jako dobré cvičení doporučuji čtenáři, aby zpaměti určil, co se bude tisknout postupně v posledních čtyřech příkazech tisku a přesvědčil se o správnosti svého úsudku na počítači. Té' se mů'e mů'e pokusit vytisknout toté', pou'ije-li indexované proměnné. Příklad 5.7. Napišme program, který utvoří v hromadě posloupnost objektů, které se říká seznam, ka'dý objekt bude obsahovat celočíselnou hodnotu a ukazatel na další objekt. Poslední objekt seznamu bude ukazovat na nulovou adresu. Řešení: Vytvoříme typ objekt, definujeme dva ukazatele na objekt typu objekt. První bude ukazovat na začátek seznamu a druhý v'dy na nový objekt, který se do seznamu zařazuje. Vytvoříme seznam deseti objektů, které budou nabývat celočíselných hodnot jedna a' deset. Seznam v tomto případě vytváříme od konce. Postup by měl být jasný z programu. #include <stdio.h> main() /* Vytvoreni dynamicke struktury seznam, jeji naplneni,vytisteni
a zruseni */ { struct objekt { objekt *dalsi; int hodnota; }; objekt *seznam, *novy_objekt; seznam=NULL; // nebo seznam=0; novy_objekt= new objekt; // Vytvareni seznamu for(int i=10;i>=1;i--) { novy_objekt->hodnota=i;novy_objekt->dalsi=seznam; seznam=novy_objekt; novy_objekt=new objekt; } // Tisk seznamu novy_objekt=seznam; for(i=1;i<=10;i++) { printf(ťHodnota %d. objektu = %d adresa objektu %p\nť, i,novy_objekt->hodnota,novy_objekt); novy_objekt=novy_objekt->dalsi; } // Uvolneni seznamu z dynamicke pameti objekt *pred=seznam;delete (seznam,novy_objekt); do {objekt *predpred=pred; pred=pred->dalsi; delete predpred; }while(pred!=NULL); // nebo while(pred=!=0); }
K popisu typu jsme pou'ili standardní typ struct. Je samozřejmé, 'e bychom mohli pou'ít i typu class. V tomto odstavci budeme však pou'ívat typ struct, proto'e příklady zde uvedené naprogramujeme v další kapitole s vyu'itím typu class tak, 'e tento typ pou'ijeme v plné šíři pro tzv. objektově orientované programování. Příklad 5.8. Napišme program, který bude vytvářet tentý' seznam, ale sudé prvky se budou plnit hodnotami typu float a liché hodnotami typu int. Řešení: V typu objekt přidáme anonymní union pro hodnotu, která mů'e být typu float nebo int. #include <stdio.h> void main() /* Vytvoreni dynamicke struktury seznam, jeji naplneni a vytisteni. Naplni stridave celociselnymi hodnotami a hodnotami typu float. Cviceni na ukazatele, struktury a uniony*/ { struct objekt{ objekt *dalsi; union{ int c_hodnota; float f_hodnota; }; };
}
objekt *seznam, *novy_objekt; seznam=NULL; // Vytvoreni seznamu for(int i=10;i>=1;i--){ novy_objekt= new objekt; if (i/2*2==i) novy_objekt->f_hodnota=i; else novy_objekt->c_hodnota=i; novy_objekt->dalsi=seznam;seznam=novy_objekt; } // Tisk seznamu novy_objekt=seznam; for(i=1;i<=10;i++){ i/2*2==i?printf(ťHodnota %d. objektu =%f\nť,i,novy_objekt->f_hodnota): printf(ťHodnota %d. objektu =%d\nť,i,novy_objekt->c_hodnota); novy_objekt=novy_objekt->dalsi; } // Uvolneni seznamu z dynamicke pameti objekt *pred=seznam;delete (seznam,novy_objekt); do {objekt *predpred=pred; pred=pred->dalsi; delete predpred; }while(pred!=NULL);
Na tomto místě je vhodné se zmínit o dynamicky definovaných vícerozměrných polí. (Matice a kubické matice.) Podrobnější příklady jsou uvedeny ve [4]. Pro čtenáře by bylo dobrým cvičením, kdyby tam uvedené příklady přepsal do jazyka C++, což je celkem mechanická práce. Jen je třeba si uvědomit rozdíly mezi funkcí malloc a operátorem new. Píše-li programátor v jazyku C např. /*...*/ double **a=(double**)malloc(m*sizeof (double*)); /*...*/ for(i=0;i<m;i++)a[i]=(double*)malloc(n*sizeof(double)); /*...*/
pak v C++ píše //... double **a=new double*[m]; //... for(i=0;i<m;i++)a[i]=new double[n]; //...
4. Bitová pole Bitové pole si mů'eme představit jako strukturu, její' velikost je ale pevně omezena velikostí typu int. Nejmenší délka jedné polo'ky v bitovém poli je jeden bit. Bitové pole se definuje jako struktura, ale ka'dá polo'ka je určena svým jménem a délkou v bitech. Proto'e máme mo'nost definice jak unsigned int, tak signed int, je vhodné v'dy uvést, je-li polo'ka se znaménkem či bez znaménka. Bitové pole má dvě základní oblasti pou'ití
1. Ulo'ení několika celých čísel v jednom slově, co' se pou'ívá pro šetření pamětí. Toto pou'ití není příliš časté. 2. Pro přístup k jednotlivým bitům slova pomocí identifikátorů, co' je pou'ití mnohem častější, proto'e se pak operace s jednotlivými bity provádějí elegantně a přehledně. Příklad 5.9. Napišme datum tak, aby se vešlo do jednoho slova, do kterého lze zapsat celočíselná hodnota v systému, ve kterém předpokládáme, 'e sizeof(int) dává hodnotu 2.
Řešení: #include<stdio.h> main() { struct datum{ unsigned den: 5; // obsazuje bity 0 - 4 unsigned mesic: 4; // obsazuje bity 5 - 8 unsigned rok: 7; // obsazuje bity 9 - 15 }; datum dnes={26,11,2010-1950},zitra,pozitri; zitra.den= dnes.den+1; pozitri.den=dnes.den+2; printf(ť\nDnesni datum: %d. %d. %d, zitra je %d. ť ťa pozitri je %d.\nť,dnes.den,dnes.mesic,dnes.rok+1950, zitra.den,pozitri.den); }
Polo'ka rok zabírá 7 bitů, tzn. 'e největší hodnotou, kterou by bylo mo'né zobrazit je 27 - 1 = 127. Abychom mohli zobrazit letopočty naší doby, odečetli jsme při inicializaci hodnotu 1950. Tu pak musíme při pou'ití data v'dy přičíst. Příklad 5.10. Napišme fragment kompilátoru, který manipuluje s tabulkou symbolů. Řešení: S ka'dým identifikátorem v programu se spojuje určitá informace (jedná-li se o objekt externí, statický nebo slovní symbol apod.) Nejlépe se takováto informace zakóduje mno'inou jednobitových příznaků v objektu typu int nebo char. Bez pou'ití bitových polí, mů'eme postupovat takto: // ... const SLOVSYMB = 01, EXTERNAL = 02, STATIC = 04; unsigned flags=0; /* Cisla jsou mocninami dvou: 01 = 001; 02 = 010; 04 = 100. Pak se pristup k bitum stava zalezitosti operaci posuvu, maskovani a jednotkoveho doplnku, napriklad*/ flags |= EXTERNAL | STATIC; // nastavi bity EXTERNAL a STATIC ve flags, zatim co printf(ť\nflags = %dť,flags); flags &= ~(EXTERNAL | STATIC); // je vynuluje a podminka if((flags & (EXTERNAL | STATIC))==0)... // je splnena, jsou-li oba bity vynulovane. // ...
Pou'ijeme-li bitové pole, lze toté' naprogramovat elegantněji: // ... struct{ unsigned je_slovni_symbol: 1; unsigned je_extern: 1; unsigned je_static: 1; }Flags; Flags.je_extern=Flags.je_static=1; // Nastavi bity Flags.je_extern=Flags.je_static=0; // Nuluje bity if(Flags.je_extern==0 && Flags.je_static==0)... // Testuje, jsou-li bity vynulovany //...
Výhodou bitových polí je příjemnější práce. Nevýhodou je, 'e někde se bitová pole ukládají zleva doprava, jinde zprava doleva. To někdy způsobuje jisté nepříjemnosti (obtí'nější přenositelnost programů). Dále je třeba si pamatovat, 'e bitová pole se mohou ukládat pouze do proměnných typu int nebo unsigned; nejsou to pole a nemají adresy. Není tedy mo'né pou'ívat operátor adresy &.
5. Cvičení 1. [31.] Definujte strukturu, která bude mít položky prijmeni,jmeno a bydliste. Sestavte funkci napln(), která bude schopna tuto strukturu naplnit daty. 2. [32.] Strukturu z příkladu 31 alokujte dynamicky a naplňte ji daty opět pomocí funkce napln(). 3. [33.] Sestavte program, který bude vytvářet seznam obdobný seznamu z příkladu 5.7 (nebo 5.8) s tím rozdílem, že naplnění daty budete provádět čtením dat z klávesnice eventuálně z nějakého souboru. Seznam uspořádejte vzestupně. 4. [34.] Napište program, kterým do vytvořeného seznamu ze cvičení 33 vložíte na správné místo nový objekt. 5. [35.] Sestavte program, který bude využívat bitové pole pro úschovu data. Program vyzkoušejte tak, že načtete z klávesnice datum do bitového pole. To pak vytiskněte jako unsigned a pak datum vytiskněte jako skutečné datum.
Kapitola 6 Základy objektově orientovaného programování V této kapitole se seznámíme se základy objektově orientovaného programování (dále jen OOP). Podrobný výklad se vymyká záměru těchto skript a to nejenom proto, že by přesáhla povolený počet autorských archů. Skripta pojednávají o jazyku C++, který, mimo jiné, umožňuje realizovat objektově orientované projekty. OOP je však metodika, která není svázána s konkrétním programovacím jazykem a která umožňuje strukturovat a konstruovat systémy, přičemž výhody OOP jsou tím zřetelnější, čím větší je projektovaný systém. V těchto skriptech využijeme OOP k tomu, abychom se podrobněji seznámili se strukturou class, kterou již poněkud povrchně známe z 5. kapitoly.
1. Úvod OOP je technika, která pracuje s tzv. objekty. Objekt může mít následující vlastnosti: •
•
•
Dědičnost:Vlastnosti objektu dědí nově vytvářený objekt, tzv. potomek. Potomek dědí vlastnosti předka a může je rozšiřovat. Tato vlastnost je vlastností objektů reálného světa. Předpokládejme např. že základním objektem je budova. Jeho potomkem může být škola, protože má vlastnosti budovy a speciální vlastnost - je to školní budova. Potomky objektu škola mohou být mateřská škola základní škola, gymnázium atd. Při definování objektu přecházíme od obecných vlastností k vlastnostem speciálním. Zapouzdření:Objekt spojuje data a metody (metoda je pojem OOP a znamená totéž, čemu jinde říkáme algoritmus, procedura, podprogram nebo funkce) do jedné struktury. Nechceme-li, nejsou data dostupná jinak než prostřednictvím metod objektu. Polymorfismus: Tuto vlastnost je možno pojmenovat česky mnohotvárnost. Podstatou je, že některé metody se mohou jmenovat stejně a mohou být sdíleny v hierarchii objektů tak, že každý objekt implementuje stejně se jmenující metodu způsobem, který je pro něj vhodný a patřičný.
Použití této metodiky je výhodné zejména při tvorbě informačních systémů, kde vlastnosti objektů OOP se blíží vlastnostem reálných objektů (i když pochopitelně svět je ve své mnohotvárnosti přeci jen složitější, než si myslí někteří fundamentalističtí zastánci OOP), počítačové grafice a někdy i ve vědeckých a technických výpočtech, speciálně při složitých projektech.
2. Příklady objektově orientovaného programování v C++ V tomto článku se na příkladech seznámíme s OOP. Příklady budeme stručně komentovat v odladěných programech. Předpokládáme, že čtenář, který došel až k této kapitole, si programy sám implementuje na počítači, eventuálně vylepší a zjistí co se vlastně vytiskne. Výstupy budeme proto komentovat jen výjimečně. Příklad 6.1. Definujme třídu, která zajistí přičítání a odečítání jedničky k a od proměnné hodnota, která je dostupná pouze metodám (v C++ funkcím zapouzdřeným ve třídě.
Řešení: // Program definujici tridu, ktera je nazvana pocitadlo // Nazev souboru pocitad1.h class pocitadlo { private: unsigned int hodnota; public: /* prototyp konstruktoru a definice vnitřní funkce. Slovní symbol se nemusí psát. Konstruktor při definici typu pocitadlo zajistí vytvoření objektu a inicializaci potřebných dat objektu. Jmenuje se vzdy jako typ class*/ pocitadlo(){hodnota=0;} // Nasleduji dalsi definice metod jako vnitřní funkce void zvetseni(){if(hodnota<65535) hodnota++;} void zmenseni(){if(hodnota>0) hodnota--;} unsigned int zpristupnena_hodnota(){return(hodnota);} }; // Vsimnete se, ze definice tridy musi koncit strednikem!!! // Nasleduje program využívající trídy počitadlo // Nazev souboru pocitad1.cpp #include <stdio.h> #include ťpocitad1.hť void main() { /* Definice objektu c1, c2 a automaticke pocatecni prirazeni hodnoty pomocí konstruktoru */ pocitadlo c1,c2; for(int i = 1;i <= 15; i++) { c1.zvetseni(); printf(ť\nc1 = %uť,c1.zpristupnena_hodnota()); c2.zvetseni(); } printf(ť\nPo ukonceni cyklu je c2 = %uť,c2.zpristupnena_hodnota()); for (i=1;i<=5;i++) c2.zmenseni(); printf(ť\nPo ukonceni druheho cyklu je c2 = %uť, c2.zpristupnena_hodnota()); printf(ť\nKonecna hodnota c1 =%uť,c1.zpristupnena_hodnota()); }
V další alternativě tohoto příkladu budeme definovat funkce třídy mimo tuto třídu, což se obvykle dělá, nelze-li funkce definovat jako vnitřní.
// Program definujici tridu, ktera je nazvana pocitadlo // Nazev souboru pocitad2.h class pocitadlo { private: unsigned int hodnota; public: pocitadlo(){hodnota=0;} // Nasleduji jen prototypy void zvetseni(); void zmenseni(); unsigned int zpristupnena_hodnota(); }; /* Nasleduji definice metod. Vsimnete si, ze kazda metoda je oznacena jmenem tridy pred niz je uveden typ funkce. Je samozrejme, ze mezi definicemi muze byt uveden i konstruktor. Tyto definice nemusi byt ani uvedeny v temze souboru jako trida, ke ktere patri, ale mohou byt umisteny v souboru jinem. Souborum, ktere maji v C++ priponu h se rika hlavickove soubory.*/ void pocitadlo::zvetseni(){if(hodnota<65535) hodnota++;} void pocitadlo::zmenseni(){if(hodnota>0) hodnota--;} unsigned int pocitadlo:: zpristupnena_hodnota(){return(hodnota);} // Tento program vyuziva tridu pocitadlo // Nazev souboru pocitad2.cpp #include <stdio.h> #include ťpocitad2.hť void main() { pocitadlo c1; // Automaticke pocatecni prirazeni hodnoty pocitadlo c2; // ť ť ť ť for(int i = 1;i <= 15; i++) { c1.zvetseni(); printf(ť\nc1 = %uť,c1.zpristupnena_hodnota()); c2.zvetseni(); } printf(ť\nPo ukonceni cyklu je c2 = %uť,c2.zpristupnena_hodnota()); for (i=1;i<=5;i++) c2.zmenseni(); printf(ť\nPo ukonceni druheho cyklu je c2 = %uť, c2.zpristupnena_hodnota()); printf(ť\nKonecna hodnota c1 =%uť,c1.zpristupnena_hodnota()); }
Příklad 6.2. Napišme program, který utvoří v hromadě posloupnost objektů, které se říká seznam, každý objekt bude obsahovat celočíselnou hodnotu a ukazatel na další objekt. Poslednímu objektu seznamu bude přiřazena hodnota NULL.
Řešení: Tento příklad byl řešen v příkladu 5.7. Nyní jen funkce zapouzdříme do třídy objekt a vytvoříme z nich metody. Mezi metodami je nová metoda destruktor. Tuto metodu si můžeme představit jako inverzní funkci ke konstruktoru. Konstruktor vytváří příslušný objekt a přiřazuje datům počáteční hodnoty, je-li to nutné. Destruktor uvolňuje objekt z paměti a ruší ho. V implemntacní části jsme definovali statické proměnné zac_sez pro začátek seznamu a pom_prv pro pomocný prvek, aby byly dostupné právě pro metody dané třídy. V příkladu 6.5 si ukážeme si ukážeme jinou možnost s využitím spřátelených funkcí.
// Hlavickovy soubor seznam.h #include <stdio.h> #include class objekt { private: objekt *dalsi; int hodnota; public: // Konstruktor objekt(){} // Prototypy void vytvor_seznam(int pocet=1); void tiskni_seznam(); void uvolni_pamet(); // Destruktor ~objekt(){delete(zac_sez,pom_prv);} }; static objekt *zac_sez, *pom_prv; // Staticke promenne /* Definice zapouzdrenych funkci. Tyto funkce nelze definovat jako vnitrni. (Ctenar si muze zopakovat, proc tomu tak je.) */ void objekt::vytvor_seznam(int pocet) { zac_sez=NULL;pom_prv=new objekt; for (int i=pocet;i>=1;i--) { pom_prv->hodnota=i; pom_prv->dalsi=zac_sez; zac_sez=pom_prv; pom_prv=new objekt; } } void objekt::tiskni_seznam() { pom_prv=zac_sez; int i = 0; do { i++; printf(ťadresa %d. objektu = %p a jeho hodnota = %d\nť,i, pom_prv,pom_prv->hodnota); pom_prv=pom_prv->dalsi; } while(pom_prv!=NULL); } #include ťseznam4.hť main() { clrscr(); // Mazani obrazovky v systemu, kde byl program laden objekt l; // Definice objektu typu objekt l.vytvor_seznam(10); // Vytvoreni deseti objektu l.tiskni_seznam(); // a jejich tisk }
Data a metody, jak již víme mohou být private, pak jsou přístupné pouze metodám dané třídy, public, pak jsou přístupná pro všechny podle pravidel platných pro data a funkce v jazyku C++. Dále mohou být definovány metody a data jako protected. Taková data jsou přístupná pro odvozené třídy z třídy dané (potomky třídy). Příklad 6.3. Sestavme systém OOP, ve kterém můžeme definovat geometrické obrazce, tyto obrazce vytvářet, rušit, posouvat, zvětšovat a zmenšovat. V systému definujme třídu ümístiť, jejího potomka ťbodť a potomka bodu ťkružniciť. Systém je otevřený, takže je možné definovat libovolný počet tříd pro zobrazení dalších obrazců.
Řešení: Tento příklad ukazuje výhody OOP právě při vytváření systémů počítačové grafiky. Lze z něj pochopit princip dědičnosti a polymorfismu. Nevýhodou je, že je nutno pracovat s konkrétní grafickou knihovnou (byla použita grafická knihovna firmy Borland). Vysvětlující poznámky jsou přímo v jednotlivých souborech, protože je to stručnější a (podle autorova názoru i) přehlednější. /********************************************************************** * obrazce.h... hlavickovy soubor pro jednoduchy graficky projekt, kde * * se definuji jednotlive obrazce na zaklade drive defino-* * vanych objektu. Vyuziva se polymorfismu ( prototypy * * funkci zacinajicich slovnim symbolem virtual). Pouziva-* * ji se i data typu protected, pristupna funkcim zapouz- * * drenych v odvozenych tridach. * **********************************************************************/ // Definice booleovskych (logickych) promennych enum boolean {false,true}; // Trida ťumistiť popisuje umisteni x a y souradnic na obrazovce class umisti { protected: //Dovoluje odvozenym tridam vyuzivat ťprotectedť data int x,y; public: // Tyto funkce jsou pristupne z vnejsiho prostredi umisti(int px,int py) {x=px;y=py;} // Konstruktor int getx(){return(x);} int gety() {return(y);} }; // Trida ťbodť zajisti viditelnost nebo neviditelnost bodu // Trida je potomkem ťumistiť; ťpublicť zajisti, ze objekty odvozene // tridy mohou uzivat metody predka, nebyly-li tyto metody v odvo// zene tride predefinovany. class bod:public umisti{ protected: boolean viditelny; // Tridy odvozene z ťbodť budou potrebovat pristup public: // Prototypy bod(int px,int py); // Konstruktor virtual void zhasni(); // Zhasinat lze ruzne obrazce, proto ťvirtualť virtual void rozsvit(); // Totez plati pro rozsviceni boolean je_viditelny(); void posun(int nx,int ny); void presun(int presun_o); };
// Trida kruznice je potomkem tridy ťbodť a zaroven ťumistiť class kruznice: public bod { protected: // Novy udaj, ostatni se dedi int polomer; public: /********************************************************************** * Prototypy vcetne virtualnich. Nyni se jedna o rozsviceni a zhasnuti * * kruznice. Tez o jeji zvetseni a zmenseni. Pod tymiz jmeny muzeme * * zhasinat, rozsvecet, zvetsovat a zmensovat elipsu, ctverec, proste * * libovolny obrazec. Vzdy se vi, co se ma zhasnout, rozsvitit atd., * * i kdyz se zapozdrene funkce jmenuji vzdy stejne. To je typicky * * polymorfismus. * **********************************************************************/ kruznice(int px,int py,int pr); virtual void zhasni(); virtual void rozsvit(); virtual void expanduj(int expanduj_o); virtual void zmensi(int zmensi_o); }; /********************************************************************** * Prototyp specialni funkce uzite v souboru obrazce.cpp pro presun * * obrazce. * **********************************************************************/ boolean kam_presun(int &deltax,int &deltay); /********************************************************************** * obrazce.cpp - Tento soubor obsahuje definice pro tridy bod a kruzni-* * ce deklarovane v souboru obrazce.h. Konstruktor * * ťumistiť je definovan jako vnitrni primo v souboru * * obrazce.h * **********************************************************************/ #include <stdio.h> #include // Grafika #include // Prace s obrazovkou a klavesnici #include ťobrazce.hť // Metody tridy bod // Konstructor bod::bod(int px, int py):umisti(px,py) { viditelny=false; // a priorne neviditelny } void bod:: rozsvit() { viditelny=true; putpixel(x,y,getcolor()); // Uzije preddefinovanou barvu } void bod:: zhasni() { viditelny=false; putpixel(x,y,getbkcolor()); // Vymaze bod barvou pozadi } void bod::posun(int nx,int ny) { zhasni(); x=nx; y=ny; // posun bodu rozsvit(); } // Funkce pro posuny obrazcu boolean kam_presun(int &deltax,int &deltay) { int keychar;
boolean quit; deltax=0; deltay=0; do{ keychar=getch(); // Cti znak z klavesnice if(keychar==13) // Cetl se ťenterť return(false); if(keychar==0) // Zvlastni znak { quit=true; // Predpoklad: znak je vyuzitelny keychar=getch(); switch(keychar){ case 72:deltay=-1;break; // Znak ťsipka doluť case 80:deltay=1;break; // Znak ťsipka nahoruť case 75:deltax=-1;break; // Znak ťsipka vlevoť case 77:deltax=1;break; // Znak ťsipka vpravoť default:quit=false;// Nepripustny znak se ignoruje } }; }while (!quit); return(true); } void bod:: presun(int presun_o) { int deltax, deltay, obrazecx, obrazecy; rozsvit(); obrazecx=getx(); obrazecy=gety(); // Pocatecni poloha obrazce // a ted obrazec popotahujeme po obrazovce while (kam_presun(deltax,deltay)){ obrazecx += (deltax*presun_o); obrazecy += (deltay*presun_o); posun(obrazecx,obrazecy); } } // Clenove tridy kruznice // konstruktor kruznice::kruznice(int px, int py, int pr):bod(px,py) { polomer=pr; } void kruznice::rozsvit() { viditelny=true; circle(x,y,polomer); // Kresli se kruznice } void kruznice::zhasni() { unsigned int barva; // Uschovej stavajici barvu barva=getcolor(); setcolor(getbkcolor()); viditelny=false; circle(x,y,polomer); setcolor(barva); // Vrat barvu, jinak se vse ztrati na prozadi } void kruznice::expanduj( int expanduj_o) { zhasni(); polomer += expanduj_o; if (polomer < 0) polomer = 0; rozsvit();
} void kruznice::zmensi(int zmensi_o) { // Vyuzije se funkce expanduj se zapornym argumentem expanduj(-zmensi_o); } /************************************************************************ * UKAZOBJ.CPP - Ukazka vyuziti objektove orientovaneho programovani.Pro * * vyuziti je nutne zavest soubor obrazce.cpp a graphics.h * * a je nutno definovat konkretni cestu k ovladaci nejlepe:* ************************************************************************/ #define CESTA ť\\tc\\bgiť /* Tento prikaz v jazyku C++ uzivany mene nez v jazyku C, [protoze lze obvykle (i zde) nahradit definici cons] bude podrobne vysvetlen v kap.7. */ #include ťobrazce.cppť int main() { int go=DETECT,gm,k; kruznice *ukruznice = new kruznice(150,80,50); /* Nasleduje prikaz umoznujici otevrit grafickou knihovnu v implementaci BORLAND TURBO C++; v proměnné go se automaticky ulozi informace o prislusnem ovladaci (driveru), promenna gm zajisti preklopení do spravneho grafickeho rezimu, v konstante CESTA je ulozena cesta do adresare, kde je ulozen ovladac */ initgraph(&go,&gm,CESTA); // Popotahuj kruznici o pet obrazovych prvku (angl. pixel) ukruznice->presun(5); // Stiskni enter a popotahuj kruznici o 30 obrazovych prvku ukruznice->presun(30); // Stiskni enter a zvetsi kruznici na o 50 obrazovych prvku ukruznice->expanduj(50); k=getch(); // Stiskni enter a zmensi kruznici 95 obrazovych prvku ukruznice->zmensi(95); k=getch(); // Nasleduje prikaz, ukoncujici v implementaci BORLAND graficky rezim closegraph();return(0); }
3. Spřátelené funkce a třídy Zapouzdření zajiš»uje, že data typu private nejsou přístupná jiným objektům. To má velké výhody při úpravách programových projektů a jejich údržbě, klade to však velké nároky na projektanta, od kterého se očekává, že zajistí dostatečný počet metod pro manipulaci s daty při všech alternativách, které se mohou vyskytnout. To se někdy nemusí podařit, a proto v C++ existuje mechanismus ťpřátelť. Přátelé (tedy spřátelené funkce a třídy) mají přístup ke všem datům, právě tak, jako metody příslušné třídy. Můžeme si to představit tak, že se do štítu, kterým každá třída chrání svá data, navrtají otvory, kterými jsou data přístupná ťpřátelůmť. Nebudeme zde rozebírat výhody a nevýhody těchto ötvorůť, citem však každý pozná, že (např. pro údržbu systému) čím méně otvorů a ťpřátelť bude, tím lépe. Spřátelelené funkce a třídy se využijí obvykle ve složitých projektech, které se vymykají této publikaci. Abychom však nezůstali jen u planého popisu, sestavíme dva školní příklady, které sice nejsou příliš duchaplné, ale techniku použití ťpřátelť snad dostatečně osvětlí.
Příklad 6.4. Sestavme program, ve kterém třída zvec disponuje metodou, která zvětší privátní údaj třídy o jedničku. Dále sestavme spřátelenou funkci, která bude schopna tento privátní údaj vytisknout. Řešení: Program je jednoduchý a snad i jasný. Zajímavé je na něm jen umístění prototypu spřátelené funkce. Je lhostejné, je-li umístěn v části private nebo public. Funkce pracuje s formálním parametrem typu zvec, který voláme odkazem pro ušetření místa. Je samozřejmé, že lze použít i standardní volání hodnotou, které je opět rychlejší. #include<stdio.h> class zvec { private: int hodn; // Prototyp spratelene funkce friend void print(zvec &); public: // Vnitrni funkce tridy (vcetne konstruktoru) zvec(int n=0){hodn=n;} void pricti(){hodn++;} }; // Definice spřátelené funkce void print(zvec &i){printf(ťhodnota = %d\nť,i.hodn);} #includeťfriendf.hť main() { zvec z(10000); z.pricti(); /* Nasleduje pouziti spratelene funkce: */print(z); z.pricti();/* a opet spratelena funkce: */print(z); }
Příklad 6.5. Sestavme stejný program jako v příkladu 6.2 s tím rozdílem, že jednotlivé celočíselné hodnoty se budou číst z klávesnice, při čemž nejprve udáme, kolik jich budeme číst. Navíc budeme definovat spřátelenou třídu, která zařadí nový objekt na správné místo seznamu, budeme-li předpokládat, že prvky seznamu tvoří klesající posloupnost. Řešení: Opět je třeba zdůraznit, že i spřátelenou třídu je možno uvést v oblasti public i private. Pro zjednodušení algoritmu se v programu předpokládá klesající posloupnost zařazených hodnot seznamu. Objekty seznamu se tvoří öd zaduť, tj. první udaný objekt z klávesnice je v seznamu umístěn jako poslední. // Soubor friendc.h - hlavickovy soubor #include<stdio.h> #include <dos.h> class objekt //Tato trida vlastne nahrazuje strukture { private: objekt *dalsi; int hodnota; friend class seznam; friend class zarad_objekt; }; class seznam // Spratelena trida seznam
{
private: objekt *zac_sez; objekt *pom_prv; friend class zarad_objekt; public: seznam() { zac_sez=NULL; } void vytvor_seznam(int); void tiskni_seznam(); void cisteni_pameti(); ~seznam(){cisteni_pameti();}
}; class zarad_objekt {
};
/* Spratelena trida zarad_objekt k objektu i seznamu */
private: objekt *novy_objekt; objekt *predchozi_objekt; public: zarad_objekt(){novy_objekt=new objekt[1];} void zarad(seznam &);
// Soubor friendc.cpp - implementace metod #includeťfriendc.hť void seznam:: cisteni_pameti() { objekt *pred=zac_sez; if(pred==NULL)return; do {objekt *predpred=pred; pred=pred->dalsi; delete predpred; }while(pred!=0); } void seznam::vytvor_seznam(int pocet=10) { printf(ť\nCti %d prvku \nť,pocet); zac_sez=NULL;pom_prv=new objekt; for (int i=pocet;i>=1;i--) { scanf(ť%dť,&pom_prv->hodnota); pom_prv->dalsi=zac_sez; zac_sez=pom_prv; pom_prv=new objekt; } } void seznam::tiskni_seznam() { pom_prv=zac_sez; int i = 0; while(pom_prv!=0) { i++; printf(ťadresa %d. objektu = %p a jeho hodnota = %d\nť,i, pom_prv,pom_prv->hodnota);
}
}
pom_prv=pom_prv->dalsi;
void zarad_objekt::zarad(seznam &s) { printf(ť\nCti hodnotu noveho objektu\nť); scanf(ť%dť,&novy_objekt->hodnota); predchozi_objekt=s.pom_prv=s.zac_sez; while(s.pom_prv!=NULL) { if(s.pom_prv->hodnota<=novy_objekt->hodnota) if(s.pom_prv==s.zac_sez) {novy_objekt->dalsi=s.pom_prv; s.zac_sez=novy_objekt;break; } else { novy_objekt->dalsi=s.pom_prv; predchozi_objekt->dalsi=novy_objekt;break; } else if(s.pom_prv->dalsi==NULL) { novy_objekt->dalsi=NULL;s.pom_prv->dalsi=novy_objekt; break; } { predchozi_objekt=s.pom_prv;s.pom_prv=s.pom_prv->dalsi; } } } // Soubor frhlac.cpp - hlavni program #include ťfriendc.cppť main() { seznam sez;zarad_objekt a; printf(ť\nCti pocet clenu seznamu\nť); int n; scanf(ť%dť,&n); sez.vytvor_seznam(n); sez.tiskni_seznam(); a.zarad(sez); sez.tiskni_seznam(); }
4. Přetížené operátory V odstavci 7.3 jsme mluvili o přetížených funkcích. Nyní se ještě stručně zmíníme o přetížených operátorech. Máme-li možnost definovat objekty nových typů, musíme mít také možnost definovat nové operátory, kterými realizujeme operace nad těmito objekty. Můžeme si vymýšlet názvy nových operátorů, ale můžeme použít operátorů, na které jsem zvyklí. Dejme si příklad. Představme si, že si definujeme operaci sčítání nad nějakými dvěma námi
definovanými objekty z1, z2 (třeba komplexním hodnotami). Můžeme to učinit standardním způsobem tak, že napíšeme z1.pricti(z2);
nebo z2.pricti(z1);
To znamená, že pošleme zprávu ťpřičti hodnotu z2ť objektu z1 nebo zprávu objektu z2 o ťpřičtení objektu z1. To je méně přehledné, než zápis z1 + z2. Operátor + můžeme tudíž přetížit o další typ na který je aplikovatelný. Můžeme samozřejmě definovat operátor * jako operátor sčítání; kompilátoru (a matematikům) to nevadí, ale normálním lidem ano, proto se takovéto transakce nedoporučují. Než budeme pokračovat ve výkladu, osvětleme si stručně slovní symbol this. Tento slovní symbol (česky znamenající ťtento, tato, totoť) je v jazyku C++ definován jako ukazatel na objekt, který posílá zprávu sám sobě. Ve většině případů se nemusí tento ukazatel explicitně zapisovat, ale někdy, jak v závěrečném příkladu tohoto článku uvidíme, ano. Binární operátory (tj. operátory mající dva operandy) jako např. +, -, *, / atd. se mohou definovat jako metody s jedním parametrem uvnitř třídy (druhým parametrem je pak vždy this nebo jako spřátelené funkce se dvěma parametry. Unární operátory (tj. operátory s jedním operandem) se mohou definovat jako metody bez parametru uvnitř třídy, nebo jako spřátelená funkce s jedním parametrem. Tedy výraz operand_1 B operand_2může být interpretován jako operand_1.operatorB(operand_2)nebo operatorB(operand_1,operand_2). Uvažujme výraz vyjádřený unárním operátorem U operandU nebo Uoperand. Ten může být interpretován jako operand.operatorU()nebo operatorU(operand). Přetěžujeme-li operátory ++ a - nelze odlišit prefixovou a postfixovou aplikaci. Někdy je použití spřátelené funkce nezbytné. Např. z+27 lze zapsat jako z.operator+(27) ovšem zápis
27+z zapsaný jako 27.operator(z) je nesmyslný, protože 27 není objekt definovaný uživatelem. Uvedený příklad je natolik ilustrativní, že lze doporučit binární operace definovat výhradně jako spřátelené funkce. Příklad 6.6. Zapišme program z příkladu 6.1 tak, že počitadlo vylepšíme užitím přetížených operátorů ++ a -. Řešení: /*********************************************************************** * Hlavickovy soubor pro lepsi popis tridy pocitadlo s vyuzitim preti- * * zenych operatoru ++ a -* ***********************************************************************/ //Soubor vlp_pctd.h #include<stdio.h> class pocitadlo { private: unsigned int hodnota; public: pocitadlo(){hodnota=0;} // Konstruktor s nulovanim data hodnota // Prototypy operatoru void operator++(); void operator--(); unsigned operator()(); }; //Implementace metod vylepsene tridy pocitadlo //Soubor vlp_pctd.cpp #includeťvlp_pctd.hť void pocitadlo::operator ++() { if (hodnota<65535) hodnota++; } void pocitadlo::operator --() { if(hodnota>0) hodnota--; } unsigned int pocitadlo::operator ()() { return hodnota; } //Test programu pro vylepsene pocitadlo //File vlp_pctt.cpp #include ťvlp_pctd.cppť #include<stdio.h> void main() { pocitadlo me_nepostradatelne_pocitadlo; for(int i=0;i<12;i++) { me_nepostradatelne_pocitadlo++; printf(ť\nme_nepostradatelne_pocitadlo = %dť, me_nepostradatelne_pocitadlo()); } for(i=0;i<2;i++) me_nepostradatelne_pocitadlo--; printf(ť\nme_nepostradatelne_pocitadlo = %dť,
}
me_nepostradatelne_pocitadlo());
Příklad 6.7. Sestavme systém umožňující práci se zlomky. Řešení: Tento druhý příklad je mnohem rozsáhlejší. V jazyku C++ není standardně definována práce se zlomky, a proto jsme museli následující operátory přetížit tak, aby byly schopny se zlomky pracovat. Operátor ¯ Přetížení proudového operátoru na tisk zlomků.¯ Operátor Užití prirad Přiřaď čitatel a jmenovatel zlomku. = Přiřaď jeden zlomek druhému. op= Realizace operací op=, kde op je +,-,*,nebo /. == Rovnost dvou zlomků. != Nerovnost dvou zlomků. + Sčítání zlomků. - Odčítání zlomků. * Násobení zlomků. / Dělení zlomků. real Konvertování zlomku na číslo desetinné. << Přetížení proudového operátoru na tisk zlomků. Operátory definované ve třídě zlomek odpovídají přesně právě vyjmenovaným operátorům. V sekci private třídy zlomek je uvedena metoda kraceni, která umožní vypočítané zlomky zkrátit, tedy např. -12/36 na -1/3. Konstruktor umožní specifikovat uživateli čitatel a jmenovatel. Není-li čitatel a jmenovatel specifikován, je čitatel roven nule a jmenovatel roven jedničce (využilo se dosazených parametrů). Čtenář si přečte (a doufám, že i prakticky ověří), že operace +,-,*,/,==,!=,real a << jsou definovány jako spřátelené funkce. Všechny s výjimkou funkce real mají dva parametry, a proto není vhodné, aby byly definovány jako metody. Funkce real má jeden parametr, a proto mohla být definována jako metoda. //Hlavickovy soubor pro praci s racionalnimi cisly // Soubor zlomek.h #include #include<stdlib.h> class zlomek { private: void kraceni(zlomek &cis); long citatel,jmenovatel; public: zlomek(long cit=0,long jmen=1) { citatel=cit; jmenovatel=jmen; kraceni(*this); } void prirad(long cit,long jmen) //Prislusny citatel a jmenovatel se prirazedi racionalnimu cislu. //Uziti: //zlomek cislo; //cislo.prirad(12,18); { citatel=cit; jmenovatel=jmen; kraceni(*this); } zlomek operator =(zlomek &cis) /*Argument je volan odkazem, abychom se vyhnuli jeho kopii (uspori se pamet) */ //Operator prirazeni
//Uziti: //zlomek cis1,cis2; //cis2=cis1: Toto znamena cis2.operator =(cis1); { citatel=cis.citatel; jmenovatel=cis.jmenovatel; kraceni(*this); return(*this); } zlomek operator = (long cele_cislo) //Operator prirazeni //Uziti: //zlomek cis; //cis=1234; { citatel=cele_cislo; jmenovatel=1; return(*this); } zlomek operator +=(zlomek &cis) //Operator pricteni //Uziti: //zlomek cis1,cis2; //cis2+=cis1; { long stary_jmen=jmenovatel; long stary_cit=citatel; long j=cis.jmenovatel; long c=cis.citatel; jmenovatel*=j; citatel=stary_cit*j+stary_jmen*c; kraceni(*this); return(*this); } zlomek operator -=(zlomek &cis) //Operator odcitani //Uziti: //zlomek cis1,cis2; //cis2-=cis1; { long stary_jmen=jmenovatel; long stary_cit=citatel; long j=cis.jmenovatel; long c=cis.citatel; jmenovatel *= d; citatel=stary_cit*j-stary_jmen*c; kraceni(*this); return(*this); } zlomek operator *=(zlomek &cis) //Operator nasobeni //Uziti: //num_n cis1,cis2; //cis2*=cis1; { citatel *= cis.citatel; jmenovatel *= cis.jmenovatel; kraceni(*this); return(*this); } zlomek operator /=(zlomek &cis) //Operator deleni //Uziti:
//zlomek cis1,cis2; //cis2/=cis1; //Je-li jmenovatel nula, operace se neprovede. { if(jmenovatel!=0) { citatel *= cis.jmenovatel; jmenovatel *= cis.citatel; kraceni(*this); return(*this); } } //Spratelene funkce pro realizaci binarnich operaci friend zlomek operator + (zlomek &cis1,zlomek &cis2); //Operator pricteni //Uziti: //zlomek cis1,cis2,cis3; //cis3=cis1+cis2; //a to znamena: cis3=operator(cis1,cis2); friend zlomek operator - (zlomek &cis1,zlomek &cis2); //Operator odecteni //Uziti: //zlomek cis1,cis2,cis3; //cis=cis1-cis2; friend zlomek operator * (zlomek &cis1, zlomek &cis2); //Operator nasobeni //Uziti: //zlomek cis1,cis2,cis3; //cis3=cis1*cis2; friend zlomek operator / (zlomek &cis1,zlomek &cis2); //Operator deleni //Uziti: //zlomek cis1,cis2,cis3; //cis3=cis1/cis2; friend int operator == (zlomek &cis1,zlomek &cis2); //Operator rovnosti //Uziti: //zlomek cis1,cis2,cis3; //if(cis1==cis2)... friend int operator != (zlomek &cis1,zlomek &cis2); //Operator nerovnosti //Uziti: //zlomek cis1,cis2,cis3; //if(cis1!=cis2)... friend double real(zlomek &cis); //Konverze racionalniho cisla na realne //Uziti: //zlomek cis1;double r; //r=real(cis1); friend ostream &operator <<(ostream &s,zlomek &cis1); //Proudovy vystup jako pretizeny operator //Uziti: //zlomek cis; //cout<
//Implementace metod tridy zlomek (pro praci s racionalnimi cisly) // Soubor zlomek.cpp #include ťzlomek.hť #include<stdlib.h> void zlomek::kraceni(zlomek &cis) //Vraceni zkraceneho racionalniho cisla (napr. kraceni(20/30)=2/3) { int nsd; //nsd je nejvetsi spolecny delitel if(cis.citatel==0) { cis.jmenovatel=1; return; } nsd=(abs(cis.citatel)> abs(cis.jmenovatel)? abs(cis.citatel): abs(cis.jmenovatel)); if(nsd==0) return; do { if(nsd==1)break; if((cis.citatel%nsd==0) && (cis.jmenovatel%nsd==0))break; else nsd--; }while(1);//Pozor! Simulace nekonecneho cyklu. Pohov! cis.citatel/=nsd;cis.jmenovatel/=nsd; //Prirazeni spravneho znamenka if(cis.citatel<0&&cis.jmenovatel<0) { cis.citatel=-cis.citatel; cis.jmenovatel=-cis.jmenovatel; } else if(cis.citatel<0||cis.jmenovatel<0) { cis.citatel=-abs(cis.citatel); cis.jmenovatel=abs(cis.jmenovatel); } } ostream& operator << (ostream &s,zlomek &cis) { if(cis.jmenovatel!=1) return (s << cis.citatel << '/' << cis.jmenovatel); else return (s << cis.citatel); } zlomek operator + (zlomek &cis1,zlomek &cis2) { long cvysledek,jvysledek; jvysledek=cis1.jmenovatel*cis2.jmenovatel; cvysledek=cis1.citatel*cis2.jmenovatel +cis2.citatel*cis1.jmenovatel; return zlomek(cvysledek,jvysledek); } zlomek operator - (zlomek &cis1,zlomek &cis2) { long cvysledek,jvysledek; jvysledek=cis1.jmenovatel*cis2.jmenovatel; cvysledek=cis1.citatel*cis2.jmenovatel -cis2.citatel*cis1.jmenovatel; return zlomek(cvysledek,jvysledek);
} zlomek operator * (zlomek &cis1,zlomek &cis2) { long cvysledek,jvysledek; jvysledek=cis1.jmenovatel*cis2.jmenovatel; cvysledek=cis1.citatel*cis2.jmenovatel; return zlomek(cvysledek,jvysledek); } zlomek operator / (zlomek &cis1,zlomek &cis2) { long cvysledek,jvysledek; if(cis2.citatel!=0) { jvysledek=cis1.jmenovatel*cis2.citatel; cvysledek=cis1.citatel*cis2.jmenovatel; return zlomek(cvysledek,jvysledek); } } int operator==(zlomek &cis1,zlomek &cis2) { return (float)cis1.citatel/(float)cis1.jmenovatel== (float)cis2.citatel/(float)cis2.jmenovatel; } int operator!=(zlomek &cis1,zlomek &cis2) { return (float)cis1.citatel/(float)cis1.jmenovatel!= (float)cis2.citatel/(float)cis2.jmenovatel; } double real(zlomek &cis) { return (double)cis.citatel/(double)cis.jmenovatel; // nebo return double(cis.citatel/double(cis.jmenovatel) } //Test programu pro tridu class zlomek //Soubor zlomekt.cpp #includeťzlomek.cppť #include main() { //Predefinovani proudove funkce pro vystup do souboru muj_vystup ofstream cout(ťmuj_vystupť); zlomek a(4,5); zlomek b(-10,6); cout<<ťa = ť<
a=d; cout<<ť\n(a=long d=4) = ť<
Výsledky testovacího programu byly zapsány do souboru muj_vystup ve tvaru: a = 4/5 b = -5/3 (a*=b) = -4/3 Po prirazeni a=b, a = -5/3 (a+=b) = -10/3 (a-=b) = -5/3 Po prirazeni a=b; a-=a; a = 0 (a+=15) = 15 (a=1234567); a = 1234567 (a=long d=4) = 4 (a+=(b=5)) = 9 j = 4/5 k = 5/4 zlomek l=j+k; l = 41/20 (l+1) = 61/20 k=5/4; (l=l+k) = 43/10 17 / 15 = 17/15 1 * 17/15 = 1 Hodnota 17/15 se nerovna 1 Hodnota zlomku 17/15 = 1.133333 Po cislo.prirad(12,15); cislo = 4/5
5. Závěrečná poznámka k OOP V celé této kapitole a v kapitole předcházející jsme hovořili o tom, že třídy jsou právě tím nástrojem, který umožňuje objektově orientované programování. V literatuře se často (např. [8]) uvádějí příklady, že objekty typu struct v C++ mohou zahrnovat také metody, ale není zde možnost rozlišit private a public data i metody a ztrácí se tak výhoda zapouzdření. Mělo by tomu tak být, ale zkoušel jsem zaměnit ťnástrojť class ťnástrojemť struct v implementaci BORLAND TURBO C++ a v překladači C++ v operačním systému LINUX a v obou se objekty typu class a typu struct chovaly naprosto stejně. Nedoporučuji tuto skutečnost využívat, ale považoval jsem za vhodné se o ní závěrem zmínit.
6. Cvičení 1. [36.] Sestavte program obdobný programu v příkladu 6.5 s tím rozdílem, že prvky seznamu mohou tvořit rostoucí i klesající posloupnost. 2. [37.] Sestavte program, kterým vytvoříte seznam objektů (čísla nebo písmena, nebo slova), které budete číst z klávesnice nebo z nějakého souboru. Tyto objekty pak seřadíte v neklesající nebo nerostoucí posloupnost. Zda se jedná o neklesající nebo nerostoucí posloupnost, může být zadáno volbou nějakého parametru. Setříděné objekty vytiskněte. 3. [38.] Sestavte systém pro práci s komplexním hodnotami. 4. [39.] Sestavte systém pro sčítání a odčítání vektorů, násobení vektoru skalárem, skalární a vektorový součin. Pro jednoduchost uvažujte trojrozměrné vektory.
Kapitola 7 Poznámky k předpřekladači Předpřekladač jsme zatím používali intuitivně tak, že jsme v příkladech v této publikaci prakticky vždy používali příkazy #include, kterými jsme zaváděli hlavičkové soubory např. #include<stdio.h> nebo naše uživatelské soubory, např. #includeťzlomek.cppť nebo #includeťzlomek.hť. Nyní se v závěru této publikace zmíníme o předpřekladači (obvykle se předpřekladači říká preprocesor) podrobněji. Činnost předpřekladače se dá shrnout do několika bodů: • • • • •
Zpracovává zdrojový text programu před použitím překladače. Nekontroluje syntaktickou správnost programu Provádí pouze záměnu textů, např. identifikátorů konstant za odpovídající hodnoty. Vypustí ze zdrojového textu všechny komentáře. Provádí výběr podmíněně překládaných částí programu.
1. Direktiva #define 1.1 Direktiva #define bez parametrů Tato direktiva je velmi často u'ívána v jazyku C jako direktiva bez parametrů, např. začátek programu z příkladu 1.7 v jazyku C++: /*Tisk tabulky prevodu teplot ze stupnu Fahrenheit na stupne Celsius pro fahr = 0, 20,..., 300 */ #include #include main() { // Definice konstant pro dolni a horni mez a krok zmeny const int DOLNI=0,HORNI=300,KROK=20; //...
musíme zapsat v jazyku C takto: /*Tisk tabulky prevodu teplot ze stupnu Fahrenheit na stupne Celsius pro fahr = 0, 20,..., 300 */ #include #include /* Konstanty definovane stylem jazyka C (v jazyku C++ mozne, ale mene obvykle */ #define DOLNI 0 #define HORNI 300 #define KROK 20 main() { //...
Všimněte si, 'e nesmíme v definici konstanty zapsat na konci středník. Ten by se toti' zapsal s uvedenou hodnotou kdykoliv, kdy' by předpřekladač narazil na příslušný text, např. DOLNI a to by způsobilo syntaktické chyby. Mů'eme samozřejmě psát
#define PI 3.1415926 ale i #include<math.h> //Hlavickovy soubor pro matematickou knihovnu #define PI (4 * atan(1.0)) // atan je arctg ...
Konstanty, kterým často říkáme makra, jsou-li takto definovány se nerozvinou, jsou-li uzavřeny v uvozovkách, např. #define MAKRO past d#include<stdio.h> main() {printf(ťToto je MAKROť);}
Tento program vytiskne text: Toto je MAKRO a nikoliv Toto je past, co' je právě ta past.
1.2 Direktiva #define s parametry Při sestavování programů se často vyskytne případ, kdy mnohokrát pou'íváme nějakou funkci, která je krátká, nebo při řešení diferenciálních rovnic měníme často okrajové nebo počáteční podmínky. Pak s výhodou mů'eme pou'ít direktivy s parametry. Mů'eme např. psát: #define na_treti(x) ((x)*(x)*(x)) Zdálo by se, 'e závorky jsou nadbytečné, není tomu tak. Kdybychom toti' napsali: #define na_treti(x) x*x*x pak po volání: na_treti(a+b) se rozvine do: a+b*a+b*a+b, co' je chybné. Ani vynechání vnějších závorek není vhodné. Např. #define cti(c) c=getchar() se po volání: if(cti(r) == 'b') rozvine do známé chyby: if(r=getchar() == 'b')Na závěr tohoto odstavce ještě upozorníme na případ, 'e direktiva s parametry mů'e být někdy tak dlouhá, 'e se nevejde na jeden řádek. Pak ji přerušíme znakem zpětné lomítko a pokračujeme na další řádce. Např. shora uvedené makro na_treti lze zapsat: #define na_treti(x) ((x)\ *(x)*(x))
Posledním, ale důle'itým, poučením bude, 'e mezi makrem a první otevírací okrouhlou závorkou nesmí být mezera.
2. Operátory # a ## Tyto operátory se neužívají tak často, a proto pro úsporu místa je objasníme pouze na dvou příkladech, ze kterých si čtenář význam snadno domyslí:
Příklad 7.1. #define otevri_soubor(jmeno_souboru) fopen(#jmeno_souboru,ťrť) main() {// ... FILE *ms; ms=otevri_soubor(muj_soubor); // ...}
Předpřekladač ťrozepíšeť toto makro na: //... FILE *ms; ms=fopen(ťmuj_souborť,ťrť); //...
Příklad 7.2. #define print_var(num) printf(ťvarť #num ť = %d\nť,var##num) //... int var1,var2,var3; //... print_var(3); //...
Zde předpřekladač rozepíše makro na: int var1,var2,var3; //... printf(ťvarť ť3ť ť = %d\nť,var3); //...
3. Podmíněný překlad Při ladění programů pomáhají často profesionálně sestavené ladící programy. Jejich nevýhodou je, že při snaze pokrýt co nejvíce možností jsou tyto programy někdy dost složité a mnoho programátorů proto programy píše tak, že do nich zapisují vlastní ladící tisky, které po odladění pro zvýšení přehlednosti programu ruší, přičemž při troše nepozornosti zruší víc než ladící tisky, z čehož vznikají mnohé nepříjemnosti. V jazycích C a C++ je umožněn velmi praktický tzv. podmíněný překlad, který si na několika krátkých ukázkách vysvětlíme. Příklad 7.3 Napišme jednoduchou funkci, která bude napsána tak, aby byla přijatelná, jak pro původní jazyk C jak byl definován v [3], tak pro ANSI C a C++. Využijme podmíněného překladu ve třech základních variantách. Řešení: 1. varianta: Zde běžným způsobem definujeme makro K&R jako nulové. Část podmíněného příkazu mezi #if a #else se provede, je-li makro rovné jedničce. Je-li makro rovné nule, provede se část mezi #else a #endif. Část za #else mimo #endif může být vynechána, dovoluje-li logika ladícího tisku.
2. 3. 4. 5. 6. 7. 8. 9. 10. 11.
#define K&R 0 #define na_treti(x) ((x)*(x)*(x)) #if K&R f(x) int x; {return (na_treti(x)-4*x+7);} #else f(int x) { return (na_treti(x)-4*x+7);} #endif
12. varianta: Zde bude nedefinováno makro K&R, což realizujeme direktivou #undef (Pozor! Makro je prázdné. Pohov!). Podmíněný překlad se pouze změní tak, že místo příkazu #if bude příkaz #ifdef. Příkazy mezi #ifdef a #else se provedou, je-li makro definované. Zbytek si čtenář domyslí sám jako cvičení. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22.
#undef K&R #define na_treti(x) ((x)*(x)*(x)) #ifdef K&R f(x) int x; {return (na_treti(x)-4*x+7);} #else f(int x) { return (na_treti(x)-4*x+7);} #endif
23. varianta: Této variantě čtenář jistě porozumí bez velkého přemýšlení a se zkušenostmi z 2. varianty sám. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33.
#define K&R #define na_treti(x) ((x)*(x)*(x)) #ifndef K&R f(x) int x; {return (na_treti(x)-4*x+7);} #else f(int x) { return (na_treti(x)-4*x+7);} #endif
Literatura [1] Brodský, J.-Skočovský, L.: Operační systém UNIX a jazyk C. Praha, SNTL, 1989 [2] Herout, P: Učebnice jazyka C. České Budějovice, KOPP, 1992 [3] Kernighan, B.W.-Ritchie, D.M.: Programovací jazyk C. Bratislava, Praha, Alfa, SNTL, 1988; slovenský překlad originálu The C Programming Language. Englewood Cliffs, Prentice-Hall, 1978 [4] Kračmar S.-Vogel, J.: Programovací jazyk C. Praha, Ediční středisko ČVUT, 1994 [5] Renner, G.: Borland C++. Kompendium znalostí a zkušeností. Brno, UNIS, 1992 [6] Richta K.-Brůha, I.: Programovací jazyk C. Praha, Ediční středisko ČVUT, 1991 [7] Stroustrup, B.:The C++ programming language. New York, Addison-Wesley Publishing Company, 1987 [8] Wiener, R.: An introduction to object-oriented programming and C++. New York, Addison-Wesley Publishing Company, 1988 File translated from TEX by TTH, version 1.41.