Vous êtes sur la page 1sur 32

Jzyk ANSI C.

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

Przedmowa do pierwszego wydania

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

Rozdzia 2. Typy, operatory i wyraenia


2.1.
2.2.
2.3.
2.4.
2.5.
2.6.
2.7.
2.8.
2.9.
2.10.

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

Jzyk ANSI C. Programowanie

2.11.
2.12.

Wyraenia warunkowe
Priorytety operatorw 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
Ptle while i for
Ptla 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 wartoci innych ni int
Zmienne zewntrzne
Zakres
Pliki nagwkowe
Zmienne statyczne
Zmienne rejestrowe
Struktura blokowa
Inicjalizacja
Rekurencja
Preprocesor jzyka C

Rozdzia 5. Wskaniki i tablice


5.1.
5.2.
5.3.
5.4.
5.5.
5.6.
5.7.
5.8.
5.9.
5.10.
5.11.
5.12.

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

Rozdzia 7. Wejcie i wyjcie


7.1.
7.2.
7.3.
7.4.
7.5.
7.6.
7.7.
7.8.

Standardowe operacje wejcia-wyjcia


printf formatowanie danych wyjciowych
Listy argumentw o zmiennej dugoci
scanf formatowane dane wejciowe
Dostp do plikw
stderr i exit obsuga bdw
Wierszowe operacje wejcia-wyjcia
Inne funkcje

Rozdzia 8. Interfejs systemu UNIX


8.1.
8.2.
8.3.
8.4.
8.5.
8.6.
8.7.

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

Dodatek A Opis jzyka C


A.1.
A.2.
A.3.
A.4.
A.5.
A.6.
A.7.
A.8.
A.9.
A.10.
A.11.
A.12.
A.13.

Wprowadzenie
Konwencje leksykalne
Zapis skadni
Identyfikatory obiektw
Obiekty i L-wartoci
Konwersje
Wyraenia
Deklaracje
Instrukcje
Deklaracje zewntrzne
Zakres i wi zanie
Przetwarzanie wstpne
Gramatyka

Dodatek B Standardowa biblioteka jzyka C


B.1.
B.2.
B.3.
B.4.
B.5.
B.6.

Wejcie i wyjcie: <stdio.h>


Wykrywanie klas znakw: <ctype.h>
Ci gi znakowe: <string.h>
Funkcje matematyczne: <math.h>
Funkcje narzdziowe: <stdlib.h>
Diagnostyka: <assert.h>

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

Jzyk ANSI C. Programowanie

B.7.
B.8.
B.9.
B.10.
B.11.

Listy argumentw o zmiennej dugoci: <stdarg.h>


Skoki odlege: <setjmp.h>
Sygnay: <signal.h>
Data i godzina: <time.h>
Ograniczenia okrelane przez implementacj: <limits.h> i <float.h>

298
298
299
300
302

Dodatek C Podsumowanie zmian

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.

Jzyk ANSI C. Programowanie

4.1. Funkcje podstawy


Na pocztek zaprojektujemy i napiszemy program, ktry wypisuje kady wiersz danych
wejciowych zawierajcy okrelony wzorzec cig znakw (bdzie to uproszczona
wersja programu grep systemu UNIX). Przykadowo 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 mona podzieli w naturalny sposb na trzy czci:


while (jest kolejny wiersz)
if (wiersz zawiera wzorzec)
wypisz wiersz

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

Rozdzia 4. Funkcje i struktura programu

pokaemy, jak przeksztaci wzorzec w parametr przekazywany przy uruchamianiu


programu. Kod zawiera take nieco zmodyfikowan funkcj getline. Porwnanie
jej z wersj z rozdziau 1. moe dostarczy wartociowych spostrzee.
#include <stdio.h>
#define MAXLINE 1000

/* dopuszczalna dugo wiersza */

int getline(char line[], int max)


int strindex(char source[], char searchfor[]);
char pattern[] = "ould"; /* wzorzec do wyszukania */
/* wyszukuje wszystkie wiersze zawierajce 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 dugo */
int getline(char s[], int lim)
{
int c, i;
i = 0;
while (--lim > 0 && (c=getchar()) != EOF && c != '\n')
s[i++] = c;
if (c == '\n')
s[i++] = c;
s[i] = '\0';
return i;
}
/* strindex: zwraca index t w s lub 1, jeeli nie wystpuje */
int strindex(char s[], char t[])
{
int i, j, k;
for (i = 0; s[i] != '\0'; i++) {
for (j=i, k=0; t[k]!='\0' && s[j]==t[k]; j++, k++)
;
if (k > 0 && t[k] == '\0')
return i;
}
return -1;
}

87

Jzyk ANSI C. Programowanie

Definicja funkcji ma zawsze nastpujc posta :


typ_zwracany nazwa_funkcji(deklaracje_argumentw)
{
deklaracje i instrukcje
}

Rne elementy mona pomija . Absolutne minimum to


dummy () {}

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;

Jeeli to konieczne, warto wyraenia jest przeksztacana na typ zadeklarowany jako


zwracany przez funkcj. Wyraenie nastpujce po sowie return ujmuje si czsto
w nawiasy, ale nie jest to wymagane.
Funkcja wywoujca moe w kadym przypadku zignorowa zwracan warto . Co wicej,
wyraenie po sowie return nie jest elementem wymaganym. Gdy zostanie pominite,
funkcja nie bdzie zwracaa adnej wartoci. Sterowanie zostaje przekazane do funkcji
wywoujcej bez zwracania wartoci take po dojciu do kocowego nawiasu klamrowego.
Jest to dopuszczalne, ale gdy funkcja z jednego miejsca zwraca warto , a z innego
nie sygnalizuje wystpowanie nieprawidowoci w pracy programu. W kadym przypadku, w ktrym funkcja nie zwraca wartoci, prba jej odczytania prowadzi do uzyskania przypadkowych danych (mieci).
Program wyszukujcy cig zwraca z funkcji main informacj o przebiegu jego wykonywania, ktr w tym przypadku jest liczba znalezionych wierszy. Warto ta moe by wykorzystywana przez rodowisko, ktre wywoao program.
Mechanika kompilowania i adowania programu C, ktry zosta zapisany w wielu plikach
rdowych, rni si w zalenoci od systemu. Przykadowo w systemie UNIX zadanie
to realizuje wspomniane w rozdziale 1. polecenie cc. Zamy, e trzy funkcje przykadowego 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 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

Polecenie cc wykorzystuje rozszerzenia .c i .o do odrniania plikw rdowych od plikw


wynikowych.
wiczenie 4.1. Napisz funkcj strrindex(s,t), ktra zwraca pozycj ostatniego wystpienia t w s lub -1, jeeli wyszukiwany cig nie zosta znaleziony.

4.2. Zwracanie wartoci innych ni int


Dotychczas przykadowe funkcje albo nie zwracay adnej wartoci (void), albo zwracay
liczb int. Co z funkcjami zwracajcymi wartoci innych typw? Wiele funkcji liczbowych, takich jak sqrt, sin czy cos, zwraca typ double. Inne wyspecjalizowane funkcje
zwracaj jeszcze inne typy. Aby zilustrowa prac z takimi funkcjami, napiszemy i wywoamy funkcj atof(s), ktra konwertuje cig s na jego odpowiednik typu zmiennoprzecinkowego, podwjnej precyzji. Funkcja atof jest rozwiniciem funkcji atoi, ktrej
wersje zostay przedstawione w rozdziaach 2. i 3. atof zapewnia obsug opcjonalnego
znaku liczby oraz kropki dziesitnej, a take sytuacji, w ktrych nie wystpuje cz
cakowita lub cz uamkowa wartoci. Przedstawiona tu wersja nie jest wysokiej jakoci
procedur konwersji danych wejciowych. Taka funkcja zajaby stanowczo zbyt wiele
miejsca. Dopracowan wersj atof zawiera standardowa biblioteka jzyka jest ona
zdefiniowana w nagwku <stdlib.h>.
Przede wszystkim typ zwracanej wartoci, jeeli nie jest to int, musi zosta okrelony
w samej funkcji. Nazw typu umieszcza si przed nazw funkcji:
#include <ctype.h>
/* atof: konwertuje cig s na liczb double */
double atof(char s[])
{
double val, power;
int i, sign;
for (i = 0; isspace(s[i]); i++) /* pomi biae znaki */
;
sign = (s[i] == '-') ? -1 : 1;
if (s[i] == '+' || s[i] == '-')
i++;

89

Jzyk 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, 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

Rozdzia 4. Funkcje i struktura programu

W wietle tego, co powiedzielimy o dopasowaniu deklaracji do definicji, moe si to


wydawa zaskakujce. Przyczyn takiej niezgodnoci jest zasada, e gdy brak prototypu
funkcji, jej deklaracja nastpuje automatycznie w chwili pierwszego uycia w wyraeniu, na przykad
sum += atof(line)

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();

to rwnie wstrzymuje kompilator od przyjmowania zaoe dotyczcych argumentw.


Sprawdzanie poprawnoci parametrw zostaje cakowicie wyczone. Ta szczeglna
interpretacja pustej listy argumentw ma umoliwi kompilowanie starszych programw
w jzyku C przez nowsze kompilatory. Nie naley jednak stosowa takiej skadni w nowych programach. Jeeli funkcja pobiera argumenty, deklarujemy je. Jeeli nie pobiera adnych, uywamy typu void.
Jeli dysponujemy (waciwie zadeklarowan) funkcj atof, moemy wykorzysta j
do utworzenia prostej funkcji atoi (konwertujcej cig znakw na liczb int):
/* atoi: konwertuje cig s na liczb cakowit przy uyciu atof */
int atoi(char s[])
{
double atof(char s[]);
return (int) atof(s);
}

Zwr my uwag na struktur deklaracji i instrukcj return. Warto wyraenia w wierszu


return wyraenie;

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

Jzyk ANSI C. Programowanie

4.3. Zmienne zewntrzne


Program w jzyku C skada si ze zbioru obiektw zewntrznych zmiennych i funkcji.
Wewntrz funkcji definiowane s obiekty wewntrzne, czyli jej argumenty i zmienne
lokalne. Zmienne zewntrzne s definiowane poza funkcjami, dziki czemu mog by
dostpne nie w jednej, ale w wielu funkcjach. Same funkcje s zawsze obiektami zewntrznymi, poniewa jzyk C nie dopuszcza definiowania funkcji wewntrz funkcji.
Standardowo zewntrzne zmienne i funkcje maj t waciwo , e wszystkie odwoania
do nich, czyli takie, ktre uywaj tej samej nazwy, nawet w funkcjach kompilowanych
niezalenie, pozostaj odwoaniami do tego samego obiektu (norma okrela t waciwo
terminem dowizywanie obiektw zewntrznych, ang. external linkage). Pod tym
wzgldem zmienne zewntrzne zachowuj si tak jak bloki COMMON jzyka Fortran lub
zmienne w najbardziej zewntrznym bloku w jzyku Pascal. Wkrtce pokaemy, jak
definiowa zmienne i funkcje zewntrzne, ktre s widoczne jedynie w obrbie pojedynczego pliku rdowego.
Poniewa zmienne zewntrzne s dostpne globalnie, stanowi alternatyw dla argumentw i wartoci zwracanych przez funkcje rwnie umoliwiaj wymian danych
midzy funkcjami. Kada funkcja moe uzyska dostp do zmiennej zewntrznej przy
uyciu jej nazwy, o ile tylko nazwa ta zostaa wczeniej w pewien sposb zadeklarowana.
Jeeli funkcje maj korzysta wsplnie z wielu rnych zmiennych, zmienne zewntrzne
s wygodniejsze i efektywniejsze ni dugie listy argumentw. Jak jednak pisalimy
w rozdziale 1., korzystanie z tej moliwoci powinno wiza si z pewn ostronoci,
moe mie bowiem zy wpyw na struktur programu i prowadzi do kodu z nadmiernie zoon sieci powiza midzy funkcjami.
Zmienne zewntrzne znajduj take zastosowania wynikajce bezporednio z ich wikszego zakresu i duszego czasu ycia. Zmienne automatyczne to wewntrzne obiekty
funkcji. Powstaj w chwili wejcia do funkcji i zostaj zlikwidowane w chwili wyjcia
z niej. Zmienne zewntrzne s trwae, zachowuj swoj warto pomidzy wywoaniami
rnych funkcji. Jeeli wic dwie funkcje musz korzysta z tych samych danych, a nie
wystpuje sytuacja, w ktrej jedna z nich wywouje drug, zapisanie wsplnych danych
w zmiennych zewntrznych jest czsto najwygodniejszym rozwizaniem, pozwalajcym
unikn wprowadzania dodatkowego mechanizmu przekazywania wartoci do i z kadej
ze wspdziaajcych funkcji.
Przeanalizujmy to zagadnienie na konkretnym przykadzie. Naszym zadaniem jest napisanie programu kalkulatora, ktry umoliwia korzystanie z operatorw +, -, * i /. Poniewa jest to prostsze w implementacji, kalkulator bdzie korzysta z odwrotnej notacji
polskiej, a nie notacji infiksowej (odwrotna notacja polska jest uywana przez niektre
kalkulatory kieszonkowe oraz jzyki programowania, na przykad Forth i PostScript).
W odwrotnej notacji polskiej wszystkie operandy poprzedzaj operator. Wyraenie infiksowe, na przykad
(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 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

Operacje umieszczania danych na stosie i zdejmowania z niego s banalne, ale do czasu


uzupenienia programu o wykrywanie i obsug bdw pozostaj wystarczajco zoone,
aby uzasadniao to umieszczenie ich w osobnych funkcjach. Pozwoli to przede wszystkim
unikn powtarzania kodu. Rwnie odrbna funkcja powinna odpowiada za pobieranie
kolejnego operatora lub operandu.
Gwnym zaoeniem projektowym jest to, gdzie konkretnie jest stos, a waciwie
ktre procedury maj do niego bezporedni dostp. Jedn z moliwoci jest pozostawienie jego obsugi w main. Mona przekazywa stos i biec pozycj stosu do procedur,
ktre pobieraj i zapisuj wartoci. Jednak w funkcji main nie s potrzebne zmienne
sterujce stosem. Wykonuje ona tylko operacje zapisania danych i odczytania ich. Zdecydowalimy wic o przechowywaniu stosu i zwizanych z nim informacji w zmiennych
zewntrznych, dostpnych funkcjom push i pop, ale nie main.
Zapisanie takiego projektu w postaci kodu nie jest trudne. Jeeli mamy zapisa program
w jednym pliku rdowym, bdzie on wyglda tak:
#include ... /* wiersze include */
#define ... /* wiersze define */

93

Jzyk ANSI C. Programowanie

deklaracje funkcji dla main


main() { ... }
zewntrzne zmienne dla push i pop
void push(double f) { ... }
double pop(void) { ... }
int getop(char s[]) { ... }
procedury wywoywane przez getop

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'

/* dopuszczalny rozmiar operandu lub operatora */


/* 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\n");
break;
case '\n':
printf("\t%.8g\n", pop());
break;
default:
printf("error: unknown command %s\n", s);
break;
}
}
return 0;
}

Poniewa + i * to operatory dziaa przemiennych, kolejno zdejmowania operandw ze


stosu nie ma znaczenia. Jednak w przypadku operatorw i / musi istnie rozrnienie
midzy wartoci po lewej stronie znaku i wartoci po prawej stronie znaku. W instrukcji
/* B
D */

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

/* dopuszczalna gboko stosu wartoci */

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

Jzyk ANSI C. Programowanie

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

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 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

Jzyk ANSI C. Programowanie

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?

Omwimy te zagadnienia na przykadzie programu kalkulatora, ktry teraz podzielony


zostanie na kilka plikw. Z praktycznego punktu widzenia jest to zbyt may program, aby
faktycznie warto byo go dzieli , jednak wystarczy on do zilustrowania problemw, ktre
pojawiaj si w wikszych projektach.
Zakres (ang. scope) nazwy to cz programu, w ktrej nazw t mona stosowa . Dla
zmiennej automatycznej, deklarowanej na pocztku funkcji, zakresem jest funkcja,
w ktrej zmienna zostaa zadeklarowana. Zmienne lokalne o tej samej nazwie, ale
w rnych funkcjach nie maj ze sob adnego zwizku. To samo mona powiedzie
o parametrach funkcji s one w praktyce zmiennymi lokalnymi.

98

Rozdzia 4. Funkcje i struktura programu

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];

pojawiaj si poza funkcjami, s to definicje zmiennych zewntrznych sp i val. Powoduj


one przydzielenie pamici, peni take funkcje deklaracji dla kodu w pozostaej czci
pliku rdowego. Z drugiej strony wiersze
extern int sp;
extern double val[];

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

Jzyk 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 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.

4.5. Pliki nagwkowe


Rozwamy podzielenie programu kalkulatora na kilka plikw rdowych. Mogoby to
by potrzebne, gdyby poszczeglne jego komponenty zostay 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 pozostaych, poniewa w rzeczywistym programie byyby czci odrbnie
kompilowanej biblioteki.
Pozostaje jeden problem do rozwizania definicje i deklaracje elementw wykorzystywanych w wicej ni jednym pliku. Dymy do maksymalnej centralizacji budowanego systemu, aby kada z jego czci miaa tylko jedno waciwe miejsce, nieulegajce zmianie w toku dalszej ewolucji kodu. Aby osign ten cel, umieszczamy wsplne
elementy w pliku nagwkowym (ang. header file, najczciej nazywany krtko nagwkiem), calc.h. Plik ten bdzie wczony do kodu plikw, ktre korzystaj z jego zawartoci,
dyrektyw #include. Dyrektyw t opiszemy dokadnie w podrozdziale 4.11. Program
wyglda tak:

100

Rozdzia 4. Funkcje i struktura programu

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.

4.6. Zmienne statyczne


Zmienne sp i val w pliku stack.c oraz buf i bufp w pliku getch.c su do prywatnego
uytku przez funkcje znajdujce si w tym samym pliku rdowym. adne inne nie powinny mie do nich dostpu. Deklaracja static zastosowana w odniesieniu do zmiennej
zewntrznej lub funkcji ogranicza zakres obiektu do pozostaej czci kompilowanego
pliku rdowego. Zewntrzna deklaracja static jest wic metod ukrywania nazw takich
jak buf i bufp nazw, ktre musz by zewntrzne, bo s wspuytkowane przez
rne funkcje, ale nie powinny by widoczne dla kodu wywoujcego te funkcje.
Statyczne przechowywanie zmiennych okrelamy, wstawiajc na pocztku zwykej
deklaracji sowo static. Jeeli dwie procedury i dwie zmienne s kompilowane w tym
samym pliku, jak w przykadzie
static char buf[BUFSIZE];
static int bufp = 0;

/* bufor dla ungetch */


/* nastpna wolna pozycja w buforze */

int getch(void) { ... }


void ungetch(int c) { ... }

101

Jzyk ANSI C. Programowanie

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.

4.7. Zmienne rejestrowe


Deklaracja register zwraca uwag kompilatora na to, e dana zmienna bdzie wyjtkowo intensywnie wykorzystywana. Ide tej deklaracji jest wskazanie, e pewne zmienne
powinny zosta umieszczone w rejestrach komputera. Z zasady prowadzi to do szybszych i mniejszych programw. Kompilator moe, ale nie musi, dostosowa si do takiego zalecenia.
Oto przykady deklaracji register:
register int x;
register char c;

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

Rozdzia 4. Funkcje i struktura programu

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.

4.8. Struktura blokowa


Jzyk C nie jest jzykiem, w ktrym struktura programu opiera si na blokach, jak
jest na przykad w Pascalu nie mona definiowa funkcji wewntrz funkcji. Mimo to
struktura blokowa obowizuje przy definiowaniu zmiennych. Deklaracje zmiennych
(i ich inicjalizacja) mog zosta umieszczone po nawiasie klamrowym otwierajcym
dowoln instrukcj blokow, a nie tylko po nawiasie klamrowym otwierajcym blok instrukcji funkcji. Zmienne deklarowane w ten sposb przesaniaj zmienne o takich samych
nazwach wystpujce poza blokiem, a ich czas ycia koczy si wraz z wyjciem
z bloku. Na przykad w kodzie
if (n > 0) {
int i; /* deklaracja nowej zmiennej i */
for (i = 0; i < n; i++)
...
}

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

Jzyk ANSI C. Programowanie

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 */

Warto inicjalizujca zmienne zewntrzne i statyczne musi by wyraeniem o staej


wartoci. Inicjalizacja jest wykonywana jednokrotnie, jeszcze przed rozpoczciem waciwego procesu wykonywania programu. Inicjalizacja zmiennych automatycznych i rejestrowych nastpuje przy kadym wejciu wykonywanego programu do funkcji lub bloku.
Warto inicjalizujca zmienne automatyczne i rejestrowe nie musi by staa moe
to by dowolne wyraenie oparte na wartociach wczeniej zdefiniowanych, a nawet
wywoaniach funkcji. Przykadowo inicjalizacja programu wyszukiwania binarnego
z podrozdziau 3.3 moe by zapisana nastpujco:
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 skrcona


forma czca instrukcje deklaracji i przypisania. Wybr jest kwesti stylu. W ksice
z zasady nie czymy deklaracji i przypisania, poniewa warto pocztkowa okrelona
w bloku deklaracji jest atwa do przeoczenia, a odrbne przypisanie moe nastpi
w miejscu, w ktrym zmienna jest wykorzystywana.
Tablic mona zainicjalizowa , umieszczajc po deklaracji list wartoci elementw
ujt w nawiasy klamrowe i rozdzielan przecinkami. Aby na przykad zainicjalizowa
tablic days dugociami miesicy, 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 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";

Jest to skrt duszej, cho rwnowanej konstrukcji:


char pattern[] = { 'o', 'u', 'l', 'd', '\0' };

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

Jzyk ANSI C. Programowanie

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);
}

Przenielimy operacj zamieniania elementw miejscami do osobnej funkcji swap


jest przecie wywoywana 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, 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.

4.11. Preprocesor jzyka C


Jzyk C realizuje pewne mechanizmy jzykowe za porednictwem preprocesora. Jest to
pierwszy krok wykonywany przed waciwym procesem kompilacji. Dwie najczciej
stosowane dyrektywy preprocesora to #include, wczajca do procesu kompilacji
zawarto innego pliku, i #define, zastpujca nazw wskazanym cigiem znakw. W tym
podrozdziale opiszemy te inne moliwoci preprocesora: kompilacj warunkow i makra
z argumentami.

4.11.1. Wstawianie plikw


Mechanizm wstawiania plikw uatwia przede wszystkim obsug zbiorw dyrektyw
#define i deklaracji. Kady wiersz postaci
#include "nazwa_pliku"

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

Jzyk ANSI C. Programowanie

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

Mamy tu do czynienia z najprostsz postaci makra, opart na substytucji wszystkie


dalsze wystpienia nazwa zostan zastpione przez tekst_zastpujcy. Nazwa w #define
ma tak sam posta jak nazwa zmiennej. Tekst zastpujcy moe by dowolny. Normalnie s to wszystkie znaki do koca wiersza, ale duga definicja moe zosta podzielona
na kilka kolejnych wierszy przez wstawienie znaku \ na kocu kadego wiersza, ktry ma
by kontynuowany. Zakres nazwy wskazanej w #define siga od wiersza #define do koca
kompilowanego pliku rdowego. Definicja moe korzysta z wczeniejszych definicji.
Substytucja nie obejmuje miejsc, w ktrych nazwa jest czci duszej nazwy i fragmentw ujtych w cudzysw. Po zdefiniowaniu na przykad nazwy YES substytucja nie
nastpi w printf("YES") ani w YESMAN.
Zastpujcy nazw tekst moe by dowolny. Na przykad
#define forever for (;;) /* ptla niesko czona */

definiuje nowe sowo, forever, ktre bdzie zastpowane ptl nieskoczon.


Mona take definiowa makra z argumentami, dziki ktrym tekst zastpujcy jest
rny w poszczeglnych wywoaniach makra. Przykadem moe by makro max:
#define max(A, B) ((A) > (B) ? (A) : (B))

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

Rozdzia 4. Funkcje i struktura programu

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 */

prowadzi do dwukrotnego zwikszenia wikszej wartoci. Czsto warto zadba o ujcie


wyraenia w nawiasy, aby zapewni waciw kolejno wykonywania oblicze. Pomylmy, co si stanie, gdy makro
#define square(x) x * x /* B
D */

zostanie wywoane w wyraeniu square(z+1).


Makra s bardzo wartociowym narzdziem. Jednym z praktycznych przykadw ich
zastosowania jest wczanie do kompilacji pliku <stdio.h>, w ktrym operacje getchar
i putchar s czsto zdefiniowane jako makra. Pozwala to unikn obcienia programu
mechanizmem wywoywania funkcji przy odczycie pojedynczych znakw. Rwnie
funkcje w <ctype.h> s zazwyczaj implementowane jako makra.
Definicje nazw mona wycofywa dyrektyw #undef. Moliwo t wykorzystuje si
czsto w celu uzyskania gwarancji, e dana procedura bdzie funkcj, a nie makrem:
#undef getchar
int getchar(void) { ... }

Parametry formalne nie s zastpowane w cigach znakowych otoczonych znakami


cudzysowu. Jeeli jednak nazw parametru poprzedza w tekcie zastpujcym znak #,
to zostanie on zamieniony na ujty w cudzysw cig znakw, w ktrym parametr jest
zastpiony podanym argumentem faktycznym. W poczeniu z konkatenacj cigw
pozwala to na przykad utworzy nastpujce makro wywietlajce wartoci potrzebne
w procesie debugowania:
#define dprint(expr) printf(#expr " = %g\n", expr)

Po jego wywoaniu, na przykad w instrukcji


dprint(x/y);

makro zostaje rozwinite do postaci


printf("x/y" " = &g\n", x/y);

cigi znakowe s automatycznie czone, wic w efekcie uzyskujemy


printf("x/y = &g\n", x/y);

109

Jzyk ANSI C. Programowanie

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

wic paste(name, 1) tworzy nazw name1.


Reguy zagniedania operatora ## s do zoone. Szczegy mona znale w dodatku A.
wiczenie 4.14. Zdefiniuj makro swap(t,x,y) wymieniajce wartoci dwch argumentw,
ktrych typ to t (pomocna bdzie struktura blokowa).

4.11.3. Warunkowe wstawianie kodu


Istnieje moliwo sterowania prac samego preprocesora przy uyciu instrukcji warunkowych, wykonywanych w trakcie jego dziaania. Zapewnia to moliwo wybirczego wstawiania kodu, w zalenoci od warunkw, ktrych wartoci s obliczane w czasie
kompilowania.
Wiersz #if oblicza warto staego wyraenia cakowitego (ktre nie moe zawiera
operatora sizeof, konwersji typw i staych enum). Jeeli wyraenie ma warto rn od
zera, wstawione zostaj dalsze wiersze, a do wiersza #endif, #elif lub #else (instrukcja
preprocesora #elif odpowiada else if). Wyraenie defined(nazwa) w wierszu #if ma
warto 1, jeeli nazwa zostaa wczeniej zdefiniowana, a 0 w pozostaych przypadkach.
Aby na przykad zapewni , e zawarto pliku hdr.h bdzie wczana do kodu tylko raz,
mona otoczy j wierszami dyrektyw warunkowych:
#if !defined(HDR)
#define HDR
/* tu znajduje si waciwa tre nagwka hdr.h */
#endif

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

Rozdzia 4. Funkcje i struktura programu

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

Vous aimerez peut-être aussi