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ñ.
• Zmienne i wyra¿enia arytmetyczne w jêzyku C
• Kompilowanie kodu
• Wykorzystanie preprocesora jêzyka C
• Typy i operatory
• Metody sterowania wykonywaniem programu
• Wykorzystanie funkcji
• Struktura programu
• Zasada dzia³ania wskaŸników
• Struktury danych
• Operacje wejœcia i wyjœcia
• Zastosowanie rekurencji
Poznaj tajniki jêzyka C!
Spis treci
Przedmowa
7
Przedmowa do pierwszego wydania
9
Wstp
11
Rozdzia 1. Wprowadzenie
15
1.1.
Pierwsze kroki
16
1.2.
Zmienne i wyraenia arytmetyczne
18
1.3.
Instrukcja for
24
1.4.
Stae symboliczne
26
1.5.
Znakowe operacje wejcia-wyjcia
26
1.6.
Tablice
34
1.7.
Funkcje
36
1.8.
Argumenty — przekazywanie jako warto
40
1.9.
Tablice znaków
41
1.10.
Zmienne zewntrzne i zakres zmiennych
44
Rozdzia 2. Typy, operatory i wyraenia
49
2.1.
Nazwy zmiennych
49
2.2.
Typy danych i ich rozmiar
50
2.3.
Stae
51
2.4.
Deklaracje
54
2.5.
Operatory arytmetyczne
55
2.6.
Operatory porównania i logiczne
56
2.7.
Konwersja typów
57
2.8.
Inkrementacja i dekrementacja
61
2.9.
Operatory bitowe
63
2.10.
Operatory i wyraenia przypisania
65
Jzyk ANSI C. Programowanie
4
2.11.
Wyraenia warunkowe
67
2.12.
Priorytety operatorów i kolejno wykonywania oblicze
68
Rozdzia 3. Sterowanie wykonywaniem programu
71
3.1.
Instrukcje i bloki
71
3.2.
if-else
72
3.3.
else-if
73
3.4.
switch
75
3.5.
Ptle while i for
76
3.6.
Ptla do-while
80
3.7.
break i continue
81
3.8.
goto i etykiety
82
Rozdzia 4. Funkcje i struktura programu
85
4.1.
Funkcje — podstawy
86
4.2.
Zwracanie wartoci innych ni int
89
4.3.
Zmienne zewntrzne
92
4.4.
Zakres
98
4.5.
Pliki nagówkowe
100
4.6.
Zmienne statyczne
101
4.7.
Zmienne rejestrowe
102
4.8.
Struktura blokowa
103
4.9.
Inicjalizacja
104
4.10.
Rekurencja
105
4.11.
Preprocesor jzyka C
107
Rozdzia 5. Wskaniki i tablice
113
5.1.
Wskaniki i adresy
113
5.2.
Wskaniki i argumenty funkcji
115
5.3.
Wskaniki i tablice
118
5.4.
Arytmetyka adresów
121
5.5.
Wskaniki znakowe i funkcje
124
5.6.
Tablice wskaników, wskaniki do wskaników
128
5.7.
Tablice wielowymiarowe
131
5.8.
Inicjalizacja tablic wskaników
134
5.9.
Wskaniki a tablice wielowymiarowe
134
5.10.
Argumenty wiersza polece
135
5.11.
Wskaniki do funkcji
140
5.12.
Rozbudowane deklaracje zmiennych i funkcji
143
Rozdzia 6. Struktury
149
6.1.
Struktury — podstawy
149
6.2.
Struktury i funkcje
151
6.3.
Tablice struktur
154
6.4.
Wskaniki do struktur
158
6.5.
Struktury cykliczne (odwouj ce si do siebie)
161
Spis treci
5
6.6.
Wyszukiwanie w tabelach
166
6.7.
typedef
168
6.8.
union
170
6.9.
Pola bitowe
172
Rozdzia 7. Wejcie i wyjcie
175
7.1.
Standardowe operacje wejcia-wyjcia
175
7.2.
printf — formatowanie danych wyjciowych
178
7.3.
Listy argumentów o zmiennej dugoci
180
7.4.
scanf — formatowane dane wejciowe
181
7.5.
Dostp do plików
185
7.6.
stderr i exit — obsuga bdów
188
7.7.
Wierszowe operacje wejcia-wyjcia
189
7.8.
Inne funkcje
191
Rozdzia 8. Interfejs systemu UNIX
195
8.1.
Deskryptory plików
196
8.2.
Niskopoziomowe operacje wejcia-wyjcia — odczyt i zapis
197
8.3.
open, creat, close, unlink
198
8.4.
lseek — dostp swobodny
201
8.5.
Przykad — implementacja fopen i getc
202
8.6.
Przykad — listy zawartoci katalogów
206
8.7.
Przykad — mechanizm alokacji pamici
211
Dodatek A Opis jzyka C
217
A.1.
Wprowadzenie
217
A.2.
Konwencje leksykalne
217
A.3.
Zapis skadni
221
A.4.
Identyfikatory obiektów
222
A.5.
Obiekty i L-wartoci
224
A.6.
Konwersje
225
A.7.
Wyraenia
228
A.8.
Deklaracje
241
A.9.
Instrukcje
257
A.10.
Deklaracje zewntrzne
261
A.11.
Zakres i wi zanie
264
A.12.
Przetwarzanie wstpne
266
A.13.
Gramatyka
273
Dodatek B Standardowa biblioteka jzyka C
281
B.1.
Wejcie i wyjcie: <stdio.h>
282
B.2.
Wykrywanie klas znaków: <ctype.h>
291
B.3.
Ci gi znakowe: <string.h>
291
B.4.
Funkcje matematyczne: <math.h>
293
B.5.
Funkcje narzdziowe: <stdlib.h>
294
B.6.
Diagnostyka: <assert.h>
297
Jzyk ANSI C. Programowanie
6
B.7.
Listy argumentów o zmiennej dugoci: <stdarg.h>
298
B.8.
Skoki odlege: <setjmp.h>
298
B.9.
Sygnay: <signal.h>
299
B.10.
Data i godzina: <time.h>
300
B.11.
Ograniczenia okrelane przez implementacj: <limits.h> i <float.h>
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 szczegóy
swoich mechanizmów przed innymi czciami programu, dla których s one nieistotne.
Zapewnia to przejrzysto i znacznie uatwia wprowadzanie zmian.
Jzyk C zosta zaprojektowany w taki sposób, 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
ródowe programu mog by kompilowane niezalenie i póniej jednoczenie ado-
wanedo pamici razem z wczeniej skompilowanymi funkcjami bibliotek. Nie bdziemy
omawia tu dokadnie tego rodzaju procedur, poniewa róni si one w zalenoci
od stosowanego systemu.
Deklaracja i definicja funkcji to obszar, w którym 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 argumentów. Skadnia definicji funkcji równie
jest zmieniona, dziki czemu deklaracja i definicja maj tak sam posta . Umoliwia
to kompilatorowi wykrycie znacznie wikszej liczby bdów ni wczeniej. Co wicej,
waciwy sposób deklarowania argumentów zapewnia automatyczne konwersje typów.
Standard ucila reguy dotyczce zakresu nazw. W szczególnoci wymaga on, aby kady
obiekt zewntrzny mia tylko jedn definicj. Mechanizm inicjalizacji zosta uogólniony
— w ANSI C mona inicjalizowa tablice i struktury automatyczne.
Preprocesor jzyka równie zosta usprawniony. Jego nowe mechanizmy obejmuj
peniejszy zbiór dyrektyw kompilacji warunkowej, moliwo budowania cigów znako-
wych z argumentów makr oraz wiksz kontrol nad procesem rozwijania makra.
Jzyk ANSI C. Programowanie
86
4.1. Funkcje — podstawy
Na pocztek zaprojektujemy i napiszemy program, który wypisuje kady wiersz danych
wejciowych zawierajcy okrelony wzorzec — cig znaków (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 sposób na trzy czci:
while (jest kolejny wiersz)
if (wiersz zawiera wzorzec)
wypisz wiersz
Cho jest oczywicie moliwe umieszczenie caego kodu w funkcji
main
, lepszym po-
dejciem okazuje si wykorzystanie moliwoci strukturalizowania kodu i zapisanie
kadej czci w odrbnej funkcji. Z trzema maymi elementami atwiej pracowa ni
z jednym duym — nieistotne szczegóy pozostaj ukryte w funkcjach, a prawdopo-
dobiestwo wystpienia niepodanych interakcji jest ograniczone do minimum.
Co wicej, gotowe elementy mog znale zastosowanie w innych programach.
„while jest kolejny wiersz” to funkcja
getline
, któr 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)
, która zwraca pozycj
(indeks) w cigu
s
, od którego 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 wzorców, bdzie mona wymieni funkcj
strindex
na inn.
Reszta kodu pozostanie bez zmian (standardowa biblioteka zawiera funkcj
strstr
,
która jest podobna do
strindex
, ale zwraca wskanik zamiast indeksu).
Po takim przygotowaniu projektu napisanie waciwego programu jest ju czynnoci
stosunkowo prost. Poniej przedstawiono cao , zarówno gówny program, jak i sto-
sowane funkcje, aby Czytelnik móg wygodnie przeanalizowa ich wspódziaanie. W tej
wersji wyszukiwany cig jest literaem (sta), wic trudno mówi o ogólnoci rozwizania.
Do inicjalizowania tablic znakowych powrócimy ju wkrótce, natomiast w rozdziale 5.
Rozdzia 4. • Funkcje i struktura programu
87
pokaemy, jak przeksztaci wzorzec w parametr przekazywany przy uruchamianiu
programu. Kod zawiera take nieco zmodyfikowan funkcj
getline
. Porównanie
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;
}
Jzyk ANSI C. Programowanie
88
Definicja funkcji ma zawsze nastpujc posta :
typ_zwracany nazwa_funkcji(deklaracje_argumentów)
{
deklaracje i instrukcje
}
Róne elementy mona 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. Jeeli zwracany typ
danych nie zosta okrelony, kompilator przyjmuje, e jest to
int
.
Program to po prostu zbiór definicji zmiennych i funkcji. Komunikacja midzy funkcjami
odbywa si za porednictwem argumentów funkcji, wartoci zwracanych przez funkcje
i zmiennych zewntrznych. Funkcje mog by umieszczone w pliku ródowym w dowol-
nej kolejnoci, a program moe by podzielony na wiele plików ródowych, 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 przy-
padku, w którym funkcja nie zwraca wartoci, próba jej odczytania prowadzi do uzy-
skania przypadkowych danych (mieci).
Program wyszukujcy cig zwraca z funkcji
main
informacj o przebiegu jego wykonywa-
nia, któr w tym przypadku jest liczba znalezionych wierszy. Warto ta moe by wyko-
rzystywana przez rodowisko, które wywoao program.
Mechanika kompilowania i adowania programu C, który zosta zapisany w wielu plikach
ródowych, róni si w zalenoci od systemu. Przykadowo w systemie UNIX zadanie
to realizuje wspomniane w rozdziale 1. polecenie
cc
. Zaómy, e trzy funkcje przykado-
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
Rozdzia 4. • Funkcje i struktura programu
89
kompiluje trzy wymienione pliki, umieszcza kod obiektów 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 odróniania plików ródowych od plików
wynikowych.
wiczenie 4.1. Napisz funkcj
strrindex(s,t)
, która zwraca pozycj ostatniego wy-
stpienia
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 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 cig
s
na jego odpowiednik typu zmiennoprze-
cinkowego, podwójnej precyzji. Funkcja
atof
jest rozwiniciem funkcji
atoi
, której
wersje zostay przedstawione w rozdziaach 2. i 3.
atof
zapewnia obsug opcjonalnego
znaku liczby oraz kropki dziesitnej, a take sytuacji, w których 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 nagówku
<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++;
Jzyk ANSI C. Programowanie
90
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 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 minimali-
stycznego kalkulatora (nadajcego si chyba tylko do podliczania wypat z bankomatu),
który 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 []);
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. Jeeli definicja funkcji i jej wywoanie
w
main
maj niespójnie okrelone typy, a s w tym samym pliku ródowym, kompilator
zgosi bd. Jednak gdy (co jest bardziej prawdopodobne) funkcja
atof
bdzie kompilowana
niezalenie, brak zgodnoci nie zostanie wykryty, a funkcja zwróci liczb
double
, która
w
main
bdzie traktowana jako
int
— uzyskiwane wtedy wartoci bd niemal zupe-
nie przypadkowe.
Rozdzia 4. • Funkcje i struktura programu
91
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 wyra-
eniu, na przykad
sum += atof(line)
Jeeli nazwa, która nie zostaa wczeniej zadeklarowana, wystpuje w wyraeniu, a bez-
porednio po niej jest umieszczony otwierajcy znak nawiasu, nastpuje deklaracja na
podstawie kontekstu — dana nazwa jest uznawana za nazw funkcji, która zwraca
warto
int
. Nie s natomiast przyjmowane adne zaoenia dotyczce jej argumentów.
Co wicej, jeeli deklaracja funkcji nie zawiera argumentów, jak w instrukcji
double atof();
to równie wstrzymuje kompilator od przyjmowania zaoe dotyczcych argumentów.
Sprawdzanie poprawnoci parametrów zostaje cakowicie wyczone. Ta szczególna
interpretacja pustej listy argumentów ma umoliwi kompilowanie starszych programów
w jzyku C przez nowsze kompilatory. Nie naley jednak stosowa takiej skadni w no-
wych programach. Jeeli funkcja pobiera argumenty, deklarujemy je. Jeeli nie po-
biera adnych, uywamy typu
void
.
Jeli dysponujemy (waciwie zadeklarowan) funkcj
atof
, moemy wykorzysta j
do utworzenia prostej funkcji
atoi
(konwertujcej cig znaków 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 niektóre 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 opcjo-
nalnym znakiem.
Jzyk ANSI C. Programowanie
92
4.3. Zmienne zewntrzne
Program w jzyku C skada si ze zbioru obiektów 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 ze-
wntrznymi, poniewa jzyk C nie dopuszcza definiowania funkcji wewntrz funkcji.
Standardowo zewntrzne zmienne i funkcje maj t waciwo , e wszystkie odwoania
do nich, czyli takie, które uywaj tej samej nazwy, nawet w funkcjach kompilowanych
niezalenie, pozostaj odwoaniami do tego samego obiektu (norma okrela t waciwo
terminem „dowizywanie obiektów 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. Wkrótce pokaemy, jak
definiowa zmienne i funkcje zewntrzne, które s widoczne jedynie w obrbie po-
jedynczego pliku ródowego.
Poniewa zmienne zewntrzne s dostpne globalnie, stanowi alternatyw dla ar-
gumentów i wartoci zwracanych przez funkcje — równie 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 sposób zadeklarowana.
Jeeli funkcje maj korzysta wspólnie z wielu rónych zmiennych, zmienne zewntrzne
s wygodniejsze i efektywniejsze ni dugie listy argumentów. 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 nad-
miernie zoon sieci powiza midzy funkcjami.
Zmienne zewntrzne znajduj take zastosowania wynikajce bezporednio z ich wik-
szego 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
rónych funkcji. Jeeli wic dwie funkcje musz korzysta z tych samych danych, a nie
wystpuje sytuacja, w której jedna z nich wywouje drug, zapisanie wspólnych danych
w zmiennych zewntrznych jest czsto najwygodniejszym rozwizaniem, pozwalajcym
unikn wprowadzania dodatkowego mechanizmu przekazywania wartoci do i z kadej
ze wspódziaajcych funkcji.
Przeanalizujmy to zagadnienie na konkretnym przykadzie. Naszym zadaniem jest napi-
sanie programu kalkulatora, który umoliwia korzystanie z operatorów
+
,
-
,
*
i
/
. Po-
niewa jest to prostsze w implementacji, kalkulator bdzie korzysta z odwrotnej notacji
polskiej, a nie notacji infiksowej (odwrotna notacja polska jest uywana przez niektóre
kalkulatory kieszonkowe oraz jzyki programowania, na przykad Forth i PostScript).
W odwrotnej notacji polskiej wszystkie operandy poprzedzaj operator. Wyraenie infik-
sowe, na przykad
(1 – 2) * (4 + 5)
Rozdzia 4. • Funkcje i struktura programu
93
jest wprowadzane jako
1 2 – 4 5 + *
Nawiasy nie s potrzebne. Notacja jest jednoznaczna, o ile tylko liczba operandów kadego
operatora jest staa.
Implementacja jest prosta. Kady operand zostaje umieszczony na stosie. Po pobraniu
operatora program zdejmuje ze stosu waciw liczb operandów (dwa w przypadku
operatorów binarnych), wykonuje operacj i zapisuje wynik ponownie na stosie. W po-
wyszym przykadzie oznacza to umieszczenie na stosie liczb 1 i 2, nastpnie zastpienie
ich rónic, –1. W kolejnym kroku na stos trafiaj liczby 4 i 5, które zostaj nastpnie
zastpione sum, 9. Kolejna operacja to mnoenie, wic ze stosu zostaj pobrane wartoci
–1 i 9, które 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, która wykonuje odpowiednie operacje na pobiera-
nych 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 bdów pozostaj wystarczajco zoone,
aby uzasadniao to umieszczenie ich w osobnych funkcjach. Pozwoli to przede wszystkim
unikn powtarzania kodu. Równie odrbna funkcja powinna odpowiada za pobieranie
kolejnego operatora lub operandu.
Gównym zaoeniem projektowym jest to, gdzie konkretnie jest stos, a waciwie
— które procedury maj do niego bezporedni dostp. Jedn z moliwoci jest pozosta-
wienie jego obsugi w
main
. Mona przekazywa stos i biec pozycj stosu do procedur,
które pobieraj i zapisuj wartoci. Jednak w funkcji
main
nie s potrzebne zmienne
sterujce stosem. Wykonuje ona tylko operacje zapisania danych i odczytania ich. Zdecy-
dowalimy 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 ródowym, bdzie on wyglda tak:
#include ...
/* wiersze include */
#define ...
/* wiersze define */
Jzyk ANSI C. Programowanie
94
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 ródowe lub wicej zajmiemy si ju
niedugo.
Funkcja
main
to ptla zawierajca rozbudowan instrukcj
switch
, która rozgazia
sterowanie w zalenoci od typu operatora lub operandu. Jest to bardziej typowy przy-
kad jej uycia 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 '/':
Rozdzia 4. • Funkcje i struktura programu
95
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 operandów ze
stosu nie ma znaczenia. Jednak w przypadku operatorów
–
i
/
musi istnie rozrónienie
midzy wartoci po lewej stronie znaku i wartoci po prawej stronie znaku. W instrukcji
push(pop() – pop());
/* B D */
kolejno obliczania wartoci wywoa
pop
nie jest okrelona. Aby zagwarantowa wa-
ciw, 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;
}
}
Jzyk ANSI C. Programowanie
96
Zmienna jest zmienn zewntrzn, jeeli jest zdefiniowana poza funkcj, tak wic
wspóuytkowane przez funkcje
pop
i
push
zmienne stosu i indeksu stosu zostaj zde-
finiowane poza tymi funkcjami. Jednak funkcja
main
nie odwouje si do stosu ani jego
indeksu — reprezentacja moe pozosta ukryta.
Przejdmy teraz do implementacji
getop
, funkcji, która pobiera kolejny operator lub ope-
rand. Zadanie jest proste. Pomijamy spacje i tabulatory. Jeeli nastpny znak nie jest
cyfr lub kropk dziesitn, zwracamy go. W pozostaych przypadkach pobieramy cig
cyfr (który moe zawiera kropk dziesitn) i zwracamy
NUMBER
, czyli warto sygnalizu-
jc, 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 znaków tworzcych 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 duo, znak, na który nie
jest przygotowany.
Problem byby rozwizany, gdyby istniaa moliwo cofnicia operacji odczytu ostatniego
znaku danych wejciowych. Wówczas program, który odczyta o jeden znak za duo,
mógby „odda ” ten znak do strumienia, a inne elementy programu dziaayby tak,
Rozdzia 4. • Funkcje i struktura programu
97
jak gdyby znak ten nigdy nie by odczytywany. Okazuje si, e skonstruowanie takiego
mechanizmu nie jest trudne, wystarczy para wspópracujcych ze sob funkcji.
getch
zwraca kolejny znak danych wejciowych.
ungetch
zapamituje znaki zwrócone na
wejcie w taki sposób, aby dalsze wywoania
getch
zwracay 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, jeeli nie jest on pusty. W pozo-
staych przypadkach wywouje po prostu funkcj
getchar
. Niezbdna jest równie
zmienna indeksujca, która 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
, która umoliwia wycofanie jednego znaku.
Omówimy j w rozdziale 7. W powyszym przykadzie uylimy tablicy, a nie poje-
dynczego znaku, aby zaprezentowa bardziej ogólne 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. Utwórz polecenie wypisujce element na wierzchoku stosu bez jego
usuwania ze stosu, polecenie duplikujce element na wierzchoku stosu, polecenie
zamieniajce miejscami dwa górne 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.
Jzyk ANSI C. Programowanie
98
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)
, która zwraca do danych wejciowych
cay cig znaków. 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
. Zastanów 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 kompi-
lowane jednoczenie. ródowy tekst programu mona przechowywa w wielu plikach,
a wczeniej skompilowane procedury mog by adowane z bibliotek. Wie si to z kil-
koma 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?
Omówimy te zagadnienia na przykadzie programu kalkulatora, który teraz podzielony
zostanie na kilka plików. Z praktycznego punktu widzenia jest to zbyt may program, aby
faktycznie warto byo go dzieli , jednak wystarczy on do zilustrowania problemów, które
pojawiaj si w wikszych projektach.
Zakres (ang. scope) nazwy to cz programu, w której nazw t mona stosowa . Dla
zmiennej automatycznej, deklarowanej na pocztku funkcji, zakresem jest funkcja,
w której zmienna zostaa zadeklarowana. Zmienne lokalne o tej samej nazwie, ale
w rónych funkcjach nie maj ze sob adnego zwizku. To samo mona powiedzie
o parametrach funkcji — s one w praktyce zmiennymi lokalnymi.
Rozdzia 4. • Funkcje i struktura programu
99
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 ródowym ni ten,
w którym jest wykorzystywana, konieczne staje si uycie deklaracji
extern
.
Wane jest, aby rozrónia 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 ródowego. 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
(której rozmiar jest okrelony gdzie indziej). Nie tworz one jednak zmiennych
i nie rezerwuj pamici.
We wszystkich plikach tworzcych program ródowy moe wystpi tylko jedna definicja
zmiennej zewntrznej. Inne pliki mog zawiera deklaracje
extern
umoliwiajce do-
stp 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. Wówczas ich powizanie
zostanie zapewnione przez nastpujcy ukad definicji i deklaracji:
Jzyk ANSI C. Programowanie
100
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 dostp-
noci 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 których
s stosowane.
4.5. Pliki nagówkowe
Rozwamy podzielenie programu kalkulatora na kilka plików ródowych. Mogoby to
by potrzebne, gdyby poszczególne 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 elementów wyko-
rzystywanych w wicej ni jednym pliku. Dymy do maksymalnej centralizacji bu-
dowanego systemu, aby kada z jego czci miaa tylko jedno waciwe miejsce, nieule-
gajce zmianie w toku dalszej ewolucji kodu. Aby osign ten cel, umieszczamy wspólne
elementy w pliku nagówkowym (ang. header file, najczciej nazywany krótko nagów-
kiem), calc.h. Plik ten bdzie wczony do kodu plików, które korzystaj z jego zawartoci,
dyrektyw
#include
. Dyrektyw t opiszemy dokadnie w podrozdziale 4.11. Program
wyglda tak:
Rozdzia 4. • Funkcje i struktura programu
101
Mamy tu do czynienia z problemem wywaenia midzy deniem do tego, aby kady plik
mia dostp wycznie do tych informacji, które s mu niezbdne, a prozaiczn potrzeb
codziennej praktyki — praca ze zbyt du liczb plików nagówka jest uciliwa. Do
pewnych granic dobrym rozwizaniem jest stosowanie jednego nagówka 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 nagówków.
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 ródowym.
adne inne nie po-
winny mie do nich dostpu. Deklaracja
static
zastosowana w odniesieniu do zmiennej
zewntrznej lub funkcji ogranicza zakres obiektu do pozostaej czci kompilowanego
pliku ródowego. Zewntrzna deklaracja
static
jest wic metod ukrywania nazw takich
jak
buf
i
bufp
— nazw, które musz by zewntrzne, bo s wspóuytkowane przez
róne 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];
/* bufor dla ungetch */
static int bufp = 0;
/* nastpna wolna pozycja w buforze */
int getch(void) { ... }
void ungetch(int c) { ... }
Jzyk ANSI C. Programowanie
102
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 sposób 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 za-
deklarowana jako
static
, jej nazwa nie jest widoczna poza plikiem, w którym zostaa
zadeklarowana.
Deklaracji
static
mona take uy w odniesieniu do zmiennych wewntrznych. We-
wntrzne zmienne
static
pozostaj zmiennymi lokalnymi funkcji, podobnie jak zmienne
automatyczne, jednak w przeciwiestwie do zmiennych automatycznych nie prze-
staj 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 sposób, aby nie korzystaa z funkcji
ungetch
. Wskazówka: uyj wewntrznej zmiennej statycznej.
4.7. Zmienne rejestrowe
Deklaracja
register
zwraca uwag kompilatora na to, e dana zmienna bdzie wyjt-
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 moe, ale nie musi, dostosowa si do takie-
go zalecenia.
Oto przykady deklaracji
register
:
register int x;
register char c;
Deklaracje takie mona stosowa wycznie w odniesieniu do zmiennych automatycz-
nych i parametrów formalnych funkcji. W przypadku parametrów formalnych wyglda
to tak:
f(register unsigned m, register long n)
{
register int i;
...
}
W praktyce zmienne rejestrowe podlegaj pewnym ograniczeniom wynikajcym z mo-
liwoci 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
Rozdzia 4. • Funkcje i struktura programu
103
tak opisanych zmiennych lub niezgodnoci typów sowo
register
jest ignorowane. Dodat-
kowo nie mona pobra adresu zmiennej rejestrowej (ten temat omówimy w rozdziale 5.),
niezalenie od tego, czy zostaa ona faktycznie umieszczona w rejestrze. Zakres ograni-
cze co do typów i liczby zmiennych rejestrowych jest zaleny od komputera.
4.8. Struktura blokowa
Jzyk C nie jest jzykiem, w którym 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 in-
strukcji funkcji. Zmienne deklarowane w ten sposób 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 którym jest zadeklaro-
wana. 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, równie 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, które
przesaniaj nazwy uywane w szerszym zakresie. Jest to bowiem najkrótsza droga do
pomyek i bdów.
Jzyk ANSI C. Programowanie
104
4.9. Inicjalizacja
O inicjalizacji wspominalimy ju kilkukrotnie, ale zawsze pozostawaa ona na margi-
nesie innych tematów. W tym podrozdziale, po omówieniu rónych klas pamici da-
nych, 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 równoci 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 wa-
ciwego procesu wykonywania programu. Inicjalizacja zmiennych automatycznych i reje-
strowych 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 skrócona
forma czca instrukcje deklaracji i przypisania. Wybór 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 którym zmienna jest wykorzystywana.
Tablic mona zainicjalizowa , umieszczajc po deklaracji list wartoci elementów
— 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 }
Rozdzia 4. • Funkcje i struktura programu
105
Gdy rozmiar tablicy nie jest okrelony, kompilator okrela j, zliczajc wartoci pocztko-
we elementów. W tym przypadku jest ich 12.
Jeeli lista pocztkowych wartoci elementów tablicy zawiera mniej elementów 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 elementów
wewntrznych bez podania wartoci wszystkich elementów poprzedzajcych.
Tablice znaków s traktowane w sposób szczególny. W miejsce nawiasów klamrowych
i rozdzielonej przecinkami listy mona uy cigu:
char pattern = "ould";
Jest to skrót duszej, cho równowanej 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 znaków. 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 odwrócenie
kolejnoci zapisanych elementów. Tak zrobilimy w przykadowej funkcji
itoa
w pod-
rozdziale 3.6. Alternatyw stanowi rozwizanie rekurencyjne, w którym funkcja
printd
rozpoczyna prac od wywoania samej siebie w celu wywietlenia cyfr bardziej znacz-
cych ni cyfra aktualnie przetwarzana. Dopiero po powrocie z wywoanej funkcji wy-
pisywana 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)
Jzyk ANSI C. Programowanie
106
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
, która z kolei przekazuje
1
trzeciej.
Ta ostatnia wypisuje znak
1
i koczy prac. Wówczas funkcja na drugim poziomie
wypisuje znak
2
i równie koczy prac. Funkcja najwyszego poziomu wypisuje
3
i prze-
twarzanie pocztkowego wywoania
printd(123)
zostaje zakoczone.
Innym ciekawym przykadem rekurencji jest algorytm sortowania quicksort, opraco-
wany przez C.A.R. Hoare’a w 1962 roku. Z tablicy wybierany jest jeden element, a pozo-
stae zostaj podzielone na dwa podzbiory — elementów mniejszych oraz elementów
wikszych lub równych. Ten sam proces jest nastpnie powtarzany rekurencyjnie dla
kadego z podzbiorów. Gdy podzbiór 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 rób, 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 elementów 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];
Rozdzia 4. • Funkcje i struktura programu
107
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 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. Rekuren-
cja jest szczególnie 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 znaków, wywoujc
procedur rekurencyjn.
wiczenie 4.13. Napisz rekurencyjn wersj funkcji
reverse(s)
, odwracajcej „w miejscu”
cig znaków
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 znaków. W tym
podrozdziale opiszemy te inne moliwoci preprocesora: kompilacj warunkow i makra
z argumentami.
4.11.1. Wstawianie plików
Mechanizm wstawiania plików uatwia przede wszystkim obsug zbiorów 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 cudzy-
sów, wyszukiwanie pliku rozpoczyna si najczciej w katalogu programu ródowego.
Jeeli plik nie zostanie w nim znaleziony albo gdy zamiast cudzysowu uyto znaków
<
i
>
, wyszukiwanie przebiega zgodnie z zasadami okrelonymi przez implementacj.
Wczane dyrektyw
#include
pliki mog take zawiera wiersze
#include
.
Jzyk ANSI C. Programowanie
108
Na pocztku pliku ródowego znajduje si najczciej caa grupa wierszy
#include
,
które wczaj do programu podstawowe instrukcje
#define
i deklaracje
extern
. Mog
równie zapewnia dostp do deklaracji prototypów funkcji bibliotecznych, zapisanych
w nagówkach takich jak
<stdio.h>
(cilej: nagówki nie musz by plikami; zasady
dostpu do nagówków wyznacza implementacja).
Wczanie wierszem
#include
to podstawowa metoda czenia deklaracji w duych
programach. Gwarantuje ona, e wszystkie pliki ródowe bd miay dostp do tych
samych definicji i deklaracji zmiennych. Eliminuje to jeden z najbardziej uciliwych
rodzajów bdów w kodzie. Naturalnie gdy wczany plik ulega zmianie, wszystkie zale-
ne 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. Nor-
malnie s to wszystkie znaki do koca wiersza, ale duga definicja moe zosta podzielona
na kilka kolejnych wierszy przez wstawienie znaku
\
na kocu kadego wiersza, który ma
by kontynuowany. Zakres nazwy wskazanej w
#define
siga od wiersza
#define
do koca
kompilowanego pliku ródowego. Definicja moe korzysta z wczeniejszych definicji.
Substytucja nie obejmuje miejsc, w których nazwa jest czci duszej nazwy i frag-
mentów ujtych w cudzysów. 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
, które bdzie zastpowane ptl nieskoczon.
Mona take definiowa makra z argumentami, dziki którym tekst zastpujcy jest
róny w poszczególnych 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));
Rozdzia 4. • Funkcje i struktura programu
109
Dopóki argumenty s spójne, makro
max
moe pracowa z dowolnym typem danych.
Nie ma potrzeby definiowania rónych nazw
max
dla rónych typów danych, tak jakby
to byo w przypadku zastosowania funkcji.
Gdy przyjrzymy si sposobowi rozwijania makra
max
, zwrócimy uwag, e wie si
on z pewnymi puapkami. Wartoci wyrae s obliczane dwukrotnie. Staje si to istot-
nym problemem, gdy pojawiaj si efekty uboczne wynikajce ze stosowania operatorów
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. Pomyl-
my, 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 przykadów ich
zastosowania jest wczanie do kompilacji pliku
<stdio.h>
, w którym operacje
getchar
i
putchar
s czsto zdefiniowane jako makra. Pozwala to unikn obcienia programu
mechanizmem wywoywania funkcji przy odczycie pojedynczych znaków. Równie
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 cudzysów cig znaków, w którym parametr jest
zastpiony podanym argumentem faktycznym. W poczeniu z konkatenacj cigów
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);
Jzyk ANSI C. Programowanie
110
W argumencie faktycznym kady znak
"
jest zastpowany przez
\"
, a kady znak
\
przez
\\
, dziki czemu wynik to poprawna staa tekstowa.
Operator preprocesora
##
umoliwia konkatenowanie argumentów 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. Szczegóy mona znale w dodatku A.
wiczenie 4.14. Zdefiniuj makro
swap(t,x,y)
wymieniajce wartoci dwóch argumentów,
których typ to
t
(pomocna bdzie struktura blokowa).
4.11.3. Warunkowe wstawianie kodu
Istnieje moliwo sterowania prac samego preprocesora przy uyciu instrukcji wa-
runkowych, wykonywanych w trakcie jego dziaania. Zapewnia to moliwo wybiórcze-
go wstawiania kodu, w zalenoci od warunków, których wartoci s obliczane w czasie
kompilowania.
Wiersz
#if
oblicza warto staego wyraenia cakowitego (które nie moe zawiera
operatora
sizeof
, konwersji typów i staych
enum
). Jeeli wyraenie ma warto rón 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 nagówka hdr.h */
#endif
Pierwsza operacja wczania pliku hdr.h powoduje zdefiniowanie nazwy
HDR
. Przy kolej-
nych próbach wczenia preprocesor stwierdza, e nazwa zostaa ju zdefiniowana, i prze-
chodzi do wiersza
#endif
. Podejcie takie mona stosowa bardzo szeroko. Zachowanie
penej konsekwencji pozwala w kadym nagówku wcza do kompilacji dowolne inne
wymagane nagówki bez cigego ledzenia ich wzajemnych zalenoci.
Rozdzia 4. • Funkcje i struktura programu
111
Nastpujca sekwencja sprawdza tekst powizany z nazw
SYSTEM
, aby okreli , która
wersja nagówka 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 zde-
finiowana. Wczeniejszy przykad z
#if
mona zapisa jako
#ifndef HDR
#define HDR
/* tu znajduje si waciwa tre nagówka hdr.h */
#endif