background image

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!

background image

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

background image

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

background image

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

background image

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

background image

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.

background image

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.

background image

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

background image

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.cgetline.c i strindex.c.
W takiej sytuacji polecenie

cc main.c getline.c strindex.c

background image

Rozdzia 4. • Funkcje i struktura programu

89

kompiluje trzy wymienione pliki, umieszcza kod obiektów w plikach main.ogetline.o
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++;

background image

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

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

main

 bdzie traktowana jako 

int

 — uzyskiwane wtedy wartoci bd niemal zupe-

nie przypadkowe.

background image

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.

background image

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)

background image

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

background image

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 '/':

background image

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

background image

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,

background image

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.

background image

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:

Jak zapisywa  deklaracje, aby deklarowanie zmiennych waciwie przebiegao
w czasie kompilacji?

Jaki powinien by  ukad deklaracji, aby wszystkie elementy zostay waciwie
poczone w chwili adowania programu?

Jaki ukad deklaracji zapewnia, e nie s one powtarzane?

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.

background image

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:

background image

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:

background image

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) { ... }

background image

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

background image

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.

background image

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 }

background image

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)

background image

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

background image

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

.

background image

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

background image

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

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

background image

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.

background image

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