Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Datentypen

Standardtypen

Jede Programmiersprache braucht eine gewisse Anzahl von Datentypen, die direkt in der Sprache verstanden werden. Diese können dann vom Entwickler genutzt werden, um weitere (komplexe) Datentypen zu konstruieren.

C hat eine Reihe von Standarddatenytpen, die ähnlich zu denen von Java sind

  • char
  • int
  • short
  • long
  • long long
  • float
  • double
  • long double

Es fehlt ganz offensichtlich der Datentyp boolean und auch die Bit-Breite der Datentypen ist nicht identisch zu Java (siehe unten).

Besonders beachtenswert sind die zusammengesetzten Bezeichnungen: Ein long ist etwas anderes als ein long long und long long bezeichnet – obwohl es zwei Worte sind – einen Datentyp.

C wurde mit dem Ziel entwickelt, dass sich in C geschriebene Programme auf unterschiedlichen Plattformen (Prozessoren, Betriebssystemen) kompilieren und ausführen lassen. Aufgrund der Maschinennähe von C sollten die Datentypen besonders gut zu dem verwendeten Prozessor auf der Plattform passen, sodass man sich dazu entschieden hat, die Bitbreiten der Datentypen nicht zu spezifizieren. Stattdessen fordert der C-Standard einige Beziehungen zwischen den Datentypen.

  • Die Breite (in Bits) der Standardtypen ist nicht festgelegt und hängt von der Plattform ab
  • Der kleinste Datentyp ist immer char
  • Es gibt nur Relationen zwischen den Typen und Mindestgrößen
    • long long (mind. 64 Bit) >= long
    • long (mind. 32 Bit) >= int
    • int (mind. 16 Bit) >= short
    • short (mind. 16 Bit) >= char
    • char (mind. 8 Bit)

Auf den ersten Blick könnte man denken: „OK, welche Plattform unterstützt denn nicht wenigstens 64bit nativ?“ Die Antwort ist, dass C auch heute noch auf Plattformen eingesetzt wird, die eine Standardbitbreite 16 Bit haben, z. B. diverse Mikrocontroller.

Laut C-Standard, sollte der Datentyp int derjenige sein, der von der Plattform am besten unterstützt wird – der native Datentyp. Im Allgemeinen heißt das, dass es der Datentyp sein sollte, welcher der Breite der Prozessorregister entspricht.

Es gibt noch eine Reihe von (historisch entstandenen) alternativen Namen für die Datentypen:

Type Aliases
short short int
signed short
signed short int
unsigned short unsigned short
unsigned short int
int signed
signed int
unsigned int unsigned
long long int
signed long
signed long int
unsigned long unsigned long int
long long long long int
signed long long
signed long long int
unsigned long long unsigned long long int

Im Folgenden ein paar Beispiele für die Bitbreiten der C-Datentypen auf unterschiedlichen Plattformen.

Datentyp Arduino MacOS X Win32 Win64
char 8 Bit 8 Bit 8 Bit 8 Bit
short 16 Bit 16 Bit 16 Bit 16 Bit
int 16 Bit 32 Bit 32 Bit 32 Bit
long 32 Bit 64 Bit 32 Bit 32 Bit
long long 64 Bit 64 Bit 64 Bit
float 32 Bit 32 Bit 32 Bit 32 Bit
double 32 Bit 64 Bit 64 Bit 64 Bit

Wie man sieht, definieren nur Arduino und Win32 den Datentyp int entsprechend der Breiter der Prozessorregister. Die anderen Plattformen haben sich entschieden, int bei 32 Bit zu belassen, damit die Kompatibilität mit älteren Programmen gewahrt bleibt.

Ob man mit unterschiedlichen Breiten der Datentypen leben kann, hängt vom Programm ab. Geht es z. B. darum, Netzwerkpakete zu verarbeiten, dann benötigt man Datentypen mit bekannter Bitbreite, um die Daten darin abzulegen. Will man nur ein Zahlenratespiel implementieren oder über ein Array mit einigen Elementen laufen, ist es egal, ob ein int 16 oder 32 Bit hat.

Das Problem wird gelöst durch die Definition von Datentypen mit fester Breite. Jetzt hat man die Wahl.

In stdint.h werden Typen mit fester Breite deklariert, die plattformübergreifend gelten

  • int8_t
  • int16_t
  • int32_t
  • int64_t
  • uint8_t
  • uint16_t
  • uint32_t
  • uint64_t

Wertebereich der Zahlentypen

Anders als in Java, werden die Ganzzahl-Datentypen in C noch einmal darin unterschieden, ob sie mit oder ohne Vorzeichen benutzt werden. Hierfür gibt es weitere Schlüsselworte (signed und unsigned), die anzeigen, ob ein Datentyp mit oder ohne Vorzeichen gewünscht wird.

  • C-Datentypen gibt es mit und ohne Vorzeichen
    • unsigned → Ohne Vorzeichen, z. B. unsigned int
    • signed → Mit Vorzeichen, z. B. signed int
    • Ohne Angabe → signed, z. B. int
  • Wertebereich ändert sich durch Vorzeichen
Vorzeichen Breite Min Max
signed 8 Bit \( -2^{7} \) \( 2^{7} - 1 \)
signed 16 Bit \( -2^{15} \) \( 2^{15} - 1 \)
signed 32 Bit \( -2^{31} \) \( 2^{31} - 1 \)
signed 64 Bit \( -2^{63} \) \( 2^{63} - 1 \)
unsigned 8 Bit \( 0 \) \( 2^{8} \)
unsigned 16 Bit \( 0 \) \( 2^{16} \)
unsigned 32 Bit \( 0 \) \( 2^{32} \)
unsigned 64 Bit \( 0 \) \( 2^{64} \)
Datentyp Breite Wertebereich
Fließkomma 32 Bit +/- \( 3.402,823,4 * 10^{38} \)
Fließkomma 64 Bit +/- \( 1.797,693,134,862,315,7 * 10^{308} \)

Literale

Literale erlauben die Angabe von Werten direkt im Quelltext. Hier gibt es wegen der Gemeinsamkeiten von C und Java keine Überraschungen.

  • Die Schreibweise von Integer- und Float-Literalen in C entspricht der in Java
    • xxxx – Integer-Literal in Dezimalschreibweise, z. B. 1299
    • 0xHHHH – Integer-Literal in hexadezimaler Schreibweise, z. B. 0xCAFEBABE
    • 0xxxx – Integer-Literal in oktaler Schreibweise, z. B. 0777
    • xxx.xxx – Double-Literal
  • Durch einen Suffix kann der Datentyp angezeigt werden
    • L – long, z. B. 182721L
    • LL – long long, z. B. 18272222LL
    • u – unsigned, z. B. 0x8a632ffeuLL

Typumwandlung

C ist eine typsichere Sprache (wie auch Java), d. h. der Compiler überprüft die Typen von Variablen und Literalen bei Zuweisungen und stellt sicher, dass bei der Zuweisung keine inkompatiblen Typen zum Einsatz kommen.

Anders als der Java-Compiler meckert der C-Compiler aber nicht, wenn man einen größeren an einen kleineren Typ (z. B. long und short) zuweist (narrowing), sondern führt die Zuweisung einfach durch. Wenn der zugewiesene Wert außerhalb des Wertebereichs der Variable liegt, kommt es zu einem Überlauf und Daten gehen verloren. Variablen, deren Typ vorzeichenbehaftet ist, springen dann auch gerne in den negativen Bereich.

  • Implizite Typumwandung, d. h. ohne Cast
    • charshortintlong
    • Wenn ein Operand double ist, wird der andere zu double konvertiert
    • Wenn ein Operand float ist, wird der anderer zu float
  • Explizite Typumwandung mit Cast
    • Syntax: (typ) wert
    • Analog zu Java
  • C-Umwandlungen machen nicht immer das, was man erwartet

Will man eine Typ explizit umwandeln, kann man dazu einen Cast verwenden, der mithilfe des Cast-Operators () eine Umwandlung durchführt.

unsigned char c;
short s;
int i;
long l;
double d;

i = 100000;
s = i; /* narrowing, impliziter Cast */
printf("%d %d\n", i, s); /* 100000 -31072 */

s = (short) i; /* narrowing, expliziter Cast */
printf("%d %d\n", i, s); /* 100000 -31072 */

l = i; /* widening, impliziter Cast */
printf("%d %ld\n", i, l); /* 100000 100000 */

Das Beispiel zeigt, dass C einfach den zu großen int-Wert i in die short-Variable s presst. Da s vorzeichenbehaftet ist, springt der Wert ins Negative um. Warum? Bei der Zuweisung werden einfach die unteren 16 Bit von i (11000011010100000) in s geschrieben 1000011010100000. Das oberste Bit ist gesetzt, also handelt es sich um eine negative Zahl. Da diese in Zweierkomplementdarstellung vorliegt, müssen wir 1 abziehen und die Bits flippen: 0111100101100000, um den Wert zu erhalten. Diese ist 31072, unter Beachtung des Vorzeichens also tatsächlich -31072.

Die Typumwandlung kann man auch durch einen Cast s = (short) i herbeiführen. Am Ergebnis ändert sich allerdings nichts.

c = i; /* narrowing, impliziter Cast */
printf("%d %d\n", i, c); /* 100000 160 */

d = 3 / 2; /* Ausdruck ist int */
printf("%f\n", d); /* 1.000000 */

d = 3 / 2.0; /* Ausdruck ist double */
printf("%f\n", d); /* 1.500000 */

d = (double) i;
printf("%f\n", d); /* 100000.000000 */

In der letzten Zeile des Beispiels sieht man eine explizite Umwandlung eines int in einen double-Wert. Hier ist der Compiler immerhin so nett und schreibt nicht das Bitmuster in die Variable, sondern konvertiert den Wert in das korrekte Format für Fließkommazahlen.

Boolean

  • C hat keinen Datentyp für boolean
  • Wird über int oder char simuliert
    • 0 = false
    • 1 = true
  • Per Definition gilt: <> 0 = true
int i = 7;
int bigger;
int smaller;

bigger = i > 8;
smaller = i < 8;

printf("%d\n", bigger); /* -> 0 */
printf("%d\n", smaller); /* -> 1 */

Details zu der Frage, wie man in C mit Wahrheitswerten umgeht, wurden bereits im Abschnitt zu den Kontrollstrukturen diskutiert.

Eigene Typen

  • Mit typedef TYP NAME kann man eigene Typen definieren
  • Besonders wichtig für struct und enum (siehe unten)
typedef short int smallNumber;
typedef unsigned char byte;

smallNumber x;
byte b;

Hierbei bezeichnet TYP einen vorhandenen Datentyp und NAME gibt den Namen für den neu zu definierenden Typ an.

Auf den ersten Blick wirkt es etwas unnötig, dass man für einen vorhandenen Datentyp wie short int mit einem neuen Namen smallNumber zu versehen. Es gibt aber drei wichtige Einsatzzwecke für typedef:

  • Datentypen plattformunabhängig definieren
  • Enumerationen deklarieren
  • Strukturen deklarieren

Enumerationen

Häufig benötigt man einen Aufzählungsdatentyp, mit dem man eine Auswahl aus einer Reihe von Elementen darstellen kann. Hierzu bietet C Enumerationen an.

  • Aufzählungstypen (Enumerationen) können mit enum definiert werden
  • Verhalten sich wie int
typedef enum { Red, Green, Blue } Color;
typedef enum { Rain=1, Snow=2, Wind=4 } Weather;

Color c = Green;
Weather w = Snow;

printf("%d\n", c); /* -> 1 */
printf("%d\n", w); /* -> 2 */
printf("%d\n", w + c + Wind); /* -> 7 */

w = 9; /* Außerhalb des Wertebereiches */

Aus Effizienzgründen werden Enumerationen in C intern als int-Werte dargestellt. Gibt man bei der Deklaration einer Enumeration keine Zahlenwerte an, werden die Elemente einfach bei 0 beginnend nummeriert. Wie das Beispiel zeigt, kann man aber auch explizit die Werte für die Elemente vorgeben.

C macht allerdings – anders als Java – wenig Anstalten, den Wahren Charakter von Enumerationen als Zahlenwerte zu verbergen. Man kann einfach mit ihnen rechnen und auch Werte zuweisen, die außerhalb des Wertebereiches liegen. C-Enumerationen sind daher nicht typsicher.

Objekte

C ist eine prozedurale Programmiersprache und kennt deswegen keine Objekte. Ein C-Programm besteht aus Funktionen und globalen Daten.

  • C hat keine Objekte
  • Variablen in einem Block { } sind nur in dem Block gültig (→ Java)
  • Variablen außerhalb eines Blocks
    • sind global
    • leben solange das Programm lebt
    • werden mit static auf eine Datei beschränkt
  • Der Wert einer lokalen Variable ist nach der Deklaration nicht initialisiert (↔ Java)

Man kann die Sichtbarkeit von Variablen in C nur auf vier Ebenen kontrollieren:

  • global → jede Variable, die nicht als static gekennzeichnet ist, außerhalb einer Funktion
  • global innerhalb einer Datei → jede Variable, die als static gekennzeichnet ist, außerhalb einer Funktion
  • in einer Funktion → lokale Variablen in einer Funktion
  • in einem Block → lokale Variablen in einem Block, in einer Funktion
/* Sichtbar im gesamten Programm */
int global;

/* Sichtbar nur in dieser Datei */
static int file_global;

void f() {
    /* Sichtbar in der ganzen Funktion */
    int function_local;

    {
      /* Sichtbar nur im Block */
      int block_local;
    }
}

Das Schlüsselwort static hat in C also eine ganz andere Bedeutung als in Java. Während es in Java die Variable unabhängig vom Objekt macht – also globaler – schränkt es in C die Sichtbarkeit auf die aktuelle Quelltext-Datei ein.

In C besitzen nur globale Variablen nach der Definition einen bekannten Wert. Für lokale Variablen gilt dies nicht, d. h. sie müssen initialisiert werden oder sie tragen einen undefinierten, beliebigen Wert.

#include <stdio.h>

int global;

void f() {
    int local; /* Zuweisung fehlt hier */
    printf("global=%d, local=%d", global, local);
}

int main() {
    f();
    return 0;
}
Ausgabe
global=0, local=21849

Der Wert ist nicht wirklich beliebig, sondern repräsentiert die Daten, die aus vorangegangenen Funktionsaufrufen auf dem Stack an der entsprechenden Stelle gespeichert sind. Mit der Option -Wall würde der C-Compiler vor diesem Problem warnen:

initialization.c:7:3: warning: 'local' is used uninitialized in this
                               function [-Wuninitialized]
    7 |   printf("global=%d, local=%d", global, local);
      |   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Welche Größe haben Datenobjekte in C?

  • Alle Datenobjekte in C haben eine feste Größe während ihrer Lebenszeit (Ausnahme: Dynamisch erzeugte)
  • Größe wird bei der Erzeugung festgelegt
    • Globale Variablen beim Kompilieren (Daten-Segment)
    • Lokale Variablen beim Funktionsaufruf (Stack)
    • Dynamische Daten durch den Programmierer (Heap)

Copyright © 2022 Thomas Smits