Spis treści

Klasy i obiekty II

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:

...
Punkt(const Punkt&);
...

Definicja:

...
Punkt::Punkt(const Punkt &punkt){
this->x = punkt.x;
this->y = punkt.y;
cout << "Konstruktor kopiujący!" << endl;
}
...

Konstruktor kopiujący jest wywoływany automatycznie w następujących sytuacjach:

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.

class Matrix{
  ...
  void wyswietl() const;
  ...
};

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

...
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();

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:

//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;
}

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ę:

class Lista{
  ...
  friend class Node;
  ...
};

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:

class Matrix{
  public:
    static int licznik;
    Matrix(){licznik++;}
   ~Matrix(){licznik--;}
};

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:

#include "Matrix.h"
 
int Matrix::licznik = 0;
...

Odwoływanie się do pól statycznych:

int main(){
  Matrix m;
  cout << Matrix::licznik <<endl;
  cout << Matrix.licznik << endl;
}

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:

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;
  }
}

Ćwiczenia

  1. [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.
  2. [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
    DTab wypelniona(int wypelnienie);

    Co się stanie gdy w zwróconej DTab będziemy chcieli zmienić jakieś elementy, albo ją rozszerzyć? Dopisz do klasy konstruktor kopiujący.

  3. 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 :!:
  4. [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.
  5. [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 List).
  6. [3 punkty] Zaimplementuj klasę o nazwie Matrix, która będzie reprezentować macierz o dowolnych rozmiarach. Wymagania dotyczące klasy Matrix:

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 Wyjątki.