Twoje Centrum Szkoleniowe

Nauczmy się dziś czegoś nowego!

Kurs programowania - C++

C++: Dziedziczenie i polimorfizm

Dziedziczenie umożliwia tworzenie nowych klas bazując na istniejących klasach, co ułatwia ponowne wykorzystanie kodu i organizację złożonych programów. Z kolei polimorfizm pozwala na wykorzystanie jednego interfejsu do różnych form funkcjonalności, co zwiększa elastyczność i skalowalność kodu.

W kontekście C++, dziedziczenie i polimorfizm są ze sobą ściśle powiązane. Dziedziczenie pozwala na tworzenie hierarchii klas, gdzie klasy pochodne dziedziczą atrybuty i zachowania po klasach bazowych, natomiast polimorfizm umożliwia wykorzystanie tych klas w sposób, który nie jest z góry określony przez ich hierarchię.

Przyjrzyjmy się bliżej tym dwóm koncepcjom, ich znaczeniu w projektowaniu oprogramowania i sposobom, w jakie współpracują one ze sobą, aby zwiększyć możliwości i efektywność kodu napisanego w języku C++. 

 

 

Rodzaje dziedziczenia

W C++ istnieją różne rodzaje dziedziczenia, które definiują, jak klasy pochodne mogą korzystać z członków klasy bazowej:

  1. Publiczne: Gdy klasa pochodna dziedziczy publicznie, wszystkie publiczne i chronione składowe klasy bazowej zachowują swój dostęp w klasie pochodnej.
  2. Chronione (Protected): W tym przypadku, publiczne i chronione składowe klasy bazowej stają się chronione w klasie pochodnej.
  3. Prywatne: Tutaj, publiczne i chronione składowe klasy bazowej stają się prywatnymi składowymi klasy pochodnej.

 

Wielokrotne dziedziczenie

C++ oferuje również możliwość wielokrotnego dziedziczenia, co oznacza, że klasa może dziedziczyć funkcjonalność z więcej niż jednej klasy bazowej. Jest to potężne narzędzie, ale również może prowadzić do skomplikowanych zależności i problemów, takich jak diament problemu dziedziczenia, gdzie ta sama klasa bazowa jest dziedziczona przez różne ścieżki.

 

Konstruktory i destruktory w dziedziczeniu

Konstruktory i destruktory są wywoływane w określonej kolejności, zgodnie z hierarchią dziedziczenia. Konstruktory klas bazowych są zwykle wywoływane przed konstruktorami klas pochodnych, podczas gdy destruktory działają w odwrotnej kolejności, co zapewnia prawidłową inicjalizację i deinicjalizację obiektów.

 

Polimorfizm Statyczny i Dynamiczny

W C++ istnieją dwa główne rodzaje polimorfizmu: statyczny i dynamiczny. Polimorfizm statyczny jest osiągany głównie przez przeciążanie funkcji i szablonów. Pozwala to na używanie tej samej nazwy funkcji dla różnych kontekstów, w zależności od argumentów przekazanych do funkcji. Z kolei polimorfizm dynamiczny korzysta z wirtualnych funkcji i jest związany z dziedziczeniem. Umożliwia różne implementacje tej samej metody w klasach pochodnych, co jest decydowane w czasie wykonania programu, a nie jego kompilacji.

 

Wirtualne Funkcje i Klasy Abstrakcyjne

Kluczowym elementem polimorfizmu dynamicznego są wirtualne funkcje. Definiują one interfejs, który klasy pochodne mogą nadpisać, dostarczając własne implementacje. Klasy zawierające przynajmniej jedną wirtualną funkcję nazywane są klasami abstrakcyjnymi i służą jako baza dla polimorfizmu. Klasy abstrakcyjne często zawierają co najmniej jedną czysto wirtualną funkcję, która musi być zaimplementowana przez klasy pochodne.

 

Dynamiczne Rzutowanie Typów

Dynamiczne rzutowanie typów, czyli dynamic_cast, jest wykorzystywane do bezpiecznego rzutowania obiektów do ich właściwego typu w hierarchii dziedziczenia. Jest to szczególnie przydatne w kontekście polimorfizmu, gdy musimy zarządzać obiektami różnych klas pochodnych poprzez wskaźniki lub referencje do klasy bazowej. dynamic_cast sprawdza w czasie wykonania, czy rzutowanie jest legalne, co zapewnia bezpieczeństwo typów i pomaga uniknąć błędów wynikających z niewłaściwego rzutowania.

 

 

Przykład dziedziczenia w C++

#include <iostream>
using namespace std;

// Klasa bazowa
class Pojazd {
public:
    Pojazd() { cout << "Tworzenie Pojazdu" << endl; }
    virtual ~Pojazd() { cout << "Usuwanie Pojazdu" << endl; }
    virtual void uruchom() { cout << "Pojazd uruchomiony" << endl; }
};

// Klasa pochodna
class Samochod : public Pojazd {
public:
    Samochod() { cout << "Tworzenie Samochodu" << endl; }
    ~Samochod() { cout << "Usuwanie Samochodu" << endl; }
    void uruchom() override { cout << "Samochod uruchomiony" << endl; }
};

int main() {
    Pojazd *pojazd = new Samochod();
    pojazd->uruchom();
    delete pojazd;
    return 0;
}

 

Analiza Kodu

 

#include <iostream>
using namespace std;

// Klasa bazowa
class Pojazd {
public:
    Pojazd() { cout << "Tworzenie Pojazdu" << endl; }
    virtual ~Pojazd() { cout << "Usuwanie Pojazdu" << endl; }
    virtual void uruchom() { cout << "Pojazd uruchomiony" << endl; }
};

Klasa Bazowa Pojazd:

  • Posiada konstruktor, który wyświetla komunikat przy tworzeniu obiektu.
  • Zawiera wirtualny destruktor, co jest kluczowe w hierarchii dziedziczenia, zwłaszcza przy pracy z wskaźnikami do obiektów klas bazowych. Zapewnia to poprawne wywołanie destruktorów klas pochodnych.
  • Metoda uruchom() jest oznaczona jako wirtualna, co pozwala na jej nadpisanie w klasach pochodnych.

 

 

// Klasa pochodna
class Samochod : public Pojazd {
public:
    Samochod() { cout << "Tworzenie Samochodu" << endl; }
    ~Samochod() { cout << "Usuwanie Samochodu" << endl; }
    void uruchom() override { cout << "Samochod uruchomiony" << endl; }
};

Klasa Pochodna Samochod:

  • Dziedziczy po klasie Pojazd.
  • Konstruktor i destruktor wypisują komunikaty przy tworzeniu i usuwaniu obiektów klasy Samochod.
  • Nadpisuje metodę uruchom(), co jest sygnalizowane przez słowo kluczowe override.

 

 

int main() {
    Pojazd *pojazd = new Samochod();
    pojazd->uruchom();
    delete pojazd;
    return 0;
}

Funkcja main:

  • Inicjuje wskaźnik typu Pojazd, przypisując do niego nowo utworzony obiekt klasy Samochod.
  • Jest to przykład polimorfizmu. Obiekt klasy Samochod jest traktowany jako Pojazd, ale zachowuje swoje specyficzne cechy (w tym nadpisaną metodę uruchom()).
  • Wywoływana jest metoda uruchom() klasy Samochod, mimo że wskaźnik jest typu Pojazd. Jest to możliwe dzięki wirtualnym metodom.
  • Używane jest słowo kluczowe delete do zwolnienia pamięci. Dzięki wirtualnemu destruktorowi w klasie Pojazd, destruktor Samochod jest również poprawnie wywoływany.

 

 

Przykład polimorfizmu w C++

#include <iostream>
using namespace std;

// Klasa bazowa
class Figura {
public:
    virtual void rysuj() = 0; // Czysta funkcja wirtualna
};

// Klasa pochodna
class Kwadrat : public Figura {
public:
    void rysuj() override { cout << "Rysowanie Kwadratu." << endl; }
};

class Kolo : public Figura {
public:
    void rysuj() override { cout << "Rysowanie Koła." << endl; }
};

int main() {
    Figura* f1 = new Kwadrat();
    Figura* f2 = new Kolo();
    f1->rysuj();
    f2->rysuj();
    delete f1;
    delete f2;
    return 0;
}

 

Analiza Kodu

 

#include <iostream>
using namespace std;

// Klasa bazowa
class Figura {
public:
    virtual void rysuj() = 0; // Czysta funkcja wirtualna
};

Klasa Bazowa Figura:

  • Jest to klasa abstrakcyjna, ponieważ zawiera czystą funkcję wirtualną rysuj(). Oznacza to, że nie można utworzyć bezpośrednio instancji tej klasy.
  • Słowo kluczowe virtual wskazuje, że funkcja rysuj() może być nadpisana w klasie pochodnej.

 

 

// Klasa pochodna
class Kwadrat : public Figura {
public:
    void rysuj() override { cout << "Rysowanie Kwadratu." << endl; }
};

class Kolo : public Figura {
public:
    void rysuj() override { cout << "Rysowanie Koła." << endl; }
};

Klasy Pochodne Kwadrat i Kolo:

  • Obydwie klasy dziedziczą po klasie Figura.
  • Implementują one metodę rysuj(), co jest wymagane, ponieważ Figura jest klasą abstrakcyjną.
  • Słowo kluczowe override zapewnia, że metoda jest prawidłowo nadpisana z klasy bazowej.

 

 

int main() {
    Figura* f1 = new Kwadrat();
    Figura* f2 = new Kolo();
    f1->rysuj();
    f2->rysuj();
    delete f1;
    delete f2;
    return 0;
}

Funkcja main:

  • Tworzone są dwa wskaźniki typu Figura, f1 i f2, ale inicjalizowane są jako Kwadrat i Kolo.
  • Jest to przykład polimorfizmu. Mimo że f1 i f2 są wskaźnikami do Figura, wywołują odpowiednie implementacje metody rysuj() w zależności od rzeczywistego typu obiektu, na który wskazują.
  • Metoda rysuj() jest wywoływana dla każdego obiektu, pokazując polimorficzne zachowanie.
  • Następuje zwolnienie zasobów za pomocą delete, co jest ważne przy zarządzaniu pamięcią dynamiczną w C++.

Testy przypięte do lekcji
Aby uzyskać dostęp do testów i ćwiczeń interaktywnych - Zaloguj się
Aby widzieć ocenę lekcji - Zaloguj się