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.

Point.cpp
#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: kolejność deklaracji funkcji w klasie. Oraz wykorzystywali konwencję nazewnictwa metod, klas i pól klas z konwencja nazewnicza.

Point.h
#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

Point.cpp
//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, 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:

  1. 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;
    }
  2. 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!

  3. Przy przeciążaniu operatorów (patrz laboratorium 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:

  1. public - pole lub metoda znajdująca się w bloku public jest dostępna wszędzie
  2. protected - dostęp tylko dla klas dziedziczących po danej klasie i dla klas zaprzyjaźnionych
  3. 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 <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();
}

Wyodrębnianie klas

Przy pomocy klas można zamodelować w kodzie programu różne elementy, które można podzielić na kilka grup:

  1. obiekty wartości
  2. encje
  3. 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 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).

std::experimental::optional<std::string> MaybeString(bool create) {
  if (create) {
     return std::experimental::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

  1. 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. [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).
  3. [2 plusy] Napisz klasę wartości SimpleUrl, pozwalającą sparsować i przechowywać dane o URL. Powinnien być pomocny serwis 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. 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 <string>
      #include <cstdint>
  4. [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, jak i podanie osobno pierwszego imienia i nazwiska):
    • 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
  5. [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. 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 <std::string, std::string> &model) const;
    • Przestrzeń nazw: nets
    • Importy:
      #include <string>
      #include <unordered_map>
  6. [2 punkty] Napisz klasę pozwalającą zamodelować obiekt 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 <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;
      }
    }
    • Moduł: netsjson
    • Pliki z implementacją: SimpleJson.h/cpp
    • Używana struktura danych: JsonValue
    • Sygnatury metod w klasie View:
      std::experimental::optional<JsonValue> ValueByName(const std::string &name) const;
        std::string ToString() const;
    • Przestrzeń nazw: nets
    • Importy:
      #include <experimental/optional>
      #include <string>
  7. [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 
pl/dydaktyka/jimp2/2017/labs/klasy1.txt · ostatnio zmienione: 2017/07/17 08:08 (edycja zewnętrzna)
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0