====== Klasy i obiekty I ====== //The great thing about Object Oriented code is that it can make small, simple problems look like large, complex ones.// =====Czym jest klasa a czym obiekt===== Klasa definiuje nowy typ danych podobnie jak struktura, ale w znacznie szerszym znaczeniu. Obiekt to instancja danej klasy: jest to utworzona zmienna o typie określonym przez daną klasę. ====Struktury a klasy=== Struktury są swego rodzaju kontenerami danych, klasy są bytami bardziej samoistnymi. Właściwości klas: * Mogą posiadać **pola**, będące obiektami klas, lub zmiennymi typów prostych * Mogą posiadać **metody** - funkcje związane z daną klasą i mające nieograniczony dostęp do pól danej klasy * Mogą chronić dostępu do pewnych swoich pól i metod * Mogą być rozszerzane i rozbudowywane poprzez mechanizm dziedziczenia. =====Podział na pliki===== Deklaracja i definicja klasy powinny byc rozdzielone. Deklaracja klasy umieszczana jest w **pliku nagłówkowym** o rozszerzeniu **.h** a definicja w osobnym pliku o rozszerzeniu **.cpp**. Definiując metody klasy, używamy operatora zasięgu (**::**), aby wskazać, że definicja dotyczy metody klasy, a nie zwyczajnej funkcji z globalnej przestrzeni nazw. Np. #include "Point.h" ... void Point::ToString(ostream *out) const { (*out) << "(" << x << ";" << y << ")"; } ... Ponieważ programy składają się często z wielu plików, istnieje zagrożenie, że podłączając pliki nagłówkowe klas kompilator mógłby dołączyć deklaracje klasy kilkukrotnie. Przed takim niebezpieczeństwem służą dyrektywy preprocesora //#ifndef, #define, #endif//. Nazwa stałej definiowanej jest kwestią konwencji, np. CLion generuje stałe postaci **PROJECTNAME_FILENAME_H**. Użycie nazwy projektu chroni nas przed ewentualnym konfliktem nazw plików z zewnętrzną biblioteką. Jednak w przypadku dużych projektów w celu wyeliminowania możliwości konfliktu między popularnymi nazwami plików dobrze jest również dołączyć do stałej ścieżkę do pliku np. **EXCERCISES_LAB4_GEOMETRY_POINT_H**. To gwarantuje, że deklaracja klasy zostanie dołączona podczas kompilacji do całego programu tylko raz. ====Deklaracja klasy==== Na laboratoriach będziemy wykorzystywać konwencję porządku deklaracji metod: [[https://google.github.io/styleguide/cppguide.html#Declaration_Order|kolejność deklaracji funkcji w klasie]]. Oraz wykorzystywali konwencję nazewnictwa metod, klas i pól klas z [[https://google.github.io/styleguide/cppguide.html#General_Naming_Rules|konwencja nazewnicza]]. #ifndef PROJECTNAME_PATH_POINT_H_ #define PROJECTNAME_PATH_POINT_H_ class Point { public: //Konstruktor bezparametrowy Point(); //Konstruktor parametrowy Point(double x, double y); //Destruktor wykonywany przed zwolnieniem pamięci ~Point(); //Metody nie modyfikujące stanu obiektu (const na końcu metody) //nie mogą zmodyfikować tego obiektu. void ToString(std::ostream *out) const; double Distance(const Point &other) const; //metody akcesorów są publiczne i tylko w przy ich pomocy //można się dostać z zewnątrz do pól klasy double GetX() const; double GetY() const; //metody seterów pozwalające zmienić stan obiektu //po jego zainicjalizowaniu void SetX(double x); void SetY(double y); private: //w przeciwienstwie do pythona C++ wymaga jawnej deklaracji składowych pól klasy: double x_, y_; }; #endif // PROJECTNAME_PATH_POINT_H_ ==== Definicja klasy ==== //Definicja znajduje się w pliku Point.cpp #include #include #include "Point.h" using ::std::ostream; using ::std::endl; using ::std::pow; using ::std::sqrt; /* Aby wskazać, ze definicja funkcji dotyczy metody danej klasy stosujemy tzw. operator zasięgu - "::" */ //Specjalna inicjalizacja zmiennych. Zmienne są inicjowane //nim zostanie wywołane ciało konstruktora Point::Point():x_(0),y_(0){ cout << "Konstruktor bezparametrowy" << endl; } Point::Point(double x, double y){ cout << "Konstruktor parametrowy" << endl; x_ = x; y_ = y; } Point::~Point(){ cout << "Destruktor! Nic nie robie, bo nie musze zwalniać pamięci!"; cout << endl; } double Point::Distance(const Point &other) const{ return sqrt(pow(GetX()-other.GetX(),2)+pow(GetY()-other.GetY(),2)); } void Point::ToString(ostream *out) const{ (*out) << "(" << GetX() << ";" << GetY() << ")"; } =====Pola, metody, konstruktor, destruktor===== Pola to zmienne znajdujące się wewnątrz definicji klasy. Pola mogą być dowolnego typu. Zadaniem konstruktora jest zainicjowanie ich odpowiednimi wartościami. Metody to funkcje wykonujące jakieś operacje charakterystyczne dla danej klasy. Metody zdefiniowane w pliku nagłówkowym są traktowane przez kompilator jako funkcje //inline//. Może to poprawić wydajność w przypadku małych funkcji, ale pogorszyć w przypadku nagminnego stosowania tego mechanizmu. Do pól i metod dostajemy się dzięki następującym operatorom: * **. (kropka)** - operator kropki umożliwia dostę do pól i metod gdy obiekt jest zwykłą zmienną, lub referencją * **- > (strzałka)** - operator strzałki umożliwia dostęp do pól i metod, gdy działamy na wskaźniku do obiektu. Ponieważ klasy są bytami bardzo złożonymi, a ich polami mogą być w szczególności dynamicznie zaalokowane tablice, konieczne było zaprojektowanie mechanizmu tworzenia i usuwania obiektów danych klas. * **Konstruktor** - specjalna metoda klasy, która jest wywoływana automatycznie podczas tworzenia obiektu danej klasy. Konstruktor musi nazywać się tak jak klasa! Służy przede wszystkim do ustawiania wartości dla pól tworzonego obiektu. * //konstruktor domyślny// - jeśli nie zostanie zdefiniowany żaden konstruktor, obiekt i tak będzie można utworzyć dzięki tzw. konstruktorowi domyślnemu. Nie inicjalizuje on pól klasy żadnymi wartościami - jedynie przydziela danemu obiektowi pamięć. * **Destruktor** - podobnie jak konstruktor jest to specjalna metoda klasy wywoływana podczas niszczenia obiektu. Jeśli nie nie alokujemy dynamicznie pamięci, pisanie destruktora nie jest konieczne. Destruktor musi nazywać się tak jak klasa, ale poprzedzony jest znakiem tyldy. Konstruktor i destruktor nie zwracają żadnego typu. ===== Wskaźnik "this" ===== Każdy obiekt klasy ma pośród swoich pól wskaźnik o nazwie //this//, który wskazuje na niego samego (Odpowiednik **self** z Pythona). Wskaźnika //this// nie deklarujemy przy deklaracji metody - jest to wbudowany mechanizm. Istnieje kilka zastosowań wskaźnika. Poniżej przedstawione zostały dwa najpopularniejsze: - Odwoływanie się do własnych pól wewnątrz metody, gdy parametry przekazywane do niej przesłaniają zmienne klasy: Punkt::Punkt( double x, double y){ this->x = x; this->y = y; } - Wywołania kaskadowe. W klasie Punkt z poprzedniego laboratorium zaimplementowane zostały dwie metody ustawiające współrzędna x i y punktu. Z użyciem wywołań kaskadowych (//ang. fluent API//) ustawienie współrzędnych wyglądałoby tak:Punkt p; p.setX(10).setY(12); Pomyśl jak zaimplementować taką funkcjonalność z użyciem wskaźnika //this//. Uwaga: przydadzą się referencje! - Przy przeciążaniu operatorów (patrz laboratorium [[.:operatory|Przeciążanie operatorów]]). =====Modyfikatory dostępu===== Każda klasa może chronić dostępu do swoich pól i metod za pomocą trzech modyfikatorów dostępu: - **public** - pole lub metoda znajdująca się w bloku //public// jest dostępna wszędzie - **protected** - dostęp tylko dla klas dziedziczących po danej klasie i dla klas zaprzyjaźnionych - **private** - pola lub metody znajdujące się w tym bloku są dostępne tylko dla obiektów tej klasy i klas zaprzyjaźnionych ===== Korzystanie z klas i tworzenie obiektów ===== Aby wykorzystać klasę w pisanym programie konieczne jest włączenie jej pliku nagłówkowego za pomocą dyrektywy //#include//. Obiekty tworzymy tak samo jak zwykłe zmienne. Jeśli istnieje konstruktor parametrowy, to parametry podajemy w nawiasach po nazwie zmiennej. #include #include #include #include #include "Point.h" using namespace std; int main(void){ //wywołuje konstruktor domyślny Point p; Point p2 (); Point p3 {}; //brace initilizer preferowany //wywołuje konstruktor parametryczny Point p4 (12,34); Point p5 {30, 20}; const Point *ptr_p = new Point(3,4); p2.ToString(&cout); cout << ptr_p->Distance(p2) << endl; delete ptr_p; //parametry przekazywane do make_unique tworzące //wskaznik unique_ptr przyjmują argumety konstruktora parametrycznego //stąd możliwe są dwa wywowłania: auto ptr_p2 = make_unique(); auto ptr_p3 = make_unique(-15,90); stringstream ss; ptr_p2.ToString(&ss); ss << " i " ptr_p3.ToString(&ss); cout << "Odległość między punktami " << ss.str() << " wynosi " << ptr_p2->Distance(*ptr_p3) << endl; cout << "Zostanie wywołany destruktor punktów ptr_p2 i ptr_p3?" << endl; //ronica miedzy emplace_back a push_back: vector vp; //push_back kopiuje przekazany punkt na koniec wektora vp.push_back(Point {9,8}); //natomiast emplace_back tworzy obiekt na koncu wektora //argumenty przekazane do funkcji odpowiadają konstruktorowi parametrycznemu vp.emplace_back(5, -5); //wiec mozna tez wywołać: vp.emplace_back(); } =====Wyodrębnianie klas===== Przy pomocy klas można zamodelować w kodzie programu różne elementy, które można podzielić na kilka grup: - obiekty wartości - encje - usługi/strategie Na tych laboratoriach zajmiemy się głównie obiektami wartości natomiast pozostałe zostaną omówione na pozostałych zajęciach. Obiekty wartości stanowią najprostszy wariant zastosowania i modelują niezmienne wartości, tzn. raz po utworzeniu ich stan nie może się zmienić. Gdyby powyższa klasa Point nie miała metod SetX/Y byłaby właśnie obiektem wartości. Wartości pozwalają wprowadzić do kodu programu większy porządek łącząc dane z oczywistymi operacjami na tych danych. Na dodatek szczegóły implementacji zostają ukryte jako prywatne pola klasy przez co mogą się zmieniać bez potrzeby zmiany użytkowników klasy (przynajmniej dopóki nie będzie wymagana też zmiana metod dostępowych). Zostawia to furtkę na późniejsze poprawę wydajności pod względem pamięciowym lub czasowym bez zbędnego wstrzymywania prac nad resztą kodu. ===== Typ optional ===== W standardzie C%%++%%17 zostanie wprowadzony typ [[http://en.cppreference.com/w/cpp/experimental/optional|optional]], który wprowadza jasny przekaz w API, że wartości zawracanej z funkcji może potencjalnie nie być. (W C%%++%%14 można już używać tego typu, ale należy go zaikludować z gałęzi experimental). #include #include using ::std::experimental::optional; using ::std::experimental::make_optional; using ::std::string; using ::std::cout; using ::std::endl; optional MaybeString(bool create) { if (create) { return make_optional("CREATED"); } else { return {}; } } int main() { auto value = MaybeString(false); if (value) { cout << *value << endl; } else { cout << "EMPTY" << endl; } //albo jeszcze prosciej: cout << value.value_or("EMPTY") << endl; auto another = MaybeString(true); if (another) { cout << another->substr(0,3) << endl; } } ====== Ćwiczenia ====== - Przetestuj klasę Punkt. Zobacz kiedy wywołują się destruktory a kiedy konstruktory. Napisz funkcję która przyjmuje dwa obiekty klasy Punkt jako parametry i wyświetla odległość między nimi. Jak wywoływane są konstruktory i destruktory? Czy można bezpośrednio pobrać wartość współrzędnych punktu? - [2 plusy] Napisz klasę Square, która będzie posiadać jako pola obiekty klasy Point (4 wierzchołki). Napisz metody obliczające obwód (Circumference) i pole kwadratu (Area). - [2 plusy] Napisz klasę wartości SimpleUrl, pozwalającą sparsować i przechowywać dane o [[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax|URL]]. Powinnien być pomocny serwis [[http://regex101.com|regex101]] można tam wkleić listę urli z testów i "wykombinować" pasującego regexa. W celu poprawy czytelności kodu (C++11 nie obsługuje nazwanych grup:?:) można zdefiniować enum CapturingGroups {SCHEME=2,HOST=4, ...} i przy pomocy enuma odwoływać się do obiektu smatch. {{:pl:dydaktyka:jimp2:2017:labs:urls.txt|Do testów}} * Moduł: **netsurl** * Pliki z implementacją: **SimpleUrl.h/cpp** * Używana struktura danych: **SimpleUrl** * Sygnatury metod w klasie SimpleUrl: std::string Login() const; std::string Host() const; std::string Path() const; uint16_t Port() const; std::string Scheme() const; std::string Query() const; std::string Fragment() const; * Przestrzeń nazw: **nets** * Importy: #include #include - [2 plusy] Napisz klasę wartości Name, pozwalającą przechowywać pełne imię (imiona) i nazwisko osoby. Udostępniającą następujące akcesory (nie każda osoba musi mieć drugie imię a tym bardziej trzecie, konstruktor powinien być w stanie sparsować pełną nazwę w postaci pojedynczego łańcucha znaków): * FirstName (Thomas) * SecondName (Jorge) * ThirdName (Jelly) * Surname (Cucumber) * ToFullInitials (T. J. J. C.) * ToFirstNamesInitials (T. J. J. Cucumber) * ToSurnameNames (Cucumber Thomas Jorge Jelly) * ToNamesSurname (Thomas Jorge Jelly Cucumber) * IsBeforeBySurname - metody porównują słownikowo czy nazwisko jest przed podanym jako argument * IsBeforeByFirstName * Kod: #include #include namespace model { using ::std::string; using ::std::stringstream; using ::std::experimental::optional; class Name { public: explicit Name(const string &first_names_surname); string FirstName() const; optional SecondName() const; optional ThirdName() const; string Surname() const; string ToFullInitials() const; string ToFirstNamesInitials() const; string ToSurnameNames() const; string ToNamesSurname() const; bool IsBeforeBySurname(const Name &other) const; bool IsBeforeByFirstName(const Name &other) const; private: string first_name_; string second_name_; string third_name_; string last_name_; }; } - **[3 punkty + 1 punkt za zaliczenie testu Injection] Szablony to bardzo popularny sposób tworzenia dynamicznych stron Internetowych w różnych frameworkach takich jak Spring, Django, czy Rails itp... Istnieje wiele bibliotek wspierających renderowanie stron www na podstawie szablonów np. [[https://github.com/no1msd/mstch|mustache]]. Napisz silnik wspierający tworzenie tekstów na podstawie szablonu. Projekt ma się składać z pojedynczej klasy View, która jest w stanie przyjąć w konstruktorze szablon, a następnie wyrenderować go. Przy tej implementacji w szablonie można umieścić specjalne pola pomiędzy podwójnymi nawiasami klamrowymi z nazwą, które zostaną podmienione w trakcie rednerowania. Np. w wyniku wykonania kodu nastąpi podmienienie tekstu %%{{%%name%%}}%% na Xavier: **View view{"Hello {{name}}!"}; cout << view.Render(map {{"name","Xavier"}}); * Moduł: **netstemplateengine** * Pliki z implementacją: **SimpleTemplateEngine.h/cpp** * Używana struktura danych: **View** * Sygnatury metod w klasie View: std::string Render(const std::unordered_map &model) const; * Przestrzeń nazw: **nets** * Importy: #include #include - **[2 punkty] Napisz klasę pozwalającą zamodelować obiekt [[https://www.w3schools.com/js/js_json_datatypes.asp|JSON]], który jest jednym z najbardziej popularnych formatów wymiany danych w Internecie. Projekt ma się składać z pojedynczej klasy JsonValue, która posiada różne konstruktory odpowiadające różnym typom danych JSONa: liczba (zmienno przecinkowa jak i całkowita), łańcuch znaków, wartość logiczna, tablica wartości JSONa i obiekt. Obiekt stanowi mapę między nazwami, a wartościami które są dowolnego z wcześniejszych typów. **#include #include #include #include #include "SimpleJson.h" using ::std::vector; using ::std::map; using ::std::cout; using ::std::endl; using ::std::string; using ::nets::JsonValue; using ::std::literals::operator""s; int main() { vector js {JsonValue{56.6},JsonValue{45},JsonValue{"abc"s}}; map obj_v {{"values",JsonValue{js}},{"name",JsonValue{"Test name"}},{"age",JsonValue{13}}}; JsonValue obj {obj_v}; // {"age": 13, "name": "Test name", "values": [56.6, 45, "abc"]} kolejność argumentów nie ma znaczenia w przypadku obiektu cout << obj.ToString() << endl; cout << "name: " << obj.ValueByName("name")->ToString() << endl; cout << "values: " << obj.ValueByName("values")->ToString() << endl; cout << "age: " << obj.ValueByName("age")->ToString() << endl; //obiekty optional można traktować jak wartości boolean (true wartość obecna, false optional jest pusty) if (obj.ValueByName("xyz")) { cout << "is present" << endl; } else { cout << "is absent" << endl; } } * Moduł: **netsjson** * Pliki z implementacją: **SimpleJson.h/cpp** * Używana struktura danych: **JsonValue** * Sygnatury metod w klasie View: std::experimental::optional ValueByName(const std::string &name) const; std::string ToString() const; * Przestrzeń nazw: **nets** * Importy: #include #include - [4 plusy] Zdeklaruj i zdefiniuj klasę DynamicznaTablica. Napisz program testujący działanie klasy. Deklaracja klasy powinna wyglądać następująco:#ifndef DTAB_H #define DTAB_H class DTab{ private: double * tab; int length; int last; // Metoda rozszerzajaca rozmiar tablicy do rozmiaru podanego jako parametr void resize(int newSize); public: // Konstruktor bezparametrowy. Powinien tworzyć tablicę o rozmiarze 10. (wykorzystaj metode resize) DTab(); // Tworzy tablice o rozmiarze podanym jako parametr. (wykorzystaj metode resize) DTab(int initLength); // Destruktor. Uwaga! Tablicę tworzymy dynamicznie, czyli tutaj jest wymagany! ~DTab(); // Dodaje element do na koniec tablicy. Jeśli tablica jest za mała // rozszerza ją. (wykorzystaj metode resize) void add(double element); / Pobiera element z tablicy z podanego indexu double get(int index); // Ustawia element o danym indeksie na daną wartość void set(double element, int index); // wyświetla tablice. void print(); }; #endif