1. Fordítás
  2. Új nyelvi elemek
  3. Objektum-orientált fejlesztés
  4. Osztályok C++-ban
  5. Statikus deklarációk és névterek
  6. C++ I/O
  7. Operátor túlterhelés
  8. Osztály bővítés C++-ban
  9. Kivételkezelés
  10. Sablonok
  11. A jegyzetről

Fordítás

A futtatható állomány előállítása három lépésből áll: preprocesszálás → fordítás → összeállítás (preprocessor → compiler → linker).

Preprocesszálás

A preprocesszálás végeredménye ritkán jelenik meg külön állományként, a fordítás részeként fut le.



Ha azt akarjuk, hogy a fordító csak preprocesszáljon, akkor a -E kapcsolót kell használnunk gcc vagy clang esetén. Ekkor a stdoutra kiírja azt, ami egyébként a fordítónak ment volna. Ha a kimenetet megnézzük, az include egyszerűen csak beilleszti a megadott állományt a forrásba, ezen kívül belekerül pár különös jel, ezekkel tud a fordító a hibakijelzésnél az eredeti források soraira hivatkozni.


Fordítás

A fordítás során minden egyes .cc/.cpp forrásból objektum állomány (.o/.obj) készül. Az objektum állomány tartalmazza, a változók neveit, a függvények neveit és gépi kódú forrását, valamint hogy ezek a függvények hol hivatkoznak meg változókat és más függvényeket. Itt még nincs eldöntve, hogy a memóriában hol fognak az egyes függvény kódok és globális változók elhelyezkedni.

A külső nevek típusának az ellenőrzése a fordító feladata, de ahhoz, hogy ezt megtehesse, tudnia kell róla.



Ha azt akarjuk, hogy csak a preprocesszáló és a fordító fusson le, használjuk a -c kapcsolót.


A máshol definiált függvényeket és globális változókat deklarálni minden egyes forrásban, ahol használni akarjuk komoly hibaforrás és rengeteg pluszmunka, ezért szokás egy .h forrásba beletenni mindazon változókat és függvényeket, amit egy másik modul számára elérhetővé akarunk tenni. Ezt teszi minden egyes rendszerkönyvtár.





Előfordulhat, hogy a util.h-ban definiált Record típust használja több modul is, pl. a függvények paraméterlistájában. Elkerülhetetlen, hogy emiatt közvetve többször is befűződjön a util.h. A típust újradeklarálni viszont nem lehet.



Ezért szokás a fejléceket makrókkal körülbástyázni, hogy ha újra megjelennének egy fordítási egységben, akkor lényegében nem tartalmaznak semmit.


A gyakorlatban minden népszerű fordító támogatja a nem szabványos #pragma once direktívát, amivel ugyanezt lehet egyetlen plusz sorral megvalósítani. A vizsgán ne használd.

Minden olyan függvényt, amit nem használunk modulon kívüli használatra érdemes úgy deklarálnunk, hogy azt más modulok még véletlenül se érhessék el, hogy egy nagyobb program esetén ne érjenek kellemetlen meglepetések. C-ben ezt a static kulcsszó használatával lehet elérni, C++-ban lehetőség van static kulcsszót használni vagy névtelen névteret létrehozni (a névterekről később).




A példában a foo változót megtalálta, de a bart nem.

Összeállítás

A linker feladata, hogy eldöntse, hogy futás során hol fognak elhelyezkedni a memóriában a globális változók és a függvények gépi kódja, és hogy ez utóbbiak a már megállapított címen keressék őket.

Nagyon fontos tudni, hogy a linkerben a C++ már díszített (decorated) neveket használ, ellentétben a C-vel, mert C++-ban már nem elég pusztán a függvény nevét tudni a beazonosításhoz.




A C++ kód nem látta azt a C függvényt, amelyiket nem extern "C" kulcsszóval deklaráltunk, illetve a C kód nem látta azt a C++ függvényt, amit nem ezzel a kulcsszóval definiáltunk.

Az extern "C" lényegében azt mondja: „erre a deklarációra vagy definícióra ne használj díszített neveket!” Van extern "C" { /* ... */ } formája is, amivel egyszerre több deklarációra vagy definícióra lehet ezt a módot beállítani. Elsősorban C rutinkönyvtárak fejléceiben találkozni vele:

#ifdef __cplusplus
extern "C" {
#endif

/* ... */

#ifdef __cplusplus
}
#endif

Új nyelvi elemek

A const kulcsszó

Előnyei:



Egyszerűbb függvény deklaráció

C-ben a int main() int main(...)-nak felelt meg, a paraméter nélküli függvényeket int main(void)-ként kellett deklarálni. C++-ban ez már nem érvényes, a int main() ekvivalens a int main(void) deklarációval.

Változó deklaráció bárhol

Bárhol lehet változót deklarálni, nem csak a blokkok elején és a for ciklusokban.


Függvények túlterhelése

A függvényeket már nem csak a nevük, hanem a paraméterlistájuk is azonosítja, amíg egyértelmű.



Többek között emiatt van szükség az extern "C" módosítóra.

Függvény paraméter alapértelmezett értékek

A függvény utolsó paramétereinek adhatunk alapértelmezett értéket, ezzel azok a paraméterek elhagyhatóvá válnak. Paramétereket elhagyni csak a függvény végéről lehet, nem lehet pl. a harmadikat elhagyni, a negyediket viszont megadni.



Referenciák

Mutatók mellett referenciákat is használhatunk. A háttérben ezek is mutatók, de nem tudjuk a mutató értékét megváltoztatni, csak azt az értéket, amire mutat.

Ha a referenciát const kulcsszóval deklaráljuk, a fordító képes automatikusan ideiglenes változót létrehozni, ha kell (lásd a példát). Ha azonban a visszatérési érték referencia, akkor a biztonságos működéshez csak olyan referenciát adhatunk vissza, ami nincs a függvény élettartamához kötve. Ha mégis megtesszük, a program még le is fordulhat, de nem várt eredményeket produkálhat.



inline függvények

A #define direktívával létrehozott makró-függvényeknek kellemetlen mellékhatásai lehetnek:

#define MUL(a, b) a*b
// ...
#define SIX 1+5
#define NINE 8+1
int val = MUL(SIX, NINE);
// val erteke 42 (nem 54)

Zárójelezéssel javíthatunk rajta, a deklaráció nem lett szebb, és még így is problémáink akadhatnak:

#define MAX(a, b) ((a)>(b)?(a):(b))
// ...
int a = 6;
int b = 5;
int val = MAX(++a, b);
// a es val erteke mar 8

Ezen kívül ha makrókat használunk, nincsenek típusdeklarációk, nem lehet használni a túlterhelést, az alapértelmezett értékeket, és egyebeket, amikről eddig nem volt szó. De szeretnénk, ha nem hajtana végre külön függvényhívást, ha a fordító is úgy gondolja, hogy ez kézenfekvő.

inline int max(int a, int b)
{
    return a > b ? a : b;
}
// ...
int a = 6;
int b = 5;
int val = max(++a, b);
// a es val erteke 7

A fordító nem veszi figyelembe az inline kulcsszót, ha

Implicit typedef struktúrákra

A struktúrákra nem kell ezentúl struct StrukturaNev formában hivatkozni, simán StrukturaNev is megteszi.

struct Rect {
    int x, y;
    int width, height;
};

// OK
struct Rect rect1;

// C++-ban mar ez is OK
Rect rect2;

Objektum-orientált fejlesztés

Az OOP lényege, hogy objektumokkal dolgozunk. Objektum lehet pl. egy komplex szám, egy dinamikus tömb vagy egy gomb a képernyőn. Az objektumokon műveleteket lehet végezni. Az objektumok négy fontos elvet követnek:

Egységbezárás (encapsulation)
Az objektum és a műveletei olyan formában jelennek meg a nyelvben, hogy szintaktikailag egységet alkotnak. Ez a gyakorlatban azt jelenti, hogy az osztályoknak (és a struktúráknak) tagfüggvényei lehetnek, amik megvalósítják a műveleteket.
Adatrejtés (information hiding)
Az objektum belső állapota nem érhető el közvetlenül. Pl. a dinamikus tömb hosszához nem férhet hozzá mezőnhivatkozáson keresztül, csak művelettel kérheti le és állíthatja, így a dinamikus tömb megvalósíthatja az automatikus újraallokálást.
Öröklés (inheritance)
Az objektumok osztálya van, az osztályok pedig más osztályokat bővíthetnek („örökölhetnek” – nem szeretem ezt a szóhasználatot, helyette rendre a „bővített” szót fogom használni). Pl. a Bicycle egy bicikli adatait tárolja el (pl. kerékátmérő és sebességek száma). Viszont biciklitől függően lehet, hogy további adatokat akarunk megadni, pl. városi kerékpár (CityBike) esetén a kosár űrtartalmát. Nem kell újra megírnunk azt a függvényt, ami a kerék fordulatszámából (a kerékátmérő ismeretében) a bicikli sebességét kiszámolja, hiszen az ugyanaz a városi kerékpárra, mint bármely más kerékpárra. A leszármazott osztályok új mezőkkel és függvényekkel bővíthetik az ősosztályt, valamint az arra megjelölt függvényeket akár le is cserélhetik.
Behelyettesíthetőség
Az osztályok bővítései behelyettesíthetővé válnak oda, ahol a bővített osztályba („szülőosztályba”) tartozó objektum szerepelhet. Pl. ha egy függvény Bicycle* vagy Bicycle& típusú paramétert vár, ott rendre CityBike* és CityBike& típusú érték is megadható.
Típustámogatás (extra, csak C++-ra és egy pár más nyelvre igaz)
Az osztályok ugyanúgy működhetnek, mint a beépített típusok. Vagyis a Complex osztály ugyanúgy működhet, mint a float vagy a double.

Osztályok C++-ban


A class abban különbözik a struct-tól, hogy az alapértelmezett láthatóság private. Háromféle láthatósági szint létezik:




Figyeljük meg, hogy a destruktorokat fordított sorrendben hívja meg, amikor az objektum kikerül a scope-ból.

Nagyon fontos, hogy a konstruktorokat nem hívjuk meg sehol. A fordító nem fog szólni miatta, de ettől helytelen gyakorlat. Inkább hozzunk létre egy függvényt, amit a konstruktor és a konstruktort hívó függvény meghív.

A destruktort sem hívjuk meg kézzel.

A Point osztálynak nincs paraméter nélküli, azaz alapértelmezett konstruktora. Ezért az alábbi nem fordul:



A new és a delete kulcsszó

A C++ a memóriaallokációt nyelvi szinten tartalmazza. Allokálni a new operátorral lehet, felszabadítani a delete illetve a delete[] operátorral. Az utóbbit abban az esetben kell alkalmazni, ha tömböt szeretnénk felszabadítani. A C-vel ellentétben a memóriaallokáció mindig típusos:


Fontos megjegyezni, hogy az így létrehozott objektumok destruktorait nem a scope-ból való kikerülésükkor, hanem a delete/delete[] meghívásakor hívja meg automatikusan.

A malloc hívással ellentétben ez meghívja a konstruktort is, még akkor is, ha tömböt allokálunk:




Jól látható, hogy a tömböt visszafelé szabadítja fel:


A másoló konstruktor

A Point osztályunknak van egy rejtett konstruktora. A tagfüggvény szignatúrája valahogy így néz ki:

Point::Point(const Point &point);

Ezt a fordító automatikusan generálta: egyszerűen minden mezőt lemásol.


A változódeklarációnál leírt = nem ugyanaz, mint az = művelet.

A másoló konstruktort testre is szabhatjuk, és ez néha szükséges is. Az alábbi StringBuffer osztály egy karaktertömböt allokál. Ha nem bíráljuk felül a másoló konstruktort, akkor egy az egyben átveszi a mutatót a másik objektumból, és amelyiknek a destruktorát másodjára hívja meg, az már felszabadított területre próbálja meghívni a delete-et.



A this mindig az aktuális objektumra mutat. Akkor használjuk, ha az aktuális objektumunkra kell egy mutató (meglepő módon), vagy egy olyan mezőt akarunk elérni, aminek a nevét elfedi egy lokális változó. Vagy azért, mert ilyen hülye szokásunk van.

Statikus deklarációk és névterek

A static kulcsszó függvényekben

A static kulcsszó C-ben és C++-ban egyaránt rendelkezésre áll függvényeken belül is:

void foo()
{
    static int count = 0;
    printf("%d\n", ++count);
}

Az ilyen statikus változók inicializálója csak egyszer hívódik meg, és hívásról hívásra megőrzi az értékét, pont mint egy globális változó. A globális változóval ellentétben viszont nem érhető el, csak a függvényen belülről.

A static kulcsszó osztályokban

Az osztályhoz kötött statikus mezők és függvények szintén globális változóként működnek, azaz nem kötöttek objektum példányhoz, a láthatóságuk pedig a public/protected/private elérési sémákkal szabályozhatók.



Figyeljük meg, hogy a statikus mező deklaráció egy extern változódeklarációval egyenértékű, vagyis a változót még külön egyszer deklarálni kell, ahol egyébként a kezdőérték is megadható. Megosztott komponenseknél ezt az osztály implementációját tartalmazó forrásba célszerű tenni.

A friend kulcsszó

A friend kulcsszóval hozzáférést adhatunk az osztály protected és private mezőihez más függvények és osztályok számára (ez esetben az osztály valamennyi tagfüggvénye hozzáférhet a mezőkhöz). A friend deklarációt az osztályon belül bárhova írhatjuk, minden esetben ugyanazt jelenti.


Névterek

Más nyelvekben, ahol nincs lehetőség globális változókat vagy függvényeket deklarálni, előfordul, hogy az összetartozó konstansokat, függvényeket vagy globális változókat egy osztályban deklarálják statikus elemekként. Ami szép, mert az összetartozó elemeket egybefogja, és csúnya, mert mindezt osztályokkal teszi. C++-ban erre nincs szükség: használhatjuk a namespace kulcsszót.



Figyeljük meg, hogy a show függvényben (illetve bármely deklarációban a névteren belül) a névtér saját változóit részesíti előnyben. A névtér nélküli globális változókat elérni a négyesponttal tudja. A négyesponttal kezdődő minősített neveknél a névtereket teljesen kézzel adjuk meg. Az alábbi példa – bár nem szép – demonstrálja, hol lehet ez szükséges:



C++-ban minden standard C könyvtárnak megtalálható a C++ verziója, ami annyiban különbözik, hogy a rendszerhívások belekerülnek a std névtérbe. A header nevét a „.h” elhagyásával és a „c” előtag hozzáadásával kapjuk meg.

Globális statikus változók és függvények (vagyis amik csak az adott fordítási egységben láthatók) üres névtérrel is deklarálhatók:

namespace
{
    // mas forditasi egysegbol nem lathato,
    // pont mint a static kulcsszoval
    int lokalis = 1;
}

Névterekben található változók és függvények semmilyen formában nem oszthatóak meg C modulokkal.

A using kulcsszó

A using kulcsszó használatával egyszerűsíthetjük a névterekben lévő elemek elérését. A deklaráció mindig a fordítási egységre vonatkozik, vagyis header esetén minden C++ forrásra érvényes lesz, amelyik a headert befűzi, ezért headerbe ilyet írni nem szép dolog. A using önmagában egyetlen nevet tesz elérhetővé a névtér megadása nélkül. A using namespace a névtér valamennyi elemét elérhetővé teszi a névtérmegjelölés (kvalifikáció) nélkül.


C++ I/O

Folyamok (streamek)

Ahogyan a C-s szöveges I/O legtöbbet használt része a formátum stirng, úgy a C++ I/O alapja a stream. A stdout C++-ban a std::cout, a stderr pedig a std::cerr.

Az elmaradhatatlan „Hello World!” alkalmazás:


A std::endl azon kívül, hogy kiír egy soremelés karaktert, üríti a buffert is: a karaktereket ugyanis a rendszer nem egyenként szokta a rendszer a képernyőre írni, hanem kötegelve (ez C-ben sem volt másképp, legfeljebb nem tudtatok róla :D).

A std::cout-hoz hasonlóan van std::cin is:


A program beolvassa a radius változóba a stdinről egy tört értéket, majd kiszámolja a kör kerületét és kiírja.

Figyeljük meg, hogy a shift operátor nyilai a kimenetnél a kimenet felé mutatnak, a bemenetnél pedig onnan indulnak ki (hogy miért tud a shift operátor ilyen különösen viselkedni, arról később lesz szó).

Írás állományokba

Az ifstream és az ofstream osztályokkal történik. Szöveges formázott elérés:




Egyéb elérési módok (itt most ifstream és ofstream helyett a szülőosztályt, az istreamet és az ostreamet használom):



Mezők formázása



Operátor túlterhelés

Operátorokat definiálhatunk olyan típusokra, amikre egyébként nincsenek. Nem változtathatjuk meg az eredeti működést (vagy legalábbis nagyon nem javaslom), nem változtathatjuk meg a precedenciát, nem hozhatunk létre teljesen új operátorokat (pl. **)

Az operátorok legalább egyik tagja osztály kell, hogy legyen.



Figyeljük meg, milyen sorrendben értékeli ki őket:

 
 

Kérdés még, hogy hogyan különböztetjük meg a prefix és a posztfix ++ operátort.

class A
{
    int val;
public:
    //
    // ...
    //
    
    // ++a
    A& operator++()
    {
        ++val;
        return *this;
    }
    
    // a++
    A operator++(int)
    {
        // elmentjuk az eredeti erteket
        A result(*this);
        ++val;
        return result;
    }
    
    //
    // ...
    //
};

A posztfix ++ és -- operátorhoz meg kell követelnünk egy extra int paramétert, amit nem használunk fel.

Típuskonverzió, mint operátor



Figyeljük meg, hogy a típuskonverziós operátoroknál hiányzik a visszatérési érték típusa, mert azt az operator kulcsszó után adjuk meg.

A függvényhívás operátor az egyetlen, amely tetszőleges számú paramétert vehet fel. A példában nem ez szerepel, de pl. egy 0 paraméteres függvényhívás operátort double operator()() szignatúrával tagfüggvényként, vagy double operator()(Poly3 &p) kívülről.



Az következő operátorok nem terhelhetőek túl: . :: ?: sizeof. Minden más igen, beleértve néhány különös megoldást is:



Osztály bővítés C++-ban

Nézzünk először egy viszonylag egyszerű példát: dátum és idő. Az idő napot is megjelöl, a dátum viszont csak napot:


A szülőosztály lehet rejtett, ilyenkor a következőképpen alakul a mezőinek a láthatósága az új osztályban:

Szülőosztály:publicprotectedprivate
Mező:
public public protected private
protected protected protected private
private nem érhető el nem érhető el nem érhető el

A táblázat alapján elmondhatjuk, hogy a szülőosztály láthatósági megszorítása egy „láthatósági plafont” ad.

Virtuális metódusok

Az új osztály a bővítendő osztály függvényei helyett újakat definiálhat, de ebből a szempontból meg kell különböztetnünk a virtuális és a hagyományos függvényeket:







A virtuális függvény azt jelenti, hogy az osztályhoz tartozni fog egy virtuális függvény tábla, amely az osztály virtuális függvényeinek címét tartalmazza. Az objektumpéldány tartalmaz egy mutatót az osztály virtuális függvény táblájára. Normális esetben a tagfüggvényeket úgy hívja meg, hogy annak a címét fordítási időben a linker írja be. Virtuális függvény esetében viszont az objektum egy rejtett mezőjéből kiolvassa a virtuális függvény tábla címét, majd a tábla megfelelő elemét, amely tartalmazni fogja a meghívott függvény címét.

Így amikor Person&-ként hivatkoztunk az Employee objektumra, a print() függvény meghívásánál a Person osztály print() függvényét drótotza be még a linker, viszont a printVirtual() esetén a referencia mögötti objektum osztályától függ, hogy melyik függvényt hívja meg.

Ha valamelyik szülőosztályban egy függvény virtuálisként lett megjelölve, akkor annak minden gyermekosztálybeli megfelelője implicit módon virtuális lesz, akár kitesszük a virtual kulcsszót, akár nem. A kód megértését segíti, ha kitesszük.

Mivel a virtuális függvény alapvető tulajdonsága, hogy címe van, inline nem lehet.

Virtuális destruktor

A destruktor is lehet virtuális. Azt mondják ökölszabálynak: ha az osztálynak van legalább egy virtuális metódusa, akkor a destruktornak is virtuálisnak kell lennie. A valóság ennél árnyltabb:



Figyeljük meg, hogy az első esetet leszámítva – helyesen – mindkét destruktor lefutott. Az első esetben a delete művelet során azonban, megnézte, mivel van dolga: SDA*, meghívta a destruktorát, és felszabadította a területet. Az utolsó esetben megnézte, mivel van dolga: VDA*, meghívta a destruktorát, és felszabadította a területet. Volt azonban egy fontos különbség: az előbbi esetben kifejezetten az SDA::~SDA() destruktort hívta meg, míg az utóbbi esetben a meghívandó függvény címét a virtuális metódustáblából nézte ki, így végül a VDB::~VDB() függvény lefuttatására került sor.

Absztrakt osztályok és metódusok

A virtuális függvények lehetnek absztrakt metódusok, ami azt jelenti, hogy az adott osztályban nem tartozik hozzá implementáció.



Ha az osztálynak van absztrakt függvénye, nem példányosítható:


Többszörös öröklés

A Stingifiable megfelelő azoknak az osztályoknak, amelyek értelmesen szöveggé alakíthatóak, de mi van azokkal, amik egész számmá is alakíthatóak? A felvázolt Number osztálynál egyértelmű, mit kell tenni, de egy komplex típusnál vehetnénk pl. a valós rész egészrészét. De mindkettőről tudjuk, hogy kiválóan átalakíthatóak szöveggé is. Itt jön be a többszörös öröklés:


A többszörös öröklés nem csak absztrakt szülőosztályokra vonatkozik, teljes értékű osztályok is lehetnek szülőosztályok többszörös öröklésnél is.

Virtuális szülőosztályok

Nézzünk egy másik példát, ahol egy klub csoportvezetőit és csoporttagjait tartjuk számon. Azonban a klub vezetőjét leszámítva mindenki tagja a csoportvezetők csoportjának, vagyis egyszerre csoporttagok és csoportvezetők.


Első ránézésre jónak néz ki. Próbáljuk ki:



A fordítás sajnos elszállt. Nézzük meg, hogyan néznek ki az egyes osztályok példányai a memóriában, de először csak egy egyszerű példára, a már felvázolt Date-re:

Date
int year;
int month;
int day;
DateTime
int year;
int month;
int day;
int hour;
int minute;
int second;

Ha egy DateTime típusú objektumra úgy tekintünk, mint egy Date típusúra, akkor azt látjuk, amire számítunk: évet, hónapot, napot, egy-egy int-ként, egymás után.

Nézzük az egyszerű, virtuális metódusos öröklést a Person és az Employee osztályokra:

Person
void *vtable;
std::string name;
Date birthDate;
Employee
void *vtable;
std::string name;
Date birthDate;
Date hiringDate;

A vtable természetesen a virtuális metódustábla mutató, minden osztály példányaiban az osztály (nem az objektum: az pazarlás lenne) saját táblájára mutat.

A többszörös öröklés tényleges megvalósítása fordítófüggő, ezért nem részletezzük. A lényeg, hogy a TeamLeader és a TeamMember egyenként már tartalmazza a Member mezőit, és ezzel a bővítési mechanizmussal nincs esély arra, hogy a MiddleTeamLeader szülőosztálybeli függvényei megfelelő objektumot lássanak, miközben nincs két Member szülő beágyazva.

A megoldás a virtuális szülőosztály. A segítségével a TeamLeader és a TeamMember „részei” a MiddleTeamLeader osztálynak közös Membert látnak.




Adatmodellezési elvek

Ha a gyakorlatban virtuális ősosztályokat kell használnod, esélyes, hogy valamit nagyon rosszul modelleztél le.

A gyakorlatban fontos érteni a különbséget az „X egy Y” és az „X része Y” kijelentés között, mert nem lesz mindig egyértelmű. Az első öröklést (a téglalap egy síkidom, van pl. területe), a második objektum kompozíciót jelent (pl. a téglalap egy pontból és egy méretből áll, a téglalap viszont nem pont és nem méret).

Típuskonverziók

Konverziók megvalósítása

Mindenről volt már szó, összefoglalva:



Típuskonverziós operátorok

Nézzük a klasszikus C-ben megismert típuskonverziót:


Egy szó nélkül lefordítja:


Ez nem probléma, ha tudja az ember, hogy mit csinál, de jó lenne, ha meg tudnánk tenni ezt úgy is, hogy figyelmeztessen a rendszer:



Nem engedi, mert ez két teljesen különböző osztály.



Sajnos a static_cast engedi a bővítést is, cserébe viszont gyors, és nem dobja meg a program méretét:



A megoldás erre az esetre a dynamic_cast lenne, amelyhez be kell kapcsolni a fordítóban a futási idejű típusinformációt (RTTI, run-time type information). A példám sajnos nem úgy fut le, mint amit vártam: elvileg null pointert kellene visszaadnia, vagy kivételt dobnia (erről később), ehhez képest egyik sem történik meg: „Segmentation fault” üzenetet kapok.


A dynamic_cast használata kerülendő, mert lassú, és megdobja a program méretét.

A const_cast segítségével a const kulcsszótól szabadulhatunk meg. Használjuk módjával, csak nagyon indokolt esetben.


Megmaradt a reinterpret_cast, ami bármilyen mutatót bármilyen mutatóra vált kérdés és megfontolás nélkül, valamint akár int típusú értéket is hajlandó mutatóként értelmezni (ha elég széles a platformon: 64 bites fordítás esetén már nem int, hanem long long szélességű a mutató).



Kivételkezelés

A hibakezelésre C-ben nincs igazán jó megoldás. Feláldozhatjuk a függvények visszatérési értékét, és visszatérhetnek hibakóddal, de nagyon megbonyolítja a kódot:

int calculate(Value **result)
{
    // f1, f2, f3 valamit kiszamol, ami el is szallhat
    // 0-val valo visszateres a siker
    // ossze akarjuk vonni oket
    *result = new Value();
    Value *merged = *result;
    Value value;
    int error;
    error = f1(&value);
    if(error)
    {
        delete *result;
        *result = 0;
        return error;
    }
    
    merged->merge(value);
    
    error = f2(&value);
    if(error)
    {
        delete *result;
        *result = 0;
        return error;
    }
    
    merged->merge(value);
    
    error = f3(&value);
    if(error)
    {
        delete *result;
        *result = 0;
        return error;
    }
    
    merged->merge(value);
    
    return 0;
}

Erre alkották meg a kivételkezelést. Először maradjunk meg csak a hibakódoknál, de most már ne áldozzuk fel a visszatérési értéket.

Value* calculate()
{
    Value *result = new Value();
    
    try
    {
        result->merge(f1());
        result->merge(f2());
        result->merge(f3());
    }
    catch(int error)
    {
        delete result;
        throw error;
    }
    
    return result;
}

Sokkal letisztultabb, átláthatóbb és egyszerűbb. A throw segítségével dobhatunk valamilyen értéket, amit a try catch ágával kaphatunk el. Nem kell feláldoznunk a függvény visszatérési értékét sem.

Dobni bármilyen típust lehet, minden típusra írhatunk külön catch ágat. Az ágak közül mindig a legelső for lefutni, ami illeszkedik az adott értékre, vagyis ha el szeretnénk kapni szülő- és gyermekosztályhoz is tartozó példányt, akkor az utóbbit kell előre írni, egyébként a szülőosztály példányát elkapó ág fog lefutni mindig.

Kérdés még, hogy el lehet-e kapni azt, amiről nem tudjuk, hogy micsoda, vagyis „minden mást.”



A dobott típusok megjelölése

A függvény szignatúrában megjelölhetjük, hogy milyen típusú értékeket dobhat a függvény. Ez nem jelenti azt, hogy a belső, jelöletlen függvények nem dobhatnak mást, vagy nagyon csúnya vége lesz:



A helyzet akkor is ugyanez, ha nem közvetlenül az f() függvény dobja a kivételt, hanem valamelyik másik, amelyik nem is deklarálta a dobott típusokat.

Persze tekinthetjük égbekiáltó hibának, ha olyan kivételt dobnak, amire nem vagyunk egyáltalán felkészülve. Ez néhány esettől eltekintve követendő gyakorlatnak is tekinthető. Ha mégis szeretnénk kultúráltan, egy catch ágban kezelni, akkor használhatunk egy egyéni unexpected függvényt és a szabvány C++ könyvtár std::bad_exception osztályát:



A szabvány C++ könyvtár kivétel osztályai

A szabvány C++ könyvtár deklarál pár kivétel osztályt. Nézzük ezeket header szerint.

<exception>

exception

A szabvány C++ kivételek ősosztálya. Természetesen ebből származtatva saját kivétel osztályokat is létrehozhatunk. Ha ilyet kapunk el, az alábbi függvényt használhatjuk, vagy ha saját osztályt hozunk létre, akkor az alábbi függvényt érdemes felülcsapni:

virtual const char* what() const throw()

Amíg ezen a szinten kapjuk el a kivételeket, ez a legtöbb, amit kiszedhetünk belőle: egy stringet. Természetesen ha saját osztályt készítünk, akkor további paraméterek átadására is lehetőség van.

Ha saját osztályt készítünk a what() függvény felülcsapásán kívül érdemes copy konstruktort (ha a sima másolás nem felel meg) és esetleg operator= függvényt írnunk.

bad_exception
Ezt már ismerjük.

<new>

bad_alloc
A new operátor dobja, ha nem sikerül az allokációt végrehajtania.

<stdexcept>

runtime_error
Csak futás közben észrevehető hibák esetén dobja, pl. mert nem megfelelő értékek jöttek ki (pl. nullával való osztás). A gyermekosztályai: range_error, overflow_error, underflow_error.
logic_error
A program logikában történő hibák esetén dobja. A gyermekosztályai: invalid_argument, out_of_range, length_error, domain_error.

Sablonok

A konstansoknál és az inline függvényeknél megpróbáltunk megszabadulni a preprocesszoros megoldásoktól. Van azonban még egy terület, amit ezek nem képesek lefedni, amit a preprocesszoros direktíva igen: a típusfüggetlenség.

Vegyük az alábbi kódrészletet:

#define MIN(a, b) ((a) < (b) ? (a) : (b))

Ez ugyanúgy fog működni int-re, float-ra, long long-ra, vagy bármilyen osztályra, amely az operator<-t definiálja. Erre megoldást nyújtanak a C++ sablonok:



A sablon paramétereket a fordító kikövetkeztetheti, adhatunk alapértelmezett paramétereket (a következőben példában látható lesz), de megadhatjuk a paraméterek értékét közvetlenül is. Ez néhány esetben szükséges is lehet, ha pl. csak a függvény visszatérési értékének típusa függ a paramétertől, a fordító nemigen tudja kikövetkeztetni ilyen esetben.

Nagyon fontos tudni, hogy a sablon függvények nem igazi függvények, önmagukban nem fordíthatóak. Ha sablonokat megosztva akarunk használni, teljes egészében a header állományba kell írni.

A sablonok megalkotásakor először a class kulcsszót használták a típusok megjelöléséhez, hogy ne kelljen új kulcsszót bevezetni, de végül a szabványosítás során az egyértelműbb typename szó mellett döntöttek (amit egyéb okokból egyébként is be kellett vezetni), a class ilyen felhasználását viszont a kompatibilitás megőrzése végett meghagyták.

Nézzük meg, hogyan néz ki egy sablon osztály deklarációja:



Látható, hogy a függvény implementációt is sablonként kell definiálnunk, ami elég körülményessé teszi a leírását, merő tömörségből és átláthatóságból a konstruktort és az allocate metódust leszámítva az összes függvény definíciót az osztályba írtam.

A szabvány sablon könyvtár

A standard template library (STL) sok hasznos generikus adatszerkezetet tartalmaz, hogy ezeknek a sziszifuszi lekódolásával már ne kelljen foglalkozni.

Legyen az a feladat, hogy egy szöveges állományban kell megszámolnunk, hogy melyik szó hányszor szerepel és sorrendben kiírni úgy, hogy az első helyen a legritkább szó szerepeljen, a végére pedig a leggyakoribb szó kerüljön. Fárasztó munka lenne a nulláról megírni, de az STL segítségével könnyedén megoldható:


A donut.txt bemenetre esetén a válasza az stl.txt-ben található.