Forum: PC-Programmierung C++ heap Speicher über Destruktor löschen


von Christian (Gast)


Lesenswert?

Hallo zusammen.

Ich habe sagen wir eine Klasse Matrix, deren Konstruktor Speicher für 
eine matrix mit Länge*Breite erzeugt.Beim Beenden wird der Destruktor 
aufgerufen und löscht mir diesen Speicher.Soweit so gut.
Wenn ich nun in main.cpp 2 Instanzen erzeuge und zuweise 
"Klasse1=Klasse2"
dann wird der Destruktor ja 2 mal aufgerufen, löscht aber nur einen 
Speicher(aufgrund der Zuweisung). Der andere Speicher wird vom Heap 
nicht mehr gelöscht.
Meine Frage ist nun, was es in C++ für Möglichkeiten gibt, soetwas zu 
verhindern.(Mir fällt nichts ein.)
Danke.

Der Code:
1
//in header file:
2
class Matrix
3
{
4
protected:  
5
        ...
6
  int * m_values;  
7
8
public:
9
        Matrix( int n_Rows = 16, int n_Columns = 16 )
10
        {
11
             m_values = new _T [n_Rows*n_Columns];
12
        }
13
  ~Matrix( )
14
        {
15
       delete [] m_values;
16
  }
17
}
18
//in main.cpp
19
//ruft Konstruktor auf und reserviert heap1
20
Matrix cMatrix1;
21
//ruft Konstruktor auf und reserviert heap2
22
Matrix cMatrix3;
23
24
cMatrix3 = cMatrix1;
25
return 0;//von main.cpp
26
////ruft Destruktor auf und löscht nicht heap2
27
}

: Bearbeitet durch User
von Peter II (Gast)


Lesenswert?

Christian schrieb:
> Meine Frage ist nun, was es in C++ für Möglichkeiten gibt, soetwas zu
> verhindern.(Mir fällt nichts ein.)

du muss dafür sorgen, das beim Kopieren von Objekte ein neuer Zeiger 
angelegt wird.

Dafür muss du den Zuweisungs und Kopie Operator überschreiben. Dann 
funktioniert auch das freigeben vom Speicher.

von Tom K. (ez81)


Lesenswert?

Oder statt der manuellen Speicherverwaltung einen std::vector<int> 
nehmen.


http://en.wikipedia.org/wiki/Rule_of_three_(C%2B%2B_programming)

von Oliver S. (oliverso)


Lesenswert?

Christian schrieb:
> Wenn ich nun in main.cpp 2 Instanzen erzeuge und zuweise
> "Klasse1=Klasse2"
> dann wird

dir der Compiler erst einmal erzählen, daß er dafür einen von dir 
beigstellten Copy-Operator benötigt, weil er den default-Operator 
vorsichtshalber gelöscht hat. Beim gcc klingt das so:

>C:\pocketcpp\VectorTest\Matrix.cpp:27:11: error: use of deleted function 
>'Matrix& Matrix::operator=(const Matrix&)'
>  cMatrix1 = cMatrix3;
           ^
>C:\pocketcpp\VectorTest\Matrix.cpp:4:7: note: 'Matrix& Matrix::operator=>
>(const Matrix&)' is implicitly deleted because the default definition would be 
>ill-formed:

Der Rest liegt dann in deiner Hand.

: Bearbeitet durch User
von Christian (Gast)


Lesenswert?

Merkwürdig, VisualStudio Compiler lässt es aber mit sich machen, noch 
nicht mal mit Warnung. Es erscheint nur eine "Debug assertion failed" 
Meldung.

Gruss
Christian

von Peter II (Gast)


Lesenswert?

Christian schrieb:
> Merkwürdig, VisualStudio Compiler lässt es aber mit sich machen, noch
> nicht mal mit Warnung. Es erscheint nur eine "Debug assertion failed"
> Meldung.

warum soll es das nicht machen? Es ist ja kein Fehler vorhanden.

von Karl H. (kbuchegg)


Lesenswert?

Christian schrieb:
> Merkwürdig, VisualStudio Compiler lässt es aber mit sich machen, noch
> nicht mal mit Warnung.

Das ist ein Service vom gcc, das er das macht, indem er den/die 
Konstruktor/en analysiert.

Das machen längst nicht alle Compiler. Als Programmierer muss man eben 
wissen, was man tut.

Konkret ist das Vorgehen als die "Rule of 3" bekannt:

Benötigt eine Klasse eine der 3 speziellen Member Funktionen
* Destruktor
* Copy Konstruktor
* Zuweisungsoperator
 in einer programmierer-definierten Version, dann benötigt sie sehr 
wahrscheinlich alle 3 programmierer-definiert.

Die Rule of 3 kommt daher, weil der Compiler diese 3 Member Funktionen 
selbst erzeugt, wenn der Programmierer keine schreibt. Ist es für die 
Klasse notwendig, eine der 3 Member Funktionen selbst zu schreiben, weil 
die automatisch generierte falsch wäre, dann sind die anderen beiden 
meistens ebenfalls falsch.

Edit:
Sehe gerade, dass Tom K. die 'Rule of Three' schon verlinkt hatte.

: Bearbeitet durch User
von Dr. Sommer (Gast)


Lesenswert?

Du musst den Copy-Constructor, Copy-Assignment-operator, 
Move-Constructor und Move-Assignment-Operator überschreiben:
1
#include <cstddef>
2
#include <algorithm>
3
4
//in header file:
5
class Matrix {
6
  public:
7
    using T = int;  // Der Bezeichner _T darf nicht verwendet werden (reserviert für die Standard-Library); daher verwenden wir nur "T".
8
  protected:
9
10
    size_t m_nRows, m_nCols;
11
    T * m_values;
12
  public:
13
    Matrix (size_t n_Rows = 16, size_t n_Columns = 16) :
14
        m_nRows (n_Rows), m_nCols (n_Columns), m_values (new T [m_nRows * m_nCols]) {
15
    }
16
    ~Matrix () {
17
      if (m_values != nullptr)
18
        delete [] m_values;
19
    }
20
    // Kopie der Matrix anlegen - neuen Speicher anfordern
21
    Matrix (const Matrix& src) :
22
        m_nRows (src.m_nRows), m_nCols (src.m_nCols), m_values (new T [m_nRows * m_nCols]) {
23
      std::copy (src.m_values, src.m_values + (m_nRows * m_nCols), m_values);
24
    }
25
    // Move-Constructor
26
    Matrix (Matrix&& src) :
27
      m_nRows (src.m_nRows), m_nCols (src.m_nCols), m_values (src.m_values) {
28
      src.m_values = nullptr;
29
    }
30
    // Kopie-Zuweisung
31
    Matrix& operator = (const Matrix& src) {
32
      if (m_values != nullptr) {
33
        delete [] m_values;
34
        m_values = nullptr; // <- wegen möglicher Exceptions im "new"
35
      }
36
      m_nRows = src.m_nRows;
37
      m_nCols = src.m_nCols;
38
      m_values = new T [m_nRows * m_nCols];
39
      std::copy (src.m_values, src.m_values + (m_nRows * m_nCols), m_values);
40
      return *this;
41
    }
42
    // Move Assignment
43
    Matrix& operator = (Matrix&& src) {
44
      if (m_values != nullptr)
45
        delete [] m_values;
46
      m_nRows = src.m_nRows;
47
      m_nCols = src.m_nCols;
48
      m_values = src.m_values;
49
      src.m_values = nullptr;
50
      return *this;
51
    }
52
};
53
54
int main () {
55
//ruft Konstruktor auf und reserviert heap1
56
  Matrix cMatrix1;
57
//ruft Konstruktor auf und reserviert heap2
58
  Matrix cMatrix3;
59
  
60
// Kopie anlegen (langsam)
61
  cMatrix3 = cMatrix1;
62
  
63
// Inhalt von cMatrix3 nach cMatrix4 verschieben; cMatrix3 ist danach leer (schnell)
64
  Matrix cMatrix4 (std::move (cMatrix3));
65
  
66
  return 0; //von main.cpp
67
////ruft Destruktor auf und löscht nicht heap2
68
}
Kürzer und sicherer gehts mit std::unique_ptr:
1
#include <cstddef>
2
#include <algorithm>
3
#include <memory>
4
5
//in header file:
6
class Matrix {
7
  public:
8
    using T = int;  // Der Bezeichner _T darf nicht verwendet werden (reserviert für die Standard-Library); daher verwenden wir nur "T".
9
  protected:
10
11
    size_t m_nRows, m_nCols;
12
    std::unique_ptr<T []> m_values;
13
  public:
14
    Matrix (size_t n_Rows = 16, size_t n_Columns = 16) :
15
        m_nRows (n_Rows), m_nCols (n_Columns), m_values (new T [m_nRows * m_nCols]) {
16
    }
17
    ~Matrix () = default;
18
    // Kopie der Matrix anlegen - neuen Speicher anfordern
19
    Matrix (const Matrix& src) :
20
        m_nRows (src.m_nRows), m_nCols (src.m_nCols), m_values (new T [m_nRows * m_nCols]) {
21
      std::copy (src.m_values.get (), src.m_values.get () + (m_nRows * m_nCols), m_values.get ());
22
    }
23
    // Move-Constructor
24
    Matrix (Matrix&& src) = default;
25
    // Kopie-Zuweisung
26
    Matrix& operator = (const Matrix& src) {
27
      m_nRows = src.m_nRows;
28
      m_nCols = src.m_nCols;
29
      m_values = std::unique_ptr<T[]> (new T [m_nRows * m_nCols]);
30
      std::copy (src.m_values.get (), src.m_values.get () + (m_nRows * m_nCols), m_values.get ());
31
      return *this;
32
    }
33
    // Move Assignment
34
    Matrix& operator = (Matrix&& src) = default;
35
};
36
37
int main () {
38
//ruft Konstruktor auf und reserviert heap1
39
  Matrix cMatrix1;
40
//ruft Konstruktor auf und reserviert heap2
41
  Matrix cMatrix3;
42
  
43
// Kopie anlegen (langsam)
44
  cMatrix3 = cMatrix1;
45
  
46
// Inhalt von cMatrix3 nach cMatrix4 verschieben; cMatrix3 ist danach leer (schnell)
47
  Matrix cMatrix4 (std::move (cMatrix3));
48
  
49
  return 0; //von main.cpp
50
////ruft Destruktor auf und löscht nicht heap2
51
}
Noch besser gehts indem man std::vector verwendet:
1
#include <vector>
2
#include <cstddef>
3
4
using T = int;
5
class Matrix : public std::vector<T> {
6
  public:
7
    Matrix (size_t n_Rows = 16, size_t n_Columns = 16) : std::vector<T> (n_Rows * n_Columns) {
8
      
9
    }
10
};
11
12
int main () {
13
  //ruft Konstruktor auf und reserviert heap1
14
    Matrix cMatrix1;
15
  //ruft Konstruktor auf und reserviert heap2
16
    Matrix cMatrix3;
17
    
18
  // Kopie anlegen (langsam)
19
    cMatrix3 = cMatrix1;
20
    
21
  // Inhalt von cMatrix3 nach cMatrix4 verschieben; cMatrix3 ist danach leer (schnell)
22
    Matrix cMatrix4 (std::move (cMatrix3));
23
24
}

Außerdem gibts in C++ keinen heap; nur Free Storage und Automatic 
Storage. Die Variablen cMatrix1,3,4 und m_nCols, m_nRows, m_values sind 
alle im Automatic Storage (sie werden automatisch gelöscht), und der 
Ziel-Speicher von m_values ist im Free Storage (und muss daher manuell 
mit delete gelöscht werden).

Wenn es auf einem Ziel-Computer einen heap gibt und der Compiler/stdlib 
den mit "new" angeforderten Speicher auf diesen heap packt, können 
natürlich auch m_nCols, m_nRows auf dem Heap sein, wenn man nämlich "new 
Matrix" macht. Daher kann man nie so wirklich sagen ob eine Variable auf 
dem heap ist oder nicht; ob eine Variable im Automatic oder Free Store 
ist allerdings schon.

von Christian (Gast)


Lesenswert?

Danke soweit.
Kopy Konstruktor funktioniert.

Christian

von Dr. Sommer (Gast)


Lesenswert?

Karl Heinz schrieb:
> Christian schrieb:
>> Merkwürdig, VisualStudio Compiler lässt es aber mit sich machen, noch
>> nicht mal mit Warnung.
>
> Das ist ein Service vom gcc, das er das macht, indem er den/die
> Konstruktor/en analysiert.
Und von allen Standard C++ konformen Compilern.
> Das machen längst nicht alle Compiler. Als Programmierer muss man eben
> wissen, was man tut.
Dann sind diese Compiler nicht Standard-konform.

von Karl H. (kbuchegg)


Lesenswert?

Dr. Sommer schrieb:
> Karl Heinz schrieb:
>> Christian schrieb:
>>> Merkwürdig, VisualStudio Compiler lässt es aber mit sich machen, noch
>>> nicht mal mit Warnung.
>>
>> Das ist ein Service vom gcc, das er das macht, indem er den/die
>> Konstruktor/en analysiert.
> Und von allen Standard C++ konformen Compilern.

Verzeih. Ich bin da schon einige Zeit nicht mehr auf dem laufenden.
Wird das von einem der letzten Standards gefordert?

von Christian (Gast)


Lesenswert?

Das hab ich verstanden.
Erkenne aber nicht wirklich einen Unterschied zwischen Heap und 
Free/Automatic Storage.

cMatrix1,2,3,4 werden über Konstruktor aufgerufen und deswegen 
automatic, oder?
Wann ist free, wann automatic?

Christian

von Dr. Sommer (Gast)


Lesenswert?

Karl Heinz schrieb:
> Verzeih. Ich bin da schon einige Zeit nicht mehr auf dem laufenden.
> Wird das von einem der letzten Standards gefordert?

http://en.cppreference.com/w/cpp/language/copy_constructor - "If no 
user-defined copy constructors are provided for a class type (struct, 
class, or union), the compiler will always declare a copy constructor as 
an inline public member of its class."

Das ist aber mindestens seit C++98 schon so...

von Dr. Sommer (Gast)


Lesenswert?

Christian schrieb:
> Das hab ich verstanden.
> Erkenne aber nicht wirklich einen Unterschied zwischen Heap und
> Free/Automatic Storage.
Free/Automatic Storage sind die abstrakten Begriffe aus dem C++ 
Standard, die für den C++ Programmierer relevant sind. Heap und Stack 
hingegen sind für Compiler/stdlib-Autoren interessant.
> cMatrix1,2,3,4 werden über Konstruktor aufgerufen und deswegen
> automatic, oder?
Würde ich "new Matrix (3, 4);" schreiben würde da natürlich auch der 
Konstruktur aufgerufen, aber die Matrix-Instanz liegt dennoch im Free 
Store.

Alle Variablen selber sind in Automatic Store; die Ziele von Pointern, 
wenn sie aus "new" gefüllt werden, sind im Free Store.

von Karl H. (kbuchegg)


Lesenswert?

Dr. Sommer schrieb:
> Karl Heinz schrieb:
>> Verzeih. Ich bin da schon einige Zeit nicht mehr auf dem laufenden.
>> Wird das von einem der letzten Standards gefordert?
>
> http://en.cppreference.com/w/cpp/language/copy_constructor - "If no
> user-defined copy constructors are provided for a class type (struct,
> class, or union), the compiler will always declare a copy constructor as
> an inline public member of its class."


Ja. Sag ich doch.

Für mich hat deine Antwort jetzt so geklungen, als ob sich da in den 
letzten Standards was geändert hätte.


Denn das hier
ich
>> Das ist ein Service vom gcc, das er das macht,
>> indem er den/die Konstruktor/en analysiert.
du
> Und von allen Standard C++ konformen Compilern.

folgt ja gerade nicht. Ob der COmpiler die Konstruktoren analysiert oder 
nicht, hat nichts damit zu tun, ob er standard-konform ist oder nicht.

Laut C++ Standard ist kein Compiler dieser Welt dazu verpflichtet, 
problamtische Rule of 3(5) Velretzungen zu erkennen bzw. Vorkehrungen zu 
treffen, wie dann eben keinen Copy Konstruktor oder Assignment Op 
automatisch zu generieren.

Oder reden wir da jetzt gerade aneinander vorbei?

: Bearbeitet durch User
von Christian (Gast)


Lesenswert?

Funktioniert mit überschriebenen Copy Konstruktor:
Matrix( const Matrix & original )
{

  m_values = new _T [original.m_rows*original.m_columns];

  //operator=(original);
}
Muss aber Instanz so anlegen:

Matrix cMatrix2=cMatrix1;
... sonst wird der Kopy Konstruktor ja nicht aufgerufen und das Problem 
bleibt.

Meine Frage noch:
Wenn ich die Zeile //operator=(original) auskommentiere verhält es sich 
wie ohne Kopy Konstruktor, d.h. die debug assertion Meldung kommt 
wieder.
Was passiert da?

von Peter II (Gast)


Lesenswert?

Dr. Sommer schrieb:
> http://en.cppreference.com/w/cpp/language/copy_constructor - "If no
> user-defined copy constructors are provided for a class type (struct,
> class, or union), the compiler will always declare a copy constructor as
> an inline public member of its class."
>
> Das ist aber mindestens seit C++98 schon so...

und was hat das mit dem Problem zu tun?

Dort steht doch das er selber ein copy_constructor anlegt und das macht 
der MS doch? Dann müsste der GCC sogar fehlerhaft sein, denn er macht es 
ja scheinbar nicht.

Oder lese ich den text falsch?

von Christian (Gast)


Lesenswert?

Mir noch aufgefallen:
Dr. Sommer schrieb:

//ruft Konstruktor auf und reserviert heap1
  Matrix cMatrix1;
//ruft Konstruktor auf und reserviert heap2
  Matrix cMatrix3;

// Kopie anlegen (langsam)
  cMatrix3 = cMatrix1;

Die Kopie wird hier nicht angelegt. Es bleibt dabei, das der Default 
Operator aufgerufen wird, nicht der Copy.
Erst wenn:
Matrix cMatrix3 = cMatrix1;
...wird Copy aufgerufen.

von Karl H. (kbuchegg)


Lesenswert?

Christian schrieb:

> Was passiert da?


Ich schätze mal, das dein Verständnis davon, wann ein Copy Konstrultor 
und wann ein Zuweisungsoperator benutzt wirf fehlerhaft ist.

Ein Konstruktor (also auch ein Copy Konstruktor), wird immer dann 
benutzt, wenn ein neues Objekt erzeugt wird.

In
1
  Matrix cMatrix2=cMatrix1;

wird ein neues Objekt erzeugt, nämlich cMatrix2.
Objekterzeugung ist IMMER ein Aufruf eines Konstruktors

Hier hingegen
1
  Matrix cMatrix2;
2
3
  cMatrix2 = cMatrix1;

existiert das Objekt cMatrix2 schon, ehe das Programm zur 
problematischen Stelle kommt. Das = ist hier also kein Konstruktor 
Aufruf, sondern der Aufruf eines Zuweisungsoperators.

Du musst
* Copy Konstruktor
UND
* Zuweisungsoperator
überschreiben!

Die beiden haben auch verschiedene Aufgaben.
Ein Konstruktor erzeugt ein neues Objekt aus dem nichts. Ein 
Zuweisungsoperator arbeitet aber bereits auf einem gültigen Objekt. D.h. 
unter Umständen muss der im Objekt allokierte Resourcen freigeben, ehe 
er dann die für die Zuweisung notwendige Semantik durchführt!
Das ist ein wesentlicher Unterschied! Und daher ist es in C++ wichtig, 
zwischen Initialisierung und Zuweisung zu unterscheiden. Initialisierung 
mündet in einem Konstruktoraufruf und findet statt/kann nur statt 
finden, wenn ein NEUES Objekt erzeugt wird. Zuweisung hingegen operiert 
immer auf einem bereits existierendem Objekt.

: Bearbeitet durch User
von Dr. Sommer (Gast)


Lesenswert?

> Für mich hat deine Antwort jetzt so geklungen, als ob sich da in den
> letzten Standards was geändert hätte.
Deine klang so als würde nur der GCC die default Funktionen erzeugen :o)
> folgt ja gerade nicht. Ob der COmpiler die Konstruktoren analysiert oder
> nicht, hat nichts damit zu tun, ob er standard-konform ist oder nicht.
Das muss er aber, um den automatischen Konstruktor anzulegen bzw. nicht 
anzulegen.
> Laut C++ Standard ist kein Compiler dieser Welt dazu verpflichtet,
> problamtische Rule of 3(5) Velretzungen zu erkennen bzw. Vorkehrungen zu
> treffen, wie dann eben keinen Copy Konstruktor oder Assignment Op
> automatisch zu generieren.
Ja, beim Copy-Constructor nicht. Aber der Default Constructor wird zB 
nicht angelegt wenn ein selbst definierter Konstruktor da ist...

Christian schrieb:
> m_values = new _T [original.m_rows*original.m_columns];
Bitte keinen Bezeichner _T verwenden, das wird vom C++ Standard 
verboten!!!
http://stackoverflow.com/questions/228783/what-are-the-rules-about-using-an-underscore-in-a-c-identifier/228797#228797

Peter II schrieb:
> Dort steht doch das er selber ein copy_constructor anlegt und das macht
> der MS doch? Dann müsste der GCC sogar fehlerhaft sein, denn er macht es
> ja scheinbar nicht.
Der GCC macht das auch, aber der default copy constructor macht nicht 
das was der TO braucht...

Christian schrieb:
> Die Kopie wird hier nicht angelegt. Es bleibt dabei, das der Default
> Operator aufgerufen wird, nicht der Copy.
Ja, deswegen habe ich auch den copy assignment operator definiert, weil 
der hier aufgerufen wird...

von Klaus W. (mfgkw)


Lesenswert?

Dr. Sommer schrieb:
> http://en.cppreference.com/w/cpp/language/copy_constructor - "If no
> user-defined copy constructors are provided for a class type (struct,
> class, or union), the compiler will always declare a copy constructor as
> an inline public member of its class."
>
> Das ist aber mindestens seit C++98 schon so...

Das war es auch schon vorher, aber was hat das mit Warnungen zu tun, 
wenn ich es nicht selbst definiere?

von Dr. Sommer (Gast)


Lesenswert?

Klaus Wachtler schrieb:
> Das war es auch schon vorher, aber was hat das mit Warnungen zu tun,
> wenn ich es nicht selbst definiere?
Nix, Warnungen sind Compiler-Sache

von Karl H. (kbuchegg)


Lesenswert?

Dr. Sommer schrieb:
>> Für mich hat deine Antwort jetzt so geklungen, als ob sich da in den
>> letzten Standards was geändert hätte.
> Deine klang so als würde nur der GCC die default Funktionen erzeugen :o)

Dann haben wir aneinander vorbei geredet und reden ohnehin vom selben.
Meine Aussage bezog sich auf Olivers Aussage von 11:19 bzw. Christians 
Erwiderung (11:27) darauf.

Notiz an mich: klarer darstellen, worauf sich Antworten beziehen und wie 
die Posting-Kette dazu war.

von Dr. Sommer (Gast)


Lesenswert?

Karl Heinz schrieb:
> Notiz an mich: klarer darstellen, worauf sich Antworten beziehen und wie
> die Posting-Kette dazu war.
Wir brauchen ein Forum mit Graphenstruktur - die Posts werden als Knoten 
in einem gerichteten Graphen dargestellt, mit Kanten als "Antwort-Auf" 
Relation :o)

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.