Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00250 005405 13644892 na godz. na dobę w sumie
Język ANSI C. Programowanie. Wydanie II - książka
Język ANSI C. Programowanie. Wydanie II - książka
Autor: , Liczba stron: 328
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-246-2578-9 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> c - programowanie
Porównaj ceny (książka, ebook (-25%), audiobook).

Zobacz ćwiczenia do tej książki >

Drogi Czytelniku, właśnie trzymasz w rękach nowe wydanie książki zaliczanej do klasyki literatury informatycznej. Napisana przez autorów języka ANSI C w najlepszy możliwy sposób przedstawia arkana tego języka. A co można powiedzieć o samym języku? To też klasyka. To język wymagający systematyczności i skupienia, ale dający w zamian wiele możliwości i świetne wyniki. To najczęściej nauczany język programowania - jego znajomość stanowi znakomity fundament do poznania kolejnych, bardziej złożonych języków. Mimo swojego zaawansowanego wieku jest on ceniony i w wielu dziedzinach wciąż niezastąpiony.

Dzięki tej książce zdobędziesz kompletną wiedzę na temat języka C. Poznasz wszystkie dostępne typy, operatory i wyrażenia. Nauczysz się sterować wykonywaniem programu oraz wykorzystywać funkcje. Ponadto dogłębnie poznasz coś, co sprawia początkującym programistom najwięcej problemów - wskaźniki. Następnie zapoznasz się także z funkcjami wejścia i wyjścia. Dowiesz się, jak uzyskać dostęp do plików, formatować dane wyjściowe oraz obsługiwać błędy. Książka ta jest bogata w przykłady, a każdy z nich został przetestowany przez autorów. 'Język ANSI C. Programowanie. Wydanie II' to niezastąpiona pozycja na półce każdego studenta informatyki, pasjonata programowania i zawodowca. Wraz z książką został wydany zeszyt zawierający rozwiązania do wszystkich zawartych w niej ćwiczeń.

Poznaj tajniki języka C!

Znajdź podobne książki Ostatnio czytane w tej kategorii

Darmowy fragment publikacji:

Jêzyk ANSI C. Programowanie. Wydanie II Autorzy: Brian W. Kernighan, Dennis M. Ritchie T³umaczenie: Pawe³ Koronkiewicz ISBN: 978-83-246-2578-9 Tytu³ orygina³u: C Programming Language (2nd Edition) Format: 158×235, stron: 328 Drogi Czytelniku, w³aœnie trzymasz w rêkach nowe wydanie ksi¹¿ki zaliczanej do klasyki literatury informatycznej. Napisana przez autorów jêzyka ANSI C w najlepszy mo¿liwy sposób przedstawia arkana tego jêzyka. A co mo¿na powiedzieæ o samym jêzyku? To te¿ klasyka. To jêzyk wymagaj¹cy systematycznoœci i skupienia, ale daj¹cy w zamian wiele mo¿liwoœci i œwietne wyniki. To najczêœciej nauczany jêzyk programowania – jego znajomoœæ stanowi znakomity fundament do poznania kolejnych, bardziej z³o¿onych jêzyków. Mimo swojego zaawansowanego wieku jest on ceniony i w wielu dziedzinach wci¹¿ niezast¹piony. Dziêki tej ksi¹¿ce zdobêdziesz kompletn¹ wiedzê na temat jêzyka C. Poznasz wszystkie dostêpne typy, operatory i wyra¿enia. Nauczysz siê sterowaæ wykonywaniem programu oraz wykorzystywaæ funkcje. Ponadto dog³êbnie poznasz coœ, co sprawia pocz¹tkuj¹cym programistom najwiêcej problemów – wskaŸniki. Nastêpnie zapoznasz siê tak¿e z funkcjami wejœcia i wyjœcia. Dowiesz siê, jak uzyskaæ dostêp do plików, formatowaæ dane wyjœciowe oraz obs³ugiwaæ b³êdy. Ksi¹¿ka ta jest bogata w przyk³ady, a ka¿dy z nich zosta³ przetestowany przez autorów. „Jêzyk ANSI C. Programowanie. Wydanie II” to niezast¹piona pozycja na pó³ce ka¿dego studenta informatyki, pasjonata programowania i zawodowca. Wraz z ksi¹¿k¹ zosta³ wydany zeszyt zawieraj¹cy rozwi¹zania do wszystkich zawartych w niej æwiczeñ. (cid:129) Zmienne i wyra¿enia arytmetyczne w jêzyku C (cid:129) Kompilowanie kodu (cid:129) Wykorzystanie preprocesora jêzyka C (cid:129) Typy i operatory (cid:129) Metody sterowania wykonywaniem programu (cid:129) Wykorzystanie funkcji (cid:129) Struktura programu (cid:129) Zasada dzia³ania wskaŸników (cid:129) Struktury danych (cid:129) Operacje wejœcia i wyjœcia (cid:129) Zastosowanie rekurencji Poznaj tajniki jêzyka C! Spis treĂci Przedmowa Przedmowa do pierwszego wydania WstÚp Rozdziaï 1. Wprowadzenie 1.1. 1.2. 1.3. 1.4. 1.5. 1.6. 1.7. 1.8. 1.9. 1.10. Pierwsze kroki Zmienne i wyraĝenia arytmetyczne Instrukcja for Staïe symboliczne Znakowe operacje wejĂcia-wyjĂcia Tablice Funkcje Argumenty — przekazywanie jako wartoĂÊ Tablice znaków Zmienne zewnÚtrzne i zakres zmiennych Rozdziaï 2. Typy, operatory i wyraĝenia Typy danych i ich rozmiar Staïe 2.1. Nazwy zmiennych 2.2. 2.3. 2.4. Deklaracje 2.5. Operatory arytmetyczne 2.6. Operatory porównania i logiczne 2.7. 2.8. 2.9. Operatory bitowe 2.10. Operatory i wyraĝenia przypisania Konwersja typów Inkrementacja i dekrementacja 7 9 11 15 16 18 24 26 26 34 36 40 41 44 49 49 50 51 54 55 56 57 61 63 65 JĊzyk ANSI C. Programowanie 2.11. Wyraĝenia warunkowe 2.12. Priorytety operatorów i kolejnoĂÊ wykonywania obliczeñ Rozdziaï 3. Sterowanie wykonywaniem programu 3.1. 3.2. 3.3. 3.4. 3.5. 3.6. 3.7. 3.8. Instrukcje i bloki if-else else-if switch PÚtle while i for PÚtla do-while break i continue goto i etykiety Rozdziaï 4. Funkcje i struktura programu 4.1. 4.2. 4.3. 4.4. 4.5. 4.6. 4.7. 4.8. 4.9. 4.10. 4.11. Funkcje — podstawy Zwracanie wartoĂci innych niĝ int Zmienne zewnÚtrzne Zakres Pliki nagïówkowe Zmienne statyczne Zmienne rejestrowe Struktura blokowa Inicjalizacja Rekurencja Preprocesor jÚzyka C Rozdziaï 5. Wskaěniki i tablice Arytmetyka adresów 5.1. Wskaěniki i adresy 5.2. Wskaěniki i argumenty funkcji 5.3. Wskaěniki i tablice 5.4. 5.5. Wskaěniki znakowe i funkcje 5.6. 5.7. 5.8. 5.9. Wskaěniki a tablice wielowymiarowe 5.10. Argumenty wiersza poleceñ 5.11. Wskaěniki do funkcji 5.12. Tablice wskaěników, wskaěniki do wskaěników Tablice wielowymiarowe Inicjalizacja tablic wskaěników Rozbudowane deklaracje zmiennych i funkcji Rozdziaï 6. Struktury Struktury — podstawy Struktury i funkcje Tablice struktur 6.1. 6.2. 6.3. 6.4. Wskaěniki do struktur 6.5. Struktury cykliczne (odwoïujÈce siÚ do siebie) 4 67 68 71 71 72 73 75 76 80 81 82 85 86 89 92 98 100 101 102 103 104 105 107 113 113 115 118 121 124 128 131 134 134 135 140 143 149 149 151 154 158 161 6.6. Wyszukiwanie w tabelach 6.7. 6.8. 6.9. typedef union Pola bitowe Rozdziaï 7. WejĂcie i wyjĂcie Standardowe operacje wejĂcia-wyjĂcia printf — formatowanie danych wyjĂciowych Listy argumentów o zmiennej dïugoĂci scanf — formatowane dane wejĂciowe 7.1. 7.2. 7.3. 7.4. 7.5. DostÚp do plików 7.6. 7.7. Wierszowe operacje wejĂcia-wyjĂcia 7.8. stderr i exit — obsïuga bïÚdów Inne funkcje Rozdziaï 8. Interfejs systemu UNIX 8.1. Deskryptory plików 8.2. Niskopoziomowe operacje wejĂcia-wyjĂcia — odczyt i zapis 8.3. 8.4. 8.5. 8.6. 8.7. open, creat, close, unlink lseek — dostÚp swobodny Przykïad — implementacja fopen i getc Przykïad — listy zawartoĂci katalogów Przykïad — mechanizm alokacji pamiÚci Dodatek A Opis jÚzyka C Konwencje leksykalne Zapis skïadni Identyfikatory obiektów A.1. Wprowadzenie A.2. A.3. A.4. A.5. Obiekty i L-wartoĂci A.6. Konwersje A.7. Wyraĝenia A.8. Deklaracje A.9. Instrukcje A.10. Deklaracje zewnÚtrzne A.11. A.12. A.13. Gramatyka Zakres i wiÈzanie Przetwarzanie wstÚpne Dodatek B Standardowa biblioteka jÚzyka C B.1. WejĂcie i wyjĂcie: stdio.h B.2. Wykrywanie klas znaków: ctype.h B.3. B.4. B.5. B.6. Diagnostyka: assert.h CiÈgi znakowe: string.h Funkcje matematyczne: math.h Funkcje narzÚdziowe: stdlib.h Spis treĞci 166 168 170 172 175 175 178 180 181 185 188 189 191 195 196 197 198 201 202 206 211 217 217 217 221 222 224 225 228 241 257 261 264 266 273 281 282 291 291 293 294 297 5 JĊzyk ANSI C. Programowanie Listy argumentów o zmiennej dïugoĂci: stdarg.h Skoki odlegïe: setjmp.h Sygnaïy: signal.h B.7. B.8. B.9. B.10. Data i godzina: time.h B.11. Ograniczenia okreĂlane przez implementacjÚ: limits.h i float.h Dodatek C Podsumowanie zmian Skorowidz 298 298 299 300 302 305 309 6 Rozdziaï 4. Funkcje i struktura programu Funkcje dzielÈ duĝe zadania obliczeniowe na mniejsze oraz umoĝliwiajÈ wielokrotne wykorzystywanie tego samego kodu. WïaĂciwie napisane funkcje ukrywajÈ szczegóïy swoich mechanizmów przed innymi czÚĂciami programu, dla których sÈ one nieistotne. Zapewnia to przejrzystoĂÊ i znacznie uïatwia wprowadzanie zmian. JÚzyk C zostaï zaprojektowany w taki sposób, aby korzystanie z funkcji byïo efektywne i ïatwe. Program skïada siÚ z reguïy z duĝej liczby maïych funkcji. Duĝe funkcjÚ sÈ stosowane rzadko. Program moĝe byÊ zapisany w jednym lub wielu plikach. Pliki ěródïowe programu mogÈ byÊ kompilowane niezaleĝnie i póěniej jednoczeĂnie ïado- wanedo pamiÚci razem z wczeĂniej skompilowanymi funkcjami bibliotek. Nie bÚdziemy omawiaÊ tu dokïadnie tego rodzaju procedur, poniewaĝ róĝniÈ siÚ one w zaleĝnoĂci od stosowanego systemu. Deklaracja i definicja funkcji to obszar, w którym norma ANSI wprowadziïa najbardziej rzucajÈce siÚ w oczy zmiany w jÚzyku C. Jak widzieliĂmy juĝ w rozdziale 1., moĝna teraz okreĂlaÊ w deklaracji funkcji typy jej argumentów. Skïadnia definicji funkcji równieĝ jest zmieniona, dziÚki czemu deklaracja i definicja majÈ takÈ samÈ postaÊ. Umoĝliwia to kompilatorowi wykrycie znacznie wiÚkszej liczby bïÚdów niĝ wczeĂniej. Co wiÚcej, wïaĂciwy sposób deklarowania argumentów zapewnia automatyczne konwersje typów. Standard uĂciĂla reguïy dotyczÈce zakresu nazw. W szczególnoĂci wymaga on, aby kaĝdy obiekt zewnÚtrzny miaï tylko jednÈ definicjÚ. Mechanizm inicjalizacji zostaï uogólniony — w ANSI C moĝna inicjalizowaÊ tablice i struktury automatyczne. Preprocesor jÚzyka równieĝ zostaï usprawniony. Jego nowe mechanizmy obejmujÈ peïniejszy zbiór dyrektyw kompilacji warunkowej, moĝliwoĂÊ budowania ciÈgów znako- wych z argumentów makr oraz wiÚkszÈ kontrolÚ nad procesem rozwijania makra. JĊzyk ANSI C. Programowanie 4.1. Funkcje — podstawy Na poczÈtek zaprojektujemy i napiszemy program, który wypisuje kaĝdy wiersz danych wejĂciowych zawierajÈcy okreĂlony wzorzec — ciÈg znaków (bÚdzie to uproszczona wersja programu grep systemu UNIX). Przykïadowo wyszukiwanie wzorca „ould” w zbiorze wierszy Ah Love! could you and I with Fate conspire To grasp this sorry Scheme of Things entire, Would not we shatter it to bits -- and then Re-mould it nearer to the Heart s Desire! spowoduje wypisanie Ah Love! could you and I with Fate conspire Would not we shatter it to bits -- and then Re-mould it nearer to the Heart s Desire! Tak postawione zadanie moĝna podzieliÊ w naturalny sposób na trzy czÚĂci: while (jest kolejny wiersz) if (wiersz zawiera wzorzec) wypisz wiersz ChoÊ jest oczywiĂcie moĝliwe umieszczenie caïego kodu w funkcji main, lepszym po- dejĂciem okazuje siÚ wykorzystanie moĝliwoĂci strukturalizowania kodu i zapisanie kaĝdej czÚĂci w odrÚbnej funkcji. Z trzema maïymi elementami ïatwiej pracowaÊ niĝ z jednym duĝym — nieistotne szczegóïy pozostajÈ ukryte w funkcjach, a prawdopo- dobieñstwo wystÈpienia niepoĝÈdanych interakcji jest ograniczone do minimum. Co wiÚcej, gotowe elementy mogÈ znaleěÊ zastosowanie w innych programach. „while jest kolejny wiersz” to funkcja getline, którÈ napisaliĂmy juĝ w rozdziale 1. „wypisz wiersz” to funkcja printf, dostÚpna w standardowej bibliotece. Oznacza to, ĝe musimy jedynie napisaÊ procedurÚ okreĂlajÈcÈ, czy wiersz zawiera wzorzec. Moĝemy rozwiÈzaÊ ten problem, piszÈc funkcjÚ strindex(s,t), która zwraca pozycjÚ (indeks) w ciÈgu s, od którego zaczyna siÚ ciÈg t, lub -1, jeĝeli s nie zawiera t. Poniewaĝ pierwsza pozycja w tablicach jÚzyka C ma indeks 0, indeksy bÚdÈ miaïy wartoĂci dodatnie lub 0, a wartoĂÊ ujemna, taka jak -1, moĝe zostaÊ wykorzystana do sygnalizowania nieudanego wyszukiwania. Jeĝeli w przyszïoĂci bÚdzie potrzebny bardziej wyszukany mechanizm wyszukiwania wzorców, bÚdzie moĝna wymieniÊ funkcjÚ strindex na innÈ. Reszta kodu pozostanie bez zmian (standardowa biblioteka zawiera funkcjÚ strstr, która jest podobna do strindex, ale zwraca wskaěnik zamiast indeksu). Po takim przygotowaniu projektu napisanie wïaĂciwego programu jest juĝ czynnoĂciÈ stosunkowo prostÈ. Poniĝej przedstawiono caïoĂÊ, zarówno gïówny program, jak i sto- sowane funkcje, aby Czytelnik mógï wygodnie przeanalizowaÊ ich wspóïdziaïanie. W tej wersji wyszukiwany ciÈg jest literaïem (staïÈ), wiÚc trudno mówiÊ o ogólnoĂci rozwiÈzania. Do inicjalizowania tablic znakowych powrócimy juĝ wkrótce, natomiast w rozdziale 5. 86 Rozdziaá 4. • Funkcje i struktura programu pokaĝemy, jak przeksztaïciÊ wzorzec w parametr przekazywany przy uruchamianiu programu. Kod zawiera takĝe nieco zmodyfikowanÈ funkcjÚ getline. Porównanie jej z wersjÈ z rozdziaïu 1. moĝe dostarczyÊ wartoĂciowych spostrzeĝeñ. #include stdio.h #define MAXLINE 1000 /* dopuszczalna dáugoĞü wiersza */ int getline(char line[], int max) int strindex(char source[], char searchfor[]); char pattern[] = ould ; /* wzorzec do wyszukania */ /* wyszukuje wszystkie wiersze zawierające wzorzec */ main() { char line[MAXLINE]; int found = 0; while (getline(line, MAXLINE) 0) if (strindex(line, pattern) = 0) { printf( s , line); found++; } return found; } /* getline: pobiera wiersz do s, zwraca dáugoĞü */ int getline(char s[], int lim) { int c, i; i = 0; while (--lim 0 (c=getchar()) != EOF c != ) s[i++] = c; if (c == ) s[i++] = c; s[i] = ; return i; } /* strindex: zwraca index t w s lub –1, jeĪeli nie wystĊpuje */ int strindex(char s[], char t[]) { int i, j, k; for (i = 0; s[i] != ; i++) { for (j=i, k=0; t[k]!= s[j]==t[k]; j++, k++) ; if (k 0 t[k] == ) return i; } return -1; } 87 JĊzyk ANSI C. Programowanie Definicja funkcji ma zawsze nastÚpujÈcÈ postaÊ: typ_zwracany nazwa_funkcji(deklaracje_argumentów) { deklaracje i instrukcje } Róĝne elementy moĝna pomijaÊ. Absolutne minimum to dummy () {} czyli funkcja, która nic nie robi i nic nie zwraca. Funkcja tego rodzaju okazuje siÚ czasem przydatna jako tymczasowa „atrapa” w trakcie pracy nad programem. Jeĝeli zwracany typ danych nie zostaï okreĂlony, kompilator przyjmuje, ĝe jest to int. Program to po prostu zbiór definicji zmiennych i funkcji. Komunikacja miÚdzy funkcjami odbywa siÚ za poĂrednictwem argumentów funkcji, wartoĂci zwracanych przez funkcje i zmiennych zewnÚtrznych. Funkcje mogÈ byÊ umieszczone w pliku ěródïowym w dowol- nej kolejnoĂci, a program moĝe byÊ podzielony na wiele plików ěródïowych, o ile tylko kaĝda funkcja znajduje siÚ w caïoĂci w jednym pliku. Instrukcja return reprezentuje mechanizm zwracania wartoĂci z funkcji wywoïywanej do funkcji lub Ărodowiska wywoïujÈcego. Po sïowie return moĝe znajdowaÊ siÚ dowolne wyraĝenie: return wyraĝenie; Jeĝeli to konieczne, wartoĂÊ wyraĝenia jest przeksztaïcana na typ zadeklarowany jako zwracany przez funkcjÚ. Wyraĝenie nastÚpujÈce po sïowie return ujmuje siÚ czÚsto w nawiasy, ale nie jest to wymagane. Funkcja wywoïujÈca moĝe w kaĝdym przypadku zignorowaÊ zwracanÈ wartoĂÊ. Co wiÚcej, wyraĝenie po sïowie return nie jest elementem wymaganym. Gdy zostanie pominiÚte, funkcja nie bÚdzie zwracaïa ĝadnej wartoĂci. Sterowanie zostaje przekazane do funkcji wywoïujÈcej bez zwracania wartoĂci takĝe po dojĂciu do koñcowego nawiasu klamrowego. Jest to dopuszczalne, ale — gdy funkcja z jednego miejsca zwraca wartoĂÊ, a z innego nie — sygnalizuje wystÚpowanie nieprawidïowoĂci w pracy programu. W kaĝdym przy- padku, w którym funkcja nie zwraca wartoĂci, próba jej odczytania prowadzi do uzy- skania przypadkowych danych (Ămieci). Program wyszukujÈcy ciÈg zwraca z funkcji main informacjÚ o przebiegu jego wykonywa- nia, którÈ w tym przypadku jest liczba znalezionych wierszy. WartoĂÊ ta moĝe byÊ wyko- rzystywana przez Ărodowisko, które wywoïaïo program. Mechanika kompilowania i ïadowania programu C, który zostaï zapisany w wielu plikach ěródïowych, róĝni siÚ w zaleĝnoĂci od systemu. Przykïadowo w systemie UNIX zadanie to realizuje wspomniane w rozdziale 1. polecenie cc. Zaïóĝmy, ĝe trzy funkcje przykïado- wego programu sÈ zapisane w trzech plikach, o nazwach main.c, getline.c i strindex.c. W takiej sytuacji polecenie cc main.c getline.c strindex.c 88 Rozdziaá 4. • Funkcje i struktura programu kompiluje trzy wymienione pliki, umieszcza kod obiektów w plikach main.o, getline.o i strindex.o, a nastÚpnie ïaduje je wszystkie do pliku wykonywalnego o nazwie a.out. W przypadku wystÈpienia bïÚdu, na przykïad w main.c, plik moĝe zostaÊ skompilowany ponownie niezaleĝnie od innych i zaïadowany razem z przygotowanymi wczeĂniej. Umoĝliwia to polecenie cc main.c getline.o strindex.o Polecenie cc wykorzystuje rozszerzenia .c i .o do odróĝniania plików ěródïowych od plików wynikowych. mwiczenie 4.1. Napisz funkcjÚ strrindex(s,t), która zwraca pozycjÚ ostatniego wy- stÈpienia t w s lub -1, jeĝeli wyszukiwany ciÈg nie zostaï znaleziony. 4.2. Zwracanie wartoĂci innych niĝ int Dotychczas przykïadowe funkcje albo nie zwracaïy ĝadnej wartoĂci (void), albo zwracaïy liczbÚ int. Co z funkcjami zwracajÈcymi wartoĂci innych typów? Wiele funkcji liczbo- wych, takich jak sqrt, sin czy cos, zwraca typ double. Inne wyspecjalizowane funkcje zwracajÈ jeszcze inne typy. Aby zilustrowaÊ pracÚ z takimi funkcjami, napiszemy i wywo- ïamy funkcjÚ atof(s), która konwertuje ciÈg s na jego odpowiednik typu zmiennoprze- cinkowego, podwójnej precyzji. Funkcja atof jest rozwiniÚciem funkcji atoi, której wersje zostaïy przedstawione w rozdziaïach 2. i 3. atof zapewnia obsïugÚ opcjonalnego znaku liczby oraz kropki dziesiÚtnej, a takĝe sytuacji, w których nie wystÚpuje czÚĂÊ caïkowita lub czÚĂÊ uïamkowa wartoĂci. Przedstawiona tu wersja nie jest wysokiej jakoĂci procedurÈ konwersji danych wejĂciowych. Taka funkcja zajÚïaby stanowczo zbyt wiele miejsca. DopracowanÈ wersjÚ atof zawiera standardowa biblioteka jÚzyka — jest ona zdefiniowana w nagïówku stdlib.h . Przede wszystkim typ zwracanej wartoĂci, jeĝeli nie jest to int, musi zostaÊ okreĂlony w samej funkcji. NazwÚ typu umieszcza siÚ przed nazwÈ funkcji: #include ctype.h /* atof: konwertuje ciąg s na liczbĊ double */ double atof(char s[]) { double val, power; int i, sign; for (i = 0; isspace(s[i]); i++) /* pomiĔ biaáe znaki */ ; sign = (s[i] == - ) ? -1 : 1; if (s[i] == + || s[i] == - ) i++; 89 JĊzyk ANSI C. Programowanie for (val = 0.0; isdigit(s[i]); i++) val = 10.0 * val + (s[i] - 0 ); if (s[i] == . ) i++; for (power = 1.0; isdigit(s[i]); i++) { val = 10.0 * val + (s[i] - 0 ); power *= 10; } return sign * val / power; } DrugÈ, równie waĝnÈ rzeczÈ jest to, ĝe procedura wywoïujÈca musi wiedzieÊ, ĝe funkcja atof zwraca wartoĂÊ innÈ niĝ int. JednÈ z moĝliwoĂci zapewnienia tego jest jawne zadeklarowanie atof w tej procedurze. DeklaracjÚ takÈ widaÊ w programie minimali- stycznego kalkulatora (nadajÈcego siÚ chyba tylko do podliczania wypïat z bankomatu), który sumuje pobieranÈ z wejĂcia pojedynczÈ kolumnÚ liczb. Liczby mogÈ zawieraÊ znak, a po kaĝdej jest drukowana suma pobranych juĝ wartoĂci: #include stdio.h #define MAXLINE 100 /* prymitywny kalkulator */ main() { double sum, atof(char []); char line[MAXLINE]; int getline(char line[], int max); sum = 0; while (getline(line, MAXLINE) 0) printf( g , sum += atof(line)); return 0; } Deklaracja double sum, atof(char []); mówi, ĝe sum to zmienna typu double, a atof to funkcja, która pobiera jeden argument char[] i zwraca wartoĂÊ double. Deklaracja i definicja funkcji muszÈ byÊ zgodne. Jeĝeli definicja funkcji i jej wywoïanie w main majÈ niespójnie okreĂlone typy, a sÈ w tym samym pliku ěródïowym, kompilator zgïosi bïÈd. Jednak gdy (co jest bardziej prawdopodobne) funkcja atof bÚdzie kompilowana niezaleĝnie, brak zgodnoĂci nie zostanie wykryty, a funkcja zwróci liczbÚ double, która w main bÚdzie traktowana jako int — uzyskiwane wtedy wartoĂci bÚdÈ niemal zupeï- nie przypadkowe. 90 Rozdziaá 4. • Funkcje i struktura programu W Ăwietle tego, co powiedzieliĂmy o dopasowaniu deklaracji do definicji, moĝe siÚ to wydawaÊ zaskakujÈce. PrzyczynÈ takiej niezgodnoĂci jest zasada, ĝe gdy brak prototypu funkcji, jej deklaracja nastÚpuje automatycznie w chwili pierwszego uĝycia w wyra- ĝeniu, na przykïad sum += atof(line) Jeĝeli nazwa, która nie zostaïa wczeĂniej zadeklarowana, wystÚpuje w wyraĝeniu, a bez- poĂrednio po niej jest umieszczony otwierajÈcy znak nawiasu, nastÚpuje deklaracja na podstawie kontekstu — dana nazwa jest uznawana za nazwÚ funkcji, która zwraca wartoĂÊ int. Nie sÈ natomiast przyjmowane ĝadne zaïoĝenia dotyczÈce jej argumentów. Co wiÚcej, jeĝeli deklaracja funkcji nie zawiera argumentów, jak w instrukcji double atof(); to równieĝ wstrzymuje kompilator od przyjmowania zaïoĝeñ dotyczÈcych argumentów. Sprawdzanie poprawnoĂci parametrów zostaje caïkowicie wyïÈczone. Ta szczególna interpretacja pustej listy argumentów ma umoĝliwiÊ kompilowanie starszych programów w jÚzyku C przez nowsze kompilatory. Nie naleĝy jednak stosowaÊ takiej skïadni w no- wych programach. Jeĝeli funkcja pobiera argumenty, deklarujemy je. Jeĝeli nie po- biera ĝadnych, uĝywamy typu void. JeĂli dysponujemy (wïaĂciwie zadeklarowanÈ) funkcjÈ atof, moĝemy wykorzystaÊ jÈ do utworzenia prostej funkcji atoi (konwertujÈcej ciÈg znaków na liczbÚ int): /* atoi: konwertuje ciąg s na liczbĊ caákowitą przy uĪyciu atof */ int atoi(char s[]) { double atof(char s[]); return (int) atof(s); } ZwróÊmy uwagÚ na strukturÚ deklaracji i instrukcjÚ return. WartoĂÊ wyraĝenia w wierszu return wyraĝenie; zostaje przeksztaïcona na typ wartoĂci zwracanej przez funkcjÚ przed wyjĂciem z tej funkcji. WartoĂÊ atof, typu double, jest konwertowana automatycznie na int po dojĂciu do tego wiersza — funkcja atoi ma zwracaÊ liczbÚ caïkowitÈ. Operacja taka moĝe prowadziÊ do utraty czÚĂci danych (czÚĂci uïamkowej liczby), wiÚc niektóre kompilatory generujÈ po jej napotkaniu ostrzeĝenie. Operacja (int) jest jawnÈ informacjÈ o tym, ĝe konwersja typu jest zamierzona, dziÚki czemu ostrzeĝenie nie jest wyĂwietlane. mwiczenie 4.2. Dodaj do funkcji atof moĝliwoĂÊ obsïugi notacji wykïadniczej, postaci: 123.45e-6 gdzie po liczbie zmiennoprzecinkowej moĝe wystÈpiÊ litera e lub E i wykïadnik, z opcjo- nalnym znakiem. 91 JĊzyk ANSI C. Programowanie 4.3. Zmienne zewnÚtrzne Program w jÚzyku C skïada siÚ ze zbioru obiektów zewnÚtrznych — zmiennych i funkcji. WewnÈtrz funkcji definiowane sÈ obiekty wewnÚtrzne, czyli jej argumenty i zmienne lokalne. Zmienne zewnÚtrzne sÈ definiowane poza funkcjami, dziÚki czemu mogÈ byÊ dostÚpne nie w jednej, ale w wielu funkcjach. Same funkcje sÈ zawsze obiektami ze- wnÚtrznymi, poniewaĝ jÚzyk C nie dopuszcza definiowania funkcji wewnÈtrz funkcji. Standardowo zewnÚtrzne zmienne i funkcje majÈ tÚ wïaĂciwoĂÊ, ĝe wszystkie odwoïania do nich, czyli takie, które uĝywajÈ tej samej nazwy, nawet w funkcjach kompilowanych niezaleĝnie, pozostajÈ odwoïaniami do tego samego obiektu (norma okreĂla tÚ wïaĂciwoĂÊ terminem „dowiÈzywanie obiektów zewnÚtrznych”, ang. external linkage). Pod tym wzglÚdem zmienne zewnÚtrzne zachowujÈ siÚ tak jak bloki COMMON jÚzyka Fortran lub zmienne w najbardziej zewnÚtrznym bloku w jÚzyku Pascal. Wkrótce pokaĝemy, jak definiowaÊ zmienne i funkcje zewnÚtrzne, które sÈ widoczne jedynie w obrÚbie po- jedynczego pliku ěródïowego. Poniewaĝ zmienne zewnÚtrzne sÈ dostÚpne globalnie, stanowiÈ alternatywÚ dla ar- gumentów i wartoĂci zwracanych przez funkcje — równieĝ umoĝliwiajÈ wymianÚ danych miÚdzy funkcjami. Kaĝda funkcja moĝe uzyskaÊ dostÚp do zmiennej zewnÚtrznej przy uĝyciu jej nazwy, o ile tylko nazwa ta zostaïa wczeĂniej w pewien sposób zadeklarowana. Jeĝeli funkcje majÈ korzystaÊ wspólnie z wielu róĝnych zmiennych, zmienne zewnÚtrzne sÈ wygodniejsze i efektywniejsze niĝ dïugie listy argumentów. Jak jednak pisaliĂmy w rozdziale 1., korzystanie z tej moĝliwoĂci powinno wiÈzaÊ siÚ z pewnÈ ostroĝnoĂciÈ, moĝe mieÊ bowiem zïy wpïyw na strukturÚ programu i prowadziÊ do kodu z nad- miernie zïoĝonÈ sieciÈ powiÈzañ miÚdzy funkcjami. Zmienne zewnÚtrzne znajdujÈ takĝe zastosowania wynikajÈce bezpoĂrednio z ich wiÚk- szego zakresu i dïuĝszego „czasu ĝycia”. Zmienne automatyczne to wewnÚtrzne obiekty funkcji. PowstajÈ w chwili wejĂcia do funkcji i zostajÈ zlikwidowane w chwili wyjĂcia z niej. Zmienne zewnÚtrzne sÈ trwaïe, zachowujÈ swojÈ wartoĂÊ pomiÚdzy wywoïaniami róĝnych funkcji. Jeĝeli wiÚc dwie funkcje muszÈ korzystaÊ z tych samych danych, a nie wystÚpuje sytuacja, w której jedna z nich wywoïuje drugÈ, zapisanie wspólnych danych w zmiennych zewnÚtrznych jest czÚsto najwygodniejszym rozwiÈzaniem, pozwalajÈcym uniknÈÊ wprowadzania dodatkowego mechanizmu przekazywania wartoĂci do i z kaĝdej ze wspóïdziaïajÈcych funkcji. Przeanalizujmy to zagadnienie na konkretnym przykïadzie. Naszym zadaniem jest napi- sanie programu kalkulatora, który umoĝliwia korzystanie z operatorów +, -, * i /. Po- niewaĝ jest to prostsze w implementacji, kalkulator bÚdzie korzystaï z odwrotnej notacji polskiej, a nie notacji infiksowej (odwrotna notacja polska jest uĝywana przez niektóre kalkulatory kieszonkowe oraz jÚzyki programowania, na przykïad Forth i PostScript). W odwrotnej notacji polskiej wszystkie operandy poprzedzajÈ operator. Wyraĝenie infik- sowe, na przykïad (1 – 2) * (4 + 5) 92 Rozdziaá 4. • Funkcje i struktura programu jest wprowadzane jako 1 2 – 4 5 + * Nawiasy nie sÈ potrzebne. Notacja jest jednoznaczna, o ile tylko liczba operandów kaĝdego operatora jest staïa. Implementacja jest prosta. Kaĝdy operand zostaje umieszczony na stosie. Po pobraniu operatora program zdejmuje ze stosu wïaĂciwÈ liczbÚ operandów (dwa w przypadku operatorów binarnych), wykonuje operacjÚ i zapisuje wynik ponownie na stosie. W po- wyĝszym przykïadzie oznacza to umieszczenie na stosie liczb 1 i 2, nastÚpnie zastÈpienie ich róĝnicÈ, –1. W kolejnym kroku na stos trafiajÈ liczby 4 i 5, które zostajÈ nastÚpnie zastÈpione sumÈ, 9. Kolejna operacja to mnoĝenie, wiÚc ze stosu zostajÈ pobrane wartoĂci –1 i 9, które zastÚpuje nastÚpnie ich iloczyn, –9. Na zakoñczenie, po dojĂciu do koñca wiersza, wartoĂÊ ze szczytu stosu zostaje wypisana na ekranie. Struktura programu jest wiÚc pÚtlÈ, która wykonuje odpowiednie operacje na pobiera- nych kolejno operatorach i operandach: while (nastÚpny operator lub operand nie jest znakiem koñca pliku) if (liczba) zapisz na stosie else if (operator) zdejmij operandy ze stosu wykonaj operacjÚ zapisz wynik na stosie else if (znak nowego wiersza) zdejmij wartoĂÊ ze szczytu stosu i wypisz else bïÈd Operacje umieszczania danych na stosie i zdejmowania z niego sÈ banalne, ale do czasu uzupeïnienia programu o wykrywanie i obsïugÚ bïÚdów pozostajÈ wystarczajÈco zïoĝone, aby uzasadniaïo to umieszczenie ich w osobnych funkcjach. Pozwoli to przede wszystkim uniknÈÊ powtarzania kodu. Równieĝ odrÚbna funkcja powinna odpowiadaÊ za pobieranie kolejnego operatora lub operandu. Gïównym zaïoĝeniem projektowym jest to, gdzie konkretnie jest stos, a wïaĂciwie — które procedury majÈ do niego bezpoĂredni dostÚp. JednÈ z moĝliwoĂci jest pozosta- wienie jego obsïugi w main. Moĝna przekazywaÊ stos i bieĝÈcÈ pozycjÚ stosu do procedur, które pobierajÈ i zapisujÈ wartoĂci. Jednak w funkcji main nie sÈ potrzebne zmienne sterujÈce stosem. Wykonuje ona tylko operacje zapisania danych i odczytania ich. Zdecy- dowaliĂmy wiÚc o przechowywaniu stosu i zwiÈzanych z nim informacji w zmiennych zewnÚtrznych, dostÚpnych funkcjom push i pop, ale nie main. Zapisanie takiego projektu w postaci kodu nie jest trudne. Jeĝeli mamy zapisaÊ program w jednym pliku ěródïowym, bÚdzie on wyglÈdaï tak: #include ... /* wiersze include */ #define ... /* wiersze define */ 93 JĊzyk ANSI C. Programowanie deklaracje funkcji dla main main() { ... } zewnÚtrzne zmienne dla push i pop void push(double f) { ... } double pop(void) { ... } int getop(char s[]) { ... } procedury wywoïywane przez getop Zagadnieniem dzielenia programu na dwa pliki ěródïowe lub wiÚcej zajmiemy siÚ juĝ niedïugo. Funkcja main to pÚtla zawierajÈca rozbudowanÈ instrukcjÚ switch, która rozgaïÚzia sterowanie w zaleĝnoĂci od typu operatora lub operandu. Jest to bardziej typowy przy- kïad jej uĝycia niĝ ten przedstawiony w podrozdziale 3.4. #include stdio.h #include stdlib.h /* dla atof() */ #define MAXOP 100 /* dopuszczalny rozmiar operandu lub operatora */ #define NUMBER 0 /* sygnaá, Īe pobrano liczbĊ */ int getop(char []); void push(double); double pop(void); /* kalkulator z odwrotną notacją polską */ main() { int type; double op2; char s[MAXOP]; while ((type = getop(s)) != EOF) { switch (type) { case NUMBER: push(atof(s)); break; case + : push(pop() + pop()); break; case * : push(pop() * pop()); break; case - : op2 = pop(); push(pop() - op2); break; case / : 94 Rozdziaá 4. • Funkcje i struktura programu op2 = pop(); if (op2 != 0.0) push(pop() / op2); else printf( error: zero divisor ); break; case : printf( .8g , pop()); break; default: printf( error: unknown command s , s); break; } } return 0; } Poniewaĝ + i * to operatory dziaïañ przemiennych, kolejnoĂÊ zdejmowania operandów ze stosu nie ma znaczenia. Jednak w przypadku operatorów – i / musi istnieÊ rozróĝnienie miÚdzy wartoĂciÈ po lewej stronie znaku i wartoĂciÈ po prawej stronie znaku. W instrukcji push(pop() – pop()); /* BàĄD */ kolejnoĂÊ obliczania wartoĂci wywoïañ pop nie jest okreĂlona. Aby zagwarantowaÊ wïa- ĂciwÈ, konieczne jest wczeĂniejsze pobranie pierwszej wartoĂci do zmiennej tymczasowej. WidaÊ to w kodzie funkcji main. #define MAXVAL 100 /* dopuszczalna gáĊbokoĞü stosu wartoĞci */ int sp = 0; /* nastĊpna wolna pozycja stosu */ double val[MAXVAL]; /* stos */ /* push: zapisuje f na stosie */ void push(double f) { if (sp MAXVAL) val[sp++] = f; else printf( error: stack full, can t push g , f); } /* pop: zdejmuje i zwraca wartoĞü z wierzchoáka stosu */ double pop(void) { if (sp 0) return val[--sp]; else { printf( error: stack empty ); return 0.0; } } 95 JĊzyk ANSI C. Programowanie Zmienna jest zmiennÈ zewnÚtrznÈ, jeĝeli jest zdefiniowana poza funkcjÈ, tak wiÚc wspóïuĝytkowane przez funkcje pop i push zmienne stosu i indeksu stosu zostajÈ zde- finiowane poza tymi funkcjami. Jednak funkcja main nie odwoïuje siÚ do stosu ani jego indeksu — reprezentacja moĝe pozostaÊ ukryta. Przejděmy teraz do implementacji getop, funkcji, która pobiera kolejny operator lub ope- rand. Zadanie jest proste. Pomijamy spacje i tabulatory. Jeĝeli nastÚpny znak nie jest cyfrÈ lub kropkÈ dziesiÚtnÈ, zwracamy go. W pozostaïych przypadkach pobieramy ciÈg cyfr (który moĝe zawieraÊ kropkÚ dziesiÚtnÈ) i zwracamy NUMBER, czyli wartoĂÊ sygnalizu- jÈcÈ, ĝe pobrana zostaïa liczba. #include ctype.h int getch(void); void ungetch(int); /* getop: pobiera nastĊpny operator lub operand (liczbĊ) */ int getop(char s[]) { int i, c; while ((s[0] = c = getch()) == || c == ) ; s[1] = ; if (!isdigit(c) c != . ) return c; /* nie jest liczbą */ i = 0; if (isdigit(c)) /* pobierz czĊĞü caákowitą */ while (isdigit(s[++i] = c = getch())) ; if (c == . ) /* pobierz czĊĞü uáamkową */ while (isdigit(s[++i] = c = getch())) ; s[i] = ; if (c != EOF) ungetch(c); return NUMBER; } Co to za funkcje getch i ungetch? CzÚsto zdarza siÚ, ĝe program nie moĝe okreĂliÊ, czy odczytaï wystarczajÈcÈ iloĂÊ danych wejĂciowych aĝ do momentu, gdy odczyta ich zbyt duĝo. Takim przypadkiem jest wïaĂnie odczytywanie znaków tworzÈcych liczbÚ: do czasu odczytania pierwszego znaku, który nie jest cyfrÈ, pobierana liczba pozostaje niekompletna. Jednak jest to moment, gdy program odczytaï juĝ o jeden znak za duĝo, znak, na który nie jest przygotowany. Problem byïby rozwiÈzany, gdyby istniaïa moĝliwoĂÊ cofniÚcia operacji odczytu ostatniego znaku danych wejĂciowych. Wówczas program, który odczyta o jeden znak za duĝo, mógïby „oddaÊ” ten znak do strumienia, a inne elementy programu dziaïaïyby tak, 96 Rozdziaá 4. • Funkcje i struktura programu jak gdyby znak ten nigdy nie byï odczytywany. Okazuje siÚ, ĝe skonstruowanie takiego mechanizmu nie jest trudne, wystarczy para wspóïpracujÈcych ze sobÈ funkcji. getch zwraca kolejny znak danych wejĂciowych. ungetch zapamiÚtuje znaki zwrócone na wejĂcie w taki sposób, aby dalsze wywoïania getch zwracaïy je przed odczytaniem nowych z rzeczywistego strumienia. Ich wspóïpraca jest prosta. ungetch zapisuje wycofane znaki we wspólnym buforze — tablicy znaków. getch odczytuje zawartoĂÊ bufora, jeĝeli nie jest on pusty. W pozo- staïych przypadkach wywoïuje po prostu funkcjÚ getchar. NiezbÚdna jest równieĝ zmienna indeksujÈca, która rejestruje pozycjÚ bieĝÈcego znaku w buforze. Poniewaĝ bufor i indeks wykorzystujÈ dwie funkcje, getch i ungetch, a wartoĂci tych zmiennych muszÈ zostaÊ zachowane miÚdzy wywoïaniami, konieczne jest uĝycie zmiennych zewnÚtrznych. Obie funkcje i deklaracje zmiennych moĝna zapisaÊ tak: #define BUFSIZE 100 char buf[BUFSIZE]; /* bufor dla ungetch */ int bufp = 0; /* nastĊpna wolna pozycja w buforze */ int getch(void) /* pobiera znak (moĪe byü znakiem wczeĞniej wycofanym) */ { return (bufp 0) ? buf[--bufp] : getchar(); } void ungetch(int c) /* wycofuje znak do strumienia danych wejĞciowych */ { if (bufp = BUFSIZE) printf( ungetch: too many characters ); else buf[bufp++] = c; } Standardowa biblioteka zawiera funkcjÚ ungetc, która umoĝliwia wycofanie jednego znaku. Omówimy jÈ w rozdziale 7. W powyĝszym przykïadzie uĝyliĂmy tablicy, a nie poje- dynczego znaku, aby zaprezentowaÊ bardziej ogólne podejĂcie. mwiczenie 4.3. W oparciu o schemat przedstawiony w przykïadach program kalkulatora moĝna ïatwo rozbudowywaÊ. Dodaj obsïugÚ operatora modulo ( ) i obsïugÚ liczb ujemnych. mwiczenie 4.4. Utwórz polecenie wypisujÈce element na wierzchoïku stosu bez jego usuwania ze stosu, polecenie duplikujÈce element na wierzchoïku stosu, polecenie zamieniajÈce miejscami dwa górne elementy oraz polecenie usuwajÈce caïÈ zawartoĂÊ stosu. mwiczenie 4.5. Dodaj dostÚp do funkcji biblioteki, takich jak sin, exp, i pow. Patrz math.h w czÚĂci 4. dodatku B. 97 JĊzyk ANSI C. Programowanie mwiczenie 4.6. Dodaj polecenia obsïugi zmiennych (ïatwo jest zapewniÊ moĝliwoĂÊ korzystania z dwudziestu szeĂciu zmiennych przy uĝyciu jednoliterowych nazw). Dodaj zmiennÈ przechowujÈcÈ ostatniÈ wypisanÈ wartoĂÊ. mwiczenie 4.7. Napisz procedurÚ ungets(s), która zwraca do danych wejĂciowych caïy ciÈg znaków. Czy funkcja ta powinna korzystaÊ ze zmiennych buf i bufp, czy raczej tylko z funkcji ungetch? mwiczenie 4.8. Zmodyfikuj funkcje getch i ungetch, przyjÈwszy zaïoĝenie, ĝe nigdy nie bÚdzie wycofywany wiÚcej niĝ jeden znak. mwiczenie 4.9. Nasze funkcje getch i ungetch nie obsïugujÈ poprawnie wycofywania znaku EOF. Zastanów siÚ, jakie powinny one mieÊ cechy w przypadku cofania znaku EOF, po czym zaimplementuj nowÈ koncepcjÚ. mwiczenie 4.10. Alternatywna organizacja pracy z danymi wejĂciowymi opiera siÚ na uĝyciu getline w celu pobrania caïego wiersza. DziÚki temu funkcje getch i ungetch nie sÈ potrzebne. PrzeksztaïÊ kalkulator, tak aby jego praca opieraïa siÚ na takim podejĂciu do danych wejĂciowych. 4.4. Zakres Funkcje i zmienne zewnÚtrzne tworzÈce program w jÚzyku C nie muszÈ byÊ kompi- lowane jednoczeĂnie. ½ródïowy tekst programu moĝna przechowywaÊ w wielu plikach, a wczeĂniej skompilowane procedury mogÈ byÊ ïadowane z bibliotek. WiÈĝe siÚ to z kil- koma istotnymi pytaniami: Q Jak zapisywaÊ deklaracje, aby deklarowanie zmiennych wïaĂciwie przebiegaïo w czasie kompilacji? Q Jaki powinien byÊ ukïad deklaracji, aby wszystkie elementy zostaïy wïaĂciwie poïÈczone w chwili ïadowania programu? Q Jaki ukïad deklaracji zapewnia, ĝe nie sÈ one powtarzane? Q Jak inicjuje siÚ zmienne zewnÚtrzne? Omówimy te zagadnienia na przykïadzie programu kalkulatora, który teraz podzielony zostanie na kilka plików. Z praktycznego punktu widzenia jest to zbyt maïy program, aby faktycznie warto byïo go dzieliÊ, jednak wystarczy on do zilustrowania problemów, które pojawiajÈ siÚ w wiÚkszych projektach. Zakres (ang. scope) nazwy to czÚĂÊ programu, w której nazwÚ tÚ moĝna stosowaÊ. Dla zmiennej automatycznej, deklarowanej na poczÈtku funkcji, zakresem jest funkcja, w której zmienna zostaïa zadeklarowana. Zmienne lokalne o tej samej nazwie, ale w róĝnych funkcjach nie majÈ ze sobÈ ĝadnego zwiÈzku. To samo moĝna powiedzieÊ o parametrach funkcji — sÈ one w praktyce zmiennymi lokalnymi. 98 Rozdziaá 4. • Funkcje i struktura programu Zakres zmiennej zewnÚtrznej lub funkcji siÚga od punktu jej zadeklarowania do koñca kompilowanego pliku. Jeĝeli na przykïad main, sp, val, push i pop sÈ zdefiniowane w jednym pliku, w kolejnoĂci przedstawionej wczeĂniej, czyli main() { ... } int sp = 0; double val[MAXVAL]; void push(double f) { ... } double pop(void) { ... } to zmienne sp i val moĝna stosowaÊ w funkcjach push i pop, po prostu wymieniajÈc ich nazwÚ. Nie sÈ wymagane dodatkowe deklaracje. Jednak nazwy te nie sÈ widoczne w main, podobnie jak funkcje push i pop. Z drugiej strony, jeĝeli odwoïania do zmiennej zewnÚtrznej majÈ wystÈpiÊ przed jej zdefiniowaniem lub zmienna ta jest definiowana w innym pliku ěródïowym niĝ ten, w którym jest wykorzystywana, konieczne staje siÚ uĝycie deklaracji extern. Waĝne jest, aby rozróĝniaÊ deklaracjÚ zmiennej zewnÚtrznej od jej definicji. Deklaracja informuje o wïaĂciwoĂciach zmiennej (przede wszystkim jej typie). Definicja powoduje dodatkowo przydzielenie pamiÚci. Jeĝeli wiersze int sp; double val[MAXVAL]; pojawiajÈ siÚ poza funkcjami, sÈ to definicje zmiennych zewnÚtrznych sp i val. PowodujÈ one przydzielenie pamiÚci, peïniÈ takĝe funkcje deklaracji dla kodu w pozostaïej czÚĂci pliku ěródïowego. Z drugiej strony wiersze extern int sp; extern double val[]; deklarujÈ na potrzeby kodu w dalszej czÚĂci pliku, ĝe sp ma typ int, a val to tablica liczb double (której rozmiar jest okreĂlony gdzie indziej). Nie tworzÈ one jednak zmiennych i nie rezerwujÈ pamiÚci. We wszystkich plikach tworzÈcych program ěródïowy moĝe wystÈpiÊ tylko jedna definicja zmiennej zewnÚtrznej. Inne pliki mogÈ zawieraÊ deklaracje extern umoĝliwiajÈce do- stÚp do tej zmiennej (deklaracje extern mogÈ znaleěÊ siÚ takĝe w pliku zawierajÈcym definicjÚ). Rozmiar tablicy musi zostaÊ okreĂlony w definicji, a w deklaracji extern jest opcjonalny. Inicjalizacja zmiennej zewnÚtrznej moĝe zostaÊ poïÈczona tylko z jej definicjÈ. ChoÊ w tym przypadku ukïad taki nie ma raczej uzasadnienia, funkcje push i pop mogÈ byÊ zdefiniowane w jednym pliku, a zmienne val i sp w innym. Wówczas ich powiÈzanie zostanie zapewnione przez nastÚpujÈcy ukïad definicji i deklaracji: 99 JĊzyk ANSI C. Programowanie W pliku file1: extern int sp; extern double val[]; void push(double f) { ... } double pop(void) { ... } W pliku file2: int sp = 0; double val[MAXVAL]; Poniewaĝ deklaracje extern w pliku file1 poprzedzajÈ definicje funkcji, zmienne moĝna w tych funkcjach stosowaÊ. Jedna para deklaracji wystarczy dla zapewnienia dostÚp- noĂci zmiennych w caïym pliku file1. Taki sam ukïad naleĝaïoby zastosowaÊ, gdyby definicje sp i val znajdowaïy siÚ w tym samym pliku, ale po definicjach funkcji, w których sÈ stosowane. 4.5. Pliki nagïówkowe Rozwaĝmy podzielenie programu kalkulatora na kilka plików ěródïowych. Mogïoby to byÊ potrzebne, gdyby poszczególne jego komponenty zostaïy znacznie rozbudowane. Przyjmijmy, ĝe funkcja main trafia do pliku main.c, push, pop i ich zmienne do pliku stack.c, funkcja getop do pliku getop.c, a getch i ungetch — do getch.c. Oddzielamy te ostatnie od pozostaïych, poniewaĝ w rzeczywistym programie byïyby czÚĂciÈ odrÚbnie kompilowanej biblioteki. Pozostaje jeden problem do rozwiÈzania — definicje i deklaracje elementów wyko- rzystywanych w wiÚcej niĝ jednym pliku. DÈĝymy do maksymalnej centralizacji bu- dowanego systemu, aby kaĝda z jego czÚĂci miaïa tylko jedno wïaĂciwe miejsce, nieule- gajÈce zmianie w toku dalszej ewolucji kodu. Aby osiÈgnÈÊ ten cel, umieszczamy wspólne elementy w pliku nagïówkowym (ang. header file, najczÚĂciej nazywany krótko nagïów- kiem), calc.h. Plik ten bÚdzie wïÈczony do kodu plików, które korzystajÈ z jego zawartoĂci, dyrektywÈ #include. DyrektywÚ tÚ opiszemy dokïadnie w podrozdziale 4.11. Program wyglÈda tak: 100 Rozdziaá 4. • Funkcje i struktura programu Mamy tu do czynienia z problemem wywaĝenia miÚdzy dÈĝeniem do tego, aby kaĝdy plik miaï dostÚp wyïÈcznie do tych informacji, które sÈ mu niezbÚdne, a prozaicznÈ potrzebÈ codziennej praktyki — praca ze zbyt duĝÈ liczbÈ plików nagïówka jest uciÈĝliwa. Do pewnych granic dobrym rozwiÈzaniem jest stosowanie jednego nagïówka dla caïego programu zawierajÈcego wszystko, co jest uĝywane przez wiÚcej niĝ jednÈ jego czÚĂÊ. Takie rozwiÈzanie zastosowaliĂmy w przykïadzie. WiÚksze programy wymagajÈ bardziej rozbudowanej struktury i wiÚkszej liczby nagïówków. 4.6. Zmienne statyczne Zmienne sp i val w pliku stack.c oraz buf i bufp w pliku getch.c sïuĝÈ do prywatnego uĝytku przez funkcje znajdujÈce siÚ w tym samym pliku ěródïowym. ¿adne inne nie po- winny mieÊ do nich dostÚpu. Deklaracja static zastosowana w odniesieniu do zmiennej zewnÚtrznej lub funkcji ogranicza zakres obiektu do pozostaïej czÚĂci kompilowanego pliku ěródïowego. ZewnÚtrzna deklaracja static jest wiÚc metodÈ ukrywania nazw takich jak buf i bufp — nazw, które muszÈ byÊ zewnÚtrzne, bo sÈ wspóïuĝytkowane przez róĝne funkcje, ale nie powinny byÊ widoczne dla kodu wywoïujÈcego te funkcje. Statyczne przechowywanie zmiennych okreĂlamy, wstawiajÈc na poczÈtku zwykïej deklaracji sïowo static. Jeĝeli dwie procedury i dwie zmienne sÈ kompilowane w tym samym pliku, jak w przykïadzie static char buf[BUFSIZE]; /* bufor dla ungetch */ static int bufp = 0; /* nastĊpna wolna pozycja w buforze */ int getch(void) { ... } void ungetch(int c) { ... } 101 JĊzyk ANSI C. Programowanie to ĝadna inna procedura nie ma dostÚpu do zmiennych buf i bufp, a ich nazwy nie wchodzÈ w konflikt z takimi samymi nazwami w innych plikach tego samego programu. W taki sam sposób moĝna ukryÊ zmienne wykorzystywane przez funkcje push i pop do obsïugi stosu — deklarujÈc sp i val jako static. ZewnÚtrzna deklaracja static jest najczÚĂciej stosowana w odniesieniu do zmiennych, ale moĝe byÊ uĝyta takĝe w odniesieniu do funkcji. Normalnie nazwy funkcji majÈ charakter globalny — sÈ widoczne w caïym programie. Jeĝeli jednak funkcja jest za- deklarowana jako static, jej nazwa nie jest widoczna poza plikiem, w którym zostaïa zadeklarowana. Deklaracji static moĝna takĝe uĝyÊ w odniesieniu do zmiennych wewnÚtrznych. We- wnÚtrzne zmienne static pozostajÈ zmiennymi lokalnymi funkcji, podobnie jak zmienne automatyczne, jednak w przeciwieñstwie do zmiennych automatycznych nie prze- stajÈ istnieÊ w chwili wyjĂcia z funkcji. W efekcie wewnÚtrzne zmienne statyczne to prywatna pamiÚÊ trwaïa pojedynczej funkcji. mwiczenie 4.11. Zmodyfikuj funkcjÚ getop w taki sposób, aby nie korzystaïa z funkcji ungetch. Wskazówka: uĝyj wewnÚtrznej zmiennej statycznej. 4.7. Zmienne rejestrowe Deklaracja register zwraca uwagÚ kompilatora na to, ĝe dana zmienna bÚdzie wyjÈt- kowo intensywnie wykorzystywana. IdeÈ tej deklaracji jest wskazanie, ĝe pewne zmienne powinny zostaÊ umieszczone w rejestrach komputera. Z zasady prowadzi to do szyb- szych i mniejszych programów. Kompilator moĝe, ale nie musi, dostosowaÊ siÚ do takie- go zalecenia. Oto przykïady deklaracji register: register int x; register char c; Deklaracje takie moĝna stosowaÊ wyïÈcznie w odniesieniu do zmiennych automatycz- nych i parametrów formalnych funkcji. W przypadku parametrów formalnych wyglÈda to tak: f(register unsigned m, register long n) { register int i; ... } W praktyce zmienne rejestrowe podlegajÈ pewnym ograniczeniom wynikajÈcym z moĝ- liwoĂci wykorzystywanej platformy sprzÚtowej. Tylko kilka zmiennych w kaĝdej funkcji moĝna przechowywaÊ w rejestrach i tylko wybrane typy sÈ dopuszczalne. Nadmiar deklaracji register jest jednak nieszkodliwy, poniewaĝ w przypadku zbyt duĝej liczby 102 Rozdziaá 4. • Funkcje i struktura programu tak opisanych zmiennych lub niezgodnoĂci typów sïowo register jest ignorowane. Dodat- kowo nie moĝna pobraÊ adresu zmiennej rejestrowej (ten temat omówimy w rozdziale 5.), niezaleĝnie od tego, czy zostaïa ona faktycznie umieszczona w rejestrze. Zakres ograni- czeñ co do typów i liczby zmiennych rejestrowych jest zaleĝny od komputera. 4.8. Struktura blokowa JÚzyk C nie jest jÚzykiem, w którym struktura programu opiera siÚ na blokach, jak jest na przykïad w Pascalu — nie moĝna definiowaÊ funkcji wewnÈtrz funkcji. Mimo to struktura blokowa obowiÈzuje przy definiowaniu zmiennych. Deklaracje zmiennych (i ich inicjalizacja) mogÈ zostaÊ umieszczone po nawiasie klamrowym otwierajÈcym dowolnÈ instrukcjÚ blokowÈ, a nie tylko po nawiasie klamrowym otwierajÈcym blok in- strukcji funkcji. Zmienne deklarowane w ten sposób przesïaniajÈ zmienne o takich samych nazwach wystÚpujÈce poza blokiem, a ich „czas ĝycia” koñczy siÚ wraz z wyjĂciem z bloku. Na przykïad w kodzie if (n 0) { int i; /* deklaracja nowej zmiennej i */ for (i = 0; i n; i++) ... } zakres zmiennej i to blok wykonywany przy wartoĂci warunku „prawda”. Zmienna ta nie ma ĝadnych powiÈzañ ze zmiennymi o nazwie i poza blokiem, w którym jest zadeklaro- wana. Zmienna automatyczna deklarowana i inicjalizowana w bloku jest deklarowana i inicjalizowana przy kaĝdym wejĂciu do tego bloku. Analogiczna zmienna static jest inicjalizowana przy pierwszym wejĂciu do bloku. Zmienne automatyczne, w tym parametry formalne, równieĝ przesïaniajÈ zmienne zewnÚtrzne i funkcje o tej samej nazwie. W ukïadzie deklaracji int x; int y; f(double x) { double y; ... } wewnÈtrz funkcji f wszystkie wystÈpienia x odnoszÈ siÚ do parametru (typu double). Poza funkcjÈ f nazwa zmiennej x odnosi siÚ do liczby int, zmiennej zewnÚtrznej. To samo moĝna powiedzieÊ o zmiennej y. Do dobrej praktyki programowania naleĝy unikanie stosowania nazw zmiennych, które przesïaniajÈ nazwy uĝywane w szerszym zakresie. Jest to bowiem najkrótsza droga do pomyïek i bïÚdów. 103 JĊzyk ANSI C. Programowanie 4.9. Inicjalizacja O inicjalizacji wspominaliĂmy juĝ kilkukrotnie, ale zawsze pozostawaïa ona na margi- nesie innych tematów. W tym podrozdziale, po omówieniu róĝnych klas pamiÚci da- nych, moĝemy przejĂÊ do usystematyzowania reguï tego procesu. Gdy brak jawnej inicjalizacji, zmienne zewnÚtrzne i statyczne majÈ wartoĂÊ 0, a zmienne automatyczne i rejestrowe pozostajÈ niezdefiniowane — nie zawierajÈ uĝytecznej wartoĂci. Zmienne skalarne moĝna inicjalizowaÊ przy ich definiowaniu — wystarczy wprowadziÊ po ich nazwie znak równoĂci i wyraĝenie: int x = 1; char squota = ; long day = 1000L * 60L * 60L * 24L; /* milisekund/dzieĔ */ WartoĂÊ inicjalizujÈca zmienne zewnÚtrzne i statyczne musi byÊ wyraĝeniem o staïej wartoĂci. Inicjalizacja jest wykonywana jednokrotnie, jeszcze przed rozpoczÚciem wïa- Ăciwego procesu wykonywania programu. Inicjalizacja zmiennych automatycznych i reje- strowych nastÚpuje przy kaĝdym wejĂciu wykonywanego programu do funkcji lub bloku. WartoĂÊ inicjalizujÈca zmienne automatyczne i rejestrowe nie musi byÊ staïa — moĝe to byÊ dowolne wyraĝenie oparte na wartoĂciach wczeĂniej zdefiniowanych, a nawet wywoïaniach funkcji. Przykïadowo inicjalizacja programu wyszukiwania binarnego z podrozdziaïu 3.3 moĝe byÊ zapisana nastÚpujÈco: int binsearch(int x, int v[], int n) { int low = 0; int high = n - 1; int mid; ... } Nie jest wymagane pisanie: int low, high, mid; low = 0; high = n - 1; W efekcie inicjalizacja zmiennych automatycznych i rejestrowych to po prostu skrócona forma ïÈczÈca instrukcje deklaracji i przypisania. Wybór jest kwestiÈ stylu. W ksiÈĝce z zasady nie ïÈczymy deklaracji i przypisania, poniewaĝ wartoĂÊ poczÈtkowa okreĂlona w bloku deklaracji jest ïatwa do przeoczenia, a odrÚbne przypisanie moĝe nastÈpiÊ w miejscu, w którym zmienna jest wykorzystywana. TablicÚ moĝna zainicjalizowaÊ, umieszczajÈc po deklaracji listÚ wartoĂci elementów — ujÚtÈ w nawiasy klamrowe i rozdzielanÈ przecinkami. Aby na przykïad zainicjalizowaÊ tablicÚ days dïugoĂciami miesiÚcy, piszemy: int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } 104 Rozdziaá 4. • Funkcje i struktura programu Gdy rozmiar tablicy nie jest okreĂlony, kompilator okreĂla jÈ, zliczajÈc wartoĂci poczÈtko- we elementów. W tym przypadku jest ich 12. Jeĝeli lista poczÈtkowych wartoĂci elementów tablicy zawiera mniej elementów niĝ tablica, pozostaïym przypisywana jest wartoĂÊ 0. Dotyczy to zmiennych zewnÚtrznych, statycznych i automatycznych. Podanie zbyt dïugiej listy wartoĂci jest bïÚdem. Nie ma skïadni umoĝliwiajÈcej powtarzanie wartoĂci na liĂcie albo inicjalizowanie elementów wewnÚtrznych bez podania wartoĂci wszystkich elementów poprzedzajÈcych. Tablice znaków sÈ traktowane w sposób szczególny. W miejsce nawiasów klamrowych i rozdzielonej przecinkami listy moĝna uĝyÊ ciÈgu: char pattern = ould ; Jest to skrót dïuĝszej, choÊ równowaĝnej konstrukcji: char pattern[] = { o , u , l , d , }; W tym przypadku rozmiar tablicy to 5 (cztery znaki plus koñcowa staïa ). 4.10. Rekurencja Funkcje jÚzyka C mogÈ byÊ wywoïywane rekurencyjnie. Oznacza to, ĝe funkcja moĝe, bezpoĂrednio lub poĂrednio, wywoïaÊ siebie samÈ. Rozwaĝmy przykïad wypisywania liczby jako ciÈgu znaków. Jak pisaliĂmy wczeĂniej, cyfry sÈ wypisywane w niewïaĂciwej kolejnoĂci — mniej znaczÈce sÈ dostÚpne przed bardziej znaczÈcymi. KolejnoĂÊ ich wypisywania musi byÊ odwrotna. SÈ dwa rozwiÈzania tego problemu. Pierwszym jest zapisanie cyfr w tablicy i odwrócenie kolejnoĂci zapisanych elementów. Tak zrobiliĂmy w przykïadowej funkcji itoa w pod- rozdziale 3.6. AlternatywÚ stanowi rozwiÈzanie rekurencyjne, w którym funkcja printd rozpoczyna pracÚ od wywoïania samej siebie w celu wyĂwietlenia cyfr bardziej znaczÈ- cych niĝ cyfra aktualnie przetwarzana. Dopiero po powrocie z wywoïanej funkcji wy- pisywana jest cyfra bieĝÈca. Poniĝej przedstawiamy takÈ funkcjÚ, ponownie w wersji niezapewniajÈcej poprawnego przetwarzania najwiÚkszej liczby ujemnej. #include stdio.h /* printd: wypisuje n jako liczbĊ dziesiĊtną */ void printd(int n) { if (n 0) { putchar( - ); n = -n; } if (n / 10) 105 JĊzyk ANSI C. Programowanie printd(n / 10); putchar(n 10 + 0 ); } Gdy funkcja wywoïuje rekurencyjnie samÈ siebie, kaĝde wywoïanie otrzymuje nowy zestaw wszystkich zmiennych automatycznych, caïkowicie niezaleĝny od wczeĂniejszego. W efekcie po wywoïaniu printd(123) pierwsza funkcja printd otrzymuje argument n = 123. Przekazuje ona 12 do drugiej funkcji printd, która z kolei przekazuje 1 trzeciej. Ta ostatnia wypisuje znak 1 i koñczy pracÚ. Wówczas funkcja na drugim poziomie wypisuje znak 2 i równieĝ koñczy pracÚ. Funkcja najwyĝszego poziomu wypisuje 3 i prze- twarzanie poczÈtkowego wywoïania printd(123) zostaje zakoñczone. Innym ciekawym przykïadem rekurencji jest algorytm sortowania quicksort, opraco- wany przez C.A.R. Hoare’a w 1962 roku. Z tablicy wybierany jest jeden element, a pozo- staïe zostajÈ podzielone na dwa podzbiory — elementów mniejszych oraz elementów wiÚkszych lub równych. Ten sam proces jest nastÚpnie powtarzany rekurencyjnie dla kaĝdego z podzbiorów. Gdy podzbiór ma mniej niĝ dwa elementy, dalsze sortowanie nie jest potrzebne i proces rekurencji zostaje zakoñczony. Nasza wersja programu sortujÈcego metodÈ quicksort nie jest najszybsza, ale za to jest jednÈ z najprostszych. Podziaï bazuje na Ărodkowym elemencie kaĝdej podtablicy. /* qsort: sortuje v[left]...v[right] rosnąco */ void qsort(int v[], int left, int right) { int i, last; void swap(int v[], int i, int j); if (left = right) /* nic nie rób, jeĪeli tablica zawiera */ return; /* mniej niĪ dwa elementy */ swap(v, left, (left + right)/2); /* przenieĞ element partycji */ last = left; /* do v[0] */ for (i = left + 1; i = right; i++) /* partycja */ if (v[i] v[left]) swap(v, ++last, i); swap(v, left, last); /* przywróü element partycji */ qsort(v, left, last-1); qsort(v, last+1, right); } PrzenieĂliĂmy operacjÚ zamieniania elementów miejscami do osobnej funkcji swap — jest przecieĝ wywoïywana w trzech miejscach. /* swap: zamienia miejscami v[i] i v[j] */ void swap(int v[], int i, int j) { int temp; temp = v[i]; 106 Rozdziaá 4. • Funkcje i struktura programu v[i] = v[j]; v[j] = temp; } Standardowa biblioteka zawiera wersjÚ funkcji qsort, która potrafi sortowaÊ obiekty dowolnego typu. Rekurencja nie przyczynia siÚ do oszczÚdzania pamiÚci — stos wykorzystywanych przez kolejne poziomy wywoïañ wartoĂci musi byÊ gdzieĂ przechowywany. Nie jest teĝ rozwiÈzaniem szybszym. Jednak kod rekurencyjny jest bardziej zwarty i czÚsto ïatwiejszy do napisania i intuicyjnego zrozumienia niĝ jego nierekurencyjny odpowiednik. Rekuren- cja jest szczególnie wygodna przy przetwarzaniu rekurencyjnie zdefiniowanych struktur danych, takich jak drzewa. Ciekawy przykïad znajdziemy w podrozdziale 6.5. mwiczenie 4.12. Zaadaptuj koncepcjÚ funkcji printd do napisania rekurencyjnej wersji funkcji itoa. Innymi sïowy, przeksztaïÊ liczbÚ caïkowitÈ na ciÈg znaków, wywoïujÈc procedurÚ rekurencyjnÈ. mwiczenie 4.13. Napisz rekurencyjnÈ wersjÚ funkcji reverse(s), odwracajÈcej „w miejscu” ciÈg znaków s. 4.11. Preprocesor jÚzyka C JÚzyk C realizuje pewne mechanizmy jÚzykowe za poĂrednictwem preprocesora. Jest to pierwszy krok wykonywany przed wïaĂciwym procesem kompilacji. Dwie najczÚĂciej stosowane dyrektywy preprocesora to #include, wïÈczajÈca do procesu kompilacji zawartoĂÊ innego pliku, i #define, zastÚpujÈca nazwÚ wskazanym ciÈgiem znaków. W tym podrozdziale opiszemy teĝ inne moĝliwoĂci preprocesora: kompilacjÚ warunkowÈ i makra z argumentami. 4.11.1. Wstawianie plików Mechanizm wstawiania plików uïatwia przede wszystkim obsïugÚ zbiorów dyrektyw #define i deklaracji. Kaĝdy wiersz postaci lub #include nazwa_pliku #include nazwa_pliku zostaje zastÈpiony zawartoĂciÈ pliku nazwa_pliku. Jeĝeli nazwa pliku jest ujÚta w cudzy- sïów, wyszukiwanie pliku rozpoczyna siÚ najczÚĂciej w katalogu programu ěródïowego. Jeĝeli plik nie zostanie w nim znaleziony albo gdy zamiast cudzysïowu uĝyto znaków i , wyszukiwanie przebiega zgodnie z zasadami okreĂlonymi przez implementacjÚ. WïÈczane dyrektywÈ #include pliki mogÈ takĝe zawieraÊ wiersze #include. 107 JĊzyk ANSI C. Programowanie Na poczÈtku pliku ěródïowego znajduje siÚ najczÚĂciej caïa grupa wierszy #include, które wïÈczajÈ do programu podstawowe instrukcje #define i deklaracje extern. MogÈ równieĝ zapewniaÊ dostÚp do deklaracji prototypów funkcji bibliotecznych, zapisanych w nagïówkach takich jak stdio.h (ĂciĂlej: nagïówki nie muszÈ byÊ plikami; zasady dostÚpu do nagïówków wyznacza implementacja). WïÈczanie wierszem #include to podstawowa metoda ïÈczenia deklaracji w duĝych programach. Gwarantuje ona, ĝe wszystkie pliki ěródïowe bÚdÈ miaïy dostÚp do tych samych definicji i deklaracji zmiennych. Eliminuje to jeden z najbardziej uciÈĝliwych rodzajów bïÚdów w kodzie. Naturalnie gdy wïÈczany plik ulega zmianie, wszystkie zaleĝ- ne od niego pliki programu muszÈ byÊ kompilowane ponownie. 4.11.2. Makra Definicja ma postaÊ: #define nazwa tekst_zastÚpujÈcy Mamy tu do czynienia z najprostszÈ postaciÈ makra, opartÈ na substytucji — wszystkie dalsze wystÈpienia nazwa zostanÈ zastÈpione przez tekst_zastÚpujÈcy. Nazwa w #define ma takÈ samÈ postaÊ jak nazwa zmiennej. Tekst zastÚpujÈcy moĝe byÊ dowolny. Nor- malnie sÈ to wszystkie znaki do koñca wiersza, ale dïuga definicja moĝe zostaÊ podzielona na kilka kolejnych wierszy przez wstawienie znaku na koñcu kaĝdego wiersza, który ma byÊ kontynuowany. Zakres nazwy wskazanej w #define siÚga od wiersza #define do koñca kompilowanego pliku ěródïowego. Definicja moĝe korzystaÊ z wczeĂniejszych definicji. Substytucja nie obejmuje miejsc, w których nazwa jest czÚĂciÈ dïuĝszej nazwy i frag- mentów ujÚtych w cudzysïów. Po zdefiniowaniu na przykïad nazwy YES substytucja nie nastÈpi w printf( YES ) ani w YESMAN. ZastÚpujÈcy nazwÚ tekst moĝe byÊ dowolny. Na przykïad #define forever for (;;) /* pĊtla nieskoĔczona */ definiuje nowe sïowo, forever, które bÚdzie zastÚpowane pÚtlÈ nieskoñczonÈ. Moĝna takĝe definiowaÊ makra z argumentami, dziÚki którym tekst zastÚpujÈcy jest róĝny w poszczególnych wywoïaniach makra. Przykïadem moĝe byÊ makro max: #define max(A, B) ((A) (B) ? (A) : (B)) ChoÊ wyglÈda jak wywoïanie funkcji, uĝycie max sprowadza siÚ do rozwiniÚcia nazwy w kod wstawiany wewnÈtrz wiersza. Kaĝde wystÈpienie parametru formalnego (tutaj A i B) zostanie zastÈpione podanym argumentem. Tak wiÚc wiersz x = max(p+q, r+s); przyjmie postaÊ x = ((p+q) (r+s) ? (p+q) : (r+s)); 108 Rozdziaá 4. • Funkcje i struktura programu Dopóki argumenty sÈ spójne, makro max moĝe pracowaÊ z dowolnym typem danych. Nie ma potrzeby definiowania róĝnych nazw max dla róĝnych typów danych, tak jakby to byïo w przypadku zastosowania funkcji. Gdy przyjrzymy siÚ sposobowi rozwijania makra max, zwrócimy uwagÚ, ĝe wiÈĝe siÚ on z pewnymi puïapkami. WartoĂci wyraĝeñ sÈ obliczane dwukrotnie. Staje siÚ to istot- nym problemem, gdy pojawiajÈ siÚ efekty uboczne wynikajÈce ze stosowania operatorów zwiÚkszania i zmniejszania albo operacji wejĂcia-wyjĂcia. Przykïadowo max(i++, j++) /* BàĄD */ prowadzi do dwukrotnego zwiÚkszenia wiÚkszej wartoĂci. CzÚsto warto zadbaÊ o ujÚcie wyraĝenia w nawiasy, aby zapewniÊ wïaĂciwÈ kolejnoĂÊ wykonywania obliczeñ. PomyĂl- my, co siÚ stanie, gdy makro #define square(x) x * x /* BàĄD */ zostanie wywoïane w wyraĝeniu square(z+1). Makra sÈ bardzo wartoĂciowym narzÚdziem. Jednym z praktycznych przykïadów ich zastosowania jest wïÈczanie do kompilacji pliku stdio.h , w którym operacje getchar i putchar sÈ czÚsto zdefiniowane jako makra. Pozwala to uniknÈÊ obciÈĝenia programu mechanizmem wywoïywania funkcji przy odczycie pojedynczych znaków. Równieĝ funkcje w ctype.h sÈ zazwyczaj implementowane jako makra. Definicje nazw moĝna wycofywaÊ dyrektywÈ #undef. MoĝliwoĂÊ tÚ wykorzystuje siÚ czÚsto w celu uzyskania gwarancji, ĝe dana procedura bÚdzie funkcjÈ, a nie makrem: #undef getchar int getchar(void) { ... } Parametry formalne nie sÈ zastÚpowane w ciÈgach znakowych otoczonych znakami cudzysïowu. Jeĝeli jednak nazwÚ parametru poprzedza w tekĂcie zastÚpujÈcym znak #, to zostanie on zamieniony na ujÚty w cudzysïów ciÈg znaków, w którym parametr jest zastÈpiony podanym argumentem faktycznym. W poïÈczeniu z konkatenacjÈ ciÈgów pozwala to na przykïad utworzyÊ nastÚpujÈce makro wyĂwietlajÈce wartoĂci potrzebne w procesie debugowania: #define dprint(expr) printf(#expr = g , expr) Po jego wywoïaniu, na przykïad w instrukcji dprint(x/y); makro zostaje rozwiniÚte do postaci printf( x/y = g , x/y); ciÈgi znakowe sÈ automatycznie ïÈczone, wiÚc w efekcie uzyskujemy printf( x/y = g , x/y); 109 JĊzyk ANSI C. Programowanie W argumencie faktycznym kaĝdy znak jest zastÚpowany przez , a kaĝdy znak przez \, dziÚki czemu wynik to poprawna staïa tekstowa. Operator preprocesora ## umoĝliwia konkatenowanie argumentów faktycznych w trakcie rozwijania makr. Jeĝeli parametr w tekĂcie zastÚpujÈcym sÈsiaduje ze znakami ##, parametr ten jest zastÚpowany argumentem faktycznym, znaki ## i biaïe znaki zostajÈ usuniÚte, a wynik jest analizowany ponownie. Przykïadowo makro paste ïÈczy dwa argumenty: #define paste(front, back) front ## back wiÚc paste(name, 1) tworzy nazwÚ name1. Reguïy zagnieĝdĝania operatora ## sÈ doĂÊ zïoĝone. Szczegóïy moĝna znaleěÊ w dodatku A. mwiczenie 4.14. Zdefiniuj makro swap(t,x,y) wymieniajÈce wartoĂci dwóch argumentów, których typ to t (pomocna bÚdzie struktura blokowa). 4.11.3. Warunkowe wstawianie kodu Istnieje moĝliwoĂÊ sterowania pracÈ samego preprocesora przy uĝyciu instrukcji wa- runkowych, wykonywanych w trakcie jego dziaïania. Zapewnia to moĝliwoĂÊ wybiórcze- go wstawiania kodu, w zaleĝnoĂci od warunków, których wartoĂci sÈ obliczane w czasie kompilowania. Wiersz #if oblicza wartoĂÊ staïego wyraĝenia caïkowitego (które nie moĝe zawieraÊ operatora sizeof, konwersji typów i staïych enum). Jeĝeli wyraĝenie ma wartoĂÊ róĝnÈ od zera, wstawione zostajÈ dalsze wiersze, aĝ do wiersza #endif, #elif lub #else (instrukcja preprocesora #elif odpowiada else if). Wyraĝenie defined(nazwa) w wierszu #if ma wartoĂÊ 1, jeĝeli nazwa zostaïa wczeĂniej zdefiniowana, a 0 w pozostaïych przypadkach. Aby na przykïad zapewniÊ, ĝe zawartoĂÊ pliku hdr.h bÚdzie wïÈczana do kodu tylko raz, moĝna otoczyÊ jÈ wierszami dyrektyw warunkowych: #if !defined(HDR) #define HDR /* tu znajduje siĊ wáaĞciwa treĞü nagáówka hdr.h */ #endif Pierwsza operacja wïÈczania pliku hdr.h powoduje zdefiniowanie nazwy HDR. Przy kolej- nych próbach wïÈczenia preprocesor stwierdza, ĝe nazwa zostaïa juĝ zdefiniowana, i prze- chodzi do wiersza #endif. PodejĂcie takie moĝna stosowaÊ bardzo szeroko. Zachowanie peïnej konsekwencji pozwala w kaĝdym nagïówku wïÈczaÊ do kompilacji dowolne inne wymagane nagïówki bez ciÈgïego Ăledzenia ich wzajemnych zaleĝnoĂci. 110 Rozdziaá 4. • Funkcje i struktura programu NastÚpujÈca sekwencja sprawdza tekst powiÈzany z nazwÈ SYSTEM, aby okreĂliÊ, która wersja nagïówka ma zostaÊ wïÈczona do kodu: #if SYSTEM == SYSV #define HDR sysv.h #elif SYSTEM == BSD #define HDR bsd.h #elif SYSTEM == MSDOS #define HDR msdos.h #else #define HDR default.h #endif #include HDR Wiersze #ifdef i #ifndef to wyspecjalizowane formy sprawdzenia, czy nazwa zostaïa zde- finiowana. WczeĂniejszy przykïad z #if moĝna zapisaÊ jako #ifndef HDR #define HDR /* tu znajduje siĊ wáaĞciwa treĞü nagáówka hdr.h */ #endif 111
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

Język ANSI C. Programowanie. Wydanie II
Autor:
,

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: