C-Datentypen

Video zum Kapitel

Link zu YouTube

Deklaration und Definition von Variablen

Um in Programmen Daten verarbeiten zu können, verwaltet man sie in Variablen. Damit die Programmiersprache weiß, welche Art von Daten sich in einer Variable verstecken und insbesondere wie viel Speicher für diese Variable beschafft werden muss, haben Variablen in C einen Datentyp. Der Datentyp bleibt für die gesamte Lebensdauer der Variable gleich und kann sich in C nicht mehr ändern.

Variablen werden durch eine Variablendeklaration bekannt gemacht und der entsprechende Datentyp wird zugeordnet. Die Variablendefinition beschafft den notwendigen Speicher für die Ablage der Daten.

Variablendeklaration und Variablendefinition

  • ordnet einer Variablen einen Datentyp zu
  • beschafft Speicher für die Variable
  • Syntax: TYP VARIABLENNAME;
  • Syntax: TYP VARIABLENNAME, VARIABLENNAME, ...;
/* Deklaration und Definition */
int a; /* einzelne Variable vom Typ int */
int b, c; /* mehrere Variablen vom Typ int */

a = 3; /* Zuweisung */
a = 4; /* erneute Zuweisung */
a = 3.4; /* Fehler: Falscher Datentyp */

Anders als in Skriptsprachen, wie z. B. Python, können einer Variablen keine Daten eines anderen Typs zugewiesen werden. Der Datentyp der Variablen kann sich später auch nicht ändern, sie hat immer denselben Typ. Dies erlaubt dem Compiler den Speicher einmal anzufordern und dann sicher zu sein, dass jede Zuweisung an die Variable in den Speicher passen wird.

Wenn man eine Variable nur deklarieren will, die Definition aber an anderer Stelle erfolgt, dann muss man das Schlüsselwort extern verwenden.

Variablendeklaration ohne Variablendefinition

  • macht eine Variable und deren Datentyp bekannt
  • Definition erfolgt an anderer Stelle
  • Syntax: extern TYP VARIABLENNAME;
  • Syntax: extern TYP VARIABLENNAME, VARIABLENNAME, ...;
/* Deklaration ohne Definition */
extern int a; /* einzelne Variable vom Typ int */
extern int b, c; /* mehrere Variablen vom Typ int */

Der Compiler kann nicht überprüfen, ob die Variable wirklich definiert wurde, da er immer nur eine einzige Quelldatei betrachtet. Der Linker wird aber nach dem Symbol der Variable in den Objektdateien suchen, die er zusammen bindet. Wenn die Variable nicht in einer definiert (und exportiert wurde), gibt er eine Fehlermeldung aus.

Linker Fehlermeldung bei fehlender Definition
/usr/bin/ld: /tmp/cc3N5eqp.o: in function `main':
extern.c:(.text+0x6): undefined reference to `b'
/usr/bin/ld: extern.c:(.text+0xc): undefined reference to `a'
collect2: error: ld returned 1 exit status

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 Bitbreite 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.

  • Breite (in Bits) der Standardtypen ist nicht festgelegt und hängt von der Plattform ab
  • 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 64 Bit nativ?“ Die Antwort ist, dass C auch heute noch auf Plattformen eingesetzt wird, die eine Standardbitbreite 16 Bit haben, z. B. diverse Mikrocontroller. In Steuerungssystemen sind bis heute sogar 8-Bit-Prozessoren im Einsatz.

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 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 Breite 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 – 8 Bit signed integer
  • int16_t – 16 Bit signed integer
  • int32_t – 32 Bit signed integer
  • int64_t – 64 Bit signed integer
  • uint8_t – 8 Bit unsigned integer
  • uint16_t – 16 Bit unsigned integer
  • uint32_t – 32 Bit unsigned integer
  • uint64_t – 64 Bit unsigned integer

In modernen C-Programmen sollte man die Datentypen aus stdint.h verwenden und sich so die Kopfschmerzen bezüglich der nicht genormten Bitbreiten ersparen.

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. Daraus ergibt sich, dass die Datentypen unterschiedliche Wertebereiche haben, abhängig davon, ob sie signed oder unsigned sind.

  • C-Datentypen gibt es mit und ohne Vorzeichen
    • unsignedOhne Vorzeichen
      z. B. unsigned int
    • signedMit Vorzeichen
      z. B. signed int
    • Ohne Angabe → signed
      z. B. int ist signed 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} - 1 \)
unsigned 16 Bit \( 0 \) \( 2^{16} - 1 \)
unsigned 32 Bit \( 0 \) \( 2^{32} - 1 \)
unsigned 64 Bit \( 0 \) \( 2^{64} - 1 \)
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
    • Llong, z. B. 182721L
    • LLlong long, z. B. 18272222LL
    • uunsigned, 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. Hier lauert aber dann eine der größten Problemfelder von C, das sogenannte undefined behavior, das weiter unten noch genauer erläutert wird.

  • 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

In vielen Programmiersprachen gibt es einen expliziten Datentyp für Wahrheitswerte, der in Java z. B. als boolean bezeichnet wird. C verzichtet auf einen solchen und bildet dies einfach auf int ab.

  • C hat bis C23 keinen Datentyp für boolean
  • wird über int oder char simuliert
    • == 0 → false
    • != 0 → true
int i = 7;
int bigger = i > 8;
int 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. Die Abbildung auf int führt dazu, dass man in C einige Abkürzungen nehmen kann, die in anderen Sprachen nicht möglich sind.

Mit dem C23-Standard wurde der Datentyp (und das Schlüsselwort) bool mit den Konstanten true und false eingeführt. Es gilt, dass true dem Wert 1 und false dem Wert 0 entspricht.

Eigene Typen

In C wird typedef verwendet, um Aliasnamen für bestehende Datentypen zu erstellen. Dies kann die Lesbarkeit des Codes verbessern, besonders bei komplexen Datentypen wie Strukturen, Unions oder Pointern. Mit typedef kann man kürzere oder aussagekräftigere Namen für Datentypen definieren, was den Code verständlicher und wartbarer macht.

  • 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 Ganzzahl 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, anders als in Java. Dafür sind sie sehr viel effizienter implementiert und haben wenig Overhead.

Beginnend mit dem C23-Standard kann man dem Compiler auch mitteilen, welchen Ganzzahlentyp er für die Enumeration verwenden soll.

typedef enum : unsigned long long {
    a0 = 0x00FFFFFFFFFFFFFFULL,
    a1 = 0xFFFFFFFFFFFFFFFFULL
} BigEnum;

Andernfalls kann der Compiler selbst einen Datentyp wählen und verschiedene Compiler treffen hier durchaus unterschiedlichen Entscheidungen.

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 so lange, wie das Programm lebt
    • werden mit static auf eine Datei beschränkt
  • eine lokalen Variable ist nach der Definition 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(void) {
    /* 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 auf gewisse Weise globaler – schränkt es in C die Sichtbarkeit auf die aktuelle Quelltext-Datei ein.

In C besitzen nur globale Variablen durch die Definition einen bekannten und vordefinierten Wert, nämlich 0. Für lokale Variablen gilt dies nicht, d. h. sie müssen initialisiert werden oder sie tragen einen undefinierten Wert, also einen beliebigen, Wert.

#include <stdio.h>

int global; /* wird mit 0 initialisiert */

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

int main(int argc, char** argv) {
    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 allozierter Speicher)
  • Größe wird bei der Erzeugung festgelegt
    • globale Variablen beim Kompilieren (Daten-Segment)
    • lokale Variablen beim Funktionsaufruf (Stack)
    • dynamische Daten durch den Programmierer (Heap)
  • sizeof-Operator liefert die Größe eines Objektes in Bytes
    • wird zur Compilezeit bestimmt, keine Laufzeitinformation
    • sizeof(var): Größe der Variable (in Bytes)
    • sizeof(typ): Größe des Typs (in Bytes)
int a;
long int b;
double c;

/* sizeof von Variablen */
printf("sizeof(a): %lu\n", sizeof(a)); /* -> sizeof(a): 4 */
printf("sizeof(b): %lu\n", sizeof(b)); /* -> sizeof(b): 8 */
printf("sizeof(c): %lu\n", sizeof(c)); /* -> sizeof(c): 8 */

/* sizeof eines Datentyps */
printf("sizeof(float): %lu\n", sizeof(float)); /* -> sizeof(float): 4 */

Es ist wichtig, im Kopf zu behalten, dass sizeof vom Compiler ausgewertet wird und dann der konstante Wert mit der Größe des Datenobjektes in das Programm eingesetzt wird. Deswegen ist es auch egal, ob man eine Variable oder einen Datentyp angibt. Der Compiler ersetzt in der ersten Variante die Variable durch den Datentyp und bestimmt dann dessen Größe.

Zur Laufzeit muss man in einem C-Programm selbst Buch über die Größe von dynamisch erzeugten oder an Funktionen übergebenen Datenobjekten führen. Dies wird beim Thema Arrays noch etwas genauer diskutiert, soll hier aber kurz vorweggenommen werden.

#include <stdio.h>

void f(char data[]) {
    printf("sizeof(data): %lu\n", sizeof(data));
}

int main() {
    char data[100];
    f(data); /* -> sizeof(data): 8 */
}

Bei der Option -Wall warnt der Compiler auch vor diesem Fehler:

sizeof.c: In function 'f':
sizeof.c:4:41: warning: 'sizeof' on array function parameter 'data' will return size of 'char *' [-Wsizeof-array-argument]
    4 |     printf("sizeof(data): %lu\n", sizeof(data));
      |                                         ^
sizeof.c:3:13: note: declared here
    3 | void f(char data[]) {
      |        ~~~~~^~~~~~

Copyright © 2025 Thomas Smits