C++ Datenelemente und Datenobjekte

r34ln00b

Lt. Commander
Registriert
Feb. 2006
Beiträge
1.096
Ich kenne mich nicht gut in C++ aus und sicherlich ist meine Frage nicht neu, leider weiß ich nicht nach was genau ich suchen müsste für eine Antwort.

Ich habe eine Klasse, zum Beispiel Element.cpp, welches nur ein "int i" speichert/hält.
Nun habe ich eine weitere Klasse, in der ich mehrere solcher Daten vom Typ Element.
Dabei habe ich Probleme mit dem erzeugen von neuen Objekten mittels "new".
Ich kann mir ein Element "erzeugen" mit
Code:
Element elem_1;
und dann mittels
Code:
 elem_1.i = 10;
setzen. Die Größe von elem_1 wird mir mittel "sizeof" mit 48 genannt.
Auch kann ich mir ein Objeckt erzeugen mittels
Code:
Element* elem_2 = new Element()
und den int mit
Code:
elem_2 -> i = 20
setzen. Seine Größe ist 48 und damit die selbe wie von elem_1.
Worin unterscheiden sich nun beide Methoden? Von Java bin ich es gewohnt, dass ich keine Datenelemente von Datenobjekten ändern/setzen kann, wenn es nicht ein Objekt gibt und ich dessen Daten ändern kann.
Wie ist es hier bei C++ zu verstehen? Habe ich mit beiden Methoden ein Objekt vorhanden? Wenn ja, inwiefern unterscheiden sich diese? Wozu dann einen Pointer zu einem Datenobjekt erzeugen, wenn ich direkt das Datenobjekt haben könnte (ausgenommen ich möchte später keine Kopie des Datenobjekts übergeben sondern den Pointer)?

Grüße
 
Du solltest dich schnell mal mit Pointern beschäftigen. Aber kurz gesagt:

Im ersten Fall allokierst du den Speicherplatz sofort und du brauchst dich um nichts zu kümmern. Das Objekt ist bereits erstellt und der Speicher wird bei Programmende automatisch wieder freigegeben

Im zweiten Fall allokierst du den Speicher während der Laufzeit. Dann musst du es aber auch wieder löschen mit delete am Ende! Hier hast du maximale Flexibiltät, dafür können Speicherlecks auftreten, wenn du vergisst den Speicherplatz wieder freizugeben.

Wenn du Java gewohnt bist, kannst du einfach erstere Art benutzen.
 
Hallo,
poste bitte mal den kompletten Code.

Im ersten Beispiel erzeugst du dein Objekt auf dem Stack und mit zweiten (mit new) auf dem Heap. Beide Objekte können also gar nichts voneinander wissen. Der Fehler liegt woanders.

Was meinst du übrigens mit "sizeof"? Ist das eine Methode von dir?? Es gibt in C++ nämlich auch einen operator sizeof!

greetz
hroessler
Ergänzung ()

@MPQ: Selbst den Speicher deleten macht man heute auch in C++ in den meißten Fällen nicht mehr selbst. Heute empfiehlt sich der Einsatz von smart pointern. Die deleten ihren allokierten Speicher selbst, wenn sie ihren Scope verlassen haben. Mit C++ 11 ist dies sogar noch angenehmer geworden. Zudem erlauben smart pointers mehr Kontrolle über die Zeiger. :)

Wenn er Java gewohnt ist, sollte aber eher auf dem Heap allokieren. Das tut Java bei Klassen nämlich auch...ausschließlich. So werden seine Objekte auch per Reference/Pointer übergeben wie er es aus Java gewohnt ist. Nur, und da bin ich bei dir, ne automatische Garbage Collection gibt es halt nicht...

greetz
hroessler
 
Zuletzt bearbeitet von einem Moderator:
"sizeof" ist der Operator von C++, nicht selbst programmiert.

Achso, mir war nicht bewusst, dass ein Objekt auf dem Stack erzeugt wird. Hatte angenommen, nicht-primitive (int, char, ..) Datentypen/-objekte müssten erste deklariert und dann (zwingend) initialisiert werden.

Zwei weitere Fragen:
Ein Memoryleak tritt demnach auf, wenn ich eine Funktion aufrufe, in der ein Datenobjekt mittels "new" erzeugt wird. Wenn dieses Datenobjekt aber nur für diese eine Funktionen nötig ist, muss ich es auch mittels "delete" (zum Beispiel in der Funktion, die es auch erzeugt) löschen?

Wenn ich einen Pointer auf ein Datenobjekt habe und diese vergleiche, dann werden nur die Adressen der Pointer verglichen? Ob zum Beispiel "0x1031c60" kleiner/größer/gleich als "0x1031d20" ist? Ähnlich zu int?
 
Zuletzt bearbeitet:
r34ln00b schrieb:
Hatte angenommen, nicht-primitive (int, char, ..) Datentypen/-objekte müssten erste deklariert und dann (zwingend) initialisiert werden.
Im Grunde passiert das ja auch, nur eben auf dem Stack und nicht mit einer zusätzlichen Speicherallokation auf dem Heap.

Standardmäßig hat jede Klasse einen Standardkonstruktor ohne Parameter, der wiederum die Standard-Konstruktoren seiner Member aufruft. Der wird automatisch aufgerufen, sobald du ein Objekt dieser Klasse durch Deklaration erzeugst.

Nehmen wir mal folgende "Klasse":

Code:
struct A {
  A()          { std::cout << "A::A();"         << std::endl; }  // Default-Konstruktor
  A(const A& ) { std::cout << "A::A(const A&);" << std::endl; }  // Copy-Konstruktor
  A(      A&&) { std::cout << "A::A(A&&);"      << std::endl; }  // Move-Konstruktor
  ~A()         { std::cout << "A::~A();"        << std::endl; }  // Destruktor
  
  A& operator = (const A& ) { std::cout << "A::operator = (const A&);\n" << std::endl; return *this; }  // Copy-Assignment
  A& operator = (      A&&) { std::cout << "A::operator = (A&&);\n"      << std::endl; return *this; }  // Move-Assignment
  
  bool operator == (const A&) { std::cout << "A::operator == (const A&);" << std::endl; return true;  }
  bool operator != (const A&) { std::cout << "A::operator != (const A&);" << std::endl; return false; }
};

Und benutzen diese:
Code:
void someFunction() {
  A stackObject;
  A* heapObject = new A();
}

dann wirst du feststellen, dass für das Stack-Objekt zunächst der Standardkonstruktor aufgerufen wird, und am Ende des Scopes, in der es deklariert wurde - in diesem Fall also am Ende der Funktion - der Destruktor. Das Objekt wird also automatisch zerstört. Der Sinn des Ganzen ist das RAII-Prinzip.

Für das Heap-Objekt wird hingegen nur der Standardkonstruktor aufgerufen, weil das delete fehlt. Unter anderem aus diesem Grund ist es auch nur vergleichsweise selten sinnvoll, Objekte mit new zu erzeugen - lokale Objekte, die nur innerhalb einer Funktion benutzt werden, schon gar nicht.

Mit dem oben angegebenen Struct kannst du ja mal etwas herumspielen, um zu verstehen, was bei den Stack-Objekten wann passiert. Der große Unterschied zu den Heap-Objekten, von denen man in der Regel ja nur die Pointer hin und her schiebt, ist, dass sie (häufig implizit) kopiert und verschoben werden.

Anderes Beispiel:
Code:
class I {
public:
  I(int value)
  : _value(value) { }
  // Ganz viel Code
private
  int _value;
};


Die Klasse hat keinen Default-Konstruktor mehr, weil explizit nur ein Konstruktor mit einem Parameter angegeben ist. Sehr wohl existieren aber Standard-Copy- und Move-Konstruktoren (siehe oben) und entsprechende Assignment-Operatoren, die nichts anderes tun als dieselbe Operation mit allen Member-Variablen durchzuführen. Deswegen:

Code:
I working1(1);  // I::I(int)
I working2 = 1;  // I::I(int)
I working3 = { 1 };  // I::I(int)
I working4 = I(1);  // I::I(int)
I notWorking;  // Geht nicht, weil kein Standardkonstruktor vorhanden

I* pointer1 = nullptr;  // Hier wird ja nur der Pointer initialisiert
I* pointer2 = new I(42);  // I::I(int)
I* pointer3 = new I();  // Geht nicht, kennst du ja aus Java


Wenn ich einen Pointer auf ein Datenobjekt habe und diese vergleiche, dann werden nur die Adressen der Pointer verglichen?
Richtig. Wenn du die eigentlichen Objekte mit den Operatoren vergleichen willst, musst du wahlweise die Pointer dereferenzieren, oder aber du stellst dich von Anfang an so geschickt an, dass du nie zwei Heap-Objekte miteinander vergleichen musst ;) Bisher bin ich jedenfalls auch ganz gut ohne ausgekommen.
 
Zuletzt bearbeitet:
Nochmal kurz zum "vergleichsweise selten sinnvoll, Objekte mit new zu erzeugen":
Angenommen ich möchte einen Baum erstellen, Knoten und Referenzen (unter den einzelnen Knoten) enthält.
Von Java bin ich es gewohnt, neue Datenobjekte zu erstellen (mittels "new"), in diesen Objekten meine Information zu speichern und diese in den Baum einzuordnen. Warum ist das nicht sinnvoll bzw. wie würde ich dies in C++ machen?
Mein Stack ist doch vergleichsweise klein und wenn ich viele Daten in den Baum einpflegen möchte, muss ich über den Heap gehen oder nicht?
 
"Vergleichsweise selten" heißt ja nicht "nie".

Klar ist: Die Knoten von so einer dynamischen Struktur müssen auf dem Heap erzeugt werden - schon allein, damit die Pointer eine sinnvolle Bedeutung haben und man sie zum Referenzieren anderer Knoten benutzen kann. Das ist bei Stack-Objekten ja nur so lange der Fall, wie die entsprechende Variable gültig ist.

Die Knotenwerte müssen, anders als in Java, nicht per new erzeugt werden - die sind ja Teil des Knotens. Wenn man jetzt eine (naiv und unvollständig implementierte) Linked List hat:

Code:
template<typename ElementType>
class LinkedList {
public:
  LinkedList(const ElementType& head, LinkedList* tail)
  : _head(head), _tail(tail) { }
  [...]
private:
  ElementType _head;
  LinkedList* const _tail;
};

[...]

LinkedList<A>* a = new LinkedList<A>(A(), nullptr);
LinkedList<A>* b = new LinkedList<A>(A(), a);

wird vielleicht deutlich, was ich meine - der Wert des einzelnen Listenelements wird nicht per new erzeugt, weil er ja ohnehin in dem Knotenobjekt selbst gespeichert wird. Und falls das doch mal gebraucht wird, kann man immer noch einen (Smart) Pointer als ElementType wählen - wobei Smart Pointer auch wieder nur ganz normale Objekte sind.


Edit:
Sehr große lokale Arrays sind natürlich auch ein Einsatzgebiet für den Heap, aber für sowas verwendet man in der Regel Container-Klassen wie std::vector, die von sich aus ihre Daten auf den Heap allokieren. Das std::vector-Objekt selbst liegt aber auf dem Stack. Im Grunde nutzt du dadurch den Heap, ohne davon etwas zu merken.


Edit 2:
Es gibt natürlich auch außerhalb dessen zahlreiche Fälle, in denen man gar nicht darum herum kommt, Objekte auf dem Heap zu erstellen. Es wäre zum Beispiel Blödsinn, die Widgets einer GUI-Library nicht direkt auf dem Heap zu erzeugen - a) weil man sie nur so über ihren Pointer identifizieren kann, b) weil die Objekte sehr komplex sind und nicht kopiert oder verschoben werden können sollen.

Der Punkt ist nur - Java treibt es da ja wirklich auf die Spitze. In Java wird ja ausnahmslos alles, was komplexer ist als ein int, zwangsweise auf den Heap gelegt, mit dem enormen Speicher-Overhead, der durch das implizite Erben von Object dazu kommt. Nehmen wir mal folgende Java-Klasse:

Code:
public class Vector<T> {
  public final T[] data;
  public final int size;
  public Vector(int size) {
    this.data = new T[size];
    this.size = size;
  }
  public Vector<T> add(Vector<T> other) {
    if (this.size != other.size)
      throw new VeryAnnoyingException();
    Vector<T> result = new Vector<T>(this._size);
    for (int i = 0; i < this.size; i++)
      [...]
  }
}

Hier wird einerseits das Vector-Objekt selbst auf dem Heap erzeugt, dazu kommt dann noch die Allokation des Arrays der Erwähnte Overhead durch Object. Effizientes Arbeiten ist in Java mit so einer Klasse unmöglich.

Das (quasi) gleiche in C++:
Code:
template<typename T, size_t Size>
class Vector {
public:
  // Addition
  Vector operator + (const Vector& other) const {
    Vector result;
    for (size_t i = 0; i < Size; i++)
      result[i] = this->_data[i] + other[i];
    return result;
  }
  // Zugriff auf Elemente
  const T& operator [] (size_t id) const { return this->_data[id]; }
        T& operator [] (size_t id)       { return this->_data[id]; }
private:
  T _data[Size];
};

Solange so ein Objekt nicht zu groß wird (ab mehreren hundert Bytes), wird niemand auf die Idee kommen, sowas mit new zu erzeugen. Zumal der Compiler die Addition auch sehr gut optimieren kann.
 
Zuletzt bearbeitet:
Wie Viking schon sagt: Man braucht new und delete in der Regel nur sehr selten und sollte sie vermeiden.
Wenn du selbst ne dynamische Kontainerstruktur bauen willst (Hier würde ich erstmal die Sinnfrage stellen... STL hat seit C++11 eigtl alles was man braucht?) gib jedem Knoten einen Member vom Typ eines der stl-container wie zB std::vector und füg dort deine Objekte ein mit push_back oder emplace/emplace_back. Sollte es pro Knoten nur ein einziges Objekt geben dann ist diese halt Member des Knoten und wird direkt im ctor initialisiert. So spart man sich komplett alle Pointer.
Ich schreib schon seit über 2 Jahren an einer Software und abgesehen vom QT-Gui code gibts nichtmal 10 new/delete und auch diese werde ich nochmal versuchen abzuschaffen - sie stören mich aber auch nicht besonders da sie nicht zur Laufzeit passieren. Bei meiner Software leben quasi alle Instanzen indem ich sie in einem stl-container aufbewahre der sich dann um new/delete automatisch kümmert.
Auch dieses stack/heap Thema habe ich noch nie als wichtig empfunden. Jeder Anfänger wundert sich irgendwann, dass lokal angelegte Arrays ne begrenzte Größe haben und lernt dann, dass man eh keine C-Arrays in C++ verwendet und eigtl muss man sich danach nicht mehr damit beschäftigen.
 
Zuletzt bearbeitet:
Zurück
Oben