C++ w dwunastu długich krokach
Transkrypt
C++ w dwunastu długich krokach
Uniwersytet Wrocławski
Wydział Fizyki i Astronomii
Zbigniew Koza
C++
w dwunastu długich krokach
Wrocław 2006
Redakcja techniczna, opracowanie tekstu i skład: Zbigniew Koza
c 2006 by Zbigniew Koza and Uniwersytet Wrocławski
Copyright °
Drukarnia Uniwersytetu Wrocławskiego
Plac Solny 12, 50-061 Wrocław
tel. 713438389, 713752305, fax 713447258
Wszystkim Kózkom – dużym i małym
Spis treści
1 Pierwszy program w C++
1.1 Dla kogo jest ta książka? . . . . . . . . . . . . . . . . . . . . . .
1.2 Rys historyczny . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2.1 Dlaczego C++? . . . . . . . . . . . . . . . . . . . . . . .
1.2.2 C++ a C . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2.3 Aktualny standard języka . . . . . . . . . . . . . . . . .
1.3 Zanim napiszemy swój pierwszy program . . . . . . . . . . . .
1.3.1 Środowisko programistyczne . . . . . . . . . . . . . . . .
1.4 Pierwszy program . . . . . . . . . . . . . . . . . . . . . . . . .
1.4.1 Kompilacja kodu źródłowego . . . . . . . . . . . . . . .
1.4.2 Błędy kompilacji . . . . . . . . . . . . . . . . . . . . . .
1.4.3 Uruchamianie programu w środowisku Dev-C++ . . . .
1.4.4 Struktura prostego programu w C++ . . . . . . . . . .
1.5 Obiekt std::cout i literały . . . . . . . . . . . . . . . . . . . .
1.6 Definiowanie obiektów . . . . . . . . . . . . . . . . . . . . . . .
1.7 Identyfikatory, słowa kluczowe i dyrektywy . . . . . . . . . . .
1.8 Zapis programu . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.9 Cztery działania matematyczne i typ double . . . . . . . . . .
1.10 Jeszcze więcej matematyki . . . . . . . . . . . . . . . . . . . . .
1.11 Upraszczanie zapisu obiektów i funkcji biblioteki standardowej
1.12 Źródła informacji . . . . . . . . . . . . . . . . . . . . . . . . . .
1.13 Q & A . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.14 Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.15 Problemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
13
13
13
14
16
16
16
16
18
18
19
20
20
21
22
24
24
25
26
28
28
29
30
30
2 Wyrażenia i instrukcje
2.1 Instrukcje sterujące . . . . . . . . . . . . . . . . . . . . . .
2.1.1 Instrukcja if... else... . . . . . . . . . . . . . .
2.2 Pętle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.1 Pętla for . . . . . . . . . . . . . . . . . . . . . . .
2.2.2 Pętle while i do . . . . . . . . . . . . . . . . . . .
2.2.3 Instrukcje break i continue . . . . . . . . . . . .
2.3 Typy wbudowane . . . . . . . . . . . . . . . . . . . . . . .
2.3.1 Typy całkowite . . . . . . . . . . . . . . . . . . . .
2.3.2 Typy zmiennopozycyjne . . . . . . . . . . . . . . .
2.3.3 Typ logiczny . . . . . . . . . . . . . . . . . . . . .
2.3.4 Zapis literałów całkowitych i zmiennopozycyjnych
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
31
31
31
33
33
35
35
36
36
38
40
40
5
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6
Spis treści
2.4
2.5
Wyrażenia arytmetyczne, promocje i konwersje standardowe
Tworzenie obiektów stałych . . . . . . . . . . . . . . . . . .
2.5.1 Modyfikator const . . . . . . . . . . . . . . . . . . .
2.6 Popularne typy standardowe . . . . . . . . . . . . . . . . . .
2.6.1 Strumienie . . . . . . . . . . . . . . . . . . . . . . .
2.6.2 Napisy . . . . . . . . . . . . . . . . . . . . . . . . . .
2.6.3 Wektory . . . . . . . . . . . . . . . . . . . . . . . . .
2.6.4 Słowniki . . . . . . . . . . . . . . . . . . . . . . . . .
2.7 Obiekty lokalne i globalne. Zasięg. Przesłanianie . . . . . .
2.8 Operatory . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.8.1 Priorytet operatorów . . . . . . . . . . . . . . . . . .
2.8.2 Łączność operatorów . . . . . . . . . . . . . . . . . .
2.8.3 Wartość operatorów . . . . . . . . . . . . . . . . . .
2.8.4 Opis wybranych operatorów . . . . . . . . . . . . . .
2.8.5 Operatorowe patologie . . . . . . . . . . . . . . . . .
2.9 Wyrażenia i instrukcje . . . . . . . . . . . . . . . . . . . . .
2.10 Q & A . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.11 Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.12 Problemy . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3 Funkcje
3.1 Referencje . . . . . . . . . . . . . . . . .
3.2 Funkcje swobodne . . . . . . . . . . . .
3.3 Po co są funkcje? . . . . . . . . . . . . .
3.4 Funkcje składowe – wprowadzenie . . . .
3.5 Argumenty funkcji . . . . . . . . . . . .
3.5.1 Przekazywanie argumentów przez
3.5.2 Przekazywanie argumentów przez
3.5.3 Przekazywanie argumentów przez
3.6 Funkcje zwracające referencję . . . . . .
3.7 Operatory jako funkcje swobodne . . . .
3.8 Stos funkcji . . . . . . . . . . . . . . . .
3.9 Funkcje otwarte (inline) . . . . . . . .
3.10 Funkcje jako argumenty innych funkcji .
3.11 Rekurencja . . . . . . . . . . . . . . . .
3.12 Argumenty domyślne . . . . . . . . . . .
3.13 Lokalne obiekty statyczne . . . . . . . .
3.14 Funkcja main . . . . . . . . . . . . . . .
3.14.1 Argumenty funkcji main . . . . .
3.14.2 Wartość funkcji main . . . . . . .
3.15 Polimorfizm nazw funkcji . . . . . . . .
3.16 Deklaracja a definicja funkcji . . . . . .
3.17 Q & A . . . . . . . . . . . . . . . . . . .
3.18 Quiz . . . . . . . . . . . . . . . . . . . .
3.19 Problemy . . . . . . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
wartość . . . .
referencję . . .
stałą referencję
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
41
43
43
44
44
46
47
49
50
51
53
53
53
53
56
56
57
57
58
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
59
59
60
62
64
64
64
64
65
67
68
71
73
75
75
76
77
79
79
79
80
80
81
81
82
7
Spis treści
4 Tablice i wskaźniki
4.1 Wskaźniki . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.1.1 Definiowanie wskaźników . . . . . . . . . . . . . .
4.1.2 Wskaźniki typu void*, czyli wycieczka w stronę C
4.1.3 Wskaźnik zerowy . . . . . . . . . . . . . . . . . . .
4.1.4 Czym grozi nieumiejętne użycie wskaźników? . . .
4.1.5 Wskaźniki stałe i wskaźniki na stałe . . . . . . . .
4.1.6 Wskaźniki na wskaźniki . . . . . . . . . . . . . . .
4.2 Tablice . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.1 Tablice wielowymiarowe . . . . . . . . . . . . . . .
4.2.2 Inicjalizacja tablic . . . . . . . . . . . . . . . . . .
4.2.3 Zastosowanie operatora sizeof do tablic . . . . .
4.2.4 Tablice a wskaźniki . . . . . . . . . . . . . . . . . .
4.2.5 Tablice wskaźników i wskaźniki na tablice . . . . .
4.2.6 Tablice jako argumenty funkcji . . . . . . . . . . .
4.2.7 Teksty literalne i tablice znaków . . . . . . . . . .
4.2.8 Porównanie tablic i wektorów . . . . . . . . . . . .
4.3 Pamięć wolna (sterta) . . . . . . . . . . . . . . . . . . . .
4.4 Q & A . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.5 Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.6 Problemy . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
83
83
83
85
85
85
86
86
87
88
88
90
90
91
91
92
93
94
96
97
97
5 Klasy i obiekty
5.1 Struktury . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.1 Podstawowe zasady definiowania i używania struktur
5.1.2 Inicjalizacja struktur . . . . . . . . . . . . . . . . . .
5.1.3 Dostęp do składowych poprzez wskaźnik . . . . . . .
5.2 Co to są klasy? . . . . . . . . . . . . . . . . . . . . . . . . .
5.3 Definiowanie klas . . . . . . . . . . . . . . . . . . . . . . . .
5.3.1 Klasa jako „zmodyfikowana” struktura . . . . . . . .
5.3.2 Konstruktory . . . . . . . . . . . . . . . . . . . . . .
5.3.3 Destruktor . . . . . . . . . . . . . . . . . . . . . . .
5.4 Funkcje składowe (metody) . . . . . . . . . . . . . . . . . .
5.4.1 Metody i metody stałe . . . . . . . . . . . . . . . . .
5.4.2 Przeciążanie operatorów w klasie . . . . . . . . . . .
5.4.3 Konstruktor kopiujący i operator = . . . . . . . . . .
5.4.4 Wskaźnik this . . . . . . . . . . . . . . . . . . . . .
5.5 Udostępnianie składowych . . . . . . . . . . . . . . . . . . .
5.5.1 Sekcje public i private . . . . . . . . . . . . . . . .
5.5.2 Funkcje i klasy zaprzyjaźnione . . . . . . . . . . . .
5.6 Interfejs i implementacja . . . . . . . . . . . . . . . . . . . .
5.6.1 Podział definicji klasy na interfejs i implementację .
5.7 Kontrakty, niezmienniki i asercje . . . . . . . . . . . . . . .
5.7.1 Kontrakty . . . . . . . . . . . . . . . . . . . . . . . .
5.7.2 Niezmienniki . . . . . . . . . . . . . . . . . . . . . .
5.8 Hermetyzacja danych . . . . . . . . . . . . . . . . . . . . . .
5.9 Różnice między klasami i strukturami . . . . . . . . . . . .
5.10 Dygresja: składowe statyczne . . . . . . . . . . . . . . . . .
5.11 Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
99
99
99
101
101
101
102
102
103
106
107
108
109
110
112
113
113
114
115
115
118
118
119
120
120
121
122
8
Spis treści
5.12 Problemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
6 Dynamiczne struktury danych
6.1 Stos na bazie tablicy dynamicznej . . . . . . . . . . . .
6.1.1 Interfejs stosu . . . . . . . . . . . . . . . . . . . .
6.1.2 Implementacja stosu . . . . . . . . . . . . . . . .
6.1.3 Test stosu . . . . . . . . . . . . . . . . . . . . . .
6.2 Stos na bazie listy pojedynczo wiązanej . . . . . . . . .
6.2.1 Rekurencyjne struktury danych . . . . . . . . . .
6.2.2 Interfejs klasy . . . . . . . . . . . . . . . . . . . .
6.2.3 Implementacja . . . . . . . . . . . . . . . . . . .
6.3 Dygresja: przestrzenie nazw i zagnieżdżanie definicji klas
6.4 Q & A . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.5 Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.6 Problemy . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
125
125
125
126
131
132
133
133
134
136
138
138
138
7 Dziedziczenie i polimorfizm
7.1 Dziedziczenie . . . . . . . . . . . . . . . . . . . . . . . . . .
7.1.1 Do czego służy dziedziczenie? . . . . . . . . . . . . .
7.1.2 Definiowanie klasy pochodnej . . . . . . . . . . . . .
7.1.3 Inicjalizacja klasy bazowej . . . . . . . . . . . . . . .
7.1.4 Relacje „X ma Y”, „X jest Y” oraz „X zarządza Y”
7.1.5 Kolejność inicjalizacji i destrukcji obiektu . . . . . .
7.1.6 Sekcja protected . . . . . . . . . . . . . . . . . . .
7.1.7 Zastępowanie (overridnig) funkcji składowych . . . .
7.2 Polimorfizm . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2.1 Niedoskonałości „zwykłego” dziedziczenia . . . . . .
7.2.2 Definiowanie metod polimorficznych . . . . . . . . .
7.2.3 vtable . . . . . . . . . . . . . . . . . . . . . . . . .
7.2.4 Dygresja: klasy abstrakcyjne . . . . . . . . . . . . .
7.3 Jak to się robi w Qt? . . . . . . . . . . . . . . . . . . . . . .
7.4 Q & A . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.5 Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.6 Problemy . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
139
139
139
141
142
144
146
146
147
149
149
151
153
155
156
159
159
160
8 Strumienie
8.1 Strumienie buforowane i niebuforowane . . . . . . . . . . .
8.2 Klawiatura, konsola, plik, strumień napisowy . . . . . . . .
8.3 Stan strumienia . . . . . . . . . . . . . . . . . . . . . . . . .
8.4 Manipulatory i formatowanie strumienia . . . . . . . . . . .
8.4.1 Manipulator std::setw . . . . . . . . . . . . . . . .
8.4.2 Manipulator std::setprecision . . . . . . . . . . .
8.5 Strumienie wyjścia . . . . . . . . . . . . . . . . . . . . . . .
8.6 Strumienie wejścia . . . . . . . . . . . . . . . . . . . . . . .
8.6.1 Funkcje składowe get i getline . . . . . . . . . . .
8.6.2 Inne funkcje operujące na strumieniach wejściowych
8.7 Przykład . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.8 Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.9 Problemy . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
161
161
162
164
164
165
166
167
167
168
169
169
172
172
.
.
.
.
.
.
.
.
.
.
.
.
9
Spis treści
9 Biblioteki
9.1 Podział programu na pliki . . . . . . . . . . . . . . . . . . .
9.1.1 Zasady kompilacji programów podzielonych na pliki
9.1.2 Przygotowanie plików źródłowych i nagłówkowych .
9.1.3 Kompilacja przy pomocy wiersza poleceń . . . . . .
9.1.4 Kompilacja przy pomocy programu make . . . . . .
9.1.5 Kompilacja przy pomocy projektów . . . . . . . . .
9.1.6 Automatyzacja tworzenia pliku Makefile . . . . . . .
9.2 Używanie gotowych bibliotek . . . . . . . . . . . . . . . . .
9.2.1 Dekorowanie nazw i deklaracja extern "C" . . . . .
9.2.2 Przykłady . . . . . . . . . . . . . . . . . . . . . . . .
9.3 Kompilacja i instalacja bibliotek z plików źródłowych . . . .
9.3.1 Przykłady . . . . . . . . . . . . . . . . . . . . . . . .
9.3.2 Systemy kontroli wersji . . . . . . . . . . . . . . . .
9.4 Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9.5 Problemy . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
173
173
174
175
176
178
179
181
182
182
183
186
186
187
188
188
10 Preprocesor i szablony
10.1 Preprocesor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10.1.1 Rola preprocesora w C++ . . . . . . . . . . . . . . . . . .
10.1.2 Dyrektywy preprocesora . . . . . . . . . . . . . . . . . . .
10.1.3 Przykład . . . . . . . . . . . . . . . . . . . . . . . . . . .
10.1.4 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . .
10.2 Szablony . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10.2.1 Szablony klas . . . . . . . . . . . . . . . . . . . . . . . . .
10.2.2 Szablony funkcji składowych . . . . . . . . . . . . . . . .
10.2.3 Szablony funkcji swobodnych . . . . . . . . . . . . . . . .
10.2.4 Specjalizacja szablonu . . . . . . . . . . . . . . . . . . . .
10.2.5 Używanie szablonów funkcji do upraszczania pracy z szablonami klas . . . . . . . . . . . . . . . . . . . . . . . . .
10.2.6 Gdzie umieszczać definicje szablonów? . . . . . . . . . . .
10.2.7 Szablony a programowanie generyczne . . . . . . . . . . .
10.2.8 Dygresja: konstrukcja typedef . . . . . . . . . . . . . . .
10.3 Quiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10.4 Problemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
189
189
189
190
193
195
195
195
198
199
200
.
.
.
.
.
.
201
202
202
202
203
204
11 Wprowadzenie do STL
11.1 Co to jest STL? . . . . . . . . . . . . . . . . . . . . . . . .
11.2 Pojemniki . . . . . . . . . . . . . . . . . . . . . . . . . . .
11.3 Iteratory . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11.3.1 Co to są iteratory? . . . . . . . . . . . . . . . . . .
11.3.2 Przykład użycia iteratorów . . . . . . . . . . . . .
11.3.3 Rodzaje iteratorów . . . . . . . . . . . . . . . . . .
11.4 Algorytmy . . . . . . . . . . . . . . . . . . . . . . . . . . .
11.4.1 Co to są algorytmy? . . . . . . . . . . . . . . . . .
11.4.2 Funktory . . . . . . . . . . . . . . . . . . . . . . .
11.4.3 Obiekty funkcyjne . . . . . . . . . . . . . . . . . .
11.4.4 Wartości pobierane i zwracane przez algorytmy . .
11.4.5 Obiekty funkcyjne a efektywność szablonów funkcji
.
.
.
.
.
.
.
.
.
.
.
.
205
205
206
206
206
207
209
209
209
210
212
213
214
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
10
Spis treści
11.5 Wektory (std::vector) . . . . . . . .
11.5.1 Opis szablonu std::vector<T>
11.5.2 Przykłady . . . . . . . . . . . .
11.6 Liczby zespolone . . . . . . . . . . . .
11.7 Napisy (std::string) . . . . . . . . .
11.8 Q & A . . . . . . . . . . . . . . . . . .
11.9 Quiz . . . . . . . . . . . . . . . . . . .
11.10Problemy . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
216
216
219
220
221
223
223
224
12 Pojemniki i algorytmy
12.1 Przegląd pojemników STL . . . . . . . . . . . . . .
12.1.1 Słowniki . . . . . . . . . . . . . . . . . . . .
12.1.2 Zbiory . . . . . . . . . . . . . . . . . . . . .
12.1.3 Wielosłowniki i wielozbiory . . . . . . . . .
12.1.4 Słowniki i zbiory mieszające . . . . . . . . .
12.1.5 Kolejki o dwóch końcach . . . . . . . . . . .
12.1.6 Listy . . . . . . . . . . . . . . . . . . . . . .
12.1.7 Stosy, kolejki i kolejki priorytetowe . . . . .
12.1.8 Wektory i zbiory bitów . . . . . . . . . . .
12.1.9 Wektory numeryczne (std::valarray<T>)
12.2 Przegląd algorytmów swobodnych . . . . . . . . . .
12.2.1 Algorytmy niemodyfikujące . . . . . . . . .
12.2.2 Algorytmy modyfikujące . . . . . . . . . . .
12.2.3 Sortowanie . . . . . . . . . . . . . . . . . .
12.2.4 Algorytmy numeryczne . . . . . . . . . . .
12.2.5 Inne algorytmy . . . . . . . . . . . . . . . .
12.3 Kontrola poprawności użycia STL . . . . . . . . .
12.4 Składniki dodatkowe . . . . . . . . . . . . . . . . .
12.5 Q & A . . . . . . . . . . . . . . . . . . . . . . . . .
12.6 Problemy . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
225
225
226
230
231
231
233
234
234
236
237
237
238
239
242
244
244
245
245
245
246
13 Co dalej?
247
A Wybrane opcje kompilatora g++
249
B Dodatkowe elementy języka
251
B.1 Instrukcja goto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
B.2 Instrukcja switch . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
Wstęp
Niniejszy podręcznik stanowi zapis wykładu prowadzonego w 2006 r. w Instytucie Fizyki Uniwersytetu Wrocławskiego w ramach finansowanego ze środków Unii
Europejskiej projektu „Technologie Informatyczne od Podstaw”. Wykład adresowany jest do osób pracujących, które znają już jakiś język programowania (np.
Pascal, Basic, PHP) i chciałyby poznać język C++ w celu podniesienia swoich
kwalifikacji.
Dlaczego C++? Odpowiedź jest prosta – w ciągu blisko dwudziestu lat swego istnienia C++ osiągnął status języka programowania najczęściej używanego
w zastosowaniach komercyjnych. Co więcej, każdy kto pozna C++, niejako mimochodem nauczy się rozumieć drugi podstawowy język programowania – język
C – i będzie miał znacznie ułatwioną drogę do opanowania wielu innych języków
(np. Java, C#, bash).
Decydujący wpływ na zakres zawartego tu materiału ma długość kursu – 12
dwugodzinnych wykładów i tyleż ćwiczeń w laboratorium komputerowym. Odpowiada to zaledwie 80% typowego jednosemetralnego kursu uniwersyteckiego.
Z własnego doświadczenia wykładowcy języka C++ wiem jednak, że w praktyce
nawet dwa semestry nie starczają, by w pełni omówić wszystkie ważne aspekty programowania w C++. Dlatego w swoim wykładzie skoncentrowałem się na
najważniejszych cechach języka. Mam nadzieję, że to ograniczenie wpłynie pozytywnie na jakość wykładu – większość książek dostępnych obecnie w księgarniach
jest bowiem bardzo rozwlekła, gdyż ich autorzy usiłują przedstawić kompletny
opis języka, włącznie z tymi jego elementami, których sami nigdy w życiu nie używali. Natomiast mój podręcznik z założenia stawia sobie znacznie skromniejszy
cel: rzetelne nauczenie Czytelnika podstawowych zasad programowania we współczesnym C++. Po zakończeniu kursu jego uczestnicy powinni móc uczestniczyć
w typowym projekcie programistycznym i samodzielnie rozwijać swoje umiejętności w zakresie programowania, np. studiując odpowiednie biblioteki C lub C++
potrzebne do realizacji danego zadania.
Język C++ jest trudny zarówno do nauczania, jak i uczenia się. Nie powstał
bowiem (jak np. Pascal) w wyniku teoretycznych rozważań nad metodologią programowania; przeciwnie, C++ został zaprojektowany przez praktyków (a konkretnie przez Bjarne’a Stroustrupa) jako narzędzie mające pomóc w rozwiązaniu
problemów praktycznych. Problemów, dodajmy, które zazwyczaj objawiają się
dopiero w dużych projektach i mają związek z wydajnością i kosztami pracy
programistów. Tu właśnie tkwi źródło trudności związanych z dydaktyką C++:
niemal wszystkie charakterystyczne elementy tego języka, które odróżniają go od
takich języków, jak C czy Pascal, znajdują zastosowanie (i uzasadnienie) dopiero
11
12
Wstęp
w dużych projektach. Tymczasem kurs poświęcony podstawom języka musi ograniczać się do omawiania krótkich programów; analogicznie ćwiczenia laboratoryjne muszą bazować na niewielkich projektach, które można zrealizować w ciągu
kilku godzin. Ale jak osobę, która pisze wyłącznie programy liczące 100-200 wierszy i uparła się, by wszystkie zmienne (włącznie ze zmiennymi sterującymi pętli)
umieszczać w przestrzeni globalnej („bo tak szybciej”) przekonać, że popełnia
grzech główny? Jak przekonać ją o użyteczności klas czy wyjątków? Jak wyjaśnić
zasady dzielenia programów na osobne pliki bez sprawiania wrażenia, że są to
jakieś kosmiczne wymysły mające tylko utrudniać ludziom życie?
Mając na względzie powyższe trudności oraz fakt, że moim celem jest nauczenie C++ „od podstaw” w 12 wykładach, podręcznik podzieliłem na 12 rozdziałów, przy czym stopień zaawansowania kilku początkowych dostosowany jest do
potrzeb (i poziomu) osób piszących nie tyle nawet programy, ile krótkie „wprawki”. Pierwszy rozdział zawiera krótki opis języka, kilka przykładowych programów
oraz informacje niezbędne do ich kompilacji. Kolejne 3 rozdziały zawierają zwięzły opis nieobiektowych cech języka, a więc m.in. konstrukcji wyrażeń i instrukcji, definiowania zmiennych i tablic oraz posługiwania się funkcjami swobodnymi.
Cztery pierwsze rozdziały wprowadzają więc Czytelnika w świat programowania
proceduralnego w C++ lub, innymi słowy, w świat C++ traktowanego jako „ulepszona” wersja języka C. Kolejne cztery rozdziały poświęcone są programowaniu
obiektowemu w C++. Znajdziemy więc tu m.in. objaśnienie tak fundamentalnych pojęć, jak obiekty i klasy (z metodami, konstruktorami i destruktorami),
dziedziczenie, hermetyzacja danych i polimorfizm. Rozdział 9 przedstawia sposoby wykorzystywania bibliotek zewnętrznych (napisanych w C lub C++) oraz
metody dzielenia własnego projektu na wiele plików źródłowych. Trzy ostatnie
rozdziały poświęcone są omówieniu programowania generycznego, co obejmuje
wprowadzenie w takie zagadnienia, jak szablony i biblioteka STL.
Układ książki może sugerować, iż wykład rozpoczyna się od prezentacji języka
C, po czym przechodzi do omawiania C++ jako rozszerzenia języka C. W rzeczywistości jednak nie zamierzam opisywać tu samego języka C, który – mimo że de
facto jest podzbiorem języka C++ – zbytnio ustępuje swemu następcy, by warto
było poświęcać mu oddzielnie uwagę. Układ podręcznika odzwierciedla natomiast
fakt, że język C++ jest tworem eklektycznym, umożliwiającym programowanie
w co najmniej trzech godnych polecenia stylach (proceduralnym, obiektowym,
generycznym) lub ich kombinacji.
Niniejszy podręcznik powstał w oparciu o moje wieloletnie doświadczenie
w nauczaniu C++. Wiele jego rozdziałów zostało zainspirowanych książkami
B. Stroustrupa [1], B. Milewskiego [2] i J. Liberty’ego [3]. Pierwsza z nich jest
lekturą obowiązkową każdego programisty C++. Na tę jedyną książkę nie warto
żałować pieniędzy – po prostu trzeba ją mieć i sięgać po nią możliwie często.
Niestety, nie jest to lektura łatwa, nie jest też pomyślana jako podręcznik dla
początkujących. Dwie pozostałe cenię za oryginalne podejście pedagogiczne do
tematu. Żadna z nich nie stara się zastąpić książki Stroustrupa; usiłują natomiast z dość dobrym skutkiem przedstawić język C++ od podstaw w sposób
zrozumiały dla przeciętnego entuzjasty programowania.
Na zakończenie pozwolę sobie jeszcze podziękować Bartoszowi Milewskiemu
– mojemu pierwszemu nauczycielowi C++.
Rozdział 1
Pierwszy program w C++
Dla kogo jest ta książka? Rys historyczny. Pierwszy program. Środowisko programistyczne. Kompilator i kompilacja. Błędy kompilacji. Typy int i double, obiekty std::cin
i std::cout. Instrukcja using namespace std.
1.1
Dla kogo jest ta książka?
Podręcznik napisałem z myślą o osobie pragnącej nauczyć się języka C++ od
podstaw. Zakładam, że Czytelnik biegle posługuje się komputerem (np. potrafi zainstalować program lub „odstrzelić” zawieszoną aplikację), ma dostęp do
Internetu i zna już jakiś język programowania. Oczywiście niezbędna jest też
przyzwoita znajomość języka angielskiego – co najmniej w stopniu pozwalającym swobodnie rozumieć komunikaty diagnostyczne kompilatora, a najlepiej na
poziomie pozwalającym rozumieć oryginalną dokumentację bibliotek.
1.2
Rys historyczny
Dawno, dawno temu, czyli w połowie XX-go wieku komputery były bardzo kosztownymi urządzeniami, które wymagały niezwykle pracochłonnej obsługi. W tych
zamierzchłych czasach to ludzie dostosowywali się do możliwości maszyn cyfrowych i obsługiwali je, wpisując programy jako ciągi „zer” i „ jedynek” poprzez
ręczne ustawienie przekaźników; wyniki obliczeń również otrzymywano jako sekwencje zer i jedynek odczytywanych ze stanu lampek. Nieco później pojawiły
się asemblery – specjalne programy, które pozwalały obsłudze komputerów pisać
programy w nieco bardziej dla ludzi zrozumiałych kategoriach rozkazów, liczb,
zmiennych; programy te były następnie tłumaczone przez specjalny program na
kod maszynowy – człowiek mógł wreszcie „zapomnieć” o zerach i jedynkach.
Następnie pojawiły się języki proceduralne, np. FORTRAN; programy w nich
napisane przypominały już wzory matematyczne, były więc już całkiem dobrze
dostosowane do rozwiązywania wielu zagadnień technicznych. Wciąż jednak koszt
komputerów był ogromny, a ich moc – szczególnie z dzisiejszej perspektywy – była bardzo ograniczona. W tych czasach komputery były obsługiwane wyłącznie
przez wysoko wykwalifikowany personel, a główną troską programistów było pisanie programów działających możliwie jak najszybciej i zajmujących jak najmniej
13
14
Rozdział 1. Pierwszy program w C++
zasobów maszyny (głównie pamięci operacyjnej). W tych warunkach nie martwiono się specjalnie kwestią wygody obsługi komputerów – łatwiej i taniej było
wyszkolić ludzi do obsługi stosunkowo prymitywnych komputerów niż odwrotnie.
Dziś komputery są jednocześnie i bardzo szybkie, i tanie, a przez to powszechnie dostępne; co więcej, dysponują zasobami sprzętowymi i mocą obliczeniową tysiące lub nawet miliony razy przewyższającymi te dostępne jeszcze 30 lat temu.
Typowymi użytkownikami komputerów nie są już wyłącznie błyskotliwi i wszechstronnie wykształceni fachowcy – obecnie komputerami posługują się już nawet
przedszkolacy. Ci nowi, nierzadko niepiśmienni użytkownicy wymagają, by komputery wyposażone były w zupełnie inny rodzaj programów – niezawodnych i łatwych w obsłudze. Takie programy są jednak z natury bardzo złożone1 . Ich wytworzenie wymaga zaangażowania dziesiątek, setek, a czasami tysięcy ludzi. Masowy
rynek oznacza potencjalnie wielkie zyski, ale też i ogromne koszty oraz ryzyko
utraty rynku na rzecz konkurencji. Stąd wynika potrzeba ograniczenia kosztów.
Te z kolei najefektywniej jest redukować już podczas pisania programów, co oznacza konieczność wyposażenia programistów w możliwe jak najlepsze „narzędzia
produkcji”: edytory (do wprowadzania tekstu), debuggery (do wyszukiwania błędów), systemy kontroli wersji (do zarządzania poprawkami w programach) i –
przede wszystkim – język programowania z możliwie najlepszym kompilatorem,
czyli programem tłumaczącym programy napisane przez ludzi na zrozumiały dla
komputera kod maszynowy.
1.2.1
Dlaczego C++?
W tej sytuacji pod koniec lat osiemdziesiątych na scenę wkracza C++. Język
ten, zaprojektowany jako obiektowe rozszerzenie języka C, szybko osiągnął status najpopularniejszego niespecjalistycznego języka programowania. Stało się tak
dlatego, że pozwala on na zwiększenie wydajności programistów. Szacuje się, że
jeden programista stosujący C++ może sobie poradzić z kilkukrotnie większą
(w sensie funkcjonalnym) porcją kodu niż jego kolega posługujący się C. Ponadto
kod napisany w C++ jest łatwiejszy w utrzymaniu, rozwoju i ponownym wykorzystaniu w innym projekcie; nie ustępuje przy tym wcale prędkością programom
napisanym w C, natomiast w zakresie tym bije na głowę takie popularne języki,
jak Java, C# czy Lisp.
Jakie są główne zalety C++?
• C++ umożliwia stosowanie wielu stylów programowania i pracę
na wielu poziomach abstrakcji. Można w nim stosować wstawki asemblerowe, a więc programować w języku maszyny; można też programować
proceduralnie w stylu takich języków, jak Pascal czy C; można też programować obiektowo, a więc na wysokim poziomie abstrakcji, ułatwiającym
programistom myślenie w kategoriach rozwiązywanego przez nich zadania,
a nie abstrakcyjnych zer i jedynek, rozkazów maszynowych, adresów czy
rejestrów.
• C++ zawiera mechanizmy ułatwiające wielokrotne wykorzystywanie tego samego kodu. Jest to najważniejsza cecha języka. Programy
w C++ (tak jak w C) można dzielić na osobne części („moduły”); części
1 Na przykład projekt KDE to ponad 4 miliony wierszy kodu nad którym pracuje ponad 800
programistów; tylko w maju 2002 roku dokonano 11 014 zmian w kodzie źródłowym [4].
Rys historyczny
15
te można następnie wykorzystywać w dowolnym innym programie C++,
przy czym taki moduł wystarczy raz skompilować, a następnie tylko dołączać do nowych programów (w ten sposób otrzymuje się „biblioteki”).
Ponadto w C++ takie moduły można w bardziej naturalny sposób niż w C
odseparować od siebie (dzięki hermetyzacji danych i interfejsom), tworząc
z nich prawdziwe „czarne skrzynki”. Dzięki tzw. wyjątkom twórcy bibliotek
mogą precyzyjnie określić zachowanie się ich kodu w wypadku sytuacji nadzwyczajnych i nawet wymusić określone zachowanie na użytkownikach tych
bibliotek. Tak jak w innych językach programowania, w C++ można uniknąć powielania tego samego kodu poprzez zastosowanie funkcji. Ale C++
oferuje dużo więcej – szablony, dzięki którym ten sam kod można stosować
do obiektów różnych typów, a także przestrzenie nazw, dzięki którym łatwo
jest zarządzać nazwami używanymi w różnych bibliotekach.
• C++ jest rozszerzeniem najpopularniejszego języka lat 80-tych
– języka C. Dlatego programy napisane w C++ mają pełny dostęp do
wszystkich niezliczonych bibliotek kiedykolwiek napisanych w C.
• Kompilator C++ przejmuje na siebie wiele zadań, za realizację
których jeszcze niedawno całkowitą odpowiedzialność ponosił programista. Kompilator sprawdza na przykład, czy użycie zmiennych lub
funkcji jest zgodne z ich deklaracją. Możemy też poinstruować go, że pewne
zmienne mogą być wykorzystywane tylko w określonych partiach programu
albo że ich wartości nie mogą być tam modyfikowane, albo że muszą być
zainicjalizowane. W związku z tym, że kompilator C++ z definicji znacznie
dokładniej analizuje kod źródłowy, zaleca się nawet, by programy napisane
w „czystym” C kompilować kompilatorem C++.
• C++ posiada konstrukcje ułatwiające zarządzanie zasobami komputera. Należą do nich m.in. konstruktory, destruktory i wyjątki. Stosując
je systematycznie, można zagwarantować np., że z naszego programu nie
będzie „wyciekać” pamięć operacyjna.
• Programy napisane w C++ są co najmniej równie szybkie, jak te
napisane w C. A niekiedy szybsze (np. dzięki szablonom sortowanie jest
nieco szybsze w C++ niż w C).
• Biblioteki standardowe C++ są o wiele bardziej funkcjonalne od
ich odpowiedników ze standardowej biblioteki C. Standardowa biblioteka C++ zawiera m.in. wysokopoziomowy typ napisów std::string,
który całkowicie zwalnia programistę od troski o to, w jaki sposób napisy
są faktycznie interpretowane przez procesor. Podobnie klasa std::vector
jest dużo bardziej elastyczna od standardowych tablic języka C, a o czymś
takim jak słownik (std::map) możemy w C tylko pomarzyć.
Oczywiście C++ ma też wady:
• C++ jest trudny zarówno do nauczania, jak i uczenia się;
• C++ stanowi niezwykle trudne wyzwanie dla twórców kompilatorów. O ile
mi wiadomo, żaden powszechnie używany kompilator (tj. gcc i MS Visual
C++) nie jest jeszcze w 100% zgodny ze standardem ISO z 1998 r.
• Ze względu na swój olbrzymi potencjał, w rękach niedouczonego programisty C++ jest jak przysłowiowa zapałka w ręku dziecka.
16
1.2.2
Rozdział 1. Pierwszy program w C++
C++ a C
Jak już wspomniałem, C++ jest rozszerzeniem C. Wiele osób wyciąga stąd wniosek, że aby nauczyć się „trudnego” C++, dobrze jest rozpocząć od „łatwego” C.
Wniosek ten jest jednak równie fałszywy jak teza, że przed nauką jazdy samochodem należy nauczyć się jeździć rowerem (bo także ma kierownicę, hamulce i koła).
Język C wymaga stosowania od samego początku dość karkołomnych konstrukcji,
których niemal nie używa się w C++ (np. funkcji printf/scanf czy nieustannej
żonglerki wskaźnikami). Dlatego programiści obeznani w języku C, aby dobrze
opanować C++ muszą oduczyć się wielu nawyków nabytych podczas pracy w C.
Jeżeli ostatecznym celem jest C++, nauka C jest marnowaniem czasu!
1.2.3
Aktualny standard języka
Na przestrzeni lat język C++ podlegał ciągłej ewolucji. Większość zmian polegała na dodawaniu nowych cech rozszerzających możliwości języka. Kilka istotnych
modyfikacji doprowadziło jednak do niezgodności nowszych wersji języka z wersjami starszymi. Dlatego programy napisane 10 lat temu mogą nie spełniać wymagań aktualnego standardu. W niniejszym podręczniku za standard w wymiarze
teoretycznym uznaje się standard ISO C++ opisany w piątym wydaniu książki
Bjarne’a Stroustrupa „Język C++”. Z kolei w wymiarze praktycznym za standardowe uznaje się tu programy kompilowane szeroko dostępnym kompilatorem
GNU g++ w wersji 3.4.2 z opcjami -ansi -pedantic. Oba te warunki powinny
zapewnić całkowitą zgodność prezentowanych tu konstrukcji języka z aktualnie
obowiązującym standardem ISO C++ z 1998 roku, a więc w efekcie uniezależnić
przedstawiane tu informacje od platformy czy konkretnej implementacji kompilatora.
1.3
1.3.1
Zanim napiszemy swój pierwszy program
Środowisko programistyczne
Programista, jak każdy zawodowiec, potrzebuje specjalistycznego warsztatu pracy. Nie mam tu oczywiście na myśli samego komputera, lecz specjalnych programów służących do tworzenia nowych lub ulepszania starych programów. Takie
zastaw programów nazywa się środowiskiem programistycznym.
Istnieją dwa podstawowe rodzaje środowisk. Pierwsze, typowe dla systemu
Unix i jego pochodnych, to po prostu kolekcja oddzielnych programów uruchamianych z powłoki systemowej; do tej grupy należy m.in. używany w tym podręczniku kompilator gcc. Drugi rodzaj środowisk to tzw. zintegrowane środowiska
programistyczne (IDE, ang. Integrated Development Environments), w których
programista do wszystkich potrzebnych narzędzi ma dostęp z jednego programu
sterującego zintegrowanego ze specjalistycznym edytorem; do tej kategorii należą
m.in. kompilatory firm Microsoft (Visual Studio) i Borland (C++ Builder).
Zaletą pierwszego rozwiązania jest to, że daje użytkownikom pełną swobodę
w sposobie posługiwania się kompilatorem. W szczególności kompilator gcc można łatwo i ściśle zintegrować z wieloma dostępnymi edytorami tekstu (np. emacs
czy vim) lub istniejącymi środowiskami zintegrowanymi (np. C++ Builder), można też na jego podstawie zbudować od podstaw zupełnie nowe środowisko typu
Zanim napiszemy swój pierwszy program
17
Menu
Paski narzędzi
Okno widoku projektu,
klasy lub zmiennych
Okienko edycji programu
Zakładki okna raportu
Pasek stanu
Rysunek 1.1: Główne elementy okna programu Dev-C++.
IDE (np. KDeveloper i Anjuta w systemie Linux). Podczas zajęć laboratoryjnych
towarzyszących bieżącemu kursowi korzystać będziemy z programu Dev-C++,
który jest (darmowym) zintegrowanym środowiskiem programistycznym opartym na kompilatorach serii gcc i pracującym w systemie Windows. Program ten
można pobrać z Sieci ze strony http://www.bloodshed.net2 . Wybraliśmy go,
gdyż (i) jego używanie nie jest obwarowane licencjami komercyjnymi; (ii) oparty
jest na bardzo dobrym zestawie kompilatorów gcc/MinGW; (iii) zajmuje niewiele
miejsca, jest szybki i łatwy w instalacji i obsłudze; (iv) towarzyszy mu ogromna
liczba opcjonalnych bibliotek; (v) jest programem dobrze nam znanym, od kilku
lat używanym (z wyboru) przez naszych studentów.
Typowy wygląd okna programu Dev-C++ przedstawia Rys. 1.1. Głównymi
elementami interfejsu użytkownika w tym programie są:
• Okno edytora tekstu. Jest dostosowane do pisania programów w C lub
C++, a wiele jego właściwości może być swobodnie konfigurowanych przez
użytkownika.
• Paski narzędzi. Ułatwiają dostęp do często wykonywanych czynności, np.
kompilacji programu, obsługi projektów, wyszukiwania tekstu.
• Menu. Udostępnia wszystkie funkcje programu.
• Okno widoku projektu. Ułatwia orientację w dużych programach.
• Okna raportu. Zawierają dodatkowe informacje, np. pełny komunikat
kompilatora o przebiegu kompilacji.
• Pasek stanu. Podaje podstawowe informacje o edytowanym pliku.
2 Jeśli Czytelnik zdecyduje się zainstalować u siebie środowisko Dev-C++, warto rozszerzyć
je o najnowszą wersję środowiska MSYS (http://www.mingw.org), które zwiera programy niezbędne do kompilowania programów i bibliotek dostępnych na licencji GNU. Jeszcze większe
możliwości daje instalacja systemu CygWin (http://sources.redhat.com).
18
1.4
Rozdział 1. Pierwszy program w C++
Pierwszy program
Oto nasz pierwszy, najprostszy program w C++. Jego celem jest wyświetlenie na
ekranie napisu Pierwszy program w C++.
Wydruk 1.1: Pierwszy program w C++.
#include <iostream>
int main()
{
std :: cout << ”Pierwszy program w C++” << ”\n”;
}
Tekst programu należy zapisać w pliku o odpowiednim rozszerzeniu. W przypadku kompilatora gcc 3.4.2 za standardowe rozszerzenia nazw plików zawierających programy w C++ uznaje się plik.cc, plik.cp, plik.cxx, plik.cpp, plik.CPP,
plik.c++ oraz plik.C. W niniejszym kursie stosować będę bodaj najpopularniejsze
rozszerzenie plik.cpp.
1.4.1
Kompilacja kodu źródłowego
Programy pisze się w postaci możliwie jak najbardziej zrozumiałej dla ludzi, po
czym przy pomocy specjalnych programów tłumaczy się je na ciąg instrukcji
przeznaczonych bezpośrednio dla komputera. Obie te wersje zwyczajowo zwie
się „programem”. Aby uniknąć ewentualnych nieporozumień, program w postaci
zrozumiałej dla ludzi zwie się kodem źródłowym, a w postaci przeznaczonej dla
komputera – kodem maszynowym lub wynikowym3 .
Program tłumaczący kod źródłowy na maszynowy zwany jest kompilatorem,
a sam proces tłumaczenia – kompilacją. Słowa ‘kompilator’ i ‘kompilacja’ są jednak wieloznaczne. Jeszcze nie tak dawno temu proces tłumaczenia kodu źródłowego na maszynowy składał się z wielu kroków wykonywanych przez osobne
programy. W wypadku języka C były to m.in. preprocesor, kompilator, asembler,
konsolidator. Najważniejszym instrumentem w tej orkiestrze był (i jest) kompilator – od jego jakości w największym stopniu zależy jakość kodu maszynowego.
Stąd po pewnym czasie cały proces translacji kodu źródłowego zaczęto nazywać
kompilacją; analogicznie cały zestaw programów zaangażowanych w tak rozumianą kompilację zwie się kompilatorem.
Z biegiem czasu pojawiła się tendencja, by możliwie jak najbardziej uprościć
obsługę procesu tłumaczenia programów. Obecnie w zintegrowanych środowiskach programistycznych translację programu można wykonać po prostu za naciśnięciem myszą odpowiedniego przycisku. Z kolei twórcy kompilatora gcc przyjęli
zasadę, że wszystkie etapy translacji można (jawnie lub nie) wykonać przy pomocy polecenia gcc, przy czym szczegółami samego procesu translacji steruje się
tu przy pomocy rozlicznych opcji podawanych w wierszu poleceń.
Wśród profesjonalistów żelazną regułą jest, by duże programy dzielić na stosunkowo małe fragmenty umieszczane w osobnych plikach źródłowych. W tym
3 Słowo „program” ma jeszcze trzecie znaczenie informatyczne – ‘kod znajdujący się w fazie
realizacji przez maszynę’; aby uniknąć dwuznaczności, zwie się go ‘procesem’.
Pierwszy program
19
wypadku translację kodu źródłowego dzieli się na kompilację poszczególnych plików, w wyniku czego otrzymuje się tzw. pliki obiektowe, i łączenie (zwane też konsolidacją lub linkowaniem) plików obiektowych z plikami bibliotecznymi w jeden
plik wykonywalny. W środowisku kompilatora gcc proces ten zazwyczaj sterowany jest specjalnym programem make, natomiast w zintegrowanych środowiskach
programistycznych efektywna praca z programem podzielonym na wiele plików
wymaga utworzenia tzw. projektu. Z początku będziemy pisać wyłącznie małe
programy mieszczące się w jednym pliku i ani szczegółami obsługi programu make,
ani sposobami tworzenie projektów nie musimy się jeszcze zajmować; ważne jest
jednak, by zdawać sobie sprawę z pewnego zamieszania pojęciowego panującego w
zintegrowanych środowiskach programistycznych. Wiele z nich, np. Visual C++
czy Anjuta, pod nazwą ‘compile’ rozumie wyłącznie pierwszą fazę translacji, czyli
tłumaczenie bieżącego pliku źródłowego na kod obiektowy. Z kolei Dev-C++ pod
nazwą ‘compile’ (lub ‘kompiluj’) rozumie cały proces translacji aż do utworzenia
pliku wykonywalnego4 .
Pora na małe podsumowanie tego przydługiego wywodu. Aby przetłumaczyć
plik źródłowy plik.cpp na plik wykonywalny plik.exe należy
• Kompilator gcc: Wydać polecenie g++ plik.cpp -o plik.exe
• Środowisko Dev-C++: Przycisnąć kombinację Ctrl+F9.
Zwróćmy uwagę na to, że polecenie wywołujące kompilator C++ nazywa się
g++ a nie gcc. W gruncie rzeczy g++ i gcc to ten sam kompilator; wywołanie go
jako g++ (lub c++) powoduje jedynie automatyczne połączenie kompilowangeo
programu ze standardowymi bibliotekami C++ (zamiast C).
Kompilator gcc posiada mnóstwo opcji. Opis najważniejszych z nich znajduje
się w dodatku A.
1.4.2
Błędy kompilacji
Dość rzadko zdarza się, by już pierwsza wersja programu była całkowicie poprawna. Zazwyczaj kody źródłowe zawierają różnego rodzaju naruszenia składni języka. Typowy błąd to literówka, pominięcie nawiasu lub definicji zmiennej. Dobry
kompilator powinien nie tylko wskazać miejsce wystąpienia błędu, ale także zasugerować sposób usunięcia go. W praktyce bywa z tym różnie, a do prawidłowego
‘rozszyfrowania’ komunikatów kompilatora potrzebna jest wieloletnia wprawa.
Rozpatrzmy prosty przykład. Załóżmy, że w programie 1.1 ze str. 18 zapomniano zakończyć średnikiem instrukcję cout <<... . Po skompilowaniu takiego
błędnego programu w środowisku Dev-C++ uzyskamy efekt jak na Rys. 1.2.
Brązowy pasek i krzyżyk na lewym marginesie wskazują wiersz, w którym kompilator wykrył błąd. Komunikat o błędzie wyświetlany jest na dole w okienku
komunikatów. Zawiera on numer wiersza (tu: 6), nazwę pliku (tu: 1.cpp) oraz
komunikat diagnostyczny (expected ‘;’before ‘\}’token). Gdyby błędów było
więcej, moglibyśmy łatwo dotrzeć do miejsca detekcji każdego z nich – w tym
celu wystarczyłoby kliknąć odpowiedni wiersz w okienku komunikatów.
Miejsce detekcji błędu często różni się od jego rzeczywistej lokalizacji. W naszym przypadku różnica jest niewielka – tylko jeden wiersz – ale czasami może
nawet dojść do sytuacji, gdy kompilator odkryje błąd w innym pliku niż ten,
4 Ta
sama czynność w języku Visual C++ zwie się budowaniem (ang. ‘build’).
20
Rozdział 1. Pierwszy program w C++
Tu brakuje średnika
Tu wykryto błąd
Lista komunikatów kompilatora
Rysunek 1.2: Wygląd okna programu Dev-C++ w przypadku wykrycia przez
kompilator błędu w kodzie źródłowym.
w którym błąd faktycznie popełniono. Sam komunikat również nie jest klarowny
nawet dla osób biegle władających językiem angielskim. Nie narzekajmy jednak
zbytnio: gdyby kompilator mógł sam poprawiać nasze błędy, mógłby też sam
pisać programy, a więc my nie bylibyśmy już do niczego potrzebni.
1.4.3
Uruchamianie programu w środowisku Dev-C++
Aby uruchomić w środowisku Dev-C++ skompilowany program, wystarczy wybrać z menu pozycję Uruchom/Uruchom lub wcisnąć kombinację Ctrl+F10. Jeśli
jednak w ten sposób uruchomimy program 1.1, spotka nas przykra niespodzianka:
Dev-C++ otworzy okienko konsoli, uruchomi w niej nasz program, po czym natychmiast zamknie okno wraz z konsolą! Całość ledwie mignie nam przed oczami.
Aby uchronić okno programu przed natychmiastowym zamknięciem, zazwyczaj
na samym końcu funkcji main umieszcza się instrukcję
system(”pause”);
1.4.4
Struktura prostego programu w C++
Króciutkie (a więc też, niestety, raczej trywialne) programy w C++ mają następującą strukturę:
Wydruk 1.2: Ogólna struktura prostych programów w C++.
#include <iostream>
int main()
{
...
}
przy czym wielokropek należy zastąpić tu instrukcjami programu. Na razie to
nie do końca prawdziwe stwierdzenie proszę przyjąć jak dogmat. Wyjaśnienie roli
i znaczenia poszczególnych konstrukcji użytych w powyższym schemacie zostanie
przedstawione w kolejnych rozdziałach.
Obiekt std::cout i literały
1.5
21
Obiekt std::cout i literały
Nasz pierwszy program (wydruk 1.1 na str. 18) zawiera tylko jedną instrukcję:
std :: cout << ”Pierwszy program w C++” << ”\n”;
Jej znaczenie jest następujące. std::cout oznacza strumień danych związany z bieżącym okienkiem. Do strumienia tego przesyłane są dane przy pomocy operatora
<<. Najpierw przesyłany jest napis Pierwszy program w C++. Następnie do obiektu std::cout trafia kolejny, dość tajemniczy napis \n – jednak tak naprawdę nie
jest to „zwyczajny” napis, lecz specjalny znak sterujący, którego przesłanie na
konsolę powoduje przemieszczenie kursora na początek kolejnego wiersza.
Spójrzmy teraz na tę samą instrukcję z bardziej ogólnego punktu widzenia:
• std:: w nazwie obiektu std::cout oznacza, że mamy do czynienia z obiektem z biblioteki standardowej; std jest tu nazwą przestrzeni nazw, a ::
tzw. operatorem zasięgu. Zapis std::cout oznacza, że używamy obiektu
cout z przestrzeni nazw std.
• Nazwa obiektu cout to skrót od angielskiego „console output” (wyjście na
konsolę).
• Przed pierwszym użyciem strumienia std::cout należy zastosować makrodefinicję #include <iostream>
• „Zwyczajne” napisy w C++ umieszcza się między znakami cudzysłowu.
Dalej takie napisy będę nazywał tekstami.
• Znaki sterujące (zwane też znakami specjalnymi) zapisuje się jako ciąg
dwóch liter, z których pierwsza jest ukośnikiem (\).
• Zapis << oznacza specjalny operator. Służy on m.in. do przesyłania danych
na konsolę.
• Wywołania operatora << można łączyć ze sobą w ciąg; odpowiada to kierowaniu na konsolę kolejnych tekstów lub obiektów.
• Dwa kolejne teksty (ujęte w cudzysłów) można ze sobą łączyć w jeden. Dlatego zamiast std::cout << "Pierwszy program w C++"<< "\n" można napisać
std :: cout << ”Pierwszy program w C++\n”;
• Każda instrukcja (prosta) musi kończyć się średnikiem.
Liczby wyświetla się na konsoli równie łatwo, jak napisy. Jedyna komplikacja
polega na tym, że komputery rozróżniają wiele rodzajów liczb. W szczególności
starannie rozróżniają liczby całkowite od rzeczywistych.
Spójrzmy na wydruk 1.3:
Wydruk 1.3: Program wyświetlający liczby i napisy.
#include <iostream>
int main()
{
std :: cout << ”Mam ” << 40 << ” lat\nLiczba pi = ” << 3.14 << ”...\n”;
}
22
Rozdział 1. Pierwszy program w C++
Program ten wyświetla dwie linijki tekstu:
Mam 40 lat
Liczba pi = 3.14...
Jak widać, obiekt std::cout w taki sam sposób – operatorem << – wyświetla
teksty, liczby całkowite i liczby rzeczywiste (oczywiście te ostatnie zapisujemy
z kropką, a nie przecinkiem dziesiętnym!). Warto też zwrócić uwagę na stosowanie
odstępów w napisach występujących przed lub po liczbach. Liczby wyświetlane
są bowiem bez żadnych odstępów.
1.6
Definiowanie obiektów
Nasze dotychczasowe programy mają pewną ułomność: ograniczają się do wyświetlania liczb lub tekstów umieszczonych dosłownie („literalnie”) w tekście
programu. W fachowym żargonie mówi się, że te programy wyświetlają wartości literałów . Wartość literału jest znana już podczas kompilacji i nie może ulec
zmianie podczas wykonywania programu. Naturalne pytanie brzmi: w jaki sposób zdefiniować obiekty, których wartość mogłaby ulegać zmianie podczas pracy
programu? Odpowiedź przynosi program 1.4:
Wydruk 1.4: Program modyfikujący wartości zmiennych.
1
5
#include <iostream>
#include <string>
int main()
{
std :: cout << ”Jak masz na imie? ”;
std :: string imie;
std :: cin >> imie;
std :: cout << ”ile masz lat? ”;
int wiek;
std :: cin >> wiek;
10
std :: cout << ”Witaj, ” << imie
<< ”! Nie wiedzialem, ze masz ” << wiek << ” lat!\n”;
15
}
Definicja funkcji main składa się z siedmiu instrukcji5 ułożonych w trzech
grupach. W wierszach 6-8 program wczytuje imię użytkownika, w wierszach 1012 prosi go o podanie wieku, po czym w długiej instrukcji zajmującej wiersze
14 i 15 wyświetla komunikat zawierający zarówno imię jak i wiek użytkownika.
Przyjrzyjmy się teraz szczegółowo sposobowi realizacji tych zadań.
Znaczenie wierszy 6, 10 i 14-15 powinno już być jasne: program wyświetla
w nich komunikaty dla użytkownika. Natomiast w wierszach 7 i 11 mamy przykłady zastosowania nowej konstrukcji języka – definicji obiektu. Obiekt definiuje
się w bardzo prosty sposób: najpierw podajemy nazwę klasy obiektu, a po niej –
5 Liczba
instrukcji równa jest liczbie średników.
Definiowanie obiektów
23
nazwę definiowanego obiektu6 . Definicja obiektu jest traktowana jak instrukcja,
dlatego należy zakończyć ją średnikiem. Co więcej, w C++ obowiązuje
Reguła 1.1 Definicje mogą przeplatać się ze zwykłymi instrukcjami
Dlatego obiekty można (i należy!) definiować dopiero tam, gdzie są naprawdę potrzebne. W naszym przykładzie zdefiniowano dwa obiekty: imie, (klasy
std::string) oraz wiek (klasy int). Klasa std::string służy do obsługi wszelkiego rodzaju tekstów, natomiast int to klasa liczb całkowitych (int to skrót od
angielskiego słowa integer oznaczającego właśnie liczbę całkowitą). Po zdefiniowaniu obiekty są gotowe do użycia. W wierszach 8 i 12 zmienia się ich wartości na
te, które zostaną wprowadzone przez użytkownika przy pomocy klawiatury. Służy do tego obiekt std::cin (jego nazwa pochodzi od angielskiego zwrotu console
input, czyli ‘wejście z konsoli’) oraz operator >>.
W przeciwieństwie do wielu innych języków programowania, w C++ nie czyni
się żadnych niejawnych założeń co do klasy niezadeklarownych obiektów. Proszę
zapamiętać następującą zasadę:
Reguła 1.2 Każdy obiekt musi mieć jawnie zadeklarowaną klasę (tj. typ)
Po przeczytaniu powyższej reguły uważnemu Czytelnikowi mogą przyjść do
głowy dwa pytania. Po pierwsze, gdzie w programie 1.4 znajdują się wymagane
przez nią definicje obiektów std::cout i std::cin? Odpowiedź brzmi: za deklarację
tych obiektów odpowiedzialna jest dyrektywa #include <iostream>. Jej działanie
polega na dołączeniu do kodu programu wszystkich deklaracji związanych z działaniem standardowych strumieni wejścia i wyjścia. Bez tej dyrektywy kompilator
nie wiedziałby, jaka jest klasa tych obiektów, a w związku z tym uznałby program
za błędny.
Drugie pytanie brzmi: skoro tak stanowczo wymaga się, by przed użyciem
jakiegokolwiek obiektu zdefiniować jego klasę, jak rozwiązano w C++ zagadnienie definiowania samych klas? W szczególności skąd kompilator „wie”, jakie jest
znaczenie słówek int oraz std::string użytych w programie na oznaczenie klas
obiektów? Otóż kilka klas podstawowych, m.in. int, jest zdefiniowanych w samym
języku C++; wszystkie inne muszą być jawnie zdefiniowane przed pierwszym
użyciem. W szczególności definicja klasy std::string włączana jest do programu
dyrektywą #include <string>.
Odpowiedź na powyższe pytania zawiera jednocześnie wyjaśnienie znaczenia dwóch pierwszych wierszy programu 1.4. Znajdujące się w nich dyrektywy7
#include służą do dosłownego wstawienia w miejscu ich wystąpienia treści plików
o nazwach podanych w nawiasach ostrokątnych. Takie pliki zwie się plikami nagłówkowymi . Użyte w programie pliki iostream i string wchodzą w skład każdej
instalacji kompilatora C++, gdyż stanowią część biblioteki standardowej tego
języka. Dodatkowo każda niestandardowa biblioteka instaluje w kompilatorze dodatkowe pliki nagłówkowe (w moim kompilatorze jest ich – bagatela – około 1270).
Zasadniczo pliki nagłówkowe służą do informowania kompilatora o możliwościach
danej biblioteki (tj. stanowią jej interfejs).
6 Wiele podręczników C++ w użytym tu kontekście zastosowałoby terminy „zmienna” (zamiast „obiekt”) i „typ” (zamiast „klasa”). Ja uważam takie rozróżnianie za zbędne i niepedagogiczne; prowadzi ono bowiem do sytuacji, gdy słowa „obiekt” i „klasa” pojawiają się w połowie
kursu, co sugeruje, że obiekty i klasy to coś niesamowicie skomplikowanego.
7 Dyrektywy zostaną szczegółowo omówione w Rozdziale 10.
24
Rozdział 1. Pierwszy program w C++
Plik nagłówkowe bardzo często same także zawierają dyrektywy #include włączające kolejne pliki nagłówkowe. Wiąże się z tym następująca ciekawostka: dwie
niewinnie wyglądające w programie 1.4 dyrektywy #include w rzeczywistości włączają do jego tekstu zawartość nie dwóch, lecz 98 plików nagłówkowych, a po ich
włączeniu całkowita liczba wierszy programu wynosi nie 17, lecz. . . 23 726 (dane
dla kompilatora gcc 3.4.4 cygming special).
1.7
Identyfikatory, słowa kluczowe i dyrektywy
Definiując obiekty, musimy nadać im nazwy. Jak się wkrótce przekonamy, w C++
można (i zazwyczaj trzeba) definiować też inne „byty”: funkcje, klasy, wyliczenia
itp. Wszystkie one muszą mieć nazwy. W żargonie C/C++ nie używa się jednak
w tym kontekście słowa „nazwa”, lecz „identyfikator”.
Identyfikatory mogą składać się wyłącznie liter, cyfr i znaku podkreślenia (_),
przy czym pierwszym znakiem nie może być cyfra (gdyż cyframi rozpoczynać
się mogą wyłącznie liczby). Ilość znaków w identyfikatorze (we współczesnych
kompilatorach) jest praktycznie nieograniczona. We wszystkich identyfikatorach
litery małe uważa się za różne od wielkich. Poprawnymi (i wzajemnie różnymi)
identyfikatorami są więc np. rozmiar, Rozmiar, rozmiar_tablic, s00, M_PI.
W punkcie 1.6 wspomniałem już, że pewne identyfikatory, np. int, zdefiniowane są w samym języku. Są to tak zwane słowa kluczowe. Słowa kluczowe mają
ściśle określone, niezmienne znaczenie. Nie można więc używać ich jak zwykłych
identyfikatorów (np. jako nazw zmiennych lub typów). W C++ istnieją aż 73
słowa kluczowe (w praktyce używa się ok. 50). W niniejszym podręczniku słowa
kluczowe wyróżniono pogrubieniem.
Oprócz słów kluczowych specjalną rolę w programie odgrywają tzw. dyrektywy preprocesora. Są to specjalne „identyfikatory” poprzedzone znakiem #. Jest
ich w sumie kilkanaście, z czego w praktyce używa się ok. 6. W podręczniku dyrektywy preprocesora również wyróżniam pogrubieniem (tak jak słowa kluczowe).
Regułę 1.2 można teraz rozszerzyć następująco:
Reguła 1.3 Każdy identyfikator, który nie jest słowem kluczowym lub dyrektywą
preprocesora, najpierw musi być zadeklarowany, a dopiero potem może zostać
użyty.
Jak już wiemy, do deklarowania identyfikatorów bibliotecznych służy dyrektywa
#include<...>; z kolei najczęściej spotykaną metodą deklaracji własnych identyfikatorów jest podanie ich definicji (tj., definicji obiektu, funkcji, etc.).
1.8
Zapis programu
Wróćmy jeszcze raz do programu 1.4 (str. 22) i przyjrzyjmy się jego zapisowi.
Mam nadzieję, że Czytelnik zwrócił uwagę na jego czytelność. Aby ją osiągnąć,
zastosowałem następujące zasady:
• Dyrektywy #include rozpoczynają się od pierwszej kolumny i poprzedzają
wszelkie inne fragmenty programu.
• Definicja funkcji rozpoczyna się od pierwszej kolumny, natomiast jej treść
jest (równomiernie) wcięta.
Cztery działania matematyczne i typ double
25
• Klamry otwierająca ({ ) i zamykająca (}) znajdują się w tej samej kolumnie.
• Klamry są jedynymi znakami w swoich wierszach.
• W jednym wierszu znajduje się co najwyżej jedna instrukcja;
• Funkcjonalnie różne fragmenty programu zostały oddzielone od siebie pustymi wierszami.
• Identyfikatorom nadano nazwy odpowiadające ich przeznaczeniu.
• Operatory oddzielono od reszty kodu odstępami.
• Dzięki zastosowaniu w wierszu 15 wcięcia wyrównującego położenie operatorów << nie ma wątpliwości, że jest on kontynuacją wiersza 14.
Podobnych zasad jest dużo więcej. Należy je sobie stopniowo przyswajać i stosować od samego początku nauki. Zabałaganiony program to zaproszenie dla
wszelkiego rodzaju rodzaju błędów – a w konsekwencji strata czasu i pieniędzy.
Reguła 1.4 Programy pisze się dla ludzi a nie dla komputerów.
Znaczenie tej reguły jest take, że tekst programu musi być czytelny przede wszystkim dla człowieka, bo tylko człowiek może zrozumieć sens programu.
1.9
Cztery działania matematyczne i typ double
Pierwsze komputery i pierwsze programy służyły wyłącznie do wykonywania nużących obliczeń matematycznych. W gruncie rzeczy procesory komputerów wciąż
potrafią niewiele więcej niż dodawać, odejmować, mnożyć i dzielić. Sposób wykonywania tych operacji w C++ przedstawia wydruk 1.5.
Wydruk 1.5: Cztery operacje arytmetyczne w akcji.
3
7
12
int main()
{
double x, y;
std :: cout << ”Podaj dwie liczby, x i y.\nx = ”;
std :: cin >> x;
std :: cout << ”y = ”;
std :: cin >> y;
std :: cout << x << ” + ” << y << ” = ” << x + y << ”\n”;
std :: cout << x << ” − ” << y << ” = ” << x − y << ”\n”;
std :: cout << x << ” ∗ ” << y << ” = ” << x ∗ y << ”\n”;
std :: cout << x << ” / ” << y << ” = ” << x / y << ”\n”;
std :: cout << ”2(” << x << ” + ” << y << ”) = ” << 2∗(x + y) << ”\n”;
}
Podaj
x = 3
y = 4
3 + 4
3 - 4
3 * 4
3 / 4
2(3 +
dwie liczby, x i y.
= 7
= -1
= 12
= 0.75
4) = 14
26
Rozdział 1. Pierwszy program w C++
Jak widać, dodawanie, odejmowanie, mnożenie i dzielenie wykonuje się za pomocą znanych ze szkoły operatorów +, -, * oraz /. Dodatkowo w zapisie działań
matematycznych można używać nawiasów. Jak zwykle, mnożenie i dzielenie mają pierwszeństwo nad dodawaniem i odejmowaniem (czyli wartością wyrażenia
2+2*2 jest 6 a nie 8).
W piątym wierszu programu 1.5 pojawiło się nowe słowo kluczowe: double.
Służy ono do definiowania liczb rzeczywistych.
1.10
Jeszcze więcej matematyki
Operacje matematyczne to wdzięczny motyw nauki programowania, istnieje bowiem ogromna liczba gotowych i łatwych w użyciu bibliotek matematycznych.
Ot, choćby wchodząca w skład biblioteki standardowej C++ biblioteka cmath.
Jak już Czytelnik zapewne się domyśla, włącza się ją do programu makrodefinicją
#include<cmath>. Udostępnia ona najbardziej podstawowe i najczęściej używane
stałe i funkcje matematyczne znane ze szkoły lub studiów. Podstawowe stałe
matematyczne przedstawiam w tabeli 1.1, a wybrane funkcje – w tabeli 1.2.
Stałe zdefiniowane w bibliotece cmath można by obliczyć przy pomocy funkcji
z tablicy 1.2, ale to kosztowałoby dużo więcej czasu niż zastosowanie gotowych
wartości. Z kolei wśród funkcji warto zwrócić uwagę na kilka „smaczków”.
• Powszechny wśród początkujących błąd polega na stosowaniu abs do liczb
zmiennopozycyjnych. Do takich liczb należy używać fabs lub std::abs.
• Kłopoty z funkcjami abs i fabs wynikają stąd, że C++ „odziedziczył” je
z języka C. W C++ zastąpiono je jedną funkcją std::abs. Wiele osób zapomina jednak pisać „przedrostek” std:: przed abs.
• Podobny problem jest z funkcją liczącą potęgi. Funkcja pow została przejęta
z języka C i oblicza potęgi poprzez złożenie funkcji wykładniczej i logarytmicznej (tj. zawsze korzysta ze wzoru xy = exp(y ln x)). W C++ dodano
bardziej efektywną funkcję std::pow, która potęgi całkowite (np. x4 ) oblicza
wyłącznie przy pomocy mnożeń i dodawań, stosując przy tym bardzo małą
(często najmniejszą możliwą) liczbę tych operacji.
• Funkcja atan2 przydaje się w sytuacji, gdy mamy współrzędne punktu
i chcemy obliczyć kąt, jaki tworzy on z osią „x”.
Proszę się nie obawiać – w dalszej części nie będę korzystał z funkcji Bessla;
należy za to zwrócić uwagę na to, że nawet tak skomplikowane funkcje mamy „na
wyciągnięcie ręki”.
nazwa
M_E
M_LN2
M_LN10
M_LOG10E
M_LOG2E
wartość
exp(1)
ln 2
ln 10
log10 e
log2 e
nazwa
M_PI
M_PI_2
M_PI_4
M_2_SQRTPI
wartość
π
π/2
π/4
√
2/ π
nazwa
M_1_PI
M_2_PI
M_SQRT2
M_SQRT1_2
wartość
1/π
2/π
√
2√
1/ 2
Tabela 1.1: Stałe matematyczne z biblioteki standardowej C i C++ .
Jeszcze więcej matematyki
nazwa
abs
fabs
std::abs
sin, cos, tan, ctan
asin, acos, atan, actan
atan2
exp, pow
std::pow
log, log10, log2
sqrt, cbrt, hypot,
expm1, log1p
sinh, cosh, tanh
asinh, acosh, atanh
erf, erfc, tgamma
j0, j1, jn, y0, y1, yn
27
opis
wartość bezwzględna liczby całkowitej (tj. |n|)
wartość bezwzględna liczby rzeczywistej (tj. |x|)
wartość bezwzględna dowolnej liczby
funkcje trygonometryczne
odwrotne funkcje trygonometryczne
dwuargumentowa wersja funkcji atan
funkcja wykładnicza i potęgowa
„inteligentna” funkcja potęgowa
logarytmy (naturalny, dziesiętny i o podstawie 2)
√ √
, 3 i długość przeciwprostokątnej trójkata
odpowiednio exp(x) − 1 oraz ln(1 + x)
funkcje hiperboliczne
odwrotne funkcje hiperboliczne
funkcje specjalne
funkcje Bessla pierwszego i drugiego rodzaju
Tabela 1.2: Wybrane funkcje udostępniane w standardowej bibliotece cmath.
Biblioteka cmath posłuży do wprowadzenia kolejnych koncepcji: inicjalizacji
obiektów, wywoływania funkcji bibliotecznych oraz dodawania komentarzy. Zagadnienia te ilustruję w programie 1.6. Jego celem jest pobranie od użytkownika
informacji o długości
boków trójkąta (a, b, c) i obliczenie pola trójkąta ze wzoru
√
Herona: S = p (p − a)(p − b)(p − c), gdzie p = (a + b + c)/2.
Definicja funkcji main rozpoczyna się definicją trzech zmiennych rzeczywistych o nazwach a, b, i c. Zwróćmy uwagę na to, że w jednej instrukcji można
zdefiniować kilka obiektów tego samego typu (tej samej klasy), oddzielając ich
nazwy przecinkami. Następnie w wierszach 10-15 zmiennym a, b, i c przypisywane
są wartości wprowadzane z klawiatury przez użytkownika. Wiersz 16 przedstawia
konstrukcję bardzo typową dla C++. Otóż definiuje się tu zmienną p klasy double
(a więc p odpowiada liczbie rzeczywistej), po czym w tej samej instrukcji nadaje
się jej wartość początkową.
Reguła 1.5 Obiekty definiuj dopiero wtedy, gdy są naprawdę potrzebne. Jeśli jest
to możliwe, od razu określ ich wartość początkową.
W wierszu 17 mamy podobną sytuację – definiujemy obiekt pole i od razu przypisujemy mu wartość początkową. W tym celu posługujemy się funkcją biblioteczną
sqrt, która – zgodnie z tabelą 1.2 – oblicza pierwiastek kwadratowy swojego argumentu. Żeby móc użyć tej funkcji, w wierszu 4 dołączyłem do programu deklaracje
wszystkich funkcji standardowej biblioteki matematycznej.
Dodatkowo w powyższym programie w wierszach 1 i 8 zastosowałem (skromne) komentarze. Komentarze w C++ rozpoczynają się parą ukośników (//) i kończą wraz z końcem wiersza. Są zupełnie pomijane przez kompilator, a służą opisowi programu (por. Reguła 1.4 ze str. 25). W C++ można też stosować (choć
nie jest to zalecane) komentarze jak w języku C: rozpoczyna się je znakami /*,
a kończy parą */.
28
Rozdział 1. Pierwszy program w C++
Wydruk 1.6: Zastosowanie funkcji sqrt z biblioteki cmath.
1
// program oblicza pole trójkąta ze wzoru Herona. (c) Z. Koza, 2005
#include <iostream>
#include <cmath>
5
int main()
{
double a, b, c; // boki trójkąta
std :: cout << ”podaj dlugosci bokow a, b i c trojkata:\na = ”;
std :: cin >> a;
std :: cout << ”b = ”;
std :: cin >> b;
std :: cout << ”c = ”;
std :: cin >> c;
double p = (a + b + c)∗0.5;
double pole = sqrt(p∗(p − a)∗(p − b)∗(p − c));
std :: cout << ”pole trojkata o podanych dlugosciach bokow wynosi ”
<< pole << ”\n”;
10
15
20
}
podaj dlugosci bokow a, b i c trojkata:
a = 3
b = 4
c = 5
pole trojkata o podanych dlugosciach bokow wynosi 6
1.11
Upraszczanie zapisu obiektów i funkcji biblioteki standardowej
Wiele osób narzeka, że w zasadzie nazwy wszystkich obiektów, klas i funkcji
biblioteki standardowej C++ należy poprzedzać „przedrostkiem” std::. Ja ten
przedrostek lubię, bo informuje mnie o źródle pochodzenia danej nazwy. Istnieje
prosty sposób, by pozbyć się konieczności używania std::. W tym celu wystarczy na początku programu umieścić instrukcję using namespace std;. Ilustruje to
program8 1.7.
1.12
Źródła informacji
Podczas pisania programu niezbędny jest dostęp do dokładnego opisu języka
i używanych bibliotek. Najlepszy opis języka C++ znajdziemy w książce Stroustrupa [1]. Opis biblioteki standardowej języka C (która wchodzi w skład biblioteki standardowej) dostępny jest powszechnie w wielu formatch (m.in. man
pages i info w Linuksie, HLP w Windows). Plik w formacie HLP można „podpiąć” do środowiska Dev-C++. W tym celu wystarczy zainstalować pakiet „GNU
8 Przy okazji na wydruku 1.7 zapoznajemy się z dwoma nowymi słowami kluczowymi: using
oraz namespace.
Q&A
29
Wydruk 1.7: Zastosowanie instrukcji using namespace do uproszczenia zapisu
1
#include <iostream>
using namespace std;
5
int main()
{
cout << ”jestem krotki\n”; // teraz nie muszę pisać std:: cout
}
C Library Reference”. Opis biblioteki standardowej C++ znajduje się w książce
Stroustrupa [1], natomiast nie ma go jeszcze w postaci elektronicznej. Bardzo
dobry opis biblioteki STL (która wchodzi w skład biblioteki standardowej C++)
można znaleźć na stronie http://www.sgi.com. W przypadku bibliotek niestandardowych ich opis (tj. dokumentacja) musi stanowić część samej biblioteki.
1.13
Q&A
Jaka jest różnica między zmienną a obiektem?
W wielu podręcznikach rozróżnia się słowa ‘zmienna’ i ‘obiekt’. Wg. tej konwencji zmienna ma typ (klasę) zdefiniowaną w samym języku, (np. double, int),
a obiekt ma typ zdefiniowany w bibliotece (np. standardowej) lub przez programistę. W sumie różnica jest wyłącznie terminologiczna.
Czy można ignorować ostrzeżenia generowane przez kompilator?
Nie, nie i jeszcze raz nie! Kompilator ostrzega przed konstrukcjami zgodnymi
z definicją języka (a więc formalnie poprawnymi), jednak z reguły inaczej interpretowanymi przez kompilator niż przez człowieka. Każdą instrukcję można
i należy tak napisać, by jej kompilacja nie generowała ostrzeżeń.
Podobno w kompilatorze gcc istnieje kilka poziomów ostrzeżeń?
Tak (i dotyczy to każdego kompilatora C/C++). Domyślnie kompilator gcc praktycznie nie generuje żadnych ostrzeżeń. Pierwszy poziom uzyskuje się opcją -Wall,
a drugi – opcją -Wextra (lub równoważną jej opcją -W). Niektórzy dorzucają
do tego -pedantic. Poziom trzeci uzyskuje się po gruntownym przeanalizowaniu
podręcznika kompilatora i wielu eksperymentach z kilkudziesięcioma opisanymi
w nim dodatkowymi opcjami (por. zadanie 1 oraz s. 249).
Czy w tekstach programów można używać polskich liter?
Polskie litery mogą pojawić się wyłącznie w napisach lub komentarzach. Nie
polecam używania ich w programach uruchamianych w konsoli Windows, gdyż
zamiast liter na ekranie zobaczy się „krzaki”. Z kolei używanie polskich liter
w komentarzach prowadzi do kłopotów (znów te „krzaki”!) przy przenoszeniu
programów między systemami operacyjnymi.
Dlaczego w innych podręcznikach C++ zamiast #include<iostream> używa się #include<iostream.h>?
Bo to są stare podręczniki opisujące standard C++ z zeszłego tysiąclecia. W nowym standardzie nazwy plików nagłówkowych biblioteki standardowej C++ nie
zawierają rozszerzenia .h.
30
Rozdział 1. Pierwszy program w C++
Co oznacza litera ‘c’ w nazwie biblioteki cmath?
Oznacza, że biblioteka ta rozszerza bibliotekę standardową języka C. Oryginalna nazwa tej rozszerzanej biblioteki nie zawiera początkowego ‘c’ i kończy się
rozszerzeniem .h (w tym wypadku jest to math.h). W ten sposób obie biblioteki
mogą funkcjonować bezkolizyjnie. Proszę zwrócić uwagę na to, że użyłem słowa
rozszerza – biblioteka cmath z reguły zawiera dyrektywę #include<math.h> oraz
dużo dodatkowych deklaracji.
1.14
1.
2.
3.
4.
5.
Quiz
W jaki sposób w Twoim kompilatorze kompiluje się programy?
Czym różni się kompilacja od łączenia programów?
Jakie dwa znaczenia ma słowo kompilacja w kontekście informatycznym?
Dlaczego należy posługiwać się możliwie jak najnowszą wersją kompilatora?
W jaki sposób w Twoim środowisku programistycznym ustala się opcje
kompilatora?
1.15
Problemy
1. W zależności od używanego środowiska programistycznego ustaw jego parametry tak, aby Twój kompilator domyślnie generował rozsądną liczbę
komunikatów diagnostycznych i traktował ostrzeżenia jak błędy9 .
[Dev-C++] Wybierz z menu programu Dev-C++ opcję Narzędzia / Opcje
kompilatora i na zakładce kompilator w polu Dodaj te polecenia do wiersza
poleceń kompilatora umieść -W -Wall -Weffc++ -Wfloat-equal -Werror.
[Linux/Unix] Jeżeli programy kompilujesz bezpośrednio poleceniem g++,
utwórz do niego alias tak, by było rozwijane do
g++ -W -Wall -Weffc++ -Wfloat-equal -Werror.
[MS Visual Studio] Ustaw „warning level” na 3.
2. Napisz, skompiluj i uruchom program, który wczytuje liczbę i wyświetla jej
odwrotność.
3. Napisz, skompiluj i uruchom program, który wczytuje współrzędne punktu
(x i y) w układzie kartezjańskim i oblicza jego odległość od środka układu
współrzędnych oraz kąt, jaki tworzy on z osią „x”.
4. Napisz, skompiluj i uruchom program, który wczytuje wartości współczyn2
ników trójmianu
√ kwadratowego ax + bx + c i wyznacza jego pierwiastki
(x1,2 = (−b ± ∆)/2a, gdzie ∆ = b2 − 4ac).
5. Przetestuj poprzedni program dla różnych wartości parametrów, włączając
w to parametry, dla których pierwiastki nie istnieją.
6. Odnajdź w swojej instalacji kompilatora plik math.h, a w nim odszukaj
definicję stałej M_PI.
9 Doświadczeni
programiści nie używają opcji -Werror, gdyż mają ją w głowie.
Rozdział 2
Wyrażenia i instrukcje
Instrukcje sterujące. Pętle. Typy wbudowane. Obiekty stałe. Strumienie, napisy, wektory
i słowniki. Obiekty lokalne i globalne. Operatory. Wyrażenia i instrukcje.
Programy przedstawione w tym rozdziale wciąż będą się ograniczać do jednej
funkcji main, ale zakres wyłożonego tu materiału pozwoli nam pisać całkiem już
skomplikowany kod o właściwościach, które trudno byłoby osiągnąć w takich
językach, jak FORTRAN, C czy Pascal. A to dopiero drugi rozdział!
2.1
2.1.1
Instrukcje sterujące
Instrukcja if... else...
Zamieszczony w poprzednim rozdziale program 1.6 (str. 27) ma pewną wadę:
przed przystąpieniem do obliczeń nie sprawdza, czy trójkąt o bokach podanych
przez użytkownika w ogóle istnieje. Wydruk 2.1 ilustruje, w jaki sposób można
do programu 1.6 dodać odpowiedni test sensowności danych.
W wierszu 15 dodano instrukcję sterującą if. Po słowie kluczowym if umieszczamy w nawiasach okrągłych specjalny test z reguły zawierający operatory relacyjne (zestawienie wszystkich takich operatorów zawiera tabela 2.1). W przypadku programu 2.1 testowane wyrażenie to znany ze szkoły warunek trójkąta.
Instrukcja występująca bezpośrednio za testem wykonywana jest wtedy i tylko
wtedy, gdy testowany warunek jest spełniony. W naszym przypadku nie jest to
zwykła pojedyncza instrukcja, lecz ich ciąg ujęty w nawiasy klamrowe (od wiersza
16 do 21). Taki ujęty w klamry zestaw instrukcji zwany jest instrukcją blokową.
Po tej instrukcji może (ale nie musi) pojawić się słowo kluczowe else i kolejna
instrukcja. Instrukcja następująca po else wykonywana jest wtedy i tylko wtedy,
gdy warunek testowany w instrukcji if nie jest spełniony.
Oczywiście każda z instrukcji występujących po if lub else sama może być
instrukcją warunkową. Typowy przykład przedstawia wydruk 2.2. Złożone instrukcje warunkowe są mało czytelne i podatne na błędy. Dlatego należy ich
unikać.
W programie 2.1 warto zwrócić jeszcze uwagę na dwie nowinki. Pierwsza
występuje w wierszu 15 i jest nią operator and, który oznacza koniunkcję (iloczyn) logiczny. Wyrażenie warunek1 and warunek2 jest prawdziwe wtedy i tylko
31
32
Rozdział 2. Wyrażenia i instrukcje
Wydruk 2.1: Przykład zastosowania instrukcji if
1
5
#include <iostream>
#include <cmath>
int main()
{
double a, b, c; // boki trójkąta
10
15
20
}
std :: cout << ”podaj dlugosci bokow a, b i c trojkata:\na = ”;
std :: cin >> a;
std :: cout << ”b = ”;
std :: cin >> b;
std :: cout << ”c = ”;
std :: cin >> c;
// boki trójkąta muszą spelniać 3 nierówności trójkąta :
if ( (a + b > c) and (a + c > b) and (b + c > a) )
{
double p = (a + b + c)∗0.5;
double pole = std::sqrt(p∗(p − a)∗(p − b)∗(p − c));
std :: cout << ”pole trojkata o podanych dlugosciach bokow wynosi ”
<< pole << ”\n”;
}
else
std :: cerr << ”∗∗∗ Trojkat o podanych dlugosciach bokow nie istnieje ∗∗∗\n”;
Wydruk 2.2: Złożony ciąg instrukcji if ...else
...
if (z > 0)
sign = 1;
else if (z == 0)
sign = 0;
else
sign = −1;
...
Operator
Przykład użycia
<
<=
==
>=
>
!=
if(x
if(x
if(x
if(x
if(x
if(x
< y)
<= y)
== y)
>= y)
> y)
!= y)
Znaczenie
mniejszy niż
mniejszy niż lub równy
równy
większy niż lub równy
większy niż
różny od
Tabela 2.1: Operatory relacyjne (porównawcze) w C++.
Pętle
33
wtedy, gdy prawdziwe są jego oba argumenty. Alternatywny (i zdecydowanie bardziej popularny, gdyż odziedziczony z języka C) zapis operatora and to && (np.
if(x >0 && x < 10). . .
Druga nowinka występuje w wierszu 23, w którym zastosowano strumień
std::cerr do natychmiastowego wyświetlenia komunikatu diagnostycznego na
ekranie. W przeciwieństwie do strumienia std::cout, strumień std::cerr nie jest
optymalizowany pod kątem efektywności i zapisywane w nim znaki są natychmiast wyświetlane na ekranie.
2.2
2.2.1
Pętle
Pętla for
Wyobraźmy sobie następujący problem: co miesiąc do funduszu emerytalnego
wpłacamy x = 100 złotych składki. Fundusz potrąca z niej y = 10%, a resztę
inwestuje, osiągając stały dochód z = 5% w skali roku. Jaką kwotą będziemy
dysponować w momencie przejścia na emeryturę po n = 35 latach pracy?
Prosty program rozwiązujący ten problem przedstawiam na wydruku 2.3.
Wydruk 2.3: Przykład zastosowania instrukcji for.
1
5
#include <iostream>
int main()
{
double skladka = 100.0; // 100 zlotych
double prowizja = 0.1;
// 10% prowizji od kazdej skladki
double rentownosc = 0.05; // 5% rentownosci inwestycji (w skali roku)
int ile lat = 35;
double kapital = 0.0;
for (int i = 0; i < 12∗ ile lat ; i++)
{
kapital ∗= (1.0 + rentownosc/12.0);
kapital += skladka ∗ (1.0 − prowizja);
}
std :: cout << ”suma wplaconych skladek: ” << skladka ∗ 12 ∗ ile lat;
std :: cout << ”\nkapital koncowy = ” << kapital << ”\n”;
10
15
}
Wiersze 5-10 poświęcone są definicji zmiennych. Warto zwrócić uwagę na to,
że od samego początku staram się nadawać zmiennym nazwy adekwatne do ich
przeznaczenia i przypisać im wartości początkowe. W wierszu 11 pojawia się kilka
nowych konstrukcji, wśród których wyróżnia się instrukcja for. Służy ona do
wielokrotnego powtarzania tych samych instrukcji i oprócz słowa kluczowego for
składa się z dwóch części: preambuły (ujętej w nawiasy okrągłe) i wielokrotnie
wykonywanej instrukcji, przy czym sama preambuła składa się z trzech części
oddzielonych średnikami. Ilustruje to następujący schemat:
for (instr. inicjująca; warunek kontynuacji; instr. kończąca krok)
INSTRUKCJA;
34
Rozdział 2. Wyrażenia i instrukcje
Znaczenie poszczególnych składników instrukcji for jest następujące.
• Instrukcja inicjująca. Dowolna instrukcja służąca do rozruchu pętli. Wykonywana jest dokładnie raz. W praktyce umieszcza się w niej definicje
zmiennej sterującej działaniem pętli wraz z inicjatorem. Czasami pomija
się część definicyjną, a pozostawia inicjator. Czasami widuje się tu definicje
kilku zmiennych sterujących (tego samego typu) z inicjatorami. Wg. aktualnego standardu C++ wszelkie zmienne zdefiniowane w instrukcji inicjującej
są dostępne wyłącznie wewnątrz pętli. W przypadku programu 2.3 oznacza to, że zmiennej i można używać wyłącznie w wierszach 11-15. Starsze
kompilatory (np. MS Visual C++ 6.0) zakładały, że zmienne definiowane
w instrukcji inicjującej są dostępne także za pętlą.
• Warunek kontynuowania pętli. Absolutnie dowolne wyrażenie (nie instrukcja!), którego wartość można przypisać zmiennej typu bool. Wartość
tego wyrażenia obliczana jest każdorazowo przed wykonaniem głównej instrukcji pętli. Jeżeli wartość ta równa jest true, nastąpi wywołanie INSTRUKCJI, a po niej instrukcji kończącej krok. W przeciwnym wypadku
(gdy warunek kontynuowania pętli nie jest spełniony), pętla jest przerywana
i sterowanie programu przechodzi do następnej instrukcji za pętlą for.
• Instrukcja kończąca krok. Instrukcja, która wykonywana jest zawsze
po właściwej instrukcji pętli. Najczęściej modyfikuje zmienne zdefiniowane
w instrukcji inicjującej.
• INSTRUKCJA. Wielokrotnie wykonywana instrukcja (poniżej zwana instrukcją główną). Najczęściej jest to instrukcja blokowa (por. str. 31).
W naszym przykładowym programie 2.3 instrukcja for steruje wykonaniem
instrukcji blokowej zapisanej w wierszach 12-15. Ta główna instrukcja pętli składa
się z dwóch kroków: w wierszu 13 powiększamy kapitał o zysk z inwestycji (w stosunku miesięcznym, stąd dzielenie przez 12), a w wierszu 14 dodajemy kolejną
składkę (pomniejszoną o prowizję towarzystwa, 10%). Aby obliczyć wzrost kapitału w ciągu miesiąca, należy go pomnożyć przez 1 + rentownosc/12. Służy do
tego operator *=. Analogicznie, aby uwzględnić wzrost kapitału na skutek wpłacenia kolejnej składki, należy powiększyć go o skladka * (1 - prowizja). Operacji
tej odpowiada operator +=. Szerzej o operatorach += i *= piszę w rozdziale 2.8.
Preambuła instrukcji for ma w programie 2.3 typową postać. Instrukcją inicjującą jest int i = 0. Jej zadaniem jest zdefiniowanie zmiennej sterującej i oraz
nadanie jej wartości początkowej 0. Zmienna ta przechowuje informację o tym,
ile razy wykonywana była główna instrukcja pętli. Każdorazowo przed wykonaniem głównej instrukcji pętli sprawdzany jest warunek jej kontynuacji. W naszym
przypadku brzmi on i < 12 * ile_lat. Uwzględniając, że ile_lat ma wartość 35,
powyższy warunek równoważny jest zapisowi i < 12 * 35. Każdorazowo po wykonaniu głównej instrukcji wykonywana jest instrukcja kończąca krok. W naszym
wypadku brzmi ona i++. Zapis ten oznacza rozkaz zwiększenia zmiennej i o jeden. Dzięki temu mamy gwarancję, że pętla zostanie wykonana 12 * 35 razy, czyli
dokładnie tyle, ile jest miesięcy w ciągu 35 lat.
W dobrze skonstruowanej pętli for preambuła powinna zawierać wyłącznie
instrukcje związane ze sterowaniem pętli. Czasem instrukcje sterujące umieszcza
się również w głównej instrukcji pętli, ale powinno się tego unikać (wyjątkiem są
Pętle
35
omówione poniżej, „eleganckie” instrukcje break i continue). Oddzielenie części
sterującej od wykonawczej to kwintesencja prawidłowego zastosowania pętli for.
Dowolna część preambuły instrukcji for może być pusta. Jeżeli pominiemy
warunek kontynuowania pętli, kompilator uzna, że chodzi nam o pętlę nieskończoną (czyli tak, jakbyśmy w miejsce warunku wpisali true).
Reguła 2.1 Preambuła for ( ; ; ) definiuje pętlę nieskończoną.
2.2.2
Pętle while i do
Czasami sterowanie pętlą for nie wymaga definiowania specjalnych zmiennych
sterujących. W tych wypadkach zamiast instrukcji for z pustą instrukcją inicjującą i pustą instrukcją kończącą krok stosuje się instrukcję while lub do...while.
Pierwszą z nich stosujemy w wypadku, gdy liczba realizacji instrukcji sterowanej
pętlą może być równa zero; jeśli instrukcja ma być wykonana co najmniej raz,
bardziej poręczna jest instrukcja do...while.
Spostrzeżenie 2.1 Instrukcja
for ( ; warunek; )
INSTRUKCJA;
Jest równoważna instrukcji
while (warunek)
INSTRUKCJA;
// <−− ta instrukcja może nie wywołać się ani razu
Spostrzeżenie 2.2 Ciąg instrukcji
3
INSTRUKCJA;
for ( ; warunek; )
INSTRUKCJA;
// <-- ta sama instrukcja, co przed for
Jest równoważny instrukcji
do
INSTRUKCJA;
while(warunek);
2.2.3
// <−− ta instrukcja wykona się co najmniej raz
Instrukcje break i continue
Wydruk 2.4 przedstawia przykład zastosowania pętli nieskończonej. W programie
tym prosimy użytkownika o hasło1 tak długo, aż wpisze słowo „Gucio”. W tym
momencie instrukcja break przerywa działanie pętli i przenosi sterowanie do wiersza 14.
Oprócz instrukcji break do sterowania pętlą można wykorzystać instrukcję
continue. Jak wskazuje jej nazwa, służy ona do kontynuowania danej pętli. Konkretnie rzecz ujmując, continue powoduje natychmiastowe uznanie bieżącego wywołania zestawu instrukcji sterowanych pętlą za zakończone i przejście do kroku
1 W programie profesjonalnym wpisywane hasło nie byłoby – tak jak tu – wyświetlane na
ekranie.
36
Rozdział 2. Wyrażenia i instrukcje
Wydruk 2.4: Użycie instrukcji break do przerwania nieskończonej pętli for.
1
5
10
15
#include <iostream>
#include <string>
int main()
{
std :: string kod;
for ( ; ; )
{
std :: cout << ”podaj haslo: ”;
std :: cin >> kod;
if (kod == ”Gucio”)
break;
}
std :: cout << ”Witam w systemie!\n”;
}
kończącego krok. Użycie continue pozwala uniknąć stosowania rozbudowanych instrukcji if...else. Przykład zastosowania tej instrukcji przedstawiam w dalszej
części podręcznika (np. na wydruku 4.5 ze str. 93).
Oczywiście wewnątrz pętli można jako instrukcje stosować („zagnieżdżać”)
inne pętle. W tym przypadku obowiązuje następująca reguła.
Reguła 2.2 Instrukcja break użyta w pętli zagnieżdżonej w innej pętli przerywa
działanie tylko jednej z nich (tj. pętli wewnętrznej).
2.3
Typy wbudowane
W poprzednim rozdziale pobieżnie zapoznaliśmy się z dwoma typami (czyli klasami) wbudowanymi – int i double. Oba te typy należą do kategorii tzw. typów
prostych, czyli typów (= klas) zapisywanych w formie słów kluczowych i reprezentujących elementarne „atomy”, z których budowane są typy złożone.
Typy proste dzielą się na całkowite i zmiennopozycyjne. Typy całkowite odpowiadają liczbom całkowitym, a zmiennopozycyjne – rzeczywistym.
2.3.1
Typy całkowite
Jak pamiętamy z lekcji matematyki, nie istnieje najmniejsza ani największa liczba
całkowita. Oczywiście komputery nie są w stanie zapisać w swojej pamięci liczb
dowolnie dużych czy dowolnie małych. To oznacza, że komputery nie używają
do obliczeń tych samych liczb, które poznajemy w szkole. Komputery używają okrojonych liczb całkowitych, tzn. takich podzbiorów liczb całkowitych (lub
naturalnych), w których istnieją zarówno wartość najmniejsza jak i największa.
W C++ zdefiniowano cztery rodzaje takich namiastek liczb naturalnych i tyleż namiastek liczb całkowitych. Ich właściwości prezentuje Tabela 2.2. Wartości
minimalne dla poszczególnych typów podane w tej w tabeli mają charakter orientacyjny: standard nie nakłada (niemal) żadnych ograniczeń na te wartości i w
37
Typy wbudowane
Typ
long int
int
short
signed char
unsigned long int
unsigned int
unsigned short
unsigned char
Wartość minimalna
−2 147 483 648
−2 147 483 648
−32 768
−128
0
0
0
0
Wartość maksymalna
2 147 483 647
2 147 483 647
32 767
127
4 294 967 295
4 294 967 295
65 535
255
Dodatkowe typy dostępne w kompilatorach gcc:
long long
unsigned long long
−9 223 372 036 854 775 808
0
9 223 372 036 854 775 807
18 446 744 073 709 551 615
Tabela 2.2: Wbudowane typy całkowite C++. Wartości minimalne i maksymalne dla różnych kompilatorów i platform mogą być różne. Tu podałem wartości
używane przez 32-bitowy kompilator MinGW g++ 3.4.2.
różnych kompilatorach mogą mieć one inne wartości. Spowodowane to zostało
troską o wydajność – przyjęto bowiem, że typ int powinien odpowiadać najbardziej wydajnemu typowi całkowitemu na danej platformie. Dlatego w 16-bitowych
komputerach klasy PC (lata 80-te ubiegłego wieku) największą wartością liczb
typu int było zaledwie 32 767; obecnie jest to 2 147 483 647 a w przyszłości może
to być 9 223 372 036 854 775 807.
Jak widać w tabeli 2.2, obiekty części typów całkowitych mogą przyjmować
wyłącznie wartości dodatnie lub zero. Typy te zwie się typami bez znaku. Nazwy
tych typów zawierają modyfikator unsigned. Pozostałe typy mogą przyjmować
wartości ujemny lub dodatnie i należą do typów ze znakiem.
Standard nie określa, co się stanie, gdy wynik działania matematycznego przekroczy dozwolony zakres wartości w danym typie. W praktyce komputerowe typy
całkowite mają strukturę pierścienia: zwiększenie o jeden liczby największej daje
w wyniku liczbę najmniejszą. I odwrotnie: zmniejszenie najmniejszej możliwej
liczby o jeden daje w wyniku liczbę największą.
Spostrzeżenie 2.3 Suma dwóch dodatnich liczb typu int może być ujemna. Podobnie suma dwóch liczb ujemnych może być dodatnia, a ich iloczyn ujemny.
Spostrzeżenie 2.4 Suma liczb dowolnego typu całkowitego może być mniejsza
od każdego ze składników.
O powyższych właściwościach „komputerowych” liczb całkowitych należy zawsze pamiętać podczas pisania programów. Wartości minimalne i maksymalne
typu int są obecnie dość duże, jednak bez trudu można natrafić na sytuację,
w której wartości wyrażeń przekroczą dopuszczalny zakres. Należy umieć przewidywać możliwość wystąpienia takiej sytuacji i odpowiednio dostosować do niej
program.
Oprócz typów przedstawionych w tabeli 2.2, wiele kompilatorów standardowo rozpoznaje dodatkowe typy. Ich nazwy z reguły kończą się znakami _t. Do
tej kategorii należą m.in. size_t (typ wartości operatora sizeof), time_t (typ
38
Rozdział 2. Wyrażenia i instrukcje
używany przez funkcję time) czy int32_t (typ całkowity ze znakiem zajmujący
dokładnie 32 bity). Tak naprawdę wszystkie one równoważne są jakiemuś typowi
wbudowanemu z tabeli 2.2.
Pewną osobliwością wśród typów całkowitych jest typ char i jego typy pochodne: signed char i unsigned char. Można go używać jak zwykłych liczb całkowitych o mocno ograniczonym zakresie, jednak przy wyświetlaniu obiektów tego
typu na ekranie pojawiają się nie liczby, lecz odpowiadające im znaki (z tego
powodu obiekty tych klas zwie się znakami ). Ilustruje to następujący fragment
programu:
Wydruk 2.5: Wyświetlanie obiektów różnych typów całkowitych.
int i = 100;
short s = 100;
char c = 100;
std :: cout << i << ” ” << s << ” ” << c << ”\n”;
100 100 d
Wysłanie do strumienia std::cout obiektów typu int i short powoduje wyświetlenie ich wartości, natomiast wysłanie obiektu typu char powoduje wyświetlenie
znaku o kodzie ASCII równym wartości liczby (tu: 100).
Oto kilka rad praktycznych dotyczących typów całkowitych:
• W normalnych warunkach do celów obliczeniowych należy wykorzystywać
wyłącznie typ int.
• Do przechowywania ogromnych ilości niewielkich liczb można stosować typy
short lub char.
• Typy bez znaku należy używać wyłącznie do manipulowania bitami.
• Zasadniczo typ char należy traktować jak znak a nie liczbę.
• Typ long long jest niestandardowy, a operacje na nim są bardziej czasochłonne niż operacje na typie int.
2.3.2
Typy zmiennopozycyjne
Jak uczy matematyka, między dowolnymi (różnymi) liczbami rzeczywistymi istnieje nieskończenie wiele liczb rzeczywistych. Skoro jednak w komputerach można
zapisać tylko skończoną ilość różnych liczb, nie mogą one przechowywać dowolnych liczb rzeczywistych. Dlatego komputery przechowują przybliżone wartości
liczb rzeczywistych. Służą do tego specjalne typy zwane typami zmiennopozycyjnymi .
W języku C++ istnieją trzy wbudowane typy zmiennopozycyjne – double,
long double i float. Ich podstawowe właściwości (dla kompilatora CygWin gcc
3.4.4) przedstawia tabela 2.3. Pierwsza kolumna przedstawia wartość największą
w danym typie. Druga zawiera wartość tzw. parametru epsilon (), który określa
względną dokładność, z jaką w danym typie przybliżane są liczby rzeczywiste: jeżeli w którymś z typów zmiennopozycyjnych zechcemy zapisać liczbę rzeczywistą
x, to popełnimy błąd2 mniejszy niż x. Trzecia kolumna przedstawia ilość bajtów
2 Przy
poprawnym zaokrąglaniu błąd jest mniejszy lub równy x/2.
39
Typy wbudowane
Nazwa
Wartość maksymalna
double
long double
float
1.79769 · 10
1.18973 · 104932
3.40282 · 1038
308
Bajty
−16
2.22045 · 10
1.08420 · 10−19
1.19209 · 10−7
8
12
4
Tabela 2.3: Wbudowane typy zmiennopozycyjne C++. Podane tu wartości są
używane przez kompilator CygWin g++ 3.4.4.
zajmowanych przez obiekty danego typu. Parametr ten nie musi odpowiadać dokładności danego typu. Np. w procesorach klasy Pentium (32-bitowych) obiekty
typu long double zajmują 12 bajtów, ale wykorzystywane jest tylko 10 z nich, co
wynika z architektury tych procesorów.
Spostrzeżenie 2.5 Jeżeli w dwóch zmiennych typu double zapiszemy z dokładnością dodatnie liczby rzeczywiste, to błąd ich sumy, iloczynu i ilorazu będzie
rzędu , ale błąd różnicy może równie dobrze wynosić , jak i 1016 · .
Spostrzeżenie 2.6 Suma dwóch dodatnich liczb zmiennopozycyjnych może być
równa jednemu ze składników (tj. x>0, y>0 i (x+y) = y).
Spostrzeżenie 2.7 Nie jest prawdą, że między dwoma różnymi liczbami zmiennopozycyjnymi istnieje inna liczba tego samego typu.
Spostrzeżenie 2.8 Obliczanie tej samej wielkości zmiennopozycyjnej na różne
sposoby daje zazwyczaj różne wyniki. Na przykład (a + b) + c rzadko kiedy ma
tę samą wartość co a + (b + c).
Spostrzeżenie 2.9 Jeżeli pewną wielkość obliczymy na 2 różne sposoby i zapiszemy wyniki w zmiennych zmiennopozycyjnych w1, i w2, to mogą one spełniać
dowolny z warunków w1 < w2, w1 == w2, w1 > w2.
Reguła 2.3 Wystrzegaj się stosowania operatora == do typów zmiennopozycyjnych. Zamiast if(x == y) pisz np. if(fabs(x-y) < eps) lub podobną instrukcję.
Do obliczeń powinno się używać typu double. Typu long double można używać do przechowywania pośrednich wyników obliczeń. Z kolei typ float, niegdyś
podstawowy typ zmiennopozycyjny, dziś ma zastosowanie wyłącznie w sytuacji,
gdy musimy przechować ogromne ilości liczb i chcemy zaoszczędzić nieco pamięci
komputera.
W typach zmiennoprzecinkowych pewne wartości mają znaczenie specjalne
i służą nie do reprezentowania liczb rzeczywistych, lecz do sygnalizacji błędów
(takich jak dzielenie przez zero, logarytmowanie liczb ujemnych itp.). Do tej kategorii należą INF (nieskończoność), INF (−nieskończoność) i NaN (nie-liczba, ang.
Not a Number ). Ich znaczenie ilustruje następujący wiersz kodu, wyświetlający
kolejno nieskończoność (jako 1.INF) i NaN (jako -1.#IND).
std::cout << 1.0/0.0 << "\n" << sqrt(-1) << "\n";
1.#INF
-1.#IND
40
Rozdział 2. Wyrażenia i instrukcje
Sygnały błędów propagują poprzez wyrażenia arytmetyczne aż do wartości końcowej. Jeżeli jeden z argumentów wyrażenia arytmetycznego ma wartość NaN,
to całe wyrażenie ma także wartość NaN. Na przykład wartością wyrażenia
0*sqrt(-1) + 1 jest NaN. Wartością 1.0/0.0 + 1 jest 1.#INF, ale 10/(1.0/0.0) ma
wartość 0. Zwróćmy uwagę, że tego typu sygnalizacji nie posiadają typy całkowite;
dlatego próba obliczenia wyrażenia 1/0 zazwyczaj kończy się padem programu.
2.3.3
Typ logiczny
W języku C++ istnieje tzw. typ logiczny o nazwie bool. Obiekty tego typu mogą przyjmować dwie wartości: false (fałsz) i true (prawda). Obiektom tych typów można przypisywać dowolne liczby (całkowite lub nawet zmiennopozycyjne).
Efektem takiego przypisania jest false, jeśli przypisywana liczba ma wartość zero,
natomiast w każdym innym przypadku wartością obiektu typu bool jest true. Z
kolei jeżeli obiekt typu bool zastosujemy w dowolnym wyrażeniu arytmetycznym,
false zostanie zamienione na 0, a true na 1.
Reguła 2.4 Dla dowolnego wyrażenia arytmetycznego wyr i zmiennej b typu bool
przypisanie b = wyr jest równoważne wyrażeniu b = (wyr != 0);
Spostrzeżenie 2.10 Zapis if(x)... oznacza if(x != 0)...
2.3.4
Zapis literałów całkowitych i zmiennopozycyjnych
Zapis liczb typu int składa się wyłącznie z cyfr, które mogą być poprzedzone
znakiem + lub −. Przykład: 12, -123290. Oczywiście w zapisie liczb nie wolno
używać odstępów.
Liczby można zapisywać w notacji dziesiętnej, ósemkowej lub szesnastkowej.
Pierwszą cyfrą liczby dziesiętnej musi być cyfra różna od zera. W przypadku liczb
ósemkowych pierwszą cyfrą jest zero, a drugą musi być dowolna cyfra z przedziału
0. . . 7. Zapis liczb szesnastkowych rozpoczyna się cyfrą 0, po której musi wystąpić
litera x (lub X), a po niej ciąg cyfr szesnastkowych (0. . . 9, a,b,. . . ,f; A,B,. . . ,F).
Liczbę 30 można więc w C++ zapisać jako 30 (notacja dziesiętna), 036 (notacja
ósemkowa) lub 0x1e (notacja szesnastkowa). Notacji ósemkowej lub dziesiętnej
używa się wyłącznie do manipulowania bitami.
Aby wskazać, że dana liczba całkowita jest obiektem bez znaku (np. typu
unsigned), należy zakończyć ją literą u. Tak więc w instrukcji int x = 1 zmiennej x
przypisuje się wartość typu int, a wartość typu unsigned w instrukcji int x = 1u.
Zapis liczb całkowitych może też kończyć się literą s (co wskazuje na typ short)
lub l (dla typu long). Dlatego literał 0xaul to zapisana w notacji szesnastkowej
liczba typu unsigned long o wartości 10.
Nieco łatwiejsze reguły obowiązują w zapisie liczb zmiennopozycyjnych. Liczby typu double mogą się składać ze znaku (+ lub −), części całkowitej (zawsze
interpretowanej dziesiętnie), kropki dziesiętnej i części ułamkowej, np. 3.14. Dodatkowo można użyć notacji naukowej, w której po literze e lub E podaje się
wykładnik (przy „podstawie” 10). Na przykład liczbę 3.14 można zapisać np.
jako 0.314e1, 3.14e0, 314e-2 lub 314E-2.
Cechą odróżniającą zapis liczb zmiennopozycyjnych od całkowitych jest kropka dziesiętna lub litera e.
Wyrażenia arytmetyczne, promocje i konwersje standardowe
41
Zapis liczb zmiennopozycyjnych może się kończyć literą f (na oznaczenie typu
float), lub l (typ long double). Jako przykład niech posłuży nam instrukcja
long double pi = 3.1415926535897932385L;. Gdybyśmy w zapisie literału opuścili
L, kompilator potraktowałby jego typ jako double, skutkiem czego stracilibyśmy
ok. 4 cyfr znaczących dokładności (wartością pi byłoby 3.1415926535897931160).
Osobną kwestię stanowią obiekty typu char. Zapisuje się je jako litery ujęte
w apostrofy, np. ’a’, ’8’, ’;’ itd. Pewnych wartości w typie char nie można zapisać w postaci znaków. W tych wypadkach zapisuje się je jako ujęte w apostrofy
sekwencje dwóch znaków, z których pierwszym jest ukośnik (\). Do tej kategorii
należą m.in. ’\n’ (znak końca linii), ’\b’ (cofnięcie kursora o jedną pozycję w lewo), ’\r’ (cofnięcie kursora na początek bieżącego wiersza), ’\a’ („dzwonek”),
’\t’ (tabulator) oraz ’\’’ (apostrof).
Oto kilka przykładów:
int n = 100u;
// liczba typu unsigned;
n ˆ= 0xff00;
// liczba typu int w zapisie szesnastkowym;
float f = 1.0e−30f; // liczba typu float
2.4
Wyrażenia arytmetyczne, promocje i konwersje standardowe
Wyrażenia arytmetyczne buduje się głównie z operatorów +, -, * oraz / i %(reszta
z dzielenia liczb całkowitych). Obowiązuje następująca reguła:
Reguła 2.5 Jeżeli oba argumenty wbudowanego operatora arytmetycznego są tego samego typu T, to wynik jest też typu T.
Regułę tę ilustruje następujący przykład:
Wydruk 2.6: Przykład ilustrujący regułę 2.5.
#include <iostream>
int main()
{
double x = 1/3;
std :: cout << ”x = ” << x << ”\n”;
}
x = 0
W powyższym programie zmiennopozycyjnemu obiektowi x przypisuje się wartość
ilorazu 1/3. Jednakże zarówno 1 jak i 3 są tego samego typu (int). Dlatego,
w myśl reguły 2.5, ich iloraz też musi być typu int i wynosi 0. Dlatego ostatecznie
x ma wartość 0 (a nie, jak można by naiwnie oczekiwać, 0.33333...). Kolejny
problem z dzieleniem pojawia się w sytuacji, gdy licznik i mianownik są różnego
znaku. Ponieważ standard (świadomie) nie definiuje, czy w tym wypadku reszta
z dzielenia jest dodatnia czy ujemna, wynik dzielenia liczb o różnych znakach jest
nieokreślony (tj. może być inny w różnych komputerach).
Spostrzeżenie 2.11 Wyrażenia, w których występuje operator dzielenia, sprawiają kłopoty nawet doświadczonym programistom. Zawsze sprawdzaj, czy nieświadomie nie dzielisz dwóch wyrażeń całkowitych.
42
Rozdział 2. Wyrażenia i instrukcje
Nie ma żadnych przeszkód, by argumenty operatorów arytmetycznych były
różnych typów (wbudowanych). Przed obliczeniem wartości operacji matematycznej kompilator uzgadnia typy obu argumentów tak, aby były identyczne. W tym
celu zamienia się typ „mniej pojemny” na typ „bardziej pojemny’. Zakłada się
przy tym, że typy zmiennopozycyjne są „bardziej pojemne” od całkowitych, a typy bez znaku są pojemniejsze od typów ze znakiem o tej samej liczbie bajtów
w reprezentacji maszynowej. Proces ten zwie się konwersją (niejawną). Jeżeli oba
argumenty operatora są typu „mniejszego” od int (np. char, unsigned char, bool
i short), to zamienia się je na int. Proces ten zwie się promocją całkowitą. Jak
widać, podczas opracowywania wyrażeń kompilator stara się nie gubić informacji.
Oczywiście nie zawsze jest to możliwe – na przykład konwersja dużych liczb typu
int na float wiąże się z pewną utratą dokładności.
Spostrzeżenie 2.12 Jeżeli x jest typu int, a y typu unsigned, to każde z wyrażeń
x-y, x+y, x*y i x/y jest typu unsigned i nie może mieć wartości ujemnej.
Ta pozornie niewinna właściwość może mieć poważne następstwa. Ilustruje to
wydruk 2.7.
Wydruk 2.7: Mieszanie typów ze znakiem i bez to recepta na ból głowy.
1
5
10
#include <iostream>
int main()
{
unsigned u duza = 4294967290u;
unsigned u mala = 3u;
int i ujemna = −2;
if (2 − u mala > u duza) // jesli 2 − 4294967290u > 4294967290u
std :: cout << ”niespodzianka\n”;
if (i ujemna > u duza) // jesli −2 > 4294967290u
std :: cout << ”druga niespodzianka\n”;
}
niespodzianka
druga niespodzianka
Autor tego programu mógł myśleć, że wartością wyrażenia 2 - u_mala jest
2-3, czyli -1. Jednak w rzeczywistości typem tego wyrażenia jest unsigned, a jego
wartość to 4294967295. Nieco inny problem pojawia się w wierszu 9, w którym
porównujemy liczbę ze znakiem z liczbą bez znaku. W wypadku mieszanych porównań komputer zamienia typ bez znaku na typ ze znakiem (int), a więc liczbę
294967290u zinterpretuje jako -6, natomiast całe wyrażenie jako if (-2 > -6). . .
Reguła 2.6 Należy unikać mieszania typów ze znakiem i bez znaku.
Niestety, w C++ trudno jest dziś pominąć typy bez znaku, gdyż kilka niezwykle
popularnych funkcji biblioteki standardowej zwraca właśnie tego typu wartości.
Co prawda kompilatory z reguły ostrzegają przed użyciem typów mieszanych
w porównaniach, ale zezwalają na mieszania typów w operacjach arytmetycznych.
W przypadku programu 2.7 kompilator gcc 4.4.2 ostrzeże nas przed konstrukcją
w wierszu 9, ale nie dopatrzy się niczego podejrzanego w wierszu 7.
Tworzenie obiektów stałych
43
Wydruk 2.8: Zastosowanie modyfikatora const.
1
5
#include <iostream>
#include <string>
int main()
{
const std:: string tajny kod = ”Gucio”;
const int maks liczba prob = 5;
bool poprawny kod = false;
for (int i = 0; i < maks liczba prob; i++)
{
std :: string haslo;
std :: cout << ”podaj haslo:”;
std :: cin >> haslo;
poprawny kod = (haslo == tajny kod);
if (poprawny kod)
break;
}
if (poprawny kod)
std :: cout << ”Witam w systemie!\n”;
else
std :: cout << ”Nie znasz hasla?”;
10
15
20
}
2.5
2.5.1
Tworzenie obiektów stałych
Modyfikator const
W programach (lub ich fragmentach) pewne wartości nigdy nie powinny ulec
zmianie. Na przykład znana z lekcji matematyki liczba π ma zawsze tę samą
wartość 3.14 . . . Właściwość tę można zapisać w programie przy pomocy słowa
kluczowego const umieszczanego w definicji obiektu przed nazwą jego typu (lub
po niej). Na przykład liczbę π można zdefiniować instrukcją
const double pi = 3.1415926535897932;
lub
double const pi = 3.1415926535897932;
Obiekty tego typu nazywamy stałymi lub obiektami stałymi . Stałe typu liczbowego lub napisowego zwane są stałymi symbolicznymi. W przypadku definicji
stałych typu matematycznego, użycie stałych symbolicznych wiąże się z wygodą
– łatwiej zapamiętać ich nazwy niż wartości.
Przykład użycia modyfikatora const przedstawia wydruk 2.8 stanowiący modyfikację programu 2.4 ze strony 36. W programie tym zdefiniowano dwie stałe:
tajny_kod oraz maks_liczba_prob o dość oczywistym znaczeniu.
Jaką korzyść daje zastosowanie w tym programie obiektów stałych? Czy wiersz
10 nie mógłby brzmieć po prostu for (int i = 0; i < 5; i++)? Owszem, mógłby. Ale kod, w którym wartości stałych wpisywane są bezpośrednio w instrukcjach
44
Rozdział 2. Wyrażenia i instrukcje
programu (w postaci literałów) byłby niezwykle trudny do pielęgnacji i rozszerzania. Wyobraźmy sobie, że nasz kod stanowi fragment większej biblioteki i że
należy zmienić liczbę dozwolonych prób wpisania hasła z 5 do 6. Programista
mający wykonać takie zadanie musiałby przejrzeć kod całego programu w poszukiwaniu wszystkich miejsc, w których użyto liczby 5, gdyż maksymalna liczba
prób wpisania hasła mogłaby być użyta także w innych miejscach programu. Programista musiałby więc dokładnie wczytać się w kod programu by odróżnić różne
powody użycia liczby 5. W przypadku użycia stylu programowania zastosowanego w programie 2.8, do zmiany wartości dopuszczalnej liczby prób wpisania hasła
wystarczyłaby modyfikacji instrukcji w wierszu 7.
Płyną stąd następujące morały:
Reguła 2.7 Jeżeli jakaś wartość w programie nie jest oczywista lub jeżeli istnieje
choć śladowe prawdopodobieństwo, że jej wartość kiedyś może ulec zmianie, (np.
w komputerach o większej precyzji zmiennopozycyjnej), należy zdefiniować stałą
symboliczną i posługiwać się wyłącznie tą stałą.
Reguła 2.8 Najlepszym sposobem unikania błędów jest ograniczenie liczby okazji do ich popełnienia. Dlatego należy ograniczać do niezbędnego minimum liczbę
(i rodzaj) instrukcji, w których można użyć danego obiektu.
Spostrzeżenie 2.13 Programy, w których systematyczne używa się modyfikatora
const są łatwiejsze w utrzymaniu i z reguły zawierają mniej błędów.
2.6
Popularne typy standardowe
Opisane w rozdziale 2.3 typy wbudowane zaliczają się do tzw. typów niskopoziomowych, czyli typów bezpośrednio odpowiadających możliwościom mikroprocesorów. Stąd właśnie przy posługiwaniu się nimi trzeba ciągle pamiętać, że stanowią
one tylko różne „namiastki” znanych nam ze szkoły zbiorów liczb całkowitych
czy rzeczywistych. Ze względu na potrzebę zapewnienia maksymalnej szybkości
programów, typy te nie oferują żadnej kontroli błędów. Jest to niewygodne, ale
efektywne.
Oprócz typów wbudowanych język C++ oferuje tzw. typy standardowe, czyli
typy (klasy) zdefiniowane w bibliotece standardowej C++. Klasy te należą do
typów wysokopoziomowych. Ich definicja jest dostosowana bardziej do potrzeb
programistów niż możliwości komputerów, zawiera też opcjonalne mechanizmy
wyłapywania błędów w czasie wykonywania programu.
Z kilkudziesięciu typów dostępnych w bibliotece standardowej C++ poniżej
skrótowo omawiam cztery najbardziej potrzebne. Dokładniejszy opis przedstawionych poniżej klas oraz reszty tej biblioteki znajduje się w rozdziałach 11 i 12.
2.6.1
Strumienie
Jak już wiemy, odczyt danych z klawiatury następuje za pośrednictwem strumienia std::cin i operatora >>. Podobnie zapis danych na ekranie monitora następuje
poprzez strumień std::cout (lub std::cerr) oraz operator <<.
W bardzo podobny sposób można posługiwać się strumieniami związanymi
z plikami. Demonstruje to program 2.9, który otwiera plik dane.txt, odczytuje
z niego wszystkie liczby i zapisuje ich sumę w pliku suma.txt.
Popularne typy standardowe
45
Wydruk 2.9: Praca z plikami.
1
5
10
15
#include <fstream> // <−− biblioteka strumieni związanych z plikami
int main()
{
std :: ifstream F (”dane.txt”); // <−− otwarcie pliku do czytania
if (F)
{
double suma = 0.0;
do
{
double x;
F >> x;
// <−− czytanie z pliku
if (F)
// <−− jeżeli plik jest w ”dobrym” stanie
suma += x;
}while(F);
// <−− dopóki plik jest w ”dobrym” stanie
std :: ofstream G (”suma.txt”); // <−− otwarcie pliku do pisania
G << suma << ”\n”;
// <−− zapisywanie w pliku
}
}
W pierwszym wierszu tego programu włącza się plik nagłówkowy fstream
zawierający wszystkie deklaracje niezbędne do posługiwania się plikami w C++.
Wiersz 5 zawiera definicję strumienia F klasy std::ifstream. Nazwa tej klasy
pochodzi od wyrażenia input file stream, czyli „strumień wejściowy związany
z plikiem”). Obiekty tej klasy służą wyłącznie do odczytywania danych z plików
– za ich pośrednictwem nie można plików modyfikować. Obiekt F inicjalizowany
jest tekstem "dane.txt". Oznacza to, że strumień F ma być związany z plikiem
dane.txt. Innymi słowy, obiekt F ma reprezentować dane odczytywane z pliku
dane.txt.
Instrukcja if w wierszu 6 sprawdza, czy próba otwarcia pliku w wierszu 5
rzeczywiście się powiodła. Zasada jest następująca: dowolny strumień (np. F lub
std::cout) może być przypisany obiektowi typu bool (wymaganemu m.in. jako
argument instrukcji if). Wartością tego obiektu będzie true, jeżeli strumień jest
w stanie poprawnym; w przeciwnym wypadku nastąpił jakiś błąd, strumień został
„zepsuty” i przypisanie go zmiennej typu bool umieści w niej wartość false.
Spostrzeżenie 2.14 Instrukcja if (F) jest najprostszą metodą sprawdzania stanu strumienia F.
W wierszu 12 odczytujemy z pliku F wartość liczby rzeczywistej i przypisujemy
ją zmiennej zmiennopozycyjnej x.
Spostrzeżenie 2.15 Odczytywanie danych z plików wykonuje się tak samo, jak
odczytywanie danych z klawiatury – w każdym wypadku podstawową metodą jest
użycie obiektu strumieniowgo i operatora >>.
W wierszu 13 ponownie sprawdzany jest stan strumienia. Test ten należy
wykonywać po każdej operacji na strumieniu choćby po to, by sprawdzić, czy nie
46
Rozdział 2. Wyrażenia i instrukcje
doszliśmy do końca pliku. W wierszu 15 definiuje się poprawność stanu strumienia
jako warunek kontynuowania wczytywania danych.
Po dojściu programu do wiersza 16 strumień F jest w stanie niepoprawnym,
uznajemy więc, że wszystkie dane zostały wczytane. Następnie otwieramy plik
G, w którym zapiszemy sumę wczytanych liczb. Robimy to niemal dokładnie tak
samo, jak w wypadku pliku do odczytu – jedyną różnicą jest typ definiowanego
obiektu: std::ofstream (ang. output file stream).
W wierszu 17 zapisujemy dane w pliku G.
Spostrzeżenie 2.16 Zapisywanie danych w pliku wykonuje się tak samo, jak
wyświetlanie ich na monitorze – podstawową metodą jest użycie operatora <<.
Strumienie w języku C++ to temat-rzeka, któremu poświęcono osobne, wielusetstronicowe podręczniki. Niemniej jednak podane dotąd informacje (wraz z opisaną na następnej stronie funkcją getline) wystarczają w 99% praktycznych sytuacji.
2.6.2
Napisy
Z napisami, czyli obiektami klasy std::string, mieliśmy już do czynienia wielokrotnie (m.in. w programie 1.4 na str. 22). W zasadzie potrafimy już je wczytywać
z klawiatury lub wyświetlać na ekranie.
Napisy są typami wysokopoziomowymi i można przy ich pomocy wykonywać wiele skomplikowanych operacji. Kilka najbardziej użytecznych właściwości
napisów demonstruje program 2.10.
Wydruk 2.10: Podstawowe operacje na obiektach klasy std :: string .
6
10
15
std :: string s1(”Baba”);
std :: string s2(”Jaga”);
std :: string s ;
s = s1 + ” ” + s2;
// dodawanie napisów
std :: cout << s << ”\n”;
std :: cout << s.size() << ”\n”; // z ilu znaków składa się napis?
std :: cout << s[1] << ”\n”;
// jaki jest drugi znak napisu?
s [4] = ’−’;
// zmiana piątego znaku
std :: cout << s << ”\nWpisz tekst skladajacy sie z kilku slow:\n”;
getline (std :: cin , s );
// wczytywanie napisu wielowyrazowego
s += ”...”;
std :: cout << s << ”\n”;
std :: cout << s.substr(0,2) << ”\n”; // pobranie fragmentu napisu
Baba Jaga
9
a
Baba-Jaga
Wpisz tekst skladajacy sie z kilku slow:
Jan Kowalski
Jan Kowalski...
Ja
Popularne typy standardowe
47
Wiersz 9 ilustruje dodawanie napisów. W sumach można mieszać napisy typu
std::string i literały napisowe, o ile obiekt klasy std::string będzie pierwszym
lub drugim składnikiem sumy.
Wiersz 11 zawiera wywołanie funkcji size, która zwraca ilość znaków wchodzących w skład napisu. Zwróćmy uwagę na nietypowy sposób wywołania tej
funkcji: jej nazwa znajduje się po nazwie obiektu, na rzecz którego jest wywoływana; dodatkowo od nazwy obiektu funkcja oddzielona jest kropką. Puste nawiasy po nazwie funkcji informują, że poza obiektem s funkcja size nie przyjmuje
żadnych innych argumentów. Funkcje „pisane po kropce” są charakterystyczne
dla języków obiektowych i zwane są funkcajmi składowymi lub metodami . Poza
sposobem wywołania niczym nie różnią się one od zwykłych funkcji. Funkcjami
składowymi zajmiemy się bliżej w rozdziale 5 – na razie wystarczy nam wiedza
o ich istnieniu i sposobach używania.
Wiersze 12 i 13 zawierają kolejny nowy element języka: operator [], zwany
także operatorem indeksowania. Dzięki niemu napis s można w pewnych sytuacjach traktować jak ciąg znaków (obiektów typu char). Operator [] umożliwia
dostęp do kolejnych elementów tego ciągu. Wyrażenie s[n] oznacza n + 1-wszy
element tego ciągu. Innymi słowy s[0] to pierwszy znak napisu s, s[1] to znak
drugi itd. Ostatnim znakiem niepustego napisu s jest więc s[s.size()-1]. Wiersz
12 demonstruje odczytywanie n-tego znaku napisu, a wiersz 13 – jego modyfikację.
W definicji operacji strumieniowych przyjęto zasadę, że podczas wczytywania danych operator >> pomija tzw. białe znaki, czyli spacje, tabulatory, znaki
końca wiersza itp. Aby do obiektu typu std::string wczytać napis składający
się z wielu wyrazów, należy posłużyć się inną metodą. Jedna z nich polega na
użyciu funkcji getline (wiersz 15), która wczytuje wszystkie znaki aż do końca
bieżącego wiersza.
Reguła 2.9 Operator >> pomija w strumieniach wszelkie tzw. białe znaki, czyli spacje, tabulatory, znaki końca wiersza itp. Aby pobrać ze strumienia napis
zawierający białe znaki, można użyć funkcji getline.
Wiersz 16 demonstruje zastosowanie operatora += do dopisywania tekstu na końcu
napisu. Z kolei wiersz 17 demonstruje funkcję składową substr klasy std::string.
Wyrażenie s.substr(n,k) to napis składający się z k kolejnych znaków napisu s
począwszy od znaku s[n].
2.6.3
Wektory
Wektory są niewątpliwie najczęściej stosowanym typem standardowym. Prosty
program ilustrujący ich użycie przedstawia program 2.11, który wczytuje ze standardowego wejścia wiersz i wyświetla na ekranie informacje o tym, jakie wystąpiły
w nim znaki i ile razy.
W wierszu 4 włączany jest do programu plik nagłówkowy vector zawierający
wszystkie deklaracje niezbędne do posługiwania się wektorami. Z kolei w wierszu
8 znajduje się definicja wektora v. Ma ona bardzo charakterystyczną postać, z którą spotykamy się po raz pierwszy. Przy definiowaniu wektorów obowiązują dwie
zasady. Po pierwsze, definiując wektor, koniecznie należy w nawiasach ostrokątnych podać typ (klasę) elementów wektora. Typy, w których definicji wymaga się
podania nazwy dodatkowego typu, zwie się typami parametrycznymi. W naszym
przypadku typ int jest parametrem typu std::vector<int>. Jako typ elementów
48
Rozdział 2. Wyrażenia i instrukcje
można podać dosłownie dowolny typ zdefiniowany w programie (nawet inny wektor!). Po drugie, po nazwie definiowanego wektora można w nawiasach okrągłych
umieścić parametr lub parametry służące do jego inicjalizacji. W naszym przykładzie do inicjalizacji wektora v używa się liczby 100. Oznacza to, że tworzony
wektor ma mieć dokładnie 100 elementów. Uwaga: wektorów (i innych typów parametrycznych) nie można inicjalizować w „zwykły” sposób, tj. poprzez użycie
operatora =. Zapis std::vector<int> v = 100 jest błędny.
Wydruk 2.11: Zastosowanie klasy std :: vector.
1
#include
#include
#include
#include
<iostream>
<string>
<cctype> // <−− deklaracja funkcji isgraph
<vector> // <−− deklaracja typu std::vector
5
10
15
20
int main()
{
std :: vector<int> v(256); // <−− wektor 256 liczb typu int
std :: string s ;
getline (std :: cin , s );
// <−− wczytanie wiersza z std::cin do s
for (int i = 0; i < s. size (); i++)
{
char c = s[i ];
// <−− (i+1)−y znak w napisie
v[c]++;
// <−− uaktualnienie licznika znaków
}
for (int i = 0; i < 256; i++)
{
if (v[ i ] != 0 and isgraph(i))
{
char c = i;
std :: cout << c << ”: ” << v[i] << ”\t”;
}
}
}
Ala ma kota. To kot Ali.
.: 2 A: 2 T: 1 a: 3 i: 1
k: 2
l: 2
m: 1
o: 3
t: 2
Domyślnie wszystkie elementy nowotworzonego wektora wypełniane są zerami. Wynika stąd, że wiersz 8 zawiera definicję wektora v składającego się ze 100
obiektów typu int, każda o wartości 0.
Dostęp do elementów wektora zapewnia znany nam już z rozdziału 2.6.2 operator []. W wektorach zastosowano tę samą konwencję co w napisach: pierwszy
element wektora ma indeks 0, drugi ma indeks 1, n-ty ma indeks n + 1, a ostatnim elementem niepustego wektora v jest v[v.size()-1]. Operator indeksowania
używany jest w wierszach 14 (zliczanie wystąpień poszczególnych liter) i 21 (odczytywanie wartości elementu wektora).
Reguła 2.10 Dowolny wektor v posiada elementy o indeksach od 0 do v.size()-1.
Nigdy, przenigdy nie wolno indeksować wektora liczbą spoza tego zakresu.
Wektory mogą być puste – w tym wypadku metoda size zwraca 0. Wektorów
takich w ogóle nie można indeksować operatorem [].
Popularne typy standardowe
49
Wyjaśnienia wymaga jeszcze wiersz 18, a szczególnie użyta w nim funkcja
isgraph. Funkcja ta służy do sprawdzenia, czy jej argument jest znakiem, który
można bezpiecznie wyświetlić na ekranie (istnieją bowiem znaki, które są przez
konsolę interpretowane jako znaki sterujące; przesłanie takiego znaku na konsolę
może całkowicie zmienić tryb jej pracy).
Reasumując, w wierszu 10 program wczytuje z klawiatury linijkę tekstu, po
czym w pierwszej pętli for rozbija tekst na poszczególne litery i zlicza je w wektorze v, a w drugiej pętli for wyświetla zawartość tych niezerowych elementów
wektora v, którym odpowiadają „normalne”, drukowalne znaki.
2.6.4
Słowniki
Słowniki stanowią swoiste rozszerzenie wektorów i w związku z tym są typem
danych o wszechstronnych zastosowaniach. Nie mam najmniejszych wątpliwości,
że powinny znajdować się w podręcznym „arsenale” każdego programisty C++.
Niestety, moje doświadczenie wykładowcy C++ prowadzi do wniosku, że studenci niezwykle rzadko korzystają ze słowników. Prawdopodobnie wynika to z faktu,
że aby móc w pełni wykorzystać możliwości tej klasy danych, należy mieć za
sobą praktycznie cały kurs C++, a już na pewno dobrze opanować tajniki standardowej biblioteki STL. Żeby odwrócić tę niekorzystną tendencję, poniżej, jako
swoisty aperitif, przedstawiam nieco uproszczony3 program zliczający ilość wystąpień w kodzie programu słowa kluczowego int.
Wydruk 2.12: Prosty przykład zastosowania słowników (std :: map).
1
5
10
15
#include <iostream>
#include <map >
// <−− deklaracja funkcji słownikowych
int main()
{
std :: map<std::string, unsigned int> slownik; // definicja słownika
for ( ; ; )
// nieskończona pętla
{
std :: string s ;
// definicja pustego napisu s
std :: cin >> s;
// próba wczytania kolejnego słowa
if (std :: cin)
// jeżeli udało się wczytać kolejne słowo
slownik[s]++;
// aktualizacja licznika wystąpień słowa s
else
// w przeciwnym wypadku
break;
// koniec pętli
}
std :: cout << ”slowo ’int’ wystapilo ” << slownik[”int”] << ” razy\n”;
}
Ponieważ program wczytuje dane ze standardowego wejścia, uruchamiając
go, należy przekierować standardowy strumień wejścia z klawiatury do pliku, np.
uruchamiając program komendą program.exe < plik.
Na pełne omówienie tego programu jest nieco za wcześnie, tym niemniej warto
zwrócić uwagę na kilka użytych w nim właściwości słowników.
3 Uproszczenie polega na tym, iż program ten definiuje słowa nieco inaczej niż jesteśmy do
tego przyzwyczajeni.
50
Rozdział 2. Wyrażenia i instrukcje
• Słowniki zdefiniowane są w pliku nagłówkowym map (wiersz 2).
• Słowniki są typami sparametryzowanymi dwoma innymi typami (wiersz 6).
Pierwszy z nich zwany jest typem klucza, drugi – typem wartości.
• Dostęp do elementów słownika zapewnia operator [] (wiersze 12 i 16),
przy czym typ argumentu tego operatora jest typem klucza słownika (tu:
std::string), a typ wartość zwracanej przez operator [] to drugi typ parametryzujący słownik (tu: unsigned int).
2.7
Obiekty lokalne i globalne. Zasięg. Przesłanianie
Programy pisane w C++ nierzadko pisane są przez zespoły wielu programistów,
którzy nie muszą ze sobą ściśle współpracować. Prowadzi to do wielu problemów.
Jeden z nich wiąże się z nazwami identyfikatorów. Czy przed użyciem jakiegoś nowego identyfikatora na oznaczenie obiektu lub funkcji, np. size, programista musi
sprawdzać, czy nazwy tej nie użył już wcześniej on sam lub któryś z jego czterdziestu kolegów? Oczywiście odpowiedź brzmi – tylko w absolutnie wyjątkowych,
dobrze zdefiniowanych sytuacjach! W C++ istnieją rozwinięte mechanizmy kontroli zakresu widzialności identyfikatorów, które pozwalają praktycznie zupełnie
wyeliminować konflikty nazw.
Podstawowe zasady unikania konfliktu nazw w C++ ilustruje program 2.13.
Wydruk 2.13: Zakresy lokalne i globalne; przesłanianie identyfikatorów.
1
#include <iostream>
int x = 0;
5
10
15
// <−− zmienna w zakresie globalnym
int main()
{
// <−− tu się zaczyna zakres lokalny
std :: cout << ”x = ” << x << ”\n”;
int x = 7;
// <−− przesłania definicję z wiersza 3
std :: cout << ”x = ” << x << ”\n”;
for (int i = 0; i < 2; i++)
{
double x = 11.11;
// <−− przesłania definicję z wiersza 8
std :: cout << ”x = ” << x << ”\n”;
}
// <−− odsłania definicję z wiersza 3
std :: cout << ”lokalne x = ” << x << ”\n”;
std :: cout << ”globalne x = ” << ::x << ”\n”;
}
// <−− tu się kończy zakres lokalny
x = 0
x = 7
x = 11.11
x = 11.11
lokalne x = 7
globalne x = 0
Operatory
51
W programie tym, nie licząc obiektu std::cout, występują wyłącznie zmienne
o nazwie x. Są to oczywiście różne zmienne – każda z nich przechowuje inną
wartość, o czym świadczy wynik działania programu. Pierwsza zmienna x zdefiniowana jest w wierszu 3. Miejsce jej definicji jest dość niezwykłe – jest to
pierwszy przypadek użycia w tym podręczniku instrukcji poza funkcją main, w
tzw. zakresie globalnym.
W C++ starannie rozróżnia się dwa obszary kodu programu. Pierwszy z nich
to obszar objęty (dowolną) parą nawiasów klamrowych zwany zakresem lokalnym.
Drugi to obszar pozostający poza jakąkolwiek parą klamer, zwany zakresem globalnym. Obowiązuje następująca zasada: obiekty zdefiniowane w zakresie globalnym (czyli tzw. obiekty globalne) mogą być wykorzystywane w całym programie,
choćby składał się z tysięcy plików i milionów wierszy kodu. W przypadku dużych programów właściwość ta jest potencjalnie bardzo niebezpieczna i niezwykle
utrudnia pielęgnację kodu, dlatego zmiennych globalnych używa się w wyjątkowych sytuacjach. Jak dotąd poznaliśmy tylko trzy obiekty globalne: std::cin,
std::cout i std::cerr; wprowadzania innych obiektów globalnych nie planuję.
Reguła 2.11 Należy zdecydowanie unikać stosowania obiektów globalnych.
Zakresy lokalne definiowane są przez pary odpowiadających sobie klamer.
W naszym przykładowym programie mamy dwa zakresy lokalne. Pierwszy obejmuje wiersze 6-17, drugi składa się z wierszy 11-14 i zawiera się w pierwszym zakresie. Obiekty lokalne istnieją wyłącznie w zakresie, w którym zostały utworzone
(stąd nazwa ‘lokalne’). Wraz z dojściem sterowania programu do klamry zamykającej zakres niszczone są wszystkie zdefiniowane w nim obiekty. Np. zmienna
x zdefiniowana w wierszu 12 „ginie” po dojściu sterowania programu do klamry
w wierszu 14, jest więc niewidoczna w wierszu 15. W każdym zakresie można definiować zmienne o dowolnych nazwach, nie przejmując się zmiennymi w zakresach
nadrzędnych (oczywiście w każdym zakresie można definiować tylko jeden obiekt
o danej nazwie). Jeżeli przypadkiem w zakresie lokalnym użyje się nazwy wykorzystanej już w zakresie nadrzędnym, kompilator uzna za obowiązującą definicję
z zakresu wewnętrznego. Zjawisko to zwane jest przesłanianiem nazw ; wyjaśnia
ono, dlaczego w wierszu 13 program używa zmiennej x zdefiniowanej w wierszu
12, a nie zmiennych zdefiniowanych w wierszu 3 lub 8.
Reguła 2.12 Należy unikać przesłaniania nazw zmiennych.
Każdy obiekt globalny musi być dostępny w całym programie. Aby uzyskać
dostęp do przesłoniętego obiektu globalnego, należy jego nazwę poprzedzić operatorem ::, co ilustruje wiersz 16. Nawet jeżeli zmienna globalna nie jest przesłonięta, warto jej nazwę poprzedzać tym operatorem w celu zwiększenia czytelności
kodu.
2.8
Operatory
Jak już wiemy, w języku C++ instrukcje tworzy się z wyrażeń, a podstawowym
budulcem wyrażeń są identyfikatory obiektów i funkcji oraz operatory. Pełny
wykaz operatorów C++ przedstawia tabela 2.4.
52
Rozdział 2. Wyrażenia i instrukcje
Tabela 2.4: Operatory w C++.
priorytet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
operator
::
.
->
[]
()
++
-typeid
dynamic_cast<...>(...)
static_cast<...>(...)
reinterpret_cast<...>(...)
const_cast<...>(...)
sizeof
++
-~
!
+
&
*
new
delete
delete []
(typ)wyrażenie
.*
->*
*
/
%
+, <<
>>
<
<=
>
>=
==
!=
&
^
|
&& lub and
|| lub or
? :
=
*=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=
throw
,
nazwa/opis
zasięg
wybór składowej
wybór składowej
indeksowanie
wywołanie funkcji
zwiększ o 1 (n++)
zmniejsz o 1 (n--)
identyfikacja typu
dynamiczna konwersja typu
statyczne konwersja typu
niesprawdzana konwersja typu
konwersja z lub na const
rozmiar obiektu lub wyrażenia
zwiększ o 1 (++n)
zmniejsz o 1 (--n)
negacja bitowa
negacja logiczna
plus jednoargumentowy
minus jednoargumentowy
pobranie adresu
wyłuskanie wartości
przydział pamięci
zwolnienie obiektu
zwolnienie tablicy
konwersja typu w stylu C
wybór składowej
wybór składowej
mnożenie
dzielenie
obliczanie reszty z dzielenia
dodawanie, odejmowanie
bitowe przesunięcie w lewo
bitowe przesunięcie w prawo
mniejszy
mniejszy lub równy
większy
większy lub równy
równy
nierówny
iloczyn bitowy
bitowa różnica symetryczna
suma bitowa
koniunkcja wyrażeń logicznych
suma wyrażeń logicznych
wyrażenie warunkowe
przypisanie wartości
wykonaj operację i przypisz
zgłoszenie wyjątku
operator przecinkowy
Operatory
53
W praktyce większość wyrażeń zawiera kilka operatorów. Jeżeli w wyrażeniu
nie ma nawiasów, kompilator musi w sposób deterministyczny określić porządek wartościowania poszczególnych operatorów. Służą do tego reguły związane
z priorytetem i łącznością operatorów.
2.8.1
Priorytet operatorów
Zgodnie z tabelą 2.4, operatory podzielono na 18 grup różniących się priorytetem.
Jeżeli w jakiś wyrażeniu zastosuje się operatory o różnym priorytecie, zawsze
najpierw zostanie wyznaczona wartość tego z nich, który ma najwyższy priorytet.
Tak więc w wyrażeniu 2+2*2 najpierw zostanie wykonane mnożenie (priorytet 5),
a dopiero później dodawanie (priorytet 6).
2.8.2
Łączność operatorów
Jeżeli w wyrażeniu występuje kilka operatorów o tym samym priorytecie, o ich
kolejności decyduje tzw. łączność. Operatory mogą być łączne albo prawostronnie, albo lewostronnie. Łączność lewostronna oznacza wartościowanie operatorów
od lewej do prawej. Łączność prawostronna oznacza wartościowanie od prawej do
lewej.
Reguła 2.13 Prawostronnie łączne są operatory jednoargumentowe oraz operatory przypisania. Pozostałe operatory są łączne lewostronnie.
Zgodnie z regułą 2.13, w wyrażeniu 1 + 1e-100 - 1 najpierw zostanie wykonane
dodawanie (łączność lewostronna), w związku z czym wartością całego wyrażenia będzie 0.0. Analogicznie w wyrażeniu x = y = 0 najpierw zostanie wykonane
przypisanie y = 0 (łączność prawostronna).
2.8.3
Wartość operatorów
Niemal wszystkie operatory mają wartość (z tego powodu można je traktować
jak funkcje o specjalnym zapisie). W każdym miejscu programu, w którym może
pojawić się wyrażenie, może też pojawić się operator. Dlatego z punktu widzenia
składni języka poprawne są instrukcje if (a=0) czy if (a == 3,14), choć w każdym z tych przypadków kompilator wygeneruje ostrzeżenie4 . Wartości poszczególnych operatorów zostaną omówione poniżej, dla każdego operatora osobno.
2.8.4
Opis wybranych operatorów
Operatory ++ i -Jak już wiemy, operator ++ służy do powiększania wartości zmiennej o jeden.
Operator ten występuje w dwóch wcieleniach: jako operator przedrostkowy (pisany przed zmienną, np. ++x) i przyrostkowy pisany za zmienną, np. x++. Obie
wersje tego operatora różnią się zwracaną przezeń wartością.
Reguła 2.14 Wartością wyrażenia x++ jest stara wartość x (przed zwiększeniem
o 1), a wartością wyrażenia ++x jest nowa wartość x (po zwiększeniu o 1).
4 Pierwszy
warunek jest zawsze fałszywy, drugi – zawsze prawdziwy.
54
Rozdział 2. Wyrażenia i instrukcje
Dlatego po wykonaniu ciągu instrukcji
int
int
int
int
x = 0;
y = ++x;
xx = 0;
yy = x++;
zmienne x i xx będą miały wartość 1, wartością y będzie 1, a wartością yy będzie 0.
Operator sizeof
Operator sizeof zwraca liczbę bajtów zajmowanych przez zmienne danego typu.
Typ można podac bezpośrednio jako argument lub pośrednio poprzez wyrażenie.
Z definicji sizeof(char) ma wartość 1. Przykłady: sizeof(int), sizeof(x + 2.0).
Operator konwersji statycznej
Operator static_cast służy do zamiany typu (tzw. rzutowania) obiektu lub wyrażenia. Pożądany typ podajemy w nawiasach ostrokątnych, a konwertowane wyrażenie w nawiasach okrągłych. Typowy przykład to instrukcja
double z = static cast<double>(m)/n;
gdzie m i n są typu int. Bez zastosowania static_cast operator dzielenia obciąłby część ułamkową wyniku. Zamiast static_cast<TYP>(WYRAŻENIE) można też
stosować zapis (TYP)(WYRAŻENIE) lub TYP(WYRAŻENIE). Powyższą instrukcję można by więc zapisać jako double z = double(m)/n lub double z = (double)m / n.
W C/C++ rzutowanie typów zmiennoprzecinkowych na całkowite odbywa się poprzez obcięcie części ułamkowej. Wartością wyrażenia int(-2.5) jest więc -2.
Operatory arytmetyczne
Operatory arytmetyczne (priorytet 5 i 6 w tabeli 2.4) zostały opisane w rozdziałach 1.9 (str. 25) i 2.4 (str. 41). Do omówienia pozostał operator %, który oblicza
resztę z dzielenia. Na przykład wartością 8 % 3 jest 2, gdyż resztą z dzielenia 8
przez 3 jest właśnie 2.
Operatory relacyjne
Z operatorami relacyjnymi zetknęliśmy się już w rozdziale 2.1.1 (str. 31). Należy
pamiętać, że ich wartością może być wyłącznie false lub true
Operator negacji logicznej
Operator ! zamienia true na false i vice versa.
Operatory bitowe
Każdą liczbę typu całkowitego (np. int lub unsigned) można traktować jak liczbę
w układzie dwójkowym, tj. składającą się z jedynek (= „prawda”)lub zer (=
„fałsz”). Operator & służy do obliczania iloczynu dwóch liczb bit po bicie. Na
przykład 7&3 równa się 3. Analogicznie operator | oblicza alternatywę dwóch liczb
bit po bicie, np. 7|3 równa się 7. Z kolei operator ^ wyznacza różnicę symetryczną
Operatory
55
(7^3 ma wartość 4). Jednoargumentowy operator ~ zamienia wszystkie zera na
jedynki, a jedynki na zera. Z kolei operator << przesuwa wszystkie bity w lewo,
a operator >> w prawo. Na przykład 3<<2 ma wartość 12 (przesunięcie o dwie
pozycje w lewo).
W praktyce operator & służy do testowania wartości lub zerowania bitów,
operator | do ich ustawiania na 1, a operator ^ do ich „przewracania” z 0 na 1
lub z 1 na 0. Z kolei operatory << i >> ułatwiają konstruowanie masek bitowych.
Operatory alternatywy i koniunkcji logicznej
Jak już wiemy, operator && można zapisać w postaci słownej (and), a operator
oror w postaci or. Operatory te mają pewną niezwykle ważną właściwość: wyrażenia opracowywane są w nich w kierunku od lewej do prawej, przy czym jeżeli w
jakimś punkcie obliczeń można przewidzieć wartość wyrażenia, to nie wartościuje
się jego pozostałych argumentów. Własność tę można zilustrować na przykładzie
instrukcji if (n!=0 and m/n > 10) n++. Jeżeli pierwszy człon koniunkcji (n != 0)
jest fałszywy, całe wyrażenie na pewno też jest fałszywe, dlatego sprawdzanie drugiego warunku (m/n > 10) zostanie całkowicie pominięte. Dzięki temu wyrażenie
if (n != 0 and m/n > 10) jest bezpieczne nawet dla n == 0.
Analogicznie w wyrażeniu if(f(x) or g(y))... funkcja g będzie wywołana
tylko wtedy, gdy f(x) zwróci false (lub 0).
Wyrażenie warunkowe
Operator ? : jest operatorem trójargumentowym i stanowi swoiste uogólnienie
instrukcji warunkowej if. Składnia tego operatora wygląda następująco:
WARUNEK ? WARTOŚĆ TRUE : WARTOŚĆ FALSE
Przed znakiem zapytania zapisuje się dowolne wyrażenie, które można poddać
konwersji na typ bool. Jeżeli WARUNEK jest spełniony, wartością operatora jest
WARTOŚĆ_TRUE; w przeciwnym wypadku jego wartością jest WARTOŚĆ_FALSE.
Reguła 2.15 Jeżeli to tylko możliwe, zamiast nieczytelnego operatora ? : należy
posługiwać się instrukcją if.
W pewnych wypadkach wyrażenia warunkowego nie można jednak zastąpić instrukcją warunkową. Należą do nich inicjalizacje, np. instrukcja
const int rozmiar_tablicy = (wymiar == 2 ) ? 1000 : 100;
definiuje obiekt rozmiar_tablicy klasy const int i nadaje mu wartość początkową
1000 lub 100 zależnie od tego, czy wartość zmiennej wymiar równa jest 2.
Operatory przypisania
W C++ istnieje kilkanaście operatorów przypisania. Najpopularniejszy jest operator „zwyczajnego przypisania”, czyli =. Jak wiemy, instrukcja x = y; nadaje
zmiennej x wartość zmiennej y. Zgodnie z tabelą 2.4, do dyspozycji mamy jeszcze 10 innych operatorów przypisania. Stanowią one skrócony zapis wykonania
pewnej operacji (arytmetycznej) i przypisania. Na przykład wyrażenie x += z
równoważne jest wyrażeniu x = x + z. Analogicznie x >>= z to x = x >> z itd.
56
Rozdział 2. Wyrażenia i instrukcje
Spostrzeżenie 2.17 Złożone operatory przypisania, np. +=, zwiększają czytelność programu.
Wartością każdego operatora przypisania jest wartość jego lewego argumentu po przypisaniu mu argumentu prawego. Dlatego x = y += 2 czy if (x=1) są
wyrażeniami (formalnie) poprawnymi.
Operator przecinkowy
Operator przecinkowy to koncepcja z natury chora; nadużywana prowadzi do nieobliczalnych konsekwencji. Trzeba go jednak omówić, gdyż dość często używany
jest nieświadomie. Operator ten służy do ustalania kolejności obliczeń. Wyrażenie
f(x), g(x) oznacza: „najpierw oblicz f(x), potem g(x), a jako wartość wyrażenia
przyjmij wartość wyrażenia po ostatnim przecinku (czyli g(x)). Oto dwa popularne przykłady błędnego użycia tego operatora:
pi = 3,14;
v[i,j] = 0;
// pi będzie miało wartość 3!
// równoważne: v[j] = 0;
Jedyny znane mi miejsce, w którym operator przecinkowy ma rozsądne zastosowanie, to skomplikowana preambuła pętli for, np.
for (i = 0, x = 10.0; x >= 0 && i < 100; i++, x -= f(x))
W powyższej pętli dzięki operatorowi przecinkowemu udało się umieścić inicjalizację 2 zmiennych w części inicjalizacyjnej pętli; podobnie w kroku kończącym
pętli zmieściły się dwie instrukcje modyfikujące wartości zmiennych sterujących.
Reguła 2.16 Strzeż się operatora przecinkowego.
2.8.5
Operatorowe patologie
Niektóre wyrażenia operatorowe mają wartość nieokreśloną w definicji języka,
mimo że z formalnego punktu widzenia są zupełnie poprawne. Rozpatrzmy taką
oto „niewinną” instrukcję:
int n = f(j) + g(j);
Standard nie definiuje, czy podczas wyznaczania sumy wyrażeń f(j) i g(j) najpierw zostanie wywołana funkcja f czy g, co może mieć wpływ na wartość przypisywaną zmiennej n. Z tego samego względu wartość wyrażenia x++ + ++x jest
także nieokreślona. Jedynymi operatorami, dla których zdefiniowano kolejność
wartościowania argumentów są operatory &&, || oraz operator przecinkowy.
Inny problem pojawia się w dzieleniu liczb o różnych znakach. Otóż standard języka nie gwarantuje, że dla liczb całkowitych m i n o przeciwnych znakach
wyrażenia m/n i m % n będą miały w każdej implementacji C++ tę samą wartość.
Analogicznie nie zdefiniowano wartości operatorów << i >>, gdy jeden z ich
argumentów jest ujemny.
2.9
Wyrażenia i instrukcje
Ten rozdział zawiera tylko jedno stwierdzenie: dowolną instrukcję prostą tworzy wyrażenie zakończone średnikiem. Oznacza to m.in., że 7; jest (formalnie)
poprawną instrukcją C++. Cóż, takie są reguły tej gry!
Q&A
2.10
57
Q&A
Słowo kluczowe while występuje zarówno w pętli while jak i do...while.
Czy nie prowadzi to do zbytniego zamieszania?
Owszem, prowadzi. Aby programista mógł łatwo stwierdzić, w jakim znaczeniu
użyto słowo while, stosuje się następującą konwencję w zapisie programów. Jeżeli słowo while rozpoczyna pętlę while, musi być pierwszym słowem w danym
wierszu. Jeżeli słowo to kończy pętlę do, powinno następować bezpośrednio po
klamrze zamykającej instrukcję blokową – przykład takiego zapisu znajduje się
w piętnastym wierszu programu 2.9 (str. 45).
Dlaczego w definicji języka nie zdefiniowano zachowania się operatorów /, %, << i >> dla ujemnych argumentów?
Powodem jest troska o wydajność. Gdyby wprowadzić takie definicje do języka,
na niektórych platformach każdemu wywołaniu powyższych operatorów musiałoby towarzyszyć sprawdzenie znaku jego argumentów, które w 99,9% wypadków
byłoby zbędne, bo w praktyce ludzie i tak używaliby niemal wyłącznie argumentów dodatnich, a więc niewymagających takiego sprawdzenia. Poza tym C++
posiada mechanizmy tworzenia własnych klas danych; można więc samemu zbudować klasę liczbową, w której omawiane tu operatory będą miały ściśle określone
wartości dla każdej pary argumentów.
Czy muszę uczyć się całej tabeli operatorów wraz z ich priorytetami?
Oczywiście, że nie! Tabela 2.4 ma charakter informacyjny, a jej głównym celem
jest uczulenie Czytelników na kwestię priorytetów. W praktyce w razie jakichkolwiek wątpliwości co do kolejności obliczeń problem rozwiązuje się poprzez użycie
nawiasów. Jednak używanie wielu nawiasów szkodzi czytelności wyrażeń. Priorytety wprowadzono m.in. po to, by nawiasy były potrzebne jak najrzadziej.
Czy zamiast funkcji getline do wczytywania wierszy nie można posłużyć się funkcją get?
Owszem, ale posługiwanie się funkcją get jest nieco bardziej skomplikowane, gdyż
w przeciwieństwie do getline nie może ona jako argumentu przyjąć obiektu typu
std::string; poza tym get jest funkcją składową, a tych wolę nie wprowadzać
zbyt wielu na tak wczesnym etapie nauki. Dla niecierpliwych podaję przykład
zastosowania funkcji składowej get do kopiowania strumieni:
void kopiuj(std :: ostream & wy, std::istream & we)
{
char c;
while(we.get(c))
wy.put(c);
}
2.11
Quiz
1. Jaka jest podstawowa różnica między liczbami całkowitymi a obiektami
typu int?
2. Jaka jest podstawowa różnica między liczbami rzeczywistymi a obiektami
typu double?
58
Rozdział 2. Wyrażenia i instrukcje
3. Podaj przykład twierdzenia matematycznego, które jest słuszne dla liczb
rzeczywistych, a nie jest słuszne dla obiektów typu double.
4. W wielu firmach sofwareowych nie wolno używać liczb zmiennopozycyjnych
bez zgody kierownika projektu. Co może być tego powodem?
5. Dlaczego w dużych programach należy ograniczać liczbę obiektów globalnych?
6. Dlaczego warto używać modyfikatora const?
7. Jak można sprawdzić długość napisu?
8. Jaka jest różnica między instrukcjami while i do?
9. Do czego służy preambuła pętli for?
10. Które operatory są łączne prawostronnie, a które lewostronnie?
11. Który operator ma najmniejszy, a który największy priorytet?
2.12
Problemy
1. Oblicz (najlepiej bez komputera) wartości następujących wyrażeń:
0xa - 012
1 << 4
1 + 1e-20 - 1
0xff ^ 0xf0
0xff | 0xaa
13 % 3
0xf & 0x8
3 == 3 == 3
~(-1)
11218917 & 1
3,14 + 1
4 > 3 > 2
1/2
0 - 1u > 0
11218917 | 1
34678 ^ 34678
123456 + ~123456
8 >> 1
-1 < 2 & 1 < 2
300 << 1
2. Ciąg Fibonacciego fn zdefiniowany jest wzorami rekurencyjnymi f1 = 1,
f2 = 1, fn = fn−2 + fn−1 dla n 3. Napisz program, który w wektorze 10
liczb typu int umieści 10 kolejnych liczb Fibonacciego, a następnie wyświetli
je na ekranie, po jednej liczbie w wierszu.
3. Napisz program, który wczytuje linijkę z konsoli, po czym wyświetla ją
w od końca do początku (np. po wpisaniu napisu Jan Kowalski program
wyświetli ikslawoK naJ).
4. Napisz program szyfrujący. Powinien on do każdego wczytanego znaku dodawać 13. Wejście i wyjście programu powinno być skojarzone z plikami
o nazwach wczytywanych z klawiatury.
5. Napisz dekoder do poprzedniego zadania.
6. Proszę przerobić program 2.11 (str. 48) tak, aby zliczał wystąpienia dowolnego znaku w całym pliku.
7. Rozpatrzmy ciąg liczb naturalnych cn spełniający warunek
{
3cn−1 + 1, jeśli cn−1 jest nieparzyste
cn =
cn−1 /2, jeśli cn−1 jest parzyste
Istnieje nieudowodniona hipoteza, że niezależnie od wartości pierwszego
elementu tego ciągu prędzej czy później jeden z jego elementów będzie miał
wartość 1. Napisz program, który sprawdza tę hipotezę dla wszystkich c1
mniejszych od 1000.
Skorowidz
adaptacja pojemnika, 234
algorytm (STL), 209, 237
argc, 79
argument
domyślny funkcji, 76
tablicowy, 91
wskaźnikowy, 91
argv, 79
asercja (assert), 118, 130, 192
bajt zerowy, 92
bool, 40
boost (biblioteka), 247
char, 38
CVS, 187
default (etykieta), 252
definicja
funkcji, 80
obiektu, 22
deklaracja
funkcji, 80
dekorowanie nazw funkcji, 182
destruktor, 106
double, 38
dynamiczna struktura danych, 125
dyrektywa preprocesora, 24, 189
define, 176, 190
elif, 191
else, 191
endif, 176, 191
if, 191
ifdef, 191
ifndef, 176, 191
include, 190
undefine, 191
dziedziczenie, 139
hierarchia, 145
prywatne, 159
enum, 127
a const int, 127
etykieta, 251
exit, 80
float, 38
friend, 114
funkcja
argumenty faktyczne, 61
argumenty formalne, 61
exit, 80
globalna, 61
main, 79
mieszająca, 231
nazwa kwalifikowana, 137
overriding, 147
prototyp, 80
rekurencyjna, 75
składowa, 47, 108
statyczna, 121
sygnatura, 80
zaprzyjaźniona, 114
funktor, 210
generyczne programowanie, 202
goto, 251
hermetyzacja danych, 120
identyfikator, 24
implementacja, 63, 126
klasy, 115
inicjalizacja
struktur, 101
tablic, 88
instrukcja
a wyrażenie, 56
blokowa, 31, 34
break, 35, 252
continue, 35
253
254
Skorowidz
do, 35
for, 33
goto, 251
if...else, 31
return, 61
switch, 251
while, 35
literał, 22, 40
long, 36
long double, 38
long long, 36
main, 79
interfejs, 63, 125
klasy, 115
iterator (STL), 206
metoda, 47, 108, 152
czysto wirtualna, 155
statyczna, 121
stała, 108
jednostka kompilacji, 174
namespace, 136
klasa, 101
a przestrzeń nazw, 136
abstrakcyjna, 155
bazowa, 141
implementacja, 115
interfejs, 115
pochodna, 141
podstawowa, 141
rekurencyjna, 133
zagnieżdżona, 137
zaprzyjaźniona, 114
klucz (słownika, zbioru), 225, 226
kolejka (STL), 235
priorytetowa, 235
kompilacja
programu, 18, 174
warunkowa, 191
kompilator, 14, 18
g++, opcje, 249
komunikaty, 108
konkretyzacja (szablonu), 198
konsolidator, 18, 174
konstruktor, 103
domyślny, 105
kopiujący, 105
domyślny, 110
w formie szblonu, 201
preambuła, 103
kontrakt, 118, 130
konwersja
niejawna, 42
standardowa, 41
liczby zespolone, 220
lista (STL), 234
lista inicjalizacyjna, 103
NaN, 39
napisy, 46, 221
NDEBUG, 119
niezmiennik, 119
obiekt
funkcyjny, 210, 212
globalny, 50
lokalny, 50
statyczny, 77
stały, 43
zanurzony, 145
operator, 51
. (kropka), 101
, (przecinek), 56
::, 148
!, 54
&&, ||, and, or, 55
++, 53
* (wyłuskanie), 84
@ (adres), 84
->, 101
+, -, *, /, %, 54
--, 53
=, +=, -=, *=, /=, %=, 55
<<=, >>=, ^=, |=, &=, 55
<, <=, >, >= , ==, 54
^, ~, |, &, 54
? :, 55
[], 47
łączność, 53
priorytet, 53
przeciążony, 80
przypisania
domyślny, 111
sizeof, 54, 90, 145
static cast<...>, 54
Skorowidz
pamięć wolna, 94
plik, 162
binarny, 162
Makefile, 178
nagłówkowy, 23, 115
obiektowy, 174
tekstowy, 162
pojemnik, 206
polimorfizm, 149, 152
nazw funkcji, 80
predykat, 212
preprocesor, 18, 189
private:, 113
programowanie generyczne, 202
projekt (programu), 181
promocja całkowita, 42
protected:, 146
prototyp funkcji, 80
przeciążanie operatorów, 80
przestrzeń nazw, 136
gnu cxx, 137, 206, 233, 238
std, 28, 136
przesłanianie zmiennych, 50
public:, 113
referencje, 59
stałe, 60
rekurencja, 75
struktur danych, 133
relacja „X jest Y”, 144
relacja „X ma Y”, 144
relacja „X zarządza Y”, 144
repozytorium CVS, 187
size t, 37
składowa
chroniona, 147
klasy, 102
prywatna, 113, 147
publiczna, 113, 147
struktury, 100
sortowanie, 242
kryterium, 243
list, 234
stable sort, 230
w stylu C (qsort), 214
static, 77
statyczna
składowa klasy, 121
zmienna lokalna, 77
statyczność, 94
stałe, 43
symboliczne, 43, 127
std::pair, 201, 226
std::string, 162
std::vector, 216
sterta, 94
STL, 205
algorytmy, 209, 237
funktory, 210
iteratory, 206
pojemniki, 206, 225
predykaty, 212
stos (STL), 234
stos funkcji, 72
struktura, 99
rekurencyjna, 133
strumień, 44, 161
buforowany, 161
cerr, cin, clog, cout, 162
napisowy, 162
tryb binarny, 162
tryb tekstowy, 162
wejścia, 167
wyjścia, 167
sygnatura funkcji, 80
szablon
funkcji, 198
funkcji swobodnej, 199
klasy, 195
konkretyzacja, 198
specjalizacja, 200
częściowa, 200
słowa kluczowe, 24
słownik (STL), 49, 226
mieszający, 231
tablica, 87
a wskaźnik, 90
inicjalizacja, 88
jako argument funkcji, 91
wielowymiarowa, 88
tablica mieszająca, 231
template, 195
this, 112
typedef, 202
typy
bez znaku, 37
255
256
Skorowidz
całkowite, 36
parametryczne, 47
ze znakiem, 37
zmiennopozycyjne, 36, 38
using namespace, 28
vtable, 153
wartownik pliku, 176
wektor (STL), 47, 216
bitowy, 236
numeryczny, 237
wielowymiarowy, 219
wielosłownik (STL), 231
wielozbiór (STL), 231
wiązanie dynamiczne, 153
wskaźnik, 83, 84
a tablica, 90
na funkcję, 75, 91
na stałą, 86
na wskaźnik, 86
stały, 86
stały na stałą, 86
this, 112
void*, 85
zerowy, 85
wyliczenia, 127
wzorzec, patrz: szablon
zakres
globalny, 51
lokalny, 51
zaprzyjaźnianie funkcji, 114
zasięg, 50
zbiór (STL), 230
bitowy, 236
mieszający, 231
znak, 38
Bibliografia
[1] Bjarne Stroustrup, Język C++, wydanie piąte zmienione i rozszerzone,
WNT, Warszawa 2000.
[2] Bartosz Milewski, C++ in Action. Industrial-Strength Programming
Techniques, Addison-Wesley, New York 2001; wersja elektroniczna:
http://www.relisoft.com/book.
[3] Jesse Liberty, C++ dla każdego, Helion 2002 (okrojona wersja angielska Teach Yourself C++ in 21 Days dostępna jest w Internecie, np.
http://newdata.box.sk/bx/c/).
[4] http://www.kde.org/whatiskde/project.php#factsandfigures.
[5] M. Galssi i in., GNU Scientific Library Reference Manual, Edition 1.7 for
GSL Version 1.7, http://www.gnu.org/software/gsl/manual/.
[6] Donald Knuth, Sztuka programowania, WNT, Warszawa 2002.
[7] Qt Reference Documentation, http://doc.trolltech.com/4.0/.
[8] The GNU Make Manual, http://www.gnu.org/software/make/manual/.
[9] Gary V. Vaughan, Ben Elliston, Tom Tromey i Ian L. Taylor, GNU Autoconf,
Automake and Libtool, http://sources.redhat.com/autobook/.
[10] Karl Fogel i Moshe Bar, Open Source Development with CVS, 3rd Edition,
http://cvsbook.red-bean.com/.
[11] Tomasz Marciniak, Podstawy CVS, http://bsd.amu.edu.pl/wyklady/cvs.html.
[12] Ben Collins-Sussman, Brian W. Fitzpatrick i C. Michael Pilato, Version
Control with Subversion, http://svnbook.red-bean.com/.
[13] Bjarne Stroustrup, A Brief Look at C++0x,
http://www.artima.com/cppsource/cpp0x.html.
[14] Standard Template Library Programmer’s Guide,
http://www.sgi.com/tech/stl/.
[15] http://pl.wikipedia.org/wiki/Sito Eratostenesa.
[16] Biblioteka boost: http://www.boost.org/.
[17] Scott Meyers C++. 50 efektywnych sposobów na udoskonalenie Twoich programów, Helion, 2003.
257