Dynaaminen muistinhallinta

Kopioinnin ja sijoituksen estäminen


Lukuohje: tämän sivun alkuosa antaa taustatietoa myöhempiä ohjelmointikursseja varten. Voit halutessasi siirtyä suoraan kohtaan Kopioinnin ja sijoituksen estäminen. Aiheen alkuperäinen kurssikohta on TUNI Plus -materiaalissa.

Alustus asettaa muuttujalle alkuarvon samalla, kun se esitellään. Sijoitus muuttaa jo olemassa olevan muuttujan arvoa =-operaattorilla.

C++ määrittelee ohjelmoijan tekemille luokille oletusarvoisesti kopiorakentajan ja kopiosijoitusoperaattorin. Siksi kopiointi toimii, vaikka ohjelmoija ei olisi erikseen määritellyt, mitä näissä tilanteissa tapahtuu:

class Mun_oma_luokka {
    // ...
};

Mun_oma_luokka olio;
Mun_oma_luokka olio2{olio}; // alustuksessa kopiorakentaja
olio = olio2;              // sijoituksessa kopiosijoitus

Oletustoiminta kopioi olion jäsenmuuttujat jäsen jäseneltä. Tavallisille arvoille tämä on yleensä oikein. Osoitinta sisältävässä luokassa osoitteen kopioiminen voi kuitenkin olla väärin: kaksi oliota saattavat luulla omistavansa saman dynaamisen muistin.

Matalakopioinnin ongelma

Kuvitellaan tehtävälista, joka hallitsee dynaamisesti varattuja alkioita tavallisilla osoittimilla. Kun lista kopioidaan oletusarvoisesti, kopioituvat myös osoitteet, mutta niiden osoittamaa rakennetta ei kopioida:

Lista lista;
// ...
{
    Lista apulista{lista}; // vaarallinen oletuskopio
    // ...
}

Sama ongelma syntyy sijoituksessa:

Lista lista;
// ...
{
    Lista apulista;
    // ...
    apulista = lista; // vaarallinen oletussijoitus
}
Matalakopioinnissa kaksi listaoliota osoittaa samaan dynaamiseen rakenteeseen
Matalakopioinnissa osoittimet kopioituvat, mutta niiden osoittamat alkiot eivät.

Kun lohko päättyy, apulista-olion purkaja vapauttaa sen hallitseman muistin. Jos myös lista osoitti samaan muistialueeseen, sen osoittimet jäävät osoittamaan vapautettuun muistiin. Tätä kutsutaan jäänneviitteeksi tai roikkuvaksi osoittimeksi. Jos molemmat oliot yrittävät vapauttaa saman muistin, seurauksena voi olla myös virheellinen kaksoisvapautus.

Osoittimia sisältävän olion kopioiminen arvoparametriksi aiheuttaa saman oletuskopioinnin:

void funktio(Mun_oma_luokka muodollinen_parametri) {
    // ...
}

int main() {
    Mun_oma_luokka todellinen_parametri;
    funktio(todellinen_parametri); // parametri kopioidaan
}

Mahdolliset ratkaisut

Osoittimia sisältävän luokan kopiointi voidaan ratkaista kahdella tavallisella tavalla:

  • Syväkopiointi: kopioidaan myös osoittimien takana oleva tietorakenne uuteen, erikseen varattuun muistiin. Tällöin kummallakin oliolla on oma itsenäinen kopionsa.
  • Kopioinnin estäminen: kielletään kopiointi, jolloin vahingossa tehty kopio aiheuttaa käännösvirheen.

Osoitteiden kopioiminen ilman osoitettujen tietorakenteiden kopioimista on matalakopiointi (shallow copy). Syväkopioinnin toteutusta ei käsitellä tällä kurssilla, koska se vaatii lisää C++-kielen ominaisuuksia.

Kopioinnin ja sijoituksen estäminen

Luokan kopiorakentajan ja kopiosijoitusoperaattorin voi poistaa käytöstä merkitsemällä ne avainsanalla delete:

class Lista {
public:
    Lista(const Lista& alustusarvo) = delete;
    Lista& operator=(const Lista& sijoitusarvo) = delete;
    // ...
};

Kopiorakentaja ottaa parametrikseen const-viitteen saman luokan olioon. Kopiosijoitusoperaattori ottaa vastaavan parametrin ja palauttaa viitteen tähän olioon. Kun nämä operaatiot on poistettu käytöstä, niitä yrittävä koodi ei käänny.

Tällä kurssilla kopiointi kannattaa estää omilta luokilta, joiden yksityinen osa hallitsee dynaamista tietorakennetta. Näin vältetään vahingossa syntyvät matalakopiot ja niistä seuraavat vaikeasti jäljitettävät virheet.

Viiteparametrilla kopioiminen vältetään

Kopioinnin estäminen ei estä luokan olion käyttämistä funktion parametrina. Parametriksi voi antaa viitteen tai vakio-viitteen, jolloin erillistä kopiota ei muodosteta:

bool lue_tehtavatiedosto(Lista& tehtavat);
bool talleta_tehtavatiedosto(const Lista& tehtavat);

Ensimmäinen funktio voi muuttaa listaa viitteen kautta. Jälkimmäinen lupaa olla muuttamatta sitä. Kopiorakentajaa tarvitaan arvoparametrille, koska muodollinen parametri alustetaan todellisesta parametrista kopiona; viiteparametri puolestaan viittaa suoraan alkuperäiseen olioon.

Muistisääntö: jos luokka omistaa dynaamista muistia, päätä aina selvästi, sallitaanko kopiointi. Tämän kurssin listoissa kopiointi estetään.