C/Osoittimet
Muuttujalla on muistissa paikka, jonne sen arvo on tallennettu. Kun luot muuttujan, muuttuja saa pinosta (osa tietokoneen muistia) itselleen tilaa. Osoitin (engl. pointer, josta toinen nimi pointteri) on muuttuja, joka sisältää muistiosoitteen toisen muuttujan muistipaikkaan. Osoitinmuuttujan tyyppi kertoo osoitettavan muuttujan tyypin. Osoittimen arvo voi myös olla NULL', milloin se ei osoita mihinkään. Onkin turvallista alustaa osoittimen alkuarvoksi NULL. NULL-osoitin ei koskaan osoita mihinkään (esim. muistipaikka 0 jota ei voi varata). NULL on esikääntäjän makro joka määritellään stdlib.h
-otsaketiedostossa.
Osoitin itse on kooltaan sellainen, että siihen mahtuu mikä tahansa muistiosoite sisälle, usein neljä (32-bittiset arkkitehtuurit) tai kahdeksan (64-bittiset) tavua, mutta et voi tehdä mitään oletuksia tämän pohjalta. Osoittimen arvoa ei voi myöskään tallentaa esim. int-tyyppiseen muuttujaan, mutta ruudulle sen voi tulostaa printf
:n muotoilusäännöllä %p
. Osoittimet ja huono osoittimien hallinta voivat aiheuttaa pahoja, vaikeasti havaittavia virheitä. Osoittimien hallinta kannattaa opetella hyvin ennen vaativampaa työtä.
Osoitintietotyyppit merkitään merkillä * osoitettavan tyypin perässä, ennen muuttujaa, eli float-tyyppiseen arvoon osoittavan muuttujan tyyppi on float*, määrittely esim. float* p = NULL;.
Muuttujan osoitteen saa selville &-operaattorilla, joka laitetaan muuttujan nimen eteen lausekkeessa, jossa osoitetta halutaan käyttää. Ennestään tunnetussa osoitteessa olevaa, eli osoittimen osoittamaa tietoa taasen käytetään laittamalla osoittimen eteen operaattori *. Älä kuitenkaan sekoita tätä operaattoria tietotyypissä käytettäviin *-merkkeihin.
void on poikkeus. Normaalisti void-tyyppistä muuttujaa ei voi määritellä, mutta osoitin void-tyyppiin on sallittu ja ilmaisee, että osoittimen osoittamaa tyyppiä ei ole määritelty. C:ssä tällaisia osoittimia voi ilman erityistä muunnosta sijoittaa mihin tahansa osoitinmuuttujaan, ja päinvastoin. void-osoitinta ei voi seurata suoraan; siitä ei voi lukea arvoa, eikä siihen voi kirjoittaa arvoa. Minkä tahansa (arvo-)osoittimen voi muuntaa void *-tyypiksi ja takaisin alkuperäiseksi tyypiksi, ja se toimii silloin täysin alkuperäisen osoittimen tavoin.
Esimerkkejä osoittimien käytöstä
muokkaa#include <stdio.h> int main() { int x; int* y; y = &x; //asetetaan osoittimen y -arvoksi x:n muistipaikka *y = 32; //y:n osoittaman muistipaikan arvo on nyt 32 (X:n arvo on siis nyt 32) //Testitulosteet printf(" x = %d\n", x); printf("&x = %p\n", (void *)&x); printf(" y = %p\n", (void *)y); return 0; }
Tuplaosoitus
muokkaaSaattaa olla hankala ymmärtää milloin tarvitaan osoitinta, joka osoittaa toisen osoittimen osoittamaan muistipaikkaan. Sovelluksia kuitenkin löytyy paljon dynaamisen muistinkäsittelyn puolelta.
#include <stdio.h> int main() { int **pp_i; //Luodaan osoitin, joka osoittaa int* -tyyppiseen muuttujaan. int *p_i; int i = 0; p_i = &i; pp_i = &p_i; *p_i = 3; printf("%d\n", *p_i); // Antavat saman printf("%d\n", **pp_i); // tulosteen return 0; }
Määreet
muokkaaMuuttujien const- ja volatile-määreet (vakio ja herkästi muuttuva) pätevät myös osoittimiin. On tärkeää huomata, että määreet määritellään erikseen jokaiselle osoitustasolle; usein monimutkaisemmat osoitinmäärittelyt sekoittavat jopa kokeneita C-ohjelmoijia.
int *a; /* muuttuja a, joka osoittaa kokonaislukuun. sekä osoitinta että kokonaislukua, johon se osoittaa, voi muuttaa. */ const int *a; /* muuttuja a, joka osoittaa kokonaislukuvakioon. osoitinta voi muuttaa, muttei sitä kokonaislukua, johon se osoittaa. */ int * const a; /* vakio a, joka osoittaa kokonaislukuun. osoitettua kokonaislukua voi muuttaa, mutta osoitinta ei. */ const int * const a; /* vakio a, joka osoittaa kokonaislukuvakioon. ei osoitinta eikä sen osoittamaa kokonaislukua voi muuttaa. */
volatile-osoittimien kohdalla kääntäjä ei saa olettaa, ettei arvo muutu. Esimerkiksi seuraavanlaista koodia saattaa nähdä, kun yritetään käyttää suoraan laitteistorekistereitä alhaisen tason koodissa:
volatile char *ready = ...; while (!*ready) { /* ilman volatilea kääntäjä saisi olettaa, että ready-osoittimen osoittama arvo ei muutu 'yhtäkkiä', eli tästä tulisikin loputon silmukka! */ }
C99-versiossa on kolmas vain osoittimiin liittyvä määre restrict. Jos funktio ottaa useamman restrict-osoitinparametrin, kääntäjä saa olettaa, etteivät ne viittaa samaan muistipaikkaan. Tästä on hyötyä erinäisten optimointien kanssa.
Funktio-osoittimet
muokkaaArvo-osoitinten lisäksi on myös funktio-osoittimia, jotka nimensä mukaisesti osoittavat funktioon. Näiden syntaksi on monimutkainen, mutta perusperiaate on seuraava:
- kirjoita funktion esittely
- poista parametrien nimet
- aseta funktion nimen ympärille sulkeet, ja vielä nimen eteen tähti sulkeiden sisään
Esimerkki: funktio, joka ottaa kaksi int-kokonaislukua ja palauttaa sellaisen, esiteltäisiin int funktio(int luku1, int luku2). Sitä vastaava funktio-osoitin on int (*osoitin)(int, int).
Funktio-osoitinta voi kutsua näin: (*funktio)(1, 2). Myös pelkkä funktio(1, 2) toimii. Funktion osoitteen voi ottaa samalla &-osoiteoperaattorilla kuin arvonkin, mutta myös pelkkä funktion nimi riittää esimerkiksi antaessa funktio-osoitinargumenttia.
typedef on kätevä funktio-osoitintyypeille:
typedef int (*Vertailufunktio)(int, int); int suurin_luku_kolmesta(Vertailufunktio vertaa, int luku1, int luku2, int luku3);
Funktio-osoitinta ei voi välttämättä sijoittaa tavalliseen eli arvo-osoittimeen lainkaan, vaikka se olisi void *. Joillain alustoilla tämä voi hyvinkin toimia, mutta C-standardi ei takaa sitä. Funktio-osoittimen voi sen sijaan sijoittaa mihin tahansa toiseen funktio-osoitintyyppiin ja takaisin alkuperäiseen tyyppiin, ja tämän taataan toimivan. Funktiota ei saa kuitenkaan koskaan kutsua vääräntyyppisen osoittimen kautta.
Ohjeita osoittimien käyttöön
muokkaa- Osoitin on kelvollinen vain niin kauan kuin sen osoittama tieto on olemassa (muistia ei vapautettu muuhun käyttöön). Huomaathan, että esim. funktion eli aliohjelman paikallisten muuttujien käyttämä muisti vapautuu, kun sen suoritus päättyy! Seuraava johtaa kelvottomaan osoittimeen, jotka aiheuttavat usein kummallisia bugeja:
int *pituusasetus() { int pituus = 8; /* oletuspituus on 8. */ return &pituus; /* palautamme osoittimen niin että käyttäjä voi muuttaa asetusta, ja että muistamme muutokset. */ } /* palautimme osoittimen muistipaikkaan, joka vapautuu - voi ei! */
- Osoitinta käytetään usein tiedon turhan kopioinnin (hidasta) välttämiseksi, mutta jos tietoa on vain hyvin vähän (kuten yksi muuttuja), ei tällä itse asiassa säästetä mitään, sillä itse osoitinkin täytyy kuitenkin kopioida.
- Ei kannata koskaan määritellä useita osoittimia yhdessä lausekkeessa. Osoittimen tason ilmaisevat tähdet määritellään erikseen joka muuttujalle. Esimerkiksi int* p1, p2; määrittelee osoittimen p1 kokonaislukuun ja kokonaisluvun p2, ei osoitinta p2 (olisi int *p1, *p2).
- Varmista ohjelmasi muistinkäsittelyn toimivuus (myös virheellisillä syötteillä) ensin tutkimalla lähdekoodia huolellisesti, sitten muistivuotodebuggerin avulla.
C-ohjelmointikieli |
---|
Yksinkertainen C-kielinen ohjelma — Muuttujat — Operaattorit — Kommentit — Ohjausrakenteet — Funktiot — Osoittimet — Dynaaminen muistinvaraus — Taulukot — Merkkijonot — Tietueet — Esikääntäjä — Otsikkotiedostot C-kielen varatut sanat — Standardikirjasto — Aiheesta muualla |