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ż:
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);
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
.
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;
Plik z chomika:
rc51
Inne pliki z tego folderu:
1_1.pdf
(430 KB)
1_4.pdf
(685 KB)
1_2.pdf
(475 KB)
1_3.pdf
(470 KB)
1_5.pdf
(816 KB)
Inne foldery tego chomika:
- NOWE, 2015-01
- NOWE, 2015-02
- NOWE, 2015-03
- NOWE, 2015-04
- NOWE, 2015-05
Zgłoś jeśli
naruszono regulamin