Programmieren in C/C++: Präprozessor
Der Präprozessor ist ein von C-Syntax unabhängiger Sprachsyntax. Beim Übersetzungsprozess einer C-Datei wird der Präprozessor als zweiter Prozess (nach dem Entfernen der Kommentare) ausgeführt und das Ergebnis dieser Übersetzung dem eigentlichen C-Compiler vorgelegt. Der Präprozessor erzeugt dabei keinen ausführbaren Code (also keine Maschinensprachebefehle wie der C-Compiler) sondern entspricht im Wesentlichen einer Textersetzung. Prinzipiell könnte der C-Präprozessor auch als Präprozessor für andere Sprachen genutzt werden.
Das Resultat des Übersetzungsprozesses kann mittels des Compilerschalters 'gcc -E' dargestellt werden (im Compiler Explorer dazu '-E' im Feld 'Compiler options' eintragen). Dieser stoppt den Übersetzungsprozess nach Durchlauf des Präprozessors und gibt den erzeugten C-Code auf der Standardausgabe aus. Vorsicht, bei vielen Include-Anweisungen ist die Ausgabe schnell mal mehrere tausend Zeilen lang. Im Compiler Explorer kann alternativ im Compiler-Fenster unter 'Add new...' über '# Preprocessor' ein weiteres Fenster mit der Preprocessor Ausgabe geöffnet werden.
Der Syntax des Präprozessors weicht deutlich vom C-Syntax ab. Wesentliche Kennzeichen des Syntax sind:
- Kein Semikolon zum 'Abschluss' eines Befehls
- Die meisten Befehle starten mit einem # und Enden am Zeilenende
- Zur Fortführung eines Befehls in der nächsten Zeile muss die fortzuführende Befehlszeile mit einem Backslash '\' abgeschlossen werden
Die Befehle lassen sich in folgende Kategorien unterteilen:
- Include-Anweisung
- Object-Like Makros
- Function-Like Makros
- Bedingte Übersetzung
- Sonstiges
Bei tiefergehendes Interesse in die Arbeitsweise des Präprozessors und des Syntax wird auf das CPP Manual der GCC Online Dokumentation verwiesen!
Include
[Bearbeiten]Entspricht einer Textersetzung, wobei die Textzeile mit der Include-Anweisung durch den Inhalt der adressierten Datei ersetzt wird.
Syntax: #include <h-char-sequence >
Syntax: #include "y-char-sequence"
Syntax: #include Object-Like-Makro //Standard-C
Die Include-Anweisung kann jede Datei inkludieren. Damit der folgende Compilerlauf keine Fehler meldet, sollte diese Datei C-Syntax enthalten. Typischerweise werden die inkludierten Files Header-Files genannt und haben die Dateiendung .h oder .hpp.
Die inkludierten Files werden ebenfalls vom Präprozessor geparst, so dass z.B. hier enthaltene Include-Anweisungen ebenfalls aufgelöst werden (Vorsicht, Gefahr der doppelten Includierung von identischen Dateien und damit ggf. eine Endlosschleife)
Beispiele:
#include <stdio.h>
#include "class.h"
#define includefile1 <stdint.h>
#define includefile2 "main.h"
#include includefile1
//Der Include-Anweisungen folgende Anweisungen werden nicht übersetzt!
#include "test.h" int var=3;
int main(int argc,char *argv[]) {
var++; //KO Anweisung 'int var=3;' wurde nicht übersetzt
return 0;
}
Suchpfade
[Bearbeiten]"y-char-sequence"
[Bearbeiten]Suchpfad für die einzubindende Datei ist das aktuelle Arbeitsverzeichnis, von dem der Compiler gestartet wurde. Über relative Pfadangaben können Dateien in 'Nachbarschaft' und über absolute Pfadangabe Dateien von 'überall' inkludiert werden:
#include "test.h"
#include "verzeichnis/hallo.h"
#include "\home\xyz\test.h"
#include "../lib/test.h"
//Vorsicht, Windows nutzt '\' und Unix '/' zur Pfadtrennung,
<h-char-sequence>
[Bearbeiten]Nutzung des vom Compiler vorgegebenen Suchpfades zum Suchen nach der angegebenen Datei. Im Compiler-Suchpfade sind unter anderem die Verzeichnisse der Header-Dateien der Standard-C-Library enthalten. Über relative Pfadangaben können Dateien in 'Nachbarschaft' zum Suchpfad inkludiert werden:
#include <stdio.h>
#include <sys/stat.h>
Mittels gcc -Ixxx
kann der Suchpfade um die Pfadangabe xxx ergänzt werden, so dass z.B. die Header-Dateien von Librarys direkt (ohne Pfadangabe) angesprochen werden können. Die Liste der vom Compiler genutzten Suchpfade kann mittels 'cpp -v' oder 'cpp -v /dev/null' ausgegeben werden:
Probleme im Umgang mit Header-Dateien
[Bearbeiten]Bei unachtsamer Nutzung der include Anweisung können gewollt/ungewollt:
- Identische Header-Datei mehrmals zu einer C-Datei inkludiert werden (direkt oder auch indirekt):
datei.h #include <stdio.h> //doppelt inkludiert int var; //*1) typedef unsigned int UI; #define HALLO 1
main.c #include <stdio.h> #include "datei.h" #include "datei.h" //doppelt!
- Eine Header-Datei in unterschiedlichen Dateien inkludiert werden:
datei1.h int var; //*2) typedef unsigned int UI; #define HALLO 1
func1.c func2.c #include "datei.h" ...
#include "datei.h" ...
Damit in beiden Fällen ein fehlerfreier C/C++-Code resultiert, unterliegt der Inhalt von Header-Dateien einigen Restriktionen:
- Keine Definitionen von Variablen und Funktionen in Header-Dateien (da Header-Datei in unterschiedlichen Dateien inkludiert werden, die dann zu eigenständigen Variablen/Funktionen in jeder erzeugten Objektdatei führt, siehe *1) und *2))
- Beliebig viele Deklarationen möglich (sofern diese vom identischen Datentyp sind)
- Keine doppelte Definition von Datentypen (aufgrund der Gefahr bei doppelter Einbindung der identischen Header-Datei)
- Keine doppelte Definition von Markos (Gefahr bei doppelter Einbindung der identischen Header-Datei)
Datentypen und Makros sind fester Bestandteil von Header-Dateien. Zur Sicherstellung, dass diese bei doppelter Einbindung einer Header-Datei nicht doppelt ausgeführt werden, sind Include-Wächter notwendig:
#ifndef _Projektname_Dateiname_Dateiendung_INCLUDED_
#define _Projektname_Dateiname_Dateiendung_INCLUDED_
//Eigentlicher Inhalt der Header-Datei
#endif
Alternativ kann nachfolgende, von vielen Compiler unterstützte Anweisung, am Anfang der Header-Datei genutzt werden (siehe auch pragma once)
#pragma once
//Eigentlicher Inhalt der Header-Datei
Anwendung
[Bearbeiten]Einzubindende-Dateien werden u.A. für folgende Anwendungsfälle genutzt:
- Deklarationen von Funktionen, die in einer C-Datei geschrieben sind und von anderen C-Datei genutzt werden sollen (entspricht der Public Zugriffsmethode in OOP):
class.h int class_set(int val,int attr); int class_init(void);
class.c func1.c int class_set(int val,int attr) { ... } int class_init(void) { ... } static void class_priv(void) { ... }
#include "class.h" void func1_init(void) { ... class_init(); ... class_set(4,1); } //Für class_priv() //kein Prototyp //vorhanden, somit //nicht aufrufbar!
- Definition von projektweiten Konstanten und Datentypen, welche von allen C-Dateien genutzt werden können:
common.h #define DNS_ADDR "141.41.1.150" #define ERROR(text, arg... ) \ (fflush(stdout),\ fprintf(stderr,\ "\e[31m%s() Error:\e[30m "\ text"'\n",__func__,##arg) ) #define MODE_TemperaturOnly 1 #define MODE_DruckOnly 2 #define MODE_AttitudeOnly 3 #define MODE_TemperaturDruck 4 #define MODE MODE_AttidueOnly typedef struct { int mode; int debuglevel; ... } global_t;
- Zur Nutzung von Librarys muss einerseits die Library als solches zur ausführbaren Datei dazu gebunden werden (Compilerschalter gcc -lxxx). Zum Aufruf der in der Library enthaltenen Funktionen sind ergänzend die Prototypen der Funktionen, die notwendigen Datentypen und die Konstanten über include in die Source-Datei einzubinden:
stdio.h //Auszug aus stdio.h ... extern int fprintf (FILE *__restrict __stream, const char *__restrict __format, ...); extern int printf (const char *__restrict __format, ...); extern int sprintf (char *__restrict __s, const char *__restrict __format, ...) __attribute__ ((__nothrow__)); extern int vfprintf (FILE *__restrict __s, const char *__restrict __format, __gnuc_va_list __arg); ...
- Die Prototypen, die Datentypen und die Konstanten der Standard-C Library sind in diverse Header-Dateien verteilt: C-Standard-Bibliothek
Sonstiges
[Bearbeiten]- C89: Max Rekursionstief 8
- C99: Max Rekursionstief 15
- Bei #include <> muss der Compiler die Datei nicht laden, sondern kann eine eigene (gespeicherte Header-Datei) nutzen (zwecks schnellerer Übersetzung)
- Include lädt einzig die Deklarationen in die aktuelle C-Datei ein. Die dazugehörigen Definitionen sind beim Linker Durchlauf, z.B. durch Librarys separat beizufügen (Compilerschalter gcc -lxxx)
- C++ benutzt ein Name-Mangling System (siehe Name mangling), so dass auf deren Funktionen und Variablen nicht direkt zugegriffen werden kann. Wird aus einem C++-Projekt eine Library erstellt, so kann das Name-Mangling wie folgt 'deaktiviert' werden:
extern "c" void test(void); #ifdef __cplusplus extern "C" { #endif void test(void); #ifdef __cplusplus } #endif
- Zum Inkludieren von Standard-C Header Dateien in C++ muss dem Dateienamen ein c vorangestellt werden. Die Endung .h entfällt:
#include <cstring> //zum Einbinden der Datei string.h unter c++
- Mit jeder Include Anweisung erhöht sich die Zeitdauer für einen Compiledurchlauf. Einerseits muss jede zu inkludierende Datei von der Festplatte geladen werden und andererseits vergrößert sich mit jeder Datei der zu übersetzende Code. In diesen Sinn gilt 'weniger ist mehr'. Also gerne mal regelmäßig die Include-Anweisungen durchgehen und überlegen, ob diese in der Tat benötigt werden. Tipp: Hinter der Include-Anweisung im Kommentar vermerken, für welche Funktionen diese Header-Datei benötigt wird
Object-Like Makros
[Bearbeiten]Entspricht einer Textersetzung auf Wortbasis, wobei das Wort name
im folgenden Code durch sequence-of-tokens
ersetzt wird. Innerhalb von Kommentaren und Strings erfolgt keine Textersetzung.
Syntax: #define name sequence-of-tokensopt
'Sequence-of-tokens'
endet am Zeilenende. Soll das Makro in der Folgezeile fortgesetzt werden, so muss die Zeilenfortsetzung (siehe Grundlagen:Zeilenfortsetzung) genutzt werden. Wenn 'sequence-of-tokens'
leer/nicht angegeben ist, wird 'name'
durch nichts ersetzt, d.h. 'name'
wird aus dem Source-Code entnommen und durch ein 'Leerzeichen' ersetzt.
Die bedingte Übersetzung prüft oftmals nur ab, ob ein Makro definiert ist (mittels #ifdef name
oder #if defined(name)
). Der 'zugewiesene' Wert sequence-of-tokens
ist dabei nicht von Interesse, so dass dessen Angabe in diesem Anwendungsfall ebenfalls nicht nötig ist.
Wie Variablen und Funktionsnamen dürfen Makros nicht doppelt definiert werden. Soll ein vorhandenes Makro gelöscht oder neu gesetzt werden, so kann das vorhandene Makro mit #undef
gelöscht werden. Im nachfolgenden Source-Code erfolgt dann keine Textersetzung für dieses Makro mehr.
Syntax: #undef name
Die resultierenden Fehlermeldungen nach der Textersetzung sind Fehlermeldungen aufgrund des ungültigen C-Syntax, hervorgerufen durch die Textersetzung. Auf den ersten Blick sind diese Fehlermeldung nur schwer nachvollziehbar, da gedanklich die Textersetzung vor Interpretation der Fehlermeldung durchzuführen ist. Daher empfiehlt sich in solchen Fällen, den resultierenden Source Code nach der Textersetzung mittels gcc -E
sich darstellen zu lassen.
Beispiele:
#define ROT1 var
int ROT1=2,ROT11=3; //Nur Wortweise ersetzung!
#define ROT2
int ROT2 var1=1; //ROT2 wird durch nichts ersetzt
# define ROT3 8 //Leerzeichen werden eleminiert
#define ROT4 /*Test */ "Hello World"
#define ROT5 5;
int a=ROT5,b=ROT5+6; //KO Compilerfehler, da ';' bestandteil der Ersetzung
#define ROT6 1,"hallo",7
char str[]="ROT6"; //Keine Ersetzung von ROT6, da innerhalb eines Strings
struct {int a; char str[7]; int b;} var2={ROT6};
#define else //C-Befehl else durch nichts ersetzen
if(1==2) printf("hallo"); else printf("welt");
#define ADDAB a+b
int d,e,f=ADDAB;
#define ROT1 //KO da Mehrfachdefinition verboten
#undef ROT1 //für begrenzte Gültigkeit oder Überschreibung
int ROT1=2; //Definition einer Variablen ROT1
#define ROT1
ROT1=2; //ROT1 wird durch nichts ersetzt
#define ROT7 BACKSLASH \
#define ROT8 8 //Befehlsfortführung
#define XYZ 9
enum {XYZ=9}; //KO XYZ wird durch nichts ersetzt
Ergänzend zu den selbst definierten Makros gibt die C-Spezifikation folgende vordefinierten Makros vor, welche vom Compiler durch die entsprechende Gegebenheiten ausgetauscht werden:
name | Datentyp | Bedeutung |
---|---|---|
__LINE__ | Konstante vom Type int | Wird ersetzt durch die aktuelle Zeilennummer, in welcher das Makro steht |
__FILE__ | const char * | Wird ersetzt durch den Dateinamen der C-Datei |
__DATE__ | const char * | Wird ersetzt durch das aktuelle Datum zum Compilezeitpunkt |
__TIME__ | const char * | Wird ersetzt durch die aktuelle Uhrzeit zum Compilezeitpunkt |
__STDC__ | Konstante vom Type int | Wert=1, wenn der Compiler auf Standard-C konform eingestellt ist |
__STDC_VERSION__ | Konstante vom Type int | Wert=YYYYMM Jahr und Monat der C-Version Bsp: 199409 für C89 199901 für C99 |
__func__ (ab C99) | char * | wird ersetzt durch einen Zeiger auf einen String, in welchem der aktuelle Funktionsname enthalten ist |
Beispiel für Nutzung dieser Makros
printf("File:"__FILE__ " erstellt am "__DATE__" um "__TIME__ " gestartet\n");
#define MELDUNG(text) fprintf( stderr, "Datei [%s], Zeile %d: %s\n" \
,__FILE__, __LINE__, text )
Weiterhin gibt es vom Compiler vordefinierte Makros, die in Abhängigkeit des Betriebssystems, der Rechenbreite, des Prozessors und vielen anderen Bedingungen gesetzt werden. Neben der reinen Informationen dienen diese Makros auch dazu, plattformunabhängigen Code zu schreiben. Auszug aus Compilerinternen Makros:
name | Datenty | Bedeutung |
---|---|---|
__GNUC__ | --- | Defined, wenn der Compiler die GNU-C Spezifikation unterstützt |
__INCLUDE_LEVEL__ | int | Zahl, welche den aktuellen Include-Level angibt |
__CHAR_UNSIGNED__ | --- | Defined, wenn Char vom Datentyp unsigned ist |
__TIMESTAMP__ | const char * | String mit dem letzten Änderungsdatum der Datei |
und viele weitere:
__WIN32
__unix__
__linux__
__i386__
__CYGWIN32__
Die vom Compiler vordefinierten Makros können mit nachfolgender Anweisung ausgegeben/angezeigt werden:
cpp -dM -E
oder cpp -dM -E -xc /dev/null
Ergänzend zur #define
Anweisung im Source-Code können Makros mit dem Compileraufruf gesetzt werden:
gcc -Dxxxx[=definition] //für Object-Like Makros
gcc -Dxxx[(arglist)=Definition] //für Function-Like Makros
Diese Makro-Definition wird gerne von den Build-Tool genutzt, über welche globale Einstellungen getätigt werden. Ein typisches hier gesetztes Makro ist NDEBUG das im Auslieferungszustand (RELEASE) der Software gesetzt wird. Mittels bedingter Übersetzung können auf Grundlage dieses Makros z.B. Debug-Ausgaben unterbunden werden.
Anwendung
[Bearbeiten]- Objekt-like-Makros werden oft in Verbindung mit bedingter Übersetzung zur Erzeugung von:
- Host-System (Compiler) unabhängigen Code
- Target-System (Rechnerarchitektur) unabhängigen Code
- Debug/Release Konfiguration
- Softwarevarianten Verwaltung
- Derivatensteuerung
genutzt. Entsprechende Beispiele siehe Bedingte Übersetzung
- Objekt-like-Makros stellen eine Alternative zu const Variablen dar. Da erste keinen Speicherplatz beanspruchen, resultiert hieraus kleiner und schneller Code:
const int val=4711; #define VAL 4711 //Für die Größenfestsetzung von Arays #define ARR_SIZE 16 int arr[ARR_SIZE];
- Zur Beschreibung von projektweit gültigen Konstanten:
#define DNS "192.168.0.1" #define TASK_MAX 32
- In der Header-Datei 'inttypes.h' sind Konstanten als Alternative für die printf Formatstringanweisungen enthalten:
#include <inttypes.h> //Auszug aus inttypes.h #define PRId8 "d" #define PRId64 "lld" #define SCNi8 "hhi" //so dass ein Formatstring wie folgt zusammengesetzt werden kann: char var=47; long lo=11; printf("var=%" SCNi8 " lo=%" PRId64 "\n",var,lo);
Function-Like Makros
[Bearbeiten]Function-like Makros entsprechen einer doppelten Textersetzung. Einerseits wird der Name durch sequence-of-tokens
und ergänzend die in sequence-of-tokens
enthaltenden Identifier
(aus der identifier-List
) durch den beim Aufruf des Makros enthaltenen Text ersetzt:
Syntax: #define name( identifier-Listopt) sequence-of-tokensopt
Beispiel für die doppelte Textersetzung:
#define ADD(a,b) a+b
int var=ADD(7,8);
//1. Ersetzung von sequence-of-tokens → int var=a+b(7,8);
//2. Identifier a wird durch 7 ersetzt → int var=7+b(8);
//3. Identifier b wird durch 8 ersetzt → int var=7+8;
//Function-Like Makros entsprechend generischen inline Funktionen
int a=1 ,b=2 ,c=ADD(a,b); //Integer Addition
float x=1.0,y=2.0,z=ADD(x,y); //Float Addition
Hinweise:
Sequence-of-tokens
endet am Zeilenende. Soll sich das Makro über mehrere Zeilen erstrecken, so ist die Zeilenfortsetzung (siehe Grundagen:Zeilenfortsetzung) zu nutzen- Zwischen name und '(' ist kein Leerzeichen erlaubt (andernfalls handelt es sich dann um ein Objekt-like-Makro und die öffnende Klammer gehört zu sequence-of-tokens)
#define ADD (a,b) a+b //Mit Leerzeichen int var=ADD(7,8); //→ int var=(a,b) a+b(7,8)
- wird ein Identifier beim Aufruf ausgelasen, so wird der Identifier durch nichts ersetzt
#define ADD(a,b) a+b q=ADD( ,8); //Erster Identifier wird durch nichts ersetzt //Erzeugt ungültigen C-Syntax → Compilerfehler q=ADD( , ); //Präprozessorfehler //Beide Identifier werden durch nichts ersetzt //Erzeugt ungültigen C-Syntax → Compilerfehler q=ADD( ); //Präprozessorfehler
Makros basieren auf reine Textersetzung. Wird der Rückgabewert eines Makros in einem Ausdruck weiterverarbeitet, so kann der resultierende C-Code eine andere Abarbeitungsreihenfolge bedingen, als erwartet:
#define ADD(a,b) a+b
int a=ADD(1,2)*3; //a=1+2*3 → 7
int b=5 ,c=8;
int d=ADD(b,c)*ADD(b,c); //d=b+c*b+c; → 53
Zur Lösung dieser Problematik empfiehlt sich, 'sequence-of-tokens' zu klammern:
#define ADD(a,b) (a+b)
int a=ADD(1,2)*3; //a=(1+2)*3 → 9
int b=5 ,c=8;
int d=ADD(b,c)*ADD(b,c); //d=(b+c)*(b+c); → 169
Werden Ausdrücke beim Aufruf von Makros eingesetzt, so bleiben diese bei der Ersetzung der Identifier erhalten:
#define MUL(a,b) (a*b)
float a = MUL(7+8,8-7); //a=7+8*8-7 → 64
Zur Lösung dieser Problematik ist es ratsam, auch die Identifier zu klammern:
#define MUL(a,b) ((a)*(b))
float a = MUL(7+8,8-7); //a=(7+8)*(8-7) → 15
Wird in einem Makro eine 'lokale' Variable benötigt, so bietet sich ein Compound-Statement an (siehe Grundlagen:Block / Compound-Statement):
//Quelle: https://gcc.gnu.org/onlinedocs/cpp/Swallowing-the-Semicolon.html
#define SKIP_SPACES(p, limit) { \
char *lim = (limit); \
while (p < lim) { \
if (*p++ != ' ') { \
p--; break; } \
} \
}
char str[]=" hallo Welt";
char *ptr=str;
SKIP_SPACES(ptr,str+sizeof(str));
Da C-Zeilen, und damit auch Makroaufrufe mit einem Semikolon abgeschlossen werden, ergibt sich ein Problem bei Nutzung solcher Makros in Verbindung mit einer IF-Anweisung:
char str[]=" hallo Welt";
char *ptr=str;
if(ptr != NULL)
SKIP_SPACES(ptr,str+sizeof(str)); //Makro wird mit einem Semikolon abgeschlossen!
else
printf("Error: ptr==NULL");
Das abschließende Semikolon erzeugt in diesem Anwendungsfall eine Leeranweisung, so dass zwischen if
und else
zwei Anweisungen stehen. Dies Problem kann umgangen werden, wenn anstatt des Compound-Statement eine do {} while(0)
Anweisung genutzt wird:
#define SKIP_SPACES(p, limit) do{ \
char *lim = (limit); \
while (p < lim) { \
if (*p++ != ' ') { \
p--; break; } \
} \
}while(0)
char str[]=" hallo Welt";
char *ptr=str;
if(ptr != NULL)
SKIP_SPACES(ptr,str+sizeof(str));
else
printf("Error: ptr==NULL");
Sind in Makros bedingte Anweisungen oder das logische UND/ODER enthalten und wird das Makro mit Argumenten aufgerufen, die einen Seiteneffekt hervorrufen (bspw. Postinkrement oder Funktionsaufrufe, die eine globale/statisch lokale Variable ändern) (siehe Seiteneffekt), so kann der Seiteneffekt ebenfalls zu unerwarteten Verhalten herbeiführen:
#define MAX(var,a,b) (var=a>b?a:b)
int a=7,b=8,c;
MAX(c,a++,b++); //→(c=a++>b++?a++:b++);
//b wird zweimal erhöht, a nur einmal!
Zur Vermeidung der doppelten Ausführung (und zur Geschwindigkeitssteigerung) sollten die Makroargumente wie bei einem Funktionsaufruf in lokale Variablen zwischengespeichert werden (Call-by-Value):
#define MAX(var,a,b) do { typeof(a) a_=(a); \
typeof(b) b_=(b); \
var=a_ > b_ ? a_ : b_;} while(0)
Nach der GNU-C Spezifikation bietet sich alternativ zur 'do {} while(0)' Kapselung ein Embedded Statement an (siehe Grundlagen:Embedded Statement). Dies hat ergänzend den Vorteil, dass dieses einen Rückgabewert hat:
#define MAX(a,b) ({ typeof(a) a_=(a); \
typeof(b) b_=(b); \
a_ > b_ ? a_ : b_;})
int a=7,b=8,c;
c=MAX(a++,b++);
Function-Like Makros sind ein mächtiges, aber auch fehleranfälliges Werkzeug. Es empfiehlt sich:
- Ausreichend Klammern (sowohl das Makro als auch die Makroparameter)
- Möglichst Makronamen in GROSSBUCHSTABEN zu schreiben, so dass beim Lesen des Source-Codes klar ist, dass hier ein Makro und keine Variable/Funktion aufgerufen wird
- Innerhalb von Makros den Komma-Operator zum Trennen von Anweisungen nutzen, sofern die Anweisung nicht in einer
do {} while(0)
oder oder Embedded Statement gekapselt sind - Bei Makroaufrufen sind Argumente mit Seiteneffekte (wie Pre-/Postinkrement) zu vermeiden, da diese durch eventuelle Mehrfachauswertung zu unerwarteten Verhalten führen
Hinweis:
- Eine alternative/weiterführende Beschreibung ist in der Online-Dokumentation von gnu zu finden: [Marco-Arguments]
Anwendung
[Bearbeiten]- Funktionalitäten unabhängig vom Datentyp bereitstellen (generische Funktionen)
#define MAX(a,b) ({ /* siehe oben */ }) #define FOREACH(iterator,array) \ for(typeof(array[0]) *iterator=&array[0];\ iterator<(array+sizeof(array)/sizeof(array[0]));\ iterator++) int arri[]={1,5,3,9}; FOREACH(ptr1,arri) printf("%d\n",*ptr1);
- Als Alternative zu einem 'langsamen' Funktionsaufruf (entspricht einer inline Funktion)
#define read_SCL(port) (*AT91C_PIOA_PDSR & i2c_mask[port][1]) #define set_SCL(port) (*AT91C_PIOA_SODR = i2c_mask[port][1]) #define clear_SCL(port) (*AT91C_PIOA_CODR = i2c_mask[port][1])
- Lesbareren und dennoch schnellen Code zu erzeugen
struct vl { struct vl *next; char keydata[]; }; struct vl ele3={NULL ,"key\0value"}; struct vl ele2={&ele3,"schluessel\0wert"}; struct vl ele1={&ele2,"schluessel\0wwwweeeerrrttt"}; struct vl *liste = &ele1; //Unleserlicher Code printf("Key='%s' Value='%s'\n",(char *)(&ele2+1), (char *)(&ele2+1)+strlen((char *)(&ele2+1))+1); //Besser lesbarer Code #define VL_KEY(vl) (char *)(vl+1) #define VL_VALUE(vl) (char *)(vl+1)+strlen(VL_KEY(vl))+1 printf("Key='%s' Value='%s'\n",VL_KEY(&ele1),VL_VALUE(&ele1));
- Für Debug/Release-Zwecke Debugaufrufe im Code zu belassen und diese im Bedarfsfall durch den Aufruf einer Debug-Routine oder durch nichts zu ersetzen
#define DEBUG_PRINT(a) printf(a) #define DEBUG_PRINT(a) //Makro wird durch nichts ersetzt #define DERIVATE(a) func1(a) #define DERIVATE(a) func2(a,2)
- Generisches Makro zum Sortieren eines Arrays (siehe [Quicksort as a C macro])
Sonstiges
[Bearbeiten]- Über den Ellipsis Puncturator '…' kann einem Function-like Makro beliebig viele Parameter übergeben werden:
#define ERROR(text, arg...) (fflush(stdout), fprintf(stderr, \ "\e[31m%s() Error:\e[30m " text "'\n", \ __func__, ##arg)) #define ERROR1(text, ...) (fflush(stdout), fprintf(stderr, \ "\e[31m%s() Error:\e[30m " text "'\n", \ __func__, __VA_ARGS__)) ERROR("Return-Wert=%d",ret); ERROR1("Var1=%d var2=%d",var1,var2);
- Einige Library- und OS-Funktionen sind als Makros implementiert. Beispielsweise:
assert() pthread_cleanup_pop()
Bedingte Übersetzung
[Bearbeiten]Die bedingte Übersetzung erlaubt es, Textbereiche ein- resp. auszublenden (d.h. Text Bereiche aus dem Source-Code zu entfernen/drinnen zu lassen).
Syntax:
#if const-expression-1
group-of-lines-1 //beliebige Anzahl an Zeilen, die bei gültiger Bedingungen
//eingeblendet, andernfalls ausgeblendet werden
#elif const-expression-2
group-of-lines-2
…
#else
group-of-lines-x
#endif
Const-expression
ist eine Integerkonstante oder ein Ausdruck, welcher durch den Präprozessor zu einer Integerkonstante ausgewertet wird. Wenn dieser 0 ist, gilt die Bedingung als nicht erfüllt, andernfalls als erfüllt. Mögliche Operatoren für den Konstantenausdruck sind bspw. ==
>=
!=
&
|
&&
||
+
-
<<
. Der Wertebereich der Integerkonstanten entspricht nach Standard-C dem Wertebereich von long
und ab C99 dem Wertebereich von intmax_t
(largests integer type found on the target).
Beispiel:
#define A 1
#if 1
#if A == 1
#if (A+1) == 1
#if (A>>2)&1 == 0x01
#define HALLO ich
#define HAL "ich"
#if HALLO == ich //OK Textvergleich
#if HAL == "ich" //KO Stringvergleich nicht möglich!
Hinweise:
- Der ergänzende Makro Operator
defined(name)
wird zu 1 aufgelöst, wennname
als Makro definiert wurde (egal, ob und welcher Inhalt zugewiesen wurde), andernfalls zu 0.#ifdef
und#ifndef
kombiniert die#if
Anweisung und dendefined
-Operator in einer Anweisung:
#ifdef name
entspricht#if defined name
#ifndef name
entspricht#if !defined name
#define A #if defined A #if ! defined (B) #ifdef A #ifndef B
- Ein nicht definiertes Makro wird zu 0 ersetzt. Ein definiertes Makro ohne Wertzuweisung wird durch nichts ersetzt, so dass ein Vergleich eines Makros mit einer Integerkonstanten ggf. fehlerhaft sein kann:
#define A 1 #define B #if A==1 #if B==0 //KO Präprozessorfehler (B wird durch nichts ersetzt) #if C==0 //OK (C wird durch 0 ersetzt)
- Bedingte Übersetzungen können beliebig verschachtelt sein.
#define MAKRO1 2 #define MAKRO2 1 #if MAKRO1==1 #if MAKRO2==1 #else #endif #elif MAKRO2==2 # if MAKRO==1 # else # endif #endif
Anwendung
[Bearbeiten]- Debug/Release (Im Debug-Mode zusätzliche Debug-Ausgaben aktivieren)
#ifdef NDEBUG #define DBGPRINT(val) #else #define DBGPRINT(val) printf("%s",val); #endif
- Host-System Abhängigkeiten nutzen (GCC,CLANG,Microsoft / Windows/Linux)
#ifdef __unix__
- Traget-Unabhängigkeiten (für unterschiedliche Prozessor, Ressourcenausstattung)
#ifdef __CHAR_UNSIGNED__ #if INT_MAX==32768 #if sizeof(int)==2 //KO Präprozessor ist sizeof nicht definiert
- Derivate-Steuerung (Low-Cost / High-Cost)
#define VERSION_CHROM_V90 90 #define VERSION_CHROM_V89 89 #if VERSION == VERSION_CHROM_V90 && defined __unix__
- Alternativ zur Derivate-Steuerung über
#if
#endif
empfiehlt sich dasIS_ACTIVE()
Makro aus dem Linux- und RIOT-Kernel. Dieses Makro wird zu 1 (Übergabeparameter definiert und mit 1 belegt) oder 0 (Übergabeparameter nicht definiert oder Wert != 1) aufgelöst, so dass der Compilerif(0) {...}
als 'toten' Code wegoptimiert resp. beiif(1) {...}
die IF-Anweisung wegoptimiert.
#define IS_ACTIVE(val) __is_active(val) #define __is_active(val) ___is_active(__PREFIX_WHEN_##val) #define __PREFIX_WHEN_1 0, #define ___is_active(arg1_or_junk) __take_second_arg(arg1_or_junk 1, 0, 0) #define __take_second_arg(__ignored, val, ...) val #define MA #define MA0 0 #define MA1 1 int main(int argc,char *argv[]) { int var=1; if(IS_ACTIVE(M)) { var|= 0b0001; } if(IS_ACTIVE(MA)) { var|=0b0010; } if(IS_ACTIVE(MA0)) { var|=0b0100; } if(IS_ACTIVE(MA1)) { var|=0b1000; }
Error/Warning Anweisung
[Bearbeiten]Erzeugung einer Compiler Warning/Fehler-Meldung mit dem Inhalt des preprocessor-tokens
(muss daher kein String sein).
Syntax: #error preprocessor-tokens
Syntax: #warning preprocessor-Tokens //Nur GNU-C
Anwendung
[Bearbeiten]- Zur Überprüfung, ob Makros 'richtig' gesetzt wurden
#define BUF_SIZE 511 #if (BUF_SIZE % 256) != 0 #error Bufsize muss ein vielfaches von 256 betragen #endif #if (BUF_SIZE & (BUF_SIZE -1)) != 0 #error Bufsize muss eine 2er Potenzzahl sein #endif #ifndef __unix__ #error Windows ist doof! #endif
Pragma
[Bearbeiten]Anweisung an den Compiler zum Aktivieren/Deaktivieren bestimmter Compiler-Funktionalitäten.
Syntax: #pragma preprocessor-tokens
Die preprocessor-tokens
sind Compiler-Abhängig!
Anwendung
[Bearbeiten]- Compiler-Optimierung für einzelne Funktionen einschalten
#pragma GCC push_options #pragma GCC optimize ("-O3") //Einschaltung der max. Compiler Optimierung void ws2812_send(void) { } //Vorherige Compiler Optimierung wieder herstellen. #pragma GCC pop_options
- Anweisung an den Compiler, dass nachfolgende Schleife 'parallelisiert' werden kann
//Quelle: https://gcc.gnu.org/onlinedocs/gcc/Loop-Specific-Pragmas.html void foo (int n, int *a, int *b, int *c) { int i, j; #pragma GCC ivdep for (i = 0; i < n; ++i) a[i] = b[i] + c[i]; }
Stringizing-Operator
[Bearbeiten]Wird einem Identifier im Ersatztext eines Function-like Makros ein #
vorangestellt, so wird bei der Ersetzung (durch den Präprozessor) das Argument durch Einschließen in doppelte Hochkommata in eine Zeichenkette umgewandelt (stringizing).
Beispiel:
#define TOSTR(X) #X
char string[] = "hallo";
//Ausgabe des Inhaltes von string gefolgt vom Name der Variablen als String
printf("'%s' '%s'", string, TOSTR( string ) );
Anwendung
[Bearbeiten]- Zum Aufbau einer eigenen Symboltabelle, in welcher der Name der Variablen und die Adresse der Variablen enthalten ist:
struct var {int *adr; char name[100];}; #define VAR(x) {&x,#x} int a,b,c; struct var symboltable[]={VAR(a),VAR(b),VAR(c)};
- Umwandlung einer Konstanten in einen String
#define TCP_PORT_DEFAULT 4711 int tcp_port=TCP_PORT_DEFAULT; #define STRINGIFY(x) #x #define STRINGIFY_RESOLVE(x) STRINGIFY(x) //Ein Stringconcatenate funktioniert nur über Stringkonstanten //Zum Umwandlung einer Integerzahl in eine Stringkonstante //bietet sich der Stringify-Operator an const char *message_help= "TCP-Port (Default: " STRINGIFY_RESOLVE(TCP_PORT_DEFAULT) ")\n";
Verkettung von Makroparametern / Token Merging / Token Concatenation
[Bearbeiten]Der Verkettungsoperator ##
erlaubt es, zwei Makroparameter innerhalb eines Function-like Makros zu einem zu verschmelzen.
Beispiel:
#define GLUE(X,Y) X ## Y
printf( "%d\n", GLUE(2, 34) ); //Gibt 234 als Zahl zurück
#define TEMP(i) temp##i
TEMP(1) = TEMP(2 +k) +x; //-->temp1=temp2+k+x;
#define PRIVATE(member) private_##member
struct class {
int PRIVATE(xyz);
};
obj.PRIVATE(xyz)=4711;
Anwendung
[Bearbeiten]- Umwandlung einer Konstanten in einen String
#define DEBUG_STATUS_DEBUG 0 #define DEBUG_STATUS_INFO 1 #define DEBUG_STATUS_WARN 2 #define DEBUG_STATUS_ERROR 3 #define DEBUG_STATUS_STR0 "Error+Warn+Info+Debug" #define DEBUG_STATUS_STR1 "Error+Warn+Info" #define DEBUG_STATUS_STR2 "Error+Warn" #define DEBUG_STATUS_STR3 "Error" #define DEBUG_STATUS_DEFAULT DEBUG_STATUS_WARN int debug_status= DEBUG_STATUS_DEFAULT; #define CAT(a,b) a ## b #define CAT_RESOLVE(a,b) CAT(a,b) const char *message_help= "Debug (Default=" CAT_RESOLVE(DEBUG_STATUS_STR,DEBUG_STATUS_DEFAULT) ")";
Hinweis:
- Auf Basis des Verkettungsoperators sind diverse nützliche Funktionalitäten darstellbar. Mehr dazu siehe [C Preprocessor tricks, tips, and idioms]