Viele der Flüchtigkeitsfehler bei der Programmierung fallen erst viel später auf – meist nach Release des Produktes. Diese sind dann schwer zu debuggen und wären ohne viel Komfort-Verlust bei der Programmierung aufgefallen, wenn man die im Folgenden vorgestellten Tipps befolgt hätte uns so ohne Mühe eine bessere Codequalität abgeliefert hätte. Ich habe diese nach der „Bequemlichkeit“ der Umsetzung und nicht nach der Schwere der verursachten Probleme geordnet:

1. vorzeichenbehaftete und vorzeichenlose Datentypen nicht mischen

Zu fatalen, oft lange unbemerkten Fehlern können Vergleich und Zuweisung von Variablen mit unterschiedlichen Wertebereichen führen. Oft habe ich in diesem Zusammenhang sogar Casts von signed zu unsigned gesehen, die nur dazu dienten, den Compiler ruhig zu stellen – der Programmierer hat sich dabei aber überhaupt keine Gedanken über den Wertebereich gemacht und prompt in die Falle getappt. Aufgrund des Casts wurde der Fehler von mehreren anderen Programmierern nicht entdeckt.

Speziell bei Flags, Zählern und Rückgabewerten sollte man stickt auf eine konsistente Benutzung achten. Es hat sich ebenfalls bewehrt, die Sonderstellung von vorzeichenlosen Variablen und Konstanten auch im Namen zu reflektieren und auch bei der Zuweisung von Konstanten, diese als unsigned zu markieren:

int          myvalue   = 123;
unsigned int u_myvalue = 123u;

2. Rückgabewerte immer prüfen und behandeln

Die meisten Funktionen liefern Rückgabewerte. Nach meiner Erfahrung werden diese jedoch oft nicht hinreichend geprüft – und falls doch wird meist nur eine nichtssagende Fehlermeldung ausgegeben.
Man sollte jedoch immer versuchen diese abzufragen und auch zu behandeln.
Sicher, in manchen Fällen ist das nicht sinnvoll. In diesen speziellen Fällen sollte man das dann aber auch genau so kennzeichnen. Also:

/* mit Prüfung des Fehlerwertes und
 * Vorbelegung mit einem Fehlerwert
 */
int a = -1; 
a = wichtige_funktion("irgend ein Wert");

// bzw. wenn der Rückgabewert irrelevant ist explizit:
(void)wichtige_funktion("irgend ein Wert");

So es sich anbietet, sollte man sogar noch einen Schritt weiter gehen und als Rückgabewerte nur Aufzählungstypen zulassen und diese wie im Tipp 4 via switch prüfen. Um den (GNUC)-Compiler zu veranlassen, auf ignorierte Rückgabewerte explizit hinzuweisen, kann man dies auch noch durch ein Attribut fordern:

int wichtige_funktion(const char* argument) __attribute__ ((warn_unused_result));

 3. konstante Daten auch immer als solche kennzeichnen

Ein String wie zum Beispiel „das ist ein Test“ hat den Typ const char *. Speichert man diesen String in einer Variablen, sollte man diese auch als konstant definieren, um versehentlichen überschreiben des konstanten Inhalts zu verhindern und dem Compiler die Möglichkeit zu geben, zu prüfen ob dies in der jeweiligen Funktion versucht wird:

const char * c_mystring = "konstanter Inhalt";

Auch hier gilt wiederum für den üblicherweise vergeßlichen Programmierer: eine Kennzeichnung im Namen der „Variablen“ kann nicht schaden. Als const sollte man übrigens nach Möglichkeit auch die Rückgabewerte und Parameter von Funktionen deklarieren.

4. Aufzählungstypen in einer Switch-Anweisung als Selektion verwenden

Aufzählungstypen (Enums) bringen Übersicht – nicht nur für den Programmierer, sondern auch für den Compiler. Es ist wichtig alle Elemente des Aufzählungstyps in der Switch-Anweisung zu behandeln. Im Zweifelsfall solten nicht genutzte Werte mit default: abfangen werden und zu einer Fehlerbehandlung führen. Besser ist es jedoch, wenn immer alle Werte in der Switch-Anweisung behandeln werden und nie default: verwendet wird, damit man bei späteren Erweiterungen des verwendeten Aufzählungstyps überall dort Warnungen bekommt, wo der neue Wert noch nicht behandelt wird. Ein Fehlverhalten aufgrund eines nicht behandelten Wertes ist meist schwer zu entdecken.

5. Warnungen des Compilers sichten und nicht (global) unterdrücken

Die Warnungen des Compilers bieten eine sehr wertvolle Hilfe um die üblichen Flüchtigkeitsfehler zu entdecken. Arbeitet man viel mit fremdem Code kann es schon mal vorkommen, dass man mit der Zeit gegenüber den Warnungen des Compilers abstumpft. Ich bilde hier keine Ausnahme. Dennoch möche ich hiermit noch einmal ausdrücklich dafür werben, sich diese Meldungen gründlich und gewissenhaft anszusehen und Projekte sogar mit der Compiler-Option -Werror zu übersetzen. Der anfängliche „Bequemlichkeitsverlust wird durch die bei der Fehlersuche eingesparte Zeit mein um ein vielfaches wieder gut gemacht. Nach Möglichkeit sollte man bei diesem Review auch gleich die Ursachen für die Warnungen beseitigen.

Aber wie immer gilt:

Ausnahmen bestätigen die Regel

Einige zu Warnungen führende Konstrukte können durchaus beabsichtigt sein. Gerade hier sollte man sich nicht dazu hinreißen lassen, diese Warnung per Compiler-Option zu deaktivieren. Eleganter und vor allem sicherer ist es bestimmte Warnungen für einzelne Quelltextzeilen zu deaktivieren. Der GCC bietet dazu die GCC-DIAG-Pragma:

#include <gcc_diag.h>
...
int variable            = 2;
unsigned int u_variable = 1;
...
GCC_DIAG_OFF(sign-compare);
if (u_variable < variable) {
GCC_DIAG_ON(sign-compare);
...
}

Beispiel: Warnung über ungenutze Funktionsparameter

Der Hinweis des Compiliers über ungenutzte Funktionsparameter ist gut gemeint und an der richtigen Stelle ein wertvoller Hinweis zur Fehlersuche. Aber was macht man, wenn man einen Parameter zur Zeit tatsächlich (noch) nicht verwenden möchte, aber die API diesen schon definieren soll? Oft sieht man dabei krude Lösungsversuche wie z.B.:

void function(int arg)
{
//...

// done to silence the compiler
   arg = arg;
}

Was natürlich wiederum zu Ärger mit Code-Analyse-Tools wie z.B. Klocwork führen kann und absolut schlechter Programmierstil ist. Besser ist es, dem Compiler mitzuteilen, dass man sich sehr wohl des ungenutzten Parameters bewusst ist:

void function(__attribute__((unused))int arg)
{
//...

}

6. typgerechtes Programmieren

Cast-Operationen sollte man tunlichst vermeiden und durch Unions ersetzen, dies klappt in den meisten Fällen und gibt dem Compiler die Chance Zuweisungen zu prüfen. Oft höre ich in diesem Zusammenhang, dass ein Cast aus Effizienzgründen verwendet wird und eine union zusätzlichen Code erzeugen würden. Dies stimmt so gerade bei den modernen Compilern nicht mehr. Im Gegenteil kann der Compiler den gewünschten Code besser verstehen und optimieren. Ist z.B. im Kernel-Umfeld eine union keine Option, hilft das Makro container_of() weiter.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Name *

fünf × vier =