Wykład 4. Funkcje własne
Transkrypt
Wykład 4. Funkcje własne
Wykład 4. Funkcje własne 4.1. Programowanie strukturalne Programowanie strukturalne jest jedną z metod (paradygmatów) programowania. Metoda ta polega ona na dekompozycji dużego projektu programistycznego na mniejsze zadania cząstkowe. Zadania cząstkowe wykonywane są przez funkcje, które przekazują sobie dane za pośrednictwem argumentów. Funkcja jest to podprogram o określonym działaniu, stanowiący element biblioteki języka C++, lub napisany przez programistę jako tzw. funkcja własna. 4.2. Zalety programowania strukturalnego Podział programu na funkcje, realizujące wydzielone zadania składowe, ułatwia opracowanie projektu. Gotowy program można łatwo ulepszać i rozwijać, modyfikując funkcje własne. Można utworzyć biblioteki własnych funkcji z danej dziedziny zastosowań, ułatwia pisanie innych programów z danej dziedziny. Ta sama funkcja może być bez zmian wykorzystana w różnych programach. Jeżeli jakieś zadanie cząstkowe jest wielokrotnie wykonywane w różnych miejscach programu, to powierzenie tego zadania funkcji własnej skutkuje znacznym skróceniem tekstu programu. 2 4.3. Wywoływanie funkcji w main W celu uruchomienia w ciele main funkcji (własnej lub bibliotecznej), trzeba wykonać instrukcję wywołania tej funkcji. Wywołanie funkcji w main: int main() { instrukcja; instrukcja; instrukcja_wywołania; instrukcja_wywołania; → //rozpoczęcie wykonania { instrukcja; instrukcja; ---instrukcja; } ← //powrót do main instrukcja; instrukcja; === return 0; } W chwili, gdy kolejną instrukcją main jest instrukcja wywołania podprogramu, następuje przerwanie działania main. Następnie rozpoczyna się wykonanie podprogramu. Po jego zakończeniu następuje automatyczny powrót do kolejnej instrukcji w main – następnej po instrukcji wywołania funkcji. 3 4.3. Definicja funkcji własnej Ogólna postać definicji funkcji typ_funkcji nazwa_funkcji (typ nazwa_argumentu, typ nazwa_argumentu, . . . ) { instrukcja; instrukcja; // - - - instrukcja; } Typ funkcji jest to typ wartości, jaką przyjmuje instrukcja wywołania tej funkcji po zakończeniu jej działania. Na przykład funkcja biblioteczna sqrt jest typu double. double y=sqrt(x); wartość sqrt(x) po prawej stronie operatora = zostaje zapamiętana w zmiennej y, której typ jest zgodny z typem funkcji. Istnieją funkcje o typie void. Instrukcja wywołania funkcji typu void nie przyjmuje żadnej wartości. gFunkcji typu void nie wolno wywoływać w wyrażeniach, ani po prawej stronie instrukcji przypisania. Jej wywołanie ma postać samodzielnej instrukcji, np: putch(’A’); //napisanie litery A Nazwa funkcji nadawana jest dowolnie przez programistę. Argumenty funkcji służą do pobrania przez funkcję danych do obliczeń (argumenty wejściowe) lub przekazania wyników obliczeń (argumenty wyjściowe). 4 4.4. Miejsce definicji funkcji własnej w programie Przykład 1 Funkcja typu void z argumentami wejściowymi Zadaniem funkcji jest dodanie argumentów i wyprowadzenie wyniku na ekran. #include <studio.h> #include <conio.h> void dodaj(int,int); //prototyp funkcji int main() } int a,b; printf("\nPodaj dwie dodawane liczby: "); scanf("%d%d",&a,&b); dodaj(a, // ↓ getch(); // ↓ return 0;// ↓ } // ↓ // ↓ // ↓ // ↓ void dodaj(int x,int { int s; s=x+y; printf(”%d”,s); } //powrót do main → b); //wywołanie → dodaj ↓ ↓ // autom. przypisania: ↓ // x=a, y=b ↓ ↓ ↓ ↓ y) //definicja funkcji Definicje funkcji własnych piszemy za main. Prototypy (deklaracje) funkcji własnych piszemy przed main. 5 Prototypy są to nagłówki funkcji, w których można pominąć nazwy argumentów, ale trzeba napisać ich typy. W odróżnieniu od nagłówka definicji, prototyp jest zakończony średnikiem. Dzięki obecności prototypu kompilator może rozpoznać nazwę funkcji oraz sprawdzić, czy w instrukcjach wywołania użyto odpowiedniej (zgodnej z definicją) liczby, typów i kolejności argumentów. Prototypy funkcji bibliotecznych znajdują się w zbiorach nagłówkowych, włączanych do programu za pomocą dyrektyw #include. 4.5. Przekazywanie danych wejściowych z main do funkcji Instrukcja wywołania funkcji dodaj(a,b) powoduje: (a) przekopiowanie argumentów wywołania do argumentów funkcji (b) uruchomienie sekwencji instrukcji zawartych w ciele funkcji Po zakończeniu działania funkcji następuje przejście do kolejnej instrukcji main . dodaj(a, void dodaj(int x, { int s; s=x+y; printf(”%d”,s); } //powrót do main b); int y) //def. funkcji b a 5 118 y = b; x = a; x y 5 118 6 Liczba argumentów w instrukcji wywołania funkcji musi być taka sama, jak w definicji funkcji; także ich kolejność i typy muszą być zgodne. Przekazanie danych odbywa się jednokierunkowo. Ewentualne zmiany wartości zmiennych x, y w czasie działania funkcji dodaj nie wpływają na stan zmiennych a, b w funkcji main. 4.6. Zmienne wskaźnikowe Zmienne wskaźnikowe (zwane w skrócie wskaźnikami) służą do przechowania adresów innych zmiennych określonego typu. Zmienną wskaźnikową deklarujemy, pisząc symbol ’*’ przed nazwą zmiennej. Na przykład: float *wf; // zmienna wf zawiera adres zmiennej typu float double *wd; // zmienna wd zawiera adres zmiennej typu double int *wi; // zmienna wi zawiera adres zmiennej typu int; Chcąc wpisać do zmiennej wskaźnikowej konkretny adres innej zmiennej, stosujemy operator adresu &, jak poniżej: int x=9; int *wx; wx=&x; To samo można uzyskać, stosując krótszy zapis, jak poniżej: int x=9, *wx=&x; Zmienna wx wskazuje na zmienną x (jest jej wskaźnikiem), Zmienna x jest zmienną wskazywaną przez wx. W C++ istnieje stała o nazwie NULL, która oznacza adres pusty. Wskaźnik o zawartości NULL nie wskazuje na żadną komórkę pamięci. Ilustrację tych pojęć stanowi rysunek: int x=9, *wx=&x; 7 wx x FFF4 –––––→ &x== FFF4 9 wx=NULL; wx NULL Utworzenie wskaźnika na zmienną x pozwala odwołać się do zmiennej x za pośrednictwem tego wskaźnika. W tym celu zmienną wskaźnikową poprzedzamy gwiazdką: double x,*wx=&x; //utworzenie wskaźnika na zmienną x *wx=3.56; //odwołanie się przez wskaźnik do zmiennej x 4.7. Mechanizm przekazywania danych z funkcji do main za pomocą argumentu wskaźnikowego Przekazywanie wyniku przez wskaźnik opiera się na bardzo prostym pomyśle. Funkcja ma dodatkowy argument – wskaźnikowy. Po wywołaniu funkcji, do tego argumentu wskaźnikowego zostaje przekopiowany adres tej zmiennej funkcji main, do której chcemy przekazać wynik. Odwołując się w ciele funkcji do tego adresu, przekazujemy wynik do main. Pokazuje to przykład 2. 8 Przykład 2 Zadaniem funkcji jest dodanie wartości dwóch liczb całkowitych. Funkcja jest typu void i posiada dwa argumenty wejściowe i jeden argument wskaźnikowy. Argumenty wejściowe służą do przekazania z main do funkcji wartości dodawanych liczb. Argument wskaźnikowy służy do przekazania z main do funkcji adresu zmiennej, do której należy przesłać wynik. #include <studio.h> #include <conio.h> void dodaj(int,int,*int); //prototyp funkcji int main() } int a,b,suma; printf("\nPodaj dwie dodawane liczby: "); scanf("%d%d",&a,&b); dodaj(a, b, &suma); //wywołanie printf(”%d”,suma);↓ ↓ ↓ getch(); // ↓ ↓ ↓ ↓ ↓ // x=a,y=b return 0; // ↓ } // ↓ ↓ ↓ // w=&suma // ↓ ↓ ↓ // ↓ ↓ ↓ // ↓ ↓ ↓ void dodaj(int x,int y, int* w) //definicja { int s; s=x+y; *w=s; //przekazanie wyniku pod adres w==&suma } //skok do instrukcji printf w main → 9 Przykład 3. Program z funkcją własną pierw_2, obliczającą metodą Newtona z dokładnością EPS pierwiastek kwadratowy z liczby x. Funkcja jest typu void, ma dwa argumenty wejściowe do przekazania wartości x, EPS. Funkcja przekazuje wynik obliczeń za pośrednictwem argumentu wskaźnikowego. #include <stdio.h> #include <conio.h> #include <math.h> void pierw_2(double,double,double *); //prototyp int main() { const double EPS=1e-6; double wynik,x; printf("\nPodaj x: "); scanf("%lf",&x); pierw_2(x,EPS,&wynik); //wywołanie funkcji printf("\nWynik: %.6lf",wynik); getch(); return 0; } void pierw_2(double x, double eps, double *w) { double p; if (x<eps*eps) p=0; else { if(x>1.0) p=x/2.0; else p=x*2.0; double p0=p+2*eps; while(fabs(p0-p)>=eps) { p0=p; p=(x+p*p)/(2*p); } } *w=p; //przekazanie p pod adres w==&wynik } 10 4.8. Przekazywanie danych z funkcji do main przez argumenty referencyjne Relacja referencji Stosując operator referencji &, jak w zapisie poniżej: int x, &y=x; gdzie & jest operatorem referencji, powodujemy, że obie zadeklarowane w ten sposób zmienne: x oraz y, pozostają w związku referencyjnym. Oznacza to, że obie zmienne są zlokalizowane w tym samym obszarze pamięci. Wszelkie zmiany wartości jednej z tych zmiennych będą dotyczyły także drugiej zmiennej. Można powiedzieć, że jedna zmienna ma dwie różne nazwy: x oraz y. Jeżeli utworzymy referencję pomiędzy argumentem wyjściowym funkcji, a zmienną użytą w instrukcji wywołania, to wszelkie działania na argumencie wyjściowym będą jednocześnie działaniami na zmiennej w instrukcji wywołania. Dzięki temu można wyniki działania funkcji bezpośrednio przekazywać do main. dodaj(a,b,suma);//instrukcja wywołania void dodaj(int x,int y,int &w) //nagłówek funkcji W wyniku wywołania występuja automatyczne operacje: int x=a; int y=b; int &w=suma; Zatem zmienne w, suma są w związku referencyjnym – jest to ta sama zmienna o dwóch różnych nazwach! 11 Przykład 4. Funkcja dodaj, która przekazuje sumę swoich argumentów wejściowych przez argument referencyjny #include <studio.h> #include <conio.h> void dodaj(int,int,&int); //prototyp funkcji int main() } int a,b,suma; printf("\nPodaj dwie dodawane liczby: "); scanf("%d%d",&a,&b); dodaj(a, b, suma);//wywołanie printf(”%d”,suma);↓ ↓ ¦ getch(); // ↓ ↓ ¦ return 0; // ↓ ↓ ¦//x=a,y=b, } // ↓ ↓ ¦//&w=suma ↓ ¦ // ↓ // ↓ ↓ ¦ // ↓ ↓ ¦ void dodaj(int x,int y, int &w) //definicja { int s; s=x+y; w=s; //przekazanie wyniku s do &w=suma } //skok do instrukcji printf w main → 12 Przykład 5. Program z funkcją własną pierw_2, obliczającą metodą Newtona z dokładnością EPS pierwiastek kwadratowy z liczby x. Funkcja przekazuje wynik obliczeń przez argument referencyjny. #include <stdio.h> #include <conio.h> #include <math.h> void pierw_2(double,double, double &); //prototyp int main() { const double EPS=1e-6; double wynik,x; printf("\nPodaj x: "); scanf("%lf",&x); pierw_2(x,EPS, wynik); //wywołanie funkcji printf("\nWynik: %.6lf",wynik); getch(); return 0; } void pierw_2(double x,double eps, double &w) { double p; if (x<eps*eps) p=0; else { if(x>1.0) p=x/2.0; else p=x*2.0; double p0=p+2*eps; while(fabs(p0-p)>=eps) { p0=p; p=(x+p*p)/(2*p); } } w=p; //przekazanie wartości p do &w=wynik } 13 4.9. Przekazywanie danych za pomocą instrukcji return Wykonanie zawartej w bloku funkcji instrukcji return powoduje, że instrukcja wywołania funkcji jako całość przyjmuje taką wartość, jak wyrażenie po słowie return. Wstawiając po słowie return wynik obliczeń, przekazujemy go więc jako wartość, którą przyjmuje instrukcja wywołania: Funkcja biblioteczna sqrt zwraca wynik typu double przez return. Przy wywołaniu: x=sqrt(2); wyrażenie sqrt(2) przyjmie wartość 1.414... i ta wartość zostaje zapamiętana w zmiennej x. Można wywołać tego rodzaju funkcję jako argument printf: printf(”%.3lf”,sqrt(2)); co spowoduje wyprowadzenie wartości 1.414 na ekran. Wywołując funkcję sqrt w taki sposób: sqrt(2); również spowodujemy jej uruchomienie. Nie da to jednak żadnej korzyści, bo ta wartość nie zostanie nigdzie zapamiętana, ani wyprowadzona na ekran. Funkcja zawierająca return nie może być typu void – funkcja ma taki typ, jak wyrażenie po return. 14 Przykład 6. Funkcja dodaj, która przekazuje sumę swoich argumentów wejściowych przez return #include <studio.h> #include <conio.h> int dodaj(int,int); //prototyp funkcji int main() } int a,b,suma; printf("\nPodaj dwie dodawane liczby: "); scanf("%d%d",&a,&b); suma=dodaj(a,b);//wywołanie printf(”%d”,suma); getch(); return 0; } int dodaj(int x,int y) //definicja funkcji { int s; s=x+y; return s; //przekazanie wyniku pod &w=suma } //skok do instrukcji printf w main → Przykład 7. Program z funkcją własną pierw_2, obliczającą metodą Newtona z dokładnością EPS pierwiastek kwadratowy z liczby x. Funkcja przekazuje wynik obliczeń przez return. #include <stdio.h> #include <conio.h> #include <math.h> double pierw_2(double,double); //prototyp 15 int main() { double p,x; const double EPS=1e-6; printf("\nPodaj argument pierwiastka: "); scanf("%lf",&x); p=pierw_2(x,EPS); //wywołanie funkcji printf("\nWynik: %.6lf",p); getch(); return 0; } //definicja funkcji double pierw_2(double x,double eps) { double p; if (x<eps*eps) p=0; else { if(x>1.0) p=x/2.0; else p=x*2.0; double p0=p+2*eps; while(fabs(p0-p)>=eps) { p0=p; p=(x+p*p)/(2*p); } } return p; //przekazanie wyniku } Na zakończenie tego punktu, zapamiętajmy, że: Funkcja, która nie jest typu void, musi zawierać przynajmniej jedną instrukcję return. Funkcja typu void nie może zawierać instrukcji return. Wyrażenie po return musi mieć typ zgodny z typem funkcji. Przez return można zwracać tylko jedną wartość. Jeżeli funkcja ma zwrócić więcej danych, to trzeba ją zaopatrzyć w odpowiednie argumenty wyjściowe – wskaźnikowe lub referencyjne. 16 4.10. Funkcja wywołująca samą siebie – rekurencja Funkcja może wywoływać w swoim bloku samą siebie. Daje to możliwość prostego programowania obliczeń rekurencyjnych, które stanowią istotę wielu algorytmów. Zaletą procedur rekurencyjnych jest duża prostota zapisu, a ich wadą – duża zajętość obszaru pamięci przeznaczonego na przechowanie zmiennych automatycznych. Przykład 8. Funkcje znajdujące n-ty wyraz szeregu Fibonacci: 1, 1, 2, 3, 5, 8, 13, 21 … Dwa pierwsze wyrazy szeregu są jedynkami, a każdy następny wyraz jest sumą dwóch wyrazów poprzedzających. Oznaczając n–ty wyraz szeregu jako wn, można to zapisać następująco: Jeśli n<3, to wn=1, w przeciwnym przypadku wn = wn-1 + wn-2. Zapis ten prowadzi bezpośrednio do pokazanej realizacji rekurencyjnej funkcji własnej fibon. Przykład 8. Obliczenie n-tego wyrazu szeregu Fibonacci #include <conio.h> #include <stdio.h> double fibon(unsigned); //prototyp int main() { printf("Podaj numer wyrazu: "); unsigned n; scanf("%d",&n); printf("Wyraz nr %u: %le",n, fibon(j)); getch(); return 0; } 17 //definicje funkcji fibon //wersja rekurencyjna double fibon(unsigned n) { double w1,w2; if (n<3) return 1; w1=fibon(n-1); w2=fibon(n-2); return w1+w2; } //wersja iteracyjna double fibon(unsigned n) { double post=1,ost=1,wyraz; if(n<3) return 1.0; for(int j=2;j<=n;j++) { wyraz=post+ost; post=ost; ost=wyraz; } return wyraz; }