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;
}
Luodaan osoitinmuuttuja dyn_muuttujan_osoite, jotta seuraavalla rivillä luotavan muuttujan osoitteelle on talletuspaikka.
Varataan uusi dynaaminen muuttuja, alustetaan sen arvoksi 7 ja talletetaan sen osoite myöhempää käyttöä varten.
Käytetään dynaamista muuttujaa sen muistiosoitteen ja *-operaattorin avulla.
Kun dynaamista muuttujaa ei enää tarvita, vapautetaan sille varattu muisti 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.