14.5 — Publiczne i prywatne elementy członkowskie oraz specyfikatory dostępu

Załóżmy, że idziesz ulicą w rześki jesienny dzień i jesz burrito. Chcesz gdzieś usiąść, więc rozglądasz się. Po lewej stronie znajduje się park ze skoszoną trawą i cienistymi drzewami, kilkoma niewygodnymi ławkami i wrzeszczącymi dziećmi na pobliskim placu zabaw. Po prawej stronie znajduje się rezydencja nieznajomego. Za oknem widać wygodny rozkładany fotel i trzaskający kominek.

Z ciężkim westchnieniem wybierasz park.

Kluczowym wyznacznikiem Twojego wyboru jest to, że park jest przestrzenią publiczną, a rezydencja prywatną. Ty (i każda inna osoba) masz swobodny dostęp do przestrzeni publicznych. Jednak tylko członkowie rezydencji (lub osoby, którym udzielono wyraźnego pozwolenia na wejście) mają dostęp do prywatnej rezydencji.

Dostęp członków

Podobna koncepcja dotyczy elementów typu klasy. Każdy członek typu klasy ma właściwość zwaną an poziomem dostępu który określa, kto może uzyskać dostęp do tego elementu członkowskiego.

C++ ma trzy różne poziomy dostępu: publiczny, prywatny, I protected. W tej lekcji omówimy dwa powszechnie używane poziomy dostępu: publiczny i prywatny.

Powiązana treść

Chroniony poziom dostępu omawiamy w rozdziale poświęconym dziedziczeniu (lekcja 24.5 — Specyfikatory dziedziczenia i dostępu).

Za każdym razem, gdy uzyskiwany jest dostęp do elementu członkowskiego, kompilator sprawdza, czy poziom dostępu elementu członkowskiego pozwala na dostęp do tego elementu. Jeżeli dostęp nie jest dozwolony, kompilator wygeneruje błąd kompilacji. Ten system poziomów dostępu jest czasami nazywany nieformalnie kontrolą dostępu.

Domyślnie elementy struktury są publiczne

Członkowie posiadający publiczny poziom dostępu członkowie publiczni. Członkowie publiczni są członkami typu klasy, do którego nie ma żadnych ograniczeń dotyczących dostępu. Podobnie jak w przypadku parku w naszej początkowej analogii, dostęp do członków publicznych może mieć każdy (o ile znajdują się w zasięgu).

Dostęp do elementów publicznych mogą uzyskać inni członkowie tej samej klasy. Warto zauważyć, że dostęp do członków publicznych można również uzyskać za pomocą publicznie, co nazywamy istniejącym kodem poza członkowie danego typu klasy. Przykłady publicznie obejmują funkcje niebędące członkami, a także elementy innych typów klas.

Kluczowa informacja

Domyślnie elementy struktury są publiczne. Dostęp do elementów publicznych mogą uzyskać inni członkowie danego typu klasy oraz użytkownicy publiczni.

Termin „publiczny” jest używany w odniesieniu do kodu, który istnieje poza członkami danego typu klasy. Obejmuje to funkcje niebędące członkami, a także elementy członkowskie innych typów klas.

Domyślnie wszyscy członkowie struktury są członkami publicznymi.

Rozważmy następującą strukturę:

#include <iostream>

struct Date
{
    // struct members are public by default, can be accessed by anyone
    int year {};       // public by default
    int month {};      // public by default
    int day {};        // public by default

    void print() const // public by default
    {
        // public members can be accessed in member functions of the class type
        std::cout << year << '/' << month << '/' << day;
    }
};

// non-member function main is part of "the public"
int main()
{
    Date today { 2020, 10, 14 }; // aggregate initialize our struct

    // public members can be accessed by the public
    today.day = 16; // okay: the day member is public
    today.print();  // okay: the print() member function is public

    return 0;
}

W tym przykładzie dostęp do członków można uzyskać w trzech miejscach:

  • W ramach funkcji członkowskiej print(), uzyskujemy dostęp do year, month, I day elementy ukrytego obiektu.
  • W main(), mamy bezpośredni dostęp today.day aby ustawić jego wartość.
  • W main(), nazywamy funkcją składową today.print().

Wszystkie trzy rodzaje dostępu są dozwolone, ponieważ dostęp do członków publicznych można uzyskać z dowolnego miejsca.

Ponieważ main() nie jest członkiem Date, uważa się, że jest częścią publicznie. Jednakże, ponieważ publicznie ma dostęp do członków społeczeństwa, main() może uzyskać bezpośredni dostęp do członków Date (co obejmuje wezwanie do today.print()).

Domyślnie członkowie klasy są prywatni

Członkowie posiadający prywatny poziom dostępu członkowie prywatni. Członkowie prywatni są członkami typu klasy, do którego dostęp mają tylko inni członkowie tej samej klasy.

Rozważmy następujący przykład, który jest prawie identyczny z powyższym:

#include <iostream>

class Date // now a class instead of a struct
{
    // class members are private by default, can only be accessed by other members
    int m_year {};     // private by default
    int m_month {};    // private by default
    int m_day {};      // private by default

    void print() const // private by default
    {
        // private members can be accessed in member functions
        std::cout << m_year << '/' << m_month << '/' << m_day;
    }
};

int main()
{
    Date today { 2020, 10, 14 }; // compile error: can no longer use aggregate initialization

    // private members can not be accessed by the public
    today.m_day = 16; // compile error: the m_day member is private
    today.print();    // compile error: the print() member function is private

    return 0;
}

W tym przykładzie dostęp do członków można uzyskać w tych samych trzech miejscach:

  • W ramach funkcji członkowskiej print(), uzyskujemy dostęp do m_year, m_month, I m_day elementy ukrytego obiektu.
  • W main(), mamy bezpośredni dostęp today.m_day aby ustawić jego wartość.
  • W main(), nazywamy funkcją składową today.print().

Jeśli jednak skompilujesz ten program, zauważysz, że wygenerowane zostaną trzy błędy kompilacji.

W main(), wypowiedzi today.m_day = 16 i today.print() teraz oba generują błędy kompilacji. Dzieje się tak, ponieważ main() jest częścią społeczeństwa i społeczeństwo nie ma bezpośredniego dostępu do członków prywatnych.

W print(), dostęp do członków m_year, m_month, I m_day jest dozwolony. Dzieje się tak, ponieważ print() jest członkiem klasy, a członkowie klasy mają dostęp do prywatnych członków.

Skąd więc wziął się trzeci błąd kompilacji? Być może zaskakujące jest to, że inicjalizacja today powoduje teraz błąd kompilacji. W lekcji 13.8 -- Inicjowanie agregatu Struct zauważyliśmy, że agregat nie może zawierać „żadnych prywatnych ani chronionych, niestatycznych elementów danych”. Nasza Date klasa ma prywatne elementy danych (ponieważ elementy członkowskie klas są domyślnie prywatne), zatem nasza Date klasa nie kwalifikuje się jako agregat. Dlatego nie możemy już używać inicjalizacji agregowanej do jej inicjowania.

Jak prawidłowo inicjować klasy (które zazwyczaj nie są agregatami) omówimy w nadchodzącej lekcji 14.9 -- Wprowadzenie do konstruktorów.

Kluczowa informacja

Członkowie klasy są domyślnie prywatni. Dostęp do elementów prywatnych mogą uzyskać inni członkowie klasy, ale nie można uzyskać do nich dostępu publicznego.

Klasa zawierająca elementy prywatne nie jest już agregacją i dlatego nie można już używać inicjalizacji agregacji.

Nazywanie zmiennych składowych prywatnych

W C++ powszechną konwencją jest nadawanie nazw członkom danych prywatnych rozpoczynających się przedrostkiem „m_”. Dzieje się tak z kilku ważnych powodów.

Rozważmy następującą funkcję składową jakiejś klasy:

// Some member function that sets private member m_name to the value of the name parameter
void setName(std::string_view name)
{
    m_name = name;
}

Po pierwsze, przedrostek „m_” pozwala nam łatwo odróżnić elementy danych od parametrów funkcji lub zmiennych lokalnych w obrębie funkcji składowej. Łatwo widzimy, że „m_name” jest członkiem, a „name” nie. Pomaga to wyjaśnić, że ta funkcja zmienia stan klasy. Jest to ważne, ponieważ gdy zmieniamy wartość elementu danych, pozostaje ona poza zakresem funkcji składowej (podczas gdy zmiany parametrów funkcji lub zmiennych lokalnych zazwyczaj tego nie robią).

Z tego samego powodu zalecamy używanie przedrostków „s_” dla lokalnych zmiennych statycznych i przedrostków „g_” dla zmiennych globalnych.

Po drugie, przedrostek „m_” pomaga zapobiegać kolizjom nazewnictwa pomiędzy prywatnymi zmiennymi składowymi a nazwy zmiennych lokalnych, parametrów funkcji i funkcji składowych.

Gdybyśmy nazwali naszego prywatnego członka name zamiast m_name, wówczas:

  • Nasz name parametr funkcji przesłaniłby name prywatny element danych.
  • Gdybyśmy mieli funkcję składową o nazwie name, otrzymalibyśmy błąd kompilacji z powodu redefinicji identyfikator name.

Najlepsza praktyka

Rozważ nadanie nazw swoim prywatnym członkom danych zaczynając od przedrostka „m_”, aby pomóc w odróżnieniu ich od nazw zmiennych lokalnych, parametrów funkcji i funkcji składowych.

Publiczni członkowie klas mogą również stosować tę konwencję, jeśli jest to pożądane. Jednakże publiczni członkowie struktur zazwyczaj nie używają tego przedrostka, ponieważ struktury zazwyczaj nie mają wielu funkcji składowych (jeśli takie istnieją).

Ustawianie poziomów dostępu za pomocą specyfikatorów dostępu

Domyślnie członkowie struktur (i związki) są publiczne, a członkowie klas są prywatne.

Możemy jednak jawnie ustawić poziom dostępu naszych członków za pomocą specyfikatora dostępu. Specyfikator dostępu ustawia poziom dostępu wszystkich elementów , które następują po specyfikatorze. C++ udostępnia trzy specyfikatory dostępu: public:, private:, I protected:.

W poniższym przykładzie używamy zarówno specyfikatora dostępu public: , aby upewnić się, że print() funkcja składowa może być używana przez społeczeństwo, jak i specyfikatora dostępu private: , aby nasze elementy członkowskie danych były prywatne.

class Date
{
// Any members defined here would default to private

public: // here's our public access specifier

    void print() const // public due to above public: specifier
    {
        // members can access other private members
        std::cout << m_year << '/' << m_month << '/' << m_day;
    }

private: // here's our private access specifier 

    int m_year { 2020 };  // private due to above private: specifier
    int m_month { 14 }; // private due to above private: specifier
    int m_day { 10 };   // private due to above private: specifier
};

int main()
{
    Date d{};
    d.print();  // okay, main() allowed to access public members

    return 0;
}

Ten przykład się kompiluje. Ponieważ print() jest członkiem publicznym ze względu na public: specyfikator dostępu, main() (który jest częścią publiczną) może uzyskać do niego dostęp.

Ponieważ mamy członków prywatnych, nie możemy zagregować inicjalizacji d. W tym przykładzie zamiast tego używamy domyślnej inicjalizacji elementu członkowskiego (jako tymczasowe obejście).

Ponieważ klasy domyślnie mają dostęp prywatny, możesz pominąć wiodący private: specyfikator dostępu:

class Foo
{
// private access specifier not required here since classes default to private members
    int m_something {};  // private by default
};

Jednakże, ponieważ klasy i struktury mają różne domyślne poziomy dostępu, wielu programistów woli używać jawnych informacji:

class Foo
{
private: // redundant, but makes it clear that what follows is private
    int m_something {};  // private by default
};

Chociaż jest to technicznie zbędne, użyj jawny private: specyfikator wyjaśnia, że następujące elementy są prywatne, bez konieczności wnioskowania, jaki jest domyślny poziom dostępu na podstawie tego, czy Foo zdefiniowano jako klasę, czy strukturę.

Podsumowanie poziomu dostępu

Oto krótka tabela podsumowująca różne poziomy dostępu:

Poziom dostępuDostęp specyfikatorDostęp członkówDostęp do klasy pochodnejPubliczny dostęp
Publicznypubliczny:taktaktak
Chronionychroniony:taktaknie
Prywatnyprywatny:taknienie

Typ klasy jest dozwolony używaj dowolnej liczby specyfikatorów dostępu w dowolnej kolejności i można ich używać wielokrotnie (np. możesz mieć kilku publicznych członków, potem kilku prywatnych, a potem bardziej publicznych).

Większość klas używa zarówno prywatnych, jak i publicznych specyfikatorów dostępu dla różnych członków. Zobaczymy przykład tego w następnej sekcji.

Dobre praktyki dotyczące poziomu dostępu dla struktur i klas

Teraz, gdy omówiliśmy, czym są poziomy dostępu, porozmawiajmy o tym, jak powinniśmy z nich korzystać.

Struktury powinny całkowicie unikać specyfikatorów dostępu, co oznacza, że ​​wszystkie elementy członkowskie struktury będą domyślnie publiczne. Chcemy, aby nasze struktury były agregatami, a agregaty mogą mieć tylko publiczne elementy członkowskie. Użycie public: specyfikatora dostępu byłoby zbędne w przypadku ustawienia domyślnego, a użycie private: lub protected: spowodowałoby, że struktura nie byłaby agregowana.

Klasy powinny ogólnie mieć tylko prywatne (lub chronione) elementy danych (albo przy użyciu domyślnego poziomu dostępu prywatnego, albo specyfikatora dostępu private: (lub protected:). Powody tego omówimy na następnej lekcji 14.6 — Funkcje dostępu.

Klasy zwykle mają publiczne funkcje składowe (więc te funkcje składowe mogą być używane publicznie po utworzeniu obiektu). Czasami jednak funkcje składowe stają się prywatne (lub chronione), jeśli nie są przeznaczone do użytku publicznego.

Najlepsza praktyka

Klasy powinny ogólnie ustawiać zmienne składowe jako prywatne (lub chronione), a funkcje składowe jako publiczne.

Struktury powinny generalnie unikać używania specyfikatorów dostępu (domyślnie wszystkie elementy członkowskie będą miały wartość publiczną).

Poziomy dostępu działają dla poszczególnych klas

Jeden niuans dostępu w C++ poziomach, które często jest pomijane lub źle rozumiane, jest to, że dostęp do elementów jest definiowany dla poszczególnych klas, a nie dla poszczególnych obiektów.

Wiesz już, że funkcja członkowska może bezpośrednio uzyskiwać dostęp do prywatnych elementów członkowskich (obiektu ukrytego). Ponieważ jednak poziomy dostępu są przypisane do klasy, a nie do obiektu, funkcja członkowska może również uzyskać bezpośredni dostęp do prywatnych elementów DOWOLNEGO innego obiektu tego samego typu klasy, który znajduje się w zakresie.

Zilustrujmy to przykładem:

#include <iostream>
#include <string>
#include <string_view>

class Person
{
private:
    std::string m_name{};

public:
    void kisses(const Person& p) const
    {
        std::cout << m_name << " kisses " << p.m_name << '\n';
    }

    void setName(std::string_view name)
    {
        m_name = name;
    }
};

int main()
{
    Person joe;
    joe.setName("Joe");
    
    Person kate;
    kate.setName("Kate");

    joe.kisses(kate);

    return 0;
}

Wypisuje:

Joe kisses Kate

Jest kilka rzeczy, na które warto zwrócić uwagę.

Najpierw m_name została ustawiona jako prywatna, więc dostęp do niej mogą uzyskać tylko członkowie klasy Person (nie public).

Po drugie, ponieważ nasza klasa ma prywatnych członków, nie jest ona agregatem i nie możemy używać inicjalizacji agregowanej do inicjowania naszych obiektów Person. W ramach obejścia (do czasu znalezienia odpowiedniego rozwiązania tego problemu) stworzyliśmy publiczną funkcję składową o nazwie setName() która pozwala nam przypisać nazwę naszym obiektom Person.

Po trzecie, ponieważ kisses() jest funkcją składową, ma bezpośredni dostęp do składowej prywatnej m_name. Jednak możesz być zaskoczony, widząc, że ma także bezpośredni dostęp do p.m_name! Działa to, ponieważ p jest Person obiektu i kisses() może uzyskać dostęp do prywatnych elementów dowolnego Person obiektu w zakresie!

Dodatkowe przykłady wykorzystania tego zobaczymy w rozdziale o przeciążaniu operatorów.

Techniczna i praktyczna różnica między strukturami i klasami

Teraz, gdy omówiliśmy poziomy dostępu, możemy w końcu omówić techniczne różnice między strukturami i klasami. Gotowy?

Klasa domyślnie ustawia swoich członków na prywatne, podczas gdy struktura domyślnie ustawia swoich członków na publiczne.

Tak, to wszystko.

Nota autora

Aby być pedantycznym, jest jeszcze jedna drobna różnica — struktury dziedziczą z innych typów klas publicznie, a klasy prywatnie. Omówimy, co to oznacza w rozdziale o dziedziczeniu, ale ten konkretny punkt jest praktycznie nieistotny, ponieważ i tak nie powinieneś polegać na wartościach domyślnych przy dziedziczeniu.

W praktyce używamy struktur i klas w różny sposób.

Ogólnie rzecz biorąc, używaj struktury, gdy spełnione są wszystkie poniższe warunki:

  • Masz prosty zbiór danych, który nie wymaga ograniczania dostęp.
  • Wystarczająca jest inicjalizacja agregowana.
  • Nie masz niezmienników klas, potrzeb konfiguracyjnych ani czyszczenia.

Kilka przykładów, gdzie można zastosować struktury: globalne dane programu constexpr, struktura punktowa (prosty zbiór elementów int, których nie można ustawić jako prywatnych), struktury używane do zwracania zestawu danych z funkcja.

Użyj klasy w inny sposób.

Chcemy, aby nasze struktury były agregatami. Jeśli więc używasz jakichkolwiek możliwości, które sprawiają, że Twoja struktura nie jest zagregowana, prawdopodobnie powinieneś zamiast tego używać klasy (i postępować zgodnie ze wszystkimi najlepszymi praktykami dotyczącymi klas).

Czas quizu

Pytanie nr 1

a) Co to jest element publiczny?

Pokaż rozwiązanie

b) Co to jest element prywatny Member?

Pokaż rozwiązanie

c) Co to jest specyfikator dostępu?

Pokaż rozwiązanie

d) Ile jest specyfikatorów dostępu i jakie one są?

Pokaż rozwiązanie

Pytanie nr 2

a) Napisz klasę o nazwie Point3d. Klasa powinna zawierać:

  • Trzy prywatne zmienne członkowskie typu int nazwany m_x, m_y, I m_z;
  • Publiczna funkcja składowa o nazwie setValues() która umożliwia ustawienie wartości dla m_x, m_y, I m_z.
  • Publiczna funkcja składowa o nazwie print() która wypisuje punkt w następującym formacie: <m_x, m_y, m_z>

Upewnij się, że wykonuje się następujący program poprawnie:

int main()
{
    Point3d point;
    point.setValues(1, 2, 3);

    point.print();
    std::cout << '\n';

    return 0;
}

To powinno zostać wydrukowane:

<1, 2, 3>

Pokaż rozwiązanie

b) Dodaj funkcję o nazwie isEqual() do swojej klasy Point3d. Poniższy kod powinien działać poprawnie:

int main()
{
	Point3d point1{};
	point1.setValues(1, 2, 3);

	Point3d point2{};
	point2.setValues(1, 2, 3);

	std::cout << "point 1 and point 2 are" << (point1.isEqual(point2) ? "" : " not") << " equal\n";

	Point3d point3{};
	point3.setValues(3, 4, 5);

	std::cout << "point 1 and point 3 are" << (point1.isEqual(point3) ? "" : " not") << " equal\n";

	return 0;
}

To powinno zostać wydrukowane:

point 1 and point 2 are equal
point 1 and point 3 are not equal

Pokaż rozwiązanie

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:  
514 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze