Programowanie w języku C++ Część siódma Autor Roman Simiński Kontakt roman.siminski@us.edu.pl www.us.edu.pl/~siminski Niniejsze opracowanie zawiera skrót treści wykładu, lektura tych materiałów nie zastąpi uważnego w nim uczestnictwa. Opracowanie to jest chronione prawem autorskim. Wykorzystywanie jakiegokolwiek fragmentu w celach innych niż nauka własna jest nielegalne. Dystrybuowanie tego opracowania lub jakiejkolwiek jego części oraz wykorzystywanie zarobkowe bez zgody autora jest zabronione.
Pojęcie sterty heap Sterta Sterta (ang.heap) to wydzielony obszar pamięci: przeznaczony do przechowywania danych dynamicznych, kontrolowany ręcznie przez programistę, ograniczony pod względem rozmiaru. Rozmiar sterty zmienia się wraz ze zmianą środowiska systemowego i kompilatora. Zwykle jest on jednak wystarczający dla typowych programów. Jednak przetwarzanie grafiki czy danych multimedialnych może wymagać ustawień specyficznych. Typowy scenariusz wykorzystania sterty: przydział niezbędnej w danym momencie ilości pamięci najpóźniej jak to możliwe, wykorzystanie przydzielonego obszaru, zwolnienie przydzielonej pamięci natychmiast, gdy nie jest już ona potrzebna. Copyright Roman Simiński Strona : 2
Dynamiczny przydział pamięci w językach C i C++ Zarządzanie pamięcią przydzielaną dynamicznie w języku C W języku C dynamiczny przydział pamięci realizowały funkcje: void * malloc( size_t size ), void * calloc( size_t nitems, size_t size ), void * realloc( void * ptr, size_t size ). Obszary pamięci przydzielone tymi funkcjami należało zwolnić funkcją: void free( void * ptr ). Zarządzanie pamięcią przydzielaną dynamicznie w języku C++ W języku C++ dynamiczny przydział pamięci realizować będzie: operator new, a zwalnianie przydzielonej pamięci: operator delete. Copyright Roman Simiński Strona : 3
Dynamiczny przydział pamięci w językach C i C++ Zarządzanie pamięcią przydzielaną dynamicznie w języku C Funkcje przydzielające pamięć nie są częścią języka, pochodzą z biblioteki zarządzania pamięcią. Funkcje przedzielające pamięć traktowane są jako amorficzne bloki, o liczonym w bajtach rozmiarze zdefiniowanym przez programistę. Typ przdzielanych obszarów nie jest funkcją znany, programista musi panować nad rozmiarami przydzielonych bloków. Zarządzanie pamięcią przydzielaną dynamicznie w języku C++ Zarządzanie pamięcią zostało włączone języka, operatory new i delete są znane kompilatorowi, Stosowanie operatorów new i delete zapewnia aktywowanie konstruktorów dla inicjalizacji tworzonych obieków oraz destruktorów dla czynności kończących przy usuwaniu obiektu z pamięci. Copyright Roman Simiński Strona : 4
Przydział pamięci dla obiektu klasy Square Użycie operatora new powoduje przydzielenie pamięci dla obiektu w obszarze zwanym stertą, oraz inicjalizację tego obiektu z wykorzystaniem odpowiedniego konstruktora. Square * p; p = new Square; cout << p->getside(); delete p; Utworzenie obiektu i inicjalizacja wywołanie konstruktora bezparametrowego Usunięcie obiektu i czynności kończące wywołanie destruktora Można sterować rodzajem inicjalizacji Square * a = new Square; Square * b = new Square( 10 ); Square * c = new Square( *b ); // Aktywowanie konstruktora domyślnego // Aktywowanie konstruktora ogólnego // Aktywowanie konstruktora kopiującego Usuwanie obiektu operatorem delete delete a; delete b; delete c; Operator delete wywołuje destruktor i zwalnia przydzieloną pamięć. Copyright Roman Simiński Strona : 5
Uwagi na temat zwalniania pamięci Operator delete może być stosowany wyłącznie dla obiektów utworzonych operatorem new. Mieszanie operatorów new i delete z funkcjami typu malloc i free daje niezdefiniowane rezultaty. Jeżeli operator delete zostanie użyty dla wskaźnika zerowego, nic się nie stanie a operacja ta nie jest błędna. Dwukrotne (lub wielokrotne) usunięcie tego samego obiektu jest błędem i prowadzi do problemów. Uwaga odwoływanie się do obszaru pamięci, który został wcześniej zwolniony jest błędem zarówno w C jak i w C++. Square * p = new Square; delete p; // To nie jest rozs dne: ą p->setside( 100 ); cout << p->getside(); Copyright Roman Simiński Strona : 6
Uwagi na temat zwalniania pamięci Dobrą praktyką jest zerowanie wskaźnika tuż po zwolnieniu pamięci, Pozwala to na kontrolowanie czy można odwołać się do obiektu wskazywanego w innym miejscu programu. Square * p = new Square; delete p; p = 0; To jest rozsądne: // Wersja 1-sza: if( p ) p->setside( 100 ); // Wersja 2-ga: if( p!= 0 ) p->setside( 100 ); // Wersja 3-cia: if( p!= NULL ) p->setside( 100 ); Uwaga w jęzuku C++ można zakładać, że wskaźnik pusty (pokazujący na nic) ma wartość zero. Korzystanie ze stałej NULL wymaga włączenia przynajmniej pliku nagłówkowego stddef.h. Copyright Roman Simiński Strona : 7
Przedział pamięci dla tablic obiektów Operator new może być wykorzystany do utworzenia tablicy obiektów. Stosowanie tego operatora zapewnia, że dla każdego elementu tablicy zostanie aktywowany konstruktor domyślny. Square * stab = new Square[ 10 ]; Tak utworzona tablica może być wykorzystana w normalny dla tablic sposób: for( int i = 0; i < 10; i++ ) cout << stab[ i ].getside(); Taką tablicę należy jednak usunąć w specyficzny sposób: delete [] stab; Zapewni to uaktywnienie destruktora o ile istnieje dla każdego elementu tablicy. Copyright Roman Simiński Strona : 8
Dynamiczne tablice mogą mieć rozmiar określony zmienną int numofsquares; cout << "Podaj liczbe kwadratow :"; cin >> numofsquares; Square * stab = new Square[ numofsquares ]; for( int i = 0; i < numofsquares; i++ ) cout << stab[ i ].getside() << endl; delete [] stab; Operatory new i delete mogą być stosowane dla typów wbudowanych int n; cout << "Podaj liczbe elementow:"; cin >> n; int * itab = new int[ n ]; for( int i = 0; i < n; i++ ) cout << itab[ i ] << endl; delete [] itab; Copyright Roman Simiński Strona : 9
Należy uważać na dynamicznie przydzielane obszary pamięci int n; cout << "Podaj liczbe elementow:"; cin >> n; int * itab = new int[ n ]; itab = new int[ 10 ]; // Poprzednio przydzielony obszar został utracony for( int i = 0; i < n; i++ ) cout << itab[ i ] << endl; delete [] itab; Można temu częściowo zaradzić int * const itab = new int[ n ]; Tak zdefiniowany wskaźnik nie może być później modyfikowany. Copyright Roman Simiński Strona : 10
Przydział pamięci dla tablic wielowymiarowych Przy alokacji tablic wielowymiarowych wszystkie wymiary poza pierwszym muszą być nieujemnymi wyrażeniami o wartości znanej na etapie kompilacji. Wymiar pierwszy może być zadany zmienną. const int MAX_LINE_LEN = 256; int numoflines; char ( * lines )[ MAX_LINE_LEN ]; cout << "Podaj liczbe linii: "; cin >> numoflines; lines = new char[ numoflines ][ MAX_LINE_LEN ]; for( int i = 0; i < numoflines; i++ ) sprintf( lines[ i ], "Linia nr: %-2d", i + 1 ); cout << endl << lines[ i ]; delete [] lines; Copyright Roman Simiński Strona : 11
Gdy pamięci jest za mało... Jeżeli operator new nie potrafi znaleźć ciągłego, wolnego obszaru pamięci dla obiektu: sprawdza czy programista zdefiniował specjalną funkcję obsługi takiej sytuacji, jeżeli taka funkcja istnieje, jest ona wywoływana, w przeciwnym wypadku generowany jest wyjątek, dawniej rezultatem operatora była wartość zero. Programista definiuje własną funkcję obsługi braku pamięci wykorzystując funkcję set_new_handler() zdefiniowaną w pliku nagłówkowym new.h. Ma to być bezparametrowa funkcja o rezultacie void, np: void out_of_memory() cerr << "Brak pamięci"; exit( EXIT_FAILURE ); Copyright Roman Simiński Strona : 12
Czy to działa sprawdź kiedy skończy się pamięć #include <iostream> #include <cstdlib> #include <new> using namespace std; void out_of_memory() cerr << "Brak pamięci"; exit( EXIT_FAILURE ); int main() set_new_handler( out_of_memory ); for(;;) new int[ 100000 ]; return EXIT_SUCCESS; Copyright Roman Simiński Strona : 13