Konstruktory, destruktory i kopiowanie obiektów

Kurs: Wstęp do programowania
Lekcja 6: Zaawansowane techniki programistyczne
Temat 5: Konstruktory, destruktory i kopiowanie obiektów

⇓ spis treści ⇓


W programowaniu obiektowym w języku C++ kluczowe znaczenie mają mechanizmy inicjalizacji i niszczenia obiektów, czyli konstruktory i destruktory, a także kopiowanie obiektów, które wpływa na efektywność i bezpieczeństwo aplikacji. Konstruktory są odpowiedzialne za inicjalizację obiektów w momencie ich tworzenia, a destruktory za zwalnianie zasobów, gdy obiekty są niszczone. Kopiowanie obiektów może odbywać się na różne sposoby, a niewłaściwe zarządzanie kopiami może prowadzić do wycieków pamięci lub innych problemów z zasobami. W tej lekcji szczegółowo omówimy konstrukcję, działanie i znaczenie konstruktorów, destruktorów oraz metod kopiowania obiektów w C++.

Konstruktory

Konstruktor to specjalna metoda, która jest wywoływana automatycznie, gdy tworzony jest obiekt klasy. Służy do inicjalizacji danych obiektu i przygotowania go do działania. Konstruktor ma tę samą nazwę co klasa i nie posiada zwracanego typu, nawet void.

Rodzaje konstruktorów
  • Konstruktor domyślny: Konstruktor bez argumentów, który jest używany do tworzenia obiektu bez żadnych parametrów.
  • Konstruktor z parametrami: Konstruktor, który przyjmuje argumenty i umożliwia inicjalizację obiektu z określonymi wartościami.
  • Konstruktor kopiujący: Konstruktor, który tworzy nowy obiekt jako kopię istniejącego obiektu.
Przykład konstruktora domyślnego
class Samochod {
public:
    Samochod() {
        std::cout << "Konstruktor domyślny: Samochód został utworzony." << std::endl;
    }
};

int main() {
    Samochod auto1; // Wywołanie konstruktora domyślnego
    return 0;
}

W tym przykładzie konstruktor domyślny Samochod() jest wywoływany automatycznie, gdy obiekt auto1 jest tworzony.

Przykład konstruktora z parametrami
class Samochod {
public:
    std::string marka;
    Samochod(std::string m) : marka(m) {
        std::cout << "Konstruktor z parametrami: Samochód marki " << marka << " został utworzony." << std::endl;
    }
};

int main() {
    Samochod auto1("Toyota"); // Wywołanie konstruktora z parametrami
    return 0;
}

W tym przykładzie konstruktor z parametrami Samochod(std::string m) inicjalizuje pole marka przy użyciu wartości przekazanej jako argument.

Konstruktor kopiujący

Konstruktor kopiujący jest używany do tworzenia nowego obiektu jako kopii istniejącego obiektu. Jest to szczególnie przydatne w przypadku klas, które zarządzają dynamiczną pamięcią lub innymi zasobami.

Przykład konstruktora kopiującego
class Samochod {
public:
    std::string marka;
    Samochod(const Samochod& inny) : marka(inny.marka) {
        std::cout << "Konstruktor kopiujący: Skopiowano samochód marki " << marka << "." << std::endl;
    }
};

int main() {
    Samochod auto1("Toyota");
    Samochod auto2 = auto1; // Wywołanie konstruktora kopiującego
    return 0;
}

W powyższym przykładzie konstruktor kopiujący Samochod(const Samochod& inny) tworzy obiekt auto2 jako kopię obiektu auto1.

Destruktory

Destruktor to metoda, która jest automatycznie wywoływana, gdy obiekt przestaje istnieć. Destruktory są używane do zwalniania zasobów, takich jak pamięć dynamiczna, pliki czy połączenia sieciowe. Destruktor ma tę samą nazwę co klasa, ale poprzedzoną znakiem ~, i nie przyjmuje argumentów ani nie zwraca wartości.

Przykład destruktora
class Samochod {
public:
    ~Samochod() {
        std::cout << "Destruktor: Samochód został zniszczony." << std::endl;
    }
};

int main() {
    Samochod auto1; // Tworzenie obiektu
    return 0; // Destruktor jest wywoływany automatycznie
}

W tym przykładzie destruktor ~Samochod() jest wywoływany automatycznie, gdy obiekt auto1 przestaje istnieć (np. po zakończeniu bloku main()).

Kopiowanie obiektów

Kopiowanie obiektów może odbywać się na dwa sposoby: za pomocą konstruktora kopiującego lub operatora przypisania. W przypadku klas, które zarządzają dynamiczną pamięcią, kopiowanie może wymagać głębokiej kopii, aby uniknąć problemów związanych ze współdzieleniem zasobów.

Operator przypisania

Operator przypisania = jest używany do kopiowania wartości z jednego obiektu do drugiego. Domyślny operator przypisania wykonuje płytką kopię, ale możemy go przeciążyć, aby wykonać głęboką kopię.

Przykład operatora przypisania
class Samochod {
public:
    std::string marka;
    Samochod& operator=(const Samochod& inny) {
        if (this != &inny) {
            marka = inny.marka;
        }
        return *this;
    }
};

int main() {
    Samochod auto1("Toyota");
    Samochod auto2;
    auto2 = auto1; // Wywołanie operatora przypisania
    return 0;
}

W powyższym przykładzie operator przypisania operator= sprawdza, czy obiekt nie przypisuje sam do siebie (za pomocą if (this != &inny)), a następnie kopiuje wartość pola marka.

Głębokie i płytkie kopiowanie

Płytka kopia to kopiowanie wartości wskaźników, co oznacza, że zarówno oryginalny obiekt, jak i kopia wskazują na te same dane w pamięci. Może to prowadzić do problemów, gdy jeden z obiektów zwalnia pamięć, a drugi nadal próbuje do niej uzyskać dostęp.

Przykład problemu z płytką kopią
class Bufor {
public:
    char* dane;
    Bufor(size_t rozmiar) {
        dane = new char[rozmiar];
    }

    ~Bufor() {
        delete[] dane;
    }
};

int main() {
    Bufor buf1(10);
    Bufor buf2 = buf1; // Płytka kopia: buf2.dane wskazuje na te same dane co buf1.dane
    return 0;
}

W tym przykładzie destruktor ~Bufor() zwalnia pamięć, ale ponieważ oba obiekty buf1 i buf2 wskazują na te same dane, może dojść do wycieku pamięci lub błędów dostępu.

Głębokie kopiowanie

Głęboka kopia to kopiowanie danych, na które wskazują wskaźniki, co oznacza, że kopia ma swoje własne kopie danych, a nie tylko wskaźniki do tych samych zasobów.

Przykład głębokiej kopii
class Bufor {
public:
    char* dane;
    size_t rozmiar;

    Bufor(size_t rozmiar) : rozmiar(rozmiar) {
        dane = new char[rozmiar];
    }

    Bufor(const Bufor& inny) : rozmiar(inny.rozmiar) {
        dane = new char[rozmiar];
        std::copy(inny.dane, inny.dane + rozmiar, dane);
    }

    ~Bufor() {
        delete[] dane;
    }

    Bufor& operator=(const Bufor& inny) {
        if (this != &inny) {
            delete[] dane;
            rozmiar = inny.rozmiar;
            dane = new char[rozmiar];
            std::copy(inny.dane, inny.dane + rozmiar, dane);
        }
        return *this;
    }
};

W powyższym przykładzie konstruktor kopiujący i operator przypisania wykonują głęboką kopię, co oznacza, że każdy obiekt ma własne kopie danych w pamięci.

Podsumowanie

Konstruktory, destruktory i kopiowanie obiektów są kluczowymi elementami programowania obiektowego w C++. Zrozumienie, jak działają te mechanizmy, pozwala na efektywne zarządzanie zasobami i unikanie błędów związanych z pamięcią. Konstruktory inicjalizują obiekty, destruktory zwalniają zasoby, a kopiowanie obiektów musi być starannie zaprojektowane, aby uniknąć problemów z płytką i głęboką kopią. Dzięki tej lekcji zdobyłeś szczegółową wiedzę, która jest niezbędna do tworzenia bezpiecznych i wydajnych aplikacji w C++.

Następny temat ==> Przeładowywanie operatorów



Spis Treści - Wstęp do programowania

Lekcja 3: Rozwiązywanie problemów i poprawność programów Lekcja 4: Praca z różnymi typami danych Lekcja 5: Obsługa plików i pamięci Lekcja 6: Zaawansowane techniki programistyczne Lekcja 7: Wskaźniki i pamięć dynamiczna Lekcja 8: Struktura kodu i abstrakcja Lekcja 9: Rekurencja i jej zastosowania Lekcja 10: Analiza wydajności algorytmów Lekcja 11: Technika "dziel i zwyciężaj" Lekcja 12: Struktury danych o dynamicznej budowie Lekcja 13: Struktury hierarchiczne: Drzewa Lekcja 14: Struktury danych z bibliotek Lekcja 15: Algorytmy z nawrotami Lekcja 16: Programowanie dynamiczne Lekcja 17: Programowanie zachłanne Lekcja 18: Praca z grafami

Jeśli chciałbyś być poinformowany o następnych kursach to zapisz się do naszego newslettera: