Zum Inhalt springen

Programmieren in C/C++: DatentypeSpecifier

Aus Wikibooks


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

Öffnen im Compiler Explorer

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!

Öffnen im Compiler Explorer

  • 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;

Öffnen im Compiler Explorer

  • 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);

Öffnen im Compiler Explorer

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);
}

Öffnen im Compiler Explorer

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);
}

Öffnen im Compiler Explorer

  • 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);
}

Öffnen im Compiler Explorer

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++;
}

Öffnen im Compiler Explorer

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));

Öffnen im Compiler Explorer

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);

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

Hinweise:

  • Aufgrund der lvalue Konversion ist der kompatible Datentyp zu "abc" char * und nicht char[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:

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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++)
   ...

Öffnen im Compiler Explorer

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));
}

Öffnen im Compiler Explorer

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));
}

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

  • es kann kein Zeiger hierüber definiert werden
short b;
auto *a=&b;

Öffnen im Compiler Explorer

  • auto kann nicht als Rückgabeparameter oder Übergabeparameter von Funktionen genutzt werden:
auto foo(auto par1, auto par2) {}

Öffnen im Compiler Explorer

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)

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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;
}

Öffnen im Compiler Explorer

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;
}

Öffnen im Compiler Explorer

  • 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

Öffnen im Compiler Explorer

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
  }
}

Öffnen im Compiler Explorer

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]);
  }
  ...
}

Öffnen im Compiler Explorer

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

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;
}

Öffnen im Compiler Explorer

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;

Öffnen im Compiler Explorer

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};

Öffnen im Compiler Explorer