Az elõzõ részekben egy összetett grafikus programrendszer lehetséges alaptípusainak példáján mutattuk be az OOP egyes jellemzõit. A point típus a location-ból lett származtatva, ugyanígy point-ból létrehozhatunk egy vonalakat leíró osztályt, amelyet például line-nak nevezhetünk, és mondjuk line-ból származtathatunk mindenféle poligont. A poligonokra két vázlatos példát mutattunk (triangle, rectangle), ezekbõl felépítve pedig már egy "valóságos" objektumok leírására való típus, a house vázlatos leírását adtuk. (Ezek a példák természetesen nem egy mûködõ program létrehozására, hanem sokkal inkább egy-egy OOP fogalom megvilágítására voltak kihegyezve.) Az eddigi példák többségének ugyanakkor volt egy közös vonása, nevezetesen az, hogy deklaráltunk bennük egy-egy show azonosítójú függvénymezõt azzal a céllal, hogy az az adott típusú objektumot a képernyõn megjelenítse. Az alapvetõ különbség az általunk elképzelt objektumtípusok között az, hogy milyen módon kell õket a képernyõre kirajzolni. Az egésznek van egy nagy hátránya: akárhányszor egy újabb alakzatot leíró osztályt definiálunk, a hozzátartozó show függvényt mindannyiszor újra kell írnunk. Ennek az az oka, hogy a C++ alapvetõen háromféleképpen tudja az azonos névvel ellátott függvényeket egymástól megkülönböztetni:
Egy komolyabb grafikus rendszer esetében azonban igen gyakran elõfordul az a helyzet, hogy csak az osztálydeklarációk állnak rendelkezésre forrásállományban (.h kiterjesztésû include file-okban), maguk a függvénymezõ definíciók pedig csak tárgykód formájában vannak meg (.obj file-okban). Ebben az esetben, ha a felhasználó a meglévõ osztályokból akar újabbakat származtatni, akkor a korai kötés korlátai miatt nem lesz könnyû dolga az alakzatmegjelenítõ rutinok megírásánál. A C++ ezt a problémát az ún. késõi kötés (late binding) lehetõségével hidalja át. Ezt a késõi kötést speciális függvénymezõk, a virtuális függvények (virtual functions) teszik lehetõvé.
Az alapkoncepció az, hogy a virtuális függvényhívásokat csak futási idõben oldja fel a C++ rendszer - innen származik a késõi kötés elnevezés. Tehát azt a döntést, hogy például melyik show függvényt is kell aktivizálni, el lehet halasztani egészen addig a pillanatig, amikor a ténylegesen megjelenítendõ objektum pontos típusa ismertté nem válik a program futása során. Egy virtuális show függvény, amely el van "rejtve" egy elõzetesen lefordított modulkönyvtár egy B alaposztályában, nem lesz véglegesen hozzárendelve a B típusú objektumokhoz olyan módon, ahogy azt normál függvénymezõk esetében láttuk. Nyugodtan származtathatunk egy D osztályt a B-bõl, definiálhatjuk a D típusú objektumok megjelenítésére szolgáló show függvényt. Az új forrásprogramot (amelyik az új D osztály definícióját is tartalmazza) lefordítva kapunk egy .obj file-t, amelyet hozzászerkeszthetünk a grafikus könyvtárhoz (a .lib file-hoz). Ezután a show függvényhívások, függetlenül attól, hogy a B alaposztálybeli függvénymezõre, vagy az új, D osztálybeli függvénymezõre vonatkoznak, helyesen lesznek végrehajtva. Lássuk ezt az egész fogalomkört egy kevésbé absztrakt példán.
Tekintsük a jól bevált point típust, és egészítsük ki azt egy olyan függvénnyel, amelyik egy ilyen típusú objektum által reprezentált pontot a képernyõ egyik helyérõl áthelyezi egy másikra helyre. A point-ból azután származtassunk egy körök leírására alkalmas circle típust (olyan meggondolás alapján, hogy egy pont egy olyan kör, amelynek sugara 0, tehát a point típust egyetlen adatmezõvel, a sugárral kell kiegészíteni).
enum colortype { black, red, blue, green, yellow, white };
class location {
protected:
int x;
int y;
public:
location(int ix, int iy);
~location(void);
int get_x(void);
int get_y(void);
};
class point : public location
{
protected:
colortype color;
public:
colortype get_color(void);
void show(void);
void hide(void);
void move(int dx, int dy);
point(int ix,int iy);
~point(void);
};
// A point:: fuggvenyek definicioja:
...
void point::move(int dx, int dy)
{ // dx, dy offsetekkel athelyezi
if (color) hide( ); // Ez itt a point::hide
x += dx;
y += dy;
if (color) show( ); // Ez itt a point::show
}
class circle : public point
{
protected:
int radius; // A sugara
public: // Konstruktorhoz kezdeti x,y,r
circle(int ix,int iy,int ir);
colortype get_color(void);
void show(void);
void hide(void);
void move(int dx, int dy);
void zoom(int factor);
};
// A circle:: fuggvenyek definicioja:
...
void circle::move(int dx, int dy)
{ // dx, dy offsetekkel athelyezi
if (color) hide( ); // Ez itt a circle::hide
x += dx;
y += dy;
if (color) show( ); // Ez itt a circle::show
}
...
Figyeljük meg, hogy a két move függvény
mennyire egyforma! A visszatérési típusuk void
és a paraméter-szignatúrájuk is azonos, sõt,
még a függvénytörzsek is azonosnak tûnnek.
Hát akkor mi alapján tudja a fordító, hogy
mikor melyik függvényrõl van szó? Ez esetben
- ahogy azt a normál függvénymezõk esetében
láttuk - a move függvényeink abban különböznek,
hogy más-más osztályhoz tartoznak ( point::move,
illetve circle::move). Fölvetõdik a kérdés:
Ha a két move függvény törzse is egyforma,
akkor miért kell belõlük 2 példány, miért
ne örökölhetné circle ezt a függvényt
point-tól? Nos azért, mert csak felületes ránézésre
egyforma a két függvénytörzs, ugyanis más-más
hide, illetve show függvényeket hívnak
- ezeknek csak a neve és a szignatúrája azonos. Ha
circle a point-tól
örökölné a move függvényt,
akkor az nem a megfelelõ hide, illetve show függvényeket
hívná: nevezetesen a körre vonatkozó függvények
helyett a pontra vonatkozókat aktivizálná. Ez azért
van így, mert a korai kötés következtében
a point::hide, illetve a point::show lenne a point
típus move függvényéhez hozzárendelve,
és az öröklés által a circle típus
move függvénye is ezeket függvényeket
hívná. Ezt a problémát úgy oldhatjuk
meg, ha a show, illetve a hide függvényeket
virtuális fügvényekként deklaráljuk. Ekkor
nyugodtan írhatunk egy közös move függvényt
a point és a circle számára (pontosabban
fogalmazva a circle típus örökölhetné
a point típus move függvényét),
és majd futási idõben fog kiderülni, hogy melyik
show, illetve hide függvényt kell meghívni.