Konsolen-Simulation des Algorithmus "Türme von Hanoi" unter Verwendung einer Klasse
-
Cpp_Anfaeger schrieb:
Ufff ... Klassen sind schon umfangreicher und brauchen relativ mehr Speicher als normale Programmierung ...
http://s14.directupload.net/images/131208/5afa74r2.jpg
Soweit sind die Klassen fertig designed u. programmiert. Es scheint auch zu funktionieren.
Klassen sind schon umfangreicher, aber man gewinnt Übersicht, wenn man es richtig anstellt. Mehr Speicher brauchen sie deshalb nicht.
So umfangreich, wie Du es gemacht hast, brauchen sie auch gar nicht zu sein. Versuche nicht alles zwanghaft in eine Klasse zu packen. SeppJ hat Dir mit dem Link schon den Tipp gegeben, dass Du zumindest den Destruktor überall einsparen kannst. Der wird automatisch richtig erzeugt, solange Du einfache Member hast und nicht etwa Pointer mit allokiertem Memory.
Die Klasse Simulation ist wahrscheinlich völlig überflüssig, da reicht eine Funktion. Bei der Klasse Display sind zumindest die print-Funktionen überflüssig; hier würde gleich eine Ausgabe auf std::cout reichen.
Schau mal wasDisplay
sonst noch macht und was die MethodeSimulation::recurFunction
macht. In beiden stehen Größen und Abläufe, die über das Aussehen der Ausgabe entscheiden. Das ist ungünstig geschnitten, da sind zu viele implizite Schnittstellen zwischen beiden. D.h. wenn Du in der eine was änderst, musst Du in der anderen in der Regel auch Änderungen vornehmen.Die Zeilen 313 bis 328 lassen sich in einer kleinen for-Schleife zusammenfassen, wenn man den String für
Slice
gleich im Konstruktor mitgibt. Tipp: ein Objekt sollte 'gebrauchsfertig' sein, wenn der Konstruktor verlassen wird. Das spart dann auch gleich die MethodeSlice::setSlice
ein. So sähe es dann z.B. aus:// 3 Türme erstellen Tower tower_1, tower_2, tower_3; // Turm_1 mit Scheiben auffüllen string bild = "***********************"; for (int i = 5; i > -1; --i) { tower_1.addSlice(Slice(bild)); if( i > 0 ) bild.erase( bild.end()-4, bild.end() ); }
Wenn man eine Klasse entwirft, sollte man sich zunächst darüber Gedanken machen, was die Klasse repräsentiert, welche Verantwortlichkeit sie hat und wie sie vom Anwender benutzt wird. Die Internas spielen zunächst keine Rolle.
Beispiel Turm: Er repräsentiert den Stapel Scheiben, hält die Scheiben. Man muss eine Scheibe drauf legen und entfernen können - oder eine Scheibe von einem Turm zum anderen verschieben können. Es greift der Verschiebe-Algorithmus darauf zu und die Darstellung des Turms (Turm-View).
Dein Turm:class Tower { public: Tower(); // ~Tower(); // muss nicht // -- Schnittstelle zum Verschiebe-Algorithmus void addSlice(Slice slice); // 1x aufnehmen Slice getSlice(); //oberste Scheibe zurückgeben void removeSlice(); // 1x löschen // -- Schnittstelle zur Darstellung (View) Slice getAt(const int &iter); // irgendwo eine Scheibe int getSize(); // Towergröße auslesen // Member };
das sieht doch schon mal sehr sauber aus. Nur weiter so - Übung macht den Meister
Gruß
Werner
-
Danke dir für die guten Tipps zur Gestaltung der Klassen!
Werner Salomon schrieb:
SeppJ hat Dir mit dem Link schon den Tipp gegeben, dass Du zumindest den Destruktor überall einsparen kannst. Der wird automatisch richtig erzeugt, solange Du einfache Member hast und nicht etwa Pointer mit allokiertem Memory.
Achso, deswegen hat der Designer von Qt Creator standardmäßig keine Destruktoren in die Vorlagen eingeblendet. Ich hatte schon gedacht, dass das ein Bug wäre
Werner Salomon schrieb:
In beiden stehen Größen und Abläufe, die über das Aussehen der Ausgabe entscheiden. Das ist ungünstig geschnitten, da sind zu viele implizite Schnittstellen zwischen beiden. D.h. wenn Du in der eine was änderst, musst Du in der anderen in der Regel auch Änderungen vornehmen.
Ich habe versucht, die Klasse Display so gut wie möglich losgelöst zu erstellen. Inwieweit muss ich sie ändern, wenn ich in der Klasse Simulation etwas ändere? Ich habe gedacht, die Klasse Display gibt die Gebrauchsanweisung vor, wie sie zu benutzen ist, und daran muss jeder sich halten, der sie benutzen möchte.
Werner Salomon schrieb:
Die Zeilen 313 bis 328 lassen sich in einer kleinen for-Schleife zusammenfassen
Ja, ich habe den Hang, alles so zu programmieren, damit ich sofort sehe, was vor sich geht. Dass da wiederum mehr Zeilen herauskommen ... super! Diese Technik ist wirklich neu für mich
:
// 3 Türme erstellen Tower tower_1, tower_2, tower_3; // Turm_1 mit Scheiben auffüllen string bild = "***********************"; for (int i = 5; i > -1; --i) { tower_1.addSlice(Slice(bild)); if( i > 0 ) bild.erase( bild.end()-4, bild.end() ); }
-
Cpp_Anfaeger schrieb:
Werner Salomon schrieb:
In beiden stehen Größen und Abläufe, die über das Aussehen der Ausgabe entscheiden. Das ist ungünstig geschnitten, da sind zu viele implizite Schnittstellen zwischen beiden. D.h. wenn Du in der eine was änderst, musst Du in der anderen in der Regel auch Änderungen vornehmen.
Ich habe versucht, die Klasse Display so gut wie möglich losgelöst zu erstellen. Inwieweit muss ich sie ändern, wenn ich in der Klasse Simulation etwas ändere? Ich habe gedacht, die Klasse Display gibt die Gebrauchsanweisung vor, wie sie zu benutzen ist, und daran muss jeder sich halten, der sie benutzen möchte.
ich meinte gar nicht
Display
sondern das main-Programm und die Simulation. In Der MethodeDisplay::gotoXY()
hast Du X und Y vertauscht - das verwirrt natürlich. X wird zur Zeilennummer (nach unten ansteigend) und Y ist die Spalte.Die Positionen in
Simulation::recurFunction
müssen sich nach der Größe und Anzahl der Scheiben immain
richten. D.h. wenn Du bei dem einen wesentliche Änderungen durchführt, dann muss das andere auch angepasst werden.
Außerdem ist Simulation::recurFunction direkt von Display abhängig, da es dort als lokale Variable benutzt wird. Das ist zumindest aus Sicht des Designs problematisch - natürlich weniger bei so einem kleinen Programm. Bei größeren Applikationen sollte man das besser trennen.Ich könnte mir einen move-Algorithmus vorstellen, der etwa so aussieht:
template< typename View > void move( std::size_t n, Tower& src, Tower& via, Tower& dst, const View& view ) { if( n == 1 ) { move1( src, dst ); view(); } else { move( n-1, src, dst, via, view ); move1( src, dst ); view(); move( n-1, via, src, dst, view ); } }
move1
ist eine (friend-)Funktion zum Verschieben eine Scheibe vom Turmsrc
(Source) nachdst
(Destination).view
ist ein Funktor, in dem der Anwender die Ausgabe der Szene unterbringen kann.Cpp_Anfaeger schrieb:
Werner Salomon schrieb:
Die Zeilen 313 bis 328 lassen sich in einer kleinen for-Schleife zusammenfassen
Ja, ich habe den Hang, alles so zu programmieren, damit ich sofort sehe, was vor sich geht.
Das solltest Du auch so beibehalten. Was mich störte war eigentlich nur das S
lice::setSlice
. Folgendes wäre auch noch eine Alternative:Slice slices[] = { Slice( "***" ), Slice( "*******" ), Slice( "***********" ), Slice( "***************" ), Slice( "*******************" ), Slice( "***********************" ) }; // 3 Türme erstellen + Turm_1 mit Scheiben auffüllen Tower tower_1( begin(slices), end(slices) ), tower_2, tower_3; // Tower tower_1( slices, slices + sizeof(slices)/sizeof(*slices) ), tower_2, tower_3; // bei C++98
Dann brauch es noch einen weiteren Konstruktor:
class Tower { public: Tower(); template< typename InItr > Tower( InItr first, InItr last ) : m_tower_content( first, last ) {}
.. wobei ich auch auch auf die Slice-Klasse ganz verzichten würde. Es reicht, im Turm eine Menge von (Scheiben-)Ids abzulegen - von 1 für die kleinste Scheibe und dann fortlaufend aufsteigend. Und erst bei der Anzeige wird entschieden, wie eine Scheibe genau aussieht.
Gruß
Werner
-
Werner Salomon schrieb:
In Der Methode Display::gotoXY() hast Du X und Y vertauscht
Hmmm ... eigentlich dachte ich, dass Bill das in seiner Klasse vertauscht hatte
, weil ich das Ganze in meiner Klasse umdrehen musste, damit aus x = line wird bzw. y = column:
void Display::gotoXY(const int &line, const int &column) { COORD p; p.Y = line; // Zeile p.X = column; // Spalte SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), p); }
Werner Salomon schrieb:
Was mich störte war eigentlich nur das Slice::setSlice.
Mich hat es auch gestört. Super ... wieder was gelernt ... ich hatte vorher nicht herausfinden können, wie man die Initialisierung von 6 Elementen auf einmal syntaktisch hinbekommt.
Zu den Templates ... ich denke, dass ich noch viel mehr mit Template machen muss. Es wird ja oft gesagt, dass man das nur kurz machen sollte, um zu wissen, was das ist, da man es später kaum benutzt. Bisher dachte ich, dass man nur Template benutzt, wenn es darum geht, Schreibarbeit für Überladungen zu sparen. Sollte man generell Template verwenden? Hat es bezogen auf die Geschwindigkeit Vorteile, wenn man Template benutzt?
-
Hat es bezogen auf die Geschwindigkeit Vorteile, wenn man Template benutzt?
Nicht direkt. Templates sparen nur Schreib- und Wartungs-Aufwand. Sie machen an sich den Code nicht schneller.
Sollte man generell Template verwenden?
Nein, absolut nicht!
-
Arcoth schrieb:
Hat es bezogen auf die Geschwindigkeit Vorteile, wenn man Template benutzt?
Nicht direkt. Templates sparen nur Schreib- und Wartungs-Aufwand. Sie machen an sich den Code nicht schneller.
Sollte man generell Template verwenden?
Nein, absolut nicht!
Habe ich mir auch gedacht ... bis ich diese Diskussion gelesen hatte. Ich weiß allerdings nicht, ob ich die Übersetzung richtig verstanden habe:
http://stackoverflow.com/questions/8925177/c-templates-for-performance
-
Cpp_Anfaeger schrieb:
Habe ich mir auch gedacht ... bis ich diese Diskussion gelesen hatte. Ich weiß allerdings nicht, ob ich die Übersetzung richtig verstanden habe:
http://stackoverflow.com/questions/8925177/c-templates-for-performance
Das ist auch richtig. Templates sind in den Fällen viel schneller. Aber du musst gucken, gegenüber was sie schneller sind: Da geht es um den Weg, wie man in C generisch programmiert. Das funktioniert nämlich damit, dass man in C das Typsystem aushebelt, was typabhängige Optimierungen ziemlich unmöglich macht.
template <typename T> void foo(T &t); void bar(int &i); void foobar(void *v);
Mal angenommen, alle davon würden das gleiche tun. Dann wären foo und bar in allen Fällen gleich schnell, denn foo<int> wäre identisch zu bar. Der Vorteil von foo wäre, dass man sich nicht auf int beschränkt hätte. foobar wäre aber möglicherweise langsamer als die beiden anderen Varianten, selbst wenn man es mit einem entsprechend gecasteten int* aufruft. Denn wenn der Code für foobar erzeugt wird, dann kann der Compiler nicht wissen, womit die Funktion später mal aufgerufen wird und kann diesbezüglich weniger optimieren.
-
Achso, danke alles klar ... nur gegenüber C in diesen Fällen.
-
Das hat mit dem Typsystem oder dessen Umgehung nicht unbedingt was zu tun, sondern damit, dass die Generik in dem Fall erst zur Laufzeit aufgelöst wird. Etwas entsprechendes kannst du in C++ mit virtuellen Funktionen bauen, das hat die gleichen Nachteile.
-
Bashar schrieb:
Das hat mit dem Typsystem oder dessen Umgehung nicht unbedingt was zu tun, sondern damit, dass die Generik in dem Fall erst zur Laufzeit aufgelöst wird. Etwas entsprechendes kannst du in C++ mit virtuellen Funktionen bauen, das hat die gleichen Nachteile.
Ist vielleicht etwas ungünstig formuliert gewesen. Der Verzicht auf das Typensystem macht hier notwendig, dass es erst zur Laufzeit aufgelöst wird. Aber ja, nicht jeder Verzicht auf das Typensystem macht dies unbedingt notwendig und nicht jede Auflösung zur Laufzeit ist bedingt durch eine Umgehung des Typsystems.