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!

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

Öffnen im Compiler Explorer (mit Compilerschalter -E)
Öffnen im Compiler Explorer (mit Preprocessor Fenster)

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:
Öffnen im Compiler Explorer (Compilerinterne Suchpfade)

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!

Öffnen im Compiler Explorer

  • 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

Öffnen im Compiler Explorer

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 )

Öffnen im Compiler Explorer

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

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

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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)

Öffnen im Compiler Explorer

  • 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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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!

Öffnen im Compiler Explorer

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)

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

Hinweise:

  • Ein nicht definiertes Makro wird zu 0 ersetzt. Ein definiertes Makro ohne Wertzuweisung wird durch nichts ersetzt, so dass ein Vergleich mit einer Integerkonstanten fehlerhaft ist:
#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)

Öffnen im Compiler Explorer

  • Der ergänzende Makro Operator defined(name) wird zu 1 aufgelöst, wenn name als Makro definiert wurde (egal, ob und welcher Inhalt zugewiesen wurde), andernfalls 0. Die Kombination von defined und der #if Anweisung lautet:
  • #ifdef name entspricht #if defined name
  • #ifndef name entspricht #if !defined name
#define A
#if   defined A
#if ! defined (B)
#ifdef A
#ifndef B

Öffnen im Compiler Explorer

  • 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

Öffnen im Compiler Explorer

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__

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

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;

Öffnen im Compiler Explorer

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

Öffnen im Compiler Explorer

Hinweis: