Funkcje - podstawy
Można powiedzieć, że w języku C program jest zbiorem definicji zmiennych i funkcji. Komunikacja pomiędzy funkcjami odbywa się za pośrednictwem argumentów wywołania funkcji i wartości zwracanych przez funkcje, a także za pomocą zmiennych zewnętrznych (globalnych). Definicja funkcji ma następującą postać
wartoscZwracana NazwaFunkcji(deklaracje parametrow)
{
deklaracje
instrukcje
}
Kilka ważniejszych informacji związanych z funkcjami
- W pliku źródłowym funkcje moogą występować w dowolnej kolejności a program można podzielić między kilka plików źródłowych pod warunkiem, że żadna z funkcji nie zostanie podzielona.
- Wywołana funkcja przekazuje wartość do miejsca wywołania dzięki instrukcji return
- Funkcja wywołująca może zignorować zwracaną wartość.
- Instrukcja return nie musi zawierać wyrażenia. W takiej sytuacji do miejsca wywołania nie przekazuje się żadnej wartości.
- Sterowanie wraca bez wartości wówczas gdy wykonywanie funkcji zakończy się po osiągnięciu nawiasu klamrowego zamykającego funkcję.
- Pominięcie w definicji typu powrotu przez domniemanie oznacza, że funkcja zwraca wartość typu int
#include <stdio.h>
int Max(int x, int y)
{
if (x>y)
return x;
return y;
// mozna krocej
//return (x>y)?x:y;
//
}
int main(void)
{
int x=10, y=12;
int a=45, b=75;
printf("Max z %d i %d to %d\n",x,y,Max(x,y));
printf("Max z 1 i 7 to %d\n",Max(1,7));
printf("Max z %d i %d to %d\n",a,b,Max(a,b));
return 0;
}
|
|
Listing 2.1
|
Max z 10 i 12 to 12
Max z 1 i 7 to 7
Max z 45 i 75 to 75
|
|
Output 2.1
Efekt działania programu z listingu 2.1
|
Analizując powyższy przykład warto zauważyć, że
- Główna część programu, którą wpisujemy w main nie jest niczym innym jak funkcją; main to specjalna funkcja, od której rozpoczyna się wykonanie programu.
- Jeśli nie przekazujemy do funkcji żadnych argumentów, lub jeśli funkcja nic nie zwraca to jako typ zwracanej wartości piszemy void
- Funkcja main w przypadku poprawnego zakończenia zwraca wartość zero.
Jeśli nie chcemy w danym miejscu umieszczać ciała funkcji, czyli tego co jest między nawiasami klamrowymi, ale mimo to chcemy (czasem musimy) jakoś zasygnalizować istnienie pewnej funkcji, to możemy zamiast jej definicji napisać jej deklarację. Deklaracja funkcji ma następującą postać
wartoscZwracana NazwaFunkcji (deklaracje parametrow);
Deklarację nazywa się też prototypem funkcji. W deklaracji nazwy parametrów traktowane są jak komentarze. Nie muszą występować a jeśli wystąpią to nie muszą być zgodne z nazwami użytymi przy definicji.
Globalnie i lokalnie
- Program w języku C to zbiór obiektów (zmiennych i funkcji) zewnętrznych (globalnych).
- Wewnętrzne mogą być tylko zmienne. Zmienną wewnętrzną nazwiemy każdą zmienną zadeklarowaną wewnątrz funkcji; będziemy o nich także mówić: zmienne automatyczne.
- Zmienne zewnętrzne definiuje się poza wszystkimi funkcjami są więc one dostępne dla wszystkich funkcji.
- W języku C nie dopuszcza się definiowania funkcji wewnątrz innej funkcji.
- Zmienne zewnęrzne umożliwiają prostą komunikację pomiędzy funkcjami. Zmienne globalne istnieją przez cały czas działania programu.
- Jeśli nie podano inaczej, zmienne zewnętrzne są zawsze inicjalizowane zerami, natomiast wartości dla zmiennych automatycznych są nieokreślone (przypadkowe).
- Mówiąc o zasięgu nazwy określać będziemy tą część programu wewnątrz której można danej nazwy użyć.
- Zmienne lokalne o tej samej nazwie, które występują w różnych funkcjach, nie są ze sobą związane w zadne sposób. Podobnie wygląda sytuacja z parametrami funkcji, które nie są niczym innym jak zmiennymi lokalnymi (o zasięgu ograniczonym tylko do danej funkcji).
#include <stdio.h>
int x; //zmienna globalna
int y; //zmienna globalna
int z=0; //zmienna globalna
void Change(int i)//i jest lokalne w tej funkcji
{
i=i*i;
z+=1;
}
void Swap1(int i, int y)//i i y sa lokalne w tej funkcji
{
int temp;//zmienna lokalna; jej zasieg to cialo funkcji swap1
temp=i;
i=y;
y=temp;
z+=1;
}
void Swap2(void)
{
int temp;//zmienna lokalna; jej zasieg to cialo funkcji swap2
temp=x;
x=y;
y=temp;
z+=1;
}
int main(void)
{
x=2;
y=7;
printf("x=%d, y=%d z=%d\n",x,y,z);
Swap1(x,y);
printf("x=%d, y=%d z=%d\n",x,y,z);
Swap2();
printf("x=%d, y=%d z=%d\n",x,y,z);
Change(x);
printf("x=%d z=%d\n",x,z);
}
|
|
Listing 2.2
|
x=2, y=7 z=0
x=2, y=7 z=1
x=7, y=2 z=2
x=7 z=3
|
|
Output 2.2
Efekt działania programu z listingu 2.2
|
Przyglądając się ostaniemu przykładowi zaobserwować można jak zachowują się argumenty przekazywane do funkcji. Otóż funkcja swap1, mimo iż chce, nie może zmienić ich wartości; pracuje ona na ich lokalnej kopii. Taki sposób przekazywnia argumentów nazywamy przekazywaniem przez wartość.
Zmienne statyczne
Deklarację/definicję zmiennej czy funkcji można poprzedzić słowem kluczowym static
- Jeśli dotyczyć ono będzie zmiennej zewnętrznej (globalnej) czy funkcji to wówczas ta zmienna czy funkcja będzie widoczna jedynie w pliku w którym została zadeklarowana (o podziale programu na wiele plików będzie w dalszej części wykładu).
- Jeśli dotyczyć ono będzie zmiennej wewnętrznej wówczas taka zmienna nie zniknie pomiędzy wywołaniami tej samej funkcji. Wewnętrzne zmienne statyczne można uważać za prywatną, stałą pamięć wewnątrz pojednyńczej funkcji (patrz przykład poniżej).
Zmienne statyczne, jeśli nie podano inaczej, są zawsze inicjowane zerami.
#include <stdio.h>
void funkcja(void)
{
int x=0;
static int y=0;
printf("x=%d, y=%d\n",x,y);
x++;
y++;
}
int main(void)
{
funkcja();
funkcja();
funkcja();
return 0;
}
|
|
Listing 2.3
|
x=0, y=0
x=0, y=1
x=0, y=2
|
|
Output 2.3
Efekt działania programu z listingu 2.3
|
Preprocesor - makroinstrukcje
Preprocesor jest programem, który poddaje tekst źródłowy naszego programu pewnym przkształceniom zanim rozpocznie się właściwy proces kompilacj (patrz rysunek poniżej)

Najprostszy rodzaj makroinstrukcji ma postać
#define NAZWA tekstZastepujacy
Dalsze wystąpienia ciągu
NAZWA będą zastąpowane przez ciąg znaków tworzących
tekstZastepujacy Zwykle makroinstrukcja zajmje jeden wiersz, ale w przypadku koniczności kontynuowania jej w kolejnych wierszach na kończu należy stawiać znak \ Zasięg nazwy wprowadzanej przez
#define rozciąga się od miejsca definicji do końca tłumaczonego pliku źródłowego. Makroinstrukcja może korzystać z poprzednich makroinstrukcji. Makrorozwinięć (czyli zastosowanie makroinstrukcji) dokonuje się jedynie dla całych jednostek leksykalnych i nie stosuje wewnątrz stałych napisowych. Istnieje także możliwość definiowania makr z argumentami (patrz przykład poniżej). Argumenty makroinstrukcji mogą być we wstawianym tekście poprzedzone operatorem # W takim przypadku preprocesor umieszcza odpowiedniu argument wewnątrz cudzysłowu, przekształcając go tym samym w łańcuch. Operator ## możliwia sklejanie argumentów podczas rozwijania makra. Anulowanie makroinstrukcji możliwe jest dzięki dyrektywie
#undef nazwaMakroinstrukcji
przy czym nie ma potrzeby podawania listy argumentów, nawet jeśli zdefiniowana poprzednio makroinstrukcja takie argumenty posiadała.
#include <stdio.h>
#define OK 0
#define LINIA printf("==============================\n");
#define ERROR(tekst) printf("Blad:"#tekst"\n");
#define BIGERROR(tekst) ERROR(tekst) printf("Prosze zakonczyc prace\n");
#define TEST1 printf("Ble, ble...\n");
#define TEST2(tekst) tekst##1
int main(void)
{
LINIA
ERROR(To jest kontrolowany blad)
LINIA
BIGERROR(Kontrolowany bug numer 2)
LINIA
TEST2(TEST)
return OK;
}
|
|
Listing 2.4
|
==============================
Blad:To jest kontrolowany blad
==============================
Blad:Kontrolowany bug numer 2
Prosze zakonczyc prace
==============================
Ble, ble...
|
|
Output 2.4
Efekt działania programu z listingu 2.4
|
#include
Każdy wiersz źródłowy programu postaci
#include "nazwaPliku"
lub
#include <nazwaPliku>
zastępowany jest zawartością pliku o wskazanej nazwie. W pierwszym przypadku, poszukiwanie pliku zaczyna się tam gdzie znaleziono przetwarzany w danym momencie plik źródłowy. Jeśli nie zostanie tam znaleziony, lub jego nazwa zawarta jest w nawiasach ostrych (drugi przypadek) to pliku tego szuka się zgodnie z zasadmi określonymi w danej implementacji.
Często kilka wierszy o takiej postaci pojawia się na początku pliku źródłowego. Ich zadaniem jest dołączenie wspólnych definicji
#define i deklaracji
extern lub wprowadzenie deklaracji prototypów funkcji bibliotecznych ze standardowych nagłówków jak
stdio.h
#include <stdio.h>
int main(void)
{
#include "wypisz.txt"
return 0;
}
|
|
Listing 2.5
|
printf("Tekscik testowy\n");
|
|
Listing 2.5.1
Plik wypisz.txt
|
|
Output 2.5
Efekt działania programu z listingu 2.5
|
Kompilacja warunkowa
Za pomocą instrukcji warunkowych wykonywanych w fazie preprocesora można wybiórczo włączać do programu pewne fragmenty kodu. W tym celu wykorzystywać będziemy instrukcje analogiczne do poznanej już if-else, ale dla odróżnienia poprzedzając słowa kluczowe snakiem #
#if (wyrazenie1)
instrukcje
#elif (wyrazenie2)
instrukcje
#elif (wyrazenie3)
instrukcje
#else
instrukcje
#endif
W tak zmienionej instrukcji
if-else możemy użyć wyrażenia
defined (NAZWA)
które jest równe 1 jeśli nazwa została wcześniej zdefiniowana za pomocą
#define albo 0 w przeciwny przypadku. Ponadto do sprawdzania czy nazwa została uprzednio zdefiniowana, wprowadza się dwie specjalne i derektywy:
#ifdef oraz
#ifndef
#include <stdio.h>
#define TAK 1
#define NIE 0
#define ZONA 1
#define ZALUJE NIE
void funkcjaZwierzenia(void)
{
#if defined(ZONA)
printf("Mam zone...\n");
printf("Porozmawiajmy o Niej troche...\n");
#if ZALUJE
printf("Wcale nie jest mi z Nia dobrze!\n");
#else
printf("Coz to za cudowna Kobieta!\n");
#endif
#else
printf("Nie znam pojecia ZONA!\n");
#endif
}
void funkcjaSamooceny(void)
{
#ifdef TAK
printf("Znam makro TAK, ktore ma wartosc %d\n",TAK);
#endif
//#define JAKIES 15
#ifndef JAKIES
#define JAKIES 135
#endif
printf("Znam makro JAKIES, ktore ma wartosc %d\n",JAKIES);
}
int main(void)
{
funkcjaZwierzenia();
funkcjaSamooceny();
}
|
|
Listing 2.6
|
Mam zone...
Porozmawiajmy o Niej troche...
Coz to za cudowna Kobieta!
Znam makro TAK, ktore ma wartosc 1
Znam makro JAKIES, ktore ma wartosc 135
|
|
Output 2.6
Efekt działania programu z listingu 2.6
|
Rekursja vs. iteracja
Tutaj powinno być wyjaśnienie co to jest rekursja (rekurencja) i co to jest iteracja. Nie mam jednak zdrowia tego pisać, więc powiem tak:
- Iteracja czyli wielokrotne powtarzanie (w pętli) zadanego fragmentu kodu.
- Rekursja czyli wywoływanie funkcji przez samą siebie lub inaczej definiowanie funkcji przez użycie w definicji tejże funkcji czyli definiowaniu obiektu w oparciu o ten obiekt.
Klasycznym przykładem na funkcję rekursywną i iteracyją jest funkcja obliczająca silnię. Mamy do dyspozycji dwie definicje silni:
- Iteracyjną: silnia z n jest to iloczyn liczb całkowitych od 1 do n.
Rekursywną: silnia z n jest to iloczyn n * silnia z (n-1).
W obu przypadkach zakładamy, że n jest liczbą całkowitą większą od zera. Jeśli n jest równe zero, wówczas przyjmujemy
silnia(0)=1. Poniżej przedstawiono odpowiednie wersja funkcji.
double SilniaI(int n)
{
int i=1;
double s=1;
for(;s=s*i,i
|
|
Listing 2.7
Iteracyjna wersja funkcji obliczającej silnię.
|
double SilniaR(int n)
{
if (n==0)
return 1;
return n*SilniaR(n-1);
}
|
|
Listing 2.8
Rekursywna wersja funkcji obliczającej silnię.
|