ST-Computer C-Kurs (Teil 4)

Mit diesem Teil endet mein C-Kurs. Man könnte zwar noch sehr viel zum Thema C sagen, aber eine Fachzeitschrift kann ein Buch eben nicht ganz ersetzen, und wer sich ernsthaft mit der Programmierung in C beschäftigt, wird sicherlich schon eines der inzwischen zahlreichen Bücher zum Thema besitzen. Im vorliegenden Teil werde ich Ihnen die meisten der noch ausstehenden Konzepte vorführen, die da wären:

Aber zuallererst die Lösung der Aufgabe aus dem letzten Teil. Es waren mindestens zwei Verbesserungen der Makros, zum Setzen und Löschen von Punkten auf dem Bildschirm, gefragt. In Listing 1 können Sie sehen, was mir dazu eingefallen ist. Der Nachteil dieser Lösung liegt auf der Hand. Man braucht im Gegensatz zur 'Einfachen' eine ganze Weile, bis man einsieht, daß sie funktioniert.

1. Argumente aus der Kommandozeile

Oft will man Programme schreiben, denen man bestimmt Parameter mit auf den Weg geben will, und die dann anhand der Argumente wissen, was sie zu tun haben, ohne weitere Unterhaltungen mit dem Benutzer zu führen. Die Parameter werden meist Dateinamen oder Optionen für das Programm sein. Stellen Sie sich zum Beispiel ein Programm list vor, das so aufgerufen

list -p dateil -n datei2 datei3

eine Listing der Dateien dateil — datei3 erzeugt, wobei dateil auf den Drucker und datei2 bzw. datei3 auf dem Bildschirm mit Zeilennummern versehen ausgegeben werden sollen. Die Minuszeichen vor den Optionen sind übrigens eine UNIX Konvention, die sich inzwischen ziemlich verbreitet hat.

Sicher werden Sie vom Desktop aus schon Programme gestartet haben, die die Endung .ttp hatten (das soll bedeuten: tos takes parameter). Nach dem Start meldet sich dann zuerst eine Dialogbox, in die man die Parameter, die man dem Programm mitgeben will, eintragen kann.

Wenn ein Programm auf diese Weise gestartet wird, legt TOS die übergebenen Argumente an einer bestimmten Stelle der base page - das ein Bereich, der 256 Bytes vor dem Beginn des Programms liegt - ab. C bietet nun dem

/* Loesung der Aufgabe aus dem Teil III */

/* #define short Int *//* MEGAMAX Besitzer muessen diese Zeile mitnehmen */
short *ytabtl;  /* Tabelle der Anfangsadressen aller Zeilen */ 
long  xbios() ;
#define  spixel(x,y)  *(ytab[y] + (x>>4)) |=  (l « <~(x) & 15)) 
#define  cpixel(x,y)  *(ytab[y) + (x>>4)) &~ ~(1 « (~(x) & 15))
main()
{
register short i,x,y;
ytab[O] = (short *) xbios(3);  /* Tabelle initialisieren */ 
for (i = 1; i < 400; i++)
ytab[i] - ytab[i-1] + 40;

for (x - 0; x < 640; x++) 
  for (y - 0; y < 400; y++) 
    spixel(x,y);               /* Alle Bildschirmpunkte setzen */

for (x - 0; x < 640; x++) 
  for (y - 0; y < 400; y++) 
     cpixel(x,y);              /* Alle Bildschirmpunkte loeschen */
}
/* Ende Loesung der Aufgabe aus Teil III */

Benutzer eine einheitliche Schnittstelle, um auf diese Parameter zuzugreifen. Dazu erhält die altbekannte mainO Funktion zwei Parameter mit auf den Weg, die ich Ihnen bis jetzt einfach unterschlagen habe. Schauen Sie dazu gleich einmal Listing 2 an. Wie Sie sehen, hat mainQ zwei Argumente. Einen int Wert namens arge und einen komplizierteren Datentyp argv. Wissen Sie noch, wie man die Vereinbarung für argv liest? - Richtig, mit der „Von-Rechts-Nach-Links-Regel". Also argv ist ein Array (die []) von Zeigern (der *) auf char Werte. Diese Zeiger sind genau die Adressen der Argumente aus der Kommandozeile. Schauen Sie sich bitte Bild l an. argv[l] zeigt auf das erste, argv[2] auf das zweite Argument usw. argvfO] hat eine Sonderstellung. Er zeigt, je nach C-Compiler, auf mehr oder minder sinnvolle Dinge. Unter UNIX wird mit argv[0] den Namen des Programms geliefert. Bei Bild 1 bekäme man also den String "tue__nichts". Beim C-Compiler des

Entwicklungspakets erhält man immer „C Runtime" und beim MEGA-MAX z. B. gar nichts. Damit man weiß, wieviele Argumente in der Kommandozeile stehen, gibt es arge. Der Name steht natürlich für 'argument count' und man erhält in dieser Variable die Zahl der Argumente. argv[0] wird allerdings mitgezählt, so daß in Bild l arge = 5 gilt.

Das Beispielprogramm 1 (Listing 2) tut nun nichts anderes, als die übergebenen Argumente auszudrucken. Beim Atari darf die Summe der Längen aller Argumente zusammen nicht mehr als 127 Zeichen sein, und wenn man die Dialogbox des Desktop zur Eingabe benutzt, hat man doch viel weniger Zeichen zur Verfügung.

/* ST Computer C - Kurs Beispiel 4.1 */ 
/* * Druckt alle in der Kommandozeile uebergebenen Argumente aus. */
#define Wait()   gemdos(OxOl)
main (argc, argv) 
int   argc; 
char  * argv[]; 
{
  int   i ;
  for (i = 0; i < argc; i++)
     printf ( "argv[%2d] - %s\n", i , argv[i ] )

  printf ( "Ende mit beliebiger Taste\n") ; 
  Wait() ;
}

Listing 2

Der argv ist, vielleicht haben Sie es be merkt, ein Beispiel für ein zweidimen sionales Array. Durch die Äquivalent zwischen Zeigern und Arrays ist es z. B. möglich, durch

c = argv[3][6];

auf das 6. Zeichen des 3. Strings in der Kommandozeile zuzugreifen. Wieviele Dimensionen ein Array haben kann, hängt vom jeweiligen C ab (drei sind es aber fast immer). Einen Unterschied sollten Sie sich als speicherplatzbewußter Programmierer klarmachen. Durch

int *test[10];

reservieren Sie Platz für 10 Zeiger auf int-Werte. Der eigentliche Speicherplatz für die Werte selbst muß dann während der Laufzeit dynamisch durch malloc() oder calloc() angefordert werden. Wohingegen

int test[10][10];

den Platz für 100 int Werte reserviert.

Im ersten Moment sieht es so aus, als würde die Zeigerschreibweise mehr Speicherplatz erfordern als die Array-schreibweise, muß man doch, um ebenfalls 100 int-Werte zu haben, zusätzlich zu den 10 Zeigern noch den Platz für die Werte selbst belegen. Die Zeigerschreibweise hat dennoch zwei entscheidende Vorteile. Erstens nutzt man bei den meisten Problemen gar nicht den gesamten reservierten Platz im Array aus. Durch die Arrayschreib-weise wird also unter Umständen unnütz Speicher belegt. Zweitens ist der Zugriff über Zeiger wesentlich schneller, da der Compiler indirekt über die Zeiger adressieren kann, während bei einem Array über eine Multiplikation und Addition adressiert werden muß. Was man also verwendet, hängt im

Einzelfall vom Füllungsgrad des Arrays und den Zeitanforderungen an den Zugriff ab.

2. Typedefs

Bevor wir uns auf Strukturen, Unions und Zeiger stürzen, will ich Ihnen noch eine Konstruktion in C zeigen, die zwar nichts zum eigentlichen Sprachumfang beiträgt, aber gerade beim Umgang mit komplizierten Datenstrukturen das Leben etwas leichter macht. Gemeint ist die Typ-Definition mit dem Schlüsselwort typedef. Vor der Erklärung gleich ein Beispiel:

typedef int BIRNEN, AEPFEL;

Definitionen dieser Art dürfen überall dort stehen, wo auch sonst Definitionen und Deklarationen erlaubt sind. Im obigen Beispiel passiert folgendes. Von der Stelle im Programm, an der die Definition auftaucht, sind AEPFEL und BIRNEN völlig gleichberechtigte Datentypen neben dem Typ int und sind von ihrer Bedeutung her auch völlig identisch mit int-Werten. Es sind dann zum Beispiel folgende Variablendeklarationen legal:

BIRNEN bauml, baum2; AEPFEL baum3, baum4;

Es ist natürlich auch weiterhin möglich, Äpfel und Birnen zu addieren, da der Compiler intern aus Objekten der obigen Art wieder int-Werte macht. Der typedef ist also in erster Linie „syntaktischer Zucker", der Programme leichter lesbar macht, da man am Typ einer Variablen gleich erkennen kann, für was sie benutzt wird. Häufig benutzt man Typ Definitionen, um ein Programm zwischen verschiedenen Rechnern oder Compilern übertragbar zu machen. Hat man z. B. mit

MEGAMAX C ein Programm geschrieben, das folgenden typedef verwendet:

typedef int SHORT;

und benutzt dann im Programm selbst konsequent den neuen Typ SHORT, braucht man bei einer Neuübersetzung z. B. mit LATTICE C nur die Zeile mit dem typedef so zu ändern:

typedef short SHORT;

und das Programm müßte problemlos laufen (abgesehen natürlich von einigen anderen Unterschieden zwischen den beiden Compilern).

Richtig nützlich werden typedefs allerdings erst bei komplexeren Datenstrukturen, wie Sie im weiteren Verlauf noch sehen werden.

Theoretisch ist es einem C Compiler möglich, anhand von typedefs eine genaue Überprüfung von Typen und ihrer Verwendung vorzunehmen, allerdings kenne ich kein C, das von dieser Möglichkeit Gebrauch macht.

3. Structs und Unions

Unter dieser Überschrift steht der letzte größere Abschnitt, der uns noch bleibt. Für Pascal Programmierer bietet sich hier fast nichts Neues. Es gibt kaum Unterschiede zwischen Pascal records und C structs.

In einem struct faßt man Objekte, die zueinander gehören, zusammen und bringt so Ordnung in seinen Datensalat. Als Trivialbeispiel kann z. B. ein Punkt in einem Koordinatensystem dienen (vielleicht ein Bildschirmpunkt). Wenn wir in der 2. Dimension bleiben, hat ein Punkt einen x- und einen y-Wert. In C würde eine entsprechende Definition so aussehen:

struct punkt [ int x,y; j;

Variable dieses Typs können Sie dann wie üblich definieren:

struct punkt pl, p2[10], *p3;

Das Schlüsselwort struct muß in diesem Fall dabeistehen. Im obigen Beispiel steht nun p l für ein Objekt vom Typ punkt, p2 ist ein Array von 10 Punkten und p3 enthält die Adresse einer Struktur vom Typ punkt. Schön und gut, werden Sie sagen, aber wie kann ich Werte in so einen struct hineinstecken oder welche herausholen? Dafür gibt es in C den Y-Operator. Er selektiert ein bestimmtes Mitglied eines Structs. Wenn z. B. pl für die Mitte des Atari-Bildschirm stehen soll, nichts einfacher als das:

pl.x = 320; pl.y = 200;

Der Zugriff auf einen x-Wert im p2-Array sähe dann beispielsweise so aus:

xdistanz = p2[i].x - p2[i-l].x;

Einen Spezialfall stellt p3 dar. Der Zugriff, auf den x-Wert des structs auf den p3 zeigt, bewerkstelligt man so:

xwert = (*p3).x;

Die Klammern sind unbedingt notwendig, da der '.' eine höhere Priorität hat als der '*'. Da die obige Konstruktion aber sehr häufig auftritt, gibt es in C einen eigenen Operator dafür:

xwert = p3—x;

ist von der Wirkung her vollkommen identisch mit der vorigen Zuweisung.

Einige Wermutstropfen gibt es bei der Verwendung von structs. Ein struct kann nie als Ganzes zugewiesen oder als Parameter an Funktionen übergeben werden. Folgende Ausdrücke sind also falsch:

FALSCH! pl = p2[4]; FALSCH! setze_punkt(pl);

wohingegen die entsprechenden Operationen mit Zeigern erlaubt sind:

P3 = &p2[4]; setze__punkt(&pl);

Es gibt allerdings Versionen von C, in denen obige Operationen erlaubt sind. Man sollte allerdings damit sehr vorsichtig zu Werke gehen, da Programme unter Umständen nicht mehr portabel sind.

Als Komponenten von Strukturen sind wieder Strukturen und (was uns beim nächsten großen Beispiel noch beschäftigen wird) Zeiger auf Strukturen erlaubt.

Folgende Vereinbarungen und Zuweisungen sind also völlig legal:

struct linie [
struct punkt anfang;
struct punkt ende; ] H, 12, *13;
ll.anfang.x = I2.ende.x; 13—anfang.x = 125;

Kommen wir zu den Unions. Alles, was ich zu den structs gesagt habe, gilt genauso für Unions. Der einzige Unterschied: Wo bei Structs Zucht und Ordnung herrscht und alle Komponenten brav nebeneinanderliegen, herrscht bei Unions eher „Datentypen Gruppensex" vor. Das heißt, alle Komponenten einer Union liegen im Speicher aufeinander. Es wird also nur soviel Platz reserviert, wie das größte Objekt in der Union verbraucht. Ein Beispiel ist:

union objekt [  
int laenge;  
float radius;  
];

Je nachdem, ob objekt einen Strich oder ein Kreis ist, enthält die Datenstruktur die Länge oder den Radius. Sich zu merken, was gerade in einer Union abgespeichert ist, liegt voll in der Verantwortung des Programmierers.

Jetzt ist es an der Zeit, noch einmal kurz auf typedef zu kommen. Structs und Unions sind das ideale Betätigungsfeld für Typ-Definitionen. Sie sparen nicht nur viel Schreibarbeit, sondern machen Programme wesentlich transparenter. Als Beispiel können Sie sich Listing 3 ansehen. Es ist ein Headerfile zu dem großen Beispiel, das wir als nächstes besprechen werden. Sie brauchen die Funktion der dort definierten structs im Moment noch nicht zu verstehen, sondern sollen sich nur die Art und Weise anschauen, in der der typedef verwendet wird. Eine Variable vom Typ struct Zeilen kann im Programm dann z. B. so vereinbart werden:

ZEILE zl,z2[10];
ZEILZEIG z3;

z3 ist ein Zeiger auf einen struct vom Typ ZEILE. Beachten Sie, daß der '*' vor z3 nicht geschrieben wird, da bereits in der typedef-Vereinbarung mit *ZEIL2EIG Zeilzeig als Zeigertyp vereinbart wird.

/* DATEI BTREE.H »/  
/* Definition der Datenstrukturen -fuer Crossreferenzprogramm i/  
typedef struct seilen  
int nummer; /* Nummer der Zeile */  
int datei; /* Index des Dateinamens in namelist l/  
struct zeilen *next; /* Verweis auf Nachfolger */  
} ZEILE, *ZEILZEIG;  
typedef struct bezeichner  
char »text; /* Text des Bezeichners */  
int anzahl; /* Haeu-fiqkeit des Auftretens */  
ZEILZEIG znumliste; /* Liste der Nummern der Zeilen in denen »/  
/* der Bezeichner auftritt */  
ZEILZEIG last; /* zeigt auf letzten Eintrag in znumliste*/  
struct bezeichner *links, /* linker bzw. rechter Nach-folger im ti  
*rechts; /* Baum. */  
} BEZEICHNER, *BEZZEIG;  
#define NIL (BEZZEIG) 0 /* Ende von BTREE.H */  

4. Rekursivität am Beispiel eines Crossreferenzprogramms

Im Folgenden kommt das letzte große Beispiel innerhalb meiner Einführung in C, Es werden praktisch alle Sprachelemente, die C zu bieten hat, verwendet, insbesondere von Zeigern und Strukturen wird ausgiebig Gebrauch gemacht. Außerdem sehen Sie Beispiele für rekursive Datenstrukturen, wie Listen und Bäume, die in allen Problemkreisen der Informatik immer wieder auftauchen.

Für die Erscheinungsform des Listings möchte ich mich von vornherein entschuldigen. Aus Platzgründen war es nötig, mit einem engeren Zeilenabstand auszudrucken als üblich. Außerdem mußte ich mehrere Anweisungen teilweise auf einer Zeile unterbringen. Sie sollten sich diese Vorgehensweise auf keinen Falls als Vorbild für Ihre eigenen Programme nehmen.

Das Programm erstellt aus beliebigen Eingabetexten ein sogenanntes Cross-referenzlisting. Das bedeutet für jeden Bezeichner (für jedes Wort): in einer Reihe von Eingabedateien wird in einer Liste ausgegeben, wievielmal, in welcher Datei und in welcher Zeile der Bezeichner auftaucht. Obwohl nicht auf Programme beschränkt, ist ein Crossreferenzlisting besonders bei größeren Programmen, die in mehrere Module zerfallen, nützlich, um einen Überblick über verwendete Funktionen und Variable zu bewahren. Das Programm bietet verschieden Optionen an, die Sie dem Kommentar am Anfang des Listings oder der Funktion usage() entnehmen können. Besonders nützlich ist die Möglichkeit, mit -f eine Datei anzugeben, in der Bezeichner stehen, die nachher nicht im Listing auftauchen sollen. Sie können dadurch z. B. verhindern, daß die C-Schlüsselworte mit ausgegeben werden. Ich habe mit dem Text zu diesem Teil des C-Kurses eine noch komfortablere Version des Programms an den Verlag geschickt. Sie erlaubt z. B., die Wildcards '*' und '?' in der Kommandozeile. Auf Anfrage sollten Sie diese Version vom Verlag erhalten können.

Nun zum Programm selbst. Da dies eine Einführung in C ist, werde ich nur ganz kurz auf das 'Wie' des Programmes eingehen. Die prinzipielle Behandlung von Binärbäumen und verketteten Listen ist ja in allen Programmiersprachen in etwa gleich. Wenn sie diese Thematik interessiert, sollten Sie sich zum Beispiel das Buch 'Algorithmen und Datenstrukturen' von Niklas Wirth anschauen. Die Beispielprogramme sind dort allerdings in Pascal geschrieben.

Nur soviel: das Programm läuft über die Eingabedateien und sammelt alle Bezeichner auf, die es dabei findet. Diese werden mit den Einträgen in einem -alphabetisch geordneten Binärbaum verglichen. Ist der Bezeichner bereits eingetragen, wird der anzahl Eintrag um eins erhöht und ein Knoten vom Typ ZEILE an die Liste mit den Zeilennummern angehängt. Ist er noch nicht da, wird ein neuer Knoten in den Baum gehängt. Wie der Baum in etwa aussieht, können Sie an Bild 2 sehen.

Bemerkenswert ist, wie man in C rekursive Datenstrukturen aufbaut. Sehen wir uns dazu den Aufbau des structs zeilen genauer an. Er ist typisch für den Knoten einer einfach verketteten linearen Liste. Linear bedeutet, das jeder Knoten genau einen Nachfolger hat.

struct Zeilen (  
/* * Hier irgendwelche Daten  
* die im Knoten gespeichert  
* werden sollen. */  
struct zeilen *next;  
]

In die Zeigervariable next wird beim Zusammenbau der Liste die Adresse des nächsten Knotens eingetragen. Wenn die next Variable den Wert NIL = ÖL enthält, ist nach Vereinbarung das Ende der Liste erreicht.

Ein Baumknoten ist ganz genauso aufgebaut, nur, daß er statt einem Nachfolger deren zwei hat.

Da man von vornherein nicht weiß, wieviele Knoten man im Verlauf des Programms braucht, muß man den Speicherplatz dynamisch verwalten. Dazu habe ich die beiden Funktionen tree__alloc() und num__alloc() geschrieben, die jeweils Platz für ein Objekt des gewünschten Typs anfordern und als Ergebnis einen Zeiger auf das Objekt liefern. Beachten Sie bitte, wie man mit sizeof die Größe eines Objekts bestimmen kann. Falls Sie übrigens glauben, daß damit meine Behauptung, es gebe in C keine eingebauten Funktionen, zusammenbricht, dann irren Sie. Der Ausdruck sizeof (Datentyp) oder sizeof(Variable) wird zur Übersetzungszeit vom Compiler ausgewertet. Während der Laufzeit steht an seiner Stelle nur noch eine Konstante, nämlich genau die Größe des Objekts in Bytes.

In main() können Sie an einem größeren Beispiel noch einmal sehen, wie man Optionen und Parameter aus der Kommandozeile übernimmt. Besonders interessant ist die Behandlung der -f Option. Das nachfolgende Argument wird als Dateiname interpretiert und die do__crossref() Funktion darauf losgelassen. Diese trägt alle gefundenen Bezeichner unter Datei Nummer 0 ein. Beim späteren Ausdrucken muß man jetzt nur noch darauf achten, keine Bezeichner mit Nummer 0 auszudrucken. So einfach ist das!

In do__crossref() wird entschieden, was ein Bezeichner ist. In der vorliegenden Version werden C-Kommentare überlesen. Wer Crossreferenzlistings für Pascal erstellen will, muß an dieser Stelle eine kleine Änderung machen.

Die Arbeit des Eintragens in den Baum respektive die Zeilenlisten übernehmen die beiden Funktionen tree__insert() und num__insert().

tree__insert() arbeitet rekursiv, das heißt, sie ruft sich selbst auf. Um die Funktion zu verstehen, machen Sie am besten eine Simulation von Hand und zeichnen sich mit Hilfe von Kästchen und Pfeilen auf, wie sich der Baum entwickelt. Beachten Sie bitte, das die Variable root (engl. Wurzel) zu Beginn des Programms auf den Wert NIL initialisiert wird.

Im letzten Teil wird dann das Listing ausgedruckt. Dabei wird der Baum (wiederum rekursiv) so durchlaufen, daß die Bezeichner in alphabetischer Reihenfolge gefunden werden (Genauer in ASCII Reihenfolge - erst alle Großbuchstaben, dann die Kleinen).

5. Schluß

Damit ist meine Einführung in C beendet. Ich hoffe, daß Sie einigen Nutzen daraus ziehen konnten und selbst schon einige Programme zum Laufen gebracht haben. Die C-Kenner unter den Lesern werden sicher bemängeln, daß ich einige Teilaspekte von C nicht erwähnt habe, wie Zeiger auf Funktionen oder die genauen Regeln für Gültigkeitsbereiche von Variablen und Funktionen. Meine Auswahl war zugegebenermaßen rein subjektiv. Mein Hauptanliegen war es, dem totalen Anfänger die Möglichkeiten von C näherzubringen und ihn zu motivieren, selbst weitere Entdeckungen zu machen. Dazu war es natürlich notwendig, nicht allzusehr zu theoreti-sieren. Ich hoffe, daß mir das gelungen ist.

In der nächsten Ausgabe der ST Computer werde ich voraussichtlich mit einem neuen Kurs unter dem Titel 'GEM und C' beginnen, in dem ich dann etwas tiefer in der Trickkiste wühlen und vollständige Applikationen für den Atari unter der GEM Oberfläche vorstellen will.


Thomas Weinstein
Aus: ST-Computer 09 / 1986, Seite 28

Links

Copyright-Bestimmungen: siehe Über diese Seite