Wie räumt man am besten C++-Code auf?

Hallo liebe C+±Pros,

Ich bin ein Anfänger, was C++ angeht. Ich musste mir das Programmieren zudem selbst beibringen, weshalb mein Wissen wohl lückenhaft ist.

Die Problemstellung ist folgende:

Ein Haufen C/C++ - Code liegt vor mir. Der Ersteller des Codes hat quasi alle Funktionen in eine Datei geschrieben. :frowning: Ich möchte diesen Code nun ein wenig aufräumen, weil er ziemlich unübersichtlich ist.

Wie geht man da am besten vor?

Hier, was ich bisher gemacht habe:

Ich habe Funktionen aus der Haupt-CPP-Datei entfernt und in separate CPP-Dateien gesteckt. Das Kompilieren funktionert ohne Fehlermeldungen und auch sonst funktioniert alles soweit. Ich habe diese CPP-Dateien über #include dateiXY.cpp in die Originaldatei eingebunden. In jede dieser neuen CPP-Dateien musste ich die notwendigen Header hinzufügen.

Würdet ihr das auch so machen? Was könnte ich besser machen?

In einem C+±Buch habe ich jetzt gelesen, dass man möglichst keine C+±Dateien in andere C+±Dateien einbinden soll.

Wieso nicht? Wie macht man das sonst?

Eine weitere Frage zu einem anderen Thema habe ich noch. Und zwar beinhaltet die jetzige Originaldatei immer noch sehr viel unübersichtlichen Code. Es wurde für das Programm ein sehr großer Runtimer verwendet, der viele if-Schleifen enthält. Ich könnte nun ja die ganzen if-Schleifen in Funktionen packen, doch dann müsste ich auch die ganzen Parameter und Datentypen heraussuchen, was für mich im Moment zu schwierig und insbesondere zu aufwendig erscheint.

Gibt es eine einfache Möglichkeit diese if-Schleifen mit einem Platzhalter zu ersetzen, wobei der eigentliche Code wieder in eine CPP-Datei ausgelagert wird? Ich würde mich an dieser Stelle sehr über ein kleines Code-Beispiel freuen. :smile:

Im Voraus vielen Dank für Eure Tipps und Ideen!

Viele Grüße,

Hank

Hallo Hank,

prinzipiell sollte man in Header-Dateien *.h oder *.hpp nur Dinge deklarieren. In den entsprechenden Quelldateien *.cpp sollten dann die Implementierungen (Definition) zu finden sein. Ausnahmen gibt es da natürlich auch - Stichwort Templates. Bei kleineren Klassen/Funktionen (z.b. auch Get/Set) kann man auch Dinge inline implementieren, d.h. Deklaration und Definition landen im Header, bedarf jedoch auch angepasste makefiles (um Änderung beim Kompilieren mitzubekommen) und spätestens statischen Member wandern so oder so irgendwann in die Quelldatei. Besser ist also die Trennung.

Man sollte keine cpp-Datei irgendwo einbinden (Ausnahme: Templates).
Generell sollten die Quell-Dateien einzeln kompilierbar sein (mit den ensprechenden Deklarationen aus den Headern der nötigen anderen Resourcen). Der Linker verbindet dann die durch das Kompilieren entstandenen Objekt-Datein zu einem ausführbaren Programm - vereinfacht gesagt.

g++ -c test.cpp -o test.o // kompiliere test.cpp zu test.o
g++ -c other.cpp -o other.o // kompiliere other.cpp zu test.o
g++ other.o test.o -o test // linke die beiden Objekt-Dateien zum Programm test

Ist natürlich in VisualStudio (oder was auch immer du als Entwicklungsumgebung verwendest) das gleiche, man bekommt es nur nicht wirklich mit…

Wenn du jetzt in test.cpp etwas änderst brauchst du nur einen Teil neu kompilieren und neu linken. Um nur einen Vorteil zu nennen…

Hier schnell ein kurzes Beispiel dazu (könnte aufgrund von Tippfehlern nicht kompilieren):

-( werden die Zeilenumbrüche nicht richtig interpretiert, warum auch immer :frowning:

-----------\> test.h: test.h: test.cpp: test.cpp: 

Zum zweiten Thema:

Evtl. verstehe ich nicht genau was du meinst und wie du die if-Anweisungen beseitigen willst. Also bitte gib uns ein Beispiel in Code-Schnipsel.

Ansonsten gibt es zum Ersetzen die Möglichkeit von Makros, jedoch ist das schlechter Stil. Dazu am besten einfach mal das Buch "C++ Coding Standards: 101 Rules, Guidelines, and Best Practices" von Sutter und Alexandrescu anschauen. Mal davon abgesehen, das da noch sehr viele andere hilfreiche Tips dabei sind (genauer 100). Einhellige Meinung ist: Makros sollten nicht verwendet werden, bis auf ein paar Sonderfälle!

Oder suche mal im Netz nach der switch-Anweisung....

So wie es scheint, ist das Design deines Quellcodes unter Umständen nicht ganz so gelungen. Mit einem kleinen Beispiel könnten man deine Fragen bestimmt einfacher beantworten.

Bspw. frage ich mich, warum die Funktionen so viele Paramter benötigen. Evtl. können einige Parameter auch Member werden und dann wird die Geschichte einfacher ...usw... Aufsplitten in mehrere Klassen die Teilaufgaben gekapselt übernehmen, evtl. auch Vererbung... usw....

Grüße sascha

PS.: if-Anweisung sind Anweisungen, keine Schleifen

Hey Sascha,

vielen Dank für Deine ausführliche Antwort. Ich werde mir das morgen (ohh… nicht mehr aktuell; doch schon heute:wink: mal genauer anschauen.

Hier erstmal ein paar mehr Infos zum zweiten Thema:

Es handelt sich dabei um einen Runtimer, der so lange immer wieder von vorne ausgeführt wird bis eine Liste von Befehlen abgearbeitet wurde, daraufhin wird er verlassen (wenn man das so sagen kann). Diese Befehle werden über unterschiedliche Schnittstellen an Geräte gesendet und diese senden eine Antwort zurück. Die gesendeten Befehle und die Antworten werden jeweils gespeichert. Mit Parametern meinte ich alles, das, was an die if-Anweisungen übergeben wird, also hier insbesondere: Das File-Handle, die Schalter [Wenn man das so nennt; z.B.: m_iBefehl; siehe unten], den Befehlsstring und den Antwortstring.

Die Originalversion des Runtimers enthält viele stark verschachtelte if-Anweisungen. Das vereinfachte Grundgerüst des Runtimers sieht ungefähr so aus:

void __fastcall TSteuerung::RunTimer(TObject *Sender)

{
static int err,iErr=0;
static char Befehl[300],Antwort[300],Geraetename[30];
sDaten Daten;

if(m_iBefehl == BEFEHL_HOLEN)
{
//Wenn kein Programm geladen, hole Dir eine Zeile

if(m_fProg != NULL)

err += GETLIN(m_fProg, Befehl);
//Wenn das Ende der Befehlsliste erreicht ist, …
if (err == 2)

{
//… dann beende den Runtimer
m_iBefehl == KEINE_BEFEHLE_MEHR_ALSO_BEENDEN
}

}

if (m_iBefehl == BEFEHL_ABARBEITEN)

{
if (m_iBefehl != ALLE_BEFEHLE_ABGEARBEITET)
{
//Wenn keine Fehler beim Einlesen aufgetreten sind, und ein //sinnvoller Befehl übergeben wurde,…
if (err == 0 && …)
{//…dann öffne die Ports und sende an die Geräte.}

//Wenn keine Fehler bis hier her aufgetreten sind, UND Befehl Ok, …
if (err == 0 && …)

{
//…dann arbeite den Befehl ab

m_iBefehl = BEFEHL_ABARBEITEN;}

}
if (m_iBefehl == BEFEHL_ABARBEITEN)

{
//Antwort von Gerät empfangen
,
Daten = m_tCom->Lesen(&iErr);
strcat(Antwort,Daten.szm_Puffer);
//Antwort und Befehl für Log-File aufarbeiten und in Log-File schreiben
,
fprintf(m_fLog,"************\n" "Befehl: ,Befehl);
fprintf(m_fLog,„Antwort:\n“,Antwort);

fflush(m_fLog);

m_iBefehl = BEFEHL_HOLEN;

}
}

Hallo Hank,

das sieht doch schon sehr nach der switch-Anweisung aus.

Jedoch entnehme ich jetzt auch dem Kontext, das du schon eher in der Nähe der Hardware arbeitest, war mir bei dem ersten Post nicht so aufgefallen. Ich denke das ändert die Geschichte ein wenig. Da zumindest meiner Meinung nach, dort nicht unbedingt jedes C+±Design-Pattern eingehalten werden sollte. Dort ist meistens die Performance wichtiger als super duper sauberes C++ und alles muss in einer Klasse wegekapselt sein und allgemein usw. Wenn man es so hinbekommt und die Performance stimmt, okay, jedoch wiedersprechen sich in gewisser Weise Allgmeinheit, Geschwindigkeit und auch der Aufwand der investiert werden muss. Bitte korrigiert mich jemand falls ich da komplett falsch liegen sollte. Wie auch immer, es sollte natürlich trotzdem übersichtlich sein und das ist ja auch dein Ziel.

So oder ungefähr so handle ich ähnliche Sachen (erst nach dem Device schauen und dann den Befehl anschauen ist wahrscheinlich besser) - Ist wieder nur ein Besipiel aus dem Kopf:

enum ECommands { BEFEHL\_HOLEN, BEFEHL\_LESEN, \_\_BEFEHL\_\_ };
enum EDevice { DEVICE\_A, DEVICE\_B, \_\_DEVICE\_\_ };

HandleCommand( ... )
{
 bool ReturnValue = false;

 switch( m\_iBefehl )
 {
 case BEFEHL\_HOLEN: 
 { ReturnValue = HandleSub( ... ); } break;
 case BEFEHL\_LESEN:
 { ReturnValue = HandleSub( ... ); } break;
 default: 
 { ReturnValue = false; }
 }

 return ReturnValue;
}

HandleSub( ... )
{
 bool ReturnValue = false;

 switch( irgendwas anderes darunterliegendes bspw. )
 {
 case DEVICE\_A: { ReturnValue = ReadData( m\_Devices[DEVICE\_A] ); } break;
 default: { ReturnValue = false; } break;
 }

 return ReturnValue;
}

bool ReadData( const EDevice Device )
{
 // read
}

Grüße Sascha

Hey Sascha,

die switch-Anweisung ist ne schöne Sache, die kann ich auch gut für den restlichen Code gebrauchen. Dadurch gewinnt man schon einiges.

Am liebsten würde ich den Code aber noch übersichtlicher gestalten, d.h. Teile der Codes in andere Dateien packen. Diese Dateien sollen dann wiederum in unterschiedliche Ordner.

Beispiel 0:
if (z=3)
{
if(x=1)
{//viel Code 1}
if{y=2}
{/viel Code 2}
}//if (z=3) -> ENDE

Also aus dem vorherigen wird:

Überordner „if (z=3)“:
{
Hauptdatei „if (z=3)“

Unterordner „if(x=1)“:
{Unterordnerhauptdatei „if(x=1)“}

Unterordner „if(y=2)“:
{Unterordnerhauptdatei „if(y=2)“}
}

Dadurch steht dann weniger Code in der Hauptdatei „if (z=3)“ und man gewinnt an Übersichtlichkeit.
Also mit Makros würde das wohl gehen, z.B.: könnte man den Code der einzelnen if-Anweisungen in ein Makro packen und die Definition des Makros jeweils in eine eigene Headerdatei auslagern. Ich habe auch noch eine andere Möglichkeit gefunden; aber von allen Möglichkeiten, die ich bisher gefunden habe, wird abgeraten. :confused: Hier trotzdem zwei Beispiele dazu:

Erstes Beispiel: Lösung des Problems mit Makros:
------------header vieltext.h ------------
#define VIEL_TEXT „TextTextTextTextTextTextTextTextTextText“\
„TextTextTextTextTextTextTextTextTextText“\
„TextTextTextTextTextTextTextTextTextText\n“
------------header vieltext.h ENDE------------

------------Hauptdatei hauptdatei.cpp ------------
#include vieltext.h
int main(void) {
printf(VIEL_TEXT);
}
------------Hauptdatei hauptdatei.cpp ENDE------------

(Quelle: http://www.hs-augsburg.de/~sandman/c_von_a_bis_z/c_0…)

Zweites Beispiel: Das Problem könnte man auch so lösen, indem man den Code in eine andere Datei auslagert:

------------ refrain.block ------------

{ ::std::cout
#include

int main()
{ ::std::cout

Hallo Hank,

so wie es aussieht solltest du dir als erstes mal aufzeichen welche Daten und auch entsprechend passende Funktionen gruppiert werden können. Dazu ist am besten UML geeignet. Schau dazu einfach mal bei Wikipedia. Danach kannst du die Daten dann in Klassen mit samt ihren behandelnden Funktionen packen. Ist ja auch der eigentliche Sinn und Zweck von Objekt orientiertem Design, was ja auch C++ zu Grunde liegt. Einfach so beliebige Teil in andere Dateien/Ordner und Funktionen auszulagern macht keinen Sinn, da du später sonst Probleme haben wirst diese wieder zusammenzubekommen. Ein Beispiel:

Du hast bspw. eine Klasse für ein Device, diese enhält alle Informationen zu diesem und irgendwelche Funktionen die nur dieses betreffen. Sie stellt nur die wirklich wichtigen Dinge nach aussen zur Verfügung (public), andere Dinge sind protected oder private. Hängt davon ab, ob du später die Klasse noch beerben möchtest. Evtl. später SpecialDevice erbt von Device und erweitert Device um spezielle Funktionalitäten usw. Dazu vielleicht einfach in einem guten C+±Buch nochmal nachlesen. Die interne Logik dieser Device-Klasse sollte nach aussen verborgen bleiben.

Du musst so oder so die Sachen zerpflücken, daran wirst du nicht vorbeikommen. Dein Handler/Runtime oder wie auch immer man das dann nennen mag, hält dann Instanzen aller nötigen Dinge und ruft die Funktionen dann auf, evtl. dann in einer switch-Anweisung. Das hängt ein wenig vom Problem ab und ist von hier so nicht wirklich einschätzbar.

Die einzelnen Klassen kannst du dann einfach in Dateien (*.h für Deklarationen und *.cpp für Definitionen) und auch Ordnern unterbringen. Makros sind langfristig keine gute Wahl, da du sie später wahrscheinlich so oder so wieder entfernen musst, wie schon gesagt, bis auf ein paar kleine Sonderfälle. Wenn du Konstanten hast kann du sehr leicht auch sowas machen:

class Whatever
{
 // bla
 static const float MEINE\_ZAHL\_DREI = 3.333f;
 // bli
 // blu
}

Makros können ja auch einfach in inline Funktionen untergebracht werden, ob diese der Kompiler auch wirklich inline packt, hängt von vielen Kriterien ab. Jedoch sollte Optimierung so oder so erst passieren, wenn die prinzipielle Funktionalität gegeben ist.

„Premature optimization is the root of all evil“
von Donald Knuth

Der hat übrigens sehr interessante Bücher, jedoch haben die nicht viel mit C++ zu tun, die sind eher allgemein und nicht auf eine bestimmte Sprache zugeschnitten. Jedoch geht das auch schon wirklich ans Eingemachte :wink:

Du kannst natürlich auch einfach Funktionen auslagern und diese dann mit tausenden Parametern aufrufen, jedoch hat das dann nicht viel mit c++ zu tun und macht die langfristig das Leben auch nicht leichter.

Wenn du das dennoch machen möchtest kannst du einfach eine Header-Datei die Deklaration packen und in der entsprechenden Source-Datei implemetieren.

#ifdef __cplusplus
extern „C“ {
#endif

// your deklaration

#ifdef __cplusplus
}
#endif

Dazu vielleicht auch nochmal hier nachschauen:
http://www.parashift.com/c+±faq-lite/

Übrigens eine sehr gute Geschichte um C/C++ zu lernen. Ich will gar nicht anfangen auf einzelne Dinge auf der Website hinzuweisen, die sind einfach fast alle lesenswert.

Viel Spaß beim lesen!!

Grüße Sascha

Hey Sascha,

ich komme ja gar nicht hinterher alle Deine Vorschläge
abzuarbeiten. :wink: Vielen Dank! :smile:)

Also, ich habe mich mal zu dem Stichwort UML
informiert.

Die englische Wikipedia liefert dazu folgende
Definition:

„Unified Modeling Language (UML) is a standardized
general-purpose modeling language in the field of
software engineering.“

Eine Modeling Language ist eine künstliche Sprache, die
dazu verwendet wird Strukturen graphisch darzustellen.
Das Ziel einer solchen Sprache ist komplexe Strukturen
mit Hilfe von Graphiken zu vereinfachen.

Nach der Definition habe ich sogar schon sowas wie UML
verwendet. :smiley: Die Programmstruktur habe ich bereits mit
OpenOffice Draw graphisch dargestellt. Jede
verschachtelte if-Anweisung wurde dabei bildlich zu
einem Ast meines (Programm-)baumes.

Reverse Engineering im Zusammenhang mit UML ist
sicherlich einer der Gründe, warum UML gerne zur
Programmierung verwendet wird. Beim Reverse Engineering
wird Source Code in eine graphische Darstellung
überführt. Dies ermöglicht z.B. das Editieren des
Quellcodes in dieser graphischen Umgebung (Wenn das so
nicht stimmt, korrigiert mich bitte.). Wie verbreitet
ist UML eigentlich in der Programmierung? Verwendet das
jeder?

Es gibt einiges an Software zu diesem Thema. Hier eine
Liste dazu: http://www.jeckle.de/umltools.htm Ich habe
mir gerade Visio von Microsoft installiert (weil ich
die Programme kostenlos beziehen kann). Mit diesem
Programm und einer IDE von demselben Anbieter kann man
das z.B. machen. Hier eine Anleitung
dazu:http://msdn.microsoft.com/de-
de/library/aa140255(office.10).aspx

Kann man mit einer solchen Reverse Engineering Software
auch die gesamte Programmstruktur, d.h. mit allen if-
Anweisungen, while-Schleifen, etc. darstellen lassen?
Das wäre echt super! :smile:))

Jetzt verstehe ich auch endlich besser, was es mit
dieser objektorientierten Programmierung auf sich hat,
und was ihre Vorteile sind.

Hehe… vor lauter Lesen komme ich gar nicht mehr dazu
Fragen zu stellen. :wink:

Inline-Deklarationen/Funktionen dienen zur
Laufzeitoptimierung. Japp, aber bis ich das vielleicht
machen werde, wird noch viel Zeit vergehen. Es entsteht
ein Laufzeitaufwand für Funktionsaufruf,
Parametervermittlung und Wertrückgabe (Quelle:
http://www.imb-
jena.de/~gmueller/kurse/c_c++/c_inline.html). Code kann
man damit auch schön in separate Dateien auslagern. Für
jeden, der sich für Laufzeitoptimierung mit Inline-
Funktionen interessiert, hier ein kleiner Einblick:
http://www.uni-
koeln.de/rrzk/kurse/unterlagen/CPPKURS/HTML/inline.htm

So, ich werde mich jetzt mal daran machen ein
Minimalbeispiel zur Verwendung von Klassen zu
schreiben.

Zitat Sascha: „Die einzelnen Klassen kannst du dann
einfach in Dateien (*.h für Deklarationen und *.cpp für
Definitionen) und auch Ordnern unterbringen.“

Das scheint dabei irgendwie wichtig zu sein. :wink:

Gute Nacht,

Hank