Programmieren in C/C++/ Druckversion
Vorwort
BearbeitenWie ist dieses Buch entstanden
BearbeitenDieses 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
BearbeitenDie 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
BearbeitenFehlermeldungen 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 Compilerfehler Umgehung 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; }
- 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 }
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
BearbeitenLesen 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++
BearbeitenDas 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
BearbeitenDie 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
BearbeitenDie 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
Bearbeiten
Literatur
BearbeitenKurze 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
BearbeitenDie 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):
>>man strcpy >>man printf.3
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
BearbeitenDie 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
Bearbeiten1972 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
BearbeitenC-Syntax
BearbeitenJava, 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; }
- 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')])
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
BearbeitenDas 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
- 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
- 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);
- 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
BearbeitenNeben 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
- 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
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
BearbeitenC 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
BearbeitenC 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
int a=7,b=8;
a=a+b+7;
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
BearbeitenDer 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
BearbeitenNeben 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)
Schlecht Gut 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];
Zusammenspiel von C und Betriebssystem
BearbeitenC 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
BearbeitenEin 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 232=4 294 967 295 Karteikarten vorhanden
- Bei einem 16-Bit System sind 216=65 536 Karteikarten vorhanden
- Bei einem 64-Bit System sind 264=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
Bearbeiten
Toolchain
BearbeitenDie 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
BearbeitenC-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
BearbeitenFü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
BearbeitenNeben 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
BearbeitenEine 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
BearbeitenDie 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
BearbeitenMit 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
BearbeitenIm 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.
>>make #startet Make, welches die Datei makefile einliest und ausführt
>>make -f xyz #startet make, welches die Datei xyz einliest und ausführt
Mit dem Aufruf vom Make können Ziele(Targets) definiert werden, welche behandelt werden sollen. Typische Ziele sind:
>>make all #Prüft das gesamte Projekt auf Änderungen und erstellt die Executable
>>make clean #Löscht alle Zwischenergebnisse (also alle Objekt-Dateien)
Entwicklungsumgebung
BearbeitenEin 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
BearbeitenIn 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
BearbeitenLinux
BearbeitenIm 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:
>>sudo apt install gcc
>>sudo apt install make
Die Erstellung des Source-Codes kann mit einem beliebigen Editor (z.B. gedit, nano) erfolgen.
Windows 10/11 64-Bit Nutzer
BearbeitenSowohl 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
BearbeitenmacOS 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
BearbeitenBeim GCC-Compiler übernimmt der Aufruf von gcc gleichermaßen den Compiler- und den Linker-Aufruf. Eine Übersetzung wird wie folgt gestartet:
>>gcc datei1.c datei2.c datei3.c …
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:
>>gcc datei1.c datei2.c datei3.c -o prog
Der Aufruf kann beliebig um Compiler-Parameter ergänzt werden:
>>gcc datei1.c datei2.c datei3.c -o prog -fsanitize=address -Wall
Nach fehlerfreien Compilerdurchlauf wird die Executeable wie folgt gestartet:
>>./a.out #Bei Aufruf von gcc ohne -o
>>./prog #Bei Aufruf von gcc mit -o prog
Wichtige Terminal/bash-Befehle: Siehe auch: Ubuntuusers/Befehlsübersicht
>>cd xxx #Change Directory, zum Wechseln des Arbeitsverzeichnisses
>>pwd #zur Ausgabe des aktuellen Verzeichnisses
>>ls #zur Auflistung des Inhaltes des aktuellen Verzeichnisses
>>ls -la #zur tabellarischen Auflistung des Inhaltes des aktuellen Verzeichnisses
>>ll #in vielen Installationen ist dies ein Alias auf 'ls -la'
>>cp xxx yyyy #zum Kopieren einer Datei 'xxx' in die neue Datei 'yyy'
>>mv xxx yyy #zum Verschieben/Umbenennen von Dateien
>>mkdir xxx #zum Anlegen eines Unterverzeichnisses
>>rm xxx #zum Löschen einer Datei
>>rmdir xxx #zum Löschen eines Unterverzeichnisses (dieses muss jedoch leer sein)
>>cat xxx #Eigentlich ein Befehl zum Zusammenfügen mehrerer Dateien.
#Da ohne explizite Angabe einer Zieldatei die Ausgabe auf der
#Standardausgabe erfolgt, kann mit diesem Befehl der Inhalt
#einer Datei angezeigt werden.
>>less xxx #scrollfähige Anzeige einer Datei (Beenden mit Taste 'q')
>>more xxx #Wie less, aber ohne die Fähigkeit, rückwärts zu scrollen
>>vi main.c #ein einfacher, aber universeller Texteditor
>>nano main.c #ein etwas komfortabler Texteditor
Nutzung des Online-Compilers Compiler Explorer
BearbeitenDer 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.
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
BearbeitenEin 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:
int lauf = 4711;
int var = 2;
int *ptr=&var;
*--ptr=13;
printf("%d\n",lauf); //Erwartet 4711, Ausgabe 13
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
BearbeitenDie 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
BearbeitenDer 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
BearbeitenKommentare 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:
/* Blockkommentar mit zwei öffnenden Klammern /*
über mehrere Zeilen, nicht verschachtelt */
//Zeilenkommentar
/* Kleiner Bereich zum auskommentieren */
#if 0
Großer Bereich zum auskommentieren
#endif
Namenskonventionen
BearbeitenFolgende 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
BearbeitenMit 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:
char str1[]="Strings müssen am Ende abgeschlossen sein
so dass dies ein Fehler ist";
char str2[]="Dies\
ist ein Test"; //Vorsicht, führende Leerzeichen vor 'ist'
//bleiben erhalten!
/*mehrzeiliges Makro*/
#define MAX(a,b) (a>b?\
b: \
a)
//Dies ist ein Zeilenkommentar \
welcher in dieser Zeile fortgesetzt wird
/\
* dies ist ein Blockkommentar*\
/
//hinter der Zeilenfortsetzung darf nur ein CR/LF folgen
#define MAX2(a,b) \ //so dass hier kein Kommentar folgen darf
a>b?a:b
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
BearbeitenVorrangig 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
BearbeitenEine 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
BearbeitenErfolgt 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++;
}
|
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++;
}
|
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);
}
|
Der Linker meldet beim Zusammenfügen der Objekt-Dateien, dass die Variable a bereits woanders definiert sei (multiple definition of `a';)!
Beispiel 2:
#include <stdio.h> //fuer printf() size_t stderr
int printf=7;
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
BearbeitenErfolgt 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);
}
|
Sichtbarkeit von Funktionsparameter in Protypen
BearbeitenSoll 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:
int a; //a im File Scope
int func(int a, int b); //a und b im Function Prototyp scope
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
BearbeitenZum 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
BearbeitenFü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)
BearbeitenDer 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):
void foo(void) { //foo() wird vor dem ersten Aufruf definiert
//Hier kein Zugriff auf var möglich!
//Hier kein Zugriff auf Datentyp uchar möglich!
}
int var; //var wird vor dem ersten Aufruf definiert
typedef unsigned char uchar; //Definition des Datentyps uchar
int main(void) {
foo(); //da zuvor definiert kann diese Funktion hier aufgerufen werden
var++; //dito
}
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) {
}
|
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
- 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; }
- 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.c Datei1.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
- 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
BearbeitenDie 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:
char var1; //Speicherplatzreservierung von 1 Byte
//Inhalt der Speicherstelle wird als Zahl interpretiert
//(Hinweis: ASCII-Zeichen sind Zahlen)
double var2=47.11; //Speicherplatzreservierung von 8 Byte
//Inhalt der Speicherstellen wird als Gleitkommazahl
//interpretiert (bestehend aus je einer Ganzzahl für
//Mantisse und Exponent)
short arr[10]; //Speicherplatzreservierung von 10*2 Byte
//Inhalt der 10 Speicherstellen werden als Ganzzahl
//interpretiert
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:
int vari=47;
vari+12; //(int) + (int) → Ganzzahladdition
float varf=12.3;
3.1415F+varf; //(float) + (float) → Gleitkommaaddition
int *ptr=&vari;
ptr+3; //(int *) + (int) → Zeigeraddition
int arr[3];
arr+3 //(int (*)) + (int) → Nicht definiert
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.:
int vari1=47;
int vari2=11;
vari1 + 12 + vari2; //((int) + (int))→(int) + (int)
40 < vari1 < 50; //((int) < (int))→(int) < (int)
Wie Operatoren haben auch Funktionen Operanden (in Form von Übergabeparameter). Das Ergebnis eines Funktionsaufrufes wird ebenfalls in Form eines Datentyps zurückgegeben:
double add(int par1,par2) {return par1+par2;}
int vari1=47;
int vari2=11;
vari1 + add(var2,12); //(int) + ((int),(int))→(int)
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
BearbeitenKenngröß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 232=4.294.967.295=4Gi Bytes/Speicherstellen adressiert werden
- Bei einem 16-Bit System können 216=65.536=64Ki Bytes/Speicherstellen adressiert werden
- Bei einem 64-Bit System können 264=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
BearbeitenIntention 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
BearbeitenGanzzahlen 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
- 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
BearbeitenFü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
BearbeitenDie maximale Größe von Arrays/Strukturen/Unions ist in C/C++ von der zugrundeliegenden Rechnerarchitektur abhängig:
char a[100000]; //Speicherbedarf=100.000*1Byte=100.000 Bytes
//Bei 16-Bit Architektur KO
//Ab 32-Bit Architektur OK
struct size_16 {
char a,b,c,d,e,f,g,h,
i,j,k,l,m,n,o,p;
};
struct size_256 {
struct size_16 a,b,c,d,e,f,g,h,
i,j,k,l,m,n,o,p;
};
struct size_4096 {
struct size_256 a,b,c,d,e,f,g,h,
i,j,k,l,m,n,o,p;
};
struct size_65536 {
struct size_4096 a,b,c,d,e,f,g,h,
i,j,k,l,m,n,o,p;
};
struct size_65536 var65536; //Speicherbedarf=65536 Bytes
//Bei 16-Architektur KO
//Ab 32-Architektur OK
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
BearbeitenEine 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
BearbeitenDie 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:
//Prototypen
size_t strlen(char *str); //Die max. Stringlänge ist durch den
//adressierbaren Speicher vorgegeben
void *malloc(size_t size);
void *memcpy(void *dest, const void *src, size_t n);
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:
typedef unsigned long long size_t; //Bei einer 64-Bit Rechnerarchitektur
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);
Boolescher-Datentype/Operatoren
BearbeitenUrsprü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:
int a=3;
double b=0.0;
int *ptr=&a;
while(1);
if(a) {}
for( ; b && ptr ; );
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:
int a=3;
int b= (a==3); //b=1
In den if()/for()/while() Anweisungen ist folglich kein explizites Konvertieren eines Ganzzahl-/Gleitkomma-/Zeigerwertes mittels des Vergleichs-Operators in einen Booleschen Wert notwendig:
int a;
double b;
if(a) //entspricht if(a!=0)
if(!b) //entspricht if(b==0.0)
while(1) //entspricht while(True)
for(int lauf=1; lauf ; lauf--);
Umgedreht kann ein boolescher Wert oder auch das Ergebnis einer booleschen Operation direkt mit einer Ganzzahl- oder Gleitkommazahl verknüpft werden:
int a=3,b=2;
a=a+1 && 7 << 3 & a==3;
if( a==1 & b==2)
if(3 < a < 9) //Vorsicht, die Schreibweise lässt ein
//anderes Verhalten erwarten!
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:
_Bool var1=12;
printf("var1=%d\n",var1); //→ 1
float var2=13.2;
var1=var2;
printf("var1=%d\n",var1); //→ 1
var1--; //Post/Pre Inkrement/Dekrement in C++ nicht auf Datentyp bool! möglich
printf("var1=%s\n",var1?"true(1)":"false(0)"); //→ false
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.
#include <stdbool.h>
bool foo(int par,int min,int max) {
if((par >= min) && (par <= max))
return true;
else
return false;
}
Hinweis:
- In C++ gibt es anstatt des Datentypes
_Bool
den Datentypbool
. Auch sind die beiden Konstantentrue
undfalse
ohne zusätzliches Einbinden der Header-Datei vorhanden. Der weitere Umgang ist jedoch identisch zu C.
Literale/Konstanten
BearbeitenZahlenwerte 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:
int var=17; //0000 0000 0000 0000 0000 0000 0001 0001
short var=0x307;// 0000 0011 0000 0111
char var=017; // 0000 1111
int var=-1; //1111 1111 1111 1111 1111 1111 1111 1111
float var=0.0; //0000 0000 0000 0000 0000 0000 0000 0000
float var=1.0; //0011 1111 1000 0000 0000 0000 0000 0000
Ganzzahl-Konstanten
BearbeitenIm 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:
123 +123 -123 → Formal wird das Minus als Operator angesehen und die positive Zahl 123 negiert 0x123 → Konstante ist in hexadezimaler Schreibweise notiert 0123 → Konstante ist in oktaler Schreibweise notiert 0b011 → Konstante ist in binärer Schreibweise notiert nicht offizieller C-Syntax, wird aber von vielen Compiler unterstützt ab C23-Spezifikation offiziell im C-Syntax enthalten! 6'432'108 → Tausendertrenner Nur in C++ Spezifikation enthalten ab C23-Spezifikation auch im C-Syntax enthalten. 5123123123 → Wertebereich größer als 2^31 so dass automatisch zum nächst größeren Datentyp konvertiert wird
Mit den Suffixen L/l und LL/ll kann der Datentyp long und long long erzwungen werden:
12L → Konstante wird in eine long Integer Konstante konvertiert 4711LL → Konstante wird in eine Long Long Konstante konvertiert
Mit dem Suffix U/u kann die Konstante explizit auf unsigned gesetzt werden:
12U → Konstante wird als vorzeichenlose Zahl gesepeichert 4711ULL → Konstante wird in eine vorzeichenlose Long Long Konstante konvertiert
Char-Konstanten
BearbeitenIn einfachen Anführungsstrichen geschriebene ASCII Zeichen werden entsprechend der ASCII-Tabelle als Zahlenwert (Datentyp int) dargestellt:
'A' → Integerkonstante 65 'a' → Integerkonstante 97 …
Ü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)
ESCAPE Integer- → Bedeutung / (Terminal)funktionalität Sequence konstante '\0' = 0 → 0 Character = Stringendezeichen '\a' = 7 → Bell (erzeugt auf dem Terminal einen Ton) '\b' = 8 → Backspace (Bewegt den Cursor um eine Position nach links) '\t' = 9 → Horizontal Tab (Bewegt den Curso auf die nächste horizontale Tabulatorposition, Abstand ist Terminalspezifisch) '\n' = 10 → NewLine (Zeilenvorschub) '\v' = 11 → Vertial Tab '\r' = 13 → Carriage Return (Wagenrücklauf, Bewegt den Cursor an den Zeilenanfang) '\"' = 34 → Double Quote '\'' = 39 → Single Quote '\?' = 63 → Question mark '\\' = 92 → Backslash '\f' = 12 → FormFeed (Erzeugt auf dem Terminal/Drucker einen Seitenvorschub) '\ooo' = 0ooo → Oktal, wobei o Digits von 0..7 sein müssen '\xdd' = 0xdd → Hexadezimal, wobei d Digits von 0..9 A..F sein müssen '\udddd' Unicode mit 4 Digits '\Udddddddd' Unicode mit 8 Digits
Beispiel
'A' == 65 == '\101' == '\x41' == '\u0041' == '\U00000041'
Über Prefixe können alternative Zieldatentypen/Konvertierungen erzwungen werden:
u'b' → unicode 16 (nur C++) U'b' → unicode 32 (nur C++) L'b' → wchar_t
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
BearbeitenAlle Gleitkommakonstanten sind ohne weitere Angaben vom Datentyp double!
555.12 555. → 555.0 .1 → 0.1 4e2 → 400.0 0x22.11p13 → Konstante in Hexadezimalen Schreibweise (ab C99)
Über Suffixe kann ein alternativer Datentyp erzwungen werden:
3.0F → Datentyp float 3.0l → Datentyp long double
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
Konstanten Ausdrücke
BearbeitenErgä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:
int var1=7*3+5; → wird vom Compiler durch 26 ersetzt
int var2=(4710+(8-1))&-8; → wird vom Compiler durch 4712 ersetzt
var1=4*4*var1+77*(77%3); → wird vom Compiler durch 16 * var1 + 154 ersetzt
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
BearbeitenStringkonstanten erzeugen ein Array von Characters. Mit dem abschließenden Hochkommata wird das Array um ein Stringendezeichen ergänzt:
"abc" → (char []) {'a','b','c','\0'}
Adjacent Strings Konstanten werden vom Compiler zu einer String-Kontante zusammengefasst, so dass nur ein Stringendezeichen vorhanden ist:
"xxx" "yyy" → (char []) {'x','x','x','y','y','y','\0'}
Über Prefixe können alternative Zieldatentypen/Konvertierungen erzwungen werden:
L"string" → (wchar_t[7]){L's',L't',L'r' …)
Über Postfixes können in C++ Objekte erzeugt werden:
"string"s → erzeugt einen String vom Datentyp std::string "string"sw → Erzeugt einen String vom Datentyp std::string_view
String Konstanten werden Compilerintern über unnamed Variablen realisiert, welche als globale Variable angelegt werden und per default const sind:
printf("Wert=%d",4);
//entspricht
const char unnamed1[]="Wert=%d";
printf(unnamed1,4);
//String Konstanten sind per Default Konstant
char *str="hallo"; //Typkonflikt!, da "hallo" vom Datentyp const char[] ist
str[0]='H'; //Laufzeitfehler, da nun schreibender Zugriff auf im
//Readonly Speicher angelegt Variable.
Für doppelte String Konstanten wird nur einmal Speicher reserviert:
const char *str1="hallo";
const char *str2="hallo";
if(str1==str2) printf("Identische Startadressen");
Initialisierungsliste / Compound Literal
BearbeitenArrays, 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:
int arr[5] = { 1,2,3,4,5 };
struct xyz { int x,y,z;}; //Definition einer Struktur
struct xyz var={1,2,3};
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:
int *p=(int []){2,4};
//entspricht
int unnamed0[]={2,4};
int *p=unnamed0;
drawline((struct point){1,2},(struct point){2,3});
drawlinep( &(struct point){1,2},&(struct point){2,3});
//entsprechen
struct point unnamed1={1,2},unnamed2={2,3};
drawline(unnamed1,unnamed2);
drawlinep(&unnamed1,&unnamed2);
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
Variableninitialisierung
BearbeitenMit 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
BearbeitenGlobale 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:
int g=1; //Speicherstelle der Variable g wird
//mit 1 belegt
short array[3]={1,1+1, sin(0.0)}; //Speicherstellen des Arrays wird
//mit 1, 2 und 0 belegt werden
Nicht initialisierte globale und static lokale Variablen werden mit 0 initialisiert! D.h. die Speicherstellen dieser Variablen werden mit dem Wert 0 vorbelegt:
int f; //f hat den Wert 0
double arr[10]; //Alle Elemente von arr werden mit 0.0 initialisiert!
Lokale Variable
BearbeitenDer 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:
int var=sin(b);
//entspricht
int var; //a)Variablendefinition/Speicherplatzreservierung
var=sin(b); //b) Initialisierung über separate Zuweisungs-Operation
//d.h. es werden Maschinensprachebefehle erzeugt, welche
//den Inhalt der Variablen b holt, anschließend die sin()
//Funktion aufruft und dessen Rückgabewert der Variablen
//var zuweist.
Folglich können die Initialisierungswerte/Inhalte der Initialisierungsliste aus beliebigen Ausdrücken stehen und müssen keine Konstanten oder Konstanten Ausdrücke sein:
void foo(float f,float g) {
float summe=f+g;
float arr[3] = {f+f,g-f,sin(f)}; //Initialisierungswerte
//werden zur Laufzeit berechnet
}
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:
switch(value) {
case 0:
int lok=7;
printf("%d",lok); //OK, lok beinhaltet den Wert 7
break;
default:
printf("%d",lok); //KO, Variable lok vorhanden,
//aber nicht initialisiert. D.h. Ausgabe einer
break; //Zufallszahl
}
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:
int foo(void) {
int var_foo; //Variable var_foo bekommt den identischen
return var_foo; //Speicherbereich wie var_far zugwiesen
}
int far(void) {
int var_far=7;
return var_far;
}
printf("%d\n",foo()); //Zufallswert (0, da Stack mit 0 initialisiert ist)
printf("%d\n",far()); //Stack belegen
printf("%d\n",foo()); //Zufallswert (7, da Speicherstelle zuvor mit 7 belegt wurde
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++
BearbeitenIn 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:
int variable(10); //Bei primitiven Datentype entspricht dies
//int variable=10;
struct test {
int a;
test(int par) {a=par;} //Konstruktor der Struktur test
};
struct test test(4); //Konstruktor test(int par) wird aufgerufen
struct test test(4,5); //KO, kein passender Konstruktor vorhanden
Sonstiges
BearbeitenFolgende 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
extern int var=7; //Fehlerhaft
- Const und lokale Static Variablen sollten initialisiert werden, auch wenn C in diesem Fall keinen Fehler meldet!
const int konstante; //Syntaktisch OK, Initialisierungswert fraglich!
static int statisch;
Anweisung (Statement) & Expression
BearbeitenEine 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)
BearbeitenEntsprechend 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:
a=sqrt(a*a+b*b);
//Ausdruck mit Nebenwirkungen
int func(int par) { a++; return par*2;}
b=func(1)*func(2);
//Ausdruck mit undefinierten Verhalten
a=++a + 1;
a=for(lauf=10;lauf>0;lauf--); //for-Anweisung ist kein Ausdruck
//d.h. liefert keinen Wert zurück
Discarded Value
BearbeitenDiscarded 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:
int a=7;
a; //Variable a wird gelesen, der Wert jedoch verworfen
7; //Konstante wird verworfen
a+7; //Ergebnis wird Berechnet und verworfen
printf("hallo"); //Printf hat einen Rückgabewert, dessen Wert verworfen wird
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();
- 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
- Java kennt Dicarded Values nicht. Ausnahme, es handelt sich um einen Funktionsaufruf, dessen Rückgabewert nicht genutzt wird.
Sequence Point
BearbeitenEin 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.
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
- 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
Block / Compound-Statement
BearbeitenEin 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/textsuperscriptOPT}
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.
for(..) Anweisung; //For-Schleife führt nur diese eine Anweisung aus
for(…) {Block} //For-Schleife führt Block (als eine Anweisung) aus
if(..) Anweisung; else Anweisung; //IF-Anweisung führt nur 'eine'
//Anweisung aus
if(..) {Block} else {Block} //IF-Anweisung führt Block aus
if( ) {Block}; else {Block}; //Semikolon ist eine separate
//Leer-Anweisung, so dass else
//nicht zur IF-Anweisung zugeordnet
//werden kann (2 Anweisungen vor Else)
void foo(void) {
int var1;
{ //Block innerhalb von Block als Strukturierungswerkzeug
int var2;
struct xyz{int x,y,z}; //Defintion eines Datentyps im Block
struct xyz var3;
}
struct xyz var4; //KO, Datentyp hier nicht mehr bekannt
}
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:
void foo(void) {
{
int label=1;
//Funktion innerhalb einer Funktion
void dummy(void){
label++; //Zugriff auf Variablen des umschließenden Blockes
}
dummy(); //Aufruf der nur in diesem Block gültigen Funktion
}
dummy(); //KO, Funktion hier nicht mehr gültig
}
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 } }
Embedded Statement
BearbeitenEmbedded 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-listOPT})
Der Rückgabewert wird nicht über eine return-Anweisung angegeben, sondern entstammt aus der letzten Anweisung innerhalb der Liste:
int var;
var={int par=7;} //KO Block hat keine Rückgabewert.
var=({int par=7; par++; par;}); //OK Embedded-Statement hat einen
//Rückgabewert, der sich aus der
//letzten Anweisung 'Par;' ergibt
//Hier 8.
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:
int var1=8;
int b=({int d=var1++;d>10?10:d;});
if( ({int d=b;b=var1;b=d;}) > 1)
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:
void test(int (*fptr)(int),int arr[10]) {
for(int lauf=0;lauf<10;lauf++)
arr[lauf]=fptr(arr[lauf]);
}
int main(int argc, char *argv[]) {
int arr[]={1,2,3,4,5,6,7,8,9,10};
//Aufruf mit einer anonymen Funktion
test( ({int func(int a) {return a%2?a:0;} ; func;})
,arr);
}
Funktionen / Prozeduren
BearbeitenFunktionen/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;
- 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);
- → 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);
Return-Anweisung
BearbeitenMit 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 expressionOPT
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:
int foo(double a, double b) {
return (int)(a*a+b*b);
}
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
- 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
BearbeitenAus Datentypsicht kann der Funktionsname inkl. den Parameter(n) in einem Ausdruck mit dem Datentyp des Rückgabewertes der Funktion ausgetauscht werden:
int foo(void) {return 4711;}
int var;
var=1+foo(); //Aus Datentypsicht (int)=(int)+(int)
Daraus ergibt sich, dass bei Arrays/Strukturen/Unions direkt auf einzelne Komponenten zugegriffen werden kann, ohne diese zuvor einer Variablen zuzuweisen:
struct xyz{int x,y,z;};
struct xyz foo(int par) {
struct xyz var;
var.x=var.y=var.z=par;
return var;
}
if(foo(12).x == 12) printf("Identisch\n");
Fehlerrückgabe
BearbeitenViele 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)
char *strdup(char *src) {
char *dst;
if((dst=malloc(strlen(src)+1))==NULL)
return NULL;
strcpy(dst,src);
return dst;
}
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):
#include <errno.h>
int var;
if(-1==scanf("%d",&var) ) printf("Es wurde keine Zahl erkannt\n");
int ret=open("/etc/ptmp",O_WRONLY|O_CREAT|O_EXCL,
S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
if(-1==ret)
printf("open failed() with %s\n",strerror(errno));
if(-1==ret)
printf("open failed() with %m (nur glibc)\n");
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
BearbeitenMit 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:
int main(void) { }
int main(int argc, char *argv[]) { }
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:
int main(int argc, char *argv[]) {
//Variante 1: Darstellung des Inhaltes des Arrays von Strings
for(int lauf=0;lauf < argc; lauf++)
printf("%d.Parameter: '%s'\n",lauf,argv[lauf]);
//Variante 2: Darstellung des Inhaltes des Arrays von Strings
(void)argv; //Wird hier nicht benötigt
for(char **str=argv;*str!=NULL;str++)
printf("Parameter: '%s'\n",*str);
return EXIT_SUCCESS;
}
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
BearbeitenWie 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
BearbeitenMit 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:
int printf(const char *fmt, ... );
Die wie folgt aufgerufen werden kann:
printf("Hello World"); //es folgt kein Parameter
printf("%d",100); //es folgt ein Parameter vom Datentyp int
printf("%f",1.1); //es folgt ein Parameter vom Datentyp double
printf(conststr,1,2,3,"xyz") //Ohne Worte
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.:
signed char varc=-1;
float varf =0.1;
printf("%d %f",varc, varf); //varc wird beim Aufruf nach int konvertiert!
//varf wird beim Aufruf nach double konvertiert!
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)
BearbeitenIm Normalfall ergibt sich die Anzahl der Elemente eines Arrays aus einer Konstanten, so dass der Compiler zur Compilezeit passend Speicher reserviert (siehe Kap. Array):
int arr[4]; //Compiler reserviert für Variable arr
//4*sizeof(int) Speicherplatz
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:
void foo(size_t n) {
int arr[n*2+1]; //Arraydimension ergibt sich zum Ausführungszeitpunkt
n++; //Nachträgliches Ändern von n ändert nicht
//die Größe des Arrays!
}
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):
void foo(size_t n) {
char str[n];
memset(str,0,n*sizeof(char)); //Array mit 0 füllen
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:
void foo(size_t n) {
int arr[n*2+1]; //VLA
size_t size;
size=sizeof(arr); //sizeof(arr) entspricht einem Funktionsaufruf
//welcher die Speichergröße des VLA's ermittelt
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; }
Operatoren
BearbeitenIn 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:
++ → (Postfix) ++ vs. ++(Präfix)
-- → (Postfix) -- vs. --(Präfix)
+/- → Vorzeichen vs. Addition/Subtraktion
() → Funktionsaufruf() vs. Befehlsbestandteil if() vs. Priorisierung
Für den Umgang mit dem Datentyp Zeiger besitzen einige Punctuatoren ergänzende Funktionalitäten (siehe Kap Zeiger):
& → Bitweises UND:
Anwendung: Ganzahlausdruck & Ganzahlausdruck
& → Adressoperator:
Anwendung: &Variable
* → Multiplikation
Anwendung: Ganzahl-/Gleitkommaausdruck * Ganzahl-/Gleitkommaausdruck
* → Datentyp Zeiger
Anwendung: Datentyp * Variablenname;
* → Dereferenzierung:
Anwendung: * 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
BearbeitenDie 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:
a=7; //Zuweisung
a=b=7; //Erste Zuweisung (b=7) hat einen Rückgabewert, welcher im
//Anschluss a zugewiesen wird
if(a=7) //Variablen a wird der Wert 7 zugewiesen. Der Rückgabewert
//der Zuweisung wird im Anschluss auf True oder False geprüft
if(7=a) //Besser, da aufgrund der fehlerhafter Zuweisung ein
//Compilerfehler geworfen wird
x*=y=z //entspricht x=x*(y=z)
a=b=d+7; //entspricht a=(b=(d+7))
a=b==c; //a bekommt das boolesche Ergebnis der Vergleichsoperation
//zugewiesen
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
- 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).
Komma-Operator
BearbeitenDer 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:
int a=(5,3); //5 wird verworfen, 3 dient als Rückgabewert, so dass
//a den Wert 3 erhält.
int b=5,3; //KO, da Komma Bestandteil der Definition ist
int c=1,d=2,e=3;
int f=(c+=2,a+a); //c um zwei erhöht, a+a in f
int g= c+=2,a+a; //KO, da Komma Bestandteil der Defintion ist
void func(int value){}
func(1); //Funktion wird mit dem Wert 1 aufgerufen
func(1,2); //KO, da Komma Bestandteil des Funktionsaufrufes ist
func((1,2)); //Funktion wird mit dem Wert 2 aufgerufen
int arr[12];
arr[3,4]=1; //3 wird verworfen, 4 dient als Index für das Array
'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]);
- 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;
- An Stellen, wo nur eine Anweisung erlaubt ist (und man zu faul ist, einen Block zu öffnen)
if(…) y=2,z=3;
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
?-Operator / Conditional-Operator
BearbeitenOperator 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
int a=5;
a=a > 10 ? "String" : 4; //KO, "String" ist vom Datentyp (char*) und
// 4 ist vom Datentyp (int).
- 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)
int a=13;
int b=a>10 ? : 0; //wenn a > 10 wird true, andernfalls false zurückgegeben
- 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.
int a=7,max=5;
max=a>max ? a : max; //Alles bestens
int var0,var1,var2;
var0=a>5 ? a++,5:6,a=1; //das zweite Komma wird als separate Anweisung
//ausgeführt und kommt somit immer zur
//Ausführung
var1=(a>5 ? a++,5:6),a=1; //durch Klammerung klare Abgrenzung
var2= a>5 ? a++,5 : (6,a=1); //durch Klammerung klare Abgrenzung
Die Nutzung des ?-Operators empfiehlt sich an diversen Stellen:
- Bei Makros
#define MAX(a,b) (a>b?a:b)
- Beim Fehlerhandling
char *str=strstr("hallo123","lox"); //Ausgabe von str nur, wenn Suche erfolgreich (str!=NULL) war printf("%s",str!=NULL?str : "(not found)" );
- Prüfen, ob eine Funktion erfolgreich ausgeführt wurde, bevor eine weitere Funktion ausgeführt wird
int ret;
ret=init1(); //im Fehlerfreien gibt init1() 0 zurück
ret=ret?:init2(); //Aufruf von init2() nur wenn ret==0 ist
//andernfalls wird ret zurückgegeben
Logische Verknüpfungen
BearbeitenAlle 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:
int a=0,b=0;
//a=-1; //Als alternativer Startwert
if(++a || ++a)
b=1;
Diese Funktionalität ist insbesondere beim Umgang mit Fehlern vorteilhaft:
- Prüfen, ob eine Variable einen gültigen Wert hat, bevor sie weiterverwendet wird
char *str = strstr("hallo123","la");
if(str!=NULL && strlen(str) )
printf("Len = %ld",strlen(str));
- Prüfen, ob eine Funktion erfolgreich ausgeführt wurde, bevor eine weitere Funktion aufgerufen wird:
int ret;
ret=init1(); //Bei erfolgreicher Initialisierung wird 1 zurückgegeben
ret=ret && init2();
Bitweise Verknüpfungen
BearbeitenAlle 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
BearbeitenDer 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);
- 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
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:
size_t size;
short var;
size=sizeof var+0; //size=2, da sizeof auf var angewendet wird
size=sizeof(var+0); //size=4, da sizoeof auf den resultierenden datentyp
//angewendet wird
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
Anweisungen
BearbeitenIf-Anweisung
BearbeitenSyntax: 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:
if(a==1)
if(a=1) //Erst Zuweisung, dann prüfen der Rückgabe der Zuweisung
if(a) //entspricht a!=0
if(printf("hallo")<0) //prüfen, ob printf() fehlschlägt
if( ({int dummy=a;a=b;b=dummy;})==6)
if(a=7) //Zuweisung!
if(7=a) //KO, Eine Konstante kann kein L-Value sein
if(7==a)
{printf("a ist 7");}; //Semikolon hinter Blocksope ist eine zweite
else //Anweisung, so dass die ELSE Anweisung nicht
{printf("Compilerfehler");};//zur IF-Anwesigung zugeordnet werden kann.
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
BearbeitenSyntax: for(InitialisierungsteilOPT ; TestOPT ; FortsetzungOPT) Statement
Die For-Anweisung wird vom Compiler wie folgt umgesetzt:
InitialisierungsteilOPT
Goto Next
Goon:
//Continue-Anweisung entspricht goto Continue
//Break-Anweisung entspricht goto Break;
Statement
Continue:
FortsetzungOPT
Next:
if(TestOPT) goto Goon
Break:
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!
- 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]; }
- 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++);
- 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;
- Zeiger als Schleifenvariable
char src[]="hallo"; char dst[strlen(src)+1]; char *s,*d; for(s=src,d=dst; *s; ) *d++=*s++; *d=0;
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++)
- Zur Geschwindigkeitssteigerung sollte im Testteil auf 0 abgefragt werden, d.h. die Schleifenvariable dekrementiert werden
for(int lauf=700000;lauf;lauf--)
- Alternativ zu einer Laufvariablen empfiehlt sich die Nutzung von Pointer als Schleifenvariable;
for(char *ptr=str; *ptr; ptr++);
While-Anweisung
BearbeitenSyntax: while(expression) Statement
Syntax: do Statement while(expression);
Die While-Anweisungen werden vom Compiler wie folgt umgesetzt:
while()-Anweisung | do while()-Anweisung |
---|---|
Continue: |
GoOn: |
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);
- 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;
Switch-Anweisung
BearbeitenSyntax: 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) |
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");
- 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; } }
- 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)
- 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"); } }
- 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 }
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; }
- Siehe:Hash Table in C
Label + Goto
BearbeitenSyntax: 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()
undlongjmp()
) - 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: ;
- 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:
goto label1;
if(a==3) {
label2:
printf("True\n");
}
else
label1:
{
printf("False\n");
}
if(a==3) goto label2;
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; }
- 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);
Sonstiges
Bearbeiten??-Trigraphs
BearbeitenZeichenfolge 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
BearbeitenDie Programmiersprache C kennt nachfolgende grundlegende Datentypen und Zeiger auf diese Datentypen (siehe auch Grundlagen:Grundlegende Datentypen und Typsicherheit)
Datentyp | Bsp. für Datentyp | Bsp. für Zeiger auf Datentyp |
---|---|---|
Ganzzahl |
char var;
int var;
short int var;
long int var;
long long int var;
|
char *ptr;
int *ptr;
short int *ptr;
long int *ptr;
long long int *ptr;
|
Gleitkommazahl |
double var;
float var;
long double var;
|
double *ptr;
float *ptr;
long double *ptr;
|
Funktionen |
void func(void) {}
|
void (*pfunc)(void);
|
Arrays |
int arr[10];
float arr[3][3];
|
int (*parr);
float (*parr)[3];
void (*pfunc[5])(void);
|
Strukturen/Unions |
struct xyz {int x,y,z};
union abc {int a,b,c};
|
struct xyz *ptr;
union abc *ptr;
|
Aufzählungstyp |
enum abc {A,B,C};
|
enum abc *ptr;
|
Boolescher Datentyp |
_Bool var;
|
_Bool *ptr;
|
Komplexe Zahlen |
_Complex var;
|
_Complex *ptr;
|
Bis auf die Datentypen Funktionen, Array und die Zeiger sollen die wesentlichen Aspekte dieser Datentypen im nachfolgenden beschrieben werden.
Der grundlegende Datentyp ist 'int'!
- Es wird mit nichts Kleinerem gerechnet als mit dem Datentyp 'int' (siehe Implizite Typumwandlung) :
char var1=10,var2=20,var3; var3=var1+var2; //Addition findet auf Basis des Datentyps int statt
- Es gibt zwar den Booleschen Datentyp, dieser entspricht jedoch einem Ganzzahldatentyp und
true
undfalse
entsprechen den Integerwerten 0 und 1 - Der Aufzählungsdatentyp und seine Aufzählungselemente sind vom Datentyp 'int'
- Bis einschl. C90 wurde bei der Definition von Variablen ohne Angabe des Datentyps, bei Definition einer Funktion ohne Angabe des Datentyps der Übergabeparameter, des Rückgabedatentyps der Datentyp implizit auf int gesetzt (implizit int)
static var2=2; foo(par1,par2){ auto var1; var1=par1+par2; return var1; }
Ganzzahl Datentypen
BearbeitenGrundlegend kennt die Programmiersprache C nur zwei Ganzzahldatentypen:
- Datentyp:
int
- Vorzeichenbehafteter (signed) Datentyp, dessen Bitbreite von der Rechnerarchitektur und des Compilers abhängig ist
- Datentyp
char
- Datentyp mit einer Mindestbreite von 8-Bit u.A. zur Speicherung von Zeichen typischerweise im ASCII Format. Die C-Spezifikation [C11 6.2.5 Types] lässt es dem Compiler frei, ob char als vorzeichenbehafteter oder vorzeichenloser Datentyp implementiert wird.
- Ergänzend kann der Datentyp char auch zur Speicherung von ganzen Zahlen genutzt werden. In diesem Fall empfiehlt sich, diese explizit als signed und unsiged zu definieren. Wenn nur ASCII Zeichen gespeichert werden, so ist diese Angabe nicht nötig!
Optional können Qualifier dem Datentyp vorangestellt werden:
unsigned
/signed
zur Angabe, ob der Inhalt als vorzeichenlose Zahl oder als vorzeichenbehaftete Zahl zu interpretieren ist. Ohne Angabe sind Variablen vom Datentyp int vorzeichenbehaftetshort
/long
/long long
(letzteres seit C99) als vorangestellten Qualifier zuint
zur Vergrößerung/Verkleinerung der Bitbreite. Bei Angabe des Qualifiers ist das Schlüsselwortint
optional. Die Beschreibunglong long int
undlong long
sind gleichbedeutend!
Als Bitbreite für die Datentypen ist im C-Standard nur ein Mindestbreite definiert. Die genaue Bitbreite hängt von der Rechenbreite des Systems und vom Compiler ab (siehe [C11 6.2.5 Types]):
Datentype | Mindest breite |
Typisch bei 32-Bit Architektur |
Typisch bei 64-Bit Architektur |
printf Format Anweisung |
---|---|---|---|---|
char |
8 | 8 | 8 | %c
|
signed char unsigned char |
8 | 8 | 8 | %hhd %hhu
|
short short int |
16 | 16 | 16 | %hd / %hu
|
int |
16 | 16/32 | 32 | %d / %u
|
long long int |
32 | 32 | 32/64 | %ld / %lu
|
long long long long int |
64 | 64 | 64 | %lld / %llu
|
In der Header-Datei limits.h
sind die tatsächlichen Grenzwerte der Datentypen gespeichert:
Konstante | Beschreibung | Typ. bei 64-Bit Architektur |
---|---|---|
CHAR_BIT |
Anzahl der Bits in einem Char | 8 |
SCHAR_MIN |
min. Wert, den der Typ bei signed char aufnehmen kann | -128 |
SCHAR_MAX |
max. Wert, den der Typ bei signed char aufnehmen kann | 127 |
UCHAR_MAX |
max. Wert, den der Typ bei unsigned char aufnehmen kann | 255 |
CHAR_MIN |
min. Wert, den der Typ char aufnehmen kann | 0 oder SCHAR_MIN |
CHAR_MAX |
max. Wert, den der Typ char aufnehmen kann | SCHAR_MAX oder UCHAR_MAX |
SHRT_MIN |
min. Wert, den der Typ short int annehmen kann | -32.768 |
SHRT_MAX |
max. Wert, den der Typ short int annehmen kann | 32.767 |
USHRT_MAX |
max. Wert, den der Typ unsigned short int annehmen kann | 65.535 |
INT_MIN |
min. Wert, den der Typ int annehmen kann | -2.147.483.648 |
INT_MAX |
max. Wert, den der Typ int annehmen kann | 2.147.483.647 |
UINT_MAX |
max. Wert, den der Typ unsigned int aufnehmen kann | 4.294.967.296 |
LONG_MIN |
min. Wert, den der Typ long int annehmen kann | -2.147.483.648 oder -9.223.372.036.854.775.808 |
LONG_MAX |
max. Wert, den der Typ long int annehmen kann | 2.147.483.647 oder 9.223.372.036.854.775.807 |
ULONG_MAX |
max. Wert, den der Typ unsigned long int annehmen kann | 4.294.967.296 oder 18.446.744.073.709.551.616 |
LLONG_MIN |
min. Wert, den der Typ long long int annehmen kann | -9.223.372.036.854.775.808 |
LLONG_MAX |
max. Wert, den der Typ long long int annehmen kann | 9.223.372.036.854.775.807 |
ULLONG_MAX |
max. Wert, den der Typ unsigned long long int annehmen kann | 18.446.744.073.709.551.615 |
Aufgrund dessen, dass die Breite der Datentypen von der Rechnerarchitektur und vom Compiler abhängig ist, sind in der Header-Datei stdint.h
Aliase für Datentypen enthalten, welche in ihrem Alias die tatsächliche Breite und das Vorzeichens beinhalten:
uint8_t --> unsigned 8-Bit
int8_t --> signed 8-Bit
uint16_t --> unsigned 16-Bit
int16_t --> signed 16-Bit
...
Für C++ sehen diese Datentypen wie folgt aus:
std::intptr_t
std::int8_t std::uint8_t
std::int16_t std::uint16_t
std::int32_t std::uint32_t
Std::int64_t std::uint64_t
Zur Erzeugung von portablen/rechnerunabhängigen Programmen empfiehlt sich, diese Datentypen zu nutzen. Die Rechenbreite ist dann unabhängig vom Compiler und Betriebssystem.
Gleitkomma Datentypen
BearbeitenGleitkommazahlen werden im Computer auf Basis folgender Schreibweise dargestellt (siehe auch: Gleitkommazahl):
Dezimalsystem Dualsystem
12,3456710 = 1234567 * 10^-5 0,110 = 1,1001100110011 * 2^-4
------- -- --------------- --
Mantisse Exponent Mantisse Exponent
- Mantisse
- Vorzeichenlose ganze Zahl (ggf. mit einer festen Position des Dezimalpunktes=Festkommazahl)
- Exponent
- Vorzeichenbehaftete ganze Zahl, welche um einen Bias verschoben ist
- Vorzeichen
- Vorzeichen der (vorzeichenlosen) Mantisse
Zur Speicherung im Computer werden das Vorzeichen, die Mantisse und der Exponent getrennt im Binärformat gespeichert, für den Programmierer aber als eine (zusammenhängende) Zahl dargestellt:
Gleitkommazahlen haben genauso wie ganze Zahlen einen beschränkten Wertebereich:
- Die Anzahl der Bits der Mantisse bestimmen die Anzahl der Nachkommastellen:
Bei 23-Bit Mantisse hat das niederwertigste Bit die Wertigkeit: 2^-22=1/2^22=1/(2^10*2^10*2^2)=1/(1024*1024*4)≅1/(4.000.000)=0,000.000.25 → Es könnten also nur 7..8 dezimale Nachkommastellen gespeichert werden
- Mit dem Exponent kann 'quasi' der kleinste und der größte Wertebereich angegeben werden:
Bei 8-Bit Exponent mit einem Bias von 127 liegt der Wertebereich des Exponenten im Bereich von -126 … +127 (Exponent -127 und +127 wird zur Darstellung weitere Zahlen benötigt). Die kleinste darstellbare Zahl (unter Vernachlässigung der Nachkommastellen der Mantisse ist: 2^-126=1/2^126≅1,175*10^-38 Die größte darstellbare Zahl ist: 2^127 ≅1,7*10^38
Die genaue Darstellung (Anzahl der Bits für Mantisse und Exponent, Darstellung von NAN/INF, ….) ist in C nicht spezifiziert und somit compiler- und rechnerabhängig. Zumeist wird mittlerweile die IEEE 754 Standardisierung genutzt (Norm wurde von Intel mit der Entwicklung der 8087 FPU entworfen).
Grunddatentypen und der Wertebereich (nach IEEE 754)
Daten- type |
Speicher- platz |
Exponent | Mantisse | Größte Zahl | Kleinste Zahl | Genauigkeit | printf Format Anweisung |
---|---|---|---|---|---|---|---|
float |
4 Byte | 8 bit | 23 bit | +-3,4*1038 | 1,2*10-38 | 6 Stellen | --- |
double |
8 Byte | 11 bit | 52 bit | +-1,7*10308 | 2,3*10-308 | 12 Stellen | %f %e %g |
long double |
10 Byte | ≥8 bit | ≥ 63 bit | +-1,1*104932 | 3,4*10-4932 | 18 Stellen | %Lf %Le %Lg |
Aufgrund des nicht standardisierten Formates sollte ein Gleitkommadatentyp nicht im Binärformat für den Datenaustausch (mit Netzwerk, über Dateien, ..) genutzt werden. Andere Rechnersysteme/Programmiersprachen würde aufgrund der anderen Interpretation andere Zahlenwerte aus den übertragenen/gespeicherten Binärdaten auslesen!
float var1;
write(file_hdl,&var1,sizeof var1); //Schreiben des Binärwertes
//in eine Datei
double var2;
send(socket_hdl,&var2,sizeof var2,0);//Senden des Binärwertes
//über ein Netzwerk
Die Standard-C-Library bietet einige mathematische Funktionen wie sin() / cos() / tan() / atan2() / sqrt() / pow() / exp()/…
an. Die Prototypen dieser Funktion sind in der Header-Datei math.h
beschrieben, so dass diese bei Nutzung dieser Funktion zu inkludieren ist. Ergänzend ist die Shared Library libm.so über den Compilerschalter '-lm' einzubinden:
#include <math.h>
//Math-Library libm.so mittels Linker-Command '-lm' einbinden
int main(int argc,char *argv[]) {
double var1=47.11;
double var2=sqrt(var1);
Die Funktionen basieren auf den Datentyp double
, d.h. sowohl der Übergabewert als auch der Rückgabewert ist vom Datentyp double
. Die Prototypen sehen wie folgt aus:
double sin(double);
double cos(double);
double tan(double);
double asin(double);
double acos(double);
double atan(double); //Wertebereich der Rückgabe von -PI/2..+PI/2
double atan2(double,double); //Wertebereich der Rückgabe von -Pi .. +PI
double sqrt(double);
double log(double);
Über den Suffix f oder l im Funktionsnamen kann der zugrundeliegende Datentyp auf float (Suffix f) oder long double (Suffix l) geändert werden:
float sinf(float);
long double sinl(long double);
Ergänzend zu den Prototypen sind die gebräuchlichen Naturkonstanten in math.h
definiert (siehe auch: math.h):
M_E Value of e
M_LOG2E Value of log_2 e
M_LOG10E Value of log_10 e
M_LN2 Value of log_e 2
M_LN10 Value of log_e 10
M_PI Value of π
M_PI_2 Value of π/2
M_PI_4 Value of π/4
M_1_PI Value of 1/π
M_2_PI Value of 2/π
M_2_SQRTPI Value of 2/√π
M_SQRT2 Value of √2
M_SQRT1_2 Value of 1/√2
Gleitkommazahlen können nicht nur Zahlenwerte, sondern auch Sonderwerte annehmen. Diese Sonderwerte sind ebenfalls in math.h
beschrieben (siehe auch: math.h):
INFINITY A constant expression of type float representing
positive or unsigned infinity, if available; else a
positive constant of type float that overflows at
translation time.
NAN A constant expression of type float representing a
quiet NaN. This macro is only defined if the
implementation supports quiet NaNs for the float type.
Mit fesetround()/fegetround()
kann gesetzt/gelesen werden, wie mit Ergebnissen umzugehen sind, die nicht exakt darstellbar sind. Mögliche Werte sind 'round to nearest' (default), 'round up', 'round down' und 'round toward zero'.
Komplexe Zahlen
BearbeitenNach dem Wikipedia Artikel komplexe Zahlen stellen komplexe Zahlen eine Erweiterung der reellen Zahlen dar. Ziel der Erweiterung ist es, algebraische Gleichungen wie x2+1=0 bzw. x2=-1 lösbar zu machen. ... Da die Quadrate aller reellen Zahlen größer oder gleich 0 sind, kann die Lösung der Gleichung x2=-1 keine reelle Zahl sein. Man braucht eine ganz neue Zahl, die man üblicherweise i nennt, mit der Eigenschaft i2=-1. Diese Zahl i wird als imaginäre Einheit bezeichnet.
Komplexe Zahlen werden nun als Summe a+b*i definiert, wobei a und b reelle Zahlen sind und i die oben definierte imaginäre Einheit ist.
Der Datentyp für komplexe Zahlen (erst ab C99 enthalten) beinhaltet folglich zwei Gleitkommazahlen, eine für den reelen Teil und eine für den imaginären Teil. Entsprechend den Gleitkommazahlen steht der komplexe Datentyp ebenfalls in drei unterschiedlichen Genauigkeiten zur Verfügung:
Datentype | Speicherplatz |
---|---|
float _Complex |
2x4Byte |
double _Complex _Complex |
2x8Byte |
long double _Complex |
2x10Byte |
Wie auch beim Booleschen Datentyp ist in der Header-Datei complex.h
für den einfacheren Umgang mit diesem Datentyp das Makro complex
als Textersetzung für _Complex
gesetzt.
Zur Darstellung von komplexen Konstanten wurde die Gleitkommakonstante um den Suffix i ergänzt. Durch Anhängen von i wird aus dem ansonsten rellen Teil der imaginäre Teil und damit aus der Gleitkommakonstante eine komplexe Gleitkommakonstante:
_Complex varc1=2i;
_Complex varc2=1+2i;
printf("%f %f\n",creal(varc1),cimag(varc1));
printf("%f %f\n",creal(varc2),cimag(varc2));
Normale Gleitkommavariablen werden als reellen Teil einer komplexen Zahl angesehen. Bei Operationen von Gleitkommazahlen und komplexen Zahlen wird der imaginäre Teil der Gleitkommazahl auf 0 gesetzt. Beim Zuweisen einer komplexen Zahl an eine Gleitkommazahl wird nur der reele Teil 'gelesen'.
Soll eine Gleitkommavariablen zu einem imaginären Teil gewandelt werden, so muss diese bspw mit der Konstanten 1.0i multipliziert werden:
double vard1=2i; //Vorsicht, es wird nur der reele Teil
printf("%f\n",vard1); //der komplexen Zahl in vard1 gespeichert!
_Complex varc1=1+2i;
vard1=1;
varc1=varc1+vard1;
printf("%f %f\n",creal(varc1),cimag(varc1));
varc1=varc1+vard1*1.0i;
printf("%f %f\n",creal(varc1),cimag(varc1));
Die Standard-C-Library bietet diverse mathematische Funktionen wie csin(), ccos(), csqrt(), cpow() und ergänzend Umrechnungsfunktionen von Gleitkommazahlen in komplexe Zahlen (und umgedreht) wie creal(),cimag() ,cabs() für den Umgang mit komplexen Zahlen an:
double _Complex csin(double _Complex);
double _Complex csqrt(double _Complex);
double creal(double _Complex);
double cimag(double _Complex);
Zur Nutzung dieser Funktionen muss die Header-Datei complex.h
inkludiert und ergänzend die Shared Library libm.so über den Compilerschalter '-lm' eingebunden werden.
#include <complex.h>
//Math-Library mittels Linker-Command '-lm' einbinden
int main(int argc, char *argv[]) {
double _Complex xyz=6+2i; //Reeler-Teil=6 Imaginärer Teil=2
printf("Reeller Teil: %f Imaginärer Teil:%f",creal(xyz),cimag(xyz));
Die Funktionen beruhen auf den Datentyp double _Complex
, d.h. sowohl der Übergabewert als auch der Rückgabewert ist double _Complex
resp. double
(bei den Umrechnungsfunktionen). Über den Suffix f und l im Funktionsnamen kann der zugrundeliegende Datentyp geändert werden:
float crealf(float _Complex z);
long double creall(long double _Complex z);
Hinweis:
- Der Datentyp
_Complex
und die dazugehörigen mathematischen Funktionen sind in der C-Spezifikation als optional gekennzeichnet.
Boolschescher Datentyp
BearbeitenSiehe Grundlagen: Boolescher-Datentype/Operatoren
void / unvollständiger Datentype
BearbeitenDer Datentyp void
dient vorrangig dazu, nicht vorhandene Über- und Rückgabewerte von Funktionen anzuzeigen. Eine Variable vom Datentyp void
kann nicht angelegt werden. Ein Zeiger vom Datentyp void
ist möglich, kann aber nicht dereferenziert werden (siehe Kap. Zeiger:Void-Zeiger)
void func(void); //Void zur Darstellung der nicht vorhandenen Parameter und
//des nicht vorhandenen Rückgabewertes
void var; //Eine Variable vom Datentyp void kann nicht angelegt werden
void *ptr; //Void-Zeiger
Hinweise:
sizeof(void)
ist nach der C-Spezifikation nicht erlaubt. Dennoch geben viele Compiler hier den Wert 1 zurück!- Mit einem expliziten Cast auf den Datentyp void wird dem Compiler mitgeteilt, dass diese Variable in Gebrauch ist, der Wert in dieser Operation aber nicht genutzt wird. Hierüber kann die Compilerwarning 'unused Variable' unterbunden werden:
void func(int par1, int par2) { int var1=12; int var2=13; (void)par2; //Angabe, dass diese Variable genutzt wird. (void)var2; //Angabe, dass diese Variable genutzt wird. if(var1==par1) //var1 und par1 werden verwendet
Datentypkonvertierung
BearbeitenDie Abarbeitungsreihenfolge der Operatoren wird durch die Prioritätenliste/Rangfolge (siehe C-Programmierung:Liste der Operatoren nach Priorität) vorgegeben. Operatoren mit höherer Priorität werden vor Operatoren mit niedriger Priorität ausgeführt:
//Der Ausdruck
c = sizeof(x) + ++a / 3;
//wird aufgrund der Prioritäten wie folgt ausgewertet:
c= (sizeof(x)) + ( (++a) / 3);
Bei identischer Priorität ergibt sich die Abarbeitungsreihenfolge aus der Assoziativität (L-R oder R-L)
a=33 / 5 / 2;
//Wird aufgrund der Assoziativität wie folgt ausgewertet.
//a= (33 / 5) / 2;
//und damit zu 3 und nicht zu 16 (bei 33 / (5/2)) ausgewertet.
a = b = c = d*2; //→ a=(b=(c=(d*2)));
a = b = 1+c = d; //→ a=(b=((1+c)=d)); //Compilerfehler, da
//1+c kein lvalue ist
Für das Rechnen/Vergleichen müssen beide Operatoren vom identischen Datentyp sein! Sind diese nicht identisch, so müssen die Datentypen 'angeglichen' werden. Dies kann einerseits ‚automatisch‘ mittels 'implizierter' Typumwandlung oder ‚manuell‘ mittels ‚expliziter' Typumwandlung erfolgen.
Beim Zuweisungsoperator (inkl. Parameterzuweisung bei Funktionsaufrufen und Funktionsrückgabewerte) gilt dies ebenso, nur dass hier der Quelldatentyp an den Zieldatentyp angepasst werden muss. Die impliziten Regeln finden hier keine Anwendung.
Hinweis:
- Empfehlenswert ist, die Datentypen der Variablen so zu wählen, dass der Compiler keine implizite Typumwandlung tätigt. Kann dies nicht vermieden werden, so sollte die explizite Typumwandlung genutzt werden (um sich einen möglichen Datenverlust bewusst zu machen).
- Der Datentyp selbst sollte den möglichen Wertebereich der Variablen entsprechen und nicht unnötig groß gewählt werden.
Datentyp zur Speicherung der Stundenzeit: Wertebereich: 0 ... 23 -> unsigned char Datentyp zur Speicherung einer Jahreszahl: Wertebereich: 2000 v.C. ... 4000 n.C -> signed short Datentyp zur Speicherung einer Temperatur: Wertebreich: -273,0°C ... 2000,0°C -> float
Implizite Typumwandlung
BearbeitenDiese Regeln wurden so aufgestellt, dass dabei stets ein Datentyp in einen anderen Datentyp mit höherem Rang umgewandelt wird (Rangordnung: long double, double, float, long long, long, int) [C11 6.3.1.8] .
Regel/ Priorität |
If either operand has Type |
And the other operand has Type |
Converts both to |
---|---|---|---|
1 | long double | any real type | long double |
2 | double | any real type | double |
3 | float | any real type | float |
4 | any unsigned type | any unsigned type | The unsigned type with the greater rank |
5 | any signed type | any signed type | The signed type with the greater rank |
6 | any unsigned type | a signed type of greater rank that can represent all vaues of the unsigned type |
The signed type |
7 | any unsigned type | a signed type of greater rank that cannot represent all values ot the unsigned type |
The unsigned version the the signed type |
8 | any other type | any other type | No conversion |
Ergänzend gilt das Regelwerk zu Integer Promotion [C11 6.3.1.1], welche Datentypen kleiner als signed int zu int und kleiner als unsigned int und unsigned int konvertiert (Compiler kann hiervon abweichen, wenn sichergestellt ist, dass kein Datenverlust eintritt).
Regel 6 und 7 sind der nicht klar definierten Bitbreite der ganzzahligen Datentypen geschuldet und versuchen, Konvertierungsverluste zu vermeiden. Sie lesen sich zunächst kryptisch, lassen sich aber einfach am folgenden Beispiel erklären:
Operand 1: unsigned int (hier 32-Bit)
Operand 2: long (hier 32-Bit / 64-Bit)
- Im Fall, dass der Datentyp
long
64-Bit breit ist, kann dieser problemlos die vorzeichenlose 32-Bit Zahl ohne Konvertierungsverluste repräsentieren, so dass der Zieltypesigned long
ist (Regel 6) - Im Fall, dass der Datentyp
long
32-Bit breit ist, kann dieser mit seinen Wertebereich von -2.147.483.648 bis +2.147.483.647 nicht den vorzeichenlosen Zahlenbereich von 0…4.294.967.295 darstellen. In diesem Fall hat der unsigned Datentyp einen höheren Rang, so dass als Zieltypunsigend long
gewählt wird(Regel 7)
Regel 7 bedeutet die Gefahr eines Konvertierungsverlustes, dessen man sich bewusst sein sollte! Diese tritt insb. dann in Kraft, wenn ein Operand vom Typ 'unsigned long long' ist (64-Bit).
Für die Konvertierung von Pointer, Arrays, Strukturen, Unions zu anderen Datentypen, als sich selbst gilt Regel 8. In diesem Fall gibt der Compiler zumeist einen Error, in wenigen Ausnahmefällen eine Warning aus:
char *string; //Pointer
int arr[3]; //Array
struct {int x,y;} var1; //Strukturen
union {int x,y;} var2; //Union
var1 = var2; //Error Incompatible Types
string=arr; //Error Incompatible Types
arr=var1; //Error Assignment to expression with array type
//(=incompatible Types)
string=var2; //Error Incompatbiles Types
Beispiele von impliziten Typumwandlungen:
char varc=100;
short vars=100;
int vari=100;
vars=varc + vars;
//Wird aufgrund der impliziten Regel wie folgt umgesetzt
vars=(short)((int)varc+(int)vars);
vari=5.0*(int)sin(vars);
//Wird aufgrund der impliziten Regel wie folgt umgesetzt
vari=(int)(5.0*(double)(int)sin((double)vars));
//Hinweis: da sin() nur Werte im Bereich -1…0…+1 zurückgibt kommen
//als Werte für a hier nur 5, 0 und -5 in Frage!
Hinweis:
- Im CompilerExplorer können sie sich die impliziten Typumwandlungen anzeigen lassen. Dazu gehen sie bitte wie folgt vor:
- Im Source-Fenster mit '+Add new' ein Compiler Fenster öffnen
- Im Compiler-Fenster mit '+Add new' ein 'GCC Tree/RTL' Fenster öffnen
- Im GCC Tree/RTL-Fenster unter 'Select a pass…' 'original tree' auswählen
Um implizite Typumwandlung besser erkennen zu können, wird oftmals bei Variablennamen die 'Ungarische Notation' angewendet (siehe Ungarische Notation). Aufgrund der besonderen Namensgebung kann der Programmierer ohne großen Aufwand frühzeitig mögliche Typkonflikte erkennen. Der Variablenname setzt sich wie folgt zusammen:
{Präfix}{Datentyp}{Bezeichner}
- Präfix: p->Pointer h->Handle i->index c->count f->flag rg->Array
- Datentyp: ch->Character st->string w->word b->byte…
- Bezeichner: Zum Binden der Variable an eine konkrete Aufgabe (keine Unterstriche).
- Bezeichner ist optional, wenn aus Präfix und Datentyp die 'Aufgabe' der Variable direkt sichtbar ist
Beispiele:
char rgchtemp[10]; //Array (Range) vom Datentyp character
int ich; //Index zum Adressieren eines Arrays vom Datentyp
//character
Explizite Typumwandlung
BearbeitenProgrammierer erzwingt durch explizite Typumwandlung (auch CASTen genannt) eine Umwandlung eines Datentyps in einen anderen. Dazu wird der Zieldatentyp in runden Klammern vor Quelldatentype geschrieben:
short a=4;
double b=(double)(a+1); //Das Ergebnis von (a+1) wird nach double gecastet
//(Addition erfolgt auf Basis des Datentyps.
//integer)
double c=(double)a+1; //a wird nach double gecastet, so dass nachfolgende
//Addition auf Basis von double basiert.
In der Rangfolge der Operatoren steht der Cast-Operator unterhalb von bspw. Funktionsaufrufen, Arrayzugriffen aber auch der Dereferenzierung:
Priorität | Symbol | Assoziativität | Bedeutung |
---|---|---|---|
15 | ... | ... | ... |
14 |
++/-- (Präfix) |
R-L |
Präfix-Inkrement/Dekrement |
13 |
|
L-R | Multiplikation/Division/Modulo |
12 | ... | ... | ... |
Im Zweifel gilt auch hier, den zu wandelnden Typ ergänzend zu klammern.
Hinweis:
- Bedenke, dass die explizite Typumwandlung so aufgestellt sein sollte, dass der Zieldatentyp dem notwendigen Datentyp entspricht. Andernfalls wendet der Compiler ergänzend eine implizierte Typumwandlung an:
int a; double b=4.7; short c1= (long)( (float)a+b ); //b ist vom Typ double, so dass a nach dem Cast auf float // implizit vom Compiler auf double gecastet wird //c ist vom Typ short, so dass das Ergebnis der Addtion nach dem expliziten //Cast auf long auf short gecastet wird. //Nach Anwendung der impliziten Cast sieht der Ausdruck wie folgt aus: short c2=(short)(long)( (double)(float)a+4.7);
Mittels expliziter Typumwandlung können Ganzzahl nach Gleitkommazahlen (und andersherum) und Zeiger in einen anderen Zeiger und auf andere Datentypen gewandelt werden. Eine Konvertierung von Arrays, Strukturen, Unions zu anderen Datentypen als sich selbst ist weiterhin nicht möglich:
char *string; //Pointer
int arr[3]; //Array
struct stru {int x,y;} var1; //Strukturen
union unio {int x,y;} var2; //Union
var1 = (struct stru) var2; //Error Conversion to non-scalar type
string=(char *)arr; //OK
arr=(int *)var1; //Error Assignment to expression with array type
string=(char *)var2; //Error cannot convert to pointer type
Bei den möglichen Konvertierungen sollte Folgendes berücksichtigt werden:
- Vorzeichenlose Ganzzahl → Vorzeichenlose Ganzzahl: Wenn der Zieldatentyp größer ist, wird die Zahl durch führende '0' erweitert. Wenn der Zieldatentyp kleiner ist, werde die zu vielen Bitstellen abgeschnitten/verworfen (ggf. Datenverlust).
- Vorzeichenbehaftete Ganzzahl → Vorzeichenbehaftete Ganzzahl: Wenn der Zieldatentyp größer ist, wird vorzeichenrichtig erweitertet (d.h. das Auffüllen erfolgt auf Basis des Vorzeichens). Wenn der Zieldatentyp kleiner ist, werden auch hier die zu vielen Bits abgeschnitten/verworfen (Vorsicht: Negative Zahlen können dabei in positive Zahlen gewandelt werden)
- Gleitkommazahl → Gleitkommazahl: Hier wird vereinfacht ausgedrückt sowohl die Mantisse als auch der Exponent einzeln kopiert. Wenn die Zielmantisse kleiner ist, gehen 'Nachkommastellen' verloren. Wenn der Quellexponent einen größeren Wert beinhaltet, als der Zielexponent 'aufnehmen' kann, so wird die Zahl auf Unendlich gesetzt. Umgedreht, wenn der Quellexponent kleiner ist, als der Zielexponent 'aufnehmen' kann, so wird die Zahl auf 0 gesetzt
- Ganzzahl → Gleitkommazahl: Hier wird, vereinfacht ausgedrückt, die Ganzzahl in die Mantisse kopiert. Um Datenverluste zu vermeiden, sollte die Bitbreite der Zielmantisse größer gleich der Bitbreite der Ganzzahl sein
- Gleitkommazahl → Ganzzahl: Im Wesentlichen wird hier die Mantisse übernommen, so dass hier die Bitbreite des Zieldatentyps mindestens der Bitbreite der Mantisse sein sollte
- Zeiger ↔ Ganzzahl: Die Konvertierung ist möglich. Da bei 64-Bit Systemen der Zeiger 64-Bit und Integer 32-Bit breit ist, meckert ggf. der Compiler. Für solche Fälle existieren die Datentypen 'intptr_t' und 'uintptr_t', über welche sichergestellt ist, dass ein Zeiger in einen Ganzzahldatentyp gespeichert werden kann!
- Zeiger ↔ Gleitkommazahl: Diese Konvertierung ist nicht möglich!
- Struktur/Union ↔ Ganzzahl/Gleitkommazahl: Diese Konvertierung ist nicht möglich. Es können nur einzelnen Strukturelemente konvertiert werden, sofern diese vom Typ Ganzzahl/Gleitkommazahl sind
- Array ↔ Ganzzahl/Gleitkommazahl: Der Arrayname entspricht einen Zeiger, so dass hier ein Zeiger in eine Ganzzahl oder umgedreht konvertiert wird (siehe Zeiger ↔ Ganzzahl)
In C++ gibt es weitere CAST-Operatoren, auf welche hier derzeit noch nicht weiter eingegangen wird!
- Static_cast (Entspricht dem Expliziten Cast)
- Const_cast
- Dynamic_cast
- Reinterpret_cast
Struktur/Verbundtyp
BearbeitenNach dem Wikipedia Artikel Verbund (Datentyp) ist ein Verbund (englisch object composition) ist ein Datentyp, der aus einem oder mehreren Datentypen zusammengesetzt wurde. Die Komponenten können wiederum Verbünde enthalten, wodurch auch komplexe Datenstrukturen definiert werden können.
Für jedes Strukturelement wird Speicher reserviert. Alle Strukturelemente liegen hintereinander im Speicher.
Syntax: struct StrukturnameOpt {Datentype Strukturelementname; … }OPT VariablenlisteOpt;
Ein Verbund entspricht einer Java Klasse mit dem Unterschied zu C (nicht C++) , dass ein Verbund keine Methoden beinhaltet und keine Zugriffsbeschränkung für die Attribute gesetzt werden können.
Der Syntax erlaubt es, gleichermaßen einen Datentyp (über Strukturname) zu definieren und Variablen von diesem Datentyp (über Variablenliste) anzulegen. Da beide Elemente optional sind, ergeben sich diverse Kombinationsmöglichkeiten:
- Nur Strukturname → Definition eines neuen Datentyps
struct xyz1 {int x; int y,z;}; //Definition eines neuen Datentyps struct xyz1 var1; //Definition einer Variablen dieses //Datentyps var1.x=7; //Zugriff auf ein Strukturelement
- C: Der Strukturname ist nur mit dem vorangestellten
struct
gültig. Der Strukturname stellt somit einen eigenen Namensraum, getrennt von dem Namensraum der Variablen/Funktionen, dar. Daher kann ein Strukturname identisch zu einem Variablennamen sein: struct xyz2 {int x,y,z;}; //Definition des Datentyps 'struct xyz2' struct xyz2 xyz2; //Definition der Variablen 'xyz2' auf //Basis des Datentyps 'struct xyz2' xyz2 var2; //Compilerfehler, zur Nutzung des Datentyps //'struct xyz2' muss struct vorangestellt //werden!
- C++: Der Strukturname ist sowohl mit als auch ohne dem vorangestellten
struct
gültig. Auch hier stellt der Strukturname einen eigenen Namensraum dar, der jedoch eine niedrigere Priorität als der der Variablen hat: struct xyz3 {int x,y,z;}; xyz3 xyz3={1,1,1}; //'xyz3' beschreibt hier den Datentypen xyz3.x=1; //und nach Definition einer Variable die Variable! xyz3 var2; //Fehler, xyz beschreibt hier die Variable
- Nur Variablenliste → Definition einer/mehrerer Variablen von einem 'unnamed' Datentyp
struct { int x,y,z; } xyz,arr[3],*ptr=&xyz; //Definition von Variablen des Datentyps //struct {...}. Da dieser Datentyp nicht benannt //wurde, kann im späteren keine weitere Variable //von diesem Datentyp angelegt werden. xyz.x =8; //Nutzung der Variablen arr[1].y =9; ptr->x = arr[1].y;
- Angewendet wird die Schreibweise gerne, wenn eine Struktur innerhalb einer Struktur definiert wird und die innere Struktur zur besseren Strukturierung dient:
struct aussen { //Äußere Struktur struct { //Innere Struktur int a,b,c; } anwendung1; struct { //Innere Struktur int x,y,z; } anwendung2; }; struct aussen var; var.anwendung1.a=10; //Nutzung der Variablen var.anwendung2.y=12;
- Strukturname und Variablenliste → Definition eines Datentyps und Definition von Variablen. D.h. es können im Nachhinein weitere Variablen von diesem Datentyp angelegt und die hier angelegten Variablen genutzt werden.
struct xyz{ //Definition eines Datentyps int x,y,z; } var1,*ptr1; //und Definition von Variablen dieses Datentyps struct xyz var2,*ptr2; //Definition weiterer Variablen dieses Datentyps var1.x=var2.y; //Nutzung der Variablen
- Kein Strukturname und keine Variablenliste → "Anonymous Struct", d.h. Definition eines 'unnamed' Datentyps, von der ergänzend keine Variable angelegt wird. Anonymus Structs sind nur innerhalb von Strukturen erlaubt/einsetzbar. Hier dienen sie der besseren Strukturierung und ersparen dem Programmierer die Benennung des normalerweise notwendigen Strukturelementes:
struct außen { int a; struct { int b,c,d; } anwendung1; //'Normale' innere Struktur struct { int x,y,z; }; //Anonyme Struct als innere Struktur } var; var.a=4711; //Nutzung der Variablen var.anwendung1.b=1;//Bei Zugriff auf anonyme Struktur ist die Benennung //des Strukturelementes notwendig var.x=1; //'Einfacherer' Zugriff auf inneres Element bei //'anonymous struct'
Hinweise:
- Eine Struktur (und auch eine C++ Klasse) wird mit einem Semikolon abgeschlossen
- Eine alternative Beschreibung zu Strukturen finden sie in Structures in C: From Basics to Memory Alignment
Strukturelement
BearbeitenInnerhalb der Struktur können beliebige Strukturelemente von beliebigen Datentypen angelegt werden. Einzige Voraussetzung, der Datentyp des anzulegenden Strukturelementes muss zuvor definiert worden sein. Ergänzend kann der neue Datentyp auch in der Struktur selbst definiert werden, sofern mit der Definition des Datentyps auch eine Variable angelegt wird:
struct abc {
int a,b,c;
}; //Definition des Datentyps struct abc
struct xyz { //Definition des Datentyps struct xyz
int x,y,z; //Strukturelement vom Type int
struct abc abc; //Strukturelement vom Type struct abc
struct def { //Definition eines neuen Datentyps innerhalb
int d,e,f; //der Struktur bei gleichzeitigen Anlegen zweier
} def,geh; //Strukturelementen von diesem neuen Datentyp
struct uvw {
int u,v,w; //Warning, reine Datentypdefinition innerhalb
}; //einer Struktur nicht möglich
};
struct def abc; //Innere Datentypbeschreibung auch
//außerhalb der Struktur nutzbar
Zugriff auf Strukturelemente
BearbeitenDie Programmiersprache C unterscheidet zwei 'Zugriffsarten' auf die Strukturelemente, abhängig vom zugrundeliegenden Datentyp:
- Handelt es sich beim zugrundeliegenden Datentyp um eine (struct)Variablen, so erfolgt der Zugriff über den Punkt-Operator
.
:
struct { int a,b,c; } abc; abc.a=4711; //abc ist eine Variable einer Struktur abc.b=abc.c;
- Handelt es sich beim zugrundeliegenden Datentyp um einen Zeiger auf eine (struct)Variable, so erfolgt der Zugriff über den Zeiger-Operator
->
:
struct xyz { int x,y,z; }; struct xyz var1; //Variable von Datentyp struct xyz; struct xyz *ptr1; //Zeiger auf eine Struktur vom Datentyp struct xyz ptr1=&var1; //Initialisierung des Zeigers //(mit der Adresse der Variablen var1) var1.x= 7; //Zugriff auf das Strukturelement x über die Variable var1 ptr1->x=7; //Zugriff auf das Strukturelement x über den Zeiger ptr1 (*ptr1).x=7; //Der Zeiger-Operator entspricht dem Zugriff auf ein //dereferenziertes Strukturelement
Werden innerhalb von Strukturen weitere Strukturen und Zeiger auf Strukturen angelegt, so müssen diese Regeln auf jedes innere Strukturelement einzeln angewendet werden:
struct abc { //Äußere Struktur
int a,b,c;
struct xyz {
int x,y,z;
} xyz; //Inneres Strukturelement xyz vom Datentyp struct xyz
struct {
char str[10];
} strstr; //Inneres Strukturelement strstr vom Datentyp
//struct strstr
struct xyz *ptr; //Inneres Strukturelement ptr vom Datentyp Zeiger
//auf struct xyz
} var2,*ptr;
var2.a=8; //var2 vom Datentyp 'struct abc'. Zugriff über '.'
var2.xyz.x=1; //xyz vom Datentyp 'struct xyz'. Zugriff über '.'
var2.strstr.str[0]='a';//strstr vom anonymes Struct. Zugriff über '.'
ptr=&var2; //ptr mit einer Adresse initialisieren
var2.ptr = &var2.xyz; //Strukturelement ptr mit einer Adresse initialisieren
ptr->a='a'; //ptr vom Datentyp 'Zeiger auf struct abc'
//Zugriff über '->'
var2.ptr->x=3; //ptr vom Datentyp 'Zeiger auf struct xyz'.
//Zugriff über '->'
ptr->xyz.x=3;
ptr->ptr->x=3;
Initialisierung von Strukturen
BearbeitenMit dem Anlegen einer Strukturvariablen kann diese auch initialisiert werden. Eine 'Vorbelegung' der Strukturelemente bei der Definition der Struktur ist in C nicht möglich.
Die Initialisierung erfolgt über eine Initialisierungsliste (siehe Kap Grundlagen:Initialisierungsliste / Compound Literal). Innerhalb der Initialisierungsliste stehen die Initialisierungswerte entweder in der Reihenfolge der Datentypdefinition oder werden explizit mit dem ‘.‘ Operator angesprochen (designated initializers). Werden einzelne Strukturelemente ausgelassen, so werden diese mit 0 initialisiert:
struct abc{
int a,b;
int c=3; //KO: Kein Initialisierungswert von Strukturelementen möglich
char str[10];
};
struct abc v1={1,2,"hallo"}; //Alle Strukturelemente werden initialisiert
struct abc v2={ 1,2 }; //Initialisierung der Strukturlemente a und b
//Rest wird mit 0 initialisiert
struct abc v3={ .b=3}; //Initialisierung einzelner/designated Strukturele.
//Rest wird mit 0 initialisiert
struct abc v4={.a=strlen(v1.str)}; //Initialisierungswert ergibt
//sich erst zur Laufzeit.
//Nur als lokale Variable möglich!
struct abc v5={.b=1,.a=2,.b=12}; //Nur C: Reihenfolge und Doppelbenennung
//der Strukturelemente egal/möglich
struct abc v6={}; //Alle Strukturelemente mit 0 initialisieren
Eine Initialisierung einer Struktur über nachfolgende Art bewirkt etwas anderes, als erwartet:
struct abc { int a,b,c; };
struct abc var={var.b=12,var.c=3};
Innerhalb der Initialisierungsliste wird über 'var.b=12' und 'var.c=3' die zu diesem Zeitpunkt noch nicht initialisierte Variable var die Elemente b und c initialisiert. Mit dem Werten der Initialisierungsliste (hier {12,3}) wird nachfolgend die Variable erneut initialisiert und 12 dem Strukturelement a und 3 dem Strukturelement b zugewiesen. Abhängig vom Compiler ist das Strukturelement c entweder 3 oder 0.
Bei verschachtelten Strukturen muss entsprechend obiger Aussage für jede innere Struktur eine eigene Initialisierungsliste erstellt werden. Die inneren Initialisierungslisten können entfallen, was jedoch nicht empfohlen wird und ergänzend vom Compiler bei '-Wall' als Warning angemerkt wird:
struct abc {
int a,b;
struct xy {
int x,y;
} xy;
struct xy arr[2];
};
struct abc v1={ //Initialisierung über Reihenfolge
1,2,
{3,4},
{ {5,6},{7,8}}
};
struct abc v2={ //Initialisierung über 'designated initializers'
.arr={{.x=5,.y=6},{7,8}},
.xy={3,4},
.a=1,.b=2
};
struct abc v3={ //Fehlende innere Initialisierungsliste
1,2,
3,4,
5,6,7,8
};
Zuweisen/Kopieren von Strukturen
BearbeitenStrukturen werden über den Namen als 'ganzes' angesprochen, so dass eine Zuweisung als 'ganzes' möglich ist. Bei bspw. einer 12-Byte großen Struktur werden bei Nutzung der Variable die gesamten 12-Byte gelesen/geändert!
struct xyz {int x,y,z;} var1,var2;
var1=var2; //12-Bytes kopiert
//--> entspricht memcpy(&var1,&var2,sizeof(xyz));
Die Zuweisung als 'ganzes' gilt auch bei der Parameterübergabe und -rückgabe von Funktionen:
struct xyz {int x,y,z;};
//Funktionsdefinition
struct xyz function(struct xyz par) {
return (struct xyz){par.z,par.y,par.x};
}
//Funktionsaufruf
struct xyz var=function((struct xyz){1,2,3});
//Mit dem Aufruf der Funktion werden 12-Bytes in die Variable par kopiert
//Mit dem Ende der Funktion werden 12-Bytes in die Variable var kopiert
Bedenke, dass bei großen Strukturen das Kopieren Rechenzeit benötigt und folglich vermieden werden sollte! Alternativ sollten Zeiger auf Strukturen über- und zurückgeben werden.
Über Compound Literal (siehe Kap. Grundlagen:Initialisierungsliste / Compound Literal) kann einer Strukturvariablen als 'ganzes' einen neuen Wert zugewiesen werden:
struct abc {int a,b,c;};
var1={.a=7}; //KO, Initialisierungsliste kann hier nicht
//angewendet werden
var1=(struct abc){.a=7}; //Wertezuweisung über Compound Literal möglich
Formel gesehen entspricht das Compound Literal einer 'unnamed Variable', die als erstes angelegt und initialisiert wird. Der Inhalt dieser Variablen wird im Anschluss der eigentlichen Variable zugewiesen!
Prototyp / Deklaration einer Struktur
BearbeitenSoll der Datentyp einer Struktur genutzt werden, der erst an weiter hinten liegenden Stellen im Programm definiert wird, so ist wie bei Variablen/Funktionen ein Prototyp/Deklaration notwendig:
struct abc; //Prototyp der Struktur abc
...
struct abc { int a,b,c;}; //Definition der Struktur abc
Die Deklaration sagt einzig aus, dass die Struktur später definiert wird, beinhaltet aber nicht die Strukturelemente. Folglich kann auf Basis dieses Prototyps keine Variable definiert werden, sondern einzig ein Zeiger auf solche eine Struktur:
struct xyz; //Deklaration / Prototyp des Datentyps
// d.h. keine Benennung der Strukturelemente
struct xyz var1; //KO, es kann auf Basis der Deklaration keine
// Variable angelegt werden
struct xyz *pttr1; //OK, von einer Strukturdeklaration kann ein Zeiger
// angelegt werden!
Anwendung:
- Struktur, welche auf sich selbst verweist (Verkettete Liste)
struct vl { //Entspricht gleichermaßen einem Prototyp, so dass //innerhalb dieser Struktur dieser Datentyp zum //Anlegen eines Strukturelementes genutzt werden kann struct vl *next; //Zeiger auf das nächste Element char daten[100]; };
- Entsprechend der objektorientierten Programmierung, wenn alle Attribute der Klasse private sind:
class.h struct class; //Prototyp für Struktur //so dass Nutzer dieser //Struktur ein Zeiger auf diese anlegen, //aber nicht auf die Strukturelemente //zugreifen können. //Prototyp der public Methoden void konstruktor(struct class ** me);
main.c class.c #include "class.h" int main(int argc, char*argv[]) { struct class *obj1; konstruktor(&obj1); .. //Kein Zugriff auf //Strukturelemente möglich return 0; }
#include "class.h" struct class { int attr1; int attr2; }; void konstruktor(struct class ** me) { struct class *this; this=(struct class *) malloc(sizeof(struct class)); this->attr1=10; *me=this; }
Vergleichen von Strukturen
BearbeitenEin Vergleich von Strukturen über den direkten Weg ist nicht möglich. Vielmehr müssen die Strukturelemente händisch verglichen werden:
struct xyz {int inx,y,z;} var1,var2;
if(var1 == var2) //KO, ein Vergleich von Strukturvariablen ist nicht möglich
//Manueller Vergleich über den Vergleich der Strukturelemente
if(var1.x == var2.x && var1.y == var2.y && var1.z == var2.z)
Hinweis
- Bei Rechnerarchitekturen mit nicht ausgerichteter Speicherausrichtung (Alignment) kann ergänzend ein Vergleich über memcmp() erfolgen
Speicherplatzbedarf einer Struktur
BearbeitenFür jedes Strukturelement wird Speicher entsprechend der Größe des Datentyps reserviert. Der Speicherplatzbedarf der Gesamtstruktur ergibt sich aus der Summe der Strukturelemente:
struct xyz{
int x; //Strukturelement belegt 4 Byte Speicher
int y; //Strukturelement belegt 4 Byte Speicher
int z; //Strukturelement belegt 4 Byte Speicher
}var1; //Speicherplatz der Struktur = 4+4+4 = 12 Byte
sizeof(var1) //ergibt 12
sizeof(struct xyz) //ergibt 12
Die Ermittlung der tatsächlichen Speichergröße einer Struktur erfolgt über den sizeof-Operator:
struct xyz{int x,y,z;} a;
sizeof(a); //=12 OK
sizeof(a.x); //=4 OK
sizeof(struct xyz); //=12 OK
sizeof(struct xyz.x); // KO
//Ist dies dennoch notwendig, so kann dies über den Umweg eines Zeigers
//erfolgen:
sizeof(((struct xyz *)0)->x); //=4 OK
Hinweis:
- Bei Rechnerarchitekturen mit ausgerichteter Speicherausrichtung (Alignment) (siehe Speicherausrichtung) kann der tatsächliche Speicherbedarf größer sein! Hier fügt der Compiler ggf. zwischen den Strukturelemente Füllbytes (Padding Bytes) ein
- Die Reihenfolge der Strukturelemente wird vom Compiler nicht geändert. D.h. die Zuordnung der Speicherstellen zu den Strukturelementen erfolgt in der Definitionsreihenfolge
Interne Organisation
BearbeitenCompilerintern werden die Strukturelemente über einen Offset dargestellt. Das erste Strukturelement bekommt dabei immer den Offset 0 zugewiesen. Das nachfolgende Element bekommt als Offset die Größe des Datentyps des vorherigen Elementes zugewiesen. Usw..
Der Compiler setzt den Zugriff auf ein Strukturelement einer (Struktur)Variablen so um, dass er zunächst die Startadresse der Variablen holt und zu dieser den Offset des Strukturelementes addiert. Die Anzahl der zu lesenden/schreiben Bytes ergibt sich aus dem Datentyp des Strukturelementes:
struct xyz { //Das Anlegen der Variable var bedeutet,
int x,y,z; //12Byte Speicher zu reservieren.
} var1; //(Beispielhaft soll var1 die Speicheradressen
//0x100..0x10B belegen). Der Zugriff auf
//die Variable erfolgt immer über die Startadresse
var1.x=7; //Zugriff auf Speicheradresse 0x100+0 (Offset)
var1.y=8; //Zugriff auf Speicheradresse 0x100+4 (Offset)
var1.z=9; //Zugriff auf Speicheradresse 0x100+8 (Offset)
Der Offset eines Strukturelementes kann mit dem offsetof-Operator ermittelt werden:
Syntax: offsetof(type,Strukturelement)
Wie beim sizeof-Operator gilt der offsetof-Operator als Konstantenausdruck und der Rückgabedatentyp ist size_t
. Zur Nutzung des Operators muss die Header-Datei 'stddef.h' inkludiert werden:
#include <stddef.h> //Zur Nutzung des OffsetOf Operators
struct xyz {int x,y,z;} var1;
offsetof(struct xyz,z) --> 8
offsetof(var1,z) --> KO: Es wird ein Datentyp und
keine Variable erwartet
Explizites Cast
BearbeitenEine Struktur kann nicht in einen anderen Datentyp und damit auch nicht in einen anderen Strukturdatentyp mit identischen Strukturelementen konvertiert werden:
struct xyz {int x,y,z;} xyz;
struct abc {int a,b,c;} abc;
int var;
xyz=abc; //KO, Datentyp struct xyz != struct abc
xyz=(struct xyz)abc; //KO, siehe zuvor
int neu1 = (int) xyz; //KO
int neu2 = (int) xyz.x; //OK (Das Strukturelement x ist vom Datentyp int)
struct xyz neu2 = (struct xyz)3; //KO
Incomplete Array
BearbeitenDas letzte Element einer Struktur kann ein Array ohne Angabe einer Dimensionsgröße sein (Incomplete Array/Flexible Array Member). Bei der Definition einer Variablen von solch einem Datentyp wird in der Tat kein Speicher für das letzte Element reserviert, so dass dies 'händisch' erfolgen muss. Auf das Array kann ungeachtet dessen unproblematisch zugegriffen werden:
struct vl {
struct vl *next;
char data[]; //incomplete Array / flexible Array Member
};
struct vl a; //Es wird nur Speicher für den next Zeiger reserviert
a.data[0]='7'; //Syntax OK, jedoch erzeugt dies ein Laufzeitfehler,
//da kein Speicherplatz für das Incomplete Array reserviert
//wurde
Die Speicherplatzreservierung für das incomplete Array kann auf zwei verschiedene Arten erfolgen:
- Speicherplatzreservierung für das Incomplete Array über Variableninitialisierung (nur GCC-C und nur im Falle einer globalen Variablen)
struct vl ele1 = { .next = NULL, .data = {32, 31, 30} //Durch Initialisierung der einzelnen Array Elemente }; //Vorsicht, sizeof(ele) gibt dennoch nur 4/8 zurück struct vl ele2 = { .next = NULL, .data[3-1] = 0 //Durch Initialisierung des letzten Array Elementes }; //Vorsicht, sizeof(ele) gibt dennoch nur 4/8 zurück
- Speicherplatzreservierung für das Incomplete Array über malloc
struct vl *b=malloc(sizeof(struct vl)+ //Größe der Struktur sizeof(char[3] )); //Größe des Arrays b->data[1]='7'; //OK, da mit dem Anlegen von b Speicher für 2 //data Element reserviert wurde
Das Incomplete Array bietet sich überall dort an, wo Daten gespeichert werden sollen, deren Größe sich erst zur Laufzeit ergibt.
Anwendung
BearbeitenDie 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++
BearbeitenDer wesentliche Syntax von Strukturen wurde in C++ übernommen. D.h.:
- das abschließende Semikolon
- der Zugriff auf die Strukturelemente
- Speicherplatzreservierung
- die Definition von Variablen mit der Definition des Datentyps
- ...
Ergänzend wurde in C++ der Datentyp class eingeführt, der weitestgehend identisch zu struct ist. Geändert/Hinzugefügt wurden folgende Sachverhalte:
- Classen/Strukturen können Methoden haben
- Operatoren (Zuweisungsoperator, Addition, ...) können überladen werden
- Alle Strukturelemente (und Methoden) einer struct sind per default public
- Alle Attribute (und Methoden) einer Class sind per default private
- Zur Nutzung des Datentyps ist das führende struct nicht notwendig (dies bedingt dann auch, dass der Strukturdatentyp kein separater Namensraum ist)
struct xyz { int x,y,z; }; xyz var_xyz;
- Initialisierungswerte für Strukturelemente und Klassenattribute bei der Datentypbeschreibung angegeben werden können:
struct xyz { int x=3; int y=1; int z; }; xyz var={7}; //var.x=7 var.y=1 var.z=0
- In C++ kann eine Struktur ebenfalls über 'designated initializers' initialisiert werden. Jedoch muss die Reihenfolge der Initialisierungselement identisch zur Datentypdefinition sein. Auch ist keine Doppeltbenennung erlaubt:
struct xyz { int x,y,z; }; xyz var1={.x=1, .y=2, .z=3}; //OK, Reihenfolge identisch xyz var2={.y=1, .z=3}; //OK, Reihenfolge identisch xyz var3={.y=1, .x=0}; //KO, Reihenfolge nicht identisch xyz var4={.x=1, .x=2}; //KO, keine Doppeltbenennung möglich
Union
BearbeitenÄhnlich wie eine Struktur ist ein Union ein Datentyp, der aus einem oder mehreren Datentypen zusammengesetzt wird. Bei sog. Union beginnen jedoch alle Komponenten nicht wie bei Strukturen an nacheinander folgenden Speicheradressen, sondern an der identischen Speicheradresse, d.h. ihre Speicherbereiche überlappen sich ganz oder zumindest teilweise. Eine Union kann folglich zu einem Zeitpunkt nur ein Element enthalten. Der benötigte Speicherplatz ergibt sich aus der größten Komponente.
Syntax: union UnionnameOpt {Datentyp Variablenname; …}Opt VariablenlisteOpt;
Ein Schutz/Zugriffssteuerung der Unionelemente ist nicht vorhanden. Es kann in beliebiger Reihenfolge auf die einzelnen Elemente zugegriffen werden.
Die Anwendung ist identisch wie bei Strukturen, so dass im Folgenden nur die Abweichungen zu den Strukturen beschrieben werden.
Speicherplatzbedarf und interne Struktur einer Union
BearbeitenDie Größe der Union ergibt sich aus dem größtem Unionelement. Alle Unionelemente überlappen sich, so dass der Offset zum Basiselement 0 ist:
union abc {
char a;
short b;
int c;
} abc;
printf("Größe: %zu\n",sizeof (union abc)); //-> 4
printf("Offset a:%zu\n",offsetof(union abc,a)); //-> 0
printf("Offset b:%zu\n",offsetof(union abc,b)); //-> 0
printf("Offset c:%zu\n",offsetof(union abc,c)); //-> 0
Wird ein kleineres Unionelement belegt und im Anschluss ein größeres Unionelement gelesen, so ist der Inhalt der durch das kleinere Element nicht beschriebenen Speicherstellen undefiniert:
union abc {
char a;
short b;
int c;
} abc;
abc.a=0x11;
printf("%08x",abc.c);
Initialisierung von Union
BearbeitenBei einer Union kann nur ein Element initialisiert werden, d.h. die Initialisierungsliste kann nur einen Wert beinhalten. Ohne explizite Benennung des Unionelementes in der Initialisierungsliste wird das erste Element aus der Datentypbeschreibung initialisiert. Mit expliziter Benennung mittels 'designated initializers' können auch andere Elemente initialisiert werden:
union abc {
char a;
short b;
int c;
} abc;
abc=3; //KO keine Initialisierungsliste
abc=(union abc){3}; //OK Initialisierung des ersten Elementes a
abc=(union abc){.b=3}; //OK Initialisierung des Elementes b
abc=(union abc){3,4}; //KO nur ein Initialisiuerngswert erlaubt
Anwendung
BearbeitenMittels des Datentyps union können diverse Anwendungsfälle abgedeckt werden:
- Über Union lassen sich größere Datentypen in kleinere Datentypen unterteilen, um z.B. einzelne Bytes zu extrahieren:
union floatu {
float var; //Float ist 4-Byte groß
unsigned int hex; //Integer ist 4-Byte groß
char byte[4]; //Ohne Worte
};
union floatu var={1.234};
printf("%f\n",var.var); //Darstellung des Float-Wertes
printf("%x\n",var.hex); //Darstellung des Float-Wertes 'BinäreZahl'
printf("%hhx %hhx %hhx %hhx\n", //Darstellung der einzelnen Bytes
var.byte[0],var.byte[1],var.byte[2],var.byte[3]);
union longlong {
long long ll; //Long Long ist 8-Byte groß
int i[2]; //Ohne Worte
short s[4];
char c[8];
};
union longlong var2={0x123456789ABCDEFULL};
printf("%llx\n",var2.ll);
printf("%x %x\n",var2.i[0],var2.i[1]);
//Vorsicht: Die Aufteilung der Bytes ist von der Rechnerarchitektur
//(Endianes) und von der Ausrichtung durch den Compiler abhängig
- Ein weiterer Anwendungsfall ergibt sich, wenn in einer Struktur unterschiedliche Datensätze gespeichert werden sollen (siehe Tagged Union):
struct set { char *index; char *value; }; struct get { char *index; char *value; }; struct cli{ //Enum zur Darstellung des aktiven Unionelementes hilfreich enum {SETTER,GETTER} tag; union { //Interpretation abhängig von tag struct set set; struct get get; } ; }; struct cli var={.tag=SETTER, .set.index="hallo"}; if(var.tag==SETTER) printf("Set: %s",var.set.index); else printf("Get: %s",var.get.index);
- Mittels eines Tagged Union kann komfortabel ein Binärbaum realisiert werden:
//Quelle: https://github.com/Hirrolot/datatype99 struct BinaryTree; //Prototyp struct BinaryTreeNode { struct BinaryTree *lhs; int x; struct BinaryTree *rhs; } BinaryTreeNode_t; struct BinaryTree { enum { Leaf, Node } tag; union { int leaf; struct BinaryTreeNode node; } data; }; int sum(const struct BinaryTree *tree) { switch (tree->tag) { case Leaf: return tree->data.leaf; case Node: return sum(tree->data.node.lhs) + tree->data.node.x + sum(tree->data.node.rhs); } return -1; // Invalid input (no such variant). }
C++
BearbeitenDie Eigenschaften des Datentyps union entsprechen den Eigenschaften des Datentyps struct in C++.
Enum/Aufzählungstyp
BearbeitenNach dem Wikipedia Artikel Aufzählungstyp ist ein Aufzählungstyp (englisch enumerated type) ein Datentyp für Variablen mit einer endlichen Wertemenge. Alle zulässigen Werte des Aufzählungstyps werden bei der Deklaration des Datentyps mit einem eindeutigen Namen (Identifikator) definiert, sie sind Symbole.
Syntax: enum enumnameOpt {definition-list[=expression]}Opt VariablenlisteOpt
Die Anwendung ist identisch wie bei Strukturen, so dass nachfolgend nur die Abweichungen zu den Strukturen beschrieben werden.
Datentyp von enum
BearbeitenIn C entspricht der Datentyp Enum dem Datentyp int
so dass Zuweisungen/Vergleiche mit Ganzzahlen möglich sind. Ganzzahloperationen funktionieren ebenso:
enum STATUS {OK,KO=5}; //Definition des Datentyps
enum STATUS status; //Definition einer Variable dieses Datentyps
status=OK;
if(status == 5)
status++;
status=5.7;
Enumelemente
BearbeitenDie Elemente der Definitionsliste sind Integerkonstanten. Diese können auch in Kombination mit anderen Datentypen genutzt werden. Auch gibt es keinen gesonderten Namensraum für Elemente der Definitionsliste. Sie 'konkurrieren' folglich mit allen Variablen- und Funktionsnamen:
enum mode {OK, KO};
enum {FIRST,SECOND,LAST}; //Anonymes Enum!
enum {EINS,ZWEI,LAST}; //KO, Symbolname LAST bereits vergeben
enum mode var1=OK;
var1=4711; //OK, enum entspricht Datentyp int
int var2=KO; //OK, EnumElement entspricht Datentyp int
int OK=KO; //KO, Symbolname OK bereits für EnumElement vergeben
var1=FIRST; //OK, var1 und FIRST sind zwar unterschiedlich
//Enumtypen,jedoch entspricht beides dem Datentyp int
Der erste Enumelement der Definitionsliste bekommt den Wert 0 zugewiesen. Folgeelemente bekommen den Wert des Vorgängerelementes +1 zugewiesen. Enumelemente können mit einer Konstanten (und Konstantenausdruck) initialisiert werden:
enum {bill=10, john=bill+2, fred=john+1} ;
//Negative Werte sind möglich
enum {error=-3, warning, info, ok}; //warning=-2 info=-1 …
//Doppelte Wertzuweisung sind möglich
enum {Ostfalia=10,Wolfenbuettel=10};
//Hinter dem letzten Enumelement kann ein Komma folgen
enum {test=10,};
Hinweis:
- Es empfiehlt sich, die Enumelemente in GROSSBUCHSTABEN zu schreiben. Hiermit wird gekennzeichnet, dass es sich um Konstanten und nicht um Variablen/Funktionen handelt
Anwendung
BearbeitenDie Nutzung des Datentyps enum empfiehlt sich überall dort, wo eine Fallunterscheidung notwendig ist:
- Statusrückgabe von Funktionen:
enum STATUS {OK,MEMORY_OVERFLOW, DIVISION_BY_ZERO}; enum STATUS funct(…) { return MEMORY_OVERFLOW; }
- Beschreibung der Zustände und der Ereignisse eines Zustandsautomates:
enum EREIGNIS {EREIGNIS1,EREIGNIS2,EREIGNIS3}; void zustandsautomat(enum EREIGNIS ereignis) { static enum {IDLE,OPERAND,OPERATOR,} zustand=IDLE; switch(zustand) { case IDLE: case OPERAND: //Compiler meckert bei fehlendem Case über OPERATOR } }
C++
BearbeitenErgänzend zu den Abweichungen bei Structs sind hier weitere Unterscheidungsmerkmale vorhanden:
- Enums stellen einen eigenen Datentyp dar, der nicht mit int kompatible ist. Das bedingt unter anderem, dass einige Operatoren wie z.B. ++ nicht mehr auf Variablen des Datentyps enum angewendet werden. Die Enumelemente als solches sind jedoch kompatibel zum Ganzzahldatentyp:
enum Color {RED,GREN,BLUE}; enum Color var1=RED; var1=4; //KO var1=var1+1; //KO var1++; //KO int var2=RED; //OK if(RED==2) //OK
- Auch enums können Methoden besitzen.
- Über Operatorüberladung können z.B. nicht vorhandene Operatoren wie ++/-- ergänzt werden
- Über unscoped/scoped Enumerations wird der 'Namensraum' gesteuert:
- Unscoped Enumeration (Elemente der Definitionsliste stehen wie bei C im Zugriff)
Syntax: enum nameOpt : typeOPT {enumerator=constexp, …};
enum Color {RED,GREN,BLUE }; Color r = RED;
- Scoped enumeration (Elemente der Definitionsliste haben einen eigenen 'Namensraum')
Syntax: enum class|struct nameOpt : typeOPT {enumerator=constexp, …};
enum class Color {RED,GREN=20,BLUE }; Color r = Color::BLUE;
Bitfelder
BearbeitenNach dem Wikipediaartikel Bitfeld bezeichnet in der Informationstechnik und Programmierung ein Bitfeld ein vorzeichenloses Ganzzahldatentyp, in dem einzelne Bits oder Gruppen von Bits aneinandergereiht werden. Es stellt eine Art Verbunddatentyp auf Bit-Ebene dar. Im Gegensatz dazu steht der primitive Datentyp, bei dem der Wert aus allen Stellen gemeinsam gebildet wird.
Der Syntax entspricht dem struct-Syntax, mit der Ergänzung, dass hinter den Strukturelementen noch die Bitbreite getrennt durch ein Doppelpunkt angegeben wird:
struct time {
unsigned int hour: 5; //0..23 Bits 0..4
unsigned int minute: 6; //0..59 Bits 5..10
Unsigend int second: 6; //0..59 Bits 11..16
} myTime;
Als mögliche Datentypen für die Strukturelemente stehen nur die Ganzzahldatentypen int, short, long , long long und char zur Verfügung. Der Datentyp eines Strukturelementes muss mindestens die Anzahl der Bits enthalten, wie diese über die Bitbreite gefordert wird.
Von den Strukturelementen kann keine Adresse bestimmt werden! Auch der offsetf
- Operator schlägt fehl.
Der Datentyp entspricht im Wesentlichen einem Ganzzahlzahldatentyp, so dass bei Zugriff auf eine Variable dieses Datentyps eine Ganzzahl gelesen/geschrieben wird. Bei Zugriff auf die einzelnen Strukturelemente werden die entsprechenden Bits dieser Ganzzahlvariablen maskiert, so dass nur die betroffenen Bits geändert werden:
struct test {
unsigned char first:2;
unsigned char second:4;
unsigned char third:2;
};
struct test var={.first=1,.second=1,.third=1};
//entspricht
unsigned char var1=0b01000101;
var.second=1;
//entspricht:
var1=(var1&0b11000011) | (1<<2);
var.second++;
//entspricht
int second =(var1>>2)&0b00001111;
second++;
var1=(var1&0b11000011) | ((second&0b00001111)<<2);
Anwendung
Bearbeiten- Komprimierte Speicherung der Uhrzeit (wie dies z.B. in Realtimeclocks erfolgt)
union { struct { //Datentyp zum 'Bitweisen' Zugriff unsigned int hour:5; unsigned int minute:6; unsigned int second:6; }; unsigned int hex; //Datentyp zum Zugriff aller Elemente } myTime; myTime.hour = 13; myTime.minute = 37; myTime.second = 59; printf("Zeit: %02d:%02d:%02d\n", myTime.hour,myTime.minute,myTime.second ); //Alternativer/Händischer Zugriff auf die einzelnen Bits printf("Es ist jetzt %02d:%02d:%02d Uhr\n",((myTime.hex>> 0)&0b011111), ((myTime.hex>> 5)&0b111111), ((myTime.hex>>11)&0b111111));
- Aufteilung von FloatingPoint Zahlen in seine Komponenten
union float_mes { float flo; struct ieee { //aus ieee754.h kopiert #if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ unsigned int negative:1; unsigned int exponent:8; unsigned int mantissa:23; #endif /* Big endian. */ #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ unsigned int mantissa:23; unsigned int exponent:8; unsigned int negative:1; #endif /* Little endian. */ } ieee; int hex; }; //aus ieee754.h kopiert #define IEEE754_FLOAT_BIAS 0x7f /* Added to exponent. */ union float_mes test={0.1}; printf("float: %f\n",test.flo); printf("hex: %x\n",test.hex); printf("bin: "); for(unsigned int flag=0x80000000; flag; flag=flag>>1) printf("%c%s",test.hex&flag?'1':'0', flag&0x80800000?":":(flag&1?"\n":"")); printf("bitfield: (%c)%x * 2^(%d-%d)\n",(test.ieee.negative==1?'-':'+'), test.ieee.mantissa, test.ieee.exponent, IEEE754_FLOAT_BIAS);
C++
BearbeitenBitfelder existieren mit den bekannten Ergänzungen auch in C++.
Datatype-/Storage Class Specifier
BearbeitenTypedef Storage Class Specifier
BearbeitenAnweisung zum Erstellen eines Alias für einen Datentyp.
Syntax: typedef T type_ident[,type_ident];
T
ist der zu ersetzende Datentyp. Type_ident
ist der Alias, wobei auch mehrere mit Komma separierte Aliase gesetzt werden können. Der Syntax entspricht einer normalen Variablendefinition mit vorangestellten typedef
. Aufgrund des vorangestellten wird nun keine Variablen angelegt, sondern der 'Variablenname' ist der Alias.
Auf Basis des Alias können in Anschluss beliebig viele Variablen definiert werden:
typedef unsigned long long ull;
ull var1;
typedef signed long sl1,sl2,*psl;
sl1 var2; //--> signed long var2;
psl var3; //--> signed long *var3;
struct xyz {
ull x,y,z;
};
typedef struct xyz xyz_t;
xyz_t var4; //--> struct xyz var4;
typedef struct abc { int a,b,c;} abc_t;
abc_t var5; //Definition über Alias
struct abc var6; //Definition über struct abc
typedef struct {int d,e,f;} def_t; //Unnamed Struct
def_t var7;
typedef int arr_alias[4];
arr_alias arr;
typedef void func_alias(void); //Alias für eine Funktion
func_alias *func; //Zeiger auf Funtion
Anwendungsfälle von typedef sind:
- Source-Code unabhängig von der Rechnerarchitektur zu entwickeln:
//Bitbreite des Datentyps int ist von der Rechnerarchitektur abhängig #if ARCHITEKTUR == 16 typedef unsigned int uint16_t; #elif ARCHITEKTUR == 32 typedef unsigned short uint16_t; #endif //Diese Definition sind in der Header-Datei stdint.h enthalten!
- Weniger Tipparbeit:
//Vermeidung des führende struct/union/enum typedef enum {OK,KO,var11} STATUS_T; STATUS_T var1; //Vermeidung der Angabe weiterer Specifier typedef const volatile unsigned int cvuint16_t; cvuint16_t var2=4711;
- Dem Datentyp über seinen Namen eine Bedeutung zuzuordnen:
typedef char * cpointer; typedef struct { int raeder; enum {ROT,GELB}farbe;} auto_t; typedef void (*fptr_t)(void);
Hinweise:
- Zur Verdeutlichung, dass es sich bei dem Alias nicht um eine Variable oder Funktion handelt, wird oftmals der Suffix '_t' an den Alias angehängt
- Der Alias konkurriert mit Variablen und Funktionsnamen, kann also nicht doppelt benutzt werden
- Wird ein Typedef innerhalb eines Blockes definiert, so ist dieser nur innerhalb des Blockes gültig
Internal/External Linkage
BearbeitenC-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
BearbeitenMit dem Storage Class Specifier extern
wird ausgesagt, dass die Variable/Funktion 'external linkage' hat (also projektweit im Zugriff steht) und an dieser Stelle kein Speicherplatz für die Variable/Funktion definiert wird. Sie entspricht folglich dem Funktions-Prototyp mit der expliziten Aussage, dass diese Variable/Funktion 'anderweitig' definiert ist.
Syntax: extern Datentyp Variablenname;
extern Datentyp Funktionsname(Datentyp,Datentyp);
Beispiel:
datei1.c | datei2.c |
---|---|
extern int var; //Zugriff auf
//'externe' Variable
extern int func(int par); //Zugriff
//auf 'externe' Funktionen
int main(int argc, char *argv[])
{
extern int var2; //Zugriff auf
//'externe' vVariable
func(var+var2);
}
|
int var; //Definition der
//'externen' Variable
int var2=8; //Definition der
//'externen' Variable
int func(int par) //Definition der
{ //'externen' Funkt.
return(par*2);
}
|
Das Weglassen des Schlüsselwortes extern entspricht der Definition einer Variable/Funktion, d.h. der Speicherreservierung. Die Linkage ist in diesem Fall ebenfalls extern. Die Extern Anweisung wird benötigt:
- wenn auf Funktionen / Variablen zugegriffen werden sollen, die erst weiter hinten im Code definiert werden und projektweit im Zugriff stehen
- wenn auf Funktionen / Variablen zugegriffen werden, die in anderen Dateien enthalten sind
- wenn auf Funktionen / Variablen zugegriffen werden, die in Libraries enthalten sind
- wenn auf Funktionen / Variablen zugegriffen werden, die mit einer anderen Sprache übersetzt wurden
Hinweise:
- Wenn in zwei unterschiedlichen Dateien eines Projektes je eine initialisierte globale Variable mit identischen Namen angelegt wird, so gibt der Linker den Fehler "multiple definition" dieser Variablen aus
- Wie bereits erwähnt wurde Unix und C parallel entwickelt und einer der neuen Features von Unix war die Bereitstellung eines Linkers. Der Extern Storage Class Specifier ist eine Anweisung an den Linker, an den Stellen der Nutzung der Variablen/Funktion in der eigenen Objektdatei die Adresse der eigentlichen Variablen/Funktion aus einer anderen Objektdatei einzutragen
Internal Linkage / Static Storage Class Specifier
BearbeitenDie genaue Bedeutung ist abhängig davon, ob es sich um eine globale Funktion/Variable oder einer lokalen Variable handelt.
Static globale Variable / Static Funktionen
BearbeitenWeist den Compiler an, dass diese Variable/Funktion nur innerhalb dieser C-Datei genutzt wird (internal Linkage) und für anderen C-/Objekt-Dateien nicht im Zugriff stehen. Static entspricht dem Zugriffsmodifikator 'public' aus der objektorientierten Programmierung mit dem Unterschied, das der Zugriff auf diese Variable/Funktion nicht innerhalb des Objektes, sondern innerhalb der Datei beschränkt ist.
Ergänzend dient static der besseren Lesbarkeit des Codes (Zugriff auf diese Variable/Funktion nur aus dieser Datei). Auch der Compiler kann den Zugriff auf solche Variablen/Funktionen optimieren und schnelleren/kompakteren Code erzeugen.
Im Sinne der C-Spezifikation steht der Static Storage Class Specifier dafür, die Variable auf internal Linkage zu setzen (diese nach außen zu anderen Objektdateien nicht bekannt zu machen). Extern im Gegenzug steht für external Linkage. Die Nutzung beider Anweisungen zusammen widersprechen sich folglich und führen zu Compilerfehler. Auch bedeutet static, dass hiermit Speicherplatz reserviert wird (entgegen der 'extern' Anweisung, die eher einem Prototyp entspricht).
Für eine statische lokale Variable kann kein Prototyp erzeugt werden. Solche Variablen müssen immer vor der ersten Verwendung definiert werden!
Beispiele:
- Nutzung von static Variablen/Funktionen innerhalb einer Datei
//Prototypen basierend auf den unten stehenden statische Variablen/Funktionen
static void func(void); //OK
extern void func(void); //KO (Prototyp muss ebenfalls static sein)
extern static void func(void); //KO (extern und static wiedersprechen sich)
void func(void); //KO (Prototyp muss ebenfalls static sein)
extern static int var1; //KO a)extern und static widersprechen sich
// b)von einer static Variable kann kein
// Prototyp angelegt werden!
int main(int argc, char *argv[]) {
func();
var1=1; //KO, da kein Prototyp möglich ist
return 0;
}
//Definition von statische Variablen/Funktionen
static int var1; //Static Variable
static void func(void) { //Static Funktion
printf("var1=%d",var1);
}
- Nutzung von static Variablen/Funktionen innerhalb mehrerer Dateien
- Static entspricht dem private Zugriffsmodifikatior, mit dem Unterschied dass diese Variablen/Funktionen nur innerhalb der Datei gelten und in mehreren Dateien unabhängig voneinander existieren können
datei1.c | datei2.c |
---|---|
static int var=4711;
static void func(void) {
printf("datei1.c var=%d\n",var);
}
|
static int var=0715;
static void func(void) {
printf("datei2.c var=%o\n",var);
}
|
Static lokale Variable
BearbeitenDiese Variablen behalten über der Laufzeit der Funktion ihre Gültigkeit, d.h. sie verlieren nicht ihren Inhalt bei Beendigung der Funktion, sondern behalten den Wert bei. Static lokale Variablen entsprechen folglich einer globalen Variablen, mit dem Unterschied, dass von außerhalb der Funktion keiner auf diese Variable zugreifen kann. Dementsprechend erfolgt die Initialisierung wie bei globalen Variablen über Speichervorbelegung (Initialisierungswert muss eine Konstanten sein) und nicht zur Laufzeit bei jeden 'Aufruf'. Nichtinitialisierte Variablen werden entsprechend mit 0 initialisiert:
Static Lokale Variablen | entsprechen weitestgehend |
---|---|
void func(void) {
int var=7; //Initialisierung erfolgt mit
//jedem Funktionsaufruf
var++; //7->8 7->8 7->8
static int summe=8; //Initialisierung
//erfolgt vor Aufruf
//der main-Funktion
summe++; //8->9 9->10 10->11
}
|
//Namensverschleierung der static
//Variablen, so dass kein anderer
//auf diese Variablen 'zugreifen'
//kann.
int $a$1_xyz=0;
void func(void) {
int var=7;
var++;
$a$1_xyz++;
}
|
Hinweis:
- Wird eine Funktion mit statisch lokalen Variablen von nebenläufigen Prozessen aufgerufen, so besteht ein Dateninkonsistenzproblem. Solche Funktionen sind folglich nicht Nebenläufigkeitsfest. In der Spezifikation sind diese als MT-UNSAFE (MultiThread-Unsafe) gekennzeichnet. strtok() beispielsweise speichert die aktuelle Position in einer statische Variablen, so das diese Funktion nicht gleichzeitig von nebenläufigen Prozessen aufgerufen werden darf.
Typeof Operator
BearbeitenMittels des Typeof-Operator kann der Datentyp einer Variablen ermittelt werden.
Syntax: typeof (T) //GNU-C und C23
__typeof__(T) //GNU-C
T ist wahlweise eine Variable, ein Datentyp oder eine Expression. typeof entspricht einem Konstantenausdruck und wird vom Compiler durch den ermittelten Datentyp ersetzt. Der Rückgabewert ist der ermittelte Datentyp, der nicht in einer Variablen gespeichert oder mit einem anderen Datentyp verglichen werden kann. Typeof kann einzig dazu genutzt werden, weitere Variablen von T anzulegen (entspricht somit dem auto Datentyp bei anderen Programmiersprachen):
typedef unsigned int UI;
UI var1[19];
typeof( UI ) var2;
typeof( int) var3;
typeof(var1) var4;
var4[3]=1;
typeof(var1[2]) var5;
var5=7;
struct { //Unnamed Struct
int x,y,z;
}xyz;
typeof(xyz) abc; //Definition einer neuen Variable von
//einem unnamed struct.
if(typeof(var1) == int) //KO, Ersetzung findet zur Compilezeit
printf("True"); //statt und der Vergleichsoperator kann
//nicht auf Datentypen angewendet werden
//Typeof einer Expression
char var6;
typeof(var6+1) var7; //Es wird mit nichts kleineren gerechnet
//als int. Resultierender Datentyp ist int!
printf("sizeof(var6)=%zu sizeof(var7)=%zu\n",sizeof(var6),sizeof(var7));
Anwendung:
- Zur Darstellung von generischen Function-Like Makros (siehe Präprozessor:Function-Like Makros), so dass auf Basis von Typeof lokale Variablen vom Datentyp der 'übergebenen' Variablen erzeugt werden können:
#define SWAP1(a,b) ({typeof(*a) dummy=*a; *a=*b; *b=dummy;}) #define SWAP2(a,b) ({auto dummy=*a; *a=*b; *b=dummy;}) //C++ int a=7; int b=8; SWAP1(&a,&b);
Hinweis:
- typeof war bis C23 eine GCC-Compilererweiterung. Mit C23 wurde diese Erweiterung in den Standard übernommen
Register Storage Class Specifier
BearbeitenHinweis an den Compiler, dass diese lokale Variable 'häufig' genutzt wird und folglich der Compiler diese so optimieren soll, dass ein schneller Zugriff auf diese möglich ist.
Syntax: register Datentyp variablenname;
Der Storage Class Specifier kann nur auf lokale Variablen und auf Funktionsparameter angewendet werden. Hiermit wird der Compiler gebeten, die Variablen, sofern möglich, in einem Prozessorregister zu halten:
C-Programm | x86 32-Bit Assemblerprogramm |
---|---|
int var1=1;
register int var2=2;
int var3=3;
if(var1==var2)
printf("var1==var2");
|
...
mov DWORD PTR [ebp-12], 1 //var1
mov ebx, 2 //var2
mov DWORD PTR [ebp-16], 3 //var3
cmp DWORD PTR [ebp-12], ebx
jne .L2 //if(var1==var2)
...
.L2:
|
Hinweise:
- Moderne Compiler versuchen Variablen temporär im 'gecachten' Speicher zu legen oder teilen dem OS mit, dass die zugehörige Seite nicht ausgelagert werden darf. Folglich ist die Verwendung des Storage Class Specifier
register
nicht mehr nötig, bzw. wird sogar vom Compiler ignoriert - Ein Register hat keine Speicheradresse, so dass mit dem Adress-Operator keine Adresse ermittelt werden kann
- Ein Debugger stellt beim Darstellen von Variablen den Speicherinhalt der Variablen dar. Aufgrund dessen, dass Compiler Variablen im gecachten Speicher temporär verwalten, kann der angezeigte Variableninhalt vom tatsächlichen Variableninhalt abweichen
Volatile Type Qualifier
BearbeitenVolatile 'informiert' den Compiler, dass die zugehörige Variable durch andere (nebenläufige Threads) manipuliert werden kann. Der Compiler schließt folglich diese Variable aus allen Optimierungsprozessen aus, so dass die Variable nicht über einen Sequence Point in Register zwischengespeichert werden darf. Stattdessen wird innerhalb eines Sequence Points bei einem Lesezugriff solch einer Variablen diese aus dem Speicher geladen resp. das Ergebnis zum Ende des Sequence Points zurückgeschrieben.
Syntax: volatile Datentyp variablenname;
Beispiel:
- Nutzung einer 'normalen' cachebaren Variablen zum Datenaustausch zwischen Threads
Consumer-Thread Producer-Thread int thread_run=0; //Cachebare Variable void consumer(void) { //Warten, bis Producer //Daten erzeugt hat! while(thread_run==0); //Consume Data }
extern int thread_run; void producer(void) { //Produce Data //Signalisiere Consumer, //dass Daten bereitstehen thread_run=1; }
x86 32-Bit Assemblerprogramm x86 32-Bit Assemblerprogramm consumer: mov eax, DWORD PTR thread_run .L2: test eax, eax je .L2 ret
producer: mov DWORD PTR thread_run, 1 ret
- Durch Compileroptimierung wird die Variable thread_run nur einmal aus dem Speicher gelesen und im jedem while-Zyklus der gecachte Inhalt (Inhalt der Variablen eax) genutzt. Änderungen in thread_run werden folglich nicht erkannt!
- Nutzung einer volatile Variablen zum Datenaustausch zwischen Threads
Consumer-Thread Producer-Thread volatile int thread_run=0; //None cacheable variable void consumer(void) { //Warten, bis Producer //Daten erzeugt hat! while(thread_run==0); //Consume Data }
extern volatile int thread_run; void producer(void) { //Produce Data //Signalisiere Consumer, //dass Daten bereitstehen thread_run=1; }
x86 32-Bit Assemblerprogramm x86 32-Bit Assemblerprogramm consumer: .L2: mov eax, DWORD PTR thread_run test eax, eax je .L2 ret
producer: mov DWORD PTR thread_run, 1 ret
- Volatile bedingt, dass der Inhalt der Variable thread_run bei jedem Schleifendurchlauf aus dem Speicher gelesen wird.
Anwendung:
- wenn Variablen für den Datenaustausch zwischen Threads, zwischen Hauptprogramm und Signalhandler und zwischen dem Hauptprogramm und ISR genutzt werden. (Sorgt jedoch nicht dafür, dass die Daten konsistent sind! Diese müssen z.B. gesondert über Semaphoren oder _Atomic gesichert werden)
- bei Hardwarezugriffen, da
- ein Lesezugriff immer von der Peripherie und nicht aus einem Cache bedient wird:
ad1=*(uint32_t *)(0xFFFFF000); ad2=*(uint32_t *)(0xFFFFF000); //Hier erfolgt kein separater Lesezugriff //Stattdessen optimiert der Compiler //den zweiten Lesezugriff durch den //zuvor gelesen Wert weg!
- Schreibzugriffe immer geschrieben werden und nicht wegoptimiert werden:
*(uint32_t *)(0xFFFFF004)=1; //Compiler optimiert diesen Schreibzugriff weg *(uint32_t *)(0xFFFFF004)=80; //und führt nur diesen aus!
- Hardware/Peripherie kann wie ein nebenläufiger Thread angesehen werden!
- zum 'Ausbremsen' einer For-Schleife, also zum aktiven Begrenzen der Ausführungsgeschwindigkeit eines Programmes:
for(volatile int delay=0;delay<100000;delay++) ...
Auto Storage Class Specifier
BearbeitenIn C ist der Storage Class Specifier auto ein Relikt aus der Vorgängersprache B, welche a) noch keine unterschiedlichen Datentypen kannte und b) lokale Variablen als solche explizit definiert werden mussten. Mit dem Schlüsselwort auto wurde dort ausgedrückt, dass es sich um eine lokale Variable (vom einzig vorhandenen Datentyp) handelt, resp. mit dem Schlüsselwort extern, dass es sich um eine globale Variable handelt.
In C (bis C17) wird mit auto (automatic storage Duration) dementsprechend ausgesagt, dass es sich um eine lokale Variable handelt. Wenn ergänzend kein Datentyp angegeben wird, wird der Datentyp der Variable implizit auf int gesetzt!
auto var; //KO, Lokale Variable kann hier nicht angelegt werden
void foo(void){
auto short a=1; //Lokale Variable, Datentyp short
auto b=1.0; //Lokale Variable, Datentyp int (implicit int)
printf("%zu %zu\n",sizeof(a),sizeof(b));
}
Variablendefinitionen innerhalb von Funktion werden auch ohne dem Storage Class Specifier Auto auf automatic storage Duration gesetzt, so dass dieses Schlüsselwort eigentlich entfallen könnte. Da jedoch einige Librarys (z.B. OpenSSL) diesen Storage Class Specifier nutzen (siehe Type Inference for object definitions ), ist 'auto' weiterhin Bestandteil der Spezifikation.
Ab C23 und in C++ wird mit dem Schlüsselwort auto ausgedrückt, dass der Datentyp der Variablen sich aus dem Initialisierungswert ergibt:
void foo(void) {
auto a=1; //Datentyp int
auto b=1.0; //Datentyp double
printf("%zu %zu\n",sizeof(a),sizeof(b));
}
Im Gegensatz zu C++ hat das Schlüsselwort auto ab C23 diverse Einschränkungen (siehe auto in C23)
- je Variabledefintion kann nur eine Variable definiert werden
auto a=1.0,b=2LL; //KO
- es kann kein Zeiger hierüber definiert werden
short b; auto *a=&b;
- auto kann nicht als Rückgabeparameter oder Übergabeparameter von Funktionen genutzt werden:
auto foo(auto par1, auto par2) {}
Const Type Qualifier
BearbeitenMit dem Type Qualifier const wird eine Variable auf nur lesbaren Zugriff gesetzt.
Syntax: Datentyp const Variablenname;
const Datentyp Variablenname;
Die Wertzuweisung solcher Variablen erfolgt über die Initialisierung. Der Schreibschutz erfolgt im Wesentlichen zur Compilezeit, welcher den Compilevorgang abbricht, wenn eine Zuweisung an solche eine Variable erfolgt (eine Const Variable kann kein lvalue sein):
int var0; //ReadWrite
const int var1=7; //ReadOnly
int const var2; //wenn var2 ein globale Variable, dann 0,
//andernfalls Zufallswert
var1++; //KO
var2=var0; //KO Datentyp (const int)=(int)
var0=var1; //OK Datentyp (int)=(const int)
Zur Laufzeit erfolgt keine Kontrolle. Wird bspw. die Adresse solch einer Variablen in einem Zeigervariablen gespeichert und diese im Anschluss dereferenziert, so hängt das Laufzeitverhalten von diversen Faktoren ab:
const int var=7;
int *ptr=&var;
*ptr=4711; //Schreibender Zugriff auf var.
//bei lokaler Variable führt dies zur Änderung der Konstanten
//bei globaler Variable führt dies zum Programmabsturz
Alle C-Compiler weisen in der Regel globalen und static lokalen Const-Variablen einer separaten Speicherklasse zu (rodata-Segment). In der Regel wird diese Speicherklasse bei:
- Embedded Systemen in der Tat nur im Speicher mit reinen Lesezugriff gehalten (ROM, Flash). Ein indirekter Schreibzugriff auf solche Variable bewirkt keine Änderung. Das Programm wird normal fortgeführt
- Systemen mit MMU (Windows, Linux, macOS) in einen Speicherbereich gehalten, der durch die MMU schreibgeschützt wird. Ein indirektes Schreiben auf diesen Bereich bewirkt somit ein Laufzeitfehler (Segmentation fault), verursacht durch die MMU
Const lokale Variablen werden wie normale lokale Variablen auf dem Stack gehalten, so dass hier kein zusätzlicher Schutz besteht. Ein indirektes Schreiben solch einer Variablen führt zur Änderung der Konstanten.
Anwendung
- Variablen, die nicht geändert werden sollen
- Variablenübergabe an Funktionen, zur Kennzeichnung, dass diese ReadOnly sind
void foo(const int par1) { par1=4711; //KO
Hinweise:
- Const Variable sollten initialisiert werden, da später kein schreibender Zugriff möglich ist. Der Compiler gibt leider bei nicht initialisierten Konstanten keinen Fehler aus!
- Const als Rückgabewert einer Funktion ist möglich, aber wenig sinnvoll. Im Gegensatz zu C++, bei welcher mit constexptr gesagt wird, dass die ganze Funktion Konstant ist (und dadurch zur Compilezeit ausgerechnet werden kann) wird bei C einzig gesagt, dass der Rückgabewert konstant ist:
const als Funktionsrückgabewert bei C constexpr in C++ const int func(int var) { return var*3; } int main(int argc, char *argv[]) { int par=7; //Rückgabewert ist Konstant const int var = func(par+1); return 0; }
constexpr int func(int var) { return var*3; } int main(int argc, char *argv[]) { int par=7; //Normaler Funktionsaufruf int var = func(par+1); //Aufgrund des konstanten Übergabeparameters //ist die ganze Funktion konstant und wird //zur Compilezeit ausgerechnet int var2 = func(4711); //Compiler ersetzt Funktionsaufruf durch int var=3*4711; return 0; }
- CONST Variablen erhöhen die Sicherheit eines Programmes. So sind bspw. in der Programmiersprache RUST alle Variablen const, resp. die Variable darf nur vom Owner geändert werden. Alle Borower haben nur lesenenden Zugriff
- Alternativ zu const Variablen können in C/C++ konstante Werte über Makros dargestellt werden, welche einen kompakteren/schnelleren Code ermöglichen:
const int var=7; #define MAKRO 7 if(var==7) ... if(MAKRO==7) ...
_Atomic Type Qualifier
BearbeitenThreads sind nebenläufige Ausführungsstränge innerhalb eines Prozesses, welche sich alle globalen Variablen teilen. Die Zuteilung der Rechenzeit der einzelnen Threads erfolgt durch den Scheduler. Ein laufender Thread kann zu jedem Zeitpunkt die Rechenzeit entzogen/zugeteilt werden. Zu jedem Zeitpunkt bedeutet, dass der Entzug auf Maschinenspracheebene und nicht auf C-Befehlsbene stattfindet.
Ein Datenaustausch zwischen den Threads erfolgt typischerweise über globale Variablen. Wird ein Thread während des Zugriffs auf eine globale Variable angehalten, so besteht dies Gefahr von Dateninkonsistenz:
thread1 | thread2 |
---|---|
long long var=0x00000000FFFFFFFF;
void *thread1(void *par) {
while(1) {
//Do something
//var++;
}
}
|
extern long long var;
void *thread2(void *par) {
long long copy;
while(1) {
copy=var;
//Do something
}
}
|
x86 32-Bit Assemblerprogramm | x86 32-Bit Assemblerprogramm |
mov eax, DWORD PTR var
mov edx, DWORD PTR var+4
add eax, 1
adc edx, 0
mov DWORD PTR var, eax
//Threadwechsel hier
mov DWORD PTR var+4, edx
|
mov eax, DWORD PTR var
mov edx, DWORD PTR var+4
mov DWORD PTR [ebp-8], eax
mov DWORD PTR [ebp-4], edx
|
Findet ein Thread-Wechsel zwischen den beiden Schreibbefehlen mov DWORD PTR var...)
statt, so würde die Variable var zum einen aus dem neuen niederwertigen 32-Bits (hier 0xFFFFFFFF+1=0x00000000) und dem alten höherwertigen 32-Bits (hier 0x00000000) bestehen.
Zur Lösung des Problems stehen diverse Möglichkeiten zur Verfügung:
- Scheduling auf Hochsprachenebene (in C nicht gegeben)
- Absicherung des Zugriffs auf diese Variable über Semaphoren (händisch)
- Absicherung durch Sperrung des Interrupts (nicht sinnvoll)
- Absicherung über atomare (nichttrennbare/-unterbrechbare) Maschinenbefehle
Über den Type Specifier _Atomic wird seit C11 der Compiler angewiesen, Schreibzugriffe auf solche Variablen über atomare Maschinenbefehle abzusichern.
Syntax: _Atomic(type-name) Variablenname
_Atomic type-name Variablenname
Zugriffe auf solche Variablen werden als nicht unterbrechbare Einheit dargestellt, so dass sichergestellt ist, dass bei einem Zugriff immer der gesamte Wert gelesen/geschrieben wird:
_Atomic long long var=0x00000000FFFFFFFF;
void *thread1(void *par) {
while(1) {
//Do Something
var++; //Befehls wird als atomare Befehl ausgeführt
}
}
Der Type Specifier ist optional und muss vom Compiler nicht unterstützt werden. Auch werden nicht alle Datentypen unterstützt. In der Header-Datei "stdatomic.h" sind diverse Makros als Alternative/Ergänzung enthalten.
_Thread_local Storage Class Specifier
BearbeitenErgänzend zu lokalen (automatic Storage Duration) und globalen (static Storage Duration) Variablen wird mit Thread Local Storage eine für jeden erzeugten Thread eigenständige Variable erzeugt (Thread Storage Duration).
Syntax: __thread Datentyp Variablenname; //GCC
_Thread_local Datentyp Variablenname; //ab C11
thread_local Datentype Variablenname; //ab C++11
Die Speicherplatzreservierung und Initialisierung solcher Variablen erfolgt mit dem Start des Threads.
Beispiel:
//Compilerschalter: -lpthread
__thread int tls=2; //Thread Local Storage
//Alternative Weg für Thread Local Storage
struct data {
pthread_t id;
int data;
} data[2];
static void *run(void *arg)
{
struct data *this=arg;
printf("data->data=%d",this->data++);
printf("->%d\n",this->data);
printf("tls=%d",tls++);
printf("->%d\n",tls);
...
}
int main(int argc, char *argv[])
{
for(size_t lauf=0;lauf<sizeof(data)/sizeof(data[0]);lauf++) {
pthread_create(&data[lauf].id,NULL,run,(void *)&data[lauf]);
}
...
}
Die Variable errno
, in welcher der genaue Fehlercode abgelegt wird, wird von der Laufzeitumgebung als Thread Local Variable angelegt, so dass jeder Thread seine eigene errno
Variable besitzt.
Hinweis:
- Ändert die zugrundeliegenden Adressierungsart, wie auf Variablen zugegriffen wird
- Global: über feste Adressen -> absolute Adressierung
- Lokale: relativ zum Basepointer -> indizierte Adressierung
- Thread: relativ zu einem weiteren Pointer, der mit jedem Threadwechsel neu gesetzt wird. Siehe auch https://gcc.gnu.org/onlinedocs/gcc/Thread-Local.html#Thread-Local
- Speicherplatz für solche Variablen wird in den Sektionen
.tdata
und.tbss
reserviert. Siehe auch: https://uclibc.org/docs/tls.pdf
Sonstiges
BearbeitenStorage Class Specifier (static, extern, auto, register, _Thread_local
) definieren die 'Speicherklasse', in der der Speicher für Variablen reserviert werden soll. Infolgedessen sind diese Specifier nur bei der Definition von Variablen anwendbar. Eine Anwendung bei der reinen Datentypbeschreibung und bei Struktur-/Unionelemente ist nicht möglich:
static struct xyz { //KO, static kann nicht auf den Datentyp angewendet
// werden
extern int x; //KO, extern kann nicht auf ein Strukturelement
//angewendet werden
int y;
};
void foo(void) {
register struct cd{ //OK, register wirkt auf die Variablendefinition
int c;
int d;
} var;
}
Datatypespecifier (const, volatile, _Atomic
) wirken sich auf den Zugriff aus, d.h. sie beschränken den Zugriff oder garantieren bestimmte Eigenschaften beim Zugriff (durch Nutzung anderer Maschinensprachebefehle). In diesem Sinne können Datatypespecifier sowohl auf Variablen, als auch auf Struktur-/Unionelemente angewendet werden:
struct xyz {
int a;
const int b; //Strukturelement ist nur lesbar
volatile int c; //Strukturelement wird beim Zugriff
//nicht gecached
_Atomic int d;
const volatile int e;
};
struct xyz xyz1;
xyz1.b=7; //KO Strukturelement x ist nur lesbar
xyz1.c=7; //OK
const struct xyz xyz2={1,2};
xyz2.x=7; //KO
xyz2.y=7; //KO
const struct ab{ //KO Datentyp kann nicht auf const gesetzt werden
int a;
int b;
};
volatile struct c{ //OK hier wirkt volatile auf die Variablendefinition
int c;
int d;
} hallo;
Compound-Literal entsprechen anonymen Variablen. Per Default sind diese Variablen 'normale' Variablen, also nicht im Zugriff beschränkt. Über Type Qualifier kann der Zugriff auf diese Variablen eingeschränkt werden:
(const float []){1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6};
struct xyz{int x,y,z;};
(volatile struct xyz){.y=2};
(_Atomic int []) {1,2,3};