C++ objektově orientovaná nadstavba programovacího jazyka C (2. část) Josef Dobeš Katedra radioelektroniky (13137), blok B2, místnost 722 dobes@fel.cvut.cz 18. května 2015 České vysoké učení technické v Praze, Fakulta elektrotechnická 1
1 Dynamická alokace proměnných, struktur a poĺı Dynamická alokace proměnných (nejčastěji struktur) a jejich poĺı se v C++ nejčastěji provádí pomocí operátorů new, delete a delete [ ]. Při dynamickém alokování zejména rozsáhlých datových struktur je však třeba pracovat s určitou ideou obsazování paměti, jinak pamět ové nároky programu prudce rostou. Ještě častější chybou je neuvolňování dříve dynamicky alokované paměti anebo její nesprávné uvolňování, zejména nesprávné uvolňování dynamicky alokovaných poĺı. V následujícím programu bude demonstrováno nesprávné i správné uvolňování dynamicky alokovaných struktur. Pro jednoduchost je zvolena pouze jednoduchá struktura obsahující dvě proměnné float a příkazem typedef je definován ukazatel na tuto strukturu. Po prvním řádku programu main máme definovány dva ukazatele na deklarovanou strukturu, není však dané to, na co ukazují. Tuto situaci můžeme znázornit následujícím způsobem: Operátor new dává manažeru systémové paměti následující dvě instrukce: Alokuje potřebnou pamět (její velikost závisí na typu specifikovaném za operátorem) Uschová adresu v paměti do ukazatele, čímž ukazatel označuje umístění v paměti 2
#include <iostream> struct test_score { float midterm, final; ; typedef test_score * test_pointer; int main() { test_pointer score_ptr, another_score_ptr; score_ptr = new test_score; score_ptr->midterm = 99.0; score_ptr->final = 95.0; another_score_ptr = score_ptr; score_ptr = new test_score; score_ptr->midterm = 40.0; score_ptr->final = 35.0; std::cout << "1st midterm mark = " << another_score_ptr->midterm << ", 1st final mark = " << another_score_ptr->final << std::endl; std::cout << "2nd midterm mark = " << score_ptr->midterm << ", 2nd final mark = " << score_ptr->final; another_score_ptr = NULL; // nesprávný způsob likvidace ukazatele delete score_ptr; // správný způsob likvidace // implicitně je nacrácena hodnota (int)0 3
Po aplikaci prvního příkazu new je už jasná poloha struktury v paměti, nejsou však dosud definovány jednotlivé její prvky: Po následujících dvou přiřazovacích příkazech však již jsou tyto prvky definovány: a po přiřazení jednoho ukazatele druhému oba dva ukazatele označují stejné místo v paměti: 4
Po aplikaci druhého příkazu new a následujících dvou přiřazujících příkazů již oba ukazatelé označují jiné místo v paměti a taktéž obsahy obou struktur jsou již různé: Následující dva příkazy výstupu tuto skutečnost potvrzují: 1st midterm mark = 99, 1st final mark = 95 2nd midterm mark = 40, 2nd final mark = 35 Následující přiřazení nulového pointeru NULL je typickou a častou chybou v paměti zůstanou prvky struktury, které však již nejsou dosažitelné: Správný způsob likvidace struktury je proveden příkazem delete, po němž jsou pamět ová místa pro prvky struktury uvolněna: 5
2 Operátor delete [ ] Při uvolňování dynamicky alokovaného pole se používá také operátor delete, za který ovšem musíme přidat prázdné indexové závorky: delete [ ] array; Při uvolňování dynamicky alokovaného pole objektů se pro každý z prvků zavolá destruktor. Důležité poznámky: Pokud při uvolňování pole neuvedeme za kĺıčovým slovem závorky [ ], překladač nebude hlásit chybu. V případě pole objektů se ale zavolá destruktor pouze pro první prvek pole a záleží na konkrétní struktuře programu, co to způsobí. U některých (především starších) překladačů jazyka C++ je nutné do lomených závorek za delete připsat skutečný počet prvků (týká se to např. starších verzí překladače Borland C++) 6
3 Nezdařená alokace dynamické proměnné Norma ANSI/ISO jazyka C++ stanoví, že operátor new v případě neúspěchu (v typickém případě nedostatku paměti) vyvolá výjimku bad alloc. Výjimku lze tedy ošetřit pomocí bloků try a catch: try // v ANSI C++ { pld = new long double; catch(bad_alloc) { // řešení situace Chceme-li se vyhnout práci s výjimkami, použijeme operátor new s dodatečným parametrem nothrow: // v ANSI C++ pld = new(nothrow) long double; if (!pld) error(); 7
4 Předávání odkazem V jazyce C++ můžeme také předávat parametry funkcí odkazem. Před parametr, který chceme předat odkazem, musíme napsat znak &. V těle funkce se s takovým parametrem pracuje stejně jako s parametrem předaným hodnotou (použije se tzv. reference). Jazyk C++ je tedy v tomto ohledu bližší jazyku Fortran než jazyk C. Následující program přehodí obě proměnné: #include <stdio.h> void swap(int &a, int &b) { int c = a; a = b; b = c; int main() { int a = 5, b = 7; printf("%d %d\n", a, b); swap(a, b); printf("%d %d", a, b); V jazyce C bychom přehození obou proměnných museli řešit pomocí ukazatelů. 8
5 Odvozené třídy class student { public : enum year {junior, senior, grad; student(char *nm, int id); void print() const; protected : int student_id; char name[30]; ; class grad_student : public student { public : enum support {fellowship, other; grad_student(char *nm, int id, year x); void print() const; protected : char thesis[80]; ; V tomto příkladu je grad student odvozená třída a student je základní třída. Užitím kĺıčového slova public za dvojtečkou v záhlaví odvozené třídy říkáme, že členy protected a public ve třídě student jsou děděny jako protected a public do třídy grad student. Privátní členy nejsou dostupné. Nyní každý graduate student je student, ale student nemusí být graduate student. 9
6 Redefinice unárního operátoru V následujících dvou příkladech bude redefinován unární operátor ++ tak, aby byl použitelný pro správné sčítání vteřin, minut, hodin a dní. 6.1 Použití funkce třídy V tomto případě je aritmetika správného sčítání vteřin, minut, hodin a dní naprogramována funkcí třídy operator ++(), která používá další funkci třídy tick() a vrací data třídy pomocí vždy vytvořeného ukazatele this. Správná aritmetika je zajištěna výrazy konstruktoru a výpis dní, hodin, minut a vteřin zajišt uje funkce print(), která nemodifikuje data třídy. #include <iostream> #include <iomanip> using namespace std; class clock { public : inline clock(unsigned long i); //konstruktor a konverze na dny, hodiny, void print() const; //formátovaný tiskový výstup, nemodifikuje objekt void tick(); //přidá jednu vteřinu, tato tedy není const clock operator ++() { tick(); return *(this); //this ukazuje na objekt private : unsigned long tot_secs, secs, mins, hours, days; ; 10
inline clock::clock(unsigned long i) { //konstruktor nemá typ tot_secs = i; secs = tot_secs % 60; mins = (tot_secs / 60) % 60; hours = (tot_secs / 3600) % 24; days = tot_secs / 86400; void clock::tick() { clock temp = clock(++tot_secs); secs = temp.secs; mins = temp.mins; hours = temp.hours; days = temp.days; void clock::print() const { cout << setw(4) << days << " d:" << setw(2) << hours << " h:" << setw(2) << mins << " m:" << setw(2) << secs << " s" << endl; int main() { clock t1(59), t2(172799); // = dva dny minus jedna vteřina cout << "Pocatecni casy jsou..." << endl; t1.print(); t2.print(); ++t1; ++t2; cout << "Po pricteni jedne sekundy..." << endl; t1.print(); t2.print(); 11
6.2 Použití separátní funkce V tomto případě je unární operátor ++ redefinován pomocí funkce, která není členem třídy. První část programu je proto mírně odlišná od předcházející verze, druhá část programu je však přesně stejná jako předcházející strana, tj. clock unchanged.cpp (viz též dole). #include <iostream> #include <iomanip> using namespace std; class clock { public : inline clock(unsigned long i); void print() const; void tick(); private : unsigned long tot_secs, secs, mins, hours, days; ; /* Definice funkcí, která není členem třídy */ clock operator ++(clock& c) { //je využito odkazu c.tick(); return c; #include "clock unchanged.cpp" 12
6.2.1 Prefixový a postfixový operátor //Prefixová a postfixová forma přetížení operátoru ++: //Upravená verze Richarda Vachuly tak, aby fungovala pod Digital Mars i GCC #include <iostream> class Foo { int value; public : //Konstruktor: Foo(int val = 0) : value(val) { //Digital Mars musí val mít v () ne v { //Přetížení unárních operátorů Foo& operator++(); //prefix Foo operator++(int); //postfix //Přetížení binárního operátoru friend std::ostream& operator<<(std::ostream& outp, const Foo& f); ; Foo& Foo::operator++() { //unary prefix this->value++; return *this; Foo Foo::operator++(int) { //unary postfix Foo temp(*this); ++(*this); return temp; std::ostream& operator<<(std::ostream& outp, const Foo& f) { //binary return outp << f.value; 13
int main() { Foo foo(5); // std::cout << foo << std::endl; std::cout << ++foo << std::endl; std::cout << foo << std::endl; std::cout << foo++ << std::endl; std::cout << foo << std::endl; // return 0; dává očekávaný výsledek 5 6 6 6 7 7 Virtuální funkce Virtuální funkce umožňují v hierarchii tříd vytváření mimořádného polymorfismu. Třída, která obsahuje pouze virtuální funkce se nazývá abstraktní třída (takovou třídu lze deklarovat, ale ne definovat objekt této třídy). 14
V následujícím příkladu máme jednu abstraktní bázovou třídu, která obsahuje jednu standardní virtuální funkci a jednu čistou ( pure ) virtuální funkci. Čistá virtuální funkce musí být ve všech dceřiných třídách definována, zatímco u standardní virtuální funkce lze v odvozených třídách použít její implicitní ( default ) chování definované v rodičovské třídě. 15
#include <iostream> using namespace std; // kvúli cout const double PI = 3.14159; // zde nejde o přesnost výpočtu class shape { // abstraktní bázová třída, obsahuje pouze virtuální funkce public : virtual double area() const { return 0; // definováno "default" chování virtual char *get_name() = 0; // čistě virtuální funkce "pure" virtual protected : double x, y; // Digital Mars i GNU GCC fungují dobře i bez tohoto řádku ; class rectangle : public shape { // "Obdélník" odvozený z abstraktní třídy public : rectangle(double h, double w) : height(h), width(w) { // přiřazení délek double area() const { return height * width; char *get_name() { return (char *)"Obdelnik: "; // pro Digital Mars C++ private : // (char *) není nutné double height, width; // pro GNU GCC warning ; class circle : public shape { // "Kruh" odvozený z abstraktní třídy public : circle(double r) : radius(r) { // přiřazení poloměru double area() const { return PI * radius * radius; char *get_name() { return (char *)"Kruh: "; private : double radius; ; 16
Kromě funkcí tříd popisujících chování obdélníku a kruhu vytvoříme i jednodušší odvozenou funkci pro úsečku, která pro výpočet obsahu použije implicitní chování funkce rodičovské třídy. Čistě virtuální funkce však musí být definována. class line : public shape { // zde je použit implicitní konstruktor public : char *get_name() { return (char *)"Usecka: "; // musí být, jde o pure ; // bylo použito default chování pro výpo čet plochy, tj. nulová plocha Hlavní program pak jednoduše pracuje se všemi třemi tvary (obdélníkem, kruhem i úsečkou): int main() { shape *ptr_shape; //shape something; // chyba abstraktní bázovou t řídu nelze vytvořit přímo rectangle rec(4, 5.5); // toto je přímá inicializace výšky a šířky circle cir(10); // zde je přímá inicializace jen pro poloměr kruhu line lin; cout << "Tento program pouziva hierarchie trid pro tvary...\n" ; ptr_shape = &rec; cout << ptr_shape->get_name() << "plocha = " << ptr_shape->area() << '\n'; ptr_shape = ○ cout << ptr_shape->get_name() << "plocha = " << ptr_shape->area() << '\n'; ptr_shape = &lin; cout << ptr_shape->get_name() << "plocha = " << ptr_shape->area(); // Digital Mars i GNU GCC vracejí implicitn ě nulu, tj. return nemusí být 17
Pokud bychom se pokusili definovat objekt rodičovské třídy, která je abstraktní, dostali bychom chybové hlášení: shape something; ^ virtual.cpp(40) : Error: cannot create instance of abstract class 'shape' --- errorlevel 1 Chybové hlášení bychom rovněž obdrželi, pokud bychom v odvozené třídě line nedefinovali funkci get name(): line lin; ^ virtual.cpp(43) : Error: cannot create instance of abstract class 'line' --- errorlevel 1 Nakonec uvedeme výstup programu pracujícího s virtuálními funkcemi: Tento program pouziva hierarchie trid pro tvary... Obdelnik: plocha = 22 Kruh: plocha = 314.159 Usecka: plocha = 0 18
8 Šablony Dalším nástrojem umožňujícím značný polymorfismus jsou šablony. Pomocí šablon můžeme vytvořit kód, který je použitelný pro různé datové typy. V následujícím příkladu jsou demonstrovány nejrůznější způsoby vzájemných záměn. Několik datových typů můžeme zaměňovat pomocí šablony, řetězce je však třeba zaměňovat pomocí zvláštní funkce se stejným jménem: #include <iostream> #include <string.h> // Digital Mars připouští i #include <string> tj. bez h #include <complex> //using namespace std; // kvůli rozlišení swap a std::swap NELZE zde použít template < class T > void swap(t& x, T& y) { T temp; temp = x; x = y; y = temp; void swap(char *s1, char *s2) { int max_len = (strlen(s1) > strlen(s2))? strlen(s1) : strlen(s2); char *temp = new char[max_len + 1]; strcpy(temp, s1); strcpy(s1, s2); strcpy(s2, temp); delete [] temp; 19
V hlavním programu pak vyzkoušíme širokou škálu záměn nejrůznějších datových typů: int main() { int k = 5, l = 9; std::complex < double > a = 3.14, b = 8; // pro jeden reálný argument je % // definováno v _complex.h jako přiřazení reálné části, imaginární bude 0 std::complex < float > c = 4, d = 7; // komplexní proměnné poloviční délky char str1[6] = "Brno", str2[6] = "Praha"; // stejná délka [6] kvůli záměně /*"Záměna proměnných int"*/ swap(k, l); // přímá záměna dvou proměnných int první funkcí std::cout << k << " " << l << std::endl; // i u endl se musí uvádět std:: /*"Záměna řetězců"*/ swap(str1, str2); // vyvolá se druhá definovaná funkce záměny řetězců std::cout << str1 << " " << str2 << std::endl; /*"Záměna prvního znaku v řetězci"*/ swap(*str1, *str2); // přímá záměna prvního prvku pole ([0]) první funkcí std::cout << str1[0] << " " << str2[0] << std::endl; // *str1 == str1[0] /*"Záměna komplexních proměnných"*/ std::swap(a, b); // swap je člen namespace std, swap bez std nelze použít std::cout << a << " " << b << ", velikost a = " << sizeof(a) << std::endl; std::swap(c, d); std::cout << c << " " << d << ", velikost c = " << sizeof(c); 20
Komplexní čísla je však nutné vzájemně vyměňovat pomocí std::swap; pokud bychom použili jen funkci swap, dostaneme chybové hlášení: swap(c, d); ^ template.cpp(41) : Error: ambiguous reference to symbol Had: swap(t&,t&) and: std::swap(_tp&,_tp&) --- errorlevel 1 Výstup tohoto programu demonstrujícího záměny dat pomocí šablon je tedy následující: 9 5 Praha Brno B P (8,0) (3.14,0), velikost a = 16 (7,0) (4,0), velikost c = 8 21
Obsah 1 Dynamická alokace proměnných, struktur a poĺı 2 2 Operátor delete [ ] 6 3 Nezdařená alokace dynamické proměnné 7 4 Předávání odkazem 8 5 Odvozené třídy 9 6 Redefinice unárního operátoru 10 6.1 Použití funkce třídy.............................. 10 6.2 Použití separátní funkce............................ 12 6.2.1 Prefixový a postfixový operátor.................... 13 7 Virtuální funkce 14 8 Šablony 19 22