Unterabschnitte


8.7 struct und union


8.7.1 Strukturen

Sinn von Strukturen ist es, logisch zusammengehörende Informationen auch von der Programmierung her zusammenzufassen.

Mit dem Schlüsselwort struct kann man Datentypen schaffen, die es bislang nicht gibt und die aus einfacheren Typen zusammengesetzt sind.

Die Definition solcher Strukturen entspricht der Vereinbarung von RECORDs in Pascal oder Modula2.

Einfache Variablen kann man etwa so vereinbaren:

int     i, j;
Entsprechend werden Strukturen vereinbart:
struct
{
    int
        anzahl,
        farbe;
    double
        wert;
}
a, b;
deklariert 2 Variablen mit den Namen a und b. Jede dieser beiden Variablen enthält zwei ganze Zahlen (anzahl, farbe) und eine doppelt genaue Gleitkommazahl (wert).

Die einzelnen Elemente kann man mit dem Variablennamen, einem Punkt (.) und dem Elementnamen ansprechen:

    a.anzahl = 5;
    a.farbe  = 0;
    a.wert   = 3.14159265;
    b = a;
    printf( "b = %d, %d, %f\n", b.anzahl, b.farbe, b.wert );

Braucht man eine bestimmte Struktur an mehreren Stellen, beispielsweise zur Vereinbarung eines Funktionsparameters, dann kann man auch die Struktur mit einem Strukturnamen (hier s) versehen und für spätere Vereinbarungen nutzen:

struct s
{
    int
        anzahl,
        farbe;
    double
        wert;
};

struct s  a, b;

    /* ... */
    a.anzahl = 5;
    a.farbe  = 0;
    a.wert   = 3.14159265;
    b = a;
    printf( "b = %d, %d, %f\n", a.anzahl, a.farbe, a.wert );

Mischformen sind auch möglich. Man darf Variablen einer bestimmten Struktur vereinbaren und gleichzeitig einen Strukturnamen vergeben, der später verwendet werden kann:

struct s
{
    int
        anzahl,
        farbe;
    double
        wert;
}  a, b;

print_struct_s( struct s parameter )
{
    printf( "%d, %d, %f",
            parameter.anzahl,
            parameter.farbe,
            parameter.wert
        );
}

main()
{
    a.anzahl = 5;
    a.farbe  = 0;
    a.wert   = 3.14159265;
    b = a;
    printf( "b = " );
    print_struct_s( b );
    printf( "\n" );
}

Als Einträge in Strukturen sind alle bis dahin14bekannten Datentypen erlaubt, insbesondere auch andere Strukturen oder Zeiger auf Strukturen.

Man könnte also etwa eine Koordinate definieren, die aus der x- und der y-Position auf einer Zeichenfläche besteht und diese Struktur für weitere Typen verwenden, wie einen Punkt oder eine Linie:

struct koord
{
    int
        x,
        y;
};

struct punkt
{
    struct koord    position;
    int             farbe;
};

struct linie
{
    struct koord
        anfang,
        ende;
    int
        farbe;
};                

struct linie    l;
struct punkt    p;

/* ... */
    l.anfang.x = 235;
    l.anfang.y = 0;
    l.ende.x   = 100;
    l.ende.y   = 150;
    l.farbe    = 12;

    p.position = l.anfang;
    p.farbe    = l.farbe;

Wen beim Benutzen der Strukturdefinition das ewige Schreiben des Schlüsselwortes struct nervt, der kann sich mit typedef für eine Struktur einen neuen Namen ausdenken und diesen verwenden:

typedef struct
{
    int
        x,
        y;
} koord_t;

typedef struct
{
    koord_t         position;
    int             farbe;
} punkt_t;

typedef struct
{
    koord_t         anfang, ende;
    int             farbe;
} linie_t;

linie_t         l;
punkt_t         p;


l.anfang.x = 235;
l.anfang.y = 0;
l.ende.x   = 100;
l.ende.y   = 150;
l.farbe    = 12;

/* ... */
    p.position = l.anfang;
    p.farbe    = l.farbe;

Der Anhang _t an den Typnamen soll nur kennzeichnen, daß sich hinter diesen Namen ein Datentyp verbirgt und ist nicht zwingend. Etwas Systematik bei der Vergabe von Namen schadet aber nie.

Beim Übergeben einer Struktur an eine Funktion als Parameter wird die Struktur wie andere Variablen auch als Wertparameter übergeben. Dies kann bei großen Strukturen uneffektiv sein. Dann sollte man stattdessen die Adresse der Struktur übergeben und den Parameter in der Funktion als Zeiger auf die jeweilige Struktur behandeln.

Die Größe einer Struktur im Speicher muß übrigens nicht die Summe der einzelnen Elemente sein. Viele Rechner können nämlich nicht alle Datentypen an beliebigen Adressen unterbringen. Je nach Prozessor kann es sein, daß alle Typen mit 2, 4, oder 8 Byte an geraden (oder ggf. auf durch 4 oder 8 teilbaren) Adressen liegen müssen. Hat man auf einem solchen Rechner (angenommen: char benötigt 1 Byte, int 2 Byte) folgende Variable s vereinbart:

struct
{
    int     i1;
    char    c;
    int     i2;
}
    s;
dann muß der Compiler zwischen den Elementen c und i2 ein ungenutztes Byte einfügen, damit i2 auf einer geraden Adresse liegt.

Durch die Forderung nach geraden Adressen vergrößert sich also auf dem Beispielrechner die struct von 5 auf 6 Byte. Am besten ist es ohnehin, über die genaue Größe von irgendwelchen Objekten im Speicher keine Vermutungen anzustellen, sondern mit sizeof das Rechnen auf den Compiler abzuwälzen. In dem obigen Beispiel würde also sizeof(s) auf jedem Rechner die richtige Größe liefern.


8.7.1.1 Bitfelder

sind spezielle Einträge in Strukturen. Sie haben mit den bisher bekannten Feldern15nichts zu tun!

Sinn derselben ist es, in Strukturen Platz zu sparen. Hat man beispielsweise ein System, auf dem eine int-Variable 16 bit, also 2 Byte, belegt, dann benötigt man für jeden int-Eintrag in einer struct natürlich auch 2 Byte. Oft braucht man aber nicht den ganzen Zahlenbereich, den der Typ int bietet. char trifft aber den benötigten Platz auch nicht immer genau.

Mit Hilfe von Bitfeldern kann man bei der Definition einer struct angeben, wieviele bits man für den entsprechenden Eintrag braucht. Dies ist aber nur bei Einträgen möglich, die als int deklariert werden (signed oder unsigned). Folglich darf die angegebene bit-Zahl nicht größer sein, als die Länge einer int auf dem verwendeten Rechnersystem.

Die bit-Zahl wird hinter dem Elementnamen und einem Doppelpunkt eingetragen. Der Zugriff auf die Elemente ist identisch mit den normalen Strukturelementen. Jedes Bitfeld kann wie eine andere ganze Zahl verwendet werden; nur ist ihr Wertebereich kleiner.

Zumindest hintereinander liegende Bitfelder in einer Struktur faßt der Compiler nach Möglichkeit in einem Maschinenwort zusammen, also in einer int.

Werden mehr Bitfelder angegeben, als in ein Maschinenwort passen, dann beginnt der Compiler automatisch ein neues Wort.

Das Bilden von Feldern aus Bitfeldern ist nicht zulässig, wohl aber von Feldern aus Strukturen mit Bitfeldern.

Das Berechnen der Adresse eines Bitfelds mit dem Adreßoperator & ist nicht zulässig.

Der Zugriff auf Bitfelder ist in der Regel langsamer als auf normale struct-Elemente.

Bitfelder kann man auch nutzen, um gezielt auf Teile eines Wortes zugreifen zu können, in dem einzelne bits oder Gruppen davon eine bestimmte Bedeutung haben. Dies ist jedoch implementierungsabhängig. Insbesondere ist im Standard auch nicht festgelegt, in welcher Reihenfolge die Teile im Wort liegen.

Beispiel:

/* stpack.c 31. 7.95 kw
 */

/* Die folgende struct benoetigt
 * sizeof(double) + 3*sizeof(int) =
 * 8                3*2           =    (in Turbo-C, Prospero C)
 *                                = 14 Bytes.
 * bzw.
 * sizeof(double) + 3*sizeof(int) =
 * 8                3*4           =    (in GNU-C)
 *                                = 20 Bytes.
 */

typedef struct
{
  double           d;
  unsigned int     farbe;
  unsigned int     wert;
  unsigned int     an_aus;
}
test_normal;


/* Die folgende struct belegt nur 10 oder 12 Byte.
 * Wenn die angegebenen Wertebereiche ausreichen, dann kann man
 * diese struct genauso verwenden wie test_normal.
 */

typedef struct
{
  double           d;              /* 8 Bytes     */
  /* alles weitere passt in eine int, also
   * 16 oder 32 Bit, je nach Rechner:
   */
  unsigned int     farbe      :8;  /* 0...255     */
  int              wert       :4;  /* -8...7      */
  unsigned int     an_aus     :1;  /* 0...1       */
  /* die restlichen Bits der struct werden nicht
   * genutzt.
   */
}
test_packed;

main()
{
  test_packed      a;
  printf( "test_normal ist %ld Bytes lang\n",
          (long)sizeof(test_normal)
          );
  printf( "test_packed ist %ld Bytes lang\n",
          (long)sizeof(test_packed)
          );

  a.d      = 1.0;
  a.farbe  = 200;
  a.wert   = 5;
  a.an_aus = 1;
  printf( "a = %4.1f, %u, %d, %u\n",
          a.d, a.farbe, a.wert, a.an_aus
          );
}

Dieses Programm erzeugt auf meinem Rechner (Linux, mit GCC) die Ausgabe:

test_normal ist 20 Bytes lang
test_packed ist 12 Bytes lang
a =  1.0, 200, 5, 1

8.7.2 union

Eine union ist struct sehr ähnlich.

Der wesentliche Unterschied liegt darin, daß die Elemente einer union nicht hintereinander im Speicher liegen (wie bei einer struct), sondern alle an der gleichen Stelle im Arbeitsspeicher. Genauer gesagt: alle Elemente beginnen an der selben Stelle im Speicher (sind die Elemente unterschiedlich groß, dann enden sie dementsprechend an verschiedenen Stellen).

Das bedeutet, daß man immer nur eines der vereinbarten Elemente einer union belegen kann; alle anderen werden dabei überschrieben.

Eine union ist immer so groß wie ihr größtes Element.

Man kann eine union dazu benutzen, um unterschiedliche Elemente zu speichern, wenn man sich sicher ist, daß immer nur eine Variante benutzt wird.

Die Vereinbarung und Verwendung ist identisch mit der struct, es wird nur das Schlüsselwort union statt struct verwendet.

Beispiel:

/* stmanta.c 31. 7.95 kw
 */

union manta_oder_iq
{
  int     iq;
  float   geschwindigkeit;
};

typedef struct
{
  char                name[30];
  char                beruf[20];
  union manta_oder_iq miq;
}
person_t;

void lese_person( person_t *p )
{
  printf( "Bitte den Namen eingeben: " );
  scanf( "%s", (*p).name );
  printf( "Bitte den Beruf eingeben: " );
  scanf( "%s", (*p).beruf );
  if( strcmp( (*p).name, "Manni" ) )
  {
    printf( "Bitte den Intelligenzquotienten:  " );
    scanf( "%d", &((*p).miq.iq) );
  }
  else
  {
    printf( "Bitte die Hoechstgeschwindigkeit: " );
    scanf( "%f", &((*p).miq.geschwindigkeit) );
  }
}

void zeige_person( person_t p )
{
  printf( "Name: %s\nBeruf: %s\n", p.name,p.beruf );
  if( strcmp( p.name, "Manni" ) )
  {
    printf( "Der IQ ist %d\n", p.miq.iq );
  }
  else
  {
    printf( "Manni faehrt %f km/h\n", p.miq.geschwindigkeit );
  }
}

main()
{
  person_t    person;
  lese_person( &person );
  zeige_person( person );
  return 0;
}

Dieses Programm liest Name und Beruf einer Person ein. Wenn der Name ,,Manni`` ist, dann wird seine Höchstgeschwindigkeit abgefragt, ansonsten der IQ (Intelligenzquotient) der Person. person.miq.iq kann einen Namen speichern, oder person.miq.geschwindigkeit kann eine Geschwindigkeit speichern; nie aber beides gleichzeitig.

Die Ausgabe des Programms erfolgt analog: Am Namen wird erkannt, ob in person.miq eine Geschwindigkeit gespeichert ist oder ein IQ.

Die Verantwortung, aus einer union den Typ auszulesen, der auch darin gespeichert wurde, liegt beim Programmierer.

AnyWare@Wachtler.de