Jason. Cała informatyka w jednym miejscu!

Kolejny rozdział poświęcony językowi C. Przyjrzymy się być może dla niektórych z Was nietypowemu zapisowi w kodzie źródłowym. Chodzi o funkcje. Zarówno w tym języku, jak i w C++, to nie wygląda tak, że wstawimy sobie funkcję gdzie chcemy i mamy z bani. Języki te są bardzo wyczulone na położenie zarówno deklaracji, jak i definicji funkcji. Robi to jakąś różnicę? Jak najbardziej i to DUŻĄ. Bo przy deklaracji wstawiamy prototyp funkcji w języku C, a przy definicji "pełną" jej strukturę. Zapraszam po wyjaśnienia.

CZEMU PROTOTYP FUNKCJI W JĘZYKU C JEST ISTOTNIEJSZY NIŻ CI SIĘ WYDAJE?

Prototyp to najprościej pisząc sam nagłówek funkcji pozbawiony treści bloku. Jego zadaniem jest poinformowanie kompilatora, że w chwili wywołania pożądanej funkcji jej pełna definicja gdzieś znajduje się w kodzie i musi ją odszukać. Ponadto, umożliwia sprawdzenie poprawności wprowadzonych parametrów aktualnych (czyli wartości wprowadzanych do funkcji). W dalszej części pokażę trzy kody źródłowe w których jeden z nich będzie pokazywał czym grozi zignorowanie dodania prawidłowego nagłówka.

Prototyp funkcji w języku C zwykle umieszczany jest w przeznaczonych do tego plikach nagłówkowych, aby pomóc kompilatorowi "skleić" cały program w jeden plik wykonywalny (i tam powinien się znajdować, bo taka jest praktyka!). Czas na prezentację przykładowego kodu źródłowego z wykorzystaniem funkcji oraz jej prototypu:

#include <stdio.h>

int max(int, int);			// prototyp funkcji

int main(void)
{
	printf("Wieksza liczba z %d i %d jest %d.\n", 3, 5, max(3.0, 5.0));
	getchar();
	
	return 0;
}

int max(int n, int m)
{
	return (n > m) ? n : m;
}

Jak widzimy, mamy program z własną funkcją "max" zwracającą większą z dwóch podanych liczb. Natomiast pod dyrektywą "#include" znajduje się jeszcze prototyp funkcji "max"! Jak uprzedziłem, składa się jedynie z nagłówka czyli typu zwracanej wartości ("int"), nazwy, a także ile przyjmuje parametrów oraz jakiego typu. Zwróćcie dodatkową uwagę, że nie muszę podawać nawet nazw parametrów!

Jeszcze jedna ważna rzecz! Wiem, że w wywołaniu dorzuciłem "double" zamiast "int" (w tym języku wolno tak zrobić), ale uniknę jeszcze przez chwilę tłumaczenia i zaprezentuję kolejny kod źródłowy prezentujący "wybrakowany" prototyp funkcji w języku C obowiązujący przed standardem ANSI C w 1989 roku:

#include <stdio.h>

int max();			// prototyp funkcji przed ANSI C (1989)

int main(void)
{
	printf("Wieksza liczba z %d i %d jest %d.\n", 3, 5, max(3.0));
	getchar();
	
	return 0;
}

int max(int n, int m)
{
	return (n > m) ? n : m;
}

Zastanówcie się spokojnie, skompilujcie program i zobaczcie jakie będą kwiatki. Mamy tutaj ogromne zróżnicowanie w działaniu, a przede wszystkim szereg konsekwencji. Po pierwsze, kompilator nie uprzedzi, że wprowadzamy niewłaściwy typ do funkcji (tamten przykład stosował "obcinanie" części ułamkowej i jakoś się nam "upiekło"). Po drugie, kompilator nie znając żadnych detali na temat parametrów "zakłada", że będzie to typ "int". Mało tego, pozwala na użycie funkcji z niezgodną liczbą parametrów!!! W tym przypadku wystarczyło jedynie to, że prototyp funkcji w języku C nie dostarcza dostatecznej ilości informacji, a już tragedia wisi w powietrzu. Może Was ciekawić CO w takim razie komputer "podstawia" do drugiego parametru? Byle jaką "śmieciową" wartość, która aktualnie znajduje się w pamięci. W każdym razie, to nie jest nic dobrego...bo jest to niezdefiniowane zachowanie!

FUNKCJA BEZ PROTOTYPU TEŻ ZADZIAŁA

Dla tych, którzy nie są przyzwyczajeni do podziału na prototyp funkcji i pełną definicję, można jeszcze napisać w następujący sposób. To jest ten sam przykład w pierwszym fragmencie:

#include <stdio.h>

int max(int n, int m)			// prototyp funkcji + pełna definicja
{
	return (n > m) ? n : m;
}

int main(void)
{
	printf("Wieksza liczba z %d i %d jest %d.\n", 3, 5, max(3.0, 5.0));
	getchar();
	
	return 0;
}

Język C weryfikuje istnienie konkretnej funkcji "schodząc" z góry na dół. Nawet jeśli zdefiniowalibyście tę funkcję ale poniżej "main", kompilator zgłosiłby błąd z powodu nieodnalezienia żadnych "poszlak" wywoływanej funkcji. Powyższy zapis również jest poprawny z punktu widzenia kompilacji, aczkolwiek nie jest on dobrą praktyką przy posługiwaniu się tym językiem. Ponadto, pozbawiacie się możliwości "podzielenia" funkcji na osobne pliki tak, aby w pliku nagłówkowym był sam prototyp funkcji w języku C (i wtedy dołączacie dyrektywą "include"), a w osobnym pliku źródłowym z rozszerzeniem ".c" była pełna definicja funkcji:

PLIK NAGŁÓWKOWY "MAX.H"

#ifndef MAX_I
#define MAX_I

int max(int, int);

#endif

PLIK ŹRÓDŁOWY "MAX.C"

#include "max.h"

int max(int n, int m)
{
	return (n > m) ? n : m;
}

PLIK ŹRÓDŁOWY "MAIN.C"

#include <stdio.h>
#include "max.h"	// import naszej funkcji "max"

int main(void)
{
	printf("Wieksza liczba z %d i %d jest %d.\n", 3, 5, max(3.0, 5.0));
	getchar();
	
	return 0;
}

Tak się prawidłowo pisze, ale pouczać nie zamierzam. Do pliku nagłówkowego należy dodatkowo otoczyć kod tzw. "include guard", żeby uniknąć wielokrotnego definiowania tych samych prototypów. Plik źródłowy musi otrzymać import przez dyrektywę "include" (koniecznie popatrz na otoczenie nazwy cudzysłowami zamiast nawiasami kątowymi, które oznacza wskazanie na szukanie pliku w katalogu zawierającym ten plik, który to ma zamiar importować), a dopiero do pliku z "main" trafia raz kolejny import samego pliku nagłówkowego i w ten sposób nie łączymy ze sobą dwóch odpowiedzialności :D.


To by było na tyle.

PODOBNE ARTYKUŁY