POLITECHNIKA WARSZAWSKA Instytut Automatyki i i Robotyki ZASADY PROGRAMOWANIA KOMPUTERÓW Język Język programowania: C/C++ Środowisko programistyczne: C++Builder 6 Wykład 9.. Wskaźniki i i zmienne dynamiczne. 1
Alokacja statyczna Zmienne statyczne globalne - tworzone są w momencie uruchomienia programu na skutek zawartych w nim deklaracji zmiennych globalnych i istnieją aż do zakończenia jego wykonywania (zajmują więc cały czas pamięć). Zmienne statyczne lokalne - tworzone są w momencie wywołania funkcji na skutek zawartych w niej deklaracji zmiennych lokalnych i istnieją aż do wyjścia z funkcji (zajmują więc przez ten czas pamięć). Przydział pamięci na zmienne statyczne jest to tzw. alokacja statyczna, która odbywa się automatycznie (bez udziału programisty) w obszarze pamięci operacyjnej, zwanym stosem (ang. stack) programu. Stos ma stały rozmiar (rzędu 1MB) i łatwo może ulec przepełnieniu (stack overflow), np. w przypadku zbyt dużych tablic lub źle napisanej rekurencji. 2
Alokacja dynamiczna Zmienne dynamiczne - tworzone są podczas wykonywania programu za pomocą odpowiedniej instrukcji (new) i istnieją aż do momentu skasowania, za pomocą przeznaczonej do tego instrukcji (delete). Zajmują więc pamięć tylko do momentu skasowania. Potem tę samą pamięć można w tym samym lub innym programie wykorzystać na inne zmienne dynamiczne (które trzeba od nowa utworzyć). Przydział pamięci na zmienne dynamiczne, czyli tzw. alokacja dynamiczna, odbywa się w trakcie pracy programu z udziałem programisty (na jego żądanie -instrukcją new) i z wykorzystaniem całej dostępnej pamięci komputera (łącznie z RAM), zwanej stertą (ang. heap). W systemie Windows wlicza się w to również wolną przestrzeń na dysku twardym. 3
Alokacja dynamiczna c.d. c.d. Przydzieloną pamięć dynamiczną należy zwolnić odpowiednimi instrukcjami (delete), czyli posprzątać po sobie. Jeśli się tego nie zrobi, pamięć nie będzie (w ogólnym przypadku) zwolniona aż do zamknięcia systemu; kolejne uruchomienia takiego programu powodują wówczas ciągłe zmniejszanie się dostępnej pamięci. Po co stosujemy zmienne dynamiczne? umożliwiają tworzenie struktur dynamicznych, o nie znanej z góry wielkości (listy, drzewa), bez niepotrzebnego rezerwowania na zapas pamięci statycznej; umożliwiają tworzenie bardzo dużych tablic; umożliwiają bardzo sprawne działania na tablicach można je w każdej chwili podczas działania programu usunąć z pamięci i na zwolnionym miejscu utworzyć nową zmienną dynamiczną - w tym samym lub innym programie. Ułatwiają więc równoległą pracę wielu programów w systemie operacyjnym. 4
Wskaźniki i i zmienne wskazywane Do zmiennych statycznych z reguły odwołujemy się poprzez ich nazwy (identyfikatory): a = 1; // podstaw 1 pod zmienną o nazwie a Do zmiennych dynamicznych z reguły odwołujemy się poprzez ich adresy (wskaźniki), używając gwiazdki * stojącej po ich lewej stronie: *w = 1; // podstaw 1 pod zmienną wskazywaną przez w Zatem: w - jest tu adresem, czyli wskaźnikiem i musi być typu wskaźnikowego; jest to zmienna wskaźnikowa, będąca zwykłą zmienną statyczną (ma swoją nazwę, podobnie jak inne zmienne statyczne, i miejsce w pamięci zajęte przez cały czas wykonywania programu). *w - jest zmienną dynamiczną, wskazywaną przez adres w i może być dowolnego typu (typ ten podaje się przy deklaracji wskaźnika). Dlatego zmienne dynamiczne nazywamy inaczej zmiennymi wskazywanymi. 5
Wskaźniki i i zmienne wskazywane c.d. c.d. Nazwy wskaźników mogą być zupełnie dowolne, tak jak dowolne mogą być nazwy wszystkich innych zmiennych: *a = "co to?"; // pod zmienną wskazywaną przez a podstaw napis "co to?" *wsk_i = 20; // pod zmienną wskazywaną przez wsk_i podstaw liczbę 20 *adres = 3.8; // pod zmienną wskazywaną przez adres podstaw wartość 3.8 cout << *adres << *a; // wydrukuj zmienną wskazywaną przez adres // i zmienną wskazywaną przez a Na zmiennych dynamicznych można wykonywać te same operacje, co na zmiennych statycznych, tylko należy się do nich inaczej odwoływać - poprzez wskaźnik, czyli używając operatora * do wyłuskania wartości tych zmiennych 6
Wskaźniki i i zmienne wskazywane -- przykłady Zmienne dynamiczne mogą różnych typów, tak jak wszystkie inne zmienne. Zmienne dynamiczne mogą być dowolnie rozrzucone w pamięci komputera: Wskaźnik przechowuje adres początku obszaru pamięci przydzielonego 7 na zapamiętanie zmiennej dynamicznej.
Deklaracja wskaźnika Żeby skorzystać ze zmiennej dynamicznej, musimy najpierw zadeklarować wskaźnik do tej zmiennej, podając typ zmiennej wskazywanej. Piszemy więc tak: int *wsk_i; co należy czytać: zmienna wsk_i jest wskaźnikiem do zmiennej typu int. Deklaracja wskaźnika typ zmiennej wskazywanej *nazwa wskaźnika; Jeśli jednak wskaźnik wskazuje rekord, to wcześniej trzeba ten typ rekordowy, czyli strukturę, zdefiniować. 8
Przykłady deklarowania i i używania wskaźników struct Sam { string marka; int rocznik; double cena; };... int main( ) {... int *wsk_i; string *a; Sam *ad_auta;... *wsk_i = 20; *a = "co to?"; (*ad_auta). cena = 35000;... return 0; } Zmienna ad_auta jest wskaźnikiem do zmiennej typu auta: *ad_auta. cena = 35000; (*ad_auta). cena = 35000; wersja krótsza w zapisie: ad_auta -> cena = 35000; Tak nie wolno, bo kropka jest silniejsza od gwiazdki równoważne 9
Adres (czyli wskaźnik) zmiennej wskazywanej otrzymujemy w trakcie działania programu w wyniku wykonania instrukcji Jak to działa: Tworzenie zmiennych dynamicznych Tworzenie zmiennej dynamicznej nazwa wskaźnika = new typ zmiennej wskazywanej ; Operator new przydziela pamięć na zmienną dynamiczną typu podanego i adres tej zmiennej podstawia pod wskaźnik o podanej nazwie. Jeżeli alokacja (przydział pamięci) się nie powiedzie, np. pamięci już nie wystarczy, pod wskaźnik wstawi adres pusty (NULL). w = new int ; // przydziel pamięć na zmienną typu int i adres zapamiętaj jako wskaźnik w. ad_auta = new Sam ; // przydziel pamięć na zmienną typu Sam i adres zapamiętaj jako ad_auta 10
Adres (czyli wskaźnik) zmiennej wskazywanej kasujemy w trakcie działania programu w wyniku wykonania instrukcji Usuwanie zmiennej dynamicznej Jak to działa: delete nazwa wskaźnika ; Operator delete zwalnia pamięć zarezerwowaną na zmienną dynamiczną pod adresem o podanej nazwie i kasuje ten wskaźnik (potem nie można się już do niego odwoływać). delete w; delete ad_auta; Usuwanie zmiennych dynamicznych Nie wolno używać delete do zwalniania pamięci nie przydzielonej przez new (np. określonej przez wskaźnik do zmiennej statycznej) Nie wolno dwukrotnie zwalniać tego samego obszaru pamięci Można zastosować delete do wskaźnika pustego (nic się nie stanie) 11
Przykłady tworzenia i i usuwania zmiennych dynamicznych int *w; // deklaracja wskaźnika do zmiennej integer; w = new int; // przydzielenie pamięci zmiennej typu int wskazywanej przez w *w = 20; // podstawienie liczby 20 pod adres w cout << "Wartosc wskazywana powiekszona o 5 " << (*w)+5 << endl; // dodanie liczby 5 do wartości wskazywanej przez w delete w; // zwolnienie pamięci pod adresem w i skasowanie tego adresu 12
Operator ** i i & int *w; int* w; int * w; Zapisy równoważne: w jest wskaźnikiem do zmiennej typu int int a ; *w & a weź to, co jest pod adresem w znajdź wskaźnik do zmiennej a (jej adres) w - wskaźnik (zmienna statyczna) *w - zmienna dynamiczna (wskazywana przez w) a - zmienna statyczna & a - wskaźnik (zmienna statyczna) w *w & a a 13
Operator & i i nietypowe odwołania Do zmiennych statycznych możemy się dostać za pomocą adresów, używając operatora &: int *w; // w jest wskaźnikiem do zmiennej typu int int a = 1; // podstaw 1 pod zmienną o nazwie a w = & a ; // podstaw pod w wskaźnik do zmiennej a // zatem od tej pory zamiast a możemy pisać *w (tylko po co?) cout << *w; // wydrukuj to, co jest pod adresem w (czyli 1) Do zmiennych dynamicznych można się dostać za pomocą nazwy, używając operatora &: int *w; // w jest wskaźnikiem do zmiennej typu int int *w = &b; // zaś b jest nazwą zmiennej dynamicznej typu int // wskazywanej przez w (bo adres zmiennej b = wskaźnikowi w) 14
Dynamiczna rezerwacja tablic jednowymiarowych int main( ) { // zarezerwujemy tablicę T typu int int *T ; // T jest wskaźnikiem do pierwszego elementu tablicy int n; cout << "ile elementow w tablicy? " << endl; cin >> n; T=new int [n]; // rezerwujemy pamięć dla n elementów tablicy typu int // i adres pierwszego z nich zapamiętujemy jako T... for (int i=0; i<n; i++) cin >> T[ i ]; // zwykłe działania na tablicy... delete [ ] T; // tak zwalniamy pamięć przydzieloną na tablicę...... return 0; } Tak nie wolno działać na tablicach statycznych: int n; cin >> n; int T [n] ; Zamiast : int * T ; T=new int [n]; można napisać od razu: int * T = new int [n]; 15
Tablice w notacji wskaźnikowej Wszystkie elementy tablicy są umieszczone w ciągłym obszarze pamięci jeden za drugim: Nazwa tablicy użyta w wyrażeniach jest adresem początkowego elementu tablicy (uwaga: wyjątkiem jest funkcja sizeof, która odnosi się do całej tablicy) zmienna przechowująca adres wskaźniki Do elementów tablic rezerwowanych dynamicznie możemy się odwoływać w sposób tradycyjny, zgodnie z notacją tablicową. Ale notacja wskaźnikowa z natury rzeczy umożliwia szybszy, bezpośredni dostęp do elementów tablicy, nie wymagający wyznaczania wartości indeksów, a tylko dodawania liczb naturalnych do adresu.