Dynaaminen muistinhallinta

C++:n älykkäät osoittimet


Älykäs osoitin on kirjastotietotyyppi, joka hallitsee dynaamisesti luodun olion elinkaarta. Tällä kurssilla käytämme pääasiassa tyyppiä shared_ptr: olio tuhotaan automaattisesti, kun viimeinen sitä omistava shared_ptr lakkaa olemasta tai asetetaan osoittamaan muualle.

Tavallisella osoittimella ohjelmoijan pitäisi huolehtia muistin vapauttamisesta itse. Se on helppo unohtaa. Älykkäät osoittimet vähentävät tätä riskiä hallitsemalla olion elinkaarta automaattisesti.

shared_ptr ja make_shared

Älykkäät osoittimet määritellään otsakkeessa <memory>. C++:ssa on muun muassa tyypit shared_ptr, unique_ptr ja weak_ptr; tässä keskitymme jaettuun omistukseen eli shared_ptr-tyyppiin.

#include <iostream>
#include <memory>

using namespace std;

int main() {
    shared_ptr<int> int_oso_1 = make_shared<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_1 += 4;
        int_oso_2 = int_oso_1;

        cout << *int_oso_1 << " " << *int_oso_2 << endl;
        cout << int_oso_1 << " " << int_oso_2 << endl;
        cout << int_oso_1.use_count() << endl;
    }
    cout << int_oso_1.use_count() << endl;
}

make_shared<int>(1) luo dynaamisen int-olion, jonka alkuarvo on 1, ja palauttaa sitä omistavan shared_ptr<int>-osoittimen. Kun luodaan luokkaolio, make_shared kutsuu luokan rakentajaa sille annetuilla parametreilla.

shared_ptr-muuttujalla ei ole erillistä nimeä hallitsemallaan dynaamisella oliolla. Osoittimen kautta olioon pääsee käsiksi tuttuun tapaan *- ja ->-operaattoreilla.

Jaettu omistus ja viitelaskuri

shared_ptr pitää kirjaa saman olion omistavien jaettujen osoittimien määrästä. Tätä määrää voi tarkastella use_count()-jäsenfunktiolla. Kun laskuri saavuttaa nollan, oliolla ei ole enää omistajia ja se tuhotaan.

Esimerkin alussa osoittimet omistavat eri oliot, joten kummankin laskuri on 1. Rivi int_oso_2 = int_oso_1; siirtää toisen osoittimen omistuksen samaan olioon: 9:n sisältäneen olion laskuri putoaa nollaan ja se tuhotaan, kun taas 1:stä muokattu olio saa kaksi omistajaa. Lohkon lopussa paikallinen int_oso_2 tuhoutuu, ja jäljelle jäävä laskuri on 1.

Tulosteessa osoitearvot vaihtelevat suorituskerrasta toiseen. Arvot ja laskurien järjestys ovat tässä olennaiset:

1 9
osoite_1 osoite_2
1 1

5 5
osoite_1 osoite_1
2
1

Kun myös int_oso_1 poistuu näkyvyydestä main-funktion lopussa, sen hallitsema olio tuhotaan. Älä sekoita osoittimien määrää olion viitteiden määrään yleisesti: use_count() laskee juuri kyseisen omistavan shared_ptr-ryhmän jäsenet.

Kopiointi luo jaetun omistajan

Person person;
shared_ptr<Person> p1 = make_shared<Person>(person);
shared_ptr<Person> p2 = make_shared<Person>(person);
shared_ptr<Person> p3 = p1;

cout << p1.use_count() << endl; // 2
cout << p2.use_count() << endl; // 1
cout << p3.use_count() << endl; // 2

p1 ja p2 eivät tässä jaa samaa oliota: kumpikin make_shared-kutsu luo uuden Person-olion. Sen sijaan p3 = p1 kopioi omistuksen, joten p1 ja p3 osoittavat samaan olioon.

Kaavio shared_ptr-osoittimista ja niiden viitelaskureista
Jaetut osoittimet ja niiden viitelaskurit.

Muita shared_ptr-ominaisuuksia

shared_ptr tukee tavallisten osoittimien tapaan olion dereferointia ja jäsenen käyttöä. Sitä voi verrata toiseen yhteensopivaan osoittimeen tai nullptr-arvoon. Osoitinta ei voi kasvattaa tai pienentää ++- ja ---operaattoreilla.

Jos rajapinta tarvitsee tavallisen osoittimen, sen voi hakea get()-jäsenfunktiolla:

shared_ptr<double> jaettu = make_shared<double>(1.0);
double* tavallinen = jaettu.get();

get()-paluuarvo on ei-omistava tavallinen osoitin. Se ei pidennä olion elinaikaa, eikä sitä saa käyttää olion vapauttamiseen. Älä säilytä sitä pidempään kuin tiedät shared_ptr-omistajan olevan voimassa. shared_ptr-muuttujan voi asettaa myös arvoon nullptr.

Omistusketjun silmukat

Jos oliot omistavat toisensa shared_ptr-osoittimilla kehämäisesti, viitelaskurit eivät pääse nollaan, vaikka muut osoittimet poistuisivat. Tällöin oliot jäävät muistiin.

struct Testi {
    shared_ptr<Testi> osoite;
};

void fun() {
    shared_ptr<Testi> oso1 = make_shared<Testi>();
    shared_ptr<Testi> oso2 = make_shared<Testi>();
    oso1->osoite = oso2;
    oso2->osoite = oso1;
}

Tässä kumpikin olio pitää toisen elossa. Laajemmissa ohjelmissa tällainen ei-omistava linkki tehdään usein weak_ptr-tyypillä. Sitä ei käsitellä tällä kurssilla tarkemmin, mutta se ratkaisee omistuskehän aiheuttaman ongelman.

Esimerkki: kirjan varaukset

Kurssin esimerkissä examples/06/reservations/ jaettuja osoittimia käytetään kirjan varausten mallintamiseen. Kun varaus luo uuden omistavan shared_ptr-osoittimen kirjaan, viitelaskuri kertoo, kuinka monta varausta pitää kirjaoliota käytössä. Kun varaukset päättyvät, laskuri vähenee ja olio vapautuu automaattisesti viimeisen omistajan poistuessa.

Muista: kopioitu shared_ptr jakaa omistuksen, kun taas uusi make_shared-kutsu luo uuden olion.

Kertaa: Älykkäät osoittimet

Ennusta viitelaskurin arvo lohkon jälkeen.

shared_ptr<int> p = make_shared<int>(3);
{
    shared_ptr<int> q = p;
    cout << p.use_count() << " ";
}
cout << p.use_count();
q = p jakaa omistuksen, joten laskuri on lohkon sisällä 2. Lohkon lopussa q tuhoutuu, ja p jää ainoaksi omistajaksi.

Mitä std::unique_ptr ilmaisee?

unique_ptr omistaa resurssin yksin ja vapauttaa sen automaattisesti omistajan eliniän päättyessä.

Mitä std::move tekee unique_ptr-osoittimelle?

unique_ptr ei ole kopioitavissa. std::move mahdollistaa omistajuuden siirron lähdeosoittimelta kohteelle.