Wykład 2
Funkcje

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


#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

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


#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

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

Tekscik testowy
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:

Klasycznym przykładem na funkcję rekursywną i iteracyją jest funkcja obliczająca silnię. Mamy do dyspozycji dwie definicje silni: 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ę.