Klasse und Frage zum Konstruktor



  • Ich hab hier dann mal nochmal was verändert. Jetzt funktioniert das soweit.

    #include<iostream>
    using namespace std;
    
    class komplex
    {
    private:
    	int real;
    	int imag;
    
    public:
    	komplex()		//Konstruktor
    	{
    		real = 0;
    		imag = 0;
    	}
    
    	~komplex()		//Destruktor; was könnte der hier für eine Funktion haben?
    	{
    
    	}
    
    	void changeKomplex()
    	{
    		cout << "Neuer Realteil: " ; cin >> real;
    		cout << "Neuer Imaginärteil: "; cin >> imag;		
    	}
    
    	void getReal_Imag(komplex &zahl)
    	{
    		cout << zahl.real << "+" << imag << "i" << endl;
    	}
    };
    
    void showKomplex(komplex zahl)
    {
    	zahl.getReal_Imag(zahl);
    }
    
    int main()
    {
    	komplex data1, data2;		//Klassendeklaration
    
    	data1.changeKomplex();
    	data2.changeKomplex();
    
    	showKomplex(data1);
    	showKomplex(data2);
    
    return 0;
    }
    

    Ich frage mich nun nur schon die ganze Zeit, was der Destruktor in diesem Fall macht? Könnt ihr mir helfen? Ist das Programm soweit ok?



  • Ich wusste gar nicht, dass man bei einer so primitiven Klasse so viel falsch machen kann.



  • 314...

    Meinst du meinen neuen oder den alten Code?



  • vip@r schrieb:

    Ich hab hier mal eine Klasse schreiben wollen, die einen komplexen Datentyp zur Verfügung stellt.

    Es wirkt auf mich so, als wuerdest du versuchen eine Klasse zu schreiben ohne zu wissen was du genau damit machen willst. Meine "Aufgabe" vorhin ist den anderen Weg gegangen: ich habe ein Ziel und dort will ich hin. Die Klasse ist der Weg dorthin. Wenn du nun kein Ziel hast, dann wird die Klasse schwammig. Dann tust du dir schwer die Klasse richtig zu implementieren weil du selber nicht genau weisst was du tun sollst. Das Auto Beispiel war zB bis auf das srand (was ja mit der Klasse ansich nichts zu tun hat) ordentlich. Dein Komplex hier ist leider garnichts.

    Also definiere dir eine Aufgabenstellung:

    Schreib ein Programm, dass 2 Brueche einliest und diese dann miteinander multipliziert, dividiert, subtrahiert und addiert. Und gib alle 4 Ergebnisse in Bruchform aus. bzw. kannst du das natuerlich auch mit komplexen Zahlen statt Bruechen machen wenn du in der Mathematik fit genug bist.

    Im Prinzip spielt es keine grosse Rolle was du als Ziel definierst. Die Klasse ist dann der Weg dorthin. uU brauchst du auch mal mehrere Klassen. Aber wichtig ist: ein klares Ziel.



  • vip@r schrieb:

    Ich frage mich nun nur schon die ganze Zeit, was der Destruktor in diesem Fall macht? Könnt ihr mir helfen? Ist das Programm soweit ok?

    Theoretisch: Der Destruktor zerstört als erstes die Klassen-Elemente (indem er deren Destruktoren aufruft), danach die Basisklasse(n) (genauso) und führt anschließend den Code im Destruktor-Rumpf aus.
    Praktisch: Die Member zu zerstören ist trivial, Basisklasse hast du nicht - und der Rumpf ist auch leer - also macht dieser Destruktor *trommelwirbel* gar nichts. Überhaupt ist ein Destruktor mit leerem Rumpf in den meisten Fällen* unnötig - den kann der Compiler genausogut selber aufbauen, ohne sich auf deine Mithilfe zu verlassen.

    *der einzige Sinn, der mir auf Anhieb dafür einfällt, ist die Definition eines virtuellen Destruktors.



  • Mir ist schon klar, dass der Rumpf des Destruktors momentan noch leer ist und somit auch nichts zerstören kann. Aber er ist ja deshalb noch leer weil ich eben nicht wüsste, was ich da zerstören könnte. Deshalb ich gefragt 🙂



  • Hier mal der Code für die Bruch-Aufgabe:

    #include<iostream>
    using namespace std;
    
    class bruch
    {
    private:
    	int nenner;
    	int zaehler;
    
    public:
    	bruch()
    	{
    		nenner = 1;
    		zaehler = 1;
    	}
    
    	void setZaehler_Nenner(int z, int n)
    	{
    		zaehler = z;
    		nenner = n;
    	}
    
    	void showNenner_Zaehler(bruch zahl)
    	{
    		cout << zahl.zaehler << "/" << zahl.nenner;
    	}
    
    	int getNenner(bruch zahl)
    	{
    		return zahl.nenner;
    	}
    
    	int getZaehler(bruch zahl)
    	{
    		return zahl.zaehler;
    	}
    };
    
    void eingeben(bruch &zahl)
    {
    	int zaehler, nenner;
    
    	cout << "Zaehler eingeben: "; cin >> zaehler;
    	cout << "Nenner eingeben: "; cin >> nenner;
    
    	zahl.setZaehler_Nenner(zaehler, nenner);
    }
    
    void showBruch(bruch zahl)
    {
    	zahl.showNenner_Zaehler(zahl);
    }
    
    bruch multiplikation(bruch zahl1, bruch zahl2)
    {
    	int nenner, nenner1, nenner2, zaehler, zaehler1, zaehler2;
    	bruch zahl;
    
    	nenner1 = zahl1.getNenner(zahl1);
    	nenner2 = zahl2.getNenner(zahl2);
    	zaehler1 = zahl1.getZaehler(zahl1);
    	zaehler2 = zahl2.getZaehler(zahl2);
    
    	nenner = nenner1 * nenner2;
    	zaehler = zaehler1 * zaehler2;
    
    	zahl.setZaehler_Nenner(zaehler, nenner);
    
    return zahl;
    }
    
    bruch addition(bruch zahl1, bruch zahl2)
    {
    	int nenner, nenner1, nenner2, zaehler, zaehler1, zaehler2;
    	bruch zahl, zahl_erweitert1, zahl_erweitert2, erweitern1, erweitern2;
    
    	nenner1 = zahl1.getNenner(zahl1);
    	nenner2 = zahl2.getNenner(zahl2);
    	zaehler1 = zahl1.getZaehler(zahl1);
    	zaehler2 = zahl2.getZaehler(zahl2);
    
    	erweitern1.setZaehler_Nenner(nenner2, nenner2);
    	erweitern2.setZaehler_Nenner(nenner1, nenner1);
    
    	zahl_erweitert1 = multiplikation(zahl1, erweitern1);
    	zahl_erweitert2 = multiplikation(zahl2, erweitern2);
    
    	zaehler1 = zahl_erweitert1.getZaehler(zahl_erweitert1);
    	zaehler2 = zahl_erweitert2.getZaehler(zahl_erweitert2);
    
    	nenner1 = zahl_erweitert1.getNenner(zahl_erweitert1);
    	nenner2 = zahl_erweitert2.getNenner(zahl_erweitert2);
    
    	zaehler = zaehler1 + zaehler2;
    	nenner = nenner1;
    
    	zahl.setZaehler_Nenner(zaehler, nenner);
    
    return zahl;
    }
    
    int main()
    {
    	bruch zahl1, zahl2;
    
    	eingeben(zahl1);
    	eingeben(zahl2);
    	showBruch(multiplikation(zahl1, zahl2));
    	cout << endl;
    	showBruch(addition(zahl1, zahl2));
    
    return 0;
    }
    

    Ich weiß, das ist alles sehr sehr umständlich, aber so wie der Code jetzt da steht funktioniert immerhin schon mal die multiplikat und addition von zwei Brüchen (aber ohne Minus-Vorzeichen).



  • vip@r schrieb:

    Mir ist schon klar, dass der Rumpf des Destruktors momentan noch leer ist und somit auch nichts zerstören kann. Aber er ist ja deshalb noch leer weil ich eben nicht wüsste, was ich da zerstören könnte. Deshalb ich gefragt 🙂

    Bei Klassen, die nur aus eigenständigen Objekten bestehen, hat ein Destruktor nicht viel zu tun - damit kannst du dich beschäftigen, wenn du einen Grund für eigene Aufräumarbeiten in deinen Klassen hast.

    Zu deinem Bruch-Code: Die Addition sieht reichlich kompliziert aus, ansonsten fallen mir noch einige Optimierungen ein, die du auf später verschieben könntest.

    PS: Und als nächstes üben wir die Operator-Überladung 😃



  • Operator Überladung? Gerne... Hat mich sowieso schon interessiert. Also nur her mit den Informationen 🙂



  • Operator-Überladung bedeutet, daß du die bekannten C++ Operatoren auch mit deinen eigenen Datentypen einsetzen könntest. Das heißt, anstelle deiner Funktion bruch multiplikation(bruch,bruch); schreibst du einen bruch operator*(bruch,bruch); und kannst dann im Hauptprogramm per bruch1*bruch2 rechnen.

    PS: Übrigens würde deiner Klasse ein einziger expliziter Konstruktor ausreichen:

    bruch(int z=0,int n=1);
    

    Der erfasst dank Default-Argumenten auch die Default-Konstruktion (mit Wert 0 - ist passender als 1) und Umwandlung aus einer ganzen Zahl.



  • Du hast den Konstruktor nicht schoen benutzt. Man sollte zB einen Bruch direkt erstellen koennen:

    Bruch b(1,3);
    um einen Bruch mit dem Wert 1/3 anzulegen.

    Ansonsten ist der Code halt sehr umstaendlich und die Namensgebung tut weh :p

    Aber auch wenn wir keine operatoren Uerberladen, so gibt es doch einige Sachen die den Code besser machen wuerden (ich gehe nur auf das Design ein, dass deine Implementierung zu umstaendlich ist, weisst du ja selber - aber um die geht es hier ja nicht).

    Als erstes:

    zahl.showNenner_Zaehler(zahl);
    

    sowas sollte dir immer zu denken geben. Ich verkuerze es etwas: a.f(a);
    warum nicht nur a.f()?

    void showNenner_Zaehler() {
       cout<<zaehler<<" / "<<nenner;
    }
    

    zaehler und nenner sind hier ja bekannt, da showNenner_Zaehler ja Teil der Klasse Bruch ist und somit Zugriff auf die Member der Klasse hat. Innerhalb von Memberfunktionen hast du ja den impliziten this Zeiger, so dass der obrige Code auch so geschrieben werden koennte:

    void showNenner_Zaehler() {
       cout<<this->zaehler<<" / "<<this->nenner;
    }
    

    Bei Auto ist dir dieser Fehler nicht passiert. Das verwundert mich hier ziemlich. Weisst du uU was dich hier verwirrt hat?

    Funktionen wie zB kuerzen() (Brueche sollte man kuerzen koennen) gehoeren auf jedenfall in die Klasse. Auch showBruch() gehoert in die Klasse (wie du das ja mit show_ZaehlerNenner eh schon gemacht hast - nur wozu dann noch showBruch erstellen?)

    Momentan ist es so, dass dein Bruch nicht viel selber macht - er bietet nicht viel Abstraktion gegenueber 2 integers die du zaehler/nenner nennst. Wie man das zB sehr schoen in addition() sieht. Bruch fasst bei dir nur zaehler und nenner zu einem Paar zusammen. Besser waere aber, wenn Bruch sich wie eine echte Zahl anfuehlen wuerde, mit der man rechnen kann.

    Beispiel:

    Bruch multiplizieren(Bruch lhs, Bruch rhs) {
       Bruch result(lhs.getZaehler()*rhs.getZaehler(), lhs.getNenner()*rhs.getNenner());
       result.kuerzen();
       return result;
    }
    
    Bruch addieren(Bruch lhs, Bruch rhs) {
       int lhsNenner=lhs.getNenner();
       int rhsNenner=rhs.getNenner();
    
       lhs.erweitern(rhsNenner);
       rhs.erweitern(lhsNenner);
    
       Bruch result(lhs.getZaehler()+rhs.getZaehler(), lhs.getNenner());
       result.kuerzen();
       return result;
    }
    

    falls kuerzen zu kompliziert ist, kann man es weglassen.

    So sieht der Code schon besser aus und du stellst recht schnell fest dass du noch Funktionen wie kuerzen(), erweitern(), invertieren(), etc. brauchst.

    Auch sind subtraktion und division ganz simpel:

    Bruch subtrahieren(Bruch lhs, Bruch rhs) {
       return addieren(lhs, multiplizieren(rhs, -1));
    }
    
    Bruch dividieren(Bruch lhs, Bruch rhs) {
       rhs.invertieren();
       return multiplizieren(lhs, rhs);
    }
    

    Du baust dir solche Bausteine aus denen du dann deinen Code zusammen baust.
    Operator Ueberladung macht das ganze natuerlich syntaktisch schoener, weil du subtrahieren dann zB so schreiben kannst:

    Bruch operator-(Bruch lhs, Bruch rhs) {
       return lhs+(rhs*-1);
       //bzw: lhs+(-rhs)
    }
    

    Aber das ist nur syntax zucker und aendert nichts an der logik des Codes.



  • CStoll schrieb:

    vip@r schrieb:

    Ich frage mich nun nur schon die ganze Zeit, was der Destruktor in diesem Fall macht? Könnt ihr mir helfen? Ist das Programm soweit ok?

    Theoretisch: Der Destruktor zerstört als erstes die Klassen-Elemente (indem er deren Destruktoren aufruft), danach die Basisklasse(n) (genauso) und führt anschließend den Code im Destruktor-Rumpf aus.

    Nope, da hast du die Reihenfolge verwurstelt. Das wäre ja schlimm wenns so rum laufen würde.

    Korrekte Reihenfolge:

    1: Destruktor-Rumpf ausführen
    2: Member-Destruktoren aufrufen ("von unten nach oben")
    3: Basisklassen-Destruktoren aufrufen ("von rechts nach links")

    Also genau verkehrt rum wie konstruiert wird.



  • vip@r schrieb:

    So, ich frage mich nun schon seit geraumer Zeit, was es nun in Klassen immer mit diesen Konstruktoren bzw. Destruktoren auf sich hat. Was macht ein Konstruktor/Destruktor?

    In meinen Folien die ich zur Hand hab, steht, dass diese Konstruktoren Initialisierungsaufgaben übernehmen sollen. Aber für was? Wofür ist das gut? Könnt ihr mir helfen?

    Wenn Du eine Variable nicht initialisierst(ihr einen Wert zuweist), bekommt sie einen "zufälligen" Wert zugewiesen. Wenn die Variable aber z.B. den Ausgangswert 0 haben müßte, wären nachfolgende Berechnungen dann falsch.

    Ein Konstruktor hat ansonsten noch diverse andere Aufgaben. Z.B. die Reservierung von Speicher für ein Objekt. Der Destruktor wiederum hat unter anderem die Aufgabe, diesen Speicherbereich wieder freizugeben, wenn er nicht mehr benötigt wird.


Anmelden zum Antworten