8.4 Tulajdonlási szerződés és cleanup ágak

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.

8.4.1 Tulajdonos, kölcsönzött hozzáférés és átadás

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

8.4.2 A létrehozás és megszüntetés szerződéspárjai

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:

Í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:

E=(𝚕𝚊𝚋𝚎𝚕=NULL,𝚟𝚊𝚕𝚞𝚎𝚜=NULL,𝚌𝚘𝚞𝚗𝚝=0). E = (\texttt{label}=\mathrm{NULL}, \texttt{values}=\mathrm{NULL}, \texttt{count}=0).

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:

A struct batch ** ezért nem díszítés: a függvény a hívó pointerobjektumát módosítja.

8.4.3 Kidolgozott példa: mérési adatok létrehozása

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-clang

A példában minden allokációméret C bájtban értendő. A szorzás biztonsági feltétele, ha nn az elemszám és s=sizeof(𝚒𝚗𝚝)s=\operatorname{sizeof}(\texttt{int}):

ns≤SIZE_MAX⇔n≤⌊SIZE_MAXs⌋. ns \leq \mathrm{SIZE\_MAX} \iff n \leq \left\lfloor\frac{\mathrm{SIZE\_MAX}}{s}\right\rfloor.

Mivel s>0s>0, az osztásos ellenőrzést még a potenciálisan túlcsorduló szorzás előtt elvégezhetjük.

8.4.4 Minden hibakimenet végigvezetése

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.

8.4.4.1 Miért működik a közös cleanup ág?

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.

8.4.4.2 Fordított sorrend és élettartamhatárok

A megszerzési sorrend:

kezelőobjektum→címke→értéktömb. \text{kezelőobjektum}\rightarrow\text{címke}\rightarrow\text{értéktömb}.

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.

8.4.5 Idempotens takarítás és hibákat elfedő konvenciók

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:

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.

8.4.6 Feladatok

8.4.6.1 Szerződés értelmezése

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?

8.4.6.2 Részleges inicializálás követése

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?

8.4.6.3 Nullázó felszabadító készítése és diagnózis

Í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. */

8.4.7 Megoldások

8.4.7.1 Szerződés értelmezése

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.

8.4.7.2 Részleges inicializálás követése

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.

8.4.7.3 Nullázó felszabadító

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.

8.4.8 Összefoglaló

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.