Programmieren in C/C++: DatentypeSpecifier
Typedef Storage Class Specifier
[Bearbeiten]Anweisung zum Erstellen eines Alias für einen Datentyp.
Syntax: typedef T type_ident[,type_ident];
T
ist der zu ersetzende Datentyp. Type_ident
ist der Alias, wobei auch mehrere mit Komma separierte Aliase gesetzt werden können. Der Syntax entspricht einer normalen Variablendefinition mit vorangestellten typedef
. Aufgrund des vorangestellten wird nun keine Variablen angelegt, sondern der 'Variablenname' ist der Alias.
Auf Basis des Alias können in Anschluss beliebig viele Variablen definiert werden:
typedef unsigned long long ull;
ull var1;
typedef signed long sl1,sl2,*psl;
sl1 var2; //--> signed long var2;
psl var3; //--> signed long *var3;
struct xyz {
ull x,y,z;
};
typedef struct xyz xyz_t;
xyz_t var4; //--> struct xyz var4;
typedef struct abc { int a,b,c;} abc_t;
abc_t var5; //Definition über Alias
struct abc var6; //Definition über struct abc
typedef struct {int d,e,f;} def_t; //Unnamed Struct
def_t var7;
typedef int arr_alias[4];
arr_alias arr;
typedef void func_alias(void); //Alias für eine Funktion
func_alias *func; //Zeiger auf Funtion
Anwendungsfälle von typedef sind:
- Source-Code unabhängig von der Rechnerarchitektur zu entwickeln:
//Bitbreite des Datentyps int ist von der Rechnerarchitektur abhängig #if ARCHITEKTUR == 16 typedef unsigned int uint16_t; #elif ARCHITEKTUR == 32 typedef unsigned short uint16_t; #endif //Diese Definition sind in der Header-Datei stdint.h enthalten!
- Weniger Tipparbeit:
//Vermeidung des führende struct/union/enum typedef enum {OK,KO,var11} STATUS_T; STATUS_T var1; //Vermeidung der Angabe weiterer Specifier typedef const volatile unsigned int cvuint16_t; cvuint16_t var2=4711;
- Dem Datentyp über seinen Namen eine Bedeutung zuzuordnen:
typedef char * cpointer; typedef struct { int raeder; enum {ROT,GELB}farbe;} auto_t; typedef void (*fptr_t)(void);
Hinweise:
- Zur Verdeutlichung, dass es sich bei dem Alias nicht um eine Variable oder Funktion handelt, wird oftmals der Suffix '_t' an den Alias angehängt
- Der Alias konkurriert mit Variablen und Funktionsnamen, kann also nicht doppelt benutzt werden
- Wird ein Typedef innerhalb eines Blockes definiert, so ist dieser nur innerhalb des Blockes gültig
Internal/External Linkage
[Bearbeiten]C-Projekte bestehen aus mehreren C-Dateien, welche beim Compilieren in Objektdateien gewandelt werden und über den Linker zu einer ausführbaren Datei zusammengebunden werden. Der Zugriff auf Inhalte 'externer' Objektdateien wird über die 'internal', 'external' und 'none Linkage' gesteuert, wobei none gleichzusetzen mit external Linkage ist. External Linkage bedeutet, dass solche Variablen/Funktionen im gesamten Projekt (bestehend aus div. Objektdateien und Libraries) in Zugriff steht und somit nur einmal existiert (= globale Variable/Funktion). Internal Linkage bedeutet, dass die Variable/Funktionen nur in der aktuellen Objekt-/C-Datei im Zugriff stehen.
External Linkage / Extern Storage Class Specifier
[Bearbeiten]Mit dem Storage Class Specifier extern
wird ausgesagt, dass die Variable/Funktion 'external linkage' hat (also projektweit im Zugriff steht) und an dieser Stelle kein Speicherplatz für die Variable/Funktion definiert wird. Sie entspricht folglich dem Funktions-Prototyp mit der expliziten Aussage, dass diese Variable/Funktion 'anderweitig' definiert ist.
Syntax: extern Datentyp Variablenname;
extern Datentyp Funktionsname(Datentyp,Datentyp);
Beispiel:
datei1.c | datei2.c |
---|---|
extern int var; //Zugriff auf
//'externe' Variable
extern int func(int par); //Zugriff
//auf 'externe' Funktionen
int main(int argc, char *argv[])
{
extern int var2; //Zugriff auf
//'externe' vVariable
func(var+var2);
}
|
int var; //Definition der
//'externen' Variable
int var2=8; //Definition der
//'externen' Variable
int func(int par) //Definition der
{ //'externen' Funkt.
return(par*2);
}
|
Das Weglassen des Schlüsselwortes extern entspricht der Definition einer Variable/Funktion, d.h. der Speicherreservierung. Die Linkage ist in diesem Fall ebenfalls extern. Die Extern Anweisung wird benötigt:
- wenn auf Funktionen / Variablen zugegriffen werden sollen, die erst weiter hinten im Code definiert werden und projektweit im Zugriff stehen
- wenn auf Funktionen / Variablen zugegriffen werden, die in anderen Dateien enthalten sind
- wenn auf Funktionen / Variablen zugegriffen werden, die in Libraries enthalten sind
- wenn auf Funktionen / Variablen zugegriffen werden, die mit einer anderen Sprache übersetzt wurden
Hinweise:
- Wenn in zwei unterschiedlichen Dateien eines Projektes je eine initialisierte globale Variable mit identischen Namen angelegt wird, so gibt der Linker den Fehler "multiple definition" dieser Variablen aus
- Wie bereits erwähnt wurde Unix und C parallel entwickelt und einer der neuen Features von Unix war die Bereitstellung eines Linkers. Der Extern Storage Class Specifier ist eine Anweisung an den Linker, an den Stellen der Nutzung der Variablen/Funktion in der eigenen Objektdatei die Adresse der eigentlichen Variablen/Funktion aus einer anderen Objektdatei einzutragen
Internal Linkage / Static Storage Class Specifier
[Bearbeiten]Die genaue Bedeutung ist abhängig davon, ob es sich um eine globale Funktion/Variable oder einer lokalen Variable handelt.
Static globale Variable / Static Funktionen
[Bearbeiten]Weist den Compiler an, dass diese Variable/Funktion nur innerhalb dieser C-Datei genutzt wird (internal Linkage) und für anderen C-/Objekt-Dateien nicht im Zugriff stehen. Static entspricht dem Zugriffsmodifikator 'public' aus der objektorientierten Programmierung mit dem Unterschied, das der Zugriff auf diese Variable/Funktion nicht innerhalb des Objektes, sondern innerhalb der Datei beschränkt ist.
Ergänzend dient static der besseren Lesbarkeit des Codes (Zugriff auf diese Variable/Funktion nur aus dieser Datei). Auch der Compiler kann den Zugriff auf solche Variablen/Funktionen optimieren und schnelleren/kompakteren Code erzeugen.
Im Sinne der C-Spezifikation steht der Static Storage Class Specifier dafür, die Variable auf internal Linkage zu setzen (diese nach außen zu anderen Objektdateien nicht bekannt zu machen). Extern im Gegenzug steht für external Linkage. Die Nutzung beider Anweisungen zusammen widersprechen sich folglich und führen zu Compilerfehler. Auch bedeutet static, dass hiermit Speicherplatz reserviert wird (entgegen der 'extern' Anweisung, die eher einem Prototyp entspricht).
Für eine statische lokale Variable kann kein Prototyp erzeugt werden. Solche Variablen müssen immer vor der ersten Verwendung definiert werden!
Beispiele:
- Nutzung von static Variablen/Funktionen innerhalb einer Datei
//Prototypen basierend auf den unten stehenden statische Variablen/Funktionen
static void func(void); //OK
extern void func(void); //KO (Prototyp muss ebenfalls static sein)
extern static void func(void); //KO (extern und static wiedersprechen sich)
void func(void); //KO (Prototyp muss ebenfalls static sein)
extern static int var1; //KO a)extern und static widersprechen sich
// b)von einer static Variable kann kein
// Prototyp angelegt werden!
int main(int argc, char *argv[]) {
func();
var1=1; //KO, da kein Prototyp möglich ist
return 0;
}
//Definition von statische Variablen/Funktionen
static int var1; //Static Variable
static void func(void) { //Static Funktion
printf("var1=%d",var1);
}
- Nutzung von static Variablen/Funktionen innerhalb mehrerer Dateien
- Static entspricht dem private Zugriffsmodifikatior, mit dem Unterschied dass diese Variablen/Funktionen nur innerhalb der Datei gelten und in mehreren Dateien unabhängig voneinander existieren können
datei1.c | datei2.c |
---|---|
static int var=4711;
static void func(void) {
printf("datei1.c var=%d\n",var);
}
|
static int var=0715;
static void func(void) {
printf("datei2.c var=%o\n",var);
}
|
Static lokale Variable
[Bearbeiten]Diese Variablen behalten über der Laufzeit der Funktion ihre Gültigkeit, d.h. sie verlieren nicht ihren Inhalt bei Beendigung der Funktion, sondern behalten den Wert bei. Static lokale Variablen entsprechen folglich einer globalen Variablen, mit dem Unterschied, dass von außerhalb der Funktion keiner auf diese Variable zugreifen kann. Dementsprechend erfolgt die Initialisierung wie bei globalen Variablen über Speichervorbelegung (Initialisierungswert muss eine Konstanten sein) und nicht zur Laufzeit bei jeden 'Aufruf'. Nichtinitialisierte Variablen werden entsprechend mit 0 initialisiert:
Static Lokale Variablen | entsprechen weitestgehend |
---|---|
void func(void) {
int var=7; //Initialisierung erfolgt mit
//jedem Funktionsaufruf
var++; //7->8 7->8 7->8
static int summe=8; //Initialisierung
//erfolgt vor Aufruf
//der main-Funktion
summe++; //8->9 9->10 10->11
}
|
//Namensverschleierung der static
//Variablen, so dass kein anderer
//auf diese Variablen 'zugreifen'
//kann.
int $a$1_xyz=0;
void func(void) {
int var=7;
var++;
$a$1_xyz++;
}
|
Hinweis:
- Wird eine Funktion mit statisch lokalen Variablen von nebenläufigen Prozessen aufgerufen, so besteht ein Dateninkonsistenzproblem. Solche Funktionen sind folglich nicht Nebenläufigkeitsfest. In der Spezifikation sind diese als MT-UNSAFE (MultiThread-Unsafe) gekennzeichnet. strtok() beispielsweise speichert die aktuelle Position in einer statische Variablen, so das diese Funktion nicht gleichzeitig von nebenläufigen Prozessen aufgerufen werden darf.
Datentyp spezifische Operatoren
[Bearbeiten]C war einer der ersten Sprachen, die Datentypen eingeführt hat. Mit der Einführung der Datentypen wurde jedoch keine Möglichkeit geschaffen, den Datentyp abzufragen, zu vergleichen oder gar in einen String zu wandeln. Der Datentyp dient einzig der Reservierung von passend Speicher und der Auswahl der passenden Maschinensprachebefehle.
Mit der Weiterentwicklung der Sprache, resp. auf den Compilererweiterungen wurde Datentyp spezifische Operatoren ergänzt.
Typeof Operator
[Bearbeiten]Mittels des Typeof Operator kann der Datentyp einer Variablen ermittelt werden.
Syntax: typeof (T) //GNU-C und C23
__typeof__(T) //GNU-C
T ist wahlweise eine Variable, ein Datentyp oder eine Expression. typeof entspricht einem Konstantenausdruck und wird vom Compiler durch den ermittelten Datentyp ersetzt. Der Rückgabewert ist der ermittelte Datentyp, der nicht in einer Variablen gespeichert oder mit einem anderen Datentyp verglichen werden kann. Typeof kann einzig dazu genutzt werden, weitere Variablen von T anzulegen (entspricht somit dem auto Datentyp bei anderen Programmiersprachen):
typedef unsigned int UI;
UI var1[19];
typeof( UI ) var2;
typeof( int) var3;
typeof(var1) var4;
var4[3]=1;
typeof(var1[2]) var5;
var5=7;
struct { //Unnamed Struct
int x,y,z;
}xyz;
typeof(xyz) abc; //Definition einer neuen Variable von
//einem unnamed struct.
if(typeof(var1) == int) //KO, Ersetzung findet zur Compilezeit
printf("True"); //statt und der Vergleichsoperator kann
//nicht auf Datentypen angewendet werden
//Typeof einer Expression
char var6;
typeof(var6+1) var7; //Es wird mit nichts kleineren gerechnet
//als int. Resultierender Datentyp ist int!
printf("sizeof(var6)=%zu sizeof(var7)=%zu\n",sizeof(var6),sizeof(var7));
Anwendung:
- Zur Darstellung von generischen Function-Like Makros (siehe Präprozessor:Function-Like Makros), so dass auf Basis von Typeof lokale Variablen vom Datentyp der 'übergebenen' Variablen erzeugt werden können:
#define SWAP1(a,b) ({typeof(*a) dummy=*a; *a=*b; *b=dummy;}) #define SWAP2(a,b) ({auto dummy=*a; *a=*b; *b=dummy;}) //C++ int a=7; int b=8; SWAP1(&a,&b);
Hinweis:
- typeof war bis C23 eine GCC-Compilererweiterung. Mit C23 wurde diese Erweiterung in den Standard übernommen
_Generic Operator
[Bearbeiten]Mit C11 wurde der _Generic Operator, bei dem ähnlich wie bei einer Switch-Anweisung anstatt über eine Ganzzahl über Datentypen selektiert werden kann, eingeführt. Wie auch der typeof Operator wird der _Generic Operator nicht zur Laufzeit, sondern zur Compilezeit ausgewertet.
Syntax: _Generic(controlling-expression, association-list)
wobei die association-list eine kommaseparierte Liste bestehend aus
type-name : expression
default : expression
ist
Die controlling-expression
kann hierbei ein beliebiger Ausdruck sein, der einen Datentyp zurückgibt. Der Ausdruck selbst wird nicht ausgeführt! type-name
kann ein beliebiger Datentyp sein. Sofern dieser identisch zum resultierenden Datentyp der controlling-expression
ist, wird der gesamte _Generic-Operator ähnlich wie bei einem Objekt-Like Makro (siehe Präprozessor:Object-Like Makros) durch expression
ersetzt.
default
wird genutzt, wenn kein kompatibler Datentyp gefunden wird. Wird default
nicht genutzt und es ist kein kompatibler Datentyp in der association-list
enthalten, so gibt es einen Compilerfehler.
Einsatz findet der _Generic-Operator vorrangig in Verbindung mit Makros, so dass Generische Funktionen darstellbar sind:
#define sqrt(X) _Generic((X), \
long double: sqrtl, \
default: sqrt, \
float: sqrtf \
)(X)
long double x=sqrt((long double)par); //sqrt wird durch sqrtl ersetzt
const int y=sqrt((long long)par); //sqrt wird durch sqrt ersetzt
float z=sqrt((float)par); //sqrt wird durch sqrtf ersetzt
Hinweise:
- Aufgrund der lvalue Konversion ist der kompatible Datentyp zu
"abc"
char *
und nichtchar[4]
- Alle gültigen Datentypen sind erlaubt, somit auch Zeiger auf Funktion und void
- Qualifiers wie
const
,volatile
,restrictv
und_Atomic
werden nicht beachtet - Siehe auch Paul J. Lucas _Generic in C
Register Storage Class Specifier
[Bearbeiten]Hinweis an den Compiler, dass diese lokale Variable 'häufig' genutzt wird und folglich der Compiler diese so optimieren soll, dass ein schneller Zugriff auf diese möglich ist.
Syntax: register Datentyp variablenname;
Der Storage Class Specifier kann nur auf lokale Variablen und auf Funktionsparameter angewendet werden. Hiermit wird der Compiler gebeten, die Variablen, sofern möglich, in einem Prozessorregister zu halten:
C-Programm | x86 32-Bit Assemblerprogramm |
---|---|
int var1=1;
register int var2=2;
int var3=3;
if(var1==var2)
printf("var1==var2");
|
...
mov DWORD PTR [ebp-12], 1 //var1
mov ebx, 2 //var2
mov DWORD PTR [ebp-16], 3 //var3
cmp DWORD PTR [ebp-12], ebx
jne .L2 //if(var1==var2)
...
.L2:
|
Hinweise:
- Moderne Compiler versuchen Variablen temporär im 'gecachten' Speicher zu legen oder teilen dem OS mit, dass die zugehörige Seite nicht ausgelagert werden darf. Folglich ist die Verwendung des Storage Class Specifier
register
nicht mehr nötig, bzw. wird sogar vom Compiler ignoriert - Ein Register hat keine Speicheradresse, so dass mit dem Adress-Operator keine Adresse ermittelt werden kann
- Ein Debugger stellt beim Darstellen von Variablen den Speicherinhalt der Variablen dar. Aufgrund dessen, dass Compiler Variablen im gecachten Speicher temporär verwalten, kann der angezeigte Variableninhalt vom tatsächlichen Variableninhalt abweichen
Volatile Type Qualifier
[Bearbeiten]Volatile 'informiert' den Compiler, dass die zugehörige Variable durch andere (nebenläufige Threads) manipuliert werden kann. Der Compiler schließt folglich diese Variable aus allen Optimierungsprozessen aus, so dass die Variable nicht über einen Sequence Point in Register zwischengespeichert werden darf. Stattdessen wird innerhalb eines Sequence Points bei einem Lesezugriff solch einer Variablen diese aus dem Speicher geladen resp. das Ergebnis zum Ende des Sequence Points zurückgeschrieben.
Syntax: volatile Datentyp variablenname;
Beispiel:
- Nutzung einer 'normalen' cachebaren Variablen zum Datenaustausch zwischen Threads
Consumer-Thread Producer-Thread int thread_run=0; //Cachebare Variable void consumer(void) { //Warten, bis Producer //Daten erzeugt hat! while(thread_run==0); //Consume Data }
extern int thread_run; void producer(void) { //Produce Data //Signalisiere Consumer, //dass Daten bereitstehen thread_run=1; }
x86 32-Bit Assemblerprogramm x86 32-Bit Assemblerprogramm consumer: mov eax, DWORD PTR thread_run .L2: test eax, eax je .L2 ret
producer: mov DWORD PTR thread_run, 1 ret
- Durch Compileroptimierung wird die Variable thread_run nur einmal aus dem Speicher gelesen und im jedem while-Zyklus der gecachte Inhalt (Inhalt der Variablen eax) genutzt. Änderungen in thread_run werden folglich nicht erkannt!
- Nutzung einer volatile Variablen zum Datenaustausch zwischen Threads
Consumer-Thread Producer-Thread volatile int thread_run=0; //None cacheable variable void consumer(void) { //Warten, bis Producer //Daten erzeugt hat! while(thread_run==0); //Consume Data }
extern volatile int thread_run; void producer(void) { //Produce Data //Signalisiere Consumer, //dass Daten bereitstehen thread_run=1; }
x86 32-Bit Assemblerprogramm x86 32-Bit Assemblerprogramm consumer: .L2: mov eax, DWORD PTR thread_run test eax, eax je .L2 ret
producer: mov DWORD PTR thread_run, 1 ret
- Volatile bedingt, dass der Inhalt der Variable thread_run bei jedem Schleifendurchlauf aus dem Speicher gelesen wird.
Anwendung:
- wenn Variablen für den Datenaustausch zwischen Threads, zwischen Hauptprogramm und Signalhandler und zwischen dem Hauptprogramm und ISR genutzt werden. (Sorgt jedoch nicht dafür, dass die Daten konsistent sind! Diese müssen z.B. gesondert über Semaphoren oder _Atomic gesichert werden)
- bei Hardwarezugriffen, da
- ein Lesezugriff immer von der Peripherie und nicht aus einem Cache bedient wird:
ad1=*(uint32_t *)(0xFFFFF000); ad2=*(uint32_t *)(0xFFFFF000); //Hier erfolgt kein separater Lesezugriff //Stattdessen optimiert der Compiler //den zweiten Lesezugriff durch den //zuvor gelesen Wert weg!
- Schreibzugriffe immer geschrieben werden und nicht wegoptimiert werden:
*(uint32_t *)(0xFFFFF004)=1; //Compiler optimiert diesen Schreibzugriff weg *(uint32_t *)(0xFFFFF004)=80; //und führt nur diesen aus!
- Hardware/Peripherie kann wie ein nebenläufiger Thread angesehen werden!
- zum 'Ausbremsen' einer For-Schleife, also zum aktiven Begrenzen der Ausführungsgeschwindigkeit eines Programmes:
for(volatile int delay=0;delay<100000;delay++) ...
Auto Storage Class Specifier
[Bearbeiten]In C ist der Storage Class Specifier auto ein Relikt aus der Vorgängersprache B, welche a) noch keine unterschiedlichen Datentypen kannte und b) lokale Variablen als solche explizit definiert werden mussten. Mit dem Schlüsselwort auto wurde dort ausgedrückt, dass es sich um eine lokale Variable (vom einzig vorhandenen Datentyp) handelt, resp. mit dem Schlüsselwort extern, dass es sich um eine globale Variable handelt.
In C (bis C17) wird mit auto (automatic storage Duration) dementsprechend ausgesagt, dass es sich um eine lokale Variable handelt. Wenn ergänzend kein Datentyp angegeben wird, wird der Datentyp der Variable implizit auf int gesetzt!
auto var; //KO, Lokale Variable kann hier nicht angelegt werden
void foo(void){
auto short a=1; //Lokale Variable, Datentyp short
auto b=1.0; //Lokale Variable, Datentyp int (implicit int)
printf("%zu %zu\n",sizeof(a),sizeof(b));
}
Variablendefinitionen innerhalb von Funktion werden auch ohne dem Storage Class Specifier Auto auf automatic storage Duration gesetzt, so dass dieses Schlüsselwort eigentlich entfallen könnte. Da jedoch einige Librarys (z.B. OpenSSL) diesen Storage Class Specifier nutzen (siehe Type Inference for object definitions ), ist 'auto' weiterhin Bestandteil der Spezifikation.
Ab C23 und in C++ wird mit dem Schlüsselwort auto ausgedrückt, dass der Datentyp der Variablen sich aus dem Initialisierungswert ergibt:
void foo(void) {
auto a=1; //Datentyp int
auto b=1.0; //Datentyp double
printf("%zu %zu\n",sizeof(a),sizeof(b));
}
Im Gegensatz zu C++ hat das Schlüsselwort auto ab C23 diverse Einschränkungen (siehe auto in C23)
- je Variabledefintion kann nur eine Variable definiert werden
auto a=1.0,b=2LL; //KO
- es kann kein Zeiger hierüber definiert werden
short b; auto *a=&b;
- auto kann nicht als Rückgabeparameter oder Übergabeparameter von Funktionen genutzt werden:
auto foo(auto par1, auto par2) {}
Const Type Qualifier
[Bearbeiten]Mit dem Type Qualifier const wird eine Variable auf nur lesbaren Zugriff gesetzt.
Syntax: Datentyp const Variablenname;
const Datentyp Variablenname;
Die Wertzuweisung solcher Variablen erfolgt über die Initialisierung. Der Schreibschutz erfolgt im Wesentlichen zur Compilezeit, welcher den Compilevorgang abbricht, wenn eine Zuweisung an solche eine Variable erfolgt (eine Const Variable kann kein lvalue sein):
int var0; //ReadWrite
const int var1=7; //ReadOnly
int const var2; //wenn var2 ein globale Variable, dann 0,
//andernfalls Zufallswert
var1++; //KO
var2=var0; //KO Datentyp (const int)=(int)
var0=var1; //OK Datentyp (int)=(const int)
Zur Laufzeit erfolgt keine Kontrolle. Wird bspw. die Adresse solch einer Variablen in einem Zeigervariablen gespeichert und diese im Anschluss dereferenziert, so hängt das Laufzeitverhalten von diversen Faktoren ab:
const int var=7;
int *ptr=&var;
*ptr=4711; //Schreibender Zugriff auf var.
//bei lokaler Variable führt dies zur Änderung der Konstanten
//bei globaler Variable führt dies zum Programmabsturz
Alle C-Compiler weisen in der Regel globalen und static lokalen Const-Variablen einer separaten Speicherklasse zu (rodata-Segment). In der Regel wird diese Speicherklasse bei:
- Embedded Systemen in der Tat nur im Speicher mit reinen Lesezugriff gehalten (ROM, Flash). Ein indirekter Schreibzugriff auf solche Variable bewirkt keine Änderung. Das Programm wird normal fortgeführt
- Systemen mit MMU (Windows, Linux, macOS) in einen Speicherbereich gehalten, der durch die MMU schreibgeschützt wird. Ein indirektes Schreiben auf diesen Bereich bewirkt somit ein Laufzeitfehler (Segmentation fault), verursacht durch die MMU
Const lokale Variablen werden wie normale lokale Variablen auf dem Stack gehalten, so dass hier kein zusätzlicher Schutz besteht. Ein indirektes Schreiben solch einer Variablen führt zur Änderung der Konstanten.
Anwendung
- Variablen, die nicht geändert werden sollen
- Variablenübergabe an Funktionen, zur Kennzeichnung, dass diese ReadOnly sind
void foo(const int par1) { par1=4711; //KO
Hinweise:
- Const Variable sollten initialisiert werden, da später kein schreibender Zugriff möglich ist. Der Compiler gibt leider bei nicht initialisierten Konstanten keinen Fehler aus!
- Const als Rückgabewert einer Funktion ist möglich, aber wenig sinnvoll. Im Gegensatz zu C++, bei welcher mit constexptr gesagt wird, dass die ganze Funktion Konstant ist (und dadurch zur Compilezeit ausgerechnet werden kann) wird bei C einzig gesagt, dass der Rückgabewert konstant ist:
const als Funktionsrückgabewert bei C constexpr in C++ const int func(int var) { return var*3; } int main(int argc, char *argv[]) { int par=7; //Rückgabewert ist Konstant const int var = func(par+1); return 0; }
constexpr int func(int var) { return var*3; } int main(int argc, char *argv[]) { int par=7; //Normaler Funktionsaufruf int var = func(par+1); //Aufgrund des konstanten Übergabeparameters //ist die ganze Funktion konstant und wird //zur Compilezeit ausgerechnet int var2 = func(4711); //Compiler ersetzt Funktionsaufruf durch int var=3*4711; return 0; }
- CONST Variablen erhöhen die Sicherheit eines Programmes. So sind bspw. in der Programmiersprache RUST alle Variablen const, resp. die Variable darf nur vom Owner geändert werden. Alle Borower haben nur lesenenden Zugriff
- Alternativ zu const Variablen können in C/C++ konstante Werte über Makros dargestellt werden, welche einen kompakteren/schnelleren Code ermöglichen:
const int var=7; #define MAKRO 7 if(var==7) ... if(MAKRO==7) ...
_Atomic Type Qualifier
[Bearbeiten]Threads sind nebenläufige Ausführungsstränge innerhalb eines Prozesses, welche sich alle globalen Variablen teilen. Die Zuteilung der Rechenzeit der einzelnen Threads erfolgt durch den Scheduler. Ein laufender Thread kann zu jedem Zeitpunkt die Rechenzeit entzogen/zugeteilt werden. Zu jedem Zeitpunkt bedeutet, dass der Entzug auf Maschinenspracheebene und nicht auf C-Befehlsbene stattfindet.
Ein Datenaustausch zwischen den Threads erfolgt typischerweise über globale Variablen. Wird ein Thread während des Zugriffs auf eine globale Variable angehalten, so besteht dies Gefahr von Dateninkonsistenz:
thread1 | thread2 |
---|---|
long long var=0x00000000FFFFFFFF;
void *thread1(void *par) {
while(1) {
//Do something
//var++;
}
}
|
extern long long var;
void *thread2(void *par) {
long long copy;
while(1) {
copy=var;
//Do something
}
}
|
x86 32-Bit Assemblerprogramm | x86 32-Bit Assemblerprogramm |
mov eax, DWORD PTR var
mov edx, DWORD PTR var+4
add eax, 1
adc edx, 0
mov DWORD PTR var, eax
//Threadwechsel hier
mov DWORD PTR var+4, edx
|
mov eax, DWORD PTR var
mov edx, DWORD PTR var+4
mov DWORD PTR [ebp-8], eax
mov DWORD PTR [ebp-4], edx
|
Findet ein Thread-Wechsel zwischen den beiden Schreibbefehlen mov DWORD PTR var...)
statt, so würde die Variable var zum einen aus dem neuen niederwertigen 32-Bits (hier 0xFFFFFFFF+1=0x00000000) und dem alten höherwertigen 32-Bits (hier 0x00000000) bestehen.
Zur Lösung des Problems stehen diverse Möglichkeiten zur Verfügung:
- Scheduling auf Hochsprachenebene (in C nicht gegeben)
- Absicherung des Zugriffs auf diese Variable über Semaphoren (händisch)
- Absicherung durch Sperrung des Interrupts (nicht sinnvoll)
- Absicherung über atomare (nichttrennbare/-unterbrechbare) Maschinenbefehle
Über den Type Specifier _Atomic wird seit C11 der Compiler angewiesen, Schreibzugriffe auf solche Variablen über atomare Maschinenbefehle abzusichern.
Syntax: _Atomic(type-name) Variablenname
_Atomic type-name Variablenname
Zugriffe auf solche Variablen werden als nicht unterbrechbare Einheit dargestellt, so dass sichergestellt ist, dass bei einem Zugriff immer der gesamte Wert gelesen/geschrieben wird:
_Atomic long long var=0x00000000FFFFFFFF;
void *thread1(void *par) {
while(1) {
//Do Something
var++; //Befehls wird als atomare Befehl ausgeführt
}
}
Der Type Specifier ist optional und muss vom Compiler nicht unterstützt werden. Auch werden nicht alle Datentypen unterstützt. In der Header-Datei "stdatomic.h" sind diverse Makros als Alternative/Ergänzung enthalten.
_Thread_local Storage Class Specifier
[Bearbeiten]Ergänzend zu lokalen (automatic Storage Duration) und globalen (static Storage Duration) Variablen wird mit Thread Local Storage eine für jeden erzeugten Thread eigenständige Variable erzeugt (Thread Storage Duration).
Syntax: __thread Datentyp Variablenname; //GCC
_Thread_local Datentyp Variablenname; //ab C11
thread_local Datentype Variablenname; //ab C++11
Die Speicherplatzreservierung und Initialisierung solcher Variablen erfolgt mit dem Start des Threads.
Beispiel:
//Compilerschalter: -lpthread
__thread int tls=2; //Thread Local Storage
//Alternative Weg für Thread Local Storage
struct data {
pthread_t id;
int data;
} data[2];
static void *run(void *arg)
{
struct data *this=arg;
printf("data->data=%d",this->data++);
printf("->%d\n",this->data);
printf("tls=%d",tls++);
printf("->%d\n",tls);
...
}
int main(int argc, char *argv[])
{
for(size_t lauf=0;lauf<sizeof(data)/sizeof(data[0]);lauf++) {
pthread_create(&data[lauf].id,NULL,run,(void *)&data[lauf]);
}
...
}
Die Variable errno
, in welcher der genaue Fehlercode abgelegt wird, wird von der Laufzeitumgebung als Thread Local Variable angelegt, so dass jeder Thread seine eigene errno
Variable besitzt.
Hinweis:
- Ändert die zugrundeliegenden Adressierungsart, wie auf Variablen zugegriffen wird
- Global: über feste Adressen -> absolute Adressierung
- Lokale: relativ zum Basepointer -> indizierte Adressierung
- Thread: relativ zu einem weiteren Pointer, der mit jedem Threadwechsel neu gesetzt wird. Siehe auch https://gcc.gnu.org/onlinedocs/gcc/Thread-Local.html#Thread-Local
- Speicherplatz für solche Variablen wird in den Sektionen
.tdata
und.tbss
reserviert. Siehe auch: https://uclibc.org/docs/tls.pdf
Sonstiges
[Bearbeiten]Storage Class Specifier (static, extern, auto, register, _Thread_local
) definieren die 'Speicherklasse', in der der Speicher für Variablen reserviert werden soll. Infolgedessen sind diese Specifier nur bei der Definition von Variablen anwendbar. Eine Anwendung bei der reinen Datentypbeschreibung und bei Struktur-/Unionelemente ist nicht möglich:
static struct xyz { //KO, static kann nicht auf den Datentyp angewendet
// werden
extern int x; //KO, extern kann nicht auf ein Strukturelement
//angewendet werden
int y;
};
void foo(void) {
register struct cd{ //OK, register wirkt auf die Variablendefinition
int c;
int d;
} var;
}
Datatypespecifier (const, volatile, _Atomic
) wirken sich auf den Zugriff aus, d.h. sie beschränken den Zugriff oder garantieren bestimmte Eigenschaften beim Zugriff (durch Nutzung anderer Maschinensprachebefehle). In diesem Sinne können Datatypespecifier sowohl auf Variablen, als auch auf Struktur-/Unionelemente angewendet werden:
struct xyz {
int a;
const int b; //Strukturelement ist nur lesbar
volatile int c; //Strukturelement wird beim Zugriff
//nicht gecached
_Atomic int d;
const volatile int e;
};
struct xyz xyz1;
xyz1.b=7; //KO Strukturelement x ist nur lesbar
xyz1.c=7; //OK
const struct xyz xyz2={1,2};
xyz2.x=7; //KO
xyz2.y=7; //KO
const struct ab{ //KO Datentyp kann nicht auf const gesetzt werden
int a;
int b;
};
volatile struct c{ //OK hier wirkt volatile auf die Variablendefinition
int c;
int d;
} hallo;
Compound-Literal entsprechen anonymen Variablen. Per Default sind diese Variablen 'normale' Variablen, also nicht im Zugriff beschränkt. Über Type Qualifier kann der Zugriff auf diese Variablen eingeschränkt werden:
(const float []){1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6};
struct xyz{int x,y,z;};
(volatile struct xyz){.y=2};
(_Atomic int []) {1,2,3};