Języki programowania Nowoczesne techniki programowania Wykład 2 Witold Dyrka witold.dyrka@pwr.wroc.pl 19/10/2011
Prawa autorskie Slajdy do wykładu powstały w oparciu o slajdy Bjarne Stroustrupa do kursu Foundations of Engineering II (C++) prowadzonego w Texas A&M University http://www.stroustrup.com/programming oraz C ++ Language Tutorial http://www.cplusplus.com/doc/tutorial/control/
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 języka 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
Program wykładów 1. Pierwszy program. Obiekty, typy i wartości 2. Wykonywanie obliczeń. Błędy 3. Pisanie i wykańczanie programu 4. Techniki pisania funkcji i klas 5. Strumienie wejścia/wyjścia 6. Wskaźniki i tablice 7. Systematyka technik i języków programowania
Plan na dziś (1) Wykonywanie obliczeń Kontener danych Wektor (vector) Błędy
Wykonywanie obliczeń dane (wejściowe) Kawałek kodu, często zawiły, zwykle całkiem spory dane (wyjściowe) dane Dane wejściowe (Input): klawiatura, pliki, inne urządzenia wejścia, inne programy, inne części programu Obliczenia to co program robi z danymi wejściowymi aby otrzymać dane wyjściowe Dane wyjściowe (Output): ekran, pliki, inne urządzenia wyjścia, inne programym, inne części programu
Zasady wykonywania obliczeń Zadaniem programisty jest wyrazić obliczenia: Poprawnie Prosto Efektywnie Jak? Technika dziel i zwyciężaj - podziel duży problem na kilka małych podproblemów Technika abstrakcji - twórz fragmenty programu, aby można było z nich korzystać bez wnikania w szczegóły implementacji Uporządkowanie danych (formaty we/wy, protokoły przetwarzania, struktury danych)
Jak wytłumaczyć nasze sposoby działania komputerowi? Wyrażenia, np. d = a+b*c kombinacja literałów, zmiennych, operatorów itd., która zwraca pewną wartość Instrukcje Selekcja (wybór): jeśli ranny nie oddycha, rozpocznij resuscytację; w przeciwnym wypadku, połóż w pozycji ustalonej Iteracja (pętla) wykonaj sekwencję 30 wdechów i 2 uciśnięć mostka powtarzaj sekwencję dopóki nie powróci naturalny oddech Funkcje Franek, zadzwoń po pogotowie i daj znać co powiedzieli Zosia, zatrzymaj jakieś auto i przyprowadź ludzi tutaj Przepływ sterowania, np. ja Franek ja pogotowie
Elementy języka Sposoby działania (algorytmy) wyrażamy wykorzystując elementy języka programowania, np. + : dodawanie * : mnożenie if (wyrażenie) instrukcja else instrukcja ; selekcja while (wyrażenie) instrukcja ; iteracja f(x); funkcja Łącząc elementy języka tworzymy programy
Wyrażenia (ang. expressions) // oblicz powierzchnię: int dlugosc = 20; // najprostsze wyrażenie: literał // (tutaj użyty do zainicjowania zmiennej) int szerokosc = 40; int pole = dlugosc*szerokosc; // mnożenie int srednia = (dlugosc+szerokosc)/2; // dodawanie i dzielenie /* Uwaga! Obowiązują zwykłe reguły kolejności działań. Jeśli masz wątpliwości użyj nawiasów. Jeśli wyrażenie skomplikowane użyj nawiasów. Nie pisz zbyt skomplikowanych wyrażeń (np. podziel na prostsze). Używaj znaczących nazw zmiennych. */ double srednia2 = (dlugosc+szerokosc)/2; // czy uzyskasz spodziewany wynik? // pamiętaj o bezpieczeństwie typów!
Operatory
Instrukcje Instrukcja to: wyrażenie + średnik deklaracja instrukcja sterująca, która określa przebieg programu Na przykład: a = b; double d2 = 2.5; if (x == 2) y = 4; while (cin >> number) numbers.push_back(number); int average = (length+width)/2; return x;
Selekcja (wybór) jeżeli Przykład: wybór większej z dwóch wartości if (a<b) // Zauważ: tu nie ma średnika max = b; else // Zauważ: tu nie ma średnika max = a; Składnia (ang. syntax) if (warunek) instrukcja-1 // warunek dowolne wyrażenie logiczne // jeśli warunek spełniony, wykonaj instrukcję-1 else instrukcja-2 // jeśli nie, wykonaj instrukcję-2
Selekcja (wybór) jeżeli - bloki instrukcji Przykład: wybór większej z dwóch wartości if (a<b) { // Jeśli więcej niż jedna instrukcja max = b; // - utwórz blok { cout << ''b jest większe!''; else { max = a; cout << ''a jest większe!'';
Selekcja (wybór) jeżeli - może być wielostopniowa Zapis: if (x > 0) cout << "x jest dodatnie"; else if (x < 0) cout << "x jest ujemne"; else cout << "x jest zerem"; x jest dodatnie T x>0 N Oznacza to samo co taki zapis: if (x > 0) cout << "x jest dodatnie"; else { if (x < 0) cout << "x jest ujemne"; else cout << "x jest zerem"; x jest ujemne T x<0 N x jest zerem
Selekcja przełącznik Zastosowanie wybór opcji z zamkniętego zbioru switch (ktory) { case 'k': // wyrażenie stałe, przyrównanie który do 'k' std::cout << "Emerytura w wieku 60 lat"; break; // wychodzi z instrukcji switch, opcjonalne case 'm': // wyrażenie stałe, przyrównanie który do 'm' std::cout << "Emerytura w wieku 65 lat"; break; default: // jeśli żadna z powyższych, opcjonalne std::cout << "Brak emerytury?" break;
Iteracja (pętla dopóki ) // licz i drukuj tabelę kwadratów liczbę od 0 do 99: int main() { int i = 0; while (i<100) { cout << i << '\t' << square(i) << '\n'; ++i ; // inkrementacja i // Ciekawostka: własnie to zadanie rozwiązywał pierwszy praktycznie wykorzystany komputer z zapisywanym do pamięci EDSAC, zbudowany w Cambridge w 1949 r. Twórcą programu (w języku assembler) był David Wheeler
Jak działa pętla dopóki? przed rozpoczęciem iteracji: zmienna iteracyjna (zmienna kontrolna); i inicjacja zmiennej kontrolnej; int i = 0 w pętli: sprawdzenie kryterium zatrzymania; i>=100 coś do zrobienia w każdej iteracji; cout << zwiększenie zmiennej kontrolnej; ++i int i = 0; while (i<100) { cout << i << '\t' << square(i) << '\n'; ++i ;
Iteracja po sekwencji (pętla for ) for (int i = 0; i<100; ++i) { cout << i << '\t' << square(i) << '\n'; Wszystkie informacje sterujące znajdują się w nagłówku pętli: for (inicjacja; warunek; inkrementacja) instrukcja_do_wykonania UWAGA! Jest jeszcze pętla rób... dopóki (do... while)
Instrukcje dodatkowe (break, continue, goto) break natychmiastowe opuszczenie pętli for (int i=10; i>0; i--) { cout << i << ", "; if (i==3) { cout << "odliczanie przerwane!"; break; continue natychmiastowe rozpoczęcie kolejnej iteracji for (int i=10; i>0; i--) { if (i==5) continue; cout << i << ", "; goto bezwarunkowy skok do inne miejsca w programie int n=10; loop: Nigdy nie używaj! cout << n << ", "; n--; if (n>0) goto loop; 10,9,8,7,6,5,4,3,odlicz anie przerwane 10,9,8,7,6,4,3,2,1, 10,9,8,7,6,5,4,3,2,1,
Funkcje Ok, ale czym było square(i)? Wywołaniem funkcji square(i): int square(int x) { return x*x; Funkcję definujemy, gdy chcemy wyodrębnić pewien zestaw obliczeń, ponieważ: jest on logicznie oddzielny można go użyć w wielu miejscach naszego programu Dzieki temu program jest: bardziej przejrzysty łatwiejszy do testowania i pielęgnacji
Funkcje (2) Przykład: int square(int x) { return x*x; Składnia: zwracany_typ nazwa_funkcji ( lista_parametrów ) { treść_funkcji; return wartość; // zwracanego_typu
Przepływ sterowania int main() { int square(int x) int i=0; { while (i<100) { return x * x; square(i); i<100 ++i; i==100
Przykład funkcji Definicja: int max(int a, int b) { // ta funkcja przyjmuje 2 parametry if (a<b) return b; else return a; Przykłady wywołania: int x = max(7, 9); // x przyjmuje wartość 9 int y = max(19, -27); // y przyjmuje wartość 19 int z = max(20, 20); // z przyjmuje wartość 20
Deklaracja a definicja funkcji int main() { int a = 10, b = 5; int c = pole(a,b); return 0; int pole(int wysokosc, int szerokosc) { return wysokosc*szerokosc; Ten kod źródłowy nie zostanie skompilowany... Dlaczego? Kompilator analizuje kod od góry Kiedy dojdzie do linijki: int c = pole(a,b); nie będzie wiedział co to jest pole!
Deklaracja a definicja funkcji Rozwiązanie: prototyp funkcji, czyli jej interfejs, czyli deklaracja, że pole to funkcja, która przyjmuje dwa argumenty całkowitoliczbowe (int) i zwraca wartość całkowitoliczbową To wystarczy do sprawdzenia poprawności kodu funkcji main Przepis na pole, czyli definicja może się znaleźć nawet w innym pliku int pole(int, int); int main() { int a = 10, b = 5; int c = pole(a,b); return 0; int pole(int wysokosc, int szerokosc) { return wysokosc*szerokosc; // Może być w innym pliku niż main
Plan na dziś (2) Wykonywanie obliczeń Kontener danych Wektor (vector) Błędy
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ś (3) Wykonywanie obliczeń Kontener danych Wektor (vector) Błędy
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 W 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ć? Znalazł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.....by nauczyć się pisać instrukcje warunkowe i pętle Funkcję definujemy, gdy chcemy wyodrębnić pewien zestaw obliczeń, który jest logicznie oddzielny można go użyć w wielu miejscach naszego programu Nie wymyślamy koła! wykorzystujemy bibliotekę standardową, np. typ vector zamiast tablicy Raportujemy błędy przy użyciu mechanizmu wyjątków Odrobaczamy program systematycznie Jeśli programista mówi: znalazłem ostatni błąd, to żartuje;-)
A za 2 tygodnie... Projekt i implementacja kompletnego małego programu prostego kalkulatora Wykład będzie wzorem jak zrobić projekt semestralny