Informatyka
" Dr inż. Lucjan Miękina
" Katedra Robotyki i Mechatroniki
" D-1, p.412, tel. 012 617 3510
" E-mail: miekina@agh.edu.pl
" Strona www: http://galaxy.uci.agh.edu.pl/~miekina
Obiektowe programowanie w języku C++
Język C został uzupełniony o tzw. rozszerzenie obiektowe, umożliwiające i wspierające
programowanie zorientowane obiektowo (OOP). Realizowane to jest przez wprowadzenie
szeregu nowych słów kluczowych i mechanizmów języka.
Rozwój technik programowania:
" liniowe - brak procedur i funkcji, wykorzystanie instrukcji skoku warunkowego i
bezwarunkowego jako substytutów strukturalizacji. Realizowane przez język maszynowy,
assembler i wczesne wersje Basic-a.
" proceduralne - opiera się na przepływie danych przez odpowiednio dobrane procedury i
funkcje, realizujÄ…ce czÄ…stkowe zadania przetwarzania tych danych. ObowiÄ…zuje schemat:
Wybierz procedurę realizującą operację na danych, do kodowania procedury użyj
najlepiej dostosowanego algorytmu. Języki : Fortran, Algol, Pascal, C, Ada, itd.
" modułowe - wymienione wyżej języki proceduralne zawierają wsparcie do modularnego
programowania, które opiera się na wyodrębnieniu części reprezentacji problemu w postaci
modułu, będącego zbiorem metod (funkcji) i danych, na których te metody operują (stałe,
zmienne). Najbardziej istotną nową cechą podejścia modułowego jest możliwość ukrycia
danych modułu przed nieautoryzowanym dostępem.
" obiektowe - gdzie położono nacisk na :
tworzenie reprezentacji problemu w postaci zbliżonej do niego samego
Å‚atwÄ… konserwacjÄ™ i rozbudowÄ™ oprogramowania
możliwość wielokrotnego użycia modułów programowych (ang. reusability)
lepsze wsparcie dla budowy dużych systemów i pracy grupowej
" komponentowe, będące dalszą ewolucją obiektów w kierunku zgodności na poziomie
binarnym, a nie zródłowym. Oznacza to, że tworzy się komponenty poddane kompilacji i
konsolidacji (a więc gotowe do użycia), które zapewniają jednolity interfejs do usług,
których dostarczają.
Cechy obiektowego podejścia do programowania.
Niezależnie od języka implementacji, istnieją ogólne zasady obiektowego podejścia do tworzenia
oprogramowania, nazywane modelem obiektowym. Składa się on z poniższych pojęć:
lð
obiekt każdy byt (pojęcie lub rzecz), mający znaczenie w dziedzinie zastosowania
lð
klasa uogólnienie zbioru obiektów, mających takie same atrybuty, operacje i znaczenie. Jeśli pewne
klasy mają wspólną część, to powinna ona być ich wspólną klasą bazową (lub inaczej nadklasą).
lð
komunikat (ang. message) specyfikacja wymiany informacji między obiektami, zawierająca zlecenie
wykonania określonej operacji lub wymiany danych.
lð
abstrakcja proceduralna i abstrakcja danych - polega na ukrywaniu nieistotnych cech obiektu w
trakcie operacji z nim związanych, dzięki temu że obiekt może być wyposażony w wiedzę o swych
operacjach. Dzięki temu ułatwia się operacje i unika błędów, a także skraca się notację - można lepiej
panować nad złożonymi systemami lub algorytmami.
lð
hermetyzacja (ang. encapsulation) - polega na selektywnym dostępie do szczegółów obiektu
(atrybutów i operacji) i dzięki temu zapewnieniu niezależności (braku niepotrzebnych sprzężeń).
Prywatne atrybuty i operacje są niedostępne dla otoczenia, stanowiąc elementy wewnętrznego
mechanizmu działania obiektu. Publiczne atrybuty i operacje są dostępne dla otoczenia, umożliwiając
odwoływanie się do obiektu za pomocą komunikatów. Chronione atrybuty i operacje są dostępne w
ciÄ…gu podklas danej klasy.
lð
dziedziczenie (ang. inheritance) - polega na przejmowaniu cech pewnych obiektów przez inne, z
równoczesnym dodawaniem nowych cech - tworzenie tzw. specjalizacji. Oznacza to
przyporządkowanie atrybutów i operacji do klas zgodnie z hierarchiczną zależnością, jakiej podlegają
klasy. Dzięki temu można budować systemy złożone, lecz elastyczne w zastosowaniu.
" polimorfizm (ang. polymorphism) polega na nadaniu takiej samej nazwy różnym atrybutom i
operacjom w klasach należących do jednej hierarchii (od nadklasy przez wszystkie podklasy). Pozwala
m.in. na wywołanie tzw. wirtualnej metody (procedury lub funkcji), która może mieć różne znaczenie
w zależności od kontekstu wywołania.
Języki realizujące wsparcie dla techniki programowania obiektowego.
Zestawienie według popularności w zastosowaniach:
C++, Java, Pascal, Ada 95, SmallTalk, Modula,
W oparciu o wymienione zasady modelu obiektowego, powstaje język bardziej zbliżony do
opisywanego problemu (w odróżnieniu od zbliżonego do maszyny).
Dzięki tym założeniom program obiektowo zorganizowany może się składać prawie wyłącznie z
deklaracji zmiennych obiektowych (instancji); w trakcie tych deklaracji wykonywane sÄ…
wszystkie operacje inicjalizacji obiektów (definiowania ich stanu początkowego), tak by
mogły one zacząć funkcjonować w otoczeniu innych obiektów. Dalsze funkcjonowanie
każdego obiektu polega na odbiorze komunikatów wysyłanych przez inne obiekty i
odpowiednim ich przetwarzaniu, w ramach którego możliwe jest również wysyłanie
własnych komunikatów, czyli oddziaływanie na otoczenie. Taki model daje większe
możliwości, co jest związane głównie z decentralizacją funkcjonalności (każda klasa
odpowiada za swoją część operacji złożonego systemu) i nie jest potrzebny żaden
nadrzędny mechanizm, który musiałby być odpowiednio bardziej złożony.
Typ obiektowy (klasa).
Klasa może być traktowana jako specjalny rodzaj typu strukturowego, składający się z:
" pól (jak w strukturze). Pola klasy służą do opisu stanu obiektu, a więc opisują jego
własności. Każdy typ obiektowy jest zwykle opisany unikalnym zbiorem własności.
" metod (będących w istocie funkcjami znanymi z języka C) zadeklarowanych wewnątrz
deklaracji klasy. Metody służą do wykonywania operacji zdefiniowanych dla typu
obiektowego, a więc definiują zachowanie się obiektu. Metody operują w założeniu na
polach dostępnych w klasie i mogą wywoływać inne metody klasy, a także funkcje
zewnętrzne.
Klasa jest blisko związana ze strukturą - po zamianie słowa kluczowego struct na słowo class z
deklaracji struktury uzyskuje się deklarację klasy. Ponieważ klasa jest uogólnieniem struktury,
więc większość właściwości struktur przechodzi na klasy (m.in. sposób deklarowania,
identyfikowania i dostępu do pól). W związku z tym dalej omawia się tylko te właściwości klas,
które nie wynikają z opisu struktur.
UWAGA: W języku C++ struktura może mieć funkcje.
Składnia deklaracji klasy.
Deklaracja klasy pierwotnej (czyli nie wywodzącej się z innej, już wcześniej zdefiniowanej) ma
postać:
class
{
[public:]
[lista deklaracji pól/metod publicznych]
[protected:]
[lista deklaracji pól/metod chronionych]
[private:]
[lista deklaracji pól/metod prywatnych]
};
Name jest nazwÄ… klasy, czyli nowo tworzonego typu obiektowego.
Ciało klasy składa się z dowolnej liczby sekcji, różniących się co do sposobu dostępu do zadeklarowanych
w nich składników. Wyróżnia się sekcje:
" prywatne (private) . Składniki zdefiniowane w sekcji prywatnej są widoczne jedynie w obrębie
obiektów danej klasy, tzn. można ich używać wewnątrz metod (funkcji) tej klasy. Jeśli zdefiniowane są
pola prywatne, to dla zapewnienia możliwości odczytu ich wartości spoza klasy można zdefiniować
specjalne funkcje dostępu, które mogą być publiczne lub chronione. Podobne funkcje można
zdefiniować dla zapewnienia zmiany wartości wybranych pól prywatnych.
" chronione (protected). Składniki zdefiniowane w sekcji chronionej są widoczne jedynie w obrębie
obiektu danej klasy i klasy pochodnej.
" publiczne (public). Składniki zdefiniowane w sekcji publicznej są widoczne wszędzie (podobnie jak dla
struktur). W tej sekcji zwykle muszą być zadeklarowane specjalne funkcje klasy: konstruktor i
destruktor, o ile są przewidziane (jeśli nie są przewidziane, to kompilator generuje ich domniemane
wersje, typowo nie wykonujące żadnych specyficznych operacji).
W ciele klasy może wystąpić dowolna ilość sekcji w dowolnej kolejności.
Reprezentacja obiektów w pamięci.
Składniki danej sekcji są umieszczone w pamięci w takiej kolejności jak je zadeklarowano, natomiast
kolejność ułożenia sekcji zależy od implementacji (czyli kompilatora).
Obiekty mogą być alokowane jako zmienne:
1. lokalne
2. modułowe lub globalne
3. dynamiczne, tworzone z użyciem new i usuwane z użyciem delete
4. składowe innych obiektów (klas lub tablic)
W przypadku 1 i 2 uzyskuje się maksymalną wydajność kosztem zmniejszonej elastyczności. W przypadku
3 istnieje dodatkowe obciążenie na skutek wykonywania procedur przydziału i zwalniania pamięci, ale
uzyskuje się pełną kontrolę nad czasem istnienia obiektu. W przypadku 4 decyduje sposób tworzenia
nadrzędnego obiektu.
Konstruktor i destruktor
Są to specjalne metody należące do klasy. Konstruktor nosi nazwę taką jak nazwa klasy (typu
obiektowego), a destruktor takÄ… samÄ…, ale poprzedzonÄ… znakiem ~.
" Wywołanie konstruktora ma na celu utworzenie i zainicjowanie obiektu. Konstruktor jest
wywoływany na rzecz pewnego amorficznego (bezpostaciowego) obszaru pamięci, który
jest zamieniany (formatowany) na obiekt. Wewnątrz konstruktora są dostępne wszystkie
składniki klasy (pola i metody). Klasa może definiować więcej niż 0 konstruktorów
(konieczne jest by były to funkcje przeciążone o różnej ilości i typie parametrów
formalnych) o funkcjonalności dostosowanej do potrzeb; każdy z nich może inicjować
obiekt tej samej klasy w inny sposób. Jeśli nie jest zdefiniowany żaden konstruktor, to
automatycznie generuje siÄ™ konstruktor domniemany, bezparametrowy.
" Klasa może posiadać konstruktor kopiujący, wywoływany z jednym argumentem typu tej
samej klasy.
" Wywołanie konstruktora może być jawne (przy dynamicznym tworzeniu obiektu
operatorem new) i niejawne (w miejscu deklaracji zmiennej odpowiedniego typu).
" Zadaniem destruktora jest zniszczenie obiektu, czyli przekształcenie tej części pamięci
operacyjnej, którą zajmował obiekt w amorficzny obszar pamięci.
" Wywołanie destruktora może być jawne (przy usuwaniu operatorem delete obiektu
utworzonego dynamicznie) i niejawne (w miejscu wyjścia sterowania z zakresu deklaracji
zmiennej odpowiedniego typu).
" UWAGA: samo wywołanie destruktora nie powoduje zwolnienia pamięci zajmowanej
przez obiekt, a jedynie zniszczenie obiektu.
Typ referencyjny .
Typem referencyjnym (odniesienia nie jest to ścisłe określenie, a jedynie próba dodatkowego
objaśnienia) jest taki typ, którego zmienne są synonimami zmiennych typu pokrewnego.
Każde odwołanie do zmiennej typu referencyjnego jest uznawane za odwołanie do
zwiÄ…zanej z niÄ… przez referencjÄ™ innej zmiennej.
Podobnie jak w przypadku zmiennych wskaznikowych, konieczne jest odpowiednie
zainicjowanie zmiennej referencyjnej, tak by było wiadomo z jaką zmienną jest ona
skojarzona (lub inaczej mówiąc jakiej zmiennej jest synonimem).
Deklaracja zmiennej referencyjnej wymaga użycia symbolu & (ampersand) pomiędzy nazwą
typu bazowego i nazwÄ… zmiennej referencyjnej.
Przykład:
int Fix = 12;
int &Ref = Fix; /* deklaracja i inicjalizacja zmiennej
referencyjnej Ref */
Ref = Ref + 1; // ta operacja dotyczy zmiennej Fix !!
Zmienna Ref jest synonimem zmiennej Fix, każda operacja na Ref dotyczy w istocie zmiennej
Fix.
Zastosowanie referencji.
" Modyfikacja sposobu przekazywania argumentów wywołania do funkcji. Jeśli argument
jest przekazany przez referencję, to funkcja operuje bezpośrednio na tym argumencie, a nie
na jego kopii, jak w standardowym C.
" Modyfikacja sposobu zwracania rezultatu wywołania funkcji.
SÅ‚owo kluczowe this.
Słowo kluczowe this representuje wskaznik do obiektu, którego metoda jest w danej chwili wykonywana.
Jest to zatem wskaznik do bieżącego obiektu, dlatego słowo kluczowe this może się pojawić tylko w kodzie
metod klasy.
Domyślnie kompilator traktuje identyfikator każdej składowej zdefiniowanej w klasie tak, jak gdyby był
on poprzedzony napisem this-> , czyli był składnikiem klasy. Przy wywołaniu dowolnej metody klasy,
konieczne jest podanie jej nazwy (z wymaganymi parametrami wejściowymi w nawiasie okrągłym), a jeśli
wywołuje się metodę z innej klasy niż bieżąca lub z innego obiektu niż bieżący przed nazwą podaje się
nazwÄ™ obiektu lub wskaznik do niego, np.:
class PierwszaKlasa {
public:
void Metoda();
};
int main() {
PierwszaKlasa ObiektPierwszaKlasa; // obiekt
PierwszaKlasa* pObiektPierwszaKlasa = &ObiektPierwszaKlasa; // wskaznik
ObiektPierwszaKlasa.Metoda(); // wywołanie za pomocą ident. obiektu
pObiektPierwszaKlasa->Metoda(); // wywołanie za pomocą wskaznika
}
Skutkiem takiego sposobu wywołania metody jest zainicjowanie wskaznika this wartością adresu obiektu
ObiektPierwszaKlasa lub adresu przechowywanego przez wskaznik pObiektPierwszaKlasa. Dzieje siÄ™ to w
prologu każdej metody należącej do klasy, tak że metoda posiada zawsze aktualną wartość wskaznika this
(wie na rzecz którego obiektu działa).
Możliwe zastosowania wskaznika this:
" odczyt adresów obiektów, patrz metoda Info w klasie complex
" porównywanie adresów obiektów, patrz metoda Set w klasie complex
" odróżnianie identyfikatorów składowych klasy (pól lub metod) od innych identyfikatorów, patrz
pierwszy konstruktor klasy complex
" zwracanie z metody referencji do bieżącego obiektu. Zwykle ma to postać instrukcji:
return *this;
Przykład deklaracji klasy
Poniżej zadeklarowano prostą klasę complex , która opisuje wybrane własności i operacje
zbioru liczb zespolonych.
UWAGA: biblioteki standardowe dostarczane obecnie z kompilatorami C++ zawierajÄ… klasy
implementujące pełną funkcjonalność właściwą dla danych typu zespolonego, w tym również
funkcje operatorowe +, -, *, / i inne, które dla tych liczb definiują na nowo wymienione
standardowe działania.
Klasa typowo jest zadeklarowana w pliku nagłówkowym, a zdefiniowana w pliku zródłowym.
Plik nagłówkowy complex.h
class complex
{
double Re;
double Im;
public:
complex(double re, double im); // konstruktor
complex(complex& c); // konstruktor kopiujÄ…cy
// destruktor
~complex();
void Set(complex& c);
void SetRe(double re) { this->Re = re; };
void SetIm(double im) { Im = im; };
double GetRe(void) { return Re; };
double GetIm(void) { return Im; };
double abs(void);
void Info(char* name);
};
Jak widać, niektóre metody zostały zarówno zadeklarowane jak i zdefiniowane w tym pliku. Są
to tzw. metody otwarte (ang. inline), czyli takie których kod będzie umieszczony wszędzie tam,
gdzie następuje ich wywołanie.
Plik zródłowy complex.cpp, gdzie podany jest kod brakujących funkcji:
#include
#include
#include "complex.h"
// konstruktor
complex::complex(double Re, double Im) {
this->Re = Re; this->Im = Im;
}
// konstruktor kopiujÄ…cy
complex::complex(complex& c) {
Re = c.Re; Im = c.Im;
}
// destruktor
complex::~complex(){
}
// f. do kopiowania pol obiektu
void complex::Set(complex& c) {
if(this == &c)
return;
Re = c.Re; Im = c.Im;
}
// f. do obliczania modułu l. zespolonej
double complex::abs(void) {
return sqrt(Re * Re + Im * Im);
}
void complex::Info(char* name) {
printf("\n %s: %p, (%12.5e, %12.5e)", name, this, Re, Im);
}
Prosty przykład programu obiektowego
Program korzysta z klasy complex:
#include "complex.h"
int main()
{
// nie-jawne tworzenie obiektow:
// konstruktorem bezparametrowym
complex c;
// ust. wartosci poczatkowej Re
c.SetRe(1.);
// ust. wartosci poczatkowej Im
c.SetIm(2.);
complex c2(100., 200.); // konstruktorem z parametrami
complex c2bis(c2); // konstruktorem kopiujacym
// wypisanie informacji o obiektach
c.Info("c");
c2.Info("c2");
c2bis.Info("c2bis");
return 0;
}
Prosty przykład programu obiektowego 2
Program korzysta z klasy complex, ale tworzy obiekty w sposob jawny, uzywajac operatora
new, a do usuwania operatora delete. Utworzone obiekty znajda sie w obszarze sterty.
#include "complex.h"
int main()
{
// jawne tworzenie obiektow:
complex* c = new complex; // konstruktorem bezparametrowym
// ust. wartosci poczatkowej Re
c->SetRe(1.);
// ust. wartosci poczatkowej Im
c->SetIm(2.);
complex* c2 = new complex(100., 200.); // konstruktorem z parametrami
complex* c2bis = new complex(*c2); // konstruktorem kopiujacym
// wypisanie informacji o obiektach
c->Info("c");
c2->Info("c2");
c2bis->Info("c2bis");
// usuwanie obiektow, konieczne by uniknac przeciekow pamieci
delete c;
delete c2;
delete c2bis;
return 0;
}
Funkcje operatorowe
Funkcje te pozwalają definiować semantykę (czyli znaczenie) operatora. Język C++ posiada
niejawnie zdefiniowane standardowe operatory, poznane wcześniej. Ponieważ każda funkcja
może być przeciążona, można nadawać nowy, dodatkowy sens znanym operatorom.
UWAGA: nie można definiować nowych operatorów.
Funkcja operatorowa ma nazwÄ™:
operator#
gdzie # jest jest nazwą jednego z predefiniowanych operatorów.
Operator może być zdefiniowany jako:
1. funkcja składowa klasy. W tego typu sytuacji lewy (albo jedyny) argument operacji
pochodzi z klasy, a prawy (o ile istnieje) jest dostarczany w postaci argumentu funkcji
operatorowej (w tym przypadku nazwanego par ).
2.funkcja globalna. Wtedy ma ona tyle parametrów, ilu argumentów wymaga dany operator.
Poniżej podano przykład operatora dodawania zrealizowanego dla klasy complex w postaci
funkcji składowej klasy. Funkcja ta jest zdefiniowana w ciele klasy complex, (jeden z
wcześniejszych przykładów) w miejscu kodu tuż za definicją konstruktora:
complex operator+(const complex& par) {
return complex(Re + par.Re, Im + par.Im);
}
Dla porównania, funkcja operatorowa nie należąca do klasy complex i realizująca analogiczną
operację ma postać:
complex operator+(const complex& lpar, const complex& ppar) {
return complex (lpar.Re + ppar.Re, lpar.Im + ppar.Im);
}
Nieco inną postać musi posiadać operator kopiujący dla klasy, ponieważ musi on zwrócić
wskazanie na obiekt, do którego zachodzi kopiowanie wartości z prawostronnego argumentu
operacji kopiowania.
Przykład:
complex& complex::operator=(complex& r)
{
Re = r.Re;
Im = r.Im;
return *this;
}
Można zauważyć, że:
" typ rezultatu jest referencjÄ… do klasy
" argumentem instrukcji return jest wyłuskanie obiektu wskazywanego przez this.
Inną kategorią często spotykanych funkcji operatorowych są operatory porównania (==, !=, <,
<=, >, >=), które mają typ rezultatu ustalony jako bool, np.:
bool complex::operator==(complex& r)
{
if(Re == r.Re && Im == r.Im)
return true;
return false;
}
Przykład programu wykorzystującego klasę complex z funkcjami operatorowymi:
#include complex.h
int main() {
complex c, c1(1., 1.), c2(2., 2.);
// dodawanie i przypisanie, teraz mozliwe w notacji matematycznej !!!
c = c1 + c2; // a NAPRAWDE: c = c1.operator+(c2);
c.Info("c = c1 + c2");
return 0;
}
Rezultat wykonania:
c = c1 + c2: 80470d4, ( 3.00000e+00, 3.00000e+00)
Statyczne składniki klasy
Klasa jest typem danych, a nie obiektem. Każdy obiekt danej klasy standardowo posiada swoją
kopię pól. Jednak w pewnych zastosowaniach należy się posłużyć innym modelem, w którym
wybrane pola są wspólne dla wszystkich obiektów danej klasy. Można je wtedy zadeklarować
wewnątrz klasy z użyciem modyfikatora static:
// Plik deklaracji Klasa1.h
class Klasa1 {
static char* ClassName; // deklaracja pola statycznego
public:
void Metoda();
};
// Plik definicji Klasa1.cpp
#include "Klasa1.h"
#include
char* Klasa1::ClassName = "Klasa1"; // definicja pola statycznego
void Klasa1::Metoda() {
std::cout << "\nKlasa1::Metoda(): ClassName=" << ClassName;
}
Zaprzyjaznienia
Zaprzyjaznienie jest deklaracją, która nadaje wybranej funkcji zewnętrznej lub innej klasie takie
same przywileje dostępu do składników pewnej klasy, jakie mają jej własne metody.
Przykład:
class ExtClass; // deklaracja zapowiadajÄ…ca klasy
class C {
int Priv; // pole prywatne
friend class ExtClass; // inna klasa zaprzyjazniona z klasÄ… C
friend int ExtFun(void); // zewn. funkcja zaprzyjazniona z klasÄ… C
};
Mechanizm ten definiuje dodatkowy poziom udostępniania składników klasy wybranym
funkcjom lub klasom, stąd należy go używać z ostrożnością.
Deklaracja funkcji lub klasy zaprzyjaznionej może być umieszczona w sekcji publicznej lub
prywatnej, bez zmiany znaczenia.
Podobnie jak dla metody klasy, funkcja zaprzyjazniona jest zadeklarowana wewnÄ…trz deklaracji
klasy i dlatego staje się elementem zewnętrznego interfejsu klasy.
W przypadku gdy wszystkie metody danej klasy są zaprzyjaznione z inną klasą, można
zdefiniować zaprzyjaznienie dla całej klasy; w przykładzie powyżej klasa ExtClass jest
zaprzyjazniona z klasÄ… C.
Przykład funkcji zaprzyjaznionej z klasą.
Funkcja Info, będąca składową klasy Rysunek jest zaprzyjazniona z klasą Pt i dzięki temu może
mieć dostęp do jej prywatnych pól X i Y:
#include int main() {
using namespace std;
Pt pt;
class Pt;
Rysunek rys;
class Rysunek {
rys.Info(pt);
public: }
Rezultat wykonania:
void Info(Pt& p);
}; X = 1
class Pt { Y = 2
int X, Y;
friend void Rysunek::Info(Pt& pt);
public:
Pt() : X(1), Y(2) { }
};
void Rysunek::Info(Pt& p) {
UWAGA: nie tylko funkcje składowe klasy
cout << "X = " << p.X << endl;
mogą być zaprzyjaznione z innymi klasami
cout << "Y = " << p.Y << endl;
} - także funkcje globalne.
Przykład klasy zaprzyjaznionej z inną klasą.
Klasa Rysunek jest zaprzyjazniona z klasą Pt i dzięki temu każda z jej metod może mieć dostęp
do prywatnych składników klasy Pt, w tym przykładzie pól X i Y :
#include
int main() {
using namespace std;
Pt pt;
class Pt;
Rysunek rys;
class Rysunek {
rys.Info(pt);
public:
}
void Info(Pt& p);
Rezultat wykonania:
};
X = 1
class Pt {
Y = 2
int X, Y;
friend class Rysunek;
public:
Pt() : X(1), Y(2) { }
};
void Rysunek::Info(Pt& p) {
cout << "X = " << p.X << endl;
cout << "Y = " << p.Y << endl;
}
Klasy zagnieżdżone
Możliwe jest umieszczenie deklaracji klasy wewnątrz deklaracji innej klasy. Deklaracja klasy
wewnętrznej pozostaje ukryta na zewnątrz klasy ją zawierającej. W związku z tym, używa się
zagnieżdżonych deklaracji klas w przypadku, gdy reprezentują one część funkcjonalności
wymaganą tylko w klasie zawierającej. Ponadto, ze względu na komplikację kodu, nie stosuje się
raczej tego typu deklaracji do bardziej rozbudowanych klas, deklarujÄ…c je oddzielnie.
#include
class List {
class Element { // prywatna deklaracja klasy wewnetrznej
int Value; // dane przechowywane w elemencie listy
Element* pNext;
public:
Element(int v, Element* next) { Value = v; pNext = next; }
Element* Next() { return pNext; }
void Info() { std::cout << "\n" << Value; }
}; // koniec kl. Element
Element* First;
public:
List() { First = 0; };
void insert(int v) { First = new Element(v, First); }
void Info() {
Element* p = First;
while (p) {
p->Info(); p=p->Next();
}
}
}; // koniec kl. List
Klasa Element jest zadeklarowana w części prywatnej klasy List, więc jej deklaracja nie jest
widoczna poza klasÄ… List.
Klasy zagnieżdżone
Gdyby klasa Element była zadeklarowana publicznie, można by jej użyć zewnętrznie, np. w
funkcji (np. main), podajÄ…c tzw. kwalifikowanÄ… nazwÄ™ klasy:
List::Element el(1, 0);
Jednak zwykle klasa zagnieżdżona jest deklarowana prywatnie, co oznacza że jest przeznaczona
tylko do użytku wewnątrz klasy ją zawierającej, i nigdzie indziej !
Przykład programu wykorzystującego klasy zagnieżdżone:
#include Nested.h
int main() {
List list;
list.insert(1);
list.insert(2);
list.insert(3);
list.Info();
return 0;
}
Rezultat wykonania:
3
2
1
Dziedziczenie
Jest to mechanizm, który umożliwia definiowanie nowych typów jako potomków już
istniejącego typu obiektowego. Funkcjonuje jeszcze inne określenie tej relacji: klasa bazowa -
klasa pochodna.
Istotą dziedziczenia jest przejmowanie pól i metod przodka przez jego potomka, z możliwością
dodania własnych pól i metod lub zmiany znaczenia metod przodka (tzw. przedefiniowanie lub
pokrycie).
Przedefiniowanie polega na napisaniu metody o takiej samej nazwie i takim samym lub innym
zbiorze parametrów formalnych, ale działającej odmiennie, a więc modyfikującej zachowanie się
obiektu. Metoda przedefiniowana w klasie potomnej może mieć dostęp do metody oryginalnej w
klasie bazowej, np. w celu wywołania jej jako pewnej fazy swego działania (by nie powtarzać raz
napisanego kodu).
Relacja generalizacja-specjalizacja: klasa potomna jest tworzona w celu konkretyzacji
funkcjonowania obiektu. Osiąga się to przez wprowadzenie nowych pól (dodatkowe parametry) i
metod (dodatkowe lub zmienione funkcje). Jest to zatem mechanizm coraz większej specjalizacji
obiektu.
Metody wirtualne stosuje się w celu wykorzystania kolejnego ważnego mechanizmu
obiektowego - polimorfizmu (wielopostaciowości). Polega on na zdefiniowaniu w obrębie ciągu
klas od bazowej do pochodnej, rodziny metod o takich samych nazwach i zestawach parametrów
formalnych, lecz o innej treści (a więc zmodyfikowanym działaniu). Każda z metod ma w
nagłówku słowo kluczowe virtual i jej adres jest przechowywany w tablicy metod wirtualnych
VMT. W zależności od tego na rzecz jakiego obiektu dana metoda jest wywołana, następuje
wybór odpowiedniego jej aspektu. Odbywa się to dynamicznie - w czasie działania programu.
Dlatego mówi się o dynamicznym wiązaniu.
Dziedziczenie
RelacjÄ™ generalizacja-specjalizacja na
TDataSegm ent
schematach struktury obiektowej
przedstawia siÄ™ specjalnym symbolem.
TMessage TTrigD ata TDataVector TDataDiscrete
TTimeData TFreqOctData TFreqNBD ata
Klasy TMessage, TTrigData, TDataVector i TDataDiscrete sÄ… klasami pochodnymi
(potomkami) klasy bazowej TDataSegment (przodka).
Klasy TTimeData, TFreqOctData i TFreqNBData sÄ… klasami pochodnymi (potomkami) klasy
TDataVector.
Klasy pochodne
Klasą pochodną jest klasa, która wywodzi się od innych klas, zwanych jej klasami bazowymi.
Definicja klasy pochodnej ma ogólną postać:
class :
{
};
jest ciągiem nazw zdefiniowanych wcześniej klas, każda z tych nazw
może być poprzedzona kwalifikatorem wirtualności (słowo kluczowe virtual) lub dostępności
(słowa kluczowe public, protected lub private).
Jeśli nazwa pewnej klasy bazowej występuje w liście dziedziczenia klasy pochodnej, to każdy
obiekt klasy pochodnej składa ze wszystkich składników tej klasy bazowej (dziedziczonych) i
dodatkowo z własnych składników pól i metod.
Jeśli posłużono się kwalifikatorem wirtualności, to znaczy że dana klasa bazowa jest
odziedziczona wirtualnie w obiekcie klasy pochodnej występuje tylko jeden podobiekt (pole)
reprezentujÄ…cy tÄ™ klasÄ™ bazowÄ….
Reguły dostępności składników klasy bazowej:
" prywatne składniki klasy bazowej są niedostępne w klasach pochodnych, niezależnie od
sposobu dziedziczenia tej klasy jako całości.
" przy dziedziczeniu prywatnym (private) wszystkie składniki klasy bazowej stają się
prywatnymi w klasie pochodnej
" przy dziedziczeniu chronionym (protected) składniki chronione klasy bazowej pozostają
chronionymi w klasie pochodnej, a składniki publiczne stają się chronionymi
" dziedziczenie publiczne klasy bazowej nie zmienia dostępności składników w klasie lub
klasach pochodnych składniki publiczne pozostają publicznymi, a chronione pozostają
chronionymi.
Przykład.
class CDerived :
virtual public CBase1,
protected CBase2,
private CBase3
{
// deklaracje pól i metod, jak w każdej klasie
};
Klasa CDerived dziedziczy z trzech klas bazowych: wirtualnie i publicznie z klasy CBase1, w
sposób chroniony z klasy CBase2 i prywatnie z klasy CBase3.
Sens takiej deklaracji jest następujący: klasa CDerived łączy w sobie funkcje zawarte w
trzech klasach z których dziedziczy, a dodatkowo dostarcza funkcjonalności zdefiniowanej
wewnątrz siebie za pomocą własnych pól i metod.
Konstruktory klas pochodnych.
Konstruktory te, poza inicjatorami własnych pól mogą zawierać inicjatory podobiektów, czyli
obiektów klas bazowych zawartych w danej klasie. Inicjator podobiektu ma postać:
Base ( lista_argumentów_inicjatora )
gdzie Base jest nazwą klasy bazowej, a lista_argumentów_inicjatora to ciąg wartości
oddzielonych przecinkami, zgodny z listą argumentów odpowiedniego konstruktora klasy Base.
Wartości te są nadawane poszczególnym argumentom konstruktora klasy Base, który w tym
miejscu będzie wywoływany.
Kolejność tworzenia obiektu złożonego:
" tworzenie i inicjalizacja obiektów odziedziczonych wirtualnie
" w kolejności określonej przez listę dziedziczenia tworzy się i inicjuje wszystkie podobiekty
niewirtualnych klas bazowych
" w kolejności określonej przez deklaracje pól klasy tworzy się i wstępnie inicjuje wszystkie
pola obiektu
" wykonuje się ciało konstruktora, gdzie ostatecznie inicjuje się (jeśli to konieczne) wybrane
pola obiektu
"
Destruktory klas pochodnych.
Niszczenie obiektu zachodzi w kolejności odwrotnej do jego tworzenia:
" najpierw wykonuje się ciało destruktora
" następnie w kolejności odwrotnej do określonej przez deklaracje pól klasy niszczy się jej
pola obiektowe
" następnie w kolejności odwrotnej do określonej przez listę dziedziczenia niszczy się
wszystkie podobiekty niewirtualnych klas bazowych
" następnie w kolejności odwrotnej do przyjętej w trakcie tworzenia obiektu niszczy się
wszystkie podobiekty wirtualnych klas bazowych
Metody wirtualne
Metody wirtualne
Rezultat wykonania programu:
HAU! HAU! HAU! MIAU! HAUuUu! HAUuUu! HAUuUu!
Poniższe przykłady ilustrują różnice między statycznym i dynamicznym wiązaniem.
WiÄ…zanie statyczne: WiÄ…zanie dynamiczne:
#include
#include
using namespace std;
using namespace std;
class A {
class A {
public:
public:
virtual void f() {
void f() {
cout << "Klasa A" << endl;
cout << "Klasa A" << endl;
}
}
};
};
class B : A {
class B : A {
void f() {
void f() {
cout << "Klasa B" << endl;
cout << "Klasa B" << endl;
}
}
};
};
void g(A& arg) {
void g(A& arg) {
arg.f();
arg.f();
}
}
int main() {
int main() {
B x;
B x;
g(x);
g(x);
}
}
Rezultat wykonania:
Rezultat wykonania:
Klasa B
Klasa A
Gdy funkcja g() jest wywoływana, wywołuje się Typ aktualnego obiektu możę być określony w
trakcie wykonywania programu.
funkcja A::f() , mimo że argument wskazuje na
Słowo virtual określa że kompilator powinien
obiekt typu B. W fazie kompilacji, kompilator wie
wybrać odpowiedni aspekt funkcji f() nie na
tylko że argument funkcji g() jest referencją do
podstawie typu referencji, ale na podstawie typu
obiektu typu pochodnego od A; nie może zaś
obiektu identyfikowanego przez tÄ™ referencjÄ™.
określić typu rzeczywistego obiektu (A lub B).
Przykład dziedziczenia
// klasa bazowa
class Punkt2D
{
protected:
int X, Y;
public:
Punkt2D(int x, int y) : X(x), Y(y) {
printf(" ctor Punkt2D\n");
}
virtual void ToJa(void) {
printf("To ja, obiekt klasy Punkt2D: X=%d, Y=%d\n", X, Y);
}
};
// klasa pochodna
class Punkt3D : public Punkt2D
{
int Z;
public:
Punkt3D(int x, int y, int z) : Punkt2D(x, y), Z(z) {
printf("ctor Punkt3D\n");
}
void ToJa(void) {
printf("To ja, obiekt klasy Punkt3D: X=%d, Y=%d, Z=%d\n",
X, Y, Z);
}
};
// program główny
main() {
Punkt2D p2(1, 1); // dekl. obiektu p2 klasy Punkt2D
Punkt3D p3(2, 2, 2); // dekl. obiektu p3 klasy Punkt3D
p2.ToJa();
p3.ToJa();
return 0;
}
Wykonanie tego programu spowoduje wyprowadzenie na ekran:
ctor Punkt2D
ctor Punkt2D
ctor Punkt3D
To ja, obiekt klasy Punkt2D: X=1, Y=1
To ja, obiekt klasy Punkt3D: X=2, Y=2, Z=2
Klasy abstrakcyjne
Służą one do zdefiniowania tzw. interfejsu, który będzie użyty we wszystkich klasach
pochodnych wywodzących się z danej klasy bazowej. Interfejs oznacza zbiór publicznie
dostępnych metod danej klasy, za pomocą których można z poziomu innych obiektów lub z ciała
funkcji globalnych wywoływać operacje zdefiniowane w klasie przez jej metody.
Klasę abstrakcyjną w języku C++ definiuje się w sposób pośredni, poprzez zadeklarowanie co
najmniej jednej funkcji składowej jako czysto wirtualnej, np.:
virtual void fcw() = 0;
Funkcja taka nie ma w danej klasie ciała, a więc nie jest znany jej kod.
Dlatego nie można:
" utworzyć obiektu o typie zgodnym z typem klasy abstrakcyjnej (nie ma ona tej częsci
funkcjonalności, jaka jest kojarzona z jej czysto wirtualnymi funkcjami).
" użyć klasy abstrakcyjnej jako typu parametru funkcji
" użyć klasy abstrakcyjnej jako typu zwracanej wartosci
" użyć klasy abstrakcyjnej jako typu docelowego w przeksztalceniu typu (rzutowaniu).
Można jednak:
" deklarować wskazniki i referencje do obiektów klasy abstrakcyjnej.
" użyć klas abstrakcyjnych jako nadklasy dla klas konkretnych. W trakcie dziedziczenia
podklasy muszą zdefiniować wszystkie funkcje czysto wirtualne; dopiero w momencie
zdefiniowania wszystkich f. czysto wirtualnych uzyskuje sie klasę konkretną, dla której można
utworzyć obiekt/y.
Przykład.
System operacyjny powinien udostępniać w ograniczony sposób informacje o swoich
sterownikach. W tym celu można zastosować klasę abstrakcyjną, która tylko definiuje interfejs
do wszystkich sterowników, tym samym wymuszając ich zgodność:
class Device {
public:
virtual int open() = 0;
virtual int close() = 0;
virtual int read(const char*) = 0;
virtual int write(const char*) = 0;
virtual int ioctl(int, ...) = 0;
//...
};
Wszystkie sterowniki urządzeń muszą dziedziczyć z klasy Device.
Zawieranie (relacja całość-część)
Określa zawieranie się jednych obiektów w innych. Na schematach struktury
obiektowej stosuje się pokazane niżej oznaczenia, przy czym napisy 0-1 i 0..n
oznaczają, że klasa Pojemnik, o ile istnieje jej obiekt, zawiera 0 lub więcej obiektów
klasy Element jako tzw. pole obiektowe (obiekt zawarty w innym obiekcie).
Całość Pojemnik
1
1
-B
1 0..1
1 0..1
Agregacja
1
1
1 0..n
1 +A 0..n
Część 1 Cz ęść2 Element
Wyszukiwarka
Podobne podstrony:
wyklad organizacja 18 03 2013
0202 04 03 2009, wykład nr 2 , Budowa i funkcje błony komórkowej oraz transport przez błony(1)
Wykład 2 (06 03 2009) ruchy kamery, plan, punkty widzenia kamery
Wykład 3 (13 03 2009) montaż
Wykład 5 (27 03 2009) narracja, gatunek filmowy
0203 11 03 2009, wykład nr 3 , Białka powierzchni komórkowej Cząsteczki adhezyjne
0106 30 03 2009, cwiczenia nr 6 , Wrzeciono podziałowe Paul Esz
Rośnie rola Syrii we wspieraniu terroryzmu (18 08 2009)
Wybory parlamentarne odbędą się 30 stycznia 2010 roku (18 05 2009)
wyklad w dniu 19 03 2010
W lutym wzrost przemocy w Iraku (02 03 2009)
wyklad farma 18 02 13
USA Zestrzeliliśmy irański samolot bezzałogowy (16 03 2009)
Ustawa z dnia 20 03 2009 o bezpieczeństwie imprez masowych
więcej podobnych podstron