Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

1

20.02.2015, 00:50

Gehirnbruch bei Kopierkonstruktor (Listing 11.7)

Hallo liebe Community, hab mal wieder n kleines Verständnisproblem bei einem der Listings 8|

Zu meiner Frage:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <iostream>

using namespace std;

// Klassen
//

// Klasse für ein Raumschiff
//
class CRaumschiff
{
    public:
        CRaumschiff ();
        ~CRaumschiff ();
        CRaumschiff (const CRaumschiff &rhs);

        void ZeigeWerte ();

    private:
        int *m_pGeschwindigkeit;

};

// Konstruktor
//
CRaumschiff::CRaumschiff ()
{
    // Speicher reservieren
    //
    m_pGeschwindigkeit = new int;
    * m_pGeschwindigkeit = 1541;

} // Konstruktor

// Destruktor
//
CRaumschiff::~CRaumschiff ()
{
    // Wenn pGeschwindigkeit ein gültiger Zeiger ist,
    // Speicher wieder freigeben.
    //
    if (m_pGeschwindigkeit != NULL)
    {
        delete (m_pGeschwindigkeit);
        m_pGeschwindigkeit = NULL;
    }

} // Destruktor

// Kopierkonstruktor
CRaumschiff::CRaumschiff (const CRaumschiff &rhs)
{
    cout << "Kopierkonstruktor wurde aufgerufen.";
    cout << endl;

    // Tiefe Kopie
    m_pGeschwindigkeit = new int;
    *m_pGeschwindigkeit = *(rhs.m_pGeschwindigkeit);

} // Kopierkonstruktor

// ZeigeWerte
//
void CRaumschiff::ZeigeWerte ()
{
    // Wenn der Zeiger gültig ist, Geschwindigkeit anzeigen
    //
    if (m_pGeschwindigkeit != NULL)
    {
        cout << "Geschwindigkeit: " << * m_pGeschwindigkeit;
        cout << endl;
    }

} // ZeigeWerte

// Eine beliebige Funktion, die eine Instanz
// der Klasse CRaumschiff übernimmt
void Funktion (CRaumschiff Schiff)
{
    Schiff.ZeigeWerte ();

} // Funktion

// Hauptprogramm
//
int main ()
{
    // Ein neues Raumschiff erzeugen
    CRaumschiff Jaeger;

    // Geschwindigkeit des Raumschiffs anzeigen lassen
    Jaeger.ZeigeWerte ();

    // Die Instanz an eine Funktion übergeben
    Funktion (Jaeger);

    return (0);
}


Ich weiß jetzt zwar, was der Kopierkonstruktor bewirkt, aber ich verstehe absolut NULL, wie er dort hin kommt...
-Warum wird eine konstante Referenz übergeben?
-Ist dies denn eine Referenz von unserem "Jaeger" oder einfach nur irgendeine Referenz einer Instanz?
-Zu was genau gehört "m_pGeschwindigkeit" (Z.57) ?
-Was passiert genau intern im Kopierkonstruktor (Z. 53+54 mal ausgenommen :D )?

Habe das Thema Pointer und Referenzen, sowie auch die Speicherreservierung drauf, nur diese Funktionsweise des Kopierkonstruktors verwirrt mit total :dash:

Danke schon mal im Voraus!

birdfreeyahoo

Alter Hase

Beiträge: 756

Wohnort: Schorndorf

Beruf: Junior Software Engineer

  • Private Nachricht senden

2

20.02.2015, 01:47

Eine konstante Referenz wird übergeben, weil du eine Referenz auf das ursprüngliche Objekt brauchst, also das "Original". Wenn es pass-by-value wäre, hättest du nur eine Kopie und ich glaube an der Stelle wird erklärt dass das schiefgeht ohne eigenen Kopierkonstruktor (zumal das Aufrufen des Kopierkonstruktors zum übergeben in den Kopierkonstruktor keinen Sinn macht ^^).

Die Referenz ist konstant, weil du das Objekt nur kopieren sollst und es nicht verändern sollst. Das ist nicht der Sinn und eigentlich auch gar nicht erwünscht, dass das Original geändert wird.

Der Kopierkonstruktor wird hier beim Aufrufen von Funktion aufgerufen. Weil das Raumschiff by-value übergeben wird, wird es kopiert. Dem neuen Objekt sein Kopierkonstruktor wird aufgerufen (heißt immernoch Konstruktor ^^) und der Parameter ist eine Referenz auf Jäger. Das neue Objekt ist das mit was du in der Funktion arbeitest.

m_pGeschwindigkeit ist ein Pointer der in CRaumschiff deklariert wurde.

Der Kopierkonstruktor ist aufgrund folgendes Phänomens hier notwendig:
Der Standardkopierkonstruktor kopiert jedes Feld 1 zu 1 in das neue Objekt.
In diesem Fall würde m_pGeschwindigkeit auch kopiert werden. Was passiert wenn wir einen Zeiger kopieren? Richtig, wir haben einen zweiten Zeiger der aber auf das gleiche Objekt (hier ein int) zeigt.
In manchen Fällen ist das nicht schlimm, hier aber schon.
Sagen wir Schiff2 ist eine Kopie von Schiff1. In Schiff2 änderst du die Geschwindigkeit, also das auf was m_pGeschwindigkeit zeigt. Da in Schiff1 m_pGeschwindigkeit auf den gleichen Integer zeigt, hast du ein Problem. Alle Kopien teilen sich einen Integer (beachte es ist ein Pointer!). Das führt zu unerwünschtem Verhalten, da alle immer die gleiche Geschwindigkeit haben.
Das passiert wenn der Standardkopierkonstruktor aufgerufen wird.

Jetzt haben wir einen eigenen. Dieser setzt den Zeiger des neuen Objekts auf ein neues int-Objekt (new int) und kopiert nur den Wert (mit dem *-Operator).
Damit zeigen beide Zeiger auf andere Integer und die Raumschiffe haben unabhängig voneinander Geschwindigkeiten.

3

20.02.2015, 07:01

Um das Beispiel von Birdfreeyahoo weiter zu spielen.

Wenn eines der Schiffe seinen Destruktor durchläuft sollte da ja auch ein

C-/C++-Quelltext

1
delete m_pGeschwindigkeit;
auftauchen.

Das aber hat zur Folge das alle anderen Schiffe jetzt auf einen Speicherbereich zeigen der nicht mehr gültig ist.
Der Worst-Case ist dann jetzt ein Programm Absturz.

Gruß Koschi
Wer aufhört besser werden zu wollen hört auf gut zu sein!

aktuelles Projekt:Rickety Racquet

Schorsch

Supermoderator

Beiträge: 5 145

Wohnort: Wickede

Beruf: Softwareentwickler

  • Private Nachricht senden

4

20.02.2015, 10:41

Es bietet sich an wenn du selbst ein wenig Code schreibst und das ganze mal testest. Mach dir doch mal eine Konsolenanwendung und guck was passiert wenn du einer Klasse explizit einen Kopierkonstruktor gibst und was ist wenn du eine Instanz der Klasse an eine Funktion übergibst. Versuch auch da ruhig mal mit verschiedenen Übergabemethoden zu spielen, also per Zeiger, per Referenz, per Value und das ganze auch mal mit const zu versehen. Spiel ruhig alle Möglichkeiten mal durch. Bevor du das ganze hin tippst, mach dir erst mal bewusst was die Version die du umsetzen will eigentlich machen müsste und ob das ganze so überhaupt kompilieren würde. Wenn du es selbst an eigenem Code siehst sollte das alles verständlicher werden.
„Es ist doch so. Zwei und zwei macht irgendwas, und vier und vier macht irgendwas. Leider nicht dasselbe, dann wär's leicht.
Das ist aber auch schon höhere Mathematik.“

5

20.02.2015, 11:22

Danke für die super Antworten :thumbsup:
Ich weiß was hier für eine Problematik auftritt, wenn wir keinen Kopierkonstruktor ausprogrammieren, ich weiß auch wie das Resultat davon aussieht...

-Ich verstehe einfach nur nicht was genau wir neuen Speicher zuweisen (Z.57). Gehört der pointer auf m_pGeschwindigkeit (beim Programmdurchlauf) zu dem von uns erstellten Jaeger? Oder doch zu der Referenz? Oder einfach zu irgendeiner Kopie?

-Desweiteren haben wir ja eine Referenz übergeben, also wenn wir diese ändern oder etwas neues zuweisen, dann ändert sich ja auch die Ausgangsvariable...In Z.58 haben wir jedoch nur einen Wert der Referenz genommen und ihn nicht verändert. Hab ich das so richtig verstanden?
:D

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »EizoTheAssAssin« (20.02.2015, 11:30)


6

20.02.2015, 14:34

Zitat

-Ich verstehe einfach nur nicht was genau wir neuen Speicher zuweisen (Z.57). Gehört der pointer auf m_pGeschwindigkeit (beim Programmdurchlauf) zu dem von uns erstellten Jaeger? Oder doch zu der Referenz? Oder einfach zu irgendeiner Kopie?

Genau deshalb ist dieser Pointer da ein schlechtes Beispiel: Verwirrung. Der soll vermutlich nur demonstrieren, was ohne copy-ctor schief gehen kann.

Du kannst sehen, dass in den Konstruktoren (+ Copy-Constructor), die Variable per new auf dem Heap instanziiert wird. Also einfach ein pointer auf einen Speicherbereich mit dem Wert.
Das heißt aber auch, dass dieser Speicher wieder freigegeben wird. Das passiert dann, wenn das Objekt zerstört (destructed) wird - also im Destruktor.

Auf Zeile 89 wird der Konstruktor aufgerufen, und der pointer von Jeager auf den Speicher gesetzt und 1541 zugewiesen. Auf Zeile 95 wird der Copykonstrutor mit Jeager als parameter aufgerufen. Ein Neues Objekt Schiff wird erstellt, dessen pointer auf neuen Speicherbereich gesetzt und diesem Speicherbereich der Wert des Jeager-Speicherbereichs zugewiesen. Dann haben beide (Jaeger und Schiff) einen separaten Speicherbereich und durch die Zuweisung hat der den gleichen Inhalt. Am Ende der Funktion wird Schiff destructed und somit der Speicherbereich (vom Schiff) wieder freigegeben (Z. 44). Jaeger hat deßhalb trotzdem noch einen validen pointer auf seinen Speicherbereich.

Beachte, dass man, wenn man den Copy-Constructor überlädt, man auch den Assignment-Copy-Constructor überladen sollte. Sonst werden die Daten bei der Zuweisung via = nicht kopiert (oder mithilfe des Standard-Assignment-Operators --> ungewünschtes Verhalten möglich). [1]

Zitat

-Desweiteren haben wir ja eine Referenz übergeben, also wenn wir diese ändern oder etwas neues zuweisen, dann ändert sich ja auch die Ausgangsvariable...In Z.58 haben wir jedoch nur einen Wert der Referenz genommen und ihn nicht verändert. Hab ich das so richtig verstanden?

Richtig. Beachte, dass die Begriffe "Referenz" und "Pointer"/"Zeiger" in C/C++ unterschiedliche Bedeutungen haben. Im folgenden Beispiel ist a und c ein Wert, b, d und e eine Referenz und p ein Pointer.
Das "den Wert der Referenz/worauf der Pointer zeigt nehmen" heißt übrigens "Dereferenzieren" - bringt Sinn.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int a = 10;
const int& b = a; // b referenziert a
cout >> b; // 10
b = 20; // fehler, b ist const
int c = a; // copy
c = 20;
cout >> c >> a; // 20 10
int& d = a;
d = 20;
cout >> d >> a; // 20 20

//int* p = a; // meh
int* p = new int; // p --> [??]
*p = a; // a = 10, p --> [10]
a = 20; // p --> [10]
int& e = *p;
e = 25; // p --> [25]
delete p;

void main() {
    MyClass obj1; // calls ctor of obj1
    MyClass obj2(obj1); // calls copy-ctor of obj2 with obj1 as parameter
    obj1 = obj2; // calls assignment-copy-ctor of obj1 with obj2 as parameter
} // calls dtor of obj1 and obj2.

// assignment copy ctor:
class MyClass {
public:
    MyClass& operator = (const MyClass& rhs) { ... return *this; }
};
EnvisionGame(); EnableGame(); AchieveGame(); - Visionen kann man viele haben. Sie umzusetzen und auf das Ergebnis stolz zu sein ist die eigentliche Kunst.

Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von »iSmokiieZz« (20.02.2015, 14:42)


7

20.02.2015, 20:40

Zitat

Auf Zeile 95 wird der Copykonstrutor mit Jeager als parameter aufgerufen.

Aber in der Zeile wird doch die Funktion "Funktion" aufgerufen. 8|
Außerdem hat der Copykonstruktor doch als Parameter irgendeine Referenz einer Instanz von CRaumschiff übergeben, oder lieg ich da daneben ? Es steht doch nirgendwo, dass der Copykonstruktor Jaeger bekommt ?

Ich habe mir das heute morgen nochmal in Ruhe angeschaut und hatte folgendes im Kopf:
m_pGeschwindigkeit (in Z.57) ist die Kopie des Zeigers des Jaegers und bekommt einen neuen Speicherbereich zugewiesen.
In der nächsten Zeile bekommt diese Kopie den Wert des Zeigers von der uns erstellten Referenz zugewiesen (Nicht die Adresse)
Also existiert: Jaeger, die Kopie von Jaeger, Referenz von CRaumschiff (Falls irgendwas falsch ist korrigier mich bitte :?: )

Habe jetzt von dir, mir und nem anderen Forum was im Kopf , weiß grade garnicht welcher Schinken der richtige ist :dash:

Zitat

Beachte, dass man, wenn man den Copy-Constructor überlädt, man auch den Assignment-Copy-Constructor überladen sollte. Sonst werden die Daten bei der Zuweisung via = nicht kopiert (oder mithilfe des Standard-Assignment-Operators --> ungewünschtes Verhalten möglich). [1]

Habe den Copy Constructor garnicht überladen :?:
Wir haben hier den Normalen Konstruktor und den Kopierkonstruktor. Was ist denn der Assignment-Copy-Constructor?


Das Beispiel von dir is 1a :thumbsup: , nur die Zeile 29 versteh ich nicht

Sorry für die vielen Fragen :D

birdfreeyahoo

Alter Hase

Beiträge: 756

Wohnort: Schorndorf

Beruf: Junior Software Engineer

  • Private Nachricht senden

8

20.02.2015, 23:21

Der Copykonstruktor einer Instanz wird beim Erzeugen aufgerufen, wenn sie als Kopie eines anderen Raumschiffes erstellt wird.
Wenn du Funktion aufrufst, übergibst du Jaeger by-value. Das heißt, eine Kopie wird in die Funktion übergeben.

Ein neues Objekt wird erzeugt und der Kopierkonstruktor mit Jaeger als Parameter aufgerufen. Das passiert durch das Aufrufen automatisch. Wenn du das nicht willst musst du deinen Jaeger per Referenz oder Zeiger übergeben.

m_pGeschwindigkeit in Z.57 ist die Variable in der neuen Instanz und der Integer auf den gepointet wird bekommt den Wert des Integers von Jaeger.
Es existiert: Der Jaeger, Die Kopie des Jaegers, mehr nicht.

Du hast den Copy-Constructor überladen, indem du einen deklariert hast.
Der Assignment-Copy-Constructor ist glaub ich die Überladung des Zuweisungsoperators (?)

9

20.02.2015, 23:53

Zitat

Aber in der Zeile wird doch die Funktion "Funktion" aufgerufen.

Richtig, aber die Funktion bekommt einen Parameter. Wenn du einen Parameter übergibst, ist das nichts anderes, als die Initialisierng einer Variable vorstellen:

C-/C++-Quelltext

1
2
3
4
5
6
7
void Funktion(CRaumschiff schiff)
{
...
}

CRaumschiff jaeger;
Funktion(jaeger); // CRaumschiff schiff(jeager);
In der letzten Zeile wird beim Aufrufen der Funktion der Parameter übergeben. Die neue Variable wird via Copy-Ctor initialisiert (siehe Kommentar hinter der Zeile). Ohne Funktion könnte man sowas schreiben:

C-/C++-Quelltext

1
2
3
CRaumschiff jaeger;
CRaumschiff schiff(jaeger); // ruft den copy-ctor von der Klasse CRaumschiff für schiff auf, welcher eine Referenz auf jaeger bekommt.
...
(Was natürlich Quark ist, weil du auf jaeger schon zugreifen kannst, dann brauchst du schiff nicht. Die Funktion kann ja aber nicht auf jaeger zugreifen.)

Zitat

m_pGeschwindigkeit (in Z.57) ist die Kopie des Zeigers des Jaegers und bekommt einen neuen Speicherbereich zugewiesen.
In der nächsten Zeile bekommt diese Kopie den Wert des Zeigers von der uns erstellten Referenz zugewiesen (Nicht die Adresse)

So in etwa. Es wird halt nur der Wert kopiert ;)

Das Objekt "Jaeger" (vom Typ CRaumschiff) existiert von Z. 89 bis 98.
Das Objekt "Schiff" (vom Typ CRaumschiff) existiert von Z. 78 bis 82.
Das Objekt "rhs" (vom Typ konstanter Referenz auf ein CRaumschiff) existiert von Z. 51 bis 60.

Zitat

Habe jetzt von dir, mir und nem anderen Forum was im Kopf , weiß grade garnicht welcher Schinken der richtige ist

Stell deine Schinken vor, dann können wir auch sagen, welches der richtige ist ;)

Zitat

Habe den Copy Constructor garnicht überladen

Strenggenommen ist es kein "überladen" des copy-ctors, sondern eher ein überschreiben des Standard Copy-Ctors. ^^

Zitat

Wir haben hier den Normalen Konstruktor und den Kopierkonstruktor. Was ist denn der Assignment-Copy-Constructor?

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct MyStruct {
   int i;
   MyStruct() : i(10) {} // ctor von MyStruct mit initialisierer-liste, es wird der Copy-Ctor von int für i aufgerufen.
   //MyStruct() { i = 10; } // ctor ohne initialisierer-liste, es wird der copy-assignment-operator von int für i aufgerufen
   MyStruct(const MyStruct& rhs) : i(rhs.i) {} // copy-ctor von MyStruct, ruft den Copy-Ctor von int für i mit der referenz auf rhs.i als parameter auf
   //MyStruct(const MyStruct& rhs) { i = rhs.i; } // alternativer copy-ctor von MyStruct, hier wird statt des Copy-Ctors von i, der Assignment-Copy-Operator von i aufgerufen.
   MyStruct& operator = (const MyStruct& rhs) { // copy-assignment-operator für MyStruct - für aufruf siehe Copy-Ctor von MyClass
       i = rhs.i; // beachte: rhs ist referenz auf MyStruct.
       return *this;
   }
};

class MyClass {
private:
    MyStruct s;
public:
    MyClass() {} // Der ctor von s wird hier automatisch aufgerufen, entspricht also folgender Zeile:
    //MyClass() : s() {}
    MyClass(const MyClass& rhs) : s(rhs.s) {} // copy-ctor von MyClass. Für s wird der Copy-Ctor von MyStruct aufgerufen (Z. 5) mit einer Referenz zu rhs.s als parameter.
    //MyClass(const MyClass& rhs { s = rhs.s; } // alternativer copy-ctor von MyClass. Für s wird nun der Assignment-Copy-Operator aufgerufen. (Z. 7)
    MyClass& operator = (const MyClass& rhs) { // assignment-copy-operator für MyClass
        s = rhs.s; // ruft Assignment-Copy-Operator von MyStruct auf mit einer Referen zu rhs.s als Parameter auf.
        return *this;
    }
};

MyClass c1;
MyClass c2 = c1; // copy-assignment-operator von MyClass (Z. 20)
MyClass c3(c2); // Copy-Ctor von MyClass (Z. 18)


Ich hoffe, dass Zeile 29 in meinem Beispiel jetzt auch verständlicher ist.
Ich kenne dieses Buch nicht wirklich, aber ich kann mir vorstellen, dass das alles dort auch nochmal drin erklärt wird ;)

Edit:
Fehler in den beiden Copy-Assignment-Operatoren im unteren Beispiel.
Die returns fehlten (return *this;). this ist der pointer auf das aktuelle objekt. Dieser wird dereferenziert und bei der rückgabe (als MyClass& -Referenz z.B.) implizit wieder referenziert.
EnvisionGame(); EnableGame(); AchieveGame(); - Visionen kann man viele haben. Sie umzusetzen und auf das Ergebnis stolz zu sein ist die eigentliche Kunst.

Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von »iSmokiieZz« (21.02.2015, 10:37)


Schorsch

Supermoderator

Beiträge: 5 145

Wohnort: Wickede

Beruf: Softwareentwickler

  • Private Nachricht senden

10

21.02.2015, 10:03

Ein neues Objekt wird erzeugt und der Kopierkonstruktor mit Jaeger als Parameter aufgerufen. Das passiert durch das Aufrufen automatisch. Wenn du das nicht willst musst du deinen Jaeger per Referenz oder Zeiger übergeben.

Deshalb solltest du bei Klassen die nicht kopiert werden sollen, was relativ oft der Fall ist, einen privaten Kopierkonstruktor anlegen. Dadurch sorgst du dafür dass der Compiler keinen Standardkonstruktor anlegt. Willst du jetzt eine Instanz der Klasse per Value an eine Funktion übergeben, so meckert dein Compiler dass du das nicht darfst weil der Kopierkonstruktor eben private ist und so nicht zugreifbar. Du verhinderst also dass du selbst aus versehen Kopien anlegst.
„Es ist doch so. Zwei und zwei macht irgendwas, und vier und vier macht irgendwas. Leider nicht dasselbe, dann wär's leicht.
Das ist aber auch schon höhere Mathematik.“

Werbeanzeige