6.2 Pointeraritmetika, objektumhatár és provenance

A pointeraritmetika nem tetszőleges memóriacímekkel végzett egészaritmetika. C-ben a pointer referált típusa meghatározza a lépés egységét, az objektumhatár pedig azt, hogy a lépés egyáltalán megengedett-e. Egy numerikusan hihető cím ezért még nem jelent jogszerű hozzáférést: számít, melyik tömbből indultunk, meddig tart annak élettartama, és milyen műveletet akarunk végezni.

A Címképzés, dereferencia és null pointer részben megkülönböztettük a pointer értékét a rajta keresztül elért objektumtól. Most ehhez tartományt rendelünk. A Promóciók, signedness és túlcsordulás szabályai továbbra is érvényesek: az index számítása már azelőtt hibás lehet, hogy a pointeraritmetika elkezdődne.

6.2.1 A pointerlépés egysége

ISO C17: pointerhez egész értéket adhatunk, illetve pointerből egész értéket vonhatunk ki, ha a pointer teljes objektumtípusra mutat, és a művelet az alább meghatározott tömbhatárokon belül marad. A „teljes” itt azt jelenti, hogy a referált típus mérete ismert; a void és a függvénytípus nem ilyen.

Legyen a egy NN elemű, T elemtípusú tömb, ahol N>0N>0. Ha

p=a+i, p = a+i,

akkor a p + n eredménye az i+ni+n pozíciót jelöli. A megengedettség feltétele:

0≤i≤N,0≤i+n≤N. 0 \le i \le N, \qquad 0 \le i+n \le N.

Ezek matematikai indexekre vonatkozó feltételek; nem azt javasolják, hogy az ellenőrzéshez előbb esetleg túlcsorduló C-kifejezést számítsunk ki.

6.2.1.1 Tömbkép és címeltolódás

A következő számpélda profilja Linux, x86-64 System V ABI, LP64, CHAR_BIT == 8, sizeof(int) == 4. Az offsetek C bájtban értendők. A 0x1000 kezdőcím kizárólag illusztráció, nem mérési eredmény és nem garantált elhelyezés.

Legyen a tömb tartalma {12, 7, -3, 9}:

Pozíció Jelentés Offset a tömb kezdetétől Illusztratív cím Elemként dereferálható?
a + 0 a[0], értéke 12 0 0x1000 Igen
a + 1 a[1], értéke 7 4 0x1004 Igen
a + 2 a[2], értéke -3 8 0x1008 Igen
a + 3 a[3], értéke 9 12 0x100c Igen
a + 4 Tömbvégi határjelző 16 0x1010 Nem

Ha p = a + 1, akkor p + 2 nem két C bájttal, hanem két int elemmel lép tovább:

Δ=2⋅sizeof(𝚒𝚗𝚝)=2⋅4=8 C bájt. \Delta = 2\cdot\operatorname{sizeof}(\texttt{int}) = 2\cdot4 = 8\text{ C bájt}.

Az eredmény a + 3, tehát az illusztratív cím 0x100c.

Bizonyításvázlat. A tömbelemek folytonosan, egymást követően helyezkednek el. Egy elem sizeof(T) C bájtot foglal, beleértve az elem típusának esetleges belső kitöltését is. Ezért két, nn indexkülönbségű elem kezdete között n⋅sizeof(T)n\cdot\operatorname{sizeof}(T) C bájt az eltolódás. Ez a tárolási kapcsolat magyarázza a lépésméretet, de nem alakítja a nyelvi pointerműveletet korlátlan egészaritmetikává.

Az unsigned char * egyes lépése egy C bájt, nem feltétlenül nyolc bit. A void * aritmetikáját az ISO C17 nem engedi meg; ha egy fordító mégis elfogadja, az bővítés, nem hordozható C.

6.2.2 Azonos tömb, objektumhatár és one-past

ISO C17: pointeraritmetikával a tömb elemeire és a közvetlenül az utolsó elem utáni, one-past pozícióra juthatunk. A one-past érték képzése megengedett, elemként történő dereferálása nem.

A négy elemű tömbnél:

Az utolsó pont fontos: nem elég elkerülni a hibás pointer dereferálását. A tömbön túlvezető aritmetikai eredmény képzése is nem definiált viselkedés, azaz UB lehet. Az utólagos visszalépés nem javítja meg.

A szokásos feldolgozási tartomány ezért fél-nyílt:

[𝚏𝚒𝚛𝚜𝚝,𝚕𝚊𝚜𝚝). [\texttt{first},\texttt{last}).

A first az első feldolgozható elemre, a last a kizárt végpontra mutat. A last lehet a tömb belsejében vagy annak one-past pozícióján. Az üres tartományt azonos kezdő- és végpointer jelölheti, feltéve, hogy ezek a szerződés szerint érvényes pointerek.

A null pointer más eset: a null pointerhez adott nulla sem megengedett pointeraritmetika. Egy üres tartományt null pointerpárral megengedő API-nak ezért külön kell kezelnie ezt az esetet; a last - first ott nem válik automatikusan szabályossá.

6.2.2.1 Az objektum nem azonos a memórialappal

Az aritmetikai határt nem az OS által leképezett lap, hanem a C-objektum határa adja. Két egymás mellé kerülő önálló változó nem válik kételemű tömbbé.

E műveletek szabályai szerint egy nem tömbelemként létező objektum úgy kezelhető, mint egy egyelemű tömb. Egy élő int x esetén tehát &x + 1 képezhető, de azon keresztül nem érhetünk el egy feltételezett „következő változót”.

A határ betartása szükséges, de nem elégséges: dereferáláskor az élettartamnak, a referált típusnak és a hozzáférés egyéb feltételeinek is megfelelőnek kell lennie. A megmaradó numerikus cím nem tartja életben az objektumot.

6.2.3 Pointerkülönbség és reprezentálhatóság

ISO C17: két megfelelő, kompatibilis teljes objektumtípusra mutató pointer kivonása akkor megengedett, ha ugyanannak a tömbnek az elemeit vagy one-past pozícióját jelölik, és az eredmény reprezentálható ptrdiff_t típussal.

A ptrdiff_t a <stddef.h> fejlécben deklarált előjeles egész típus. Az eredmény elemszám, nem C bájtban mért távolság.

Ha

p=a+i,q=a+j, p=a+i,\qquad q=a+j,

akkor

q−p=j−i. q-p=j-i.

A korábbi példában:

(a+4)−(a+1)=4−1=3. (a+4)-(a+1)=4-1=3.

A fordított kivonás eredménye −3-3. A két pozíció közötti tárolási eltolódás a megadott profilban 3⋅4=123\cdot4=12 C bájt, de a pointerkülönbség továbbra is 3.

A reprezentálhatósági feltétel nem formaság. Egy implementáció elvileg megengedhet olyan nagy tömböt, amelynek két távoli pozíciója közötti különbség nem fér el ptrdiff_t típusban. Ilyenkor a kivonás UB, még azonos tömb esetén is. A <stdint.h> fejléc PTRDIFF_MIN és PTRDIFF_MAX makrói írják le e típus értéktartományát.

A későbbi konverzió nem javítja meg a problémát. A (size_t)(q - p) előbb elvégzi a pointerkivonást, és csak utána konvertál. Ha pedig a különbség szabályosan negatív, az előjel nélküli konverzió akkor is váratlanul nagy értéket eredményezhet.

6.2.4 Relációs összehasonlítás: nem általános címrendezés

ISO C17: ugyanazon tömbben a <, <=, > és >= operátorok az elemek sorrendjét követik. A one-past pozíció minden tényleges elem után következik.

Két független tömb pointereire viszont nem alapozhatunk hordozható rendezést. Attól, hogy a debuggerben látható címek számként rendezhetők, a p < q még nem lesz definiált C-művelet.

Vannak külön aggregátumszabályok: például ugyanazon struktúra később deklarált tagjának pointere nagyobbnak hasonlít, a union tagjainak pointerei pedig egyenlőnek. Ezek nem adnak általános felhatalmazást független objektumok összehasonlítására, és pointerkivonásra sem.

Az == és != nem azonos kategória a relációs rendezéssel. Megfelelő típusú, meghatározott pointerértékek egyenlősége független objektumok esetén is vizsgálható. Az egyenlőség azonban önmagában nem bizonyít közös tömbhöz tartozást: egy tömbvégi pointer és egy közvetlenül utána elhelyezett objektum kezdőpointere egyenlőnek hasonlíthat. Ettől az első tömb határa nem tolódik ki.

6.2.5 Pointer, egész reprezentáció és provenance

ISO C17: a pointer–egész konverziók általános eredménye implementáció által meghatározott. Ha a pointer értéke a választott egész típusban nem reprezentálható, a pointerből egészbe konverzió UB. Nem szabad automatikusan feltételezni, hogy az unsigned long vagy a size_t megfelelő pointertároló típus.

A <stdint.h> opcionálisan biztosítja a uintptr_t előjel nélküli egész típust. Ha létezik, bármely érvényes void * pointer átalakítható erre, majd változatlan értékből vissza void * típusra úgy, hogy a visszakapott pointer az eredetivel egyenlőnek hasonlítson. A UINTPTR_MAX makró jelenléte használható az elérhetőség ellenőrzésére.

Objektumpointer esetén a szemléltetett út:

𝚒𝚗𝚝 *→𝚟𝚘𝚒𝚍 *→𝚞𝚒𝚗𝚝𝚙𝚝𝚛_𝚝→𝚟𝚘𝚒𝚍 *. \texttt{int *} \longrightarrow \texttt{void *} \longrightarrow \texttt{uintptr\_t} \longrightarrow \texttt{void *}.

Ez nem garantálja, hogy az egész reprezentációhoz hozzáadott sizeof(int) ugyanazt eredményezi, mint a típusos pointerlépés. Nem általános garancia függvénypointerekre sem. A visszaalakítás nem hosszabbítja meg az eredeti objektum élettartamát.

6.2.5.1 Mit jelent itt a provenance?

A provenance a pointer eredetének és objektumhoz kapcsolódásának leírására használt fogalom. Hasznos mentális modell: a pointer nem pusztán egy számsor, hanem hozzáférési lehetőség egy meghatározott objektumhoz, meghatározott feltételekkel.

Ez nem feltétlenül külön, futásidőben tárolt mezőt jelent. Nem azonos a lapvédelemmel vagy egy OS-jogosultsággal sem.

A provenance szabványosítása és pontos modellezése szabványfejlesztési, implementációs és kutatási kérdés. Az ISO C17 konkrét objektum-, élettartam- és pointeraritmetikai szabályait nem szabad összemosni későbbi javaslatokkal, és nem indokolt egységes, minden fordítóra érvényes jövőbeli modellt feltételezni.

A gyakorlati következtetés stabil: ha rendelkezésre áll az eredeti, típusos pointer, azon végezz határellenőrzött aritmetikát. Ne próbálj egész címekből hozzáférési jogosultságot rekonstruálni.

6.2.6 Kidolgozott példa: negatív telemetriaérték keresése

Az alábbi teljes program minimum ISO C17-et igényel. Kijelölt laborprofilja: Linux x86-64 System V LP64, GCC 14.2.0 vagy Clang 19.1.7, glibc 2.40. A program nem igényel GNU-bővítést. A parancsok fordítási javaslatok; itt nem történt fordítás vagy futtatás.

A kereső előfeltétele, hogy first és last ugyanazon élő int tömb rendezett határpontjai legyenek. Nem fogad null pointerpárt. Sikertelen kereséskor last értékével tér vissza; ez nem dereferálható találat.

Szerep Objektum vagy hozzáférés Élettartam és érvénytelenítés
Tulajdonosi felelősség A main blokkja kezeli a samples tömböt A blokk elhagyásáig
Tároló A samples automatikus tárolási idejű tömb Nem történik áthelyezés vagy újrafoglalás
Kölcsönzött hozzáférések first, last, p, hit, before, after Nem élhetik túl érvényes hozzáférésként a tömböt
Sikertelen keresés hit == last Ekkor nincs elemolvasás
Szerződéssértés Hibás határ vagy lejárt élettartam Nincs általános futásidejű ellenőrzés; UB következhet
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

static int *find_first_negative(int *first, int *last)
{
    for (int *p = first; p != last; ++p) {
        if (*p < 0) {
            return p;
        }
    }
    return last;
}

int main(void)
{
    int samples[] = {12, 7, -3, 9};
    size_t n = sizeof samples / sizeof samples[0];

    int *first = samples;
    int *last = first + n;
    int *hit = find_first_negative(first, last);

    /* A negyelemu tombnel a kulonbseg reprezentalhato. */
    ptrdiff_t count = last - first;
    printf("count=%td\n", count);

    if (hit != last) {
        printf("index=%td value=%d\n", hit - first, *hit);
    } else {
        puts("not found");
    }

#ifdef UINTPTR_MAX
    void *before = first;
    uintptr_t encoded = (uintptr_t)before;
    void *after = (void *)encoded;

    printf("roundtrip_equal=%d\n", after == before);
#endif

    return 0;
}
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O2 \
    pointer_bounds.c -o pointer_bounds.gcc

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

A várt első két sor count=4 és index=2 value=-3. Ha a uintptr_t rendelkezésre áll, a várt harmadik sor roundtrip_equal=1. Ezek levezetett elvárások, nem rögzített futtatási eredmények.

Bizonyításvázlat. A ciklus elején p a first és last közötti zárt pointertartományban van. A törzs csak p != last esetén fut, így a dereferencia a fél-nyílt feldolgozási tartományban marad. Az utolsó elem utáni növelés még szabályos végpointert képez; ezt követően a ciklus kilép. A függvény nem tárolja el a kölcsönzött pointereket.

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

A sizeof(T) értékével történő kézi felszorzás gyakran kettős skálázást okoz: a p + n már elemekben lép. A kívánt harmadik következő elemhez p + 3 kell, nem p + 3 * sizeof *p.

A „majd ellenőrzöm, hogy túlment-e” megközelítés is hibás lehet. Előbb az indexet kell ellenőrizni, és csak utána képezni a pointert. Egy már hibásan létrehozott pointer utólagos összehasonlítása nem helyreállítás.

A figyelmeztetés- vagy sanitizermentes futás nem bizonyítja a szabályosságot. Különösen a puszta, később nem dereferált határátlépő pointerképzést nem feltétlenül jelzi minden diagnosztikai eszköz.

6.2.8 Feladatok

6.2.8.1 Lépés és különbség

Egy élő int a[6] tömbben p = a + 2. A fenti laborprofilban osztályozd a p + 3, p + 4, p + 5 kifejezéseket. Mennyi (a + 6) - p, és hány C bájt a megfelelő tárolási eltolódás?

6.2.8.2 Ellenőrzött pointerképzés

Írj position nevű függvényt int *base, size_t length, size_t index paraméterekkel. Null bázis vagy index > length esetén adjon vissza null pointert, egyébként base + index értékét.

Előfeltétel: nem null base esetén a hívó biztosítja, hogy az élő tömbben legalább length elem következzen a bázistól. Ki a tulajdonos, és dereferálható-e minden nem null eredmény?

6.2.8.3 Diagnosztikai vizsgálat

Az alábbi hibás ISO C17-részlet három műveletét egymástól függetlenül vizsgáld. Mindkét tömb helyi, élő objektum; egyik pointer sem tulajdonos.

int a[2] = {1, 2};
int b[2] = {3, 4};

int *past = a + 3;
ptrdiff_t distance = b - a;
int ordered = a < b;

Melyik művelet hibás, és változtat-e ezen, ha a debugger szomszédos címeket mutat?

6.2.9 Megoldások

6.2.9.1 Lépés és különbség

A p + 3 az a[5] elemre mutat, dereferálható. A p + 4 az a + 6 one-past érték, képezhető, de nem dereferálható. A p + 5 képzése UB.

A különbség 6−2=46-2=4 elem; a megadott profilban az eltolódás 4⋅4=164\cdot4=16 C bájt. Ellenőrzési feltétel: a képzett index legyen a [0,6][0,6] tartományban, elemhozzáféréshez pedig a [0,6)[0,6) tartományban.

6.2.9.2 Ellenőrzött pointerképzés

ISO C17-megoldásrészlet, a <stddef.h> fejléc szükséges hozzá:

int *position(int *base, size_t length, size_t index)
{
    if (base == NULL || index > length) {
        return NULL;
    }
    return base + index;
}

A hívó marad a tömb tulajdonosi felelőse; az eredmény kölcsönzött pointer. A tömb élettartamának vége érvényteleníti a hozzáférést. Az index == length eredmény a kijelölt tartomány végpontja, ezért nem kezelhető feltétlenül dereferálható elemként.

Ellenőrzési esetek: null bázis, nulla index, index == length, valamint index > length. A függvény a hívó által megadott length valódiságát nem tudja megállapítani.

6.2.9.3 Diagnosztikai vizsgálat

Mindhárom művelet hibás: az a + 3 túllép a megengedett one-past határon, a kivonás különböző tömböket érint, a relációs összehasonlítás pedig független tömbök pointereit rendezi. Ezekhez nincs garantált kimenet.

A szomszédos gépi címek nem változtatják meg az objektumhatárokat. A helyességi ellenőrzés alapja a tömbazonosság és az indexhatár, nem a debugger címképe vagy a diagnosztika hiánya.

6.2.10 Összefoglaló

A pointeraritmetika egysége a referált típus egy eleme. A megengedett tartomány a tömb első elemétől a one-past pozícióig tart, de a végpointer nem dereferálható. Kivonáskor közös tömb és ptrdiff_t-reprezentálhatóság szükséges; relációs összehasonlítással nem rendezhetők tetszőleges objektumcímek. A uintptr_t opcionális round-trip lehetőség, nem általános címmanipulációs felhatalmazás. A numerikus reprezentáció soha nem helyettesíti az objektum-, élettartam- és tartományszerződést.