6.1 Címképzés, dereferencia és null pointer

A pointer nem maga a referált objektum, hanem olyan érték, amellyel egy objektumhoz közvetett hozzáférést fejezhetünk ki. A pointert tároló változó eközben szintén önálló objektum: saját típusa, tárolója, címe és élettartama van. A helyes programhoz ezt a két objektumot következetesen külön kell választani.

A Scope, linkage, storage duration és lifetime rész alapján egy név láthatósága és egy objektum élettartama külön fogalom. Az Inicializálás, indeterminate érték és reprezentáció rész pedig arra figyelmeztetett, hogy a lefoglalt tároló önmagában nem biztosít használható értéket. Pointereknél mindkét különbség döntő: a pointerváltozó még létezhet akkor is, amikor az egykor referált objektum már nem él.

Ebben a részben objektumokra mutató pointerekkel dolgozunk, ISO C17 szerint. A függvénypointereket később tárgyaljuk.

6.1.1 Név, objektum, pointerérték és saját cím

A következő C17-részlet egy függvény blokkjában értendő:

int count = 3;
int *p = &count;

A deklarációban az int *p jelentése: p egy int típusú objektumra mutató pointer. A kifejezésben szereplő egyoperandusú & operátor címképzést végez: &count típusa int *, értéke a count objektumra mutató pointer.

A kapcsolatot címek kitalálása nélkül is lerajzolhatjuk:

Név → saját objektum Az objektumban tárolt érték További kapcsolat
count → int objektum 3 Nincs pointerkapcsolat
p → int * objektum &count Tárolt pointerérték → count objektuma

A három fontos kifejezés:

Kifejezés Típus Jelentés
p int * A pointerobjektumból kiolvasott pointerérték
&p int ** A pointerobjektum saját címe
*p int típusú lvalue A referált count objektumot jelölő kifejezés

Az lvalue itt objektumot jelölő kifejezés: például egy értékadás bal oldalán megmondja, melyik objektumot módosítjuk.

/* C17-részlet, az előző deklarációk után. */
int snapshot = *p;  /* count értékének olvasása */
*p = 8;             /* count módosítása */

A * ebben a környezetben az indirekció, közkeletűen dereferencia operátora. Önmagában nem feltétlenül jelent értékkiolvasást: a környező kifejezés dönti el, hogy olvasunk vagy írunk.

Mindkét hozzáférés érvényes, mert count élő, inicializált, megfelelően igazított int objektum; p pontosan erre mutat, és az írás célja módosítható. A példában a környező függvény kezeli mindkét automatikus objektum élettartamát. p csak kölcsönzött hozzáférést ad: sem a pointer másolása, sem létezése nem hosszabbítja meg count élettartamát.

A címképzés itt közönséges, címezhető objektumra történik. C17-ben például bitmezőnek és register tárolásiosztály-jelölővel deklarált objektumnak nem képezhető így címe.

6.1.2 Null pointer constant és NULL

ISO C17: null pointer constant az olyan egész konstanskifejezés, amelynek értéke nulla, vagy ilyen kifejezés void * típusra alakítva. Például a 0, a 0L és a (void *)0 megfelel ennek.

Ha null pointer constant pointertípusra konvertálódik, az eredmény az adott típus null pointere. Ez nem mutat objektumra vagy függvényre.

/* C17-részlet. */
#include <stddef.h>

int *a = 0;
int *b = NULL;
int *c = (void *)0;

Mindhárom változó null pointerrel inicializálódik. A NULL könyvtári makró, amelyet például a <stddef.h> definiál; kifejtése az implementáció által meghatározott null pointer constant. Nem szabad arra építeni, hogy minden implementációban ugyanaz a szöveges alakja vagy típusa.

Fontos különbség a nulla érték és a megfelelő konstanskifejezés között:

/* C17: hibás tanítási részlet, diagnosztikát igénylő
   nyelvi megszorítássértés. */
int zero = 0;
int *bad = zero;

A zero változó kiolvasása nem egész konstanskifejezés, ezért itt nincs megengedett implicit egész–pointer konverzió. Egy cast hozzáadása sem megfelelő javítás: az egészből pointerré alakítás más szabályok alá tartozik.

A null pointer szemantikai érték, nem előírt bitminta. A nyelv nem követeli meg, hogy reprezentációja csupa nulla bit legyen. Ezzel szemben a szabványos nulla-inicializálás pointer esetén valóban null pointert eredményez; például egy inicializáló nélküli statikus tárolási idejű pointer ilyen értéket kap.

Használható pointerértéknél az alábbi tesztek egyenértékűek:

Egyik sem általános érvényességvizsgálat. A nem null pointer is lehet dereferálásra alkalmatlan.

6.1.3 A dereferencia érvényességi szerződése

Egy pointerrel végzett objektumhozzáférést nem a numerikus cím „hihetősége”, hanem több feltétel együttese igazol.

Ellenőrzendő feltétel Mit kell tudnunk?
Pointerérték A pointer inicializált, használható értékű, nem null
Élettartam A referált objektum élettartama még tart
Típus A hozzáférés típusa megfelel az objektumra vonatkozó típus- és aliasing-szabályoknak
Igazítás (alignment) A cím teljesíti a hozzáférési típus igazítási követelményét
Tartomány A pointer ténylegesen elérhető objektumot vagy tömbelemet jelöl
Művelet Olvasáskor az érték használható; íráskor az objektum módosítható

Többszálú programban ehhez a hozzáférések megfelelő szinkronizálása is hozzátartozik; itt egyszálú példákat vizsgálunk.

Az int * típus nem bizonyítja automatikusan ezeket a feltételeket. Egy cast nem hoz létre int objektumot, nem javítja meg az igazítást, és nem éleszt újjá egy megszűnt objektumot. A pointer típusa a hozzáférés szándékát és nyelvi szabályait írja le, nem futásidejű tanúsítvány.

Tartományhatárra példa a tömb végét közvetlenül követő, one-past pointer. Bizonyos műveletekhez szabályos érték, de azon a helyen már nincs dereferálható tömbelem. Ennek részletes szabályait a Pointeraritmetika, objektumhatár és provenance rész tárgyalja.

Bizonyításvázlat az alapmintára. Az int count = 3 élő, inicializált int objektumot hoz létre. A &count közvetlenül ennek címét adja, ezért az igazítás és az objektum kijelölése megfelelő. Az érték int * változóba kerül, típuseltérés nélkül. Amíg count él és p ezt az értéket őrzi, a *p útján történő olvasás, illetve írás teljesíti a fenti feltételeket.

A lényeg nem egy cím ellenőrzése, hanem a pointer létrejöttének és későbbi használatának követése.

6.1.4 Null, inicializálatlan és dangling pointer

A három állapotot nem szabad összemosni.

Állapot Hogyan keletkezik? Mire használható?
Null Például int *p = NULL; Összehasonlítható, továbbadható megfelelő szerződés szerint; nem dereferálható
Inicializálatlan automatikus pointer Például blokkbeli int *p; Értéke indeterminate; használat előtt értéket kell adni neki
Dangling A korábban referált objektum élettartama megszűnik A korábbi célhoz már nem biztosít érvényes hozzáférést

Az inicializálatlan pointer nem „véletlenszerűen null vagy érvényes”. Indeterminate értékét nem szabad érvényes pointerértékként kiolvasni, ellenőrizni vagy kiírni.

A dangling állapotnál ugyancsak fontos C17-részlet, hogy az objektum élettartamának végén a rá mutató pointer értéke indeterminate lesz:

/* C17-részlet egy függvényben; nincs hibás dereferencia. */
#include <stddef.h>

int *p = NULL;

{
    int local = 9;
    p = &local;
}                   /* local élettartama véget ér */

/* Itt p régi értékét nem olvassuk ki. */
p = NULL;           /* Az új értékadás nem olvassa a régi értéket. */

A belső blokk kezeli local élettartamát; p kölcsönzött hozzáférése a blokk végén érvénytelenné válik. A pointerobjektum tárolója ettől még létezik. A p = NULL biztonságos felülírás, de nem módosítja a korábban esetleg készített pointermásolatokat.

Gyakori hibás javítás az if (p != NULL) beszúrása egy élettartamhibás hozzáférés elé. Ez nem állítja helyre az objektumot; ebben a példában már a pointerérték kiolvasása sem megalapozott.

6.1.5 Kidolgozott példa: a pointer átállítása és a cél módosítása

A következő teljes program minimum ISO C17-et igényel. Választott, helyben ellenőrizendő laborprofil: Linux x86-64, System V ABI, LP64, 8 bites C bájt, GCC 14.2.0 vagy Clang 19.1.7, glibc 2.40. A program nem használ platformbővítést; az alábbi eredmények nyelvi levezetésből származnak, nem itt rögzített futtatásból.

/* pointer_basics.c */
#include <stddef.h>
#include <stdio.h>

int main(void)
{
    int first = 3;
    int second = 7;
    int *p = &first;

    printf("first cime: %p\n", (void *)&first);
    printf("p erteke:   %p\n", (void *)p);
    printf("p sajat cime: %p\n", (void *)&p);

    int observed = *p;
    *p = observed + 1;

    p = &second;
    *p = 8;

    printf("first=%d, second=%d\n", first, second);
    printf("pointermeret=%zu, int-meret=%zu\n",
           sizeof p, sizeof *p);

    p = NULL;
    if (p == NULL) {
        puts("p most nem jelol objektumot");
    }

    return 0;
}

Fordítás a megadott verziókra beállított fordítóparancsokkal:

gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O0 -g \
    pointer_basics.c -o pointer_basics-gcc

clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O0 -g \
    pointer_basics.c -o pointer_basics-clang

A main blokk kezeli az összes helyi objektum tárolóját és élettartamát. p nem tulajdonos, csak az aktuálisan kijelölt objektumhoz ad kölcsönzött hozzáférést.

Művelet Referált objektum Érvényesség indoka
observed = *p first Élő, inicializált int; címét &first szolgáltatta
*p = observed + 1 first Ugyanez, továbbá módosítható; a kiszámolt érték 4
p = &second Nincs dereferencia Csak p tárolt pointerértékét cseréljük le
*p = 8 second Élő, módosítható int; helyes típus, igazítás és objektumhatár

A levezetett értékeredmény first=4, second=8. A p = &second nem másolja át first értékét, és nem semmisíti meg first objektumát. A p = NULL ugyancsak csak a pointerobjektumot módosítja.

A dereferenciákhoz nem tartozik futásidejű hibakód: helyességüket a program szerkezete biztosítja. Az előfeltételek megsértését nem szabad garantált folyamathibának tekinteni; nem definiált viselkedés, vagyis UB lehet az eredmény.

6.1.6 Pointerméret és címkiírás

A sizeof p a pointerobjektum mérete, míg a sizeof *p az int objektumtípus mérete. A sizeof egysége C bájt; ennek bitszáma CHAR_BIT.

Méretkifejezés Mit mér? A megadott x86-64 System V LP64 profilban
sizeof p Egy int * objektumot 8 C bájt
sizeof *p Egy int objektumot 4 C bájt
sizeof(char) Egy char objektumot 1 C bájt

Ezek profilhoz kötött méretadatok, nem univerzális C-layout. A pointer mérete nem a referált objektum méretének függvénye, és a pointer nem tárol automatikusan objektumhosszt.

A példában a sizeof *p operandusa nem változó hosszúságú tömbtípusú, ezért nem értékelődik ki: nincs tényleges dereferencia. Így ez a méretkifejezés akkor is használható lenne, ha p null lenne. A helyes méret lekérdezése azonban nem igazolja egy későbbi *p hozzáférés helyességét.

ISO C17: a printf %p konverziója void * argumentumot vár. Ezért írjuk:

/* C17-részlet; p itt használható int * értéket tartalmaz. */
printf("%p\n", (void *)p);

A változó argumentumszámú részben az int * nem konvertálódik automatikusan void * típussá. A cast itt szükséges típusillesztés, nem érvényességjavítás.

A %p szöveges alakja implementáció által meghatározott; nem kötelező egy adott hexadecimális formátum. A kiírás a pointerértéket figyeli meg, nem olvassa a referált objektumot. Az eredmény nem bizonyít hozzáférési jogot, élettartamot vagy hordozható memórialayoutot, és külön futások között sem tekintendő stabil azonosítónak.

6.1.7 Feladatok

6.1.7.1 Magyarázó feladat: két pointer, két objektum

A következő C17-részlet ugyanazon élő blokkban fut:

int x = 5;
int y = 9;
int *p = &x;
int *q = p;

*q = 6;
p = &y;

Rajzold le a végső név–objektum–pointer kapcsolatokat. Mennyi x, y és *q? Miért nem változott meg q a p átállításakor?

6.1.7.2 Kódolási feladat: opcionális kimeneti pointer

Írj int store_if_present(int *out, int value) függvényt. Null out esetén ne írjon, és adjon vissza 0-t; egyébként tárolja value értékét, és adjon vissza 1-et.

Fogalmazd meg a nem null argumentum teljes szerződését is.

6.1.7.3 Diagnosztikai feladat: elégtelen nullvizsgálat

Az alábbi hibás C17-részletben a szükséges headerek rendelkezésre állnak:

int *p = NULL;

{
    int temporary = 12;
    p = &temporary;
}

if (p != NULL) {
    printf("%p: %d\n", p, *p);
}

Azonosítsd az élettartamhibát és a formátumszerződés megsértését. Javítsd a példát úgy, hogy a kiírás a temporary élő értékét használja.

6.1.8 Megoldások

6.1.8.1 Magyarázó feladat megoldása

A végső kapcsolatok:

Pointerobjektum Tárolt érték → cél
p &y → y, értéke 9
q &x → x, értéke 6

Tehát x == 6, y == 9, és *q == 6. A q = p inicializálás pointerértéket másolt; nem hozott létre állandó összeköttetést a két pointerváltozó között.

A *q hozzáférések érvényesek, mert x élő, inicializált, módosítható int, és q a címét tartalmazza. A kölcsönzött hozzáférés az x élettartamának végén érvénytelenedik. Ellenőrzéskor a pointerek célját és az objektumok értékét külön kell követni.

6.1.8.2 Kódolási feladat megoldása

/* C17-függvényrészlet. */
#include <stddef.h>

int store_if_present(int *out, int value)
{
    if (out == NULL) {
        return 0;
    }

    *out = value;
    return 1;
}

Nem null out esetén a hívó köteles élő, megfelelően igazított, módosítható int objektumra mutató pointert átadni. Az objektumnak a hívás végéig élnie kell. Korábbi értékének inicializáltsága itt nem szükséges, mert a függvény csak írja.

A hívó kezeli az objektum élettartamát; a függvény kölcsönzi a hozzáférést, nem őrzi meg a pointert. Csak a null esethez tartozik meghatározott hibajelzés. A dangling argumentum nem válik biztonságossá a nullvizsgálattól.

Ellenőrzési feltétel: store_if_present(NULL, 7) eredménye 0; élő int címével és 7 értékkel hívva az eredmény 1, a cél értéke pedig 7.

6.1.8.3 Diagnosztikai feladat megoldása

A blokk végén temporary élettartama megszűnik, ezért p korábbi értékére már a feltételben sem támaszkodhatunk. Ettől független szerződéshiba, hogy %p mellett nincs void * argumentum. Nincs garantált hibás kimenet.

A javított C17-részlet:

int *p = NULL;

{
    int temporary = 12;
    p = &temporary;

    printf("%p: %d\n", (void *)p, *p);
    p = NULL;
}

A dereferencia idején temporary élő, inicializált int, a címképzés biztosítja a megfelelő típust, igazítást és objektumhatárt. A pointert még az élettartam vége előtt nullra állítjuk. Ellenőrzési feltétel: a kiírt egész érték 12, a blokk után pedig p null; konkrét címszöveget nem követelünk meg.

6.1.9 Összefoglaló

A pointerváltozó és a referált objektum két külön objektum. p pointerérték, &p a pointerobjektum saját címe, *p pedig közvetett objektumelérés.

A biztonságos dereferencia indoklásához élettartam, megfelelő típus, igazítás, tartomány és műveleti jogosultság szükséges. A nullteszt ezekből csak a null értéket zárja ki. A pointermásolás nem hosszabbít élettartamot, a cast nem javít érvényességet, a címkiírás pedig megfigyelés, nem helyességi bizonyíték.