complex
Klasy w C++
Klasa jest nowym typem danych zdefiniowanym przez użytkownika.
Wartości tak zdefiniowanego typu nazywamy obiektami.
Najprostsza klasa jest po prostu strukturą (rekordem w Pascalu), czyli paczką kilku różnych zmiennych.
Składnia deklaracji klasy:
. | specyfikator_klasy: | |
. | . | nagłówek_klasy { |
. | nagłówek_klasy: | |
. | . | słowo_kluczowe_klasy |
. | . | słowo_kluczowe_klasy specyfikator_zagnieżdżonej_nazwy |
W tym rozdziale przyjrzymy się definiowaniu klas na przykładzie klasy Zespolona
, której obiekty reprezentują
(oczywiście) liczby zespolone.
Bez klas byłoby tak:
struct Zespolona{ double re, im; };
Dokładnie to samo można wyrazić używając klas:
class Zespolona{ public: double re, im; };
Ale taka definicja nie wystarcza, potrzebujemy operacji na tym typie danych. Możemy je zdefiniować tak:
Zespolona dodaj(Zespolona z1, Zespolona z2){ Zespolona wyn; wyn.re = z1.re + z2.re; wyn.im = z1.im + z2.im; return wyn; }Ma to jednak tę wadę, że trudno się zorientować, czym tak naprawdę jest typ Zespolona (trzeba przeczytać cały program, żeby znaleźć wszystkie definicje dotyczące liczb zespolonych).
W C++ możemy powiązać definicję typu danych z dozwolonymi na tym typie operacjami:
class Zespolona{ public: double re, im; Zespolona dodaj(Zespolona); Zespolona odejmij(Zespolona); double modul(); };
Zauważmy, że:
Zwiększyła się czytelność programu: od razu widać wszystkie operacje dostępne na naszym typie danych.
Zmieniła się liczba parametrów operacji.
Nie podaliśmy (jeszcze) ani treści operacji, ani nazw parametrów.
To co podaliśmy powyżej jest specyfikacją interfejsu typu Zespolona. Oczywiście trzeba też określić implementację (gdzieś dalej w programie).
Zespolona Zespolona::dodaj(Zespolona z){ Zespolona wyn; wyn.re = re + z.re; wyn.im = im + z.im; return wyn; } Zespolona Zespolona::odejmij(Zespolona z){ Zespolona wyn; wyn.re = re - z.re; wyn.im = im - z.im; return wyn; } double Zespolona::modul(){ return sqrt(re*re + im*im); }
Przy poprzedniej definicji klasy Zespolona, można było pisać następujące instrukcje:
Zespolona z; double mod; ..... mod = sqrt(z.re*z.im+z.im*z.im); // Błąd !!!
Nie znamy na razie metody zmuszającej użytkownika do korzystania tylko z dostarczonych przez nas operacji. To bardzo źle, bo:
Upada poprzednio postawiona teza, że wszystkie operacje na typie danych są zdefiniowane tylko w jednym miejscu.
Użytkownik pisząc swoje operacje może (tak jak w przykładzie z mod) napisać je źle.
Projektując klasę, zwykle nie chcemy, żeby użytkownik mógł bez naszej wiedzy modyfikować jej zawartość (przykład ułamek: nie chcielibyśmy, żeby ktoś wpisał nam nagle mianownik równy zero).
Program użytkownika odwołujący się do wewnętrznej reprezentacji klasy niepotrzebnie się od niej uzależnia (np. pola re
nie
możemy teraz nazwać czesc_rzeczywista
).
Na szczęście w C++ możemy temu bardzo łatwo zaradzić. Każda składowa klasy (zmienna lub metoda) może być:
Prywatna (private:
).
Publiczna, czyli ogólnodostępna (public:
).
Domyślnie wszystkie składowe klasy są prywatne, zaś wszystkie składowe struktury publiczne. Jest to zresztą (poza domyślnym trybem dziedziczenia i oczywiście słowem kluczowym) jedyna różnica pomiędzy klasami a strukturami w C++.
Zatem teraz mamy następującą deklarację:
class Zespolona{ private: // tu można pominąć private: double re, im; public: Zespolona dodaj(Zespolona); Zespolona odejmij(Zespolona); double modul(); };
Teraz zapis:
mod = sqrt(z.re*z.re+z.im*z.im); // Błąd składniowy (choć wzór poprawny)jest niepoprawny. Użytkownik może natomiast napisać:
mod = z.modul();
Czy chcemy mieć niezainicjowane obiekty? Oczywiście nie:
{ Zespolona z1; cout << z1.modul(); // Wypisze się coś bez sensu }
Jak temu zaradzić? Można dodać metodę ini(), która będzie inicjować liczbę, ale … to nic nie daje. Dalej nie ma możliwości zagwarantowania, że zmienna typu Zespolona będzie zainicjowana przed pierwszym jej użyciem.
Na szczęście w C++ możemy temu skutecznie zaradzić.
Rozwiązaniem są konstruktory.
Konstruktor jest specjalną metodą klasy.
Ma taką samą nazwę jak klasa.
Nie można podać typu wyniku konstruktora.
Nie można przekazać z niego wyniku instrukcją return
.
Można w nim wywoływać funkcje składowe klasy.
Można go wywołać jedynie przy tworzeniu nowego obiektu danej klasy.
W klasie można (i zwykle tak się robi) zdefiniować wiele konstruktorów.
Konstruktor może mieć (nie musi) parametry.
Konstruktor jest odpowiedzialny za dwie rzeczy:
zapewnienie, że obiekt będzie miał przydzieloną pamięć (to jest sprawa kompilatora),
inicjację obiektu (to nasze zadanie, realizuje je treść konstruktora).
Wyróżnia się kilka rodzajów konstruktorów:
Konstruktor bezargumentowy:
można go wywołać bez argumentów,
jest konieczny, jeśli chcemy mieć tablice obiektów tej klasy.
Konstruktor domyślny:
jeśli nie zdefiniujemy żadnego konstruktora, to kompilator sam wygeneruje konstruktor domyślny (bezargumentowy).
ten konstruktor nie inicjuje składowych typów prostych,
dla składowych będących klasami lub strukturami wywołuje ich konstruktory bezargumentowe,
jeśli składowa będąca klasą lub strukturą nie ma konstruktora bezargumetowego bądź jest on niedostępny, generowanie konstruktora domyślnego kończy się błędem kompilacji.
Konstruktor kopiujący:
można go wywołać z jednym argumentem tej samej klasy, przekazywanym przez referencję,
jeśli żadnego takiego konstruktora nie zdefiniujemy, to kompilator wygeneruje go automatycznie. Uwaga: automatycznie wygenerowany konstruktor kopiujący kopiuje obiekt składowa po składowej, więc zwykle się nie nadaje dla obiektów zawierających wskaźniki!!!
jest wywoływany niejawnie przy przekazywaniu parametrów do funkcji i przy przekazywaniu wyników funkcji!!!
Teraz deklaracja naszej klasy wygląda następująco:
class Zespolona{ private: // tu można pominąć private: double re, im; public: // konstruktory Zespolona(double, double); // operacje Zespolona dodaj(Zespolona); Zespolona odejmij(Zespolona); double modul(); }; Zespolona::Zespolona(double r, double i){ re = r; im = i; } // ... reszta definicji
Jakie są konsekwencje zdefiniowania konstruktora?
Zespolona z; // Błąd! Nie ma już konstruktora domyślnego Zespolona z(3,2); // OK, taki konstruktor jest zdefiniowany.Zatem nie można teraz utworzyć niezainicjowanego obiektu klasy Zespolona!
Każde użycie konstruktora powoduje powstanie nowego obiektu. Można w ten sposób tworzyć obiekty tymczasowe:
double policz_cos(Zespolona z){ // ..... }Można tę funkcję wywołać tak:
Zespolona z(3,4); policz_cos(z);ale jeśli zmienna
z
nie jest potrzebna, to można wywołać tę funkcję także tak:
policz_cos( Zespolona(3,4) );
Utworzony w ten sposób obiekt tymczasowy będzie istniał tylko podczas wykonywania tej jednej instrukcji.
Dla klasy Zespolona nie ma potrzeby definiowania konstruktora kopiującego (ten wygenerowany automatycznie przez kompilator zupełnie nam w tym przypadku wystarczy). Gdybyśmy jednak chcieli, to musielibyśmy zrobić to następująco:
class Zespolona{ private: // tu można pominąć private: double re, im; public: // konstruktory Zespolona(double, double); Zespolona(Zespolona&); // operacje Zespolona dodaj(Zespolona); Zespolona odejmij(Zespolona); double modul(); }; Zespolona::Zespolona(const Zespolona& z){ re = z.re; im = z.im; }
Jest zupełnie naturalne, by chcieć używać liczb zespolonych, które są tak naprawdę liczbami rzeczywistymi. Możemy to teraz robić następująco:
Zespolona z(8,0);Gdybyśmy mieli często używać takich liczb, to wygodniej by było mieć konstruktor, który sam dopisuje zero:
class Zespolona{ // ... public: // konstruktory Zespolona(double); // ... }; Zespolona::Zespolona(double r) { re = r; im = 0; }
Przedstawione rozwiązanie jest zupełnie poprawne. Można definiować wiele konstruktorów, kompilator C++ na podstawie listy argumentów zdecyduje, którego należy użyć. Możemy to jednak zapisać prościej korzystając z parametrów domyślnych:
class Zespolona{ private: // tu można pominąć private: double re, im; public: // konstruktory Zespolona(double, double = 0); Zespolona(Zespolona&); // operacje Zespolona dodaj(Zespolona); Zespolona odejmij(Zespolona); double modul(); }; Zespolona::Zespolona(double r, double i){ re = r; im = i; } // .....
Uwaga: nie można deklarować wartości domyślnej i w nagłówku funkcji i w jej implementacji.
Zdefiniowanie konstruktora liczb zespolonych z jednym argumentem (liczbą typu double) ma dalsze konsekwencje. Poniższe wywołanie jest teraz poprawne:
policz_cos( 6 );Innymi słowy zdefiniowanie w klasie
K
konstruktora, którego można wywołać z jednym parametrem typu T
, oznacza zdefiniowanie konwersji z typu T
do typu K
.
O tym jak definiować konwersje w drugą stronę powiemy później (omawiając operatory).
Gdy obiekt kończy swoje istnienie automatycznie zwalnia się zajmowana przez niego pamięć.
Nie dotyczy to jednak zasobów, które obiekt sam sobie przydzielił w czasie swego istnienia.
Rozwiązaniem tego problemu są destruktory.
Destruktor to metoda klasy. Klasa może mieć co najwyżej jeden destruktor. Destruktor nie ma parametrów.
Nie można specyfikować typu wyniku destruktora. Nie można w nim używać instrukcji return
z parametrem.
Nazwa destruktora jest taka sama jak nazwa klasy, tyle że poprzedzona tyldą.
Destruktor jest odpowiedzialny za dwie rzeczy:
zwolnienie pamięci zajmowanej przez obiekt (to sprawa kompilatora),
zwolnienie zasobów (to nasze zadanie, zwalnianie zasobów zapisujemy jako treść destruktora).
Zasobami, które obiekty przydzielają sobie najczęściej są fragmenty pamięci.
W klasie Zespolona destruktor nie jest potrzebny, ale można go zdefiniować:
class Zespolona{ private: // tu można pominąć private: double re, im; public: // konstruktory i destruktory Zespolona(double, double = 0); Zespolona(Zespolona&); ~Zespolona(); // operacje Zespolona dodaj(Zespolona); Zespolona odejmij(Zespolona); double modul(); }; Zespolona::~Zespolona(){ // W tej klasie nie mamy żadnych zasobów do zwolnienia }
complex
W tym rozdziale przedstawiliśmy definiowanie klasy na przykładzie liczb zespolonych.
Taki przykład wybrano, gdyż reprezentuje dobrze znane pojęcie, jest prosty, a jednocześnie pozwala na pokazanie wielu interesujących własności klas.
Warto jednak zaznaczyć, że standardowa biblioteka C++ zawiera własną definicję liczb zespolonych w postaci szablonu complex
(szablony omawiamy w osobnym rozdziale).
Oto fragment programu używającego szablonu complex
.
#include <iostream> #include <complex> using namespace std; int main(){ complex<double> c1; complex<double> c2(7.0, 3.5); cout << "c1 = " << c1 << ", c2 = " << c2; cout << ", c1 + c2 = " << c1+c2 << endl; }
Wynikiem działania tego programu jest wypisanie
c1 = (0,0), c2 = (7,3.5), c1 + c2 = (7,3.5)
Treść automatycznie generowana z plików źródłowych LaTeXa za pomocą oprogramowania wykorzystującego LaTeXML.
strona główna | webmaster | o portalu | pomoc
© Wydział Matematyki, Informatyki i
Mechaniki UW, 2009-2010. Niniejsze materiały są udostępnione bezpłatnie na licencji Creative Commons Uznanie autorstwa-Użycie niekomercyjne-Bez utworów zależnych 3.0 Polska.
Projekt współfinansowany przez Unię Europejską w ramach
Europejskiego Funduszu Społecznego.
Projekt współfinansowany przez Ministerstwo Nauki i Szkolnictwa Wyższego i przez Uniwersytet Warszawski.