2008-11-01 23 views
51

Ésta es una pregunta un poco principiantes, pero no lo han hecho en C++ en mucho tiempo, así que aquí va ...asignar dinámicamente una matriz de objetos

tengo una clase que contiene una matriz asignada dinámicamente, diga

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; 
    } 
} 

Pero ahora quiero crear una matriz dinámicamente asignada de estas clases. Aquí está mi código actual:

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

Pero esto explota terriblemente. Porque el nuevo objeto A creado (con la llamada A(3)) se destruye cuando finaliza la iteración de bucle for, y esto significa que el interno de esa instancia A obtiene delete [] -ed.

¿Entonces creo que mi sintaxis debe ser terriblemente incorrecta? Supongo que hay algunos arreglos que parecen una exageración, que estoy con la esperanza de evitar:

  • Creación de un constructor de copia para A.
  • Usando vector<int> y vector<A> así que no tengo que preocuparme de todo esto.
  • En lugar de tener arrayOfAs ser una matriz de objetos A, que sea una matriz de A* punteros.

Creo que esto es sólo una cuestión de principiantes donde hay una sintaxis que realmente funciona cuando se intenta asignar dinámicamente una serie de elementos que tienen una asignación dinámica interna.

(También, las críticas de estilo apreciado, ya que ha pasado un tiempo desde que hice C++.)

Actualización para futuros espectadores: Todas las respuestas a continuación son realmente útiles. Martin es aceptado por el código de ejemplo y la útil "regla de 4", pero realmente sugiero leerlos todos. Algunas son buenas, concisas declaraciones de lo que está mal, y algunas señalan correctamente cómo y por qué vector s son un buen camino a seguir.

Respuesta

115

Para construir contenedores, obviamente desea utilizar uno de los contenedores estándar (como std :: vector). Pero este es un ejemplo perfecto de las cosas que debe tener en cuenta cuando su objeto contiene punteros RAW.

Si su objeto tiene un puntero sin formato, debe recordar la regla de 3 (ahora la regla de 5 en C++ 11).

  • Constructor
  • Destructor
  • constructor de copia
  • operador de asignación
  • Mover Constructor (C++ 11)
  • Asignación Mover (C++ 11)

Este es porque si no se define, el compilador generará su propia versión de estos métodos (ver a continuación). Las versiones generadas por el compilador no siempre son útiles cuando se trata de punteros RAW.

El constructor de copias es el más difícil de corregir (no es trivial si desea proporcionar una fuerte garantía de excepción). El operador de Asignación se puede definir en términos de Copy Constructor ya que puede usar la copia y el intercambio de idioma internamente.

Consulte a continuación para obtener detalles completos sobre el mínimo absoluto de una clase que contiene un puntero a una matriz de enteros.

Sabiendo que no es trivial hacerlo correctamente, debería considerar el uso de std :: vector en lugar de un puntero a una matriz de enteros.El vector es fácil de usar (y expandir) y cubre todos los problemas asociados con las excepciones. Compare la siguiente clase con la definición de A a continuación.

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

Mirando a su problema:

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. 
} 

El compilador operador de asignación generada está muy bien para casi todas las situaciones, pero cuando punteros RAW están en juego es necesario prestar atención. En su caso, está causando un problema debido al problema de copia superficial. Terminaste con dos objetos que contienen punteros en la misma pieza de memoria. Cuando el A (3) sale del ámbito al final del ciclo, llama a delete [] en su puntero. Por lo tanto, el otro objeto (en la matriz) ahora contiene un puntero a la memoria que ha sido devuelto al sistema.

El compilador generó el constructor de copia; copia cada variable miembro usando el constructor de copia de miembros. Para los punteros esto solo significa que el valor del puntero se copia desde el objeto fuente al objeto de destino (por lo tanto, copia poco profunda).

El operador de asignación generada por el compilador; copia cada variable miembro mediante el uso de ese operador de asignación de miembros. Para los punteros esto solo significa que el valor del puntero se copia desde el objeto fuente al objeto de destino (por lo tanto, copia poco profunda).

Así que el mínimo para una clase que contiene un puntero:

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

¡Excelente respuesta! ¡Voto ascendente! ¡Ojalá pudiera volver a votar esto! –

+0

¿Te gustan algunos artículos sobre el problema de excepciones al que te refieres? – shoosh

+0

¿Por qué capitaliza "en bruto"? Seguramente, no es una abreviatura para nada, sino que simplemente significa "en bruto" como sin modificar, simple, no es un puntero inteligente u otro tipo de envoltorio. – jalf

4
  1. Utilice una matriz o un contenedor común para objetos solo si tienen constructores por defecto y de copia.

  2. De lo contrario, guarde los punteros (o punteros inteligentes, pero es posible que se produzcan algunos problemas en este caso).

PD: Siempre definir propio defecto y copiar los constructores de lo contrario generados se prevé el uso

10

me gustaría recomendar el uso std :: vector: algo así como

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

No hay nada malo con la ligera exageración de STL, y podrá dedicar más tiempo a implementar las características específicas de su aplicación en lugar de reinventar la bicicleta.

2

Hace falta un operador de asignación de forma que:

arrayOfAs[i] = A(3); 

funciona como debería.

+0

En realidad, este utiliza el operador de asignación no es el constructor de copia. El lado izquierdo ya ha sido completamente construido. –

+0

Oops, pedo cerebro. Gracias por atrapar eso. Fijo. –

+0

Desafortunadamente no. Porque tanto el A (3) original como la matriz de As [i] contienen el miembro myArray que apunta a la misma área en el montón. El primero en salir del alcance eliminará el objeto. El segundo para salir del alcance también lo eliminará, esto causa el problema. –

6

El constructor de su objeto A asigna otro objeto dinámicamente y almacena un puntero a ese objeto dinámicamente asignado en un puntero sin formato.

Para ese escenario, usted debe definir su propio constructor de copias, operador de asignación y destructor. Los generados por el compilador no funcionarán correctamente. (Esto es un corolario de la "Ley de los Tres Grandes": una clase con cualquiera de destructor, operador de asignación, constructor de copia generalmente necesita todos 3).

Ha definido su propio destructor (y mencionó la creación de un constructor de copia), pero necesita definir los otros 2 de los tres grandes.

Una alternativa es almacenar el puntero a su int[] asignado dinámicamente en algún otro objeto que se encargue de estas cosas por usted. Algo así como vector<int> (como mencionó) o boost::shared_array<>.

Para reducir esto: para aprovechar RAII en toda su extensión, debe evitar el uso de punteros sin procesar en la medida de lo posible.

Y como usted solicitó otras críticas de estilo, una menor es que cuando borre punteros sin procesar no necesita verificar 0 antes de llamar al delete - delete maneja ese caso haciendo nada para no tener que desordenar tu código con los cheques.

+0

Tantas respuestas realmente buenas; Realmente quiero aceptar la mayoría de ellos, incluido el tuyo, como el "mejor". Muchas gracias. Y también para la crítica de estilo. – Domenic

+0

Es la regla de 4. También necesita un constructor normal. Si no inicializa los punteros, entonces tienen valores aleatorios. –

+0

@Martin - tienes razón. Siempre lo escuché como la "regla de 3" ya que el constructor se toma como "dado". Pero creo que incluirla explícitamente en la regla es una mejor manera de hacerlo. –

2

¿Por qué no tiene un método setSize.

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

Me gusta la "copia" pero en este caso el constructor predeterminado no está realmente haciendo nada. SetSize podría copiar los datos del m_array original (si existe). Debería almacenar el tamaño de la matriz dentro de la clase para hacer eso.
O
El SetSize podría eliminar el m_array original.

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

Mediante la función de colocación de new operador, se puede crear el objeto en su lugar y evitar el copiado:

colocación (3): * nuevo operador (std :: size_t tamaño nulo, void * ptr) no exceptuado;

Simplemente devuelve ptr (no se asigna espacio de almacenamiento). Observe que, si la función es invocada por una expresión nueva, se realizará la inicialización adecuada (para los objetos de clase, esto incluye llamar a su constructor predeterminado).

que sugieren lo siguiente:

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); 
} 
Cuestiones relacionadas