Język C++
Transkrypt
Język C++
Język C++ historia, współczesność, przyszłość Język C++ jest wieloparadygmatowym językiem programowania. Stworzony w latach osiemdziesiątych XX wieku przez Bjarne Stroustrupa •C++98 ISO/IEC 14882:1998 •C++03 ISO/IEC 14882:2003 •C++11 ISO/IEC 14882:2011 •C++14 ISO/IEC 14882:2014 C++11/14 – dlaczego standard jest ważny? Standard to brak zależności od • rodzaju kompilatora • systemu operacyjnego • CPU Standard odwołuje się / opisuje działanie abstrakcyjnej maszyny. Kompilator ma za zadanie zrealizować ten opis na konkretnym sprzęcie. C++98/C++03 – abstrakcyjna maszyna była jednowątkowa C++11/C++14 – abstrakcyjna maszyna zaprojektowana jako wielowątkowa – model pamięci (organizacja pamięci i sposoby dostępu do pamięci) – na niskim poziomie gwarantowane operacje atomowe w określonej kolejności C++ podstawowe cechy • Główne cechy języka: • język kompilowalny, ogólnego przeznaczenia, określany jako język „średniego poziomu” – dokument opisujący standard C++11 ma 1338 stron • silna (statyczna) kontrola typów podczas kompilacji: pewna forma weryfikacji poprawności kodu, pozwalająca na wczesne wykrycie błędów lub niezamierzonego działania • język swobodnego formatu, rozmieszczenie znaków na stronie nie ma znaczenia, ale każda instrukcja musi być zakończona średnikiem ; • C++ nie wspiera własności specyficznych dla danej platformy lub niebędących własnościami ogólnego przeznaczenia C++ style programowania • C++ nie narzuca żadnego stylu, daje programiście możliwość wyboru. • programowanie proceduralne: organizowanie kodu w postaci procedur, wykonujących ściśle określone operacje, dane nie powiązane z procedurami, jako parametry wywołania procedur • programowanie obiektowe: zbiór obiektów komunikujących się pomiędzy sobą w celu wykonywania zadań, obiekt to element łączący stan (dane) i zachowanie (metody)… programowanie funkcjami wirtualnymi • programowanie uogólnione: kod programu bez wcześniejszej znajomości typów danych, szukanie i systematyka abstrakcyjnych reprezentacji efektywnych algorytmów, struktur danych i innych elementów programowych… programowanie szablonami C++ literatura (1) – kanon literatury International Standard (można kupić – cena zaporowa) ISO/IEC 14882:2011(E) C++11 Final Document www.open-std.org/jtc1/sc22/wg21/ N3290 (2011-04-11) Bjarne Stroustrup • Język C++ • Programowanie. Teoria i praktyka z wykorzystaniem C++ (Wyd. II popr.) C++ literatura (2) – „stare ale jare” (niestety, nie C++11) Bruce Eckel Thinking in C++, vol. I i II (po angielsku – on-line) Jerzy Grębosz • Symfonia C++ Standard (C++03) • Pasja C++ (niestety stare) C++ literatura (3) Siddhartha Rao C++. Dla każdego. Wydanie VII Nicholas A. Solter, Scott J. Kleper C++ Zaawansowane programowanie Wydanie III po angielsku Stephen Prata Język C++. Szkoła programowania. Wydanie VI C++ literatura (4) Anthony Williams Język C++ i przetwarzanie współbieżne w akcji D. Ryan Stephens C++ Receptury (O’Reilly) Nicolai M. Josuttis C++. Biblioteka standardowa. Podręcznik programisty David Vandevoorde, Nicolai M. Josuttis C++ szablony. Vademecum profesjonalisty Wydanie II po angielsku Aktualizacja w roku 2015 C++ literatura (5) Scott Meyers – „C++ 50 efektywnych sposobów na udoskonalenie Twoich programów” – „Język C++ bardziej efektywny” – „STL w praktyce: 50 sposobów efektywnego wykorzystania” Herb Sutter KURSY DOSTĘPNE ON-LINE Karol „Xion” Kuczmarski – Kurs C++ (Megatutorial) Sektor van Skijlen – C++ bez cholesterolu Piotr Białas, Wojciech Palacz – Zaawansowane C++ pl.wikibooks.org/wiki/C++ – niekompletny jeszcze… Frank B. Brokken – C++ Annotations Ver. 9.8.x – „Wyjątkowy język C++ 47 łamigłówek…” – „Wyjątkowy język C++ 40 nowych łamigłówek…” – „Niezwykły styl języka C++ 40 nowych łamigłówek…” – „Język C++ Standardy kodowania 101 zasad…” (współautor: Andrei Alexandrescu) Twój edytor i kompilator • Używanie gcc zamiast g++ – GCC (GNU Compiler Collection) kompiluje różne języki (C, C++, Objective-C, Objective-C++, Java, Fortran, Ada). gcc rozpoznaje kod źródłowy C++ po rozszerzeniach: .C, .cc, .cpp, .CPP, .c++, .cp, .cxx – gcc nie konsoliduje skompilowanego kodu z biblioteką standardową c++ – jeśli użyjesz gcc to będziesz musiał podać ręcznie ścieżkę do plików nagłówkowych oraz do biblioteki standardowej! Nie utrudniajmy sobie życia i używajmy g++ W przypadku Dev-C++ chodzi o zapisanie pliku źródłowego z rozszerzeniem .cpp Kompilator i konsolidator: gcc.gnu.org (g++) • Kompilator g++ – tłumaczy kod źródłowy na język assembler lub rozkazy komputera • Konsolidator g++ (linker) – dopasowuje odwołania symboli do ich definicji • Najnowsze wersje wspierają standard C++11 (wersja 4.9.1), a także C++14 • Program zarządzający bibliotekami ar, ranlib (archiver, librarian) – biblioteki statyczne .a (oraz pliki obiektowe .o) wymagane do skonsolidowania pliku wykonywalnego podczas linkowania – biblioteki dynamiczne .so zawierają kod maszynowy ładowany do pamięci po uruchomieniu wykorzystującego je programu, muszą więc być dostępne podczas uruchamiania programu • Warto zapoznać się z narzędziem make Kompilator – wsparcie nowego standardu Kompilowanie kodu według nowego standardu Wsparcie kompilatora dla standardu C++11 (C++14) wymaga dodatkowej opcji (flagi): g++ -std=c++11 … g++ -std=c++14 … Przykładowo (linux): Program jest w katalogu /usr/bin Pliki nagłówkowe w katalogu /usr/include/c++/4.8 Biblioteki w katalogu /usr/lib/gcc/i486-linux-gnu/4.8 Na pracowni komputerowej kompilator g++ 4.92 i nowszy jest dostępny tylko w nowych instalacjach (Dev-C++ 5.11) Kompilowanie i linkowanie (konsolidacja) Prosty program o nazwie myprog z pliku prog1.cc g++ -std=c++11 -o myprog prog1.cc // to samo: g++ -std=c++11 prog1.cc -o myprog Plik obiektowy (bez konsolidacji do programu) g++ -std=c++11 -c -o prog1.o prog1.cc Konsolidacja do programu wykonalnego g++ -std=c++11 -o myprog prog1.o Uruchomienie programu (linux) ./myprog gdzie „ . ” (kropka) oznacza pełną nazwę ścieżki, chyba że ścieżka do katalogu z programem jest w zmiennej PATH Kompilatory – kilka uwag Można mieć zainstalowane kilka wersji g++ oraz biblioteki standardowej. Napisz: g++ i postukaj „tab” (pokażą się wszystkie programy zaczynające się na g++) Zwykle g++ to link symboliczny do jednej z wersji. Sprawdzenie wersji: g++ -v // lub: g++ --version Inne kompilatory warte uwagi: clang ver. 3.7, Intel C++ ver. 15, Microsoft Visual Studio C++ 2015 Kompilowanie plików nagłówkowych • • • • niektóre kompilatory pozwalają na prekompilowanie plików nagłówkowych, w dużych projektach znacznie może to przyspieszyć proces kompilacji g++ kompilując plik .h tworzy plik z rozszerzeniem .h.gch prekompilowany plik jeśli znaleziony, może być brany jako pierwszy przed plikiem .h student robi to zwykle przez pomyłkę, niepotrzebnie umieszczając na liście plików źródłowych do kompilowania również pliki .h (może to prowadzić do zaskakujących problemów w stylu „edytuję plik .h i nic się nie dzieje”) Pierwszy program Program wymaga napisania funkcji: auto main() -> int { } lub int main() { } W kodzie – tylko jedna funkcja main. Uwaga: dawniej (przed rokiem 1998) dopuszczano postać funkcji zwracającej void (tzn. „nic”), teraz musi zwracać int. Paradoksalnie, jest to jedyna funkcja, w której (skoro „coś” zwraca) nie trzeba pisać instrukcji „return”. Można (ale nie trzeba) jawnie napisać: int main() { return 0; } Pierwszy program: średnik Uważaj na średnik – są miejsca, w których średnik jest konieczny, a są, w których jest zbędny lub nieprawidłowy. int fun3() { Nie stawiaj średnika za nawiasem kończącym return 2; definicję funkcji – jest niepotrzebny! }; #include ”mojplik.h” ; #define FLAGA ; class Klasa { } ; Nie stawiaj średnika na końcu dyrektywy preprocesora – to jest błąd! Średnik konieczny jest na końcu definicji klasy! Przykład kompilującego się for (int i=0; i<10; ++i); kodu, w którym przez pomyłkę // tutaj instrukcja, którą może ktoś mamy niezamierzone działanie… // zamierzał wykonać 10 razy… Pierwszy program – który coś robi #include <iostream> using namespace std; int main() { cout << "I am Jan B. " << "za zycia napisalem ponad " << 100 << " ksiazek!\n"; cout << "A Ty ile napisales: "; int liczba; cin >> liczba; if (liczba < 100) cout << "\n…Tak malo!"; return 0; // return EXIT_SUCCESS } #include <iostream.h> // NIE UŻYWAĆ czasem implementowane tak: #include <iostream> using namespace std; w nowych kompilatorach ostrzeżenia, a nawet może się nie skompilować Można też wybrać konkretne using std::cout; using std::cin; Albo podczas wywołania pisać std::cout oraz std::cin itd. Biblioteki z C: #include <cstdlib> #include <cstdio> #include <cassert> Zajrzyjmy w głąb <iostream> extern istream cin; extern ostream cout; extern ostream cerr; extern ostream clog; Deklaracje obiektów odpowiadających za pracę na strumieniu wejście / wyjście. Obiekty konstruowane przed main() Hierarchia klas odpowiedzialnych za pracę na strumieniach #include <ostream> #include <istream> cin – obiekt odpowiedzialny za obsługę standardowego strumienia wejściowego (zwykle powiązanego z klawiaturą), wywołanie powoduje opróżnienie buforu cout cout – obiekt odpowiedzialny za strumień wyjściowy (zwykle powiązany z monitorem) cerr – obiekt standardowego strumienia komunikatów o błędach, powiązany przez system z monitorem, strumień niebuforowany clog – obiekt wyprowadzany standardowo tak jak cerr, strumień buforowany Operatory, manipulatory, znaki specjalne operator<< oraz operator>> są to operatory przesunięcia bitowego, jednak dla obiektów strumienia są przeciążone i stają się „operatorami wejścia/wyjścia” Co lepiej na końcu: std::endl czy \n ? MANIPULATORY ( tak naprawdę funkcje) endl – dodaje do buforu znak ’\n’ oraz wykonuje flush – opróżnienie buforu ends – wkłada znak kończący łańcuch znakowy, czyli symbol zerowy ’\0’ flush – opróżnia bufor ws – czyta i ignoruje białe znaki Kod dla guru (przykład): ostream& ostream::operator<< ( ostream& (*op) (ostream&) ) { return (*op) (*this); } std::ostream& std::endl (std::ostream& s) { } s.put('\n'); s.flush(); return s; Można tak: std::cout << std::endl; lub tak: std::endl ( std::cout ); ZNAKI SPECJALNE (stałe znakowe) \n nowa linia \r powrót do początku linii \t pozioma tabulacja \a alarm dźwiękowy \0 symbol zerowy (koniec łańcucha) MNIEJ UŻYWANE \v pionowa tabulacja \b powrót o jedną pozycję \f nowa strona (drukarka) \? znak zapytania KONIECZNE W ŁAŃCUCHU ZNAKOWYM \\ lewy ukośnik \’ apostrof \” cudzysłów Deklaracja – co to jest? Deklaracja – to wprowadzenie w danej jednostce translacji (pliku) nazwy (lub nazw), albo redeklaracja nazw wprowadzonych poprzednimi deklaracjami. Deklaracje generalnie określają jak mają być rozumiane dane nazwy. Deklaracja może być też definicją, chyba że (i wtedy są to tylko deklaracje): • deklarujemy funkcję bez definiowania jej ciała void fun( double d, short n, int ); nie ma definicji (ciała) funkcji, czyli części ujętej w nawiasy { } to jest to tylko deklaracja • deklaracja poprzedzona jest specyfikatorem extern, w znaczeniu obiektu zdefiniowanego w innym pliku extern double d; deklarujemy, że w innym pliku będzie zdefiniowana zmienna typu double o nazwie d. UWAGA: jeśli użyjemy specyfikatora extern oraz inicjalizujemy zmienną, np. extern double d = 3.14; to oznacza to już definicję a nie deklarację! • deklaracja z użyciem extern jako sposób konsolidacji (linkowania) kodu extern ”C” int fun ( float ); extern ”C” { /* tutaj lista deklaracj */ } gdy chcemy „zlinkować” z kodem z innego języka, musimy zadeklarować nazwy obiektów tam zdefiniowanych Deklaracje (2) • deklarujemy statyczną składową w definicji klasy class Foo { static int n; }; zmienna statyczna w definicji klasy to dopiero jej deklaracja – jak się później dowiemy, taką zmienną definiuje się dopiero poza ciałem klasy • deklarujemy nazwę klasy (bez jej definiowania): class Foo; • deklarujemy (silny) typ wyliczeniowy (C++11) enum class EColor; enum struct EShape : char; „klasa wyliczeniowa” (albo „silny typ wyliczeniowy”) pozwala na uprzednią deklarację wraz ze specyfikacją typy danych wyliczeniowych (typ musi być całkowity) Deklaracjami nazywamy również: • deklarację z użyciem typedef typedef int Calkowity, *PtrCalkowity; Calkowity n1; PtrCalkowity ptr1; n1 jest typu int, zaś ptr1 jest typu „wskaźnik do int” • deklarację użycia using lub dyrektywę using using std::cout; using namespace std; deklaracja użycia czegoś dyrektywa użycia którejś przestrzeni nazw Definicje – reguła jednej definicji (One Definition Rule) Jedna definicja – żadna jednostka translacji (plik) nie może zawierać więcej niż jednej definicji jakiejkolwiek zmiennej, funkcji, klasy, typu wyliczeniowego lub szablonu. Definicja może się znajdować w programie, zewnętrznej bibliotece (standardowej, użytkownika). Definicja klasy konieczna jest w danym pliku, gdy typ klasy używany jest w sposób wymagający znajomości kompletnej definicji. class Foo; struct Foo* ptr1; Foo *ptr2; One ring to rule them all, one ring to find them, One ring to bring them all and in the darkness bind them. w tych przypadkach nie ma konieczności znajomości definicji klasy, wystarczy deklaracja jej nazwy Czasami definicja może się „powtórzyć” w różnych plikach. Dotyczy to klasy, typu wyliczeniowego, funkcji inline (extern inline), szablonu klasy, statycznej zmiennej oraz metody składowej w szablonie klasy, niestatycznego szablonu funkcji, specjalizacji szablonu… (C++11 §3.2.5) – wszystko to pod pewnymi warunkami! (zasadniczo jest to powtórzenie tego samego kodu z ew. dopisanymi wartościami domyślnymi funkcji) Organizacja kodu (header guard) DEKLARACJE zmienny, funkcji nie-inline lub DEFINICJE funkcji inline DEFINICJE klas, DEFINICJE szablonów plik nagłówkowy ( .h ) Cytat z „Megatutorial-u” Karola Kuczmarskiego (Państwa niewiele starszego kolegi…) Dyrektywa #include jest głupia jak cały preprocesor. Wielokrotne włączenie tego samego nagłówka (#include) – wielokrotna definicja – pogwałcenie reguły ODR – błąd! Aby temu zapobiec, w plikach nagłówkowych zawsze korzystamy z dyrektyw preprocesora (blokada, tzw. header guard) #ifndef FIGURA_H #define FIGURA_H // tutaj cała zawartość pliku #endif // FIGURA_H DEFINICJE zmiennych, funkcji, metod klas plik źródłowy ( .cc ) Nie korzystamy z dyrektywy #pragma once • • pierwotnie działała tylko w niektórych kompilatorach, np. Visual C++ pragma nie jest polecana przez twórców gcc jako dyrektywa z definicji „zależna od implementacji”, choć działa w g++ od ver. 3.4 Pułapki myślenia o blokadach header guard (czyli zestaw #ifndef #define … #endif) nie chroni przed problemem podczas konsolidacji plików, jeśli w pliku nagłówkowym, włączonym do tych różnych plików, zdefiniowaliśmy coś, co pogwałci ODR. Skompiluje się, ale linker zgłosi „multiple definition” header guard chroni jeden dany plik źródłowy przed wielokrotnym włączeniem (i kompilacją) tego samego pliku nagłówkowego, wielokrotne włączenie może nastąpić również nie wprost, przez inne włączane pliki plik h1.h plik h2.h plik test.cc plik main.cc #ifndef H1_H #define H1_H void fun() { } #endif #ifndef H2_H #define H2_H #include ”h1.h” // coś jeszcze #endif #include ”h2.h” void fun2() { fun(); } #include ”h1.h” #include ”h2.h” int main() { fun(); } g++ main.cc test.cc –o prog /tmp/cccOPU18.o: In function `fun()': test.cc:(.text+0x0): multiple definition of `fun()' /tmp/ccGypJJH.o:main.cc:(.text+0x0): first defined here collect2: ld returned 1 exit status Namespace – przestrzeń nazw Namespace – „przestrzeń nazw” – obszar posiadający swoją nazwę lub bez nazwy, służący do deklaracji. Nazwy zdeklarowane wewnątrz namespace są „zamknięte” – odwołanie do nich możliwe jest poprzez nazwę przestrzeni nazw lub odpowiednią deklarację / dyrektywę użycia. Namespaces służą unikaniu kolizji nazewniczych. Definicja namespace może być rozbita na wiele części i może się znajdować nawet w różnych plikach – zatem sami możemy nawet coś dodać do danej „przestrzeni nazw” – nawet tej zdefiniowanej w jakiejś zewnętrznej bibliotece. inlineopcjonalnie namespace nazwaopcjonalnie { /* zawartość */ } inline – nowość w C++11 – w celu wspierania różnych wersji danej biblioteki, czyli ewolucji kodu, ze wskazaniem na bieżącą (np. najnowszą) wersję Cała biblioteka standardowa jest zamknięta w przestrzeni o nazwie std Konieczna zatem jest dyrektywa użycia: using namespace std; ale nigdy w pliku nagłówkowym! W plikach nagłówkowych raczej piszemy std::obiekt, ewentualnie stosujemy deklarację użycia, np. using std::cout Namespace – reguły istnienia, tworzenia, użycia Namespace – tylko w przestrzeni globalnej albo (zagnieżdżone) wewnątrz innej przestrzeni tym samym namespace nie można zagnieździć (definiować) wewnątrz definicji funkcji (również main) ani nie można zdefiniować wewnątrz klasy using – deklaracja/dyrektywa użycia czegoś z przestrzeni nazw void f(); namespace A { void g(); namespace X { } using ::f; // globalne f } using A::g; // g z A void h() { X::f(); // woła ::f X::g(); // woła A::g } namespace A { int i; } namespace A1 { using A::i; using A::i; // tu ok, można powtórzyć } void f() { using A::i; using A::i; // a tu błąd! } Namespace – użycie, zagnieżdżenia namespace A { void f(int); } using A::f; // f jest synonimem A::f; // czyli A::f(int) namespace A { void f(char); } void foo() { f(’a’); // woła f(int) } // pomimo że f(char) istnieje void bar() { using A::f; // f jest synonimem A::f; // zarówno A::f(int) i A::f(char) f(’a’); // woła f(char) } Można dodawać kolejną zawartość przestrzeni nazw (nawet w kolejnych plikach) ale musi być to zrobione w tym samym zasięgu znaczeniowym. Bezpośrednią nadrzędną przestrzenią nazw dla danej deklaracji jest ta przestrzeń, w której deklaracja po raz pierwszy się pojawia. Później definicja (danej deklaracji) może być w innym zakresie, ale z precyzyjną specyfikacją co do pierwotnego wystąpienia deklaracji. namespace A { namespace B { void f(); class C { void m(); }; } void B::f() { extern void h(); // to jest deklaracja A::B::h } void B::C::m() { // definicja metody m() } } Jeśli w dwóch różnych przestrzeniach te same nazwy – konflikt w momencie użycia Typy danych oraz specyfikatory Podstawowe typy wbudowane: char, int, float, double wchar_t – rozszerzony typ znakowy (wielkość zależna od implementacji) char16_t i char32_t – do reprezentacji znaków standardu Unicode Specyfikatory (rozszerzają lub zawężają, ze znakiem lub bez znaku) short – long, signed – unsigned • short int (inaczej: short), int, long int (inaczej: long), long long int (inaczej: long long) oficjalnie w C++11 ze wzg. na zgodność z C99 • float, double, long double Typ bool true – odpowiednik wartości całkowitej 1 false - odpowiednik wartości całkowitej 0 (dwa stany logiczne: true, false – to są stałe) nie nadawać stanu logicznego za pomocą operacji arytmetycznej (+ lub -) → niejawna konwersja typów • operatory: && || ! < > <= >= == != • komendy sterujące: if, for, while, do, ? : • kompilator przekształca int w bool typedef, using typedef – synonim typu istniejącego (nie żadna nowa definicja), najczęściej używany do uproszczenia zapisu (wiele razy w bibliotece standardowej) np. typedef basic_fstream<char> fstream; // w nagłówku fstream typedef basic_string<char> string; // w nagłówku string using – może być użyte zamiennie jako typedef typedef std::vector<int>::iterator It; using It = std::vector<int>::iterator; // te dwie linie robią to samo typedef const char* (*Fptr)( double ); using Fptr = const char* (*) (double); // wskaźnik do funkcji, też to samo co wyżej Zasięg zmiennych, przesłanianie – przykład int a = 1; // zmienna globalna zakomentowanie globalnej zmiennej i próba namespace mojeKlocki odwołania się do niej spowoduje błąd kompilacji { int a = 7; int b = 8; } namespace { int c = 99; // int a = 3; spowodowałoby kolizję ze zmienną globalną } int main() { int a = 2; { int a = 3, c = 100; for (int i=0; i<10; ++i); // nic nie robi, bo uwaga - gdzie kończy się instrukcja cout << "a lokalne = "<< a <<endl; // 3 using namespace mojeKlocki; cout << "a lokalne = "<< a <<endl; // 3 cout << "a z mojeKlocki = "<< mojeKlocki::a <<endl; // 7 cout << "b z mojeKlocki = "<< b <<endl; // 8 int b = 12; cout << "b lokalne = "<< b <<endl; // 12 cout << "a nielokalne = "<< ::a <<endl; // 1 cout << "c z nienazwanej przestrzeni " << ::c << endl; // 99, to też jest zmienna globalna } cout << "a lokalne = "<< a <<endl; // 2 } Obiekt globalny – istnieje przez cały czas wykonania programu – domyślnie łączony zewnętrznie – deklaracja extern – można użyć w innych plikach źródłowych – deklaracją static zasięg można ograniczyć do pliku wystąpienia definicji (bez kolizji nazw) – lepszy sposób na „łączenie wewnętrzne” – użycie nienazwanej przestrzeni nazw (namespace) – jeśli const, to zachowuje się jak static (chyba że extern const) – domyślnie inicjowany wartością zera Statyczny obiekt lokalny – istnieje przez cały czas wykonania programu – deklaracja z modyfikatorem static – wartość takiego obiektu przetrwa między kolejnymi wywołaniami funkcji – zasięg ograniczony jest do bieżącego kontekstu – w klasie – jeden egzemplarz dla wszystkich obiektów klasy – domyślnie inicjowany wartością zera pamięć statyczna Rodzaje obiektów i ich cechy (static – global) Obiekty globalne i statyczne (globalne) – przykłady // w przestrzeni nazw lub przestrzeni globalnej int i; // domyślnie łączenie zewnętrzne const int ci = 0; // domyślnie globalny const jest static (łączony wewnętrznie) extern const int eci; // jawna deklaracja łączenia zewnętrznego static int si; // jawnie static // podobnie funkcje – uwaga – nie ma globalnych funkcji stałych (const) int foo(); // domyślnie łączenie zewnętrzne static int bar(); // jawna deklaracja static // nienazwana przestrzeń nazw jako polecany sposób na ograniczenie zakresu // widzialności nazw do danej jednostki translacji namespace { int i; // mimo łączenia zewnętrznego niedostępne // w innych jednostkach translacji class niewidoczna_dla_innych { }; } sterta Obiekt automatyczny – obiekt lokalny – przydział pamięci następuje automatycznie w chwili wywołania funkcji – czas trwania obiektu kończy się wraz z zakończeniem bloku, w którym został zaalokowany – zasięg ograniczony jest do bieżącego kontekstu – należy uważać na wskaźniki i referencje do obiektów lokalnych – obiekt domyślnie nie jest inicjowany Obiekt z czasem trwania określanym przez programistę – obiekt z pamięcią przydzielaną dynamicznie (operator new) – czas życia – do usunięcia operatorem delete – obiekt bez nazwy – identyfikowany pośrednio przez wskaźnik – zawieszony wskaźnik - wskazujący na nieobsługiwany obszar pamięci (wskaźnik zwisający) – wyciek pamięci - obszar pamięci przydzielany dynamicznie na który nie wskazuje żaden wskaźnik stos Rodzaje obiektów i ich cechy (stack, heap) Stałe (const) a preprocesor • za pomocą preprocesora #define PI 3.1415 od miejsca zdefiniowana do końca pliku • modyfikator const const float pi = 3.1415; zasięg taki jak zasięg zmiennej, typ musi być określony, stała musi być zainicjalizowana • stałej zdefiniowanej za pomocą preprocesora nie można śledzić – bo polega na zamianie jednego symbolu na np. podaną wartość, zdecydowanie definiujmy stałe jako zmienne danego typu • preprocesor można czasem użyć jako sprytnej makrodefinicji, np. wypisywania kontrolnego zmiennych (za Bruce Eckelem): #define PRINT (STR, VAR) cout << STR ” = ” << VAR << endl #define PR (x) cout << #x ” = ” << x << ”\n” wtedy gdzieś w kodzie: PRINT(”wartosc”, a ); auto – dedukcja typu (C++11) auto – dawniej oznaczało tylko zmienną lokalną (automatyczną) • dedukcji typu w oparciu o typ inicjalizatora lub typu zwracanego przez funkcję auto i = 7; // typ int auto x = wyrażenie // x będzie typu zwracanego przez wyrażenie • dedukcja odbywa się tak jak w szablonach, z wyjątkiem rozpoznawania listy { a, b, c }, którą auto widzi jako std::initializer_list<T> (gdzie T to typ a, b, c) template<class T> int whatever(T t) { T x; // równoważne do auto x poza szablonem }; auto – zastosowania ( C++11 ) • przykłady auto a = 7; // a jest typu int const auto *ptr = &a, b = 5; // ptr typu const int*, b typu const int static auto d = 3.14; // d typu double auto x = { 1, 2, 3 }; // x typu std::initializer_list<int> • działa również z operatorem new new auto(1); // alokowanym typem jest int auto z = new auto(’a’); // alokowanym typem jest char, z jest typu char* • szczególnie wygodne do dedukcji typów iteratorów for( auto i = m.begin(); i != m.end(); ++i ) … // niech m jest typu map<int,string> const auto& y = m; // y jest typu const std::map<int, std::string>& • niestety, wewnątrz wyrażeń lambda auto nie działa auto – zastosowania ( C++11 ) • zmienne zadeklarowane za pomocą auto są nadal wielkościami statycznymi, stąd niemożliwe jest: void fun( auto arg ) { } // źle: autodedukcja typu argumentu niemożliwa class Foo { auto m = 1; // źle: autodedukcja typu zwykłej składowej klasy niemożliwa // bo np. auto m = f(); wprowadzałoby spory problem w szukaniu // właściwej interpretacji tego czym jest f() }; auto tablica[5]; // źle: autodedukcja typu z którego zbudowana jest tablica • możliwe jest class Foo { static const auto n = 0; // static tak }; • uwaga auto s = ”hello world”; // jest typu const char* auto& s = ”hello world”; // jest typu referencja do const char[12] czyli tablicy auto – nowe metody w kontenerach, nowa pętla for ( C++11 ) W kontekście auto przydatne są nowe metody kontenerów: • zwracają jawnie stałe iteratory: cbegin(), cend(), crbegin(), crend() auto ci = m.cbegin(); // ci typu std::map<int, std::string>::const_iterator Nowa składnia dla pętli for (tzw. range-based loop) vector<int> v { 1,2,3,4,5 }; for ( int i : v ) cout << i << endl; // i bezpośrednio każdym elementem wektora for ( auto i : v ) cout << i << endl; // to samo co powyżej for ( int& i : v ) cout << ++i; // może być też referencją i zmieniać zawartość! for ( auto& i : v ) cout << ++i; // to samo co powyżej for (const int i : v ) jakasMetoda( i ); // const/volatile też możliwe Można przebiegać po tablicach, kontenerach oraz dowolnych typach wyposażonych w iteratory, zwracane przez begin() i end() short tablica[5]; for ( auto& t : tablica ) { t = -t; } std::unordered_multiset<std::shared_ptr< T >> obj; for ( const auto& r : obj ) cout << r; // wypisuje wskaźnik // pytanie: czemu powyższe przez referencję? w C++11 nie ma problemu zagnieżdżonych nawiasów szablonów, nie trzeba rozdzielać spacją auto – referencje, modyfikatory ( C++11 ) Dla zmiennych nie zadeklarowanych wprost jako referencje, modyfikatory const/volatile na najwyższym poziomie są ignorowane: const vector<int> w; auto v1 = w; // v1 typu vector<int>, const zignorowane auto& v2 = w; // v2 typu const vector<int>& - ale jeśli przez referencję, to ok Tablice i nazwy funkcji redukują się do wskaźników: double tablica[5]; auto t1 = tablica; // t1 typu double* - to się nazywa ”array decay to pointer” auto& t2 = tablica; // t2 typu double(&)[5] – właściwy typ tylko jeśli przez referencję Jeżeli const/volatile nie na najwyższym poziomie, to zostają: auto i = 10; map<int, string> m; const auto *pi = &i; // pi jest typu const int* const auto& pm = m; // pm typu const map<int, string>& Za pomocą auto można deklarować więcej zmiennych w linii: auto zmienna = s, *ptr_zmienna = &s; // dedukcja typu inicjalizatora – ten sam typ auto i = 3, d = 3.14; // błąd – rożne typy inicjalizatorów Operatory • zwracają wartości na podstawie argumentów (argumentu) • 18 poziomów ważności – nie uczyć się wszystkiego! raczej używać nawiasów ( ) do czytelnego oddzielenia; niektóre zapamiętać • operatory =, ++, -- dodatkowo zmieniają wartość argumentu (skutek uboczny, ang. side effect) • operator przypisania = kopiuje p-wartość do l-wartości • operatory matematyczne +, -, *, /, % • można połączyć z operatorem przypisania +=, -=, *=, /=, %= • zatem np. b %= 4; równoważne jest b = b % 4; • operator % (modulo) tylko z liczbami typu całkowitego • operatory relacji <, >, <=, >=, ==, != zwracają wartość logiczną • operatory logiczne && (iloczyn), || (suma) • operatory bitowe & (koniunkcja), | (alternatywa), ^ (różnica symetryczna), ~ (bitowy operator negacji) Operatory – ciąg dalszy • operatory przesunięć <<, >> jeśli po lewej liczba ze znakiem, to przesunięcie >> nie musi być operacja logiczną • można łączyć z operatorem przypisania <<=, >>= • bity przesunięte poza granicę są tracone • operatory jednoargumentowe ! (negacji logicznej), -, + • operatory adresu &, wyłuskania *, -> i rzutowania • rzutowanie: float a = 3.14; int b = (int)a; albo int b = int(a); • operatory alokacji i usuwania: new, delete • operator trójargumentowy ? : co się stanie: int a = --b ? b : (b = -10); // jeśli b=1, to a=-10 • operator , zwraca wartość ostatniego z wyrażeń • operator sizeof Operatory – tabela ważności Level Operator Description Grouping 1 :: scope Left-to-right 2 () [] . -> ++ -dynamic_cast static_cast reinterpret_cast const_cast typeid postfix Left-to-right ++ -- ~ ! sizeof new delete unary (prefix) *& indirection and reference (pointers) +- unary sign operator 4 (type) type casting Right-to-left 5 .* ->* pointer-to-member Left-to-right 6 */% multiplicative Left-to-right 7 +- additive Left-to-right 8 << >> shift Left-to-right 9 < > <= >= relational Left-to-right 10 == != equality Left-to-right 3 Right-to-left 11 & bitwise AND Left-to-right 12 ^ bitwise XOR Left-to-right 13 | bitwise OR Left-to-right 14 && logical AND Left-to-right 15 || logical OR Left-to-right 16 ?: conditional Right-to-left 17 = *= /= %= += -= >>= <<= &= ^= |= assignment Right-to-left 18 , comma Left-to-right Operatory – rzutowanie • static_cast (konwersje niejawne, zawężające, zmieniające typ – podczas kompilowania) int b = static_cast<int>(a); void *vp; int *num = static_cast<int*>(vp); • const_cast (od typów z modyfikatowem const lub volatile do takich samych typów bez modyfikatora lub w drugą stronę) • reinterpret_cast (pełna odpowiedzialność użytkownika, bez kontroli) • dynamic_cast (rzutowanie "w dół" od abstrakcyjnego typu ogólnego do typu pochodnego – zajdzie gdy operacja taka ma sens – podczas wykonywania programu) Typy złożone w c++ (litania) Poprzez złożone typy w języku c++ rozumie się: • tablice obiektów danego typu • funkcje, mające parametry danego typu, a zwracające void lub referencje lub obiekty danego typu • wskaźniki do void lub obiektów, lub funkcji danego typu (włączając w to statyczne składniki klasy) • referencje do obiektów lub funkcji (tzw. referencje lewej wartości i referencje prawej wartości) • klasy, zawierające obiekty różnych typów oraz metody składowe, wraz z odpowiednimi ograniczeniami dostępu • unie, które są rodzajem klasy, mogącej zawierać obiekt różnych typów, w różnych chwilach czasu • typy wyliczeniowe, zawierające listę nazwanych stałych wartości • wskaźniki do niestatycznych składowych klasy enum – typ wyliczeniowy „konwencjonalny” enum – autonomiczny typ wyliczeniowy enum EPozycja { eAsystent, // 0 eAdiunkt, // 1 eProfesor // 2 }; definiowanie zmiennych podobnie jak dla typu wbudowanego: EPozycja pracownik = eAsystent; poważne mankamenty • możliwa niejawna konwersja z enum do int (może prowadzić do błędów, jeśli ktoś takiej konwersji nie chce) int a = eAsystent; // ok, konwersja! pracownik = 3; // bez rzutowania to jest błąd można też zadać wartość enum EPozycja { eAsystent = 5, eAdiunkt = eAsystent + 2, eProfesor }; nie można robić inkrementacji: pracownik++; „wyciekanie” identyfikatorów do zewnętrznego zakresu względem miejsca zdefiniowania typu wyliczeniowego (np. enum zdefiniowany w przestrzeni globalnej eksportuje nazwy wszędzie… kolizja nazw) sizeof( EPozycja ) = ? • nie można określić typu, na jakim zbudowane sa identyfikatory … pewnie 4 ale… może być mniej • niemożliwa jest uprzedzająca deklaracja typu wyliczeniowego nienazwany enum – ma sens właśnie przez to, że jego identyfikatory (z listy wyliczeniowej) są widziane na zewnątrz jako stałe (całkowite): enum { jeden = 1, dwa = 2, cztery = 4 }; • enum – silny typ wyliczeniowy (C++11) enum class nazwa { lista identyfikatorów }; zamiast class może być struct • nazwy z listy wyliczeniowej nie wyciekają na zewnątrz • nie następuje niejawna automatyczna konwersja na int enum Alert { green, yellow, election, red }; // standardowy, stary typ wyliczeniowy enum class Color { red, blue }; // nowy, silny, identyfikatory nieznane na zewnątrz enum struct TrafficLight { red, yellow, green }; // jak widać, nie koliduje z niczym Alert a = 7; // błąd: zwykły przypadek, nie ma konwersji z int na enum Color c = 7; // błąd: nie ma konwersji int->Color int a2 = red; // ok: możliwa konwersja Alert::red->int int a3 = Alert::red; // błąd w C++98, ok w C++11 int a4 = blue; // błąd: blue nieznane w tym zakresie int a5 = Color::blue; // błąd: brak konwersji Color->int Color a6 = Color::blue; // ok • • można (opcjonalnie) zdefiniować typ (musi być całkowity), na którym zbudwany jest nowy enum (domyślnie – int) i dzięki temu kontrolować wielkość enum class Color : char { red, blue }; // sizeof( Color ) taki sam jak sizeof( char ) możliwa jest deklaracja wyprzedzająca enum class Color : char; // deklaracja void foo(Color* p); // teraz można już użyć std::array – tablica na miarę naszych czasów (C++11) • łączy w sobie szybkość zwykłej C-tablicy z zaletami bycia kontenerem standardowym, czyli np. „wie jaki ma rozmiar” • zawiera w sobie agregat; potrzebny nagłówek <array> • wielkość i przetrzymywany typ trzeba z góry określić array<int, 3> a = { 1, 3, 7 }; // znak = opcjonalny, ale… array<string, 2> b { { string("Windows"), "Linux" } }; // powyższe zagnieżdżenie to inicjalizacja wewnętrznego agregatu // ten zapis nie jest przejawem „uniwersalnej inicjalizacji” poprzez // initializer_list<T> ponieważ array nie ma napisanego konstruktora • można używać jak tablicę, albo odpytać daną pozycję metodą at(n), można zapytać o pierwszy – front() i ostatni – back() element • metody empty() – true gdy pusta czyli… zrobiona tak: array<int, 0> a; • size() – rozmiar tablicy, max_size() – hipotetyczny maksymalny rozmiar • fill( const T& val ) – wypełnienie wszystkich elementów wartością val Referencje – lewe ( T &, const T & ) Terminologia wprowadzająca l-value (lewa-wartość, l-wartość) coś, co można zmodyfikować, np. poprzez przypisanie (stoi po lewej stronie = ) r-value (prawa-wartość, p-wartość) coś, co stoi po prawej stronie operacji przypisania, często rozumiana jako niemodyfikowalne Referencja ( T & ) „zwykła” to jakby „przezwisko” na coś. „Przezwisko” nie może istnieć samo, bez powiązania z tym, co określa. Zatem referencja w momencie definicji musi być zainicjalizowana i nie może być przestawiona na coś innego. Nie istnieją: • referencje do referencji Niestała referencja ( T & ) może • tablice referencji wskazywać na l-wartość. Stała • wskaźniki do referencji referencja ( const T & lub T const & ) może wskazywać na l-warość i p-wartość. W roli p-wartości może wystąpić obiekt, który nie musi być stały, jak i obiekt, którego nie wolno modyfikować (np. tymczasowy). Do tej pory nie można było rozróżnić, na co pokazuje stała referencja. Referencje – zakazane cv, prawe ( T && ) (C++11) Nie istnieje: T & const Przykład Kwalifikatory cv dla referencji, są niedopuszczalne. Wprowadzone przez typedef, albo argument szablonu, są zignorowane. pamiętajmy, że w c++ funkcjonuje pojęcie kwalifikatora cv, czyli const i/lub volatile, zatem to co piszemy o const, dotyczy też volatile int a = 3; typedef int& RINT; const RINT aref = a; aref = 4; // teraz ma wartość 4 wbrew pozorom, aref jest referencją „l-wartości” do int, a nie do const int napisanie const RINT tu oznacza nie const int& a próbę int& const – coś takiego jest ignorowane albo innymi słowy: referencja musi być zadeklarowana z const, potem tego const nie można dołożyć na zasadzie zmiany typu deklarowanej referencji C++11 wprowadza referencję „p-wartości” ( && ), która ma służyć wskazywaniu na p-wartości, ale w rozumieniu takim, że można je modyfikować. Służyć to ma budowaniu semantyki (składni) „przenoszenia”. Pojawiają się dzięki temu „konstruktory przenoszące” (move constructors) i „przenoszące operatory przypisania” (move assignment operator). Więcej o tym – w dalszej części wykładu. Wskaźniki Wskaźniki – zawierają adres i informację o typie (wyjątek: void*) T* – zwykły wskaźnik (do typu T) const T*, T const* – wskaźnik do stałego obiektu („gwarancja nietykalności”) T* const – wskaźnik stały („gwarancja nieprzesuwalności”) const T* const, T const* const – stały wskaźnik do stałego obiektu Ponownie uwaga na typedef: typedef int* pointer; typedef const pointer const_pointer; const_pointer jest typu int* const, a nie typu const int* const int ci = 10, *pc = &ci, *const cpc = pc, **ppc; int i, *p, *const cp = &i; pc – wskaźnik na stały int, cpc – stały wskaźnik na stały int, ppc – wskaźnik do wskaźnika na stały int, p – wskaźnik na int, cp – stały wskaźnik na int Wskaźniki – własności i arytmetyka • wskaźnik jak tablica int *vInt = n; // wcześniej int n[10]; vInt = &n[0]; // to samo można nimi operować jakby były tablicą, vInt[2] to samo co n[2] * tu jako operator wyłuskania zmiennej ze wskaźnika vInt – to adres początku tablicy (pierwszego jej elementu) vInt + 1 – to adres drugiego elementu tablicy *(vInt + 2) – to zawartość wskazywana pod adresem vInt + 2 • operacje ++ lub - – są one inteligentne, tzn. na podstawie typu wskaźnika kompilator wie o ile bajtów ma przeskoczyć • operacje + lub – ograniczone – można dodawać lub odejmować liczby całkowite (operacja inteligentna tzn. z wykorzystaniem wiedzy na temat wskazywanego typu) – nie można dodawać dwóch wskaźników – można odjąć dwa wskaźniki – wynikiem jest liczba elementów danego typu znajdujących się pomiędzy nimi: int tab[] = { 1, 2, 5, 7 }; int *p1 = tab; int *p2 = &tab[3]; cout << p2 – p1 << endl; // 3 Wskaźniki – przykłady • można dokonać zmian… double f1 = 0.; const double pi = 3.14; double *vZmienna = &f1; const double *vStala1 = π const double *vStala2; // wskaźnik do stałego obiektu, jeszcze nie ustawiony vStala2 = vZmienna; •*vZmienna = 25.; double * const vStalyZmienna = const_cast<double * const>( vStala1 ); vZmienna = vStalyZmienna; • nie wszystkie zmiany możliwe… nie można usunąć przydomka const z żadnego obiektu (można tylko rzutować) T * & - referencja do wskaźnika na typ T T & * - takie coś nie istnieje! przydaje się jako argument funkcji, wtedy wskaźnik – argument, można wewnątrz funkcji przestawić na inny adres Funkcje – argumenty, wartości zwracane • funkcja to podprogram • funkcję identyfikuje jej nazwa, trzeba ją zadeklarować – wyjątek to funkcja main • definicja funkcji jest deklaracją, niemniej starajmy się deklarować wszystkie funkcje • deklarację funkcji można zagnieździć w innej funkcji, ale definicji funkcji nie można zagnieżdżać w innej funkcji (nawet w main) • funkcja może przyjmować dowolne parametry i zwracać dany typ lub nic nie zwracać (wtedy piszemy void) void fun(); // nic nie zwraca, ale można wewnątrz funkcji napisać // pustą instrukcję wyjścia return; int fun(string, int); // deklaracja nie wymaga podania nazw zmiennych, // ale dla czytelności kodu warto je pisać auto fun( double ) -> double; // nowa notacja C++11 ( -> trailing return type ) auto fun( char ); // możliwe w C++14 ale wtedy przed wywołaniem funkcja // musi być zdefiniowana, sama deklaracja nie wystarczy bo nieznany jest typ zwracany • nigdy nie zwracamy adresu (referencji) do obiektu lokalnego (czas jego życia się skończył…) • main zwraca zawsze int – z przyczyn historycznych nie musimy wołać komendy return, kompilator nie napotkawszy jej wstawia na koniec bloku tej funkcji return 0; Funkcje – sposoby przekazania parametrów • sposoby przekazywania parametrów do funkcji void fun(float f); // przez wartość, do wnętrza funkcji tworzona jest kopia // obiektu f, więc oryginału nie można zmienić (uszkodzić) void fun(const float f); // to nie ma sensu, tworzona jest kopia // i nawet tej kopii nie da się zmienić, czytelniej więc byłoby // jako argument używać float f, a w pierwsze linii funkcji np. // const float& argf = f; void fun(float& f); // przez referencję (adres), można // modyfikować obiekt podawany jako parametr void fun(const float& f); // przez referencję do stałego obiektu, // optymalny sposób! – nie jest tworzona kopia, a argument jest // chroniony przed zmianą void fun(float&& f); // przez referencję do prawej wartości, większy sens // ma dla typów złożonych, które umożliwiają operacje przenoszenia void fun(const float&& f); // zwykle bez sensu, bo blokuje przenoszenie void fun(float* f); // przez wskaźnik, można modyfikować void fun(const float* f); // wskaźnik do stałego obiektu, nie można modyfikować Funkcje – wywołanie a parametry • jaka jest różnica pomiędzy parametrem "przez referencję" i "przez wskaźnik"? Sposób wywołania funkcji: float mojaLiczba = 0.; fun(mojaLiczba); // przez referencję, tak samo jak przez wartość fun(&mojaLiczba); // przez wskaźnik, trzeba podać adres obiektu za pomocą & • na temat dedukcji typu zwracanego przez funkcję: auto f(); // zwracany typ nieznany auto f() { return 5; } // zwracany typ int auto f(); // redeklaracja – ok int f(); // błąd – traktowane jako deklaracja inne funkcji auto f() { return f(); } // błąd, dopóki typ zwracany jest nieznany, // nie można wołać rekurencyjnie auto suma(int i) { if (i==1) return i; // zwracany typ teraz znany else return suma(i-1) + i; // można więc dalej wołać rekurencyjnie } // taka funkcja może mieć wiele instrukcji return ale każda zwracająca taki sam typ Funkcje – tablice argumentami, inline • tablice jako argumenty funkcji nie są przekazywane przez wartość void func1(int a[], int rozmiar); // musimy podać rozmiar void func2(int *a, int rozmiar); // array-to-pointer decay void func3(int (&a) [10]); // tylko 10-elementowa tablica void func4(int macierz[][3], int rozmiar); • funkcje inline (krótkie, w celu szybkiego wywoływania) • treść rozwijana w miejscu ich wystąpienia, o ile nie jest zbyt skomplikowana • dla zwykłej funkcji: deklaracja (bez specyfikatora) w nagłówku void fun(); definicja w plku źródłowym poprzedzona specyfikatorem inline inline void fun() { /* definicja */ } • podobnie dla metody składowej (tylko definicja ze słowem inline) • wszystkie funkcje zdefiniowane wewnątrz klas są automatycznie inline • jeśli pobierany jest adres funkcji – nie następuje rozwinięcie (w szczególności w procesie „debugowania” – krokowego śledzenia działania programu) Funkcje – wartości domyślne argumenty domniemane (od prawej do lewej) tylko w deklaracji void fun(int a, void*, float = 3.14, char znak= '\0'); • deklaracja argumentu domyślnego tylko raz (w danym zakresie ważności) • w deklaracji funkcji – deklaracje można powtarzać, ale nie z powtórzonymi w nich wartościami domyślnymi void fun(int a); // w deklaracjach można zmieniać // nazwy zmiennych, tylko po co… void fun(int a = 5); // tak jest dobrze • w definicji funkcji jeśli ta jest jednocześnie jej deklaracją • obiekty lokalne nie mogą być wartościami domyślnymi • w nowym (lokalnym) zakresie ważności możliwa jest deklaracja z innymi wartościami domyślnymi – nie jest to dobra praktyka! Funkcje – wartości domyślne, przykłady void g(int = 0, ...); // ok, bo … (wielokropek) to nie argument, tylko ich lista void f(int, int); void f(int, int = 7); // powtórzenie deklaracji z dodaną wartością domyślną void h() { f(3); // OK, woła f(3, 7) void f(int = 1, int); // błąd: niezależne od wartości domyślnych deklaracji // z innego – zewnętrznego – zasięgu } void m() { void f(int, int); // nie ma wartości domyślnych f(4); // błąd: niepoprawna liczba argumentów void f(int, int = 5); // OK f(4); // OK, woła f(4, 5); void f(int, int = 5); // błąd: nie można redeklarować, nawet // z taką samą wartością domyślną } void n() { f(6); // OK, woła f(6, 7) } Funkcje – dowolna liczba argumentów … - wielokropek umożliwia napisanie funkcji przyjmującej dowolną liczbę argumentów • Przynajmniej jeden (pierwszy) argument takiej funkcji musi być podany jawnie. • Obsługa (odczyt) takich argumentów za pomocą makr, pochodzących z języka C. • Konieczne włączenie nagłówka <cstdarg> ( lub stdarg.h ) int suma ( int liczba, … ) { va_list ap; // utworzenie zmiennej typu va_list (variable argument list) va_start( ap, liczba ); // ustawienie ap na pierwszy, jawnie podany, argument int sum = 0; for (int i = 0; i < liczba; ++i ) { sum += va_arg( ap, int ); // odczyt kolejnej zmiennej, sami określamy jej typ! } va_end( ap ); // porządkowanie stosu, ustawienie ap na 0 return sum; } int main() { cout << sum(3, 1, 1, 1, 1, 1) << endl; // OK, możemy mniej liczyć, 3 cout << sum(8, 1, 1, 1, 1, 1, 1) << endl; // śmieci, wyszliśmy poza listę } Wady: argumenty poza kontrolą typów. Popularne przykłady z biblioteki: printf, sprintf Funkcje – argumenty funkcji main int main(int argc, char* argv[]) { // … to samo: int main(int argc, char** argv) { // … • argc – liczba argumentów pierwszym zawsze jest ścieżka i nazwa programu argv[0] – zapisana w pierwszej pozycji tej tablicy • kolejne argumenty można konwertować po włączeniu nagłówka #include <cstdlib> za pomocą funkcji: atoi(), atol(), atof() • możemy wykorzystać obiekt klasy istringstream – klasa ta dziedziczy po klasie istream, ta zaś dziedziczy po klasie ios, zaś ta po klasie ios_base – oznacza to, że obiekt ten "ma w sobie" wszystkie funkcje zdefiniowane w powyższych klasach – ponadto ma zdefiniowaną własną funkcję: void str(const string& tekst) const; string str() const; Pomiędzy innymi typami a „łańcuchem znakowym” Warto wiedzieć, że sytuacja, gdy „skazani byliśmy” na printf (sprintf) nie ma już miejsca! W nagłówku <string> dostępna jest seria przeciążonych funkcji to_string, działających komfortowo i bezpiecznie z punktu widzenia kontroli typów. Nie musimy się też martwić o wielkość wypełnianego buforu! #include <string> // konwertuje zmienną typu int na łańcuch znakowy std::string std::string to_string( int value ); // taki sam, gdy działało sprintf o odpowiednio dużym buforze std::sprintf(buf, "%d", value); // podobnie pozostałe: std::string to_string( long value ); std::string to_string( long long value ); std::string to_string( unsigned value ); std::string to_string( unsigned long value ); std::string to_string( unsigned long long value ); std::string to_string( float value ); std::string to_string( double value ); std::string to_string( long double value ); Pomiędzy „łańcuchem znakowym” a innymi typami Podobnie w drugą stronę, jeśli mamy łańcuchy znakowe (np. parametry programu), możemy teraz skorzystać z następujących funkcji konwersji. Działają one następująco: opuszczają białe znaki, czytają cyfry (tak wiele ile jest poprawne dla ustawionej bazy base, resztę ignorują), jeśli podstawi się jako drugi parametr niezerowy wskaźnik, to wpisane w niego zostaje adres pierwszego nieskonwertowanego znaku oraz jego indeks. #include <string> // konwertuje łańcuch znakowy std::string na typ całkowity Int stoi( const std::string& str, size_t *pos = 0, int base = 10 ); long stol( const std::string& str, size_t *pos = 0, int base = 10 ); long long stoll( const std::string& str, size_t *pos = 0, int base = 10 ); unsigned long stoul( const std::string& str, size_t *pos = 0, int base = 10 ); unsigned long long stoull( const std::string& str, size_t *pos = 0, int base = 10 ); // konwertuje łańcuch znakowy std::string na typ zmiennoprzecinkowy float stof( const std::string& str, size_t *pos = 0 ); double stod( const std::string& str, size_t *pos = 0 ); long double stold( const std::string& str, size_t *pos = 0 ); Klasa std::string Utworzenie obiektu typu std::string odbywa się podobnie jak dowolnej zmiennej typu wbudowanego. Jednak w tym przypadku można też stworzyć obiekt zainicjalizowany danymi – obiekt budowany jest przez specjalną metodę składową, konstruktor. Konstruktorów może być dowolnie wiele, muszą różnić się argumentami. Zbadajmy jaki jest rozmiar i bufor obiektu s1: #include <iostream> #include <string> using namespace std; auto main() -> int { string s1; // pusty string } for (auto i(0); i<1025; ++i) { s1 += ”a”; cout << s1.size() << ” – ” << s1.capacity() << endl; } Dodatkowo co będzie gdy: s1.clear(); s1.empty(); // zwraca true lub false s1.shrink_to_fit(); s1.reserve(57); // jakie capacity() ? Tworzymy kolejne obiekty std::string Oto kilka sposobów na utworzenie / przypisanie obiektu typu std::string const char *t = ”tekst do inicjalizacji”; s1 = t; string s2( s1); // obiekt „na wzór” istniejącego wcześniej string s3( t, 8 ); // pierwsze 8 znaków string s4( s2, 6, 8 ); // od 6-tego do 6+8 -mego, czyli… string s5( 100, ’*’ ); // chcę mieć sto gwiazdek string s6 = ”konstrukcja”; string s7 = { ”uniwersalna inicjalizacja” }; // = opcjonalnie Działania na stringach bez problemu: s1 = s1 + ” drugi ” + s2; s1 += s6; Rozmiary, usuwanie… Maksymalny rozmiar i pewna stała: max_size() // zwykła metoda składowa string().max_size(); // string „w locie” Sprawdźcie jaka jest wartość tej stałej: std::string::npos Wielkie usuwanie (erase – metoda składowa): erase( nr_od, nr_ile ); // zwraca „referencję do” erase( adres_od, adres_do ); // zwraca „adres” nast. znaku Specjalne funkcje adresowe (zwracające tzw. iteratory czyli obiekty „udające” wskaźniki – przechowalniki adresu i wiedzy o typie): begin(); // adres początku „zerowej pozycji” end(); // adres za ostatnim elementem, „za-ostatni” Usuwanie… Przykład, dodatkowo z algorytmem find: #include <iostream> #include <algorithm> #include <string> using namespace std; int main () { std::string s = "To jest dobry przyklad"; std::cout << s << '\n'; s.erase(0, 3); // usuń "To " std::cout << s << '\n'; s.erase(std::find(s.begin(), s.end(), ' ')); // usuń pierwszą spację ' ' std::cout << s << '\n'; } s.erase(s.find(' ')); // Znajdź kolejną i usuń wszystko od niej do końca std::cout << s << '\n'; Małe ćwiczenie Narysujmy za pomocą „erase” taką sekwencję… ****************** ***************** **************** *************** I tak dalej… #include <iostream> #include <string> using namespace std; int main () { string str (20, ’*’); while ( ! str.empty() ) { cout << str << endl; str.erase(str.end()-1); } } Wczytywanie z pliku Utwórzmy obiekt do obsługi strumienia plikowego i wczytajmy… a potem wypiszmy! #include <fstream> string s10; string str; cout << "Wprowadz tekst: "; cin >> str; cout << "Wczytano to: " << str << endl; getline (cin, str, '@'); // koniec = znaczek @ cout << "Wczytano tamto: " << str << endl; Bufor cin nadal trzyma starą zawartość, tu poczytajcie jak to wyczyścić http://cpp0x.pl/kursy/Kurs-C++/Poziom-1/Obsluga-strumienia-wejsciowego/12 ifstream plik("tekst.txt"); // np. wziąć z: pl.lipsum.com while ( ! plik.eof() ) { getline (plik, str); s10 += str; // czego tu brakuje? Znak końca linii… + ’\n’ } // wypiszcie na ekran… cout << s10; Przebiegamy po stringu… String to forma kontenera sekwencyjnego… jakby tablicy znaków… s1 = "wlazl kotek na plotek i mruga"; for ( auto c : s1 ) cout << c << " "; // range-based loop for ( auto& c : s1 ) c = ( c==’w’ ) ? ’W’ : c; // zamieniamy na wielkie W, co z nawiasami? for ( int i=0; i < s1.length(); ++i ) cout << s1[i] << " "; ITERATOR – inteligentny „pośrednik” pomiędzy kontenerami (zasobnikami), „wskaźnik” z adresem do operacji na konkretnych typach, strumieniach… string::iterator it; // na razie pusty auto it = s1.begin(); it = s1.begin(); // początek … end() koniec while ( it != s1.end() ) { cout << *it << endl; ++it; } ITERATOR STRUMIENIA copy (s1.begin(), s1.end(), ostream_iterator<char>(cout,"\n")); // używamy algorytmu copy (ten z nagłówka <algorithm>) // tworzymy w locie iterator strumienia wyjściowego, ostream_iterator // konieczny nagłówek #include <iterator> Typy abstrakcyjne – klasa i obiekt • Z myślenia w kategoriach "jak to zrobić" przechodzimy do myślenia bezpośredniego nad zagadnieniem, czyli "co zrobić" • Odwrócona kolejność tworzenia: opis danych, przepływ danych, algorytmy • Najważniejsze są dane, na których operujemy • klasa – matryca, "plan" według którego powstaje obiekt (opisana zawartość, a także sposób utworzenia – konkretyzacji) – nowy typ danych zawiera w sobie składniki danych innego typu oraz funkcje (metody) – enkapsulacja (kapsułkowanie) • obiekt – obiekt to egzemplarz klasy – samodzielna, ograniczona jednostka posiadająca zespół cech i zachowań – każdy obiekt ma własną kopię atrybutów (wyjątek: dane statyczne), metody (ich implementacja) są wspólne – obiekty współpracują ze sobą, działanie jest "na rzecz" jakiegoś obiektu Kiedy klasa jest dobra? • klasa – reprezentuje wspólne właściwości grupy obiektów – czy istnieje potrzeba tworzenia więcej niż jednego egzemplarza klasy? (są specjalne wyjątki – singleton) – jeśli nie ma różnić pomiędzy egzemplarzami klasy: prawdopodobnie taka klasa powinna być wartością – nie jest tylko pojemnikiem na dane, które mogą być modyfikowane przez funkcje – udostępnia uproszczony obraz złożonego bytu, określa dopuszczalne do wykonania czynności • co nie jest (dobrą) klasą – zgrupowanie kilku funkcji – kontener na dane (typu struktura w C) tylko z funkcjami typu set i get) Cele klasy • cel klasy – powinien być dobrze zdefiniowany, a klasa łatwa do zrozumienia i prosta w użyciu Czy potrafisz określić cel klasy w jednym zdaniu? – nie należy dodawać do klasy metod zupełnie z nią nie związanych, tylko po to aby zaspokoić oczekiwania grupy klientów – jeśli klient po zetknięciu z klasą nie jest pewien do czego ona służy, projekt może być słaby i niepoprawny – wielkość klasy: jeśli liczba metod przekracza 15-25, to warto się zastanowić czy nie należałoby z jednej "wielkiej" klasy zrobić kilka mniejszych, czytelniejszych Obiekt – własności • obiekt – powołuje klasę do życia – stan obiektu jest sumą wszystkich statycznych i dynamicznych wartości jego właściwości, właściwość jest niepowtarzalną cechą obiektu – stan obiektu określają typy proste lub złożone – to, jak obiekt reaguje na nasze polecenia i co robi z innymi obiektami, zależy od jego stanu – stan obiektu kontrolują metody, zwykle metody wywoływane są przez klienta (wyjątek to metody np. do obsługi błędów, przerwań) • zachowanie obiektu – sposób, w jaki obiekt działa i reaguje na komunikaty – komunikat może zmienić stan obiektu, może też spowodować wysłanie komunikatów do innych obiektów – metody stałe: takie, które (gwarantują, że) nie zmieniają stanu obiektu – wszystko co nie powinno być dostępne dla normalnego klienta, powinno być ukrywane Model obiektowy • model obiektowy – w uproszczeniu: można myśleć o klasach jak o rzeczownikach, a o ich metodach jak o czasownikach – kluczowe elementy modelu obiektowego • abstrakcja danych abstrakcja danych • hermentyzacja wynik definiowania klas, • hierarchia koncentrujemy się na zewnętrznym hierarchia sposób tworzenia wzajemnych relacji pomiędzy abstrakcjami danych wyglądzie obiektu i oddzielamy ważne zachowania od wewnętrznych szczegółów implementacji hermetyzacja (ukrywanie danych) wynik ukrywania wewnętrznych szczegółów implementacji, istotna w momencie rozpoczęcia implementacji Typy hierarchii "jest-czymś", realizowane poprzez dziedziczenie, umożliwia stosowanie relacji ogólne-specyficzne RACHUNEK BANKOWY jest: ROR LOKATA "ma-coś", budowanie z elementów składowych, wprowadza stosunek część-całość SAMOCHÓD ma: silnik siedzenie koło kierownica Zalety modelu obiektowego • zachęca do tworzenia systemów, które mogą podlegać zmianom, systemy są elastyczne i stabilne • myślenie w kategoriach (klas i) obiektów jest naturalne dla człowieka • oddzielenie klienta i programisty (hermetyzacja danych) • wielokrotne wykorzystanie prostych klas, unikanie replikacji kodu • rozszerzalność projektów (np. poprzez dziedziczenie), czyli zachęta do ponownego wykorzystywania istniejącego oprogramowania Interfejs i implementacja • interfejs to punkt widzenia użytkownika na to, jak obiekt wygląda i co można z nim zrobić • klient używa klasy bez wgłębiania się w jej wewnętrzne działanie, dobrze zaprojektowany interfejs spełnia wymagania użytkownika • specyfikacja interfejsu – w plikach nagłówkowych • implementacja określa w jaki sposób coś jest wykonywane, model obiektowy pozwala na ochronę implementacji (przed klientem) • model obiektowy pozwala na zmienianie implementacji podczas gdy interfejs pozostaje niezmieniony Klasa KLASA podstawowa jednostka abstrakcji danych w języku C++ • posiada trzy regiony dostępu: prywatny, chroniony i publiczny • zawiera sygnatury – metod niestatycznych i statycznych – deklaracje danych składowych zwykłych i statycznych • może zawierać deklarację (definicję) innej klasy – zagnieżdżonej Nazwy deklarowane w klasie = zakres ważności to obszar całej klasy. Domyślna etykieta dostępu (odwrotnie niż w strukturze) private Dostęp do składników klasy class MojaKlasa { public: int nr_pokoju; std::string etykieta; }; int getNr(); string getName(); Skąd zwykła (niestatyczna) metoda wie, na jakim komplecie danych (na jakim obiekcie) pracuje? Otrzymuje niejawnie specjalny wskaźnik: this dane składowe powinny być zdecydowanie w części prywatnej! Dostęp do składników klasy: MojaKlasa mojObiekt; MojaKlasa *mojWskaznik = &mojObiekt; MojaKlasa &mojaReferencja = mojObiekt; mojObiekt.nr_pokoju; mojWskaznik->etykieta; mojaReferencja.getName(); zawiera adres konkretnego obiektu danego typu this is it (kilka słów o „tym” wskaźniku) (stały) wskaźnik this – niejawnie zdefiniowana składowa każdej (niestatycznej) metody klasy, zawiera adres obiektu this przekazywany jest jako parametr (niejawny) niestatycznym metodom klasy, aby znały adres obiektu, na którego zmiennych działają void Prostokat::ustawParam(double x, double y) { this->bokX = x; // można jawnie zapisać, ale nie trzeba this->bokY = y; } typ wskaźnika this zależy od atrybutów metody (const, volatile), jeśli metoda jest const (volatile), to podobnie wskaźnik this (wtedy jest stałym wskaźnikiem do stałego obiektu) przypadki użycia wskaźnika this • jawne użycie this – w przypadku kopiowania obiektu, sprawdzenie żeby obiekt się nie chciał sam na siebie skopiować (jak zobaczymy później: standardowe w operatorze przypisania =) void Prostokat::kopiuj(const Prostokat& figura) { if (this != &figura) { // tu sprawdzamy czy nie to samo bokX = figura.bokX; bokY = figura.bokY; } } • nie wolno używać this do usuwania obiektu (np. delete this), za wyjątkiem sytuacji specjalnych – obiekt umieszczony jest w pamięci dynamicznej za pomocą operatora new „z umieszczeniem”, wtedy „ręcznie” sterujemy kreacją i destrukcją obiektu Klasa – prawa dostępu public: protected: private: w dowolnej kolejności etykiety mogą się powtarzać protected • tak jak private, plus dostęp dla domyślny • dostęp bez klas pochodnych ograniczeń (z (dziedziczenie) private wnętrza i poza • dostęp tylko z zakresem klasy) wnętrza klasy (z • tutaj jest interfejs zewnątrz dla klas lub • składniki to funkcje fukcji - przyjaciół) • tutaj szczegóły implementacji public Klasa – konstruktor class Trivia { int i; float f; public: Trivia(int n=0); Trivia(int k, float d); ~Trivia(); }; konstruktor ( c-tor ) • funkcja wywołana podczas tworzenia obiektu, po przydzieleniu (lub wskazaniu miejsca w) pamięci • nazwa taka sama jak nazwa klasy • niczego nie zwraca (ale nie piszemy void) • może występować w wielu odmianach, z różną liczbą argumentów (przeciążone wersje) • „domyślny” – taki, który można wywołać bez podania parametrów (czyli bezparametryczny lub z wartością/warościami domyślną/domyślnymi argumentów Czym się różni: Trivia::Trivia(int n) { i=n; f = 0; } // tu jest przypisanie od: Trivia::Trivia(int n) : i(n), f(0) { } // tu jest inicjalizacja Czy można pomieszać kolejność: Trivia::Trivia(int n, float d) : f(d), i(n) { /* … */ } lista inicjatorów konstruktora, „miejsce”, gdzie powstają i są inicjalizowane obiekty otwarcie { oznacza skonstruowanie obiektu „Można”, ale to wcale nie zmienia kolejności tworzenia obiektów (najpierw i, potem f), a kompilator ostrzeże o odwrotnej (niż zapisana w kodzie) inicjalizacji! Klasa – destruktor class Trivia { // to co poprzednio ~Trivia(); }; destruktor ( d-tor ) • funkcja wywoływana podczas usuwania obiektu • nazwa taka jak nazwa klasy poprzedzona znaczkiem ~ • jest tylko jeden destruktor, niczego nie zwraca • destruktor nie może mieć żadnych parametrów • destruktor powinien „posprzątać” wszelkie dynamicznie zaalokowane wewnątrz klasy zasoby • operator delete najpierw woła destruktor (potem zwalnia pamięć) • zgłoszenie wyjątku gwarantuje posprzątanie obiektów na stosie (wywołanie ich destruktorów) • wyskok za pomocą instrukcji goto też wywołuje destruktor Trivia::~Trivia() { cout << "Good bye" << endl; } Konstruktor kopiujący T::T (const T&) • służy do skonstruowana obiektu, który jest kopią innego, już istniejącego obiektu tej klasy (inicjalizator kopiujący) Foo::Foo( Foo& ); – może posiadać również argumenty domyślne Foo::Foo(Foo&, float = 3.14); – może być w postaci Foo::Foo( const Foo& ); Foo::Foo( volatile Foo& ); Foo::Foo( const volatile Foo& ); • jeśli go nie ma, kompilator sam go utworzy, na zasadzie tworzenia wiernej kopii (bit po bicie) Konstruktor kopiujący T::T (const T&) • generowany konstruktor kopiujący bezpieczny (const) chyba że któryś składnik klasy ma swój konstruktor kopiujący bez przydomka const • jeśli klasa zawiera obiekty abstrakcyjne, to do kopiowania wołane są ich konstruktory kopiujące • kiedy pracuje copy constructor: – wywołanie jawne (inicjalizacja przez przypisanie) Foo nowy = stary; // stary też klasy Foo Foo nowy = Foo(stary); // ale nie: nowy = stary; tu pracuje operator = – przekazanie jako argument funkcji przez wartość – zwrócenie wartości funkcji (obiekt tymczasowy inicjalizowany konstruktorem kopiującym – zależy od optymalizacji kompilatora) Konstruktor kopiujący – kiedy konieczny? class A { A::A(const A& src) { // klasa bez konstruktora kopiującego numer = src.numer; int numer; nazwa = new char[src.strlen()+1]; char* nazwa; strcpy(nazwa, src.nazwa); }; } // gdzieś w programie: // konstruktor tworzy dynamiczną tablicę, // do której kopiuje słowo "Trzy" A a1(3, "Trzy"); A a2 = a1; // a2 to wierna kopia a1 a2.setNumber(4); a2.setName("Cztery"); cout << "a1 nazwa: " << a1.getName(); // "Cztery" ! • Prawdziwa tragedia w chwili likwidowania obiektów, destruktory dwa razy spróbują usuwać tablicę pod tym samym adresem • Analogiczny problem mamy gdy stosujemy operator przypisania = • Zwykle w klasie, w której występują wskaźniki, konieczne jest napisanie konstruktora kopiującego Zbudujemy klasę Definicję klasy zapiszmy w pliku tstring.h #ifndef TSTRING_H #define TSTRING_H #include <cstring> // w pliku nagłówkowym NIE // otwieramy przestrzeni std class TString { public: // interfejs private: // implementacja // składowe klasy pola dostępu do klasy z zewnątrz Zapiszmy też prosty plik main.cc #include ”tstring.h” #include <iostream> using namespace std; int main () { TString s1; } size_t jest nazwą (typedef) na bezznakowy typ całkowity wystarczająco pojemny aby opisać wielkość dowolnego obiektu [ m.in. zwracany przez sizeof ] class TString { protected: private: }; // pamiętaj o średniku #endif }; // póki nie będziemy dziedziczyć, // to pole nas nie interesuje char* ptr; std::size_t size; Zdefiniujmy konstruktor Zdeklarujmy konstruktor (c-tor) : class TString { public: TString( const char* s = nullptr ); }; Metody definiujemy w tstring.cc #include ”tstring.h” #include <iostream> using namespace std; TString::TString( const char* s ) : ptr(nullptr), size(0) { if (s > 0) { Kompilujemy dodatkowo dodając w linii size = strlen(s); ptr = new char[ size + 1 ]; opcję –D definiowananazwa czyli –D DEBUG (może być bez spacji), przykładowo: strcpy( ptr, s ); g++4.8 –std=c++11 –DDEBUG } main.cc tstring.cc –o prog #ifdef DEBUG cout << "TString c-tor " << size << " - " << ( ptr ? ptr : "pusty" ) << endl; #endif Możemy teraz dopisać w main kolejny obiekt, np. } TString s2("inicjalizacja slowem"); Zdefiniujmy destruktor Zdeklarujmy destruktor (d-tor): class TString { public: TString( const char* s = nullptr ); ~TString(); }; TString::~TString() { Definicję destruktora, jak i wszystkich kolejnych metod składowych klasy, dopisujemy jako ciąg dalszy (czyli poniżej definicji konstruktora) w pliku tstring.cc w pliku tstring.cc jako dalsza część #ifdef DEBUG cout << "TString d-tor " << size << " - " << ( ptr ? ptr : "pusty" ) << endl; #endif delete [] ptr; } Śledzenie pokrokowe programu (debuger) gdb Kod trzeba skompilować z flagą –g (oraz nie używać flag optymalizujących takich jak –O –O2 itd.) http://www.yolinux.com/TUTORIALS/GDB-Commands.html Zdefiniujmy konstruktor kopiujący Zdeklarujmy konstruktor kopiujący (cc-tor): // poniżej to nie jest przypisanie class TString { public: TString( const char* s = 0 ); TString( const TString& s ); ~TString(); }; if (size > 0) { ptr = new char[ size + 1 ]; strcpy( ptr, s.ptr ); } Możemy dopisać w main.cc TString s3 = s2; // albo tak: TString s3 ( s2 ); // albo tak: TString s3 { s2 }; TString::TString( const TString& s ) : ptr(nullptr), size( s.size ) { w pliku tstring.cc jako dalsza część Operacja podobna do tej z konstruktora. #ifdef DEBUG cout << "TString cc-tor " << size << " - " << ( ptr ? ptr : "pusty" ) << endl; #endif } Zdefiniujmy operator przypisania kopiujący Zdeklarujmy operator= kopiujący: class TString { public: TString& operator= ( const TString& s ); }; Możemy dopisać w main.cc // poniżej jest przypisanie, bo obiekt // po lewej już istnieje s3 = ”alfa beta”; s3 = s2; TString& TString::operator= (const TString& s ) { if ( this != &s ) { // if ( *this != s ) { delete [] ptr; ptr = nullptr; size = s.size; if ( size > 0 ) { this – specjalny wskaźnik, który otrzymuje ptr = new char[ size + 1 ]; każda niestatyczna składowa klasy, a w którym strcpy( ptr, s.ptr ); zapisany jest adres bieżącego obiektu, na } którego argumentach działać ma metoda } #ifdef DEBUG cout << "TString copy operator= " << size << " - " << ( ptr ? ptr : "pusty" ) << endl; #endif return *this; // nie zapomnij zwrócić obiektu! } Zdefiniujmy konstruktor przenoszący Konstruktor przenoszący (mvc-tor): class TString { public: }; TString( TString&& s ); Możemy dopisać w main.cc // move „maskuje” tożsamość obiektu TString s4 = std::move( s2 ); // std::move będzie niepotrzebne // jeśli inicjalizować będzie obiekt // tymczasowy np. zwracany przez // funkcję jako wartość TString::TString( TString&& s ) : ptr(s.ptr), size(s.size) { // obiekt źródłowy zostaje pozbawiony zasobów // ale pozostawiony w stanie do dalszego użytku czyli można coś np. do niego przypisać s.ptr = nullptr; Operacja przenoszenia dzieje się automatycznie s.size = 0; wtedy, gdy obiekt źródłowy „nie ma nazwy” i „nie ma adresu” #ifdef DEBUG cout << "TString mvc-tor " << size << " - " << ( ptr ? ptr : "pusty" ) << endl; #endif } Zdefiniujmy operator przypisania przenoszący Zdeklarujmy operator= przenoszący: class TString { public: TString& operator= ( TString&& s ); Możemy dopisać w main.cc // ponownie „ukrywamy obiekt” // za pomocą std::move s3 = std::move( s1 ); TString& TString::operator= }; ( TString&& s ) { if ( this != &s ) { delete [] ptr; // usuń dotychczasowy zasób size = s.size; // typy proste się tylko (po prostu) kopiuje ptr = s.ptr; // tu zabieramy adres wskaźnika (przeniesienie praw własności) s.size = 0; // obiekt, któremu zabraliśmy, zerujemy s.ptr = nullptr; // wskaźnik również zerujemy } #ifdef DEBUG cout << "TString move operator= " << size << " - " << ( ptr ? ptr : "pusty" ) << endl; #endif return *this; // nie zapomnij zwrócić obiektu! } Konwersja typów – konstruktor konwersji • definiujemy konstruktor, który ma jeden argument – obiekt (lub referencję) innego typu, za jego pomocą kompilator dokona automatyczną konwersję typów • klasa docelowa jest odpowiedzialna za konwersję typów class A { /* … */ }; class B { public: B(const A&) { /* … */ } }; void fun(B argb); // gdzieś w programie: A obiektA; fun(obiektA); // wymagany obiekt klasy B // kompilator wie jak przekonwertować B na A B obiektB = obiektA // zaskakujące? // działa (cc-tor klasy B) c-tor konwersji A na B Konwersja typów – operator konwersji • słowo operator, poprzedzające nazwę typu, do którego ma zostać dokonana konwersja (przeciążanie operatora) • klasa źródłowa jest odpowiedzialna za konwersję typów • tylko tak można zdefiniować konwersję z typów abstrakcyjnych do typów wbudowanych class A { public: float r, s; char* nazwa; const char* cNazwa; A(float f1 = 1.0, float f2 = 3.14); operator B() const { return B(r); } operator char*() const { return nazwa; } operator const char*() const { returnc Nazwa;} }; class B { // … B(int n); }; void fun(B argb); // gdzieś w programie: A obiektA; fun(obiektA); // działa operator konwersji fun(22); // działa konstruktor klasy B Konwersja typów – explicit Konstruktor konwersji: • Jeśli nie chcemy niejawnego (automatycznego) konwertowania, należy deklarację konstruktora poprzedzić słowem kluczowym explicit B(const A&); • Wtedy można tylko jawnie: fun(B(obiektA)); obiektB = B(obiektA); Operator konwersji: (C++11 – tylko w nowym standardzie) • Jeśli nie chcemy niejawnego (automatycznego) konwertowania, należy deklarację operatora poprzedzić słowem kluczowym explicit operator A(); • Wtedy można tylko jawnie: obiektB = B(obiektA); albo obiektB = (B)obiektA; albo obiektB = static_cast<B>( obiektA ); Konwersja typów – konflikty class A { public: A(const B); }; class B { public: operator A() const; }; void fun(A a); // gdzieś w programie: B b; fun(b); // niejednoznaczność • • • "przeciążenie wyjścia" class A { /* … */ }; class B { /* … */ }; class C { public: operator A() const; operator B() const; }; // tu się zaczyna problem // przeładowane wersje fun void fun(A a); void fun(B b); // gdzieś w programie: C c; fun(c); // niejednoznaczność Należy się zdecydować na jeden sposób konwersji Konwersja jest jednostopniowa (tzn. jeśli mamy zdefiniowane B→A i C→B, to jeśli na rzecz argumentu typu C zostanie podany argument typu A, nie nastąpi łańcuch konwersji od C do A Najpierw sprawdzana jest dwuznaczność, potem kontrola dostępu dziedziczenie [ inheritance ] • technika definiowania nowej klasy z wykorzystaniem już istniejącej • klucz do tworzenia relacji dziedziczenia to określenie wspólnego zachowania klas • nie potrzebujemy kodu źródłowego, tylko plik nagłówkowy – możemy np. dziedziczyć z klas bibliotecznych (które potem linkujemy) class B : public A { /* ... */ }; lista pochodzenia A – klasa podstawowa (bazowa) B – klasa pochodna klasy A klasa pochodna • dziedziczy wszystkie składniki klasy podstawowej (atrybuty i zachowanie) • można w niej zdefiniować – dodatkowe dane składowe – dodatkowe funkcje składowe • można w niej przedefiniować – składniki / funkcje już istniejące w klasie podstawowej – redefiniowany składnik zasłania składnik z klasy podstawowej relacja dziedziczenia – znaczenie • relacja: jest – czymś • relacja: uogólnienie – uszczegółowienie ( klasa bazowa – klasa pochodna ) • klasa pochodna może – rozszerzać możliwości klasy bazowej (implementacja nowych metod) – uściślać (ponowna implementacja metod istniejących w klasie bazowej) Klasa pochodna zawsze może być traktowana jako klasa bazowa (w dziedziczeniu publicznym), oznacza to, że: – można wskaźnikiem (referencją) klasy bazowej pokazywać na obiekty klas pochodnych i nie jest to operacja powodująca utratę części wskazywanego obiektu – dziedziczenie prywatne nie jest prawdziwym dziedziczeniem sposoby dziedziczenia (public, protected, private) klasa bazowa A klasa pochodna B private public protected protected public public dziedziczenie interfejsu • dostęp do części prywatnej klasy bazowej A tylko przez jej interfejs • mamy dostęp do części public i protected z tym że protected na zewnątrz niedostępny (tak samo jak private) klasa bazowa A klasa pochodna B private protected protected protected public klasa bazowa A klasa pochodna B private private private protected public dziedziczenie implementacji • domyślny, niepodanie specyfikacji oznacza dziedziczenie private class B : A { /* ... */ }; • stosujemy gdy chcemy ukryć fakt dziedziczenia deklaracja dostępu (using) • umożliwia selektywne zachowanie sposobu dziedziczenia składowych • należy umieścić w wybranej części klasy pochodnej using klasa_podstawowa::nazwa_skladnika; można również według starego przepisu (bez słowa using) klasa_podstawowa::nazwa_skladnika; za pomocą using można zachować (powtórzyć) zakres dostępu z klasy bazowej lub zmienić z protected na public (i vice versa) • class A { // niedostępne w klasie pochodnej int n; void getVal(int); protected: int k; int calc(); public: int calc(int); void getVal(); }; class B : private A { protected: using A::k; using A::calc; // nie rozróżnia nazw przeciążonych public: using A::getVal; // nie zadziała bo getVal jest też }; // w części private • deklaracja dostępu nie może posłużyć do odsłonięcia nazwy zasłoniętej w klasie pochodnej, również w przypadku redefinicji funkcji (wirtualnej) • nie usuwa ew. wieloznaczności w dziedziczeniu wielokrotnym (najpierw zawsze jest rozstrzygana wieloznaczność) dziedziczenie kilkupokoleniowe i inicjalizacja A B C • klasa B jest dla klasy C klasą podstawową bezpośrednią, zaś klasa A – klasą podstawową pośrednią • inicjalizowanie klasy podstawowej poprzez wywołanie jej konstruktora C::C(int i, float f) : B(i,f) { // ... B::B(int i, float f) : A(i) { // ... lista inicjatorów konstruktora • wywołujemy tylko konstruktor bezpośredniej klasy podstawowej • jeśli tego nie zrobimy, użyty będzie konstruktor domyślny, kolejność jest “od góry” (klasa A), “do dołu” (klasa C) • gwarantowane jest też wywołanie destruktorów, w kolejności odwrotnej (czyli od C do A) kompozycja i dziedziczenie // wcześniej definiujemy klasy: MW, MX, MY, MZ oraz klasę A class B : public A { MY my; • kolejność wywołania konstruktorów MX mx; elementów składowych jest związana public: z kolejnością ich wystąpienia B(int i) : mx(), my(), A() { /*...*/ } w definicji klasy, a nie z kolejnością ~B(); na liście inicjatorów }; class C : public B { • w przykładzie po lewej, MW mw; kolejność konstrukcji: MZ mz; A, MY, MX, B, MW, MZ, C public: C() : mw(3.14), B(45) { /*...*/ } • kolejność destrukcji ~C(); jest dokładnie odwrotna: }; C, MZ, MW, B, MX, MY, A ukrywanie nazw w klasach pochodnych • przedefiniowanie (redefining) w przypadku zwykłych funkcji składowych klasy bazowej • zasłanianie (overriding) w przypadku funkcji wirtualnych klasy bazowej class A { public: int fun() const; int fun(float) const; }; class B : public A { public: int fun() const; // przedefiniowanie }; class C : public A { public: void fun() const; // zmiana zwracanego typu }; class D : public A { public: int fun(char*) const; // zmiana listy argumentów }; we wszystkich przypadkach niewidoczne (zasłonięte) stają się również funkcje przeciążone w klasie bazowej, tzn. tutaj: int fun(float) const; gdyby w klasie A była metoda prywatna, to dostępu do niej nie mamy w klasach pochodnych, ale możemy ją przedefiniować !!! tak, że będzie działać nasza nowa wersja, tak jakby była tą funkcją składową z części prywatnej A czego się nie dziedziczy (C++98) • konstruktory (patrz C++11) • operator= • destruktor KONSTRUKTOR KOPIUJĄCY • • • trzeba je zdefiniować samemu (lub zostaną wygenerowane automatycznie!) • można jednak we własnych definicjach wywołać wersje z klas podstawowych do obsłużenia odziedziczonej części obiektu ten generowany automatycznie wykorzysta konstruktory kopiujące klas-przodków i składników – chyba że któryś z tych konstruktorów kopiujących jest prywatny – uwaga: definicja jakiegokolwiek konstruktora (np. właśnie kopiującego) wyklucza automatyczne generowanie zwykłego konstruktora definiowany przez nas może je wywołać jawne wywołanie konstruktora class A { public: kopiującego klasy A, inaczej zostałby A(const A& a); wywołany zwykły konstruktor }; domyślny klasy A class B : public A { public: B(const B& b) : A(b) { /* ... */ } }; dziedziczenie konstruktorów ( C++11 ) Deklaracja using może być użyta z konstruktorami klasy bazowej class Foo { public: explicit Foo(int); // explicit jako przykład „dobrego stylu” void fun(); }; class Bar : public Foo { public: using Foo::fun; // tu nic nowego, w zasadzie niepotrzebne using Foo::Foo; // powoduje niejawną deklarację Bar::Bar(int); // taki konstruktor zdefiniowany/wygenerowany tylko w przypadku użycia void fun(); // nadpisuje Foo::fun() Bar( int, int ); // tu już samemu napisany konstruktor, bez dziedziczenia }; Bar b1( 7 ); // ok w C++11 dzięki dziedziczeniu konstruktora Bar b2( 3, 5 ); // normalne wywołanie Bar::Bar(int, int); Dziedziczone konstruktory zachowują swoją specyfikację (tzn. są np. explicit lub są wyrażeniem stałym constexpr). dziedziczenie konstruktorów – dostępność, inicjalizacja składników ( C++11 ) Może się okazać, że odziedzczony konstruktor jest prywatny class Foo { private: explicit Foo(int); }; class Bar : public Foo { public: using Foo::Foo; błąd objawia się private: w momencie próby użycia string s; int x, y; }; Bar b1( 7 ); // błąd – woła Bar(int), który woła Foo(int), a ten jest niedostępny Jeśli klasa potomna ma jeszcze jakieś składowe, to użycie dziedziczonego konstruktora jest ryzykowne. Składowe klasy Bar będą albo domyślnie inicjalizowane (s) albo niezainicjalizowane (x, y). Oczywiście można: string s = ”niezainicjalizowany”; int x = 0, y = 0; czego się nie dziedziczy OPERATOR PRZYPISANIA operator= • ten generowany automatycznie wywoła operatory= klasy-przodka i składników – chyba, że któryś z tych operatorów jest prywatny – chyba, że są składniki const lub składniki będące referencją – bo te wymagają inicjalizacji musi być podany zakres ( A:: ) • definiując operator= możemy je użyć class A { public: A& operator=(const A& a); }; class B : public A { public: B& operator=(const B& b) { A::operator=(b); // ... return *this; } }; ponieważ nowodefiniowany B::operator= przesłania funkcję operatora klasy bazowej alternatywnie mozna tak: (*this).A::operator=(b); lub A *wsk = this; // możemy wskaźnikiem klasy bazowej (*wsk) = b; // pokazać na obiekt pochodny lub A &ref = *this; // możemy referencji do klasy bazowej ref = b; // przypisać obiekt klasy pochodnej co jest dziedziczone i warto wspomnieć • składniki statyczne i oczywiście definiujemy je dla klasy w której są zdeklarowane – możemy je zasłaniać class A { public: static int ca; static int getNew() { return ca; } }; class B : public A { public: static int ca; static int getNew() { return ca; } // zasłania funkcję z klasy A static int getOld() { return A::ca; } // tak możemy się dostać do “starej” wartości }; int A::ca = 2; int B::ca = 5; // z powodu re-deklaracji w klasie B, musimy zdefiniować • statyczne funkcje składowe – gdy przedefiniowane – zasłaniają funkcje z klasy podstawowej (wszystkie przeciążone wersje), również wtedy gdy następuje zmiana sygnatury funkcji co jeszcze jest dziedziczone • operatory konwersji typów – bo w klasach pochodnych mamy komplet informacji do wykonania konwersji • konstruktory konwersji nie są dziedziczone, ale… class C { public: C(int n) : c(n) {} int c; }; class D : public C { public: D(int n) : C(n+3), c(n) {} int c; }; class A { public: A(int n) : a(n) {} A (const C& c) : a(c.c) {} int a; }; class B : public A { public: B(int n) : A(n+2), a(n) {} int a; }; void fun(const A& a) { cout << "a.a = " << a.a << endl; } void fun2(const B& b) { cout << "b.a = " << b.a << endl; } int main() { C c(11); D d(22); A a(33); B b(44); fun(c); // normalnie, wypisze 11 – konwersja typu fun(b); // co wypisze? 44 czy 46? fun(d); // co wypisze? 22 czy 25? fun2(c); // błąd – bo konstr. konwersji się nie dziedziczy } obiekt klasy B pokazywany referencją do klasy bazowej A jest widziany jako obiekt klasy A, więc wypisana jest część obiektu z klasy A (tu zasłonięta w klasie B) obiekt klasy D jest również obiektem typu klasy C, więc możliwa jest konwersja obiektu typu D na obiekt typu A, wypisana jest ta część obiektu z klasy C (tu zasłonięta w klasie D) rzutowanie w górę (upcasting) i w dół • jest bezpieczne bo od typu bardziej wyspecjalizowanego przechodzimy do typu bardziej ogólnego • jest naturalne: wskaźnikiem (referencją) typu bazowego pokazujemy na typ pochodny class A { public: int a; }; class B : public A { public: int b; }; // gdzieś w programie… B b; A *wskA = &b; A &refA = b; – poprzez wskA i refA oczywiście nie mamy dostępu do części zdefiniowanej w klasie B (tzn. int b), ale np. poprzez jawne rzutowanie (w dół !) można się tam dostać • co jeśli przez wartość? A a = b; – to też dopuszczalne, ale następuje nieodwracalna strata części obiektu klasy B (tu zadziała konstruktor kopiujący z klasy A, który nic nie wie o dodatkowej części z klasy B) polimorfizm – czego oczekujemy? class A { public: void getMe() { cout << "Jestem A/n"; } }; class B : public A { public: void getMe() { cout << "Jestem B/n"; } }; class C : public B { public: void getMe() { cout << "Jestem C/n"; } }; // …gdzieś w programie B b; C c; A *ptrA = &b; A &refA = c; A a = b; ptrA->getMe(); // "Jestem A" refA.getMe(); // "Jestem A" a.getMe(); // "Jestem A" to nas nie zadowala, bo przecież pokazywane są obiekty klas pochodnych chcielibyśmy, żeby wskaźnik (referencja) inteligentnie reagowały na typ obiektu na który pokazują, wołając jego funkcję… polimorfizm - rozwiązanie class A { public: w klasie bazowej virtual void getMe() { cout << "Jestem A/n"; } (tutaj klasie A) }; musimy w deklaracji class B : public A { public: funkcji dodać void getMe() { cout << "Jestem B/n"; } virtual }; class C : public B { public: void getMe() { cout << "Jestem C/n"; } funkcja getMe() jest wirtualna w każdej }; klasie pochodnej, można (ale nie trzeba // …gdzieś w programie bo jest to mylące) dopisać "virtual" również w klasie B i C… B b; C c; • ściśle rzecz biorąc polimorficzne jest A *ptrA = &b; wywołanie funkcji, a nie funkcja A &refA = c; • klasa, w której jest zdefiniowana A a = b; lub odziedziczona funkcja wirtualna, ptrA->getMe(); // "Jestem B" nazywa się klasą polimorficzną refA.getMe(); // "Jestem C" a.getMe(); // "Jestem A" – nieodwracalne "przycięcie" do A funkcje wirtualne – kilka szczegółów • • • • • funkcja globalna nie może być wirtualna (bo przecież polimorficzne orientowanie ze względu na typ obiektu…) funkcja wirtualna nie może być statyczna funkcja wirtualna może być przyjacielem jakiejś innej klasy, ale tylko konkretna realizacja funkcji wirtualnej z danej klasy jest tym przyjacielem (a nie wszystkie funkcje) bo przyjaźni się nie dziedziczy w klasie pochodnej można zasłonić funkcję wirtualną z klasy bazowej (definicja obiektu lub innej funkcji o tej samej nazwie), ale w kolejnej klasie pochodnej (do klasy pochodnej) można ją znów zdefiniować i korzystać z polimorfizmu jeśli zmienia się zakres dostępu dla funkcji wirtualnej, np. w klasie bazowej funkcja ta była public, a w klasie pochodnej jest protected lub private sposób dostępu taki jak w typie użytego wskaźnika lub referencji class A { public: virtual void f() { cout << "Jestem A" << endl; } }; class B : public A { private: void f() { cout << "Jestem B" << endl; } dostęp rozstrzygany }; na poziomie wiedzy int main() wyniesionej z klasy { bazowej A, bo pokazujemy A *ptrA = new B; wskaźnikiem klasy bazowej ptrA->f(); // "Jestem B" B &refB = dynamic_cast<B&>(*ptrA); refB.f(); // błąd - virtual void B::f() is private } konstruktor, destruktor – wirtualny • konstruktory nie są dziedziczone (C++98), nie mogą być wirtualne – żeby zadziałał polimorfizm to musi być pokazywany obiekt danego typu (wskaźnikiem, referencją), a tego obiektu "jeszcze nie ma", jest konstruowany • destruktor – nie jest dziedziczony, ale tak! Jeśli klasa posiada choć jedną deklarację funkcji jako virtual, jej destruktor też deklarujmy jako virtual – wtedy destruktory klas pochodnych też będą virtual – działać będzie polimorfizm i poprawna destrukcja obiektu dziedziczenie – klasa abstrakcyjna • • • tworzona po to, aby być klasą bazową do dziedziczenia będziemy korzystać z polimorfizmu (virtual) implementacja metod niepotrzebna, deklaracja interfejsu virtual void funkcja() = 0; // czysto wirtualna – ta wersja funkcji nigdy nie ma być wykonana, konieczność implementacji (uściślenia) w klasie pochodnej – klasa jest abstrakcyjna gdy ma choć jedną funkcję wirtualną – dziedziczona jako czysto wirtualna, więc jeśli nie ma jej definicji w klasie pochodnej, klasa pochodna też jest klasą abstrakcyjną – nie można stworzyć żadnego obiektu klasy abstrakcyjnej – funkcja nie może zwracać przez wartość obiektu klasy abstrakcyjnej – nie może być typem w jawnej konwersji FUNKCJE WIRTUALNE i ich ciała virtual void funkcja() { } // zwykła, musi mieć definicję virtual void funkcja() = 0; // pure virtual, bez definicji ► może mieć definicję, umieszcza się ją poza ciałem klasy → taką funkcję można wywołać tylko wprost (z operatorem zakresu) czyli klasa::funkcja() lub z wnętrza konstruktora (destruktora) klasy, w której jest ona czysto wirtualna → niezdefiniowanie ciała funkcji "pure virtual" w którejś z kolejnych klas pochodnych, czyni z tej klasy pochodnej znowu klasę abstrakcyjną dziedziczenie kontra zawieranie – przykład uniwersytecki TOsoba TOsoba nazwisko adres data urodzenia TStudent status wydział kursy nazwisko adres data urodzenia TNauczyciel funkcja kursy TOsoba nazwisko adres data urodzenia TStudent TNauczyciel funkcja kursy prowadzone status wydział kursy TDoktorant TDoktorant nie może się zapisywać na kursy podstawowe TDoktorantNaucz wielokrotne dziedziczenie spowoduje zapewne pojawienie się konfliktu niejednoznaczności, np. funkcja print() odziedziczona podwójnie… doktorant z obowiązkiem prowadzenia zajęć dydaktycznych dziedziczenie wielokrotne – alternatywa 1 1 • funkcje składowe implementacji klasy TDoktorantNaucz muszą wywoływać odpowiednie funkcje obiektów pomocniczych nauczycielProxy i doktorantProxy • mamy podwójne obiekty klasy TOsoba, więc trzeba zapewnić poprawne zarządzanie stanem gdy zmieniane są dane TOsoba, taka niespójność jest uciążliwa • zalety to lepsza hermetyzacja, implementator może udostępnić jedynie te funkcje, których klient powinien używać TNauczyciel TDoktorantNaucz 1 TDoktorant class TDoktorantNaucz { private: TNauczyciel nauczycielProxy; TDoktorant doktorantProxy; // sporo kodu do napisania }; dziedziczenie i zawieranie – alternatywa 2 • TDoktorantNaucz dziedziczy wszystkie cechy klasy TDoktorant, a pośrednio również TStudent i TOsoba, trzeba zaś napisać funkcje, które wiążą się z klasą TNauczyciel • nadal istnieje problem podwójnego obiektu klasy TOsoba, ale łatwiej nim zarządzać, korzystać z odziedziczonego po klasie TDoktorant, a kontrolując dostęp do TNauczyciel nie używać danych TOsoba z nim związanych TDoktorant TDoktorantNaucz 1 TNauczyciel class TDoktorantNaucz : public TDoktorant { private: TNauczyciel nauczycielProxy; // trochę kodu do napisania }; dziedziczenie wielokrotne – alternatywa 3 • TOsoba nazwisko adres data urodzenia TStudent wirtualna klasa bazowa TNauczyciel • funkcja kursy prowadzone status wydział kursy TDoktorant • TDoktorantNaucz wszystkie wirtualne klasy bazowe inicjalizuje się w konstruktorze ostatniej klasy pochodnej, czyli konstruktor klasy TOsoba trzeba wywołać przy tworzeniu obiektu klasy TDoktorantNaucz, jest to niewygodne jeśli konstruktor ostatniej klasy pochodnej nie wywołuje jawnie konstruktora wirtualnej klasy bazowej, kompilator próbuje wywołać domyślny konstruktor wirtualnej klasy bazowej łatwiej pisać kod, gdy wirtualna klasa bazowa posiada konstruktor domyślny, ale w naszym przypadku to nie ma sensu (nie ma przecież "domyślnego" nazwiska etc.) dziedziczenie wielokrotne - koszty TOsoba nazwisko adres data urodzenia TStudent wirtualna klasa bazowa TNauczyciel funkcja kursy prowadzone status wydział kursy TOsoba TNauczyciel TDoktorant TStudent TDoktorantNaucz TDoktorant istnienie konstruktora w klasie (np. domyślnego) zależy wyłącznie od projektu interfejsu, nie należy dodawać funkcji składowych tylko po to, aby uniknąć błędów kompilacji TDoktorantNaucz dziedziczenie – statyczna relacja • dziedziczenie jest relacją statyczną - trudno ją zmienić • kiedy relacje między klasami zmieniają się, przydatność dziedziczenia jest ograniczona • relacje w hierarchii dziedziczenia są określone i zakodowane na stałe • • • TOsoba { virtual } TAsystentBadan • TStudent TNauczyciel • TDoktorant TDoktorantNaucz chcemy dodać do naszej "abstrakcji uniwersytetu" asystenta badań, nie musi on być studentem i nie musi prowadzić zajęć dydaktycznych co jednak zrobić jeśli TDoktorant podejmie pracę jako TAsystentBadan, nawet na innym wydziale? problem wynika stąd, że "prowadzenie badań" to właściwość jaką może nabyć każda osoba, nie tylko student lub wykładowca w wyniku złożoności relacji zachodzi tu konflikt wymagań, którego nie da się rozwiązać za pomocą dziedziczenia dziedziczenie jest odpowiednim mechanizmem do modelowania tych relacji między klasami, które zawsze są spełnione klasa mieszana – mix-in-class • klasa mieszana pozwala na dodanie nowych możliwości do innych klas • nie tworzymy egzemplarza klasy mieszanej (nie ma to sensu) • użycie klas pozwala łączyć różne możliwości w nowe jednostki • klasy mieszane reprezentują statyczne relacje, nowe własności można dodać w trakcie projektowania hierarchii, nie zaś dynamicznie w trakcie wykonywania programu MozeBycStudentem TOsoba • • enum EWyksztalcenie { ePodstawowe, eSrednie, eLicencjat, eMagister, eDoktor }; clas MozeBycStudentem { public: void setWydzial( EWydzial dep ); EWydzial getWydzial() const; virtual bool zapiszNaKurs( const TKurs& ) = 0; virtual bool usunZKursu( const TKurs& ) = 0; virtual void pokazKursy() const = 0; virtual EWyksztalcenie getWyksztalcenie() const; // więcej kodu }; • TStudent chcemy dodać możliwość zostania studentem za pomocą klasy mieszanej MozeBycStudentem klasa ta dodaje metody potrzebne do zapisania się na kursy oraz do identyfikacji studenta w klasie TStudent trzeba zaimplementować wszystkie wirtualne metody dziedziczone po MozeBycStudentem, w której można też zdefiniować jakąś domyślną implementację klasy mieszane - dyskusja Dodajemy dalszą funkcjonalność za pomocą klas mieszanych, to znaczy klasę reprezentującą osoby z kwalifikacjami do prowadzenia kursów MozeNauczac oraz do prowadzenia badań MozeWykBadania MozeWykBadania MozeNauczac TOsoba • • • klasa TOsoba nie musi już być wirtualną klasą bazową, co upraszcza zarządzanie kodem elastyczność i prostotę projektu uzyskuje się dzięki rozłożeniu możliwości na kilka klas w hierarchii z użyciem klas mieszanych można dodawać nowe możliwości bez wpływu na inne klasy w hierarchii MozeBycStudentem TStudent TAsystentBadan TNauczyciel TDoktorant TDoktorantBadacz TDoktorantNaucz Kiedy klasy mieszane? 1. istnieje wiele niezależnych właściwości, które klasa może posiadać 2. trzeba wybiórczo dodać nową własność do niektórych klas w istniejącej hierarchii dynamiczna zmiana sytuacji – czyli co po studiach? Wiemy już, że należy unikać niepotrzebnego powielania danych (wirtualne klasy bazowe) - bo powoduje to utratę zasobów i problemy z zarządzaniem tymi danymi Warto też do minimum ograniczyć ilość kopiowanych danych kiedy przekształcamy lub kopiujemy obiekt • • • • a co jeśli osoba jest doktorantem na jednym wydziale i równocześnie asystentem badań na innym? - do zarządzania potrzeba wtedy dwóch niezależnych obiektów TDoktorant oraz TAsystentBadan, a w obu powtarzają się dane części TOsoba a co jeśli osoba studiuje dwa kierunki? Widzimy brak elastyczności dziedziczenia wielokrotnego w dynamicznie zmieniających się sytuacjach - często ma miejsce w bazach danych kiedy student kończy studia i staje się doktorantem, zmiany w obiekcie powinny dotyczyć jedynie tych części, które rzeczywiście ulegają zmianie, czyli powinna istnieć możliwość dodania do obiektu TStudent części TDoktorant jeśli TDoktorant staje się obiektem TNauczyciel, możliwości klasy TDoktorant powinny zostać zmienione przez możliwości klasy TNauczyciel Jak przekształcić TStudent w TDoktorant? • trzeba utworzyć nowy obiekt TDoktorant i zainicjalizować go (skopiować dane) z obiektu TStudent • ponosimy tu niepotrzebne koszty kopiowania części TOsoba, która się przecież nie zmienia dynamiczna zmiana sytuacji – role Dana osoba może pełnić wiele ról, ale w konkretnym momencie pełni tylko jedną rolę Każda osoba posiada n ról jako członka uniwersytetu Każda rola należy tylko do jednej osoby (relacja "do kogo") Od każdego obiektu TCzlonekUniwersytetu można uzyskać informację o tym do kogo należy dana rola Obiekt TOsoba przechowuje listę wszystkich możliwych ról pełnionych przez daną osobę – nie powiela się danych osobowych Role są oddzielone od osoby, która je pełni, role tworzą odrębną hierarchię – do każdej osoby można przypisać dowolną liczbę ról, nawet tę samą rolę dwa razy (np. student dwóch kierunków) pełni rolę 0 .. n TOsoba TCzlonekUniwersytetu do kogo TStudent TNauczyciel TBadacz TDoktorant Implementacja – problem określania typu • Klasy TStudent, TNauczyciel, TBadacz posiadają różne metody ale wspólną klasę bazową TCzlonekUniwersytetu. Obiekt TOsoba zwraca za pomocą metody aktualną rolę danej osoby – ale jest to obiekt typu TCzlonekUniwersytetu • Polimorficzne używanie obiektów tej klasy bazowej może nie być zbyt użyteczne, ponieważ nie jest możliwe uchwycenie we wspólny interfejs zachowania wszystkich klas pochodnych • Konieczne jest poznanie rzeczywistego typu obiektu, czyli użycie mechanizmu RTTI (elastyczność kosztem złożoności kodu) role i ich konsekwencje Dostęp do danych TOsoba jest teraz możliwy tylko przez metody klasy TCzlonekUniwersytetu Obiekt TCzlonekUniwersytetu nie zależy od osoby, ale zawiera informacje potrzebne osobie do pełnienia danej roli Role są przenośne Niepotrzebne stają się klasy złożone, typu TDoktorantBadacz, ponieważ osobie można przypisać rolę badacza oraz rolę nauczyciela (w danej chwili pełniona jest tylko jedna z nich) Można więc powiązać konkretną rolę z wieloma osobami – można np. utworzyć grupę osób prowadzących te same badania, czyli pełniących taką samą rolę… Dwie osoby mogą prowadzić taki sam wykład (rola wykładowcy), sześć osób może prowadzić takie same ćwiczenia… klasy mieszane vs pełnione role Klasy mieszane dodają statyczne możliwości (decyzję trzeba podjąć podczas projektowania hierarchii klas) Utworzony obiekt może odpowiadać na komunikaty będące zawarte w klasie bazowej (klasach bazowych) Klasy mieszane łatwe do zrozumienia i implementacji MozeWykBadania MozeNauczac TAsystentBadan TOsoba Problem – gdy potrzeba wiele kombinacji różnych klas, może dojść do eksplozji kombinatorycznej • Hierarchie dziedziczenia wielokrotnego są trudniejsze do zrozumienia od hierarchii dziedziczenia jednokrotnego, dodanie wirtualnych klas bazowych komplikuje jeszcze bardziej MozeBycStudentem TStudent TNauczyciel TDoktorantBadacz TStudentBadacz Obiekty pełniące rolę to lepsze rozwiązanie w dynamicznie zmieniających się sytuacjach TDoktorant TNauczycielDoksztalc TDoktorantNaucz Można utworzyć obiekt TOsoba bez żadnych ról, które przypisze się później klasy mieszane a role – przypadki zastosowań Klasy mieszane dodają statyczne możliwości (decyzję trzeba podjąć podczas projektowania hierarchii klas) Utworzony obiekt może odpowiadać na komunikaty będące zawarte w klasie bazowej (klasach bazowych) Klasy mieszane łatwe do zrozumienia i implementacji Klasy mieszane a role role – lepsze gdy istnieje zbyt wiele możliwych kombinacji ról i kombinacje te mogą się zmieniać dynamicznie klasy mieszane – gdy kombinacja ról jest mała i jedna osoba może pełnić tylko jedną rolę danego rodzaju Problem – zależność od mechanizmu RTTI lub podobnych, potrzeba napisania dodatkowego kodu do używania i konwersji obiektów TCzlonekUniwersytetu • Klasy pochodne od klasy TCzlonekUniwersytetu trzeba określić w czasie kompilacji programu Obiekty pełniące rolę to lepsze rozwiązanie w dynamicznie zmieniających się sytuacjach Można utworzyć obiekt TOsoba bez żadnych ról, które przypisze się później