С++ Аллокаторы и работа с памятью std::allocator и операторы new и delete

Стандартная библиотека предоставляет класс распределителя памяти std::allocator, распределяющий неинициализированную память, а так же операторы: operator new, operator new[], operator delete, operator delete[] поверх которых и создан std::allocator

Стандартный аллокатор std::allocator

Построен поверх операторов new и delete. Позволяет:

  • выделить блоки памяти (allocate)
  • освободить блоки памяти (deallocate)
  • переместить блоки памяти (construct). Обратите внимание на то, что construct не выделяет память для объекта !!!
  • уничтожить объект в памяти с вызовом его деструктора (destroy). Обратите внимание на то, что destroy не освобождает память из под объекта !!!

Подробнее std::allocator
Подробнее operator new и operator delete

Требует подключения заголовка: #include <memory>

#include <memory>

template<typename T>
class allocator {
    // Выделение памяти для n объектов типа T  КОНСТРУКТОР НЕ ВЫЗЫВАЕТСЯ
    // (Выделение памяти, достаточной для хранения объекта типа T без инициализации)
    T* allocate (int n);
    // Освобождение памяти занятой n объектами типа T с адреса p
    // (Освобождение неинициализированной памяти 
    // размера, достаточного для хранения объекта типа T)
    void deallocate(T* p, int n);
    // Создание объетка типа T со значением v 
    // по адресу p. Используется семантика перемещения
    // (Создание объекта типа T в неинициализированной памяти) 
    // вызывает   ::new((void *) p) T(args...);  КОНСТРУКТОР ВЫЗЫВАЕТСЯ
    void construct(T* p, const T & v);
    // Уничтожение объета T по адресу p
    // (Уничтожение объекта типа T и возвращение 
    // памяти в неинициализированное состояние)
    // явно ВЫЗЫВАЕТ ДЕСТРУКТОР: p->~_Up();
    void destroy(T* p);
};

std::allocator::construct на самом деле имеет 2 формы:

// Использует конструктор копирования
void construct( pointer p, const_reference val );
// 
template< class U, class... Args >
void construct( U* p, Args && ... args );

Пример реализации ф-ции которая перемещает объект в памяти:

// перемещает элемент в новый участок памяти, создавая копию
// в неинициализированной памяти, а затем уничтожает оригинал.
template<typename T>
T * moveObjectToNewLocation(T * object)
{
    // создаем стандартный аллокатор для объектов типа T
    std::allocator<T> alloc;
    // Выделяем новую память под 1 элемент типа T
    T * newLocation = alloc.allocate(sizeof(T));
    // Копирование объекта в новую память
    // здесь будет использован конструтор копирования !
    alloc.construct(newLocation, *object);

    // Уничтожение object->~U(); Здесь нам это не нужно
    //alloc.destroy(object);

    // Освобождаем память из под изначального объекта
    // иначе имеем утечку !!!
    alloc.deallocate(object, sizeof(T));
    return newLocation;
}

// Будем оперировать объектами такого вот класса
class MyStruct {
public:
    MyStruct(const int v) : val{ v } {
        cout << "MyStruct(" 
             << static_cast<void*>(this) 
             << ")" << endl;
    }

    MyStruct(const MyStruct & rhs) : val{ rhs.val } {
        cout << "MyStruct(const MyStruct & = " 
             << static_cast<void*>(this) << ")" << endl;
    }

    int getVal() const { return val; }

    ~MyStruct() {
        cout << "~MyStruct(" 
             << static_cast<void*>(this) 
             << ")" << endl;
    }
private:
    int val;
};

// собсна код
int main(int argc, char* argv[]) 
{
    // создаем
    MyStruct * m1 = new MyStruct{ 10 };
    cout << m1->getVal() << endl; // 10

    // перемещаем данные объекта в новую область памяти
    MyStruct * m2 = moveObjectToNewLocation(m1);

    // Данный выызов теперь "в не закона" 
    // - неопределенное поведение !!!
    cout << m1->getVal() << endl; 
    cout << m2->getVal() << endl; // 10

    // удаляем память выделенную 
    // в moveObjectToNewLocation
    delete m2; 
}

Оператор new:

Операторы new и delete - ВЫЗЫВАЮТ конструкторы и деструкторы.

Явные операторы ::operator new(...) и ::operator delete(...) НЕ ВЫЗЫВАЮТ конструкторы и деструкторы:

Существует 3 варианта вызова оператора new:

// 1) (throwing) Выдаст исключение std::bad_alloc если память выделить не удалось
void* operator new (std::size_t size) throw (std::bad_alloc);     
void* operator new[] (std::size_t size) throw (std::bad_alloc);   

// 2) (nothrow) не будет выдавать исключение если 
// память выделить не удалось, но при этом вернется nullptr
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw(); 
void* operator new[] (std::size_t size, const std::nothrow_t& nothrow_value) throw(); 

// 3) (placement) - размещение:
void* operator new (std::size_t size, void* ptr) throw();
void* operator new[] (std::size_t size, void* ptr) throw();

Примеры:

std::cout << "1: ";
// Выделяет память с помощью вызова ::operator new(sizeof(MyClass)),      
// а затем строит объект в выделенной памяти, после ВЫЗЫВАЕТ КОНСТРУКТОР MyClass
MyClass * p1 = new MyClass;     

std::cout << "2: ";
// Выделяет память с помощью вызова ::operator new(sizeof(MyClass), std::nothrow),    
// а затем строит объект в выделенной памяти, после ВЫЗЫВАЕТ КОНСТРУКТОР MyClass
MyClass * p2 = new (std::nothrow) MyClass;      

std::cout << "3: ";
// НЕ ВЫДЕЛЯЕТ ПАМЯТЬ, вызывает ::operator new(sizeof(MyClass), p2)       
// строит объект в памяти ПО АДРЕСУ p2, после ВЫЗЫВАЕТ КОНСТРУКТОР MyClass
new (p2) MyClass;

std::cout << "4: ";
// Выделяет память с помощью вызова ::operator new(sizeof (MyClass))       
// НЕ ВЫЗЫВАЕТ КОНСТРУКТОР MyClass 
MyClass * p3 = (MyClass*) ::operator new(sizeof(MyClass));      

delete p1;
delete p2;
delete p3;

// Вывод кода выше:
// 1 : constructed[00CBE0B8]
// 2 : constructed[00CBE278]
// 3 : constructed[00CBE278] 
// 4 :

Оператор delete:

// (обычный) Освобождает блок памяти, на который указывает ptr (если не нулевой), 
// высвобождает ранее выделенную память (при помощи operator new)
void operator delete (void* ptr) throw();
void operator delete[] (void* ptr) noexcept;    
// (nothrow) То же, что и выше
void operator delete (void* ptr, const std::nothrow_t& nothrow_constant) throw();
void operator delete[] (void* ptr, const std::nothrow_t& nothrow_constant) noexcept;
// (placement) Ничего не делает.    
void operator delete (void* ptr, void* voidptr2) throw();
void operator delete[] (void* ptr, void* voidptr2) noexcept;

Данный оператор НЕ ВЫЗЫВАЕТ ДЕСТРУКТОР, если очень нужно, то деструктор необходимо вызвать явно через экземпляр объекта до вызова оператора:

void * ptr = ::operator new(sizeof(MyClass), std::nothrow);
new (ptr) MyClass();    // Размещающий
static_cast<MyClass*>(ptr)->~MyClass();
::operator delete (ptr);

Примеры:

Аналог ф-ции выше, той что использовала аллокатор. Здесь мы работаем на более низком уровне:

// перемещает элемент в новый участок памяти, создавая копию
// в неинициализированной памяти, а затем уничтожает оригинал.
template<typename T>
T * moveObjectToNewLocation2(T * object)
{
    // Выделяем новую память под 1 элемент
    void * newLocation = ::operator new(sizeof(T));
    // Копирование размещающий new Здесь используется к-тор копирования
    new (static_cast<T*>(newLocation)) T(*object);
    // Освобождаем память без вызова деструктора
    // Если этого не сделать - утечка!!!
    ::operator delete (object); 
    return static_cast<T*>(newLocation);
}
// выделяем память под 3 объекта
void * arrPtr = ::operator new[](sizeof(MyClass) * 3);

// когда понадобится конструируем в этой памяти реальные объекты

// от начала блока памяти
MyClass * ptr1 = new (arrPtr) MyClass(10); 
// со смещением в один блок размером с MyClass
MyClass * ptr2 = new (static_cast<MyClass*>(arrPtr) + sizeof(MyClass)) MyClass(20); 
// со смещением в два блока размером с MyClass
MyClass * ptr3 = new (static_cast<MyClass*>(arrPtr) + sizeof(MyClass) * 2) MyClass(30);

// Явно вызываем деструкторы
ptr1->~MyClass();
ptr2->~MyClass();
ptr3->~MyClass();

// освобождаем блок памяти:
::operator delete[](arrPtr);