web-dev-qa-db-fra.com

Allocation dynamique d'un tableau d'objets

C'est une sorte de question pour les débutants, mais je n'ai pas fait de C++ depuis longtemps, alors voilà ...

J'ai une classe qui contient un tableau alloué dynamiquement, disons

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

Mais maintenant, je veux créer un tableau alloué dynamiquement de ces classes. Voici mon code actuel:

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

Mais cela explose terriblement. Parce que le nouvel objet A créé (avec l'appel A(3)) est détruit lorsque l'itération de la boucle for se termine, ce qui signifie que le myArray interne de cette instance de A obtient delete [] - éd.

Je pense donc que ma syntaxe doit être terriblement erronée? Je suppose qu'il y a quelques correctifs qui semblent exagérés, que j'espère éviter:

  • Création d'un constructeur de copie pour A.
  • En utilisant vector<int> Et vector<A>, Je n'ai donc pas à me soucier de tout cela.
  • Au lieu d'avoir arrayOfAs un tableau d'objets A, faites-en un tableau de pointeurs A*.

Je pense que ce n'est qu'une chose pour les débutants où il y a une syntaxe qui fonctionne réellement lorsque vous tentez d'allouer dynamiquement un tableau de choses qui ont une allocation dynamique interne.

(De plus, les critiques de style sont appréciées, car cela fait un moment que je n'ai pas fait C++.)

Mise à jour pour les futurs téléspectateurs: Toutes les réponses ci-dessous sont vraiment utiles. Martin est accepté en raison de l'exemple de code et de la "règle de 4" utile, mais je suggère vraiment de les lire tous. Certains sont de bons énoncés succincts de ce qui ne va pas, et certains indiquent correctement comment et pourquoi vector sont une bonne façon de procéder.

54
Domenic

Pour construire des conteneurs, vous voulez évidemment utiliser l'un des conteneurs standard (comme un std :: vector). Mais ceci est un parfait exemple des choses que vous devez considérer lorsque votre objet contient des pointeurs RAW.

Si votre objet a un pointeur RAW, vous devez vous rappeler la règle de 3 (maintenant la règle de 5 en C++ 11).

  • Constructeur
  • Destructeur
  • Copier le constructeur
  • Opérateur d'assignation
  • Déplacer le constructeur (C++ 11)
  • Déplacer l'affectation (C++ 11)

En effet, s'il n'est pas défini, le compilateur générera sa propre version de ces méthodes (voir ci-dessous). Les versions générées par le compilateur ne sont pas toujours utiles lorsqu'il s'agit de pointeurs RAW.

Le constructeur de copie est le plus difficile à corriger (ce n'est pas trivial si vous voulez fournir la garantie d'exception forte). L'opérateur d'affectation peut être défini en termes de constructeur de copie, car vous pouvez utiliser l'idiome de copie et d'échange en interne.

Voir ci-dessous pour plus de détails sur le minimum absolu pour une classe contenant un pointeur vers un tableau d'entiers.

Sachant qu'il n'est pas trivial de le faire correctement, vous devriez envisager d'utiliser std :: vector plutôt qu'un pointeur vers un tableau d'entiers. Le vecteur est facile à utiliser (et à étendre) et couvre tous les problèmes associés aux exceptions. Comparez la classe suivante avec la définition de A ci-dessous.

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

En regardant votre problème:

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

L'opérateur d'affectation généré par le compilateur convient à presque toutes les situations, mais lorsque des pointeurs RAW sont en jeu, vous devez faire attention. Dans votre cas, cela cause un problème en raison de la copie superficielle problème. Vous vous êtes retrouvé avec deux objets qui contiennent des pointeurs vers le même morceau de mémoire. Lorsque le A(3) sort de la portée à la fin de la boucle, il appelle delete [] sur son pointeur. Ainsi, l'autre objet (dans le tableau) contient maintenant un pointeur sur la mémoire qui a été renvoyé au système.

Le constructeur de copie généré par le compilateur; copie chaque variable membre en utilisant ce constructeur de copie membres. Pour les pointeurs, cela signifie simplement que la valeur du pointeur est copiée de l'objet source vers l'objet de destination (d'où une copie superficielle).

L'opérateur d'affectation généré par le compilateur; copie chaque variable membre en utilisant cet opérateur d'affectation de membres. Pour les pointeurs, cela signifie simplement que la valeur du pointeur est copiée de l'objet source vers l'objet de destination (d'où une copie superficielle).

Donc, le minimum pour une classe qui contient un pointeur:

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;
         }   
 }
119
Martin York

Je recommanderais d'utiliser std :: vector: quelque chose comme

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

Il n'y a rien de mal à la légère surpuissance de STL, et vous pourrez passer plus de temps à mettre en œuvre les fonctionnalités spécifiques de votre application au lieu de réinventer le vélo.

10
IMil

Le constructeur de votre objet A alloue dynamiquement un autre objet et stocke un pointeur vers cet objet alloué dynamiquement dans un pointeur brut.

Pour ce scénario, vous devez définir votre propre constructeur de copie, opérateur d'affectation et destructeur. Les générés par le compilateur ne fonctionneront pas correctement. (Ceci est un corollaire à la "Loi des Trois Grands": Une classe avec n'importe quel destructeur, opérateur d'affectation, constructeur de copie a généralement besoin des 3).

Vous avez défini votre propre destructeur (et vous avez mentionné la création d'un constructeur de copie), mais vous devez définir les deux autres 2 des trois grands.

Une alternative est de stocker le pointeur sur votre int[] Alloué dynamiquement dans un autre objet qui s'occupera de ces choses pour vous. Quelque chose comme un vector<int> (Comme vous l'avez mentionné) ou un boost::shared_array<>.

Pour résumer cela - pour tirer pleinement parti de RAII, vous devez éviter de traiter les pointeurs bruts dans la mesure du possible.

Et puisque vous avez demandé d'autres critiques de style, une mineure est que lorsque vous supprimez des pointeurs bruts, vous n'avez pas besoin de vérifier 0 avant d'appeler delete - delete gère ce cas en ne faisant rien vous n'avez pas à encombrer votre code avec les chèques.

6
Michael Burr
  1. Utilisez un tableau ou un conteneur commun pour les objets uniquement s'ils ont des constructeurs par défaut et copiés.

  2. Stockez les pointeurs autrement (ou les pointeurs intelligents, mais peut rencontrer certains problèmes dans ce cas).

PS: définissez toujours vos propres constructeurs par défaut et copiés sinon la génération automatique sera utilisée

4
noonex

Pourquoi ne pas avoir une méthode setSize.

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

J'aime la "copie" mais dans ce cas, le constructeur par défaut ne fait vraiment rien. SetSize peut copier les données hors du m_array d'origine (s'il existe). Vous devez stocker la taille du tableau dans la classe pour ce faire.
OU
Le SetSize pourrait supprimer le m_array d'origine.

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
baash05

Vous avez besoin d'un opérateur d'affectation pour que:

arrayOfAs[i] = A(3);

fonctionne comme il se doit.

2
Jim Buck

En utilisant la fonction de placement de l'opérateur new, vous pouvez créer l'objet en place et éviter de copier:

placement (3): opérateur void * new (std :: size_t size, void * ptr) noexcept;

Renvoie simplement ptr (aucun stockage n'est alloué). Notez cependant que si la fonction est appelée par une nouvelle expression, l'initialisation correcte sera effectuée (pour les objets de classe, cela inclut l'appel à son constructeur par défaut).

Je suggère ce qui suit:

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);
}
1
Saman Barghi