Dynamisches Zuordnen eines Arrays von Objekten

Lesezeit: 10 Minuten

Dynamisches Zuordnen eines Arrays von Objekten
Dominik

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

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

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

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

Aber das explodiert fürchterlich. Denn das Neue A erstelltes Objekt (mit der A(3) Anruf) wird zerstört, wenn die for Schleifeniteration endet, und das bedeutet, dass die internal myArray davon A Instanz bekommt delete []-ed.

Also denke ich, dass meine Syntax schrecklich falsch sein muss? Ich denke, es gibt ein paar Fixes, die wie ein Overkill erscheinen, den ich hoffentlich vermeiden möchte:

  • Erstellen eines Kopierkonstruktors für A.
  • Verwenden vector<int> und vector<A> also muss ich mich um all das nicht kümmern.
  • Anstatt zu haben arrayOfAs ein Array von sein A Objekte, sei ein Array von A* Zeiger.

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 Zuweisung dynamisch zuzuweisen.

(Auch Stilkritik ist willkommen, da es schon eine Weile her ist, seit ich C++ gemacht habe.)

Update für zukünftige Zuschauer: Alle Antworten unten sind wirklich hilfreich. Martins wird wegen des Beispielcodes und der nützlichen „4er-Regel“ 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 sind ein guter Weg.

Zum Erstellen von Containern möchten Sie natürlich einen der Standardcontainer verwenden (z. B. einen 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 an die 3er-Regel erinnern (jetzt die 5er-Regel in C++11).

  • Konstrukteur
  • Destruktor
  • Konstruktor kopieren
  • Aufgabenverwalter
  • Move-Konstruktor (C++11)
  • Zuweisung verschieben (C++11)

Dies liegt daran, dass der Compiler, wenn er nicht definiert ist, seine eigene Version dieser Methoden generiert (siehe unten). Die vom Compiler generierten Versionen sind nicht immer nützlich, wenn es um RAW-Zeiger geht.

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

Unten finden Sie alle Details zum absoluten Minimum für eine Klasse, die einen Zeiger auf ein Array von Ganzzahlen enthält.

Da Sie wissen, dass es nicht trivial ist, es richtig zu machen, sollten Sie in Betracht ziehen, std::vector anstelle eines Zeigers auf ein Array von Ganzzahlen zu verwenden. Der Vektor ist einfach zu verwenden (und zu erweitern) und deckt alle Probleme ab, die mit Ausnahmen verbunden sind. 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)  {}
};

Betrachte dein 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 für fast alle Situationen in Ordnung, aber wenn RAW-Zeiger im Spiel sind, müssen Sie aufpassen. In Ihrem Fall verursacht es ein Problem wegen der seichte Kopie Problem. Sie haben am Ende zwei Objekte, die Zeiger auf denselben Speicher enthalten. Wenn A(3) am Ende der Schleife den Gültigkeitsbereich verlässt, ruft es delete auf [] auf seinem Zeiger. Somit enthält das andere Objekt (im Array) nun einen Zeiger auf den Speicher, der an das System zurückgegeben wurde.

Der vom Compiler generierte Kopierkonstruktor; kopiert jede Member-Variable mit diesem Member-Kopierkonstruktor. Für Zeiger bedeutet dies nur, dass der Zeigerwert vom Quellobjekt zum Zielobjekt kopiert wird (daher flache Kopie).

Der vom Compiler generierte Zuweisungsoperator; kopiert jede Member-Variable mit diesem Member-Zuweisungsoperator. 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;
         }   
 }

  • Haben Sie einige ähnliche Artikel über das Ausnahmeproblem, auf das Sie sich beziehen?

    – tschüss

    13. Dezember 2008 um 21:30 Uhr

  • Warum schreiben Sie “roh” groß? Sicherlich ist es keine Abkürzung für irgendetwas, sondern bedeutet einfach “roh” wie in unmodifiziert, einfach, kein Smart-Pointer oder eine andere Art von Wrapper.

    – jalf

    31. Dezember 2009 um 11:19 Uhr

  • @jalf Sie heißen “Schreckenszitate” 🙂

    – Josperry

    28. Januar 2012 um 21:19 Uhr

  • Warum gibt der Bewegungszuweisungsoperator nichts zurück?

    – Daniele

    7. Juli 2016 um 9:08 Uhr

  • @Daniele: Weil das ein Bug ist. Festsetzung.

    – Martin York

    8. Juli 2016 um 21:42 Uhr

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

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

Gegen den 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.

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

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

Sie haben Ihren eigenen Destruktor definiert (und Sie erwähnten das Erstellen eines Kopierkonstruktors), aber Sie müssen die beiden anderen der großen Drei definieren.

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

Um es auf den Punkt zu bringen – um RAII in vollem Umfang nutzen zu können, sollten Sie den Umgang mit rohen Zeigern so weit wie möglich vermeiden.

Und da Sie nach anderen Stilkritiken gefragt haben, ist eine kleine, dass Sie beim Löschen von Rohzeigern vor dem Aufrufen nicht nach 0 suchen müssen deletedelete behandelt diesen Fall, indem es nichts tut, damit Sie Ihren Code nicht mit den Überprüfungen überladen müssen.

  • So viele wirklich gute Antworten; Ich möchte wirklich die meisten von ihnen, einschließlich Ihrer, als die “Besten” akzeptieren. Danke sehr. Und auch für die Stilkritik.

    – Domenik

    1. November 2008 um 16:41 Uhr

  • Es ist die Regel von 4. Es braucht auch einen normalen Konstruktor. Wenn Sie die Zeiger nicht initialisieren, haben sie zufällige Werte.

    – Martin York

    1. November 2008 um 16:45 Uhr

  • @ Martin – du hast recht. Ich habe es immer als die “Regel von 3” gehört, da der Konstruktor als “gegeben” angesehen wird. Aber ich denke, es explizit in die Regel aufzunehmen, ist ein besserer Weg, dies zu tun.

    – Michael Burr

    1. November 2008 um 23:48 Uhr

  1. Verwenden Sie Arrays oder allgemeine Container nur dann für Objekte, wenn sie über Standard- und Kopierkonstruktoren verfügen.

  2. Speichern Sie Zeiger anderweitig (oder intelligente Zeiger, aber in diesem Fall können einige Probleme auftreten).

PS: Immer eigenen Default definieren und Konstruktoren kopieren, sonst werden automatisch generierte verwendet

Dynamisches Zuordnen eines Arrays von Objekten
Jim Buck

Sie benötigen einen Zuweisungsoperator, damit:

arrayOfAs[i] = A(3);

funktioniert wie es soll.

  • Tatsächlich verwendet dies den Zuweisungsoperator, nicht den Kopierkonstruktor. Die linke Seite ist bereits fertig aufgebaut.

    – Martin York

    1. November 2008 um 16:47 Uhr

  • Leider nicht. Da sowohl das ursprüngliche A(3) als auch das Array von As[i] enthalten das Mitglied myArray, das auf denselben Bereich auf dem Heap zeigt. Der erste, der den Gültigkeitsbereich verlässt, löscht das Objekt. Der zweite, der den Gültigkeitsbereich verlässt, löscht ihn ebenfalls, dies verursacht das Problem.

    – Martin York

    13. Dezember 2008 um 20:33 Uhr

1646956212 692 Dynamisches Zuordnen eines Arrays von Objekten
Martin York

Warum nicht eine setSize-Methode haben.

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 nicht wirklich etwas. Die SetSize könnte die Daten aus dem ursprünglichen m_array kopieren (falls vorhanden). Dazu müssten Sie die Größe des Arrays in der Klasse speichern.
ODER
Die 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);
}

  • Tatsächlich verwendet dies den Zuweisungsoperator, nicht den Kopierkonstruktor. Die linke Seite ist bereits fertig aufgebaut.

    – Martin York

    1. November 2008 um 16:47 Uhr

  • Leider nicht. Da sowohl das ursprüngliche A(3) als auch das Array von As[i] enthalten das Mitglied myArray, das auf denselben Bereich auf dem Heap zeigt. Der erste, der den Gültigkeitsbereich verlässt, löscht das Objekt. Der zweite, der den Gültigkeitsbereich verlässt, löscht ihn ebenfalls, dies verursacht das Problem.

    – Martin York

    13. Dezember 2008 um 20:33 Uhr

1646956212 624 Dynamisches Zuordnen eines Arrays von Objekten
Saman Bargi

Verwenden Sie die Platzierungsfunktion von new Operator können Sie das Objekt an Ort und 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 Speicherplatz zugewiesen). Beachten Sie jedoch, dass, wenn die Funktion von einem neuen Ausdruck aufgerufen wird, die richtige Initialisierung durchgeführt wird (für Klassenobjekte umfasst dies den Aufruf ihres Standardkonstruktors).

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

989310cookie-checkDynamisches Zuordnen eines Arrays von Objekten

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy