Programowanie. Teoria i praktyka z wykorzystaniem C++
Programowanie. Teoria i praktyka z wykorzystaniem C++
Programowanie. Teoria i praktyka z wykorzystaniem C++
Create successful ePaper yourself
Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.
<strong>Programowanie</strong>.<br />
<strong>Teoria</strong> i <strong>praktyka</strong><br />
z <strong>wykorzystaniem</strong> <strong>C++</strong><br />
Autor: Bjarne Stroustrup<br />
T³umaczenie: £ukasz Piwko<br />
ISBN: 978-83-246-2233-7<br />
Tytu³ orygina³u: Principles and Practice Using <strong>C++</strong><br />
Format: 172×245, stron: 1112<br />
Zaczerpnij wiedzê o <strong>C++</strong> od samego twórcy jêzyka!<br />
• Jak zacz¹æ pracê w zintegrowanym œrodowisku programistycznym?<br />
• Jak profesjonalnie tworzyæ programy u¿ytkowe?<br />
• Jak korzystaæ z biblioteki graficznego interfejsu u¿ytkownika?<br />
Jeœli zale¿y Ci na tym, aby zdobyæ rzeteln¹ wiedzê i perfekcyjne umiejêtnoœci programowania<br />
z u¿yciem jêzyka <strong>C++</strong>, powinieneœ uczyæ siê od wybitnego eksperta i twórcy tego<br />
jêzyka – Bjarne Stroustrupa, który jako pierwszy zaprojektowa³ i zaimplementowa³ <strong>C++</strong>.<br />
Podrêcznik, który trzymasz w rêku, daje Ci szansê odkrycia wszelkich tajników tego<br />
jêzyka, obszernie opisanego w miêdzynarodowym standardzie i obs³uguj¹cego<br />
najwa¿niejsze techniki programistyczne. <strong>C++</strong> umo¿liwia pisanie wydajnego i eleganckiego<br />
kodu, a wiêkszoœæ technik w nim stosowanych mo¿na przenieœæ do innych jêzyków<br />
programowania.<br />
Ksi¹¿ka „<strong>Programowanie</strong> w <strong>C++</strong>. <strong>Teoria</strong> i <strong>praktyka</strong>” zawiera szczegó³owy opis pojêæ<br />
i technik programistycznych, a tak¿e samego jêzyka <strong>C++</strong>, oraz przyk³ady kodu.<br />
Znajdziesz tu równie¿ omówienia zagadnieñ zaawansowanych, takich jak przetwarzanie<br />
tekstu i testowanie. Z tego podrêcznika dowiesz siê, na czym polega wywo³ywanie<br />
funkcji przeci¹¿onych i dopasowywanie wyra¿eñ regularnych. Zobaczysz te¿, jaki<br />
powinien byæ standard kodowania. Poznasz sposoby projektowania klas graficznych<br />
i systemów wbudowanych, tajniki implementacji, wykorzystywania funkcji oraz<br />
indywidualizacji operacji wejœcia i wyjœcia. Korzystaj¹c z tego przewodnika, nauczysz<br />
siê od samego mistrza pisaæ doskona³e, wydajne i ³atwe w utrzymaniu programy.<br />
• Techniki programistyczne<br />
• Infrastruktura algorytmiczna<br />
• Biblioteka standardowa <strong>C++</strong><br />
• Instrukcje steruj¹ce i obs³uga b³êdów<br />
• Implementacja i wykorzystanie funkcji<br />
• Kontrola typów<br />
• Interfejsy klas<br />
• Indywidualizacja operacji wejœcia i wyjœcia<br />
• Projektowanie klas graficznych<br />
• Wektory i pamiêæ wolna<br />
• Kontenery i iteratory<br />
• <strong>Programowanie</strong> systemów wbudowanych<br />
• Makra<br />
Wykorzystaj wiedzê Bjarne Stroustrupa i pisz profesjonalne programy w <strong>C++</strong>!
Spis treci<br />
Wstp 19<br />
Sowo do studentów 21<br />
Sowo do nauczycieli 22<br />
Pomoc 23<br />
Podzikowania 23<br />
Uwagi do czytelnika 25<br />
0.1. Struktura ksiki 26<br />
0.1.1. Informacje ogólne 27<br />
0.1.2. wiczenia, praca domowa itp. 28<br />
0.1.3. Po przeczytaniu tej ksiki 29<br />
0.2. Filozofia nauczania i uczenia si 29<br />
0.2.1. Kolejno tematów 32<br />
0.2.2. <strong>Programowanie</strong> a jzyk programowania 34<br />
0.2.3. Przenono 34<br />
0.3. <strong>Programowanie</strong> a informatyka 35<br />
0.4. Kreatywno i rozwizywanie problemów 35<br />
0.5. Uwagi dla autorów 35<br />
0.6. Bibliografia 36<br />
0.7. Noty biograficzne 37<br />
Bjarne Stroustrup 37<br />
Lawrence „Pete” Petersen 38<br />
3
4 SPIS TRECI<br />
Rozdzia 1. Komputery, ludzie i programowanie 39<br />
1.1. Wstp 40<br />
1.2. Oprogramowanie 40<br />
1.3. Ludzie 42<br />
1.4. Informatyka 45<br />
1.5. Komputery s wszdzie 46<br />
1.5.1. Komputery z ekranem i bez 46<br />
1.5.2. Transport 47<br />
1.5.3. Telekomunikacja 48<br />
1.5.4. Medycyna 50<br />
1.5.5. Informacja 51<br />
1.5.6. Sigamy w kosmos 52<br />
1.5.7. I co z tego 53<br />
1.6. Ideay dla programistów 54<br />
Cz I Podstawy 61<br />
Rozdzia 2. Witaj, wiecie! 63<br />
2.1. Programy 64<br />
2.2. Klasyczny pierwszy program 64<br />
2.3. Kompilacja 67<br />
2.4. czenie 69<br />
2.5. rodowiska programistyczne 70<br />
Rozdzia 3. Obiekty, typy i wartoci 77<br />
3.1. Dane wejciowe 78<br />
3.2. Zmienne 80<br />
3.3. Typy danych wejciowych 81<br />
3.4. Operacje i operatory 82<br />
3.5. Przypisanie i inicjacja 85<br />
3.5.1. Przykad usuwania powtarzajcych si sów 87<br />
3.6. Zoone operatory przypisania 89<br />
3.6.1. Przykad zliczania powtarzajcych si sów 89<br />
3.7. Nazwy 90<br />
3.8. Typy i obiekty 92<br />
3.9. Kontrola typów 94<br />
3.9.1. Konwersje bezpieczne dla typów 95<br />
3.9.2. Konwersje niebezpieczne dla typów 96
SPIS TRECI 5<br />
Rozdzia 4. Wykonywanie oblicze 103<br />
4.1. Wykonywanie oblicze 104<br />
4.2. Cele i narzdzia 105<br />
4.3. Wyraenia 107<br />
4.3.1. Wyraenia stae 108<br />
4.3.2. Operatory 109<br />
4.3.3. Konwersje 111<br />
4.4. Instrukcje 112<br />
4.4.1. Selekcja 113<br />
4.4.2. Iteracja 119<br />
4.5. Funkcje 122<br />
4.5.1. Po co zaprzta sobie gow funkcjami 124<br />
4.5.2. Deklarowanie funkcji 125<br />
4.6. Wektor 126<br />
4.6.1. Powikszanie wektora 127<br />
4.6.2. Przykad wczytywania liczb do programu 128<br />
4.6.3. Przykad z uyciem tekstu 130<br />
4.7. Waciwoci jzyka 132<br />
Rozdzia 5. Bdy 139<br />
5.1. Wstp 140<br />
5.2. róda bdów 141<br />
5.3. Bdy kompilacji 142<br />
5.3.1. Bdy skadni 142<br />
5.3.2. Bdy typów 143<br />
5.3.3. Nie bdy 144<br />
5.4. Bdy konsolidacji 145<br />
5.5. Bdy czasu wykonania 146<br />
5.5.1. Rozwizywanie problemu przez wywoujcego 147<br />
5.5.2. Rozwizywanie problemu przez wywoywanego 148<br />
5.5.3. Raportowanie bdów 149<br />
5.6. Wyjtki 151<br />
5.6.1. Nieprawidowe argumenty 151<br />
5.6.2. Bdy zakresu 152<br />
5.6.3. Nieprawidowe dane wejciowe 154<br />
5.6.4. Bdy zawania zakresu 156<br />
5.7. Bdy logiczne 157<br />
5.8. Szacowanie 159<br />
5.9. Debugowanie 161<br />
5.9.1. Praktyczna rada dotyczca debugowania 162<br />
5.10. Warunki wstpne i kocowe 165<br />
5.10.1. Warunki kocowe 167<br />
5.11. Testowanie 168
6 SPIS TRECI<br />
Rozdzia 6. Pisanie programu 175<br />
6.1. Problem 176<br />
6.2. Przemylenie problemu 176<br />
6.2.1. Etapy rozwoju oprogramowania 177<br />
6.2.2. Strategia 177<br />
6.3. Wracajc do kalkulatora 179<br />
6.3.1. Pierwsza próba 180<br />
6.3.2. Tokeny 182<br />
6.3.3. Implementowanie tokenów 183<br />
6.3.4. Uywanie tokenów 185<br />
6.3.5. Powrót do tablicy 187<br />
6.4. Gramatyki 188<br />
6.4.1. Dygresja — gramatyka jzyka angielskiego 192<br />
6.4.2. Pisanie gramatyki 193<br />
6.5. Zamiana gramatyki w kod 194<br />
6.5.1. Implementowanie zasad gramatyki 194<br />
6.5.2. Wyraenia 195<br />
6.5.3. Skadniki 199<br />
6.5.4. Podstawowe elementy wyrae 200<br />
6.6. Wypróbowywanie pierwszej wersji 201<br />
6.7. Wypróbowywanie drugiej wersji 205<br />
6.8. Strumienie tokenów 206<br />
6.8.1. Implementacja typu Token_stream 207<br />
6.8.2. Wczytywanie tokenów 209<br />
6.8.3. Wczytywanie liczb 210<br />
6.9. Struktura programu 211<br />
Rozdzia 7. Koczenie programu 217<br />
7.1. Wprowadzenie 218<br />
7.2. Wejcie i wyjcie 218<br />
7.3. Obsuga bdów 220<br />
7.4. Liczby ujemne 224<br />
7.5. Reszta z dzielenia 225<br />
7.6. Oczyszczanie kodu 227<br />
7.6.1. Stae symboliczne 227<br />
7.6.2. Uycie funkcji 229<br />
7.6.3. Ukad kodu 230<br />
7.6.4. Komentarze 231<br />
7.7. Odzyskiwanie sprawnoci po wystpieniu bdu 233<br />
7.8. Zmienne 236<br />
7.8.1. Zmienne i definicje 236<br />
7.8.2. Wprowadzanie nazw 240<br />
7.8.3. Nazwy predefiniowane 242<br />
7.8.4. Czy to ju koniec? 243
SPIS TRECI 7<br />
Rozdzia 8. Szczegóy techniczne — funkcje itp. 247<br />
8.1. Szczegóy techniczne 248<br />
8.2. Deklaracje i definicje 249<br />
8.2.1. Rodzaje deklaracji 252<br />
8.2.2. Deklaracje staych i zmiennych 252<br />
8.2.3. Domylna inicjacja 254<br />
8.3. Pliki nagówkowe 254<br />
8.4. Zakres 256<br />
8.5. Wywoywanie i warto zwrotna funkcji 261<br />
8.5.1. Deklarowanie argumentów i typu zwrotnego 261<br />
8.5.2. Zwracanie wartoci 263<br />
8.5.3. Przekazywanie przez warto 264<br />
8.5.4. Przekazywanie argumentów przez sta referencj 265<br />
8.5.5. Przekazywanie przez referencj 267<br />
8.5.6. Przekazywanie przez warto a przez referencj 269<br />
8.5.7. Sprawdzanie argumentów i konwersja 271<br />
8.5.8. Implementacja wywoa funkcji 272<br />
8.6. Porzdek wykonywania instrukcji 276<br />
8.6.1. Wartociowanie wyrae 277<br />
8.6.2. Globalna inicjacja 277<br />
8.7. Przestrzenie nazw 279<br />
8.7.1. Dyrektywy i deklaracje using 280<br />
Rozdzia 9. Szczegóy techniczne — klasy itp. 287<br />
9.1. Typy zdefiniowane przez uytkownika 288<br />
9.2. Klasy i skadowe klas 289<br />
9.3. Interfejs i implementacja 289<br />
9.4. Tworzenie klas 291<br />
9.4.1. Struktury i funkcje 291<br />
9.4.2. Funkcje skadowe i konstruktory 293<br />
9.4.3. Ukrywanie szczegóów 294<br />
9.4.4. Definiowanie funkcji skadowych 296<br />
9.4.5. Odwoywanie si do biecego obiektu 298<br />
9.4.6. Raportowanie bdów 299<br />
9.5. Wyliczenia 300<br />
9.6. Przecianie operatorów 302<br />
9.7. Interfejsy klas 303<br />
9.7.1. Typy argumentów 304<br />
9.7.2. Kopiowanie 306<br />
9.7.3. Konstruktory domylne 306<br />
9.7.4. Stae funkcje skadowe 309<br />
9.7.5. Skadowe i funkcje pomocnicze 310<br />
9.8. Klasa Date 312
8 SPIS TRECI<br />
Cz II Wejcie i wyjcie 319<br />
Rozdzia 10. Strumienie wejcia i wyjcia 321<br />
10.1. Wejcie i wyjcie 322<br />
10.2. Model strumieni wejcia i wyjcia 323<br />
10.3. Pliki 325<br />
10.4. Otwieranie pliku 326<br />
10.5. Odczytywanie i zapisywanie plików 328<br />
10.6. Obsuga bdów wejcia i wyjcia 330<br />
10.7. Wczytywanie pojedynczej wartoci 332<br />
10.7.1. Rozoenie problemu na mniejsze czci 334<br />
10.7.2. Oddzielenie warstwy komunikacyjnej od funkcji 337<br />
10.8. Definiowanie operatorów wyjciowych 338<br />
10.9. Definiowanie operatorów wejciowych 339<br />
10.10. Standardowa ptla wejciowa 340<br />
10.11. Wczytywanie pliku strukturalnego 341<br />
10.11.1. Reprezentacja danych w pamici 342<br />
10.11.2. Odczytywanie struktur wartoci 343<br />
10.11.3. Zmienianie reprezentacji 347<br />
Rozdzia 11. Indywidualizacja operacji wejcia i wyjcia 353<br />
11.1. Regularno i nieregularno 354<br />
11.2. Formatowanie danych wyjciowych 354<br />
11.2.1. Wysyanie na wyjcie liczb cakowitych 355<br />
11.2.2. Przyjmowanie na wejciu liczb cakowitych 356<br />
11.2.3. Wysyanie na wyjcie liczb zmiennoprzecinkowych 357<br />
11.2.4. Precyzja 358<br />
11.2.5. Pola 360<br />
11.3. Otwieranie plików i pozycjonowanie 361<br />
11.3.1. Tryby otwierania plików 361<br />
11.3.2. Pliki binarne 362<br />
11.3.3. Pozycjonowanie w plikach 365<br />
11.4. Strumienie acuchowe 365<br />
11.5. Wprowadzanie danych wierszami 367<br />
11.6. Klasyfikowanie znaków 368<br />
11.7. Stosowanie niestandardowych separatorów 370<br />
11.8. Zostao jeszcze tyle do poznania 376<br />
Rozdzia 12. Model graficzny 381<br />
12.1. Czemu grafika? 382<br />
12.2. Model graficzny 383<br />
12.3. Pierwszy przykad 384
SPIS TRECI 9<br />
12.4. Biblioteka GUI 387<br />
12.5. Wspórzdne 388<br />
12.6. Figury geometryczne 388<br />
12.7. Uywanie klas figur geometrycznych 389<br />
12.7.1. Nagówki graficzne i funkcja main 390<br />
12.7.2. Prawie puste okno 390<br />
12.7.3. Klasa Axis 392<br />
12.7.4. Rysowanie wykresu funkcji 394<br />
12.7.5. Wielokty 394<br />
12.7.6. Prostokty 395<br />
12.7.7. Wypenianie kolorem 397<br />
12.7.8. Tekst 398<br />
12.7.9.Obrazy 399<br />
12.7.10. Jeszcze wicej grafik 400<br />
12.8. Uruchamianie programu 401<br />
12.8.1. Pliki ródowe 402<br />
Rozdzia 13. Klasy graficzne 407<br />
13.1. Przegld klas graficznych 408<br />
13.2. Klasy Point i Line 410<br />
13.3. Klasa Lines 412<br />
13.4. Klasa Color 414<br />
13.5. Typ Line_style 416<br />
13.6. Typ Open_polyline 418<br />
13.7. Typ Closed_polyline 419<br />
13.8. Typ Polygon 420<br />
13.9. Typ Rectangle 422<br />
13.10. Wykorzystywanie obiektów bez nazw 426<br />
13.11. Typ Text 428<br />
13.12. Typ Circle 430<br />
13.13. Typ Ellipse 431<br />
13.14. Typ Marked_polyline 433<br />
13.15. Typ Marks 434<br />
13.16. Typ Mark 435<br />
13.17. Typ Image 436<br />
Rozdzia 14. Projektowanie klas graficznych 443<br />
14.1. Zasady projektowania 444<br />
14.1.1. Typy 444<br />
14.1.2. Operacje 445<br />
14.1.3. Nazewnictwo 446<br />
14.1.4. Zmienno 448
10 SPIS TRECI<br />
14.2. Klasa Shape 448<br />
14.2.1. Klasa abstrakcyjna 450<br />
14.2.2. Kontrola dostpu 451<br />
14.2.3. Rysowanie figur 454<br />
14.2.4. Kopiowanie i zmienno 456<br />
14.3. Klasy bazowe i pochodne 458<br />
14.3.1. Ukad obiektu 459<br />
14.3.2. Tworzenie podklas i definiowanie funkcji wirtualnych 461<br />
14.3.3. Przesanianie 461<br />
14.3.4. Dostp 463<br />
14.3.5. Czyste funkcje wirtualne 464<br />
14.4. Zalety programowania obiektowego 465<br />
Rozdzia 15. Graficzne przedstawienie funkcji i danych 471<br />
15.1. Wprowadzenie 472<br />
15.2. Rysowanie wykresów prostych funkcji 472<br />
15.3. Typ Function 476<br />
15.3.1. Argumenty domylne 477<br />
15.3.2. Wicej przykadów 478<br />
15.4. Typ Axis 479<br />
15.5. Warto przybliona funkcji wykadniczej 481<br />
15.6. Przedstawianie danych na wykresach 486<br />
15.6.1. Odczyt danych z pliku 487<br />
15.6.2. Ukad ogólny 488<br />
15.6.3. Skalowanie danych 489<br />
15.6.4. Budowanie wykresu 490<br />
Rozdzia 16. Graficzne interfejsy uytkownika 497<br />
16.1. Róne rodzaje interfejsów uytkownika 498<br />
16.2. Przycisk Next 499<br />
16.3. Proste okno 500<br />
16.3.1. Funkcje zwrotne 501<br />
16.3.2. Ptla oczekujca 504<br />
16.4. Typ Button i inne pochodne typu Widget 505<br />
16.4.1. Widgety 505<br />
16.4.2. Przyciski 506<br />
16.4.3. Widgety In_box i Out_box 507<br />
16.4.4. Menu 507<br />
16.5. Przykad 508<br />
16.6. Inwersja kontroli 511<br />
16.7. Dodawanie menu 513<br />
16.8. Debugowanie kodu GUI 517
SPIS TRECI 11<br />
Cz III Dane i algorytmy 523<br />
Rozdzia 17. Wektory i pami wolna 525<br />
17.1. Wprowadzenie 526<br />
17.2. Podstawowe wiadomoci na temat typu vector 527<br />
17.3. Pami, adresy i wskaniki 529<br />
17.3.1. Operator sizeof 531<br />
17.4. Pami wolna a wskaniki 532<br />
17.4.1. Alokacja obiektów w pamici wolnej 533<br />
17.4.2. Dostp poprzez wskaniki 534<br />
17.4.3. Zakresy 535<br />
17.4.4. Inicjacja 536<br />
17.4.5. Wskanik zerowy 537<br />
17.4.6. Dealokacja pamici wolnej 538<br />
17.5. Destruktory 540<br />
17.5.1. Generowanie destruktorów 542<br />
17.5.2. Destruktory a pami wolna 542<br />
17.6. Dostp do elementów 544<br />
17.7. Wskaniki na obiekty klas 545<br />
17.8. Babranie si w typach — void* i rzutowanie 546<br />
17.9. Wskaniki i referencje 548<br />
17.9.1. Wskaniki i referencje jako parametry 549<br />
17.9.2. Wskaniki, referencje i dziedziczenie 550<br />
17.9.3. Przykad — listy 551<br />
17.9.4. Operacje na listach 552<br />
17.9.5. Zastosowania list 554<br />
17.10. Wskanik this 555<br />
17.10.1. Wicej przykadów uycia typu Link 557<br />
Rozdzia 18. Wektory i tablice 563<br />
18.1. Wprowadzenie 564<br />
18.2. Kopiowanie 564<br />
18.2.1. Konstruktory kopiujce 566<br />
18.2.2. Przypisywanie z kopiowaniem 567<br />
18.2.3. Terminologia zwizana z kopiowaniem 569<br />
18.3. Podstawowe operacje 570<br />
18.3.1. Konstruktory jawne 571<br />
18.3.2. Debugowanie konstruktorów i destruktorów 573<br />
18.4. Uzyskiwanie dostpu do elementów wektora 575<br />
18.4.1. Problem staych wektorów 576<br />
18.5. Tablice 577<br />
18.5.1. Wskaniki na elementy tablicy 578<br />
18.5.2. Wskaniki i tablice 580
12 SPIS TRECI<br />
18.5.3. Inicjowanie tablic 582<br />
18.5.4. Problemy ze wskanikami 583<br />
18.6. Przykady — palindrom 586<br />
18.6.1. Wykorzystanie acuchów 586<br />
18.6.2. Wykorzystanie tablic 587<br />
18.6.3. Wykorzystanie wskaników 588<br />
Rozdzia 19. Wektory, szablony i wyjtki 593<br />
19.1. Analiza problemów 594<br />
19.2. Zmienianie rozmiaru 596<br />
19.2.1. Reprezentacja 597<br />
19.2.2. Rezerwacja pamici i pojemno kontenera 598<br />
19.2.3. Zmienianie rozmiaru 599<br />
19.2.4. Funkcja push_back() 599<br />
19.2.5. Przypisywanie 600<br />
19.2.6. Podsumowanie dotychczasowej pracy nad typem vector 601<br />
19.3. Szablony 602<br />
19.3.1. Typy jako parametry szablonów 603<br />
19.3.2. <strong>Programowanie</strong> ogólne 605<br />
19.3.3. Kontenery a dziedziczenie 607<br />
19.3.4. Liczby cakowite jako parametry szablonów 608<br />
19.3.5. Dedukcja argumentów szablonu 610<br />
19.3.6. Uogólnianie wektora 610<br />
19.4. Sprawdzanie zakresu i wyjtki 613<br />
19.4.1. Dygresja — uwagi projektowe 614<br />
19.4.2. Wyznanie na temat makr 615<br />
19.5. Zasoby i wyjtki 617<br />
19.5.1. Potencjalne problemy z zarzdzaniem zasobami 617<br />
19.5.2. Zajmowanie zasobów jest inicjacj 619<br />
19.5.3. Gwarancje 620<br />
19.5.4. Obiekt auto_ptr 621<br />
19.5.5. Technika RAII dla wektora 622<br />
Rozdzia 20. Kontenery i iteratory 629<br />
20.1. Przechowywanie i przetwarzanie danych 630<br />
20.1.1. Praca na danych 630<br />
20.1.2. Uogólnianie kodu 631<br />
20.2. Ideay twórcy biblioteki STL 634<br />
20.3. Sekwencje i iteratory 637<br />
20.3.1. Powrót do przykadu 639<br />
20.4. Listy powizane 641<br />
20.4.1. Operacje list 642<br />
20.4.2. Iteracja 643<br />
20.5. Jeszcze raz uogólnianie wektora 645
SPIS TRECI 13<br />
20.6. Przykad — prosty edytor tekstu 647<br />
20.6.1. Wiersze 649<br />
20.6.2. Iteracja 650<br />
20.7. Typy vector, list oraz string 653<br />
20.7.1. Funkcje insert() i erase() 654<br />
20.8. Dostosowanie wektora do biblioteki STL 656<br />
20.9. Dostosowywanie wbudowanych tablic do STL 658<br />
20.10. Przegld kontenerów 660<br />
20.10.1. Kategorie iteratorów 662<br />
Rozdzia 21. Algorytmy i sowniki 667<br />
21.1. Algorytmy biblioteki standardowej 668<br />
21.2. Najprostszy algorytm — find() 669<br />
21.2.1. Kilka przykadów z programowania ogólnego 670<br />
21.3. Ogólny algorytm wyszukiwania — find_if() 671<br />
21.4. Obiekty funkcyjne 673<br />
21.4.1. Abstrakcyjne spojrzenie na obiekty funkcyjne 674<br />
21.4.2. Predykaty skadowych klas 675<br />
21.5. Algorytmy numeryczne 676<br />
21.5.1. Akumulacja 677<br />
21.5.2. Uogólnianie funkcji accumulate() 678<br />
21.5.3. Iloczyn skalarny 679<br />
21.5.4. Uogólnianie funkcji inner_product() 681<br />
21.6. Kontenery asocjacyjne 681<br />
21.6.1. Sowniki 682<br />
21.6.2. Opis ogólny kontenera map 684<br />
21.6.3. Jeszcze jeden przykad zastosowania sownika 687<br />
21.6.4. Kontener unordered_map 689<br />
21.6.5. Zbiory 691<br />
21.7. Kopiowanie 693<br />
21.7.1. Funkcja copy() 693<br />
21.7.2. Iteratory strumieni 694<br />
21.7.3. Utrzymywanie porzdku przy uyciu kontenera set 696<br />
21.7.4. Funkcja copy_if() 696<br />
21.8. Sortowanie i wyszukiwanie 697<br />
Cz IV Poszerzanie horyzontów 703<br />
Rozdzia 22. Ideay i historia 705<br />
22.1. Historia, ideay i profesjonalizm 706<br />
22.1.1. Cele i filozofie jzyków programowania 706<br />
22.1.2. Ideay programistyczne 708<br />
22.1.3. Style i paradygmaty 714
14 SPIS TRECI<br />
22.2. Krótka historia jzyków programowania 717<br />
22.2.1. Pierwsze jzyki 718<br />
22.2.2. Korzenie nowoczesnych jzyków programowania 719<br />
22.2.3. Rodzina Algol 724<br />
22.2.4. Simula 731<br />
22.2.5. C 733<br />
22.2.6. <strong>C++</strong> 736<br />
22.2.7. Dzi 738<br />
22.2.8. róda informacji 740<br />
Rozdzia 23. Przetwarzanie tekstu 745<br />
23.1. Tekst 746<br />
23.2. acuchy 746<br />
23.3. Strumienie wejcia i wyjcia 750<br />
23.4. Sowniki 750<br />
23.4.1. Szczegóy implementacyjne 755<br />
23.5. Problem 757<br />
23.6. Wyraenia regularne 759<br />
23.7. Wyszukiwanie przy uyciu wyrae regularnych 761<br />
23.8. Skadnia wyrae regularnych 764<br />
23.8.1. Znaki i znaki specjalne 764<br />
23.8.2. Rodzaje znaków 765<br />
23.8.3. Powtórzenia 766<br />
23.8.4. Grupowanie 767<br />
23.8.5. Alternatywa 767<br />
23.8.6. Zbiory i przedziay znaków 768<br />
23.8.7. Bdy w wyraeniach regularnych 769<br />
23.9. Dopasowywanie przy uyciu wyrae regularnych 770<br />
23.10. róda 775<br />
Rozdzia 24. Dziaania na liczbach 779<br />
24.1. Wprowadzenie 780<br />
24.2. Rozmiar, precyzja i przekroczenie zakresu 780<br />
24.2.1. Ograniczenia typów liczbowych 783<br />
24.3. Tablice 784<br />
24.4. Tablice wielowymiarowe w stylu jzyka C 785<br />
24.5. Biblioteka Matrix 786<br />
24.5.1. Wymiary i dostp 787<br />
24.5.2. Macierze jednowymiarowe 789<br />
24.5.3. Macierze dwuwymiarowe 792<br />
24.5.4. Wejcie i wyjcie macierzy 794<br />
24.5.5. Macierze trójwymiarowe 795
SPIS TRECI 15<br />
24.6. Przykad — rozwizywanie równa liniowych 796<br />
24.6.1. Klasyczna eliminacja Gaussa 797<br />
24.6.2. Wybór elementu centralnego 798<br />
24.6.3. Testowanie 799<br />
24.7. Liczby losowe 800<br />
24.8. Standardowe funkcje matematyczne 802<br />
24.9. Liczby zespolone 803<br />
24.10. róda 804<br />
Rozdzia 25. <strong>Programowanie</strong> systemów wbudowanych 809<br />
25.1. Systemy wbudowane 810<br />
25.2. Podstawy 813<br />
25.2.1. Przewidywalno 815<br />
25.2.2. Ideay 815<br />
25.2.3. ycie z awari 816<br />
25.3. Zarzdzanie pamici 818<br />
25.3.1. Problemy z pamici woln 819<br />
25.3.2. Alternatywy dla ogólnej pamici wolnej 822<br />
25.3.3. Przykad zastosowania puli 823<br />
25.3.4. Przykad uycia stosu 824<br />
25.4. Adresy, wskaniki i tablice 825<br />
25.4.1. Niekontrolowane konwersje 825<br />
25.4.2. Problem — le dziaajce interfejsy 826<br />
25.4.3. Rozwizanie — klasa interfejsu 829<br />
25.4.4. Dziedziczenie a kontenery 832<br />
25.5. Bity, bajty i sowa 834<br />
25.5.1. Bity i operacje na bitach 835<br />
25.5.2. Klasa bitset 839<br />
25.5.3. Liczby ze znakiem i bez znaku 840<br />
25.5.4. Manipulowanie bitami 844<br />
25.5.5. Pola bitowe 846<br />
25.5.6. Przykad — proste szyfrowanie 847<br />
25.6. Standardy pisania kodu 851<br />
25.6.1. Jaki powinien by standard kodowania 852<br />
25.6.2. Przykadowe zasady 854<br />
25.6.3. Prawdziwe standardy kodowania 859<br />
Rozdzia 26. Testowanie 865<br />
26.1. Czego chcemy 866<br />
26.1.1. Zastrzeenie 867<br />
26.2. Dowody 867<br />
26.3. Testowanie 867<br />
26.3.1. Testowanie regresyjne 868<br />
26.3.2. Testowanie jednostkowe 869
16 SPIS TRECI<br />
26.3.3. Algorytmy i nie-algorytmy 875<br />
26.3.4. Testy systemowe 882<br />
26.3.5. Testowanie klas 886<br />
26.3.6. Znajdowanie zaoe, które si nie potwierdzaj 889<br />
26.4. Projektowanie pod ktem testowania 890<br />
26.5. Debugowanie 891<br />
26.6. Wydajno 891<br />
26.6.1. Kontrolowanie czasu 893<br />
26.7. róda 895<br />
Rozdzia 27. Jzyk C 899<br />
27.1. C i <strong>C++</strong> to rodzestwo 900<br />
27.1.1. Zgodno jzyków C i <strong>C++</strong> 901<br />
27.1.2. Co jest w jzyku <strong>C++</strong>, czego nie ma w C 903<br />
27.1.3. Biblioteka standardowa jzyka C 904<br />
27.2. Funkcje 905<br />
27.2.1. Brak moliwoci przeciania nazw funkcji 906<br />
27.2.2. Sprawdzanie typów argumentów funkcji 906<br />
27.2.3. Definicje funkcji 907<br />
27.2.4. Wywoywanie C z poziomu <strong>C++</strong> i <strong>C++</strong> z poziomu C 909<br />
27.2.5. Wskaniki na funkcje 911<br />
27.3. Mniej wane rónice midzy jzykami 912<br />
27.3.1. Przestrze znaczników struktur 912<br />
27.3.2. Sowa kluczowe 913<br />
27.3.3. Definicje 914<br />
27.3.4. Rzutowanie w stylu jzyka C 915<br />
27.3.5. Konwersja typu void* 916<br />
27.3.6. Typ enum 917<br />
27.3.7. Przestrzenie nazw 917<br />
27.4. Pami wolna 918<br />
27.5. acuchy w stylu jzyka C 919<br />
27.5.1. acuchy w stylu jzyka C i const 922<br />
27.5.2. Operacje na bajtach 922<br />
27.5.3. Przykad — funkcja strcpy() 923<br />
27.5.4. Kwestia stylu 923<br />
27.6. Wejcie i wyjcie — nagówek stdio 924<br />
27.6.1. Wyjcie 924<br />
27.6.2. Wejcie 925<br />
27.6.3. Pliki 927<br />
27.7. Stae i makra 927<br />
27.8. Makra 928<br />
27.8.1. Makra podobne do funkcji 929<br />
27.8.2. Makra skadniowe 930<br />
27.8.3. Kompilacja warunkowa 931<br />
27.9. Przykad — kontenery intruzyjne 932
SPIS TRECI 17<br />
Dodatki 941<br />
Dodatek A Zestawienie wasnoci jzyka 943<br />
A.1. Opis ogólny 944<br />
A.2. Literay 946<br />
A.3. Identyfikatory 950<br />
A.4. Zakres, pami oraz czas trwania 950<br />
A.5. Wyraenia 953<br />
A.6. Instrukcje 962<br />
A.7. Deklaracje 964<br />
A.8. Typy wbudowane 965<br />
A.9. Funkcje 968<br />
A.10. Typy zdefiniowane przez uytkownika 971<br />
A.11. Wyliczenia 972<br />
A.12. Klasy 972<br />
A.13. Szablony 983<br />
A.14. Wyjtki 986<br />
A.15. Przestrzenie nazw 988<br />
A.16. Aliasy 988<br />
A.17. Dyrektywy preprocesora 989<br />
Dodatek B Biblioteka standardowa 991<br />
B.1. Przegld 992<br />
B.2. Obsuga bdów 995<br />
B.3. Iteratory 997<br />
B.4. Kontenery 1001<br />
B.5. Algorytmy 1008<br />
B.6. Biblioteka STL 1016<br />
B.7. Strumienie wejcia i wyjcia 1018<br />
B.8. Przetwarzanie acuchów 1024<br />
B.9. Obliczenia 1028<br />
B.10. Funkcje biblioteki standardowej C 1032<br />
B.11. Inne biblioteki 1040<br />
Dodatek C Podstawy rodowiska Visual Studio 1043<br />
C.1. Uruchamianie programu 1044<br />
C.2. Instalowanie rodowiska Visual Studio 1044<br />
C.3. Tworzenie i uruchamianie programu 1044<br />
C.4. Póniej 1046
18 SPIS TRECI<br />
Dodatek D Instalowanie biblioteki FLTK 1047<br />
D.1. Wprowadzenie 1048<br />
D.2. Pobieranie biblioteki FLTK z internetu 1048<br />
D.3. Instalowanie biblioteki FLTK 1048<br />
D.4. Korzystanie z biblioteki FLTK w Visual Studio 1049<br />
D.5. Sprawdzanie, czy wszystko dziaa 1050<br />
Dodatek E Implementacja GUI 1051<br />
E.1. Implementacja wywoa zwrotnych 1052<br />
E.2. Implementacja klasy Widget 1053<br />
E.3. Implementacja klasy Window 1054<br />
E.4. Klasa Vector_ref 1055<br />
E.5. Przykad — widgety 1056<br />
Sowniczek 1059<br />
Bibliografia 1065<br />
Skorowidz 1069<br />
Zdjcia 1105
6<br />
Pisanie programu<br />
Pisa program, znaczy rozumie.<br />
— Kristen Nygaard<br />
P<br />
isanie programu polega na stopniowym modyfikowaniu swojego wyobraenia<br />
na temat tego, co si chce zrobi i jak si chce to wyrazi. W tym i nastpnym<br />
rozdziale zbudujemy program. Zaczniemy od pierwszego mglistego pomysu,<br />
przejdziemy etapy analizy, projektowania, implementacji, testowania, ponownego<br />
projektowania, na ponownej implementacji koczc. Chcemy pokaza<br />
proces mylowy, który ma miejsce podczas tworzenia oprogramowania. W midzyczasie<br />
omówimy organizacj programu, typy definiowane przez uytkownika<br />
oraz techniki przetwarzania danych wejciowych.<br />
6.1. Problem<br />
6.2. Przemylenie problemu<br />
6.2.1. Etapy rozwoju oprogramowania<br />
6.2.2. Strategia<br />
6.3. Wracajc do kalkulatora<br />
6.3.1. Pierwsza próba<br />
6.3.2. Tokeny<br />
6.3.3. Implementowanie tokenów<br />
6.3.4. Uywanie tokenów<br />
6.3.5. Powrót do tablicy<br />
6.4. Gramatyki<br />
6.4.1. Dygresja — gramatyka jzyka angielskiego<br />
6.4.2. Pisanie gramatyki<br />
6.5. Zamiana gramatyki w kod<br />
6.5.1. Implementowanie zasad gramatyki<br />
6.5.2. Wyraenia<br />
6.5.3. Skadniki<br />
6.5.4. Podstawowe elementy wyrae<br />
6.6. Wypróbowywanie pierwszej wersji<br />
6.7. Wypróbowywanie drugiej wersji<br />
6.8. Strumienie tokenów<br />
6.8.1. Implementacja typu Token_stream<br />
6.8.2. Wczytywanie tokenów<br />
6.8.3. Wczytywanie liczb<br />
6.9. Struktura programu<br />
175
176 ROZDZIA 6 • PISANIE PROGRAMU<br />
6.1. Problem<br />
Pisanie programu zaczyna si od postawienia problemu. To znaczy, jest problem, do rozwizania<br />
którego chcemy napisa program. Aby ten program by dobry, naley dokadnie zrozumie<br />
problem. Jeli program bdzie rozwizywa nie ten problem, co trzeba, to nie bdzie przydatny<br />
bez wzgldu na to, jak moe by elegancki. Czasami zdarzaj si szczliwe przypadki, e program<br />
spenia jakie przypadkowe poyteczne zadanie, mimo e zosta napisany w cakiem innym<br />
celu. Lepiej jednak nie liczy na takie szczcie. My chcemy napisa taki program, który bdzie<br />
w prosty i jasny sposób rozwizywa dokadnie ten problem, dla którego zosta napisany.<br />
Jaki program byoby najlepiej napisa na tym etapie nauki? Taki, który:<br />
Ilustruje techniki projektowania i pisania programów.<br />
Umoliwia zapoznanie si z charakterem decyzji, które programista musi podejmowa,<br />
oraz implikacjami, które te decyzje pocigaj.<br />
Nie wymaga zastosowania zbyt wielu nowych konstrukcji programistycznych.<br />
Jest wystarczajco skomplikowany, aby zmusi nas do przemylenia jego projektu.<br />
Mona napisa na kilka sposobów.<br />
Rozwizuje atwy do zrozumienia problem.<br />
Rozwizuje problem wart uwagi.<br />
Jest na tyle may, e mona go w caoci przedstawi i zrozumie.<br />
Wybór pad na program „zmuszajcy komputer do wykonywania typowych dziaa arytmetycznych<br />
na wyraeniach, które mu podamy” — tzn. chcemy napisa prosty kalkulator. Programy<br />
tego typu s uyteczne. Mona je znale w kadym komputerze biurkowym, a nawet<br />
mona znale takie komputery, które pozwalaj uruchamia tylko tego typu programy —<br />
kalkulatory kieszonkowe.<br />
Jeli np. wpiszemy<br />
2+3.1*4<br />
bdziemy oczekiwa, e program zwróci wynik<br />
14.4<br />
Niestety kalkulator taki nie bdzie mia adnych funkcji, których ju nie spenia nasz komputer,<br />
ale to by byo zbyt wysokie wymaganie wobec pierwszego programu.<br />
6.2. Przemylenie problemu<br />
Od czego wic zacz? Pomyl o postawionym problemie i tym, jak go rozwiza. Po pierwsze<br />
zdecyduj si, co program ma robi i w jaki sposób bdziesz si z nim komunikowa. Póniej<br />
bdziesz móg zastanowi si nad tym, jak napisa odpowiedni kod. Opisz wstpn wersj<br />
swojego rozwizania i zastanów si, co naley poprawi. Moesz spróbowa przedyskutowa<br />
problem i jego rozwizanie z koleg lub koleank. Próba przedstawienia komu wasnych<br />
myli jest doskonaym sposobem na znalezienie sabych punktów wasnego rozumowania,
6.2. PRZEMYLENIE PROBLEMU 177<br />
nawet lepszym ni napisanie ich. Papier (lub komputer) nie bdzie z Tob dyskutowa i krytycznie<br />
ocenia Twoich pogldów. Idealny etap projektowania to taki, który przeprowadza si<br />
w towarzystwie.<br />
Niestety nie ma takiej ogólnej strategii rozwizywania problemów, która odpowiadaaby<br />
wszystkim ludziom i pozwalaa rozwiza kady problem. Istnieje mnóstwo ksiek, których<br />
autorzy twierdz, e pomog Ci efektywniej rozwizywa problemy, oraz caa masa publikacji<br />
na temat projektowania programów. Nasza droga nie wiedzie poprzez nie. W zamian opiszemy<br />
gar ogólnych wskazówek, które mog by pomocne w rozwizywaniu mniejszych problemów.<br />
Nastpnie szybko przejdziemy do wypróbowywania tych sugestii na naszym maym kalkulatorze.<br />
Czytajc tre naszych rozwaa na temat kalkulatora, oceniaj to, co widzisz, bardzo sceptycznym<br />
okiem. Aby zachowa realizm, przedstawimy ewolucj naszego programu poprzez<br />
szereg wersji. Opiszemy argumenty, które doprowadziy nas do powstania kadej z nich. Oczywicie<br />
znaczna cz tej argumentacji musi by niekompletna lub bdna, inaczej szybko bymy<br />
skoczyli ten rozdzia. Naszym celem jest pokazanie przykadowych problemów i procesów<br />
mylowych charakterystycznych dla procesu projektowania i implementowania programu.<br />
Wersja, z której jestemy ostatecznie zadowoleni, zostanie przedstawiona dopiero na kocu<br />
nastpnego rozdziau.<br />
Pamitaj, e w tym i nastpnym rozdziale proces dochodzenia do finalnej wersji programu<br />
— droga wiodca przez niepene rozwizania, pomysy i bdy — jest co najmniej tak<br />
samo wany, jak wersja ostateczna i waniejszy ni narzdzia techniczne jzyka programowania,<br />
których bdziemy uywa (wrócimy do nich póniej).<br />
6.2.1. Etapy rozwoju oprogramowania<br />
Poniej przedstawimy nieco terminologii zwizanej z tworzeniem oprogramowania. Pracujc<br />
nad rozwizaniem problemu, wielokrotnie powtarza si nastpujce fazy:<br />
Analiza — przemyl, co masz zrobi, i opisz, jak to aktualnie rozumiesz. Taki opis nazywa<br />
si zestawem wymaga lub specyfikacj. Nie bdziemy szczegóowo opisywa technik<br />
opracowywania i opisywania takich wymogów. Temat tej ksiki tego nie obejmuje, ale<br />
naley pamita, e w miar zwikszania si rozmiaru problemu techniki te nabieraj<br />
wagi.<br />
Projektowanie — utworzenie ogólnej struktury systemu i podjcie decyzji, na jakie<br />
czci podzieli implementacj oraz jak powinny si one ze sob komunikowa. W ramach<br />
projektowania zastanów si jakie narzdzia — np. biblioteki — moesz wykorzysta<br />
do opracowania struktury programu.<br />
Implementacja — napisz kod, usu bdy oraz sprawd za pomoc testów, czy robi to,<br />
co powinien.<br />
6.2.2. Strategia<br />
Oto gar wskazówek, które — jeli zostan zastosowane z rozwag i wyobrani — bd pomocne<br />
w wielu projektach programistycznych:<br />
Jaki problem jest do rozwizania? Przede wszystkim naley stara si dokadnie opisa,<br />
co chce si zrobi. Najczciej oznacza to sporzdzenie opisu problemu lub, jeli kto
178 ROZDZIA 6 • PISANIE PROGRAMU<br />
inny da nam zadanie do wykonania, prób odszyfrowania, co dokadnie to oznacza.<br />
W tym momencie naley przyj punkt widzenia uytkownika (nie programisty czy<br />
implementatora). To znaczy, zamiast zastanawia si, jak rozwiza problem, pomyl, co<br />
program ma w ogóle robi. Zadawaj pytania typu: „Co ten program moe dla mnie zrobi?”<br />
albo „W jaki sposób chciabym komunikowa si z tym programem?”. Pamitaj, e<br />
wikszo z nas moe korzysta z wasnego bogatego dowiadczenia jako uytkownika<br />
komputerów.<br />
Czy problem zosta jasno nakrelony? W realnym wiecie nigdy nie jest. Nawet<br />
w takim wiczeniu trudno jest opisa go wystarczajco precyzyjnie. Dlatego staramy si<br />
wszystko wyjani. Szkoda by byo, gdybymy rozwizali nie ten problem, co trzeba.<br />
Inna puapka to wygórowane wymagania. Obmylajc, co bymy chcieli, atwo moemy<br />
ulec chciwoci lub nadmiernym ambicjom. Zawsze lepiej jest obniy wymagania,<br />
aby uatwi napisanie specyfikacji programu, jego zrozumienie, uytkowanie oraz<br />
(tak trzeba mie nadziej) implementacj. Gdy zadziaa, zawsze mona napisa<br />
wzbogacon wersj 2.0.<br />
Czy problem wydaje si moliwy do rozwizania w przewidzianym czasie oraz przy<br />
okrelonym zasobie umiejtnoci i dostpnych narzdziach? Nie ma sensu rozpoczyna<br />
projektu, którego nie mamy szans ukoczy. Jeli jest za mao czasu na implementacj<br />
(wcznie z testowaniem) wszystkich wymaganych funkcji programu,<br />
zazwyczaj lepiej jest go w ogóle nie zaczyna. Lepiej zamiast tego zdoby wicej<br />
zasobów (zwaszcza czasu) lub (idealnie) zmieni wymagania, aby uatwi zadanie.<br />
Spróbuj podzieli program na dajce si ogarn mylami czci. Nawet najmniejszy<br />
program rozwizujcy realny problem mona podzieli na czci.<br />
Znasz jakie narzdzia, biblioteki itp., które mog by pomocne? Odpowied prawie<br />
zawsze brzmi: tak. Od samego pocztku nauki programowania masz do dyspozycji<br />
zawarto standardowej biblioteki <strong>C++</strong>. Póniej poznasz znaczn jej cz<br />
i dowiesz si, jak poszukiwa jeszcze wicej. Bdziesz korzysta z bibliotek graficznych,<br />
macierzy itp. Majc odrobin dowiadczenia, bdziesz w stanie znale tysice<br />
bibliotek w internecie. Pamitaj — nie ma sensu wywaa otwartych drzwi, jeli<br />
tworzy si oprogramowanie do realnego uytku. Co innego w czasie nauki programowania.<br />
Wówczas takie dziaania w celu sprawdzenia, jak to zrobili inni, maj<br />
gboki sens. Cay czas, który oszczdzisz dziki wykorzystaniu istniejcych bibliotek,<br />
moesz powici na prac nad innymi czciami problemu albo na odpoczynek.<br />
Skd wiadomo, czy dana biblioteka spenia nasze wymagania i prezentuje<br />
odpowiedni jako? To trudne pytanie. Mona spyta znajomych, popyta na<br />
grupach dyskusyjnych i wypróbowa kilka krótkich przykadów, zanim si zdecydujemy.<br />
Wyodrbnij takie czci rozwizania, które mona opisa oddzielnie od reszty (i potencjalnie<br />
wykorzysta w kilku miejscach programu, a nawet w innych programach).<br />
Umiejtno znajdowania takich czci przychodzi z dowiadczeniem, dlatego w ksice<br />
tej przedstawiamy wiele takich przykadów. Uywalimy ju wektorów, acuchów<br />
i strumieni (cin i cout). W tym rozdziale po raz pierwszy przedstawimy przykady<br />
projektów, implementacji i wykorzystania czci programów dostpnych jako typy
6.3. WRACAJC DO KALKULATORA 179<br />
zdefiniowane przez uytkownika (Token i Token_stream). Jeszcze wicej takich przykadów<br />
znajduje si w rozdziaach 8. oraz 13. – 15., w których zostan opisane techniki<br />
ich tworzenia. Na razie zastanów si nad tak analogi — gdybymy projektowali<br />
samochód, zaczlibymy od opisu jego podzespoów, takich jak koa, silnik, fotele,<br />
klamki do drzwi itp. Kad z nich wyprodukowalibymy osobno, aby nastpnie<br />
uy jej do zoenia caego pojazdu. W nowoczesnym samochodzie wykorzystuje<br />
si dziesitki tysicy takich czci. Realne programy nie róni si w niczym od<br />
samochodów pod tym wzgldem (poza tym, e czciami s fragmenty kodu). Nie<br />
przyszo by nam do gowy budowa samochodu z surowych materiaów, jak elazo,<br />
plastik i drewno. Analogicznie nie tworzymy powanych programów, bezporednio<br />
uywajc wyrae, instrukcji typów wbudowanych w jzyk. Projektowanie i implementowanie<br />
takich czci jest najwaniejszym tematem tej ksiki, a nawet w ogólne<br />
programowania. Zajrzyj te do rozdziaów 9. (typy definiowane przez uytkownika),<br />
14. (hierarchie klas) oraz 20. (typy ogólne).<br />
Zbuduj niewielk wersj programu z ograniczon funkcjonalnoci, która rozwizuje<br />
najwaniejsz cz problemu. Rzadko si zdarza, abymy od samego pocztku dokadnie<br />
znali charakter problemu. Czsto nam si tak wydaje (chyba wiadomo, co to jest program<br />
kalkulator?), ale w rzeczywistoci jest inaczej. Tylko dziki wytonemu myleniu<br />
o problemie (analiza) i eksperymentowaniu (projekt i implementacja) mona na tyle<br />
dokadnie zrozumie problem, aby napisa dobry program. Dlatego tworzymy ograniczon<br />
wersj, aby:<br />
Odkry wasne braki w rozumowaniu, pojmowaniu problemu oraz narzdziach.<br />
Sprawdzi, czy nie trzeba zmieni niektórych szczegóów w specyfikacji problemu,<br />
aby zadanie byo wykonalne. Rzadko si zdarza, aby w czasie analizy i we wstpnej<br />
fazie projektowania udao si przewidzie wszystkie moliwe trudnoci. Naley<br />
skorzysta z informacji, które zyskujemy podczas pisania i testowania kodu.<br />
Tak wstpn wersj o ograniczonej funkcjonalnoci czasami nazywa si prototypem.<br />
Jeli (co jest prawdopodobne) ta pierwsza wersja nie dziaa lub jest tak brzydka, e nie<br />
mamy ochoty si ni duej zajmowa, wyrzucamy j i tworzymy nowy prototyp, tym razem<br />
wzbogaceni o nowe dowiadczenia. Powtarzamy ten proces a do uzyskania zadowalajcego<br />
wyniku. Nie kontynuuj pracy w baaganie, który z czasem tylko si pogorszy.<br />
Zbuduj pen wersj, najlepiej wykorzystujc w tym celu czci z wersji wstpnej.<br />
Chodzi o to, aby program tworzy z dziaajcych czci, a nie pisa cay kod na raz.<br />
Mona oczywicie mie nadziej, e jakim cudem nieprzetestowane fragmenty kodu<br />
bd dziaa i na dodatek robi to, co zaplanowano.<br />
6.3. Wracajc do kalkulatora<br />
W jaki sposób bdziemy komunikowa si z kalkulatorem? To atwe — wiemy ju, jak posugiwa<br />
si strumieniami cin i cout, a graficzne interfejsy uytkownika (GUI) zostan opisane<br />
dopiero w rozdziale 16. W zwizku z tym wybór pad na okno konsoli. Program bdzie pobiera<br />
z klawiatury wyraenia, oblicza ich warto i drukowa wynik na ekranie. Na przykad:
180 ROZDZIA 6 • PISANIE PROGRAMU<br />
Wyraenie: 2+2<br />
Wynik: 4<br />
Wyraenie: 2+2*3<br />
Wynik: 8<br />
Wyraenie: 2+3–25/5<br />
Wynik: 0<br />
Wyraenia, tzn. 2+2 i 2+2*3, powinien wpisywa uytkownik. Reszta naley do programu. Wy-<br />
wietlenie sowa Wyraenie: bdzie zacht dla uytkownika do wpisania wyraenia. Mogliby-<br />
my napisa Prosz wpisa wyraenie i znak nowego wiersza:, ale to wydawao nam si zbyt rozwleke.<br />
Z drugiej strony taki przyjemny znaczek > byby chyba za bardzo tajemniczy. Takie<br />
szkicowanie przykadów uycia we wczesnej fazie pracy jest bardzo wane. Dziki temu<br />
mona si dowiedzie, jaki jest minimalny zestaw funkcji programu. W projektowaniu i analizie<br />
przykady takie nazywaj si przypadkami uycia.<br />
Wikszo ludzi, którzy po raz pierwszy stykaj si z problemem kalkulatora, wpada na<br />
nastpujcy pomys, jeli chodzi o gówn logik programu:<br />
wczytaj_wiersz<br />
oblicz // wykonuje prac<br />
wydrukuj_wynik<br />
Takie zapiski to oczywicie nie jest prawdziwy kod, tylko tzw. pseudokod. Stosuje si go we<br />
wczesnych fazach projektowania, gdy nie ma jeszcze pewnoci co do tego, jak zastosowa notacj.<br />
Np., czy obliczenia ma by wywoaniem funkcji? Jeli tak, to jakie bdzie przyjmowa<br />
argumenty? Jest po prostu za wczenie na zadawanie takich pyta.<br />
6.3.1. Pierwsza próba<br />
Na tym etapie nie jestemy jeszcze gotowi napisa programu kalkulatora. Nie przemyleli-<br />
my jeszcze wszystkiego, ale mylenie to cika praca i — jak wikszo programistów — nie<br />
moemy si doczeka, eby ju co napisa. Spróbujemy wic swoich si i napiszemy prosty<br />
kalkulator, aby zobaczy, do czego nas to doprowadzi. Nasz pierwszy pomys wyglda tak:<br />
#include "std_lib_facilities.h"<br />
int main()<br />
{<br />
cout >lval>>op>>rval; // Wczytuje co w rodzaju 1 + 3.<br />
if (op=='+')<br />
res = lval + rval; // dodawanie<br />
else if (op=='–')<br />
res = lval – rval; // odejmowanie<br />
cout
6.3. WRACAJC DO KALKULATORA 181<br />
Wczytujemy par wartoci oddzielonych operatorem, np. 2+2, obliczamy wynik (tu 4) i drukujemy<br />
go na ekranie. Zmienn przechowujc warto z lewej strony operatora nazwalimy<br />
lval, a z prawej strony rval.<br />
To nawet dziaa! Co z tego, e program nie jest ukoczony? To wspaniae uczucie zrobi<br />
co, co dziaa! Moe to programowanie i informatyka s atwiejsze, ni gosz plotki? Moliwe,<br />
ale nie dajmy si ponie emocjom z powodu tego pierwszego sukcesu. Oto lista czynnoci:<br />
1. Oczyci kod.<br />
2. Doda obsug dzielenia i mnoenia (np. 2*3).<br />
3. Doda obsug wyrae zawierajcych wicej ni jeden operand (np. 1+2+3).<br />
W szczególnoci pamitamy, e zawsze naley sprawdza, czy uytkownik poda sensowne<br />
dane (zapomnielimy z popiechu wczeniej), oraz e porównywanie jednej wartoci z wieloma<br />
staymi lepiej wykona za pomoc instrukcji switch ni if.<br />
czenie dziaa w acuchy, np. 1+2+3+4, obsuymy, sumujc wartoci w czasie wczytywania.<br />
Tzn. wczytujemy 1, widzimy +2, wic dodajemy 2 do 1 (uzyskujc w ten sposób wynik<br />
3). Dalej widzimy +3, a wic dodajemy 3 do poprzedniego wyniku itd. Po kilku falstartach<br />
i poprawieniu kilku bdów skadni uzyskalimy nastpujcy rezultat:<br />
#include "std_lib_facilities.h"<br />
int main()<br />
{<br />
cout >lval; // Wczytywanie pierwszego argumentu wyraenia z lewej.<br />
if (!cin) error("Na pocztku nie ma argumentu wyraenia.");<br />
while (cin>>op) { // Wczytywanie operatora i prawego argumentu wyraenia na zmian.<br />
cin>>rval;<br />
if (!cin) error("Nie ma drugiego argumentu wyraenia.");<br />
switch(op) {<br />
case '+':<br />
lval += rval; // Dodawanie: lval = lval + rval<br />
break;<br />
case '–':<br />
lval –= rval; // Odejmowanie: lval = lval – rval<br />
break;<br />
case '*':<br />
lval *= rval; // Mnoenie: lval = lval · rval<br />
break;<br />
case '/':<br />
lval /= rval; // Dzielenie: lval = lval / rval<br />
break;<br />
default: // Koniec operatorów — drukowanie wyniku.<br />
cout
182 ROZDZIA 6 • PISANIE PROGRAMU<br />
}<br />
}<br />
error("Nieprawidowe wyraenie.");<br />
Wyglda niele, ale gdy wpiszemy wyraenie 1+2*3, to ujrzymy wynik 9 zamiast 7, którego<br />
spodziewalibymy si na podstawie wiedzy zdobytej w szkole podstawowej. Analogicznie<br />
wynikiem wyraenia 1-2*3 bdzie -3 zamiast spodziewanego -5. Kalkulator wykonuje dziaania<br />
w zej kolejnoci — wyraenie 1+2*3 jest liczone jako (1+2)*3 zamiast 1+(2*3). Analogicznie<br />
1-2*3 jest liczone jako (1-2)*3 zamiast 1-(2*3). Lipa! Moglibymy uzna, e zasada, i „mnoenie<br />
wie mocniej ni dodawanie” jest gupi i przestarza konwencj, ale nie moemy zignorowa<br />
wielowiekowej tradycji, aby uatwi sobie programowanie.<br />
6.3.2. Tokeny<br />
Musimy zatem znale sposób na wczytywanie czci wiersza „na zapas”, aby sprawdzi, czy<br />
nie ma tam gdzie operatora * (albo /). Jeli jest, musimy zmieni kolejno wykonywania dziaa.<br />
Niestety próbujc wczyta nieco danych z wyprzedzeniem, napotkamy kilka trudnoci:<br />
1. Nie wymagamy, aby wyraenie znajdowao si w jednym wierszu. Na przykad ponisze<br />
te jest poprawne:<br />
1<br />
+<br />
2<br />
2. Jak znale znaki * i / wród cyfr i plusów w kilku wierszach danych wejciowych?<br />
3. Jak zapamita, gdzie znajdowa si znaleziony znak *?<br />
4. Jak wykona obliczenia, które nie s cile typu „od lewej do prawej”?<br />
Postanowilimy by wielkimi optymistami i zaj si tylko punktami 1 – 3. Ostatnim zajmiemy<br />
si troch póniej.<br />
Poszukamy pomocy. Przecie kto na pewno zna typowy sposób wczytywania danych typu<br />
liczby i operatory i zapisywania ich w taki sposób, aby mona je byo atwo wykorzysta w obliczeniach.<br />
Ta konwencjonalna i przydatna technika nazywa si rozbiorem na skadniki, czyli<br />
tokeny (ang. tokenize) — wczytuje si dane i dzieli je na tokeny. Na przykad wyraenie<br />
45+11.5/7<br />
zostaoby rozoone na tokeny<br />
45<br />
+<br />
11.5<br />
/<br />
7<br />
Token to sekwencja znaków, która reprezentuje pewn cao, np. liczb lub operator. Kompilator<br />
<strong>C++</strong> dzieli na tokeny kod ródowy. W istocie róne formy rozkadu na czynniki s<br />
podstaw analizy wikszoci rodzajów tekstów. W wyraeniach matematycznych w jzyku<br />
<strong>C++</strong> potrzebujemy trzech rodzajów tokenów:
6.3. WRACAJC DO KALKULATORA 183<br />
literay zmiennoprzecinkowe — zgodnie z definicj w <strong>C++</strong>, np. 3.14, 0.274e2 i 42;<br />
operatory — +, -, *, / oraz %;<br />
nawiasy — ( i ).<br />
Wydaje si, e trudnoci mog nastrcza literay zmiennoprzecinkowe. Wczytanie liczby 12<br />
wydaje si znacznie atwiejsze ni 12.3e-3. Ale kalkulatory zazwyczaj wykonuj dziaania na<br />
liczbach zmiennoprzecinkowych. Analogicznie podejrzewamy, e aby nasz kalkulator by przydatny,<br />
musi obsugiwa nawiasy.<br />
W jaki sposób reprezentuje si takie tokeny w programie? Mona spróbowa zapamitywa,<br />
gdzie kady token si zaczyna, a gdzie koczy, ale to moe by uciliwe (zwaszcza jeli<br />
pozwolimy na wpisywanie wyrae obejmujcych wicej ni jeden wiersz). Dodatkowo, jeli<br />
bdziemy zapisywa wartoci jako sekwencje znaków, bdziemy musieli póniej znale<br />
sposób na odczytanie tych wartoci. To znaczy, jeli liczb 42 zapiszemy jako znaki 4 i 2, póniej<br />
bdziemy musieli odgadn, e te dwa znaki reprezentuj liczb 42 (tzn. 4*10+2). Oczywistym<br />
i konwencjonalnym rozwizaniem tego problemu jest przedstawienie kadego tokenu<br />
jako pary (rodzaj, warto). Pierwszy element informuje o rodzaju tokenu — liczba, operator,<br />
nawias. Drugi natomiast np. w przypadku liczb okrela dokadn warto.<br />
Jak wic wykorzysta pomys par (rodzaj, warto) w kodzie? Zdefiniujemy typ Token<br />
do reprezentowania tokenów. Po co? Przypomnij sobie, po co uywamy typów: przechowuj<br />
potrzebne nam dane i pozwalaj wykonywa na nich róne operacje. Na przykad typ int pozwala<br />
przechowywa liczby cakowite i umoliwia dodawanie, odejmowanie, mnoenie oraz dzielenie<br />
tych liczb. Natomiast typ string przechowuje acuchy znaków i pozwala je np. czy. W jzyku<br />
<strong>C++</strong> i jego bibliotece standardowej dostpnych jest wiele typów, np. char, int, double,<br />
string, vector i ostream. Nie ma jednak typu Token. W istocie mona wymieni mnóstwo typów<br />
— tysice, a nawet dziesitki tysicy — które chcielibymy mie do dyspozycji, a których<br />
nie ma w jzyku ani jego bibliotece standardowej. Do naszych ulubionych typów, które<br />
nie s standardowo dostpne, nale Matrix (zobacz rozdzia 24.), Date (zobacz rozdzia 9.) oraz<br />
reprezentujce go liczby cakowite nieskoczonej precyzji (poszukaj w internecie informacji na<br />
temat typu Bignum). Jeli przemylisz to, dojdziesz do wniosku, e jzyk nie moe standardowo<br />
obsugiwa dziesitek tysicy typów — kto by je zdefiniowa i zaimplementowa, kto by<br />
je potem znalaz, nie mówic ju o tym, jak gruby musiaby by podrcznik do nauki takiego<br />
jzyka. Jzyk <strong>C++</strong> wzorem innych nowoczesnych jzyków programowania rozwizuje ten<br />
problem, pozwalajc uytkownikowi definiowa wasne (niestandardowe) typy (ang. userdefined<br />
type — typ zdefiniowany przez uytkownika).<br />
6.3.3. Implementowanie tokenów<br />
Jak powinien wyglda token? To znaczy, jakie waciwoci powinien mie nasz typ Token?<br />
Musi nadawa si do reprezentowania operatorów (np. + i -) i wartoci liczbowych (np. 42 i 3.14).<br />
Oczywistym rozwizaniem jest zaimplementowanie czego takiego, co moe zawiera informacj<br />
na temat rodzaju tokenu i w razie potrzeby jego warto:
184 ROZDZIA 6 • PISANIE PROGRAMU<br />
Pomys ten mona zaimplementowa w jzyku <strong>C++</strong> na wiele sposobów. Przedstawiamy<br />
najprostszy, który wydaje nam si uyteczny:<br />
class Token { // Bardzo prosty typ zdefiniowany przez uytkownika.<br />
public:<br />
char kind;<br />
double value;<br />
};<br />
Token to typ (tak samo jak int czy char), a wic mona go uywa do definiowania zmiennych<br />
i przechowywania wartoci. Skada si z dwóch czci (nazywanych skadowymi) — kind<br />
(rodzaj) oraz value (warto). Sowo kluczowe class oznacza „typ zdefiniowany przez uytkownika”.<br />
Wskazuje definicj typu z zerem lub wiksz liczb skadowych. Pierwsza skadowa<br />
o nazwie kind jest znakiem char, a wic mona jej uy do przechowywania znaków '+' i '*',<br />
które bd reprezentoway operatory. Przy uyciu tego typu mona tworzy nastpujce instrukcje:<br />
Token t; // Zmienna t jest typu Token.<br />
t.kind = '+'; // Zmienna t reprezentuje znak +.<br />
Token t2; // Zmienna t2 jest innym obiektem typu Token.<br />
t2.kind = '8'; // Cyfra 8 oznacza rodzaj (kind) tokenu bdcy liczb.<br />
t2.value = 3.14;<br />
Aby uzyska dostp do skadowej, posugujemy si odpowiedni notacj — nazwa_obiektu.<br />
nazwa_skadowej. Tekst t.kind mona przeczyta jako „rodzaj obiektu t”, a t2.value jako<br />
„warto obiektu t2”. Obiekty typu Token mona kopiowa tak samo jak typu int:<br />
Token tt = t; // Inicjacja kopii<br />
if (tt.kind != t.kind) error("To niemoliwe!");<br />
t = t2;<br />
// przypisanie<br />
cout
6.3. WRACAJC DO KALKULATORA 185<br />
};<br />
double value; // Dla liczb: warto.<br />
Token(char ch) // Tworzy Token ze znaku.<br />
:kind(ch), value(0) { }<br />
Token(char ch, double val) // Tworzy Token ze znaku i wartoci typu double.<br />
:kind(ch), value(val) { }<br />
Nie s to zwyke funkcje skadowe, tylko tzw. konstruktory. Maj tak sam nazw jak ich<br />
typ i su do inicjalizowania (tworzenia) obiektów typu Token. Na przykad:<br />
Token t1('+'); // Inicjacja zmiennej t1 — t1.kind = '+'.<br />
Token t2('8',11.5); // Inicjacja zmiennej t2 — t2.kind = '8' i t2.value = 11.5.<br />
Tekst :kind(ch), value(0) w pierwszym konstruktorze oznacza: „Zainicjuj skadow kind wartoci<br />
ch, a value ustaw na 0”. W drugim konstruktorze znajduje si tekst :kind(ch), value(val),<br />
który oznacza: „Zainicjuj skadow kind wartoci ch, a value ustaw na val”.W obu przypadkach<br />
nie trzeba robi nic wicej, aby utworzy obiekt typu Token, dlatego tre funkcji jest pusta —<br />
{ }. Specjalna skadnia inicjujca (ang. member initializer list — lista wartoci inicjujcych<br />
skadowe), która zaczyna si dwukropkiem, jest uywana tylko w konstruktorach.<br />
Zauwa, e konstruktor nie zwraca wartoci. Dlatego nie trzeba (a nawet nie mona) definiowa<br />
typu zwrotnego konstruktora. Wicej na temat konstruktorów napiszemy w rozdziaach<br />
9.4.2 i 9.7.<br />
6.3.4. Uywanie tokenów<br />
Moe spróbujemy dokoczy nasz kalkulator! Chocia warto by byo najpierw opracowa jaki<br />
plan dziaania. Jak bdziemy wykorzystywa obiekty typu Token w programie? Moemy<br />
wczyta dane wejciowe do wektora takich obiektów:<br />
Token get_token(); // Wczytuje token ze strumienia cin.<br />
vector tok; // Tutaj bdziemy zapisywa tokeny.<br />
int main()<br />
{<br />
while (cin) {<br />
Token t = get_token();<br />
tok.push_back(t);<br />
}<br />
// …<br />
}<br />
Teraz moemy wczyta wyraenie w caoci i dopiero po tym obliczy jego warto. W przypadku<br />
11*12 otrzymamy co takiego:<br />
W strukturze tej moemy znale operator i jego operandy. Gdy to zrobimy, z atwoci wykonamy<br />
dziaanie mnoenia, poniewa liczby 11 i 12 zostay zapisane jako wartoci liczbowe, a nie<br />
acuchy.
186 ROZDZIA 6 • PISANIE PROGRAMU<br />
Teraz przeanalizujemy bardziej skomplikowane wyraenie. Dla wyraenia 1+2*3 tok bdzie<br />
zawiera pi obiektów typu Token:<br />
W tym przypadku operator mnoenia mona znale za pomoc prostej ptli:<br />
for (int i = 0; i
6.3. WRACAJC DO KALKULATORA 187<br />
6.3.5. Powrót do tablicy<br />
Jeszcze raz przeanalizujemy problem, tym razem starajc si nie wyrywa z nieprzemylanymi<br />
pomysami. Jedyne, co odkrylimy, to fakt, e obliczenie przez program tylko jednego<br />
wyraenia sprawia nam trudnoci. Chcielibymy mie moliwo obliczenia wielu wyrae<br />
w jednym uruchomieniu programu. W zwizku z tym wzbogacamy nasz pseudokod w nastpujcy<br />
sposób:<br />
while (nie_skoczone) {<br />
wczytaj_wiersz<br />
oblicz // wykonaj prac<br />
wydrukuj_wynik<br />
}<br />
To z pewnoci komplikuje spraw, ale musimy wzi pod uwag fakt, e kalkulatorów zazwyczaj<br />
uywa si do wykonywania kilku oblicze po kolei. Czy mamy kaza uytkownikowi uruchamia<br />
nasz program ponownie, aby wykona kade obliczenie? Moglibymy, ale w wielu<br />
nowoczesnych systemach operacyjnych uruchamianie programów trwa za dugo, a wic lepiej<br />
tego nie robi.<br />
Kiedy patrzymy na nasz pseudokod, nasze pocztkowe próby rozwizania problemu i przykady<br />
uycia, nasuwa si nam kilka pyta (i kilka niemiaych odpowiedzi):<br />
1. Jeli uytkownik wpisze 45+5/7, jak znajdziemy poszczególne elementy — 45, 5, / i 7?<br />
Odpowied: podzielimy na tokeny!<br />
2. W jaki sposób oznaczymy koniec wyraenia? Oczywicie znakiem nowego wiersza (zawsze<br />
podejrzliwie traktuj zwroty typu „oczywicie” — „oczywicie” to nie aden powód!<br />
3. Jak zaprezentujemy wyraenie 45+5/7 jako dane, aby mona byo obliczy wynik? Przed<br />
wykonaniem dodawania musimy w jaki sposób zamieni znaki 4 i 5 w liczb cakowit<br />
45 (tj. 4*10+5). Zatem podzia na tokeny jest czci rozwizania.<br />
4. Jak sprawi, aby wyraenie 45+5/7 byo obliczane jako 45+(5/7), a nie (45+5)/7?<br />
5. Ile wynosi 5/7? Okoo .71, a wic to nie jest liczba cakowita. Z dowiadczenia wiemy,<br />
e uytkownicy kalkulatorów oczekuj wyników zmiennoprzecinkowych. Czy powinnimy<br />
pozwoli na wpisywanie liczb zmiennoprzecinkowych? Oczywicie!<br />
6. Czy moemy pozwoli na uywanie zmiennych? Moglibymy na przykad napisa:<br />
v=7<br />
m=9<br />
v*m<br />
Dobry pomys, ale zostawimy to na póniej. Na razie zajmiemy si podstawow funkcjonalnoci.<br />
Najwaniejsza decyzja to prawdopodobnie odpowied na pytanie w punkcie 6. W rozdziale 7.8<br />
zobaczysz, e odpowied ta pocignie za sob prawie podwojenie rozmiaru wstpnej wersji projektu.<br />
To podwoioby czas potrzebny na uruchomienie wstpnej wersji programu. Podejrzewamy,<br />
e pocztkujcy potrzebowaby nawet cztery razy wicej czasu i niewykluczone, e straciby<br />
w kocu cierpliwo. We wczesnych fazach prac nad projektem naley zawsze unika
188 ROZDZIA 6 • PISANIE PROGRAMU<br />
przesady z liczb funkcji. Wstpna wersja zawsze powinna by prosta i zawiera tylko najwaniejsze<br />
funkcje. Kiedy uda Ci si zmusi co do dziaania, moesz postawi sobie bardziej ambitne<br />
wymagania. Budowa programu etapami jest znacznie atwiejsza ni wszystkiego na raz.<br />
Odpowied „tak” na pytanie 6. miaaby jeszcze jeden zy wynik: mogoby by trudno oprze<br />
si pokusie dodania jeszcze innych funkcji. Moe warto pomyle o funkcjach matematycznych<br />
albo o ptlach? Gdy zacznie si dodawa kolejne „fajne” funkcje, trudno przesta.<br />
Z programistycznego punktu widzenia najbardziej kopotliwe s punkty 1, 3 i 4. Ponadto<br />
s ze sob powizane, poniewa gdy znajdziemy ju 45 i +, co mamy z nimi zrobi? Tzn., jak<br />
zapisa je w programie? Oczywicie czciowym rozwizaniem tego problemu jest podzia na<br />
tokeny, ale tylko czciowym.<br />
Co zrobiby dowiadczony programista? Gdy mamy do rozwizania jaki trudny techniczny<br />
problem, czsto mona znale jakie standardowe rozwizanie. Wiemy, e ludzie pisz kalkulatory,<br />
przynajmniej od kiedy istniej komputery przyjmujce dane symboliczne z klawiatury,<br />
a wic od 50 lat. Musi by jakie standardowe rozwizanie! W takiej sytuacji dowiadczony<br />
programista konsultuje si z kolegami i przeszukuje dostpn literatur. Byoby gupstwem<br />
myle, e w jeden dzie uda si wymyle co lepszego, ni inni wymylili przez 50 lat.<br />
6.4. Gramatyki<br />
Istnieje standardowa odpowied na pytanie, jak rozszyfrowa znaczenie wyraenia: najpierw<br />
wprowadzone znaki naley zebra i podzieli na tokeny (to ju sami odkrylimy). Jeli uytkownik<br />
wpisze:<br />
45+11.5/7<br />
program powinien utworzy nastpujc list tokenów:<br />
45<br />
+<br />
11.5<br />
/<br />
7<br />
Token to sekwencja znaków, któr uwaamy za jak jednostk, np. operator lub liczb.<br />
Po utworzeniu tokenów program musi upewnia si, e cae wyraenie jest poprawnie<br />
rozumiane. Na przykad wiemy, e wyraenie 45+11.5/7 oznacza 45+(11.5/7), a nie<br />
(45+11.5)/7. Sk w tym, jak nauczy program tej przydatnej zasady (dzielenie „wie mocniej”<br />
ni dodawanie)? Standardowa odpowied jest taka, e piszemy gramatyk definiujc skadni<br />
naszych danych wejciowych, a nastpnie piszemy program, w którym implementujemy zasady<br />
tej gramatyki. Na przykad:<br />
// Prosta gramatyka wyrae:<br />
Expression:<br />
Term<br />
Expression "+" Term // dodawanie<br />
Expression "–" Term // odejmowanie<br />
Term:<br />
Primary
6.4. GRAMATYKI 189<br />
Term "*" Primary // mnoenie<br />
Term "/" Primary // dzielenie<br />
Term "%" Primary // reszta z dzielenia (modulo)<br />
Primary:<br />
Number<br />
"(" Expression ")" // grupowanie<br />
Number:<br />
floating-point-literal<br />
Jest to zestaw prostych zasad. Ostatni naley czyta nastpujco: „Number (liczba) to litera<br />
zmiennoprzecinkowy”. Natomiast tre przedostatniej jest taka: „Primary (czynnik) jest liczb<br />
lub znakiem '(', po którym jest wyraenie i znak ')'”. Reguy dla Expression (wyraenia) i Term<br />
(skadnika) s podobne. Kada z nich jest zdefiniowana z uwzgldnieniem jednej z regu,<br />
które znajduj si dalej.<br />
Jak pamitamy z podrozdziau 6.3.2, nasze tokeny — zgodnie z definicj w jzyku <strong>C++</strong><br />
— to:<br />
litera zmiennoprzecinkowy (zgodny z definicj w jzyku <strong>C++</strong>, np. 3.14, 0.274e2<br />
lub 42);<br />
+, -, *, / oraz % — operatory;<br />
( i ) — nawiasy.<br />
Uywajc gramatyki i tokenów, zrobilimy bardzo duy pojciowy skok w stosunku do naszego<br />
pocztkowego pseudokodu. Tego rodzaju postpy chcielibymy robi zawsze, ale rzadko si<br />
to udaje bez pomocy. Do tego wanie su dowiadczenie, literatura i mentorzy.<br />
Na pierwszy rzut oka gramatyka ta wydaje si bezsensowna. Czsto tak jest z notacj<br />
techniczn. Pamitaj jednak, e jest to ogólna i elegancka (co w kocu docenisz) notacja do<br />
opisu czego, co potrafisz robi przynajmniej od czasów szkoy podstawowej. Nie masz problemu<br />
z obliczeniem wyraenia 1-2*3 albo 1+2-3 lub 3*2+4/2. Potrafisz jednak wyjani, jak<br />
to robisz? Umiesz to tak wyjani, aby zrozumia to nawet kto, kto nigdy nie mia stycznoci<br />
z konwencjonaln arytmetyk? Czy Twoje wyjanienia bd miay zastosowanie dla wszystkich<br />
kombinacji operatorów i argumentów? Aby wystarczajco szczegóowo i precyzyjnie<br />
objani co komputerowi, potrzebna jest odpowiednia notacja — a gramatyka naley do<br />
najlepszych konwencjonalnych narzdzi do jej tworzenia.<br />
Jak czyta si gramatyk? Majc pewne dane wejciowe, zaczyna si od pierwszej reguy,<br />
Expression (wyraenie), i przeszukuje kolejne, znajdujc te, które pasuj do tokenów w miar<br />
ich wczytywania. Wczytywanie strumienia tokenów zgodnie z zasadami gramatyki nazywa si<br />
parsowaniem (analiz skadniow), a program, który to robi, nazywamy parserem (ang.<br />
parser) lub analizatorem skadni (ang. syntax analyzer). Nasz analizator odczytuje tokeny od<br />
lewej do prawej, dokadnie w takiej kolejnoci, jak je wpisujemy i czytamy. Wypróbujemy jaki<br />
bardzo prosty przykad: czy 2 jest wyraeniem?<br />
1. Wyraenie (Expression) musi by skadnikiem (Term) lub koczy si skadnikiem.<br />
Skadnik musi by czynnikiem (Primary) lub koczy si czynnikiem. Ten czynnik<br />
musi zaczyna si znakiem ( lub by liczb (Number). Oczywicie 2 nie jest znakiem (,<br />
tylko literaem zmiennoprzecinkowym, a wic liczb, która jest czynnikiem.
190 ROZDZIA 6 • PISANIE PROGRAMU<br />
2. Przed tym czynnikiem (liczba 2) nie ma znaku /, * ani %, a wic jest to kompletny<br />
skadnik (a nie zakoczenie wyraenia z operatorem /, * lub %).<br />
3. Przed skadnikiem tym (Primary 2) nie ma znaku + ani -, a wic jest to pene wyraenie<br />
(Expression), a nie zakoczenie wyraenia z operatorem + lub -.<br />
W zwizku z tym zgodnie z nasz gramatyk 2 jest wyraeniem. Przegld gramatyki mona<br />
przedstawi graficznie:<br />
Na rysunku zostaa przedstawiona cieka, któr przemierzylimy przez definicje. Odwracajc<br />
nasze rozumowanie, moemy powiedzie, e 2 jest wyraeniem, poniewa jest literaem zmiennoprzecinkowym,<br />
który jest liczb, liczba jest czynnikiem, czynnik jest skadnikiem, a skadnik<br />
wyraeniem.<br />
Spróbujmy czego bardziej skomplikowanego. Czy 2+3 jest wyraeniem? Naturalnie<br />
znaczna cz rozumowania bdzie taka sam jak dla 2:<br />
1. Wyraenie musi by skadnikiem lub mie go na kocu. Skadnik musi by czynnikiem<br />
lub koczy si czynnikiem, który z kolei musi zaczyna si od znaku ( lub by liczb.<br />
Oczywicie 2 nie jest znakiem (, ale jest literaem zmiennoprzecinkowym, który jest<br />
liczb, ta z kolei jest czynnikiem.<br />
2. Przed tym czynnikiem (liczba 2) nie ma znaku /, * ani %, a wic jest to kompletny<br />
skadnik (a nie zakoczenie wyraenia z operatorem /, * lub %).<br />
3. Za skadnikiem tym (Primary 2) jest znak +, a wic jest to koniec pierwszej czci wyraenia<br />
i musimy poszuka skadnika za tym znakiem. Dokadnie w taki sam sposób,<br />
jak w przypadku 2 dowiadujemy si, e 3 jest skadnikiem. Poniewa za 3 nie ma znaku +<br />
ani -, uznajemy, e jest to peny skadnik, a nie pierwsza cz wyraenia z operatorem +<br />
lub -. W zwizku z tym 2+3 spenia zasad Expression+Term, a wic jest wyraeniem.
6.4. GRAMATYKI 191<br />
Znowu nasz tok rozumowania moemy przedstawi graficznie (pomijamy zasad literau<br />
zmiennoprzecinkowego jako liczby dla uproszczenia):<br />
Na rysunku przedstawilimy ciek, któr przemierzylimy przez definicje. Odwracajc nasze<br />
rozumowanie, moemy powiedzie, e 2+3 jest wyraeniem, poniewa 2 jest skadnikiem,<br />
który jest wyraeniem, 3 jest skadnikiem oraz wyraenie ze znakiem + i skadnikiem równie<br />
jest wyraeniem.<br />
Prawdziwym powodem, dla którego zainteresowalimy si gramatykami, jest fakt, e mog<br />
one nam pomóc w rozwizaniu problemu poprawnego przetwarzania wyrae z operatorami *<br />
i +. Spróbujemy zatem przeanalizowa wyraenie 45+11.5*7. Zabawa w komputer szczegó-<br />
owo sprawdzajcy wszystkie reguy byaby mudna. Dlatego pominiemy niektóre porednie<br />
etapy, które opisalimy ju przy analizie 2 i 2+3. Oczywicie 45, 11.5 i 7 to literay zmiennoprzecinkowe,<br />
które s liczbami, liczby za s czynnikami, a wic moemy zignorowa<br />
wszystkie reguy poniej czynnika (Primary). Otrzymujemy:<br />
1. 45 jest wyraeniem, po którym znajduje si znak +, a wic szukamy wyrazu zamykajcego<br />
regu Expression+Term.<br />
2. 11.5 jest skadnikiem, po którym znajduje si znak *, a wic szukamy czynnika koczcego<br />
regu Term*Primary.<br />
3. 7 jest czynnikiem, a wic 11.5*7 jest skadnikiem zgodnie z regu Term*Primary. Teraz<br />
widzimy, e 45+11.5*7 jest wyraeniem zgodnie z regu Expression+Term. Mówic<br />
dokadniej, jest to wyraenie, w którym najpierw jest wykonywane mnoenie 11.5*7,<br />
a potem dodawanie 45+11.5*7, dokadnie tak, jak gdybymy napisali 45+(11.5*7).<br />
Na nastpnej stronie przedstawiamy graficzn ilustracj naszego rozumowania (znowu pomijamy<br />
zasad literau zmiennoprzecinkowego jako liczby dla uproszczenia).
192 ROZDZIA 6 • PISANIE PROGRAMU<br />
Powyszy rysunek obrazuje nasz tok rozumowania. Zauwa, jak regua Term*Primary zapewnia,<br />
e 11.5 zostanie pomnoone przez 7 zamiast dodane do 45.<br />
Pocztkowo moe wydawa Ci si to trudne do zrozumienia, ale ludzie czytaj gramatyki,<br />
a te prostsze atwo zrozumie. Naszym celem nie byo jednak nauczy Ci rozumie wyrae<br />
2+2 czy 45+11.5*7. To ju kady potrafi. Szukalimy sposobu na objanienie komputerowi, co<br />
oznacza wyraenie 45+11.5*7 i wiele innych skomplikowanych wyrae, które mog zosta<br />
mu podsunite do obliczenia. Skomplikowane gramatyki nie nadaj si do odczytu przez<br />
czowieka, ale komputery radz sobie z nimi doskonale. Analizuj ich reguy szybko i prawidowo,<br />
a przychodzi im to z atwoci. W tym wanie komputery s dobre — w dokadnym<br />
wykonywaniu polece.<br />
6.4.1. Dygresja — gramatyka jzyka angielskiego<br />
Jeli nigdy dotd nie miae do czynienia z gramatykami, to pewnie czujesz si oszoomiony.<br />
W istocie moesz czu si niepewnie, nawet jeli ju si z czym takim spotkae wczeniej.<br />
Spójrz na poniszy fragment gramatyki jzyka angielskiego:<br />
Sentence :<br />
Noun Verb<br />
// np. <strong>C++</strong> rules<br />
Sentence Conjunction Sentence // np. Birds fly but fish swim<br />
Conjunction :<br />
"and"<br />
"or"<br />
"but"<br />
Noun :<br />
"birds"<br />
"fish"<br />
"<strong>C++</strong>"<br />
Verb :<br />
"rules"<br />
"fly"<br />
"swim"
6.4. GRAMATYKI 193<br />
Zdania s zbudowane z czci mowy (np. rzeczowników, czasowników i czników). Mona je<br />
przeanalizowa pod ktem tych regu, aby sprawdzi, które sowa s rzeczownikami, czasownikami<br />
itd. Ta prosta gramatyka zawiera take semantycznie bezsensowne zdania, tj. „<strong>C++</strong> fly<br />
and birds rules”, ale poprawienie tego to cakiem inna kwestia, któr naleaoby si zaj w znacznie<br />
bardziej zaawansowanej ksice.<br />
Wiele osób uczono tych podstawowych zasad na zajciach z jzyka angielskiego w szkole.<br />
Istniej nawet powane dowody neurologiczne potwierdzajce, e takie zasady s zakodowane<br />
w naszych mózgach.<br />
Spójrz na podobne do wczeniejszych drzewo parsowania, ale tym razem przedstawiajce<br />
proste angielskie zdanie:<br />
To nie jest takie skomplikowane. Jeli miae problemy ze zrozumieniem podrozdziau 6.4,<br />
wró do niego teraz i przeczytaj go ponownie. Za drugim razem moe by o wiele bardziej<br />
zrozumiay!<br />
6.4.2. Pisanie gramatyki<br />
Skd wzilimy te zasady gramatyki wyrae? Trzeba przyzna, e pomogo nam w tym<br />
dowiadczenie. Robimy to w taki sposób, w jaki ludzie zazwyczaj pisz gramatyki wyrae.<br />
Napisanie prostej gramatyki jest atwe. Wystarczy wiedzie, jak:<br />
1. Odróni zasad od tokenu.<br />
2. Wstawi jedn zasad za drug (sekwencja).<br />
3. Wyrazi alternatywne wzorce (alternacja).<br />
4. Wyraa powtarzajce si wzorce (repetycja).<br />
5. Rozpozna pierwsz zasad gramatyki.
194 ROZDZIA 6 • PISANIE PROGRAMU<br />
W rónych ksikach i rónych systemach parsowania stosuje si róne notacje i terminologi.<br />
Na przykad niektórzy nazywaj tokeny znakami terminalnymi, a zasady znakami nieterminalnymi<br />
lub produkcjami. My umieszczamy tokeny w podwójnych cudzysowach i zaczynamy<br />
od pierwszej zasady. Alternatywy znajduj si w osobnych wierszach, np.:<br />
List:<br />
"{" Sequence "}"<br />
Sequence:<br />
Element<br />
Element " ," Sequence<br />
Element:<br />
"A"<br />
"B"<br />
Zatem sekwencja (Sequence) jest elementem (Element) lub elementem i sekwencj oddzielonymi<br />
od siebie przecinkiem. Element to litera A lub B. Lista (List) to sekwencja w nawiasach<br />
klamrowych. Moemy generowa te listy (jak?):<br />
{ A }<br />
{ B }<br />
{ A,B }<br />
{A,A,A,A,B }<br />
Ponisze natomiast nie s listami (dlaczego?):<br />
{ }<br />
A<br />
{ A,A,A,A,B<br />
{A,A,C,A,B }<br />
{ A B C }<br />
{A,A,A,A,B, }<br />
Tej sekwencji nie nauczye si w przedszkolu ani nie zakodowae w mózgu, chocia nadal<br />
nie jest to nauka najwyszych lotów. W rozdziaach 7.4 i 7.8.1 objanimy sposoby wyraania<br />
zasad syntaktycznych za pomoc gramatyk.<br />
6.5. Zamiana gramatyki w kod<br />
Istnieje wiele sposobów na zmuszenie komputera do respektowania zasad gramatyki. Uyjemy<br />
najprostszego — napiszemy po jednej funkcji dla kadej zasady i wykorzystamy nasz typ<br />
Token do reprezentowania tokenów. Program implementujcy gramatyk czsto nazywa si<br />
parserem.<br />
6.5.1. Implementowanie zasad gramatyki<br />
Do zaimplementowania naszego kalkulatora potrzebujemy czterech funkcji — jednej do<br />
wczytywania tokenów i po jednej dla kadej zasady w gramatyce:<br />
get_token() // Wczytuje znaki i tworzy tokeny.<br />
// Wykorzystuje strumie cin.<br />
expression() // Obsuguje operatory + i –.
6.5. ZAMIANA GRAMATYKI W KOD 195<br />
// Wywouje funkcje term() i get_token().<br />
term() // Obsuguje operatory *, /, i %.<br />
// Wywouje funkcje primary() i get_token().<br />
primary() // Obsuguje liczby i nawiasy.<br />
// Wywouje funkcje expression() i get_token().<br />
Uwaga: kada funkcja zajmuje si okrelon czci wyraenia i reszt zostawia pozostaym<br />
funkcjom. To radykalnie upraszcza kod funkcji. Metod t mona porówna ze wspóprac<br />
grupy ludzi, gdzie kady wykonuje swoje zadanie, a problemy znajdujce si poza jego specjalnoci<br />
przekazuje do rozwizania innym.<br />
Co powinny te funkcje robi? Kada funkcja powinna wywoywa inne funkcje gramatyki<br />
zgodnie z zasad, któr implementuje, oraz funkcj get_token(), jeli wymagany jest token.<br />
Jeli na przykad funkcja primary() ma postpi zgodnie ze swoj zasad (Expression), musi<br />
wywoa:<br />
get_token() // Do obsugi znaków ( i ).<br />
expression() // Do obsugi wyraenia.<br />
Co powinny takie funkcje zwraca? Co z odpowiedzi, której w rzeczywistoci oczekujemy?<br />
Dla wyraenia 2+3 funkcja expression() mogaby zwróci 5. Istotnie, wszystkie potrzebne<br />
informacje s dostpne. Tego spróbujemy! Dziki temu unikniemy odpowiedzi na jedno z naszych<br />
najtrudniejszych pyta: „Jak zaprezentowa wyraenie 45+5/7 w postaci danych, aby mona<br />
byo obliczy jego warto?”. Zamiast zapisywa reprezentacj 45+5/7 w pamici, obliczamy<br />
warto tego wyraenia przy wczytywaniu. Ten may pomys stanowi prawdziwy przeom!<br />
Dziki temu czterokrotnie zmniejszy si objto kodu w stosunku do tego, co uzyskalibymy,<br />
gdyby funkcja expression() zwracaa co skomplikowanego do obliczenia póniej. Wanie<br />
zaoszczdzilimy sobie okoo 80 procent pracy.<br />
Do towarzystwa nie pasuje funkcja get_token() — poniewa obsuguje tokeny, a nie wyraenia,<br />
nie moe zwraca wartoci podwyrae. Na przykad znaki ( i + nie s wyraeniami.<br />
Musi zatem zwraca typ Token. Dochodzimy do wniosku, e potrzebujemy nastpujcych<br />
funkcji:<br />
// Funkcje realizujce zasady gramatyczne:<br />
Token get_token() // Wczytuje znaki i tworzy tokeny.<br />
double expression() // Obsuguje znaki + i –.<br />
double term() // Obsuguje znaki *, / i %.<br />
double primary() // Obsuguje liczby i nawiasy.<br />
6.5.2. Wyraenia<br />
Najpierw napiszemy funkcj expression(). Gramatyka dla niej jest nastpujca:<br />
Expression:<br />
Term<br />
Expression '+' Term<br />
Expression '–' Term<br />
Poniewa to jest nasza pierwsza próba zamiany zasad gramatyki na kod, przedstawimy kilka<br />
bdnych rozwiza. Tak to zazwyczaj wyglda przy stosowaniu nowych technik, a poza tym<br />
jest to dobry sposób na nauczenie si wielu rzeczy. W szczególnoci pocztkujcy programista
196 ROZDZIA 6 • PISANIE PROGRAMU<br />
moe nauczy si bardzo duo, patrzc na dramatycznie róne zachowania podobnych fragmentów<br />
kodu. Czytanie kodu to przydatna umiejtno, któr naley pielgnowa.<br />
6.5.2.1. Wyraenia — pierwsza próba<br />
Patrzc na zasad Expression'+' Term, najpierw próbujemy wywoa funkcj expression(),<br />
nastpnie znale znak + (i -) oraz wywoa funkcj term():<br />
double expression()<br />
{<br />
double left = expression(); // Wczytuje i oblicza warto wyraenia.<br />
Token t = get_token(); // nastpny token<br />
}<br />
switch (t.kind) {<br />
case '+':<br />
return left + term();<br />
case '–':<br />
return left – term();<br />
default:<br />
return left;<br />
}<br />
// Sprawdza, jaki to jest rodzaj tokenu.<br />
// Wczytuje i sprawdza warto skadnika,<br />
// nastpnie wykonuje dodawanie.<br />
// Wczytuje i sprawdza warto skadnika,<br />
// nastpnie wykonuje odejmowanie.<br />
// Zwraca warto wyraenia.<br />
Wyglda dobrze. To jest prawie naiwna transkrypcja gramatyki. Jest do prosta — najpierw<br />
wczytuje wyraenie, a nastpnie sprawdza, czy za nim znajduje si znak + lub - i jeli tak,<br />
wczytuje skadnik (Term).<br />
Niestety w rzeczywistoci to jest bez sensu. Skd wiadomo, gdzie koczy si wyraenie,<br />
dziki czemu mona poszuka znaków + i -? Przypominam, e nasz program czyta od lewej<br />
do prawej i nie moe wybiec do przodu, aby sprawdzi, czy nie ma dalej znaku +. W istocie ta<br />
wersja funkcji expression() nigdy nie wyjdzie poza pierwszy wiersz — zaczyna od wywoania<br />
funkcji expression(), która zaczyna od wywoania funkcji expression() itd. w nieskoczono.<br />
Nazywa si to nieskoczon rekurencj (ang. infinite recursion), która jednak bdzie miaa<br />
koniec — gdy skoczy si w komputerze pami do przechowywania nigdy niekoczcych<br />
si wywoa funkcji expression(). Pojcie rekurencji oznacza wywoywanie funkcji przez<br />
siebie sam. Nie wszystkie rekurencje s nieskoczone, poza tym technika ta jest bardzo<br />
przydatna w programowaniu (zobacz rozdzia 8.5.8).<br />
6.5.2.2. Wyraenia — druga próba<br />
Co w takim razie robimy? Kady skadnik jest wyraeniem, ale nie kade wyraenie jest skadnikiem.<br />
Moemy wic zacz od szukania skadnika i przej do szukania penego wyraenia<br />
tylko wówczas, gdy znajdziemy znak + lub -. Na przykad:<br />
double expression()<br />
{<br />
double left = term();<br />
Token t = get_token();<br />
switch (t.kind) {<br />
case '+':<br />
// Wczytuje skadnik i oblicza jego warto.<br />
// nastpny token<br />
// Sprawdza, jaki to rodzaj tokenu.
6.5. ZAMIANA GRAMATYKI W KOD 197<br />
}<br />
return left + expression(); // Wczytuje wyraenie i sprawdza jego warto,<br />
// a nastpnie wykonuje dodawanie.<br />
case '–':<br />
return left – expression(); // Wczytuje wyraenie i oblicza jego warto,<br />
// a nastpnie wykonuje odejmowanie.<br />
default:<br />
return left;<br />
// Zwraca warto skadnika.<br />
}<br />
To jako dziaa. Wypróbowalimy ten kod w ukoczonym programie i stwierdzilimy, e przetwarza<br />
wszystkie poprawne wyraenia (i ani jednego niepoprawnego). W wikszoci przypadków<br />
nawet zwraca prawidowe wyniki. Na przykad wyraenie 1+2 jest wczytywane jako skadnik<br />
(o wartoci 1), po którym jest znak + i wyraenie (które w tym przypadku jest skadnikiem<br />
o wartoci 2) — zwracany wynik to 3. Analogicznie dla wyraenia 1+2+3 zwracana jest warto 6.<br />
Moemy dugo cign list przypadków, w których kod ten zwraca prawidowe wyniki, ale<br />
do rzeczy — co stanie si z wyraeniem 1-2-3? Funkcja expression() wczyta 1 jako skadnik<br />
i przejdzie do 2-3 jako wyraenia (skadajcego si ze skadnika 2 i wyraenia 3). Nastpnie<br />
odejmie warto wyraenia 2-3 od 1. Innymi sowy obliczy warto wyraenia 1-(2-3), która<br />
wynosi 2 (liczba dodatnia). Nas jednak w szkole uczono (a moe i wczeniej), e 1-2-3 oznacza<br />
tyle, co (1-2)-3, a wic wynik powinien by -4 (liczba ujemna).<br />
W ten sposób utworzylimy bardzo fajny program, który nie robi tego, co trzeba. Sytuacj<br />
pogarsza fakt, e w wielu przypadkach zwraca prawidowy wynik. Na przykad dla wyraenia<br />
1+2+3 zostanie zwrócony wynik 6, poniewa 1+(2+3) jest równowane z (1+2)+3. Jaki by nasz<br />
podstawowy z programistycznego punktu widzenia bd? Zawsze naley zada sobie to pytanie,<br />
gdy znajdzie si bd. Dziki temu bdzie mona unikn powtórzenia go innym razem.<br />
Naszym problemem jest to, e popatrzylimy na kod i postanowilimy zgadywa. To rzadko<br />
wystarcza! Musimy rozumie, co robi nasz kod, i umie wyjani, czemu robi to, co trzeba.<br />
Ponadto analizowanie bdów jest czsto najlepszym sposobem na znalezienie poprawnego<br />
rozwizania. Nasza funkcja expression() szuka najpierw skadnika, a nastpnie, jeli znajduje si<br />
za nim znak + lub -, wyraenia. Jest to w rzeczywistoci implementacja nieco innej gramatyki:<br />
Expression:<br />
Term<br />
Term '+' Expression // dodawanie<br />
Term '–' Expression // odejmowanie<br />
Rónica midzy t a nasz gramatyk polega na tym, e my chcielimy, aby wyraenie 1-2-3 byo<br />
traktowane jako wyraenie 1-2, po którym jest znak - i skadnik 3. Natomiast uzyskalimy<br />
skadnik 1, po którym znajduje si znak - i wyraenie 2-3. Innymi sowy chcielimy, aby 1-2-3<br />
oznaczao (1-2)-3, a nie 1-(2-3).<br />
Tak, debugowanie bywa mudne, trudne i czasochonne, ale w tym przypadku pracujemy<br />
nad zasadami wyuczonymi w podstawówce. Sk w tym, e musimy „wpoi” je komputerowi,<br />
który uczy si znacznie wolniej od nas.<br />
Zauwa, e moglibymy zdefiniowa 1-2-3 jako 1-(2-3) zamiast (1-2)-3 i unikn caej<br />
tej dyskusji. Czsto najtrudniejszymi problemami w programowaniu s zasady, które zostay<br />
ustalone dla ludzi wiele lat przed tym, jak zaczlimy uywa komputerów.
198 ROZDZIA 6 • PISANIE PROGRAMU<br />
6.5.2.3. Wyraenia — do trzech razy sztuka<br />
Co teraz? Spójrz jeszcze raz na gramatyk (t poprawn w podrozdziale 6.5.2) — kade wyra-<br />
enie zaczyna si skadnikiem, po którym moe znajdowa si znak + lub -. Musimy wic<br />
poszuka skadnika, sprawdzi, czy znajduje si za nim jeden z tych znaków, i robi tak a do<br />
wyczerpania plusów i minusów. Na przykad:<br />
double expression()<br />
{<br />
double left = term();<br />
// Wczytuje skadnik i oblicza jego warto.<br />
Token t = get_token();<br />
// nastpny token<br />
while ( t.kind=='+' || t.kind=='–') { // Szuka znaków + i –.<br />
if (t.kind == '+')<br />
left += term();<br />
// Oblicza warto skadnika i wykonuje dodawanie.<br />
else<br />
left –= term();<br />
// Oblicza warto skadnika i wykonuje odejmowanie.<br />
t = get_token();<br />
}<br />
return left; // Jeli nie ma wicej znaków + lub –, zwraca odpowied.<br />
}<br />
Ten kod jest nieco bardziej skomplikowany, poniewa musielimy wprowadzi ptl, aby<br />
móc wyszuka wszystkie plusy i minusy. Ponadto powtarzamy si troch — dwa razy sprawdzamy<br />
znaki + i - oraz dwa razy wywoujemy funkcj get_token(). Poniewa to tylko zaciemnia<br />
kod, pozbdziemy si nadmiarowego testu znaków + i -:<br />
double expression()<br />
{<br />
double left = term(); // Wczytuje skadnik i oblicza jego warto.<br />
Token t = get_token(); // nastpny token<br />
while(true) {<br />
switch(t.kind) {<br />
case '+':<br />
left += term(); // Oblicza warto skadnika i wykonuje dodawanie.<br />
t = get_token();<br />
break;<br />
case '–':<br />
left –= term(); // Oblicza warto skadnika i wykonuje odejmowanie.<br />
t = get_token();<br />
break;<br />
default:<br />
return left;<br />
// Jeli nie ma wicej znaków + lub –, zwraca odpowied.<br />
}<br />
}<br />
}<br />
Warto zauway, e pomijajc ptl, ta wersja kodu jest podobna do pierwszej próby (podrozdzia<br />
6.5.2.1). Usunlimy wywoanie funkcji expression() wewntrz funkcji expression()<br />
i zastpilimy je ptl. Innymi sowy przekonwertowalimy wyraenie z zasad gramatyki na<br />
ptl szukajc skadnika ze znakiem + lub -.
6.5. ZAMIANA GRAMATYKI W KOD 199<br />
6.5.3. Skadniki<br />
Zasada dotyczca skadnika (Term) w gramatyce jest bardzo podobna do zasady wyraenia<br />
(Expression):<br />
Term:<br />
Primary<br />
Term '*' Primary<br />
Term '/' Primary<br />
Term '%' Primary<br />
W konsekwencji kod równie powinien by bardzo podobny. Oto pierwsza wersja:<br />
double term()<br />
{<br />
double left = primary();<br />
Token t = get_token();<br />
while(true) {<br />
switch (t.kind) {<br />
case '*':<br />
left *= primary();<br />
t = get_token();<br />
break;<br />
case '/':<br />
left /= primary();<br />
t = get_token();<br />
break;<br />
case '%':<br />
left %= primary();<br />
t = get_token();<br />
break;<br />
default:<br />
return left;<br />
}<br />
}<br />
}<br />
Niestety ten kod nie da si skompilowa — operator reszty z dzielenia (%) nie jest zdefiniowany<br />
dla liczb zmiennoprzecinkowych. Zostaniemy o tym uprzejmie poinformowani. Odpowiadajc<br />
wczeniej „oczywicie” na pytanie dotyczce tego, czy pozwoli te na wpisywanie liczb zmiennoprzecinkowych,<br />
nie przemylelimy sprawy dokadnie i padlimy ofiar wasnej zachannoci<br />
na funkcje. Tak jest zawsze! Co z tym zrobimy? Moemy sprawdza, czy argumenty<br />
operatora % s liczbami cakowitymi i jeli nie, zgasza bd. Moemy te usun ten operator<br />
z kalkulatora. Wybierzemy najprostsze rozwizanie. Dodamy ten operator póniej (rozdzia 7.5).<br />
Po usuniciu operatora % funkcja dziaa — tzn. prawidowo analizuje skadniki i oblicza ich<br />
wartoci. Jednak dowiadczony programista zauway pewien niepodany szczegó, który sprawia,<br />
e funkcja term() jest nie do przyjcia. Co si stanie, jeli kto wpisze 2/0? Nie mona<br />
dzieli przez zero. Jeli do tego dopucimy, komputer wykryje to i zamknie program, wywietlajc<br />
raczej mao pomocny komunikat o bdzie. Niedowiadczony programista przekona si<br />
o tym na wasnej skórze. Dlatego lepiej doda odpowiedni test i wywietli lepszy komunikat<br />
o bdzie:
200 ROZDZIA 6 • PISANIE PROGRAMU<br />
double term()<br />
{<br />
double left = primary();<br />
Token t = get_token();<br />
while(true) {<br />
switch (t.kind) {<br />
case '*':<br />
left *= primary();<br />
t = get_token();<br />
break;<br />
case '/':<br />
{ double d = primary();<br />
if (d == 0) error("Dzielenie przez zero.");<br />
left /= d;<br />
t = get_token();<br />
break;<br />
}<br />
default:<br />
return left;<br />
}<br />
}<br />
}<br />
Dlaczego umiecilimy instrukcje obsugujce operator / w bloku? Poniewa kompilator nalega.<br />
Jeli chcesz definiowa i inicjowa zmienne w instrukcjach switch, musisz umieci je<br />
w blokach.<br />
6.5.4. Podstawowe elementy wyrae<br />
Zasada gramatyczna definiujca podstawowe czynniki wyrae równie jest prosta:<br />
Primary:<br />
Number<br />
'(' Expression ')'<br />
Kod sucy do jej implementacji jest nieco zagmatwany, poniewa jest tu wicej okazji do<br />
popenienia bdu skadni:<br />
double primary()<br />
{<br />
Token t = get_token();<br />
switch (t.kind) {<br />
case '(': // Obsuga reguy '(' expression ')'.<br />
{ double d = expression();<br />
t = get_token();<br />
if (t.kind != ')') error("Oczekiwano ')'.");<br />
return d;<br />
}<br />
case '8':<br />
// Za pomoc znaku '8' reprezentujemy liczby.<br />
return t.value; // Zwraca warto liczby.<br />
default:<br />
error("Oczekwiano czynnika.");<br />
}<br />
}
6.6. WYPRÓBOWYWANIE PIERWSZEJ WERSJI 201<br />
W zasadzie nie ma tu nic nowego, czego nie ma w funkcjach expression() i term().Uylimy<br />
tych samych narzdzi jzykowych, zastosowalimy t sam technik wyodrbniania tokenów<br />
oraz te same techniki programistyczne.<br />
6.6. Wypróbowywanie pierwszej wersji<br />
Aby sprawdzi dziaanie tych funkcji, musimy zaimplementowa funkcj get_token() i utworzy<br />
funkcj main(). To drugie zadanie jest banalne — bdziemy wywoywa funkcj expression()<br />
i drukowa zwracany przez ni wynik:<br />
int main()<br />
try {<br />
while (cin)<br />
cout
202 ROZDZIA 6 • PISANIE PROGRAMU<br />
Kontynuujemy, wpisujc 5+6. Program zwraca w odpowiedzi 5, a wic na ekranie widzimy:<br />
2<br />
3<br />
4<br />
2<br />
5+6<br />
5<br />
Jeli nie masz dowiadczenia programistycznego, pewnie jeste bardzo skonsternowany!<br />
W istocie nawet dowiadczony programista mógby osupie. Co tam si dzieje? W tym momencie<br />
chcemy zamkn program. Jak to zrobi? Zapomnielimy zaprogramowa polecenie<br />
zamykajce program, ale mona go zmusi do zamknicia za pomoc bdu. Wpisujemy wic x<br />
i program zakoczy dziaanie komunikatem Nieprawidowy token. W kocu co zadziaao zgodnie<br />
z planem!<br />
Zapomnielimy odróni na ekranie dane wejciowe od wyjciowych. Zanim przejdziemy<br />
do rozwizywania zagadki, poprawimy to niedocignicie, aby lepiej widzie, co si dzieje.<br />
Na razie wystarczy dodanie do danych wyjciowych znaku =:<br />
while (cin) cout
6.6. WYPRÓBOWYWANIE PIERWSZEJ WERSJI 203<br />
1 2 3 4+5 6+7 8+9 10 11 12<br />
= 1<br />
= 4<br />
= 6<br />
= 8<br />
= 10<br />
Co takiego? Nie 2 ani 3? Czemu 4, a nie 9 (bo 4+5)? Czemu 6, a nie 13 (bo 6+7)? Przyjrzyj si<br />
uwanie — program zwraca co trzeci token! Moe „poera” cz wprowadzonych danych<br />
bez obliczania ich wartoci? Tak wanie robi. Spójrz na funkcj expression():<br />
double expression()<br />
{<br />
double left = term(); // Wczytuje skadnik i oblicza jego warto.<br />
Token t = get_token(); // nastpny token<br />
while(true) {<br />
switch(t.kind) {<br />
case '+':<br />
left += term(); // Oblicza warto skadnika i wykonuje dodawanie.<br />
t = get_token();<br />
break;<br />
case '–':<br />
left –= term(); // Oblicza warto skadnika i wykonuje odejmowanie.<br />
t = get_token();<br />
break;<br />
default:<br />
return left; // Jeli nie ma wicej znaków + lub –, zwraca odpowied.<br />
}<br />
}<br />
}<br />
Gdy funkcja get_token() zwróci inny Token ni + lub -, po prostu zwracamy warto. Nie uywamy<br />
tego tokenu i nie zapisujemy go do uytku przez inn funkcj. To nie byo zbyt mdre.<br />
Odrzucanie danych wejciowych bez sprawdzenia nawet, co to dokadnie jest, to niezbyt dobry<br />
pomys. atwo mona si przekona, e ten sam defekt ma funkcja term(). To wyjania,<br />
dlaczego nasz kalkulator zjada dwa tokeny na trzy.<br />
Poprawimy funkcj expression(), aby nie zjadaa tokenów. Gdzie moemy zapisa nastpny<br />
token (t), jeli program go nie potrzebuje? Moglibymy opracowa wiele rónych skomplikowanych<br />
rozwiza, ale zastosujemy najbardziej oczywiste (wydaje si oczywiste, gdy si<br />
je ju zobaczy) — ten token zostanie wykorzystany przez jak inn funkcj, a wic umiecimy<br />
go z powrotem w strumieniu wejciowym, aby móg zosta stamtd ponownie wczytany! W istocie<br />
mona by byo wstawia znaki do strumienia istream, ale nie o to nam chodzi. My chcemy<br />
mie tokeny, a nie bawi si ze znakami. Potrzebujemy strumienia wejciowego, w którym<br />
bdzie mona zapisywa tokeny.<br />
Zaómy wic, e mamy strumie tokenów — Token_stream — o nazwie ts. Przyjmijmy<br />
te, e ma on funkcj skadow o nazwie get(), która zwraca nastpny token i funkcj skadow<br />
putback(t), ta za wstawia token t z powrotem do strumienia. Implementacj strumienia<br />
Token_stream przedstawimy w podrozdziale 6.8, gdy bdziemy ju wiedzieli, jak go uy.
204 ROZDZIA 6 • PISANIE PROGRAMU<br />
Majc strumie Token_stream, moemy zmodyfikowa funkcj expression() w taki sposób,<br />
eby niepotrzebne tokeny zapisywaa wanie w nim:<br />
double expression()<br />
{<br />
double left = term(); // Wczytuje skadnik i oblicza jego warto.<br />
Token t = ts.get(); // Pobiera nastpny token ze strumienia tokenów.<br />
while(true) {<br />
switch(t.kind) {<br />
case '+':<br />
left += term(); // Oblicza warto skadnika i wykonuje dodawanie.<br />
t = ts.get();<br />
break;<br />
case '–':<br />
left –= term(); // Oblicza warto skadnika i wykonuje odejmowanie.<br />
t = ts.get();<br />
break;<br />
default:<br />
ts.putback(t); // Wstawia token t z powrotem do strumienia tokenów.<br />
return left; // Jeli nie ma wicej znaków + lub –, zwraca odpowied.<br />
}<br />
}<br />
}<br />
Takie same zmiany musz zosta wprowadzone w funkcji term():<br />
double term()<br />
{<br />
double left = primary();<br />
Token t = ts.get(); // Pobiera nastpny token ze strumienia tokenów.<br />
}<br />
while(true) {<br />
switch (t.kind) {<br />
case '*':<br />
left *= primary();<br />
t = ts.get();<br />
break;<br />
case '/':<br />
{ double d = primary();<br />
if (d == 0) error("Dzielenie przez zero.");<br />
left /= d;<br />
t = ts.get();<br />
break;<br />
}<br />
default:<br />
ts.putback(t); // Wstawia token t z powrotem do strumienia tokenów.<br />
return left;<br />
}<br />
}<br />
W ostatniej funkcji naszego parsera, primary(), musimy tylko zmieni get_token() na ts.get().<br />
Funkcja ta uywa kadego wczytanego przez siebie tokenu.
6.7. WYPRÓBOWYWANIE DRUGIEJ WERSJI 205<br />
6.7. Wypróbowywanie drugiej wersji<br />
Moemy przetestowa nasz drug wersj programu. Wpisz 2 i znak nowego wiersza. Brak reakcji.<br />
Wpisz jeszcze jeden znak nowego wiersza, aby sprawdzi, czy program nie zasn. Nadal<br />
nic. Wpisz 3 i znak nowego wiersza. Jest odpowied — 2. Spróbuj wyraenia 2+2 ze znakiem<br />
nowego wiersza. Odpowied brzmi 3. Teraz na ekranie znajduj si nastpujce dane:<br />
2<br />
3<br />
=2<br />
2+2<br />
=3<br />
Hmm. Moe wprowadzenie funkcji putback() i uycie jej w funkcjach expression() i term()<br />
nie pomogo w rozwizaniu problemu? Spróbujmy czego innego:<br />
2 3 4 2+3 2*3<br />
=2<br />
=3<br />
=4<br />
=5<br />
Tak, to s poprawne odpowiedzi! Ale brakuje ostatniej (6). Wci mamy problem z tokenami,<br />
ale tym razem nie chodzi o ich zjadanie, lecz o to, e wynik jest zwracany dopiero po wpisaniu<br />
kolejnego wyraenia. Program nie drukuje od razu wyniku. Opónia to do momentu wczytania<br />
pierwszego tokenu nastpnego wyraenia. Niestety nie widzi go, dopóki nie naciniemy klawisza<br />
Enter po wpisaniu nastpnego wyraenia. Wniosek taki, e program nie dziaa le, tylko<br />
odpowiada z opónieniem.<br />
Jak to poprawi? Jednym z oczywistych rozwiza jest wprowadzenie „polecenia drukowania”.<br />
Rol t niech peni rednik, którego pojawienie si bdzie oznaczao koniec wyraenia<br />
i wymuszao wydruk wyniku. Przy okazji dodamy polecenie zamknicia programu. Do tego<br />
celu doskonale nada si znak k (od sowa koniec). Obecnie w funkcji main() mamy taki kod:<br />
while (cin) cout
206 ROZDZIA 6 • PISANIE PROGRAMU<br />
2;<br />
= 2<br />
2+3;<br />
= 5<br />
3+4*5;<br />
= 23<br />
k<br />
W tym momencie mamy ju dobr wstpn wersj programu. Nie jest to jeszcze to, czego<br />
chcielimy, ale stanowi dobr baz do rozszerzania. Co waniejsze, moemy poprawia bdy<br />
i dodawa nowe funkcje, pracujc ju z dziaajcym programem.<br />
6.8. Strumienie tokenów<br />
Zanim przejdziemy do dalszego opisu kalkulatora, przedstawimy implementacj strumienia<br />
Token_stream. Jeli nie dostarczymy poprawnych danych do programu, to nic nam si nie uda.<br />
Strumie ten zaimplementowalimy ju na samym pocztku, ale najpierw chcielimy skupi<br />
si na problemach oblicze w minimalnym rozwizaniu.<br />
Nasz kalkulator przyjmuje na wejciu sekwencj tokenów, np. (1.5+4)*11 z podrozdziau<br />
6.3.3. Potrzebujemy czego takiego, co bdzie wczytywa znaki ze standardowego strumienia<br />
wejciowego i podawa programowi kolejne tokeny, gdy ten o to poprosi. Poza tym zauwa-<br />
ylimy, e program czsto wczytuje o jeden token za duo, a wic musimy wygospodarowa<br />
jakie miejsce, aby przechowa go do póniejszego uytku. Jest to cakowicie normalne. Jeli<br />
wczytujemy wyraenie 1.5+4 od lewej do prawej, skd mamy wiedzie, e liczba 1.5 to ju<br />
caa liczba, jeli nie wczytamy znajdujcego si za ni znaku +? Dopóki go nie zobaczymy,<br />
równie dobrze moemy podejrzewa, e jestemy w trakcie wczytywania liczby 1.55555.<br />
Dlatego potrzebujemy „strumienia”, który zwraca token, gdy go zadamy za pomoc funkcji<br />
get(), i w którym moemy przechowywa nadmiarowe tokeny, zapisujc je tam za pomoc<br />
funkcji putback(). Wszystko w jzyku <strong>C++</strong> ma okrelony typ, dlatego musimy zacz od<br />
zdefiniowania typu Token_stream.<br />
Pewnie zauwaye sowo public w definicji typu Token. Tam nie miao to adnego widocznego<br />
znaczenia. Natomiast w typie Token_stream, o którym teraz mowa, sowo to bdzie miao<br />
wane zastosowanie. W jzyku <strong>C++</strong> typy zdefiniowane przez uytkownika czsto skadaj<br />
si z dwóch czci — interfejsu publicznego (oznaczonego etykiet public:) i prywatnego<br />
(oznaczonego etykiet private:). Chodzi o oddzielenie tego, co jest potrzebne uytkownikowi<br />
do wygodnego korzystania z typu, od implementacji typu, w której uytkownik nie powinien<br />
mie moliwoci grzebania:<br />
class Token_stream {<br />
public:<br />
// interfejs uytkownika<br />
private:<br />
// szczegóy implementacyjne<br />
// (bezporednio niedostpne uytkownikom typu Token_stream)<br />
};<br />
Oczywicie uytkownik i implementator to czsto jedna i ta sama osoba odgrywajca róne<br />
role. Naley jednak zaznaczy, e rozrónienie midzy interfejsem publicznym przeznaczonym
6.8. STRUMIENIE TOKENÓW 207<br />
dla uytkowników i prywatnym dla implementatorów jest doskonaym narzdziem wspomagajcym<br />
strukturalizacj kodu. Interfejs publiczny powinien zawiera wycznie to, co jest<br />
uytkownikowi potrzebne, a wic najczciej zestaw funkcji, take konstruktorów sucych<br />
do inicjowania obiektów. Implementacj prywatn stanowi kod tych publicznych funkcji.<br />
Najczciej s to dane i funkcje wykonujce skomplikowane dziaania, o których uytkownik<br />
nie musi wiedzie i których nie musi bezporednio uywa.<br />
Rozszerzymy troch nasz typ Token_stream. Jakie wymagania powinien spenia? Bez<br />
wtpienia potrzebujemy funkcji get() i putback(), poniewa to one byy powodem, dla którego<br />
w ogóle wymylilimy co takiego jak strumie tokenów. Zadaniem typu Token_stream jest wytwarzanie<br />
tokenów ze znaków wczytywanych na wejciu. Zatem nasz strumie musi wczytywa<br />
dane ze strumienia cin. Oto najprostsza moliwa wersja typu Token_stream:<br />
class Token_stream {<br />
public:<br />
Token_stream(); // Tworzy obiekt typu Token_stream, który wczytuje dane ze strumienia<br />
cin.<br />
Token get();<br />
// Daje token (obiekt typu Token).<br />
void putback(Token t); // Wkada token z powrotem.<br />
private:<br />
// szczegóy implementacyjne<br />
};<br />
To wszystko, czego uytkownik potrzebuje do korzystania z typu Token_stream. Dowiadczony<br />
programista mógby si zastanawia, dlaczego cin jest jedynym moliwym ródem znaków<br />
— przypominamy, e zdecydowalimy si na razie pobiera dane tylko z klawiatury. Zrewidujemy<br />
t decyzj w rozdziale 7.<br />
Dlaczego zastosowalimy dugaw nazw putback() zamiast krótszej put()? Chcielimy<br />
podkreli asymetri midzy funkcjami get() i putback() — to jest strumie wejciowy, a nie<br />
co, co mona wykorzysta take do ogólnych celów zwizanych z wysyaniem danych na wyjcie.<br />
Poza tym w bibliotece istream take znajduje si funkcja putback(), a spójno nazw jest jedn<br />
z podanych cech kadego systemu. Dziki temu atwiej jest zapamitywa te nazwy i unika<br />
bdów.<br />
Po tym wstpie moemy ju utworzy typ Token_stream i uy go:<br />
Token_stream ts; // Obiekt typu Token_stream o nazwie ts.<br />
Token t = ts.get(); // Daje nastpny token ze strumienia ts.<br />
// …<br />
ts.putback(t); // Wkada token t z powrotem do strumienia ts.<br />
Mamy ju wszystko, czego potrzebujemy do napisania pozostaej czci kalkulatora.<br />
6.8.1. Implementacja typu Token_stream<br />
Zaimplementujemy trzy wymienione funkcje strumienia Token_stream. Jak bdzie si ten<br />
strumie prezentowa? Tzn., jakie dane musz by w nim przechowywane, aby spenia swoje<br />
zadanie? Potrzebujemy miejsca do przechowywania tokenów, które woymy do niego. Dla<br />
uproszczenia zaómy, e mona w nim przechowywa tylko jeden token na raz. Na potrzeby<br />
naszego programu to wystarczy (i na potrzeby wielu innych te). W zwizku z tym potrzebujemy
208 ROZDZIA 6 • PISANIE PROGRAMU<br />
miejsca do przechowania jednego tokenu i wskanika oznaczajcego, czy to miejsce jest wolne<br />
czy zajte:<br />
class Token_stream {<br />
public:<br />
Token_stream(); // Tworzy obiekt typu Token_stream, który wczytuje dane ze strumienia<br />
cin.<br />
Token get();<br />
// Daje token (funkcja get() zostaa zdefiniowana w innym miejscu).<br />
void putback(Token t); // Wkada token z powrotem.<br />
private:<br />
bool full; // Informuje, czy w buforze jest token.<br />
Token buffer; // Miejsce do przechowywania tokenu zapisanego w strumieniu za pomoc funkcji<br />
//putback().<br />
};<br />
Teraz moemy zdefiniowa (napisa) nasze trzy funkcje skadowe. Konstruktor i funkcja<br />
putback() bd atwe, poniewa maj mao do zrobienia. Dlatego zajmiemy si nimi na pocztku.<br />
Konstruktor ustawia tylko zmienn full na warto oznaczajc, e bufor jest pusty:<br />
Token_stream::Token_stream()<br />
:full(false), buffer(0) // Bufor jest pusty.<br />
{<br />
}<br />
Jeli definicja skadowej znajduje si poza definicj klasy, naley wskaza, do której klasy ma<br />
nalee. Suy do tego nastpujca notacja:<br />
nazwa_klasy::nazwa_skadowej<br />
Tutaj definiujemy konstruktor klasy Token_stream. Konstruktor to funkcja skadowa o takiej<br />
samej nazwie, jak klasa, do której naley.<br />
Po co definiowa skadow poza klas? Przede wszystkim dla zachowania przejrzystoci<br />
— w definicji klasy mona znale gównie informacje o tym, co klasa potrafi „robi”. Definicje<br />
funkcji skadowych to implementacje okrelajce sposób, w jaki s robione róne rzeczy.<br />
Wolimy umieci je gdzie indziej, aby nie przeszkadzay. Ideaem, do którego dymy, jest,<br />
aby kada jednostka logiczna programu miecia si w caoci na ekranie. Definicje klas zazwyczaj<br />
speniaj ten wymóg, ale tylko jeli definicje ich funkcji skadowych przeniesie si gdzie<br />
indziej, poza klas.<br />
Skadowe klasy inicjuje si w licie inicjatorów skadowych (podrozdzia 6.3.3). Instrukcja<br />
full(false) ustawia skadow full na false, a buffer(0) inicjuje skadow buffer „udawanym<br />
tokenem”, który wymylilimy na poczekaniu. Z definicji typu Token (podrozdzia 6.3.3) wynika, e<br />
kady token musi zosta zainicjowany, dlatego nie moglimy zignorowa Token_stream::buffer.<br />
Funkcja skadowa putback() zapisuje swój argument w buforze strumienia Token_stream:<br />
void Token_stream::putback(Token t)<br />
{<br />
buffer = t; // Kopiuje t do bufora.<br />
full = true; // Bufor jest peny.<br />
}
6.8. STRUMIENIE TOKENÓW 209<br />
Sowo kluczowe void (oznaczajce „nic”) wskazuje, e funkcja putback() nie zwraca adnej<br />
wartoci. Aby upewni si, e funkcja ta nie zostanie wywoana dwa razy bez odczytania (za<br />
pomoc funkcji get()) tego, co zostao zapisane w strumieniu midzy tymi wywoaniami,<br />
mona doda specjalny test:<br />
void Token_stream::putback(Token t)<br />
{<br />
if (full) error("Wywoanie funkcji putback(), gdy bufor jest peny.");<br />
buffer = t; // Kopiuje t do bufora.<br />
full = true; // Bufor jest peny.<br />
}<br />
Test skadowej full polega na sprawdzeniu warunku wstpnego: „Nie ma adnego tokenu<br />
w buforze”.<br />
6.8.2. Wczytywanie tokenów<br />
Ca prawdziw prac wykonuje funkcja get(). Jeli w zmiennej Token_stream::buffer nie ma<br />
tokenu, funkcja ta musi wczyta znaki ze strumienia cin i zoy z nich tokeny:<br />
Token Token_stream::get()<br />
{<br />
if (full) { // Sprawdzenie, czy jest gotowy token.<br />
// Usunicie tokenu z bufora.<br />
full=false;<br />
return buffer;<br />
}<br />
char ch;<br />
cin >> ch; // Uwaga: >> pomija biae znaki (spacje, nowe wiersze, tabulatory itp.).<br />
}<br />
switch (ch) {<br />
case ';': // drukowanie<br />
case 'k': // koniec<br />
case '(': case ')': case '+': case '–': case '*': case '/': case '%':<br />
return Token(ch); // Kady znak reprezentuje sam siebie.<br />
case '.':<br />
case '0': case '1': case '2': case '3': case '4':<br />
case '5': case '6': case '7': case '8': case '9':<br />
{ cin.putback(ch); // Wstawia cyfr z powrotem do strumienia wejciowego.<br />
double val;<br />
cin >> val;<br />
// Wczytuje liczb zmiennoprzecinkow.<br />
return Token('8',val); // '8' reprezentuje „liczb”.<br />
}<br />
default:<br />
error("Nieprawidowy token.");<br />
}
210 ROZDZIA 6 • PISANIE PROGRAMU<br />
Przeanalizujemy szczegóowo funkcj get(). Najpierw sprawdza, czy w buforze jest token.<br />
Jeli tak, zwraca go:<br />
if (full) { // Sprawdzenie, czy jest gotowy token.<br />
// Usunicie tokenu z bufora.<br />
full=false;<br />
return buffer;<br />
}<br />
Znakami musimy zajmowa si tylko wówczas, gdy full ma warto false (tzn. nie ma tokenu<br />
w buforze). W takim przypadku wczytujemy znak i postpujemy z nim odpowiednio do potrzeb.<br />
Szukamy nawiasów, operatorów i liczb. Jakikolwiek inny znak powoduje wywoanie funkcji<br />
error(), która zamyka program:<br />
default:<br />
error("Nieprawidowy token.");<br />
}<br />
Funkcja error() zostaa opisana w rozdziale 5.6.3 i jest dostpna w pliku nagówkowym<br />
std_lib_facilities.h.<br />
Musielimy zdecydowa si na jaki sposób reprezentowania kadego rodzaju tokenu, tzn.<br />
trzeba byo dobra wartoci dla skadowej kind. Dla uproszczenia i uatwienia debugowania<br />
zdecydowalimy si, e nawiasy i operatory same bd reprezentowa swój rodzaj tokenu.<br />
Dziki temu ich przetwarzanie jest bardzo atwe:<br />
case '(': case ')': case '+': case '–': case '*': case '/':<br />
return Token(ch); // Kady znak reprezentuje sam siebie.<br />
Mówic szczerze, w pierwszej wersji programu zapomnielimy o znakach: ';' oznaczajcym<br />
drukowanie i 'k' oznaczajcym koniec programu. Dodalimy je, dopiero gdy byy potrzebne<br />
w drugiej wersji.<br />
6.8.3. Wczytywanie liczb<br />
Zostay nam jeszcze liczby, z którymi wcale tak atwo nie pójdzie. Jak dowiedzie si, jaka jest<br />
warto 123? To tyle samo, co 100+20+3, ale co zrobi z 12.34? Jest te pytanie, czy zezwala<br />
na stosowanie notacji naukowej, np. 12.34e5. Aby poradzi sobie z tym wszystkim, moglibymy<br />
potrzebowa wielu godzin, a nawet dni. Na szczcie nie jest tak le. Strumienie w <strong>C++</strong><br />
„wiedz”, jak wygldaj literay, i potrafi zamienia je na wartoci typu double. Musimy tylko<br />
znale sposób na zmuszenie strumienia cin do robienia tego w funkcji get():<br />
case '.':<br />
case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7':<br />
case '8':case '9':<br />
{ cin.putback(ch); // Wstawia cyfr z powrotem do strumienia wejciowego.<br />
double val;<br />
cin >> val;<br />
// Wczytuje liczb zmiennoprzecinkow.<br />
return Token('8',val); // '8' reprezentuje „liczb”.<br />
}<br />
Decyzja o wyborze znaku '8' do reprezentowania „liczb” w tokenach zostaa podjta arbitralnie.
6.9. STRUKTURA PROGRAMU 211<br />
Skd wiadomo, e ma pojawi si liczba? Zgadujc na podstawie wasnych dowiadcze<br />
lub zagldajc do podrcznika jzyka <strong>C++</strong> (np. w dodatku A), mona stwierdzi, e litera liczbowy<br />
musi zaczyna si od cyfry lub znaku . (kropki dziesitnej). Sprawdzamy wic, czy tak<br />
jest. Nastpnie chcemy, aby liczb t wczyta strumie cin, ale wczytalimy ju pierwszy znak<br />
(cyfr lub kropk), a wic pozwalajc temu strumieniowi zaj si reszt, uzyskalibymy nieprawidowe<br />
wyniki. Moglibymy spróbowa poczy warto pierwszego znaku z „reszt” wczytan<br />
przez cin. To znaczy, jeli wpisano by 123, otrzymalibymy 1 i strumie cin wczytaby 23. Wówczas<br />
trzeba by byo doda 100 do 23. A fuj! A to tylko banalny przykad. Na szczcie (i nie<br />
przypadkowo) cin dziaa bardzo podobnie do Token_stream pod tym wzgldem, e take pozwala<br />
wstawi odczytany znak z powrotem. Zamiast wic wykonywa zagmatwane obliczenia<br />
arytmetyczne, wstawiamy pierwsz cyfr z powrotem do cin i odczytujemy z niego ca liczb.<br />
Zauwa, e cay czas unikamy skomplikowanej pracy i znajdujemy prostsze rozwizania<br />
— czsto w tym celu wykorzystujc zawarto biblioteki. To jest istota programowania —<br />
nieustanne poszukiwanie jak najprostszych rozwiza. Niektórzy — nie zawsze susznie —<br />
wyraaj to sowami: „Dobry programista to leniwy programista”. W tym, i tylko w tym,<br />
sensie rzeczywicie powinnimy by leniwi. Po co pisa duo kodu, skoro mona to samo<br />
zrobi, piszc go mniej?<br />
6.9. Struktura programu<br />
Jest takie przysowie, które gosi, e czasami trudno zauway las, poniewa zasaniaj go drzewa.<br />
W analogiczny sposób mona straci z oczu program, patrzc na te jego wszystkie funkcje, klasy<br />
itp. Spójrzmy zatem na nasz program, pomijajc chwilowo szczegóy:<br />
#include "std_lib_facilities.h"<br />
class Token { /* … */ };<br />
class Token_stream { /* … */ };<br />
Token_stream::Token_stream() :full(false), buffer(0) { /* … */ }<br />
void Token_stream::putback(Token t) { /* … */ }<br />
Token Token_stream::get() { /* … */ }<br />
Token_stream ts; // Udostpnia funkcje get() i putback().<br />
double expression(); // Deklaracja umoliwiajca funkcji primary() wywoywanie funkcji expression().<br />
double primary() { /* … */}<br />
// Obsuga nawiasów i liczb.<br />
double term() { /* … */} // Obsuga operatorów *, / i %.<br />
double expression() { /* … */} // Obsuga operatorów + i –.<br />
int main() { /* … */ } // Gówna ptla i obsuga bdów.<br />
Kolejno deklaracji ma znaczenie. Nie mona uy nazwy, zanim zostanie zadeklarowana. Dlatego<br />
ts musi zosta zadeklarowana, zanim zostanie uyta w ts.get(), a funkcja error() musi zosta<br />
zadeklarowana przed funkcjami parsera, poniewa wszystkie z niej korzystaj. Na schemacie<br />
wywoa mona zauway ciekawe zaptlenie: funkcja expression() wywouje term(),<br />
która wywouje primary(), a ta z kolei wywouje expression().
w i c z e n i a<br />
212 ROZDZIA 6 • PISANIE PROGRAMU<br />
Mona to przedstawi graficznie (wywoania funkcji error() odkadamy na bok, poniewa<br />
wszystkie funkcje j wywouj):<br />
To oznacza, e nie moemy po prostu zdefiniowa tych trzech funkcji — nie da si ustali takiej<br />
kolejnoci, w której kada z nich zostaaby zdefiniowana przed pierwszym uyciem. Potrzebujemy<br />
przynajmniej jednej deklaracji, która nie jest równoczenie definicj. Zdecydowalimy<br />
si zadeklarowa z wyprzedzeniem funkcj expression().<br />
Czy to dziaa? Jeli odpowiednio zdefiniuje si sowo „dziaa”, mona powiedzie, e tak.<br />
Przechodzi kompilacj, da si uruchomi, poprawnie oblicza wyniki wyrae i zgasza sensowne<br />
komunikaty o bdach. Ale czy dziaa tak, jak sobie tego yczymy? Nie bdzie zaskoczeniem,<br />
gdy powiem „nie za bardzo”. Pierwsz wersj wypróbowalimy w podrozdziale 6.6.<br />
Wówczas usunlimy powany bd. Druga wersja (podrozdzia 6.7) nie jest o wiele lepsza.<br />
Ale w porzdku, tego si spodziewalimy. Program zadowalajco spenia swoje gówne zadanie,<br />
czyli pozwala zweryfikowa nasze podstawowe pomysy i zorientowa si, co robi dalej.<br />
Pod tym wzgldem odnielimy sukces, ale spróbuj z niego skorzysta — bez problemu doprowadzi<br />
Ci do szau!<br />
WYPRÓBUJ<br />
Uruchom powysz wersj kalkulatora i sprawd, co robi. Spróbuj doj, dlaczego tak<br />
dziaa.<br />
wiczenia<br />
Celem tego zestawu wicze jest poprawienie bdów w programie, aby zamieni go w co<br />
uytecznego.<br />
1. We kalkulator z pliku calculator02buggy.cpp. Spraw, eby da si skompilowa. Musisz<br />
znale i poprawi kilka bdów. Nie ma ich w tekcie ksiki.<br />
2. Zmie znak polecenia zamknicia programu na x.
WICZENIA 213<br />
3. Zmie znak polecenia drukowania na =.<br />
4. Dodaj do funkcji main() komunikat powitalny:<br />
„Witaj w naszym prostym kalkulatorze.<br />
W wyraeniach stosuj liczby zmiennoprzecinkowe.”<br />
5. Dodaj do komunikatu powitalnego informacj o tym, jakie operatory s obsugiwane oraz<br />
jak drukowa wynik i zakoczy dziaanie programu.<br />
6. Znajd trzy bdy logiczne w programie, które zostay tam przemylnie ukryte, i popraw<br />
je, aby kalkulator zwraca prawidowe wyniki.<br />
Powtórzenie<br />
1. Co rozumiemy pod pojciem „Programowa to zrozumie”? Wymie trzy gówne fazy<br />
produkcji oprogramowania.<br />
2. W rozdziale tym zosta szczegóowo opisany proces tworzenia kalkulatora. Napisz krótk<br />
specyfikacj wymaga dla takiego programu.<br />
3. W jaki sposób dzieli si problem na mniejsze, atwiejsze do ogarnicia czci?<br />
4. Dlaczego utworzenie ograniczonej wersji programu jest dobrym pomysem?<br />
5. Co jest zego w mnoeniu wymaga dotyczcych funkcjonalnoci na pocztku pracy nad<br />
programem?<br />
6. Co to jest „przypadek uycia”?<br />
7. Jaki jest cel przeprowadzania testów?<br />
8. Posikujc si informacjami zawartymi w rozdziale, opisz rónic midzy skadnikiem<br />
(Term), wyraeniem (Expression), liczb (Number) i czynnikiem (Primary).<br />
9. Dane wejciowe kalkulatora rozkadalimy na nastpujce elementy: skadnik, wyraenie,<br />
czynnik i liczba. Rozó w ten sposób wyraenie (17+4)/(5–1).<br />
10. Dlaczego w programie nie ma funkcji o nazwie number()?<br />
11. Co to jest token?<br />
12. Co to jest gramatyka? Co to jest regua gramatyki?<br />
13. Co to jest klasa? Do czego su klasy?<br />
14. Co to jest konstruktor?<br />
15. Dlaczego w funkcji expression() klauzula default instrukcji switch wstawia token z powrotem<br />
do strumienia?<br />
16. Co to znaczy „wczyta z wyprzedzeniem”?<br />
17. Co robi funkcja putback() i dlaczego jest przydatna?<br />
18. Co nastrcza trudnoci w implementacji operatora % (modulo) w funkcji term()?<br />
19. Do czego su dwie zmienne skadowe klasy Token?<br />
20. Dlaczego czasami skadowe klasy dzieli si na publiczne i prywatne?<br />
21. Co dzieje si w klasie Token_stream, gdy w buforze jest token i zostanie wywoana funkcja<br />
get()?<br />
22. Po co zostay dodane znaki ';' i 'k' do instrukcji switch w funkcji get() w klasie Token_<br />
stream?
214 ROZDZIA 6 • PISANIE PROGRAMU<br />
23. Kiedy powinno si zacz testowanie programu?<br />
24. Co to jest „typ zdefiniowany przez uytkownika”? Do czego moe si przyda?<br />
25. Co to jest interfejs do „typu zdefiniowanego przez uytkownika” w jzyku <strong>C++</strong>?<br />
26. Dlaczego powinno si uywa kodu z bibliotek?<br />
Terminologia<br />
analiza implementacja przypadek uycia<br />
analizator skadniowy interfejs pseudokod<br />
class parser public<br />
dzielenie przez zero private skadowa klasy<br />
funkcja skadowa projekt token<br />
gramatyka prototyp zmienna skadowa klasy<br />
Praca domowa<br />
1. Jeli jeszcze tego nie zrobie, rozwi wszystkie wiczenia Wypróbuj.<br />
2. Dodaj moliwo uywania w programie zarówno nawiasów okrgych (), jak i klamrowych<br />
{}, aby mona byo pisa wyraenia typu {(4+5)*6}/(3+4).<br />
3. Dodaj operator silni ! jako operator przyrostkowy. Na przykad 7! oznacza 7*6*5*4*3*2*1.<br />
Niech operator ten wie mocniej ni * i /. To znaczy, 7*8! powinno oznacza 7*(8!) ,<br />
a nie (7*8)!. Zacznij od dodania operatora wyszego poziomu do gramatyki. Aby pozosta<br />
w zgodzie ze standardow matematyczn definicj silni, niech 0! wynosi 1.<br />
4. Zdefiniuj klas Name_value przechowujc acuch i warto. Utwórz konstruktor (podobny<br />
jak w klasie Token). Zmodyfikuj wiczenie 19. z rozdziau 4., uywajc vector<br />
zamiast dwóch wektorów.<br />
5. Dodaj do gramatyki jzyka angielskiego z podrozdziau 6.4.1 przedimek the, aby mona<br />
byo za jej pomoc opisywa zdania typu „The birds fly but the Fish swim”.<br />
6. Napisz program sprawdzajcy poprawno zdania zgodnie z gramatyk z podrozdziau<br />
6.4.1. Przyjmij zaoenie, e kade zdanie koczy si kropk otoczon biaymi znakami, np.<br />
birds fly but the fish swim . jest zdaniem, a birds fly but the fish swim (brak kropki<br />
na kocu) i birds fly but the fish swim. (brak spacji przed kropk) nie. Dla kadego<br />
wpisanego zdania program niech zwraca tylko prost odpowied Dobrze lub le. Wskazówka:<br />
nie zawracaj sobie gowy tokenami, wystarczy wczyta dane do acucha za pomoc operatora<br />
>>.<br />
7. Napisz gramatyk dla wyrae logicznych. S one podobne do arytmetycznych, tylko posuguj<br />
si operatorami ! (nie), ~ (uzupenienie), & (i), | (lub) oraz ^ (lub wyczajce).<br />
Operatory ! i ~ s jednoargumentowe i prefiksowe. Operator ^ ma pierwszestwo przed |<br />
(podobnie jak * przed +), a wic x|y^z oznacza x|(y^z), a nie (x|y)^z. Operator & ma<br />
pierwszestwo przed ^, a wic x^y&z oznacza x^(y&z).<br />
8. Przerób gr „Byki i krowy” z wiczenia 12. w rozdziale 5., uywajc liter zamiast cyfr.<br />
9. Napisz program wczytujcy cyfry i skadajcy z nich liczby cakowite. Na przykad 123<br />
zostanie wczytane jako znaki 1, 2 i 3. Odpowied programu powinna by nastpujca:
w i c z e n i a<br />
WICZENIA 215<br />
Dekompozycja liczby 123: liczba setek: 1; liczba dziesitek: 2; liczba jednostek: 3. Liczba ma by<br />
wysyana na wyjcie jako typ int. Obsu liczby jedno-, dwu-, trzy- i czterocyfrowe.<br />
Wskazówka: aby uzyska cakowitoliczbow warto 5 znaku '5', odejmij od niego '0',<br />
tzn. '5'-'0'==5.<br />
10. Permutacja to uporzdkowany podzbiór pewnego zbioru. Wyobra sobie na przykad, e<br />
chcesz zdoby szyfr do sejfu. Jest 60 moliwych liczb, a kombinacja, której potrzebujesz,<br />
skada si z trzech rónych liczb. Jest P(60,3) permutacji dla tej kombinacji, gdzie P definiuje<br />
nastpujcy wzór:<br />
We wzorze tym ! oznacza przyrostkowy operator silni. Na przykad 4! wynosi 4*3*2*1.<br />
Kombinacje s podobne do permutacji, z t rónic, e nie jest w nich wana kolejno<br />
elementów. Gdyby na przykad robi sobie deser lodowy, chcc uy trzech rónych<br />
smaków lodów z piciu dostpnych, nie zaleaoby Ci, czy bananowy znajdzie si na<br />
wierzchu czy na samym dole, oby gdzie by. Wzór kombinacji jest nastpujcy:<br />
Zaprojektuj program proszcy uytkownika o podanie dwóch liczb, pytajcy, czy ma obliczy<br />
permutacje czy kombinacje i drukujcy wynik. To bdzie wymagao podzielenia go<br />
na kilka czci. Wykonaj analiz opisanych wyej wymaga. Napisz, co dokadnie program<br />
ma robi. Napisz pseudokod i podziel go na czci. Ten program powinien mie<br />
wbudowany mechanizm sprawdzania bdów. Spraw, aby dla kadego rodzaju bdnych<br />
danych byy zwracane odpowiednie komunikaty o bdzie.<br />
Podsumowanie<br />
Jedn z podstawowych czynnoci programistycznych jest odpowiednie rozpoznanie danych<br />
wejciowych. W taki czy inny sposób musi poradzi sobie z tym problemem kady program.<br />
Do najtrudniejszych zada naley rozszyfrowanie tego, co wytworzy bezporednio czowiek.<br />
Na przykad cige problemy sprawia wiele aspektów technologii rozpoznawania gosu. Proste<br />
wersje tego problemu, jak nasz kalkulator, mona rozwiza za pomoc gramatyk definiujcych<br />
wprowadzane dane.