DELETE() - Wird das Objekt wirklich gelöscht?



  • Hallo Zusammen,

    ich hätte eine Verständnisfrage die sich mir selbst nicht ganz erschließt und auch die Unterlagen meines Profs nicht wirklich eine Antwort liefern.
    Wir haben die C++-Programmierung begonnen und starteten mit Klassen und somit auch die Erstellung und Zerstörung von Objekten.
    Als Teilaufgabe einer Praktikumsaufgabe sollen wir über den Destruktor sicher stellen dass für eine Variable(char*) des Konstruktors dynamischer Speicher belegt ist und wenn das der Fall ist, diesen freigeben und ausgeben dass das Auto gelöscht wurde.

    In unserer Klasse soll der Destruktor dazu benutzt
    werden den dynamischen Speicher des Attributs Modell wieder freizugeben. Jedoch muss
    darauf geachtet werden, dass dafür ein dynamischer Speicher belegt ist. Dies kann daran
    erkannt werden, dass der Wert des Pointers modell ungleich NULL ist.

    Mein gesamter Code ist kompilierbar und endet nicht auf einen Fehler, es geht wesentlich um die folgenden Codefetzen. Die Funktion "zeichenketteEinleisen()" funktioniert wie sie soll.

    #include <iostream>
    #include <cstdlib>
    #include <cstdio>
    #include "Auto.h"
    
    using namespace std;
    
    //Prototypen
    char* zeichenketteEinlesen();
    
    int main()
    {
        char* ptrMain;
        int anzTuerenMain, leistungMain;
    
        Auto* a1 = new Auto;
        a1->setModell("Audi A3");
        a1->setAnzTueren(4);
        a1->setLeistung(200);
    
        Auto* a2 = new Auto;
    
        cout << "Geben Sie bitte das Modell ein." << endl;
        ptrMain = zeichenketteEinlesen();
        cout << "\nGeben Sie bitte die Tueranzahl ein." << endl;
        scanf("%d", &anzTuerenMain);
        cout << "\nGeben Sie bitte die Leistung in PS ein." << endl;
        scanf("%d", &leistungMain);
    
        Auto* a3 = new Auto(ptrMain, anzTuerenMain, leistungMain);
    
        a1->ausgabe();
        a2->ausgabe();
        a3->ausgabe();
    
        delete a1;
        delete a2;
        delete a3;
    
        a1->ausgabe();
        a2->ausgabe();
        a3->ausgabe();
    
        return 0;
    }
    

    und hier der Code für den Konstruktor und Destruktor

    #include <iostream>
    #include <cstdlib>
    #include <string>
    #include "Auto.h"
    
    using namespace std;
    
        Auto::Auto()
        {
            this->modell = "";
            this->anzTueren = 0;
            this->leistung = 0;
        }
    
        Auto::Auto(char* modell, int anzTueren, int leistung)
        {
            this->modell = modell;
            this->anzTueren = anzTueren;
            this->leistung = leistung;
        }
    
        Auto::~Auto()
        {
            if(this->modell != NULL)
            {
                free(this->modell);
                cout << "\nDas Auto " << this->modell << " wurde geloescht!" << endl;
            }
        }
    

    durch die Erzeugung eines Objektes durch NEW reserviert das Programm doch automatisch dynamischen Speicher für das Objekt und durch den DELETE Befehl wird automatisch reservierter Speicher freigeben. So habe ich es jedenfalls verstanden.

    Für mich würde das heißen dass auch automatisch Speicher für den Pointer Modell reserviert wird was es doch vollkommen unnötig macht zu überprüfen ob Modell dynamischen Speicher belegt oder nicht und ich diesen nicht explizit (free(this->modell);) wieder freigeben muss da Good Guy DELETE das doch für mich übernimmt. Jedoch verwirrt es mich auch dass selbst nach dem DELETE Befehl immer noch die Objektwerte ausgegeben werden können (anzTueren, leistung). Ich bin mir extrem unsicher ob ich die Aufgabe denn jetzt durch diesen Code erfüllt habe und ob das Objekt wirklich gelöscht ist oder nur kein Speicher mehr dem Objekt gehört und wie kann ich das überprüfen?

    Vielen Dank im Voraus 🙂



  • Da hast du aber Glück, daß "deine Festplatte nicht formatiert wird", denn dein Code ist UB (undefined behavior), d.h. es kann alles mögliche passieren (aber auch, daß die selber Werte wie vor dem delete ausgegeben werden).
    Als Programmierer mußt du sicherstellen, daß dein Programm niemals auf freigegebenen Speicher mehr zugreift!

    Und free(this->modell) ist ebenfalls UB, denn dieser Speicher wurde nicht mit malloc vorher reserviert.
    Aber auch delete this->modellwäre hier falsch, d.h. lösche diese Zeile einfach.

    Auch wenn diese Aufgabe wohl das Verwenden von dynamischer Speicherreservierung üben soll, so ist es unnötig, denn hier würden lokale Objekte ausreichen:

     Auto a1("Audi A3", 4, 200);
    

    (dann sollte eigentlich auch const char* modell im Konstruktor verwendet werden, damit das standardkonform kompiliert - wegen der Verwendung von String-Literalen!)

    Edit: Um den Code für die Aufgabe korrekt zu implementieren, müßtest du im Konstruktor von Auto für modell dynamisch Speicher per new anlegen (d.h. eine Kopie des übergebenen String-Parameters anlegen), um diesen dann wieder im Destruktor per delete zu löschen.
    Selbiges gilt dann auch für setModell.

    Lies aber mal Regel der Großen Drei (Dreierregel) durch (bzw. weitere Ressource dazu in deinem C++-Buch bzw. Internet).
    In gutem C++-Code sollte aber die "Rule of Zero" gelten, d.h. möglichst kein eigener Kopierkonstruktor, Zuweisungsoperator und Destruktor.



  • Ne, du hast da nen Riesenbock drin, wundert mich, dass dir das nicht direkt um die Ohren fliegt. Und zwar rufst im Destruktor von Auto ein free auf einem char-Pointer auf, dessen Zeiger nie mit malloc erzeugt wurde. In Zeile 17 setzt du mit a1->set_modell( "Audi A3" ) eine Zeichenkette, die als Konstante im Datensegment lebt und nicht freigegeben werden darf. Was mit a2 und a3 ist kann ich nicht sagen, weil ich nicht weiß, wie der Rückgabewert von zeichenketteEinlesen() aussieht. Du kannst dir die ganzen Probleme ersparen, wenn du stattdessen std::string benutzt.

    Die Aussage:
    In unserer Klasse soll der Destruktor dazu benutzt
    werden den dynamischen Speicher des Attributs Modell wieder freizugeben. Jedoch muss
    darauf geachtet werden, dass dafür ein dynamischer Speicher belegt ist. Dies kann daran
    erkannt werden, dass der Wert des Pointers modell ungleich NULL ist.

    ist pauschal falsch. Du musst sicherstellen, dass die Zeichenkette, die der Methode set_modell() übergeben wurde, per malloc auf dem Heap erzeugt wurde.

    Das beobachtete Verhalten ist ok, wenn Speicher freigegeben wird bedeutet das nur, dass er als frei markiert wird. Der Inhalt bleibt erst ein Mal unberührt. Was du beim Lesen siehst ist quasi die Leiche des Objekts.



  • @Th69 sagte in DELETE() - Wird das Objekt wirklich gelöscht?:

    Da hast du aber Glück, daß "deine Festplatte nicht formatiert wird", denn dein Code ist UB (undefined behavior), d.h. es kann alles mögliche passieren (aber auch, daß die selber Werte wie vor dem delete ausgegeben werden).

    gibt es dafür aber nicht auch Warnungen in den Build Messages? da habe ich nur line 11 warning: deprecated conversion from string constant to 'char*' --- 'this->modell = "";' in der auto.cpp

    Und free(this->modell) ist ebenfalls UB, denn dieser Speicher wurde nicht mit malloc vorher reserviert.

    das war auch meine Annahme und hat mich bis jetzt verwundert wieso der Compiler nichts sagt. Ich habe es mir dann einfach damit erklärt dass ein Teil des reservierten Speichers durch NEW automatisch für den Pointer Modell benutzt wird.

    Aber auch delete this->modellwäre hier falsch, d.h. lösche diese Zeile einfach.

    heißt meine Annahme war richtig und im Destruktor müsste eigentlich nur die cout-Zeile stehen und ich muss mir um nichts Gedanken machen wenn 'free(this->modell)' nicht mehr dort steht? 😃



  • Eigentlich ja - beachte aber mein "Edit" oben. 😉

    Bei UB ist aber eben das gemeine, daß der Compiler solche Fehler nicht erkennt (bzw. erkennen kann) und es daher keine Warnungen oder Fehler diesbzgl. gibt (blöde, daß man als Programmeirer noch selber nachdenken muß ;-- ).



  • @DocShoe Der Rückgabewert von zeichenketteEinlesen() ist ein char-Pointer.

    char* zeichenketteEinlesen()
    {
        char zeichen[1];
        char* ptrZeichen = NULL;
        int z = 0;
    
        ptrZeichen = (char*) malloc(sizeof(char)*1);
    
        while(zeichen[0] != '\n')
        {
            zeichen[0] = getchar();
            ptrZeichen = (char*) realloc(ptrZeichen, sizeof(char)*(z+1));
    
            if(zeichen[0] == '\n')
            {
                *(ptrZeichen+z) = '\0';
    
                return ptrZeichen;
            }
            else
            {
                *(ptrZeichen+z) = zeichen[0];
                z++;
            }
        }
    
        return ptrZeichen;
    }
    

    deswegen nahm ich an dass kein neues malloc() wirklich von nöten wäre für das free()-statement da ich doch eig. nur weiterhin mit der Adresse spiele die den Modellnamen beinhaltet.



  • @Th69 alles klar vielen Dank. 🙂 dann les ich mir das mal durch und hantier mal etwas mit dem new im konstruktor rum 🙂



  • Ob einem der Speicher nur nicht gehört oder ob er "wirklich gelöscht" wurde, kann in C++ nicht festgestellt werden. Was auch immer "wirklich gelöscht" bedeuten soll.



  • @TGGC sagte in DELETE() - Wird das Objekt wirklich gelöscht?:

    Ob einem der Speicher nur nicht gehört oder ob er "wirklich gelöscht" wurde, kann in C++ nicht festgestellt werden. Was auch immer "wirklich gelöscht" bedeuten soll.

    na dass das Objekt und alles was dazu gehört nicht mehr existiert. Wenn durch den DELETE Befehl doch nur der Speicher freigegeben wird aber nicht geleert wird, würde das doch unweigerlich heißen dass je nach Größe meines Speichers bei unendlicher Instanziierung von Objekten ohne das NEW-Statement irgendwann das Programm abschmiert da es keinen freien Speicher mehr gibt da dieser dann ja erst bei Beendigung des Programms freigegeben werden würde.



  • Die "korrekte" Lösung wäre, in deinem Auto einen std::string für das Modell zu nehmen und dich nicht von Hand um Speicher zu kümmern. Damit würdest zu zwar nicht die Aufgabe lösen, aber besseren Code (das heißt: korrekten, kürzeren und einfacheren Code) erhalten.

    Ein Auto soll sich um Auto-Eingeschaften kümmern, nicht um Anlegen/Verwalten von char-Pointern. Sag das deinem Lehrer.

    In dieser Aufgabe ist also wohl gefordert, den String in selbst verwalteten Speicher zu kopieren (im Konstruktor und bei setModell). Aber was passiert bei setModell("Kurzname") gefolgt von setModell("neuer längerer Name") - auch hier musst du nun schon wieder irgendwie mit Speicher hantieren, anstelle dich um dein Auto zu kümmern.

    Manuelle Speicherverwaltung führt ins Chaos. Immer.



  • @StudiengangPanik sagte in DELETE() - Wird das Objekt wirklich gelöscht?:

    na dass das Objekt und alles was dazu gehört nicht mehr existiert. Wenn durch den DELETE Befehl doch nur der Speicher freigegeben wird aber nicht geleert wird, würde das doch unweigerlich heißen dass je nach Größe meines Speichers bei unendlicher Instanziierung von Objekten ohne das NEW-Statement irgendwann das Programm abschmiert da es keinen freien Speicher mehr gibt da dieser dann ja erst bei Beendigung des Programms freigegeben werden würde.

    Nein, das heisst es nicht. Falls neue Objekte Speicher benutzen, der nicht "geleert" wurde oder wenn das Objekt irgendwann später noch vor dem Moment "geleert" wird, wo der physikalische Speicher ausgeht, stimmt deine Annahme nicht.



  • Vielleicht sollte man bei Speicher nicht von leer / nicht-leer sprechen, sondern von verwaltet und unverwaltet (oder hat jemand noch bessere Ideen?). Mit delete oder free "leerst" du nicht, sondern sagst, dass dieser Speicher jetzt wieder anderweitig verwendet werden kann. Die einfachste Operation für den Speicher ist, einfach nichts zu tun, d.h. wenn ein Wert drin stand, bleibt der unverändert stehen. Nur irgendwo anders wird in irgendwelchen Metadaten gespeichert, dass dieser Speicher nun anderweitig verwendet werden kann. Daher kann es passieren, dass du auch nach dem delete/free immer noch den korrekten Wert liest, aber der Speicher kann inzwischen auch anderweitig verwendet sein.



  • @wob sagte in DELETE() - Wird das Objekt wirklich gelöscht?:

    Nur irgendwo anders wird in irgendwelchen Metadaten gespeichert, dass dieser Speicher nun anderweitig verwendet werden kann.

    In welchen Metadateien sollte das sein, dafür gibt es die MMU die das Adressmapping macht. so kann jedes Programm mir jeder beliebigen Adresse im Speicher Starten denn die echte Adresse wo die Daten liegen wird erst bei Zugriff über die MMU ermittelt. Das ist dann auch der gleiche "einfache" Schutz warum man nicht so einfach in den Speicherbereich eines anderen Programmes greifen kann. Aber das führt hier eigentlich zu weit, aber im Grunde sollte man diese zusammenhänge kennen, wenn man Sinnvolöl was Programieren will, sonst geht man eben davon aus das in irgendwelchen Metadateien die Speicherbelegung abgelegt ist.
    Und nein dem TO hier in jedem 2. Post nahe zu legen das er doch std::string benutzen soll, wird für ihn eine weniger Zielführende Hilfe sein. Er soll sich eben mit der Speicherverwaltung auseinander setzen. Aber zum Schluss frag ich mich immer warum Studiert jemand Informatik wenn er sich vorm Studium noch nie mit dem Thema beschäftigt hat und jetzt schon bei nen 3 Zeiler Programm scheitert?

    In diesem Sinne
    Mazze


  • Mod

    @CTecS sagte in DELETE() - Wird das Objekt wirklich gelöscht?:

    @wob sagte in DELETE() - Wird das Objekt wirklich gelöscht?:

    Nur irgendwo anders wird in irgendwelchen Metadaten gespeichert, dass dieser Speicher nun anderweitig verwendet werden kann.

    In welchen Metadateien sollte das sein, dafür gibt es die MMU die das Adressmapping macht. so kann jedes Programm mir jeder beliebigen Adresse im Speicher Starten denn die echte Adresse wo die Daten liegen wird erst bei Zugriff über die MMU ermittelt

    Jedes malloc allokiert also mindestens eine ganze Speicherseite? Ich denke nicht.



  • @CTecS zitier mal bitte die Zeile in der steht dass ich Informatik studiere. Programmierung als Modul gibt es nicht nur im Informatikstudium, scheinst dich ja gut auszukennen. Abgesehen davon musste ich mich bis jetzt in C# nie so umständlich um die Speicherverwaltung kümmern wie in C oder C++ aber das ist nur meine Meinung und die Kenntnisse in C# sind auch nur auf Ausbildungsbasis da ich nie wirklich Interesse am Coden hatte aber es bleibt nunmal ein Pflichtmodul. ¯_(ツ)_/¯


  • Mod

    @StudiengangPanik sagte in DELETE() - Wird das Objekt wirklich gelöscht?:

    Abgesehen davon musste ich mich bis jetzt in C# nie so umständlich um die Speicherverwaltung kümmern wie in C oder C++

    Nee, deine Probleme sind 100% selbstgemacht. Du programmierst C mit new/delete, nicht C++ wie es gedacht ist. Du merkst, dass du korrektes C++ machst, daran dass du dich nicht mehr um Ressourcenverwaltung kümmern brauchst. Das ist natürlich nicht deine Schuld, dass dir das so beigebracht wird, aber es ist auch nicht Problem der Sprache, dass dein Lehrer sie vergewaltigt.



  • @StudiengangPanik sagte in DELETE() - Wird das Objekt wirklich gelöscht?:

    Abgesehen davon musste ich mich bis jetzt in C# nie so umständlich um die Speicherverwaltung kümmern wie in C oder C++

    Wahrscheinlich ist "manuelle Speicherverwaltung" und "zeigen, wie komplex C++ sein kann" das Ziel der Übung.

    Ansonsten würde man in C++ deine 28 Zeilen lange Funktion zeichenketteEinlesen() einfach durch ein getline ersetzen. Siehe Beispiel hier: https://en.cppreference.com/w/cpp/string/basic_string/getline

    Oder du würdest den operator>> verwenden, weil du dann beliebige Dinge einlesen könntest und nicht nur Zeichenketten.



  • Puuuuuh.... zeichenketteEinlesen() gewinnt auch mühelos den Worst-Perfomance Award. Du reservierst für jedes Zeichen, das du eingibst, den Speicher neu. Ja, funktioniert, und ja, das passiert wahrscheinlich schneller als jeder Benutzer tippen kann, aber es ist halt schnarchlangsam, weil Speicheranforderungen teuer sind (teuer im Sinne von kostet CPU Zeit).
    Also wenn du wirklich bei der malloc Version bleiben möchtest/musst solltest du Speicher mit einer akzeptablen Länge anfordern (sagen wir 100 Zeichen) und erst dann reallokieren, wenn dieser Speicher voll ist. Wenn der Speicher voll ist reallokierst du neuen Speicher und gibst als Größe die bisherige Größe * Faktor an:

    char* zeichenketteEinlesen()
    {
        size_t capacity = 100;
        size_t size = 0;
    
        char* retval = malloc( capacity +1 ); // +1 für abschließende 0
        for( ;; )
        { 
          char ch = getchar();
          if( ch == '\n' )
          {
             retval[size] = 0;
             return retval;
          }
          if( size == capacity )
          {
             capacity *= 1.5;
             retval = reinterpret_cast<char*>( realloc( retval, capacity +1 ) ); // wieder +1 für abschließende 0
          }
          retval[size++] = ch;
       }
    }
    

    Es bleibt aber noch zu sagen, dass das ein Garant für üble Fehler bleibt. Funktionen, die dynamische Objekte erzeugen und den Besitz an den Aufrufer übertragen sorgen dafür, dass einem sowas in größeren Projekten um die Ohren fliegt, weil man sich irgendwann nicht mehr über die Besitzverhältnisse im Klaren ist. Für genau solche Dinge wurde std::string gemacht.

    PS:
    Ich habe ja noch die leise Hoffnung, dass der Dozent in Wirklichkeit ein C++ Crack ist und auf RAII hinauswill. Er versucht halt nur seine Kompetenz zu verschleiern.😂



  • Und was noch niemand zur Sprache gebracht hat:
    Beim Kopieren eines Autos fliegt dir das Alles wieder um die Ohren:

    int main()
    {
        Auto a1;
        a1.setModell( zeichenketteEinlesen() );
    
        // soweit ist noch alles ok. a1.modell zeigt auf die Adresse der Zeichenkette, die in zeichenketteEinlesen einge-
        // lesen ist und gibt sie in seinem Destruktor wieder frei. Das ist ok, weil der Speicher der Zeichenkette mit malloc
        // angefordert wurde.
        Auto a2 = a1;
    
       // Boom! Beim Kopieren der Objekte werden deren member kopiert, bei Zeigern einfach der Wert. Sowohl a1.modell 
       // als auch a2.modell zeigen jetzt auf die gleiche Adresse. Im Destruktor von a2 wird a2.modell per free() frei- 
       // gegeben, soweit ist alles ok. Im Destruktor von a1 wird a1.modell ebenfalls freigegeben, aber da a1.modell und
       // a2.modell auf die gleiche Adresse zeigen wird für die gleiche Adresse 2x free() aufgerufen => Undefined behaviour.
    }
    


  • @DocShoe sagte in DELETE() - Wird das Objekt wirklich gelöscht?:

    Puuuuuh.... zeichenketteEinlesen() gewinnt auch mühelos den Worst-Perfomance Award. Du reservierst für jedes Zeichen, das du eingibst, den Speicher neu. Ja, funktioniert, und ja, das passiert wahrscheinlich schneller als jeder Benutzer tippen kann, aber es ist halt schnarchlangsam, weil Speicheranforderungen teuer sind (teuer im Sinne von kostet CPU Zeit).
    Also wenn du wirklich bei der malloc Version bleiben möchtest/musst solltest du Speicher mit einer akzeptablen Länge anfordern (sagen wir 100 Zeichen) und erst dann reallokieren, wenn dieser Speicher voll ist. Wenn der Speicher voll ist reallokierst du neuen Speicher und gibst als Größe die bisherige Größe * Faktor an:

    ja, das dachte ich mir eig. auch bei der Aufgabenstellung dass das mehr Sinn macht aber Anforderung war dass wir jedes Zeichen den Speicher neu reservieren sollten mit der Begründung man wüsste ja nicht wie viele Zeichen der User eingibt und wenn man dann Speicher für 100 Zeichen reserviert hat und dieser nur 40 eingibt man zu viel Speicher verbraucht. Was teilweise plausibel für mich als Anfänger klingt und das ja auch nur als Übung gilt für malloc/realloc und pointer.

    Es bleibt aber noch zu sagen, dass das ein Garant für üble Fehler bleibt. Funktionen, die dynamische Objekte erzeugen und den Besitz an den Aufrufer übertragen sorgen dafür, dass einem sowas in größeren Projekten um die Ohren fliegt, weil man sich irgendwann nicht mehr über die Besitzverhältnisse im Klaren ist. Für genau solche Dinge wurde std::string gemacht.

    dann will ich mal hoffen dass mein Prof langsam mal dazu kommt. 😃

    PS:
    Ich habe ja noch die leise Hoffnung, dass der Dozent in Wirklichkeit ein C++ Crack ist und auf RAII hinauswill. Er versucht halt nur seine Kompetenz zu verschleiern.😂

    ehrlich gesagt keine Ahnung ^^ vertrauen zum Prof oder den Praktikumsleitern bauen die Aufgaben jedenfalls nicht auf. 😃


Anmelden zum Antworten