Referenzen und andere Fehlkonstruktionen



  • Hallo zusammen,

    nachdem ich es endlich geschafft habe mich hier im Forum anzumelden - es sind erstaunlich viele Namen, die mir in den Sinn kamen bereits belegt - nun zu meiner Frage.

    Ich belege zz. den Kurs "Einführung in C++" wo wir als Aufgabe bekommen haben eine Klasse "MyString" zu schreiben und diese mit einem gegebenen Testprogramm zu testen. An sich kein Problem, jedoch harkt es noch ein ein paar stellen, da C++ doch etwas anderes ist als Pascal, Java und C# (Die Programmiersprachen, in denen ich gewöhlich programmiere).

    Unter anderem sollen wir einen Copy-Konstruktor implementieren:

    MyString(MyString s);
    

    Jedoch meckert so der gcc direkt und sagt, dass vermutlich eine Referenz, also:

    MyString(MyString& s);
    

    gemeint ist. Soweit so gut, jedoch meckert der Compiler bei dieser Implementation im Testprogramm bei folgender Zeile ("Keine passende Funktion für Aufruf MyString::MyString(MyString)"):

    MyString b = "Blumentopferde";
    

    Meine Implementation sieht folgendermaßen aus:

    MyString::MyString(MyString& s) 
    : m_length(s.length()), m_max_length(s.capacity()) {
    	m_c_pointer = new char[s.capacity()];
    	for (int len = 0; len < s.length(); len++) {
    		m_c_pointer[len] = s.m_c_pointer[len];
    	}
    }
    

    --

    Ein zweites Problem habe ich mit der Zeile

    format(a.substring(iPos,iLae));
    
    [...]
    
    void format(MyString &Ls) {
    	cout << " (" << Ls.length()   << "|" 
    		     << Ls.capacity() << ")  -  ";
    }
    

    im Testprogramm.
    Meine Implementation von substring sieht dabei folgendermaßen aus:

    MyString MyString::substring(int pos, int len) {
    	if (pos >= length()) {
    		len = 0;
    	} else if ((!len) || (pos + len >= length())) {
    		len = length() - pos;
    	}
    	MyString *subStr = new MyString(len);
    	for (int i = 0; i < len; i++) {
    		subStr->m_c_pointer[i] = m_c_pointer[pos + i];
    	}
    	return *subStr;
    }
    

    und

    a.substring(iPos,iLae).print();
    

    funktioniert ohne Probleme.
    [edit]Die Fehlermeldung, die ich bekomme lautet übrigends: "ungültige Initialisierung einer nicht-konstanten Referenz des Typs »MyString&« von temporärem Wert des Typs »MyString«"[/edit]

    Um sich das ganze mal im Zusammenhang anschauen zu können habe ich das Projekt mal hochgeladen:
    http://uni.larskiesow.de/cpp/blatt07/mystring.zip

    Ich hoffe, dass ihr mir bei meinen beiden Problemen (möglichst mit Erklärung, warum es so ist) weiterhelfen könnt.

    Gruß Lars



  • Hador_ schrieb:

    Unter anderem sollen wir einen Copy-Konstruktor implementieren:

    MyString(MyString s);
    

    Jedoch meckert so der gcc direkt und sagt, dass vermutlich eine Referenz, also:

    MyString(MyString& s);
    

    gemeint ist.

    Genau. Mit Referenz, am besten auch const.

    Soweit so gut, jedoch meckert der Compiler bei dieser Implementation im Testprogramm bei folgender Zeile ("Keine passende Funktion für Aufruf MyString::MyString(MyString)"):

    MyString b = "Blumentopferde";
    

    Hierfür brauchst du auch noch einen Konstruktor der const char* als Parameter akzeptiert.

    Meine Implementation sieht folgendermaßen aus:

    MyString::MyString(MyString& s) 
    : m_length(s.length()), m_max_length(s.capacity()) {
    	m_c_pointer = new char[s.capacity()];
    	for (int len = 0; len < s.length(); len++) {
    		m_c_pointer[len] = s.m_c_pointer[len];
    	}
    }
    

    Je nachdem wie length() definiert ist müsstest du hier auch noch die terminierende Null mitkopieren, oder?

    MyString MyString::substring(int pos, int len) {
    	if (pos >= length()) {
    		len = 0;
    	} else if ((!len) || (pos + len >= length())) {
    		len = length() - pos;
    	}
    	MyString *subStr = new MyString(len);
    	for (int i = 0; i < len; i++) {
    		subStr->m_c_pointer[i] = m_c_pointer[pos + i];
    	}
    	return *subStr;
    }
    

    Nicht new hier benutzt, sondern ein temporäres Objekt normal anlegen und zurückgeben. Sonst hast du ein Speicherleck.



  • Der Compiler meckert zu recht beim Copy-Ctor. Der Grund ist recht einfach: wenn du einen String als Argument deklarierst und nicht eine Referenz auf einen String, dann legt der Compiler in der Funktion ein eigenes String-Objekt an. Um das zu machen kopiert er beim Funktionsaufruf das Objekt, das du als Parameter übergibst. Dieses kopieren geschieht aber mit dem copy-Ctor. Und jetzt überleg mal wie das wohl aussieht, wenn er beim Aufruf des Copy-Ctor erstmal das Argument per Copy-Ctor kopieren muss... 😉

    MyString b = "Blumentopferde";
    

    Hier verstecken sich zwei Konstruktoraufrufe (!):
    Wenn der Compiler sieht MyString b = <irgendetwas>, dann wandelt er erst das <irgendetwas> in ein temporäres MyString-Objekt um und ruft dann für die Konsturiktion von b den Copy-Ctor auf mit dem temporären Objekt als Argument. Soll heißen, die Initialisierung mit = ist immer eine Kopie, und wenn der Typ nicht stimmt wird er erst umgewandelt. Allerdings wird die Kopie meist wegoptimiert (sollte sie aber nur, wenn der Copy-Ctor keine Nebeneffekte hat)

    Warum der Compiler jetzt meckert: der Copy-Ctor wird mit einem temporären Objekt aufgerufen. Da man die nicht noch großartig verändern kann, kann man allgemein temporäre objekte nur an Funktionen übergeben, die entweder eine Kopie anlegen (was beim copy-Ctor ja nicht geht) oder eine konstante Referenz als parameter haben. Bei deinem copy-Ctor ist die Referenz aber nicht konstant, deswegen findet der Compiler keine geeignete Funktion, der er das temporäre Objekt übergeben kann.

    Lösung: Copy-Konstruktoren immer mit einer const Referenz als Signatur schreiben. So machts übrigens auch der Compiler, wenn er den Copy-Ctor für dich generiert:

    class CopyMe
    {
    public:
      CopyMe(Copyme const& other); //oder auch (const CopyMe& other), ist beides das selbe.
    }
    


  • Danke erst einmal für die schnellen Antworten.
    Den Konstruktor habe ich nun folgendermaßen umgesetzt:

    MyString::MyString(const MyString& s) 
    : m_length(s.m_length), m_max_length(s.m_max_length) {
    	m_c_pointer = new char[s.m_max_length];
    	for (int len = 0; len < s.m_length; len++) {
    		m_c_pointer[len] = s.m_c_pointer[len];
    	}
    }
    

    Und substring habe ich so verändert, dann die Objekte nun nicht mehr mit new erzeugt werden:

    MyString MyString::substring(int pos, int len) {
    	if (pos >= length()) {
    		len = 0;
    	} else if ((!len) || (pos + len >= length())) {
    		len = length() - pos;
    	}
    	MyString subStr(len);
    	for (int i = 0; i < len; i++) {
    		subStr.m_c_pointer[i] = m_c_pointer[pos + i];
    	}
    	return subStr;
    }
    

    Eine Frage noch dazu: Wird ein Objekt, wenn es mit className varName(arg) erzeugt wird immer automatisch freigegeben und muss niemals selbst zerstört werden?

    --

    Soweit, sogut, allerdings meckert er auch immer noch bei folgender Zeile:

    format(a.substring(iPos,iLae));
    

    An sich ist es ja das, was pumuckl eben bereits angesprochen hat: Durch a.substring(...) wird ein temporäres Objekt erzeugt und nun müsste der Parameter von format konstant sein, oder das ganz vorher in etwa so zwischengespeichert werden:

    MyString xy = a.substring(iPos,iLae);
    	format(xy);
    

    Jedoch wären beides Änderungen im gegebenen Testprogramm und nicht im meiner Klasse. Dementsprechend sind diese Möglichkeiten an sich tabu.
    Daher die Frage: Gibt es für das Problem noch eine andere Möglichkeit? Oder ist einfach das Testprogramm "falsch"?

    @Fellhuhn: Konstruktor habe ich und der String soll explizit ohne Endnull gespeichert werden.



  • Du kannst format doch ändern. Nimm dort als Parameter eine konstante Referenz was du sowieso immer machen solltest wenn du am Parameter in der Funktion nichts ändern willst.



  • Braunstein schrieb:

    Du kannst format doch ändern. [...]

    Hador_ schrieb:

    [...] Jedoch wären beides Änderungen im gegebenen Testprogramm und nicht im meiner Klasse. Dementsprechend sind diese Möglichkeiten an sich tabu.
    Daher die Frage: Gibt es für das Problem noch eine andere Möglichkeit? Oder ist einfach das Testprogramm "falsch"?



  • Hador_ schrieb:

    ...jedoch harkt es noch ein ein paar stellen, da C++ doch etwas anderes ist als Pascal, Java und C# (Die Programmiersprachen, in denen ich gewöhlich programmiere).

    Vielleicht solltest du erst einmal ein paar Grundlegende Unterschiede zwischen Java/C# und C++ genannt bekommen (Pascal liegt für mich zulange zurück), gerade zum Begriff Referenz. Den der Begriff Referenz und auch die Übergaben sind anders geregelt (Das ist ein Fallstrick in beide Richtungen).

    C++ ist näher an den Grundlagen, und weniger abstrahiert wie Java/C#. Das heißt auch das man in C++ mehr angeben muss, und gleichzeitig mehr steuern kann.

    C++ kennt drei wesentliche Arten der Parameterübergabe: Wert, Referenz, Zeiger (Kombiniert mit konstant/nicht konstant in verschiedenen Ausprägungen). In C#/Java wird vieles davon Typabhängig geregelt (Integrale Typen als Wert=Kopie, ansonsten als "Referenz" [Was in C++ aber dem Zeiger mit einer C++ Referenz-syntax entspricht]). Beides hat seine Vorteile (In C++ sieht man leichter um was es geht, in Java/C# ist wegen der geringeren Anzahl der Möglichkeit das Anwenden einfacher).

    Hier mal eine Zusammenstellung aller mir auf anhieb einfallenden Parameterübergaben unter C++ in Kombination mit const (Achtung: Doppelzeiger etc. absichtlich außen vor gelassen, ebenso wie Referenzen auf Zeiger):

    void foo(int a);
    // 1. Übergabe einer int-Kopie
    // Gearbeitet wird mit einer änderbaren Kopie
    
    void foo(int & a);
    // 2. Übergabe einer int-Referenz
    // Gearbeitet wird mit dem übergebenen Wert, unter anderen Namen. Eine
    // Änderung, ändert den ursprungswert. Temporäre Übergaben sind unzulässig.
    // Von der Syntax der Referenz in C#/Java ähnlich, aber null ist unzulässig,
    // zudem kann eine Refenz nicht auf einen anderen Zeilwert verweisen.
    
    void foo(int * a);
    // 3. Übergabe eines Zeigers auf einen int-Wert
    // Gearbeite wird mit einen Zeiger auf einen Wert, der Zeiger kann null
    // annehmen. Das was du in C#/Java als Referenz kennst, entspricht in C++
    // einem Zeiger, nur das der Zugriff eingeschränkter als unter C++ ist.
    // In C++ ist sowohl der Wert als der Zeiger (Die Adresse in dem der Wert
    // steht) beliebig änderbar
    
    void foo(int const a);
    // 4. Wie 1, nur die Kopie kann nicht geändert werden
    
    void foo(int const & a);
    void foo(const int & a); // Alternativschreibweise
    // 5. Wie 2, nur die Referenz kann nicht geändert werden
    // Zusätzlich darf man an eine konstanten Referenz auch temporäre Wärte binden!
    
    void foo(int const * a);
    void foo(const int * a); // Alternativschreibweise
    // 6. Wie 3, wobei der Wert, auf den der Zeiger verweist konstant ist.
    // Der Zeiger darf zwar auf eine andere Adresse "umgebogen" werden, der Wert ist
    // über diesen Zeiger aber nicht Änderbar.
    
    void foo(int * const a);
    // 7. Wie 3, wobei der Zeiger konstant ist, nicht aber der Wert
    // Der Zeiger (die Speicheradresse) ist konstant, der Wert ist aber änderbar.
    
    void foo(int const * const a);
    void foo(const int * const a); // Alternativschreibweise
    // 8. Wie 3, Nur das Zeiger und Wert konstant sind
    

    Wegen der Alternativschreibweise:
    Viele schreiben const vor den Wert, ich habe mir immer die erste Art angewöht, da man dann immer (auch bei Zeigern) direkt ablesen kann was konstant ist. Gelesen wird hier von links nach rechts:
    int const * = Int-Konstante Zeiger
    int * const = Int Zeiger-Konstante
    int const * cosnt = Int-Konstante Zeiger-Konstante

    cu André



  • Danke, das ist doch mal eine schöne Übersicht 👍

    Aber demnach hatte ich es ja schon richtig verstanden und man kommt um eine Änderung an der main-Methode nicht herum.

    Lars

    --

    EDIT:
    Ich habe nun die Funktion format geändert, jedoch geht es so wie gedacht doch nicht:

    [...]
      format(a.substring(iPos,iLae));
    [...]
    
    void format(MyString const &Ls) {
    	cout << " (" << Ls.length()   << "|" 
    		     << Ls.capacity() << ")  -  ";
    }
    

    Denn nun sagt mir der Compiler :"Fehler: Die Übergabe von »const MyString« als »this«-Argument von »int MyString::length()« streicht Qualifizierer" 😕



  • Hador_ schrieb:

    ...
    Denn nun sagt mir der Compiler :"Fehler: Die Übergabe von »const MyString« als »this«-Argument von »int MyString::length()« streicht Qualifizierer" 😕

    Da stößt Du an const-Correctness (ebenfalls ein Konzept, das Java im Wesentlichen unbekannt ist 😉 ):
    - format() "verspricht", das übergebene Objekt nicht zu ändern (const ... &),
    - sie ruft aber length() auf, die dieses Versprechen NICHT macht.
    Lösung:
    MyString::length() "const" machen:

    class MyString {
    ....
       size_t length() const;
    

    Dasselbe natürlich auch mit capacity()...
    Falls das nicht geht, kommst Du nicht darum herum, format() ebenfalls zu "ent-const-en" (bitte hier nicht "const_cast" vorschlagen: Wenn die Klasse nicht in seinem Zugriff liegt, kann er auch nicht die const-Heit garantieren).

    Gruß,

    Simon2.



  • Wieder was gelernt, danke!

    Damit läuft nun alles. Jetzt muss ich nur noch dem Übungsleiter eine Mail schreiben und fragen, ob die Änderung der Main-Funktion so ok ist.

    Gruß Lars


Anmelden zum Antworten