2008-11-01 11 views
51

Zuweisung Dieser Art von Anfänger Frage, aber ich habe nicht C++ in einer langen Zeit getan, so hier geht ...dynamisch ein Array von Objekten

Ich habe eine Klasse, die eine dynamisch zugewiesene Array enthält, sagen

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 zugeordnetes 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 explodiert schrecklich. Da das neue Objekt A erstellt (mit dem A(3) Aufruf) zerstört wird, wenn die for Schleife Iteration abgeschlossen ist, und dies bedeutet, dass die interne myArray dieser A Instanz delete [] -ed wird.

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

  • Erstellen eines Kopierkonstruktors für A.
  • Mit vector<int> und vector<A> so muss ich mich nicht um all dies kümmern.
  • Anstatt arrayOfAs ein Array von A Objekte zu sein, muss es ein Array von A* Zeigern sein.
  • Ich würde denken, das ist nur ein Anfänger Sache, wo es eine Syntax gibt, die tatsächlich funktioniert, wenn versucht wird, eine Reihe von Dingen dynamisch zuzuweisen, die interne dynamische Zuordnung haben.

    (Auch Stil Kritik geschätzt, da es schon eine Weile her, seit ich C++ tat.)

    Update 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 vector s ein guter Weg sind.

    Antwort

    115

    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, dann müssen Sie sich an die Regel 3 (jetzt die Regel 5 in C++ 11) erinnern.

    • Constructor
    • Destructor
    • Copykonstruktor
    • Zuweisungsoperator
    • Verschieben Constructor (C++ 11)
    • Verschieben Zuordnung (C++ 11)

    Diese weil, wenn nicht definiert, der Compiler eine eigene Version dieser Methoden erzeugt (siehe unten). Die vom Compiler erzeugten Versionen sind nicht immer nützlich, wenn es um RAW-Zeiger geht.

    Der Kopierkonstruktor ist der richtige, um 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 Idiom zum Kopieren und Tauschen intern verwenden können.

    Siehe unten für vollständige Details über das absolute Minimum für eine Klasse, die einen Zeiger auf ein Array von ganzen Zahlen enthält.

    Wissen, dass es nicht trivial ist, es richtig zu bekommen, sollten Sie std :: vector anstelle eines Zeigers auf ein Array von ganzen Zahlen 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) {} 
    }; 
    

    Mit Blick auf 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 Compiler erzeugt Zuweisungsoperator ist für fast alle Situationen in Ordnung, aber wenn RAW Zeiger im Spiel sind, müssen Sie achten. In Ihrem Fall verursacht es ein Problem wegen der seichten 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 den Speicher, der an das System zurückgegeben wurde.

    Der Compiler generierte Kopierkonstruktor; Kopiert jede Elementvariable mithilfe des Kopierkonstruktors für Mitglieder. Für Zeiger bedeutet dies nur, dass der Zeigerwert vom Quellobjekt zum Zielobjekt kopiert wird (daher flache Kopie).

    Der Compiler generiert Zuweisungsoperator; kopiert jede Mitgliedsvariable mit diesem Zuweisungsoperator für Mitglieder. Für Zeiger bedeutet dies nur, dass der Zeigerwert vom Quellobjekt zum Zielobjekt kopiert wird (daher flache Kopie).

    So 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; 
         } 
    } 
    
    +4

    Die beste Antwort! Upvoted! Ich wünschte nur, ich könnte das wieder auffrischen! –

    +0

    Haben Sie Artikel zu den Ausnahmen, auf die Sie sich beziehen? – shoosh

    +0

    Warum kapitalisieren Sie "roh"? Sicher, es ist keine Abkürzung für irgendetwas, sondern bedeutet einfach "roh" wie in unmodified, plain, nicht ein Smart-Pointer oder eine andere Art von Wrapper. – jalf

    4
    1. Verwenden Sie Array oder allgemeinen Container für Objekte nur, wenn sie Standard-und Kopierkonstruktoren haben.

    2. Speichern Zeiger sonst (oder Smart-Zeiger, aber möglicherweise einige Probleme in diesem Fall zu treffen).

    PS: Immer eigenen Standard definieren und Kopierkonstruktoren sonst automatisch generiert wird

    10

    verwendet wird, würde ich empfehlen, std :: vector: so etwas wie

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

    Es ist nichts falsch mit Der kleine Overkill von STL, und Sie können mehr Zeit damit verbringen, die spezifischen Funktionen Ihrer App zu implementieren, anstatt das Fahrrad neu zu erfinden.

    2

    Sie benötigen einen Zuweisungsoperator, so dass:

    arrayOfAs[i] = A(3); 
    

    funktioniert wie es sollte.

    +0

    Eigentlich ist diese verwendet den Zuweisungsoperator nicht der Kopierkonstruktor. Die linke Seite wurde bereits vollständig erstellt. –

    +0

    Hoppla, Hirnfurz. Danke, dass du das verstanden hast. Fest. –

    +0

    Leider nicht. Da sowohl das Original A (3) als auch das Array von As [i] das Element myArray enthalten, das auf denselben Bereich im Heap verweist. Der erste, der den Gültigkeitsbereich verlässt, löscht das Objekt. Der zweite, der den Gültigkeitsbereich verlässt, wird ebenfalls gelöscht, was das Problem verursacht. –

    6

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

    Für dieses Szenario müssen Sie müssen definieren Sie Ihre eigenen Kopie Konstruktor, Zuweisungsoperator und Destruktor. Die vom Compiler erzeugten werden nicht korrekt funktionieren. (Dies ist eine logische Folge des "Gesetzes der Großen Drei": Eine Klasse mit Destruktor, Zuweisungsoperator, Kopierkonstruktor benötigt in der Regel alle 3).

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

    Eine Alternative ist, den Zeiger auf Ihre dynamisch zugewiesene int[] in einem anderen Objekt zu speichern, das sich um diese Dinge kümmern wird. So etwas wie ein vector<int> (wie du erwähnt hast) oder ein boost::shared_array<>.

    Um dies zu verhindern - um RAII in vollem Umfang zu nutzen, sollten Sie vermeiden, rohe Zeiger so weit wie möglich zu behandeln.

    Und da man für einen anderen Stil Kritik gebeten, eine geringfügige ist, dass, wenn Sie rohe Zeiger löschen Sie müssen für 0 nicht überprüfen, bevor delete Aufruf - delete behandelt diesen Fall durch nichts zu tun, so dass Sie nicht haben Durcheinander Sie Code mit den Schecks.

    +0

    So viele wirklich gute Antworten; Ich möchte die meisten von ihnen, darunter auch deine, als "die Besten" akzeptieren. Vielen Dank. Und auch für die Stilkritik. – Domenic

    +0

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

    +0

    @Martin - du hast Recht. Ich habe es immer als "Regel von 3" gehört, da der Konstruktor als "gegeben" gilt. Aber ich denke, es ist ein besserer Weg, es in die Regel aufzunehmen. –

    2

    Warum nicht eine Methode setSize haben.

    Ich mag die "Kopie", aber in diesem Fall ist der Standard-Konstruktor nicht wirklich etwas zu tun. Die SetSize könnte die Daten aus dem ursprünglichen m_array kopieren (falls es existiert). Sie müssten die Größe des Arrays innerhalb der Klasse speichern, um dies zu tun.
    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); 
    } 
    
    2

    Mit der Platzierung Merkmale new Operator, können Sie das Objekt an Ort und Stelle erstellen und Kopieren vermeiden:

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

    Gibt einfach ptr zurück (kein Speicher ist zugewiesen). Beachten Sie jedoch, dass die richtige Initialisierung ausgeführt wird, wenn die Funktion von einem new-Ausdruck aufgerufen wird (für Klassenobjekte enthält dies den Aufruf des Standardkonstruktors).

    Ich schlage vor, die folgenden:

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