Osoittimet¶
Osoitintietotyypit ovat tietotyyppejä, joiden arvojoukko koostuu tietyntyyppisten tietoalkioiden muistiosoitteista. Toisin sanoen osoitintyyppiseen muuttujaan (osoittimeen, pointteriin) voidaan tallettaa muistiosoite. Osoittimeen voidaan siis tallettaa jonkin tarpeellisen tiedon sijainti muistissa.
Kuten jo aiemmin opimme, C++:ssa osoittimia määritellään:
kohdetyyppi* muuttujan_nimi;
eli aivan kuin oltaisiin määrittelemässä normaalisti
kohdetyyppi-tyyppistä muuttujaa, mutta muuttujan nimen ja tyypin
nimen väliin lisätään *
-merkki.
Tuloksena saadaan muuttuja, johon voidaan tallettaa kohdetyyppi-tyyppisen muuttujan muistiosoite (sen sijainti muistissa). Kohdetyyppi on välttämätön, sillä sen perusteella osoittimeen talletetusta muistiosoitteesta löytyvää dataa (bittejä) osataan käsitellä oikein.
Kaikista ohjelmassa käytetyistä muuttujista saadaan tarvittaessa
selville niiden muistiosoite unaarisella &
-operaattorilla.
Osoittimeen talletetussa muistiosoitteessa sijaitsevan muistipaikan
sisältöön päästään käsiksi unaarisella *
-operaattorilla. Unaarisen
*
-operaattorin toimintaidea on analoginen sen kanssa, kuinka sitä
käytettiin iteraattoreiden kanssa.
Tutkitaan yksinkertainen esimerkki:
#include <iostream>
int main() {
int i;
int* ip = nullptr; // Palataan nullptr:iin tämän materiaaliosion lopussa
i = 5;
ip = &i;
*ip = 42;
std::cout << i << "," << *ip << "," << ip << "," << &ip << std::endl;
}
ip
, siis varataan sille tilaa
muistista jostain vapaasta kohdasta.
Kuvitellaan, että tämä muisti varataan kohdasta 0x000008.i
arvo 5, siis talletetaan luvun 5
binääriesitys osoitteesta 0x000002 alkaviin muistipaikkoihin.i
osoite (0x000002)
&
-operaattorilla ja talletetaan se muuttujaan ip
eli
osoitteesta 0x000008 alkaviin muistipaikkoihin.ip
:hen talletetusta osoitteesta (0x000002)
alkaviin muistipaikkoihin luvun 42 binääriesitys (samat muistipaikat,
joissa muuttuja i
on tallessa, siis muuttujan i
arvo
muuttuu).ip
on muuttuja, senkin sijainti muistissa
saadaan &
-operaattorilla.
Kuvitteellisessa esimerkissämme näytölle tulostuu
42,42,0x000002,0x000008.
Todellinen tuloste tietenkin vaihtelee tietokoneesta,
käyttöjärjestelmästä ja suorituskerrasta riippuen.Kuvallisesti esimerkin muistinkäyttö näyttäisi main
-funktion
lopussa seuraavalta:
Esimerkissä on oletettu osoittimen olevan kooltaan 4 tavua eli 32-bittiä. Todellisuudessa osoittimet ovet useimmissa nykyisissä tietokonelaitteissa 64-bittisiä.
Esimerkistä havaitaan myös, että lausekkeessa käytettynä *ip
käyttäytyy kuin int
-tyyppinen muuttuja:
Sille evaluoituu arvo muistipaikasta, johon ip
osoittaa, ja siihen
voidaan suorittaa sijoitus operaattorilla =
, jolloin arvo tallettuu
kyseiseen muistipaikkaan.
Sama pätee muunkin tyyppisiin osoittimiin.
Osoittimen arvo kertoo, missä jokin tieto sijaitsee. Koska itse muistipaikan numero ei ole olennainen, esitetään edellisen kuvan sisältö usein yksinkertaistettuna tähän tapaan:
Operaattorilla *
osoitimesta saadaan selville varsinainen
osoitettu tieto. Sanomattakin(?) on selvää, että osoittimeen voi
tallentaa vain sellaisen muuttujan osoitteen, jonka tyyppi on sama
kuin osoittimen kohdetyyppi.
Esimerkissä ip
-osoittimeen alustetaan arvo nullptr
. Tämä on
tärkeä pieni yksityiskohta. Tällaista osoitinta kutsutaan null-
tai nil-pointteriksi. Kaikkiin osoitinmuuttujiin voidaan sijoittaa
nullptr
.
Osoittimelle saadaan näin asetettua arvo, joka ei osoita minnekään.
Osoittimen yhtä- ja erisuuruutta voi vertailla
nullptr
-arvon kanssa ja nullptr
:ää voi käyttää
osoitintyyppisenä parametrina. Usein myös funktiot, joiden paluuarvo
on tyypiltään osoitin, palauttavat virhetilanteessa nullptr
:n.
Jos ohjelman suorituksen aikana yritetään seurata [*] null-osoitinta, on tuloksena aina ajonaikainen virhe ja ohjelman suorituksen päättyminen. Null-osoitinta kannattaa aina käyttää osoitinmuuttujien alustusarvona ja virhepaluuarvona funktioilta, joiden paluuarvon tyyppi on osoitin.
Jos osoittimeen on talletettu class
- tai struct
-tyyppisen arvon
muistiosoite, osoitetun tiedon metodeja ja kenttiä käsitellään
->
operaattorilla.
[*] | Eli tutkia tietoa, johon osoitin osoittaa. Eli kohdistaa
osoittimeen unäärinen * -operaattori tai ->
-operaattori. |
i
, siis varataan sille tilaa muistista jostain vapaasta kohdasta. Kuvitellaan nyt, että muisti varataan vaikkapa osoitteesta 0x000002.