2_2.pdf

(905 KB) Pobierz
Od zera do gier kodera
2
ZAAWANSOWANA
OBIEKTOWOŚĆ
Nuda jest wrogiem programistów.
Bjarne Stroustrup
C++ jest zasłużonym członkiem licznej obecnie rodziny języków obiektowych. Oferuje on
wszystkie koniecznie mechanizmy, służące praktycznej realizacji idei programowania
zorientowanego obiektowo. Poznaliśmy je w dwóch rozdziałach poprzedniej części kursu.
Między C++ a innymi językami OOP występują jednak pewne różnice. Nasz język ma
wiele specyficznych dla siebie możliwości, które mają za zadanie ułatwienie życia
programiście. Często też przyczyniają się do powstania obiektywnie lepszych programów.
W tym rozdziale poznamy tę właśnie stronę OOPu w C++. Przedstawione tu zagadnienia,
choć w zasadzie niezbędne do wystarczającej znajomości języka, są w dużej części
przydatnymi udogonieniami. Nie niezbędnymi, lecz wielce interesującymi i praktycznymi.
Poznanie ich sprawi, że nasze obiektowe programy będą wygodne w konstruowaniu i
późniejszej modyfikacji. Programowanie stanie się po prostu łatwiejsze i przyjemniejsze -
a to chyba będzie bardzo znaczącym osiągnięciem.
Zobaczmy więc, jakie wyjątkowe konstrukcje OOP oferuje nam C++.
O przyjaźni
W czasie pierwszych spotkań z programowaniem obiektowym wspominałem dość często
o jego zaletach, wymieniając wśród nich podział kodu na drobne i łatwe to zarządzania
kawałki. Tymi fragmentami (także pod względem koncepcyjnym) są oczywiście klasy.
Plusem, jaki niesie za soba stosowanie klas, jest wyodrębnienie kodu i danych w obiekty
zajmujące się konkretnymi zadaniami i reprezentującymi konkretne obiekty. Instancje
klas współpracują ze sobą i dzięki temu wypełniają zadania aplikacji. Tak to wygląda -
przynajmniej w teorii :)
Atutem klas jest niezależność, zwana fachowo hermetyzacją lub enkapsulacją. Objawia
się ona tym, iż dana klasa posiada pewien zestaw pól i metod, z którym tylko wybrane są
dostępne dla świata zewnętrznego. Jej wewnętrzne sprawy są całkowicie chronione; służą
ku temu specyfikatory dostępu, jak private i protected .
Opatrzone nimi składowe są w zasadzie całkiem odseparowane od świata zewnętrznego,
bo ten jest dla nich potencjalnie groźny. Upubliczniając swoje pole klasa narażałaby
przecież swoje dane na przypadkowe lub celowe, ale zawsze niepożądane modyfikacje.
To tak jakby wyjść z domu i zostawić drzwi niezamknięte na klucz: nie jest to wpradzie
bezpośrednie zaproszenie dla złodzieja, ale taka okazja może go uczynić - w myśl
znanego powiedzenia.
Ale przecież nie wszyscy są źli - każdy ma przynajmniej kilku przyjaciół . Przyjaciel jest
to osoba, na którą można liczyć; o której wiemy, że nie zrobi nam nic złego. Większość
ludzi uważa, że przyjaźń jest w życiu bardzo ważna - i nie muszą nas do tego
374
Zaawansowane C++
przekonywać żadni socjologowie. Wszyscy wiemy to dobrze z własnego, życiowego
doświadczenia.
No dobrze, ale co to ma wspólnego z programowaniem?… Otóż bardzo wiele, zwłaszcza z
programowaniem obiektowym. Mianowicie, klasa także może mieć przyjaciół : mogą
być nimi globalne funkcje, metody innych klas, a także inne klasy w całości. Cóż to
jednak znaczy, że klasa ma jakiegoś przyjaciela?… Wyjaśnijmy więc, że:
Przyjaciel (ang. friend ) danej klasy ma dostęp do jej wszystkich składników - także
tych chronionych , a nawet prywatnych .
Jeżeli zatem klasa posiada przyjaciela, to oznacza to, że dała mu „klucze” (dostęp) do
swojego „mieszkania” (niepublicznych składowych). Przyjaciel klasy ma do nich prawie
takie samo prawo, jak metody tejże klasy. Pewne drobne różnice wyjaśnimy sobie przy
okazji osobnego omówienia zaprzyjaźnionych funkcji i klas.
Dowiedzmy się teraz, jak zaprzyjaźnić z klasą jakiś inny element programu. Jest
oczywiście i jak zwykle bardzo proste ;) Należy bowiem umieścić w definicji klasy tzw.
deklarację przyjaźni (ang. friend declaration ):
friend deklaracja_przyjaciela ;
Słowem kluczowym friend poprzedzamy w niej deklarację_przyjaciela . Tą deklaracją
może być:
¾ prototyp funkcji globalnej
¾ prototyp metody ze zdefiniowanej wcześniej klasy
¾ nazwa zadeklarowanej wcześniej klasy
Oto najprostszy i niezbyt mądry przykład:
class CFoo
{
private :
std::stringm_strBardzoOsobistyNapis;
public :
//konstruktor
CFoo() { m_strBardzoOsobistyNapis = "Kocham C++!" ; }
// deklaracja przyjaźni z funkcją
friend void Wypisz(CFoo*);
};
// zaprzyjaźniona funkcja
void Wypisz(CFoo* pFoo)
{
std::cout << pFoo->m_strBardzoOsobistyNapis;
}
Zaprzyjaźniony byt - w tym przypadku funkcja - ma tu pełen dostęp do prywatnego pola
klasy CFoo . Może więc wypisać jego zawartość dla każdego obiektu tej klasy, jaki
zostanie mu podany.
Deklaracja przyjaźni w tym przykładzie wydaje się być umieszczona w sekcji public
klasy CFoo . Tak jednak nie jest, gdyż:
2972880.002.png
Zaawansowana obiektowość
375
Deklaracja przyjaźni może być umieszczona w każdym miejscu definicji klasy i
zawsze ma to samo znaczenie .
Jest więc obojętne, gdzie się ona pojawi. Zwykle piszemy ją albo na początku, albo na
końcu klasy, wyróżniając na przykład zmniejszonym wcięciem. Pokazujemy w ten
sposób, że nie podlega ona specyfikatorom dostępu.
Nie ma więc czegoś takiego jak „publiczna deklaracja przyjaźni” lub „prywatna deklaracja
przyjaźni”. Przyjaciel pozostaje przyjacielem niezależnie od tego, czy się nim chwalimy,
czy nie.
Skoro teraz wiemy już z grubsza, czym są przyjaciele klas, omówimy sobie osobno
zaprzyjaźnianie funkcji globalnych oraz innych klas i ich metod.
Funkcje zaprzyjaźnione
Najpierw zobaczymy, jak zaprzyjaźnić klasę z funkcją - tak, aby funkcja miała dostęp do
niepublicznych składników z danej klasy.
Deklaracja przyjaźni z funkcją
Chcąc uczynić jakąś funkcję przyjacielem klasy, musimy w definicji klasy podać
deklarację zaprzyjaźnionej funkcji, poprzedzając ją słowem kluczowym friend .
Ilustracją tego faktu nie będzie poniższy przykład. Mamy w nim klasę opisującą okrąg -
CCircle . Zaprzyjaźniona z nią funkcja PrzecinajaSie() sprawdza, czy podane jej dwa
okręg mają punkty wspólne:
#include <cmath>
class CCircle
{
private :
//środek okręgu
struct { float x, y; } m_ptSrodek;
// jego promień
float m_fPromien;
public :
//konstruktor
CCircle( float fPromien, float fX = 0.0f , float fY = 0.0f )
{ m_fPromien = fPromien;
m_ptSrodek.x = fX;
m_ptSrodek.y = fY; }
// deklaracja przyjaźni z funkcją
friend bool PrzecinajaSie(CCircle&, CCircle&);
};
// zaprzyjaźniona funkcja
bool PrzecinajaSie(CCircle& Okrag1, CCircle& Okrag2)
{
// obliczamy odległość między środkami
float fRoznicaX = Okrag2.m_ptSrodek.x - Okrag1.m_ptSrodek.x;
float fRoznicaY = Okrag2.m_ptSrodek.y - Okrag1.m_ptSrodek.y;
float fOdleglosc = sqrt(fRoznicaX*fRoznicaX + fRoznicaY*fRoznicaY);
2972880.003.png
376
Zaawansowane C++
//odległość ta musi być mniejsza od sumy promieni, ale większa
// od ich bezwzględnej różnicy
return (fOdleglosc < Okrag1.m_fPromien + Okrag2.m_fPromien
&& fOdleglosc > abs(Okrag1.m_fPromien - Okrag2.m_fPromien);
}
Bardzo dobrze widać tu ideę przyjaźni: funkcja PrzecinajaSie() ma dostęp do
składowych m_ptSrodek oraz m_fPromien z obiektów klasy CCircle - mimo że są
prywatne pola klasy. CCircle deklaruje jednak przyjaźń z funkcją PrzecinajaSie() , a
zatem udostępnia jej swoje osobiste dane.
Zauważmy jeszcze, że w deklaracji przyjaźni podajemy cały prototyp funkcji, a nie tylko
jej nazwę. Możliwe jest bowiem zdefiniowanie kilku funkcji o tej nazwie, np. tak:
bool PrzecinajaSie(CCircle&, CCircle&);
bool PrzecinajaSie(CRectangle&, CRectangle&);
bool PrzecinajaSie(CPolygon&, CPolygon&);
// itd. (wraz z ewentualnymi kombinacjami krzyżowymi)
Klasa będzie jednak przyjaźniła się tylko z tą funkcją, której deklarację zamieścimy po
słowie friend . Zapamiętajmy po prostu, że:
Jedna zwykła deklaracja przyjaźni oznacza przyjaźń z jedną funkcją .
Na co jeszcze trzeba zwrócić uwagę
Wszystko wydawałoby się raczej proste. Nie zaszkodzi jednak powiedzieć wprost o
pewnych „oczywistych” faktach związanych z zaprzyjaźnionymi funkcjami.
Funkcja zaprzyjaźniona nie jest metodą
Jedno słówko friend może bardzo wiele zmienić. Porównajmy choćby te dwie klasy:
class CFoo
{
public :
void Funkcja();
};
class CBar
{
public :
friend void Funkcja();
};
Różnią się one tylko tym słówkiem… ale jest to różnica znacząca. W pierwszej klasie
Funkcja() jest jej metodą: zadeklarowaliśmy ją tak, jak wszystkie normalne metody
klas. Znamy to już dobrze, gdyż proces definiowania metod poznaliśmy przy pierwszym
spotkanie z OOPu. Do pełni szczęścią na leży jeszcze tylko zdefiniować ciało emtody
CFoo::Funkcja() i wszystko będzie w porządku.
Deklaracja w drugiej klasie jest natomiast opatrzona słówkiem friend , które zupełnie
zmienia jej znaczenie. Funkcja() nie jest tu metodą klasy CBar . Jest wprawdzie
zaprzyjaźniona z nią, ale nie jest jej składnikiem: nie ma dostępu do wskaźnika this .
Aby z tej zaprzyjaźnionej funkcji mógł być w ogóle jakiś użytek, trzeba jej zapewnić
dostęp do obiektu klasy CBar , bo jej samej nikt go „nie da”. Wobec braku parametrów
funkcji pewnie będzie to wymagało zadeklarowania globalnej zmiennej obiektowej typu
CBar .
2972880.004.png
Zaawansowana obiektowość
377
Pamiętaj zatem, iż:
Funkcje zaprzyjaźnione z klasą nie są jej składnikami . Nie posiadają dostępu do
wskaźnika this tej klasy , gdyż nie są jej metodami .
W praktyce więc należy jakoś podać takiej funkcji obiekt klasy, która się z nią przyjaźni.
Zobaczyliśmy w poprzednim przykładzie, że prawie zawsze odbywa się to poprzez
parametry. Referencja do obiektu klasy CCircle była parametrem zaprzyjaźnionej z nią
funkcji PrzecinajaSie() . Tylko posiadając dostęp do obiektu klasy, która się z nią
przyjaźni, funkcja zaprzyjaźniona może odnieść jakąś korzyść ze swojego
uprzywilejowanego statusu.
Deklaracja przyjaźni jest też deklaracją funkcji
Mamy też drugi ważny fakt związany z deklaracją funkcji zaprzyjaźnionej.
Deklaracja przyjaźni jako prototyp funkcji
Otóż, taka deklaracja przyjaźni jest jednocześnie deklaracją funkcji jako takiej. Musimy
zauważyć, że w zaprezentowanych przykładach funkcje, które były przyjacielami klasy,
zostały zdefiniowane dopiero po definicji tejże klasy. Wcześniej kompilator nic o nich nie
wiedział - a mimo to pozwolił na ich zaprzyjaźnienie! Czy to jakaś niedoróbka?
Ależ skąd! Kompilator uznaje po prostu deklarację przyjaźni z funkcją także za deklarację
samej funkcji. Linijka ze słowem friend pełni więc funkcję prototypu funkcji, która może
być swobodnie zdefiniowana w zupełnie innym miejscu. Z kolei wcześniejsze
prototypowanie funkcji, przed deklaracją przyjaźni, nie jest konieczne. Mówiąc po ludzku,
w poniższym kodzie:
bool PrzecinajaSie(CCircle&, CCircle&);
class CCircle
{
// (ciach - szczegóły)
friend bool PrzecinajaSie(CCircle&, CCircle&);
};
// gdzieś dalej definicja funkcji...
początkowy prototyp funkcji PrzecinajaSie() , umieszczony przed definicją CCircle , nie
jest koniecznie wymagany. Bez niego kompilator skorzysta po prostu z deklaracji
przyjaźni jak z normalnej deklaracji funkcji.
Deklaracja przyjaźni z funkcją może być jednocześnie deklaracją samej funkcji .
Wcześniejsza wiedza kompilatora o istnieniu zaprzyjaźnianej funkcji nie jest niezbędna ,
aby funkcja ta mogła zostać zaprzyjaźniona.
Dodajemy definicję
Najbardziej zaskakujące jest jednak to, że deklarując przyjaźń z jakąś funkcją możemy
tę funkcję jednocześnie… zdefiniować! Nic nie stoi na przeszkodzie, aby po zakończeniu
deklaracji nie stawiać średnika, lecz otworzyć nawias klamrowy i wpisać treść funkcji:
class CVector2D
{
private :
float m_fX, m_fY;
2972880.005.png 2972880.001.png
Zgłoś jeśli naruszono regulamin