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.
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.
NULLISO 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:
p == NULL: null-e?p == 0: null-e?!p: null-e?if (p): nem null-e?Egyik sem általános érvényességvizsgálat. A nem null pointer is lehet dereferálásra alkalmatlan.
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.
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.
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-clangA 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.
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.
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?
Í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.
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.
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.
/* 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.
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.
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.