Osoitin on C:n muuttujatyyppi. Osoittimessa on tietyn muistipaikan osoite (tai tyhjä osoitin NULL). Olkoon tämä osoittimen arvo.
Osoittimen arvon kohdalla olevassa muistipaikassa voi olla esim. muuttuja, olio, taulukon alkio, toinen osoitin, funktio, olion metodi tai varaamatonta muistia. Olkoon tämä muistipaikka osoittimen pää.
Sisäisesti kaikki osoitinmuuttujat ovat samankaltaisia, mutta C:n tyypityksestä johtuen on aina määriteltävä, mihin tyyppiin tai luokkaan tietty osoitin kuuluu. Esimerkiksi
int *i; // osoitin kokonaislukuun. std::string *s; // osoitin STL-merkkijonoon. char **c; // osoitin merkkiosoittimeen. int (*f)(); // osoitin funktioon, joka palauttaa kokonaisluvun. void* v; /* void-osoitin, eli sen osoittaman muistipaikan tyyppiä ei ole määritelty, mutta jollaisen esim. tietyt C-funktiot voivat palauttaa. Käyttääkseen void-osoittimen päässä olevaa muuttujaa ohjelmoijan on muunnettava osoitin tyyppiin, johon se oikeasti osoittaa. */
Muistipaikoista katso esim. monisteen osat
Yleisin syy osoitinvirheisiin on esim. seuraavankaltainen tilanne:
{ ... int* i; *i = 10; // VIRHE!!! sijoitetaan i-osoittimen päähän 10. ... }
Eli ensin esitellään osoitinmuuttuja, jonka jälkeen yritetään suoraan kirjoittaa (tai lukea) osoittimen päähän. Seurauksena (ainakin Windowsissa) yleensä Access Violation.
Syy: Jos osoittimen arvoksi ei määritellä jonkun tietyn muuttujan osoitetta, osoitin osoittaa varaamattomaan muistipaikkaan!
Korjattu versio:
{ ... int* i; int j; i = &j; // sijoitetaan i-osoittimeen j:n muistipaikka. *i = 10; // sijoitetaan i-osoittimen päähän (eli j-muuttujaan) 10. ... }
Muita syitä voi olla esim. taulukolle (kuten C-merkkijonolle) varatun koon yli osoittaminen, virheellisen osoittimen antaminen scanf-funktiolle tai osoittaminen tilapäiseen muuttujaan esim. aliohjelman sisällä ja samanarvoisen osoittimen käyttö myöhemmin pääohjelmassa. Siis:
int* f() { int j; // tilapäinen muuttuja, vapautetaan funktiosta poistuttaessa int *result = &j; // sijoitetaan result-osoittimeen j:n muistipaikka return result; // VIRHE!!! } int main() { ... int *i = f(); cout << *i; // VIRHE!!! i osoittaa varaamattomaan muistiin. ... }
Viitteet ja osoittimet ovat sisäisesti samankaltaisia, siis eivät varsinaisia muuttujia/olioita, vaan viitteitä muualle. Viitteiden/osoittimien käyttö nopeuttaa parametrinvälitystä, koska välitettävää oliota ei tarvitse kopioida, vaan ainoastaan viite kopioidaan.
Kaikissa "normaaleissa" tilanteissa on periaatteessa mahdollista käyttää viiteparametreja. Kuitenkin, usein samat asiat voidaan hoitaa tehokkaammin käyttämällä suoraan osoittimia, erityisesti tilanteissa, joissa parametreille tehdään muistinvarausoperaatioita tai parametrin arvoa on tarpeen muuttaa funktiossa (esim. taulukon läpikäynti). Lisäksi NULL-osoittimen käytöllä voi helposti kontrolloida virhetilanteita (tosin saman voi periaatteessa hoitaa myös poikkeuksilla).
"Oikeita" sovelluksia kehitettäessä osoittimia joutuu käyttämään jatkuvasti hyvin monenlaisissa tilanteissa, lähtien olemassaolevien funktiokirjastojen käytöstä.
Periaatteessa ei ole. Ks. osoittimet ja referenssit.
Kysymys voisi olla ennemmin: Pitääkö osoittimia tai referenssejä aina käyttää? Omia ohjelmia tehdessä on kohtuullisen helppoa nähdä tilanteet, jolloin osoitin/viiteparametrien käyttö on järkevää tai välttämätöntä. Valinta osoitinten ja refenssien välillä on usein makuasia. Tärkeämpää on selkeä koodi ja johdonmukainen esitystapa.
Osoittimien yhteydessä * tarkoittaa muuttujan esittelyssä, että kyseessä on osoitinmuuttuja. Esim. ks. Yleistä osoittimista.
Osoittimen päässä olevan muuttujan saa selville *-operaattorilla (joka siis nyt on eri merkityksessä kuin osoittimen esittelyssä). Esim:
*:m merkitys riippuu asiayhteydestä. Osoittimen esittelyssä * tarkoittaa, että kyseessä on osoitin. Olemassaolevan osoittimen yhteydessä * tarkoittaa, että haetaan osoittimen päässä oleva muuttuja/olio.
Myös &:n merkitys riippuu asiayhteydestä (ks. esim Moniste
8.5.6: 6-merkillä monta eri merkitystä). Tavallisten muuttujien yhteydessä tarkoittaa muuttujan osoitteen hakemista (joka sitten voidaan sijoittaa osoittimeen).
Kyllä. tosin ko. merkintää voi käyttää vain osoitintyyppisillä muuttujilla, jolloin merkit kumoavat toisensa. Esim:
Funktion parametri on osoitin, joten se esitellään *-merkinnällä.
Kun osoitinmuuttuja esitellään, sen arvosta ei voida tehdä mitään oletuksia (kuten ei perustyyppisten muuttujien arvosta yleensäkään). Osoitinmuuttuja voidaan alustaa suoraan esittelyn yhteydessä. Esim:
Toinen vaihtoehto on esitellä osoitin ensin ja sijoittaa toisen muuttujan osoite siihen myöhemmin. Turvallisinta olisi määrittää osoitin NULL:iksi aina, kun sille ei ole erityisesti määrätty mitään muuttujaa osoitettavaksi. Esim.
Ks. edellinen. Nollaosoitin tarkoittaa, että osoitin ei osoita mihinkään muistipaikkaan. Tämä on hyödyllistä virhetilanteiden käsittelyssä (esim. new-operaattori palauttaa NULL-osoittiminen, jos muistia ei saada varattua) ja ohjelman ohjauksessa muutenkin.
Osoittimeen sijoitettava arvo on muistipaikka, joka saadaan &-operaattorilla. Varsinainen sijoitus hoidetaan aivan normaalisti = -operaattorilla. Ks. aiemmat esimerkit.
Osoittimen osoittaman paikan (eli osoittimen pään) sisältöön pääsee käsiksi *-operaattorilla, jolloin arvoa voi (muuttujan/olion tyypistä riippuen) muuttaa aivan normaalisti. Esim.
Ks. Osoittimista yleensä. Varsinaisten tyyppien ja olioiden lisäksi osoitin voi osoittaa myös toiseen osoittimeen (joka edelleen voi osoittaa osoittimeen tai varsinaiseen tyyppiin. Osoittimen osoittimia käsitellään kuten muitakin osoittimia, mutta nyt *-ja &-merkkien määrä on syytä katsoa tarkasti. Esim.
"Kaikki" kaipaisi hieman lisämäärittelyjä. Periaatteessa minne tahansa muistiin voidaan osoittaa, tietyin rajoituksin osoittimen päässä voi muuttujien tai olioden lisäksi olla myös koodia. Tällöin on käytettävä funktio-tai metodiosoittimia, joiden syntaksi poikkeaa hieman tavallisista osoittimista. Periaate on kuitenkin aivan sama: Määritellään funktiotyyppi, jolle annetaan tietynlaiset argumentit ja tietyntyyppinen palautusarvo, jonka jälkeen osoittimen voi ohjata "mihin tahansa" funktioon, jolla on määrätyntyyppiset argumentit ja palautusarvo. Ks myös moniste 21.6: Funktio-osoitin.
Kysymys kaipaisi käsitteiden määrittelyä. Jos olio luodaan dynaamisesti (new-operaattorilla), siihen on pakko viitata ainakin sen luontihetkellä osoittimella (toki tämän osoittimen voi kääntää osoittamaan johonkin muualle...).
Oikeissa ohjelmistoprojekteissa tulee jatkuvasti esille asioita, joita ei välttämättä ole ikinä käsitelty. Jos jotain outoa tulee vastaan, kannattaa tutkia asiaa itse esim. jostain lähdeteoksesta tai (helpompi tapa) kysyä ystävälliseltä ohjaajalta. :-)
Tässä on havaittavissa muna vai kana -ongelma. Tilanteet, joissa osoittimia todella tarvitaan saattavat ohjelmointikurssin alkuvaiheessa olla tarpeettoman mutkikkaita. Siksi on parempi ensin käydä vain läpi osoittimien syntaksi ja harjoitella aluksi "tylsillä" esimerkeillä. Uskokaa pois, kyllä niille osoittimille vielä käyttöä löytyy...
Harjoitus tekee mestarin. Palautetta, kritiikkiä ja parannusehdotuksista demotehtävistä otetaan hyvin mieluusti vastaan muutenkin.
Kurssi on hyvin pitkälle C++ -painotteinen. Toisaalta keskeiset ideat ovat kaikissa ohjelmointikielissä samat, eli jos osaa hyvin yhden kielen, on kohtuullisen helppoa siirtyä tarvittaessa toiseen.
Väitän, että Ohjelmointikurssi on Tietotekniikan (ja siinä sivussa koko yliopiston) kursseista ylivoimaisesti käytännöllisimmästä päästä. Esimerkit seuraavat.
Osoittimet ja referenssit
Oleellisimmat viitteen ja osoittimen erot ovat, että viite referoi aina samaan muuttujaan, ja että "tyhjiä viitteitä" ei ole olemassa. Tämä on rajoitus osoittimeen verrattuna, mutta toisaalta turvallisempaa.
Pintapuolisesti viitteen ja osoittimen ero näkyy syntaksissa. Esimerkki:
...
int a,b;
int& refa = a; // viitemuuttuja on alustettava esittelyn yhteydessä.
int* pa; // aluksi määrittelemätön osoitin.
pa = &b; // "käännetään" osoitin b-muuttujaan.
pa = &a; // ...ja a-muuttujaan.
refa = b; // VIRHE!!! Viitemuuttuja viittaa a-muuttujaan.
...
Jos funktiolle välittää taulukon (esim. C-merkkijonon) joutuu välttämättä tekemisiin osoittimien kanssa, koska "taulukkoparametri" on oikeasti vain osoitin taulukon alkuun.
Ks. ed. kysymys. Vaikka useimmat tilanteet pystyy periaatteessa tekemään ilman osoittimia, ne ovat käytännössä usein huomattavasti kätevämpiä käyttää kuin viitteet (Erityisesti dynaamiset tietorakenteet).
Osoittimien tarpeellisuus
* ja & -merkkien käyttö
Osoittimen arvo on jonkin muuttujan tai olion muistiosoite. Muistiosoitteen saa ohjelmassa &-operaattorilla. Esim:
{
int* a; // osoitin kokonaislukuun
int b = 5;
a = &b; // sijoitetaan b:n osoite a-osoittimeen
cout << *a; // Tulostetaan 5. (a osoittaa b:hen, jonka arvo on 5)
}
//Olk a esim. osoitin kokonaislukuun ja osoittimen päässä on järkevä muuttuja.
&* a == a == &*&*&* a;
// Myös osoittimen pään arvon voi hakea useammin &*-merkin takaa.
*&* a == *a == *&*&* a;
Koska parametri on osoitin, niin siihen täytyy sijoittaa jokin muistipaikka, siis kutsussa haetaan halutun muuttujan osoite &-operaattorilla.
Ks. myös edelliset.
Osoittimien käytöstä
int a;
int* pa = &a;
int a;
int * pa = NULL; // alustetaan osoitin tyhjäksi
... // koodia
if (pa != NULL ) { ... } // pa on NULL, ei suoriteta
pa = &a; // ohjataan pa osoittamaan a-muuttujaan
int a = 3;
int *b = &a;
a = 5; // muutetaan a:n arvoa normaalisti
*b = 10; // ...ja osoittimen kautta
// Nyt a == *b == 10
Huom. jos osoittimen osoittama tyyppi on tyyppiä const, arvoa ei (tietenkään) voi muuttaa. Esim.
const int a = 3;
const int *b = &a;
a = 5; // VIRHE!!! a on tyyppiä const int
*b = 10; // VIHRE!!! *b on tyyppiä const int
// a == 3 edelleen...
int a = 2;
int b = 1;
int *pa = &a;
int *pb = NULL; // alustetaan aluksi tyhjäksi turvallisuuden takia
int **ppa = &pa; // osoittimen osoitin
*ppa = &b; // käännetään pa-osoitin b-muuttujaan
*pa = 3; // muutetaan b:n arvoa
ppa = &pb; // huom! pb on edelleen NULL
pb = &a;
**ppa = 5; // muutetaan a:n arvoa
Osoittimen osoittimilla voidaan tehdä esim. monipuolisia dynaamisia tietorakenteita.
Toisaalta, jos kyseessä on staattinen tietyn lohkon ( { ... } ) sisällä toimiva olio, siihen ei voi enää lohkosta poistumisen jälkeen viitata, koska olio tuhoutuu lohkon lopussa. Tietysti oliosta voi luoda kopion, jota voi käyttää muuallakin.
Palutetta ja yleisiä aiheita
Linkkejä