Johdanto muistinhallintaan

Palautetaan mieleen (kierrokselta 4) tehtävä, jossa toteutettiin split-funktio. Kyseisen funktion paluuarvona oli vector. Tehtävänannon vinkeissä oli huomiona, että paluuarvona ei saa välittää viitettä vector:iin vaan vector itse. Vinkille oli syynsä: Kun olemme tehokkuussyistä opetelleet välittämään suuria tietorakenteita (esim. vector) arvoparametrin sijaan const-viiteparametreina, niin helposti tulee mieleen, että paluuarvon välittämisessä voisi toimia samoin.

Vaikka idea tehokkuuden parantamisesta välittämällä paluuarvona vector:in sijaan viite vector:iin saattaa ensin tuntua hyvältä, ei se toimi. Olemmehan jo edellisellä ohjelmointikurssilla oppineet, että kun funktion suoritus päättyy, kaikki funktion paikalliset muuttujat poistetaan tietokoneen muistista. Kuvallisesti tämän voisi esittää vaikkapa näin:

Viite paikalliseen muuttujaan

Funktiossa split on muodostettu paikallinen vector-muuttuja ja viite siihen.

Viite paikalliseen muuttujaan palautettu pääohjelmaan

Jos funktio split välittää pääohjelmalle paluuarvona viitteen paikalliseen muuttujaansa…

Jäänneviite

… tulee pääohjelman käytössä olevasta viitteestä nk. jäänneviite, kun funktion split suorituksen päätyttyä sen paikalliset muuttujat poistuvat tietokoneen muistista. Jäänneviite on hieman harhaanjohtava termi, koska se voi tarkoittaa yhtä hyvin sekä viitettä että osoitinta, joka viittaa sellaiseen muuttujaan, jota ei enää ole olemassa.

Paikalliseen muuttujaan osoittavan viitteen palauttaminen on hyvä esimerkki virheestä, jollaisia tapahtuu, jos ohjelmoija ei ymmärrä ohjelmointikielen muistinhallinnan toimintaa.

Tähän asti kaikki käyttämämme muuttujat ovat olleet nk. automaattisia muuttujia, eli kääntäjä on huolehtinut siitä, minne ne tallennetaan tietokoneen muistissa ja milloin ne tuhotaan. Kun alamme tehdä monimutkaisempia ohjelmia, tulee vastaan tilanteita, joissa olisi parempi, että ohjelmoija voi itse määrätä, koska muuttuja tuhotaan. Esimerkiksi yllä olevassa kuvasarjassa olisi hyödyksi, jos ohjelmoija voisi päättää, kuinka kauan vector:in sisältö on käytettävissä.

Tällä kierroksella perehdymme siihen, miten muistinhallinta tehdään C++-ohjelmissa manuaalisesti. Yksinkertaisin tapa harjoitella tätä on toteuttaa itse tietorakenne, jonka koko on dynaaminen (= tietorakenne, jonka koko voi muuttua). Tämä tarkoittaa, että toteutamme itse jotain hiukan samantyylistä kuin Pythonin list tai STL:n vector C++:ssa.

Emme tee tätä harjoitusta siksi, että ohjelmoijan tarvitsisi toteuttaa dynaamisia säiliöitä itse. Oikeastaan useimmissa moderneissa ohjelmointikielissä se ei ole edes mahdollista. Teemme harjoituksen, jotta opimme muistinhallinnan yleisiä periaatteita, ja osaamme käyttää dynaamista muistinhallintaa muunlaisissa tarkoituksissa, joita tulee vastaan jo seuraavilla kierroksilla. Lisäksi harjoituksen avulla ymmärrämme, miten esimerkiksi STL:n säiliöt toimivat. Muistinhallinnan syvällinen ymmärtäminen auttaa ohjelmoijaa välttämään virheitä, vaikka hän käyttäisikin ohjelmakoodissaan valmiita dynaamisia tietorakenteita.

Manuaalista muistinhallintaa harjoitellessamme tutustumme myös osoittimiin huomattavasti perustellisemmin, kuin ensimmäisessä katsauksessa, jonka otimme jo kurssin alussa arvo- ja viitesemantiikkaa vertaillessamme. Osoitin on C++:ssa tärkeä työkalu ohjelmiston korkeamman tason rakenteen toteuttamisessa (epäsuora osoittaminen ja pääsyn/omistamisen jakaminen).

Tällä kierroksella siis toteutamme itse dynaamisen tietorakenteen, mutta myöhemmillä kierroksilla osoittimia ja dynaamista muistinhallintaa käytetään moniin muihin tarkoituksiin.