The great thing about Object Oriented code is that it can make small, simple problems look like large, complex ones.
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 są swego rodzaju kontenerami danych, klasy są bytami bardziej samoistnymi. Właściwości klas:
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.
Na laboratoriach będziemy wykorzystywać konwencję porządku deklaracji metod: kolejność deklaracji funkcji w klasie. Oraz wykorzystywali konwencję nazewnictwa metod, klas i pól klas z 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 znajduje się w pliku Point.cpp #include <cmath> #include <ostream> #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 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:
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 i destruktor nie zwracają żadnego typu.
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:
Punkt::Punkt( double x, double y){ this->x = x; this->y = y; }
Punkt p; p.setX(10).setY(12);
Pomyśl jak zaimplementować taką funkcjonalność z użyciem wskaźnika this. Uwaga: przydadzą się referencje!
Każda klasa może chronić dostępu do swoich pól i metod za pomocą trzech modyfikatorów dostępu:
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 <memory> #include <vector> #include <iostream> #include <sstream> #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<Point>(); auto ptr_p3 = make_unique<Point>(-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<Point> 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(); }
Przy pomocy klas można zamodelować w kodzie programu różne elementy, które można podzielić na kilka grup:
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.
W standardzie C++17 zostanie wprowadzony typ 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 <string> #include <experimental/optional> using ::std::experimental::optional; using ::std::experimental::make_optional; using ::std::string; using ::std::cout; using ::std::endl; optional<string> 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; } }
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;
#include <string> #include <cstdint>
#include <string> #include <experimental/optional> 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<string> SecondName() const; optional<string> 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_; }; }
View view{"Hello {{name}}!"}; cout << view.Render(map {{"name","Xavier"}});
std::string Render(const std::unordered_map <std::string, std::string> &model) const;
#include <string> #include <unordered_map>
#include <string> #include <iostream> #include <vector> #include <map> #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<JsonValue> js {JsonValue{56.6},JsonValue{45},JsonValue{"abc"s}}; map<string, JsonValue> 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; } }
std::experimental::optional<JsonValue> ValueByName(const std::string &name) const; std::string ToString() const;
#include <experimental/optional> #include <string>
#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