24.5 — Specyfikatory dziedziczenia i dostępu

W poprzednich lekcjach w tym rozdziale nauczyłeś się trochę o tym, jak działa dziedziczenie bazowe. We wszystkich naszych dotychczasowych przykładach korzystaliśmy z dziedziczenia publicznego. Oznacza to, że nasza klasa pochodna publicznie dziedziczy klasę bazową.

W tej lekcji przyjrzymy się bliżej dziedziczeniu publicznemu, a także dwóm innym rodzajom dziedziczenia (prywatnemu i chronionemu). Zbadamy także, w jaki sposób różne rodzaje dziedziczenia współdziałają ze specyfikatorami dostępu, aby umożliwić lub ograniczyć dostęp członkom.

Do tego momentu poznaliśmy specyfikatory dostępu prywatnego i publicznego, które określają, kto może uzyskać dostęp do członków klasy. Dla przypomnienia, dostęp do członków publicznych może uzyskać każdy. Dostęp do członków prywatnych mogą uzyskać jedynie funkcje członkowskie tej samej klasy lub znajomi. Oznacza to, że klasy pochodne nie mają bezpośredniego dostępu do prywatnych elementów klasy bazowej!

class Base
{
private:
    int m_private {}; // dostępne są tylko dla członków Bazy i przyjaciół (nie klas pochodnych)
public:
    int m_public {}; // może uzyskać dostęp każdy
};

Jest to całkiem proste i powinieneś już się do tego przyzwyczaić.

Specyfikator dostępu chronionego

W przypadku klas dziedziczonych sytuacja staje się nieco bardziej skomplikowana.

C++ ma trzeci specyfikator dostępu, o którym jeszcze nie rozmawialiśmy, ponieważ jest przydatny tylko w kontekście dziedziczenia. Specyfikator dostępu protected umożliwia klasie, do której należy element członkowski, przyjaciołom i klasom pochodnym na dostęp do elementu członkowskiego. Jednakże elementy chronione nie są dostępne spoza klasy.

class Base
{
public:
    int m_public {}; // może uzyskać dostęp każdy
protected:
    int m_protected {}; // Dostęp do nich mogą uzyskać członkowie Base, przyjaciele i klasy pochodne
private:
    int m_private {}; // dostępne są tylko dla członków Bazy i przyjaciół (ale nie klas pochodnych)
};

class Derived: public Base
{
public:
    Derived()
    {
        m_public = 1; // dozwolony: może uzyskać dostęp do publicznych elementów bazy z klasy pochodnej
        m_protected = 2; // dozwolony: może uzyskać dostęp do chronionych elementów podstawowych z klasy pochodnej
        m_private = 3; // niedozwolone: nie można uzyskać dostępu do prywatnych elementów bazowych z klasy pochodnej
    }
};

int main()
{
    Base base;
    base.m_public = 1; // dozwolony: może uzyskać dostęp do elementów publicznych z zewnątrz klasy
    base.m_protected = 2; // niedozwolone: nie można uzyskać dostępu do chronionych elementów spoza klasy
    base.m_private = 3; // niedozwolone: nie można uzyskać dostępu do prywatnych elementów spoza klasy

    return 0;
}

W powyższym przykładzie widać, że chroniony element bazowy m_protected jest bezpośrednio dostępny dla klasy pochodnej, ale nie publicznie.

Kiedy więc powinienem używać specyfikatora dostępu chronionego?

Dzięki atrybutowi chronionemu w klasie bazowej klasy pochodne mogą uzyskać bezpośredni dostęp do tego elementu. Oznacza to, że jeśli później zmienisz cokolwiek w tym chronionym atrybucie (typ, znaczenie wartości itp.), prawdopodobnie będziesz musiał zmienić zarówno klasę bazową ORAZ wszystkie klasy pochodne.

Dlatego użycie specyfikatora dostępu chronionego jest najbardziej przydatne, gdy ty (lub twój zespół) będziecie wywodzić się z własnych klas, a liczba klas pochodnych jest rozsądna. W ten sposób, jeśli dokonasz zmian w implementacji klasy bazowej i w rezultacie konieczne będą aktualizacje klas pochodnych, możesz dokonać aktualizacji samodzielnie (i nie będzie to trwało wiecznie, ponieważ liczba klas pochodnych jest ograniczona).

Uznaczenie twoich członków jako prywatnych oznacza, że ​​klasy publiczne i pochodne nie mogą bezpośrednio wprowadzać zmian w klasie bazowej. Jest to dobre do izolowania klas publicznych lub pochodnych od zmian w implementacji i do zapewnienia prawidłowego utrzymania niezmienników. Oznacza to jednak również, że Twoja klasa może potrzebować większego publicznego (lub chronionego) interfejsu do obsługi wszystkich funkcji potrzebnych do działania klas publicznych lub pochodnych, co wiąże się z własnymi kosztami budowy, testowania i utrzymania.

Ogólnie rzecz biorąc, jeśli to możliwe, lepiej ustawić członków prywatnych, jeśli to możliwe, i używać chronionych tylko wtedy, gdy planowane są klasy pochodne, a koszt zbudowania i utrzymania interfejsu dla tych członków prywatnych jest zbyt wysoki.

Najlepsza praktyka

Preferuj członków prywatnych zamiast chronionych członków.

Różne rodzaje dziedziczenia i ich wpływ na dostęp

Po pierwsze, istnieją trzy różne sposoby dziedziczenia klas z innych klas: publiczne, chronione i prywatne.

Aby to zrobić, po prostu określ, jaki typ dostępu chcesz wybrać przy wyborze klasy, z której chcesz dziedziczyć:

// Dziedzicz z bazy publicznej
class Pub: public Base
{
};

// Dziedzicz z bazy w sposób chroniony
class Pro: protected Base
{
};

// Dziedzicz z bazy prywatnie
class Pri: private Base
{
};

class Def: Base // Domyślnie dziedziczenie prywatne
{
};

Jeśli nie wybierzesz typu dziedziczenia, domyślnie C++ do dziedziczenia prywatnego (tak jak członkowie domyślnie mają dostęp prywatny, jeśli nie określisz inaczej).

To daje nam 9 kombinacji: 3 specyfikatory dostępu członków (publiczne, prywatne i chronione) oraz 3 typy dziedziczenia (publiczne, prywatne i chronione).

Jaka jest więc między nimi różnica? W skrócie, gdy elementy są dziedziczone, specyfikator dostępu dla dziedziczonego elementu może zostać zmieniony (tylko w klasie pochodnej) w zależności od rodzaju zastosowanego dziedziczenia. Innymi słowy, elementy członkowskie, które były publiczne lub chronione w klasie bazowej, mogą zmieniać specyfikatory dostępu w klasie pochodnej.

Może to wydawać się nieco zagmatwane, ale nie jest tak źle. Resztę tej lekcji spędzimy na szczegółowym badaniu tego.

Przechodząc przez przykłady, pamiętaj o następujących zasadach:

  • Klasa może zawsze uzyskać dostęp do swoich własnych (niedziedziczonych) elementów.
  • Osoby publiczne uzyskują dostęp do elementów klasy w oparciu o specyfikatory dostępu klasy, do której uzyskuje dostęp.
  • Klasa pochodna uzyskuje dostęp do odziedziczonych elementów w oparciu o dostęp specyfikator dziedziczony z klasy nadrzędnej. Różni się to w zależności od specyfikatora dostępu i rodzaju zastosowanego dziedziczenia.

Dziedziczenie publiczne

Dziedziczenie publiczne jest zdecydowanie najczęściej używanym rodzajem dziedziczenia. W rzeczywistości bardzo rzadko będziesz widzieć lub używać innych typów dziedziczenia, dlatego powinieneś skupić się przede wszystkim na zrozumieniu tej sekcji. Na szczęście dziedziczenie publiczne jest również najłatwiejsze do zrozumienia. Kiedy dziedziczysz klasę bazową publicznie, odziedziczone publiczne elementy członkowskie pozostają publiczne, a odziedziczone chronione elementy członkowskie pozostają chronione. Dziedziczone prywatne składowe, które były niedostępne, ponieważ były prywatne w klasie bazowej, pozostają niedostępne.

Specyfikator dostępu w klasie bazowejSpecyfikator dostępu w przypadku dziedziczenia publicznego
PublicznyPubliczny
ChronionyChroniony
PrywatnyNiedostępny

Oto przykład pokazujący, jak to działa:

class Base
{
public:
    int m_public {};
protected:
    int m_protected {};
private:
    int m_private {};
};

class Pub: public Base // uwaga: dziedzictwo publiczne
{
    // Dziedziczenie publiczne oznacza:
    // Publiczne odziedziczeni członkowie pozostają publiczni (więc m_public jest traktowane jako publiczne)
    // Chronieni dziedziczeni członkowie pozostają chronieni (więc m_protected jest traktowane jako prywatne) chroniony)
    // Prywatni odziedziczeni członkowie pozostają niedostępni (więc m_private jest niedostępny)
public:
    Pub()
    {
        m_public = 1; // okay: m_public został odziedziczony jako publiczny
        m_protected = 2; // ok: m_protected został odziedziczony jako chroniony
        m_private = 3; // nie w porządku: m_private jest niedostępny z klasy pochodnej
    }
};

int main()
{
    // Dostęp zewnętrzny wykorzystuje specyfikatory dostępu klasy, do której uzyskuje się dostęp.
    Base base;
    base.m_public = 1; // OK: m_public jest ekspertem w Base
    base.m_protected = 2; // nie w porządku: m_protected jest chroniony w Base
    base.m_private = 3; // nie w porządku: m_private jest prywatny w Base

    Pub pub;
    pub.m_public = 1; // ok: m_public jest profesor w Pub
    pub.m_protected = 2; // nie w porządku: m_protected jest chroniony w Pub
    pub.m_private = 3; // nie w porządku: m_private jest niedostępny w Pub

    return 0;
}

To jest to samo, co w powyższym przykładzie, w którym wprowadziliśmy specyfikator dostępu chronionego, z tą różnicą, że utworzyliśmy także instancję klasy pochodnej, aby pokazać, że w przypadku dziedziczenia publicznego wszystko działa identycznie w bazowej i pochodnej class.

Dziedziczenie publiczne jest tym, czego powinieneś używać, chyba że masz konkretny powód, aby tego nie robić.

Najlepsza praktyka

Używaj dziedziczenia publicznego, jeśli nie masz konkretnego powodu, aby zrobić inaczej.

Dziedziczenie chronione

Dziedziczenie chronione jest najrzadziej spotykaną metodą dziedziczenia. Prawie nigdy nie jest używany, z wyjątkiem bardzo szczególnych przypadków. W przypadku dziedziczenia chronionego chronione są elementy publiczne i chronione, a elementy prywatne pozostają niedostępne.

Ponieważ ta forma dziedziczenia jest tak rzadka, pominiemy przykład i po prostu podsumujemy w tabeli:

Specyfikator dostępu w klasie bazowejSpecyfikator dostępu w przypadku dziedziczenia chronionego
PublicznyChroniony
ChronionyChroniony
PrywatnyNiedostępny

Dziedziczenie prywatne

W przypadku dziedziczenia prywatnego wszyscy członkowie z klasy bazowej są dziedziczeni jako prywatni. Oznacza to, że elementy prywatne są niedostępne, a elementy chronione i publiczne stają się prywatne.

Zauważ, że nie ma to wpływu na sposób, w jaki klasa pochodna uzyskuje dostęp do elementów odziedziczonych od swojego rodzica! Wpływa tylko na kod próbujący uzyskać dostęp do tych elementów poprzez klasę pochodną.

class Base
{
public:
    int m_public {};
protected:
    int m_protected {};
private:
    int m_private {};
};

class Pri: private Base // uwaga: spadek prywatny
{
    // Dziedziczenie prywatne oznacza:
    // Odziedziczone publicznie elementy członkowskie stają się prywatne (więc m_public jest traktowane jako prywatne)
    // Chronieni dziedziczeni członkowie staną się prywatnymi (więc m_protected jest traktowany jako prywatny)
    // Prywatni odziedziczeni członkowie pozostają niedostępni (więc m_private jest niedostępny)
public:
    Pri()
    {
        m_public = 1; // ok: m_public jest teraz prywatny w Pri
        m_protected = 2; // OK: m_protected jest teraz prywatny w Pri
        m_private = 3; // nie w porządku: klasy pochodne nie mają dostępu do prywatnych elementów klasy bazowej
    }
};

int main()
{
    // Dostęp zewnętrzny wykorzystuje specyfikatory dostępu klasy, do której uzyskuje się dostęp.
    // W tym przypadku specyfikatory dostępu base.
    Base base;
    base.m_public = 1; // OK: m_public jest ekspertem w Base
    base.m_protected = 2; // nie w porządku: m_protected jest chroniony w Base
    base.m_private = 3; // nie w porządku: m_private jest prywatny w Base

    Pri pri;
    pri.m_public = 1; // nie w porządku: m_public jest teraz prywatny w Pri
    pri.m_protected = 2; // nie w porządku: m_protected jest teraz prywatny w Pri
    pri.m_private = 3; // nie w porządku: m_private jest niedostępny w Pri

    return 0;
}

Podsumowując w formie tabeli:

Specyfikator dostępu w klasie bazowejSpecyfikator dostępu w przypadku dziedziczenia prywatnego
PublicznyPrywatny
ChronionyPrywatny
PrywatnyNiedostępny

Dziedziczenie prywatne może być przydatne, gdy klasa pochodna nie ma oczywistego związku z klasą bazową, ale używa klasy bazowej do wewnętrznej implementacji. W takim przypadku prawdopodobnie nie chcemy, aby publiczny interfejs klasy bazowej był eksponowany przez obiekty klasy pochodnej (tak by było, gdybyśmy dziedziczyli publicznie).

W praktyce dziedziczenie prywatne jest rzadko stosowane.

Ostatni przykład

class Base
{
public:
	int m_public {};
protected:
	int m_protected {};
private:
	int m_private {};
};

Base może uzyskać dostęp do swoich własnych składowych bez ograniczeń. Publiczność może uzyskać dostęp tylko do m_public. Klasy pochodne mają dostęp do m_public i m_protected.

class D2 : private Base // uwaga: spadek prywatny
{
	// Dziedziczenie prywatne oznacza:
	// Odziedziczone publicznie elementy członkowskie stają się prywatne
	// Ochroneni dziedziczeni członkowie staną się prywatnymi
	// Prywatni odziedziczeni członkowie pozostają niedostępni
public:
	int m_public2 {};
protected:
	int m_protected2 {};
private:
	int m_private2 {};
};

D2 może uzyskać dostęp do swoich członków bez ograniczeń. D2 może uzyskać dostęp do członków m_public i m_protected Base, ale nie m_private. Ponieważ D2 odziedziczył Base prywatnie, m_public i m_protected są teraz uważane za prywatne, gdy są dostępne przez D2. Oznacza to, że społeczeństwo nie może uzyskać dostępu do tych zmiennych podczas korzystania z obiektu D2, podobnie jak żadne klasy wywodzące się z D2.

class D3 : public D2
{
	// Dziedziczenie publiczne oznacza:
	// Odziedziczone publicznie elementy członkowskie pozostają publiczne
	// Ochroneni dziedziczeni członkowie pozostają chronieni
	// Prywatni odziedziczeni członkowie pozostają niedostępni
public:
	int m_public3 {};
protected:
	int m_protected3 {};
private:
	int m_private3 {};
};

D3 może uzyskać dostęp do swoich własnych elementów bez ograniczeń. D3 ma dostęp do członków m_public2 i m_protected2 D2, ale nie m_private2. Ponieważ D3 dziedziczy D2 publicznie, m_public2 i m_protected2 zachowują swoje specyfikatory dostępu, gdy są dostępne przez D3. D3 nie ma dostępu do m_private Base, które było już prywatne w Base. Nie ma też dostępu do m_protected ani m_public Base, które stały się prywatne, gdy D2 je odziedziczył.

Streszczenie

Sposób, w jaki specyfikatory dostępu, typy dziedziczenia i klasy pochodne współdziałają ze sobą, powoduje wiele zamieszania. Aby spróbować jak najbardziej wyjaśnić sprawę:

Po pierwsze, klasa (i przyjaciele) zawsze mogą uzyskać dostęp do swoich własnych, niedziedziczonych członków. Specyfikatory dostępu wpływają tylko na to, czy osoby z zewnątrz i klasy pochodne mogą uzyskać dostęp do tych elementów.

Po drugie, gdy klasy pochodne dziedziczą elementy członkowskie, elementy te mogą zmieniać specyfikatory dostępu w klasie pochodnej. Nie ma to wpływu na własne (niedziedziczone) składowe klas pochodnych (które mają własne specyfikatory dostępu). Wpływa tylko na to, czy osoby z zewnątrz i klasy wywodzące się z klasy pochodnej mogą uzyskać dostęp do tych odziedziczonych elementów.

Oto tabela wszystkich kombinacji specyfikatorów dostępu i typów dziedziczenia:

Specyfikator dostępu w klasie bazowejSpecyfikator dostępu w przypadku dziedziczenia publicznegoSpecyfikator dostępu w przypadku dziedziczenia prywatnegoSpecyfikator dostępu w przypadku dziedziczenia chronionego
PublicznyPublicznyPrywatnyChroniony
ChronionyChronionyPrywatnyChroniony
PrywatnyNiedostępnyNiedostępnyNiedostępny

Na koniec, chociaż w powyższych przykładach pokazaliśmy tylko przykłady z użyciem zmiennych składowych, te reguły dostępu obowiązują dla wszystkich członków (np. funkcji składowych i typów zadeklarowanych wewnątrz klasy).

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
171 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze