3.9 Die Wertebereiche der Datentypen ermitteln
Der C-Standard selbst legt den Wertbereich und die Größe der einzelnen Datentypen nicht fest, sondern schreibt nur die Relation zwischen den Größen der Datentypen vor. Für jeden Basisdatentyp werden lediglich Mindestgrößen gefordert. Dadurch ergeben sich für den Compiler verschiedene Gestaltungsmöglichkeiten und auch Kommandozeilen-Argumente (z. B. für den CGG unter Linux), die aber für einen Einsteiger / eine Einsteigerin zu umfangreich sind. Im Ernstfall sollten Sie immer zuerst z. B. mit man gcc die verschiedenen Optionen studieren.
Eine Möglichkeit unter vielen ist, dass der Datentyp int so festgelegt wird, dass seine Größe der Datenwortgröße des Prozessors entspricht (wie wir bereits gesagt haben, muss dies bei manchen Prozessoren sogar so festgelegt werden). Das handhaben aber nicht alle Compiler so, und es gibt auch andere Schemata. Die Größe des Zeigertyps richtet sich wiederum häufig nach der Größe des Speicherbereichs, der vom Programm aus erreichbar (adressierbar) sein muss. Es ist daher durchaus möglich, dass der Speicherbereich kleiner oder größer ist, als die Prozessorarchitektur es zulässt. Auf modernen Betriebssystemen ist die Speicherverwaltung sowieso eine Sache für sich und derart komplex, dass man mindestens ein weiteres Buch benötigt, um diese zu erklären. Eines dieser Bücher ist z. B. »Moderne Betriebssysteme« von Andrew S. Tanenbaum. Aber Vorsicht: Sie sollten erst Experte bzw. Expertin werden, ehe Sie sich dieses Buch zulegen, sonst verstehen Sie keine einzige Zeile. Wenn Sie allerdings gerade mit dem Informatikstudium beginnen, dann sollten Sie sich das Buch von Tanenbaum auf jeden Fall anschaffen – Sie werden es früher oder später benötigen.
Wenn man jetzt davon ausgeht, dass ein Byte 8 Bit groß ist, was auf vielen Architekturen der Fall ist, dann sind die anderen Datentypengrößen alle ein Vielfaches von 8 Bit. Deshalb ergeben sich verschiedene sogenannte Datenmodelle (auch Programmiermodelle genannt). Wir wollen an dieser Stelle nicht näher auf die verschiedenen Datenmodelle eingehen. Es geht uns vielmehr darum, dass Sie verstehen, warum die Datentypen auf unterschiedlichen Systemen unterschiedliche Wertebereiche haben können.
In Tabelle 3.4 finden Sie eine Übersicht über die gängigen Datenmodelle.
Modell |
char |
short |
int |
long |
long long |
void* |
---|---|---|---|---|---|---|
IP16 |
8 |
16 |
16 |
32 |
64 |
16 |
LP32 |
8 |
16 |
16 |
32 |
64 |
32 |
ILP32 |
8 |
16 |
32 |
32 |
64 |
32 |
LLP64 |
8 |
16 |
32 |
32 |
64 |
64 |
LP64 |
8 |
16 |
32 |
64 |
64 |
64 |
ILP64 |
8 |
16 |
64 |
64 |
64 |
64 |
SILP64 |
8 |
64 |
64 |
64 |
64 |
64 |
Tabelle 3.4 Bits von Datentypen verschiedener Datenmodelle
Wenn Sie wissen wollen, welche implementierungsabhängigen Wertebereiche die einzelnen Datentypen auf dem auszuführenden System haben, finden Sie die vom Compiler-Hersteller vergebenen Größen in der Header-Datei limits.h für Integer-Typen und in float.h für Gleitkommatypen. Benötigen Sie hingegen Integer-Typen mit einer festen Größe, dann bietet Ihnen der Standard entsprechende Typen wie int8_t, int16_t usw. an, die in der Header-Datei stdint.h definiert sind.
3.9.1 Limits von Integer-Typen
Möchten Sie erfahren, welchen Wertebereich int oder die anderen Ganzzahldatentypen auf Ihrem System haben, finden Sie in der Header-Datei limits.h entsprechende Konstanten dafür. Für den Datentyp int beispielsweise finden Sie die Konstanten INT_MIN für den minimalen und INT_MAX für den maximalen Wert.
Um diese Werte zu ermitteln, müssen Sie selbstverständlich auch den Header limits.h in Ihr Programm inkludieren. Inkludieren ist das C-Fachwort für »eine Header-Datei mit #include einfügen« und kommt aus dem Lateinischen (includere = dt. einfügen). Das folgende Listing gibt Ihnen den tatsächlichen Wertebereich für die Datentypen char, short, int, long und long long auf Ihrem System aus:
00 // Kapitel3/limits.c
01 #include <stdio.h>
02 #include <limits.h>
03 int main(void) {
04 printf("min. char-Wert : %d\n", SCHAR_MIN);
05 printf("max. char-Wert : +%d\n", SCHAR_MAX);
06 printf("min. short-Wert : %d\n", SHRT_MIN);
07 printf("max. short-Wert : +%d\n", SHRT_MAX);
08 printf("min. int-Wert : %d\n", INT_MIN);
09 printf("max. int-Wert : +%d\n", INT_MAX);
10 printf("min. long-Wert : %ld\n", LONG_MIN);
11 printf("max. long-Wert : +%ld\n", LONG_MAX);
12 printf("min. long long-Wert: %lld\n", LLONG_MIN);
13 printf("max. long long-Wert: +%lld\n", LLONG_MAX);
14 return 0;
15 }
Listing 3.3 Das Listing zeigt, dass Standardtypen auf unterschiedlichen Systemen unterschiedlich groß sein können.
Die Ausgabe des Programms hängt natürlich von der Implementierung ab. Bei uns sieht sie unter Windows 11 (AMD 64) und Pelles C wie folgt aus:
min. char-Wert : -128
max. char-Wert : +127
min. short-Wert : -32768
max. short-Wert : +32767
min. int-Wert : -2147483648
max. int-Wert : +2147483647
min. long-Wert : -2147483648
max. long-Wert : +2147483647
min. long long-Wert: -9223372036854775808
max. long long-Wert: +9223372036854775807
Einen Überblick über alle Konstanten in der Header-Datei limits.h und deren Bedeutungen finden Sie auf den entsprechenden Manpages, in der Online-Hilfe des Compilers oder im Web unter http://en.cppreference. com/w/c/types/limits. Und vergessen Sie auch hier ist nicht das Committee Draft des C11-Standard unter http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf. Dasselbe gilt auch für die gleich folgenden Limits von Gleitkommazahlen.
3.9.2 Limits von Fließkommazahlen
Auch bei Fließkommazahlen gibt es eine Header-Datei mit Konstanten, in der Sie die Wertebereiche (Limits) ermitteln können. Alle implementierungsabhängigen Wertebereiche für Fließkommazahlen sind in der Header-Datei float.h deklariert. Zur Demonstration zeigen wir Ihnen nachfolgend ein einfaches Listing. Dieses ermittelt die Genauigkeit der Dezimalziffern aus den Konstanten FLT_DIG (für float), DBL_DIG (für double) und LDBL_DIG (für long double), die im Header float.h deklariert sind:
00 // Kapitel3/float_digits.c
01 #include <stdio.h>
02 #include <float.h>
03 int main(void) {
04 printf("float Genauigkeit : %d\n", FLT_DIG);
05 printf("double Genauigkeit : %d\n", DBL_DIG);
06 printf("long double Genauigkeit: %d\n", LDBL_DIG);
07 return 0;
08 }
Auf unserem PC sieht die Ausgabe des Programms so aus:
float Genauigkeit : 6
double Genauigkeit : 15
long double Genauigkeit: 15
3.9.3 Integer-Typen mit fester Größe verwenden
Wenn Sie sich nicht auf die implementierungsabhängigen Größen der Basis-Integer-Typen auf den verschiedenen Systemen verlassen wollen oder können bzw. einen Integer-Typ mit einer vorgegebenen Breite benötigen, finden Sie seit C99 entsprechende Typen in der Header-Datei stdint.h definiert. Mit »vorgegebener Breite« ist die Anzahl der Bits zur Darstellung des Wertes gemeint. Die speziellen Formatierungsspezifizierer für die printf- und scanf-Familien hingegen finden Sie in der Header-Datei inttypes.h. Die einzelnen Typen können Sie hierbei in folgende Gruppen aufteilen (N steht für die Anzahl von Bits, und Typen mit u (unsigned) sind vorzeichenlos):
-
intN_t und uintN_t: ein Integer-Typ mit einer Breite von exakt N Bits wie beispielsweise int64_t bzw. uint64_t für einen Integer-Typ mit 64 Bit Breite. Entsprechend den Typen finden Sie in der Header-Datei stdint.h auch die zugehörigen Limits für die minimalen und maximalen Werte mit INTN_MIN und INTN_MAX bzw. UINTN_MAX.
-
int_leastN_t und uint_leastN_t: ein Integer-Typ mit einer Breite von mindestens N Bits. Er ist damit garantiert der kleinste Typ der Implementation. Auch dazu finden Sie mit INT_LEASTN_MIN und INT_LEASTN_MAX bzw. UINT_LEASTN_MAX entsprechende Limits für die minimalen bzw. maximalen Werte. N kann hierbei 8, 16, 32 oder 64 sein.
-
int_fastN_t und uint_fastN_t: ein schneller Integer-Typ mit einer Breite von mindestens N Bits. Dieser Typ ist garantiert der schnellste Integer-Typ in der Implementation. Auch hierzu finden Sie mit INT_FASTN_MIN und INT_FASTN_MAX bzw. UINT_FASTN_MAX entsprechende Limits für die minimalen bzw. maximalen Werte. N kann hierbei 8, 16, 32 oder 64 sein.
-
intmax_t und uintmax_t: der garantiert größtmögliche Integer-Typ der Implementation. Den minimalen und maximalen Wert können Sie mit INTMAX_MIN und INTMAX_MAX bzw. UINTMAX_MAX ermitteln.
Wenn Sie die Header-Datei stdint.h eingebunden haben, können Sie diese Integer-Typen mit fester Bitbreite genauso einsetzen und verwenden wie die Integer-Typen ohne feste Größe:
00 // Kapitel3/fixed_ints.c
01 #include <stdio.h>
02 #include <stdint.h>
03 int main(void) {
04 int64_t bigVar = 12345678;
05 printf("bigVar : %lld\n", bigVar);
06 printf("sizeof(int64_t) : %zu\n", sizeof(int64_t));
07 printf("INT64_MAX : %lld\n", INT64_MAX);
08 return 0;
09 }
Das Beispiel gibt bei der Ausführung Folgendes aus:
bigVar : 12345678
sizeof(int64_t) : 8
INT64_MAX : 9223372036854775807
3.9.4 Sicherheit beim Kompilieren mit static_assert
Mit der C11-Funktion static_assert() überprüfen Sie einen konstanten Ausdruck zwischen den Klammern zur Übersetzungszeit. Trifft die Auswertung des Ausdrucks nicht zu, bricht der Compiler mit einer Fehlermeldung ab, die Sie ebenfalls mit angeben können. Auch hier wurde mit static_assert() ein komfortables Makro definiert, um nicht die Schreibweise mit dem vorangestellten Unterstrich verwenden zu müssen. Damit Sie diese Funktion verwenden können, müssen Sie die Header-Datei assert.h einbinden. Ein einfaches Beispiel hierzu ist:
// Kapitel3/static_assert_1.c
#include <assert.h>
…
static_assert( sizeof(long double) == 16,
"Need 16 byte long double" );
Hier fordern wir den Compiler auf, den Ausdruck sizeof(long double) == 16 zu überprüfen. Unsere Anwendung erfordert 16 Bytes für ein long double auf dem System, auf dem der Quellcode übersetzt wird. Ist der Ausdruck wahr, wird der Quellcode weiter übersetzt. Ist der Ausdruck hingegen falsch, bricht der Compiler die Übersetzung ab und gibt die dahinter angegebene Fehlermeldung aus (hier: Need 16 byte long double).
Wollen Sie beispielsweise sichergehen, dass Ihr Programm nicht auf einem System übersetzt wird, auf dem unsigned char mehr als 8 Bit hat, können Sie dies mit static_assert() folgendermaßen umsetzen:
// Kapitel3/static_assert_2.c
#include <assert.h> // für static_assert
#include <limits.h> // für CHAR_BIT
…
static_assert( CHAR_BIT == 8,
"unsigned char hat hier nicht 8 Bit!" );
Die Verwendung von static_assert() empfehlen wir besonders für größere Projekte. Sie kostet überhaupt keine Laufzeit der Anwendung, weil in diesem Fall die Sicherheitschecks nur vom Compiler benutzt werden. Lediglich die Übersetzungszeit nimmt logischerweise zu. Aber wir denken, dass man damit leben kann, weil hiermit auf so manche Sicherheitsüberprüfung während der Laufzeit des Programms verzichtet werden kann.
Wahrscheinlich werden Sie als Einsteiger bzw. Einsteigerin kaum mit static_assert() in Berührung kommen, und wenn Sie an dieser Stelle nicht alles genau verstanden haben, können wir Sie beruhigen: Sie benötigen static_assert() nicht zwingend, ein guter und vor allem übersichtlicher Programmierstil tut es auch.