Kontrola dostępu
Składowa klasy może być prywatna (private), chroniona (protected) lub publiczna (public):
Jeśli składowa jest prywatna, jej nazwy mogą używać tylko funkcje składowe i zaprzyjaźnione klasy, w której jest zadeklarowana.
Jeśli składowa jest chroniona, jej nazwy mogą używać tylko funkcje składowe i zaprzyjaźnione klasy, w której jest zadeklarowana, oraz funkcje składowe i zaprzyjaźnione klas pochodnych tej klasy. Jeśli składowa jest publiczna, może być używana przez wszystkie funkcje.
Powyższe zasady odzwierciedlają zasadę, że są trzy rodzaje funkcji dostępu do klas: funkcje implementacji klasy (składowe i zaprzyjaźnione), funkcje implementacji klas pochodnych (składowe i zaprzyjaźnione klas pochodnych) oraz inne funkcje. Można to przedstawić graficznie:
Znaczenie specyfikatorów dostępu jest dla wszystkich nazw takie samo, tzn. to, do czego odnosi się nazwa, nie jest ważne pod tym względem. Oznacza to, że można tworzyć prywatne funkcje, typy, stałe itd. oraz prywatne zmienne składowe. Na przykład wydajna i nieintruzyjna klasa listy często musi zawierać struktury danych do zapanowania nad elementami. Lista jest nieintruzyjna, gdy nie wymaga modyfikowania swoich elementów (np. poprzez wymóg, aby typy elementów zawierały pole łącza). Informacje i struktury danych służące do organizacji takiej listy mogą być prywatne:
class List {
public:
void insert(T);
T get();
//...
private:
struct Link { T val; Link* next; };
struct Chunk {
enum { chunk_size=15};
Link v[chunk_size];
Chunk* next;
};
Chunk* allocated;
Link* free;
Link* get_free();
Link* head;
};
Definicje funkcji publicznych są proste:
void List::insert(T val)
{
Link* lnk = get_free();
lnk−>val = val;
lnk−>next = head;
head = lnk;
}
template
T List::get()
{
if (head == nullptr)
throw Underflow{}; // Underflow to klasa wyjątków
Link* p= head;
head = p−>next;
p−>next = free;
free = p;
return p−>val;
}
Jak zwykle definicja funkcji pomocniczych (tutaj prywatnych) jest trochę trudniejsza:
typename List::Link* List::get_free()
{
if (free == nullptr) {
//... alokacja nowego fragmentu pamięci i umieszczenie łączy w liście free...
}
Link* p = free;
free = free−>next;
return p;
}
Do zakresu List wchodzimy poprzez napisanie List:: w definicji funkcji składowej. Jednak jako że typ zwrotny funkcji get_free() jest określony przed miejscem pojawienia się nazwy List::get_free(), musi zostać użyta pełna nazwa List::Link zamiast skrótu Link. Alternatywą jest użycie notacji przyrostkowej dla typów zwrotnych:
auto List::get_free() −> Link*
{
//...
}
Funkcje nie będące składowymi (z wyjątkiem zaprzyjaźnionych) nie mają takiego dostępu:
void would_be_meddler(List* p)
{
List::Link* q=0; // błąd: List::Link jest prywatna
//...
q = p−>free; // błąd: List::free jest prywatna
//...
if (List::Chunk::chunk_size > 31) { // błąd: List::Chunk::chunk_size jest prywatna
//...
}
}
Składowe klasy są domyślnie prywatne. Natomiast składowe struktury są domyślnie publiczne. Oczywistą alternatywą dla używania typu składowego jest umieszczenie typu w otaczającej przestrzeni nazw. Na przykład:
struct Link2 {
T val;
Link2* next;
};
template
class List {
private:
Link2* free;
//...
};
Struktura Link jest parametryzowana niejawnie parametrem T z List. W przypadku struktury Link2 musieliśmy to zrobić jawnie.
Jeśli typ składowy nie zależy od wszystkich parametrów klasy szablonowej, lepsza może być wersja nieskładowa. Jeśli zagnieżdżanie jest niepożądane, ale zagnieżdżona klasa nie jest sama w sobie specjalnie przydatna, dobrym pomysłem może być zadeklarowanie jej jako zaprzyjaźnionej z tą, która miała ją zawierać:
template
class Link3 {
friend class List; // tylko List ma dostęp do Link3
T val;
Link3* next;
};
template
class List {
private:
Link3* free;
//...
};
Kompilator może zmienić kolejność części klasy oznaczonych specyfikatorami dostępu. Na przykład:
public:
int m1;
public:
int m2;
};
Kompilator może wstawić m2 przed m1 w układzie obiektu klasy S. To może zaskoczyć programistę i jest całkowicie zależne od implementacji, dlatego wielu specyfikatorów dostępu do składowych należy używać tylko wtedy, gdy ma się ku temu dobry powód.
Składowe chronione
Przy projektowaniu hierarchii klas czasami tworzy się funkcje przeznaczone do użytku przez implementatorów klas pochodnych, nie zaś zwykłych użytkowników. Przykładowo możemy napisać (wydajną) funkcję niekontrolowanego dostępu dla implementatorów klas pochodnych i (bezpieczną) kontrolowaną funkcję dostępową dla pozostałych użytkowników. Pomysł ten można zrealizować, deklarując niekontrolowaną wersję jako chronioną (protected). Na przykład:
public:
char& operator[](int i); // dostęp kontrolowany
//...
protected:
char& access(int i); // dostęp niekontrolowany
//...
};
class Circular_buffer : public Buffer {
public:
void reallocate(char* p, int s); // zmienia lokalizację i rozmiar
//...
};
void Circular_buffer::reallocate(char* p, int s)// zmienia lokalizację i rozmiar
{
//...
for (int i=0; i!=old_sz; ++i)
p[i] = access(i); // bez niepotrzebnego sprawdzania
//...
}
void f(Buffer& b)
{
b[3] = 'b'; // OK (kontrolowane)
b.access(3) = 'c'; // błąd: funkcja Buffer::access() jest chroniona
}
Klasa pochodna ma dostęp do składowych chronionych klasy bazowej tylko dla obiektów swojego typu:
protected:
char a[128];
//...
};
class Linked_buffer : public Buffer {
//...
};
class Circular_buffer : public Buffer {
//...
void f(Linked_buffer* p)
{
a[0] = 0; // OK: dostęp do składowej chronionej klasy Circular_buffer
p−>a[0] = 0; // błąd: dostęp do chronionej składowej innego typu
}
};
To chroni nas przed subtelnymi błędami, które mogą powstawać, gdy jedna klasa pochodna uszkodzi dane należące do innej klasy pochodnej.
Używanie składowych chronionych
Prosty model prywatnych i publicznych zmiennych składowych wspomaga techniki tworzenia typów konkretnych. Ale gdy tworzone są klasy pochodne, wyróżniamy dwa rodzaje użytkowników klas: właśnie klasy pochodne i „ogólne społeczeństwo”. Składowe i funkcje zaprzyjaźnione stanowiące implementację klasy operują na jej obiektach w imieniu tych użytkowników. Model prywatnych i publicznych składowych umożliwia rozróżnienie implementatorów i ogólnych użytkowników, ale nie pomaga w korzystaniu z klas pochodnych. Składowe chronione są znacznie bardziej narażone na nadużycia od składowych prywatnych. Zwłaszcza błędem projektowym jest deklaracja jako chronionych danych składowych. Umieszczenie większej ilości danych we wspólnej klasie, aby wszystkie klasy pochodne miały do nich dostęp, jest proszeniem o ich zniszczenie. Co gorsza, danych chronionych podobnie
jak publicznych nie można łatwo restrukturyzować, bo nie da się znaleźć wszystkich miejsc, w których są używane. W ten sposób dane chronione stają się ciężarem.Na szczęście nie trzeba używać danych chronionych. Domyślnie w klasach składowe są
prywatne i w większości przypadków nie trzeba tego zmieniać. Z doświadczenia wiem, że zawsze jest inne rozwiązanie niż umieszczenie dużej ilości informacji we wspólnej klasie bazowej, aby mogły z nich korzystać klasy pochodne.
Ale powyższe rozważania nie dotyczą funkcji chronionych, które tworzy się w celu definiowania operacji do użytku w klasach pochodnych. Przykładem takiej funkcji jest Ival_slider. Gdyby w tamtym przykładzie klasa implementacyjna była prywatna, dalsza
derywacja byłaby niemożliwa. Z drugiej strony, publiczne udostępnianie szczegółów implementacyjnych w klasie bazowej to zaproszenie dla błędów i pomyłek.
Dostęp do klas bazowych
Klasę bazową, podobnie jak składową, można zadeklarować jako prywatną (private), chronioną (protected) lub publiczną (public). Na przykład:
class Y : protected B { /*...*/};
class Z : private B { /*...*/};
Każdy specyfikator dostępu służy w projekcie klasy do czegoś innego:
- Derywacja publiczna sprawia, że klasa pochodna staje się podtypem swojej klasy bazowej. Na przykład X jest rodzajem B. Jest to najczęściej spotykana forma derywacji.
- Bazy prywatne są najczęściej używane do definiowania klas poprzez ograniczenie interfejsu do klasy bazowej w celu zapewnienia silniejszej gwarancji. Na przykład B jest szczegółem implementacyjnym Z. Dobrym przykładem jest szablon wektora wskaźników wzbogacający swoją bazę Vector o sprawdzanie typów.
- Bazy chronione są przydatne w hierarchiach klas, w których dalsza derywacja jest oczywista. Derywacja chroniona, podobnie jak prywatna, jest wykorzystywana do reprezentowania szczegółów implementacyjnych. Dobrym przykładem jest Ival_slider.
Specyfikator dostępu klasy bazowej można opuścić i wówczas domyślnie stosowany jest specyfikator private (w przypadku klas) lub public (w przypadku struktur). Na przykład:
struct YY : B { /*...*/}; // B jest bazą publiczną
Wielu programistom wydaje się, że baza powinna być publiczna (aby wyrażać relację podtypu) i brak specyfikatora dostępu dla bazy może ich zaskoczyć w przypadku klas, ale raczej nie, jeśli chodzi o struktury.
Specyfikator dostępu klasy bazowej kontroluje dostęp do jej składowych oraz konwersję wskaźników i referencji z typu klasy pochodnej na typ klasy bazowej. Przyjmijmy na przykład, że klasa D jest pochodną klasy B:
- Jeżeli B jest bazą prywatną, jej publiczne i chronione składowe mogą być używane tylko przez funkcje składowe i zaprzyjaźnione klasy D. Tylko funkcje zaprzyjaźnione i składowe D mogą konwertować D* na B*.
- Jeżeli B jest bazą chronioną, jej publiczne i chronione składowe mogą być używane tylko przez funkcje składowe i zaprzyjaźnione klasy D oraz przez funkcje składowe i zaprzyjaźnione klas pochodnych klasy D. Tylko funkcje zaprzyjaźnione i składowe klasy D oraz przyjaciele i składowe klas pochodnych klasy D mogą konwertować D* na B*.
- Jeżeli B jest bazą publiczną, jej publicznych składowych mogą używać wszystkie funkcje. Ponadto jej składowych chronionych mogą używać składowe i przyjaciele klasy D oraz składowe i przyjaciele klas pochodnych klasy D. Każda funkcja może dokonać konwersji D* na B*.
Jest to zasadniczo powtórzenie zasad dotyczących dostępu do składowych. Projektując klasę, poziom dostępu do bazy wybiera się tak samo jak dla składowych. Zobacz na przykład Ival_slider.
Wielodziedziczenie a kontrola dostępu
Jeśli do nazwy klasy bazowej można dotrzeć różnymi ścieżkami w strukturze wielodziedziczenia, to nazwa ta jest dostępna, gdy można uzyskać do niej dostęp poprzez którąkolwiek z tych ścieżek. Na przykład:
int m;
static int sm;
//...
};
class D1 : public virtual B { /*...*/};
class D2 : public virtual B { /*...*/};
class D12 : public D1, private D2 { /*...*/};
D12* pd = new D12;
B* pb = pd; // OK: dostępna przez D1
int i1 = pd−>m; // OK: dostępna przez D1
Nawet jeśli do obiektu można dotrzeć wieloma ścieżkami, to i tak da się do niego odnosić w sposób jednoznaczny. Na przykład:
class X2 : public B { /*...*/};
class XX : public X1, public X2 { /*...*/};
XX* pxx = new XX;
int i1 = pxx−>m; // błąd niejednoznaczności: XX::X1::B::m czy XX::X2::B::m?
int i2 = pxx−>sm; // OK: jest tylko jedna składowa B::sm w obiekcie klasy XX (sm jest składową statyczną)
Deklaracje using i kontrola dostępu
Deklaracji using nie można wykorzystywać w celu zdobycia dostępu do dodatkowych informacji. Jest to tylko technika umożliwiająca uzyskiwanie dostępu do już dostępnych informacji w wygodniejszy sposób. Z drugiej strony, gdy jest dostęp, można go przyznać innym użytkownikom. Na przykład:
private:
int a;
protected:
int b;
public:
int c;
};
class D : public B {
public:
using B::a; // błąd: składowa B::a jest prywatna
using B::b; // sprawia, że składowa B::b będzie publicznie dostępna przez D
};
Używając deklaracji using w połączeniu z derywacją prywatną lub chronioną, można określać interfejsy do niektórych, ale nie wszystkich elementów zawartości klasy. Na przykład:
public:
using B::b;
using B::c;
};
Język C++. Kompendium wiedzy. Wydanie IV, Autor: Bjarne Stroustrup, Wydawnictwo: Helion