Konstruktor Vererbung



  • Hey ihr lieben 🙂

    Tolles Forum habt ihr, hab' mich jetzt mal angemeldet um eine Frage zu stellen.

    Wir haben jetzt mit Vererbung in der Uni angefangen und ich habe ein Verständnisproblem bezüglich des Vererbens eines Konstruktor.

    Ich möchte eine Klasse Konto schreiben, die 3 Unterkonten (Giro, Tagesgeld, Spar) besitzt. Die Eigenschaften sind natürlich sehr ähnlich, alle haben einen Inhaber, einen Saldo, und haben bestimmte Methoden, z.b. ein- & auszahlen.

    Mein Problem ist nun, dass ich nicht weiß, ob ich direkt einen Konstruktor in der Oberklasse Konto erstellen soll, denn ich möchte garkein Objekt Konto erstellen. Ich habe mir aber überlegt, dass der Konstruktor bei allen Konten gleich aussieht und daher ihn doch vererbe, das is doch Sinn der Sache, oder nicht? Nun habe ich ein paar Sachen ausprobiert:

    * ohne Konstruktor in abgeleiteter Klasse Sparkonto, aber mit Konstruktor in Basisklasse
    hier sagt er natürlich bei

    Sparkonto konto2(123,"asdf");
    
    no matching function for call to ‘Sparkonto::Sparkonto(int, const char [5])’
    

    Irgendwie klar, denn es existiert ja tatsächlich kein Konstruktor "Sparkonto", lediglich ein Konstruktor "Konto".
    Kann ich so einen Konstruktor überhaupt vererben, oder muss ich so oder so einen eigenen für jede Klasse erstellen (mal abgesehen vom Standardkonstruktor)

    * Konstruktor in Abgeleiteter und in Basisklasse

    Dann erscheint dieser Fehler bei der Implementierung meines Konstruktors "Sparkonto"

    no matching function for call to ‘Konto::Konto()’
    

    * Habe dann gelesen, dass, wenn abgeleitete Klassen aufgerufen werden, und der Konstruktor der Basisklasse Parameter erwartet, diese mitgeliefert werden müssen. Also:

    Sparkonto::Sparkonto(unsigned int _kontonummer, string _inhaber):Konto(_kontonummer,_inhaber){
    

    Das funktioniert auch so.
    AAber, im Grunde brauche ich doch dann den Konstruktor meiner Basisklasse überhaupt nicht, denn die Konstruktoren meiner abgeleiteter Klassen werden dadurch doch auch nur komplizierter?

    Also ich wäre euch super dankbar, wenn mir das jemand auseinander dröseln könnte, wie das mit den Konstruktoren läuft und ob ich die tatsächlich immer neu coden muss 🙂

    Danke für die Antworten im Voraus!



  • Konstruktoren werden nicht vererbt.

    Wenn du jedoch in der Basisklasse Konto bereits einen Konstruktor einrichtest, kannst du diesen in Konstruktoren von abgeleiten Klassen aufrufen. Dadurch musst du in abgeleiteten Klassen keinen direkten Zugriff mehr auf die Membervariablen der Basisklasse haben, was die Kapselung erhöht.

    Konto::Konto(unsigned int kontonummer, const std::string& inhaber)
    : m_kontonummer(kontonummer) // das ist die Konstruktor-Initialisierungsliste
    , m_inhaber(inhaber)         // die m_... sind Membervariablen von Konto
    {
    }
    
    Sparkonto::Sparkonto(unsigned int kontonummer, const std::string& inhaber)
    : Konto(kontonummer, inhaber) // Argumente an Basisklassenkonstruktor weiterleiten
    {
    }
    

    Noch ein paar Anmerkungen:

    • Übergib Objekte grösserer Klassen (wie std::string ) als Const-Referenz, um nicht unnötig zu kopieren.
    • Achte darauf, dass du in Headerdateien kein using namespace einsetzt, da das den Namensraum für alle inkludierenden Dateien irreversibel offenlegt. Lieber einmal mehr std:: schreiben.
    • Bezeichner, die mit Unterstrich beginnen oder zwei aufeinanderfolgende Unterstriche enthalten, sind prinzipiell für Compiler und Implementierung reserviert (okay, ein Unterstrich und Kleinbuchstaben ist nicht in allen Fällen schlimm, aber besser, man vermeidet allfällige Probleme).


  • Ganz herzlichen Dank 🙂 auch für die Anmerkungen unten!

    Das heißt also, dass ich durchaus aber den Konstructor in jede class Deklaration schon neu mit reinschreibe wie immer und dann bei der Implementation einfach auf den Konstruktor der Basisklasse verwende, korrekt?

    Was meinteste mit besser Kapselung? Muss z.B. mein Saldo, das im Konstruktor gesetzt wird nicht mehr protected sein? Ah wobei höchstwahrscheinlich schon, denn ich möchte ja später mit der Methode Geld einzahlen (die befindet sich aber in Sparkonto) und damit saldo erhöhen, korrekt?

    Schöne Grüße 🙂



  • Ich bins nochmal, es scheint, als könnte ich meine Beiträge nicht editieren..

    Jedenfalls schreibst du auch, dass ich möglichst keine Unterstriche vor die Parameter machen. Zwar waren die jetzt vorgegeben, aber grundsätzlich ist das verständlich. Kann ich für die Parameter die selben Namen verwenden wie für die lokalen Variablen, wenn ich nen This Zeiger benutze, oder ist das bei Klassen nicht sinnvoll oder so?

    Danke 🙂



  • deadball schrieb:

    Ganz herzlichen Dank 🙂 auch für die Anmerkungen unten!

    Das heißt also, dass ich durchaus aber den Konstructor in jede class Deklaration schon neu mit reinschreibe wie immer und dann bei der Implementation einfach auf den Konstruktor der Basisklasse verwende, korrekt?

    Ich verstehe da jetzt nicht ganz, was du meinst, aber in jeder Klasse, die von der Basisklasse erbt kannst/musst du einen Konstruktor der Basisklasse aufrufen. Wenn kein Default Konstruktor vorhanden ist, musst du die Parameter explizit angeben, wie es Nexus ja zeigt. Du musst einfach nötigen Parameter übergeben.

    Was meinteste mit besser Kapselung? Muss z.B. mein Saldo, das im Konstruktor gesetzt wird nicht mehr protected sein? Ah wobei höchstwahrscheinlich schon, denn ich möchte ja später mit der Methode Geld einzahlen (die befindet sich aber in Sparkonto) und damit saldo erhöhen, korrekt?

    Ja mit der Kapselung meinte er, dass die Basisklasse für seine Member verantwortlich ist und diese somit private sein können.

    Jedenfalls schreibst du auch, dass ich möglichst keine Unterstriche vor die Parameter machen. Zwar waren die jetzt vorgegeben, aber grundsätzlich ist das verständlich. Kann ich für die Parameter die selben Namen verwenden wie für die lokalen Variablen, wenn ich nen This Zeiger benutze, oder ist das bei Klassen nicht sinnvoll oder so?

    Ja, grundsätzlich ginge das. Allerdings werden Member einer Klasse üblicherweise mit einem Prä oder Postfix markiert. z.B

    class mensch
    {
     int m_alter; // präfix m_
     int groesse_; // postfix _
    };
    

    Dann ist das mit den gleichen Namen ja kein Problem mehr. Immer this-> vor einem Zugriff zu schreiben halte ich für unschön und redundant, wenn es nicht wirklich notwendig ist.

    Achja. Du hast dich vielleicht registiert, aber angemeldet bist du anscheinend nicht. 😉
    Dann kannst du deine Beiträge auch editieren.



  • So, jetzt bin ich auch angemeldet 😃

    Weshalb sind this Zeiger redundant?

    Das mit dem Konstruktor habe ich wohl falsch verstanden.. Wenn kein Standardkonstruktor vorhanden ist, also ein Konstruktor in der Basisklasse angegeben wurde, so muss ich IMMER diesen Konstruktor explizit also wieder per ":" angeben?
    Gibt es denn dann überhaupt den Fall, dass ich nicht explizit den den Konstruktor der Basisklasse angeben muss, oder bin ich grad verwirrt? 🙂



  • Der this Zeiger ist redundant, weil du ihn ja nicht schreiben musst, damit die richtigen Variablen ausgewählt werden. Es gibt vereinzelt Fälle, wo man ihn wirklich explizit braucht, mit fällt aber ausser den gleichen Namen nichts ein, aber ich dachte ich hätte mal so was gehabt.

    Du kannst natürlich selbst einen Default Konstruktor anbieten, wo du dann einfach mit Standardwerten initialisierst.

    class foo
    {
    int n_;
    std::string name_;
    
    public: 
      foo ():n_ ( 2 ), name_ ( "hans-peter" ) {}
      foo ( int n, std::string name ):n_ ( n ), name_ ( name ){}
    };
    

    man könnte den Konstruktor aber auch so schreiben:

    class foo
    {
    int n_;
    std::string name_;
    
    public: 
      explicit foo ( int n = 2, std::string name = "hans-peter" ):n_ ( n ), name_ ( name ){}
    };
    

    Das zweite ist default Initialisierung von Parametern. Wenn nichts anderes angegeben wurde, dann nimmt n 2 an und name "hans-peter". Das explicit ist dazu da, um eine automatische Konvertierung zu vermeiden. (für näheres zu explicit frag google, oder ein gescheites Buch).



  • jakap schrieb:

    Weshalb sind this Zeiger redundant?

    Naja, eigentlich (miss-)braucht man sie in diesem Beispiel nur als Workaround für den entstehenden Namenskonflikt. Ich finde es auch in anderen Kontexten praktisch, Membervariablen speziell kennzuzeichnen, und da kann ich gleich einen Nutzen aus meinen Bezeichnern ziehen.

    jakap schrieb:

    Wenn kein Standardkonstruktor vorhanden ist [...] so muss ich IMMER diesen Konstruktor explizit also wieder per ":" angeben?

    Ja.

    jakap schrieb:

    Gibt es denn dann überhaupt den Fall, dass ich nicht explizit den den Konstruktor der Basisklasse angeben muss, oder bin ich grad verwirrt? 🙂

    Das sagst du ja selber: Wenn die Basisklasse einen Standardkonstruktor anbietet und du ihn aufrufen willst. Ich persönlich halte es jedoch für gute Praxis, den Aufruf auch in diesem Fall explizit kennzuzeichnen.

    Verwechsle nicht Standardkonstruktor (Konstruktor mit null Parametern) und compiler-generierter Konstruktor.



  • Ihr seid unglaublich.

    Einen default Konstruktor zu schreiben wäre wohl etwas zu viel des Guten hier, aber gut dass es funktioniert.

    So noch eine Frage würde ich gerne stellen, dann schreib ich die Klasse zu Ende. Soll/kann ich sie danach hier rein stellen oder ist das eher zu viel?

    Naja, die Frage: Ist es möglich, dass ich nur bestimmte Teile der geerbten Methode ändere?

    Also ich hab' ne Methode "drucken" in "Konto", die den Kontotypen, den Inhaber, Saldo, etc. ausgeben. Jetzt kommt z.B. in Tagesgeldkonto noch ein Referenzkonto hinzu, das ausgegeben werden soll.
    Wie kann ich das realisieren? Muss ich in jeder abgeleiteten Klasse die Methode komplett neu schreiben, oder kann ich Teile der Methode übernehmen, ohne sie neu zu schreiben (nicht, dass das jetzt das Problem wäre, aber so hab' ich ja in meinem Fall 3 Mal X (hier sinds nur 4 oder so, aber was wenns länger wird?) Zeilen vollkommen identischen Code; möchte ja auch irgendwie eleganter und übersichtlich programmieren ;))?

    Danke für die prompten Antworten, hoffe ich geh' euch nicht auf die Nerven..

    Schöne Grüße



  • jakap schrieb:

    So noch eine Frage würde ich gerne stellen, dann schreib ich die Klasse zu Ende. Soll/kann ich sie danach hier rein stellen oder ist das eher zu viel?

    Kommt drauf an, wie gross sie ist. Es ist allerdings besser, konkrete Fragen zu haben, als einfach nur Code zu posten. 😉

    jakap schrieb:

    Naja, die Frage: Ist es möglich, dass ich nur bestimmte Teile der geerbten Methode ändere?

    Also ich hab' ne Methode "drucken" in "Konto", die den Kontotypen, den Inhaber, Saldo, etc. ausgeben. Jetzt kommt z.B. in Tagesgeldkonto noch ein Referenzkonto hinzu, das ausgegeben werden soll
    Wie kann ich das realisieren?

    Lagere die einzelnen Funktionalitäten (Ausgabe der jeweiligen Konti) in separate Funktionen in der Basisklasse aus. Wahrscheinlich empfiehlt sich hier protected , wenn man die Methoden nicht direkt von aussen aufrufen können soll, wohl aber in abgeleiteten Klassen.

    class konto
    {
        protected:
            void inhaber_ausgeben() const;
            void saldo_ausgeben() const;
    
        public:
            void konstostand_ausgeben() const
            {
                inhaber_ausgeben();
                saldo_ausgeben();
            }
    };
    
    class sparkonto : public konto
    {
        private:
            void referenzkonto_ausgeben() const;
    
        public:
            // Methode überschreiben
            void konstostand_ausgeben() const
            {
                inhaber_ausgeben();
                saldo_ausgeben();
                referenzkonto_ausgeben();
            }
    
            // Alternativ: Basisklassenversion explizit aufrufen
            void kontostand_ausgeben() const
            {
                konto::kontostand_ausgeben();
                referenzkonto_ausgeben();
            }
    };
    

    Wichtig: Ich habe an dieser Stelle auf Laufzeitpolymorphie verzichtet, weil du mit diesem Konzept womöglich noch nicht vertraut bist. Wenn du allerdings Objekte von abgeleiteten Klassen als Basisklasse ansprechen willst (als Abstraktionsmöglichkeit), möchtest du vielleicht die für den jeweiligen Kontotyp spezifischen Informationen ausgeben. Dazu müsstest du die Methode konstostand_ausgeben() als virtual deklarieren. Auch an den virtuellen Destruktor sollte dann gedacht sein.

    Aber du kannst dir ruhig Zeit lassen. Denk einfach dran, bei sowas:

    void funktion(const konto& k)
    {
        k.kontostand_ausgeben();
        // ...
    }
    
    int main()
    {
        sparkonto s;
        funktion(s); // Es werden nur die Infos
        // von konto, nicht von sparkonto ausgegeben.
    }
    

    jakap schrieb:

    Danke für die prompten Antworten, hoffe ich geh' euch nicht auf die Nerven..

    Absolut nicht. Ich wünschte, alle Fragesteller wären so engagiert und gingen so gut auf Antworten ein. Was man hier manchmal sieht...



  • Nexus schrieb:

    ...

    Genau so hätt ichs auch gemacht, wollte gerade ansetzen, als ich gesehen habe, dass du bereits gepostet hast, aber ich wollte genau auf das selbe hinweisen. Seperate Funktionen und dann aufrufen derjenigen, die man braucht.

    Selbst dem Engagement wollte ich so in etwa hinschreiben! 👍



  • Um ehrlich zu sein, hatte ich ziemliche Angst, dass du in der Zwischenzeit genau das Gleiche schreibst. Gut, dass du erst etwas später begonnen hast! 🙂



  • Nexus schrieb:

    Um ehrlich zu sein, hatte ich ziemliche Angst, dass du in der Zwischenzeit genau das Gleiche schreibst. Gut, dass du erst etwas später begonnen hast! 🙂

    ^^
    Hattest Glück, weil ich gerade was anderes am lesen war. 😉



  • Danke für das Lob 🙂 kenne das teilweise aus nem Matheforum da wird wild aneinander vorbei geredet.

    Habe heute das erste Mal mit Vererbung was gemacht, aber von den Virtuellen Funktionen in der VL auch was gehört, von wegen erst zur Laufzeit bekannte Klassen etc. Ich denke ich brauche so etwas, wenn ichs richtig machen möchte, da du schreibst

    Wichtig: Ich habe an dieser Stelle auf Laufzeitpolymorphie verzichtet, weil du mit diesem Konzept womöglich noch nicht vertraut bist. Wenn du allerdings Objekte von abgeleiteten Klassen als Basisklasse ansprechen willst (als Abstraktionsmöglichkeit), möchtest du vielleicht die für den jeweiligen Kontotyp spezifischen Informationen ausgeben. Dazu müsstest du die Methode konstostand_ausgeben() als virtual deklarieren. Auch an den virtuellen Destruktor sollte dann gedacht sein.

    Ich möchte eine Klasse 2 fach vererben und auch in meiner Ausgabefunktion (die ich mittlerweile gesplittet habe ;)) den jeweiligen Kontotyp ausgeben. Ich könnte ja auch n String mit in den Konstruktor packen, in den ich schreibe, wie das Konto heißt? Aber gehts schöner?=)

    Und ich habe gesehen, dass, wenn ich Referenzen bei Methoden für Strings benutze, dass die const sein müssen. Ist das, damit ich nicht eine längere Zeichenkette in den übergebenen Parameter schreibe? Aber auf der anderen Seite ist das doch wie ein Zeiger, da ist das doch egal, oder?
    Auf jeden Fall gehts nur mit const. Ohne const läufts, wenn ich in meiner main() den Konstruktor mit String Parameter aus einer Variable aufrufe, was ich logisch finde, es ist eine Referenz auf den Speicherplatz der Variable. Könnte ich denn da den Inhalt verändern? Weil meine String Variable ist ja nicht konstant.

    Ui ähm ich hoffe das kann man verstehen.. im Grunde gehts hier drum:

    main(){
    string a="test";
    funktion(a);
    funktion("test");
    }
    funktion(string &a){
    cout << a;
    a="veränderung" //das geht, wenns nicht "const string &a" ist..
                    //
    }
    

    Und überhaupt benutzt ihr auch viel const in den Klassen:

    class konto
    {
        protected:
            void inhaber_ausgeben() const;
            void saldo_ausgeben() const;
    ...
    

    Ist das eine Vorsichtsmaßnahme?

    So ich geh ins Bett, machts gut & schönen Dank :xmas2:



  • Ich gehe mal nur auf das 'const' ein:

    Wenn du einen Parameter als 'string &' deklarierst, dann darfst du auch nur exakt den Typ 'string' als Referenz übergeben. Bei einem String-Literal "xyz" müßte erst ein temporäres string-Objekt erstellt werden (das würde dann aber vom Compiler wieder nach dem Aufruf gelöscht - und jegliche Änderung wäre verworfen) - und daher wird das vom Compiler nicht erlaubt.
    Bei 'const string &' jedoch übergibst du ein konstantes Objekt, das in der Methode nicht verändert werden kann, und daher kannst du auch einfach "MeinKonto" übergeben, da du ja nur lesend auf diesen Parameter zugreifen kannst.

    Und die andere Verwendung des 'const' ist bei Methoden. Damit beschreibst du, daß diese Methode keine Änderung an dem Objekt(membern) vornimmt, d.h. daß es nur lesend auf Daten zugreift bzw. nur Ausgaben tätigt. Somit kannst du diese Methoden auch mit einem konstanten Objekt aufrufen:

    const string s("Hello");
    
    size_t len = s.length(); // length ist als 'const' deklariert
    
    s.append("World"); // <- Compilerfehler, da append das Objekt verändern würde
    

    Such mal hier im Forum nach dem Begriff "const correctness" - dann findest du weitere Erklärungen (und auch das Schlüsselwort "mutable" (!), mit dem man die logische Konstantheit aufrecht erhalten kann).

    Edit: schon zu lange kein C++ mehr programmiert...



  • Th69 schrieb:

    (und auch das Schlüsselwort "mutual", mit dem man die logische Konstantheit aufrecht erhalten kann).

    ⚠ Das Keyword heißt nicht "mutual" sondern "mutable" 😉



  • ups... setzen - sechs -)



  • Sooo besten Dank, das Programm ist fertig und läuft, aber ist wohl noch nicht ganz elegant. Aber es läuft 😉

    Allerdings drängt sich bei mir beim Betrachten von virtuellen Methoden folgendes Problem auf: "Virtual bedeutet im Grunde nichts anderes, als `redefinierbar'."

    quelle:http://velociraptor.mni.fh-giessen.de/Programmierung/ProgII-htmldir/node9.html

    Ich dachte, geerbte Methoden könne man immer redefinieren (ich setze das jetzt mal mit ändern gleich, darf ich das?)? Oder überlade ich geerbte Methoden nur?

    Schöne Grüße & Danke im Voraus



  • Das spezielle an virtual ist, dass du auch wenn der dynamische Typ nicht bekannt ist seine Funktion aufrufen kannst (mit einem Zeiger auf die Basisklasse).
    Wenn wir in C++ jetzt kein virtual hätten, könnte man mit einem Zeiger auf eine Basiklasse lediglich Funktionen der Basisklasse aufrufen und man hätte somit keine Polymorphie. (im Sinne von Laufzeitpolymorphie mit Vererbung).

    Also schau mal das hier an:

    struct base
    {
     virtual void foo () { std::cout << "base foo";}
     void bar () { std::cout << "base bar"; }
    
     virtual ~base () {}
    };
    
    struct a : base
    {
     void foo () { std::cout << "a foo"; }
     void bar () { std::cout << "a bar"; }
    };
    
    int main ()
    {
     base* b = new a;
     b->foo (); // gibt "a foo" aus
     b->bar (); // gibt "base bar" aus
    
     delete b; // dank dem virtuellen destruktor können wir auch das Objekt korrekt zerstören
    }
    


  • Krass, das ist mir noch nie aufgefallen bisher, aber hatte auch selten Methoden, die identisch hießen. Aber eigentlich bräuchte ich doch theoretisch dann die Funktionen garnicht in der Basisklasse zu definieren, wenn ich eh nicht vorhabe sie zu benutzen, aber es dient auch eher zur logischen Strukturierung, oder zur Benutzung, wenns komplizierter wird, korrekt?

    Btw, ich schätze statt struct meinst du class oder?

    Habe jetzt den Sinn verstanden denk ich. Das heißt also, ich habe 1 Basisklasse, und eine abgeleitete Klasse, in beiden eine quasi identische Methode definiert und kann immer nur die Methode der Basisklasse aufrufen?

    Allerdings frage ich mich jetzt immer noch, ob die neue Methode die alte überlädt, oder richtig "neu schreibt", auch wenn das jetzt nicht mehr so wichtig ist 😉

    Dankeschön.. ihr seid genial=)


Anmelden zum Antworten