2008-11-01 6 views
51

Это своего рода начинающих вопрос, но я не сделал C++ в течение длительного времени, так что здесь идет ...Динамически выделение массив объектов

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

class A 
{ 
    int* myArray; 
    A() 
    { 
     myArray = 0; 
    } 
    A(int size) 
    { 
     myArray = new int[size]; 
    } 
    ~A() 
    { 
     // Note that as per MikeB's 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* указателей.

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

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

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

ответ

115

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

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

  • Конструктор
  • Destructor
  • Конструктор копирования
  • Оператор присваивания
  • Move Конструктор (C++ 11)
  • Move Assignment (C++ 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; 
     } 
} 
+4

Самый отличный ответ! Upvoted! Мне просто жаль, что я не смог бы это сделать снова! –

+0

Есть ли у вас некоторые статьи о проблеме с исключениями, о которой вы говорите? – shoosh

+0

Почему вы используете «raw»? Разумеется, это не аббревиатура ни для чего, а просто означает «сырой», как в немодифицированном, обычном, не в умном указателе, так и в другом виде обертки. – jalf

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

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

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

10

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

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

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

2

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

arrayOfAs[i] = A(3); 

работает как надо.

+0

На самом деле это использует оператор присваивания не конструктор копирования. Левая сторона уже полностью построена. –

+0

Упс, мозговой пердит. Спасибо, что поймал это. Исправлена. –

+0

К сожалению, нет. Поскольку как исходные A (3), так и arrayofAs [i] содержат член myArray, который указывает на ту же область в куче. Первый, выходящий из области действия, удалит объект. Второй, выходящий за рамки, также удалит его, что вызовет проблему. –

6

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

Для этого сценария вы должны определить свой собственный конструктор копирования, оператор присваивания и деструктор. Сгенерированные компилятором будут работать некорректно. (Это является следствием «Закона Большой тройки»: класс с любым деструктором, оператором присваивания, конструктором копирования вообще нуждается во всех 3).

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

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

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

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

+0

Так много действительно хороших ответов; Я действительно хочу принять большинство из них, включая вас, как «лучших». Большое спасибо. А также для критики стиля. – Domenic

+0

Его правило 4. Ему также нужен нормальный конструктор. Если вы не инициализируете указатели, тогда они имеют случайные значения. –

+0

@ Мартин - ты прав. Я всегда слышал это как «правило 3», поскольку конструктор воспринимается как «данный». Но я думаю, что явное включение его в правило - лучший способ сделать это. –

2

Почему нет метода SetSize.

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

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

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

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

размещения (3): * недействительным оператора новый (Std :: size_t размер, недействительным * 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); 
}