Személyes eszközök
Keresés

 

A InfoWiki wikiből


Hatáskör

A deklaráció helye befolyásolja az azonosító (változónév) hatáskörét. A hatáskör a forráskódhoz köthető fogalom, azt adja meg, hogy a név a forráskód mely soraiban használható fel (mely soraiban látható).

A változók hatásköri szabálya szerint a deklaráció helyétől (sorától) a deklarációt tartalmazó programblokk végéig tart a hatáskör.


A programblokkot a kezdő kapcsos, és a bezáró kapcsos zárójelpár definiálja. Vagyis:

static void Main()
{
  int a; // itt kezdődik az 'a' név hatásköre
  ..
  ..
} // és eddig tart

A befoglaló blokk legegyszerűbb esetben a Main() függvény programblokkja, de később fogunk látni, hogy a programblokkokon belül szűkebb, beágyazott programblokkok is létrehozhatók. Ilyen lehet az alábbi eset:

static void Main()
{
  int a; // itt kezdődik az 'a' név hatásköre
  ..
  ..
  {
    int b; // itt kezdődik a 'b' név hatásköre
    ..
  }  // b hatáskör vége
  .. // itt már nincs 'b'
  .. 
  ..
} // 'a' hatáskörének vége

A hatásköri szabály szerint amennyiben a forráskódban a hatáskörön kívüli ponton vagyunk, úgy ott a deklarált azonosító (változó) nem látható, rá nem lehet hivatkozni, úgy viselkedik, mintha nem is létezne, nem is deklaráltuk volna.

A hatásköri szabály adja meg azt, hogy a program szövegében pontosan mely részeken lehet elérni, használni egy változót. A hatáskörön kívül a változó nem használható fel, nem lehet rá hivatkozni. A fordító ez eseteben unknown identifier, ismeretlen azonosító hibaüzenettel jelzi a problémát.


Amennyiben egy változót a program egy más pontján is fel akarunk használni, akkor a deklarációját kell megfelelő helyre mozgatni. A fenti példábana b változó hatásköre elég szűk, a belső programblokkra korlátozódik csak. Amennyiben az azt követő programsorokban is szeretnénk a b változót használni, a deklarációját ki kell rakni a belső programblokkon kívülre:

static void Main()
{
  int a; // itt kezdődik az 'a' név hatásköre
  ..
  ..
  int b; // itt kezdődik a 'b' név hatásköre
  {
    ..
  }  
  .. // még itt is van 'b'
  .. 
  ..
} // 'a' és 'b' hatáskörének vége

Újradeklarálás

Egy változó hatáskörén belül nem deklarálható újra. Az alábbi példa hibás:

static void Main()
{
  int a; // itt kezdődik az 'a' név hatásköre
  ..
  ..
  {
    int a; // újabb 'a' változó bevezetése lehetetlen
  }  
  .. 
  int a; // itt is tilos
  ..
} // 'a' hatáskörének vége

De hatáskörén kívül újradeklarálása lehetséges:

static void Main()
{
  ..
  ..
  {
    int b; // egy 'b' változó bevezetése lehetetlen
    ..
  } a 'b' hatáskörének végre
  .. 
  int b; // ez egy másik 'b' változó, újradeklarálás
  ..
} // ezen második 'b' hatáskörének vége

Mindaddig amíg a hatáskörök nem fedik át egymást, szabad ugyanazon nevű változót újra-és-újra deklarálni. Persze megfontolandó, hogy miért van erre szükség, gyakran értelmet zavaró is lehet. Egy, a kódot felületesen átnéző elemző nem feltétlenül veszi észre, hogy a 'b' már megszűnt, és újradeklarálásra került. Azt hiheti, a két 'b' változó ugyanaz.

Újradeklaráláskor a változó újra létrejön, bekerül a memóriába. Semmi sem garantálja, hogy ugyanarra a helyre kerül, mint az előző, ugyanazon nevű változó volt, de még ebben az esetben sem lehetséges, hogy az új változó emlékezzen a korábbi értékére.


Egy újradeklarált változó függetlenül a program korábbi eseményeitől, kezdetben definiálatlan-nak van minősítve, először mindenképpen értéket kell kapjon.

Élettartam

Felmerül a kérdés, miért tennénk egy változó deklarációját egy belső programblokk belsejébe, amikor úgy csak korlátozzuk annak használhatóságát, látható haszon nélkül.

Több okai is lehet annak, hogy egy változó nevének használhatóságát korlátozzuk (például védekezhetünk véletlen elírásos hibákkal szemben is ilyen módon), de most egy másik lehetséges előnyt vizsgáljunk meg alaposabban - az élettartamot.

Volt már szó arról, hogy az adott típusú adatnak tárolási igénye van, amely megadja hogy hány byte-nyi helyet fogunk a memóriából erre a célra lekötni. De nem volt arról még szó, hogy mennyi ideig kell ezt tenni.

A literálok a program szövegébe beszúrt változók, mindaddig foglalják a memóriát, amíg a programkódunk maga a memóriába van betöltve. Ezzel tehát sokat nem tudunk kezdeni.

A változók a program futása közben kerülnek be a memóriába, és kerülnek ki. Ez egy dinamikus folyamat. Úgy kell elképzelni, hogy a programunk fut, eközben változókat tölt be a memóriába (foglal számukra helyet), és kidobja azokat a változókat a memóriából, amelyekre nincs szükség.

Mikor kerülnek be a memóriába a változók? Nos, a program futása során szekvenciális sorrendben történik az utasítások végrehajtása (ez megfelel a program szövegében szereplő sorrendnek). Amikor a program futása során egy változódeklarációs részre ér, akkor a változónak lefoglalja a helyet. Ez pontosan megegyezik a hatáskör belépési pontjának.

A program folytatja az utasítások végrehajtását. Amikor a program kilép az adott változó hatásköréből, a változó kikerül a memóriából, és az általa korábban lefoglalt hely felszabadul - más változók kerülhetnek erre a helyre.

Ennek megfelelően minél később deklarálunk egy változót, annál később kerül be a memóriába - ez nyilván jó. De a legkülső blokkban deklarált változók felszabadítására csak akkor kerülhet sor, ha a legkülső blokkot hagyja el a program. Ez általában elég késői időpont.

De amennyiben egy belső blokkba helyezzük a változót, a kilépés sokkal korábban megtörténik. Vizsgáljuk meg újra az mádosik példát:


static void Main()
{
  int a; // itt kezdődik az 'a' név hatásköre
  ..
  ..
  {
    int b; // itt kezdődik a 'b' név hatásköre
    ..
  }  // b hatáskör vége
  .. // itt már nincs 'b'
  .. 
  ..
} // 'a' hatáskörének vége

A b változóról nem csak azt vehetjük észre, hogy aránylag későn kerül be a memóriába, hanem a belső blokk elhagyása után aránylag hamar ki is kerül a memóriából. Amennyiben a belső blokk alatti programterületen nem használjuk a b változót, nincs igazán szükség annak deklarációját a blokkon kívülre helyezni.

Minél belsőbb blokkban, minél később deklarálunk a változóinkat, annál gazdaságosabban bánunk a memóriával. A fordítóprogram képes optimalizációs lépések végrehajtására, felülbírálván a mi döntéseinket, de alapvetően arra támaszkodik, amit mi a kódban leírunk. Ezért elsősorban a programozó feladata, hogy a deklarációit a megfelelő helyre tervezze a forráskódban.

Valóság

A valóságban a fent leírtak nem feltétlenül működnék így (de működhetnének). A valóságban a memóriafoglalás az operációs rendszer megszólítását jelenti. A memóriaterületek feletti gazdálkodás az operációs rendszer privilégiuma. Csakis ő tudja, hány programot töltött már be a memóriába, melyik mennyi memóriát igényelt, és mely memóriaterületeket kötötte már le. Amikor egy program új memóriát igényel, azt el kell kérnie az operációs rendszertől.

Az operációs rendszer egy memóriakérést elég sokáig bírál el. Át kell tekintenie az eddigi foglalásokat, keresni kell egy megfelelő méretű szabad memóriaszakaszt, hogy meghatározhassa a kérésnek megfelelő legoptimálisabb terület kezdőcímét. Erre több algoritmus is létezik (best-fit, first-fit, ...). A foglalás tényét be kell jegyeznie a nyilvántartási listájára. Amennyiben nincs elég szabad hely a memóriában, bizonyos régen használt memóriaterületeket kiírhat diszkre az ún. lapozófájlba, és a helyét felszabadíthatja.

Amennyiben a futó programunk minden egyes int változónk deklarációjakor egyesével kérné el a 4-4 byte-okat, akkor a deklarációs sorok végrehajtása mindig nagyon lassú lenne. Az egész programunk igen lassan döcögne, a felhasználó ideges és türelmetlen lenne.

A valóságban a legtöbb fordítóprogram (amely alaposan ismeri az operációs rendszer fenti tulajdonságát) e helyett elemzi a kódot, és a változóinkat nagyobb blokkokba szervezi (összegzi a várható memóriaigényt). Egyszerre e miatt sok változó memóriaterületét foglalja le, egyetlen memóriafoglalás-hívást intézve az operációs rendszer felé. E miatt A fenti esetben az a és b változók valószínűleg egyszerre kerülnek be a memóriába, és egyszerre is kerülnek ki onnan.

Persze újra megkérdőjeleződik ekkor, hogy mi értelme is van a változóinkat belső blokkokban deklarálni, ha a valóságban mégsem takarékoskodunk a memóriaterületekkel.

Két dolgot kell megjegyeznünk:

  • nem csak ezen előnye van a belső blokkokban történő deklarációnak
  • van olyan eset, amikor mégis kivitelezhető az optimális megoldás

Nézzünk erre egy példát:

static void Main()
{
  int a; // itt kezdődik az 'a' név hatásköre
  ..
  ..
  {
    int b; // itt kezdődik a 'b' név hatásköre
    ..
  }  // b hatáskör vége
  .. 
  ..
  {
    int c; // itt kezdődik a 'c' név hatásköre
    ..
  }  // c hatáskör vége
  .. 
  ..
} // 'a' hatáskörének vége

A fenti esetben a belépés után 8 byte foglalódik le. Ebbe kerül be máris az a változó, amelyet a program első sorában deklarálunk. Az első blokkba belépve a még el nem használt 4 byte-ra kerül be a b változó, mely ott is marad amíg ki nem lépünk a blokkjából.

Amikor kilépünk a b blokkjából, az általa használt 4 byte a blokkosítás miatt nem szabadul ugyan fel, de egyelőre felhasználatlan marad. Amikor belépünk a c változó blokkjába, ő kerülhet be a korábban b által foglalt helyre.

A fenti esetben a memóriafoglalás 8 byte-ból megúszható. Ha mindhárom változót a blokkaikon kívül, a legkülső blokkban deklaráltuk volna, akkor a c deklaráció sorára lépéskor a b hatásköre még tartott volna, így nem kerülhetne a c az ő korábbi helyére a memóriában:

static void Main()
{
  int a; // itt kezdődik az 'a' név hatásköre
  ..
  ..
  int b; // itt kezdődik a 'b' név hatásköre
  {
    ..
  } 
  .. 
  ..
  int c; // itt kezdődik a 'c' név hatásköre
  {
    ..
  }
  .. 
  ..
} // 'a', 'b', és 'c' hatáskörének vége

Amennyire csak lehet, próbáljuk tehát a változóinkat minél később, minél belsőbb, beágyazott programblokkban deklarálni. Ezzel segíthetünk a fordítóprogramnak a memóriakihasználás optimalizálásában.

Memóriaterület újrahasznosítása

Amikor egy változóra nincs szükség, a memóriaterületét saját kódunk egy másik változójának adhatja ki a rendszer. Előfordulhat azonban az a szituáció is, hogy a terület felszabadítása után az operációs rendszer ezt egy másik program másik változójának adja oda. Jegyezzük meg, hogy a felszabadítással nem jár együtt a memóriaterület törlése is.

Igen érdekes bajba került e miatt a Windows NT 4.0 egyszer. Az operációs rendszerben tárolásra kerültek felhasználók nevei és jelszavai, melyet diszken tárolt egy erősen védett és titkosított, kódolt file-ban. Ez természetesen igen értékes információ volt, sok hacker próbálta feltörni ezt a védelmet.

Aztán kiderült egy érdekes hiba: mely szerint amikor egy felhasználó belépett a gépre, megadván felhasználói nevét és jelszavát, az NT a teljes listát betöltötte a memóriába, dekódolta, hogy eldönthesse a belépő ismer-e egy nevet és jelszót a listáról. A művelet befejeztekor a dekódolt táblázat memóriaterületét felszabadította.

Egy hacker olyan programot írt, amely a login után azonnal lefutott, és elkérte az operációs rendszertől az összes szabad memóriát. Ezek után már csak egy dolga volt - minden (mostmár általa birtokolt) byte-t át kellett néznie, hogy melyik terület rejti a dekódolt neveket és jelszavakat. Egy nagyon rövid és egyszerű programmal megtette azt, amit erős jelszófeltörő algoritmusokkal és kódokkal mások nem tudtak megtenni - ki tudta listázni a képernyőre a legális neveket és jelszavakat.

A fenti hibát publikussá válása után a Microsoft azonnal javította természetesen.

Amennyiben valamely változónkban érzékeny, értékes adatok vannak, a hatáskörből kilépése előtt írjuk felül valamilyen semleges értékkel. Így megakadályozhatjuk az információ kiszivárgását.


Hernyák Zoltán
A lap eredeti címe: „http://wiki.ektf.hu/wiki/Mp1/page140
Nézetek
nincs sb_44.223.31.148 cikk