|
|
pl:dydaktyka:jimp2:2017:labs:klasy2 [2017/03/28 03:24] mwp [Metody const] |
pl:dydaktyka:jimp2:2017:labs:klasy2 [2019/06/27 15:50] |
====== Klasy i obiekty II ====== | |
| |
===== Zasady żądzące życiem obiektu ===== | |
| |
Zapoznać się z regułami przedstawionymi w dokumentacji na stacku | |
[[http://stackoverflow.com/documentation/c%2b%2b/1206/the-rule-of-three-five-and-zero#t=201703272317519853522|Zasada zera, zasada pięciu, zasada trzech]]. | |
| |
| |
<file cpp XXX.h> | |
class XXX { | |
//w zeszłym odcinku: | |
//domyślny konstruktor | |
XXX(); | |
//konstruktory parametryczne | |
XXX(int param); | |
XXX(const std::string name); | |
| |
//Rule of five:// | |
//1. konstruktor kopiujący | |
XXX(const XXX &xxx); | |
//2. konstruktor przenoszący | |
XXX(XXX &&xxx); | |
//3. operator przypisania kopiujący | |
XXX &operator=(const XXX &xxx); | |
//4. operator przypisania przenoszący | |
XXX &operator=(XXX &&xxx); | |
//5. Destruktor | |
~XXX(); | |
} | |
</file> | |
| |
**Reguła 3** była stosowana dla C%%++%%03 przed wprowadzeniem referencji do r-wartości i obejmowała funkcje: 1, 3, 5. **Reguła 5** mówi, że jeśli ktoś potrzebuje przedefiniować jedną z tych metod to najprawdopodobniej musi to zrobić z wszystkimi 5. | |
| |
==== Za co właściwie odpowiadają te metody? ==== | |
| |
Metody te są wywoływane w trakcie życia obiektu w celu oddelegowania odpowiedzialności za zarządzanie zasobami do samego obiektu. Są to operacje konstruowania obiektu jeszcze nie istniejącego na podstawie innego (już zainicjalizowanego), przypisywanie innego obiektu do istniejącego gdy oba już są zainicjalizowane lub niszczenie obiektu. | |
| |
Przykład: | |
<file cpp main.cpp> | |
int main() { | |
//Konstrukcja obiektu za pomocą domyślnego konstruktora | |
//nieciekawe | |
XXX old_xxx {}; | |
| |
//Konstrukcja obiektu na podstawie już zaincjalizowanego | |
XXX new_xxx {old_xxx}; | |
| |
//znowu nieciekawy konstruktor domyślny... | |
XXX another_xxx {}; | |
| |
//tutaj przypisujemy stan obiektu jednego do drugiego | |
//ale obydwa są już zaincjalizowane... | |
XXX another_xxx = new_xxx; | |
| |
//tutaj kończy się zakres funkcji main i wszystkie trzy obiekty tracą ważność | |
//zostają wywołane więc destruktory | |
} | |
</file> | |
| |
==== A co z przenoszącymi wersjami z C++11 ==== | |
| |
W przypadku kopiowania stary obiekt zostaje w nienaruszonym stanie (był zawsze przekazywany jako const &). Jeśli dokonujemy przenoszenia stary obiekt jest niszczony, a jego stan jest przenoszony do nowej lokalizacji, czyli liczba zasobów trzymanych w obiektach pozostaje stała... | |
| |
Przykład: | |
<file cpp main.cpp> | |
| |
XXX make_copy(XXX xxx) { | |
return xxx; | |
} | |
| |
int movement_example() { | |
//Konstrukcja obiektu za pomocą domyślnego konstruktora | |
//nieciekawe | |
XXX old_xxx {}; | |
| |
//Konstrukcja obiektu na podstawie już zaincjalizowanego | |
//z przeniesieniem old_xxx jest niszczony | |
XXX new_xxx {move(old_xxx)}; | |
| |
//znowu nieciekawy konstruktor domyślny... | |
XXX another_xxx {}; | |
| |
//tutaj przypisujemy stan obiektu jednego do drugiego | |
//ale obydwa są już zaincjalizowane... | |
//stan new_xxx teraz będzie w another_xxx, a sam new_xxx jest niszczony | |
XXX another_xxx = move(new_xxx); | |
| |
//tutaj zostanie wywoały konstruktor kopiujący (argument wysyłany przez wartość) | |
//a następnie konstruktor przenoszący (bo wartość tymczasowa wytworzona przez funkcję make_copy() | |
//i tak zaraz zginie | |
XXX one_more = make_copy(another_xxx); | |
//tutaj watość tymczasowa z funkcji make_copy już jest niszczona i wywoływany jest destruktor | |
| |
//... | |
| |
//tutaj kończy się zakres funkcji main i wszystkie trzy obiekty tracą ważność | |
//zostają wywołane więc destruktory (na wszystkich trzech, ale tylko jeden ma ważny stan) | |
} | |
</file> | |
| |
==== Co jest tymi zasobami? ==== | |
| |
Zasoby to wszystko co musi być najpierw zainicjalizowane, a następnie zniczone, ale nie przy użyciu konstruktora i destruktora. | |
Np. pamięć jako surowy wskaźnik (inicjalizacja new, zwalnianie delete), uchwyt do pliku (open, close), połączenie z bazą danych, itp... | |
| |
Jeśli obiekt nie posiada żadnych zasobów tylko składa się z typów prostych i kontenerów biblitecznych to można wykorzystać **regułę 0** i polegać na powyższych funkcjach automatycznie wygenerowanych przez kompilator. | |
| |
Jeśli jednak klasa **XXX** wygląda tak: | |
<file cpp XXX.h> | |
class XXX { | |
X() : name_{new char[1024]} { | |
| |
} | |
private: | |
char *name_; | |
}; | |
</file> | |
| |
Brak destruktora powoduje wyciek pamięci, która nigdy nie jest zwalniana: | |
<file cpp XXX.h> | |
class XXX { | |
//... | |
//5. destruktor: | |
~XXX() { | |
delete [] name_; | |
} | |
</file> | |
| |
Brak konstruktora może teraz doprowadzić do tragedii wielokrotnej próby zwolnienia tej samej pamięci: | |
<file cpp main.cpp> | |
int main() { | |
XXX old; | |
XXX new_xxx{old}; | |
//teraz zarówno old jak i new_xxx pokazują na ten sam fragment pamięci | |
//... niedobrze bo zaraz zostanie uruchominy destruktor pierwsego i drugiego obiektu | |
//i drugi destruktor będzie chciał zwolnić drugi raz ten sam fragment pamięci... | |
} | |
</file> | |
| |
Więc: | |
<file cpp XXX.h> | |
class XXX { | |
//... | |
//konstruktor kopiujący: | |
XXX(const XXX& xxx) { | |
size_t sz = strlen(xxx.name_); | |
name_ = new char[sz]; | |
strcpy(name_,xxx.name); | |
//Teraz nowy obiekt pokazuje na nowy fragment pamięci, | |
//ale ze skopiowaną informacją | |
} | |
//operator przypisania: | |
XXX & operator=(const XXX& xxx) { | |
//jeśli ktoś wpadł na pomsył x = x; | |
if (this == &xxx) { | |
return xxx; | |
} | |
//w przyciwynym wypadku mamy x = y; | |
//musimy sami zwolnic pamięć po x (czyli this): | |
delete[] name_; | |
//i wreszcie kopiowanie, ten kod jest | |
//jest identyczny więc można by go wydzielić do innej metody... | |
size_t sz = strlen(xxx.name); | |
name = new char[sz]; | |
strcpy(name,xxx.name); | |
} | |
</file> | |
| |
I ostatnia poprawka: | |
| |
<file cpp XXX.h> | |
class XXX { | |
//... | |
//konstruktor przenoszący: | |
XXX(XXX &&xxx) : name_{nullptr} { | |
swap(name_,xxx.name_); | |
//Bardzo popularna szutczka | |
//wiemy, ze za chwilę xxx zostanie zniszczony | |
//za pomocą destrukotra, więc inicjalizujemy | |
//this na nullptr i wymieniamy się z xxx | |
//delete nullptr jest bezpieczna operacją i nic się nie stanie... | |
} | |
//operator przenoszący: | |
XXX & operator=(XXX &&xxx) { | |
//jeśli ktoś wpadł na pomsył x = move(x); | |
if (this == &xxx) { | |
return xxx; | |
} | |
//w przyciwynym wypadku mamy x = move(y); | |
//musimy sami zwolnic pamięć po x (czyli this): | |
delete[] name_; | |
//i wreszcie przenosimy stan, ten kod jest | |
//jest identyczny więc można by go wydzielić do innej metody... | |
name_ = nullptr; | |
swap(name_,xxx.name_); | |
} | |
</file> | |
| |
===== Konstruktory kopiujące ===== | |
Oprócz trzech rodzajów konstruktorów wymienionych na poprzednich laboratoriach: konstruktora domyślnego, konstruktora parometrowego oraz konstruktora bezparametrowego istnieje jeszcze jeden specjalny typ konstruktora, tzw. **konstruktor kopiujący**. | |
| |
Konstruktor kopiujący jest przydatny w chwili, kiedy klasy posiadają pola dynamicznie alokowane. Konstruktor kopiujący **musi** przyjmować jako parametr referencje do obiektu. Przykład konstruktora kopiującego dla klasy Punkt z poprzednich laboratoriów (nie jest on w tej klasie wymagany). Deklaracja:<code cpp>... | |
Punkt(const Punkt&); | |
...</code> | |
Definicja:<code cpp>... | |
Punkt::Punkt(const Punkt &punkt){ | |
this->x = punkt.x; | |
this->y = punkt.y; | |
cout << "Konstruktor kopiujący!" << endl; | |
} | |
... | |
</code> | |
| |
Konstruktor kopiujący jest wywoływany automatycznie w następujących sytuacjach: | |
* Podczas przekazywania obiektów przez wartość do funkcji (tworzone są ich lokalne kopie), | |
* Podczas zwracania obiektów przez funkcje, | |
* Podczas deklaracji obiektu gdy jako parametr konstruktora podany zostanie inny obiekt. | |
* **Ważne!** Podczas **deklaracji** obiektu połączonej z inicjalizacją przy użyciu operatora **=** (przypisania). W innych przypadkach przypisania poza deklaracją, konstruktor kopiujący nie zadziała!<code cpp>... | |
Punkt p(12,34); | |
Punkt p2(p); // Konstruktor kopiujący. | |
Punkt p3 = p; // Konstruktor kopiujący. | |
Punkt p4; | |
| |
//NIE ZADZIAŁA KONSTRUKTOR KOPIUJĄCY | |
//TYLKO DOMYŚLNY OPERATOR PRZYPISANIA | |
p4 = p2; | |
</code> | |
| |
===== Klasy i const===== | |
Słowo kluczowe **const** ma różne znaczenie w zależności od kontekstu w jakim jest stosowane. | |
====Metody const==== | |
W odniesieniu do metod klasy oznacza, że dana metoda nie może modyfikować elementów składowych klasy jak również nie może wywoływać innych metod niż te zdeklarowane jako **const**. Jeśli można, warto zadeklarować metodę jako //const//. Umożliwi to poprawne korzystanie z obiektów tej klasy zadeklarowanych jako stałe.<code cpp> | |
class Matrix{ | |
... | |
void Print() const; | |
... | |
}; | |
</code> | |
====Zmienne/obiekty const==== | |
W odniesieniu do zmiennych i obiektów i innych zmiennych, oznaczają że nie można modyfikować zmiennej/obiektu. Innymi słowy nie można wywoływać na rzecz danego obiektu innych metod niż zadeklarowane jako **const**<code cpp>... | |
const Matrix m(["1 2 3; 1 2 3"]); | |
| |
//Metoda ustawiająca wartość w komórce macierzy o indeksie (1,1) | |
//Wywołanie takiej metody wygeneruje błąd podczas kompilacji. | |
m.set(1,1,12); | |
| |
//Gdyby metoda wyświetl nie była zadeklarowana jako const | |
//takie wywołanie też spowodowałoby błąd | |
m.wyswietl(); | |
</code> | |
| |
| |
=====Funkcje i klasy friend===== | |
Funkcja i klasy zaprzyjaźnione mają nieograniczony dostęp do wszystkich pól i metod klasy której są //przyjaciółmi//. | |
====Funkcje zaprzyjaźnione==== | |
Funkcja zaprzyjaźniona definiowana jest poza zasięgiem klasy, ale informacja o tym że jest ona //przyjacielem// musi znaleźć się w klasie: | |
<code cpp> | |
//Plkik Matrix.h | |
class Matrix{ | |
double** data; | |
... | |
friend void wyzeruj(Matrix& m); | |
... | |
}; | |
| |
// Plik main.cpp | |
| |
// Funkcja może modyfikować prywatne dane klasy | |
// Matrix, ponieważ została określona jako friend | |
void wyzeruj(Matrix& matrix){ | |
for(int r = 0; r < matrix.rows; r++) | |
for(int c = 0; c < matrix.cols; c++) | |
matrix.data[r][c] = 0; | |
} | |
</code> | |
====Klasy zaprzyjaźnione==== | |
Analogicznie do funkcji klasy zaprzyjaźnione maja nieograniczony dostęp d wszystkich pól i metod klasy które są przyjaciłmi. Jeśli klasa Node ma być przyjacielem klasy Lista, to w tej drugiej należy umieścić następującą deklarację: | |
<code cpp> | |
class Lista{ | |
... | |
friend class Node; | |
... | |
}; | |
</code> | |
| |
| |
===== Słowo kluczowe static===== | |
Słowo kluczowe //static// może być stosowane zarówno w stosunku do pól klasy jak i jej metod, ale w obu przypadkach ma nieco inne znaczenie. | |
| |
==== Pola statyczne ==== | |
Pola statyczne są swego rodzaju zmiennymi globalnymi, należącymi jednak do zasięgu klasy. Zmienna statyczna jest współdzielona przez wszystkie obiekty klasy. Do zmiennych statycznych można odwoływać się nie mając utworzonego obiektu. | |
| |
Słowo kluczowe **static** dodaje się tylko i wyłącznie podczas deklaracji zmiennej. Użycie słowa kluczowego //static// podczas inicjalizacji zmiennej powoduje błąd składniowy. | |
| |
Deklaracja zmiennej statycznej w pliku nagłówkowym: | |
<code cpp> | |
class Matrix{ | |
public: | |
static int licznik; | |
Matrix(){licznik++;} | |
~Matrix(){licznik--;} | |
}; | |
</code> | |
| |
Zmienna statyczna musi zostać zainicjalizowana w przestrzeni pliku. Nie można inicjalizować statycznej zmiennej w konstruktorze, lub innej funkcji. Inicjalizacja zmiennej statycznej w pliku cpp: | |
<code cpp> | |
#include "Matrix.h" | |
| |
int Matrix::licznik = 0; | |
... | |
</code> | |
| |
Odwoływanie się do pól statycznych: | |
<code cpp> | |
int main(){ | |
Matrix m; | |
cout << Matrix::licznik <<endl; | |
cout << Matrix.licznik << endl; | |
} | |
</code> | |
| |
====Metody statyczne==== | |
Metody statyczne można wywoływać bez konieczności tworzenia obiektów klasy. Można z tego wywnioskować, że metody statyczne **nie posiadają** wskaźnika **this**. | |
| |
Nie można zatem wewnątrz metod statycznych wywoływać żadnych innych metod niestatycznych, ani odwoływać się do pól niestatycznych. | |
| |
Metody statyczne należą jednak do zasięgu klasy i mają dostęp do wszystkich pól klasy: | |
<code cpp> | |
class Matrix{ | |
... | |
static void wyswietl(Matrix& m); | |
}; | |
| |
void Matrix::wyswietl(Matrix & m){ | |
for(int r = 0; r < matrix.rows; r++){ | |
for(int c = 0; c < matrix.cols; c++) | |
cout << matrix.data[r][c] << endl; | |
cout << endl; | |
} | |
} | |
</code> | |
| |
| |
| |
====== Ćwiczenia ====== | |
- **[1 punkt] Dopisz konstruktor kopiujący do klasy Punkt z poprzedniego laboratorium i prześledź jak wyglądają wywołania konstruktorów. Zobacz, że przypisanie obiektów poza deklaracją nie wywołuje konstruktora kopiującego.** | |
- **[1 punkt] Sprawdź jak zachowa się DTab bez konstruktora kopiującego. Na przykład napisz funkcję, która będzie wypełniać DTab podaną jako parametr liczbą i zwracać ją. Funkcja powinna mieć następujący nagłówek** <code cpp>DTab wypelniona(int wypelnienie);</code> **Co się stanie gdy w zwróconej DTab będziemy chcieli zmienić jakieś elementy, albo ją rozszerzyć? Dopisz do klasy konstruktor kopiujący.** | |
- {{ .:lista.png?300|Lista powiązana}} [3 plusy] Zaimplementuj listę powiązaną przechowującą obiekty klasy //string//. Tablice nie są zawsze wystarczająco dobrym mechanizmem do przechowywania danych, zwłaszcza jeśli nie wiemy dokładnie ile tych danych będzie. Dynamiczne tablice (jak ta implementowana na poprzednich zajęciach) są pewnym rozwiązaniem, ale niezbyt wydajnym pod względem pamięci - realokacja pamięci, alokacja zbędnej pamięci, etc. \\ Lista powiązana składa się z dwóch elementów (patrz rysunek obok): | |
* Nadrzędnego obiektu udostępniającego interfejs służący do dodawania, usuwania, wyszukiwania elementów w liście. Posiada on wskaźnik do głowy listy (patrz poniżej). Może przechowywać informacje na temat długości listy. | |
* Elementów składowych listy, tzw. Węzłów, z których każdy posiada pole przechowujące dane i pole będące wskaźnikiem do następnego węzła. Obiekt nadrzędny posiada jedynie wskaźnik do pierwszego elementu listy (tak zwanej głowy). Odnajdywanie //n-tego// elementu listy odbywa się poprzez "przeskakiwanie" po węzłach aż do szukanego elementu. \\ __Pamiętaj o destruktorze i konstruktorze kopiującym__ :!: | |
- [1 plus] Napisz dwie klasy: Rodzic i Dziecko. Klasa Rodzic powinna mieć takie pola jak imię, nazwisko, wiek, oraz dziecko (zakładamy dla uproszczenia, że rodzic ma tylko jedno dziecko :) ). Dziecko powinno mieć takie pola jak imię, nazwisko, wiek, szkoła. Zdefiniuj klasę Rodzic jako zaprzyjaźnioną klasy Dziecko. Przetestuj, czy można modyfikować zmienne prywatne klasy Dziecko z poziomu metod klasy Rodzic. Napisz na przykład metodę //przepiszDoInnejSzkoly(string nazwa)// która będzie zmieniać szkołę dziecka operując bezpośrednio na jego danych. \\ Następnie usuń linijkę odpowiedzialną za określenie klasy zaprzyjaźnionej i spróbuj skompilować ponownie program. | |
- [3 plusy] Napisz klasę Marsjanin, która będzie miała statyczne pole //liczbaMarsjan//, określające liczbę stworzonych obiektów Marsjanin. Każdy Marsjanin powinien atakować gdy liczba wszystkich Marsjan jest większa od 5 i ukrywać się w przeciwnym wypadku. \\ Napisz program który w pętli nieskończonej będzie tworzył lub usuwał obiekty klasy Marsjanin i wywoływał metodę //atakuj// dla wszystkich Marsjan. Obiekty powinny być przechowywane w liście (zobacz [[http://www.cplusplus.com/reference/stl/list/|List]]). | |
- **[3 punkty] Zaimplementuj klasę o nazwie Matrix, która będzie reprezentować macierz o dowolnych rozmiarach. Wymagania dotyczące klasy Matrix:** | |
* Klasa powinna wewnętrznie reprezentować macierz przy pomocy tablicy dwuwymiarowej obiektów typu Complex (wykorzystaj klasę napisaną na poprzednim laboratorium). Umawiamy się, że liczba zespolona zapisywana jest w następujący sposób: <code>4.5i6</code> Co oznacza w zapisie matematycznym <code>4.5 + 6i</code> | |
* Klasa Matrix powinna posiadać konstruktor parometrowy określający jej wymiary, konstruktor bezparametrowy, oraz kopiujący. Dopisać konstruktor, który będzie przyjmować napis //const char*// (lub obiekt klasy string z biblioteki //string.h//) w notacji Matlaba i parsować go, aby można było stworzyć obiekt Matrix w taki sposób: <code cpp> Matrix m("[1i3 2i5 3; 3 4 5; 6 7 8]");</code> | |
* Klasa Matrix powinna udostępniać metody pozwalające na ustawianie/pobieranie jej elementów | |
* Klasa Matrix powinna udostępniać metody umożliwiające następujące operacje: | |
* Dodawanie macierzy. Metoda powinna klasy powinna pobierać jako parametr inny obiekt klasy Matrix i zwracać wynik będący sumą macierzy reprezentowanej przez obiekt na rzecz którego wywoływana jest metoda i macierzy reprezentowanej przez obiekt podany jako parametr. Przykładowe wywołanie:<code cpp>Matrix m("[1 2 3;3 4 5; 2 3 4]"); | |
Matrix m2("[3 2 1; 5 4 3; 7 6 5]"); | |
Matrix wynik = m.add(m2); | |
</code> | |
* Odejmowanie macierzy (pozostałe analogicznie) | |
* Mnożenie macierzy zarówno przez inna macierz jak i przez liczbę (w tym wypadku Complex) | |
* Dzielenie macierzy zarówno przez inna macierz jak i przez liczbę (w tym wypadku Complex) | |
* Podnoszenie macierzy do potęgi! | |
* Metodę //print// wyświetlającą dana macierz w formacie Matlaba, z ładnym formatowaniem (podział na wiersze) | |
* Zabezpiecz program przed sytuacjami wyjątkowymi! Na przykład: dodawanie macierzy o różnych wymiarach, mnożenie macierzy o nieodpowiednich wymiarach, odwracanie macierzy zerowej. | |
| |
* Napisz program który umożliwi testowanie klasy Matrix. Program powinien przyjmować jako parametr dwie macierze zapisane w formacie Matlaba. Funkcja //main// powinna wyglądać następująco:<code cpp>#include <iostream> | |
#include "Matrix.h" | |
| |
int main(int argc, char* argv[]){ | |
Matrix m1(argv[1]); | |
Matrix m2(argv[2]); | |
| |
cout << "Macierz pierwsza: " << m1.print() << endl; | |
cour << "Macierz druga: " << m2.print() << endl; | |
| |
cout << "Dodawanie" << (m1.add(m2)).print() << endl; | |
cout << "Odejmowanie" << (m1.sub(m2)).print() << endl; | |
cout << "Mnożenie" << (m1.mul(m2)).print() << endl; | |
cout << "Dzielenie" << (m1.div(m2)).print() << endl; | |
cout << "Potęgowanie" << (m1.pow(2)).print() << endl; | |
cout << "Potęgowanie" << (m2.pow(2)).print() << endl; | |
} | |
</code> | |
| |
<WRAP center round info 60%> | |
Zabezpieczenie programu przed sytuacjami wyjątkowymi w "podstawowej" wersji, wymaganej na laboratorium, związane jest ze sprawdzeniem odpowiedniej wartości i wypisaniem komunikatu dla użytkownika. Bardziej profesjonalny sposób obsługi takich sytuacji związany jest z **mechanizmem wyjątków**. Jeżeli chcesz dowiedzieć się o nim czegoś więcej, zapraszam do zapoznania się z nieobowiązkową instrukcją do laboratorium [[.:wyjatki|Wyjątki]]. | |
</WRAP> | |
| |
| |