Programmeren in C/C-Preprocessor
De C-Preprocessor met zijn preprocessor-directives staat, technisch gezien, los van de C-Compiler en heeft tot taak de broncode "voor te koken", dat wil zeggen, losse bestanden in te voegen, stukken code conditioneel op te nemen of weg te laten en voorgedefineerde teksten te vervangen (macro expansion). Dit gebeurt allemaal voordat de C-Compiler zelf aan de beurt komt die uiteindelijk alleen het resultaat van de interpretatie ziet.
In dit hoofdstuk zullen de belangrijkste preprocessor constructies worden behandeld.
#include
[bewerken]Het #include statement voegt een bestand in (to include) in de output van de preprocessor. Het is de verantwoordelijkheid van de programmeur ervoor te zorgen dat het bestand dat wordt geinclude het correcte bestand is, de preprocessor let niet op de inhoud ervan. Het ingevoegde bestand wordt wel door de preprocessor behandeld.
Het #include statement kent twee vormen:
- #include "foobar.h, de preprocessor verwacht het bestand "foobar.h" in de huidige directory. Deze vorm wordt gebruikt voor die bestanden die bij het project zelf horen.
- #include <foobar.h>, de preprocessor zoekt naar het bestand in de bibliotheek-directories. Deze vorm wordt gebruikt voor interfaces van (standaard) biblotheken. De preprocessor zal alle opgegeven bibliotheek directories nazoeken om het bestand te vinden. Welke directories dit precies zijn, kan in de regel worden ingesteld door middel van compiler-opties.
Meestal (maar niet altijd) hebben de bestanden die op deze manier worden ingevoegd de bestands-extensie ".h", maar dit is geenzins een vereiste. Het is mogelijk, en niet eens zo vreselijk ongebruikelijk, niet alleen header-files (.h), maar ook andere formaten in te voegen, bijvoorbeeld specifieke stukken C broncode. Dit kan voordelen hebben, omdat op deze manier makkelijk verschillende stukken code voor verschillende versies van het uiteindelijke programma kunnen worden ingevoegd. Dit kan lelijk fout gaan als men per abuis de naam van het bestand zelf invoegt, dus in een bestand foobar.c een #include "foobar.c" neerzet. De consequenties hiervan zijn afhankelijk van de preprocessor in kwestie, maar leveren nooit het gewenste resultaat.
/* een aantal standaard-bibliotheek includes. Onder unix staan
* deze in de regel in de directory ''/usr/include'', maar de
* locatie is afhankelijk van de compiler.
*/
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
/* enkele OS-afhankelijke headers met relatieve paden ten
* opzichte van opgegeven library paden.
*/
#include <sys/acct.h>
#include <net/netinet.h>
/* een aantal project includes */
#include "foobar.h"
/* includes met relatieve paden (ten opzichte van de huidige
* directory).
*/
#include "foo/foobar.h"
#include "bar/foobar.h"
#define
[bewerken]Een #define directive wordt gebruikt om een macro te definiëren. Een macro is een bepaalde tekst die door de C-Preprocessor wordt vervangen door de gewenste waarde. Deze macro's worden vaak gebruikt voor constante waarden, zodat deze slechts op een plaats hoeven te worden gewijzigd, in plaats van op verschillende plekken in de source code.
/* definiëer FOOBAR */
#define FOOBAR 123
/* en "ondefinieer" FOOBAR weer */
#undef FOOBAR
Het verdient aanbeveling om in de code alle constanten met behulp van een macro te definiëren. Als ergens in de sourcecode ergens een getal 10 opduikt, is het niet duidelijk wat dat getal precies betekent. Door er met een macro een goed-gekozen naam aan te geven, is het onmiddellijk duidelijk welke functie die 10 heeft.
#define NUM_USERS 10
#define RECORD_SiZE 10
#define MAX_LOG_ENTRIES_PER_DAY 10
Als NUM_USERS verandert (de directie heeft eindelijk besloten de stokoude, overbelaste server te vervangen) hoeft de waarde 10 maar op een plaats vervangen te worden. Als de 10 hardcoded in de broncode staat, moeten alle bestanden apart worden nagegaan en voor iedere 10 moet bekeken worden of het om het aantal gebruikers gaat, de afmetingen van een record of het maximum aantal log-meldingen per dag. Onnodig te vermelden dat dat een hoofdpijn-klus is. Lijstjes als hierboven (en vaak nog veel langere) zijn dan ook een vast onderdeel van de broncode.
Het is een goede gewoonte (en is bij professionele organisaties veplicht) om namen van macros uitsuiltend in uppercase te schrijven, ter onderscheiding van variabelen en functies. Iedere ervaren programmeur heeft zich al eens het hoofd gebroken over het onverwachtte gedrag dat voortvloeit uit verwarring over dit onderscheid. Het resultaat bestaat meestal uit rare compiler-foutmeldingen (de macro-expansie is immers prima gelukt) in stukken code die er verder prima uitzien, of onverwacht gedrag in runtime. Ervaring is recht evenredig met het aantal fouten dat je gemaakt hebt, zegt men dan.
Omdat sommige macro-definities vrij lang zijn, kunnen meerdere regels worden gebruikt. Dit wordt aangegeven door middel van een backslash (\) direct voor het newline-karakter. Dat wil zeggen dat na de backslash geen spaties of tabs meer mogen volgen. Technisch gesproken is dit "escaping the newline", het ontsnappen aan het nieuwe-regel-karakter. De backslash staat dan ook wel bekend als een escape.
/* macros met argumenten */
#define FOO(bar) (2*(bar)+1)
/* macro bestaande uit meerdere regels */
#define HELLO(world) \
printf("Hello %s\n", world)
Het is van belang te begrijpen dat het hier om tekstuele vervanging gaat. Als de boven gedefinieerde macro HELLO meermaals in de tekst wordt gebruikt, zal het printf-statement evenzovele keren in de output-stream opduiken. Bij korte macro's is dit over het algemeen geen probleem, maar in sommige gevallen zijn macro's er omvangrijk en kan veelvuldig gebruik een object-file opleveren die vele malen groter is dan verwacht. Verder zijn macro's, omdat het om een tekstuele vervanging gaat, soms lastig te volgen en te debuggen. Overdadig gebruik moet dan ook beslist worden afgeraden.
Een tweede consequentie is dat als er een fout wordt gemaakt in de macro-definitie, de C-Compiler (en niet de C-Preprocessor!) een foutmelding zal geven op de plek waar de macro wordt gebruikt. Omdat op die plek alleen de macro-aanroep staat, die prima in orde is, en niet de macro definitie, kan dit zeer verwarrend werken.
Tenslotte is het handig om er rekening mee te houden dat de macro-aanroep midden in een expressie kan staan. Omdat macro-expansie tekstueel plaatsvindt kan dit rare effecten hebben in verband met operator-precedentie, vooral als er sprake is van macro's met argumenten. Het is dan ook ten sterkste aan te raden niet zuinig te zijn met haakjes en de precedentie altijd expliciet te maken.
/* macros met argumenten */
#define FOUT(oeps) 2*oeps +1
#define GOED(prima) (2*(prima)+1)
int fout = 5*FOUT(2); /* 5 * 2 * 2 + 1 = 21 (niet wat je verwachten zou...) */
int nog_fouter = 5*FOUT(1+1); /* 5 * 2 * 1 + 1 + 1 = 12 (echt fout, dus...) */
int goed = 5*GOED(2); /* 5 * ( 2 * (2) + 1) = 5 * 5 = 25 */
int ook_goed = 5*GOED(1+1); /* 5 * ( 2 * (1+1) + 1) = 5 * (2 * 2 + 1) = 5 * 5 = 25 */
#if, #ifdef, #ifndef
[bewerken]Met behulp van conditionele preprocessor directives kunnen selectief stukken code worden opgenomen of genegeerd. Dit is buitengewoon handig in velerlei omstandigheden, selectief opnemen van debug-code, bijvoorbeeld. De algemene structuur is die van een if-then-else constructie zoals die in vele talen voorkomt.
#if (<constant_expressie>)
[bewerken]De #if preprocessor directive voegt afhankelijk van de expressie code in. Het is belangrijk daarbij op te merken dat de expressie alleen mag bestaan uit elementen die bekend zijn op het moment dat de C-Preprocessor actief is. Het is dus nutteloos (en zelfs fout) in de expressie C-variabelen of functie-aanroepen te gebruiken, die zijn namelijk pas bekend als de C-Compiler aan de beurt komt. De compiler zelf ziet uiteindelijk maar een van de printf-statements.
#if (FOOBAR == 2)
printf("FOOBAR is gedefinieerd en FOOBAR is gelijk aan 2\n");
#else
printf("FOOBAR is niet gedefinieerd of FOOBAR is niet gelijk aan 2\n");
#endif
Het volgende voorbeeldje (ontleend aan real life code van een beunhaas) werkt niet naar behoren, wat de beunhaas in kwestie erg verbaasde. Het probleem zit er natuurlijk in dat sizeof(int) == 4 geen constant_expressie is als de preprocessor aan het werk is. Deze heeft geen notie van int en kent geen sizeof operator.
/* HEEL ERG FOUT! */
#if (sizeof(int) == 4)
printf("sizeof(int) == 4\n");
#else
printf("sizeof(int) != 4\n");
#endif
Deze constructie wordt ook af en toe gebruikt om bepaalde stukken code "uit te commenten", dat wil zeggen, uit te schakelen. Dat ziet er dan als volgt uit:
void foobar(void)
{
printf("Dit is code die aangeschakeld is\n");
#if 0
/* Wat hier staat is volstrekt onbelangrijk, want
* de compiler zal het nooit te zien krijgen. De
* 0 in "#if 0" zorgt ervoor dat alles tot "#endif"
* weggelaten wordt.
*/
printf("Dit is code die uitgeschakeld is.\n");
#endif
}
#ifdef en #ifndef
[bewerken]Deze constructies werken op dezelfde manier als de #if-directive, met dien verstande dat de genoemde macro wel (#ifdef) of niet (#ifndef) gedefinieerd is.
#ifdef FOOBAR
printf("FOOBAR is gedefinieerd\n");
#else
printf("FOOBAR is niet gedefinieerd\n");
#endif
en
#ifndef FOOBAR
printf("FOOBAR is niet gedefinieerd\n");
#else
printf("FOOBAR is gedefinieerd\n");
#endif
De #ifndef constructie wordt veel gebruikt als guard (wachtpost) in header bestanden om te voorkomen dat interfaces dubbel worden gedefineerd als een header om de een of andere reden tweemaal wordt ingevoegd (met '#include "foobar.h"). Dit kan makkelijker optreden dan beginnelingen soms denken. Om deze reden is het gebruikelijk headerfiles met deze constructie te beschermen.
/*
* FILE: foobar.h
* AUTHOR: Pietje Puk <p.puk@nowhere.net>
* PURPOSE: Barring the Foo.
*/
#ifndef GUARD_FOOBAR
#define GUARD_FOOBAR
/* Hier staat de echte interface. Zodra deze eenmaal is ingevoegd is "GUARD_FOOBAR" gedefinieerd en zullen
* de volgende #include directives dit stuk overslaan. Handig ....
*/
struct foobar
{
int foo;
char* bar;
};
#endif
/*
* End of file
*/
Voorgedefineerde macro's, __FILE__ en __LINE__
[bewerken]De preprocessor houdt intern een aantal standaard macro's bij met een grote variëteit aan functionaliteiten. Hiermee zijn dingen als het operating system, de compiler in kwestie en een groot aantal andere zaken beschikbaar. Hiet voert te ver ze hier allemaal te vermelden, en bovendien zijn veel van deze voorgedefinieerde macro's compiler-specifiek. Raadpleeg de documentatie van je compiler voor een volledig overzicht.
Het bovengenoemde tweetal verdient echter specifieke aandacht (ze worden ook vereist door de standaard).
Macro | Resultaat |
---|---|
__FILE__ | De naam van het huidige bestand. |
__LINE__ | Het regelnummer waarop de macro wordt aangeroepen. |
In de praktijk wordt deze functionaliteit vaak gebruikt om fouten op te sporen. In de source code ziet dat er dan ongeveer zo uit:
#ifdef DEBUG_MODE
#define ERROR(text) fprintf(stderr, "file %s, line %d : %s", __FILE__, __LINE__, text)
#else
#define ERROR(text)
#endif
iedere keer als ERROR wordt aangeroepen, worden de bestandsnaam en het regelnummer automatisch ingevoegd, wat foutmeldingen een stuk makkelijker traceerbaar maakt, maar als DEBUG_MODE niet is gedefineerd, wordt er geen enkele foutmelding gegenereerd. Constructies als deze (en er zijn nog veel fraaiere en handiger exemplaren) besparen veel tijd bij het zoeken naar fouten.