KLASY
Klasa jest typem deklarowanym i definiowanym przez użytkownika. Klasa to zbiór zmiennych połączonych ze zbiorem odpowiadających im funkcji np. Jeden sposób reprezentacji samochodu polega na wymienieniu jego składników: drzwi, koła okna, siedzenia itp. Inny sposób daje nam informacje o możliwościach samochodu. Może on poruszać się, przyspieszać, zwalniać itp. Klasa jest typem hermetycznym polega na łączeniu wszystkich informacji, możliwości, zalet jednostki w jeden obiekt. Hermetyzacja wszystkich informacji o samochodzie ma dla programisty wiele zalet. Wszystko jest zgromadzone w jednym miejscu. Można łatwo dane reprezentować, kopiować, itp. Klienci twojej klasy to inne klasy lub funkcje wykorzystujące twoją klasę. Każda klasa może składać się z dowolnie złożonego zbioru zmiennych i innych klas. Zmienne w klasie określone są jako wewnętrzne zmienne klasy lub jako wewnętrzne funkcje klasy lub częściej jako metody klasy. 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
DEKLAROWANIE KLASY.
Do deklaracji klasy służy słowo class. Po nim podajemy nazwę tworzonej klasy, a następnie w klamrach zmienne wewnętrzne i metody. Deklarację kończy się średnikiem. Oto przykład deklaracji klasy o nazwie Kot:
class Kot
{
public:
int jegoWiek;
float jegoWaga;
Miaucz();
};
Deklaracja tej klasy nie rezerwuje pamięci na nią. Mówi ona kompilatorowi co to jest Kot, jakie dane zawiera (jegoWiek, jegoWaga) i co potrafi robić (Miaucz()). Dodatkowo, deklaracja niesie za sobą informację o rozmiarze klasy Kot, tzn. ile miejsca w pamięci należy zarezerwować na każdego stworzonego Kota. W naszym przypadku jeśli int zajmuje 2 bajty, a float - 4 bajty to każdy Kot będzie zajmował 6 bajtów. Miaucz() nie zajmuje miejsca ponieważ dla metod klasy nie rezerwuje się obszaru pamięci.
DEFINIOWANIE OBIEKTU.
Tak samo jak mamy zmienną typu float lub int lub char itp. tak samo mamy już zadeklarowany typ Kot. Czyli jeśli możemy zdefiniować zmienną o nazwie masa, która jest typu float np.
float masa;
Tak samo możemy zdefiniować zmienną o nazwie filemon, która jest typu Kot:
Kot filemon;
Należy tutaj odróżnić: deklaracja klasy Kot jest jedynie opisem kota (takim, jakiego potrzebujemy), natomiast definicja tyczy się konkretnego obiektu (czyli filemona), który jest niewątpliwie kotem, a wiec typu Kot.
DOSTĘP DO ZASOBÓW KLASY.
Jeśli mamy definiowany obiekt typu Kot (np. filemon) to dostęp do jego zmiennych wewnętrznych i funkcji odbywa się za pomocą operatora kropki ( . ). Jeśli chcemy nadać zmiennej wewnętrznej jegoWaga wartość 5,5 to musimy napisać tak:
filemon.jegoWaga = 5.5;
Podobnie wywołuje się funkcje wewnętrzne:
filemon.Miausz();
PRZYPISYWANIE DO OBIEKTÓW.
Wiemy, ze w żadnym wypadku nie można napisać tak:
int = 5; <-- BŁĄD
To jest błąd ponieważ nie można przypisać 5 do typu całkowitego (int). Prawidłowy zapis jest następujący:
int x;
x = 5;
Podobnie jak poprzednio również w przypadku klasy nie można napisać: np.
Kot.jegoWiek = 5;
Najpierw musi być zdefiniowany obiekt typu kot i dopiero wtedy można przypisać wartość do odpowiedniej zmiennej wewnętrznej:
Kot filemon;
filemon.jegoWiek = 5;
PRYWATNE KONTA PUBLICZNE.
Ale zanim będziemy próbowali dostępu do zasobów klasy i przypisywania do obiektów wyjaśnijmy sobie parę rzeczy. Przy deklaracji klas używa się różnych słów kluczowych. Jednymi z ważniejszych są public i private. Wszystkie elementy klasy - dane i metody - są domyślnie traktowane jako prywatne. Oznacza to, ze dostęp do nich może być realizowany tylko poprzez metody danej klasy. Do elementów publicznych mamy dostęp bezpośredni we wszystkich obiektach danej klasy. Na pierwszy rzut oka, to rozróżnienie może wydawać się nieco niezrozumiałe. Żeby rozwiązać wątpliwości spójrzmy na prosty przykład:
class Kot
{
int jegoWiek;
float jegoWaga;
Miausz();
};
W tej deklaracji, wszystkie trzy elementy klasy: jegoWiek, jegoWaga, i Miaucz() są prywatne (ponieważ domyślnie przyjmuje się, że jeśli nie określi się tego pewnie, to elementy klasy są prywatne). Jeśli teraz napiszemy:
Kot filemon;
filemon.jegoWiek = 5; <-- BŁĄD
Okaże się, że jest to błąd!, ponieważ jest to próba dostępu do prywatnych danych. (kompilator zgłosi komunikat błędu). Krótko mówiąc, definicja klasy Kot mówi kompilatorowi, że dostęp do jego elementów może być realizowany tylko przez metody klasy Kot. Aby mieć dostęp do elementów klasy Kot trzeba powiedzieć kompilatorowi, które informacje ma traktować jako publiczne.
class kot
{
public:
int jegoWiek;
float jegoWaga;
Miaucz();
};
Teraz, jegoWiek, jegoWaga i Miaucz() są publiczne. Teraz dopiero linia:
filemon.jegoWiek = 5;
skompiluje się poprawnie i bez problemów. Na pewno zwróciliście uwagę, że deklaracje, definicje i operacje na klasach są bardzo podobne do deklaracji, definicji i operacji na strukturach. To prawda dlatego, że struktura jest po prostu klasą, której metody są domyślnie publiczne (w przeciwieństwie do klas)
JAK NAJWIĘCEJ ELEMENTÓW PRYWATNYCH.
Starajcie się, aby elementy danej klasy były prywatne. Zawsze jednak musicie stworzyć funkcje publiczne (metody dostępu), dzięki którym inne części programu będą mogły pobierać i ustawiać wartości zmiennych wewnętrznych klasy. Dzięki słowom kluczowym private i public dzielimy deklarację klasy na bloki prywatne i publiczne. Spójrzcie na przykład
class Kot
{
public:
int jegoWiek;
float jegoWaga;
void Miaucz();
};
Kot filemon;
filemon.jegoWiek = 8;
filemon.jegoWaga = 5.5
filemon.Miaucz();
A teraz porównajmy poprzedni program z tym
class samochod
{
public:
void Uruchom();
void Przyspieszaj();
void Hamuj();
void UstawRocznik(int rok);
int PobierzRocznik();
private:
int rok;
char model[255];
};
samochod garbus; <- definicja obiektu o nazwie garbus
int kupiony; <- definicja zmiennej lokalnej kupiony
garbus.UstawRocznik(81); <- przypisz 81 do zmiennej rok
kupiony = garbus.PobierzRocznik(); <- przypisz do zmiennej kupiony 81
garbus.Uruchom(); <- wywołaj metodę uruchom()
IMPLEMENTACJA METODY KLASY.
Każda metoda klasy musi być odpowiednio zdefiniowana. Definicja funkcji klasy składa się kolejno z nazwy tej klasy, dwóch dwukropków, nazwy funkcji i listy jej argumentów. Napiszmy sobie kompletną deklarację klasy Kot i implementację funkcji pozwalających na dostęp do elementów klasy (metody dostępu)
#include <iostream.h> //dla cout
class Kot
{
public:
int PobierzWiek();
void UstawWiek(int wiek);
void Miaucz();
private:
int jegowiek();
};
int Kot::PobierzWiek()
{
return jegoWiek;
}
void Kot::UstawWiek(int wiek)
{
jegoWiek = wiek;
}
void Kot::Miaucz()
{
cout<<”Miau.\n”;
}
int main()
{
Kot filemon;
filemon.UstawWiek(5);
filemon.Miaucz();
cout<<”Filemon jest kotem, który ma ”;
cout<<filemon.pobierzWiek()<<” lat.\n”;
filemon.Miaucz();
return 0;
}
Efektem programu jest:
Miau.
Filemon jest kotem, który ma 5 lat.
Miau.
W programie tym pojawiły się nowe nie używane przez nas do tej pory słowa i znaki:
::, cout, <<
Ponieważ różne elementy programu (struktury i klasy) mogą mieć metody (funkcje składowe klasy) o tej samej nazwie, więc definiując taką metodę musimy podać kompilatorowi do jakiej klasy lub struktury ta metoda należy. W tym celu używa się tzw. operatora zasięgu (podwójny dwukropek) np.
void Kot::Miaucz();
Gdzie:
void - oznacza typ wartości zwracanej przez funkcję miaucz()
Kot - oznacza nazwę klasy
:: - oznacza w tym przypadku, że funkcja zapisana po prawej stronie jest składową klasy o nazwie zapisanej po lewej stronie podwójnego dwukropka
miaucz() - nazwa funkcji składowej klasy Kot (metody klasy Kot)
Ponieważ uczymy się programowania w C++ to musimy zdawać sobie sprawę, że funkcje takie jak printf() czy scanf() są funkcjami języka C. Natomiast odpowiednikami tych funkcji w języku C++ są strumienie: wyjściowy i wejściowy cout i cin. cout - standardowy strumień wyjściowy, pozwala wypisać na ekran. cin - standardowy strumień wejściowy, pozwala pobrać z klawiatury. cout używa operatora << (wstaw do). Operator ten wstawia drugi argument (po prawej stronie znaku) do strumienia podanego jako pierwszy argument (po lewej stronie znaku). Natomiast cin używa operatora >> (weź z). Należy pamiętać, że jeżeli korzystamy z cout i chcemy wypisać na ekranie ciąg znaków to musimy ten ciąg znaków zaznaczyć w (”...”).
KONSTRUKTORY I DESTRUKTORY.
Zmienną typu int można zdefiniować na dwa sposoby. Możemy zdefiniować zmienną i nadać jej wartość później, gdzieś tam w programie np.
int masa;
.
.
.
masa = 7;
Można również zdefiniować zmienną i od razu nadać jej wartość: (zainicjować)
int masa = 7;
No dobrze, ale jak można inicjować zmienne wewnętrzne klasy. Twórcy C++ przewidzieli taką możliwość i dostarczyli użytkownikowi specjalny mechanizm do inicjowania zmiennych tego typu. Najprościej wydaje się, należałoby przed skorzystaniem ze zmiennej zawołanie jakiejś funkcji, która dokonałaby inicjacji zmiennej. Jednak rozwiązanie takie jest podatne na błędy i nieeleganckie. Lepszym sposobem jest pozwolenie projektantowi typu (klasy) na dostarczenie specjalnie wyróżnionej funkcji odpowiedzialnej za inicjację. Gdy taka funkcja istnieje, to przedział pamięci dla zmiennej i jej inicjacja stają się pojedynczą operacją zamiast dwóch odrębnych. Funkcja inicjująca jest nazywana konstruktorem. Konstruktor charakteryzuje się tym, że ma tę samą nazwę co klasa. Konstruktor może pobierać argumenty, nie może natomiast zwracać wartości (zawsze void). Jeśli deklarujemy konstruktor to powinniśmy również zadeklarować tzw. destruktor. Tak jak konstruktor tworzy i inicjuje obiekt tak destruktor czyści miejsce w pamięci (czyli zwalnia zarezerwowaną przez konstruktor pamięć). Nazwa destruktora musi być taka sama jak nazwa klasy ale poprzedzona znakiem tyldy (~). Destruktor nie pobiera żadnych argumentów ani nie zwraca żadnej wartości. Przykład deklaracji destruktora klasy Kot wygląda:
~Kot()
KONSTRUKTORY DOMYŚLNE.
Konstruktor bez argumentów jest konstruktorem domyślnym. Jeśli napiszemy:
Kot filemon(5);
to wymuszamy wykorzystanie konstruktora klasy Kot pobierającego jeden argument (wartość 5). Natomiast w przypadku takim:
Kot filemon;
kompilator pozwala na pominięcie nawiasów. Zostanie wywołany domyślny konstruktor, nie pobierający żadnych argumentów. Napiszemy program z użyciem konstruktorów i destruktorów
#include <iostream.h>
class Kot
{
public:
Kot(int wiek);
~Kot();
int PobierzWiek();
void UstawWiek();
void Miaucz();
private:
int jegoWiek;
};
Kot::Kot(int wiek) <- konstruktor klasy Kot
{
jegoWiek = wiek;
}
Kot::~Kot() <- destruktor - nic nie robi
{
}
int Kot::pobierzWiek()
{
return jegoWiek;
}
void Kot::ustaWwiek(int wiek)
{
jegoWiek = wiek;
}
void Kot::Miaucz()
{
cout<<”Miausz.\n”;
}
int main()
{
kot filemon(5);
filemon.Miaucz();
cout<<”Filemon jest kotem, który ma ”;
cout<<filemon.PobierzWiek()<<” lat.\n”;
filemon.Miaucz();
filemon.UstawWiek(7);
cout<<”Teraz Filemon ma ”;
cout<<filemon.PobierzWiek()<<” lat.\n”;
return 0;
}
Wynik działania programu:
Miau.
Filemon jest kotem, który ma 5 lat.
Miau.
Teraz Filemon ma 7 lat.
W tym programie dodaliśmy konstruktor i destruktor. Konstruktor pobiera wartość całkowitą. Zarówno konstruktor jak i destruktor nigdy nie zwracają żadnej wartości. Implementacja konstruktora (definicja konstruktora) jest podobna do metody UstawWiek(). Nie zwraca ona żadnej wartości. W programie głównym zamieściliśmy definicję obiektu filemon (będącego klasą Kot). Do konstruktora obiektu przekazywana jest wartość 5. Jak widać nie musimy wywoływać funkcji UstawWiek(), ponieważ filemon został stworzony od razu ze zmienną jegoWiek ustawioną na wartość 5. Niżej zmiennej jegoWiek nadana jest wartość 7 wywołując funkcję (metodę) UstawWiek().
FUNKCJE WEWNĘTRZNE TYPU CONST.
Jeśli funkcję wewnętrzną zadeklarujemy jako const, to gwarantujemy, że nie będzie ona zmieniać wartości żadnej zmiennej wewnętrznej danej klasy. Podajmy sobie przykład deklaracji funkcji wewnętrznej jakasFunkcja(), która nie pobiera żadnych argumentów i zwraca void. Oto ona:
void jakasFunkcja() const;
Funkcje dostępu często są deklarowane jako const. Stworzona przez nas klasa kot miała dwie funkcje dostępu:
void UstawWiek(int wiek);
int PobierzWiek() const;
Jeśli zadeklarujesz 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 o błędzie. Np. jeśli zmienimy funkcję PobierzWiek(), tak aby zliczała ile razy odczytywaliśmy wartość zmiennej jegoWiek (za pomocą dodatkowej zmiennej w klasie np. licznik) to kompilator wygeneruje błąd. Stanie się tak, ponieważ zawartość obiektu klasy Kot zostanie zmieniona w momencie wywołania funkcji: PobierzWiek(). Deklarowanie funkcji wewnętrznych jako const (tam gdzie jest to możliwe) świadczy o dobrym stylu programowania. Kompilator sam wychwyci wszystkie błędy związane z niezamierzonymi zmianami zawartości obiektów.
INTERFEJS A IMPLEMENTACJA.
Klientami nazywamy te części programu, które tworzą i wykorzystują obiekty danej klasy. Interfejs klasy (czyli 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. Np. w deklaracji klasy Kot ustalamy, że kot (obiekt tej klasy) czyli w naszym przypadku filemon będzie miał zmienną jegoWiek, która może być inicjowana przez konstruktor, której wartość może być zmieniona za pomocą funkcji odstępu ustawWiek() i która może być odczytywana za pomocą funkcji PobierzWiek(). Gwarantujemy, że każdy kot będzie umiał zamiauczeć (funkcja Miaucz()). Jeśli zadeklarowaliśmy funkcję PobierzWiek() jako const (powinniśmy) to gwarantujemy również, że funkcja PobierzWiek() nie zmieni wartości obiektu kot.
IMPLEMENTACJA FUNKCJI JAKO INLINE.
Programując z użyciem klas, powszechnie korzysta się z wielu małych funkcji. W istocie dostarcza się funkcje tam, gdzie program o tradycyjnej strukturze używałby struktury danych po prostu w pewien typowy sposób. Może to prowadzić do olbrzymich nieefektywności, ponieważ koszt wywołania funkcji jest dużo wyższy, niż kilka odwołań do pamięci, stanowiących treść prościutkiej funkcji. Do rozwiązania tego problemu zaprojektowano udogodnienie w postaci funkcji inline. Metoda zdefiniowana (a nie jedynie zadeklarowana) w deklaracji klasy jest traktowana jako funkcja o atrybucie inline. Innymi słowy dzięki takiemu zaprojektowaniu klasy nie trzeba uwzględniać minimalnego kosztu czasu wykonania. Nawet najdrobniejsza operacja noże być zrealizowana efektywnie np.
class Kot
{
public:
int PobierzWage() {return jegoWaga;} <- inline
void UstawWage(int waga);
};
Zwróćcie uwagę na definicję PobierzWage(). Treść funkcji rozpoczyna się bezpośrednio po jej deklaracji (po nawiasach w prototypie nie stawiamy średnika). Podobnie jak w przypadku zwykłej funkcji, definicja rozpoczyna się od klamry otwierającej i kończy się klamrą zamykającą. Nie koniecznie funkcje typu inline należy deklarować i definiować w obrębie klasy. Można to zrobić poza deklaracją klasy np.
inline int Kot::PobierzWage()
{
return jegoWaga;
}
W tym przypadku słowo kluczowe inline występuje bezpośrednio przed typem wartości zwracanej przez funkcję. Deklarowanie klasy kot z funkcjami typu inline wyglądałaby tak:
#include <iostream.h>
class Kot
{
public:
Kot(int wiek);
~Kot();
int PobierzWiek() {return jegoWiek;}
void UstawWiek(int wiek) {jegoWiek = wiek;}
void Miaucz() {cout<<”Miaucz.\n”;}
private:
int jegoWiek;
};
- 1 -
część publiczna
część prywatna