Muistin varaaminen ja vapauttaminen
Nyt kannattaisi lukea aluksi Sekalaiset-osiosta kappale muistialueista.
Kuten nyt tiedät, pidetään kaikki paikalliset muuttujat pinossa (stack). Paikalliset muuttujat eivät ole kuitenkaan pysyviä, mikä on melkoinen ongelma. Globaalit muuttujat ovat kyllä pysyviä, mutta niitä sää sörkkiä kuka tahansa ja niinpä tiedon kapselointi ei toteudu ollenkaan. Ja vielä samaan hengenvetoon mainittakoon, että pino on melko pieni, joten suurien alueiden säilyttäminen siellä ei ole mahdollista. Sanonpa senkin, että muistia ei voi myöskään kätevästi vapauttaa, joten vähänkään kehittyneempi muistinhallinta ei ole mahdollista. Muistin kulutus kasvaa kuin bensan hinta konsanaan. Mutta meillähän on vapaa muistialue, jota ratkaisee kaikki nämä ongelmat! Joko olet valmis marssimaan dynaamisen muistinhallinnan liehuvan lipun alla?
Muistin varaaminen tehdään new-käskyllä. Muistialuetta ei pidä tietenkään hukata, vaan joku osoitin pitää laittaa osoittamaan sen alkua. Varattu muistialue vapautetaan delete-käskyllä. Sen jälkeen osoitin kannattaa asettaa nollaksi, koska se ei enää osoita mitään aluetta. Jos nollaamaton, satunnaiseen paikkaan osoittava osoitin annetaan deletelle uudestaan, niin tapahtuu kauheita.
int *pOsoitin = new int; // luodaan osoitin ja varataan muisti *pOsoitin = 666; // käytetään muistia, * alkuun koska haluamme muuttaa // muistialueen sisältöä, emme osoittimen sisältöä (=osoitetta) delete pOsoitin; // vapautetaan muisti pOsoitin = 0; // varmuuden vuoksi
Taulukot ja oliot muistia varaamalla
Omimmillaan muistin varaaminen on suuria tietomääriä - siis yleensä taulukoita - käsitellessä. Taulukon varaaminen tapahtuu antamalla taulukon koko muuttujatyypin yhteydessä. Osoitin osoittaa taulukon ensimmäiseen alkioon, joten delete vapauttaa vain taulukon ensimmäisen alkion. delete [] sen sijaan vapauttaa koko taulukon.
int *pTaulukko = new int[800]; pTaulukko[0] = 10; // taulukon ensimmäisen alkion arvon on 10 delete [] pTaulukko; pTaulukko = 0;
Nyt ei tarvitse laittaa *-merkkiä osoittimen eteen, koska taulukon kanssa käytettävä []-operaattori ajaa saman asian.
Myös olioita voidaan luoda muistia varaamalla. Kun olioita käyttää, tulee muistaa että käyttää olion osoitinta. Siis olioon viitataan (*pOlio).muuttuja. Tuo on kuitenkin vähän inhottava kirjoittaa, joten on kehitelty mukavampi tapa tehdä sama asia: pOlio->muuttuja.
class Hirmu { public: int ika; }; Hirmu *karvainenHirvio = new Hirmu; (*karvainenHirvio).ika = 10; karvainenHirvio->ika = 20; // mukavampi tapa delete karvainenHirvio;
Varo vaanivaa vaaraa
Kun muistin hallinta ei ole enää niin paljon kääntäjän vastuulla, on paljon enemmän mahdollisuuksia mokata. Ja sinisilmäinen aloittelijahan käyttää jokaisen mahdollisuuden hyväkseen ja oppii kantapään kautta jokaisen sudenkuopan oikein isän kädestä (tässä vaiheessa kai vanha kansa napsii jo nitroja).
Kauhein virhe on tietenkin vahingossa muuttaa osoitinta. Kun se osoittaa väärään paikkaan muistialuetta ohjelma kaatuu kuin lonkkavikainen tuhatjalkainen rasvatuissa liukuportaissa. Jos taas muistialue vapautetaan ja sitä yhä käytetään, on tilanne suunnilleen sama. Alueella saattaa olla jo jotain muuta käyttöä ja niinpä sinne kirjoitettaessa meneekin kaikki pahemman kerran sekaisin.
Pienempi paha on jos muistia vain hukataan. Ohjelma ei kaadu, mutta kuluttaa muistia turhaan. Esimerkiksi jos käytät jotain osoitinta - joka yhä osoittaa varattuun muistiin - muistin varaukseen uudestaan, jää vanha alue vapauttamatta ja menee hukkaan. Myös jos vapautat taulukon deletellä ilman []-merkkejä, jää taulukko ekaa alkiota lukuunottamatta vapauttamatta ja taas menee muistia tärviölle. Eikä siinä kaikki, vaan tuloksena saattaa olla myös muistin korruptoituminen ja ohjelman kaatuminen tai muistin loppuminen kesken ja ohjelman kaatuminen tai vastaava virhetilanne. Eli iso virhe se muistin hukkaaminenkin on.
Saatat ehkä ihmetellä, että kun tämä dynaamisen muistinhallinnan hienous tuo niin monia uusia vaaroja ohjelmointiin, niin miksi tätä yleensäkään on olemassa. Raaka tosiasia on, että kehittyneemmät ja ohjelmoinnin kannalta välttämättömät tietorakenteet eivät ole mahdollisia ilman dynaamista muistinhallintaa. Sellaisia rakenteita ovat esimerkiksi lennossa kokoaan muuttavat tai tehokkaat hakumahdollisuudet omaavat rakenteet. Näiden tietorakenteiden vaatima muistinkäyttö on niin monimutkaista, ettei kääntäjän ole mahdollista hoitaa niiden vaatimaa muistinkäsittelyä - yksinkertaisesti kääntämisvaiheessa ei vielä voida tietää, miten tietorakenteet tulevat tarvitsemaan muistia. Eli käyttäjän on itse ohjelmoitava niiden muistinkäyttö.
Toki on myös mahdollista tehdä ohjelmaan osa, joka valvoo muistinkäyttöä. Siis ohjelman suorituksen aikana seuraa mitkä muistialueet ovat käytössä ja mitkä saa jo vapauttaa. Sellaista kutsutaan roskien kerääjäksi (garbage collector). Esimerkiksi Javassa roskienkeruu on, mutta C++:ssa ei ole eikä todennäköisesti tule. C++:n on saatavana kyllä roskienkeruumekanismeja erillisinä paketteina, joten sellaisenkin voi ottaa käyttöön jos haluaa. Vaikka roskienkeruussa on joitain ongelmia, niin täytyy silti henkilökohtaisesti tunnustaa että ihan mahtava apuväline se on...