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.
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
elemű, T elemtípusú tömb, ahol
.
Ha
akkor a p + n eredménye az
pozíciót jelöli. A megengedettség feltétele:
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.
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:
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,
indexkülönbségű elem kezdete között
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.
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:
a + 4 képezhető;(a + 4) - 1 az utolsó elemre mutat;*(a + 4) nem használható tömbelem elérésére;a + 5 már önmagában sem megengedett.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:
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á.
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.
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
akkor
A korábbi példában:
A fordított kivonás eredménye
.
A két pozíció közötti tárolási eltolódás a megadott profilban
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.
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.
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:
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.
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.
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.clangA 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.
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.
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?
Í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?
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?
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 elem; a megadott profilban az eltolódás C bájt. Ellenőrzési feltétel: a képzett index legyen a tartományban, elemhozzáféréshez pedig a tartományban.
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.
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.
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.