Automatická správa pamäte v C
Hoci je C nízkoúrovňový jazyk bez moderných vymožeností, je možné (a žiaduce) spravovať v ňom pamäť automaticky. V tomto článku si predstavíme osvedčený spôsob správy pamäte v C založený na počítanie referencií.
C nie je objektovo orientované, nemá teda dedičnosť ani polymorfizmus. Kým dedičnosť nie je nevyhnutne potrebná (pozri napr. Go), polymorfizmu sa v niektorých prípadoch nemožno vyhnúť. Naša malá knižnica pre prácu s objektmi v C (COX = C Object Extensions) si teda musí pre každý typ objektu ( "triedu") držať v pamäti adresu na funkcie zdieľané viac objekty, ak sa pre rôzne typy objektov správajú inak. V našom jednoduchom prípade budeme mať len funkciu (resp. Metódu) pre mobilizáciu objektu a prevod na textovú reprezentáciu (pre ladenie):
struct cox_type { void(*finalizer)(void*); cox_string_t(*descriptor)(void*); };
Každý objekt bude mať hlavičku obsahujúce odkaz na typ a počítadlo referencií:
struct cox_base { struct cox_type* type; unsigned int refcount; };
Ak chceme napríklad automaticky spravovaný objekt pre reťazce, deklarujeme ho takto:
struct cox_string { struct cox_base base; char* cstr; unsigned long len; }; typedef struct cox_string* cox_string_t;
Každá inštancia teda okrem vlastného reťazca (cstr) a jeho dĺžky (ľan) obsahuje aj informácie o type a čítač referencií. Typ je definovaný nasledovne:
static struct cox_type cox_string_type = {.finalizer = &cox_string_destroy, .descriptor = &cox_string_describe };
Uvoľnenie reťazca je jednoduché, najskôr vrátime pamäť alokovanú pre cstr a potom objekt samotný. Popis objektu je ešte jednoduchšie, reťazec totiž v tomto prípade vracia seba samého:
static void cox_string_destroy(void* obj) { cox_string_t str = obj; free(str->cstr); free(str); #ifdef DEBUG printf("cox_string destroyed\n"); #endif } static cox_string_t cox_string_describe(void* obj) { return obj; }
Nie príliš zložité je tiež vytvorenie objektu reťazca:
cox_string_t cox_string_create(const char* s) { cox_string_t str = malloc(sizeof(struct cox_string)); str->base.type = &cox_string_type; str->base.refcount = 1; str->len = strlen(s); str->cstr = malloc(str->len + 1); strcpy(str->cstr, s); return str; }
Stačí alokovať pamäť, priradiť informáciu o type, čítač referencií nastaviť na 1 a nakoniec alokovať pamäť pre cstr a text skopírovať.
Ak už objekt nepoužívame, uvoľníme ho. Pre ľubovoľný objekt implementovaný pomocou našej knižnice budeme pre uvoľnenie používať funkciu cox_release:
void cox_release(void* obj) { struct cox_base* base = obj; cox_refcount_lock(); base->refcount--; int should_destroy = base->refcount == 0; cox_refcount_unlock(); if (should_destroy) { base->type->finalizer(obj); } }
Táto funkcia zníži čítač referencií a pokiaľ tento klesne na nulu, tj. Na objekt neexistujú žiadne ďalšie referencie, zavolá sa deštruktor z informácie o type, čím sa uvoľnia všetky prostriedky používané objektom (vrátane pamäte pre objekt samotný).
Všimnite si funkcií cox_refcount_lock()
a
cox_refcount_unlock()
. Tie sú dôležité pri aktualizácii
čítača referencií vo viacvláknové prostredí. Ich implementácia závisí
na konkrétnom operačnom systéme, najjednoduchšie je použiť mutex alebo
semafor (napr. Vo Windows funkcii CreateSemaphore atď.).
Ak teda vytvoríme reťazec a po použití na neho zavoláme
cox_release()
, objekt sa automaticky uvoľní:
cox_string_t str = cox_string_create("Hello, world!");
...
cox_release(str);
Ak takýto objekt pridáme napr. Do nejakej kolekcie, táto kolekcia zvýši jeho čítač referencií a cox_release objekt neodstráni z pamäte, pretože ešte existuje ďalšie referencie.
Aby sme nemuseli explicitne volať cox_release()
vo všetkých
vetvách kódu, deklarujeme si atribút __auto
:
#define __auto __attribute__((cleanup(cox_release_indirect))) void cox_release_indirect(void* p) { cox_release(*(void**)p); }
Potom môžeme deklarovať premenné takto:
__auto cox_string_t str = cox_string_create("Hello, world!");
vďaka čomu nie je nutné použitie cox_release()
, pretože
taká premenná sa automaticky uvoľní (resp. zníži sa jej počítadlo
referencií) po opustení jej rozsahu platnosti.
Atribút __auto
je užitočný pre lokálne premenné, problém
ale nastáva, ak chceme vrátiť objekt z funkcie, bez toho aby sme sa museli
starať o jeho uvoľnení, lebo návratom z funkcie opúšťame rozsah platnosti
premenných. Pre tento účel sa hodia tzv. Autorelease pool:
struct cox_autoreleasepool { void** objs; unsigned int count; unsigned int capacity; struct cox_autoreleasepool* next; struct cox_autoreleasepool* prev; }; typedef struct cox_autoreleasepool* cox_autoreleasepool_t;
Každé vlákno má svoj zásobník poolov, do ktorých ukladá objekty určené k neskoršiemu (automatickému) uvoľnenie. Typicky teda budeme mať:
cox_string_t str = cox_string_create("Hello, world!");
cox_autorelease(str);
...
cox_autoreleasepool_destroy(pool);
pričom pool za nás zvyčajne spravuje slučka udalostí alebo nejaký podobný mechanizmus, takže sa o explicitné spravovanie poolov starať nemusíme. Príslušná mašinéria je definovaná takto:
__thread static cox_autoreleasepool_t autopool = NULL; cox_autoreleasepool_t cox_autoreleasepool_create() { cox_autoreleasepool_t pool = malloc(sizeof(struct cox_autoreleasepool)); pool->count = 0; pool->capacity = 100; pool->objs = malloc(sizeof(void*) * pool->capacity); pool->next = NULL; if (autopool != NULL) autopool->next = pool; pool->prev = autopool; autopool = pool; return pool; } void cox_autoreleasepool_destroy(cox_autoreleasepool_t pool) { if (pool->next != NULL) cox_autoreleasepool_destroy(pool->next); for (int i = 0; i < pool->count; i++) cox_release(pool->objs[i]); free(pool->objs); autopool = pool->prev; free(pool); #ifdef DEBUG printf("cox_autoreleasepool destroyed\n"); #endif } void* cox_autorelease(void* obj) { if (autopool == NULL) fprintf(stderr, "no autorelease pool in place, leaking memory\n"); else { if (autopool->count == autopool->capacity) { autopool->capacity *= 2; autopool->objs = realloc(autopool->objs, sizeof(void*) * autopool->capacity); } autopool->objs[autopool->count++] = obj; } return obj; }
Ak chceme teda z nejakej funkcie bezpečne vrátiť nejaký objekt, stačí použiť:
cox_my_object_t my_function() {
cox_my_object_t obj = ...;
...
return cox_autorelease(obj);
}
Uvedená metóda má pochopiteľne svoju réžiu, ako všetky spôsoby automatickej správy pamäti, však výhody plne prevažujú nad nevýhodami, zvlášť pre kód vyššej úrovne, ktorého autori s vyššie uvedenými funkciami pre prácu s čítačom referenciou neprídu do styku, pretože ich volanie bude schované v príslušných knižniciach .
Tu predstavený spôsob správy pamäte používa napríklad knižnica Grand Central Dispatch alebo - v trochu abstraktnejšie forme - Windows Runtime (WinRT).