Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: Piotr Szawdyński
Kurs C++

Konstruktor, destruktor i konstruktor kopiujący klasy

[lekcja] Omówienie konstruktora, destruktora i konstruktora kopiującego.

Krótkie przytoczenie faktów

Bardzo często, a właściwie prawie zawsze tworząc klasę mamy potrzebę zainicjowania jej początkowymi wartościami. Do tej pory, aby to uczynić musiałeś tworzyć publiczną funkcję wewnątrz klasy, a następnie pamiętać o każdorazowym jej wywołaniu zaraz po zarezerwowaniu pamięci. Język C++ umożliwia dużo lepsze rozwiązanie niż to, które stosowaliśmy do tej pory.

Konstruktor klasy

Konstruktor jest specyficzną funkcją, która jest wywoływana zawsze gdy tworzony jest obiekt. Jeśli programista nie utworzy konstruktora dla klasy, kompilator automatycznie utworzy konstruktor, który nic nie będzie robił. Konstruktor nie pojawi się nigdzie w kodzie, jednak będzie on istniał w skompilowanej wersji programu i będzie wywoływany za każdym razem, gdy będzie tworzony obiekt klasy. Jeśli chcemy zmienić domyślne własności konstruktora jaki jest tworzony przez kompilator C++ wystarczy, że utworzymy własny konstruktor dla klasy. Poniższy przykład pokazuje jak zapisuje się konstruktor dla klasy.
C/C++
class NazwaTwojejKlasy
{
public:
    NazwaTwojejKlasy(); //To jest definicja konstruktora
};

NazwaTwojejKlasy::NazwaTwojejKlasy()
{
    //Tu inicjujemy wartości zmiennych klasy
}
Pierwsze co rzuca się w oczy w przypadku konstruktora, to brak zwracanego typu danych. Druga istotna własność konstruktora to jego nazwa. Konstruktor musi nazywać się tak samo jak nazwa klasy. Konstruktorom możesz przekazywać parametry tak samo jak funkcjom. C++ umożliwia również tworzenie kilku konstruktorów dla jednej klasy (muszą się one jednak różnić parametrami wejściowymi tak jak w przypadku funkcji).

Nadawanie domyślnych wartości zmiennym w chwili narodzin klasy

Gdy tworzymy klasę, wszystkie zmienne jakie są zadeklarowane wewnątrz niej są zainicjowane przypadkowymi wartościami, które w konstruktorze następnie ustawiamy na takie, jakie uważamy za stosowne. Przykład:
C/C++
#include <iostream>
#include <conio.h>
using namespace std;
class JakasKlasa
{
    int a;
    char b;
    std::string c;
public:
    JakasKlasa();
};

JakasKlasa::JakasKlasa()
{
    cout << "Klasa utworzona, wartosci zmiennych: " << endl;
    cout << "a = " << a << endl;
    cout << "b = '" << b << "'" << endl;
    cout << "c = \"" << c << "\"" << endl;
    a = 123;
    b = 'x';
    c = "napis";
    cout << "Wartosci zmiennych po zainicjowaniu w konstruktorze: " << endl;
    cout << "a = " << a << endl;
    cout << "b = '" << b << "'" << endl;
    cout << "c = \"" << c << "\"" << endl;
}

int main()
{
    JakasKlasa tZmienna;
    getch();
    return( 0 );
}
Takie rozwiązanie jest oczywiście poprawne, niemniej jednak czasami zachodzi potrzeba zainicjowania zmiennej w trakcie tworzenia klasy, a nie po jej utworzeniu. Aby to zrobić, należy użyć następującego zapisu:
C/C++
#include <iostream>
#include <conio.h>
using namespace std;
class JakasKlasa
{
    int a;
    char b;
    std::string c;
public:
    JakasKlasa();
};

JakasKlasa::JakasKlasa()
    : a( 123 )
     , b( 'x' )
     , c( "napis" )
{
    cout << "Klasa utworzona, wartosci zmiennych: " << endl;
    cout << "a = " << a << endl;
    cout << "b = '" << b << "'" << endl;
    cout << "c = \"" << c << "\"" << endl;
}

int main()
{
    JakasKlasa tZmienna;
    getch();
    return( 0 );
}
Taki zapis ma kilka bardzo istotnych zalet:
  • Jest szybszy - brzmi to trochę absurdalnie, ale różnice są znaczne gdy przyjdzie do wykonywania pomiarów czasowych.
  • Jest czytelniejszy - programista nie musi analizować zawartości konstruktora, by wiedzieć jaką domyślną wartością zostanie zainicjowana klasa.
  • Umożliwia inicjowanie zmiennych zdefiniowanych jako stałe.
  • Umożliwia inicjowanie zmiennych zdefiniowanych jako referencje (i tylko tą metodą jesteś w stanie zainicjować zmienną zadeklarowaną np. tak: int& zmienna;).
  • Jest metodą stosowaną przy dziedziczeniu klas (również niezastąpioną).
Warto więc od początku wpajać sobie nawyk, który został tu zaprezentowany, ponieważ w przyszłości może Ci to oszczędzić dużo problemów, takich jak poniższe.

Problem nr 1.

C/C++
class JakasKlasa
{
    int & a;
public:
    JakasKlasa( int & fA );
};

JakasKlasa::JakasKlasa( int & fA )
{
    a = fA;
}

int main()
{
    int zmiennaA;
    JakasKlasa tZmienna( zmiennaA );
    return( 0 );
}
Komunikat:
     C:\kurs\problem01.cpp    In constructor `JakasKlasa::JakasKlasa(int&)':
9    C:\kurs\problem01.cpp    uninitialized reference member `JakasKlasa::a'

Rozwiązanie nr 1.

C/C++
class JakasKlasa
{
    int & a;
public:
    JakasKlasa( int & fA );
};

JakasKlasa::JakasKlasa( int & fA )
    : a( fA )
{
}

int main()
{
    int zmiennaA;
    JakasKlasa tZmienna( zmiennaA );
    return( 0 );
}

Problem nr 2.

C/C++
class JakasKlasa
{
    const int a;
public:
    JakasKlasa( int fA );
};

JakasKlasa::JakasKlasa( int fA )
{
    a = fA;
}

int main()
{
    JakasKlasa tZmienna( 55 );
    return( 0 );
}
Komunikat:
C:\kurs\problem2.cpp: In constructor `JakasKlasa::JakasKlasa(int)':
C:\kurs\problem2.cpp:9: error: uninitialized member `JakasKlasa::a' with `const' type `const int'
C:\kurs\problem2.cpp:10: error: assignment of read-only data-member `JakasKlasa::a'

Rozwiązanie nr 2.

C/C++
class JakasKlasa
{
    const int a;
public:
    JakasKlasa( int fA );
};

JakasKlasa::JakasKlasa( int fA )
    : a( fA )
{
}

int main()
{
    JakasKlasa tZmienna( 55 );
    return( 0 );
}

Destruktor klasy

Często jest tak, że podczas życia klasy rezerwujemy pamięć, którą chcielibyśmy zwalniać zawsze przed usunięciem klasy. Pierwszym wariantem jest pamiętanie o wywołaniu funkcji, która będzie za to odpowiedzialna. Takie podejście jest jednak ryzykowne, ponieważ bardzo łatwo zapomnieć o wywoływaniu funkcji, która będzie zwalniała ewentualną zarezerwowaną dynamicznie pamięć.
Lepszym rozwiązaniem tego problemu jest wykorzystanie destruktorów. Destruktor jest specjalną funkcją, która jest wywoływana zawsze tuż przed zniszczeniem (usunięciem) klasy z pamięci.
Budowa destruktora wygląda następująco:
C/C++
class NazwaTwojejKlasy
{
public:
    ~NazwaTwojejKlasy(); //To jest definicja destruktora
};

NazwaTwojejKlasy::~NazwaTwojejKlasy()
{
    //Tu wykonujemy wszystkie operacje jakie mają się wykonać automatycznie tuż przed zwolnieniem pamięci zajmowanej przez klasę.
}
Destruktor, tak samo jak konstruktor nie posiada zwracanego typu. Drugą ważną cechą jest to, że destruktor musi być zawsze bezparametrowy. Trzecią, a zarazem ostatnią ważną cechą jest możliwość zdefiniowania tylko i wyłącznie jednego destruktora dla danej klasy.
Poniżej przedstawiam bardzo prosty przykład, demonstrujący działanie konstruktora i destruktora.

Przykład

C/C++
#include <iostream>
#include <conio.h>
using namespace std;

class KlasaCL
{
public:
    KlasaCL();
    ~KlasaCL();
};

int main()
{
    KlasaCL * tKlasa;
    cout << "Rezerwuje pamiec za pomoca new" << endl;
    tKlasa = new KlasaCL;
    cout << "Wchodze do bloku {" << endl;
    {
        KlasaCL tKlasa;
    }
    cout << "Wyszedlem z bloku }" << endl;
    cout << "Zwalniam pamiec, ktora zostala zarezerwowana za pomoca new" << endl;
    delete tKlasa;
    getch();
    return( 0 );
}

KlasaCL::KlasaCL()
{
    cout << "=> Konstruktor wywolany!" << endl;
}

KlasaCL::~KlasaCL()
{
    cout << "=> Destruktor wywolany!" << endl;
}

Konstruktor kopiujący

Rozdział w trakcie tworzenia

Przykład

C/C++
#include <iostream>
#include <iomanip>
#include <string>
#include <conio.h>

using namespace std;
class OsobaCL
{
protected:
    string m_imie;
    string m_nazwisko;
    OsobaCL * m_nastepnaOsoba;
public:
    OsobaCL();
    ~OsobaCL();
    OsobaCL( const OsobaCL & fOsoba );
    void Wypelnij();
    OsobaCL * GetNastepnaOsoba();
    void ShowDaneOsoby( int fImieWidth = 12, int fNazwiskoWidth = 20 );
    void DodajOsobeNaKoniec();
};

OsobaCL::OsobaCL()
{
    m_nastepnaOsoba = NULL;
}

OsobaCL::~OsobaCL()
{
    if( m_nastepnaOsoba != NULL )
    {
        delete m_nastepnaOsoba;
        m_nastepnaOsoba = NULL;
    }
}
OsobaCL::OsobaCL( const OsobaCL & fOsoba )
{
    m_nastepnaOsoba = NULL;
    m_imie = fOsoba.m_imie;
    m_nazwisko = fOsoba.m_nazwisko;
}

OsobaCL * OsobaCL::GetNastepnaOsoba()
{
    return( m_nastepnaOsoba );
}

void OsobaCL::Wypelnij()
{
    cout << "Podaj imie: ";
    cin >> m_imie;
    cout << "Podaj nazwisko: ";
    cin >> m_nazwisko;
}

void OsobaCL::ShowDaneOsoby( int fImieWidth, int fNazwiskoWidth )
{
    cout << setw( fImieWidth ) << m_imie << " " << setw( fNazwiskoWidth ) << m_nazwisko << endl;
}

void OsobaCL::DodajOsobeNaKoniec()
{
    //Szukanie ostatniej osoby
    OsobaCL * tOstatniaOsoba = this; //Zapisanie wskaźnika obecnej osoby do zmiennej
    while( tOstatniaOsoba->m_nastepnaOsoba != NULL ) tOstatniaOsoba = tOstatniaOsoba->m_nastepnaOsoba;
   
    //Utworzenie nowej osoby i zapisanie do ostatniej osoby
    OsobaCL * tNowaOsoba = new OsobaCL;
    tNowaOsoba->Wypelnij();
    tOstatniaOsoba->m_nastepnaOsoba = tNowaOsoba;
}

int main()
{
    /*Utworzenie pierwszej osoby*/
    OsobaCL * tPierwszaOsoba = new OsobaCL;
   
    /*Wypełnienie pierwszej osoby*/
    tPierwszaOsoba->Wypelnij();
   
    /*Wczytywanie kolejnych osób*/
    char tZnak;
    do
    {
        cout << "Czy chcesz dodac nowa osobe? (T/N) ";
        do
        {
            tZnak = getch();
        } while(( tZnak != 'n' ) &&( tZnak != 'N' ) &&( tZnak != 't' ) &&( tZnak != 'T' ) );
       
        cout << tZnak << endl;
        if(( tZnak == 't' ) ||( tZnak == 'T' ) )
        {
            tPierwszaOsoba->DodajOsobeNaKoniec(); //Dodawanie nowej osoby
        }
    } while(( tZnak != 'n' ) &&( tZnak != 'N' ) );
   
    cout << "Lista elementow: " << endl;
    /*Wyświetlanie wszystkich osób*/
    OsobaCL * tWyswietlOsoby = tPierwszaOsoba;
    while( tWyswietlOsoby != NULL )
    {
        tWyswietlOsoby->ShowDaneOsoby();
        tWyswietlOsoby = tWyswietlOsoby->GetNastepnaOsoba();
    }
   
    cout << "Prezentacja dzialania konstruktora kopiujacego: " << endl;
    /* Zrób kopię pierwszej osoby i wyświetl wszystkie osoby dowiązane do niej */
    OsobaCL tKopiaPierwszejOsoby( * tPierwszaOsoba );
   
    /*Wyświetlanie wszystkich osób*/
    tWyswietlOsoby = & tKopiaPierwszejOsoby;
    while( tWyswietlOsoby != NULL )
    {
        tWyswietlOsoby->ShowDaneOsoby();
        tWyswietlOsoby = tWyswietlOsoby->GetNastepnaOsoba();
    }
   
    /*Zwalnianie pamięci*/
    delete tPierwszaOsoba;
   
    getch();
    return( 0 );
}
Poprzedni dokument Następny dokument
Funkcje w klasie, czyli metody Unia w C++