Deklaracja (prototyp) funkcji



Pobieranie 99,61 Kb.
Data01.03.2019
Rozmiar99,61 Kb.

FUNKCJE
Jedną z najbardziej użytecznych cech nowoczesnych języków programowania jest to, że można w nich posługiwać się podprogramami. Podprogram, to program w programie, za pomocą którego możemy definiować własne instrukcje. Za przykład przyjąć możemy podprogram realizujący operację liczenia pola kwadratu, na podstawie zadania jego przekątnej. Od tej chwili, w każdym miejscu naszego programu, w którym chcielibyśmy wyznaczyć pole powierzchni kwadratu, wykorzystujemy w tym celu napisany przez nas podprogram. Taki podprogam, który zwraca jakąś wartość nazywamy po prostu funkcją.

Wprowadzę teraz dwa pojęcia, które pojawią się w dalszej części opisu:



(1) Deklaracja (prototyp) funkcji - to zapis informujący kompilator, że gdzieś w programie znajduje się nasza funkcja zwracająca określony typ wartości i pobierająca określoną ilość i typ parametrów. Deklaracja nie stanowi właściwej zawartością funkcji.

Ogólna składnia deklaracji:



typ identyfikator(parametry);

gdzie:


& typ określa jakiego rodzaju wartość zwraca funkcja (może to być dowolny typ języka, z wyjątkiem tablicy i funkcji);

& identyfikator to nazwa funkcji, pod jaką będziemy ją wywoływać;

& parametry (opcjonalne) - określają argumenty, jakie muszą być przekazane do funkcji,.
(2) Definicja funkcji - określa co ma ona robić, czyli jest to właściwy jej kod.

Ogólna składnia definicji:



[specyfikator1] typ nazwa_funkcji([lista_parametrów])

{

ciało_funkcji

}

gdzie:


& Specyfikator1 (opcjonalny) - może to być słowo: extern, które oznacza, że funkcja ma być dostępna również w innych plikach projektu lub static - oznaczające, że funkcja będzie dostępna tylko w obrębie pliku, gdzie została zadeklarowana.

& ciało_funkcji - właściwy kod funkcji.
Przykład programu zawierającego funkcję:
#include

int pies(int ile);

using namespace std; /*deklaracja funkcji*/

int main()

{

int m=20;



cout<<”Glos”<

m=pies(5);

cout<<”\nOstatecznie m= “<

}

int pies(int ile) /*definicja funkcji*/



{

int i;


for(i=0; i{

cout<<”Hau-Hau !!”;



}

return 123;

}
Wywołanie tego programu da nam wynik:

Glos


Hau-Hau !! Hau-Hau !! Hau-Hau !! Hau-Hau !! Hau-Hau !!

Ostatecznie m=123


Przykładowe deklaracje funkcji:

float kwadrat (int bok); kwadrat, to funkcja z arg. typu int; zwracająca wartość float;

int przypadek(void); brak argumentu; zwraca wart typu int;

char znak_x(); bez arg; zwraca char;

void pin(…); nie zwraca żadnej wart; wywoływana z bliżej (teraz jeszcze) argumentami;
Nazwy arg umieszczone w nawiasach przedstawionych w deklaracji są nieistotne i można je pominąć np.

void as(int 1, char 2, int 3); można zapisać również jako

void as(int , char , int );
Dzieje się tak dlatego, że w deklaracji podajemy kompilatorowi informacje na temat liczby i typu arg. Ich nazwy nie są istotne.

ZWRACANIE REZULTATU PRZEZ FUNKCJE
Podam wpierw przykład programu liczącego potęgi danej liczby:
#include

long potega(int stopien, long liczba);

using namespace std;

int main()

{

int poczatek, koniec;


cout<<”Program liczacy potegi liczb”

<<”całkowitych\n”

<<”z zadanego przedziału \n”

<<”Podaj poczatek przedziału :”;

cin>>poczatek;

cout<<”\nPodaj konic przedziału:” ;

cin>>koniec;


//petla dajac wyniki przedziału//

for(int i=poczatek; i<=koniec; i++)

{

cout<


<<”do kwadratu=”

<

<<”a do szescianu=”

<

<

}

}


long potega(int stopien, long liczba)

{

long wynik=liczba;



for(int i=1 : i< stopien ; i++)

{

wynik = wynik * liczba;



}

return wynik; /* lub na przykład return (wynik +6); - najpierw obliczana jest wartość, potem rezultat jest elementem zwrotu*/

}
Co to znaczy, że funkcja zwraca jakąś wartość?

Oznacza to, że wyrażenie będące wywołaniem tej funkcji ma jakąś wartość np. wyrażenie potega(2,2) ma samo w sobie wartośc 4. Wobec tego, że wyrażenie to posiada jakąś wartość, możemy wykorzystać je w większych wyrażeniach jak np. 4+1/2+potega(2,2).


UWAGI:

  1. jeżeli przypomnimy sobie deklarację funkcji ‘potega’, to miała ona zwracać wartość typu long. W przypadku jednak gdy w ciele funkcji damy np. return 1.33 (czyli liczbę zmiennoprzecinkową), to kompilator dokona niejawnej próby konwersji typu ze zmiennoprzecinkowej na typ long. Kompilator powinien domyślić się jak to zrobić i w rezultaie zwrócić 1.

  2. Jeżeli zadeklarowaliśmy funkcję tak, by zwracała typ void (czyli nic), a w ciele funkcji damy np. return q; kompilator wykryje błąd (funkcja miała nic nie zwracać, a zwraca q) i odwrotnie (tzn. w przypadku, gdy funkcja ma coś zwrócić, a występuje w jej definicji return).


PRZESYŁANIE ARG. DO FUNKCJI PRZEZ WARTOŚĆ
Na wstępie przedstawię różnicę pomiędzy argumentem formalnym funkcji, a argumentem aktualnym funkcji.

& Argument formalny funkcji- argument występujący w deklaracji funkcji;

& Argument aktualny funkcji (inaczej zwany argumentem wywołania funkcji) to parametr, z którym aktualnie funkcja ma wykonać jakieś działanie.

Poniższy przykład rozjaśni nieco pojęcie o tych dwóch rodzajach parametrów.

Zadajmy dowolną funkcję, np.

void alarm(int stopien, int wyjscie)

{

cout<<”Alarm”<

<<”stopnia”

<<”skierowac się do wyjscia nr”

<

}

I niech w naszym programie funkcję tą wywołamy w następujący sposób:



int a,b;



alarm(1,10);

alarm(a,b);

NAZWY ‘stopien’ i ‘alarm’ występujące w definicji funkcji alarm są argumentami formalnymi, natomiast w momencie wywoływania funkcji – czyli dla 1, 10, a, b – są argumentami aktualnymi.
W realizacji naszej funkcji, argumenty do niej przesłane są tylko kopiami. Jakiekolwiek działanie na nich nie wpływa na kształt oryginału. Dowód przedstawi poniższy przykład.
void zwieksz(int formalny)

{

formalny+=1000;



cout<<”funkcja modyfikuje arg formalny\n\t”

<<”i teraz arg ten =”<

}

W funkcji tej zwiększamy wartość argumentu formalnego. Następnie funkcję wywołujemy w takim na przykład fragmencie programu:



int zadowolenie=2;

cout<<”Przed losowaniem LOTTO, zadowolenie=”<

zwieksz(zadowolenie);

cout<<”Po losowaniu, zadowolenie=”<

po wykonaniu fragmentu programu otrzymujemy
Przed losowaniem LOTTO, zadowolenie=2

Funkcja modyfikuje arg formalny

I teraz arg ten=1002

Po losowaniu, zadowolenie=2wiekszolenie,,endl;

O, szcz w takim na przykład fragmencie programu:

przedtawi poniższy przykład

k np.ratu, wykorzystamy w tym
Czym jest STOS ?

& Stos- podręczna pamięć, gdzie przechowywane są zdefiniowane w obrębie funkcji zmienne.
Przy przekazywaniu parametrów przez wartość funkcja tworzy lokalną kopię zmiennej danego typu i zapamiętuje tam przekazaną wartość. Z tego względu wewnątrz funkcji, można dowolnie modyfikować wartość tej zmiennej (chyba, że ma atrybut const), a wartość zmiennej przekazanej nie ulegnie zmianie.
Dla naszego przykładu:

Do funkcji przesyłamy wartość liczbową argumentu aktualnego (w naszy przypadku zadowolenie=2). Przesłana wartość służy do inicjalizacji parametru formalnego, czyli zmiennej lokalnej tworzonej przez funkcję na stosie i jest kopiowana w obrębie funkcji, która pracuje na tej kopi właśnie. W naszym przykładzie dodanie 1000 nie następuje do komórki pamięci, gdzie występuje zadowolenie, ale do zmiennej lokalnej w obrębie funkcji, gdzie mieści się kopia (o nazwie formalny). Po opuszczeniu funkcji ten fragment stosu jest niszczony, znika więc też kopia, nie pozostaje żadne ślad działania w obrębie funkcji.



PRZESYŁANIE ARG. PRZEZ WSKAŹNIK
Metoda ta podaje przy deklarowanym parametrze operator wyłuskania (*). Polega to na tym, że zamiast tworzyć kopię zmiennej w wywołanej funkcji przekazujemy jedynie jej adres. Wykorzystywane jest to głównie w dwóch przypadkach: kiedy chcemy przekazać do funkcji całe tablice, albo kiedy funkcja musi zwrócić więcej niż jedną wartość (zmiana wartości zmiennej przekazanej jako wskaźnik powoduje jej faktyczną zmianę, czyli można zmieniać z poziomu jednej funkcji zmienne deklarowane w innej. Nie wolno przekazywać do funkcji, które pobierają parametry przez adres stałych, gdyż one nie posiadają swoich adresów. Oto przykład:

#include


int wyswietl(char *napis)

{ printf("%s",napis);

return 0;

}
int main(void)

{ char tekst[]="To jest tekst do wyświetlenia";

wyswietl(tekst);

return 0;

}
PRZESYŁANIE ARG. PRZEZ REFERENCJĘ


Rozpocznę od przykładu:
#include

void zer(int wart, int &ref);

using namespace std;

int main()

{

int a=44, b=77;



cout<<”Przed wywolaniem funkcji:zer\n”;

cout<<”a=”<

zer(a,b);

cout<<”Po powrocie z funkcji: zer\n’;

cout<<”a=”<

}

void zer(int wart, int &ref)



{

cout<<”\tW funkcji zer przed zerowaniem\n”;

cout<<”\twart=”<

wart=0;


ref=0;

cout<<”\tW funkcji zer po zerowaniu\n”;

cout<<”\twart=”<

}
Na wyjściu otrzymamy:

Przed wywołanie funkcji: zer

a=44,b=77

W funkcji zer przed zerowaniem

wart=44,ref=77

W funkcji zer po zerowaniu

wart=0,ref=0

Po powrocie z funkcji: zer

a=44, b=0

KOMENTARZ:

W miejscu „Po powrocie z funkcji: zer:” wartość a jest nietknięta (zgodnie z naszymi oczekiwaniami, gdyż argument ten został przekazany przez wartość). Jednak obiekt b ma wartość równą 0. Wytłumaczę poniżej dlaczego tak jest.




  1. Zadeklarowaliśmy funkcję zer, jako funkcję działającą na dwóch argumentach, gdzie pierwszy z nich przesyłany jest przez wartość, drugi natomiast jest przesyłany przez referencję (informuje o tym znak &)

  2. W main występują dwie zmienne wysyłane do funkcji zer (są to parametry a i b)

  3. Wewnątrz funkcji zer, zaraz przed końcem jej kodu, wpisujemy jeszcze wartości dwóch parametrów formalnych wart i ref

  4. Następuje operacja zamieniająca wartość zmiennych wart i ref poprzez nadanie im wartości równej 0 (wszystko to zostaje wyrzucone na ekran)

  5. Kończymy pracę funkcji. Nie zwraca ona żadnej wartości (jest typu void)

  6. Po powrocie do main wypisujemy na ekranie wartości zmiennych a i b; b posiada teraz nową wartość równą 0. Dzieje się tak, ponieważ do funkcji zamiast liczby 77 (czyli wartości zmiennej b), został wysłany adres zmiennej b w pamięci komputera. Funkcja odebrała ten adres i na stosie stworzyła sobie referencję (przezwisko). Komórce pamięci o przysłanym adresie nadała pseudonim ref (czyli komórka znana w main jako b, stała się w funkcji zer komórką o pseudonimie ref). Stąd b i ref to dwie nazwy tej samej komórki. Ponieważ do obiektu o ‘przezwisku’ ref wpisano zero, to taka sama operacja odbyła się w main dla obiektu b.

WNIOSEK:


Przesłanie argumentów funkcji przez referencję pozwala tej funkcji na modyfikowanie zmiennych (nawet lokalnych) znajdujących się poza tą funkcją.

KIEDY MOŻNA POMINĄĆ DEKLARACJĘ FUNKCJI
Każda definicja funkcji jest także przy okazji jej deklaracją. Stąd, jeżeli w pliku definicja funkcji jest wcześniej (czyli po prostu wyżej) niż linijka z jakimkolwiek jej wywołaniem to nie trzeba osobnej jej deklaracji (w innym przypadku kompilator zaprotestuje).

Przykład:


void funkcja_gorna(void)

{

}



main()

{

funkcja_gorna();



funkcja_dolna();

}

void funkcja_dolna (void)



{

}

Próba kompilacji powyższego programu zakończy się komunikatem o błędzie występującym przy pojawiającym się po pierwszym wyrażeniu funkcja_dolna, gdyż jej deklaracja (łącznie z definicją) znajduje się kilka linijek poniżej. W praktyce, podczas pisania rozległych składniowo programów najlepiej wykorzystywać więc zarówno deklarację jak i definicję funkcji, co zaoszczędzi wielu nieporozumień występujących podczas kompilacji.


ARGUMENTY DOMNIEMANE
Zacznijmy od przykładu:

void temperatura(float stopnie, int skala)

{

cout<<”Temperatura komory:”;



switch(skala)

{

case 0:



cout<

break;


case 1:

cout<<(stopnie+273)<<”K\n”;

break;

case 2:


cout<

break;


}

}
Argumentem przesyłanym dla powyżej zdefiniowanej funkcji jest temperatura, podana w stopniach Celsjusza, jednak na ekran chcemy czasem wydrukować temp. w stopniach Celsjusza, czasem w Kelvina, a czasem w stopniach Fahrenheita. Drugi argument funkcji mówio tym w jakiej sklali chcemy otrzymać temperaturę na wyjściu (dla 0 Celsjusze, 1 Kelvina, 2 Fahrenheita). Do zmiany stopni Celsjusza na stopnie Fahrenheita mamy funkcję cel_to_far(float stopnie);


Weźmy teraz kilkaset wywołań naszej funkcji. Zauważmy, że za każdym razem, gdy chcemy temp. otrzymać w stopniach Celsjusza, musimy jako drugi argument funkcji podać 0. Wprowadzę argument domniemany, wykorzystywany w przypadkach, gdy chcemy, by kompilator sam ‘domyślił’ się (domniemał), że chodzi nam o skalę Celsjusza. Wystarczy w tym celu funkcję zadeklarować w nadtępujący sposób:
void temperatura(float stopnie,int skala=0);

czyli dla tak wywołanej funkcji  temperatura(66.3); kopilator będzie wiedział, że są to stopnie Celsjusza (domniema, że drugim argumentem jest 0).


UWAGA:

O tym, że argument jest domniemany informujemy kompilator tylko raz, w deklaracji funkcji. Jeżeli definicja występuje później, to w definicji już się tego nie powtarza. Od tego momentu możemy wywoływać funkcję już z jednym tylko argumentem(lecz nadal w możemy wpisywać drugi argument dla wywołania temp. w skali innej niż Celsjusza).


Wprowadzenie kilku argumentów domniemanych (argumenty takie muszą być na końcu listy)
int multi(int x,float m, int a=4, float y=6.55, int k=10);  tutaj nie jest możliwe opuszczenie domniemanego argumentu a lub y przy jednoczesnym umieszczeniu argumentu k. Błędna jest zatem konstrukcja:

multi(2,3.14,7, ,5);

Wcześniej mówiłem, że jeśli funkcja napisana jest powyżej jakiegokolwiek jej wywołania, to wystarczy sama definicja (bez konieczności jej deklarowania). W takim wypadku argumety domniemane wpisujemy w definicji tej funkcji (która jakby nie było jest przecież teraz jej pełnoprawną deklaracją)
NIENAZWANY ARGUMENT
Może zdarzyć się tak, że w kilkuset linijkowym programie występowałaby funkcja
void ton(int wysokosc);
powodująca, że komputer podczas jej wywoływania zapiszczy jednym z sygnałów ostrzegawczych, gdzie argument wysokosc mówi nam o wysokości tonu tego sygnału. Natępnie nie chcemy korzystać już z tej funkcji podczas pracy naszego programu, więc zredukowaliśmy funkcję do:
void ton(int wysokosc)

{

}



Pojawia się teraz problem z argumentem formalnym wysokosc, który nie zostaje teraz do niczego wykorzystywany. Nie stanowi to błędu, aczkolwiek kompilator cały czas będzie ciągle osztrzegał nas, że czegoś zapomnieliśmy. Jeżeli natomiast wyrzucimy ten argument z deklaracji i definicji funkcji, to wiele wywołań funkcji ton pociągnie za sobą wiele ostrzeżeń podczas procesu kompilacji. W tej sytuacji zamiast wyrzucenia całego argumentu z definicji funkcji wyrzucamy tylko jego nazwę, a typ argumentu pozostawiamy, tak jak zaprezentowano to poniżej:
void ton(int)

{

}



Kompilator wówczas potraktuje ton jako funkcję wywoływaną z jednym tylko argumentem typu int, ale tego argumentu nie będziemy używać. Cięcia takiego wystarczy dokonać tylko w definicji funkcji, ponieważ w deklaracji nazwy argumentów, jeżeli już tam istnieją są przez kompilator ignorowane (jako że w deklaracji skupia się on tylko na odczytaniu typu zmiennych i ich ilości). Żeby zachować pewien charakter niewykorzystywanej już przez nas funkcji możemy również zastosować poniższy sposób na usunięcie argumentu:
void ton(int/*wysokosc*/)

{

}


FUNKCJE inline (w linii)
Załóżmy, że mamy niewielką funkcję (czyli taką, która nie zawiera zbyt wielu instrukcji) wykorzystywaną wiele razy w większym programie. Np.
int zao(float liczba)

{

Return(liczba+0.5);



}

Funkcja ta służy do zaokrąglania liczb rzeczywistych do całkowitych poprze obcięcie części ułamkowej (6.8+0.5=7.37, 6.2+0.5=6.76). W języku C++ koszt wywołania funkcji jest relatywnie niski, jednak jeśli funkcję naszą zamierzamy wywołać tysiące razy, to czas zużyty na wywołanie i powrót może stać się znaczący. Pojawia się wtedy alternatywa:


Albo

1=zao(m) +zao(n*16.7);


Albo

1=(int)(m+0.5)+(int)((n*16.7)+0.5);


Drugi zapis wykona się szybciej, ale ostatecznie może się nam nie chcieć w setkah linii z podobnymi wyrażeniami wpisywać tak długi kod. Istnieje jednak wyjście kompromisowe, które łączy jasność zapisu (jak w przypadku funkcji) z szybkością wykonania (jak wpisania algorytmu zaokrąglania w linię). Naszą funkcję definiujemy jako:
inline int zao(float liczba)

{

return(liczba+0.5);



}
Zapis taki powoduje, że ilekroć w programie umieścimy wywołanie funkcji zao, kompilator dosłownie umieści jej ciało (treść) w linijce, w której to wywołanie nastąpiło. Nie będzie więc sytuacji z wywołaniem i powrotem z tej funkcji. W rezultacie kod taki wykonywał się będzie szybciej.
WNIOSKI:

  1. funkcje typu inline pomyślane zostały dla naprawdę małych, krótkich funkcji i tylko wtedy mają sens. Jeżeli funkcja, którą zdefiniowalismy jest niewielka, to jej treść może zająć mniej miejsca niż kod generowany zawiązany z obsługą wywołania funkcji i powrotem z niej. Wtedy program może być skrócony (mowa tutaj o przypadkach, gdy krótką funkcję wywołujemy w kilku miejscach programu, ale za to miliony razy). W innych przypadkach długość programu może się zwiększyć.

  2. Czasami niektóre kompilatory nie potrafią poradzić sobie z funkcjami typu inline i kompilują je jak zwykłe funkcje.

  3. Jeżeli funkcja jest typu inline, to kompilator napotykając w jakiejś linii jej wywołanie, musi w tej linii wstawić właściwe instrukcje. Zatem teraz sama deklaracje nie wystarczy. Definicja ciała (treść) funkcji musi być już w tym momencie znana kompilatorowi. Stąd funkcje typu inline muszą być na samej górze tekstu programu albo nawet w pliku nagłówkowym, gdzie znajdują się deklaracje innych, zwykłych funkcji, a który to plik dołączany jest w czasie kompilacji madułów naszego programu

Podam teraz przykład programu z funkcją typu inline:


#include

float


poczatek_x, //poczatek ukladu wspolrzednych

poczatek_y,

skala_x=1, //skale: pozioma i pionowa

skala_y=1,

inline float wspx(float wspolrzedna)

{

Return((wspolrzedna-poczatek_x)*skala_x);



}
inline float wspy(float wspolrzedna)

{

return((wspolrzedna-poczatek_y)*skala_y);



}

using namespace std;

int main()

{

float x1=100, y1=100; //przykladowy punkt



cout<<”Mamy w punkt o wspolrzednych\n”;

cout<<”x =”<

<<”y =”< //zmieniamy poczatek ukladu wspolrzednych
poczatek_x=20;

poczatek_y=-500;


cout<<”gdy przesuniemy uklad wspolrzednych tak,\n”

<<”ze poczatek znajdzie się w punkcie \n”

<

<<”\nto nowymi wspolrzednymi punktu\n”

<<”w takim ukladzie sa :”

<<”x=”<
<<”y=”<//zageszczamy skale na osi poziomej

skala_x=0.5;

cout<<”Gdy dodatkowo zmienimy skale pozioma tak,”



<<”ze skala_x=”<
<<\nto ten sam punkt ma teraz wspolrzedne:\n”

<<”x=”<
<<”y=”<}
Na wyjściu otrzymujemy:

Mamy w punkt o wspolrzednych

X=100 y=100

Gdy przesuniemy uklad wspolrzednych tak,

Ze poczatek znajdzie się w punkcie

20,-500

To nowymi wspolrzednymi punktu



W takim ukladzie sa: x=80, y=600

Gdy dodatkowo zmienimy skale poziomatak, ze skala_x=0.5

To ten sam punkt ma teraz wspolrzedne:

X=40 y=600


Zdefiniowane przy pomocy inline funkcje wspx oraz wspy korzystają z niektórych zmiennych globalnych i jak widać zdefiniowane są zgodnie z zasadą powyżej miejsc, gdzie są po raz pierwszy wywoływane. W rezultacie, w każdym miejscu programu, gdzie wywołujemy skalowanie funkcji, wówczas odbywa się ono w sposób maksymalnie szybki – mechnizmem inline.





©operacji.org 2019
wyślij wiadomość

    Strona główna