KLASY
WAŻNE – zanim o obiektach
Przypomnienie idei programowania
strukturalnego
Promowanie uporządkowania kodu i
struktury danych
Standardowo – zstępująco: wymaga
podzielenia zadania na niezależne procedury
Na ogół manipulowanie danymi odbywa się
na niskim poziomie zadań
Ważniejsze w duecie kod-dane są procedury
– stąd inna nazwa programowania
strukturalnego – podejście proceduralne
Pisanie programu
strukturalnego
Wymaga doświadczenia
Trzeba cały czas mieć na uwadze jednocześnie
dane i działania na nich
Główny powód utworzenia programu, to
zamierzone przekształcenie danych w wyniki
Tymczasem, przekształcenia dokonują się na
odległym od sterowania poziomie, a dostęp do
danych przez konwersje i referencje może
odbiegać od założonego
Pisanie programu przypomina organizację biura
projektowego, w którym każdy z pracowników
ma mało-limitowany dostęp do projektu
Zanim o obiektach..
Strukturalizację programu (poznaną dotychczas)
a więc wyodrębnianie elementów wspólnych
i jednokrotne ich opracowanie (co przynosi
niewątpliwy zysk czasowo/objętościowy) można
przyrównać do realizacji algorytmicznej idei dziel i
zwyciężaj na polu „pragmatyki biurowej” -
zdefiniowania i rozdzielenia zadań do wykonania.
Dotychczas
realizowany jest ciąg zadań
o cząstkowych znaczeniach niejasno
związanych z celem ogólnym, a wszystko
trzeba powiązać mając „w głowie całość”
tj.
algorytm
i
struktury danych
na których
ten algorytm działa
Korzystanie z „gotowców” obcego
autorstwa nie jest łatwe, wymaga
„wejścia” w sposób myślenia innego
twórcy
Dotychczas proceduralnie
Co gorsze użytkownik jest traktowany jak idiota,
który może sterować programem tylko w jeden
określony przez programistę sposób (i musi się
tego sposobu nauczyć)
Oczywiście MY wiemy, że jest jeden procesor
wykonujący sekwencję rozkazów
A tymczasem użytkownik (współcześnie coraz
mniej obeznany z ograniczeniami wynikającymi z
idei sekwencyjnej realizacji przetwarzania danych)
żyje w świecie OBIEKTÓW o określonych cechach
oraz funkcjonalności i tego oczekuje od programu
Dlaczego to takie
ważne
Przez komplikację
Samo się napędza
Sprzedaż komputeropodobnej techniki jest
możliwa dzięki:
Masowej produkcji i niskiej cenie
Łatwości (intuicyjności) obsługi, rosnącej idioto-
odporności oprogramowania
Efektywnym algorytmom
Atrakcyjnej, nawiązującej do rzeczywistości szacie
graficznej
Jest oczywiste, że przy tak dużych pieniądzach,
odpowiednio zasilane są zarówno techniczne jak
i programistyczne badania warunkujące rozwój (i
kasę dla milionów ludzi zatrudnionych w branży)
A że rozwój jest szalony
Jesteśmy uzależnieni od tej techniki, marzymy
aby inne dziedziny tak się rozwijały
30 lat temu
R-32U – masa ok. 4 t, zużycie energii 38 kW, 11 osób
obsługi, cena ~0,5 mln $, RAM 1 MB, 1 MFlop/s
Dziś
Laptop – masa ok. 1 kg, zużycie energii – 100 W, RAM
4GB, 2 GFlop/s, cena 500$
W samochodach taki rozwój oznaczałby dziś
masę 0,5 kg, zużycie paliwa 20ml/100 km, cenę
2$ (taniej kupić nowy niż zaparkować)
Nic dziwnego, że z chwilą pojawienia się PC
mnóstwo ludzi zafascynowało się tą techniką
Brak standardów użytkowych
Brak unormowania interfejsu użytkownika i
pisanie programów przez zafascynowanych
amatorów skutkowały pojawieniem się
mnóstwa programów (ok. 40% softwaru ‘80) z
bardzo skomplikowaną obsługą.
Rozwój Windows (a wcześniej Maców)
Zastopował programy amatorskie – tak znaczny jest
stopień komplikacji systemu, że amatorzy „wymiękli”
Ujednolicił obsługę – autorzy „rzucili” się na
„gotowce” (API Windows)
Rozwój sprzętu umożliwił realizację opracowań
softwarowych bogatych graficznie,
zorientowanych na UŻYTKOWNIKA i jego
postrzeganie funkcjonalności narzędzi
Prawdopodobnie
Przejście do techniki obiektowej (OOP-
Object Oriented Programming) jest być
może ważniejszą pojedynczą zmianą niż:
przejście od języka maszynowego do assemblera
przejście od assemblera do języków
proceduralnych
A każda z nich „odsuwa o krok” programistę
od nieprzyjaznego i obcego świata binariów
w stronę rzeczywistości
Jednolite podejście do Analizy-
Projektowania-Realizacji
Przez tworzenie struktur autonomicznych wiążących
zarówno dane jak i sposoby oddziaływania na nie
z ukryciem szczegółów realizacyjnych.
Niezmiernie wartościowe przy tworzeniu programów w
środowisku wieloakcyjnym oraz zmianie podejścia do
sterowania pracą programów z „prowokowanej”
(Ja daję Ci możliwość i czekam aż ją zaakceptujesz)
na obsługę zdarzeń
- rób co chcesz
(myszą, klawiaturą itp.) - jestem przygotowany na
prawie wszystkie twoje wymysły
Wymyślono podejście
obiektowe
Jesteśmy otoczeni przez obiekty
działające zgodnie ze swoją naturą (nie
można wydrukować samochodem czy
jeździć żarówką – wymienione operacje
są niewłaściwe, bo nie stanowią
elementu funkcjonowania obiektu)
Struktury danych awansowano do roli
niemal pierwszoplanowej
Obiekty (Smalltalk, Simula, C++,
Eiffel, Ada ... a od v. 5.5 też TP -
sama nazwa object z TP)
Podstawowe pojęcie programowania
zorientowanego obiektowo – OBIEKT TO:
Złożona struktura o ustalonej liczbie
elementów
pola - określające dane obiektu
metody - określające funkcjonowanie
Zdefiniowane jako
typ niezależny
typ potomny od już istniejącego
Przykład z zegarem
cyfrowym
Stan wyświetlany jest na wyświetlaczu
Regulacji możemy dokonać przyciskając H/M
W terminologii OOP (object oriented
programming) przyciski odpowiadają metodom
Klasa – model zegara a obiekt to konkretny
zegar zbudowany wg modelu, może mieć
własne stany czasu (pola)
Klasy enkapsulują pola i metody (łączą w
jedno tworząc funkcjonalną całość)
11:22
H M
OOP - definicje
Klasa jest wzorcem dla swoich zmiennych
– obiektów (opisem – dokumentacją)
Obiekt jest zmienną swojej klasy.
Pomimo, że różne zmienne mają wspólne
właściwości i charakterystyki, każdy
obiekt może znajdować się we własnym
stanie
Komunikat reprezentuje operację
wykonywaną na obiekcie. Metoda określa
sposób wykonania komunikatu
OOP - dziedziczenie
Jest możliwością tworzenia klas jako
potomnych od klasy rodzicielskiej
Klasy pochodne dziedziczą charakterystyki
i działanie klasy rodzicielskiej
Klasy pochodne definiują własne
(dodatkowe) operacje i nowe (dodatkowe)
właściwości, mogą
zmieniać/modyfikować/uzupełniać
operacje odziedziczone
Dziedziczenie - uwagi
NIE trzeba od początku definiować całej
rzeczywistości (wirtualnej), można oprzeć
się na klasach już zdefiniowanych,
modyfikując ich właściwości, czy
zmieniając/dodając funkcjonalność
Wspaniałe rozwiązanie w rozbudowanych
aplikacjach
Sukces wielu (np. Windows) oparty jest
na obiektach
OOP - polimorfizm
Cenna właściwość programowania
obiektowego
Umożliwia generowanie własnych
odpowiedzi na identyczne komunikaty
Pozwala każdej klasie na posiadanie
własnych wersji metod dostosowanych do
aktualnych potrzeb
(w literaturze określa się też jako opóźnione
wiązanie lub komunikat abstrakcyjny)
Siła techniki obiektowej
Enkapsulacja (obudowanie hermetyczne) -
integrowanie danych i operującego na nich
kodu
Dziedziczenie - tworzenie obiektów pochodnych
na bazie już istniejących z możliwością
zróżnicowania/wzbogacenia potomków
Polimorfizm (wielopostaciowość)- wykorzystanie
wspólnych elementów funkcjonalności klas
macierzystej i pochodnej -
każdy z „rodziny”
wie jak zapiąć płaszcz - jedni na guziki
(lewo/prawo zależnie od płci) inni na suwak
Klasy w C++
C++ rozszerza C dodając
możliwości programowania
zorientowanego obiektowo
(pierwotna różnica C-C++)
Deklarowanie klas bazowych
Jeśli deklarujemy klasę bazową (praprzodka)
class NazwaKlasy
{
private:
<prywatne dane składowe>
<prywatne funkcje składowe>
protected:
<chronione dane składowe>
<chronione funkcje składowe>
public:
<publiczne dane składowe>
<publiczne funkcje składowe>
};
Trzy poziomy widzialności
swoich składowych
Część prywatna: tylko funkcje składowe
danej klasy mają dostęp do składowych.
Zmienne (obiekty) nie mają dostępu
Część chroniona: tylko funkcje składowe
i klas pochodnych mają dostęp.
Zmienne nie mają dostępu
Cześć publiczna: widoczne dla
składowych, ich pochodnych i
zmiennych klasowych
Poziomy prywatności – uwagi
Części klas mogą występować w dowolnej
kolejności, także więcej niż jeden raz
Jeśli nie użyto etykiet sekcji private,
protected, public – kompilator traktuje
jako chronione (protected)
Należy unikać danych (pól/własności) w
części publicznej, aby ich odczyt/zmiana
mogła być realizowana tylko przez użycie
funkcji składowych
Deklarowanie hierarchii klas
class NazwaKlasy: [public] NazwaKlasyRodzicielskiej
{
private:
<prywatne dane składowe>
<prywatne funkcje składowe>
protected:
<chronione dane składowe>
<chronione funkcje składowe>
public:
<publiczne dane składowe>
<publiczne funkcje składowe>
};
Konstruktory i destruktory
Konstruktor to metoda klasy, której rolą jest
utworzenie obiektu danej klasy
W językach C/C++, C#, Java – ma nazwę taką jak
nazwa klasy, w Pascalu to metoda proceduralna
poprzedzona słowem constructor zamiast procedure
Destruktor to metoda klasy, której rolą jest
usunięcie obiektu
W C i Java brak konstruktora/destruktora powoduje
automatyczne ich utworzenie; w Pascalu jest
obowiązkowy wyłącznie przy polimorfizmie
Zadania konstruktora (bliżej)
obliczenie rozmiaru obiektu
alokacja obiektu w pamięci
wyczyszczenie (zerowanie) obszaru pamięci
zarezerwowanej dla obiektu
wpisanie do obiektu informacji łączącej go z
odpowiadajacą mu klasą (połączenie z
metodami klasy)
wykonanie kodu klasy bazowej
wykonanie kodu wywołanego konstruktora
Uwaga: istnieją różnice w językach OOP oraz/lub
ich implementacjach
O klasach i obiektach
Klasy służą do tworzenia opisu formalnego typu danych
(taka super struktura).
W przypadku klas wiadomo jednak "z definicji", że będzie
to bardziej złożony typ (tzw. agregat) zawierający
praktycznie zawsze i dane "tradycyjnych" typów i funkcje
(nazywane "metodami").
Podobnie jak definiując strukturę tworzy się nowy formalny
typ danych, tak i tu - definiując klasę tworzy się nowy typ .
Jeśli zadeklaruje się użycie zmiennych danego typu
formalnego, to takie zmienne to właśnie obiekty.
Innymi słowy, klasy stanowią definicje formalnego typu,
natomiast obiekty - to zmienne danego typu (danej klasy).
Nie ma przeszkód, by obiekty były składowymi innych
strukturalnych typów (tablice, rekordy, obiekty itp)
Klasy i obiekty
class Klasa
{
int p_tab[80]
public:
int dane;
void Inicjuj(void)
int Funkcja(int
our_param);
} Obiekt; //
OD RAZU
Zawiera:
Chronionąną tablicę
p_tab
Publicznie dostępną
całkowitą dane
Publicznie dostępne
metody Inicjuj i Funkcja
Mamy do czynienia
jednocześnie z
definicją i deklaracją
(globalną)
Można zadeklarować nasz
obiekt w bloku, gdzie będzie
potrzebny
class Klasa
{
int prywatna_tab[80]
public:
int dane;
void Inicjuj(void)
int Funkcja(int argument);
};
main()
{
...
Klasa Obiekt; //tu tylko deklaracja – definicja klasy globalnie!!!
...
Przypisanie wartości polom
obiektu, użycie metody
main()
{
...
Klasa Obiekt;
Obiekt.dane = 13;
Obiekt.Funkcja(44);
...
Widać podobieństwo
do korzystania ze
struktur:
Operator kropki (dot)
„wyłuskuje”
składową klasy
[!!!] UWAGA!
W C++ nie możemy zainicjować danych
wewnątrz deklaracji klasy:
class Klasa
{
private:
int p_tab[80] = { 1, 2, 3 }; //ŹLE !
public:
int dane = 123; //ŹLE !
...
Inicjowanie danych
Inicjowanie danych odbywa się w
programie:
przy pomocy przypisania (dane publiczne),
bądź
za pośrednictwem funkcji (zazwyczaj
publicznej) należącej do danej klasy i mającej
dostęp do wewnętrznych danych klasy/obiektu
(np. dane prywatne).
Inicjowania danych mogą dokonać także
specjalne funkcje - tzw. konstruktory.
Statusy
Dane znajdujące się wewnątrz deklaracji klasy mogą
mieć status: public, private, bądź protected.
Jeżeli część obiektu jest prywatna, to oznacza, że żaden
element programu spoza obiektu nie ma do niej dostępu.
W naszej Klasie prywatną część stanowi tablica złożona z
liczb całkowitych:
int p_tab[80];
Do (prywatnych) elementów tablicy dostęp mogą
uzyskać tylko funkcje związane (ang. associated) z
obiektem danej klasy.
Funkcje takie muszą zostać zadeklarowane wewnątrz
definicji danej klasy i są nazywane członkami klasy - ang.
Member functions.
Nie przesadzić
Funkcje mogą mieć status private i stać się dzięki temu
wewnętrznymi funkcjami danej klasy (a w konsekwencji
również prywatnymi funkcjami obiektów danej klasy).
Jest to jedna z najważniejszych cech nowoczesnego stylu
programowania w C++. Na tym polega idea hermetyzacji
danych i funkcji wewnątrz klas i obiektów.
Gdyby jednak cała zawartość (i dane i funkcje)
znajdujące się w obiekcie zostały dokładnie
"zakapsułkowane", to okazałoby się, że obiekt stał się
"ślepy i głuchy", a w konsekwencji - niedostępny i
(prawie) kompletnie nieużyteczny dla programu i
programisty.
Po co nam obiekt, do którego nie możemy odwołać się z
zewnątrz żadną metodą?
Pamiętać o public
Dane zawarte w obiekcie, podobnie jak
zwykłe zmienne wymagają
zainicjowania. Funkcja inicjująca dane -
zawartość obiektu musi zawsze
posiadać status public aby mogła być
dostępna z zewnątrz i zostać wywołana
Funkcje i dane dostępne z zewnątrz
stanowią tzw. INTERFEJS OBIEKTU.
Na początek – prosta klasa
zliczająca wybrane znaki
Definicja klasy:
class Licznik
{
private:
char znak;
int ile;
public:
void Inicjuj(char);
void PlusJeden(void);
};
Wewnętrzne pola
znak i ile służą do
przechowywania
informacji
Funkcje są tylko
zapowiedziane (to
prototypy)
void Licznik::Inicjuj(char
x)
{
znak = x;
ile = 0;
}
void
Licznik::PlusJeden(void)
{
ile++;
}
funkcje nie są definiowane
"niezależnie", lecz w
stosunku do własnej klasy
Aby wskazać, że funkcje
są członkami klasy Licznik
stosujemy
operator :: (oper.
widoczności/przesłaniania
- ang. Scope resolution
operator). Taki sposób
zapisu definicji funkcji
oznacza dla C++, że
funkcja jest członkiem
klasy (ang. Member
function).
A main?
(na początku odpowiednie
includy)
void main()
{
char znak_we; //zwykła zmienna znakowa
Licznik licznik; //deklaracja obiektu
Licznik.Inicjuj('A');
cout << "\nWpisz tekst zawierajacy litery A";
cout << "\nPierwsze wystąpienie kropki";
cout << "\n - oznacza Koniec zliczania: ";
for(;;) //pętla bez końca
{
cin >> znak_we;
if (znak_we == ‘.') break; //jednak koniec jest
if(licznik.znak == toupper(znak_we)) licznik.PlusJeden();
}
cout << "\nLitera " << licznik.znak
<< " wystapila " << licznik.ile
<< " razy.";
}
Ten pomysł prowadzi do błędu –
pola znak i ile są niedostępne
Potrzebne są dwie metody dostępu do
nich:
char Licznik::Pokaz(void)
{
return znak;
}
int Licznik::Efekt(void)
{
return ile;
}
I zmiana w main()
….
if(licznik.Pokaz() == toupper(znak_we))
licznik.PlusJeden();
}
cout << "\nLitera " << licznik.Pokaz()
<< " wystapila " << licznik.Efekt()
<< " razy.";
}
Pierwsza metoda „Pokaz” zwraca znak
Ten sam problem wystąpi przy próbie
pobrania od obiektu efektów jego pracy
- stanu pola licznik.ile. Do tego też
niezbędna jest autoryzowana do
dostępu metoda. Nazwiemy ją Efekt()
OCZYWIŚCIE potrzebne są zapowiedzi tych
metod w części publicznej definicji
klasy
Podobna klasa - nowocześniej
Inicjowanie za pomocą
konstruktora
class Licznik
{private:
char znak;
int ile;
public:
Licznik(char); //Konstruktor
void PlusJeden(void);
char Pokaz(void);
int Efekt(void);
};
Licznik::Licznik(char x) //Def. konstruktora
{
znak = x;
ile = 0;
}
void main()
{
Licznik licznik('A'); //Zainicjowanie obiektu
licznik
UWAGA Konstruktor nie ma typu
Jego nazwa jest identyczna z nazwą
klasy
Domyślnie tworzony konstruktor jest
bezparametryczny – tutaj jego
pokrycie mógłby tylko posłużyć do
wyzerowania pola ile
Rodzaje konstruktorów
Konstruktor domyślny – generowany automatycznie przez
kompilator, gdy autor nie zamieścił własnego
Konstruktor zwykły – zamieszczony przez autora, zwykle inicjuje
(także z parametrami domyślnymi) ukryte wartości obiektu
Konstruktor kopiujący -konstruktor, którego jedynym
argumentem niedomyślnym jest referencja do obiektu swojej
klasy. Jest on używany niejawnie wtedy, gdy działanie programu
wymaga skopiowania obiektu (np.: przy przekazywaniu obiektu
do funkcji przez wartość). Gdy konstruktor kopiujący nie został
zdefiniowany, jest on generowany niejawnie (nawet gdy są
zdefiniowane inne konstruktory) i domyślnie powoduje
kopiowanie wszystkich składników po kolei. Zablokowanie tego
konstruktora (np. przez umieszczenie go w sekcji prywatnej lub
chronionej) oznacza brak zezwolenia na kopiowanie obiektu.
Konstruktor konwertujący (C++) -konstruktor, którego jedynym
argumentem niedomyślnym jest obiekt dowolnej klasy lub typ
wbudowany. Powoduje niejawną konwersję z typu argumentu
na typ klasy własnej konstruktora.
Różnie można dziedziczyć
class NazwaKlasy: [
public/private
] NazwaKlasyRodzicielskiej
{
private:
<prywatne dane składowe>
<prywatne funkcje składowe>
protected:
<chronione dane składowe>
<chronione funkcje składowe>
public:
<publiczne dane składowe>
<publiczne funkcje składowe>
};
public
– u potomka tak jak u przodka, co widać to widać
private
– wszystko co odziedziczone staje się prywatną sprawą
potomka
Kolejność wywołań
konstruktorów
jak jest kaskada potomstwa?
Kolejność wywołań konstruktorów klasy bazowej, czy też
obiektów składowych danej klasy, jest określona kolejnością:
Konstruktory klas bazowych w kolejności w jakiej znajdują się
w sekcji dziedziczenia w deklaracji klasy pochodnej (bo
można w C dziedziczyć po kilku przodkach równolegle – nie
tylko w linii dziadek/ojciec ale ojciec1/ojciec2 – i inne
kombinacje).
Konstruktory obiektów składowych klasy w kolejności, w
jakiej obiekty te zostały zadeklarowane w ciele klasy.
Konstruktor klasy.
W
konstruktor może być dziedziczony i
wirtualny,
ze względu na brak dziedziczenia wielokrotnego oraz
konieczność dziedziczenia od klasy bazowej (TObject) nie
istnieje problem kolejności wywołań konstruktorów
Lista inicjalizacyjna
konstruktora - rola
Nadanie wartości danym podczas konstrukcji
.
Dla danej stałej (oznaczonej jako
) jest to jedyna
możliwość nadania jej wartości początkowej, podczas
gdy
nie-const mogą zostać zainicjalizowane
zarówno na liście inicjalizacyjnej jak też w formie
zwykłego przypisania im wartości.
Wywołanie
jedyny sposób na wywołanie takich konstruktorów.
Wywołania konstruktora klas bazowych - jest to jedyny
sposób na wywołanie takich konstruktorów.
Gdy klasa bazowa lub obiekt składowy klasy posiada
konstruktor domniemany, jego wywołanie nie musi się
pojawić na liście inicjalizacyjnej
Destruktor
obowiązkowa w C nazwa ~NazwaKlasy
w Pascalu – słowo kluczowe destructor
Destruktor - w
obiektowych językach programowania
specjalna
, wywoływana przed usunięciem
.
Pod względem funkcjonalnym jest to przeciwieństwo
. Jest pojedynczy, niedozwolone przeciążanie bo
brak parametrów
Destruktor ma za zadanie wykonać czynności składające się
na jego "zniszczenie", inne niż zwolnienie zadeklarowanej
pamięci, przygotowujące obiekt do fizycznego usunięcia. Po
jego wykonaniu obiekt znajduje się w
i nie
można już wtedy z tym obiektem zrobić nic poza fizycznym
usunięciem lub ponownym wywołaniem konstruktora.
Destruktor zwykle wykonuje takie czynności, jak zamknięcie
połączenia z
/
/
, odrejestrowanie
się z innych obiektów, czasem również zanotowanie faktu
usunięcia, a także usunięcie obiektów podległych, które
obiekt utworzył lub zostały mu przydzielone jako podległe
(jeśli jest ich jedynym właścicielem) lub wyrejestrowanie się z
jego użytkowania (jeśli jest to obiekt przezeń współdzielony).
Składowe statyczne
W C/C++ składowa klasy (pole lub funkcja) może być
deklarowana jako statyczna
Publiczna metoda statyczna może być wywołana, nawet
wtedy gdy nie istnieje obiekt danej klasy
(nazwa_klasy::składowa)
Statyczność deklarujemy przy pomocy słowa static, np.
static int liczbaObiektów;
Po utworzeniu obiektu danej klasy, dysponuje on zestawem
pól tej klasy (każdy obiekt tej klasy własnym zestawem)
natomiast w przypadku pól statycznych występują
one jednoinstancyjnie
Pola statyczne muszą być deklarowane na zewnątrz jako
globalne
Cel użycia:
Komunikacja między obiektami
Umieszczenie informacji wspólnej dla wszystkich obiektów
Funkcje i klasy
zaprzyjaźnione
Istnieją sytuacje, w których funkcja nie będąca metodą
danej klasy powinna mieć dostęp do składowych
prywatnych lub chronionych tej klasy
Zwykła funkcja zaprzyjaźniona
friend void Spr( const Wekt3&);
Zaprzyjaźniona metoda innej klasy
friend void T4::Spr(const Wekt3&);
Np. porównanie identyczności danych pochodzących z
dwóch baz
Zwykle deklarację zaprzyjaźnienia umieszcza się
na początku definicji klasy, a funkcja
zaprzyjaźniona ma dostęp do danych chronionych i
prywatnych, tak jak gdyby były one publiczne (tzn.
potrzebna jest nazwa obiektu, w którego klasie ją
„zaprzyjaźniono” i „.” nazwa składowej)
przykład – klasa liczb
zespolonych
class – klasycznie
class lZespolone
{
protected:
double Re; double Im;
public:
lZespolone (double re=0, double im=0)
{przypisz(re,im);}//zwykły konstruktor
lZespolone(lZespolone& zrodlo);//konstr. kopujący
void przypisz(double re=0,double im=0);//wpisanie
void dajRe(){return Re;} //zwraca Re
void dajIm(){return Im;} //zwraca Im
friend lZespolone dodaj(lZespolone& a1, lZespolone& a2);
}
Zespolone – brakujące
metody
lZespolone::lZespolone(lZespolone& zrodlo)
{Re=zrodlo.Re; Im=zrodlo.Im;}
lZespolone::przypisz(double re, double im)
{Re=re; Im=im;}
//i w końcu zaprzyjaźnione dodawanie
lZespolone dodaj(lZespolone& a1,lZespolone& a2)
{
lZespolone wynik(a1); //pomocniczy wynik skopiowany
wynik.Re+=a2.Re;
wynik.Im+=a2.Im;// dodane składowe drugiego argumentu
return wynik;
}
Pozostaje banalny main
Zespolone – poprawka z
operatorem
Wygodnie by było, gdyby:
Można użyć znaku podstawienia (np.
c3=c2;)
Można użyć znaku dodawania (np.
c3=c1+c2;)
Lub w „porozumieniu” z iostream
wyprowadzać w prosty sposób (np.
cout<<c;), w jakiejś ustalonej formie …
Z ostream (przydatny)
friend ostream& operator <<(ostream & os,
lZespolone& z); // to w klasie
ostream& operator <<(ostream& os, lZespolone&
z)
{os<<"("<<z.Re<<"+j"<<z.Im<<")";
return os;}
// to jako definicja zaprzyjaźnionego operatora
Przeciążanie operatorów
Język C++ umożliwia przeciążanie operatora tzn.
zmianę jego znaczenia na potrzeby danej klasy
W tym celu definiujemy funkcję o nazwie
operator op
gdzie op to nazwa (symbol/symbole)
konkretnego operatora
Taka funkcja (zwykła lub metoda) musi posiadać
przynajmniej jeden argument danej klasy, co
uniemożliwia zamianę typowych (wbudowanych)
operatorów dla takich typów jak int, float itp.
Prosty przykład przeciążania
Powiedzmy, że istnieje klasa TWektor z
metodą Dodaj dodawania do własnego pola
zawierającego składowe n-elementowego
wektora p, składowych innego wektora:
void TWektor::Dodaj(TWektor x)
{ int i;
for (i=0;i<n;i++) p[i]+=x.p[i];
}
//metodę wywołujemy np. a.Dodaj(b);
//gdzie a i b są obiektami klasy TWektor
//na pewno wygodniej byłoby zapisać a+=b;
Przeciążanie operatora +=
W klasie pochodnej np. Wektor definiujemy:
void Wektor::operator += (Wektor b)
{ for (int i=0;i<n;i++) p[i]+=b.p[i]; }
oczywiście, wcześniej w klasie Wektor nie możemy
zapomnieć o dodaniu wiersza
void operator += (Wektor);
Jeżeli w treści funkcji korzystamy z pól prywatnych
(lub chronionych) to funkcja operator musi mieć
statut zaprzyjaźnionej
W C++ można przeciążać większość operatorów,
poza (czterema): :: *
(jednoargumentowy adresu)
? :
Po przeciążeniu zachowane są pierwotne
reguły pierwszeństwa, łączności, liczby
argumentów
Przeciążanie cd
Funkcja definiująca przeciążenie operatora
nie musi być metodą
*
:
void operator += (Wektor &a, Wektor &b)
{ for (int i=0;i<a.n;i++) a.p[i]+=b.p[i]; }
Dzięki przekazywaniu referencyjnemu
parametrów, możliwa jest zmiana
argumentu!
Jeśli operator jest metodą, ma o jeden
parametr mniej (domyślnie działa na klasie)
*
Cztery operatory: = [ ] ( ) -> muszą być
zdefiniowane jako metody
Przeciążanie =
Operacja przypisania jednego obiektu drugiemu
jest zawsze wykonalna, odpowiednią
implementację podstawienia zapewnia kompilator
Jeśli w klasie istnieją pola dynamiczne,
wygenerowany przez kompilator operator =
będzie działał nieprawidłowo (jak automatyczny
konstruktor kopiujący)
Jeśli chcemy, by w takim przypadku prawidłowo
działało:
a=b;
funkcję przeciążającą podstawienie musimy
zaprojektować sami (jako metodę klasy – bo =
jest jednym z czterech operatorów tego
wymagających)
Przeciążanie = cd
Trzymając się dalej klasy Wektor (z dynamicznym
zestawem n składowych wektora p
void Wektor :: operator = (Wektor& b)
// najpierw usuń część dynamiczną
{ delete [n] p;
// utwórz własne pole p
p=new int[n=b.n];
// przepisz do nowego z b
for (int i=0;i<b.n;i++) p[i]=b.p[i];
} // wyżej p po lewej to nasze własne,
//po prawej z argumentu
Przeciążanie = cd
Przykład poprawnie działa dla
b=a; // a i b są typu wektor
UWAGA źle działa dla banalnego a=a;
ponieważ najpierw usunie źródło
danych a potem je przepisze skąd?!
Poprawa pierwsza: warunkowe
usunięcie, gdy to są różne obiekty
if (this != &b) {
delete [n]p; p=new int [n=b.n];}
Przeciążanie = cd
Uwaga ! Źle działa dla
a=b=c; // a,b,c typu wektor
Ponieważ powyższe działa jak a=(b=c); a
przecież metoda operator = jest typu void
Pomaga użycie dla niej typu Wektor&
(koniecznie z referencją &) i dodanie wiersza
return *this;
Występujący tu wskaźnik this oznacza
samego siebie i często występuje niejawnie
(kompilator go używa do wyrażeń po lewej
stronie operacji podstawienia)
Przeciążanie = cd –
ostateczna wersja
Wektor& Wektor:: operator = (Wektor& b)
{ if (this!=&b) { //gdy są różne
if (n!=b.n){ // i mają różne długości
delete [n] p; p=new int[n=b.n]; //twórz
swoje
}
// przepisz do nowego z b gdy różne
for (int i=0;i<b.n;i++) p[i]=b.p[i];
}
return *this; //zwróć samego siebie
}
Warto pamiętać, że przeciążanie nie jest
dziedziczone i w klasach pochodnych trzeba
je definiować oddzielnie, a korzystające z pól
chronionych i prywatnych muszą być
zaprzyjaźnione
Przeciążanie uwagi
Intuicyjnie pożądane przeciążanie znacznie
utrudnia czytanie tekstu. Jest techniką z wyższej
półki, której towarzyszą częste manieryczne
„ściemniania” treści
Analiza takiego programu jest trudna, choć
autorowi ułatwia pracę, a „tajne” zwykle wtyczki
zaprzyjaźnione są normą (niepublikowaną)
Dodatkową trudność stanowi przecież
standardowe rozbicie definicji i deklaracji klasy na
dwa pliki:
*.h i *.cpp (o ile mamy dostęp do prawdziwych
źródeł)
Gdy dodać do tego ciągłe udoskonalanie młodego
przecież języka i zmiany standardów, różnice
implementacyjne – to analiza i poprawianie C jest
super krzyżówką
Przeciążanie - wnioski
Trzeba dokładnie się zastanowić, które
operatory przeciążać budując nową klasę
Bezwzględnie wymagane jest przeciążanie =
(jak i budowa własnego konstruktora
kopiującego) przy polach dynamicznych
Przeciążanie należy stosować, gdy poprawi to
czytelność programu – w innych przypadkach
lepiej zbudować odpowiednie metody
I pamiętać – przeciążane operatory nie są
dziedziczne