Verschiedene Objekte innerhalb eines Vectors unterscheiden



  • Ich merke gerade, meine Fragestellung war schlecht formuliert.

    Das Kapitel mit virtual hatte ich auch schon, das hab ich auch soweit verstanden (denke ich), das klappt auch, ich weiß nur nicht ob das hier etwas damit zu tun hat.

    Mein Problem ist, die Basisklasse hat 2 Attribute sowie die zugehörigen get/set Methoden. Die abgeleiteten Klassen haben aber mehr Attribute und die zugehörigen get/set Methoden.

    Wenn mein Vector jetzt aber vom Typ Person ist, dann bekomme ich innerhalb der Schleife überhaupt nicht die Möglichkeit die getter Methoden der abgeleiteten Klasse aufzurufen, ausser ich caste. Aber dazu muss ich ja wissen welches Objekt ich gerade habe.

    myPersons[i].getName();
    myPersons[i].getAge();
    

    Das geht immer. Aber wenn an der Position i jetzt ein Worker ist müsste es auch

    myPersons[i].getSalary();
    

    geben. Tut es aber nicht, ausser ich mache

    ((Worker)myPersons[i]).getSalary();
    

    Dafür muss ich aber vorher ja irgendwie prüfen ob das Objekt jetzt wirklich ein Worker oder Child oder Person ist.



  • Dann verpasse doch Person eine virtualle Methode Ausgabe , welche Du dann für Worker und Child entsprechend spezialisierst. Dann kannst Du auch über Person die zusätzlichen Infos ausgeben.



  • scrabbl schrieb:

    ich kämpfe mich gerade durch unzählige Tutorials bzgl. Vererbung
    und bin dabei auf ein Problem gestoßen das mit Vererbung selber jetzt
    nichts direkt zu tun hat.

    Ich hab eine Basisklasse "Person".
    Davon erben 2 weitere Klassen "Worker" und "Child".

    In meiner main-Funktion erzeuge ich jetzt jeweils 2 Personen, 2 Worker und
    2 Childs und stopfe alle in einen Vector vom Typ Person.

    Was genau stopfst Du in den Vektor?

    scrabbl schrieb:

    Ich wollte jetzt aber einen Schritt weitergehen und per for-Schleife über den
    Vector laufen und eine einfach Ausgabe machen.
    Jetzt muss ich aber doch in der for-Schleife irgendwie unterscheiden ob das
    jetzt gerade eine Person, ein Worker oder ein Child ist, weil es dann unterschiedliche Werte zum Ausgeben gibt.

    for(unsigned int i = 0; i < myPersons.size(); i++){
         if(myPersons[i] == Person)
               //Ausgabe
    }
    

    Die Frage ist, wo etwas unterschiedlich gemacht werden soll. Wenn Du im Nutzer-Code testen musst, um was für eine Klasse es sich gerade handelt, dass ist das meistens ein Fehldesign. Wenn Du über eine Basisklassenfunktion eine Funktion "Ausgabe" oder "Print" aufrufen kannst und die unterschiedlichen Ausgaben über Laufzeitpolymorphie erzeugt werden, dann ist das schon besser (Basisklasse mit virtueller Funktion):

    for(unsigned int i = 0; i < myPersons.size(); i++){
         myPersons[i]->ausgabe();
    }
    

    scrabbl schrieb:

    So geht es mal nicht.
    Wie kriege ich denn das raus ?

    So geht es sowieso nicht, weil Du wahrscheinlich im Vektor nur Überreste von Objekten speicherst ("Slicing").

    Eines der wichtigsten Eigenschaften von C++ ist Dir entgangen: In C++ zwingt man Dir keine Indirektion auf. Wenn Du schreibst

    Klasse obj;
    

    dann ist obj keine Referenz, kein Zeiger, sondern das Objekt selbst! Wenn Du dann schreibst:

    Klasse foo = obj;
    

    dann wird das Objekt selbst kopiert. foo und obj beziehen sich dann auf verschiedene Objekte, die nur gleich aussehen. Das ist wie mit den anderen Typen auch:

    int i = 23;
    int j = i;
    

    i und j sind zwei verschiedene int-Objekte, die nur im Moment denselben Wert speichern.

    Du hast Nachholbedarf. C++ funktioniert eben doch nicht so, wie Du es von anderen Sprachen (wahrscheinlich) schon kennst.



  • Hallo

    @Tachyon: Danke, so hab ichs hinbekommen, wobei ich mich immernoch frage wie das geht wenn man nur an einen Wert ran möchte..

    krümelkacker schrieb:

    Was genau stopfst Du in den Vektor?

    6 Objekte. 2 der Basisklasse Person, je 2 der abgeleiteden Klassen Worker und Child.

    krümelkacker schrieb:

    So geht es sowieso nicht, weil Du wahrscheinlich im Vektor nur Überreste von Objekten speicherst ("Slicing").

    Wieso Überreste ??

    std::vector<Person> myPersons;
    
    Person p1;
    p1.init("Hans", 50, 1.79);
    myPersons.push_back(p1);
    
    Worker w1;
    w1.init("Didi", 44, 1.69, 2000);
    myPersons.push_back(w1);
    
    Child c1;
    c1.init("Matze", 8, 1.35, 6);
    myPersons.push_back(c1);
    

    Dann hab ich doch einen Vector vom Typ Person mit 3 richtigen Objekten. Wieso sollen das nur Überreste sein ??
    Person hat, wie man sieht, 3 Attribute (Name, Alter, Größe). Worker hat zusätzlich noch Gehalt und Child die Anzahl an Freunden.
    Diese 3 Objekte sind jetzt in einem Vector über den ich nun in einer for-Schleife laufe. Nach Tchyons Rat hab ich nun eine virtuelle Methode "ausgabe" geschrieben die mir dann, je nach Objekt, eben alle Werte ausgibt. Das klappt auch, was ich mich aber frage ist:

    Wenn ich jetzt sage ich will über diesen Vector laufen und nur das Gehalt aller Worker ausgeben. Oder nur das Alter und die Anzahl Freunde aller Childs, dann kann ich doch nicht für jedes beliebige Szenario eine virtuelle Methode erstellen oder ?
    Ich muss doch dann in der Schleife irgendwie prüfen ob es zBsp ein Worker Objekt ist, dieses dann casten und kann dann die entsprechenden getter aufrufen.
    Oder sehe ich da was grundlegend falsch ?

    krümelkacker schrieb:

    Du hast Nachholbedarf. C++ funktioniert eben doch nicht so, wie Du es von anderen Sprachen (wahrscheinlich) schon kennst.

    Das schließe ich nicht aus. Das zwei INT aber unterschiedliche Objekte sind obwohl sie denselben Wert haben war mir durchaus klar 😛

    Viele Grüße



  • Ich muss doch dann in der Schleife irgendwie prüfen ob es zBsp ein Worker Objekt ist, dieses dann casten und kann dann die entsprechenden getter aufrufen.
    Oder sehe ich da was grundlegend falsch ?

    Ja. Alle deine Objekte im vector sind vom Typ Person, da gibt es nichts zum Casten.

    mit 3 richtigen Objekten. Wieso sollen das nur Überreste sein ??

    Was du da in den vector stopfst, sind Kopien des Personen-Teils deiner Objekte. Dazu:
    http://en.wikipedia.org/wiki/Object_slicing

    Das Problem lässt sich durch Verwendung von Zeigern umgehen.
    Mit C++11 bietet sich ein vector<unique_ptr<Person>> an, unter C++03 ein ptr_vector (z.B. bei boost dabei).



  • scrabbl schrieb:

    krümelkacker schrieb:

    Was genau stopfst Du in den Vektor?

    6 Objekte. 2 der Basisklasse Person, je 2 der abgeleiteden Klassen Worker und Child.

    Falsch. Du präsentierst dem Vektor zwar Objekte verschiedener dynamischer Typen, der Vektor kopiert und speichert aber immer nur jeweils den Person-Teil davon.

    scrabbl schrieb:

    krümelkacker schrieb:

    So geht es sowieso nicht, weil Du wahrscheinlich im Vektor nur Überreste von Objekten speicherst ("Slicing").

    Wieso Überreste ??

    siehe oben, siehe "slicing".

    scrabbl schrieb:

    [...]
    Dann hab ich doch einen Vector vom Typ Person mit 3 richtigen Objekten. Wieso sollen das nur Überreste sein ??

    Habe ich schon beantwortet.

    scrabbl schrieb:

    [...] Childs [...]

    Children

    scrabbl schrieb:

    [...] dann kann ich doch nicht für jedes beliebige Szenario eine virtuelle Methode erstellen oder ?

    Ich muss doch dann in der Schleife irgendwie prüfen ob es zBsp ein Worker Objekt ist, dieses dann casten und kann dann die entsprechenden getter aufrufen.
    Oder sehe ich da was grundlegend falsch ?

    In Deinem Fall wird es nie ein Worker-Objekt im Vektor sein. Abgesehen davon, sind solche Fallunterscheidungs-Orgien doof, da du sie wahrscheinlich nicht nur an einer Stelle, sondern gleich an mehreren brauchst. Das ist, was die Code-Pflege und Anpassbarket angeht, Schei*e.

    scrabbl schrieb:

    krümelkacker schrieb:

    Du hast Nachholbedarf. C++ funktioniert eben doch nicht so, wie Du es von anderen Sprachen (wahrscheinlich) schon kennst.

    Das schließe ich nicht aus. Das zwei INT aber unterschiedliche Objekte sind obwohl sie denselben Wert haben war mir durchaus klar 😛

    Siehste. Und was glaubst Du, wie ein std::vector funktioniert? Das push_back erwartet eine Referenz auf ein Person-Objekt. Das, was der Vektor dann macht, ist, ein neues Person-Objekt per Kopierkonstruktor zu erstellen. Das neue Objekt ist hinterher nie ein Worker.



  • --- Huch. Mal wieder auf "antworten" statt "editieren" geklickt ---

    Und was glaubst Du, wie ein std::vector funktioniert? Das push_back erwartet eine Referenz auf ein Person-Objekt. Das, was der Vektor dann macht, ist, ein neues Person-Objekt per Kopierkonstruktor zu erstellen. Das neue Objekt ist hinterher nie ein Worker. Genauso speichert ein std::vector<int> nur int-Objekte, die durch das Kopieren erzeugt werden. Damit Laufzeitpolymorphie funktioniert, musst Du indirekt arbeiten => Zeiger (bzw Referenzen).



  • Okay, danke. Ich hab es jetzt mit Zeigern gemacht und einer virtuellen Ausgabemethode:

    std::vector<Person*> myPersons;
    
    Person* p1 = new Person();
    p1->init("Hans", 50, 1.79);
    myPersons.push_back(p1);
    
    Worker* w1 = new Worker();
    w1->init("Didi", 44, 1.69, 2000);
    myPersons.push_back(w1);
    
    Child* c1 = new Child();
    c1->init("Susi", 10, 1.35, 7);
    myPersons.push_back(c1);
    
    for(unsigned int i = 0; i < myPersons.size(); i++){
    	myPersons[i]->ausgabe();
    }
    

    Ausgabe:

    Mein Name ist Hans, ich bin 50 Jahre alt und 1.79m groß.
    Mein Name ist Didi, ich bin 44 Jahre alt, 1.69m groß und verdiene 2000 Euro.
    Mein Name ist Susi, ich bin 10 Jahre alt, 1.35m groß und habe 7 Freunde.

    Ist das jetzt korrekt von der Vorgehensweise ?

    Viele Grüße



  • scrabbl schrieb:

    Ist das jetzt korrekt von der Vorgehensweise ?

    Was die Polymorphie angeht, ja.
    Aber:

    1. Die init()-Methode ist kein gutes Design. Für sowas sind Konstruktoren da. Was passiert wohl, wenn man mal vergisst, das init für ein Objekt aufzurufen? Richtig, du hättest eine Person ohne Namen, Alter und Größe.

    2. Wenn du mit new() allokierte Objekte in deinem vector speicherst, musst du auch dran denken, sie wieder freizugeben, da du sonst Speicherlecks erhälst. Es gibt dafür auch in den boost-Beibliotheken extra Container, die das für dich übernehmen, alternativ kann man auch Smartpointer benutzen, aber das sind schon etwas fortgeschrittene Konzepte. Für den Anfang ist es vielleicht vom Lerneffekt her sogar besser, immer dran zu denken, dass die Dinger auch wieder freigegeben werden müssen.



  • pumuckl schrieb:

    1. Die init()-Methode ist kein gutes Design. Für sowas sind Konstruktoren da. Was passiert wohl, wenn man mal vergisst, das init für ein Objekt aufzurufen? Richtig, du hättest eine Person ohne Namen, Alter und Größe.

    Das stimmt, dazu hab ich jetzt aber noch eine Frage. Wenn ich die init-Methoden wegwerfe und es über die Konsruktoren machen, hab ich in der Basisklasse Person einen Konstruktor der Name, Alter und Größe entgegennimmt.

    In der abgeleiteten Worker Klasse einen der Name, Alter Größe und Gehalt entgegennimmt. Hier wirft mir der Compiler einen Fehler das der Worker Konstruktor "no matching function in Person::Person" hätte.

    Das liegt wohl daran das bei abgeleiteten Klassen zuerst der Konstruktor der Basisklasse aufgerufen wird und dann erst der eigene und dann passt es ja nicht zusammen.
    Wenn ich in der Basisklasse jetzt einfach einen zweiten, leeren Konstruktor hinzufüge geht das Ganze fehlerfrei. Ist das die normale Vorgehensweise ?



  • zeig mal deinen Code. Das hört sich alles sehr seltsam an.

    Normalerweise ist es kein Problem, dass der Konstruktor einer abgeleiteten Klasse mehr Argumente bekommt, als der Konstruktor der Basisklasse. Dies ist sogar ein recht häufiger Fall.

    Felix



  • Du musst den Konstruktor der Basisklasse selbst aufrufen.



  • 314159265358979 schrieb:

    Du musst den Konstruktor der Basisklasse selbst aufrufen.

    Wenn du ihm noch die Begriffe "Basisinitialisierung" und "Initialisierungsliste" gibts, kann er auch was damit anfangen.



  • scrabbl schrieb:

    std::vector<Person*> myPersons;
    
    Person* p1 = new Person();
    p1->init("Hans", 50, 1.79);
    myPersons.push_back(p1);
    
    Worker* w1 = new Worker();
    w1->init("Didi", 44, 1.69, 2000);
    myPersons.push_back(w1);
    
    Child* c1 = new Child();
    c1->init("Susi", 10, 1.35, 7);
    myPersons.push_back(c1);
    
    for(unsigned int i = 0; i < myPersons.size(); i++){
    	myPersons[i]->ausgabe();
    }
    

    Ist das jetzt korrekt von der Vorgehensweise ?

    Kommt drauf an, was es werden soll und ob Du es für OK hältst, die Objekte nicht zu löschen.

    Oft kommt es mir so vor, als werden bei solchen Beispielen Klassen für etwas missbraucht, was lieber (ggf änderbare) Eigenschaften hätten sein sollen. Ein "Child" wird im erwachsenen Alter nicht unbedingt arbeitslos bleiben. 😉 Neben der Lebenszeit/Specherleckproblematik ist hier die Modellierung meiner Meinung nach fragwürdig. Man sollte sein Design daran ausrichten, wie verschiedene Dinge verwendet werden. Vererbung mit polymorphen Basisklassen setze ich so gut wie nie persönlich ein. Aber hin und wieder kommt es dann mal vor, dass irgendein Programmmodul eine Menge von verschiedenartigen Objekten gleich behandeln muss und/oder im Sinne einer abstrakten Callback-Schnittstelle Daten austauschen will/muss.



  • Person.h

    class Person {
    protected:
    	std::string name;
    	int age;
    	double size;
    
    public:
    	//Person(){};
    	Person(std::string _name, int _age, double _size);
    	virtual ~Person(){}
    
    	virtual void ausgabe();
    };
    

    Person.cpp

    #include "Person.h"
    
    Person::Person(std::string _name, int _age, double _size) {
    	name = _name;
    	age = _age;
    	size = _size;
    }
    
    void Person::ausgabe(){
    	std::cout << "Mein Name ist " << name << ", ich bin " << age
                       << " Jahre alt und " << size << "m groß." << std::endl;
    }
    

    Worker.h

    #include "Person.h"
    
    class Worker : public Person{
    private:
    	int gehalt;
    public:
    	Worker(std::string _name, int _age, double _size, int _gehalt);
    
    	void ausgabe();
    };
    

    Worker.cpp

    #include "Worker.h"
    
    Worker::Worker(std::string _name, int _age, double _size, int _gehalt){
    	name = _name;
    	age = _age;
    	size = _size;
    	gehalt = _gehalt;
    }
    
    void Worker::ausgabe(){
    	std::cout << "Mein Name ist " << name << ", ich bin " << age
                       << " Jahre alt, " << size << "m groß und verdiene "
                       << gehalt << " Euro." << std::endl;
    }
    

    Mehr ist es eigtl gar nicht. Und der Konstruktor des Workers sagt mir er würde keine "matching function" finden. Sobald ich den auskommentierten, leeren Konstruktor wieder dazunehme geht es einwandfrei.

    Über die Sinnhaftigkeit des Beispiels kann man bestimmt streiten. Hab mir das so nicht ausgedacht, stammt aus einem der unzähligen online Tutorials 🙂



  • Genau das, was PI gemeint hat und was siebenschwein geschrieben hat: Such mal im Netz (oder im C++-Einsteigerbuch deiner Wahl) nach "Initialisierungsliste".

    Dann kommst du auch drum herum, die Attribute von Person als protected zu deklarieren sondern kannst sie wie es sich meist gehört wieder private machen 😉


Anmelden zum Antworten