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:
ipsisältää osoitteen, jossaisijaitsee.&ipon itse osoitinmuuttujan muistiosoite.
Osoitteiden todellinen esitysmuoto ja arvot riippuvat tietokoneesta, käyttöjärjestelmästä ja suorituskerrasta. Havainnollistetaan tapahtumia kuvitteellisilla osoitteilla 0x000002 ja 0x000008:
- Luodaan kokonaislukumuuttuja
i, joten sille varataan tilaa muistista. Kuvitellaan, että tämä tila alkaa osoitteesta0x000002. - Luodaan kokonaislukuosoitin
ip, joten myös sille varataan oma tila. Kuvitellaan, että osoitinmuuttuja sijaitsee osoitteesta0x000008alkavissa muistipaikoissa. - Muuttujalle
isijoitetaan arvo5. Sen binääriesitys tallentuu osoitteesta0x000002alkaviin muistipaikkoihin. - Ilmaus
&iselvittää muuttujaniosoitteen (0x000002). Se sijoitetaan muuttujaanip, jonka oma muistipaikka alkaa osoitteesta0x000008. - Ilmaus
*ip = 42tallentaa luvun42binääriesityksen osoitteesta0x000002alkaviin muistipaikkoihin. Nämä ovat samat muistipaikat, joihin muuttujaniarvo tallennettiin, joten myösi:n arvo muuttuu. - Koska
ipon itsekin muuttuja, sen sijainnin saa selville ilmauksella&ip. Tässä kuvitteellisessa esimerkissä tuloste on42,42,0x000002,0x000008: ensini, sitten*ip, sen jälkeen osoittimen sisältämä osoite ja lopuksi osoitinmuuttujan oma osoite. Todellinen tuloste vaihtelee ympäristön ja suorituskerran mukaan.
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:
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;
nullptr ja että se osoittaa yhä elossa olevaan kohteeseen.