6.3 const pointerek, több szintű indirection és void*

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.

6.3.1 A referált objektum és a pointer külön minősíthető

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.

6.3.1.1 Mit ígér a const a függvényhatáron?

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:

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.

6.3.2 Két indirekciós szint és kimeneti paraméter

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 *out a nagyobb értékű bemeneti objektumra mutat; egyenlőségnél az elsőre. Null out esetén a függvény -1 értékkel tér vissza, és nincs kimeneti írás. Egyébként null bemeneti pointer esetén *out null lesz, a visszatérési érték pedig -1. A nem null bemeneteknek élő, olvasható int objektumokra, a nem null out é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.

6.3.3 Miért nem konvertálható általánosan a T** const T**-rá?

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:

  1. A p típusa int *, ezért a *p kifejezés típusa módosítható int.
  2. Ha a megjelölt konverzió megengedett lenne, a slot a p pointerobjektumot const int * tárolására alkalmas helyként kezelhetné.
  3. A *slot = &frozen így egy valóban konstans objektum címét juttathatná a p változóba.
  4. A *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.

6.3.4 A void* típuseltörlés, nem általános hozzáférési engedély

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.

6.3.4.1 Miért nem általános kimeneti paraméter a void**?

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.

6.3.4.2 A void* aritmetika nem ISO C

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.

6.3.5 Kidolgozott példa és ownership-szerződés

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_clang

Az 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.

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

„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.

6.3.7 Feladatok

  1. 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?

  2. 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.

  3. 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;

6.3.8 Megoldások

6.3.8.1 A mutálhatósági szintek felismerése

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.

6.3.8.2 Ellenőrzött módosítás dupla pointeren keresztül

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.

6.3.8.3 A kimeneti pointer típusának helyreállítása

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.

6.3.9 Összefoglaló

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.