Lukijan oletetaan tuntevan osoittimiin (pointtereihin) liittyvät perusmääritelmät kuten sen, miten osoitintyyppinen muuttuja määritellään. (Ks. TKK:n Tik-76.002-kurssin aineistossa olevaa kirjoitusta Osoitinmuuttujista ja niiden käytöstä.)
Osoittimia käytetään usein dynaamisten tietorakenteiden
muodostamiseen ja käsittelyyn, ja silloin osoitin yleensä
viittaa johonkin tietueeseen (structure).
Perusasioiden havainnollistamiseksi tarkastellaan seuraavassa
kuitenkin sellaista yksinkertaista tilannetta, jossa
osoitin viittaa C-kielen perustyyppiä kuten double
olevaan objektiin.
Olettakaamme siis, että ohjelmassa on määrittely
double *p;Tällöinhän muuttaja
p
on tyyppiä
"osoitin double
-tyyppiseen objektiin" kun taas
*p
on tyyppiä double
.
Se tavallinen virhe on, että
varataan tilaa osoittimelle p
ja sitten ruvetaan kirjoittamaan jotain
muuttujaan *p
ilman,
että sille olisi varattu tilaa.
Yksinkertaisimmassa tapauksessa tähän tapaan:
double *p; *p = 1.0;Tästä seuraa usein ohjelman kaatuminen johonkin virhetilanteeseen kuten
Bus error
tai Segmentation fault
.
Toiminta riippuu kuitenkin varsin monista asioista, sekä
ohjelmasta että käytettävästä tietokoneesta. On täysin mahdollista,
että ohjelma ei kaadu välittömästi mutta sotkeutuu niin, että
myöhemmin saadaan virheilmoitus jostakin ohjelman muusta kohdasta,
jossa ei ole mitään vikaa.
Käytännössä tilanne on usein mutkikkaampi, koska osoittimia
käytetään dynaamisten tietorakenteiden yhteydessä eikä koodista
välttämättä näe suoraan, että siinä esiintyy sentapainen rakenne
kuin *p
yllä.
Jos esimerkiksi on määritelty
typedef struct {char nimi[20]; int koodi;} henkilotietue; henkilotietue *henkilo[100];jolloin
henkilo
on taulukko, jossa on osoittimia
määrätyntyyppisiin tietueisiin, ja sitten yritetään vaikkapa
lukea dataa tyyliin
for(j=0;(henkilo[0]->nimi[j]=getchar()) != '\n'; j++);niin kyse on olennaisesti samanlaisesta tilanteesta kuin edellä kuvatussa yksinkertaisessa tapauksessa. Merkitseehän C-kielen sääntöjen mukaan rakenne
henkilo[0]->nimi
samaa kuin
(*(henkilo[0])).nimi
,
joten meillä vain on p
:n tilalla henkilo[0]
ja yksinkertaisen sijoituksen tilalla operaatio, jossa muutetaan
osoittimen osoittaman tietueen yhden kentän yhtä alkiota.
Virhe on siinä, että esimerkiksi määrittely
double *p;varaa tilaa vain ja ainoastaan yhdelle osoittimelle, ei lainkaan tilaa
double
-tyyppiselle objektille.
Vaikka osoittimet sattuisivatkin jossakin koneessa olemaan
samankokoisia (tai pienempiä) kuin double
-tyyppiset
objektit, niin tila on varattu nimenomaan osoittimelle eikä sitä
tilaa voi käyttää niiden objektien tallettamiseen, joihin
osoitin voi viitata.
Periaatteelliselta kannalta olisi oikeampaa sanoa, että
kielen määritelmän kannalta muuttujaa *p
ei edes ole olemassa
pelkän määrittelyn double *p;
jälkeen.
Puhe tilan varaamattomuudesta ehkä konkretisoi asioita: ohjelma
yrittää kirjoittaa tilaan, jota ei ole asianmukaisesti varattu
ohjelman käyttöön. Käytännössä ohjelma saattaa silloin kirjoittaa
sellaiseen osoitteeseen, joka ei lainkaan ole ohjelman käytössä
olevalla muistialueella, jolloin käyttöjärjestelmä ehkä havaitsee
tilanteen ja katkaisee suorituksen ja antaa virheilmoituksen.
Voi myös käydä niin, että ohjelma käytännössä kirjoittaa sellaiseen
tilaan, joka on varattu jollekin muulle muuttujalle, jolloin
ohjelma "satunnaisesti" muuttaa "satunnaisesti valitun" muuttujan arvoa.
Voipa käydä niinkin, että ohjelma kirjoittaa oman koodinsa päälle
ja alkaa käyttäytyä erittäin hullusti.
Itse asiassa tähän virheeseen useimmiten liittyy myös se, että
osoitinmuuttujalle p
ei ole annettu mitään arvoa, ei ainakaan
järkevää arvoa. Silloin jo itse viittaus *p
on laiton.
Edellä olevassa tilanteessa on kyse tällaisesta, koska muuttujan
p
määrittelyssä sille ei anneta alkuarvoa eikä sille
muutenkaan anneta arvoa ennen viittausta *p
.
Seuraava on luonnollisestikin laillista:
double *p; double x; p = &x; *p = 1.0;Tällöinhän on, ennenkuin
*p
:hen viitataan,
paitsi määritelty muuttuja p
myös annettu sille
arvo &x
eli x
:n osoite ja
x
:llä on asianmukainen määrittely.
Toisin sanoen tällöin *p
viittaa laillisesti
johonkin double
-tyyppiseen objektiin, jolle
on varattu tila (muuttujan x
määrittelyllä).
Käytännössä yleisempää kuin se, että osoitinmuuttuja asetetaan
viittaamaan tavalliseen muuttujaan kuten yllä, on se, että
halutaan luoda objekti, johon osoitinmuuttuja
viittaa. Tässä kirjoituksessa kuvatun virheen perussyy lienee se,
että kuvitellaan sellaisen luomisen sisältyvän osoitinmuuttujan
määrittelyyn ikäänkuin määrittely double *p;
varaisi tilan sekä p
:lle että *p
:lle.
Tätä väärinkäsitystä ehkä ruokkii C-kielen määrittelyjen rakenne:
kun kerran määrittely double x;
määrittelee
double
-tyyppisen muuttujan x
, jolle
voidaan esim. seuraavaksi sijoittaa arvo, niin on helppoa johtua
ajattelemaan, että määrittely
double *p;
määrittelee myös
double
-tyyppisen muuttujan *p
,
jolle voidaan esim. seuraavaksi sijoittaa arvo.
Mutta oikea tulkinta on, että se määrittelee
double *
-tyyppisen muuttujan p
ja että
tästä seuraa, että voidaan käyttää double
-tyyppistä
muuttujaa *p
jos ja kun muuttuja
p
on ensin asetettu osoittamaan johonkin
(double
-tyyppiseen) objektiin.
Tilan varaaminen uudelle objektille tehdään tavallisimmin
C-standardissa määritellyllä malloc
-funktiolla.
Se kuuluu stdlib
-kirjastoon eli sen käyttö
edellyttää direktiiviä
#include <stdlib.h>Funktio
malloc
on kuulunut C-kieleen alusta alkaen,
mutta sen tarkka määritelmä on aikojen kuluessa muuttunut.
Nykyisen C-standardin mukaan sen tyyppi on void *
(aiemmin char *
)
eli se palauttaa "geneerisen osoittimen", jonka voi muuntaa
mihin tahansa osoitintyyppiin. Muutosta ei C-standardin mukaisessa
järjestelmässä yleensä tarvitse tehdä erikseen, vaan se tapahtuu
automaattisesti.
Funktion malloc
argumentti on luku, joka ilmoittaa
varattavan objektin koon muistin perusyksiköinä, käytännössä
tavuina (bytes). Tämä koko tavallisimmin saadaan operaattorin
sizeof
avulla:
sizeof(
t)
antaa tyyppiä t olevien objektien koon tavuina.
Esimerkki:
double *p; p = malloc(sizeof(double)); *p = 1.0;Tällöin
malloc
-funktion kutsu varaa tilan objektille,
jonka koko on double
-tyyppisen objektin viemä tila,
ja palauttaa osoittimen kyseiseen tilaan. Koska kutsu on sijoituksen
oikeana puolena, niin sen tyyppi muuntuu automaattisesti sitä
tyyppiä olevaksi, jota oikea puoli (tässä p
) on,
siis esimerkissämme tyyppiin double *
.
Kutsu voidaan suorittaa myös osoitintyyppisen muuttujan määrittelyn yhteydessä, esim.
double *p = malloc(sizeof(double)); *p = 1.0;käyttäen hyväksi sitä, että C:ssä muuttuja voidaan alustaa määrittelynsä yhteydessä. Koska alustava lauseke ei ole vakio vaan sisältää funktion kutsun, tämä on kuitenkin sallittua vain, jos määrittely on paikallinen eli jonkin funktion sisällä.
Jos on määritelty esimerkiksi taulukko, jonka alkiot ovat osoittimia, ja halutaan luoda kutakin alkiota kohden objekti, johon se viittaa, niin on parasta tehdä asia silmukalla. Esimerkki:
#define NIMPIT 20 #define TIETLKM 1000 #include <stdlib.h>> typedef struct {char nimi[NIMPIT]; int koodi;} henkilotietue; henkilotietue *henkilo[TIETLKM]; int i; for(i=0; i<TIETLKM; i++) henkilo[i] = malloc(sizeof(henkilotietue));Periaatteessa ohjelman tulisi aina
malloc
-funktion
kutsun jälkeen tarkistaa, onnistuiko kutsu. Se voi nimittäin
epäonnistua, jos haluttua määrää muistia ei voi varata; tavallisimmin
tämä johtuu villiintyneestä rekursiosta. Virhetilanteissa
malloc
palauttaa arvon NULL
, ja tällöin
kaikki yritykset käyttää malloc
in palauttamaa arvoa
osoittimena ovat laittomia. Tarkistus voidaan tehdä esimerkiksi
seuraavaan tapaan, olettaen, että error
on ohjelmassa
määritelty funktio, joka antaa sopivan virheilmoituksen ja lopettaa
ohjelman suorituksen (esim. kutsulla exit(1)
):
double *p; p = malloc(sizeof(double)); if(p == NULL) error(); *p = 1.0;
Jukka Korpela 9.6.1997