Stáhnout prezentaci
Prezentace se nahrává, počkejte prosím
1
Dennis MacAlistair Ritchie
Programovací jazyk C Ken Thompson 1943 - Dennis MacAlistair Ritchie 1941 - Počítač PDP-7 Operační systém UNIX byl vyvinut v sedmdesátých letech pro společnost Bell Telephone Laboratories (divize AT&T) na počítači PDP-7. V souvislosti s UNIXem byl také vyvinut programovací jazyk C. UNIX byl ve vyšších verzích psán v C-čku sám, což umožnilo jeho přenositelnost na další počítače (strojový kód je sám o sobě zpravidla nepřenositelný). V rámci projektu „Cesta k vědě“ (veda.gymjs.net) vytvořil V. Pospíšil Modifikace a šíření dokumentu podléhá licenci GNU GPL ( Použity původní materiály Antonína Šulce a stránka ( )
2
Richard Matthew Stallman 1953 -
Programovací jazyk C Richard Matthew Stallman GNU Compiler Collection (zkráceně GCC) je sada kompilátorů vytvořených v rámci projektu GNU. Původně se jednalo pouze o překladač programovacího jazyka C (a zkratka tehdy znamenala GNU C Compiler), později byly na stejném společném základě vytvořeny překladače jazyků C++, Fortran, Ada a dalších. Původním autorem GCC je Richard Stallman, který ho roku 1987 vytvořil jako jednu ze základních částí svého projektu GNU; dnes projekt zastřešuje nadace Free Software Foundation. GCC je šířen pod licencí GNU GPL a stal se již de facto standardním kompilátorem v open source operačních systémech unixového typu, ale používá se i v některých komerčních operačních systémech, např. na Mac OS X. Existují také jeho portace pro Microsoft Windows (např. mingw).
3
První progámek a jeho překlad
Programů, které zajišťují překlad zdrojáku do strojového kódu a sestavení jednotlivých přeložených souborů do jednoho je více. My budeme používat GNU překladač gcc. Jako příklad uveďme jednoduchý prográmek, který je uložen v souboru hello.c . #include <stdio.h> #include <stdlib.h> int main( void ) { printf( "Hello, world!\n" ); } /*main*/ Co která řádka konkrétněji znamená později, program jen vypíše text „Hello, world!“ na obrazovku. Povšimněte si použití syntax highlight, který je běžný u většiny textových editorů. Překlad i linkování se zařídí příkazem g++ (který je součástí systému gcc) g++ -c hello.c g++ -o hello hello.o Výsledkem bude binární soubor hello.o a spustitelný program hello.
4
Program ve strojovém kódu
Skripty pro překlady Je-li souborů se zdrojovým kódem více, je výhodné použít skript Makefile nebo další nástroje jako CMake ( nebo SCons ( Sestavování (link) #define itemDIRECTORY "directory" #define itemVOTES "votes" #define itemLIST "list" #define itemFILE "file" #define itemOUTPUT "output" #define itemUNVALID "unvalid" #define MAX_NR_OF_CANDIDATES 100 #define cand2D( x, y ) ( MAX_NR_OF_CANDIDATES*(y) + (x) ) class CElectionProcessor { public: CElectionProcessor( char *descFileName, CCategory *cat = NULL, CEntryList *ent = NULL, CVoteList *vot = NULL); ~CElectionProcessor( void ); void WriteElectionSetup( FILE *targetFILE = stdout ); int Run( FILE *out ); char *GetVoteListFileName( void ) { return voteListFileName; }; char *GetMailDirectory( void ) { return mailDirectory; }; char *GetOn FileName( void ) { return on FileName; }; char *GetMailFileNameList( void ) { return mailFileNameList; }; char *GetOutputFileName( void ) { return outputFileName; }; Zdrojový kód 1 Překlad jednoho souboru = jedno zavolání překlada-če. Velké programy mají obvykle stovky zdrojových souborů. Přeložený modul 1 Program ve strojovém kódu Přeložený modul 2 Překládat lze navíc něko-lika režimy (pro debug-ging, pro různé typy počí-tačů), nebo jenom změně-né části kvůli úspoře času. To nelze ručně. Přeložený modul 3 Zdrojový kód 2 Překlad (compile) Zdrojový kód 3
5
Skripty pro překlady Je-li souborů se zdrojovým kódem více, je výhodné použít skript Makefile nebo další nástroje jako CMake ( nebo SCons ( #define itemDIRECTORY "directory" #define itemVOTES "votes" #define itemLIST "list" #define itemFILE "file" #define itemOUTPUT "output" #define itemUNVALID "unvalid" #define MAX_NR_OF_CANDIDATES 100 #define cand2D( x, y ) ( MAX_NR_OF_CANDIDATES*(y) + (x) ) class CElectionProcessor { public: CElectionProcessor( char *descFileName, CCategory *cat = NULL, CEntryList *ent = NULL, CVoteList *vot = NULL); ~CElectionProcessor( void ); void WriteElectionSetup( FILE *targetFILE = stdout ); int Run( FILE *out ); char *GetVoteListFileName( void ) { return voteListFileName; }; char *GetMailDirectory( void ) { return mailDirectory; }; char *GetOn FileName( void ) { return on FileName; }; char *GetMailFileNameList( void ) { return mailFileNameList; }; char *GetOutputFileName( void ) { return outputFileName; }; Zdrojový kód 1 Přeložený modul 1 Sestavování (link) Program ve strojovém kódu Přeložený modul 2 za běhu programu Přeložený modul 3 Dynamicky linkovatelná knihovna (.dll ve windows, .so pod linuxem) Zdrojový kód 2 Překlad (compile) Zdrojový kód 3
6
Skripty pro překlady Nyní se podívejme, jak by se překládal a linkoval program, který má více zdrojových souborů. Řekněme, že struktura zdrojáků je následující: /inc/main.h main.o /src/main.cpp binary /inc/module.h module.o /src/module.cpp překlad linkování Překlad i linkování se zařídí příkazem g++ následovně g++ -c /src/module.c g++ -c /src/main.c g++ -o binary main.o module.o Vidíme, že složitost příkazů narůstá. Pokud by zdrojáků bylo dvacet, byl by překlad již velmi komplikovaná záležitost.
7
Skripty pro překlady - Makefile
Práci nám usnadní program make. Předpokládejme strukturu /inc/main.h main.o binary /inc/source1.h /src/main.cpp /src/source1.cpp Skript pro make se jmenuje Makefile. Popisuje, které soubory se mají jak překládat a říká programu make, co má přeložit, pokud se některý soubor změní. Například byl-li změněn od posledního překla-du pouze soubor source2.h, program make provede pouze příkaz g++ na šestém řádku. Příkaz make lze zavolat přímo se jménem modulu, např. make source1 a pak se provede pouze příslušná část skriptu. Pozn.: před příkazy g++ (případně jinými) v jednotlivých oddílech skriptu musí být znak „tabelátor“. source1.o /inc/source2.h /src/source2.cpp source2.o binary: main.o source1.o source2.o g++ $(CFLAGS) -o binary main.o source1.o source2.o main: ./inc/main.h ./src/main.cpp g++ $(CFLAGS) -c ./src/main.cpp source1: ./inc/source1.h ./src/source1.cpp g++ $(CFLAGS) -c ./src/source1.cpp source2: ./inc/source2.h ./src/source2.cpp g++ $(CFLAGS) -c ./src/source2.cpp clean: rm –f *.o *.d
8
Skripty pro překlady - Odkazy
Příklad složitějšího skriptu Makefile: Podrobější informace o Makefile na webu (EN): Alternativní nástroje k Makefile CMake SCons
9
Základní syntax 1) 2) 3) 4) 5) 6) 7)
1) Direktiva preprocesoru (začíná znakem #). Zde je vnořen soubor stdio.h 2) Deklarace funkce. Speciálně funkce main je vstupní bod programu. 3) Blok příkazů (např. tělo funkce). Začíná znakem { a končí znakem } . 4) Deklarace proměnné. Zde je deklarována proměnná count, která má celočíselný typ int. 5) Cyklus typu for. Proběhne 500x . 6) Příkaz vykonaný v každém průběhu cyklu. 7) Konec funkce, návratová hodnota je 0.
10
Komentáře Komentáře dovolují programátorovi vpisovat do zdrojového kódu poznámky, díky nimž se kód stane přehlednějším. Lze do nich také vpisovat například „TO DO“ poznámky nebo dočasně vyřadit část kódu pro potřeby testování. #include <stdio.h> int main( void ) { // Funkce main je vstupní bod programu int count; // Deklarace řídící proměnné cyklu for( count = 1; count <= 500; count++ ) printf( “I will not throw paper airplanes in class. \n“ ); // Cykl proběhne 500x a vypíše zprávu return 0; // Konec programu, návratová hodnota je 0 } /*main*/ // …….. komentář až do konce řádku /* */ … cokoliv mezi těmito dvojznaky je komentář Dobrá rada : Vždy pište komentáře i do sebejednodušších programů v poměru 1 řádek kódu ku 1 řádku komentáře. Umožní to orientovat se v programu komukoliv, kdo ho dostane po vás, nebo i vám samotným po delší době.
11
Typy a deklarace proměnných
Popis Velikost Rozsah char Znak nebo velmi malé celé číslo 1 byte signed : -128 až unsigned : 0 to 255 short int short Malé celé číslo 2 byty signed : až unsigned : 0 to 65535 int long int Celé číslo 4 byty signed : až unsigned : 0 až long long int long long Velké celé číslo 8 bytů signed : -moc až + moc unsigned : 0 až SPOUSTA bool Booleaovská hodnota. Může být buď true nebo false. true nebo false float Reálné číslo s jednoduchou přesností +/- 3.4e +/- 38 (~7 číslic) double long double Reálné číslo s dvojitou +/- 1.7e +/- 308 (~15 číslic) wchar_t Rozšířený znak 2 or 4 byty 1 rozšířený znak int cislo; // Deklarace celočíselné proměnné. Její hodnota je náhodná. int cislo = 1 ; // Deklarace celočíselné proměnné. Její hodnota je jedna. float pi = ; // Deklarace reálné proměnné, její hodnota je double hodnota = 2.56e-5; // Deklarace reálné proměnné, její hodnota je
12
Lexikální rozsah platnosti
Deklarace proměnné je platná pouze ve svém bloku kódu (a všech vnořených) #include <stdio.h> int main( void ) { int pocet = 50; for( int count = 1; count <= pocet; count++ ) int druhaMocnina; int dalsiCislo; druhaMocnina = count * count; printf( “Druha mocnina %i je %i \n“, count, druhaMocnina ); dalsiCislo = count * pocet; printf( “%i * %i = %i \n“, count, pocet, dalsiCislo ); } /*for*/ pocet = 100; dalsiCislo = 20; } /*main*/ Proměnná pocet je viditelná v celém bloku funkce main (a tedy i uvnitř for cyklu). Proměnná count je viditelná pouze v bloku for cyklu. Proměnné druhaMocnina a dalsiCislo jsou viditelné pouze v bloku for cyklu. Jejich použití za tímto blokem vyvolá chybu při překladu. Chyba!
13
Lexikální rozsah platnosti
Deklarace proměnné je platná pouze ve svém bloku kódu (a všech vnořených). Pokud je ve vnořeném bloku deklarována další proměnná téhož jména, je výše deklarovaná proměnná dočasně překryta. Přístupná bude po opuštění vnořeného bloku. #include <stdio> int main( void ) { int dalsiCislo = 500; printf( “Cyklus brzy zacne, dalsiCislo = %i \n“, dalsiCislo ); for( int count = 1; count <= 5; count++ ) int dalsiCislo = count + 1; printf( “count = %i, dalsiCislo = %i \n“, count, dalsiCislo ); } /*for*/ printf( “Cyklus probehl. dalsiCislo = %i \n“, dalsiCislo ); } /*main*/ Cyklus brzy zacne, dalsiCislo = 500 count = 1, dalsiCislo = 2 count = 2, dalsiCislo = 3 count = 3, dalsiCislo = 4 count = 4, dalsiCislo = 5 count = 5, dalsiCislo = 6 Cyklus probehl, dalsiCislo = 500 Dobrá rada : Jména proměnných držte unikátní v celém logickém celku (tělo jedné procedury, modul). Překrytí jmen proměnných jsou zbytečná a vedou k častým chybám.
14
Lexikální rozsah platnosti
Proměnné je možné deklarovat i na globální úrovni, tj. kdekoliv ve zdrojovém kódu mimo těla funkcí (vč. funkce main). Ty jsou pak viditelné kdekoliv od své deklarace. Dobrá rada: Deklaraci globálních proměnných je třeba si dobře rozmyslet speciálně v případech, kdy se program skládá z více souborů se zdrojovým kódem! zdroják1.c int jakesiCislo; zdroják2.c float jakesiCislo; PROBLÉM
15
Konstanty Deklarace proměnné uvozená klíčovým slovem const se považuje za hodnotu určenou pouze ke čtení. Jakýkoliv pokus o zápis do takto deklarované proměnné vyústí v chybu při překladu. Konstanty je možné definovat i pomocí direktivy preprocesoru define. Před vlastním překladem preprocesor fyzicky nahradí každý výskyt jména konstanty udanou hodnotou. O direktivách preprocesoru bude pojednáno podrobně později. Zápis číselných konstant je dovolen v desítkové, osmičkové a šestnáctkové soustavě. Osmičkové číslo začína nulou, šestnáctkové dvojznakem 0x. Reálná čísla mohou být zapsána v exponenciální formě (se základem deset). const int pathWidth = 100; const char tabulator = '\t' ; const double pi = ; #define pathWidth 100 #define tabulator '\t' #define pi // Dekadicky // Osmičkově 0x4B // Šestnáctkově 1.5e-12 // Reálné číslo
16
Konstanty 'z‚ 'p' "Hello world" "How do you do?" Pro zápis znakových a řetězcových konstant se používají jednoduché a dvojité uvozovky. Znakové i řetězcové konstanty mohou obsahovat speciální znaky (znak se zpětným lomítkem, viz tabulka vlevo dole). Pro práci s řetězcovými konstantami platí následující pravidla : Dlouhý řetězec lze na více řádek rozdělit pomocí zpětného lomítka. \n Nový řádek \r Návrat kurzoru na začátek řádky \t Tabulátor \v Vertikální tabulátor \b Backspace \f Posun na novou stránku (form/page feed) \a Pípnutí \' Jednoduchá uvozovka (') \" Dvojitá uvozovka (") \? Otazník (?) \\ Zpětné lomítko (\) " Retezec, který je dlouhy a nevejde se \ na jednu radku" Lze spojit za sebou několik řetězcových konstant "toto tvori" "jeden cely" "retezec" "znaku" Řetězec rozšířených znaků je označen znakem L. L"Toto je řetězec rozšířených znaků : α, β, €, £ …" Pozor, tato pravidla neplatí pro práci s řetězcovými proměnnými (viz později)!
17
Složené typy – statická pole
Pole je vektor (soubor, souhrn, posloupnost - jak chcete) proměnných stejného typu. Tyto proměnné ovšem nejsou samostatně pojmenované, pouze oindexované a jsou přístupné přes jméno celého pole a příslušný index. Statické pole (tedy takové, u nějž je počet prvků znám již při překladu), se deklaruje takto: typ jméno [n] kde n je počet prvků. Schéma příslušné struktury lze zobrazit například takto: 0. prvek 1. prvek 2. prvek 3. prvek 4. prvek ... (n-2). prvek (n-1). prvek Pole má n prvků indexovaných od nuly. int poleCisel [10]; int jinePoleCisel [5] = { 1, 2, 3, 4, 5 } ; float realnePole [4] = { 0.1, 1.2, 2.3, 3.4 } ; float spatne [3] = { 0.1, 0.2, 0.3, 0.4, 0.5 } ; Příklady na deklaraci statických polí. prvky pole lze již při deklaraci naplnit specifikova-nými hodnotami pomocí složených závorek. Poslední z příkladů vyvolá chybu při překladu. poleCisel [3] = 12 ; jinePoleCisel [0] = poleCisel[3] + poleCisel[4] ; realnePole[4] = 1.5 ; realnePole[ poleCisel[3] ] = 10; Příklady na přístup do statických polí. Předposlední z příkladů vyvolá (pravděpodobně) chybu při překladu, poslední pak buď chybu za běhu nebo způsobí zcela iracionální a náhodné chování programu.
18
enum jméno typu { hodnota1, hodnota2, hodnota3, . . } jméno proměnné;
Složené typy – Výčty Výčty jsou ordinární (celočíselné) typy, které mohou ale obsahovat pouze vybrané hodnoty. Tyto hodnoty jsou nějak pojmenované. enum jméno typu { hodnota1, hodnota2, hodnota3, . . } jméno proměnné; enum mesic_typ { leden, unor, brezen, duben, kveten, cerven, cervenec, srpen, zari, rijen, listopad, prosinec } mesic; mesic = leden; mesic = unor; if( mesic == brezen ) JaroJeTady(); enum barvy_typ { bila = 1, modra = 2, cervena = 4, zelena = 8, cerna = 16 } barva; Například pro jména měsíců by bylo možné deklarovat výčet mesic_typ, který obsahuje jména měsíců. S těmito jmény pak lze pracovat jako s hodnotami proměnné. Pozn.: mesic je ve skutečnosti celočíselná proměnná a jména měsíců jsou zástupná jména pro celá čísla. Defaultově je první položka výčtu nula, lze to ovšem změnit přiřazením. Barvy ve druhém příkladu mají jako hodnoty přiřazené mocniny dvou a je tedy možné je použít jako flags (viz binární operátory).
19
Složené typy - Struktury
Datová struktura je skupina proměnných shromážděná pod jedním jménem. Syntax její deklarace vypadá následovně: struct jméno typu { typ člen 1; typ člen 2; typ člen 3; ... } jméno proměnné; Typy jednotlivých členů můžou být libovolné základní typy nebo další složené (struktura, výčet, pole). K jednotlivým členům proměnné typu struktura se přistupuje pomocí operátoru "." (tečka): struct complex { float Re; float Im; } A, B, C; A.Re = 10.1; A.Im = -3; V příkladu nalevo jsou vytvořeny tři proměnné A, B a C jako struktury představující komplexní číslo (obsahují dvě reálná čísla, jedno pro reálnou a druhé pro imaginární část). Do proměnné A je poté uložena hodnota i .
20
Složené typy - Struktury
Se strukturou lze nakládat stejně jako se základními typy, například vytvářet pole: Zadejte nazev : Blade_Runner Zadejte rok : 1982 Zadejte Nazev : Matrix Zadejte rok : 1999 Zadejte nazev : Taxi_Driver Zadejte rok : 1976 Zadal jste nasledujici tri filmy : Blade_Runner (1982) Matrix (1999) Taxi_Driver (1976) #include <stdio.h> struct film_typ { char Nazev[100]; int Rok; } filmy [10]; int main ( void ) for ( int n = 0; n < 3; n++ ) printf( "Zadejte nazev : " ); scanf( "%s", filmy[n].Nazev ); printf( "Zadejte rok: " ); scanf( "%u", & filmy[n].Rok ); } printf( "\nZadal jste nasledujici tri filmy : \n" ); for ( int n = 0; n < 3; n++ ) printf( "%s (%i) \n", filmy[n].Nazev, filmy[n].Rok );
21
Přetypování Převod výrazu či proměnné jednoho typu na typ jiný se nazývá přetypování (type-casting). U některých typů provádí překladač přetypování sám - tzv. implicitní přetypování : short a = 1024; // Proměnná typu short má délku 2 byte int b ; // Proměnná typu int má délku 4 byte b = a; // Pokud přiřazujeme do proměnné typu int proměnnou typu short, dva byty proměné // short se zkopírují a dva zbývající byty proměnné int vynulují. V některých případech je ale překladači nutné říct, co přesně má na co přetypovat. Zejména je to třeba při práci s ukazateli, ale i některé základní typy to vyžadují. Přetypování lze v zásadě zapsat dvěma způsoby: int a = 1024; float b; b = (float) a; // Starší způsob vyhovující normě ANSI-C b = float (a); // Přetypování na způsob volání funkce a = (int) b; a = int (b); Převod celého čísla na reálné je relativně komplikovaná záležitost, protože organizace jednotlivých bitů ve float a int jsou zcela odlišné, ačkoliv celková délka je v obou případech stejná (4 byte).
22
Přiřazení není matematická rovnost!
Operátory Máme-li deklarované proměnné, chceme s nimi také nějak operovat. Pro tento účel C integruje tzv. operátory. Na rozdíl od mnoha jiných jazyků nejsou operátory pojmenovány slovy, ale symboly, které se skládají nikoliv z písmen, ale jiných, na klávesnici běžně dostupných znaků. Operátorů je poměrně velký počet. Přiřazovací operátor (=) Příkaz vlevo uloží do proměnné a hodnotu 5 v prvním případě, respektive obsah proměnné b v druhém. Výraz nalevo od rovnítka se označuje jako lvalue (left value), výraz napravo jako rvalue (right value). Lvalue musí vždy být proměnná, zatímco rvalue může být jakákoliv kombinace proměnných, konstant či výsledů jiných operací. Přiřazování se vždy děje zprava doleva – hodnota proměnné vlevo se změní až po vyhodnocení výrazu napravo. Předchozí hodnota je v tomtéž okamžiku zapomenuta. a = 5; a = b; int a, b; // Deklarace, hodnoty a a b jsou nyní neurčené a = 5; // Do a je uložena hodnota 5 b = 4; // Do b je uložena hodnota 4 a = b; // Do a je uložena hodnota b, tedy 4 b = 15; // Do b je uloženo 15, v a je stále 4 (předchozí rovnítko již // nehraje žádnou roli Přiřazení není matematická rovnost!
23
Operátory Aritmetické operátory ( +, -, *, /, % )
Aritmetické operátory fungují tak, jak od nich čekáme (součet, rozdíl, násobek, podíl). Podle typu operandů je operátor dělení celočíselný nebo reálný. Operátor % (modulo) je definován pouze pro celá čísla a celočíselné proměnné a vrací zbytek po dělení : int a, b; // Deklarace, hodnoty a a b jsou nyní neurčené a = 5 + 3; // Do a je uložena hodnota 8 b = 5 / 3; // Do b je uložena hodnota 1 a = 6 / 3; // Do a je uložena hodnota 2 b = 7 % 3; // Do a je uložena hodnota 1 float x = 6.2 / 12.8; // Do x je uložena hodnota x = a - b; // Do x je uložena hodnota a – b = 2 – 1 = 1. Zde dochází k automatickému // přetypování z celočíselného typu na reálný. Pozn. : Lvalue jednoho operátoru může být rvalue (nebo částí rvalue) jiného operátoru. Jsou tedy dovoleny konstrukce typu : a = 5 + (b = 3); a = b = c = d = 6; Podobné konstrukce mohou být užitečné třeba v cyklech, obecně je ale lepší se jim vyhnout, neboť znepřehledňují program.
24
Operátory Bitové operátory ( ^, &, |, ~ ) bitové
Bitové operátory provádějí logické operace s jednotlivými bity operandů (celočíselných). Označíme-li 0 za false a 1 za true, pak: bitové exkluzivní OR respektive XOR (nonekvivalence) 0 & 0 = 0 0 & 1 = 0 1 & 0 = 0 1 & 1 = 1 0 | 0 = 0 0 | 1 = 1 1 | 0 = 1 1 | 1 = 1 0 ^ 0 = 0 0 ^ 1 = 1 1 ^ 0 = 1 1 ^ 1 = 0 bitové AND (konjunkce) bitové OR (disjunkce) Operace jsou provedeny s každý bitem operandů zvlášť, tedy například: Bitová konjunkce a & b 200 217 Bitová disjunkce a | b 200 217 Bitová nonekvivalence a ^ b 200 217 17 Bitová negace (doplněk) pak převrátí smysl všech bitů, tj: Bitová negace ~a 200 55
25
Obecně pro n-tý dektektor nahradíme konstantu 4 konstantou 2n-1.
Operátory Bitové operátory ( ^, &, |, ~ ) Bitové operátory ve spojení s celočíselnými proměnnými mohou tvořit účinnou a úspornou metodu ukládání skupin binárních stavů (flags). Máme-li například v experimentu částicové fyziky řadu subdetektorů (řekněmě 32) u nichž je pro každý event nutné uložit stav (zanznamenal/nezaznamenal průchod částice). Pokud bychom použili 32 proměnných typu bool, tato data by pro každý event zabrala 32 byte a pro milion událostí 32 MB, což je hodně. Přitom stav včech detektorů lze úsporně uložit do 4 byte, pokud každý bit celočíselné proměnné typu int přiřadíme právě jednomu detektoru : detektory = detektory | 4 ; // Přiřazení stavu 3. detektoru „ZÁSAH“ detektory = detektory & (~4) ; // Přiřazení stavu 3. detektoru „NIC“ stav = detektory & 4 ; // Zjištění stavu 3. detektoru 0. bit 1. bit 2. bit 3. bit 4. bit … 30. bit 31. bit 1 … ZÁSAH NIC 1 … 1 … OR AND 1 … NOT 1 … = = 1 … 1 … Obecně pro n-tý dektektor nahradíme konstantu 4 konstantou 2n-1. int detektory;
26
Operátory Relační operátory Logické operátory ( &&, ||, ! )
Relační operátory slouží pro porovnávání číselných hodnot. Bez rozsáhlejších úprav (přetěžování) je nelze aplikovat na žádné jiné. Jejich výsledek je buď 0 (false) nebo 1 (true) : a == b // a je rovno b a != b // a není rovno b a < b // a je menší než b a <= b // a je menší nebo rovno b a > b // a je větší než b a >= b // a je větší nebo rovno b Logické operátory ( &&, ||, ! ) Logické operátory slouží pro spojování podmínek (logických výrazů) do větších celků. Operandy mohou být proměnné typu bool nebo celá čísla, kde pak 0 je považována za false a cokoliv jiného za true. Výsledek je booleanovská hodnota. Nepleťe si je s bitovými operátory! P && Q // Vrací true jsou-li P i Q pravdivé P || Q // Vrací true je-li alespoň jeden z P a Q pravdivý ! P // Vrací true je-li P nepravdivý
27
Operátory Inkrementace a dekrementace ( ++, -- )
Operátor ++ zvýší hodnotu operandu o jedna, -- ji o jedna sníží. Existují v prefixové a postfixové variantě. Efekty jsou následující (předpokládejme, že hodnota b před operací je 10) : b // Zvětší obsah b o jedna, tj. potom b je 11 a = b // a je rovno 10, b je rovno 11 a = ++b // a je rovno 11, b je rovno 11 b // Zmenší obsah b o jedna, tj potom b je 9 a = b // a je rovno 10, b je rovno 9 a = --b // a je rovno 9, b je rovno 9 Bitové posuny ( <<, >> ) Operátor posune bity celočíselné proměnné vlevo či vpravo o udaný počet míst a zbytek doplní nulami. Tedy například 0. 1. 2. 3. 4. 5. 6. 7. 1 char a = 64; char b = a << 3; 64 8 0. 1. 2. 3. 4. 5. 6. 7. 1 char a = 133; char b = a >> 2; 133 20
28
if (podmínka) příkaz 1; else příkaz 2 ;
Podmínky Podmínky slouží pro rozhodování a větvení programu jejich syntax je následující: if (podmínka) příkaz 1; else příkaz 2 ; Část else je nepovinná. Tedy například if ( a >= 0 ) printf( “Cislo a je nezaporne“ ); else printf( “Cislo a je zaporne“ ); Jako podmínka může být libovolná booleanovská hodnota (nebo i celočíselná, kde 0 je považována za false, cokoliv jiného za true ). Na místě příkazu 1 a 2 nemusí být jen jediný příkaz, ale také příkazový blok : if ( a >= 0 ) { printf( “Cislo a je nezaporne \n“ ); printf( “Absolutni hodnota cisla a je %i \n“, a ); } else printf( “Cislo a je zaporne“ ); printf( “Absolutni hodnota cisla a je %i \n“, - a ); POZOR! Záměna operátoru == za operátor = v podmínce je zdrojem velmi častých a těžko objevitelných chyb: int a = 0; if( a = 2 ) printf( “a je dvojka“ ); vytiskne hlášení „a je dvojka“ i přes to, že v a očekáváme hodnotu 0! CVIČĚNÍ : výpočet kvadratické rovnice
29
for ( inicializace; test; akce ) příkaz;
for cyklus For cyklus slouží pro opakované provádění příkazu nebo bloku příkazů. for ( inicializace; test; akce ) příkaz; V částech inicializace, a akce může být teoreticky jakýkoliv platný příkaz nebo výraz, v části podmínka pak booleanovský výraz. Je-li tento výraz platný, cykl pokračuje, není-li cykl končí. Příkaz inicializace se provede jednou na začátku cyklu, příkaz akce se pak provede pokaždé na konci každého průběhu. Je obvyklé, že jednotlivé části se používají následovně: Inicializace …. deklarace řídící proměnné cyklu (celočíselná hodnota) Podmínka …… porovnání řídící proměnné s nějakou konstantou či předem známou hodnotou Akce …………. inkrementace či dekrementace řídící proměnné for ( int loop = 0; loop < 10; loop++ ) printf( “Ahoj! “ ); // Vytiskne 10x řetězec „Ahoj“ for ( int loop = 0; loop < 10; loop++ ) { // Vytiskne čísla od nuly do devíti a jejich druhé mocniny printf( “loop = %i \n“, loop ); printf( “loop ^2 = %i \n“, loop * loop ); } /*for( loop )*/
30
while( podmínka ) příkaz;
while cyklus While cyklus slouží pro opakované provádění příkazu nebo bloku příkazů. while( podmínka ) příkaz; Slouží zejména v případě, kdy není zcela jasné, kolik průběhů bude cykl mít. Příkaz (nebo blok příkazů) se vykonává tak dlouho, dokud je podmínka platná. Může být použito například při načítání řádek ze souboru. Shcematicky : while ( ! soubor.JeNaKonci() ) { soubor.NactiData(); ZpracujData(); } Cyklus bude tak dlouho načítat a zpracovávat data, dokud nenarazí na konec souboru. Pak skončí. Funkční příklad zde: int count; // Deklarace promene poctu opakovani printf( “Zadejte počet opakovani : “ ); scanf( “%i“, &count ); // Počet zada uzivatel z klavesnice while ( count > 0 ) // Cykl probiha dokud je počet kladny { printf( “%i, “ ); // Tiskne se hlaseni o zbyvajicim poctu count--; // Počet se snizuje o jedna } printf( “ZAZEH!“ ); // Hlaseni o konci cyklu Pozn. : zadá-li uživatel nulu nebo záporné číslo, cykl neproběhne ani jednou!
31
do příkaz; while( podmínka ) ;
do-while cyklus Do-while cyklus slouží pro opakované provádění příkazu nebo bloku příkazů. do příkaz; while( podmínka ) ; Od samotného cyklu while se liší pouze v jediném – testovací podmínka je až na konci, takže tělo cyklu se minimálně jednou provede. char z = 'a'; do { printf( “%c“, z ); z++; } while ( z <= 'z‚ ); // Vytiskne pismena od a do z . Pozn. : všimněte si, že znaková konstanta se dá použít na místě číselné hodnoty. V takovém případě překladač nahradí znak jeho ASCII hodnotou.
32
Příkazy break a continue
V souvislosti s cykly jsou zavedeny dva příkazy, a to break a continue. První z nich okamžitě ukončí provádění cyklu, druhý z nich ukončí provádění průběhu. Jejich použití a rozdíly jsou zřejmé z následujících příkladů: for ( int loop = 0; loop < 20; loop++ ) { // Vytiskne lichá čísla mezi nulou a dvacítkou if( (loop % 2) == 0 ) continue; printf( “%i, “, loop ); } /*for( loop )*/ int loop = 0; while( true ) { // Vytiskne lichá čísla mezi nulou a dvacítkou if( loop == 20 ) break; if( (loop % 2) == 0 ) continue; printf( “%i, “, loop ); loop++; } /*while*/ V obou případech bude stejný výstup: 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, CVIČĚNÍ : výpočet faktoriálu resp. součet řady
33
Výběrový příkaz switch-case
Příkaz switch umožňuje větvení podle více hodnot celočíselných výrazů nebo výčtů. Výraz se vyhodnotí a jestliže odpovídá některé hodnotě case, skočí vykonávání na řádek, kde se case nachází. Jestliže neodpovídá ani jedna hodnota skočí na příkaz default (pokud je definován). Příaz(y) se vykonávají dokud nenarazí na break, po němž se přesune chod programu za složenou závorku. switch (výraz) { case konstanta 1: skupina příkazů 1; break; case konstanta 2 : skupina příkazů 2; . . . default: def. skupina příkazů } switch ( akce ) { case 1: printf( “Provadim akci 1 … \n“ ); break; case 2 : printf( “Provadim akci 2 … \n“ ); break; case 3 : printf( “Provadim akci 3 … \n“ ); break; default: printf( “Tuto akci neznam! \n“ ); break; } Zapomenete-li v nějaké sekci case napsat na konci break, provádění programu „proleze“ do další sekce. To lze v mnoha případech udělat i záměrně.
34
Základy vstupu a výstupu
int printf( const char *format, ... ) Funkce printf , implementována v knihovně stdio, zavádí standardní metodu pro formátovaný výstup na stdout. Může mít libovolný počet argumentů, první z nich je řetězec udávající formátování. Na tomto řetězci konkrétně záleží, kolik argumentů jakého typu musí funkce mít. char *jmeno = "Bobiku"; int vek = 21; printf( "Ahoj %s, jsi %d let stary (a bude hur).\n", jmeno, vek ); Ahoj Bobiku, jsi 21 let stary (a bude hur). Ve formátovacím řetězci jsou jednak normální znaky, jednak speciální sekvence, které jsou před tiskem nahrazeny obsahem proměnných. Pokud nesouhlasí počet a typ sekvencí s počtem a typem proměnných, výstup je nedefinovaný. Funkce vrátí celkový počet vytištěných znaků (nebo záporné číslo v případě chyby).
35
Základy vstupu a výstupu
Kód Příslušná proměnná %c Znak %d Celočíselná proměnná se znaménkem %i %e Exponenciální zápis reálného čísla s malým „e“ %E Exponenciální zápis reálného čísla s velkým „E“ %f Reálné číslo %g Automatický výběr variant %e nebo %f (co je kratší) %G Automatický výběr variant %E nebo %f (co je kratší) %o Celé číslo v osmičkovém zápisu %s Řetězec %u Celé číslo bez znaménka %x Celé číslo bez znaménka v šestnáctkovém zápisu, malé znaky „a“ až „f“ %X Celé číslo bez znaménka v šestnáctkovém zápisu, velké znaky „A“ až „F“ %p Ukazatel Tabulka shrnuje nejčastěji používané formátovací znaky. Mezi procento a písmeno lze vložit číslo, které udává šířku výstupu : int vek = 21; printf( "Je mi %10u let.\n", vek ); printf( "Je mi %010u let.\n", vek ); Je mi let. Je mi let.
36
Základy vstupu a výstupu
U reálných čísel lze přidat počet platných desetinných míst takto : int pi = ; printf( "pi = %f \n", pi ); printf( "pi = %8f \n", pi ); printf( "pi = %8.3f \n", pi ); pi = pi = pi = Počet znaků lze zadat záporný, pak se zarovnává doleva : char *jmeno = „Bobik"; printf( "Dnesni menu : %10s !\n", jmeno ); printf( "Dnesni menu : %-10s !\n", jmeno ); Dnesni menu : Bobik ! Dnesni menu : Bobik ! Počet mezer lze zadat i z programu pomocí znaku "*" : -> 1 -> 2 -> for( int i = 1; i < 4; i++ ) printf( "->%*i \n", 2*i, i );
37
Základy vstupu a výstupu
int scan( const char *format, ... ) Pro načítání hodnot je definována funkce scanf. Parametry má obdobné jako printf - řídící řetězec určuje konkrétní počet a typ proměnných, do kterých se bude načítat. Pokud řídící řetězec obsahuje cokoliv jiného než sekvence začínající "%", musí se tyto znaky objevit na vstupu (což je pro uživatele matoucí a není to doporučeno). int i; float f; double d; printf( "Zadejte cele cislo: " ); scanf( "%d", &i ); printf( "Zadejte realne cislo: " ); scanf( "%f", &f ); printf( "Zadejte realne cislo s dvojitou presnosti: " ); scanf( "%lf", &d ); printf( "Zadal jste %d, %f, and %f\n", i, f, d ); Povšimněte si sekvence "%lf", která načítá reálné číslo s dvojitou přesností a také znaku "&" před jmény proměnných ve scanf. To jsou dereference, tj. funkci se předává nikoliv samotná proměnná, ale ukazatel na ni (o ukazatelích později) - parametr je předán odkazem. CVIČĚNÍ : výpočet různých řad závislých na parametru dle výběru uživatele
38
Základy vstupu a výstupu
int fprintf( FILE *output, const char *format, ... ) int fscanf( FILE *input, const char *format, ... ) Funkce printf a scanf operovaly se standardním výstupem a vstupem (stdout, stdin). Existují ale varianty těchto funkcí, které se chovají zcela shodně, výstup resp. vstup mají ale přesměrovaný na libovolný soubor. Jsou to funkce fprintf a fscanf. Aby bylo možné pracovat se souborem na disku, je třeba jej nejprve otevřít. K tomu slouží funkce fopen. FILE * fopen( const char *fname, const char *mode ); Fopen má dva argumenty. První z nich popisuje cestu k souboru na disku, druhý z nich pak režim, ve kterém bude soubor otevřen. Mód Význam Existuje Neexistuje “r” Otevře pro čtení Čte od začátku Chyba “w” Vytvoří pro zápis Přepíše obsah Vytvoří nový “a” Připojí pro zápis Píše na konec “r+“ Otevře pro čtení i zápis Čte/píše od začátku “w+“ Vytvoří pro čtení i zápis “a+“ Připojí pro čtení i zápis POZOR! Pro MS Windows jsou v cestách k souborům zpětná lomítka a je tedy třeba je zadávat dvojitě: "C:\\Tmp\\soubor.txt"
39
Základy vstupu a výstupu
int fprintf( FILE *output, const char *format, ... ) int fscanf( FILE *input, const char *format, ... ) Funkce printf a scanf operovaly se standardním výstupem a vstupem (stdout, stdin ). Existují ale varianty těchto funkcí, které se chovají zcela shodně, výstup resp. vstup mají ale přesměrovaný na libovolný soubor. Jsou to funkce fprintf a fscanf. Aby bylo možné pracovat se souborem na disku, je třeba jej nejprve otevřít. K tomu slouží funkce fopen. FILE * fopen( const char *fname, const char *mode ); FILE je struktura definovaná v knihovně stdio popisující otevřený soubor. O její obsah se běžný programátor nemusí starat, pouze ukazatel na ni používá jako typ proměnné, která "obsahuje" soubor a předává ji jako argument všem příslušným funkcím. O ukazatelích později. Po ukončení práce se souborem je třeba jej zavřít : fclose( FILE * fileDescriptor ); Bez zavolání tohoto příkazu se neuloží obsah bufferů a navíc bude soubor nadále zablokovaný pro jakýkoliv další proces!
40
Základy vstupu a výstupu
#include <stdio.h> int main( void ) { char *polePrijmeni[] = { "Novak", "Jirousek", "Chudy", "Posedly" } ; char *poleJmen[] = { "Josef", "Jan", "Petr", "Maniak" } ; int poleVeku[] = { 27, 35, 16, 1024 } ; // Defininice tri statickych poli s informacemi FILE *vystup = fopen( "seznam.txt", "w" ); // Vytvari se novy soubor pro vystup (pokud uz existuje, premaze se) fprintf( vystup, "Jmeno Prijmeni Vek\n" ); fprintf( vystup, "~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" ); // Tiskne se hlavicka tabulky for( int i = 0; i < 4; i++ ) fprintf( vystup, "%10s %10s %5i \n", poleJmen[i], polePrijmeni[i], poleVeku[i] ); // Tisknou se radky tabulky fclose( vystup ); // Vystupni soubor je zavren } /*main*/
41
Základy vstupu a výstupu
#include <stdio.h> int main( void ) { char jmeno[100], prijmeni[100]; int vek; FILE *vstup = fopen( "seznam.txt", "r" ); // Otvira se soubor pro vstup printf( "Jmeno Prijmeni Vek\n" ); printf( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" ); // Tiskne se hlavicka tabulky na obrazovku fscanf( vstup, "Jmeno Prijmeni Vek\n" ); fscanf (vstup, "~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" ); // Preskoci hlavicku v souboru while( ! feof( vstup ) ) { // Nacte se obsah souboru a vytiskne jako tabulka na obrazovku fscanf( vstup, "%s%s%i\n", jmeno, prijmeni, &vek ); printf( "%10s %10s %5i \n", jmeno, prijmeni, vek ); } /*while*/ fclose( vstup ); // Zavira se vstupni soubor } /*main*/ CVIČĚNÍ : zápis a načítání různých seznamů z/do souborů
42
typ jméno( parametr1, parametr2, ... ) { příkazy }
Funkce Funkce je část kódu, která se používá často a opakovaně na více místech. Bylo by nesmírně "neekonomické" opisovat kód znovu a znovu tam, kde je jej třeba. Proto je možné tuto část kódu zapouzdřit a nějak jej pojmenovat a ve zbytku programu už jen tímto jménem "volat". Obecná definice funkce vypadá následovně : typ jméno( parametr1, parametr2, ... ) { příkazy } #include <stdio.h> int soucet( int a, int b ) { int r; r = a + b; return r; } /*soucet*/ void main ( void ) int z; z = soucet( 5, 3 ); z = soucet( z, 2 ); printf( " = %i\n", z ); } /*main*/ typ typ návratové hodnoty jméno jméno funkce parametr# hodnoty, které se do funkce předávají příkazy samotný blok kódu Klíčové slovo return ukončuje provádění funkce a vrací řízení provádění programu těsně za volání funkce. Funkce nemusí vracet řádnou hodnotu, pak se na místo typu použije void a po klíčovém slovu return nebude uvedena žádná hodnota či proměnná. Stejně tak není nutné, aby měla nějaké vstupní parametry (rovněž se použije void).
43
Funkce – argumenty předávané hodnotou
Argumenty funkce jsou v jejím těle přístupné jako deklarované proměnné. Předávání může probíhat dvěma způsoby : hodnotou a odkazem. #include <stdio.h> int pracujSCisly( int a, int b ) { a++; b--; printf( "pracujSCisly : a = %i, b = %i\n", a, b ); return a + b; } /*pracujSCisly*/ void main ( void ) int arg1 = 10; int arg2 = 15; printf( "arg1 = %i, arg2 = %i\n", arg1, arg2 ); int z = pracujSCisly( arg1, arg2 ); printf( "arg1 = %i, arg2 = %i, z = %i\n", arg1, arg2, z ); } /*main*/ Argument předaný hodnotou se v těle funkce chová jako nezávisle deklarovaná proměnná s hodnotou přiřazenou při deklaraci. Jakákoliv práce s ním neovlivní hodnoty proměnných v nadřazeném bloku, odkud byla funkce zavolána. arg1 = 10, arg2 = 15 pracujSCisly: a = 11, b = 14 arg1 = 10, arg2 = 15, z = 25
44
Funkce – argumenty předávané odkazem
Argumenty funkce jsou v jejím těle přístupné jako deklarované proměnné. Předávání může probíhat dvěma způsoby : hodnotou a odkazem. #include <stdio.h> int pracujSCisly( int & a, int & b ) { a++; b--; printf( "pracujSCisly : a = %i, b = %i\n", a, b ); return a + b; } /*pracujSCisly*/ void main ( void ) int arg1 = 10; int arg2 = 15; printf( "arg1 = %i, arg2 = %i\n", arg1, arg2 ); int z = pracujSCisly( arg1, arg2 ); printf( "arg1 = %i, arg2 = %i, z = %i\n", arg1, arg2, z ); } /*main*/ Argument předaný odkazem naopak zůstává neustále svázán s proměnnou v nadřazeném bloku (předává se vlastně ukazatel). Argument předaný hodnotou se v definici funkce označuje znakem "&" za typem argumentu. arg1 = 10, arg2 = 15 pracujSCisly: a = 11, b = 14 arg1 = 11, arg2 = 14, z = 25
45
Funkce – defaultové parametry
Pro pohodlí programátora je možné nastavit defaultové hodnoty argumentů. Ty se pak při volání funkce vůbec nemusejí uvádět: [ 10, 10 ] [ 5, 1 ] [ -1, 1 ] #include <stdio.h> void tiskniDvojiciCisel( int x = -1, int y = +1 ) { printf( "[ %i, %i ] ", x, y ); return; } /*tiskniDvojiciCisel*/ void main ( void ) tiskniDvojiciCisel( 10, 10 ); tiskniDvojiciCisel( 5 ); tiskniDvojiciCisel(); } /*main*/ Defaultová hodnota se do definice funkce přidá pomocí rovnítka (jako přiřazovací příkaz). Hodnoty se do parametrů vkládají při překladu. Z tohoto důvodu je nutné, aby všechny parametry s defaultovými hodnotami byly až za parametry bez nich, v opačném případě by překladač nevěděl, co má dělat. povoleno : void tiskniDvojiciCisel( int x = -1, int y = +1 ) {} povoleno : void tiskniDvojiciCisel( int x, int y = +1 ) {} zakázáno : void tiskniDvojiciCisel( int x = -1, int y ) {}
46
Funkce - přetěžování Je možné definovat několik funkcí které mají stejné jméno a liší se jen argumenty. To je dobré v okamžiku, kdy mají tyto funkce provádět stejné věci pro různé typy argumentů. Například : int secti( int x, int y ) { return x + y; } double secti( double x, int y ) { return x + (double)y; } double secti( double x, double y ) { return x + y; } void main ( void ) { int a = 1; int b = 2; double c = ; double d = 2.7; int resInt; double resDoub; resInt = secti( a, b ); resDoub = secti( a, c ); resDoub = secti( c, d ) } /*main*/ První volání secti zavolá první z definovaných funkcí, druhé druhou a třetí poslední definovanou funkci. Přitom se všechny jmenují stejně a ze jména je jasné, že provádí aritmetickou operaci sčítání. Která funkce se kdy přesně volá rozhoduje překladač na základě typů argumentů. Pozn.: nelze deklarovat stejně se jmenující funkce se stejnými argumenty, lišící se pouze typem navrácené hodnoty.
47
Funkce – rekurzivní volání
Funkce může volat sama sebe. To je velice mocný nástroj pro řešení některého typu úloh - tzv. rekurzivní volání. Jako příklad takové úlohy může být vyhledávání souboru určitého jména v adresářové struktuře. Základní algoritmus takového vyhledávání zobrazuje vývojový diagram vlevo. Předpokládáme, že jméno hledaného souboru je známé na všech úrovních volání funkce. Argument funkce Prohlédni je adresář, ve kterém se má s hledáním začít. Její návratová hodnota je buď plná cesta k souboru, nebo prázdný řetězec (" ") v případě, že soubor nebyl nalezen. Prohlédni( adresář ) Načti seznam souborů ANO Obsahuje seznam hledaný soubor ? Vrať adresář + soubor NE Načti seznam podadresářů Cykl přes podadresáře (podadr) Volej Z = Prohlédni (adresář + podadr ) NE Z je rovno " " Vrať Z ANO Vrať " "
48
Funkce – rekurzivní volání
Jako příklad funkčního kódu je zde výpočet faktoriálu (byť pro tuto úlohu je rekurze zbytečná): #include <stdio.h> int SpoctiFaktorial( int n ) { if( n <= 1 ) return 1; return n * SpoctiFaktorial( n - 1 ); } void main ( void ) int N; printf( "Zadejte cele cislo : " ); scanf( "%u", &N ); printf( "Faktorial cisla %u je %u. \n", N, SpoctiFaktorial( N ) ); Všimněte si volání funkce SpoctiFaktorial primo z funkce printf - zde je jako argument printf použita návratová hodnota funkce SpoctiFaktorial.
49
Složené typy – klíčové slovo typedef
Je-li v programu třeba deklarovat více proměnných stejného složeného typu na různých místech, je nepraktické vypisovat typ znovu a znovu. Proto v C existuje klíčové slovo typedef, které překladači říká, aby si daný typ zapamatoval pod určeným jménem. Toto jméno je pak možné použít kdykoliv na deklaraci proměnné. typedef struct complex { float Re; float Im; }; complex Secti( complex clen1, complex clen 2 ) complex soucet; soucet.Re = clen1.Re + clen2.Re; soucet.Im = clen1.Im + clen2.Im; return soucet; } void TiskniComplex ( complex cislo ) if( cislo.Im < 0.0 ) printf( "%f - %fi", cislo.Re, abs( cislo.Im ) ); else printf( "%f + %fi", cislo.Re, cislo.Im ); V příkladu je definován komplexní typ complex, který je pak v následujících funkcích použit jako typ argumentů a návratové hodnoty (o tom, jak se přesně volají funkce a o ostatních věcech použitých v příkladu později). complex A, B, C; A.Re = 10; A.Im = -3; B.Re = -5; B.Im = 5; C = Secti( A, B ); TiskniComplex( A); TiskniComplex( B ); TiskniComplex( C ); 10 - 3i -5 + 5i 5 + 2i CVIČĚNÍ : základní aritmetické operace se strukturou complex
50
ukazatelNaPI uchovává adresu v paměti, na které je uložena hodnota PI
Ukazatele V C existuje speciální třída proměnných, které mají za úkol uschovávat odkazy na jiné proměnné. Říká se jim ukazatele. Ukazatele uschovávají nikoliv nějakou hodnotu, nýbrž odkaz do paměti (adresu), na níž lze patřičnou hodnotu nalézt. Schematicky : 3.1415 PI AF35:010B ukazatelNaPI AF35:01BC Jméno proměnné Adresa proměnné Obsah proměnné PI uchovává hodnotu ukazatelNaPI uchovává adresu v paměti, na které je uložena hodnota PI Pomocí ukazatelů se v C předávají parametry odkazem, deklarují pole, alokuje paměť pro větší data, pracuje s řetězci a podobně. Některé jazyky tento mechanizmus programátorům neumožňují, protože je potenciálním zdrojem chyb a problémů, nicméně C se díky němu vyznačuje obrovskou flexibilitou. Ukazatel na proměnnou nějakého typu deklarujeme pomocí znaku * za typem: float PI; // Proměnná typu float float * ukazatelNaPI; // Proměnná typu ukazatel na float
51
Ukazatele - reference a dereference
Pro snažší práci s ukazateli jsou definovány dva operátory. Operátor & (reference) a operátor * (dereference). Operátor reference vrací odkaz na proměnnou, které mu byla dána jako vstup, tj. : float PI = ; // Proměnná typu float, obsahuje hodnotu float * ukazatelNaPi = & PI; // Proměnná typu ukazatel na float, obsahuje odkaz na PI Naopak operátor dereference vrací hodnotu proměnné, na níž ukazuje ukazatel, který mu byl dán jako vstup: float realnaPromenna = 1024; float * ukazatel = &realnaPromenna; *ukazatel = 2048; printf ( "Realna promenna ma hodnotu %f \n", realnaPromenna ); Realna promenna ma hodnotu 2048 #include <stdio.h> void tiskniDvojiciCisel( int x, int y ) { printf( "[ %i, %i ] ", x, y ); } void main ( void ) { int a = 3; int b = 4; int * pa = &a; int * pb = &b; tiskniDvojiciCisel( *pa, *pb ) int * pa = &b; int * pb = &a; tiskniDvojiciCisel( *pa, *pb ) } /*main*/ [ 3, 4 ] [ 4, 3 ] Ukazatel je samozřejmě standardní proměnná a během programu je možné libovolně měnit na co ukazuje.
52
Ukazatelová aritmetika
Pro ukazatele jsou definovány standardní aritmetické operátory +, - a operátory inkrementace a dekrementace ++, --. Operátory ovšem nemění hodnotu proměnné, na kterou ukazatel míří, ale mění cílovou polohu ukazatele! Paměť obsahující po sobě jdoucí celá čísla 1024 156 -103 98 515 1547 2048 ... ukazatelNaInt *ukazatelNaInt == 98 ukazatelNaInt *ukazatelNaInt == 515 ukazatelNaInt *ukazatelNaInt == 1547 ukazatelNaInt *ukazatelNaInt == -103 ukazatelNaInt *ukazatelNaInt == 1024 ukazatelNaInt *ukazatelNaInt == 156 Postupné inkrementace ukazatele ukazatelNaInt++ Uvedený příklad je pro operátor ++, ostatní fungují obdobně. Například ukazatelNaInt + 3 vrátí ukazatel na paměťovou buňku o tři dále k vyšším adresám, ukazatelNaInt - 2 vrátí ukazatel na buňku o dvě zpět k nižším adresám. Velikost posunu závisí na velikosti typu ukazatele. Pro int * je velikost buňky 4 byte, pro double * je velikost buňky 8 byte, pro char * je tato velikost 1 byte.
53
Ukazatelová aritmetika
#include <stdio.h> int strlen( char * string ) { int len = 0; while( *string != 0 ) { len++; string++; } return len; } void main ( void ) char * str1 = "Ahoj, jak se mas?"; char * str2 = "Je to vsechno na ... ehm ..."; printf ( "Delka retezce \"%s\" je %i znaku.", str1, strlen(str1) ); printf ( "Delka retezce \"%s\" je %i znaku.", str2, strlen(str2) ); Delka retezce "Ahoj, jak se mas?" je 17 znaku. Delka retezce "Je to vsechno na ... ehm ..." je 28 znaku. Ukazatelové aritmetiky se využívá například ve funkci počítající délku řetězce strlen. V C je řetězec tvořen posloupností znaků zakončených znakem s ASCII hodnotou 0 (o řetězcích podrobně později). Délka řetězce je tedy spočtena tak, že se postupně posouvá ukazatel od začátku řetězce po jednotlivých znacích a dokud neukazuje na nulový znak (terminátor), přičítá se jednička k celkové délce.
54
Ukazatelová aritmetika
Operátor "-" je zaveden také pro dva ukazatele jako operandy. Vrácen je rozdíl poloh ukazatelů v buňkách, tj. Paměť obsahující po sobě jdoucí celá čísla 1024 156 -103 98 515 1547 2048 ... ukazatelNaInt2 *ukazatelNaInt2 == 2048 ukazatelNaInt1 *ukazatelNaInt1 == 1024 Dva různé ukazatele ukazatelNaInt2 - ukazatelNaInt1 == 6 Funkci strlen lze modifikovat za využití operátoru "-" ve výše zmíněné formě na o něco málo rychlejší variantu (v každém průběhu cyklu je o jednu inkrementaci méně). int strlen( char * string ) { char *ptr = string; while( *ptr != 0 ) ptr ++; return ptr - string; }
55
Ukazatel na void a NULL Lze deklarovat ukazatel bez specifického určení typu, na který ukazuje. Takový ukazatel se deklaruje pomocí klíčového slova void: void * ukazatel; // Ukazatel bez určeného typu S takovým ukazatelem se zachází, jako by velikost jeho příslušné paměťové buňky byla 1 byte. Nelze jej jednoduše dereferencovat bez explicitního přetypování. Pokud překladači výslovně neřekneme, co se pod ukazatelem typu void * zrovna skrývá, nemá šanci to sám zjistit: float PI = ; void * ukazatel = &PI; // Správně - ukazatel nyní směřuje na obsah proměnné PI float promenna = * ukazatel; // Špatně - překladač neví, na co ukazatel směřuje a zahlásí // chybu při překladu int celeCislo = * (int *) ukazatel; // Správně - překladač bude hodnotu uloženou ve 4 byte pod // ukazatelem považovat za celé číslo a umístí ji do // proměnné celeCislo. Tato hodnota ovšem nebude mít s // konstantou pi nic společného. Do každého ukazatele lze uložit speciálně nadefinovanou hodnotu NULL - takový ukazatel pak neukazuje na nic. int* ukazatel = NULL; *ukazatel = 100; SEGMENTATION VIOLATION
56
Ukazatelové špeky Ukazatele jsou mocný nástroj, ale také se dá s jejich pomocí nadělat mraky chyb. #include <stdio.h> void main ( void ) { short celeCislo = 2048; float realneCislo = ; int *ukazatelNaCeleCislo = &celeCislo; *ukazatelNaCeleCislo = ; printf ( "cele cislo : %i, realne cislo : %f.", celeCislo , realneCislo ); } Cele cislo : , realne cislo : Výsledná chyba je ovšem závislá na architektuře, překladači a dokonce na nastavení překladače. Obecně špatné nastavení pozic ukazatelů respektive chyba ve velikosti objektů, na které ukazují, způsobí v lepším případě pád programu, v horším případě nečekané a na první pohled naprosto nelogické chování programu. Odstranit takovou chybu je vskutku detektivní práce.
57
Správa paměti Hromada Kód aplikace (heap) Globální proměnné Zásobník
ŽÁDOSTI PROGRAMÁTORA Hromada (heap) Kód aplikace Globální proměnné VOLÁNÍ FUNKCÍ Dynamické proměnné Zde se alokuje paměť pro větší datové celky na žádost programátora Zásobník (stack) Paměť je z pohledu aplikace v C rozdělena na tři části. Jednak základní statická paměť, kde je uložen kód aplikace spolu s globálními proměnnými, potom zásobník, kde se skladují lokální proměnné deklarované v tělech funkcí a nakonec hromada. Statické proměnné Zde se skladují lokální proměnné deklarované v tělech funkcí Paměť na zásobníku se přiřazuje automaticky, o paměť na hromadě je třeba požádat.
58
Správa paměti - zásobník
#include <stdio> int funkceB( int clen1, int clen2 ) { int kVraceni = (clen1 + clen2 ) / 2; return kVraceni; } double funkceA( int a, int b ) double cislo = ; int vysledek; vysledek = funkceB( a, b ); return vysledek * cislo; void main ( void ) double val = funkceA( 2, 4) ; printf ( "X = %f", val ); Stack overflow Tato chyba může nastat například při chybě v rekurzivním algoritmu 4B - kVraceni : 3 4B - kVraceni : ? 4B - clen2 : 4 4B - clen1 : 2 4B - vysledek : 3 4B - vysledek : ? 8B - cislo : 4B - b : 4 4B - a : 2 8B - val : 8B - val : ? ZÁSOBNÍK
59
Správa paměti – dynamická alokace
Aplikace1 void *data char *poleZnaku int *poleCisel void *seznam char *retezec1 int *pole char *retezec2 char *retezec3 Aplikace2 Hromada je společná pro všechny aplikace, o korektní přiřazování paměti a ochranu paměti přiřazené aplikaci se stará systém. Programátor musí v programu o přidělení dynamické paměti na hromadě žádat a musí ji po ukončení práce uvolnit. Aplikace3 float *matice HROMADA
60
Správa paměti – operátory new a delete
Žádost o přidělení paměti na hromadě podává programátor pomocí operátoru new. ukazatel = new typ ukazatel = new typ [počet prvků] Po zavolání operátoru new vytvoří systém v paměti místo na proměnou udaného typu (nebo na zadaný počet proměnných daného typu v řadě za sebou). Není-li paměti dostatek nebo nastala nějaká jiná chyba, je do proměnné ukazatel vložena hodnota NULL. V opačném případě je do něj uložena adresa prvního prvku. Příkaz k uvolnění paměti vydává programátor pomocí operátoru delete. delete ukazatel delete [ ] ukazatel Pozn.: pravděpodobnost přeplnění hromady je díky systému virtuální paměti poměrně malá, může ovšem nastat při nějaké programátorské chybě (např. alokace paměti v nekonečném cyklu).
61
Správa paměti – operátory new a delete
#include <stdlib.h> #include <stdio.h> int main () { int pocet,n; int * ukazatel; printf( "Kolik chcete zadat cisel? " ); scanf( "%u", &pocet ); ukazatel = new int [pocet]; if ( ukazatel == NULL) printf( "Chyba - nelze alokovat pamet" ); else for ( n = 0; n < pocet; n++ ) printf( "Zadejte cislo : " ); scanf( "%u", ukazatel + n ); } printf( "Bylo zadano : " ); for ( n = 0; n < pocet; n++ ) printf( "%i, ", ukazatel [n] ); delete [ ] p; Zde je typický příklad na použití dynamicky alokované paměti. Program má za úkol uschovat nějaké množství čísel - ale během překladu není jasné, kolik jich bude. Patřičná paměť je na čísla vyhrazena až poté, co uživatel zadá jejich počet. Všimněte si použití ukazatelové aritmetiky při načítání jednotlivých čísel z klávesnice a také možnosti pracovat s typovým ukazatelem u jako s polem (zadáním indexu do hranatých závorek) : *( u + index ) u [index] ekvivalentní
62
Správa paměti v ANSI-C Operátory new a delete jsou specialitou jazyka C++. Dnes už těžko narazíte na překladač, který by přeložil C a C++ nikoliv, ale pro pořádek - v C jsou pro práci s hromadou definovány následující funkce: void * malloc ( size_t size ); void * calloc ( size_t num, size_t size ); void * realloc ( void * ptr, size_t size ); void free ( void * ptr ) Funkce malloc je ekvivalentní jednoduchému operátoru new - alokuje místo pro jednu proměnnou o velikosti size bytů (či prostě alokuje paměť o dané velikosti). Funkce calloc je pak ekvivalent operátoru new [] - alokuje místo pro num proměnných o velikosti size. Obě funkce vrací ukazatel na void, tj. programátor si ho podle potřeby musí dále přetypovat. Funkce free pak zajišťuje uvolnění alokované paměti. int * cislo = new int; float * vektor = new float [3]; delete cislo; delete [] vektor; int * cislo = (int *) malloc( sizeof( int ) ); float * vektor = (int *) calloc( 3, sizeof( float ) ); free( cislo ); free( vektor ); Funkce realloc zvětší velikost již jednou alokované paměti a je-li to nutné, překopíruje obsah na jinou adresu (kterou vrátí).
63
Složené typy – Ukazatele na struktury
Dynamická alokace a ukazatele se často používají spolu se strukturami - struktura je kompaktní datový objekt, vhodný pro databázové algoritmy a počet alokovaných struktur aplikace je zpravidla dán uživatelským vstupem. Pro jednodušší přístup ke členům struktury na kterou je pouze odkaz (ukazatel) je zaveden operátor "->". #include <stdio.h> #include <stdlib.h> typedef struct complex { float Re; float Im; } void main( void ) { complex *complexVektor = new complex[100]; complex *ukazatel = complexVektor ; for( int n = 0; n < 100; n++ ) ukazatel -> Re = ( (float)rand() ) / ( (float)RAND_MAX ); ukazatel -> Im = ( (float)rand() ) / ( (float)RAND_MAX ); ukazatel ++; } for( int n = 0; n < 100; n++ ) TiskniComplex( complexVektor[n] ); delete [ ] complexVektor; Program naplní pole o 100 prvcích náhodnými komplexními čísly o reálné i imaginární části z intervalu <0, 1> a pak je vytiskne (funkce TiskniComplex viz průsvitka 21). Všimněte si přetypování celočíselných hodnot rand() a RAND_MAX na reálné typy ještě před dělením - kdyby tomu tak nebylo, dělení by se bralo jako celočíselné a výsledek by byl vždy nulový!
64
Ukazatelové špeky 2 Š P A T N Ě D O B Ř E
Je-li členem struktury nějaký ukazatel, pak při dealokaci původní struktury musíte dealokovat i paměť, na kterou ukazuje - jinak dojde k zapomenutí odkazu a část paměti zůstane alokovaná až do konce aplikace (memory leak). typedef struct complex { float Re; float Im; } typedef struct dveCisla { complex *A; complex *B; } void main( void ) { dveCisla *promenna = new dveCisla; dveCisla->A = new complex; dveCisla->B = new complex; . . . delete dveCisla; } Š P A T N Ě typedef struct complex { float Re; float Im; } typedef struct dveCisla { complex *A; complex *B; } void main( void ) { dveCisla *promenna = new dveCisla; dveCisla->A = new complex; dveCisla->B = new complex; . . . delete dveCisla->A; delete dveCisla->B; delete dveCisla; } D O B Ř E CVIČĚNÍ : práce se spojovým seznamem (buňka je nějaká struktura)
65
Složené typy – dynamická pole
Jak bylo v posledním příkladu naznačeno, operátor new [ ] vrací paměť vyhrazenou pro určitý počet prvků daného typu. Také bylo ukázáno, že za pomoci operátoru indexace "[ ]" lze k těmto prvkům přistupovat jako k prvkům statického pole. To proto, že každé pole je určeno ukazatelem na první prvek v paměti a indexace probíhá pomocí operátoru "[ ]" - tj. statická pole a prostor alokovaný pomocí new [ ] (dynamická pole) se principiálně nijak neliší. int statInt [10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } int * dynInt; dynInt = (int *) new int [10]; for( int n = 0; n < 10; n++ ) dynInt[n] = statInt[9 - n]; for( int n = 0; n < 10; n++ ) printf( "%i, ", synInt[n] ); delete [ ] dynInt; Dynamická pole mají oproti statickým tu výhodu, že nepotřebují znát svou velikost již během překladu. Navíc statická pole jsou vytvářena na zásobníku a ten je na rozdíl od hromady poměrně malý, takže není vhodný pro velká pole. Pozn. : Operátor "[]" je definován pomocí ukazatelové aritmetiky. *( u + index ) u [index] ekvivalentní
66
zde místo dvacítky bude pětka?
Předávání polí jako argumenty Je-li třeba předat pole jako argument nějaké funkce, musí být předány dvě věci : ukazatel na první prvek a celková délka pole. V C (na rozdíl například od C#) toto není automatizováno a programátor se o to musí postarat sám: #include <stdio.h> void KopirujPole( int *odkud, int odkudN, int *kam, int kamN ) { int prvku; if( odkudN > kamN ) prvku = kamN; else prvku = odkudN; for( int n = 0; n < prvku; n ++ ) kam[n] = odkud[n]; } void main( void ) int statInt [10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } int * dynInt = (int *) new int [20]; KopirujPole( statInt, 10, dynInt, 20 ); for( int n = 0; n < 20; n++ ) printf( "%i, ", dynInt[n] ); delete [] dynInt; Na výstupu z tohoto programu budou číslice jedna až deset, následované dalšími deseti náhodnými čísly. Procedura KopirujPole zkopírovala celé statické pole do dynamického, které je ale dvakrát větší. V druhých deseti prvcích dynamického pole zůstaly čísla, která byla v paměti před alokací - což je zcela náhodné smetí. Zde je také vidět nutnost inicializace důležitých proměnných! Co se stane, když zde místo dvacítky bude pětka?
67
Složené typy - Řetězce v C
Řetězec v ANSI-C je pole znaků, zakončené znakem s ASCII hodnotou 0. Pole může být statické nebo dynamické - jak již bylo zmíněno, mezi těmito druhy polí není principiální rozdíl. Zapíšeme-li do zdrojového kódu řetězcovou konstantu, překladač jí zavede jako statické pole při startu programu. Znak nula na konci (terminátor) je důležitý, neboť s řetězcem se běžně nepředává jeho délka - všechny procedury pracují se znaky řetězce tak dlouho, dokud nenarazí na nulu. int strlen( char * string ) { char *ptr = string; while( *ptr != 0 ) ptr ++; return ptr - string; } Např. funkci strlen se předává pouze ukazatel na první znak řetězce, ne však jeho délka. Se znaky se pracuje tak dlouho, až přijde nula. Z předchozí definice řetězce je zřejmé, že práce s nimi není nic jednoduchého. Např. v BASICu lze řetězce běžně sčítat : LET A$ = "Ahoj, " LET B$ = ", jak se mas?" PRINT A$ + "Tomasi" + B$ Což v ANSI-C tak snadno nejde (je třeba použít specializované funkce).
68
Složené typy - Řetězce v C
#include <stdio.h> char * SectiRetezce( char *prvni, char *druhy ) { int delka1 = strlen( prvni ); int delka2 = strlen( druhy ); char *soucet = new char [delka1 + delka2 + 1]; int index = 0; for( int n = 0; n < delka1; n++ ) soucet[n] = prvni[n]; for( int n = 0; n < delka2; n++ ) soucet[n + delka1] = druhy[n]; soucet[ delka1 + delka2 ] = 0; return soucet; } void main( void ) char *mujRetezec = SectiRetezce( "Ahoj, Tomasi, ", "jak se mas?" ); printf( "%s\n", mujRetezec ); delete [] mujRetezec; Zde je příklad funkce, která sečte dva řetězce. Všimněte si, že místo pro nový řetězec musí být alokováno - a tudíž po použití i zrušeno! Podobné funkce jsou ovšem již napsány v knihov-ně string,viz např. : CVIČĚNÍ : základní řetězcové operace (sčítání, vyhledávání apod.)
69
Direktivy preprocesoru
Preprocesor je program, který projede zdrojový kód ještě před překladem a udělá v něm nějaké úpravy. V první řadě odstraní komentáře, ale jeho úloha může být o mnoho složitější - například může rozhodnout o tom, které části kódu budou překládány a které ne (například v závislosti na architektuře). Preprocesor reaguje na speciální značky ve zdrojovém kódu, které začínají znakem #. Například : complex *ukazatel; ukazatel = new complex; . . . #ifdef _DEBUG printf( "Re = %f, Im = %f [ukazatel = 0x%X]\n", ukazatel->Re, ukazatel->Im ); #else printf( "Re = %f, Im = %f\n", ukazatel->Re, ukazatel->Im ); #endif Je-li definována direktiva preprocesoru "_DEBUG", vytiskne se reálná i imaginární část komplexního čísla, na které míří ukazatel spolu s adresou tohoto čísla. To má význam při ladění programu. Při běžném použití ("_DEBUG" není definována) se tiskne jen samotné číslo. Preprocesor na základě podmínky #ifdef _DEBUG vymaže ze zdrojového kódu příslušnou část, než jej pošle překladači.
70
Direktivy preprocesoru
#define Direktiva #define dovoluje nadefinovat a pojmenovat nějaký symbol či makro. Preprocesor pak všechny výskyty tohoto jména nahradí příslušným symbolem. #define PI float frekvence = 10.5; float omega = 2 * PI * frekvence; float frekvence = 10.5; float omega = 2 * * frekvence; #define radky 10 #define sloupce 5 #define matice( mat, x, y ) ( (mat)[ (y)*sloupce + (x) ] ) float *2Dpole = new float [ radky * sloupce ]; for( int m = 0; m < radky; m++ ) for( int n = 0; n < sloupce; n++ ) matice( 2Dpole, n, m ) = ( (float)rand() ) ) / ( (float) MAX_RAND ); Závorky kolem argumentů makra nejsou povinné, ale je bezpečnější je tam udělat pro případ, že by do argumentu přišel nějaký složitější výraz. Pozn. : je možné definovat pouze jméno bez hodnoty - pak se dá použít např v #ifdef . float *2Dpole = new float [ 10 * 5 ]; for( int m = 0; m < radky; m++ ) for( int n = 0; n < sloupce; n++ ) ( (2Dpole)[ (m)*5 + (n) ] = ( (float) rand() ) ) / ( (float) MAX_RAND );
71
Direktivy preprocesoru
#ifdef, #ifndef, #elif, #else, #endif Podmíněné direktivy dovolují na základě definovaných symbolů pomocí #define určit, které části kódu se budou překládat a které nikoliv. Typicky se tak určuje verze pro ladění či pro uživatele, kód specifický pro různé architektury či různé verze programu. #define _DEBUG complex *ukazatel; ukazatel = new complex; . . . #ifdef _DEBUG printf( "Re = %f, Im = %f [ukazatel = 0x%X]\n", ukazatel->Re, ukazatel->Im ); #else printf( "Re = %f, Im = %f\n", ukazatel->Re, ukazatel->Im ); #endif
72
Direktivy preprocesoru
#ifdef, #ifndef, #elif, #else, #endif Podmíněné direktivy dovolují na základě definovaných symbolů pomocí #define určit, které části kódu se budou překládat a které nikoliv. Typicky se tak určuje verze pro ladění či pro uživatele, kód specifický pro různé architektury či různé verze programu. #define Win_x32 // #define Win_x64 // #define Linux #ifdef Win_x32 printf ( "Verze aplikace \"tojsemtovymnouk\" pro 32 bitovou verzi Windows ... \n" ); #elif Win_x64 printf ( "Verze aplikace \"tojsemtovymnouk\" pro 64 bitovou verzi Windows ... \n" ); #elif Linux printf ( "Verze aplikace \"tojsemtovymnouk\" pro Linux ... \n" ); #else printf ( "Verze aplikace \"tojsemtovymnouk\" pro nespecifikovanou architekturu ... \n" ); #endif Jak uvidíme později, tento mechanizmus je také běžnou součástí ochrany před vícenásobným vložením hlaviček (soubory typu *.h) do zdrojových kódů.
73
Direktivy preprocesoru
#error Pomocí direktivy #error lze uměle vyvolat chybu při překladu. To je užitečné pro potřeby ladění. Předchozí příklad by šel modifikovat třeba takto: // #define Win_x32 // #define Win_x64 // #define Linux #ifdef Win_x32 printf ( "Verze aplikace \"tojsemtovymnouk\" pro 32 bitovou verzi Windows ... \n" ); #elif Win_x64 printf ( "Verze aplikace \"tojsemtovymnouk\" pro 64 bitovou verzi Windows ... \n" ); #elif Linux printf ( "Verze aplikace \"tojsemtovymnouk\" pro Linux ... \n" ); #else #error Je nutne specifikovat architekturu! #endif Chyba při překladu nastane, pokud programátor zapomene definovat (resp. odkomentovat) jeden z definovaných symbolů Win_x32, Win_x64, Linux.
74
Direktivy preprocesoru
#include Direktiva na místo sebe zkopíruje obsah zadaného souboru. #include "hlavicka.h" // Prohledá aktuální adresář a vloží na toto místo celý soubor hlavicka.h #include <stdlib> #include <stdlib.h> // Prohledá seznam knihoven C/C++ a vloží na toto místo hlavičku příslušné // standardní knihovny Předdefinovaná makra preprocesoru __LINE__ je nahrazeno číslem řádku __FILE__ je nahrazeno jménem souboru se zdrojovým kódem __DATE__ je nahrazeno aktuálním datem __TIME__ je nahrazeno aktuálním časem __FUNCTION__ je nahrazeno jménem funkce (pouze pro GCC) __PRETTY_FUNCTION__ je nahrazeno jménem funkce včetně rozlišení jejího typu (pouze pro GCC)
75
Direktivy preprocesoru
#define TestMemError( ptr ) \ { \ if( (ptr) == NULL ) { \ fprintf( stderr, "Chyba alokace, soubor %s, řádek %i !\n" , __FILE__, __LINE__ ); \ exit( - 1); \ } \ } void main( void ) { int * poleInt = new int [100000]; TestMemError( poleInt ); float * poleFloat = new float [100000]; TestMemError(poleFloat ); float * poleChar = new char [ ]; TestMemError(poleChar ); . . . Zde je příklad na použití maker a předdefinovaných proměnných preprocesoru. Není-li dost paměti na alokaci velkých polí, program by normálně spadl s jakousi nesrozumitelnou hláškou a výpisem. Takhle napíše, proč byl ukončen a kde přesně k chybě došlo. Všimněte si definice většího makra na více řádků za pomoci zpětného lomítka. Povšimněte si také výpisu chybové hlášky na standardní chybový výstup (stderr).
76
Modulární programování
Je obvyklé, že větší program má zdrojový kód o mnoha tisících či dokonce desetitisících řádek. Kdyby toto všechno bylo v jednom souboru, byly by jakékoliv úpravy obtížné a orientace těžká. Navíc bývá obvyklé, že více programů má společné součásti (vstupně/výstupní operace, práce s diskem, výpočetní rutiny apod.). Z těchto důvodů je moudré rozdělit program do více souborů podle tematických celků. Modul pro V/V operace Modul pro práci s HDD Modul pro výpočty Hlavní modul (fce main) operaceVV.h praceHDD.h vypocty.h aplikace.c operaceVV.c praceHDD.c vypocty.c Celá aplikace. Každý soubor (modul) se překládá zvlášť. Problém : každý modul potřebuje volat funkce z ostatních. Jak ale překladač ví, které funkce jsou ve kterém modulu? Odpověď : Zavedením hlaviček a možnosti deklarovat funkci, aniž bylo definováno její tělo.
77
Modulární programování
typedef struct complex { float Re; float Im; }; complex Secti( complex clen1, complex clen 2 ); void TiskniComplex ( complex cislo ); void main( void ) { complex A, B; A.Re = 1; A.Im = 2; B.Re = 3; B.Im = 4; complex C = Secti( A, B ); TiskniComplex( C ); } complex Secti( complex clen1, complex clen 2 ) complex soucet; soucet.Re = clen1.Re + clen2.Re; soucet.Im = clen1.Im + clen2.Im; return soucet; void TiskniComplex ( complex cislo ) if( cislo.Im < 0.0 ) printf( "%f - %fi", cislo.Re, abs( cislo.Im ) ); else printf( "%f + %fi", cislo.Re, cislo.Im ); Hlavičky (soubory *.h) obsahují pouze deklarace funkcí, deklarace externích proměnných, definice typů a případné symboly preprocesoru. Vlastní těla funkcí obsahují soubory typu *.c . Na příkladu vlevo je vidět rozdíl mezi deklarací a definicí funkce. Deklarace obou použitých funkcí jsou bezprostřed-ně za definicí typu complex. Říkají překladači, že takovéto funkce existují a jsou definovány někde jinde. Proto je lze ihned použít ve funkci main. Překladač se při překladu souboru nestará o to, zda funkce opravdu někde existují - to udělá na závěr linker.
78
Modulární programování
Předchozí zdrojový kód lze rozdělit například do těchto tří souborů. V complex.h je definice typu complex a deklarace funkcí, které s ním pracují. V souboru complex.c jsou definice funkcí, které pracují s komplexními čísly. Musí zde být direktivou #include vnořený soubor complex.h, protože samotné hlavičky se nepřekládají (pouze *.c soubory). Nakonec v aplikace.c je funkce main, která modul complex využívá. typedef struct complex { float Re; float Im; }; complex Secti( complex clen1, complex clen 2 ); void TiskniComplex ( complex cislo ); complex.h complex.c #include "complex.h" complex Secti( complex clen1, complex clen 2 ) { complex soucet; soucet.Re = clen1.Re + clen2.Re; soucet.Im = clen1.Im + clen2.Im; return soucet; } void TiskniComplex ( complex cislo ) if( cislo.Im < 0.0 ) printf( "%f - %fi", cislo.Re, abs( cislo.Im ) ); else printf( "%f + %fi", cislo.Re, cislo.Im ); #include "complex.h" void main( void ) { complex A, B; A.Re = 1; A.Im = 2; B.Re = 3; B.Im = 4; complex C = Secti( A, B ); TiskniComplex( C ); } aplikace.c
79
Modulární programování
Podobný mechanizmus jako pro funkce (deklarace/definice) existuje pro proměnné. Globální proměnné lze deklarovat v hla-vičkách pomocí klíčového slova extern. Pozor! Když na extern zapomenete a prsknete proměnnou do hlavičky jen tak, vyloží si to překladač, jako že má definovat proměnnou tohoto jména v každém modulu, který se na complex odkazuje. A to je samozřejmě něco úplně jiného, než byl původní záměr ... typedef struct complex { float Re; float Im; }; complex Secti( complex clen1, complex clen 2 ); void TiskniComplex ( complex cislo ); extern complex A; extern complex B; complex.h complex.c #include "complex.h" complex A, B; complex Secti( complex clen1, complex clen 2 ) { . . . } void TiskniComplex ( complex cislo ) #include "complex.h" void main( void ) { A.Re = 1; A.Im = 2; B.Re = 3; B.Im = 4; complex C = Secti( A, B ); TiskniComplex( C ); } aplikace.c
80
Modulární programování
V případě složitějších programů se nezřídka stává, že dochází k vícenásobným či dokonce rekurzivním vnořením hlaviček. To je ovšem nežádoucí a je proto třeba zavést nějaký ochranný mechanizmus. V C je to standardně použití direktiv preprocesoru #define a #ifndef následujícím způsobem: #ifndef soubor1_H #define soubor1_H deklarace . . . #endif soubor1.h #ifndef soubor2_H #define soubor2_H #include "soubor1.h" deklarace #endif soubor2.h #include "soubor1.h" #include "soubor2.h" void main( void ) { } main.c Ačkoliv jsou v tomto kódu (nesmyslné) požadavky na vícenásobná vnoření téže hlavičky, efektivně se každá hlavička dostane do každého modulu jen jednou. CVIČĚNÍ : převedení předchozích složitějších cvičení do modulů
81
Zdroje http://www.cppreference.com/ http://www.cplusplus.com/
82
Shrnutí Úvod Překlad a linkování, skript Makefile
Základní syntax, komentáře Typy a deklarace proměnných, lexikální rozsah platnosti Konstanty Složené typy - statická pole, výčty a struktury přetypování Operátory Podmínky, cykly, větvení Základy vstupu a výstupu Funkce (definice, předávání parametrů, přetěžování, rekurzivní volání) Klíčové slovo typedef Ukazatele a ukazatelová aritmetika Správa paměti, alokace dynamických struktur Složené typy - dynamická pole a řetězce Direktivy preprocesoru Modulární programování
Podobné prezentace
© 2024 SlidePlayer.cz Inc.
All rights reserved.