Académique Documents
Professionnel Documents
Culture Documents
Programowanie. Wydanie II
Autorzy: Brian W. Kernighan, Dennis M. Ritchie
Tumaczenie: Pawe Koronkiewicz
ISBN: 978-83-246-2578-9
Tytu oryginau: C Programming Language (2nd Edition)
Format: 158235, stron: 328
Drogi Czytelniku, wanie trzymasz w rkach nowe wydanie ksiki zaliczanej do klasyki
literatury informatycznej. Napisana przez autorw jzyka ANSI C w najlepszy moliwy
sposb przedstawia arkana tego jzyka. A co mona powiedzie o samym jzyku? To
te klasyka. To jzyk wymagajcy systematycznoci i skupienia, ale dajcy w zamian
wiele moliwoci i wietne wyniki. To najczciej nauczany jzyk programowania jego
znajomo stanowi znakomity fundament do poznania kolejnych, bardziej zoonych
jzykw. Mimo swojego zaawansowanego wieku jest on ceniony i w wielu dziedzinach
wci niezastpiony.
Dziki tej ksice zdobdziesz kompletn wiedz na temat jzyka C. Poznasz wszystkie
dostpne typy, operatory i wyraenia. Nauczysz si sterowa wykonywaniem programu
oraz wykorzystywa funkcje. Ponadto dogbnie poznasz co, co sprawia pocztkujcym
programistom najwicej problemw wskaniki. Nastpnie zapoznasz si take
z funkcjami wejcia i wyjcia. Dowiesz si, jak uzyska dostp do plikw, formatowa
dane wyjciowe oraz obsugiwa bdy. Ksika ta jest bogata w przykady, a kady
z nich zosta przetestowany przez autorw. Jzyk ANSI C. Programowanie. Wydanie II
to niezastpiona pozycja na pce kadego studenta informatyki, pasjonata programowania
i zawodowca. Wraz z ksik zosta wydany zeszyt zawierajcy rozwizania do
wszystkich zawartych w niej wicze.
Zmienne i wyraenia arytmetyczne w jzyku C
Kompilowanie kodu
Wykorzystanie preprocesora jzyka C
Typy i operatory
Metody sterowania wykonywaniem programu
Wykorzystanie funkcji
Struktura programu
Zasada dziaania wskanikw
Struktury danych
Operacje wejcia i wyjcia
Zastosowanie rekurencji
Poznaj tajniki jzyka C!
Spis treci
Przedmowa
Wstp
11
Rozdzia 1. Wprowadzenie
15
1.1.
1.2.
1.3.
1.4.
1.5.
1.6.
1.7.
1.8.
1.9.
1.10.
Pierwsze kroki
Zmienne i wyraenia arytmetyczne
Instrukcja for
Stae symboliczne
Znakowe operacje wejcia-wyjcia
Tablice
Funkcje
Argumenty przekazywanie jako warto
Tablice znakw
Zmienne zewntrzne i zakres zmiennych
Nazwy zmiennych
Typy danych i ich rozmiar
Stae
Deklaracje
Operatory arytmetyczne
Operatory porwnania i logiczne
Konwersja typw
Inkrementacja i dekrementacja
Operatory bitowe
Operatory i wyraenia przypisania
16
18
24
26
26
34
36
40
41
44
49
49
50
51
54
55
56
57
61
63
65
2.11.
2.12.
Wyraenia warunkowe
Priorytety operatorw i kolejno wykonywania oblicze
Instrukcje i bloki
if-else
else-if
switch
Ptle while i for
Ptla do-while
break i continue
goto i etykiety
Funkcje podstawy
Zwracanie wartoci innych ni int
Zmienne zewntrzne
Zakres
Pliki nagwkowe
Zmienne statyczne
Zmienne rejestrowe
Struktura blokowa
Inicjalizacja
Rekurencja
Preprocesor jzyka C
Wskaniki i adresy
Wskaniki i argumenty funkcji
Wskaniki i tablice
Arytmetyka adresw
Wskaniki znakowe i funkcje
Tablice wskanikw, wskaniki do wskanikw
Tablice wielowymiarowe
Inicjalizacja tablic wskanikw
Wskaniki a tablice wielowymiarowe
Argumenty wiersza polece
Wskaniki do funkcji
Rozbudowane deklaracje zmiennych i funkcji
Rozdzia 6. Struktury
6.1.
6.2.
6.3.
6.4.
6.5.
Struktury podstawy
Struktury i funkcje
Tablice struktur
Wskaniki do struktur
Struktury cykliczne (odwouj ce si do siebie)
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
Spis treci
6.6.
6.7.
6.8.
6.9.
Wyszukiwanie w tabelach
typedef
union
Pola bitowe
Deskryptory plikw
Niskopoziomowe operacje wejcia-wyjcia odczyt i zapis
open, creat, close, unlink
lseek dostp swobodny
Przykad implementacja fopen i getc
Przykad listy zawartoci katalogw
Przykad mechanizm alokacji pamici
Wprowadzenie
Konwencje leksykalne
Zapis skadni
Identyfikatory obiektw
Obiekty i L-wartoci
Konwersje
Wyraenia
Deklaracje
Instrukcje
Deklaracje zewntrzne
Zakres i wi zanie
Przetwarzanie wstpne
Gramatyka
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
B.7.
B.8.
B.9.
B.10.
B.11.
298
298
299
300
302
305
Skorowidz
309
Rozdzia 4.
Funkcje
i struktura programu
Funkcje dziel due zadania obliczeniowe na mniejsze oraz umoliwiaj wielokrotne
wykorzystywanie tego samego kodu. Waciwie napisane funkcje ukrywaj szczegy
swoich mechanizmw przed innymi czciami programu, dla ktrych s one nieistotne.
Zapewnia to przejrzysto i znacznie uatwia wprowadzanie zmian.
Jzyk C zosta zaprojektowany w taki sposb, aby korzystanie z funkcji byo efektywne
i atwe. Program skada si z reguy z duej liczby maych funkcji. Due funkcj s
stosowane rzadko. Program moe by zapisany w jednym lub wielu plikach. Pliki
rdowe programu mog by kompilowane niezalenie i pniej jednoczenie adowanedo pamici razem z wczeniej skompilowanymi funkcjami bibliotek. Nie bdziemy
omawia tu dokadnie tego rodzaju procedur, poniewa rni si one w zalenoci
od stosowanego systemu.
Deklaracja i definicja funkcji to obszar, w ktrym norma ANSI wprowadzia najbardziej
rzucajce si w oczy zmiany w jzyku C. Jak widzielimy ju w rozdziale 1., mona teraz
okrela w deklaracji funkcji typy jej argumentw. Skadnia definicji funkcji rwnie
jest zmieniona, dziki czemu deklaracja i definicja maj tak sam posta . Umoliwia
to kompilatorowi wykrycie znacznie wikszej liczby bdw ni wczeniej. Co wicej,
waciwy sposb deklarowania argumentw zapewnia automatyczne konwersje typw.
Standard ucila reguy dotyczce zakresu nazw. W szczeglnoci wymaga on, aby kady
obiekt zewntrzny mia tylko jedn definicj. Mechanizm inicjalizacji zosta uoglniony
w ANSI C mona inicjalizowa tablice i struktury automatyczne.
Preprocesor jzyka rwnie zosta usprawniony. Jego nowe mechanizmy obejmuj
peniejszy zbir dyrektyw kompilacji warunkowej, moliwo budowania cigw znakowych z argumentw makr oraz wiksz kontrol nad procesem rozwijania makra.
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!
Cho jest oczywicie moliwe umieszczenie caego kodu w funkcji main, lepszym podejciem okazuje si wykorzystanie moliwoci strukturalizowania kodu i zapisanie
kadej czci w odrbnej funkcji. Z trzema maymi elementami atwiej pracowa ni
z jednym duym nieistotne szczegy pozostaj ukryte w funkcjach, a prawdopodobiestwo wystpienia niepodanych interakcji jest ograniczone do minimum.
Co wicej, gotowe elementy mog znale zastosowanie w innych programach.
while jest kolejny wiersz to funkcja getline, ktr napisalimy ju w rozdziale 1.
wypisz wiersz to funkcja printf, dostpna w standardowej bibliotece. Oznacza to,
e musimy jedynie napisa procedur okrelajc, czy wiersz zawiera wzorzec.
Moemy rozwiza ten problem, piszc funkcj strindex(s,t), ktra zwraca pozycj
(indeks) w cigu s, od ktrego zaczyna si cig t, lub -1, jeeli s nie zawiera t. Poniewa
pierwsza pozycja w tablicach jzyka C ma indeks 0, indeksy bd miay wartoci dodatnie
lub 0, a warto ujemna, taka jak -1, moe zosta wykorzystana do sygnalizowania
nieudanego wyszukiwania. Jeeli w przyszoci bdzie potrzebny bardziej wyszukany
mechanizm wyszukiwania wzorcw, bdzie mona wymieni funkcj strindex na inn.
Reszta kodu pozostanie bez zmian (standardowa biblioteka zawiera funkcj strstr,
ktra jest podobna do strindex, ale zwraca wskanik zamiast indeksu).
Po takim przygotowaniu projektu napisanie waciwego programu jest ju czynnoci
stosunkowo prost. Poniej przedstawiono cao , zarwno gwny program, jak i stosowane funkcje, aby Czytelnik mg wygodnie przeanalizowa ich wspdziaanie. W tej
wersji wyszukiwany cig jest literaem (sta), wic trudno mwi o oglnoci rozwizania.
Do inicjalizowania tablic znakowych powrcimy ju wkrtce, natomiast w rozdziale 5.
86
87
czyli funkcja, ktra nic nie robi i nic nie zwraca. Funkcja tego rodzaju okazuje si czasem
przydatna jako tymczasowa atrapa w trakcie pracy nad programem. Jeeli zwracany typ
danych nie zosta okrelony, kompilator przyjmuje, e jest to int.
Program to po prostu zbir definicji zmiennych i funkcji. Komunikacja midzy funkcjami
odbywa si za porednictwem argumentw funkcji, wartoci zwracanych przez funkcje
i zmiennych zewntrznych. Funkcje mog by umieszczone w pliku rdowym w dowolnej kolejnoci, a program moe by podzielony na wiele plikw rdowych, o ile tylko
kada funkcja znajduje si w caoci w jednym pliku.
Instrukcja return reprezentuje mechanizm zwracania wartoci z funkcji wywoywanej
do funkcji lub rodowiska wywoujcego. Po sowie return moe znajdowa si dowolne
wyraenie:
return wyraenie;
88
kompiluje trzy wymienione pliki, umieszcza kod obiektw w plikach main.o, getline.o
i strindex.o, a nastpnie aduje je wszystkie do pliku wykonywalnego o nazwie a.out.
W przypadku wystpienia bdu, na przykad w main.c, plik moe zosta skompilowany
ponownie niezalenie od innych i zaadowany razem z przygotowanymi wczeniej.
Umoliwia to polecenie
cc main.c getline.o strindex.o
89
Drug, rwnie wan rzecz jest to, e procedura wywoujca musi wiedzie , e funkcja
atof zwraca warto inn ni int. Jedn z moliwoci zapewnienia tego jest jawne
zadeklarowanie atof w tej procedurze. Deklaracj tak wida w programie minimalistycznego kalkulatora (nadajcego si chyba tylko do podliczania wypat z bankomatu),
ktry sumuje pobieran z wejcia pojedyncz kolumn liczb. Liczby mog zawiera znak,
a po kadej jest drukowana suma pobranych ju wartoci:
#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("\t%g\n", sum += atof(line));
return 0;
}
Deklaracja
double sum, atof(char []);
mwi, e sum to zmienna typu double, a atof to funkcja, ktra pobiera jeden argument
char[] i zwraca warto double.
Deklaracja i definicja funkcji musz by zgodne. Jeeli definicja funkcji i jej wywoanie
w main maj niespjnie okrelone typy, a s w tym samym pliku rdowym, kompilator
zgosi bd. Jednak gdy (co jest bardziej prawdopodobne) funkcja atof bdzie kompilowana
niezalenie, brak zgodnoci nie zostanie wykryty, a funkcja zwrci liczb double, ktra
w main bdzie traktowana jako int uzyskiwane wtedy wartoci bd niemal zupenie przypadkowe.
90
Jeeli nazwa, ktra nie zostaa wczeniej zadeklarowana, wystpuje w wyraeniu, a bezporednio po niej jest umieszczony otwierajcy znak nawiasu, nastpuje deklaracja na
podstawie kontekstu dana nazwa jest uznawana za nazw funkcji, ktra zwraca
warto int. Nie s natomiast przyjmowane adne zaoenia dotyczce jej argumentw.
Co wicej, jeeli deklaracja funkcji nie zawiera argumentw, jak w instrukcji
double atof();
zostaje przeksztacona na typ wartoci zwracanej przez funkcj przed wyjciem z tej
funkcji. Warto atof, typu double, jest konwertowana automatycznie na int po dojciu
do tego wiersza funkcja atoi ma zwraca liczb cakowit. Operacja taka moe
prowadzi do utraty czci danych (czci uamkowej liczby), wic niektre kompilatory
generuj po jej napotkaniu ostrzeenie. Operacja (int) jest jawn informacj o tym, e
konwersja typu jest zamierzona, dziki czemu ostrzeenie nie jest wywietlane.
wiczenie 4.2. Dodaj do funkcji atof moliwo obsugi notacji wykadniczej, postaci:
123.45e-6
gdzie po liczbie zmiennoprzecinkowej moe wystpi litera e lub E i wykadnik, z opcjonalnym znakiem.
91
92
Nawiasy nie s potrzebne. Notacja jest jednoznaczna, o ile tylko liczba operandw kadego
operatora jest staa.
Implementacja jest prosta. Kady operand zostaje umieszczony na stosie. Po pobraniu
operatora program zdejmuje ze stosu waciw liczb operandw (dwa w przypadku
operatorw binarnych), wykonuje operacj i zapisuje wynik ponownie na stosie. W powyszym przykadzie oznacza to umieszczenie na stosie liczb 1 i 2, nastpnie zastpienie
ich rnic, 1. W kolejnym kroku na stos trafiaj liczby 4 i 5, ktre zostaj nastpnie
zastpione sum, 9. Kolejna operacja to mnoenie, wic ze stosu zostaj pobrane wartoci
1 i 9, ktre zastpuje nastpnie ich iloczyn, 9. Na zakoczenie, po dojciu do koca
wiersza, warto ze szczytu stosu zostaje wypisana na ekranie.
Struktura programu jest wic ptl, ktra wykonuje odpowiednie operacje na pobieranych kolejno operatorach i operandach:
while (nastpny operator lub operand nie jest znakiem koca 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
bd
93
Zagadnieniem dzielenia programu na dwa pliki rdowe lub wicej zajmiemy si ju
niedugo.
Funkcja main to ptla zawierajca rozbudowan instrukcj switch, ktra rozgazia
sterowanie w zalenoci od typu operatora lub operandu. Jest to bardziej typowy przykad jej uycia ni ten przedstawiony w podrozdziale 3.4.
#include <stdio.h>
#include <stdlib.h> /* dla atof() */
#define MAXOP 100
#define NUMBER '0'
94
op2 = pop();
if (op2 != 0.0)
push(pop() / op2);
else
printf("error: zero divisor\n");
break;
case '\n':
printf("\t%.8g\n", pop());
break;
default:
printf("error: unknown command %s\n", s);
break;
}
}
return 0;
}
push(pop() pop());
kolejno obliczania wartoci wywoa pop nie jest okrelona. Aby zagwarantowa waciw, konieczne jest wczeniejsze pobranie pierwszej wartoci do zmiennej tymczasowej.
Wida to w kodzie funkcji main.
#define MAXVAL 100
int sp = 0;
/* nastpna 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\n", f);
}
/* pop: zdejmuje i zwraca warto z wierzchoka stosu */
double pop(void)
{
if (sp > 0)
return val[--sp];
else {
printf("error: stack empty\n");
return 0.0;
}
}
95
Zmienna jest zmienn zewntrzn, jeeli jest zdefiniowana poza funkcj, tak wic
wspuytkowane przez funkcje pop i push zmienne stosu i indeksu stosu zostaj zdefiniowane poza tymi funkcjami. Jednak funkcja main nie odwouje si do stosu ani jego
indeksu reprezentacja moe pozosta ukryta.
Przejdmy teraz do implementacji getop, funkcji, ktra pobiera kolejny operator lub operand. Zadanie jest proste. Pomijamy spacje i tabulatory. Jeeli nastpny znak nie jest
cyfr lub kropk dziesitn, zwracamy go. W pozostaych przypadkach pobieramy cig
cyfr (ktry moe zawiera kropk dziesitn) i zwracamy NUMBER, czyli warto sygnalizujc, e pobrana zostaa liczba.
#include <ctype.h>
int getch(void);
void ungetch(int);
/* getop: pobiera nastpny operator lub operand (liczb) */
int getop(char s[])
{
int i, c;
while ((s[0] = c = getch()) == ' ' || c == '\t')
;
s[1] = '\0';
if (!isdigit(c) && c != '.')
return c;
/* nie jest liczb */
i = 0;
if (isdigit(c)) /* pobierz cz cakowit */
while (isdigit(s[++i] = c = getch()))
;
if (c == '.') /* pobierz cz uamkow */
while (isdigit(s[++i] = c = getch()))
;
s[i] = '\0';
if (c != EOF)
ungetch(c);
return NUMBER;
}
Co to za funkcje getch i ungetch? Czsto zdarza si, e program nie moe okreli , czy
odczyta wystarczajc ilo danych wejciowych a do momentu, gdy odczyta ich zbyt
duo. Takim przypadkiem jest wanie odczytywanie znakw tworzcych liczb: do czasu
odczytania pierwszego znaku, ktry nie jest cyfr, pobierana liczba pozostaje niekompletna.
Jednak jest to moment, gdy program odczyta ju o jeden znak za duo, znak, na ktry nie
jest przygotowany.
Problem byby rozwizany, gdyby istniaa moliwo cofnicia operacji odczytu ostatniego
znaku danych wejciowych. Wwczas program, ktry odczyta o jeden znak za duo,
mgby odda ten znak do strumienia, a inne elementy programu dziaayby tak,
96
jak gdyby znak ten nigdy nie by odczytywany. Okazuje si, e skonstruowanie takiego
mechanizmu nie jest trudne, wystarczy para wsppracujcych ze sob funkcji. getch
zwraca kolejny znak danych wejciowych. ungetch zapamituje znaki zwrcone na
wejcie w taki sposb, aby dalsze wywoania getch zwracay je przed odczytaniem
nowych z rzeczywistego strumienia.
Ich wsppraca jest prosta. ungetch zapisuje wycofane znaki we wsplnym buforze
tablicy znakw. getch odczytuje zawarto bufora, jeeli nie jest on pusty. W pozostaych przypadkach wywouje po prostu funkcj getchar. Niezbdna jest rwnie
zmienna indeksujca, ktra rejestruje pozycj biecego znaku w buforze.
Poniewa bufor i indeks wykorzystuj dwie funkcje, getch i ungetch, a wartoci tych
zmiennych musz zosta zachowane midzy wywoaniami, konieczne jest uycie
zmiennych zewntrznych. Obie funkcje i deklaracje zmiennych mona zapisa tak:
#define BUFSIZE 100
char buf[BUFSIZE]; /* bufor dla ungetch */
int bufp = 0;
/* nastpna wolna pozycja w buforze */
int getch(void) /* pobiera znak (moe by znakiem wczeniej wycofanym) */
{
return (bufp > 0) ? buf[--bufp] : getchar();
}
void ungetch(int c) /* wycofuje znak do strumienia danych wejciowych */
{
if (bufp >= BUFSIZE)
printf("ungetch: too many characters\n");
else
buf[bufp++] = c;
}
Standardowa biblioteka zawiera funkcj ungetc, ktra umoliwia wycofanie jednego znaku.
Omwimy j w rozdziale 7. W powyszym przykadzie uylimy tablicy, a nie pojedynczego znaku, aby zaprezentowa bardziej oglne podejcie.
wiczenie 4.3. W oparciu o schemat przedstawiony w przykadach program kalkulatora
mona atwo rozbudowywa . Dodaj obsug operatora modulo (%) i obsug liczb ujemnych.
wiczenie 4.4. Utwrz polecenie wypisujce element na wierzchoku stosu bez jego
usuwania ze stosu, polecenie duplikujce element na wierzchoku stosu, polecenie
zamieniajce miejscami dwa grne elementy oraz polecenie usuwajce ca zawarto
stosu.
wiczenie 4.5. Dodaj dostp do funkcji biblioteki, takich jak sin, exp, i pow. Patrz
<math.h> w czci 4. dodatku B.
97
wiczenie 4.6. Dodaj polecenia obsugi zmiennych (atwo jest zapewni moliwo
korzystania z dwudziestu szeciu zmiennych przy uyciu jednoliterowych nazw). Dodaj
zmienn przechowujc ostatni wypisan warto .
wiczenie 4.7. Napisz procedur ungets(s), ktra zwraca do danych wejciowych
cay cig znakw. Czy funkcja ta powinna korzysta ze zmiennych buf i bufp, czy raczej
tylko z funkcji ungetch?
wiczenie 4.8. Zmodyfikuj funkcje getch i ungetch, przyjwszy zaoenie, e nigdy nie
bdzie wycofywany wicej ni jeden znak.
wiczenie 4.9. Nasze funkcje getch i ungetch nie obsuguj poprawnie wycofywania
znaku EOF. Zastanw si, jakie powinny one mie cechy w przypadku cofania znaku EOF,
po czym zaimplementuj now koncepcj.
wiczenie 4.10. Alternatywna organizacja pracy z danymi wejciowymi opiera si na
uyciu getline w celu pobrania caego wiersza. Dziki temu funkcje getch i ungetch nie
s potrzebne. Przekszta kalkulator, tak aby jego praca opieraa si na takim podejciu do
danych wejciowych.
4.4. Zakres
Funkcje i zmienne zewntrzne tworzce program w jzyku C nie musz by kompilowane jednoczenie. rdowy tekst programu mona przechowywa w wielu plikach,
a wczeniej skompilowane procedury mog by adowane z bibliotek. Wie si to z kilkoma istotnymi pytaniami:
Q Jak zapisywa deklaracje, aby deklarowanie zmiennych waciwie przebiegao
w czasie kompilacji?
Q Jaki powinien by ukad deklaracji, aby wszystkie elementy zostay waciwie
poczone w chwili adowania programu?
Q Jaki ukad deklaracji zapewnia, e nie s one powtarzane?
Q Jak inicjuje si zmienne zewntrzne?
98
Zakres zmiennej zewntrznej lub funkcji siga od punktu jej zadeklarowania do koca
kompilowanego pliku. Jeeli na przykad main, sp, val, push i pop s zdefiniowane
w jednym pliku, w kolejnoci przedstawionej wczeniej, czyli
main() { ... }
int sp = 0;
double val[MAXVAL];
void push(double f) { ... }
double pop(void) { ... }
to zmienne sp i val mona stosowa w funkcjach push i pop, po prostu wymieniajc ich
nazw. Nie s wymagane dodatkowe deklaracje. Jednak nazwy te nie s widoczne w main,
podobnie jak funkcje push i pop.
Z drugiej strony, jeeli odwoania do zmiennej zewntrznej maj wystpi przed jej
zdefiniowaniem lub zmienna ta jest definiowana w innym pliku rdowym ni ten,
w ktrym jest wykorzystywana, konieczne staje si uycie deklaracji extern.
Wane jest, aby rozrnia deklaracj zmiennej zewntrznej od jej definicji. Deklaracja
informuje o waciwociach zmiennej (przede wszystkim jej typie). Definicja powoduje
dodatkowo przydzielenie pamici. Jeeli wiersze
int sp;
double val[MAXVAL];
deklaruj na potrzeby kodu w dalszej czci pliku, e sp ma typ int, a val to tablica liczb
double (ktrej rozmiar jest okrelony gdzie indziej). Nie tworz one jednak zmiennych
i nie rezerwuj pamici.
We wszystkich plikach tworzcych program rdowy moe wystpi tylko jedna definicja
zmiennej zewntrznej. Inne pliki mog zawiera deklaracje extern umoliwiajce dostp do tej zmiennej (deklaracje extern mog znale si take w pliku zawierajcym
definicj). Rozmiar tablicy musi zosta okrelony w definicji, a w deklaracji extern
jest opcjonalny.
Inicjalizacja zmiennej zewntrznej moe zosta poczona tylko z jej definicj.
Cho w tym przypadku ukad taki nie ma raczej uzasadnienia, funkcje push i pop mog
by zdefiniowane w jednym pliku, a zmienne val i sp w innym. Wwczas ich powizanie
zostanie zapewnione przez nastpujcy ukad definicji i deklaracji:
99
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 mona
w tych funkcjach stosowa . Jedna para deklaracji wystarczy dla zapewnienia dostpnoci zmiennych w caym pliku file1. Taki sam ukad naleaoby zastosowa , gdyby
definicje sp i val znajdoway si w tym samym pliku, ale po definicjach funkcji, w ktrych
s stosowane.
100
Mamy tu do czynienia z problemem wywaenia midzy deniem do tego, aby kady plik
mia dostp wycznie do tych informacji, ktre s mu niezbdne, a prozaiczn potrzeb
codziennej praktyki praca ze zbyt du liczb plikw nagwka jest uciliwa. Do
pewnych granic dobrym rozwizaniem jest stosowanie jednego nagwka dla caego
programu zawierajcego wszystko, co jest uywane przez wicej ni jedn jego cz .
Takie rozwizanie zastosowalimy w przykadzie. Wiksze programy wymagaj bardziej
rozbudowanej struktury i wikszej liczby nagwkw.
101
to adna inna procedura nie ma dostpu 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 sposb mona ukry zmienne wykorzystywane przez funkcje push i pop do
obsugi stosu deklarujc sp i val jako static.
Zewntrzna deklaracja static jest najczciej stosowana w odniesieniu do zmiennych,
ale moe by uyta take w odniesieniu do funkcji. Normalnie nazwy funkcji maj
charakter globalny s widoczne w caym programie. Jeeli jednak funkcja jest zadeklarowana jako static, jej nazwa nie jest widoczna poza plikiem, w ktrym zostaa
zadeklarowana.
Deklaracji static mona take uy w odniesieniu do zmiennych wewntrznych. Wewntrzne zmienne static pozostaj zmiennymi lokalnymi funkcji, podobnie jak zmienne
automatyczne, jednak w przeciwiestwie do zmiennych automatycznych nie przestaj istnie w chwili wyjcia z funkcji. W efekcie wewntrzne zmienne statyczne to
prywatna pami trwaa pojedynczej funkcji.
wiczenie 4.11. Zmodyfikuj funkcj getop w taki sposb, aby nie korzystaa z funkcji
ungetch. Wskazwka: uyj wewntrznej zmiennej statycznej.
Deklaracje takie mona stosowa wycznie w odniesieniu do zmiennych automatycznych i parametrw formalnych funkcji. W przypadku parametrw formalnych wyglda
to tak:
f(register unsigned m, register long n)
{
register int i;
...
}
W praktyce zmienne rejestrowe podlegaj pewnym ograniczeniom wynikajcym z moliwoci wykorzystywanej platformy sprztowej. Tylko kilka zmiennych w kadej funkcji
mona przechowywa w rejestrach i tylko wybrane typy s dopuszczalne. Nadmiar
deklaracji register jest jednak nieszkodliwy, poniewa w przypadku zbyt duej liczby
102
tak opisanych zmiennych lub niezgodnoci typw sowo register jest ignorowane. Dodatkowo nie mona pobra adresu zmiennej rejestrowej (ten temat omwimy w rozdziale 5.),
niezalenie od tego, czy zostaa ona faktycznie umieszczona w rejestrze. Zakres ogranicze co do typw i liczby zmiennych rejestrowych jest zaleny od komputera.
zakres zmiennej i to blok wykonywany przy wartoci warunku prawda. Zmienna ta nie
ma adnych powiza ze zmiennymi o nazwie i poza blokiem, w ktrym jest zadeklarowana. Zmienna automatyczna deklarowana i inicjalizowana w bloku jest deklarowana
i inicjalizowana przy kadym wejciu do tego bloku. Analogiczna zmienna static jest
inicjalizowana przy pierwszym wejciu do bloku.
Zmienne automatyczne, w tym parametry formalne, rwnie przesaniaj zmienne
zewntrzne i funkcje o tej samej nazwie. W ukadzie deklaracji
int x;
int y;
f(double x)
{
double y;
...
}
wewntrz funkcji f wszystkie wystpienia x odnosz si do parametru (typu double). Poza
funkcj f nazwa zmiennej x odnosi si do liczby int, zmiennej zewntrznej. To samo
mona powiedzie o zmiennej y.
Do dobrej praktyki programowania naley unikanie stosowania nazw zmiennych, ktre
przesaniaj nazwy uywane w szerszym zakresie. Jest to bowiem najkrtsza droga do
pomyek i bdw.
103
4.9. Inicjalizacja
O inicjalizacji wspominalimy ju kilkukrotnie, ale zawsze pozostawaa ona na marginesie innych tematw. W tym podrozdziale, po omwieniu rnych klas pamici danych, moemy przej do usystematyzowania regu tego procesu.
Gdy brak jawnej inicjalizacji, zmienne zewntrzne i statyczne maj warto 0, a zmienne
automatyczne i rejestrowe pozostaj niezdefiniowane nie zawieraj uytecznej wartoci.
Zmienne skalarne mona inicjalizowa przy ich definiowaniu wystarczy wprowadzi
po ich nazwie znak rwnoci i wyraenie:
int x = 1;
char squota = '\'';
long day = 1000L * 60L * 60L * 24L; /* milisekund/dzie */
104
Gdy rozmiar tablicy nie jest okrelony, kompilator okrela j, zliczajc wartoci pocztkowe elementw. W tym przypadku jest ich 12.
Jeeli lista pocztkowych wartoci elementw tablicy zawiera mniej elementw ni
tablica, pozostaym przypisywana jest warto 0. Dotyczy to zmiennych zewntrznych,
statycznych i automatycznych. Podanie zbyt dugiej listy wartoci jest bdem. Nie ma
skadni umoliwiajcej powtarzanie wartoci na licie albo inicjalizowanie elementw
wewntrznych bez podania wartoci wszystkich elementw poprzedzajcych.
Tablice znakw s traktowane w sposb szczeglny. W miejsce nawiasw klamrowych
i rozdzielonej przecinkami listy mona uy cigu:
char pattern = "ould";
W tym przypadku rozmiar tablicy to 5 (cztery znaki plus kocowa staa '\0').
4.10. Rekurencja
Funkcje jzyka C mog by wywoywane rekurencyjnie. Oznacza to, e funkcja moe,
bezporednio lub porednio, wywoa siebie sam. Rozwamy przykad wypisywania
liczby jako cigu znakw. Jak pisalimy wczeniej, cyfry s wypisywane w niewaciwej
kolejnoci mniej znaczce s dostpne przed bardziej znaczcymi. Kolejno ich
wypisywania musi by odwrotna.
S dwa rozwizania tego problemu. Pierwszym jest zapisanie cyfr w tablicy i odwrcenie
kolejnoci zapisanych elementw. Tak zrobilimy w przykadowej funkcji itoa w podrozdziale 3.6. Alternatyw stanowi rozwizanie rekurencyjne, w ktrym funkcja printd
rozpoczyna prac od wywoania samej siebie w celu wywietlenia cyfr bardziej znaczcych ni cyfra aktualnie przetwarzana. Dopiero po powrocie z wywoanej funkcji wypisywana jest cyfra bieca. Poniej przedstawiamy tak funkcj, ponownie w wersji
niezapewniajcej poprawnego przetwarzania najwikszej liczby ujemnej.
#include <stdio.h>
/* printd: wypisuje n jako liczb dziesitn */
void printd(int n)
{
if (n < 0) {
putchar('-');
n = -n;
}
if (n / 10)
105
printd(n / 10);
putchar(n % 10 + '0');
}
Gdy funkcja wywouje rekurencyjnie sam siebie, kade wywoanie otrzymuje nowy
zestaw wszystkich zmiennych automatycznych, cakowicie niezaleny od wczeniejszego.
W efekcie po wywoaniu printd(123) pierwsza funkcja printd otrzymuje argument
n = 123. Przekazuje ona 12 do drugiej funkcji printd, ktra z kolei przekazuje 1 trzeciej.
Ta ostatnia wypisuje znak 1 i koczy prac. Wwczas funkcja na drugim poziomie
wypisuje znak 2 i rwnie koczy prac. Funkcja najwyszego poziomu wypisuje 3 i przetwarzanie pocztkowego wywoania printd(123) zostaje zakoczone.
Innym ciekawym przykadem rekurencji jest algorytm sortowania quicksort, opracowany przez C.A.R. Hoarea w 1962 roku. Z tablicy wybierany jest jeden element, a pozostae zostaj podzielone na dwa podzbiory elementw mniejszych oraz elementw
wikszych lub rwnych. Ten sam proces jest nastpnie powtarzany rekurencyjnie dla
kadego z podzbiorw. Gdy podzbir ma mniej ni dwa elementy, dalsze sortowanie
nie jest potrzebne i proces rekurencji zostaje zakoczony.
Nasza wersja programu sortujcego metod quicksort nie jest najszybsza, ale za to jest
jedn z najprostszych. Podzia bazuje na rodkowym elemencie kadej podtablicy.
/* qsort: sortuje v[left]...v[right] rosnco */
void qsort(int v[], int left, int right)
{
int i, last;
void swap(int v[], int i, int j);
if (left >= right) /* nic nie rb, jeeli 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);
}
106
v[i] = v[j];
v[j] = temp;
}
Standardowa biblioteka zawiera wersj funkcji qsort, ktra potrafi sortowa obiekty
dowolnego typu.
Rekurencja nie przyczynia si do oszczdzania pamici stos wykorzystywanych
przez kolejne poziomy wywoa wartoci musi by gdzie przechowywany. Nie jest te
rozwizaniem szybszym. Jednak kod rekurencyjny jest bardziej zwarty i czsto atwiejszy
do napisania i intuicyjnego zrozumienia ni jego nierekurencyjny odpowiednik. Rekurencja jest szczeglnie wygodna przy przetwarzaniu rekurencyjnie zdefiniowanych struktur
danych, takich jak drzewa. Ciekawy przykad znajdziemy w podrozdziale 6.5.
wiczenie 4.12. Zaadaptuj koncepcj funkcji printd do napisania rekurencyjnej wersji
funkcji itoa. Innymi sowy, przekszta liczb cakowit na cig znakw, wywoujc
procedur rekurencyjn.
wiczenie 4.13. Napisz rekurencyjn wersj funkcji reverse(s), odwracajcej w miejscu
cig znakw s.
lub
#include <nazwa_pliku>
zostaje zastpiony zawartoci pliku nazwa_pliku. Jeeli nazwa pliku jest ujta w cudzysw, wyszukiwanie pliku rozpoczyna si najczciej w katalogu programu rdowego.
Jeeli plik nie zostanie w nim znaleziony albo gdy zamiast cudzysowu uyto znakw
< i >, wyszukiwanie przebiega zgodnie z zasadami okrelonymi przez implementacj.
Wczane dyrektyw #include pliki mog take zawiera wiersze #include.
107
Na pocztku pliku rdowego znajduje si najczciej caa grupa wierszy #include,
ktre wczaj do programu podstawowe instrukcje #define i deklaracje extern. Mog
rwnie zapewnia dostp do deklaracji prototypw funkcji bibliotecznych, zapisanych
w nagwkach takich jak <stdio.h> (cilej: nagwki nie musz by plikami; zasady
dostpu do nagwkw wyznacza implementacja).
Wczanie wierszem #include to podstawowa metoda czenia deklaracji w duych
programach. Gwarantuje ona, e wszystkie pliki rdowe bd miay dostp do tych
samych definicji i deklaracji zmiennych. Eliminuje to jeden z najbardziej uciliwych
rodzajw bdw w kodzie. Naturalnie gdy wczany plik ulega zmianie, wszystkie zalene od niego pliki programu musz by kompilowane ponownie.
4.11.2. Makra
Definicja ma posta :
#define nazwa tekst_zastpujcy
Cho wyglda jak wywoanie funkcji, uycie max sprowadza si do rozwinicia nazwy w kod
wstawiany wewntrz wiersza. Kade wystpienie parametru formalnego (tutaj A i B)
zostanie zastpione podanym argumentem. Tak wic wiersz
x = max(p+q, r+s);
przyjmie posta
x = ((p+q) > (r+s) ? (p+q) : (r+s));
108
Dopki argumenty s spjne, makro max moe pracowa z dowolnym typem danych.
Nie ma potrzeby definiowania rnych nazw max dla rnych typw danych, tak jakby
to byo w przypadku zastosowania funkcji.
Gdy przyjrzymy si sposobowi rozwijania makra max, zwrcimy uwag, e wie si
on z pewnymi puapkami. Wartoci wyrae s obliczane dwukrotnie. Staje si to istotnym problemem, gdy pojawiaj si efekty uboczne wynikajce ze stosowania operatorw
zwikszania i zmniejszania albo operacji wejcia-wyjcia. Przykadowo
max(i++, j++) /* B
D */
109
W argumencie faktycznym kady znak " jest zastpowany przez \", a kady znak \
przez \\, dziki czemu wynik to poprawna staa tekstowa.
Operator preprocesora ## umoliwia konkatenowanie argumentw faktycznych w trakcie
rozwijania makr. Jeeli parametr w tekcie zastpujcym ssiaduje ze znakami ##, parametr
ten jest zastpowany argumentem faktycznym, znaki ## i biae znaki zostaj usunite,
a wynik jest analizowany ponownie. Przykadowo makro paste czy dwa argumenty:
#define paste(front, back) front ## back
Pierwsza operacja wczania pliku hdr.h powoduje zdefiniowanie nazwy HDR. Przy kolejnych prbach wczenia preprocesor stwierdza, e nazwa zostaa ju zdefiniowana, i przechodzi do wiersza #endif. Podejcie takie mona stosowa bardzo szeroko. Zachowanie
penej konsekwencji pozwala w kadym nagwku wcza do kompilacji dowolne inne
wymagane nagwki bez cigego ledzenia ich wzajemnych zalenoci.
110
Nastpujca sekwencja sprawdza tekst powizany z nazw SYSTEM, aby okreli , ktra
wersja nagwka ma zosta wczona 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 zostaa zdefiniowana. Wczeniejszy przykad z #if mona zapisa jako
#ifndef HDR
#define HDR
/* tu znajduje si waciwa tre nagwka hdr.h */
#endif
111