Magasszintű nyelvek

A kulisszák mögött


Mi történik a magas szintű nyelvekben?

Gondolkoztál rajta miért nevezzük az osztálypéldányokat "managed", avagy referencia típusoknak?

Ha magas szintű nyelvek után C++-ra váltasz akkor rájössz miért.

Egy referenciatípus viselkedése ugyanaz mint egy pointeré.
Ez egy olyan változó, aminek az értéke egy memóracím, és az igazi cucc az abban a változóban van amelyik azon a memóriacímen van.
Ha módosítod a referenciatípusú változó egyik tulajdonságát, akkor úgymond az azon a memóriacímen levő objektum tulajdonsága kerül módosításra.

Emiatt történik, hogy ha egy referenciatípusú változót adsz paraméterként egy függvénynek, akkor szabatosan fogalmazva "látszik a változás a függvényen kívül".
Ilyenkor a függvény adott paraméterébe csak a memóriacím másolódik át, és a függvény is az adott memóriacímen levő objektumot módosítgatja.

Fontos megjegyezni hogy amikor új értéket adsz egy ilyen változónak olyankor a memóracímet írod felül, nem pedig az objektumot amire a memóriacíme mutat.

Miért így történik?
A futásidejű polimorfizmus miatt.

Azért mert amikor egy új változót bevezetsz a programodba, akkor annak emóriában elfoglalt méretét tudnia kell fordítóprogramnak avgy le se fordul.

Ezt pedig jó tudni, hiszen ha egy típustól ötven másik örököl és mindegyiknek más a mérete, de azt akarjuk, hogy akármelyik altípus hozzárendelhető legyen, akkor elvileg bajban vagyunk.

De mégsem, mert a fordító ilyenkor csak egy memóriacímnyi mérete foglal le a memóriában, és ezt a pointert te használhatod arra hogy mutasson akármelyik altípuspéldányra, hiszen mindegyik rendelkezik a feltípus összes tulajdonságával.
Ezt hívjuk Liskov behelyettesítési elvnek.

A különbség csak annyi, hogy C++-ban képes vagy arra, hogy az osztályokról nem-referencia típuzst hozol létre, hanem olyat aminek a memóriafoglalata egyenló az adott altípus specifikus méretével, és akkor természetesen ezt már nem használhatod arra hogy belerakj egy másik altípust mert annak már bármilyen más mérete lehet.

Szóval C++-ban ezt megteheted:

Shape * s = new Triangle();
s = new Quadrilateral();
s = new Square();

Shape shape = Shape(); // ez is menni fog
shape = Triangle(); // ez itt compiler error lesz

Biztosan hallottad már, hogy C és C++ nyelvekben nincsen automatikus memóriakezelés.

Ez csak féligazság.
MInden olyan változó amely adott blokkban van deklarálva automatikusan van memóriakezelve C és C++ nyelvekben is, hiszen ők a "stack"-en belül lettek létrehozva.

Viszont ez a két nyelv engedi nekünk hogy "stack"-en kívül is foglalhassunk egybefüggő memóriaterületet amit bárhol lefoglalhatunk a programunk futésa során, és bárhol máshol fel is szabadíthatjuk.
Ezt hívjuk kézi memóriakezelésnek C és C++ nyelvekben.

A kérdés most viszont az, hogy a magas szintű nyelvek hogyan kezelik a "stack"en kívül létrehozott memóriaterületet automatikusan?
A cél az, hogy az adott memóriaterületet akkor szabadítsuk fel, ha már egyelten pointer sincs amely arra mutat.

C# vagy Java nyelven "stack"en kívülre kerül minden referenciatípusú változó amikor deklarálod:

Shape s = new Shape;

Ugyanez C++-ban:

Shape * s = new Shape();

A new keyword C++-ban anniyval több a memcpy függvénynél, hogy automatikusan hívja a konstruktorokat.

Hogyha 's' volt az egyeltne változó amely a memóriacímét tartalmazza, és belőle is eltűnteted így:

s = null;

Akkor ebben az esetben automatikusan felszabadításra kerül a memóriaterület hiszen már egyetlen változó sem mutat az adott memóriacímre.

Ha ugyanezt tennéd C és C++ nyelvben:

s = nullptr;

Akkor sikeresen létrehoztál egy memóriaszivárgást, és ez a memóriaterület feleslegesen foglalva marad az alkalmazásod futásának végéig.

C++ ban a "delete" keyword felszabadítja a memóriaterületet úgy hogy a destruktorokat automatikusan hívja:

delete s;

Tehát ez csak egy destruktorhívással több mint a C nyelv free() függvénye.

A kérdés az, hogy tudunk valahogy automatikus memóriakezelész bevezetni a C++ programunkba ha szeretnénk?
C++ nyelvben vannak ilyen "smart pointer"-eink.
Ebből több fajta is van ráadásul.
Az std::shared_ptr például folyamatosan számolja hogy éppen az adott címre hányan mutatnak, és ha eléri a nullát akkor felszabadít automatikusan.

Illetve van az std::unique_ptr amely felszabadítja a memóriaterületet amikor véget ér az adott blokk futása.
Ennek leginkább akkor van értelme, ha mindössze csak a dinamikus memóriafoglalás lehetősége miatt foglalunk "stack"-en kívül, hiszen stack-en csak konstans méretű tömböket tudunk létrehozni.