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:
#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.