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.
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:
{NULL, 0}.data érvényes, és tőle kezdve
legalább size C bájt jogszerűen olvasható.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.
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.
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.
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.
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:
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
,
a memcmp legrosszabb esetben
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.
Legyen a teljes hossz , a kezdőeltolás , a kért hossz pedig . A kívánt matematikai feltétel:
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
.
Ekkor az
kivonás nem csordul alul. Ha ezután
,
akkor matematikailag
.
Mivel
á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.
| 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.
A következő program két API-t definiál:
byte_sub nem írja a forrásadatot. Az
eredményparaméter legyen érvényes, írható byte_view
objektum, a forrás bájtjaitól különálló tárolóban. Hiba esetén
változatlan marad.byte_copy írható célpuffert kap, annak kapacitásával.
A forrás és cél átfedhet; ezért memmove-ot használ. Nulla
kapacitásnál a cél lehet null pointer. Hiba esetén nem másol.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-clangA szerződésekből levezetett várt eredmény nulla kilépési kód, nem mért futtatási eredmény.
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é.
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?
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.
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.
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.
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.
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.
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.