Egy változó neve eltűnhet a látókörünkből úgy, hogy a hozzá tartozó objektum tovább él. Fordítva is előfordulhat: egy pointerváltozó még elérhető, miközben az objektum, amelyre korábban mutatott, már nem létezik. Az ilyen helyzeteket nem lehet pontosan leírni pusztán azzal, hogy valami „lokális”, „globális”, „stacken van” vagy „heapen van”.
Négy külön kérdést kell feltennünk:
A Szimbólumok, linkage és relocation rész linkeroldali nézőpontját most ISO C17 nyelvi szabályokkal egészítjük ki. A deklarációk olvasásához a Deklarációk, typedef és típusminősítők rész jelöléseit használjuk.
A hatókör a forrásprogramhoz tartozó fogalom, nem futás közbeni időintervallum. Ha egy belső deklaráció elfed egy külsőt, attól a külső objektum nem szűnik meg.
Blokkhatókör (block scope) tartozik például a függvénytörzsben vagy egy belső blokkban deklarált közönséges lokális azonosítóhoz. A függvénydefiníció paraméterének neve szintén blokkhatókörű, és a függvénytörzs végéig használható. Egy közönséges objektumazonosító hatóköre a deklarátora befejezése után kezdődik, tehát nem visszamenőleg, a blokk elején.
Fájlhatókör (file scope) tartozik a blokkokon és paraméterlistákon kívüli deklarációk azonosítóihoz. Ez a deklaráció helyétől a fordítási egység végéig tart. Nem jelenti azt, hogy a név automatikusan használható egy másik fordítási egységben: ott is megfelelő deklaráció kell.
Függvényhatókör (function scope) C-ben
kizárólag a címkékhez tartozik. Egy goto célcímkéje az
egész függvényben megnevezhető, akár a címke szöveges előfordulása előtt
is. A függvény neve ettől még nem függvényhatókörű!
Függvényprototípus-hatókör (function prototype
scope) vonatkozik a nem definícióként szereplő
függvénydeklaráció paraméterneveire. Például az
int parse(int count); deklarációban a count
neve az adott függvénydeklarátor végéig rendelkezik hatókörrel. Ez a
deklaráció nem hoz létre futás közbeni paraméterobjektumot.
Az alábbi ISO C17-részletben két külön objektum szerepel:
/* Függvénytörzs-részlet. */
int limit = 10;
{
int limit = 3; /* Elfedi a külső nevet. */
limit += 1;
}
/* Itt ismét a külső limit neve használható; értéke 10. */A külső objektum a belső blokk végrehajtása alatt is él. Csak a nevével történő közvetlen elérését akadályozza az elfedés.
A kapcsolás az azonosítókhoz kapcsolódik; nem azt mondja meg, hogy egy objektum meddig él.
Külső kapcsolás (external linkage) esetén a megfelelő deklarációk különböző fordítási egységekben is ugyanazt az objektumot vagy függvényt jelölhetik. Ehhez a deklarációknak, típusoknak és definícióknak a C szabályai szerint összhangban kell lenniük.
Belső kapcsolás (internal linkage) esetén
az összetartozó deklarációk ugyanazon fordítási egységen belül jelölnek
közös entitást. Tipikus eset a fájlhatókörben static
kulcsszóval deklarált objektum vagy függvény.
Kapcsolás nélkül (no linkage) szerepelnek
például a közönséges automatikus lokális változók, a paraméterek és a
blokkban deklarált static objektumok nevei. Azonos
írásmódjuk nem kapcsolja össze őket más blokkok hasonló nevű
deklarációival.
Az extern nem egyszerűen azt jelenti, hogy „külső
kapcsolás”. Ha látható egy korábbi, belső vagy külső kapcsolású
deklaráció, az extern ezt a kapcsolást örökölheti:
/* ISO C17, fordításiegység-részlet. */
static int state;
void reset_state(void)
{
extern int state; /* A korábbi, belső kapcsolású objektum. */
state = 0;
}Itt nincs második state objektum. A blokkhatókörű
deklaráció ugyanazt nevezi meg, mint a fájlhatókörű deklaráció. Korábbi
látható deklaráció nélkül egy blokkhatókörű
extern int state; külső kapcsolást adna.
A következő táblázat ISO C17 szerinti, egymástól független
deklarációs helyzeteket foglal össze. A tárolási idő és az élettartam
az objektum tulajdonsága, nem a névé. A
malloc(n) sorban pozitív méretű, sikeres allokációt
feltételezünk.
| Deklaráció vagy entitás | A név hatóköre | Kapcsolás | Az objektum tárolási ideje | Az objektum élettartama |
|---|---|---|---|---|
Fájlhatókörű int total; |
Fájl | Külső | Statikus | A program teljes végrehajtása |
Fájlhatókörű static int hidden; |
Fájl | Belső | Statikus | A program teljes végrehajtása |
Blokkban int local; |
Blokk | Nincs | Automatikus | A blokk adott végrehajtásába való belépéstől annak befejezéséig |
Függvénydefiníció int arg paramétere |
Blokk | Nincs | Automatikus | Az adott függvényhíváshoz kötött |
Blokkban static int count; |
Blokk | Nincs | Statikus | A program teljes végrehajtása |
Blokkban extern int total;, a fenti total
látható deklarációjával |
Blokk | Külső | A megnevezett objektumé: statikus | A program teljes végrehajtása |
Fájlhatókörű _Thread_local int tls; |
Fájl | Külső | Szálhoz kötött | Szálanként külön objektum, az adott szál teljes végrehajtása |
Blokkban static _Thread_local int cache; |
Blokk | Nincs | Szálhoz kötött | Szálanként külön objektum, az adott szál teljes végrehajtása |
Blokkban int *p = malloc(n);: maga a
p |
Blokk | Nincs | Automatikus | A blokk adott végrehajtásához kötött |
Az előző p által megcímzett allokált objektum |
Nincs deklarált neve | Név hiányában nem alkalmazható | Allokált | Az allokációtól a deallokációig |
int parse(int count); prototípusban
count |
Prototípus | Nincs | Nem keletkezik objektum | Nem alkalmazható |
Függvényen belüli done: címke |
Függvény | Nincs | Nem objektum | Nem alkalmazható |
A táblázat két fontos ellenpéldát tartalmaz. A blokkhatókörű
static neve csak helyben használható, de objektuma az egész
végrehajtás alatt él. A malloc eredményét tároló
automatikus pointerváltozó megszűnhet anélkül, hogy az allokált objektum
megszűnne.
Automatikus tárolási időnél a nem változó hosszúságú objektum élettartama a hozzá tartozó blokkba való belépéstől a blokk végrehajtásának bármilyen módon történő befejezéséig tart. Másik függvény meghívása csak felfüggeszti ezt a végrehajtást: nem fejezi be az élettartamot. Rekurzív belépéskor új objektumpéldány keletkezik.
A változó hosszúságú tömb, vagyis VLA kivétel: élettartama a deklarációjától a hatókörének elhagyásáig tart. Részleteit a Többdimenziós tömbök, VLA és layout rész tárgyalja.
Statikus tárolási időnél az objektum a program
teljes végrehajtása alatt él. Inicializálása a programindulás előtt
megtörténik. C-ben a blokkhatókörű static inicializálása
nem az első függvényhíváskor végrehajtott, tetszőleges kód.
Szálhoz kötött tárolási időnél minden szálhoz külön
objektum tartozik. Blokkhatókörben a _Thread_local mellé
static vagy extern szükséges. Másik szálból az
objektum közvetett elérésének eredménye ISO C17 szerint implementáció
által meghatározott; a szálak közötti pointerátadásra ezért itt nem
építünk hordozható szerződést.
Allokált tárolási időnél a létrehozás és
megszüntetés nem a blokkhatárt követi. A részletes könyvtári szerződés a
malloc, calloc, free és allokációs szerződés
témája. A sikeres realloc a régi objektum élettartamát
lezárja akkor is, ha az új tároló címe ugyanolyannak látszik; ezt a realloc és dinamikusan növekvő buffer rész bontja
ki.
Ezek nyelvi kategóriák, nem kötelező stack-, heap- vagy ELF-szekció-hozzárendelések. A gyakori megvalósítást a C objektumok a folyamat virtuális címterében rész külön kezeli.
Legyen egy objektum élettartama a végrehajtás eseményeivel jelölt
ahol a kezdet, a megszűnés. Egy időpontban végzett objektumhozzáférés szükséges feltétele:
Ez nem elégséges feltétel: a megfelelő objektumhatár, típus, igazítás és művelettől függően az inicializált érték is szükséges. Ezeket a következő részek pontosítják.
Bizonyításvázlat. A pointer másolása csak pointerértéket másol; nem módosítja a célobjektum tárolási idejét. Ha a célobjektum automatikus élettartama a függvényből visszatéréskor véget ér, egy kimásolt cím nem hosszabbíthatja meg azt.
Az élettartamon kívüli objektumhozzáférés nem definiált viselkedés, azaz UB. ISO C17-ben az objektumra vagy közvetlenül mögé mutató pointer értéke az objektum élettartamának végén meghatározatlanná (indeterminate) válik. A régi címet ezért ne hasonlítgassuk vagy naplózzuk érvényességvizsgálatként.
/* ISO C17, szándékosan hibás részlet. */
static int *bad_value(void)
{
int value = 42;
return &value;
}
/* A *bad_value() kiértékelése UB: a célobjektum már nem él. */A value tárolójának életciklusát a függvényhívás kezeli;
a visszaadott pointer csak kölcsönzött hozzáférés lenne. Érvénytelenítő
eseménye a visszatérés. Nincs meghatározott hibakimenet: sem a
42, sem összeomlás nem garantált.
A tárolóban esetleg megmaradó bitek nem jelentenek tovább élő
objektumot. A NULL-ellenőrzés sem igazolja az élettartamot.
Skalár eredménynél a természetes javítás az érték szerinti
visszaadás.
static: tartós, de közös állapotAz alábbi önálló példa minimum ISO C17-et igényel. Választott laborprofil: GCC 14.2.0, Linux x86-64 System V ABI, glibc 2.40. A program nem támaszkodik konkrét címméretre vagy objektumelrendezésre.
/* lifetime.c */
#include <assert.h>
#include <stdio.h>
static unsigned fresh_value(void)
{
unsigned value = 0;
return ++value;
}
static unsigned *shared_value(void)
{
static unsigned value = 0;
value = (value + 1u) % 10u;
return &value;
}
int main(void)
{
unsigned first = fresh_value();
unsigned second = fresh_value();
unsigned *borrowed = shared_value();
unsigned snapshot = *borrowed;
unsigned *again = shared_value();
assert(first == 1u && second == 1u);
assert(borrowed == again);
assert(snapshot == 1u && *borrowed == 2u);
printf("%u %u %u %u\n",
first, second, snapshot, *borrowed);
return 0;
}Fordítás a külön tanulói laborban:
gcc -std=c17 -Wall -Wextra -Wpedantic -O0 -g lifetime.c -o lifetimeA szabályokból levezetett várt kimenet 1 1 1 2; ez nem
mért futtatási eredmény.
A fresh_value minden hívása új automatikus objektumot
kap. A shared_value összes hívása ugyanazt a statikus
objektumot módosítja. A snapshot önálló értékmásolat, míg a
két pointer ugyanahhoz az objektumhoz ad hozzáférést.
A statikus tárolót a C futtatási környezet kezeli; logikai
tulajdonosa a függvényt biztosító modul. A pointerek nem tulajdonosok,
ezért nem szabad rajtuk free-t hívni. Célobjektumuk a
program végéig él. Újabb hívás nem érvényteleníti a pointert, de
módosítja a megfigyelt értéket. A példa szerződése soros használat;
nincs allokációs hibája, az assertionök szerződéssértést
jelezhetnek.
A reentrancia (reentrancy) azt jelenti, hogy egy művelet korábbi meghívásának befejezése előtt újra meghívható a szerződése megsértése nélkül. Egy statikus munkapuffer vagy részszámítási állapot ezt könnyen megakadályozza: a belső hívás felülírhatja a külső hívás adatait. A hosszú élettartam tehát nem jelent hívásonként külön állapotot.
A módosítható statikus objektum párhuzamos elérése szinkronizáció
nélkül adatversenyt is okozhat. A static önmagában nem
szinkronizáció, a _Thread_local pedig nem oldja meg az
ugyanazon szálon történő rekurzív vagy callbackes újrabelépést.
A „lokális tehát rövid életű” következtetést a blokkhatókörű
static cáfolja. A „static tehát belső
kapcsolású” következtetés csak megfelelő deklarációs környezetben igaz:
blokkban a névnek nincs kapcsolása.
Az automatikus pointerváltozó megszűnése nem szabadítja fel az általa megcímzett allokációt. Fordítva, a célobjektum megszűnése nem szünteti meg az összes rá mutató pointerváltozót. Ezek megmaradhatnak érvénytelen értékkel.
Végül a lokális cím visszaadásának static-kal történő
„javítása” szerződésváltozás: a hívásonként külön eredményből közös,
később felülírható állapot lesz.
Fájlhatókörben szerepel a static int state;, egy
függvény blokkjában pedig az extern int state;. Hány
objektum van? Milyen a második név hatóköre, kapcsolása, illetve a
megnevezett objektum tárolási ideje?
Írd át a bad_value függvényt úgy, hogy statikus objektum
és allokáció nélkül adjon vissza 42-t. Indokold, miért nem
baj a lokális objektum megszűnése!
Vizsgáld meg ezt az ISO C17 szerinti, szándékosan hibás függvénytörzs-részletet:
int *saved = 0;
{
int object = 9;
saved = &object;
}
int result = *saved;Ki kezeli az object tárolóját? Mikor érvénytelenedik a
kölcsönzött hozzáférés? Javítana-e a helyzeten, ha csak a
saved deklarációjához adnánk static
kulcsszót?
Egy objektum van. A második név blokkhatókörű, de a látható korábbi deklarációból belső kapcsolást örököl. Az objektum statikus tárolási idejű, élettartama a program teljes végrehajtása.
Ellenőrzési feltétel: a válasz külön kezelje a második deklaráció szűkebb hatókörét és a változatlan objektumélettartamot.
/* ISO C17, függvénydefiníció-részlet. */
static int good_value(void)
{
int value = 42;
return value;
}A visszatérési kifejezés kiértékelésekor a value még él.
A hívó az egész értéket kapja meg, nem az objektum címét. Ellenőrzési
feltétel: az eredmény további használata ne függjön a függvény lokális
tárolójától.
Az object automatikus tárolójának életciklusát a belső
blokk végrehajtása kezeli. A saved csak kölcsönzött
hozzáférést tárol. A belső blokk elhagyása lezárja a célobjektum
élettartamát; az ezt követő dereferencia UB, garantált eredmény
nélkül.
A saved statikussá tétele csak a pointerobjektum életét
hosszabbítaná meg. A célobjektumét nem. Helyes javítás például a
szükséges érték kimásolása még a belső blokkban. Ellenőrzési feltétel:
minden célobjektum-hozzáférés annak élettartamán belül történjen.
A hatókör a név használhatóságát, a kapcsolás a deklarációk összetartozását írja le. A tárolási idő az objektum tárolásának időbeli kategóriája, az élettartam pedig az objektum létezésének konkrét intervalluma. Egy pointer fennmaradása nem hosszabbítja meg a célobjektum életét; egy hosszú életű objektum pedig még nem biztosít reentráns vagy szálbiztos interfészt.