C++ 6. Dinamikus memóriakezelés
dátum: 2008-04-18
Kategóriák:
C/C++/C++ alapok
Ahogy az előző cikk végén már beharangoztam, most már tényleg a dinamikus memóriakezelést fogjuk megvizsgálni.
Előtte viszont beszélek egy kicsit (csak felületesen, a valóság sokkal csúnyább) arról, hogy hogyan kezeli a C++ a memóriát:
Megkülönböztetünk változókat (objektumokat) aszerint, hogy hogyan jöttek létre: statikusan (az eddig ismert hagyományos úton) vagy dinamikusan (erről még lesz szó). Van még egy kritérium, mégpedig az, hogy egy objektum lokális vagy globális. Az utóbbiak "pongyolán" megfogalmazva azok az objektumok amiket mindenki mindenhonnan elérhet (tipikusan ezek a main felett deklaráltak). Lokális egy változó akkor, ha egy blokkon belül deklarálták, ez azt jelenti, hogy a láthatósága és élettartama a blokkon belülre korlátozódik.
Most pedig rátérünk a dinamikus memóriakezelésre. Mi is a dolog lényege? A normál statikus változók a saját blokkjuk végéig léteznek és csak ott fognak megsemmisülni. Ez gondot jelenthet akkor, ha szűkös a memória, de egyébként sem hasznos dolog olyan elemeket a memóriában hagyni amikre már nincs szükség. Pontosan ez a dinamikus memória filozófiája: adataink számára akkor foglalunk helyet ha szükséges és ha nincs rájuk szükség akkor azonnal felszabadítjuk a memóriát. Persze senki ne essen túlzásokba, nem kell mindent agyba-főbe kézzel kezelni, pl. egy szimpla változónak felesleges dinamikusan helyet foglalni.
A mostani kezdő szintünkön a leghasznosabb dolog a dinamikus tömb. Nagyon egyszerű a dolog, pontosan ugyanúgy viselkedik mint a hagyományos társa annyi a különbség, hogy kézzel intézük a foglalást/felszabadítást. Ezekre a feladatokra a C++ rendelkezésünkre bocsájtja a new és delete operátorokat. A C programozóknak ismerős malloc és free műveletek is felhasználhatóak, de az újak hatékonyabbak (majd később, kulcsszó: konstruktor).
A foglalás a következőképpen történik:
int * t = new int [10];
Ahogy látható egy pointert használunk fel, a foglalás után t a tömb első elemének memóriabeli helyére mutat (tehát pontosan ugyanúgy történik minden mint a normális tömbök esetén).
Persze egy elemet is kezelhetünk így:
int * t = new int();
Itt nem szögletes hanem hagyományos zárójelet adtunk meg, hogy ez miért van azt majd egy későbbi fejezetben elmondom (kulcsszó: konstruktor - még mindig). Fontos, hogy figyeljünk a helyes zárójel használatára mert az esetek nagy részében akkor is lefordul a program ha szögletes helyett kerek zárójelet használunk (futás közben meg nézünk, hogy mi van - a minimum a szegmens hiba).
Amit lefoglaltunk, azt fel is kell szabadítani:
int * t = new int [10];
delete [] t;
int * p = new int();
delete p;
A delete -nek ugyanúgy mint a new -nak két különböző formája van és fontos, hogy mindig a megfelelőt használjuk, mert a fordító ezt nem ellenőrzi:
int * t = new int [10];
delete t; //Lefordul, de hibás
A fenti esetben a program gond nélkül fordul és futni is fog (jó eséllyel ekkor sem történik semmi gond), de a memóriát nem fogja felszabadítani ezért ún. memóriaszivárgás (művésznevén memory leak) keletkezik. Ez persze egy néhány kilobyte -os tömb esetén nem probléma de nagyobb projektek megérezhetik (pl. egy játék ami gond nélkül felzabál néhányszáz megabyte memóriát - csúnya lenne ha szivárogna).
Mai első példánk rendkívül kreatív lesz: a véletlenszámgenerátorral "készítünk" néhány számot, azokat eltároljuk egy dinamikus tömbben majd kiírjuk. A véletlenszámgenerátor - ahogy a neve is mutatja - számokat generál (rendszerint a rendszeridőből - elnézést a rossz viccért :) ).
#include <iostream>
#include <ctime>
int main(int argc, char * argv[])
{
srand(unsigned(time(NULL))); //vszámgenerátor beállítása
int * t = new int [10];
for(int i = 0;i < 10;++i)
{
t[ i ] = rand() % 100; //0-100 -ig sorsolunk számokat
std::cout << t[ i ] << " ";
}
std::cout << std::endl;
delete[] t;
std::cin.get();
return 0;
}
A program egyszerű mint a faék, biztos vagyok benne, hogy mindenki megértette.
A következő programban létrehozunk egy dinamikus mátrixot (n x n -es tömb) feltöltjük kiíratjuk és megsemmisítjük. A programunkhoz kapcsolódik a programozás egyik legszebb kifejezése, a "pointerre mutató pointer" (az én személyes kedvencem az "explicit típuskonverzió"). Létrehozunk ugyanis egy pointereket tartalmazó tömböt és ezek a pointerek fognak majd más tömbökre mutatni (tehát a tömbünk tömböket fog tartalmazni). A "belső" tömböket is dinamikusan hozzuk létre, ezért a végén trükkös lesz a felszbadítás.
#include <iostream>
#include <ctime>
int main(int argc, char * argv[])
{
srand(unsigned(time(NULL)));
const int size = 10;
//10 db pointerre mutató pointer
int **t = new int * [size];
for(int i = 0;i < size;++i)
{
t[ i ] = new int [size]; //A belső tömbök létrehozása
for(int j = 0;j < size;++j)
{
t[ i ][ j ] = i + j;
std::cout << t[ i ][ j ] << " ";
}
std::cout << std::endl;
}
//A belső tömbök felszabadítása egyenként
for(int i = 0;i < size;++i){ delete[] t[ i ]; }
//A "fő" tömb felszabadítása
delete[] t;
std::cin.get();
return 0;
}
A program eredménye egy "érdekes" mértani alakzat lesz, a kétjegyű számok miatt az elja csálé lesz a mátrixnak, dehát ez van :) .
Az eddigiek alapján már lehet készíteni pl. egy egyszerű torpedót (házi feladat jelleggel).
Mielőtt befejezném a mai adagot egy kis érdekesség: a szabvány előírja, hogy a tömbök méretének fordítási időben ismertnek kell lennie. A fordítóprogramok nem szokták ezt erőltetni ezért ha nem szabvány szerint fordítunk akkor nem szól, egyébként warningot fog dobálni (a warning olyan dolgokra figyelmeztet ami nem akadályozza a fordítást, de gondot okozhat, érdemes figyelni rájuk). Tehát ha a felhasználótól kérjük be a tömb méretét az nem szabványszerű, de hibátlanul fog futni. Később majd megmutatok egy olyan adatszerkezetet amivel ilyen gondok nincsenek (kulcsszó: std::vector).
A dinamikus memóriakezelés még sokszor elő fog jönni (többek közt az osztályoknál, öröklődésnél, kivételkezelésnél, operátortúlterhelésnél - elrettentésnek ennyi is elég).
Legközelebb kivételkezelünk (pl. megtudjuk mit tegyünk ha elfogy a memória), addig is gyakoroljatok.
