Klasy abstrakcyjne i interfejsy

Kurs: Wstęp do programowania
Lekcja 8: Struktura kodu i abstrakcja
Temat 4: Klasy abstrakcyjne i interfejsy

⇓ spis treści ⇓


Klasy abstrakcyjne i interfejsy są podstawowymi narzędziami w programowaniu obiektowym, które umożliwiają projektowanie elastycznych i skalowalnych systemów. Klasa abstrakcyjna w C++ to klasa, która nie może być bezpośrednio instancjonowana, ponieważ jej głównym celem jest służyć jako baza dla innych klas. Zawiera co najmniej jedną metodę czysto wirtualną, której implementację muszą dostarczyć klasy pochodne. Dzięki klasom abstrakcyjnym i interfejsom możemy definiować wspólne interfejsy dla różnych klas, co ułatwia zarządzanie złożonością systemu i pozwala na bardziej modułowe podejście do projektowania.

Co to jest klasa abstrakcyjna?

Klasa abstrakcyjna to klasa, która nie może być bezpośrednio instancjonowana. Zawiera co najmniej jedną metodę czysto wirtualną, która jest deklarowana przy użyciu składni = 0. Taka metoda czysto wirtualna nie posiada implementacji i musi zostać zaimplementowana w klasach pochodnych. Klasy abstrakcyjne służą do definiowania interfejsów dla grupy powiązanych klas, umożliwiając tworzenie bardziej elastycznych i rozszerzalnych struktur.

Składnia i przykład klasy abstrakcyjnej

Oto jak wygląda składnia klasy abstrakcyjnej w C++:

#include <iostream>

class Ksztalt {
public:
    virtual void rysuj() const = 0; // Metoda czysto wirtualna
    virtual ~Ksztalt() {} // Wirtualny destruktor
};

class Kolo : public Ksztalt {
public:
    void rysuj() const override {
        std::cout << "Rysuję koło" << std::endl;
    }
};

class Kwadrat : public Ksztalt {
public:
    void rysuj() const override {
        std::cout << "Rysuję kwadrat" << std::endl;
    }
};

int main() {
    Ksztalt* k1 = new Kolo();
    Ksztalt* k2 = new Kwadrat();

    k1->rysuj();
    k2->rysuj();

    delete k1;
    delete k2;
    return 0;
}

W powyższym przykładzie Ksztalt jest klasą abstrakcyjną, ponieważ zawiera metodę czysto wirtualną rysuj(). Klasy Kolo i Kwadrat dziedziczą po Ksztalt i dostarczają implementacje metody rysuj(). Dzięki temu możemy używać wskaźników do klasy bazowej Ksztalt, aby wywoływać odpowiednie metody w klasach pochodnych, co jest przykładem polimorfizmu.

Metody czysto wirtualne

Metoda czysto wirtualna to metoda, która nie posiada implementacji w klasie bazowej i musi zostać zaimplementowana w klasach pochodnych. Deklaruje się ją za pomocą składni = 0:

class Przycisk {
public:
    virtual void kliknij() = 0; // Metoda czysto wirtualna
};

Metoda kliknij() jest czysto wirtualna i musi zostać zaimplementowana w klasach, które dziedziczą po klasie Przycisk. Jeśli klasa pochodna nie zaimplementuje tej metody, to również stanie się klasą abstrakcyjną.

Dlaczego używamy metod czysto wirtualnych?
  • Wymuszanie implementacji w klasach pochodnych: Metody czysto wirtualne zapewniają, że każda klasa pochodna musi dostarczyć swoją własną implementację, co jest kluczowe dla polimorfizmu.
  • Projektowanie interfejsów: Klasy abstrakcyjne mogą definiować wspólne interfejsy dla grupy klas, co ułatwia projektowanie i utrzymanie kodu.

Interfejsy w C++

W języku C++ nie istnieje oddzielny mechanizm do definiowania interfejsów, jak w niektórych innych językach (np. w Java). Zamiast tego, interfejsy tworzy się za pomocą klas abstrakcyjnych, które zawierają wyłącznie metody czysto wirtualne. Taka klasa pełni rolę interfejsu, który określa zestaw funkcjonalności, jakie muszą zaimplementować klasy pochodne.

Przykład interfejsu
#include <iostream>

class Drukowalny {
public:
    virtual void drukuj() const = 0; // Metoda czysto wirtualna
    virtual ~Drukowalny() {} // Wirtualny destruktor
};

class Dokument : public Drukowalny {
public:
    void drukuj() const override {
        std::cout << "Drukuję dokument" << std::endl;
    }
};

class Zdjecie : public Drukowalny {
public:
    void drukuj() const override {
        std::cout << "Drukuję zdjęcie" << std::endl;
    }
};

int main() {
    Drukowalny* d1 = new Dokument();
    Drukowalny* d2 = new Zdjecie();

    d1->drukuj();
    d2->drukuj();

    delete d1;
    delete d2;
    return 0;
}

W powyższym przykładzie klasa Drukowalny pełni rolę interfejsu, ponieważ zawiera wyłącznie metodę czysto wirtualną drukuj(). Klasy Dokument i Zdjecie implementują ten interfejs, definiując własne wersje metody drukuj().

Polimorfizm a klasy abstrakcyjne

Polimorfizm to jedna z najważniejszych cech programowania obiektowego, która pozwala na wywoływanie metod klas pochodnych za pośrednictwem wskaźników lub referencji do klasy bazowej. Klasy abstrakcyjne odgrywają kluczową rolę w realizacji polimorfizmu, ponieważ umożliwiają definiowanie wspólnego interfejsu dla różnych klas. Dzięki temu możemy tworzyć bardziej elastyczne i rozszerzalne systemy.

Przykład polimorfizmu z klasami abstrakcyjnymi
#include <iostream>

class Zwierze {
public:
    virtual void wydajDzwiek() const = 0; // Metoda czysto wirtualna
    virtual ~Zwierze() {}
};

class Pies : public Zwierze {
public:
    void wydajDzwiek() const override {
        std::cout << "Hau hau!" << std::endl;
    }
};

class Kot : public Zwierze {
public:
    void wydajDzwiek() const override {
        std::cout << "Miau!" << std::endl;
    }
};

void wydajDzwiekZwierzecia(const Zwierze& z) {
    z.wydajDzwiek();
}

int main() {
    Pies pies;
    Kot kot;

    wydajDzwiekZwierzecia(pies);
    wydajDzwiekZwierzecia(kot);

    return 0;
}

W powyższym przykładzie funkcja wydajDzwiekZwierzecia przyjmuje referencję do klasy Zwierze i wywołuje metodę wydajDzwiek(). Dzięki polimorfizmowi wywoływane są odpowiednie metody w klasach pochodnych Pies i Kot.

Destruktory w klasach abstrakcyjnych

Wirtualny destruktor w klasie abstrakcyjnej jest niezbędny, jeśli zamierzamy usuwać obiekty klas pochodnych za pośrednictwem wskaźników do klasy bazowej. Brak wirtualnego destruktora może prowadzić do niezdefiniowanego zachowania i wycieków pamięci, ponieważ destruktor klasy pochodnej nie zostanie wywołany.

Przykład użycia wirtualnego destruktora
#include <iostream>

class Baza {
public:
    virtual ~Baza() {
        std::cout << "Destruktor Baza" << std::endl;
    }
    virtual void funkcja() const = 0;
};

class Pochodna : public Baza {
public:
    ~Pochodna() {
        std::cout << "Destruktor Pochodna" << std::endl;
    }
    void funkcja() const override {
        std::cout << "Funkcja Pochodna" << std::endl;
    }
};

int main() {
    Baza* obiekt = new Pochodna();
    delete obiekt; // Wywołuje destruktory w odpowiedniej kolejności
    return 0;
}

W tym przykładzie destruktor klasy Baza jest wirtualny, co zapewnia, że destruktor klasy Pochodna zostanie poprawnie wywołany, gdy obiekt zostanie usunięty za pomocą wskaźnika do klasy bazowej.

Podsumowanie

Klasy abstrakcyjne i interfejsy są fundamentalnymi narzędziami w programowaniu obiektowym, które umożliwiają projektowanie elastycznych i skalowalnych systemów. Klasy abstrakcyjne definiują wspólne interfejsy dla grupy klas, co ułatwia zarządzanie złożonością kodu i wspiera polimorfizm. Zrozumienie, jak tworzyć i używać klas abstrakcyjnych oraz jak stosować interfejsy, jest kluczowe dla efektywnego projektowania obiektowego w języku C++.

Następny temat ==> Mechanizm enkapsulacji



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: