Muistin varaaminen dynaamisesti¶
Dynaamisilla tietorakenteilla tarkoitetaan sellaisia rakenteita, joiden
tarvitsema muisti varataan (ja vapautetaan) dynaamisesti
ohjelmoijan kirjoittamien komentojen perusteella.
C++:ssa nämä komennot ovat new
ja delete
, ja ne esitellään
tarkemmin alla.
Komennot suoritetaan ajoaikona ohjelman suorituksen yhteydessä,
jolloin myös muistin varaaminen (ja vapauttaminen) tapahtuu ajoaikana.
Dynaamiset tietorakenteet sisältävät tavallisesti
useita, tyypiltään samoja alkioita.
Alkiotyyppi voidaan määritellä C++:n struct
-rakenteen avulla,
jolla voi olla useita kenttiä.
Alkiot linkitetään toisiinsa niin, että kullakin alkiolla (struct
:lla)
on linkkikenttä (osoitin), joka osoittaa säiliön seuraavaan alkioon.
Esimerkkejä dynaamisista tietorakenteista ovat linkitetty lista, pino, jono, puu, verkko jne. Nämä rakenteet tyypillisesti määritellään luokkina, ja niitä voidaan käsitellä kyseisen luokan jäsenfunktioiden avulla. Kuten minkä tahansa luokan tapauksessa ohjelmoijan vastuulla on toteuttaa kyseiset jäsenfunktiot.
Edellä mainituista rakenteista linkitetty lista esitellään tarkemmin myöhemmin tällä kierroksella.
Komentoja muistin varaamiseen ja vapauttamiseen¶
Tähän asti kaikki käytetyt muuttujat ovat olleet automaattisia, eli muistin varaaminen niille ja muistin vapauttaminen sen jälkeen kun sitä ei tarvita, on tapahtunut automaattisesti:
- Kun muuttuja on määritelty, kääntäjä on huolehtinut siitä, että sille varataan jostain tarvittava määrä muistia.
- Kun muuttujan elinikä päättyy, [1] kääntäjä on silloinkin järjestellyt asian niin, että tarpeeton muisti on vapautettu.
On kuitenkin monia tilanteita, jossa joustavien tietorakenteiden toteuttamiseksi ohjelmoijan on pystyttävä itse kontrolloimaan muuttujien elinkaaria (aika muuttujan muistinvarauksesta sille varatun muistin vapauttamiseen). Tällaisia muuttujia, joiden elinkaaren kontrollointi on täysin ohjelmoijan vastuulla, kutsutaan dynaamisiksi muuttujiksi. Mekanismeja ja työkaluja, joilla dynaamisia muuttujia hallitaan, kutsutaan dynaamiseksi muistinhallinnaksi.
C++:ssa on kaksi peruskäskyä, joilla dynaamista muistia varataan ja
vapautetaan: new
ja delete
.
Jos vapaata muistia ei ole riittävästi jäljellä, kun new
-operaatio
suoritetaan, siitä aiheutuu poikkeus, joka keskeyttää ohjelman
suorituksen. Kyseisen poikkeustilanteen käsittely on kuitenkin
periaattellisella tasollakin sen verran haastavaa, että tällä
kurssilla näytellään ikään kuin muisti ei koskaan loppuisi.
Käskyllä new
ohjelmoija voi varata uuden dynaamisen muuttujan:
- Muuttujan elinkaari alkaa sillä hetkellä, kun
new
-käsky onnistuu muistin varaamaan. - Dynaamisella muuttujalla ei ole nimeä, mutta
new
-operaation arvoksi evaluoituu osoitin, jonka arvo kertoo, missä kohdassa keskusmuistia uusi dynaaminen muuttuja sijaitsee. - Jotta muuttujalla voisi tehdä jotain hyödyllistä, sen osoite
(
new
-komennon paluuarvo) on talletettava osoitinmuuttujaan. - Jos muuttuja on tyypiltään luokka,
new
huolehtii rakentajan kutsumisesta.
Kun dynaamista muuttujaa ei enää tarvita, siis siitä halutaan
hankkiutua eroon, muisti vapautetaan delete
-komennolla:
- Muuttujan elinkaari päättyy ja sille varattu muisti vapautetaan muuhun käyttöön.
- Jos muuttuja on tyypiltään luokka, jolle on määritelty purkaja,
delete
huolehtii purkajan kutsumisesta.
Koska kääntäjä ei mitenkään automatisoi dynaamisen muuttujan
käsittelyä, on tärkeää muistaa, että jokaikinen new
-komennolla varattu
muuttuja pitää myös muistaa vapauttaa delete
-komennolla sen jälkeen,
kun sitä ei enää tarvita.
Jos tätä ei muista tehdä, tai sählää asiat jotenkin niin, [2] että
kaikkea new
-komennolla varattua muistia ei pystykään vapauttamaan,
seurauksena on tilanne, jota kutsutaan muistivuodoksi:
Ohjelma pitää muistia varattuna, vaikka se ei enää tarvitse sitä.
Muistivuoto on huono asia varsinkin ohjelmissa, joiden suoritus kestää pitkään. Jos muistia varataan toistuvasti, mutta osaa siitä ei koskaan vapauteta, ohjelman kuluttama keskusmuistin määrä kasvaa, mikä kuormittaa koko järjestelmää.
Ensimmäinen esimerkki dynaamisesta muistinkäsittelystä:
int main() {
int* dyn_muuttujan_osoite = nullptr;
dyn_muuttujan_osoite = new int(7);
cout << "Osoite: " << dyn_muuttujan_osoite << endl;
cout << "Alku: " << *dyn_muuttujan_osoite << endl;
*dyn_muuttujan_osoite = *dyn_muuttujan_osoite * 4;
cout << "Loppu: " << *dyn_muuttujan_osoite << endl;
delete dyn_muuttujan_osoite;
}
*
-operaattorin avulla.delete
-operaattorilla.Kun koodi käännetään ja suoritetaan, saadaan seuraava tulostus:
Osoite: 0x1476010
Alku: 7
Loppu: 28
Muista tässä ja muissakin suoritusesimerkeissä, joissa osoittimien arvoja tulostetaan näytölle, että muistiosoitteen arvo saattaa vaihdella eri suorituskerroilla.
[1] | Paikallisilla muuttujilla elinikä päättyy ja muisti vapautuu, kun ohjelman suoritus poistuu muuttujan määrittelylohkosta. Olion jäsenmuuttujien elinikä päättyy ja muisti vapautuu, kun kyseisen olion elinikä päättyy. |
[2] | Esimerkiksi hukataan sen osoitinmuuttujan arvo, joka pitää kirjaa dynaamisesti varatun muuttujan muistiosoitteesta. |
dyn_muuttujan_osoite
, jotta seuraavalla rivillä luotavan muuttujan osoitteelle on talletuspaikka.