Programmierstil

  • Lesbarkeit
  • Variablennamen
  • eigene Datentypen verwenden
  • Kommentare
  • Klammern weglassen

Bei den kleinen Programmen, die wir bisher geschrieben haben, spielt es keine Rolle wie man sie schreibt - man wirft einen Blick drauf und hat das Programm soweit überblickt. Die bisher geschriebenen Programme hatten vielleicht 20 Zeilen Quelltext. Wie sieht es beispielsweise mit Windows aus? Hier müssen 4 Millionen Zeilen Code überblickt werden - für Windows 3. WindowsXP wird auf 40 Millionen Zeilen geschätzt.

Wenn Projekte größer werten, so wird man irgendwann ganz automatisch den Überblick verlieren. Daher ist es wichtig, sich möglichst einfache Regeln aufzustellen und diese strikt einzuhalten. Programmieren hat auch etwas mit Disziplin zu tun: Je disziplinierter man programmiert, desto einfacher wird es.

Lesbarkeit

An Anfang steht immer die Lesbarkeit der Datei, an der man arbeitet. Die absolute Grundregel ist den Quelltext zwischen den Klammern einzurücken:

int main( void )
{
  int a = 0;
 
  if( a == 1 )
  {
    zweimalEingerueckteFunktion();
  }
 
  return 0;
}

Früher verwendete man Tabs, heutzutage rate ich dringend dazu Leerzeichen zu verwenden. Der Quelltext oben ist mit 2 Leerzeichen eingerückt. Früher verwendete man den Tabs entsprechend häufig 8 Zeichen, was den Quelltext allerdings stark auseinanderzieht:

int main( void )
{
        int a = 0;
 
        if( a == 1 )
        {
                zweimalEingerueckteFunktion();
        }
 
        return 0;
}

4 Zeichen sind derzeit üblich:

int main( void )
{
    int a = 0;
 
    if( a == 1 )
    {
        zweimalEingerueckteFunktion();
    }
 
    return 0;
}

Ich verwende 2 Leerzeichen, man sollte sich mit allen Mitprogrammierern hier einigen.

Variablennamen

Wir haben in diesem Tutorial deutlich Datentypen und Werte unterschieden. Ein Variablenname benennt einen Wert, aber keinen Datentyp. Das bedeutet, dass der Variablenname ausdrücken sollte worum es geht. Mit der sogenannten ungarischen Notation schreibt man den Datentyp vor den eigentlichen Variablennamen, z.B. ein i für Integervariablen, ein u für vorzeichenlose Integers (unsigned int) oder ein c für ein Char. Einem String wird ein s oder ein str vorgesetzt, einem Pointer ein p. Ein Character-Pointer ist also pcVariablenname. Oder ein sVariablenname. Oder ein strVariablenname. Die Idee der ungarischen Notation stammt aus einer Zeit, als man zusammengehörende Daten noch nicht zusammengehörend verwaltet hat. Je mehr eigene Datentypen man verwendet, desto unschärfer wird die Idee, bzw. desto länger werden die Variablennamen. Für eine Struktur „Person“ kann man nicht mehr p voransetzen, denn p bedeutet ja bereits Pointer. Wieviele Buchstaben benötigt man, um eindeutig zu sein? Will man konsequent belieben, so muss man den Typ ausschreiben:

struct Person personMutter, personVater, personKind

Viel Text, aber nur wenig Information. Ich rate davon heutzutage ab. Es gibt nur wenige Ausnahmen, die man als Programmierer kennen muss und auch durchaus so weiterverwenden sollte, denn diese Konvention gibt es gewissermaßen seit den Gründertagen der Programmierung, jeder benutzt sie und jeder versteht sie: Innerhalb eines kurzen, überschaubaren Abschnittes kann man Variablennamen verwenden, die nur aus einem Zeichen bestehen. Gerade in Schleifen hat man häufig eine Integervariable, die einen Index beschreibt. Diese sollte i heißen. Verschachtelt man Schleifen oder braucht aus anderen Gründen weitere Integervariablen, so zählt man einfach weiter: j, k, l, … Diese Buchstaben werden von Entwicklern intuitiv als Integers verstanden. Alle vorherigen (a-h) und späteren Buchstaben (so etwa ab o) werden intuitiv als Fließkommazahlen (float, double) verstanden.

Koordinaten heißen entsprechend wie in der Mathematik gerne x, y und z, Achsen werden gerne mit u und v betitelt. Bei diesen Variablen (ab u) geht man von Fließkommazahlen aus, aber benutzt sie gerne auch als (Integer-)Indizes in mehrdimensionalen Arrays. Diese Notation stammt noch aus grauer Vorzeit. Dazu eine kleine Anekdote aus dieser Zeit: In C müssen Variablen deklariert werden, die Programmiersprache Fortran nimmt eine implizite Deklaration bei unbekannten Variablennamen nach genau diesem Muster vor. In Fortran werden Fließkommazahlen „real“ statt float genannt. Der Datentyp einer Variablen stand also bereits durch den Anfangsbuchstaben des Variablennamen fest, sofern der Programmierer nicht eine explizite Deklaration durchführte und damit den Typ selbst bestimmte. Damit entstand der Spruch

god is real until declared integer.

Den Variablennamen „god“ beginnt mit g und ist daher ein real (Fließkommazahl), sofern der Entwickler den Namen nicht für etwas anderes verwendet.

Die einzigen Reste der ungarischen Notation, die man heute noch beachten sollte sind eben diese einzelnen Buchstaben als Variablenname, sowie str für Strings und p für Pointer. Gelegentlich sieht man noch m oder Unterstriche für „Member“ in Strukturen. Aber entweder grundsätzlich m oder grundsätzlich _, das sollte man auf keinen Fall vermischen. Selten sieht man ein 'a' bei den Funktionsparametern, hier im Beispiel 'aPerson'. a steht für nicht für „ein“, sondern für argument. Das gibt einem die Möglichkeit innerhalb einer Funktion zwischen Variablen, die als Argument an die Funktion übergeben wurden und lokalen Variablen zu unterscheiden. Faktisch besteht zwischen diesen Variablen in C allerdings kein Unterschied. In C muss vor einer Membervariablen (Mitglied einer Struktur) eben der Name der Variablen stehen, das Präfix nutzt in C also nichts. Im Hinblick auf C++ ist ein Präfix allerdings eine Überlegung wert, denn hier gibt es die Möglichkeit auf Member zuzugreifen, ohne dass der Variablenname vorangestellt ist, so dass es zu Verwechslungen kommen könnte.

struct Person
{
  char _firstName[64];
  char _familyName[64];
};
 
void printPerson( struct Person * aPerson )
{
  printf( "Person: %s %s\n", aPerson->_firstName, aPerson->_familyName );
}

Bei den Argumenten rate ich von einem Präfix ab, bei den Membern einer Struktur rate ich vom 'm' ab und bin kein Freund des Unterstrichs, kann allerdings diejenigen verstehen, die ihn bevorzugen. Privat nutze ich (Xin) ihn nicht, im Job ist er jedoch Standard. Wofür Du Dich auch entscheidest, wenn dann mach es konsequent. Ansonsten rate ich von der ungarischen Notation zugunsten des folgenden Punktes ab:

Eigene Datentypen verwenden

Heutzutage sollte man jedem eigenständigen Datensatz auch einen eigenen Typ geben. Das kann man im Extremfall auch komplett durchziehen, in dem man sich weigert die Standard Datentypen an eigene Funktionen zu übergeben, sondern für alles, was übergeben wird, auch einen eigenen Datentyp definiert:

struct Firstname
{
  char value[64];
};
 
struct Familyname
{
  char value[64];
};
 
struct Street
{
  char value[64];
};
 
struct Town
{
  char value[64];
};
 
struct HouseNumber
{
  unsigned int value;
};
 
struct ZipCode
{
  unsigned int value;
};
 
struct Person
{
  struct Firstname   firstName;
  struct FamilyName  familyName;
  struct Street      street;
  struct HouseNumber houseNumber;
  struct ZipCode     zipCode;
  struct Town        town;
};
 
struct Person findPerson( struct FamilyName * searchPattern )
{
  ...
};

Die Funktion findPerson verlangt einen Familiennamen. Solange man mit Datentypen arbeitet, wird der Compiler meckern, wenn man eine Stadt übergibt. Je mehr Mühe man sich macht, die Semantik (die Bedeutung) eines Wertes durch die Definition eines eigenen Datentyps zu erklären, desto schwieriger wird es Funktionen mit Daten zu versorgen, die eine andere Bedeutung haben. findPerson( struct FamilyName ) wird sich intern an das char Array[64] innerhalb von struct FamilyName wenden, aber statt das char-Array wird mit dem Datentyp auch die Bedeutung (die Semantik) des char-Arrays festgelegt. Ein char-Array mit einer anderen Semantik (z.B. struct Town) kann nicht an die Funktion übergeben werden - auch nicht versehentlich, denn der Compiler würde das ablehnen. Früher half man sich mit der ungarischen Notation, in dem man jeder Variablen den Datentyp voranstellte, um solche Fehler zu vermeiden. Die Funktion findPerson bekam als Argument ein char * übergeben und der Programmierer musste wissen, dass er der Funktion nur eine Variable strFamNameVariablenName übergeben durfte und kein strTownVariablenName.

Macht man sich die Mühe, dem Computer die Semantik der Daten zu benennen, wird die ungarische Notation überflüssig, der Compiler weigert sich einfach den fehlerhaften Quelltext zu kompilieren. Wer weniger Fehler macht, ist ein besserer Programmierer und meiner Erfahrung nach ist ein Datentyp schneller definiert, als die Fehler behoben, die man ansonsten erstmal finden muss.

Kommentare

(Hauptartikel : Kommentare)

In C leitet man einen Kommentar mit /* ein und beendet ihn mit */:

#include <stdio.h>
#include <stdlib.h>
 
/* main: Einstiegspunkt des Programms */
int main( void )
{
  /* Hier wird etwas ausgegeben */
  printf( "Hello proggen.org\n" );
 
  /* Hier endet das Programm */
  return EXIT_SUCCESS;
}

Das ist ein Beispiel für Kommentare. Kommentieren sollte man im Idealfall die angelegten Datentypen, eventuell kurz die Bedeutung der Funktionen. Aufwendige Algorithmen oder längere Passagen sollte man abschnittsweise kommentieren und beschreiben, was in diesem Bereich des Codes so passiert. Bei größeren Funktionen ist es oftmals auch eine Hilfe, wenn die lokalen Variablen ebenfalls kurz beschrieben sind - dazu gehören auch die Funktionsparameter:

/* myStrlen: zählt die Buchstaben eines Strings
 
   Parameter string: String, dessen Länge gezählt wird, darf NULL sein
*/
unsigned int myStrlen( char const * string )
{
  if( string != NULL )
  {
    /* Zeiger ist gültig, also zählen */
 
    unsigend int i = 0;
 
    while( string[i] != '\0' )
    {
      i = i + 1;
    }
 
    /* Länge des Strings zurückgeben */
    return i; 
  }
 
  /* Zeiger ist ungültig => Länge 0 */
  return 0;
}

Bitte nicht übertreiben:

/* myStrlen: zählt die Buchstaben eines Strings
 
   Parameter string: String, dessen Länge gezählt wird, darf NULL sein
*/
unsigned int myStrlen( char const * string )
{
  /* Zuerst prüfen ob der String ein Nullzeiger ist */
 
  if( string != NULL )
  {
    /* Zeiger ist gültig, also zählen */
 
    unsigend int i = 0;          /* Index auf Buchstaben im String */
 
    while( string[i] != '\0' )   /* Zeichen im String an Position i mit Nullbyte vergleichen */
    {
      i = i + 1;                 /* i um eins erhöhen */
    }
 
    /* Länge des Strings zurückgeben */
    return i; 
  }
 
  /* Zeiger ist ungültig => Länge 0 */
  return 0;
}

Quelltext der nur aus Kommentaren besteht, besteht höchstwahrscheinlich aus sehr vielen alten Kommentaren, die nach Änderungen im Quelltext nicht mehr angepasst wurden. Wird man von Kommentaren erschlagen, geht die Optik des Quelltextes verloren.

Wenn Code und Kommentar nicht übereinstimmen, können auch beide falsch sein

Auch der Aufbau des Quelltextes - zum Beispiel durch die korrekte Einrückung des Quellcodes - ist ein Teil der Dokumentation und Kommentierung des Codes, denn er spiegelt die Struktur des Algorithmus wider.

Wir haben oben gesehen, dass jeder Entwickler wissen sollte, dass i eine Index-Variable ist, wenn es nur eine Schleife gibt. Wenn man viele lokale Variablen hat, so lohnt es sich, diese zu beschreiben. Trivialitäten sollte man allerdings nicht kommentieren, also bitte niemals etwas kommentieren, was 1:1 im Quelltext steht - ein Kommentar sollte in einem langen Text entweder eine Orientierungshilfe („Zeiger ist ungültig, also zählen“) sein, wo man sich im Quelltext gerade befindet. Er sollte Abschnitte in wenigen Worten grob zusammenfassen oder bemerkenswerte und eventuell komplizierte Details erläutern, zum Beispiel die Definition, dass ein Nullpointer 0 Bytes lang ist, obwohl überhaupt kein zählbarer String übergeben wurde („Zeiger ist ungültig ⇒ Länge 0“). In jedem Fall sollte ein Kommentar ein Mehrwert zum Quelltext darstellen, ein Kommentar hinter i = i + 1 mit i um eins erhöhen ist kein Mehrwert, er lenkt nur von wichtigeren Kommentaren ab.

Variablennamen als Kommentar benutzen

Der Kommentar „Länge des Strings zurückgeben“ wird in dem Moment überflüssig, indem man die Variable i dem Problem anpasst. i ist natürlich ein Index, doch hier geht es darum die Länge zu ermitteln. Nennen wir die Variable length, so ist bei einer Funktion, die die Länge eines Strings ermitteln soll, die Zeile return length; genauso wenig kommentierungsbedürftig, wie zu erläutern, wofür die Variable length definiert wird.

/* myStrlen: zählt die Buchstaben eines Strings
 
   Parameter string: String, dessen Länge gezählt wird, darf NULL sein 
*/
unsigned int myStrlen( char const * string )
{
  /* Zuerst prüfen ob der String ein Nullzeiger ist */
 
  if( string != NULL )
  {
    /* Zeiger ist gültig, also zählen */
 
    unsigend int length = 0;
 
    while( string[length] != '\0' )
    {
      length = length + 1;
    }
 
    return length; 
  }
 
  /* Zeiger ist ungültig => Länge 0 */
  return 0;
}

Variablennamen richtig zu wählen ist genauso wichtig wie Funktionsnamen zu wählen. Lass dir ruhig ein wenig Zeit, um die richtige Wahl zu treffen. Besonders, wenn Du mehrere Variablen gleichen Typs verwendest, zähle sie bitte nicht durch wie Element1, Element2, Element3 oder a, b, c, … sondern gib ihnen erklärende Namen, zum Beispiel: previous, current, succeeding. Ein Algorithmus, der durch den Variablennamen beschreibt, was er tut, ist viel leichter lesbar, als ein Algorithmus, der ausschließlich mit Kürzeln arbeitet.

Klammern weglassen

Bei Bedingungen und Schleifen muss man nicht zwangsläufig geschweifte Klammern verwenden, wenn man nur eine Anweisung hat. Wir können unsere Funktion myStrlen also weiter kürzen:

/* myStrlen: zählt die Buchstaben eines Strings
 
   Parameter string: String, dessen Länge gezählt wird, darf NULL sein 
*/
unsigned int myStrlen( char const * string )
{
  /* Zuerst prüfen ob der String ein Nullzeiger ist */
 
  if( string != NULL )
  {
    /* Zeiger ist gültig, also zählen */
 
    unsigend int length = 0;
 
    while( string[length] != '\0' )
      length = length + 1;
 
    return length; 
  }
 
  /* Zeiger ist ungültig => Länge 0 */
  return 0;
}

Grundsätzlich kann man sogar noch weiter kürzen, in dem man die Funktion weiter zusammenschiebt:

/* myStrlen: zählt die Buchstaben eines Strings
 
   Parameter string: String, dessen Länge gezählt wird, darf NULL sein 
*/
unsigned int myStrlen( char const * string )
{
  unsigend int length = 0;
 
  if( string != NULL )               /* falls Zeiger gültig: zählen */
    while( string[length] != '\0' ) 
      length = length + 1;
 
  /* falls Zeiger ist ungültig => Länge 0 */
  return length; 
}

Die while-Schleife ist eine Anweisung, der eine andere Anweisung untergeordnet ist. Damit ist eine solche Konstruktion möglich. Das Entfernen von Klammern hat keine Auswirkung auf die Geschwindigkeit des Programms. Es gibt einige wenige Programmierer, die auf die Klammerung auch nie verzichten, um Mehrdeutigkeiten zu verhindern:

if( a == 4 )
  if( b == 5 )
    printf( "a ist 4 und b ist 5\n" );
else 
  printf( "a ist nicht 4\n" );

Dem Entwickler ist hier ein Fehler passiert. C orientiert sich nicht an der Einrückungstiefe, sondern nur an Klammern. Hier sind keine Klammern, also ordnet C das else dem letzten if() zu. In Wirklichkeit steht da also folgendes:

if( a == 4 )
  if( b == 5 )
    printf( "a ist 4 und b ist 5\n" );
  else 
    printf( "a ist nicht 4\n" );

Der Text „a ist nicht 4“ wird also immer dann ausgegeben, wenn a gleich 4 ist und b ungleich 5. Um das korrekt zu formulieren, muss C erklärt werden, dass der else-Teil nicht zum inneren if gehört:

if( a == 4 )
{
  if( b == 5 )
    printf( "a ist 4 und b ist 5\n" );
}
else 
  printf( "a ist nicht 4\n" );

So passts.

Ich persönlich klammere einzelne Zeilen nicht. Der hier beschriebene Fehler ist bekannt und jeder muss ihn kennen, denn solch ein Fehler kann auch einem anderen Autor passieren, dennoch muss ein anderer Entwickler den Code debuggen können. Auch ich mache ihn im Schnitt etwa einmal alle zwei Jahre. Für mich ist es daher interessanter weniger Text lesen zu müssen, um den Quelltext schneller zu verstehen.

Wichtig ist, dass man Klammern bei Funktionen oder der Definition von Strukturen nicht weglassen kann - das gilt nur für if, else, while, do und for.

Ziel dieser Lektion

Wichtigster Punkt dieser Lektion ist, dass Du Dir Gedanken darüber machst, dass es nicht nur zum Programmieren gehört Anweisungen in die richtige Reihenfolge zu bringen, sondern eben auch, sie optisch so aufzubereiten, dass man sie nach einem halben Jahr oder noch längerer Zeit wieder entziffern und gegebenenfalls debuggen kann. Hierfür hast Du nun einige grundlegende Regeln an die Hand bekommen.

Wir beschäftigen uns in der kommenden Lektion optimierten Operatoren, wir verkürzen damit regelmäßig auftretende Ausdrücke.