Klasy
Klasy
stanowią
rozszerzenie
możliwości
C++,
pozwalające
na
reprezentowanie i rozwiązywanie złożonych, rzeczywistych problemów.
Tworzenie nowych typów
Poznaliśmy już różne rodzaje typów zmiennych. Typ zmiennej zawiera
informację o rodzaju wartości przechowywanych przez tę zmienną. Jeżeli
zdefiniujecie zmienną Liczba jako unsigned short, to wiadomo będzie, że
zmienna ta będzie mogła przyjmować wartości od 0 do 65535 (przy
założeniu, że unsigned short zajmuje dwa bajty).
W praktyce, to właśnie oznacza, że zmienna jest typu unsigned short. Nie
chodzi tu o to, że zakres wartości jest "produktem ubocznym" bycia zmienną
typu unsigned short.
Oprócz informacji o rozmiarze, typ zawiera również dane o możliwościach i
właściwościach obiektu. Np. liczby typu short mogą być dodawane. Oznacza
to, że sama deklaracja zmiennych Liczba1 i Liczba2 jako short mówi, że
liczby te będzie można dodać, a wartość przypisać innej zmiennej.
Rozmiarze w pamięci
Rodzaju przechowywanej informacji
Możliwych do wykonania czynnościach
Typ zawiera informacje o:
Co to jest typ?
Typ można porównać do kategorii.
Jedną z umiejętności wyróżniających człowieka jest przypisywanie rzeczy i
zjawisk do określonych kategorii.
W lesie nie widzimy tysiąca kształtów, widzimy zwierzęta i drzewa. Również
zwierzęta możemy podzielić: zające, wilki, jelenie itp.
Przykład:
W C++ typ to obiekt z określonym rozmiarem, zbiorem możliwych do
wykonania operacji i stanem. Programista C++ może stworzyć dowolny
typ. Każdy z tych typów może mieć taką samą funkcjonalność jak
standardowe typy.
Tworzymy klasyfikacje, porządek, grupy, podziały i klasy. W skrócie można
powiedzieć, że tworzymy różne typy rzeczy.
Pomarańcza
Cytrus
Owoc
Roślina
Żywa rzecz
W jakim celu tworzymy nowe typy?
Zazwyczaj programy piszemy po to, aby rozwiązywać rzeczywiste, istniejące
problemy takie jak obsługa bazy danych o zatrudnionych czy symulacja pracy
systemu centralnego ogrzewania.
Mimo, że każdy z tych
problemów można rozwiązać
posługując
się
jedynie
liczbami
całkowitymi
i
znakami,
to
nieporównywalnie szybciej i
łatwiej możemy otrzymać
wynik
dla
złożonego
problemu jeśli stworzymy
reprezentacje obiektów, o
których mówimy, i które
analizujemy.
Innymi
słowy,
jeśli
symulujemy
działanie
systemu
centralnego
ogrzewania
, to wygodnie
jest
stworzyć
zmienne
reprezentujące
pokoje
,
termostaty
i
termy
. Im
bardziej zmienne (model)
przystają
do
rzeczywistości, tym łatwiej
jest napisać program, który
rozwiąże dany problem.
Klasy i ich zmienne
Nowy typ tworzy się poprzez zadeklarowanie klasy.
Klasa to zbiór zmiennych połączonych ze zbiorem odpowiadających im
funkcji.
Przykład:
sposoby reprezentacji
samochodu
polega na wymienieniu jego
składników:
drzwi,
koła,
okna,
siedzenia,
itp..
informacje o możliwościach
samochodu:
może się poruszać,
przyspieszać,
zwalniać
itp...
I
II
Można prowadzić samochód bez znajomości budowy silnika. Podobnie można
wykorzystywać klasy.
Hermetyzacja
polega na łączeniu wszystkich informacji, możliwości,
zalet jednostki w jeden obiekt.
Zalety hermetyzacji wszystkich informacji o samochodzie:
wszystko jest zgromadzone w jednym miejscu
można łatwo dane reprezentować, kopiować i nimi manipulować.
Klienci waszej klasy to inne klasy lub funkcje wykorzystujące przez was
napisaną klasę. Hermetyzacja pozwala na wykorzystanie obiektu bez
znajomości jego wewnętrznych mechanizmów i sposobu działania.
Przykład
Jedyna rzecz, którą musicie wiedzieć, to co dana klasa robi, a nie jak
ona to robi.
Metody klasy to funkcje w danej klasie. Stanowią one taką samą część
klasy jak zmienne wewnętrzne. Metody decydują o możliwościach danej
klasy.
Każda klasa może się składać z dowolnie złożonego zbioru zmiennych i
innych klas. Zmienne w klasie określane są jako wewnętrzne zmienne klasy
lub jako wewnętrzne dane klasy.
Przykład:
Klasa samochód ma wewnętrzne zmienne reprezentujące siedzenia, radio,
koła itp.
Wewnętrzne zmienne klasy, określane również jako wewnętrzne dane klasy,
stanowią zmienne danej klasy.
Są jej częścią, podobnie jak koła, silnik itp. są częścią samochodu.
Funkcje w klasie zazwyczaj operują na wewnętrznych zmiennych klasy.
Określane są one jako wewnętrzne funkcje klasy lub częściej jako metody
danej klasy.
Metody klasy Samochód to np.: UruchomSilnik () czy Zahamuj () .
Klasa Kot może mieć zmienne wewnętrzne takie jak wiek i waga. Metody
to np.: Zasnij () , Miaucz () , LapMyszy () .
Deklarowanie klasy
Do deklarowania klasy służy słowo kluczowe class. Po nim podajemy nazwę
tworzonej klasy, a następnie w klamrach zmienne wewnętrzne i metody.
Deklarację kończy się średnikiem.
Przykład deklaracji klasy o nazwie Kot:
class Kot
{
public:
unsigned int nWiek;
unsigned int nWaga;
Miauczy() ;
};
Deklaracja tej klasy nie rezerwuje pamięci na nią. Mówi ona kompilatorowi co
to jest Kot, jakie dane zawiera (nWiek, nWaga) i co potrafi robić ( Miauczy () ).
Dodatkowo, deklaracja niesie ze sobą informacje o rozmiarze Kota, tzn. ile
miejsca należy zarezerwować na każdego stworzonego Kota.
W tym przypadku (jeśli int zajmuje
2 bajty) to każdy Kot będzie
zajmował 4 bajty: nWiek zajmuje 2
bajty, podobnie nWaga.
Miauczy () nie zajmuje miejsca
ponieważ dla metod klasy nie
rezerwuje się obszaru w pamięci.
Definiowanie obiektu
Obiekt nowego typu definiuje się tak, jak np. zmienną typu int:
unsigned int nMasa; // definicja unsigned int
Kot oFilemon; // definicja Kota
Klasy a obiekty
Kiedy posiadamy kota, to nie jest to jego definicja, lecz konkretne zwierzę.
Musicie odróżniać ogólną definicję od zwierzątka, aktualnie strącającego
wazon w waszym pokoju.
Podobnie w C++ odróżniamy definicję klasy Kot, która jest jedynie opisem
kota od konkretnego obiektu typu Kot. oFilemon jest obiektem typu Kot,
dokładnie tak samo jak nMasa jest zmienną typu unsigned int.
Obiekt to pojedyncze, indywidualne wystąpienie klasy.
Dostęp do zasobów klasy
Jeśli macie zdefiniowany obiekt Kot (np. oFilemon) to dostęp do jego
zmiennych wewnętrznych i funkcji odbywa się za pomocą operatora kropki ( .
).
Przykład
Jeżeli chcecie nadać zmiennej wewnętrznej nJegoWaga wartość 50 to trzeba
napisać:
oFilemon.nJegoWaga = 50;
Podobnie wywołuje się funkcje wewnętrzne:
oFilemon.Miaucz() ;
Przypisywanie do obiektów
W C++ nie przypisuje się wartości do typów, tylko do zmiennych. W żadnym
wypadku nie można napisać tak:
int = 5 ;
Kompilator
zwróci
błąd
, ponieważ nie
można przypisać 5 do
typu całkowitego (int).
Zamiast tego, trzeba zdefiniować zmienną typu int i dopiero jej nadać
wartość 5.
int x ; //definicja x jako int
x = 5 ; //przypisanie wartości 5 do x
Można powiedzieć, że zapis ten jest skrótem rozkazu "Przypisz 5 do zmiennej
x, która jest typu int". Podobnie jak poprzednio również w przypadku klasy
nie można napisać tak:
Najpierw trzeba zdefiniować obiekt typu Kot i dopiero wtedy można przypisać
5 do odpowiedniej zmiennej wewnętrznej.
Kot.nWiek = 5 ;
Kot oFilemon;
oFilemon.nWiek = 5;
Przykład
Przykład
Prywatne kontra publiczne
Przy deklaracji klas używa się różnych słów kluczowych. Jednymi z
ważniejszych są:
Wszystkie elementy klasy -
dane i metody - są domyślnie
traktowane jako prywatne.
private
Oznacza to, że dostęp do nich
może być realizowany tylko
poprzez metody danej klasy.
public
Do elementów publicznych ma
się dostęp bezpośredni, we
wszystkich
obiektach
danej
klasy.
Przykład:
class Kot
{
unsigned int nWiek;
unsiqned int nWaga;
Miaucz () ;
};
W tej deklaracji, wszystkie trzy elementy
klasy: nWiek, nWaga i Miaucz() są
prywatne (ponieważ domyślnie przyjmuje
się, że jeśli nie określi się tego jawnie, to
elementy klasy są prywatne).
Kot oFilemon;
oFilemon.nWiek = 5;
Kompilator zgłosi komunikat błędu.
Próba dostępu do prywatnych danych
Krótko mówiąc, definicja klasy Kot mówi
kompilatorowi, że dostęp do jej elementów
może być realizowany tylko poprzez metody
klasy Kot.
Uwaga: Aby mieć dostęp do elementów klasy Kot trzeba powiedzieć
kompilatorowi, które informacje ma traktować jako publiczne.
class Kot
{
public:
unsiqned int nWiek;
unsigned int nMasa;
Miaucz () ;
};
Teraz nWiek, nMasa i Miaucz() są
publiczne.
Linia:
oFilemon.nWiek = 5;
skompiluje się bez problemów.
Zawsze jednak musicie stworzyć funkcje publiczne (metody dostępu), dzięki
którym będziecie mogli wykonywać operacje na danych przechowywanych w
obiektach klasy.
Metody dostępu to publiczne funkcje danej klasy, dzięki którym inne
części programu będą mogły pobierać i ustawiać wartości zmiennych
wewnętrznych. Funkcje dostępu pozwalają na oddzielenie od siebie
problemu przechowywania danych od problemu ich wykorzystywania.
Pozwala to na zmianę sposobu przechowywania informacji bez
jakichkolwiek zmian funkcji, które z nich korzystają.
Dzięki słowom kluczowym private i public można dzielić deklarację
klasy na bloki publiczne i prywatne.
Starajcie się, żeby elementy danej klasy były prywatne.
class Kot
{
public:
unsiqned int nWiek;
unsigned int nWaga;
Miaucz () ;
};
Kot oFilemon;
oFilemon.nWiek = 8 ;
oFilemon.nWaga = 18 ;
oFilemon.Miaucz() ;
Przykład
class Samochod
{
public: //elementy publiczne
void Uruchom();
void Przyspieszaj();
void Hamuj();
void UstawRocznik (int nRok );
int PobierzRocznik();
private: //elementy prywatne
int nRok ;
char model[255];
}; //koniec deklaracji
Samochod oStary; //stwórz obiekt
int nKupiony; //lokalna zmienna typu int
oStary.UstawRocznik ( 84 ) ; //przypisz 84 do nRok
nKupiony = oStary.PobierzRocznik() ;//przypisz do zmiennej
//nKupiony wartość 84
oStary.Uruchom() ; //wywołaj metodę Uruchom()
Porównajmy go z tym przykładem:
Implementacja metod klasy
Definicja funkcji klasy składa się kolejno z nazwy tej klasy, dwóch
dwukropków, nazwy funkcji i listy jej parametrów.
słowa
public,
które
informuje
kompilator, że wszystko co po nim
występuje ma być traktowane jako
publiczne.
PobierzWiek () pozwala na dostęp
do zmiennej prywatnej nWiek
funkcja UstawWiek() pobiera jako
parametr, wartość typu całkowitego i
przypisuje
ją
do
zmiennej
wewnętrznej nWiek.
Zwróćmy uwagę, że funkcja main ()
nie ma możliwość bezpośredniej
zmiany wartości ani odczytania
zmiennej nWiek, ponieważ zmienna
ta jest prywatna. Funkcja main ()
wykorzystuje metodę PobierzWiek
(), która, jako funkcja wewnętrzna
klasy Kot, ma dostęp do zmiennych
w klasie Kot. Dzięki tej funkcji można
przekazać wartość zmiennej nWiek
do funkcji main ().
Konstruktory i destruktory
Zmienną typu int można zdefiniować na dwa sposoby. Można zdefiniować
zmienną i nadać jej wartość później, gdzieś w programie:
int nMasa; // definicja zmiennej
... // inne instrukcje
nMasa=7; // przypisanie wartości
Można również zdefiniować zmienną i od razu nadać jej wartość:
int nMasa = 7;
Inicjalizacja łączy w sobie definicję zmiennej z operacją przypisania wartości.
Nie ma żadnych przeszkód, aby wartość tę później zmienić. Inicjalizacja
zmiennej daje gwarancję, że nie będzie ona miała wartości nieustalonej.
Jak można inicjalizować zmienne wewnętrzne klasy?
Otóż w klasach występują specjalne funkcje wewnętrzne nazywane
konstruktorami
. Konstruktor może pobierać parametry, nie może
natomiast zwracać wartości (zawsze void). Konstruktor klasy musi
nazywać się tak samo jak klasa.
Jeśli deklaruje się konstruktor to powinno się zadeklarować również
destruktor
.
Tak jak konstruktor tworzy i inicjalizuje obiekt, tak destruktor czyści
miejsce w pamięci (np. zwalnia zarezerwowaną pamięć). Nazwa
destruktora musi być taka jak nazwa klasy poprzedzona znakiem tyldy
(). Destruktor nie pobiera żadnych argumentów ani nie zwraca wartości.
Przykładowa deklaracja destruktora klasy Kot wygląda
tak:
Kot ();
Konstruktory domyślne
Konstruktor bez parametrów nazywany jest konstruktorem domyślnym.
Jeśli napisze się:
kompilator pozwala na pominięcie nawiasów. Zostanie wywołany domyślny
konstruktor, nie pobierający żadnych parametrów.
Kot oFilemon(5) ;
Kot oFilemon ;
to wymusza wykorzystanie konstruktora klasy Kot pobierającego jeden
parametr (w tym przypadku o wartości 5). Natomiast w przypadku takim:
Konstruktory tworzone przez kompilator
Jeśli nie zadeklaruje się żadnego konstruktora, to kompilator automatycznie
stworzy konstruktor domyślny (konstruktor domyślny nie pobiera żadnych
parametrów). Domyślny konstruktor, stworzony przez kompilator, nie robi nic.
To tak, jakby się stworzyło konstruktor, nie pobierający parametrów i nie
posiadający treści:
Kot :: Kot ()
{
}
W tym miejscu należy zwrócić uwagę na dwie ważne rzeczy:
Konstruktor domyślny to dowolny konstruktor nie pobierający
parametrów. Nie ma znaczenia czy zadeklaruje się go samemu, czy
zrobi to kompilator.
Jeżeli zadeklaruje się jakikolwiek konstruktor (nie ważne czy pobiera on
parametry, czy nie) to kompilator nie stworzy konstruktora domyślnego.
W tym wypadku, jeśli chce się mieć konstruktor domyślny, trzeba go
samemu zadeklarować.
Jeżeli nie zadeklaruje się destruktora, to podobnie jak w przypadku
konstruktora, kompilator stworzy domyślny destruktor, nie wykonujący
żadnych czynności:
Kot :: Kot()
{
}
Zapamiętajcie, że jeżeli zadeklarujecie konstruktor to zadeklarujcie
również destruktor (nawet jeśli jest on pusty). Nie zabiera to wiele
czasu, a czyni program bardziej eleganckim i czytelnym.
Dodany
został
konstruktor,
pobierający wartość całkowitą.
Deklaracja destruktora (destruktor
nie pobiera parametrów). Zarówno
konstruktor, jak i destruktor nigdy nie
zwraca żadnej wartości.
Funkcje wewnętrzne typu const
Jeśli funkcję wewnętrzną zadeklaruje się jako const, to gwarantuje to, że
nie będzie ona zmieniać wartości żadnej zmiennej wewnętrznej danej
klasy.
Przykład
Deklaracji funkcji wewnętrznej Fun (), nie pobierającej żadnych argumentów
i zwracającej void:
void Fun () const;
Funkcje dostępu często są deklarowane jako const. Stworzona wcześniej
klasa Kot miała dwie funkcje dostępu:
void UstawWiek (int nWiek) ;
int PobierzWiek();
UstawWiek () nie może być
zadeklarowana jako const,
ponieważ zmienia wartość
zmiennej nJegoWiek.
Druga funkcja, PobierzWiek () może, a wręcz powinna być zadeklarowana
jako const, ponieważ nie modyfikuje żadnej zmiennej w klasie, a jedynie
zwraca aktualną wartość zmiennej nJegoWiek.
void UstawWiek (int nWiek) ;
int PobierzWiek()const;
UWAGA: Jeżeli zadeklaruje się funkcję jako const, a późniejsza
implementacja funkcji zmieni obiekt danej klasy (poprzez zmianę
wartości należącej do klasy), to kompilator zgłosi komunikat błędu.
Jeżeli zmieni się funkcję PobierzWiek () tak, aby zliczała ile razy
odczytywaliśmy wartość zmiennej nJegoWiek (za pomocą dodatkowej
zmiennej w klasie, np. nLicznik) to
kompilator wygeneruje błąd
.
Stanie się tak, ponieważ zawartości obiektu klasy Kot zostanie zmieniona w
momencie wywołania funkcji PobierzWiek () .
Przykład
Wykorzystuj funkcje typu const wszędzie, gdzie to jest możliwe. Pozwoli
to kompilatorowi na pomoc w wyszukiwaniu błędów. Usuwanie błędów z
programu będzie przebiegać szybciej i sprawniej.
Implementacja funkcji jako inline
Każdą funkcję można zadeklarować jako inline. Dotyczy to również funkcji
wewnętrznych danej klasy. Słowo kluczowe inline występuje bezpośrednio
przed typem wartości zwracanej przez funkcję.
inline int Kot::PobierzWage()
{
return nJegoWaga;
//zwróć wartość zmiennej
wewnętrznej
}
Innym sposobem uzyskania funkcji typu inline jest umieszczenie definicji
funkcji bezpośrednio w deklaracji klasy. Taka definicja spowoduje
automatyczne stworzenie funkcji typu inline.
class Kot
{
public:
int PobierzWage()
{
return nJegoWaga; //
inline
};
void UstawWage ( int waga ) ;
};
Interfejs a implementacja
Klientami nazywamy te części programu, które tworzą i wykorzystują
obiekty danej klasy. Interfejs klasy (deklarację) można traktować jako
kontrakt między tymi klientami. Mówi on jakie dane są dostępne w klasie
i jak klasa się zachowuje.
W deklaracji klasy Kot ustala się, że każdy Kot (obiekt tej klasy) będzie miał
zmienną wewnętrzną nJegoWiek, która może być zainicjowana poprzez
konstruktor, której wartość może być zmieniona za pomocą funkcji dostępu
UstawWiek () i która może być odczytana za pomocą funkcji PobierzWiek ().
Gwarantuje się również, że każdy Kot będzie umiał zamiauczeć (funkcja
Miaucz () ).
Jeśli zadeklaruje się funkcję PobierzWiek () jako const (powinno się), to
gwarantuje się również, że PobierzWiek () nie zmieni zawartości obiektu Kot.
Przykład
Umieszczanie deklaracji klas w plikach
nagłówkowych
Mimo ze deklaracje można umieszczać w tekstach źródłowych, to nie jest to
dobry zwyczaj. Przyjęła się konwencja umieszczania deklaracji w specjalnych
plikach, zwanych nagłówkowymi. Mają one zazwyczaj tę samą nazwę co
odpowiadający im plik źródłowy. Zmienia się jedynie rozszerzenie na .H lub
.HPP.
Przykładowo, umieszczamy deklarację klasy Kot w pliku KOT. H, a definicje
metod w pliku KOT. CPP. Aby dodać deklarację klasy do treści programu,
musimy dołączyć plik KOT. H do pliku KOT. CPP. Na początku pliku KOT. CPP
wpisujemy linię:
#include "kot.h"
Deklaracja klasy mówi kompilatorowi co to jest za klasa, jakie dane
przechowuje i jakie posiada funkcje. Deklarację klasy nazywamy jej
interfejsem ponieważ mówi ona użytkownikowi jak ma się komunikować
z klasą. Interfejs zazwyczaj jest przechowywany w plikach .H (plikach
nagłówkowych).
Definicja funkcji to informacja dla kompilatora jak dana funkcja działa.
Definicja klasy nazywana jest implementacją metody klasy i jest
przechowywana w plikach .CPP. Szczegóły implementacji dotyczą jedynie
autora klasy. Klienci klasy (czyli te fragmenty programu, które ją
wykorzystują), nie muszą wiedzieć jak funkcje są zaimplementowane.
Listing zawiera implementacje
funkcji wewnętrznych klasy
Kot (wszystkie są typu inline).
Ich funkcjonalność nie uległa
zmianie (w stosunku do
poprzedniej
deklaracji
i
definicji).