1. Elementy. 1.1 Prosty program w C. Funkcja a procedura. Moze na poczatek pare uwag: jezyk C rozroznia male i duze litery. Instrukcje zwykle koncza sie srednikiem (ale blok juz nie). /* To co jest miedzy tymi znakami / jest komentarzem */ /* ale uwaga: komentarz sie zaczyna i konczy sekwencjami dwuznakowymi, sama * ani sam / nie jest znakiem poczatku ani konca komentarza. */ #include /* To kaze kompilatorowi wlaczyc w tym miejscu plik "stdio.h" z katalogu "include", tak jakby byl on tu wpisany. Wiecej o dyrektywach bedzie w 1.5; dyrektywa NIE MA srednika */ int main(void) { /* Funkcja (w C procedura to tez funkcja, o wyniku typu "void", co oznacza "nic") main musi byc w kazdym programie w C - chyba ze nie chcemy uzywac standardowej biblioteki ani kodu startujacego, ale to juz do specjalnych zastosowan. Jesli program sklada sie z wielu plikow zrodlowych, to funkcje main powinien miec tylko jeden z nich. Zwykle deklaruje sie, ze ta funkcja jest typu int (integer), choc DOS bierze z tego wyniku tylko dolne 8 bitow. Nawias po nazwie oznacza, ze to funkcja. Slowo void w nawiasie, ze nie ma parametrow (jego brak oznaczalby, ze nie jest to okreslone). Ogolna postac funcji: typ nazwa(parametry) { tresc }. Nawiasy klamrowe {} oznaczaja blok (wewnatrz nich umieszcza sie tresc funkcji, rowniez wewnatrz takich nawiasow mozna umiescic wiele instrukcji tam, gdzie bez tego moglaby byc tylko jedna. O mozliwych parametrach funkcji main bedzie nieco dalej, w 1.2. */ printf("to nie jest elegancki jezyk programowania\n" "ale za to malo jest roboty z pisaniem programu"); /* printf jest funkcja z biblioteki C, ktora formatuje i wypisuje na standardowe wyjscie (stdout), pierwszym jej parametrem jest jest format - i moze to byc po prostu tekst do wypisania, tylko bez znakow '%', bo taki znak zaczyna sekwencje sterujaca - jak juz trzeba go wypisac, trzeba napisac %%, wypisze sie jeden %. Uzycie sekwencji sterujacej wymaga dodatkowych parametrow, ktore beda potraktowane w sposob opisany przez te sekwencje. Jest pare odmian tej funkcji, ktore dzialaja nieco inaczej - przed formatem moze byc dodatkowy parametr, rozne moga byc sposoby przekazywania dalszych parametrow - ale format jest zawsze taki sam, i kazda ma w swojej nazwie "printf". Bardziej szczegolowo bedzie w 1.3. */ return(0); /* To jest najprostszy sposob zakonczenia programu - instrukcja return z funkcji main. Wartosc w nawiasie podaje jaka ma byc wartosc funkcji, i DOS pozwala ja testowac w poleceniu "if errorlevel ..." w plikach .BAT (w zakresie 0-255). Jesli typ funkcji jest void, nalezy uzyc return bez wartosci funkcji. */ } /* i to juz koniec funkcji main i pierwszego programu */ 1.2 Parametry funkcji main() i jak wygladaja tablice w C. Moze na poczatek ogolnie o deklaracji i parametrach funkcji w C: Trzeba rozroznic deklaracje i definicje funkcji - deklaracja ma postac: typ nazwa(parametry); (srednik po nawiasie zamykajacym) i nie zawiera tresci funkcji; w definicji zamiast srednika jest { tresc }. Jesli funkcja byla deklarowana, definicja musi byc zgodna z deklaracja. Deklaracja jest niezbedna, jesli wywolanie funkcji poprzedza jej definicje, ale warto deklarowac wszystkie funkcje gdzies na poczatku, zeby program byl bardziej czytelny. Mozna zadeklarowac funkcje nie deklarujac parametrow - piszac po nazwie pusty nawias - i kiedys w C deklarowalo sie funkcje tylko w taki sposob - ale lepiej jest podawac parametry. C przekazuje parametry przez stos, odkladajac najpierw ostatni, na koncu pierwszy - w ten sposob w pamieci sa one w kolejnosci w jakiej wystepuja przy wywolaniu. Przekazywac mozna wartosc lub adres, ale w przypadku gdy parametr jest tablica - zawsze przekazuje sie adres; rowniez adres zostanie przekazany, jesli jako parametr poda sie nazwe funkcji bez nawiasu (jesli poda sie z nawiasem, to funkcja zostanie wywolana podczas wylicznia parametrow i zwrocona wartosc bedzie parametrem). Nie zaleca sie rowniez przekazywania przez wartosc typow zlozonych. Lista parametrow - podaje sie typ, nazwe (w deklaracji mozna ja pominac), i oddziela przecinkami poszczegolne parametry. Jesli jest pusta, wpisuje sie slowo void (brak czegokolwiek w nawiasie oznacza - w deklaracji - niepodanie parametrow, a nie ich brak). Kiedys (kompilatory C nadal to przyjmuja) podawano parametry tak: typ_funkcji nazwa_funkcji(nazwa_parametru1,nazwa_parametru2) typ_parametru1 nazwa_parametru1; typ_parametru2 nazwa_parametru2; { tresc funkcji } ale wtedy zadeklarowane typy parametrow nie sa przekazywane poza funkcje; zeby przekazywac je i sprawdzac zgodnosc z deklaracja, pisze sie (i dla C++ jest to wymagane) tak: typ_funkcji nazwa_funkcji( typ_parametru1 nazwa_parametru1, typ_parametru2 nazwa_parametru2) { tresc funkcji } tyle, ze w starej wersji jesli kilka parametrow bylo tego samego typu mozna bylo ich typ podac razem, a w nowej nie. Jesli do funkcji przekazuje sie adres, trzeba to zadeklarowac. Jesli przekazuje sie tablice, mozna zadeklarowac parametr tak, jak deklaruje sie tablice (np. int tablica[3]), albo jako adres elementu tablicy (np. int *tablica). Bardziej skompilowane jest deklarowanie funkcji jako parametru - jako wskaznika do funkcji - i rowniez jej uzycie. Wyglada to np. tak: int fun1(int (*fun)(int),int arg) { return((*fun)(arg)); } - uzycie gwiazdki w deklaracji parametru powoduje zadeklarowanie wskaznika, w tresci uzycie fun jako adresu, bez nawiasu wokol *fun kompilator zrozumialby fun jako funkcje, ktorej wartosc jest wskaznikiem do typu int, a nie jako wskaznik do funkcji typu int. Zeby przekazac parametr do funkcji, trzeba przy wywolaniu: - jesli parametr przekazuje sie przez wartosc, podac na danej pozycji wartosc, badz wyrazenie (np. nazwe zmiennej), ktora ma byc przekazana do funkcji; - jesli przez adres, a dany obiekt nie jest ani tablica, ani funkcja, trzeba jego nazwe poprzedzic znakiem &. Ten podzial nie jest scisly: przekazany adres moze byc wynikiem wyliczenia jakiegos wyrazenia, i wtedy w wywolaniu funkcji moze wystapic wyrazenie, choc w deklaracji parametru jest adres. Raczej nalezy myslec o tym jako o typie przekazywanej wartosci - czy jest to typ wskaznikowy (i do jakiego typu), czy nie. W pelnej wersji funkcja main() moze miec 3 parametry: main(int arg_count, char *arg_vector[], char *env_strings[]) Pierwszy z nich jest typu int (w C deklaracja zmiennej podaje najpierw typ, a potem nazwy) i podaje, ile napisow jest w tablicy arg_vector, bedacej drugim parametrem. Jako pierwszym z napisow jest przekazywana nazwa programu - zwykle z pelna sciezka, dalsze sa kolejnymi napisami z linii wywolania programu - jesli jakies sa. Trzeci przekazuje, w analogiczny sposob, ale bez podania ilosci, napisy ze srodowiska (environment). Te tablice zawieraja wskazniki do napisow, i po ostatnim z nich jest zawsze wskaznik NULL - pusty. Deklaracja typu (zamiast *nazwa[] mozna tu rownie dobrze napisac **nazwa, * przed nazwa oznacza ze jest ona traktowana jako wskaznik i wartoscia ma byc to, co jest wskazywane, [] po nazwie oznacza ze jest to tablica, i jesli chodzi o uzywanie tego - nie ma roznicy) oznacza, ze sa to tablice o nieokreslonej dlugosci, ktore zawieraja wskazniki do typu char (=znak). Taki wskaznik moze byc potraktowany jako tablica - tu uwaga: w C tablice indeksuje sie od zera, dlatego arg_vector[1] tym, co uzytkownik nazywa pierwszym parametrem, i np. arg_vector[0][1] jest drugim znakiem z nazwy programu - zwykle ':' (napisy sa reprezentowane w taki sposob, ze wskaznik pokazuje ich pierwszy znak, a po ostatnim jest zero, oznaczajace jego koniec). Roznice miedzy uzyciem * i [] najlepiej widac po takim przykladzie: char *napis1="Ala ma kota"; - deklaruje zmienna napis1, ktora jest wskaznikiem do napisu "Ala ma kota", i w pamieci komputera jest wpisany adres takiego napisu (ta forma deklaracji nadaje jej wartosc poczatkowa), natomiast: char napis2[]="Ala ma kota"; - deklaruje tablice zawierajaca napis "Ala ma kota" (ten napis jest jej wartoscia poczatkowa), i adres napisu nigdzie sie nie zapisuje; dlugosc tablicy jest dlugoscia napisu plus 1 (na konczace 0). Pierwsza roznica jest taka, ze druga forma zajmie w sumie mniej miejsca - o jeden wskaznik, druga taka, ze uzycie nazwy zmiennej co innego oznacza - w pierwszym przypadku bierze wartosc zmiennej, w drugim adres; na napis1 mozna podstawic adres innego napisu przez proste: napis1="Ala ma Asa", natomiast na napis2 mozna co najwyzej przepisac cos innego, i jeszcze trzeba uwazac, zeby sie zmiescilo. Niemniej jednak, jesli uzyje sie napis1 lub napis2 jako parametru np. funkcji printf() - wynik bedzie dokladnie taki sam. W przypadku parametru funkcji main() jeszcze jedna roznica, ktora powoduje, ze nieco wygodniej jest uzywac char **arg_vector, jesli program ma jakies parametry modyfikujace jego dzialanie (czesto podaje sie je ze znakiem '-' na poczatku, ale to juz kwestia umowy, mozna sobie napisac program z opcjami, i +opcja uzywac do wlaczenia, -opcja do wylaczenia), ktore sa podane na poczatku listy parametrow, i ich ilosc moze byc rozna, a po nich nastepuja pozostale parametry w stalej kolejnosci - mozna przetworzyc je na poczatku, a nastepnie zwiekszyc arg_vector o ich ilosc tych opcji, i miec stale parametry w ustalonych miejscach tablicy arg_vector. A z tablica sie nie da. 1.3 Funkcje printf(), scanf() i ich odmiany. printf(char *format, parametr1, parametr2, ...) vprintf(char *format, tablica_parametrow) fprintf(FILE *stream, char *format, parametr, ...) vfprintf(FILE *stream, char *format, tablica_parametrow) sprintf(char *bufor, char *format, parametr, ...) vsprintf(char *bufor, char *format, tablica_parametrow) cprintf(char *format, parametr1, parametr2, ...) (przydalaby sie do kompletu vcprintf, ale chyba takiej nie ma) scanf(char *format, ¶metr1, ¶metr2, ...) (i taki sam zestaw z dodanymi v,f,vf,s,vs,c na poczatku) Funkcje o nazwach zaczynajacych sie od 'v' potrzebuja jednego parametru po formacie - jest on tablica zawierajaca parametry, ktore dla innych funkcji musza byc wyliczone (o ile jakies sa). 'f' przed "printf" oznacza, ze zamiast na stdout pisze sie na stream (i w szczegolnosci mozna tam napisac stdout). 's' oznacza, ze wynik formatowania ma byc wpisany do tablicy znakow, dobrze zeby sie zmiescil, bo sprintf nie ma jak tego sprawdzic. Wartoscia kazdej z tych funkcji jest ilosc wypisanych znakow, a w wypadku bledu pisania (nie w 'c' i 's') - stala EOF. Sa one zdefiniowane w stdio.h, z wyjatkiem cprintf, zdefiniowanej w conio.h. Funkcje printf(),vprintf(),fprintf() mozna latwo zdefiniowac uzywajac vfprintf() - przekazujac stdio jako pierwszy parametr, jesli definiowana funkcja nie jest fprintf (wtedy przekazuje sie pierwszy parametr taki jaki jest), i adres nastepnego parametru po formacie jako trzeci parametr, jesli nie jest nia vprintf. Funkcje bez 'v' maja zmienna ilosc parametrow - robi sie to tak: int printf(const char *format,...) { return(vfprintf(stdio,format,1+&format)); } Wielokropek (...) oznacza, ze moze byc wiecej parametrow, i funkcja sama sie zorientuje, ile ich jest. A znak & oznacza wziecie adresu, w tym wypadku parametru format. Do tego adresu dodaje sie jedynka, i tu ujawnia sie kolejna cecha C: dodawanie do adresu zwieksza go nie o wartosc dodawana, ale dziala tak, jak indeksowanie tablicy, pokazuje nastepny element - czyli 1+&format jest adresem tego, co znajduje sie po wskazniku do formatu - a tam sa dalsze parametry. Slowo 'const' oznacza, ze parametr ma byc stala - funkcja nie moze go zmieniac (o ile w samej funkcji nie jest zadeklarowany inaczej). I chyba trzeba powiedziec przy okazji, jak przekazuje sie parametry do funkcji: sa one odkladane na stos, zaczynajac od ostatniego, tak ze funkcja je dostaje w pamieci w takiej kolejnosci, w jakiej sa napisane (odkladanie na stos odwraca kolejnosc). Funkcja scanf() i jej odmiany robia cos odwrotnego do printf() - rozpakowuja dane, przeczytane np. ze standardowego wejscia (stdin), i umieszczaja je w zmiennych. Ale maja nieco niedogodnosci. Rzecz w tym, ze jesli funkcji scanf() kazemy przeczytac liczbe - to albo zasygnalizuje blad, jesli trafi na cos, co nie jest liczba, albo bedzie wczytywac kolejne linie, jesli beda puste, i nie jest na tyle "madra", by czytajac z klawiatury rozpoznac spacje jako koniec wczytywanej liczby - musi byc koniec linii i juz. Z tych powodow lepiej wczytywac cala linie do bufora i uzywac sscanf() - przynajmniej nie bedzie zadnych niespodzianek, a jak bedzie blad, to mozna pokazac, co sie przeczytalo i zobaczyc, co bylo zle. No, chyba ze wiadomo dokladnie jak bedzie wygladalo to, co ma byc czytane i mamy pewnosc, ze nie bedzie z tym klopotow... A teraz nieco o formacie (nie wszystko, bo tego jest dosc na spora ksiazke). Moze na poczatek o tym, jak w C podaje sie stala typu napis (char *) - tekst napisu umieszcza sie miedzy znakami ". A co jesli taki znak jest potrzebny w napisie? Jest sposob: piszemy \". Jak potrzebny \ - piszemy \\ (mozna sie pomylic przy nazwach plikow w DOS-ie - kompilator zinterpretuje po swojemu pojedyncze \-e!). Podobny jest sposob, by podawac stale typu char - podaje sie w ', np. 'a', i jak trzeba podac apostrof, to tak: '\''. Ale znak \ ma tez inne zastosowanie: pozwala podawac znaki specjalne, np. \n to znak przejscia do nowej linii (LF), \r to powrot do poczatku linii (CR), \t to tabulator, \b - backspace, \a - alarm. A co zrobic, jesli jest potrzebny jakis inny znak specjalny? Zawsze mozna podac jego kod, np. \012 oznacza kod 12 oktalnie (czyli 10, a wiec \n), nalezy podawac 3 cyfry, chyba ze nastepny znak nie jest cyfra, mozna tez heksadecymalnie, np. \x12 oznacza kod 12 hex (czyli 18). Sekwencje sterujace zaczynaja sie od znaku % (i jesli potrzeba taki znak wypisac, powtarza sie go 2 razy (i to juz jest co innego, niz ze znakiem \, sekwencja z \ jest przetwarzana przy kompilacji na pojedynczy znak, a %% po skompilowaniu pozostaje %% i dopiero przy wypisywaniu zamienia sie na pojedynczy). Potem moga byc rozne znaki modyfikujace format, i na koniec znak okreslajacy rodzaj konwersji. Najwazniejsze z tych znakow to: d=i=dziesietnie ze znakiem, u=bez znaku (unsigned), o=oktalnie, x=heksadecymalnie (mozna i X, bedzie duzymi litarami przy wypisywaniu, a wczyta na long przy czytaniu), s=napis(string), c=znak(char), f=zmiennoprzecinkowo (z .), e tez, tylko z wykladnikiem (np. 1.23e12), g=f lub e, G i E jak g i e, p=wskaznik (w zaleznosci od modelu near lub far, ale mozna wymusic jaki sie chce przez modyfikator F/N), n=ilosc znakow przetworzonych ma byc wpisana do zmiennej, ktorej adres jest parametrem, przydaje sie zeby przy czytaniu liczby dowiedziec sie, ile miala cyfr... Modyfikatory: l=long (dla calkowitych), double przy czytaniu liczb zmiennoprzecinkowych, h=short (dla calkowitych) - tych uzywa sie bezposrednio przed typem konwersji, przedtem moze byc opis rozmiaru pola - liczba podajaca ilosc znakow, i po kropce dodatkowa liczba, moze ona podawac ilosc znakow po kropce, minimalna ilosc znakow dla liczby calkowitej (%4.3d wypisze minimum 3 cyfry w polu 4 znakowym), maksymalna ilosc znakow z napisu (np. %.7s), a na poczatku moze byc + (zeby zawsze byl znak, nawet dodatniej), - (przesuniecie w lewo, uzyteczne dla napisow, jak chcemy w polu 20 znakow umiescic napis, a nie wiadomo ile on ma - uzywamy %-20.20s i zawsze dobrze wyjdzie), mozna jeszcze uzyc # (inna forma) lub * zamiast liczby podajacej ilosc znakow (ktorakolwiek) - to wezmie wartosc z parametru... Warto zapamietac przynajmniej formaty d, u, x, c, s, modyfikator l, i sposob z wypisywaniem napisu - to zawsze sie moze przydac... Jak przekazywac parametry? Do printf() zwykle przez wartosc, poza formatem %n, ktory wymaga adresu zmiennej typu int (i o to trzeba sie postarac, piszac & przed nazwa zmiennej), i %s, ktory wymaga adresu napisu (a to akurat wyjdzie samo, jak sie poda zmienna typu char *, albo nazwe tablicy - w C tablic nie mozna przekazywac przez wartosc, i jest przekazywany adres). Do scanf() - wszystko przez adres (chyba ze jest * w modyfikatorze formatu, ale po co?), wiec zwykle trzeba pisac & przed nazwami zmiennych, na ktore ma byc cos wczytane - wyjatkiem sa oczywiscie typy wskaznikowe (np. char *), jesli wartosc zmiennej jest adresem, pod ktory mamy wczytac. Wartoscia funkcji printf i podobnych jest ilosc wypisanych znakow, wartoscia scanf jest ilosc poprawnie przetworzonych parametrow (nie wlicza sie do nich zapytanie o ilosc znakow). Zeby sprawdzic, ze przetworzony napis byl liczba i nie bylo po niej nic wiecej, mozna uzyc funkcji sscanf z formatem np. "%ld%c", podajac jako kolejne parametry adresy zmiennych typu long i int, i sprawdzajac, czy wynik jest 1 (0 oznacza brak liczby, 2 jakies znaki po niej), lub "%ld%n", wynik powinien byc 1, i zmienna typu int powinna przybrac wartosc rowna dlugosci napisu (napis[zmienna]==0). 1.4 Stale, zmienne i typy. Tak jak wiekszosc jezykow programowania, C posiada typy zmiennych calkowitych (char, int, short, long) i zmiennoprzecinkowych (float, double, long double). Zakres calkowitych moze zalezec od komputera i od uzywanego kompilatora, w zasadzie przyjmuje sie, ze: char jest typem calkowitym o zakresie najbardziej odpowiednim na pojedynczy znak tekstu, dajacym sie adresowac - zwykle adresy kolejnych elementow tablicy tego typu roznia sie o 1; int jest typem najbardziej odpowiednim dla procesora do operacji na liczbach calkowitych; long jest nie mniejszy od int, i nie mniejszy niz 32-bitowy short jest nie wiekszy od int, zwykle 16-bitowy. Zwykle liczby calkowite sa ze znakiem (mozna podac, ze char ma byc bez znaku, ale lepiej tego unikac, bo cos moze zaczac zle dzialac), mozna nazwe typu poprzedzic przez unsigned i uzyskac typ bez znaku. Dla wiekszosci kompilatorow na PC zakresy zmiennych calkowitych sa: typ unsigned ze znakiem char 0..255 -128..127 int 0..65535 -32768..32767 short 0..65535 -32768..32767 long 0..4294967295 -2147483648..2147483647 ale dla 32-bitowych zakres int moze byc taki, jak long. Jezeli napisze sie jakas liczbe, kompilator zwykle uznaje, ze ma ona typ int. Jesli jest istotne, aby miala jakis inny, mozna uzyc litery L po liczbie, aby byla long, U zeby byla unsigned, lub LU zeby byla unsigned long. Moze to miec znaczenie przy porownaniach, latwo zrobic blad deklarujac zmienna jako unsigned i sprawdzajac, czy ma wartosc -1 (moze miec -1U, ale -1 nie). A jesli jest bardzo wazne, by miec typ char lub unsigned char - trzeba rzutowac typ. Wyliczajac wyrazenie calkowite kompilator dla kazdego dzialania wybiera typ, zamienia operandy na ten typ, i wtedy wylicza. Jesli ktorys z operandow jest typu krotszego od int - zamieni go na int, nastepnie jesli ktorys jest long - zamieni drugi na long, i jesli teraz ktorys jest unsigned - potraktuje drugi jako unsigned. Tyle teorii, praktyka pokazuje, ze suma dwoch unsigned jest mniejsza od zera, jesli nastapilo przeniesienie, ale unsigned long juz nie. Co do zakresu zmiennoprzecinkowych sa jakies standardy: float jest typem 32-bitowym (3.4e-38..3.4e+38) double jest typem 64-bitowym (1.7e-308..1.7e+308) long double nie jest standardem, jak jest na PC to 80-bitowy (3.4e-4932..1.1e+4932; zakresy sa podane we FLOAT.H). Przy dzialaniach, jesli jest argument zmiennoprzecinkowy, oba sa zamieniane na double (na PC na long double; jesli byl krotszy od int, jest zamieniany poprzez int). Wynik zwykle zamienia sie na double (jesli np. ma byc uzyty jako parametr funkcji, a nie jest okreslone jaki typ parametru jest wymagany). Od kazdego typu mozna utworzyc wskaznikowy, piszac w deklaracji zmiennej gwiazdke przed nazwa zmiennej (nieco to sie komplikuje, jesli typem zmiennej ma byc adres funkcji), mozna tez tworzyc adres do zmiennej wskaznikowej piszac dwie gwiazdki... Mozna definiowac wlasne typy strukturalne - wyglada to np. tak: struct nazwa_struktury { typ1 zmienna1,zmienna2; typ2 zmienna3; } nazwa_s1,nazwa_s2; - cos takiego zadeklaruje zmienne nazwa_s1 i nazwa_s2 jako struktury zawierajace pola o podanych nazwach i typach, mozna sie do nich odwolywac np. przez nazwa_s1.zmienna1. Zamiast struct mozna uzyc union - roznica jest taka, ze w struct kazdy element ma oddzielne miejsce w pamieci, a w union wspolne - union przydaje sie, jesli chcemy do tych samych komorek w pamieci odwolywac sie przez rozne nazwy zmiennych o roznych typach. Definicje typu i zmiennej wygladaja prawie tak samo - roznia sie tylko slowem "typedef" w definicji typu - definiujac zmienna mozna za to uzyc jednego ze slow okreslajacych "storage class": auto (jest tworzona na stosie przy wejsciu do funkcji - tylko zmienne wewnatrz funkcji), static (jest pamietana na stale, dostepna tylko w funkcji, ktora ja definiuje, lub w funkcjach wpisanych w pliku, w ktorym jest zdefiniowana - jesli poza funkcja), extern (jest zdefiniowana w innym pliku), register (jesli mozliwe, w rejestrze procesora - nie ma adresu). Jesli nie jest uzyte zadne z nich, to zmienne wewnatrz funkcji sa auto, a zdefiniowane poza funkcja - dostepne przez deklaracje extern z innych plikow. W odniesieniu do funkcji mozna uzyc static (funkcja dostepna tylko w tym pliku), albo extern (tylko w deklaracji - ze jest w innym pliku, ale jak sie go nie poda, kompilator i tak przyjmie extern). Uwaga: C ma jakies wlasne funkcje i zmienne, przypadkowe uzycie nazwy identycznej z ktoras z nazw funkcji lub zmiennych C moze spowodowac fatalne skutki jesli nazwa jest dostepna z zewnatrz (jest nazwa funkcji lub zmiennej definiowanej poza funkcja, i nie jest static), i na dodatek oznacza zupelnie co innego. Przyklad: typ charp = char * i zmienne c1 i c2 typu char oraz cp1 i cp2 typu char * mozna zdefiniowac w taki sposob: typedef char *charp; char c1,*cp1,c2; charp cp2; A oto pare deklaracji funkcji i zmiennych wskazujacych na nie: char fc(char *); /* funkcja typu char, parametr char * */ char *fcp(char *); /* tu funkcja typu char * */ char (*pfc)(char *)=fc; /* a to zmienna typu wskaznika do tej pierwszej funkcji, z nadana wartoscia poczatkowa */ char *(*pfcp)(char *); /* i zmienna typu wskaznika do drugiej */ Przy definiowaniu struktur i unii mozna sie obejsc bez typedef - zawsze struct nazwa albo union nazwa bedzie oznaczac ich typ, ale mozna tez uzyc typedef do nadania innej nazwy strukturze: typedef struct nazwa1 { cos tam } nazwa2; i teraz nazwa2 oznacza to samo, co struct nazwa1. Mozna sobie zdefiniowac nazwy typow zmiennych bez znaku, np.: typedef unsigned char byte,BYTE,*bptr; /*dwie nazwy i wskaznik*/ typedef unsigned short word; typedef unsigned long ulong,dword; No i zagadnienie gdzie mozna umieszczac te definicje - mozna gdziekolwiek "nie w srodku" czegos innego - ale zwykle grupuje sie je gdzies na poczatku pliku zrodlowego, po dyrektywach #include i #define, i w kolejnosci: typedef, zmienne, deklaracje funkcji; i mozna wewnatrz dowolnego bloku (ale jesli w bloku sa wykonywalne instrukcje, to tylko przed nimi), poza danymi. Pisownia stalych: zmiennoprzecinkowe powinny zawierac kropke (po tym sie je rozpoznaje) i moga po e miec wykladnik. Calkowite moga byc pisane w roznych notacjach: oktalnie, dziesietnie, hex - i to trzeba jakos podac. Zasady sa takie: jak sie zaczyna od zera, to jest oktalnie, jak od 0x, to hex, jak od cyfry >0 - dziesietnie. Umieszczenie na koncu L daje stala long, U - unsigned (mozna i UL). Stale typu char pisze sie w apostrofach, np. 'a'. Niektore stale trzeba zapisywac z \, np. '\'' oznacza apostrof, '\n' koniec linii. Stale typu char * pisze sie w cudzyslowie, np. "jezyk C". Tu jesli w stalej ma wystapic cudzyslow trzeba go poprzedzic znakiem \. Te stale w pamieci komputera maja zero na koncu napisu (mozna uzyskac stala bez zera na koncu, jesli jest to wartosc poczatkowa tablicy, i to konczace zero juz sie w niej nie miesci - kompilator wtedy jeszcze nie protestuje, nie zaprotestuje tez jesli tablica jest dluzsza niz potrzeba), i jest ono wykorzystywane przez funkcje ze string.h (i nie tylko) do rozpoznawania konca napisu. Jak sie chce, mozna zapisac jeden napis zawierajacy zera w srodku - ale wtedy trzeba jakos poznawac, ze to nie jest koniec np. "Ala\0ma\0kota\0" - tu na koncu beda dwa zera, mozna uzyc funkcji strlen(), zeby dostac dlugosc napisu, dodac do wskaznika dlugosc i jedynke, znowu uzyc strlen(), i powtarzac az wyjdzie dlugosc 0. Taka strukture maja zmienne srodowiska (environment) w systemie DOS na PC. I jeszcze taki problem: co robic, jak stala typu char * jest za dluga na jedna linie? W C mozna wprawdzie pisac bardzo dlugie linie, ale to sie zle czyta. Dlatego mozna pisac ja w kawalkach, konczac kazdy kawalek i zaczynajac nastepny cudzyslowem. Innym sposobem mogloby byc kontynuowanie linii, ale to gorzej wyglada. 1.5 Dyrektywy preprocesora. W odroznieniu od instrukcji, ktore moga byc wieloliniowe, i koncza sie srednikiem, dyrektywy sa jednoliniowe - jesli nie mieszcza sie, trzeba napisac \ na koncu linii i kontynuowac w nastepnej, i nie maja srednika na koncu. Zaczynaja sie znakiem # na poczatku linii. W programie przykladowym wystapila dyrektywa #include - zeby wlaczyc do programu definicje z pliku stdio.h z katalogu "include". Moze jeszcze male uzupelnienie: jesli nazwa pliku jest w <>, to plik bedzie poszukiwany tylko w tym katalogu, jesli w "" - to na poczatku kompilator poszuka go w aktualnym katalogu (lub w podanym, jesli nazwa jest podana ze sciezka) - tej formy nalezy uzyc, jesli tworzy sie wlasny plik z definicjami. Inna czesto uzywana dyrektywa jest #define - sluzy do zdefiniowania nazwy, ktora wszedzie w tekscie programu zostanie zastapiona przez to, co sie zdefiniuje. Mozna napisac po prostu tak: #define Pi 3.1415926536 /* liczba Pi (to tez jest w definicji) */ i lepiej nie napisac srednika w tej linii... no, akurat liczby Pi moze sie za czesto nie uzywa tak, by dalo to blad niewykrywalny przy kompilacji, ale jesli zdefiniuje sie przesuniecie skali Celsjusza #define Celsjusz 273.16; i napisze instrukcje: temp_kelvina=Celsiusz+temp_celsiusza; to wynikiem bedzie temp_kelvina=273.16;+temp_celsiusza; i na temp_kelvina zawsze bedzie wpisywane to samo; moze kompilator da ostrzezenie, ze wyrazenie +temp_celsiusza; nie ma efektu... Ale mozna zdefiniowac cos wiecej (i wtedy o wiele latwiej zrobic podobny blad, i wiele innych): wyrazenie z parametrami, np. #define szescian(x) x*x*x to akurat jest z bledem: jesli napisze sie szescian(1+2), wyjdzie 1+2*1+2*1+2, co po wyliczeniu daje 7, a nie, jakby sie chcialo, 27. Zeby wyszlo "dobrze", trzeba uzywac nawiasow wokol x: #define szescian(x) (x)*(x)*(x) Wyrazenie moze miec wiele parametrow, oddzielonych przecinkami. Nie wolno jednak uzyc w nim spacji w pierwszej czesci, okreslajacej co jest definiowane, bo pierwsza spacja oznacza koniec tej czesci. Mozna tez napisac definicje pusta, np.: #define puste(x,y), zeby takie wyrazenia w programie byly traktowane tak jakby ich nie bylo. Jest tez dyrektywa #undef, do usuwania definicji z #define. Do kompilacji warunkowej sa: #ifdef, #ifndef, #if, #else, #endif. #ifdef nazwa powoduje, ze nastepujace po niej linie zostana zignorowane, jesli nazwa nie jest zdefiniowana; #ifndef ma odwrotne dzialanie, #if wyrazenie wylacza kompilacje, jesli wartoscia wyrazenia jest zero. Zakres ktorego to dotyczy, konczy sie na dyrektywie #else, ktora odwraca wynik odpowiadajacej jej (bo te dyrektywy mozna zagniezdzac) dyrektywy #if (lub #ifdef, #ifndef), lub #endif, ktora konczy zakres dzialania kompilacji warunkowej. Przykladem zastosowania moze byc uzaleznienie kompilowania pewnych fragmentow od systemu, lub uzytego kompilatora - np. pisanie z adresowaniem ekranu w DOS-ie na PC mozna zrobic uzywajac definicji z conio.h, a w Unix-ie tego nie ma, uzywa sie definicji z curses.h, ktorych z kolei nie ma w DOS-ie. Sa one rowniez stosowane w plikach wlaczanych przez #include - powtorne wlaczenie tego samego pliku jest wykrywane (pierwsze definiuje jakas nazwe przez #define, nastepne sprawdzaja przez #ifdef) i powoduje zignorowanie tego pliku - zeby oszczedzac czas. 1.6 Operatory i wyrazenia. W jezyku C wystepuja nastepujace operatory, podane tu w kolejnosci priorytetu (jesli nie jest podane inaczej, przez P w komentarzu, lacznosc jest lewostronna - czyli jesli sa rownorzedne operatory, najpierw wykonuje sie lewy), od najwyzszego do najnizszego: /* nawiasy, wybor elementu */ () [] -> . /* jednoargumentowe (P) */ ! ~ - ++ -- (typ) * & sizeof /* mnozenie/dzielenie/mod */ * / % /* dodawanie/odejmowanie */ + - /* przesuniecie bitow */ << >> /* porownywanie */ < <= > >= /* porownywanie */ == != /* iloczyn bitowy */ & /* roznica bitowa */ ^ /* suma bitowa */ | /* iloczyn logiczny */ && /* suma logiczna */ || /* wybor wyrazenia (P) */ ?: /* przypisanie (P) */ = operator= /* wyliczanie kolejne */ , Jak widac jest tego calkiem duzo. Niektore wymagaja omowienia. Nawias [] sluzy do wyboru elementu tablicy, kropka do wyboru pola w strukturze, -> tez wybiera pole w strukturze, ale po lewej jego stronie powinien byc wskaznik do struktury - i ze wzgledu na to, ze te operatory maja najwyzszy priorytet, jest on potrzebny zeby nie trzeba bylo uzywac nawiasow, zeby dostac sie do elementu struktury, kiedy mamy jej adres (np. przekazany jako parametr funkcji). Negacja logiczna ! (1 jesli argument jest 0, inaczej 0), bitowa ~ (wszystkie bity odwrocone, arytmetyczna -; zwiekszanie i zmniejszanie o 1 (++ --, te operatory wyjatkowo moga wystapic rowniez po swoim argumencie, i wtedy dzialaja po jego uzyciu w wyrazeniu, czyli jesli i bylo 1, to ++i ma wartosc 2, a i++ - 1, i w kazdym wypadku stanie sie 2); rzutowanie typu (casting) wykonuje sie przez (typ); przez * mozna dostac to, co wskazuje argument, przez & mozna dostac adres argumentu (wiec *&arg znaczy to samo, co arg; (*arg).pole to samo, co arg->pole; arg.pole to samo, co (&arg)->pole - nawiasy konieczne); sizeof zwraca rozmiar argumentu w bajtach. Przesuwanie bitow - przyklady: 1<<3 jest 8, 5>>1 jest 2. Porownywanie: == znaczy rowne, != nierozne, inne sa oczywiste. Moze mniej oczywiste jest to, ze wynikiem porownanie jest 0 lub 1 (0 jesli warunek nie jest spelniony, 1 jesli jest spelniony). Iloczyn i suma logiczna (nie mylic z bitowymi): wynik jest 0 lub 1; dla iloczynu 0 jesli ktorys argument jest 0, dla sumy 1 jesli jest nie 0; jesli argumenty sa wyrazeniami, a po wyliczeniu pierwszego wiadomo jaki bedzie wynik, to drugie nie jest wyliczane - to moze byc wazne, jesli wyliczenie ma efekt uboczny, bo zawiera operator np. ++, albo wywoluje funkcje. Wybor wyrazenia wyglada tak: wyrazenie1 ? wyrazenie2 : wyrazenie3, i jesli wyrazenie1 jest niezerowe, to wylicza sie wyrazenie2, i to, co wyjdzie, jest wartoscia; a jesli zerowe, wylicza sie wyrazenie3, i to, co wyjdzie, jest wartoscia. Pozwala to zdefiniowac wybieranie mniejszego i wiekszego elementu (definicje sa w stddef.h): #define min(a,b) ( (a)<(b) ? (a) : (b) ) i analogicznie wiekszego, ale moze to zle zadzialac, jesli a lub b jest wyrazeniem, ktorego wyliczenie daje efekt uboczny, ktory jest potrzebny raz - bo moze zadzialac dwa razy; przykladem moze chyba byc czytanie znaku i wypisywanie spacji jesli jest < spacji: putch(max(getch(),' '); bo jak poda sie znak wiekszy od spacji, wczyta nastepny, i juz bez sprawdzania, czy jest mniejszy czy wiekszy wyswietli go. Wyliczanie - warto zwrocic uwage, ze przecinki oddzielajace zmienne w deklaracji (mozna im od razu nadac wartosci poczatkowe), lub parametry funkcji, nie sa operatorami kolejnego wyliczania, i kolejnosc wyliczania ich wartosci nie jest okreslona (i nawet w przypadku parametrow funkcji jest z reguly odwrotna): wylicza sie po kolei podane wyrazenia, wynik ostatniego jest wartoscia calosci. Ma mniejszy priorytet niz przypisanie - pozwala bez uzycia nawiasow napisac kilka przypisan jako jedna instrukcje. I przypisanie: fakt, ze to jest operator dopuszczalny w wyrazeniu, powoduje ze mozna wewnatrz wyrazenia dokonac dodatkowych przypisan, tylko trzeba uzywac nawiasow. Przyklad zastosowania: while((c=getchar())!=EOF) { ... (cos z przeczytanym znakiem) } a mozna i tak: while(c=getchar(),c!=EOF) { ... } Ale przypisanie, oprocz prostego =, moze uzyc operatorow: *= /= %= += -= <<= >>= &= ^= |= i dziala to w ten sposob, ze z tym, co jest po lewej stronie takiego operatora, wykonuje sie dzialanie dwuargumentowe (drugim argumentem jest wartosc wyrazenia po prawej stronie), i wynik zapisuje na to, co jest po lewej stronie, np. a+=2; dodaje do a 2. Ale to nie jest to samo, co napisanie a=a+2; w przypadku, gdy a jest wyrazeniem (sa pewne ograniczenia co do wyrazen, jakie moga wystapic po lewej stronie operatora przypisania - musi ono okreslac cos, na co mozna cos przypisac, np. element tablicy), jego wyliczenie wykona sie raz, a nie dwa razy, co da zauwazalny skutek jesli a bedzie s[i++]: s[i++]+=1; - zwiekszy s[i] o 1, a nastepnie i tez o 1; s[i++]=s[i++]+1; - najprawdopodobniej wezmie s[i], doda 1, doda 1 do i, zapisze wartosc pierwszej sumy na s[i] (ale juz z nowym i), i znow doda 1 do i. s[i]=s[i]+1; i++; - dziala tak, jak pierwsze, poza tym, ze adres s[i] wylicza sie dwa razy, zamiast raz. 1.7 Instrukcje sterujace. if(wyrazenie) instrukcja; - istrukcja wykona sie, jesli wyliczenie wyrazenia da wartosc rozna od zera; if(wyrazenie) instr1; else instr2; - w zaleznosci od tego, czy wyrazenie bedzie nie zero, czy zero, wykona sie instr1 lub instr2. Warto zauwazyc, ze w C instrukcja przed else konczy sie srednikiem (chyba, ze jest to blok w {}, wtedy nie ma srednika po nawiasie }). Jesli wyrazenie zawiera przypisanie, to warto pisac to tak: if((a=b)!=0) ..., bo przy if(a=b) ... kompilator bedzie alarmowal, ze byc moze zostal zrobiony blad, ze moze mialo byc if(a==b) ... Jesli jest kilka if-ow, i potem jakies else, kompilator odnosi to else do najblizszego if-a, do ktorego jeszcze nie przypisal else, jesli ma byc inaczej, to trzeba to zaznaczyc nawiasami klamrowymi. Jesli potrzebna jest sekwencja instrukcji typu: if(i==1) instr1; else if(i==2) instr2; else if(i==3) instr3; ... to mozna to zapisac inaczej uzywajac instrukcji switch: switch(i) { case 1: instr1; break; case 2: instr2; break; ... } Instrukcja break wewnatrz switch ma specjalne znaczenie - konczy wykonywanie switch, bez niej po instr1 wykonalaby sie instr2. Za to nie trzeba pisac nawiasow klamrowych, jesli ma byc kilka instrukcji dla jakiejs wartosci i (te wartosci musza byc calkowite). Mozna tez umiescic wewnatrz switch etykiete default - jesli wartosc i nie bedzie zadna z podanych, wykonaja sie intrukcje po tej etykiecie. Ale jej brak nie powoduje bledu jesli wartosc i nie "trafi". Instrukcje petli - najprostsza jest while(wyrazenie) instrukcja; i dopoki wyrazenie jest niezero wykonuje instrukcje, przy czym wylicza wyrazenie na poczatku i po kazdym wykonaniu instrukcji. Mozna jednak chciec przed rozpoczeciem petli ustawic wartosc jakiejs zmiennej i uzywac jej jako licznika powtorzen - do tego jest forma: for(instr1; wyrazenie; instr3) instr2; instr1 wykonuje sie raz na poczatku; wyrazenie jest wyliczane przed kazdym wykonaniem petli i petla nie jest wiecej wykonywana jesli wartosc bedzie zero; w kazdym przejsciu petli wykonuje sie najpierw instr2 a potem instr3. Po co tak jest widac z takiego przykladu: for(i=0; i