Deklaracje zmiennych typów prostych inicjowane poprzez nadanie w momencie tworzenia określonych wartości:
int liczba = 123.6;
char * nazwa = new char[100];
Klasa w języku C++:
class magnetofon
{
enum {wl, wyl} stan;
int głosnosc;
double stan_tasmy;
void zmien_glosnosc(int stopien_glosnosci);
void przewijaj(double tasma);
}
Deklaracja obiektu tej klasy:
magnetofon panasonic;
Deklaracja metod będących interfejsem klasy (dla programisty korzystającego z obiektu tej klasy dostępna jest jedynie publiczna część owego interfejsu):
class faktura
{
private:
double k_cena;
int k_ilosc;
int k_vat;
double k_wartosc;
void oblicz_wartosc();
public:
void wyswietl();
void dane_do_faktury(double cena, int ilosc, int vat);
}
Definicje metod (implementacja) klasy faktura:
void faktura::oblicz_wartosc()
{
k_wartosc=(k_cena*k_ilosc*k_vat)/100;
}
void faktura::wyswietl()
{
cout << ”Cena: ” << k_cena << ”\n”
<< ”Ilość: ” << k_ilosc << ”\n”
<< ”VAT: ” << k_vat << ”\n”
<< ”Wartość: ” << k_wartosc << ”\n”;
}
void faktura::dane_do_faktury(double cena, int ilość, int vat)
{
k_cena = cena;
k_ilosc = ilosc;
k_vat = vat;
oblicz_wartosc()
}
Nazwa klasy umieszczona z podwójnym dwukropkiem przed nazwą metody to informacja dla kompilatora, która identyfikuje funkcję jako metodę ściśle określonej klasy. Chcąc wywołać taką metodę należy najpierw powołać obiekt takiej klasy, której składową jest rzeczona metoda, a następnie użyć go podczas wywołania:
main()
{
faktura fakt;
fakt.dane_do_faktury(10.4, 4, 22);
}
Konstrukcja i dekonstrukcja. Obiekt to zmienna jakiegoś typu, zatem można ją inicjalizować. Rolę inicjatora odgrywa specjalna metoda zwana konstruktorem. Konstruktor ma dwie cechy charakterystyczne: nie może zwracać wartości oraz jego nazwa jest taka sama jak nazwa klasy. Aby konstruktor był dostępny w funkcji main() jego deklaracja musi być umieszczona w sekcji public klasy.
W języku C++ można nie definiować konstruktora, wówczas kompilator sam utworzy
i wywoła konstruktor domyślny, który jednak nie robi nic pożytecznego. Można również zdefiniować więcej niż jeden konstruktor; stosowana jest wówczas zasada przeciążania nazw.
Definicja konstruktora:
faktura(int cena, int ilosc, int vat);
Deklaracja konstruktora obiektów klasy faktura:
faktura::faktura(int cena, int ilosc, int vat)
{
k_cena = cena;
k_ilosc = ilosc;
k_vat = vat;
}
Wywołanie konstruktora obiektów klasy faktura:
main()
{
faktura fakt(10.4, 2, 7);
}
Wywoływany podczas tworzenia obiektu konstruktor sugeruje istnienie mechanizmu odwrotnego, który z chwilą zaprzestania wykorzystywania obiektu niszczy go. Funkcja pełniąca tą rolę nazywa się destruktorem. Jego nazwa to połączenie tyldy i nazwy konstruktora. Destruktor to miejsce, w którym można zwolnić pamięć zajmowaną przez pola obiektu lub wykonać inne operacje porządkowe.
Konstruktor wywoływany jest jawnie podczas tworzenia obiektu, natomiast destruktor uruchamiany jest automatycznie (niejawnie) w chwili, gdy obiekt przestaje być dostępny, np. gdy kończy działanie funkcja, w której obiekt był zadeklarowany lokalnie.
Destruktor obiektów klasy faktura:
~faktura()
{
cout << ”Faktura zlikwidowana.”;
}
class kontrahent
{
private:
char * k_nazwa;
char * k_nip;
public:
kontrahent(char * nazwa, char * nip);
~kontrahent();
void wyswietl();
};
kontrahent::kontrahent(char * nazwa, char *nip)
{
k_nazwa = new char[strlen(nazwa)+1];
strcopy(k_nazwa,nazwa);
k_nip = new char[strlen(nip)+1];
strcopy(k_nip,nip);
}
kontrahent::~kontrahent()
{
delete [] k_nazwa;
delete [] k_nip;
}
void kontrahent::wyswietl()
{
cout << ”Nazwa przedsiębiorstwa: ” << k_nazwa << ”\n”
<< ”NIP: ” << k_nip << ”\n”;
}
Utworzenie i inicjacja obiektu klasy kontrahent:
kontrahent kontr(”TRANSKOM”, ”588-125-75-35”);
Kompozycja. Pomiędzy klasami mogą zachodzić dwa rodzaje relacji: jedna z nich to relacja jest, druga to relacja ma. Mimo, że w programowaniu obiektowym pierwszy z tych typów wykorzystywany jest znacznie częściej, to drugi jest bardziej intuicyjny i łatwiejszy do zaimplementowania, lecz - nie wiadomo dlaczego - niedoceniany. Między klasami faktura i kontrahent zachodzi właśnie ten drugi typ relacji: ma. Kontrahent musi być umieszczony na fakturze, zatem można powiedzieć, że faktura posiada (zawiera) kontrahenta lub korzystając z terminologii obiektowej - faktura ma kontrahenta. Obecna tu relacja to: istnieje obiekt zewnętrzny, w którym znajduje się obiekt wewnętrzny. W programowaniu obiektowym umieszczanie (zawieranie) jednych obiektów w drugich nazywa się obudowywaniem lub kompozycją.
class faktura
{
private:
…
kontrahent kontr;
…
public:
…
};
Obiekt klasy kontrahent jest wewnętrznym obiektem klasy faktura. Poprzez zadeklarowanie w części prywatnej jest niedostępny dla kodu zewnętrznego. Składowe prywatne klasy kontrahent nie są dostępne dla klasy faktura; jedynym sposobem na komunikację tych klas jest publiczny interfejs klasy kontrahent (klasa zewnętrzna ma dostęp do interfejsu klasy wewnętrznej, natomiast nie ma dostępu do jej implementacji). Powyższa deklaracja nie wywołuje konstruktora klasy kontrahent; nie jest to możliwe, ponieważ programista nie może przekazać danych kontrahenta do sekcji private klasy faktura. Ponadto, można również przypuszczać, że obiekt kontrahent powinien zostać zainicjalizowany wcześniej niż obiekt faktura, gdyż ten drugi mógłby np. odwoływać się w swoim konstruktorze do pierwszego. Załóżmy, że wywołanie konstruktora klasy kontrahent powinno odbyć się bezpośrednio przed uruchomieniem kodu konstruktora klasy faktura. Język C++ udostępnia w tym celu konstrukcję zwaną listą inicjatorów konstruktora, czyli umieszczone po deklaracji konstruktora obiektu zewnętrznego wywołania konstruktorów obiektów wewnętrznych. Prawidłowa postać konstruktora klasy faktura:
faktura(int cena, int ilość, int vat, char * nazwa, char * nip)
: kontr(nazwa, nip) {
…
};
I jej wywołanie w kodzie funkcji main(), przekazujące do wewnętrznego obiektu klasy kontrahent nazwy kontrahenta oraz jego numeru identyfikacji podatkowej:
main()
{
…
faktura fakt(6.84, 10, 22, ”TRANSKOM”, ”558-125-75-35”);
…
}
Ponieważ dane potrzebne do wyliczenia wartości brutto faktury będą przekazywane za pomocą argumentów metody dane_do _faktury (zob. deklaracja klasy faktura), należy usunąć te informacje z konstruktora:
faktura(char* nazwa, char* nip):kontr(nazwa, nip) {};
Ciało przedstawionego konstruktora jest puste, ponieważ jego jedynym zadaniem jest w tej chwili wywołanie konstruktora klasy kontrahent. Skoro konstruktor klasy faktura nie wprowadza zmian w środowisku działania programu można zrezygnować z destruktora tej klasy, usuwając z kodu jego deklarację i definicję. Ponadto w klasie faktura drobnej zmianie ulegnie metoda wyswietl(), która oprócz danych faktury będzie wyświetlać dane kontrahenta:
void faktura::wyswietl()
{
kontr.wyswietl();
cout << ”Cena: ” << k_cena << ”\n”
<< ”Ilość: ” << k_ilosc << ”\n”
<< ”VAT: ” << k_vat << ”\n”
<< ”Wartość: ” << k_wartosc << ”\n”;
}
Dzięki wykorzystaniu obiektu wewnętrznego następuje wywołanie metody wyswietl() klasy kontrahent. Nie odwołujemy się bezpośrednio do pól klasy kontrahent, które przechowują nazwę i NIP, ponieważ ze względu na miejsce ich deklaracji (sekcja private klasy kontrahent) jest to niemożliwe; korzystamy natomiast z interfejsu tej klasy, który zapewnia pośredni dostęp do jej implementacji.
Kod gotowy do wykorzystania. Powołanie obiektu klasy faktura:
faktura fakt(”TRANSKOM”, ”558-125-75-35”);
wywołanie jego metody pozwalającej na wprowadzenie danych do faktury:
fakt.dane_do_faktury(6.84, 10, 22);
i wyświetlenie rezultatu obliczeń:
fakt.wyswietl();
Czy jest jakiś prosty sposób na rozbudowę takiego programu? Czy jeśli pojawi się nowy wzór faktury, to trzeba zmieniać kod metody wyswietl() klasy faktura? Jeśli tak, to co zrobić w sytuacji gdy klasa ta zostanie dołączona w postaci biblioteki wykorzystywanej również przez inne programy?
Dziedziczenie. Załóżmy, że pojawia się potrzeba drukowania trzech rodzajów faktur, a nie jednej: zwykłej (jak dotychczas), z logo firmy A oraz z logo firmy B. Myśląc obiektowo konieczne będzie utworzenie dwóch dodatkowych klas faktura. Jednak większość metod
i pól tych dwóch klas będzie taka sama, jak w klasie faktura, a różnice będą występowały jedynie w metodzie wyswietl(), drukującej różne logo firm. Nasuwa się pytanie, czy nie można odziedziczyć cech i funkcji klasy faktura pomijając metodę wyswietl()? Skoro faktura z logo firmy jest typową fakturą, na tej relacji można oprzeć dziedziczenie.
Kod klasy faktura_z_logo_A:
class faktura_z_logo_A : public faktura
{
public:
faktura_z_logo_A(char * nazwa, char * nip) :
faktura(nazwa, nip) {}
void wyswietl();
};
void faktura_z_logo_A::wyswietl()
{
cout << ”LOGO FIRMY A \n”;
faktura::wyswietl();
}
Dziedziczenie może być procesem wielostopniowym, np. gdy klasa faktura jest potomkiem jakiejś bardziej ogólnej klasy rachunek, to dziedzicząca po klasie faktura klasa faktura_z_logo_A ma publiczne metody i pola klasy macierzystej rachunek. Gdy w klasie potomnej zostanie zadeklarowana metoda o takiej samej nazwie jak w klasie macierzystej, wówczas obiekt klasy potomnej będzie wykonywał swoją metodę.
W przeciwnym razie będzie szukał metody w klasie macierzystej.
main()
{
faktura_z_logo_A fakt_A(”TRANSKOM”, ”558-125-75-35”);
// wywołanie metody klasy faktura
fakt_A.dane_do_faktury(1.5, 100, 22);
// wywołanie metody klasy faktura_z_logo_A
fakt_A.wyswietl();
}
Konstruktor klasy faktura_z_logo_A jest bardzo podobny do konstruktora klasy faktura, różnica dotyczy jedynie listy inicjatorów. Nie wywoływany jest bezpośrednio konstruktor klasy kontrahent (na tym poziomie nie ma nawet dostępu do owego konstruktora, ponieważ jest on zdefiniowany w sekcji prywatnej klasy macierzystej), lecz konstruktor klasy macierzystej faktura, który z kolei inicjuje obiekt klasy kontrahent. Jest to tzw. łańcuch wywołań. Podobnie jest z destruktorami: wywoływane są sekwencyjnie, w momencie gdy obiekty przestają być potrzebne.
Nowym elementem jest wywołanie faktura::wyswietl(), umieszczone w definicji metody wyswietl() klasy faktura_z_logo_A. W ten sposób w klasach potomnych można wywoływać obiekty klas macierzystych. Wywołanie metody wyswietl() bez operatora zasięgu faktura:: uruchomi ciąg rekurencyjnych wywołań metody wyswietl() z klasy faktura_z_logo_A, co w efekcie prędzej czy później doprowadzi do zawieszenia programu. Po co zatem wywoływana jest metoda wyswietl() klasy macierzystej w klasie potomnej? Otóż chcąc zmodyfikować logo faktury pozostawiając resztę informacji bez zmian, dopisujemy więc w metodzie wyswietl() klasy faktura_z_logo_A linię drukującą logo, a dalszą część faktury wyświetlamy dzięki odziedziczonej metodzie wyswietl() klasy faktura.
Tak może wyglądać program główny drukujący różne wzory faktur:
main()
{
char wzor;
faktura fakt(”TRANSKOM”, ”558-125-75-35”);
faktura_z_logo_A fakt_A(”TRANSKOM”, ”558-125-75-35”);
faktura_z_logo_B fakt_A(”TRANSKOM”, ”558-125-75-35”);
cout << ”Wybierz wzór faktury: ”;
cout << ”o-ogólny, a-logo A, b-logo B: \n”;
cin >> wzor;
switch(wzor)
{
case 'O':
case 'o': fakt.dane_do_faktury(8.5, 9, 22);
fakt.wyswietl();
break;
case 'A':
case 'a': fakt_A.dane_do_faktury(8.5, 9, 22);
fakt_A.wyswietl();
break;
case 'B':
case 'b': fakt_B.dane_do_faktury(8.5, 9, 22);
fakt_B.wyswietl();
break;
}
}
Mimo, iż powyższy program działa poprawnie, daje się zauważyć pewną nadmiarowość. Optymalizacja powyższego kodu dotyczyć będzie warunków case, które wywołują te same metody dotyczące różnych klas. Można napisać funkcję np. fakturuj() i w niej wywoływać wszystkie metody. Faktura z logo jest fakturą, więc do funkcji fakturuj() należy przekazać parametr formalny typu faktura, zmuszając kompilator do wykonania rzutowania argumentu aktualnego do typu faktura. Jaka metoda wyswietl() zostanie wywołana: ta z klasy faktura czy z klas potomnych?
Kod zoptymalizowanej funkcji main() oraz funkcji fakturuj():
main()
{
char wzor;
faktura fakt(”TRANSKOM”, ”558-125-75-35”);
faktura_z_logo_A fakt_A(”TRANSKOM”, ”558-125-75-35”);
faktura_z_logo_B fakt_A(”TRANSKOM”, ”558-125-75-35”);
cout << ”Wybierz wzór faktury: ”;
cout << ”o-ogólny, a-logo A, b-logo B: \n”;
cin >> wzor;
switch(wzor)
{
case 'O':
case 'o': fakturuj(fakt);
break;
case 'A':
case 'a': fakturuj(fakt_A);
break;
case 'B':
case 'b': fakturuj(fakt_B);
break;
}
}
void fakturuj(faktura f)
{
f.dane_do_faktury(8.5, 9, 22);
f.wyswietl();
}
Polimorfizm. Mechanizm ten uznawany za istotę programowania obiektowego nazywa się również (wielopostaciowością), czyli przyjmowaniem przez kod różnych postaci w zależności od kontekstu. W przypadku wybrania dowolnego wzoru faktury zawsze wywoływana jest metoda wyswietl() z klasy faktura. Kompilator dokonując skojarzenia wywołania funkcji z jej ciałem realizuje tzw. wiązanie; w przedstawionym przypadku rzutuje do typu faktura wszystko, co jest przekazywane do funkcji fakturuj(), której argumentem jest obiekt klasy faktura a wiersz f.wyswietl() wywołuje metodę wyswietl() właśnie z tej klasy. Rozwiązaniem problemu będzie przeniesienie wiązania z fazy kompilacji programu do fazy jego uruchomienia, gdzie jest już znany typ obiektu w funkcji fakturuj().W tym celu należy w klasie macierzystej zadeklarować funkcję wyswietl() jako wirtualną, umieszczając przed jej nazwą słowo kluczowe virtual, natomiast do funkcji fakturuj() trzeba przekazać adres obiektu klasy macierzystej:
class faktura
{
…
public:
…
//dodanie słowa kluczowego virtual
virtual void wyświetl();
…
};
…
// argument przekazywany przez referencję
void fakturuj(faktura& f)
{
f.dane_do_faktury(8.5, 9, 22)
f.wyswietl();
}
…
Słowo kluczowe virtual zapobiega wykonywanemu podczas kompilacji programu wiązaniu wywołania funkcji z jej ciałem. Jest to tzw. późne wiązanie, kiedy znany jest już typ obiektu przekazywanego do funkcji fakturuj(). W wielkim uproszczeniu późne wiązanie polega na niejawnym umieszczeniu w klasach wskaźników do tablic, w których przechowywane są adresy znajdujących się w tych klasach funkcji wirtualnych. Podczas odwołania się do takiej funkcji uruchamiany jest specjalny kod, który korzystając ze wspomnianych wskaźników, wyszukuje w odpowiedniej tablicy adresu funkcji wirtualnej, po czym wywołuje tę funkcję. W jakiej tablicy będzie wykonywane wspomniane wyszukiwanie zależy od typu (klasy) obiektu, którego będzie dotyczyło. Późne wiązanie może być odmiennie realizowane w różnych językach, jednak ogólna zasada jest taka, jak przedstawiono powyżej.
Abstrakcja. Klasa faktura pełni dwa bardzo ważne zadania: dostarcza struktury macierzystej, na podstawie której mogą być budowane bardziej skonkretyzowane klasy potomne oraz gwarantuje możliwość korzystania w klasach potomnych z wywołań polimorficznych. Ograniczenie roli tej klasy do wyłącznie tych dwóch zadań uzyskuje się deklarując ją jako klasę abstrakcyjną. Klasa abstrakcyjna to pewien model stanowiący budulec dla struktur potomnych. Klasa abstrakcyjna udostępnia klasom wywodzącym się z niej swój interfejs, lecz nie pozwala na tej podstawie tworzyć obiektów. Jeśli klasa faktura byłaby klasą abstrakcyjną wówczas programista nie mógłby powołać obiektu tej klasy, lecz powstała na jej podstawie klasa faktura_z_logo_A mogłaby bez problemów korzystać z publicznych metod i pól swojej klasy macierzystej. Aby utworzyć klasę abstrakcyjną należy powołać w niej przynajmniej jedną metodę w pełni wirtualną, czyli taką, przed którą znajduje się słowo kluczowe virtual, a po niej umieszczony jest znak równości i zero (=0).
class faktura
{
…
public:
virtual void wyswietl()=0;
…
}
W klasach pochodnych faktura_z_logo_A oraz faktura_z_logo_B metoda wirtualna wyswietl() powodowała wyświetlenie odpowiedniego logo oraz wywołanie metody wyswietl() klasy macierzystej faktura, odpowiedzialnej za wyświetlenie danych faktury. Po zadeklarowaniu tej ostatniej jako w pełni wirtualnej taka koncepcja przestaje mieć sens. Jak zatem wyświetlić wartości pól k_cena, k_ilosc, k_vat oraz k_wartosc z abstrakcyjnej klasy faktura, skoro są zadeklarowane w jej sekcji private, a więc niedostępne poza tą klasą? Można oczywiście przesunąć je do sekcji publicznej lecz byłoby to naruszeniem zasad programowania obiektowego - te pola są wewnętrzne dla faktury, w związku z czym nie powinny być dostępne poza fakturą. Jednak pola, o których mowa, powinny być udostępniane w klasach dziedziczących po klasie faktura. W tym celu używa się specyfikatora dostępu protected. Wszystkie metody i pola umieszczone w sekcji protected klasy są dostępne dla niej oraz wszystkich jej klas potomnych, natomiast nie są widziane poza kodem tych klas.
class faktura
{
private:
kontrahent kontr;
protected:
double k_cena;
int k_ilosc;
int k_vat;
double k_wartosc;
void oblicz_wartosc();
public:
faktura(char * nazwa, char * nip) : kontr(nazwa, nip) {};
virtual void wyswietl()=0;
void dane_do_faktury(double cena, int ilosc, int vat);
void wyswietl_kontr();
};
…
void faktura::wyswietl_kontr()
{
kontr.wyswietl();
}
…
class faktura_z_logo_A : public faktura
{
Public:
faktura_z_logo_A(char* nazwa, char* nip) :
faktura(nazwa, nip) {};
void wyswietl();
};
/*----------------------------------------------------------*/
void faktura_z_logo_A::wyswietl()
{
cout << ”LOGO FIRMY A \n”;
wyswietl_kontr();
cout << ”Cena: „ << k_cena << ”\n”
<< ”Ilość: „ << k_ilosc << ”\n”
<< ”VAT: „ << k_vat << ”\n”
<< ”Wartość: „ << k_wartosc << ”\n”;
}
// klasa faktura_z_logo_B wygląda analogicznie jak powyższa
class faktura_z_logo_B : public faktura
{
…
};
…
void fakturuj(faktura& f)
{
double cena = 0;
int ilosc = 0;
int VAT = 0;
cout << ”Podaj dane do faktury: \n”;
cout << ”Cena: ”;
cin >> cena;
cout << ”Ilość: ”;
cin >> ilosc;
cout << ”VAT: ”;
cin >> vat;
f.dane_do_faktury(8.5, 9, 22);
cout << ”\n\nF A K T U R A\n”;
f.wyswietl() ;
cout << ”\n\n”;
}
int main()
{
char * nazwa = new char[100];
char * nip = new char[13];
char wzor;
cout << ”Podaj nazwę przedsiębiorstwa: ”;
cin >> nazwa;
cout << ”Podaj NIP firmy: ”;
cin >> nip;
cout << ”Wybierz wzór faktury: ”;
cout << ”a-logo A, b-logo B: \n”;
cin >> wzor;
faktura_z_logo_A fakt_A(nazwa, nip);
faktura_z_logo_B fakt_B(nazwa, nip);
switch(wzor)
{
case 'A':
case 'a': fakturuj(fakt_A);
break;
case 'B':
case 'b': fakturuj(fakt_B);
break;
}
}
Warto zwrócić uwagę na umieszczenie w klasie faktura publicznej metody wyswietl_kontr(), wyświetlającej dane kontrahenta. Ponieważ obiekt klasy kontrahent pozostał w sekcji private klasy faktura, taka pośrednicząca metoda jest jedynym sposobem dotarcia do danych kontrahenta z poziomu klas potomnych.
W programowaniu obiektowym metody tego typu spotykane są stosunkowo często. Ponadto, zwraca uwagę sposób jej wywołania w metodach wyswietl() klas pochodnych. Nie trzeba tu używać operatora zasięgu faktura:: (choć jego użycie nie zaszkodzi niczemu), gdyż kompilator automatycznie będzie poszukiwał wywoływanej metody w klasie macierzystej.
W roku 1735 niejaki Karol Lineé, bardziej znany jako Lineusz, zaproponował podział świata roślinnego I zwierzęcego na jednostki taksonomiczne, dając tym samym podwaliny systematyce, czyli nauce zajmującej się badaniem organizmów, sporządzaniem ich opisów, katalogowaniem oraz klasyfikacją. Klasyfikacja to nic innego jak przyporządkowanie obiektów (np. Roślin, zwierząt) do grup (klas), które spełniają warunek zupełności
i rozłączności, ze względu na podobieństwo ich cech. Od czasów pierwszego komputera trzeba było czekać wiele lat, zanim tak zdawałoby się naturalne myślenie obiektowe zaproponowane przez Lineusza, znalazło odzwierciedlenie w metodach programowania. Dla programistów, którzy długo korzystali z technik strukturalnych, przestawienie się na myślenie obiektowe jest często barierą nie do pokonania. Nie wystarczy nauczyć się pewnych elementów składni, trzeba zupełnie zmienić sposób postrzegania programu komputerowego. Należy jak najbardziej zbliżyć projekt do opisywanej natury. Problem oraz jego rozwiązanie nie mogą istnieć w dwóch całkowicie odrębnych światach. Na zrozumienie programowania obiektowego jest tylko jedna recepta: rozbicie pojęcia na samodzielne jednostki - klasy, ustalenie panujących między nimi zależności i relacji i dopiero wówczas pisanie kodu.
CEiB ŻAK technik informatyk sem. 3
inż. Adam Całun Programowanie strukturalne i obiektowe
PSiO Strona 1