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'!
- Es wird mit nichts Kleinerem gerechnet als mit dem Datentyp 'int' (siehe Implizite Typumwandlung) :
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
undfalse
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; }
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 vorzeichenbehaftetshort
/long
/long long
(letzteres seit C99) als vorangestellten Qualifier zuint
zur Vergrößerung/Verkleinerung der Bitbreite. Bei Angabe des Qualifiers ist das Schlüsselwortint
optional. Die Beschreibunglong long int
undlong 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:
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^126≅1,175*10^-38 Die größte darstellbare Zahl ist: 2^127 ≅1,7*10^38
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);
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));
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));
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));
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.
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
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
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);
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
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] .
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 Zieltypesigned 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 Zieltypunsigend 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
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!
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.
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) |
R-L |
Präfix-Inkrement/Dekrement |
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);
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
Bei den möglichen Konvertierungen sollte Folgendes berücksichtigt werden:
- Vorzeichenlose Ganzzahl → Vorzeichenlose 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 Ganzzahl → Vorzeichenbehaftete 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)
- Gleitkommazahl → Gleitkommazahl: 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
- Ganzzahl → Gleitkommazahl: 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
- Gleitkommazahl → Ganzzahl: Im Wesentlichen wird hier die Mantisse übernommen, so dass hier die Bitbreite des Zieldatentyps mindestens der Bitbreite der Mantisse sein sollte
- Zeiger ↔ Ganzzahl: 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!
- Zeiger ↔ Gleitkommazahl: Diese Konvertierung ist nicht möglich!
- Struktur/Union ↔ Ganzzahl/Gleitkommazahl: Diese Konvertierung ist nicht möglich. Es können nur einzelnen Strukturelemente konvertiert werden, sofern diese vom Typ Ganzzahl/Gleitkommazahl sind
- Array ↔ Ganzzahl/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
- 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!
- 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
- 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;
- 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;
- 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
- 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'
Hinweise:
- Eine Struktur (und auch eine C++ Klasse) wird mit einem Semikolon abgeschlossen
- Eine alternative Beschreibung zu Strukturen finden sie in Structures in C: From Basics to Memory Alignment
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
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;
- 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
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;
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
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};
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
};
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!
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]; };
- 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; }
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)
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
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
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)
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
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
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
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
- 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
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!
C++
[Bearbeiten]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
- 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
Union
[Bearbeiten]Ä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
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);
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
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
- 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);
- 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). }
C++
[Bearbeiten]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;
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
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,};
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 } }
C++
[Bearbeiten]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
- 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;
- 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;
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);
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));
- 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);
C++
[Bearbeiten]Bitfelder existieren mit den bekannten Ergänzungen auch in C++.