Struktury
Struktury
Wskaźnik do struktury:
Jeśli
do
obiektu
strukturalnego
odwołujemy się za pomocą
wskaźnika
, to
dostęp do poszczególnych pól możemy
uzyskać przez złożenie
*
i
.
lub operator
->
( znak
-
oraz znak
>
):
Wskaźnik_Obiektu
->
Nazwa_Składnika
(
* Wskaźnik_Obiektu
).
Nazwa_Składnika
* Wskaźnik_Obiektu
.
Nazwa_Składnika
Każda definicja struktury wprowadza nowy,
unikatowy typ, np.:
struct
S1
{
int
i;
}
;
struct
S2
{
int
j;
}
;
Typy
S1
i
S2
są różnymi typami, zatem w
deklaracjach:
S1 x, y;
S2 z;
zmienne
x
oraz
y
są tego samego typu
S1
,
natomiast
x
oraz
z
są różnych typów.
Wobec tego przypisania:
x = y;
y = x;
są
poprawne
, podczas gdy:
x = z;
z = y;
są
błędne
.
Dopuszczalne są natomiast przypisania
składowych o tych samych typach, np.:
x.i = z.j;
Przeładowanie funkcji
W jezyku C
w danym zakresie ważności może być
tylko
jedna
funkcja o danej nazwie.
Kompilator języka C++ daje nam większą
swobodę.
Przykład
:
void
pisz(
float
);
void
pisz(
char
,
int
,
char
);
pisz(‘C’, 123,
‘F’);
Czy masz
wątpliwość o
wywołanie jakiej
funkcji chodzi?
Przeładowanie funkcji polega na tym, że
w danym zakresie ważności jest więcej
niż jedna funkcja o takiej samej nazwie.
To, która z nich zostaje w danym
przypadku uaktywniona zależy od typu
argumentów podanych podczas
wywołania.
Funkcje takie mają tę samą nazwę, ale
muszą się różnić liczbą lub typem
argumentów.
Przeładowujemy funkcję wówczas, gdy
wykonuje ona analogiczną akcję na różnych
zestawach obiektów.
void
pisz (
int
x);
void
pisz (
int
y);
// powtórna
deklaracja
void
pisz (
int
,
char
);
void
pisz (
float
,
char
,
int
);
Powtórna deklaracja nie jest błędem.
W przypadku deklaracji kompilator nie
zareaguje.
Zaprotestuje dopiero przy definicjach tych
funkcji
(
patrz
PROG112.CPP
)
int
rysuj (
int
);
float
rysuj (
int
);
Przy przeładowaniu ważna jest tylko odmienność
argumentów.
int
rysuj (
float
,
int
);
int
rysuj (
int,
float
);
BŁĄD !
POPRAWNIE
!
void
pisz (
float
);
void
pisz (
int
);
void
pisz (
int
,
int
);
pisz (123,
(
int
)45.67);
void
pisz (
int
,
int
);
void
pisz (
int
,
int
);
void
pisz (
int
,
unsigned
int
);
poprawnie
!
Przeładowanie przy argumentach
domniemanych
void
pisz (
float
);
void
pisz (
char
*);
void
pisz (
int
,
float
= 0);
pisz (5.67);
//
pisz (
float
);
pisz (”2000 r.”);
//
pisz (
char
*
);
pisz (123);
//
pisz (
int
,
float
=
0);
pisz (123, 5.67);
//
pisz (
int
,
float
);
void
pisz (
int
);
W rzeczywistości funkcje przeładowane mają
różne nazwy.
Kompilator zmienia nazwy wszystkich funkcji
programu.
Kompilator uzupełnia nazwę funkcji dodając typ
argumentów:
void
rys(
void
);
void
rys (
void
,
float
);
void
rys (
float
,
void
);
void
rys (
void
,
float
,
float
);
rys_Fv
rys_Fvf
rys _Ffv
rys _Fvf
Zmiana nazwy funkcji dotyczy zarówno
definicji i deklaracji funkcji, jak też i wywołań.
Informacja o typie zwracanym nie jest
doczepiana do nazwy.
Rozwinięcie tej koncepcji stanowią szablony w
niektórych jezykach nazywane typami ogólnymi
(generic)
Klasy
Klasy
Definicja klasy:
class
identyfikator_typu
{
ciało klasy
}
;
gdzie
ciało
klasy
zawiera
deklaracje
składników
klasy.
class
Okrag
{
public
:
int
x, y, r;
…
}
;
Deklaracja obiektu:
Okrag Zielony;
Okrag * Wskaz;
Okrag & Moj = Zielony;
Odwołanie się do składników
obiektu:
obiekt .
składnik
wskaźnik ->
składnik
referencja .
składnik
Zielony . r = 100;
Wskaz = & Zielony;
Wskaz -> r = 100;
Moj . r = 100;
Przykład:
Składnikami
klasy mogą być też
funkcje
.
class
Okrag
{
public
:
int
x, y, r;
void
Inicjalizuj (
int
x1,
int
y1,
int
r1);
void
Rysuj (
int
x1,
int
y1,
int
r1);
}
;
W ogólnym przypadku deklaracje funkcji w
klasie mogą być pomieszane z deklaracjami
danych.
Składnik jest znany w całej definicji klasy
,
niezależnie
od
miejsca
zdefiniowania
składnika wewnątrz klasy.
Nazwy deklarowane w klasie mają zakres
ważności równy obszarowi całej klasy.
W ciele klasy, jak w kapsule, zamknięte są
dane oraz funkcje operujące na tych danych.
Takie zamknięcie nazywamy
enkapsulacją
( od ang.
encapsulation
).
Funkcje składowe klasy mają zakres klasy.
Uwagi:
Ukrywanie informacji:
class
Moj_Typ
{
private
:
int
Liczba;
// prywatne dane składowe
float
Temperatura;
char
Komunikat [80];
int
Czy_Gotowe( );
// prywatna funkcja
składowa
public
:
float
Predkosc;
// publiczna dana składowa
int
Pomiar( );
// publiczna funkcja
składowa
}
Ukrywanie informacji:
Są
3
etykiety, za pomocą których można
określać dostęp do składników klasy:
private
:
protected
:
public
:
Ukrywanie informacji:
Składnik
private
: jest dostępny tylko dla
składowych danej klasy. Jeżeli zależy nam
na ukryciu informacji, to wówczas składnik
powinien być deklarowany jako prywatny.
Składnik
protected
: jest dostępny tak, jak
składnik
private
, ale dodatkowo jest
dostępny dla klas wywodzących się z danej
klasy.
Składnik
public
:
jest
dostępny
bez
ograniczeń. Zwykle składnikami takimi są
wybrane funkcje składowe. Za ich pomocą
dokonujemy z zewnątrz operacji na danych
prywatnych.
Etykiety można umieszczać w dowolnej
kolejności, mogą też się powtarzać.
Zakłada się, że dopóki w definicji klasy nie
wystąpi żadna z tych etykiet, składniki
przez domniemanie mają dostęp
private
.
Klasa a obiekt :
class
Osoba
{
char
Nazwisko[40];
int
Wiek;
public
:
void
Zapamietaj (
char
*,
int
);
void
Pisz ( );
}
;
Osoba
Student1
,
Student2
,
Asystent
;
W pamięci utworzone zostały 3 różne
komplety danych składowych (
Nazwisko
i
Wiek
).
Natomiast funkcje składowe są
zapamiętane w pamięci
tylko jednokrotnie
.
Funkcje składowe :
Funkcje składowe mają
pełny dostęp do
wszystkich składników swojej klasy
, to
znaczy: do danych (mogą z nich korzystać) i
do innych funkcji (mogą je wywoływać).
Do składnika swojej klasy funkcje odwołują
się przez podanie jego nazwy .
Nazwa_Obiektu .
Nazwa_Funkcji_Składowej
(
argumenty
);
Student1 .
Zapamietaj
(Kowalski, 21);
Osoba * Wsk;
Wsk = &Asystent;
Osoba &Belfer = Asystent;
Wsk ->
Zapamietaj
(Kowalski, 21);
Belfer.
Zapamietaj
(Nowak, 30);
Możemy także wywołać funkcję składową
dla danego obiektu, pokazując na niego
wskaźnikiem
lub za pomocą
referencji
np.:
Funkcje składowe :
Definiowanie funkcji składowych :
Pierwszy sposób
:
wewnątrz definicji klasy
.
class
Osoba
{
char
Nazwisko [80];
// składniki prywatne
int
Wiek;
public
:
// składniki publiczne
void
Zapamietaj (
char
* Napis,
int
Lata )
{
strcpy (Nazwisko, Napis);
Wiek = Lata;
}
void
Pisz ( )
{
cout
<<
Nazwisko
<<
, lat:
<<
Wiek
<<
endl
;
}
}
;
Drugi sposób
: w
definicji klasy
umieszcza się
tylko
same deklaracje funkcji składowych
,
natomiast
definicje są napisane poza ciałem
klasy
.
class
Osoba
{
char
Nazwisko [80];
// składniki prywatne
int
Wiek;
public
:
// składniki publiczne
void
Zapamietaj (
char
* Napis,
int
Lata );
void
Pisz ( );
}
;
void
Osoba::Zapamietaj (
char
* Napis,
int
Lata )
{
strcpy (Nazwisko, Napis);
Wiek = Lata;
}
void
Osoba::Pisz ( )
{
cout
<<
Nazwisko
<<
, lat:
<<
Wiek
<<
endl;
}
Ponieważ funkcje znajdują się teraz poza
definicją klasy, dlatego ich nazwa została
uzupełniona nazwą klasy, do której mają
należeć. Służy do tego operator zakresu
::
.
Funkcja zdefiniowana poza klasą ma dokładnie
taki sam zakres, jakby była zdefiniowana
wewnątrz klasy.
Jednakże sposób definiowania funkcji wewnątrz,
czy na zewnątrz definicji klasy stanowi różnicę
dla kompilatora.
Jeśli bowiem funkcję składową zdefiniowaliśmy
wewnątrz definicji klasy
, to kompilator uznaje,
że chcemy, aby ta funkcja była typu
inline
.
inline
int
Zaokr (
float
Liczba);
{
return
(Liczba + 0.5);
}
Funkcja typu
inline
:
Jeśli ciało funkcji składowej ma nie więcej
niż dwie linijki, to funkcję tę definiujemy
wewnątrz klasy. Jest ona automatycznie
inline
.
Jeśli funkcja składowa jest dłuższa niż
dwie linijki, to definiujemy ją poza
definicją
klasy.
Nie
jest
ona
automatycznie
inline
Funkcja składowa zdefiniowana poza definicją
klasy może być typu
inline
, ale trzeba to
zaznaczyć pisząc słowo
inline
, np.:
inline
void
Osoba :: Pisz ( )
{
//
ciało funkcji
}
Odwołanie
się
do
publicznych
danych
składowych:
class
Liczby
{
int
L1:
public
:
float
L2;
void
Fun ( );
}
;
•
Są tu dwie dane składowe.
•
Składnik
L1
jest
prywatny
(przez
domniemanie).
•
Jako prywatny może być
dostępny tylko z
zakresu klasy
- czyli wewnątrz funkcji
składowej
Fun
.
•
Składnik
publiczny
L2
oprócz tego, że może
być dostępny w funkcji składowej
Fun
,
dostępny jest
także z zewnątrz klasy
.
•
Pracując jednak na nim z zewnątrz musimy
podać, o który konkretnie obiekt chodzi, np.:
Liczby Temperatura, Cisnienie;
Temperatura . L2 = 18.6;
Ciscienie . L2 = 1003;
cout
<<
Temperatura
wynosi
:
<<
Temperatura . L2
<<
stopni C\n
;
cout
<<
Ciśnienie wynosi:
<<
Cisnienie . L2
<<
hPa\n
;
Zasłanianie nazw
Przesyłanie do funkcji argumentów będących
obiektami
(
PROG57.CPP
)
Przez domniemanie zakłada się, że
obiekt jest przesyłany do funkcji
przez
wartość
.
Konsekwencja: jeśli obiekt jest duży, to
proces kopiowania może trwać długo.
Lepszym rozwiązaniem w takim
przypadku jest przesyłanie
przez
referencję
.
void
Prezentacja (Osoba &Ktos)
{
cout
<<
Mam zaszczyt przedstawić
Państwu,\n
<<
Oto we własnej osobie:
;
cout
<<
Ktos . Pisz_Dane ();
}
Konstruktor :
Definicję obiektu i nadanie mu wartości
można załatwić w jednej instrukcji.
W tym celu należy posłużyć się specjalną
funkcją składową zwaną
konstruktorem
.
Charakteryzuje się ona tym, że nazywa
się tak samo jak klasa.
class
Numer
{
int
Liczba;
public
:
Numer (
int
L )
{
Liczba = L;
}
//
konstruktor
void
Schowaj (
int
L )
{
Liczba = L;
}
int
Zwracaj ( )
{
return
Liczba;
}
}
;
(
PROG59.CPP
)
Konstruktor
•
Klasy języka C++ wyposażone są w
specjalną funkcję zwaną
konstruktorem
;
•
Konstruktor
jest specjalną funkcją
składową, wywoływaną zawsze w chwili
tworzenia obiektu danej klasy;
•
Zadaniem konstruktora
jest inicjalizacja
danych składowych (pól) obiektu danej
klasy, przydzielenie pamięci dla jego
elementów oraz wykonanie innych
czynności niezbędnych do prawidłowego
utworzenia obiektu;
•
Konstruktor
nie jest obowiązkowym
elementem definicji klasy.
• Jeśli tworząc klasę nie zdefiniujesz jawnie
jej konstruktora, kompilator automatycznie
wygeneruje tzw.
konstruktor domyślny
;
• Rozwiązanie takie, choć dość wygodne,
sprawdza się tylko dla bardzo prostych klas;
• W praktyce każda definicja nietrywialnej
klasy będzie zawierała konstruktor;
•
Nazwa konstruktora
musi być taka sama,
jak nazwa zawierającej go klasy;
•
Konstruktor
nie może zwracać żadnej
wartości.
Konstruktor
• Klasa może posiadać
więcej niż
jeden konstruktor
;
• Jest to możliwe dzięki
mechanizmowi
przeładowania
funkcji
;
Konstruktor
•
Przypomnienie
- niezainicjalizowane
zmienne będą zawierały przypadkowe
wartości;
• Reguła ta odnosi się również do klas;
• Dobra praktyka wymaga inicjalizowania
wszystkich pól klasy;
•
Przypomnienie
- konstruktora nie można
wywołać jawnie;
• Wywołanie konstruktora następuje w
chwili tworzenia obiektu danej klasy ;
• W chwili powoływania obiektu wybierasz
również wersję konstruktora, jeżeli dana
klasa definiuje ich więcej.
• Ciekawostki: niepubliczny konstruktor ?
Konstruktor
Destruktor :
Przeciwieństwem
konstruktora
jest
destruktor
.
Destruktor
to funkcja składowa klasy.
Destruktor
nazywa się tak samo, jak
klasa z tym, że przed nazwą ma znak
~
(
tylda
).
Podobnie jak konstruktor - nie ma on
określenia typu zwracanego.
Destruktor
wywoływany jest wtedy, gdy
obiekt danej klasy ma być zlikwidowany.
•
Destruktor
jest specjalną funkcją wywoływaną w
chwili likwidacji obiektu danej klasy;
•
Destruktor
jest funkcjonalnym przeciwieństwem
konstruktora;
• Do jego zadań należy najczęściej
zwalnianie
zasobów
wykorzystywanych przez obiekt i inne
czynności natury porządkowej;
•
Destruktor
nie jest obowiązkowym elementem
klasy;
•
Destruktor
możesz zdefiniować tylko raz ;
•
Destruktor
jest funkcją bezparametrową i nie
zwracającą żadnej wartości;
•
Nazwa
składa się z
nazwy klasy
poprzedzonej
znakiem
~
.
Destruktor
(p.
PROG156.CPP,
PROG157a.CPP
)
•
Destruktor
jest wywoływany w chwili
usuwania obiektu danej klasy;
• Sama likwidacja obiektu może nastąpić
poprzez
– usunięcie go ze stosu, jeśli jest to obiekt
lokalny, a operująca na nim funkcja właśnie
zakończyła działanie;
– lub w wyniku wywołania operatora
delete
,
jeśli obiekt został utworzony dynamicznie.
• W obu przypadkach wywołanie
destruktora jest ostatnią czynnością
obiektu przed jego unicestwieniem;
Destruktor
•
Pola
klasy
to nic więcej, jak tylko jej lokalne
zmienne;
•
Pola klasy
funkcjonują tak samo, jak pola
struktury i różnią się od ostatnich wyłącznie
domyślną kategorią dostępu;
• Wewnątrz klasy wszystkie pola są swobodnie
dostępne dla wszystkich funkcji składowych;
• Natomiast ich widoczność na zewnątrz klasy
jest uwarunkowana kwalifikatorami dostępu;
• Pola w sekcjach
private
i
protected
są na
zewnątrz niedostępne;
• Pola
public
mogą być czytane i zapisywane
spoza klasy bez ograniczeń.
Pola
• Rozwiązaniem problemu dostępu do pól
prywatnych są specjalne funkcje klasy
ustawiające i pobierające wartości tych pól;
• Funkcje te, zwane
funkcjami udostępniającymi
,
deklarowane są oczywiście w
sekcji publicznej
;
• Fundamentaliści: ” wszystkie pola klas powinny
być prywatne, a dostęp do nich ma byś
realizowany wyłącznie za pomocą funkcji
udostępniających”;
• Radykałowie: ” wręcz przeciwnie”;
• Wytyczenie granicy jest kwestią doświadczenia
i zdrowego rozsądku;
• Jeśli nie wiesz, co robić z danym polem, umieść
go w sekcji prywatnej.
Pola
• Zestaw publicznych funkcji składowych powinien
być ograniczony do niezbędnego minimum
zapewniającego skuteczną komunikację z
obiektami i kontrolowanie ich działania;
• Jeśli dana funkcja składowa nie musi być widoczna
na zewnątrz, powinna być zadeklarowana jako
prywatna;
• Jeśli dana funkcja składowa nie musi być widoczna
na zewnątrz, ale powinna być dostępna dla klas
pochodnych, powinna być zadeklarowana jako
chroniona (
protected
);
• Jeśli zależy Ci na szybkim wykonaniu funkcji, a
jednocześnie jest ona niewielka, możesz
zadeklarować ją jako funkcję wstawianą (
inline
);
Funkcje
• Deklaracja każdej klasy zawiera ukryte
pole wskaźnikowe o nazwie
this
;
• Po utworzeniu obiektu, wskaźnik
this
zawiera adres obiektu w pamięci;
• Oto klasa
Punkt
widziana oczami
komputera:
Wskaźnik this
Class
Punkt
{
private
:
Punkt *
this
;
int
x, y;
public
:
Punkt (
int
_x,
int
_y);
~Punkt()
....
};
• Czemu służy wskaźnik
this
?;
• Każdy obiekt danej klasy posiada własną
kopię zestawu pól;
• Natomiast funkcje składowe są
przechowywane w jednym egzemplarzu;
• Wskaźnik
this
pozwala na
zidentyfikowanie właściciela danych, do
których odwołuje się funkcja składowa;
• Jeśli chcesz uniknąć kłopotów, nigdy nie
zmieniaj wartości wskaźnika
this
!
Wskaźnik
this
Dziedziczenie
•
Dziedziczeniem
nazywamy proces tworzenia
nowych klas na podstawie klas już
istniejących;
• Klasa wykorzystywana jako podstawa w
procesie dziedziczenia jest
klasą bazową
;
• Klasy dziedziczące po klasie bazowej są to
klasy pochodne
;
• Klasa pochodna dziedziczy wszystkie
możliwości funkcjonalne klasy bazowej,
poszerzone o nowe pola i funkcje;
• Niemożliwe jest usunięcie jakichkolwiek
elementów klasy bazowej.
Dziedziczenie
• Samo dziedziczenie symbolizowane jest
przez znajdujący się w pierwszym wierszu
deklaracji klasy
dwukropek
, po którym
występuje nazwa klasy bazowej ;
• Słowo kluczowe
virtual
deklaruje
poprzedzoną nim funkcję jako
wirtualną
;
• Jako przykład funkcji wirtualnych rozpatrz
funkcje o nazwie
Pokaz()
w klasach
Punkt
i
Linia_Pozioma
;
• Ponieważ procedura narysowania na ekranie
linii różni się od procedury narysowania
punktu, należy w klasie
Linia_Pozioma
przedefiniować (
przesłonić
) funkcję
Pokaz()
.
Przesłanianie
•
Przesłanianiem
nazywamy przedefiniowywanie
funkcji klasy bazowej w klasach pochodnych ;
• Przesłanianie stosuje się w celu całkowitej
zmiany działania funkcji klasy bazowej lub,
znacznie częściej, jej uzupełnienia i rozszerzenia
o dodatkowe operacje;
• Aby rozszerzyć pierwotną definicję
nie musisz jej
przepisywać
. W nowej definicji funkcji wystarczy
najpierw wywołać funkcję klasy bazowej, a
następnie dopisać kod realizujący rozszerzenia;
• Przesłaniając funkcje klasy bazowej musisz
zapewnić
identyczność nagłówków funkcji
;
• Istotne jest również aby funkcja
była dostępna
dla klas pochodnych
.
Przesłanianie
• Odwołując się do funkcji klasy bazowej musisz
poprzedzić ją nazwą klasy i operatorem
zakresu;
• Użycie operatora zakresu jest konieczne tylko
wtedy, gdy wywoływana funkcja jest
zdefiniowana zarówno w klasie bazowej, jak i
pochodnej ;
• Jeśli funkcja jest zdefiniowana tylko w sekcji
publicznej lub chronionej klasy bazowej, a nie
wchodzi w skład definicji klasy pochodnej,
możesz ją wywołać bez użycia operatora
zakresu.
Funkcje wirtualne
•
Funkcją wirtualną
nazywamy funkcję
wywoływaną zawsze w obrębie posiadającej ją
klasy;
•
Poprzedzenie deklaracji słowem kluczowym
virtual
powoduje, że wszystkie odwołania do
funkcji będą zawsze wykonywane w obrębie
klasy, która ją zdefiniowała;
•
Jeśli podejrzewasz, że funkcja będzie
przedefiniowywana w klasach pochodnych,
warto deklarować ją jako wirtualną;
•
Jeśli przedefiniowywana funkcja jest
wywoływana przez inne funkcje klasy
bazowej, prawie na pewno powinieneś
zadeklarować ją jako wirtualną.
Inicjalizacja obiektów
• Kolejnym problemem związanym z
dziedziczeniem jest
inicjalizacja obiektów
;
• W chwili utworzenia obiektu klasy pochodnej
należy zainicjalizować też pola klasy bazowej,
a w przypadku dziedziczenia „piętrowego” -
również wszystkich klas pośrednich;
• Najskuteczniejszym na to sposobem jest po
prostu wywołanie konstruktora klasy bazowej
w konstruktorze klasy pochodnej;
• Ponieważ nie można wywołać tego
konstruktora bezpośrednio, należy
wykorzystać w tym celu listę inicjalizującą
;
• Prawidłowa inicjalizacja klasy bazowej w
konstruktorze klasy pochodnej jest sprawą
bardzo istotną.