czwartek, 13 października 2011

Drobny wykład o konstruktorach klas

Dzisiejszy artykuł poświęcę konstruktorom klas.
Mini-wykład  ten jest skierowany do raczkujących programistów. Proszę więc o wyrozumiałość, jeżeli moje tłumaczenia będą do bólu łopatologiczne.

Przykłady będę pisać w języku C++, jednak logika jest identyczna w każdym języku obiektowym.

Aspekty poruszone w artykule:

* konstruktor klasy
* przeciążanie konstruktorów
* destruktory klas
* destruktory wirtualne
* konstruktor kopiujący
* dynamiczna alokacja pamięci - przeciążanie konstruktora kopiującego




****

KONSTRUKTORY KLASY

Zacznijmy od podstaw : co to jest obiekt? Najprościej rzecz ujmując : obiekt to pojedyńczy egzemplarz (lub też "instancja") klasy. Ma on swoje składowe, tego samego typu, co inne egzemplarze tej samej klasy, ma za to różne wartości.

Rozpatrzmy przykład:
Dana jest klasa opisująca artykuł w sklepie. Zawiera ona składowe: nazwa towaru, cena i kod kreskowy. Ciało klasy:


class Artykul
{
  private:


  char nazwa [20];
  double cena;
  char barcode [20];


  public:
  void Show()
    {
      // tutaj jakiś kod do wyświetlania artykułu
    }
  
};

Niby wszystko wygląda pięknie. Problem w tym, że składowe są prywatne (zgodnie z zasadą hermetyzacji). Co wobec tego zrobić, żeby na starcie przypisać składowym jakiegoś obiektu odpowiednie wartości? W końcu nie istnieje przecież w sklepie 'pusty' artykuł. Potrzebowalibyśmy funkcję, którą uruchamialibyśmy za każdym razem, kiedy tworzymy nowy obiekt. Taka funkcja jest zaimplementowana w każdym obiektowym języku programowania, a nazywa się właśnie konstruktor klasy.

***

Wygląd konstruktora w kodzie jest następujący : nie jest on żadnego typu i nic nie zwraca, dodatkowo jest w większości przypadków metodą publiczną. Może on jednak przyjmować parametry.

Ciekawostka : istnieją też klasy wymagające tylko jednego obiektu danej klasy przez cały cykl życia programu, wówczas niekiedy konstruktor jest prywatny (np. klasy typu Singleton)

 Dla poprzedniego przykładu, konstruktor powinien wyglądać mniej więcej tak:

class Artykul
{
  private:
  char nazwa [20];
  double cena;
  char barcode [20];


  public:
  Artykul(char *Nazwa, double Cena, char * Barcode)  //konstruktor
  {
    strcpy(nazwa, Nazwa);
    cena=Cena;
    strcpy(barcode, Barcode);
  }


  void Show()
    {
      // tutaj jakiś kod do wyświetlania artykułu
    }
};

Po zadeklarowaniu takiego konstruktora, w programie możemy już przypisać obiektom wartości, przykładowo:

int main()
{
  Artykul ART("Zupka chińska", 0.99, "0021015151");  
  return 0;
}

Teraz jest już zupełnie prawidłowo.

Co ważne : każda klasa ma konstruktor, nieważne, czy jest on zadeklarowany, czy też nie. Jeżeli nie zadeklarowaliśmy żadnego konstruktora, to w momencie kompilacji kompilator sam napisze za nas odpowiedni kod, który... nie robi nic poza tym, że prostu jest ;) Nazywa się on wówczas konstruktor niejawnym.

(UWAGA - tak naprawdę domyślnie mamy 2 niejawne konstruktory. Więcej w późniejszej części artykułu, przy okazji konstruktora kopiującego)

---

Co jest szczególnie istotne, to fakt, że kompilator zadeklaruje bezargumentowy konstruktor niejawny tylko wtedy, gdy jawnie nie zadeklarowano żadnego innego konstruktora.

Przykład:


class JakasKlasa
{
  private:
  int x,y;
  public:
  // jakieś nieistotne funkcje
 };


int main()
{
  JakasKlasa Test;  // zadziała, wywołano niejawny 
                    // konstruktor JakasKlasa(void)
  return 0;
}

Jeżeli jednak przeciążymy konstruktor:


class JakasKlasa
{
  private:
  int x,y;
  public:
  JakasKlasa(int X, int Y) { x=X; y=Y;}
  // jakieś nieistotne funkcje
};




int main()
{
  JakasKlasa Test1(4,5); // Prawidłowo, wywołano JakasKlasa(int, int)
  JakasKlasa Test2;  // BŁĄD. Po znalezieniu jawnego 
                     // konstruktora JakasKlasa(int, int), 
                     // kompilator nie doda
                     // niejawnego konstruktora 
                     // JakasKlasa(void)
  return 0;
}

***

PRZECIĄŻANIE KONSTRUKTORÓW

Przeciążanie konstruktorów to po prostu używanie kilku konstruktorów w odrębie jednej klasy. Różnią się one jedynie liczbą i/lub typem przyjmowanych parametrów.

Często stosuje się konstruktor bezparametrowy oraz przeciążony konstruktor, przyjmujący jakieś wartosci. Osobiście uważam to za złą taktykę. Znacznie lepiej zadeklarować konstruktor domyślny (ustawiający domyślne wartości konstruktora, przez co można go wywołać bezparametrowo). Przykład:

Zamiast:


class JakasKlasa
{
  private:
  int x;
  int y;
  public:
  JakasKlasa() { x=y=0; }
  JakasKlasa(int X, int Y)
    {
      x=X;
      y=Y;
    }
}

Lepiej napisać:


class JakasKlasa
{
  private:
  int x;
  int y;
  public:
  JakasKlasa(int X=0, int Y=0)  //wartości domyślne
    {
      x=X;
      y=Y;
    }
}

Wówczas nawet wywołanie:
JakasKlasa Egzemplarz();
wywoła konstruktor JakasKlasa(int, int) z wartościami domyślnymi, czyli 0.

Czasami przeciążanie konstruktorów jest używane, jeżeli klasa ma zachowywać się w różny sposób, zależnie od tego, jakiego typu parametr otrzyma w konstruktorze. Osobiście bardzo odradzam takie rozwiązanie; jest ono jawnym pogwałceniem jednej z niepisanych zasad programowania obiektowego, czyli SRP (Single Responsibility Principle). Jeżeli jakaś klasa ma więcej niż jeden powód, by się zmienić, powinno się ją rozbić na kilka klas dziedziczących z wspólnej abstrakcyjnej klasy bazowej.

Więcej o zasadach programowania obiektowego napiszę w następnym artykule


Oczywiście, niekiedy przeciążanie konstruktorów jest konieczne i nieuniknione, przykładowo przy konstruktorze kopiującym - o nim nieco później.

***
DESTRUKTORY KLAS

Ta groźnie brzmiąca nazwa to określenie przeciwieństwa konstruktora : jest to funkcja, która jest uruchamiana, kiedy obiekt kończy żywot. W przypadku zmiennych statycznych i automatycznych jest on wywoływany przy zakończeniu programu, a w przypadku zmiennych dynamicznych - przy zwolnieniu pamięci poleceniem delete. Podobnie jak w przypadku konstruktora, kazda funkcja ma podczas kompilacji dodany niejawny destruktor, dopóki nie zostanie on jawnie zadeklarowany.

Wygląd destruktora:

class Klasa
{
  private:
  // cośtam
  public:
  //cośtam


  ~Klasa()  // destruktor
  {
    // rób coś
  }
}


W przeciwieństwie do konstruktorów, destruktor nie może zostać przeciążony. Dodatkowo, jest on zawsze bezargumentowy.

Przynajmniej w początkach nauki programowania, rzadko stosuje się jawne destruktory. Po co więc w ogóle przejmować się istnieniem takiego mechanizmu? Odpowiedź pojawia się, gdy składniki klasy są alokowane dynamicznie (zazwyczaj w konstruktorze) przez użycie polecenia new. Wówczas TRZEBA zadeklarować jawny destruktor klasy, by pamięć zwolnić. Oczywiście, program skompiluje się i bez tego, jednak wciąż będzie dochodziło do wycieku pamięci.

***

DESTRUKTORY WIRTUALNE

Rozpatrzmy przykład:


class JakasKlasa
{
  private:
  // jakies składowe 
  public:     
    ~JakasKlasa()
      {
         // jakieś instrukcje
      }
  // jakieś inne metody
};

class KlasaPochodna: public JakasKlasa
{
  private:
  // jakies składowe 
  public:
    ~KlasaPochodna()
      {
         // jakieś instrukcje
      }
  // jakieś inne metody
};

int main()
{
   JakasKlasa *wsk = new KlasaPochodna();  // polimorficzne rzutowanie
   // jakieś operacje
   delete wsk;
   return 0;
}

Program co prawda się skompiluje i uruchomi, ale mamy tu do czynienia z klasycznym wyciekiem pamięci. Zastanówmy się: co tak naprawdę się wykona przy okazji delete wsk ? Na wskaźnik do klasy bazowej może zostać przypisany adres klasy pochodnej; to jedna z fundamentalnych zasad polimorfizmu. Jednakże w tym przypadku po użyciu delete wsk, destruktor zostanie wywołany dla klasy bazowej, pomijając klasę pochodną!

Żeby pozbyć się tego wycieku pamięci, musimy wymóc na kompilatorze wywoływanie destruktora dla obiektu takiego, jakim jest faktycznie. Wobec tego, musimy zadeklarować destruktor jako wirtualny.

Prawidłowa wersja:


class JakasKlasa
{
  private:
  // jakies składowe 


  public:
    virtual ~JakasKlasa()
      {
         // jakieś instrukcje
      }
  // jakieś inne metody
};


class KlasaPochodna: public JakasKlasa
{
  private:
  // jakies składowe 
  public:
    virtual ~KlasaPochodna()
      {
         // jakieś instrukcje
      }


  // jakieś inne metody
};


int main()
{
   JakasKlasa *wsk = new KlasaPochodna();  // polimorficzne rzutowanie
   delete wsk;


   return 0;
}

(uwaga - słowo kluczowe virtual w klasie pochodnej jest opcjonalne - liczy się tylko w klasie bazowej. Zwiększa jednak czytelność kodu, więc lepiej je stosować.)

Jeżeli metoda zadeklarowana jest jako wirtualna, to jest ona wybierana w zależności od obiektu, który ją wywołał. W naszym przypadku najpierw wykona się dla KlasaPochodna. Wówczas klasa JakasKlasa stwierdzi, że została już zwolniona z pamięci, więc wywoła również swój destruktor.

***

KONSTRUKTOR KOPIUJĄCY


Załóżmy, że mamy zadeklarowany obiekt danej klasy. Musimy utworzyć inny obiekt, który zawiera identyczne wartości. Musielibyśmy mozolnie pisać funkcję, która przyjmuje jako parametr 2 obiekty i przypisuje wartość z jednego, do drugiego. Dodatkowo rodzi się problem - zapewne składowe klasy są prywatne, więc musielibyśmy w ciele klasy oznaczyć naszą funkcję jako zaprzyjaźnioną. A co, jeżeli nie mamy możliwości edytowania zawartości pliku z klasami? Jak widać, zadanie banalne, a problemy mnożą się same.

Z pomocą przychodzi tutaj konstruktor kopiujący.
Jest to szczególne przeciążenie konstruktora, które jest niejawnie deklarowane w każdej napisanej przez nas klasie. Ma on prototyp:
NazwaKlasy(const NazwaKlasy &)
przyjmuje więc referencję do obiektu swojej klasy i obiecuje, że przyjęty obiekt pozostanie niezmieniony.
Jego niejawna postać po prostu kopiuje wszystkie wartości z obiektu będącego parametrem, do obiektu inicjalizowanego.
Innymi słowy, obeikt jest inicjalizowany innym obiektem.

Przykład zastosowania niejawnego konstruktora kopiującego:

#include <iostream>


using std::cout;
using std::endl;


class JakasKlasa
{
  int a;
  int b;
public:
  JakasKlasa(int A=0, int B=0)  // jawny konstruktor 
                                // JakasKlasa(int, int)
    {
      a=A;
      b=B;
    }
  void Show()
  {
    cout << "a=" << a <<", b = " << b << endl;
  }
};


int main()
{
// wywołanie jawnego konstruktora JakasKlasa(int, int):
   JakasKlasa Obiekt(4,8);  


// wywołanie niejawnego konstruktora kopiującego
// JakasKlasa(const JakasKlasa &) :
  JakasKlasa InnyObiekt(Obiekt); 


// jakieś dalsze instrukcje


  system("PAUSE");
  return(0);
}

Zarówno Obiekt jak i InnyObiekt będą miały takie same wartości wszystkich składowych.
UWAGA - nie wolno mylić obiektu zainicjalizowanego innym obiektem z jego referencją! Mimo, że na starcie oba obiekty będą miały takie same wartości, są od siebie zupełnie niezależne.

***

DYNAMICZNA ALOKACJA PAMIĘCI - PRZECIĄŻANIE KONSTRUKTORA KOPIUJĄCEGO

Problem z niejawnym konstruktorem kopiującym pojawia się, jeżeli chcemy skopiować obiekt zawierający składowe alokowane dynamicznie. Wówczas domyślny konstruktor kopiujący dokona tzw. kopiowania płytkiego - skopiowane zostaną jedynie adresy, a nie wartości. Powinniśmy wtedy zadeklarować jawny konstruktor kopiujący, który wykona tzw. kopiowanie głębokie.

Rozpatrzmy gotowy przykład:
Dana jest klasa wymagająca dynamiczną alokację pamięci. Zawiera ona łańcuch znaków i jego długość. Pamięć jest alokowana dynamicznie w konstruktorze, więc wymagany jest jawny destruktor zwalniający pamięć. Zadeklarowano też działający jawny kosntruktor kopiujący.


using namespace std;
class JakasKlasa
{
  private:
  char * lancuch;
  int length;  
  public:
  JakasKlasa(const char * Lancuch)  
  {
    length=strlen(Lancuch);
    lancuch=new char[length+1];
    strcpy(lancuch, Lancuch);
  }
//jawny konstruktor kopiujący:
  JakasKlasa(const JakasKlasa & DoSkopiowania)  
  {
    length=DoSkopwiowania.length;
    lancuch=new char[length+1];
    strcpy(lancuch, DoSkopiowania.lancuch);
  }  
  ~JakasKlasa()  //destruktor - zwalnianie pamięci
  {
    delete [] lancuch;
  }


};


//przykładowe działanie na klasie:
int main()
{
 // konstruktor JakasKlasa(const char*) :
  JakasKlasa Obiekt1("TheString"); 
// jawny konstruktor kopiujący JakasKlasa(const Jakasklasa &)  
  JakasKlasa Obiekt2(Obiekt1);      




  return 0;
}

---

Uwaga - jeżeli mamy do czynienia z klasą zawierającą dynamiczną alokację pamięci, należy również przeciążyć w niej operator= . Ale operatory to już temat na inny artykuł.

1 komentarz:

  1. Betway €1000 Welcome Bonus + 100 Spins (T&Cs Apply)
    Betway €1000 Welcome Bonus + 100 Spins 김포 출장샵 (T&Cs Apply) · 1xbet korean Betway €1000 Sportsbook bonus · Betway €1000 Promo Code (T&Cs Apply) 하남 출장샵 · 울산광역 출장샵 Betway €1000 Bonus  논산 출장안마 Rating: 9/10 · ‎Review by Jayesh Bhattarwal

    OdpowiedzUsuń