# Programmieren in C/C++/ Druckversion

* Druckversion des Buches Programmieren in C/C++
* Dieses Buch umfasst derzeit etwa ? DIN-A4-Seiten einschließlich Bilder.
* Wenn Sie dieses Buch drucken oder die Druckvorschau Ihres Browsers verwenden, ist diese Notiz nicht sichtbar.
* Zum Drucken klicken Sie in der linken Menüleiste im Abschnitt „Drucken/exportieren“ auf Als PDF herunterladen.
* Mehr Informationen über Druckversionen siehe Hilfe:Fertigstellen/ PDF-Versionen.
* Hinweise:
* Für einen reinen Text-Ausdruck kann man die Bilder-Darstellung im Browser deaktivieren:
* Internet-Explorer: Extras > Internetoptionen > Erweitert > Bilder anzeigen (Häkchen entfernen und mit OK bestätigen)
* Mozilla Firefox: Extras > Einstellungen > Inhalt > Grafiken laden (Häkchen entfernen und mit OK bestätigen)
* Opera: Ansicht > Bilder > Keine Bilder
* Texte, die in Klappboxen stehen, werden nicht immer ausgedruckt (abhängig von der Definition). Auf jeden Fall müssen sie ausgeklappt sein, wenn sie gedruckt werden sollen.
* Die Funktion „Als PDF herunterladen“ kann zu Darstellungsfehlern führen.

# Vorwort
## Wie ist dieses Buch entstanden

Dieses Buch stammt aus der Vorlesung 'Programmieren in C' und dient dort als Skript für diese Vorlesung. Die Motivationen für die Veröffentlichung des Skripts in Wikibooks sind:
* Bisher kein vernünftiges Lehrbuch für die Programmiersprache C gefunden. Viele Bücher basieren vorrangig auf praktischen Beispielen und vernachlässigen syntaktische Aspekte
* Bereitstellung des Wissens für Personen außerhalb der Vorlesung
* Interaktivität: Das heißt vorerst nicht, dass hier jeder mitschreiben soll. Vielmehr geht es mir um Rückmeldung bzgl. Verbesserungsvorschläge, Ergänzungsvorschläge,... über Diskussionsbeiträge

## Für wen ist dieses Buch

Die Zielgruppe für dieses Buch sind Personen mit Programmierkenntnissen. Es wird vorausgesetzt, dass der Leser weiß, was eine IF-Bedingung ist und wann eine FOR-Schleife anzuwenden ist. Schwerpunkt dieses Buches ist der Syntax der Sprache C/C++, die 'Interpretationen' daraus und die praktische Anwendung hiervon. Der Syntax von C/C++ kennt nur wenige Ausnahmeregeln, so dass quasi alles an allen Stellen erlaubt ist. Für unerfahrene Nutzer dieser Sprache führt dies oftmals zu Frust und der Aussagen wie 'Scheiß Compiler' / 'Scheiß Sprache'. In der Tat liegt das Problem vor der Tastatur. Moderne Programmiersprachen haben aus dem offenem Syntax von C/C++ gelernt und den Syntax entsprechend eingeschränkt. D.h. bspw. den Komma-Operator nur in FOR-Schleifen erlaubt und Zeiger durch Referenzen ersetzt. Damit wurden diverse Fehlerquellen unterbunden, aber gleichermaßen die Flexibilität der Sprache reduziert. Bei korrekter Anwendung der Syntaxen kann auch mit C/C++ saubere, sichere und vor allem schnelle Programme geschrieben werden.
Im Vergleich zu anderen Sprachen entspricht C (mit den Worten eines geschätzten Kollegen gesagt) einem 'senkrechtstartenden Düsenjäger'. Wenn der Pilot (resp. der Programmierer) das Werkzeug nicht beherrscht, führt es schnell zum Crash ('Scheiß Compiler'/'Scheiß Sprache'). Wenn er es beherrscht, kann er die Vorteile beider Welten nutzen.

## Compiler ist dein Freund und Helfer

Fehlermeldungen des Compilers werden nur ungern gelesen (derweil sie sich kryptisch anhören). Anstatt die Fehlermeldung zu lesen und zu verstehen, sehe ich oftmals folgenden Umgang mit Fehlermeldungen:
* Nutzung der Lösungsvorschläge einer integrierten Entwicklungsumgebung
* Vermeidung der Fehlermeldung durch Umstellung des Codes:
    Mit CompilerfehlerUmgehung der Fehlermeldung int *foo(void) { int arr[10]; ... return arr; //Error, aufgrund der Rückgabe //einer Adresse einer lokalen //Variablen } int *foo(void) { int arr[10]; int *ptr; ... ptr=arr; //Adresse wird zuvor in eine //lokale Variable kopiert, so //dass der Compiler nicht prüfen //kann, woher die Adresse stammt return ptr; }
    Öffnen im Compiler Explorer
* Googlen nach Lösungsvorschlägen und Nutzung dieser (wobei die Autoren der Lösungsvorschläge den Fehler oftmals ebenfalls nicht verstanden haben und somit nur ein Workaround vorschlagen)
* Ignorieren von Warnings (eigentlicher Fehler rächt sich dann zumeist später)
    void foo(void) { int *ptr; *ptr=4711; //Warning, ptr is uninitialized }
    Öffnen im Compiler Explorer
In der Tat ist die Fehlermeldung kryptisch. Der Grund für eine Fehlermeldung ist, dass der Source-Code von der Spezifikation abweicht und der Compiler entsprechend keinen Assemblercode generieren kann. Die Fehlermeldung beschreibt somit die Syntaxverletzung. Sie ist eine Hilfestellung an den Programmierer, was genau falsch ist. Als Programmierer sollte man sich bei Fehlermeldungen überlegen, was wollte man mit dem Ausdruck erreichen und was hat man in der Tat beschrieben. Also nicht einfach den Fehler korrigieren, sondern die Fehlermeldung als Anregung verstehen, den Code zu überdenken.
In den letzten Jahren sind die Compiler so intelligent geworden, dass diese bei vielen typischen Programmiererfehlern eine Warning ausgeben. Sie meckern, wenn bspw. eine Zuweisung in einer IF-Bedingung steht oder einer IF-Anweisung kein Block folgt (beides ist syntaktisch korrekt, sind aber gleichermaßen auch typische Zeichen für mögliche Fehler). Defaultmäßig sind einige diese Warnings deaktiviert. Da sie helfen können, mögliche Fehler zu vermeiden empfiehlt es sich, den Warning Level auf den höchsten Mode zu setzen (Compilerschalter: -Wall).

## Programmieren lernt man nur durch Programmieren

Lesen ist ein wichtiger Aspekt des Lernens, ausprobieren ein Weiterer. Beide ergänzen sich prima. In diesem Sinne sind im Buch viele Code-Beispiele vorhanden, ohne und zum Teil mit bewussten Fehlern. Damit die notwendigen Include Anweisungen und ggf. Funktionsrümpfe den Beispielcode nicht unnötig aufblähen, wurde ein LUA Script entworfen (Danke an meinen Mitarbeiter), welches den im (verborgenen) im Buch enthaltenen Code in einen Compiler Explorer Link konvertiert. Über 'Öffnen im Compiler Explorer' können alle Beispiele ohne Abtippen direkt ausprobiert werden. Änderungen im Compilerexplorer sind lokale Änderungen und für den 'Rest' nicht sichtbar. Über das LUA Script kann nicht nur Code vorgegebenen werden, sondern auch Compilerschalter und mehr gesetzt werden. Die Dokumentation zum Lua Code ist hier zu finden: Lua Doku. Der Lua Code selbst ist hier zu finden: Lua Code.

## C/C++

Das Augenmerk des Buches liegt derzeit bei der Programmiersprache C. Viele grundlegende Konzepte sind in C und C++ identisch. Da C++ eine eigene Sprache ist, sind einige syntaktischen Abweichungen vorhanden. Im derzeitigen Stand des Buches bedeutet die Ergänzung C++ im Titel des Buches, diese syntaktischen Abweichungen zu beschreiben. Zukünftig ist geplant, die ergänzenden Konzepte wie Objektorientierung, Templates, Namensraum, ... zu ergänzen.

## Compiler

Die C-Spezifikation ist die Grundlage für alle Compilerhersteller. Insofern sind diese austauschbar. Die Spezifikation regelt das allgemeine Verhalten der Sprache, wie der Compiler dieses umsetzt, obliegt ihm. Über Compiler spezifische Schalter kann dem Compiler detailliertere Anweisungen für die Umsetzung gegeben werden (bei GCC über u.A. über __attribute__((attribute-list)), ab C++11 über [ [attribute-list] ]. An vielen Stellen gibt die C-Spezifikation ein undefiniertes Verhalten vor (z.B. bei der Rückgabe einer Adresse einer lokalen Variablen). Das Verhalten vom Compiler an diesen Stellen ist compilerabhängig. Einige fügen zusätzlichen Code ein (um z.B. zur Laufzeit einen Laufzeitfehler zu provozieren (Rückgabe der Adresse 0 im obigen Beispiel)), einige tätigen gar nichts. Ergänzend zur C-Spezifikation ergänzen einige Compiler die Spezifikation um zusätzliche Features. GCC beispielsweise ermöglicht es, in einer SWITCH-Anweisung ein CASE über einen Wertebereich zu beschreiben (Case 1 ... 3:).
Auch wenn die Sprache C sauber spezifiziert ist, wird man früher oder später compilerspezifische Eigenschaften nutzen und sich damit an einen Compiler binden. Im Falle dieses Buches ist es der GCC Compiler aus der GNU Compiler Collection.

## Sonstiges

Die C-Spezikation ist an einigen Stellen nur sehr schwer zu vermitteln. In diesem Sinne wird an einigen Stellen im Buch von C Spezifikation abgewichen. Dies soll der besseren Lesbarkeit/Verständnis dienen.

# Einführung/Literatur
## Literatur

Kurze Literaturaufzählung, nach Relevanz sortiert:
* ©ISO/IEC ISO/IEC 9899:TC2 "Programming languages - C" Spezifikation der Sprache. C11: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf C05x: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf Anmerkung: Schwer verständlich
* Harbison, Samuel P (Steele, Guy L.;) C: A reference Manual ISBN: 013089592X (pbk) Upper Saddle River, N.J. [u.a.] Prentice Hall, 2002 Anmerkung: Gute Spezifikation der Sprache. Kein Selbstlernbuch.
* Kernighan, Brian W (Ritchie, Dennis M.; Schreiner, Axel T.;) Programmieren in C mit dem C-Reference Manual in deutscher Sprache ISBN: 3446154973 ((pbk.)=978-3-446-15497-1*hbk) ISBN: 013110330X URL: http://zbmath.org/?q=an:0701.68014 München [u.a.] Hanser [u.a.],1990 Anmerkung: Spezifikation (nach für Kernighan Standard) auf Basis von Anwendungsbeispielen
* Wolfgang Sommergut Programmieren in C URL: http://c-buch.sommergut.de/index.shtml

## Spezifikation der Standardbibliotheksfunktionen

Die Spezifikation der Standard Header Dateien und der Standard Library Funktionen sind Bestandteil der C-Spezifikation [C11 7.x] und beanspruchen ~⅓ der Seiten. POSIX (Portable Operating System Interface) ist eine für UNIX entwickelte standardisierte Programmierschnittstelle u.A. für Betriebssystemaufrufe. Es beinhaltet und erweitert aber auch die Beschreibung der Standard-C Header Dateien und der Standard-C Library Funktionen. Da die POSIX Beschreibungen in den Manpages von UNIX und verwandten Betriebssystemen enthalten sind, empfehlen sich diese als alternative/ergänzende Beschreibung. Neben der
* detaillierten Funktionsbeschreibungen,
* der Parameterbeschreibung,
* der Beschreibung des Rückgabewertes
* der Fehlerarten
* und der zur Nutzung zu inkludierenden Header-Dateien
enthalten einige dieser Spezifikationen ergänzend Anwendungsbeispiele.
In Unix basierten Systemen ist die Spezifikation Bestandteil der Distribution und kann über die Kommandozeile wie folgt aufgerufen werden (Voraussetzung, gcc ist installiert):
Alternativ sind diese im Netz verfügbar. Hier empfiehlt es sich, das Wort 'man' der Suche voranzustellen, so dass in den ersten Suchergebnissen direkt ein Link auf die Manpages enthalten ist. Wenn nur der Funktionsname als Suchbegriff eingegeben wird, wird als Suchergebnis zumeist Anwenderinterpretationen geliefert, die oftmals nur die 'halbe' Wahrheit darstellen oder z.T. falsch sind.

## Sprachenvergleich

Die Programmiersprache C gibt es schon seit 50 Jahren. Nach seiner 'Hoch'-Zeit in den 90er Jahren ist diese Sprache zwar nicht mehr führend, aber immer noch unter den Top 5 Sprachen zu finden:
* IEEE Language Ranging 2019 (Quelle: The top programming languages 2019)
    1. Python
    2. Java
    3. C
    4. C++
    5. R
    6. JavaScript
    7. C#
    8. Matlab
    9. Swift
    10. Go
* TIOBe-Index Stand Januar 2023 (Quelle: TIOBe-Index)
    1. 16,36% Python
    2. 16,26% C
    3. 12,91% C++
    4. 12,21% Java
    5. 5,73% C#
    6. 4,64% Visual Basic
    7. 2,87% Java Script
    8. 2,50% SQL
    9. 1,60% Assembly Language
    10. 1,37% PHP
Gründe für die gute Positionierung:
* hohe Ausführungsgeschwindigkeit des compilierten Programms
* kein zusätzlicher Interpreter zur Ausführung notwendig (Compilersprache)
* Systemprogrammierung (Betriebssystem, Gerätetreiber sind zumeist in C/C++ geschrieben)
* eingebetteten Systemen sind in C/C++ geschrieben
* (Java) Interpreter und diverse Java 'Libraries' sind in C geschrieben
* Python Librarys sind in C/C++ geschrieben
* C Compiler sind quasi für alle Betriebssysteme und embedded Systeme verfügbar (auch aufgrund der kleinen Library)
* Energieeffizeint (siehe Heise - Grünes Programmieren in C und Rust)
Hinweis:
* Christoffer Lernö fragt sich in seinen Blog, ob/wie C durch eine andere Sprache ersetzt werden kann: The case against a c alternative

## C-Dialekte

1972 wurde C von Brian Kernighan und Dennis Ritchie mit dem Ziel entwickelt, Unix für div. Rechnersysteme (und damit nicht mehr in Assembler geschrieben) verfügbar zu machen. Dazu wurde die Vorgängersprache B unter anderem um Datentypen (einhergehend mit der Bereitstellung dieser durch die PDP-11, einem quasi Standardcomputer der 70er Jahre) und um die klare Trennung von Integer- und Pointer-Variablen erweitert (siehe Bell-Labs).
Aufbauend auf dieser Grundlage wurde C stetig weiterentwickelt. (siehe Varianten der Programmiersprache C):
* K&R C (1978)
    Kein offizieller Standard, jedoch wurde mit dem Buch von Kerningham & Ritchie 1978 (siehe Literatur) die Sprache erstmals zusammenhängend beschrieben, so dass das zugrundeliegende Buch als quasi Standard gilt.
    Besonderheiten (anhand eines Beispielcodes)
    func(str) char * str; /* 1) */ { /* 2) Zunächst Definitionsbereich */ int a; /* 3) */ b; /* 4) */ /* 2) Ab hier dann Anweisungsbereich */ b=a+1; /* 2) Hier nun keine weitere Variablendefinition erlaubt */ func(4,5,6); /* 5) */ ... // 1) Datentypdefinition bei Funktionsparameter außerhalb der-Klammern // 2) am Anfang des Blockscopes ist der Definitionsbereich, in welchen // die Variablen definiert werden. Dieser endet mit der ersten Anweisung. // Ab der ersten Anweisung können keine weiteren Variablen definiert werden. // 3) Nur wenige Datentypen vorhanden // 4) Implizites Int, d.h. Variablen/Funktionen ohne vorangestellten // Datentyp sind automatisch vom Datentyp int. // 5) kein Funktionsparametercheck, d.h. eine Funktion konnte mit anderen // Parameter aufgerufen werden, als in der Tat benötigt wurden (u.A. // aufgrund dieser Tatsache wurde das Tool lint entwickelt, welches // eine Prüfung vornimmt)
* ANSI-C oder C89/90
    Erste offizielle Spezifikation der Sprache.
    Compiler-Schalter zum Aktivieren dieser Version: -ansi oder -std=c90
    Besonderheiten:
   * Einführung weiterer Datentypen: short, long
   * Funktionsprototypen mit dem sich daraus ergebenden Funktionsparametercheck
   * Einführung weiterer Schlüsselwörter: const, volatile, unsigned
   * Einführung des Präprozessors
   * Datentypdefinition von Funktionsparameter nun in den-Klammern
   * uvm.
* C99
    Viele Optimierungen, die sich u.A. im Zuge mit der Einführung von C++ ergeben haben.
    Compiler-Schalter zum Aktivieren dieser Version: -std=c99
    Besonderheiten:
   * for-Schleife erzeugt einen eigenen Block, so dass innerhalb der for-Anweisung eine nur im Anweisungsbereich der for-Schleife lokale Variable definiert werden kann (for(int lauf=0;...)
   * Einführung von Zeilenkommentare (enden automatisch am Zeilenende) '//'
   * Frei platzierbare Definitionen, d.h. Variablen-Definitionen können nun auch zwischen Anweisungen stehen
   * VLA Variable Length Array; Array, dessen Dimension erst zur Laufzeit festgelegt wird
   * Inline Funktionen
   * Designated Initializers (z.B. struct point p={.x=1,.y=2}; )
   * Compound Literals (z.B. var=(struct xyz){1,2,3}; )
   * Neue Datentypen: long long / float _complex / double _complex /…
   * Verbot des impliziten int, d.h. Compiler gibt mindestens eine Warning, wenn z.B. eine Funktion ohne Rückgabewert definiert wurde, bzw. der Datentyp eines Übergabeparameters nicht gesetzt wurde
* C11 (Default bei gcc)
    Wenige Änderung zu C99. Enthält im wesentlichen Features, welche Compilerhersteller aufgrund von Anwenderforderungen vorab im Compiler implementiert hatten
    Compiler-Schalter zum Aktivieren dieser Version: -std=c11
    Besonderheiten
   * Unterstützung von Multi-Threading in Form durch Bereitstellung von "thread local storage class specifier" (Sichtbarkeit/Gültitgkeit von Variablen auf Thread Basis) und Bereitstellung von Funktionen zur Verwaltung von Threads, Mutexen, condition variable und atomic Operationen
   * Anonymous Structures und Unions (sinnvoll z.B. für einfacheren Strukturierung von Datentypen)
   * Entfernung der 'unsicheren' gets Funktion
   * und einige weitere
* C17
    Enthält im Wesentlichen nur Fehlerkorrekturen von C11 und keine neuen Features
    Compiler-Schalter zum Aktivieren dieser Version: -std=c17
* C23
    In Entwicklung. Voraussichtliche Veröffentlichung 2024
    Änderungen: siehe bspw. C23 implications for C libraries
    Besonderheiten
   * Unterstützung des Unicode Zeichensatzes
   * Testfunktionen für Addition/Subtraktion/Multiplikation auf Integerüberlauf
   * nullptr als Ersatz für NULL
In diesem Skript wird vorrangig der Standard C11 genutzt. Die Ergänzungen, welche C17 und spätere Versionen mitbringen, sind nach Ansicht des Autors nur Features. Am grundlegenden Syntax wurden hier nur geringfügige Änderungen vorgenommen.
Hinweise:
* Ergänzend zu diesen Standards erweitern einige Compilerherstellen den Standard um weitere Features. Sofern diese (oftmals nützliches) Features genutzt werden, baut man sich Compiler-Abhängigkeiten in seinen Code ein
* Viele etablierten Programme/Libraries nutzen C89/90 als Standard. So basieren bspw. cURL, SQLite, libxml2 auf diesen Standard. Auch der Linux Kernel bis Version 5.18 (Mai 2022) basiert auf C89

## C Besonderheiten
### C-Syntax

Java, JavaScript, PHP uvm. orientieren sich am Syntax der Programmiersprache C. Eine Einführung in den allgemeinen Syntax entfällt somit. Einzig werden hier einige Besonderheiten dargestellt:
* Im C-Syntax gibt es nur wenige Ausnahmeregeln, so dass quasi alles an allen Stellen erlaubt ist:
    //Aufruf einer Funktion im Funktionsparameterbereich int main(int argc,char *argv) { if( ({int var=1; var>argc;})) //Hier wird eine Variable //innerhalb des IF-Konstruktes angelegt! switch(argc) { int abc; //Variable außerhalb des Case-Bereiches case 1: int var1=8; hallo: //Sprungziel innerhalb von Switch case 2: var1*=2; //Vorsicht, in diesem Case-Zweig ist var1 nicht break; //initialisieirt (void)abc; //Zur Unterbildung der Compilerwarning } else goto hallo; //Sprung zu einer Stelle innerhalb der Switch-Anweisung for(int lauf1=7,lauf2=8;lauf2++,lauf1--;printf("-")); return 0; }
    Öffnen im Compiler Explorer
* Die C-Spezifikation ist so ausgelegt, dass der C-Code in kompakten/schnellen Maschinencode übersetzt werden kann und die Prozessoreigenschaften optimal genutzt werden. Dementsprechend sind in der C-Spezfikation diverse 'undefined/unspecified Behavior' vorhanden. Beispiele sind:
   * Bitbreite des Datentyp integer ist vom Prozessor und Compiler abhängig
   * Verhalten bei Integerüberlauf/unterlauf ist von der Prozessorarchitektur abhängig
   * Auswertereihenfolge der Übergabeparameter beim Funktionsaufruf ist nicht definiert
    Bei Nichtbeachtung kann dies zu nicht portablen (auf andere Compiler oder andere Prozessoren) oder gar fehlerhaften Programmen führen (siehe auch: Undefined Behavior in C and C++)
* Einige Operatoren haben in Abhängigkeit der Verwendung unterschiedliche Bedeutungen
    '*' -> Ganzzahl Multiplikation, wenn beide Operanden vom Datentyp Ganzzahl sind
    '*' -> Gleitpunktzahl Multiplikation, wenn eine der beiden Operanden vom Datentyp Gleitpunktzahl und der andere von Ganzzahl ist
    '*' -> Datentyp Zeiger anlegen, wenn der linke Operand ein Datentyp ist
    '*' -> Dereferenzierung, wenn der rechte Operand vom Datentyp Zeiger ist
* Ausführbarer Code in der Parameterliste von Funktionen
    int main(int argc,char *argv[printf("%*c",argc,'a')])
    Öffnen im Compiler Explorer
Folglich sind viele Anweisungen vom Syntax korrekt (so dass der Compiler keine Fehler ausgibt):
* führen andere Aktionen aus, als gedacht
* werden zur Verschleierung des Source-Codes genutzt (Unleserlicher Code)
* werden von Profi-Programmierer angewendet, um kleinere/schnellere Programme zu schreiben

### C-Fallstricke

Das Ziel der Spezifikation von C war eine universelle/schnelle Programmiersprache (als Ersatz für Assembler und den komplexen Sprachen wie Algol und Fortran). Aus Geschwindigkeitsaspekten sind einige Anweisungen von der zugrundeliegenden Rechnerarchitektur abhängig, so dass C nicht portabel ist. Ebenso werden aus Geschwindigkeitsgründen keine unbedingt notwendigen Überprüfungen zur Laufzeit durchgeführt:
* Die Datentypen sind nicht klar definiert (Datentyp int ist 16- oder 32-Bit breit, abhängig von der Rechenbreite der CPU)
* C kann den gesamten vom Prozessor adressierbaren Speicherbereich für Variablen und Programm nutzen, d.h. Breite des Datentyps Zeigers ist von der Rechenbreite der CPU abhängig
* Keine Prüfung der Indices bei Arrayzugriffen
    char arr[10]; arr[1]='a'; arr[10]='z'; //Korrekt arr[-1]='x'; //Korrekt
    Öffnen im Compiler Explorer
* Keine Prüfung, ob bei der Dereferenzierung von Pointer diese auf eine gültige Speicheradresse zeigen
    int vari=3; int *ptr=&vari; vari=*++ptr; //Zugriff auf falsche Variable ptr=(int *)100; //Initialisierung des Pointers mit absoluter Adresse
    Öffnen im Compiler Explorer
* Implizite Typkonvertierung bei Ganzzahl und Gleitkommazahlen
    char varc; short vars; int varii; float varf=sin(varc*vars)+varii; //entspricht: //float varf=(float)(sin((double)((int)varc*(int)Vars))+(double)varii);
    Öffnen im Compiler Explorer
* C stellt dynamischen Speicher (Heap) zur Verfügung. Der Anwender ist sowohl für die Reservierung als auch für die Freigabe des zuvor reservierten Speichers zuständig

### Laufzeitfehler

Neben Syntax-Fehler (die durch den Compiler zur Compilezeit ausgegeben werden) gibt es insb. in C bei unsachgemäßer Programmierung viele Laufzeitfehler. Diese führen:
* zum Programmabsturz während der Laufzeit
* zu einem merkwürdigen Verhalten während der Ausführung des Programms (Variable hat auf einmal einen anderen Wert, als erwartet)
Dies liegt unter anderen an folgenden Sachverhalten:
* Keine Prüfung der Indices bei Arrayzugriffen
* Keine Prüfung, ob bei der Dereferenzierung von Pointer dieser auf eine gültige Speicheradresse zeigt.
* Keine Prüfung, ob bei der Allokation von lokalen Variablen genügend Speicher (Stack) zur Verfügung steht.
* Zahlenüberlauf bei Integer-Arithmetik ist nicht definiert (siehe 'Integer-Arithmetik')
    int a=2000000000; unsigned int b=1000000000; int c=a+b; //c=-1294967296
    Öffnen im Compiler Explorer
* Division durch 0, welche nicht durch eine Exception abgefangen wird
* C-Spezifikation an einige Stellen ein undefiniertes Verhalten beschreibt, also dem Compilerhersteller die Umsetzung der Anweisung in Code frei lässt
    int lauf=5; lauf=++lauf * ++lauf + ++lauf * ++lauf; //GCC-Compiler -> lauf=130 //Clang-Compiler -> lauf=114
    Öffnen im Compiler Explorer
Die ersten beiden Fehler werden als Pufferüberlauf/BufferOverflow, die fehlende Stackprüfung als Stapelüberlauf/StackOverflow bezeichnet.
Laufzeitfehler sind für den ungeübten Programmierer schnell erstellt, jedoch nur schwer zu finden. Vorsorge (also die Analyse des Codes) ist hier die bessere Variante als Nachsorge (den Fehler erst beim Auftreten zu suchen). Insbesondere bei Nutzung von Pointern sollte der Source-Code vorm Übersetzen auf bspw. folgende Sachverhalten geprüft werden:
* wurde der Pointer vor der Dereferenzierung mit einer gültigen Speicheradresse initialisiert
* ist der Speicherbereich, auf den der Pointer zeigt, gültig
* beinhaltet der Zeiger den Wert NULL, welcher als Fehlerfall genutzt wird
* zeigt der Zeiger nach einer Änderung (z.B. durch Zeigerarithmetik) weiterhin auf eine gültige Adresse
Ähnliche Sachverhalte gilt es bei Nutzung von Arrays zu prüfen
* liegt der Indice innerhalb der Array Grenzen
* ist sichergestellt, dass bei Übergabe eines Arrays an eine Funktion (sowohl selbstgeschrieben Funktion als auch Libraryfunktionen) diese nicht über die Grenzen des Arrays auf das Array zugreift
Ergänzend zu oben aufgeführten empfehlenswerten Codereviews empfehlen sich folgende Ansätze/Hilfsmittel zum finden/vermeiden von Laufzeitfehlern:
* Bewusste Nutzung des C Syntax. So können mit dem Schlüsselwort 'const' die Zeiger in ihrer Verwendung eingeschränkt werden (wird leider nur selten genutzt)
* Erweiterte Syntaxprüfung des Compiler nutzen, z.B. durch den Compiler-Schalter: '-Wall -Werror'
* Nutzung externer Codeanalysetools (wie z.B. lint) welche eine tiefergehende Codeanalyse als der Compiler durchführen und bei welcher einige C Funktionalitäten 'deaktiviert' werden können
* Nutzung des Code sanitizer (-fsanitize=address), welcher im erzeugten Maschinencode zusätzliche Testfunktionen einbaut und Canarie zwischen den einzelnen Variablen legt, so dass Bufferüberläufe und 'Use-after-Free' Zugriffe durch Prüfung der Canarie erkannt werden
* Nutzung von Valgrind, welcher den auszuführenden Maschinencode in einen anderen Zwischencode übersetzt und diesen unter seiner Kontrolle in einer virtuellen Maschine ausführt (Verwendung von Valgrind siehe bspw.: Valgrind memcheck: Different Ways to lose your memory)
* Nutzung des _FORTIFY_SOURCE Makros (sofern durch GNU-C Library bereitgestellt) zur Vermeidung von Bufferoverflows (siehe auch # GCC's new fortification level: The gains and costs)
Hinweis:
* Ausgaben auf die Standardausgabe (bspw. über printf) sind gepuffert, d.h. sie werden nicht unmittelbar zur Anzeige gebracht. Stürzt das Programm aufgrund eines Laufzeitfehlers ab, so wird dieser Puffer nicht abschließend zur Anzeige gebracht. Im Falle von Laufzeitfehler empfiehlt sich das regelmäßige flushen des Puffers mittels 'fflush(stdout)' resp. der Nutzung der Standfehlerausgabe mittels 'fprintf(stderr,"Fehler")' oder 'perror("Fehler")'

### Zusammenfassung

C wurde mit dem Ziel entwickelt, als höhere Programmiersprache zum Erstellen eines Betriebssystems zu dienen. Schnelle Programmausführung stand somit im Vordergrund. Die fehlenden Prüfungen bei bspw. Arrayzugriffen und Pointerdereferenzierung sind gewollt. C geht davon aus, dass der Programmierer weis, was er tut. Wenn der Programmierer bestimmte Sachverhalte während der Laufzeit nicht sicherstellen kann, so muss die notwendige Überprüfung durch zusätzlichen Code händisch sichergestellt werden!
Wie gesagt ist C ein offener Syntax, welcher nur wenige Einschränkungen kennt und den Syntax ohne Schnick-Schnack in Maschinencode umsetzt. Das heißt nicht, dass C eine schlechte Programmiersprache ist, sondern dass vorausgesetzt wird, dass der Programmierer weiß, was er tut:
* Er kennt die Wertebereiche der unterschiedlichen Datentypen (Plattformabhängig) und kennt die impliziten Typkonvertierungsregeln, so dass keine Typverletzungen auftreten
* Er kennt die Dimension des Arrays und stellt sicher, dass kein Zugriff auf ein Arrayelement außerhalb der Dimension erfolgt.
* Wenn er Speicher im Heap reserviert, gibt er diesen nach Verwendung wieder frei.
In vielen neueren Sprachen wurde der Ansatz "Alles ist überall erlaubt / Der Programmierer weiß was er tut" eingeschränkt, indem:
* der Syntax eingeschränkt wurde
* zur Laufzeit Überprüfung stattfinden und ggf. eine Ausnahmebehandlung im Fehlerfall ausgeführt wird
* eine komfortable Speicherverwaltung (Garbage Collector) integriert wurde
Die Programmiersprache C fordert vom Programmierer somit mehr Bewusstsein für die Sprache ab, als andere Programmiersprachen. Wer der Sprache C mächtig ist, wird sicherlich auch andere Sprachen bewusster nutzen!
Hinweis
* Im Blog vom Tom Mewet sind einige wesentlichen Unterscheidungen von C zu anderen Sprachen dargestellt

## Compiler Sprache

C ist eine Compiler-Sprache. Aus dem Source-Code wird durch die Toolchain, bestehend aus Compiler und Linker ein vom Prozessor direkt ausführbarer Maschinencode erzeugt.
Im Wikipedia Artikel Compiler wird die Aufgabe eines Compilers wie folgt zusammengefasst: "Ein Compiler (auch Kompilierer; von englisch compile 'zusammentragen' bzw. lateinisch compilare 'aufhäufen') ist ein Computerprogramm, das Quellcodes einer bestimmten Programmiersprache in eine Form übersetzt, die von einem Computer (direkter) ausgeführt werden kann."
Beispiel eines Übersetzungsvorganges: C-Code
Assemblercode (etwas vereinfacht dargestellt)
| X86-64 | ARM | 6502 | Anmerkungen |
| MOV edx,DWORD PTR a[rip] MOV eax,DWORD PTR b[rip] | LDR r1,.LCPI0_0 LDR r0,[r1] LDR r2,.LCP0_1 LDR r2,[r2] | LDA _a LDX _a+1 JSR pushax LDA _b LDX _b+1 | Lade Inhalt der Variablen in die Register |
| ADD eax,edx | ADD r0,r0,r2 | JSR tosaddax | Addiere die Variablen |
| ADD eax,7 | ADD r0,r0,#7 | JSR incax7 | Addiere die Konstante 7 |
| MOV DWORD PTR a[rip],eax | STR r0,[r1] | STA _a STX _a+1 | Speichere Register zurück in die Variablen |
Der vom Compiler erzeugte Maschinencode liegt letztendlich als Zahlencode im Speicher (hier nicht dargestellt) und wird vom Prozessor Befehlsweise eingelesen und direkt ausgeführt. Der Start des erzeugten Maschinencode, die Rückkehr zum Betriebssystem und der Aufruf von Library-Funktionen (z.B. printf) bedingen ergänzende Abhängigkeiten. Für jeden Prozessor und jedes Betriebssystem ist folglich ein gesonderter Compiler notwendig.

### Arbeitsweise eines Compilers

Der Wikipedia Artikel Compiler beschreibt die prinzipiellen Schritte bei der Übersetzung eines Quellcodes in einen Ziel Code wie folgt:

#### Syntaxprüfung

    es wird geprüft, ob der Quellcode ein gültiges Programm darstellt, also der Syntax der Quellsprache entspricht. Festgestellte Fehler werden protokolliert. Ergebnis ist eine Zwischendarstellung des Quellcodes

#### Analyse und Optimierung

    Die Zwischendarstellung wird analysiert und optimiert. Dieser Schritt variiert im Umfang je nach Compiler und Benutzereinstellung stark. Er reicht von einfacheren Effizienzoptimierungen bis hin zu Programmanalyse

#### Codeerzeugung

    Die optimierte Zwischendarstellung wird in entsprechende Befehle der Zielsprache übersetzt. Hierbei können weitere, zielsprachenspezifische Optimierungen vorgenommen werden
Diese Schritte können sowohl unabhängig voneinander durchlaufen werden (Multi-pass-Compiler), als auch in Einem (Single-pass-Compiler). Bei Letzteren wird der Quellcode nur einmalig von vorne bis hinten analysiert und zugleich der Maschinencode erzeugt. Trifft der Compiler auf einen Variable/Funktion/Datentyp, welche zuvor nicht definiert wurde, kann der Compiler hierfür keinen Maschinencode erzeugen. Der Code darf somit keine Vorwärtsbezüge enthalten. Multi-pass-Compiler durchlaufen den Code mehrmals, so dass in einem am Anfang liegenden Durchlauf zunächst der Code nach Definitionen durchsucht wird. Die eigentliche Codeerzeugung erfolgt dann in einem späteren Durchlauf.
Die Sprache C entstammt aus einer Zeit, indem Speicher ein knappes Gut war. Typische Hauptspeichergrößen waren 32kByte...128kByte, Festplattenspeicher ca. 5MByte. Die Spezifikation der Sprache ging aufgrund dieser knappen Ressourcen von einem Single-pass-Compiler als Werkzeug aus. Daraus ergibt sich, dass in C alle Variablen/Funktionen/Datentypen vor der ersten Nutzung definiert oder per Prototyp/(Forward)deklaration beschrieben werden müssen.

### Compileroptimierungen

Neben der reinen Übersetzung unterstützen moderne Compiler div. Optimierungsmöglichkeiten wie z.B.:
* Halten von Variablen in Register
* Erkennung ungenutzter Variablen
* Optimierung von Schleifen
* Erkennung und Optimierung von 'doppelten' Anweisungen
Optimierungen versuchen, das erstellte Programm hinsichtlich der Anzahl der Maschinenbefehle (kleineren Code) oder der Ausführungsgeschwindigkeit der Befehle (schnelleren Code) zu optimieren, ändern aber nichts am Source-Code. Hohe Optimierungseinstellungen können aufgrund der Zwischenspeicherung von Variablen in Registern und der Optimierung der Ausführungsreihenfolge den erzeugten Maschinencode so entfremden, dass dieser im Debugger nicht mehr nachvollziehbar angezeigt werden kann. Daher wird in C-Projekten zwischen den Debug-Mode und dem Release-Mode unterschieden. Im ersten sind viele Optimierungen ausgeschaltet und ggf. ergänzende printf Ausgaben enthalten, so dass im Debugger die Codeabarbeitung verfolgt werden kann. Im Release-Mode wird der Compiler auf 'max' Optimierung gestellt. Ein Debuggen ist zwar auch hier möglich, aber der Zusammenhang zwischen Maschinencode und Source-Code nur schwer nachvollziehbar.
Die Optimierungsmöglichkeiten eines Compilers sind zwar gut, der Compiler kann aber nur das optimieren, was der Programmierer 'vorgibt'. Für einen optimalen Code ist folglich auch der Programmierer verantwortlich:
* Stringvergleich dauert immer länger als ein Zahlenvergleich, d.h. bspw. keine Zustandsinformation im Datentyp String speichern (zustand="ON") sondern als Zahlenwert (zustand=1)
* Mehrmalige Aufrufe einer Funktion mit identischen Übergabeparametern sind zu vermeiden
    for(index=0; index<strlen(str); index++) //die Funktion strlen wird bei jedem Schleifendurchlauf aufgerufen
* Mathematische Ausdrücke, sofern möglich in Ganzzahlarithmetik durchführen
* IF-Abfragen vermeiden (Sprünge in Maschinensprache gehören zu den langsamsten Maschinenbefehlen)
* Schleifen von oben nach unten zählen lassen (erspart ein Vergleich und damit einen Sprung) oder alternativ zur Zählvariable einen Pointer nutzen
    for(int lauf=0; lauf<10;lauf++) //Ein Vergleich für < und //ein Vergleich für True/False notwendig for(int lauf=10;lauf;lauf--) //Nur Vergleich auf True/False notwendig char str="hallo"; for(const char *ptr=str;*ptr;ptr++) //Vermeidung der Index-Variable
* Durchsuchen eines zweidimensionalen Arrays in der inneren Schleife über den rechten Index und nicht den linken Index (Optimierung, so dass der Cache optimal genutzt wird)
    SchlechtGut int zeile,spalte; for(spalte=999;spalte;spalte--) for(zeile=999;zeile;zeile--) if(matr[zeile][spalte]>max) max=matr[zeile][spalte]; int zeile,spalte; for(zeile=999;zeile;zeile--) for(spalte=999;spalte;spalte--) if(matr[zeile][spalte]>max) max=matr[zeile][spalte];
    Öffnen im Compiler Explorer

## Zusammenspiel von C und Betriebssystem

C wurde als Sprache zum Schreiben eines Betriebssystems (Unix) entwickelt, zwecks einer effizienteren Programmierung und einer Portierbarkeit (beides hat dann Unix zum Durchbruch verholfen). Die Sprache ist somit eng mit dem Unix-Betriebssystem (und anderen Betriebssystemen, die entsprechende Eigenschaften adaptiert haben) verbunden. Dies ist unter anderen daran erkennbar, dass:
* Alles ist eine Datei, d.h.
   * Standardein-, Standardaus- und Standardfehlerausgabe erfolgt über Lesen/Schreiben in eine Datei, welche mit dem Start des Programms geöffnet sind (resp. vom startenden Programm vererbt werden). printf, scanf und error nutzen diese Dateien als Arbeitsgrundlage.
   * Ein Großteil der Interaktion mit dem Betriebssystem erfolgt über Dateizugriffe (Zugriff auf Geräte, Zugriff auf Prozessorauslastung, ...)
* Start des Programms erfolgt über den Funktionsaufruf main, welcher Übergabeparameter (als Array von Strings) übergeben werden und welche einen Fehlerstatus (als Integer) an den Aufrufer zurückgibt.
* Die Nutzung von Environment-Variablen zum Datenaustausch zwischen Betriebssystem und Anwendungsprogramm (Länder-/Spracheinstellung, Systemkonfigurationen, ...)
* Klare Trennung zwischen OS-Funktionalität und C-Funktionalität:
   * OS-Funktionen gehören nicht zur C-Standardbibliothek. D.h. OS-Funktionen werden direkt, ohne Nutzung von Wrapper, aufgerufen (z.B. wird zur Erzeugung eines Threads direkt die dazugehörige OS-Funktionalität pthread_create genutzt)
   * Die Standard-C-Library dementsprechend nur wenige Funktionen bereitstellt
* Libraries / DLL beruhen auf den CallingConvention von C (Regelung, wie bspw. die Übergabeparameter an Funktionen übergeben werden) und sind folglich rudimentär C-Funktionen
* Linker (ein Programm zu binden mehrere Objekt-Dateien zu einer Objekt-Datei) ist eine Funktionalität des Betriebssystems (und nicht des Compilers). Das Ausgabeformat des Linkers ist wahlweise:
   * eine ausführbare Datei
   * eine statische Library (Archiv)
   * eine dynamische Library
    d.h. dynamische Librarys können mit diversen Compiler-Sprache erstellt werden und auch von diversen Programmiersprachen (inkl. Interpretersprachen wie Python) eingebunden werden
* Die Standardbibliotheksfunktionen von C sind in Librarys ausgelagert. printf / strlen sind bspw. in der Library libc.so und sin / sqrt in der Library libm.so enthalten. Werden aus einem Programm diese Funktionen genutzt, so müssen diese Librarys über Linkerschalter eingebunden werden (Ausnahme libc.so, welche per Default eingebunden wird)

## Alles ist Speicher

Ein Rechnersystem besteht im Wesentlichen aus einem Prozessor/CPU (bestehend aus Arithmetische/Logischen Einheit (ALU), den Registern und dem Steuerwerk) und aus Speicher. Das Zusammenspiel beider Einheiten lässt sich wie folgt darstellen:
Die Aufgabe der CPU besteht darin, die Maschinenbefehle aus dem Speicher zu laden (adressiert über den ProgrammCounter PC) und diese einen nach dem anderen auszuführen. Die zu verarbeitenden Daten (Variablen) sind ebenfalls im Speicher enthalten (angesprochen über die Speicheradresse z.B. 0xf0f, 0xf0d).
Ein Speicher ist eine Einheit zur Speicherung von Informationen. Im weitesten Sinne entspricht der Hauptspeicher (nicht Festplatte, resp. sekundärer Speicher) einem Karteikarten System:
* Wobei auf einer Karteikarte nur 8-Bit/1-Byte Information gespeichert sind
   * Vorzeichenlose Zahlenwerte von 0…255
   * Vorzeichenbehaftete Zahlenwerte von -128…+127
   * ASCII Zeichen
   * Maschinenanweisungen für den CPU
* Max. Anzahl der Karteikarten hängt von der Adressierungsbreite (zumeist identisch zur Rechenbreite) des Prozessors ab:
   * Bei einem 32-Bit System sind 2$^{32}$=4 294 967 295 Karteikarten vorhanden
   * Bei einem 16-Bit System sind 2$^{16}$=65 536 Karteikarten vorhanden
   * Bei einem 64-Bit System sind 2$^{64}$=18 446 744 073 709 551 616 =18 Exa vorhanden
* Bindeglied zwischen Prozessor und Speicher ist das Bus-System, welches sich in einen Daten- und einen Adressbus unterteilen lässt. Der Adressbus dient dabei der Adressierung einer Speicherstelle und der Datenbus zum Datentransport in die Speicherstelle (Schreiben) oder aus der Speicherstelle (Lesen).
* Es kann zu einem Zeitpunkt nur einer (zumeist die CPU) durch Vorgabe einer Adresse die Daten (Inhalt der Karteikarten) lesen oder schreiben
   * Beim Lesen wird der gesamte Inhalt der Karteikarte zurückgegeben
   * Beim Schreiben wird der gesamte Inhalt der Karteikarte neu beschrieben (Änderung einzelner Bits bedingen das vorherige Lesen des gesamten Inhaltes (Var=Var|0b0100)
   * Es gibt kein Löschen, die einzelnen Bits des Speichers sind entweder 0 oder 1
* Inhalte des Speichers hängen von der Interpretation (des Programms) ab
   * Maschinenbefehle, welches die CPU ausführt
   * Variablen als 8-Bit/16-Bit/32-Bit/Ascii/Float/Strutur/…
   * Heap, zur Darstellung von zur Laufzeit benötigten Speicher
   * Stack, zur Darstellung von lokalen Variablen
* Der Inhalt des Speichers kann
   * Flüchtig sein, d.h. er verliert nach PowerOff seinen Inhalt und ist nach PowerOn mit einem zufälligen Wert gefüllt
   * Nicht flüchtig sein, d.h. er behält auch ohne Spannungsversorgung seinen Inhalt
   * Nur lesbar sein (für Konstanten)
   * Schreib- und lesbar sein (für Variablen)
   * Nur ausführbar sein (für Funktionen)
Zusammengefasst kann gesagt werden: "Alles ist Speicher". Jede Variable belegt somit in Abhängigkeit des Datentyps Speicher, jede Funktion belegt in Abhängigkeit der dazugehörigen Anweisungen Speicher und selbst der Zugriff auf die Peripherie zur Interaktion mit der Umwelt erfolgt über Speicherzugriffe. Variablen / Funktionen / Peripherie werden folglich über Speicheradressen angesprochen, d.h. :
* Ein Zugriff auf eine Variable erfolgt über deren (Start)Speicheradresse, wobei dann in Abhängigkeit des Datentyps beginnend ab der Speicheradresse ein oder mehrere Bytes gelesen werden
* Ein Aufruf einer Funktion entspricht einem Sprung zu einer Speicheradresse, ab welcher der nächste Maschinenbefehl zur Ausführung gebracht werden soll
In C wird dieses Konzept direkt aufgegriffen, indem:
* Die Speicheradresse in einer Zeiger(variablen) gespeichert werden kann
* Über den Adressoperator zu jeder Variablen/Funktion die Speicheradresse ermittelt werden kann und z.B. in einer Zeigervariablen gespeichert werden kann
* Über Dereferenzierung auf jede Speicheradresse lesend und schreibendend zugegriffen werden kann
Alle Programmiersprachen basieren auf diesem Prinzip. Die Sprache C gibt einen Teil der Verantwortung für den Umgang mit Speicher an den Programmierer ab. Andere Sprachen versuchen, diesen Sachverhalt möglichst vor dem Programmierer zu verstecken. Dies lässt sich am Beispiel der Symboltabelle erkennen. In C werden durch den Compiler/Linker alle Variablen/Funktionssymbole direkt durch die Speicheradresse ersetzt, so dass die Symboltabelle im ausführenden Code nicht mehr enthalten ist. Dynamischer Speicher wird vom Programmierer und nicht vom Compiler verwaltet. D.h. der Programmierer kann sich Speicher anfordern, die Verwaltung des Speichers (Zuordnung zu den Attributen/Inhalten) liegt jedoch in der Verantwortung des Programmiers.

# Toolchain/Entwicklungsumgebung
## Toolchain

Die C-Toolchain besteht im Wesentlichen aus dem Compiler, den Linker und dem Loader:
* Compiler zur Übersetzung der einzelnen C-Dateien in Objekt-Dateien
* Linker (OS-Funktionalität) zum Binden mehrerer Objekt-Dateien zu einer (ausführbaren) Executable-Datei
* Loader (OS-Funktionalität) welche die Executable-Datei vom sekundären Speichermedium in den Speicher des Prozess lädt und das Programm durch einen Sprung zu main zur Ausführung bringt (inkl. Bereitstellung der Übergabeparameter)
→ Die C-Toolchain und das Betriebssystem sind folglich aufeinander abgestimmt
Die Koordination der Tools kann per Hand/Manuell, per Make oder über eine integrierte Entwicklungsumgebung (Eclipse, Visual Studio, XCODE, IntelliJ IDEA, …) erfolgen.

### Compiler

C-Dateien werden einzeln compiliert. Jeder Compilerdurchlauf erzeugt eine Objektdatei. Vor der eigentlichen C-Compilierung wird erst der C-Präprozessor ausgeführt, welcher unter anderen die Include-Anweisungen durch den Inhalt der dazugehörigen Datei ersetzt. Die vom Compiler erzeugte Objekt-Datei ist einzeln nicht ausführbar, da:
* Funktionen aufgerufen werden, welche in anderen C-Dateien definiert sind
* Auf globale Variablen zugegriffen werden, die in anderen C-Dateien definiert sind
* Ggf. keine main-Funktion enthalten ist, sondern nur Funktionen und globale Variablen enthalten sind, welche von anderen C-Datei genutzt werden
* Library-Funktionen verwendet werden (z.B. printf), die nicht Bestandteil des C-Projektes sind

#### Compiler-Varianten

Für die Sprache C sind diverse Compiler verfügbar:
* Intel-Compiler (ICC)
* Microsoft Compiler (MSVC)
* GCC Compiler (GCC)
* Clang Compiler Frontend (CLANG) mit Compiler-System LLVM
Unter Linux ist der GCC-Compiler der Standardcompiler. Alle C-Programme und das Betriebssystem selbst wurden mit diesem Compiler erstellt. Sofern dieser nicht defaultmäßig auf dem System installiert ist, kann er mit wenigen Handgriffen schnell nachinstalliert werden. Auf Windows-Systemen gibt es keinen Standardcompiler. Oft wird hier der Microsoft Compiler genutzt. Soll besonders schneller Code für Intel Prozessoren (und nicht AMD) erzeugt werden, empfiehlt sich der Intel-Compiler. Unter macOS wird sowohl sowohl CLANG/LLVM und GCC eingesetzt.

#### Compiler-Parameter

Neben der Angabe der zu übersetzenden C-Datei können beim Aufruf des Compilers weitere, optionale Parameter gesetzt werden, über welchen der Compiler konfiguriert wird. Am Beispiel des GCC-Compilers sind u.A. folgende Schalter vorhanden:
* Optimierungsqualität setzen (z.B. -Ox mit x=0 → keine Optimierung x=1 → einfache Optimierung x=2 → bessere Optimierung x=3 → höchste Optimierung x=size → Optimierung hinsichtlich Programmgröße)
* Makros (Präprozessoranweisung) setzen (z.B -Dmakro=7)
* Umfang der Fehlerprüfung (Warnings) und Umgang mit diesen setzen (-Wx mit x=all → erhöhter Warning Level x=extra → zusätzlicher Warning Level, die nicht in -Wall enthalten sind x=error → Warnings als Fehler ansehen)
* einzubindende Librarys setzen (-lx mit x=c → zum Einbinden der Standard-C-Library (defaultmäßig aktiviert) x=m → zum Einbinden der Math-Library)
* Suchpfade für Header Dateien setzen (-Idir mit dir=Verzeichnis in dem ergänzend nach Header-Dateien gesucht wird)
* Suchpfade für Library Dateien setzen (-Ldir mit dir=Verzeichnis, in dem ergänzend nach Library-Dateien gesucht wird)
* Instrumentierungskommandos (Erzeugung von zusätzlichen Maschinencode zur erweiterten Fehlerprüfung) (z.B. -fsanitize=x x=address zum Instrumentieren des Executable, so dass einige illegale Speicherzugriffe erkannt werden)

### OBJ-Datei

Eine Objektdatei speichert die vom Compiler (eigentlich die vom Assembler) übersetzten Anweisungen in einer strukturierten Form. Die Maschinenbefehle werden in der Text-Section gespeichert. Globale und statisch lokale Variablen, welche außerhalb eines Funktionskontextes existieren, werden entweder in der Data-Section (bei initialisierten Variablen) oder in der Bss-Section (bei nicht initialisierte Variablen) gespeichert. Erstere Section beinhaltet die Initialisierungswerte der Variablen. Zweitere Section wird mit Programmstart mit 0 gefüllt, so dass alle nicht initialisierten globalen Variablen den Wert 0 beinhalten.
Globale Konstanten werden in der rodata-Section gespeichert. Dies ermöglicht es dem Loader, die Zugriffsrechte auf diesen Speicherbereich auf Read-Only zu setzen. Alle Variablen und Funktionsnamen werden mit ihren zugeordneten Speicheradressen in der Symboltabelle gespeichert.
Die absolute/tatsächliche Speicheradresse wird erst beim Erstellen der Executable erzeugt. Die Sectionen, welche die bis dahin relativen/verschiebbaren Adressen beinhaltet werden hierbei in Segmente (beinhaltet absolute Speicheradressen) gewandelt. Typische Dateiformat für Objekt-Dateien sind Executable and Linking Format (ELF) und Common Object File Format (COFF).

### Linker

Die einzelnen Objekt-Dateien werden vom Linker zu einer Objektdatei zusammengefügt, d.h. alle Funktionen und globale Variablen werden in einer Datei zusammengebunden, so dass bisher 'externe' Funktionen und 'externe' globale Variablen für alle im Zugriff stehen. Auch Libraries werden 'eingebunden', so dass z.B. printf/sin ebenfalls im Zugriff stehen. Die Ausgabedatei eines Linkerdurchlaufes kann wahlweise eine Objekt-, eine Library- oder eine Executable-Datei sein. Erstes bedeutet, dass mit einem Linker Aufrufe nicht alle Objekt-Dateien auf einmal gebunden werden müssen, sondern dies in einen mehrstufigen Prozess (z.B. Ordnerweise, Klassenweise) erfolgen kann. Mit dem letzten Linker Aufruf im mehrstufigen Prozess wird entschieden, ob eine Library (enthält keine main-Funktion) oder eine Executable (bei welche alle globalen Variablen, alle statisch lokalen Variablen und alle Funktionen eine absolute Speicheradresse zugewiesen bekommen) erzeugt werden soll.

### Loader

Mit dem Start der Anwendung wird der Loader aufgerufen (bestehend aus der OS-Funktion exec (Posix)) , welcher die aktuelle laufende Anwendung durch die zu startende Anwendung ersetzt. Hierbei werden die Inhalte der Segmente aus der Objekt-Datei in die dazugehörigen Speicheradressen des Prozesses geladen. Im Anschluss wird main (resp. C-Startup, welche dann Main startet) aufrufen. Die Übergabeparameter von exec werden an Main weitergereicht.

### Make

Im Wikipedia Artikel make wird die Aufgabe von make wie folgt zusammengefasst: "make ist ein Build-Management-Tool, das Kommandos in Abhängigkeit von Bedingungen ausführt." In C/C++-Projekten wird es genutzt, die einzelnen Compiler und die anschließenden Linkeraufrufe in einem Tool zentral zu organisieren. Mit jedem Aufruf von make prüft make, in welcher C-/H-Dateien Änderungen vorgenommen wurden (C-/H-Datei neueren Datums als die dazugehörige OBJ-Datei, resp. OBJ-Dateien neueren Datums als die Executable) und übersetzt nur die geänderten Dateien. Ergänzend werden im Makefile auch Projektkonfigurationen vorgenommen (ähnlich .classpath File in Java), über welches das Projekt konfiguriert wird (Debug-Mode, Optimierung, …)" Die Projektbeschreibung ist in der Datei 'makefile' enthalten, welche bei Start von Make ohne expliziter Angabe einer Projektbeschreibung 'ausgeführt' wird.
Mit dem Aufruf vom Make können Ziele(Targets) definiert werden, welche behandelt werden sollen. Typische Ziele sind:

### Entwicklungsumgebung

Ein integrierte Entwicklungsumgebung IDE beinhaltet Softwarekomponenten, die ein einfaches Programmieren, die Ausführung und den Test unter einer Oberfläche ermöglichen. Zur Vermeidung von Medienbrüchen bieten IDE's eine eigene Verwaltungsoberfläche/-sprache an, aus welcher die Anweisungen für den Compiler, Linker, Debugger, Make usw. abgeleitet werden. Zur einfachen und schnellen Softwareentwicklung sind weitere Hilfstools wie Autovervollständigung oder Lösungsvorschläge für Compilerfehlermeldungen vorhanden. Letztere Hilfstools stellen nach Meinung des Autors einen Nachteil dar, da ergänzend zum syntaktischen Fehler oftmals auch prinzipielle Fehler vorhanden sind.

## Installation / Start beispielhafter Toolchains

In dieser Vorlesung wird der GCC-Compiler als Grundlage genutzt. Damit die Fehlermeldung des Compilers gelesen werden, wird empfohlen wahlweise den Compiler per Hand zu starten oder eine Online Version des Compilers zu nutzen. Von der Nutzung einer IDE wird abgeraten. Für Betriebssystemaufrufe wird der POSIX-Standard vorausgesetzt, wie er bspw. von LINUX, dem Windows Subsystem für Linux WSL, oder macOS unterstützt wird.

### Händische Installation der Toolchain
#### Linux

Im Normalfall sollte unter Linux die benötigte Toolchain installiert sein. Bei einigen Distributionen muss jedoch die Toolchain händisch installiert werden. Dazu geben sie folgende Befehle in ein Terminalfenster ein:
Die Erstellung des Source-Codes kann mit einem beliebigen Editor (z.B. gedit, nano) erfolgen.

#### Windows 10/11 64-Bit Nutzer

Sowohl der GCC-Compiler als auch POSIX konforme OS-Aufrufe werden von Windows von Haus aus nicht unterstützt. Mit Windows Subsystem für Linux (WSL) wird ein Linux System emuliert, welche beide Voraussetzungen erfüllt:
* Installation von WSL
    Entsprechend nachfolgender Anleitung: How to install Linux on Windows with WSL
* Upgrade + Vervollständigung
    Starten einer bash (bspw. durch Eingabe von 'bash' in der Suchzeile)
    >>sudo apt-get update >>sudo apt-get upgrade >>sudo apt install gcc >>sudo apt install make
* Datenaustausch zwischen Windows und WSL
    Für den einfachen Datenaustausch zwischen Windows und WSL empfiehlt sich, im Windows Dateisystem ein Arbeitsverzeichnis anzulegen. Das Windows Dateisystemen ist ab dem Pfad '/mnt/c/...' unter Linux gemountet:
    >>cd /mnt/c/_Projekte/AufgabeX
* Die Erstellung des Source-Codes kann mit einem beliebigen Windows-Editor (z.B. notepad++) erfolgen.

#### macOS

macOS basiert auf darwin (ein freies UNIX Betriebssystem). Defaultmäßig wird macOS ohne Compiler ausgeliefert. Die Installation eines Compilers sollte identisch zur Installation unter Linux sein.

### Händischer Start des Compilers

Beim GCC-Compiler übernimmt der Aufruf von gcc gleichermaßen den Compiler- und den Linker-Aufruf. Eine Übersetzung wird wie folgt gestartet:
Diese Anweisung startet zunächst für jede C-Datei den Compiler. Im Anschluss werden alle erzeugten Objekt-Files mit einem weiteren Linkeraufruf zusammengefügt. Als Executable wird die Datei a.out erzeugt. Mit dem Compiler-Parameter -o kann ein alternative Name für die Executable gesetzt werden:
Der Aufruf kann beliebig um Compiler-Parameter ergänzt werden:
Nach fehlerfreien Compilerdurchlauf wird die Executeable wie folgt gestartet:
Wichtige Terminal/bash-Befehle: Siehe auch: Ubuntuusers/Befehlsübersicht

### Nutzung des Online-Compilers Compiler Explorer

Der Compiler Explorer ist eine komfortable Online-Entwicklungsumgebung, welche diverse Programmiersprachen, diverse Compiler unterstützt, eine komfortable Ausführungsinstanz für die Executable bietet und den erzeugten Assemblercode darstellen kann.
    https://www.godbolt.org
Für den einfachen Start soll hier der prinzipielle Umgang mit dem Compilerexplorer erläutert werden:
* Eingabe des Source-Codes
Die Eingabe des Source-Codes ist selbsterklärend. Rechts oben in der Eingabebox sollte C als Programmiersprache gewählt werden. Der Compilevorgang startet automatisch, wenn für ca. 1 Sekunde keine Änderung mehr am Source-Code vorgenommen wird. Beim ersten Start des Compilerexplorers wird defaultmäßig der compilierte Assemblercode dargestellt. Der besseren Übersicht sollte dieses Fenster geschlossen werden. Über 'Add New-Execution only ' wird die Executable zur Ausführung gebracht. Wer den erzeugten Assemblercode oder Zwischenergebnisse des Compilers analysieren möchte, wählt Add New-Compiler.
* Executor
Sobald der Haken im grünen Kreis erscheint, ist der Compilevorgang und die Programmausführung abgeschlossen. Änderungen bei *2) und *3) führen direkt zu einem Neustart des Programms. Mit Änderung von *1) oder Wahl eines anderen Compilers wird der Compilevorgang ergänzend neu gestartet. Über die 'Menü'-Zeile können einzelne Fenster ein- und ausgeblendet werden. Es wird empfohlen, alle Fenster einzublenden!
*1) Hier können dem Compiler-Parameter übergeben werden. Es empfiehlt sich '-fsanitize=Address -Wall -Werror' vorzugeben, so dass eine Speicherüberwachung in den Source-Code integriert wird und eine erweiterte Fehlerprüfung stattfindet. *2) Hier können der Executable Parameter übergeben werden, die dann in argc und argv von main abrufbar sind. Entspricht dem händischen Start des Programms über './a.out para1 para2' *3) Hier wird die Standardeingabe nachgebildet. Allerdings wartet der Executor nicht auf eine aktive Eingabe durch den Nutzer, sondern gibt mit jeden Aufruf von scanf oder fgets die Inhalte der einzelnen Zeilen aus diesem Fenster zurück. D.h. der erste Aufruf von fgets liefert hier 'set 1 eins' und der zweite Aufruf 'set 2 zwei' zurück. *4) Sofern mit 'Compiler Output' aktiviert, werden hier alle Compiler-Warnings angezeigt. Wichtig: Warnings sollten als Fehler angesehen werden und dementsprechend im Source-Code behoben werden *5) Hier wird der Rückgabewert von 'int main' angezeigt. Sollte das Programm sich in einer Endlosschleife befinden (und damit die Laufzeit von ca. 10 Sekunden überschreiten), so wird das Programm nach ca. 10 Sekunden vom Compiler Explorer beendet und ein Returnwert von 143 angezeigt. Dem Wert 'Programm returned: xxx' sollte einer besonderen Aufmerksamkeit zugedacht sein. *6) In diesem Fenster wird die Standardausgabe dargestellt, also alles, was sie bspw. mit printf ausgeben. *7) In diesem Fenster wird die Standardfehlerausgabe dargestellt, alles, was sie bspw. Mit fprintf(stderr, ...) Oder perror(...) Ausgeben. Die Nutzung der Standardfehlerausgabe empfiehlt sich insbesondere, wenn Laufzeitfehler vorhanden sind. Aufgrund dessen, dass die Standardausgabe nicht gepuffert ist, werden alle Ausgaben direkt zur Anzeige gebracht.

## Fehlersuche von Laufzeitfehlern

Ein fehlerfrei übersetzter Code heißt nicht, dass das erzeugte Programm fehlerfrei ist. Ein erfolgreicher Compilerdurchlauf bedeutet nur, dass alle C-Anweisungen in entsprechende Maschinenbefehle umgesetzt werden konnten. Fehler können auch zur Laufzeit (siehe Kap. Einführung/Literatur-Laufzeitfehler) eines Programmes auftreten.
Laufzeitfehler machen sich wahlweise durch Programmabsturz oder durch ein fehlerhaftes Verhalten des Programmes bemerkbar. Bei einem Programmabsturz beendet das Betriebssystem die Fortführung des Programmes z.B. aufgrund eines fehlerhaften Speicherzugriffes (Segmentation Fault). Beim Compiler-Explorer begrenzt der Executor die Laufzeit auf ca. 10 Sekunden und beendet dann ebenfalls das Programm. Beide 'Programmabstürze' sind dadurch gekennzeichnet, dass die main Funktion sich nicht selbst beendet und damit der Return-Wert nicht zur Ausführung gebracht wird. Beim Compiler Explorer empfiehlt sich, den Return-Wert (und den Grünen Hacken) im Auge zu behalten. Ein fehlerhaftes Verhalten des Programms macht sich bspw. dadurch bemerkbar, dass ein Variable einen anderen Wert beinhaltet, als erwartet:
Ungeachtet der Art des Auftretens des Fehlers liegt der Fehlergrund zumeist nicht an der Stelle, an der sich der Fehler bemerkbar macht. Das Mittel der Wahl zur Fehlersuche sind die in Kap. Einführung/Literatur-Laufzeitfehler dargestellten Methoden, also ein Codereview durchzuführen. Ergänzend können printf-Anweisungen helfen, den Fehler einzugrenzen. Dabei sollten beachtet werden, dass printf-Anweisungen nicht gepuffert sind und wahlweise von einer fflush(stdout) gefolgt werden sollte oder durch fprintf(stderr,..) erfolgen sollten.
Hinweise:
* Ein Softwaretest sollte nicht nur die Gutfälle abdecken, sondern insbesondere die Schlechtfälle berücksichtigen. Dabei sollte der Kreativität freien Lauf gelassen werden. Der Anwender der späteren Software nutzt sicherlich nicht immer den von Ihnen vorgesehenen Weg, sondern nutzt alle Wege, die nicht explizit deaktiviert sind (unwissend oder auch bewusst).
* Werden Laufzeitfehler nicht behoben, so sind dies häufig Einfallstore für Viren. Die 25 gefährlichsten Softwarefehler werden von Common Weakness Enumeration (CWE) unter https://cwe.mitre.org/top25/archive/2021/2021_cwe_top25.html aufgelistet. Auf einer der obersten Plätze sind Arrayzugriffe außerhalb des reservierten Bereiches zu finden.
    Das Open Web Application Security Project (OWASP) führt ebenfalls eine Liste der Sicherheitsbedrohungen im Web: https://owasp.org/Top10/

# Grundlagen

Die Programmiersprache C/C++ beinhaltet mehrere Sprachen/ Syntaxen:
* C-Syntax
* Präprozessor-Syntax
* Printf/Scanf Formatstring Syntax
* Terminal Emulation
* Compiler/Linker Anweisungen
Letztere beiden werden nur rudimentär in dieser Vorlesung/diesem Skript behandelt. Alle anderen sind Bestandteil dieser Vorlesung.
In diesem Kapitel sollen zunächst allgemeine Eigenschaften der Sprache, der grundlegende Syntax und die Kontrollstrukturen erklärt werden. Bei vielen Erklärungen sind Code-Beispiele vorhanden. Da man aus Fehlern am meisten lernt, sind zum Teil auch negative Beispiele enthalten. Für ein besseres Verständnis empfiehlt es sich, Code-Beispiele selbst nachzuvollziehen. Sofern sich die Eigenschaften auf eine bestimmte C-/C++- oder Compiler-Version beziehen, wird dies gesondert vermerkt.

## Zeichensatz

Der Syntax von C nutzt die unteren 128 Zeichen des ASCII Zeichensatzes. Da UTF-8 in den ersten 128 Zeichen deckungsgleich zu ASCII ist, kann auch dieser zur Erstellung des Source Codes genutzt werden. Zeichen außerhalb dieses gültigen Zeichensatzes können folglich nur in Strings oder Kommentaren vorkommen. Werden Zeichen außerhalb der unteren 128 Zeichen in Strings genutzt, so gilt Folgendes zu berücksichtigen:
* Die String-Funktionalitäten der Standard-C Library gehen von dem regulären ASCII Zeichensatz aus, so dass z.B. die Suche, der Vergleich oder die Konvertierung fehlschlagen kann
* Strings werden oftmals in Verbindung mit printf genutzt, d.h. auf der Standardausgabe ausgegeben. Der verwendete Zeichensatz sollte hier identisch zum verwendeten Zeichensatz der Terminal-Emulation sein!

## Kommentarzeichen

Kommentare dienen dazu, den Source Code mit zusätzlichen Hinweisen zu versehen, so dass die Intension der Anweisungen sichtbar wird. Kommentarzeichen werden durch den Compiler vor dem eigentlichen Übersetzungsdurchlauf entfernt und durch ein Leerzeichen ersetzt [C11 6.4.9]. Innerhalb von Strings wird nicht nach Kommentarzeichen gesucht.
Syntax: /* */
Blockkommentar zum Kommentieren eines Bereiches, auch über mehrere Zeilen hinweg. Dieses Kommentarzeichen kann nicht verschachtelt werden, d.h. '*/' beendet die Kommentierung, unabhängig von der Anzahl der zuvor geöffneten Kommentierungen!
Syntax: //
Zeilenkommentar zum Kommentieren bis zum Zeilenende (wurde in der Programmiersprache BCPL definiert, aber nicht von C, sondern erst von C++ übernommen).
Sollen größere Bereiche auskommentiert werden, so empfiehlt sich die Nutzung der Präprozessoranweisung #if 0 ... #endif. Beispiele:
Öffnen im Compiler Explorer

## Namenskonventionen

Folgende Regeln gelten bzgl. der Benennung von Variablen und Funktionen:
* Variablen und Funktionsnamen können aus Buchstaben, Zahlen und dem Unterstrich bestehen. Sie müssen mit einem Buchstaben oder einem Unterstrich beginnen
* Seit C95 können weitere Zeichen aus dem Universal Coded Character Set (UCS) genutzt werden [C11 D.1]
* C ist Case sensitiv, d.h. es wird zwischen Groß- und Kleinbuchstaben unterschieden
* Schlüsselwörter können nicht für Variablen/Funktionsnamen/Datentypen genutzt werden
Namenskonventionen von Libraryfunktionen:
* In C (und C++) sind Schlüsselwörter und Standardlibraryfunktionen zumeist in Kleinbuchstaben geschrieben. In der C-Standardlibrary werden oftmals verkürzte Ausdrücke (z.B. isalnum (zum Testen ob ein Zeichen ein Buchstaben oder ein Digit ist) und in C++ der Unterstrich als Worttrenner (z.b. out_of_range) genutzt
* Makros (siehe Kap. Präprozessor) werden per Konvention in GROSSBUCHSTABEN und ggf. mit Unterstrich als Worttrenner geschrieben.
* Namen beginnend mit doppelten Unterstrich oder beginnend mit einem Unterstrich gefolgt von einem Großbuchstaben (z.B. __LINE__ _Reserved) sind für den Compiler und der Standard-C-Library vorbehalten und sollten im eigenen Programm nicht benutzt werden.

## Zeilenfortsetzung

Mit dem Backslash Operator (gefolgt von einem Zeilenende) kann eine Zeile in der nächsten Zeile fortgesetzt werden. Der Compiler löscht das \-Zeichen mit anschließendem Zeilenende und ersetzt dies durch nichts. Dies ist insbesondere bei Strings und bei Makros von Interesse, die am Ende der Zeile abgeschlossen sein müssen.
Beispiele:
Öffnen im Compiler Explorer
Hinweis:
* Innerhalb von Char-Literatoren und Strings wird '\' als Escape-Operator genutzt, welche das '\' und ein oder mehrere folgende Zeichen ersetzt (siehe Kap. Grundlagen:Literale/Konstanten). Daher darf hinter Backslash als Zeilenfortsetzungszeichen kein weiteres Zeichen folgen.

## Gültigkeit/Sichtbarkeit von Variablen

Vorrangig in der objektorientierten Programmierung werden mit Namensräumen Objekte und deren Methoden/Attribute in einer Art Baumstruktur strukturiert. Dies ermöglicht eine eindeutige Ansprache von Variablen/Objekte, aber auch eine doppelte Verwendung von Methoden-/Attributnamen in unterschiedlichen Namensräumen. Ergänzend zu den Namensräumen kann mit public/private/proteced eine Zugriffsbeschränkung von Methoden/Attributen definiert werden.
Die Programmiersprache C unterstützt keinen Namensraum. Zugriffsmodifikatoren werden indirekt über Header-Dateien getätigt. Hinsichtlich der Gültigkeit/Sichtbarkeit unterscheidet C folgende Bereiche [C11 6.2.1]:
* Funktionen (Function Scope)
* Datei (File Scope)
* Block (Block Scope)
* Funktionsparameter in Prototypen (Function Prototype Scope)
Innerhalb eines Gültigkeitsbereiches dürfen Variablen-/Funktions-/Datentypnamen nicht doppelt genutzt werden. In der Programmiersprache C++ sind weitere Scope wie z.B. Class Scope, Enumationation Scope und ergänzend das Konzept von Namensräumen vorhanden (Beschreibung folgt).

### Funktionsweite Sichtbarkeit

Eine Label-Definition (als Sprungmarke für die goto-Anweisungen) erfolgt immer mit funktionsweiter Sichtbarkeit/Gültigkeit. Die genaue Beschreibung dieses Sichtbarkeitstyps erfolgt im Rahmen des Kapitels Grundlagen:Label+Goto.

### Dateiweite Sichtbarkeit

Erfolgt eine Funktion-/Variablen-/Datentyp-Definition außerhalb eines Block-Scopes oder von Funktionsparameter, so sind diese innerhalb der gesamten Datei und bei Funktionen/Variablen ergänzend Projektweit (für alle Objektdateien) sichtbar/gültig (= global) (In der C-Spec unter dem Stichwort 'Linkage' beschrieben, siehe Kapitel DatentypeSpecifier:Internal/External Linkage). Alle globalen Funktionen/Variablen können von allen Dateien aus genutzt/zugegriffen werden (sofern sie zuvor deklariert wurden). Beispiel:
| Datei1.c | Datei2.c |
| //Deklaration von func, //welche in Datei2.c definiert wird extern void func(void); //Definition der Variablen global int global=0; int main(void) { func; global++; return 0; } | //Deklaration von global, //welche in Datei1.c definiert wird extern int global; //Definition der Funktion func void func(void) { global++; } |
Öffnen im Compiler Explorer
Wird das Schlüsselwort 'static' der Variablen/Funktionsdefinition vorangestellt, so wird die Sichtbarkeit/Gültigkeit auf Dateiweit eingeschränkt. Variablen/Funktionen können nur innerhalb der (Objekt-) Datei genutzt werden und sind für anderen (Objekt-)Dateien unsichtbar. (Siehe Kap. DatentypeSpecifier:Internal/External Linkage Internal/External Linkage)
| Datei1.c | Datei2.c |
| //Dateiweite Sichtbarkeit von var1 static int var1; int main(void) { var1++; return 0; } | //Dateiweite Sichtbarkeit von var1 static int var1; void func(void) { static int var2; //Vorsicht, static // hat hier eine andere Bedeutung var1++; } |
Öffnen im Compiler Explorer
Projektweite Gültigkeit bedeutet insbesondere, dass keine doppelten Benennung von Variablen/Funktionen/Datentypen innerhalb des gesamten Projektes erlaubt sind, d.h. alle Variablen/Funktionennamen über alle Dateien/Librarys eindeutig sein müssen. Beispiel 1:
| Datei1.c | Datei.2 |
| #include <stdio.h> int a=1; int main(int argc,char *argv) { printf("Hello World %d",a); void dummy(void); //Deklaration von Dummy dummy; return 0; } | #include <stdio.h> int a=7; void dummy(void) { printf("Hello Again %d\n",a); } |
Öffnen im Compiler Explorer
Der Linker meldet beim Zusammenfügen der Objekt-Dateien, dass die Variable a bereits woanders definiert sei (multiple definition of `a';)!
Beispiel 2:
Öffnen im Compiler Explorer
Hier meldet der Compiler eine Fehlermeldung, da das Symbol printf zu einen als Variable und zum anderen als Funktion (innerhalb der inkludierten Datei stdio.h beschrieben) genutzt wird ('printf' redeclared as different kind of symbol).

### Blockweite Sichtbarkeit

Erfolgt eine Funktion-/Variablen-/Datentyp Definition innerhalb einer Funktion, als Funktionsparameter oder eines Blockes, so sind diese nur innerhalb des Blockes sichtbar/gültig (=Lokale Variable). Blöcke können verschachtelt sein, so dass das bei identischer Namensgebung innere Definitionen Vorrang haben. Ebenso haben Blockdefinitionen Vorrang vor Datei-/Projektdefinitionen (überdecken diese):
| Datei1.c | Datei2.c |
| int main(void) { int var2; //var2 ist nur innerhalb //von main sichtbar struct xyz //Datentyp ist nur innerhalb {int x,y,z;};//von main //sichtbar/gültig extern void func(void); //Deklaration //ist nur innerhalb von //main sichtbar/gültig func; } void foo(void) { struct xyz var_xyz; //Fehler, da //Datentypedefinition hier nicht mehr //gültig ist! func; //Fehler, da Deklaration //hier nicht mehr gültig ist } | #include <stdio.h> void func(void) { int var2=1; //var2 ist //nur innerhalb von //func sichtbar { int var2=2; //var2 ist nur innerhalb //dieses Blockes sichtbar printf("%d\n",var2); } printf("%d\n",var2); } |
Öffnen im Compiler Explorer

### Sichtbarkeit von Funktionsparameter in Protypen

Soll eine Funktion aufgerufen werden, die zuvor nicht definiert wurde, so ist ein Prototyp/(Forward)deklaration der Funktion notwendig (siehe nachfolgendes Kapitel). Die Parameterliste dieses Prototyps hat seinen eigenen Gültigkeitsbereich. Beispiel:
Hinweis:
* Bei Prototypen ist die Angabe von Variablenname nicht notwendig. Die alleinige Angabe des Datentyps ist ausreichend

### Zuordnung der Gültigkeitsbereiche zu globalen/lokalen Variablen

Zum einfacheren Verständnis wird im Skript von folgenden Begrifflichkeiten gesprochen:
* Globale Variablen-/Funktionen-/Datentypdefinitionen → Projektweite Sichtbarkeit/Gültigkeit
* Statisch globale Variable/Funktionen → Dateiweite Sichtbarkeit/Gültigkeit
* Lokale Variablen/Funktionen/Funktionsparameter/Datentypen → Nur innerhalb des zugehörigen Blockes sichtbare/gültig
* Statisch lokale Variablen → Nur innerhalb der Funktion sichtbar/gültig, mit der Besonderheit, dass die Gültigkeit über der Laufzeit der Funktion gilt (siehe Kap. DatentypeSpecifier:Static Storage Class Specifier)

### Speicherzuweisung

Für die einzelnen Gültigkeitsbereiche werden im Speicher (und auch in der Objekt-Datei) unterschiedliche Speicherbereiche (Segmente) vorgehalten, d.h. der Compiler teilt den Speicher in unterschiedliche Bereiche auf und weist den Variablen/Funktionen in Abhängigkeit der Gültigkeit/Sichtbarkeit unterschiedliche Speicherbereiche zu!
Zur Ermöglichung von Rekursion werden lokale Variable auf dem Stack gehandelt. Mit Aufruf einer Funktion oder öffnen eines neuen Block-Scopes wird Speicher auf dem Stack reserviert, der bei Beenden des Scopes wieder freigegeben wird. Die Speicheradresse von lokalen Variablen ist somit nicht Fix, sondern hängt von der Aufrufhierarchie ab!
Globale und statisch globale Variablen werden nicht auf dem Stack gehalten, sondern bekommen einen 'festen' Speicherbereich (hier DATA/BSS/RODATA) während der gesamten Laufzeit des Programms zugewiesen. Die Adresse dieser Variablen sind somit konstant. Das genutzte Speichersegment von globalen Variablen hängen von der Art der Variablen ab. Initialisierte Variablen werden im DATA-Segment gesammelt, welcher mit dem Initialisierungswert der Variablen initialisiert wird. Nicht initialisierte globale Variablen landen im BSS-Segment, so dass diese mit Programmstart durch Füllen dieses Blockes mit 0 auf 0 gesetzt werden. Konstante globale Variablen werden im RODATA-Segment gehalten. Über das Betriebssystem kann dieser Speicherbereich auf nur Lesbar gesetzt werden.
Dynamische Speicheranforderungen, resp. Speicher, dessen Gültigkeit über die Laufzeit einer Funktion hinausgeht, wird im Heap gesammelt. Für die Verwaltung dieses Speichers ist der Programmierer zuständig.
Entsprechend globalen Variablen wird für Funktionen ebenfalls ein eigener Speicherbereich (TEXT Segment) vorgesehen. Im Gegensatz zu Variablen, wo im Speicher der dazugehörige Inhalt gespeichert ist, ist der Speicherinhalt von Funktionen die Maschinenanweisungen. Die Speicheradresse der Funktionen sind konstant.
Hinweis:
* Die Speicheradresse von sowohl globalen und statisch globalen Variablen als auch von Funktionen ist konstant und ändert sich während der Laufzeit des Programms nicht. Die Adressvergabe dieser Variablen und Funktionen erfolgt somit nicht zur Laufzeit sondern durch die Toolchain (hier der Linker).
* Dieses Konzept ist allgemeingültig, gilt nicht nur für die Programmiersprache C/C++

## Definition/Deklaration(Prototyp)

Der C-Compiler ist ein Single-Pass Compiler (siehe Kap Einführung/Literatur:Arbeitsweise eines Compilers ), d.h. für den Übersetzungsvorgang wird der Quellcode einmalig eingelesen und von vorne nach hinten abgearbeitet. Dies bedingt, dass keine Vorwärtsbezüge im Quelltext enthalten sein dürfen:
* keine Variable genutzt werden kann, die erst später definiert wird (da der Compiler in Abhängigkeit des Datentyps (den er dann noch nicht kennt) keine entsprechende Maschinensprachebefehle erzeugen kann)
* keine Funktion aufgerufen werden kann, die erst später definiert wird (da der Compiler nicht weiß, welcher Datentyp die Übergabewerte haben und folglich keine Maschinensprachebefehle zur Konvertierung und Übergabe der Variablen erzeugen kann)
* kein Datentyp genutzt werden kann (struct, union, enum, typedef), welcher erst später definiert wird (da der Compiler nicht weiß, wieviel Speicher er für die Variable reservieren muss)
Eine Lösungsvariante dieser Problematik ist, im Quellcode alle Variablen/Funktionen/Datentypen/Makros zu definieren, bevor diese erstmalig genutzt werden (Funktion main steht dann folglich am Ende des Quellcodes):
Öffnen im Compiler Explorer
Sollen globale Funktionen/Variablen in Datei1.c genutzt werden, die in Datei2.c definiert sind, so schlägt diese Vorgehensweise fehl. Um auch dieses zu ermöglichen, bietet C die Möglichkeiten von Prototypen/Deklarationen an. Diese teilt dem Compiler mit, wie im späteren Ablauf die Variable, die Funktion definiert wird, bzw. dass es diesen Datentyp geben wird:
| Datei1.c | Datei2.c |
| //Prototyp für Variable var extern int var; //Prototyp für Funktion foo void foo(void); int main(int argc, char *argv) { foo; //Compiler kann aufgrund var++; //der Prototypen diese //Aufruf in Maschinen- //sprachebefehle umsetzen } | //Definition der Variablen var int var; //Definition der Funktion foo void foo(void) { } |
Öffnen im Compiler Explorer
Innerhalb der C-Spezifikation werden für den Umgang mit dieser Problematik die Begriffe 'Declaration' und 'Definition' genutzt. Da diese Begriffe jedoch nicht klar abgegrenzt werden und auch für viele weitere Aspekte genutzt werden, sollen in diesem Skript in Anlehnung an den Wikipedia Artikel Deklaration (Programmierung) (im Absatz zur Programmiersprache C/C++) die Begriffe Definition/Deklaration wie folgt genutzt werden:

### Definition

    entspricht der Belegung/Reservierung von Speicherplatz
   * Bei der Definition einer Variablen wird hiermit Speicherplatz entsprechend dem Datentyp der Variable belegt:
    int var1; //entsprechend der Größe des Datentyps int wird //Speicherplatz für die Variable var1 reserviert int var2=4711; //Dito, ergänzend wird dieser Speicherplatz mit dem //Wert 4711 initialisiert
   * Bei einer Funktion wird Speicherplatz zur Aufnahme der Maschinenbefehle belegt:
    void foo(void) { //Die Anweisungen in den geschweiften Klammern //werden in Maschinenbefehle übersetzt. D.h. die //Funktion foo belegt Speicher, dessen Inhalt //die Maschinensprachebefehle sind }
   * Ergänzend wird der Begriff Definition auch in Verbindung mit der Definition von neuen Datentypen genutzt!

### Deklaration/Prototyp

    ist die Mitteilung an den Compiler, wie diese Variable/Funktion an einer späteren Stelle im Source-Code oder in einer anderen Source-Datei definiert wird. Auf Grundlage dieser 'vorab' Information kann der Compiler die passenden Maschinensprachebefehle für den Zugriff/Aufruf auf die Variablen/Funktion einsetzen:
    extern int var1; //Deklaration von var1 extern void foo(void); //Deklaration von foo void foo(void); //Deklaration von foo
Insbesondere bei dem Umgang mit der Deklaration gilt es Folgendes zu berücksichtigen:
* Deklarationen werden entweder:
   * am Anfang einer C-Datei geschrieben, so dass Funktionen/Variablen im Zugriff stehen, die erst später in der C-Datei definiert werden (entspricht dann der Private Anweisung in anderen Sprachen)
   * in Header-Dateien geschrieben, so dass diese Variable/Funktionen für alle sichtbar sind, welche diese Header-Datei einbinden (entspricht der Public Anweisung in anderen Sprachen)
* Deklarationen von Funktionen werden in der C-Spec als (function) Prototyp bezeichnet und sind dadurch gekennzeichnet, dass diese mit einem Semikolon abgeschlossen sind. Der Specifier 'extern' ist hier optional und sagt eigentlich aus, dass diese Funktion external Linkage besitzt (siehe DatentypeSpecifier:Internal/External Linkage). Die Benennung der Parametervariablen sind gleichermaßen optional:
    extern int foo(int var1, float var2); //Deklaration extern int foo(int , float ); //Deklaration int foo(int var1, float var2); //Deklaration + external Linkage int foo(int , float ); //Deklaration + external Linkage
* Für globale Variablen gibt es keine (Variablen) Prototype. Stattdessen muss zur Bekanntgabe des Datentyps der Specifier 'extern' vorangestellt werden, mit welchen ergänzend zum Ausdruck gebracht wird, das diese Variable external Linkage und damit globale Gültigkeit hat (siehe Kap DatentypeSpecifier:Extern - Storage Class Specifier). Da die Deklaration einzig zur Bestimmung der Zugriffsmethoden auf Maschinenspracheebene genutzt wird, können diese keine Initialisierungswerte enthalten:
    extern int var1; extern int var2=7; //KO Deklarationen können keine //Initialisierungswerte haben
Öffnen im Compiler Explorer
* Deklarationen von statisch globalen, lokalen und statisch lokalen Variablen nicht möglich sind:
    extern static int glob; //keine Deklaration von statisch globalen //Variablen möglich int main(int argc, char *argv) { extern int var; //Deklaration bezieht sich auf eine globale //Variable, die nicht definiert ist extern static int lok; printf("%d %d\n",var,lok); int var; static int lok; }
Öffnen im Compiler Explorer
* Deklarationen für identische Variablen/Funktionen können beliebig oft erfolgen, sofern der Datentyp identisch ist:
    extern int var; extern int var; //OK, doppelt Deklaration mit identischen Datentyp extern unsigned int var; //KO, Doppelte Deklaration mit unterschiedlichen // Datentyp
* Sofern eine Deklaration und eine Definition in der identischen Datei erfolgen überprüft der Compiler, ob Deklaration und Definition identisch sind. Es empfiehlt sich, die eigene Header-Datei (in welcher die Deklaration normalerweise enthalten ist) zu inkludieren:
    Datei1.cDatei1.h //Include der eigenen Header-Datei #include "datei1.h" int var1; //Definition von var1 short var2; //Definition von var2 int var3; //Private Variable //da kein Prototyp //im Header vorhanden void func(void) { } extern int var1; //Deklaration extern char var2; //KO, da Deklaration //von Definition //abweicht void func(int); //KO, da Deklaration //von Definition //abweicht
Öffnen im Compiler Explorer
* Innerhalb einer Datei kann eine Variable identischen Datentyps mehrmals definiert werden (bei identischen Initialisierungswert). Diese Definitionen werden vom Compiler als eine Definition angesehen. Wenn jedoch eine identische Variable in unterschiedlichen Dateien definiert wird, so legt jede Datei eine eigene unabhängige Variable an, welche dann beim Zusammenführen der Objektdateien durch den Linker zu Fehler führt. Da Header-Datei zumeist in mehrere C-Dateien inkludiert werden, sind Definitionen in Header-Dateien 'verboten'.
* Deklarationen innerhalb eines Block-Scopes oder eines Function Prototyp Scopes sind nur in diesen Bereich gültig.

## Grundlegende Datentypen und Typsicherheit

Die Programmiersprache C kennt folgende grundlegende Datentypen (siehe Kap. Datentypen). Von jedem Datentyp kann ein Zeiger angelegt werden:
| 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; |
Mit der Defintion (dem Anlegen) einer Variablen eines Datentyps wird Speicher entsprechend der Größe des Datentyps reserviert. Optional kann dieser Speicher initialisiert werden. Über den Datentyp wird ausgesagt, wie der Inhalt des Speichers zu interpretieren ist:
Die Funktionalität der Operatoren hängt vom Datentyp der Operanden ab. Der Plus-Operator angewendet auf ganzzahlige Operanden führt eine Ganzzahl Addition aus, angewendet auf gleitkomma Operanden eine Gleitkomma Addition. Ist einer der Operanden ein Zeiger, so wird eine Zeigeraddition angewendet. Für andere Datentypen ist der Plus-Operator nicht definiert, so dass der Compiler einen Fehler wirft. Bei der Betrachtung eines Ausdruckes sollte dem genutzten Datentypen folglich eine besondere Aufmerksamkeit zuteilwerden:
Das Ergebnis einer Operation hat ebenfalls einen Datentyp. Eine Ganzzahl Addition gibt als Ergebnis einen Ganzahl Datentyp zurück, eine Gleitkomma Addtition einen Gleitkomma Datentyp, usw.:
Wie Operatoren haben auch Funktionen Operanden (in Form von Übergabeparameter). Das Ergebnis eines Funktionsaufrufes wird ebenfalls in Form eines Datentyps zurückgegeben:
Sind die Datentypen der Operanden nicht 'kompatibel', so 'passt' der Compiler die Datentypen im Falle von Ganzahl und Gleitkomadatentypen per impliziter Regel an (siehe Kap. Datentypen:Implizite Typumwandlung). Über explizite Typumwandlung (siehe Kap.Datentypen:Explizite Typumwandlung) kann ein Datentyp z.T. in einen anderen Datentyp gewandelt werden. In allen anderen Fällen ist C Typsicher, d.h. der Compiler gibt zumindest eine Warning aus, wenn die Datentypen nicht identisch sind und der Compiler keine implizite Regel anwenden kann.
Tipp:
* Compiler gibt bei Fehlermeldung oftmals die erwarteten und die vorhandenen Datentyp an. Wenn solch eine Fehlermeldung kommt, sollten sie sich a) überlegen, was wollten sie syntaktisch geschrieben haben und b) was haben sie in der Tat geschrieben. Ein Google nach einer Lösung des Compilerproblems löst zumeist nicht das Problem, sondern umgeht es!
Hinweis:
* Ein guter Programmierer sollte die Datentypen so wählen, dass:
   * die Datentypen zum Inhalt und zum Wertebereiches passen
   * keine Datentypkonvertierungen notwendig sind
    andernfalls besteht die Gefahr, dass hiermit Laufzeitfehler einprogrammiert werden.
* Wenn Datentypkonvertierungen notwendig sind, sollte die explizite Konvertierung genutzt werden

## Abhängigkeiten bzgl. Rechnerarchitektur

Kenngrößen von Prozessoren sind unter anderem die Anzahl der Bits für den Datentransport und für Arithmetisch-/Logische-Operationen (→ Datenbreite) und die Anzahl der Bits für die Adressierung des Speichers (→ Adressierungsbreite). Die Datenbreite sagt dabei aus, welche max. Datenbreite ein Maschinenbefehl (zumeist in einem Taktzyklus) transportieren/berechnen kann. Werden programmtechnisch größere Datenbreiten benötigt, bedingt dies, dass hierzu mehrere Maschinenbefehle notwendig sind. Wenn kleinere Datenbreiten notwendig sind, so wird oftmals das Ergebnis abgeschnitten (sofern der Prozessor keine Befehle für den Umgang mit kleineren Datentyp beherrscht):
| 32-Bit Datentransport/Operation bei 32-Bit Datenbreite | 64-Bit Datentransport/Operation bei 32-Bit Datenbreite | 16-Bit Datentransport/Operation bei 32-Bit Datenbreite |
| int32_t a32,b32; | int64_t a64,b64; | int16_t a16,b16; |
| // a32=b32; mov eax,b32 mov a32,eax | // a64=b64; mov eax ,b64 mov edx ,b64+4 mov a64 ,eax mov a64+4,edx | //a16=b16; movz eax,b16 mov a16,ax |
| // a32=a32+b32; mov edx, a32 mov eax, b32 add eax, edx mov a32, eax | // a64=a64+b64; mov ecx , a64 mov ebx , a64+4 mov eax , b64 mov edx , b64+4 add eax , ecx adc edx , ebx mov a64 , eax mov a64+4, edx | // a16=16+b16; movzx eax, a16 mov edx, eax movzx eax, b16 add eax, edx mov a16, ax |
Die Adressierungsbreite gibt an, mit welcher Bitbreite der Speicher adressiert werden kann:
* Bei einem 32-Bit System können 2$^{32}$=4.294.967.295=4Gi Bytes/Speicherstellen adressiert werden
* Bei einem 16-Bit System können 2$^{16}$=65.536=64Ki Bytes/Speicherstellen adressiert werden
* Bei einem 64-Bit System können 2$^{64}$=18.446.744.073.709.551.616 =16Ei ~18Exa Bytes/Speicherstellen adressiert werden
Waren die Datenbreite und die Adressierungsbreite bei den ersten Prozessoren noch unterschiedlich, so sind diese heutzutage zumeist identisch. Bei einem 32-Bit Prozessor bedeutet dies, dass dieser max. 4Gi Bytes Speicherstellenstellen adressieren kann und Maschinenbefehle für den Datentransport und die Berechnung von 32-Bit Datenworten beinhaltet.
Die Datenbreite und auch die Adressierungsbereite haben direkten Einfluss auf die Programmiersprache:
* Wird defaultmäßig mit größerer Datenbreite gerechnet, als der Prozessor 'von Hause' aus unterstützt, so werden die erzeugten Maschinenprogramme größer und langsamer (Umgedreht bedeutet dies jedoch nicht, dass die Maschinenprogramme kleiner und schneller werden)
* Die max. Größe von Verbunddatentypen und Arrays ergibt sich aus der Adressierungsbreite des Prozessors
Die Intention der Programmiersprache C/C++ ist, die Prozessorarchitektur optimal zu nutzen. Dementsprechend sind diverse Eigenschaften der Sprache nicht fest spezifiziert, sondern von der Rechnerarchitektur abhängig.

### Integer Datentyp

Intention von C/C++ ist, dass der Umgang mit dem Grunddatentyp 'int' möglichst direkt in einen Maschinensprachebefehl umgesetzt werden kann. Bei einem 16-Bit Prozessor hat der Datentyp int daher eine Breite von 16-Bit und bei einem 32-Bit Prozessor eine Breite von 32-Bit (siehe Kap Datentypen:Ganzahl Datentypen). Über vorangestellte Qualifier kann dieser Grunddatentyp verkleinert/vergrößert werden.

### Integer Arithmetik

Ganzzahlen haben nur einen begrenzten Wertebereich. Wird dieser bspw. durch eine Addition überschritten (d.h. das Rechenergebnis benötigt zur Darstellung mehr Stellen, als der Datentyp 'hergibt'), so gibt es zwei Vorgehensweisen:
* Arithmetischer Überlauf (siehe arithmetischer Überlauf): Die nicht speicherfähigen Stellen werden verworfen, so dass i.d.R. ein falsches Ergebnis erzeugt wird!
    Beispiel:
    unsigned char a=254,b=3,c; c=a+b; //c ist (254+3)%256=1 c=a*b; //c ist (254*3)%256=250
Öffnen im Compiler Explorer
* Sättigungsarithmetik (siehe Sättigungsarithmetik): Alle Operationen laufen innerhalb eines festen Intervalls ab, so dass Überläufe und Unterläufe auf dieses Intervall begrenzt werden:
    Beispiel:
    unsigned char a=254,b=3,c; c=a+b; //c ist MAX((254+3),255)=255 c=a*b; //c ist MAX((254*3),255)=255
Die Spezifikation von C macht bezüglich der zu nutzenden Methodik keine Vorgaben, so dass das Handling von der ALU des Prozessors abhängt. Aus Komplexitäts- und Geschwindigkeitsgründen ist dies zumeist der arithmetische Überlauf. Nur bei Spezialprozessoren (z.B. DSP Digital Signal Processor) kommt z.T. die Sättigungsarithmetik zum Einsatz.

### Gleitkomma Datentyp und Arithmetik

Für den Umgang mit Gleitkommazahlen bieten einige Prozessoren spezielle Recheneinheiten ( Gleitkommaeinheit Floating Point Units FPU) an. Sowohl die Darstellung von Gleitkommazahlen als auch das Verhalten bei Überlauf/Unterlauf ist damit Herstellerabhängig. Um auch diese Einheiten effektiv nutzen zu können, ist die C/C++ Spezifikation in diesem Bereich ebenfalls sehr offen.

### Max. Speicherbedarf von Variablen

Die maximale Größe von Arrays/Strukturen/Unions ist in C/C++ von der zugrundeliegenden Rechnerarchitektur abhängig:
Öffnen im Compiler Explorer
Im Falle von globalen Variablen erzeugt der Compiler/Linker eine Fehlermeldung. Werden von diesen Datentypen Variablen als lokale Variablen angelegt, so erzeugt dies zur Laufzeit einen Stackoverflow.

### Zeigervariable

Eine Zeigervariable ist eine Variable, die eine Speicheradresse speichert. Die Größe dieses Datentyps hängt von der Adressierungsbreite des Prozessors ab. Unabhängig von dem Datentyp, auf den die Zeigervariable zeigt, ergibt sich folgende Breite:
* 4-Byte bei einer 32-Bit Rechnerarchitektur
* 8-Byte bei einer 64-Bit Rechnerarchitektur
* 2-Byte bei einer 16-Bit Rechnerarchitektur

### Datentyp size_t

Die maximale Größe von Arrays/Strukturen/Unions ist in C von der zugrundeliegenden Rechnerarchitektur abhängig. Sie ergibt sich aus der Adressierungsbreite. Da die Datenbreite des Datentyps int zumeist kleiner als die Adressierungsbreite ist, existiert in C/C++ der vorzeichenlose Datentyp size_t. Alle Funktionen, welche den Speicherbedarf übergeben bekommen, bzw. zurückgeben (bspw. strlen, memcpy, sizeof) , nutzen den Datentyp size_t:
Der Datentyp size_t ist in der Header-Datei stddef.h der Standard-C-Library per typedef gesetzt. Im Normalfall muss diese Header-Datei nicht explizit inkludiert werden, da bei Nutzung von z.B. strlen diese bereits durch die Header-Datei string.h inkludiert wird:
Hinweis:
* Der Datentyp size_t ist immer ein vorzeichenloser Datentyp (es gibt keine negativen Speicheradressen).
* Mit dem Datentype ssize_t wird eine vorzeichenbehaftete Variante zur Verfügung gestellt, welche hilfreich ist, wenn Speicheradresse subtrahiert werden sollen.
* Soll der Wert vom Datentyp size_t mittels printf ausgegeben werden, so ist der 'Length Modifier' z und der 'Conversion Specifier' u zu nutzen:
    size_t var=strlen("String"); printf("%zu",var);
Öffnen im Compiler Explorer

## Boolescher-Datentype/Operatoren

Ursprünglich gab es den booleschen Datentyp in der Programmiersprache C nicht. Mit C99 wurde zwar der Datentyp _Bool eingeführt, dies änderte jedoch nicht den Umgang mit Anweisungen und Operatoren, die einen booleschen Datentyp erwarten/zurückgeben!
An allen Stellen, an denen ein boolescher Datentyp erwartet wird (z.B. IF-Abfrage, bei booleschen Operationen) wird der Datentyp Ganzzahl, Gleitkommazahl oder Zeiger erwartet (= Scalar-Type) und ausgewertet. Der hierin gespeicherte Zahlenwert sagt aus, ob diese Zahl als True oder False zu interpretieren ist:
* False, wenn Ganzzahl gleich 0 oder Gleitkommazahl gleich 0.0 oder Zeiger gleich NULL
* True, wenn Ganzzahl ungleich 0 oder Gleitkommazahl ungleich 0.0 oder Zeiger ungleich NULL
Beispiel:
Öffnen im Compiler Explorer
Operatoren, welchen einen booleschen Wert zurückliefern (Vergleichs-Operatoren und Boolesche-Operatoren), liefern ein Integer-Datentyp zurück:
* (int)0 für False
* (int)1 für True
Beispiel:
Öffnen im Compiler Explorer
In den if/for/while Anweisungen ist folglich kein explizites Konvertieren eines Ganzzahl-/Gleitkomma-/Zeigerwertes mittels des Vergleichs-Operators in einen Booleschen Wert notwendig:
Umgedreht kann ein boolescher Wert oder auch das Ergebnis einer booleschen Operation direkt mit einer Ganzzahl- oder Gleitkommazahl verknüpft werden:
Öffnen im Compiler Explorer
Mit C99 wurde der Datentyp _Bool eingeführt. Dieser entspricht weiterhin einem Integerdatentyp mit der Besonderheit, dass beim Zuweisen eines Wertes dieses auf true oder false geprüft wird und Variablen dieses Datentyps nur die Werte 0 oder 1 annehmen können:
Öffnen im Compiler Explorer
Für den einfacheren Umgang mit dem 'neuen' Datentyp wurde in der Header-Datei stdbool.h die Makros true (als Austauschwert für die Ganzzahl 1) und false (als Austauschwert für die Ganzahl 0) definiert. Ergänzend wird hier der Datentyp bool als Alias auf _Bool gesetzt.
Öffnen im Compiler Explorer
Hinweis:
* In C++ gibt es anstatt des Datentypes _Bool den Datentyp bool. Auch sind die beiden Konstanten true und false ohne zusätzliches Einbinden der Header-Datei vorhanden. Der weitere Umgang ist jedoch identisch zu C.

## Literale/Konstanten

Zahlenwerte und Strings im Source-Code werden vom Compiler in binäre Zahlen umgerechnet. Über der Schreibweise/Syntax der Literale/Konstanten wird dem Compiler gesagt, in welchem Zahlenformat die Zahl auf Source-Code Ebene beschrieben ist und welchem Datentyp sie entspricht. Unabhängig wie die Konstanten im Source-Code beschrieben sind, werden die Zahlen intern als binäre Zahlen gespeichert. Beispiele, wie die Zahlen intern gespeichert werden:

### Ganzzahl-Konstanten

Im Allgemeinen ordnet der Compiler einer numerischen, ganzzahligen Konstanten den kleinstmöglichen Datentyp beginnend vom Datentyp integer zu. Sobald der Wert der Konstanten den Wertebereich von integer übersteigt, wird der nächst größere passende Datentyp gewählt. Ergänzend gilt, dass alle Ganzzahl-Konstanten als vorzeichenbehaftete Ganzzahl-Konstanten angesehen werden! Beispiele für integer Konstanten:
Mit den Suffixen L/l und LL/ll kann der Datentyp long und long long erzwungen werden:
Mit dem Suffix U/u kann die Konstante explizit auf unsigned gesetzt werden:

### Char-Konstanten

In einfachen Anführungsstrichen geschriebene ASCII Zeichen werden entsprechend der ASCII-Tabelle als Zahlenwert (Datentyp int) dargestellt:
Über den Escape-Operator (Zeichen Backslash '\') wird das/die dem ESCAPE Operator folgende Zeichen gesondert interpretiert. Die im Source-Code erhaltene Zeichenfolge wird im Computerprogramm als ein Zahlenwert (Datentyp int) gespeichert. Dies ist u.A. zur Darstellung von ' und " aber auch zur Darstellung der unteren 32 Zeichen der ASCII Tabelle notwendig (Siehe auch Steuerzeichen)
Beispiel
Öffnen im Compiler Explorer
Über Prefixe können alternative Zieldatentypen/Konvertierungen erzwungen werden:
Hinweis
* Die ASCII-Tabelle basiert auf einer 7-Bit Zeichenkodierung, so dass bspw. die Umlaute 'ä', 'ö' und 'ü' dort nicht kodiert sind. Sollen auch diese Zeichen genutzt werden, so empfiehlt sich der Datentyp wchar_t

### Gleitkommakonstanten

Alle Gleitkommakonstanten sind ohne weitere Angaben vom Datentyp double!
Über Suffixe kann ein alternativer Datentyp erzwungen werden:
Hinweis:
* Aufgrund der unterschiedlichen Basis zwischen dem menschlichen Zehner-System und dem computerinternen Binär-System entstehen insbesondere bei den Nachkommastellen Rundungsfehler. Die Konstante 0.1f wird durch die Wandlung intern als 1.00000001490116119384765625e-1 dargestellt
* Die C-Spec verlangt, dass die binäre Repräsentation von Gleitkommazahlen zur Laufzeit (z.B. durch die Funktion atof strtod scanf("%f")) identisch zur der zur Compilezeit ist. Beispiel:
    if(3.14==atof("3.14")) printf("muss erfüllt sein"); //3.14 wird vom Compiler in ein Binärwert gewandelt //Der String "3.14" wird zur Laufzeit mittels atof in ein Binärwert gewandelt //Beide Wandlungsergebnisse müssen identisch sein
Öffnen im Compiler Explorer

### Konstanten Ausdrücke

Ergänzend zu den Zahlenwerten werden auch Ausdrücke durch den Compiler ausgewertet. Als Voraussetzung für die Ausdrücke gilt, dass alle Operatoren Literale/Konstante sein müssen, so dass der Compiler die Berechnung durchführen kann. Als Operatoren können alle im normalem Syntax erlaubten Operatoren (z.B.: +,-,*,/,&,&&,|,||,>>,<<,==,!=) genutzt werden. Beispiel:
Öffnen im Compiler Explorer
Hinweis:
* Konform zur C-Spezifikation bieten einige Compiler weitere Operatoren aus der math.h Library an (z.B. sin, cos, ...). Bestehen die Aufrufparameter aus Konstanten, so ersetzt der Compiler den Ausdruck durch die 'Ergebnis'-Konstante:
    int var3=sin(1.0)*10; →wird vom Compiler durch 8 ersetzt
* Konstanten Ausdrücke werden durch den Präprozessor ausgewertet, so dass dieser die Ergebnisse ebenfalls nutzen kann:
    #define BUF_SIZE 512 #if (BUF_SIZE & (BUF_SIZE -1)) != 0 #endif

### String Konstanten

Stringkonstanten erzeugen ein Array von Characters. Mit dem abschließenden Hochkommata wird das Array um ein Stringendezeichen ergänzt:
Adjacent Strings Konstanten werden vom Compiler zu einer String-Kontante zusammengefasst, so dass nur ein Stringendezeichen vorhanden ist:
Über Prefixe können alternative Zieldatentypen/Konvertierungen erzwungen werden:
Über Postfixes können in C++ Objekte erzeugt werden:
String Konstanten werden Compilerintern über unnamed Variablen realisiert, welche als globale Variable angelegt werden und per default const sind:
Öffnen im Compiler Explorer
Für doppelte String Konstanten wird nur einmal Speicher reserviert:
Öffnen im Compiler Explorer

## Initialisierungsliste / Compound Literal

Arrays, Strukturen und Unions bestehen zumeist aus mehreren Elementen. Zur Initialisierung solcher Variablen werden die Initialisierungswerte in geschweiften Klammern als Initialisierungsliste zusammengefasst.
Syntax: {Initialisierungsliste}
Für eine Initialisierungsliste wird kein Speicher vorgehalten. Vielmehr wird die Variable direkt mit dem Inhalt der Liste 'gefüllt'. Der Datentyp der Initialisierungsliste ergibt sich aus dem Datentyp der Variable:
Wird der Initialisierungsliste ein Datentyp in runden Klammern vorangestellt, so wird hiermit eine 'unnamend' Variable/Objekt, ein Compound Literal erstellt:
Syntax: (type) {Initialisierungsliste}
Compound Literale können überall dort genutzt werden, wo andernfalls vorab initialisierte Variablen des gleichen Typs eingesetzt würden. Bei einmaligen Gebrauch solcher Variablen erspart man sich hierüber die Definition der Variablen:
Öffnen im Compiler Explorer
Der Gültigkeitsbereich eines Compound Literals entspricht der von Variablen. Wird ein Compound Literal außerhalb eines Block-Sopes angelegt, so entspricht sie einer globalen Variablen, andernfalls einer lokalen Variable.
Hinweis:
* Compound-Literal sind per Default keine Konstanten, können also zur Laufzeit geändert werden (siehe auch 7.8 Const Type Qualifier):
    float *arr=(float){1.1,2.2,3.3}; //Kein Typkonflikt arr[0]=47.11; //Änderung des 'Konstanten' arr[0] ist möglich
Öffnen im Compiler Explorer

## Variableninitialisierung

Mit der Definition einer Variable kann diese optional initialisiert werden. Der tatsächliche Zeitpunkt der Variableninitialisierung (zur Laufzeit oder zur Compilezeit) und damit die Art des Initialisierungswertes / der Inhalt der Initialisierungsliste hängt von der Gültigkeit/Sichtbarkeit der Variablen ab (siehe Gültigkeit/Sichtbarkeit von Variablen).

### Globale + Static lokale Variablen

Globale und statisch globale Variablen werden in einem während der Laufzeit des Programms festen Speicherbereich gehalten. Zum Startzeitpunkt des Programms ist der Speicherplatz solch einer Variable mit dem Initialisierungswert gefüllt, so das bei erstmaliger Nutzung dieser Variablen diese ihre Initialisierungswerte enthalten. Die Belegung des Speicherplatzes erfolgt durch den Compiler, so dass als Initialisierungswerte /Inhalt der Initialisierungsliste nur Konstanten oder Konstanten-Ausdrücke (siehe Literale/Konstanten) erlaubt sind:
Öffnen im Compiler Explorer
Nicht initialisierte globale und static lokale Variablen werden mit 0 initialisiert! D.h. die Speicherstellen dieser Variablen werden mit dem Wert 0 vorbelegt:
Öffnen im Compiler Explorer

### Lokale Variable

Der Speicherplatz für lokale Variable wird auf dem Stack zur Verfügung gestellt. Bei jeder Ausführung der Definition wird Speicher vom Stack nach dem LIFO Prinzip reserviert und im Anschluss Maschinencode zur Initialisierung der Speicherstellen ausgeführt (sofern Variable einen Initialisierungswert hat). Zum besseren Verständnis kann man sich die Variableninitialisierung als zwei Anweisungen vorstellen: a) der Definition der Variablen und b) der Zuweisung des Initialisierungswerter zur Variablen:
Folglich können die Initialisierungswerte/Inhalte der Initialisierungsliste aus beliebigen Ausdrücken stehen und müssen keine Konstanten oder Konstanten Ausdrücke sein:
Öffnen im Compiler Explorer
Aufgrund der Ausführung von Maschinencode bedingt die Initialisierung Rechenzeit. Wird eine Variableninitialisierung mit einer switch oder goto Anweisung übersprungen, so wird zwar die Variable angelegt (Speicherplatz reserviert), jedoch die Maschinenbefehle zur Initialisierung dieser Variablen nicht ausgeführt (siehe auch Switch-Anweisung und Label + Goto). Die Variable enthält dann einen Zufallswert:
Öffnen im Compiler Explorer
Nicht initialisierte lokale Variablen werden nicht initialisiert, d.h. keine zusätzlichen Maschinenbefehle zum Initialisieren erzeugt. Der Variablenwert entspricht dann einem 'Zufallswert' oder korrekter gesagt dem Inhalt der Variablen, welcher zuvor der Speicherstelle zugewiesen war:
Öffnen im Compiler Explorer
Hinweis:
* Bei erstmaligem Funktionsaufruf kann eine nicht initialisierten Variablen den Wert 0 beinhalten (Stackframe wird mit Programmstart auf 0 gesetzt). Beim nächsten Aufruf der Funktion ist zumeist die Speicherstelle der lokalen Variablen mit anderen Werten gefüllt worden, so dass nun ein anderer 'Zufallswert' als 'Initialisierungswert' enthalten ist.

### C++

In C++ ist ergänzend die Initialisierung einer Variablen über möglich. Bei Objekten wird hierüber der zugehörige Konstruktor aufgerufen. Bei primitiven Datentypen entspricht der Wert in den Klammern dem Initialisierungswert:

### Sonstiges

Folgende weitere Sachverhalten gilt es bzgl. Variableninitialisierung zu berücksichtigen:
* Nach C-Spezifikation ist eine Initialisierung von Variablen nicht notwendig. Es liegt folglich kein Fehler vor, wenn auf eine nicht initialisierte Variable lesend zugegriffen wird. Ungeachtet der C-Spezifikation überprüft der Compiler mit dem Schalter '-Wall' diesen Sachverhalt und gibt eine Warning aus.
* Deklarationen/Prototypen können keinen Initialisierungswert enthalten
* Const und lokale Static Variablen sollten initialisiert werden, auch wenn C in diesem Fall keinen Fehler meldet!

## Anweisung (Statement) & Expression

Eine Anweisung (Statement) stellt entsprechend dem Wikipedia Artikel Anweisung "ein in der Syntax einer Programmiersprache formulierte einzelne Vorschrift dar, die im Rahmen der Abarbeitung des Programms auszuführen ist". Entsprechend der C-Spezifikation [C11 6.8] kann eine Anweisung aus:
    labled-statement: Eine Sprungmarke für die Switch- und die Goto-Anweisung
    compound-statement: siehe nachfolgendes Kapitel
    expression-statement: siehe nachfolgendes Kapitel
    selection-statement: Eine if oder switch Anweisung
    iteration-statement: Eine while, do while oder eine for Anweisung
    jump-statement: Eine goto, break, continue oder return Anweisung
Die einzelnen Statements können dabei aus weiteren Statements bestehen.

### Ausdruck (Expression)

Entsprechend der C-Spezifikation [C11 6.5] ist ein Ausdruck eine Folge von Operatoren und Operanden, die die Berechnung eines Werts angibt, ein Objekt oder eine Funktion bezeichnet, Nebenwirkungen erzeugt oder eine Kombination davon ausführt. Die Wertberechnungen der Operanden eines Operators werden vor der Wertberechnung des Ergebnisses des Operators sequenziert (siehe auch Expression).
Beispiele:
Öffnen im Compiler Explorer

### Discarded Value

Discarded Value sind Ausdrücke, dessen Ergebnisse nicht verwendet werden (also keiner Variablen zugewiesen werden). Ergebnisse von Ausdrücken, die nicht verwendet werden, führen nicht zu einer Fehlermeldung:
Öffnen im Compiler Explorer
Hinweise:
* Soll bewusst der Rückgabewert verworfen werden, so kann/sollte mit dem explizten Cast auf (void) der Rückgabewert explizit 'verworfen' werden. Dies ist u.A. hilfreich bei Variablen, die (noch) nicht benutzt werden und der Compiler die Warning "unused Variable" ausgibt:
    (void)a; //Zur Vermeidung von Compiler-Warnings "Unused Variable" (void)(a+7); //hier eine explizte 'Ansage' der Verwerfung des Wertes (void)func;
Öffnen im Compiler Explorer
* Mit der Anweisung [[nodiscard]] (ab C++17, ab C23) wird der Compiler angewiesen, dass der Aufrufer einer Funktion mit Rückgabewert der Rückgabewert entgegenzunehmen ist. Wird der Rückgabewert verworfen, so gibt der Compiler eine Fehlermeldung aus:
    [[nodiscard]] int Client(int socket); Client(5); ////Fehlermeldung, da Rückgabewert nicht verwendet wird
Öffnen im Compiler Explorer
* Java kennt Dicarded Values nicht. Ausnahme, es handelt sich um einen Funktionsaufruf, dessen Rückgabewert nicht genutzt wird.

### Sequence Point

Ein Sequenzpunkt (u.A. das abschließende Semikolon am Ende der Anweisung) ist nach dem Wikipedia Artikel Sequence Point definiert als der Punkt in der Ausführung eines Computerprogramms, an dem garantiert ist, dass alle Nebenwirkungen früherer Bewertungen durchgeführt wurden und noch keine Nebenwirkungen von nachfolgenden Bewertungen durchgeführt wurden. Sequenzpunkte sind beispielsweise dann von Interesse, wenn dieselbe Variable innerhalb eines einzelnen Ausdrucks mehr als einmal geändert wird und die Ausführungsreihenfolge der Operationen nicht sauber spezifiziert ist. Ein oft zitiertes Beispiel ist der C-Ausdruck 'i=i++;'. Der Sequence Point garantiert, dass alle Operationen (hier Zuweisung und Postinkrement) ausgeführt sind. Jedoch ist die Ausführungsreihenfolge dieser beiden Operationen nicht spezifiziert. In Abhängigkeit des Zeitpunktes der Ausführung des Postinkrementes (vor oder nach der Zuweisung) kann i unterschiedliche Werte annehmen. In C und C++ führt die Auswertung eines solchen Ausdrucks zu undefiniertem Verhalten. Andere Sprachen, wie z. B. C#, definieren den Vorrang des Zuweisungs- und des Inkrementoperators so, dass das Ergebnis des Ausdrucks i=i++ garantiert ist.
Öffnen im Compiler Explorer
Weitere Sequence Points:
* Beim ?-Operator stellt sowohl das ? als auch der : ein Sequenz Point dar. Zunächst muss der linke Ausdruck vollständig bearbeitet werden, bevor einer der beiden Bedingungen bearbeitet wird
* Bei der logischen UND / ODER Funktion stellen der Operator ein Sequenz Point dar. Erst wird der linke Operand vollständig ausgeführt und ausgewertet, bevor (ggf.) der rechte Operand ausgewertet wird (siehe Logische Verknüpfungen)
* Beim Komma-Operator stellen der Komma ein Sequence-Point dar (siehe Komma-Operator)
* und weitere (siehe Appendix der C-Spec)
Operationen, bei denen die Ausführungsreihenfolge nicht spezifiziert ist:
* Bei Funktionsaufrufen ist die Abarbeitungsreihenfolge der Parameter nicht spezifiziert, so dass bspw.:
    //Aufrufruf einer Funktion, deren Parameter sich aus Funktionsaufrufen //ergeben (*pf[f1]) (f2, f3 + f4); //pf -> Array von Funktionszeigern //Die Funktionen f1,f2,f3 und f4 können in einer beliebigen Reihenfolge //aufgerufen werden
Öffnen im Compiler Explorer
* Per Postinkrement/-dekrement Operator sagt aus, dass für die eigentliche Operation der Wert der Variablen genutzt wird und im Anschluss die Variable erhöht / erniedrigt werden soll. Nach C-Spezifikation [C11 6.5.2.4] muss dies spätestens vor dem nächsten Sequence Point erfolgt sein. Der genaue Zeitpunkt innerhalb des Sequence Points ist jedoch nicht vorgegeben:
    int a=7; int b=a++ + a; //Ergebnis Abhängig, ob das Postinkrement vor der Addition //oder als letztes ausgeführt wird. a=a++ + 1; //dito
Öffnen im Compiler Explorer

### Block / Compound-Statement

Ein Block oder Compound-Statement fasst Vereinbarungen und Anweisungen zu 'einer' Anweisung/Statement zusammen. Es kann überall dort eingesetzt werden, wo nur eine Anweisung erlaubt ist.
Syntax: {Declaration-or-statement-list/textsuperscript$_{OPT}$}
Ein Block wird nicht wie bei einer Anweisung mit einem Semikolon abgeschlossen (Das angehängte Semikolon wäre dann eine separate leere Anweisung!).
Ein Block hat blockweite Sichtbarkeit (siehe Blockweite Sichtbarkeit), so dass hier definierte Variablen nur hier gültig sind. Deklarationen sind ebenfalls nur in dem Block gültig.
Die GNU-C Reference (nicht C++) erlaubt es, Funktionen innerhalb von Blöcken (und damit auch innerhalb von Funktionen) zu definieren. Diese innere Funktion steht dann nur in diesem Block zur Verfügung und kann ergänzend auf Variablen des umgebenden Blockes zugreifen:
Öffnen im Compiler Explorer
Hinweise:
* Blöcke können/sollten als Strukturierungswerk genutzt werden. Dem Leser von Source Code kann auf dieser Weise vermittelt werden, dass diese Anweisungen 'zusammen' zu lesen sind.
* Im Gegensatz zu Java, in welchem innerhalb eines Blockes kein Variablenname genutzt werden darf, die außerhalb des Blockes existiert, ist dies in C erlaubt. Die äußere Variable steht damit innerhalb des Blockes nicht mehr im Zugriff.
    int a=3; int main(void){ int a=7; //Globale Variable ist ab hier nicht mehr sichtbar/zugreifbar { int a=9; //Innere Variable überdeckt äußere Variable } }
Öffnen im Compiler Explorer

### Embedded Statement

Embedded Statements sind eine GNU-C Erweiterung (siehe GNU-C-Manual 3.18). Da sie vom Autor als sehr nützlich angesehen wird und auch von diversen Compilern und auch in C++ unterstützt wird, wird diese hier beschrieben.
Ein Block fasst mehrere Anweisungen zu einer Anweisung zusammen, hat jedoch kein Rückgabewert. Ein Embedded-Statement entspricht einem Block mit der Ergänzung, dass dieser einen Rückgabewert hat, so dass ein Embedded-Statement überall dort eingesetzt werden kann, wo eine Expression erwartet wird.
Syntax: ({Declaration-or-statement-list$_{OPT}$})
Der Rückgabewert wird nicht über eine return-Anweisung angegeben, sondern entstammt aus der letzten Anweisung innerhalb der Liste:
Öffnen im Compiler Explorer
Ein Embedded-Statement ähnelt folglich einem Funktionsaufruf mit dem Unterschied, dass a) in der Tat kein Funktionsaufruf stattfindet und b) der Rückgabewert nicht mit return, sondern über die letzte Anweisung zurückgegeben wird. Der Anwendungsfall ist dort zu finden, wo eine Expression erwartet wird und mehrere Anweisungen zum Ermitteln der Expression notwendig sind:
Öffnen im Compiler Explorer
Hinweise:
* Ein Embedded-Statement ist insbesondere bei Nutzung von Makros (siehe Kap Präprozessor) hilfreich
* Mittels eines Embedded-Statement kann eine anonyme Funktion/Lambdafunktion dargestellt werden:
Öffnen im Compiler Explorer

## Funktionen / Prozeduren

Funktionen/Prozeduren sind ein Strukturierungsmerkmal, so dass Teile der Funktionalität des Programmes wiederverwendbar sind. C/C++ unterscheidet nicht zwischen Funktionen (haben einen Rückgabewert, können damit in Ausdrücken verwendet werden) und Prozeduren (haben keine Rückgabewert, resp. Rückgabetyp void). Mit jedem Aufruf einer Funktion wird für den zugehörigen Block ein eigener Stack-Kontext angelegt, so dass Funktionen sich selbst aufrufen können (Rekursion). Jeder Funktionsinstanz hat somit seinen eigenen lokalen Variablenbereich, teilen sich jedoch die globalen und die statisch lokalen Variablen. Mit dem Aufruf einer Funktion können Parameter übergeben werden, wobei die Übergabeparameter vom Aufrufer (Caller) durch den Aufruf in die lokalen Variablen des Aufgerufenen (Callee) kopiert/zugewiesen werden (Call by Value). Wird ein Zeiger übergeben, hat der Callee über die Dereferenzierung des Zeigers die Möglichkeit, die Variablen des Caller zu verändern (Call by Reference) (siehe Kap. Zeiger:Zeiger bei Funktionsaufrufen).).
Hinweise:
* Die Klammerung hinter dem Funktionsnamen (foo) zeigt an, dass es sich um einen Funktionsaufruf (Verzweigung der Programmausführung zu der Startadresse der Funktion) handelt
    void foo(void) {printf("foo called");}; foo; //Klammern hinter dem Funktionsnamen bedeuten Aufruf der Funktion
* Die Nutzung des Funktionsnamens ohne Klammerung (foo) gibt die Startadresse der Funktion zurück. Die Startadresse kann in einem Zeiger auf Funktionen gespeichert werden (siehe Kap. Zeiger:Zeiger auf Funktionen).
    void foo(void) {printf("foo called");}; void (*fptr)(void)=foo;
Öffnen im Compiler Explorer
* Deklaration/Definition vor dem ersten Aufruf der Funktion 'notwendig', damit Compiler die Übergabeparameter entsprechend bereitstellt und ggf. eine implizite Typkonvertierung durchführt (dito Rückgabewert). Die Angabe der Variablennamen können bei der Deklaration entfallen
* Die fehlende Angabe der Übergabeparameter bedeutet in C (nicht C++), dass diese Funktion mit beliebigen Übergabeparametern aufgerufen werden kann und der Compiler keine Typprüfung durchführt:
    int func {...} //kann wie folgt aufgerufen werden func; func(4); func("hallo Welt",4.7);
Öffnen im Compiler Explorer
    → unbedingt vermeiden
    Hat die Funktion keinen Übergabewert, so wird dies in C mit void gekennzeichnet
* In C++ bedeutet die Schreibweise ohne Datentypbeschreibung, dass keine Parameter erwartet werden
* In der Parameterliste von Funktionen dient in der GNU-C Spezifikation das Semikolon zum Trennen zwischen Deklaration von Übergabeparameter und den Übergabeparametern. Alle Übergabewerte links vom Semikolon sind (Forward)deklarationen:
    void test1(int a; int b,int a) { // Forward; Parameter printf("a=%d b=%d\n",a,b); } //Aufruf test1(3,7); //a=7 b=3 //Sinnvoll im Umgang mit Variable Length Arrays void test2(int dim; const char str[dim], int dim); //Aufruf test2("hallo",6);
Öffnen im Compiler Explorer

### Return-Anweisung

Mit der Return-Anweisung wird eine Funktion (vorzeitig) beendet. Der optionale Parameter dient der Angabe des Rückgabewertes der Funktion an den Aufrufer. Der Datentyp des Parameters ergibt sich aus dem Rückgabedatentyp der Funktion.
Syntax: return expression$_{OPT}$
Der Rückgabewert expresion sagt aus, dass hier nicht nur Konstanten oder Variablen stehen können, sondern beliebige Ausdrücke erlaubt sind. Der resultierende Datentyp des Ausdruckes muss letztendlich dem Rückgabedatentyp der Funktion entsprechen:
Hinweise:
* Bei nicht void-Funktionen ist eine return-Anweisung mit einer Expression entsprechend dem Datentyp der Funktion zwingend notwendig:
    int func(void) { return 5.1; //Datentyp wird zuvor int konvertiert } double a=func; //int wird in double konvertiert
Öffnen im Compiler Explorer
* Bei einer void-Funktion ist die return-Anweisung ohne Expression optional. Hier entspricht der schließende Block der Return-Anweisung
    void func(char *str) { if(str==NULL) return; printf("%s",str); }
Sollen mehr als ein Rückgabewert an den Aufrufer zurückgegeben werden so empfiehlt sich:
* die Rückgabewerte in einer Struktur zu packen (siehe Datentypen:Struktur/Verbundtyp)
* die Rückgabewerte über die Aufrufparameter als Call-by-Reference zu übergeben (siehe Zeiger:Zeiger bei Funktionsaufrufen)

#### Rückgabewert aus Datentypsicht

Aus Datentypsicht kann der Funktionsname inkl. den Parameter(n) in einem Ausdruck mit dem Datentyp des Rückgabewertes der Funktion ausgetauscht werden:
Daraus ergibt sich, dass bei Arrays/Strukturen/Unions direkt auf einzelne Komponenten zugegriffen werden kann, ohne diese zuvor einer Variablen zuzuweisen:
Öffnen im Compiler Explorer

#### Fehlerrückgabe

Viele Funktionen können unter gewissen Umständen nicht korrekt ausgeführt werden. Z.B. aufgrund dessen, dass:
* der benötigter Speicher vom Heap nicht bereitgestellt werden kann
* Übergabeparameter außerhalb des zulässigen Bereiches liegen
* angeforderte Ressource (Datei, Geräte, …) nicht vorhanden/verfügbar sind
Solche Fälle gilt es dem Aufrufer mitzuteilen, da in diesen Fällen die vom Aufrufer geforderte Funktionalität nicht erfüllt ist und der Aufrufer ggf. nicht weiterarbeiten kann. Zur Mitteilung des Fehlers sollte vorrangig der Rückgabewerte genutzt werden. (auch wenn in C++ mit Exception eine Alternative hierzu bereitsteht)
Öffnen im Compiler Explorer
Die Standard-C Library-Funktionen geben in diesem Fall zumeist ein -1 (Rückgabewert Ganzzahl) oder einen NULL-Zeiger (Rückgabewert Zeiger) zurück. Der genaue Fehlergrund ist dann ergänzend in der globalen Variablen errno gespeichert. Mittels der Standard-C Funktion strerror kann die Zahl in einen für den Anwender/Programmierer lesbaren String umgewandelt werden (die GNU-C Library Funktion printf bietet ergänzend den Conversion Spezifier %m als alternative Ausgabemöglichkeit an):
Öffnen im Compiler Explorer
Für eine sauber Programmierung sollte diese Methodik generell angewendet werden. Jede Funktion, welche Fehlschlagen kann, teilt dies per Rückgabewert dem Aufrufer mit. Der Rückgabewert einer jeden Funktion wird vom Aufrufer kontrolliert.

### Main-Funktion

Mit Start der Anwendung übergibt das Betriebssystem die Kontrolle an die Anwendung und führt dort die Funktion main aus. Als Übergabe- und Rückgabeparameter sind folgende Varianten erlaubt:
Mit dem Start der Anwendung kann, wie bei jedem Funktionsaufruf, Parameter an den startenden Prozess übergeben werden. Als Parameter können Strings übergeben werden, welche in Form eines Arrays von Strings übergeben werden. Im Parameter argc sind die Anzahl der Strings enthalten. Im Parameter argv[x] sind die einzelnen Strings enthalten. Sofern vorhanden ist im ersten String der Programmname/Aufrufname der Anwendung enthalten:
Öffnen im Compiler Explorer
Hinweis:
* Im Compiler-Explorer können die Parameter im Executor unter 'Execution Arguments', getrennt mit Leerzeichen eingegeben werden.
Wird die Funktion main beendet, entweder über die return Anweisung oder den Aufruf der exit Funktion, wird die Kontrolle zurück an den startenden Prozess (zumeist Shell) gegeben. Über den Rückgabewert wird im Allgemeinen dem Aufrufer der Fehlerstatus mitgeteilt (EXIT_SUCCESS oder 0 bedeutet fehlerfreie , EXIT_FAILURE oder ungleich 0 fehlerhafte Ausführung). Der Start der Anwendung kann als Funktionsaufruf angesehen werden, welchen einen Wert an den Aufrufer zurückliefert.
Hinweise:
* Der Return-Wert der main-Funktion kann mit >>echo $? in der Bash abgefragt werden
* Mittels exit(int status) kann das Programm an jeder Stelle innerhalb des Programms beendet werden. status entspricht dann der expression der return Anweisung von main

### Inline Funktion

Wie der Name inline andeutet, ist die Intention von Inline, dass die aufgerufen Funktion Bestandteil der aufrufenden Funktion ist. Die aufgerufene Funktion soll somit im Kontext des Aufrufers ausgeführt werden, so dass kein eigener Stack bereitgestellt werden muss. Der Start der Funktion kann somit schneller erfolgen:
inline int foo(void) { }
Das Inline erfolgt durch den Compiler (und nicht durch den Linker). Damit das inline funktioniert, muss die zu inlinende Funktion vor dem Aufruf definiert sein, d.h. sie muss in derselben C-Datei enthalten sein [C11 6.7.4].
Hinweis:
* Das 'inlinen' von Funktionen über die eigene Objektdatei hinweg erfolgt in C/C++ über Link Time Optimization (LTO). Hierzu fügt der Compiler den Zwischencode mit in die Objektdatei, so dass der Linker das 'inlinen' vollziehen kann (siehe Interprocedural Optimization)

### Variadic Function / Ellipsis Punctuator

Mit dem Ellipsis Puncturator '…' kann in Funktionsdefinitionen angegebenen werden, dass anstatt der … Anweisung, beliebig viele Parameter/Argumente übergeben werden können. Dies wird unter anderen in der printf Anweisung genutzt:
Die wie folgt aufgerufen werden kann:
Öffnen im Compiler Explorer
Da der Compiler für die optionalen Parameter nicht den Zieldatentyp kennt, erlaubt die C-Spezifikation als Übergabedatentypen hier nur die Grunddatentypen signed int, unsigned int, long, long long, double und Zeiger. Entspricht der übergebene Parameter nicht einen dieser Datentypen, so wird er mittels implizit Cast entsprechend nachfolgender Tabelle gewandelt [C11 6.5.2.2 Default Argument Promotions]:
| Ausgangstyp | Umwandlungstyp |
| char, signed char, short | int |
| unsigned char, unsigned short | unsigned int |
| float | double |
| Alle anderen Datentypen | bleiben erhalten |
Bspw.:
Öffnen im Compiler Explorer
Zum Umgang mit dem variadischen Parameter stellt C die Funktionen va_start, va_arg, va_copy und va_end bereit. Auf Grundlage des dazugehörigen Datentyps va_list können variadische Parameter auch an Unterfunktionen weitergereicht werden (bspw vprintf). Dieses Thema bedingt jedoch genaue Kenntnisse vom Umgang mit dem Stack und ist nicht Bestandteil dieser Vorlesung.

## Variable Length Array (VLA)

Im Normalfall ergibt sich die Anzahl der Elemente eines Arrays aus einer Konstanten, so dass der Compiler zur Compilezeit passend Speicher reserviert (siehe Kap. Array):
Bei Variable Lenght Array wird die Arraydimension nicht über eine Konstante, sondern über eine Variable oder einen Ausdruck angegeben. D.h. die tatsächliche Dimension des Arrays ergibt sich erst zum Ausführungszeitpunkt der Definition:
Ein Variable Length Array kann nur als lokale Variable oder als Übergabeparameter definiert werden (wobei im letzteren Fall kein Speicher hiermit reserviert wird). Eine Initialisierung eines VLA's mit der Definition ist nicht möglich, d.h. nach der Definition beinhaltet das Array 'zufällige' Werte. Es empfiehlt sich daher, diese bspw. mit memset händisch zu initialisieren (siehe auch Kap Array:Initialisierung):
Öffnen im Compiler Explorer
Mittels sizeof kann weiterhin der Speicherbedarf ermittelt werden, wobei sizeof mit einem VLA als Argument kein Konstantenausdruck ist. Stattdessen wird die Größe zur Laufzeit ermittelt:
Öffnen im Compiler Explorer
VLA wurden erstmals in C mit C99 eingeführt. In C11 wurden VLA's als optional eingestuft und mit C23 zurück auf notwendig gesetzt. In C++ sind VLA's nicht definiert, werden jedoch vom GCC Compiler unterstützt. Als alternativen Datentyp wird hier std::array empfohlen.
Hinweis:
* VLA sind nicht direkt in einem Switch-Case Block definierbar. Wird hier ein VLA benötigt, so muss hierzu ein Block erzeugt werden:
    switch(value) { case 1: char arr1[value]; //Compilerfehler, da bei value!=1 kein Speicherplatz //für das VLA reserviert wird break; case 2: { char arr2[value]; //OK } break; }
Öffnen im Compiler Explorer

## Operatoren

In der Programmiersprache C sind folgende Punctuators [C11 6.4.6] definiert:
Die Funktionalitäten sind identisch zu Java. Aufgrund des Datentyps Zeiger gibt es in C ergänzend den Punctuator -> (siehe Kap Datentypen:Struktur/Verbundtyp). Der Präprozessor bedingt die beiden Punctuatoren # und ## (siehe Kap Präprozessor). Die letzten 6 Punctuators <: :> <% %> %: %:%: werden zu [ ] { } # ## ersetzt. Sie sind für Computersysteme gedacht, welche keine ALTGR-Taste besitzen. Java kennt ergänzend den >>> Operatoren, welcher in C über den normalen Schiebebefehl und den zugrundeliegenden Datentyp gesteuert wird.
Wie in Java sind einige Punctuators vom Kontext abhängig und haben damit unterschiedliche Funktionalitäten:
Für den Umgang mit dem Datentyp Zeiger besitzen einige Punctuatoren ergänzende Funktionalitäten (siehe Kap Zeiger):
Punctuatoren, welche von Java abweichende / ergänzende Funktionalitäten haben, werden nachfolgend gesondert beschrieben.
Sind in einer Anweisung mehrere Operatoren enthalten, so hängt die Abarbeitungsreihenfolge von der Rangfolge/Priorität der Operatoren ab (siehe C-Programmierung:Liste der Operatoren nach Priorität). Die Rangfolge ist identisch zu Java, wobei C mehr Operatoren kennt. Generell empfiehlt sich, lieber eine Klammer zu viel als zu wenig zusetzen. Klammern haben die höchste Priorität.

### Zuweisungs-Operator

Die Zuweisung (=-Operator, assignment expression) weist dem L-Value den R-Value zu. Ergänzend dient der L-Value als Rückgabewert des Operators, so dass das Ergebnis der Zuweisung weiter genutzt werden kann. Die Abarbeitungsreihenfolge ist von rechts nach links (R-L Assoziativität). Wird der 'letzte' Rückgabewert nicht verwendet, so wird dieser verworfen (siehe Kap. Discarded Value). Beispiel:
Öffnen im Compiler Explorer
Hinweise:
* Auf beiden Seiten der Zuweisung muss der identische Datentyp stehen. Wenn andernfalls eine implizite Typkonvertierung nicht möglich ist, gibt der Compiler einen Fehler aus:
    int a = 7.3; //implizite Typkonvertierung double b=a+3; //Implizite Typkonvertierung der Intergeraddition nach double float c=(float)a+(float)3; //Explizite Typkonvertierung char *d=a; //Compilerfehler, da Integerwert nicht implizit in einen //Zeiger konvertiert werden kann
Öffnen im Compiler Explorer
* Bei der Parameterübergabe beim Funktionsaufruf wird ebenfalls indirekt der Zuweisungsoperator angewendet. Die Parameter des Aufrufers werden per Zuweisung den lokalen Variablen der aufgerufenen Funktion zugewiesen
    void func(int a, float b) { printf("a=%d b=%f",a,b); } int x=4; float y=5.0; func(x,y); //der Inhalt der Variablen x wird der lokalen Variable a //(a=x) und der Inhalt der Variablen y der lokalen Variablen b //(b=y) zugewiesen. func(y,x); //Dito, mit der Ergänzung, dass eine implizite Typkonvertierung //stattfindet (a=(int)y) und (b=(float)x).
Öffnen im Compiler Explorer

### Komma-Operator

Der Komma-Operator erlaubt es, zwei Ausdrücke auszuführen, wo nur einer erlaubt wäre. Die Ergebnisse aller durch diesen Operator verknüpften Ausdrücke, außer dem letzten werden verworfen. Der letzte Ausdruck dient als Rückgabewert.
Folgendes gilt es bei der Nutzung des Komma-Operators zu beachten:
* Der Komma-Operator wird von links nach rechts ausgewertet
* Das Komma selbst stellt eine Sequence Point dar (siehe Kap Sequence Point)
* An Stellen, wo das Komma zum Syntax gehört (z.B. Trennung der Übergabeparameter bei der Funktionsdefinition und beim Funktionsaufruf, Trennung von Variablen bei der Variablendefinition/-deklaration, Trennung der der enum Konstanten) muss der Ausdruck ergänzend geklammert werden, wenn der Komma-Operator gefordert ist
* Der Komma-Operator hat von allen Operatoren die niedrigste Priorität/Rangfolge, so dass dieser Operator als letztes 'ausgewertet' wird
Beispiel:
Öffnen im Compiler Explorer
'Sinnvolle' Anwendungsbeispiele:
* Bei For-Schleifen, so dass mehrere Variablen initialisiert und mehrere Variablen mit jedem Schleifendurchlauf geändert werden können:
    int sum,lauf; int arr={5,4,3,2,1}; for(sum=100,lauf=5 ; lauf ; lauf--,sum+=arr[lauf]);
Öffnen im Compiler Explorer
* Bei der Return-Anweisung, bei welcher zur Darstellung der Zusammengehörigkeit mehrere Aktionen ausgeführt werden sollen:
    return errno=EINVAL,-1; return printf("Fehler"),-1;
Öffnen im Compiler Explorer
* An Stellen, wo nur eine Anweisung erlaubt ist (und man zu faul ist, einen Block zu öffnen)
    if(…) y=2,z=3;
Öffnen im Compiler Explorer
Hinweis:
* In vielen Ländern wird das Komma zur Darstellung von Gleitkommazahlen genutzt. Wird versehentlicher Weise dies in C/C++ übernommen, so führt dies zu einem unerwarteten Verhalten:
    double var; var=5.3; //Variable wird mit 5.3 initialisiert var=5,3; //KO, da Komma Bestandteil der Defintion ist var=(5,3); //Variable wird mit 3 initialisiert
Öffnen im Compiler Explorer

### ?-Operator / Conditional-Operator

Operator mit 3 Operanden, welcher in Abhängigkeit des ersten Operanden den zweiten oder dritten Operanden zurückgibt.
Syntax: Logical-OR-expression ? expression : condition-expression
Entspricht der if-Anweisung, wobei der ? Operator als solches einen Wert zurückgibt! Expression wird nur ausgewertet, wenn der erste Operand true ist. Andernfalls wird nur condition-expression ausgeführt. Der zurückgegebene Datentyp ergibt sich aus der Expression resp. Condition-expression. Beide sollten somit vom identischen Datentyp sein.
Folgendes gilt es bei der Nutzung des ?-Operators zu beachten
* Der Datentyp Expression und condition-Expression muss identisch sein
Öffnen im Compiler Explorer
* Nach der GNU-C Reference kann der mittlere Operand fehlen, in diesem Fall wird stattdessen das Ergebnis des linken Operanden dort eingesetzt (siehe https://gcc.gnu.org/onlinedocs/gcc/Conditionals.html#Conditionals)
Öffnen im Compiler Explorer
* Sollen innerhalb von condition-expression mehrere (durch Komma getrennte) Anweisungen enthalten sein, so sind diese zu Klammern! Andernfalls bedingt die niedrige Priorität des Komma-Operators ein anders Verhalten, als erwartet.
Öffnen im Compiler Explorer
Die Nutzung des ?-Operators empfiehlt sich an diversen Stellen:
* Bei Makros
    #define MAX(a,b) (a>b?a:b)
Öffnen im Compiler Explorer
* Beim Fehlerhandling
    char *str=strstr("hallo123","lox"); //Ausgabe von str nur, wenn Suche erfolgreich (str!=NULL) war printf("%s",str!=NULL?str : "(not found)" );
Öffnen im Compiler Explorer
* Prüfen, ob eine Funktion erfolgreich ausgeführt wurde, bevor eine weitere Funktion ausgeführt wird
Öffnen im Compiler Explorer

### Logische Verknüpfungen

Alle logischen Operationen nutzen als Operanden Ganzzahlen ,Gleitkommazahlen oder Zeiger. Der Operand wird als false angesehen, wenn der Operand 0, 0.0 oder NULL ist. Andernfalls als true. Das Ergebnis ist immer vom Datentyp int und hat den Wert von 0 oder 1.
Syntax: Expression && Expression Syntax: Expression || Expression Syntax: ! Expression
Bei der logischen UND und ODER Verknüpfung wird zuerst der linke Ausdruck ausgewertet (Operator ist ein Sequence Point). Der rechte Ausdruck wird nur ausgeführt, wenn:
* bei der logischen UND Verknüpfung der linke Ausdruck TRUE ergab
* bei der logische OR Verknüpfung der linke Ausdruck FALSE ergab
Andernfalls steht das Ergebnis schon mit der Auswertung des linken Operanden fest. In der theoretischen Informatik wird dies 'Non-strict-evaluation' genannt, bei welcher der Rückgabewert feststeht, bevor alle Ausdrücke bearbeitet worden sind.
Beispiel für das Verhalten des Ausbleibens der Auswertung des zweiten Operators:
Öffnen im Compiler Explorer
Diese Funktionalität ist insbesondere beim Umgang mit Fehlern vorteilhaft:
* Prüfen, ob eine Variable einen gültigen Wert hat, bevor sie weiterverwendet wird
Öffnen im Compiler Explorer
* Prüfen, ob eine Funktion erfolgreich ausgeführt wurde, bevor eine weitere Funktion aufgerufen wird:
Öffnen im Compiler Explorer

### Bitweise Verknüpfungen

Alle bitweisen Operationen nutzen als Operanden Ganzzahlen. Bei vorzeichenbehafteten Zahlen ist das Ergebnis nach [C11 6.5] Implementierungsabhängig, wobei das Vorzeichenbit zumeist als eigenständiges Bit angesehen wird und bei UND/ODER/EXOR als normales BIT betrachtet wird.
Syntax: Ganzzahl & Ganzzahl → Bitweise UND Operation Syntax: Ganzzahl | Ganzzahl → Bitweise ODER Operation Syntax: Ganzzahl ^ Ganzzahl → Bitweise EXOR Operation Syntax: ~ Ganzzahl → Bitweise Negierung Syntax: Ganzzahl >> additive-expression Syntax: Ganzzahl << additive-expression
Mittels des Schiebeoperators können Ganzzahlen um additive-expression bitweise nach Links << oder Rechts >> verschoben werden. Der resultierende Datentyp ergibt sich aus dem linken Operanden. Der rechte Operand muss positiv und kleiner gleich der Bitbreite des linken Operanden sein. Andernfalls ist das Ergebnis implementierungsspezifisch.
Nach [C11 6.5.7] ist das Ergebnis der Schiebeoperation implementierungsspezifisch, wenn der linke Operand eine negative vorzeichenbehaftete Zahl ist. In der Regel wendet der Compiler jedoch folgende Schiebeoperation an:
* Logisches Schieben (zu füllende Bit-Stellen werden mit 0 aufgefüllt), wenn der linke Operand eine vorzeichenlose Zahl ist
* Arithmetisches Schieben (zu füllende Bit-Stelle beim Rechtsschieben wird mit dem Vorzeichenbit aufgefüllt), wenn der linke Operand eine vorzeichenbehaftete Zahl ist. Hiermit wird das Vorzeichen beibehalten, so dass eine negative Zahl nach dem Schieben weiterhin negativ ist (In Java erfolgt dies durch den >>> - Operator)
Hinweis:
* Ein Schieben um eine Stelle nach rechts entspricht einer Division durch 2
* Ein Schieben um eine Stelle nach links entspricht einer Multiplikation mit 2

### sizeof-Operator

Der sizeof-Operator dient zur Ermittlung des Speicherbedarfs (in Bytes) einer Variablen/eines Datentyps.
Syntax: sizeof (type-name) Syntax: sizeof unary-expression
Als Argument für den Sizeof-Operator kann wahlweise ein Datentyp, eine Variable oder ein Ausdruck genutzt werden. Sizeof gilt als Konstantenausdruck, d.h. seine Berechnung erfolgt durch den Compiler, sodass:
* sizeof zur Initialisierung von globalen Variablen genutzt werden
    struct xyz{int x,y,z;}; size_t size=sizeof(struct xyz);
Öffnen im Compiler Explorer
* bei einem Ausdruck dieser nicht ausgeführt wird, sondern nur der resultierende Datentyp bestimmt wird und dessen Größe zurückgegeben
    short var=4711; size_t size=sizeof var++; //var=4711, size=2
Öffnen im Compiler Explorer
Der resultierende Datentyp des Rückgabewertes ist size_t.
Es empfiehlt sich, den sizeof-Operator mit Klammern zu nutzen, andernfalls wird im Falle eines Ausdruckes der Speicherbedarf des linken Operanden angegeben:
Öffnen im Compiler Explorer
Hinweise:
* sizeof angewendet auf char gibt immer den Wert 1 zurück.
* sizeof ist nicht auf Operanden vom Typ Funktion, void, Felder ohne Größenangabe und Bitfelder anwendbar
* wird sizeof auf ein VLA angewendet, so entspricht sizeof nicht mehr einen Konstantenausdruck, sondern einem Funktionsaufruf
    void foo(int n) { int arr[n]; size_t size=sizeof(arr); //Speicherbedarf ist kein Konstantenausdruck //sondern wird zur Laufzeit berechnet
* Sizeof entspricht nicht strlen, da sizeof den für die Variable reservierten Speicherbereich zurückgibt und nicht wie strlen den Inhalt des Speicher hinsichtlich des Stringendezeichens auswertet.
    char string[100]="Test"; printf("%zu\n",sizeof(string)); //100 printf("%zu\n",strlen(string)); //4
Öffnen im Compiler Explorer

## Anweisungen
### If-Anweisung

Syntax: if (expression) statement Syntax: if (expression) statement else statement
Das Ergebnis des Ausdruckes (Ganzzahl, Gleitkommazahl oder Zeiger) wird auf ungleich 0, 0.0 oder NULL getestet. Trifft dies zu, so wird statement ausgeführt. Andernfalls wird das else statement ausgeführt, sofern vorhanden. Sollen mehrere Anweisungen ausgeführt werden, so sind diese mittels Block zusammenzufassen (diesen dann bitte nicht mit ; abschließen).
Beispiel:
Öffnen im Compiler Explorer
Hinweis:
* Bei der Anweisung if(a==7) finden aus syntaktischer Sicht zwei Aktionen statt. Zunächst wird geprüft, ob a gleich 7 ist. Dieser Vergleich liefert true(=1) oder false(=0) zurück. Die If-Anweisung prüft im Anschluss, ob das Argument true oder false ist.

### For-Anweisung

Syntax: for(Initialisierungsteil$_{OPT}$ ; Test$_{OPT}$ ; Fortsetzung$_{OPT}$) Statement
Die For-Anweisung wird vom Compiler wie folgt umgesetzt:
Initialisierungsteil$_{OPT}$  Goto Next Goon:  //Continue-Anweisung entspricht goto Continue  //Break-Anweisung entspricht goto Break;  Statement Continue:  Fortsetzung$_{OPT}$ Next:  if(Test$_{OPT}$) goto Goon Break:
Öffnen im Compiler Explorer
Die einzelne Elemente haben folgende Bedeutung/Besonderheiten:

#### Initialisierungsteil

    Zum Initialisieren von Schleifenvariablen, wird einmalig mit Beginn der FOR-Anweisung ausgeführt.
   * kann Leer sein, dann erfolgt keine Initialisierung
   * Ab C99: Hier kann eine lokale Variable definiert werden, welche nur innerhalb dieser For-Anweisung gültig ist

#### Testteil

    Hierin wird vor jedem Schleifendurchlauf geprüft, ob die Schleifenbedingung erfüllt ist. Wenn Bedingung nicht erfüllt ist (Ganzzahl, Gleitkommazahl oder Zeiger gleich 0,0.0 oder NULL), wird die For-Schleife beendet.
   * Kann leer sein, dann wird dieser Teil als true angesehen

#### Fortsetzungsteil

    Wird nach jeden Schleifendurchlauf ausgeführt. In der Regel sollte hier die Schleifenvariable aktualisiert werden
   * Kann leer sein

#### Statement

    In der Schleife auszuführender Ausdruck
   * Sollen mehrere Ausdrücke ausgeführt werden, so sind diese in einem Block zusammen zu fassen.
    Ergänzende Anweisungen innerhalb des Statements/Block
   * 'continue' zum Fortsetzen des Schleifendurchlaufes, d.h. Sprung zum Fortsetzungs-Teil
   * 'break', zum Beenden des Schleifendurchlaufes
Beispiele:
* For-Schleife mit lokaler Schleifenvariable
    char string="hallo"; for(size_t lauf=0; lauf < strlen(string); lauf++) //Schleife ist Suboptimal, da in jedem Durchlauf die Funktion //strlen aufgerufen wird!
Öffnen im Compiler Explorer
* Schleife mit Break-Anweisung
    char dest[5]; char src[10]="HalloWelt"; for(size_t lauf=0; src[lauf]!=0; lauf++) { if(lauf >= sizeof(dest)) break; dest[lauf]=src[lauf]; }
Öffnen im Compiler Explorer
* Schleife, bei welcher die gesamte Aktion innerhalb der For-Anweisung ausgeführt wird
    char dest[5]; char src[10]="HalloWelt"; for(size_t l=0; src[l]!=0 && l <sizeof(dest); printf("l=%zu\n",l),dest[l]=src[l],l++);
Öffnen im Compiler Explorer
* Schleifenvariable muss keine Zählvariable sein
    int zahl=0b100010; int bits=0; for(int mask=0b10000000; mask!=0; mask>>=1 ) bits+= zahl & mask ? 1 : 0;
Öffnen im Compiler Explorer
* Zeiger als Schleifenvariable
    char src="hallo"; char dst[strlen(src)+1]; char *s,*d; for(s=src,d=dst; *s; ) *d++=*s++; *d=0;
Öffnen im Compiler Explorer
Hinweis:
* Zur Geschwindigkeitsoptimierung sollten im Test- und Fortsetzungsteil keine Funktionsaufrufe mit Werten enthalten sein, die sich innerhalb der Schleife nicht ändern:
    for(size_t lauf=0;lauf<strlen("hallo");lauf++)
Öffnen im Compiler Explorer
* Zur Geschwindigkeitssteigerung sollte im Testteil auf 0 abgefragt werden, d.h. die Schleifenvariable dekrementiert werden
    for(int lauf=700000;lauf;lauf--)
Öffnen im Compiler Explorer
* Alternativ zu einer Laufvariablen empfiehlt sich die Nutzung von Pointer als Schleifenvariable;
    for(char *ptr=str; *ptr; ptr++);

### While-Anweisung

Syntax: while(expression) Statement
Syntax: do Statement while(expression);
Die While-Anweisungen werden vom Compiler wie folgt umgesetzt:
| while-Anweisung | do while-Anweisung |
| Continue:  if(!expression)  goto Break  //Break-Anweisung entspricht  // goto Break;  //Continue-Anweisung entspr.  // goto Continue;  Statement  goto Continue Break: | GoOn:  //Break-Anweisung entspr.  // goto Break  //Continue-Anweisung ent.  // goto Continue  Statement Continue:  if(expression)  goto GoOn Break: |
Öffnen im Compiler Explorer
Folgendes gilt es bei der Nutzung der While-Anweisung zu beachten:
* Soll mehr als ein Statement innerhalb der While-Schleife ausgeführt werden, so sind diese in einem Block zu schreiben
* Statement kann eine Leeranweisung (gekennzeichnet durch ein Semikolon) sein
    while(1); do ; while(1);
Öffnen im Compiler Explorer
* Bei der do-While-Schleife ist das abschließende Semikolon Bestandteil des Befehls
* expression muss eine Ganzzahl, Gleitkommazahl oder Zeiger zurückgeben, der bei False (0, 0.0 oder NULL) die Schleife beendet.
Beispiele:
* Endlosschleife
    while(1)
* Schleife zum Durchlaufen eines String
    char str="string"; char *ptr=str; while(*ptr++); size_t len=ptr-str-1;
Öffnen im Compiler Explorer

### Switch-Anweisung

Syntax: switch(expression) statement Syntax: { case constant-expression: statementopt default: statementopt}
Die Switch-Anweisung entspricht einer verschachtelten If-Anweisung, mit der Besonderheit, dass der auszuführende Code nicht im If-Zweig stattfindet, sondern dort ein goto enthalten ist:
| Switch-Anweisung | Compilerumsetzung |
| switch(a) { case 1: int a=7; //Code break; case 2: printf("%d",a); case 3: { int a; //Code } case 4 ... 8: default: //Code } | if(a==1)  goto label1; else if(a==2)  goto label2; else  goto label_default; //------------------- label1:  //Code  goto label_break; label2:  //Code label_default:  goto label_break; label_break: |
Folgendes gilt es bei der Nutzung der Switch-Anweisung zu beachten:
* Expression muss zu einer Ganzzahl ausgewertet werden (int, short, char, long, long long)
* Constant-expression muss eine Integer-Konstante. Sie darf nicht doppelt im Block vorkommen. Der genaue Datentyp ergibt sich aus dem Datentyp der Expression
* Ohne den folgenden Block kann nur ein case oder default Fall enthalten sein
    int var=1; switch(var) case 1: printf("hallo");
Öffnen im Compiler Explorer
* Die geschweifte Klammer der Switch-Anweisung erzeugt einen Block, so dass hierin definierte Variablen innerhalb des gesamten Block zur Verfügung stehen (sind also nicht CASE abhängig). Die CASE Anweisungen sind nichts anderes als Sprungmarken, so dass der Initialisierungsteil der lokalen Variablen übersprungen / nicht ausgeführt werden kann:
    switch(a) { int a=3; //Variable in gesamten Block gültig case 1: int b=3; //Variabe in gesamten Block gültig printf("%d %d",a,b); //a undefiniert, b=3 break; case 2: printf("%d %d",a,b); //a und b undefiniert break; case 3: { //Block, so dass Variable int a=4711; //nur in diesem CASE gültig ist break; } }
Öffnen im Compiler Explorer
* GNU-C (siehe Switch-Statement) erlaubt ergänzend zur Angabe einer Konstante die Angabe eines Wertebereiches.
    switch(a) { case 1 ... 8: //Leerzeichen zwischen den Konstanten zwingend notwendig //andernfalls wird der Punkt zur Konstante zugehörig //interpretiert (erzeugt dann eine Gleitpunktkonstante)
Öffnen im Compiler Explorer
* Break ist optional. Wenn break weggelassen wird, geben Compiler ggf. eine Warning aus. Mittels der GCC-Compiler-Anweisung __attribute__((fallthrough)); kann diese Warning unterdrückt werden.
    switch(a) { case 0: //ggf. Compiler-Warning __attribute__((fallthrough)); case 1: case 2: break; }
* Case - Anweisungen sind nichts anderes als Sprungmarken (Labels), so dass CASE-Anweisungen überall stehen können:
    int prime(int value) {return value+1;} void hallo(int var) { switch(var) { default: if(prime(var)) case 2: case 4: printf("%d",var); else { case 3: case 6: printf("%x",var); } case 7: printf("xyz"); } }
Öffnen im Compiler Explorer
* Variable Length Arrays sind als Variablen im Block der Switch Anweisung nicht erlaubt. Ist dies notwendig, so sind diese in einen inneren Block anzulegen
    switch(var) { case 1: int arr[var]; //KO break; case 2: { int arr[var]; //OK }
Öffnen im Compiler Explorer
Hinweis:
* Eine Switch-Case Anweisung über Strings sind in C/C++ nicht möglich:
    switch(str) { case "hallo": return 1; case "du da": return 2; case "xyz": return 3; default: rReturn -1; }
    Ein direkte Lösungsansatz ist die Nutzung von strcmp zum Vergleichen von String. Da strcmp zeichenweise den String vergleicht, ist dieser Ansatz langsam. Alternativ bietet sich an, von allen String einen Hash-Wert zu berechnen und zunächst diesen Hash-Wert zu kontrollieren. Da der Hash-Wert nicht eindeutig ist, muss ergänzend ein strcmp erfolgen:
    char *str="hallo"; switch(HASH_LAUFZEIT(str)) { case HASH_COMPILEZEIT('d','u'): if(!strcmp(str,"du")) printf("Case \"du\":\n"); break; case HASH_COMPILEZEIT('h','a','l','l','o'): if(!strcmp(str,"hallo")) printf("Case \"hallo\"\n"); else if(!strcmp(str,"xyz")) printf("String='xyz'\n"); break; default: return -1; }
Öffnen im Compiler Explorer
    Siehe:Hash Table in C

## Label + Goto

Syntax: name:
Labels dienen zur Kennzeichnung einer Anweisung, so dass die Programmausführung an dieser Stelle fortgesetzt werden kann, resp. mit einer GOTO-Anweisung angesprungen werden kann. Ein Label ist durch den folgenden Doppelpunkt gekennzeichnet. Dem Label muss eine Anweisung folgen, welches im Falle einer folgenden Variablendefinition durch eine Leeranweisung (Semikolon) erzeugt werden kann.
Folgendes gilt es bei der Nutzung von Labels zu beachten:
* Ein Label entspricht der Speicheradresse des der Label-Anweisung folgenden Anweisung
* Bei nicht genutzten Labels (mit goto nicht angesprungene Labels) gibt der Compiler eine Warning aus. Diese kann beim GCC-Compiler mit __attribute__((unused)) unterdrückt werden
* die Case- und die Default-Anweisung aus der Switch-Anweisung entspricht einem Label
Syntax: goto label;
Mit der Goto-Anweisung wird die Programmausführung an der mit label gekennzeichneten Stelle fortgesetzt.
Folgendes gilt es bei der Nutzung von goto zu beachten:
* Mit einer Goto Anweisung kann an eine beliebige Steller innerhalb der identischen Funktion gesprungen werden. Es kann nicht zu einem Label einer anderen Funktion gesprungen werden (dies erfolgt über setjmp und longjmp)
* Mit der Goto Anweisung kann gleichermaßen in einen Block hinein, als auch herausgesprungen werden:
    goto first_time; for(;;) { if(a>10) goto last_time: first_time: a++; } last_time: ;
Öffnen im Compiler Explorer
* Wird mit der Goto Anweisung der Initialisierungsteil einer Variablen übersprungen, so ist zwar die Variable existent, beinhaltet aber nicht ihren Initialisierungswert
Das Label hat eine funktionsweite Sichtbarkeit (siehe Kap Gültigkeit/Sichtbarkeit von Variablen). Dies bedeutet, dass das:
* Labels nur innerhalb einer Funktion gültig ist
* Labels einen von Variablen/Funktionen/Datentypen unabhängigen Namensraum haben
* Kein Prototyp für ein Label notwendig ist, wenn es bei Nutzung über goto noch nicht gesetzt worden ist, also nach vorne gesprungen wird
Beispiel:
Öffnen im Compiler Explorer
Hinweis:
* In Java gibt es das Schlüsselwort goto, welches aber ohne Funktion ist. Es gibt jedoch Labels, die mit "break label" angesprungen werden können.
Prinzipiell sind Goto-Anweisungen zu vermeiden. In einigen Anwendungsfällen erweist sich goto als nützlich:
* Konstruktor mit mehreren Initialisierungsteilen
    int main(int argc,char *argv) { enum {INIT_2,INIT_1,INIT_START} fortschritt=INIT_START; int *ptr1; int *ptr2; if(!(ptr1=malloc(10))) goto end; fortschritt=INIT_1; if(!(ptr2=malloc(20))) goto end; fortschritt=INIT_2; // ... end: switch(fortschritt) { case INIT_2: free(ptr2); __attribute__((fallthrough)); case INIT_1: free(ptr1); __attribute__((fallthrough)); case INIT_START: } return (int)fortschritt; }
Öffnen im Compiler Explorer
* Abbruch einer verschachtelten Schleife
    int arr[10][10]; size_t zeile; size_t spalte; for(zeile=0;zeile<10;zeile++) for(spalte=0;spalte<0;spalte++) if(arr[zeile][spalte]==0) goto label_end; label_end: if((zeile==0) && (spalte==0)) printf("Nichts gefunden"); else printf("Gefunden an %zu %zu",zeile,spalte);
Öffnen im Compiler Explorer

## Sonstiges
### ??-Trigraphs

Zeichenfolge bestehend aus 3 Zeichen beginnend mit 2 Fragezeichen. Die beiden Fragezeichen entsprechen einem ESCAPE-Operator (siehe Kap Char-Konstanten), welcher das nachfolgende Zeichen durch ein anderes ASCII Zeichen ersetzt. Die Ersetzung findet auch innerhalb von Strings statt. Trigraphs wurden früher genutzt, als die ALTGR Taste noch nicht üblich war.
Bei neuer C Varianten muss zur Nutzung von Trigraphs der Compiler mittels -trigraphs aktiviert werden.

# 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 und false entsprechen den Integerwerten 0 und 1
* Der Aufzählungsdatentyp und seine Aufzählungselemente sind vom Datentyp 'int'
* Bis einschl. C90 wurde bei der Definition von Variablen ohne Angabe des Datentyps, bei Definition einer Funktion ohne Angabe des Datentyps der Übergabeparameter, des Rückgabedatentyps der Datentyp implizit auf int gesetzt (implizit int)
    static var2=2; foo(par1,par2){ auto var1; var1=par1+par2; return var1; }
Öffnen im Compiler Explorer

## Ganzzahl Datentypen

Grundlegend kennt die Programmiersprache C nur zwei Ganzzahldatentypen:
* Datentyp: int
    Vorzeichenbehafteter (signed) Datentyp, dessen Bitbreite von der Rechnerarchitektur und des Compilers abhängig ist
* Datentyp char
    Datentyp mit einer Mindestbreite von 8-Bit u.A. zur Speicherung von Zeichen typischerweise im ASCII Format. Die C-Spezifikation [C11 6.2.5 Types] lässt es dem Compiler frei, ob char als vorzeichenbehafteter oder vorzeichenloser Datentyp implementiert wird.
    Ergänzend kann der Datentyp char auch zur Speicherung von ganzen Zahlen genutzt werden. In diesem Fall empfiehlt sich, diese explizit als signed und unsiged zu definieren. Wenn nur ASCII Zeichen gespeichert werden, so ist diese Angabe nicht nötig!
Optional können Qualifier dem Datentyp vorangestellt werden:
* unsigned / signed zur Angabe, ob der Inhalt als vorzeichenlose Zahl oder als vorzeichenbehaftete Zahl zu interpretieren ist. Ohne Angabe sind Variablen vom Datentyp int vorzeichenbehaftet
* short / long / long long (letzteres seit C99) als vorangestellten Qualifier zu int zur Vergrößerung/Verkleinerung der Bitbreite. Bei Angabe des Qualifiers ist das Schlüsselwort int optional. Die Beschreibung long long int und long long sind gleichbedeutend!
Als Bitbreite für die Datentypen ist im C-Standard nur ein Mindestbreite definiert. Die genaue Bitbreite hängt von der Rechenbreite des Systems und vom Compiler ab (siehe [C11 6.2.5 Types]):
| Datentype | Mindest breite | Typisch bei 32-Bit Architektur | Typisch bei 64-Bit Architektur | printf Format Anweisung |
| char | 8 | 8 | 8 | %c |
| signed char unsigned char | 8 | 8 | 8 | %hhd %hhu |
| short short int | 16 | 16 | 16 | %hd / %hu |
| int | 16 | 16/32 | 32 | %d / %u |
| long long int | 32 | 32 | 32/64 | %ld / %lu |
| long long long long int | 64 | 64 | 64 | %lld / %llu |
In der Header-Datei limits.h sind die tatsächlichen Grenzwerte der Datentypen gespeichert:
| Konstante | Beschreibung | Typ. bei 64-Bit Architektur |
| CHAR_BIT | Anzahl der Bits in einem Char | 8 |
| SCHAR_MIN | min. Wert, den der Typ bei signed char aufnehmen kann | -128 |
| SCHAR_MAX | max. Wert, den der Typ bei signed char aufnehmen kann | 127 |
| UCHAR_MAX | max. Wert, den der Typ bei unsigned char aufnehmen kann | 255 |
| CHAR_MIN | min. Wert, den der Typ char aufnehmen kann | 0 oder SCHAR_MIN |
| CHAR_MAX | max. Wert, den der Typ char aufnehmen kann | SCHAR_MAX oder UCHAR_MAX |
| SHRT_MIN | min. Wert, den der Typ short int annehmen kann | -32.768 |
| SHRT_MAX | max. Wert, den der Typ short int annehmen kann | 32.767 |
| USHRT_MAX | max. Wert, den der Typ unsigned short int annehmen kann | 65.535 |
| INT_MIN | min. Wert, den der Typ int annehmen kann | -2.147.483.648 |
| INT_MAX | max. Wert, den der Typ int annehmen kann | 2.147.483.647 |
| UINT_MAX | max. Wert, den der Typ unsigned int aufnehmen kann | 4.294.967.296 |
| LONG_MIN | min. Wert, den der Typ long int annehmen kann | -2.147.483.648 oder -9.223.372.036.854.775.808 |
| LONG_MAX | max. Wert, den der Typ long int annehmen kann | 2.147.483.647 oder 9.223.372.036.854.775.807 |
| ULONG_MAX | max. Wert, den der Typ unsigned long int annehmen kann | 4.294.967.296 oder 18.446.744.073.709.551.616 |
| LLONG_MIN | min. Wert, den der Typ long long int annehmen kann | -9.223.372.036.854.775.808 |
| LLONG_MAX | max. Wert, den der Typ long long int annehmen kann | 9.223.372.036.854.775.807 |
| ULLONG_MAX | max. Wert, den der Typ unsigned long long int annehmen kann | 18.446.744.073.709.551.615 |
Aufgrund dessen, dass die Breite der Datentypen von der Rechnerarchitektur und vom Compiler abhängig ist, sind in der Header-Datei stdint.h Aliase für Datentypen enthalten, welche in ihrem Alias die tatsächliche Breite und das Vorzeichens beinhalten:
Für C++ sehen diese Datentypen wie folgt aus:
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

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

### 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
Öffnen im Compiler Explorer
Die genaue Darstellung (Anzahl der Bits für Mantisse und Exponent, Darstellung von NAN/INF, ….) ist in C nicht spezifiziert und somit compiler- und rechnerabhängig. Zumeist wird mittlerweile die IEEE 754 Standardisierung genutzt (Norm wurde von Intel mit der Entwicklung der 8087 FPU entworfen).
Grunddatentypen und der Wertebereich (nach IEEE 754)
| Daten- type | Speicher- platz | Exponent | Mantisse | Größte Zahl | Kleinste Zahl | Genauigkeit | printf Format Anweisung |
| float | 4 Byte | 8 bit | 23 bit | +-3,4*10$^{38}$ | 1,2*10$^{-38}$ | 6 Stellen | --- |
| double | 8 Byte | 11 bit | 52 bit | +-1,7*10$^{308}$ | 2,3*10$^{-308}$ | 12 Stellen | %f %e %g |
| long double | 10 Byte | ≥8 bit | ≥ 63 bit | +-1,1*10$^{4932}$ | 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!
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:
Öffnen im Compiler Explorer
Die Funktionen basieren auf den Datentyp double, d.h. sowohl der Übergabewert als auch der Rückgabewert ist vom Datentyp double. Die Prototypen sehen wie folgt aus:
Über den Suffix f oder l im Funktionsnamen kann der zugrundeliegende Datentyp auf float (Suffix f) oder long double (Suffix l) geändert werden:
Ergänzend zu den Prototypen sind die gebräuchlichen Naturkonstanten in math.h definiert (siehe auch: math.h):
Gleitkommazahlen können nicht nur Zahlenwerte, sondern auch Sonderwerte annehmen. Diese Sonderwerte sind ebenfalls in math.h beschrieben (siehe auch: math.h):
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

Nach dem Wikipedia Artikel komplexe Zahlen stellen komplexe Zahlen eine Erweiterung der reellen Zahlen dar. Ziel der Erweiterung ist es, algebraische Gleichungen wie x$^2$+1=0 bzw. x$^2$=-1 lösbar zu machen. ... Da die Quadrate aller reellen Zahlen größer oder gleich 0 sind, kann die Lösung der Gleichung x$^2$=-1 keine reelle Zahl sein. Man braucht eine ganz neue Zahl, die man üblicherweise i nennt, mit der Eigenschaft i$^2$=-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:
Öffnen im Compiler Explorer
Normale Gleitkommavariablen werden als reellen Teil einer komplexen Zahl angesehen. Bei Operationen von Gleitkommazahlen und komplexen Zahlen wird der imaginäre Teil der Gleitkommazahl auf 0 gesetzt. Beim Zuweisen einer komplexen Zahl an eine Gleitkommazahl wird nur der reele Teil 'gelesen'. Soll eine Gleitkommavariablen zu einem imaginären Teil gewandelt werden, so muss diese bspw mit der Konstanten 1.0i multipliziert werden:
Öffnen im Compiler Explorer
Die Standard-C-Library bietet diverse mathematische Funktionen wie csin, ccos, csqrt, cpow und ergänzend Umrechnungsfunktionen von Gleitkommazahlen in komplexe Zahlen (und umgedreht) wie creal,cimag ,cabs für den Umgang mit komplexen Zahlen an:
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.
Öffnen im Compiler Explorer
Die Funktionen beruhen auf den Datentyp double _Complex, d.h. sowohl der Übergabewert als auch der Rückgabewert ist double _Complex resp. double (bei den Umrechnungsfunktionen). Über den Suffix f und l im Funktionsnamen kann der zugrundeliegende Datentyp geändert werden:
Hinweis:
* Der Datentyp _Complex und die dazugehörigen mathematischen Funktionen sind in der C-Spezifikation als optional gekennzeichnet.

## Boolschescher Datentyp

Siehe Grundlagen: Boolescher-Datentype/Operatoren

## void / unvollständiger Datentype

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

## Datentypkonvertierung

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:
Öffnen im Compiler Explorer
Bei identischer Priorität ergibt sich die Abarbeitungsreihenfolge aus der Assoziativität (L-R oder R-L)
Öffnen im Compiler Explorer
Für das Rechnen/Vergleichen müssen beide Operatoren vom identischen Datentyp sein! Sind diese nicht identisch, so müssen die Datentypen 'angeglichen' werden. Dies kann einerseits ‚automatisch‘ mittels 'implizierter' Typumwandlung oder ‚manuell‘ mittels ‚expliziter' Typumwandlung erfolgen. Beim Zuweisungsoperator (inkl. Parameterzuweisung bei Funktionsaufrufen und Funktionsrückgabewerte) gilt dies ebenso, nur dass hier der Quelldatentyp an den Zieldatentyp angepasst werden muss. Die impliziten Regeln finden hier keine Anwendung.
Hinweis:
* Empfehlenswert ist, die Datentypen der Variablen so zu wählen, dass der Compiler keine implizite Typumwandlung tätigt. Kann dies nicht vermieden werden, so sollte die explizite Typumwandlung genutzt werden (um sich einen möglichen Datenverlust bewusst zu machen).
* Der Datentyp selbst sollte den möglichen Wertebereich der Variablen entsprechen und nicht unnötig groß gewählt werden.
    Datentyp zur Speicherung der Stundenzeit: Wertebereich: 0 ... 23 -> unsigned char Datentyp zur Speicherung einer Jahreszahl: Wertebereich: 2000 v.C. ... 4000 n.C -> signed short Datentyp zur Speicherung einer Temperatur: Wertebreich: -273,0°C ... 2000,0°C -> float

### Implizite Typumwandlung

Diese Regeln wurden so aufgestellt, dass dabei stets ein Datentyp in einen anderen Datentyp mit höherem Rang umgewandelt wird (Rangordnung: long double, double, float, long long, long, int) [C11 6.3.1.8] .
Nach [Harbison: S. 199] :
| Regel/ Priorität | If either operand has Type | And the other operand has Type | Converts both to |
| 1 | long double | any real type | long double |
| 2 | double | any real type | double |
| 3 | float | any real type | float |
| 4 | any unsigned type | any unsigned type | The unsigned type with the greater rank |
| 5 | any signed type | any signed type | The signed type with the greater rank |
| 6 | any unsigned type | a signed type of greater rank that can represent all vaues of the unsigned type | The signed type |
| 7 | any unsigned type | a signed type of greater rank that cannot represent all values ot the unsigned type | The unsigned version the the signed type |
| 8 | any other type | any other type | No conversion |
Ergänzend gilt das Regelwerk zu Integer Promotion [C11 6.3.1.1], welche Datentypen kleiner als signed int zu int und kleiner als unsigned int und unsigned int konvertiert (Compiler kann hiervon abweichen, wenn sichergestellt ist, dass kein Datenverlust eintritt).
Regel 6 und 7 sind der nicht klar definierten Bitbreite der ganzzahligen Datentypen geschuldet und versuchen, Konvertierungsverluste zu vermeiden. Sie lesen sich zunächst kryptisch, lassen sich aber einfach am folgenden Beispiel erklären:
* Im Fall, dass der Datentyp long 64-Bit breit ist, kann dieser problemlos die vorzeichenlose 32-Bit Zahl ohne Konvertierungsverluste repräsentieren, so dass der Zieltype signed long ist (Regel 6)
* Im Fall, dass der Datentyp long 32-Bit breit ist, kann dieser mit seinen Wertebereich von -2.147.483.648 bis +2.147.483.647 nicht den vorzeichenlosen Zahlenbereich von 0…4.294.967.295 darstellen. In diesem Fall hat der unsigned Datentyp einen höheren Rang, so dass als Zieltyp unsigend long gewählt wird(Regel 7)
Regel 7 bedeutet die Gefahr eines Konvertierungsverlustes, dessen man sich bewusst sein sollte! Diese tritt insb. dann in Kraft, wenn ein Operand vom Typ 'unsigned long long' ist (64-Bit).
Für die Konvertierung von Pointer, Arrays, Strukturen, Unions zu anderen Datentypen, als sich selbst gilt Regel 8. In diesem Fall gibt der Compiler zumeist einen Error, in wenigen Ausnahmefällen eine Warning aus:
Öffnen im Compiler Explorer
Beispiele von impliziten Typumwandlungen:
Öffnen im Compiler Explorer
Hinweis:
* Im CompilerExplorer können sie sich die impliziten Typumwandlungen anzeigen lassen. Dazu gehen sie bitte wie folgt vor:
   * Im Source-Fenster mit '+Add new' ein Compiler Fenster öffnen
   * Im Compiler-Fenster mit '+Add new' ein 'GCC Tree/RTL' Fenster öffnen
   * Im GCC Tree/RTL-Fenster unter 'Select a pass…' 'original tree' auswählen
Um implizite Typumwandlung besser erkennen zu können, wird oftmals bei Variablennamen die 'Ungarische Notation' angewendet (siehe Ungarische Notation). Aufgrund der besonderen Namensgebung kann der Programmierer ohne großen Aufwand frühzeitig mögliche Typkonflikte erkennen. Der Variablenname setzt sich wie folgt zusammen:
    Präfix: 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:

### Explizite Typumwandlung

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:
Öffnen im Compiler Explorer
In der Rangfolge der Operatoren steht der Cast-Operator unterhalb von bspw. Funktionsaufrufen, Arrayzugriffen aber auch der Dereferenzierung:
| Priorität | Symbol | Assoziativität | Bedeutung |
| 15 | ... | ... | ... |
| 14 | ++/-- (Präfix) +/- (Vorzeichen) !/~ & * (TYP) ... | R-L | Präfix-Inkrement/Dekrement Vorzeichen Logisches/Bitweises Nicht Adresse Zeigerdereferenzierung Typumwandlung (Explizites Cast) ... |
| 13 | / % | L-R | Multiplikation/Division/Modulo |
| 12 | ... | ... | ... |
Im Zweifel gilt auch hier, den zu wandelnden Typ ergänzend zu klammern.
Hinweis:
* Bedenke, dass die explizite Typumwandlung so aufgestellt sein sollte, dass der Zieldatentyp dem notwendigen Datentyp entspricht. Andernfalls wendet der Compiler ergänzend eine implizierte Typumwandlung an:
    int a; double b=4.7; short c1= (long)( (float)a+b ); //b ist vom Typ double, so dass a nach dem Cast auf float // implizit vom Compiler auf double gecastet wird //c ist vom Typ short, so dass das Ergebnis der Addtion nach dem expliziten //Cast auf long auf short gecastet wird. //Nach Anwendung der impliziten Cast sieht der Ausdruck wie folgt aus: short c2=(short)(long)( (double)(float)a+4.7);
Öffnen im Compiler Explorer
Mittels expliziter Typumwandlung können Ganzzahl nach Gleitkommazahlen (und andersherum) und Zeiger in einen anderen Zeiger und auf andere Datentypen gewandelt werden. Eine Konvertierung von Arrays, Strukturen, Unions zu anderen Datentypen als sich selbst ist weiterhin nicht möglich:
Öffnen im Compiler Explorer
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

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 Strukturname$_{Opt}$ {Datentype Strukturelementname; … }$_{OPT}$ Variablenliste$_{Opt}$;
Ein Verbund entspricht einer Java Klasse mit dem Unterschied zu C (nicht C++) , dass ein Verbund keine Methoden beinhaltet und keine Zugriffsbeschränkung für die Attribute gesetzt werden können.
Der Syntax erlaubt es, gleichermaßen einen Datentyp (über Strukturname) zu definieren und Variablen von diesem Datentyp (über Variablenliste) anzulegen. Da beide Elemente optional sind, ergeben sich diverse Kombinationsmöglichkeiten:
* Nur Strukturname → Definition eines neuen Datentyps
    struct xyz1 {int x; int y,z;}; //Definition eines neuen Datentyps struct xyz1 var1; //Definition einer Variablen dieses //Datentyps var1.x=7; //Zugriff auf ein Strukturelement
Öffnen im Compiler Explorer
    C: Der Strukturname ist nur mit dem vorangestellten struct gültig. Der Strukturname stellt somit einen eigenen Namensraum, getrennt von dem Namensraum der Variablen/Funktionen, dar. Daher kann ein Strukturname identisch zu einem Variablennamen sein:
    struct xyz2 {int x,y,z;}; //Definition des Datentyps 'struct xyz2' struct xyz2 xyz2; //Definition der Variablen 'xyz2' auf //Basis des Datentyps 'struct xyz2' xyz2 var2; //Compilerfehler, zur Nutzung des Datentyps //'struct xyz2' muss struct vorangestellt //werden!
Öffnen im Compiler Explorer
    C++: Der Strukturname ist sowohl mit als auch ohne dem vorangestellten struct gültig. Auch hier stellt der Strukturname einen eigenen Namensraum dar, der jedoch eine niedrigere Priorität als der der Variablen hat:
    struct xyz3 {int x,y,z;}; xyz3 xyz3={1,1,1}; //'xyz3' beschreibt hier den Datentypen xyz3.x=1; //und nach Definition einer Variable die Variable! xyz3 var2; //Fehler, xyz beschreibt hier die Variable
Öffnen im Compiler Explorer
* Nur Variablenliste → Definition einer/mehrerer Variablen von einem 'unnamed' Datentyp
    struct { int x,y,z; } xyz,arr[3],*ptr=&xyz; //Definition von Variablen des Datentyps //struct {...}. Da dieser Datentyp nicht benannt //wurde, kann im späteren keine weitere Variable //von diesem Datentyp angelegt werden. xyz.x =8; //Nutzung der Variablen arr[1].y =9; ptr->x = arr[1].y;
Öffnen im Compiler Explorer
    Angewendet wird die Schreibweise gerne, wenn eine Struktur innerhalb einer Struktur definiert wird und die innere Struktur zur besseren Strukturierung dient:
    struct aussen { //Äußere Struktur struct { //Innere Struktur int a,b,c; } anwendung1; struct { //Innere Struktur int x,y,z; } anwendung2; }; struct aussen var; var.anwendung1.a=10; //Nutzung der Variablen var.anwendung2.y=12;
Öffnen im Compiler Explorer
* Strukturname und Variablenliste → Definition eines Datentyps und Definition von Variablen. D.h. es können im Nachhinein weitere Variablen von diesem Datentyp angelegt und die hier angelegten Variablen genutzt werden.
    struct xyz{ //Definition eines Datentyps int x,y,z; } var1,*ptr1; //und Definition von Variablen dieses Datentyps struct xyz var2,*ptr2; //Definition weiterer Variablen dieses Datentyps var1.x=var2.y; //Nutzung der Variablen
Öffnen im Compiler Explorer
* Kein Strukturname und keine Variablenliste → "Anonymous Struct", d.h. Definition eines 'unnamed' Datentyps, von der ergänzend keine Variable angelegt wird. Anonymus Structs sind nur innerhalb von Strukturen erlaubt/einsetzbar. Hier dienen sie der besseren Strukturierung und ersparen dem Programmierer die Benennung des normalerweise notwendigen Strukturelementes:
    struct außen { int a; struct { int b,c,d; } anwendung1; //'Normale' innere Struktur struct { int x,y,z; }; //Anonyme Struct als innere Struktur } var; var.a=4711; //Nutzung der Variablen var.anwendung1.b=1;//Bei Zugriff auf anonyme Struktur ist die Benennung //des Strukturelementes notwendig var.x=1; //'Einfacherer' Zugriff auf inneres Element bei //'anonymous struct'
Öffnen im Compiler Explorer
Hinweise:
* 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

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:
Öffnen im Compiler Explorer

### Zugriff auf Strukturelemente

Die Programmiersprache C unterscheidet zwei 'Zugriffsarten' auf die Strukturelemente, abhängig vom zugrundeliegenden Datentyp:
* Handelt es sich beim zugrundeliegenden Datentyp um eine (struct)Variablen, so erfolgt der Zugriff über den Punkt-Operator .:
    struct { int a,b,c; } abc; abc.a=4711; //abc ist eine Variable einer Struktur abc.b=abc.c;
Öffnen im Compiler Explorer
* Handelt es sich beim zugrundeliegenden Datentyp um einen Zeiger auf eine (struct)Variable, so erfolgt der Zugriff über den Zeiger-Operator ->:
    struct xyz { int x,y,z; }; struct xyz var1; //Variable von Datentyp struct xyz; struct xyz *ptr1; //Zeiger auf eine Struktur vom Datentyp struct xyz ptr1=&var1; //Initialisierung des Zeigers //(mit der Adresse der Variablen var1) var1.x= 7; //Zugriff auf das Strukturelement x über die Variable var1 ptr1->x=7; //Zugriff auf das Strukturelement x über den Zeiger ptr1 (*ptr1).x=7; //Der Zeiger-Operator entspricht dem Zugriff auf ein //dereferenziertes Strukturelement
Öffnen im Compiler Explorer
Werden innerhalb von Strukturen weitere Strukturen und Zeiger auf Strukturen angelegt, so müssen diese Regeln auf jedes innere Strukturelement einzeln angewendet werden:
Öffnen im Compiler Explorer

### Initialisierung von Strukturen

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:
Öffnen im Compiler Explorer
Eine Initialisierung einer Struktur über nachfolgende Art bewirkt etwas anderes, als erwartet:
Öffnen im Compiler Explorer
Innerhalb der Initialisierungsliste wird über 'var.b=12' und 'var.c=3' die zu diesem Zeitpunkt noch nicht initialisierte Variable var die Elemente b und c initialisiert. Mit dem Werten der Initialisierungsliste (hier {12,3}) wird nachfolgend die Variable erneut initialisiert und 12 dem Strukturelement a und 3 dem Strukturelement b zugewiesen. Abhängig vom Compiler ist das Strukturelement c entweder 3 oder 0.
Bei verschachtelten Strukturen muss entsprechend obiger Aussage für jede innere Struktur eine eigene Initialisierungsliste erstellt werden. Die inneren Initialisierungslisten können entfallen, was jedoch nicht empfohlen wird und ergänzend vom Compiler bei '-Wall' als Warning angemerkt wird:
Öffnen im Compiler Explorer

### Zuweisen/Kopieren von Strukturen

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!
Die Zuweisung als 'ganzes' gilt auch bei der Parameterübergabe und -rückgabe von Funktionen:
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:
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

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:
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:
Öffnen im Compiler Explorer
Anwendung:
* Struktur, welche auf sich selbst verweist (Verkettete Liste)
    struct vl { //Entspricht gleichermaßen einem Prototyp, so dass //innerhalb dieser Struktur dieser Datentyp zum //Anlegen eines Strukturelementes genutzt werden kann struct vl *next; //Zeiger auf das nächste Element char daten[100]; };
Öffnen im Compiler Explorer
* Entsprechend der objektorientierten Programmierung, wenn alle Attribute der Klasse private sind:
    class.h struct class; //Prototyp für Struktur //so dass Nutzer dieser //Struktur ein Zeiger auf diese anlegen, //aber nicht auf die Strukturelemente //zugreifen können. //Prototyp der public Methoden void konstruktor(struct class ** me); main.cclass.c #include "class.h" int main(int argc, char*argv) { struct class *obj1; konstruktor(&obj1); .. //Kein Zugriff auf //Strukturelemente möglich return 0; } #include "class.h" struct class { int attr1; int attr2; }; void konstruktor(struct class ** me) { struct class *this; this=(struct class *) malloc(sizeof(struct class)); this->attr1=10; *me=this; }
Öffnen im Compiler Explorer

### Vergleichen von Strukturen

Ein Vergleich von Strukturen über den direkten Weg ist nicht möglich. Vielmehr müssen die Strukturelemente händisch verglichen werden:
Öffnen im Compiler Explorer
Hinweis
* Bei Rechnerarchitekturen mit nicht ausgerichteter Speicherausrichtung (Alignment) kann ergänzend ein Vergleich über memcmp erfolgen

### Speicherplatzbedarf einer Struktur

Für jedes Strukturelement wird Speicher entsprechend der Größe des Datentyps reserviert. Der Speicherplatzbedarf der Gesamtstruktur ergibt sich aus der Summe der Strukturelemente:
Öffnen im Compiler Explorer
Die Ermittlung der tatsächlichen Speichergröße einer Struktur erfolgt über den sizeof-Operator:
Öffnen im Compiler Explorer
Hinweis:
* Bei Rechnerarchitekturen mit ausgerichteter Speicherausrichtung (Alignment) (siehe Speicherausrichtung) kann der tatsächliche Speicherbedarf größer sein! Hier fügt der Compiler ggf. zwischen den Strukturelemente Füllbytes (Padding Bytes) ein
* Die Reihenfolge der Strukturelemente wird vom Compiler nicht geändert. D.h. die Zuordnung der Speicherstellen zu den Strukturelementen erfolgt in der Definitionsreihenfolge

### Interne Organisation

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:
Öffnen im Compiler Explorer
Der Offset eines Strukturelementes kann mit dem offsetof-Operator ermittelt werden:
Syntax: offsetof(type,Strukturelement)
Wie beim sizeof-Operator gilt der offsetof-Operator als Konstantenausdruck und der Rückgabedatentyp ist size_t. Zur Nutzung des Operators muss die Header-Datei 'stddef.h' inkludiert werden:
Öffnen im Compiler Explorer

### Explizites Cast

Eine Struktur kann nicht in einen anderen Datentyp und damit auch nicht in einen anderen Strukturdatentyp mit identischen Strukturelementen konvertiert werden:
Öffnen im Compiler Explorer

### Incomplete Array

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:
Öffnen im Compiler Explorer
Die Speicherplatzreservierung für das incomplete Array kann auf zwei verschiedene Arten erfolgen:
* Speicherplatzreservierung für das Incomplete Array über Variableninitialisierung (nur GCC-C und nur im Falle einer globalen Variablen)
    struct vl ele1 = { .next = NULL, .data = {32, 31, 30} //Durch Initialisierung der einzelnen Array Elemente }; //Vorsicht, sizeof(ele) gibt dennoch nur 4/8 zurück struct vl ele2 = { .next = NULL, .data[3-1] = 0 //Durch Initialisierung des letzten Array Elementes }; //Vorsicht, sizeof(ele) gibt dennoch nur 4/8 zurück
Öffnen im Compiler Explorer
* Speicherplatzreservierung für das Incomplete Array über malloc
    struct vl *b=malloc(sizeof(struct vl)+ //Größe der Struktur sizeof(char[3] )); //Größe des Arrays b->data[1]='7'; //OK, da mit dem Anlegen von b Speicher für 2 //data Element reserviert wurde
Öffnen im Compiler Explorer
Das Incomplete Array bietet sich überall dort an, wo Daten gespeichert werden sollen, deren Größe sich erst zur Laufzeit ergibt.

### Anwendung

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

Der wesentliche Syntax von Strukturen wurde in C++ übernommen. D.h.:
* das abschließende Semikolon
* der Zugriff auf die Strukturelemente
* Speicherplatzreservierung
* die Definition von Variablen mit der Definition des Datentyps
Ergänzend wurde in C++ der Datentyp class eingeführt, der weitestgehend identisch zu struct ist. Geändert/Hinzugefügt wurden folgende Sachverhalte:
* Classen/Strukturen können Methoden haben
* Operatoren (Zuweisungsoperator, Addition, ...) können überladen werden
* Alle Strukturelemente (und Methoden) einer struct sind per default public
* Alle Attribute (und Methoden) einer Class sind per default private
* Zur Nutzung des Datentyps ist das führende struct nicht notwendig (dies bedingt dann auch, dass der Strukturdatentyp kein separater Namensraum ist)
    struct xyz { int x,y,z; }; xyz var_xyz;
* Initialisierungswerte für Strukturelemente und Klassenattribute bei der Datentypbeschreibung angegeben werden können:
    struct xyz { int x=3; int y=1; int z; }; xyz var={7}; //var.x=7 var.y=1 var.z=0
Öffnen im Compiler Explorer
* In C++ kann eine Struktur ebenfalls über 'designated initializers' initialisiert werden. Jedoch muss die Reihenfolge der Initialisierungselement identisch zur Datentypdefinition sein. Auch ist keine Doppeltbenennung erlaubt:
    struct xyz { int x,y,z; }; xyz var1={.x=1, .y=2, .z=3}; //OK, Reihenfolge identisch xyz var2={.y=1, .z=3}; //OK, Reihenfolge identisch xyz var3={.y=1, .x=0}; //KO, Reihenfolge nicht identisch xyz var4={.x=1, .x=2}; //KO, keine Doppeltbenennung möglich
Öffnen im Compiler Explorer

## Union

Ä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 Unionname$_{Opt}$ {Datentyp Variablenname; …}$_{Opt}$ Variablenliste$_{Opt}$;
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

Die Größe der Union ergibt sich aus dem größtem Unionelement. Alle Unionelemente überlappen sich, so dass der Offset zum Basiselement 0 ist:
Öffnen im Compiler Explorer
Wird ein kleineres Unionelement belegt und im Anschluss ein größeres Unionelement gelesen, so ist der Inhalt der durch das kleinere Element nicht beschriebenen Speicherstellen undefiniert:
Öffnen im Compiler Explorer

### Initialisierung von Union

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:
Öffnen im Compiler Explorer

### Anwendung

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:
Öffnen im Compiler Explorer
* Ein weiterer Anwendungsfall ergibt sich, wenn in einer Struktur unterschiedliche Datensätze gespeichert werden sollen (siehe Tagged Union):
    struct set { char *index; char *value; }; struct get { char *index; char *value; }; struct cli{ //Enum zur Darstellung des aktiven Unionelementes hilfreich enum {SETTER,GETTER} tag; union { //Interpretation abhängig von tag struct set set; struct get get; } ; }; struct cli var={.tag=SETTER, .set.index="hallo"}; if(var.tag==SETTER) printf("Set: %s",var.set.index); else printf("Get: %s",var.get.index);
Öffnen im Compiler Explorer
* Mittels eines Tagged Union kann komfortabel ein Binärbaum realisiert werden:
    //Quelle: https://github.com/Hirrolot/datatype99 struct BinaryTree; //Prototyp struct BinaryTreeNode { struct BinaryTree *lhs; int x; struct BinaryTree *rhs; } BinaryTreeNode_t; struct BinaryTree { enum { Leaf, Node } tag; union { int leaf; struct BinaryTreeNode node; } data; }; int sum(const struct BinaryTree *tree) { switch (tree->tag) { case Leaf: return tree->data.leaf; case Node: return sum(tree->data.node.lhs) + tree->data.node.x + sum(tree->data.node.rhs); } return -1; // Invalid input (no such variant). }
Öffnen im Compiler Explorer

### C++

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

## Enum/Aufzählungstyp

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 enumname$_{Opt}$ {definition-list[=expression]}$_{Opt}$ Variablenliste$_{Opt}$
Die Anwendung ist identisch wie bei Strukturen, so dass nachfolgend nur die Abweichungen zu den Strukturen beschrieben werden.

### Datentyp von enum

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

### Enumelemente

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:
Öffnen im Compiler Explorer
Der erste Enumelement der Definitionsliste bekommt den Wert 0 zugewiesen. Folgeelemente bekommen den Wert des Vorgängerelementes +1 zugewiesen. Enumelemente können mit einer Konstanten (und Konstantenausdruck) initialisiert werden:
Öffnen im Compiler Explorer
Hinweis:
* Es empfiehlt sich, die Enumelemente in GROSSBUCHSTABEN zu schreiben. Hiermit wird gekennzeichnet, dass es sich um Konstanten und nicht um Variablen/Funktionen handelt

### Anwendung

Die Nutzung des Datentyps enum empfiehlt sich überall dort, wo eine Fallunterscheidung notwendig ist:
* Statusrückgabe von Funktionen:
    enum STATUS {OK,MEMORY_OVERFLOW, DIVISION_BY_ZERO}; enum STATUS funct(…) { return MEMORY_OVERFLOW; }
* Beschreibung der Zustände und der Ereignisse eines Zustandsautomates:
    enum EREIGNIS {EREIGNIS1,EREIGNIS2,EREIGNIS3}; void zustandsautomat(enum EREIGNIS ereignis) { static enum {IDLE,OPERAND,OPERATOR,} zustand=IDLE; switch(zustand) { case IDLE: case OPERAND: //Compiler meckert bei fehlendem Case über OPERATOR } }
Öffnen im Compiler Explorer

### C++

Ergänzend zu den Abweichungen bei Structs sind hier weitere Unterscheidungsmerkmale vorhanden:
* Enums stellen einen eigenen Datentyp dar, der nicht mit int kompatible ist. Das bedingt unter anderem, dass einige Operatoren wie z.B. ++ nicht mehr auf Variablen des Datentyps enum angewendet werden. Die Enumelemente als solches sind jedoch kompatibel zum Ganzzahldatentyp:
    enum Color {RED,GREN,BLUE}; enum Color var1=RED; var1=4; //KO var1=var1+1; //KO var1++; //KO int var2=RED; //OK if(RED==2) //OK
Öffnen im Compiler Explorer
* Auch enums können Methoden besitzen.
* Über Operatorüberladung können z.B. nicht vorhandene Operatoren wie ++/-- ergänzt werden
* Über unscoped/scoped Enumerations wird der 'Namensraum' gesteuert:
   * Unscoped Enumeration (Elemente der Definitionsliste stehen wie bei C im Zugriff)
       Syntax: enum name$_{Opt}$ : type$_{OPT}$ {enumerator=constexp, …};
       enum Color {RED,GREN,BLUE }; Color r = RED;
Öffnen im Compiler Explorer
   * Scoped enumeration (Elemente der Definitionsliste haben einen eigenen 'Namensraum')
       Syntax: enum class|struct name$_{Opt}$ : type$_{OPT}$ {enumerator=constexp, …};
       enum class Color {RED,GREN=20,BLUE }; Color r = Color::BLUE;
Öffnen im Compiler Explorer

## Bitfelder

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:
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:
Öffnen im Compiler Explorer

### Anwendung

* Komprimierte Speicherung der Uhrzeit (wie dies z.B. in Realtimeclocks erfolgt)
    union { struct { //Datentyp zum 'Bitweisen' Zugriff unsigned int hour:5; unsigned int minute:6; unsigned int second:6; }; unsigned int hex; //Datentyp zum Zugriff aller Elemente } myTime; myTime.hour = 13; myTime.minute = 37; myTime.second = 59; printf("Zeit: %02d:%02d:%02d\n", myTime.hour,myTime.minute,myTime.second ); //Alternativer/Händischer Zugriff auf die einzelnen Bits printf("Es ist jetzt %02d:%02d:%02d Uhr\n",((myTime.hex>> 0)&0b011111), ((myTime.hex>> 5)&0b111111), ((myTime.hex>>11)&0b111111));
Öffnen im Compiler Explorer
* Aufteilung von FloatingPoint Zahlen in seine Komponenten
    union float_mes { float flo; struct ieee { //aus ieee754.h kopiert #if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ unsigned int negative:1; unsigned int exponent:8; unsigned int mantissa:23; #endif /* Big endian. */ #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ unsigned int mantissa:23; unsigned int exponent:8; unsigned int negative:1; #endif /* Little endian. */ } ieee; int hex; }; //aus ieee754.h kopiert #define IEEE754_FLOAT_BIAS 0x7f /* Added to exponent. */ union float_mes test={0.1}; printf("float: %f\n",test.flo); printf("hex: %x\n",test.hex); printf("bin: "); for(unsigned int flag=0x80000000; flag; flag=flag>>1) printf("%c%s",test.hex&flag?'1':'0', flag&0x80800000?":":(flag&1?"\n":"")); printf("bitfield: (%c)%x * 2^(%d-%d)\n",(test.ieee.negative==1?'-':'+'), test.ieee.mantissa, test.ieee.exponent, IEEE754_FLOAT_BIAS);
Öffnen im Compiler Explorer

### C++

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

# Datatype-/Storage Class Specifier
## Typedef Storage Class Specifier

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:
Ö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

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

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

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

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

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.

## Typeof Operator

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):
Ö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

## Register Storage Class Specifier

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

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-ThreadProducer-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 Assemblerprogrammx86 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-ThreadProducer-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 Assemblerprogrammx86 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

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!
Ö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:
Ö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

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):
Ö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:
Ö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 Cconstexpr 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

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:
Ö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

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
Die Speicherplatzreservierung und Initialisierung solcher Variablen erfolgt mit dem Start des Threads.
Beispiel:
Öffnen im Compiler Explorer
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

## Sonstiges

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:
Ö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:
Ö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:
Öffnen im Compiler Explorer

# Zeiger

Programmieren in C/C++: Zeiger

# Array

Programmieren in C/C++: Array

# Präprozessor

Programmieren in C/C++: Präprozessor