Erweiterungen

Wir haben aus einem etwas größeren Quelltext nun ein Projekt mit mehreren Quelltexten erstellt und wissen nun, wie wir ein Projekt aus mehreren Klassen erstellen können.

In dieser Lektion geht es etwas entspannter zu, wir werden das bisherige Projekt etwas erweitern und uns somit ein wenig dem Alltag des Programmierens widmen.

Erste Schritte zum Debugging

Debugging heißt auf Deutsch soviel wie „Entkäfern“. Der Ursprung des Wortes stammt aus der Zeit, als Computer noch Lagerhallen füllten und aus Röhren bestanden. Röhren werden warm und Wärme lockt Ungeziefer an, die dann über die Stromkreise krabbeln und diese kurzschließen. Das überlebten die Käfer natürlich nicht und so musste der Computer stillgelegt werden und jemand musste den Käfer entfernen: Der Computer wurde „debuggt“. Soweit die Historie des Begriffs. Tiere im Computer sind heutzutage nicht mehr üblich, doch die Käfer im Programm haben Umgangssprachlich überlebt.

In dieser Lektion geht es um den Grundgedanken von Debugging: Wir stellen eine Erwartung und wir wollen prüfen, ob diese Grunderwartung erfüllt wird. Der einfachste Schritt ist, einfach ein printf() in den Code zu schreiben, uns ein Zwischenergebnis ausgeben zu lassen und es mit unserer Erwartung zu vergleichen.

Wenn wir so komplexe und verschachtelte Datentypen haben, dann wird ein printf() nicht reichen. Es ist also durchaus geschickt Klassen mit Methoden auszustatten, um Fragen stellen zu können. Bei den HTML-Knoten unterscheiden wir zwischen den Textknoten und den HTML-Tags. Schreiben wir also eine Funktion, die beantwortet, welchen Typ eine Node hat.

char const * node_getType( struct Node * node )
{
  if( node->Text ) return "text";
  else             return "node";
}

Da es sich um eine Funktion handelt, die für die Struktur 'Node' zuständig ist, hat sie ein entsprechendes Präfix („node_“) und landet schlussendlich auch in der Datei „node.c“. Zusätzlich zu der Definition, müssen wir die Funktion in der Headerdatei („node.h“) deklarieren, damit sie von anderen Dateien aus gefunden wird:

extern char const * node_getType( struct Node * node );

Fertig. Nun können wir beliebige Knoten fragen, welchen Typ sie haben. Um das zu demonstrieren ändere ich die Main-Routine und füge dort zwei Aufrufe hinzu:

  node_addSubNode( body, text );
 
  /* Datenstruktur fertig */
 
  printf( "Typ für text: %s\n", node_getType( text ));  // HINZUGEFUEGT
  printf( "Typ für html: %s\n", node_getType( html ));  // HINZUGEFUEGT
 
  node_delete( html );

Entsprechend erhalten wir die Ausgabe:

Typ für text: text
Typ für html: node

Wenn wir eine Node haben, von der wir nicht wissen, ob sie Text oder Node ist, können wir nun nachfragen. Da wir nur zwei Möglichkeiten haben - Text oder Knoten - können wie die Frage auch als Ja/Nein-Frage stellen und da wir wissen, dass Werte ungleich 0 für Wahr und die 0 für Falsch steht, entsprechend beantworten:

int node_isText( struct Node * node )
{
  return node->Text != NULL;
}

Wie immer, die Deklaration zusätzlich in die Headerdatei und schon haben wir eine die Datenstruktur „Node“ erweitert. Die Benutzung des Frageworts 'is' ist für Ja-/Nein-Fragen sehr geläufig.

Auch, wenn die Funktion node_isText nur aus einem einzigen Ausdruck (node→Text != NULL) besteht, der eventuell sogar kürzer zu schreiben ist, als den Funktionsaufruf und hier der Computer nicht nur den Ausdruck auswerten muss, sondern zusätzlich eben auch noch die Funktion rufen muss - also sogar zusätzlich Zeit verschwendet, so hat diese Funktion einen wichtigen Vorteil gegenüber des Ausdrucks: Der Funktionsname erklärt ohne weiteren Kommentar, was man hier wissen möchte. Solche Anhaltspunkte sind sehr hilfreich, um in einem komplexen Ausdruck zu verstehen, was eigentlich passiert. Es kann sein, dass sich die Struktur irgendwann verändert, so dass es kein Member 'Text' mehr gibt. In diesem Fall müssen alle Ausdrücke geändert werden, die das Member 'Text' verwenden. Mit Hilfe solcher Fragefunkionen (man spricht auch häufig von Gettern), muss nur die fragende Funktion geändert werden, nicht der Quelltext, der die Frage stellt.

Im folgenden Quelltext, wird die Funktion node_isText gleich verwendet:

Ausgabe

Mit viel Mühe haben wir jetzt ein Projekt erstellt, das in der Lage ist, HTML-Tags als Datenstruktur zu halten. Was wir bisher nicht haben, ist die Möglichkeit, diese Tags auf dem Bildschirm auszugeben. Die wichtigste Überprüfung ist natürlich der Vergleich des Resultates mit der Erwartung.

Die folgende Funktion verwendet statt printf() die Funktion fprintf(). Die Funktion ist identisch, nur dass ihr zusätzlich ein Ausgabestream übergeben werden kann. Die Konstante „stdout“ verweist dabei auf die Standardausgabe des Programms auf die auch printf() schreibt.

Hier eine Möglichkeit die Ausgabe zu beschreiben. Zunächst schauen wir uns wieder die einfachere und untergeordnete Struktur 'Parameter' an:

void parameter_print( struct Parameter const * par, FILE * stream )
{
  if( par )
  {
    fprintf( stream, " %s=\"%s\"", par->Name, par->Value );
  }
}

Im Formatstring befindet sich als erstes Zeichen ein Leerzeichen. Dies wird verwendet, um Abstand zum vorherigem Parameter, bzw. Tag-Namen des Knotens zu erhalten. Den Knoten geben wir wie folgt aus.

void node_print( struct Node * node, FILE * stream )
{
  if( node && stream )
  {
    if( node_isText( node ) )
      fprintf( stream, "%s", node->Text );
    else
    {
      if( node->Name )
      {
        struct Parameter * par;
 
        fprintf( stream, "<%s", node->Name );
 
        par = node->Parameter;
        while( par )
        {
          parameter_print( par, stream );
          par = par->Next;
        }
 
        if( node->SubNodes )
        {
          struct Node * sub;
 
          fprintf( stream, ">" );
 
          sub = node->SubNodes;
          while( sub )
          {
            node_print( sub, stream );
            sub = sub->Next;
          }
 
          fprintf( stream, "</%s>", node->Name );
        }
        else
        {
          fprintf( stream, " />" );
        }
      }
    }
  }
}

Was passiert: Zunächst wird überprüft, dass die beiden Argumente nicht Null sind. Damit garantieren wir nicht, dass sie gültig sind, aber zumindest sind sie nicht offensichtlich ungültig. Anschließend fragen wir mit unserer neuen Funktion node_isText(), ob die Node ein Text ist und geben ihn gegebenenfalls einfach aus.

Ist der Knoten kein Text, so schreiben wir die spitze öffnende Klammer (“<“) des Tags und den Namen des Text hin. Anschließend werden alle Parameter ausgegeben, die von sich aus ja bereits Abstand zum Tagnamen, bzw. zum Vorgänger halten.

Als nächstes stellt sich die Frage, ob weitere Unterknoten existieren. Falls ja, so gehen wir alle Unterknoten durch und rufen uns selbst rekursiv auf, um auch die Unterknoten auszugeben. Sofern es keine Unterknoten gibt, können wir das HTML-Tag direkt abschließen mittels “ />“.

Wie immer kommt die Deklaration der beiden Funktionen auch in die jeweiligen Header und anschließend rufen wir die Ausgabe-Funktion auf:

  node_addSubNode( body, text );
 
  /* Datenstruktur fertig */
 
  printf( "Typ für text: %s\n", node_getType( text ));
  printf( "Typ für html: %s\n", node_getType( html ));
 
  node_print( html, stdout );
  printf( "\n" );
 
  node_delete( html );

Wir erhalten die Ausgabe:

Typ für text: text
Typ für html: node
<html><body background="white"><h1>proggen.org</h1>C-Tutorial</body></html>

Dies entspricht im Vergleich dem Ursprünglichen HTML-Text, bis auf die Einrückung und die Tatsache, dass hier alles in einer Zeile steht. Für einen Webbrowser macht das keinen Unterschied. Für uns wäre es leserlicher.

Aufgabe

Du hast nun viel Quelltext von mir gesehen, den ich hier vorgebetet habe. Nun ist es an der Zeit, sich damit genauer auseinander zu setzen und selbst zu überlegen, wie man die Funktion node_print() so erweitert, dass sie für ein Tag nur eine Zeile verwendet und die Tags einrückt. Der Ausgabe soll also so aussehen:

<html>
  <body background="white">
    <h1>
      proggen.org
    </h1>
    C-Tutorial
  </body>
</html>

Kurze Überlegung, was benötigt wird: Wir müssen wissen wie tief wir einrücken müssen. Die Funktion node_print() nennen wir daher erstmal in node_printIntern um und fügen sie NICHT in den Header ein - damit ist sie für andere Quelltexte nämlich nicht sichtbar. Anschließend geben wir ihr ein zusätzlichen Parameter, der angibt, wie tief eingerückt werden soll:

void node_printIntern( struct Node * node, FILE * stream, unsigned int indent )
{
  if( node && stream )
  {
    if( node_isText( node ) )
      fprintf( stream, "%s", node->Text );
    else
    {
      if( node->Name )
      {
        struct Parameter * par;
        fprintf( stream, "<%s", node->Name );
 
        par = node->Parameter;
        while( par )
        {
          parameter_print( par, stream );
          par = par->Next;
        }
 
        if( node->SubNodes )
        {
          struct Node * sub;
 
          fprintf( stream, ">" );
 
          sub = node->SubNodes;
          while( sub )
          {
            node_print( sub, stream );
            sub = sub->Next;
          }
 
          fprintf( stream, "</%s>", node->Name );
        }
        else
        {
          fprintf( stream, " />" );
        }
      }
    }
  }
}

Für den vorhanden Code, der die Funktion node_print() verwendet, bauen wir eine Adapter-Funktion, die nichts anderes macht als node_printIntern() zu rufen. Dabei wissen wir ja, dass wir zunächst ohne Einrückung, also 0, beginnen.

void node_print( struct Node * node, FILE * stream )
{
  node_printIntern( node, stream, 0 );
}

Deine Aufgabe ist nun die Funktion node_printIntern() so zu ändern, dass sie die Ausgabe wie gewünscht formatiert. Dazu kannst Du eine indent()-Funktion schreiben, die Dir die Leerzeichen generiert und diese Funktion dann innerhalb von node_printIntern() aufrufen.

Wenn das nicht sofort funktioniert, dann überlege Dir, wo Du einrücken musst und versuch Schritt für Schritt zu verstehen, was das Programm wo tun muss, um einzurücken. Wenn Du zuviele Schwierigkeiten hast, melde Dich im Forum und präsentiere Deine Lösung und erkläre Dein Verständnis.

Fazit der Lektion

Diese Lektion unterscheidet sich von den vorherigen: Statt eines kleinen Test-Programms erweitern wir vorhandenen Quelltext. Das entspricht eher dem Alltagsgeschäft, schließlich schreibt man nicht täglich ein Mini-Programm, sondern Programme bauen aufeinander auf. Wir haben hier also zwei Dinge getan: Ein vorhandenes Programm erweitert und auch gesehen, dass es durchaus üblich ist, Funktionen zu schreiben, die nicht benötigt werden, um ein Programm ablaufen zu lassen, sondern nur dafür da sind, um Fragen während der Entwicklung formulieren zu können.

Wir kennen damit nun den eher alltäglichen Weg der Programmierung.