Algorithmen & Datenstrukturen
Programmieren 1


von Prof. Jürgen Plate

9 Programmierstil, Fallenstricke

9.1 Programmierstil in C

Dies ist eine kleine Stilkunde, die das Schreiben klarer Programme fördern soll. Diese Hinweise und Erfahrungen sollten an Bedeutung gewinnen, so wie Ihre Erfahrung mit C wächst.

Allgemeines

Über das Schreiben von Ausdrücken

Wegen der großen Zahl von Operatoren, des nicht immer einleuchtenden Vorrangs und der Regeln der automatischen Typangleichung liegt es in der Verantwortung des Programmierers sicherzustellen, daß ein Ausdruck in der erwarteten Art und Weise arbeitet. Es folgen hier einige Anregungen, die es Ihnen erleichtern sollen, leicht lesbare, effiziente (und fehlerfreie) Programme zu schreiben.

Programmformat

Obwohl die äußere Erscheinung des Quelltextes keine Rolle spielt, hat das Programmformat erheblichen Einfluß auf seine Verständlichkeit. Das Layout von Quellcode ist nicht nur eine Frage des persönlichen Geschmacks oder des Programmierstils. Ein vereinheitlichtes Code-Layout in einem Softwareprojekt ist eine wichtige Maßnahme zur besseren Interaktion von Teammitgliedern.

Ein paar Regeln zur Schreibweise

Über das Schreiben leicht lesbarer Programme

Wir haben wenig Zeit darauf verwandt, Kriterien dafür zu finden, was eine gute Funktion ausmacht. Hier folgen einige Merkmale, die eine gut geschriebene Funktion besitzen sollte:

Schreiben von Makros

Makroersetzung ist die am häufigsten benutzte Fähigkeit des Preprozessors. Wir geben hier einige Richtlinien als Entscheidungshilfe, wann Makros angebracht sind, und einige stilistische Hinweise, die die Benutzung vereinfachen:

Zeiger und Arrays

Die Komplexität der Arrays, Zeiger, Arrays aus Zeigern, Zeigern auf Zeilen eines Arrays, Zeiger auf Spalten eines Arrays und Zeiger auf Zeiger können überwältigend sein. Wir wollen hier einige Anregungen geben, wie Sie den "Zeiger" in die richtige Richtung finden.

Benutzung selbstdefinierter Typen

Dynamische Datenstrukturen

Auswahl der geeignetsten Bibliotheksfunktion

Die Standard-E/A-Bibliothek enthält verschiedene nützliche Funktionen. Unglücklicherweise sind es so viele, daß es manchmal schwierig ist zu entscheiden, welche Funktion man benutzen soll.

9.2 Fallen in C

Feldgrenzen

In der Lernphase mit der Sprache C erlebt man immer wieder nicht nachvollziehbare AbstürzezurLaufzeit(memory fault - core dumped) bzw. das Verschwinden von Strings, obwohl nicht darauf zugegriffen wurde. Gibt es Hinweise, um solche Katastrophen zu vermeiden?
In der Sprache C muß der Programmierer das Überprüfen der Feldgrenzen selbst in die Hand nehmen und bei Strings auf das abschließende NULL-Byte achten. Der C-Programmierer muß selbst dafür sorgen, daß solche Kontrollen, falls notwendig, vorgenommen werden. Ein Feld char string[100]; belegt genau 100 Zeichen (Byte)-Speicherplätze. Der String, der darin abgelegt werden kann, darf also höchstens 99 Zeichen lang sein, da per Konvention als Ende des Strings ein NULL-Byte ('\0') eingefügt werden muß. Ist der String zu lang, wird über den nachfolgenden Speicherplatz hinaus geschrieben. Das hat zur Folge, daß andere Variablen, die dort stehen, überschrieben werden. Im schlimmsten Fall ist der Speicherplatz dann nicht mehr für den Prozeß definiert, was zu obiger Fehlermeldung (memory fault - core dumped) führt. Übrigens läuft der Index in C von 0 bis Dimension-1.

Dumme Fehler können auch entstehen, wenn im Speicher mehrere Strings hintereinander stehen und man mit der Länge nicht aufpaßt. Schreibt man nämlich die abschließende ' \ 0 , über den ersten String hinaus, steht sie unter Umständen als erstes Zeichen im zweiten String, sodaß dieser als Leerstring erscheint, obwohl er explizit nicht belegt wurde:

char s1[10], s2[12];

        | a | b | c | d | e | f | g | h | i | j |\0 | x | y | z |\0 |  |  |
         ^s1                                     ^s2

printf("%s\n",s1);     -->   "abcdefghij"
printf("%s\n",s2);     -->   ""
Natürlich hängt es sehr von der Speicherorganisation ab, wo die Variablen im einzelnen abgelegt sind (oft sind sie auf eine Wortgrenze ausgerichtet, besitzen also etwas Spielraum!). Gibt man einen solchen String an eine Funktion weiter, so ist innerhalb der Funktion nur die Startadresse bekannt (Feldname ist Anfangsadresse). Man kann dann nur mit einem weiteren Argument überprüfen, ob der Speicherbereich überschritten wird:
int getline(char buf[], int len);    /* Deklaration (Prototyp) von getline */

main ()
  {
  int ll;
  char string[80];

  ll = getline(string, sizeof string); /* Aufruf von getline */
  return 0;
  }

int getline(char s[], int lim);
  {
  int i = 0;
  for(i=0; i<lim-1; i++)
    s[i] = ....;
  s[i] = '\0';
  return i;
  }
Folgende Regeln lassen sich daraus ableiten: Werden diese Hinweise beachtet, können viele Speicherabstürze vermieden werden.

Falsch gesetzte Semikolons

Falsche Einrückung

Bei der if-Kontrollstruktur ist oft nicht klar, zu welchem if ein else gehört:
if (i > j)
  if (k < 100)
    tuwas();
else
  tuwasanderes();
Es hat hier den Anschein, als ob das else zum ersten if gehört - das ist falsch. Ein else gehört immer zum nächst höheren if, das noch kein else hat - also zum zweiten if! Korrektur durch eine Block-Klammer:
if (i > j)
  {
  if (k < 100)
    tuwas();
  }
else
  tuwasanderes();

Die switch-Falle

Eine weitere Kontrollstruktur kann Kummer machen: switch. Wird hier ein break vergessen, so arbeitet C sequentiell weiter, auch wenn andere Marken anstehen. Jede Auswahl sollte also normalerweise mit break abgeschlossen werden.
switch (c) 
  {
  case '1': tuwas(); break; 
  case '2': tudies(); break;
  case '3': tujenes(); break;
  default:  printf("error\n"); break;
  }
Vielleicht kann hier der Präprozessor helfen:
#define CASE break;case
#define DEFAULT break;default
Dann kann die Auswahl so aussehen:
switch (c) 
  {
  CASE '1': tuwas();  
  CASE '2': tudies();
  CASE '3': tujenes();
  DEFAULT:  printf("error\n");
  }

Float-Ausdrücke

Eine weitere Falle ist folgende Rechnung:
double d = 3/4; Hier wird zunächst 3 durch 4 geteilt - was für Integerwerte natürlich 0 ist. Dann wird dieses Ergebnis d zugewiesen als 0.000000 (über eine implizite Typumwandlung) und bleibt somit natürlich 0. Abhilfe schafft die Verwendung der richtigen Konstanten 3.0 und 4.0 als double (zumindest eine):
double d - 3.0/4.0;

Vergleichsoperator vs. Zuweisungsoperator

In Vergleichen vergißt man leicht, daß der Vergleichsoperator = = und der Zuweisungsoperator = unterschiedliche Funktionen haben. Eine (scheinbare) Bedingung while (i = 20) weist der Variablen i den Wert 20 zu, was logisch gesehen wahr (ungleich 0) ist, also wieder eine Endlosschleife ergibt. Natürlich kann dies (z. B. bei Strings) auch positiv genutzt werden:
char s1(100), s2[100];

/* s1 nach s2 kopieren (bis Stringende '\0') */
while (s2[i] = s1[i])
  i++;

Reihenfolge der Auswertung

Eingabe-Probleme

Desweiteren gibt es oft bei der Bibliotheksfunktion scanf Schwierigkeiten. Vergißt man doch allzuleicht das Adreß-Zeichen & bei den Argumenten. Weniger durchschaubar ist aber, daß scanf das abschließende \n im Tastaturpuffer läßt, so daß ein nachfolgender getchar()-Aufruf dies als erstes geliefert bekommt. Möchte man scanf und getchar mischen, empfiehlt es sich, den Tastaturpuffer zu löschen:
#include 

int i, c, ret; 
ret = scanf("%d",&i);         /* gepufferte Eingabe */
if(ret != 1) error();        /* Fehlerbehandlung */
while (getchar() != '\n';    /* Eingabepuffer leeren */
getchar();                    /* Einzelzeichen lesen */
Das Überprüfen des Return-Codes ist außerdem fast ein Muß, sofern man nicht sowieso die ganze Zeile einliest und untersucht:
fgets(buf, MAX, stdin);
sscanf(buf, .... );
Dann kann auch auf versehentliche Leerzeilen reagiert werden.

Preprozessor

Konstante und Variable

(Null-)Pointer

Arrays und Pointer

Dynamische Speicherverwaltung

Zum Inhaltsverzeichnis Zum nächsten Abschnitt


Copyright © FH München, FB 04, Prof. Jürgen Plate