Unterabschnitte


8.4 Grunddatentypen

Die Datentypen in C sind zum einen die elementaren Grunddatentypen (signed char und unsigned char, signed/unsigned int, signed/unsigned long int, float, double, long double, Zeiger, size_t, ptrdiff_t), sowie daraus zusammengesetzte Datentypen (Felder, struct und union). Mit void als Datentyp kann man anzeigen, daß eine Funktion keinen Wert liefert9. Eine C-Funktion mit void als Ergebnistyp entspricht also einer PROCEDURE in Pascal oder einer SUBROUTINE in FORTRAN.


8.4.1 Ganzzahlige Datentypen:

Der wichtigste ganzzahlige Datentyp ist int. Damit kann man die üblicherweise auftretenden ganzen Zahlen beschreiben. Von dem Datentyp int gibt es verschiedene Abarten, die auch kombiniert werden können, soweit sie sich nicht widersprechen. short int ist die platzsparende Version von int, long int eine Version, die meist mehr Platz braucht, aber dafür einen größeren Zahlenbereich abdecken kann. long int ist auf den gängigen Rechnern 4 Byte lang und kann damit den Bereich von -2147483648 bis +2147483647 beschreiben.

int ist die natürliche Größe des verwendeten Prozessors und damit in der Regel 2 oder 4 Byte lang. Zwei Byte lange int können Werte von -32768 bis +32767 annehmen. short int ist meist 1 oder 2 Byte lang.

Der kleinste ganzzahlige Typ ist char. char ist nicht auf darstellbare Zeichen beschränkt wie bei Pascal, sondern ein normaler ganzzahliger Datentyp, mit dem man auch rechnen kann.

Beispielsweise ergibt der Ausdruck ('B'-'A') den Abstand zwischen B und A im verwendeten Zeichensatz (in aller Regel ist das der Wert 1).

short int muß nicht kürzer sein als int, int muß nicht kürzer sein als long. Der ANSI-Standard läßt die genauen Längen offen und schreibt nur vor, daß die Größe von char kleiner oder gleich als die von short int ist, diese wiederum kleiner oder gleich als die von int und diese kleiner oder gleich als die einer long int.

Eine weitere Differenzierung hat man mit dem Zusatz signed oder unsigned zur Verfügung.

Ob der Datentyp char eine vorzeichenbehaftete Zahl ist oder vorzeichenlos, legt der ANSI-Standard nicht fest. Bei einem Byte für eine char-Zahl kann es also sein, daß man auf einem Rechner für char einen Wertebereich von -128 bis +127 hat (signed), auf einem anderen dagegen den Bereich 0...255 (unsigned). Mit signed char bzw. unsigned char kann man den einen oder den anderen Wertebereich erzwingen. Verwendet man als Datentyp char ohne den Zusatz signed oder unsigned, dann kann man sich nur darauf verlasssen, daß man den Bereich von 0 bis +127 zur Verfügung hat.

Alle anderen ganzzahligen Datentypen sind signed, sofern man nicht ausdrücklich unsigned angibt. Es ist also gleichgültig, ob ich eine Variable so: int i; oder so: signed int i; vereinbare; in beiden Fällen ist sie vorzeichenbehaftet.

Bei jeder ANSI-konformen Implementation sind in der Datei limits.h die Wertebereiche aller ganzzahligen Datentypen eingetragen. Bei 2 Byte langenint findet man dort beispielsweise die Zeilen:

#define INT_MIN      -32768   /* Achtung! Nur Beispielwerte! */
#define INT_MAX      +32767
und für die anderen ganzzahligen Typen analog.

CHAR_BIT gibt die Anzahl der Bits in einem char an, also meist 8.

CHAR_MIN, CHAR_MAX, UCHAR_MAX, SCHAR_MIN, SCHAR_MAX geben die Wertebereiche für die Datentypen char/unsigned char/signed char an.

SHRT_MIN, SHRT_MAX, USHRT_MAX, INT_MIN, INT_MAX, UINT_MAX, LONG_MIN, LONG_MAX und schließlich ULONG_MAX enthalten die Wertebereiche für signed/unsigned short beziehungsweise für signed/unsigned long.

Um zu erfahren, wie groß eine int werden darf, kann man also ein kleines Programm schreiben:

/* grintmax.c 30. 7.95 kw
 */

#include <limits.h>

main()
{
  printf( "int hat maximal den Wert %d\n", INT_MAX );
}

8.4.1.1 size_t

ist ein ganzzahliger, vorzeichenloser Datentyp, der fast immer gleichbedeutend ist mit dem Typ unsigned long int oder mit unsigned int. Er dient dazu, die Größe eines Speicherbereichs anzugeben. Dazu kann je nach Rechner ein 2 oder 4 Byte großer Wert nötig sein, deshalb wurde der Typ size_t geschaffen, der nun auf jeder Implementation für das Arbeiten mit Speichergrößen verwendet werden kann. size_t ist in stddef.h, aber meist auch in anderen Headerdateien vereinbart.


8.4.2 Logischer Datentyp(boolean, logical)

existiert nicht. Einfach nicht da.

Stattdessen wird irgendein ganzzahliger oder gebrochener Datentyp verwendet. In der Regel ist das int, kann aber auch char, long oder gar ein Zeiger sein oder double. Die einzige Regel, die man dabei beachten muß: Der Zahlenwert 0 (oder bei float und double: 0.0, bei Zeigern: NULL) zählt als logisch falsch, alles andere als logisch wahr10.

Alle Vergleichsausdrücke liefern dementsprechend den Zahlenwert 1, also wahr, wenn die entsprechende Bedingung erfüllt ist, oder 0, also falsch, wenn die Bedingung nicht erfüllt ist.

Beispiel:

    x = 10;
    y = 20;
    if( x<y )
        printf( "x ist kleiner als y\n" );
    else
        printf( "x ist groesser oder gleich y\n" );

Da x tatsächlich kleiner ist als y, liefert der Ausdruck x<y das Ergebnis 1 (also logisch wahr) und die erste der beiden printf-Anweisungen wird ausgeführt.

Wäre x nicht kleiner als y, dann würde der Ausdruck x<y das Ergebnis 0 liefern.

So ersetzen in C normale (meist ganzzahlige) Datentypen die logischen Variablen aus anderen Sprachen.

8.4.2.1 Achtung!

Wird eine Gleitkommazahl berechnet, und dann in einem logischen Ausdruck verwendet, wird sie fast immer als ungleich 0 bewertet, da sie durch Rundungsfehler meistens nicht den Wert 0 trifft.

Deshalb sollte man statt

    double  a;
    /* ... */
    a = /* ... */;
    if( a ) /* ... */

lieber etwa folgendes schreiben:

#include <math.h>
#include <float.h>
/* ... */
    double  a;
    /* ... */
    a = /* ... */;
    if( fabs( a )>FLT_EPSILON ) /* ... */


8.4.3 Gebrochene Zahlen

An gebrochenen Zahlen (Gleitkommazahlen) kennt C die drei Datentypen float, double sowie long double. Mit dem Verhältnis dieser Typen untereinander ist es ähnlich bestellt wie mit short, int und long: Die Implementation muß sicherstellen, daß double größer oder gleich float ist und long double größer oder gleich double (bezogen auf Wertebereich und Genauigkeit). Tatsächlich sind long double und double oft das selbe.

Analog zu limits.h sind in float.h bei einer sauberen ANSI-Implementation die Wertebereiche und Genauigkeiten für die Gleitkommazahlen definiert:

Die Zahlenbereiche des verwendeten Compilers erhält beispielsweise man so:

/* grfltmax.c 31 .7.95 kw
 */

#include <float.h>

main()
{
  printf( "Die groesste float-Zahl ist %12.4e.\n", FLT_MAX );
  printf( "double dagegen haelt bis    %12.4e mit!\n", DBL_MAX );
}


8.4.4 Zeiger

(oder auch pointer) ist ein Typ, der eine Adresse des Arbeitsspeichers aufnehmen kann, also beispielsweise die Adresse einer Variablen oder einer Funktion.

Ein Zeiger ist an einen anderen Typ gebunden, beispielsweise ein Zeiger auf eine int oder ein Zeiger auf eine double.

Beschrieben wird ein Zeigertyp durch den entsprechenden daran gebundenen Datentyp mit einem nachgestellten Stern *. Ein Zeiger auf eine int hat also den Typ int *, ein Zeiger auf eine double ist double *, ein Zeiger auf eine char ist char * und so weiter.

Ein Zeiger kann auch auf eine Variable vom Typ Zeiger verweisen und erhält dann als Typbezeichnung den Typ, auf den verwiesen wird (z.B. char *) mit einem nachgestellten *, also insgesamt zwei Sterne (char **). Eine Variable vom Typ char ** zeigt also auf eine andere Variable, die ebenfalls ein Zeiger ist und wiederum auf eine Variable oder Konstante vom Typ char verweisen kann.

Hat man eine Variable vom Typ Zeiger mit dem Namen p, dann meint man mit p den Inhalt der Zeigervariablen, also die Speicheradresse. Mit *p bezeichnet man dagegen das, worauf die Zeigervariable verweist.

Beispiel:

    int *p;       /* p ist eine Variable vom Typ "Zeiger auf int"
                   */
    int i, j;     /* i und j sind "normale" int-Variablen
                   */

    /* Bisher zeigt p auf irgendwas, aber keiner weiss, worauf.
     * Deshalb erst mal auf i zeigen lassen:
     */
    p = &i;       /* i ist die Variable, &i ihre Adresse zur
                   * Laufzeit des Programms im Speicher
                   * p ist die Zeigervariable, die die Adresse
                   * einer int aufnehmen kann und hier auch tut.
                   */

    i = 25;       /* i erhaelt einen Zahlenwert.
                   */
    j = *p;       /* j erhaelt den gleichen Zahlenwert, denn:
                   * In i ist die Zahl 25 gespeichert.
                   * in p ist die Adresse von i hinterlegt und
                   * *p meint nicht diese Adresse, sondern das,
                   * worauf die Adresse zeigt, also den Inhalt von
                   * i und damit den Wert 25.
                   */

    p = &j;       /* p zeigt jetzt nicht mehr auf i, sondern auf j.
                   */
    *p = 30;      /* An die Adresse, auf die p zeigt (*p) wird die
                   * Zahl 30 geschrieben. Da p auf j zeigt, hat j
                   * jetzt den Wert 30. i wird dabei nicht
                   * veraendert.
                   */

Der Zustand vor der Zuweisung p = &i; ist in Bild 1 links zu sehen, der Zustand danach im selben Bild rechts. Dabei wird angenommen, daß

Die Diagramme stellen einen möglichen Ausschnitt des Arbeitsspeichers zur Laufzeit dar. Im allgemeinen Fall befinden sich die Variablen natürlich an anderen Adressen, und brauchen gar nicht hintereinander zu liegen.

In der Speicherzelle selbst ist der Inhalt eingetragen, daneben die (angenommene) Adresse und der jeweilige Variablenname. Ein Fragezeichen in der Speicherzelle bedeutet, daß der Inhalt nicht bekannt ist, sondern einen zufälligen Wert hat.

Abbildung 1: Vor und nach der Zuweisung p = &i;
\begin{figure}\unitlength=0.7500mm
\linethickness{0.4pt}\begin{picture}(120.00,6...
...)[cc]{?}}
\put(100.00,45.00){\makebox(0,0)[cc]{?}}
\end{picture}\par\end{figure}

Das Bild 2 zeigt die Situation nach der Zuweisung i = 25; (links) bzw. j = *p; (rechts).

Abbildung 2: Nach i = 25; bzw. j = *p;
\begin{figure}\unitlength=0.750mm
\linethickness{0.4pt}\begin{picture}(120.00,60...
... 25}}}
\put(100.00,45.00){\makebox(0,0)[cc]{{\tt 25}}}
\end{picture}\end{figure}

Das Resultat der Zuweisungen p = &j; (links) bzw. *p = 30; (rechts) ist in Bild 3 skizziert.

Abbildung 3: Nach p = &j; bzw. *p = 30;
\begin{figure}\unitlength=0.75mm
\linethickness{0.4pt}\begin{picture}(120.00,60....
... 25}}}
\put(100.00,45.00){\makebox(0,0)[cc]{{\tt 30}}}
\end{picture}\end{figure}


8.4.4.1 Zeiger auf Funktionen:

Zeiger können auch auf Funktionen zeigen. Die Funktion kann dann über den Zeiger aufgerufen werden.

Beispiel:

/* grfunzei.c 31. 7.95 kw
 *
 * testfunktion() gibt den uebergebenen Wert aus.
 */

void testfunktion( int wert )
{
  printf( "%d\n", wert );
}

int main()
{
  void (*fun_zeiger)( int wert );

  /* direkter Aufruf der Testfunktion:#
   */
  testfunktion( 5 );

  /* Holen der Adresse der Funktion:
   */
  fun_zeiger = &testfunktion;

  /* Indirekter Aufruf:
   */
  (*fun_zeiger)( 6 );
}

Dieses ansonsten sinnlose Beispiel zeigt die Verwendung eines Zeigers auf eine Funktion.

In main() wird zuerst eine Variable vereinbart:

void (*fun_zeiger)( int wert );
definiert eine Variable mit dem Namen fun_zeiger. Diese Variable hat den Typ ,,Zeiger auf eine Funktion, die den Rückgabewert void hat und einen Parameter vom Typ int``. In fun_zeiger kann man also die Adresse einer Funktion speichern.

    testfunktion( 5 );
ruft eine solche Funktion mit dem Argument 5 direkt auf; die Funktion gibt die Zahl 5 auf dem Bildschirm aus.

    fun_zeiger = &testfunktion;
Diese Zeile speichert in der Variablen fun_zeiger die Adresse der Funktion testfunktion().

Diese Schreibweise funktioniert bei älteren und neueren C-Compilern. Nach dem (neueren) ANSI-Standard kann der Adreßoperator & auch entfallen, da mit dem Namen einer Funktion gleichzeitig ihre Adresse gemeint ist. Dieses Verhalten der neueren Compiler ist mit der Behandlung von Feldern (als Adresse des ersten Feldelements) vergleichbar, siehe dazu Felder.

    (*fun_zeiger)( 6 );
Damit wird die Funktion, auf welche fun_zeiger zeigt, mit dem Argument 6 aufgerufen. Da in fun_zeiger die Adresse von testfunktion() gespeichert ist, wird diese Funktion aufgerufen.

Da bei ANSI-C-Compilern wie gesagt der Funktionsname und die Adresse der Funktion gleich behandelt werden, könnte man diese Zeile auch einfacher schreiben:

    fun_zeiger( 6 );
Zur Verdeutlichung, daß mit fun_zeiger tatsächlich über einen vereinbarten Zeiger auf eine Funktion zugegriffen wird, könnte man trotzdem die alte Schreibweise beibehalten.

Zeiger auf Funktionen werden meistens verwendet, um Funktionen als Parameter an andere Funktionen übergeben zu können. Ein Beispiel dafür ist die Übergabe der Vergleichsfunktion an bsearch() (Seite [*]) und qsort() (Seite [*]) aus der Standardbibliothek.

Eine andere Anwendungsmöglichkeit ergibt sich, wenn in Abhängigkeit von einem ganzzahligen Ausdruck eine Funktion aus einer Menge von Funktionen aufgerufen werden muß. Dann kann man ein Feld von Zeigern auf Funktionen definieren und mit den Adressen der Funktionen füllen. Die jeweilige Funktion ruft man aus dem Feld auf (mit dem Ausdruck als Index). Das ist beliebt, weil es sehr schnelle Programme ergibt.

8.4.4.2 void *:

Es gibt aber nicht nur Zeiger auf bestimmte Datentypen, sondern auch einen ,,Jokerzeiger``, der auf jede beliebige Variable oder andere Speicherstelle zeigen darf. Dies ist der Datentyp void *.


8.4.4.3 Vorsicht:

Vereinbart man eine Zeigervariable, dann heißt das nicht, daß diese Variable auf ein entsprechendes Element zeigt! Vielmehr bedeutet die Vereinbarung nur, daß die Variable auf ein Element zeigen kann. Der Inhalt der Zeigervariablen ist ebenso wie bei anderen Variablen durch die Typvereinbarung noch nicht gegeben. Erst durch Initialisierung oder Zuweisung einer Adresse (einer Variable oder mit malloc() beschaffter freier Speicher) zeigt die vereinbarte Zeigervariable auf einen entsprechenden Wert.

Ungültiger Zeiger: Eine besondere Konstante existiert vom Typ void *, also ein Zeiger auf einen beliebigen Typ. Diese Konstante hat den Namen NULL und ist immer in stddef.h vereinbart. Ein Zeiger mit diesem Wert kann niemals auf gültige Daten zeigen und wird deshalb als Wert für ungültige Zeiger verwendet. Beispielsweise liefert malloc() wenn es einen Speicherblock beschaffen soll, einen Zeiger auf den beschafften Speicher oder (wenn kein Speicher beschafft werden kann) den Wert NULL. Wenn man den Rückgabewert von malloc() also mit NULL vergleicht, dann kann man erkennen, ob der Funktionsaufruf erfolgreich war.

Der Umkehrschluß gilt leider nicht: Wenn ein Zeiger einen anderen Wert als NULL hat, dann kann man daraus noch nicht schließen, daß der Zeigerwert gültig ist!

Auf jedem System wird ein Speicherzugriff auf die Adresse NULL zu einem Laufzeitfehler führen. Zugriffe auf alle anderen Adressen sind Glückssache, wenn ein Zeiger einen unsinnigen Inhalt hat: es kann ein Laufzeitfehler auftreten, möglicherweise läuft das Programm aber auch mit fehlerhaften Daten weiter.

Deshalb die dringende Empfehlung: An einen Zeiger, der nicht definitiv auf eine gewollte Adresse zeigt, immer NULL zuweisen! Dann führt eine versehentliche Verwendung wenigstens zuverlässig zu einem Programmabbruch.

Ebenso gefährlich ist es, Zeiger auf lokale Variablen zu verwenden, wenn diese gar nicht mehr existieren. Beispiel:

/* zeiglok.c 31. 7.95 kw
 */

/* Diese Funktion liefert einen Zeiger auf eine lokale Variable.
 * Das ist ausgemachter Schwachsinn!
 * Bitte nie nachahmen!
 */
int *f_p( void )
{
  int lokal;
  return &lokal;
}

int main( int nargs, char *args[] )
{
  int *ptr;

  ptr = f_p();

  /* ptr zeigt jetzt auf eine gar nicht mehr existierende
   * Variable. Der folgende Aufruf ist also kriminell:
   */
  *ptr = 25;
  printf( "Die Variable wurde auf 25 gesetzt.\n" );
  printf( "Ihr Wert ist jetzt %d\n", *ptr );

  return 0;
}

Das genaue Verhalten eines solchen Programms ist nicht vorhersehbar; bei mir entsteht folgende Ausgabe:

Die Variable wurde auf 25 gesetzt.
Ihr Wert ist jetzt 25

Solche Fehler können nicht vom Compiler erkannt werden und sind unter Umständen auch vom Programmierer schwer zu entdecken.


8.4.4.4 Zeigerarithmetik:

Mit Zeigern kann man auch bedingt rechnen. Dies ist allerdings auf ,,sinnvolle`` Operationen beschränkt. Multiplikationen, Divisionen, Modulobildung und ähnliches ist verboten.

Erlaubt sind dagegen Addieren und Subtrahieren von ganzzahligen Ausdrücken (dies ergibt ein Ergebnis vom Typ des zugrundeliegenden Zeigers) sowie das Subtrahieren zweier Zeiger (dies ergibt ein Ergebnis vom ganzzahligen Typ ptrdiff_t).

Beim Subtrahieren zweier Zeiger müssen beide auf ein Objekt des gleichen Typs zeigen.

Bei Zeigerarithmetik ist es sehr wichtig zu wissen, daß der beteiligte ganzzahlige Datentyp (also das Ergebnis beim Subtrahieren zweier Zeiger oder der eine Operand beim Addieren zu einem Zeiger) nicht in Byte gemessen wird. Bemessungsgröße ist vielmehr die Größe, die ein Objekt des Typs im Speicher einnimmt, auf das die Zeiger verweisen.

Da der Compiler dazu die Größe des Objekts kennen muß, kann man mit Zeigern vom Typ void * keine Arithmetik treiben!

Beispiel:

double      f[10];      /* Feld mit 10 Elementen             */
double     *ptr;        /* Zeiger auf double                 */

ptr = &f[0];            /* ptr zeigt auf erstes Feldelement. */
*ptr = 0.0;             /* Dahin eine 0.0 schreiben.         */
*(ptr+2) = 3.1415;      /* f[2] wird belegt.                 */
                        /* (ptr+2) zeigt nicht 2 Byte hinter */
                        /* ptr, sondern 2 double-Elemente    */
                        /* hinter ptr. Bei 8 Byte je double  */
                        /* sind das 16 Byte hinter ptr.      */

Analog liefert die Subtraktion zweier Zeiger nicht den Abstand der beiden Operanden in Byte gemessen, sondern gemessen in der Größe des jeweiligen Objekts, auf das verwiesen wird.

Dadurch ist Zeigerarithmetik unabhängig vom verwendeten Rechnersystem und den darauf verwendeten Größen. Die Programme bleiben also selbst bei diesen Rechnereien portabel, weil die Rechnerabhängigkeiten nicht im Quelltext berücksichtigt werden, sondern beim Übersetzen vom jeweiligen Compiler.

C erhält durch Zeigerarithmetik erst richtig Leben; die größere Effektivität von Programmen in C rührt zu einem großen Teil davon. Angenommen, eine Funktion soll ein Feld mit double-Elementen löschen, also alle Werte zu 0.0 setzen. Ein ehemaliger FORTRAN- oder Pascal-Programmierer würde die Funktion vielleicht so schreiben:

/* grloel.c 31. 7.95 kw
 */

#include <stddef.h>

void loesche_double( double feld[], size_t wieviele )
{
  size_t      i;

  for( i=0; i<wieviele; i++ )
    feld[i] = 0.0;
}

Das funktioniert natürlich, aber die Lösung enthält überflüssige Berechnungen. Abgesehen von der nötigen Erhöhung des Schleifenzählers (Auswertung von ++) muß bei jedem Durchlauf die Adresse des Elementes feld[i] berechnet werden. Dazu muß das Programm die Adresse hernehmen, die mit dem Namen feld verbunden ist (das ist die Adresse des ersten Feldelements), und dazu ein Produkt addieren, nämlich i*sizeof(double) (zur Auswertung von feld[i]).

Mit Zeigerarithmetik läßt sich die Funktion wesentlich schneller machen:

/* grloes.c 31. 7.95 kw
 */

#include <stddef.h>

void loesche_double( double *feld, size_t wieviele )
{
  double
    *pfeld = feld,
    *pende = feld+wieviele;

  while( pfeld<pende )
    *pfeld++ = 0.0;
}

Jetzt wird der Parameter feld als Zeiger auf das erste Element aufgefaßt. Ein Zeiger (pfeld) wandert durch das Feld und zeigt nacheinander auf die zu löschenden Elemente. Neben der eigentlichen Zuweisung (=) muß in der Schleife nur noch zu pfeld bei jedem Durchlauf der Wert sizeof(double) addiert werden (Auswertung von ++). Die aufwendige Multiplikation entfällt, außerdem hat man statt zwei Additionen nur noch eine.

8.4.4.5 ptrdiff_t

dient analog zu size_t dazu, die Differenz von 2 Zeigern aufzunehmen. Meist wie size_t, aber mit Vorzeichen; also int oder long int.

AnyWare@Wachtler.de