Programowanie w Visual C++ 2010

Spis treści

Błędy w kodzie cz. 3 – problemy z pamięcią

Na wstępie...

W poprzednich samouczkach zetknęliśmy się z różnego rodzaju błędami syntaktycznymi oraz semantycznymi. Istnieje też grupa innych problemów, które możemy napotkać podczas działania programu, np. przy próbie dostępu do niezaalokowanej pamięci. Ujrzymy wtedy komunikat podobny do poniższego: Problemy z pamięcią

Widząc taki komunikat, klikamy Przerwij – powracamy do naszego programu i próbujemy naprawić błąd. Może nam też w tym pomóc debugger, którego znamy z poprzedniego samouczka. Szczegóły poznamy już za chwilę. :-)

Rodzaje błędów

Nieprawidłowe definicje funkcji

Zazwyczaj nie mamy problemów z określaniem typów zmiennych w definicjach funkcji, np. kiedy stosować zmienne typu int, a kiedy double. Kłopoty jednak pojawiają się, gdy musimy zastosować modyfikatory służące do zmiany sposobu przekazywania zmiennych:

  • & (tzw. dziabąg – przekazywanie przez referencję),
  • * (przez wskaźnik),
  • [] (jako tablice).

Przypomnijmy, że referencje w deklaracjach funkcji nakazują kompilatorowi, aby przekazany parametr nie był kopią zmiennej, lecz jej oryginalną wersją.

void fun(int &a)
{
    a=0;
}

//...

int b=5;
fun(b);
cout << b; //wypisane zostanie 0

Będzie nam się to często przydawać, m.in. w funkcji zamieniającej kolejność zmiennych.

Z kolei modyfikatory [] i * służą do przekazywania tablic funkcjom.

void fun(int tab[]) { ... }
//lub
void fun(int *tab) { ... }


//wywołanie obu funkcji jest takie samo:
fun(tab);

Niepoprawna obsługa tablic

Korzystając z tablic, musimy zwracać szczególną uwagę na ich obsługę. Jeżeli mamy do dyspozycji tablicę n-elementową, oznacza to, że pierwszym indeksem tej tablicy jest 0, a ostatnim n-1. Uważajmy na to np. w pętli for. Jak wiemy, język C++ nie ma kontroli zakresów tablic. Dopiero podczas działania błędnego programu, mogą wystąpić błędy, np. taki, jak na załączonym obrazku:

Błędy w rekurencji

Wykonaj poniższe zadanie:

Przez 3 sekundy patrz w sufit.

Wykonaj zadanie ponownie.

Po jakim czasie zrezygnowałeś/aś? W momencie, kiedy rozbolała Ciebie szyja czy uznałeś/aś, że to nie ma sensu? Ewidentnie zabrakło instrukcji nakazującej zaprzestania wykonywania tej czynności.

Programiści czasami mają problemy z prawidłowym sformułowaniem tzw. warunku stopu w rekurencji (bądź w ogóle o nim zapominają!). W takiej sytuacji taka funkcja wykonuje się bez końca. Komputer się nie znudzi. ;-)

Na szczęście, niekiedy takie problemy z rekurencją są zgłaszane już na etapie kompilacji – wtedy w panelu Output pojawi się odpowiednie ostrzeżenie (warning) . Jeżeli zignorujemy je i uruchomimy program, po jakimś czasie pojawi się następujący komunikat: Zakończenie działania aplikacji Kliknijmy wtedy Zamknij program – to znak, że należy zlokalizować problem (samodzielnie bądź przy pomocy debuggera).

Debugowanie

Nowe funkcje debuggera

Zanim przejdziesz do dalszego materiału, koniecznie przypomnij sobie informacje o debugowaniu, które poznałeś/aś w samouczku o błędach semantycznych.

Bez znajomości odpowiednich pojęć, chociażby takich jak breakpoint, przerobienie tej części samouczka będzie stanowić dla Ciebie problem.

Jak pamiętasz, programy możemy debugować na dwa sposoby:

  • przechodząc od breakpointa do breakpointa;
  • obserwując działanie programu krok po kroku.

W tym drugim przypadku, korzystaliśmy z trybu Step Over (klawisz F10) – żółta strzałka na marginesie okienka z kodem programu przechodziła wtedy przez odpowiednie instrukcje w funkcji main().

Jednak czasami mamy potrzebę sprawdzenia, w jaki sposób wykonują się polecenia w funkcji. Tryb Step Over uniemożliwia tego typu poczynania. Dlatego twórcy Visual Studio przygotowali również dodatkowy tryb, zwany Step Into (skrót klawiszowy F11). Tryb Step Into

W powyższym panelu, skrót do trybu Step Into znajduje się na lewo od Step Over.

Z trybem Step Into należy obchodzić się bardzo ostrożnie! Czasem możemy dotrzeć w naprawdę dziwne miejsca, np. do kodu bibliotek systemowych.

Dlatego nabierzmy wprawy w umiejętności korzystania z obu wspomnianych trybów. Polecam metodę "prób i błędów" – biblioteki powinny być zabezpieczone przed wprowadzaniem w nich zmian, więc prawdopodobnie nie powinniśmy niczego zepsuć. Aczkolwiek warto być ostrożnym.

Przed nami dwa przykłady, na których przećwiczymy debugowanie. Zatem przyda się nowy projekt – nazwijmy go bledy3 i dodajmy do niego nowy plik źródłowy.

Rekurencja

Funkcje rekurencyjne potrafią sprawiać nie lada kłopot początkującym programistom. Jeżeli mamy problem z "wyobrażeniem" sobie, w jaki sposób one działają, to w takim przypadku przyda się debugger.

Zanim przystąpimy do pracy z Visual Studio, przypomnijmy definicję silni.

Definicja silni: \[ n!=\begin{cases}1 &\mbox{ dla } n=0\\ n*(n-1)! &\mbox{ dla } n>0\end{cases} \]

Przykład: \[ 3!=3*2!=3*2*1!=3*2*1*0!=3*2*1*1=6 \]

Przystąpmy do obserwacji zachowania się rekurencyjnej funkcji obliczającej silnię. Do projektu skopiuj poniższy kod:

#include <iostream>
using namespace std;

int silnia(int x)
{
	if(x==0) return 1;
	else return x*silnia(x-1);
}

int main()
{
	int n=5;
    int s;
	s=silnia(n);
	cout << s << endl;
	return 0;
}

Tym razem nie skorzystamy z pomocy ani jednego breakpointa – chcemy dokładnie prześledzić zachowanie funkcji rekurencyjnej. Ustawmy kursor w linii #14 i z menu podręcznego wybierzmy Run To Cursor (lub użyjmy kombinacji klawiszy CTRL+F10). Zwróćmy uwagę na zawartość panelu Locals.

Pora użyć trybu Step Into – kliknijmy odpowiedni skrót na pasku narzędziowym lub wciśnijmy F11. Żółta strzałeczka powinna przeskoczyć do linii #5, a w panelu Locals pojawi się informacja o zmiennej x.

Klikajmy Step Into aż do momentu, gdy w panelu Locals zmienna x przyjmie wartość 0. Gdy klikniemy na Step Into, będąc w linii #6, zauważymy, że strzałka przeskoczyła z linii #6 do wiersza #8. Dobrze, że tak się stało. Oznacza to, iż zakończyliśmy następne wywołania funkcji silnia() i teraz będziemy się "cofać" do poprzednich wywołań. Ponownie klikając Step Into powrócimy do linii #7, a w panelu Locals ujrzymy, że wartość x wynosi 1. Dzieje się tak, ponieważ powróciliśmy do tego wywołania funkcji, kiedy to x==1.

Klikajmy Step Into "do oporu" – czyli do momentu, kiedy powrócimy do funkcji main(). Gdy strzałka przejdzie do linii #15, w panelu Locals ujrzymy zmianę wartości zmiennej s.

Uwaga – nie wykonuj Step Into na cout w linii #15!

Kiedy ta silnia jest w ogóle obliczana? Zamieńmy funkcję silnia() na poniższą (równoważną):

int silnia(int x)
{
	if(x==0) return 1;
	else
	{	x=x*silnia(x-1);
		return x;
	}
}

Tym razem, przechodząc krok po kroku przez funkcję, powinniśmy zauważyć odpowiednie zmiany wartości zmiennej x przy kolejnych powrotach z funkcji rekurencyjnej.

Tablice

W tym przykładzie skorzystamy z jednego breakpointa – skopiuj poniższy kod i ustaw przerywnik w linii #11 (dlaczego tu, a nie w wierszu #10?).

#include <iostream>
using namespace std;

int main()
{
	int tab[5];

	for(int i=0; i<5; ++i)
	{
		tab[i]=i;
	}

	cout << endl;
	return 0;
}

Po uruchomieniu programu za pomocą zielonej strzałki (tryb Debug, uruchamiany również za pomocą klawisza F5), przyjrzyjmy się dokładnie zawartości panelu Locals: Panel Locals cz.1

Być może zaciekawi Ciebie to, dlaczego przy zmiennej tab widnieje taka dziwna wartość. Otóż jest to adres komórki pamięci, w której znajduje się pierwszy element tablicy. Adres (ul. Lawendowa 17) możesz sprawdzić samodzielnie, korzystając z jednej z dwóch poniższych linijek:

cout << tab;
//lub
cout << &tab[0];
Sprawdź adresy wszystkich komórek tablicy.
Adresy komórek pamięci są przedstawione w systemie szesnastkowym. Można to poznać nie tylko po znakach, które zostały wykorzystane (\( \{0, \dots , 9, a, \dots , f\} \)), ale przede wszystkim po przedrostku 0x.

Powróćmy do analizowanego przykładu. Kliknijmy na znak plus, który widnieje przy identyfikatorze tab. Zauważymy, że pierwszemu elementowi tablicy została nadana wartość (wykonał się jeden przebieg pętli), a w pozostałych komórkach znajdują się śmieci. Panel Locals cz.2

Wykonując kolejne kroki debugowania, zauważymy stopniowe wypełnianie tablicy. Zakończmy zatem działanie programu, gdy ujrzymy zmiany we wszystkich komórkach tablicy: Panel Locals cz.3

Widzimy więc, że w ten sposób można sprawdzić, czy wszelkie operacje na tablicy są wykonywane prawidłowo.

W naszym projekcie zamieńmy pętlę for na poniższą:

	for(int i=1; i<=5; ++i)
	{
		tab[i]=i;
	}
Samodzielnie prześledź wypełnianie tablicy w panelu Locals.

Tym razem, jeżeli zbyt dużo razy wciśniemy zieloną strzałkę, powinien pojawić się komunikat podobny do poniższego: Problemy z tablicą

W takiej sytuacji klikamy Break, kończymy debugowanie i staramy się usunąć problem – taki komunikat informuje o problemie z indeksowaniem tablicy.

Podsumowanie

W niniejszym samouczku przyjrzeliśmy się kolejnym przykładom błędów semantycznych i sposobom radzenia sobie z nimi. Wiemy także, w jaki sposób możemy śledzić działanie funkcji rekurencyjnych oraz wykonywanie kolejnych operacji na tablicach.

CC By 3.0
Copyright © 2011-2016 by Katarzyna Fokow [Last update: 2017-02-01 17:17:09]
This work is licensed under a Creative Commons Attribution 3.0 Unported License