7.4 Memóriamásolás, összehasonlítás és byte view

A hálózatról vagy fájlból kapott adat nem feltétlenül sztring: tartalmazhat nulla értékű bájtokat, és nincs szükség lezáró karakterre. A nullterminált sztringeknél a tartalom jelöli a véget; egy bináris adatszeletnél ezt külön hossz tárolja. A biztonságos feldolgozás alapja ezért nem egy magányos pointer, hanem pointer, hossz és élettartam-szerződés együtt.

Ebben az egységben minden hossz C bájtban, azaz a sizeof egységében értendő. Egy C bájt CHAR_BIT bit; az itt bemutatott algoritmusok nem igénylik, hogy ez pontosan nyolc legyen. A nyelvi és könyvtári szabályok alapja ISO C17.

7.4.1 A nem tulajdonló byte view szerződése

A byte view egy összefüggő bájttartomány nem tulajdonló nézete. Minimális reprezentációja:

/* ISO C17; deklarációrészlet, platformfüggetlen. */
#include <stddef.h>

typedef struct {
    const unsigned char *data;
    size_t size;
} byte_view;

A struktúra itt csupán két összetartozó adat csomagolása. A data a tartomány elejét, a size az olvasható C bájtok számát adja meg. Nem tárol kapacitást, és nem felel az adatok felszabadításáért.

A fejezet API-jaiban a következő invariánst használjuk:

A const csak az ezen hozzáférési úton történő módosítást tiltja. A tulajdonos más pointeren keresztül továbbra is megváltoztathatja a tartalmat. A nézet ezért nem pillanatfelvétel.

Az objektumreprezentációról tanultak szerint karaktertípuson, így unsigned char hozzáférésen keresztül megvizsgálható egy objektum reprezentációja. Ez aliasing szempontból megengedett hozzáférési út, de nem szünteti meg az élettartam- és határkövetelményeket. Egy helyes típusú pointer is lehet dangling vagy tartományon kívüli.

7.4.2 A négy memóriaművelet pontos szerepe

Az ISO C17 <string.h> függvényei bájttartományokat kezelnek, nem nullterminált sztringeket. A nulla értékű bájt egyiknél sem lezárójel.

Függvény Művelet Átfedési és hozzáférési szerződés
memcpy(dst, src, n) n C bájt másolása A forrás olvasható, a cél írható; a másolt tartományok nem fedhetik egymást
memmove(dst, src, n) n C bájt másolása A forrás olvasható, a cél írható; az átfedés megengedett
memset(dst, c, n) n C bájt kitöltése A cél írható; c előbb unsigned char értékké alakul
memcmp(a, b, n) Két n hosszú tartomány összehasonlítása Mindkettő olvasható; egymással átfedhetnek

Mindegyiknél a hívó biztosítja a megfelelő méretű, élő tárolót. A függvények nem vesznek át tulajdonjogot, és nem őrzik meg a pointereket a hívás után. A másoló- és kitöltőfüggvény a célpointert adja vissza; ez nem hibajelzés. A megsértett előfeltételhez általában nem definiált viselkedés, nem pedig visszaadott hibakód tartozik.

7.4.2.1 Átfedés: nem optimalizálási részlet

A memcpy tiltása a ténylegesen érintett tartományokra vonatkozik. Egyetlen nagy tömb két diszjunkt része másolható vele. Azonos kezdőcím esetén viszont pozitív hossznál teljes az átfedés, ezért még a „saját magára másolás” sem megfelelő memcpy-hívás.

A memmove eredménye olyan, mintha először egy külön ideiglenes bájttömbbe másolná a forrást, majd onnan a célba. Ez szemantikai modell: nem követel tényleges allokációt. A megvalósítás például választhat megfelelő másolási irányt.

A memmove tehát az átfedést kezeli, de nem javítja ki a hibás hosszt, a dangling pointert vagy az írhatatlan célt. A karakteres hozzáférés aliasing-engedménye sem írja felül a memcpy külön átfedési tilalmát.

7.4.2.2 Nulla hossz és null pointer

ISO C17 könyvtári előfeltétel: a pointerargumentumoknak nulla hossz esetén is érvényes értékeknek kell lenniük. Az alábbiak ezért nem hordozható, szabályos „üres műveletek”:

/* HIBÁS ISO C17-részletek: ne használd őket. */
memcpy(NULL, NULL, 0);
memmove(NULL, NULL, 0);
memset(NULL, 0, 0);
memcmp(NULL, NULL, 0);

A nulla hossz azt jelenti, hogy nincs másolt, kitöltött vagy összehasonlított bájt; nem törli a pointerargumentumokra vonatkozó követelményeket. Saját API-nk elfogadhat {NULL, 0} nézetet, de ekkor a könyvtári hívás előtt külön ágon kell kezelnie az üres esetet. Null pointeren még a + 0 pointeraritmetika sem megengedett.

7.4.2.3 Összehasonlítás és kitöltés

A memcmp az első eltérő bájtpár alapján rendez, a bájtokat unsigned char értékként értelmezve. A visszatérési értékből csak az előjelre támaszkodhatunk:

r<0,r=0,r>0. r<0,\qquad r=0,\qquad r>0.

Nem garantált, hogy az eredmény pontosan -1 vagy 1. Különböző hosszúságú szeletek egyenlőségéhez előbb a hosszakat kell összehasonlítani. Egy közös prefix egyezése önmagában nem egyenlőség.

A memset bájtokat tölt ki, nem típusos értékeket állít be. A memset(ints, 1, sizeof ints) nem egyes értékű int elemeket hoz létre. Hasonlóképpen az általános null pointer-reprezentációról nem feltételezhető, hogy minden bitje nulla. Az itt használt unsigned char puffer nullázása viszont egyértelmű.

Szokásos lineáris költségmodellben a másolás és kitöltés O(n)O(n), a memcmp legrosszabb esetben O(n)O(n) munka. Az utóbbi korán megállhat; titkok összehasonlítására nem ad állandó idejű működési garanciát. Ezek folytonos hozzáférések, és nem adnak át új tulajdonolt tárolót a hívónak.

7.4.3 Bounds-ellenőrzött részszelet

Legyen a teljes hossz NN, a kezdőeltolás oo, a kért hossz pedig kk. A kívánt matematikai feltétel:

0≤o≤N,0≤k≤N−o. 0 \le o \le N,\qquad 0 \le k \le N-o.

A kézenfekvő o + k <= N ellenőrzés veszélyes: a size_t előjel nélküli egész típus, ezért az összeadás körbefordulhat.

Bizonyításvázlat. Először ellenőrizzük, hogy o≤No\le N. Ekkor az N−oN-o kivonás nem csordul alul. Ha ezután k≤N−ok\le N-o, akkor matematikailag o+k≤No+k\le N. Mivel NN ábrázolható size_t értékként, a megfelelő összeg is ábrázolható lenne — de az ellenőrzéshez ki sem kell számítani.

A pointerképzés csak az ellenőrzések után következhet. Nulla hosszú eredménynél a kanonikus üres nézetet állítjuk elő, így null pointerhez sem adunk eltolást. A háttérszabályokat a pointeraritmetika és objektumhatárok rész rögzíti.

7.4.3.1 Élettartam és invalidáció

Szereplő Felelősség, élettartam Érvénytelenítő esemény
Tulajdonos Gondoskodik a tároló fennmaradásáról Automatikus tároló blokkjának elhagyása; dinamikus tároló felszabadítása
Tároló Tartalmazza az elérhető bájtokat Élettartamának vége; sikeres újraallokáció a régi nézetek számára
byte_view objektum Csak pointert és hosszt tárol Saját megszűnése a leírót szünteti meg, nem a háttéradatot
Részszelet Ugyanazt a tárolót kölcsönzi Ugyanazok a háttértároló-események, mint az eredeti nézetnél

A nézet lemásolása nem hosszabbít élettartamot. Egy lokális tömbre mutató nézet visszaadása ezért hibás. A tartalom átírása önmagában nem érvényteleníti a pointert, de megváltoztatja, amit a nézeten keresztül olvasunk.

7.4.4 Kidolgozott példa: átfedő puffermozgatás

A következő program két API-t definiál:

Mindkét függvény csak a hívás idejére kölcsönzi a tárolót. A null pointert és bizonyos mérethibákat felismerik, de a hazug hosszt vagy dangling pointert nem: az érvényes háttértároló továbbra is hívói előfeltétel.

/* bytes.c — ISO C17, hosted környezet. */
#include <stdbool.h>
#include <stddef.h>
#include <string.h>

typedef struct {
    const unsigned char *data;
    size_t size;
} byte_view;

static bool byte_sub(byte_view src, size_t offset, size_t count,
                     byte_view *out)
{
    if (out == NULL)
        return false;
    if (src.size != 0 && src.data == NULL)
        return false;

    if (offset > src.size)
        return false;
    if (count > src.size - offset)
        return false;

    byte_view result = {NULL, 0};
    if (count != 0) {
        result.data = src.data + offset;
        result.size = count;
    }

    *out = result;
    return true;
}

static bool byte_copy(unsigned char *dst, size_t capacity,
                      byte_view src)
{
    if (src.size > capacity)
        return false;
    if (src.size == 0)
        return true;
    if (dst == NULL || src.data == NULL)
        return false;

    memmove(dst, src.data, src.size);
    return true;
}

int main(void)
{
    unsigned char buffer[8];
    const unsigned char initial[] = {0x10, 0x00, 0x20, 0x30};
    const unsigned char expected[] = {
        0x10, 0x00, 0x00, 0x20, 0x30
    };

    memset(buffer, 0, sizeof buffer);

    /* Különálló tömbök: nincs átfedés. */
    memcpy(buffer, initial, sizeof initial);

    byte_view all = {buffer, sizeof buffer};
    byte_view part = {NULL, 0};

    if (!byte_sub(all, 1, 3, &part))
        return 1;

    /* Forrás: [1, 4), cél: [2, 5), tehát átfednek. */
    if (!byte_copy(buffer + 2, sizeof buffer - 2, part))
        return 1;

    if (memcmp(buffer, expected, sizeof expected) != 0)
        return 1;

    return 0;
}

A tároló tulajdonosi szerepét a main tölti be: a buffer automatikus tömb a függvény végéig él. Az all és part csak kölcsönzött hozzáférések. A másolás nem invalidálja őket, de a part által megfigyelt bájtokat is megváltoztatja.

Kijelölt példalabor: Linux x86-64, System V ABI, LP64, CHAR_BIT == 8; GCC 14.2.0 és Clang 19.1.7, glibc 2.40. A kód nem támaszkodik erre a konkrét ABI-ra. Az alábbi fordításokhoz a parancsnevek a megadott kiadásokat jelöljék; a példa itt nincs lefordítva vagy futtatva.

gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O2 \
    bytes.c -o bytes-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O2 \
    bytes.c -o bytes-clang

A szerződésekből levezetett várt eredmény nulla kilépési kód, nem mért futtatási eredmény.

7.4.5 Objektumreprezentáció nem egyenlő szerializációval

Egy objektum sizeof méretű reprezentációja másolható megfelelő bájtpufferbe, de ettől még nem válik hordozható üzenetté.

A struktúra kitöltőbájtokat tartalmazhat; egész típusok szélessége és bájtsorrendje eltérhet; pointerértékek más folyamatban rendszerint értelmetlenek. A kitöltés ráadásul nem kívánt memóriaadatot is kiszivárogtathat. Két mezőnként egyenlő struktúra reprezentációja sem szükségképpen azonos, ezért a memcmp nem általános struktúraegyenlőség.

A szerializáció külön adatformátumot határoz meg: mezőszélességet, bájtsorrendet, megengedett értékeket és hosszszabályokat. A byte view ezt a már definiált formátumot szállíthatja, de nem definiálja helyettünk. A visszamásolás sem tesz egy tetszőleges bájtsorozatot automatikusan érvényes típusos értékké.

7.4.6 Feladatok

  1. Szerződésmagyarázat. Egy tízbájtos tömb első nyolc bájtját két pozícióval jobbra mozgatjuk. Melyik másolófüggvény használható? Miért nem válik szabályossá a memcmp(NULL, NULL, 0) a nulla hossztól?

  2. Kódolás. Írj byte_equal függvényt két érvényes byte view összehasonlítására. Két üres nézet legyen egyenlő; eltérő hosszúságú nézetek ne legyenek azok.

  3. Diagnosztika. Mi hibás ebben az ISO C17-részletben? Tegyük fel, hogy s egyébként érvényes nézet.

    /* HIBÁS részlet; a byte_view deklarációját feltételezi. */
    if (offset + count <= s.size) {
        out->data = s.data + offset;
        out->size = count;
    }

    Vizsgáld a túlcsordulást, az üres nézetet és az eredményparamétert is.

7.4.7 Megoldások

7.4.7.1 Szerződésmagyarázat

A forrás [0,8), a cél [2,10): átfednek, de mindkettő a tömbön belül van. memmove használható; memcpy nem.

A nulla hossz nem engedélyez null pointerargumentumot a C17 memcmp számára. Saját összehasonlítónk az üres esetet könyvtári hívás nélkül kezeli. Ellenőrzési feltétel: a mozgatás után a cél nyolc bájtja egyezzen a forrás mozgatás előtti nyolc bájtjával.

7.4.7.2 Egyenlőségvizsgálat

Az előző C17-fájlba illeszthető részlet:

static bool byte_equal(byte_view a, byte_view b)
{
    if (a.size != b.size)
        return false;
    if (a.size == 0)
        return true;

    return memcmp(a.data, b.data, a.size) == 0;
}

Mindkét nézet háttértárolója a hívás végéig éljen és legyen olvasható; egymással átfedhetnek. A függvény nem módosít adatot, nem tart meg pointert. Hibás nézetre nem ígér hibajelzést. Ellenőrzési esetek: két üres nézet; azonos tartalom; eltérő hossz; azonos hossz mellett egy eltérő bájt; beágyazott nulla bájt.

7.4.7.3 A hibás részszelet javítása

Az offset + count körbefordulhat. Például offset == SIZE_MAX, count == 2 esetén az összeg 1, így átmehet egy nyolcbájtos nézet hibás ellenőrzésén. A későbbi pointeraritmetika már önmagában szabálytalan.

Üres {NULL, 0} nézetnél a NULL + 0 sem megengedett. Végül out érvényességét is biztosítani kell: a null pointer felismerhető, de egy nem null, mégis érvénytelen pointer általánosan nem ellenőrizhető.

A kidolgozott byte_sub mindhárom problémát kezeli a saját előfeltételei mellett. Ellenőrzési határesetek: offset == size, count == 0 siker; ugyanott count == 1 hiba; offset > size hiba; üres bemenetből üres eredmény siker.

7.4.8 Összefoglaló

A byte view hozzáférést ír le, nem tulajdonjogot. Helyességéhez együtt kell a valós hossz, az élő tároló és a megengedett hozzáférés. Átfedésnél memmove szükséges; üres nézetnél külön ág védi a könyvtári előfeltételeket. A részszelet határát kivonással ellenőrizzük, az objektumreprezentációt pedig nem tévesztjük össze hordozható adatformátummal.