Programowanie obiektowe Klasy w C Wstp Klasa jest
Programowanie obiektowe Klasy w C++
Wstęp • Klasa jest kluczową koncepcją języka C++, realizującą abstrakcję danych na bardzo wysokim poziomie. • Odpowiednio zdefiniowane klasy stawiają do dyspozycji użytkownika wszystkie istotne mechanizmy programowania obiektowego: ukrywanie informacji, dziedziczenie, polimorfizm z wiązaniem późnym, a także szablony klas i funkcji. • Klasa jest deklarowana i definiowana z jednym z trzech słów kluczowych: class, struct i union. • Elementami składowymi klasy mogą być struktury danych różnych typów, zarówno podstawowych, jak i zdefiniowanych przez użytkownika, a także funkcje dla operowania na tych strukturach. Dostęp do elementów klasy określa zbiór reguł dostępu.
Deklaracja i definicja klasy • Klasa jest typem definiowanym przez użytkownika. • Deklaracja klasy składa się z nagłówka, po którym następuje ciało klasy, ujęte w parę nawiasów klamrowych; po zamykającym nawiasie klamrowym musi wystąpić średnik, ewentualnie poprzedzony listą zmiennych. • W nagłówku klasy umieszcza się słowo kluczowe class (lub struct albo union), a po nim nazwę klasy, która od tej chwili staje się nazwą nowego typu. • Klasa może być deklarowana: - Na zewnątrz wszystkich funkcji programu. Zakres widzialności takiej klasy rozciąga się na wszystkie pliki programu. - Wewnątrz definicji funkcji. Klasę taką nazywa się lokalną, ponieważ jej zakres widzialności nie wykracza poza zasięg funkcji. - Wewnątrz innej klasy. Klasę taką nazywa się zagnieżdżoną, ponieważ jej zakres widzialności nie wykracza poza zasięg klasy zewnętrznej. • Deklaracji klas nie wolno poprzedzać słowem kluczowym static.
Przykłady deklaracji • Przykładowe deklaracje klas mogą mieć postać: class Pusta {}; class Komentarz { /* Komentarz class Niewielka { int n; }; */}; • Wystąpienia klasy deklaruje się tak samo, jak zmienne innych typów, np. Pusta pusta 1, pusta 2; Niewielka nw 1, nw 2; Niewielka* wsk = &nw 1; • Zmienne pusta 1, pusta 2, nw 1, nw 2 nazywają się obiektami, zaś wsk jest wskaźnikiem do typu Niewielka, zainicjowanym adresem obiektu nw 1. • Klasy w rodzaju Pusta i Komentarz używa się często jako klasymakiety podczas opracowywania programu.
Specyfikatory (modyfikatory) dostępu • Deklaracje elementów składowych klasy można poprzedzić etykietą public: , protected: , lub private: . • Jeżeli sekwencja deklaracji elementów składowych nie jest poprzedzona żadną z tych etykiet, to kompilator przyjmuje domyślną etykietę private: . Oznacza to, że dana składowa może być dostępna jedynie dla funkcji składowych i tzw. funkcji zaprzyjaźnionych klasy, w której jest zadeklarowana. • Wystąpienie etykiety public: oznacza, że występujące po niej nazwy deklarowanych składowych mogą być używane przez dowolne funkcje, a więc również takie, które nie są związane z deklaracją danej klasy. Znaczenie etykiety protected: - później. • Ze względu na sterowanie dostępem do składowych klasy, słowa kluczowe public, protected i private nazywa się specyfikatorami dostępu.
#include <iostream> #include <conio. h> using namespace std; class Niewielka { public: int n; void komunikat() { cout<<"Tu jestesmy"; } }; void main() { Niewielka nw 1; nw 1. n = 5; cout<<"nw 1. n="<<nw 1. n<<endl; cout<<"komunikat="; nw 1. komunikat(); getch(); } Przykład
Definicja klasy • Jeżeli klasa zawiera funkcje składowe, to ich deklaracje muszą wystąpić w deklaracji klasy. Funkcje składowe mogą być jawnie deklarowane ze słowami kluczowymi inline, static i virtual; nie mogą być deklarowane ze słowem kluczowym extern. • W deklaracji klasy można również umieszczać definicje krótkich (1 -2 instrukcje) funkcji składowych; są one wówczas traktowane przez kompilator jako funkcje rozwijalne, tj. tak, jakby były poprzedzone słowem kluczowym inline. • W programach jednoplikowych dłuższe funkcje składowe deklaruje się w nawiasach klamrowych zawierających ciało klasy, zaś definiuje się bezpośrednio po zamykającym nawiasie klamrowym. • Deklaracja klasy wraz z definicjami funkcji składowych (i ewentualnie innymi wymaganymi definicjami) stanowi definicję klasy.
Przykład definicji klasy #include <iostream> #include <conio. h> using namespace std; class Punkt { public: int x, y; void init(int a, int b) { x = a; y = b; } void ustaw(int, int); }; void Punkt: : ustaw(int a, int b) { x = x + a; y = y + b; } int main() { Punkt punkt 1; punkt 1. init(1, 1); cout << punkt 1. x << endl; cout << punkt 1. y << endl; punkt 1. ustaw(10, 15); cout << punkt 1. x << endl; cout << punkt 1. y << endl; getch(); return 0; }
Konstruktory i destruktory • Konstruktory i destruktory należą do grupy specjalnych funkcji składowych. Grupa ta obejmuje: konstruktory i destruktory inicjujące, konstruktor kopiujący oraz tzw. funkcje operatorowe. • Konstruktor jest funkcją składową o takiej samej nazwie, jak nazwa klasy. Nazwą destruktora jest nazwa klasy, poprzedzona znakiem tyldy (~). • Każda klasa zawiera konstruktor i destruktor, nawet gdy nie są one jawnie zadeklarowane. Zadaniem konstruktora jest konstruowanie obiektów swojej klasy; jego wywołanie w programie pociąga za sobą: - alokację pamięci dla obiektu; - przypisanie wartości do niestatycznych zmiennych składowych; - wykonanie pewnych operacji porządkujących (np. konwersji typów). • Jeżeli w klasie nie zadeklarowano konstruktora i destruktora, to zostaną one wygenerowane przez kompilator i automatycznie wywołane podczas tworzenia i destrukcji obiektu.
#include <iostream> using namespace std; class Punkt { public: Punkt(int, int); void ustaw(int, int); private: int x, y; }; Przykład Punkt: : Punkt(int a, int b) { x = a; y = b; } void Punkt: : ustaw( int c, int d) { x = x + c; y = y + d; } int main() { Punkt punkt 1(3, 4); //konstruktor return 0; }
Własności konstruktorów i destruktorów • Konstruktory i destruktory generowane przez kompilator są public. • Konstruktory definiowane przez użytkownika również powinny być public, aby istniała możliwość tworzenia obiektów na zewnątrz deklaracji klasy. • W pewnych przypadkach szczególnych konstruktory mogą wystąpić po etykiecie protected: , a nawet private: . • Konstruktor jest wołany automatycznie, gdy definiuje się obiekt; destruktor gdy obiekt jest niszczony. • Z bloku konstruktora i destruktora mogą być wywoływane funkcje składowe ich klasy.
Kiedy wykorzystujemy konstruktor • Konstruktor jest wołany wtedy, gdy ma być utworzony obiekt jego klasy. Obiekt taki może być tworzony na wiele sposobów: - jako zmienna globalna; - jako zmienna lokalna; - przez jawne użycie operatora new; - jako obiekt tymczasowy; - jako zmienna składowa, zagnieżdżona w innej klasie.
#include <iostream> #include <conio. h> using namespace std; class Status { public: Status() { licznik++; } ~Status() { licznik--; } int odczyt() { return licznik; } private: static licznik; //deklaracja }; int Status: : licznik = 0; // Definicja int main() { Status ob 1, ob 2, ob 3; cout <<"Mamy"<< ob 1. odczyt() << " obiektyn"; //mamy 3 obiekty Status *wsk; wsk = new Status; //Alokuj dynamicznie if (!wsk) { cout << "Nieudana alokacjan"; return 1; } cout << "Mamy "<< ob 1. odczyt()<<" obiekty po alokacjin" ; // mamy 4 obiekty po alokacji // skasuj obiekt delete wsk; cout << "Mamy "<< ob 1. odczyt()<<"obiekty po destrukcjin"; // mamy 3 obiekty po destrukcji getch(); return 0; } Przykład
Przeciążanie konstruktorów • Konstruktory, podobnie jak “zwykłe” funkcje (także funkcje składowe klasy), mogą być przeciążane. • Najczęściej ma to na celu tworzenie obiektów inicjowanych zadanymi wartościami zmiennych składowych, lub też obiektów bez podanych wartości inicjalnych. • Dzięki temu można zadeklarować więcej niż jeden konstruktor dla danej klasy. • Wywołanie konstruktora w deklaracji obiektu pozwala kompilatorowi ustalić, w zależności od liczby i typów podanych argumentów, którą wersję konstruktora należy wywołać.
Przykład #include <iostream> using namespace std; class Punkt { public: Punkt() {} Punkt(int a, int b): x(a), y(b) {} int x, y; }; int main() { Punkt punkt 1(3, 4); cout << punkt 1. x << endl; Punkt punkt 2; cout << punkt 2. x << endl; return 0; }
Argumenty domyślne • Alternatywą dla przeciążania konstruktora jest wyposażenie go w argumenty domyślne, podobnie jak jest to możliwe dla zwykłych funkcji. Argument domyślny jest przyjmowany przez kompilator wtedy, gdy w wywołaniu funkcji nie podano odpowiadającego mu argumentu aktualnego. • Wartości domyślne argumentów formalnych w deklaracji (nie w definicji!) zapisujemy w następujący sposób: - Jeżeli w deklaracji konstruktora umieszczono nazwy argumentów formalnych, to po nazwie argumentu piszemy znak równości, a po nim odpowiednią wartość, np. Punkt ( int a = 0, int b = 0 ); - Jeżeli deklaracja nie zawiera nazw argumentów, a jedynie ich typy, to znak równości i następującą po nim wartość piszemy po identyfikatorze typu, np. Punkt ( int = 0, int = 0 );
#include <iostream> #include <conio. h> using namespace std; class Punkt { public: int x, y; Punkt(int = 0, int = 0); }; Punkt: : Punkt(int a, int b): x(a), y(b) { if (a==0 && b==0) cout << "Obiekt bez inicjowania. . . n"; else cout << "Obiekt z inicjowaniem. . . n"; } int main() { Punkt punkt 1(3, 4); cout << punkt 1. x << endl; //x=3 Punkt punkt 2; cout << punkt 2. x << endl; //x=0 getch(); return 0; } Przykład
Przeciążanie operatorów • W języku C++ są duże możliwości, spotykane w nielicznych współczesnych językach programowania. • Ma on do dyspozycji mechanizm, który pozwala tworzyć nowe definicje dla istniejących operatorów. • Dzięki temu programista może tak zmodyfikować działanie operatora, aby wykonywał on operacje w narzucony przez niego sposób. • Jest to szczególnie istotne w programach czysto obiektowych. Np. obiekty wielokrotnie redefiniowanej klasy Punkt możemy uważać za wektory w prostokątnym układzie współrzędnych. W geometrii i fizyce dla takich obiektów są np. określone operacje dodawania i odejmowania. Byłoby wskazane zdefiniować podobne operacje dla deklarowanych klas przez programistę. • Wykorzystamy do tego celu następującą ogólną postać definicji tzw. funkcji operatorowej, tj. funkcji, która definiuje pewien operator “@”: typ klasa: : operator@(argumenty) { // wykonywane operacje }
Reguły przeciążania operatorów • Operator @ jest zawsze przeciążany względem klasy, w której jest zadeklarowana jego funkcja operator@. Zatem w innych kontekstach operator nie traci żadnego ze swych oryginalnych znaczeń, ustalonych w definicji języka, natomiast zyskuje znaczenia dodatkowe. • Funkcja definiująca operator musi być albo funkcją składową klasy, albo mieć co najmniej jeden argument będący obiektem lub referencją do obiektu klasy (wyjątkiem są funkcje, które redefiniują znaczenie operatorów new i delete). Ograniczenie to gwarantuje, że wystąpienie operatora z argumentami typów nie będących klasami, jest jednoznaczne z wykorzystaniem jego standardowej, wbudowanej w język definicji. Jego intencją jest rozszerzanie języka, a nie zmiana wbudowanych definicji. • Nie mogą być przeciążane operatory “. ”, “. *”, “: : ”, “? : ”, “sizeof” oraz symbole “#” i “##”. • Nie jest możliwa zmiana priorytetu, reguł łączności, ani liczby argumentów operatora.
Przykład // Operator dodawania + #include <iostream> using namespace std; class Punkt { public: Punkt(): x(0), y(0){} // zapis x(0) oznacza x=0 -- został wzięty z języka Simula Punkt(int a, int b): x(a), y(a) {} //x(a) jw. int fx() { return x; } int fy() { return y; } Punkt operator+(Punkt); private: int x, y; }; Punkt: : operator+(Punkt p) { return Punkt(x + p. x +7, y + p. y); } int main() { Punkt punkt 1, punkt 2, punkt 3; //konstruktor Punkt() punkt 1 = Punkt(2, 2); punkt 2 = Punkt(3, 1); punkt 3 = punkt 1 + punkt 2; //przeciążony cout<<"punkt 1. x="<< punkt 1. fx() << endl; //2 cout << "punkt 3. x= " << punkt 3. fx() << endl; //12 int i = 10, j; j = i + 15; // znany '+' cout << " j= " << j << endl; return 0; }
Przykład // Operator relacyjny == int main() { #include <iostream> Punkt punkt 1, punkt 2; using namespace std; punkt 1 = Punkt(2, 2); class Punkt { punkt 2 = Punkt(3, 1); public: if ( punkt 1 == punkt 2 ) cout <<„ Punkt(): x(0), y(0) {} Rownen”; Punkt(int a, int b): x(a), y(a) {} else cout <<„Nierownen�”; int fx() { return x; } int i = 5, j = 6, k; int fy() { return y; } int operator==(Punkt); k = i == j; // predefiniowany '==' private: cout <<„k=„<< k << endl; int x, y; return 0; }; } int Punkt: : operator==(Punkt p) { if( p. fx() == fx() && p. fy() == fy() ) return (-1); else return (0); } • Wyniki • Nierowne • k= 0
Język C++ Dziedziczenie
Przykład #include <iostream> Using namespace std; class Bazowa { public: void ustawx(int n): x(n) {} void podajx() { cout << x << "n"; } private: int x, z; }; class Pochodna : public Bazowa { public: void ustawy(int m) { y = m; } void podajy() { cout << y << "n"; } private: int y; }; int main() { Pochodna ob; ob. ustawx(10); ob. ustawy(20); ob. podajx(); ob. podajy(); cout << sizeof(Bazowa) << endl; //4 B (2*int) cout << sizeof(Pochodna) << endl; //6 B (2*int+1*int) cout << sizeof ob << endl; //6 B return 0; }
- Slides: 23