Klasy

Autor podstrony: Krzysztof Zajączkowski

Stronę tą wyświetlono już: 4379 razy

Klasy są rozszerzeniem funkcjonalności struktur, dzięki nim można nie tylko grupować różne pola typów prostych, struktur, czy innych klas ale również i:

Podstawowa definicja klasy i tworzenie obiektów klasy

Przykładowa prosta definicja klasy z przykładowymi elementami:

#include <iostream> #include <Windows.h> // w zasadzie nie powinno się tego nagłówkowego pliku używać w konsolowych projektach, ja jednak użyłem ze względu na funkcje ZeroMemory i setlocale using namespace std; class nazwa_klasy // klasa typu nazwa_klasy { private: // prywatne pola klasy unsigned int size; int *table; protected: // chronione pola klasy public: // publiczne pola klasy nazwa_klasy():size(0),table(0){cout<<endl<<"Wywołanie konstruktora bezargumentowego";}; // domyślny przykładowy konstruktor klasy, w tym przypadku zdefiniowany wewnątrz definicji klasy nazwa_klasy(unsigned int size); // konstruktory można przeciążać a ich ciała mogą znajdować się na zewnątrz definicji klasy nazwa_klasy(nazwa_klasy &nk); // konstruktor zwany kopiującym inline int GetSize() const {return size;}; // słowo kluczowe inline sprawia, że kompilator może wstawić kod funkcji w miejscu jej wystąpienia co zapobiega skakaniu po pamięci co może okazać się czasochłonne, natomiast sama funkcja jest metodą klasy zwracającą rozmiar tablicy zapisany w polu size void SetSize(unsigned int size); // metoda ustawiająca int operator [](unsigned int k) const {return table[k];}; void operator ()(unsigned int index, int value){if(index<size)table[index]=value;}; ~nazwa_klasy(); // tak deklaruje się destruktor klasy }; // nie zapominaj o średniku bo będzie błąd // zewnętrzna definicja konstruktora klasy nazwa_klasy::nazwa_klasy(unsigned int size){ this->size = size; // przypisanie rozmiaru tablicy table = new int[size]; // deklarowanie pamięci // Teraz będzie inicjalizacja pamięci zerami ZeroMemory((PVOID)table, // wskaźnik do zerowanej pamięci sizeof(int) * size // rozmiar danych zerowanych ); cout<<endl<<"Wywołano konstruktor z jednym argumentem"; } // zwenętrzna definicja konstruktora kopiującego nazwa_klasy::nazwa_klasy(nazwa_klasy &nk):size(0),table(0){ SetSize(nk.size); for(unsigned int i = 0; i < nk.GetSize(); i++){ table[i] = nk.table[i]; } cout<<endl<<"Wywołano konstruktor kopiujący"; } // zewnętrzna definicja metody ustawiającej void nazwa_klasy::SetSize(unsigned int size){ if(this->size == size) // gdy obecny rozmiar tablicy jest taki sam co deklarowany to return ; // wychodź czym prędzej z funkcji if(table){ // jeżeli tablica ma przypisaną pamięć delete table; // to zwalniaj ją czym prędzej table = 0; // i zeruj adres } this->size = size; // podstawianie do pola klasy size wartości zmiennej size if(size){ // jeżeli nowy rozmiar pamięci jest różny od zera table = new int[size]; // deklarowanie pamięci ZeroMemory((PVOID)table, sizeof(int) * size); // zerowanie pamięci } } // zewnętrzna definicja destruktora klasy nazwa_klasy::~nazwa_klasy(){ if(table){ // gdy tablica wskazuje na niezerowy adres w pamięci to delete[] table; // zwalniaj tą pamięć table = 0; // i zeruj (z czystego przyzwyczajenia) adres wskaźnika } } void Function1(nazwa_klasy k){ // ta nic nie robiąca funkcja wywoła konstruktor kopiujący } void Function2(nazwa_klasy &k){ // a ta nie }

Warto zwrócić uwagę w powyższym kodzie ma użycie wskaźnika this, który jest domyślnym elementem obiektu klasy umożliwiającym wewnątrz takiego obiektu klasy pozyskiwanie wskaźnika klasy co często się wykorzystuje również w przypadkach gdy argument danej metody przysłoni nazwę pola klasy (przykład z powyższego kodu: this->size = size;. Równie ważnym elementem powyższego kodu jest słowo kluczowe inline, które jest sugestią dla kompilatora, aby jeżeli to jest możliwe wstawił kod funkcji bezpośrednio w miejsce jego wystąpienia, co oszczędza czas bo program nie skacze wtedy do miejsca w pamięci, gdzie została zapisana dana metoda. Z kolei słowo kluczowe const użyte w powyższym kodzie oznacza, że metoda nie będzie modyfikowała pól klasy w swoim ciele (przykład: inline int GetSize() const{return size;};).

Wszystko to, co zostało zapisane powyżej jest definicją klasy typu nazwa_klasy, obiekty klasy typu nazwa_klasy można utworzyć w następujący sposób:

int main(){ setlocale(LC_CTYPE,"Polish"); nazwa_klasy k1; // tak tworzy się obiekt z wywołaniem konstruktora bezargumentowego nazwa_klasy k2(10u); // tak tworzy się obiekt z wywołaniem konstruktora z jednym argumentem nazwa_klasy k3(k3); // jawne wywołanie konstruktora kopiującego Function1(k3); // ta funkcja wywoła konstruktor kopiujący Function2(k3); // ta funkcja nie wywoła konstruktora kopiującego cout<<endl<<endl<<"Rozmiar tablicy dynamicznej wewnątrz klasy k2: "<<k2.GetSize()<<endl; // wywoła metodę, która zwróci rozmiar tablicy dynamicznej, gdyby spróbować się bezpośrednio odwołać do pola size obiektu klasy nazwa_klasy w sposób następujący: // cout<<k2.size; // to spowodowałoby błąd ponieważ pole to jest w sekcji prywatnej w definicji klasy typu nazwa_klasy nazwa_klasy *ptk = new nazwa_klasy(5u); // tak dynamicznie tworzy się obiekt klasy nazwa_klasy z wywołaniem konstruktora z argumentem cout<<endl<<endl<<"Rozmiar tablicy dynamicznej wewnątrz wskaźnika klasy ptk "<<ptk->GetSize(); ptk->SetSize(20u); cout<<endl<<endl<<"Ustawianie wartości tablicy dynamicznej wewnątrz wskaźnika klasy ptk i wypisanie jej elementów:"<<endl; for(int i = 0; i < ptk->GetSize(); i++){ (*ptk)(i,i); } for(int i = 0; i < ptk->GetSize(); i++){ cout<<endl<<(*ptk)[i]; } cout<<endl<<endl<<"Wyświetlenie rozmiaru tablicy dynamicznej wewnątrz wskaźnika klasy ptk:"<<endl; cout<<endl<<ptk->GetSize(); delete ptk; // tak zwalnia się pamięć obiektu klasy a tym samym wywołuje się destruktor, który zwolni zadeklarowaną wewnątrz klasy pamięć cout<<"Wcisnij enter, aby zamknac program..."; cin.get(); return 0; }

Bardzo ważnym konstruktorem klasy jest konstruktor kopiujący. Ten konstruktor jest wywoływany gdy tworzona jest kopia obiektu klasy. Problem polega na tym, że gdy dany typ klasy zawiera w swoim wnętrzu pamięć zadeklarowaną dynamicznie, to standardowy konstruktor kopiujący przepisze adres dynamicznie przydzielonej pamięci a nie wykona kopii jego zawartości. Jest to ważne, bo gdyby usunąć konstruktor kopiujący z definicji klasy nazwa_klasy to po wywołaniu standardowego konstruktora kopiującego w funkcji Function1 i zakończeniu tejże funkcji doszłoby do uruchomienia destruktora, który z kolei zwolniłby pamięć oryginalnego obiektu. Mało tego, każda próba odwołania się do pamięci dynamicznej obiektu klasy k3 po wywołaniu funkcji Function1 doprowadziłaby do błędu polegającemu na próbie odwołania się do nieprzydzielonej pamięci. Destruktor klasy k2 również wywołałby błąd próby zwolnienia pamięci, która już wcześniej została zwolniona, a wszystko to przez nie obsłużenie konstruktora kopiującego.

Prywatne, chronione i publiczne pola klasy i przyjaźń

Powyższy kod nie ma jakiejś większej racji bytu, gdyż jego celem jest jedynie zademonstrowanie konstrukcji klas. Ważne jest, aby zrozumieć, że dostęp do prywatnych (private) i chronionych (protected) metod i pól klasy jest zabroniony z zewnątrz. Jedynie publiczne (public) pola i metody klasy są dostępne na zewnątrz.

Do prywatnych (private) i chronionych (protected) pól klasy mogą mieć dostęp wszystkie klasy i funkcje, które mają zadeklarowaną przyjaźń z daną klasą. Przykład deklaracji przyjaźni klasy typu point2d z klasą typu circle:

class circle; // potrzebne żeby klasa point2d mogła się zaprzyjaźnić z klasą circle class point2d; // konieczne aby funkcja Determinant mogła widzieć klasę point2d double Determinant(point2d &p1, point2d &p2); // nagłówek funkcji konieczny by klasa point2d widziała tę funkcję i mogła się z nią zaprzyjaźnić class point2d{ private: double x; protected: double y; public: point2d(): x(0),y(0) // zerowanie wartości pól klasy w liście {}; point2d(double x, double y): x(x),y(y) // ustaiwanie pól klasy w liście {}; ~point2d(){}; friend class circle; // zaprzyjaźniamy się z klasą circle friend double Determinant(point2d &p1, point2d &p2); // przyjaźń klasy z funkcją Determinant zawarta została }; // funkcja wykorzystująca przyjaźń z klasą point2d double Determinant(point2d &p1, point2d &p2) { return p1.x*p2.y-p2.x*p1.y; } class circle{ private: double ray; // promień okręgu public: point2d cp; // współrzędna środka okręgu circle(){}; circle(double x, double y, double ray): cp(x,y) // wywołanie konstruktora klasy w listingu ,ray(ray) //ustawianie pola ray {}; void operator = (point2d p){cp.x = p.x; cp.y = p.y;}; // operator podstawienia wykorzystuje przyjaźń z klasą circle, dzięki której może odwoływać się do prywatnych i chronionych pól klasy point2d ~circle(){}; };

Dziedziczenie

W poprzednim przykładzie klasa typu circle zwierała w sobie pole klasy typu point2d. Nie trudno jest jednak zauważyć, że sama klasa point2d zawiera w sobie część opisu właściwości klasy circle, a co za tym idzie można wykorzystać tutaj mechanizm dziedziczenia w sposób następujący:

#include <iostream> using namespace std; class circle; // potrzebne żeby klasa point2d mogła się zaprzyjaźnić z klasą circle class point2d{ private: double x; double y; public: point2d(): x(0),y(0) // zerowanie wartości pól klasy w liście {}; point2d(double x, double y): x(x),y(y) // ustaiwanie pól klasy w liście {}; inline int GetX() const{return x;}; // metoda dostępu do kopii danych zmiennej x, modyfikator dostępu const oznacza, ze funkcja nie będzie zmieniała pól klasy inline int GetY() const{return y;}; // metoda dostępu do kopii danych zmiennej y void SetX(int x){this->x = x;}; // metoda ustawiania pola x void SetY(int y){this->y = y;}; // metoda ustawiania pola y void Set(){cout<<"Podaj wspolzedna x: ";cin>>x;cout<<"Podaj wspolzedna y: ";cin>>y;}; // metoda klasy wczydująca informacje o współrzędnych z klawiatury void Write(){cout<<"Punkt: x= "<<x<<" y= "<<y<<endl;}; // metoda wypisująca dane o punkcie friend class circle; // zaprzyjaźnienie klasę circle z klasą point2d ~point2d(){}; }; class circle : public point2d // tutaj mamy dziedziczenie klasy point2d, w C++ można dziedziczyć wiele klas listując je po przecinku w tym miejscu, słowo public oznacza pełen dostęp do metod i pól klasy dziedziczonej przez klasę dziedziczącą { private: double ray; // promień okręgu public: circle(){}; circle(double x, double y, double ray): point2d(x,y) // wywołanie konstruktora klasy dziedziczonej w listingu ,ray(ray) //ustawianie pola ray {}; inline double GetRay() const{return ray;}; // metoda dostępu do kopii danych void SetRay(double ray){this->ray = ray < 0 ? -ray : ray;}; // metoda ustawiania promienia void Set(){ cout<<"Okrag: "; point2d::Set(); // wywołanie przysłoniętej metody wewnętrznej klasy dziedziczonej typu point2d cout<<"Podaj promien: "; cin>>ray; ray= ray < 0 ? -ray : ray; // to żeby nie było ujemnej wartości promienia }; void Write(){ // metoda przysłaniająca metodę klasy dziedziczonej cout<<"Okrag: promien= "<<ray<<" "; point2d::Write(); // wywołanie przysłoniętej funkcji klasy dziedziczonej point2d }; void operator = (point2d p){x = p.x; y = p.y;}; // operator podstawienia wykorzystuje przyjaźń z klasą circle, dzięki której może odwoływać się do prywatnych i chronionych pól klasy point2d ~circle(){}; };

W powyższym kodzie pokazano również wywoływanie metod wewnętrznych klasy dziedziczonej point2d::Set() oraz point2d::Write() wewnątrz metod klasy typu circle noszących nazwy Write() i Set(). Stwórzmy po jednym obiekcie z każdego typu utworzonych deklaracji klas i się nimi pobawmy:

int main(){ point2d p; // tworzenie punktu o domyślnych współrzędnych zerowych p.Write(); // wypisywanie współrzędnych circle c(10.,20.,50.); // tworzenie okręgu o podanych parametrach c.Write(); // wypisywanie informacji o okręgu c.Set(); // wczytywanie z klawiatury parametrów okręgu c.Write(); // wypisywanie informacji o okręgu p = c; // a tu już bardzo ciekawa rzecz się dzieje, bezpośrednie przepisanie wartości punktu z obiektu c do obiektu p p.Write(); // wypisywanie wartości cout<<"Wcisnij enter, aby zamknac program..."; cin.get(); return 0; }

Metody statyczne klas

Metody statyczne tworzy się używając słowa kluczowego static, słowo to oznacza, że taka metoda może być wywoływana bez tworzenia obiektu klasy. Ktoś może zapytać but how it? (ale jak to?), ano dla przykładu tak to:

class point2d{ private: double x; double y; public: point2d(): x(0),y(0) // zerowanie wartości pól klasy w liście {}; point2d(double x, double y): x(x),y(y) // ustaiwanie pól klasy w liście {}; inline int GetX() const{return x;}; // metoda dostępu do kopii danych zmiennej x, modyfikator dostępu const oznacza, ze funkcja nie będzie zmieniała pól klasy inline int GetY() const{return y;}; // metoda dostępu do kopii danych zmiennej y void SetX(int x){this->x = x;}; // metoda ustawiania pola x void SetY(int y){this->y = y;}; // metoda ustawiania pola y void Set(){cout<<"Podaj wspolzedna x: ";cin>>x;cout<<"Podaj wspolzedna y: ";cin>>y;}; // metoda klasy wczydująca informacje o współrzędnych z klawiatury void Write(){cout<<"Punkt: x= "<<x<<" y= "<<y<<endl;}; // metoda wypisująca dane o punkcie ~point2d(){}; static double Determinant(double x1, double y1, double x2, double y2){return x1 * y2 - y1 * x2;}; //statyczna funkcja obliczająca wyznacznik dwóch punktów (wektorów) }; int main(){ cout<<point2d::Determinant(10.,5.,2.,5.); // obliczanie i wyświetlanie wyznacznika dwóch punktów (wektorów) cout<<"Wcisnij enter, aby zamknac program..."; cin.get(); return 0; }

Jak widać metoda point2d::Determinant została wywołana zewnętrznie pomimo tego, że nie utworzono żadnego obiektu klasy. Lecz spójrzmy tylko, czy ta funkcja jest związana plami klasy? Nie! Ta funkcja wykonuje obliczenia, które są powiązane z punktami (obliczenie wyznacznika z dwóch punktów) i dlatego została ona zgrupowana pod przestrzenią nazw klasy point2d. Ta klasa nie może odwoływać się do pól klasy ani nie może wykorzystywać wskaźnika obiektu klasy this.

Propozycje książek