Start > Algorithmik > Entwicklungshinweise

Entwicklungshinweise

  1. Entwicklungsmuster
    1. Modellbildung
    2. Teilproblembildung
    3. Defensiver Programmierstil
    4. Klarer Programmierstil
    5. Dokumentation
    6. Sprechende Notation
    7. Sichtbarmachung von Abhängigkeiten
    8. KISS-Prinzip
    9. Modularität
    10. Kapselung
  2. Beispiel einer Programmentwicklung nach dem Top-Down-Ansatz
    1. Rahmenprogramm
    2. Festlegung von Teilaufgaben durch Kommentarüberschriften
    3. Festlegung der Variablen
    4. Kodierung der Funktionsaufrufe sowie Definition abstrakter Funktionen
    5. Konkretisierung der Funktionsdefinitionen
  3. Fehlersuche
    1. Syntaktische Fehler
    2. Semantische Fehler
      1. Indexfehler
      2. Initialisierungsfehler
      3. Verwechselung von Operatoren
      4. Falsch begrenzte Anweisungsblöcke
      5. Falsche Semicola
      6. Fehlerhafte Zugriffe infolge von Seiteneffekten
      7. Zeigerfehler
  4. Austestung

Entwicklungsmuster

Modellbildung

Es ist immer eine gute Idee, sich vor dem Beginn des Programmierens den grundsätzlichen Programmablauf zuerst auf Papier notieren, z.B. als Flußdiagramm, Programm­ablauf­plan, Strukto­gramm, Pseudo-Code o.ä. Steht das Programm nicht schon als Grobstruktur im Kopf, bringt es das Klimpern auf der Tastatur auch nicht! Tastaturen nehmen einem das Denken (noch) nicht ab.

Teilproblembildung

Die Aufteilung des Codes in Funktionen sollte in natürlicher Weise die zu lösenden Teilprobleme wiederspiegeln. Nachfolgend ein Beispiel eines Pseudocodes mit Darstellung der Teilprobleme anhand eines Programmes, das Meßwerte vom Benutzer einlesen, daraus dann statistische Kennziffern gewinnen und diese am Schluß dem Benutzer wieder ausgeben soll:

Hauptprogramm:

1) Lese_Meßwerte_vom_Benutzer_ein
2) Berechne_Statistik_aus_Meßwerten
3) Drucke_Statistik_auf_Bildschirm
4) Programm_Ende

1) Lese_Meßwerte_vom_Benutzer_ein:

Tue solange nicht ENTER gedrückt
  Lies_Meßwert_in_Reihe_ein

2) Berechne_Statistik_aus_Meßwerten:

Berechne_Durchschnitt
Berechne_Varianz
Berechne_Standardabweichung

3) Drucke_Statistik_auf_Bildschirm:

Drucke_Durchschnitt
Drucke_Varianz
Drucke_Standardabweichung

Defensiver Programmierstil

Keine Ausnutzung system- oder compilerspezifischer Gegebenheiten

int i = MAXINT;  /* int ist nur auf 16bit-Maschinen so groß wie short */
short s;
s = i;  /* Overflow auf 32bit-Maschine! */

Vermeidung von Seiteneffekten

Gerade Anfänger neigen dazu, alle Variablen global zu deklarieren, da sie mit der Parameter­übergabe entweder nicht vertraut sind, oder weil sie denken, es sei besser, alle Variablen stets im Zugriff zu haben. Genau das macht aber ein Programm so fehleranfällig sowie schwer erweiterbar.

Im folgenden Beispiel wird sogar eine Laufvariable i global vorgehalten! Dadurch kann die Variable unbemerkt verändert werden, das Programm wird fehlerhaft:

int i;  /* Globale Variable i */

..

int do_sth(int a[], int len)
  {
  int k, l;
  /* Hier hat der Programmierer die Deklaration einer lokalen Variablen i vergessen.
  Der Compiler meldet aber keinen Fehler, da (dummerweise) eine globale Variable i
  existiert, auf die jetzt jetzt stattdessen zugegriffen wird. */
  ..
  for (i=0; i<10; i++)  /* Zugriff auf globales i */
    ..
  }

void do_sth_other(int s[], int q)
  {
  int m, n;  /* Auch hier lokale Variable i vergessen! */
  ..
  for (i=0; i<q; i++)  /* Zugriff auf globales i */
    s[i] = 32;
  n = do_sth(s,n);  /* i durch do_sth verändert! */
  s[i] = 0;  /* Semantischer Fehler!!! */
  ..
  }

Portabilität

Wünscht man seinem Programm Portabilität, sollte man die Abstützung auf compiler- oder plattform­spezische Befehle auf ein Minimum reduzieren und besonders kennzeichnen. Die Funktionen fnsplit() und itoa() z.B. stehen auf UNIX-Plattformen i.a. nicht zur Verfügung!

Ein guter Stil ist die compiler- bzw. plattform­abhängige Kompilation:

/* OS dependant symbols */
#ifdef __unix__
  #define JOKER "*"
  #define PATHSEP "/"
  #define CMDLNSEP "-"
  #endif
#ifdef __MS__
  #define JOKER "*.*"
  #define PATHSEP "\\"
  #define CMDLNSEP "/"
  #endif

ANSI-Kompatibilität

Hier gilt dasselbe wie das bei der Porta­bilität gesagte. Bedient man sich nur der Funktionen, die im ANSI-Standard erfaßt sind, ist man sehr portabel. Allerdings bieten die meisten Compiler auch Funktionen an, die zwar nicht ANSI-kompatibel, aber sehr weit verbreitet sind:

char *strdup(const char *s)

Kontrolle der Post-Operatoren in kombinierten Ausdrücken

Durch das ANSI-Gremium ist der Zeitpunkt des Inkraftretens der Modifizierung der Variablen durch die Postoperatoren innerhalb eines Ausdruckes nicht festgelegt. Im nächsten Beispiel wird nach der Auswertung des rechten Zuweisungstermes i erhöht; ob dies aber noch vor der Zuweisung an den linken Terms passiert, ist nicht standardisiert! Alles, was fest steht, ist, daß i vor der nächsten Zeile bereits inkrementiert ist.

a[i] = i++;  /* Compilerabhängige Auswertung! */

Klarheit

Lieber zwei Zeilen mehr schreiben („umständlicher“), dafür aber einen verständlichen, sauberen Quelltext.

„Clarity on top!“

Aus

for (d=7,s4=0,dmax=sqrt(num); d<=dmax; d+=(s4=!s4)?2:4)
  if (!(num%d))
    return 0;

wird dann

d = 7; s = 4; dmax = sqrt(num);
while (d <= dmax) {
  if (num%d == 0)
    return 0;
  d += s;
  s = 6-s;

Folgender Ausdruck konvertiert zwei sedezimale Zeichen eines Strings s in den entsprechenden Character Code nach c. Zwar arbeitet er recht flott, nur weiß der Programmierer nach einigen Tagen schon selbst nicht mehr, was er damit gemeint hat.

c = (char)((s[0]>='A'?(s[0]&0xdf)-'A'+10:s[0]-'0')*16+(s[1]>='A'?(s[1]&0xdf)-'A'+10:s[1]-'0'))

Unter manchen Compilern minimal langsamer, aber dafür lesbarer ist:

c  = (char)(s[0]>='A' ? (s[0]&0xdf)-'A'+10 : s[0]-'0');
c *= (char)16;
c += (char)(s[1]>='A' ? (s[1]&0xdf)-'A'+10 : s[1]-'0');

Und manch einer würde vielleicht zur Erreichung desselben Zieles nur folgendes schreiben :-)

sscanf(s,"%x",&c);

Dokumentation

Der typische Entwicklungs­ablauf schaut so aus, daß unter Zeitdruck ein Programm fertiggestellt werden muß und keine Zeit für die Kommentierung bleibt. Das rächt sich spätestens beim anstehendem Update, da keiner mehr weiß, was die Programmzeilen eigentlich bedeuten. Und ein Update kommt so gut wie immer … Eine bequeme Möglichkeit einer netzbasierten Dokumentation gibt es mit Javadoc oder Doxygen.

Sprechende Notation

Es ist guter Stil, „sprechende“, systematische Variablen- und Funktions­namen vergeben und sich mit der Zeit ein eigenes Benennungs­schema zulegen (z.B. Konstanten groß, Variablen klein, Hilfs- und Schleifen­variablen mit h, i, j etc.).

Sichtbarmachung von Abhängigkeiten

Sehr hilfreich ist die Einrückung für Anweisungen, die von Kontroll­strukturen abhängen, um den Kontrollfluß besser zu erkennen. So wird aus:

if (sck == INVALID_SOCKET)
ShowStatus(hDlg, 0, TEXT("cs: creation failed! (%s)"), wsaMsg(WSAGetLastError()));
else {
for (is=0; is<MAX_CONNECTIONS; is++)
if (cs[is] == INVALID_SOCKET) {
cs[is] = sck;
ShowStatus(hDlg, 1, TEXT("Ok"),is,TF(inet_ntoa(csa.sin_addr)),ntohs(csa.sin_port));
WSAAsyncSelect(sck, hDlg, WM_CONNECT, FD_READ|FD_CLOSE);
return TRUE;}
ShowStatus(hDlg, 0, TEXT("Connection refused: no more slots available!"));
closesocket(sck);
ShowStatus(hDlg, 1, TEXT("cs[%d]: closed by server."), is);
return TRUE;
}

dann

if (sck == INVALID_SOCKET)
  // Kein Slot mehr frei
  ShowStatus(hDlg, 0, TEXT("cs: creation failed! (%s)"), wsaMsg(WSAGetLastError()));
else {
  // Slot noch verfügbar, also los
  for (is=0; is<MAX_CONNECTIONS; is++)
    if (cs[is] == INVALID_SOCKET) {  // Slot noch frei
      cs[is] = sck;  // Socket handle speichern
      ShowStatus(hDlg, 1, TEXT("Ok"), is, TF(inet_ntoa(csa.sin_addr)), ntohs(csa.sin_port));
      // Der via accept() ermittelte Socket hat dieselben Attribute wie der Listener-Socket.
      // Das schließt zu meldende Nachrichten ein. Sie muessen nachf. geaendert werden,
      // weil ein Verbindungssocket ja keine weiteren FD_ACCEPT-Notifications erhalten soll,
      // dafuer aber auf READ/CLOSE reagieren soll.
      WSAAsyncSelect(sck, hDlg, WM_CONNECT, FD_READ|FD_CLOSE);
      return TRUE;
      }
  ShowStatus(hDlg, 0, TEXT("Connection refused: no more slots available!"));
  closesocket(sck);
  ShowStatus(hDlg, 1, TEXT("cs[%d]: closed by server."), is);
  return TRUE;
  }

KISS-Prinzip

Das Programm sollte am Anfang so einfach wie möglich gehalten werden. Nicht alle Extras berücksichtigen, nicht alles zu 100% erledigen. Das heißt auch: keine Opti­mierungen während des Programm­entwurfs! Optimieren kann man später immer noch, aber zum Laufen muß man es erst mal bringen.

Keep it simple and stupid!

Modularisierung

Ein großes Problem in Teilprobleme (Module) zerlegen. Programmaufbau aus einfachen und gut dokumentierten Modulen („modularer Aufbau“), so evtl. Modifikationen später einfacher.

Routinen sollten nicht länger als eine Bildschirm­seite sein.

Kapselung der Module

Module soweit wie möglich geschlossen halten, sprich Variablen möglichst lokal halten, um Seiteneffekte zu vermeiden. Globale Variablen nur verwenden, wenn viele Routinen diese Variablen benötigen und eine Übergabe als Parameter ausufern würde (sog. „lockere Kopplung“) oder eine zentrale Regelung dieser Variablen nötig ist.

Top-Down-Programmentwicklung

Die nachfolgenden Beispiele zeigen die Entwicklung eines Moduls vom Rahmen zum Endprogramm, indem neue Programmelemente schrittweise eingefügt werden.

Erstellung eines syntaktisch korrekten Rahmen­programms

int main(void)
  {
  return 0;
  }

Festlegung von Teilaufgaben mit Kommentarüberschriften

/* Berechne Durchschnitt, Varianz u. Standardabweichung einer Meßwertreihe. */

int main(void)
  {
  /* Benötigte Variablen */

  /* Eingabe der Meßwerte */

  /* Statist. Kennwerte aus Meßwerten berechnen */

  /* Ausgabe der Kennwerte */

  /* Ende */
  return 0;
  }

Festlegung benötigter Variablen

/* Berechne Durchschnitt, Varianz u. Standardabweichung einer Meßwertreihe. */

#define VALMAXLEN 1000;  /* Für Meßreihe reserv. Länge */

int main(void)
  {
  /* Benötigte Variablen */
  int values[VALMAXLEN];  /* Meßreihe */
  int valLen;  /* Anzahl eingelesener Meßwerte */
  double d, v, s;  /* Durch., Varianz u. Std.abw. */

  /* Eingabe der Meßwerte */

  /* Statist. Kennwerte aus Meßwerten berechnen */

  /* Ausgabe der Kennwerte */

  /* Ende */
  return 0;
  }

Definition abstrakter Funktionen und deren Aufrufe

Es werden nur leere Funktionsrümpfe eingefügt. Hier sollte man auch schon den ersten Compilerlauf absolvieren, um syntaktische Korrektheit des Rahmenprogramms zu prüfen.

/* Berechne Durchschnitt, Varianz u. Standard­abweichung einer Meßwert­reihe. */

#define VALMAXLEN 1000;  /* Für Meßreihe reserv. Länge */

/* Meßreihe einlesen und tatsächl. Länge alen ermitteln */
void getValues(int a[], int *alen)
  {}

/* Durchschnitt, Varianz  und Standardabw. einer Meßreihe berechnen */
void computeStatistics(int a[], int alen, double *d, double *v, double *s)
  {}

/* Durchschnitt, Varianz  und Standardabw. formatiert ausgeben */
void printStatistics(double d, double v, double s)
  {}

int main(void)
  {
  /* Benötigte Variablen */
  int values[VALMAXLEN];  /* Meßreihe */
  int valLen;  /* Anzahl eingelesener Meßwerte */
  double d, v, s;  /* Durch., Varianz u. Std.abw. */

  /* Eingabe der Meßwerte */
  getValues(values, &valLen);

  /* Statist. Kennwerte aus Meßwerten berechnen */
  computeStatistics(values, valLen, &d, &v, &s);

  /* Ausgabe der Kennwerte */
  printStatistics(d, v, s);

  /* Ende */
  return 0;
  }

Konkretisierung der Funktionsdefinitionen

Hier beginnt die eigentliche Arbeit: das Füllen der Funktionen mit Inhalt.

/* Berechne Durchschnitt, Varianz u. Standardabweichung einer Meßwertreihe. */

#define VALMAXLEN 1000;  /* Für Meßreihe reserv. Länge */

/* Meßreihe einlesen und tatsächl. Länge alen ermitteln */
void getValues(int a[], int *alen)
  {
  int i;  /* Hilfsvariable */
  printf("Max. %d Werte eingeben, Abbruch mit 'Q': ", VALMAXLEN);
  for (i=0; i<VALMAXLEN; i++)
    if (scanf("%d", &a[i]) <= 0)  /* z.B. 'Q' eingegeben */
      break;
  alen = i;
  }

/* Durchschnitt, Varianz  und Standardabw. einer Meßreihe berechnen */
void computeStatistics(int a[], int alen, double *d, double *v, double *s)
  {
  int i;  /* Hilfsvariable */
  double h;  /* Hilfsvariable */

  /* Durchschnitt berechnen */
  for (i=0,h=0; i<alen; i++)
    h += a[i];
  *d = h / alen;

  /* Varianz berechnen */
  for (i=0,h=0; i<alen; i++)
    h += (*d-a[i]) * (*d-a[i]);
  *v = h / alen;

  /* Standardabweichung berechnen */
  *s = sqrt(*v);
  }

/* Durchschnitt, Varianz  und Standardabw. formatiert ausgeben */
void printStatistics(double d, double v, double s)
  {
  printf("D: %.2f, V: %.2f, S: %.2f\n", d, v, s);
  }

int main(void)
  {
  /* Benötigte Variablen */
  int values[VALMAXLEN];  /* Meßreihe */
  int valLen;  /* Anzahl eingelesener Meßwerte */
  double d, v, s;  /* Durch., Varianz u. Std.abw. */

  /* Eingabe der Meßwerte */
  getValues(values, &valLen);

  /* Statist. Kennwerte aus Meßwerten berechnen */
  computeStatistics(values, valLen, &d, &v, &s);

  /* Ausgabe der Kennwerte */
  printStatistics(d, v, s);

  /* Ende */
  return 0;
  }

Fehlersuche

Fehler zu machen liegt in der Natur des Menschen. Fehler zu finden nicht.

Eine einfache Rechnung lehrt einem, daß kein größeres Programm (ab 300 Zeilen ca.) ohne Fehler sein kann. Wenn ein einzelnes Modul einen fehlerfreien Anteil von 99% hat, dann hat ein Programm, das aus 10 solcher Moduln besteht, schon einen Fehleranteil von 1 − 0,9910 ≈ 10%. Wenn also Software-Firmen die Fehler­freiheit ihrer Programme proklamieren, glauben Sie ihnen nicht. Im besten Fall sind es Fehler, die auf Grund unwahr­scheinlicher Benutzer­eingaben oder Rand­bedingungen selten zum Tragen kommen.

Fehler unterscheidet man grob in syntaktische und semantische Fehler.

Syntaktische Fehler

Syntaktische Fehler merkt man sofort, weil schon der Compiler oder Interpreter sich weigert, das Programm zu übersetzen. Sie stellen grobe Verstöße gegen die Sprache selbst dar wie im nachfolgenden Bsp., wo die in C syntaktisch vorgeschriebene Klammer um den Ausdruck i = 1 vergessen wurde:

if i = j
  i = 0;

Syntaktische Fehler sind damit sprachabhängig. Obiges Beispiel ist z.B. in Pascal syntaktisch korrekt (wenn es auch einen ganz anderen Sinn ergibt).

Semantische Fehler

Semantische Fehler sind diffizilerer Natur: das Programm läuft eine Weile, und dann stürzt es vielleicht ab, vielleicht verhält sich das Programm auch „irgendwie“ merkwürdig, vielleicht merkt man den Fehler auch gar nicht. (Da drängt sich einem natürlich die Frage auf, ob ein Fehler, den nie jemand bemerkt, überhaupt ein Fehler ist …)

Schrittweise mit dem Debugger durch den Code gehen und Variablen ständig beobachten:

Indexfehler

Die Art und Weise, wie ein Array in C angelegt wird, verleitet oft Anfänger dazu, sich ein Array indiziert von 1 bis zu seiner Länge zu denken; so etwas erlaubt Pascal (falls das Array so deklariert wurde), aber nicht C.

#define ALEN 100

int a[ALEN];

Obiges Array kann also von a[0] bis a[99] abgefragt werden. Im nachfolgenden Beispiel wird a[0] nicht abgefragt und a[alen] gibt es nicht, dafür gibt es etwas anderes: im besten (!) Fall stürzt das Programm sofort mit dem Fehler „Index out of bounds“ ab; dann merkt man nämlich den Fehler noch rechtzeitig selbst und nicht erst der Anwender:

long sum(int a[], int alen)
  {
  long h=0; int i;
  for (i=1; i<=alen; i++)  /* Array-Index startet mit 0 und nicht mit 1 */
    h += a[i];
  return h;
  }

Initialisierungsfehler

Variablen sollten vor ihrem ersten Lesen initialisiert sein. Wozu braucht man Initiali­sierungen? Ganz einfach: wenn es egal ist, was die Variable für einen Wert hat, dann braucht man es nicht. Allerdings braucht man dann auch das Programm nicht, denn es ist dann meistens nutzlos :-). Passiert Anfängern wie Fortge­schrittenen gleichermaßen: den ersten, weil sie es nicht wissen, den zweiten, weil sie es einfach vergessen:

long sum(int a[], int alen)
  {
  long h;  /* h nicht initialisiert */ 
  int i;
  for (i=0; i<alen; i++)
    h += a[i];
  return h;  /* Den Wert von h am Schluß wissen nur die Sterne … */
  }

Verwechselung von Operatoren

Passiert gerne Anfängern, Mathematikern und Pascalianern, die sich in C, C++ oder Java versuchen (:-): Der Gleichheits- wird mit dem Zuweisungs­operator verwechselt. In Pascal fragt man der Wert einer Variablen wie folgt ab:

if (a = 3)

Notiert man dies in C, C++ oder Java, kommt ein völlig anderer Sinn heraus: a wird erst der Wert 3 zugewiesen; und dann wird erst der geklammerte Ausdruck ausgewertet, hier mit dem Wert 3. Dies ist immer wahr, da alles, was nicht 0 ist, wahr ist und entspricht völlig:

if (3)

if (a = 3) ist also völlig sinnlos, aber möglich und nicht ident zu

if (a == 3)

was auf Gleichheit abprüfen würde. Bei guten C-Compilern kann man entsprechende Optionen einschalten, so daß einen der Compiler warnt, falls er solche Konstrukte entdeckt.

Nichts­destotrotz sind Zuweisungen in Ausdrücken oft sinnvoll, da sie kompaktere Notierungen als in anderen Sprachen erlauben. In Pascal ist das z.B. nicht möglich, da die Zuweisung keinen Ausdruck darstellt. Folgende Kopier­routine macht Gebrauch davon:

while ((c=getchar()) != EOF)
  putchar(c);

Falsch begrenzte Anweisungsblöcke

Oft werden Klammern (C/C++/Java) bzw. BEGIN's und END's (Pascal) im Eifer des Gefechts falsch gesetzt oder ganz vergessen. Das passiert nicht nur Anfängern, sondern auch Profis immer wieder. Die heutigen Compiler sind noch nicht so intelligent, daß ihnen eine bloße optische Einrückung reicht, um die betreffenden Anweisungen abhängig von der vorgeschalteten Kontroll­struktur zu machen :-).

Folgende Einrückung täuscht zwei abhängige Anweisungen vor, das Programm wird aber immer mit 1 beendet. Die Anweisung return 0; ist sog. „toter“ Code: Code, der nie ausgeführt wird. Bei guten C-Compilern kann man wiederum entsprechende Optionen einschalten, so daß einen der Compiler warnt, falls er solche verdächtigen Konstrukte entdeckt:

int main(void)
  {
  ..
  if (error)
    printf("Fehler im Programm!\n");
    return 1;
  return 0;
  }

Tip: Wenn man schon absehen kann, daß nach einem if, for, while oder do mehrere abhängige Anweisungen folgen müssen, einfach immer zuerst mit einem leeren Klammerpaar starten:

if (server_is_ready) {
  }

und dann erst mit den abh. Anweisungen füllen:

if (server_is_ready) {
  init(client);
  init(connection);
  transmit(client, server);
  close(connection);
  close(client);
  return 0;
  }

Falsche Semikola

Nicht nach jeder Zeile soll ein Semikolon stehen – sondern nach jeder Anweisung. Ein typischer Anfängerfehler. Folgender Code druckt nicht wie erwartet die Quadrate von 1 bis 10. Das Semikolon ist zwar semantisch falsch, aber syntaktisch richtig: es ergibt sich somit eine sog. „leere“ Anweisung, die 10 x ausgeführt wird. Ob das allerdings in der Intention des Programmieres lag, darf füglich bezweifelt werden:

for (i=1; i<=10; i++);  /* Fehler */
  printf("%d ",i*i);

Hinweis: In C, C++ und Java sind Semikola syntaktische Bestandteile der Anweisungen, in Pascal hingegen syntaktische Trenner zwischen solchen.

Pascal:

  begin
  for i:=1 to 10 do
    writeln(i*i, ' ');
  writeln  { Kein Semikolon mehr erforderlich }
  end.

C/C++:

int main(void)
  {
  for (i=1; i<=10; i++)
    printf("%d ",i*i);
  printf("\n");  /* Semikolon erforderlich */
  }

Fehlerhafte Zugriffe infolge von Seiteneffekten

S. Entwicklungstips.

Zeigerfehler

Einer der häufigsten und gefürchtetsten Fehler in C und C++: Fehler im Umgang mit Zeigern. C zeigt sich gnadenlos, wenn man nicht genug Respekt vor ihnen mitbringt. Diese Fehler sind so typisch für C, daß sie geradezu die C-Programmierer charakterisieren. In Java wurden daher die Zeiger auch abgeschafft.

Hier wurde ein Zeiger alloziert, aber nicht darauf überprüft, ob die Allokation erfolgreich war. Die nachfolgende Referenzierung kann scheitern:

p = (char*)malloc(10000);
strcpy(p,input);

Besser wäre:

if ((p=(char*)malloc(10)) != NULL)
  strcpy(p,input);
else
  error("No memory available!");

Hier wurde ein abstrakter Datentyp als Feld namens list aufgebaut. Bei der Erstellung des Feldes in list_add werden nicht alle Zeiger alloziert (beabsichtigt), jedoch auch kein Nullpointer statt dessen eingesetzt! Die spätere Aufräumroutine list_release hat keine Möglichkeit zu unterscheiden, ob jeder einzelne Pointer alloziert war oder nicht und versucht somit, alle zu deallozieren:

static void list_add(const char *name)
  {
  if (name)
    list[list_count] = (void*)name;
  list_count++;
  }

static void list_release(void)
  {
  int i;
  for (i=list_count-1; i>=0; i++)
    free(list[i]);
  ..
  }

Besser wäre es, nicht-allozierte Zeiger als solche mit einem Nullzeiger zu kennzeichnen:

static void list_add(const char *name)
  {
  if (name)
    list[list_count] = (void*)name;
  else
    list[list_count] = NULL;
  list_count++;
  }

Außerdem empfehlenswert ist die Nullsetzung für deallozierte bzw. noch nicht benutzte Zeiger. Greift man dann trotzdem noch auf diese Zeiger zu, wird durch die Abprüfung auf NULL Schlimmes verhindert:

void my_function(void)
  {
  char *p = NULL;
  ..
  p = strdup("My text");
  if (p)
    do_sth_with(p);
  ..
  free(p); p = NULL;
  ..
  if (p)
    do_sth_with(p);
  ..
  }

Austestung

Eine kurze und unvollständige Checkliste, was man beachten sollte:

© 2002, 2009 asdala.de: Kon­takt & Daten­obhut