9.1 Struktúrák, tagok és értékmásolás

Egy C API gyakran összetartozó adatokat ad át: például egy üzenet azonosítóját, címkéjét, hosszát és tartalmát. A struct ezeket egyetlen aggregált értékbe foglalja. Ettől azonban még nem válik automatikusan erőforrás-kezelővé: egy pointertag másolása nem másolja a mögötte lévő tárolót, és nem ruház át tulajdonjogot.

A Tulajdonlási szerződés és cleanup ágak részben megismert owner–borrowed megkülönböztetés ezért a struktúrák tagjaira is érvényes. A pointereket tartalmazó deklarációk olvasásához a Deklarátorok: tömb-, pointer- és függvénykapcsolatok rész ad előfeltételt.

Az alábbi példák ISO C17-et használnak. A méretek C bájtban értendők: a sizeof egysége nem általánosan nyolcbites oktett.

9.1.1 Típusnév, adattag és aggregált érték

A struktúratípus névvel ellátott adattagokból (member) álló típus. Egy struktúraobjektum tartalmazza saját tagobjektumait; egy beágyazott struktúra vagy fix méretű tömb ténylegesen része a befoglaló objektumnak. Pointertag esetén viszont csak a pointerobjektum része: a referált tároló külön létezik.

Az alábbi blob.h egy másolt tartalmú, változó hosszúságú adatobjektum publikus interfésze:

#ifndef BLOB_H
#define BLOB_H

#include <stddef.h>

struct blob_meta {
    unsigned sequence;
    char label[8];
};

struct blob_view {
    struct blob_meta meta;
    const unsigned char *data;
    size_t size;
};

struct blob;
typedef struct blob Blob;

Blob *blob_create(struct blob_meta meta,
                  const void *src, size_t size);

struct blob_view blob_get_view(const Blob *blob);

void blob_destroy(Blob *blob);

#endif

A blob_meta a struktúra címkéje, angolul struct tag. ISO C17-ben a típus neve itt struct blob_meta, nem önmagában blob_meta. A címkék külön névtérben vannak a közönséges azonosítóktól.

A typedef struct blob Blob; ezzel szemben típusnevet vezet be a közönséges azonosítók névterébe. A Blob és a struct blob ugyanazt a típust jelöli: a typedef nem hoz létre új, elkülönült típust, és nem ad ownership-szemantikát.

A tagnevek a saját struktúrájukon belül értelmezendők. A blob_view meta tagja beágyazott struktúra, a label pedig tömbtag. A kétféle tagelérés:

A p->member jelentése (*p).member. Kiértékelt hozzáféréskor p megfelelő típusú, élő objektumra kell mutasson; a -> nem ellenőrzi sem a null pointert, sem az élettartamot.

9.1.2 Inicializálás és értékmásolás működés közben

Az aggregált inicializáló a tagokat inicializálja. A designated initializer tagnevekkel jelöli a célokat, így nem kell fejben követni a deklarációs sorrendet. A nem megadott tagok ilyen inicializáláskor a statikus inicializálásnak megfelelő értéket kapják: például az egész tag nulla, a pointertag null pointer lesz. Ez szemantikai szabály, nem „minden reprezentációs bájt nulla” ígéret.

A (struct blob_meta){ ... } alak összetett literál, angolul compound literal. ISO C17-ben ez névtelen objektumot hoz létre. Függvényblokkon belül automatikus tárolási idejű, az adott blokk végéig él; nem csak a pontosvesszőig.

A következő teljes demo.c a később megadott implementációval fordítható:

#include "blob.h"

#include <assert.h>
#include <stdlib.h>

int main(void)
{
    unsigned char input[] = {0x41u, 0x42u};

    Blob *owner = blob_create(
        (struct blob_meta){
            .sequence = 7u,
            .label = "job"
        },
        input,
        sizeof input
    );

    if (owner == NULL) {
        return EXIT_FAILURE;
    }

    struct blob_view first = blob_get_view(owner);
    struct blob_view second = first;
    struct blob_view *alias = &first;

    second.meta.label[0] = 'J';
    assert(first.meta.label[0] == 'j');
    assert(second.data == first.data);

    alias->meta.sequence = 8u;
    assert(first.meta.sequence == 8u);
    assert(second.meta.sequence == 7u);

    input[0] = 0xffu;
    assert(first.data[0] == 0x41u);

    second = (struct blob_view){
        .meta = {.sequence = 9u}
    };
    assert(second.data == NULL);
    assert(second.size == 0u);

    blob_destroy(owner);

    /* first.data innen nem használható az adat elérésére. */
    return EXIT_SUCCESS;
}

Az első két nézet inicializálása után first és second külön objektum. A second.meta.label[0] módosítása ezért nem érinti first címkéjét. Bár tömbök között önálló értékadás nem végezhető, a struktúra másolása a fix méretű tömbtagok tartalmát is másolja.

Az alias nem új struktúraérték: ugyanahhoz a first objektumhoz biztosít másik hozzáférési utat. Az azon keresztüli módosítás tehát first módosítása.

A kezdeti second = first jellegű inicializálás és a későbbi struktúra-értékadás hasonló értékmásolást eredményez, de külön művelet. Értékadásnál a cél már létező, módosítható objektum, a forrás típusa pedig kompatibilis struktúratípus kell legyen. Például const adattagot tartalmazó struktúra nem rendelhető felül ilyen értékadással.

9.1.3 Shallow copy és tulajdonlási szerződés

A struktúramásolás a pointertag értékét másolja, nem a referált objektumot. Ezt a referált erőforráshoz képest sekély másolásnak, shallow copy-nak nevezzük.

A példában mindkét nézet data tagja ugyanarra a tárolóra mutat. A meta viszont beágyazott érték: külön másolat, nem élő kapcsolat a Blob metaadataihoz.

Elem Tulajdonosi szerep Tároló és élettartam Érvénytelenítő esemény
input A hívó által kezelt forrásobjektum Automatikus tömb, a blokk végéig él Kilépés a blokkból
owner A dinamikus Blob lezárásáért felelős pointerváltozó Maga a pointer automatikus objektum A változó megszűnése önmagában nem szabadít fel
*owner és adattartalma Egyetlen birtokolt allokáció Sikeres létrehozástól a blob_destroy hívásig blob_destroy(owner)
first, second Saját metaadatérték, kölcsönzött adatelérés A nézetobjektumok a blokk végéig élnek Az adattároló felszabadítása érvényteleníti a kölcsönzött hozzáférést
alias Nem tulajdonló pointer first objektumhoz first élettartamán belül használható first élettartamának vége

A const unsigned char * csak az ezen a pointeren keresztüli írást tiltja. Nem fejez ki tulajdonjogot, és önmagában nem garantálja, hogy más hozzáférési út sem módosíthatja az adatot.

Ha ugyanilyen pointertag tulajdonosi szerződésű lenne, a sekély másolás veszélyes lehetne. Két struktúra ugyanazt a tárolót tekinthetné sajátjának, majd mindkettő felszabadítaná: ez dupla felszabadításhoz és nem definiált viselkedéshez vezetne. C-ben ezt nem oldja meg automatikus másoló konstruktor vagy destruktor. Explicit klónozási, ownership-átadási vagy tiltott másolási szerződés kell.

A másolás a tagértékekre vonatkozik, nem a teljes objektumreprezentáció azonosságára. Struktúrára nincs beépített ==, a memcmp pedig nem általános értékegyenlőség-vizsgálat. A padding szerepét a Padding, alignment és offsetof rész tárgyalja.

9.1.4 Incomplete type és opaque API

A fejlécben a

struct blob;

előzetes deklaráció (forward declaration). Bejelenti a típust, de nem közli a tagjait, így az ott még befejezetlen típus, incomplete type.

A kliens deklarálhat rá pointert, átadhatja függvénynek és tárolhatja. Nem definiálhat azonban Blob objektumot érték szerint, nem használhatja rá a sizeof operátort, és nem érheti el a rejtett tagokat. A pointer mérete ismert lehet attól függetlenül, hogy a referált típus mérete nem ismert.

Az opaque handle itt Blob *: a kliens a műveleteket ismeri, a reprezentációt nem. Ez nem hardveres hozzáférés-védelem, hanem típusos API-határ. Lehetővé teszi az implementáció változtatását anélkül, hogy a kliens a belső tagelrendezésre támaszkodna; a teljes ABI-stabilitáshoz további szerződések is szükségesek.

Az API feltételei:

9.1.5 Flexible array member és ellenőrzött méretszámítás

A rejtett típus utolsó tagja rugalmas tömbtag, flexible array member. ISO C17-ben ilyen, méret nélkül deklarált tömbtag több névvel rendelkező tagot tartalmazó struktúra utolsó tagja lehet.

A teljes blob.c:

#include "blob.h"

#include <stdint.h>
#include <stdlib.h>
#include <string.h>

struct blob {
    struct blob_meta meta;
    size_t size;
    unsigned char data[];
};

Blob *blob_create(struct blob_meta meta,
                  const void *src, size_t size)
{
    if (src == NULL && size != 0u) {
        return NULL;
    }

    if (size > SIZE_MAX - sizeof(Blob)) {
        return NULL;
    }

    Blob *blob = malloc(sizeof *blob + size);
    if (blob == NULL) {
        return NULL;
    }

    blob->meta = meta;
    blob->size = size;

    if (size != 0u) {
        memcpy(blob->data, src, size);
    }

    return blob;
}

struct blob_view blob_get_view(const Blob *blob)
{
    return (struct blob_view){
        .meta = blob->meta,
        .data = blob->size == 0u ? NULL : blob->data,
        .size = blob->size
    };
}

void blob_destroy(Blob *blob)
{
    free(blob);
}

A data nem pointertag. Elemei a struktúrával közös allokációban találhatók, ezért nincs második allokáció és második felszabadítás. Az input tartalmának másolása miatt a létrehozott objektum független a forrás későbbi módosításától vagy megszűnésétől.

Legyen S=sizeof(𝙱𝚕𝚘𝚋)S=\operatorname{sizeof}(\texttt{Blob}), nn az elemek száma, EE az elemméret. Az általános allokációs méret:

N=S+nE. N=S+nE.

Itt unsigned char elemek vannak, ezért E=1E=1 C bájt, így N=S+nN=S+n.

Bizonyításvázlat a túlcsordulás kizárására. Mivel SS ábrázolható size_t érték, a 𝚂𝙸𝚉𝙴_𝙼𝙰𝚇−S\texttt{SIZE\_MAX}-S kivonás biztonságos. Ha

n≤𝚂𝙸𝚉𝙴_𝙼𝙰𝚇−S, n\leq \texttt{SIZE\_MAX}-S,

akkor mindkét oldalhoz SS-t adva

S+n≤𝚂𝙸𝚉𝙴_𝙼𝙰𝚇. S+n\leq \texttt{SIZE\_MAX}.

Az összeadás tehát nem fordul körbe. Nagyobb elemtípusnál még a szorzás előtt ezt kell ellenőrizni:

n≤⌊𝚂𝙸𝚉𝙴_𝙼𝙰𝚇−SE⌋. n\leq \left\lfloor \frac{\texttt{SIZE\_MAX}-S}{E} \right\rfloor.

A sikeres méretellenőrzés nem garantál sikeres allokációt; ezért külön kell vizsgálni a malloc eredményét.

A sizeof(Blob) nem tartalmazza a kért dinamikus elemszámot, de tartalmazhat paddinget. Nem szabad feltételezni, hogy pontosan megegyezik a data kezdőeltolásával. A sizeof(Blob) + n alak elegendő helyet foglal, az alkalmazás pedig kizárólag a tárolt size szerinti [0,size) tartományt tekinti tartalomnak.

A rugalmas tömbtagra nem alkalmazható sizeof. Az ilyen struktúra ISO C17-ben másik struktúra tagja vagy tömb eleme sem lehet; pointerként természetesen tárolható. A struktúra-értékadás nem biztosítja a rugalmas tömbrész tartalmának másolását: klónozásához külön allokáció és explicit tartalommásolás szükséges.

9.1.5.1 Fordítási profil

A három fájlhoz választott laborprofil: Linux, x86-64 System V ABI, LP64; GCC 14.2.0 vagy Clang 19.1.7, glibc 2.39. Ez profil-előírás, nem itt ellenőrzött futtatási környezet. A verziózott parancsnevek mögötti pontos kiadást külön rögzíteni kell.

gcc-14 -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
    -O0 -g blob.c demo.c -o demo-gcc

clang-19 -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
    -O0 -g blob.c demo.c -o demo-clang

A példák itt nem kerültek lefordításra vagy futtatásra. Az assert feltételek az elvárt viselkedést rögzítik, nem közölt mérési eredmények.

9.1.6 Gyakori hibák és javításuk

„A struktúra másolása mindent lemásol.” A pointer értékét igen, a referált tárolót nem. A javítás explicit klónozás vagy dokumentált borrowed kapcsolat.

„Az owner pointer nullázása minden nézetet érvénytelenít.” A tároló felszabadítása érvényteleníti a hozzáféréseket; egyetlen változó nullázása nem módosítja a többi pointert. A nézetek élettartamát kell korlátozni.

„A compound literal örökké él.” Blokkon belüli compound literal címét visszaadni dangling pointert eredményezhet. Érték szerinti visszaadás vagy megfelelően birtokolt tároló kell.

„A struktúra kiírható hálózatra a sizeof méretével.” A belső reprezentáció nem hordozható protokollszerződés. Ennek helyes alternatíváját a Byte order és biztonságos szerializálás rész vezeti le.

9.1.7 Feladatok

9.1.7.1 Másolat vagy alias?

Magyarázd meg, miért változik first.meta.sequence az alias használatakor, de miért nem változik first.meta.label a second módosításakor! Használható marad-e first.meta.sequence a blob_destroy után?

9.1.7.2 Önálló klón

Egészítsd ki az API-t Blob *blob_clone(const Blob *source); függvénnyel! Null forrásra adjon NULL-t. Siker esetén a klón az eredeti megsemmisítése után is használható legyen.

9.1.7.3 Három hibás feltételezés diagnózisa

Értékeld külön:

Mindegyiknél nevezd meg a hibafajtát és a javítást!

9.1.8 Megoldások

9.1.8.1 Másolat vagy alias?

Az alias ugyanarra a first objektumra mutat. A second ezzel szemben külön struktúra, saját beágyazott metaadattal és címketömbbel.

A blob_destroy után first.meta.sequence továbbra is olvasható: a nézet saját tagja, amelynek élettartama nem ért véget. A first.data által korábban elért tartalom viszont már nem használható. Ellenőrzési feltétel, hogy külön kezeld a nézetobjektum és a referált tároló élettartamát.

9.1.8.2 Önálló klón

A deklaráció a fejlécbe, az alábbi kiegészítés a blob.c fájlba kerül:

Blob *blob_clone(const Blob *source)
{
    if (source == NULL) {
        return NULL;
    }

    return blob_create(
        source->meta,
        source->size == 0u ? NULL : source->data,
        source->size
    );
}

A nem null forrásnak a hívás teljes ideje alatt élnie kell. A függvény új allokációt hoz létre, ezért a két objektum külön tulajdonosi kötelezettség. Hibakor NULL az eredmény, a forrás változatlan.

Ellenőrzési feltétel: az eredeti megsemmisítése után a klónból kapott új nézet tartalma egyezzen az eredetileg bemásolt tartalommal; végül a klónt is pontosan egyszer kell megsemmisíteni.

9.1.8.3 Három hibás feltételezés diagnózisa

A kliensoldali sizeof(Blob) befejezetlen típus miatt fordításkor diagnosztizálandó korlátsértés. A foglalást az implementációra kell bízni.

Az ellenőrizetlen size_t összeadás körbefordulhat. Maga az előjel nélküli körbefordulás definiált, de túl kicsi allokációt eredményezhet; az ezt követő túlindexelés már UB. Az összeadás előtti korlátellenőrzés szükséges.

A struktúra-értékadás nem klónozza a rugalmas tömbrész tartalmát. A javítás új allokáció és explicit tartalommásolás, például a blob_clone használata.

9.1.9 Összefoglaló

A struct beágyazott tagjai az aggregált érték részei; a pointertagok referált erőforrásai nem. A másolat és az alias külön fogalom, az ownership pedig explicit API-szerződés. Az opaque típus elrejti a reprezentációt, a flexible array member pedig egyetlen allokációban kapcsolhat fejlécet és változó hosszúságú tartalmat. A helyességhez mindehhez élettartamkorlát, ellenőrzött méretszámítás és egyértelmű hibakezelés szükséges.