Динамическое распределение массива объектов

Это вопрос новичков, но я не делал С++ в течение длительного времени, поэтому здесь идет...

У меня есть класс, который содержит динамически выделенный массив, скажем

class A
{
    int* myArray;
    A()
    {
        myArray = 0;
    }
    A(int size)
    {
        myArray = new int[size];
    }
    ~A()
    {
        // Note that as per MikeB helpful style critique, no need to check against 0.
        delete [] myArray;
    }
}

Но теперь я хочу создать динамически выделенный массив этих классов. Вот мой текущий код:

A* arrayOfAs = new A[5];
for (int i = 0; i < 5; ++i)
{
    arrayOfAs[i] = A(3);
}

Но это ужасно взрывается. Поскольку новый объект A, созданный (с вызовом A(3)), разрушается, когда итерация цикла for завершается, а это означает, что внутренний myArray этого экземпляра A получает delete [] -ed.

Итак, я думаю, что мой синтаксис должен быть ужасно неправильным? Я думаю, есть несколько исправлений, которые кажутся излишними, о которых я надеюсь избежать:

  • Создание конструктора копирования для A.
  • Используя vector<int> и vector<A>, поэтому мне не нужно беспокоиться обо всем этом.
  • Вместо того, чтобы arrayOfAs быть массивом объектов A, будь то массив указателей A*.

Я бы подумал, что это всего лишь новичок, где есть синтаксис, который фактически работает при попытке динамически распределять массив вещей, которые имеют внутреннее динамическое распределение.

(Кроме того, критику стиля оценили, поскольку это было некоторое время, так как я сделал С++.)

Обновление для будущих зрителей. Все ответы ниже очень полезны. Мартин принимается из-за кода примера и полезного "правила 4", но я действительно предлагаю прочитать их все. Некоторые из них являются хорошими, краткими утверждениями о том, что неправильно, и некоторые правильно указывают, как и почему vector - хороший способ пойти.

Ответы

Ответ 1

Для создания контейнеров вы, очевидно, хотите использовать один из стандартных контейнеров (например, std::vector). Но это прекрасный пример того, что вам нужно учитывать, когда ваш объект содержит указатели RAW.

Если у вашего объекта есть указатель RAW, вам необходимо запомнить правило 3 (теперь это правило 5 в С++ 11).

  • Конструктор
  • Destructor
  • Конструктор копирования
  • Оператор присваивания
  • Переместить конструктор (С++ 11)
  • Назначение перемещения (С++ 11)

Это потому, что если не определено, компилятор будет генерировать собственную версию этих методов (см. ниже). Сгенерированные версии компилятора не всегда полезны при работе с указателями RAW.

Конструктор копирования является твердым, чтобы получить правильное (это нетривиально, если вы хотите предоставить сильную гарантию исключения). Оператор присваивания может быть определен в терминах Copy Constructor, так как вы можете использовать внутреннюю и внутреннюю замену.

Подробные сведения об абсолютном минимуме для класса, содержащего указатель на массив целых чисел, см. ниже.

Зная, что это не так, чтобы получить правильное значение, вы должны использовать std::vector вместо указателя на массив целых чисел. Вектор прост в использовании (и расширяется) и охватывает все проблемы, связанные с исключениями. Сравните следующий класс с приведенным ниже определением A.

class A
{ 
    std::vector<int>   mArray;
    public:
        A(){}
        A(size_t s) :mArray(s)  {}
};

Глядя на вашу проблему:

A* arrayOfAs = new A[5];
for (int i = 0; i < 5; ++i)
{
    // As you surmised the problem is on this line.
    arrayOfAs[i] = A(3);

    // What is happening:
    // 1) A(3) Build your A object (fine)
    // 2) A::operator=(A const&) is called to assign the value
    //    onto the result of the array access. Because you did
    //    not define this operator the compiler generated one is
    //    used.
}

Оператор присваивания сгенерированного компилятором отлично подходит практически для всех ситуаций, но когда стрелки RAW находятся в игре, вам нужно обратить внимание. В вашем случае это вызывает проблему из-за проблемы мелкой копии. Вы получили два объекта, которые содержат указатели на одну и ту же часть памяти. Когда A (3) выходит из области видимости в конце цикла, он вызывает delete [] на своем указателе. Таким образом, другой объект (в массиве) теперь содержит указатель на память, которая была возвращена в систему.

Созданный компилятором конструктор копирования; копирует каждую переменную-член, используя конструктор копии членов. Для указателей это означает, что значение указателя копируется из исходного объекта в объект назначения (следовательно, мелкая копия).

Созданный компилятором оператор присваивания; копирует каждую переменную-член, используя оператор назначения членов. Для указателей это означает, что значение указателя копируется из исходного объекта в объект назначения (следовательно, мелкая копия).

Итак, минимальный для класса, который содержит указатель:

class A
{
    size_t     mSize;
    int*       mArray;
    public:
         // Simple constructor/destructor are obvious.
         A(size_t s = 0) {mSize=s;mArray = new int[mSize];}
        ~A()             {delete [] mArray;}

         // Copy constructor needs more work
         A(A const& copy)
         {
             mSize  = copy.mSize;
             mArray = new int[copy.mSize];

             // Don't need to worry about copying integers.
             // But if the object has a copy constructor then
             // it would also need to worry about throws from the copy constructor.
             std::copy(&copy.mArray[0],&copy.mArray[c.mSize],mArray);

         }

         // Define assignment operator in terms of the copy constructor
         // Modified: There is a slight twist to the copy swap idiom, that you can
         //           Remove the manual copy made by passing the rhs by value thus
         //           providing an implicit copy generated by the compiler.
         A& operator=(A rhs) // Pass by value (thus generating a copy)
         {
             rhs.swap(*this); // Now swap data with the copy.
                              // The rhs parameter will delete the array when it
                              // goes out of scope at the end of the function
             return *this;
         }
         void swap(A& s) noexcept
         {
             using std::swap;
             swap(this.mArray,s.mArray);
             swap(this.mSize ,s.mSize);
         }

         // C++11
         A(A&& src) noexcept
             : mSize(0)
             , mArray(NULL)
         {
             src.swap(*this);
         }
         A& operator=(A&& src) noexcept
         {
             src.swap(*this);     // You are moving the state of the src object
                                  // into this one. The state of the src object
                                  // after the move must be valid but indeterminate.
                                  //
                                  // The easiest way to do this is to swap the states
                                  // of the two objects.
                                  //
                                  // Note: Doing any operation on src after a move 
                                  // is risky (apart from destroy) until you put it 
                                  // into a specific state. Your object should have
                                  // appropriate methods for this.
                                  // 
                                  // Example: Assignment (operator = should work).
                                  //          std::vector() has clear() which sets
                                  //          a specific state without needing to
                                  //          know the current state.
             return *this;
         }   
 }

Ответ 2

Я бы рекомендовал использовать std::vector: что-то вроде

typedef std::vector<int> A;
typedef std::vector<A> AS;

Нет ничего плохого в незначительном переполнении STL, и вы сможете тратить больше времени на реализацию конкретных функций своего приложения, а не на изобретательность велосипеда.

Ответ 3

Конструктор вашего объекта A динамически выделяет другой объект и сохраняет указатель на этот динамически выделенный объект в необработанном указателе.

В этом случае вы должны определить свой собственный конструктор копирования, оператор присваивания и деструктор. Созданные компилятором будут работать некорректно. (Это является следствием "Закона Большой тройки": класс с любым деструктором, оператором присваивания, конструктором копирования обычно нужен все 3).

Вы определили свой собственный деструктор (и вы упоминали о создании конструктора копирования), но вам нужно определить оба других двух больших трех.

Альтернативой является сохранение указателя на ваш динамически выделенный int[] в другом объекте, который позаботится об этих вещах для вас. Что-то вроде vector<int> (как вы упомянули) или boost::shared_array<>.

Чтобы сварить это - чтобы в полной мере использовать RAII, вам следует избегать, если это возможно, необработанных указателей.

И поскольку вы попросили другие критики стиля, второстепенным является то, что когда вы удаляете необработанные указатели, вам не нужно проверять значение 0 перед вызовом delete - delete обрабатывает этот случай, ничего не делая, t должен загромождать ваш код с помощью проверок.

Ответ 4

  • Использовать массив или общий контейнер для объектов, только если они имеют конструкторы по умолчанию и копии.

  • Сохранять указатели иначе (или интеллектуальные указатели, но в этом случае могут встречаться некоторые проблемы).

PS: Всегда определяйте собственные конструкторы по умолчанию и копии, иначе будет использоваться автогенерация

Ответ 5

Вам нужен оператор присваивания, чтобы:

arrayOfAs[i] = A(3);

работает так, как должно.

Ответ 6

Почему бы не использовать метод setSize.

A* arrayOfAs = new A[5];
for (int i = 0; i < 5; ++i)
{
    arrayOfAs[i].SetSize(3);
}

Мне нравится "копия", но в этом случае конструктор по умолчанию на самом деле ничего не делает. SetSize может скопировать данные из исходного m_array (если он существует). Вам нужно будет сохранить размер массива в классе для этого.
ИЛИ
SetSize может удалить исходный файл m_array.

void SetSize(unsigned int p_newSize)
{
    //I don't care if it null because delete is smart enough to deal with that.
    delete myArray;
    myArray = new int[p_newSize];
    ASSERT(myArray);
}

Ответ 7

Используя функцию размещения оператора new, вы можете создать объект на месте и избежать копирования:

размещение (3): void * operator new (std:: size_t size, void * ptr) noexcept;

Просто возвращает ptr (никакого хранения не выделяется). Обратите внимание, что если функция вызывается новым выражением, будет выполнена правильная инициализация (для объектов класса это включает вызов его конструктора по умолчанию).

Я предлагаю следующее:

A* arrayOfAs = new A[5]; //Allocate a block of memory for 5 objects
for (int i = 0; i < 5; ++i)
{
    //Do not allocate memory,
    //initialize an object in memory address provided by the pointer
    new (&arrayOfAs[i]) A(3);
}