Älykkäät osoittimet
Vaikka kaikki dynaamisen muistin käsittely voidaan aina toteuttaa
edellä esiteltyjen C++-kielen osoittimien sekä new
- ja
delete
-käskyjen avulla, niiden käyttö on usein melko sotkuista.
Varsinkin monimutkaisia dynaamisia rakenteita käsiteltäessä päädytään
hyvin usein tilanteeseen, jossa kaikkea new
’llä varattua muistia
ei muisteta vapauttaa delete
llä. Tämän tehtävän helpottamiseksi
C++ tarjoaa valmiina työkaluina nk. älykkäitä osoittimia (smart
pointer).
C++:n älykkäät osoittimet ovat kirjastotietotyyppejä, jotka
automatisoivat dynaamisesti varatun muistin vapauttamisen sen
jälkeen, kun kukaan ei enää viittaa siihen.
Selkokielellä sanottuna siis:
varattu muisti vapautetaan automaattisesti, kun ohjelmassa ei enää ole
yhtään (älykästä) osoitinmuuttujaa, joka osoittaa kyseiseen
muistialueeseen.
Älykkäät osoitintyypit ovat siitä mukavia, että niiden käyttö ei
radikaalisti eroa normaalien osoittimien käytöstä, mutta kaupan
päälle saavutetaan se ilo, että ohjelmoijan ei tarvitse itse murehtia
muistin vapauttamisesta.
Älykkäiden osoittimien käyttö vaatii ohjeman alkuun rivin
josta saadaan käyttöön tyypit
shared_ptr
unique_ptr
weak_ptr
Tällä kurssilla niistä tutustutaan vain shared_ptr
-tyyppiin.
shared_ptr
-osoittimet
Yksinkertainen esimerkki shared_ptr
-osoittimen käytöstä:
#include <iostream>
#include <memory> // Tämä pitää muistaa.
using namespace std;
int main() {
shared_ptr<int> int_oso_1( new int(1) );
shared_ptr<int> int_oso_2( make_shared<int>(9) );
cout << *int_oso_1 << " " << *int_oso_2 << endl;
cout << int_oso_1 << " " << int_oso_2 << endl;
cout << int_oso_1.use_count() << " " << int_oso_2.use_count() << endl << endl;
*int_oso_2 = *int_oso_2 - 4;
int_oso_1 = int_oso_2;
cout << *int_oso_1 << " " << *int_oso_2 << endl;
cout << int_oso_1 << " " << int_oso_2 << endl;
cout << int_oso_1.use_count() << " " << int_oso_2.use_count() << endl;
}
Ohjelman suoritus tuottaa seuraavat tulosteet:
1 9
0x2589010 0x2589060
1 1
5 5
0x2589060 0x2589060
2 2
Huomaa, kuinka esimerkissä ei ole yhtään delete
-käskyä, vaikka
dynaamista muistia varataan sekä new
’llä että
make_shared
-funktiolla. Tämähän juuri oli älykkäiden osoittimien
idea, vastuu dynaamisesti varatun muistin vapauttamisesta on siirretty
shared_ptr
-olioille. Tässä käytetään usein termiä omistaja
(owner), joka on vain hienompi termi kuvaamaan sitä, kenen
vastuulle muistin vapauttaminen kuuluu.
Edellisessä esimerkissä verrattiin shared_ptr
-osoittimien
käyttämistä tavallisten osoittimien käyttämiseen. Lähes kaikki
operaatiot (unaarinen *
, ->
, vertailu ja tulostus), jotka
toimivat tavallisille osoittimille, toimivat myös
shared_ptr
-osoittimille. Suurimpana erona on, että operaattorit
++
ja --
eivät toimi shared_ptr
-osoittimille. Lisäksi
sijoitus toimii toisesta samantyyppisestä shared_ptr
-osoittimesta.
Käydään vielä läpi muutama muu
shared_ptr
-olioiden hyödyllinen ominaisuus,
joille saattaa joskus tulla tarvetta:
Jos shared_ptr
-osoittimesta pitää saada muistiosoite
normaalina C++-osoittimena, se tapahtuu get
-metodilla:
shared_ptr<double> shared_double_ptr( new double );
...
double *normaali_double_ptr = nullptr;
...
normaali_double_ptr = shared_double_ptr.get();
shared_ptr
-osoittimeen ei voi sijoittaa =
-operaattorilla
normaaliosoitinta.
shared_ptr
-osoittimeen voi kuitenkin sijoittaa nullptr
:in.
shared_ptr
-osoitinta ei voi vertailla suoraan normaaliosoittimeen.
Vertailu onnistuu kuitenkin, jos shared_ptr
-osoitin muutetaan
get
-metodilla normaaliosoittimeksi, esimerkiksi:
if(normaaliosoitin == sharedosoitin.get()) {
...
}
shared_ptr
-osoitinta voi vertailla nullptr
:iin.
Tyypillä shared_ptr
on yksi hankala ominaisuus:
Jos niiden avulla muodostetaan “silmukka”, muistia ei koskaan vapauteta:
#include <iostream>
#include <memory>
using namespace std;
struct Testi {
// Tässä kohdin jotain muita kenttiä.
// ···
shared_ptr<Testi> osoite;
};
int main() {
shared_ptr<Testi> oso1(new Testi);
shared_ptr<Testi> oso2(new Testi);
oso1->osoite = oso2;
oso2->osoite = oso1;
}
Edellisestä on hyvä piirtää kuva, jotta ymmärtää kyseessä olevan
eräänlainen muna-ennen-kanaa -ongelma.
Nyt oso1
:n osoittamaa muistia
ei voida vapauttaa, koska osoitin oso2->osoite
osoittaa siihen.
Mutta toisaalta oso2
:n osoittamaa muistia ei myöskään voi
vapauttaa, koska oso1->osoite
osoittaa siihen.
Tehtävälista shared_ptr
-osoittimilla
Tutustu Qt Creatorissa projektiin examples/10/task_list_v2
.
Jos haluat myös suorittaa ja editoida ohjelmakoodia, kopioi se
student
-hakemiston alle.
Projekti sisältää edellisen kierroksen esimerkkiä vastaavan
listarakenteen toteutuksen shared_ptr
-osoittimia käyttäen.
Muokatussa esimerkissä ei ole algoritmisesti muuta uutta kuin se,
että List
-luokalle ei ole tarvinnut toteuttaa purkajaa (tai
käyttää delete
-käskyjä missään muussakaan yhteydessä), koska
shared_ptr
-osoittimet huolehtivat muistin vapauttamisesta, kun
mikään ei enää osoita siihen.
Huomaa kuitenkin, että kaikki linkitetyn listan käsittelemiseen
liittyvät eri tilanteet täytyy huomioida samoin kuin tavallisillakin
osoittimilla (ensimmäisen alkion lisääminen tyhjään listaan, viimeisen
alkion poistaminen listasta, jne).
shared_ptr
-tyyppinen osoitinmuuttuja, johon voidaan tallentaaint
-tyyppisen muuttujan muistiosoite. Alustetaan se osoittamaan dynaamisestinew
’llä varattuun muuttujaan, jonka arvo on 1.