Ograniczanie maksymalnego rozmiaru danych odchodzi w zapomnienie
Do tej pory pisząc programy w których organizowałeś dane, byłeś zmuszany do określania górnej granicy danych, jakie może pomieścić Twój program. Takie ograniczanie bardzo często nie jest jednak komfortowe i skuteczną alternatywą jest tu dynamiczne zarządzanie pamięcią.
W języku
C do przydzielania i zwalniania pamięci służyły głównie
funkcje malloc() i
free(). Korzystanie z nich było i jest nadal bardzo popularne, jednak w
C++ zostały one zastąpione
operatorami new i
delete.
Dynamiczne przydzielenie pamięci
W języku C++ do przydzielania nowego bloku pamięci służy operator
new. Jego składnia wygląda następująco:
wskaznik1 = new typ_zmiennej;
wskaznik2 = new typ_zmiennej[ ilosc_elementow_danego_typu ];
Wskaźnik jak już dowiedziałeś się w rozdziale, który był temu poświęcony wskazuje na dane, a sam najczęściej zajmuje 4 bajty bez względu na to, na jakie dane wskazuje.
Typ zmiennej informuje operator
new, o rozmiarze pamięci jaka ma zostać przydzielona. Jeśli chcemy aby nowo przydzielony blok był tablicą to aktualną składnię uzupełniamy o dodatkowy parametr, w którym określamy ilość elementów tak samo jak robiliśmy to w przypadku tablic. Operator
new na podstawie wszystkich podanych informacji przydzieli odpowiednią ilość pamięci tak, aby na pewno zmieściła się taka ilość danych o którą zażądałeś.
Jeśli przydział pamięci powiódł się, to wartość zmiennej wskaźnik będzie różna od zera. Jeśli wartość wskaźnika będzie równa 0, to pamięć nie została przydzielona. Wartość
0 bardzo często jest zastępowana stałą
NULL. Zalecane jest jednocześnie korzystanie ze stałej
NULL, ponieważ standardy związane z wartością
0 mogą się kiedyś zmienić, a w związku z tym Twoje programy przestałyby działać. Powody, dla których pamięć nie mogła zostać przydzielona to:
Dostęp do danych za pomocą wskaźników został już omówiony w rozdziale poświęconym wskaźnikom i nie będzie tu ponownie poruszany.
Zwalnianie pamięci przydzielonej dynamicznie
Zwalnianie pamięci przydzielonej dynamicznie jest jeszcze prostsze od jej przydziału i służy do tego operator
delete. Jeśli pamięć dla danych, na które wskazuje zmienna
wskaznik została przydzielona bez parametru określającego ilość elementów w tablicy, to usuwana jest następującą składnią:
Jeśli natomiast przydzieliliśmy pamięć z użyciem parametru określającego ilość elementów tablicy to musimy poinformować operator
delete o tym, że wskaźnik wskazywał na tablicę rekordów. Aby to zrobić dopisujemy zaraz za operatorem nawiasy kwadratowe
[]. Nie podajemy jednak w nich rozmiaru tablicy, ponieważ operator ten sam ustala rozmiar bloku jaki został przydzielony, a następnie go usuwa z pamięci. Składnia tej operacji wygląda następująco:
delete[] wskaznik_do_tablicy;
Kopiowanie bloków pamięci
Jeśli będziemy chcieli przekopiować zawartość pamięci z jednego miejsca do drugiego możemy zrobić to conajmniej na dwa sposoby. Sposób pierwszy to wykorzystanie jakiejkolwiek pętli i kopiowanie danych bajt po bajcie. Przykład:
for( int i = 0; i < ilosc; i++ ) nowybufor[ i ] = starybufor[ i ];
Problem jest w bardzo prosty sposób rozwiązany, jednak nie należy on do najwydajniejszych. Wydajniejszą metodą jest wykorzystanie funkcji, która służy do kopiowania bloków pamięci. Jej definicja wygląda następująco:
void * memcpy( void * adres_docelowy, const void * adres_zrodlowy, size_t ilosc );
Jako pierwszy parametr (
adres_docelowy) podajemy adres do pamięci pod którym mają się znaleźć nowe dane. Drugi parametr (
adres_zrodlowy) to miejsce z którego dane mają zostać pobrane i również określamy je za pomocą adresu. Trzecim, a zarazem ostatnim parametrem (
ilosc) jest ilość bajtów, jaka ma zostać przekopiowana ze źródła do celu.
Kopiując małe bloki pamięci różnicy w szybkości działania programu nie zaobserwujesz, jednak gdy przyjdzie Ci kopiować kilka MB danych różnice czasowe mogą być już bardzo odczuwalne.
Przykład
Przeanalizuj dokładnie działanie tego programu i poeksperymentuj z nim.
#include<iostream>
#include<string>
#include<cstring>
#include<conio.h>
using namespace std;
int main()
{
int rozmiar = 0;
int dlugosc = 0;
char * tablica = NULL;
cout << "Pusty wiersz konczy dzialanie programu." << endl;
for( int i = 0; i < 40; i++ ) cout << "-";
cout << endl;
string tWiersz;
do
{
getline( cin, tWiersz );
if( tWiersz.length() > 0 )
{
tWiersz += "\r\n";
if( dlugosc + tWiersz.length() + 1 > rozmiar )
{
cout << "Tworzy nowy blok pamieci!" << endl;
int tNarzutDanych = 20;
rozmiar = tWiersz.length() + dlugosc + 1 + tNarzutDanych;
char * tNoweDane = new char[ rozmiar ];
if( tablica != NULL ) memcpy( tNoweDane, tablica, dlugosc );
memcpy( & tNoweDane[ dlugosc ], & tWiersz[ 0 ], tWiersz.length() );
if( tablica != NULL ) delete[] tablica;
tablica = tNoweDane;
} else
{
cout << "Jest wystarczajaca ilosc miejsca!" << endl;
}
memcpy( & tablica[ dlugosc ], & tWiersz[ 0 ], tWiersz.length() );
dlugosc = tWiersz.length() + dlugosc;
tablica[ dlugosc ] = 0;
}
} while( tWiersz.length() != 0 );
if( tablica != NULL )
{
cout << "Dane jakie wypisales to: " << endl;
cout << tablica << endl;
delete[] tablica;
} else cout << "Nie wpisales niczego!";
getch();
return( 0 );
}
Jeśli przeanalizowałeś przykład i go zrozumiałeś to zapewne stwierdziłeś, że taką funkcjonalność otrzymujesz korzystając z klasy
std::string. Masz rację, jednak przykład ma na celu zademonstrowanie Tobie praktycznego, dynamicznego zarządzania pamięcią. Jeśli nie nauczysz się dynamicznie zarządzać pamięcią, możesz zapomnieć o realizowaniu jakiegokolwiek większego projektu z którego będzie płynął jakiś większy użytek, niż domowe wykorzystywanie własnych programów. Jest co prawda biblioteka szablonów, która umożliwia łatwe zarządzanie danymi jednak programista, który sam nie potrafi posługiwać się prawidłowo operatorami
new i
delete (lub funkcjami
malloc() i
free()) jest tylko jego imitacją, z której żaden pracodawca nie będzie miał pożytku.
Informacje dodatkowe