• Nie Znaleziono Wyników

Wstęp do programowania

N/A
N/A
Protected

Academic year: 2021

Share "Wstęp do programowania"

Copied!
31
0
0

Pełen tekst

(1)

Wstęp do programowania obiektowego, wykład 7

Klasy i funkcje abstrakcyjne Przeciążanie funkcji

Definiowanie i interpretacja złożonych typów danych w C++

Wskaźniki do funkcji

(2)

KLASA ABSTRAKCYJNA

(3)

Klasa abstrakcyjna

Klasa abstrakcyjna – klasa w której co najmniej jedna metoda nie jest zdefiniowana, a tylko zadeklarowana.

Takie metody powinny być wirtualne - w dziedziczących klasach muszą być dostarczone

konkretne implementacje tych metod, aby można było tworzyć ich obiekty.

Obiektów samej klasy abstrakcyjnej tworzyć nie można, bo klasa nie jest całkowicie zdefiniowana.

Klasa abstrakcyjna służy zwykle jako definicja

interfejsu, czyli zbioru metod jakie występują w klasach dziedziczących.

(4)

Metoda czysto wirtualna (abstrakcyjna)

Metoda, która nie jest zdefiniowana, a tylko zadeklarowana to tzw. metoda czysto wirtualna (inaczej metoda

abstrakcyjna).

W C++ deklaruje się taką metodę, pisząc po nawiasie kończącym listę argumentów „=0”, np. :

virtual double liczPole() = 0;

Wystarczy jedna deklaracja metody abstrakcyjnej, aby cała klasa była abstrakcyjna (nawet jeżeli posiada zdefiniowane inne metody).

W C++ można metodę abstrakcyjną zdefiniować (zdefinować jej ciało), ale klasa tak czy inaczej będzie abstrakcyjna. Do takiej definicji odwołujemy się przez specyfikator zakresu „::”. Jest to niepolecane.

(5)

Klasa konkretna / polimorfizm

Przeciwieństwem klasy abstrakcyjnej jest klasa konkretna – taka, której wszystkie metody

zostały zaimplementowane i można tworzyć jej obiekty.

Do obiektów klas pochodnych można się

odnosić poprzez wskaźniki i referencje o typie statycznym klasy bazowej (w szczególności

również abstrakcyjnej) – jest to jeden z

warunków polimorfizmu.

(6)

Przykład: figury (UML)

Na diagramach UML klasy i metody abstrakcyjne oznaczamy kursywą.

Figura

#int kolor +Figura(int kk) +double liczPole() +double liczObwod()

Kwadrat +double bok

+Kwadrat(int kk, double bb) +double liczPole()

+double liczObwod()

Kolo +double promien

+Kolo(int kk, double pp) +double liczPole() +double liczObwod()

Trojkat +double a

+double b +double c

+Trojkat(int kk, double aa, double bb, double cc) +double liczPole()

+double liczObwod()

(7)

Przykład: figury (kod 1/2)

class Figura { protected:

int kolor;

public:

Figura(int kk):kolor(kk){}

virtual double liczPole() = 0;

virtual double liczObwod() = 0;

};

class Kwadrat : public Figura { double bok;

public:

Kwadrat(int kk, double bb) : Figura(kk), bok(bb) { } double liczPole() { return bok*bok; }

double liczObwod() { return 4*bok; } };

class Koło :public Figura {

(8)

Przykład: figury (kod 2/2)

int main(void) {

Figura* tab[] = { new Kolo(1.5), new Kwadrat(1.9),

new Kolo(2.25), new Trojkat(3, 2.4, 5.2) };

for (int i = 0; i <= 4; i++) { if (tab[i] != NULL){

cout << "Pole figury numer "<< i << "to" << tab[i]->

liczPole() << endl;

cout << „Obwód figury numer "<< i << "to" << tab[i]->

liczPole() << endl;

} }

(9)

Klasa Figura jest tu abstrakcyjna, bo zawiera metody czysto wirtualne; trudno byłoby rozsądnie

zaimplementować metody takie jak liczPole czy liczObwód dla nieznanych figur geometrycznych.

Dopiero dla konkretnych figur, jak kwadrat czy koło, można to policzyć.

Dopiero w klasach dziedziczących z klasy Figura,

definiujemy implementacje metod wirtualnych. Każda klasa konkretna musi mieć zaimplementowane wszystkie odziedziczone metody (uprzednio) abstrakcyjne.

Różne figury przechowujemy za pomocą wskaźników o typie statycznym Figura, a różnych typach

dynamicznych. Wywołania metod są polimorficzne (wirtualne).

(10)

PRZECIĄŻANIE

FUNKCJI

(11)

W C++ możliwe jest zadeklarowanie dwóch lub więcej funkcji o tej samej nazwie. Nazywa się to

przeciążeniem.

Zdefiniowanie metod o tej samej nazwie i sygnaturze w dwóch klasach powiązanych dziedziczeniem to jest tzw.

przesłonięcie, a nie przeciążenie.

Kompilator określa, do której funkcji odnosi się każde wywołanie na podstawie typów parametrów formalnych i aktualnych z ewentualnymi konwersjami

(12)

Kryteria rozstrzygania najlepiej pasującej funkcji przeciążonej

Typ wartości zwracanej przez funkcję nie jest uwzględniany.

Poszukiwanie najlepiej pasującej funkcji odbywa się według kryteriów przedstawionych poniżej w podanej kolejności:

Dokładne dopasowanie (brak konwersji lub konwersje trywialne, np. tablicy do wskaźnika)

Dopasowanie z użyciem promocji (bool do int, char do int, float do double etc.)

Dopasowanie z użyciem konwersji standardowych (int do double, double do int etc.)

Dopasowanie z użyciem konwersji zdefiniowanych przez użytkownika

Dopasowanie z użyciem wielokropka (...) - zmienna ilość argumentów funkcji

(13)

DEFINIOWANIE I INTERPRETACJA

ZŁOŻONYCH TYPÓW

DANYCH W C++

(14)

Przez typy „złożone” rozumiemy takie, które są złożeniami typów

różnego rodzaju: na przykład tablica wskaźników, odnośnik do tablicy albo wskaźnik do wskaźnika do tablicy wskaźników itp.

Definiowanie typów pochodnych może czasem prowadzić do

skomplikowanych wyrażeń. Czym na przykład są x, y, z, f po następujących deklaracjach:

int tab[] = {1,2,3};

int (&x)[3] = tab;

int *y[3] = {tab,tab,tab};

int *(&z)[3] = y;

int &(*f)(int*,int&);

Linia 1 określa oczywiście, że tab jest typu 'trzyelementowa tablica

zmiennych typu int', co w wyrażeniach w sposób naturalny konwertowane jest do typu int*. Pozostałe deklaracje mogą sprawiać kłopoty.

(15)

Ogólne zasady interpretacji złożonych typów

1. zaczynamy od nazwy deklarowanej zmiennej,

2. patrzymy w prawo: jeśli jest tam nawias otwierający okrągły '(', to będzie to funkcja (odczytujemy liczbę i typ parametrów); jeśli będzie tam nawias otwierający kwadratowy '[', to będzie to tablica (odczytujemy rozmiar),

3. jeśli po prawej stronie nic nie ma lub jest nawias okrągły zamykający ')', to przechodzimy na lewo i czytamy następne elementy kolejno od prawej do lewej aż do końca lub do napotkania nawiasu otwierającego,

4. jeśli napotkaliśmy nawias okrągły otwierający, to wychodzimy z całego tego nawiasu i kontynuujemy znów od jego prawej strony,

5. gwiazdkę (*) czytamy „jest wskaźnikiem do”,

6. ampersand (&) czytamy „jest odniesieniem do”,

7. po odczytaniu liczby i typu parametrów funkcji dalszy ciąg określa typ zwracany tej funkcji,

8. po odczytaniu wymiaru tablicy dalszy ciąg określa typ elementów tablicy.

(16)

Przykład 1

int (&x)[3] = tab;

x jest:

na prawo nawias zamykający, więc patrzymy na lewo:

ODNOŚNIKIEM DO,

patrzymy dalej w lewo, napotykamy nawias otwierający;

wychodzimy zatem z nawiasu, patrzymy na prawo: jest nawias otwierający kwadratowy, więc: TABLICY

TRZYELEMENTOWEJ,

przechodzimy na lewo: ZMIENNYCH TYPU int.

Ponieważ x jest odnośnikiem (referencją), musieliśmy od razu dokonać inicjalizacji - widać, że jest ona prawidłowa, bo tab właśnie jest trzyelementową tablicą int-ów.

(17)

Przykład 2

int *y[3] = {tab,tab,tab};

y jest:

na prawo nawias kwadratowy otwierający, więc:

TRZYELEMENTOWĄ TABLICĄ,

patrzymy w lewo i czytamy do końca w lewo, bo nie ma już żadnych nawiasów: WSKAŹNIKÓW DO

ZMIENNYCH TYPU int.

Tu nie musieliśmy od razu dokonywać inicjalizacji, ale ta której dokonaliśmy jest prawidłowa, bo tab

standardowo jest konwertowana do typu int*.

Wszystkie elementy tablicy y wskazują na ten sam adres, a mianowicie na pierwszy element tablicy tab.

(18)

Przykład 3

int *(&z)[3] = y;

z jest:

1. na prawo nawias okrągły zamykający, więc patrzymy na lewo: ODNOŚNIKIEM DO,

2. na lewo nawias okrągły otwierający, więc

wychodzimy z całego nawiasu i przechodzimy na prawo: TRZYELEMENTOWEJ TABLICY,

3. patrzymy w lewo i czytamy do końca w lewo, bo nie ma już żadnych nawiasów: WSKAŹNIKÓW DO

ZMIENNYCH TYPU int.

Tu znów musieliśmy od razu dokonać inicjalizacji - do jej wykonania użyliśmy tablicy y z poprzedniego przykładu.

(19)

WSKAŹNIKI DO

FUNKCJI

(20)

Funkcje a pamięć

Wartość zmiennej zapisana jest gdzieś w pamięci komputera, a zatem ma określony adres, który można przechowywać we

wskaźnikach.

Podobnie funkcja, w postaci binarnego kodu, jest zapisana gdzieś w pamięci komputera.

Posiada więc też adres.

Stąd wynika pojęcie wskaźnika do funkcji

(inaczej wskaźnika funkcyjnego) – jest to

wskaźnik wskazujący na adres kodu funkcji.

(21)

Wskaźniki do funkcji a arytmetyka wskaźników

Wskaźniki do zmiennych są typowane: określając typ zmiennej wskazywanej dla zwykłego wskaźnika określamy jednocześnie rozmiar (i format zapisu) pojedynczego

wskazywanego przez ten wskaźnik obiektu.

Takie obiekty możemy układać w pamięci kolejno jeden po drugim tworząc tablice.

Jeśli znamy adres pierwszego z nich, to znając rozmiar

pojedynczego obiektu znamy położenie drugiego, trzeciego i, ogólnie, n-tego.

W przypadku funkcji jest inaczej. Każda funkcja może być inna i jej binarny kod może mieć inny rozmiar. Stąd:

wskaźniki do funkcji nie obsługują arytmetyki wskaźników.

(22)

Typ wskaźnika

Informacja o typie wskaźnika do zmiennej jest potrzebna:

aby prawidłowo interpretować binarną reprezentację danej (4B int-a to co innego niż 4B float-a),

aby kontrolować poprawność operacji na niej (tzw.

sprawdzanie zgodności typów – wykonywane przez kompilator).

W przypadku funkcji kompilator sprawdza czy ich wywołanie jest zgodne ze specyfikacją wyrażoną w definicji (po uwzględnieniu konwersji).

W przypadku wskaźników funkcyjnych trzeba określać nie tylko adres funkcji, ale też typy i liczbę parametrów oraz typ zwracanej wartości.

(23)

Przykład wskaźnika do funkcji

int (*pfun)(int);

Czytamy zgodnie z zasadami o złożonych typach danych:

ZMIENNA pfun JEST - na prawo nawias zamykający, więc patrzymy w lewo - WSKAŹNIKIEM DO -

wychodzimy z nawiasu, patrzymy w prawo - FUNKCJI Z JEDNYM PARAMETREM TYPU int

ZWRACAJĄCEJ - patrzymy znów w lewo - WARTOŚĆ TYPU int.

Wniosek: zmienna pfun jest zmienną wskaźnikową przystosowaną do przechowywania adresów funkcji, które pobierają jeden argument typu int i zwracają wartość typu int.

(24)

Nawiasy wokół wskaźników do funkcji

Nie można było opuścić nawiasów wokół

*pfun. Po opuszczeniu otrzymalibyśmy:

int *pfun(int);

co przeczytalibyśmy tak: ZMIENNA fun JEST - na prawo nawias otwierający - FUNKCJĄ Z JEDNYM PARAMETREM TYPU int

ZWRACAJĄCĄ - patrzymy w lewo -

WSKAŹNIK DO - dalej w lewo - ZMIENNEJ

TYPU int. A zatem byłaby to deklaracja funkcji

pfun, a nie wskaźnika do funkcji.

(25)

Wartość początkowa wskaźnika

Po definicji

int *k;

zmienna wskaźnikowa k istnieje, ale na razie nie zawiera żadnego użytecznego adresu.

Podobnie: wskaźnik do funkcji fun już

istnieje, ale nie zawiera adresu żadnej

konkretnej funkcji.

(26)

Ustalanie wartości wskaźnika do funkcji

Podobnie jak do wskaźnika p typu int* wpisujemy adres istniejącej zmiennej k typu int pisząc

p = &k;

tak na zmienną pfun można przypisać

pfun = &fun;

gdzie fun jest nazwą pewnej funkcji o nazwie fun odpowiedniego typu.

Konwersja od fun do &fun jest konwersją trywialną, tak więc równie dobrze w przypisaniu możemy pominąć operator wyłuskania adresu i napisać po prostu:

pfun = fun;

Należy pamiętać, że nie piszemy nawiasów po nazwie funkcji - nawiasy pełnią rolę operatora wywołania funkcji, tak więc:

pfun = fun();

spowodowałoby wywołanie funkcji fun i próbę przypisania rezultatu do zmiennej pfun.

(27)

Przykładowy program

#include <iostream>

#include <cmath> // funkcje matematyczne using namespace std;

const double PI = 4*atan(1.);

double nasza(double);

int main(void) {

double (*f)(double); //deklaracja wskaźnika do funkcji

f = sin; //ustawienie wskaźnika na funkcję sin

cout << "sin(PI/2) = " << (*f)(PI/2) << endl; //wywołanie f = &cos; //ustawienie wskaźnika na funkcję cos cout << " cos(PI) = " << f(PI) << endl; //wywołanie

f = nasza; //ustawienie wskaźnika na funkcję nasza

cout << " nasza(3) = " << f(3) << endl; //wywołanie }

(28)

Wskaźnik funkcyjny może być parametrem funkcji

Deklaracja takiego parametru wygląda tak samo jak deklaracja wskaźnika do funkcji:

double fun( double (*f)(double), double a, double b) {

return f(a) + f(b);

}

Jest to definicja funkcji fun, której pierwszym parametrem jest funkcja pobierająca i zwracająca double, a dwoma następnymi są zwykłe parametry typu double.

W treści funkcji f jest wywoływana kolejno dla argumentu a i b.

Wywyłanie w programie na przykład tak (funkcja atan oznacza biblioteczną funkcję arcus tangens):

#include <cmath>

const double PI = 4*atan(1.);

// ...

double result = fun(sin, 0, PI/2);

(29)

Definiowanie własnych typów

Można uprościć skomplikowany zapis wprowadzając synonim (alias) skomplikowanego typu za pomocą instrukcji typedef.

W naszym przypadku moglibyśmy zdefiniować synonim następującego typu:

wskaźnik do funkcji z jednym parametrem typu double zwracającej double:

typedef double (*FUNDtoD)(double);

i użyć go potem w deklaracji naszej funkcji fun:

double fun( FUNDtoD, double, double);

lub w deklaracjach/definicjach funkcyjnych zmiennych wskaźnikowych

#include <cmath>

// ...

FUNDtoD f = sin;

// ...

f = atan;

(30)

Interpretacja złożonych typów:

przykład 4

double (*fun[3])(double);

Czytamy: ZMIENNA fun JEST:

1. na prawo nawias kwadratowy otwierający - TRZYELEMENTOWĄ TABLICĄ

2. przechodzimy na lewo - WSKAŹNIKÓW DO

3. wychodzimy z nawiasu, patrzymy w prawo -

FUNKCJI Z JEDNYM PARAMETREM TYPU double ZWRACAJĄCEJ

4. patrzymy znów w lewo - WARTOŚĆ TYPU double.

Wniosek: zmienna fun jest tablicą trzech wskaźników do funkcji.

(31)

Interpretacja złożonych typów:

przykład 5

int &(*f)(int*,int&);

f jest:

1. na prawo nawias okrągły zamykający, więc patrzymy na lewo: WSKAŹNIKIEM DO,

2. na lewo nawias okrągły otwierający, więc

wychodzimy z całego nawiasu i przechodzimy na prawo: FUNKCJI O DWÓCH PARAMETRACH, PIERWSZYM TYPU int*, DRUGIM

ODNOŚNIKOWYM TYPU int&,

3. patrzymy w lewo i czytamy do końca w lewo, bo nie ma już żadnych nawiasów: ZWRACAJĄCEJ

ODNIESIENIE DO int.

Cytaty

Powiązane dokumenty

Zapis rozpoczyna się w sposób typowy dla bloku przedsionkowo­komorowego II stopnia typu I, po którym zamiast skrócenia widać wydłużenie odstępu PQ, czyli od razu

decydentów biorących udział w tego rodzaju przedsięwzięciach, można wskazać na zestaw uwarunkowań determinujących obecnie możliwości rozwoju projektów w formule

Sprawdza się też teza, że uczniowie chcący dostać się do technikum i szkoły zawodowej kierują się możliwością otrzymania poszukiwanego i dobrze płatnego zawodu.. Z

Pewnego razu Chałuzjew zawiadomił nas przez Raskowa- łowa, który prawie nie interesował się polityką, źe sytuacja Kolczaka znacznie się pogorszyła, i że z dnia

Profesor Krzysztof Simon, kierownik Kliniki Chorób Zakaźnych i Hepatologii Uniwersytetu Medycznego we Wrocławiu, przyznaje, że młodzi ludzie w stolicy województwa

Pacjent nie będzie miał satysfakcji z rejestrowania zdarzeń, nie będzie zadowolony z jakości usług, które nie uwzględniają jego kryteriów i oczekiwań. Oczywi- ście pojawia

Szczególnie dramatycznie wygląda kondycja Centrum Zdrowia Dziecka z dwustumi- lionowym długiem równym rocznemu kontraktowi placówki, ale w jej tle pojawiają się informacje na

(2006), Przestrzeń kulturowa w nauczaniu języka polskiego jako obcego, Wydawnictwo Uniwersytetu Warszawskiego, Warszawa, s.. Wróć do treści