Unterabschnitte


8.8 Felder und Strings


8.8.1 Felder

Muß man sich nicht einzelne Werte merken, sondern gleich mehrere gleichartige, dann bieten sich Felder an. Ein Feld wird oft auch array oder Vektor genannt.

Die Vereinbarung dafür ist auch nicht schwerer als für eine normale Variable:

int f[10] vereinbart ein Feld mit dem Namen f, welches 10 Elemente enthält. Jedes der 10 Elemente hat den Typ int.

In C kann man leider nicht die obere und untere Feldgrenze frei vorgeben wie in Pascal oder FORTRAN; in C hat das erste Feldelement immer den Index 0. f hat in unserem Beispiel 10 Elemente mit den Indices 0 bis 9. Man kann also mit dieser Deklaration auf die Elemente f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8] und f[9] zugreifen. Auf das Element f[10] zuzugreifen ist schon nicht mehr zulässig, da nur 10 Elemente vereinbart sind.

Eine andere untere Feldgrenze als 0 ist in C nicht direkt möglich; ein möglicher Ausweg wird in Vereinbarung von freien Feldgrenzen gezeigt.

Mehrdimensionale Felder sind analog mit mehreren Dimensionsangaben möglich:

int ff[2][3] vereinbart ein Feld mit sechs Elementen: ff[0][0], ff[0][1], ff[0][2], ff[1][0], ff[1][1] und ff[1][2].

Die Elemente stehen auch in der angegebenen Reihenfolge im Speicher, wenn das Programm ausgeführt wird. Dies ist die gleiche Reihenfolge wie in Pascal, aber anders als in FORTRAN.

Durchläuft man ein Feld in der abgespeicherten Reihenfolge, ändert sich also der letzte Index am schnellsten.

Das Wissen um die Reihenfolge der Elemente im Speicher ist zum einen wichtig, wenn man einzelne Elemente nicht nur über die Indices ansprechen will, sondern eine kreativere Adressierung mit Zeigerarithmetik benötigt.

Zum anderen kann es sich bei großen Feldern deutlich auf das Laufzeitverhalten auswirken: Durchläuft man ein mehrdimensionales Feld so, daß sich der letzte Index am schnellsten ändert, und der erste am seltensten, dann verwendet man aufeinanderfolgende Elemente. Dies wird auf modernen Systemen wesentlich schneller sein, als in großen Schritten mehrfach durch das Feld zu springen indem man den ersten Index am schnellsten ändert (erstens, weil der Prozessor durch caching und prefetching auf aufeinanderfolgende Elemente im Schnitt schneller zugreifen kann, und zweitens weil das Betriebssystem weniger Seiten ein- und auslagern muß, wenn man bevorzugt auf benachbarte Daten zugreift).

Damit können wir das Quadratprogramm aus Kleine Beispiele so ändern, daß zuerst alle Quadrate berechnet und in einem Feld gespeichert werden. Erst zum Schluß sollen alle berechneten Werte ausgegeben werden.

/* feq.c 31. 7.95 kw
 *
 * Dieses Programm gibt die Zahlen von 1 bis 10 und die Quadrate aus.
 */

#define IMAX        10

int printf( char *format, ... );   /* gibt format und eventuell
                                    * weitere Werte auf die Standard-
                                    * ausgabe.
                                    */

int main()
{
  int      i;                /* eine ganzzahlige Variable als
                              * Schleifenzaehler vereinbaren.
                              */
  int      imalifeld[IMAX];  /* Feld fuer die Quadrate.
                              */


  printf( "Dieses Programm gibt die ersten %d Quadratzahlen",
          IMAX
          );
  printf( " auf dem Bildschirm aus.\n\n" );

  /* Schleife ueber alle zu berechnenden Zahlen:
   */
  for( i=1; i<=IMAX; i=i+1 )
  {
    imalifeld[i-1] = i*i;
  }

  /* Schleife ueber alle auszugebenden Zahlen:
   */
  for( i=1; i<=IMAX; i=i+1 )
  {
    printf( "Das Quadrat von %d ist %d.\n",
            i, imalifeld[i-1] );
  }

  return 0;                  /* und den Wert 0 zurueckgeben.
                              */
}

Die Verwendung von i-1 als Index anstatt i ist deshalb nötig, weil i den Bereich von 1 bis 10 durchläuft, während im Feld imalifeld die Elemente von 0 bis 9 numeriert sind.

Ein Beispiel für ein mehrdimensionales Feld ist eine andere Version des Potenzenprogramms aus Kleine Beispiele, das analog zum letzten arbeitet. Zuerst werden alle Potenzen berechnet und in einem zweidimensionalen Feld gespeichert. Dann werden alle Werte ausgegeben:

/* feqp.c 31 .7.95 kw
 */

/* Dieses Programm gibt die Zahlen von 1 bis 10 und die ersten 4

* Potenzen derselben aus.
                                            */

#define IMAX        10
#define PMAX        4

int printf( char *format, ... );   /* gibt format und eventuell
                                   * weitere Werte auf die Standard-
                                   * ausgabe.
                                   */

/* Diese Funktion liefert zu einem Wert die n-te Potenz:
 * n muss dabei groesser oder gleich 0 sein!
 */

long int potenz( long wert, int n )
{
  long    ergebnis;
  int     i;

  ergebnis = 1;
  for( i=1; i<=n; i=i+1 )
    ergebnis = ergebnis * wert;

  return ergebnis;
}

int main()
{
  int      i, p;  /* Schleifenzaehler fuer Zahl und Potenz
                   */

  /* In potenzfeld[i-1][p] soll die p-te Potenz zur Zahl i
   * stehen. Die erste Potenz einer Zahl ist die Zahl selbst,
   * die nullte Potenz einer Zahl ist immer 1!
   */
  long int potenzfeld[IMAX][PMAX+1];

  printf( "Dieses Programm gibt die ersten %d Potenzen zu den",
          PMAX
          );
  printf( " ersten %d Zahlen aus.\n\n",
          IMAX
          );

  /* Schleife ueber alle zu potenzierenden Zahlen:
   */
  for( i=1; i<=IMAX; i=i+1 )
  {
    /* Schleife ueber alle Potenzen zu einer Zahl:
     */
    for( p=0; p<=PMAX; p=p+1 )
    {
      /* die p-te Potenz zur Zahl i berechnen:
       */
      potenzfeld[i-1][p] = potenz( i, p );
    }
  }

  /* Schleife ueber alle auszugebenden Zahlen:
   */
  for( i=1; i<=IMAX; i=i+1 )
  {
    /* Schleife ueber alle Potenzen zu einer Zahl:
     */
    for( p=0; p<=PMAX; p=p+1 )
    {
      /* die p-te Potenz zur Zahl i ausgeben:
       */
      printf( "%2d^%-2d=%7ld;", i, p, potenzfeld[i-1][p] );
    }
    /* Nach den Potenzen zu einer Zahl ein Zeilenvorschub:
     */
    printf( "\n" );
  }

  return 0;                  /* und den Wert 0 zurueckgeben.
                              */
}

Felder kann man von fast allen Datentypen bilden16, also auch von zusammengesetzten Datentypen (Felder, Strukturen, unions) oder Zeigern.

Ein mehrdimensionales Feld (siehe oben) kann auch als Feld von Feldern betrachtet werden:

int f[2][3];
vereinbart ein Feld mit 2 Elementen; jedes dieser Elemente ist ein Feld mit 3 Elementen vom Typ int.

8.8.1.1 Die Übergabe von Feldern an Funktionen

erfolgt anders als bei anderen Variablen.

Da Felder sehr groß werden können, wird automatisch statt des Feldes ein Zeiger auf das erste Feldelement übergeben.

Konsequenterweise wird in einer Funktion jeder als Feld deklarierte Parameter als Zeiger auf ein Feldelement behandelt. Dies fällt in der Regel nicht unangenehm auf. Zu beachten ist aber, daß dadurch Veränderungen an Feldelementen sich bis zum Aufrufer auswirken, was bei anderen Parametern durch den call by value-Aufruf nicht der Fall ist.

Zeiger auf Felder und normal vereinbarte Feldnamen können in den meisten Fällen identisch verwendet werden. Es gibt aber doch manchmal Ausnahmen!

Beispiel:

#include <stdlib.h>

int main( int nargs, char **args )
{
    int     feld1[10];
    int    *feld2;

    feld2 = malloc( 10*sizeof(int) );
    if( feld2 )
        exit( EXIT_FAILURE );
        /* ... */
}

Nur zur Sicherheit: Bei Strukturen (struct) wird nicht -wie bei Feldern- nur die Anfangsadresse übergeben, sondern ebenso wie bei einfachen Variablen eine vollständige Kopie (call by value), auch wenn in der Struktur möglicherweise wiederum Felder enthalten sein können!

In diesem Programm werden zwei Felder angelegt: feld1 ist als Variable vom Typ ,,Feld mit 10 int`` definiert; feld1 dagegen als ,,Zeiger auf int`` und erhält den Platz für das eigentliche Feld erst durch einen Aufruf von malloc() (siehe auch Freier Speicher).

In beiden Fällen kann man die einzelnen Feldelemente sowohl als feld1[0] (bzw. feld2[0]) bis feld...[9] ansprechen, als auch mit *(feld1) bzw. *(feld2) bis *(feld1+9) bzw. *(feld2+9).

Ebenfalls gleichwertig sind die beiden Formen bei der Übergabe an eine Funktion. Sowohl der Ausdruck feld1 als auch feld2 liefert in einer Parameterliste oder Zuweisung die Adresse des jeweils ersten Feldelementes.

Nun zu den Unterschieden:

8.8.1.2 Felder mit 0 Elementen:

Man glaubt ja nicht, was es alles gibt. Kann man auch ein Feld ohne Elemente (also mit der Länge 0) vereinbaren?

Laut ANSI-Standard muß das nicht möglich sein; mit einigen Compilern geht es (GNU-C, Visual C++), mit anderen nicht (Borland).

Für sich genommen, macht es sicher auch keinen Sinn.

Eine mögliche Verwendung wäre eine struct, an deren Ende (gegebenenfalls nach weiteren Nutzdaten) ein solches leeres Feld vereinbart ist.

Wenn man sich jetzt darauf verläßt, daß die Elemente einer struct in der Reihenfolge im Speicher liegen, in der sie bei der Deklaration angegeben sind (alle mir bekannten Compiler machen das tatsächlich), dann kann über das (für den Compiler eigentlich leere) Feld auf den Speicher direkt hinter der struct zugreifen; schon das nullte Feldelement liegt hinter der struct.

Das ist natürlich kriminell, solange man nicht dafür sorgt, daß einem der Speicher hinter der struct auch gehört.

Deshalb ist es sinnlos, von einer solchen struct direkt eine Variable zu vereinbaren; man hat ja keine Kontrolle darüber, was dahinter kommt.

Aber wenn man den Speicher für eine solche struct mit malloc() beschafft, steht es einem frei, als zu allokierende Größe etwas mehr anzugeben, und dementsprechend viele Elemente des Feldes verwenden zu können. Man muß natürlich wissen, wieviele Elemente allokiert wurden, um den Extraplatz nicht zu überschreiten. Dafür wird in solchen Fällen üblicherweise vor dem Feld noch eine ganzzahlige Variable (size_t oder ähnliches) vereinbart, in der die entsprechende Anzahl hinterlegt ist:

#include <stddef.h>
#include <stdlib.h>

/* Datentyp, der ein mehr oder weniger langes Feld enthält:
 */
typedef struct
{
  /* hier vielleicht noch mehr Nutzdaten... */

  /* zusätzliches Feld; wieviele Elemente ist noch nicht bekannt; */
  /* das Feld muß ganz am Ende stehen!                            */
  size_t     anzahl;    /* Anzahl der Feldelemente                */
  int        feld[0];   /* Feldelemente                           */
}
MehrOderWeniger;

/* FuelleFeld() kann auf die Elemente des Feldes zugreifen, wenn der
 * Compiler mitspielt:
 * (1) muß der Compiler akzeptieren, daß in der obigen struct
 *     0 Elemente stehen
 * (2) muß der Compiler das leere Feld auch wirklich an das
 *     Ende der struct legen (meines Wissens machen das alle)
 */
void FuelleFeld( MehrOderWeniger *zuFuellen_p, int wert )
{
  size_t    i;
  for( i=0; i<zuFuellen_p->anzahl; i++ )
  {
    zuFuellen_p->feld[i] = wert;
  }
}

int main( int nargs, char **args )
{
  /* zwei Felder allokieren, und die Anzahl der zusätzlichen Elemente
   * eintragen:
   */
  MehrOderWeniger *p_mehr
    = (MehrOderWeniger*)malloc( sizeof(MehrOderWeniger) + 100*sizeof(int) );
  p_mehr->anzahl = 100;

  MehrOderWeniger *p_weniger
    = (MehrOderWeniger*)malloc( sizeof(MehrOderWeniger) + 5*sizeof(int) );
  p_weniger->anzahl = 5;

  /* Felder initialisieren:
   */
  FuelleFeld( p_mehr, 123 );
  FuelleFeld( p_weniger, 456 );

  return 0;
} // main( int nargs, char **args )

Beim Allokieren des Speichers wird angegeben, wieviele Elemente im Feld enthalten sein sollen, und diese Anzahl wird in der struct vermerkt.

Vereinbart man eine Variable eines solchen Typs direkt, hat man keine (legale) Möglichkeit, Platz für Feldelemente zu reservieren. Deshalb ist eine solche Spielerei auf frei allokierten Speicher beschränkt.

In Anbetracht der Tatsache, daß ein solches Programm kaum noch als portabel bezeichnet werden kann, sollte man nach Möglichkeit die Finger davon lassen. Allerdings ist es halt manchmal doch recht verlockend...; beispielsweise gibt es in der Win32-API (Schnittstelle zu Windows) mehrere solcher structs.


8.8.2 Strings

In Feldern vom Typ char kann man Strings, also Zeichenketten, speichern.

Strings sind in C normalerweise Folgen von Zeichen (jeweils vom Typ char, je ein Byte lang). Das Ende eines Strings ist mit einem Nullbyte, also dem Zeichen ´\, markiert. Ein String mit 5 nutzbaren Zeichen benötigt demnach ein 6 Elemente großes char-Feld.

Schreibt man eine Zeichenkette mit doppelten Anführungszeichen in einem C-Programm (beispielsweise ¨abc, die Katze liegt im Schnee¨), dann wird an die eigentlichen Zeichen immer automatisch eine Null angehängt; siehe dazu Konstanten.

Diese Art von Zeichenketten heißt oft null terminated string; zur Arbeit damit existieren in der Standardbibliothek viele Funktionen:

strcpy() zum Kopieren, strcat() zum Aneinanderhängen von 2 Strings, strchr() um ein Zeichen in einem String zu finden, strstr() um eine Zeichenfolge in einem String zu finden, und andere.

Alle diese Funktionen verlassen sich darauf, daß die Zeichenketten mit dem Nullbyte abgeschlossen sind.

Wie in Felder beschrieben, wird bei der Übergabe eines Feldes an eine Funktion tatsächlich stattdessen ein Zeiger auf das erste Element des Feldes übergeben.

strcpy() und strcat() erhalten zwei Zeiger vom Typ char *, und liefern selbst einen Zeiger vom gleichen Typ zurück. Die Vereinbarung sieht also so aus:

char *strcpy( char *wohin, char *woher );
char *strcat( char *woran, char *woher );

und ist in der Datei string.h definiert.

Beispiel:

/* stgut.c 31. 7.95 kw
 */

/* Die Praeprozessordirektive #include dient dazu,
 * die Datei string.h hier einzufuegen.
 * In string.h sind unter anderem die Funktionen strcpy()
 * und strcat() vereinbart.
 * Stattdessen haette man die Funktionen auch hier direkt
 * vereinbaren koennen:
 */

#include <string.h>

/* Dann vereinbaren wir etwas Platz fuer einen String:
 */

char     s[20];

main()
{
  /* In der naechsten Zeile wird eine Stringkonstante
   * mit den 5 Zeichen 'S', 'O', 'S', ' ' und '\0'
   * erzeugt. Dann werden an die Funktion strcpy zwei
   * Zeiger vom Typ char * uebergeben, naemlich ein
   * Zeiger auf das erste Byte in s und ein Zeiger auf
   * das erste Byte in der Stringkonstanten, also auf
   * das 'S'.
   * Die Funktion kopiert alle 5 Zeichen aus der
   * Konstanten einschliesslich der abschliessenden '0'
   * an den Anfang des Feldes s. In s stehen danach
   * die 5 Zeichen und weitere 15, die keiner so genau
   * kennt.
   * Der Rueckgabewert von strcpy ist wieder ein Zeiger
   * auf das erste Byte der Kopie, also auf s[0]. Der
   * Rueckgabewert wird hier aber nicht verwendet.
     */
  strcpy( s, "SOS " );

  /* Nun werden analog zur letzten Anweisung die 5
     * Zeichen 'D', 'a', 's', ' ' und '\0' an den
     * Anfang von s kopiert. Die ersten 5 Zeichen, die
     * schon dort stehen, werden damit ueberschrieben und
     * sind verloren.
     */
  strcpy( s, "Das " );

  /* Mit strcat() werden an den bisherigen String in s
     * die Zeichen "ist g" und eine abschliessende '\0'
     * angehaengt, so dass sich ein zusammenhaengender
     * String "Das ist g" mit einer abschliessenden '\0'
     * ergibt.
     * Die bisherige abschliessende '\0'in c[4] wird
     * dabei von dem 'i' aus "ist g" ueberschrieben.
     */
  strcat( s, "ist g" );

  /* An den bisherigen String "Das ist g" werden analog
     * noch die Zeichen 'u', 't', '!', das Zeilen-
     * vorschubzeichen '\n' und eine abschliessende '\0'
     * angehaengt.
     */
  strcat( s, "ut!\n" );

  /* s enthaelt nun den String "Das ist gut!\n" mit der
     * beruehmten '\0' am Ende sowie 6 weitere,
     * ungenutzte Zeichen, deren Inhalt keiner weiss.
     * Der String wird mit printf() jetzt bis zur '\0'
     * ausgegeben; die uebrigen 6 Zeichen stoeren dabei
     * nicht, weil printf() bei der ersten '\0' im
     * auszugebenden String abbricht.
     */
  printf( s );
}

Achtung! In C werden standardmäßig keine Feldgrenzenüberprüfungen vorgenommen.

Deshalb werden auch Stringoperationen (z.B. mit strcpy()) ohne jedeKontrolle der Länge durchgeführt!

AnyWare@Wachtler.de