mgr inż. Wacław Libront * Bobowa 2019

ZSO Bobowa, ul. Długoszowskich 1, 38-350 Bobowa, tel: 0183514009, fax: 0183530221, email: sekretariat@zsobobowa.eu, www: zsobobowa.eu

Lekcja

Błędy. Odpluskwianie.

  1. komunikaty
  2. błędy
  3. odpluskwianie

Jak zapewne zauważyłeś, każdy pisany przez Ciebie program nie jest wolny od błędów. Zapomniany średnik lub nawias, to tzw. błędy składniowe, które nie stanowią większego problemu, bo podczas kompilacji szybko można je poprawić. Takie błędy zdarzają się zazwyczaj początkującym programistom, którym wydaje się, że kompilator wszystko zrozumie.

Ale jeśli program się wykonuje i nie robi tego, czego się spodziewaliśmy, nie pokazuje takich wyników, jakie teoretycznie przewidzieliśmy lub co gorsza - zawiesza się w trakcie działania? - błędy muszą być w samej koncepcji logicznej programu. W dalszej części opis kilku często spotykanych błędów oraz wyjaśnienie działania tzw. odpluskwiacza (debuggera).

Porządne środowisko programistyczne koloruje składnie, automatycznie wstawia wcięcia, wspomaga wpisywanie nazw funkcji i pilnuje zamykania nawiasów. Dzięki takim prostym zabiegom mamy możliwość eliminowania błędów składniowych już na etapie pisania. Weryfikacją poprawności wpisanego kodu jest kompilacja programu - program nie zostanie skompilowany i nie może być uruchomiony, jeśli nie zostaną wyeliminowane wszystkie błędy składniowe.


Komunikaty
Uruchomiliśmy program, ale nie działa tak, jak powinien. Co zrobić? Możemy tak napisać nasz program (jego wstępną wersję), aby w newralgicznych momentach, na ekranie pokazywały się komunikaty, w których wypisywane będą wartości zmiennych, od których zależy poprawne działanie programu. Na ich podstawie możemy zorientować się, co nie jest tak, jak sobie to wyobrażaliśmy.

for (int i=0; i < 100; i=i+1){
    double R=5.0 + i / 100.0;
    //sprawdzamy czy poprawnie obliczane jest R
    cout << i << "  " << R << endl;
    double VK= 4.0/3.0*M_PI*R*R*R;
    //sprawdzamy czy poprawnie obliczane jest VK
    //ale tylko dla konkretnych wartości i
    if (i >= 10 && i <= 20)
      cout << i << "  " << VK << endl;
      
    double RK=5.0/2.0;
    double HK=VK/(M_PI*RK*RK);
    if (HK >= 29.5 && HK <= 30.5)
      cout << R << "  " << HK << endl;
  }

Przykładowe komunikaty zaznaczono kolorem czerwonym. Jeżeli badany fragment programu działa poprawnie, można usunąć komunikat lub (bezpieczniej) ustawić go jako komentarz (być może się jeszcze przyda).


Błędy w programach C++ (i nie tylko)

Deklarowanie zmiennych

cin >> x; 
cout << "Wprowadzono " << x;

Nie zadeklarowano zmiennej przed wykonaniem obliczeń - bardzo często popełniany błąd. Istnieją takie języki programowania, w których nie musimy deklarować zmiennych, ale w C++ jest to niezbędne. Musimy poinformować kompilator o nazwie i typie, co pozwala uniknąć wielu poważnych błędów na dalszym etapie tworzenia programu.

int x; 
cin >> x; 
cout << "Wprowadzono " << x;

Inicjowanie zmiennych

int a, b;
int wynik = a + b;
cout << wynik;

Zmienne a i b są zadeklarowane, ale nie zainicjowane, tzn. nie przypisano im żadnych konkretnych wartości. W następnym wierszu po zainicjowaniu obliczany jest wynik na podstawie zmiennych, w których nie ma żadnych konkretnych wartoci. Dlatego też wyświetlana wartość zmiennej wynik jest „nieokreślona”. Wiele kompilatorów wstawia automatycznie do takich niezainicjowanych zmiennych wartość 0. W kompilatorach języka C wstawiana jest wartość nieokreślona (czyli praktycznie dowolna) i z tego powodu pojawiają się za każdym razem inne wyniki.

int a=3, 
int b=2;
int wynik = a + b;
cout << wynik;

Najpierw inicjuj, potem obliczaj

int a, b;
int wynik = a + b;
cin >> a >> b;

Bardzo podobny błąd, jak opisany w poprzednim przykładzie. Zmienne inicjujemy przez wpisanie wartości z klawiatury. Ale należy to zrobić przed wykonaniem obliczeń na tych zmiennych.

int a, b;
cin >> a >> b;
int wynik = a + b;

Złe operatory logiczne

i=i+1;
if (i = 5)
     cout << "równe 5";

Bardzo często mylimy operację przypisania z operacją porównania. Porównanie to symbol „==”, a przypisanie to „=”.

i=i+1
if (i == 5)
     cout << "równe 5";

Brak nawiasów klamrowych

if (a < 0)
    cout << "ustawiam na 0";
    a = 0;

W instrukcjach warunkowych i w pętlach, jeśli chcemy wykonać kilka instrukcji, należy wstawić instrukcję bloku „{}”. W pokazanym przykładzie zmienna a będzie zawsze zerowana, bo znajduje się poza obszarem działania instrukcji warunkowej - w instrukcji arunkowej wykonywana jest tylko instrukcja cout.

if (a < 0) {
    cout << "ustawiam na 0";
    a = 0;
}

Wcięcia

for (int i=1; i < 10; i++) 
	c1=int(T[i])-48; 
	c2=int(M[i])-48;

for (int i=1; i < 10; i++) c1=int(T[i])-48; c2=int(M[i])-48;

Wcięcia mają znaczenie jedynie dla czytelności kodu. Nie wystarczy wciąć, ani też wpisać kilku instrukcji w jednym wierszu, aby się wykonały.  Konieczna jest instrukcja bloku „{}”.

for (int i=1; i < 10; i++) {
	c1=int(T[i])-48; 
	c2=int(M[i])-48;
}

for (int i=1; i < 10; i++) { c1=int(T[i])-48; c2=int(M[i])-48; }

Dzielenie całkowite - rzeczywiste

double ulamek = 1 / 2;

Mimo tego, że zadeklarowano zmienną ulamek typu rzeczywistego, to tak napisana instrukcja potrafi dzielić tylko całkowicie i zmienna ulamek jest równa ZERO! Aby podzielić rzeczywiście, przynajmniej jedna zmienna powinna być napisana w sposób rzeczywisty. Poprawne rozwiązanie - dziel przez liczbę rzeczywistą, np. /1.0:

double ulamek = 1.0  /  2.0;

Nieskończona pętla

x = 0;
while (x != 0 || x != 10) { 
  cout << x << " "; x++; 
}
  x = 0; while (x != 10) { cout << x << " "; x++; }

Pętla wykonująca się bez końca może powstać przez nieprawidłowe porównanie wartości w instrukcji warunkowej, albo wskutek nieprzemyślanego warunku logicznego. Pętla nigdy się nie zakończy, ponieważ błędnie zastosowano sumę logiczną - zmienna x musiałaby mieć równocześnie dwie wartości. Pętla nie ma prawidłowego warunku zakończenia. 

x = 0; 
while ( x != 10 ) { 
  cout << x << " "; x++; 
}

Nie modyfikuj zmiennej sterującej

int i = 0; 
for (i = 0; i < 10; i++) { 
  i=i-1; 
  cout << i << endl; 
}

int i = 0; 
for (i = 0; i < 10; i++) { 
  for (i = 0; i < 5; i++) { 
    cout << "."; 
  } 
  cout << endl; 
}

Warunek zakończenia pętli jest nieosiągalny - jest to spowodowane modyfikowaniem wartości zmiennej sterujące wewnątrz pętli. Z tego powodu należy bardzo uważnie zmieniać wartości licznika pętli wyżej. Pierwszy przykład jest trywialny, a w drugim przypadku pętla zagnieżdżona ma identyczną zmienną sterującą.

int i = 0; 
for (i = 0; i < 10; i++) { 
  //usuwamy - nie wiadomo co autor miał na myśli
  //i=i-1; 
  cout << i << endl; 
}

int i = 0; 
for (i = 0; i < 10; i++) { 
  for (int j = 0; j < 5; j++) { 
    cout << "."; 
  } 
  cout << endl; 
}

Średnik na końcu wiersza

int x = 0; 
while (x++ < 10) ; 
{
  cout << x;
}

Program wypisuje liczbę tylko raz. Jest to spowodowane faktem, że na końcu wiersza z instrukcją  while został postawiony średnik, więc pętla wykona się jeden raz - sprawdzenie warunku. Instrukcja cout jest poza pętlą.

int x = 0; 
while (x++ < 10) 
{ 
  cout << x; 
}

Dane poza zakresem

int dane[] = {1, 2, 3, 4, 5}; 
for (int i = 1; i <= 5; i++) { 
  cout << dane[i] << endl; 
}

W języku C++ tablica 5-elementowa ma elementy numerowane od zera! Być może inne języki programowania (np. Pascal) przyzwyczaiły nas do numerowania zgodnie z porządkiem naturalnym od jeden. Dlatego pętla wykonując pobranie z komórki tablicy dane[5] pobierze wartości nieokreślone. W zależności od wersji kompilatora - program będzie obliczał nieokreślone wartości lub po prostu się zawiesi.

int dane[] = {1, 2, 3, 4, 5}; 
for (int i = 0; i < 5; i++) {
  cout << dane[i] << endl; 
}

Nieznana funkcja

void funkcja1() {
  funkcja2(); 
} 

void funkcja2() { }

Kompilator analizuje kod programu linijka po linijce. Jeśli wewnątrz funkcji znajduje się odwołanie do funkcji znajdującej się po niej, to kompilator wyrzuci błąd - nie poznał jeszcze takiej funkcji. Najprostszym rozwiązaniem jest zamiana miejscami funkcji.

void funkcja2(); 
void funkcja1() { 
  funkcja2(); 
} 

Zagnieżdżone bloki - nierówna ilość nawiasów otwierających i zamykających

for ( (int A=1; A <= 10; A++){
}  for (int B=1; B <= 10; B++){}
      for (int C=1; C <= 10; C++){
        int V=A*B*C;
        if (V == 300) {
        	cout << A << " " << B << "  " << C << endl; 
        	cout << "  P=" << P << "   V=" << V << endl;
}}}}

Poprawianie tak napisanego programu przypomina szukanie przysłowiowej igły - najlepiej jest poprawnie sformatować kod stosując wcięcia. Edytor pomaga unikać błędów automatycznie zamykając nawiasy. Jeśli pętla ma być zagnieżdżona, to od razu ustawiamy kursor w środku bloku poprzedniej pętli (pomiędzy nawiasami ”{}”), zgodnie z zasadą - nawiasy nie mogą się krzyżować! Nawiasy bloków powinno się ustawiać w pionie, choć niektórzy wolą oszczędzić jeden wiersz i wpisują nawias otwierający na końcu instrukcji.

for (int A=1; A <= 10; A++)
{
  for (int B=1; B <= 10; B++)
  {
    for (int C=1; C <= 10; C++)
    {
      int V=A*B*C;
      if (V == 300) 
      {
       	cout << A << " " << B << "  " << C << endl; 
       	cout << "  P=" << P << "   V=" << V << endl;
      }
    }
  }  
}
for (int A=1; A <= 10; A++) {
    for (int B=1; B <= 10; B++) {
      for (int C=1; C <= 10; C++)  {
        int V=A*B*C;
        if (V == 300) {
          cout << A << " " << B << "  " << C << endl; 
          cout << "  P=" << P << "   V=" << V << endl;
        }
      }
    }  
}

Wielkość liter

int l=10;
  for (int i=1;i <= 1; i++)
    cout << i << endl;

Zmienna „ „l” (małe l) jest równa 10, a pętla ma „zakręcić się” 10 razy - okazuje się, że zrobi to tylko jeden raz! - zamiast zmiennej „l” (małe l) programista wpisał jedynkę. W C++ wielkość liter ma znaczenie! Jeśli nazwy typu „ZMIENNA” i „zmienna” od razu są widoczne, to „i”, „I”, „1”, „l” (małe I, duże I, jedynka, małe L) nie zawsze rozróżnialne.

int L=10;
  for (int i=1; i <= L; i++)
    cout << i << endl;


Dodawanie liczb rzeczywistych – kumulowanie błędów zaokrągleń

double d = 0;
for ( int i = 0; i < 10; i++ )
     d=d + 0.1;
cout << fixed << setprecision(20);
cout << d << endl;

Jak myślicie, co zostanie wyświetlone na ekranie? Teoria matematyczna mówi, że to powinno być dokładnie 1, ale w informatyce nie ma dokładnych liczb rzeczywistych (własności układu dwójkowego). Aby uzyskać wymagane „jeden” należy zaokrąglić wynik obliczeń.

double d = 0;
for ( int i = 0; i < 10; i++ )
     d=d + 0.1;
d = round(d);
cout << fixed << setprecision(20);
cout << d << endl;

Funkcja nie zwraca wartości

int znak( int arg ){
    if( arg > 0 )  return 1;
    if( arg < 0 )  return - 1;
}

Funkcja zwraca wartość 1 lub -1 w zależności od przekazanego jako parametr argumentu. Ale co się stanie, gdy jako argument wstawimy 0 „zero”? Niektóre kompilatory nie wykrywają takich błędów i program gotowy jest zawiesić się w najmniej oczekiwanym momencie. Poprawne rozwiązanie – warunki logiczne konstruuj tak, aby się wzajemnie uzupełniały i obejmowały cały zakres:

int znak( int arg ){
    if( arg >= 0 )  return 1;
    if( arg < 0 )  return - 1;
}

Odpluskwianie - debuggowanie

Debugger Odpluskwiacz
Debugger jest programem (z reguły zintegrowanym ze środowiskiem programistycznym), który pozwala na kontrolę i analizę programu w trakcie jego działania, a w szczególności powinien pozwalać na:

Jak odpluskwiać?
Jest to zależne od środowiska, systemu, procesora, itp. Najważniejszym etapem podczas odpluskwiania jest umieszczenie w konkretnym miejscu programu punktów przerwań, tzw. breakpoints. Jeśli w trakcie wykonywania programy, debugger natknie się na takie przerwanie, to proces zostanie wstrzymany i będziemy mieli możliwość przeglądania zmiennych oraz wykonywania kolejnych instrukcji programu, „krok po kroku”. Oczywiście to tylko elementarne możliwości debuggerów, wystarczające jednak do typowej analizy niewielkich programów.


Odpluskwianie w praktyce (Dev C++)
Uruchomienie odpluskwiacza w różnych środowiskach przebiega w dość podobny sposób. Możemy wstawić do kodu jeden lub kilka punktów przerwań.

F4 – przerwania - ustawienie punktu przerwań w wierszu z kursorem. Można też kliknąć w numer wiersza z lewej strony okna z kodem.

F5 – odpluskwianie - uruchomienie programu w trybie odpluskwiania Program wykonywany jest w identyczny sposób, jak podczas normalnego uruchomienia, do momentu wykonania wiersza, w którym ustawiliśmy przerwanie. Wykonywanie programu jest zatrzymywane i wiersz podświetlany jest na niebieski kolor.

F7 – przeskocz – wykonanie podświetlonej na niebiesko instrukcji i przejście do następnej. Nie wskakujemy do środka funkcji.

F8 – wskocz do – jeśli podświetlony wiersz kodu zawiera wywołanie funkcji, to wskakujemy do niej

Zawartość zmiennych
Zawartość „odpluskwionej” zmiennej zobaczymy po najechaniu na nią wskaźnikiem myszki.

Jeśli chcemy przeglądać na bieżąco zmienne, bez „zabawy” z myszką, możemy ją umieścić w oknie przeglądarki projektu – zakładka Odpluskwiacz. Z menu Odpluskwiacz wybieramy polecenie Dodaj Zmienną, w okienku wprowadzamy nazwę zmiennej, którą zamierzamy obserwować. Po zatwierdzeniu zmienna pojawia się w oknie Odpluskwiacza (z lewej strony).

I to by było na tyle. Żmudne śledzenie zmiennych w kolejnych instrukcjach wykonywanego programu, to podstawowy sposób szukania błędów w działaniu programu. Reszta zależy od intensywnego wysiłku umysłowego programisty, który musi prześledzić program i ustalić „dlaczego robi, to co robi”, a nie „jak ja to sobie zaplanowałem”!