Dynamiczne zarządzanie pamięcią

Rodzaje pamięci

  • Pamięć kodu – pamięć rezerwowana przez kompilator, w której umieszczony jest kod programu

  • Pamięć statyczna – pamięć zarezerwowana dla zmiennych globalnych lub statycznych

  • Pamięć automatyczna (stos) – rezerwowana na potrzeby wywołań funkcji (argumenty i zmienne lokalne)

  • Pamięć wolna (sterta)

    • reszta pamięci komputera

    • dostępem do pamięci wolnej zarządzają operatory new, new[], delete i delete[]

Do dynamicznego zarządzania pamięcią wolną oraz jawnego tworzenia i usuwania obiektów służą dwa operatory new i delete. Operatory te umożliwiają zarządzanie obiektami dowolnych klas lub typów wbudowanych, a także tablicami dowolnego typu. Zastąpiły funkcje zarządzania pamięcią malloc() i ``free()``znane z języka C.

Operatory new i delete są częścią specyfikacji języka C++, a nie biblioteki standardowej.

Operatory new i new[]

Operator new jawnie przydziela pamięć obiektom wybranego typu.

float* pf = new float;

std::string* pstr1 = new string;

std::string* pstr2;
pstr2 = new string("hello");

Operator new[] jest używany do przydzielania pamięci tablicom

char* s = new char[len+1];  // len+1 znaków
std::string* strings = new std::string[40];
float** values = new float*[10];  // 10 wskaźników na double

Operator new w przeciwieństwie do funkcji malloc() zwraca zawsze wskaźnik właściwego typu – nie jest wymagana jawna konwersja przed odwołaniem się do tak utworzonej zmiennej. Jeśli operator new zostanie użyty w celu utworzenia nowego obiektu, obiekt ten zostanie zainicjowany. Typy wbudowane nie zostają zainicjowane. Nie ma potrzeby sprawdzania czy operator new zwraca wartość nullptr. W przypadku przydział pamięci lub utworzenie obiektu się nie powiodło rzucane są wyjątki. Operator new może zostać także zaimplementowany przez programistę do tworzonych przez niego typów. Umożliwia to optymalizację zarządzania pamięcią przy zachowaniu standardowego interfejsu tworzenia obiektów.

Operatory delete i delete[]

Operator delete zwalnia pamięć przydzieloną wcześniej za pomocą operatora new.

float* pf = new float;
std::string* pstr1 = new string;

delete pf;
delete pstr1;

Operator delete[] zwalnia pamięć dla tablic.

char* s = new char[len+1];    // len+1 znaków
std::string* strings = new std::string[40];
float** values = new float*[10];

delete [] s;
delete [] strings;
delete [] values;

Użycie operatora delete w stosunku do obiektu, który nie został utworzony za pomocą operatora new nie jest zdefiniowane i może być fatalne w skutkach. Operator delete może być stosowany dla wskaźników o wartości nullptr, 0 lub NULL.

Person* ptr = nullptr;

if (is_satisfied())
    ptr = new Person;

delete ptr; // poprawne, bez względu na to, czy pamięć została przydzielona

Problemy ze wskaźnikami

Podstawowe wytyczne bezpiecznego wykorzystywania wskaźników:

  • Nie uzyskuj dostępu do obiektów przez wskaźnik zerowy.

    int* p = NULL;
    *p = 7; // Błąd!
    
  • Inicjuj swoje wskaźniki.

    int* p;
    *p = 9; // Błąd!
    
  • Nie uzyskuj dostępu do nieistniejących elementów tablicy.

    int a[10];
    int* p = &a[10];
    *p = 11; // Błąd!
    a[10] = 12; // Błąd!
    
  • Nie próbuj uzyskać dostępu poprzez usunięty wskaźnik.

    int* p = new int(7);
    // ...
    delete p;
    // ...
    *p = 10; // Błąd!
    
  • Nie zwracaj wskaźnika do zmiennej lokalnej.

    int* f()
    {
       int x = 7;
       return &x;
    }
    
    // ...
    int* p = f();
    *p = 15;  // Błąd!
    

Inteligentne wskaźniki

W C++11/14 należy unikać jawnych wywołań operatorów new oraz delete. W celu dynamicznego zarządzania pamięcią (zasobami) należy używać klas inteligentnych wskaźników:

  • std::unique_ptr<T> - wskaźnik implementujący wyłączne prawo własności do zarządzanego zasobu

  • std::shared_ptr<T> - wskaźnik implementujący współdzielone prawo własności

Inteligentne wskaźniki w C++:

  • przeciążają operatory dereferencji * oraz ->

  • upraszczają dynamiczne zarządzanie pamięcią i umożliwiają uniknięcie wycieków pamięci.

Wskaźnik std::unique_ptr

  • std::unique<ptr> implementuje wyłączne prawo własności do alokowanego dynamicznie obiektu

  • Destruktor std::unique<ptr> zwalnia zasób (domyślnie wywołuje destruktor i zwalnia pamięć po obiekcie)

  • Nie może być kopiowany, ale może być przesuwany - implementuje tzw. move semantics

#include <memory>
#include <iostream>
#include <string>

int main()
{
    std::unique_ptr<std::string> ptr_word = std::make_unique<std::string>("smart_ptr");

    std::cout << *ptr_word << " has length " << ptr_word->size() << std::endl;
} // automatyczne zwolnienie pamięci po obiekcie ptr_word

Wskaźnik std::shared_ptr

std::shared_ptr jest szablonem nieingerencyjnego wskaźnika zliczającego odniesienia do wskazywanych obiektów.

Działanie: * konstruktor : tworzy licznik referencji i inicjuje go na 1 * konstruktor kopiujący: zwiększa licznik odniesień * destruktor: zmniejsza licznik odniesień, jeżeli ma on wartość 0, to kasuje obiekt

Przykład:

#include <memory>

using namespace std;

class MyClass { /* implementacja */ };

void f()
{
    shared_ptr<MyClass> p1 = make_shared<MyClass>(1);
    {
        shared_ptr<MyClass> p2 = p1;
        // licznik odniesień == 2
        /* ... */
    }  // destruktor p2, licznik = 1
}  // destruktor p1 usuwa obiekt