Programmierstil


Namengebung

Anweisungen

  1. Zuweisungen innerhalb von Ausdrücken sind nicht gestattet.
    1. Unzulässig: while ((c = getchar()) != EOF)
      n = a[++i];
      o = p = q = 0;
  1. Die Anweisung goto ist nicht erlaubt.

Benutzerdefinierte Typen

Allgemeines

  1. Die Verwendung benutzerdefinierte Typen ist der Verwendung von Standardtypen vorzuziehen.
    1. typedef int PRIORITAET;
      typedef char FEHLERCODE;
      typedef enum { SZ_KLINGEL = '\a', SZ_TAB = '\t'} SONDERZEICHEN;

  2. Werden Zeiger auf Objekte eines benutzerdefinierten Typs (insbesondere Klassen und Structs) benötigt, so ist ein Zeigertyp mit typedef zu erzeugen.
  3. 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

  1. Datenelemente sind als private zu deklarieren.
  2. Alle polymorphen Funktionen sind – obwohl obsolet – auch in abgeleiteten Klassen mit dem Schlüsselwort virtual zu versehen.
  3. inline-Elementfunktionen sind unzulässig, da sie die Implementierung, die sie eigentlich verstecken sollten, publizieren. Ausnahme: private Klassen.
  4. Polymorphe inline-Funktionen sind nicht erlaubt, sie werden vom Compiler sowieso ignoriert.
  5. Elementfunktionen, die den Objektzustand nicht verändern, sind const zu deklarieren (In der Regel sind das Selektoren).

Datenobjekte

  1. (C, C++) Variablen sind, nach Möglichkeit, dort zu deklarieren, wo sie erstmalig benutzt werden.
  2. (C, C++) Eine Variablendefinition ohne Initialisierung ist unzulässig.
  3. Ausnahmen:

    Wird eine solche Ausnahme angewandt, ist der Speicher durch memset() mit Nullen zu füllen.

  4. (Pascal, Delphi) Globale Datenobjekte sind bei ihrer Deklaration zu initialisieren. Lokale Datenobjekte werden initialisiert, bevor irgendeine andere Anweisung ausgeführt wird:
    1. 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};

  5. Standard-Makros sind bereits in diversen Standard-Header-Dateien definiert. Eigene Makros sollten, wann immer möglich, vermieden werden.
  6. Ö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.
  7. 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.
  8. Generell sind Konstanten (besonders Strings) möglichst in der Resource der Anwendung zu plazieren.
  9. Datenobjekte, insbesondere Zwischenergebnisse sind möglichst mit const zu deklarieren.
  10. Funktionslokale static-Objekte sind unzulässig. Sie sind außerhalb der Funktion als private globale Objekte zu definieren.
  11. Bitfields sind zu unzulässig. Ihre Verwendung ist nur bei hardwarenaher Programmierung oder wenn sie aus einer fremden Bibliothek stammen gestattet.

Funktionen

Parameter

  1. Eine Funktion muß durch Vorbedingungen sicherstellen, daß die Werte der Eingabeparameter gültig sind.
  2. Eine Funktion muß durch Nachbedingungen sicherstellen, daß der Rückgabewert (und, falls verwendet, der Wert jedes Ausgabeparameters) gültig ist.
  3. Funktionen sollten nur Eingabeparameter erhalten, die möglichst const zu deklarieren sind.
  4. Eingabeparameter, die von einem Klassen- oder struct-Typ sind, sollten immer als Referenzen (const &) übergeben werden.
  5. Funktionen mit Steuerparametern sind zu vermeiden. Die Konstruktion
    1. 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);

  6. Müssen dennoch Steuerparameter an eine Funktion übergeben werden, so ist hierfür ein enum zu deklarieren.
    1. 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);

  7. Die Deklarationen von Funktionen bzw. Elementfunktionen enthalten generell auch die Namen der Parameter. Die Namen sind bei Deklaration und Implementierung der Funktion gleich.
  8. (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

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

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:

Funktionen mit einem Rückgabewert

  1. Funktionen mit Rückgabewerten (außer Statusrückgaben) dürfen keine Seiteneffekte haben.
    1. 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!
  1. 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!
  2. Funktionen liefern skalare Werte oder Exemplare einer Klasse auf dem Stack zurück.
    1. 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));
      }

  3. 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.
  4. 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);
      }

  5. Die Anweisung return innerhalb von Funktionen (nicht am Ende) ist, außer zum Abbruch in Fehlerfällen, unzulässig.
  6. 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.
    1. 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

  1. 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.
  2. Eine Funktion, die vermeintlich mehrere Werte liefert, kann dies in Form eines Exemplars einer Klasse liefern, welches die Werte als Attribute besitzt.
  3. 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

  1. 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.
    1. derPuffer.LiesDaten();
      bErfolg = derPuffer.HatNeueDatenErhalten();

      Unzulässig:

      bErfolg = derPuffer.LiesDatenUndLiefereStatus();

  2. 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.
  3. 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.
  4. 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

  1. (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ß
    1. ein Default-Konstruktor zur Verfügung gestellt werden oder
    2. ein Kommentar in der Klassendefinition stehen in der Form
      1. // Kein Default-Konstruktor, weil…

    Vorsicht: Auch X::X(int i = 0) ist ein Defaultkonstruktor!

  2. (Delphi) Der Default-Konstruktor wird wie folgt deklariert
    1. constructor CKlasse.Create;

Copy-Konstruktor

  1. (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ß
    1. ein expliziter Copy-Konstruktor definiert sein oder
    2. ein private deklarierter Dummy-Copy-Konstruktor definiert sein oder
    3. ein Kommentar in der Klassendefinition stehen in der Form
      1. // Default Copy-Konstruktor ist korrekt, weil…

  2. (Delphi) Der Copy-Konstruktor wird wie folgt deklariert:
    1. 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

  1. (C++) In Klassen, die Zeiger- oder Referenzdatenelemente oder implizite Bezüge auf Systemressourcen (z.B. File- oder Window-Handles) enthalten, muß
    1. ein expliziter Destruktor definiert sein oder
    2. ein Kommentar in der Klassendefinition stehen in der Form
    3. // Default Destruktor ist korrekt, weil…

  2. (Delphi) Der Destruktor wird wie folgt deklariert:
    1. 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

  1. 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ß
    1. ein expliziter Zuweisungsoperator definiert sein oder
    2. ein private deklarierter Dummy-Zuweisungsoperator definiert sein oder
    3. ein Kommentar in der Klassendefinition stehen in der Form
      1. // Default Zuweisungsoperator ist korrekt, weil…

  2. Die Zuweisung a = a muß korrekt implementiert sein.
  3. Da Zuweisungen in Ausdrücken unzulässig sind, sollten Zuweisungsoperatoren als
    1. void operator = …

    deklariert werden.

Ausnahmen

  1. 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.
  2. Die von einer Funktion verursachten bzw. weitergegebenen Exceptions sind bei der Deklaration mit anzugeben.
  3. 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.
  4. 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.
  5. 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:
    1. Genau dann wenn eine Funktion keine Ausnahme ausgelöst hat, gelten ihre Nachbedingungen.
    2. 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.

  6. 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:
    1. 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

  1. Alle Programme sind so zu entwerfen, daß sie in einer Multitasking-Umgebung lauffähig sind.
  2. Der Benutzer eines Moduls ist nicht für die Absicherung kritischer Abschnitte verantwortlich. Klassen sind nach Möglichkeit als Monitore zu entwerfen.

Funktionen

  1. 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

  1. 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.
  2. Öffentliche Variablen sind unzulässig.
  3. Öffentliche nicht-Elementfunktionen sind zu vermeiden.
  4. friend-Konstrukte sind zu vermeiden.
  5. Zuweisungen an this sind nicht erlaubt (nicht zu verwechseln mit *this!).
  6. (C++) Die Benutzung von malloc() und free() ist nicht erlaubt, stattdessen werden new und delete genutzt.
  7. (C++) Zugriffsfunktionen, die nur eine Referenz auf ein Datenelement liefern, dürfen inline definiert werden.
  8. Der Zugriff auf Variablen anderer Objekte ist unzulässig (außer in Copy-Konstruktoren und Zuweisungsoperatoren).
  9. Zeiger- und Referenzparameter, die eine const-Semantik beinhalten, werden auch const definiert.
  10. Es sollte möglichst vermieden werden, komplexe Objekte innerhalb von Schleifen zu definieren, da dort jedesmal der Konstruktor aufgerufen wird.
  11. 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