Egy pointerparaméter típusa nem pusztán azt közli, hogy hol található
egy objektum. Azt is meghatározza, milyen műveleteket enged meg az adott
hozzáférési út. Az int *, a const int * és az
int *const ezért eltérő mutálhatósági szerződést fejez ki,
miközben egyik sem mondja meg önmagában, ki felel a referált objektum
élettartamáért.
A címképzésről és dereferenciáról tanultak alapján a pointerobjektumot meg kell különböztetnünk attól az objektumtól, amelyre mutat. A típusminősítők szabályait most erre a két — később több — szintre alkalmazzuk. A tárgyalás alapja ISO C17; a GNU-bővítéseket külön jelöljük.
Legyen T egy minősítés nélküli objektumtípus, például
int. Az alábbi táblázat inicializált, érvényes pointerekre
vonatkozik. A referált objektum olvasásához és írásához az élettartamra,
típusra és hozzáférhetőségre vonatkozó további feltételeknek is
teljesülniük kell.
| Deklaráció | A p pointer
átállítható? |
A referált objektum módosítható
*p útján? |
|---|---|---|
T *p |
Igen | Igen |
const T *p |
Igen | Nem |
T *const p |
Nem | Igen |
const T *const p |
Nem | Nem |
A const T * és a T const * ugyanazt a
típust jelöli: pointer konstansként elért T objektumra. A
csillag utáni const viszont magát a pointert minősíti.
C17-részlet:
int retries = 3;
int fallback = 5;
const int *view = &retries;
view = &fallback; /* Megengedett. */
/* *view = 8; */ /* Constraint-sértés. */
int *const fixed_pointer = &retries;
*fixed_pointer = 8; /* Megengedett. */
/* fixed_pointer = &fallback; */ /* Constraint-sértés. */
const int *const fixed_view = &retries;A constraint-sértés olyan nyelvi megszorítás megsértése, amelyhez a megfelelő C-implementációnak diagnosztikát kell adnia. Nem helyes ezeket egyszerűen „futásidejű hibának” nevezni.
A referált const hozzáférési korlátozás, nem általános
változatlansági garancia. A fenti retries továbbra is
módosítható a saját nevén vagy más, megfelelő típusú pointeren
keresztül. Ha azonban maga az objektum const minősítéssel
jött létre, módosításának kísérlete nem definiált viselkedés, akkor is,
ha egy cast eltávolította a hozzáférési út minősítését.
Az alábbiak C17 API-deklarációrészletek:
int read_setting(const int *setting, int *result);
void update_setting(int *setting);
void update_setting(int *const setting);A read_setting közvetlenül a setting
paraméteren keresztül nem írhatja a referált egész objektumot. A
result viszont kimeneti hely lehet. A típusokból még nem
derül ki, megengedett-e a null pointer, vagy mi történik hibakor: ezt
dokumentálni kell.
Az update_setting két deklarációja kompatibilis,
ugyanazt a függvényt is deklarálhatják. ISO C17 szerint
a paraméter legkülső minősítése nem különbözteti meg a
függvénytípusokat. A definícióban szereplő
int *const setting a helyi paraméterobjektum átállítását
tiltja:
setting = másik_pointer nem megengedett;*setting = 10 megfelelő előfeltételekkel
megengedett.A pointert a függvény érték szerint kapja. A helyi
setting átállítása egyébként sem állítaná át a hívó
pointerváltozóját.
A const nem jelent tulajdonjogot, szálbiztonságot vagy
automatikusan hosszabb élettartamot. Nem ígéri azt sem, hogy egy
összetett objektum minden további pointeren át elérhető része
változatlan marad.
Ha a függvénynek a hívó pointerváltozóját kell módosítania, annak
címére van szüksége. Innen ered a T ** kimeneti
paraméter.
Nézzük ezt az API-részletet:
int select_larger(const int *a, const int *b,
const int **out);A hívó ilyen változót adhat át:
const int *selected = NULL;
/* select_larger(&first, &second, &selected); */A három szint jelentése:
| Kifejezés a függvényben | Mit jelöl? | Módosíthatóság |
|---|---|---|
out |
Helyi pointerparaméter | Átállítható, de ez nem változtatja meg a hívó argumentumát |
*out |
A hívó selected pointerobjektuma |
Átállítható |
**out |
A kiválasztott egész objektum | Ezen az úton nem módosítható |
Az out tehát nem „írhatóvá teszi a konstans adatot”. Egy
olyan pointerobjektumhoz ad írható hozzáférést, amelynek értéke olvasási
nézetet biztosít.
Egy megfelelő szerződés például:
Siker esetén a visszatérési érték
0, és*outa nagyobb értékű bemeneti objektumra mutat; egyenlőségnél az elsőre. Nulloutesetén a függvény-1értékkel tér vissza, és nincs kimeneti írás. Egyébként null bemeneti pointer esetén*outnull lesz, a visszatérési érték pedig-1. A nem null bemeneteknek élő, olvashatóintobjektumokra, a nem nulloutértéknek élő, írhatóconst int *objektumra kell mutatnia.
A nullvizsgálat nem képes felismerni a dangling vagy más módon érvénytelen pointert. Ezek kizárása a hívó előfeltétele.
A visszaadott pointer kölcsönzött hozzáférés: nincs új objektum, nincs allokáció, nincs tulajdonátadás.
Az int * → const int * konverzió
megengedett. Az új hozzáférési út kevesebb műveletet enged: nem lehet
rajta keresztül írni.
Ebből azonban nem következik az int ** →
const int ** konverzió megengedettsége.
Hibás, diagnosztikát igénylő C17-részlet; nem futtatandó:
const int frozen = 42;
int *p = NULL;
const int **slot = &p; /* Nem megengedett implicit konverzió. */
*slot = &frozen;
*p = 7;Bizonyításvázlat — a tiltás típusbiztonsági indoka:
p típusa int *, ezért a *p
kifejezés típusa módosítható int.slot a
p pointerobjektumot const int * tárolására
alkalmas helyként kezelhetné.*slot = &frozen így egy valóban konstans objektum
címét juttathatná a p változóba.*p = 7 már nem hordozna const
minősítést, mégis konstans objektum módosítását kísérelné meg.Az utolsó lépések a hipotetikusan megengedett konverzió következményét szemléltetik, nem a hibás program garantált futását. Egy cast nem teszi helyessé ezt az objektumhozzáférési szerződést.
Fontos ellenpélda:
int value = 1;
int *p = &value;
int *const *slot = &p; /* Megengedett C17-ben. */Itt a const a közvetlenül referált
pointerobjektumot védi. A
*slot = másik_pointer tiltott, a **slot = 2
megengedett. Nem lehet tehát ezen az úton másfajta pointerértéket
becsempészni a p változóba.
A szabály nem az, hogy „több csillagnál tilos a const
hozzáadása”, hanem az, hogy pontosan melyik szinten történik a
minősítés.
ISO C17 szerint objektumtípusra mutató pointer
konvertálható void * típusra és vissza; a visszakonvertált
pointer az eredetivel egyenlőnek hasonlítandó. A megfelelő konverziók
C-ben implicit módon is használhatók:
int counter = 7;
void *opaque = &counter;
int *restored = opaque;Ez C-kód: C++-ban a void * → int * implicit
konverzió nem megengedett.
Az opaque nem őrzi meg ellenőrizhető futásidejű
típusinformációként, hogy int objektumot jelöl. A
visszaalakítás helyességét az API szerződésének kell biztosítania. Más
típusra alakítva a dereferencia igazítási vagy objektumhozzáférési
szabályt sérthet.
A minősítéseket itt is meg kell tartani:
const int limit = 100;
const void *opaque_readonly = &limit;
const int *limit_view = opaque_readonly;A const int * → void * implicit konverzió
eldobná a referált const minősítést, ezért nem megfelelő. A
void * szabály továbbá objektumpointerekre vonatkozik, nem
általános ISO C garancia függvénypointerek tárolására.
| Konverzió | ISO C17 |
|---|---|
int * → void * |
Megengedett |
int ** → void * |
Megengedett: egy pointerobjektum címének típusát töröljük |
int ** → void ** |
Nem megengedett implicit módon |
A void ** egy void * típusú objektumra
mutat. Nem tetszőleges pointerobjektum címét jelenti.
Ha egy függvény void **out paraméteren keresztül ír, a
hívónak valódi void * kimeneti objektumot kell
biztosítania. Egy int * változó címének
(void **) castolása nem alakítja át magát a változót
void * objektummá. A rajta keresztüli írás nem kompatibilis
lvalue-típussal érné el a pointerobjektumot; ezt az azonos pointerméret
sem teszi helyessé.
A helyes minta: külön void * változó fogadja az
eredményt, majd annak értékét konvertáljuk a
dokumentált céltípusra.
A void nem teljes objektumtípus, ezért ISO C17-ben nem
végezhető void * pointeraritmetika, és nem olvasható vagy
írható rajta keresztül referált objektum.
GNU-bővítésként egyes fordítómódok megengedik a
void * aritmetikát, egy C bájtos lépéssel. Ez nem
hordozható ISO C-kód. Objektumreprezentáció bejárásához megfelelő
előfeltételekkel unsigned char *, illetve
const unsigned char * használható. A konverzió nem szünteti
meg az objektumhatárokra vonatkozó szabályokat,
és egy C bájt nem feltétlenül nyolc bit.
Az alábbi teljes program két különböző kimeneti paramétert használ. A
select_larger a hívó objektumaira ad nézetet; a
borrowed_counter modulhoz tartozó, statikus tárolási idejű
objektumot tesz elérhetővé típuseltörléssel.
#include <assert.h>
#include <stddef.h>
static int select_larger(const int *a, const int *b,
const int **out)
{
if (out == NULL) {
return -1;
}
*out = NULL;
if (a == NULL || b == NULL) {
return -1;
}
*out = (*a >= *b) ? a : b;
return 0;
}
/*
* Siker: *out egy modultulajdonú, írható int objektumra mutat.
* Az objektum a program teljes futása alatt él.
* Nincs tulajdonátadás; a visszaadott pointer nem adható free()-nek.
* Null out: -1, kimeneti írás nélkül.
*/
static int borrowed_counter(void **out)
{
static int counter = 7;
if (out == NULL) {
return -1;
}
*out = &counter;
return 0;
}
int main(void)
{
int a = 12;
int b = 19;
const int *selected = NULL;
if (select_larger(&a, &b, &selected) != 0) {
return 1;
}
assert(selected == &b);
assert(*selected == 19);
b = 23;
assert(*selected == 23); /* A const nézet nem másolat. */
void *raw = NULL;
if (borrowed_counter(&raw) != 0) {
return 1;
}
int *counter = raw; /* A dokumentált típusra alakítjuk. */
assert(*counter == 7);
*counter = 8;
const void *erased_view = selected;
const int *restored_view = erased_view;
assert(restored_view == selected);
int status = select_larger(NULL, &b, &selected);
assert(status == -1);
assert(selected == NULL);
return 0;
}A választott fordítási profil: C17, Linux x86-64 System V,
LP64, GCC 14.2.0 vagy Clang 19.1.7, glibc 2.40. A következő
parancsokban a fordítónevek ezekre a kiválasztott kiadásokra értendők. A
példa itt nem fordítással vagy futtatással ellenőrzött eredmény; az
assert feltételek a várt viselkedést rögzítik.
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
-pedantic-errors -O0 -g pointer_contract.c -o pointer_contract_gcc
clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
-pedantic-errors -O0 -g pointer_contract.c -o pointer_contract_clangAz ownership- és élettartam-térkép:
| Objektum vagy hozzáférés | Tulajdonos, tároló | Érvényesség és érvénytelenítés |
|---|---|---|
a, b |
A hívó main automatikus tárolású objektumai |
Blokkjuk elhagyásáig élnek |
selected referált objektuma |
Kölcsönzött; a tároló továbbra is a hívóé | A célozott objektum élettartamának végén a hozzáférés érvénytelenné válik |
out a kiválasztás alatt |
Kölcsönzött hozzáférés a hívó pointerobjektumához | Csak a hívás alatt használjuk, nem tároljuk el |
Statikus counter |
A modulhoz tartozó statikus tároló | A program futása alatt él |
raw, counter,
restored_view |
Helyi, nem tulajdonló pointerobjektumok | Megszűnésük nem szünteti meg a referált objektumot |
A b értékének módosítása nem érvényteleníti a rá mutató
nézetet, csak megváltoztatja a megfigyelhető értéket. A
selected átállítása sem szünteti meg a korábbi célt. A
program egyszálú; a módosítható statikus számláló konkurens
használatához külön szinkronizációs szerződés kellene.
„A const pointeren át látott adat nem
változhat.” Más hozzáférési úton változhat. Ha pillanatfelvétel
kell, másolat szükséges.
„A kimeneti paraméternek adott pointert a függvény majd validálja.” Általános pointerérvényesség-ellenőrzés nincs. A nullvizsgálat csak egyetlen hibás állapotot fed le.
„A cast megoldja a dupla pointer inkompatibilitását.” A cast az objektum deklarált típusát nem változtatja meg. A javítás megfelelő típusú köztes pointerobjektum, nem erősebb cast.
„A visszaadott pointert nekem kell felszabadítanom.” Ezt csak ownership-szerződés döntheti el. A példában minden eredmény kölcsönzött, ezért nincs felszabadítási kötelezettség vagy jogosultság.
Magyarázó feladat. Mit enged meg az
int *const *slot az out, *out,
**out szinteknek megfelelően? Átadható-e neki egy
int *p változó címe?
Kódolási feladat. Írd meg az
int increment_selected(int *const *slot) függvényt. Null
slot, null *slot vagy INT_MAX
érték esetén adjon -1 eredményt, módosítás nélkül.
Egyébként növelje eggyel a referált egész értéket, és térjen vissza
nullával. A bemenet kölcsönzött; a függvény nem tárolhatja el.
Diagnosztikai feladat. Miért hibás a következő két sor két különböző módon? Javítsd őket a kidolgozott példa API-jával.
int *typed = NULL;
borrowed_counter((void **)&typed);
const int **read_slot = &typed;A helyi slot paraméter átállítható. A *slot
pointerobjektum ezen az úton nem módosítható, a **slot
egész objektum viszont igen. Az int ** →
int *const * konverzió megengedett, ezért
&p átadható.
Ellenőrzési feltétel: a függvénytörzsben a *slot = NULL
diagnosztikát igényelne, a **slot = 4 pedig megfelelő
élettartam- és hozzáférési feltételekkel megengedett.
A következő C17-részlet az előző programba illeszthető, azonos fordítási profillal:
#include <limits.h>
int increment_selected(int *const *slot)
{
if (slot == NULL || *slot == NULL) {
return -1;
}
if (**slot == INT_MAX) {
return -1;
}
++**slot;
return 0;
}Előfeltétel, hogy a nem null slot élő, olvasható
pointerobjektumra, annak nem null értéke pedig élő, írható
int objektumra mutasson. A kölcsönzés csak a hívásra szól;
a függvény nem érvénytelenít semmit.
Ellenőrzési feltételek: 4 értékből 5 lesz;
INT_MAX változatlan marad; null bemenetnél nincs
dereferencia; a hívó pointerének értéke egyik esetben sem változik.
Az első hibás sor castja elfedheti a diagnosztikát, de a függvény
ezután egy int * objektumba írna void * típusú
lvalue-n keresztül. A második sor önmagában tiltott implicit
minősítéskonverzió, amely diagnosztikát igényel.
Javított C17-részlet:
void *raw = NULL;
if (borrowed_counter(&raw) == 0) {
int *typed = raw;
const int *read_view = typed;
const int **read_slot = &read_view;
assert(**read_slot == *typed);
}Most minden kimeneti hely tényleges típusa megfelel az író pointer
referált típusának. A read_view külön pointerobjektum, nem
a typed tárolójának átértelmezése. A célt továbbra is a
modul birtokolja.
A const mindig egy meghatározott típusszintet minősít. A
const T * a referált objektum ezen az úton történő
módosítását, a T *const a pointer átállítását tiltja. A
T ** pointerobjektum módosítására használható, de nem
általánosan konvertálható const T ** vagy
void ** típusra.
A jó pointer-API a típus mellett rögzíti a nullkezelést, a hibakimenetet, a tulajdonost, a kölcsönzés időtartamát és az érvénytelenítés feltételeit. A minősítés a szerződés fontos része — de nem helyettesíti a teljes szerződést.