A Symbian C++ programozási nyelv

Kivétel- és memóriakezelés

Bevezetés

A telefonok memória- és tárkapacitása a számítógépéhez képest nagyságrendekkel kisebb, ahogyan a processzor teljesítménye is, ezért a Symbian-os szoftverfejlesztés egyik legfontosabb szempontja a kivétel- és memóriakezelés. A memóriaszivárgás kiküszöbölésére kifejezetten nagy gondot célszerű fordítani.

A Symbian-ban a hagyományos C++ kivételkezelés nem használható, ennek oka, hogy a Symbian (elődjének) fejlesztésekor a C++ fordítók még nem ismerték a kivételeket, a másik, hogy a C++ kivételek jóval több erőforrást és memóriát igényelnek, mint, ami megengedhető egy mobiltelefonon. Ezzel szemben a Symbian-os kivételkezelés során nem történik stackrewind, egy feltétel nélküli ugrással kerül a vezérlés a kezelési pontra, így sokkal hatékonyabban tudja kezelni a problémákat.

Leave

Hiba jelzésére a User könyvtár (statikus osztály) Leave metódusa szolgál. Azokat a függvényeket, amelyek a User::Leave metódust hívják leave-elő függvényeknek nevezzük. A User::Leave hívás logikailag megfelel a C++-ban megszokott throw-nak, mely az adott típusú kivételt dobja. Symbian-ban a kivétel típusa egy számot (hibakódot) jelent.

Például a new operátornak létezik egy felüldefiniált new (ELeave) metódusa, amely a memória elfogyása esetén leavel. Az L betűt a metódusok végére tehát nekünk kell odatennünk. Ez logikailag megfelel a Java-ban megszokott throws-nak vagy a C++-beli throw-nak. A különbség köztük azonban, hogy a throw deklarációját a fordítók ellenőrzik, míg a függvények végén az L betű helyes használatára nekünk magunknak kell figyelnünk, illetve egy külső eszköz, az ún. LeaveScan alkamazás segítségével ellenőrizhetjük, hogy kódunk megfelel-e a függvények leavel kapcsolatos konvenciójának.

void CTest::testFunctionL() { CSomeClass* obj1 = new (ELeave) CComeClass; CComeClass* obj2 = new (ELeave) CComeClass; ConstructL(); }

A Fenti kód esetén amenyiben elfogy a rendelkezésre álló memória az obj2 objektum példányosítása folyamán, a new(Eleave) használat miatt kivétel váltódik ki. Azonban az obj1 objektum destruktora nem hívódik meg, így az általa lefoglalt erőforrások nem fognak felszabadulni. Szükséges tehát a leave-elős függvényhívásokat a C++-ban megszokottakhoz hasonlóan kritikus szakaszokba foglalni.

Természetesen a C++-beli catch utasításának is létezik a Symbian-ban megfelelője: ezek a TRAP illetve TRAPD makrók. Leave-lés esetén a legközelebbi ilyen TRAP-hez kerül a vezérlés, egy változóba írva a hibakódot. Ha nem történt leave, akkor a hibakód KErrNone lesz. A TRAPD dekralálja a TInt típusú hibakezelő változót, míg a TRAP esetén egy általunk deklarált változót használhatunk.

TInt error; TRAP(error, fooFunctionL()); if (error!=KErrNone) //volt User::Leave hívás { // hiba jelzése, vagy egyéb kezelés }

CleanupStack

Ahogy azt korábban láttuk, a new operátorral (heap-en) létrehozott objektumoknak leavelés esetén nem hívódik meg a destruktora. Ez memóriaszivárgáshoz vezet. Ezért az így létrehozott objektumok mutatóit valahol el kell tárolni, hogy ha hiba történik, akkor elérhessük őket, és meghívhassuk a destruktoraikat. Ez, ahol tároljuk a CleanupStack lesz. Ahogy a neve is mutatja, ez egy Verem adatszerkezet, a megfelelő műveletek nevei is utalnak az általuk végzett tevékenységre.

A CleanupStack-re tehát valamilyen dinamikusan létrehozott erőforrások hivatkozásait tesszük fel. Ezek elsősorban C osztályú objektummutatók és R osztályú referenciaobjektumok. A felszabadítás menetét, a változó típusának megfelelően adhatjuk meg, a C osztályok esetén a CBase (kötelező) ősosztály által garantált virtuális destruktor, míg az R osztályok esetén, a speciálisan a CleanupStack kezelésére kialakított metódushívások (pl. CleanupClosePushL és a CleanupReleasePushL) használatával. További, az előbbi módszerek által nem megoldható, erőforrásfelszabadítást a TCleanupItem osztály segítségével valósíthatunk meg. Az osztály konstruktora két paramétert vár, egy tetszőleges típusú , az erőforrásra hivatkozó pointert (TAny*), illetve egy függvényreferenciát, melyben a felszabadítási mechanizmust definiálhatjuk.

Az alábbi példában különböző típusú objektumokat helyezünk a CleanupStack-re.

/*ha ptrl ősosztálya CBase ‚ a virtuális destruktor takarít.*/ Cleanupstack::PushL(ptr1); /*ha ptr2 Tany, nincs szükség takarításra, mert User::Free() is elég/* Cleanupstack::PushL(ptr2); /*ha ptr3 valamilyen R osztaly (vagy más), aminek a static void Close(TAny*) metódusa takarít*/ CleanupClosepushL(ptr3); /*ha ptr4 valamilyen R osztály (vagy más), aminek a static void Release(TAny*) metódusa takarít*/ CleanupReleasePushL (ptr4); /*ha ptr5 valamilyen nem C osztály, aminek a destruktora takarít*/ CleanupDeletePushL(ptr5); /*ha ptr6 TCleanupItem, vagy azzá konvertálható egy felüldefiniált operátorral, akor a TCleanupltem konstruktorában megadott metódus takarít/* Cleanupstack::PushL(ptr6);

Kétfázisú konstruktor

A CleanupStack segítségével sikerült felkészülni egy esetleges Leave hívás utáni takarításra. Viszont problémába ütközhetünk, ha az objektum lefoglalása után, de még a mutató CleanupStackre történő helyezése előtt történik a Leave hívás. Nézzük meg a következő kód-részletet:

TInt ValamilyenFuggvenyL() { CKulsoOsztaly * kulso = new (ELeave) CKulsoOsztaly(); /*itt lefoglalódik a hely egy CKulsoOsztaly típusnak, és meghívódik a konstruktor*/ Cleanupstack::PushL(kulso); /*...egyéb L műveletek végzése*/ Cleanupstack::PopAndDestroy(); /*vagy Cleanupstack::Pop(); ...néhány nem L-es művelet delete kulso;*/ }

Nem feltétlenül lesz probléma, a CKulsoOsztaly-nak nem sikerül helyet foglalni, akkor az új new operátorunk leave-el, ás minden rendben van; egyéb esetben pedig a Cleanupstackre kerül a külső mutató. Ha a PushL metódus leavel sem lesz gond. A PushL mindig előre lefoglal egy helyet egy mutatónak, ezért amikor leave-el, akkor ez az előre lefoglalás nem sikerül, és a korábbi PushL híváskor lefoglalt területre ekkorra már bekerült az argumentumban átadott mutató.

Vegyük azonban észre, hogy a PushL hívás előtt a CKulsoOsztaly konstruktora is lefut:

CKulsoOsztaly: :CkulsoOsztaly() //konstruktor { iTagValtozo=new(ELeave) CBelsoOsztaly(); /*ebbol meg problema lesz!*/ }

Ha viszont ekkor a CBelsoOsztaly számára nem sikerül helyet foglalni, akkor az eredmény egy félig létrehozott CKulsoOsztaly, amely számára le van foglalva a memória, és még sem került fel a CleanupStackre. Egyszerűen megfogalmazva: az a baj, hogy a konstruktor végére nem rakhatunk L betűt, pedig leave-el.

Tehát ezt nem szabad megengedni. A megoldás egy olyan kétlépéses konstruktor, ahol az egyszerű tagváltozók inicializálásának feladatát az „igazi” konstruktor végzi, míg a C osz-tálybeli tagváltozók helyfoglalását és az egyéb veszélyes feladatokat egy második metódus végzi.

Nézzünk most tehát egy helyes CKulsoOsztaly megvalósítást, azt feltételezve, hogy CBelsoOsztaly-nak nincs szüksége a kétfázisú konstrukcióra.

class CKulsoOsztaly public CBase { public: ~CKulsoOsztaly();//destruktor static CKulsoOsztaly* NewL(); //peldanyosito metodusok static CKulsoOsztaly NewLC(); protected: /*vedett konstruktor, kozvetlenul nem peldanyosithato az osztaly:*/ CKulsoOsztaly ();//elso fazis void ConstructL();//masodik fazis }; void CKulsoOsztaly::ConstructL() { /*ebbol mar nem lesz problema*/ iTagValtozo=new(ELeave) CBelsoOsztaly(); } CKulsoOsztaly* CkulsoOsztaly::NewLC() { CKulsoOsztaly* self = new (ELeave) CKulsoOsztaly(); /*konstruktor, egyszeru inicializalasok*/ CleanupStack::PushL(self); self->ConstructL();/*veszelyes inicializalasok, de ekkorra az objektum mar a Cleanupstacken van!*/ return self; } CKulsoOsztaly* CKulsoOsztaly::NewL() { CKulsoOsztaly* self = NewLC(); Cleanupstack::Pop(); return self; } TInt ValamilyenFuggvenyL() { CKulsoOsztaly* kulso = CKulsoOsztaly::NewLC(); /*most csak azért nem írhatunk NewL()-t, mert (habár a NewL() meghívja a NewLC-t) ha NewL()-t írnánk akkor a CKulsoOsztaly csak létrejönne, lefutna a konstruktora, meghívná a CBelsoOsztaly-t és nem csak felkerülne, hanem le is kerülne a CleanupStack-ről. Vagyis ha NewL()-t írunk akkor a további soroknak nincs értelme a CKulsoOsztaly szemszögéből. */ /*ketfazisu konstrukcio, az objektum a CleanupStacken maradt!*/ /*...egyeb L muveletek vegzese*/ CleanupStack::PopAndDestroy(); /*vagy CleanupStack::Pop();...nehany nem L-es muvelet delete kulso;*/ }

Ez a példa rendkívül fontos a CleanupStack és a kétfázisú konstrukció megértése szempontjából. Ezért nézzük meg, hogy mi is történik. A kiindulópont a ValamilyenFuggvenyL-ben a CKulsoOsztaly létrehozása. Ez a NewLC nevű statikus metódussal történik. Az LC végű függvények olyan allokálás jellegű műveletet végző függvények, amelyek a műveletsor végén a lefoglalt memóriaterületre mutató pointert a CleanupStackre helyezve hagyják. A különbség tehát a NewL és a NewLC között, hogy az első a kétfázisú létrehozás után (a ConstructL hívás miatt) a CleanupStackre tett objektumot onnan leveszi, míg a második rajta hagyja.

A statikus példányosító metódus először a new (ELeave) operátort használva létrehoz egy példányt a saját típusából. Mivel a konstruktor nem végez veszélyes műveletet, nem történhet leave-elés, csak ha nem sikerült helyet foglalni (ez esetben viszont nincs memóriaszivárgás sem. A következő lépés a CleanupStackre helyezni az újonnan létrehozott objektumot. Így már biztonságosan meghívható a ConstructL, mely a kétfázisú konstrukciót nem igénylő CBelsoOsztaly-t a new operátorral példányosítja (nem pedig CBelsoOsztaly::NewL-lel). Ha a CBelsoOsztaiy-nak nem sikerül helyet foglalni, a new operátor leave-el, és CleanupStackről levéve a CKulsoOsztaly megsemmisíthető. Ha nem történik hiba, akkor a NewLC visszatér, míg a NewL előbb leveszi a CleanupStackről az objektumot, és a kétfázisú konstrukció sikeresen befejeződik.

A CKulsoOsztaly példánya a CleanupStack::PopAndDestroy hívással megsemmisíthető, ha már nincs rá szükség, illetve ha nem leave-elő metódusait szeretnénk használni, akkor elég levenni (CleanupStack::Pop, és csak később megsemmisíteni (delete).

A fenti példa jól megjegyezendő, mert a konstruktorok esetleges paramétereitől eltekintve a NewL és NewLC metódusok implementációja gyakorlatilag mindig ugyanez.

Még egy fontos dolog maradt hátra. Bizonyára feltűnt már, hogy a CKulsoOsztaly destruktora akkor is meghívódhat, ha az iTagValtozo inicializálása nem sikerült. A destruktornak tehát oda kell figyelnie, hogy esetleg félig inicializált objektumot kell megszüntetnie. Mivel CKulsoOsztaly CBase-ből származik, az összes tagváltozója 0-val van inicializálva, így az iTagValtozo is. Ez a cBase viselkedés megszabadít minket sok felesleges inicializálás kiírásától, és ebben az esetben a destruktorban sem kell különösebb változtatásokat végezni, hiszen a C++ delete operátora bátran meghívható egy 0-t tartalmazó mutatóra is. Ha viszont olyan tagváltozóról van szó, amit néha megszüntetünk, néha pedig újrafoglalunk, akkor fordítsunk figyelmet a felszabadítás után a mutató 0-ra állítására. Általános szabályként szűrhetjük le tehát: ha a tagváltozónkra a destruktoron kívül memória-felszabadító műveletet végzünk, mindig állítsuk be utána az értékét 0-ra.