Funkcje - argumenty formalne i aktualne. Porównanie metod przekazywania argumentów do funkcji.

Wprowadzenie

W poprzedniej lekcji przedstawiłem Ci różne metody przekazywania argumentów do funkcji. Wiesz już dobrze w jaki sposób przekazywać argumenty do funkcji tak, aby możliwa była zmiana ich wartości.

W tej lekcji przedstawię Ci dalsze informacje na temat argumentów funkcji i przekazywania argumentów do funkcji, a także wyjaśnię kwestie związane z formalnym nazewnictwem dotyczącym funkcji. Dowiesz się o przekazywaniu typów prostych, typów pochodnych oraz typu strukturalnego.

Argumenty formalne i aktualne

Wiesz już dobrze, że w definicji i deklaracji funkcji może znajdować się lista argumentów. Ta lista argumentów nazywana jest listą argumentów formalnych natomiast argumenty znajdujące się w deklaracji funkcji nazywane są argumentami formalnymi.

Każda funkcja może być oczywiście uruchomiona z różnymi wartościami argumentów. Takie uruchomienie funkcji nazywamy wywołaniem funkcji, natomiast argumenty przekazane do funkcji w momencie jej wywołania nazywamy argumentami aktualnymi.

Spójrz zatem na poniższy program:

#include <iostream>

using namespace std;

int porownaj(int, int);


int main()
{
  int liczba1=5,liczba2=8;
   
  int wynik;
 
  wynik = porownaj(liczba1, liczba2); // wywolanie funkcji - tutaj argumenty aktualne
 
  cout <<"Wynik porownania liczb to "<<wynik<<endl;
 
  wynik = porownaj(2,2); // wywolanie funkcji - tutaj argumenty aktualne
 
  cout <<"Wynik porownania liczb to "<<wynik<<endl;

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}

int porownaj (int a, int b) // definicja funkcji - tutaj argumenty formalne
{
   if (a==b)
      return 0;
   else if (a<b)
      return -1;
   return 1;  
}
program nr 35.1

W programie znajduje się definicja jednej funkcji - funkcji porownaj. Zadaniem tej funkcji jest porównanie wartości dwóch argumentów typu int. Funkcja zwraca wartość 0 gdy oba argumenty są jednakowe, wartość -1 gdy pierwszy argument jest mniejszy od drugiego, a 1 gdy pierwszy argument jest większy od drugiego.

Przyjrzyj się komentarzom zawartym w programie - mam nadzieję, że dzięki nim jest dla Ciebie już jasne, gdzie znajdują się argumenty formalne, gdzie aktualne i w którym miejscu następuje wywołanie funkcji.

Co prawda powyższe definicje i wyjaśnienia dotyczące argumentów formalnych i aktualnych nie są Ci tak naprawdę do niczego potrzebne z punktu widzenia wiedzy praktycznej, mimo to warto, żebyś znał chociaż podstawowe nazewnictwo występujące w języku C++, bowiem pojawia się ono niekiedy w literaturze fachowej i warto wtedy wiedzieć o czym mowa. Poza tym jeśli chcesz kiedyś profesjonalnie zajmować się programowaniem, to wstydem byłoby gdybyś takich podstawowych pojęć nie znał.

Argumenty aktualne funkcji - zmienne i literały

Jeśli przyjrzysz się programom, w których do tej pory zostały przedstawione funkcje, zauważysz, że jako argumenty aktualne funkcji mogą być przekazywane zarówno zmienne, jak i literały. Okazuje się jednak, że nie dla wszystkich metod przekazywania argumentów do funkcji jest to możliwe.

Poniższy program demonstruje to zagadnienie:

#include <iostream>

using namespace std;

double poleWar(double);
double poleRef(double &);
double poleWsk(double *);


int main()
{
  double bok;    
 
  do
  {
     cout <<"Podaj poprawna wartosc boku kwadratu: ";
     cin >>bok;
     cin.ignore();      
  }  
  while (bok<=0);
 
  cout <<endl<<endl<<"Pole kwadratu o boku "<<bok<<endl;
  cout <<"przekazywanie przez wartosc: "<<poleWar(bok)<<endl; // (1)
  cout <<"przekazywanie przez referencje: "<<poleRef(bok)<<endl; // (2)
  cout <<"przekazywanie przez wskaznik: "<<poleWsk(&bok)<<endl; // (3)


  cout <<endl<<endl<<"Pole kwadratu o boku 5.5"<<endl;
  cout <<"przekazywanie przez wartosc: "<<poleWar(5.5)<<endl; // (4)
//   cout <<"przekazywanie przez referencje: "<<poleRef(5.5)<<endl; // (5)
//   cout <<"przekazywanie przez wskaznik: "<<poleWsk(5.5)<<endl;  // (6)


  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}

double poleWar(double a)
{                        
  return a * a;
}

double poleRef(double &a)
{                
  return a * a;
}
double poleWsk(double *a)
{
  return *a * *a;
}
program nr 35.2

W programie znajdują się 3 funkcje - zadaniem każdej z nich jest obliczenie pola kwadratu o długości boku przekazanego jako argument. Każda z funkcji przyjmuje jako argument właśnie długość boku kwadratu, a także zwraca wynik obliczeń. Funkcje przyjmują argumenty za pomocą różnych metod - przez wartość, przez referencję i przez wskaźnik.

Jak możesz łatwo zaobserwować w liniach opatrzonych komentarzami (1), (2) i (3) wywoływane są wspomniane funkcje, a jako argument przekazywana jest zmienna. Jak zauważysz, nie pojawiają się tutaj zupełnie żadne problemy - wszystko działa jak należy - funkcje pobierają argumenty i dokonują obliczeń.

Zupełnie inaczej wygląda sytuacja w liniach opatrzonych komentarzami (4), (5) i (6) - w tych liniach jako argumenty funkcji przekazywane są literały. Podczas przekazywania literału jako argumentu funkcji, która przyjmuje argument przez wartość nie ma najmniejszego problemu. Inaczej jest dla funkcji, które przyjmują argument przez referencję lub przez wskaźnik - zauważ, że linie (5) i (6) zostały całkowicie wykomentowane - jeśli usuniesz komentarz znajdujący się na początku którejś z tych linii, program się nie skompiluje.

Dlaczego pojawiają się problemy podczas przekazania literału do funkcji przyjmującej argumenty przez referencję lub wskaźnik? Jeśli przypomnisz sobie wszystkie informacje na temat referencji i wskaźnika, które Ci do tej pory przedstawiłem oraz przypomnisz sobie, w jaki sposób należy zapamiętać dlaczego wywołania i deklaracje funkcji przyjmujących argumenty przez referencję lub przez wskaźnik wyglądają tak a nie inaczej (wyjaśnione w poprzedniej lekcji), powinno udać Ci się zrozumieć, dlaczego pojawiają się problemy.

Prześledźmy zatem najpierw funkcję, która przyjmuje argument przez referencję. Analizując potencjalne miejsce, które może powodować błąd, otrzymujemy następującą inicjalizację:

double &a = 5.5;

Mam nadzieję, że pamiętasz, że referencja wskazuje zawsze na pewne miejsce, w którym znajduje się inna zmienna. A czy tutaj mamy gdzieś zmienną, z którą jest połączona referencja? Nie - referencję chcemy powiązać z literałem. To miejsce właśnie powoduje problem (jeśli nadal nie wiesz dlaczego przypomnij sobie lekcję na temat referencji). Czy jest zatem jakiś sposób na rozwiązanie tego problemu? Tak - wystarczy, że zmienimy deklarację i definicję funkcji tak, aby zasygnalizować, że referencja będzie referencją do stałej:

double poleRef(const double &a)

W taki sposób możemy sprawić, że funkcja będzie mogła przyjmować argument przez referencję i jednocześnie będzie mogła przyjąć jako argument literał. W ten jednak sposób ograniczamy mocno możliwość takiej funkcji - zmiana wartości argumentu wewnątrz funkcji nie będzie możliwa, bo będzie zastosowany modyfikator const.

Wróćmy teraz do funkcji, która przyjmuje argument przez wskaźnik. Analizując potencjalne miejsce, które może powodować błąd, otrzymujemy następującą inicjalizację:

double *a = 5.5;

Czy wiesz gdzie jest błąd? Jeśli nie, wróć do lekcji o wskaźnikach, a zwłaszcza do programu nr 29.4 i wyjaśnień zawartych dla tego właśnie programu. Wskaźniki służą do wskazywania na zmienne i jeśli wskaźnik nie jest ustawiony do pokazywania na daną zmienną, nie należy mu przypisywać żadnej wartości liczbowej (poza wartością 0, która symbolizuje nieustawiony wskaźnik - patrz program nr 29.5).

W naszym przykładowym programie próbujemy właśnie przekazać jako argument funkcji literał i nim zainicjalizować wskaźnik. Jak już dobrze wiesz (po przypomnieniu), nie można w ten sposób wykorzystać wskaźnika. Czy jest jakiś sposób, aby zaradzić takiej sytuacji tzn. przekazać literał do funkcji przyjmującej argument przez wskaźnik? Nie - nie ma takiej możliwości. W przypadku wskaźników nie pomoże żadna sztuczka z modyfikatorem const tak jak to zrobiliśmy dla referencji - nie jest możliwe przekazanie literału do funkcji przyjmującej argument przez wskaźnik .

Jak zatem widzisz, przekazywanie argumentów do funkcji nie zawsze jest takie proste i oczywiste. Wiesz już teraz, że literały przekażesz bez problemów do funkcji przyjmujących argumenty przez wartość, przy użyciu modyfikatora const możesz przekazać literały do funkcji przyjmujących argumenty przez referencję (ale tym samym ograniczysz mocno możliwości referencji), natomiast nie jest możliwe przekazanie literału do funkcji przyjmującej argumenty przez wskaźnik. Przekazywanie zmiennych jest za to możliwe bez problemu dla wszystkich metod przyjmowania argumentów przez funkcję (przez wartość, przez referencję i przez wskaźnik).

Argumenty aktualne funkcji - typy pochodne

Do tej pory jako argumenty do funkcji były przesyłane literały albo zmienne typów prostych. Okazuje się jednak, że możliwe jest również przesyłanie zmiennych typów pochodnych (wskaźnik, referencja). Może zatem dość do sytuacji, w której wskaźnik jest wykorzystywany do przekazania argumentu funkcji przyjmującej argument przez wartość, a referencja jest wykorzystywana do przekazania argumentu do funkcji przyjmującej argument przez wskaźnik. Tego typu zagadnienie obrazuje poniższy program:

#include <iostream>

using namespace std;

double poleWar(double);
double poleRef(double &);
double poleWsk(double *);
void info(double &, double, double, double);
void stop();


int main()
{
  double bok, bokKopia;    
 
  double &bokRef = bokKopia;
 
  double *bokWsk = &bokKopia;
 
 
  do
  {
     cout <<"Podaj poprawna wartosc boku kwadratu: ";
     cin >>bok;
     cin.ignore();      
  }  
  while (bok<=0);
 
  bokKopia=bok;
 
  cout <<endl<<endl<<"Pole kwadratu o boku "<<bok<<endl;
 
  cout <<endl<<"Metoda przekazywania przez wartosc"<<endl;
  info(bokKopia, bokRef, *bokWsk, bok);
  cout <<"przekazywanie przez wartosc (typ prosty): "<<poleWar(bokKopia)<<endl;
  info(bokKopia, bokRef, *bokWsk, bok);
  cout <<"przekazywanie przez wartosc (referencja): "<<poleWar(bokRef)<<endl;
  info(bokKopia, bokRef, *bokWsk, bok);  
  cout <<"przekazywanie przez wartosc (wskaznik): "<<poleWar(*bokWsk)<<endl;  
  info(bokKopia, bokRef, *bokWsk, bok);
  stop();
 
  bokKopia = bok; // ewentualne przywrocenie poczatkowej wartosci
 
  cout <<endl<<"Metoda przekazywania przez referencje"<<endl;
  info(bokKopia, bokRef, *bokWsk, bok);
  cout <<"przekazywanie przez referencje (typ prosty): "<<poleRef(bokKopia)<<endl;
  info(bokKopia, bokRef, *bokWsk, bok);
  cout <<"przekazywanie przez referencje (referencja): "<<poleRef(bokRef)<<endl;
  info(bokKopia, bokRef, *bokWsk, bok);
  cout <<"przekazywanie przez referencje (wskaznik): "<<poleRef(*bokWsk)<<endl;  
  info(bokKopia, bokRef, *bokWsk, bok);
  stop();  

  bokKopia = bok; // ewentualne przywrocenie poczatkowej wartosci
 
  cout <<endl<<"Metoda przekazywania przez wskaznik"<<endl;
  info(bokKopia, bokRef, *bokWsk, bok);  
  cout <<"przekazywanie przez wskaznik (typ prosty): "<<poleWsk(&bokKopia)<<endl;
  info(bokKopia, bokRef, *bokWsk, bok);  
  cout <<"przekazywanie przez wskaznik (referencja): "<<poleWsk(&bokRef)<<endl;
  info(bokKopia, bokRef, *bokWsk, bok);  
  cout <<"przekazywanie przez wskaznik (referencja): "<<poleWsk(bokWsk)<<endl;      
  info(bokKopia, bokRef, *bokWsk, bok);  

  stop();
  return 0;  
}

double poleWar(double a)
{  
  a = a*a;                      
  return a;
}

double poleRef(double &a)
{                
  a = a*a;                      
  return a;              
}
double poleWsk(double *a)
{
  *a = *a * *a;
  return *a;
}

void info(double &war1, double war2, double war3, double org)
{
  cout <<"bokKopia: "<<war1<<" bokRef: "<<war2<<" bokWsk: "<<war3<<endl;    
  war1 = org;
}
void stop()
{
  cout <<endl<<"Nacisnij ENTER aby kontynuowac"<<endl;
  getchar();    
}
program nr 35.3

Głównym elementem programu są funkcje poleWar, poleRef i poleWsk - funkcje te mają za zadanie obliczyć pole kwadratu o boku zadanym jako argument wywołania funkcji. Każda z funkcji ma przypisać wartość pola argumentowi, a następnie dodatkowo zwrócić wartość pola (czyli wartość argumentu) - porównaj z funkcjami z programu nr 35.2.

Dodatkowo zadeklarowane są funkcje info i stop. Funkcja info ma za zadanie wypisać aktualną wartość trzech pierwszych argumentów, a także przypisać wartości pierwszego argumentu wartość ostatniego argumentu (zwróć uwagę, że pierwszy argument został przekazany przez referencję). Z kolei funkcja stop ma za zadanie wyłącznie chwilowe zatrzymanie dalszego wykonania programu.

W funkcji main deklarujemy dwie zmienne typu double, a także referencję i wskaźnik. Referencję i wskaźnik wiążemy ze zmienną bokKopia. Następnie pobieramy wartość zmiennej bok ze standardowego wejścia i przypisujemy zmiennej bokKopia wartość zmiennej bok.

Po co wprowadziliśmy zmienną bokKopia i czemu referencja i wskaźnik powiązane są właśnie z tą zmienną? Ponieważ w programie będziemy korzystać z przekazywania przez wskaźnik i przez referencję, wiemy, że wartość argumentu może zostać zmieniona. My jednak chcemy wywoływać funkcję zawsze z taką samą wartością argumentów - w tym celu wykorzystaliśmy właśnie funkcję info, która oprócz wypisania wartości, dodatkowo powoduje odwrócenie wszystkich zmian jakie wykonały funkcje. Zatem zmienna bok przechowuje w naszym programie zawsze oryginalną wartość, a zmieniać będziemy tylko wartość zmiennej bokKopia, zarówno bezpośrednio, jak również przez wskaźnik i przez referencję, które są powiązane właśnie z tą zmienną.

Po pobraniu wartości boku kwadratu i wykonaniu początkowych czynności w funkcji main są następnie wywoływane funkcje. Od razu powinieneś zauważyć, że funkcje są wywoływane nie tylko z argumentem bokKopia, ale również jako argument wywołania jest wykorzystywana referencja, czyli bokRef i wskaźnik, czyli bokWsk. Jak zatem widzisz, metoda przekazywania argumentów do funkcji nie wymusza skorzystania z konkretnego typu - do funkcji przyjmującej argument przez wartość wykorzystujemy również wskaźnik i referencję.

Na początku wywołujemy funkcję przyjmującą argument przez wartość. Ponieważ dobrze już wiesz, w jaki sposób pobrać wartość zmiennej, z którą powiązana jest referencja czy wartość zmiennej na którą wskazuje wskaźnik, sposób przekazania argumentów do funkcji nie powinien Ciebie zdziwić. Nie powinno Ciebie zdziwić również to, że po wywołaniu funkcji wszystkie wypisywane wartości są niezmienione - oczywiście ponieważ przekazujemy argumenty przez wartość (mimo, że używamy wskaźnika czy referencji), to nie jest możliwa zmiana wartości argumentu przekazanego do funkcji.

Następnie wywołujemy funkcję przyjmującą argument przez referencję. Wiesz, że wywołanie funkcji przyjmującej argumenty przez referencję wygląda tak samo jak wywołanie funkcji przyjmującej argumenty przez wartość, więc nie ma tutaj żadnej nowości. Nowością są za to wartości zmiennych wypisanych przez funkcję info - jak widzisz po wywołaniu funkcji wartości ulegają zmianie. Dlaczego zmieniają się wszystkie wartości a nie tylko wartość zmiennej, która została przekazana do funkcji jako argument wywołania? Odpowiedź jest prosta - zarówno referencja, jak i wskaźnik wskazują na tę samą zmienną i niezależnie czy zmienimy wartość zmiennej bezpośrednio, czy przez referencję, czy przez wskaźnik - wartość zmiennej zostanie zmieniona, a tym samym jeśli wartość tej zmiennej zostanie wypisana przez referencję czy przez wskaźnik, również zobaczysz nową, aktualną wartość zmiennej.

Na końcu wywołujemy funkcję przyjmującą argument przez wskaźnik. Zmiany wartości zachodzą tak samo jak w przypadku funkcji przyjmującej argument przez referencję, więc nie ma tutaj nic nowego. Nowością jednak w pierwszym momencie może być dla Ciebie sposób przekazania argumentów do funkcji. Zauważ jednak, że tak naprawdę nie ma tu żadnych nowości. To jak przekazujemy zmienną do funkcji przyjmującej argument przez wskaźnik przedstawiłem Ci w poprzedniej lekcji. Ponieważ referencją posługujemy się tak jak zwykłą zmienną, to przekazanie referencji do funkcji przyjmującej argument przez wskaźnik wygląda tak samo jak przekazanie zwykłej zmiennej. Natomiast w przypadku wskaźnika musimy przekazać adres zmiennej, a jak wiemy to sam wskaźnik przechowuje adres zmiennej (czyli nie musimy skorzystać tutaj z operatora pobrania adresu &). Mam nadzieję, że jest dla Ciebie oczywiste dlaczego w tym przypadku nie używamy również operatora wyłuskania * - jeśli nie, to wróć do lekcji o wskaźnikach albo do lekcji podsumowującej wskaźniki, referencje i typy proste (lekcja 30).

Mam nadzieję, że teraz już zdajesz sobie sprawę, że sposób przekazywania argumentu do funkcji nie jest powiązany z typem jakiego używamy jako argumentu funkcji. Jak się właśnie przekonałeś dla wszystkich metod przekazywania argumentów do funkcji (przez wartość, przez referencję i przez wskaźnik) możemy wykorzystać zarówno zmienne typu prostego, jak i zmienne typu pochodnego (referencja, wskaźnik) jako argumenty tych funkcji. Pamiętaj jednak - trzeba być ostrożnym podczas tego typu operacji i być świadomym konsekwencji takiego działania.

Która metoda przekazywania argumentów do funkcji jest najlepsza?

Jeśli oczekujesz gotowej odpowiedzi na pytanie, która metoda przekazywania argumentów do funkcji jest najlepsza, to nie oczekuj, że odpowiem Ci bezpośrednio na to pytanie. Tak naprawdę, każda z metod ma jakieś swoje zalety.

Wiesz już, że jeśli chcesz umożliwić zmianę argumentów wewnątrz funkcji, powinieneś wykorzystać metodę przekazywania argumentów przez referencję lub przez wskaźnik. Z drugiej strony wiesz również, że jeśli chcesz do funkcji przekazywać literały, najrozsądniejsze jest wykorzystanie przekazywania przez wartość.

Nie wiesz jednak jeszcze tak dobrze o tym, że przekazywanie argumentów do funkcji może w znacznym stopniu wpłynąć na wydajność programu i na ilość pamięci, jaka jest potrzebna do prawidłowego działania programu.

Spójrz zatem na poniższy przykładowy program:

#include <iostream>
#include <cstring>

using namespace std;

struct Osoba
{
  string imie;
  string nazwisko;
  int wiek;              
};


void infoWar(Osoba);
void infoRef(Osoba &);
void infoWsk(Osoba *);


int main()
{  
  struct Osoba ja;
 
  ja.imie="Marcin";
  ja.nazwisko="Nabialek";
  ja.wiek=26;

  infoWar(ja);
  infoRef(ja);
  infoWsk(&ja);

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}

void infoWar(Osoba o)
{
   cout <<"Wykorzystuje dodatkowe "<<sizeof(Osoba)<<" bajtow pamieci"<<endl;    
   cout <<"Imie: "<<o.imie<<endl
        <<"Nazwisko: "<<o.nazwisko<<endl
        <<"Wiek: "<<o.wiek<<endl<<endl;
}    
   
void infoRef(Osoba &o)
{
//    cout <<"Wykorzystuje dodatkowe "<<sizeof(Osoba &)<<" bajtow pamieci"<<endl;    
   cout <<"Imie: "<<o.imie<<endl
        <<"Nazwisko: "<<o.nazwisko<<endl
        <<"Wiek: "<<o.wiek<<endl<<endl;
}      

void infoWsk(Osoba *o)
{
   cout <<"Wykorzystuje dodatkowe "<<sizeof(Osoba *)<<" bajtow pamieci"<<endl;    
   cout <<"Imie: "<<(*o).imie<<endl
        <<"Nazwisko: "<<(*o).nazwisko<<endl
        <<"Wiek: "<<(*o).wiek<<endl<<endl;    
}
program nr 35.4

Tym razem do funkcji nie została przekazana ani zmienna typu prostego ani zmienna typu pochodnego, lecz zmienna typu strukturalnego. Zauważ, że struktura Osoba została zdefiniowana poza funkcją main tak, aby była w zasięgu globalnym - czy wiesz dlaczego? Otóż definicja struktury została umieszczona w zasięgu globalnym po to, aby pozostałe funkcje zadeklarowane w programie znały zdefiniowany przez nas typ strukturalny Osoba - w przeciwnym przypadku nie byłoby możliwe przekazanie do tych funkcji zmiennej typu strukturalnego.

Nas jednak interesują 3 funkcje zadeklarowane i zdefiniowane w programie - są to funkcje infoWar, infoRef i infoWsk- wszystkie funkcje mają za zadanie wyświetlenie składowych struktury oraz ewentualnie wyświetlenie informacje o dodatkowo wykorzystanej pamięci.

W funkcji main tworzony jest obiekt typu strukturalnego Osoba i jego składowym zostają przypisane pewne wartości (w tym przypadku dane autora tego kursu C++). Następnie zostaje wypisany rozmiar struktury - zostają wypisane rozmiary poszczególnych składowych struktury i rozmiar całej struktury.

W rzeczywistości rozmiar struktury jest większy od tego wypisanego przez kompilator. Wynika to z tego, że typ string nie jest typem prostym i wyrażenie sizeof(string) zwraca rozmiar tylko małej części zajmowanej przez zmienną typu string. Konsekwencją takiej sytuacji jest to, że wypisanie rozmiaru struktury zawierającej składowe typu string również nie będzie prawidłowe, bowiem jako rozmiar składowej typu string zostanie przyjęta mniejsza wartość (np. 4 bajty). Problem pojawia się tylko przy wypisaniu rozmiaru typu string - sam program w trakcie działania prawidłowo oblicza pamięć typu string i nie ma mowy o żadnych problemach z pamięcią dla tego typu - problem pojawia się jedynie podczas próby wypisania rozmiaru zmiennych typu string lub struktur zawierających składowe tego typu.

Wróćmy jednak do głównego wątku, czyli do funkcji. W przypadku funkcji, która przyjmuje argument przez wartość, tworzona jest kopia parametru aktualnego. Jeśli uruchomisz program zauważysz, że funkcja wykorzystuje dodatkowe 12 bajtów pamięci (wiesz już, że w rzeczywistości jest to na pewno więcej). Zwróć uwagę, że struktura jest bardzo prosta - ma tylko 3 składowe pola. Dla bardziej skomplikowanej struktury wywołanie funkcji spowodowałoby oczywiście wykorzystanie znacznie większej dodatkowej ilości pamięci. Wypisanie składowych zmiennej typu Osoba nie powoduje żadnego problemu - jeśli masz wątpliwości związane ze sposobem wypisania wartości składowych, przeczytaj ponownie lekcje dotyczące struktur.

W przypadku funkcji, która przyjmuje argument przez referencję wypisanie składowych zmiennej typu Osoba jest również bezproblemowe. Niestety skorzystanie z operatora sizeof dla referencji pokaże rozmiar dokładnie taki sam jak dla funkcji przyjmującej argument przez wartość. Ponieważ jednak nie jest to prawdą, wypisanie rozmiaru zostało celowo zakomentowane. Referencja jest powiązana zawsze z konkretną zmienną, dlatego też skorzystanie z operatora sizeof zwraca rozmiar obiektu z którym jest powiązana referencja a nie rozmiar referencji. Musisz mi uwierzyć na słowo, że w takim przykładzie jak powyżej rozmiar referencji na pewno będzie mniejszy niż rozmiar zmiennej typu strukturalnego. W rzeczywistości rozmiar referencji będzie często równy rozmiarowi wskaźnika.

Z kolei w funkcji przyjmującej argument przez wskaźnik wyraźnie już i bez żadnych problemów widać jaki jest rozmiar dodatkowej pamięci wykorzystywanej na argument funkcji. Wskaźnik zajmie 4 bajty, podczas gdy kopia zmiennej (tak jak w przypadku przekazywania przez wartość) zajmuje więcej niż 12 bajtów. Zatem jak widzisz, jest to spory zysk pamięci. Jeśli struktura byłaby większa, a przekazywanych byłoby dodatkowo więcej argumentów, zysk byłby jeszcze bardziej wyraźny. Dodatkowo przydzielanie pamięci na zmienne oprócz samej pamięci zajmuje również czas, zatem wykorzystując przekazywanie przez wskaźnik (podobnie - przekazywanie przez referencję) pozwala zaoszczędzić nie tylko pamięć, ale również cenny czas.

W przypadku funkcji przyjmującej argument przez wskaźnik dość "ciekawe" jest za to wypisanie składowych zmiennej typu Osoba. Schemat odwołania do składowej struktury za pomocą wskaźnika wygląda następująco:

(*wskaznik).skladowaStruktury

Zapamiętaj na razie powyższy schemat odwoływania się za pomocą wskaźnika do składowych zmiennej typu strukturalnego. Dokładniejsze informacje na ten temat zostaną przedstawione przy omawianiu klas w języku C++.

Wady i zalety poszczególnych metod przekazywania argumentów do funkcji

Mimo że nie przedstawiłem Ci jeszcze wszystkich aspektów przekazywania argumentów do funkcji, to już teraz można pokusić się o podsumowanie i porównanie metod przekazywania argumentów do funkcji.

Przekazywanie przez wartość ma w zasadzie tylko dwie małe zalety - jest proste i gwarantuje brak błędów ze strony programisty. Wadą jest natomiast brak możliwości zmiany argumentu przekazanego do funkcji, a także to, że jest tworzona kopia argumentu przekazanego do funkcji - nie dość, że tracone jest cenne miejsce w pamięci na kopię zmiennej, to dodatkowo operacja wykonania kopii trwa jakiś czas.

Przekazywanie przez referencję ma jedną zasadniczą wadę - wywołanie funkcji wygląda tak jak wywołanie funkcji przyjmującej argument przez wartość, przez co analiza programu może być nieco trudniejsza. Zalety są jednak znacznie większe. Przede wszystkim, wykorzystywanie przekazywania przez referencję jest również stosunkowo łatwe - odwoływanie się do argumentu wewnątrz funkcji przebiega tak jak odwołanie do zwykłej zmiennej. Ponadto nie jest tworzona kopia argumentu, przez co zostają zaoszczędzone czas i miejsce w pamięci. Trzeba jednak zwrócić uwagę na jeszcze jedną wadę tej metody przekazywania argumentów - ponieważ referencja jest w swojej istocie stała (zawsze jest powiązana na stałe z obiektem, na który wskazuje), to jeśli z jakichś powodów chcielibyśmy zmienić jej powiązanie, okaże się to niemożliwe.

Przekazywanie przez wskaźnik ma zalety podobne do przekazywania argumentów przez referencję - nie jest tworzona kopia argumentów, przez co zostają zaoszczędzone czas i miejsce w pamięci. Trudniejsze jest posługiwanie się wskaźnikiem niż referencją, ale za to analiza programu jest nieco łatwiejsza (w wywołaniu funkcji widać, że przekazujemy argument przez wskaźnik). Zaletą wskaźnika jest również to, że wskaźnik nie musi wskazywać na stałe na daną zmienną (w przeciwieństwie do referencji), zatem jeśli chcemy wskaźnikiem pokazać na jakąś inną zmienną, możemy zrobić to bez problemu (w przypadku referencji jak dobrze wiesz, nie jest to możliwe).

Wybór metody przekazywania argumentów do funkcji

Mimo że wybór metody przekazywania argumentów do funkcji zależy od wielu czynników, postaram Ci się przedstawić krótką instrukcję, którą możesz kierować się (zwłaszcza na początku nauki funkcji) przy wyborze metody przekazywania argumentów do funkcji.

Wybierz metodę przekazywania przez wartość jeśli:

- przekazujesz do funkcji zmienną typu prostego (int, char, double, ...) lub typu pochodnego (referencja, wskaźnik) i nie zależy Ci na możliwości zmiany jej wartości
- przekazujesz do funkcji literał.

W przeciwnym przypadku przejdź do kryteriów przekazywania przez referencję.

Wybierz metodę przekazywania przez referencję jeśli:

- przekazujesz do funkcji zmienną typu prostego (int, char, double, ...) lub typu pochodnego (referencja, wskaźnik) i chcesz zmienić jej wartość
- przekazujesz do funkcji zmienną typu złożonego (na przykład struktury)
- nie zależy Ci na zmianie powiązania argumentu ze zmienną (referencja sama w sobie jest stała)

W przeciwnym przypadku przejdź do kryteriów przekazywania przez wskaźnik.

Wybierz metodę przekazywania przez wskaźnik jeśli:

- zależy Ci na zmianie powiązania argumentu ze zmienną (wskaźnikiem możesz pokazywać na różne zmienne)
- wykonujesz skomplikowane operacje i dobrze znasz wskaźniki

W przeciwnym przypadku rozważ przekazywanie przez wartość lub przez referencję.

Podsumowanie

W tej lekcji przedstawiłem Ci rozszerzoną wiedzę na temat przekazywania argumentów do funkcji. Wiesz już, jakie są wady i zalety poszczególnych rozwiązań, a także wiesz już, której metody należy użyć w danej sytuacji. Wiesz także, że do funkcji można przekazywać jako argumenty nie tylko zmienne dowolnego typu (zarówno typu podstawowego, pochodnego czy strukturalnego), ale również literały.

powrót