3.9 Wertebereiche der Datentypen ermitteln
Der 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.
Eine Möglichkeit dabei ist, dass der Datentyp int so festgelegt wird, dass seine Größe der Datenwortgröße des Prozessors entspricht. Das handhaben aber nicht alle Compiler so, und es gibt auch andere Schemas. 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 es die Prozessorarchitektur zulässt. Auf modernen Betriebssystemen ist die Speicherverwaltung sowieso eine spezielle Angelegenheit und so 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 werden, ehe Sie sich dieses Buch zulegen, sonst verstehen Sie keine einzige Zeile.
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 |
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 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 finden Sie beispielsweise 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 im Programm inkludieren. Inkludieren ist das C-Fachwort für »eine Header-Datei mit #include einfügen« und kommt aus dem Lateinischen (includere = 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 }
Die Ausgabe des Programms hängt natürlich von der Implementierung ab. Bei uns sieht sie unter Windows 10/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 auch hier ist nicht das Committee Draft des C11-Standard unter http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf zu vergessen. 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/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 Formatierungs-Spezifizierer für die printf()- und scanf()-Familien finden Sie hingegen 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 einer Breite 64 Bit. 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, 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 Implementierung 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, 64 sein.
-
intmax_t und uintmax_t: der garantiert größtmögliche Integer-Typ der Implementierung 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 neuen 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. Es folgt ein einfaches Beispiel hierzu:
// 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«). Die Fehlermeldung ist hier genau der Text, den Sie als zweiten Parameter an _static_assert() übergeben haben, und Sie können durchaus auch Ihren eigenen Text eingeben- etwa »Hey Du Nase, in Zeile 4711 stimmt was nicht!« Es hat sich aber eingebürgert, aussagekräftige Fehlermeldungen in englischer Sprache zu verwenden.
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 dagegen als Einsteiger 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.