Algorytmika i Programowanie.
Funkcje cz. I
Zakład Zastosowań Informatyki
w In\ynierii Lądowej
Wydział In\ynierii Lądowej
Politechnika Warszawska
Sławomir Czarnecki
Funkcje
" Dotychczas u\ywaliśmy wyłącznie jednej funkcji main() ...
" ... i korzystaliśmy z niektórych gotowych funkcji bibliotecznych.
" W języku C++, jak i w ka\dym innym języku programowania
powinniśmy tworzyć aplikacje w oparciu o własne funkcje, które
w wielu językach programowania nazywane są podprogramami lub
modułami.
" Trudno sobie bowiem wyobrazić, choć teoretycznie jest to mo\liwe,
aby nawet niedu\y program, mógł być napisany w oparciu o jedną
jedyną funkcję (funkcję main() w przypadku języka C++).
" Funkcja jest to oddzielnie napisany blok instrukcji.
" Ka\da funkcja ma nazwę, która ją identyfikuje i która jest u\ywana w
momencie wywoływania funkcji w programie.
" Dopuszcza się aby ró\ne funkcje miały te same nazwy.
" Obowiązują te same reguły nadawania nazw funkcjom co w przypadku
nadawania nazw zmiennym.
" Nazwa funkcji powinna odpowiadać temu co dana funkcja robi , np.
bool even(long n)
funkcja even(...) zwraca true jeśli
{
argument typu long jest liczbą
if (n % 2 == 0)
parzystą, w przeciwnym przypadku
return true;
else zwraca false.
return false;
}
" Wszelkie informacje, które muszą być znane aby funkcja mogła
wykonać instrukcje mogą być przekazane za pomocą tzw. listy
argumentów funkcji, co nie wyklucza pustej listy w przypadku
funkcji bezparametrowych.
" Wyra\enia na tej liście w momencie wywołania funkcji muszą być
umieszczone w tej samej kolejności i zgodnie z typem jaki był
w deklaracji (lub definicji) funkcji.
" Relacja pomiędzy argumentem funkcji w momencie jej wywołania
odpowiadającemu parametrowi tej funkcji w jej definicji jest
zilustrowana poni\ej: Argument
cout<
Parameter
bool even(long n)
{
if(n%2==0)
return true; Function definition
else
return false;
}
value 0 returned
" W przykładzie tym, funkcja even(...) zwraca 1 lub 0 (true lub false) w
zale\ności od wartości argumentu typu long, który jest przekazywany
do tej funkcji w momencie jej wywołania.
" Funkcja mo\e zwrócić:
albo pojedynczą wartość
albo nic.
" Warto podkreślić, \e zwracanie pojedynczej wartości nie oznacza
wcale, \e zwracane mogą być wyłącznie na przykład tylko liczby,
poniewa\ tą zwracaną wartością mo\e być wskaznik, który z kolei
mo\e reprezentować tablicę itd..., a zatem zwracać mo\emy bardzo
zło\one obiekty.
" Główną zaletą funkcji jest mo\liwość jej wielokrotnego wywoływania
w ró\nych miejscach programu.
" Brak mo\liwości pisania kodu instrukcji w postaci bloku
zdefiniowanego w postaci funkcji, prowadziłby szczególnie w
przypadku dłu\szych programów do powstania kodu o trudnej do
wyobra\enia długości.
" Głównym powodem tworzenia funkcji jest jednak znaczne
uproszczenie pisania bardziej zło\onych programów poprzez
podzielenie ich kodu na mniejsze i spójne logicznie fragmenty,
pozwalające na lepsze zrozumienie całego kodu zarówno przez
programistę jak i osoby czytające program a tym samym na
zmniejszenie prawdopodobieństwa popełnienia niektórych błędów
i pomyłek.
" Ponadto, podział programu na funkcję, umo\liwia tworzenie ich kodu
przez ró\ne osoby zaanga\owane w tworzenie projektu i pózniejsze
niezale\ne testowanie fragmentów aplikacji.
" Spójrzmy na następujący przykład
double power(double x, int n) // Function header
{ // Function body starts here...
double result = 1.0; // Result stored here
for(int i = 1; i<=n; i++)
result *= x;
return result;
} // ...and ends here
" Zacznijmy od nagłówka funkcji w pierwszej linii kodu:
double power(double x, int n)
" Składa się on z trzech części:
-Typu zwracanej wartości (double w naszym przypadku)
-Nazwy funkcji, power
-Parametrów funkcji zawartych pomiędzy nawiasami
double power(double x, int n)
" Funkcja zwraca wartość do funkcji,
{
w której została ona wywołana.
double result = 1.0;
" Nasza funkcja ma dwa parametry:
for(int i = 1; i<=n; i++)
parametr x typu double - wartość,
result *= x;
która jest podnoszona do danej potęgi,
return result;
parametr n typu int - wykładnik potęgi.
}
" Funkcja przeprowadza obliczenia przy
wykorzystaniu swoich parametrów
x i n oraz dodatkowo przy pomocy
zmiennej result zadeklarowanej w jej
ciele.
" Ogólnie nagłówek ma następującą postać:
zwracany_typ NazwaFunkcji(lista_parametrow)
" gdzie zwracany_typ jest dowolnym dopuszczalnym typem.
" Jeśli funkcja nie zwraca \adnej wartości, wtedy zwracany typ
specyfikujemy słowem kluczowym void.
" Słowo kluczowe void mo\e być tak\e u\ywane jako lista_parametrow
w przypadku, gdy funkcja nie ma \adnych parametrów w takim
przypadku mo\na u\yć po prostu pustych nawiasów. Mamy zatem
dwie mo\liwości w przypadku pustej listy argumentów: albo piszemy
(void) albo piszemy (). Przykładowo, nagłówek funkcji, która
nie zwraca \adnej wartości i nie ma \adnych parametrów mógłby
wyglądać następująco:
void MyFunction(void)
lub
void MyFunction()
Uwaga !
Nagłówek w postaci:
MyFunction(void)
lub
MyFunction()
jest generalnie błędny, chyba, \e byłby to tzw. konstruktor.
double power(double x, int n) Ciało funkcji
{ " Obliczenia są napisane w postaci
double result = 1.0; instrukcji w ciele funkcji bloku
instrukcji ograniczonych nawiasami
for(int i = 1; i<=n; i++)
result *= x; { ... }.
return result; " W naszym przykładzie, w pierwszej
} instrukcji deklarowana jest zmienna
result, która jest inicjalizowana
wartością 1.0.
" Zmienna result jest lokalną zmienną
funkcji zadeklarowaną w ciele funkcji.
" Oznacza to m.in., \e zmienna result
przestaje istnieć po wykonaniu
wszystkich instrukcji w ciele funkcji i
wyjściu z funkcji.
" Kolejne obliczenia są przeprowadzane
double power(double x, int n)
w pętli for.
{
" W pętli for mamy zadeklarowaną
double result = 1.0;
kolejną zmienną i, która przyjmuje
for(int i = 1; i<=n; i++)
kolejno wartości od 1 do n.
result *= x;
" Zmienna result jest mno\ona przez x
return result;
w ka\dej kolejnej iteracji pętli, co po
}
n-krotnym przebiegu pętli skutkuje
\ądaną wartością result = x*x*...*x.
" Jeśli n będzie równe 0 w momencie
wywołania funkcji power, to wtedy
pętla for nie będzie ani razu wykonana,
co spowoduje, \e wartość zmiennej
result będzie równa 1.0 tu\ przed
wyjściem z funkcji i zwróceniem jej
wartości.
double power(double x, int n)
{
" Jak ju\ zauwa\yliśmy, wszystkie
double result = 1.0;
zmienne zadeklarowane w ciele
for(int i = 1; i<=n; i++)
funkcji, podobnie jak jej parametry
result *= x;
są lokalne względem funkcji.
return result;
}
" Nie ma zatem \adnych przeciwwskazań
aby u\ywać takie same nazwy
ró\nych zmiennych w ró\nych
funkcjach
double power(double x, int n)
{
double result = 1.0;
for(int i = 1; i<=n; i++)
result *= x;
return result;
}
" Zasięg zmiennej zadeklarowanej wewnątrz funkcji
jest zdeterminowany według tych samych reguł o
jakich była mowa poprzednio.
" Zmienna jest tworzona w miejscu jej deklaracji i
przestaje istnieć na końcu bloku, który ją zawiera.
" Istnieje jednak bardzo wa\ny wyjątek od tej reguły.
" Tym wyjątkiem są zmienne zadeklarowane
wewnątrz funkcji jako static, o których powiemy
nieco pózniej.
double power(double x, int n) " W instrukcji return tworzona jest
{ kopia zwracanej zmiennej u nas
double result = 1.0; zmiennej result i tylko ta kopia jest
for(int i = 1; i<=n; i++) dostępna po bezpośrednim wyjściu z
result *= x; funkcji.
return result; " Nie wykorzystanie tej kopii w
} momencie wywołania funkcji
(w instrukcji, w której funkcja jest
wywoływana), na przykład w formie
nadania wartości jakiejś zmiennej
zdefiniowanej w funkcji, w której
wywołana została nasza funkcja,
spowoduje utratę (i to na zawsze)
mo\liwości odzyskania obliczonego
wyniku (u nas wartości zmiennej
result).
double power(double x, int n)
" Ogólna postać instrukcji return
{
jest następująca:
double result = 1.0;
return expression;
for(int i = 1; i<=n; i++)
gdzie wartość wyra\enia expression
result *= x;
musi odpowiadać typowi
return result;
zadeklarowanemu w nagłówku funkcji.
}
" Nale\y podkreślić, \e expression wcale nie musi być nazwą zmiennej,
ale mo\e być i często jest wyra\eniem, którego wartość musi jedynie
odpowiadać typowi zwracanemu przez funkcję.
" Co więcej, expression mo\e być nazwą tej samej funkcji. Mamy wtedy
tzw. rekurencyjne wywołanie funkcji, o czym będziemy mówić w
dalszym ciągu wykładu.
" W przypadku wartości zwracanej typu void, nie mo\e pojawić się za
instrukcją return ju\ \adne wyra\enie.
" Musimy wtedy albo napisać: return;
... albo po prostu nic nie pisać (co jest najczęściej praktykowane).
" Zanim wywołamy funkcję w programie, musimy najpierw ją
zadeklarować u\ywając instrukcji zwanej prototypem funkcji.
" Prototyp funkcji ma dostarczyć minimum niezbędnych informacji
kompilatorowi, które pozwoliłyby mu na sprawdzenie poprawności
u\ycia funkcji w programie.
" Deklaracja funkcji, mówiąc najprościej, wymaga ujawnienia tych
wszystkich informacji, które są zawarte w nagłówku funkcji z
dodaniem na jej końcu średnika ; .
" Oczywiście liczba parametrów, ich typ oraz kolejność muszą
być takie same i zgadzać się z nagłówkiem funkcji rozpoczynającym
definicję funkcji (gdzieś w programie, ale często nawet w innym pliku).
" Deklaracje funkcji u\ywanych w programie muszą być umieszczone
przed instrukcjami, w których te funkcje są wywoływane i najczęściej
są umieszczane na początku programu.
" W naszym przykładzie funkcji power(...), deklaracja wyglądać mo\e
następująco:
double power(double value, int index);
" Pamiętajmy o średniku na końcu deklaracji !
" Zauwa\my, \e w deklaracji nie ma przeciwwskazań do tego, aby
nazwy parametrów były inne ni\ w definicji funkcji.
" Co więcej, nazw w deklaracji funkcji u\ywamy głównie tylko po to,
aby łatwiejsze było zinterpretowanie tego, co dana funkcja robi .
" Oczywiście najczęściej (jeśli w ogóle) u\ywamy tych samych nazw.
" Jeśli chcemy, to w prototypie funkcji mo\emy pominąć nazwy
parametrów i tylko pozostawić ich typy oczywiście w odpowiedniej
kolejności, np.:
double power(double, int);
... i to całkowicie wystarcza kompilatorowi.
" Zaleca się jednak umieszczanie nazw parametrów funkcji w jej
deklaracji głównie z powodu lepszej przejrzystości kodu.
" Szczególnie w sytuacji kiedy liczba parametrów funkcji jest większa
ni\ jeden, a na dodatek, jeśli typy tych parametrów są takie same,
zalecane jest u\ywanie nazw zmiennych w deklaracji, w celu
uniknięcia ewentualnych przekłamań w interpretacji.
" Przećwiczmy nowo poznane pojęcia, na przykładzie trzech funkcji,
które deklarujemy następująco:
bool even(long);
long smallDiv(long);
void factor(long);
#include
#include
using namespace std;
bool even(long);
long smallDiv(long);
prototypy funkcji deklaracje funkcji
void factor(long);
void main()
{
long n=6934096L;
funkcja main()
factor(n);
}
bool even(long n)
{
definicja funkcji even(...)
...
}
long smallDiv(long n)
{
definicja funkcji smallDiv(...)
...
}
void factor(long n)
{
...
definicja funkcji factor(...)
}
" Napiszemy funkcję, którą wywoływać będziemy np. factor(n) aby
otrzymać na ekranie informację o rozkładzie liczby naturalnej n.
" Na przykład, wywołanie factor(18) powinno skutkować
wyświetleniem na ekranie napisu następującego:
18 = 2 x 3 x 3
" Faktoryzacja liczby naturalnej n wymaga powtórzenia następujących
kroków:
-znajdz najmniejszy naturalny dzielnik liczby n, który jest liczbą
pierwszą.
-podstaw go np. pod zmienną curFactor i wyświetl ją jako curFactor
-podziel n przez curFactor
" Potrzebować będziemy zatem dodatkowo funkcji smallDiv(n) w celu
znalezienia rozwiązania następującego podproblemu: znajdz
najmniejszy dzielnik liczby naturalnej n, który jest liczbą pierwszą.
" Do tego ostatniego zadania przyda nam się dodatkowo funkcja
even(num) sprawdzająca czy dana liczba naturalna jest parzysta czy
te\ nieparzysta.
" Zale\ności pomiędzy funkcjami, które zamierzamy zdefiniować
factor(...) , smallDiv(...) , even(...) są pokazane poni\ej:
even(...)
smallDiv(...)
factor(...)
tzn.
funkcja factor(...) wywołuje funkcję smallDiv(...) a
funkcja smallDiv(...) wywołuje funkcję even(...).
" Funkcja even
bool even(long num)
{
if (num % 2 == 0)
return true;
else
return false;
}
funkcja even(num) zwraca true jeśli jej argument num (typu long)
jest liczbą parzystą, w przeciwnym przypadku zwraca false.
" Mo\liwe jest napisanie bardziej zwięzłe tej funkcji, a mianowicie
bool even(long num)
{
return ((num % 2) == 0);
}
" Funkcja smallDiv
Algorytm znajdowania najmniejszego dzielnika liczby naturalnej n
jako liczby parzystej 2 lub jako liczby nieparzystej pomiędzy 3 a n.
Pre: liczba naturalna n > 1
1.If (jeśli) n jest parzysta
divisor jest równy 2
else (w przeciwnym przypadku)
zainicjalizuj divisor zerem 0 (zaznaczając tymczasowo, \e nie
został znaleziony, póki co, jeszcze dzielnik divisor o zadanej
własności, a następnie zainicjalizuj próbny dzielnik trial wartością 3
2.Dopóty dopóki nie został znaleziony dzielnik divisor o zadanej
własności, sprawdzaj kolejne liczby nieparzyste (trial).
Jeśli divisor (tzn. trial) został znaleziony, zachowaj jego wartość jako
divisor.
Jeśli wartość trial przekroczy "n, zachowaj n jako divisor.
3.Zwróć divisor jako najmniejszy dzielnik n (spełniający wymagany
warunek).
long smallDiv(long n)
Ustala inicjalizację divisor i trial
{
w zale\ności od tego czy n jest parzyste lub nie
long trial;
long divisor;
if (even(n)) Pre: n > 1
divisor = 2; 1. If n jest parzysta
else divisor jest równy 2
{ else
divisor = 0; inicjalizuje divisor zerem 0 (co oznacza, \e
trial = 3; divisor nie został jeszcze znaleziony)
} inicjalizuje trial jako bie\ącego kandydata na
while (divisor == 0) dzielnik n liczbą trzy 3 (trial = 3)
if (trial > sqrt(n))
divisor = n;
else if ((n % trial) == 0)
divisor = trial;
else
trial += 2;
return divisor;
}
long smallDiv(long n)
{
long trial;
long divisor;
if (even(n))
divisor = 2;
else
{
divisor = 0;
trial = 3;
}
while (divisor == 0)
if (trial > sqrt(n))
Testuje kolejne liczby nieparzyste jako
divisor = n;
potencjalne dzielniki n a\ do znalezienia divisor
else if ((n % trial) == 0)
lub zakończy testowanie w pewnym momencie
divisor = trial;
jeśli trial stanie się na tyle du\e, \e stanie się
else
jasne, \e tylko n mo\e dzielić siebie samego.
trial += 2;
return divisor;
}
long smallDiv(long n)
{
long trial;
long divisor;
if (even(n))
divisor = 2;
else
{
divisor = 0;
trial = 3;
}
while (divisor == 0)
if (trial > sqrt(n))
divisor = n;
else if ((n % trial) == 0)
divisor = trial;
else
trial += 2;
return divisor;
3. Zwraca divisor (będzie taki o jaki nam chodziło !)
}
" Funkcja factor(...)
-Zaczynamy od znalezienia najmniejszego dzielnika d0 liczby n
(zakładamy, \e: n > 1 oraz, \e 2 <= d0 < n).
-Oznacza to, \e mo\emy obliczyć i0 = n / d0.
-Jeśli i0 > 1 to mo\emy znalezć kolejny najmniejszy dzielnik d1 liczby i0
-Oznacza to, \e mo\emy obliczyć i1 = i0 / d1.
-Jeśli i1 > 1 to mo\emy znalezć kolejny najmniejszy dzielnik d2 liczby i1
-Oznacza to, \e mo\emy obliczyć i2 = i1 / d2.
-Jeśli i2 > 1 to mo\emy znalezć kolejny najmniejszy dzielnik d3 liczby i2
-Oznacza to, \e mo\emy obliczyć i3 = i2 / d3. ... i tak dalej ... .
-Mo\na wykazać, \e istnieje m taki, \e im = 1.
-Oznacza to, \e mo\emy znalezć skończony ciąg liczb naturalnych:
i0 = n / d0 , i1 = i0 / d1 , i2 = i1 / d2 , i3 = i2 / d3 ,..., im = im-1 / dm = 1
tzn.
n = d0 i0 , i0 = d1 i1 , i1 = d2 i2 , i2 = d3 i3 ,.., im-1 = dm im = dm
i po podstawieniach od ostatniej równości do pierwszej,
łatwo stwierdzamy, \e obliczone dzielniki: d0 , d1 , d2 , d3 , ... , dm
są poszukiwanymi czynnikami pierwszymi liczby n tzn.
n = d0 d1 d2 d3 ... dm .
void factor(long n)
{
long i;
long d = smallDiv(n);
cout<for(i = n / d ; i > 1 ; i /= d)
{
... tu właśnie obliczamy
d = smallDiv(i);
i0 = n / d0 , i1 = i0 / d1 , i2 = i1 / d2 ,
cout<<"x"<i3 = i2 / d3 ,..., im = im-1 / dm = 1
}
}
wyra\enie_testujące
instrukcja_kroku
wyra\enie_inicjalizujące
" Dodatkowo, w ostatniej instrukcji pętli, po ka\dym jej kroku i przed
wyświetleniem kolejnego dzielnika, poprzedzamy go znakiem x
oznaczającym mno\enie.
void main()
" Wywołanie funkcji factor(n) w
{
funkcji main() dla n=6934096 .
long n=6934096L;
factor(n);
}
Przekazywanie argumentów do funkcji
" Jest rzeczą niezmiernie wa\ną, aby zrozumieć mechanizm
przekazywania argumentów do funkcji.
" Argumenty przekazywane do funkcji w momencie jej wywołania
powinny być tego samego typu co w definicji funkcji
" Jeśli tak nie jest (jest to dopuszczalne w niektórych przypadkach)
to wtedy, jeśli jest to mo\liwe, następuje automatyczne
przekonwertowanie typu przekazywanego argumentu do typu
zadeklarowanego w prototypie funkcji (najczęściej z ostrze\eniem
kompilatora w postaci warning)
" Jeśli takie przekonwertowanie (z ró\nych powodów) nie jest mo\liwe
oznacza to błąd w programie, który jest wyświetlany w postaci error
" Generalnie istnieje tylko jeden mechanizm w języku C++
przekazywania parametrów do funkcji, który polega na
przekazywaniu przez wartość (pass-by-value) (mechanizm ten
będziemy za chwilę dokładnie omawiać).
" Od razu nale\y podkreślić, \e jest to opinia purystów językowych,
bowiem wszystko, cokolwiek przekazujemy do funkcji jest zawsze
w języku C++ przekazywane przez wartość (choć autorzy wielu
podręczników traktują mechanizm przekazywania parametrów
przez tzw. referencję jako całkowicie inny typ mechanizmu).
" Jednak ze względu na mo\liwe chwyty programowe , które w
efekcie pozwalają nam realizować jak gdyby inne warianty
przekazywania parametrów do funkcji, wprowadza się jeszcze nazwy
dla dwóch dodatkowych mechanizmów: wspomniane wy\ej
przekazywanie przez referencję i przekazywanie przez adres
wskaznik (będziemy je omawiać w dalszej części wykładu).
Mechanizm przekazywania przez wartość
" W mechanizmie tym, zmienne lub stałe, które specyfikujemy jako
argumenty funkcji, nie są, tak naprawdę, wcale przekazywane do
funkcji.
" Zamiast tych argumentów przekazywane są bowiem tylko kopie ich
wartości, tworzone w momencie wywoływania funkcji.
" Poka\emy to na przykładzie funkcji power(double x, int n):
double power(double x, int n)
{
double result = 1.0;
for(int i = 1; i <= n ; i++)
result *= x;
return result;
}
void main()
{
double val=10;
int ind=2;
" Tymczasowe kopie argumentów val ,
double res=power(val, ind);
ind są tworzone w pewnym specjalnym
}
obszarze pamięci operacyjnej
zwanym stosem, do wykorzystania
double power(double x, int n)
przez funkcję.
{
" Oznacza to, \e oryginalne argumenty
double p = 1 ;
val i ind nie są dostępne wewnątrz
for(int i = 1; i <= n ; i++)
funkcji.
p *= x;
" W czasie wykonywania kodu funkcji,
return p;
wszystkie odniesienia do parametrów
}
x, n będą automatycznie kierowane
do tych tymczasowych kopii.
" Kopie x i n przestaną istnieć po
wykonaniu się kodu funkcji (są usuwane
ze stosu).
#include
" Przykład ten ilustruje fakt,
using namespace std;
\e oryginalna wartość
int passByValue(int a)
zmiennej i zdefiniowanej
{
cout<w funkcji main() i
cout<przekazanej przez wartość
a++;
w funkcji passByValue(i);
cout<return a;
pozostanie niezmieniona.
}
" Inkrementacja jest
void main()
dokonywana bowiem
{
int i=0;
tylko na kopii i.
cout<cout<int j = passByValue(i);
cout<cout<} Ró\ne
adresy !
" Podsumowując, mechanizm ten przekazywania przez wartość
w maksymalnym stopniu chroni pierwotną zawartość zmiennych
przekazanych jako argumenty do funkcji.
" Są jednak sytuację, w których zale\ałoby nam właśnie na tym, aby
wartość przekazanego do funkcji argumentu lub argumentów została
faktycznie zmieniona, w wyniku wykonania wewnątrz funkcji kodu
na tych zmiennych.
" Czy jest to mo\liwe ?
" Odpowiedz brzmi: TAK, jest to mo\liwe.
Wskazniki jako argumenty funkcji
" Jeśli przeka\emy do funkcji wskaznik adres jakiejś zmiennej
zdefiniowanej w programie, to oczywiście mechanizm przekazywania
przez wartość ciągle obowiązuje jak poprzednio nie da się zmienić
wartości tego wskaznika (czyli adresu zmiennej), bo wewnątrz
funkcji jest, jak wiemy, tworzona na stosie kopia tego adresu, a po
wyjściu z funkcji jest ona usuwana ze stosu (i ślad po niej ginie).
" Co się zatem da zmienić ?
" Odpowiedz brzmi: zmienić się da wartość zmiennej
przechowywanej pod tym adresem.
#include
" Przykład ten ilustruje
using namespace std;
sposób przekazania do
int passByPointer(int* a)
{ funkcji adresu &i
cout<istniejącej zmiennej i
cout<jako parametru, zamiast
(*a)++;
cout<return *a;
" Definicja funkcji
}
zmieniła się w ten
void main()
{ sposób, \e jej
int i=0;
parametrem jest teraz
cout<int*, wskaznik na int,
cout<a nie int.
int j = passByPointer(&i);
cout<cout<}
#include
" Po uruchomieniu
using namespace std;
programu otrzymamy
int passByPointer(int* a)
wynik podobny jak ni\ej.
{
cout<cout<(*a)++;
cout<return *a;
}
void main()
{
int i=0;
cout<cout<int j = passByPointer(&i);
cout<cout<adres przekazany
}
&
adres odebrany
#include
using namespace std;
" W kodzie funkcji
int passByPointer(int* a)
passByPointer(),
{
cout<zarówno w instrukcji
cout<inkrementacji jak i
(*a)++;
powrotu return
cout<return *a;
musimy u\yć operator
}
wyłuskania, w celu
void main()
operowania na wartości
{
int i=0;
przechowywanej pod
cout<przekazanym adresem.
cout<int j = passByPointer(&i);
cout<cout<}
int passByPointer(int* a)
{
cout<cout<(*a)++;
cout<return *a;
}
" Zauwa\my, \e instrukcji (*a)++ nie mo\na zastąpić instrukcją
*a++ z powodu prawostronnej łączności operatorów *, ++ i ich
równego priorytetu.
" Instrukcja:
*a++
jest równowa\na instrukcji
*(a++)
w której najpierw jest inkrementowany jest adres (a = a + 1) a
następnie operator wyłuskania *, powodując efekt sięgnięcia do
zawartości komórki w pamięci o adresie a + 1 innym ni\ adres a
... i efekt byłby mniej więcej taki ...
Przekazywanie tablic do funkcji
" Mo\emy tak\e przekazywać do funkcji jako parametr tablicę, przy
czym w takim przypadku, tablica nie jest kopiowana, mimo, \e jak
jak wiemy obowiązuje zasada przekazywania parametrów przez
wartość.
" Zatem dlaczego tak się dzieje ?
" Odpowiedz jest prosta: dzieje się tak, poniewa\ nazwa tablicy jest w
momencie przekazywania jej do funkcji zamieniana na adres jej
pierwszego elementu i tylko ten adres jest kopiowany.
" Tak naprawdę, to nie ma w tym przypadku potrzeby dokonywania
przez kompilator a\ takiej zamiany, bo jak pamiętamy, nazwa tablicy
identyfikuje właśnie adres jej pierwszego elementu, zatem przekazując
tablicę do funkcji jako parametr, przekazujemy tak naprawdę adres
jej pierwszego elementu.
" Zauwa\my, \e jest to bardzo dobre rozwiązanie w przypadku wielkich
tablic kopiowanie wszystkich jej elementów byłoby bardzo
czasochłonne.
" Zilustrujmy przekazywanie tablicy jako parametru funkcji, której
zadaniem będzie znalezienie największego elementu tablicy i zamiana
znaków wszystkich jej składowych na przeciwny.
#include
przekazywanie tablicy jako wskaznik ...
using namespace std;
double passArray(double* a,int dim)
{
...i jej wymiaru
double max=a[0];
int i;
for(i=1;iznajdowanie jej maksymalnego elementu
if(a[i]>max)
max=a[i];
for(i=0;izmiana znaków wszystkich jej składowych
a[i]=-a[i];
return max;
}
#include
using namespace std;
double passArray(double* a,int dim)
{
void main()
double max=a[0];
{
int i;
int i,DIM=10;
for(i = 1 ; i < dim ; i++)
double* v=new double[DIM];
if(a[i] > max)
for(i = 0 ; i < DIM ; i++)
max = a[i];
{
for(i = 0 ; i < dim ; i++)
v[i] = (i+1) / 10.0;
a[i] = -a[i];
cout<return max;
}
}
cout<for(i = 0 ; i < DIM ; i++)
cout<delete [] v;
}
Przekazywanie tablic wielowymiarowych jako parametr funkcji
" Przekazywanie tablic wielowymiarowych jest równie proste jak tablic
jednowymiarowych.
void passMatrix(double** a,int row,int col)
" W funkcji passMatrix()
{
inicjalizujemy wszystkie
int i,j;
składowe dwuwymiarowej
for(i=0;ifor(j=0;jmacierzy zerami 0 i w przypadku
a[i][j]=0;
tablicy kwadratowej, dodatkowo
if(row==col)
inicjalizujemy wszystkie jej
for(i=0;ia[i][i]=1;
elementy na przekątnej
}
jedynkami 1.
void passMatrix(double** a,int row,int col)
{
void main()
int i,j;
{
for(i=0;iint i,j;
for(j=0;jint m=3;
a[i][j]=0;
int n=3;
if(row==col)
double** mat;
for(i=0;imat=new double*[m];
a[i][i]=1;
for(i=0;i}
mat[i]=new double[n];
passMatrix(mat,m,n);
for(i=0;ifor(j=0;jcout<<for(i=0;idelete [] mat[i];
delete [] mat;
}
Referencje jako argumenty funkcji
" Deklarując parametr funkcji jako referencję, zmieniamy w bardzo
podobny sposób jak poprzednio przy u\yciu wskaznika, efekt
przekazywania argumentów do funkcji.
" Metodę tą mo\na uznać za przeciwieństwo mechanizmu
przekazywania parametru przez wartość, w którym to następuje
kopiowanie argumentu w momencie wywołania funkcji.
" Parametrem przekazywanym jest referencja do zmiennej
(pass-by-reference), która pełni rolę aliasu - przezwiska właściwej
zmiennej.
" Zabieg ten ma efekt mechanizmu przekazywania przez adres, ale
całkowicie eliminuje u\ywanie operatora wyłuskania.
" Innymi słowy mo\na stwierdzić, \e w mechanizmie przekazywania
argumentu przez referencję, u\ywany jest niejawnie mechanizm
przekazywania przez wskaznik, w którym zwalnia się programistę od
jawnego u\ywania operatora wyłuskania co znacznie upraszcza kod
i czyni go bardziej czytelnym.
#include
" Przekazywany parametr
using namespace std;
jest zadeklarowany jako
int passByReference(int& a)
int& a referencja
{
cout<do zmiennej typu int.
a++;
cout<return a;
}
void main()
{
int i=0;
cout<int j = passByReference(i);
cout<cout<}
#include
" Wywołanie funkcji,
using namespace std;
w której przekazywana
int passByReference(int& a)
jest referencja jako
{
cout<argument nie ró\ni się
a++;
od wywołania funkcji,
cout<w której przekazywanie
return a;
}
argumentów odbywa się
void main()
przez wartość.
{
int i=0;
cout<int j = passByReference(i);
cout<cout<}
#include
using namespace std;
int passByReference(int& a)
{
cout<a++;
cout<return a;
}
void main()
{
int i=0;
cout<int j = passByReference(i);
cout<cout<}
" Po uruchomieniu
programu otrzymamy...
Zwracanie wartości z funkcji
" We wszystkich dotychczasowych przykładach, funkcja zwracała (jeśli
w ogóle) pojedynczą wartość zmiennej określonego typu.
" Czy mo\na zwrócić coś innego ni\ pojedynczą wartość ?
" Odpowiedz: w zasadzie NIE, ale...pamiętajmy, \e zwracana wartość
wcale nie musi być typu numerycznego jak: double, int, long. Równie
dobrze mo\emy zwracać wskazniki do tych zmiennych, a skoro
wskazniki mogą reprezentować tablice, to wynika stąd, \e mo\emy
zwracać z funkcji tak\e i tablicę.
" Co więcej, stosując technikę programowania zorientowanego
obiektowo, mo\emy zwracać tak\e obiekty własnych typów przez nas
definiowanych a nawet tablice tych obiektów.
" Kluczem do zwracania tablic, będzie zatem, poprawne posługiwanie
się mechanizmem zwracania wskazników (adresów) oraz referencji.
" O ile jednak, zwracanie wartości zwykłych typów nie wymaga jakiś
specjalnych komentarzy, to przy zwracaniu adresów lub referencji
nale\y być bardzo ostro\nym, poniewa\ nie jest trudno napisać złą
funkcję, w której błąd wcale nie musi się ujawnić od razu.
Zwracanie wskaznika
" Generalnie, zwracanie wskaznika jest bardzo proste.
" Wskaznik jest adresem, a zatem jeśli chcemy zwrócić adres jakiejś
zmiennej, np. value, robimy to po prostu tak:
return &value;
" Ale UWAGA ! Nie na tym polega problem ze zwracaniem
wskazników.
" Załó\my, \e zmienna value jest typu double, a funkcja ma nazwę
returnAddress(...) i zwraca adres tej zmiennej w instrukcji:
return &value jak to pokazano poni\ej:
" Funkcja main() wywołuje funkcję
#include
returnAddress(...) i przechowuje
using namespace std;
zwracany adres w zmiennej ptr, która
double* returnAddress(double data)
{ powinna wskazywać na wartość jaką
double value=data*data;
jest druga potęga argumentu d.
return &value;
" Wyświetlamy następnie argument d,
}
a za nim to co jest aktualnie
void main()
przechowywane pod adresem ptr.
{
double d=1; " Otrzymamy coś w tym stylu ...
double* ptr = 0;
ptr = returnAddress(d);
cout<cout<... co nie jest oczywiście zgodne z
}
naszymi oczekiwaniami ???
#include
" Wskazówką, która w zasadzie wyjaśnia
using namespace std;
przyczynę nieoczekiwanego wyniku,
double* returnAddress(double data)
jest ostrze\enie kompilatora:
{
warning C4172: returning address of
double value=data*data;
local variable or temporary.
return &value;
}
" Błąd pojawia się poniewa\ zmienna
void main()
value w funkcji returnAddress(...)
{
jest tworzona w momencie wywołania
double d=1;
funkcji i jest niszczona po wyjściu z
double* ptr = 0;
funkcji a więc obszar pamięci
ptr = returnAddress(d);
cout<pierwotnie zarezerwowany dla
cout<przechowywania tej zmiennej, mo\e stać
}
się i staje się, jak to widać, w naszym
przykładzie miejscem do
przechowywania innych zmiennych,
które są u\ywane przez program.
śelazna reguła zwracania adresu
śelazna reguła zwracania adresu
" Zastanówmy się w takim razie co nale\ałoby poprawić, aby zwracanie
adresu było poprawne.
" Odpowiedzi nale\y szukać w dynamicznej alokacji pamięci.
" Za pomocą operatora new, mo\emy utworzyć nową zmienną na
stercie, która będzie istnieć dopóty dopóki jej sami nie usuniemy za
pomocą operatora delete lub dopóki zakończy się wykonywanie
programu.
" Zmodyfikowana funkcja mogłaby wyglądać na przykład w ten sposób:
double* returnPointer(double data)
" Zamiast deklaracji value typu
{
double, deklarujemy value jako
double* value=new double(data);
double* i przechowywujemy w
*value =data*data;
niej adres zwrócony przez
return value;
operator new.
}
#include
" Poniewa\ zwracanym z funkcji
using namespace std;
wynikiem ma być wskaznik value,
double* returnPointer(double data)
nieznacznie modyfikujemy
{
pozostałe instrukcję funkcji.
double* value=new double(data);
*value=data*data; " Pamiętajmy jednak, \e ka\de
return value;
wywołanie funkcji powodować
}
będzie kolejne alokowanie pamięci
void main()
na stercie.
{
" Aby móc przed zakończeniem
double d=1;
double* ptr = 0; programu, zwolnić niepotrzebną
ptr = returnPointer(d);
nam pamięć, nale\y poprawnie
cout<zastosować operator delete.
cout<delete ptr;
}
" W przypadku dłu\szych programów i alokowanych na stercie
większych obiektów, poprawne zastosowanie operatora delete
mo\e mieć kluczowe znaczenie w zagwarantowaniu optymalnego
wykorzystania dostępnych zasobów pamięciowych.
Zwracanie referencji
" Mo\emy tak\e zwrócić z funkcji referencję.
" Nale\y jednak zachować podobne środki ostro\ności jak w przypadku
zwracania wskazników z funkcji.
" Pamiętajmy bowiem, \e referencja istnieje jeśli istnieje zmienna
(ogólnie obiekt), której jest przezwiskiem .
" Zwracanie referencji ma pierwszorzędne znaczenie w programowaniu
zorientowanym obiektowo.
" Referencję pozwolą nam m.in. robić to co bez tego narzędzia nie
byłoby po prostu mo\liwe (np. przeładowywanie operatorów).
" Inną bardzo wa\ną własnością referencji jest to, \e przy traktowaniu
jej jako zwracanej wartości, mo\e być ona tzw. lvalue.
" Oznacza to, \e rezultat funkcji zwracającej referencję, mo\e stać po
lewej stronie operatora przypisania =.
" W kodzie programu, na pierwszy rzut oka, mo\e się to wydawać, co
najmniej dziwne, ale w wielu przypadkach, jest bardzo po\yteczne.
" W kolejnym przykładzie poka\emy funkcję, która zwraca referencję
oraz zademonstrujemy wywołanie tej funkcji, w którym stać będzie
ona po lewej stronie operatora przypisania.
" Załó\my, \e mamy tablicę zawierającą losowe wartości.
" Chcemy napisać fragment programu realizujący następujące zadanie:
ka\dorazowa zmiana wartości składowych tej tablicy ma być
dokonana w ten sposób, \e umieszczanie nowej wartości w tej tablicy
ma zastąpić składową przechowującą najmniejszą wartość.
double& lowest(double* A, int len)
{
int j=0; //Indeks najmniejszego elementu
for(int i=1 ; i < len ; i++)
if(A[j] > A[i]) //mniejsza wartość ?...
j = i; // ...jeśli tak to zmień j
return A[j]; //Zwróć referencję do najmniejszego elementu
}
double& lowest(double* A, int len)
{
" Zobaczmy najpierw jak funkcja
int j=0;
została zaimplementowana. for(int i=1 ; i < len ; i++)
if(A[j] > A[i])
" Prototyp funkcji lowest(...)
j = i;
u\ywa double& jako specyfikacji
return A[j];
zwracanego typu, tj. funkcja }
zwraca 'referencję na double '.
double& lowest(double* A, int len)
" Funkcja ma dwa parametry:
{
jednowymiarową tablicę A
int j=0;
typu double (wskaznik na double),
for(int i=1 ; i < len ; i++)
if(A[j] > A[i])
i
j = i;
parametr len typu int, który
return A[j];
specyfikuje długość tablicy A.
}
double& lowest(double* A, int len)
" W ciele funkcji mamy oczywistą
{
pętlę for, w której ustalamy element
int j=0;
tablicy zawierający najmniejszą
for(int i=1 ; i < len ; i++)
if(A[j] > A[i])
wartość.
j = i;
return A[j];
}
double& lowest(double* A, int len)
" Zmienną j , która ma docelowo
{
identyfikować element tablicy
int j=0; //Indeks najmniejszego elementu
o najmniejszej wartości,
for(int i=1 ; i < len ; i++)
if(A[j] > A[i]) //mniejsza wartość ?...
inicjalizujemy najmniejszą
j = i; // ...jeśli tak to zmień j
mo\liwą wartością 0, a
return A[j];
następnie modyfikujemy ją
}
w pętli jeśli aktualna wartości
A[i] jest mniejsza ni\ A[j].
" Po wyjściu z pętli, zmienna j
będzie zawierać indeks tablicy
zawierający najmniejszą jej
wartość.
" Instrukcja return jest następująca:
return A[j];
" Na pierwszy rzut oka wygląda ona identycznie jak instrukcja,
w której zwracana byłaby zwykła wartość A[j] typu double.
" Jednak, poniewa\ w nagłówku funkcji mamy deklaracje, \e
zwracana ma być referencja na double, tak więc zwrócona
zostanie nie wartość A[j] typu double a referencja do
elementu A[j] typu double.
double& lowest(double* A, int len)
{
int j=0;
for(int i=1 ; i < len ; i++)
if(A[j] > A[i])
j = i;
return A[j]; //Zwróć referencję do najmniejszego elementu
}
double& lowest(double* A, int len)
" Adres elementu A[j] jest u\ywany
{
niejawnie do zainicjalizowania
int j=0;
referencji, która ma być zwrócona
for(int i=1 ; i < len ; i++)
if(A[j] > A[i])
" Referencja jest tworzona przez
j = i;
kompilator, poniewa\ taki typ
return A[j];
został zadeklarowany jako
}
zwracana wartość.
double& lowest(double* A, int len)
{
int j=0;
for(int i=1 ; i < len ; i++)
if(A[j] > A[i])
j = i;
return A[j];
}
" Nie wolno w naszym przypadku
napisać return &A[j] jako
zwracanej wartości.
" Pisząc &A[j] jako zwracaną
wartość, zwracalibyśmy jawnie
wskaznik adres elementu
A[j] (czyli A + j).
" Oczywiście byłby to błąd i
program nie mógłby być
skompilowany.
#include
double& lowest(double* A, int len)
#include
{
using namespace std;
int j=0;
double& lowest(double* A, int len);
for(int i=1 ; i < len ; i++)
void main(void)
if(A[j] > A[i])
{
j = i;
double array[] = { 3.0, 10.0, 1.5, 15.0, 2.7, 23.0,
return A[j];
4.5, 12.0, 6.8, 13.5, 2.1, 14.0 };
}
//Initialize to number of elements
" Funkcja main(), testująca
int len = sizeof array/sizeof array[0];
cout << endl;
naszą funkcję lowest(...), jest
for(int i = 0; i < len; i++)
bardzo prosta.
cout << setw(6) << array[i];
" Deklarujemy tablicę typu
lowest(array, len) = 6.9;//Change lowest to 6.9
cout << endl;
double, inicjalizujemy ją
for(i = 0 ; i < len ; i++)
12 dowolnymi wartościami i
cout << setw(6) << array[i];
na koniec, zmienną len typu
lowest(array, len) = 7.9;//Change lowest to 7.9
cout << endl;
int inicjalizujemy w podany
for(i = 0 ; i < len ; i++)
sposób (będzie ona
cout << setw(6) << array[i];
przechowywać rozmiar tablicy
}
array).
#include
double& lowest(double* A, int len)
#include
{
using namespace std;
int j=0;
double& lowest(double* A, int len);
for(int i=1 ; i < len ; i++)
void main(void)
if(A[j] > A[i])
{
j = i;
double array[] = { 3.0, 10.0, 1.5, 15.0, 2.7, 23.0,
return A[j];
4.5, 12.0, 6.8, 13.5, 2.1, 14.0 };
}
//Initialize to number of elements
int len = sizeof array/sizeof array[0];
" W funkcji main() wywołujemy
cout << endl;
funkcję lowest(...) po lewej
for(int i = 0; i < len; i++)
stronie operatora przypisania,
cout << setw(6) << array[i];
lowest(array, len) = 6.9;//Change lowest to 6.9
uzyskując efekt zmiany
cout << endl;
najmniejszego elementu tablicy
for(i = 0 ; i < len ; i++)
przy wstawianiu do niej nowego
cout << setw(6) << array[i];
lowest(array, len) = 7.9;//Change lowest to 7.9
elementu i robimy to dwukrotnie
cout << endl;
for(i = 0 ; i < len ; i++)
" Po uruchomieniu programu
cout << setw(6) << array[i];
}
zobaczymy...
#include
double& lowest(double* A, int len)
#include
{
using namespace std;
int j=0;
double& lowest(double* A, int len);
for(int i=1 ; i < len ; i++)
void main(void)
if(A[j] > A[i])
{
j = i;
double array[] = { 3.0, 10.0, 1.5, 15.0, 2.7, 23.0,
return A[j];
4.5, 12.0, 6.8, 13.5, 2.1, 14.0 };
}
//Initialize to number of elements
int len = sizeof array/sizeof array[0];
" Po pierwszym wywołaniu
cout << endl;
lowest(...), trzeci element tablicy,
for(int i = 0; i < len; i++)
cout << setw(6) << array[i];
array[2], zawierający
lowest(array, len) = 6.9;//Change lowest to 6.9
najmniejszą wartość, zostanie
cout << endl;
zmieniony na 6.9.
for(i = 0 ; i < len ; i++)
cout << setw(6) << array[i];
" Podobnie po drugim wywołaniu,
lowest(array, len) = 7.9;//Change lowest to 7.9
array[10] zostanie zmieniony
cout << endl;
na 7.9.
for(i = 0 ; i < len ; i++)
cout << setw(6) << array[i];
}
...
lowest(array, len) = 6.9;//Change lowest to 6.9
...
lowest(array, len) = 7.9;//Change lowest to 7.9
...
" Mamy tutaj przykład jak mechanizm zwracania referencji pozwala
na u\ycie funkcji po lewej stronie operatora przypisania.
śelazna zasada zwracania referencji
śelazna zasada zwracania referencji
Zmienne statyczne funkcji
" Jak wiemy ju\, zmienne lokalne, tzn. tworzone na stosie i deklarowane
w ciele funkcji, przestają istnieć po wykonaniu kodu funkcji.
" Okazuje się jednak, \e mo\emy temu zapobiec, deklarując je jako
static.
" Do czego nam mo\e się to przydać ?
" Odpowiedz: na przykład wyobrazmy sobie, \e chcielibyśmy z jakiś
powodów, zliczać ile razy w programie została wywołana dana funkcja.
Jest mo\liwe oczywiście wprowadzenie globalnej zmiennej, która
następnie byłaby inkrementowana po ka\dym wywołaniu funkcji.
Nie jest to jednak eleganckie rozwiązanie problemu, bo z ró\nych
powodów, nale\y ograniczać liczbę zmiennych globalnych i tak pisać
programy, aby liczba zmiennych globalnych była najmniejsza (jeśli
w ogóle musimy je wprowadzać).
Lepszym rozwiązaniem jest właśnie zmienna statyczna.
" Wystarczy w tym celu, zadeklarować zmienną lokalną funkcji jako
static.
" Na przykład deklaracja zmiennej count jako static i jednoczesna jej
inicjalizacja zerem wyglądałaby następująco:
static int count = 0;
" Inicjalizacja zmiennej statycznej wewnątrz funkcji jest realizowana
tylko przy pierwszym wywołaniu funkcji mamy wtedy nie tylko
inicjalizację, ale tak\e jej deklarację połączoną z definicją.
" Zmiana wartości tej zmiennej w kodzie funkcji zostaje następnie
zapamiętana do następnego wywołania funkcji i nie jest ju\ wtedy
inicjalizowana (zerem w naszym przykładzie).
" Po ka\dym kolejnym wywołaniu, funkcja operuje bowiem dalej na
zmiennej, której wartość początkowa jest taka jak przy ostatnim
wywołaniu tej funkcji.
" Poni\ej mamy demonstrację u\ycia funkcji, w której zadeklarowano
zmienną statyczną counter :
#include
using namespace std;
void counterFunction()
{
static int counter=0;
cout<}
void main()
{
for(int i=0 ; i < 10 ; i++)
counterFunction();
}
Wyszukiwarka
Podobne podstrony:
AiP wyklad03
AiP wyklad01
AiP wyklad05
AiP wyklad02
Sieci komputerowe wyklady dr Furtak
Wykład 05 Opadanie i fluidyzacja
WYKŁAD 1 Wprowadzenie do biotechnologii farmaceutycznej
mo3 wykladyJJ
ZARZĄDZANIE WARTOŚCIĄ PRZEDSIĘBIORSTWA Z DNIA 26 MARZEC 2011 WYKŁAD NR 3
Wyklad 2 PNOP 08 9 zaoczne
Wyklad studport 8
Kryptografia wyklad
Budownictwo Ogolne II zaoczne wyklad 13 ppoz
więcej podobnych podstron