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
charintshortlonglong longfloatdoublelong 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) >=longlong(mind. 32 Bit) >=intint(mind. 16 Bit) >=shortshort(mind. 16 Bit) >=charchar(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_tint16_tint32_tint64_tuint8_tuint16_tuint32_tuint64_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 intsigned→ 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.12990xHHHH– Integer-Literal in hexadezimaler Schreibweise, z. B.0xCAFEBABE0xxxx– Integer-Literal in oktaler Schreibweise, z. B.0777xxx.xxx– Double-Literal- Durch einen Suffix kann der Datentyp angezeigt werden
L– long, z. B.182721LLL– long long, z. B.18272222LLu– 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
char↔short↔int↔long- Wenn ein Operand
doubleist, wird der andere zudoublekonvertiert - Wenn ein Operand
floatist, 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
- C hat keinen Datentyp für boolean
- Wird über
intodercharsimuliert 0= false1= 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 NAMEkann man eigene Typen definieren - Besonders wichtig für
structundenum(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
enumdefiniert 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
staticauf 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
staticgekennzeichnet ist, außerhalb einer Funktion - global innerhalb einer Datei → jede Variable, die als
staticgekennzeichnet 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;
}
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)