Programmieren in C/C++: Datentypen


Die Programmiersprache C kennt nachfolgende grundlegende Datentypen und Zeiger auf diese Datentypen (siehe auch Grundlagen:Grundlegende Datentypen und Typsicherheit)

Datentyp Bsp. für Datentyp Bsp. für Zeiger auf Datentyp
Ganzzahl
          char  var;
           int  var;
     short int  var;
     long  int  var;
long long  int  var;
           char *ptr;
           int  *ptr;
     short int  *ptr;
     long  int  *ptr;
long long  int  *ptr;
Gleitkommazahl
     double var;
     float  var;
long double var;
     double *ptr;
     float  *ptr;
long double *ptr;
Funktionen
void func(void) {}
void (*pfunc)(void);
Arrays
int   arr[10];
float arr[3][3];
int   (*parr);
float (*parr)[3];
void (*pfunc[5])(void);
Strukturen/Unions
struct xyz {int x,y,z};
union abc  {int a,b,c};
struct xyz *ptr;
union abc  *ptr;
Aufzählungstyp
enum abc {A,B,C};
enum abc *ptr;
Boolescher Datentyp
_Bool var;
_Bool *ptr;
Komplexe Zahlen
_Complex var;
_Complex *ptr;

Bis auf die Datentypen Funktionen, Array und die Zeiger sollen die wesentlichen Aspekte dieser Datentypen im nachfolgenden beschrieben werden.

Der grundlegende Datentyp ist 'int'!

char var1=10,var2=20,var3;
var3=var1+var2; //Addition findet auf Basis des Datentyps int statt
  • Es gibt zwar den Booleschen Datentyp, dieser entspricht jedoch einem Ganzzahldatentyp und true und false entsprechen den Integerwerten 0 und 1
  • Der Aufzählungsdatentyp und seine Aufzählungselemente sind vom Datentyp 'int'
  • Bis einschl. C90 wurde bei der Definition von Variablen ohne Angabe des Datentyps, bei Definition einer Funktion ohne Angabe des Datentyps der Übergabeparameter, des Rückgabedatentyps der Datentyp implizit auf int gesetzt (implizit int)
static var2=2;
foo(par1,par2){
  auto var1;
  var1=par1+par2;
  return var1;
}

Öffnen im Compiler Explorer

Ganzzahl Datentypen

Bearbeiten

Grundlegend kennt die Programmiersprache C nur zwei Ganzzahldatentypen:

  • Datentyp: int
Vorzeichenbehafteter (signed) Datentyp, dessen Bitbreite von der Rechnerarchitektur und des Compilers abhängig ist
  • Datentyp char
Datentyp mit einer Mindestbreite von 8-Bit u.A. zur Speicherung von Zeichen typischerweise im ASCII Format. Die C-Spezifikation [C11 6.2.5 Types] lässt es dem Compiler frei, ob char als vorzeichenbehafteter oder vorzeichenloser Datentyp implementiert wird.
Ergänzend kann der Datentyp char auch zur Speicherung von ganzen Zahlen genutzt werden. In diesem Fall empfiehlt sich, diese explizit als signed und unsiged zu definieren. Wenn nur ASCII Zeichen gespeichert werden, so ist diese Angabe nicht nötig!

Optional können Qualifier dem Datentyp vorangestellt werden:

  • unsigned / signed zur Angabe, ob der Inhalt als vorzeichenlose Zahl oder als vorzeichenbehaftete Zahl zu interpretieren ist. Ohne Angabe sind Variablen vom Datentyp int vorzeichenbehaftet
  • short / long / long long (letzteres seit C99) als vorangestellten Qualifier zu int zur Vergrößerung/Verkleinerung der Bitbreite. Bei Angabe des Qualifiers ist das Schlüsselwort int optional. Die Beschreibung long long int und long long sind gleichbedeutend!

Als Bitbreite für die Datentypen ist im C-Standard nur ein Mindestbreite definiert. Die genaue Bitbreite hängt von der Rechenbreite des Systems und vom Compiler ab (siehe [C11 6.2.5 Types]):

Datentype Mindest
breite
Typisch bei 32-Bit
Architektur
Typisch bei 64-Bit
Architektur
printf Format
Anweisung
char 8 8 8 %c
signed char
unsigned char
8 8 8 %hhd
%hhu
short
short int
16 16 16 %hd / %hu
int 16 16/32 32 %d / %u
long
long int
32 32 32/64 %ld / %lu
long long
long long int
64 64 64 %lld / %llu

In der Header-Datei limits.h sind die tatsächlichen Grenzwerte der Datentypen gespeichert:

Konstante Beschreibung Typ. bei 64-Bit Architektur
CHAR_BIT Anzahl der Bits in einem Char 8
SCHAR_MIN min. Wert, den der Typ bei signed char aufnehmen kann -128
SCHAR_MAX max. Wert, den der Typ bei signed char aufnehmen kann 127
UCHAR_MAX max. Wert, den der Typ bei unsigned char aufnehmen kann 255
CHAR_MIN min. Wert, den der Typ char aufnehmen kann 0 oder SCHAR_MIN
CHAR_MAX max. Wert, den der Typ char aufnehmen kann SCHAR_MAX oder UCHAR_MAX
SHRT_MIN min. Wert, den der Typ short int annehmen kann -32.768
SHRT_MAX max. Wert, den der Typ short int annehmen kann 32.767
USHRT_MAX max. Wert, den der Typ unsigned short int annehmen kann 65.535
INT_MIN min. Wert, den der Typ int annehmen kann -2.147.483.648
INT_MAX max. Wert, den der Typ int annehmen kann 2.147.483.647
UINT_MAX max. Wert, den der Typ unsigned int aufnehmen kann 4.294.967.296
LONG_MIN min. Wert, den der Typ long int annehmen kann -2.147.483.648 oder
-9.223.372.036.854.775.808
LONG_MAX max. Wert, den der Typ long int annehmen kann 2.147.483.647 oder
9.223.372.036.854.775.807
ULONG_MAX max. Wert, den der Typ unsigned long int annehmen kann 4.294.967.296 oder
18.446.744.073.709.551.616
LLONG_MIN min. Wert, den der Typ long long int annehmen kann -9.223.372.036.854.775.808
LLONG_MAX max. Wert, den der Typ long long int annehmen kann 9.223.372.036.854.775.807
ULLONG_MAX max. Wert, den der Typ unsigned long long int annehmen kann 18.446.744.073.709.551.615

Aufgrund dessen, dass die Breite der Datentypen von der Rechnerarchitektur und vom Compiler abhängig ist, sind in der Header-Datei stdint.h Aliase für Datentypen enthalten, welche in ihrem Alias die tatsächliche Breite und das Vorzeichens beinhalten:

uint8_t    -->  unsigned  8-Bit
 int8_t    -->    signed  8-Bit
uint16_t   -->  unsigned 16-Bit
 int16_t   -->    signed 16-Bit
	...

Für C++ sehen diese Datentypen wie folgt aus:

std::intptr_t
std::int8_t    std::uint8_t
std::int16_t   std::uint16_t
std::int32_t   std::uint32_t
Std::int64_t   std::uint64_t

Zur Erzeugung von portablen/rechnerunabhängigen Programmen empfiehlt sich, diese Datentypen zu nutzen. Die Rechenbreite ist dann unabhängig vom Compiler und Betriebssystem.

Gleitkomma Datentypen

Bearbeiten

Gleitkommazahlen werden im Computer auf Basis folgender Schreibweise dargestellt (siehe auch:   Gleitkommazahl):

             Dezimalsystem                 Dualsystem
12,3456710 = 1234567 * 10^-5       0,110 = 1,1001100110011 * 2^-4
             -------      --               ---------------     --
             Mantisse  Exponent            Mantisse         Exponent
Mantisse
Vorzeichenlose ganze Zahl (ggf. mit einer festen Position des Dezimalpunktes=Festkommazahl)
Exponent
Vorzeichenbehaftete ganze Zahl, welche um einen Bias verschoben ist
Vorzeichen
Vorzeichen der (vorzeichenlosen) Mantisse

Zur Speicherung im Computer werden das Vorzeichen, die Mantisse und der Exponent getrennt im Binärformat gespeichert, für den Programmierer aber als eine (zusammenhängende) Zahl dargestellt:

 
Bitweise Darstellung einer Gleitkommazahl, aufgeteilt in Vorzeichen, Exponent und Mantisse

Gleitkommazahlen haben genauso wie ganze Zahlen einen beschränkten Wertebereich:

  • Die Anzahl der Bits der Mantisse bestimmen die Anzahl der Nachkommastellen:
Bei 23-Bit Mantisse hat das niederwertigste Bit die Wertigkeit:
2^-22=1/2^22=1/(2^10*2^10*2^2)=1/(1024*1024*4)1/(4.000.000)=0,000.000.25
 Es könnten also nur 7..8 dezimale Nachkommastellen gespeichert werden
  • Mit dem Exponent kann 'quasi' der kleinste und der größte Wertebereich angegeben werden:
Bei 8-Bit Exponent mit einem Bias von 127 liegt der Wertebereich des
Exponenten im Bereich von -126  +127 (Exponent -127 und +127 wird zur 
Darstellung weitere Zahlen benötigt).
Die kleinste darstellbare Zahl (unter Vernachlässigung der Nachkommastellen der Mantisse ist:
2^-126=1/2^1261,175*10^-38
Die größte darstellbare Zahl ist:
2^127         1,7*10^38

Öffnen im Compiler Explorer

Die genaue Darstellung (Anzahl der Bits für Mantisse und Exponent, Darstellung von NAN/INF, ….) ist in C nicht spezifiziert und somit compiler- und rechnerabhängig. Zumeist wird mittlerweile die   IEEE 754 Standardisierung genutzt (Norm wurde von Intel mit der Entwicklung der 8087 FPU entworfen).

Grunddatentypen und der Wertebereich (nach   IEEE 754)

Daten-
type
Speicher-
platz
Exponent Mantisse Größte Zahl Kleinste Zahl Genauigkeit printf Format Anweisung
float 4 Byte 8 bit 23 bit +-3,4*1038 1,2*10-38 6 Stellen ---
double 8 Byte 11 bit 52 bit +-1,7*10308 2,3*10-308 12 Stellen %f %e %g
long double 10 Byte ≥8 bit ≥ 63 bit +-1,1*104932 3,4*10-4932 18 Stellen %Lf %Le %Lg

Aufgrund des nicht standardisierten Formates sollte ein Gleitkommadatentyp nicht im Binärformat für den Datenaustausch (mit Netzwerk, über Dateien, ..) genutzt werden. Andere Rechnersysteme/Programmiersprachen würde aufgrund der anderen Interpretation andere Zahlenwerte aus den übertragenen/gespeicherten Binärdaten auslesen!

float var1;
write(file_hdl,&var1,sizeof var1);   //Schreiben des Binärwertes
                                     //in eine Datei
double var2;
send(socket_hdl,&var2,sizeof var2,0);//Senden des Binärwertes
                                     //über ein Netzwerk

Die Standard-C-Library bietet einige mathematische Funktionen wie sin() / cos() / tan() / atan2() / sqrt() / pow() / exp()/… an. Die Prototypen dieser Funktion sind in der Header-Datei math.h beschrieben, so dass diese bei Nutzung dieser Funktion zu inkludieren ist. Ergänzend ist die Shared Library libm.so über den Compilerschalter '-lm' einzubinden:

#include <math.h>
//Math-Library libm.so mittels Linker-Command '-lm' einbinden
int main(int argc,char *argv[]) {
  double var1=47.11;
  double var2=sqrt(var1);

Öffnen im Compiler Explorer

Die Funktionen basieren auf den Datentyp double, d.h. sowohl der Übergabewert als auch der Rückgabewert ist vom Datentyp double. Die Prototypen sehen wie folgt aus:

double sin(double);
double cos(double);
double tan(double);
double asin(double);
double acos(double);
double atan(double);          //Wertebereich der Rückgabe von -PI/2..+PI/2
double atan2(double,double);  //Wertebereich der Rückgabe von -Pi .. +PI
double sqrt(double);
double log(double);

Über den Suffix f oder l im Funktionsnamen kann der zugrundeliegende Datentyp auf float (Suffix f) oder long double (Suffix l) geändert werden:

float       sinf(float);
long double sinl(long double);

Ergänzend zu den Prototypen sind die gebräuchlichen Naturkonstanten in math.h definiert (siehe auch: math.h):

M_E         Value of e
M_LOG2E     Value of log_2 e
M_LOG10E    Value of log_10 e
M_LN2       Value of log_e 2
M_LN10      Value of log_e 10
M_PI        Value of π
M_PI_2      Value of π/2
M_PI_4      Value of π/4
M_1_PI      Value of 1/π
M_2_PI      Value of 2/π
M_2_SQRTPI  Value of 2/π
M_SQRT2     Value of 2
M_SQRT1_2   Value of 1/2

Gleitkommazahlen können nicht nur Zahlenwerte, sondern auch Sonderwerte annehmen. Diese Sonderwerte sind ebenfalls in math.h beschrieben (siehe auch: math.h):

INFINITY  A constant expression of type float representing
          positive or unsigned infinity, if available; else a
          positive constant of type float that overflows at
          translation time.
NAN       A constant expression of type float representing a
          quiet NaN. This macro is only defined if the
          implementation supports quiet NaNs for the float type.

Mit fesetround()/fegetround() kann gesetzt/gelesen werden, wie mit Ergebnissen umzugehen sind, die nicht exakt darstellbar sind. Mögliche Werte sind 'round to nearest' (default), 'round up', 'round down' und 'round toward zero'.

Komplexe Zahlen

Bearbeiten

Nach dem Wikipedia Artikel   komplexe Zahlen stellen komplexe Zahlen eine Erweiterung der reellen Zahlen dar. Ziel der Erweiterung ist es, algebraische Gleichungen wie x2+1=0 bzw. x2=-1 lösbar zu machen. ... Da die Quadrate aller reellen Zahlen größer oder gleich 0 sind, kann die Lösung der Gleichung x2=-1 keine reelle Zahl sein. Man braucht eine ganz neue Zahl, die man üblicherweise i nennt, mit der Eigenschaft i2=-1. Diese Zahl i wird als imaginäre Einheit bezeichnet. Komplexe Zahlen werden nun als Summe a+b*i definiert, wobei a und b reelle Zahlen sind und i die oben definierte imaginäre Einheit ist.
Der Datentyp für komplexe Zahlen (erst ab C99 enthalten) beinhaltet folglich zwei Gleitkommazahlen, eine für den reelen Teil und eine für den imaginären Teil. Entsprechend den Gleitkommazahlen steht der komplexe Datentyp ebenfalls in drei unterschiedlichen Genauigkeiten zur Verfügung:

Datentype Speicherplatz
float _Complex 2x4Byte
double _Complex
_Complex
2x8Byte
long double _Complex 2x10Byte

Wie auch beim Booleschen Datentyp ist in der Header-Datei complex.h für den einfacheren Umgang mit diesem Datentyp das Makro complex als Textersetzung für _Complex gesetzt.

Zur Darstellung von komplexen Konstanten wurde die Gleitkommakonstante um den Suffix i ergänzt. Durch Anhängen von i wird aus dem ansonsten rellen Teil der imaginäre Teil und damit aus der Gleitkommakonstante eine komplexe Gleitkommakonstante:

_Complex varc1=2i;
_Complex varc2=1+2i;
printf("%f %f\n",creal(varc1),cimag(varc1));
printf("%f %f\n",creal(varc2),cimag(varc2));

Öffnen im Compiler Explorer

Normale Gleitkommavariablen werden als reellen Teil einer komplexen Zahl angesehen. Bei Operationen von Gleitkommazahlen und komplexen Zahlen wird der imaginäre Teil der Gleitkommazahl auf 0 gesetzt. Beim Zuweisen einer komplexen Zahl an eine Gleitkommazahl wird nur der reele Teil 'gelesen'.
Soll eine Gleitkommavariablen zu einem imaginären Teil gewandelt werden, so muss diese bspw mit der Konstanten 1.0i multipliziert werden:

double vard1=2i;       //Vorsicht, es wird nur der reele Teil
printf("%f\n",vard1);  //der komplexen Zahl in vard1 gespeichert!
_Complex varc1=1+2i;
         vard1=1;
varc1=varc1+vard1;
printf("%f %f\n",creal(varc1),cimag(varc1));
varc1=varc1+vard1*1.0i;
printf("%f %f\n",creal(varc1),cimag(varc1));

Öffnen im Compiler Explorer

Die Standard-C-Library bietet diverse mathematische Funktionen wie csin(), ccos(), csqrt(), cpow() und ergänzend Umrechnungsfunktionen von Gleitkommazahlen in komplexe Zahlen (und umgedreht) wie creal(),cimag() ,cabs() für den Umgang mit komplexen Zahlen an:

double _Complex csin(double _Complex);
double _Complex csqrt(double _Complex);
double          creal(double _Complex);
double          cimag(double _Complex);

Zur Nutzung dieser Funktionen muss die Header-Datei complex.h inkludiert und ergänzend die Shared Library libm.so über den Compilerschalter '-lm' eingebunden werden.

#include <complex.h>
//Math-Library mittels Linker-Command '-lm' einbinden
int main(int argc, char *argv[]) {
  double _Complex xyz=6+2i;  //Reeler-Teil=6  Imaginärer Teil=2
  printf("Reeller Teil: %f Imaginärer Teil:%f",creal(xyz),cimag(xyz));

Öffnen im Compiler Explorer

Die Funktionen beruhen auf den Datentyp double _Complex, d.h. sowohl der Übergabewert als auch der Rückgabewert ist double _Complex resp. double (bei den Umrechnungsfunktionen). Über den Suffix f und l im Funktionsnamen kann der zugrundeliegende Datentyp geändert werden:

float       crealf(float _Complex z);
long double creall(long double _Complex z);

Hinweis:

  • Der Datentyp _Complex und die dazugehörigen mathematischen Funktionen sind in der C-Spezifikation als optional gekennzeichnet.

Generische mathematische Funktionen

Bearbeiten

In der tgmath.h Header Datei sind für die mathematischen Funktionen Makros enthalten, welche den Datentyp der Übergabeparameter bestimmen und basierend auf den Datentyp die dazugehörige mathematische Funktion aufrufen.

tgmath.h selbst inkludiert math.h und complex.h, so dass diese nicht weiter zu inkludieren sind.

Steht für den Übergabedatentyp keine passende Funktion zu Verfügung, so ist das Verhalten des Makros undefiniert (bspw. wenn ein komplexes Argument an eine Funktion übergeben wird, die nur für Gleitkommazahlen ausgelegt ist ceil(1+2i); )

Folgendes Regelwerk wird angewendet:

  • Ist einer der Übergabeparameter imaginär, so ist die ausgewählte Funktion in der Spezifikation der Funktion beschrieben
  • Ist einer der Übergabeparameter Komplexe, so wird die Komplexe Variante der Funktion aufgerufen, andernfalls die Gleitkomma Variante
  • Ist einer der Übergabeparameter vom Datentyp long double, so wird die long double Variante, bei integer und double die double Variante und andernfalls die float Variante aufgerufen

Beispiel:

#include <tgmath.h>
//Math-Library libm.so mittels Linker-Command '-lm' einbinden
long double foo(long double complex parlc, 
                long double         parld, 
                     float          parf, 
                     int            pari) {
  long double ret;
  ret =(long double) sqrt(parlc); //Aufruf von csqrtl()
  ret+=              sqrt(parld); //Aufruf von  sqrtl()
  ret+=(long double) sqrt(parf);  //Aufruf von  sqrtf()
  ret+=              sqrt(pari);  //Aufruf von  sqrt()
  return ret;
}

Öffnen im Compiler Explorer

Boolschescher Datentyp

Bearbeiten

Siehe Grundlagen: Boolescher-Datentype/Operatoren

void / unvollständiger Datentype

Bearbeiten

Der Datentyp void dient vorrangig dazu, nicht vorhandene Über- und Rückgabewerte von Funktionen anzuzeigen. Eine Variable vom Datentyp void kann nicht angelegt werden. Ein Zeiger vom Datentyp void ist möglich, kann aber nicht dereferenziert werden (siehe Kap. Zeiger:Void-Zeiger)

void func(void); //Void zur Darstellung der nicht vorhandenen Parameter und
                 //des nicht vorhandenen Rückgabewertes
void var;        //Eine Variable vom Datentyp void kann nicht angelegt werden
void *ptr;       //Void-Zeiger

Öffnen im Compiler Explorer

Hinweise:

  • sizeof(void) ist nach der C-Spezifikation nicht erlaubt. Dennoch geben viele Compiler hier den Wert 1 zurück!
  • Mit einem expliziten Cast auf den Datentyp void wird dem Compiler mitgeteilt, dass diese Variable in Gebrauch ist, der Wert in dieser Operation aber nicht genutzt wird. Hierüber kann die Compilerwarning 'unused Variable' unterbunden werden:
void func(int par1, int par2) {
  int var1=12;
  int var2=13;
  (void)par2;     //Angabe, dass diese Variable genutzt wird.
  (void)var2;     //Angabe, dass diese Variable genutzt wird.
  if(var1==par1) //var1 und par1 werden verwendet

Öffnen im Compiler Explorer

Datentypkonvertierung

Bearbeiten

Die Abarbeitungsreihenfolge der Operatoren wird durch die Prioritätenliste/Rangfolge (siehe C-Programmierung:Liste der Operatoren nach Priorität) vorgegeben. Operatoren mit höherer Priorität werden vor Operatoren mit niedriger Priorität ausgeführt:

//Der Ausdruck
c = sizeof(x) +  ++a / 3;  
//wird aufgrund der Prioritäten wie folgt ausgewertet:
c= (sizeof(x)) + ( (++a) / 3);

Öffnen im Compiler Explorer

Bei identischer Priorität ergibt sich die Abarbeitungsreihenfolge aus der Assoziativität (L-R oder R-L)

a=33 / 5 / 2;
//Wird aufgrund der Assoziativität wie folgt ausgewertet.
//a= (33 / 5) / 2;
//und damit zu 3 und nicht zu 16 (bei 33 / (5/2)) ausgewertet. 
a = b = c = d*2;  //→ a=(b=(c=(d*2)));
a = b = 1+c = d;  //→ a=(b=((1+c)=d)); //Compilerfehler, da 
                                       //1+c kein lvalue ist

Öffnen im Compiler Explorer

Für das Rechnen/Vergleichen müssen beide Operatoren vom identischen Datentyp sein! Sind diese nicht identisch, so müssen die Datentypen 'angeglichen' werden. Dies kann einerseits ‚automatisch‘ mittels 'implizierter' Typumwandlung oder ‚manuell‘ mittels ‚expliziter' Typumwandlung erfolgen.
Beim Zuweisungsoperator (inkl. Parameterzuweisung bei Funktionsaufrufen und Funktionsrückgabewerte) gilt dies ebenso, nur dass hier der Quelldatentyp an den Zieldatentyp angepasst werden muss. Die impliziten Regeln finden hier keine Anwendung.

Hinweis:

  • Empfehlenswert ist, die Datentypen der Variablen so zu wählen, dass der Compiler keine implizite Typumwandlung tätigt. Kann dies nicht vermieden werden, so sollte die explizite Typumwandlung genutzt werden (um sich einen möglichen Datenverlust bewusst zu machen).
  • Der Datentyp selbst sollte den möglichen Wertebereich der Variablen entsprechen und nicht unnötig groß gewählt werden.
Datentyp zur Speicherung der Stundenzeit: 
	Wertebereich: 0 ... 23  -> unsigned char
Datentyp zur Speicherung einer Jahreszahl:  
	Wertebereich: 2000 v.C. ... 4000 n.C -> signed short
Datentyp zur Speicherung einer Temperatur: 
	Wertebreich: -273,0°C ... 2000,0°C  -> float

Implizite Typumwandlung

Bearbeiten

Diese Regeln wurden so aufgestellt, dass dabei stets ein Datentyp in einen anderen Datentyp mit höherem Rang umgewandelt wird (Rangordnung: long double, double, float, long long, long, int) [C11 6.3.1.8] .

Nach [Harbison: S. 199]
Regel/
Priorität
If either operand
has Type
And the other operand
has Type
Converts both to
1 long double any real type long double
2 double any real type double
3 float any real type float
4 any unsigned type any unsigned type The unsigned type
with the greater rank
5 any signed type any signed type The signed type
with the greater rank
6 any unsigned type a signed type of greater rank that can
represent all vaues of the unsigned type
The signed type
7 any unsigned type a signed type of greater rank that cannot
represent all values ot the unsigned type
The unsigned version
the the signed type
8 any other type any other type No conversion

Ergänzend gilt das Regelwerk zu Integer Promotion [C11 6.3.1.1], welche Datentypen kleiner als signed int zu int und kleiner als unsigned int und unsigned int konvertiert (Compiler kann hiervon abweichen, wenn sichergestellt ist, dass kein Datenverlust eintritt).

Regel 6 und 7 sind der nicht klar definierten Bitbreite der ganzzahligen Datentypen geschuldet und versuchen, Konvertierungsverluste zu vermeiden. Sie lesen sich zunächst kryptisch, lassen sich aber einfach am folgenden Beispiel erklären:

Operand 1: unsigned int (hier 32-Bit)
Operand 2: long         (hier 32-Bit  / 64-Bit)
  • Im Fall, dass der Datentyp long 64-Bit breit ist, kann dieser problemlos die vorzeichenlose 32-Bit Zahl ohne Konvertierungsverluste repräsentieren, so dass der Zieltype signed long ist (Regel 6)
  • Im Fall, dass der Datentyp long 32-Bit breit ist, kann dieser mit seinen Wertebereich von -2.147.483.648 bis +2.147.483.647 nicht den vorzeichenlosen Zahlenbereich von 0…4.294.967.295 darstellen. In diesem Fall hat der unsigned Datentyp einen höheren Rang, so dass als Zieltyp unsigend long gewählt wird(Regel 7)

Regel 7 bedeutet die Gefahr eines Konvertierungsverlustes, dessen man sich bewusst sein sollte! Diese tritt insb. dann in Kraft, wenn ein Operand vom Typ 'unsigned long long' ist (64-Bit).

Für die Konvertierung von Pointer, Arrays, Strukturen, Unions zu anderen Datentypen, als sich selbst gilt Regel 8. In diesem Fall gibt der Compiler zumeist einen Error, in wenigen Ausnahmefällen eine Warning aus:

char *string;             //Pointer
int   arr[3];             //Array
struct {int x,y;} var1;   //Strukturen
union  {int x,y;} var2;   //Union
var1 = var2;    //Error Incompatible Types
string=arr;     //Error Incompatible Types
arr=var1;       //Error Assignment to expression with array type
                //(=incompatible Types)
string=var2;    //Error Incompatbiles Types

Öffnen im Compiler Explorer

Beispiele von impliziten Typumwandlungen:

char  varc=100;
short vars=100;
int   vari=100;
vars=varc + vars;
//Wird aufgrund der impliziten Regel wie folgt umgesetzt
vars=(short)((int)varc+(int)vars);

vari=5.0*(int)sin(vars);
//Wird aufgrund der impliziten Regel wie folgt umgesetzt
vari=(int)(5.0*(double)(int)sin((double)vars));
//Hinweis: da sin() nur Werte im Bereich -1…0…+1 zurückgibt kommen
//als Werte für a hier nur 5, 0 und -5 in Frage!

Öffnen im Compiler Explorer

Hinweis:

  • Im CompilerExplorer können sie sich die impliziten Typumwandlungen anzeigen lassen. Dazu gehen sie bitte wie folgt vor:
  • Im Source-Fenster mit '+Add new' ein Compiler Fenster öffnen
  • Im Compiler-Fenster mit '+Add new' ein 'GCC Tree/RTL' Fenster öffnen
  • Im GCC Tree/RTL-Fenster unter 'Select a pass…' 'original tree' auswählen


Um implizite Typumwandlung besser erkennen zu können, wird oftmals bei Variablennamen die 'Ungarische Notation' angewendet (siehe   Ungarische Notation). Aufgrund der besonderen Namensgebung kann der Programmierer ohne großen Aufwand frühzeitig mögliche Typkonflikte erkennen. Der Variablenname setzt sich wie folgt zusammen:

{Präfix}{Datentyp}{Bezeichner}
Präfix: p->Pointer h->Handle i->index c->count f->flag rg->Array
Datentyp: ch->Character st->string w->word b->byte…
Bezeichner: Zum Binden der Variable an eine konkrete Aufgabe (keine Unterstriche).
Bezeichner ist optional, wenn aus Präfix und Datentyp die 'Aufgabe' der Variable direkt sichtbar ist

Beispiele:

char  rgchtemp[10];   //Array (Range) vom Datentyp character
int   ich;            //Index zum Adressieren eines Arrays vom Datentyp
                      //character

Explizite Typumwandlung

Bearbeiten

Programmierer erzwingt durch explizite Typumwandlung (auch CASTen genannt) eine Umwandlung eines Datentyps in einen anderen. Dazu wird der Zieldatentyp in runden Klammern vor Quelldatentype geschrieben:

short a=4;
double b=(double)(a+1);   //Das Ergebnis von (a+1) wird nach double gecastet
                          //(Addition erfolgt auf Basis des Datentyps.
                          //integer)
double c=(double)a+1;     //a wird nach double gecastet, so dass nachfolgende
                          //Addition auf Basis von double basiert.

Öffnen im Compiler Explorer

In der Rangfolge der Operatoren steht der Cast-Operator unterhalb von bspw. Funktionsaufrufen, Arrayzugriffen aber auch der Dereferenzierung:

Priorität Symbol Assoziativität Bedeutung
15 ... ... ...
14

++/-- (Präfix)
+/- (Vorzeichen)
!/~
&
*
(TYP)
...

R-L

Präfix-Inkrement/Dekrement
Vorzeichen
Logisches/Bitweises Nicht
Adresse
Zeigerdereferenzierung
Typumwandlung (Explizites Cast)
...

13
  • / %
L-R Multiplikation/Division/Modulo
12 ... ... ...

Im Zweifel gilt auch hier, den zu wandelnden Typ ergänzend zu klammern.

Hinweis:

  • Bedenke, dass die explizite Typumwandlung so aufgestellt sein sollte, dass der Zieldatentyp dem notwendigen Datentyp entspricht. Andernfalls wendet der Compiler ergänzend eine implizierte Typumwandlung an:
int    a;
double b=4.7;
short  c1= (long)(  (float)a+b );
//b ist vom Typ double, so dass a nach dem Cast auf float 
//   implizit vom Compiler auf double gecastet wird
//c ist vom Typ short, so dass das Ergebnis der Addtion nach dem expliziten 
//Cast auf long auf short gecastet wird.
//Nach Anwendung der impliziten Cast sieht der Ausdruck wie folgt aus:
short c2=(short)(long)( (double)(float)a+4.7);

Öffnen im Compiler Explorer

Mittels expliziter Typumwandlung können Ganzzahl nach Gleitkommazahlen (und andersherum) und Zeiger in einen anderen Zeiger und auf andere Datentypen gewandelt werden. Eine Konvertierung von Arrays, Strukturen, Unions zu anderen Datentypen als sich selbst ist weiterhin nicht möglich:

char *string;                 //Pointer
int arr[3];                   //Array
struct stru {int x,y;} var1;  //Strukturen
union  unio {int x,y;} var2;  //Union

var1 = (struct stru) var2;    //Error Conversion to non-scalar type
string=(char *)arr;           //OK
arr=(int *)var1;              //Error Assignment to expression with array type
string=(char *)var2;          //Error cannot convert to pointer type

Öffnen im Compiler Explorer

Bei den möglichen Konvertierungen sollte Folgendes berücksichtigt werden:

  • Vorzeichenlose GanzzahlVorzeichenlose Ganzzahl: Wenn der Zieldatentyp größer ist, wird die Zahl durch führende '0' erweitert. Wenn der Zieldatentyp kleiner ist, werde die zu vielen Bitstellen abgeschnitten/verworfen (ggf. Datenverlust).
  • Vorzeichenbehaftete GanzzahlVorzeichenbehaftete Ganzzahl: Wenn der Zieldatentyp größer ist, wird vorzeichenrichtig erweitertet (d.h. das Auffüllen erfolgt auf Basis des Vorzeichens). Wenn der Zieldatentyp kleiner ist, werden auch hier die zu vielen Bits abgeschnitten/verworfen (Vorsicht: Negative Zahlen können dabei in positive Zahlen gewandelt werden)
  • GleitkommazahlGleitkommazahl: Hier wird vereinfacht ausgedrückt sowohl die Mantisse als auch der Exponent einzeln kopiert. Wenn die Zielmantisse kleiner ist, gehen 'Nachkommastellen' verloren. Wenn der Quellexponent einen größeren Wert beinhaltet, als der Zielexponent 'aufnehmen' kann, so wird die Zahl auf Unendlich gesetzt. Umgedreht, wenn der Quellexponent kleiner ist, als der Zielexponent 'aufnehmen' kann, so wird die Zahl auf 0 gesetzt
  • GanzzahlGleitkommazahl: Hier wird, vereinfacht ausgedrückt, die Ganzzahl in die Mantisse kopiert. Um Datenverluste zu vermeiden, sollte die Bitbreite der Zielmantisse größer gleich der Bitbreite der Ganzzahl sein
  • GleitkommazahlGanzzahl: Im Wesentlichen wird hier die Mantisse übernommen, so dass hier die Bitbreite des Zieldatentyps mindestens der Bitbreite der Mantisse sein sollte
  • ZeigerGanzzahl: Die Konvertierung ist möglich. Da bei 64-Bit Systemen der Zeiger 64-Bit und Integer 32-Bit breit ist, meckert ggf. der Compiler. Für solche Fälle existieren die Datentypen 'intptr_t' und 'uintptr_t', über welche sichergestellt ist, dass ein Zeiger in einen Ganzzahldatentyp gespeichert werden kann!
  • ZeigerGleitkommazahl: Diese Konvertierung ist nicht möglich!
  • Struktur/UnionGanzzahl/Gleitkommazahl: Diese Konvertierung ist nicht möglich. Es können nur einzelnen Strukturelemente konvertiert werden, sofern diese vom Typ Ganzzahl/Gleitkommazahl sind
  • ArrayGanzzahl/Gleitkommazahl: Der Arrayname entspricht einen Zeiger, so dass hier ein Zeiger in eine Ganzzahl oder umgedreht konvertiert wird (siehe Zeiger ↔ Ganzzahl)

In C++ gibt es weitere CAST-Operatoren, auf welche hier derzeit noch nicht weiter eingegangen wird!

  • Static_cast (Entspricht dem Expliziten Cast)
  • Const_cast
  • Dynamic_cast
  • Reinterpret_cast

Struktur/Verbundtyp

Bearbeiten

Nach dem Wikipedia Artikel   Verbund (Datentyp) ist ein Verbund (englisch object composition) ist ein Datentyp, der aus einem oder mehreren Datentypen zusammengesetzt wurde. Die Komponenten können wiederum Verbünde enthalten, wodurch auch komplexe Datenstrukturen definiert werden können.
Für jedes Strukturelement wird Speicher reserviert. Alle Strukturelemente liegen hintereinander im Speicher.

Syntax: struct StrukturnameOpt {Datentype Strukturelementname; … }OPT VariablenlisteOpt;

Ein Verbund entspricht einer Java Klasse mit dem Unterschied zu C (nicht C++) , dass ein Verbund keine Methoden beinhaltet und keine Zugriffsbeschränkung für die Attribute gesetzt werden können.

Der Syntax erlaubt es, gleichermaßen einen Datentyp (über Strukturname) zu definieren und Variablen von diesem Datentyp (über Variablenliste) anzulegen. Da beide Elemente optional sind, ergeben sich diverse Kombinationsmöglichkeiten:

  • Nur Strukturname → Definition eines neuen Datentyps
struct xyz1 {int x; int y,z;};   //Definition eines neuen Datentyps
struct xyz1 var1;                //Definition einer Variablen dieses
                                 //Datentyps
var1.x=7;                        //Zugriff auf ein Strukturelement

Öffnen im Compiler Explorer

C: Der Strukturname ist nur mit dem vorangestellten struct gültig. Der Strukturname stellt somit einen eigenen Namensraum, getrennt von dem Namensraum der Variablen/Funktionen, dar. Daher kann ein Strukturname identisch zu einem Variablennamen sein:
struct xyz2 {int x,y,z;};      //Definition des Datentyps 'struct xyz2'
struct xyz2 xyz2;              //Definition der Variablen 'xyz2' auf
                               //Basis des Datentyps 'struct xyz2'
xyz2 var2;                     //Compilerfehler, zur Nutzung des Datentyps
                               //'struct xyz2' muss struct vorangestellt 
                               //werden!

Öffnen im Compiler Explorer

C++: Der Strukturname ist sowohl mit als auch ohne dem vorangestellten struct gültig. Auch hier stellt der Strukturname einen eigenen Namensraum dar, der jedoch eine niedrigere Priorität als der der Variablen hat:
struct xyz3 {int x,y,z;};
xyz3 xyz3={1,1,1};        //'xyz3' beschreibt hier den Datentypen
xyz3.x=1;                 //und nach Definition einer Variable die Variable!
xyz3 var2;                //Fehler, xyz beschreibt hier die Variable

Öffnen im Compiler Explorer

  • Nur Variablenliste → Definition einer/mehrerer Variablen von einem 'unnamed' Datentyp
struct {
  int x,y,z;
} xyz,arr[3],*ptr=&xyz;  //Definition von Variablen des Datentyps 
                         //struct {...}. Da dieser Datentyp nicht benannt
                         //wurde, kann im späteren keine weitere Variable
                         //von diesem Datentyp angelegt werden.
                         
xyz.x    =8;             //Nutzung der Variablen
arr[1].y =9;
ptr->x   = arr[1].y;

Öffnen im Compiler Explorer

Angewendet wird die Schreibweise gerne, wenn eine Struktur innerhalb einer Struktur definiert wird und die innere Struktur zur besseren Strukturierung dient:
struct aussen {         //Äußere Struktur
  struct {              //Innere Struktur
    int a,b,c;
  } anwendung1;
  struct {              //Innere Struktur
    int x,y,z;
  } anwendung2;
};
struct aussen var;

var.anwendung1.a=10;    //Nutzung der Variablen
var.anwendung2.y=12;

Öffnen im Compiler Explorer

  • Strukturname und Variablenliste → Definition eines Datentyps und Definition von Variablen. D.h. es können im Nachhinein weitere Variablen von diesem Datentyp angelegt und die hier angelegten Variablen genutzt werden.
struct xyz{                 //Definition eines Datentyps
  int x,y,z;
} var1,*ptr1;               //und Definition von Variablen dieses Datentyps
struct xyz  var2,*ptr2;     //Definition weiterer Variablen dieses Datentyps

var1.x=var2.y;              //Nutzung der Variablen

Öffnen im Compiler Explorer

  • Kein Strukturname und keine Variablenliste → "Anonymous Struct", d.h. Definition eines 'unnamed' Datentyps, von der ergänzend keine Variable angelegt wird. Anonymus Structs sind nur innerhalb von Strukturen erlaubt/einsetzbar. Hier dienen sie der besseren Strukturierung und ersparen dem Programmierer die Benennung des normalerweise notwendigen Strukturelementes:
struct außen {
  int a;
  struct {
    int b,c,d;
  } anwendung1;   //'Normale' innere Struktur
  struct {
    int x,y,z;
  };               //Anonyme Struct als innere Struktur
} var;

var.a=4711;        //Nutzung der Variablen
var.anwendung1.b=1;//Bei Zugriff auf anonyme Struktur ist die Benennung
                   //des Strukturelementes notwendig
var.x=1;           //'Einfacherer' Zugriff auf inneres Element bei 
                   //'anonymous struct'

Öffnen im Compiler Explorer

Hinweise:

Strukturelement

Bearbeiten

Innerhalb der Struktur können beliebige Strukturelemente von beliebigen Datentypen angelegt werden. Einzige Voraussetzung, der Datentyp des anzulegenden Strukturelementes muss zuvor definiert worden sein. Ergänzend kann der neue Datentyp auch in der Struktur selbst definiert werden, sofern mit der Definition des Datentyps auch eine Variable angelegt wird:

struct abc {
  int a,b,c;
};                   //Definition des Datentyps struct abc
struct xyz {         //Definition des Datentyps struct xyz
  int x,y,z;         //Strukturelement vom Type int
  struct abc abc;    //Strukturelement vom Type struct abc
  struct def {       //Definition eines neuen Datentyps innerhalb
    int d,e,f;       //der Struktur bei gleichzeitigen Anlegen zweier
  } def,geh;         //Strukturelementen von diesem neuen Datentyp
  struct uvw {
    int u,v,w;       //Warning, reine Datentypdefinition innerhalb
  };                 //einer Struktur nicht möglich
};
struct def abc;      //Innere Datentypbeschreibung auch
                     //außerhalb der Struktur nutzbar

Öffnen im Compiler Explorer

Zugriff auf Strukturelemente

Bearbeiten

Die Programmiersprache C unterscheidet zwei 'Zugriffsarten' auf die Strukturelemente, abhängig vom zugrundeliegenden Datentyp:

  • Handelt es sich beim zugrundeliegenden Datentyp um eine (struct)Variablen, so erfolgt der Zugriff über den Punkt-Operator .:
struct {
  int a,b,c;
} abc;
abc.a=4711;     //abc ist eine Variable einer Struktur
abc.b=abc.c;

Öffnen im Compiler Explorer

  • Handelt es sich beim zugrundeliegenden Datentyp um einen Zeiger auf eine (struct)Variable, so erfolgt der Zugriff über den Zeiger-Operator ->:
struct xyz {
  int x,y,z;
};
struct xyz  var1;  //Variable von Datentyp struct xyz;
struct xyz *ptr1;  //Zeiger auf eine Struktur vom Datentyp struct xyz
ptr1=&var1;        //Initialisierung des Zeigers 
                   //(mit der Adresse der Variablen var1)
var1.x=  7;        //Zugriff auf das Strukturelement x über die Variable var1
ptr1->x=7;         //Zugriff auf das Strukturelement x über den Zeiger ptr1

(*ptr1).x=7;       //Der Zeiger-Operator entspricht dem Zugriff auf ein
                   //dereferenziertes Strukturelement

Öffnen im Compiler Explorer

Werden innerhalb von Strukturen weitere Strukturen und Zeiger auf Strukturen angelegt, so müssen diese Regeln auf jedes innere Strukturelement einzeln angewendet werden:

struct abc {          //Äußere Struktur
 int a,b,c;
 struct xyz {
   int x,y,z;
  } xyz;              //Inneres Strukturelement xyz vom Datentyp struct xyz
  struct {
    char str[10];
  } strstr;           //Inneres Strukturelement strstr vom Datentyp 
                      //struct strstr
  struct xyz *ptr;    //Inneres Strukturelement ptr vom Datentyp Zeiger 
                      //auf struct xyz
} var2,*ptr;
	
var2.a=8;              //var2 vom Datentyp 'struct abc'. Zugriff über '.'
var2.xyz.x=1;          //xyz vom Datentyp 'struct xyz'. Zugriff über '.'
var2.strstr.str[0]='a';//strstr vom anonymes Struct. Zugriff über '.'

ptr=&var2;            //ptr mit einer Adresse initialisieren
var2.ptr = &var2.xyz; //Strukturelement ptr mit einer Adresse initialisieren

ptr->a='a';           //ptr vom Datentyp 'Zeiger auf struct abc'
                      //Zugriff über '->' 
var2.ptr->x=3;        //ptr vom Datentyp 'Zeiger auf struct xyz'.
                      //Zugriff über '->'
ptr->xyz.x=3;
ptr->ptr->x=3;

Öffnen im Compiler Explorer

Initialisierung von Strukturen

Bearbeiten

Mit dem Anlegen einer Strukturvariablen kann diese auch initialisiert werden. Eine 'Vorbelegung' der Strukturelemente bei der Definition der Struktur ist in C nicht möglich.
Die Initialisierung erfolgt über eine Initialisierungsliste (siehe Kap Grundlagen:Initialisierungsliste / Compound Literal). Innerhalb der Initialisierungsliste stehen die Initialisierungswerte entweder in der Reihenfolge der Datentypdefinition oder werden explizit mit dem ‘.‘ Operator angesprochen (designated initializers). Werden einzelne Strukturelemente ausgelassen, so werden diese mit 0 initialisiert:

struct abc{
  int a,b;
  int c=3;    //KO: Kein Initialisierungswert von Strukturelementen möglich
  char str[10];
};
struct abc v1={1,2,"hallo"}; //Alle Strukturelemente werden initialisiert
struct abc v2={ 1,2 };       //Initialisierung der Strukturlemente a und b
                             //Rest wird mit 0 initialisiert
struct abc v3={ .b=3};  //Initialisierung einzelner/designated Strukturele.
                        //Rest wird mit 0 initialisiert 
struct abc v4={.a=strlen(v1.str)}; //Initialisierungswert ergibt
                                    //sich erst zur Laufzeit.
                                    //Nur als lokale Variable möglich!
struct abc v5={.b=1,.a=2,.b=12}; //Nur C: Reihenfolge und Doppelbenennung
                                 //der Strukturelemente egal/möglich
struct abc v6={};  //Alle Strukturelemente mit 0 initialisieren

Öffnen im Compiler Explorer

Eine Initialisierung einer Struktur über nachfolgende Art bewirkt etwas anderes, als erwartet:

struct abc { int a,b,c; };
struct abc var={var.b=12,var.c=3};

Öffnen im Compiler Explorer

Innerhalb der Initialisierungsliste wird über 'var.b=12' und 'var.c=3' die zu diesem Zeitpunkt noch nicht initialisierte Variable var die Elemente b und c initialisiert. Mit dem Werten der Initialisierungsliste (hier {12,3}) wird nachfolgend die Variable erneut initialisiert und 12 dem Strukturelement a und 3 dem Strukturelement b zugewiesen. Abhängig vom Compiler ist das Strukturelement c entweder 3 oder 0.

Bei verschachtelten Strukturen muss entsprechend obiger Aussage für jede innere Struktur eine eigene Initialisierungsliste erstellt werden. Die inneren Initialisierungslisten können entfallen, was jedoch nicht empfohlen wird und ergänzend vom Compiler bei '-Wall' als Warning angemerkt wird:

struct abc {
  int a,b;
  struct xy {
    int x,y;
  } xy;
  struct xy arr[2];
};
struct abc v1={     //Initialisierung über Reihenfolge
  1,2,
  {3,4},
  { {5,6},{7,8}}
};
struct abc v2={     //Initialisierung über 'designated initializers'
  .arr={{.x=5,.y=6},{7,8}},
  .xy={3,4},
  .a=1,.b=2
};
struct abc v3={     //Fehlende innere Initialisierungsliste
  1,2,
  3,4,
  5,6,7,8
};

Öffnen im Compiler Explorer

Zuweisen/Kopieren von Strukturen

Bearbeiten

Strukturen werden über den Namen als 'ganzes' angesprochen, so dass eine Zuweisung als 'ganzes' möglich ist. Bei bspw. einer 12-Byte großen Struktur werden bei Nutzung der Variable die gesamten 12-Byte gelesen/geändert!

struct xyz {int x,y,z;} var1,var2;
var1=var2;           //12-Bytes kopiert
//--> entspricht memcpy(&var1,&var2,sizeof(xyz));

Die Zuweisung als 'ganzes' gilt auch bei der Parameterübergabe und -rückgabe von Funktionen:

struct xyz {int x,y,z;};
//Funktionsdefinition
struct xyz function(struct xyz par) { 	
  return (struct xyz){par.z,par.y,par.x};
}
//Funktionsaufruf
struct xyz var=function((struct xyz){1,2,3});
//Mit dem Aufruf der Funktion werden 12-Bytes in die Variable par kopiert
//Mit dem Ende der Funktion werden 12-Bytes in die Variable var kopiert

Bedenke, dass bei großen Strukturen das Kopieren Rechenzeit benötigt und folglich vermieden werden sollte! Alternativ sollten Zeiger auf Strukturen über- und zurückgeben werden.

Über Compound Literal (siehe Kap. Grundlagen:Initialisierungsliste / Compound Literal) kann einer Strukturvariablen als 'ganzes' einen neuen Wert zugewiesen werden:

struct abc {int a,b,c;};
var1={.a=7};                 //KO, Initialisierungsliste kann hier nicht
                             //angewendet werden
var1=(struct abc){.a=7};     //Wertezuweisung über Compound Literal möglich

Formel gesehen entspricht das Compound Literal einer 'unnamed Variable', die als erstes angelegt und initialisiert wird. Der Inhalt dieser Variablen wird im Anschluss der eigentlichen Variable zugewiesen!

Prototyp / Deklaration einer Struktur

Bearbeiten

Soll der Datentyp einer Struktur genutzt werden, der erst an weiter hinten liegenden Stellen im Programm definiert wird, so ist wie bei Variablen/Funktionen ein Prototyp/Deklaration notwendig:

struct abc;                //Prototyp der Struktur abc
...
struct abc { int a,b,c;};  //Definition der Struktur abc

Die Deklaration sagt einzig aus, dass die Struktur später definiert wird, beinhaltet aber nicht die Strukturelemente. Folglich kann auf Basis dieses Prototyps keine Variable definiert werden, sondern einzig ein Zeiger auf solche eine Struktur:

struct xyz;           //Deklaration / Prototyp des Datentyps
                      //    d.h. keine Benennung der Strukturelemente
struct xyz var1;      //KO, es kann auf Basis der Deklaration keine
                      //    Variable angelegt werden
struct xyz *pttr1;    //OK, von einer Strukturdeklaration kann ein Zeiger
                      //    angelegt werden!

Öffnen im Compiler Explorer

Anwendung:

  • Struktur, welche auf sich selbst verweist (Verkettete Liste)
struct vl {            //Entspricht gleichermaßen einem Prototyp, so dass
                       //innerhalb dieser Struktur dieser Datentyp zum
                       //Anlegen eines Strukturelementes genutzt werden kann
    struct vl  *next;  //Zeiger auf das nächste Element
    char daten[100];
};

Öffnen im Compiler Explorer

  • Entsprechend der objektorientierten Programmierung, wenn alle Attribute der Klasse private sind:
class.h
struct class;  //Prototyp für Struktur
               //so dass Nutzer dieser 
//Struktur ein Zeiger auf diese anlegen,
//aber nicht auf die Strukturelemente
//zugreifen können.

//Prototyp der public Methoden
void konstruktor(struct class ** me);
main.c class.c
#include "class.h"

int main(int argc, char*argv[])
{
  struct class *obj1;
  konstruktor(&obj1);
  ..
  //Kein Zugriff auf 
  //Strukturelemente möglich
  return 0;
}
#include "class.h"
struct class {
  int attr1;
  int attr2;
};
void konstruktor(struct class ** me)
{
  struct class *this;
  this=(struct class *)
        malloc(sizeof(struct class));
  this->attr1=10;
  *me=this;
}

Öffnen im Compiler Explorer

Vergleichen von Strukturen

Bearbeiten

Ein Vergleich von Strukturen über den direkten Weg ist nicht möglich. Vielmehr müssen die Strukturelemente händisch verglichen werden:

struct xyz {int inx,y,z;} var1,var2;
if(var1 == var2)  //KO, ein Vergleich von Strukturvariablen ist nicht möglich

//Manueller Vergleich über den Vergleich der Strukturelemente
if(var1.x == var2.x && var1.y == var2.y && var1.z == var2.z)

Öffnen im Compiler Explorer

Hinweis

  • Bei Rechnerarchitekturen mit nicht ausgerichteter Speicherausrichtung (Alignment) kann ergänzend ein Vergleich über memcmp() erfolgen

Speicherplatzbedarf einer Struktur

Bearbeiten

Für jedes Strukturelement wird Speicher entsprechend der Größe des Datentyps reserviert. Der Speicherplatzbedarf der Gesamtstruktur ergibt sich aus der Summe der Strukturelemente:

struct xyz{
  int x;   //Strukturelement belegt 4 Byte Speicher
  int y;   //Strukturelement belegt 4 Byte Speicher
  int z;   //Strukturelement belegt 4 Byte Speicher
}var1;   //Speicherplatz der Struktur = 4+4+4 = 12 Byte
sizeof(var1)       //ergibt 12
sizeof(struct xyz) //ergibt 12

Öffnen im Compiler Explorer

Die Ermittlung der tatsächlichen Speichergröße einer Struktur erfolgt über den sizeof-Operator:

struct xyz{int x,y,z;} a;
sizeof(a);                    //=12 OK
sizeof(a.x);                  //=4  OK
sizeof(struct xyz);           //=12 OK
sizeof(struct xyz.x);         //    KO
//Ist dies dennoch notwendig, so kann dies über den Umweg eines Zeigers
//erfolgen:
sizeof(((struct xyz *)0)->x); //=4  OK

Öffnen im Compiler Explorer

Hinweis:

  • Bei Rechnerarchitekturen mit ausgerichteter Speicherausrichtung (Alignment) (siehe   Speicherausrichtung) kann der tatsächliche Speicherbedarf größer sein! Hier fügt der Compiler ggf. zwischen den Strukturelemente Füllbytes (Padding Bytes) ein
  • Die Reihenfolge der Strukturelemente wird vom Compiler nicht geändert. D.h. die Zuordnung der Speicherstellen zu den Strukturelementen erfolgt in der Definitionsreihenfolge

Interne Organisation

Bearbeiten

Compilerintern werden die Strukturelemente über einen Offset dargestellt. Das erste Strukturelement bekommt dabei immer den Offset 0 zugewiesen. Das nachfolgende Element bekommt als Offset die Größe des Datentyps des vorherigen Elementes zugewiesen. Usw..
Der Compiler setzt den Zugriff auf ein Strukturelement einer (Struktur)Variablen so um, dass er zunächst die Startadresse der Variablen holt und zu dieser den Offset des Strukturelementes addiert. Die Anzahl der zu lesenden/schreiben Bytes ergibt sich aus dem Datentyp des Strukturelementes:

struct xyz {         //Das Anlegen der Variable var bedeutet,
  int x,y,z;         //12Byte Speicher zu reservieren.
} var1;              //(Beispielhaft soll var1 die Speicheradressen 
                     //0x100..0x10B belegen). Der Zugriff auf
                     //die Variable erfolgt immer über die Startadresse
var1.x=7;            //Zugriff auf Speicheradresse 0x100+0 (Offset)
var1.y=8;            //Zugriff auf Speicheradresse 0x100+4 (Offset)
var1.z=9;            //Zugriff auf Speicheradresse 0x100+8 (Offset)

Öffnen im Compiler Explorer


Der Offset eines Strukturelementes kann mit dem offsetof-Operator ermittelt werden:

Syntax: offsetof(type,Strukturelement)

Wie beim sizeof-Operator gilt der offsetof-Operator als Konstantenausdruck und der Rückgabedatentyp ist size_t. Zur Nutzung des Operators muss die Header-Datei 'stddef.h' inkludiert werden:

#include <stddef.h>  //Zur Nutzung des OffsetOf Operators
struct xyz {int x,y,z;} var1;
offsetof(struct xyz,z)  --> 8   
offsetof(var1,z)        --> KO: Es wird ein Datentyp und 
                                keine Variable erwartet

Öffnen im Compiler Explorer

Explizites Cast

Bearbeiten

Eine Struktur kann nicht in einen anderen Datentyp und damit auch nicht in einen anderen Strukturdatentyp mit identischen Strukturelementen konvertiert werden:

struct xyz {int x,y,z;} xyz;
struct abc {int a,b,c;} abc;
int var;

xyz=abc;                  //KO, Datentyp struct xyz != struct abc
xyz=(struct xyz)abc;      //KO, siehe zuvor

int neu1 = (int) xyz;     //KO
int neu2 = (int) xyz.x;   //OK (Das Strukturelement x ist vom Datentyp int)
struct xyz neu2 = (struct xyz)3;  //KO

Öffnen im Compiler Explorer

Incomplete Array

Bearbeiten

Das letzte Element einer Struktur kann ein Array ohne Angabe einer Dimensionsgröße sein (Incomplete Array/Flexible Array Member). Bei der Definition einer Variablen von solch einem Datentyp wird in der Tat kein Speicher für das letzte Element reserviert, so dass dies 'händisch' erfolgen muss. Auf das Array kann ungeachtet dessen unproblematisch zugegriffen werden:

struct vl {
  struct vl *next; 
  char data[];   //incomplete Array / flexible Array Member
};
struct vl a;      //Es wird nur Speicher für den next Zeiger reserviert
a.data[0]='7';    //Syntax OK, jedoch erzeugt dies ein Laufzeitfehler,
                  //da kein Speicherplatz für das Incomplete Array reserviert
                  //wurde

Öffnen im Compiler Explorer

Die Speicherplatzreservierung für das incomplete Array kann auf zwei verschiedene Arten erfolgen:

  • Speicherplatzreservierung für das Incomplete Array über Variableninitialisierung (nur GCC-C und nur im Falle einer globalen Variablen)
struct vl ele1 = {
  .next = NULL,
  .data = {32, 31, 30}  //Durch Initialisierung der einzelnen Array Elemente
};  //Vorsicht, sizeof(ele) gibt dennoch nur 4/8 zurück
struct vl ele2 = {
  .next = NULL,
  .data[3-1] = 0  //Durch Initialisierung des letzten Array Elementes
};  //Vorsicht, sizeof(ele) gibt dennoch nur 4/8 zurück

Öffnen im Compiler Explorer

  • Speicherplatzreservierung für das Incomplete Array über malloc
struct vl *b=malloc(sizeof(struct vl)+  //Größe der Struktur
                    sizeof(char[3]  )); //Größe des Arrays
b->data[1]='7';           //OK, da mit dem Anlegen von b Speicher für 2 
                          //data Element reserviert wurde

Öffnen im Compiler Explorer


Das Incomplete Array bietet sich überall dort an, wo Daten gespeichert werden sollen, deren Größe sich erst zur Laufzeit ergibt.

Anwendung

Bearbeiten

Die Nutzung des Datentyps struct empfiehlt sich an vielen Stellen:

  • Zusammengehörende Daten zu Kapseln (entsprechend der objektorientierten Programmierung
  • Verkette Listen
  • Aufgrund der Typsicherheit zur Darstellung von sicherheitskritischen Aufgaben

Siehe Übungsbereich!

Der wesentliche Syntax von Strukturen wurde in C++ übernommen. D.h.:

  • das abschließende Semikolon
  • der Zugriff auf die Strukturelemente
  • Speicherplatzreservierung
  • die Definition von Variablen mit der Definition des Datentyps
  • ...

Ergänzend wurde in C++ der Datentyp class eingeführt, der weitestgehend identisch zu struct ist. Geändert/Hinzugefügt wurden folgende Sachverhalte:

  • Classen/Strukturen können Methoden haben
  • Operatoren (Zuweisungsoperator, Addition, ...) können überladen werden
  • Alle Strukturelemente (und Methoden) einer struct sind per default public
  • Alle Attribute (und Methoden) einer Class sind per default private
  • Zur Nutzung des Datentyps ist das führende struct nicht notwendig (dies bedingt dann auch, dass der Strukturdatentyp kein separater Namensraum ist)
struct xyz {
  int x,y,z;
};
xyz var_xyz;
  • Initialisierungswerte für Strukturelemente und Klassenattribute bei der Datentypbeschreibung angegeben werden können:
struct xyz {
  int x=3;
  int y=1;
  int z;
};
xyz var={7};    //var.x=7   var.y=1  var.z=0

Öffnen im Compiler Explorer

  • In C++ kann eine Struktur ebenfalls über 'designated initializers' initialisiert werden. Jedoch muss die Reihenfolge der Initialisierungselement identisch zur Datentypdefinition sein. Auch ist keine Doppeltbenennung erlaubt:
struct xyz {
  int x,y,z;
};
xyz var1={.x=1, .y=2, .z=3};  //OK, Reihenfolge identisch
xyz var2={.y=1, .z=3};        //OK, Reihenfolge identisch
xyz var3={.y=1, .x=0};        //KO, Reihenfolge nicht identisch
xyz var4={.x=1, .x=2};        //KO, keine Doppeltbenennung möglich

Öffnen im Compiler Explorer

Ähnlich wie eine Struktur ist ein Union ein Datentyp, der aus einem oder mehreren Datentypen zusammengesetzt wird. Bei sog. Union beginnen jedoch alle Komponenten nicht wie bei Strukturen an nacheinander folgenden Speicheradressen, sondern an der identischen Speicheradresse, d.h. ihre Speicherbereiche überlappen sich ganz oder zumindest teilweise. Eine Union kann folglich zu einem Zeitpunkt nur ein Element enthalten. Der benötigte Speicherplatz ergibt sich aus der größten Komponente.

Syntax: union UnionnameOpt {Datentyp Variablenname; …}Opt VariablenlisteOpt;

Ein Schutz/Zugriffssteuerung der Unionelemente ist nicht vorhanden. Es kann in beliebiger Reihenfolge auf die einzelnen Elemente zugegriffen werden.

Die Anwendung ist identisch wie bei Strukturen, so dass im Folgenden nur die Abweichungen zu den Strukturen beschrieben werden.

Speicherplatzbedarf und interne Struktur einer Union

Bearbeiten

Die Größe der Union ergibt sich aus dem größtem Unionelement. Alle Unionelemente überlappen sich, so dass der Offset zum Basiselement 0 ist:

union abc {
  char  a;
  short b;
  int   c;
} abc;
printf("Größe:   %zu\n",sizeof  (union abc));   //-> 4
printf("Offset a:%zu\n",offsetof(union abc,a)); //-> 0
printf("Offset b:%zu\n",offsetof(union abc,b)); //-> 0
printf("Offset c:%zu\n",offsetof(union abc,c)); //-> 0

Öffnen im Compiler Explorer

Wird ein kleineres Unionelement belegt und im Anschluss ein größeres Unionelement gelesen, so ist der Inhalt der durch das kleinere Element nicht beschriebenen Speicherstellen undefiniert:

union abc {
  char a;
  short b;
  int   c;
} abc;
abc.a=0x11;
printf("%08x",abc.c);

Öffnen im Compiler Explorer

Initialisierung von Union

Bearbeiten

Bei einer Union kann nur ein Element initialisiert werden, d.h. die Initialisierungsliste kann nur einen Wert beinhalten. Ohne explizite Benennung des Unionelementes in der Initialisierungsliste wird das erste Element aus der Datentypbeschreibung initialisiert. Mit expliziter Benennung mittels 'designated initializers' können auch andere Elemente initialisiert werden:

union abc {
  char a;
  short b;
  int c;
} abc;
	 
abc=3;                 //KO keine Initialisierungsliste
abc=(union abc){3};    //OK Initialisierung des ersten Elementes a
abc=(union abc){.b=3}; //OK Initialisierung des Elementes b 	 
abc=(union abc){3,4};  //KO nur ein Initialisiuerngswert erlaubt

Öffnen im Compiler Explorer

Anwendung

Bearbeiten

Mittels des Datentyps union können diverse Anwendungsfälle abgedeckt werden:

  • Über Union lassen sich größere Datentypen in kleinere Datentypen unterteilen, um z.B. einzelne Bytes zu extrahieren:
union floatu {
  float        var;       //Float ist 4-Byte groß
  unsigned int hex;       //Integer ist 4-Byte groß
  char         byte[4];   //Ohne Worte
};
union floatu var={1.234};
printf("%f\n",var.var);   //Darstellung des Float-Wertes
printf("%x\n",var.hex);   //Darstellung des Float-Wertes 'BinäreZahl'
printf("%hhx %hhx %hhx %hhx\n",   //Darstellung der einzelnen Bytes
  var.byte[0],var.byte[1],var.byte[2],var.byte[3]);

union longlong {
  long long ll;       //Long Long ist 8-Byte groß
  int       i[2];     //Ohne Worte
  short     s[4];
  char      c[8];
};
union longlong var2={0x123456789ABCDEFULL};
printf("%llx\n",var2.ll); 
printf("%x %x\n",var2.i[0],var2.i[1]);
//Vorsicht: Die Aufteilung der Bytes ist von der Rechnerarchitektur
//(Endianes) und von der Ausrichtung durch den Compiler abhängig

Öffnen im Compiler Explorer

  • Ein weiterer Anwendungsfall ergibt sich, wenn in einer Struktur unterschiedliche Datensätze gespeichert werden sollen (siehe   Tagged Union):
struct set {
  char *index;
  char *value;
};
struct get {
  char *index;
  char *value;
};
struct cli{
  //Enum zur Darstellung des aktiven Unionelementes hilfreich
  enum {SETTER,GETTER} tag;
  union {                //Interpretation abhängig von tag
    struct set set;
    struct get get;
  } ;
};
struct cli var={.tag=SETTER, .set.index="hallo"};

if(var.tag==SETTER)
  printf("Set: %s",var.set.index);
else
  printf("Get: %s",var.get.index);

Öffnen im Compiler Explorer

  • Mittels eines Tagged Union kann komfortabel ein Binärbaum realisiert werden:
//Quelle: https://github.com/Hirrolot/datatype99
struct BinaryTree; //Prototyp
struct BinaryTreeNode {
  struct BinaryTree *lhs;
  int x;
  struct BinaryTree *rhs;
} BinaryTreeNode_t;
struct BinaryTree {
  enum { Leaf, Node } tag;
  union {
    int leaf;
    struct BinaryTreeNode node;
  } data;
};
int sum(const struct BinaryTree *tree) {
  switch (tree->tag) {
    case Leaf:
      return tree->data.leaf;
    case Node:
      return sum(tree->data.node.lhs) + 
                 tree->data.node.x + 
             sum(tree->data.node.rhs);
  }
  return -1; // Invalid input (no such variant).
}

Öffnen im Compiler Explorer

Die Eigenschaften des Datentyps union entsprechen den Eigenschaften des Datentyps struct in C++.

Enum/Aufzählungstyp

Bearbeiten

Nach dem Wikipedia Artikel   Aufzählungstyp ist ein Aufzählungstyp (englisch enumerated type) ein Datentyp für Variablen mit einer endlichen Wertemenge. Alle zulässigen Werte des Aufzählungstyps werden bei der Deklaration des Datentyps mit einem eindeutigen Namen (Identifikator) definiert, sie sind Symbole.

Syntax: enum enumnameOpt {definition-list[=expression]}Opt VariablenlisteOpt

Die Anwendung ist identisch wie bei Strukturen, so dass nachfolgend nur die Abweichungen zu den Strukturen beschrieben werden.

Datentyp von enum

Bearbeiten

In C entspricht der Datentyp Enum dem Datentyp int so dass Zuweisungen/Vergleiche mit Ganzzahlen möglich sind. Ganzzahloperationen funktionieren ebenso:

enum STATUS {OK,KO=5};  //Definition des Datentyps
enum STATUS status;     //Definition einer Variable dieses Datentyps
status=OK;
if(status == 5)        
status++;           
status=5.7;

Öffnen im Compiler Explorer

Enumelemente

Bearbeiten

Die Elemente der Definitionsliste sind Integerkonstanten. Diese können auch in Kombination mit anderen Datentypen genutzt werden. Auch gibt es keinen gesonderten Namensraum für Elemente der Definitionsliste. Sie 'konkurrieren' folglich mit allen Variablen- und Funktionsnamen:

enum mode {OK, KO};
enum {FIRST,SECOND,LAST};  //Anonymes Enum!
enum {EINS,ZWEI,LAST};     //KO, Symbolname LAST bereits vergeben
enum mode var1=OK;
var1=4711;           //OK, enum entspricht Datentyp int
int var2=KO;         //OK, EnumElement entspricht Datentyp int
int OK=KO;           //KO, Symbolname OK bereits für EnumElement vergeben
var1=FIRST;          //OK, var1 und FIRST sind zwar unterschiedlich
                     //Enumtypen,jedoch entspricht beides dem Datentyp int

Öffnen im Compiler Explorer

Der erste Enumelement der Definitionsliste bekommt den Wert 0 zugewiesen. Folgeelemente bekommen den Wert des Vorgängerelementes +1 zugewiesen. Enumelemente können mit einer Konstanten (und Konstantenausdruck) initialisiert werden:

enum {bill=10, john=bill+2, fred=john+1} ;
//Negative Werte sind möglich
enum {error=-3, warning, info, ok};   //warning=-2  info=-1 …
//Doppelte Wertzuweisung sind möglich
enum {Ostfalia=10,Wolfenbuettel=10};
//Hinter dem letzten Enumelement kann ein Komma folgen
enum {test=10,};

Öffnen im Compiler Explorer

Hinweis:

  • Es empfiehlt sich, die Enumelemente in GROSSBUCHSTABEN zu schreiben. Hiermit wird gekennzeichnet, dass es sich um Konstanten und nicht um Variablen/Funktionen handelt

Anwendung

Bearbeiten

Die Nutzung des Datentyps enum empfiehlt sich überall dort, wo eine Fallunterscheidung notwendig ist:

  • Statusrückgabe von Funktionen:
enum STATUS {OK,MEMORY_OVERFLOW, DIVISION_BY_ZERO};
enum STATUS funct() {
    return MEMORY_OVERFLOW;
}
  • Beschreibung der Zustände und der Ereignisse eines Zustandsautomates:
enum EREIGNIS {EREIGNIS1,EREIGNIS2,EREIGNIS3};
void zustandsautomat(enum EREIGNIS ereignis) {
  static enum {IDLE,OPERAND,OPERATOR,} zustand=IDLE;
  switch(zustand) {
    case IDLE:
    case OPERAND:
	  //Compiler meckert bei fehlendem Case über OPERATOR
  }
}

Öffnen im Compiler Explorer

Ergänzend zu den Abweichungen bei Structs sind hier weitere Unterscheidungsmerkmale vorhanden:

  • Enums stellen einen eigenen Datentyp dar, der nicht mit int kompatible ist. Das bedingt unter anderem, dass einige Operatoren wie z.B. ++ nicht mehr auf Variablen des Datentyps enum angewendet werden. Die Enumelemente als solches sind jedoch kompatibel zum Ganzzahldatentyp:
enum Color {RED,GREN,BLUE};
enum Color var1=RED;
var1=4;              //KO
var1=var1+1;         //KO
var1++;              //KO
int var2=RED;        //OK
if(RED==2)           //OK

Öffnen im Compiler Explorer

  • Auch enums können Methoden besitzen.
  • Über Operatorüberladung können z.B. nicht vorhandene Operatoren wie ++/-- ergänzt werden
  • Über unscoped/scoped Enumerations wird der 'Namensraum' gesteuert:
  • Unscoped Enumeration (Elemente der Definitionsliste stehen wie bei C im Zugriff)
Syntax: enum nameOpt : typeOPT {enumerator=constexp, …};
enum Color {RED,GREN,BLUE };
Color r = RED;

Öffnen im Compiler Explorer

  • Scoped enumeration (Elemente der Definitionsliste haben einen eigenen 'Namensraum')
Syntax: enum class|struct nameOpt : typeOPT {enumerator=constexp, …};
	enum class Color {RED,GREN=20,BLUE };
	Color r = Color::BLUE;

Öffnen im Compiler Explorer

Bitfelder

Bearbeiten

Nach dem Wikipediaartikel   Bitfeld bezeichnet in der Informationstechnik und Programmierung ein Bitfeld ein vorzeichenloses Ganzzahldatentyp, in dem einzelne Bits oder Gruppen von Bits aneinandergereiht werden. Es stellt eine Art Verbunddatentyp auf Bit-Ebene dar. Im Gegensatz dazu steht der primitive Datentyp, bei dem der Wert aus allen Stellen gemeinsam gebildet wird.

Der Syntax entspricht dem struct-Syntax, mit der Ergänzung, dass hinter den Strukturelementen noch die Bitbreite getrennt durch ein Doppelpunkt angegeben wird:

struct time {
  unsigned int hour:   5;   //0..23  Bits 0..4
  unsigned int minute: 6;   //0..59  Bits 5..10
  Unsigend int second: 6;   //0..59  Bits 11..16
} myTime;

Als mögliche Datentypen für die Strukturelemente stehen nur die Ganzzahldatentypen int, short, long , long long und char zur Verfügung. Der Datentyp eines Strukturelementes muss mindestens die Anzahl der Bits enthalten, wie diese über die Bitbreite gefordert wird. Von den Strukturelementen kann keine Adresse bestimmt werden! Auch der offsetf - Operator schlägt fehl.

Der Datentyp entspricht im Wesentlichen einem Ganzzahlzahldatentyp, so dass bei Zugriff auf eine Variable dieses Datentyps eine Ganzzahl gelesen/geschrieben wird. Bei Zugriff auf die einzelnen Strukturelemente werden die entsprechenden Bits dieser Ganzzahlvariablen maskiert, so dass nur die betroffenen Bits geändert werden:

struct test {
  unsigned char first:2;
  unsigned char second:4;
  unsigned char third:2;
};
struct test var={.first=1,.second=1,.third=1};
//entspricht
unsigned char var1=0b01000101;

var.second=1;
//entspricht:
var1=(var1&0b11000011) | (1<<2);

var.second++;
//entspricht
int second =(var1>>2)&0b00001111;
second++;
var1=(var1&0b11000011) | ((second&0b00001111)<<2);

Öffnen im Compiler Explorer

Anwendung

Bearbeiten
  • Komprimierte Speicherung der Uhrzeit (wie dies z.B. in Realtimeclocks erfolgt)
union {
  struct {             //Datentyp zum 'Bitweisen' Zugriff
    unsigned int hour:5;
    unsigned int minute:6;
    unsigned int second:6;
    };
  unsigned int hex;    //Datentyp zum Zugriff aller Elemente
} myTime;
myTime.hour   = 13;
myTime.minute = 37;
myTime.second = 59;
printf("Zeit: %02d:%02d:%02d\n", myTime.hour,myTime.minute,myTime.second );
//Alternativer/Händischer Zugriff auf die einzelnen Bits
printf("Es ist jetzt %02d:%02d:%02d Uhr\n",((myTime.hex>> 0)&0b011111),
                                           ((myTime.hex>> 5)&0b111111),
                                           ((myTime.hex>>11)&0b111111));

Öffnen im Compiler Explorer

  • Aufteilung von FloatingPoint Zahlen in seine Komponenten
union float_mes {
  float flo;
  struct ieee { //aus ieee754.h kopiert
#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
    unsigned int negative:1;
    unsigned int exponent:8;
    unsigned int mantissa:23;
#endif /* Big endian.  */
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    unsigned int mantissa:23;
    unsigned int exponent:8;
    unsigned int negative:1;
#endif /* Little endian.  */
  } ieee;
  int hex;
};
//aus ieee754.h kopiert
#define IEEE754_FLOAT_BIAS	0x7f /* Added to exponent.  */
union float_mes test={0.1};
printf("float: %f\n",test.flo);
printf("hex:   %x\n",test.hex);
printf("bin:   ");
for(unsigned int flag=0x80000000; flag; flag=flag>>1)
  printf("%c%s",test.hex&flag?'1':'0',
         flag&0x80800000?":":(flag&1?"\n":""));
printf("bitfield: (%c)%x * 2^(%d-%d)\n",(test.ieee.negative==1?'-':'+'),
                                         test.ieee.mantissa,
                                         test.ieee.exponent,
                                         IEEE754_FLOAT_BIAS);

Öffnen im Compiler Explorer

Bitfelder existieren mit den bekannten Ergänzungen auch in C++.