Skocz do zawartości
  • 👋 Witaj na MPCForum!

    Przeglądasz forum jako gość, co oznacza, że wiele świetnych funkcji jest jeszcze przed Tobą! 😎

    • Pełny dostęp do działów i ukrytych treści
    • Możliwość pisania i odpowiadania w tematach
    • System prywatnych wiadomości
    • Zbieranie reputacji i rozwijanie swojego profilu
    • Członkostwo w jednej z największych społeczności graczy

    👉 Dołączenie zajmie Ci mniej niż minutę – a zyskasz znacznie więcej!

    Zarejestruj się teraz

Rekomendowane odpowiedzi

Opublikowano

Klasy i obiekty

 

 

Czym się różni wróbel?

 

Dotychczas programowaliśmy w stylu strukturalnym [proceduralnym], a więc rozbijaliśmy problem na mniejsze problemy, które łatwiej jest rozwiązać. Program opierał się na funkcjach, które operowały na strukturach danych, jednak dane i operacje na nich wykonywane nie były ze sobą w żaden sposób powiązane. Na przykład nic nie stawało na przeszkodzie, by do funkcji liczącej sumę liczb typu int wrzucić pojemność cysterny i temperaturę powietrza. Ten sposób programowania okazał się mało wygodny i podatny na błędy, dlatego wymyślono inny [moim zdaniem dużo lepszy ;-)] styl programowania, zwany programowaniem obiektowym [ang. OOP - Object Oriented Programming].

 

Programowanie obiektowe jest wygodniejsze i bardziej dla nas naturalne, bo pozwala nam na modelowanie obiektów i ich zachowań znanych nam ze świata rzeczywistego. Jeśli na przykład piszemy edytor tekstu, to chcielibyśmy jakoś zamodelować w programie kartkę papieru. Pisząc grę chcielibyśmy mieć model ekranu, bitmap, obiektów gry itp. W programowaniu obiektowym funkcje [zwane w OOP metodami] zostały powiązane w jedną całość z danymi [zwanymi właściwościami] na których te funkcje operują, dzięki czemu takie "agregaty" łatwiej odpowiadają rzeczywistym obiektom. Trudniej jest się też pomylić i wysłać do funkcji nieodpowiednie parametry.

 

Inną zaletą programowania obiektowego jest hermetyzacja informacji. Oznacza to tyle, że dane, które nie powinny być widoczne na zewnątrz modelowanego obiektu, są w nim ukrywane. Gdy weźmiesz odbiornik radiowy to na zewnątrz jego obudowy wyprowadzone są tylko te pokrętła, które są potrzebne. Reszta jest ukryta wewnątrz żeby nie dało się przypadkowo rozregulować radia kręcąc byle czym. Zresztą gdybyśmy dostali radio bez obudowy, to zajęło by nam dużo czasu zanim połapalibyśmy się, czym pokręcić żeby podgłośnić. Ukrywanie danych w programowaniu obiektowym ułatwia więc korzystanie z obiektów [bo ukrywa ich wewnętrzne "trybiki"] oraz zwiększa bezpieczeństwo [bo nie pozwala na majstrowanie :-)].

 

Ostatnią już chyba zaletą OOP jest polimorfizm. Polega on na tym, że kod jakby sam przystosowuje się do tego, na jakich obiektach przyszło mu pracować. Czyli mówiąc prościej orientuje się według obiektów. Godne uwagi jest szczególnie to, że program praktycznie bez żadnych zmian w kodzie jest już przygotowany do obsługi nawet takich obiektów, których jeszcze nie wymyślono! :-o Jest to chyba najważniejsza zaleta programowania obiektowego. No, dość już tej teorii, przechodzimy do praktyki. Za chwilę dowiesz się, jak można zastosować ten nowy styl programowania do języka C++.

 

 

Jak w tytule

 

Programowanie obiektowe ma ułatwiać modelowanie rzeczywistych obiektów. Zamodelujmy więc sobie jakiś taki obiekt, na przykład.. pudełko zapałek :-). Przeciętne pudełko zapałek zawiera około 40 zapałek. Możemy wyjmować zapałki dotąd, aż nie zostanie ani jedna. Dalsze wyjmowanie nie ma sensu [no bo niby co mamy wyjmować? ;-P]. Nasz program powinien o tym wiedzieć i nie pozwolić na dalsze wyjmowanie. Podobnie z wkładaniem zapałek - zmieści się tylko 40 i ani jednej więcej. W stylu proceduralnym robiło się zmienną globalną przechowującą ilość zapałek i zestaw funkcji do wyjmowania i wkładania. Jednak mimo iż funkcje takie będą "idiotoodporne", to nic nie stoi na przeszkodzie, by "na chama" zmienić tą globalną zmienną na przykład na 1000 i cały program jest do wykopania :-(. Na szczęście istnieje programowanie obiektowe, spróbujmy więc go wykorzystać.

 

W języku C++ są jak wiadomo typy wbudowane, takie jak int, char i pozostałe. Ale wśród nich nie ma czegoś takiego jak pudełko zapałek. Na szczęście język ten pozwala na definiowanie i wykorzystywanie własnych typów danych! 8-) Do tego celu służą klasy. Klasa jest jakby projektem technicznym, który określa z czego obiekt się składa, co możemy z nim robić i jak on na to zareaguje. Jest szablonem, według którego można taśmowo budować obiekty. Klasa jako taka nie istnieje w pamięci, jest tylko jakby nowym typem danych, który użytkownik dodał do kompilatora. Dopiero utworzenie obiektu tej klasy powoduje przydzielenie pamięci, tak jak to było przy deklarowaniu zmiennych.

 

Klasa jest podobna do struktury, tylko że posiada pewne dodatkowe własności. Jedną z nich jest możliwość umieszczania w niej nie tylko danych, ale i funkcji [metod], które na tych danych pracują. [Dla ciekawskich: w nowych kompilatorach struktury też to potrafią, ale i tak wszyscy używają klas ;-P].

 

Zróbmy więc sobie klasę PaczkaZapalek, która będzie modelem naszego pudełka i jednocześnie nowym typem danych.

 

 

 

class PaczkaZapalek {

public:

int ile_zapalek; //Właściwość - ilość zapałek w pudełku

void Dodaj(int ile); //Metoda dodaje zapałki do pudełka

void Wyjmij(int ile); //Metoda wyjmuje zapałki z pudełka

};

 

 

 

 

Definicja klasy rozpoczyna się od słówka class i znajduje się w nawiasach klamrowych. Zauważ że podobnie jak przy strukturze, całą definicję kończy średnik! Wiele dziwacznych błędów bierze się stąd, że programista zapomina dać tego średnika. Wewnątrz nawiasów klamrowych są składniki, którymi mogą być zmienne i funkcje. Etykietą public zajmiemy się za chwilę. Jak widzisz, obie funkcje działające na pudełku zapałek należą teraz do niego i są jego nieodłączną częścią.

 

Narazie tylko zapowiedzieliśmy kompilatorowi, jakie funkcje należą do obiektów tej klasy, za to nie wspomnieliśmy, jak one działają! Zanim stworzymy pierwszy obiekt tej klasy, musimy napisać definicje tych funkcji. Weźmy na przykład pierwszą z nich:

 

 

 

void PaczkaZapalek::Dodaj(int ile)

{

if (ile <= 0) return;

ile_zapalek += ile;

if (ile_zapalek > 40) ile_zapalek = 40; //Wyrównanie do 40, gdy za dużo

}

 

 

 

 

Przyjrzyj się nagłówkowi funkcji. Widzisz coś nowego? Tak, jest tutaj nazwa klasy, operator zakresu i dopiero nazwa funkcji. A wszystko po to, żeby kompilator wiedział, dla której klasy definiujemy funkcję. Bo przecież może być inna klasa, która ma funkcję składową o takiej samej nazwie i kompilator musi je jakoś rozróżnić. Reszta jest ci już pewnie znana. Ciało funkcji też jest dosyć proste - jeśli dodajemy jakieś zapałki, to zwiększamy odpowiednio wewnętrzną zmienną ile_zapalek. Gdy zapałek jest za dużo, wyrównujemy do 40. A tak wygląda definicja drugiej funkcji:

 

 

 

void PaczkaZapalek::Wyjmij(int ile)

{

if (ile <= 0) return;

ile_zapalek -= ile;

if (ile_zapalek < 0) ile_zapalek = 0; //Wyrównanie do 0, gdy za mało

}

 

 

 

 

No. Mamy już zdefiniowane obie funkcje, możemy już tworzyć obiekty klasy PaczkaZapalek! :-). A robi się to w taki sam sposób, jak przy definiowaniu każdej innej zmiennej typu wbudowanego. Dopisz przed funkcją main poniższą linijkę:

 

 

 

PaczkaZapalek kowbojskie; //Globalna paczka zapałek

 

 

 

 

Ponieważ jest to obiekt globalny, kompilator wyzeruje zarezerwowaną dla niego pamięć, więc pudełko na początku będzie zawierać 0 zapałek. Gdybyśmy zadeklarowali ten obiekt wewnątrz funkcji main, mógłby zawierać początkowo przypadkowe dane, a tego chyba nie chcecie [jak mawiał Arnie z "13 posterunku" ;-D].

 

Mamy obiekt kowbojskie klasy PaczkaZapalek. Jak się do niego dobrać? Podobnie jak do struktury, czyli używając kropki. Na przykład żeby zobaczyć, ile w pudełku jest zapałek, wystarczy sprawdzić jego właściwość ile_zapalek w taki oto sposób:

 

 

 

cout << "W pudelku jest " << kowbojskie.ile_zapalek << " zapalek.\n";

 

 

 

 

Podobnie korzystamy z metod obiektu, czyli jego funkcji składowych.

 

 

 

kowbojskie.Dodaj(3); //Dodajemy 3 zapałki

cout << "Dodalismy 3 zapalki. Teraz jest ";

cout << kowbojskie.ile_zapalek << " zapalek.\n";

 

 

 

 

Wywoływanie metod obiektu przypomina trochę wydawanie mu poleceń i zadawanie mu pytań. Prawda, że takie podejście jest fajniejsze? ;-) Poeksperymentuj teraz samodzielnie. Spróbuj poczęstować funkcje błędnymi wartościami i zobacz, czy zachowają się inteligentnie. A teraz spróbuj zmienić zawartość właściwości ile_zapalek na jakąś kosmiczną liczbę. Udało się? TAK?! :-o Pewnie myślisz teraz: "A miało być tak pięknie.. Dlaczego obiekt pozwala na majstrowanie przy jego wewnętrznych danych?!" Hmm.. No cóż.. A powiedzieliśmy kompilatorowi, które dane mają być dostępne z zewnątrz, a które nie? Raczej nie bardzo ;-). Lepiej więc czym prędzej to zróbmy.

 

 

Składniki prywatne i publiczne

 

Każda klasa pozwala nam określić, które składniki będą widoczne z zewnątrz, a które będą prywatnymi właściwościami klasy [takimi, o których nie powinniśmy wiedzieć, żeby czegoś nie popsuć]. Do ustalania dostępu w klasie służą trzy etykietki: private [z ang. prywatny], protected [z ang. chroniony], oraz znane ci już public [z ang. publiczny]. Składniki prywatne są widoczne tylko wewnątrz obiektu. Mogą więc na nich operować tylko funkcje składowe tego obiektu. Nawet inne obiekty tej samej klasy nie mogą ich sobie nawzajem podglądać czy zmieniać. Składniki publiczne to te, które widać z zewnątrz. Widzą je pozostałe obiekty i można je dowolnie zmieniać i używać. Natomiast o składnikach chronionych powiemy sobie innym razem, a narazie są dla nas tym samym, czym składniki prywatne.

 

Domyślnie wszystkie składniki klasy są prywatne, więc jeśli nie podasz żadnej etykiety dostępu, będą właśnie prywatne. Ale na co nam obiekt, z którego nic nie wystaje na zewnątrz i nie ma takiej siły, żeby się do niego dobrać? Zazwyczaj więc wszystkie właściwości robi się prywatne, a metody robi się publiczne, bo one wiedzą, jak się bezpiecznie obejść z prywatnymi składnikami i nie narobią nam kiermaszu ;-). Czasami niektóre funkcje można uczynić prywatnymi, na przykład jeśli są to jakieś funkcje pomocnicze które klasa wykorzystuje na swój własny tajemniczy sposób.

 

Wróćmy teraz do naszego pudełka zapałek. Uczynimy składnik ile_zapalek składnikiem prywatnym, żeby uniemożliwić niepowołany dostęp do niego. Jedynie metody Dodaj i Wyjmij będą mogły go modyfikować. Żeby jednak mieć możliwość sprawdzenia, ile zapałek jest w pudełku [przecież teraz jest to składnik prywatny!], dodamy jeszcze jedną metodę, która nam to powie. Zauważ, że nie dodajemy żadnej metody pozwalającej na ustawienie dowolnej wartości dla składnika ile_zapalek, czyli mamy to czego chcieliśmy - bezpieczeństwo.

 

 

 

class PaczkaZapalek {

private:

int ile_zapalek; //Właściwość - ilość zapałek w pudełku

public:

void Dodaj(int ile); //Metoda dodaje zapałki do pudełka

void Wyjmij(int ile); //Metoda wyjmuje zapałki z pudełka

int Pokaz() { return ile_zapalek; } //Metoda pokazuje ile jest zapałek

};

 

 

 

 

Czy widzisz coś interesującego? Dlaczego zdefiniowaliśmy metodę Pokaz wewnątrz definicji klasy i co nam to daje? No więc taki sposób definicji metody powoduje, że jest ona funkcją inline, czyli cały jej kod jest wstawiany w miejsce wywołania. Cała definicja funkcji znajduje się już wewnątrz klasy, więc nie trzeba jej już definiować poza klasą. Funkcjami inline najlepiej jest uczynić funkcje, które mało robią [np. zwracają zawartość składników prywatnych] i są wywoływane często. Dzięki temu czas potrzebny na ich wywołanie będzie krótszy.

 

Acha, żeby nie być gołosłownym, radzę ci teraz wykonać mały eksperyment. Spróbuj w funkcji main odwołać się bezpośrednio do prywatnego składnika ile_zapalek. Zobaczysz, że kompilator nie pozwoli ci przy nim majstrować i już przy kompilacji ostro zaprotestuje. Jedyną możliwością, by poznać ilość zapałek w pudełku, jest użycie "autoryzowanej" metody Pokaz(). Nie ma natomiast żadnego sposobu, by wpisać w obiekcie niepoprawną ilość zapałek. I to jest jedna z wielu zalet programowania obiektowego - ukrywanie danych, które chroni przed wieloma błędami.

 

A jaki jest koszt takiej ochrony? Czy programy obiektowe działają wolniej? Czy zajmują więcej miejsca? Cóż, niektórzy lubią zwalać swoje błędy na programowanie obiektowe, ale prawda jest inna ;-P. Program obiektowy przeważnie zajmuje tyle samo, co jego odpowiednik strukturalny. Każdy obiekt zajmuje tyle, ile zajmują jego dane składowe. Funkcje, jako instrukcje procesora, nie muszą być osobne dla każdego obiektu, więc zajmują miejsce w EXEku jednorazowo. Możesz łatwo sprawdzić rozmiar obiektu operatorem sizeof i przekonać się, że obiekt zawierający dwa składniki typu int zajmuje tyle, ile zajmowałyby łącznie te zmienne "luzem", czyli 8 bajtów.

×
×
  • Dodaj nową pozycję...