Mikro tekst o makrach
Nie lubimy makr, ale jak napisałem w starszym tekście, czasami są one niezbędne, a przynajmniej przydatne. Szczególnie jeśli piszemy w języku C, gdzie nie mamy lepszych narzędzi do tworzenia szablonów generujących kod podczas kompilacji, który jest zależny od aktualnych potrzeb. Ponieważ natknąłem się w różnych miejscach na to, że potrzebne są wyjaśnienia, toteż postaram się zamieścić krótki tekst opisujący zagadnienie oraz podam przykład wraz z objaśnieniem jak i do czego jest to wykorzystywane w realnym kodzie.Ogólnie makra tworzymy używając schematu:
#define co_definiujemy jak_definiujemywidzimy 3 bloki:
- słowo kluczowe #define
- co_definiujemy, czyli schemat bloku, który po znalezieniu zostanie zastapiony
- jak_definiujemy, czyli schemat bloku, który podczas prekompilacji zastąpi znaleziony w kodzie wzorzec co_definiujemy.
Blok określający wzorzec definicji.
Blok ten może być zdefiniowany, jako etykieta, lub jako makroinstrukcja.Przypadek etykiety jest dość prosty, np.:
#define BUFOR_SIZE 256wszędzie, gdzie użyjemy BUFOR_SIZE kompilator podczas prekompilacji wstawi nam 256.
Najczęściej podawany przykład makroinstrukcji:
#define MY_MAX(x, y) (((x)>(y))?(x):(y))Tak więc gdy dostaniemy kod:
z = MY_MAX(4, 5);to zmienna z otrzyma wartość 5, gdyż zamiast MY_MAX, zostanie wstawiona instrukcja porównująca, gdzie za x podstawiona zastanie 4, a za y 5. Na razie (chyba) wszystko wygląda prosto i przejrzyście.
Blok definicji
Zajmijmy się zatem ostatnim blokiem - blokiem definicji.Tu dochodzą znaki specjalne: '#', '##' oraz '\' na końcu linii.
- znaczek '\' na końcu linii powoduje, że do definiowanego bloku dodana zostanie zawartość linii poniżej.
- '#' zamieni nam zmienną stojącą za znaczkiem na tekst
- '##' sklei nam nazwy zmiennych
Wszystko to w przykładowym kodzie:
#include <stdio.h> #define specprint(aname, atype, avalue, astr) \ ({ \ atype aname##_v1 = avalue;\ puts(#aname);\ puts(#atype);\ puts("xxx"astr"yyy");\ printf("%d\n", aname##_v1);\ aname##_v1++;\ printf("%d\n", aname##_v1);\ }) int main() { specprint(nazwa, int, 1, "abc"); return 0; }po uruchomieniu programu na ekranie zobaczymy:
nazwa int xxxabcyyy 1 2No tak, zabawa zabawą, ale do czego to może nam się przydać w realnym kodowaniu?
Przykładem wykorzystania makr mogą być inklude'y systemu MorphOS w tzw wersji inline'owej.
Powiedzmy, że napiszemy bardzo prosty program, wyświetlający komunikat "Witaj świecie":
#include <proto/intuition.h> int main() { struct EasyStruct es; struct IntuitionBase *IntuitionBase = (struct IntuitionBase *)OpenLibrary("intuition.library", 0); if (IntuitionBase) { es.es_StructSize = sizeof(es); es.es_Flags = 0; es.es_Title = (STRPTR) "Komunikat"; es.es_TextFormat = (STRPTR) "Witaj świecie"; es.es_GadgetFormat = (STRPTR) "OK"; EasyRequestArgs(NULL, &es, NULL, NULL); CloseLibrary(IntuitionBase); } }Zobaczmy jak wygląda rzeczywiste wywołanie funkcji EasyRequestArgs:
#define EasyRequestArgs(__p0, __p1, __p2, __p3) \ LP4(588, LONG , EasyRequestArgs, \ struct Window *, __p0, a0, \ CONST struct EasyStruct *, __p1, a1, \ ULONG *, __p2, a2, \ CONST APTR , __p3, a3, \ , INTUITION_BASE_NAME, 0, 0, 0, 0, 0, 0)Jak łatwo zauważyć EasyRequestArgs wywołuje tak naprawdę makro LP4. Poszukajmy definicji tego makra:
#define LP4(offs, rt, name, t1, v1, r1, t2, v2, r2, t3, v3, r3, t4, v4, r4, bt, bn, cm1, cs1, cl1, cm2, cs2, cl2 ) \ ({ \ t1 _##name##_v1 = v1; \ t2 _##name##_v2 = v2; \ t3 _##name##_v3 = v3; \ t4 _##name##_v4 = v4; \ REG_##r1 = (ULONG) _##name##_v1; \ REG_##r2 = (ULONG) _##name##_v2; \ REG_##r3 = (ULONG) _##name##_v3; \ REG_##r4 = (ULONG) _##name##_v4; \ REG_A6 = (ULONG) (bn); \ (rt) (*MyEmulHandle->EmulCallDirectOS)(-offs); \ })czyli możemy wg tego schematu nasz program zapisać:
#include <proto/intuition.h> #ifndef EMUL_EMULINTERFACE_H #include <emul/emulinterface.h> #endif #ifndef EMUL_EMULREGS_H #include <emul/emulregs.h> #endif int main() { struct EasyStruct es; struct IntuitionBase *IntuitionBase = (struct IntuitionBase *)OpenLibrary("intuition.library", 0); if (IntuitionBase) { es.es_StructSize = sizeof(es); es.es_Flags = 0; es.es_Title = (STRPTR) "Komunikat"; es.es_TextFormat = (STRPTR) "Witaj świecie"; es.es_GadgetFormat = (STRPTR) "OK"; REG_A0 = NULL; REG_A1 = (ULONG) &es; REG_A2 = NULL; REG_A3 = NULL; REG_A6 = (ULONG) IntuitionBase; (*MyEmulHandle->EmulCallDirectOS)(-588); CloseLibrary(IntuitionBase); } }I też zadziała. I to dokładnie tak samo. Różnica jednak jest taka, że pierwszy kod skompilujemy pod AmigaOS-em, dodatkowo, chyba jest czytelniejsze. A to, że zostanie przekształcone na ten drugi sposób, to nie nasza rzecz - to zrobi za nas kompilator.
Na co uważać przy używaniu makr?
Należy pamiętać, że to co podajemy w argumencie zostanie bezpośrednio wstawione w kod, toteż:#include <stdio.h> #define MY_MAX(x, y) (((x)>(y))?(x):(y)) int main() { int m = 1, n = 2, r; r = MY_MAX(m++, n++); printf("m = %d, n = %d, r = %d\n", m, n, r); return 0; }Wynikiem działania programu będzie:
m = 2, n = 4, r = 3chociaż spodziealibyśmy się:
m = 2, n = 3, r = 2Mam nadzieję, że ten króciutki tekst przybliżył wam odrobinę zagadnienie makr
#define Autor Kaczuś
makra #define C język C morphos