Dynaaminen muistinhallinta

Johdattelua muistinhallintaan


Palautetaan mieleen tehtävä, jossa toteutettiin split-funktio. Kyseinen funktio palautti vektorin. Tehtävänannon vinkeissä huomautettiin, ettei paluuarvona pidä palauttaa viitettä vektoriin vaan vektori itse.

Vinkille oli syynsä. Olemme opetelleet välittämään suuria tietorakenteita, kuten vector-arvoja, arvoparametrin sijaan tehokkaasti (vakio)viitteenä. Siksi voisi tulla mieleen, että paluuarvona kannattaisi välittää vektorin sijasta viite siihen. Tämä ei kuitenkaan toimi, sillä funktion paikalliset muuttujat poistuvat muistista funktion suorituksen päättyessä.

Paikallisen muuttujan elinkaari

Funktiossa split on muodostettu paikallinen vector-muuttuja ja viite siihen. Funktion suorituksen aikana ne ovat olemassa:

Paikallinen viite osoittaa funktion paikalliseen vektorimuuttujaan
Funktiossa split luotu paikallinen vektori ja siihen viittaava viite.

Jos funktio split välittäisi pääohjelmalle paluuarvona viitteen paikalliseen muuttujaansa, pääohjelma saisi viitteen tähän paikalliseen vektoriin:

Funktiosta palautettu viite osoittaa paikalliseen muuttujaan
Pääohjelma saa viitteen funktiossa luotuun paikalliseen vektoriin.

Kun funktion split suoritus päättyy, sen paikalliset muuttujat tuhoutuvat. Pääohjelmassa käytössä oleva viite jää tämän jälkeen osoittamaan muuttujaan, jota ei enää ole:

Jäänneviite osoittaa jo tuhoutuneeseen paikalliseen muuttujaan
Funktion päätyttyä viitteestä tulee jäänneviite.

Tästä viitteestä tulee nk. jäänneviite. Termi on hieman harhaanjohtava, sillä se voi tarkoittaa sekä viitettä että osoitinta, joka viittaa muuttujaan, jota ei enää ole olemassa. Jäänneviitteen käyttäminen aiheuttaa määrittelemätöntä toimintaa.

Virheen voi havainnollistaa seuraavalla virheellisellä koodilla:

vector<string>& virheellinen_split(const string& teksti) {
    vector<string> osat;
    // ... jaetaan teksti osiin
    return osat;  // Virhe: palauttaa viitteen paikalliseen muuttujaan.
}

Kun osat tuhoutuu funktion lopussa, palautettu viite muuttuu välittömästi käyttökelvottomaksi. Osoittimien ja viitteiden elinkaarten ymmärtäminen auttaa välttämään tällaisia vaikeasti löydettäviä virheitä.

Palauta vektori arvona

Oikea ratkaisu on palauttaa paikallinen vektori arvona:

vector<string> split(const string& teksti) {
    vector<string> osat;
    // ... jaetaan teksti osiin
    return osat;
}

Paluuarvo ei jää viittaamaan funktion paikalliseen muuttujaan. Moderni C++ pystyy palauttamaan tällaisia säiliöitä tehokkaasti esimerkiksi siirtämällä niiden sisällön tai jättämällä tarpeettoman kopion muodostamatta. Siksi arvona palauttaminen on sekä turvallista että tavallisesti tehokasta; paikalliseen muuttujaan viitettä ei pidä palauttaa.

Automaattiset muuttujat ja dynaaminen elinaika

Tähän asti kaikki käyttämämme muuttujat ovat olleet nk. automaattisia muuttujia: kääntäjä ja C++:n elinkaarisäännöt huolehtivat siitä, minne ne tallennetaan ja milloin ne tuhotaan. Esimerkiksi funktion paikalliset muuttujat tuhotaan automaattisesti, kun funktion suoritus päättyy. Automaattisen muuttujan elinaika määräytyy siis sen koodilohkon perusteella, jossa muuttuja esitellään.

Monimutkaisemmissa ohjelmissa tulee vastaan tilanteita, joissa olisi hyödyllistä, että ohjelmoija voisi itse vaikuttaa muuttujan elinaikaan. Edellä olevissa kuvissa olisi hyvä, jos ohjelmoija voisi päättää, kuinka kauan vektorin sisältö pysyy käytettävissä.

Miksi muistinhallintaa opiskellaan?

Tällä kierroksella perehdytään manuaaliseen muistinhallintaan C++-ohjelmissa. Harjoittelemme sitä toteuttamalla itse dynaamisen tietorakenteen, jonka koko voi muuttua ajon aikana. Rakenne muistuttaa käyttötarkoitukseltaan Pythonin list-tyyppiä tai STL:n vector-säiliötä.

Harjoituksen tarkoitus ei ole, että ohjelmoijan pitäisi tavallisessa työssä toteuttaa dynaamiset säiliöt itse. Valmiit säiliöt ovat yleensä oikea ratkaisu. Toteutamme rakenteen itse, jotta opimme muistinhallinnan yleisiä periaatteita, ymmärrämme paremmin STL-säiliöiden toimintaa ja osaamme käyttää dynaamista muistia muissa tarkoituksissa. Muistinhallinnan ymmärtäminen auttaa välttämään virheitä myös valmiita tietorakenteita käytettäessä.

Manuaalista muistinhallintaa harjoitellessa tutustumme osoittimiin tarkemmin. Osoitin on C++:ssa tärkeä työkalu korkeamman tason rakenteiden toteuttamiseen: sen avulla voidaan esimerkiksi viitata tietoon epäsuorasti ja jakaa pääsyä tai omistajuutta.