bidirektionale Assoziation implementieren



  • Hi,

    man kann in verschiedenen Lehrbüchern oder auf Websides über UML die Implementierung von bidirektionalen Assoziationen zwischen Klassen finden;
    für alle die damit nichts anfangen können, folgendes Beispiel:

    class Klasse1
    {
    public:
      Klasse2* ptr2;  // Zeiger auf Klasse "Klasse2"
      ...
    }
    
    class Klasse2
    {
    public:
      Klasse1* ptr1; // vgl. oben
      ...
    }
    

    Es handelt sich in der Implementierung also einfach um zwei Klassen, auf die GEGENSEITIG mittels Zeigern zugegriffen werden soll.

    Wenn ich nun diese Schema aber in einem Beispielsprogramm anwende, also

    #include "Klasse1.h"
    #include "Klasse2.h"

    int main()
    {
    ...
    }

    gibt der Kompiler eine Fehlermeldung aus: "Klasse2 was not declared in this scope". Was muss man also tun, damit man in einer Anwendung zwei Klassen implementieren kann, die sich gegenseitig kennen, sprich wie kann ich es machen, dass ich von Klasse1 aus auf Klasse2 zugreifen kann und umgekehrt, und das eben innerhalb einer geschlossenen Anwendung, wie das letzte Minibeispiel?
    Hat jemand eine Idee, vielleicht mit kompletten Minibeispiel?

    Gruß
    Julio :xmas1:



  • Du musst eine von beiden Klassen zuerst deklarieren, ohne sie zu definieren. Beispiel:

    class Klasse1;
    
    class Klasse2
    {
       Klasse1 *foo;
    };
    
    class Klasse1
    {
       Klasse2 *bar;
    };
    

    Dann müsste es funktionieren.
    EDIT: Das Problem ist nämlich, dass der Compiler ja nichts von Klasse1 wüsste, wenn die Deklaration nicht wäre.

    Felix :xmas2:

    P.S: Woher kommen die komischen Namen foo und bar eigentlich? Ich habe die schon öfter in Beispielen gesehen und benutze sie einfach.



  • bar wird oft in c/c++ kursen und lehrgängen verwebdet, denke mal foo auch 🙂



  • Was du da hast nennt man zirkuläre Referenzen. Ich weiss zwar nicht in welchen
    Lehrbüchern und welchen Beispielen du solche Implementierungen gefunden hast.
    Aber eins ist sicher, man sollte sie tunlichst vermeiden.
    Wenn du als Programmierer bei "allokierten" Objekten selbst die Lebensdauer
    verwalten musst wirst du bei zirkulären Referenzen aud Probleme stossen die
    man besten dadurch vermeidet das man solche Situation insgesamt vermeidet.

    mfg JJ



  • naja, ich nutze sie ja ( scheinbar glücklicherweise ) eh nicht...

    😃



  • 1)funktioniert leider nicht; ich habe die Deklaration in die erste Headerdatei eingebunden;
    sobald man mit dem pointer was macht, z.B. wie hier im Konstruktor ein neues Objekt erzeugen möchte, erscheint wieder eine Fehlermeldung ala "Vorwärtsdeklaration..."

    Headerdatei Klasse1.h:

    class Klasse2;

    class Klasse1
    {
    public:
    Klasse1() {ptr2=new Klasse2;}
    Klasse2* ptr2;
    ...
    }

    Headerdatei Klasse2.h:

    class Klasse2
    {
    public:
    Klasse1* ptr1;
    ...
    }

    #include "Klasse1.h"
    #include "Klasse2.h"

    int main()
    {
    ...
    }

    Was hab ich falsch gemacht?

    1. wegen foo und bar: schau mal unter: http://www.faqs.org/rfcs/rfc3092.html

    Julio :xmas1:



  • ⚠ Du hast keine Semikolons hinter den Klassendefinitionen ⚠ . Hast du die im Original-Quelltext auch nicht?
    Beispiel:

    class Klasse1
    {
       int foo;
    }; //DONT FORGET THE SEMICOLON
    

    Wenn es das nicht war, frag nochmal nach

    Felix :xmas2:



  • hab ich im Originalquelltext, funktioniert trotzdem nicht.

    Bidirektionale Assoziationen sind Standardbeziehungen. Z.B. im BUch "Lehrbuch der Objektmodellierung" von Heide Balzert beschrieben oder unter

    http://www.fbi.fh-darmstadt.de/frames/organisation/personen/w.weber/public_html/SWT/KAP2-und-3.pdf

    Seite 80ff.

    Julio



  • John Doe schrieb:

    Was du da hast nennt man zirkuläre Referenzen. Ich weiss zwar nicht in welchen Lehrbüchern und welchen Beispielen du solche Implementierungen gefunden hast.
    Aber eins ist sicher, man sollte sie tunlichst vermeiden.

    und man sollte tunlichst vermeiden, zuerst UML und dann coden zu lernen. das haut einen doch nur um und fürht zu versuchen wie sowas, was einen ganz sicher sprengt.



  • @ volkard: kann man meiner Meinung nach verschiedener Ansicht sein. Ich selbst beschäftige mich seit kurzem mit UML, seit längerem aber mit C++. Das beschriebene Problem hatte ich schon öfter. Eine bidirektionale Assoziation zu realisieren kann gerade bei Systemen mit vielen Klassen ein Mittel der Wahl sein. Aber egal ob gut oder schlecht und in welchem Kontext etc pp;
    wie funktionierts denn nun? es muss doch in c++ eine solche Möglichkeit geben?

    Julio



  • Ich weiß, wo dein Problem liegt:
    Du definierst direkt eine Methode in der Klassendefinition von Klasse1:

    class Klasse1
    {
       public:
       Klasse1() {ptr2=new Klasse2;}
       Klasse2* ptr2;
       //...
    };
    

    Und in dieser Definition erstellst du dynamisch ein Objekt der Klasse Klasse2. Das kann nicht funktionieren, weil der Compiler nicht weiß, wie groß das Objekt ist. Du musst also folgendes machen:
    Du schreibst in die Header-Dateien (wie sich das eigentlich auch gehört 🤡 ) NUR die Defintion der Klassen OHNE Methoden-Definitionen. Dann includierst du die beiden Klassen-Defintionen, sodass der Compiler die größe der beiden kennt und DANN kannst du in einer Datei, in der die BEIDEN Klassen-Definitionen (ohne Methoden-Definitionen) stehen die Mehtoden definieren. Du musst dir also praktisch eine Header-Datei mit beiden Klassen-DEfinitionen bauen und die dann einbinden, um die Methoden zu definieren. Dann klappt das auch. Das Problem ist halt wie gesagt, dass der Compiler (oder der Linker, weiß nicht so genau 😞 ) wissen muss, wie groß ein Objekt ist, um es anzulegen. Das ganze sieht dann so aus:
    Headerdatei foo.h:

    class Klasse2;
    
    class Klasse1
    {
       public:
       Klasse1();
       Klasse2* ptr2;
       //...
    };
    
    class Klasse2
    {
       public:
       Klasse1* ptr1;
       //...
    };
    

    foo.cpp:

    #include "foo.h"
    Klasse1::Klasse1 (void) {ptr2=new Klasse2;}
    

    Felix :xmas2:



  • kuehlwalda schrieb:

    @ volkard: kann man meiner Meinung nach verschiedener Ansicht sein. Ich selbst beschäftige mich seit kurzem mit UML, seit längerem aber mit C++. Das beschriebene Problem hatte ich schon öfter. Eine bidirektionale Assoziation zu realisieren kann gerade bei Systemen mit vielen Klassen ein Mittel der Wahl sein. Aber egal ob gut oder schlecht und in welchem Kontext etc pp;
    wie funktionierts denn nun? es muss doch in c++ eine solche Möglichkeit geben?

    mit hin- und rückzeiger.
    kritisch ist aber das verhältnis solcher dinge zum destruieren. kannst ja nicht ein objekt löschen und dann das andere ins nirvana zeigen lassen.

    also mößte in jedem objekt der dtor dafür sorgen, daß der peer bescheit kriegt. zu aufwendig. smart pointer machen, die das erledigen. nee, der pointer kennt sein objekt nicht und offset_of dazu missbrauchen ist lame. jeder smart-pointer kennt seinen peer-pointer und sein parent-objekt. geht, ist lahm.
    und dann ist nicht definiert, was ein paar mit nur einem elemet sein soll. gibts inhaltliche zwänge, daß immer zwei von sowas existieren? isses sinnvoll, ein diese-zwei-dinge-besitzendes objekt zu bauen, um paareige destruktionen atomar zu machen? fragen über fragen. ein teufelskreis.



  • *unfug gelöscht*



  • @felix. 👍
    Super, danke. Das klappt ja schon viel besser. Es gibt jetzt allerdings noch ein Problem. Hier mein Miniprogramm, zum Kompilieren, OHNE eingebundene
    Headerdateien:

    #include <iostream>
    using namespace std;
    
    class Klasse2;
    
    class Klasse1
    {
    public:    
    Klasse1();
    Klasse2* ptr2;
    void do_something();
    };
    
    class Klasse2
    {
    public:
    Klasse2();
    Klasse1* ptr1;
    void do_something();
    };
    
    Klasse1 :: Klasse1() {ptr2=new Klasse2;}
    void Klasse1 :: do_something() {cout<<"do_something: Klasse1"<<endl;}
    
    Klasse2 :: Klasse2() {}
    void Klasse2 :: do_something()
         {
         ptr1=new Klasse1; 
         ptr1->do_something();
         cout<<"do_something: Klasse2"<<endl;
         }
    
    int main()
    {
    Klasse1 inst1;
    Klasse2 inst2;
    
    cout<<"Hello"<<endl;
    inst1.do_something();
    inst2.do_something();
    
    system("PAUSE");	
    return 0;
    }
    

    Das funktioniert wunderbar. Wenn ich aber das ganze aber in Headerdateien aufteile, also das Programm MIT Headerdateien, z.B.

    #include <iostream>
    #include "foo.h"
    #include "bar.h"
    using namespace std;
    
    int main()
    {
    Klasse1 inst1;
    Klasse2 inst2;
    
    cout<<"Hello"<<endl;
    inst1.do_something();
    inst2.do_something();
    
    system("PAUSE");	
    return 0;
    }
    

    Die Headerdateien foo.h und bar.h:

    class Klasse2;
    
    class Klasse1
    {
    public:    
    Klasse1();
    Klasse2* ptr2;
    void do_something();
    };
    
    class Klasse2
    {
    public:
    Klasse2();
    Klasse1* ptr1;
    void do_something();
    };
    
    Klasse1 :: Klasse1() {ptr2=new Klasse2;}
    void Klasse1 :: do_something() {cout<<"do_something: Klasse1"<<endl;}
    
    Klasse2 :: Klasse2() {}
    void Klasse2 :: do_something()
         {
         ptr1=new Klasse1; 
         ptr1->do_something();
         cout<<"do_something: Klasse2"<<endl;
         }
    

    ...dann gibts mal wieder ne Fehlermeldung, "cout undeclared";
    Warum? Der Kompiler bindet doch den <iostream> Header zusammen mit den anderen Headerdateien ein, auch vor den beiden anderen 😞

    Julio



  • @volkard
    Was heißt Unfug gelöscht.
    Programmiersprachen mit automatischer Speicherverwaltung bieten für solche Dinge wie bidirektionale Assoziationen einfach bessere Mechanismen.
    Solche Dinge lassen sich in C++ leider nicht immer elegant lösen.
    Get the Facts!



  • Ich weiß, was dein Problem ist. Du hast folgendes geschrieben:

    #include <iostream>
    #include "foo.h"
    #include "bar.h"
    using namespace std;
    
    int main()
    {
    Klasse1 inst1;
    Klasse2 inst2;
    
    cout<<"Hello"<<endl;
    inst1.do_something();
    inst2.do_something();
    
    system("PAUSE");   
    return 0;
    }
    

    Und in der einen Funktion, die du über eine Header-Datei einkopierst, beutzt du cout. Das ist allerdings im namespace std deklariert. Und wo steht bei dir using namespace std;?? Richtig, nach dem einbinden der Header-Dateien. Also entweder zuerst using namespace std; direkt nach dem #include <iostream> schreiben, oder in der Funktionsdefinition cout durch std::cout ersetzen! Dann müsste es funktionieren. Ach übrigens man sollte es tunlichst vermeiden, Funktionsdefinitionen über Header-Dateien einzukopieren. Nimm stattdesen lieber einzeln funktionierende Übersetzungsdateien. Dann brauchst du auf so etwas wie das using namespace std; nicht zu achten.

    Felix :xmas1:



  • ok, natürlich, danke.
    Ich kenne bisher, bei umfangreicheren Anwendungen, nur die Vorgehensweise mit Einbinden der Definitionen in eine Headerdatei, die dann z.B. den Namen "foo_lib.h" erhält. Was meinst Du mit "einzeln funktionierende Übersetzungsdateien"?

    Julio



  • Also das mit den einzelnen Übersetzungseinheiten geht folgendermaßen:
    Du hast zum Beispiel eine Klasse foo in der Datei foo.h deklariert:

    class foo
    {
        int bar;
        int func (void);
    };
    

    Die Definition der Methode schreibst du dann in eine andere separate Datei foo.cpp:

    #include "foo.h"
    int foo::func (void)
    {
        //Tu was
    }
    

    Um jetzt die Klasse foo benutzen zu können musst du nur noch die Header-Datei includieren und die Datei foo.cpp zu deinem Projekt hinzufügen. Das geht je nach Entwicklungsumgebung anders, da kann ich dir so auch nicht helfen. Die main.cpp sieht dann zum Beispiel so aus:

    #include "foo.h"
    
    int main (void)
    {
        foo lol;
        foo.func();
        return(0);
    }
    

    Der Compiler sieht in der main.cpp dann nur die Deklaration und kann anhand von der prüfen, ob due die Klasse richtig benutzt. Der Linker "linkt" dann später die Definition der Methode dazu. So ist das vom Stil her halt sauberer und übersichtlicher.

    Felix :xmas1:



  • ok, kapiert.

    Probier ich mal in Zukunft aus; der einzige Nachteil scheint mir, dass man so immer die zugehörigen Headerdateien in die jeweilige file einfügrn muss (<iostream>, <string>, ...), man hat aber dafür (ist ja nur mehr Schreibaufwand) eine kompakte Einheit, das ist gut.

    Nochmals muchas gracias.

    Julio :xmas1:


Anmelden zum Antworten