webentwicklung-frage-antwort-db.com.de

Dynamisches Zuweisen eines Arrays von Objekten

Dies ist eine Art Anfängerfrage, aber ich habe C++ schon lange nicht mehr gemacht, also geht es weiter ...

Ich habe eine Klasse, die beispielsweise ein dynamisch zugewiesenes Array enthält

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

Jetzt möchte ich ein dynamisch zugewiesenes Array dieser Klassen erstellen. Hier ist mein aktueller Code:

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

Aber das bläst furchtbar auf. Weil das neue A -Objekt, das (mit dem A(3) -Aufruf) erstellt wurde, zerstört wird, wenn die Iteration der for -Schleife abgeschlossen ist. Dies bedeutet, dass das interne myArray von Diese A -Instanz erhält delete [] - ed.

Also denke ich, meine Syntax muss fürchterlich falsch sein? Ich denke, es gibt ein paar Korrekturen, die wie ein Overkill wirken und die ich hoffentlich vermeiden möchte:

  • Erstellen eines Kopierkonstruktors für A.
  • Wenn ich vector<int> Und vector<A> Verwende, muss ich mir darüber keine Gedanken machen.
  • Anstatt dass arrayOfAs ein Array von A Objekten ist, muss es ein Array von A* Zeigern sein.

Ich würde denken, dass dies nur eine Anfängersache ist, bei der es eine Syntax gibt, die tatsächlich funktioniert, wenn versucht wird, ein Array von Dingen mit interner dynamischer Zuordnung dynamisch zuzuweisen.

(Auch Stilkritik wird geschätzt, da C++ schon eine Weile her ist.)

pdate für zukünftige Zuschauer: Alle Antworten unten sind wirklich hilfreich. Martins wird wegen des Beispielcodes und der nützlichen "Regel von 4" akzeptiert, aber ich empfehle wirklich, sie alle zu lesen. Einige sind gute, prägnante Aussagen darüber, was falsch ist, und einige weisen richtig darauf hin, wie und warum vectors ein guter Weg sind.

54
Domenic

Zum Erstellen von Containern möchten Sie offensichtlich einen der Standardcontainer verwenden (z. B. std :: vector). Dies ist jedoch ein perfektes Beispiel für die Dinge, die Sie berücksichtigen müssen, wenn Ihr Objekt RAW-Zeiger enthält.

Wenn Ihr Objekt einen RAW-Zeiger hat, müssen Sie sich die Regel 3 merken (jetzt die Regel 5 in C++ 11).

  • Konstrukteur
  • Zerstörer
  • Konstruktor kopieren
  • Aufgabenverwalter
  • Konstruktor verschieben (C++ 11)
  • Zuweisung verschieben (C++ 11)

Dies liegt daran, dass der Compiler, wenn er nicht definiert ist, eine eigene Version dieser Methoden generiert (siehe unten). Die vom Compiler generierten Versionen sind beim Umgang mit RAW-Zeigern nicht immer nützlich.

Der Kopierkonstruktor ist schwer zu korrigieren (es ist nicht trivial, wenn Sie eine starke Ausnahmegarantie bieten möchten). Der Zuweisungsoperator kann in Bezug auf den Kopierkonstruktor definiert werden, da Sie die Kopier- und Auslagerungssprache intern verwenden können.

Im Folgenden finden Sie ausführliche Informationen zum absoluten Minimum einer Klasse, die einen Zeiger auf ein Array von Ganzzahlen enthält.

Wenn Sie wissen, dass es nicht trivial ist, das Ganze zu korrigieren, sollten Sie erwägen, std :: vector anstelle eines Zeigers auf ein Array von Ganzzahlen zu verwenden. Der Vektor ist einfach zu verwenden (und zu erweitern) und deckt alle mit Ausnahmen verbundenen Probleme ab. Vergleichen Sie die folgende Klasse mit der Definition von A unten.

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

Betrachten Sie Ihr Problem:

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

Der vom Compiler generierte Zuweisungsoperator ist in fast allen Situationen in Ordnung, aber wenn RAW-Zeiger im Spiel sind, müssen Sie aufpassen. In deinem Fall verursacht es ein Problem wegen des flache Kopie Problem. Sie haben zwei Objekte gefunden, die Zeiger auf dasselbe Speicherelement enthalten. Wenn das A(3) am Ende der Schleife den Gültigkeitsbereich verlässt, ruft es delete [] auf seinem Zeiger auf. Daher enthält das andere Objekt (im Array) jetzt einen Zeiger auf diesen Speicher wurde an das System zurückgegeben.

Der Compiler hat einen Kopierkonstruktor generiert; kopiert jede Mitgliedsvariable mit dem Kopierkonstruktor dieses Mitglieds. Für Zeiger bedeutet dies nur, dass der Zeigerwert vom Quellobjekt zum Zielobjekt kopiert wird (daher flache Kopie).

Der Compiler hat einen Zuweisungsoperator generiert; kopiert jede Mitgliedsvariable unter Verwendung dieses Mitgliedszuweisungsoperators. Für Zeiger bedeutet dies nur, dass der Zeigerwert vom Quellobjekt zum Zielobjekt kopiert wird (daher flache Kopie).

Das Minimum für eine Klasse, die einen Zeiger enthält:

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

Ich würde empfehlen, std :: vector: so etwas wie

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

An dem leichten Overkill von STL ist nichts auszusetzen, und Sie können mehr Zeit damit verbringen, die spezifischen Funktionen Ihrer App zu implementieren, anstatt das Fahrrad neu zu erfinden.

10
IMil

Der Konstruktor Ihres A-Objekts weist ein anderes Objekt dynamisch zu und speichert einen Zeiger auf dieses dynamisch zugewiesene Objekt in einem Rohzeiger.

Für dieses Szenario definieren Sie muss Ihren eigenen Kopierkonstruktor, Zuweisungsoperator und Destruktor. Die vom Compiler generierten funktionieren nicht richtig. (Dies ist eine Folge des "Gesetzes der Großen Drei": Eine Klasse mit einem Destruktor, Zuweisungsoperator oder Kopierkonstruktor benötigt im Allgemeinen alle 3).

Sie haben Ihren eigenen Destruktor definiert (und Sie haben erwähnt, dass Sie einen Kopierkonstruktor erstellen), aber Sie müssen beide anderen 2 der großen drei definieren.

Eine Alternative besteht darin, den Zeiger auf Ihren dynamisch zugewiesenen int[] In einem anderen Objekt zu speichern, das diese Dinge für Sie erledigt. So etwas wie ein vector<int> (Wie du erwähnt hast) oder ein boost::shared_array<>.

Um dies zu reduzieren - um RAII in vollem Umfang zu nutzen, sollten Sie den Umgang mit rohen Zeigern so weit wie möglich vermeiden.

Und da Sie nach anderen Stilkritiken gefragt haben, müssen Sie beim Löschen von unformatierten Zeigern nicht nach 0 suchen, bevor Sie delete - delete aufrufen, ohne dies zu tun Sie müssen Ihren Code nicht mit den Schecks überladen.

6
Michael Burr
  1. Verwenden Sie nur dann ein Array oder einen allgemeinen Container für Objekte, wenn diese Standard- und Kopierkonstruktoren haben.

  2. Andernfalls Zeiger speichern (oder intelligente Zeiger, in diesem Fall können jedoch einige Probleme auftreten).

PS: Definieren Sie immer eigene Standard- und Kopierkonstruktoren, da sonst automatisch generierte verwendet werden

4
noonex

Warum nicht eine setSize-Methode?.

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

Ich mag die "Kopie", aber in diesem Fall macht der Standardkonstruktor eigentlich nichts. SetSize könnte die Daten aus dem ursprünglichen m_array kopieren (falls vorhanden). Dazu müsste die Größe des Arrays in der Klasse gespeichert werden.
ODER
Das SetSize könnte das ursprüngliche m_array löschen.

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

Sie benötigen einen Zuweisungsoperator, damit:

arrayOfAs[i] = A(3);

funktioniert wie es sollte.

2
Jim Buck

Mit der Platzierungsfunktion des Operators new können Sie das Objekt an der richtigen Stelle erstellen und das Kopieren vermeiden:

platzierung (3): void * operator new (std :: size_t size, void * ptr) noexcept;

Gibt einfach ptr zurück (es wird kein Speicher zugewiesen). Beachten Sie jedoch, dass beim Aufrufen der Funktion durch einen neuen Ausdruck die richtige Initialisierung ausgeführt wird (für Klassenobjekte schließt dies den Aufruf des Standardkonstruktors ein).

Ich schlage folgendes vor:

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