Definicja lub deklaracja dostarcza kompilatorowi informacji o funkcji: jaki jest jej typ zwracany, jaka jest liczba parametrów, jaki jest typ parametrów itd. W ten sposób, za każdym razem, gdy w dalszej części tekstu programu pojawia się użycie tej funkcji, kompilator może sprawdzić
Dlaczego definicja lub deklaracja, co to w ogóle jest deklaracja funkcji i do czego się przydaje?
Wyobraźmy sobie następującą sytuację: definiujemy kolejno dwie funkcje, fun1 i fun2. Funkcja fun1 wywołuje w swej treści funkcję fun2 i odwrotnie, funkcja fun2 wywołuje w swej treści funkcję fun1:
1. void fun1(int k) { 2. // ... 3. fun2(k) 4. // ... 5. } 6. 7. void fun2(int m) { 8. // ... 9. fun1(m) 10. // ... 11. }W jakiej kolejności zdefiniować te funkcje? Jeśli zdefiniujemy je tak jak wyżej, to w linii 3 kompilacja zostanie przerwana, bo nieznana jest w niej jeszcze funkcja fun2; odwrócenie kolejności nie pomoże, bo wtedy instrukcja wywołania fun1 wewnątrz fun2 spowoduje te same kłopoty.
Na szczęście jest wyjście z tej sytuacji. Kompilatorowi nie jest potrzebna definicja funkcji, to znaczy nie musi wiedzieć, co funkcja robi: musi tylko wiedzieć, ile i jakie ma parametry i jaki jest jej typ zwracany. Do tego wystarczy prototyp funkcji podany w deklaracji. Deklaracja ma formę nagłówka funkcji, po którym następuje średnik zamiast ciała (treści) funkcji. Na przykład poprawnymi deklaracjami funkcji są
string& fun1(char* c1, char* c2, bool b); void fun2(int k, double d[]); Klasa* fun3(Klasa* k1, Klasa* k2);Nie mają one ciała (treści), a więc nie są definicjami. Ale zawierają informacje o nazwie, typie zwracanym, typie i liczbie parametrów (czyli właśnie prototyp). Jest to wszystko, czego potrzebuje kompilator, aby sprawdzić formalną prawidłowość ich użycia. Tak więc nasz pierwszy przykład skompiluje się gładko, jeśli przed definicją funkcji fun1 umieścimy deklarację funkcji fun2 (ze średnikiem na końcu):
1. void fun2(int); 2. 3. void fun1(int k) { 4. // ... 5. fun2(k) 6. // ... 7. } 8. 9. void fun2(int m) { 10. // ... 11. fun1(m) 12. // ... 13. }Zauważmy, że w deklaracji (pierwsza linia) nie podaliśmy nazwy pierwszego i jedynego parametru formalnego funkcji fun2 — tylko jego typ (int). Jest to całkowicie dopuszczalne: do sprawdzenia poprawności wywołania kompilator potrzebuje informacji o liczbie i typie parametrów funkcji, ale ich nazwy nie są do niczego potrzebne i są wobec tego przez kompilator pomijane. Zatem można ich w ogóle nie pisać (choć warto, bo umiejętnie dobrane nazwy są znakomitą formą komentarza). Oczywiście w definicji nazwa zwykle jest konieczna, ale nie musi być taka sama jaka została podana w deklaracji.
Na przykład, podane poprzednio trzy deklaracje moglibyśmy równie dobrze zapisać tak:
string& fun1(char*, char*, bool); void fun2(int, double[]); Klasa* fun3(Klasa*, Klasa*);Funkcja zadeklarowana musi oczywiście być gdzieś również zdefiniowana (tylko raz). W przeciwnym razie powstanie błąd na etapie linkowania (łączenia, konsolidacji) programu. Zauważmy, że błędu nie będzie, jeśli zadeklarowana funkcja nie została w programie użyta — tak więc wolno deklarować funckje, które dopiero zamierzamy napisać, byle tylko nie próbować ich użycia. Definicja nie musi wystąpić w tym samym module (pliku). Wystarczy, że umieścimy ją w jakimś module składającym się na cały program. W innych modułach, w których funkcja ta jest używana, trzeba tylko zamieścić jej deklarację.
Ponieważ na razie nasze programy i tak mieszczą się w jednym pliku, szczegóły odłożymy do rozdziału o modułach programu .
Oczywiście wszystkie deklaracje, jeśli jest ich kilka, muszą być ze sobą zgodne, czyli definiować ten sam prototyp. Definicja również musi być zgodna z deklaracjami — to znaczy nagłówek funkcji musi być zgodny z prototypem. Jak mówiliśmy, ta zgodność nie musi dotyczyć nazw parametrów formalnych funkcji, które w ogóle nie mają znaczenia w deklaracjach i do prototypu nie należą.
Nagłówek określa prototyp, czyli zewnętrzne własności funkcji. W najprostszej postaci wygląda on tak:
Typ Nazwa ListaParamgdzie Typ określa typ wartości zwracanej (przed nią mogą wystąpić modyfikatory, o których powiemy w dalszej części rozdziału), Nazwa oznacza nazwę funkcji, a ListaParam listę parametrów formalnych.
Część nagłówka funkcji składająca się z nazwy funkcji i listy typów jej parametrów (bez nazw tych parametrów) nazywa się czasem jej sygnaturą. Typu zwracanego zwykle w sygnaturze nie uwzględnia się. Tak więc na przykład sygnaturą funkcji o prototypie
double fun(double x, char* nap);jest
fun(double, char*)
Definicja funkcji składa się z takiego samego nagłówka, tyle że teraz nie kończymy go średnikiem, tylko umieszczamy zaraz za nim, ujętą w nawiasy klamrowe, treść (ciało) funkcji, czyli sekwencję instrukcji do wykonania. Jeśli w ciele funkcji chcemy korzystać z argumentu przekazanego poprzez parametr funkcji, to oczywiście ten parametr musi mieć nazwę (którą w deklaracji mogliśmy pominąć). W programie może występować tylko jedna definicja funkcji, choć wiele deklaracji. Wyjątkiem są funkcje rozwijane, które mogą być definiowane wielokrotnie (patrz podrozdział o funkcjach rozwijanych ).
Ciało funkcji może być traktowane jak wnętrze instrukcji grupującej (złożonej). Do zakresu tej instrukcji grupującej należą również deklaracje zmiennych lokalnych opisane przez specyfikacje parametrów funkcji. Zmienne definiowane w ciele funkcji będą w czasie jej wykonywania lokalne — po wykonaniu funkcji są one usuwane. Zmiennymi lokalnymi są również zmienne wyspecyfikowane jako parametry formalne funkcji: będą one zainicjowane wartościami argumentów wywołania. Po zakończeniu wykonywania funkcji zmienne lokalne (z wyjątkiem tych zadeklarowanych jako static) będą usunięte.
Zmienne lokalne funkcji (w tym te deklarowane przez specyfikacje parametrów w nagłówku funkcji) w żaden sposób nie kolidują ze zmiennymi o tej samej nazwie w innych funkcjach. Mogą natomiast przesłaniać nazwy zmiennych globalnych, zadeklarowanych poza funkcjami i klasami. Jeśli tak jest, to niekwalifikowana nazwa występująca w ciele funkcji odnosi się zawsze do zmiennej lokalnej, natomiast dostęp do zmiennej globalnej o tej nazwie mamy poprzez operator zasięgu — czterokropek (patrz rozdział o zasięgu i widzialności zmiennych).
Nie wolno definicji funkcji zagnieżdżać, to znaczy nie można w ciele jednej funkcji definiować innej funkcji (co jest dozwolone w innych językach, jak Pascal czy Fortran 90/95). W nowym standardzie C++11 można jednak wewnątrz funkcji definiować tak zwane funkcje lambda, o których za chwilę.
Definicja (i deklaracja) funkcji może w nowym standardzie C++11 mieć inną, alternatywną, postać, a mianowicie
auto f_nazwa(parametry) -> typ_zwracany { ciało }Zamiast specyfikować typ zwracany przed nazwą funkcji, stawiamy tam słowo kluczowe auto, natomiast typ funkcji określamy zaraz za listą parametrów, poprzedzając go „strzałką” (' ->'). Nie musimy zresztą tego typu określać jawnie, możemy użyć tu wyrażenia decltype (zob. rozdział o typach danych ). Zauważmy, że jeśli tak zrobimy, to możemy w decltype użyć nazw parametrów funkcji, bo w tym miejscu jesteśmy już w zakresie funkcji. Pokażmy to na przykładzie
1. #include <iostream> 2. using namespace std; 3. 4. auto D(int a, double b) -> decltype(a*b); ➊ 5. 6. double A(int a, double b) { ➋ 7. return a*b; 8. } 9. 10. auto B(int a, double b) -> double { ➌ 11. return a*b; 12. } 13. 14. auto C(int a, double b) -> decltype(a*b) { ➍ 15. return a*b; 16. } 17. 18. double D(int a, double b) { ➎ 19. return a*b; 20. } 21. 22. int main() { 23. cout << A(4,2.5) << " " << B(4,2.5) << " " 24. << C(4,2.5) << " " << D(4,2.5) << endl; 25. }
Definiujemy tu szereg funkcji (A, B, C, D), które są właściwie identyczne — wszystkie po prostu zwracają iloczyn swoich argumentów. Definicja funkcji A (➋) jest „normalna”. W linii ➌ użyliśmy natomiast nowej składni (auto przed nazwą, typ za listą parametrów i poprzedzony strzałką). W definicji funkcji C (➍) zamiast podawać jawnie typ „poprosiliśmy” kompilator, aby sam określił, jaki powinien być typ iloczynu argumentów (oczywiście będzie to double). Widać też, że nowej składni możemy też użyć do deklarowania funkcji (➊) — sama definicja (➎) może być zapisana zarówno w nowej jak i starej składni (wtedy oczywiście typ zwracany musi się zgadzać z tym wydedukowanym przez kompilator z deklaracji).
Forma użyta w tym przykładzie w definicji funkcji C (➍) okaże się niesłychanie użyteczna przy definiowaniu szablonów funkcji.
Powiedzmy na koniec, że deklaracje i definicje funkcji nie są instrukcjami wykonywalnymi (jak na przykład w Pythonie). Zatem kolejność, w jakiej je piszemy, nie ma znaczenia, dopóki spełniony jest warunek, że co najmniej jedna deklaracja poprzedza leksykalnie instrukcje, w których funkcja jest wykorzystywana.
T.R. Werner, 21 lutego 2016; 20:17