Klasse und Frage zum Konstruktor



  • CStoll schrieb:

    Der Ansatz ist schon gut, aber:
    - Member-Variablen sollten privat sein und nur über Methoden der Klasse geändert werden

    Ich hab jetzt mal zwei neue Methoden eingefügt; changeWagenstatus und changeKilometerstand.

    CStoll schrieb:

    - für wagenfarbe hätte ich einen std::string verwendet, für wagenstatus einen enum (oder einen anderen Namen)

    Ich bin das noch so aus C gewohnt; sorry...

    CStoll schrieb:

    - die Parameter-Übergabe und Rückgabe hat etwas Java-artiges

    Was meinst du damit? Ich kenn Java nicht...

    CStoll schrieb:

    PS: Was die Zufallswerte angeht: srand() ruft man nur einmal au, nicht vor jeder Zuallszahl.

    srand hab ich jetzt nur einmal drin!

    Hier mein neuer Code:

    #include<iostream>
    #include<ctime>
    #include<string>
    using namespace std;
    
    class Auto
    {
    private:
    	string wagenfarbe;
    	int kilometerstand;
    	bool wagenstatus;
    
    public:
    	Auto(int zaehlerstand, string farbe, bool status)
    	{
    		kilometerstand = zaehlerstand;
    		wagenfarbe = farbe;
    		wagenstatus = status;
    	}
    
    	string getFarbe()
    	{
    		return wagenfarbe;
    	}
    
    	bool istKaputt()
    	{
    		return wagenstatus;
    	}
    
    	int getZaehlerstand()
    	{
    		return kilometerstand;
    	}
    
    	int changeKilometerstand(int zaehlerstand)
    	{
    		kilometerstand = zaehlerstand;
    
    		return kilometerstand;
    	}
    
    	int changeWagenstatus(bool status)
    	{
    		wagenstatus = status;
    
    		return wagenstatus;
    	}
    };
    
    Auto fahrOffRoad(Auto automobil)
    {
    	int prozent, zaehlerstand_erhoehung, zaehlerstand;
    
    	srand(time(NULL));
    	prozent = rand() % 100 + 1;
    
    	if(prozent > 50)
    	{
    		automobil.changeWagenstatus(true);
    	}
    
    	zaehlerstand_erhoehung = rand() % 100 + 1;
    	automobil.changeKilometerstand(zaehlerstand_erhoehung + automobil.getZaehlerstand());
    
    return automobil;
    }
    
    int main()
    {
    	Auto deinAuto(1000, "Rot", false);
    	//1000 km Zählerstand
    	//Farbe ist Rot
    	//Kaputt ist false, also nicht Kaputt (wenn false, dann Auto nicht kaputt)
    	Auto meinAuto(0, "Schwarz", false);
    
    	cout << "Mein Auto ist " << meinAuto.getFarbe() << " lackiert\n";
    	cout << "Dein Auto ist " << deinAuto.getFarbe() << " lackiert\n";
    
    	//Funktion die mit x Prozentiger Wahrscheinlichkeit das Auto kaputt macht
    	//und den Zählerstand um y Kilometer erhöht
    	meinAuto = fahrOffRoad(meinAuto);
    	deinAuto = fahrOffRoad(deinAuto);
    
    	if(deinAuto.istKaputt())
    	{
    		cout << "Dein Auto ist Kaputt gegangen :)\n";
    	}
    
    	if(meinAuto.istKaputt())
    	{
    		cout << "Mein Auto ist Kaputt gegangen :(\n";
    	}
    
    	cout<< "Die Zaehlerstaende lauten:\n";
    	cout<< "Mein Auto: " << meinAuto.getZaehlerstand() << " km\n";
    	cout<< "Dein Auto: " << deinAuto.getZaehlerstand() << " km\n";
    
    return 0;
    }
    

    Das Problem, welches ich vorhin geschildert habe, dass die Werte nur unterschiedlich sind, wenn ich das Programm schritweise durchmach, besteht immer noch. Ich verstehe das überhaupt nicht!



  • Ich hab jetzt mal zwei neue Methoden eingefügt; changeWagenstatus und changeKilometerstand.

    Ich hätte da eher Funktionen erzeugt, die das ausdrücken was mit dem Wagen passieren soll: void fahre(int strecke); bzw. void beschaedige(); / void repariere();

    vip@r schrieb:

    CStoll schrieb:

    - für wagenfarbe hätte ich einen std::string verwendet, für wagenstatus einen enum (oder einen anderen Namen)

    Ich bin das noch so aus C gewohnt; sorry...

    Und dann die manuelle Kopierschleife ohne Rücksicht auf Null-Terminatoren *tststs*

    CStoll schrieb:

    - die Parameter-Übergabe und Rückgabe hat etwas Java-artiges

    Was meinst du damit? Ich kenn Java nicht...

    Ich habe mich auch nicht intensiv mit Java beschäftigt, aber dort ist es afair üblich, daß die Objekte unveränderlich sind und bei Bedarf modifizierte Kopien zurückgeben. In C++ kenne ich das eher so, daß man die Objekte per Referenz übergibt und direkt beeinflußt:

    void fahrOffRoad(Auto& automobil)
    {
        int prozent, zaehlerstand_erhoehung;
    
        prozent = rand() % 100 + 1;
    
        if(prozent > 50)
        {
            automobil.beschaedige();
        }
    
        zaehlerstand_erhoehung = rand() % 100 + 1;
        automobil.fahre(zaehlerstand_erhoehung);
    }
    

    CStoll schrieb:

    PS: Was die Zufallswerte angeht: srand() ruft man nur einmal au, nicht vor jeder Zuallszahl.

    srand hab ich jetzt nur einmal drin!

    Dort wird es immer noch bei jedem Funktionsaufruf ausgeführt. Setz den Aufruf lieber an den Anang der main().



  • Dort wird es immer noch bei jedem Funktionsaufruf ausgeführt.

    Du meintest vorhin wohl das rand. Ich hab das srand gemeint; schön aneinander vorbei geredet...

    Aber wenn ich das rand an den Anfang der main schreibe, dann wird es ja nur einmal ausgeführt, oder? Ich möchte aber doch für die beiden Aufrufe der Funktion fahrOffRoad() unterschiedliche rand-Werte!

    Oder versteh ich da was grad nicht?

    Edit: Jetzt hab ich verstanden! Du meintest schon srand... srand soll tatsächlich nur 1x im Programm initialisiert werden! Die Zeit läuft ja weiter 🙂



  • vip@r schrieb:

    Dort wird es immer noch bei jedem Funktionsaufruf ausgeführt.

    Du meintest vorhin wohl das rand. Ich hab das srand gemeint; schön aneinander vorbei geredet...

    Ich meinte schon das srand() 😉

    Edit: Jetzt hab ich verstanden! Du meintest schon srand... srand soll tatsächlich nur 1x im Programm initialisiert werden! Die Zeit läuft ja weiter 🙂

    Wenigstens bei einem Thema ist der Groschen gefallen 😃



  • Hey Leute!

    Kann vielleicht nochmal jemand eine ähnliche "Übungsaufgabe" posten? Hat ja das letzte Mal recht geklappt. Würd mich freun!

    Ich hab hier mal eine Klasse schreiben wollen, die einen komplexen Datentyp zur Verfügung stellt. Hat aber nicht so ganz geklappt:

    #include<iostream>
    using namespace std;
    
    class data_komplex
    {
    private:
    
    	struct komplex
    	{
    		int real;
    		int imag;
    	};
    
    public:
    
    	data_komplex(komplex data)
    	{
    		data.imag = 0;
    		data.real = 0;		
    	}
    };
    
    int main()
    {
    	data_komplex data;
    
    	data_komplex(data);
    
    return 0;
    }
    


  • Hm, der Lernerfolg war wohl ... sagen wir mal bescheiden 🙂

    Um auf deinen zuletzt geposteten code einzugehen:
    Es gibt mehrere Fehler, die du allerdings eigentlich alle selber lösen können solltest bzw. erkennen solltest.
    Deine Aussage "Hat aber nicht so ganz geklappt" heisst wohl schlicht und einfach: compiliert nicht.

    Allerdings wird dein compiler dir sicherlich Fehlermeldungen präsentiert haben. Du solltest anfangen, diese zu lesen und vorallem auch zu verstehen.

    Vermutlich wird er erstmal meckern, dass kein Standardkonstruktor für die Klasse data_komplex vorhanden ist.
    Jetzt solltest du dir erstens überlegen, warum er überhaupt einen Standardkonstruktor erwartet, warum er keinen bereitstellt und dann schaust du dir deine Klasse an und überlegst, was für einen Konstruktor du tatsächlich hast.

    Nächster Schritt ist zu überlegen, wie man also ein data_komplex-Objekt erzeugt mit dem vorhandenen Konstruktor. Wenn du das versuchst, wirst du auf den nächsten Fehler stossen bzw. der compiler wird wieder meckern. Aber eins nach dem anderen.
    😃



  • 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