C-Datentypen
Video zum Kapitel

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.
/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 integerint16_t
– 16 Bit signed integerint32_t
– 32 Bit signed integerint64_t
– 64 Bit signed integeruint8_t
– 8 Bit unsigned integeruint16_t
– 16 Bit unsigned integeruint32_t
– 32 Bit unsigned integeruint64_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
unsigned
→ Ohne Vorzeichen
z. B.unsigned int
signed
→ Mit Vorzeichen
z. B.signed int
- Ohne Angabe →
signed
z. B.int
istsigned 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
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. 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
char
↔short
↔int
↔long
- wenn ein Operand
double
ist, wird der andere zudouble
konvertiert - wenn ein Operand
float
ist, wird der anderer zufloat
- 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
oderchar
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
undenum
(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;
}
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[]) {
| ~~~~~^~~~~~