A dinamikus memória kezelése nem ér véget a sikeres
malloc hívásnál. Egy függvény megszerezhet több erőforrást,
majd a következő műveletnél hibát észlelhet. A helyesség ilyenkor azt
jelenti, hogy minden megszerzett erőforrásnak megmarad az egyértelmű
felelőse, és minden kilépési út pontosan a szükséges felszabadításokat
végzi el.
Az allokációs szerződés alapján a megszerzett tárolót megfelelően fel kell szabadítani. A pointerek és többszintű indirekció eszközt adnak ahhoz, hogy egy függvény pointert adjon vissza output paraméteren keresztül. A cleanup és nem lokális vezérlés tárgyalásából pedig fontos felidézni: a C nem futtat automatikus erőforrás-felszabadítást pusztán azért, mert elhagyunk egy blokkot.
A tulajdonos (owner) az a programszereplő, amely egy erőforrás felszabadításáért felel. Ez lehet hívó függvény, modul vagy egy adatstruktúrán keresztül megvalósított erőforrás-kezelő. Az ownership C-ben elsősorban API-szerződés: a pointer típusa önmagában nem írja le.
A kölcsönzött pointer (borrowed pointer) hozzáférést enged, de nem ruházza át a felszabadítás felelősségét. Érvényes használatához egyszerre kell teljesülnie, hogy:
A const int * nem tulajdonlási jelölés. Azt korlátozza,
hogy ezen a hozzáférési úton módosítható-e az int; nem
akadályozza meg, hogy a valódi tulajdonos közben felszabadítsa a
tárolót.
A tulajdonlás átadása (ownership transfer)
a felszabadítási felelősség szerződéses áthelyezése. A pointer másolása
önmagában nem átadás. Például az a = b után két
pointerváltozó tartalmazhatja ugyanazt a pointerértéket, de ettől nem
lesz két jogosult felszabadító.
Hasznos szerződés: „siker esetén átveszi, hiba esetén nem veszi át”. Más szabály is tervezhető, de a hívónak minden kimenetnél tudnia kell, ki takarít.
| Szerep | Mit tehet? | Meddig érvényes? | Felszabadítás |
|---|---|---|---|
| Tulajdonos | A szerződés szerinti műveleteket végezheti | Az erőforrás lezárásáig vagy átadásáig | Az ő felelőssége |
| Kölcsönző | Csak az engedett hozzáférést használhatja | Legfeljebb a kölcsönzési idő végéig, invalidáció nélkül | Nem jogosult rá |
| Átadó | Átadás előtt tulajdonosként jár el | Sikeres átadás után már nem tulajdonos | Siker után nem szabadít |
| Átvevő | Sikeres átvételtől tulajdonos | Saját lezárásáig vagy további átadásig | Siker után az ő felelőssége |
A create/destroy és az init/deinit nem C
nyelvi kulcsszavak, hanem elterjedt API-elnevezések. A konkrét
jelentésüket dokumentálni kell. Ebben a fejezetben:
init már létező kezelőobjektumhoz szerez belső
erőforrásokat;deinit ezeket felszabadítja, de a kezelőobjektum
tárolóját nem;create a kezelőobjektumot is dinamikusan
foglalja;destroy a belső erőforrásokat és a kezelőobjektumot
is felszabadítja.Így egy automatikus tárolási idejű kezelőobjektumhoz
init/deinit, egy create által visszaadott
objektumhoz pedig destroy tartozik. E párok felcserélése
szivárgáshoz vagy nem definiált viselkedéshez vezethet.
A példánk üres állapota:
Az init csak ilyen állapotú objektumon hívható. Siker
után teljesen használható objektumot ad; hiba után visszaállítja az üres
állapotot. Ez egyszerűbb hívói szerződés, mint a „néhány mező talán már
érvényes” állapot átadása.
Az output paraméteres create szerződése:
out egy írható struct batch *
objektumra mutat;NULL, nem tartalmaz
elveszíthető tulajdont;*out az új objektum, amelyért a hívó
felel;*out == NULL.A struct batch ** ezért nem díszítés: a függvény a hívó
pointerobjektumát módosítja.
Egy mérési köteg címkét és nemnegatív egész mérési értékeket tárol. A bemeneteket lemásolja; a hívó bemeneti tárolóira nem tart meg hivatkozást. A három dinamikus erőforrás a kezelőobjektum, a címke másolata és az értéktömb.
Nyelvi profil: ISO C17, hosted környezet, GNU nyelvi
bővítések nélkül. A malloc, free és a
pointerhasználat szabályai nyelvi/könyvtári szabályok; az üres állapot
és az átadás időpontja a saját API-nk szerződése.
A példa előfeltételei: a nem null címkepointer
érvényes, nullterminált sztringre mutat. A méretellenőrzésen átjutó, nem
null bemeneti tömbből count darab inicializált
int olvasható. A bemenetek a hívás alatt változatlanok, és
nem fedik át a módosított kezelőobjektumot. A kód nem biztosít konkurens
hozzáférési szinkronizációt.
A teljes ownership.c fájl:
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum batch_status {
BATCH_OK,
BATCH_ARGUMENT,
BATCH_RANGE,
BATCH_NOMEM,
BATCH_VALUE
};
struct batch {
char *label;
int *values;
size_t count;
};
/* b elo, ervenyes objektum; ures vagy reszlegesen inicializalt is lehet. */
static void batch_deinit(struct batch *b)
{
free(b->values);
free(b->label);
*b = (struct batch){0};
}
/* Elofeltetel: b ervenyes, es ures allapotban van. */
static enum batch_status batch_init(
struct batch *b,
const char *label,
const int *values,
size_t count)
{
enum batch_status status;
size_t label_length;
if (label == NULL || values == NULL ||
count == 0 || label[0] == '\0') {
return BATCH_ARGUMENT;
}
if (count > SIZE_MAX / sizeof *b->values) {
return BATCH_RANGE;
}
label_length = strlen(label);
if (label_length == SIZE_MAX) {
return BATCH_RANGE;
}
b->label = malloc(label_length + 1);
if (b->label == NULL) {
status = BATCH_NOMEM;
goto cleanup;
}
memcpy(b->label, label, label_length + 1);
b->values = malloc(count * sizeof *b->values);
if (b->values == NULL) {
status = BATCH_NOMEM;
goto cleanup;
}
for (size_t i = 0; i < count; ++i) {
if (values[i] < 0) {
status = BATCH_VALUE;
goto cleanup;
}
b->values[i] = values[i];
}
b->count = count;
return BATCH_OK;
cleanup:
batch_deinit(b);
return status;
}
/* Csak NULL vagy batch_create altal atadott, meg elo objektum. */
static void batch_destroy(struct batch *b)
{
if (b == NULL) {
return;
}
batch_deinit(b);
free(b);
}
/* Elofeltetel: out ervenyes, irhato, es *out == NULL. */
static enum batch_status batch_create(
const char *label,
const int *values,
size_t count,
struct batch **out)
{
struct batch *b;
enum batch_status status;
*out = NULL;
b = malloc(sizeof *b);
if (b == NULL) {
return BATCH_NOMEM;
}
*b = (struct batch){0};
status = batch_init(b, label, values, count);
if (status != BATCH_OK) {
goto cleanup;
}
*out = b; /* Tulajdonlas atadasa a hivonak. */
b = NULL; /* A helyi valtozo mar nem tulajdonos. */
cleanup:
batch_destroy(b);
return status;
}
int main(void)
{
const int input[] = {3, 7, 2};
struct batch *b = NULL;
enum batch_status status =
batch_create("latency", input, 3, &b);
if (status != BATCH_OK) {
fprintf(stderr, "batch_create: status=%d\n", (int)status);
return EXIT_FAILURE;
}
{
const int *view = b->values;
printf("%s: %d\n", b->label, view[0]);
/* A kolcsonzott hozzaferes itt veget er. */
}
batch_destroy(b);
b = NULL;
return EXIT_SUCCESS;
}A struct batch mezőihez a -> operátorral
férünk hozzá pointeren keresztül. A példa nem kíván konkrét
struktúraelrendezést vagy rögzített int-szélességet.
Választott laborprofil: Linux x86-64, System V ABI,
LP64; GCC 14.2.0, illetve Clang 19.1.7, glibc 2.39 mellett. Az alábbi
parancsok feltételezik, hogy a gcc és clang
neveken ezek a kiadások érhetők el. A példa itt nem futtatott vagy mért
eredmény.
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wshadow \
-O0 -g ownership.c -o ownership-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wshadow \
-O0 -g ownership.c -o ownership-clangA példában minden allokációméret C bájtban értendő. A szorzás biztonsági feltétele, ha az elemszám és :
Mivel , az osztásos ellenőrzést még a potenciálisan túlcsorduló szorzás előtt elvégezhetjük.
A részleges inicializálást nem az jelenti, hogy inicializálatlan
pointermezőket próbálunk felszabadítani. A kezelőobjektum minden mezője
már a foglalások előtt meghatározott értékű. A
(struct batch){0} típusos inicializálás null
pointerértékeket hoz létre; nem támaszkodik a null pointer csupa nulla
bites reprezentációjára.
| Kilépési ok | Addig megszerzett erőforrás | Takarítás | Hívó által látott állapot |
|---|---|---|---|
| A kezelőobjektum foglalása sikertelen | Nincs | Nincs | *out == NULL |
| Hibás argumentum vagy méret | Kezelőobjektum | Külső destroy |
*out == NULL |
| A címke foglalása sikertelen | Kezelőobjektum | Belső deinit, külső destroy |
*out == NULL |
| Az értéktömb foglalása sikertelen | Kezelőobjektum, címke | Címke, majd kezelőobjektum | *out == NULL |
| Negatív érték másolás közben | Mindhárom | Értéktömb, címke, kezelőobjektum | *out == NULL |
| Siker | Mindhárom | A függvény nem szabadítja fel az átadott objektumot | A hívó tulajdonos |
A negatív érték felismerésekor az értéktömb csak részben lehet
feltöltve. A free számára ez nem akadály: nem olvassa ki az
egyes int értékeket. Összetettebb elemeknél viszont,
amelyek saját erőforrásokat birtokolnak, külön számlálni kellene a már
sikeresen inicializált elemeket, és csak azokat lebontani.
A példában a count csak a teljes siker után kap végleges
értéket. A cleanup nem ebből találja ki, mely foglalások sikerültek: ezt
a null vagy érvényes tulajdonló pointermezők jelzik.
A goto cleanup itt előrefelé ugrik egyetlen takarítási
pontra. Nem rejti el a program állapotát: éppen a sok külön
return mellé másolt felszabadításokat helyettesíti.
Bizonyításvázlat. Minden sikeres foglalás eredménye
azonnal egy tulajdonló mezőbe kerül. A még meg nem szerzett erőforrást
NULL jelöli. Minden későbbi hiba eléri a
batch_deinit hívást, amely minden ilyen mezőt felszabadít,
majd üres állapotot állít elő. A külső batch_destroy
ismételt deinit hívása ezért már csak null pointereket ad
át a free függvénynek. Végül felszabadul a kezelőobjektum
is.
Siker esetén a *out = b átadja a tulajdont, a
b = NULL pedig megszünteti a helyi cleanup felszabadítási
kötelezettségét. Így egyetlen átadott erőforrás sem szabadul fel idő
előtt.
A megszerzési sorrend:
A felszabadítás ennek fordítottja. A két belső foglalás itt egymástól független, de a kezelőobjektumot utoljára kell elengedni: addig abból olvassuk ki a felszabadítandó pointereket.
Általánosan, ha egy később létrehozott erőforrás egy korábbira támaszkodik, előbb a függő erőforrást kell megszüntetni. A fordított megszerzési sorrend ezért gyakori, de a valódi szabály a függőségek tiszteletben tartása.
| Objektum vagy hozzáférés | Tulajdonos | Élettartam / invalidáció |
|---|---|---|
input bemeneti tömb |
A main automatikus objektuma |
A blokk végéig él; a létrehozás csak kölcsönzi |
| Dinamikus kezelőobjektum | Kezdetben batch_create, siker után a hívó |
batch_destroy megszünteti |
| Címke és értéktömb tárolója | A kezelőobjektumon keresztül kezelt tulajdon | batch_deinit vagy batch_destroy
felszabadítja |
view kölcsönzött pointer |
Nem tulajdonos | A belső tároló felszabadítása érvényteleníti |
A kölcsönzött pointer használata felszabadítás után nem hibakódot eredményez, hanem érvénytelen hozzáférés esetén nem definiált viselkedést.
Egy művelet idempotens, ha ismételt alkalmazása
ugyanazt az állapotot eredményezi, mint egyszeri alkalmazása. A
batch_deinit ilyen, amíg maga a kezelőobjektum él: az első
hívás üres állapotot hoz létre, a következő ezt változatlanul
hagyja.
A batch_destroy viszont nem idempotens
ugyanazzal a korábbi pointerértékkel. Az első hívás megszünteti
a kezelőobjektum élettartamát. A másodikhoz már nincs élő objektum. A
NULL elfogadása ettől külön tulajdonság.
A hívó pointerének nullázása hasznos, de csak azt az egy pointerobjektumot módosítja. Más aliasokat és kölcsönzött nézeteket nem javít meg.
Gyakori hibák és javításuk:
deinit szükséges, vagy külön, dokumentált
csereművelet.destroy automatikus objektumra. A
belső erőforrások után nem dinamikusan foglalt tárolót próbálna
felszabadítani. Ilyenkor deinit kell.return szivárgást okozna; a
cleanup ágra kell jutni.A közös cleanup csak a hozzá vezető vezérlési utakra érvényes. Nem véd önmagában nem lokális kilépés vagy a folyamat rendellenes megszűnése ellen.
A batch_create sikeres visszatérése után megszűnhet-e a
bemeneti values tömb élettartama? Ki szabadítja fel a
b->values tárolót?
A bemenet {4, -1, 8}. Vezesd végig a létrehozást,
feltételezve, hogy minden foglalás sikeres. Mely tömbelemek kapnak
értéket, és mi lesz az output paraméter állapota?
Írj batch_destroy_slot(struct batch **slot) függvényt,
amely felszabadítja a *slot objektumot, majd nullázza a
hívó pointerét. Ezután magyarázd meg, miért nem javítja meg a következő,
szándékosan hibás C17-kódrészletet:
/* Hibas reszlet: b egy sikeresen letrehozott objektum tulajdonosa. */
struct batch *alias = b;
batch_destroy_slot(&b);
batch_destroy(alias); /* Ervenytelen felszabaditas. */Igen. A függvény saját tárolóba másolja az értékeket, ezért a siker
után nem függ a bemeneti tömb élettartamától. A
b->values felszabadítását a hívó a
batch_destroy(b) művelettel kezdeményezi.
Ellenőrzési feltétel: az implementáció nem tárolja
el a bemeneti values pointert; a mező saját foglalás
eredményét tartalmazza.
Először létrejön a kezelőobjektum, a címke és az értéktömb. A
nulladik elem megkapja a 4 értéket. Az első indexnél a
negatív bemenet miatt a függvény még a tárolás előtt a cleanup ágra
ugrik.
A deinit felszabadítja az értéktömböt és a címkét, majd
üres állapotot állít elő. A külső cleanup felszabadítja a
kezelőobjektumot. Az eredmény BATCH_VALUE, az output pedig
NULL.
Ellenőrzési feltétel: egyik sikeres foglalás sem marad felszabadítatlanul, és a részlegesen feltöltött tömb inicializálatlan elemeit semmi nem olvassa.
Az alábbi C17-részlet az előző teljes példába illeszthető, a
batch_destroy definíciója után:
/* Elofeltetel: slot ervenyes, irhato pointerobjektumra mutat. */
static void batch_destroy_slot(struct batch **slot)
{
batch_destroy(*slot);
*slot = NULL;
}Ugyanazzal az élő, önálló kimeneti hellyel ismét meghívható: a
második hívás már NULL értéket ad tovább. A
slot tárolója nem lehet a felszabadítandó objektum
része.
Az alias azonban külön pointerobjektum. A b
nullázása nem módosítja, ezért a második felszabadítás érvénytelen. A
javítás a második destroy elhagyása és az alias minden
további használatának megszüntetése.
Ellenőrzési feltétel: minden megszerzett erőforráshoz egyetlen tényleges felszabadítás tartozik; a felszabadítás után egyetlen alias sem kerül felhasználásra.
A megbízható C erőforráskezelés alapja az explicit szerződés: ki tulajdonos, ki kölcsönző, és melyik művelet adja át a felelősséget. A részleges inicializálást meghatározott mezőértékekkel és egyértelmű hiba utáni állapottal kell kezelni.
A goto cleanup áttekinthető eszköz a hibautak
egyesítésére. A helyességet azonban nem maga az ugrás biztosítja, hanem
az, hogy minden sikeres szerzés követhető, minden függőség megfelelő
sorrendben szűnik meg, és egyetlen erőforrás sem szabadul fel
kétszer.