« Dynamiczne zarządzanie pamięcią new i delete, lekcja »
Dynamiczne zarządzanie pamięcią za pomocą operatorów new i delete. (lekcja)
Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Zarejestruj się!
Autor: Piotr Szawdyński
Późniejsze modyfikacje: 'Dante'
Kurs C++

Dynamiczne zarządzanie pamięcią new i delete

[lekcja] Dynamiczne zarządzanie pamięcią za pomocą operatorów new i delete.

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:
C/C++
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:
  • Rozmiar bloku pamięci, który chcesz zarezerwować jest zbyt duży;
  • System nie posiada więcej zasobów pamięci i w związku z tym nie może Ci jej przydzielić.
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ą:
C/C++
delete wskaznik;
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:
C/C++
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:
C/C++
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:
C/C++
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.
C/C++
#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"; //dopisanie nowego wiersza
            if( dlugosc + tWiersz.length() + 1 > rozmiar ) //potrzeba więcej pamięci niż jest dostępne
            {
                cout << "Tworzy nowy blok pamieci!" << endl;
                int tNarzutDanych = 20; //jeśli ustawisz 0 to rezerwacja będzie się odbywała za każdym razem
                rozmiar = tWiersz.length() + dlugosc + 1 + tNarzutDanych; //nowy rozmiar bloku
                char * tNoweDane = new char[ rozmiar ]; //rezerwacja nowego bloku pamięci, który pomieści stare i nowe dane
                if( tablica != NULL ) memcpy( tNoweDane, tablica, dlugosc ); //jeśli stara tablica istnieje to skopiuj dane do nowej tablicy
               
                memcpy( & tNoweDane[ dlugosc ], & tWiersz[ 0 ], tWiersz.length() ); //skopiuj dane do nowej tablicy w wyznaczone miejsce
                if( tablica != NULL ) delete[] tablica; //zwolnij pamięć zajmowaną przez stare dane
               
                tablica = tNoweDane; //nadaj nowy wskaźnik zmiennej tablica
            } else
            { //jest wystarczająca ilość pamięci nie wymagana rezerwacja
                cout << "Jest wystarczajaca ilosc miejsca!" << endl;
            }
            memcpy( & tablica[ dlugosc ], & tWiersz[ 0 ], tWiersz.length() ); //skopiuj dane do tablicy w wyznaczone miejsce
            dlugosc = tWiersz.length() + dlugosc; //zapisz długość tekstu
            tablica[ dlugosc ] = 0; //oznacz miejsce końca tekstu w tablicy
        }
    } 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

  • Funkcji malloc() nie można stosować zamiennie z operatorem new.
  • Funkcji free() nie można stosować zamiennie z operatorem delete.
  • Zamiana operatora new na funkcję malloc() może spowodować nieprawidłowe funkcjonowanie programu - operator new wykonuje czynności, które przy użyciu funkcji malloc() trzeba wywołać ręcznie.
  • Jeśli dokonujesz zamiany funkcji malloc() na operator new, pamiętaj aby pozamieniać również funkcję free() na operator delete (lub delete[] w zależności od sytuacji).
Poprzedni dokumentNastępny dokument
Funkcje kolejne aspektyFunkcje raz jeszcze