The great thing about Object Oriented code is that it can make small, simple problems look like large, complex ones.
Do ukończenia ćwiczeń z tego laboratorium przydatne będzie zapoznanie się z informacjami dotyczącymi Makefile znajdującymi się tutaj. Należy to zrobić przed rozpoczęciem zajęć!
Klasa definiuje nowy typ danych podobnie jak struktura, ale w znacznie szerszym znaczeniu.
Obiekt to instancja danej klasy: jest to utworzona zmienna o typie określonym przez daną klasę.
Struktury są swego rodzaju kontenerami danych, klasy są bytami bardziej samoistnymi. Właściwości klas:
Deklaracja i definicja klasy powinny byc rozdzielone. Deklaracja klasy umieszczana jest w pliku nagłówkowym o rozszerzeniu .h a definicja w osobnym pliku o rozszerzeniu .cpp. Definiując metody klasy, używamy operatora zasięgu (::), aby wskazać, ze definicja dotyczy metody klasy, a nie zwyczajnej funkcji. Np.
... void Punkt::wyswietl(){ cout << "(" << x << ";" << y << ")";} ...
Ponieważ programy składają się często z wielu plików, istnieje zagrożenie, że podłączając pliki nagłówkowe klas kompilator mógłby dołączyć deklaracje klasy kilkukrotnie. Przed takim niebezpieczeństwem służą dyrektywy preprocesora #ifndef, #define, #endif.
Poniżej: jeśli nie jest zdefiniowane PUNKT_H, to zdefiniuj PUNKT_H. To gwarantuje, że deklaracja klasy zostanie dołączona podczas kompilacji do całego programu tylko raz. Przyjęło się, że dyrektywach preprocesora #ifndef, #define używa się nazwy pliku wraz z rozszerzeniem. Gwarantuje to pewną unikalność definiowanych nazw.
// Deklaracja znajduje siew pliku punkt.h #ifndef PUNKT_H #define PUNKT_H class Punkt{ private: double x, y; public: //Konstruktor bezparametrowy Punkt(); //Konstruktor parametrowy Punkt(double _x, double _y); //Destruktor ~Punkt(); double distance(Punkt inny); void wyswietl(); // Krótkie funkcje mogą być zdefiniowane w pliku // nagłówkowym i będą traktowane jako funkcje inline double getX(){return x;} double getY(){return y;} void setX(double _x){x=_x;} void setY(double _y){y=_y;} }; #endif
//Definicja znajduje się w pliku punkt.cpp #include "punkt.h" #include <math.h> #include <iostream> using namespace std; /* Aby wskazać, ze definicja funkcji dotyczy metody danej klasy stosujemy tzw. operator zasięgu - "::" */ //Specjalna inicjalizacja zmiennych. Zmienne są inicjowane //nim zostanie wywołane ciało konstruktora Punkt::Punkt():x(0),y(0){ cout << "Konstruktor bezparametrowy" << endl; } Punkt::Punkt(double _x, double _y){ cout << "Konstruktor parametrowy" << endl; x = _x; y = _y; } Punkt::~Punkt(){ cout << "Destruktor! Nic nie robie, bo nie musze zwalniać pamięci!"; cout << endl; } double Punkt::distance(Punkt inny){ return sqrt(pow(x-inny.x,2)+pow(y-inny.y,2)); } void Punkt::wyswietl(){ cout << "(" << x << ";" << y << ")"; }
Pola to zmienne znajdujące się wewnątrz definicji klasy. Pola mogą być dowolnego typu. Zadaniem konstruktora jest zainicjowanie ich odpowiednimi wartościami.
Metody to funkcje wykonujące jakieś operacje charakterystyczne dla danej klasy. Metody zdefiniowane w pliku nagłówkowym są traktowane przez kompilator jako funkcje inline. Może to poprawić wydajność w przypadku małych funkcji, ale pogorszyć w przypadku nagminnego stosowania tego mechanizmu.
Do pól i metod dostajemy się dzięki następującym operatorom:
Ponieważ klasy są bytami bardzo złożonymi, a ich polami mogą być w szczególności dynamicznie zaalokowane tablice, konieczne było zaprojektowanie mechanizmu tworzenia i usuwania obiektów danych klas.
Konstruktor i destruktor nie zwracają żadnego typu.
Każdy obiekt klasy ma pośród swoich pól wskaźnik o nazwie this, który wskazuje na niego samego. Wskaźnika this nie trzeb deklarować - jest to wbudowany mechanizm.
Istnieje kilka zastosowań wskaźnika. Poniżej przedstawione zostały dwa najpopularniejsze:
Punkt::Punkt( double x, double y){ this->x = x; this->y = y; }
Punkt p; p.setX(10).setY(12);
Pomyśl jak zaimplementować taką funkcjonalność z użyciem wskaźnika this. Uwaga: przydadzą się referencje!
Każda klasa może chronić dostępu do swoich pól i metod za pomocą trzech modyfikatorów dostępu:
Aby wykorzystać klasę w pisanym programie konieczne jest włączenie jej pliku nagłówkowego za pomocą dyrektywy #include. Obiekty tworzymy tak samo jak zwykłe zmienne. Jeśli istnieje konstruktor parametrowy, to parametry podajemy w nawiasach po nazwie zmiennej.
#include <iostream> #include "punkt.h" using namespace std; int main(void){ Punkt p; Punkt p2(12,34); Punkt *ptrP = new Punkt(3,4); p2.wyswietl(); cout << ptrP->distance(p2) << endl; delete ptrP; }
Początkujący programiści mają problemy z wyodrębnianiem obiektów. Np. Czy gdybyśmy chcieli napisać program rozwiązujący problem skoczka szachowego z użyciem obiektów - co powinno być obiektem a co nie? Jakie klasy powinny mieć jakie metody? Czy skoczek powinien być obiektem i mieć metodę skacz przyjmującą jako parametr szachownicę, czy szachownica powinna być obiektem i mieć metodę symuluj? A może i szachownica i skoczek powinny być obiektami?
Jeśli coś da się wyodrębnić jako obiekt, to powinno się to zrobić.
#ifndef DTAB_H #define DTAB_H class DTab{ private: double * tab; int length; int last; // Metoda rozszerzajaca rozmiar tablicy do rozmiaru podanego jako parametr void resize(int newSize); public: // Konstruktor bezparametrowy. Powinien tworzyć tablicę o rozmiarze 10. (wykorzystaj metode resize) DTab(); // Tworzy tablice o rozmiarze podanym jako parametr. (wykorzystaj metode resize) DTab(int initLength); // Destruktor. Uwaga! Tablicę tworzymy dynamicznie, czyli tutaj jest wymagany! ~DTab(); // Dodaje element do na koniec tablicy. Jeśli tablica jest za mała // rozszerza ją. (wykorzystaj metode resize) void add(double element); / Pobiera element z tablicy z podanego indexu double get(int index); // Ustawia element o danym indeksie na daną wartość void set(double element, int index); // wyświetla tablice. void print(); }; #endif