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 bar
t
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:
- Ha átadok egy mutatót egy függvénynek, biztos lehetek, hogy nem fog a tartalma változni, és a fordítót is biztosítom erről: ennek megfelelően optimizálhatja a kódot
- Elegánsabban lehet vele konstanst definiálni, mint a
#define
direktívával: a konstansnak lesz típusa, és lehet címe (a prefix & operátorral)
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
- a függvénynek címét vesszük, és azzal hívjuk meg (elvileg közvetlen meghívásnál még mindig befűzheti a hívás helyett)
- a függvény rekurzív
- a függvény túl bonyolult vagy nagy
- fordító hóbortból
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*
vagyBicycle&
típusú paramétert vár, ott rendreCityBike*
ésCityBike&
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 afloat
vagy adouble
.
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:
private:
csak az osztály látjaprotected:
a bővíett osztályok (gyermekosztályok) is látjákpublic:
mindenki látja
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 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 stdin
rő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: | public | protected | private |
---|---|---|---|
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 Member
t 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 esetlegoperator=
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ó.