Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00089 006066 13649812 na godz. na dobę w sumie
Lekcja programowania. Najlepsze praktyki - książka
Lekcja programowania. Najlepsze praktyki - książka
Autor: , Liczba stron: 272
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-246-3226-8 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> inne - programowanie
Porównaj ceny (książka, ebook, audiobook).

Twórz zgodnie z trzema zasadami stanowiącymi kanon dobrego oprogramowania


Czy zdarzyło Ci się kiedykolwiek...

Jeśli tak, w przyszłości na pewno chciałbyś tego uniknąć! Takie problemy dla zbyt wielu programistów są niestety chlebem powszednim. Dzieje się tak między innymi dlatego, że testowanie, diagnostyka, przenośność, wydajność czy styl programowania są często traktowane po macoszemu przez osoby tworzące oprogramowanie. A świat rządzony przez olbrzymie interfejsy, wciąż zmieniające się narzędzia, języki czy systemy nie sprzyja podstawowym zasadom tworzenia dobrego kodu - prostocie, ogólności i przejrzystości.

Programowanie to coś więcej niż samo pisanie kodu. W książce 'Lekcja programowania. Najlepsze praktyki' znajdziesz opis wszystkich zagadnień, z którymi styka się programista - od projektowania, poprzez usuwanie usterek, testowanie kodu czy poprawę jego wydajności, po problemy związane z poprawianiem oprogramowania napisanego przez innych. Wszystko zostało oparte na zaczerpniętych z realnych projektów przykładach, napisanych w językach C, C++, Java i innych.

Tylko tutaj znajdziesz omówienia następujących zagadnień:

Stwórz swój własny kod w najlepszym stylu!

Znajdź podobne książki Ostatnio czytane w tej kategorii

Darmowy fragment publikacji:

Lekcja programowania. Najlepsze praktyki Autorzy: Brian W. Kernighan, Rob Pike Tłumaczenie: Łukasz Piwko ISBN: 978-83-246-3226-8 Tytuł oryginału: The Practice of Programming Format: 172×245, stron: 272 Twórz zgodnie z trzema zasadami stanowiącymi kanon dobrego oprogramowania • Prostota – czyli kod prosty i łatwy w obsłudze • Ogólność – czyli kod działający dobrze w różnych sytuacjach i adaptujący się do nowych warunków • Przejrzystość – czyli kod łatwy do zrozumienia zarówno przez ludzi, jak i maszyny Czy zdarzyło Ci się kiedykolwiek… • pominąć oczywisty błąd w programie i spędzić cały dzień na szukaniu go? • próbować wprowadzić sensowne zmiany w programie napisanym przez kogoś innego? • przepisać program od nowa, bo nie dało się go zrozumieć? Jeśli tak, w przyszłości na pewno chciałbyś tego uniknąć! Takie problemy dla zbyt wielu programistów są niestety chlebem powszednim. Dzieje się tak między innymi dlatego, że testowanie, diagnostyka, przenośność, wydajność czy styl programowania są często traktowane po macoszemu przez osoby tworzące oprogramowanie. A świat rządzony przez olbrzymie interfejsy, wciąż zmieniające się narzędzia, języki czy systemy nie sprzyja podstawowym zasadom tworzenia dobrego kodu – prostocie, ogólności i przejrzystości. Programowanie to coś więcej niż samo pisanie kodu. W książce „Praktyka programowania” znajdziesz opis wszystkich zagadnień, z którymi styka się programista – od projektowania, poprzez usuwanie usterek, testowanie kodu czy poprawę jego wydajności, po problemy związane z poprawianiem oprogramowania napisanego przez innych. Wszystko zostało oparte na zaczerpniętych z realnych projektów przykładach, napisanych w językach C, C++, Java i innych. Tylko tutaj znajdziesz omówienia następujących zagadnień: • Styl: pisanie kodu, który dobrze działa i przyjemnie się czyta • Projektowanie: wybór algorytmów i struktur danych najlepiej nadających się do określonego zadania • Interfejsy: kontrolowanie relacji między składnikami programów • Usuwanie błędów: szybkie i metodyczne wyszukiwanie błędów • Testowanie: zapewnianie niezawodności i poprawności oprogramowania • Wydajność: maksymalizowanie szybkości działania programów • Przenośność: pisanie programów, które działają wszędzie bez żadnych zmian • Notacja: wybór języków i narzędzi, które pozwalają maszynie zrobić więcej Stwórz swój własny kod w najlepszym stylu! Idź do • Spis treści • Przykładowy rozdział • Skorowidz Katalog książek • Katalog online • Zamów drukowany katalog Twój koszyk • Dodaj do koszyka Cennik i informacje • Zamów informacje o nowościach • Zamów cennik Czytelnia • Fragmenty książek online Kontakt Helion SA ul. Kościuszki 1c 44-100 Gliwice tel. 32 230 98 63 e-mail: helion@helion.pl © Helion 1991–2011 Spis treĂci 7 11 13 16 20 28 29 33 38 39 40 42 44 47 50 51 54 59 64 68 69 70 72 73 77 79 83 86 88 89 WstÚp 1. Styl 1.1. Nazwy 1.2. Wyraĝenia i instrukcje 1.3. SpójnoĂÊ i idiomy 1.4. Makra w roli funkcji 1.5. Liczby magiczne 1.6. Komentarze 1.7. Dlaczego warto dbaÊ o styl? 2. Algorytmy i struktury danych 2.1. Przeszukiwanie 2.2. Sortowanie 2.3. Biblioteki 2.4. Sortowanie szybkie w Javie 2.5. Notacja O 2.6. Tablice rozszerzalne 2.7. Listy 2.8. Drzewa 2.9. Tablice mieszania 2.10. Podsumowanie 3. Projektowanie i implementacja 3.1. Algorytm ïañcucha Markowa 3.2. Wybór struktury danych 3.3. Budowa struktury danych w jÚzyku C 3.4. Generowanie tekstu 3.5. Java 3.6. C++ 3.7. Awk i Perl 3.8. WydajnoĂÊ 3.9. Wnioski 4 SPIS TRE¥CI 4. Interfejsy 4.1. WartoĂci oddzielane przecinkami 4.2. Prototyp biblioteki 4.3. Biblioteka dla innych 4.4. Implementacja w jÚzyku C++ 4.5. Zasady projektowania interfejsów 4.6. ZarzÈdzanie zasobami 4.7. Obsïuga bïÚdów 4.8. Interfejsy uĝytkownika 5. Usuwanie bïÚdów 5.1. Programy diagnostyczne 5.2. Dobre pomysïy, ïatwe bïÚdy 5.3. Brak pomysïów, trudne bïÚdy 5.4. Ostatnia deska ratunku 5.5. BïÚdy niepowtarzalne 5.6. NarzÚdzia diagnostyczne 5.7. BïÚdy popeïnione przez innych 5.8. Podsumowanie 6. Testowanie 6.1. Testuj kod podczas jego pisania 6.2. Systematyczne testowanie 6.3. Automatyzacja testów 6.4. Ramy testowe 6.5. Testowanie przeciÈĝeniowe 6.6. Porady dotyczÈce testowania 6.7. Kto zajmuje siÚ testowaniem 6.8. Testowanie programu markov 6.9. Podsumowanie 7. WydajnoĂÊ 7.1. WÈskie gardïo 7.2. Mierzenie czasu wykonywania i profilowanie programu 7.3. Strategie przyspieszania 7.4. Regulowanie kodu 7.5. OszczÚdzanie pamiÚci 7.6. Szacowanie 7.7. Podsumowanie 8. PrzenoĂnoĂÊ 8.1. JÚzyk 8.2. Nagïówki i biblioteki 8.3. Organizacja programu 8.4. Izolacja 8.5. Wymiana danych 8.6. KolejnoĂÊ bajtów 8.7. PrzenoĂnoĂÊ a uaktualnianie 8.8. Internacjonalizacja 8.9. Podsumowanie 93 94 95 99 108 112 114 117 121 125 126 127 131 135 138 140 143 144 147 148 153 157 159 163 166 167 168 170 171 172 177 181 184 188 191 193 195 196 202 204 208 209 210 213 215 218 SPIS TRE¥CI 9. Notacja 9.1. Formatowanie danych 9.2. Wyraĝenia regularne 9.3. Programowalne narzÚdzia 9.4. Interpretery, kompilatory i maszyny wirtualne 9.5. Programy, które piszÈ programy 9.6. Generowanie kodu za pomocÈ makr 9.7. Kompilacja w locie A Epilog B Zebrane zasady Skorowidz 5 221 222 228 234 237 242 246 247 253 255 259 5 Usuwanie bïÚdów bug b. Usterka lub bïÈd w maszynie, planie itp. poch. USA. 11 marca 1889 Pall Mall Gaz. 1/1: Powiadomiono mnie, ĝe pan Edison nie Ăpi juĝ od dwóch dni, próbujÈc znaleěÊ usterkÚ (ang. bug) w swoim fonografie — wyraĝenie oznaczajÈce poszukiwanie rozwiÈzania problemu i sugerujÈce, ĝe gdzieĂ wewnÈtrz ukryï siÚ wyimaginowany insekt, który powoduje trudnoĂci. Oxford English Dictionary, wyd. 2. W poprzednich czterech rozdziaïach przedstawiliĂmy sporo przykïadów kodu i za kaĝdym razem udawaliĂmy, ĝe wszystkie one od razu prawidïowo dziaïaïy. OczywiĂcie tak nie byïo — w kaĝ- dym z nich poczÈtkowo aĝ roiïo siÚ od bïÚdów. Sïowo bug mimo iĝ nie powstaïo w Ărodowisku programistycznym, jest niewÈtpliwie jednym z najczÚĂciej uĝywanych sïów w tej dziedzinie. Dlaczego tworzenie oprogramowania jest takie trudne? Jednym z powodów jest to, ĝe na zïoĝonoĂÊ programów ma wpïyw liczba interakcji wystÚ- pujÈcych miÚdzy ich skïadnikami, a programy sÈ peïne skïadników i relacji. Istnieje wiele technik sïuĝÈcych do zmniejszania liczby powiÈzañ miÚdzy komponentami. Zalicza siÚ do nich ukrywanie informacji, abstrakcjÚ i interfejsy oraz wïaĂciwoĂci jÚzyka, które sïuĝÈ do ich reali- zowania. SÈ równieĝ techniki zapewniajÈce integralnoĂÊ projektów programów — dowodzenie poprawnoĂci programów, modelowanie, analiza wymagañ, formalna weryfikacja — ale ĝadna z nich nie zmieniïa sposobu, w jaki tworzy siÚ oprogramowanie. Wszystkie okazaïy siÚ sku- teczne tylko w rozwiÈzywaniu bardzo maïych problemów. RzeczywistoĂÊ jest taka, ĝe zawsze znajdÈ siÚ bïÚdy, które bÚdziemy wykrywaÊ za pomocÈ testowania i eliminowaÊ za pomocÈ technik usuwania bïÚdów (ang. debugging). Dobry programista wie, ĝe usuwanie bïÚdów zajmuje tyle samo czasu, co pisanie kodu, i dlatego zawsze stara siÚ wyciÈgaÊ z nich wnioski. Kaĝdy wykryty bïÈd jest naukÈ na przyszïoĂÊ, jak uniknÈÊ powtórki takiej sytuacji lub jak rozpoznaÊ, ĝe miaïa ona miejsce. Usuwanie bïÚdów to trudna i nieprzewidywalnie czasochïonna sztuka, dlatego naleĝy zro- biÊ wszystko, aby mieÊ z niÈ jak najmniej do czynienia. Sposobów na skrócenie czasu usuwania usterek jest wiele, np. staranne opracowywanie projektu, pisanie w dobrym stylu, sprawdzanie 126 5. USUWANIE B’}DÓW warunków brzegowych, stosowanie asercji i testów sensownoĂci, programowanie defensywne, projektowanie dobrych interfejsów, ograniczanie iloĂci danych globalnych oraz korzystanie z narzÚdzi diagnostycznych. Profilaktyka zawsze jest lepsza od leczenia. Jaka jest rola jÚzyka? NajwiÚkszÈ siïÈ od zawsze ksztaïtujÈcÈ ewolucjÚ jÚzyków programo- wania jest chÚÊ zapobiegania wystÚpowaniu bïÚdów poprzez odpowiednie dobranie wïaĂciwoĂci jÚzyka. Niektóre cechy jÚzyków programowania pozwalajÈ wyeliminowaÊ caïe grupy bïÚdów, np. sprawdzanie zakresu w operacjach indeksowania, ograniczenie lub wrÚcz wyïÈczenie moĝliwoĂci stosowania wskaěników, automatyczne odzyskiwanie pamiÚci, ïañcuchowe typy danych, kon- trola typów wejĂcia-wyjĂcia i rygorystyczna kontrola typów. Z drugiej strony pewne wïasnoĂci jÚzyków zwiÚkszajÈ prawdopodobieñstwo powstawania bïÚdów: instrukcje goto, zmienne globalne, nieograniczony dostÚp do wskaěników i automatyczne konwersje typów. ProgramiĂci powinni wiedzieÊ, które wïaĂciwoĂci jÚzyka sÈ potencjalnie ryzykowne, i zachowaÊ szczególnÈ ostroĝ- noĂÊ przy ich uĝywaniu. Ponadto powinni wïÈczyÊ wszystkie narzÚdzia diagnostyczne kom- pilatora i zwracaÊ uwagÚ na zgïaszane przez niego ostrzeĝenia. WïaĂciwoĂci jÚzykowe, które uniemoĝliwiajÈ powstawanie pewnych bïÚdów, majÈ swojÈ cenÚ. JeĂli jÚzyk programowania wysokiego poziomu automatycznie usuwa niektóre bïÚdy, cenÈ jest to, ĝe ïatwiej jest nam popeïniaÊ bïÚdy wyĝszego poziomu. ¿aden jÚzyk nie sprawi, ĝe caïkiem przestaniemy popeïniaÊ bïÚdy. Chociaĝ wolelibyĂmy, aby byïo inaczej, kaĝdy programista najwiÚcej czasu spÚdza na testo- waniu kodu i usuwaniu bïÚdów. W tym rozdziale omówimy techniki produktywnego i szyb- kiego usuwania bïÚdów. Do testowania wrócimy jeszcze w rozdziale 6. 5.1. Programy diagnostyczne Kompilatory najwaĝniejszych jÚzyków programowania sÈ wyposaĝone w zaawansowane pro- gramy diagnostyczne (ang. debugger). NarzÚdzia takie wchodzÈ w skïad wielu zintegrowanych Ărodowisk programistycznych oferujÈcych w jednym pakiecie narzÚdzia do pisania i edytowa- nia kodu, kompilacji oraz wykonywania utworzonych programów. Programy diagnostyczne majÈ graficzne interfejsy, za pomocÈ których moĝna wykonywaÊ kod programu po jednej in- strukcji lub funkcji albo zatrzymywaÊ wykonywanie po wykonaniu okreĂlonych wierszy lub speïnieniu zdefiniowanych warunków. Ponadto oferujÈ moĝliwoĂÊ formatowania i wyĂwietla- nia bieĝÈcych wartoĂci zmiennych. Program diagnostyczny moĝna uruchomiÊ bezpoĂrednio, jeĂli wiadomo, ĝe wystÈpiï bïÈd. Niektóre takie programy automatycznie przejmujÈ sterowanie, gdy wykryjÈ, iĝ coĂ siÚ nie po- wiodïo w czasie wykonywania programu. Zwykle wykrycie miejsca wystÈpienia bïÚdu jest nie- trudne. W tym celu naleĝy tylko sprawdziÊ sekwencjÚ funkcji, które byïy w tym czasie wykonywane (stos wywoïañ) oraz wyĂwietliÊ wartoĂci zmiennych lokalnych i globalnych. Tyle informacji czÚsto wystarcza do znalezienia ěródïa problemu. JeĂli to zawiedzie, moĝna skorzystaÊ z punk- tów wstrzymania i funkcji wykonywania programu krok po kroku, aby znaleěÊ miejsce, w którym po raz pierwszy wystÈpiïy jakieĂ anomalie. W rÚkach doĂwiadczonego programisty korzystajÈcego z dobrego Ărodowiska program dia- gnostyczny moĝe byÊ bardzo efektywnym i wydajnym narzÚdziem, które pozwala zaoszczÚdziÊ mnóstwo nerwów. Skoro dostÚpne sÈ tak wspaniaïe narzÚdzia, po co ktoĂ miaïby usuwaÊ bïÚdy, nie korzystajÈc z ich pomocy? Po co usuwaniu bïÚdów poĂwiÚcaÊ aĝ caïy rozdziaï? Istnieje ku temu kilka dobrych powodów, zarówno obiektywnych, jak i wynikajÈcych z na- szego osobistego doĂwiadczenia. Dla niektórych jÚzyków spoza gïównego nurtu nie ma ĝadne- go programu diagnostycznego albo, jeĝeli jest, jego funkcjonalnoĂÊ jest bardzo ograniczona. 5.2. DOBRE POMYS’Y, ’ATWE B’}DY 127 Ponadto dziaïanie narzÚdzi diagnostycznych zaleĝy od systemu operacyjnego, a wiÚc nie zaw- sze moĝesz mieÊ dostÚp do swoich ulubionych programów tego rodzaju. Programy diagno- styczne sïabo radzÈ sobie z niektórymi rodzajami programów, np. wieloprocesowymi i wielowÈt- kowymi, systemami operacyjnymi i systemami rozproszonymi. W takich przypadkach konieczne jest uĝycie technik niĝszego poziomu. Programista jest wówczas zdany na siebie, do dyspozycji ma tylko instrukcje drukujÈce oraz wïasne doĂwiadczenie i umiejÚtnoĂÊ analizowania kodu. OsobiĂcie staramy siÚ nie naduĝywaÊ programów diagnostycznych i ograniczamy siÚ do sprawdzenia za ich pomocÈ stosu wywoïañ oraz wartoĂci paru zmiennych. Jednym z powodów podjÚcia takiej decyzji jest to, ĝe moĝna bardzo ïatwo pogubiÊ siÚ w skomplikowanej plÈtaninie struktur danych i Ăcieĝek wykonawczych. Naszym zdaniem wykonywanie kodu krok po kroku jest mniej produktywne niĝ jego dokïadniejsze przeanalizowanie oraz dodanie kilku instrukcji wyjĂciowych i samosprawdzajÈcego siÚ kodu w krytycznych miejscach. Na przejrzenie danych zwróconych przez kilka roztropnie rozmieszczonych instrukcji drukujÈcych potrzeba mniej czasu niĝ na wykonywanie kolejnych instrukcji za pomocÈ klikniÚÊ myszÈ. PodjÚcie decyzji, gdzie wstawiÊ instrukcjÚ drukowania, zajmuje mniej czasu niĝ przechodzenie do krytycznego fragmentu kodu po jednej instrukcji, nawet jeĂli dokïadnie wiadomo, które to miejsce. Co waĝniejsze, instrukcje diagnostyczne pozostajÈ w programie, a sesje programu diagnostycznego znikajÈ. Szukanie bïÚdów po omacku za pomocÈ programu diagnostycznego rzadko bywa produk- tywne. O wiele lepiej jest uĝyÊ go do sprawdzenia stanu programu w chwili wystÈpienia usterki i na podstawie zdobytych informacji zastanowiÊ siÚ, jak mogïo do tej sytuacji dojĂÊ. Programy diagnostyczne bywajÈ niezwykle skomplikowane i trudne do opanowania. Zwïaszcza poczÈt- kujÈcy programista moĝe mieÊ z nich sto pociech i tysiÈc utrapieñ. JeĂli programowi diagno- stycznemu zada siÚ niewïaĂciwe pytanie, to zwykle zwróci on odpowiedě, ale nie wiadomo, czy poprawnÈ. Mimo to program diagnostyczny moĝe byÊ niezwykle pomocny i kaĝdy programista powinien mieÊ go pod rÚkÈ. W wielu przypadkach jest to pierwsze narzÚdzie, z którego pomocy siÚ ko- rzysta. JeĂli jednak nie masz programu diagnostycznego albo napotkasz wyjÈtkowo trudny do rozwiÈzania problem, dziÚki technikom opisanym w tym rozdziale i tak szybko wyjdziesz z opresji. Ponadto nauczysz siÚ dziÚki nim efektywniej korzystaÊ z programów diagnostycznych, gdyĝ dotyczÈ tego, jak analizowaÊ bïÚdy i szukaÊ ich prawdopodobnych przyczyn. 5.2. Dobre pomysïy, ïatwe bïÚdy Oho! CoĂ jest nie tak. Mój program padï, wydrukowaï bzdury albo nie chce przestaÊ dziaïaÊ. Co robiÊ? PoczÈtkujÈcy programiĂci w takich sytuacjach najczÚĂciej zrzucajÈ winÚ na kompilator, bi- bliotekÚ i wszystko, tylko nie ich kod. DoĂwiadczeni programiĂci teĝ by tak chcieli, ale bÚdÈc realistami, doskonale wiedzÈ, ĝe wiÚkszoĂÊ bïÚdów powstaje wyïÈcznie z ich winy. Na szczÚĂcie gïównie robimy proste bïÚdy, które moĝna wyeliminowaÊ prostymi technikami. Przeanalizuj zwrócone przez program bïÚdne dane i spróbuj wywnioskowaÊ, w jaki sposób mogïy powstaÊ. Przejrzyj dane diagnostyczne wyprodukowane przed wystÈpieniem awarii. JeĂli masz takÈ moĝliwoĂÊ, sprawdě stos wywoïañ. Po wykonaniu tych czynnoĂci bÚdziesz juĝ mieÊ jakieĂ pojÚcie na temat tego, co i gdzie siÚ staïo. PrzemyĂl to. Jak mogïo do tego dojĂÊ? Przeanalizuj zachowanie programu od poczÈtku i zastanów siÚ, co mogïo spowodowaÊ jego wadliwe dziaïanie. Diagnostyka bïÚdów wymaga analizowania w myĂlach przeszïoĂci, podobnie jak wykrywa- nie sprawców morderstw. Zdarzyïo siÚ coĂ niemoĝliwego, a jedyna informacja, jakÈ posiadamy, 128 5. USUWANIE B’}DÓW to fakt, ĝe rzeczywiĂcie miaïo to miejsce. Aby odkryÊ przyczynÚ problemów, musimy siÚ cofnÈÊ w czasie. Po znalezieniu peïnego wyjaĂnienia bÚdziemy wiedzieÊ, jak naprawiÊ program, a przy okazji prawdopodobnie odkryjemy jeszcze kilka innych rzeczy, których siÚ nie spodziewaliĂmy. Szukaj znajomych wzorców. Odpowiedz sobie na pytanie, czy juĝ coĂ takiego widziaïeĂ. Od- powiedě typu „GdzieĂ juĝ to widziaïem” zwykle stanowi pierwszy krok do zrozumienia, a nie- jednokrotnie oznacza nawet rozwiÈzanie. CzÚsto wystÚpujÈce bïÚdy majÈ pewne cechy szcze- gólne. Przykïadowo poczÈtkujÈcy programiĂci czÚsto piszÈ tak: ? int n; ? scanf( d , n); zamiast tak: int n; scanf( d , n); co zwykle koñczy siÚ próbÈ odczytu danych z miejsca poza wyznaczonym obszarem pamiÚ- ci przy pobieraniu wiersza danych wejĂciowych. Wykïadowcy jÚzyka C natychmiast rozpoznajÈ ten problem. Niewyczerpanym ěródïem prostych bïÚdów sÈ ěle dobrane typy danych i ich konwersje w funkcjach printf i scanf: ? int n = 1; ? double d = PI; ? printf( d f , d, n); Znakiem szczególnym tego rodzaju bïÚdu jest czasami pojawienie siÚ niedorzecznych war- toĂci: wielkich liczb caïkowitych albo niewiarygodnie maïych lub duĝych wartoĂci zmienno- przecinkowych. Powyĝszy program uruchomiony na komputerze SPARC firmy Sun zwróciï nastÚpujÈcÈ astronomicznÈ liczbÚ (z koniecznoĂci podzielonÈ na kilka wierszy): 1074340347 268156158598852001534108794260233396350 1936585971793218047714963795307788611480564140 0796821289594743537151163524101175474084764156 422771408323839623430144.000000 Kolejny pospolity bïÈd dotyczy wczytywania liczb typu double za pomocÈ funkcji scanf przy uĝyciu ciÈgu f zamiast lf. Niektóre kompilatory wyïapujÈ takie bïÚdy, poniewaĝ sprawdzajÈ zgodnoĂÊ typów argumentów funkcji scanf i printf z ïañcuchami formatu. Przy wïÈczonych wszystkich ostrzeĝeniach kompilator gcc w systemie GNU dla powyĝszego wywo- ïania funkcji printf zwróci nastÚpujÈce informacje: x.c:9: warning: int format, double arg (arg 2) x.c:9: warning: double format, different type arg (arg 3) Kolejny rodzaj bïÚdu, który ïatwo rozpoznaÊ po znakach szczególnych, to brak inicjalizacji zmiennej lokalnej. Wynikiem tego zaniedbania jest zwykle niesïychanie duĝa wartoĂÊ, bÚdÈca pozostaïoĂciÈ po tym, co uprzednio znajdowaïo siÚ w tym miejscu w pamiÚci. Niektóre kompi- latory mogÈ przestrzegaÊ przed takimi bïÚdami, aczkolwiek do tego konieczne moĝe byÊ wïÈ- 5.2. DOBRE POMYS’Y, ’ATWE B’}DY 129 czenie opcji sprawdzania podczas kompilacji, a poza tym — ĝaden kompilator nie wychwyci wszystkiego. Takĝe pamiÚÊ alokowana za pomocÈ takich funkcji, jak malloc, realloc i new, moĝe byÊ bezuĝyteczna, jeĂli nie zostanie zainicjalizowana. Przeanalizuj ostatniÈ zmianÚ. Jakie zmiany w programie zostaïy ostatnio wprowadzone? JeĂli rozwijajÈc program, za kaĝdym razem dodajesz do niego tylko jednÈ rzecz, to sÈ wyïÈcznie dwie moĝliwoĂci: nowy kod spowodowaï wystÈpienie bïÚdu albo ujawniï bïÈd w starym kodzie. W znalezieniu problemu pomocne jest dokïadne przejrzenie ostatnich zmian. JeĂli bïÈd wystÚ- puje w nowej wersji programu, a nie ma go w starszej, to nowy kod jest czÚĂciÈ problemu. Dla- tego trzeba zawsze zachowywaÊ przynajmniej poprzedniÈ wersjÚ programu, aby w razie kïopo- tów móc porównaÊ zachowanie z najnowszÈ wersjÈ. Ponadto naleĝy prowadziÊ rejestr wprowadzanych zmian i naprawianych bïÚdów, by nie musieÊ zdobywaÊ tych informacji na nowo, gdy trzeba bÚdzie naprawiÊ kolejny bïÈd. Pomocne sÈ w tym systemy kontroli kodu ěró- dïowego i inne techniki Ăledzenia historii zmian. Nie popeïniaj dwukrotnie tego samego bïÚdu. Gdy naprawisz jakiĂ bïÈd, zastanów siÚ, czy nie mógï on wystÈpiÊ jeszcze gdzieĂ indziej. Taka sytuacja przydarzyïa siÚ jednemu z nas krótko przed rozpoczÚciem pisania tego rozdziaïu. Miaïo to miejsce w prostym, pisanym dla kolegi prototypie przedstawiajÈcym schemat obsïugi opcjonalnych argumentów: ? for (i = 1; i argc; i++) { ? if (argv[i][0] != - ) /* Koniec opcji */ ? break; ? switch (argv[i][1]) { ? case o : /* Nazwa pliku wyjĞciowego */ ? outname = argv[i]; ? break; ? case f : ? from = atoi(argv[i]); ? break; ? case t : ? to = atoi(argv[i]); ? break; ? ... Niedïugo po wypróbowaniu programu kolega poinformowaï nas, ĝe do nazwy pliku zawsze doïÈczany byï przedrostek -o. Byïo nam wstyd, ale bïÈd okazaï siÚ ïatwy do naprawienia. Po- prawiliĂmy jednÈ instrukcjÚ: outname = argv[i][2]; Po naprawieniu tego bïÚdu i odesïaniu programu do uĝytkownika niebawem przyszïa ko- lejna wiadomoĂÊ. Tym razem program niepoprawnie obsïugiwaï argumenty typu -f123: po konwersji wartoĂÊ liczbowa zawsze wynosiïa zero. To ten sam bïÈd, co wczeĂniej. PoprawiliĂmy zatem nastÚpnÈ klauzulÚ case: from = atoi( argv[i][2]); Poniewaĝ autor siÚ spieszyï, nie zauwaĝyï, ĝe ten sam bïÈd wystÚpowaï jeszcze w dwóch in- nych miejscach, przez co zanim udaïo siÚ ostatecznie oczyĂciÊ program z kilku wystÈpieñ iden- tycznego bïÚdu, potrzebna byïa jeszcze jedna wymiana doĂwiadczeñ z naszym kolegÈ. 130 5. USUWANIE B’}DÓW W ïatwym kodzie nietrudno popeïniÊ bïÈd, poniewaĝ widzÈc znany problem, przestajemy byÊ ostroĝni. Nawet jeĂli kod jest tak prosty, ĝe mógïbyĂ go napisaÊ z zamkniÚtymi oczami, lepiej nie zamykaj oczu podczas jego pisania. Nie odkïadaj poprawiania bïÚdów na póěniej. PoĂpiech przy wykonywaniu pracy moĝe mieÊ szkodliwe skutki takĝe w innych sytuacjach. Nigdy nie ignoruj awarii. Zawsze od razu popraw bïÈd, bo moĝe siÚ nie powtórzyÊ, aĝ bÚdzie za póěno. Sïynny staï siÚ przykïad takiego niedopa- trzenia w misji sondy „Pathfinder” wysïanej na Marsa. Po jej pomyĂlnym lÈdowaniu na po- wierzchni planety w lipcu 1997 roku komputery pokïadowe resetowaïy siÚ mniej wiÚcej raz na dzieñ, co stanowiïo wielkÈ zagadkÚ dla inĝynierów. Gdy znaleěli przyczynÚ problemów, zdali sobie sprawÚ, ĝe mieli juĝ z tym do czynienia. Takie zachowania komputerów zdarzaïy siÚ juĝ w fazie wstÚpnych testów, ale zostaïy zlekcewaĝone, poniewaĝ inĝynierowie pracowali wówczas nad czymĂ innym. Zostali wiÚc zmuszeni do zajÚcia siÚ tym dopiero póěniej, gdy maszyna znajdowaïa siÚ miliony kilometrów od nich i znacznie trudniej byïo jÈ naprawiÊ. Sprawdzaj stos wywoïañ. Mimo iĝ programy diagnostyczne pozwalajÈ badaÊ programy pod- czas dziaïania, to najczÚĂciej sÈ wykorzystywane do analizowania stanu programu, który prze- staï dziaïaÊ. Do najbardziej przydatnych informacji dostarczanych przez program diagnostycz- ny naleĝy numer wiersza kodu ěródïowego, w którym wystÈpiï problem. CennÈ wskazówkÈ sÈ równieĝ nieprawdopodobne wartoĂci argumentów (puste wskaěniki, bardzo duĝe wartoĂci caï- kowite, podczas gdy spodziewane sÈ maïe, ujemne wartoĂci tam, gdzie powinny byÊ dodatnie, ïañcuchy znaków nienaleĝÈcych do alfabetu). Oto typowy przykïad z opisu algorytmów sortowania przedstawionego w rozdziale 2. Aby posortowaÊ tablicÚ liczb caïkowitych, naleĝy wywoïaÊ funkcjÚ qsort, przekazujÈc jej jako ar- gument funkcjÚ icmp porównujÈcÈ liczby caïkowite: int arr[N]; qsort(arr, N, sizeof(arr[0]), icmp); Zaïóĝmy, ĝe pomyïkowo podano nazwÚ scmp funkcji porównujÈcej ïañcuchy: ? int arr[N]; ? qsort(arr, N, sizeof(arr[0]), scmp); Jako ĝe kompilator w tym przypadku nie moĝe wykryÊ niezgodnoĂci typów, nieuchronnie napytaliĂmy sobie biedy. Program ulega awarii spowodowanej próbÈ dostÚpu do niedozwolo- nego miejsca w pamiÚci. Program diagnostyczny dbx zwraca nastÚpujÈce informacje o stosie wywoïañ (przeredagowane, aby zmieĂciïy siÚ na stronie): 0 strcmp(0xla2, 0xlc2) [ strcmp.s :31] 1 scmp(p1 = 0x10001048, p2 = 0x1000105c) [ badqs.c :13] 2 qst(0x10001048, 0x10001074, Ox400b20, 0x4) [ qsort.c :147] 3 qsort(0x10001048, 0xlc2, 0x4, 0x400b20) [ qsort.c :63] 4 main() [ badqs.c :45] 5 __istart() [ crt1tinit.s :13] Z tych danych wynika, ĝe awaria nastÈpiïa w funkcji strcmp. WidaÊ, ĝe przekazywane do niej dwa wskaěniki sÈ o wiele za maïe, co niewÈtpliwie jest oznakÈ kïopotów. W stosie wywoïañ zostaïy podane orientacyjne numery wierszy, w których nastÈpiïo wywoïanie kaĝdej funkcji. Wiersz nr 13 w naszym pliku badqs.c zawiera takie wywoïanie: 5.3. BRAK POMYS’ÓW, TRUDNE B’}DY return strcmp(v1, v2); 131 wskazujÈce na ěródïo bïÚdu. Przy uĝyciu programu diagnostycznego moĝna równieĝ wyĂwietliÊ wartoĂci zmiennych lo- kalnych i globalnych, które takĝe mogÈ naprowadziÊ nas na jakiĂ trop. Najpierw przeczytaj, a potem poprawiaj. JednÈ z najbardziej niedocenianych efektywnych technik wykrywania bïÚdów jest uwaĝne przeczytanie kodu i zastanowienie siÚ nad nim bez do- konywania jakichkolwiek zmian. Pokusa, aby chwyciÊ za klawiaturÚ i zaczÈÊ wprowadzaÊ zmiany, jest bardzo duĝa, ale naleĝy siÚ jej oprzeÊ. Istnieje duĝe ryzyko, ĝe w ten sposób nie dowiesz siÚ, co tak naprawdÚ szwankuje, i zmienisz nie to, co trzeba, pogarszajÈc jeszcze tylko sytuacjÚ. Za- pisanie najwaĝniejszej czÚĂci programu na papierze pozwala spojrzeÊ na niego z nieco innej perspektywy, niĝ oglÈdajÈc go na ekranie, i zachÚca do refleksji. Nie stosuj jednak tej techniki rutynowo. Drukowanie kodu programu to marnotrawstwo drzew, a poza tym i tak trudno ogarnÈÊ caïÈ strukturÚ kodu, jeĂli zajmuje on kilka stron. Co wiÚcej, po wprowadzeniu pierw- szej zmiany caïy wydruk nadaje siÚ do wyrzucenia. Zrób sobie krótkÈ przerwÚ. Czasami w kodzie widzisz to, co chciaïbyĂ widzieÊ, a nie to, co jest w nim rzeczywiĂcie zapisane. JeĂli na chwilÚ siÚ oderwiesz, to po powrocie moĝe zaczniesz wiÚcej uwagi zwracaÊ na prawdziwe znaczenie kodu. Oprzyj siÚ pokusie poprawiania kodu natychmiast. Warto chwilÚ siÚ przed tym zastanowiÊ. ObjaĂnij swój kod komuĂ innemu. Dobrym sposobem jest objaĂnienie napisanego przez siebie kodu innej osobie. Zdarza siÚ, ĝe w ten sposób sami odkrywamy sedno problemu. Czasami wy- starczy tylko powiedzieÊ kilka zdañ, aby stwierdziÊ ze wstydem: „Niewaĝne, juĝ wiem, co jest nie tak. Przepraszam, ĝe Ci przeszkadzam”. To niezwykle skuteczna metoda. W rolÚ sïuchacza moĝe siÚ wcieliÊ nawet osoba niebÚdÈca programistÈ. W pewnym uniwersyteckim oĂrodku komputerowym przy stanowisku pracy pomocy technicznej umieszczono pluszowego misia. Studenci chcÈcy uzyskaÊ pomoc najpierw musieli swój problem objaĂniÊ misiowi i dopiero po- tem mogli porozmawiaÊ z czïowiekiem. 5.3. Brak pomysïów, trudne bïÚdy „Nie mam zielonego pojÚcia, o co moĝe chodziÊ”. JeĂli kompletnie nie wiesz, w czym moĝe tkwiÊ problem, to zaczynajÈ siÚ schody. WymuĂ powtarzalnoĂÊ bïÚdu. PierwszÈ czynnoĂciÈ, którÈ naleĝy wykonaÊ, jest sprawienie, aby bïÈd pojawiaï siÚ na ĝÈdanie. Tropienie bïÚdu pojawiajÈcego siÚ tylko raz na jakiĂ czas nie jest przyjemne. PoĂwiÚÊ chwilÚ na sporzÈdzenie danych wejĂciowych i opracowanie takich pa- rametrów, które pozwolÈ Ci niezawodnie spowodowaÊ wystÈpienie bïÚdu za kaĝdym razem. NastÚpnie zapakuj to wszystko w jeden pakiet, aby móc go przywoïywaÊ jednym przyciskiem albo kilkoma klawiszami. JeĂli bïÈd jest trudny do wytropienia, czynnoĂci te trzeba bÚdzie po- wtórzyÊ wielokrotnie, a wiÚc lepiej je sobie maksymalnie uproĂciÊ. JeĂli bïÚdu nie da siÚ odtworzyÊ za kaĝdym razem, spróbuj zrozumieÊ dlaczego. Czy czÚsto- tliwoĂÊ jego wystÚpowania zaleĝy od jakichĂ specyficznych warunków? Nawet jeĝeli nie moĝesz wymusiÊ pojawienia siÚ bïÚdu za kaĝdym razem, warto spróbowaÊ przynajmniej skróciÊ czas oczekiwania na jego wystÈpienie. JeĂli program moĝe dostarczaÊ danych diagnostycznych, skorzystaj z tej moĝliwoĂci. Pro- gramy symulacyjne, takie jak program generujÈcy ïañcuchy Markowa z rozdziaïu 3., powinny zawieraÊ opcjÚ generowania danych diagnostycznych, np. w celu sprawdzenia wartoĂci poczÈtkowej 132 5. USUWANIE B’}DÓW generatora liczb losowych, dziÚki którym moĝna spróbowaÊ odtworzyÊ uzyskane wyniki. Wiele programów zawiera takie opcje i warto je uwzglÚdniÊ takĝe w swoich programach. Dziel i rzÈdě. Czy dane wejĂciowe wywoïujÈce awariÚ programu moĝna jakoĂ zmniejszyÊ albo bardziej skoncentrowaÊ? Stwórz minimalny zestaw danych wejĂciowych, które powodujÈ wy- stÚpowanie bïÚdu, aby zredukowaÊ liczbÚ moĝliwoĂci. Jakie zmiany powodujÈ, ĝe bïÈd przestaje siÚ pokazywaÊ? Spróbuj wyodrÚbniÊ takie przypadki testowe, które precyzyjnie koncentrujÈ siÚ na szukanym bïÚdzie. Kaĝdy taki przypadek powinien byÊ zaplanowany na uzyskanie okreĂlone- go wyniku, potwierdzajÈcego lub wykluczajÈcego pewnÈ hipotezÚ na temat ěródïa problemów. Uĝyj algorytmu przeszukiwania binarnego. OdrzuÊ poïowÚ danych wejĂciowych i sprawdě, czy program nadal zwraca niepoprawny wynik. JeĂli nie, wróÊ do poprzedniego stanu i odrzuÊ drugÈ poïowÚ danych wejĂciowych, a pierwszÈ tym razem pozostaw. TÚ samÈ metodÚ moĝna zastosowaÊ w odniesieniu do tekstu programu. Usuñ jakÈĂ czÚĂÊ kodu ěródïowego, która Twoim zdaniem nie powinna mieÊ zwiÈzku z wystÚpujÈcym bïÚdem, i sprawdě, co siÚ stanie. Przy ma- nipulowaniu duĝymi przypadkami testowymi i duĝymi iloĂciami kodu ěródïowego programu bardzo pomocny jest edytor kodu z opcjÈ cofania zmian, która zapewnia, ĝe nie utracimy bïÚdu. Przeprowadě numerycznÈ analizÚ usterek. Czasami na trop bïÚdu moĝna wpaĂÊ, analizujÈc pewne liczbowe cechy usterki. Po napisaniu jednego z podrozdziaïów tej ksiÈĝki spostrzegli- Ămy, ĝe niektóre litery gdzieĂ siÚ z niego ulotniïy. To byïo bardzo dziwne. Poniewaĝ tekst zo- staï skopiowany i wklejony do pliku z innego miejsca, doszliĂmy do wniosku, ĝe problem tkwi w funkcji kopiowania lub wklejania edytora tekstu. Ale od czego rozpoczÈÊ poszukiwanie bïÚ- du? PostanowiliĂmy dokïadniej przyjrzeÊ siÚ danym i odkryliĂmy, ĝe braki znaków wystÚpujÈ w równych odstÚpach w tekĂcie. ObliczyliĂmy, ĝe odlegïoĂÊ miÚdzy dwoma kolejnymi brakami zawsze wynosiïa 1 023 bajty. Taka regularnoĂÊ jest bardzo podejrzana. PoszukaliĂmy w kodzie ěródïowym edytora wartoĂci zbliĝonych do 1 024 i znaleěliĂmy kilka rzeczy wartych uwagi. Jedna z nich znajdowaïa siÚ w Ăwieĝo napisanym kodzie, a wiÚc postanowiliĂmy zaczÈÊ od niej. Szybko spostrzegliĂmy bïÈd. Byïa to klasyczna pomyïka o jeden, która powodowaïa, ĝe zerowy bajt kasowaï ostatni znak w buforze o rozmiarze 1 024 bajtów. Na trop bïÚdu wpadliĂmy dziÚki przeanalizowaniu liczbowych wïaĂciwoĂci zwiÈzanych z uster- kÈ. Ile czasu nam to zajÚïo? Kilka minut spÚdziliĂmy w osïupieniu, piÚÊ minut zajÚïo nam od- krycie prawidïowoĂci w znikaniu znaków i kolejnych piÚciu minut potrzebowaliĂmy na znale- zienie i usuniÚcie bïÚdu. RozwiÈzanie tego problemu przy uĝyciu programu diagnostycznego byïoby bardzo trudne, gdyĝ w grÚ wchodziïy dwa wieloprocesowe programy obsïugiwane za pomocÈ myszy i komunikujÈce siÚ ze sobÈ poprzez system plików. WyĂwietlaj dodatkowe informacje, aby zorientowaÊ siÚ, jak dziaïa program. JeĂli nie rozu- miesz, co robi kod, to najïatwiejszym i najmniej kosztownym wydajnoĂciowo sposobem na do- wiedzenie siÚ tego jest dodanie instrukcji wyĂwietlajÈcych róĝne informacje. W ten sposób moĝna upewniÊ siÚ co do sïusznoĂci swoich ocen lub zweryfikowaÊ hipotezy na temat tego, co dziaïa ěle. JeĂli np. wydaje Ci siÚ, ĝe niemoĝliwe jest dotarcie do pewnej czÚĂci kodu, dodaj in- strukcjÚ wyĂwietlajÈcÈ informacjÚ: „Nie moĝna tu wejĂÊ”. Jeĝeli póěniej komunikat ten zosta- nie pokazany, przesuñ wyĂwietlajÈcÈ go instrukcjÚ nieco wyĝej, aby dowiedzieÊ siÚ, w którym miejscu zaczynajÈ siÚ kïopoty. Analogicznie moĝesz teĝ wyĂwietlaÊ informacjÚ: „Udaïo siÚ tu wejĂÊ” i przesuwaÊ jÈ stopniowo coraz dalej, by znaleěÊ ostatnie miejsce, w którym nic zïego siÚ nie dzieje. Komunikaty powinny róĝniÊ siÚ od siebie, aby za kaĝdym razem byïo wiadomo, który zostaï wyĂwietlony. Komunikaty powinny byÊ zwiÚzïe i zawsze mieÊ jednakowy format, aby dawaïy siÚ ïatwo przeanalizowaÊ programiĂcie lub programom pomocniczym, takim jak np. narzÚdzie grep sïu- ĝÈce do porównywania wzorców. Programy podobne do grep sÈ nieocenionym wsparciem przy 5.3. BRAK POMYS’ÓW, TRUDNE B’}DY 133 przeszukiwaniu tekstu — prostÈ implementacjÚ takiego narzÚdzia przedstawiamy w rozdziale 9. JeĂli wyĂwietlasz wartoĂci zmiennych, to za kaĝdym razem formatuj komunikat w taki sam sposób. W jÚzykach C i C++ wskaěniki prezentuj w postaci liczb szesnastkowych przy uĝyciu specyfikatorów formatu x lub p. DziÚki temu dowiesz siÚ, czy dwa wskaěniki majÈ tÚ samÈ wartoĂÊ bÈdě sÈ ze sobÈ w jakiĂ sposób powiÈzane. Naucz siÚ odczytywaÊ wartoĂci wskaěników oraz rozpoznawaÊ prawdopodobne i nieprawdopodobne wartoĂci, np. zero, liczby ujemne, nie- typowe wartoĂci i maïe liczby. Takĝe znajomoĂÊ formatów adresów przydaje siÚ podczas uĝy- wania programu diagnostycznego. JeĂli jest moĝliwoĂÊ, ĝe program zwróci bardzo duĝÈ iloĂÊ danych, to moĝe dane te wystar- czy wydrukowaÊ w postaci pojedynczych liter, np. A, B itd., aby zwiÚěle pokazaÊ, dokÈd pro- gram doszedï. Pisz samosprawdzajÈcy siÚ kod. JeĂli potrzebujesz wiÚcej informacji, to moĝesz napisaÊ wïa- snÈ funkcjÚ sprawdzajÈcÈ okreĂlony warunek, wyĂwietlajÈcÈ wartoĂci odpowiednich zmiennych i zamykajÈcÈ program: /* check: sprawdza warunek, drukuje i koĔczy dziaáanie */ void check(char *s) { if (var1 var2) { printf( s: var1 d var2 d , s, var1, var2); fflush(stdout); /* Zapewnia wysáanie wszystkich danych na wyjĞcie */ abort(); /* Sygnalizuje nienormalne zakoĔczenie dziaáania programu */ } } Funkcja check wywoïuje standardowÈ funkcjÚ jÚzyka C o nazwie abort, która przedwcze- Ănie koñczy dziaïanie programu w celu umoĝliwienia jego analizy w programie diagnostycznym. OczywiĂcie funkcjÚ check moĝna teĝ zmieniÊ w taki sposób, aby po wydrukowaniu informacji nie zamykaïa programu. NastÚpnie wywoïaj funkcjÚ check wszÚdzie tam, gdzie tego potrzebujesz: check( Przed podejrzanym kodem ); /* … Podejrzany kod … */ check( Za podejrzanym kodem ); Po naprawieniu bïÚdu nie usuwaj funkcji check z kodu ěródïowego. UmieĂÊ jÈ w komenta- rzu albo wyïÈcz jÈ za pomocÈ opcji programu diagnostycznego, aby móc jej uĝyÊ ponownie, gdy wystÈpi kolejny trudny do rozwiÈzania problem. JeĂli pojawiÈ siÚ takie problemy, zakres obowiÈzków funkcji moĝna rozszerzyÊ np. o weryfikacjÚ i wyĂwietlanie struktur danych. Moĝna nawet zastosowaÊ bardziej ogólne podejĂcie i napisaÊ procedurÚ na bieĝÈco sprawdzajÈcÈ spójnoĂÊ struktur danych i innych informacji. W programach, w których wykorzystywane sÈ skomplikowane struktury danych, warto takie funkcje napisaÊ, zanim jeszcze pojawiÈ siÚ problemy, i uczyniÊ je integralnÈ czÚĂciÈ programu. Wówczas w razie kïopotów moĝna je bez przeszkód wïÈczyÊ. Nie ograniczaj siÚ do korzystania z nich tylko pod- czas usuwania bïÚdów. Moĝesz ich uĝywaÊ we wszystkich fazach rozwoju programu, a jeĂli nie pochïaniajÈ zbyt duĝo zasobów, to nawet warto je pozostawiÊ wïÈczone caïy czas. W duĝych programach, takich jak systemy komutacyjne w komunikacji, czÚsto znacznÈ czÚĂÊ kodu stanowiÈ podprogramy monitorujÈce przepïywajÈce informacje i sprzÚt i zgïaszajÈce wszelkie usterki, niekiedy nawet automatycznie je naprawiajÈc. 134 5. USUWANIE B’}DÓW Utwórz dziennik. Kolejnym sposobem jest utworzenie pliku dziennika, w którym bÚdÈ zapi- sywane dane diagnostyczne w ĂciĂle okreĂlonym formacie. W razie wystÈpienia awarii w pliku takim powinien znaleěÊ siÚ zapis tego, co dziaïo siÚ tuĝ przed tym wydarzeniem. Serwery sie- ciowe i inne programy dziaïajÈce w sieci utrzymujÈ dzienniki, w których zapisujÈ ogromne iloĂci informacji o ruchu sieciowym — na ich podstawie kontrolujÈ siebie i swoich klientów. Poniĝej przedstawiamy fragment takiego pliku pochodzÈcego z lokalnego systemu (tekst dopasowany do strony): [Sun Dec 27 16:19:24 1998] HTTPd: access to /usr/local/httpd/cgi-bin/test.html failed for m1.cs.bell-labs.com, reason: client denied by server (CGI non-executable) from http://m2.cs.bell-labs.com/cgi-bin/test.pl Aby w pliku dziennika pojawiïy siÚ rekordy danych, trzeba pamiÚtaÊ o zapisaniu w nim zawartoĂci buforów wejĂcia i wyjĂcia. Funkcje wyjĂciowe, takie jak printf, zwykle buforujÈ swoje wyniki, aby zoptymalizowaÊ dziaïanie operacji drukowania. Przy nienormalnym zakoñ- czeniu pracy programu informacje te mogÈ zostaÊ utracone. W jÚzyku C zapisanie wszystkich tego typu danych przed zamkniÚciem programu moĝna wymusiÊ za pomocÈ funkcji fflush. Jej odpowiednikiem w jÚzykach C++ i Java jest funkcja flush zapisujÈca dane ze strumieni wyj- Ăciowych. Jeĝeli nie przeszkadzajÈ Ci dodatkowe koszty wydajnoĂciowe, problem moĝesz roz- wiÈzaÊ raz na zawsze, wyïÈczajÈc buforowanie operacji zapisu danych w dzienniku. SïuĝÈ do tego standardowe funkcje o nazwach setbuf i setvbuf. Wywoïanie funkcji setbuf(fp, NULL) spowoduje wyïÈczenie buforowania w strumieniu fp. Standardowe strumienie bïÚdów (stderr, cerr i System.err) majÈ domyĂlnie wyïÈczone buforowanie. Rysuj obrazy. Czasami w testowaniu i usuwaniu bïÚdów doskonaïÈ pomocÈ sÈ obrazy. Oczywi- Ăcie najbardziej pomagajÈ w zrozumieniu struktur danych, o czym przekonaliĂmy siÚ w roz- dziale 2., i w pisaniu programów graficznych, ale to nie jedyne ich zastosowania. Na wykresie punktowym lepiej widaÊ rozkïad wartoĂci niĝ w kolumnach liczb. Na histogramie moĝna ïa- twiej wychwyciÊ anomalie w ocenach z egzaminów, losowych liczbach, rozmiarach kubeïków alokowanych przez specjalne funkcje i uĝywanych w tablicach mieszania itd. JeĂli nie rozumiesz, co dzieje siÚ w Twoim programie, spróbuj sobie pomóc, opatrujÈc struktury danych danymi statystycznymi, które dodatkowo przedstaw w postaci wykresu. Po- niĝej zaprezentowano wykresy sporzÈdzone dla programu Markowa z rozdziaïu 3. w wersji na- pisanej w jÚzyku C. Na oĂ x zostaïy naniesione dïugoĂci ïañcuchów mieszania, a na oĂ y — licz- by elementów w tych ïañcuchach. Jako danych wejĂciowych uĝyliĂmy naszego standardowego tekstu z KsiÚgi Psalmów (42 685 sïów, 22 482 przedrostki). Pierwsze dwa wykresy zostaïy spo- rzÈdzone dla dobrych mnoĝników 31 i 37, a trzeci — dla koszmarnej wartoĂci 128. W dwóch pierwszych przypadkach dïugoĂÊ ĝadnego ïañcucha nie przekracza 15 lub 16 elementów, a wiÚk- szoĂÊ ïañcuchów skïada siÚ z 5 i 6 elementów. W trzecim przypadku dane sÈ bardziej rozpro- szone, najdïuĝszy ïañcuch ma 187 elementów i wystÚpuje bardzo duĝo ïañcuchów zawierajÈ- cych po 20 i wiÚcej elementów. 5.4. OSTATNIA DESKA RATUNKU 135 Korzystaj z narzÚdzi. Dobrze uĝyj narzÚdzi oferowanych przez swoje Ărodowisko pracy. Na przykïad program porównujÈcy pliki, taki jak diff, zestawia wyniki programu, którego wyko- nywanie zakoñczyïo siÚ powodzeniem, i takiego, którego wykonywanie zakoñczyïo siÚ niepo- wodzeniem, dziÚki czemu moĝna przeanalizowaÊ róĝnice. JeĂli program diagnostyczny zwraca duĝe iloĂci danych, to przeszukuj je za pomocÈ takiego programu jak grep oraz analizuj przy uĝyciu edytora. Wstrzymaj siÚ od drukowania na papierze danych diagnostycznych: kompute- ry lepiej radzÈ sobie z analizÈ duĝych iloĂci danych niĝ ludzie. Uĝyj skryptów powïoki, aby zautomatyzowaÊ proces przetwarzania danych diagnostycznych. Pisz proste programy do weryfikacji hipotez i swojego zrozumienia sposobu dziaïania kodu. Czy moĝna np. zwolniÊ pusty wskaěnik? int main (void) { free(NULL); return 0; } Programy do kontroli kodu ěródïowego, takie jak RCS, umoĝliwiajÈ rejestracjÚ kolejnych wersji programu, dziÚki czemu moĝna sprawdziÊ, jakie zmiany zostaïy wprowadzone, i w razie potrzeby przywróciÊ jednÈ ze starszych wersji. Oprócz funkcji podglÈdu najnowszych zmian programy te oferujÈ równieĝ moĝliwoĂÊ znalezienia najczÚĂciej modyfikowanych fragmentów kodu. W tych miejscach czÚsto kryjÈ siÚ rozmaite bïÚdy. Pisz dokumentacjÚ. JeĂli poszukiwania ěródïa problemów bÚdÈ siÚ przeciÈgaÊ, po pewnym czasie zapomnisz, co juĝ zostaïo sprawdzone, a czego jeszcze nie wiesz. Jeĝeli zaczniesz zapisy- waÊ wykonane testy i ich wyniki, to bÚdziesz mieÊ pewnoĂÊ, ĝe niczego nie przeoczysz. NotujÈc informacje o problemie, lepiej zapamiÚtasz, ĝe kiedyĂ juĝ coĂ podobnego widziaïeĂ, a przy oka- zji bÚdziesz mieÊ pomoc, gdy zechcesz objaĂniÊ problem komuĂ innemu. 5.4. Ostatnia deska ratunku Co robiÊ, jeĂli ĝadna z wymienionych technik nie pomaga? Teraz moĝe nadeszïa pora na wy- konanie programu krok po kroku w programie diagnostycznym. JeĂli masz kompletnie bïÚdne wyobraĝenie o tym, jak coĂ dziaïa, przez co szukasz problemu w niewïaĂciwym miejscu albo szukasz tam, gdzie trzeba, lecz go nie widzisz, program diagnostyczny moĝe zmusiÊ CiÚ do 136 5. USUWANIE B’}DÓW spojrzenia na sprawy z innej perspektywy. BïÚdy niedajÈce siÚ wykryÊ z powodu niewïaĂciwego rozumienia istoty problemu sÈ najgorsze. W takich przypadkach mechaniczna pomoc jest bez- cenna. Czasami bïÚdne przekonanie dotyczy bardzo prostych zagadnieñ, sÈ to np.: niepoprawna kolejnoĂÊ wykonywania operatorów, uĝycie niewïaĂciwego operatora, wciÚcia kodu niezgodne z jego strukturÈ czy bïÚdy zakresu dostÚpnoĂci zmiennych polegajÈce na tym, ĝe zmienna lo- kalna zasïania zmiennÈ globalnÈ albo zmienna globalna wcina siÚ w zakres lokalny. Programi- Ăci czÚsto zapominajÈ przykïadowo o tym, ĝe operatory i | stojÈ dalej w kolejce do wykonania niĝ operatory == i !=. Dlatego zdarza im siÚ pisaÊ taki kod: ? if (x 1 == 0) ? ... i nie mogÈ zrozumieÊ, dlaczego ten warunek nigdy nie jest speïniony. Czasami poĂlizgnie siÚ palec i omyïkowo zamiast jednego znaku równoĂci napiszÈ siÚ dwa albo odwrotnie: ? while ((c == getchar()) != EOF) ? if (c = ) ? break; Albo podczas pracy nad programem nie zostanie usuniÚty niepotrzebny kod: ? for (i = 0; i n; i++); ? a[i++] = 0; Niektóre problemy wynikajÈ z poĂpiechu: ? switch (c) { ? case : ? mode = LESS; ? break; ? case : ? mode = GREATER; ? break; ? defualt: ? mode = EQUAL; ? break; ? } Czasami wpisanie argumentów w niepoprawnej kolejnoĂci powoduje bïÈd, którego nie moĝna wykryÊ przez mechanizm sprawdzania typów, np.: ? memset(p, n, 0); /* Zapisuje n zer w p */ zamiast memset(p, 0, n); /* Zapisuje n zer w p */ Czasami coĂ zostaje zmienione bez wiedzy programisty, np. nie wiemy, ĝe jakaĂ procedura moĝe zmieniaÊ pewne globalne lub wspóïuĝytkowane zmienne. 5.4. OSTATNIA DESKA RATUNKU 137 Nieraz uĝyty algorytm lub struktura danych zawierajÈ fatalny bïÈd, którego po prostu nie dostrzegamy. PrzygotowujÈc materiaïy do omówienia list powiÈzanych, sporzÈdziliĂmy pakiet funkcji sïuĝÈcych do tworzenia nowych elementów listy oraz doïÈczania ich na poczÈtku i koñ- cu struktury danych itp. (funkcje te moĝna obejrzeÊ w rozdziale 2.). OczywiĂcie sprawdziliĂmy, czy wszystko jest w porzÈdku za pomocÈ specjalnie napisanego w tym celu programu testowego. Kilka pierwszych testów zostaïo zakoñczonych pomyĂlnie, ale w pewnym momencie nastÈpiïa efektowna awaria. Oto kod ěródïowy tamtego programu: ? while (scanf( s d , name, value) != EOF) { ? p = newitem(name, value); ? list1 = addfront(list1, p); 7 list2 = addend(list2, p); ? } ? for (p = list1; p != NULL; p = p- next) ? printf( s d , p- name, p- value); Aĝ trudno uwierzyÊ, ile kïopotów sprawiïo nam dostrzeĝenie, ĝe pierwsza pÚtla umieszczaïa ten sam wÚzeï p w obu listach, przez co gdy przystÚpowaliĂmy do drukowania, wskaěniki byïy beznadziejnie pomieszane. Takie bïÚdy sÈ trudne do wykrycia, poniewaĝ podĂwiadomie widzimy to, co chcielibyĂmy widzieÊ. Dlatego w takich przypadkach pomocny jest program diagnostyczny, który zmusza nas do zastanowienia siÚ nad innymi moĝliwoĂciami i przeĂledzenia rzeczywistego dziaïania programu zamiast myĂlenia o tym, co on powinien robiÊ. Czasami problem wynika z bïÚdu w ogólnej strukturze programu. Aby wykryÊ coĂ takiego, trzeba ponownie przejrzeÊ swoje wstÚp- ne zaïoĝenia. Zauwaĝmy przy okazji, ĝe w przykïadzie dotyczÈcym list bïÈd znajdowaï siÚ w kodzie te- stujÈcym, co znacznie utrudniaïo jego znalezienie. To straszne, jak ïatwo moĝna zmarnowaÊ czas na poszukiwaniu bïÚdów, których nie ma, bo problem tkwi w programie testujÈcym, albo na testowaniu niewïaĂciwej wersji programu tudzieĝ poniewaĝ zaniedbaïo siÚ aktualizacjÚ bÈdě kompilacjÚ programu przed wznowieniem testowania. JeĂli mimo znacznego wysiïku nie uda Ci siÚ znaleěÊ bïÚdu, to zrób sobie przerwÚ. Odpocznij i chwilowo zajmij siÚ czymĂ innym. Porozmawiaj z kolegÈ i poproĂ go o pomoc. RozwiÈzanie moĝe pojawiÊ siÚ nagle, nie wiadomo skÈd, a nawet jeĂli nie, po powrocie do pracy nie bÚdziesz juĝ tkwiÊ w tym samym zauïku. Zdarza siÚ teĝ, choÊ niezwykle rzadko, ĝe ěródïem problemów jest kompilator, biblioteka, system operacyjny, a nawet sprzÚt. Moĝna to podejrzewaÊ zwïaszcza wówczas, gdy bïÈd wystÈ- piï bezpoĂrednio po wprowadzeniu zmian w Ărodowisku. Nigdy nie naleĝy rozpoczynaÊ szuka- nia bïÚdów od tych miejsc, ale po wykluczeniu wszystkich innych moĝliwoĂci to moĝe byÊ ostatnie, co nam zostanie. KiedyĂ przenosiliĂmy duĝy program do formatowania tekstu z sys- temu Unix do komputera PC. Kompilacja zakoñczyïa siÚ bez ĝadnych problemów, ale program dziaïaï bardzo dziwnie: opuszczaï mniej wiÚcej co drugi znak w danych wejĂciowych. PierwszÈ naszÈ myĂlÈ byïo to, ĝe ma to jakiĂ zwiÈzek z uĝywaniem 16-bitowych liczb caïkowitych za- miast 32-bitowych albo z kolejnoĂciÈ bajtów. Jednak po wydrukowaniu znaków, tak jak byïy przed- stawiane pÚtli gïównej, odkryliĂmy, ĝe bïÈd tkwiï w standardowym pliku nagïówka ctype.h dostarczanym przez producenta kompilatora. Zawieraï on implementacjÚ funkcji isprint w postaci makra funkcyjnego: ? #define isprint(c) ((c) = 040 (c) 0177) a gïówna pÚtla pobierania danych byïa zdefiniowana nastÚpujÈco: 138 ? while (isprint(c = getchar())) ? ... 5. USUWANIE B’}DÓW Za kaĝdym razem, gdy na wejĂciu pojawiaïa siÚ spacja (o wartoĂci ósemkowej 40, którÈ sto- suje siÚ w zïym stylu zamiast zapisu ) lub znak o wyĝszym numerze, funkcja getchar byïa wywoïywana po raz drugi, poniewaĝ makro ewaluowaïo swój argument dwa razy, przy czym pierwszy znak znikaï bezpowrotnie. Nasz kod ěródïowy moĝe nie byï szczytem elegancji — warunek pÚtli mógïby byÊ prostszy — ale plik nagïówkowy od dostawcy kompilatora bez naj- mniejszych wÈtpliwoĂci zawieraï bïÈd. Przykïady tego bïÚdu moĝna spotkaÊ do dziĂ. Poniĝsze makro pochodzi z wciÈĝ uĝywanych plików nagïówkowych innego producenta: ? #define __iscsym(c) (isalnum(c) || ((c) == _ )) Obfitym ěródïem bïÚdów powodujÈcych nienormalne dziaïanie programów sÈ wycieki pamiÚci, tzn. przypadki nieodzyskania nieuĝywanych juĝ fragmentów pamiÚci. Kolejnym jest niezamy- kanie plików, które prowadzi do zapeïnienia tablicy plików otwartych, przez co nie moĝna otwieraÊ nastÚpnych. Awarie programów zawierajÈcych wycieki pamiÚci czÚsto wyglÈdajÈ bar- dzo tajemniczo. Poniewaĝ do usterki dochodzi po wyczerpaniu pewnych zasobów, nie da siÚ odtworzyÊ specyficznych zdarzeñ. Z rzadka kïopoty sprawia sprzÚt. W procesorze Pentium z 1994 roku wystÚpowaï bïÈd, któ- ry powodowaï, ĝe niektóre obliczenia na liczbach zmiennoprzecinkowych dawaïy zïe wyniki. Ta szeroko nagïoĂniona usterka w projekcie urzÈdzenia duĝo firmÚ kosztowaïa, ale gdy juĝ jÈ zidentyfikowano, bïÈd daïo siÚ powtarzaÊ. Jeden z najdziwniejszych bïÚdów, jakie widzieliĂmy w swojej karierze, znajdowaï siÚ w starym programie kalkulatora dziaïajÈcym w systemie dwu- procesorowym. Czasami dla wyraĝenia 0/5 zwracaï wartoĂÊ 0.5, a niekiedy drukowaï jakÈĂ in- nÈ wartoĂÊ, typu 0.7432, choÊ trzeba przyznaÊ, ĝe jak juĝ to robiï, to konsekwentnie. Nie daïo siÚ w ĝaden sposób przewidzieÊ, czy w danym przypadku wynik bÚdzie poprawny, czy nie. W koñcu odkryto, iĝ ěródïem problemu jest usterka w jednostce odpowiedzialnej za obliczenia zmiennoprzecinkowe w jednym z procesorów. Poniewaĝ do wykonywania kalkulatora byï lo- sowo wybierany albo jeden, albo drugi procesor, raz wyniki byïy poprawne, a innym razem niedorzeczne. Wiele lat temu uĝywaliĂmy maszyny, której wewnÚtrznÈ temperaturÚ moĝna byïo oszacowaÊ na podstawie liczby niepoprawnych bitów niskich w obliczeniach zmiennoprzecinkowych. Oblu- zowaïa siÚ jedna z kart ukïadu elektronicznego i w miarÚ jak rosïa temperatura, karta ta od- chylaïa siÚ coraz bardziej, co powodowaïo, ĝe wiÚcej bitów zostawaïo odciÚtych od pïyty mon- taĝowej. 5.5. BïÚdy niepowtarzalne Najtrudniejsze do wytropienia sÈ te bïÚdy, które pojawiajÈ siÚ nieregularnie — najczÚĂciej przyczynÈ ich powstawania nie jest banalne uszkodzenie sprzÚtu. Jednak cennÈ wskazówkÈ jest juĝ sam fakt, ĝe tak siÚ zachowujÈ. Moĝna dedukowaÊ, ĝe prawdopodobnÈ przyczynÈ bïÚdu jest nie usterka w algorytmie, lecz raczej to, iĝ program korzysta z danych, które za kaĝdym razem sÈ inne. Sprawdě, czy wszystkie zmienne sÈ zainicjalizowane. Moĝliwe, ĝe któraĂ z nich otrzymuje losowÈ wartoĂÊ odpowiadajÈcÈ temu, co byïo ostatnio zapisane w przypisywanym jej obszarze 5.5. B’}DY NIEPOWTARZALNE 139 pamiÚci. W jÚzykach C i C++ najczÚstszymi sprawcami sÈ zmienne lokalne funkcji i pamiÚÊ uzyskiwana za pomocÈ funkcji alokujÈcych. Wszystkim zmiennym przypisz konkretne warto- Ăci. JeĂli w programie uĝywana jest wartoĂÊ poczÈtkowa generatora liczb losowych, której czÚ- sto nadaje siÚ wartoĂÊ na podstawie aktualnej daty, to przypisz jej jakÈĂ staïÈ wartoĂÊ, np. 0. Jeĝeli dodanie kodu diagnostycznego powoduje zmianÚ zachowania lub wrÚcz znikniÚcie bïÚdu, to moĝna podejrzewaÊ nieprawidïowoĂÊ przy alokacji pamiÚci — jakaĂ instrukcja zapi- suje dane poza przydzielonym obszarem i dodanie kodu diagnostycznego wprowadza modyfi- kacjÚ rozmieszczenia elementów w pamiÚci, której skutkiem jest zmiana efektu wywoïywanego przez bïÈd. WiÚkszoĂÊ funkcji wyjĂciowych, od printf po funkcje okien dialogowych, alokuje pamiÚÊ samodzielnie, co dodatkowo zaciemnia obraz. JeĂli miejsce awarii wydaje siÚ odlegïe od wszystkiego, co mogïoby byÊ zepsute, to najbar- dziej prawdopodobnÈ przyczynÈ problemu jest bïÚdne zmienienie zawartoĂci obszaru pamiÚci w miejscu, które jest uĝywane dopiero póěniej. Czasami problem dotyczy tzw. wiszÈcego wskaěnika, czyli omyïkowego zwrócenia przez funkcjÚ wskaěnika na zmiennÈ lokalnÈ i póě- niejszego jego uĝycia. ¥rodkiem profilaktycznym przed takÈ odroczonÈ katastrofÈ jest zwróce- nie adresu zmiennej lokalnej: ? char *msg(int n, char *s) ? { ? char buf[100]; ? ? sprintf (buf, BïÈd d: s , n, s); ? return buf; ? } Zanim wskaěnik zwrócony przez funkcjÚ msg zostanie uĝyty, bÚdzie juĝ wskazywaï nic nie- znaczÈce miejsce w pamiÚci. Musisz przydzieliÊ pamiÚÊ za pomocÈ funkcji malloc, uĝyÊ sta- tycznej tablicy albo zaĝÈdaÊ, aby wywoïujÈcy dostarczyï pamiÚÊ. Uĝycie dynamicznie alokowanej wartoĂci juĝ po jej zwolnieniu objawia siÚ w podobny spo- sób. WspominaliĂmy o tym w rozdziale 2., przy okazji omawiania funkcji freeall. Poniĝszy kod zawiera bïÈd: ? for (p = listp; p != NULL; p = p- next) ? free (p); PamiÚci, która zostaïa zwolniona, nie wolno uĝywaÊ, poniewaĝ jej zawartoĂÊ mogïa siÚ zmieniÊ i nie ma pewnoĂci, ĝe instrukcja p- next wciÈĝ wskazuje wïaĂciwe miejsce w pamiÚci. W niektórych implementacjach funkcji malloc i free dwukrotne zwolnienie elementu po- woduje uszkodzenie wewnÚtrznych struktur danych, ale nie wywoïuje to ĝadnych kïopotów przez dïuĝszy czas, dopóki kolejne wywoïanie nie wywróci siÚ na tym baïaganie. Pewne funkcje alokacyjne majÈ opcje diagnostyczne, za pomocÈ których moĝna sprawdziÊ spójnoĂÊ pola dzia- ïañ przed kaĝdym wywoïaniem. WïÈcz je, jeĂli próbujesz wytropiÊ nieregularnie zachowujÈcy siÚ bïÈd. Jeĝeli w ten sposób nic nie wskórasz, moĝesz napisaÊ wïasnÈ funkcjÚ alokujÈcÈ, która mogïaby sprawdzaÊ niespójnoĂÊ swoich wïasnych zachowañ albo zapisywaÊ w dzienniku wszystkie wywoïania, aby moĝna je byïo póěniej przeanalizowaÊ. Napisanie funkcji alokujÈcej pamiÚÊ, gdy nie zaleĝy nam bardzo na szybkoĂci dziaïania, jest ïatwe, a wiÚc strategiÚ tÚ moĝna wykonaÊ, jeĝeli problem jest powaĝny. IstniejÈ teĝ Ăwietne komercyjne narzÚdzia sïuĝÈce do sprawdzania zarzÈdzania pamiÚciÈ oraz wykrywajÈce bïÚdy i wycieki pamiÚci. JeĂli nie masz do nich dostÚpu, moĝesz wykorzystaÊ niektóre z ich zalet, piszÈc wïasne funkcje malloc i free. 140 5. USUWANIE B’}DÓW Jeĝeli jedna osoba nie ma problemów z programem, a inna ma, to znaczy, ĝe istnieje jakaĂ usterka, która ujawnia siÚ tylko w okreĂlonych warunkach. Odpowiedzialne za to mogÈ byÊ jakieĂ pliki wczytane przez program, prawa dostÚpu do plików, zmienne Ărodowiskowe, Ăcieĝki dostÚpu poleceñ, ustawienia domyĂlne lub pliki uĝywane podczas uruchamiania programu. Trudno cokolwiek w takich sytuacjach doradziÊ, poniewaĝ aby odtworzyÊ Ărodowisko, w którym program zawodzi, trzeba byÊ tÈ drugÈ osobÈ. mwiczenie 5.1. Napisz wïasne wersje funkcji malloc i free, których bÚdzie moĝna uĝyÊ do rozwiÈzywania problemów z zarzÈdzaniem pamiÚciÈ. Jednym z rozwiÈzañ moĝe byÊ sprawdza- nie w kaĝdym wywoïaniu caïej przestrzeni roboczej. Odmiennym podejĂciem jest zapisywanie danych diagnostycznych w dzienniku, aby mogïy zostaÊ przetworzone przez inny program. Bez wzglÚdu na to, którÈ metodÚ wybierzesz, na poczÈtku i koñcu kaĝdego alokowanego bloku dodaj znaczniki, by ujawniÊ ewentualne przypadki przekroczenia zakresu z obu stron. 5.6. NarzÚdzia diagnostyczne W znajdowaniu bïÚdów pomocne sÈ nie tylko programy diagnostyczne. Istnieje wiele innych narzÚdzi, które mogÈ nam pomóc dotrzeÊ do waĝnych informacji w wielkich zbiorach danych, znaleěÊ anomalie lub tak zmieniÊ ukïad danych, aby ïatwiej moĝna byïo zobaczyÊ, co siÚ dzieje. Wiele z nich znajduje siÚ w standardowym wyposaĝeniu warsztatu. Niektóre zostaïy napisane w celu znalezienia konkretnego bïÚdu lub przeanalizowania specyficznego problemu. W tym podrozdziale omówimy prosty program o nazwie strings, który jest szczególnie pomocny w przeglÈdaniu plików skïadajÈcych siÚ gïównie ze znaków niedrukowalnych, a wiÚc np. plików wykonywalnych i tajemniczych formatów binarnych uĝywanych przez niektóre edytory tekstu. We wnÚtrzu czÚsto kryjÈ siÚ róĝne cenne informacje, takie jak tekst dokumen- tu, komunikaty o bïÚdach i nieudokumentowanych opcjach, nazwy plików i katalogów, a takĝe nazwy funkcji, które mogïy byÊ wywoïane przez program. Programu strings uĝywamy równieĝ do znajdowania tekstu w innych plikach binarnych. Wiele plików graficznych zawiera znaki ASCII opisujÈce program, w którym zostaïy utworzone, a pliki skompresowane i archiwa (np. ZIP) mogÈ zawieraÊ nazwy plików. Wszystkie te infor- macje moĝna odkryÊ za pomocÈ programu strings. W systemach uniksowych istnieje juĝ implementacja programu strings, chociaĝ nieco inna od tej, którÈ przedstawimy tutaj. Rozpoznaje ona programy na wejĂciu i bada tylko tekst i seg- menty danych, ignorujÈc tablicÚ symboli. Za pomocÈ opcji -a moĝna jÈ zmusiÊ do zbadania caïego pliku. Program strings pobiera tekst ASCII z plików binarnych, tak ĝe moĝna go póěniej wczy- taÊ lub przetworzyÊ przez inne programy. JeĂli znaleziony komunikat o bïÚdzie nie ma ĝadnego identyfikatora, to moĝe byÊ trudno odgadnÈÊ, jaki program go zgïosiï, nie mówiÈc juĝ, dlacze- go to zrobiï. Wówczas moĝe pomóc przeszukanie podejrzanych katalogów przy uĝyciu polece- nia zbliĝonego do zapisanego niĝej: strings *.exe *.dll | grep Tajemniczy komunikat Funkcja strings wczytuje plik i drukuje wszystkie ïañcuchy skïadajÈce siÚ przynajmniej z MINLEN = 6 drukowalnych znaków. 141 5.6. NARZ}DZIA DIAGNOSTYCZNE /* strings: pobiera znaki drukowalne ze strumienia */ void strings(char *name, FILE *fin) { int c, i; char buf[BUFSIZ]; do { /* Jeden raz dla kaĪdego áaĔcucha */ for (i = 0; (c = getc(fin)) != EOF; ) { if (!isprint(c)) break; buf[i++] = c; if (i = BUFSIZ) break; } if (i = MINLEN) /* Drukuje, jeĞli áaĔcuch jest wystarczająco dáugi */ printf( s: .*s , name, i, buf); } while (c != EOF); } ’añcuch formatu .*s uĝyty w wywoïaniu funkcji printf pobiera dïugoĂÊ ïañcucha z na- stÚpnego argumentu (i), poniewaĝ ïañcuch (buf) nie jest zakoñczony zerem. PÚtla do-while znajduje i drukuje kaĝdy ïañcuch, a dziaïanie koñczy, gdy napotka znak koñca pliku. DziÚki temu, ĝe na koñcu funkcji znajduje siÚ sprawdzenie koñca pliku, funkcja getc oraz pÚtle ïañcuchowe mogÈ mieÊ wspólny warunek zakoñczenia i jedno wywoïanie funkcji printf moĝe obsïugiwaÊ koniec ïañcucha, koniec pliku oraz zbyt dïugie ïañcuchy. W standardowej pÚtli zewnÚtrznej ze sprawdzeniem warunku na poczÈtku lub pojedynczej pÚtli z funkcjÈ getc i bardziej skomplikowanym kodem ěródïowym konieczne by byïo dwu- krotne wywoïanie funkcji printf. Takie rozwiÈzanie zastosowaliĂmy na poczÈtku, ale zrobiliĂmy bïÈd w instrukcji wywoïujÈcej funkcjÚ printf. PoprawiliĂmy go w jednym miejscu, lecz zapo- mnieliĂmy o jeszcze dwóch innych („Czy popeïniïem ten sam bïÈd jeszcze gdzieĂ indziej?”). Wówczas staïo siÚ jasne, ĝe program trzeba napisaÊ ponownie, aby byïo w nim mniej powtó- rzeñ kodu. Tak doszliĂmy do pÚtli do-while. Funkcja main programu strings wywoïuje funkcjÚ strings dla kaĝdego pliku przekazanego jej jako argument: /* main: znajduje znaki drukowalne w plikach */ int main(int argc, char *argv[]) { int i; FILE *fin; setprogname( strings ); if (argc == 1) eprintf( Sposób uĝycia: nazwy plików ); else { for (i = 1; i argc; i++) { if ((fin = fopen(argv[i], rb )) == NULL) weprintf( Nie moĝna otworzyÊ pliku s: , argv[i]); else { strings(argv[i], fin); fclose(fin); } } 142 } return 0; } 5. USUWANIE B’}DÓW Moĝe siÚ dziwisz, ĝe funkcja strings nie pobiera danych ze swojego standardowego stru- mienia wejĂciowego, gdy nie zostanÈ podane ĝadne pliki. PoczÈtkowo to robiïa. Aby wyjaĂniÊ, dlaczego teraz tego nie robi, musimy opowiedzieÊ historiÚ pewnego bïÚdu. Oczywistym testem, za pomocÈ którego moĝna sprawdziÊ program strings, jest urucho- mienie go na nim samym. Program dziaïaï prawidïowo w systemie Unix, ale w systemie Win- dows 95 polecenie C: strings strings.exe zwróciïo dokïadnie piÚÊ wierszy danych: !This program cannot be run in DOS mode .rdata @.data .idata .reloc Pierwszy wiersz wyglÈda jak komunikat o bïÚdzie, przez co zmarnowaliĂmy trochÚ czasu na dowiedzenie siÚ, ĝe jest to ïañcuch zapisany w programie, a dane wyjĂciowe sÈ poprawne, przy- najmniej jak na razie. Czasami zdarza siÚ, iĝ sesja diagnostyczna zostaje przerwana z powodu niezrozumienia ěródïa pochodzenia komunikatu. Ale danych wyjĂciowych powinno byÊ wiÚcej, wiÚc gdzie siÚ podziaïy? Wreszcie którejĂ no- cy oĂwieciïo mnie („GdzieĂ juĝ to widziaïem!”). Jest to problem z przenoĂnoĂciÈ, o którym sze- rzej piszemy w rozdziale 8. Pierwsza wersja programu wczytywaïa dane tylko ze standardowego wejĂcia i uĝywaïa do tego celu funkcji getchar. Ale w systemie Windows funkcja ta zwraca znak koñca pliku, jeĂli w danych tekstowych napotka konkretny bajt (0x1A, czyli znak Ctrl+Z). To powodowaïo przedwczesne koñczenie pracy programu. Jest to caïkowicie poprawne zachowanie, ale nie tego oczekiwaliĂmy, biorÈc pod uwagÚ na- sze doĂwiadczenia z uĝywania programu w systemie Unix. RozwiÈzaniem jest otwarcie pliku w trybie binarnym przy uĝyciu trybu „rb”. Ale strumieñ stdin jest juĝ otwarty i nie da siÚ zmieniÊ jego trybu w ĝaden standardowy sposób (moĝna by byïo uĝyÊ funkcji takich jak fdopen i setmode, ale nie naleĝÈ one do standardu jÚzyka C).W efekcie stajemy przed wyborem jednej z kilku nieprzyjemnych moĝliwoĂci: zmusiÊ uĝytkownika do podania nazwy pliku, dziÚki czemu program bÚdzie dobrze dziaïaï w systemie Windows, choÊ jest to nietypowe rozwiÈzanie dla systemu Unix; po cichu tworzyÊ niepoprawne odpowiedzi, gdy uĝytkownik systemu Windows usiïuje wczytaÊ dane ze standardowego wejĂcia; albo zastosowaÊ kompilacjÚ warunkowÈ, by dostosowaÊ zachowanie programu do róĝnych systemów, co zmniejsza jego przenoĂnoĂÊ. Zde- cydowaliĂmy siÚ na pierwszÈ z wymienionych moĝliwoĂci, poniewaĝ dziÚki temu program wszÚdzie bÚdzie dziaïaï tak samo. mwiczenie 5.2. Program strings drukuje ïañcuchy zawierajÈce przynajmniej MINLEN znaków, co czasami powoduje zwrócenie wiÚkszej iloĂci danych, niĝ potrzeba. Zmodyfikuj program strings tak, aby przyjmowaï opcjonalny argument sïuĝÈcy do okreĂlania minimalnej dïugoĂci ïañcucha. 5.7. B’}DY POPE’NIONE PRZEZ INNYCH 143 mwiczenie 5.3. Napisz funkcjÚ vis kopiujÈcÈ dane wejĂciowe na wyjĂcie i zamieniajÈcÈ bajty niedrukowalne, takie jak znak Backspace, znaki sterujÈce i znaki nienaleĝÈce do zestawu ASCII na symbole w formacie Xhh, przy czym hh oznacza szesnastkowÈ reprezentacjÚ danego znaku. W przeciwieñstwie do strings funkcja vis jest najbardziej przydatna przy analizowaniu da- nych zawierajÈcych niewielkÈ liczbÚ znaków niedrukowalnych. mwiczenie 5.4. Jaki wynik zwróci funkcja vis, jeĂli na wejĂciu otrzyma ïañcuch X0A? Co moĝna zrobiÊ, aby funkcja vis zwracaïa niedwuznaczne wyniki? mwiczenie 5.5. Rozszerz zakres dziaïania funkcji, tak aby przetwarzaïa sekwencje plików, ïamaïa dïugie wiersze w dowolnym miejscu i usuwaïa wszystkie niedrukowalne znaki. Jakie jeszcze inne zadania zgodne z przeznaczeniem programu mogïaby speïniaÊ ta funkcja? 5.7. BïÚdy popeïnione przez innych Niewielu programistów ma przyjemnoĂÊ tworzyÊ nowy system od podstaw. Znacznie czÚĂciej uĝywajÈ, modyfikujÈ, a wiÚc i poprawiajÈ, kod napisany przez innych programistów. Wszystko, co napisaliĂmy do tej pory na temat znajdowania i eliminowania bïÚdów, ma za- stosowanie takĝe do bïÚdów popeïnionych przez kogoĂ innego. Przed przystÈpieniem do pracy konieczne jest jednak zbadanie organizacji programu oraz zrozumienie sposobu myĂlenia i pracy poprzednika. W pewnym bardzo duĝym projekcie programistycznym uĝyto okreĂlenia „odkry- cie”, stanowiÈcego caïkiem dobrÈ przenoĂniÚ. Zadanie polega na odkryciu, o co chodzi w ko- dzie, którego my nie napisaliĂmy. W takich przypadkach bardzo pomocne sÈ róĝne narzÚdzia. UĝywajÈc programów do prze- szukiwania tekstu, takich jak grep, moĝna znaleěÊ wszystkie wystÈpienia wybranej nazwy. Ge- neratory odsyïaczy:g (ang. cross-referencer) pozwalajÈ zapoznaÊ siÚ ze strukturÈ programu. Wy- kres przedstawiajÈcy wywoïania funkcji jest pomocny, jeĂli nie jest zbyt duĝy. Wykonywanie kodu po jednej instrukcji za pomocÈ programu diagnostycznego pozwala odkryÊ kolejnoĂÊ zda- rzeñ. ZaglÈdajÈc do historii wersji programu, moĝna dowiedzieÊ siÚ, jak program rozwijaï siÚ w czasie. CzÚste zmiany oznaczajÈ, ĝe kod jest sïabo zrozumiany albo podlega zmieniajÈcym siÚ wymaganiom, a wiÚc moĝe stanowiÊ potencjalne ěródïo bïÚdów.
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

Lekcja programowania. Najlepsze praktyki
Autor:
,

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: