lauantai 23. elokuuta 2008

C ja muisti osa 2 - osoittimet eli pointterit.

osa 1:ssä pohdittiin hieman muistia noin yleisesti, ja käytettiin osoittimia ihan sujuvasti. Koska aloittelevilla C koodareilla on usein ongelmia ymmärtää osoittimia eli pointtereita, ajattelin lisätä jo ennestään valtaisaan osoitin-tutoriaali tulvaan omani.

Eli kuten aiemmin kerroin, C-kielessä muistin käsittely on oikeastaan kaiken a ja o. Suora pääsy tiettyyn muistipaikkaan, jonne jotain dataa on jo talletettu mahdollistaa ylimääräisen kopioinnin välttämisen, eli siis nopeuttaa ohjelmaa ja vähentää muistin kulutusta. Samalla se kuitenkin on myös C-ohjelman haastavimpia asioita, ja virheelliset muistiaccessit aiheuttavatkin todella ikäviä ja vaikeasti paikannnettavissa olevia virheitä. Mutta eiköhän tässä nyt tullut pohjustusta tarpeeksi, menen siis seuraavaksi itse asiaan.

Eli kuten ykkösosassa totesin, käyttöjärjestelmä paloittelee muistin lohkoihin, jotka se numeroi. Eli varsinainen data (esim. muuttujan arvo) on aina talletettu johonkin tiettyyn lohkoon, jota edustaa lohkon numeerinen osoite. normaalia muuttujaa käytettäessä, koodarin ei tarvitse välittää osoitteesta, sillä kääntäjä piiloittaa osoitteen käytön. Tämä kuitenkin tarkoittaa sitä, että mikäli sama data halutaan tallettaa myös toiseen muuttujaan, on data kopioitava toiseen muistilohkoon johon kääntäjä liittää uuden muuttujan nimen.

Esim:


int muuttuja1;
int muuttuja2;

muuttuja1=1; //laitetaan arvo 1 muistiin johonkin lohkoon,
// jota edustaa nimi muuttuja1

muuttuja2=muuttuja1; //kopioidaan data toiseen muistilohkoon,
// jota edustaa nimi muuttuja2


Joskus olisi kätevää pystyä linkittämään nimi muuttuja2 samaan muistilohkoon johon nimi muuttuja1 on linkitetty. Tällöin muuttuja1:tä muutettaessa, myös muuttuja2:n arvo luonnollisesti muuttuu. (koska kumpikin osoittaa samaan dataan, ei toiseen muistilohkoon kopioituun dataan). Tämä on mahdollista pointtereiden avulla.

Pointteri on yksinkertaisuudessaan muuttuja, joka sisältää muistipaikan osoitteen. Lisäksi pointteri sisältää muistipaikkaan talletetun datan tyypin, jotta kääntäjä tietäisi miten monta tavua muistipaikasta on luettava, jotta saadaan selville data johon pointteri osoittaa. (osassa 1 totesimme, että C-kielen eri tietotyypit vaativat eri määrän tilaa.) Nyt tarkkana, näytän miten pointteri tehdään. Ja ikävä kyllä, jälleen syntaxissa on kohta, joka saattaa tuntua aloittelijasta sekavalta...


int muuttuja1;
int *muuttuja2;

muuttuja1=1;
muuttuja2=&muuttuja1;


Näyttää lähes samalta kuin edellinen esimerkki. Tarkkaavainen lukijani (eli et sinä Vesku, vaan Kari), huomaa, että erona on nyt * ja & merkkien käyttö.

otetaan helpompi ensin. Eli &-merkki. &-merkillä saadaan selville muuttujan (tai funktion) (alku)osoite. Toinen &-merkin käyttötarkoitus on bitwise and operaatio, mutta ei nyt vaivata päätämme sillä... Eli kun tahdomme käyttää muuttujan osoitetta johonkin, laitamme muuttujan nimen eteen &-merkin (C++:ssa & merkkiä voidaan myös käyttää funktion argumentin edessä, funktion prototyypissä, kun tahdomme... Äh. Antaa olla, tämä tuto koskekoon vain C:tä).

Sitten *. Nyt tarkkana. Äsken käytin *-merkkiä vain yhdessä kohdassa, mutta oikeasti sillä on kaksi osoittimiin liittyvää käyttötarkoitusta. Ensin kun esittelemme, eli luomme osoittimen, *-merkki kuuluu olla muuttujan nimen edessä, kertomassa että tahdomme muuttujan olevan osoitin. datatyyppi osoittimen edessä kertoo, minkä tyyppistä dataa muistipaikka johon osoittimen laitamme sojottamaan sisältää.

Tämän jälkeen kun tahdomme tallettaa osoitteita muistipaikkaan, emme käytä *-merkkiä. osoittimen esittelyn jälkeen, *-merkki kertoo, että tahdomme käpistellä DATAA joka muistipaikassa on. Esim:


int muuttuja1;
int *muuttuja2;

muuttuja1=1;
muuttuja2=&muuttuja1;
*muuttuja2=*muuttuja2+3;
printf("%d",muuttuja1);


Mitähän kyseinen funktio tulostaisi? Käydäämpä tämä tarkasti läpi.
Ensin luomme tavallisen muuttujan tyyppiä int.
Sitten luomme osoittimen nimeltä muuttuja2, ja kerromme, että se tulee osoittamaan int tyyppiseen dataan.
Talletamme muuttuja1:een arvon 1.
Sitten, oikealta vasemmalle luettuna:
otamme &-merkillä muuttuja1 sisältämän datan (eli luvun 1) osoitteen. Ts. Sen muistipaikan osoitteen, jossa muuttuja1:n sisältämä data on talletettuna. Talletamme tämän osoitteen muuttuja2:een, eli pointteriimme.
Sitten jälleen oikealta vasemmalle:
Lisäämme luvun 3, *muuttuja2:een. Eli siihen dataan joka on talletettu muistipaikkaan johon muuttuja2 osoittaa. olipa hankala lause, mutta kun luette sen 3-5 kertaa (Vesalla menee 5), sisäistätte kyllä asian. Nyt meillä siis on luku 1+3, eli 4. Tämän talletamme nyt *muuttuja2:een, eli siihen muistipaikkaan johon muuttuja2 osoittaa. Ts. muistipaikkaan jossa on muuttuja1:n arvo talletettuna. Nyt siis muuttuja1 onkin muuttunut 1:stä 4:ksi.
Ja niinpä viimeinen printf lause tulostaa näytölle numeron 4.





Huokaistaanpa nyt siis hetki, ja kuutioidaan mielessämme mihin tätä voi käyttää...

No niin. Funktio lienee toiselle teistä lukijani jo tuttu käsite. (En tarkoita sinua Vesa). Mietitäänpä hetkinen miten funktion parametrit välitetään funktion sisälle. Ihan oikein Kari, ne välitetään kopioimalla arvo uuteen muistipaikkaan, ja kertomalla tämä uusi muistipaikka funktion sisäiselle muuttujalle. Esim:



typedef struct kauheastidataa
{
int eka;
int toka;
int kolmas;
.
.
.
int sadasviideskymmeneskolmas;
}kauheastidataa;

int main()
{
kauheastidataa dataa;
dataa.eka=1;
dataa.toka=2;
dataa.kolmas=3;
.
.
.
dataa.sadasviideskymmeneskolmas=153;
tulosta_data(dataa);
}

void tulosta_data(kauheastidataa data)
{
printf
(
"eka=%d,
toka=%d,
...,
sadasviideskymmeneskolmas=%d",
data.eka,
data.toka,
...,
data.sadasviideskymmeneskolmas
);
}


Mitä edellinen esimerkki tekisi (siis jos se olisi loppuun asti kirjoitettu, eli ... paikat olisi asianmukaisesti täytetty, älä Vesa koeta kääntää sitä tuollaisenaan)?

Funktion kutsun kohdalla, koko kauheastidataa structi kopioitaisiin uuteen muistilohkoon. Tämä ei kuitenkaan olisi kovin järkevää, sillä kauheastidataa sisältää kauheastidataa, ja kun ohjelma turhaan kopioi kauheastidataa, se hidastuu tarpeettomasti, syö enemmän muistia, ja aiheuttaa pahennusta loppukäyttäjässä. Tämän kopioinnin välttämiseksi voisimme käyttää osoitinta alkuperäiseen kauheastidataa structiin, ja antaa siis tämän kauheastidataa structin muistiosoite tulostusfunktiolle. Tällöin ainoa mitä kopioidaan, on tuo kyseinen muistiosoite. Eli teemme seuraavasti:




typedef struct kauheastidataa
{
int eka;
int toka;
int kolmas;
.
.
.
int sadasviideskymmeneskolmas;
}kauheastidataa;

int main()
{
kauheastidataa dataa;
dataa.eka=1;
dataa.toka=2;
dataa.kolmas=3;
.
.
.
dataa.sadasviideskymmeneskolmas=153;
tulosta_data(&dataa);
}

void tulosta_data(kauheastidataa *data)
{
printf
(
"eka=%d,
toka=%d,
...,
sadasviideskymmeneskolmas=%d",
*(data.eka),
*(data.toka),
...,
*(data.sadasviideskymmeneskolmas)
);
}


Structit tarjoavat myös -> operaattorin, jota voidaan käyttää *(structi.jäsen) asemasta. Eli printtifunktion voisi kirjoittaa myös:


void tulosta_data(kauheastidataa *data)
{
printf
(
"eka=%d,
toka=%d,
...,
sadasviideskymmeneskolmas=%d",
data->eka,
data->toka,
...,
data->sadasviideskymmeneskolmas
);
}


Ja vielä yksi maininnan arvoinen seikka.
oletko ikinä miettinyt miten merkkijonot talletetaan C-kielessä? (Kysymys ei koske sinua Vesa...). Mieti tarkasti, oletko ikinä nähnyt esim. seuraavaa:


char *merkkijono="merkkejä";
printf("%s", merkkijono);


Kun tuota tarkemmin tutkii, on merkkijonojen syvin olemus oikeastaan aika selvä. Eli itse asiassa C:n merkkijonojen käsittely tapahtuu luomalla osoitin char tietotyyppiin. Sitten C:n merkkijonoja käsittelevät funktiot on rakennettu niin, että ne aloittavat merkkijonon käsittelyn paikasta johon char osoitin osoittaa, ja olettavat merkkijonon jatkuvan kunnes kohtaavat merkin '\0' (eli (char)0, eli arvon 0x00) jossain muistipaikassa. Ts. merkkijonoja käsiteltäessä aletaan paikasta johon osoitin osoittaa, ja luetaan muistia eteenpäin 8-bitin eli yhden tavun palasissa, tulkiten jokainen luettu arvo merkiksi ASCII taulukon osoittamalla tavalla, kunnes törmätään tavuun, jossa kaikki 8 bittiä ovat nollia. Tässä siis piilee vaara. Kun käsittelet mekrrijonoja, varmista aina että viimeinen merkki on '\0'. Muuten tuloksena on .... jotain ei toivottua.

Ja nyt, mene kokeilemaan. Sitten kun linux koodarit saavat aikaan ensimmäisen kaatunisen virheilmoituksen "Segmentation Fault" kanssa, (ja windows koodarit muuten vaan crashin), tulkaa lukemaan seuraava pätkä muistin varaamisesta ;)
(Koetan kirjoitella senkin tämän viikonlopun aikana, tai viimeistään alkuviikolla).

Ei kommentteja: