C++ Вызов деструкторов объектов и очистка памяти после realloc

Всем привет! Вопрос чисто теоретический: Есть ли в С++ аналог realloc из ванильного Си? Чтобы можно было менять размеры массива? И смежный вопрос: будут ли правильно(т.е. с вызовом деструкторов) удаляться объекты при вызове delete[] array,если перед этим было array=realloc(....) ? И если нет, то как решить эту проблему? P.S. я знаю,что есть волшебный std::vector, но меня очень интересует реализация без примочек из STL.


Ответы (2 шт):

Автор решения: Stanislav Volodarskiy

Есть ли в С++ аналог realloc из ванильного Си? Чтобы можно было менять размеры массива?

Аналога нет. Но в большинстве ситуаций realloc деградирует до malloc/memcpy/free. Выделяется новый блок памяти, содержимое старого блока копируется в его начало, старый блок памяти удаляется.

будут ли правильно(т.е. с вызовом деструкторов) удаляться объекты при вызове delete[] array,если перед этим было array=realloc(....) ? И если нет, то как решить эту проблему?

Эту проблему нельзя решить, когда она возникла. Если вы не знаете как был выделен кусок памяти, у вас нет способа его удалить или изменить его размер. С самого начала должна быть строгая дисциплина. Для каждого указателя вы знаете как он был выделен и применяете только соответствующие функции. Или calloc/malloc/realloc/free, или new[]/delete[] (или new/delete если речь не о массивах). Указатель полученный вызовом в стиле C может обрабатываться только в стиле C. Указатель полученный вызовом в стиле C++ может обрабатываться только в стиле C++.

я знаю,что есть волшебный std::vector, но меня очень интересует реализация без примочек из STL.

Он не волшебный. Он написан на C++ и хорошо показывает как из низкоуровнего управления памятью получить высокоуровневый безопасный интерфейс. И без дополнительных накладных расходов в большинстве ситуаций.

→ Ссылка
Автор решения: LShadow77

Чтобы понять, почему нельзя перемешивать "сишные" malloc()/realloc()/free() и "плюсовые" new()/delete()/new[]()/delete[](), надо понимать принципиальную разницу между этими двумя способами работы с динамической памятью. А разница эта заключается в том, что malloc()/realloc()/free() работают с голой памятью, в то время, как операторы new и delete имеют дело уже с объектами, для которых дополнительно вызываются цепочки конструкторов и деструкторов.

Когда создаётся динамический массив объектов, то оператор new[] делает следующее:

  1. Выделяет память под массив (для этого, как правило, вызывается обычная malloc()). Если память выделить не удалось, бросает исключение std::bad_alloc().
  2. Если объекты заданного типа имеют нетривиальный конструктор (отличный от того, что по умолчанию), то вызывает в цикле конструктор для каждого объекта в массиве.

Оператор delete[], соответственно, действует наоборот:

  1. Если объекты заданного типа имеют нетривиальный деструктор, то в обратном цикле (справа налево) вызывает деструктор для каждого объекта.
  2. Освобождает память (вызвав free()).

И вот тут должен возникнуть вопрос: а откуда оператор delete[] собственно узнаёт длину массива? Ведь в качестве аргумента этому оператору передаётся только указатель! А ответ прост: если существует нетривиальный деструктор, то оператор new[] выделяет память не только под массив, но и под его длину, определённым образом сохраняет эту длину в выделенную область и возвращает смещённый указатель! И это только одна из причин, почему нельзя вызывать free() и realloc() для указателя, который был получен с помощью new[] - потому что этот указатель может оказаться смещённым по отношению к тому адресу, который возвратила malloc() внутри оператора new[]!

И так: существует ли аналог realloc() для вариантов с new[] и delete[]? Ну например, какой-нибудь гипотетический оператор renew Type[new_length]. Увы, нет такого оператора. Можно ли реализовать нечто похожее без использования std::vector? Можно, но для этого придётся всё равно написать собственный аналог std::vector.

Ниже я набросал демку, реализующую шаблонный класс CDynamicArray, который имеет три основных метода: New(), ReNew() и Delete(), а также перегружает некоторые операторы, мимикрируя под обычный массив. ReNew(), как нетрудно догадаться, действует, как аналог realloc() Можно сказать, что прога демонстрирует принципы, по которым работает сам std::vector, так что думаю, полезно будет к ознакомлению. И да, может содержать ошибки, хоть я и отладил (если кто найдёт, просьба указать).

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <new>

//Шаблонный класс-велосипед, реализует динамический массив, похожий по поведению на std::vector
template <typename TData>
class CDynamicArray
{
    TData*  m_pArray;
    size_t  m_length;

    void _copy_array(CDynamicArray* pSrcArray)
    {
        m_length = pSrcArray->m_length;

        if (m_length)
        {
            TData* pCopy = (TData*)malloc(m_length*sizeof(TData));
            if (!pCopy) throw std::bad_alloc();
            for (TData *pDst=pCopy, *pSrc=pSrcArray->m_pArray, *pEnd = pCopy+m_length; pDst<pEnd; ++pDst,++pSrc)
            {
                //вызываем конструктор копирования (palacement new)
                new(pDst) TData(*pSrc);
            }
            m_pArray = pCopy;
            return;
        }

        m_pArray = nullptr;
    }

    void _delete()
    {
        TData* pDst = m_pArray + m_length;
        while (pDst > m_pArray) (--pDst)->~TData(); //вызываем деструктор напрямую
        free(m_pArray);                             //освобождаем память
    }

public:
    CDynamicArray(): m_pArray(nullptr), m_length(0)     {}
    template <typename... TArgs> CDynamicArray(size_t length, TArgs... args): m_length(0) {New(length,args...);}
    CDynamicArray(CDynamicArray& rSrcArray)             {_copy_array(&rSrcArray);}
    CDynamicArray& operator=(CDynamicArray& rSrcArray)  {_delete(); _copy_array(&rSrcArray); return *this;}
    ~CDynamicArray() {_delete();}

    template <typename... TArgs> void New(size_t length, TArgs... args)
    {
        if (m_length) _delete();

        if (length)
        {
            TData* pArray = (TData*)malloc(length*sizeof(TData));
            if (!pArray) throw std::bad_alloc();

            for (TData *pDst=pArray, *pEnd = pArray+length; pDst<pEnd; ++pDst)
            {
                //вызываем конструктор (palacement new)
                new(pDst) TData(args...);
            }

            m_pArray = pArray;
            m_length = length;
            return;
        }

        m_pArray = nullptr;
        m_length = 0;
    }

    template <typename... TArgs> void ReNew(size_t new_length, TArgs... args)
    {
        if (new_length != m_length)
        {
            TData* pNewArray = (TData*)malloc(new_length*sizeof(TData));
            if (!pNewArray) throw std::bad_alloc();

            size_t min_length = (new_length > m_length)? m_length: new_length;
            TData* pSrc = m_pArray;
            TData* pDst = pNewArray;
            TData* pDstEnd = pNewArray + min_length;

            //копируем объекты в новую область
            for (; pDst<pDstEnd; ++pDst,++pSrc) new(pDst) TData(*pSrc);  

            if (new_length > m_length)
            {
                //создаём новые объекты
                pDstEnd = pNewArray + new_length;
                for (; pDst<pDstEnd; ++pDst) new(pDst) TData(args...);
            }

            //удаляем старый массив
            _delete();

            m_pArray = pNewArray;
            m_length = new_length;
        }
    }

    void Delete()
    {
        _delete();
        m_pArray = nullptr;
        m_length = 0;
    }

    TData* GetArray()           {return m_pArray;}
    size_t GetLength()          {return m_length;}
    TData& operator[](int i)    {assert ((i>=0) && (i<m_length)); return m_pArray[i];}
    operator TData*()           {return m_pArray;}
    TData* operator+(int i)     {return m_pArray + i;}
};


//тестовый класс
class CTest
{
    int m_val;

public:
    CTest(int val): m_val(val)      {printf("Constructor for %d\n", m_val);}
    //CTest(CTest& rSrc): m_val(rSrc.m_val)  {printf("Copiing %d\n", m_val);}
    ~CTest()                        {printf("Destructor of %d\n", m_val);}

    int GetVal()                    {return m_val;}
};


int main()
{
    CDynamicArray<CTest> arr, arr2;

    arr.New(4, 44);
    printf("Array New:\t\t");
    for (int i=0; i<arr.GetLength(); ++i) printf("%d ", arr[i].GetVal());
    puts("\n");

    arr.ReNew(7, 77);
    printf("Array ReNew+:\t\t");
    for (CTest* p = arr; p<arr+arr.GetLength(); ++p) printf("%d ", p->GetVal());
    puts("\n");

    arr.ReNew(2, 0);
    printf("Array ReNew-:\t\t");
    for (int i=0; i<arr.GetLength(); ++i) printf("%d ", arr[i].GetVal());
    puts("\n");

    arr2 = arr;
    arr.Delete();
    printf("Array copy:\t\t");
    for (int i=0; i<arr2.GetLength(); ++i) printf("%d ", arr2[i].GetVal());
    puts("\n");

    arr2.Delete();
    return 0;
}

Хочу обратить внимание, что и здесь мы не можем использовать функцию realloc(), т.к. при переносе данных в новую область памяти, старая область становится неопределена и мы не можем вызвать деструкторы для старых копий объектов. И даже если в случае уменьшения длины массива не гарантируется, что не будет выполнен перенос памяти. Так что остаётся только одно: создавать новую область, копировать объекты из старой области, удалять старую область (с вызовом деструкторов).

→ Ссылка