Oliot, osoittimet ja taulukot

Osoitintietotyypit


Osoitintyyppi on tietotyyppi, jonka arvo kertoo tietyn tyyppisen tiedon sijainnin muistissa. Osoitintyyppiseen muuttujaan eli osoittimeen voidaan tallentaa muistiosoite, jotta ohjelma voi käsitellä tuossa osoitteessa olevaa tietoa. Myös STL:n iteraattorit hyödyntävät osoittimia muistissa kulkemiseen.

Osoittimiin tutustuttiin jo johdantoluvussa. C++:ssa osoitin määritellään merkitsemällä kohdetyypin ja muuttujan nimen väliin tähti:

kohdetyyppi* muuttujan_nimi;

Esimerkiksi int*-tyyppiseen osoittimeen voi tallentaa kokonaislukumuuttujan osoitteen. Kohdetyyppi kertoo kääntäjälle, miten osoitteen takana olevat bitit tulkitaan.

int luku = 5;
int* osoitin = &luku;

Unäärinen &-operaattori ottaa muuttujan osoitteen. Unäärinen *-operaattori puolestaan dereferoi osoittimen eli antaa pääsyn sen osoittamaan arvoon. Osoittimen kautta voi sekä lukea että muuttaa kohdetta:

cout << *osoitin << endl;  // lukee arvon 5
*osoitin = 42;                 // muuttaa luku-muuttujan arvoksi 42

Osoitintahti esiintyy siis kahdessa eri yhteydessä: määrittelyssä se kertoo osoitintyypistä (int* osoitin), lausekkeessa se dereferoi osoittimen (*osoitin). Nämä liittyvät toisiinsa, mutta tekevät eri asioita.

Osoitin ja osoitettu arvo

Tarkastellaan kokonaislukua ja siihen osoittavaa osoitinta. Esimerkissä tulostetaan muuttujan arvo, osoittimen dereferoinnin tulos, osoittimen sisältämä osoite ja osoitinmuuttujan oma osoite:

#include <iostream>

int main() {
    int i = 5;
    int* ip = nullptr;

    ip = &i;
    *ip = 42;

    std::cout << i << ","
              << *ip << ","
              << static_cast<const void*>(ip) << ","
              << static_cast<const void*>(&ip) << std::endl;
}

Ohjelman tulosteen kaksi ensimmäistä arvoa ovat 42 ja 42: muuttujan i arvoa muutettiin osoittimen kautta. Kaksi viimeistä tulostettavaa osoitetta ovat puolestaan eri asioita:

  • ip sisältää osoitteen, jossa i sijaitsee.
  • &ip on itse osoitinmuuttujan muistiosoite.

Osoitteiden todellinen esitysmuoto ja arvot riippuvat tietokoneesta, käyttöjärjestelmästä ja suorituskerrasta. Havainnollistetaan tapahtumia kuvitteellisilla osoitteilla 0x000002 ja 0x000008:

  1. Luodaan kokonaislukumuuttuja i, joten sille varataan tilaa muistista. Kuvitellaan, että tämä tila alkaa osoitteesta 0x000002.
  2. Luodaan kokonaislukuosoitin ip, joten myös sille varataan oma tila. Kuvitellaan, että osoitinmuuttuja sijaitsee osoitteesta 0x000008 alkavissa muistipaikoissa.
  3. Muuttujalle i sijoitetaan arvo 5. Sen binääriesitys tallentuu osoitteesta 0x000002 alkaviin muistipaikkoihin.
  4. Ilmaus &i selvittää muuttujan i osoitteen (0x000002). Se sijoitetaan muuttujaan ip, jonka oma muistipaikka alkaa osoitteesta 0x000008.
  5. Ilmaus *ip = 42 tallentaa luvun 42 binääriesityksen osoitteesta 0x000002 alkaviin muistipaikkoihin. Nämä ovat samat muistipaikat, joihin muuttujan i arvo tallennettiin, joten myös i:n arvo muuttuu.
  6. Koska ip on itsekin muuttuja, sen sijainnin saa selville ilmauksella &ip. Tässä kuvitteellisessa esimerkissä tuloste on 42,42,0x000002,0x000008: ensin i, sitten *ip, sen jälkeen osoittimen sisältämä osoite ja lopuksi osoitinmuuttujan oma osoite. Todellinen tuloste vaihtelee ympäristön ja suorituskerran mukaan.
Muistin tila pääohjelman lopussa: kokonaisluku ja osoitinmuuttuja sijaitsevat eri muistipaikoissa, ja osoitin viittaa kokonaislukuun
Muistin käyttö pääohjelman lopussa: ip sisältää i-muuttujan osoitteen.

Kuvan esimerkissä osoittimen on oletettu olevan 4 tavun eli 32 bitin kokoinen. Nykyisissä tietokoneissa osoittimet ovat usein 64-bittisiä, mutta niiden koko riippuu käytetystä ympäristöstä.

Muistiosoitteen tarkka numero ei yleensä ole ohjelmoijalle tärkeä. Osoittimen voi siksi kuvata nuolella, joka osoittaa kohteeseen:

Osoitin kuvattuna nuolena, joka osoittaa muistipaikkaa
Nuoli havainnollistaa osoitussuhteen ilman muistiosoitteen numeroa.

Osoittimen kohteen tyyppi

int*-osoittimeen kuuluu tallentaa int-muuttujan osoite, double*-osoittimeen double-muuttujan osoite ja niin edelleen. Kohdetyypin perusteella kääntäjä tietää, miten monta tavua luetaan ja miten sisältöä käsitellään, kun osoitin dereferoidaan.

Osoitin ei siis ole itse kohteen arvo, vaan osoite, josta kohde löytyy. Esimerkiksi ip sisältää osoitteen ja *ip tarkoittaa kyseisessä osoitteessa olevaa kokonaislukua.

Luokka- ja tietuetyyppisen kohteen jäseniin pääsee osoittimen kautta nuolioperaattorilla ->. Esimerkiksi synttarisankari->tulosta() vastaa kirjoitusasua (*synttarisankari).tulosta().

Tyhjä osoitin: nullptr

Osoitin voidaan alustaa arvoon nullptr, joka ei osoita mihinkään kohteeseen. Se on hyödyllinen esimerkiksi silloin, kun osoittimelle ei vielä ole sopivaa kohdetta tai funktio haluaa ilmoittaa, ettei etsimäänsä kohdetta löytynyt.

int* ip = nullptr;

if (ip == nullptr) {
    std::cout << "Osoitin ei osoita kohteeseen." << std::endl;
}

Osoittimia voi verrata arvoon nullptr, mutta null-osoitinta ei saa dereferoida. Sen dereferointi aiheuttaa määrittelemätöntä toimintaa ja johtaa usein ohjelman kaatumiseen. Tarkista ennen dereferointia, että osoitin ei ole null ja että sen kohde on edelleen olemassa.

Yksityiskohtaisempi huomio null-osoittimen käyttämisestä löytyy kurssin lähdemateriaalista. Osoitintyyppeihin ja niiden käyttöön palataan myöhemmin perusteellisemmin.

Kokeile itse: tarkista osoitin ennen käyttöä

Mikä muutos estää *ip-lauseketta käyttämästä tyhjää osoitinta?

int* ip = nullptr;
cout << *ip;
Tyhjää osoitinta ei saa dereferoida. Tarkista ensin, että osoitin ei ole nullptr ja että se osoittaa yhä elossa olevaan kohteeseen.