Programowanie obiektowe C++ Programowanie zorientowane obiektowo Wykład 0 Witold Dyrka witold.dyrka@pwr.wroc.pl 26/09/2011
Prawa autorskie itp. Wiele slajdów do tego wykładu powstało w oparciu o: slajdy Bjarne Stroustrupa do kursu Foundations of Engineering II (C++) prowadzonego w Texas A&M University http://www.stroustrup.com/programming
Materiały Literatura Bjarne Stroustrup. Programowanie: Teoria i praktyka z wykorzystaniem C++. Helion (2010) Jerzy Grębosz. Symfonia C++ standard. Edition 2000 (2008) Dowolny podręcznik programowania zorientowanego obiektowo w języku C++ w standardzie ISO 98 www.cplusplus.com - The C++ Reference Network (j.ang.) Środowisko programistyczne Microsoft Visual C++ (rekomendowane) Dowolne środowisko korzystające z GCC
Plan na dziś Bezpieczeństwo typów Kontener vector Obsługa błędów
Bezpieczeństwo typów Zasada bezpieczeństwa typów: Każdy obiekt będzie użyty tylko zgodnie z jego typem Zmienna będzie użyta dopiero po inicjacji Tylko operacje zdefiniowane dla zadeklarowanego typu zmiennej będą użyte Każda operacja zdefiniowana dla zmiennej nadaje jej prawidłową wartość Ideał statycznej kontroli typów Program naruszający bezpieczeństwo typów nie zostanie skompilowany Kompilator zgłasza każde naruszenie Ideał dynamicznej kontroli typów Jeśli program narusza bezpieczeństwo typów, zostanie to wykryte w czasie uruchomienia Istnieje kod, który wykrywa każde naruszenie nie znalezione przez kompilator
Bezpieczeństwo typów w C++ C++ nie zapewnia (pełnej) statycznej kontroli typów Żaden powszechnie używany język nie jest w pełni bezpieczny Pełna statyczna kontrola typów mogłaby za bardzo ograniczać programistę C++ nie zapewnia (pełnej) dynamicznej kontroli typów Istnieją inne języki, które ją zapewniają Pełna dynamiczna kontrola typów mogłaby za bardzo ograniczać programistę Pełna dynamiczna kontrola typów często generuje większy i wolniejszy kod
Naruszenie bezpieczeństwa typów niejawne zawężanie (ang. implicit narrowing) // Uwaga: C++ nie zabrania próby umieszczenia dużej wartości // w małej zmiennej (chociaż kompilator może ostrzec przed tym). int main() { int a = 20000; a: char c = a; int b = c; if (a!= b) //!= oznacza relację nierówności cout << "ups!: " << a << "!=" << b << '\n'; else cout << "Świetnie! Mamy duże znaki\n"; } 20000 c: Spróbuj, jaka będzie wartość b na Twoim komputerze???
Naruszenie bezpieczeństwa typów (niezainicjowana zmienna) // Uwaga: C++ pozwala użyć niezainicjowanej zmiennej. // Jednak kompilator przeważnie ostrzega przed tym. int main() { int x; char c; double d; // x otrzymuje losową wartość początkową // c otrzymuje losową wartość początkową // d otrzymuje losową wartość początkową // nie każda zawartość komórki pamięci stanowi poprawną // wartość zmiennoprzecinkową double dd = d; // potencjalny błąd: niektóre implementacje // nie kopiują niepoprawnych wartości zmiennoprzecinkowych cout << " x: " << x << " c: " << c << " d: " << d << '\n'; } Zawsze inicjuj swoje zmienne. Wyjątek: zmienne, których wartości są wprowadzane z zewnątrz. Uwaga: tryb debugowania może inicjować zmienne
Plan na dziś Bezpieczeństwo typów Kontener vector Obsługa błędów
Vector kontener danych W zasadzie wszystkie interesujące obliczenia dotyczą zbiorów danych. Do zapisu kolekcji danych może posłużyć klasa kontenerowa vector // wczytywanie pomiarów temperatury int main() { vector<double> temps; // deklaracja zmiennej temps, która jest wektorem typu double // przechowującej listę temperatur (takich jak 32.4) double temp; // zmienna przechowująca pojedyńczy pomiar while (cin>>temp) // cin wczytuje wartość i zapisuje ją w temp temps.push_back(temp); // zapisanie temperatury w wektorze // inne operacje } // wyrażenie cin>>temp będzie spełnione (wartość true) dopóki na wejściu cin // nie pojawi się coś czego nie można uznać za double: np. wyraz koniec
Wektor Vector jest typem danych zdefiniowanym w bibliotece standardowej C++ vector<t> przechowuje sekwencję wartości typu T Można to wyobrazić sobie tak: size() Niech wektor o nazwie v posiada 5 elementów: {1,4,2,3,5}: v: 5 elementy v: v[0] v[1] v[2] 1 4 2 v[3] 3 v[4] 5
Uzupełnianie wektora danymi vector<int> v; // tworzy pusty wektor v: 0 v.push_back(1); // dodaje element o wartości 1 1 1 v.push_back(4); // dodaje element o wartości 4 // na końcu (the back) 2 1 4 v.push_back(3); // dodaje element o wartości 3 // na końcu 3 1 4 3 v[0] v[1] v[2] v.push_back(3); zmienna (obiekt) metoda (funkcja)
Używanie wektorów // liczenie średniej i mediany temperatur: int main() { vector<double> temps; double temp; // temperatury w stopniach Celsjusza while (cin>>temp) temps.push_back(temp); // wczytuje wektor temperatur double sum = 0; for (int i = 0; i< temps.size(); ++i) sum += temps[i]; // suma temperatur cout << "Średnia temperatury: " << sum/temps.size() << endl; sort(temps.begin(),temps.end()); } cout << "Mediana temperatury: " << temps[temps.size()/2] << endl;
Ideał Powinniśmy móc w dowolny sposób wykorzystać podstawowe elementy języka i podstawowe biblioteki Sprawdźmy czy jest tak z wczytywaniem i sortowaniem wektorów?
Przykład lista słów /* Lista slow kod samego sedna programu: wczytuje listę łańcuchów do wektora łańcuchów, sortuje w kolejności alfabetycznej i drukuje posortowany wektor. */ vector<string> words; string s; while (cin>>s && s!= "quit") words.push_back(s); sort(words.begin(), words.end()); // && oznacza AND (iloczyn logiczny) // wczytywanie // sortowanie for (int i=0; i<words.size(); ++i) cout<<words[i]<< "\n"; // drukowanie
Przykład usuwanie powtórzeń // Program usuwa powtórzone wyrazy vector<string> words; string s; while (cin>>s && s!= "quit") words.push_back(s); sort(words.begin(), words.end()); for (int i=1; i<words.size(); ++i) if(words[i-1]==words[i]) pozbądź się i-tego wyrazu (words[i]) // (pseudokod) for (int i=0; i<words.size(); ++i) cout<<words[i]<< "\n"; // Usunąć powtórzone wyrazy można na wiele sposobów, niektóre całkiem zagmatwane. // Celem dobrego programisty jest wybrać proste i jasne rozwiązanie // biorąc pod uwagę takie ograniczenia jak czas kodowania, czas działania, pamięć.
Usuwanie wyrazów rozwiązanie // Usuwa powtórzone wyrazy poprzez kopiowanie tylko wyrazów unikalnych vector<string> words; string s; while (cin>>s && s!= "quit") words.push_back(s); sort(words.begin(), words.end()); vector<string>w2; // wektor przechowujący wyrazy unikalne if (0<words.size()) { // rób tylko jeśli wektor words nie pusty w2.push_back(words[0]); // wstaw pierwszy element do w2 for (int i=1; i<words.size(); ++i) if(words[i-1]!=words[i]) // jeśli dwa kolejne wyrazy w words różne w2.push_back(words[i]); // dodaje nowy wyraz do w2 } cout<< "znaleziono " << words.size()-w2.size() << " powtórzeń\n"; for (int i=0; i<w2.size(); ++i) cout << w2[i] << "\n";
Plan na dziś Bezpieczeństwo typów Kontener vector Obsługa błędów
Błędy w programie Podstawowym celem programisty jest poprawność kodu uświadomiłem sobie, że od teraz dużą część mojego życia spędzę szukając i poprawiając moje własne błędy. Maurice Wilkes Przypuszczam, że unikanie, znajdowanie i poprawianie błędów stanowi 95% lub więcej wysiłku w poważnym tworzeniu oprogramowania. Bjarne Stroustrup
Błędy w programie Źródła błędów Rodzaje błędów Sprawdzanie argumentów funkcji Raportowanie błędów Wykrywanie błędów Wyjątki Debugowanie (odrobaczanie) Testowanie
Źródła błędów Słaba specyfikacja (80% dużych projektów wysypuje się przez złą specyfikację) Niekompletny program Nieoczekiwane argumenty funkcji (np. funkcja sqrt() nie przyjmie wartości -1) Niespodziewane dane wejściowe (np. uszkodzony plik z danymi) Niespodziewany stan Błędy logiczne
Rodzaje błędów Błędy kompilacji błędy typów błędy składni Błędy konsolidacji Błędy czasu wykonania (ang. run-time) wykryte przez system ( program się wysypał ) wykryte przez biblioteki (wyjątki) wykryte przez kod użytkownika Błędy logiczne wykryte przez programistę/testera
Oczekiwania Każdy program 1)Powinien wytworzyć pożądany wynik dla każdego poprawnego wejścia 2)Powinien zwrócić sensowny komunikat o błędzie dla niepoprawnych danych wejściowych Twój program 3)Nie musi przejmować się błędami sprzętu 4)Nie musi przejmować się błędami systemu 5)Może zakończyć działanie po znalezieniu błędu
Błędy argumentów funkcji Kompilator sprawdza liczbę i typy argumentów funkcji: int pole(int wysokosc, int szerokosc) { return wysokosc*szerokosc; } int x1 = pole(7); // błąd kompilacji: zła liczba argumentów int x2 = pole("siedem", 2); // błąd kompilacji: argument 1 ma zły typ int x3 = pole(7, 10); // ok int x5 = pole(7.5, 10); // działa, ale: 7.5 zostanie obcięte do 7; // większość kompilatorów zgłosi ostrzeżenie int x = pole(10, -7); // typy się zgadzają, ale wartość -7 nie ma sensu!!!
Obsługa błędów int x = pole(10, -7); // typy się zgadzają, ale wartość -7 nie ma sensu!!! Tym błędem musi zająć się programista Dwie strategie sprawdzania poprawności argumentów sprawdza wywołujący funkcję + wie jak obsłużyć błąd - musi znać specyfikację funkcji, dłuższy i nieczytelny kod sprawdza funkcja wywoływana + prostszy, bardziej elegancki kod - nie wie co zrobić z błędem Pomysł: funkcja zwraca informacje o błędzie, a wywołujący decyduje co z tym zrobić
Raportowanie błedów (1) Zwracanie wartości błędu int pole(int wysokosc, int szerokosc) // zwraca -1 jeśli błędne dane { if(wysokosc <=0 szerokosc <= 0) return -1; return wysokosc*szerokosc; } Wywołujący musi (może?) sprawdzić wynik i podjąć działanie int z = pole(x,y); if (z<0) { cerr<<"problem z obliczaniem pola"; return; // Kolejny problem: jaką wartość błędu może zwracać funkcja max()?
Raportowanie błędów (2) Ustawianie znacznika błędu: int errno = 0; int pole(int wysokosc, int szerokosc) { if(wysokosc <=0 szerokosc <= 0) errno = 7; return wysokosc*szerokosc; } Wywołujący musi (może?) sprawdzić wynik i podjąć działanie int z = pole(x,y); if (errno==7) { cerr<<"problem z obliczaniem pola"; return; // Te same problemy co poprzednio nie używaj!!
Raportowanie błedów (3) Zgłaszanie wyjątków: class Zle_pole { }; // Tak w C++ definiujemy typ użytkownika, tutaj typ błędu int pole(int wysokosc, int szerokosc) { if (wysokosc <=0 szerokosc <= 0) throw Zle_pole(); // wyrzucamy obiekt typu return wysokosc*szerokosc; // Zle_pole } Wywołujący próbuje czy we fragmencie kodu nie zgłoszono wyjątku, chwyta go i decyduje co zrobić try { int z = pole(x,y); // jeżeli pole() nie zgłosiło wyjątku } // wykonuje przypisanie i idzie dalej catch(zle pole) { // jeżeli pole() zgłosiło wyjątek Zle_pole(), obsługuje go: cerr << "Ups! Problem z obliczaniem pola"; }
Wyjątki - idea Program, czyli grupa funkcji przybywa na dzikie i niebezpieczne tereny. Jedna z nich wyrusza na rekonesans www.sidzina.net.pl Sygnalizuje go rzucając wyjątek: Wtem, napotyka problem: msw-pttk.org.pl Reszta chwyta, że żółta flara oznacza misia i podejmuje dalsze działania artofmanliness.com
Dlaczego wyjątki? Obsługa wyjątków jest uniwersalna Nie można o nich zapomnieć: wystarczy, że umieścisz całość programu w bloku try catch, a program zakończy działanie, jeśli wyjątek pojawi się i nie zostanie obsłużony Prawie każdy typ błędu można zgłosić wyjątkiem Jednak to wciąż programista musi zastanowić się co zrobić z każdym wyjątkiem
Wyjątki błąd poza zakresem int main() try { vector<int> v(10); // wektor 10-ciu int-ów for (int i = 0; i<v.size(); ++i) v.at(i) = i; // ustawiamy wartości // at(i) = [i] + wyjątek zakresu for (int i = 0; i<=10; ++i) // drukujemy 10 wartości (???) cout << "v[" << i << "] == " << v.at(i) << endl; } catch (out_of_range&) { // wyjątek out_of_range cerr << "ups któryś indeks wektora poza zakresem\n"; } catch ( ) { cerr << "ups jakiś błąd\n"; } // wszystkie inne wyjątki
Odrobaczanie Na proces debugowania składa się: 1) Zauważenie, że coś działa inaczej niż oczekiwano 2) Sprawdzenie co tak na prawdę się dzieje 3) Poprawienie A jak tego nie robić? dopóki (program nie sprawia wrażenia działającego) { zaglądam tu i tam, szukając czegoś co wygląda dziwnie; zmieniam to, aby wyglądało lepiej ; }
Krok 1: zadbaj o strukturę programu Jeżeli masz znaleźć błąd program musi być łatwy do czytania 1) Komentuj wyjaśniaj swoje pomysły 2) Używaj znaczących nazw 3) Rób wcięcia trzymaj się jednego stylu 4) Podziel kod na małe funkcje - unikaj funkcji dłuższych niż strona kodu 5) Jeśli to możliwe, unikaj skomplikowanego kodu, np. zagnieżdżonych pętli i instrukcji warunkowych 6) Używaj funkcji bibliotecznych są sprawdzone
Krok 2: skompiluj program Jeśli program się nie kompiluje - sprawdź: 1) Czy zakończyłeś literały łańcuchowe ('') i znakowe (')? 2) Czy zakończyłeś każdy blok ({ }) 3) Czy zamknąłeś wszystkie nawiasy? 4) Czy każda nazwy została zadeklarowana? 5) Czy każda nazwa została zadeklarowana przed użyciem? 6) Czy zakończyłeś każdą instrukcję wyrażeniową średnikiem?
Krok 3: Prześledź program Podejdź do tego jakbyś był komputerem Czy dane na wyjściu spełniają Twoje oczekiwania? Warto wyrzucić wartości paru zmiennych na wyjście błędu: cerr << "x == " << x << ", y == " << y << '\n'; Uważaj musisz zobaczyć to co programu rzeczywiście robi, nie to co uważasz, że powinien robić: for (int i=0; 0<month.size(); ++i) { // błąd! for( int i = 0; i<=max; ++j) { Gdzie szukać błędu? // podwójny błąd!
Krok 3: Gdzie szukać błędu? Wprowadź testy poczytalności (ang. sanity checks), if (liczba_elementow<0) throw runtime_error("ujemna liczba elementow"); if (maksymalny_sensowny<liczba_elementow) throw runtime_error("nieoczekiwanie wielka liczba elementow"); if (x<y) throw runtime_error("niemozliwosc: x<y"); Napisz je tak, aby nie trzeba było ich usuwać z kodu w momencie, gdy program będzie wyglądał na ukończony
Krok 3: Gdzie szukać błędu? Warunki początkowe i końcowe class Zly_argument { }; class Zle_pole { }; int pole(int wysokosc, int szerokosc) { if (wysokosc <=0 szerokosc <= 0) throw Zly_argument(); int p = wysokosc*szerokosc; if (p<=0) throw Zle_pole(); return p; } Zasady Zawsze przemyśl warunki początkowe i końcowe Zapisz je przynajmniej w postaci komentarza Sprawdzaj, gdy szukasz błędu Kiedy w przykładzie warunek początkowy będzie spełniony, a końcowy nie?
Krok 3: Gdzie szukać błędu? Sprawdź przypadki brzegowe Czy zainicjowałeś każdą zmienną sensowną wartością? Czy funkcja dostała prawidłowe argumenty? Czy funkcja zwróciła prawidłową wartość? Czy poprawnie obsłużyłeś pierwszy i ostatni element? (np. pętli) Czy poprawnie obsłużyłeś przypadki puste? (brak elementów, brak wejścia) Czy poprawnie otworzyłeś plik? Czy rzeczywiście wczytałeś wejście i zapisałeś wyjście?
Kilka ogólnych zasad Jeżeli nie możesz znaleźć błędu, szukasz w złym miejscu Nie dowierzaj swojemu przekonianiu, że wiesz gdzie jest błąd Nie zgaduj, kieruj się danymi na wyjściu: Szukaj od miejsca, gdzie wiesz, że jest ok Co się dzieje dalej, dlaczego? Szukaj przed miejscem, gdzie dostałeś zły wynik Co mogło się wydarzyć? Znalezłeś robaka? Zastanów się, czy naprawienie go rozwiązuje cały problem Często szybka poprawka wprowadza nowe błędy Jeśli programista mówi: znalazłem ostatni błąd, to żartuje;-)
Dziś najważniejsze było to, że... Każdy obiekt używamy tylko zgodnie z jego typem Nie wymyślamy koła! wykorzystujemy bibliotekę standardową, np. typ vector zamiast tablicy Raportujemy błędy przy użyciu mechanizmu wyjątków Jeśli programista mówi: znalazłem ostatni błąd, to żartuje;-)
A za 2 tygodnie......zajmiemy się klasami: Klasy Implementacja i interfejs Konstruktory Funkcje składowe Składowe stałe Składowe statyczne Typ wyliczeniowy