Pobierz prezentację
Pobieranie prezentacji. Proszę czekać
OpublikowałRadosław Łuczak Został zmieniony 9 lat temu
1
Język C/C++ Funkcje
2
Funkcje - wstęp Funkcję można uważać za operację zdefiniowaną przez programistę i reprezentowaną przez nazwę funkcji. Operandami funkcji są jej argumenty, ujęte w nawiasy okrągłe i oddzielone przecinkami. Typ funkcji określa typ wartości zwracanej przez funkcję.
3
Kiedy używamy funkcji Często wykonujemy jakieś zadanie, np. obliczanie wartości sinus, zadanie jest bardzo trudne.
4
Metoda zstępująca w programowaniu Całe zadanie dzielimy na podzadania, podzadania są instrukcjami programu, są to instrukcje abstrakcyjne (nie występują w języku programowania), muszą mieć sformalizowaną postać (funkcje), w funkcjach należy określić jaki algorytm realizują i jak się komunikują z otoczeniem.
5
Deklaracja funkcji Deklaracja funkcji ma postać: –typ nazwa(deklaracje argumentów); Występujące tutaj trzy elementy: typ zwracany, nazwa funkcji i wykaz argumentów nazywane są łącznie prototypem funkcji. W języku C++ obowiązuje zasada deklarowania prototypu każdej funkcji przed jej użyciem. Prototyp funkcji o tym samym identyfikatorze może wystąpić w tym samym programie wiele razy (przeciążanie funkcji), natomiast brak prototypu funkcji wywoływanej w programie jest błędem syntaktycznym. Argumenty w deklaracji funkcji nazywa się również argumentami formalnymi lub parametrami formalnymi. Wykonanie instrukcji deklaracji funkcji nie alokuje żadnego obszaru pamięci dla parametrów formalnych.
6
Przykłady prototypów funkcji int f1(); void f3(); void f3(void); int* f5(int); int (*fp6) (const char*, const char*); extern double sqrt(double); extern char *strcpy(char *to, const char *from); extern int strlen(const char *st); extern int strcmp(const char *s1, const char *s2); int printf(const char *format,...);
7
Wywołanie funkcji Wywołanie funkcji jest poleceniem obliczenia wartości wyrażenia, zwracanej przez nazwę funkcji. Instrukcja wywołania ma składnię: –nazwa (argumenty aktualne); gdzie nazwa jest zadeklarowaną wcześniej nazwą funkcji, argumenty aktualne są wartościami argumentów formalnych, zaś para nawiasów okrągłych () jest operatorem wywołania. Liczba, kolejność i typy argumentów aktualnych powinny dokładnie odpowiadać zadeklarowanym argumentom formalnym. Przy niezgodności typów argumentów kompilator stara się wykonać konwersje niejawne; jeżeli nie może dokonać sensownej konwersji, sygnalizuje błąd.
8
Co się dzieje przy wywoływaniu funkcji ? Zastosowanie operatora wywołania do nazwy funkcji powoduje alokację pamięci dla argumentów formalnych i przekazanie im wartości argumentów aktualnych. Od tej chwili argumenty formalne stają się zmiennymi lokalnymi o wartościach inicjalnych równych przesłanym do nich wartościom argumentów aktualnych. Zasadniczym sposobem przekazywania argumentów do funkcji jest przekazywanie przez wartość: do każdego argumentu formalnego jest przesyłana kopia argumentu aktualnego. Ponieważ argumenty formalne po wywołaniu stają się zmiennymi lokalnymi funkcji, zatem wszelkie wykonywane na nich operacje nie zmieniają wartości argumentów aktualnych. Wyjątkiem od tej zasady jest przesłanie do funkcji adresów argumentów aktualnych za pomocą wskaźników lub referencji - później.
9
Definicja funkcji Składnia definicji funkcji jest następująca: typ nazwa (deklaracje argumentów) { instrukcje } Definicja podaje typ zwracany przez funkcję, jej nazwę oraz argumenty formalne. Argumentami formalnymi funkcji mogą być zmienne wszystkich typów podstawowych, struktury, unie oraz wskaźniki i referencje do tych typów, a także zmienne typów definiowanych przez użytkownika. Nie mogą być nimi tablice, ale mogą być wskaźniki do tablic (jeśli są tablice, to są traktowane jako wskaźniki). Typ funkcji nie może być typem tablicowym ani funkcyjnym, ale może być wskaźnikiem do jednego z tych typów. Zarówno typ zwracany, jak i typy argumentów muszą być podawane w postaci jednoznacznych identyfikatorów. Identyfikatory mogą być nazwami typów wbudowanych (np. char) lub zdefiniowanych wcześniej przez użytkownika. Inaczej mówiąc, w nagłówku funkcji nie mogą występować definicje typów.
10
Ciało funkcji Instrukcje w bloku (nazywanym również ciałem funkcji) mogą być instrukcjami deklaracji. Ostatnią instrukcją przed nawiasem klamrowym zamykającym blok funkcji musi być instrukcja return; jedynie dla funkcji typu void instrukcja return; jest opcją. Zatem definicja –int f() { }; jest błędna, natomiast definicja –void f() { }; jest poprawna.
11
Instrukcja return Instrukcja return; występuje często w postaci: return wyrażenie; gdzie wyrażenie określa wartość zwracaną przez funkcję. Jeżeli typ tego wyrażenia nie jest identyczny z typem funkcji, to kompilator będzie próbował osiągnąć zgodność typów drogą niejawnych konwersji. Jeżeli okaże się to niemożliwe, to kompilacja zostanie zaniechana. Zgodność typu zwracanego z zadeklarowanym typem funkcji można również wymusić drogą konwersji jawnej. W bloku funkcji może wystąpić więcej niż jedna instrukcja (instrukcje warunkowe) return;.
12
Uwagi Deklaracje argumentów muszą podawać oddzielnie typ każdego argumentu; nazwy argumentów są opcjonalne. Definicja, która nie zawiera nazw argumentów, a jedynie ich typy, jest syntaktycznie poprawna.
13
Przykład #include //deklaracja funkcji - prototyp int dods(int, int); intmain() { int i, j, k; cout <<”Wprowadz dwie liczby typu int: ”; cin >> i >> j; cout << '\n'; k = dods (i,j); //wywolanie funkcji cout << ”i=” << i << ”\tj=” << j << '\n'; cout << ”dods(i,j)= ”<< k << '\n'; return 0; } int dods (int n, int m) { if (n + m > 10) return n + m; else return n; }
14
Wywołanie funkcji Wywołanie funkcji zawiesza wykonanie funkcji wołającej i powoduje zapamiętanie adresu następnej instrukcji do wykonania po powrocie z funkcji wołanej. Adres ten, nazywany adresem powrotnym, zostaje umieszczony w pamięci na stosie programu (ang. run-time stack). Wywołana funkcja otrzymuje wydzielony obszar pamięci na stosie programu, nazywany rekordem aktywacji lub stosem funkcji. W rekordzie aktywacji zostają umieszczone argumenty formalne, inicjowane jawnie w deklaracji funkcji, lub niejawnie przez wartości argumentów aktualnych.
15
Parametry domyślne Jawne inicjowanie argumentów w deklaracji (nie w definicji) funkcji można traktować jako przykład przeciążenia funkcji; tematem tym zajmiemy się później bardziej szczegółowo. Weźmy następujący przykład: w prototypie funkcji, która symuluje ekran monitora, wprowadźmy inicjalne wartości domyślne dla szerokości, wysokości i tła ekranu char* ekran(int x=80, int y=24, char bg = ' '); Wprowadzone wartości początkowe argumentów x, y oraz bg są domyślne w tym sensie, że jeżeli w wywołaniu funkcji nie podamy argumentu aktualnego, to na stosie funkcji zostanie “położona” wartość domyślna argumentu formalnego. Jeżeli teraz zadeklarujemy zmienną char* kursor, to wywołanie kursor = ekran(); jest równoważne wywołaniu kursor = ekran(80, 24, ' ');
16
Parametry domyślne Jeżeli w wywołaniu podamy inną od domyślnej wartość argumentu, to zastąpi ona wartość domyślną, np. wywołanie –kursor = ekran(132); jest równoważne wywołaniu –kursor = ekran(132, 24, ' '); zaś wywołanie –kursor = ekran(132, 66); jest równoważne wywołaniu –kursor = ekran(132, 66, ' '); Składnia ostatniego wywołania pokazuje że nie można podać wartości pierwszego z prawej argumentu nie podając wszystkich wartości po lewej.
17
Parametry domyślne Deklaracja funkcji nie musi zawierać wartości domyślnych dla wszystkich argumentów, ale podawane wartości muszą się zaczynać od skrajnego prawego argumentu, np. –char* ekran( int x, int y, char bg = ' ' ); –char* ekran( int x, int y = 24, bg = ' ' ); Wobec tego deklaracje –char* ekran (int x = 80, int y, char bg = ' '); –char* ekran ( int x, int y = 24, char bg ); są błędne.
18
Przekazywanie argumentów przez wartość W języku C++ przekazywanie argumentów przez wartość jest domyślnym mechanizmem językowym. Przy braku takiego mechanizmu każda zmiana wartości argumentu formalnego nie poprzedzonego modyfikatorem const wywołałaby taką samą zmianę argumentu aktualnego. Założenie to zostało podyktowane tym, że ewentualne zmiany wartości argumentów aktualnych, będące wynikiem wykonania jakiejś funkcji, są na ogół traktowane jako niepożądane efekty uboczne.
19
Przekazywanie argumentów przez wartość #include Using namespace std; int imax(int x, int y); intmain() { float zf = 35.7; double zd = 11.0; int ii; ii = imax( zf, zd ); cout << ”ii =”<< ii << endl; return 0; } int imax(int x, int y) { if (x > y) return x; else return y; }
20
Przekazywanie argumentów przez wartość Przekazywanie argumentów przez wartość nie będzie dogodnym mechanizmem w dwóch przypadkach: –gdy wartości argumentu aktualnego muszą być przez funkcję zmodyfikowane, –gdy przekazywany argument reprezentuje duży obszar pamięci (np. tablica, struktura). Dla tablic jest stosowany mechanizm, który nakazuje kompilatorowi przekazanie argumentów aktualnych w postaci adresu pierwszego elementu tablicy zamiast kopii całej tablicy. Natomiast w pierwszym przypadku należy znaleźć własne rozwiązanie.
21
Przekazywanie argumentów przez wartość Klasycznym przykładem nieprzydatności przekazywania argumentów przez wartość jest operacja zamiany wartości dwóch zmiennych. Funkcja void zamiana(int x, int y) { int pomoc = y; y = x; x = pomoc; }
22
Przekazywanie argumentów przez wskaźnik Pierwszy z nich polega na zastosowaniu wskaźników. Sposób ten ilustruje poniższy przykład. #include using namespace std; void zamiana1 (int*, int* ); intmain() { int i = 10; int j = 20; zamiana1( &i, &j ); cout <<"Po zamianie i=" << i << "\tj="<< j << endl; return 0; } void zamiana1(int* x, int* y) { int pomoc = *y; *y = *x; *x = pomoc; }
23
Przekazywanie argumentów przez adres Poprzedni program wydaje się być mało czytelny. Alternatywne rozwiązanie tego samego zadania wykorzystuje referencje zamiast wskaźników, dając prostszą i bardziej czytelną notację. #include Using namespace std; void zamiana2 (int& i, int& j ); intmain() { int i = 10; int j = 20; zamiana2( i, j ); cout <<"Po zamianie i=" << i << "\tj="<< j << endl; return 0; } void zamiana2(int& x, int& y) { int pomoc = y; y = x; x = pomoc; }
24
Wskaźniki do funkcji Tak jak nazwa tablicy, nazwa funkcji jest stałym wskaźnikiem. Ponieważ nazwa funkcji jest stałym wskaźnikiem, to wskaźnik do funkcji jest wskaźnikiem do stałego wskaźnika, który jest nazwą funkcji. int f(int); // deklaracja funkcji f int (*pf) (int); // deklaracja wskaźnika do funkcji typu int funkcja(int) pf = &f; // adres f przypisany jest do pf
25
Przykład #include using namespace std; int suma(int (*) (int), int); // dwa parametry: wskaźnik do funkcji int square(int); int cube(int); main() { cout << suma(square,5) << endl; cout << suma(cube,5) << endl; } int suma(int (*pf)(int k), int n) // k nie jest używane ale wymagane { int s = 0; for (int i =0; i <= n; i++) s += (*pf)(i); return s; }
26
Przykład int square(int k) { return k*k; } int cube(int k) {return k*k*k; }
27
Zapowiedź funkcji, zmienne globalne // zmienne globalne #include using namespace std; int a[10], n; float war; void wprowadz(); void wariancja(); main() { wprowadz(); wariancja(); cout <<"wariancja= "<<war; getchar(); } void wprowadz() { cout >n; for (int i=1; i<=n; i++) { cout >a[i]; } //************************************************ float srednia() { float s=0; for (int i=1; i<=n; i++) s+=a[i]; return s/=n; } void wariancja() { float sr=srednia(); war=0; for (int i=1; i<=n; i++) war=war+(a[i]-sr)*(a[i]-sr); war=sqrt(war/n); }
28
Przeciążanie funkcji Funkcje można w C++ (ale nie w C) przeciążać (przeładowywać). Oznacza to sytuację, gdy w tym samym zakresie są widoczne różne definicje funkcji o tej samej nazwie. wywołania funkcji muszą się różnić, tak by można było jednoznacznie wybrać jedną z nich na podstawie wywołania.
29
Przeciążanie funkcji Sygnatury nie różnią się: –int f(int i); –float f(int j); –int f(int k;int l=0); Sygnatury różnią się: –int f(int i); –int f(float i); –int f(int i; int j);
30
Przykład #include using namespace std; int f(int i) { return i; } int f(float i) { return 2*i; } int main(int argc, char* argv[]) { cout<<"Pierwsze wywolanie: "<<f(1)<<endl; cout<<"drugie wywolanie: "<<f((float)1); getch(); return 0; }
31
Kiedy przeciążać funkcje Przeciążanie stosujemy wtedy, gdy funkcje realizują podobny algorytm, ale różny algorytm. Przykładem może być maksimum dla jednej, dwu i trzech liczb. #include using namespace std; int max(int i) { return i;} int max(int i, int j){ return (i>j?i:j);} int max(int i, int j, int k){ return ((i>j)&&(i>j)?i:(j>k)?j:k);} int main(int argc, char* argv[]) { cout<<"max(1)="<<max(1)<<endl; cout<<"max(1,2)="<<max(1,2)<<endl; cout<<"max(1,2,3)="<<max(1,2,3)<<endl; getch(); return 0; }
32
Podsumowanie zagadnień związanych z funkcjami Co nam daje funkcja: –Wydzielamy kod, –Łatwiej zarządzać zmiennymi (funkcje korzystają ze zmiennych lokalnych), Budowa funkcji: –Nagłówek funkcji, –Ciało funkcji Kiedy używamy funkcji: –Trudny problem, –Powtarzalne wykorzystanie algorytmu, –Jeśli coś wykonujemy, to funkcja zwraca wartość typu void, w przeciwnym razie (coś obliczamy), zwraca pewną wielkość o określonym typie.
33
Przykład pokazujący na łatwiejsze zarządzanie zmiennymi #include using namespace std; void cos(int i) { int j=i*2; //zmienne i i j nie mają ZADNEGO związku ze zmiennymi i i j w funkcji głównej return j; } int main(int argc, char* argv[]) { int i, j; j=9; i=10; cos(2); j=i; i=i+2; //zmienne i i j z cos nie licza sie getch(); return 0; }
34
Podsumowanie zagadnień związanych z funkcjami Bardzo ważnym elementem w projektowaniu funkcji jest określenie sposobu komunikacji funkcji z programem. Są cztery różne możliwości: –Zmienne globalne, –Przez wartość, –Przez wskaźnik, –Przez referencję.
35
Komunikacja przez zmienne globalne Wtedy, gdy mamy w programie bardzo ważne zmienne, z których korzystamy wszędzie, Wada – niebezpieczeństwo wystąpienia efektów ubocznych (ostatni przykład ale zmienne i i j są globalne), Zaleta – łatwy dostęp do danych, krótszy tekst programu. Mimo zalet zmienne globalne należy używać bardzo rzadko, najlepiej wcale.
36
Przekazywanie przez wartość Kiedy używamy: –Gdy chcemy do funkcji przekazać wartość jakiejś danej, Czym jest parametr formalny: – identyfikator Czym jest parametr aktualny: –Wyrażeniem Przykłady: –int f(int); –f(1), f(1+1), f(i), f(2+i), f(f(8))
37
Przykład #include using namespace std; //************************************************ float moja_potega(float x, int n) { float il=1.; for (int i=0;i<n;i++) il=il*x; return il; } //************************************************ main() { cout <<"4^5="<<moja_potega(4,5)<<endl; float x=2.; int i=4; cout <<"2^4="<<moja_potega(x,i); getchar(); }
38
Przekazywanie danych przez referencję Kiedy używamy: –Kiedy chcemy zmienić wartość zmiennej (najczęściej), –Możemy także w ten sposób przekazywać dane (gdy tylko przekazujemy, to nie jest zalecane), Schemat przekazywania: –Nagłówek funkcji: int f(int &n) –Wywołanie: f(liczba) –Niepoprawne wywołanie: f(i+4), f(5)
39
Obliczanie wariancji - przekazywanie danych przez referencje – uwaga na tablice #include using namespace std; void wprowadz(int a[10], int &n) { cout >n; for (int i=1; i<=n; i++) { cout >a[i]; } //************************************************* float srednia(int a[10], int n) { float s=0; for (int i=1; i<=n; i++) s+=a[i]; return s/n; } //************************************************ //float wariancja(int *a, int n) void wariancja(int a[10], int n, float &war) { float sr=srednia(a,n); war=0; for (int i=1; i<=n; i++) war=war+(a[i]-sr)*(a[i]-sr); war=war/n; // return (war/n); } //************************************************ main() { int n,a[10]; float war; wprowadz(a,n); wariancja(a,n,war); cout <<"wariancja= "<<war; //cout<<"wariancja="<<wariancja(a,n); getchar(); }
40
Przekazywanie danych przez wskaźniki Kiedy używamy: –Kiedy chcemy zmienić wartość zmiennej (najczęściej), –Możemy także w ten sposób przekazywać dane (gdy tylko przekazujemy, to nie jest zalecane), Schemat przekazywania: –Nagłówek funkcji: int f(int *n) –Wywołanie: f(&liczba) –Niepoprawne wywołanie: f(i+4), f(5)
41
Obliczanie wariancji - przekazywanie danych przez wskaźniki– uwaga na tablice #include void wprowadz(int *a, int *n) { cout >*n; for (int i=0; i<*n; i++) { cout >a[i]; } //************************************************ float wariancja(int *a, int n) { float sr=0; for (int i=0; i<n; i++) sr+=a[i]; sr/=n; float war=0; for (int i=0; i<n; i++) war=war+(a[i]-sr)*(a[i]-sr); //war=sqrt(war/*n); return sqrt(war/n); } //************************************************ main() { int n,a[10]; float war; wprowadz(a,&n); cout <<"wariancja= "<<wariancja(a,n); getchar(); }
42
Co jeszcze dają nam funkcje? Możliwość wykonywania różnych algorytmów dla danych (to, że wykonujemy TEN SAM algorytm dla różnych danych, to oczywiste), Przeciążania funkcji, Stosowania ustawień domyślnych (parametry domyślne).
43
W jaki sposób dokonywać podziału całego algorytmu na funkcje? (trochę już było)
44
Algorytm abstrakcyjny W momencie rozwiązywania złożonego problemu należy skupić się na wyrażeniu rozwiązania w kategoriach języka naturalnego. Takie rozwiązanie będzie nazywane algorytmem abstrakcyjnym. Wyraża on tylko ogólną strategię rozwiązania problemu wraz z ogólną strukturą rozwiązania, które chcemy otrzymać. Algorytm abstrakcyjny zawiera instrukcje abstrakcyjne i dane abstrakcyjne (tzn. takie, które nie mają swoich odpowiedników w danym języku programowania).
45
Metoda zstępująca Tworzenie programu polega więc na określaniu kolejnych uściśleń, tak aby w kolejnych krokach instrukcje i dane abstrakcyjne wyrażać w kategoriach wybranego języka programowania. Można więc powiedzieć, że kolejne uściślenia prowadzą do programu na niższym programie abstrakcji (instrukcje i dane abstrakcyjne za każdym razem są coraz bliższe danemu językowi programowania). Taki proces kończy się w momencie wyrażenia całego rozwiązania w wybranym języku programowania. Taki stopniowy rozkład problemu i jednoczesne uściślanie rozwiązania nosi nazwę metody zstępującej. Ważne jest, by wybrany język programowania pozwalał na zapis takiej drogi otrzymania rozwiązania.
46
Projektowanie metodą zstępującą Sam proces projektowania programu powinien uwzględniać możliwości (instrukcje danego j. programowania). Należy jednak używać możliwie najdłużej symboliki naturalnej dla danego problemu i odkładać jak najdłużej niezbędne decyzje zależne od szczegółów komputera i samego języka programowania. Dzięki temu zmiana języka lub komputera wymaga możliwie małych zmian. Także z pewnych błędnych rozwiązań można się wycofać stosunkowo łatwo (czasem może być potrzebny powrót do pierwszego szkicu rozwiązania).
47
Sprawdzanie poprawności Ważną cechą stopniowego uściślania programu jest to, że równolegle można sprawdzać poprawność tworzonego programu. Należy dowodzić poprawności każdej kolejnej wersji programu (oczywiście na pewnym poziomie abstrakcji). Dowody to nie są operacje w sensie matematycznym. Raczej zdroworozsądkowym, logicznym.
48
Sprawdzanie poprawności Na każdym kolejnym poziomie dowodu poprawności zakłada się, że dalsze uściślenia prowadzące do kolejnego niższego poziomu abstrakcji (bliższego danemu językowi programowania) zachowują odpowiednie kryteria poprawności. Oznacza to, że zaczynamy od pierwszego, najbardziej abstrakcyjnego etapu, i dowodzimy jego poprawności, zakładając, że instrukcje i operacje abstrakcyjne są uściślone poprawnie. Ponieważ programy abstrakcyjne są znacznie prostsze i krótsze więc dowody poprawności są również krótkie i mniej uciążliwe. W kolejnym etapie stosujemy to samo podejście do poszczególnych instrukcji abstrakcyjnych kończąc w chwili, gdy każdą instrukcję zapiszemy w wybranym języku programowania.
49
Uwagi dotyczące zasad wyboru funkcji 1.Funkcja powinna być „logiczna” w tym sensie, że powinna wykonywać pewne, ściśle określone zadanie, powinno ono wynikać z algorytmu, 2.Funkcja powinna wykonywać wszystkie operacje powiązane ze sobą logicznie, 3.Funkcja powinna komunikować się za pomocą parametrów (zmienne globalne używamy w ostateczności, a najlepiej wcale), 4.Należy dokładnie rozważyć, co funkcja robi i jak się komunikuje – jakie dane potrzebuje, co powinna obliczać: 1.Gdy potrzebuje danych – przekazywanie przez wartość, 2.Gdy oblicza parametr – przez wskaźnik i przez referencję, 3.Gdy oblicza jedną wartość, to używamy return, 4.Gdy obliczamy dwie lub więcej wartości, to obliczane wielkości przekazujemy przez parametry.
50
Uwagi dotyczące metody zstępującej i wstępującej Metoda zstępująca (od ogółu do szczegółu): –Wtedy, gdy znamy całe zagadnienie, dla którego piszemy program, –Dzielimy całe (duże) zagadnienie na mniejsze podzagadnienia, –Mniejsze podzagadnienia dzielimy na jeszcze mniejsze, –Iteracje powtarzamy tak długo, aż wszystkie operacje będą mogły być zapisane za pomocą elementów danego języka.
51
Przykłady problemów, które możemy projektować z wykorzystaniem metody zstępującej Są to problemy, które są w sposób zamknięty zdefiniowane. Czyli są to np. problemy opisane za pomocą odpowiednich przepisów prawnych, wynikają ze znanych procedur obowiązujących w firmie itd., Przykładem takich problemów mogą być systemy płacowe, kadrowe, finansowe.
52
Przykłady problemów, które możemy projektować z wykorzystaniem metody wstępującej Są to problemy, które nie są zdefiniowane w sposób ściśle określony, Czyli w trakcie eksploatacji mogą pojawiać się nowe potrzeby, które nie dyskwalifikują naszego rozwiązania, Przykładem takich problemów mogą być np. aplikacje typu system operacyjny, kalkulator.
Podobne prezentacje
© 2024 SlidePlayer.pl Inc.
All rights reserved.