Höhere Datentypen

C kennt mit Ausnahme von Mengen (sets) die strukturierten und dynamischen Datentypen wie Pascal auch: Arrays (Felder, Vektoren), Strukturen (feste und variante Records), Zeiger (Pointer) und Files (Dateien), die hier jedoch in einem eigenständigen Kapitel behandelt werden. Und wenn man Mengen doch benötigt, dann helfen vielleicht die Bit-Felder weiter, die in diesem Kapitel in einem eigenen Abschnitt eingeführt werden.

Eindimensionale Arrays

Eine über einen ganzzahligen Index ansprechbare endliche Sequenz von Speicherplätzen desselben Typs heißt auch in C Array oder Feld. Deklaration und Verwendung werden im folgenden Beispiel arrays1.c demonstriert.

Beispiel:

/* arrays1.c */
#include <stdio.h>

void main(void)
{
   int i, a[10], j ;
   char s[10]="Hello!";
   for (i=0; i<10; i++)
      a[i]=i*i;
   printf("\n&i=%d",&i); /* Adresse von i ausgeben */
   printf("\n&j=%d",&j); /* Adresse von j ausgeben */
   printf("\na=%d",a); /* Was ist a, das Array? */
   printf("\n&a=%d",&a); /* Adresse von a */
   printf("\n&a[0]=%d",&(a[0])); /* Adresse von a[0] */
   printf("\n&a[1]=%d",&(a[1])); /* Adresse von a[1] */
   printf("\na[0]=%d",a[0]); /* Wert in a[0] */
   printf("\na[1]=%d\n",a[1]); /* Wert in a[1] */
   for (i=0; i<10; i++)
      printf("s[%d]=%d ",i,s[i]); /* Was steht in s? */
   putchar('\n');
} /* end main */

Das Ablauflisting zu arrays1.c:

&i=2063807476
&j=2063807432
a=2063807436
&a=2063807436
&a[0]=2063807436
&a[1]=2063807440
a[0]=0
a[1]=1
s[0]=72 s[1]=101 s[2]=108 s[3]=108 s[4]=111 s[5]=33 s[6]=0 s[7]=0
s[8]=0 s[9]=0

Zu beachten ist hierbei:

Der Name des Arrays (z.B. a in arrays1.c) steht bereits für die Adresse des Feldes; d.h. z.B. bei scanf() muß kein Adreßoperator & mehr angegeben werden!

Der Indexbereich eines Arrays beginnt stets bei 0. Das oben deklarierte Array int a[10]; besitzt somit zwar zehn Komponenten, aber a[10] gibt es nicht!

Bereits in Abschnitt 4.2. wurden Zeichenketten vorgestellt. Auch dies sind Arrays. So deklariert und definiert in dem obigen Beispielprogramm arrays1.c die Zeile

char s[10]="Hello!";

eine Zeichenkette s mit zehn Speicherplätzen, wovon jedoch auch das Stringendezeichen ASCII-0 einen Platz belegt! s[0] ist hier 'H' usw.

Noch einmal sei daran erinnert, daß in der Headerdatei string.h eine ganze Reihe von Funktionen für Strings deklariert sind. Sehen Sie sich unter UNIX doch einmal die Datei /usr/include/string.h bzw. bei einem PC-Compiler die Datei string.h im entsprechenden INCLUDE-Verzeichnis an!

Und noch etwas: auch wenn die Deklaration char *s2; etwas anderes als ein Array, nämlich einen Zeiger, beschreibt, so kann s2 dennoch syntaktisch genauso wie das obige Array s verwendet werden: hier kann also ebenfalls mit s2[0] auf das erste Element von s2 zugegriffen werden, sofern ein solches existiert!

Mehrdimensionale Arrays

Selbstverständlich können Arraystrukturen auch geschachtelt werden. Ein zweidimensionales Array von int-Werten kann z.B. deklariert werden durch

int Matrix[10][20];

Hiermit steht eine Datenstruktur mit 10*20 int-Speicherplätzen zur Verfügung; anschaulich kann auf die Elemente in den 10 Zeilen und 20 Spalten zugegriffen werden wie folgt.

Matrix[0][0]=1; /* das allererste Element */
Matrix[9][19]=200; /* das allerletzte Element */
Matrix[9][0]=181; /* das erste Element der letzten Zeile */

Bemerkung: Wie zuvor ist auch hier (natürlich) der Name Matrix bereits die Adresse des Feldes!

Strukturen (struct)

Was in Pascal die Records sind, heißt bei C struct (Struktur oder Verbund). Diese werden z.B. für die Arbeit mit Datenbanken benötigt; nachstehend sehen wir uns ein kleines Beispiel mit einem struct an. Spätestens hier wird im übrigen die Verwendung von typedef sehr sinnvoll!

/* structs1.c */
#include <stdio.h>
#define STRLEN 128
struct Personal
{
   char Nachname[STRLEN];
   char Vorname[STRLEN];
   int PersonalNr;
   float Gehalt;
} personal1;
typedef struct Einsatz
{
   char Name[STRLEN];
   char Fach[STRLEN];
   int Stunden;
} EINSATZ;
void main(void)
{
   struct Personal personal2;
   EINSATZ einsatz;
   strcpy(personal1.Nachname,"Müller");
   strcpy(personal1.Vorname,"Alfons");
   personal1.PersonalNr=111;
   personal1.Gehalt=999.99;
   personal2=personal1;
} /* end main */

In diesem Beispiel wird ein Datentyp "struct Personal" vereinbart mit den Komponenten Nachname, Vorname, PersonalNr und Gehalt der entsprechend genannten Datentypen. Damit ist noch kein Speicherplatz allokiert worden. Dies geschieht erst durch das Anfügen des Bezeichners "personal1" hinter die Strukturdefinition! personal1 ist hier also eine (externe) global gültige Variable mit den genannten Komponenten. Der Zugriff, wie weiter unten in dem Beispielprogramm structs1.c zu sehen, geschieht (wie bei Pascal) durch den Punktoperator: personal1.Nachname greift auf die Komponente Nachname der Variablen personal1 zu.

Wie im Hauptprogramm zu sehen ist, muß allerdings bei jeder neuen Deklaration einer Variablen von diesem Strukturtyp auch das Wort struct mitgeschrieben werden, was zumindest unbequem ist. Die Typendefinition EINSATZ im obigen Beispiel zeigt, wie es angenehmer geht: innerhalb der typedef-Klausel wird die Struktur Einsatz deklariert und dieser dann der Synonymname EINSATZ (in Großbuchstaben) gegeben. Eine Variable einsatz kann dann wie oben im Hauptprogramm einfach durch

EINSATZ einsatz;

deklariert werden.

Natürlich können Strukturen (wie alle anderen höheren Datentypen) auch geschachtelt werden. (Ha, da kommt Freude auf!)
Legen wir die Deklarationen aus dem obigen Beispiel structs1.c zugrunde, so kann mit

struct STRUKTUR
{
struct Personal p;
EINSATZ e;
} struktur;

eine Variable struktur vom Typ struct STRUKTUR vereinbart werden; korrekt sind dann die Zugriffe struktur.p.PersonalNr oder struktur.e.Stunden.

Anmerkung: Auf den Strukturen von C baut C++ mit seiner Klassenbildung auf. Eine Klasse (class) in C++ ist im wesentlichen eine C-Struktur mit der wichtigen gegenüber C neuen Eigenschaft, daß nun auch Funktionen (die sogenannten Methoden) mit in die Struktur aufgenommen werden (können).

Variante Strukturen (union)

Während beim normalen struct alle Komponenten im Speicher hintereinander liegen, gestattet der Datentyp union das umzusetzen, was in Pascal variante Records sind: mehrere Komponenten liegen übereinander, so daß ein- und derselbe Platz für verschiedenartige Daten genutzt werden kann. Die Syntax ist ansonsten wie bei den (festen) structs.

Auch hier am besten ein kleines Beispiel.

/* unions1.c */
#include <stdio.h>
#include <string.h>
union U2
{
   unsigned char s[10];
   unsigned int i;
} u2;
void main(void)
{
   printf("sizeof(struct U2): %d\n",sizeof(union U2)); /* ==10 */
   printf("Sizeof(struct u2): %d\n",sizeof(u2));
   u2.i=123;
   strcpy(u2.s,"A");
/* Nun ist (auf dem PC!) u2.i==65 (ASCII-Code) */
} /* end main */

Verwendet werden solche varianten Strukturen (unions) z.B. dann, wenn in einem Datenbestand sich ausschließende verschiedenartige Ausprägungen auftreten können und man die jeweils auf gleichviel Speicherplatz unterbringen möchte.

Beispiel: Im PC-Bereich werden unions beispielsweise eingesetzt für die Arbeit mit den Registern, die einmal byteweise, ein anderes Mal in Worteinheiten angesprochen werden müssen[22]. Der Turbo C Compiler von Borland deklariert[23] deshalb die folgenden Strukturen und die Union REGS:

struct WORDREGS /* wortweise adressierte Register */
{
unsigned int ax, bx, cx, dx, si, di, cflag, flags;
};

struct BYTEREGS /* byteweise adressierte Halbregister */
{
   unsigned char al, ah, bl, bh, cl, ch, dl, dh;
};
union REGS /* die variable Verbindung dieser beiden */
{
   struct WORDREGS x;
   struct BYTEREGS h;
};

Wird nun eine Variable

union REGS reg;

deklariert, so kann mit

reg.x.ax = 0xFF00;
/* höherwertiges Byte auf 0xFF setzen, niedrigerwertiges Byte auf 0x00 */

dafür gesorgt werden, daß reg.h.ah den Wert 0xFF, reg.h.al den Wert 0x00 erhält.

Bit-Felder

Ein Spezialfall einer Struktur sind die sogenannten Bit-Felder (bit fields)24. Hier wird in einer Strukturvereinbarung durch einen Doppelpunkt getrennt jeweils vorgeschrieben, wieviele Bits eine Komponente umfassen soll. Dieser Ansatz eignet sich insbesondere gut zur Verwaltung von Flags, logischen Merkern.

Wichtig: Fast alle Aspekte von Bit-Feldern sind implementierungsabhängig; es gibt keine Arrays von Bit-Feldern, und Bit-Felder haben keine Adressen, so daß der Adreßoperator auf sie nicht angewendet werden kann.

Auch hier wieder ein Beispiel. (Mit den beiden #define-Direktiven werden die symbolischen Konstanten TRUE und FALSE auf 1 und 0 gesetzt, wobei allerdings dem konkreten Rechner überlassen bleibt, in welcher internen Darstellung und Speicherbreite 1 und 0 ermittelt werden, daher die etwas eigenwillige Deklaration (1==1), die eben immer TRUE ist!)

/* bitfields.c */
#include <stdio.h>
#define TRUE (1==1)
#define FALSE (0==1)
void main(void)
{
   struct Bitfields /* diese Deklaration darf auch lokal stehen */
   {
      unsigned int optionA : 1;
      unsigned int optionB : 1;
      unsigned int optionC : 1;
   } bf;
   printf("\nstruct Bitfields benötigt %d Bytes.\n",sizeof(struct Bitfields));
   bf.optionA=TRUE;
   bf.optionB=TRUE;
   bf.optionC=FALSE;
   printf("Option A ist%s aktiv.\n",(bf.optionA ? "" : " nicht"));
   printf("Option B ist%s aktiv.\n",(bf.optionB ? "" : " nicht"));
   printf("Option C ist%s aktiv.\n",(bf.optionC ? "" : " nicht"));
   bf.optionA = ~bf.optionA; /* Erinnern Sie sich noch an ~? */
   printf("\nNach \"bf.optionA = ~bf.optionA;:\n");
   printf("Option A ist%s aktiv.\n",(bf.optionA ? "" : " nicht"));
   printf("Option B ist%s aktiv.\n",(bf.optionB ? "" : " nicht"));
   printf("Option C ist%s aktiv.\n",(bf.optionC ? "" : " nicht"));
} /* end main */

Ablauflisting:

struct Bitfields benötigt 4 Bytes.
Option A ist aktiv.
Option B ist aktiv.
Option C ist nicht aktiv.

Nach "bf.optionA = ~bf.optionA;":
Option A ist nicht aktiv.
Option B ist aktiv.
Option C ist nicht aktiv.

Es wird lokal in main() ein Bitfeld bzw. eine Struktur Bitfields deklariert, die über drei 1-Bit-Komponenten (optionA, optionB, optionC) verfügt. Wie das Programm zeigt, wird rechnerabhängig für die Bitfeld-Variable bf die nächsthöhere ganzzahlige Grundeinheit genommen, hier 4 Bytes auf einer UNIX-Anlage. Auf einem PC werden in dieser Situation üblicherweise zwei Bytes verwendet.

Pointer

Ein Pointer (Zeiger) ist ein Datentyp, bei dem Speicheradressen verwaltet werden. Eine Variable von einem Pointertyp kann also jeweils eine konkrete Speicheradresse (z.B. von einer anderen statischen Variablen) beinhalten. Pointer sind insoweit dynamisch, als sie mit ihrer Deklaration zunächst keinen weiteren Speicherplatz zugewiesen bekommen als den für die eigentliche Adresse.

Die allgemeine Syntax der Pointerdeklaration hat die Grundform:

<datentyp> * <bezeichner>;

Und wieder ist es wie bei Pascal: die Pointervariable selbst gehorcht den üblichen Gültigkeitsregeln wie alle anderen Variablen auch; der Speicherplatz, auf den sie aktuell zeigt, kann sich jedoch irgendwo im Arbeitsspeicher befinden!

Beispiele:

int *pi; /* pi ist ein Zeiger auf einen int-Speicherplatz */
char *s; /* s ist ein Zeiger auf einen char-Speicherplatz, */
/* damit jedoch bereits als Zeichenkette nutzbar! */
float *pf; /* pf ist entsprechend ein Pointer auf float */

Sehen wir uns ein erstes kleines Beispielprogramm an, in dem auch von der sogenannten Pointerarithmetik Gebrauch gemacht wird: zu einem Pointer dürfen wir int-Werte addieren (und von ihm in den definierten Grenzen auch abziehen), auf eigenes Risiko, denn wir müssen dabei selbst aufpassen, daß wir in erlaubtem Speicherbereich bleiben.[25]

/* pointer1.c */
#include <stdio.h>
void main(void)
{
  int dummy=99, i;
  char *s, c;
  int *pi, a[3]={ 100,110,120 } ; /*Initialisierung eines Arrays*/
  printf("1.Teil:\n");
  c='A';
  s=&c; /* & ist der Adreßoperator */
  printf("s=%s s=%d &c=%d\n",s,s,&c);
  printf("2.Teil:\n");
  pi=a;
  printf("pi=%d *pi=%d *pi+1=%d *(pi+1)=%d\n",pi,*pi,*pi+1,*(pi+1));
  pi++;
  printf("pi=%d *pi=%d *pi+1=%d *(pi+1)=%d\n",pi,*pi,*pi+1,*(pi+1));
  printf("3.Teil:\n");
  i=100;
  pi=&i;
  printf("pi=%d *pi=%d *pi+1=%d *(pi+1)=%d\n",pi,*pi,*pi+1,*(pi+1));
} /* end main */

Ablauflisting:

1.Teil:
s=A$Æ s=3678 &c=3678

2.Teil:
pi=3670 *pi=100 *pi+1=101 *(pi+1)=110
pi=3672 *pi=110 *pi+1=111 *(pi+1)=120

3.Teil:
pi=3682 *pi=100 *pi+1=101 *(pi+1)=99

Zu dem Programm im einzelnen: Zunächst werden zwei int-Variablen deklariert (dummy und i), wobei dummy bereits auf 99 initialisiert wird. Dann werden ein Zeiger auf char (s) und ein char (c), ein Zeiger auf int (pi) und ein int-Array mit drei Komponenten (a) vereinbart. Das Array erhält bereits bei der Deklaration seine Startwerte: a[0]=100, a[1]=110 und a[2]=120.

Im 1.Teil wird dann c auf 'A' und der Zeiger s auf die Adresse von c (&c) gesetzt. Im Ablauflisting sehen wir denn auch, daß s[0] nun den Wert 'A' besitzt, die Adressen s und &c sind auch tatsächlich gleich (hier: 3678). Allerdings sehen wir bei der Textausgabe von s, daß nach dem 'A' noch aller möglicher Schrott (hier beispielhaft: $Æ) folgt: das liegt daran, daß Zeichenketten soweit reichen, bis '\0' gefunden wird!

Im 2.Teil wird der Zeiger pi auf a, d.h. die Adresse des Arrays gesetzt. (Wir erinnern uns: Arrays sind bereits die Adressen!) Im darauffolgenden printf werden der Reihe nach ausgegeben: der Inhalt des Pointers pi (3670), der Inhalt des Speicherplatzes (3670), auf den pi zeigt (*pi, hier: 100), dann dieser Wert plus 1 (101) und zuletzt der Inhalt des nächsten Speicherplatzes, hier 110, denn pi zeigte ja auf das erste Element des Arrays a, und 110 ist der zweite Array-Eintrag; pi+1 ist die oben erwähnte Pointerarithmetik. Anschließend wird mit pi++; pi einen Speicherplatz weitergesetzt, das ist nicht 1 Byte, sondern 1*sizeof(int)! Die printf()-Ausgabe der entsprechenden Werte erklärt sich fast selbst.

Im 3.Teil schließlich erhält i den Wert 100, pi wird auf die Adresse von i (hier ist das 3682) gesetzt. Und als Überraschung und Warnung gleichzeitig: mit (pi+1) stoßen wir diesmal auf einen Speicherplatz, der mit i natürlich nichts mehr zu tun hat: dort steht der Wert *(pi+1)=99, der zuvor auf die dummy-Variable zu Kontrollzwecken zugewiesen wurde. Sie sehen: C läßt uns eine ganze Menge Freiheit!

Pointer auf Pointer und Arrays von Pointern

Mit Pointern kann man viel machen. Man kann sie zum Beispiel auch iterieren (Zeiger auf Zeiger). Ebenso können Arrays von Pointern und Pointer auf Arrays deklariert werden und vieles mehr. Im folgenden sollen beispielhaft einige Aspekte diskutiert werden, die für die weitere Praxis der C-Programmierung relevant sein können.

Beispiel: Betrachten wir die folgenden Deklarationen.

char **bildschirm;
char (*bs2)[80];
char *bs3[80];
char *(bs4[80]);
char bs5[25][80];

Hier ist bildschirm ein Zeiger auf Zeiger von char; solch eine Variable könnte z.B. dazu benötigt werden, einen Bildschirminhalt zu verwalten, wobei die Bildschirmgröße zur Compilezeit nicht zwingend feststehen muß[26].

bs2 ist ein Pointer auf ein Array von 80 Zeichen; bs3 und bs4 sind identische Arrays von jeweils 80 Zeigern auf char; bs5 schließlich ist ein harmloses zweidimensionales Array von Zeichen.

Das nachfolgende Beispiel illustriert den nicht ganz trivialen Umgang mit Pointern auf Pointer am Beispiel einer Variablen bildschirm, die in der Praxis dazu dienen kann, den jeweiligen Bildschirminhalt zu verwalten.

Mit der Deklaration char** bildschirm; gibt es indes erst einen einzigen Speicherplatz! Dieser ist so groß, wie auf dem jeweiligen System Pointer(variablen), Adressen eben, sind. Um wie hier fünfundzwanzig Zeilen verwalten zu können, muß zunächst für diese fünfundzwanzig (Zeilen-)Pointer Speicherplatz allokiert werden, was mit der Bibliotheksroutine malloc() (memory allocate, Prototyp in stdlib.h) geschehen kann. Bei malloc() ist zum einen anzugeben, wieviel Speicherplatz benötigt wird, in der Regel geschieht dies mit der Verwendung von sizeof(), und zum anderen, für was für einen Typ der Pointer dienen soll. In unserem Fall benötigt bildschirm ZEILEN-mal die Größe eines Pointers auf char (das ist sizeof(char*)), und bildschirm selbst ist ein Zeiger auf Zeiger auf char. (Das steht ja auch in der Deklaration von bildschirm.) Dies sieht dann konkret so aus:

bildschirm=(char **)malloc(sizeof(char*)*ZEILEN);

Jetzt stehen ZEILEN-viele Pointer auf char zur Verfügung. Nun kann für jede Zeile Speicherplatz für einen solchen Pointer auf char allokiert werden, der SPALTEN-viele Zeichen aufnehmen muß; für i von 0 bis ZEILEN-1 ist also folgendes notwendig:

bildschirm[i]=(char *)malloc(sizeof(char)*SPALTEN);

Die so bereitgestellten Speicherplätze stehen solange im Programmlauf zur Verfügung, bis sie (oder Teile davon) mit free() wieder freigegeben werden. free() ist ebenfalls eine in stdlib.h deklarierte Bibliotheksfunktion.

/* pointer2.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SPALTEN 81 /* Diese Werte könnten auch dynamisch erst */
#define ZEILEN 25 /* festgelegt werden als Variablen! */
void main(void)
{
   char **bildschirm;
   int i,j;
   /* Speicherplatz für die ZEILEN viele Zeilenpointer wird geholt*/
   bildschirm=(char **)malloc(sizeof(char*)*ZEILEN);
   if (bildschirm==NULL) /* malloc() scheiterte! */
   {
      fprintf(stderr,"\nFehler bei malloc()!\n");
      exit(EXIT_FAILURE); /* einfachste Fehlerbehandlung! */
   }
   for (i=0; i<ZEILEN; i++)
   {
      /* Speicherplatz für jede Zeile wird geholt */
      bildschirm[i]=(char *)malloc(sizeof(char)*SPALTEN);
      if (bildschirm[i]==NULL) /* malloc() scheiterte! */
      {
         fprintf(stderr,"\nFehler bei malloc()!\n");
         exit(EXIT_FAILURE);
      }
      /* Sicherheitshalber eine sehr umsichtige Initialisierung */
      for (j=0;j<SPALTEN; j++)
         bildschirm[i][j]='?';
      /* Kopieren eines Mustertextes in die i-te Zeile */
      strcpy(bildschirm[i],"Mustertext");
   }
   /* Kontrollausgabe der Zeilen */
   for (i=0; i<ZEILEN; i++)
   {
      printf("\n%d->:%s:",i,bildschirm[i]);
      /*Alternative dazu: printf("\n%d->:%s:",i,*(bildschirm+i));*/
   }
   /* Und hier wird der gesamte Bildschirm zeichenweise ausgegeben*/
   printf("\n");
   for (i=0; i<ZEILEN; i++)
   {
      for (j=0; j<SPALTEN-1; j++)
      {
         putchar(bildschirm[i][j]);
      }
      putchar('\n');
   }
   /* Der Speicherplatz wird wieder freigegeben: zuerst der
      für alle Zeilen und danach der für die Zeilenpointer */
   for (i=0; i<ZEILEN; i++)
      free(bildschirm[i]);
   free(bildschirm);
} /* end main */
/* end of file pointer2.c */


Kommandozeilenargumente und Umgebungsinformation

ANSI C berücksichtigt die Tatsache, daß in der Praxis zum einen sehr häufig beim Aufruf Argumente direkt an das auszuführende Programm mitgegeben werden müssen, zum zweiten, daß Informationen aus dem Umgebungsbereich (environment) verarbeitet werden müssen. Auf die Möglichkeiten, die C hier bietet, soll in diesem Abschnitt eingegangen werden.

Kommandozeilenargumente: argc und argv

Bisher war die Deklaration der (Hauptprogramm-)Funktion main() stets

void main(void)

- das heißt: an main() wurde bisher nichts übergeben. Hier sieht C über die Deklaration

void main(int argc, char **argv)

oder, synonym dazu,

void main(int argc, char *argv[])

die Möglichkeit vor, eine Anzahl von Parametern (als Anzahl von Zeichenketten) beim Programmaufruf mitzugeben. Der Name argc steht dabei für den argument counter, den Zähler, argv (argument vector) ist das Array (oder der Zeiger) auf die argc vielen Zeichenketten, der sogenannte argument vector.

Wird das Programm argument beispielsweise aufgerufen in der Form

argument 1 2 3

so sind argc==4 (!), argv[0]=="argument\0", argv[1]=="1\0", argv[2]=="2\0" und nun erraten Sie es bereits argv[3]=="3\0".

Ein minimales Beispiel hierzu finden Sie im nächsten Unterabschnitt.

Umgebungsinformation: envp

Die Informationen über den Umgebungsbereich können gegebenenfalls über einen dritten Parameter bei main() abgerufen werden. Mit der Deklaration

void main(int argc, char **argv, char **envp)

oder, synonym dazu,

void main(int argc, char *argv[], char *envp[])

kann mit der Variablen envp (environment pointer) ein Zeiger auf den Umgebungsbereich angelegt werden. Dies funktioniert in entsprechender Weise bei den beiden Betriebssystemen UNIX und DOS.

Die Verwendung von envp (und der Parameter argc und argv) soll das kleine Beispiel envp.c illustrieren.

Nachstehend noch die Ablauflistings von envp bzw. ENVP.EXE einmal unter UNIX, einmal unter DOS.

Bildschirmprotokoll von envp.c unter UNIX:

Aufruf des Programms:
envp argument1 argument2

Ausgabe des Programms:

Argument[0]=[envp]
Argument[1]=[argument1]
Argument[2]=[argument2]

Umgebungsbereich:
1: "_=./envp"
2: "LANG=n-computer"
3: "NLSPATH=/usr/lib/nls/n-computer/%N.cat:/usr/lib/nls/C/%N.cat"
4: "PATH=/bin:/usr/bin:/usr/contrib/bin:/usr/local/bin:."
5: "COLUMNS=80"
6: "SHELL_LEVEL=1"
7: "EDITOR=/usr/contrib/bin/me"
8: "CCOPTS=-Aa"
9: "HISTFILE=/users/guest/.sh_history"
10: "LOGNAME=guest"
11: "MAIL=/usr/mail/guest"
12: "NETID=pcra24.bg.bib.de"
13: "SHELL=/bin/ksh"
14: "HISTORY=100"
15: "TMOUT=1200"
16: "HOME=/users/guest"
17: "FCEDIT=vi"
18: "TERM=vt220-ncsa"
19: "PWD=/tmp"
20: "TZ=MEZ-1MESZ"

Bildschirmprotokoll von envp.c unter DOS:

Aufruf des Programms:
envp argument1 argument2

Ausgabe des Programms:

Argument[0]=[D:\C\STUFF\ENVP.EXE]
Argument[1]=[argument1]
Argument[2]=[argument2]

Umgebungsbereich:
1: "COMSPEC=c:\4dos\4dos.com"
2: "TEMP=c:\tmp"
3: "OLDPATH=d:\bat;c:\bat;c:\dos;c:\sw\bib;c:\util;c:\novell\netware4;x:\util"
p> 4: "NETNAME=PCRA24"
5: "TMP=c:\tmp"
6: "LOGNAME=BAEUMLECOURTH"
7: "PLANPATH=H:\VERW\INFO\PLAN"
8: "WINPMT=[Windows aktiv $t$h$h$h] $p$g$s"
9: "PATH=c:\4dos;c:\dos;c:\sw\bib;c:\sw\util;c:\novell;c:\bat"

Rekursion und Rekursive Strukturen: Listen und Bäume

Wie in jeder anständigen Programmiersprache[27] ist Rekursion auch in C möglich, gleichermaßen direkte Rekursion wie indirekte. In den folgenden Abschnitten sollen mit zunehmendem Schwierigkeitsgrad drei Beispiele für Rekursion in C vorgestellt werden.

Rekursion

Das Grundprinzip der direkten Rekursion in C: eine Funktion f() ruft sich selbst auf, sofern nicht eine Abbruchbedingung (Rekursionsboden) erfüllt ist. Die indirekte Rekursion unterscheidet sich davon dadurch, daß zwei oder mehr Funktionen beteiligt sind, die sich wechselseitig aufrufen.

Zur einfachen, direkten Rekursion sei das beliebte Beispiel der Berechnung der Fakultät angeführt: zu einer Zahl n soll n!, das Produkt aller natürlichen Zahlen von 1 bis n, berechnet werden. Nachfolgend ein Programmlisting hierzu.

/* fakultaet.c */
#include <stdio.h>
long fakultaet(long);
void main(void)
{
   long n=5;
   printf("\nDie Fakultät von %ld ist %ld.\n",n,fakultaet(n));
} /* end main */
long fakultaet(long n)
{
   if (n>1)
      return n*fakultaet(n-1);
   return 1;
} /* end fakultaet */


Lineare Listen

Eine lineare Liste ist eine (dynamische) Datenstruktur, die, über Zeiger verknüpft, beliebig (endlich) viele Werte (eines gewissen Typs) z.B. geordnet verwalten kann. (Hier klicken für ein Bildchen.)

Wird in eine solche, aufsteigend geordnete, lineare Liste (von int-Werten beispielsweise) ein neuer Wert aufgenommen, so muß dafür ein neuer Speicherplatz mit malloc() allokiert und der entsprechende Eintrag an die passende Stelle der bisherigen Liste eingefügt werden; ist der neue Wert der größte, so muß der neue Speicherplatz einfach an das Ende der Liste angehängt werden. Ist die Liste (noch) leer, so bildet dieser Wert zusammen mit dem entsprechenden Zeiger die komplette lineare Liste.

Das nachstehende Programm linliste.c zeigt beispielhaft, wie auf der Kommandozeile eine Reihe von int-Werten mitgegeben werden, die zugehörige sortierte lineare Liste aufgebaut und dann wieder ausgegeben wird.

Anmerkungen zu dem Programm:

Die Datenstruktur struct LineareListe bzw. LISTE beinhaltet der Einfachheit halber nur die zwei Komponenten element (int) und next (LISTE*), einen Zeiger auf das nächste Listenelement. Dieser hat den Wert NULL[28], wenn kein Nachfolger in der Liste existiert, dies also der letzte Eintrag in der Liste ist.

Wie für gute Programme üblich: wird linliste ohne die korrekte Anzahl Parameter, hier also ohne Parameter, aufgerufen, so wird eine kurze Erläuterung ausgegeben, wie das Programm korrekt aufgerufen werden muß.

Das schöne Konstrukt while(*(++argv)) testet, ob *argv!=NULL ist, d.h. ob noch weitere Parameter im Argumentenvektor argv vorhanden sind; ++argv bewirkt, daß dieser Zeiger bei jedem Schleifendurchlauf weitergesetzt wird. Hier muß der Präfixoperator ++ genommen werden, damit nicht argv[0] (der Programmname selbst) mit verarbeitet wird.

atoi() ist eine Konvertierungsfunktion der Standardbibliothek: atoi steht für ascii-to-int und wandelt eine Zeichenkette um in einen int-Wert. (Dazu verwandte Funktion sind atof(), atod() usw.)

/* linliste.c */
#include <stdio.h>
#include <stdlib.h>
/* Struktur und deren Typ deklarieren */
typedef struct LineareListe
{
   int element;
   struct LineareListe *next;
} LISTE;
/* Prototypen */
void Einfuegen(LISTE **,int);
void Ausgeben(LISTE *);
/* Hauptprogramm */
int main(int argc, char **argv)
{
   LISTE *liste;
   liste=NULL;
/* Falls keine Parameter angegeben: kurze Erläuterung geben */
   if (argc==1)
   {
      fprintf(stderr,"\nAufruf: linliste intwert "
         "{ intwert ... }\n");
      return(EXIT_FAILURE);
   } /* end argc==1, d.h. keine Parameter */
   /* Die Werte werden in die lineare Liste eingefügt */
   while (*(++argv))
      Einfuegen(&liste,atoi(*argv));
   /* Die lineare Liste wird zur Kontrolle ausgegeben */
   Ausgeben(liste);
   /* An das Betriebssystem zurückgeben: alles ok! */
   return(EXIT_SUCCESS);
} /* end main */
/* Einfuegen von wert an passender Stelle (aufsteigend sortiert) */
void Einfuegen(LISTE **pliste,int wert)
{
   if (*pliste==NULL)
   {
      *pliste = (LISTE*)malloc(sizeof(LISTE));
      if (*pliste == NULL)
      {
         fprintf(stderr,"\nmalloc()-Aufruf schlug fehl!\n");
         exit(EXIT_FAILURE);
      } /* end if malloc() schlug fehl */
      (*pliste)->element=wert;
      (*pliste)->next=NULL;
   } /* end if *pliste==NULL */
   else if (wert < (*pliste)->element) /* hier einfügen */
   {
      LISTE *ptr2;
      int tmp;
      ptr2=(LISTE*)malloc(sizeof(LISTE));
      if (ptr2 == NULL)
      {
         fprintf(stderr,"\nmalloc()-Aufruf schlug fehl!\n");
         exit(EXIT_FAILURE);
      } /* end if malloc() schlug fehl */
      /* Auf dem neuen Platz wird der alte Listeneintrag */
      /* gespeichert - und dort wird der neue Wert eingetragen */
      tmp=(*pliste)->element;
      (*pliste)->element=wert;
      ptr2->element=tmp;
      /* Nun wird der neue Wert an der aktuellen Position einge-*/
      /* schoben, der Rest der Liste nach hinten gehängt. */
      ptr2->next=(*pliste)->next;
      (*pliste)->next=ptr2;
   } /* Position zum Einfügen gefunden */
   else
   {
      Einfuegen(&((*pliste)->next),wert);
   } /* rekursiver Zweig - weiter hinten anhängen oder einfügen*/
} /* end Einfuegen */
/* Einfache Ausgabe der linearen Liste */
void Ausgeben(LISTE *liste)
{
   if (liste==NULL)
      printf(" (Ende der Liste)\n");
   else
   {
      printf("%d ",liste->element);
      Ausgeben(liste->next);
   }
} /* end Ausgeben */
/* Ende der Datei linliste.c */

Aufruf des Programms:

linliste 3 7 1 6 4 2 5 8 9 10

Ausgabe des Programms:

1 2 3 4 5 6 7 8 9 10 (Ende der Liste)

Bäume

Lineare Listen sind ein Spezialfall von Bäumen. Ein Baum ist eine dynamische Datenstruktur mit einzelnen Elementen (Knoten) und einer Ordnung mit Vorgänger und Nachfolger, so daß jeder Knoten (außer dem ersten, der sogenannten Wurzel) genau einen (direkten) Vorgänger und endliche viele Nachfolger besitzt. Ist die Anzahl der Nachfolger auf 2 beschränkt, so spricht man von einem binären Baum. (Hier klicken für ein Bildchen.)

Das kleine Programm baum1.c illustriert exemplarisch die Deklaration von und den Umgang mit binären Bäumen: Die Funktion InsertTree() trägt einen neuen Wert (zwischen 1 und 99) so in den Baum ein, daß dieser gemäß der Inorder-Sortierung aufgebaut wird, das bedeutet, daß links unterhalb eines Knotens nur kleinere, rechts unterhalb eines jeden Knotens nur größere Werte abgespeichert werden. Vgl. hierzu das Ablauflisting zum folgenden Beispielprogramm.
Die Funktion DisplayTree() gibt den jeweiligen Baum mit einfacher Liniengraphik dargestellt auf den Bildschirm aus.
Die Funktion Deallocate() schließlich gibt den für den Baum per malloc() reservierten Speicherplatz wieder frei.
Sämtliche Funktionen sind der Natur der Sache angemessen rekursiv formuliert, denn der Datentyp TREE ist ebenfalls eine rekursive Struktur!

Ablauflisting:

Aktueller Inhalt des Baumes: (leer)
/** Listing hier gekürzt **/

Welche ganze Zahl im Bereich 1..99 soll aufgenommen werden?
[0=Ende] >66

Aktueller Inhalt des Baumes:
          +------------------45-------------------+
+--------23---------+                    +--------75---------+
12                 25                   66                  78---+
                                                                99
Welche ganze Zahl im Bereich 1..99 soll aufgenommen werden?
[0=Ende] >77
Aktueller Inhalt des Baumes:
          +------------------45-------------------+
+--------23---------+                    +--------75---------+
12                 25                   66              +---78---+
                                                       77        99
Welche ganze Zahl im Bereich 1..99 soll aufgenommen werden?
[0=Ende] >0