Menu

Makra w C: #define, makra funkcyjne i zasady nawiasów

Makra obiektowe i funkcyjne z #define: dlaczego każdy argument i całe ciało potrzebują nawiasów, pułapka wielokrotnego obliczania, makra wieloliniowe z do-while(0) i kiedy lepszym narzędziem jest funkcja albo const.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Makro to nazwa, którą preprocesor zastępuje jakimś tekstem, zanim uruchomi się kompilator. To cały mechanizm, a każda reguła na tej stronie z niego wynika: makra nie mają typów, nie respektują zasięgu, nie obliczają argumentów i nie wiedzą, czym jest wywołanie funkcji. Kopiują tekst.

Używane ostrożnie są niezastąpione: przełączniki przenośności, makra asercji przechwytujące numer linii, stałe czasu kompilacji działające w rozmiarach tablic. Używane niedbale produkują błędy, które wyglądają na niemożliwe, dopóki nie rozwiniesz pliku.

Makra obiektowe

Najprostsza forma definiuje nazwę, która oznacza wartość:

Zwyczajowo takie nazwy zapisuje się jako UPPER_SNAKE_CASE, żeby czytelnik od razu widział, że symbol to makro, a nie zmienna. Ta konwencja ma tu większe znaczenie niż zwykle, bo makra ignorują zasięg: #define wewnątrz funkcji nadal obowiązuje w reszcie pliku i bez oporów przepisze zmienną o tej samej nazwie w innej funkcji.

#undef NAME usuwa definicję, więc nazwę można później zdefiniować na nowo bez ostrzeżenia o redefinicji.

Makra funkcyjne

Umieść listę parametrów bezpośrednio po nazwie, bez spacji przed nawiasem otwierającym, a makro będzie przyjmować argumenty:

Reguła spacji jest prawdziwa: #define SQUARE (x) ((x)*(x)) definiuje makro obiektowe o nazwie SQUARE, którego tekst zastępczy zaczyna się od (x). Błąd kompilatora, który potem wystąpi, nie wspomni o spacji.

Zasady nawiasów

Są dwie i obie istnieją dlatego, że ciało jest wklejane w wyrażenie, którego nie widzisz, pisząc makro.

Zasada 1: każdy parametr w nawiasach. Bez tego argument, który sam jest wyrażeniem, zostaje rozerwany przez priorytety operatorów:

#define SQUARE_BAD(x) x * x

SQUARE_BAD(2 + 3)   /* staje się 2 + 3 * 2 + 3  ==  11, a nie 25 */

Zasada 2: całe ciało w nawiasach. Bez tego otaczające wyrażenie rozrywa wynik:

#define DOUBLE_BAD(x) (x) + (x)

10 / DOUBLE_BAD(5)  /* staje się 10 / (5) + (5)  ==  7, a nie 1 */

Uruchom obie porażki obok ich poprawionych wersji:

Obie złe wersje kompilują się bez jednego ostrzeżenia i dają złe liczby. Nawyk do wyrobienia jest mechaniczny: każdy parametr w nawiasy, potem wynik w nawiasy. ((a) > (b) ? (a) : (b)) wygląda na zaszumione, a ten szum jest celowy.

Pułapka wielokrotnego obliczania

Nawiasy nie uratują cię przed drugim zagrożeniem. Makro wkleja tekst argumentu w każdym miejscu, w którym pojawia się parametr, więc argument z efektem ubocznym wykonuje się więcej niż raz:

Makro rozwija się do ((i++) > (j) ? (i++) : (j)). Porównanie zwiększa i do 6 i porównuje 5 z 3; gałąź prawdy znowu zwiększa i do 7 i daje 6. Zatem m to 6, a i to 7, a żadna z tych wartości nie jest tym, co powinno dać "większe z i i j". Funkcja max(i++, j) działałaby poprawnie, bo argument jest obliczany raz przed wywołaniem.

Ta sama pułapka łapie wszystko, co ma efekt uboczny: SQUARE(rand()) wywołuje rand() dwa razy i mnoży dwie różne liczby. CHECK(read_byte()) pochłania dwa bajty.

Dwie obrony, w kolejności preferencji:

  1. Użyj prawdziwej funkcji. Jeśli potrzebujesz jej dla kilku typów, napisz po jednej na typ albo użyj funkcji static inline w nagłówku.
  2. Jeśli to musi być makro, udokumentuj wyraźnie, że argumenty są obliczane więcej niż raz, i nie umieszczaj efektów ubocznych w miejscach wywołania.

Makra wieloliniowe i do-while(0)

Ciało makra może obejmować kilka linii, jeśli każda linia poza ostatnią kończy się ukośnikiem wstecznym. Naiwna wersja wygląda dobrze:

#define LOG_TWICE(msg) \
    printf("%s\n", msg); \
    printf("%s\n", msg)

A potem psuje się po cichu dokładnie tam, gdzie ma to znaczenie:

if (verbose)
    LOG_TWICE("hello");
else
    printf("cicho\n");

Po podstawieniu if obejmuje tylko pierwszy printf, drugi wykonuje się bezwarunkowo, a else nie ma już pasującego if. To błąd kompilacji, którego komunikat nie wskazuje niczego przydatnego. Objęcie ciała zwykłymi klamrami nie pomaga: końcowy ; po LOG_TWICE("hello") staje się pustą instrukcją, która kończy if, i else znowu się psuje.

Działa idiom do { ... } while (0):

do { ... } while (0) to pojedyncza instrukcja, która wykonuje swoje ciało dokładnie raz i wymaga średnika po sobie, więc miejsce wywołania wygląda jak zwykłe wywołanie funkcji i zachowuje się tak w każdym kontekście przepływu sterowania. Uważaj na ukośniki wsteczne: zabłąkana spacja po ukośniku kończy kontynuację i powoduje niezrozumiały błąd.

Makra predefiniowane i wzorce asercji

Preprocesor sam definiuje kilka makr i to z ich powodu niektórych rzeczy po prostu nie da się zrobić funkcją:

__FILE__ i __LINE__ rozwijają się tam, gdzie zostały napisane, więc umieszczenie ich w makrze przechwytuje pozycję wywołującego. Funkcja by tego nie potrafiła: wewnątrz funkcji zawsze zgłaszałyby plik i linię samej funkcji logującej. Dokładnie tak standardowe makro assert zgłasza położenie niespełnionego wyrażenia.

Inne przydatne nazwy predefiniowane: __DATE__, __TIME__ i __STDC_VERSION__ (na przykład 201710L dla C17).

Dwa operatory, które warto znać

Wewnątrz ciała makra # zamienia parametr w literał łańcuchowy ("stringizing"), a ## skleja tokeny ("token pasting").

SHOW(width * height) staje się printf("width * height" " = %d\n", (width * height));: kompilator łączy sąsiadujące literały łańcuchowe, więc jedno wywołanie wypisuje zarówno tekst wyrażenia, jak i jego wartość. To sztuczka debugowania warta zapamiętania.

## jest rzadszy i pojawia się głównie w makrach generujących kod: #define MAKE_VAR(n) int var_##n zamienia MAKE_VAR(3) w int var_3. Używaj go oszczędnie; identyfikatorów zbudowanych przez preprocesor nie da się wyszukać po nazwie, co utrudnia poruszanie się po kodzie.

Makra, funkcje i const

Używaj najsłabszego narzędzia, które wykona zadanie:

  • Zmienna const: const double PI = 3.14159; ma typ, podlega zasięgowi, widać ją w debuggerze i nie może jej przedefiniować niezwiązany nagłówek. Wybieraj ją dla wartości używanych w czasie działania.
  • enum: enum { MAX_USERS = 100 }; daje nazwaną stałą całkowitą, która działa tam, gdzie C wymaga stałej czasu kompilacji, i ma typ oraz zasięg. Dobra na limity całkowite.
  • Funkcja static inline: sprawdzana pod kątem typów, oblicza każdy argument dokładnie raz, a nowoczesne kompilatory wstawiają ją w miejsce wywołania tak samo jak makro. To właściwy zamiennik niemal każdego makra funkcyjnego.
  • Makro: gdy potrzebujesz __LINE__, gdy coś musi działać, zanim istnieją typy (rozmiary tablic w starszym kodzie), gdy przełączasz kod kompilacją warunkową albo gdy generujesz powtarzalny kod.

Jedno miejsce, w którym makra pozostają bezkonkurencyjne, to rozmiary tablic, bo wynik musi być stałą czasu kompilacji:

To makro ma własne, dobrze znane zastrzeżenie: jest poprawne tylko dla prawdziwej tablicy. Przekaż wskaźnik, a tym staje się tablica przekazana do funkcji, i sizeof zmierzy wskaźnik, dając złą odpowiedź z pełnym przekonaniem i bez słowa ostrzeżenia.

Debugowanie makra

Gdy makro źle się zachowuje, nie wpatruj się w nie. Rozwiń je:

gcc -E program.c | tail -30

Podstawiony tekst od razu pokaże, czy problemem jest brakujący nawias, podwójne obliczanie, czy nazwa kolidująca z czymś innym. Kompiluj też z -Wall -Wextra: GCC i clang dodają notatki "in expansion of macro", które łączą zgłoszoną linię błędu z definicją.

Dalej: pliki nagłówkowe, w których #define i #include łączą się tak, że jeden projekt może obejmować wiele plików źródłowych bez dwukrotnego wklejania tych samych deklaracji.

Najczęściej zadawane pytania

Czym jest makro w C?

Nazwanym fragmentem tekstu, który preprocesor wstawia do kodu źródłowego przed kompilacją. #define MAX 100 sprawia, że każde późniejsze MAX staje się 100; #define SQUARE(x) ((x) * (x)) przyjmuje argumenty i wstawia je do wzorca. Makra nie mają typów i nie podlegają regułom zasięgu: to zastępowanie tekstu.

Dlaczego makra w C potrzebują tylu nawiasów?

Bo ciało jest wklejane w otaczający kod, a potem parsowane jako całość. #define SQUARE(x) x * x zamienia SQUARE(2 + 3) w 2 + 3 * 2 + 3, czyli 11 zamiast 25. Objęcie nawiasami każdego argumentu i całego ciała, ((x) * (x)), sprawia, że wstawiony tekst grupuje się tak, jak zamierzasz, niezależnie od otoczenia.

Czym różni się makro od funkcji w C?

Funkcja jest kompilowana raz, sprawdzana pod kątem typów i oblicza każdy argument dokładnie raz. Makro jest wklejane przy każdym użyciu, niczego nie sprawdza i może obliczyć argument kilka razy, więc MAX(i++, j) może zwiększyć i dwukrotnie. Wybieraj funkcje (a dla stałych const/enum); makr używaj do tego, czego funkcje nie potrafią, na przykład przechwytywania __LINE__ albo generowania kodu.

Dlaczego makro wieloliniowe otacza się do { ... } while (0)?

Żeby zachowywało się jak pojedyncza instrukcja. Samo ciało { ... } psuje się, gdy stoi po nim średnik wewnątrz if/else (średnik kończy if przedwcześnie), a zwykły ciąg instrukcji psuje się w if bez klamer. do { ... } while (0) to jedna instrukcja, która przyjmuje końcowy średnik i wykonuje swoje ciało dokładnie raz.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ