InfoC adventi naptár
Reflektivitás
Van egy programunk, amely különféle típusú objektumok adatait tárolja. Ezeknek az objektumoknak az adatait ki kell írni fájlokba, és vissza kell olvasni őket onnan. Néha meg kell jeleníteni egy objektum adatait egy párbeszédablakban, hogy a felhasználó átírhassa azokat stb.
Legyen erre egy egyszerű példa egy téglalap:
struct Teglalap { int px, py; /* pozicio a kepernyon */ int szeles, magas; /* meret */ };
És a fájlba írt változat, ahogyan ott egy téglalap kinéz:
PozicioX=5 PozicioY=10 MeretX=15 MeretY=20
A következő problémákkal szembesülünk. Ha a programot továbbfejlesztjük, a téglalapnak esetleg új tulajdonsága jelenik meg, pl. az új verzió a színét is eltárolja. A kódot egy csomó helyen módosítanunk kell:
- A
teglalap.h
fájlban, ahol a struktúra definiálva van. - A
teglalap_save()
függvényben, amelyik egy téglalap adatait kiírja egy fájlba. - A
teglalap_load()
függvényben, amelyik beolvassa azokat. - A
teglalap_edit()
függvényben két helyen is; egyrészt ahol létrehozzunk a téglalap tulajdonságai párbeszédablakban a beviteli mezőket, másrészt pedig ahol visszaolvassuk azokat, és egy adott struktúrába elmentjük. - … és így tovább.
A másik probléma az, hogy esetleg többféle objektumot szeretnénk hasonló
módon kezelni. A kör objektumokat is hasonló módon mentjük fájlba, jelenítünk
meg hozzájuk ablakot. A kor_save()
függvény szinte ugyanúgy néz
ki, mint a teglalap_save()
függvény, csak mások a tulajdonságok
nevei. A hasonlóság pedig azt sejtteti, hogy valahogyan általánosítani lehetne.
Vagy éppen kellene.
Tartalom
1 Reflektivitás
Ami itt hiányzik nekünk a C-ből, az a
reflektivitás. Arra lenne szükségünk itt, hogy valahogyan fel tudjuk sorolni
egy struktúra adattagjait – például úgy, hogy sztringként tudjuk megadni azt,
hogy melyikre szeretnénk hivatkozni. Meg hogy egy for()
ciklussal
végig tudjunk menni rajtuk. Ha a struktúrában egy tömb lenne, akkor
ez működne… De a különálló adattagokra ilyet nem lehet.
Azt viszont meg lehet csinálni, hogy felsoroljuk az adattagok neveit és melléjük pointereket egy adott példány adattagjaira. Ez könnyen általánosítható bármilyen típusú tagra, úgyhogy maradjunk az egészeknél:
#include <stdio.h> typedef struct Teglalap { int px, py; /* pozicio a kepernyon */ int szeles, magas; /* meret */ } Teglalap; void teglalap_save(Teglalap *t) { int i; struct { char const *nev; int *adattag; } t_adatai[] = { { "PozicioX", &t->px }, { "PozicioY", &t->py }, { "MeretX", &t->szeles }, { "MeretY", &t->magas }, { NULL } }; for (i=0; t_adatai[i].nev!=NULL; ++i) printf("%s=%d\n", t_adatai[i].nev, *t_adatai[i].adattag); } int main() { Teglalap t={5, 10, 15, 20}; teglalap_save(&t); }
Itt a for()
ciklus elég kényelmes már, és jól használható az adattagok
táblázatos formája is. A probléma csak az, hogy az int*
pointerek egy adott téglalap
példányhoz kötődnek. Emiatt kellett a függvényen belül definiálni a tömböt; hogy kezdeti
értékként a pointerek megkapják az egyes intekre mutató értékeket, a konkrét
*t
téglalap adattagjaira mutatva. Tehát a struktúra inicializálását ugyanígy szerepeltetni
kell a többi helyen is, vagyis ha módosul a téglalap struktúra, akkor még mindig sok helyen kell
javítgatni a programot.
Ha több téglalap példányunk van, akkor bár az int*
pointerek eltérőek, viszont
minden téglalap memóriaképe megegyezik. Egy téglalap memóriaterületének elejétől számítva egy
bizonyos adattag mindig ugyanannyi bájtnyira van. Ha az adattagokat leíró struktúrában ezt
tárolnánk a cím helyett, akkor azt bármelyik téglalapra használhatnánk. Az offszetet úgy
kapjuk, hogy kivonjuk az adott adattag memóriacíméből a téglalap elejének memóriacímét; persze
mindkét pointert char*
-gá konvertáljuk, hogy az eredményt bájtokban kapjuk:
printf("%d\n", (int)((char*)&t.px-(char*)&t)); printf("%d\n", (int)((char*)&t.py-(char*)&t));
Ezt visszafelé is meg lehet csinálni. Ha egy char*
típusú mutatóhoz –
amelyik egy téglalap memóriaképének az elejére mutat – hozzáadjuk az így létrehozott
offszeteket, akkor a szóban forgó adattagra kapunk egy char*
típusú
pointert, amit aztán a megfelelő típusúra cast-olhatunk:
int pxo=(int)((char*)&t.px-(char*)&t); // ofszet int pyo=(int)((char*)&t.py-(char*)&t); printf("%d", *(int*)((char*)&t + pxo)); // ofszetbol pointer printf("%d", *(int*)((char*)&t + pyo));
Ezek az offszetek az adott téglalap példányoktól függetlenek; így már akkor is meghatározhatóak, amikor még nem tudjuk, melyik téglalappal kell majd dolgoznia egy függvénynek. Ez egyben azt is jelenti, hogy a téglalapokat leíró tömböt (amely a neveket és az offszeteket tartalmazza) csak egyszer kell definiálnunk, nem pedig minden függvényben.
2 A végeredmény
Még egy apró trükk. Az offszetek meghatározásához
még arra sincsen szükség, hogy akár egyetlen egy téglalap is létezzen. A
NULL pointert ugyanis castolhatjuk Teglalap*
típusúvá. Ennek ugyan
egy adott elemére nem hivatkozhatunk ((Teglalap*)0)->px
, de egy adott
elem memóriacímére igen: &(((Teglalap*)0)->px)
. Ebből kivonva a NULL
pointert kapjuk az offszetet. Így született a lenti OFFSETOF
makró.
#include <stdio.h> #define OFFSETOF(STRUKT,MEMBER) ((char*)(&((STRUKT *)0)->MEMBER)-(char*)0) #define MEMBER(MEMBERTYPE,STRUKTP,OFFSET) (*(MEMBERTYPE *)((char*)STRUKTP+OFFSET)) typedef struct Teglalap { int px, py; int szeles, magas; } Teglalap; struct { char const *nev; int offset; } teglalap_leiro[] = { { "PozicioX", OFFSETOF(Teglalap, px) }, { "PozicioY", OFFSETOF(Teglalap, py) }, { "MeretX", OFFSETOF(Teglalap, szeles) }, { "MeretY", OFFSETOF(Teglalap, magas) }, { NULL } }; int main() { Teglalap t={5, 10, 15, 20}; int i; for (i=0; teglalap_leiro[i].nev!=NULL; ++i) printf("%s=%d\n", teglalap_leiro[i].nev, MEMBER(int, &t, teglalap_leiro[i].offset)); return 0; }
Némely C fordítók megengedik azt, hogy void*
mutatókat vonjunk ki egymásból.
Ilyenkor az eredményt ugyanúgy bájt egységekben kapjuk, mint char*
esetén. Ez
azonban nem szabványos, ezért kellenek a char*
-ok.