Programmierstil
Anweisungen
- Zuweisungen innerhalb
von Ausdrücken sind nicht gestattet.
| Unzulässig: |
while ((c = getchar()) != EOF)
n = a[++i];
o = p = q = 0; |
- Die Anweisung goto ist nicht
erlaubt.
Benutzerdefinierte Typen
Allgemeines
- Die Verwendung benutzerdefinierte Typen ist der Verwendung von Standardtypen
vorzuziehen.
typedef int PRIORITAET;
typedef char FEHLERCODE;
typedef enum { SZ_KLINGEL = '\a', SZ_TAB = '\t'} SONDERZEICHEN;
- Werden Zeiger auf Objekte eines benutzerdefinierten Typs (insbesondere
Klassen und Structs) benötigt, so ist ein Zeigertyp mit typedef
zu erzeugen.
Zu jeder öffentlichen Klasse bzw. Struct-Typ ist generell ein Zeigertyp
zu deklarieren. Dieser Zeigertyp trägt den
(eigentlichen) Namen der Klasse mit einem vorangestellten großen
P.
class CComplex;
typedef CComplex* PComplex; // Groß/Kleinschreibung trotz typedef!
Klassendeklarationen
- Datenelemente sind als private zu
deklarieren.
- Alle polymorphen Funktionen sind – obwohl obsolet – auch in abgeleiteten
Klassen mit dem Schlüsselwort virtual zu versehen.
- inline-Elementfunktionen sind
unzulässig, da sie die Implementierung, die sie eigentlich verstecken
sollten, publizieren. Ausnahme: private Klassen.
- Polymorphe inline-Funktionen sind nicht erlaubt, sie werden
vom Compiler sowieso ignoriert.
- Elementfunktionen, die den Objektzustand nicht verändern, sind
const zu deklarieren (In der Regel sind das Selektoren).
Datenobjekte
- (C, C++) Variablen sind, nach Möglichkeit, dort zu deklarieren,
wo sie erstmalig benutzt werden.
- (C, C++) Eine Variablendefinition ohne Initialisierung ist unzulässig.
Ausnahmen:
- Vektoren mit deutlich mehr als 10 Elementen (Strings sind immer – ggf.
mit "" – zu initialisieren),
- (C++) Exemplare von Klassen mit explizit deklariertem default-Konstruktor,
- (C) Bei hardwarenaher Programmierung (z.B. zeitkritische Interrupt-Handler)
darf in Ausnahmenfällen mit nicht initialisierten funktionslokalen
Variablen gearbeitet werden. Dies ist in einem Kommentar zu begründen.
Wird eine solche Ausnahme angewandt, ist der Speicher durch memset()
mit Nullen zu füllen.
- (Pascal, Delphi) Globale Datenobjekte sind bei ihrer Deklaration zu
initialisieren. Lokale Datenobjekte werden initialisiert, bevor irgendeine
andere Anweisung ausgeführt wird:
function Maximum(const dieWerte: CMesswerte): double;
var i: integer;
begin
result := 0.0; // zuerst das Funktionsergebnis
i := 0; // dann
die lokalen Datenobjekte
Anweisungen;
end {Maximum};
- Standard-Makros sind bereits in diversen Standard-Header-Dateien definiert.
Eigene Makros sollten, wann immer möglich, vermieden werden.
- Öffentliche Variablen (extern) sind unzulässig.
Der Zugriff auf ein Modul/Klassengedächtnis geschieht über Zugriffsfunktionen.
Private globale Variablen sowie private Funktionen erhalten das Schlüsselwort
static.
- Konstanten sind, zur Wahrung der
Typensicherheit, immer als konstante Datenobjekte, nicht aber mit #define
zu definieren. (C) Ausnahme C51: Hier würde wertvoller Datenspeicher
ver(sch)wendet.
- Generell sind Konstanten (besonders Strings) möglichst in der
Resource der Anwendung zu plazieren.
- Datenobjekte, insbesondere Zwischenergebnisse sind möglichst mit
const zu deklarieren.
- Funktionslokale static-Objekte sind unzulässig. Sie sind
außerhalb der Funktion als private globale Objekte zu definieren.
- Bitfields sind zu unzulässig. Ihre Verwendung
ist nur bei hardwarenaher Programmierung oder wenn sie aus einer fremden
Bibliothek stammen gestattet.
Funktionen
Parameter
- Eine Funktion muß durch Vorbedingungen sicherstellen, daß
die Werte der Eingabeparameter
gültig sind.
- Eine Funktion muß durch Nachbedingungen sicherstellen, daß
der Rückgabewert (und, falls verwendet, der Wert jedes Ausgabeparameters)
gültig ist.
- Funktionen sollten nur Eingabeparameter
erhalten, die möglichst const zu deklarieren sind.
- Eingabeparameter, die von einem Klassen- oder struct-Typ sind,
sollten immer als Referenzen (const &) übergeben werden.
- Funktionen mit Steuerparametern
sind zu vermeiden. Die Konstruktion
int LiesFehleranzahl(void);
int LiesErfolganzahl(void);
int LiesEreignisanzahl(void);
ist unbedingt vorzuziehen gegenüber
enum STATUS { STAT_FEHLER, STAT_ERFOLGE, STAT_TOTAL };
int LiesAnzahl(const STATUS stat);
- Müssen dennoch Steuerparameter
an eine Funktion übergeben werden, so ist hierfür ein enum
zu deklarieren.
enum STATUS { STAT_FEHLER, STAT_ERFOLGE, STAT_TOTAL };
int LiesAnzahl(const STATUS stat);
int nAnzahl = LiesAnzahl(STAT_FEHLER);
Handelt es sich um eine Elementfunktion, so ist dieses enum lokal
in der Klasse zu deklarieren, deren Element die Funktion ist.
class CGeraet
{
enum STATUS { STAT_FEHLER, STAT_ERFOLGE, STAT_TOTAL
};
int LiesAnzahl(const STATUS stat);
};
int nAnzahl = derDrucker.LiesAnzahl(CGeraet::STAT_FEHLER);
- Die Deklarationen von Funktionen bzw. Elementfunktionen enthalten generell
auch die Namen der Parameter. Die Namen sind bei Deklaration und Implementierung
der Funktion gleich.
- (C, Pascal) Funktionen eines Moduls, das einen abstrakten
Datentyp kapselt und die als Elementfunktionen dieses Datentyps aufgefaßt
werden können, erhalten einen Zeiger auf das bearbeitete Exemplar
als ersten Parameter. Dieser Zeiger trägt den Namen this (alternativ
auch pThis).
LONG DifferenzInSekunden(PZeit this, const CZeit dieZielzeit);
Überladen von Funktionen
Funktionen dürfen ausschließlich polymorph überladen
werden. Konstruktionen der Form
class CBasis
{
public:
void f(const int i);
};
class CErbe : public CBasis
{
public:
void f(const int i);
};
sind unzulässig, da sie die Gefahr bergen, daß die tatsächlich
aufgerufene Funktion bis zur Unzugänglichkeit verschleiert wird. Im
Beispiel oben würde
typedef CBasis* PBasis;
typedef CErbe* PErbe;
PErbe pErbe = new CErbe;
PBasis pBasis = PBasis(pErbe); // zulässiger Cast
pBasis->f();
zu einem Aufruf von CBasis::f() führen, was bedeutete,
daß pErbe sein Verhalten aufgrund des Casts ändert. CErbe::f()
wäre völlig unerreichbar. Stattdessen sind Funktionen, die in
Abkömmlingen überladen werden sollen, generell als virtual
zu deklarieren:
class CBasis
{
public:
virtual void f(const int
i);
};
class CErbe : public CBasis
{
public:
virtual void f(const int
i);
};
Funktionen mit einem
Rückgabewert
- Funktionen mit Rückgabewerten (außer Statusrückgaben)
dürfen keine Seiteneffekte haben.
| Unzulässig: |
SucheUndErsetzeMinimum(int nAnzahl, int naZahlen[]);
// Sucht das Minimum im Vektor und entfernt es |
| Lassen sich Seiteneffekte nicht vermeiden, weil z.B. eine unteilbare
Operation implementiert werden soll (z.B. CStack::Pop() liefert
das Top-Element eines Stacks und aktualisiert den Stackzeiger), so ist
dies ausführlich zu kommentieren! |
- Funktionen, die einen Zeiger
zurückliefern, liefern im Fehlerfall NULL. Ein zurückgelieferter
Wert, der von NULL verschieden ist, stellt einen gültigen
Zeiger dar!
- Funktionen liefern skalare Werte oder Exemplare einer Klasse auf dem
Stack zurück.
CComplex Wurzel(const CComplex& z);
// liefert die komplexe Wurzel.
CComplex Wurzel(const CComplex& z);
// Kommentare
{
CComplex result(0.0, 0.0);
…
return result;
}
…
{
cout << Wurzel(CComplex(1.0, -3.0));
}
- Funktionen, die eine neues Exemplar einer Klasse liefern, das auf dem
Heap gespeichert wird, erzeugen ein Objekt mit new (C++) oder
malloc() (C). Der Aufrufer ist jeweils dafür verantwortlich,
daß der so angeforderte Speicher mit delete (C++), free()
bzw. einer unten beschriebenen Delete…()-Funktion wieder freigegeben
wird.
Der Name einer solchen Funktion beginnt mit New. Bei der Deklaration
der Funktion sollte im Kommentar darauf hingewiesen werden, daß der
Aufrufer für die Freigabe des angeforderten Speichers verantwortlich
ist..
Zu einer solchen New…()-Funktion sollte eine passende Delete…()-Funktion
existieren, die den angeforderten Speicher freigibt.
PComplex NewWurzel(const CComplex& z);
// liefert die komplexe Wurzel. Der Aufrufer ist
für die
// Freigabe des angeforderten Speichers verantwortlich.
PComplex NewWurzel(const CComplex& z);
// Kommentare
// RETURN VALUE ein Zeiger auf die komplexe Wurzel von z.
// DESCRIPTION Der Aufrufer ist für die Freigabe des
// angeforderten
Speichers verantwortlich.
{
PComplex result = new CComplex(0.0, 0.0);
…
return result;
}
…
{
PComplex p = NewWurzel(CComplex(1.0, -3.0));
cout << *p;
DeleteWurzel(p);
}
- Die Anweisung return innerhalb von Funktionen (nicht am Ende)
ist, außer zum Abbruch in Fehlerfällen, unzulässig.
- Wenn der Rückgabewert einer Funktion in einer Variablen zwischengespeichert
wird, beginnt der Name dieser Variablen mit dem entsprechendem Typ-Prefix
und dem Wort "result", das geeignet
ergänzt werden darf. Diese Variable wird in der ersten Zeile der Funktion
nach den Vorbedingungen definiert. Eine solche Funktion hat nur eine einzige
return-Anweisung in der letzten Zeile, die result zurückgibt.
int Summe(const int naWerte[], const nAnzahl)
{
PRECONDITION(0 < nAnzahl);
int nResult = 0;
for (int i = 0; i < nAnzahl; ++i)
{
nResult += naWerte[i];
}
if (100 < nResult)
{
nResult = 100;
}
return nResult;
}
Unzulässige Konstruktionen sind kommentiert:
int Summe(const int naWerte[], const nAnzahl)
{
int nResult = 0;
for (int i = 0; i < nAnzahl; ++i)
{
nResult += naWerte[i];
}
// ------------------------ UNZULÄSSIGE
KONSTRUKTIONEN FOLGEN
if (Bedingung1())
{
return nResult; //
UNZULÄSSIG!
//
nResult, aber nicht am Ende
}
if (Bedingung2())
{
Anweisungen();
if (15 == nResult)
{
return
-27; // UNZULÄSSIG!
//
weder nResult noch am Ende
// und in einem Block
}
Anweisungen();
}
return 0;
// UNZULÄSSIG!
// Am Ende, aber nicht nResult
}
Funktionen ohne Rückgabewert
- Funktionen ohne Rückgabewert dürfen Seiteneffekte haben.
Solche Seiteneffekte dürfen sich nur auf die Klasse bzw. das Modul,
dessen Element die jeweilige Funktion ist, erstrecken.
- Eine Funktion, die vermeintlich mehrere Werte liefert, kann dies in
Form eines Exemplars einer Klasse liefern, welches die Werte als Attribute
besitzt.
| Läßt sich keine Klasse definieren, die die Werte geeignet
kombiniert, so tut die Funktion mit an Sicherheit grenzender Wahrscheinlichkeit
mehr als sie sollte! |
Funktionen mit
einem Statusrückgabewert
- Funktionen mit einem Status-Rückgabewert sind i. a. unzulässig.
Es ist ein Modifier aufzurufen, der eine Operation ausführt und dann
ein Selektor, der das Ergebnis der Operation liefert.
derPuffer.LiesDaten();
bErfolg = derPuffer.HatNeueDatenErhalten();
Unzulässig:
bErfolg = derPuffer.LiesDatenUndLiefereStatus();
- Status-Rückgabewerte dürfen nur verwendet werden, wenn die
Ausführung einer Operation unteilbar
mit der Ermittlung des Statuswerts verbunden ist oder zwei Funktionsaufrufe
aus Effizienzgründen zu teuer sind.
- Funktionen mit einem Statusrückgabewert dürfen Seiteneffekte
haben. Solche Seiteneffekte dürfen sich nur auf die Klasse bzw. das
Modul, dessen Element die jeweilige Funktion ist, erstrecken.
- Funktionen, die einen Statuswert zurückliefern, tun dies in Form
eines vorzeichenbehafteten Ganzzahlwerts (int, long, signed char).
Ist der zurückgegebene Wert kleiner als 0, liegt ein Fehler vor. Ein
Rückgabewert größer oder gleich 0 ist ein gültiges
Ergebnis.
Default-Konstruktor
- (C++) Default-Konstruktoren (Konstruktoren ohne Parameter) werden automatisch
für jede Klasse erzeugt. Werden sie in Klassen mit anderen Konstruktoren
nicht explizit definiert, kann das zu Problemen führen (z.B. bei Arrays
aus Objekten). Ist daher für eine Klasse irgendein Konstruktor definiert,
so muß
- ein Default-Konstruktor zur Verfügung gestellt werden oder
- ein Kommentar in der Klassendefinition stehen in der Form
// Kein Default-Konstruktor, weil…
Vorsicht: Auch X::X(int i = 0) ist ein Defaultkonstruktor!
- (Delphi) Der Default-Konstruktor wird wie folgt deklariert
constructor CKlasse.Create;
Copy-Konstruktor
- (C++) Copy-Konstruktoren werden automatisch für jede Klasse
erzeugt, sie kopieren alle Datenelemente einer Klasse. Die (implizite)
Deklaration für eine Klasse X lautet X::X(const X&).
In Klassen, die Zeiger- oder Referenzdatenelemente oder implizite Bezüge
auf Systemressourcen (z.B. File- oder Window-Handles) enthalten, muß
- ein expliziter Copy-Konstruktor definiert sein oder
- ein private deklarierter Dummy-Copy-Konstruktor definiert sein oder
- ein Kommentar in der Klassendefinition stehen in der Form
// Default Copy-Konstruktor ist korrekt, weil…
- (Delphi) Der Copy-Konstruktor wird wie folgt deklariert:
constructor CKlasse.Copy(const einExemplar: CKlasse);
Konstruktorverhalten
Schlägt eine Aktion eines Konstruktors fehl (z.B. Anforderung von
Speicher für ein Datenelement), so ist der Konstruktor dafür
verantwortlich, daß bereits ausgeführte Aktionen rückgängig
gemacht werden (z.B. Freigabe bereits angeforderten Speichers anderer Datenelemente).
Der Konstruktor löst in diesem Fall eine geeignete Ausnahme aus.
Destruktor
- (C++) In Klassen, die Zeiger- oder Referenzdatenelemente oder implizite
Bezüge auf Systemressourcen (z.B. File- oder Window-Handles) enthalten,
muß
- ein expliziter Destruktor definiert sein oder
- ein Kommentar in der Klassendefinition stehen in der Form
// Default Destruktor ist korrekt, weil…
- (Delphi) Der Destruktor wird wie folgt deklariert:
destructor CKlasse.Free; virtual;
Polymorpher Destruktor
Per Voreinstellung ist ein Destruktor nicht polymorph defniert. Das
führt bei abgeleiteten Klassen zum Aufruf des Destruktors der Basisklasse.
Daher muß in einer Klasse, die polymorphe Funktionen enthält,
auch ein polymorpher Destruktor definiert sein.
Zuweisungsoperator
- Zuweisungsoperatoren werden defaultmäßig für jede Klasse
erzeigt, sie kopieren alle Elemente einer Klasse. Die (implizite) Deklaration
für eine Klasse X lautet X& X::operator=(const X&).
In Klassen, die Zeiger- oder Referenzdatenelemente oder implizite Bezüge
auf Systemressourcen (z.B. File- oder Window-Handles) enthalten, muß
- ein expliziter Zuweisungsoperator definiert sein oder
- ein private deklarierter Dummy-Zuweisungsoperator definiert sein oder
- ein Kommentar in der Klassendefinition stehen in der Form
// Default Zuweisungsoperator ist korrekt, weil…
- Die Zuweisung a = a muß korrekt implementiert sein.
- Da Zuweisungen in Ausdrücken
unzulässig sind, sollten Zuweisungsoperatoren als
void operator = …
deklariert werden.
Ausnahmen
- Im Fehlerfall löst eine Funktion eine Exception aus. Die Rückgabe
von Fehlercodes ist zu unzulässig. Ausnahme: Sprachen die
Exceptions nicht unterstützen.
- Die von einer Funktion verursachten bzw. weitergegebenen Exceptions
sind bei der Deklaration mit anzugeben.
- Die Prüfung der Vorbedingungen geht dem Code der Funktion geht
generell voraus. Sind keine Vorbedingungen zu prüfen, so ist dies
in einem Kommentar zu erwähnen.
- Dem Code der Funktion folgt generell die Prüfung der Nachbedingungen.
Sind keine Nachbedingungen zu prüfen, so ist dies in einem Kommentar
zu erwähnen.
- Funktionen lösen genau dann Ausnahmen
aus, wenn sich trotz eingehaltener Vorbedingungen die Nachbedingungen nicht
herstellen lassen. Ausnahmen dürfen nicht verwendet werden, um Statuswerte
zu liefern! Es gilt immer:
- Genau dann wenn eine Funktion keine Ausnahme ausgelöst hat, gelten
ihre Nachbedingungen.
- Genau dann wenn eine Funktion eine Ausnahme ausgelöst hat, gelten
ihre Nachbedingungen nicht.
Das bedeutet insbesondere, daß eine Funktion keine Ausnahme auslösen
darf, wenn ihre Nachbedingungen gelten und daß eine Funktion eine
Ausnahme auslösen muß, wenn ihre Nachbedingungen nicht gelten.
Ferner löst eine Funktion eine Ausnahme aus, wenn ihre Vorbedingungen
nicht gelten. Die eigentliche Funktionsimplementierung kann sich auf die
Einhaltung der Vorbedingungen verlassen. Der Aufrufer einer Funktion ist
für die Einhaltung der Vorbedingungen verantwortlich. Eine Funktion
versucht nicht selbst, nicht erfüllte Vorbedingungen herzustellen.
- Kann eine Funktion eine Ausnahme selbst behandeln, so wird nach der
Behandlung (und Behebung der Situation, die zu der Ausnahme geführt
hat) der gesamte Funktionskörper erneut ausgeführt:
double Kehrwert(const double x)
// Die Funktion liefert 1/x, falls (0 != x) und 0.0 sonst.
// Das Beispiel (aus [Meye88]) ist
natürlich etwas konstruiert,
// da (0.0 == x) selbstverständlich eine Vorbedingung ist!
{
double result = 0.0;
bool bDivisionDurchNull = false;
do
{
try
{
result
= 0.0;
if
(!bDivisionDurchNull)
{
result
= 1.0 / x;
}
catch
(xmath)
{
bDivisionDurchNull
= true;
continue;
}
}
} while (false);
return result;
}
Zusicherungen
Funktionen sind mit Vor- und Nachbedingungen zu sichern [Meye88].
Hierzu empfiehlt sich die Verwendung geeigneter Makros, die sich ggf. durch
ein #ifdef DEBUG deaktivieren lassen. Da unterschiedliche Compiler
verschiedene Standardmakros für diesen Zweck zur Verfügung stellen,
ist es wenig sinnvoll, hier näher darauf einzugehen.
Nebenläufige Programmierung
Klassen und Module
- Alle Programme sind so zu entwerfen, daß sie in einer Multitasking-Umgebung
lauffähig sind.
- Der Benutzer eines Moduls ist nicht für die Absicherung kritischer
Abschnitte verantwortlich. Klassen sind nach Möglichkeit als Monitore
zu entwerfen.
Funktionen
- Funktionen sind möglichst wiedereintrittsfest zu entwerfen.
Andernfalls muß durch gegenseitigen Ausschluß (mutex) sichergestellt
werden, daß kritische Abschnitte nicht mehrfach betreten werden können.
Weitere Richtlinien
- Alle Elementvariablen werden in allen Konstruktoren initialisiert.
Es ist auch möglich, eine gemeinsame Initialisierungsfunktion zu definieren,
die von allen Konstruktoren der Klasse aufgerufen wird. Dann darf kein
Konstruktor Elementvariablen individuell initialisieren, es sei denn, die
Semantik des Konstruktors macht dies erforderlich.
- Öffentliche Variablen sind unzulässig.
- Öffentliche nicht-Elementfunktionen sind zu vermeiden.
- friend-Konstrukte sind zu vermeiden.
- Zuweisungen an this sind nicht erlaubt (nicht zu verwechseln
mit *this!).
- (C++) Die Benutzung von malloc() und free() ist nicht
erlaubt, stattdessen werden new und delete genutzt.
- (C++) Zugriffsfunktionen, die nur eine Referenz auf ein Datenelement
liefern, dürfen inline definiert werden.
- Der Zugriff auf Variablen anderer Objekte ist unzulässig (außer
in Copy-Konstruktoren und Zuweisungsoperatoren).
- Zeiger- und Referenzparameter, die eine const-Semantik beinhalten,
werden auch const definiert.
- Es sollte möglichst vermieden werden, komplexe Objekte innerhalb
von Schleifen zu definieren, da dort jedesmal der Konstruktor aufgerufen
wird.
- Implizite Typkonvertierungen (casts) sind verboten. Stattdessen sind
Konversionsfunktionen zu verwenden bzw. Cast-Operatoren zu implementieren.
[zurück]
| [weiter] | [Inhalt]
| [Einleitung] | [Layout]
| [Anhänge]
Copyright © 1996 by Uwe Sauerland