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