• Nie Znaleziono Wyników

Dynamiczne zarządzanie pamięcią

W dokumencie Język C++ – podstawy programowania (Stron 27-33)

kw = kw ∗ kw ;

Należy pamiętać, że gdy funkcja będzie wywoływana w kilku miejscach, jej kod będzie kopiowany w tych miejscach. Może to spowodować znaczny wzrost objętości pliku wykonywalnego, co w efekcie może spowolnić wyko-nywanie programu a nie zwiększenie szybkości jak można by się spodziewać.

Należy zdawać sobie sprawę, że słowo kluczowe inline jest w rzeczywistości życzeniem postawionym kompilatorowi a nie jego dyrektywą. Oznacza to, że funkcja będzie rozwinięta, jeśli będzie to możliwe, gdy funkcji nie można rozwinąć tworzona jest zwyczajna wywoływana funkcja.

1.11. Dynamiczne zarządzanie pamięcią

Do przechowywania danych program potrzebuje odpowiedniej ilości pa-mięci. Przydzielanie pamięci może odbywać się automatycznie, gdy np. wy-stąpi deklaracja:

char nazw [ ] =" Jan ␣ Kowalski ";

Taka deklaracja powoduje zarezerwowanie obszaru pamięci potrzebnego do przechowywania tablicy znaków ”Jan Kowalski”. Można również zarezerwo-wać określoną ilość pamięci, tak jak w tej deklaracji:

i n t l i c z b y [ 1 0 0 ] ;

Tego typu deklaracja powoduje zarezerwowanie dla tablicy liczby 100 jed-nostek pamięci, z których każda jest w stanie przechowywać wartość typu int. W języku C można rezerwować pamięć w trakcie działania programu.

Służy do tego celu funkcja malloc(), która pobiera tylko jeden argument – ilość potrzebnej pamięci w bajtach. Znajduje ona odpowiedni obszar wol-nej pamięci i zwraca adres jego pierwszego bajtu. Rozpatrzmy następujące instrukcje wykorzystujące malloc() do utworzenia tablicy:

double ∗ wsk ;

wsk = ( double ∗ ) m a l l o c ( 30 ∗ s i z e o f(double ) ) ;

Powyższy kod rezerwuje pamięć dla trzydziestu wartości typu double. Jak pamiętamy, nazwa tablicy jest adresem jej pierwszego elementu, stąd przy-pisanie wskaźnikowi wsk adresu pierwszej wartości double sprawia, że można z niego korzystać tak jak ze zwykłej nazwy tablicy wsk[]. Schemat postępo-wania jest następujący:

18 1. Specyficzne elementy języka C++

— deklarujemy wskaźnik

— wywołujemy funkcję malloc()

— odwołujemy się do elementów tablicy za pomocą nazwy wskaźnika Ta metoda pozwala na tworzenie tablic dynamicznych, czyli takich, któ-rych rozmiar jest określany w trakcie działania programu. Taką możliwość ilustruje program pokazany na wydruku 1.10.

Listing 1.10. Dynamicznie alokowana pamięć

#include <s t d i o . h>

Przebieg działania programu może mieć postać:

Podaj i l o s c elementow :

1.11. Dynamiczne zarządzanie pamięcią 19 Wpisaliśmy 5 liczb, ale program zaakceptował tylko 3 liczby. Spowodowane jest to faktem, że rozmiar tablicy został ustalony jako 3. Pobranie rozmiaru tablicy realizują instrukcje:

p u t s (" Podaj ␣ i l o s c ␣ elementow : ␣ ") ; s c a n f ("%d", &max) ;

W instrukcji:

wsk = (double ∗ ) m a l l o c (max ∗ s i z e o f(double) ) ;

rezerwujemy miejsce w pamięci dla żądanej ilości elementów i przypisujemy adres bloku pamięci wskaźnikowi wsk. Gdy nie można przydzielić pamięci, funkcja malloc() zwraca wskaźnik zerowy i program kończy działanie:

i f ( wsk ==NULL) {

p u t s (" Blad ␣ p r z y d z i a l u ␣ pamieci , ␣ k o n i e c ") ; e x i t (EXIT_FAILURE) ;

}

Funkcja free() zwalnia zarezerwowaną pamięć. Funkcje malloc() i free() za-rządzają razem pamięcią komputera. Dzięki tablicom dynamicznym wyko-rzystujemy optymalnie pamięć. Jeżeli wiemy, że program większość czasu będzie potrzebował tylko 100 elementów a od czasu do czasu albo tylko jeden raz będzie potrzebował 10000 elementów to należy stosować tablice dynamiczne. Bez możliwości korzystania z tablic dynamicznych należało-by utworzyć tablicę o rozmiarze 10000 elementów. Taka tablica cały czas zajmowałaby niepotrzebnie pamięć. Jest to zwykłe marnowanie pamięci.

Ponieważ dynamiczne przydzielanie pamięci jest istotne dla tworzenia wydajnych programów C++ oferuje dodatkowe, bardzo wygodne narzędzia jakim są operatory new (przydział) i delete (zwalnianie). Składnia wyrażeń z tymi operatorami ma postać:

new typ_obiektu

delete wskaznik_obiektu

Użycie operatora new ma dwie zalety w stosunku do funkcji malloc():

— nie wymaga użycia operatora sizeof

— operator new zwraca wskaźnik żądanego typu, nie ma potrzeby wykony-wania konwersji typu wskaźnika

Instrukcja:

new unsigned short i n t

20 1. Specyficzne elementy języka C++

alokuje na stercie dwa bajty( ta ilość zależy od konkretnego kompilato-ra), a zwracaną wartością jest adres pamięci. Musi on zostać przypisany do wskaźnika. Aby stworzyć na stercie obiekt typu unsigned short, możemy użyć instrukcji:

unsigned short i n t ∗ wsk ; wsk = new unsigned short i n t;

Można też zainicjalizować wskaźnik w trakcie tworzenia:

unsigned short i n t ∗ wsk = new unsigned short i n t;

Tak określony wskaźnik zachowuje się jak każdy inny wskaźnik, wobec tego możemy np. przypisać obiektowi na stercie dowolną wartość:

∗ wsk = 9 9 ;

Ta instrukcja oznacza: „Umieść 99 jako wartość obiektu wskazywanego przez wsk” lub „Przypisz obszarowi sterty wskazywanemu przez wskaźnik wsk wartość 99”. Gdy operator new nie jest w stanie przydzielić pamięci, zgłasza wyjątek. Gdy przydzielona pamięć nie jest już potrzebna, należy ją zwolnić.

Do tego celu należy użyć słowa kluczowego delete (ang. usuń) z właściwym wskaźnikiem. Instrukcja delete zwalnia pamięć zaalokowaną na stercie, ta pamięć może być wykorzystana ponownie do innych zadań. Należy pamię-tać, że pamięć zaalokowana operatorem new nie jest zwalniana automatycz-nie, staje się niedostępna, taką sytuację nazywamy wyciekiem pamięci (ang.

memory leak). Pamięć możemy zwrócić w następujący sposób:

delete wsk ;

Najczęściej operatora new używa się do obsługi tablic. Dziesięcioelementowa tablica liczb całkowitych może być utworzona i przypisana w następujący sposób:

i n t ∗ wsk = new i n t[ 10 ] ;

Zwolnienie tak przydzielonej pamięci można zrealizować za pomocą instruk-cji:

delete [ ] wsk ;

Jak widać obsługa dynamicznego przydziału pamięci dzięki operatorom jest bardzo wygodna. Należy wiedzieć, że operator delete zwalnia pamięć, na którą on wskazuje, wskaźnik nadal pozostaje wskaźnikiem. Ponowne wywo-łanie delete dla tego wskaźnika spowoduje załamanie programu. Zaleca się,

1.11. Dynamiczne zarządzanie pamięcią 21 aby podczas zwalniania wskaźnika ustawić go na zero (NULL). Kompila-tor gwarantuje, że wywołanie delete z pustym wskaźnikiem jest bezpieczne.

Program pokazany na wydruku 1.11 ilustruje zastosowanie operatorów new i delete. Pamięć jest dwukrotnie przydzielona i dwukrotnie zwalniana.

Listing 1.11. Dynamiczny przydział pamięci. Operatory new i delete

1#include <i o s t r e a m . h>

#include <c o n i o . h>

3

i n t main ( )

5 {

i n t zmienna = 5 5 ;

7 i n t ∗zm = &zmien n a ; i n t ∗ s t e r t a = new i n t;

9 ∗ s t e r t a = 1 5 5 ;

c o u t << " zmienna : ␣ " << zmienna << ’ \n ’;

11 c o u t << " ∗zm : ␣" << ∗zm << ’ \n ’;

c o u t << " ∗ s t e r t a : ␣" << ∗ s t e r t a << ’ \n ’;

13 delete s t e r t a ; s t e r t a = new i n t;

15 ∗ s t e r t a = 1 1 1 ;

c o u t << " ∗ s t e r t a : ␣" << ∗ s t e r t a << ’ \n ’;

17 delete s t e r t a ; g e t c h ( ) ;

19 return 0 ; }

Po uruchomieniu programu mamy następujący komunikat:

zmienna : 55

∗zm : 55

∗ s t e r t a : 155

∗ s t e r t a : 111

W instrukcjach

i n t zmienna = 5 5 ; i n t ∗zm = &zmien n a ;

program deklaruje i inicjalizuje lokalną zmienną wartością 55 oraz deklaruje i inicjalizuje wskaźnik, przypisując mu adres tej zmiennej. W instrukcji:

i n t ∗ s t e r t a = new i n t;

deklarowany jest wskaźnik sterta inicjalizowany wartością uzyskaną w wyni-ku wywołania operatora new. Powoduje to zaalokowanie na stercie miejsca dla wartości typu int. W instrukcji:

22 1. Specyficzne elementy języka C++

∗ s t e r t a = 1 5 5 ;

przypisano wartość 155 do nowo zaalokowanej pamięci.

Instrukcje:

c o u t << " zmienna : ␣ " << zmienna << ’ \n ’; c o u t << " ∗zm : ␣" << ∗zm << ’ \n ’;

c o u t << " ∗ s t e r t a : ␣" << ∗ s t e r t a << ’ \n ’;

wypisują odpowiednie wartości. Wydruki wskazują, że rzeczywiście mamy dostęp do zaalokowanej pamięci. W kolejnej instrukcji:

delete s t e r t a ;

pamięć zaalokowana jest zwracana na stertę. Czynność ta zwalnia pamięć i odłącza od niej wskaźnik. Teraz sterta może wskazywać na inny obszar pamięci. Nowy adres i wartość przypisana jest w kolejnych instrukcjach:

s t e r t a = new i n t;

∗ s t e r t a = 1 1 1 ;

a w końcu wypisujemy wynik i zwalniamy pamięć:

c o u t << " ∗ s t e r t a : ␣" << ∗ s t e r t a << ’ \n ’; delete s t e r t a ;

Wspominaliśmy już o zjawisku wycieku pamięci. Taka sytuacja może na-stąpić, gdy ponownie przypiszemy wskaźnikowi adres bez wcześniejszego zwolnienia pamięci, na którą on wskazuje. Rozważmy fragment programu:

i n t ∗ wsk = new i n t;

∗ wsk = 1 1 1 ;

wsk = new i n t; // e r r o r wsk = 9 9 9 ;

Na początku tworzymy wskaźnik wsk i przypisujemy mu adres rezerwowa-nego na stercie obszaru pamięci. W tym obszarze umieszczamy wartość 111.

W trzeciej instrukcji przypisujemy wskaźnikowi wsk adres innego obszaru pamięci a czwarta instrukcja umieszcza w tym obszarze wartość 999. Nie ma sposobu na odzyskanie pierwszego obszaru pamięci. Poprawna postać tego kodu może być następująca:

i n t ∗ wsk = new i n t;

∗ wsk = 1 1 1 ; delete wsk ; wsk = new i n t; wsk = 9 9 9 ;

1.12. Słowa kluczowe języka C++ 23

W dokumencie Język C++ – podstawy programowania (Stron 27-33)