Programm stürzt nach korrekter Ausgabe ab.



  • @Swordfish Ich verstehe worauf ihr hinaus wollt. In den späteren Übungen werden Vektoren etc ja auch noch behandelt. Für die Prüfung möchte ich einfach alles nochmal durchkauen, was der Prof so von uns wollte. Und der Lerneffekt ist bei dieser Sache ja auch nicht ausgeblieben. 🙂



  • Und an @Wade1234: kapa wird doch in Zeile 8 erhöht. 🤔

    also in dem programm, das du (laut forensoftware) vor 4 stunden gezeigt hast, wurde kapa überhaupt nicht erhöht. ansonsten würde ich dir dringend empfehlen, irgendwie bücher über softwareengineering und objektorientierte programmierung zu lesen und durchzuarbeiten; das ist viel wichtiger, als c++ in allen einzelheiten zu kennen, bzw. "veraltetes" aber funktionierendes c++ ist besser, also "modernes" aber fehlerhaftes c++.



  • Doch sicher wurde es erhöht! Hier: help = new Student[kapa+=100];. Und das ist auch genau der Grund, warum man das nicht in einer Zeile machen sollte: man übersieht es einfach.



  • @Wade1234 sagte in Programm stürzt nach korrekter Ausgabe ab.:

    edit: und help musst du auch noch auf 0 (oder besser nullptr) setzen.

    Warum?

    Siehe auch meinen vorherigen Kommentar zu dieser Variable.



  • @Wade1234 sagte in Programm stürzt nach korrekter Ausgabe ab.:

    edit: und help musst du auch noch auf 0 (oder besser nullptr) setzen.

    Ja. Ist idiomatischer Unfug.



  • naja es kostet eben 2 taktzyklen, den nullzeiger zu setzen, aber verhindert evtl. ganz furchtbare programmfehler, weil das programm dann ganz sicher (100%) abstürzt, weshalb es sich lohnt, sich das setzen von nullzeigern anzugewöhnen. also soweit ich weiß...... 🙄

    edit: achja statt += in der eckigen klammer sollte man den wert der variable eine zeile vorher erhöhen. der compiler kann jeden noch so primitiven code optimieren, aber es leidet zumindest die lesbarkeit bzw. verständlichkeit darunter und manchmal versteht der compiler es auch nicht. also meiner erfahrung nach kann der compiler am besten optimieren, wenn du den code "maximal doof" programmierst, also für jede noch so blöde berechnung eine eigene ergebnisvariable erstellt wird usw.. das hat auch den vorteil, dass du mit einfachem copy paste codeteile von a nach b bewegen kannst, ohne dass da probleme auftauchen. aber der compiler optimiert es eben, wenn im quelltext 10000 mal hintereinander die gleiche berechnung durchgeführt wird.



  • @Wade1234 sagte in Programm stürzt nach korrekter Ausgabe ab.:

    weil das programm dann ganz sicher (100%) abstürzt, weshalb es sich lohnt, sich das setzen von nullzeigern anzugewöhnen. also soweit ich weiß......

    Ja. Ähm. Nee. Sowas crasht in der Regel auch beim Zugriff auf bereits freigegebenen Speicher wenn die Runtime nicht ganz Banane ist.

    @Wade1234 sagte in Programm stürzt nach korrekter Ausgabe ab.:

    aber der compiler optimiert es eben, wenn im quelltext 10000 mal hintereinander die gleiche berechnung durchgeführt wird.

    Das ist so 'ne Sache. In

    for (auto it = foo.begin(); it != foo.end(); ++it)
        // ... whatever.
    

    muss der Compiler eben nachweisen können daß sich foo.end() im Schleifenkörper nicht ändern tut.

    @Sedna sagte in Programm stürzt nach korrekter Ausgabe ab.:

    In den späteren Übungen werden Vektoren etc ja auch noch behandelt.

    Davon hat @SeppJ nicht geredet. Selber implementieren, nicht std::vector<> nutzen.



  • @Swordfish sagte in Programm stürzt nach korrekter Ausgabe ab.:

    Ja. Ähm. Nee. Sowas crasht in der Regel auch beim Zugriff auf bereits freigegebenen Speicher wenn die Runtime nicht ganz Banane ist.

    kann man sich denn darauf verlassen? soweit ich das mitbekommen habe, stürzt das programm erst nach korrekter ausgabe, und nicht schon beim zugriff auf den eigentlich ungültigen speicher ab. wenn du den nullzeiger dereferenzierst, bekommst du die fehlermeldung garantiert.



  • @Wade1234 sagte in Programm stürzt nach korrekter Ausgabe ab.:

    wenn du den nullzeiger dereferenzierst, bekommst du die fehlermeldung garantiert.

    Nein. Es gibt Plattformen wo das völlig legal ist.



  • @Wade1234 sagte in Programm stürzt nach korrekter Ausgabe ab.:

    naja es kostet eben 2 taktzyklen, den nullzeiger zu setzen,

    Der wichtige Punkt in meiner Aussage war der zweite: "Siehe auch meinen vorherigen Kommentar zu dieser Variable." Meine Aussage im referenzierten Punkt war, dass man den Scope dieser Variablen bitte unbedingt verkleinern soll! Wenn diese Variable nur da definiert ist, wo sie gebraucht wird, ist das =0-Setzen auch überhaupt nicht relevant, da diese Variable dann direkt danach aus dem Scope verschwindet.

    Wenn ich Code sehe, der nach jedem delete pointer ein pointer = NULL (ja, in solchem Code ist es sehr oft NULL oder 0, aber nur selten nullptr) macht, dann frage ich mich gerne, ob der Autor selbst nicht so recht wusste, wie lange die Variable denn noch lebt bzw. wer verantwortlich ist. Besondern überflüssig ist, noch im Destruktur als allerletzten Befehl eine Member-Pointervariable auf 0 zu setzen. Es geht dabei auch gar nicht um Taktzyklen (im letztgenannten Fall kann der Compiler das einfach wegoptimieren), sondern darum, dass es ein Befehl mehr ist, den man sich angucken muss. Und vor allem fragt man sich dann noch, ob man nun anderswo im Code auch noch ifs einbauen muss und da noch weitere deletes sind. Kurzum: dieses Pattern zeugt häufig davon, dass eine Klasse eben mehr tut als sie sollte und/oder dass Besitzverhältnisse unklar sind - und das ist die Ursache. Das Nullsetzen nach delete erlaubt hingegen doppelte deletes und ist Symptom der unklaren Besitzverhältnisse. (so sehe ich das zumindest)



  • @wob sagte in Programm stürzt nach korrekter Ausgabe ab.:

    Besondern überflüssig ist, noch im Destruktur als allerletzten Befehl eine Member-Pointervariable auf 0 zu setzen.

    Ne, da hast Du recht, das zeugt von Schizophrenie. Wenn der d-tor eines Objekts gelaufen ist, ist es tot. Und aus.



  • Warum nicht eine einfach verkettete Liste benutzen, anstatt das Array was man immer wieder erweitern muss? Also einfach jedem Student einen Zeiger auf den nächsten Student mitgeben. So ist man auf das leidliche umkopieren nich angewiesen, die Liste kann endlos Verlängert werden und man kann auch einen Student einfach aus der Liste werfen ohne alles wieder neu zusammen kopieren zu müssen. Ist wie ein vector nur ohne einen vector zu benutzen.


  • Mod

    Eine verkettete Liste wäre unmittelbar zwar von der Funktion her identisch, aber wir können ja schon etwas weiter denken, dass die typischen Anforderungen an eine Verwaltungsliste - die vielleicht noch in zukünftigen Aufgabenstellungen kommen - eher von einer arrayartigen Datenstruktur erfüllt werden, als von einer verketteten Liste. Naja, eigentlich passt keine der beiden Strukturen so richtig gut, aber beim Array hat man immerhin den wichtigen Indexzugriff, mit dem man viele Unpässlichkeiten halbwegs ausgleichen kann.

    Ich wage auch zu behaupten, dass eine verkettete Liste wesentlich komplizierter zu implementieren ist, und das wo der Fragesteller schon beim simplen dynamischen Array strauchelt.



  • @CTecS Weil die Aufgabenstellung nunmal ein Array vorschreibt.



  • @Sedna
    Probiere mal bitte folgenden Code aus, damit du an einem Beispiel die Probleme deiner Klasse Student siehst.

    #include <iostream>
    
    
    class Student{
    public:
        Student(){}
        Student(const char name[], const char vorname[], unsigned matnr, unsigned fachsem){
            this->name = name;
            this->vorname = vorname;
            this->matnr = matnr;
            this->fachsem = fachsem;
        }
    
        void Print() const
        {
            printf("Name: %s %s\n", this->vorname, this->name);
            printf("Matrikelnummer: %i\n", this->matnr);
            printf("Semester: %i\n\n", this->fachsem);
        }
    
    private:
        const char* name;
        const char* vorname;
        unsigned matnr;
        unsigned fachsem;
    };
    
    
    // Programm zur Demonstration von Zeiger spezifischen Fehlern.
    int main()
    {
    
        std::string Name, Vornahme;
        unsigned int MatNr;
        unsigned int FachSem;
    
        Name = "Kleber";        // Geht dank std::string.
        Vornahme = "Klaus";
        MatNr = 100243;
        FachSem = 1;
    
        Student S1(Name.c_str(), Vornahme.c_str(), MatNr, FachSem);
    
        Name = "Gausse";
        Vornahme = "Gundula";
        MatNr = 100244;
        FachSem = 1;
    
        Student S2(Name.c_str(), Vornahme.c_str(), MatNr, FachSem);
    
        // Was zeigt der folgende Code an?
        S2.Print();
    
        // Und was zeigt der folgende Code an?
        S1.Print();
    
        // Problem: Ein Zeiger ist wie ein Zeigefinger. Er zeigt auf etwas. Deine Klasse
        // Student besteht aus zwei Zeigern, deren Inhalt auf Klassen externe Daten zeigen.
        // Verändert man dieses externe Daten, so zeigt der Zeiger natürlich auf die neuen
        // Daten.
    
        //  Weiteres Beispiel:
        Name = "";
    
        // Was zeigt der folgende Code an?
        S2.Print();
    
        // Und was zeigt der folgende Code an?
        S1.Print();
    
        // Tipp:
        // Deine Klasse Student besteht aus Name, Vornahme, Matrikelnummer und Fachsemster.
        // Alles Variablen welche die Klasse besitzen muss.
        //
        // Daher nimm in der ersten Iteration als Stringdefinition char name[256] und kopiere
        // Strings mittels strcpy_s(),... Das ist zwar nicht das Gelbe vom Ei, bewahrt dich
        // aber vor so einigen Zeiger-Tücken. Und du kannst es immer noch als char* 
        // verkaufen, auch wenn dem genau genommen so nicht ist.
        //
        // Und natürlich ist std::string ein muss!
    
    
    
    
        // Fortgeschrittenes Thema. Auch Zeiger auf std::string Interna sind nie gut.
        // Übergibt man dem String 'Name' einen größeren String, so löscht std::string
        // den internen char* String (auf welchem Student.name zeigt) und allokiert
        // einen neuen internen größeren String. Dadurch zeigt Student.name ins
        // Nirvana.
    
        //Name = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
        //Name = "";
    
        //S1.Print();
    
    
        return 0;
    }
    


  • @Sedna sagte in Programm stürzt nach korrekter Ausgabe ab.:

    Was haltet ihr davon?

    class Verwaltung{
    public:
        Verwaltung(){
            allStud = new Student[kapa];
        }
        ~Verwaltung(){
            delete[] allStud;
            allStud = 0;
        }
    
        newStud(const char name[], const char vorname[], unsigned matnr, unsigned fachsem){
            ...
        }
    
    private:
        Student *allStud;
        Student *help;
        int index = 0;
        int kapa = 100;
    };
    

    Deine Verwaltung besitzt also Resourcen (das allStud Array). Diese werden auch im Destruktor wieder freigegeben. Dazwischen passiert Unfug aber darauf sind andere ja schon eingegangen.

    Was jetzt noch ein Problem ist: Verwaltung ist kopierbar. Da du es nicht verhinderst/verbietest, erzeugt bekommst du gratis vom Compiler einen Kopierkonstruktor und einen Zuweisungsoperator spendiert. Die machen aber nicht das was du willst, denn sie kopieren einfach alle Member. D.h. wenn du sowas schreibst...

    void fun() {
        Verwaltung v1;
        // v1 verwenden
        Verwaltung v1 = v2;
    } // kaboom
    

    ...dann kompiliert das wunderschön ohne Fehler, aber crasht vermutlich beim Ausführen. Weil nämlich v2 als flache 1:1 Kopie von v1 erstellt wird, d.h. es werden einfach alle Member kopiert. D.h. der v2.allStud hat den selben Wert wie v1.allStud. D.h. spätestens wenn beide Objekte zerstört werden und versuchen "ihren" Speicher freizugeben, passiert ein Fehler -- denn beim Zerstören des 2. Objekts ist der Zeiger ja schon ungültig, er wurde ja schon freigegeben.

    Der einfachste Fix ist die Klassen non-copyable und non-assignable zu machen.

    Google:
    rule of three
    rule of five
    rule of zero



  • @Wade1234 sagte in Programm stürzt nach korrekter Ausgabe ab.:

    naja es kostet eben 2 taktzyklen, den nullzeiger zu setzen, aber verhindert evtl. ganz furchtbare programmfehler, weil das programm dann ganz sicher (100%) abstürzt, weshalb es sich lohnt, sich das setzen von nullzeigern anzugewöhnen. also soweit ich weiß...... 🙄

    Nicht sehr weit wie es aussieht. Blöd nämlich dass der Compiler schlau ist und sieht dass er das mal locker flockig "as if" wegoptimieren kann. Weil nach der Zuweisung in einem konformen "well formed" C++ Programm einfach kein Zugriff auf die Variable mehr kommen kann.

    Siehe
    https://godbolt.org/z/gmlTwY

    int* dummy = new int(123);
    
    class Foo {
    public:
        Foo() : p(new int()) {}
        ~Foo() {
            delete p;
            p = dummy;
        }
    
        int* p = nullptr;
    };
    
    void delFoo(Foo* f) {
        delete f;
    }
    

    =>

    delFoo(Foo*):
            test    rdi, rdi
            je      .L1
            push    rbp
            mov     rbp, rdi
            mov     rdi, QWORD PTR [rdi]
            test    rdi, rdi
            je      .L3
            mov     esi, 4
            call    operator delete(void*, unsigned long)
    .L3:
            mov     rdi, rbp
            mov     esi, 8
            pop     rbp
            jmp     operator delete(void*, unsigned long)
    .L1:
            ret
    _GLOBAL__sub_I_dummy:
            sub     rsp, 8
            mov     edi, 4
            call    operator new(unsigned long)
            mov     DWORD PTR [rax], 123
            mov     QWORD PTR dummy[rip], rax
            add     rsp, 8
            ret
    dummy:
            .zero   8
    

    Wie man hier sehr leicht sehen kann, ist die Zuweisung p = dummy; im generierten Assemblercode einfach weg.

    In Debug Builds sieht es anders aus. Macht die Sache aber nur noch schlimmer, denn dann hast du u.U. ein Programm das im Debug funktioniert und im Release crasht. Und keiner weiss wieso.

    Wenn du wirklich den Zeiger ala "darf zwar nicht passieren aber sicher ist sicher" auf NULL setzen willst, dann musst du schon Kunstgriffe wie volatile bemühen.


  • Mod

    Ist halt im Endeffekt sowieso alles hinfällig, weil man - wie in diesem Thread schon so oft gesagt - sowieso niemals mit Pointern auf diese Art und Weise herumfrickeln würde, sondern immer eine Art von Smartpointer oder anderen Ressourcenhalter benutzen würde. Deren interne Richtigkeit relativ einfach sicherzustellen ist, da die Komplexität überschaubar ist. Danach ist dann alles über das übliche Lebenszeitmanagement des Halterobjekts geregelt und die einzige Art und Weise, wie das dann überhaupt noch theoretisch schief gehen könnte, wäre der unvorstellbare Fall, dass der Compiler selbst defekt ist.

    Außer natürlich, man hat es mit einem Lehrer zu tun, der programmiert, als wäre es 1972. Dabei wurde C++ buchstäblich erfunden, damit man eben nicht so zu programmieren braucht.

    Diese dumme Aufgabenstellung reizt mich so sehr, dass ich geneigt bin, dem TE einfach die Hausaufgaben zu machen, aber eben mit einem selbstgeschriebenem Vector als unterliegender Datenstruktur...



  • @hustbaer sagte in Programm stürzt nach korrekter Ausgabe ab.:

    Wenn du wirklich den Zeiger ala "darf zwar nicht passieren aber sicher ist sicher" auf NULL setzen willst, dann musst du schon Kunstgriffe wie volatile bemühen.

    dass der compiler das erstmal als unnötig rausoptimiert, ist klar, weil danach ja nichts mehr passiert. wenn du das programm aber irgendwann mal erweiterst und dann weiter unten den nullzeiger dereferenzierst, bleibt das doch im code drin und das programm stürzt bei gängigen betriebssystemen ab.



  • @Wade1234 Ach du meinst wenn man aus welchem Grund auch immer die grässliche Variante ganz ohne Smart-Pointer macht, wo es dann Klassen gibt die mehr als eine Resource besitzen bzw. "nebenher" auch noch was anderes machen? Ja, in dem Fall sollte man das machen. Nur ... sollte man so halt generell nicht programmieren. Ganz egal ob (vordefinierte) Smart-Pointer jetzt erlaubt sind oder nicht - dann schreibt man sich halt ggf. nen einfachen Smart-Pointer schnell selbst (muss ja meist nicht viel können).

    Und selbst definierte Smart-Pointer zu verbieten kann mMn. nur einen Grund haben: den Leuten vor Augen zu führen wie scheisse alles wird wenn man sie nicht verwendet. z.B. indem man sie erstmal den Code schreiben lässt, dann zeigt was bei ihrem Code alles schief gehen kann, und ihnen dann zeigt wie man es ohne Smart-Pointer fixt, wie ekelig der Code dadurch wird und wie einfach und elegant es geht wenn man Smart-Pointer verwendet. (Und Smart-Pointer steht hier natürlich stellvertretend für sämtliche Resourcen bzw. generell Dinge wo ein "undo" für jedes "do" erforderlich ist.)


    Ich dachte wirklich du meinst das direkt so im Dtor als letzte Anweisung, und in Klassen wo klar ist dass sie nicht erweitert werden. Mit Kommentaren dabei ala "prevent problems with double deletion" oder so. Und das mag OK sein wenn man mal um nen Compiler-Bug rumarbeiten muss, aber generell halte ich es halt für ne doofe Idee.


Anmelden zum Antworten