Struktury
description
Transcript of Struktury
StrukturyStruktury
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 _Fvff
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)
KlasyKlasy
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ładowefloat Temperatura;char Komunikat [80];
int Czy_Gotowe( ); // prywatna funkcja składowa
public:float Predkosc; // publiczna dana składowaint 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 prywatneint 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; } // konstruktorvoid 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ą.