menu

darmowe eBooki

Programuję w Delphi i C++ Builder

okładka

Jak szybko nauczyć się programowania w dwóch różnych językach?

Sprawdź sam, czytając darmowy fragment eBooka Programuję w Delphi i C++ Builder.

Czy ta wiedza okazała się dla Ciebie przydatna? Chcesz wiedzieć więcej? Zobacz tutaj.

Google Adsense

migawka z forum

Dowcipy o programistach/informatykach

Witam, grunt to potrafić śmiać sie z samego siebie  ;DPrzychodzi informatyk do domu po pra...

Rysiek z "Klanu"

W związku z ogromnym poruszeniem we wszystkich mediach w naszym kraju, dotyczącym tak kultowej postaci jak Rysiek z &quo...

Propozycje zmian w serwisie Guide C++

Witam, ze względu na to że nie znalazłem podobnego tematu na forum, a mam kilka sugestii   ;) postanowiłem je ...

buttony

SDJ
vortal programistów
Vademecum sieci komputerowych
Soldiers'04 - fan klub Legii Warszawa
www.katalog.bajery.pl
katalog stron
katalog najlepszych stron
jestem w katalogu
Wszystko o C++
[zamknij]

Korzystamy z plików cookies i umożliwiamy zamieszczanie ich osobom trzecim. Pliki cookie pozwalają na poznanie twoich preferencji na podstawie zachowań w serwisie.
Uznajemy, że jeżeli kontynuujesz korzystanie z serwisu, wyrażasz na to zgodę.

jesteś w: Kurs C++ / Klasy
Szkoła Hakerów - Kurs Hackingu Bez Cenzury

Klasy

To jest rozdział, na który czekałem z niecierpliwością. Klasy to wspaniałe narzędzie. Jeżeli zrozumiałeś struktury i polubiłeś je to na pewno klasy Ci się spodobają. Można powiedzieć, że klasy to jakby rozbudowane struktury. Znakomicie nadają się do tworzenie obiektów mających swoje odzwierciedlenie w świecie rzeczywistym. Z pomocą klas można zamodelować niemal wszystko! Począwszy od pudełka zapałek na statku kosmicznym skończywszy. Brzmi fajnie :) Wkrótce sam się przekonasz, że wykreowanie dowolnych obiektów jest dziecinnie proste. Wprowadzenie klas diametralnie zmieniło dotychczasowy tok myślenia o programowaniu. Powstał zupełnie nowy rodzaj programowania. Chodzi tutaj o programowanie obiektowo orientowane [nie mylić z programowaniem obiektowym]. Nazwa trochę tajemnicza, ale za moment wszystko się wyjaśni. Programowanie orientowane obiektowo jest bardzo elastyczne na późniejsze modyfikacje. Nierzadko pozwala zaoszczędzić mnóstwo czasu. Krótko mówiąc polega ono na takim powiązaniu wszystkich obiektów w programie, aby program sam był w stanie dopasować się do przyszłych udoskonaleń i to bez konieczności ingerencji w kod źródłowy! To fantastyczne narzędzie jest stosowane w większości programów okienkowych. Tobie również polecam nauczenie się go. Ta lekcja oraz dwie kolejne umożliwią Ci zapoznanie się z tym sposobem programowania. Tak więc zaczynjmy!
Wyjdźmy od konkretnego przykładu. Powiedzmy, że chcesz napisać niewielką bazę adresową. Najpierw zbierzmy do kupy niezbędne elementy. W bazie należy umieścić imię, nazwisko, adres oraz telefon wybranych osób. Zatem będą nam potrzebne tablice tekstowe do przechowywania imienia, nazwiska i adresu. Numer telefonu umieścimy w zmiennej typu long. Mamy już zestaw zmiennych do przechowywania danych. Zwykle takie bazy danych zawierają kilkadziesiąt lub kilkaset osób. Należałoby więc utworzyć tablice dla każdej zmiennej. Tutaj znów pojawia się problem dotyczący rozmiaru pamięci. Do tego celu doskonale nadadzą się poznane niegdyś wskaźniki oraz dynamiczna rezerwacja pamięci. Zatem przygotujmy sobie kilka wskaźników.

  1. char *nazwisko;
  2. char *imie;
  3. char *adres;
  4. long *telefon;

Teraz wystarczy utworzyć dowolnie dużą tablicę dla każdej zmiennej. Na początek weźmy 200 osób. Czy zauważasz tutaj coś dziwnego? Musimy utworzyć tablicę zawierającą 200 elementów. Z czego każdy obiekt prócz ostatniego jest tablicą tekstową :-/ Dziwnie brzmi i dziwna jest tego realizacja. Tak naprawdę należałoby napisać tak:

  1. char **nazwisko;;
  2. char **imie;;
  3. char **adres;;
  4. long *telefon;

Myślę, że jeszcze nie zrzuciłeś komputera z biurka :) Wiem, że te dwie gwiazdy są kompletnie niezrozumiałe, ale nie przejmuj się. W naszym przykładzie skorzystamy z klas, więc takie kombinacje nie będą potrzebne. Jednak dla formalności przeczytajmy powyższe definicje. Pierwsza oznacza tyle, co nazwisko jest wskaźnikiem do wskaźnika typu char :-/ Identycznie wygląda sytuacja w przypadku imienia oraz adresu. Z numerem telefonu jest inaczej. Tutaj wystarcza tylko jedna gwiazda, gdyż jest to typ long. Teraz pokażę przykład rezerwacji pamięci dla takich danych.

  1. nazwisko = new char *[200];
  2. imie = new char *[200];
  3. adres = new char *[200];
  4. telefon = new long [200];

Trzy pierwsze instrukcje rezerwują pamięć dla 200 wskaźników typu char. Ostatnia tworzy w pamięci 200 elementów typu long. Trochę to zagmatwane, co? Nie dość, że jest to ciężkie w realizacji to jeszcze łatwo popełnić błąd. Dlatego właśnie nie będę dalej opisywał tego sposobu. Pokażę natomiast jak można to zrobić o wiele prościej wykorzystując klasy.

  1. class osoba
  2. {
  3. char nazwisko [40];
  4. char imie [20];
  5. char adres [40];
  6. long telefon;
  7. };

Powyżej znajduje się definicja klasy osoba. W jej wnętrzu umieszczone zostały wszystkie niezbędne składniki. Zauważ podobieństwo do definicji struktur. Tutaj także obowiązuje średnik kończący definicję. Zajmijmy się teraz składnikami. Pierwszy to tablica do przechowywania nazwiska. Dla ułatwienia jej rozmiar jest narzucony odgórnie. Podobnie wygląda sytuacja dla zmiennych zawierających imię oraz adres. Numer telefonu to jedynie zmienna typu long. Pewnie już się domyślasz jak się definiuje obiekty typu osoba. No pewnie!

  1. osoba gostek;

Utworzyliśmy sobie obiekt klasy osoba o nazwie gostek. Nas jednak interesują wewnętrzne składniki tego obiektu. Wiesz jak można się do nich odnieść? Tak naprawdę obecnie nie można tego zrobić :-O Pewnie myślisz, że ściemniam. Okazuje się, że wszystkie składniki wewnątrz klasy osoba są szczelnie zamknięte w jej wnętrzu. Mało tego. W obecnej chwili nikt nie ma do nich dostępu :-/ To dlatego, że wszystkie składniki są prywatne. To jest jedna z nowości, jaka pojawia się w klasach, a która odróżnia je od struktur. Jest to zjawisko enkapsulacji lub hermetyzacji. Obie nazwy doskonale oddają istotę problemu. No dobra, ale co nam po takiej upośledzonej klasie, do której nie można się dostać? Właściwie to nic :( Aby dostęp do składników był możliwy należy uczynić z nich składniki publiczne. Jak sama nazwa wskazuje będą one dostępne dla każdego. Zobacz na przykład:

  1. class osoba
  2. {
  3. public:
  4. char nazwisko [40];
  5. char imie [20];
  6. char adres [40];
  7. long telefon;
  8. };

Pojawia nam się tutaj słówko public. To jest etykieta dostępu. Informuje ona kompilator, jak należy traktować obiekty znajdujące się bezpośrednio za nią. Istnieją trzy etykiety. Etykietę public właśnie opisałem, tak więc nie będę się powtarzał. Kolejna to private, czyli prywatny. Składniki znajdujące się za nią są dostępne jedynie w danej klasie. Jeżeli nie podasz żadnej etykiety to przez domniemanie zostanie użyta ta etykieta. Tak było w pierwszym przykładzie. Ostatnią etykietą jest protected [chroniony]. Ma ona dość specyficzne zastosowanie. Dokładnie powiem o tym w następnym rozdziale. Na razie przyjmijmy, że ma takie samo znaczenie, jak private. Sposób odnoszenia się do publicznych składników klasy jest analogiczny jak w przypadku struktur. Najpierw podajesz nazwę obiektu, następnie kropkę i na końcu konkretny obiekt składowy. Dla przypomnienia fragment kodu:

  1. osoba gostek;
  2. if (gostek.telefon == 997) //uciekać to mendziarz!! :-O
  3. else //wporzo, to nasz ziom :)

Taki dostęp jest możliwy tylko w przypadku składników publicznych. Pamiętaj o tym! Teraz pewnie zastanawiasz się, jakie są korzyści z używania wspomnianych etykiet dostępu. Uprzyjemnijmy sobie trochę życie. Pokażę to na przykładzie. Kupujesz telewizor. Przez nieuwagę konstruktorów nie posiada on obudowy :) Zatem wszystkie elementy są dostępne dla użytkownika [public]. Każdy może teraz trochę pomajstrować przy telewizorze. Pojawia się wówczas niebezpieczeństwo. Można przypadkiem zrobić zwarcie i uszkodzić sprzęt. Nie wspominając o porażeniu prądem. To był przykład klasy z publicznymi składnikami. Chyba dostrzegasz ryzyko z tym związane? Oczywiście konstruktorzy sprzętu RTV nie mogą sobie na to pozwolić. Należy tak zaprojektować telewizor, aby użytkownik mógł z niego bezpiecznie i łatwo korzystać. Zatem wszelkie podzespoły elektroniczne powinny być zamknięte pod obudową, aby nie korciły majsterkowiczów. Przekładając to na język C++. Dostęp do takkich podzespołów powinien być prywatny. Chyba rozumiesz na czym to polega?

funkcje składowe

Tutaj pojawia się nowość w stosunku do struktur. W strukturach mogliśmy definiować tylko składniki. Dodatkowo były one publiczne. Klasy dają większe możliwości. Oprócz zwykłych składników w klasie można umieszczać także funkcje składowe! Jaka z tego korzyść? Ogromna! Funkcje składowe mają o wiele większe prawa wobec klasy, w której się znajdują, niż zwykłe funkcje. Mogą bez problemów operować na składnikach swojej klasy, nawet tych prywatnych! Powróćmy do naszej bazy adresowej. Klasa osoba zawiera wszystkie składniki prywatne. Wiemy już, że funkcje składowe mogą korzystać z prywatnych składników. Zatem dodajmy kilka przydatnych funkcji składowych do klasy osoba.

  1. class osoba
  2. {
  3. private:
  4. char nazwisko [40];
  5. char imie [20];
  6. char adres [40];
  7. long telefon;
  8.  
  9. public:
  10. char * wez_nazwisko ();
  11. char * wez_imie ();
  12. char * wez_adres ();
  13. long wez_telefon ();
  14.  
  15. void wpisz_nazwisko (char *nazwisko_);
  16. void wpisz_imie (char *imie_);
  17. void wpisz_adres (char *adres_);
  18. void wpisz_telefon (long telefon_);
  19. };

Ale się nam rozrosła ta definicja. Po kolei. Pierwsze linijki już znasz. Zwykłe definicje składników klasy. Są one zdefiniowane pod etykietą private, więc dostęp do nich mają tylko uprzywilejowani. Teraz pojawia się nowość. Deklaracje kilku funkcji. Pierwsza o nazwie wez_nazwisko służy do pobrania wartości zapisanej w zmiennej nazwisko. Jej rezultatem jest wskaźnik do tej zmiennej. Podobnie wygląda sytuacja w przypadku dwóch pozostałych funkcji. Funkcja czwarta zwraca numer telefonu. W definicji klasy znajdują się jedynie deklaracji tych funkcji. Oczywiście nic nie stoi na przeszkodzie, aby ciała funkcji składowych umieścić bezpośrednio w ciele klasy. Wtedy takie funkcje będą automatycznie funkcjami inline. Jednakże odradzam takie postępowanie za względów estetycznych. Często w takich sytuacjach robi się bałagan i trudniej jest się odnaleźć. To taka mała uwaga. Powróciwszy do tematu. Czas teraz na przedstawienie ciał tych funkcji, czyli ich definicji. Prezentują się one następująco:

  1. char * osoba::wez_nazwisko ()
  2. {
  3. return nazwisko;
  4. }
  5.  
  6. char * osoba::wez_imie ()
  7. {
  8. return imie;
  9. }
  10.  
  11. char * osoba::wez_adres ()
  12. {
  13. return adres;
  14. }
  15.  
  16. long osoba::wez_telefon ()
  17. {
  18. return telefon;
  19. }

Jak widzisz definicje funkcji składowych różnią się nieco od zwykłych funkcji. Zasadniczą różnicą jest określenie, do której klasy dana funkcja należy. Aby to ustalić trzeba przed nazwą funkcji wstawić nazwę klasy oraz operator zakresu [::]. Łatwo to zapamiętać. Skoro funkcja należy do pewnej klasy, to należy jeszcze określić, do jakiej. Jeżeli chodzi o działanie funkcji to właściwie wszystko powinno być jasne, ale powiem krótko. Funkcja wez_nazwisko jest wywoływana bez żadnych argumentów, a zwraca wskaźnik do tablicy tekstowej. W jej wnętrzu znajduje się zaledwie jedna instrukcja wykonująca tę czynność. Pamiętasz jeszcze słówko return? Tak tak, za jego pomocą zwracaliśmy rezultat w funkcji. Tutaj też się przydaje. Trzy kolejne funkcje są identyczne. Przypuśćmy, że chcesz zapisać wartość składnika klasy nazwisko. Wystarczy wywołać funkcję na rzecz konkretnego obiektu klasy osoba. Jednakże zanim to zrobimy zdefiniujmy sobie funkcję do kopiowania tablic znakowych, która zaraz się przyda.

  1. void kopiuj_string (char *string_zrodlo, char *string_cel)
  2. {
  3. for (int i = 0; ; i++)
  4. {
  5. string_cel [i] = string_zrodlo [i];
  6. if (string_zrodlo [i] == NULL) break;
  7. }
  8. }

Wyjaśnijmy, jak to działa. Funkcja kopiuj_string przyjmuje dwa argumenty. Pierwszy to wskaźnik do tablicy znakowej będącej obiektem źródłowym, czyli tym kopiowanym. Obiekt drugi to również wskaźnik do tablicy znakowej, w której należy zapisać kopiowany tekst. Zarówno obiekt pierwszy, jak i drugi może być tablicą lub wskaźnikiem do takiej tablicy. To dlatego, że w C++ tablice i wskaźniki są traktowane na równi. Funkcja zwraca typ void, czyli tak naprawdę nic nie zwraca. W jej ciele znajduje się pętla for. W tej pętli definiujemy sobie licznik obiegów. Podczas każdego obiegu następuje przekopiowanie jednego znaku z tablicy źródłowej do docelowej oraz inkrementacja licznika obiegów. Dodatkowo sprawdzana jest wartość kopiowanego znaku. Jeżeli jest równy NULL oznacza to, że właśnie skopiowaliśmy ostatni znak z tablicy. Warto o tym pamiętać! Każda tablica musi posiadać taki znak kończący, dzięki czemu można się zorientować, jaka jest jej długość. Pewnie teraz się zastanawiasz, dlaczego zastosowałem tutaj takie kombinacje z kopiowaniem poszczególnych znaków. Czy nie można by napisać tak:

  1. void kopiuj_string (char *string_zrodlo, char *string_cel)
  2. {
  3. string_cel = string_zrodlo;
  4. }

Oczywiście, że można. Tylko co to da?. Domyślasz się już na czym polega błąd? Wystarczy spojrzeć na instrukcję: string_cel = string_zrodlo;. Jest to przypisanie adresu obiektu pokazywanego przez wskaźnik string_zrodlo do wskaźnika string_cel. W wyniku takiej operacji w pamięci nadal istnieje jedna tablica znakowa. Tyle, że pokazują na nią dwa wskaźniki. A co w przypadku, gdy jest ona tworzona dynamicznie? Co się stanie w momencie zwolnienia jej? Wówczas wskaźnik string_cel będzie pokazywał na jakieś przypadkowe dane. Nam chodziło tutaj o skopiowanie, a nie o przypisanie. Zatem coś zupełnie innego. Teraz chyba rozumiesz, dlaczego musiałem zastosować taki manewr z kopiowaniem poszczególnych znaków. Tak naprawdę taką operację można wykonać znacznie prościej. Zamiast żmudnie kopiować znak po znaku można posłużyć się jedną z funkcji bibliotecznych dostarczonych przez producenta kompilatora. Ja jednak postanowiłem zrobić to ręcznie, gdyż korzystanie z funkcji bibliotecznych wymaga pewnej znajomości dyrektyw preprocesora. O dyrektywach jeszcze nie mówiliśmy, więc nie chciałem dodatkowo komplikować.
Mając już funkcję do kopiowania stringów możemy kontynuować omawianie wywoływania funkcji składowych.

  1. osoba gostek;
  2. char nowe_nazwisko [30];
  3. kopiuj_string (gostek.wez_nazwisko (), nowe_nazwisko);

Trochę się nam kod skomplikował. W pierwszej linijce definiujemy sobie obiekt klasy osoba o nazwie gostek. To już znamy i umiemy od dawna. Druga linijka też powinna być zrozumiała. Zwykła definicja tablicy o nazwie nazwisko. To w niej właśnie będziemy umieszczać kopiowany tekst. Trzecia linijka jest bardziej złożona i składa się jakby z dwóch etapów. Najpierw jest wywołanie funkcji kopiuj_string. Ta funkcja pobiera dwa argumenty będące wskaźnikami do tablic znakowych. Jej działanie skupia się na skopiowaniu zawartości pierwszej tablicy do tablicy drugiej. My, jako pierwszy argument podajemy rezultat funkcji składowej klasy osoba wywołanej na rzecz obiektu gostek. Aby było jasne jest to instrukcja gostek.wez_nazwisko() i zwraca wskaźnik do składnika obiektu gostek o nazwie nazwisko. Drugim argumentem jest tablica znakowa nazwana nowe_nazwisko. To w niej właśnie zostanie umieszczone nazwisko obiektu gostek. Jeżeli byś chciał pobrać imię, lub adres to obowiązują tutaj te same reguły. W przypadku odczytywania numeru telefonu sprawa wygląda następująco:

  1. osoba gostek;
  2. long nowy_telefon;
  3. nowy_telefon = gostek.wez_telefon ();

Chcąc wywołać funkcje składową należy przed jej nazwą podać nazwę obiektu oraz kropkę [.]. Chyba rozumiesz. Wymienione funkcje służyły do odczytu poszczególnych składników klasy osoba. Kolejne funkcje działają w drugą stronę, czyli zapisują konkretne wartości.

  1. void osoba::wpisz_nazwisko (char *nazwisko_)
  2. {
  3. kopiuj_string (nazwisko_, nazwisko);
  4. }
  5. void osoba::wpisz_imie (char *imie_)
  6. {
  7. kopiuj_string (imie_, imie);
  8. }
  9. void osoba::wpisz_adres (char *adres_)
  10. {
  11. kopiuj_string (adres_, adres);
  12. }
  13. void osoba::wpisz_telefon (long telefon_)
  14. {
  15. telefon = telefon_;
  16. }

W zasadzie tutaj nie ma żadnych nowości. Funkcja wpisz_nazwisko przyjmuje jeden argument. Jest to wskaźnik do tablicy znakowej. Zwraca typ void, czyli nic. W ciele funkcji następuje przypisanie odpowiedniej wartości do składnika nazwisko. Podobnie wygląda sytuacja w przypadku dwóch kolejnych funkcji. Sposób wywołania jest analogiczny, jak poprzednio.

  1. osoba gostek;
  2. gostek.wpisz_nazwisko ("Matołek");
  3. gostek.wpisz_imie ("Koziołek");
  4. gostek.wpisz_adres ("Pacanów 5/13");
  5. gostek.telefon (9879879);

Tym oto sposobem za pomocą funkcji składowych klasy osoba dobraliśmy się do jej prywatnych [?] składników. Wiemy już trochę o funkcjach składowych. Teraz pogadamy o funkcji, która jest wywoływna automatycznie podczas tworzenia nowego obiektu.

konstruktor

Konstruktor, to specjalna funkcja składowa danej klasy posiadająca taką samą nazwę, jak klasa, do której należy. Nowością tutaj jest fakt, iż konstruktor w przeciwieństwie do zwykłych funkcji nic nie zwraca. Nawet typu void! Konstruktor jest wywoływany automatycznie w momencie kreowania nowego obiektu danej klasy. Oczywiście można go wywołać ręcznie, jak zwykłą funkcję składową, ale to raczej nie ma sensu. Rozbudujmy teraz naszą klasę o konstruktor.

  1. class osoba
  2. {
  3. private:
  4. char nazwisko [40];
  5. char imie [20];
  6. char adres [40];
  7. long telefon;
  8.  
  9. public:
  10. osoba ();//konstruktor
  11. char * wez_nazwisko();
  12. char * wez_imie();
  13. char * wez_adres();
  14. long wez_telefon();
  15.  
  16. void wpisz_nazwisko (char *nazwisko_);
  17. void wpisz_imie (char *imie_);
  18. void wpisz_adres (char *adres_);
  19. void wpisz_telefon (long telefon_);
  20. };

W taki sposób prezentuje się deklaracja konstruktora. Zapamiętaj sobie, że konstruktor ma zawsze nazwę taką, jak jego klasa oraz nigdy nie zwraca żadnej wartości. Jeżeli chodzi o definicję konstruktora to sprawa też jest prosta.

  1. osoba::osoba ()
  2. {
  3. //dowolne instrukcje, np. inicjalizujące składniki klasy
  4. }

Taki konstruktor zostanie wywołany w momencie definiowania obiektu klasy osoba. Zatem linijka:

  1. osoba gostek;

Utworzy obiekt klasy osoba i dodatkowo wywoła konstruktor. Oczywiście konstruktor w takiej postaci ma zerowe zastosowanie. Jego ciało jest puste. Przebudujmy go zatem, aby był bardziej przydatny. Powiedzmy, że podczas tworzenia obiektu chcemy go zainicjalizować konkretnymi wartościami. Wystarczy za nazwą konstruktora w nawiasie umieścić jego argumenty. Czyli dokładnie jak z funkcjami. Looknij sobie:

  1. osoba (char *imie_, char *nazwisko_, char *adres_, long telefon_);

To jest deklaracja. Pora na definicję. Jego definicję zapiszemy w następujący sposób:

  1. osoba (char *imie_, char *nazwisko_, char *adres_, long telefon_)
  2. {
  3. kopiuj_string (imie_, imie);
  4. kopiuj_string (nazwisko_, nazwisko);
  5. kopiuj_string (adres_, adres);
  6. telefon = telefon_;
  7. }

Pierwsza instrukcja kopiuje tablicę znakową pokazywaną przez wskaźnik imie_ do obiektu klasy o nazwie imie. Analogicznie wygląda sytuacja w przypadku dwóch pozostałych instrukcji. Instrukcja czwarta to najzwyczajniejsze podstawienie wartości zmiennej telefon_ do składnika klasy telefon. Teraz uważaj! Do tej pory definiowaliśmy obiekty podając typ i nazwę obiektu w taki sposób:

  1. int liczba_int;
  2. long liczba_long;
  3. char znak_char;
  4. osoba obiekt_osoba;

Zauważ, co się dzieje w czwartej linijce. Niby zwykła definicja. No tak, ale teraz w klasie osoba istnieje konstruktor przyjmujący kilka argumentów. My nie podaliśmy żadnych argumentów, zatem ostatnia linijka zostanie uznana za błąd. Aby teraz utworzyć obiekt klasy osoba należy zapisać tak:

  1. osoba obiekt_osoba ("Aniela", "Dulska", "Kraków, ul.Smocza 6", 1111111);

Po prostu w nawiasie tuż za nazwą obiektu trzeba umieścić argumenty widniejące przy definicji konstruktora. Oczywiście konstruktor obowiązują również zasady przeładowania oraz argumentów domniemanych. Bardzo często zdarza się, że w klasie istnieje kilka konstruktorów. Wówczas warto umieścić tam także konstruktor domniemany, czyli bezargumentowy. Konstruktor posiada jeszcze jedną fajną cechę. Jest to lista inicjalizacyjna konstruktora.

lista inicjalizacyjna konstruktora

Lista inicjalizacyjna konstruktora umożliwia szybką inicjalizację składników klasy. Jest ona niemal niezbędna podczas tworzenia obiektów klas odziedziczonych. O dziedziczeniu powiemy sobie dopiero w następnej lekcji. Póki co pamiętaj, że lista inicjalizacyjna służy do inicjalizacji składników klasy. Taka lista jest umieszczana tuż za definicją konstruktora po znaku dwukropka. Jej realizacja wygląda następująco:

  1. osoba (char *imie_, char *nazwisko_, char *adres_, long telefon_) : telefon (telefon_)
  2. {
  3. kopiuj_string (imie_, imie);
  4. kopiuj_string (nazwisko_, nazwisko);
  5. kopiuj_string (adres_, adres);
  6. }

Dzięki temu już na starcie składnik telefon został zainicjalizowany wartością argumentu telefon_. Co prawda poprzedni sposób jest równie dobry, jednak są pewne sytuacje gdy lista inicjalizacyjna jest niezastąpiona. Jeżeli chodzi o inicjalizację pozostałych składników to niestety nie można ich zainicjalizować na liście inicjalizacyjnej. Konkretnie na tej liście nie może być żadnych wywołań funkcji. Jedynie składniki. Teraz powiedzmy, jak są usuwane obiekty czyli czym jest destruktor.

destruktor

Destruktor jest także funkcją składową klasy. Podobnie, jak konstruktor posiada nazwę swojej klasy poprzedzoną znaczkiem tyldy [~]. Sam destruktor nie usuwa obiektu z pamięci. Jest wywoływany tuż przed zniszczeniem obiektu. Destruktor jest przydatny w sytuacjach, gdy obiekt zawiera w sobie dynamiczne tablice. Wtedy podczas usuwania obiektu należy te tablice zwolnić, aby nie zaśmiecały pamięci. Oczywiście można to zrobić ręcznie, jednak po co się trudzić. Destruktor jest wywoływany automatycznie podczas likwidacji obiektu, więc wystarczy umieścić w nim instrukcje zwalniające pamięć i można nawet zapomnieć o zwalnianiu tablic. Dodajmy do naszej klasy destruktor.

  1. class osoba
  2. {
  3. private:
  4. char nazwisko [40];
  5. char imie [20];
  6. char adres [40];
  7. long telefon;
  8.  
  9. public:
  10. osoba ();
  11. ~osoba (); //destruktor
  12. char * wez_nazwisko ();
  13. char * wez_imie ();
  14. char * wez_adres ();
  15. long wez_telefon ();
  16.  
  17. void wpisz_nazwisko (char *nazwisko_);
  18. void wpisz_imie (char *imie_);
  19. void wpisz_adres (char *adres_);
  20. void wpisz_telefon (long telefon_);
  21. };

Defincicja destruktora jest analogiczna, jak w przypadku innych funkcji. Również należy przed nazwą funkcji podać nazwę klasy oraz operator zakresu.

  1. osoba::~osoba ()
  2. {
  3. //dowolne instrukcje, wywoływane przed usunięciem obiektu z pamięci
  4. }

O destruktorze to chyba wszystko. No po za tym, że destruktor może być wirtualny :-/ O wirtualności funkcji jeszcze nie mówiliśmy, ale wkrótce powiemy. Konkretnie w następnym rozdziale. Póki co przejdźmy do funkcji operatorowych.

funkcje operatorowe

Temat operatorów został już poruszony dość dawno. Tam przedstawiłem jedynie działanie poszczególnych operatorów. Nie mówiłem nic o definiowaniu własnych. Oczywiście definiowanie nowych operatorów to nic innego, jak zwykłe przeładowanie nazwy. Tym zagadnieniem zajmowaliśmy się podczas omawiania funkcji. Podczas przeładowywania operatorów obowiązują te same zasady, jak przy zwykłych funkcjach. Czyli różne rodzaje argumentów, różna ilość lub kolejność. Funkcje operatorowe to nic innego, jak przeładowane operatory. Takie operatory mają to do siebie, że są automatycznie wywoływane. Spójrz na taki zapis:

  1. int zmienna;
  2. zmienna = 34 + 32;

W powyższym zapisie widnieją dwa operatory. Pierwszy to operator przypisania [=]. Drugi to operator dodawania [+]. Taki zapis nas nie dziwi. Następuje w nim zsumowanie dwóch liczb i podstawienie wyniku do zmiennej. Jednak co by się stało, gdybyśmy zapisali tak:

  1. class okrag
  2. {
  3. double promien;
  4. };
  5.  
  6. okrag kolko1;
  7. okrag kolko2;
  8. kolko1 = kolko2;

Dwie pierwsze linijki oczywiście są jak najbardziej poprawne. Jednak co ma oznaczać linijka trzecia? Logicznym wydaje się, że chcemy podstawić zawartość kolka2 do kolka1. Kompilator widząc takie wyrażenia stara się odnaleźć właściwy operator przypisania. My takiego nie zdefiniowaliśmy, więc zostanie wygenerowana standardowa wersja operatora przypisania. Z reguły to wystarcza. Jednak są sytuacje, kiedy klasa rezerwuje pamięć. Wówczas podczas przypisania należy przekopiować zawartość pamięci. Operator generowany przez kompilator oczywiście tego nie zrobi. Zmiast skopiować zawartość pamięci dokona bezmyślnego podstawienia wskaźników. Znów przykład, aby było łatwiej zrozumieć:

  1. class tablica
  2. {
  3. private:
  4. char *tekst;
  5.  
  6. public:
  7. tablica (char *tekst_wzor);
  8. ~tablica ();
  9. char *wez_tekst ();
  10. };
  11.  
  12. tablica::tablica (char *tekst_wzor)
  13. {
  14. tekst = new char [strlen (tekst_wzor)];
  15. strcpy (tekst, tekst_wzor);
  16. }
  17.  
  18. tablica::~tablica ()
  19. {
  20. delete [] tekst;
  21. tekst = NULL;
  22. }
  23.  
  24. char *tablica::wez_tekst ()
  25. {
  26. return tekst;
  27. }

Klasa służy do przechowywania tablic znakowych. Jest ona jeszcze niedokończona. Brakuje jej miliona detali. Jednak do naszych celów wystarczy. Zawiera ona tylko jeden składnik. Jest to wskaźnik do tablicy znakowej, którą klasa będzie przechowywać. W momencie tworzenia obiektu startuje konstruktor. Jego zadanie skupia się na zarezerwowaniu odpowiedniej ilości pamięci i zainicjalizaoaniu jej odpowiednimi wartościami. W momencie usuwania obiektu rusza do pracy destruktor, który zwalnia pamięć i ustawia wskaźnik do tablicy na NULL. Tak na wszelki wypadek. W przykładzie użyłem dwóch funkcji bibliotecznych: strcpy, która kopiuje tablice znakowe oraz strlen zwracającą rozmiar tablicy. Chcemy teraz skorzystać z naszej klasy. Definiujemy sobie kilka obiektów.

  1. tablica string1 ("string1"), string2 ("string2");

Mamy dwa obiekty klasy tablica. Postanawiamy wstawić zawartość obiektu string2 do obiektu string1. Pytanie tylko jak to zrobić? Od razu nasuwa się na myśl operator przypisania. Zatem skorzystajmy z niego.

  1. string2 = string1;

Jak myślisz, czy taki zapis jest poprawny? Jeżeli odpowiedziałeś, że tak to się zgadza. Kompilator rzeczywiście nie będzie protestował. Jeżeli natomiast pomyślałeś, że nie, to także nie popełniłeś błędu :-/ Składniowo taki zapis jest legalny. Pomimo, iż nie zdefiniowaliśmy operatora przypisania zostanie on wygenerowany automatycznie i błędu nie będzie. Kompilator chciał się przysłużyć i zdefiniował za nas operator. Problem w tym, że wykonał on dla nas niedźwiedzią przysługę. Operator przypidania generowany przez kompilator działa na takiej zasadzie. Dokonuje przypisania wszystkich składników. Czy rozumiesz co to oznacza w naszym przypadku? Nam chodziło o skopiowanie tablicy znakowej z obiektu string1 do obiektu string2. Zamiast skopiowania nastąpiło podstawienie. W wyniku tego w pamięci znajduje się nadal jedna tablica znakowa, na którą pokazuje nie jeden, a dwa wskaźniki. Pierwszy z obiektu string1, a drugi z obiektu string2. Deklaracja operatora rozpoczyna się od typu zwracanego. Operator przypiuje tylko wartości, więc nie zwraca nic, choć mógłby. Następnie jest słowo operator i znak konkretnego operatora. W naszym przpadku jest to znak równości, gdyż chodzi o operator przypisania. Argumentem jest oczywiście obiekt klasy, w której znajduje się operator. Z reguły stosuje się przesyłanie przez referencję. Dodatkowo warto skorzystać ze słówka const aby operator mógł pracować na obiektach stałych. Zerknij sobie:

  1. void operator= (const tablica &wzor)
  2. {
  3. delete [] tekst;
  4. tekst = new char [strlen (wzor.wez_tekst ())];
  5. strcpy (tekst, wzor.wez_tekst ());
  6. }

Najpierw następuje usunięcie dotychczasowej tablicy. Później jest rezerwacja nowej tablicy wraz z inicjalizacją. Oczywiście w klasach można także definiować także inne operatory. Ja pokazałem jedynie operator przypisania, gdyż jest najbardziej przydatny. Jeżeli chcesz zdefiniować inny operator to należy postępować podobnie, jak z operatorem przypisania. Teraz powiemy sobie o funkcjach, które posiadają pewne przypileje wobec klas.

funkcje zaprzyjaźnione

Funkcje zaprzyjaźnione to funkcje, które są traktowane, jak funkcje składowe danej klasy. Są one jakby uprzywilejowane i mogą dowolnie gmyrać w klasie, z którą się przyjaźnią. Mogą korzystać ze składników prywatnych. Aby funkcja stała się przyjacielem klasy należy to określić w definicji samej klasy. Wystarczy w klasie umieścić deklaracje wybranej funkcji poprzedzoną słówkiem friend. To słowo oznacza właśnie przyjaźń.

  1. class skarbiec
  2. {
  3. private:
  4. long zloto;
  5. long szmaragdy;
  6. long diamenty;
  7. friend long wez_zloto (skarbiec &skarb);
  8. friend long wez_szmaragdy (skarbiec &skarb);
  9. friend long wez_diamenty (skarbiec &skarb);
  10. };
  11.  
  12. long wez_zloto (skarbiec &skarb)
  13. {
  14. return skarb.zloto;
  15. }
  16.  
  17. long wez_szmaragdy (skarbiec &skarb)
  18. {
  19. return skarb.szmaragdy;
  20. }
  21.  
  22. long wez_diamenty (skarbiec &skarb)
  23. {
  24. return skarb.diamenty;
  25. }

Klasa skarbiec przechowuje w sobie liczne skarby. Praktycznie nie ma sposobu, aby się do nich dobrać. Nie istnieją bowiem żadne funkcje składowe w tej klasie. Istnieją natomiast funkcje globalne zaprzyjaźnione z klasą skarbiec. Mają one szczególne prawa wobec klasy, z którą się przyjaźnią. Dlatego tez mogą odczytywać, a nawet modyfikować składniki prywatne zaprzyjaźnionej klasy. Na koniec powiemy o składnikach statycznych w klasie.

obiekty statyczne w klasie

Czy pamiętasz jeszcze modyfikator static opisywany przy omawianiu zmiennych? W tamtym rozdziale mówiłem, że działanie tego modyfikatora jest uzależnione od zakresu ważności. Inne było działanie w przypadku zakresu globalnego, lokalnego, czy funkcji. W klasach obiekt statyczny zachowuje się jeszcze inaczej. Jest on wspólny dla wszystkich obiektów danej klasy. Oznacza to, że istnieje tylko jeden raz w pamięci, a jest dostępny dla każdego obiektu klasy, w której się znajduje. Deklaracja takiego obiektu jest identyczna jak w poprzednich przypadkach. Jednakże definicja jest zupełnie inna.

  1. class osoba
  2. {
  3. public:
  4. static int liczba_obiektow; //deklaracja obiektu statycznego
  5.  
  6. private:
  7. char nazwisko [40];
  8. char imie [20];
  9. char adres [40];
  10. long telefon;
  11.  
  12. public:
  13. osoba ();
  14. ~osoba ();
  15. char * wez_nazwisko ();
  16. char * wez_imie ();
  17. char * wez_adres ();
  18. long wez_telefon ();
  19.  
  20. void wpisz_nazwisko (char *nazwisko_);
  21. void wpisz_imie (char *imie_);
  22. void wpisz_adres (char *adres_);
  23. void wpisz_telefon (long telefon_);
  24. };
  25.  
  26. int osoba::liczba_obiektow; //dopiero tutaj umieszcza się definicję obiektu statycznego

Definicja składnika statycznego jest trochę zbliżona do definicji funkcji składowej. Także tutaj trzeba określić, w której klasie obiekt się znajduje. Taki składnik jest wspólny dla wszystkich obiektów klasy osoba. Wynika z tego pewien wniosek. Do składnika statycznego można się odnieść na kilka sposobów. Jeżeli jest on publiczny to stosujemy tutaj składnię poznaną na początku. Najpierw nazwa obiektu, a później składnika.

  1. osoba gostek;
  2. if (gostek.liczba_obiektow > 200); //sorr'y, ale już jest komplet :(
  3. else if (gostek.liczba_obiektow == 199); //masz farta, załapałeś się, jako ostatni :)

Składnik statyczny w klasie jest czasami bardzo przydatny. Pozwala niekiedy zaoszczędzić sporo pamięci. Powiedzmy, że nasza baza adresowa zawiera 1000000 osob :-O Każda osoba zajmuje 40 [tablica na nazwisko] + 20 [imię] + 40 [adres] + 4 [telefon]. Średnio na jedną osobę przypadają 104 bajty. W przypadku 1000000 osób mamy 104 * 1000000, czyli 104000000 bajty. To daje nam jakieś 101562,5 kB, czyli 99,18 MB! Jeżeli w każdym obiekcie klasy osoba istniałby jeszcze składnik przechowujący ilość osób, to rozmiar jednego obiektu klasy osoba wynosiłby 108 B, co daje 105468,75 kB i 102,99 MB. Różnica wynosi ok. 3,81 MB. Prawie 4 MB! hmm. W porównaniu do 103 MB jest to niewiele. Jednakże jest to marnotrawstwo. Aby temu zaradzić wystarczy zdefiniować składnik liczba_obiektów, jako statyczny i już mamy więcej przestrzeni w RAM-ie :) Drugim sposobem dobrania się do składnika statycznego jest dostęp poprzez operator zakresu w taki sposób:

  1. osoba gostek;
  2. if (osoba::liczba_obiektow > 200); //sorr'y, ale już jest komplet :(
  3. else if (osoba::liczba_obiektow == 199); //masz farta, załapałeś się, jako ostatni :)

Chyba nie wymaga to większego komentarza? Ostatni sposób to dostęp za pomocą funkcji. Tutaj sprawa wygląda analogicznie, jak w przypadku zwykłych składników. To już koniec tego rozdziału. Oczywiście to nie oznacza, że koniec tematu klas. O klasach będą trzy jeszcze trzy następne lekcje. Tak więc jeżeli czegoś nie zrozumiałeś do dalej nie ruszysz :(