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 doyear,month, Idayelementy ukrytego obiektu. - W
main(), mamy bezpośredni dostęptoday.dayaby 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 dom_year,m_month, Im_dayelementy ukrytego obiektu. - W
main(), mamy bezpośredni dostęptoday.m_dayaby 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
nameparametr funkcji przesłaniłbynameprywatny element danych. - Gdybyśmy mieli funkcję składową o nazwie
name, otrzymalibyśmy błąd kompilacji z powodu redefinicji identyfikatorname.
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ępu | Dostęp specyfikator | Dostęp członków | Dostęp do klasy pochodnej | Publiczny dostęp |
|---|---|---|---|---|
| Publiczny | publiczny: | tak | tak | tak |
| Chroniony | chroniony: | tak | tak | nie |
| Prywatny | prywatny: | tak | nie | nie |
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?
b) Co to jest element prywatny Member?
c) Co to jest specyfikator dostępu?
d) Ile jest specyfikatorów dostępu i jakie one są?
Pytanie nr 2
a) Napisz klasę o nazwie Point3d. Klasa powinna zawierać:
- Trzy prywatne zmienne członkowskie typu
intnazwanym_x,m_y, Im_z; - Publiczna funkcja składowa o nazwie
setValues()która umożliwia ustawienie wartości dlam_x,m_y, Im_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>
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

