300 likes | 522 Views
Zachodniopomorska Szkoła Biznesu. Podstawy programowania II. Wykład 4: Dynamiczna alokacja pamięci, referencje, rekurencja. Zapotrzebowanie na pamięć. W wielu programach nie ma możliwości przewidzenia (na etapie projektowania) ilości przetwarzanych danych,np.:
E N D
Zachodniopomorska Szkoła Biznesu Podstawy programowania II Wykład 4: Dynamiczna alokacja pamięci, referencje, rekurencja
Zapotrzebowanie na pamięć • W wielu programach nie ma możliwości przewidzenia (na etapie projektowania) ilości przetwarzanych danych,np.: • Baza danych - dowolna liczba rekordów, • Obróbka grafiki i dźwięku - nieznane rozmiary pliku. • Zapotrzebowanie na pamięć jest w wielu sytuacjach teoretycznie nieograniczone. • Projektant lub programista musi jednak na etapie tworzenia programu poczynić pewne założenia. • W przypadku wykorzystania statycznych struktur danych(np. zwykła tablica), niezbędne jest określenie maksymalnego rozmiaru danych już podczas kodowania. • Deklaracja nadmiarowej tablicy powoduje, że pamięć nie jest wykorzystana w sposób efektywny. Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Przykład danych statycznych • Program ma przetwarzać kwoty transakcji finansowych firmy z ostatniego roku.Deklaracja tablicy statycznej: #define MAX 1000 // wartość arbitralna!float kwoty[MAX]; • Dobór wartości MAX bardzo rzadko okaże się trafny, w zależności od firmy i roku: • Transakcji może być więcej niż MAX, np. 1005:- program przestaje spełniać swoje funkcje,- wyjście z błędem (przy braku zabezpieczenia) • Transakcji może być dużo mniej niż MAX, np. 90:- niepotrzebnie zajęta pamięć. • Nie ma przy tym znaczenia czy tablica jest globalna czy lokalna, ponieważ zawsze trzeba przy jej tworzeniu podać rozmiar. Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Co to jest alokacja pamięci? • Każdy program potrzebuje pewnej ilości pamięci do przechowywania • swojego kodu • stosu • wartości stałych (np. wypisywanych tekstów) • wartości zmiennych (statycznych) Ilość ta jest znana w chwili ładowania programu, • Następuje wtedy żądanie przydziału (alokacji) pamięci od systemu operacyjnego. Jeżeli brakuje pamięci, program się nie uruchamia. • Program może również żądać przydziału dodatkowej pamięci od systemu operacyjnego (dynamiczna alokacja pamięci) • Przydzielona pamięć powinna być zwolniona przez program przed jego zakończeniem Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Alokacja dynamiczna: operator new • Operator jednoargumentowy (nie funkcja!) new wymaga podania typu danej, jaka ma być umieszczona w pamięci,np.: new typ(np. new int) • Rozmiar przydzielonej pamięci zależy od podanego typu i jest określany automatycznie • W odpowiedzi new generuje wskaźnik, którego wartość wskazuje otrzymane miejsce w pamięci, a typ wskaźnika odpowiada typowi podanemu jako operand, np.: int *wsk = new int; // wskaźnik na int • Przydział pamięci może się nie powieść! Operator new zwraca w takim przypadku wskaźnik zerowy (NULL). • Należy zawsze sprawdzić czy alokacja się powiodła. Zaniedbanie może prowadzić do katastrofalnych skutków. Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Cechy zmiennych dynamicznych • Zmienna taka nie ma nazwy, dostępna jest tylko poprzez wskaźnik • Zmienna istnieje od chwili alokacji do chwili zwolnienia pamięci • Zakres dostępności zmiennej jest podobny do zmiennych statycznych (czyli wszędzie), pod warunkiem, że dostępny jest wskaźnik do tej zmiennej • Zmienna nie jest inicjalizowana automatycznie, może zawierać "śmieci" • Przydział pamięci odbywa się z tzw. sterty (ang. heap), której maksymalny rozmiar jest często jedną z opcji projektu w kompilatorze Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Rozmieszczenie danych w pamięci • W przypadku danych tworzonych dynamicznie, w pamięci programu znajdują się wskaźniki, a właściwe dane - w pamięci przydzielonej od systemu operacyjnego. • Zmienne przydzielone przez system w kolejnych alokacjach mogą fizycznie znajdować się w odległych miejscach pamięci Pamięć programu Pamięć przydzielana dynamicznie wskaźnik 1 zmienna 1 wskaźnik 2 zmienna 3 wskaźnik 3 zmienna 2 Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Zwolnienie pamięci: operator delete • Pamięć pobrana dynamicznie powinna zostać zwolniona przed zakończeniem programu. Służy do tego operator (nie funkcja) jednoargumentowy delete, który wymaga podania wskaźnika na wcześniej zaalokowaną pamięć, np.: int *wsk = new int; // alokacja// tu korzystamy z danej *wskdelete wsk; // zwolnienie • Operator delete nie zwraca żadnego wyniku (typ void) Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Zwolnienie pamięci - problemy • Zaniechanie zwolnienia pamięci powoduje, że przydzielony jej fragment pozostaje niedostępny dla innych programów aż do restartu systemu operacyjnego • Po zwolnieniu pamięci nie wolno już korzystać z danej zapisanej pod adresem zapisanym we wsk!Ten fragment pamięci już do nas nie należy!W szczególności nie wolno tej zmiennej powtórnie zwolnić. • Jako argument dla delete wolno podać wyłącznie wartość wskaźnika, który wskazuje na pamięć przydzieloną dynamicznie. • Zwolnienie pamięci jeżeli wskaźnik wynosi NULL (0) nie spowoduje katastrofy (system wykryje nieprawidłowość i nie wykona zwolnienia pamięci)Dobrym pomysłem może być więc ustawianie wskaźnika na wartość NULL natychmiast po zwolnieniu pamięci. Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Korzystanie z danych dynamicznych • Ponieważ dostęp do pobranej pamięci istnieje poprzez wskaźnik, w celu dostępu do wartości zmiennej należy korzystać z operatora * lub [], np.: int *wsk = new int;if (wsk!=NULL) // czy przydzielono pamięć?{ cin >> *wsk; cout << "Podana wartość: " << wsk[0]; cout << "Do kwadratu: " >> (*wsk) * (*wsk); delete wsk; // zwolnienie pamięci} else // alokacja nie powiodła się cout << "Brak pamięci!\n"; Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Utrata dostępu do danej • Wskaźnik zwrócony przez operator new, jest jedynym łącznikiem z pobranym miejscem pamięci, a więc również z daną, która tam się znajduje. • W przypadku zniszczenia wartości tego wskaźnika niemożliwe jest: • korzystanie z tego fragmentu przydzielonej pamięci • odczyt i zapis danej, która tam jest zapisana • zwolnienie pobranej pamięci • Przykład:int *wsk = new int; // pierwszy fragment pamięci *wsk=10;wsk = new int;// drugi fragment pamięci// utraciliśmy dostęp do fragmentu pierwszego*wsk=20;delete wsk; Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Inicjalizacja zmiennej dynamicznej • Podczas tworzenia zmiennej dynamicznej można od razu podać jej wartość • Składnia new typ(wartość) • Przykład int *iw=new int(20);cout << *iw; // 20float *fw=new float(-2.5);cout << *fw; // -2.5 Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Alokacja pamięci na strukturę • W przypadku danych prostych (jak int lub float), sens dynamicznej alokacji jest wątpliwy: • i tak trzeba tworzyć daną statyczną (wskaźnik) • zużycie pamięci wzrasta (miejsce na wskaźnik i daną) • Swoją wartość okazuje dynamiczna alokacja w przypadku danych złożonych, np.: struct Duza{//tu duża liczba pól - potrzeba sporo pamięci};Duza * wsk = new Duza; • W tym przypadku jedyną daną statyczną jest wskaźnik wsk, natomiast liczne dane znajdują się w pamięci przydzielonej dynamicznie Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Dostęp do składników struktury (1) • Dostęp za pomocą operatora . struct Zespolona{ float re, im;};Zespolona * z = new Zespolona;if (z!=NULL){ // przydzielono pamięć (*z).re=10; // nawias konieczny! (*z).im=20; cout << (*z).re << "+"<< (*z).im << "i\n"; delete z;} else // alokacja nie powiodła się cout << "Brak pamięci!\n"; Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Dostęp do składników struktury (2) • Dostęp za pomocą operatora -> struct Zespolona{ float re, im;};Zespolona * z = new Zespolona;if (z!=NULL){ // przydzielono pamięć z->re=10; z->im=20; cout << z->re << "+"<< z->im << "i\n"; delete z;} else // alokacja nie powiodła się cout << "Brak pamięci!\n"; Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Tablica zmiennych dynamicznych (1) • Jeżeli trzeba utworzyć pewną liczbę zmiennych w sposób dynamiczny, można wykorzystać statyczną tablicę wskaźników Pamięć programu Pamięć systemu operacyjnego T[0] zmienna T[1] zmienna T[2] T[3] zmienna T[4] T[5] zmienna T[6] zmienna T[7] zmienna T - tablicawskaźników zmienna zmienna Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Tablica zmiennych dynamicznych (2) struct Zespolona{ float re, im; };Zespolona *z[10];// tablica wskaźnikówint blad=0; // znacznik błędufor (int i=0; i<10; i++){z[i]=new Zespolona; // utworzenie zmiennej i if (z[i]==NULL) blad=1;}if (!blad){// tutaj można wykorzystać utworzone zmienne,npz1[5]->re=10; z1[5]->im=2; // liczba 10-2icout << z1[5]->re << "+" << z1[5]->im << "i\n";} else cout << "Brak pamięci!\n");for (int i=0;i<10; i++) if (z[i]!=NULL)delete z[i]; Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Dynamiczna alokacja tablicy • Dynamiczne utworzenie tablicy :wskaźnik=new typ[rozmiar]; • Operator new generuje w tym przypadku również wskaźnik do typu typ, z tym że przydzielany jest obszar pamięci pozwalający zapisać rozmiar danych typu typ. • Przykładint *tab=new int[10]; - tablica 10 int'ów • Nie ma możliwości inicjalizacji takiej tablicy • Programista musi być świadom czy wskaźnik wskazuje na jedną daną czy na pierwszą daną z tablicy (i pamiętać rozmiar) • Ze wskaźnika można korzystać w identyczny sposób jak ze "zwykłej" tablicy - z użyciem operatora * lub [] • Zwolnienie tablicy utworzonej dynamicznie odbywa się za pomocą konstrukcji: delete [] wskaźnik; Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Alokacja tablicy - przykłady • int *wsk, *tab;wsk=new int; tab=new int[20]; // ten sam typ wskaźnika dla // zmiennej prostej i tablicy • *wsk = 10; // wstawienie wartości*tab = 10; // zerowy element tablicy *(tab+1)=11; // pierwszy element tablicy wsk[0]=20; // poprawne ale mylącetab[0]=20; // zerowy element tablicy tab[1]=1; // pierwszy element tablicytab[19]=100; // ostatni element tablicy wsk[1]=10;// Błąd! To nie nasza pamięć! • delete wsk;delete [] tab; Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Alokacja tablicy - rozmieszczenie • Struktury danych dla następujących zmiennych:int *T=new int[8]; Pamięć programu Pamięć systemu operacyjnego T T[0] T[1] wskaźnik na int T[2] T[3] T[4] T[5] T[6] T[7] tablica 8 zmiennychtypu int Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Przykład alokacji tablicy int ile;cout << "Podaj liczbę danych: ";cin >> ile;float *tab=new float[ile];// zmienny rozmiar!!!if (tab!=NULL){ for (int i=0; i<ile; i++) { cout << "Podaj wartość " << i << ": "; cin >> tab[i]; // wprowadzenie danych } // tu można korzystać z podanych liczbdelete [] tab;} else cout << "Brak pamięci!\n"; Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Przykładowy program • Program: Statystyka.cpp - wczytywanie tablicy z pliku tekstowego - dynamiczna alokacja tablicy Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Referencje • Referencja jest typem pochodnym łączącym pewne cechy typu bazowego oraz wskaźnika (nie ma tylko analogii do tablicy!) • Referencje nie występują w języku C, tylko w C++, C#, Javie • Można ją rozumieć jako przezwisko (alias) pewnej już istniejącej zmiennej. Referencja zawiera w sobie adres i typ tej zmiennej ale nie wymaga stosowania operatora * (jak wskaźnik), "na oko" wygląda w programie jak zwykła zmienna • Składnia deklaracji: typ & nazwa_referencji; • Przykład void main(){ int a=0,b=0; int &ref=a; ref=10; // ref=10 i a=10!!! ref=b; // ref=0, a=0 ref=20; // ref=20 i a=20 (b=0)} Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Referencje jako parametr funkcji • Sens użycia referencji jako parametru jest podobny jak wskaźnika (poza przesyłaniem tablic), tyle że dostęp do zmiennej jest wygodniejszy (nie wymaga użycia operatorów *) • Wywołując funkcję trzeba pamiętać, że może ona zmienić wartość zmiennej (w wywołaniu tego nie widać) • Przykład:void plusminus(int *tab, int n, int &plus, int &minus){plus=0, minus=0; // zerujemy referencje for (int i=0; i<n; i++) if (tab[i]>0) plus++; else minus++;} // plus i minus obliczonevoid main(){ int tablica[20], ileplus, ileminus; plusminus(tablica, 20, ileplus, ileminus);} Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Referencje - uwagi • Podobnie jak wskaźnik, referencja zawsze wskazuje na jakieś miejsce w pamięci, nawet nie została w programie zainicjowana.Użycie takiej referencji jest niebezpieczne. • Należy pamiętać o tym, że referencja jest przezwiskiem jakiejś innej zmiennej i jej zmiana powoduje również zmiany tamtej zmiennej. • Referencję należy zainicjować przypisując jej zmienną, później można podstawiać również zwykłe wyrażenia, np.:void plus5(int &liczba){ liczba+=5; // zwiększamy podaną zmienną o 5} Wywołania:int a;plus5(a); // podajemy zmienną aplus5(a+1); // błąd, a+1 to nie zmiennaplus5(10); // bład, 10 to nie zmienna Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Referencja na wskaźnik • Skoro można utworzyć referencję do zwykłej zmiennej, to można i do wskaźnika:Składnia: typ * & nazwa_referencji; • Przykład: void main(){ int a,b, *wsk1; int *& refwsk=wsk1; wsk1=&a; // wsk1 i refwsk wskazują na a refwsk= &b; // wsk1 i refwsk wskazują na b} Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Rekurencja • W niektórych zagadnieniach występuje potrzeba wywołania funkcji przez nią samą, np.: void odliczaj(int n){ cout << n <<endl; if (n>0) odliczaj(n-1); // rekurencja} • Podczas każdego wywołania rekurencyjnego tworzony jest na stosie oddzielny zestaw zmiennych lokalnych (tutaj zmienna n) • Należy zawsze pamiętać o zdefiniowaniu warunku zakończenia rekurencji, inaczej funkcja będzie wywoływać się w nieskończoność (w końcu zapychając pamięć) Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Rekurencja - silnia • Wzór rekurencyjny: n! = n* (n-1)! , n>1; 0!=1!=1. • long silnia(short n){ if (n>0) return n*silnia(n-1); else return 1;} • Wzór iteracyjny: n! = n*(n-1)*...*2*1, n>1; 0!=1!=1. long silnia(short n){ long sil=1; for (;n>0;n--) sil*=n; return sil;} Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja
Rekurencja - ciąg Fibonacciego • Wzór rekurencyjny na wyraz k ciągu Fibonacciego:Fib(k) = Fib(k-1) + Fib(k-2), k>2; Fib(1)=Fib(2)=1.Ciąg: 1 1 2 3 5 8 13 21 34 55 89 ... • unsigned long Fib(unsigned short k){ if (k>2) return Fib(k-1)+Fib(k-2); else return 1;} Podstawy programowania II - Dynamiczna alokacja pamięci, referencje, rekurencja