A makró látszólag lehet konstans, függvény vagy rövidített utasítás, de a C nyelvben ezek egyikével sem azonos: az előfeldolgozó, vagyis a preprocessor helyettesítési szabálya. Ez a különbség akkor válik veszélyessé, amikor egy ártalmatlannak tűnő makróhívás megváltoztatja a műveleti sorrendet, vagy többször hajt végre egy mellékhatást.
A Headerek, deklarációk és include-higiénia részben látott headerek makródefiníciókat is közvetíthetnek. Ezek azonban nem követik a függvények és változók blokkszintű hatóköreit. A példákhoz A szükséges C szintaxis tömör áttekintése kifejezés- és függvényfogalmait használjuk.
A nyelvi alap az ISO C17; az eltéréseket külön jelöljük. A teljes példa kijelölt laborprofilja GCC 14.2.0 vagy Clang 19.1.7, Linux x86-64 System V ABI, LP64 és glibc 2.40. A bemutatott eredmények levezetett, várt eredmények, nem dokumentált futtatási megfigyelések.
Az object-like macro, azaz objektumszerű makró egy azonosítóhoz helyettesítési listát rendel. A névben szereplő „objektum” itt nem nyelvi memóriaobjektumot jelent: a makrónak nincs címe, típusa vagy tárolója.
Az alábbi C17-részlet két definíciót mutat:
#define RETRY_LIMIT 3
#define WINDOW_SIZE 2 + 3A RETRY_LIMIT megfelelő előfordulásának helyére a
3 előfeldolgozási token kerül. A WINDOW_SIZE
három tokent helyettesít be: 2, +,
3.
Ezért:
/* C17-részlet. */
int capacity = 10 * WINDOW_SIZE;a kifejtés után lényegében:
int capacity = 10 * 2 + 3;A fordító ezt értékű kifejezésként értelmezi, nem pedig alakban. A javítás:
#define WINDOW_SIZE (2 + 3)A helyettesítés tokenalapú, nem tetszőleges
részszövegek cseréje. A RETRY_LIMIT_EXTRA azonosító része
nem cserélődik ki, és a "RETRY_LIMIT" sztringliterál
belseje sem.
A function-like macro, vagyis függvényszerű makró paramétereket fogad:
#define SQUARE(x) ((x) * (x))Definíciókor a makrónév és a nyitó zárójel között nem lehet szóköz. A
#define SQUARE (x) már objektumszerű makrót definiálna.
Használatkor viszont a SQUARE (3) is kiváltja a
függvényszerű makró kifejtését.
A makró argumentuma tokensorozat, nem előre kiszámított érték. Az argumentumokat vessző választja el, kivéve a belső, párosított kerek zárójeleken belüli vesszőket. A kapcsos zárójel önmagában nem védi meg a vesszőt az argumentumelválasztó szereptől.
Egy kifejezésmakróban rendszerint külön kell zárójelezni minden paraméterelőfordulást és az egész eredményt:
#define BAD_SQUARE(x) x * x
#define SQUARE(x) ((x) * (x))A BAD_SQUARE(2 + 3) kifejtése:
2 + 3 * 2 + 3Először a szorzás történik meg, ezért az eredmény . A javított változat:
((2 + 3) * (2 + 3))eredménye .
A zárójelezés azonban nem szünteti meg a többszörös
kiértékelést. A SQUARE(n++) hibás, UB-s tanítási
példa: kifejtése ((n++) * (n++)), amelyben ugyanazon skalár
objektum két módosítása között nincs megfelelő sorrendi kapcsolat. Ehhez
nincs garantált eredmény; nem helyes azt mondani, hogy „legfeljebb
kétszer növeli”.
Nem minden többszörös kiértékelés UB. A következő önálló program definiált, de meglepő viselkedést szemléltet.
Fájl: macro_demo.c; minimum C17, a fent megadott
laborprofil.
#include <stdio.h>
#define BAD_SQUARE(x) x * x
#define SQUARE(x) ((x) * (x))
#define MIN_EXPR(a, b) ((a) < (b) ? (a) : (b))
static inline int min_int(int a, int b)
{
return a < b ? a : b;
}
int main(void)
{
int n = 3;
int macro_result = MIN_EXPR(n++, 10);
int after_macro = n;
n = 3;
int function_result = min_int(n++, 10);
if (printf("%d %d\n",
BAD_SQUARE(2 + 3), SQUARE(2 + 3)) < 0) {
return 1;
}
if (printf("%d %d; %d %d\n",
macro_result, after_macro,
function_result, n) < 0) {
return 1;
}
return 0;
}Külön laborban használható fordítási parancsok:
gcc -std=c17 -Wall -Wextra -Wpedantic -O0 macro_demo.c -o macro_demo-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -O0 macro_demo.c -o macro_demo-clangBizonyításvázlat a makrós és függvényes változat eltérésére. A makró helyettesítése:
((n++) < (10) ? (n++) : (10))n++ értéke 3, mellékhatása után
n értéke 4.n++ értéke 4, majd
n értéke 5.Így a makrós eredmény 4, a végső n pedig
5. A függvényhívás egyetlen n++ argumentumot
értékel ki: a függvény 3 és 10 értéket kap,
ezért az eredmény 3, a végső n pedig
4.
Sikeres kimeneti írás mellett a várt sorok:
11 25
4 5; 3 4
A static inline függvény valódi, típusellenőrzött
függvény. Az inline nem garantálja a gépi kód
beillesztését, és nem ez biztosítja az egyszeri argumentumkiértékelést:
az a függvényhívás tulajdonsága. Részletes kapcsolási szabályait a static inline, _Generic és headeres eszköztár
tárgyalja.
A függvény sem tesz minden hívást biztonságossá: a
min_int(n++, n++) C17-ben továbbra is UB. A külön
argumentumok kiértékeléséhez a nyelv nem biztosít itt szükséges
sorrendet; ezt a sequencing szabályai
részletezik.
Közönséges helyettesítéskor az argumentumokban található makrók előbb
kifejtődnek. A helyettesítés eredményét az előfeldolgozó újra
megvizsgálja további makrók szempontjából. Két fontos kivétel a
# és a ## közvetlen operandusaként szereplő
paraméter.
A # stringification, azaz sztringesítés: az
argumentum tokenjeiből sztringliterált készít. A ##
token pasting, azaz tokenösszeillesztés: két tokenből egyetlen
új előfeldolgozási tokent alkot.
/* C17-részlet. */
#define SLOT 7
#define STR_RAW(x) #x
#define STR(x) STR_RAW(x)
#define CAT_RAW(a, b) a ## b
#define CAT(a, b) CAT_RAW(a, b)A különbségek:
| Használat | Kifejtés | Indok |
|---|---|---|
STR_RAW(SLOT) |
"SLOT" |
A # mellett nincs előzetes argumentumkifejtés. |
STR(SLOT) |
"7" |
A külső szint előbb kifejti a SLOT makrót. |
CAT_RAW(metric_, SLOT) |
metric_SLOT |
A ## mellett nincs előzetes kifejtés. |
CAT(metric_, SLOT) |
metric_7 |
A külső szint a második argumentumot 7-re fejti
ki. |
A sztringesítés nem a forrás karakterhű másolata: például a belső whitespace-sorozatokat egyetlen szóközzé alakítja. Az összeillesztésnek érvényes előfeldolgozási tokent kell eredményeznie; nem alkalmas tetszőleges szövegek összeragasztására.
A kétlépcsős minta hasznos generált azonosítókhoz és diagnosztikai sztringekhez. Közönséges számításhoz azonban többnyire szükségtelen bonyolítás.
Az X-macro technika egy adatleírást egyetlen helyen,
egy listamakróban rögzít, és ebből több kódrészletet generál: a struct
deklarációját, a tagok számát, a név szerinti kiírást vagy egy
szerializálót. A listamakró paramétere maga is egy makró (szokásosan
X), amelyet minden felhasználási hely másképp definiál. Így
egy új tag felvétele egyetlen sor, és a struct, a számláló és a kiírás
nem csúszhat el egymástól.
/* xmacro.c — C17: egyetlen taglistából struct, tagszám és kiírás */
#include <stddef.h>
#include <stdio.h>
/* A taglista egyetlen helyen: X(típus, név, printf-formátum). */
#define CONFIG_FIELDS(X) \
X(int, port, "%d") \
X(unsigned, workers, "%u") \
X(const char *, bind, "%s")
/* 1. A struct a listából épül. */
#define DECLARE_FIELD(type, name, fmt) type name;
struct config {
CONFIG_FIELDS(DECLARE_FIELD)
};
#undef DECLARE_FIELD
/* 2. A tagszám a listából számolódik: minden tag egy "+ 1". */
#define COUNT_FIELD(type, name, fmt) + 1
enum { CONFIG_FIELD_COUNT = 0 CONFIG_FIELDS(COUNT_FIELD) };
#undef COUNT_FIELD
/* 3. A kiírás is a listából generálódik: a nevet a # operátor adja. */
#define PRINT_FIELD(type, name, fmt) \
if (printf("%s=" fmt "\n", #name, c->name) < 0) return -1;
static int print_config(const struct config *c)
{
CONFIG_FIELDS(PRINT_FIELD)
return 0;
}
#undef PRINT_FIELD
_Static_assert(CONFIG_FIELD_COUNT == 3, "a taglista három elemű");
int main(void)
{
const struct config c = { 8080, 4u, "127.0.0.1" };
printf("fields=%d\n", CONFIG_FIELD_COUNT);
return print_config(&c) == 0 ? 0 : 1;
}A CONFIG_FIELDS(DECLARE_FIELD) előfeldolgozás után
int port; unsigned workers; const char * bind; lesz, a
tagszám pedig a 0 + 1 + 1 + 1 konstans kifejezés. A
kiírásban a #name sztringesítés (lásd fent) adja a tag
nevét. Minden felhasználás után #undef-eljük a segédmakrót,
hogy a következő felhasználás saját definíciót adhasson neki.
A technika korlátai: a lista és a típus szinkronja csak addig garantált, amíg minden tag ténylegesen a listából jön létre; a hibaüzenetek a makrókifejtésre mutatnak, nem a listára; a debugger és sok eszköz a kifejtett kódot látja. A C++26 statikus reflexiója (C++26 nyelvi kitekintés) éppen ezt a kézi listát váltja ki: ott a tagok listája magából a típusból kérdezhető le.
Buildjegyzőkönyv (lab-gcc profil,
gcc -std=c17 -Wall -Wextra -Wpedantic -O2). A program
figyelmeztetés nélkül fordult, kimenete: fields=3,
port=8080, workers=4,
bind=127.0.0.1. A gcc -E -P kimenete a fenti
struct-deklarációt és CONFIG_FIELD_COUNT = 0 + 1 + 1 + 1-et
mutatta.
ISO C99-től, így C17-ben is használható variadikus makró:
/* C17-részlet; <stdio.h> szükséges. */
#define LOG(...) fprintf(stderr, __VA_ARGS__)Például a LOG("ready\n") és a
LOG("attempt=%d\n", attempt) egyaránt használható. A
__VA_ARGS__ a változó argumentumrész tokenjeit helyettesíti
be.
Rögzített paraméter után más a helyzet:
#define TRACE(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)C17-ben a TRACE("ready\n") elhagyott variadikus
argumentummal nem szabványos használat. A
TRACE("ready\n", ) pedig a helyettesítés után fölösleges
vesszőt hagy a függvényhívásban.
ISO C23-ban a __VA_OPT__ erre
szabványos megoldást ad:
/* Kizárólag C23-részlet. */
#define TRACE(fmt, ...) \
fprintf(stderr, fmt __VA_OPT__(,) __VA_ARGS__)A vessző csak nem üres variadikus tokenlista esetén jelenik meg. A
GNU-féle , ##__VA_ARGS__ vesszőelnyelő megoldás
fordítóbővítés, nem C17-es helyettesítője ennek.
A C17-es fallback a teljes argumentumlistát fogadó
LOG(...), vagy két külön nevű makró. A C23-as alak
használata előtt a tényleges fordítótámogatást külön ellenőrizni kell;
ezt a Preprocessor-labor és feature detection
folytatja.
Két csupasz utasítást helyettesítő makró megtörheti az
if–else szerkezetet. A szokásos burkolás:
/* C17-részlet. */
#define ADD_AND_COUNT(total, count, value) \
do { \
(total) += (value); \
++(count); \
} while (0)Használata:
/* C17-részlet. */
if (accepted)
ADD_AND_COUNT(total, count, value);
else
++rejected;A do törzse egyszer végrehajtódik, majd a nulla feltétel
lezárja a ciklust. Az egész szerkezet egyetlen utasítás; a pontosvesszőt
a hívó írja.
A példa szerződése szerint total és count
módosítható egész változók, és egyik aritmetikai művelet sem lép túl a
típus megengedett tartományán. A burkolás csak szintaktikai védelmet ad:
nem biztosít típusbiztonságot, nem javítja a paraméterek ismételt
használatát, és a benne szereplő break vagy
continue célpontját is befolyásolja.
A makróba bevezetett lokális név ráadásul ütközhet a hívó nevével. A projektprefix csökkenti ezt a kockázatot, de nem hoz létre valódi névhigiéniát.
A #if fordítás előtt választ tokenszakaszokat; nem
futásidejű if. A #ifdef NAME csak a makró
létezését vizsgálja. Ezért egy nullára definiált kapcsolóra is igaz.
Egy C17-es konfigurációs részlet:
#include <stdio.h>
#ifndef APP_TRACE
#define APP_TRACE 0
#endif
#if (APP_TRACE != 0) && (APP_TRACE != 1)
#error "APP_TRACE must be 0 or 1"
#endif
#if APP_TRACE
#define APP_LOG(...) fprintf(stderr, __VA_ARGS__)
#else
#define APP_LOG(...) ((void)0)
#endifA konfiguráció szerződése: az APP_TRACE numerikus
0 vagy 1. A GCC/Clang
-DAPP_TRACE=1 kapcsolója fordítóeszköz-funkció, nem C
nyelvi szintaxis.
A defined(APP_TRACE) az előfeldolgozási feltételben a
definíció létezését vizsgálja; a #ifdef APP_TRACE ennek
egyszerűbb alakja. A #error diagnosztikát követel meg,
ezért alkalmas tiltott konfiguráció jelzésére; a buildben ezt hibának
kell kezelni.
A #if makrókifejtése után megmaradó közönséges
azonosítókat az előfeldolgozó nullának tekinti. Egy elgépelés így
észrevétlenül kikapcsolhat egy ágat. A GCC/Clang -Wundef
figyelmeztetése ebben segíthet. C-változó, enum-konstans
vagy sizeof nem használható úgy, mintha a #if
a C fordító teljes kifejezésértelmezője volna.
Fontos hibaminta:
/* C17-részlet: konfigurációfüggő mellékhatás! */
APP_LOG("processed=%d\n", ++processed);Kikapcsolt naplózásnál a növelés is eltűnik. A program szükséges állapotváltozását külön utasításba kell tenni. Mindkét konfigurációt fordítani és ellenőrizni kell: az inaktív ág sikeres kihagyása nem bizonyítja annak helyességét.
Fejtsd ki a #define DOUBLE(x) x + x makróval a
3 * DOUBLE(2 + 1) kifejezést! Mi az értéke? Javítsd a
zárójelezést, majd indokold meg, miért veszélyes továbbra is a
DOUBLE(n++).
Írj static inline int twice_int(int x) függvényt a
bemeneti tartományra. Milyen eredményt és végső n értéket
vársz, ha kezdetben n = 4, és a hívás
twice_int(n++)?
Miért jelez hibát az alábbi, szándékosan hibás C17-részlet? Javítsd
úgy, hogy csak az APP_TRACE egyes értéke legyen
tiltott!
#define APP_TRACE 0
#ifdef APP_TRACE
#error "Trace mode is forbidden in this build"
#endifA kifejtés 3 * 2 + 1 + 2 + 1, értéke
.
A javítás:
#define DOUBLE(x) ((x) + (x))Ezzel az eredmény
.
Az ellenőrzési feltétel a tokenkifejtésből kapott műveleti sorrend. A
DOUBLE(n++) továbbra is két, egymáshoz képest nem
megfelelően rendezett módosítást eredményez, tehát UB.
/* C17-részlet; a macro_demo.c main függvénye elé illeszthető. */
static inline int twice_int(int x)
{
return x + x;
}A megadott tartományban az eredmény
,
amelyet minden szabványos C17-es int képes ábrázolni. Az
argumentum egyszer értékelődik ki: az eredmény 8, a végső
n értéke 5. Ez a két érték az ellenőrzési
feltétel.
Az APP_TRACE definiálva van, ezért a #ifdef
ága aktív, függetlenül a nulla értéktől. A javítás:
#if APP_TRACE
#error "Trace mode is forbidden in this build"
#endifEllenőrzési feltétel: 0 mellett ez a direktíva ne
okozzon diagnosztikát, 1 mellett okozzon. Külső
buildkonfigurációhoz a fix definíció helyett a korábbi
#ifndef-es alapértékminta szükséges.
A makró tokenhelyettesítés, nem függvényhívás. A zárójelezés védi a
precedenciát, de nem akadályozza meg a mellékhatások ismétlését.
Számításokhoz általában típusellenőrzött függvényt válassz; makrót ott
használj, ahol valóban előfeldolgozás szükséges. A #,
##, variadikus argumentumok és konfigurációs kapcsolók
esetén a kifejtett tokensorozatot vizsgáld, ne csupán a hívás
látszólagos jelentését.