Vyšší odborná škola, Jihlava, Tolstého 16
Obor Počítačové systémy
ALGORITMY A PROGRAMOVÁNÍ V JAZYCE C/C++ 2. díl
Ing. David Matoušek Mgr. Antonín Přibyl
3. vydání leden 2005
2
Algoritmy a programování v jazyce C/C++
1 2
Úvod ................................................................................................................................... 6 Práce se soubory v jazyce C............................................................................................... 7 2.1 Základní funkce.......................................................................................................... 7 2.1.1 Otevření souboru – fopen................................................................................... 8 2.1.2 Zavření souboru – fclose .................................................................................... 8 2.1.3 Test konce souboru – feof .................................................................................. 9 2.2 Textové soubory......................................................................................................... 9 2.2.1 Čtení a zápis do souboru – getc, putc, fgets, fputs, fprintf, fscanf ..................... 9 2.2.2 Jednoduché příklady......................................................................................... 10 2.3 Binární soubory ........................................................................................................ 15 2.3.1 Blokové čtení a zápis dat – fread, fwrite.......................................................... 15 2.3.2 Posun ukazovátka v souboru – fseek................................................................ 15 2.3.3 Zjistění aktuální pozice ukazovátka – ftell....................................................... 16 2.4 Další užitečné funkce pro práci se soubory.............................................................. 20 2.4.1 Vyprázdnění souborových bufferů – fflush ..................................................... 20 2.4.2 Přesun ukazovátka souboru na začátek – rewind ............................................. 20 2.4.3 Přesměrování souboru – freopen...................................................................... 20 2.4.4 Vytvoření dočasného souboru – tmpfile .......................................................... 20 2.4.5 Smazání souboru – remove .............................................................................. 21 2.4.6 Zjištění poslední chyby při práci se souborem – ferror.................................... 21 2.4.7 Smazání chyby – clearerr ................................................................................. 21 2.5 Spolupráce s operačním systémem .......................................................................... 22 2.5.1 Návratová hodnota funkce main ...................................................................... 22 2.5.2 Parametry funkce main..................................................................................... 22 3 Úvod do programovacího jazyka C++ ............................................................................. 24 3.1 Základní rozdíly mezi C a C++ ................................................................................ 24 3.1.1 Zjednodušený zápis komentáře ........................................................................ 24 3.1.2 Umístění deklarací v programu na libovolném místě ...................................... 24 3.2 Typ bool ................................................................................................................... 24 3.3 Klíčová slova jazyka C++ ........................................................................................ 24 3.4 Funkce v C++ ........................................................................................................... 25 3.4.1 Přetížení funkcí (Function Overloading).......................................................... 25 3.4.2 Implicitní parametry funkcí.............................................................................. 25 3.4.3 Parametry funkce volané odkazem .................................................................. 26 3.5 Zjednodušená deklarace datových typů ................................................................... 28 3.6 Operátory jazyka C++ .............................................................................................. 28 3.6.1 Přetížené operátory (Overloading Operators) .................................................. 28 3.6.2 Nové operátory jazyka C++ ............................................................................. 28 3.6.3 Vkládací operátor << a extrakční operátor >> ................................................. 29 3.6.4 Ukazatelový selektor –>................................................................................... 30 3.6.5 Operátor příslušnosti :: ..................................................................................... 31 3.6.6 Operátory pro dynamickou správu paměti ....................................................... 32 3.7 Závěrečné shrnutí ..................................................................................................... 33 4 Základy OOP v jazyce C++ ............................................................................................. 35 4.1 Definice třídy............................................................................................................ 36 4.2 Konstruktor............................................................................................................... 37 4.2.1 Přetížení konstruktoru ...................................................................................... 38 4.2.2 Konstruktor s implicitními parametry .............................................................. 40 4.3 Destruktor................................................................................................................. 40
Obsah
3
5
6
7
8
4
4.4 Vložené (inline) metody........................................................................................... 42 4.5 Ukazatel this............................................................................................................. 43 4.6 Dynamická alokace instancí..................................................................................... 43 4.7 Závěrečné shrnutí ..................................................................................................... 43 Dědičnost, polymorfismus, virtuální metody a abstraktní třídy....................................... 45 5.1 Jednoduchá dědičnost............................................................................................... 45 5.1.1 Odvození přístupových úrovní ......................................................................... 47 5.1.2 Konstruktor, destruktor a dědičnost ................................................................. 47 5.2 Polymorfismus a virtuální metody ........................................................................... 47 5.2.1 Základní pohled na virtuální metody ............................................................... 48 5.2.2 Podrobnější výklad virtuálních metod.............................................................. 49 5.2.3 Implementace pozdní vazby............................................................................. 49 5.2.4 RTTI – dynamická identifikace typů ............................................................... 50 5.2.5 Operátor pro bezpečnější přetypování.............................................................. 50 5.2.6 Abstraktní třída................................................................................................. 51 5.2.7 Polymorfismus a parametry volané odkazem .................................................. 51 5.3 Vícenásobná dědičnost............................................................................................. 56 5.3.1 Problémy při odvození z více bází ................................................................... 56 5.3.2 Řešením je virtuální bázová třída..................................................................... 57 5.4 Závěrečné shrnutí ..................................................................................................... 57 Další rysy OOP v programovacím jazyce C++................................................................ 58 6.1 Výjimky.................................................................................................................... 58 6.1.1 catch ................................................................................................................. 58 6.1.2 try ..................................................................................................................... 58 6.1.3 throw................................................................................................................. 59 6.2 Konstantní členy....................................................................................................... 59 6.2.1 Konstantní atributy........................................................................................... 60 6.2.2 Konstantní metody ........................................................................................... 60 6.3 Statické členy ........................................................................................................... 62 6.3.1 Statické atributy................................................................................................ 62 6.3.2 Statické metody ................................................................................................ 62 6.4 Spřátelené třídy a funkce.......................................................................................... 65 6.5 Vložené datové typy................................................................................................. 66 Proudová knihovna........................................................................................................... 67 7.1 Bázový proud – ios................................................................................................... 67 7.1.1 Stavy datového proudu..................................................................................... 67 7.1.2 Formátování ..................................................................................................... 67 7.2 Výstup – ostream...................................................................................................... 70 7.3 Vstup – istream......................................................................................................... 71 7.4 Souborové proudy .................................................................................................... 71 7.4.1 Vstupní souborový proud – ifstream................................................................ 72 7.4.2 Výstupní souborový proud – ofstream ............................................................. 72 7.4.3 Vstupně/výstupní souborový proud – fstream ................................................. 72 7.5 Řetězcové proudy..................................................................................................... 73 7.5.1 Vstupní řetězcový proud – istrstream............................................................... 73 7.5.2 Výstupní řetězcový proud – ostrstream............................................................ 73 7.5.3 Vstupně/výstupní řetězcový proud – strstream ................................................ 73 Programování pod Linuxem............................................................................................. 77 8.1 Základní pojmy ........................................................................................................ 77 8.1.1 Unix.................................................................................................................. 77
Algoritmy a programování v jazyce C/C++
8.1.2 Linux ................................................................................................................ 77 8.1.3 GNU ................................................................................................................. 77 8.2 Souborový systém, spouštění programů................................................................... 78 8.3 Editory...................................................................................................................... 79 8.4 Kompilace programu................................................................................................ 79 8.5 Ladění programu ...................................................................................................... 80 8.6 Knihovny.................................................................................................................. 80 8.6.1 Statické knihovny............................................................................................. 80 8.6.2 Dynamické knihovny ....................................................................................... 81 8.7 Standardy.................................................................................................................. 81 9 Neprobrané partie z C++ .................................................................................................. 84
Obsah
5
1 Úvod Tento učební text navazuje na skriptum Algoritmy a programování v jazyku C/C++ 1. díl. Kromě souborů, které se ještě převážně týkají programování v jazyce C, jsou především probírány možnosti programování v jazyce C++. Po uvedení základních odlišností mezi jazyky C a C++ jsou vysvětleny základy objektově orientovaného programování. Poté výklad přechází k náročnějším pojmům, jako je dědičnost, polymorfismus, virtuální metody a abstraktní třídy. Dále je věnována velká pozornost proudové knihovně. Na závěr jsou uvedeny a vysvětleny základní principy programování v unixových operačních systémech.
Autoři si text rozdělili takto: Mgr. Antonín Přibyl je autorem kapitoly 8, ostatní text a formátování proved Ing. David Matoušek.
Autoři, leden 2005
6
Algoritmy a programování v jazyce C/C++
2 Práce se soubory v jazyce C Každý program (zapsaný v libovolném programovacím jazyce) potřebuje nutně zpracovávat vstup a výstup. Byl by jistě „skvělý“ program, který dokáže provést rychlý výpočet Fourierovy transformace, ale jak bychom jej mohli používat bez vstupu a výstupu? Program bez vstupu neumožňuje žádné ovládání (například z klávesnice) a program bez výstupu zase nedokáže zobrazit žádné výsledky (například na obrazovce). Možnostem použití standardního vstupu (klávesnice) a standardního výstupu (obrazovky) jsme se věnovali již dříve. Nyní si ukážeme, že kromě těchto standardních prostředků lze používat i tzv. soubory. Soubor je nejčastěji definován jako uspořádaná množina dat uložená mimo operační paměť. Ve většině případů jsou soubory uloženy na disku a pak také používáme pojem diskový soubor. Výhodou souborů je skutečnost, že data v nich uložená zůstávají k dispozici i po ukončení programu nebo vypnutí počítače (nevymažou se, protože nejsou v paměti, ale na disku). Takže program může své výsledky trvale uložit. Jako klasický příklad programu, který nutně potřebuje používat diskové soubory, lze uvést textový editor. Bylo by jistě nešikovné, kdybychom mohli editovat text, měli možnost jej vytisknout na tiskárně, ale už nemohli daný text uložit do souboru. Znamenalo by to, že k textu se nelze nikdy později vrátit a provést nějaká rozšíření či úpravy. Místo toho musí být celý text napsán znovu. Editor bez možnosti práce se soubory se chová jako obyčejný psací stroj. Ten také neumožňuje výsledky práce uchovat jinak, než v tištěné podobě bez možnosti pozdějších úprav. Pomocí knihovních funkcí jazyka C můžeme se soubory pracovat buď pomocí funkcí odpovídajících volání služeb operačního systému nebo používat obecnější (a také jednodušší) způsob založený na tzv. proudech. Druhý z uvedených způsobů má výhodu právě v tom, že nesouvisí přímo s operačním systémem. Tak tedy lze vytvářet programy, které jsou implementačně nezávislé a tedy snadno přenositelné mezi různými operačními systémy (UNIX, DOS, Windows). To je hlavní důvod, proč se tomuto způsobu ovládání souborů budeme věnovat. Odpovídající funkce jsou definovány v hlavičkovém souboru stdio.h. Z hlediska programování rozeznáváme dva typy souborů: • textové soubory – data jsou uložena v „čitelné“ podobě, • binární soubory – data jsou uložena jako sekvence bajtů, tedy úsporněji.
2.1
Základní funkce
Základní funkce pro práci se soubory jsou použitelné jak pro textové, tak i pro binární soubory. První věcí, pokud chceme pracovat se soubory na úrovni proudů, je definice proměnné typu ukazatel na FILE (jedná se o strukturu, její definice je však implementačně závislá a proto ji nebudeme dále rozebírat): FILE *vstup;
2. Práce se soubory v jazyce C
7
2.1.1
Otevření souboru – fopen Každý soubor, se kterým chceme pracovat, je třeba nejdříve otevřít pomocí funkce fopen. Hlavička:
FILE *fopen(const char *FileName, const char *Mode); Parametry: • FileName je řetězec jména souboru, který chceme otevřít, • Mode je řetězec, který definuje způsob otevření, viz tab. 2.1. Tab. 2.1 Význam řetězce Mode
Mode "r"nebo "rt" "w" nebo "wt" "a" nebo "at" "rb", "wb", "ab" "r+"nebo "r+t" "w+" nebo "w+t" "a+" nebo "a+t" "r+b", "w+b", "a+b"
Význam otevře daný soubor pro čtení (textový režim) vytvoří daný soubor pro zápis (textový režim) otevře daný soubor pro připojení na konec (textový režim) to samé pro binární režim otevře daný soubor pro čtení i zápis (textový režim) vytvoří daný soubor pro čtení i zápis (textový režim) otevře daný soubor pro připojení na konec (textový režim) to samé pro binární režim
Několik poznámek (platí pro textové i binární soubory): • pokud je soubor otevírán podle "r" nebo "r+", musí existovat, • pokud je soubor otevírán pro zápis "w" nebo "w+" a existuje, bude přepsán (pokud neexistuje, vytvoří se), • pokud je soubor otevírán pro připojení na konec "a" nebo "a+" a neexistuje, bude vytvořen. Návratová hodnota: Pokud funkce fopen proběhla bez chyby, vrací ukazatel na FILE. Návratová hodnota se tedy přiřazuje do zavedeného ukazatele (přes něj voláme další funkce). V případě, že soubor není možno otevřít, vrací NULL. Zde jsou dva příklady: a) Otevře soubor POKUS.TXT umístěný v aktuálním adresáři pro čtení v textovém režimu: FILE *f; f=fopen("POKUS.TXT","r"); b) Otevře soubor POKUS.TXT umístěný v adresáři PROGRAMY disku C pro zápis v textovém režimu (připomeňme, že znak \ musí být v zápisu řetězce zdvojen, protože se jedná o řídicí znak jazyka C): FILE *f; f=fopen("C:\\PROGRAMY\\POKUS.TXT","w"); 2.1.2
Zavření souboru – fclose Soubor zavíráme funkcí fclose, zde je její hlavička:
int fclose(FILE *File);
8
Algoritmy a programování v jazyce C/C++
Parametr: • File je soubor (jako FILE*), který chceme zavřít. Návratová hodnota: V případě úspěchu vrací funkce fclose hodnotu 0, jinak EOF. Příklad (zavřeme dříve otevřený soubor): fclose(f); Poznámka: Po ukončení operací nad souborem je třeba soubor vždy zavřít. Toto pravidlo platí hlavně pro soubory, do kterých bylo zapisováno. Zavření souboru totiž zajistí vyprázdnění souborových bufferů a tím skutečné zapsání všech dat do souboru!!! Viz také kapitolu 2.4.1. 2.1.3
Test konce souboru – feof Většina programů čte soubor od začátku do konce, například po jednotlivých znacích. Pro ukončení cyklu, který realizuje takový algoritmus, je třeba zjistit, zda již nebylo dosaženo konce souboru. Pokud je třeba testovat konec souboru, je nejvýhodnější použít makro feof. Zde je jeho hlavička: int feof(FILE *File); Parametr: • File je soubor (jako FILE*), který testujeme na konec. Návratová hodnota: Makro feof vrací nulovou hodnotu (false), pokud jsme ještě nedosáhli konce souboru. Jinak vrací nenulovou hodnotu (true).
2.2
Textové soubory
Textové soubory jsou tvořeny posloupností znaků členěných do řádků různé délky. Konec řádku je proveden jedním (případně oběma) z těchto znaků (níže uvedené funkce ošetřují všechny možnosti): • '\r' – carriage return (návrat vozíku), dekadicky 13 (0x0D), • '\n' – life feed (posun o řádek), dekadicky 10 (0x0A). Zpracování textového souboru je sekvenční (od začátku směrem ke konci). Zpracovávají se jednotlivé znaky, celé skupiny znaků nebo celé řádky. 2.2.1
Čtení a zápis do souboru – getc, putc, fgets, fputs, fprintf, fscanf Pokud chceme číst/zapisovat do souboru, je třeba jej otevřít voláním funkce fopen. Jinak se jedná o poměrně jednoduchou akci, protože uvedené funkce pracují podobně, jako funkce operující nad standardním vstupem a výstupem.
2. Práce se soubory v jazyce C
9
Tab. 2.2 Porovnání funkcí pro práci se standardním vstupem a výstupem s funkcemi pro práci se soubory
Standardní vstup/výstup getchar čte znak ze standardního vstupu putchar zapisuje znak na standardní výstup gets čte řetězec znaků ze standarního vstupu puts zapíše řetězec znaků na standardní výstup printf formátovaný výstup scanf formátovaný vstup
Soubory getc čte znak ze souboru putc zapisuje znak do souboru fgets čte řetězec znaků ze souboru fputs zapíše řetězec znaků do souboru fprintf formátovaný výstup do souboru fscanf formátovaný vstup ze souboru
Stačí tedy uvést hlavičky nových funkcí: int getc(FILE *f); int putc(char c, FILE *f); char *fgets(char *s, int n1, FILE *f); int fputs(const char *s, FILE *f); int fprintf(FILE *f, const char* format, arg1, arg2,...,argn); int fscanf(FILE *f, const char* format, adr1, adr2,...,adrn); Každý překladač definuje standardní vstup a výstup jako proměnné typu FILE*. V závislosti na typu překladače bývá definováno několik standardních souborů dle tab. 2.3 (minimálně však první tři!). Tab. 2.3 Standardní soubory
Identifikátor stdin stdout stderr stdaux stdprn
Význam standardní vstup (obvykle klávesnice) standardní výstup (obvykle obrazovka) výpis chybových hlášení (obvykle obrazovka) sériové rozhraní paralelní rozhraní
Nyní by mělo být jasné, že tyto dva zápisy způsobí stejnou akci: printf("Ahoj...\n"); fprintf(stdout,"Ahoj...\n"); 2.2.2
Jednoduché příklady V této kapitole je uvedeno několik základních příkladů, řazení je od jednodušších ke složitějším.
Příklad 2.02.01: Program čte obsah souboru TEXT.TXT po jednom znaku a znaky vypisuje na obrazovce. Testuje se úspěšnost otevření souboru. Program končí, když je funkcí getc přečten znak EOF (konec souboru). Všimněte si, že proměnná c je typu int a ne char, jak by se dalo předpokládat. Je to z toho důvodu, že hodnota EOF (celočíselná) se při konverzi na typ char zobrazí jako platný
1
Čtení je ukončeno, pokud se přečte n-1 znaků (vtipné omezení délky načteného řetězce).
10
Algoritmy a programování v jazyce C/C++
znak a nikoli jako informace o konci souboru. Pokud zavedeme proměnnou c typu char, musíme pro testování konce souboru použít makro feof! #include <stdio.h> void main() { FILE* f; int c; if(f=fopen("TEXT.TXT","r"),f==NULL) fprintf(stderr,"Soubor nelze otevrit\n"); else{ while(c=getc(f),c!=EOF) putchar(c); fclose(f); } } Příklad 2.02.02: Program čte obsah souboru MAIN.C po řádcích a ty zapisuje do souboru SEZNAM.TXT, v novém souboru je každý řádek očíslován. Řádky se načítají funkcí fgets (maximální délka řádku je 199 znaků). Konec souboru je testován makrem feof. Zvláštností je výpis chyb do standardního souboru stderr a vracení chybových kódů 1 (nelze otevřít vstupní soubor) a 2 (nelze vytvořit výstupní soubor). Více viz kapitolu 2.5. #include <stdio.h> int main() { FILE *fr, *fw; char radek[200]; int pocet=0; if(fr=fopen("MAIN.C","r"),fr==NULL){ fprintf(stderr,"Soubor MAIN.C nelze otevrit\n"); return 1; } if(fw=fopen("SEZNAM.TXT","w"),fw==NULL){ fprintf(stderr,"Soubor SEZNAM.TXT nelze vytvorit\n"); return 2; } while(fgets(radek,200,fr),!feof(fr)) fprintf(fw,"%04d: %s",++pocet,radek); fclose(fr); fclose(fw); printf("Dobehlo uspesne"); return 0; }
2. Práce se soubory v jazyce C
11
Příklad 2.02.03: Program vytiskne čísla 0 až 9 do souboru POKUS.DAT, každé číslo bude na samostatném řádku. #include <stdio.h> void main() { FILE* f; int i; const char* jmeno="POKUS.DAT"; if(f=fopen(jmeno,"w"),f==NULL) fprintf(stderr,"Soubor %s nelze otevrit\n",jmeno); else{ for(i=0;i<10;i++) fprintf(f,"%d\n",i); fclose(f); } } Příklad 2.02.04: Program čte celá čísla ze souboru POKUS.DAT a vypočítá jejich součet a aritmetický průměr. Algoritmus čtení je navržen tak, že není předem zadán počet čísel. #include <stdio.h> void main() { FILE* f; int i,soucet=0,pocet=0; float prumer; const char* jmeno="POKUS.DAT"; if(f=fopen(jmeno,"r"),f==NULL) fprintf(stderr,"Soubor %s nelze otevrit\n",jmeno); else{ while(!feof(f)){ if(fscanf(f,"%d",&i)==11){ pocet++; soucet+=i; } } prumer=2pocet?(float)soucet/pocet:0; fclose(f); } printf("Součet=%d\nArit. prumer=%f\n",soucet,prumer); }
1
Jednoduchý test, zda se podařilo přečíst celé číslo ze souboru (kdyby řádek neobsahoval zápis čísla, ale jen „obyčejný řetězec“). Rovněž lze psát: if(fscanf(f,"%d",&i))
2
Použit ternární operátor (aby se zabránilo možnému dělení nulou) a přetypování na float (aby bylo dělení reálné).
12
Algoritmy a programování v jazyce C/C++
Příklad 2.02.05: Mějme soubor PRIJMENI.TXT, který obsahuje příjmení osob (na každém řádku je jedno příjmení, maximální délka 30 znaků). Program načte obsah tohoto souboru a zjistí počet řádků, podle toho dynamicky naalokuje pole řetězců pro načtení všech příjmení. Načte jednotlivé řádky. Potom v tomto poli provede seřazení metodou Bubble Sort a výsledek uloží do souboru SERAZENO.TXT. Při vlastní realizaci řazení nedochází k přesouvání řetězců v paměti, ale pouze k výměně adres jednotlivých řetězců v poli Radky! Takže se nepoužívá funkce strcpy (na kopírování řetězců). Připomeňme, že řetězce musíme porovnávat pomocí funkce strcmp! #include #include #include #include
<stdio.h> <malloc.h> //pro Dev C++, jinak bude alloc.h <string.h>
int main() { //vstupni a vystupni soubory: FILE *fr, *fw; //pocet radku: int N=0; //maximalni delka radku+1: const int MAX=31; //pole obsahujici adresy retezcu z jednotlivych radku: char **Radek=NULL; //pomocny ukazatel: char *p; //pro cteni jednoho znaku: char c; //pro cykly: int i,d; //otevre vstupni soubor (spocita radky): if(fr=fopen("PRIJMENI.TXT","r"),fr==NULL){ fprintf(stderr,"Soubor PRIJMENI.TXT nelze otevrit\n"); return 1; } //hleda '\n' a podle toho pocita radky: while(c=getc(fr),!feof(fr)) if(c= ='\n') N++; //zavre soubor: fclose(fr); //otevre vystupni soubor: if(fw=fopen("SERAZENO.TXT","w"),fw==NULL){ fprintf(stderr,"Soubor SERAZENO.TXT nelze vytvorit\n"); return 2; } //alokace pole radku: Radek=(char**)malloc(N*sizeof(char*));
2. Práce se soubory v jazyce C
13
if(Radek==NULL){ fprintf(stderr,"Chyba pri alokaci pameti\n"); return 3; } //alokace jednotlivych radku: for(i=0;i0){ //vymeni ukazatele, pokud je spatne poradi: p=Radek[i]; Radek[i]=Radek[i+1]; Radek[i+1]=p; c=1; } d--; }while(c!=0);
//zapise data: for(i=0;i
14
Algoritmy a programování v jazyce C/C++
printf("Dobehlo uspesne"); getch(); return 0; }
2.3
Binární soubory
Binární soubory jsou tvořeny posloupností bajtů vyjadřující data v binární podobě, obvykle se jedná o skupiny položek stejného typu (například několik struktur stejného typu zapsaných za sebou). S jednotlivými položkami lze pak pracovat buď sekvenčně (procházet je od začátku do konce) nebo v libovolném pořadí (protože jsou položky stejného typu, lze součinem velikosti položky a pořadového čísla zjistit pozici v souboru). Po otevření souboru (pokud soubor neotevřeme v režimu připojení na konec) je vnitřní ukazovátko v souboru nastaveno na první položku, po každé operaci čtení/zápisu se ukazovátko posune na další položku. Funkcí fseek se pak lze posouvat na libovolné místo. 2.3.1
Blokové čtení a zápis dat – fread, fwrite Pro čtení/zápis binárních souborů se používají funkce fread a fwrite. Zde jsou jejich hlavičky:
int fread(void *buffer, int size, int n, FILE *f); int fwrite(const void *buffer, int size, int n, FILE *f); Parametry obou funkcí: • buffer – adresa vyrovnávací paměti (odsud se data čtou při použití fwrite nebo se sem zapisují při použití fread), • size – velikost jedné položky v bajtech, • n – počet současně čtených/zapisovaných položek, • f – soubor, nad nímž se provádí operace. Návratová hodnota: Pokud proběhlo volání funkce bez chyby, vrací počet skutečně zapsaných/přečtených položek (ne bajtů!). 2.3.2
Posun ukazovátka v souboru – fseek Pokud chceme položky souboru číst/zapisovat v libovolném pořadí (někdy se také říká v náhodném pořadí), použijeme k přestavení ukazovátka v souboru funkci fseek. Hlavička:
int fseek(FILE *f, long posuv, int odkud); Tato funkce přesune ukazovátko souboru f o tolik bajtů, kolik je specifikováno posuv. Místo odkud se posouvá je dáno odkud (viz tab. 2.4). Při úspěšném posunutí vrací 0. Tab. 2.4 Možné hodnoty parametru odkud
odkud SEEK_CUR SEEK_END SEEK_SET
Význam aktuální pozice v souboru konec souboru začátek souboru
2. Práce se soubory v jazyce C
15
Příklad použití (umístí ukazovátko na předposlední bajt souboru): int fseek(f,-1,SEEK_END); Příklad použití (umístí ukazovátko na začátek, vhodné pro opakování čtení; pro tento případ má smysl používat funkci fseek i pro textové soubory; také lze použít funkci rewind, viz kapitolu 2.4.2): int fseek(f,0,SEEK_BEGIN); 2.3.3
Zjistění aktuální pozice ukazovátka – ftell Funkce ftell vrací aktuální pozici ukazovátka souboru (při chybě pak –1), hlavička:
long ftell(FILE *f); Příklad: Níže je uvedena funkce fsize, která zjistí délku otevřeného souboru. K tomu jsou použity funkce ftell a fseek. long fsize(FILE *f) { long puvodni_pozice, delka; //schování původní pozice1: puvodni_pozice=ftell(f); //přesun na konec souboru: fseek(f,0,SEEK_END); //ukazovátko je délka: delka=ftell(f); //přesun zpět: fseek(f,puvodni_pozice,SEEK_SET); return delka; } Příklad 2.02.06: Program čte obsah textového souboru OSOBY.TXT po řádcích. Každý řádek obsahuje jméno, příjmení a osobní číslo (vše odděleno libovolným počtem mezer nebo tabulátorů). Dané údaje jsou načteny do struktury typu TOsoba a uloženy v binární formě do souboru OSOBY.DAT. Nakonec je vypsána délka obou soborů (použita funkce fsize). Struktura TOsoba dovoluje načítat jména dlouhá 11 znaků, příjmení dlouhá 15 znaků a osobní číslo jako celé číslo se znaménkem. Z textového souboru se údaje načítají funkcí fscanf a v binární podobě pak ukládají funkcí fwrite. Příklad vstupního souboru je uveden níže. Vstupní soubor měl délku 217 bajtů, výstupní soubor pak 192 bajtů (jedná se o 6 záznamů délky 32 bajtů, takže 6·32 = 192). Příklad souboru OSOBY.TXT: Jaroslav Jankovsky 10001 Jaroslav Venclovsky 10002 Miroslav Vapenik 10003 Ondrej Chalupsky 10004 1
Pak lze tuto funkci použít i nad souborem, v němž jsme se již pohybovali.
16
Algoritmy a programování v jazyce C/C++
Josef Jarosovsky Frantisek
10005 Jandacek 10006
Vlastní program: #include <stdio.h> //struktura pro ukladani udaju do bin. souboru: typedef struct{ char Jmeno[12]; char Prijmeni[16]; int OsCislo; } TOsoba; //zjistuje velikost otevreneho souboru: long fsize(FILE *f) { long puvodni_pozice, delka; puvodni_pozice=ftell(f); fseek(f,0,SEEK_END); delka=ftell(f); fseek(f,puvodni_pozice,SEEK_SET); return delka; } int main() { //vstupni a vystupni soubory: FILE *fr, *fw; //promenna pro nacteni/ulozeni udaju: TOsoba o; //otevre vstupni textovy soubor: if(fr=fopen("OSOBY.TXT","rt"),fr==NULL){ fprintf(stderr,"Soubor OSOBY.TXT nepodarilo otevrit\n"); return 1; } //otevre vystupni binarni soubor: if(fw=fopen("OSOBY.DAT","wb"),fw==NULL){ fprintf(stderr,"Soubor OSOBY.DAT nepodarilo vytvorit\n"); return 2; } //cteni radku z textoveho souboru a zapis v binarni forme: do{ //vyplni strukturu o nulami: memset(&o,0,sizeof(o)); //není nezbytné //cte jmeno, prijmeni a osobni cislo: if(fscanf(fr,"%11s",&o.Jmeno)!=1) break; if(fscanf(fr,"%15s",&o.Prijmeni)!=1) break; if(fscanf(fr,"%d",&o.OsCislo)!=1) break;
2. Práce se soubory v jazyce C
17
//zapise binarne: fwrite(&o,sizeof(o),1,fw); } while(!feof(fr)); //vypise delky souboru: printf("Delka souboru OSOBY.TXT=%d B\n",fsize(fr)); printf("Delka souboru OSOBY.DAT=%d B\n",fsize(fw)); //zavre soubory: fclose(fr); fclose(fw); return 0; } Hexadecimální zobrazení souboru OSOBY.DAT: Jednotlivé bajty 4A 61 72 6F 73 6C 6F 76 73 6B 79 00 4A 61 72 6F 73 6C 6C 6F 76 73 6B 79 4D 69 72 6F 73 6C 6E 69 6B 00 00 00 4F 6E 64 72 65 6A 75 70 73 6B 79 00 4A 6F 73 65 66 00 73 6F 76 73 6B 79 46 72 61 6E 74 69 61 63 65 6B 00 00
61 00 61 00 61 00 00 00 00 00 73 00
76 00 76 00 76 00 00 00 00 00 65 00
00 00 00 00 00 00 00 00 00 00 6B 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
4A 11 56 12 56 13 43 14 4A 15 4A 16
61 27 65 27 61 27 68 27 61 27 61 27
6E 00 6E 00 70 00 61 00 72 00 6E 00
6B 00 63 00 65 00 6C 00 6F 00 64 00
Jako znaky Jaroslav....Jank ovsky........'.. Jaroslav....Venc lovsky.......'.. Miroslav....Vape nik..........'.. Ondrej......Chal upsky........'.. Josef.......Jaro sovsky.......'.. Frantisek...Jand acek.........'..
Příklad 2.02.07: Program přečte obsah binárního souboru OSOBY.DAT (z předchozího příkladu) pozpátku a vypíše jej na obrazovce. Tento příklad demonstruje používání libovolného přístupu k datům uloženým v binárním souboru. Přesuny jsou realizovány funkcí fseek. Před každým čtením je třeba pozici v souboru přesunout o tolik délek struktury TOsoba, kolikátý záznam čteme. Takže pro první záznam od konce musí být vůči konci pozice –sizeof(TOsoba), pro druhý dvojnásobek a tak dál. V programu se používá proměnná p, která se před každým čtení sníží o délku struktury TOsoba. Zajímavé je použití výsledku funkce printf (vrací počet vypsaných znaků). Takto jsme elegantně realizovali podtržení řádku s hlavičkou vypisovaných údajů (viz dále). #include <stdio.h> //struktura pro nacteni dat z bin. souboru: typedef struct{ char Jmeno[12]; char Prijmeni[16]; int OsCislo; } TOsoba;
18
Algoritmy a programování v jazyce C/C++
int main() { //vstupni soubor: FILE *fr; //promenna pro nacteni udaju: TOsoba o; //pozice pro cteni od konce: int p=0; //pocet znaku vypsanych v zahlavi: int x; //otevre vstupni binarni soubor: if(fr=fopen("OSOBY.DAT","rb"),fr==NULL){ fprintf(stderr,"Soubor OSOBY.DAT nelze otevrit\n"); return 2; } //tisk hlavicky: x=printf("%-16s %-11s %7s","Jmeno","Prijmeni","OsCislo"); printf("\n"); //radek pomlcek: for(;x>0;x--) printf("-"); printf("\n"); //presun na konec souboru: fseek(fr,0,SEEK_END); //presun o zaznam zpet, cteni a vypis: while(p-=sizeof(TOsoba),fseek(fr,p,SEEK_END)==0) if(fread(&o,sizeof(TOsoba),1,fr)==1) printf("%-16s %-11s %07d\n", o.Jmeno,o.Prijmeni,o.OsCislo); //zavre soubor: fclose(fr); return 0; } Na obrazovce se objeví: Jmeno Prijmeni OsCislo -----------------------------------Frantisek Jandacek 0010006 Josef Jarosovsky 0010005 Ondrej Chalupsky 0010004 Miroslav Vapenik 0010003 Jaroslav Venclovsky 0010002 Jaroslav Jankovsky 0010001
2. Práce se soubory v jazyce C
19
2.4
Další užitečné funkce pro práci se soubory
V této kapitole si uvedeme několik dalších funkcí definovaných v hlavičkovém souboru stdio.h. Opět se jedná o funkce obecně použitelné, nezávislé na operačním systému (také se jedná o funkce schválené standardizací ANSI C). 2.4.1
Vyprázdnění souborových bufferů – fflush Operační systémy pracují s diskovými sobory tak, že k nim přistupují skrze tzv. buffery (vyrovnávací paměti). To jim umožní najednou načíst/zapsat více bajtů a tak urychlit práci se souborem. Z toho vyplývá, že pokud požadujeme například zápis jednoho znaku, neprovede se tato operace do souboru okamžitě, ale znak se uloží do bufferu. Až je celý buffer zaplněn, zapíšou se všechny znaky najednou. Pokud tedy není zapsán dostatečný počet znaků, nedojde k jejich faktickému uložení, ale stále zůstávají v bufferu. Funkce fflush vyprázdní buffer souboru (tím zajistí zápis dat na disk). Tuto funkci je vhodné použít v případě, že chceme k nově vytvořeným datům přistoupit dříve, než soubor zavřeme (typicky v režimu současného čtení a zápisu). Stejnou akci provede funkce fclose při zavírání souboru, proto je tak důležité zavírat soubory! Hlavička: int fflush(FILE *f); Návratová hodnota: Funkce vrací 0 po úspěšném provedení a EOF při chybě. 2.4.2
Přesun ukazovátka souboru na začátek – rewind Funkce rewind přesune ukazovátko souboru na začátek, což je operace ekvivalentní použití fseek(f,0,SEEK_SET). Hlavička: void rewind(FILE *File); 2.4.3
Přesměrování souboru – freopen Funkce freopen umožňuje přesměrovat otevřený soubor do jiného souboru. Tuto funkci nejčastěji používáme pro přesměrování některého ze standardních souborů (stdout, stdin, stderr) do diskového souboru. Takže například místo výpisu dat na obrazovce, dostaneme stejný výstup do diskového souboru. Hlavička: FILE *freopen( const char *FileName,const char *Mode,FILE *File); Parametry: • FileName – řetězec názvu souboru, do kterého proběhne přesměrování, • Mode – způsob otevření souboru dle tab. 2.1, • FILE – soubor, který chceme přesměrovat. Návratová hodnota: Pokud funkce freopen proběhla bez chyby, vrací platný ukazatel na přesměrovaný soubor. V případě chyby vrací NULL. 2.4.4
Vytvoření dočasného souboru – tmpfile Funkce tmpfile vytvoří dočasný soubor, který otevře v režimu "w+b". Jméno je souboru přiděleno dynamicky, takový soubor se automaticky smaže při zavření. Hlavička: FILE *tmpfile();
20
Algoritmy a programování v jazyce C/C++
Návratová hodnota: Vrací ukazatel na strukturu FILE pro dočasný soubor. V případě chyby vrací NULL. 2.4.5
Smazání souboru – remove Funkce remove smaže soubor určený jeho jménem. Hlavička:
int remove(const char* FileName); Návratová hodnota: Funkce vrací 0 po úspěšném provedení a –1 při chybě. 2.4.6
Zjištění poslední chyby při práci se souborem – ferror Funkce ferror detekuje chybu při práci se souborem. Hlavička:
int ferror(FILE *File); Návratová hodnota: Funkce vrací 0, pokud k žádné chybě nedošlo. Jinak vrací kód chyby (závislé na operačním systému). 2.4.7
Smazání chyby – clearerr Funkce clearerr nuluje příznak chyby při práci se souborem, takže funkce ferror po volání této funkce vrací vždy hodnotu 0. Hlavička: void clearerr(FILE *File); Příklad 2.02.08: Program přečte obsah binárního souboru OSOBY.DAT pozpátku a vypíše jej na obrazovce nebo uloží do souboru OBRACENE.TXT. Jedná se o rozšíření příkladu 2.02.07. Výstup do souboru je řešen přes přesměrování standardního souboru stdout. Uživatel má možnost zvolit si, zda výpis proběhne na obrazovku nebo do souboru (v druhém případě proběhne přesměrování funkcí freopen). #include <stdio.h> #include //struktura pro nacteni z bin. souboru: typedef struct{ char Jmeno[12]; char Prijmeni[16]; int OsCislo; } TOsoba; int main() { //vstupni soubor: FILE *fr; //promenna pro nacteni udaju: TOsoba o; //pozice pro cteni od konce: int p=0; //pocet znaku vypsanych v zahlavi: int x;
2. Práce se soubory v jazyce C
21
//volba vypisu: int c; //otevre vstupni binarni soubor: if(fr=fopen("OSOBY.DAT","rb"),fr==NULL){ fprintf(stderr,"Soubor OSOBY.DAT nelze otevrit\n"); return 2; } //kam bude vystup? printf("Provest tisk na obrazovku? (A/N)\n"); c=toupper(getchar()); if(c= ='N') freopen("OBRACENE.TXT","wt",stdout); //tisk hlavicky: x=printf("%-16s %-11s %7s","Jmeno","Prijmeni","OsCislo"); printf("\n"); //radek pomlcek: for(;x>0;x--) printf("-"); printf("\n"); //presun o zaznam zpet, cteni a vypis: while(p-=sizeof(TOsoba),fseek(fr,p,SEEK_END)==0) if(fread(&o,sizeof(TOsoba),1,fr)==1) printf("%-16s %-11s %07d\n", o.Jmeno,o.Prijmeni,o.OsCislo); //zavre soubory: fclose(fr); fclose(stdout); return 0; }
2.5
Spolupráce s operačním systémem
Na závěr kapitoly pojednávající o souborech si ještě vysvětlíme, jak může program spolupracovat s operačním systémem. Vše se skrývá v hlavičce funkce main (existují i další varianty): int main(int argc, char** argv); 2.5.1
Návratová hodnota funkce main Návratová hodnota typu int je vracena operačnímu systému jako informace o běhu programu. Je obvyklé, že příkazem return 0 indikujeme úspěšné dokončení programu. Pokud dojde k chybě, můžeme jinými hodnotami návratových kódů signalizovat konkrétní chybu. Této možnosti jsme již využili v příkladu 2.02.02. 2.5.2
Parametry funkce main Zajímavější jsou parametry funkce main. Jedná se vlastně o text zadaný uživatelem jako parametry programu. Jednotlivé řetězce oddělené mezerami se uvažují samostatně.
22
Algoritmy a programování v jazyce C/C++
Počet jednotlivých řetězců je k dispozici v parametru argc (argument count) a řetězce samotné pak v poli řetězců argv (argument values). argv[0] vždy obsahuje plnou cestu ke spuštěnému programu, argv[1] je první řetězec zadaný jako parametr programu, atd. Pokud například napíšeme: PROGRAM.EXE VYSTUP.TXT VSTUP.TXT bude platit:
argc = 3 argv[0] = "C:\\ADRESAR\\PROGRAM.EXE" argv[1] = "VYSTUP.TXT" argv[2] = "VSTUP.TXT"
Vidíme tedy, že takto lze například vytvořit program, který si jméno vstupního a výstupního souboru přečte z parametrů programu. Jména souborů nebudou zadána v programu „natvrdo“ (bez možnosti změny) ani nebude vyžadováno zadávání jmen při spuštění programu. Praktický příklad je uveden v kapitole 7.5.3.
2. Práce se soubory v jazyce C
23
3 Úvod do programovacího jazyka C++ Programovací jazyk C++ je objektovým rozšířením jazyka C. Z toho důvodu je zřejmé, že zachovává zpětnou kompatibilitu k programovacímu jazyku C. Program napsaný v jazyce C lze bez vážnějších problémů přeložit i na překladači jazyka C++ (překladač C++ pouze více dbá na „čistotu“ kódu, např. souhlas datových typů). Není tedy nutné používat dva kompilátory: jeden pro C a druhý pro C++. Název C++ sám dosti napovídá o povaze tohoto programovacího jazyka. V syntaxi jazyka C značí zápis: C++ zvýšení proměnné C o 1. C++ je tedy, už podle označení, vylepšením jazyka C. Vylepšením se myslí skutečnost, že jsou v jazyce C++ zavedeny prostředky pro OOP (objektově orientované programování).
3.1
Základní rozdíly mezi C a C++ V této kapitole popíšeme základní rozdíly mezi jazyky C a C++.
3.1.1
Zjednodušený zápis komentáře V jazyce C se komentář může zapisovat pouze v této formě: /*Toto je komentář*/ V jazyce C++ je možno zapisovat komentář zkráceně, musí se však celý vejít na řádek: //Toto je kratší komentář, celý se vejde na řádek
3.1.2
Umístění deklarací v programu na libovolném místě V jazyce C je nutno deklarovat proměnné na začátku bloku. V jazyce C++ může být deklarace proměnné provedena až v místě bezprostřední potřeby. Takže je zcela přípustný například tento zápis: for(int i=0; i<100; i++) printf("%i",i); Proměnná i je deklarována těsně před svým použitím. Nadále je však nutno dodržet základní pravidlo: deklarace předchází použití.
3.2
Typ bool
Novinkou v C++ je zavedení typu logická proměnná. V jazyce C platí, že logické výrazy vrací celé číslo (0 nepravda, 1 pravda). Dále platí, že celočíselný výraz hodnoty 0 je chápán jako nepravdivý. Pokud je hodnota výrazu různá od 0, chápe se jako pravda. Tyto zvyklosti jsou v jazyce C++ z větší části zachovány. Pouze logické výrazy mají jako výsledek typ bool a proměnná tohoto typu nabývá hodnoty false (0, nepravda) a true (1, pravda).
3.3
Klíčová slova jazyka C++
V C++ je samozřejmě možno používat klíčová slova z jazyka C. Kromě toho jsou definována nová klíčová slova. Na tomto místě uvedeme jen ta, která budeme používat: class,
24
Algoritmy a programování v jazyce C/C++
public, protected, private, inline, virtual, operator, friend, catch, try, throw, template, dynamic_cast, namespace, using.
3.4
Funkce v C++
Funkce se v C++ deklarují prakticky stejným způsobem jako v jazyce C. Kromě toho je však možno používat tzv. přetížené funkce a funkce s implicitními parametry. Rovněž je možno „zjednodušit“ některé zápisy. 3.4.1
Přetížení funkcí (Function Overloading) Jazyk C++ umožňuje deklarovat několik funkcí stejného jména, různého prototypu. V místě volání funkce musí překladač rozhodnout (podle počtu a typu dosazených aktuálních parametrů, případně podle typu návratové hodnoty), kterou ze stejně pojmenovaných funkcí zavolá. void fce(int i) { printf("\n1. funkce: i=%i",i); } void fce(int i, double j) { printf("\n2. funkce: i=%i, j=%lf",i,j); } //volání funkce: fce(5); //volá se 1. funkce fce(5,1.75);//volá se 2. funkce fce(5,1); //volá se 2. funkce, 1 se přetypuje na double 1.0 3.4.2
Implicitní parametry funkcí Funkce může být deklarována tak, že některé parametry mají zadány implicitní hodnotu. To umožňuje volání funkce s menším počtem parametrů, než je uvedeno. Neuvedené parametry se nastaví podle implicitního přiřazení. Implicitní parametry je možno umísťovat pouze na konci seznamu deklarací parametrů: void test(int x, int y=3) { printf("%i %i",x,y); } //funkci test lze pak volat: test(1,2); //vytiskne: 1 2 test(0); //vytiskne: 0 3 Přetížení funkce je možno používat současně s implicitními parametry funkce: void fce(int i=1) { printf("\n1. funkce: i=%i",i); }
3. Úvod do programovacího jazyka C++
25
void fce(int i, double j=0) { printf("\n2. funkce: i=%i, j=%lf",i,j); } //volání funkce: fce(); //volá se 1. funkce fce(5,1.75);//volá se 2. funkce fce(5,1); //volá se 2. funkce, 1 se přetypuje na double 1.0 fce(5);//překladač neumí rozhodnout, kterou funkci má zavolat 3.4.3
Parametry funkce volané odkazem Základním rysem programovacího jazyka C je skutečnost, že jsou implementovány pouze funkce (tedy procedury nejsou zavedeny). Navíc jsou parametry do funkcí předávány vždy voláním hodnotou. To způsobuje, že funkce nemůže parametry měnit a předávat je jako výsledky. Například následující program nemůže, přes všechna naše očekávání, změnit hodnotu globální proměnné x a vytiskne postupně: x=2 i=3 x=2 #include <stdio.h> void test(int i) { i=3; printf("i=%i\n",i); } void main() { int x=2; printf("x=%i\n",x); test(x); printf("x=%i\n",x); } Připomeňme, že pokud chceme v jazyce C naprogramovat funkci, která má umožnit změnu parametrů, je třeba předávat ukazatele na dané proměnné. Říkáme, že parametry jsou volány přes ukazatel. Toto je modifikace předchozího programu, která již změní hodnotu proměnné x: #include <stdio.h> void test(int *i) { *i=3; printf("*i=%i\n",*i); }
26
Algoritmy a programování v jazyce C/C++
void main() { int x=2; printf("x=%i\n",x); test(&x); printf("x=%i\n",x); } Tento program tedy postupně vytiskne: x=2 *i=3 x=3 Je zřejmé, že toto řešení je poněkud těžkopádné. Protože uvnitř funkce se na danou proměnnou musíme odkazovat pomocí operátoru dereference (*) a jako parametr funkce musíme předat adresu proměnné x (použít operátor reference &). Aby se programovací jazyk C++ přiblížil zvyklostem z jiných vyšších programovacích jazyků, je možno používat tzv. referenci (pozor, nezaměňovat s operátorem reference). Reference je speciální typ ukazatele, který umožňuje stejný způsob zápisu, jako pro běžné proměnné. Reference se deklaruje pomocí operátoru reference (&). V našem případě je možno funkci test napsat takto, zde se jedná o parametry volané odkazem: #include <stdio.h> void test(int& i) { i=3; printf("i=%i\n",i); } void main() { int x=2; printf("x=%i\n",x); test(x); printf("x=%i\n",x); }
//i je reference
//pozor, zde již není &
Program postupně vypíše: x=2 i=3 x=3 Všimněme si, že při použití reference není nutno v těle funkce přistoupit k proměnné přes operátor dereference (*) a i volání funkce probíhá normálně, není třeba zapisovat operátor reference (&) pro předání adresy objektu.
3. Úvod do programovacího jazyka C++
27
3.5
Zjednodušená deklarace datových typů
V C++ není nutno používat operátor typedef pro deklaraci uživatelsky definovaných typů odvozených ze: struct, union, enum, class1. Tyto dva zápisy jsou korektní, pouze ten vpravo je kratší: typedef struct CLOVEK{ float vaha; int vyska; };
3.6
struct CLOVEK{ float vaha; int vyska; };
Operátory jazyka C++
V jazyce C++ je samozřejmě možno používat operátory jazyka C. Kromě toho jsou implementovány nové operátory a také je možno provádět přetěžování. 3.6.1
Přetížené operátory (Overloading Operators) V jazyce C++ je dovoleno přetížit standardní operátory jazyka C, tzn. přiřadit operátorům jazyka C kromě jejich hlavního významu i nějaký jiný (nový) význam. Přetížení se provádí pomocí tzv. operátorových funkcí. Přetížení je možno zajistit pro nově zavedený datový typ. Význam operátorů pro datové typy jazyka (myšleno například int), nelze změnit. Příklad ukazuje přetížení operátoru + tak, aby bylo možno snadno provádět součet dvou komplexních čísel: struct complex{ float re, im; }; complex operator+(complex a1, complex a2) { complex temporary; temporary.re=a1.re+a2.re; temporary.im=a1.im+a2.im; return temporary; } //pak lze psát: complex a,b,c; a.re=1; a.im=2; b.re=-1; b.im=1; c=a+b;
//použití přetíženého operátoru +
printf("\n%f %f",c.re,c.im); 3.6.2
Nové operátory jazyka C++ Programovací jazyk C++ definuje některé nové operátory, jako nové operátory lze označit i operátory << a >>, které jsou v jazyce C++ přetíženy (mají dvě funkce). Jako nový označíme rovněž operátor –>, který se v jazyce C nepoužívá příliš často. Viz tab. 3.1. 1
Viz kapitolu 4.
28
Algoritmy a programování v jazyce C/C++
Tab. 3.1 Nové operátory jazyka C++
Operátor << >> –> :: new, new[ ] delete, delete[ ]
Popis vkládací operátor (insertor), posuv vlevo extrakční operátor (extraktor), posuv vpravo ukazatelový selektor operátor příslušnosti (scope resolution) vytvoření dynamického objektu zrušení dynamického objektu
Nyní uvedeme význam a použití těchto nových operátorů: 3.6.3
Vkládací operátor << a extrakční operátor >> Tyto dva operátory již známe ze standardního jazyka C. Slouží pro bitový posuv doleva nebo doprava. V C++ jsou tyto operátory přetíženy a mají i jiný, snad ještě mnohem důležitější, význam. Tyto dva operátory značně usnadňují vstup a výstup pro základní datové typy. Uvědomme si, že ve standardním C je nutno používat funkci scanf pro vstup z klávesnice a funkci printf pro výstup na obrazovku. Navíc je nutno znát konverze pro všechny datové typy. S použitím operátorů << a >> se tyto operace značně zjednodušší. Vstup nebo výstup pak probíhá přes tzv. v/v proudy (I/O streams). Standardní vstupní proud (vstup z klávesnice) se jmenuje cin, standardní výstupní proud (výstup na obrazovku) se jmenuje cout. Aby bylo možno operátory používat v tomto novém významu, je třeba do zdrojového textu přidat hlavičkový soubor iostream.h direktivou #include: #include
• Vkládací operátor (<<) vkládá prvky do proudu, syntaxe je: proud<<prvek Například konstrukce: cout<<"Ahoj, jak se máš?"; vloží do standardního výstupního proudu (tzn. vypíše na obrazovku) řetězec: Ahoj, jak se máš?
• Extrakční operátor (>>) čte prvky z proudu, syntaxe je: proud>>prvek Například konstrukce: char z; cin>>z; načte ze standardního vstupního proudu (tzn. z klávesnice) jeden znak a uloží jej do proměnné z.
3. Úvod do programovacího jazyka C++
29
Zde je trochu lepší příklad použití obou operátorů (zasláním speciálního „znaku“ endl se odřádkuje – provede se přechod na nový řádek; všimněte si, že operátory << a >> lze zřetězovat): #include void main() { int i=2; char c='a'; float f; cin>>f; cout<<"Reálné číslo je: "< struct complex{ float re, im; }; ostream& operator <<(ostream& str, complex c) { str<<"reálná část:"<
Ukazatelový selektor –> Měli bychom se předně opravit, tento operátor je definován i ve standardním jazyce C, jenže tam se prakticky nepoužívá.
30
Algoritmy a programování v jazyce C/C++
Ukazatelový selektor slouží pro zjednodušení zápisu v případě, že chceme přistoupit k položkám struktury, na kterou máme ukazatel. Význam tohoto operátoru snadno nahlédneme z následujícího příkladu: #include <stdio.h> void main() { struct CLOVEK { int vyska; float vaha; }; typedef CLOVEK *PCLOVEK; CLOVEK Novak; PCLOVEK Ukazatel; Novak.vyska=189; Novak.vaha=85; Ukazatel=&Novak; printf("Novák je vysoký: %i cm\n",(*Ukazatel).vyska); printf("Novák váží: %f kg\n",(*Ukazatel).vaha); } Všimněme si, že k položkám struktury se přistupuje jednoduše, pomocí selektoru. Tak například přiřazení do položky vyska proměnné Novak se provede takto: Novak.vyska=189; Pokud chceme k dané struktuře přistoupit pomocí ukazatele na ni, musíme použít operátor dereference (*). Protože je priorita operátoru dereference nižší, než priorita operátoru selekce (.), musí se výraz uzávorkovat: printf("Novák váží: %f kg\n",(*Ukazatel).vaha); Tento zápis je dosti složitý, proto je v C implementován ukazatelový selektor –>, jehož použití má stejný efekt jako: (*)., takže poslední dva řádky lze zapsat přehledněji: printf("Novák je vysoký: %i cm\n",Ukazatel->vyska); printf("Novák váží: %f kg\n",Ukazatel->vaha); 3.6.5
Operátor příslušnosti :: Operátor příslušnosti umožňuje například přistoupit ke globálnímu objektu, který je zastíněn lokální deklarací. Další význam vyplyne v kapitole 4.1.
3. Úvod do programovacího jazyka C++
31
Například ve funkci test je globální proměnná x zastíněna lokální deklarací, takže se vypíše 3 a ne 0: #include <stdio.h> int x=0;
//definice globální proměnné x
void test() { int x=3;
//zastínění globálního x
printf("\n%i",x); } void main() { test(); } Pokud chceme ve funkci test přistoupit ke globální proměnné x musíme použít operátor ::, například (teď se vypíše globální x=0, lokální x=3): #include <stdio.h> int x=0; void test() { int x=3; printf("\nglobální x=%i, lokální x=%i",::x,x); } void main() { test(); } 3.6.6
Operátory pro dynamickou správu paměti Dynamickou správou paměti rozumíme dynamické vytváření a rušení datových objektů. Vytvoření dynamického objektu (new) Vzpomeňme si, že ve standardním jazyce C je možno vytvářet dynamické objekty pomocí funkce malloc. Tyto dynamické objekty se ruší pomocí funkce free. Vytvoření dynamické proměnné tímto způsobem je dosti složité. V jazyce C++ je k dispozici operátor new, s jehož použitím je vytvoření dynamické proměnné hračkou. Pokud chceme vytvořit dynamické pole, které obsahuje 1000 položek typu int napíšeme:
32
Algoritmy a programování v jazyce C/C++
#include #include <process.h> void main() { int *p; p=new int[1000]; if(p==NULL) exit(-1); else cout<<"Ok, pole vytvořeno..."; . . delete[] p; //uvolnění pole z paměti, když už jej nepotřebujeme } Poznamenejme, že stejně snadno lze vytvářet i jednoduché dynamické proměnné a dokonce jim i přiřadit počáteční hodnotu. Například tento zápis: #include void main() { int *p=new int(10); //vlastně volání konstruktoru . *p=10; cout<<*p; . delete p; //uvolnění proměnné z paměti, když už ji nepotřebujeme } nejdříve vytvoří dynamickou proměnnou typu int s počáteční hodnotou 10, ukazatel na ni je p. Obsah této dynamické proměnné je možno měnit pomocí ukazatele na ni, samozřejmě je možno obsah dynamické proměnné vypsat. Zrušení dynamického objektu (delete) Pro zrušení dynamického objektu vytvořeného pomocí operátoru new se používá operátor delete nebo delete[ ]. Operátor delete uvolňuje z paměti jednoduché dynamické objekty (všechny proměnné vyjma polí). Operátor delete[ ] uvolňuje z paměti dynamická pole. Operátor delete resp. delete[ ] byl již použit ve dříve uvedených příkladech.
3.7
Závěrečné shrnutí Nakonec si shrneme dříve uvedené poznatky formou tabulek.
3. Úvod do programovacího jazyka C++
33
Při inicializaci přiřaď ukazateli hodnotu 0 nebo NULL. Nikdy nesmíš dynamický objekt zrušit více než jednou. Nastav ukazatel na NULL nebo 0, pokud jsi na něm aplikoval operátor delete. Pro přístup k dynamické proměnné musíš použít operátor dereference (*). Pro přístup k položkám dynamické proměnné typu struct, union, class je výhodné použít ukazatelový selektor (–>). FUNKCE A JEJICH PARAMETRY Funkce může mít libovolný počet parametrů, tedy i nulový. Funkce může být přetížena, definována více než jednou. Pak se v místě použití zavolá ta varianta, jejíž prototyp nejvíce odpovídá daným parametrům. Funkce může používat implicitní parametry, které se však musí uvést na konci seznamu parametrů. Pokud bude při volání funkce implicitní parametr vynechán, dosadí se implicitní hodnota. Funkce může mít návratovou hodnotu (vracet výsledek), nebo nemusí (je definována jako void), pak se v podstatě jedná o proceduru. Pokud má funkce měnit hodnoty parametrů, musíme předat adresu proměnné nebo použít referenci. OPERÁTORY JAZYKA C++ V jazyce C++ je možno používat operátory jazyka C. V C++ je možno přetížit standardní operátory, tzn. přisoudit jim více než jeden význam. Insertor << je určen pro usnadnění výstupu. Extraktor >> je určen pro usnadnění vstupu. Ukazatelový selektor –> zjednodušuje přístup k položkám struktury pomocí ukazatele. Operátor příslušnosti :: umožní například zpřístupnění globální proměnné, která je zastíněna lokální deklarací. Jeho použití v C++ je však obecnější. Pro dynamickou správu paměti se používají nové operátory new a delete.
34
Algoritmy a programování v jazyce C/C++
4 Základy OOP v jazyce C++ Objektově orientované programování (OOP) se často definuje, jako metoda tvorby programů, která věrně napodobuje způsob, jakým zacházíme s objekty reálného světa. Jiný pohled zase oceňuje skutečnost, že při OOP jsou spojována data s funkcemi, které nad nimi můžeme vyvolávat. Mějme například matici. Pak je jisté, že budeme chtít používat operace jako součet a součin matic, stanovení determinantu nebo inverzní matice. Určitě bude snazší, když bude zřejmé, které funkce můžeme nad maticí používat. Představa, že musíme studovat manuály funkcí a zjistit, které funkce můžeme použít, je dosti úmorná. Tento způsob může snadno vést k nefunkčnímu programu. Spojení dat s použitelnými funkcemi se nazývá zapouzdření a vede k vyšší bezpečnosti programování. Datům v tomto modelu pak obvykle říkáme atributy (datové položky, vlastnosti), použitelným funkcím metody (členské funkce). Dalším pojmem je instance (objekt). Jedná se o proměnnou objektového typu. Jako základní objektový typ se používá tzv. třída – class. Z hlediska C++ lze však jako objektové typy chápat i strukturu nebo sjednocení (struct, union). Jednou ze základních vlastností tříd je dědičnost. Což je schopnost odvodit ze stávající třídy (báze, předka) novou třídu (potomka, následníka). Nová třída přijme všechny vlastnosti bázové třídy a může je dále rozšířit. V odvozené třídě tedy zůstávají k dispozici metody a atributy bázové třídy, navíc je možno přidávat další. Tímto způsobem lze například definovat základní třídu hierarchie, která poskytuje základní možnosti (například třídu TSeznam, představující spojový dynamický seznam) a odvodit od ní specializované třídy (například třídu TSetridenySeznam, která již prvky zatřiďuje ve vzestupném pořadí). Vzhledem k náročnosti tohoto tématu je dědičnosti věnována kapitola 5. Velmi důležitým pojmem je úroveň přístupu k položkám třídy. Je totiž možno zajistit, že ne všechny atributy nebo metody jsou dostupné zvenčí (takto chráníme instanci třídy před možnými, často asi nechtěnými, chybami ze strany uživatele): • public – veřejná položka, tyto atributy a metody jsou dostupné kdekoli; ne jenom v samotné třídě, ale i zvenku, • private – soukromá položka, tyto atributy a metody jsou dostupné pouze v samotné třídě, nelze k nim přistupovat zvenku; tato úroveň je ve třídě implicitní (pokud nestanovíme jinak, jsou všechny metody a atributy privátní – vně nepřístupné), • protected – chráněná položka, tyto atributy a metody jsou dostupné pouze v samotné třídě, případně ve třídách, které z ní odvodíme; nejsou dostupné zvenku. Platí vlastně jednoduchá (nebo chcete-li logická) pravidla: • atributy jsou nejčastěji ukrývány před vnějším okolím. Proto se obvykle zařazují do úrovně private. Pokud však předpokládáme, že z dané třídy odvodíme následníka, bude vhodné mít daný atribut k dispozici. Pak je „šikovnější“, zařadit jej do úrovně protected. • metody jsou obvykle zařazovány do úrovně public, jinak bychom je nemohli volat. Existují ale jistě metody, které nesmí mít neznalý uživatel k dispozici a v tomto případě můžeme použít úrovně private nebo protected.
4. Základy OOP v jazyce C++
35
4.1
Definice třídy
Definice třídy probíhá prakticky stejně, jako definice struktury nebo sjednocení. Uvažujme třídu, která podobně jako struktura TOsoba z příkladu 2.02.06 (viz kapitolu 2.3.3), udržuje informace o osobě. Nazveme ji TClovek. Bylo by rozumné, aby tato třída kromě atributů Jmeno, Prijmeni a OsCislo disponovala například metodami pro výpis aktuálního stavu nebo metodami pro zadání jména, příjmení a osobního čísla. Jistě metoda pro výpis stavu (nazveme ji Vypis) je v pořádku, proč ale definovat metody pro zadaní hodnot atributů? Zvyklost je taková, že chceme vyloučit nesprávné zadání (například omezit počet znaků v řetězcích nebo nedovolit zadávat záporná osobní čísla), proto umístíme atributy do sekce protected (nebo private, pokud nepředpokládáme dědení). Kontrolu platnosti zadání nám pak zajistí metoda. Ta bude umístěna ve veřejné úrovni a bude tedy moci s atributy pracovat, přestože atributy sami jsou vně nepřístupné. Metody si nazveme NastavJmeno, NastavPrijmeni, NastavOsCislo. Níže je uvedena celá definice třídy TClovek, celý příklad je označen jako PR2.04.01. Délka obou řetězců je zavedena konstantou DelkaRet, v deklaracích atributů se přidává jeden znak na zarážku: #include #include <string.h> const int DelkaRet=20; class TClovek{ protected: char Jmeno[DelkaRet+1]; char Prijmeni[DelkaRet+1]; int OsCislo; public: void NastavJmeno(const char *Jmeno); void NastavPrijmeni(const char *Prijmeni); void NastavOsCislo(int OsCislo); void Vypis(); }; Následují definice jednotlivých metod (obvykle se umisťují mimo definici třídy, viz také kapitolu 4.4). Vzhledem k tomu, že názvy parametrů metod se shodují s názvy atributů třídy, musel být použit operátor příslušnosti (::). Například proměnná Jmeno je chápána jako lokální parametr metody NastavJmeno (překryje atribut třídy), ale spojení TClovek::Jmeno určuje jednoznačně atribut třídy. Pomocí funkce strncpy je zajištěno, že uživatel nemůže přesáhnout délku řetězce pro jméno nebo příjmení. Podobně, pokud je zadáno záporné osobní číslo, je nahrazeno hodnotou 0. To je tedy smysl metod, které nastavují atributy (zajišťují kontrolu mezí nebo rozsahů). Pro výpis stavu instance je použita proudová knihovna, konkrétně proud cout a operátor <<. void TClovek::NastavJmeno(const char *Jmeno) { strncpy(TClovek::Jmeno,Jmeno,DelkaRet); } 36
Algoritmy a programování v jazyce C/C++
void TClovek::NastavPrijmeni(const char *Prijmeni) { strncpy(TClovek::Prijmeni,Prijmeni,DelkaRet); } void TClovek::NastavOsCislo(int OsCislo) { if(OsCislo>0) TClovek::OsCislo=OsCislo; else TClovek::OsCislo=0; } void TClovek::Vypis() { cout<<Jmeno<<" "<
4.2
Konstruktor
Zatím jsme neřešili jeden zásadní problém. A sice: jak zajistit inicializaci instance? V předchozím příkladu bylo nutno vyvolat pomocné metody, které tuto operaci provedli. Při použití technik OOP, lze však daný problém řešit jednodušeji. Každá třída obvykle obsahuje minimálně jednu speciální metodu, která se automaticky provede při vytvoření instance dané třídy. Tato metoda tedy konstruuje danou třídu a tudíž se 4. Základy OOP v jazyce C++
37
nazývá konstruktor. Konstruktor se vyznačuje tím, že nemá žádnou návratovou hodnotu (ani void). Konstruktor má stejný název jako třída samotná, pro náš příklad tedy TClovek. Pro náš příklad bude vhodný konstruktor, který nemá parametry – bezparametrický konstruktor. Ten zajistí nastavení nějakých výchozích hodnot do všech atributů. Například lze nastavit prázdné řetězce do Jmeno a Prijmeni a hodnotu 0 do OsCislo. Postupme ale dál. 4.2.1
Přetížení konstruktoru Kromě bezparametrického konstruktoru by bylo žádoucí, definovat mechanizmus, který umožní vložit hodnoty atributů do instance třídy již v rámci jejího vytvoření. To můžeme zajistit konstruktorem s parametry – parametrickým konstruktorem. Jazyk C++ dovoluje konstruktor (stejně jako ostatní metody) přetěžovat. Platí obecné pravidlo, každý konstruktor se musí lišit počtem nebo typem parametrů (ale už ne návratovou hodnotou, konstruktor nemá návratovou hodnotu). Nyní si tedy můžeme ukázat úpravu třídy TClovek ve formě nového příkladu PR2.04.02. Bezparametrický konstruktor nastaví do nultých položek obou řetězců zarážku, tím je definuje jako prázdné a nastaví OsCislo na hodnotu 0. Také je možno vyvolat „nastavovací“ metody tak, jak to provádí druhý (parametrický) konstruktor. Všimněte si drobné úpravy metody Vypis. Pokud má OsCislo hodnotu 0, chápe se daná instance jako nedefinovaná. #include #include <string.h> const int DelkaRet=20; class TClovek{ protected: char Jmeno[DelkaRet+1]; char Prijmeni[DelkaRet+1]; int OsCislo; public: //konstruktory: TClovek(); TClovek( const char *Jmeno, const char *Prijmeni, int OsCislo); void NastavJmeno(const char *Jmeno); void NastavPrijmeni(const char *Prijmeni); void NastavOsCislo(int OsCislo); void Vypis(); }; //bezparametricky konstruktor: TClovek::TClovek() { Jmeno[0]='\0'; Prijmeni[0]='\0'; OsCislo=0; }
38
Algoritmy a programování v jazyce C/C++
//parametricky konstruktor: TClovek::TClovek( const char *Jmeno, const char *Prijmeni, int OsCislo) { NastavJmeno(Jmeno); NastavPrijmeni(Prijmeni); NastavOsCislo(OsCislo); } void TClovek::NastavJmeno(const char *Jmeno) { strncpy(TClovek::Jmeno,Jmeno,DelkaRet); } void TClovek::NastavPrijmeni(const char *Prijmeni) { strncpy(TClovek::Prijmeni,Prijmeni,DelkaRet); } void TClovek::NastavOsCislo(int OsCislo) { if(OsCislo>0) TClovek::OsCislo=OsCislo; else TClovek::OsCislo=0; } //zmena!!! void TClovek::Vypis() { if(OsCislo==0) cout<<"NEDEFINOVANO"<<endl; else cout<<Jmeno<<" "<
4. Základy OOP v jazyce C++
39
Zápis: TClovek c1,c2("Frantisek","Cempirek",20); způsobil vytvoření a inicializaci dvou instancí. Pro inicializaci c1 je použit bezparametrický konstruktor, pro c2 jsou parametry uvedeny, takže se použije konstruktor parametrický. 4.2.2
Konstruktor s implicitními parametry Je jisté (protože konstruktor je v podstatě obyčejná metoda), že kromě přetěžování můžeme používat i implicitní parametry. Takže výše uvedený příklad se dá upravit tak, že použijeme pouze jeden konstruktor a ten bude mít jako implicitní parametry prázdné řetězce (pro Jmeno a Prijmeni) nebo hodnotu 0 (pro OsCislo). Tuto úpravu si ukážeme dále.
4.3
Destruktor
Destruktor je speciální metoda, která se automaticky vykoná, před zánikem instance dané třídy. Každá třída může mít maximálně jeden destruktor, destruktor tedy není možno přetížit. Destruktor totiž nemá návratovou hodnotu ani parametry. Název destruktoru je opět odvozen od názvu třídy, předchází mu však vlnka (~). Například destruktor z našeho příkladu se musí jmenovat ~TClovek, protože třída se jmenuje TClovek1. Destruktor se obvykle používá pro uvolnění dynamicky alokovaných objektů, které byly vytvořeny v konstruktoru (či později), z paměti. Jak využijeme destruktor v „naší“ třídě TClovek? Uvědomme si zásadní skutečnost. Každá instance této třídy počítá s vymezením celkem 42 znaků na jméno a příjmení. Takže v paměti zabírá celkem 46 bajtů (42 bajtů v řetězcích a 4 bajty v celém čísle OsCislo). Jistě, můžeme délku řetězců zkrátit. Pak se může stát, že instance některá jména či příjmení „nepojme“. Bude lepší alokovat řetězce dynamicky, podle skutečně požadované délky. Takže atributy Jmeno a Prijmeni budou nyní místo řetězců ukazatele. Proto metody NastavJmeno a NastavPrijmeni musí nejdříve zrušit původní řetězec a potom alokovat nový podle žádané délky (nakonec samozřejmě překopírují znaky ze vstupního parametru). Kdo ale tyto dynamicky alokované řetězce uvolní, až bude rušena instance? Normálně k tomu musíme opět použít metodu (například Uvolni), elegantnější je ale definovat destruktor (při rušení instance se vyvolá automaticky). Upozorněme ještě na jeden aspekt realizace. Při prvním volání metod NastavJmeno nebo NastavPrijmeni musí být správně inicializovány atributy Jmeno nebo Prijmeni. Atributy však můžeme inicializovat v hlavičce konstruktoru (za dvojtečkou v hlavičce konstruktoru): TClovek::TClovek( const char *Jmeno, const char *Prijmeni, int OsCislo) :Jmeno(NULL),Prijmeni(NULL) Tato konstrukce zajistí inicializaci obou ukazatelů ještě před vstupem do těla konstruktoru, ve kterém jsou volány problematické metody NastavJmeno nebo NastavPrijmeni.
1
Operátor ~ je použit záměrně, protože normálně znamená negaci. Destrukce je opakem konstrukce.
40
Algoritmy a programování v jazyce C/C++
PR2.04.03: #include #include <string.h> class TClovek{ protected: char *Jmeno; char *Prijmeni; int OsCislo; public: //konstruktor: TClovek( const char *Jmeno="", const char *Prijmeni="", int OsCislo=0); //destruktor: ~TClovek(); void NastavJmeno(const char *Jmeno); void NastavPrijmeni(const char *Prijmeni); void NastavOsCislo(int OsCislo); void Vypis(); }; //vyznamne zmeny: TClovek::TClovek( const char *Jmeno, const char *Prijmeni, int OsCislo) :Jmeno(NULL),Prijmeni(NULL) { NastavJmeno(Jmeno); NastavPrijmeni(Prijmeni); NastavOsCislo(OsCislo); } //destruktor: TClovek::~TClovek() { if(Jmeno!=NULL) delete[] Jmeno; if(Prijmeni!=NULL) delete[] Prijmeni; } //vyznamne zmeny: void TClovek::NastavJmeno(const char *Jmeno) { //zjisti delku retezce: int delka=strlen(Jmeno); //uvolni dynamicky retezec, pokud existoval: if(TClovek::Jmeno!=NULL) delete[] TClovek::Jmeno;
4. Základy OOP v jazyce C++
41
//alokuj retezec podle zjistene delky: TClovek::Jmeno=new char[delka+1]; //kopie retezcu: strcpy(TClovek::Jmeno,Jmeno); } //vyznamne zmeny: void TClovek::NastavPrijmeni(const char *Prijmeni) { int delka=strlen(Prijmeni); if(TClovek::Prijmeni!=NULL) delete[] TClovek::Prijmeni; TClovek::Prijmeni=new char[delka+1]; strcpy(TClovek::Prijmeni,Prijmeni); } void TClovek::NastavOsCislo(int OsCislo) { if(OsCislo>0) TClovek::OsCislo=OsCislo; else TClovek::OsCislo=0; } void TClovek::Vypis() { if(OsCislo==0) cout<<"NEDEFINOVANO"<<endl; else cout<<Jmeno<<" "<
4.4
Vložené (inline) metody
Metody třídy mohou být deklarovány pomocí klíčového slova inline. Pak to znamená, že při překladu je na místo, kde je funkce volána, vloženo přímo tělo funkce a ne instrukce CALL na danou funkci. Vykonání funkce pak bude rychlejší, neboť není třeba provést časově náročný adresovací výpočet pro volání podprogramu.
42
Algoritmy a programování v jazyce C/C++
Dále musíme upozornit na skutečnost, že klíčové slovo inline představuje pouze jakési doporučení pro překladač. Překladač se pokusí dosadit tělo funkce do místa jejího volání. Pokud to však nebude možné, neprovede to. Překladač pak zpravidla podá varovné hlášení a funkci bude volat běžným způsobem. Definice vložené metody může proběhnout dvěma způsoby: • pouze zápisem klíčového slova inline před příslušnou metodu při deklaraci třídy, • přímým zápisem těla dané metody v definici třídy (tento postup vlastně představuje úspornější formu zápisu metody), viz níže: class TClovek{ . . void Vypis() { if(OsCislo==0) cout<<"NEDEFINOVANO"<<endl; else cout<<Jmeno<<" "<
4.5
Ukazatel this
Krátce řečeno: this představuje ukazatel na danou instanci třídy. Tento ukazatel se vytváří automaticky v každé třídě, aniž bychom jeho vytvoření iniciovali deklarací. Navíc se automaticky doplňuje při přístupu k libovolné položce třídy. Proto není nutné uvádět vždy název třídy následovaný operátorem příslušnosti a názvem položky. Postačí jméno položky. Na tomto místě není bohužel možno předvést smysluplné použití tohoto ukazatele, vrátíme se k němu později.
4.6
Dynamická alokace instancí
Zatím jsme instance tříd vytvářeli staticky. V mnoha případech je však při použití OOP zvykem vytvářet instance tříd dynamicky. Pochopitelně, dynamická alokace probíhá operátorem new. Otázkou zůstává, jak zapsat případné parametry konstruktoru. Níže je uvedeno srovnání zápisů při použití statické a dynamické instance třídy TClovek. Pro statickou instanci je zaveden identifikátor c, pro ukazatel na dynamickou instanci identifikátor p_c. //statická instance: //alokace+inicializace: TClovek c("Jan","Lichy",10);
//volání metody: c.Vypis(); //uvolnění instance: //samovolně
4.7
//dyn. vytvořená instance: //nejdříve ukazatel: TClovek *p_c; //pak alokace+inicializace: p_c=new TClovek("Tomas","Sudy",20); //volání metody: p_c->Vypis(); //uvolnění instance: delete p_c;
Závěrečné shrnutí Nakonec si shrneme dříve uvedené poznatky formou tabulek.
4. Základy OOP v jazyce C++
43
TŘÍDA: ZÁKLADNÍ POJMY Zapouzdření je spojení datových objektů s funkcemi, které na ně lze aplikovat. Atributy (datové položky) třídy mají stejný význam jako položky struktury. Metody (členské funkce) jsou funkce zapouzdřené spolu s datovými položkami do dané třídy. Instance (objekt) je proměnná typu třída. METODY Metoda je funkce, která operuje nad atributy instance. Konstruktor je speciální metoda, která slouží pro vytvoření instance dané třídy. Konstruktor se vyznačuje tím, že nemá žádnou návratovou hodnotu. Konstruktor slouží pro inicializaci instance. Destruktor je speciální metoda, která se automaticky vykoná před zánikem instance dané třídy. Každá třída může mít maximálně jeden destruktor, destruktor není možno přetížit. Destruktor nemá návratovou hodnotu ani parametry. Destruktor se obvykle používá pro uvolnění dynamicky alokovaných proměnných, které byly v instanci vytvořeny. Inline metoda je vykonána rychleji, než běžné metody, neboť není třeba provést časově náročný adresovací výpočet pro volání podprogramu. Tato technika se hodí pouze pro ty funkce, které mají být volány často a jsou krátké.
44
Algoritmy a programování v jazyce C/C++
5 Dědičnost, polymorfismus, virtuální metody a abstraktní třídy Dědičností rozumíme schopnost odvodit z existující třídy novou třídu, která rozšíří její schopnosti. Původní třída (z níž se dědí) se označuje jako bázová třída nebo jako předek (předchůdce). Nová třída (zdědila vlastnosti původní třídy) se nazývá odvozená třída nebo potomek (následník).
5.1
Jednoduchá dědičnost
V kapitole 4 byla zavedena třída TClovek. Vytvořme z ní odvozenou třídu TStudent, která rozšiřuje její schopnosti. U každého studenta máme uveden studijní obor (řetězec – bude alokován dynamicky) a ročník (celé číslo). Nejdříve si tedy uvedeme deklaraci nové třídy: class TStudent:public TClovek{ protected: char *Obor; int Rocnik; public: TStudent( const char *Jmeno="", const char *Prijmeni="", int OsCislo=0, const char *Obor="", int Rocnik=0); ~TStudent(); void NastavObor(const char *Obor); void NastavRocnik(int Rocnik); void Vypis(); }; Vidíme, že odvození třídy TStudent ze třídy TClovek je uvedeno v hlavičce definice nové třídy. Kromě odvození public jsou k dispozici další způsoby (viz kapitolu 5.1.1) Vraťme se ale k naší třídě TStudent. Pro zajištění potřebné režie byl definován konstruktor a destruktor (minimálně je třeba vytvořit a uvolnit dynamickou paměť pro řetězec Obor) a dále metody pro nastavení oboru a ročníku a výpisu studenta. Podívejme se na jejich realizaci: TStudent::TStudent( const char *Jmeno, const char *Prijmeni, int OsCislo, const char *Obor, int Rocnik) :TClovek(Jmeno,Prijmeni,OsCislo), Obor(NULL) { NastavObor(Obor); NastavRocnik(Rocnik); }
5. Dědičnost, polymorfismus, virtuální metody a abstraktní třídy
45
TStudent::~TStudent() { if(Obor!=NULL) delete[] Obor; } void TStudent::NastavObor(const char *Obor) { int delka=strlen(Obor); if(TStudent::Obor!=NULL) delete[] TStudent::Obor; TStudent::Obor=new char[delka+1]; strcpy(TStudent::Obor,Obor); } void TStudent::NastavRocnik(int Rocnik) { if(Rocnik>0) TStudent::Rocnik=Rocnik; } void TStudent::Vypis() { //volání metody předka: TClovek::Vypis(); cout<<"Obor: "<
46
Algoritmy a programování v jazyce C/C++
5.1.1
Odvození přístupových úrovní Kromě varianty public, jsou možné další dva způsoby odvození (private a protected). Pokud používáme odvození public, nedochází ke změně přístupových úrovní položek v odvozené třídě. Například metoda TClovek::NastavOsCislo zůstane v následníku TStudent nadále veřejně přístupnou. Pokud místo public napíšeme například private, přejdou všechny položky báze v následnické třídě do úrovně private, budou tedy vně nedostupné (například metodu NastavOsCislo pak nebude možno vyvolávat z vnějšku). Tab. 5.1 Odvození přístupových úrovní při dědění
Přístupová úroveň položky bázové třídy public private protected
Odvození public public nedostupný protected
Odvození private private nedostupný private
Odvození protected protected nedostupný protected
5.1.2
Konstruktor, destruktor a dědičnost Pokud má odvozená třída konstruktor (a je rozumné, aby jej měla – viz kapitolu 5.2.3) vyvolá se nejdříve kód konstruktoru bázové třídy a teprve potom kód konstruktoru odvozené třídy. V našem příkladu je tedy nejdříve volán konstruktor třídy TClovek a až potom konstruktor třídy TStudent. Tento způsob má dosti zásadní význam: nejdříve je třeba vybudovat bázi a teprve potom připojit nové atributy, které zavádí odvozená třída. Pokud má odvozená třída destruktor, vyvolá se nejdříve kód destruktoru této třídy a potom destruktor třídy bázové (pokud báze měla destruktor). Čili nejdříve se ruší atributy zavedené v nové třídě (TStudent ) a teprve potom atributy bázové třídy (TClovek). Pořadí je opět zvoleno logicky: atributy báze se ruší až nakonec, protože mohou být důležité ještě v okamžiku, kdy rušíme atributy odvozené třídy.
5.2
Polymorfismus a virtuální metody
Jednou ze základních vlastností polymorfismu je skutečnost, že do ukazatele bázové třídy můžeme vložit nejen adresu instance této třídy, tedy (vzhledem k našemu příkladu): TClovek *p=new TClovek; ale i adresu třídy odvozené: p=new Student; Tato formulace se někdy zkráceně zavádí jak pravidlo o zastoupení předka potomkem. Otázkou je, jaké výhody nám tento rys přinese. Můžeme však vymyslet celou řadu použití. Například lze vytvořit pole nebo spojový seznam, který obsahuje ukazatele typu TClovek*. Do těchto položek pak můžeme ukládat nejen adresy instancí třídy TClovek, ale i odvozených tříd. Například instance třídy TStudent. Vytvoříme pak mnohotvaré (polymorfní) pole či spojový seznam. Abychom objasnili pojem virtuální metoda, ukažme si výše naznačené polymorfní pole na příkladu. Příklad 2.05.02: Vytvořme polymorfní pole, které umožňuje ukládat instance tříd TClovek nebo TStudent. Vytvořme dvě instance (každá jedné třídy) a použijme metodu Vypis.
5. Dědičnost, polymorfismus, virtuální metody a abstraktní třídy
47
Vypsán pouze hlavní program, zbytek stejný jako v příkladu PR2.05.01: int main() { //polymorfni pole: TClovek* pole[10]; //vytvoreni a inicializace instanci: pole[0]=new TClovek("Ondrej","Maly",1027); pole[1]=new TStudent("Jan","Cempirek",1020,"PS",2); //vypis stavu: pole[0]->Vypis(); pole[1]->Vypis(); //uvolneni instanci: delete pole[0]; delete pole[1]; return 0; } Jistě očekáváte výpis první instance typu TClovek (3 údaje) a druhé instance typu TStudent (5 údajů). Místo toho je i u instance typu TStudent vypsáno pouze jméno, příjmení a osobní číslo. Ostatní údaje se nezobrazí. Pro instanci, jejíž ukazatel je uložen v pole[1], byla totiž volána původní metoda TClovek::Vypis: Ondrej Maly 1027 Jan Cempirek 1020 Problém nám pomohou vyřešit virtuální metody. 5.2.1
Základní pohled na virtuální metody Řešme problém chybného výpisu instancí v příkladu PR2.05.02 tak, že metodu Vypis označíme jako virtuální. Zrovna tak bude třeba odstranit logickou chybu z předchozího příkladu. Touto chybou je skutečnost, že destruktor není virtuální. Proto se při rušení instance třídy TStudent vyvolá přes ukazatel typu TClovek* pouze destruktor báze. Je to vážná chyba, nedojde k uvolnění dynamicky alokovaného řetězce z atributu Obor. Metodu definujeme jako virtuální použitím klíčového slova virtual. Označení metody jako virtuální se provádí při definici bázové třídy. Při zápisu metody se již klíčové slovo virtual neopakuje. Dobrým zvykem je zopakovat klíčové slovo virtual i při definici odvozené třídy, aby se zdůraznilo, že daná metoda je virtuální. Příklad 2.05.03: Upravme příklad PR2.05.02 tak, aby byl destruktor i metoda Vypis virtuální. Tím odstraníme chyby z předchozího řešení. Nyní bude polymorfismus fungovat korektně. #include <string.h> #include class TClovek{ protected: char *Jmeno;
48
Algoritmy a programování v jazyce C/C++
char *Prijmeni; int OsCislo; public: //konstruktor: TClovek( const char *Jmeno="", const char *Prijmeni="", int OsCislo=0); //destruktor: virtual ~TClovek(); void NastavJmeno(const char *Jmeno); void NastavPrijmeni(const char *Prijmeni); void NastavOsCislo(int OsCislo); virtual void Vypis(); }; class TStudent:public TClovek{ protected: char *Obor; int Rocnik; public: TStudent( const char *Jmeno="", const char *Prijmeni="", int OsCislo=0, const char *Obor="", int Rocnik=0); virtual ~TStudent(); //pouze zopakovano void NastavObor(const char *Obor); void NastavRocnik(int Rocnik); virtual void Vypis();//pouze zopakovano }; //zbytek stejný… Nyní již bude výpis i destrukce instancí fungovat správně: Ondrej Maly 1027 Jan Cempirek 1020 Obor: PS, rocnik: 2 5.2.2
Podrobnější výklad virtuálních metod U statických metod je rozhodování překladače, jestli vyvolá metodu bázové třídy nebo odvozené třídy, jednoduché. Vyvolá vždy metodu bázové třídy. Vyvolání překladač zajistí přímým adresováním metody, adresu totiž zná. V terminologii objektově orientovaného programování se tento mechanismus nazývá časná (statická) vazba (early binding). Nevýhody použití statické vazby byly ukázány v příkladu PR2.05.02. Označíme-li v deklaraci bázové třídy příslušnou metodu jako virtuální, je to pokyn pro překladač, aby přeložil každé volání metody zajištěním výpočtu konkrétní adresy za běhu programu a vyvoláním podprogramu nacházejícího se na takto vypočítané adrese. Tento způsob se v terminologii OOP označuje jako pozdní (dynamická) vazba (late binding). 5.2.3
Implementace pozdní vazby Pokud má bázová třída (TClovek) zaveden konstruktor a alespoň jednu virtuální metodu, připojí překladač k datovým položkám třídy tabulku adres virtuálních metod (zkráceně se označuje jako virtuální tabulka).
5. Dědičnost, polymorfismus, virtuální metody a abstraktní třídy
49
Při vytvoření odvozené třídy (TStudent) zdědí tato třída virtuální tabulku od bázové třídy (TClovek) a může jí měnit (proto je nutné, aby následník definoval vlastní konstruktor): • překrytím zděděných virtuálních metod (změní se adresa v příslušném řádku tabulky), • přidáním dalších virtuálních metod (do dalších řádků tabulky). 5.2.4
RTTI – dynamická identifikace typů Celkem logickou otázkou je, jak při polymorfismu rozeznat skutečný typ instance, jejíž adresa je vložena do ukazatele bázové třídy. Dynamická identifikace typů (RTTI1) umožňuje určit typ instance polymorfního typu za běhu programu. Nejdůležitějším prostředkem RTTI je operátor typeid operátory == a !=. Operátor typeid Syntaxe: typeid(výraz)
nebo
typeid(jméno_typu)
První formu použijeme v případě, že máme k dispozici dereferencovaný ukazatel nebo proměnnou objektového typu a chceme zjistit informace o daném typu. Druhou formu můžeme použít pro porovnávání. Operátor typeid vrací konstantní instanci třídy type_info (viz níže), která obsahuje řetězec jména typu a definuje význam operátorů == a !=. Třída typeinfo Třída type_info podává informaci o třídě. Třída bývá definována v hlavičkovém souboru typeinfo.h. Tab. 5.2 Položky třídy type_info
Metoda/operátor const char* name( ) const; bool operator==( const typeinfo& ti) const; bool operator!=( const typeinfo& ti) const;
Význam Řetězec názvu třídy. true (1) pokud jsou oba výrazy stejného typu, jinak false (0). true (1) pokud jsou oba výrazy různého typu, jinak false (0).
Příklad použití bude uveden později. 5.2.5
Operátor pro bezpečnější přetypování V předchozím textu jsme diskutovali pojem RTTI. Pomocí této techniky bychom mohli zjistit, zda je ukazatel správného typu a potom ukazatel přetypovat. Přetypovat potřebujeme pro vyvolání dalších metod, které jsou dostupné v následnické třídě, ale nebyly definovány ve třídě bázové. Tento postup je jistě správný, ale zdlouhavý. Proto je v C++ zaveden operátor dynamic_cast. Operátor dynamic_cast se používá především pro polymorfní třídy. Slouží k bezpečnému přetypování z předka na potomka (nebo opačně), k tomu používá RTTI. Syntaxe: dynamic_cast (V) kde T je typ, na který se má přetypovat výraz V. Přetypovat lze ukazatel na ukazatel nebo referenci na referenci. 1
RTTI (Run Time Type Identification)
50
Algoritmy a programování v jazyce C/C++
Pokud je přetypování neúspěšné, vrací NULL (pro případ ukazatelů) nebo vyvolá výjimku Bad_cast (pro případ reference). Při úspěšném přetypování vrátí výraz V přetypovaný na typ T. Příklad použití bude uveden později. 5.2.6
Abstraktní třída S polymorfismem velmi úzce souvisí další z pojmů objektově orientovaného programování, který nyní probereme. Jedná se o pojem abstraktní třídy. V některých případech potřebujeme definovat bázovou třídu jen proto, abychom její typ mohli použít pro deklaraci ukazatele. Do tohoto ukazatele pak můžeme přiřadit adresu instance libovolného z následníků. Metody bázové třídy, jejichž význam má být definován až v následnících, se označují jako čisté virtuální metody. Třída, která definuje alespoň jednu čistou virtuální metodu, nemůže vytvořit svoji instanci a je proto nazývána abstraktní třídou. Tělo čisté virtuální metody není třeba zapisovat, její definice patří až následníku (toto pravidlo se netýká destruktoru). Pokud je daná bázová třída abstraktní, nelze ji použít přímo. Lze definovat následníky, kteří musí překrýt význam všech čistých virtuálních metod. Pro definici čisté virtuální metody je vyhrazena tato syntaxe: virtual typ_návratové_hodnoty jméno_metody(parametry)=0; Příklad použití bude uveden později. 5.2.7
Polymorfismus a parametry volané odkazem Poslední „perličkou“ je skutečnost, že kromě pravidla o zastoupení předka potomkem při použití ukazatelů (do ukazatele bázové třídy lze vložit i adresu instance třídy odvozené), lze tuto techniku používat i pro parametry volané odkazem. Pokud tedy bude definována funkce, která má parametr volaný odkazem typu bázové třídy (TClovek&), lze do daného parametru předat i instanci třídy odvozené (TStudent). Tedy pravidlo o zastoupení předka potomkem platí i pro parametry volané odkazem.
Příklad 2.05.04: V tomto příkladu si předvedeme všechny zatím pouze teoreticky prezentované vlastnosti OOP spojené s polymorfismem a virtuálními metodami: • změníme hierarchii tříd s použitím abstraktní třídy TObjekt (z abstraktní báze TObjekt je odvozena třída TClovek a z ní pak třída TStudent), • v bázi TObjekt jsou destruktor a metoda Vypis definovány jako čisté virtuální metody, • přetížíme insertor (<<) v bázi TObjekt a tím zajistíme výpis instance (výpis je proveden metodou Vypis), na této technice se dokumentuje možnost zastoupení předka potomkem pro parametry volané odkazem (do insertoru se vypisovananá instance předává odkazem), • ukážeme si dynamickou identifikaci typu za běhu programu, tedy RTTI (pro zjištění možností poskytovaných instancí), • přetypujeme ukazatel z TClovek* na TStudent* operátorem dynamic_cast (pro možnost vyvolat metody NastavObor nebo NastavRocnik). #include #include <string.h> #include
5. Dědičnost, polymorfismus, virtuální metody a abstraktní třídy
51
class TObjekt{ public: TObjekt(); virtual ~TObjekt()=0; virtual void Vypis(ostream& s)=0; }; class TClovek:public TObjekt{ protected: char *Jmeno; char *Prijmeni; int OsCislo; public: TClovek( const char *Jmeno="", const char *Prijmeni="", int OsCislo=0); virtual ~TClovek(); void NastavJmeno(const char *Jmeno); void NastavPrijmeni(const char *Prijmeni); void NastavOsCislo(int OsCislo); virtual void Vypis(ostream& s); }; class TStudent:public TClovek{ protected: char *Obor; int Rocnik; public: TStudent( const char *Jmeno="", const char *Prijmeni="", int OsCislo=0, const char *Obor="", int Rocnik=0); virtual ~TStudent(); void NastavObor(const char *Obor); void NastavRocnik(int Rocnik); virtual void Vypis(ostream& s); }; TObjekt::TObjekt() { } TObjekt::~TObjekt() { } ostream& operator<<(ostream& s,TObjekt& o) { o.Vypis(s); return s; }
52
Algoritmy a programování v jazyce C/C++
TClovek::TClovek( const char *Jmeno, const char *Prijmeni, int OsCislo) :TObjekt(),Jmeno(NULL),Prijmeni(NULL) { NastavJmeno(Jmeno); NastavPrijmeni(Prijmeni); NastavOsCislo(OsCislo); } TClovek::~TClovek() { if(Jmeno!=NULL) delete[] Jmeno; if(Prijmeni!=NULL) delete[] Prijmeni; } void TClovek::NastavJmeno(const char *Jmeno) { //zjisti delku retezce: int delka=strlen(Jmeno); //uvolni dynamicky retezec, pokud existoval: if(TClovek::Jmeno!=NULL) delete[] TClovek::Jmeno; //alokuj retezec podle zjistene delky: TClovek::Jmeno=new char[delka+1]; //kopie retezcu: strcpy(TClovek::Jmeno,Jmeno); } void TClovek::NastavPrijmeni(const char *Prijmeni) { int delka=strlen(Prijmeni); if(TClovek::Prijmeni!=NULL) delete[] TClovek::Prijmeni; TClovek::Prijmeni=new char[delka+1]; strcpy(TClovek::Prijmeni,Prijmeni); } void TClovek::NastavOsCislo(int OsCislo) { if(OsCislo>0) TClovek::OsCislo=OsCislo; else TClovek::OsCislo=0; } void TClovek::Vypis(ostream& s) { if(OsCislo==0) s<<"NEDEFINOVANO"<<endl;
5. Dědičnost, polymorfismus, virtuální metody a abstraktní třídy
53
else s<<Jmeno<<" "<0) TStudent::Rocnik=Rocnik; } void TStudent::Vypis(ostream& s) { TClovek::Vypis(s); s<<"Obor: "<
54
Algoritmy a programování v jazyce C/C++
//volani metod odvozene tridy: //nelze: student=pole[1]; student=dynamic_cast(pole[1]); if(student!=NULL) student->NastavRocnik(3); //vypis stavu: cout<<*pole[0]<<endl; cout<<*pole[1]; //pouziti RTTI: cout<<"*pole[0] je typu: "<
5. Dědičnost, polymorfismus, virtuální metody a abstraktní třídy
55
6. Také nelze uložit do ukazatele typu TStudent* ukazatel typu TObjekt* (pravidlo o zastoupení předka potomkem je jednostranné; nelze říci, že předek může zastoupit potomka): //nelze: student=pole[1]; 7. Pokud chceme přetypovat ukazatel z TObjekt* na TStudent* musíme použít operátor dynamic_cast, úspěšnost přetypování je testována nenulovostí vraceného ukazatele: student=dynamic_cast(pole[1]); if(student!=NULL) student->NastavRocnik(3); 8. Nyní lze s úspěchem používat insertor pro výpis instancí typu TClovek a TStudent, položku získanou z pole je třeba nejdříve dereferencovat: cout<<*pole[0]<<endl; 9. Je ukázáno použití operátoru typeid pro zjištění typu instance, dále je použita metoda type_info::name, která vrací řetězec názvu třídy: cout<<"*pole[0] je typu: "<
5.3
Vícenásobná dědičnost
V C++ lze zavést i vícenásobnou dědičnost. To znamená, že třída může mít dva a více předků. Například lze zavést novou třídu TZamestnanec, která je odvozena z báze TClovek (přidáme informace o pracovním zařazení a platu). Potom vytvoříme třídu TStudZamestnanec – popisuje zaměstanance, který dálkově studuje. Pro vytvoření takové třídy se nám jistě budou hodit dříve vytvořené třídy TStudent a TZamestnanec. V hlavičce definice nové třídy zapíšeme: class TStudZamestnanec: public TStudent, public TZamestnanec… 5.3.1
Problémy při odvození z více bází Přestože vše vypadá elegantně a logicky, dopustili jsme se jedné vážné chyby. Třída TStudZamestnanec zdědí atributy báze TClovek dvakrát. V jedné linii to bude po třídě TStudent, ve druhé pak po třídě TZamestnanec. Obě třídy totiž měli společného předka, třídu TClovek. Jednak jsou zbytečně dvakrát vytvořeny dynamické řetězce a atribut OsCislo, ale to není nejvážnější problém. Dostáváme se totiž do „dvojznačných“ konstrukcí. Například při přístupu k atributu OsCislo si musíme vybrat, zda budeme pracovat s atributem ze třídy TStudent a nebo se stejně pojmenovaným atributem ze třídy TZamestnanec.
56
Algoritmy a programování v jazyce C/C++
5.3.2
Řešením je virtuální bázová třída Pokud možno se odvozování třídy ze dvou jiných, které mají stejného předka, vyhýbáme. Pokud tomu nelze zabránit, musí být při vytváření tříd TStudent a TZamestnanec použita třída TClovek jako virtuální báze: class TStudent: virtual public TClovek… class TZamestatnec: virtual public TClovek… Vícenásobnou dědičnost nebudeme dále komentovat, nepatří mezi běžně používané programátorské techniky.
5.4
Závěrečné shrnutí Na závěr si připojíme shrnující tabulku.
DĚDIČNOST, POLYMORFISMUS, VIRTUÁLNÍ METODY A ABSTRAKTNÍ TŘÍDY Dědičnost je schopnost odvodit z bázové třídy následníka, který rozšíří její schopnosti. Konstruktor předchůdce je vždy volán před kostruktorem následníka. Destruktor předchůdce je vždy volán po destruktoru následníka. Polymorfismus umožňuje zastoupit předka potomkem. Takže do ukazatele bázové třídy lze vložit i adresu instance třídy odvozené. Pro parametr volaný odkazem je možno nahradit instanci bázové třídy instancí jejího následníka. Virtuální metody umožňují v následnické třídě změnit chování metod předka. Při změně chování metody říkáme, že jsme původní metodu překryli. Tabulka adres virtuálních metod zajišťuje správnou funkci virtuálních metod. Tabulka se vytváří v konstruktoru. Takže třída nemající konstruktor nemůže bez problémů zajistit polymorfismus ani pracovat s virtuálními metodami. RTTI dovoluje zjistit typ instance za běhu programu a to i v případě, že adresa instance je vložena do ukazatele bázové třídy. V souvislosti s RTTI se používá operátor dynamic_cast pro bezpečné přetypování polymorfních typů. Abstraktní třída je použitelná jako obecná báze. Tím, že má definovány některé metody jako čisté virtuální, není možno vytvořit její instanci. Odvozené třídy musí dané čisté virtuální metody překrýt a tím vlastně definují jejich význam. Tímto způsobem se „vnucuje“ určitý uživatelský standard pro použití odvozených tříd.
5. Dědičnost, polymorfismus, virtuální metody a abstraktní třídy
57
6 Další rysy OOP v programovacím jazyce C++ V této kapitole si probereme několik zajímavostí spojených s programování v C++.
6.1
Výjimky
Výjimky představují elegantní prostředek reakce programu na chyby. Zajišťují oddělení samotného programu od bloku obsluhy chyb. V C++ je mechanismus ošetření výjimek založen na těchto klíčových slovech: • try vymezuje počátek chráněného bloku (try – „zkus“), • catch vymezuje počátek bloku obsluhy výjimek (catch – „chyť“), • throw vyvolá specifikovanou výjimku (throw – „hoď“). Než začneme popisovat použití těchto tří klíčových slov, musíme říci, že: výjimka je třída. Čili pro ni platí pravidla dědičnosti (polymorfismus, hlavně že následník může zastoupit předka). Podle zvyklostí normy je bázová třída pojmenována jako Exception a další třídy začínají písmenem E. 6.1.1
catch catch definuje blok obsluhy výjimek. Parametr musí být typu ukazatel nebo reference dané výjimky. Platí, bázová výjimka zachytává i z ní odozené výjimky. Obecná syntaxe catch je: catch1(<deklarace_výjimky>) <složený příkaz> nebo: catch2(...) <složený příkaz> 6.1.2
try try definuje chráněný blok. Obecná syntaxe try je: try <složený_příkaz> <seznam_zachytávacích_bloků>
Mějme výjimky EConvertError a EDivByZero, které jsou odvozeny ze společné báze Exception. Sledujme následující příklady: 1. Příklad (blok ošetřující výjimky odvozené z Exception): try{ . . } catch(Exception& e){ . . }
1
Zachytává pouze výjimku specifikovaného typu nebo následnických tříd.
2
Zachytává všechny výjimky, bez ohledu na typ.
58
Algoritmy a programování v jazyce C/C++
2. Příklad (2 bloky ošetřující výjimky EDivByZero a EConvertError): try{ . . } catch(EDivByZero& e){ . . } catch(EConvertError* e){ . . } 3. Příklad (3 bloky ošetřující výjimky EDivByZero a EConvertError a výjimky odvozené z Exception): try{ . . } catch(EDivByZero& e){ . . } catch(EConvertError* e){ . . } catch(Exception* e){ . . } Poznámka: Pamatujte si, že zachytávací blok, který deklaruje výjimku podle bázového typu1, musí být umístěn vždy jako poslední. Pokud by byla bázová výjimka umístěna jako první, zachytávaly by se výjimky následnických tříd vždy v tomto prvním bloku. Řízení by nebylo nikdy předáno do bloků s odvozenými výjimkami. 6.1.3
throw throw vyvolá výjimku. Obecná syntaxe throw je: throw
Příklady použití budou uvedeny v dalším textu.
6.2
Konstantní členy
Třídy v C++ mohou používat konstantní atributy a metody. Podívejme se, co tento rys přináší.
1
Exception je bázový typ pro EConvertError a EDivByZero.
6. Další rysy OOP v programovacím jazyce C++
59
6.2.1
Konstantní atributy Jako atributy třídy mohou vystupovat i konstanty. Konstantní atributy se chovají tak, že je lze nastavit pouze jednou. Výhoda použití by měla být zřejmá. Konstantní atribut totiž můžeme zařadit do sekce public bez obav, že by jej neopatrný uživatel třídy mohl modifikovat. Nicméně ani sama instance nemůže do daného atributu zapsat více než jednou. Konstantní atribut deklarujeme stejně jako prostou konstantu (typu předchází klíčové slovo const). Inicializace konstantního atributu může proběhnout pouze v hlavičce konstruktoru. 6.2.2
Konstantní metody Konstantní metody se vyznačují tím, že nemohou modifikovat obsah instance (překladač to kontroluje). Konstantní metoda má hlavičku zakončenou klíčovým slovem const. Příklad 2.06.01: Definujme třídu TIntArray, která usnadňuje práci s polem celých čísel. Usnadněním máme na mysli skutečnost, že jsou zejména testovány meze pole a dále, že může být stanoveno libovolné indexování. Pro podporu obecného indexování jsou zavedeny konstantní atributy MinIndex a MaxIndex a dále metody GetItem a SetItem (nahrazují indexovací operátor, lepší verze příkladu by mohla indexovací operátor rovnou přetížit). Do toho příkladu je zahrnuto i použití výjimek. Takže konstruktor nejdříve zkontroluje meze pole (dolní mez nesmí být vyšší než horní mez) a alokuje prvky dynamicky (počet prvků je MaxIndex–MinIndex+1). Metody SetItem a GetItem potom kontrolují rozsahy indexů a případně vyvolávají výjimku. Dále musí být index přepočítán. Přepočet je pro indexování od 0 jednoduchý, odečte se dolní mez. Metoda GetItem je definována jako konstantní. Jejím úkolem je pouze zjistit hodnotu prvku, obsah instance se v jejím důsledku nemění. V tomto pojetí může být konstantní metodou každá, která nemění obsah atributů (takže i metoda SetItem přestože může měnit obsah pole, nesmí však přiřadit do ukazetele p). #include #include <string.h> //vlastni vyjimka: class Exception{ public: Exception(char* s){ strcpy(Msg,s); } char Msg[256]; }; //pole celych cisel: class TIntArray{ protected: int* p; public: //konstantni atributy: const int MinIndex,MaxIndex;
60
Algoritmy a programování v jazyce C/C++
IntArray(int MinIndex, int MaxIndex); virtual ~IntArray(); //konstantni metoda: virtual int GetItem(int Index) const; virtual void SetItem(int Index, int Data); }; //konstruktor, inicializace konst. atributu: TIntArray::TIntArray(int MinIndex, int MaxIndex) :MinIndex(MinIndex),MaxIndex(MaxIndex) { //test mezi: if(MinIndex>MaxIndex) throw Exception("Spatne definovane meze"); //dynamicka alokace pole: p=new int[MaxIndex-MinIndex+1]; } //destruktor-rusi pole: TIntArray::~TIntArray() { delete[] p; } //cte prvek: int TIntArray::GetItem(int Index) const { if((Index>=MinIndex)&&(Index<=MaxIndex)) return p[Index-MinIndex]; else throw Exception("Index mimo rozsah"); } //zapisuje prvek: void TIntArray::SetItem(int Index, int Data) { if((Index>=MinIndex)&&(Index<=MaxIndex)) p[Index-MinIndex]=Data; else throw Exception("Index mimo rozsah"); } //testovaci program: int main() { try{ int i; TIntArray a(-5,10); cout<<"Rozsah indexu: " <
6. Další rysy OOP v programovacím jazyce C++
61
//vlozeni nejakych prvku: for(i=a.MinIndex;i<=a.MaxIndex;i++) a.SetItem(i,i*10); //vypis prvku: for(i=a.MinIndex;i<=a.MaxIndex;i++) cout<<"a["<
6.3
Statické členy
Kromě konstantních členů lze definovat i členy statické. Takové atributy a metody patří spíše třídě samotné než konkrétní instanci. Podívejme se, jaké nám tato skutečnost přináší možnosti. 6.3.1
Statické atributy Statické atributy náleží přímo třídě a ne konkrétní instanci. To znamená, že přes statické atributy mohou jednotlivé instance dané třídy vzájemně komunikovat. Statický atribut je pro danou třídu vytvořen pouze jednou. Pokud bude statický atribut zařazen do sekce private, budou jej moci používat jen instance dané třídy. Pokud bude v sekci public, bude k němu možno přistoupit kdekoliv. Deklarace konstantního atributu je dvoufázová. Poprvé se provede deklarace v samotné deklaraci třídy, druhou fází (inicializací) je přiřazení počáteční hodnoty. Ta probíhá mimo vlastní deklaraci třídy. 6.3.2
Statické metody Statické metody mohou operovat pouze nad statickými atributy třídy. Nemají k dispozici konkrétní instanci (ledaže by do metody vstoupila v jednom z parametrů), takže ukazatel this je nedostupný. Příklad 2.06.02: Příklad ukazuje použití statických atributů a metod. Jedná se o rozšíření třídy TClovek z kapitoly 4. Nyní je zajištěno, že každá osoba má vlastní osobní číslo (nejsou duplicity). Jednak se počítá počet instancí ve statickém atributu Pocet (je inicializován na 0). Při vytváření první instance se volá inicializační metoda StatInic (je statická a tak může operovat pouze nad statickými atributy), která zinicializuje statické pole OsCisla (indikuje použití daného osobního čísla). V upravené metodě NastavOsCislo se pak testuje, zda osobní číslo již nebylo přiřazeno jiné instanci. Při rušení instance se osobní čísla z pole OsCisla nemažou. Tuto akci však lze přidat do destruktoru podobně, jako operaci snižující počet alokovaných instancí.
62
Algoritmy a programování v jazyce C/C++
#include #include <string.h> //maximalni pocet osob: const int MaxPocet=1000; class TClovek{ protected: char *Jmeno; char *Prijmeni; int OsCislo; //aktualni pocet osob: static int Pocet; //obsazena/volna osobni cisla: static bool OsCisla[MaxPocet]; //inicializacni staticka metoda: static void StatInic(); public: //konstruktor: TClovek( const char *Jmeno="", const char *Prijmeni="", int OsCislo=0); //destruktor: ~TClovek(); void NastavJmeno(const char *Jmeno); void NastavPrijmeni(const char *Prijmeni); void NastavOsCislo(int OsCislo); void Vypis(); }; //inicializace poctu: int TClovek::Pocet=0; bool TClovek::OsCisla[MaxPocet]; //konstruktor-zmena: TClovek::TClovek( const char *Jmeno, const char *Prijmeni, int OsCislo) :Jmeno(NULL),Prijmeni(NULL) { //pro prvni volej inicializaci: if(Pocet==0) StatInic(); //zvys pocitadlo: Pocet++; NastavJmeno(Jmeno); NastavPrijmeni(Prijmeni);
6. Další rysy OOP v programovacím jazyce C++
63
NastavOsCislo(OsCislo); } //destruktor-zmena: TClovek::~TClovek() { if(Jmeno!=NULL) delete[] Jmeno; if(Prijmeni!=NULL) delete[] Prijmeni; //sniz pocitadlo: Pocet--; } . . //take zmena: void TClovek::NastavOsCislo(int OsCislo) { if(OsCislo<=0) //cislo mimo rozsah: TClovek::OsCislo=0; else if(OsCislo>=1 && OsCislo<=MaxPocet && OsCisla[OsCislo-1]) //cislo jiz obsazeno: TClovek::OsCislo=0; else{ //korektni cislo: TClovek::OsCislo=OsCislo; //obsad jej: OsCisla[OsCislo-1]=true; } } . . //staticka metoda: void TClovek::StatInic() { //zatim cisla nepouzita: for(int i=0;i<MaxPocet;i++) OsCisla[i]=false; } //test: int main() { //vytvoreni a inicializace instanci: TClovek c1("Frantisek","Cempirek",20), c2("Oldrich","Blatecky",10), c3("Jaroslav","Pribyl",10);
64
Algoritmy a programování v jazyce C/C++
//vypis stavu: c1.Vypis(); c2.Vypis(); c3.Vypis(); return 0; } Poznámka: Při výpisu instance c3 dostaneme hlašení NEDEFINOVANO, protože osobní číslo 10 bylo již obsazeno instanci c2.
6.4
Spřátelené třídy a funkce
Zařazením položky třídy do sekce private nebo protected zajistíme, že bude daná položka dostupná pouze v instanci dané třídy (tedy v jejích metodách) a nikde jinde. Tak můžeme snadno „odstínit“ naši instanci od okolního prostředí. Mnohdy je výhodné, aby k privátním položkám mohli přistupovat i instance jiných tříd. Zařazením položek do sekce public bychom kýženého efektu nedosáhli (pak by k položkám mohl přistoupit kdokoliv). Pro řešení daného problému nabízí C++ klíčové slovo friend. Pomocí tohoto klíčového slova můžeme zpřístupnit určené privátní položky (nebo celou třídu) jiné třídě. Spřátelení deleguje vždy třída, o jejíž položky jde. Takže třída A se může rozhodnout, že třídě B zpřístupní privátní položku a. Opačný postup: třída B se rozhodne, že chce zpřístupnit položku a třídy A, není možný. Je to celkem logické z hlediska bezpečnosti. Spřátelit lze buď vybrané metody a atributy nebo celou třídu. Jako přítel třídy může být definována i prostá funkce (není členem žádné třídy). Takový případ je velmi důležitý například pro možnost poskytnutí výpisu instance do proudu přetížením operátoru <<. Příklad 2.06.03: Příklad rozšiřuje předchozí příklad 2.06.01 a definuje význam insertoru pro třídu TIntArray. Vzhledem k tomu, že atribut p je v této třídě definován jako chráněný, není možno přistoupit k němu z vnějšku. Jedinou možností je operátorovou funkci prohlásit za přítele třídy TIntArray. #include #include <string.h> . . //pole celych cisel: class TIntArray{ protected: int* p; friend ostream& operator<<(ostream &s,const TIntArray &a); public: const int MinIndex,MaxIndex; TIntArray(int MinIndex, int MaxIndex); virtual ~TIntArray(); virtual int GetItem(int Index) const; virtual void SetItem(int Index, int Data); }; . .
6. Další rysy OOP v programovacím jazyce C++
65
//insertor pro TIntArray: ostream& operator<<(ostream &s,const TIntArray &a) { for(int i=a.MinIndex;i<=a.MaxIndex;i++) cout<<"a["<
6.5
Vložené datové typy
Do deklarace třídy je možno zařadit také prvky, které představují deklaraci typu. Mohou to být typy vytvořené pomocí konstrukcí: enum, typedef, struct, class, union. Tato technika se nejčastěji využívá pro výčet (enum). Má-li být daný typ dostupný vně třídy, musí definován v sekci public. Mimo třídu je však nutno uvádět plnou kvalifikaci (název třídy :: vložený typ). Příklad ukazuje, jak do třídy TClovek přidáme informaci o pohlaví osoby. Je rozumné tyto možnosti definovat výčtovým typem TPohlavi a ten zahrnout jako vložený typ do dané třídy. Následně deklarujeme atribut, který hodnotu typu TPohlavi v instanci třídy TClovek udržuje: class TClovek{ public: enum TPohlavi{pMuz, pZena}; . . protected: TPohlavi Pohlavi; . . };
66
Algoritmy a programování v jazyce C/C++
7 Proudová knihovna Téma proudové knihovny jsme „nakousli“ již v kapitole 3.6.3. Nyní je pravý čas na to, ukázat si, jak bohaté možnosti poskytuje programátoru. Datové proudy výrazně usnadňují vstup a výstup zabudovaných typů jazyka C++. Velmi jednoduchou konstrukcí je umožněna podpora vstupu a výstupu i pro uživatelsky definované typy (viz příklad 2.06.03 z kapitoly 6.4). Datové proudy jsou zvláštní třídy definované v hlavičce iostream.h. Standardně definované proudy a jejich význam jsou uvedeny v tab. 7.1. Tab. 7.1 Standardně definované proudy
Proud cout cin cerr
7.1
Směr výstup vstup výstup
Význam standardní výstupní proud standardní vstupní proud standardní výstup chybových zpráv
Bázový proud – ios
Třída ios je bází pro všechny proudy, proto bude užitečné povědět si nejdříve něco o tomto proudu. 7.1.1
Stavy datového proudu Stav datového proudu lze zjišťovat pomocí čtyř metod, jedna metoda slouží k nastavení tohoto stavu (s výhodou ji použijeme v případě, že chceme přetížit operátor << nebo >>). Viz tab. 7.2. Tab. 7.2 Metody pro zjišťování stavu proudu
Metoda int eof( ); int fail( ); ind bad( ); int good( ); void clear(int i=0);
Význam zjištěn konec souboru další operace selže narušen datový proud další operace snad dopadne dobře nastaví stav proudu
Pro reprezentování těchto čtyřech stavů jsou definovány hodnoty: ios::goodbit, ios::eofbit, ios::failbit, ios::badbit. 7.1.2
Formátování V této kapitole popíšeme prostředky implementované proudem ios pro formátování vstupu a výstupu. Šířka a výplňový znak Šířka a výplňový znak se nastavují pomocí metod width a fill. width určuje minimální počet znaků, které se použijí v následující numerické nebo řetězcové operaci. Je-li znaků více, vytisknou se všechny. Jednou nastavená šířka je uvažována pouze pro jeden výstup. Při dalším výstupu se použije výchozí nulová šířka. Pokud chceme zvolenou šířku používat opakovaně, musíme ji před výpisem vždy nastavit znovu!
7. Proudová knihovna
67
Tab. 7.3 Metody pro nastavení šířky a výplně
Metoda int width(int w); int width( ) const; char fill(char c); char fill( ) const;
Význam nastaví šířku výpisu zjistí šířku výpisu nastaví jako výplňový znak c zjistí výplňový znak
Příklad: #include void main() { cout.width(10); cout.fill('*'); cout<<56; } Vypíše: ********56 Nastavení/zjištění formátu Stav formátu je řízen metodami flags a setf. Jednotlivé příznaky formátu jsou uvedeny v tab. 7.4. Jedná se vlastně o symbolická pojmenování jednotlivých bitů, které dohromady tvoří bitové pole. Tab. 7.4 Příznaky k řízení formátu
Příznak ios::skipws ios::left ios::right
Význam přeskoč bílé znaky (odsazovač) na vstupu zarovnání pole – vyplnění za hodnotou (skupina ios::adjustfield) zarovnání pole – vyplnění před hodnotou (skupina ios::adjustfield) zarovnání pole – vyplnění mezi znaménkem a hodnotou (skupina ios::internal ios::adjustfield) dekadický celočíselný základ (skupina ios::basefield) ios::dec oktalový celočíselný základ (skupina ios::basefield) ios::oct hexadecimální celočíselný základ (skupina ios::basefield) ios::hex ios::showbase tisk celočíselného základu ios::showpoint tisk nul na konci ios::uppercase 'E' a 'X' místo 'e' a 'x' ios::showpos zobrazení znaménka i pro kladná čísla ('+') notace s pohyblivou řádovou čárkou: .dddddd Edd (skupina ios::floatfield) ios::scientific notace s pohyblivou řádovou čárkou: dddd.dd (skupina ios::floatfield) ios::fixed vyprázdnění výstupního bufferu po každé operaci ios::unitbuf vyprázdnění výstupního bufferu po každém znaku ios::stdio
68
Algoritmy a programování v jazyce C/C++
Tab. 7.5 Metody řídicí formátování a jejich význam
Metoda long flags(long f); long setf(long f); long flags( ) const; long setf(long setbits, long field);
long unsetf(long f);
Popis nastaví nové příznaky pro formátování podle f, vrátí předchozí nastavení to samé jako flags( ) vrátí aktuální nastavení formátovacích příznaků nastaví specifikovaný příznak setbits podle field: ios::basefield – celočíselný základ, ios::adjustfield – zarovnávání pole, ios::floatfield – notace s pohyblivou řádovou čárkou vyjme nastavený bit z příznaků, vrátí předchozí nastavení
Příklad: #include void main() { int cele=59; float real=589.458796; //ukáže základ, velká písmena: cout.flags(ios::showbase|ios::uppercase); cout.setf(ios::dec,ios::basefield); //desítkově cout<
7. Proudová knihovna
69
cout<
Manipulátor ios dec(ios& s); ios hex(ios& s); ios oct(ios& s); ostream& endl(ostream& s); ostream& ends(ostream& s); ostream& flush(ostream& s);
Význam použita decimální notace použita hexadecimální notace použita oktalová notace přidej '\n' a vyprázdni výstupní buffer přidej '\0' a vyprázdni výstupní buffer vyprázdni výstupní buffer přeskoč bílé znaky (odsazovače) ve vstupním istream ws(istream& s); proudu Parametrické manipulátory z hlavičky iomanip.h: nastav číselný základ (8,10 nebo 16) setbase(int base); nastav výplňový znak setfill(int fill); nastav přesnost pro čísla v plovoucí řádové čárce setprecision(int prec); nastav minimální šířku výpisu setw(int width); vynuluj příslušný příznak v bitovém poli formátu resetiosflags(long flags); nastav příslušný příznak v bitovém poli formátu setiosflags(long flags);
7.2
Výstup – ostream
Třída ostream je definována tak, aby operátor << zajistil výstup vestavěných typů. Umožňuje tedy výstup datových typů: char*, char, short, int, long, double, void*. Třída je přímým následníkem ios. Tab. 7.7 Metody třídy ostream
Metody ostream& flush( ); long tellp( ); ostream& write(const char* s, int n); ostream& put(char c); ostream& seekp(long pos); ostream& seekp(long off, seek_dir sd);
Význam Vyprázdní buffer výstupního proudu. Vrátí aktuální pozici ve výstupním proudu. Vloží do výstupního proudu n znaků řetězce s. Vloží znak c do výstupního proudu. Nastaví ukazovátko ve výstupním proudu na pozici danou pos. Posune ukazovátko ve výstupním proudu o relativní vzdálenost určenou off. Relativní pozice se počítá podle sd (beg – od začátku, cur – aktuální pozice, end – konec).
Příklad: #include <string.h> #include void main() { char str[]="Jak se máš?";
70
Algoritmy a programování v jazyce C/C++
cout<<"AHOJ!!!"<<endl; cout.write(str,strlen(str)); cout.flush(); //jinak se znaky poslané write nezapíší }
7.3
Vstup – istream
Třída istream je definována tak, aby operátor >> zajistil vstup vestavěných typů. Třída je přímým následníkem ios. Tab. 7.8 Metody třídy istream
Metoda istream& get(char* s, int len, char term='\n'); istream& read(char* s, int n); int peek( ); istream& putback(char c); long tellg( ); istream& seekg(long pos); istream& seekg(long off, seek_dir sd);
istream& ignore(int n=1, int delim=EOF);
Význam Načte maximálně len-1 znaků ze vstupního proudu do řetězce s (len je vlastně fyzická délka řetězce). Při nalezení ukončovacího znaku term se čte jen do tohoto znaku. Načte n znaků ze vstupního proudu do řetězce s. Zjistí další znak ve vstupním proudu bez jeho čtení (vyjmutí z proudu). Vrátí dříve přečtený znak c zpět do vstupního proudu (bude čten znovu). Vrátí aktuální pozici ve vstupním proudu. Nastaví ukazovátko ve vstupním proudu na pozici danou pos. Posune ukazovátko ve vstupním proudu o relativní vzdálenost určenou off. Relativní pozice se počítá podle sd (beg – od začátku, cur – aktuální pozice, end – konec). Přeskočí n znaků ve vstupním proudu, zastaví se při naleznutí znaku delim.
Příklad použití metod třídy istream (zadejte například ahoj@jak se máš5???): #include #include <string.h> void main() { char str[100]; cin.ignore(1000,'@'); //vynech 1000 znaků nebo do znaku '@' cin.get(str,100,'5'); //maximálně 99 znaků do znaku '5' cout<<str<<endl; }
7.4
Souborové proudy
C++ definuje tři souborové proudy, viz tab. 7.9. Souborové proudy jsou definovány v hlavičce fstream.h. Společná báze je fstreambase, pro nás je zajímavá její metoda close, která zavře soubor.
7. Proudová knihovna
71
Tab. 7.9 Souborové proudy
Třída ifstream ofstream fstream
Význam proud pro vstupní soubor proud pro výstupní soubor proud pro čtení i zápis
Tab.7.10 Režimy otevření proudu definované v ios (bitové pole)
Režim ios::in ios::out ios::ate ios::app ios::trunc ios::nocreate ios::noreplace ios::binary 7.4.1
Význam otevři pro čtení otevři pro zápis otevři a přesuň se na konec souboru přidej na konec (append) zkrať délku souboru na 0 (přepis) selže, neexistuje-li soubor (otevře existující) selže, když soubor existuje otevři jako binární (ne textový) soubor
Vstupní souborový proud – ifstream ifstream je proud pro vstupní soubor. Je to přímý následník istream. Tab. 7.11 Metody proudu ifstream
Metoda
Význam prázdný konstruktor, soubor se musí otevřít metodou open
ifstream( ); ifstream(const char* filename, int openmode=ios::in, int protect=filebuf::openprot); void open(const char* filename, int openmode=ios::in, int protect=filebuf::openprot); 7.4.2
otevře soubor se jménem filename implicitně jako vstupní s implicitní ochranou otevře soubor se jménem filename implicitně jako vstupní s implicitní ochranou
Výstupní souborový proud – ofstream ofstream je proud pro výstupní soubor. Je to přímý následník ostream. Tab. 7.12 Metody proudu ofstream
Metoda
Význam prázdný konstruktor, soubor se musí otevřít metodou open
ofstream( ); ofstream(const char* filename, int openmode=ios::out, int protect=filebuf::openprot); void(const char* filename, int openmode=ios::out, int protect=filebuf::openprot);
otevře soubor se jménem filename implicitně jako výstupní s implicitní ochranou otevře soubor se jménem filename implicitně jako výstupní s implicitní ochranou
7.4.3
Vstupně/výstupní souborový proud – fstream fstream je proud určený pro čtení i zápis do souboru. Je to přímý následník iostream (třída odvozená z istream a ostream).
72
Algoritmy a programování v jazyce C/C++
Tab. 7.13 Metody proudu fstream
Metoda
Význam prázdný konstruktor, soubor se musí otevřít fstream( ); metodou open otevře soubor se jménem filename v módu fstream(const char* filename, int podle openmode s implicitní ochranou openmode, int protect=filebuf::openprot); otevře soubor se jménem filename v módu void open(const char* filename, int podle openmode s implicitní ochranou openmode, int protect=filebuf::openprot);
7.5
Řetězcové proudy
Podobně jako souborové proudy jsou definovány řetězcové proudy. C++ definuje tři řetězcové proudy, viz tab. 7.14. Řetězcové proudy jsou definovány v hlavičce strstream.h. (v některých překadačích je název strstrea.h, tedy zkrácen na formát 8.3). Společná báze je strstreambase. Tab. 7.14 Řetězcové proudy
Třída istrstream ostrstream strstream
Význam proud pro čtení z řetězce proud pro zápis do řetězce proud pro čtení i zápis do řetězce
7.5.1
Vstupní řetězcový proud – istrstream istrstream je proud pro čtení z asociovaného řetězce str. Je to přímý následník istream. length je délka řetězce včetně zarážky. Konstruktor: istrstream(char* str, int length); 7.5.2
Výstupní řetězcový proud – ostrstream ostrstream je proud pro zápis do asociovaného řetězce str. Je to přímý následník ostream. length je délka řetězce včetně zarážky. Konstruktor: ostrstream(char* str, int length, int openmode=ios::out); 7.5.3
Vstupně/výstupní řetězcový proud – strstream strstream je proud určený pro čtení i zápis do asociovaného řetězce. Je to přímý následník iostream (třída odvozená z istream a ostream). length je délka řetězce včetně zarážky. Konstruktor: strstream(char* str, int length, int openmode1);
Příklad 2.07.01: Vytvoříme program realizující výpis vstupního souboru v hexadecimální podobě (tzv. hexadump). Příklad je praktickou ukázkou práce s proudy ifstream (vstupní soubor), ofstream (výstupní soubor) a ostrstream (proud pro zápis do řetězce). Ze vstupního souboru se čte po jednom znaku (musí pracovat v binárním režimu a je nutno vyjmout standardní příznak skipsw, aby nedošlo k přeskakování řídicích znaků tak, jak je to obvyklé v textovém režimu). Do výstupního souboru se pak znak ukládá jako jeho odpovídající číselná hodnota v šestnáctkové soustavě. Současně se znaky ukládají i do pomocného řetězce string (přes proud text) a tak je možno vypsat obsah souboru i v normální formě, jako text. 1
Viz tab. 7.10.
7. Proudová knihovna
73
Příklad zároveň ukazuje použití parametrů funkce main (viz kapitolu 2.5). Jméno výstupního souboru se načte z argv[1], jméno vstupního souboru se načte z argv[2]. #include #include #include #include
<strstream.h>
int main(int argc, char** argv) { //maximální počet sloupců const unsigned MAX=16; //vstupní a výstupní soubor: ifstream sin; ofstream sout; //jeden přečtený znak: unsigned char c; //počítadlo sloupců: unsigned count=0; //počáteční adresy: unsigned address=0; //pomocný řetězec: char string[MAX+1]; //řetězcový proud napojený na string: ostrstream text(string,sizeof(string)); //test počtu parametrů: if(argc!=3){ cerr<<"Zadejte vystupni a vstupni soubor"<<endl; return 1; } //pokus o otevření vstupního souboru: if(sin.open(argv[2],ios::in|ios::binary),!sin){ cerr<<"Vstupni soubor nelze otevrit"<<endl; return 2; } //pokus o otevření výstupního souboru: if(sout.open(argv[1]),!sout){ cerr<<"Vystupni soubor nelze otevrit"<<endl; return 3; } //odejmi standardní příznak přeskakovaní "bílých" znaků: sin.unsetf(ios::skipws); //hexadecimalní výpis velkými písmeny: sout.flags(ios::hex|ios::uppercase); //čti první znak: sin>>c;
74
Algoritmy a programování v jazyce C/C++
//čtení vstupu do konce souboru: while(!sin.eof()){ //na začátku řádku zapíše adresu: if(count==0){ sout<<setw(8)<<setfill('0')<=' '?c:'.'); //čti další znak: sin>>c; //na konci řádku nebo na konci vstupního souboru: if(!(++count%=MAX)||sin.eof()){ //pokud řádek nebyl dokončen, přidej mezery: for(unsigned i=count;i>0&&i<MAX;i++){ //3 mezery za každý chybějící znak: sout<<" "; //za každou čtveřici přidej mezeru: if((i%4)==3) sout<<" "; } //do string dej zarážku: text<<ends; //vytiskni string: sout<<string<<endl; //nastav ukazovátko v text na začátek: text.seekp(0); } } //zavři soubory: sout.close(); sin.close(); //úspěšné dokončení: return 0; } Příklad zápisu parametrů programu na příkazovou řádku: MAIN.EXE MAIN.DMP MAIN.CPP
7. Proudová knihovna
75
Začátek hexadumpu pro soubor MAIN.CPP (výše uvedený zdrojový text): 00000000: 00000010: 00000020: 00000030: 00000040: 00000050: 00000060: 00000070: 00000080: 00000090: 000000A0: 000000B0: 000000C0:
23 61 3C 63 3E 73 20 63 0A 6F 63 41 70
69 6D 69 6C 0D 74 6D 68 20 E8 6F 58 6E
6E 2E 6F 75 0A 72 61 61 20 65 6E 3D ED
63 68 6D 64 23 65 69 72 2F 74 73 31 20
6C 3E 61 65 69 61 6E 2A 2F 20 74 36 61
75 0D 6E 20 6E 6D 28 2A 6D 73 20 3B 20
64 0A 69 3C 63 2E 69 20 61 6C 75 0D 76
65 23 70 66 6C 68 6E 61 78 6F 6E 0A FD
20 69 2E 73 75 3E 74 72 69 75 73 20 73
3C 6E 68 74 64 0D 20 67 6D 70 69 20 74
69 63 3E 72 65 0A 61 76 E1 63 67 2F 75
6F 6C 0D 65 20 0D 72 29 6C F9 6E 2F 70
73 75 0A 61 3C 0A 67 0D 6E 0D 65 76 6E
74 64 23 6D 73 69 63 0A ED 0A 64 73 ED
72 65 69 2E 74 6E 2C 7B 20 20 20 74 20
65 20 6E 68 72 74 20 0D 70 20 4D 75 73
#include ..#include ..#in clude ..#include <str stream.h>....int main(int argc, char** argv)..{. . //maximální p očet sloupců.. const unsigned M AX=16;.. //vstu pní a výstupní s
. . Poznámka: Všimněte si, že proudy lze používat v logických výrazech. Tímto způsobem lze snadno testovat jejich stav. Například výraz if(soubor) bude chápán jako korektní (pokud soubor je proud). Pravdivý pak bude v případě, že nad proudem soubor nedošlo k chybě. V příkladu byla použita konstrukce: if(sin.open(argv[2],ios::in|ios::binary),!sin). V rámci podmíněného příkazu byl tedy do proudu sin otevřen vstupní soubor a dále se testovala úspěšnost této operace. Je-li výraz !sin pravdivý, soubor se nepodařilo otevřít.
76
Algoritmy a programování v jazyce C/C++
8 Programování pod Linuxem V této kapitole si popíšeme základní principy programování v C/C++ pod operačním systému Linux.
8.1
Základní pojmy Nejdříve si probereme základní pojmy.
8.1.1
Unix Unix je operační systém vyvinutý firmou Bell Laboratories v roce 1970 pro firmu Digital, který se za krátkou dobu stal velmi oblíbeným víceuživatelským, víceúlohovým operačním systémem pro celou řadu hardwarových platforem, od pracovních stanic, přes servery až po superpočítače. Přesněji řečeno UNIX je registrovaná ochranná známka vlastněná firmou X/Open Company, Ltd. a touto známkou lze označit pouze počítačový systém, který splňuje specifikace X/Open pro chování všech funkcí OS. Dnes je k dispozici mnoho unixových systémů, jak komerčních (Aix, Solaris, HP-UX, Irix) anebo volných jako je FreeBSD či Linux. 8.1.2
Linux V roce 1991 začal Linus Torvalds na univerzitě v Helsinkách vytvářet jako semestrální projekt nový operační systém pro počítače s procesory Intel x86. Jeho práce vycházela z operačního systému Minix, což byl v té době standardní unixový operační systém pro osobní použití. Linus se však rozhodl podělit se o svou práci s ostatními programátory a zveřejnil zdrojové kódy svého projektu na internetu pod licencí GNU General Public License, takže jeho zdrojový kód byl a je volně k dispozici každému. Tak se společným vývojem programátorů stal z Linuxu plnohodnotný operační systém. Linux tedy vychází z Unixu a i když s původním Unixem nesdílí ani řádku zdrojového kódu, má stejné aplikační rozhraní i filozofii. Díky tomu je kompatibilní s ostatními Unixy na úrovni zdrojových kódů. Proto má i stejné aplikace a nástroje, podporuje souběžnou práci více uživatelů, z nichž každý může spouštět libovolný počet programů. Linux byl portován na spoustu jiných platforem, než je tradiční počítač. Dnes ho najdete v telefonech, organizérech a domácí elektronice, přes pracovní stanice či servery až po řídící systémy letadel. 8.1.3
GNU Projekt GNU (GNU je rekurzivní zkratka pro "GNU's Not UNIX"; vyslovuje se [gnů]) byl založen Richardem M. Stallmanem v roce 1984 s cílem vytvořit klon operačního systému UNIX, který by byl šířen jako svobodný software (angl. free software) a který nebude zatížen copyrightem. Za tím účelem byla vytvořena speciální licence, která doslova obrací copyright (také proto bylo pro vyjádření jejího principu zvoleno pojmenování copyleft). Nejstriktnější formou copyleftové licence, která je použita pro všechny zásadní programové části GNU, je GNU GPL (General Public License = Všeobecná veřejná licence). Velice stručně lze GNU GPL shrnout tak, že předmět licence může být používán, kopírován, pozměňován a distribuován – naopak žádná jeho část nesmí být zatížena licencí v rozporu s GPL. V rámci GNU projektu bylo vyvinuto mnoho programů (v době vzniku tohoto textu je na www.gnu.org evidováno 3782 projektů), které postupně zcela plnohodnotně nahradily komponenty dosavadních uzavřených unixových systémů. Jedinou věcí, která scházela, bylo jádro operačního systému. Tato mezera byla zaplněna jádrem, které začal programovat L. Torvalds – Linuxem. Kombinací softwaru vzniklého pod hlavičkou GNU a 8. Programování pod Linuxem
77
kernelu Linux se zrodil kompletní operační systém schopný samostatného provozu bez jakékoliv součásti, jež by nevyhovovala GPL. Ke GNU softwaru, se kterým budeme pracovat při programování v jazyce C/C++ pod linuxem, bude především patřit: • Gcc – skupina kompilátorů jazyků C, C++, Objective C, Ady, Javy a fortran, • G++ – kompilátor jazyka C++, součást Gcc, • Glibc – knihovna funkcí specifikovaných v ISO C a POSIX standardech, • Gdb – ladící program (debugger), • GNU make – verze unixového programu make.
8.2
Souborový systém, spouštění programů
Souborové systémy operačních systémů Microsoft převzaly své vlastnosti od souborových systémů Unixu, jsou tu však rozdíly. Nejzásadnější uživatelské změny v unixových souborových systémech jsou: • je jen jedna velká stromová struktura začínající v kořenovém adresáři, Linux/Unix rozlišuje velká a malá písmena (standardně se u názvů souborů i adresářů používají malá písmena), jména souborů jsou dlouhá max. 255 znaků (ext3 fs, lze rozšířit), v názvu souboru se nesmí vyskytnout znak NUL (binární nula) a znak lomítko "/", celé jméno souboru včetně cesty je dlouhé max. 4 kB, a k oddělování adresářů a souborů používá normální lomítko "/", • základní adresáře OS mají pevné názvy a použití, • soubory začínající tečkou "." jsou skryté a nevidíme je pokud je extra nevyžádáme (např. .bash_profile), jsou obdobou skrytých souborů z DOSu, • Adresář automaticky obsahuje dvě položky: • aktuální adresář, • nadřazený adresář, • spustitelný soubor se neodlišuje příponou, ale atributem souborového systému. Protože jsou unixové operační systémy víceuživatelské, je nutné pro práci na takovémto systému se přihlásit. Poté už je možno prostřednictví shellu (obdoba command.com z DOSu) zadávat systému příkazy. Programy, které chce uživatel spustit se hledají v adresářích uvedených ve vyhledávací cestě, která se nastavuje proměnnou PATH (obsah lze zjistit příkazem set). Tato proměnná nejčastěji obsahuje adresáře: • /bin – základní spustitelné soubory pro použití všemi uživateli, • /sbin – systémové privilegované spustitelné soubory, používané uživatelem root, • /usr/bin – další, převážně uživatelské programy, • /usr/sbin – systémové programy, • /usr/local/bin – lokálně instalované aplikace, • ~/bin – spustitelné soubory pro použití daným uživatelem (~ představuje domovský adresář). Při takto nastavené vyhledávací cestě je nutné, aby uživatelem vytvořené soubory byly umístěné v ~/bin . Pokud jsou mimo tento adresář, je nutné shellu říci, že se daný program má spustit z aktuálního adresáře, tedy s uvedením znaku . Např.: ./mujprogram. Jestliže vyhledávací cesta neobsahuje znak ., je vhodné si ji doplnit (v shellu bash je to soubor ~./bash_profile) a nebudete tak omezeni na konkrétní adresář.
78
Algoritmy a programování v jazyce C/C++
8.3
Editory
V unixových distribucích můžete nalézt mnoho editorů jak pro terminálový přístup, tak i pro grafické prostředí, z nejznámějších lze vyjmenovat: • joe – jednoduchý editor, který vystačí téměř ve všech případech, • pico – ještě jednodušší obdoba editoru joe, • vi – extrémně mocný, ale také stejnou měrou extrémně nepřátelský nástroj, nelze s ním úspěšně pracovat metodou pokus a omyl, jeho výhodou je jeho přítomnost ve všech instalacích Unixu/Linuxu a zvýrazňování syntaxe, • emacs – komplexní nástroj, jeho vlastnosti využijí zejména zkušenější programátoři, neboť kromě zvýrazňování syntaxe, spouštění překladu přímo z editoru, umožňuje i psaní vlastních skriptů, • kedit, gedit, gxedit, xwpe – editory určené pro grafické prostředí X-Window.
8.4
Kompilace programu
Nejdříve si vytvoříme ve svém oblíbeném textovém editoru jednoduchý program v jazyce C, třeba ten úplně nejznámější: #include <stdio.h> int main(void) { printf("Hello World!\n"); return 0; } a uložíme ho do souboru hello.c. Nyní záleží na výběru překladače podle unixové/linuxové distribuce, na které pracujeme. Systémy splňující normy POSIX obsahují kompilátor s názvem cc. Linuxové distribuce obsahují GNU kompilátor gcc podporující standard ANSI, který se spouští příkazem gcc. Příkazem: gcc hello.c provedeme sestavení, překlad i linkování. Je-li zdrojový soubor bez chyb, kompilátor vytvoří spustitelný standardní soubor a.out. Spuštění programu z aktuálního adresáře provedeme příkazem: ./a.out Pokud chceme výstupní soubor pojmenovat, stačí použít parametr –o a uvést jméno výstupního souboru: gcc hello.c –o hello Chování překladače lze ovlivňovat zadanými parametry, seznam parametrů programu nalezneme v manuálových stránkách, které lze získat pro konkrétní program příkazem man, kde jako parametr uvedeme název programu, pro který chceme nápovědu zobrazit. V našem případě tedy: man gcc
8. Programování pod Linuxem
79
Programy z GNU projektu mají dokumentaci dostupnou pomocí příkazu: info gcc kde tato dokumentace je většinou podrobnější, může být provázána křížovými odkazy a často obsahuje i příklady použití jednotlivých parametrů. Nejužívanějšími parametry kompilátoru gcc budou: • –o určuje výstupní soubor • –c vytvoření objektového tvaru souboru, linkování není provedeno • –g zajistí přidání informací pro debugger • –x explicitní určení programovacího jazyka vstupního souboru • –v vypíše na standardní chybový výstup všechny příkazy provedené při překladu • –Wall kompilátor bude vypisovat všechna varovná hlášení
8.5
Ladění programu
Abychom pro ladění programu mohli použít debugger Gdb, je nutné překlad spustit s parametrem –g. Debugger pak voláme: gdb [volby] program Pro ovládání je nutno znát následující příkazy: • r spustí program, • n vykoná další řádek programu (bez vstupu do procedur), • s vykoná další řádek programu (vstupuje do procedur), • p x vypíše hodnotu proměnné x, • l vypíše část programu, • c pokračuje ve vyhodnocování, • where vypíše seznam volaných procedur. Dále můžeme u debuggeru použít: • breakpoint – zastaví běh programu, • trasování proměnných – zastaví při každé změně proměnných, • trasování procedur – zastaví se při každém vstupu do dané procedury, • zastavení při signálech, • zastavení při splnění zadaných podmínek.
8.6
Knihovny
Standardní systémové knihovny se v unixových/linuxových systémech nacházejí v adresářích /lib a /usr/lib. Abychom je mohli použít, je potřeba překladači předat cestu a jméno knihovny, jinak prohledává jen standardní knihovnu jazyka C. Pokud chceme překladači (konkrétně linkeru) říci, že má prohledávat námi zvolenou knihovnu, zapíšeme ji do příkazové řádky překladači. 8.6.1
Statické knihovny Statické knihovny jsou vlastně jednoduchou sbírkou přemístitelného kódu uloženého v jediném souboru, proto se někdy též nazývají archivy. Pokud linkeru specifikujeme nějakou statickou knihovnu, bude v ní program vyhledávat kódy funkcí, jež použijeme v programu. Vyhledanou funkci extrahuje a spojí ji s programem stejně, jako bychom přemístitelný kód specifikovali spojovacímu programu odděleně.
80
Algoritmy a programování v jazyce C/C++
Pro vytvoření statické knihovny se používá program ar. Statické knihovny používají příponu .a. Následující příklad vytváří ze souborů lib1.o a lib2.o knihovnu libtest.a. ar cr libtest.a lib1.o lib2.o Další volby programu ar: • c vytvoří nový archiv, • r vloží soubory do archivu, • d maže moduly podle jejich jména, • t vypíše tabulku modulů, ze kterých byl archiv vytvořen, • p vypíše specifikované položky archivu na standardní výstup. Některé linuxové distribuce vyžadují před použitím knihovny vytvoření jejího obsahu. Toho lze docílit programem ranlib, kterému se jako parametr předá jméno nově vytvářené statické knihovny. 8.6.2
Dynamické knihovny Dynamické knihovny jsou podobné statickým knihovnám – také obsahují sbírku předkompilovaných funkcí. Program používající dynamickou knihovnu však neobsahuje její kód. Obsahuje jen odkaz na tento kód v dynamické knihovně. Tu mohou využívat i další programy, proto se také dynamické knihovny nazývají knihovnami sdílenými. Budeme-li vytvářet dynamickou knihovnu, musíme nejdříve její moduly přeložit kompilátorem spuštěným s parametrem –fPIC: gcc -c -fPIC lib1.c Zkratka PIC označuje Position-Independent Code, což znamená, že funkce ve sdílené knihovně mohou být zavedeny do paměti na různých pozicích. Proto kód těchto funkcí nesmí záviset na pozici v paměti. Po překladu lze přemístitelné kódy spojit do jediného souboru, tedy do dynamické knihovny: gcc -shared -fPIC -o libtest.so lib1.o lib2.o Volba –shared sděluje programu ld, že má vytvořit sdílenou knihovnu a nikoliv spustitelný kód. Sdílené knihovny mají příponu .so (shared object). Pomocí příkazu ldd lze vypsat seznam všech sdílených knihoven, které jsou spojeny s daným spustitelným programem.
8.7
Standardy
Již od roku 1980 jsou snahy o sjednocení rozvíjejících se unixových systémů. Tyto snahy se postupně vyvinuly v různé standardy. Zde si povíme o těch hlavních, které ovlivňují práci programátora. ANSI C Snahou normy ANSI C (byla uznána jako mezinárodní standard v roce 1990) je nabídnout přenositelnost programu v jazyce C na různé operační systémy, tedy nejen na unixy. Není zde definována pouze syntaxe a sémantika jazyka C, ale i knihovna standardních funkcí. Prototypy těchto funkcí jsou v 15 hlavičkových souborech – viz dále.
8. Programování pod Linuxem
81
Tab. 8.1. Porovnání standardů
Hlavičky <errno.h> <math.h> <search.h> <setjmp.h> <signal.h> <stdarg.h> <stddef.h> <stdio.h> <stdlib.h> <string.h> <sys/ipc.h> <sys/msg.h> <sys/sem.h> <sys/shm.h> <sys/stat.h> <sys/times.h> <sys/types.h> <sys/utsname.h> <sys/wait.h>
ANSI C
POSIX.1
XPG3
Popis ověřuje asertivitu archiv cpio typy znaků položky adresáře chybové kódy ovládání souborů konstanty v plovoucí čárce procházení stromu souboru soubor skupin jazykové informace konstanty implementace lokální kategorie matematické konstanty katalogy zpráv soubor hesel regulární výrazy vyhledávací tabulky nelokální goto signály seznamy argumentů standardní definice standardní I/O pomocné funkce operace s řetězci archiv tar terminálové I/O čas a datum uživatelská omezení symbolické konstanty časy souboru IPC fronty semafory sdílená paměť status souboru časy procesu primitivní datové typy jméno systému řízení procesu
POSIX IEEE POSIX je rodina norem, která se od svého vzniku kolem roku 1988 zatím stále rozvíjí. Formální název pro POSIX je Portable Operating System Interface for Computer Environment. POSIX vychází sice z Unixu (hlavně ze Systému V a BSD), ale sám to není operační systém. POSIX pouze definuje, jak spolu komunikuje aplikace a operační systém.
82
Algoritmy a programování v jazyce C/C++
Pokud chcete psát aplikace přenositelné na největší množství hardwaru a operačních systémů, je POSIX způsob, jak toho dosáhnout. XPG3 XPG3 je zkratka X/Open Portability Guide, Issue 3 (1989). X/Open je mezinárodní skupina počítačových prodejců. XPG3 např. definuje některé věci, které ještě nejsou v POSIXu, viz tab. 8.1.
8. Programování pod Linuxem
83
9 Neprobrané partie z C++ Vzhledem k poměrně obsáhlým možnostem programování v C++ byly některé složitější partie přesunuty do dalšího ročníku. Jedná se především o: • šablony – možnost vytvářet třídy nebo prosté funkce z obecnějšího vzoru (takto lze například definovat třídu usnadňující práci s polem a poté stanovit, že položkami pole budou například celá čísla nebo řetězce apod.), • přetěžování operátorů – tuto možnost jsme již naznačili, ale je třeba dát podrobnější výklad k jejímu použití, • prostory jmen – možnost definice typů, funkcí nebo proměnných v prostoru, který je oddělen od ostatního programového kódu.
84
Algoritmy a programování v jazyce C/C++
Seznam použité literatury [1] Matoušek, D.: Programovací jazyk C přednášky, 2. vydání, 2001, skripta VOŠ Jihlava. [2] Matoušek, D.: Programovací jazyk C++ přednášky, 3. vydání, 2001, skripta VOŠ Jihlava. [3] Stuhl, J.: Algoritmy a programování v jazyku C/C++ 2. díl, přednášky, 1. vydání, 2002, skripta VOŠ Jihlava.
Anotace První představuje úvod, kterým se volně navazuje na první díl. Druhá kapitola je věnována práci se soubory. Kromě standardních funkcí nechybí ani příklady práce s textovými a binárními soubory. Připomenuta je rovněž spolupráce s operačním systémem (parametry programu, návratová hodnota funkce main). Třetí kapitola přestavuje „jemný“ úvod do programovacího jazyka C++. Jsou zde porovnány možnosti programování v jazycích C a C++. Kromě vysvětlení novinek (přetížení funkce, implicitní parametry funkce, volání parametrů odkazem) jsou předvedeny nové operátory poskytované jazykem C++. Čtvrtá kapitola představuje úvod do objektově orientovaného programování (OOP) v jazyce C++. Pátá kapitola diskutuje pokročilé možnosti OOP jako jsou: dědičnost, virtuální metody a polymorfismus. Šetá kapitola se věnuje zbývajícím rysům jazyka C++, které jsou v tomto textu vykládány. Jedná se o výjimky, statické a konstantní členy, spřátelené třídy a funkce. Sedmá kapitola je věnována podrobnému výkladu používání proudové knihovny. Mimo jiné je vysvětlena práce se souborovými a řetězcovými proudy. Osmá kapitola se věnuje výkladu programování pod Linuxem, tedy základní filozofii Linuxu a nástrojům použitelným při programování. Poslední kapitola stručně uvádí další možnosti programování v C++, výklad těchto partií je však ponechán do dalšího skripta.
Anotace a literatura
85