16.07.2014 Views

Programowanie. Teoria i praktyka z wykorzystaniem C++

Programowanie. Teoria i praktyka z wykorzystaniem C++

Programowanie. Teoria i praktyka z wykorzystaniem C++

SHOW MORE
SHOW LESS

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.

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!