Laboratorium zerowe: O bª dach w trakcie pisania programów i metodach radzenia sobie z nimi. March 4, 2015 Abstract Poni»szy tekst zawiera podstawy korzystania z debuggera GDB oraz zawiera opis najcz stszych problemów i bª dów pojawiaj cych si na laboratoriach oraz o sposobach radzenia sobie z nimi. W pierwszej kolejno±ci nale»y pobra szablon C++ do zadania i zapozna si z jego zawarto±ci. Po rozpakowaniu w katalogu powinny si znale¹ nast puj ce pliki: main.cpp - gªówny plik, zawieraj cy przykªady opisane poni»ej. Ten plik b dziemy modykowa. Makele - skrypt który b dziemy wykorzystywa do kompilacji programu. plot.plt - skrypt gnuplota, za pomoc którego b dziemy generowa wykresy. utils/imnmath.hpp plik z funkcjami pomocniczymi przydatnymi podczas pisania programów na laboratorium. Otwieramy konsol i przechodzimy do katalogu w którym znajduje si gªówny katalog szablonu. Poni»ej znajduje si opis problemów, które mo»na napotka podczas pisania programów oraz sposobie radzenia sobie z nimi za pomoc debuggera GDB. 1 Porównanie dokªadno±ci oat i double. Kompilujemy program za pomoc skryptu makele za komend make r e l e a s e nast pnie uruchamiamy program:. / lab Ju» na wst pie naszym oczom powinien sie ukaza bª d segmentation fault. Bez w tpienia problem znajduje si w funkcji problem1(), gdy» jej zakomentowanie nie powoduje generowania si bª du. Najprostszym sposobem szukania bª dów jest metoda: komentowania poszczególnych fragmentów kodu wraz z wykorzystaniem instrukcji printf czy cout. My skorzystamy jednak z debuggera gdb do znalezienia bª du. W pierwszej kolejno±ci nale»y uruchomi program gdb. W tym celu uruchamiamy program za pomoc komendy gdb Nast pnie podajemy nazw pliku który chcemy debugowa : 1
f i l e lab Uruchamiamy program: GDB powinien zako«czy program ale oprócz bª du: segmentation fault nie podaje»adnych pomocnych informacji. Wszystko dlatego,»e nasz program zostaª skompilowany bez opcji debugowania. Skompilujmy nasz program w trybie debugowania. make c l e a n // ( program zapyta o zakonczenie poprzedniego zadania wciskamy "y ") Tym razem program pokazuje, gdzie jest nasz problem: prosz przeczyta dokªadnie komunikat zwrócony przez gdb, gdy» jeszcze do ko«ca nie wiadomo na czym ten problem polega. Widzimy - z komunikatu,»e problemem staje si zapis do tablicy mdata. Za pomoc komendy print mo»emy wypisa warto±ci zmiennych w obecnym konteksie w ktorym gdb wykryª problem. p r i n t i // lub (p i ) wypisze obecna wartosc zmiennej i Zauwa»my,»e wynosi ona i=200, tym czasem rozmiar tablicy wynosi 200 przy czym numeracja C++/C zaczyna sie od zera, zatem ostatni mo»liwy wskaznik to 199. Zatem nasz program wychodzi poza obszar zaalokowanej tablicy. GDB pozwala równie» na wypisywanie zawarto±ci tablicy: p r i n t mdata [ 1 9 9 ] @3 // wypisze o s t a t n i e t r z y elementy macierzy mdata z w iersza 199. Wi cej informacji o wykorzystaniu GDB w kontekscie tablic wielowymiarowych znajd Pa«stwo na stronie: https: //sourceware.org/gdb/onlinedocs/gdb/arrays.html. Prosz spróbowa wywoªa komend : p r i n t mdata [ 2 0 0 ] @3 GDB powinien zwróci komunikat o wyj±ciu poza dozwolony obszar pami ci. Widzimy zatem,»e nasza p tla wychodzi poza obszar zaalokowanej pami ci. Przyczyn tego musi by zatem linijka f o r ( i n t i = 0 ; i <= 200 ; i++) zmie«my j na: f o r ( i n t i = 0 ; i < 200 ; i++) skompilujmy program make c l e a n Nast pnie komenda make spowoduje wygenerowanie wykresu problem1.png. Warto zwróci uwag na ró»nic w wynikach otrzymywanych przez zmienne double i oat. 2
2 Porównywanie liczb zmienno przecinkowych W tym przypadku b dziemy bada liczb iteracji potrzebnych do uzbie»nienia równania rekurencyjnego a n+1 := an, gdzie n oznacza indeks iteracji. Granica takiego szeregu jest znana i wynosi 1 dla a 0 > 0 przy n +, przy czym a 0 oznacza warto± pocz tkow. W problemach numerycznych niesko«czone sumy obcina si do okre±lonej warto±ci n np. n max = 1000, ze wzgl du na sko«czon precyzj numeryczn. Interesuje nas dla jakich warto±ci n dwa ci gi a n (z a 0 = 20) i b n (z b 0 = 1 20 ) zbiegn si do 1. Wiemy,»e w granicy gdy n = +, musz by sobie równe, jednak ze wzgl du na sko«czon precyzj liczb double spodziewamy si osi gn znak równo±ci, po sko«czonej liczbie iteracji do tego celu sprawdzamy ich równo± za pomoc wyra»enia if( a == b). Prosz zakomentowa funkcje problem1 i odkomentowac funkcje problem2. Skompilowa i uruchomic program: Prosz zauwa»y,»e program przy kompilacji zwróci warning dotycz cy porównywania zmiennych double. Nasz program uruchamia si ale ju» na starcie wypisuje do konsoli,»e a = 20 a b = 0. W celu wykrycia bª du wykorzystamy mo»liwo±ci debugera gdb. Skorzystamy z funkcji breakpoint, dzi ki której mo»emy wskaza debugerowi w którym miejscu dziaªania programu program ma si zatrzyma, a nast pnie mo»emy skorzysta z funkcji print, aby wypisa warto±ci zmiennych w tym miejscu programu. Dodawanie breakpointu odbywa si za pomoc komendy break (lub b) w konsoli GDB (wi cej informacji np http://www.unknownroad.com/rtfm/gdbtut/gdbbreak.html). Dodajmy breakpoint, który zatrzyma program w pliku main.cpp w linijce zaraz przed p tl for (w szablonie powinna to by linijka 42). Komenda GDB jest nast puj ca: break main. cpp : 4 2 Informacje o obecnie u»ywanych breakpointach wypisuje funkcja i n f o break Uruchamiamy program Program powinien si zatrzyma na utworzonym breakpointcie. Wypiszmy teraz warto± zmiennych a i b. p r i n t a p r i n t b Widzimy zatem,»e zmienna b ju» o pocz tku wynosi zero. Jest to standardowy problem, gdzie przy dzieleniu zmiennych typu integer 1/20 wynik jest obcinany do zmiennych typu int. Zatem w przestrzeni liczb caªkowitych 1/20 = 0. Aby ten problem naprawi zmie«my linijk double b = 1/20; na np. double b = 1. 0 / 2 0 ; // albo double b = 1. 0 / 2 0. 0 albo double b = 1 / 2 0. 0. Skompilujmy program i uruchommy go od pocz tku: Program ponownie zatrzyma si na ustawionym breakpointcie. Mo»emy ponownie wypisa warto± b: 3
p r i n t b Powinna by ona rózna od zera. Aby przej± dalej wpisujemy: continue // ( lub c ) Kiedy dany breakpoint nie jest ju» nam potrzebny mo»emy do wyª czy d i s a b l e albo usun d e l e t e numer_breakpointu numer_breakpointu Numer breakpointu zwróci nam funkcja i n f o breakpoint Zauwa»my teraz,»e mimo i» a i b po wypisaniu pokazuje 1 waek if (a == b) nie jest speªniony. Dzieje si tak dlatego,»e w rzeczywisto±ci zmienne a i b ró»ni si o bardzo maª warto±. Aby przekona si jak bardzo maªa jest ta ró»nica wykorzystajmy breakpoint z instrukcj wakow. Poni»sza instrukcja zatrzyma program w linijce 50 (je±li zmienna i b dzie wynosi 50). Linijka 50 mo»e si ró»ni w Pa«stwa przypadku wa»ne jest, aby znajdowaªa si ona wewn trz p tli for (mo»e by np. na wysoko±ci instrukcji if( a == b ) break;) break main. cpp : 5 0 i f i == 50 Uruchamiamy program: Je»eli breakpoint zostaª ustawiony poprawnie to program powinien zatrzyma si w odpowiednim miejscu. Wypiszmy teraz warto± a i b p r i n t a p r i n t b Lub lepiej: p r i n t a b Widzimy zatem,»e instrukcja wakowa if nie ma mo»liwo±ci zadziaªa, mimo,»e zmienne a i b bez w tpienia mo»na uzna za prawie równe 1. Standardowym sposobem na rozwi znie tego problemu jest obliczanie ró»nicy pomi dzy zmiennymi a b i nast pnie sprawdzenie czy ró»nica ta jest mniejsza od jakiej± zadanej wielko±ci eps. Dobrym zwyczajem jest ograniczanie wszystkich niesko«czonych p tli do sko«czonej z góry zadanej maksymalnej liczby iteracji. Prosz zobaczy jak problem2 zostaª rozwi zany w funkcji problem2_ok. 3 Inne problemy pojawiaj ce si na laboratoriach Nie ma nic gorszego ni» stracony czas na szukanie gªupich bª dów... Poni»sze uwagi maj na zadanie skróci ten czas. 4
3.1 Zerowanie tablic Samodzielna alokacja tablicy za pomoc operatora new, czy malloc nie gwarantuje,»e dane w utworzonych tablicach b d wyzerowane. Dlatego dobrym nawykiem jest zerowanie tablic zaraz po ich utworzeniu. Do tego celu mo»na wykorzysta samodzielnie napisane p tle po elementach tablicy, funkcje memset, czy w przypadku operatora new mo»na skorzysta z konstrukcji double data = new double [ 1 0 0 ] ( ) ; Dodanie () spowoduje,»e utworzona tablica b dzie wyzerowana. Wi cej sposobów na rozwi zanie tego problemu mo»na znale¹ np: http://stackoverflow.com/questions/2204176/how-to-initialise-memory-with-new-operator-in-c 3.2 Warto± bezwzgl dna - problem fabs() i abs(). Bardzo cz sto w pisanych na tych laboratoriach programach zajdzie potrzeba policzenia warto±ci bezwzgl dnej z jakiej± wielko±ci. Do tego celu b dziemy wykorzystywa funkcje abs lub fabs w zale»no±ci od j zyka. W standardzie C funkcja abs jako argument przyjmuje liczby caªkowite wobec tego abs(0.01) zostanie potraktowane jako abs(0), gdy» 0.01 zostanie obci te do warto±ci caªkowitej 0. Zatem waek abs(0.01) < 0.0001 zostanie speªniony, co jest nie prawd. W programach pisanych w j zyku C trzeba b dzie korzysta z dedykowanej liczbom zmiennoprzecinkowym funkcji fabs. Zatem poprawne b dzie napisanie fabs(0.01) < 0.0001. W j zyku C++ problem ten nie wyst puje i funkcja abs zadziaªa poprawnie. Dlatego te» osoby przyzwyczajone do C++ i pisz ce programy w C bardzo cz sto popeªniaj ten bª d. Bezpieczniej jest jednak pisa std::abs. 3.3 Zbyt rzadkie sprawdzanie kodu. Dobrym zwyczajem jest cz ste sprawdzanie poprawno±ci dziaªania kodu. Programy s krótkie a sama kompilacja trwa uªamek sekundy, zatem warto po ka»dej dokonanej modykacji kodu sprawdzi program, czy wykonuje si poprawnie nawet je»eli nie speªnia jeszcze wymaganej funkcjonalno±ci. Pozwoli to na szybkie wykrycie takich problemów jak brak inicjalizacji zmiennych, naruszenie pami ci itp. 3.4 Wypisywanie kontrolnych warto±ci. Kolejnym dobrym zwyczajem jest wypisywanie dla kontroli warto±ci ró»nych zmiennych u»ywanych przez program, w kolejnych etapach dziaªania programu. Wypisywanie to mo»e odbywa si np. do pliku w przypadku tablic, czy do konsoli. Takie problemy jak warto±ci NaN, NULL czy podobne mo»na bardzo szybko w ten sposób zlokalizowa. 3.5 Moduªowo± programów. W trakcie pisania programów bardzo cz sto zajdzie potrzeba rozwi zywania tego samo problemu ale z wykorzystaniem innej procedury numerycznej. Pisz c kilka razy ten sam kod, bardzo proste zadanie mo»e przerodzi si w kod o dªugo±ci si gaj cy nawet tysi ca linii kodu. Szukanie bªedu w takim kodzie staje si o wiele bardziej trudniejsze. Okazuje si,»e korzystaj c z takich udogodnie«jak funkcje pomocnicze, wska¹niki na funkcje, czy szablony mo»na ten sam kod skróci w elegancki sposób nawet do 100-200 linijek. Dla przykªadu: mamy do rozwi zania równanie ró»niczkowe dy dx = ecos(10x) sin(3x) π ( x 2 1 ), dobrym zwyczajem jest zastosowanie funkcji pomocniczej 5
double f ( double &x ) ( return exp ( cos (10 x ) ) s i n (3 x) M_PI ( x x 1)); zamiast pisania explicite prawej strony powy»szego równania ró»niczkowego w ka»dym miejscu programu. Je»eli tre± zadania ulegnie zmianie albo pomylimy si w denicji funkcji f (np. podczas przepisywania z tre±ci zadania) wtedy wystarczy poprawi tylko jedn linijk w funkcji f, w przeciwnym wypadku tych zmian mo»e by bardzo du»o, co mo»e prowadzi do pomyªek i straconego czasu. 3.6 Efekt Copy-Paste'a Bardzo cz sto zdarza si skorzystanie z analogicznego fragmentu kodu w ró»nych miejscach programu. Zwyczajem jest kopiowanie tego kodu (w celu zaoszcz dzenia czasu), a nast pnie jego modykacja tak, aby dziaªaª pod konkretne zadanie. W przypadku kopiowania du»ych fragmentów kodu, problemem jest nie poprawienie wszystkich zmian jakie trzeba dokona, aby kod dziaªaª poprawnie. Niestety tego typu bª dy s zwykle nie wykrywalne przez kompilator, czy debugger, a szukanie ich jest bardzo uci»liwe. Poni»ej znajduje si przykªad ilustruj cy problem: zaªó»my,»e w pierwszej cz ±ci zadania mamy do wykonania p tle: a = 0 ; f o r ( i n t i = 0 ; i < 100 ; i++) a = func_a ( a, i ) ; a w drugiej cz ±ci zadanie jest analogiczne, ale zamiast func_a mamy u»y inn funkcj np. func_b. Standardowo kopiujemy kod i zmieniamy wsz dzie z a na b, ale przypadkiem zapominamy zmieni jednej rzeczy: b = 0 ; f o r ( i n t i = 0 ; i < 100 ; i++) a = func_a (b, i ) ; W takim przypadku, program zadziaªa poprawnie, a warto± a i b na ko«cu programu b d zªe. Powy»szy problem jest bardzo oczywisty, ale nale»y pami ta,»e w rzeczywistych programach pisanych na laboratoriach zajdzie potrzeba skopiowania nawet kilkudziesi ciu linijek kodu, a miejsc do popeªnienia bª du b dzie znacznie wi cej. Cz ±ciowym sposobem na rozwi zanie tego problemu jest kopiowanie kodu porcjami, w takich ilo±ciach, aby w ka»dej porcji liczba zmian do wprowadzenia nie byªa zbyt du»a. 6