Programovací jazyk C++ Jiří Fišer KI PřF UJEP presentace pro kurs Programování III (s předpokládanou znalostí jazyka C#)
MXMVIII 1
Základní charakteristika ●
●
●
●
2
programovací jazyk C++ je objektovým rozšířením jazyka C a je s ním do značné zpětně kompatibilní, kód v jazyce C je tedy i platným kódem jazyka C++ (výjimkou jsou pouze zastaralé konstrukce jazyka C resp. naopak konstrukce nové verze jazyka C ISO 1999) styl programování se ovšem podstatně liší nebo: –
v C++ jsou preferovány OOP konstrukce
–
C++ má vlastní standardní knihovnu na vyšší úrovni abstrakce než tomu bylo v C (C knihovnu je však nadále použitelná)
C++ je velmi komplexní a složitý programovací jazyk (zvládnutí celého jazyka je práce na mnoho let)
Výhody a nevýhody ●
●
●
●
●
●
3
[+] jazyk C++ je jazykem velkého množství softwarových projektů, především v oblasti základních aplikací a komplexních softwarových systémů [+] vyjadřovací síla jazyka je značná a to na všech úrovních abstrakce (od přímé zprávy k paměti k abstraktním konstrukcím) [+] v C++ lze psát prostorově i časově efektivní programy (rychlé s malými nároky na paměť) [-+] jazyk nenabízí automatickou správu paměti [garbage collector]. Časté jsou neplatné přístupy k paměti či její úniky [-] standardní knihovna C++ je velmi omezená. Mnohé oblasti (sítě, GUI, databáze, XML, apod.) nepodporuje [-] jazyk nepodporuje reflexi (→ obtížná podpora GUI komponent)
Stručná historie jazyka 1980 : vzniká C with Classes, prvotní verze jazyka (Stroustrup) 1983 : název C++ a dostupnost prvního kompilátoru (Cfront) 1985 : vydání knihy The C++ Programming Language 1990: začátek standardizačního procesu 1998: ISO standard jazyka ISO/IEC 14882:1998 2005: většina překladačů téměř podporuje ISO standard (většinou však nikoliv v základním režimu, některé prvky nebyly v zásadě akceptovány) 2005: Technical Report 1 (knihovní rozšíření, nejsou povinné) 2009?: nová verze jazyka
4
C++ a .NET ●
●
●
5
C# a C++ jsou si velmi podobné na úrovni syntaxe (C# převzalo mnohé rysy syntaxe jazyka C i C++) podobnost na úrovni sémantiky je však výrazně menší (C# je v tomto ohledu více podobné Javě či ostatním .NET jazykům jako Visual Basic nebo Boo) kromě paměťového modelu se výrazně liší model modularity (ta je založena na textové substituci a linkování knihoven, nikoliv na samopopisných assembly), model šablon (=generik) (ten není na jedné stranu propojen s objektovým modelem, na stranu druhou je mnohem pružnější a tvoří de facto vlastní jazyk) v .NET existuje i rozšíření jazyka C++ tzv. C++/CLI. To je do značné míry kompatibilní s ISO C++, má však mnohá rozšíření pro kompatibilitu s CLR a CLS (výrazně složitější)
C# a C++ - shody ●
●
●
●
●
6
jazyk C++ se shoduje (resp. téměř shoduje) s jazykem C# v následujících oblastech (převzatých z jazyka C) základní programové konstrukce: if, while, for, break, continue, return (ale chybí) aritmetické, relační a logické operace, přiřazení (včetně složeného a inkrementací) definice proměnných jednoduchých číselných typů a jejich literály základní objektový model: datové členy, metody, základní přístupová práva (public, private).
Hello, world v C++ ●
program Hello, world lze napsat v C++ mnoha způsoby (lze např. použít i jeho klasickou C verzi).
#include
using namespace std; int main() {
textové vložení hlavičkového souboru s deklaracemi tříd, funkcí, proměnných apod. import jmenného prostoru (std je jediný standardní jmenný prostor)
cout << "Hello, world" << endl; return 0; }
výpis do std. vstupu (s přetíženou verzí operátoru bit. posunu)
hlavička hlavní funkce programu (neobjektová konstrukce) jazyk C se nezapře 7
Elementární datové typy ●
●
základní systém datových typů se neliší od C# (bool, int, double, float) u speciálnějších typů však rozdíly existují. V C++ navíc nemají mnohé číselné typy přesně definované MPH: char : číselný typ (!!) 1-bytový (odpovídá nejčastěji sbyte) [signed] short [int] : znaménkový (nejčastěji 2-bytový) unsigned short [int]: neznaménkový (nejčastěji 2-bytový) [signed] int: znaménkový (nejčastěji 4-bytový) unsigned [int]: neznaménkový (nejčastěji 4 bytový) [signed] long [int]: znaménkový (nejčastěji 4 bytový) řetězec: std::string - ne zcela plnohodnotný typ bez podpory Unicode (jen ASCII)
8
Třídy ●
základní zápis tříd se příliš neliší od C# (podporovány však nejsou vlastnosti, vše co souvisí s assembly apod.).
sekce přístupu class Zlomek { (vše v dané sekci má daný přístup) private: zde nepovinné, neboť private je implicitní sekce int _citatel; implicitní hodnoty parametrů int _jmenovatel; definují jedním zápisem více přetížených metod public: volání (pseudo)konstruktorů Zlomek(int c=0, int j=1) podobjektů : _citatel(c), _jmenovatel(j) {} Zlomek soucin(Zlomek z) { return Zlomek(_citatel * z._citatel, jmenovatel * z._jmenovatel); } void setCitatel(int c) {_citatel = c;} int citatel() const {return _citatel;}
}; 9
středník!!!
označení konstantnosti metody (metoda nemění adresáta = this)
Třídy jako hodnotové typy I ●
●
●
10
třídy jsou v jazyce C++ implicitně hodnotovými typy, tj. jejich instance (=objekty) jsou uloženy přímo na zásobníku (lokální automatické proměnné) nebo ve statické paměti (globální proměnné). [struct v C#] výhody: –
objekty automaticky zanikají po opuštění oboru viditelnosti (scope) jejich proměnných (uvolnění paměti)
–
objekty lze snadno duplikovat (kopírovací konstruktor) a přiřazovat (přiřazovací operátor)
nevýhody: –
při předávání (parametry, návratové hodnoty) se musí kopírovat celý objekt (řešení: reference)
–
není podporován obj. polymorfismus (objekty musí být typovány skutečnými typy) (řešení: dyn. alokace a ukazatele, reference)
Třídy jako hodnotové typy II ●
definice proměnných
Zlomek z;
//použije se bezparametrický konstruktor
Zlomek x (2); //použije se verze s jedním parametrem Zlomek y (1, 2); Zlomek t = z; //použije se kopírovací konstruktor x = z;
//zde se ale použije se přiřazovací operátor
kopírovací konstruktor a přiřazovací operátor se nemusí explicitně implementovat (překladač automaticky generuje verze provádějící kopírování byt po bytu) definici tohoto typu lze použít pro: 1) automatické lokální proměnné, 2) parametry (jsou předávány hodnotou), 3) statické globální proměnné, 4) datové členy 11
Definice metod mimo těla třídy I ●
●
●
●
12
definice metod přímo v těle třídy se používá pouze pro velmi jednoduché metody (ty jsou pak rozvíjeny přímo do kódu = inline metody) u ostatních metod se metody implementují (definují) mimo těla třídy navíc v jiném zdrojovém souboru. deklarace tříd (a deklarace globálních funkcí a proměnných) se uvádí v tzv. hlavičkovém souboru (s příponou .h) tento soubor poskytuje primárně rozhraní třídy (i když obsahuje i definice datových členů) implementace tříd (a globálních funkcí + definice globálních proměnných) je ve zdrojovém souboru s příponou .cpp (resp. cc, cpp, cp, c++)
Definice metod mimo těla třídy II ●
hlavičkový soubor: zlomek.h
deklarace konstruktoru class Zlomek { (bez těla jen se signaturou ) ... public: Zlomek(int citatel=0, int jmenovatel = 1); void zkrat(); deklarace metody }; ●
implementační soubor: zlomek.cpp #include"zlomek.h"
kvalifikace jménem třídy oddělovačem je vždy čtyřtečka
vložení (textu) hlavičkového souboru nutné (jinak by nebyla známa dekl. třídy) implicit. hodnoty parametrů se neopakují !!
Zlomek::Zlomek(int citatel, int jmenovatel) : _citatel(citatel), _jmenovatel(jmenovatel) {this->zkrat();} void Zlomek::zkrat() {...} 13
kvalifikace objektem this (nepov.) this je ukazatel proto ->, nikoliv tečka
Statické datové členy a metody ●
statické datové členy a statické metody mají podobnou funkci jako v C# = umožňují definovat data (resp. konstanty) a metody na úrovni tříd class Zlomek { nelze inicializovat přímo ve třídě (deklarace) static Zlomek zero; nutno při definici (v impl. souboru) static bool compare(Zlomek a, Zlomek b); }; Zlomek Zlomek::Zero = Zlomek(0,1); bool Zlomek::compare(Zlomek a, Zlomek b) { }
přístup ke statickým členům z vnějšku (kvalifikace třídou) Zlomek::Zero, Zlomek::compare(x, y) 14
Reference ●
●
●
reference umožňují snadné předávání parametrů referencí (odkazem) v zásadě jsou to proměnné obsahující odkaz na hodnotu. Reference musí po dobu své existence odkazovat na stejný paměťový objekt, odkazovaná hodnota se však může měnit (a tím i identita OOP objektu) předávání odkazem se používá 1) pro výstupní a vstupně výstupní proměnné odkazované paměťové místo se v tomto případě mění
(výstupní parametry se používají zřídka, podobně jako v C# parametry se specifikátorem ref nebo out)
2) pro efektivnější předávání hodnot (bez kopírování)
používá se téměř důsledně pro všechny neelementární typy
15
Reference II ●
reference pro výstupní parametry void Zlomek::VratSmíšenýTvar (int& celaCast, Zlomek& zlomkovaCast) const { ... přímý přístup k odkazované hodnotě (ta je přiřazením změněna) celaCast = ...; zlomkovaCast = Zlomek(...); } neinicializovaná proměnná (!!)
zde musí být označení paměťového místa (tzv. l-hodnota), to je v rámci postranního efektu metody změněno
volání: int i; Zlomek zc; z.VratSmisenyTvar(i, zc); cout << i; 16
Reference III ●
použití reference pro efektivnější předání vstupního parametru
Zlomek soucet(const Zlomek& operand) const {...} reference na konstantní objekt třídy Zlomek
překladač kontroluje neměnnost odkazované hodnoty . Do objektu nesmí být přiřazováno, nesmí být přímo měněn jeho (veřejný) datový člen a především na něj nesmí být volána nekonstantní metoda (bez const za seznamem parametrů) a nesmí být předáván nekonstantní referencí do dalších metod => nutnost důsledného používání const u metod a referencí volání: z.soucet(x), ale i z.soucet(Zlomek(1,2)) 17
Reference IV ●
reference však lze i vracet jako návratové hodnoty metod a to opět nejčastěji z důvodů eliminaci kopírování hodnot
vrácená reference musí odkazovat na objekt, který existuje i po ukončení metody !!! ●
●
tj. lze vrátit jen referenci na adresáta (this), resp, referenci předanou jako parametr (typicky konstantní) nelze vrátit referenci na objekt v lokální proměnné, parametru předaném hodnotou resp. na anonymní hodnotu
Zlomek& Přičti(const Zlomek& inkrement) { _citatel += inkrement._citatel; … chybí const (metoda není konstantní!) return *this; } operátor vracející objekt, na nějž ukazuje ukazatel, vrácená reference ukazuje na takto získaný objekt
18
Konstantní vers. nekonstantní ●
u některých typů metod existují dvě základní alternativy implementace: –
konstantní metody vracející výsledek jako nový objekt (např. Secti) ●
● ●
–
jsou využitelné při předefinování běžných operátorů
nekonstantní metody modifikující adresáta (a běžně vracející referenci na něj pro další řetězení) ●
●
19
při vícenásobném použití neefektivní (vždy je vytvářen nový dočasný objekt) bezpečnější při používání (žádné postranní efekty)
výrazně efektivnější (nedochází ke vzniku a zániku dočasných objektů) mohou vznikat nepříjemné efekty při chybném použití
Pole v jazyce C++ existují dva základní typy polí: ●
statické pole [alokované na zásobníku nebo ve statické paměti] statické pole se snadněji používá a je automaticky destruováno velikost pole však musí být známa již v době překladu
●
dynamické pole [alokované na hromadě] složitější alokace (i když přístup se následně již neliší) nutná explicitní dealokace (opomenutí = memory leak) velikost lze stanovit za běhu aplikace
obě pole používají stejnou syntaxi při přístupu, nejsou však bohužel zaměnitelná (dealokace stat. pole vede k výjimce) 20
Statické pole I ●
definice statického pole
bázový-typ identifikátor [velikost] velikost musí být známa již při překladu tj. musí to být tzv. konstantní výraz 1) číselný literál (např. 2) 2) symbolická konstanta konst. výrazu (=textové makro) #define SIZE 2 3) konstantní proměnná inicializovaná konstantním výrazem (např. const int size = 2;) 4) numerický výraz jehož operandy jsou konstantní výrazy (např. 2 * SIZE + 1) 21
Statické pole II ●
příklady definicí:
int vektor [10], Zlomek posloupnost [SIZE] ●
pro přístup ke statickému poli lze použít běžnou indexaci (první index je 0)
vektor[0], vektor[9] jazyk C++ nekontroluje meze indexů, pokud je použit index mimo rozsah 0..SIZE-1 dostane se program do nedefinovaného stavu (porušení ochrany paměti, změna chování programu, apod) pole není objekt (nelze volat žádné metody) a nelze zjistit jeho velikost (např. ve funkci, jíž je pole předáno) 22
Ukazatel ●
●
pro jazyk C++ stejně jako jazyk C je typický úzký vztah mezi ukazateli a poli ukazatel [pointer]= hodnota obsahující odkaz na paměťový objekt (= trvalejší hodnota) ukazatel lze chápat jako spojení paměťové adresy a informace o datovém typu odkazovaného objektu počáteční adresa ukazatel 1 ukazatel 2
paměťový objekt
v jazyce C++ však mohou existovat ukazatele, které neukazují na alokovaný paměťový objekt (nejsou inicializovány, objekt na než ukazovaly zanikl, apod) 23
Ukazatel II ●
●
●
hlavním problém jazyka C++ je právě možnost používání neplatných ukazatelů (výsledkem je nepříjemný nedefinovaný stav) dodržováním jistých zásad lze tento problém minimalizovat, nikoliv však zcela eliminovat definice proměné typu ukazatel
bázový_typ* identifikátor např. int* p vznikne proměnná obsahující neinicializovaný ukazatel je proto nutná inicializace ukazatelem na objekt příslušného bázového typu jednou z možností inicializace je dynamická alokace polí
24
Reference a dereference ●
pomocí referenčního operátoru můžeme získat ukazatel na libovolné paměťové místo označené proměnnou resp. l-výrazem: int i; int *p; p = &i; //p ukazuje na pam. místo proměnné i
●
opakem reference je dereference – získání l-hodnoty paměťového místa na něž odkazuje ukazatel
*p = 0; //což je totéž jako : i = 0; (přímý přístup) ●
25
při referenci i dereferenci hraje významnou roli bázový typ ukazatele (přetypování je možné, ale nebezpečné)
Pointerová aritmetika I ●
●
pointerová aritmetika umožňuje získávat ukazatele ukazující na okolní objekty v poli základní pointerově aritmetickou operací je přičtení celého čísla k ukazateli: int *ptr, *nextptr; //inicializace ukazatele ptr nextptr = ptr + n;
●
●
●
26
výsledkem je nový ukazatel odkazující paměťový objekt typu int ležící na adrese zvýšené o n*sizeof(int). pointerové operace má smysl používat jen nad souvislým polem hodnot bázového typu (zde lze nový ukazatel interpretovat jako posunutý o n-položek vpravo) další odvozené operace: ptr-n, ptr++, ptr--, ptr1-ptr2, apod.
Pointerová aritmetika II pole: int [3] 0
1
2
3
4
5
6
7
8
9
10
11
Hic sunt leones
Hic sunt leones
iptr (typu int*) iptr + 1
iptr + 1
pole: char [12] 0
1
2
bptr -1 27
3
4
5
6
7
bptr +1 bptr (typu char*)
8
9
10
11
Pole a ukazatel ●
statické pole se ve většině kontextů interpretuje jako ukazatel na první položku pole
int pole [10]; *pole = hodnota první položky = pole[0] *(pole+2) = pole[2]; ●
indexaci lze tedy rozepsat pomocí ukazatelové aritmetiky, formální rozpis se však v praxi nepoužívá. Typičtější je kompletní transformace do ukazatelového přístupu:
for(int *p=pole; *p; *p++ = 0); //klasický C-ideom hlavní výjimkou je výraz: &pole, který je typu int (*) [10] = ukazatel na pole deseti hodnoty typu int (nikoliv tedy int**) 28
Ukazatel a pole ●
●
na druhou stranu lze na ukazatel aplikovat indexaci (tj. můžeme jej chápat jako ukazatel na první položku celého pole objektů bázového typu) indexy se převádějí na ukazatelové operace podle recipročního vztahu: ptr[index] = *(ptr + index)
●
29
vždy je však nutno odlišovat ukazatel a pole na sémantické rovině. Hlavní rozdíly: –
ukazatel nemusí vždy odkazovat na celé pole hodnot bázového typu (nelze nijak ověřit na co skutečně odkazuje, není žádná kontrola mezí)
–
proměnná typu pole není na rozdíl od ukazatelové proměnné modifikovatelná: pole++ (nelze) oproti ptr++
Pole jako parametr I ●
●
●
pokud se použije pole na místě skutečného parametru, je do funkce/metody předáno jako ukazatel na první položku pole (i zde se tedy použije běžná interpretace) odpovídající formální parametr musí tedy být typu ukazatel na bázový typ pole (resp. na konstantní bázový typ je-li parametr jen vstupní) jako další parametr (typu int) by měla být předána velikost pole (nelze ji z ukazatele zjistit), výjimkou je případ kdy je v poli explicitní zarážka (koncový prvek)
double * suma (const double* v, int size); funkce přijímá jako vstupní parametr pole (nebo ukazatel ukazující na první položku souvislé oblasti paměti) a věří, že má size položek. Vrací součet prvků. 30
Pole jako parametr II ●
definice funkce (s ukazatelovým přístupem, možný je i indexový [vyzkoušejte!])
double suma(const double* v, int size) { double suma = 0; for(double *p = v;
p-v < size; p++) suma += *p;
return suma; } ●
volání funkce (N je symbolická konstanta literálu)
double vektor [N]; cout << suma(vektor, N) << endl;
31
Dynamické pole I ●
●
●
dynamické pole (přesněji dynamicky alokované pole) je dynamicky (=za běhu) alokovaná oblast paměti, k níž je přistupováno pomocí ukazatele podobně jako ke statickému poli bázový typ ukazatele určuje typ položek a musí se shodovat s typem určeným při alokaci. Velikost může být určena libovolným (tj. i nekonstantním výrazem), nelze ji však poté měnit. alokace dynamického pole (na pravé straně se podobá C#, vzniká však něco mnohem jednoduššího)
long int* dp = new long int [100]; char* bytes = new char [size]; 32
Dynamické pole II ●
použití dynamických polí přináší tři podstatné nevýhody: 1. stejně jako u konstantních polí se nekontrolují meze (ve skutečnosti se vše provádí v pointerové aritmetice a ta neověřuje, zda se posunuté ukazatele neocitli za hranicí pole mezi lvy) 2. dynamicky alokované pole musí být explicitně uvolněno. Zápis je jednoduchý, ale lze snadno zapomenout (především je-li pole vráceno funkcí/metodou). delete [] dp; //nemusí být zadána velikost pole 3. chybou je naopak vícenásobné uvolnění, uvolnění ukazatele ukazující na jiný než první prvek, uvolnění statického pole (vždy musíme vědět kdo pole tzv. vlastní)
33
Zapouzdření dynamického pole ●
●
●
●
●
34
použití pole v aplikacích není tedy příliš robustní (tj. je tzv. error-prone). naštěstí lze pomocí OOP konstrukcí vytvořit mnohem komfortnější a bezpečnější implementaci pole (s rozhraním, které je nadmnožinou rozhraní původního pole) tato implementace bude využívat interně původního pole, zapouzdří je však do nového rozhraní (tj. bude fungovat jako adaptér) mírně se sice sníží efektivita (jak paměťová tak časová), ale u většiny operací se bude jednat jen o mírné zpomalení (problematické bude jen kopírování) pro zjednodušení bude pole omezeno na celočíselný typ položek (polymorfní verze je možná jen pomocí šablon)
Zapouzdření dynamického pole II ●
●
shrnutí požadované funkčnosti: –
dynamická alokace (nikoliv však realokace, velikost se po vytvoření nemění)
–
zjištění velikosti pole (s časovou složitostí O(1))
–
indexace s kontrolou mezí
–
snadné předávání ve formě parametrů (návratové hodnoty) a přiřazování
–
automatická dealokace po opuštění oblasti platnosti
prvotní návrh datové representace objekt obsahující dvě položky: velikost pole (konstantní) ukazatel na dynamicky alokované pole
35
Referenční vers. hodnotové přiřazení ●
●
●
●
36
pro další diskusi nad datovou representací je nutné diskutovat implementaci přiřazení (resp. předávání) při hodnotovém přiřazení se při přiřazení vytvoří nová kopie dat pole (= alokace + kopírování pole data) výhody: hodnotová sémantika = elegantnější + bezpečnější nevýhody: velmi pomalé (lze řešit pomocí COW) při referenčním přiřazení se pouze přesměruje ukazatel na existující pole (nic se nekopíruje) nevýhody: referenční sémantika vede k sdílení polí = méně přehledné, více náchylné k chybám (pole není read-only) výhody: efektivnější, bližší původní sémantice (ale ne zcela, nelze snadno zajistit konstantnost u vstupních parametrů) z důvodů jednoduchosti (při zachování rozumné efektivity) zvolíme referenční sémantiku
Referenční přiřazení ●
●
●
●
37
triviální implementace referenčního přiřazení A = B:
takto příliš jednoduchá implementace však přináší jeden zásadní problém s uvolňováním paměti. problém: jaký objekt bude při vícenásobném odkazu uvolňovat sdílené pole (nelze zaručit pořadí uvolňování A a B) řešení (nejjednodušší): čítání počtu aktivních odkazů
Referenční přiřazení II ●
●
38
referenční čítaní však poněkud zkomplikuje datový návrh (referenční čítač musí být součástí sdílené paměti)
původní objekt (třídy DArray) nyní obsahuje jen ukazatel na sdílený objekt typu ShrArrayData, poskytuje však celé veřejné rozhraní. Rozhraní objektů ShrArrayData je neveřejné, objekty však nesou však většinu informací: velikost pole, odkaz na vlastní data i aktuální počet odkazů na sdílený objekt (po jeho poklesu k nule může být objekt destruován)
ShrArrayData - deklarace ●
deklarace pomocné třídy (sdílené pole)
class ShrArrayData { int size; voláno při zvýšení počtu odkazů int refs; (triviální, počet odkazů není omezen) int* data;
voláno při snížení počtu odkazů pokud klesne na nulu je vráceno true pole rušící odkaz pak musí uvolnit objekt ShrArrayData
public: ShrArrayData(int size); void acquire() {refs++; totalRefs++;} bool release(); int length() const {return size;}vrací referenci na položku pole int& getItemRef(int index); (lze pak použít i na pravé straně přiřazení)
static int totalRefs; }; 39
pro účely testování (celkový počet aktivních odkazů). Musí být po skončení programu roven 0!
ShrArrayData - metody implementace základních metod není složitá (trochu jí komplikují jen vstupní podmínky - aserce) bool ShrArrayData::release() { assert(refs > 0, "[RELEASE] refs == 0 before release"); refs--;totalRefs--; if(refs == 0) { delete [] data; return true; //object must be externally deleted } else return false; } int& ShrArrayData::getItemRef(int index) { assert(index >= 0 && index < size, "[ITEMREF] index out of range"); return data[index]; } 40
ShrArrayData - konstruktor ●
●
nepříliš složitá je i implementace konstruktoru (je jen jediný s parametrem určujícím požadovanou délku pole) i zde jsou využity aserce (výrazně usnadňují ladění), v případě problémů s alokací by bylo asi vyhození výjimky (není to programová chyba) ShrArrayData::ShrArrayData(int size) { assert(size>0, "[ARRAY ALLOC] NOT size > 0"); this->size = size; po vytvoření je objekt ihned jedenkrát odkazován this->refs = 0; (!! nepříliš vhodné řešení) acquire(); this->data = new int[size]; assert(size != 0, "[ARRAY ALLOC] memory exhausted"); }
41
DArray - základní návrh I ●
třída DArray se zaměřuje především na správu sdíleného obsahu (jeho vytváření a rušení). Dosažení bezpečné (referenční) sémantiky je zajištěno čtveřicí speciálních metod:
Tato čtveřice speciálních metod je povinná u všech objektů, které obsahují odkaz na data mimo ně (náš příklad) , resp. zapouzdřují prostředek operačního sytému. ●
●
42
generující konstruktor - vytváří nový objekt s novým obsahem (= objektem ShrArrayData a v něm zapouzdřeným polem) kopírovací konstruktor - vytváří nový objekt jako kopii původního (fyzicky se nic nekopíruje, jen se přidá další odkaz na již vytvořený objekt ShrArrayData). Je volán při předávání parametru hodnotou, definici s inicializací a při vrácení hodnoty z funkce
DArray - základní návrh II ●
●
●
43
přiřazovací operátor - kopíruje obsah do již inicializovaného objektu (z levé na pravou stranu přiřazení). Nejdříve uvolní původní obsah měněného objektu (= uvolňuje odkaz na sdílená data) a pak odkaz přesměruje na nová sdílená data (podobně jako kopírovací konstruktor) destruktor - je volán při zániku objektu pole (= u objektů ve statických proměnných po ukončení funkce main, u automatických při opuštění bloku, u dynamicky alokovaných při dealokaci pomocí delete). Destruktor musí uvolnit sdílený objekt (= snížit počet odkazů resp. objekt nakonec destruovat). Destruktory jsou na rozdíl od C# v C++ důležité (užívají se i v případě, odkazů na jiné objekty nejen na prostředky OS) ostatní metody jsou triviální, neboť jen delegují svou funkci na sdílený obsah (jen u i indexace je navíc předefinování operátoru) tj. elegantnější rozhraní (funkčně však stejné)
DArray - deklarace ●
deklarace je stručná. Všimněte si především datového členu - ukazatele na externí dynamicky alokovaný objekt.
class DArray { ShrArrayData *sdata; generativní konstruktor
public: DArray(int size); DArray(const DArray& s); ~DArray();
kopírovací konstruktor parametrem musí být reference na objekt stejné třídy
destruktor (musí být bezparametrický)
}; 44
const DArray& operator= (const DArray& rv); int& operator[](int index){ return sdata->getItemRef(index);} int length() const {return sdata->length();}
DArray - speciální metody DArray::DArray(int size) { sdata = new ShrArrayData(size);} DArray::DArray(const DArray& s) { sdata = s.sdata; sdata->acquire(); } DArray::~DArray() { if(sdata->release()) delete sdata; } const DArray& DArray::operator= (const DArray& rv) { if(sdata->release()) delete sdata; sdata = rv.sdata; sdata->acquire(); } 45
DArray - kompletní kód ●
46
následující kód obsahuje kompletní implementaci obou tříd a testovací kód (všimněte si jak je zajištěno vypsání kontrolní statické proměnné totalRefs po skončení funkce main)
Dynamická alokace objektů I ●
operátor new lze kromě alokace polí použít i pro dynamickou alokaci jednotlivých objektů. V tomto případě lze uvést i příslušné parametry konstruktoru (v případě polní verze je pro položky vždy použita bezparametrická verze konstruktoru)
DArray *da = new DArray(5); ●
objekt je následně přístupný nepřímo pomocí ukazatele. Při volání metod (a přístupu k datovým členům) se používá operátor -> namísto operátoru tečka:
např. ds->length() což je syntaktická zkratka za (*ds).length(). ●
po ukončení životnosti objektů je objekt uvolněn pomocí operátoru delete (v rámci něj je volán i případný destruktor)
delete da; ●
47
není možné zaměňovat delete a jeho polní verzi (delete [])
Dynamická alokace objektů II ● ●
kdy používat dynamickou alokaci objektů? u objektů s referenční sémantikou (= identita objektů ≈ identitě umístění v adresovém prostoru). Zde však lze referenční sémantiku zapouzdřit do hodnotového objektu (viz DArray) –
●
u složitěji strukturovaných objektů (jednotlivé části jsou dynamicky alokovány na požádání –
●
např. stromy, organizační jednotka (jednotliví zaměstnanci jsou dynamicky, GUI objekty
u objektů polymorfních tříd (polymorfismus včetně pozdní vazby je plnohodnotně možný jen u dynamicky alokovaných objektů) –
48
např: GUI objekty (okna, tlačítka), proudy apod.
GUI objekty (opět), bussines třídy apod.
Přetěžování operátorů I ●
●
●
49
mechanismus přetěžování operátorů v C++ je podobný stejnému mechanismu v jazyce C# shodné je především omezení: přetěžovat lze jen existující operátory (a to ještě ne všechny), nelze měnit jejich prioritu ani asociativitu základní rozdíly: –
operátory se v C++ definují jako instanční metody resp. výjimečně jako externí funkce (v C# vždy jako statické metody)
–
v C++ neexistuje automatické odvození příbuzných operátorů (v C# stačí např. definovat jen operátor "+" a tím jsou definovány i odvozené operátory inkrementace a složeného přiřazení +=, v C++ se musí definovat všechny tři resp. operátor inkrementace ve dvou podobách)
–
některé operátory lze předefinovat jen v C++ (->, [], volání)
Přetěžování operátorů II ●
●
základní pravidlo: přetěžování je vhodné jen pro sémanticky velmi omezenou skupinu tříd (především matematické objekty). Uplatnění v praxi tak není kromě základní domény příliš velké (jak např. definovat operátor "*" pro faktury?) unární operátory se definují jako bezparametrické metody Zlomek Zlomek::operator-() const; Zlomek Zlomek::operator++(); //preinkrementace Zlomek Zlomek::operator++(int); //postinkrementace
●
binární operátory jako metody s jedním parametrem (levý operand je předán jako this
Zlomek Zlomek::operator-(const Zlomek& rightop) const; Zlomek Zlomek::operator| (const Zlomek& rightop) const; (jakou to má sémantiku?, primárně je to bitová operace OR) 50
Přetěžování operátorů III ●
pokud není pravý operand třídou resp. je třídou uzavřenou (tj. např. knihovní třídou, v níž není operátor definován, ale kterou chceme rozšířit) je možno operátor definovat pomocí běžné funkce (s dvěma parametry) Zlomek operator+ (int leftop, const Zlomek& rightop) ostream& operator<< (ostream& out, const Zlomek& z); speciální pravidla platí pro přetěžování následujících operátorů:
zde může být jen reference (tj. nelze použít ani předání hodnotou ani ukazatelem)
new, delete (lze je přetěžovat jako metody pro řízení dyn. alokace objektů dané třídy, nebo jako funkci pro globální alokaci)
operátoru volání funkce (objekt se následně syntakticky chová jako funkce), lze předefinovat pro různé parametry 51
Hodnotové přetypování ●
●
pro hodnotové přetypování mezi objekty (tj. pro popis implicitní resp. explicitní konverze jednoho objektu na nový objekt jiného typu-třídy) lze použít: přetypovací konstruktor (u cílového typu) Zlomek::Zlomek(int i); //implicitní přetypování int -> Zlomek explicit Zlomek::Zlomek (int); // je nutné explicitní přetypování (Zlomek) 2 resp. Zlomek(2)
●
přetypovací operátor (u zdrojového typu) Zlomek::operator double (); //přetypování Zlomek -> double // použije se např. ve výrazu 2.0 + Zlomek(1/2) poznámka: pro explicitní hodnotové přetypování lze kromě klasického přetypovacího operátoru (v infixové i konstruktorové notaci) použít i novější static_cast.
52
Dědičnost ●
●
●
●
53
omezíme-li se na jednoduchou dědičnost pak je model C++ velmi blízký jazyku C# (model C++ je trochu jednodušší) dědění je relací (uspořádáním) mezi třídami. Podporuje znovupoužití kódu a poskytuje objektový polymorfismus (na rozdíl od C# je dědičnost jediným prostředkem polymorfismu, implementační rozhraní nejsou přímo podporována) objekty odvozené třídy dědí datové členy objektů třídy bázové a odvozená třída může přidávat další (na úrovni dat je dědičnost postupným skládáním = rozšiřováním) objekty odvozené třídy dědí i metody (signaturu + rozhraní + kontrakt), odvozené třídy však mohou metody překrýt (=předefinovat) resp. jen zastínit (je podporována statická i pozdní vazba). Nedědí se konstruktory.
Dědičnost - příklad I ●
základní třída Není odvozena ze žádné další třídy (v C++ neexistuje žádná kořenová univerzální nadtřída tj. obdoba třídy Object). Třída je abstraktní, i když to není explicitně vyznačeno, obsahuje však abstraktní metodu (v C++ názvosloví čistě virtuální)
class Message { protected: string msg; public: Message(const string& text) : msg(text) {} void printMessage() {cout << msg;} virtual void sendMessage() = 0; čistě virtuální metoda }; 54
= abstraktní místo těla je konstrukce = 0
Dědičnost - příklad II ● ●
odvozená (neabstraktní) třída je použito tzv. veřejné odvození tj. všechny zděděné metody si zachovají svůj přístup (především veřejné zůstávají veřejnými) ostatní typy se používají zřídka (nejedná se v zásadě o dědičnost ale o skládání, neboť původní rozhraní není přístupné)
class LocalMessage : public Message { protected: zastínění metody (statická vazba = string username; pravděpodobně sémant. chyba) public: LocalMessage(const string& text, const string& to) volání : Message(text), username(to) {} konstruktoru void printMessage() {cout << username << ":" << msg;} nadtřídy předefinování (překrytí) metody virtual void sendMessage() {...} }; 55
(virtual je zde nepovinné, důležité je u předka)
Vícenásobná dědičnost ●
●
●
56
stejně jako některé další OOP jazyka podporuje C++ vícenásobnou dědičnost, tj. schopnost třídy dědit z více nezávislých tříd. vícenásobná dědičnost na jedné straně dovoluje lépe modelovat realitu (a lépe více uplatňovat znovupoužitelnost), na druhé straně vznikají velké problémy s kolizí identifikátorů resp. signatur (třída může zdědit dvě stejnojmenné metody s různým kontraktem) a vícenásobným výskytem dat bázové podtřídy. Řešení jsou jen dílčí a navíc výrazně komplikují návrh (a popis) jazyka při rozumném využívání vícenásobné dědičnosti se však lze těmto problémům vyhnout (a neztratit se ve specifikaci jazyka).
Vícenásobná dědičnost II ●
v zásadě lze rozlišit čtyři základní metodiky dědičnosti (které jsou navíc alespoň částečně kombinovatelné) 1) dědění čistě abstraktních tříd = odpovídá implementačním rozhraním v Javě a C# (existuje jen problém s kolizí) 2) dědění abstraktních tříd s rozšiřujícími metodami (= metody využívající jen abstraktních tříd rozhraní). Stejně bezpečné jako u čistě abstraktních tříd + omezená znovupoužitelnost) 3) vícenásobná dědičnost jako skládání: nejvhodnější metoda v případě, že nehrozí vícenásobný výskyt objektů podtřídy resp. je tento výskyt cílem. 4)virtuální vícenásobná dědičnost: řeší problém vícenásobné dědičnosti je však nejsložitější.
57
Čistě abstraktní třídy = rozhraní ●
čistě abstraktní metody obsahují jen abstraktní metody. Mají tedy stejnou funkci jako konstrukce interface v Javě a C# (mohou obsahovat i statické konstantní členy) class Zobrazitelný { void Zobraz() = 0; void Skryj() = 0; }
●
58
ve standardní knihovně se však prakticky žádné čistě abstraktní třídy (rozhraní) nevyskytují (a to ani pro základní koncepce jako Comparable, Equatable apod.) STL je spíše založena na šablonách a nikoliv na dědičnosti (proto hraje v C++ větší roli statický ekvivalent impl. rozhraní tzv. koncept)
Rozšířená rozhraní ●
●
●
59
čistě abstraktní třídy nepodporují znovupoužitelnost a proto vedou v některých případech k rozsáhlé duplicitě kódu v C++ lze však vytvářet i abstraktní třídy, které obsahují implementace některých metod, jsou však z hlediska vícenásobné dědičnosti stejně bezpečné jako třídy čistě abstraktní (tj. nehrozí nejednoznačnost dědičnost při dědění více cestami). Implementované (=neabstraktní) metody však musí splňovat následující omezení: –
nesmí přistupovat k datovým členům (žádné ani nemají), místo toho volají příslušné abstraktní metody.
–
nesmí být předefinovány (či zastíněny) tj. měly by být nevirtuální.
class Zobrazitelný { .... // deklar. abstraktní metody Zobraz a Skryj void Problikni() {Zobraz(); sleep(1); Skryj()} }
Vícenásobné cesty dědičnosti ●
hlavním problém obecné vícenásobné dědičnosti je možnost existence více cest dědičnosti od bázové třídy kr třídě odvozené. Topologie dědičnosti může být složitá (vlastně jakýkoliv acyklický graf), ale nejjednodušším příkladem vícenásobných cest je tzv. kárové schéma [diamond schema] A
B
C
D ●
60
třída D je potomkem třídy A a to dvěma cestami (přes B nebo C).
Obecná vícenásobná dědičnost ●
●
●
61
obecná vícenásobná dědičnost je na úrovni dat skládáním (objekt je sjednocením datových položek bezprostředních nadtříd). Pokud nedochází k vícenásobnému dědění je tento přístup zcela přirozený (navíc zděděné metody, vždy pracují nad svou dílčí částí objektu) v případě vícenásobných cest však dochází k duplikacím zděděných dat (jeden výskyt na každou cestu). U kárového schématu jsou tak například v objektu třídy D zahrnuty dvě kopie dat bázové třídy A. v některých případech je tento efekt v souladu s modelem (= představou implementátora). Je to však dosti výjimečné a omezené jen na jednoduché topologie (= např. kárové schéma). Klasickým případem jsou vstupně-výstupní proudy (i když i ty je lepší řešit pomocí skládání + adaptéru)
Obecná vícenásobná dědičnost II ●
62
UML class diagram zjednodušené implementace IO streamů
Obecná vícenásobná dědičnost III ●
●
objekty třída InputOutputStream zapouzdřují dva otevřené nízkoúrovňové proudy (souborové deskriptory), které jsou na sobě nezávislé. Navíc jsou nad nimi dva nezávislé buffery (zděděné z nadtříd InputStream a OutputStream) následující příklady ukazují možnosti volání zděděných metod na objektu třídy InputOutputStream:
io->Read(); //jednoznačné io -> open("test.dat"); //volá se překrytá verze přímo i InputOuputStream (ta pravděpodobně volá obě zděděné)
io -> InputStream::open("test.dat") //volá se verze nad InputStream io -> close(); //syntaktická chyba io -> InputStream::close(); //volá se verze zděděná ze Stream pracující nad deskriptorem zděděným skrze InputStream 63
Obecná vícenásobná dědičnost IV ●
●
64
výše uvedený příklad má několik dalších sémantických požadavků (resp. dokonce omezení): –
metoda close by měla být překryta i ve třídě InputOutputStream (měly by uzavírat oba dílčí proudy)
–
musí být ošetřen přístup resp. uzavření neotevřeného dílčího proudu (např. pokud je otevřen je zapisovací a je provedeno čtení).
někdy je také nutno znát i pořadí vyhodnocování konstruktorů. Ty se vyhodnocují podle algoritmu prohledávání do hloubky, kde pořadí na jednotlivých vrstvách je dáno pořadím nadtříd v hlavičce třídy (zleva doprava). Některé konstruktory jsou samozřejmě prováděny vícekrát (např. u kárového schématu v pořadí: ABACD). Sémantika by však na tomto pořadí neměla pokud možno záviset.
Virtuální dědičnost I ●
●
●
jazyk C++ nabízí navíc i prostředek, který zajistí jedinečný výskyt datových členů podtřídy i při dědění více cestami. formálně se označuje jako virtuální dědění. Toto označení je dáno použitým klíčovým slovem, je však poněkud zavádějící (rozhodně nemá nic společného s virtuálními metodami). Lepší termínem by snad byla referenční dědičnost. formální použití: v místě, kde dochází k rozvětvení cest dědičnosti (které se pak někde spojí či potenciálně spojí) jsou nadtřídy uvozeny slovem virtual. class A {}; class B : virtual public A {}; //zde se dědičnost větví class C : virtual public B {}; //zde také class D : public B, public C {}; //zde se spojují (bez virtual!)
65
Virtuální dědičnost II ●
interní implementace: při virtuálním odvození se nový objekt nevytvoří složením datových členů nadtřídy a podtřídy, ale složením (interní neviditelné) reference na data podtřídy a dat nadtřídy. Při spojení cest je pak možno sloučit data nadtřídy (opakují se jen reference ne data) normální dědičnost
66
virtuální dědičnost
třída A:
A
třída B:
A +B
&A +B A
třída C:
A +C
&A +C A
třída D:
A +B A +C +D
&A +B &A +C A +D
A
Virtuální dědičnost II ●
●
●
virtuální dědičnost komplikuje representaci objektů a snižuje jejich prostorovou a časovou efektivitu (zaujímají více místa a přístup ke členům je pomalejší) komplikují překladač (a tím jej zpomalují). Komplikují rutiny pro dynamické přetypování a dynamickou identifikaci typů. zesložiťují popis jazyka (musí totiž řešit i různé okrajové podmínky). Například musí být vloženo zvláštní pravidlo pro volání konstruktorů virtuálních nadtříd (ty se volají dříve než konstruktory běžných nadtříd opět v pořadí prohledávání do hloubky, v károvém schématu v pořadí: ABCD)
nepoužívejte virtuální dědičnost příliš často (lépe je se jí zcela vyhnout) a pokud ano tak v optimálně (nadbytečné označení nadtříd jako virtuálních může vše výrazně zkomplikovat) 67
Přetypování I ●
●
jazyk C byl jazyk se slabou typovou kontrolou, tj. hodnoty (především elementární typů a ukazatele) byly konvertovatelné na na jakýkoliv jiný datový typ a to často i implicitně (= bez nutnosti použít přetypovací operátor) jazyk C++ má již silnější typovou kontrolu, ale s použitím explicitního přetypovacího operátoru lze opět přetypovávat téměř bez omezení:
(DArray&) 2.56; ●
proto standard C++ obsahuje i novou sadu přetypovacích operátorů, které jsou sémanticky omezené a tudíž i v daném kontextu bezpečnější. Tři tvoří řadu s postupným uvolňováním omezení (vždy nejlepší použít ten nejstriktnější), jeden je specializovaný (a stojí mimo).
dynamic_cast ↔ static_cast ↔ reinterpret_cats (const_cast) 68
Přetypování II ●
●
●
69
dynamic_cast přetypování v rámci hierarchie dědičnosti mezi ukazateli a referencemi na základní a odvozené třídy (především ve směru bázová třída → odvozená třída). Provádí se běhová typová kontrola. Při neúspěchu se vrací NULL ukazatel (u referencí se vyhodí výjimka bad_cast) static_cast přetypování v rámci hierarchie dědičnosti bez dynamické kontroly (vždy se povede), číselná přetypování, přetypování mezi třídami daná přetypovacími operátory a konverzními konstruktory reinterpret_cast libovolná přetypování, typicky mezi ukazately a referencemi na různé (nepříbuzné) typy, mezi čísly a ukazately, mezi strukturami a třídami (konverze na úrovni bytů)
Přetypování III ●
const_cast přetypování konstantních ukazatelů a referencí na nekonstantní. Nemělo by se objevovat (obcházení zabezpečení), ale pokud je metoda chybně navržena (chybí const u metody či referencí), pak je jediným řešením (ostatní přetypovací operátory kromě obecného nefungují). Lze kombinovat s ostatními cast-operátory. Je použitelné i pro přetypování volatile ukazatelů/referencí.
const_cast(dynamic_cast(zvire))
70