Klasy

Stronę tą wyświetlono już: 297 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:

  • sterować dostępem do poszczególnych pól i metod klasy;
  • dodają możliwość przypisania funkcji (zwanych metodami) do danego typu klasy;
  • tworzenie własnych funkcji inicjalizujących dane tworzonego obiektu klasy (zwane konstruktorami, gdzie wybrany typ konstruktora jest używany przy deklarowaniu pamięci);
  • tworzenie własnej funkcji (destruktora klasy, który zostanie wywołany automatycznie przy zwalnianiu pamięci);
  • dziedziczyć - łączyć opisy bardziej ogólne danego typu deklaracji klasy z innymi bardziej szczegółowymi opisami;
  • tworzyć interfejsy z metodami wirtualnymi co sprzyja realizacji zagadnień związanych z polimorfizmem.

Podstawowa definicja klasy i tworzenie obiektów klasy

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

Listing 1
  1. #include <iostream>
  2. #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
  3. using namespace std;
  4. class nazwa_klasy // klasa typu nazwa_klasy
  5. {
  6. private:
  7. // prywatne pola klasy
  8. unsigned int size;
  9. int *table;
  10. protected:
  11. // chronione pola klasy
  12. public:
  13. // publiczne pola klasy
  14. 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
  15. nazwa_klasy(unsigned int size); // konstruktory można przeciążać a ich ciała mogą znajdować się na zewnątrz definicji klasy
  16. nazwa_klasy(nazwa_klasy &nk); // konstruktor zwany kopiującym
  17. 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
  18. void SetSize(unsigned int size); // metoda ustawiająca
  19. int operator [](unsigned int k) const {return table[k];};
  20. void operator ()(unsigned int index, int value){if(index<size)table[index]=value;};
  21. ~nazwa_klasy(); // tak deklaruje się destruktor klasy
  22. }; // nie zapominaj o średniku bo będzie błąd
  23. // zewnętrzna definicja konstruktora klasy
  24. nazwa_klasy::nazwa_klasy(unsigned int size){
  25. this->size = size; // przypisanie rozmiaru tablicy
  26. table = new int[size]; // deklarowanie pamięci
  27. // Teraz będzie inicjalizacja pamięci zerami
  28. ZeroMemory((PVOID)table, // wskaźnik do zerowanej pamięci
  29. sizeof(int) * size // rozmiar danych zerowanych
  30. );
  31. cout<<endl<<"Wywołano konstruktor z jednym argumentem";
  32. }
  33. // zwenętrzna definicja konstruktora kopiującego
  34. nazwa_klasy::nazwa_klasy(nazwa_klasy &nk):size(0),table(0){
  35. SetSize(nk.size);
  36. for(unsigned int i = 0; i < nk.GetSize(); i++){
  37. table[i] = nk.table[i];
  38. }
  39. cout<<endl<<"Wywołano konstruktor kopiujący";
  40. }
  41. // zewnętrzna definicja metody ustawiającej
  42. void nazwa_klasy::SetSize(unsigned int size){
  43. if(this->size == size) // gdy obecny rozmiar tablicy jest taki sam co deklarowany to
  44. return ; // wychodź czym prędzej z funkcji
  45. if(table){ // jeżeli tablica ma przypisaną pamięć
  46. delete table; // to zwalniaj ją czym prędzej
  47. table = 0; // i zeruj adres
  48. }
  49. this->size = size; // podstawianie do pola klasy size wartości zmiennej size
  50. if(size){ // jeżeli nowy rozmiar pamięci jest różny od zera
  51. table = new int[size]; // deklarowanie pamięci
  52. ZeroMemory((PVOID)table, sizeof(int) * size); // zerowanie pamięci
  53. }
  54. }
  55. // zewnętrzna definicja destruktora klasy
  56. nazwa_klasy::~nazwa_klasy(){
  57. if(table){ // gdy tablica wskazuje na niezerowy adres w pamięci to
  58. delete[] table; // zwalniaj tą pamięć
  59. table = 0; // i zeruj (z czystego przyzwyczajenia) adres wskaźnika
  60. }
  61. }
  62. void Function1(nazwa_klasy k){ // ta nic nie robiąca funkcja wywoła konstruktor kopiujący
  63. }
  64. void Function2(nazwa_klasy &k){ // a ta nie
  65. }

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:

Listing 2
  1. int main(){
  2. setlocale(LC_CTYPE,"Polish");
  3. nazwa_klasy k1; // tak tworzy się obiekt z wywołaniem konstruktora bezargumentowego
  4. nazwa_klasy k2(10u); // tak tworzy się obiekt z wywołaniem konstruktora z jednym argumentem
  5. nazwa_klasy k3(k3); // jawne wywołanie konstruktora kopiującego
  6. Function1(k3); // ta funkcja wywoła konstruktor kopiujący
  7. Function2(k3); // ta funkcja nie wywoła konstruktora kopiującego
  8. 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:
  9. // cout<<k2.size;
  10. // to spowodowałoby błąd ponieważ pole to jest w sekcji prywatnej w definicji klasy typu nazwa_klasy
  11. nazwa_klasy *ptk = new nazwa_klasy(5u); // tak dynamicznie tworzy się obiekt klasy nazwa_klasy z wywołaniem konstruktora z argumentem
  12. cout<<endl<<endl<<"Rozmiar tablicy dynamicznej wewnątrz wskaźnika klasy ptk "<<ptk->GetSize();
  13. ptk->SetSize(20u);
  14. cout<<endl<<endl<<"Ustawianie wartości tablicy dynamicznej wewnątrz wskaźnika klasy ptk i wypisanie jej elementów:"<<endl;
  15. for(int i = 0; i < ptk->GetSize(); i++){
  16. (*ptk)(i,i);
  17. }
  18. for(int i = 0; i < ptk->GetSize(); i++){
  19. cout<<endl<<(*ptk)[i];
  20. }
  21. cout<<endl<<endl<<"Wyświetlenie rozmiaru tablicy dynamicznej wewnątrz wskaźnika klasy ptk:"<<endl;
  22. cout<<endl<<ptk->GetSize();
  23. delete ptk; // tak zwalnia się pamięć obiektu klasy a tym samym wywołuje się destruktor, który zwolni zadeklarowaną wewnątrz klasy pamięć
  24. cout<<"Wcisnij enter, aby zamknac program...";
  25. cin.get();
  26. return 0;
  27. }

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:

Listing 3
  1. class circle; // potrzebne żeby klasa point2d mogła się zaprzyjaźnić z klasą circle
  2. class point2d; // konieczne aby funkcja Determinant mogła widzieć klasę point2d
  3. 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ć
  4. class point2d{
  5. private:
  6. double x;
  7. protected:
  8. double y;
  9. public:
  10. point2d():
  11. x(0),y(0) // zerowanie wartości pól klasy w liście
  12. {};
  13. point2d(double x, double y):
  14. x(x),y(y) // ustaiwanie pól klasy w liście
  15. {};
  16. ~point2d(){};
  17. friend class circle; // zaprzyjaźniamy się z klasą circle
  18. friend double Determinant(point2d &p1, point2d &p2); // przyjaźń klasy z funkcją Determinant zawarta została
  19. };
  20. // funkcja wykorzystująca przyjaźń z klasą point2d
  21. double Determinant(point2d &p1, point2d &p2) {
  22. return p1.x*p2.y-p2.x*p1.y;
  23. }
  24. class circle{
  25. private:
  26. double ray; // promień okręgu
  27. public:
  28. point2d cp; // współrzędna środka okręgu
  29. circle(){};
  30. circle(double x, double y, double ray):
  31. cp(x,y) // wywołanie konstruktora klasy w listingu
  32. ,ray(ray) //ustawianie pola ray
  33. {};
  34. 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
  35. ~circle(){};
  36. };

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:

Listing 4
  1. #include <iostream>
  2. using namespace std;
  3. class circle; // potrzebne żeby klasa point2d mogła się zaprzyjaźnić z klasą circle
  4. class point2d{
  5. private:
  6. double x;
  7. double y;
  8. public:
  9. point2d():
  10. x(0),y(0) // zerowanie wartości pól klasy w liście
  11. {};
  12. point2d(double x, double y):
  13. x(x),y(y) // ustaiwanie pól klasy w liście
  14. {};
  15. 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
  16. inline int GetY() const{return y;}; // metoda dostępu do kopii danych zmiennej y
  17. void SetX(int x){this->x = x;}; // metoda ustawiania pola x
  18. void SetY(int y){this->y = y;}; // metoda ustawiania pola y
  19. 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
  20. void Write(){cout<<"Punkt: x= "<<x<<" y= "<<y<<endl;}; // metoda wypisująca dane o punkcie
  21. friend class circle; // zaprzyjaźnienie klasę circle z klasą point2d
  22. ~point2d(){};
  23. };
  24. 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ą
  25. {
  26. private:
  27. double ray; // promień okręgu
  28. public:
  29. circle(){};
  30. circle(double x, double y, double ray):
  31. point2d(x,y) // wywołanie konstruktora klasy dziedziczonej w listingu
  32. ,ray(ray) //ustawianie pola ray
  33. {};
  34. inline double GetRay() const{return ray;}; // metoda dostępu do kopii danych
  35. void SetRay(double ray){this->ray = ray < 0 ? -ray : ray;}; // metoda ustawiania promienia
  36. void Set(){
  37. cout<<"Okrag: ";
  38. point2d::Set(); // wywołanie przysłoniętej metody wewnętrznej klasy dziedziczonej typu point2d
  39. cout<<"Podaj promien: ";
  40. cin>>ray;
  41. ray= ray < 0 ? -ray : ray; // to żeby nie było ujemnej wartości promienia
  42. };
  43. void Write(){ // metoda przysłaniająca metodę klasy dziedziczonej
  44. cout<<"Okrag: promien= "<<ray<<" ";
  45. point2d::Write(); // wywołanie przysłoniętej funkcji klasy dziedziczonej point2d
  46. };
  47. 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
  48. ~circle(){};
  49. };

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:

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

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:

Listing 6
  1. class point2d{
  2. private:
  3. double x;
  4. double y;
  5. public:
  6. point2d():
  7. x(0),y(0) // zerowanie wartości pól klasy w liście
  8. {};
  9. point2d(double x, double y):
  10. x(x),y(y) // ustaiwanie pól klasy w liście
  11. {};
  12. 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
  13. inline int GetY() const{return y;}; // metoda dostępu do kopii danych zmiennej y
  14. void SetX(int x){this->x = x;}; // metoda ustawiania pola x
  15. void SetY(int y){this->y = y;}; // metoda ustawiania pola y
  16. 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
  17. void Write(){cout<<"Punkt: x= "<<x<<" y= "<<y<<endl;}; // metoda wypisująca dane o punkcie
  18. ~point2d(){};
  19. 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)
  20. };
  21. int main(){
  22. cout<<point2d::Determinant(10.,5.,2.,5.); // obliczanie i wyświetlanie wyznacznika dwóch punktów (wektorów)
  23. cout<<"Wcisnij enter, aby zamknac program...";
  24. cin.get();
  25. return 0;
  26. }

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.

Komentarze