Język C zajęcia nr 11 Funkcje W języku C idea podprogramów realizowana jest wyłącznie poprzez definiowanie i wywołanie funkcji. Każda funkcja musi być przed wywołaniem zadeklarowana. Deklaracja funkcji W deklaracji funkcji podawana jest jej nazwa, typ zwracanej wartości (jeżeli funkcja zwraca wartość), liczba i typ argumentów (jeżeli funkcja wymaga argumentów). extern double pierwiastek(double a, double b, double c); extern int podzielnik(int k, int n); extern void rysuj_okrag(float x, float y, float r); extern float podaj_czas( ); Nazwa funkcji w języku C deklarowana bez użycia słowa extern ma z definicji wiązanie zewnętrzne, czyli obszar widzialności nazwy funkcji obejmuje wszystkie pliki źródłowe składające się na program. Oznacza to, że słowo extern w deklaracji funkcji jest zbyteczne. Użycie natomiast słowa static powoduje, że nazwa deklarowanej funkcji uzyskuje wiązanie wewnętrzne. Tak zdefiniowana funkcja może być wywoływana jedynie w obrębie swojego pliku źródłowego. W deklaracji funkcji nie są wymagane nazwy jej argumentów, a jedynie ich typy. Umieszczanie nazw argumentów jest jednak polecane ze względów dokumentacyjnych. double pierwiastek(double,double,double); int podzielnik(int,int); void rysuj_okrag(float,float,float); float podaj_czas( );
Definicja funkcji Funkcja może być wielokrotnie deklarowana, ale w jednym z plików źródłowych musi być umieszczona jej definicja, która ma formę deklaracji uzupełnionej instrukcją złożoną zawierającą deklaracje i instrukcje ciała funkcji. Deklaracja: int nwp(int a, int b); Definicja: int nwp(int a, int b) int p; while(a!=b) p= a>b? a-b : b-a; if(a>b) a=p; else b=p; return p; Definicja funkcji może nie występować w pliku źródłowym. W takim przypadku definicja funkcji musi być dostarczona na etapie konsolidacji programu w postaci modułu półskompilowanego (plik z rozszerzeniem.obj) lub biblioteki (plik z rozszerzeniem.lib). Deklaracja funkcji w każdym przypadku jest jednak wymagana. Uwaga: W bloku funkcji, która zwraca wartość, musi wystąpić instrukcja powrotu return zawierająca wyrażenie. Wywołanie funkcji Wywołanie funkcji dokonywane jest za pośrednictwem operatora wywołania funkcji ( ). Podczas wywołania funkcji następuje przekazanie do niej wartości argumentów (jeżeli funkcja ta wymaga argumentów). W języku C wszystkie argumenty przekazywane są przez wartość, co oznacza, że w momencie wywołania funkcji przydzielana jest pamięć dla jej argumentów formalnych i kolejno każdemu argumentowi formalnemu przypisywana jest wartość odpowiedniego argumentu aktualnego. Uwaga: Argumenty funkcji w obszarze jej bloku są obiektami lokalnymi.
Funkcja może być wywoływana rekurencyjnie, to znaczy wewnątrz bloku funkcji jest ona ponownie wywoływana. Rekurencyjne obliczanie silni: Wprowadź i uruchom program, zinterpretuj tekst programu i wyniki: #include <stdio.h> int silnia(int n)return n?n*silnia(n-1):1; main() int n; printf("podaj liczbe n="); scanf("%d",&n); printf("wartosc silni n!=%d",silnia(n)); Podaj liczbe n=6 KLAWIATURA 6 Wartosc silni n!=720 Wskaźniki na funkcje Szczególnym rodzajem wskaźnika jest wskaźnik na funkcję, który w uproszczeniu można traktować jak adres początku tej funkcji. Równocześnie sama nazwa funkcji ma wartość adresu początku kodu stanowiącego obraz tej funkcji w pamięci. Wywołanie pewnej funkcji poprzez wskaźnik p na tę funkcję można dokonać używając wyrażenia: ( *p ) ( lista_argumentów ) Wskaźnik na funkcję musi być wcześniej zadeklarowany jako wskaźnik odpowiedniego typu, z którego wynika zarówno typ zwracanej przez funkcję wartości, jak i liczba oraz typ poszczególnych argumentów tej funkcji.
Deklaracje wskaźników na funkcje: void (*p1)(); //Wskaznik na funkcje bez //argumentow i nie zwracajaca //wartosci int (*p2)(int,char); //Wskaznik na funkcje o argumentach //int i char, zwracajaca int int *(*p3)(int); //Wskaznik na funkcje o argumencie //int, zwracajaca wskaznik na int double (*(*p4)(int))(char); //Wskaznik na funkcje o argumencie //int, zwracajaca wskaznik na //funkcje o argumencie char, ktora //zwraca double Uwaga: Operacje na wskaźnikach na funkcje podlegają dodatkowemu ograniczeniu: Na wskaźnikach na funkcje nie można wykonywać operacji arytmetycznych. Wskaźniki na tablice i tablice wskaźników Wskaźnik na tablicę należy odróżniać od wskaźnika na początkowy element tej tablicy. Podstawową zasadą, którą należy zawsze stosować, jest reguła działania operatora wyłuskania: Zastosowanie operatora wyłuskania do wskaźnika na pewien obiekt daje w rezultacie ten obiekt. Wskaźnik na tablicę: int tab[6]=11,22,33,44,55,66; int (*p)[6]=&tab; //p jest wskaznikiem na tablice int[6]
W języku C można bezpośrednio zadeklarować jedynie tablicę jednowymiarową. Posługiwanie się tablicami dwuwymiarowymi lub o większej liczbie wymiarów wymaga operowania tablicami tablic. Tablica tablic: Zapis int tab[12][40]; definiuje 12 elementową tablicę 40 elementowych tablic typu int. Wyrażenie tab[k] jest typu int*, ponieważ jest nazwą k-tej tablicy 40 elementów typu int i ma wartość wskaźnika na początkowy jej element. Wskaźnik na tablicę: Wyrażenie tab jest nazwą tablicy 12 elementowej i ma wartość wskaźnika na jej początkowy element, którym jest tablica 40 elementowa. Typem tab jest wskaźnik na 40 elementową tablicę wartości całkowitych, czyli int(*)[40]. Tablica wskaźników: Zapis char *ptr[60]; przedstawia definicją 60 elementowej tablicy wskaźników na wartości typu char. Ponieważ ptr jako nazwa tablicy jest równocześnie wskaźnikiem na jej początkowy element, więc typem ptr jest char**, czyli wskaźnik na wskaźnik na char. Konwersje arytmetyczne Wyrażenia arytmetyczne mogą być konstruowane z obiektów różnych typów. Do właściwego wyliczenia wartości całego wyrażenia konieczne jest dokonanie przekształceń, czyli konwersji arytmetycznej wszystkich elementów wyrażenia do jednego typu. Standardowa konwersja arytmetyczna wykonywana jest automatycznie (zajmuje się tym kompilator) według zasady, że typ młodszy przekształcany jest do typu starszego. Typy arytmetyczne w kolejności starszeństwa to: char, int, long, float, double, long double. Jawna konwersja zachodzi w przypadku użycia operatora rzutowania ( typ ). Wymuszona konwersja związana jest z operatorem przypisania = lub op=. Typ prawego argumentu tego operatora przekształcany jest do typu lewego argumentu. Podobna konwersja zachodzi podczas przekazywania wartości argumentów przy wywołaniu funkcji. Typ argumentu aktualnego jest przekształcany do typu argumentu formalnego określonego w deklaracji funkcji.
Konwersje wskaźnikowe Operacje wykonywane na wskaźnikach podlegają ograniczeniom: Nie można odejmować i porównywać wskaźników różnych typów. Argumentami operatora przypisania nie mogą być wskaźniki różnych typów. Do wykonania tych operacji konieczne jest użycie odpowiedniego operatora jawnej konwersji. Wskazaniu adresowemu, czyli wskaźnikowi typu void* można przypisać dowolny wskaźnik bez konieczności wykonania konwersji. Konwersja wskaźnikowa: int tab[6]=11,22,33,44,55,66; // int (*p)[10]=&tab; Blad niezgodnosci typow int (*p)[10]=(int(*)[])&tab; //Jawna konwersja wskaznikowa Operatory języka C c.d. Operator wywołania funkcji Dwuargumentowym operatorem wywołania funkcji jest para nawiasów ( ). Pierwszy argument arg1 występuje z lewej strony pary nawiasów i ma wartość nazwy funkcji, a drugi argument arg2 zawarty jest wewnątrz pary nawiasów i reprezentuje argumenty przekazywane do wywoływanej funkcji. arg1 ( arg2 ) Drugi argument arg2 operatora ( ) to lista argumentów wywołania funkcji. Pierwszym argumentem arg1 musi być wyrażenie, którego wartością jest wywoływana funkcja. Najczęściej jest to wprost nazwa funkcji lub wyłuskanie wskaźnika na daną funkcję.
Wyrażenie: ( *p ) ( lista ) wywołuje funkcję, na którą wskazuje zmienna p. Uwaga: Występująca w wyrażeniu nazwa funkcji bez operatora jej wywołania (czyli bez pary nawiasów po nazwie funkcji) ma wartość stałego wskaźnika na początek tej funkcji. Wprowadź i uruchom program, zinterpretuj wyniki: #include <stdio.h> void main() int (*p)(int)=putchar; (*p)('k'); (*p)('o'); (*p)('t'); //Definicja wskaznika p na funkcje //o argumencie int i zwracajaca int //i przypisanie mu adresu putchar kot Operatory bitowe Do wykonywania operacji na bitach binarnego przedstawienia liczb całkowitych w pamięci komputera wykorzystywane są operatory: << Przesunięcie w lewo. Wartością wyrażenia a<<b jest liczba całkowita, której reprezentacja dwójkowa ma układ bitów argumentu a przesuniętych w lewo o b pozycji binarnych. Zwalniane bity z prawej strony uzupełniane są zerami. >> Przesunięcie w prawo. Wartością wyrażenia a>>b jest liczba całkowita, której reprezentacja dwójkowa ma układ bitów argumentu a przesuniętych w prawo o b pozycji binarnych. Zwalniane bity z lewej strony uzupełniane sa zerami dla typu unsigned oraz wartością bitu znakowego dla typu signed.
Bitowa alternatywa. Wartością wyrażenia a b jest liczba całkowita, która powstaje przez wykonanie niezależnych operacji alternatywy na korespondujących bitach obydwu argumentów. & Bitowa koniunkcja. Wartością wyrażenia a&b jest liczba całkowita, która powstaje przez wykonanie niezależnych operacji koniunkcji na korespondujących bitach obydwu argumentów. ^ Bitowa różnica symetryczna. Wartością wyrażenia a^b jest liczba całkowita, która powstaje przez wykonanie niezależnych operacji różnicy symetrycznej na korespondujących bitach pierwszego i drugiego argumentu. ~ Bitowa negacja. Wartością wyrażenia ~b jest liczba całkowita, która powstaje przez wykonanie niezależnych operacji negacji na poszczególnych bitach argumentu. Jeżeli wartości zmiennych char a,b; mają układ bitów: a 00010101 b 11001100 to wyniki operacji bitowych mają układy bitów: a<<2 01010100 a>>3 00000010 a b 11011101 a&b 00000100 a^b 11011001 ~a 11101010
Wprowadź i uruchom program, zinterpretuj wyniki: #include <stdio.h> void main() int n=100; printf( %d\n,n<<2); // efekt mnozenia przez 4 printf( %d\n,n>>3); // efekt trzykrotnego dzielenia // calkowitego przez 2 Wprowadź i uruchom program, zinterpretuj wyniki: #include <stdio.h> void main() int n=100; printf( %d\n,n<<=2); printf( %d\n,n>>3); Operator połączenia Para wyrażeń może być rozdzielona operatorem połączenia,. Wyrażenie: wyr1, wyr2 w którym wyr1 i wyr2 są dowolnymi wyrażeniami ma wartość prawego wyrażenia wyr2. Operator połączenia pozwala umieścić dwa lub więcej wyrażeń w miejscu, gdzie może wystąpić jedno wyrażenie lub jedna instrukcja. if(tab[k]>tab[k+1]) x=tab[k]; tab[k]=tab[k+1]; tab[k+1]=x; if(tab[k]>tab[k+1]) x=tab[k], tab[k]=tab[k+1], tab[k+1]=x; Uwaga: Przecinki oddzielające wyrażenia na liście argumentów wywołania funkcji lub występujące na liście wartości inicjalizatora nie są operatorami połączenia.
Zadania: 1. Zmienna n typu int ma wartość 100, wyznacz wartość n po wykonaniu instrukcji: n=n<<3; n>>=3; n=--n>>1; n=n>>1>>1; 2. a) Napisz funkcję los, która otrzymuje jako argument tablicę typu double o rozmiarze n i wypełnia ją liczbami pseudolosowymi z przedziału [-1, 1], przeprowadzając wcześniejszą inicjalizację generatora pseudolosowego wartością funkcji time; parametrami funkcji są wskaźnik na początek tablicy oraz n; funkcja nie zwraca żadnej wartości. b) Napisz funkcję min, która znajduje najmniejszy element tablicy typu double o rozmiarze n; parametrami funkcji są wskaźnik na początek tablicy oraz n; funkcja zwraca wartość znalezionego minimum. c) Napisz program zawierający definicje w/w funkcji los i min, w funkcji main przeprowadza alokację n-elementowej tablicy typu double (n jest podawane przez użytkownika), następnie wypełnia ją korzystając z funkcji los i znajduje minimum wartości jej elementów korzystając z funkcji min, a następnie drukuje (w funkcji main) to minimum.