Osoittimet¶
C-kielessä on yksi muuttujatyyppi, mitä emme ole vielä käsitelleet eli osoitinmuuttuja. Nimensä mukaisesti osoitinmuuttujan arvo on muistipaikan, ts. jonkin muuttujan, osoite. Osoittimien käyttäminen usein johtaa tiiviimpään ja tehokkaampaan koodiin, jota sulautettujen ohjelmoinnissa tarvitaan, joten niihin perehdymme seuraavaksi.
Tarkastellaan seuraava muistin sisältöä, jossa on jo muutamia muuttujia. Kuvasta nähdään, että muuttujan
a
osoite muistissa on 0x0030, muuttuja x
on osoitteissa 0x0031-0x0032 ja taulukkomuuttuja msg
alkaa osoitteesta 0x0033. Käytämme tässä nyt 16-bittisiä muistiosoitteita esimerkin vuoks. Muistipaikan koko silti on 8 bittiä.(Tähän muistiin palaamme alla esimerkeissä.)
Osoitinmuuttujan ero "tavalliseen" muuttujaan:
- Muuttujan esittely varaa muistipaikan jollekin lukuarvolle jota tarvitsemme ohjelmassa. Muuttuja koodissa osoittaa tähän muistipaikkaan, jossa oleva arvo tulkitaan tyypin mukaiseksi arvoksi.
- Osoitinmuuttujan esittely varaa muistipaikan (toiselle) muistiosoitteelle. Osoitinmuuttujan muistipaikassa sijaitseva arvo on edelleen numero, mutta ero on siinä että koodissa osoitinmuuuttujan arvo tulkitaan muistipaikan osoitteeksi.
Osoitinmuuttuja tarjoaa ikään kuin alemman tason mekanismin tiedon käsittelyyn ohjelmassa, kun pelaillaan muuttujien osoitteilla eikä suoraan niiden arvoilla! Voidaan sanoa, että "tavallinen" muuttuja osoittaa muistipaikkaan suoraan ja osoitinmuuttuja epäsuorasti.
Osoitinoperaattorit¶
C-kieli tarjoaa meille kaksi operaattoria osoitinmuuttujien kanssa toimimiseen. Operaattoreilla pystytään selvittämään muuttujien osoitteet sekä noutamaan osoitettujen muistipaikkojen arvoja.
Operaattori &¶
Operaattoria & voidaan käyttää kysymään miltä tahansa muuttujalta sen osoite muistissa operaatiolla
&muuttujan_nimi
. Katsotaanpa koodiesimerkki luennoitsijan (verovähennyskelpoisessa) koti-PC:ssä:int8_t a = 47;
printf("a:n muistiosoite on %p",&a); // %p on tulostusmääre osoitinmuuttujalle
..jonka tulostus kertoo, että
a:n muistiosoite on 000000000023FE47
(64-bittinen arkkitehtuuri).Ylläkuvatussa muistissa operaatio
&a
palauttaisi osoitteen 0x0030 ja &x
osoitteen 0x0031.Operaattori *¶
Operaattorilla
*
on kolme käyttötarkoitusta. 1. Osoitinmuuttuja esitellään operaattorilla
*
, eli *muuttujan_nimi
. Koska osoite on luku, voimme sijoittaa sen osoitinmuuttujaan. int8_t *osoitin_a = &a;
2. Käytämme osoitinmuuttujaa ja *-operaattoria myös noutamaan osoitinmuuttujan osoittaman muistipaikan arvon. Ylläolevasta kuvasta:
int8_t *osoitin_a = &a;
int16_t *osoitin_x = &x;
printf("a=%d ja x=%4x", *osoitin_a, *osoitin_x);
..joka tulostaa
a=47 ja x=1234
. 3. Operaattoria * käytetään myös sijoitukseen epäsuorasti:
int8_t *osoitin_a = &a;
*osoitin_a = 44;
Tällöin osoitin_a:n osoittamaan muistipaikkaan (eli a:n muistipaikkaan) tallennetaan luku 44.
Huomaa sijoitus ilman *-operaattoria
osoitin_a = 44
sijoittaisi osoittimen omaan muistipaikkaan arvon 44 (joka tulkittaisiin siis muistiosoitteeksi). Osoittimen toiminta¶
Okei, otetaan vielä osoittimen toimintaa koodissa (toivottavasti) selventävä kuva.
Huomaa! Osoitemuuttujalle varataan aina muistista arkkitehtuurin vaatiman verran tavuja. Tässä leikki-16-bittisessä arkkitehtuurissa siis osoitinmuuttuja vie 2 tavua, 64-bittisessä koti-pc:ssä 8 tavua, jne.
Osoitinmuuttujasta¶
Osoitinmuuttujan sielunelämän esittämiseksi palataan ym. muistin kuvaan.
Huomataan, että osoitin muuttujan esittelyssä sille annetaan tyyppi. Miksi, kun sehän on muistiosoite?
int8_t a = 47;
int8_t *osoitin_a = &a;
Yllä osoitettavan muistipaikan koko on 8-bittiä
int8_t a
. Varaamme sitä osoittamaan myös 8-bittisen osoitinmuuttujan int8_t *osoitin_a
. Mutta.. meillähän oli 16-bittiset muistiosoitteet käytössä.. miten 16-bittinen muistiosoite mahtuu 8-bittiseen osoitinmuuttujaan?Itseasiassa osoitinmuuttujan tyyppi kertoo osoitetussa muistiosoitteessa olevan muuttujan tyypin. Tästä tyypistä kääntäjä siis tietää millaisen muuttujan se hakee osoitteesta.
Asiaa valaiseva esimerkki. Ylläolevassa kuvassa on 16-bittinen muuttuja
x
.// Muuttujan x esittely
int16_t x = 0x1234;
// Nyt, sos osoitin_x olisi tyyppiä int8_t, se noutaisi x:n muistiosoitteesta vain yhden tavun.
int8_t *osoitin_x = &x;
printf("x=%x", *osoitin_x);
Joka tulostaa: x=12 .. nyt jäi toinen tavu (34) hakematta!
// Jos osoitin_x olisi tyyppiä int32_t, se noutaisi x:n muistiosoitteesta neljä tavua.
int32_t *osoitin_x = &x;
printf("x=%x", *osoitin_x);
Joka tulostaa: x=12344142 .. eli mentäisiin jo muuttujan msg[] muistialueelle!
Eli osoitinmuuttuja on syytä esitellä samalla muuttujatyypillä kuin sen osoittimana muuttuja!
Osoitinmuuttujan koko on muistiosoitteen koko eikä osoitettavan arvon koko. Tämä koko siis riippuu muistin arkkitehtuurista.
Tietenkin osoitinmuuttujalle on tietysti myös oma muistipaikka ja sen osoite.
printf("osoitin_a:n muistiosoite on %p", &osoitin_a);
..joka tulostaa
osoitin_a:n muistiosoite on 000000000023FE30
}}}.
(Ylläolevassa esimerkissä on vaihtelua sen mukaan, mikä on tavu-järjestys tietokoneessa. int8_t-tyyppinen osoitin voi tulostaa myös tavun 0x34 ja int32_t bittinen osoitin jotain muuta. Mutta tässä käytämme kurssilla sovittua tavujärjestystä.)
== Osoittimet ja taulukkomuuttujat ==
Tarkastellaan seuraavaksi osoitinmuuttujien ja taulukkojen läheistä suhdetta.
<!image=tkj-osoittimet-taulukot|alt=Muuttujia muistissa>
Nyt voimme tietysti esitellä taulukolle osoitinmuuttujan ja alustaa sen osoittamaan haluttuun alkioon:
{{{highlight=c
char *osoitin_eka_msg = &msg[0]; // Osoittaa taulukon ensimmäiseen alkioon
char *osoitin_toka_msg = &msg[1]; // Osoittaa taulukon toiseen alkioon
Nyt,
osoitin_eka_msg:n arvo on 0x0033;
osoitin_toka_msg:n arvo on 0x0034;
Sama toimii myös tietenkin taulukolle, jonka arvot ovat muita kuin tavuja:
char *osoitin_eka_num = &num[0]; // Osoittaa taulukon ensimmäiseen alkioon
char *osoitin_toka_num = &num[1]; // Osoittaa taulukon toiseen alkioon
Nyt,
osoitin_eka_num:n arvo on 0x0037;
osoitin_toka_num:n arvo on 0x0039;
C-kielessä itseasiassa taulukon nimi on osoitin sen 1. alkioon , joka voidaan antaa osoitinmuuttujalle arvoksi! Eli seuraavat sijoitukset ovat laillisia
int8_t *osoitin_msg = msg;
int16_t *osoitin_num = num;
Esimerkiksi, nämä kaikki operaatiot palauttavat saman muistiosoitteen!
char sukunimi[20];
char *osoitin = sukunimi;
printf("%p\n", &sukunimi[0]);
printf("%p\n", sukunimi);
printf("%p\n", &sukunimi);
printf("%p\n", osoitin);
Joka tulostaa (luennoitsijan kotikoneessa)...
000000000023FE20
000000000023FE20
000000000023FE20
000000000023FE20
Osoittimien käyttäminen¶
Okei.. hieno homma taas kerran. Mutta mihin osoitinmuuttujaa sitten tarvitaan?
Osoitin-aritmetiikkaa¶
Osoittimilla voidaan liikkua taulukoissa (ja muistissa myös) hyvin kätevästi aritmeettisilla operaatioilla.
Kun
Kun
int8_t *osoitin = msg
, niin osoitin + 1
osoittaa msg-taulukon seuraavaan alkioon, osoitin +2
sitä seuraavaan, jne. Hienous tässä on se, että tämä toimii riippumatta taulukon muuttujatyypistä.int8_t *osoitin_msg = msg;
int16_t *osoitin_num = num;
// Koska osoitin on tyyppiä int8_t, se kasvaa aina yhden tavun verran
osoitin_msg = &msg[0] = 0x0033
osoitin_msg+1 = &msg[1] = 0x0034
osoitin_msg+2 = &msg[2] = 0x0035
// Koska osoitin on tyyppiä int16_t, se kasvaa aina kahden tavun verran
// vaikka indeksi kasvaakin vain yhdellä
osoitin_num = &num[0] = 0x0037
osoitin_num+1 = &num[1] = 0x0039
osoitin_num+2 = &num[2];= 0x003B
Eli osoittimen arvo kasvaa aina taulukon alkion koon verran, esimerkiksi
int32_t
-muuttujatyypin tapauksessa se kasvaisi 4 tavua kerrallaan.Nyt, nouto-operaatiot vastaavasti
*
-operaattorilla.*(osoitin_msg) = msg[0];
*(osoitin_msg+1) = msg[1];
*(osoitin_msg+2) = msg[2];
*(osoitin_num) = num[0];
*(osoitin_num+1) = num[1];
*(osoitin_num+2) = num[2];
Sulkeet tarvitaan osoitteen ympärille jotta kääntäjä ymmärtäisi +1:n kuuluvan osoitteeseen.
Tästä seuraa, että nyt meillä on kaikki aritmeettiset operaatiot käytössä osoittimien kanssa!
Esimerkki, osoitin silmukkamuuttujana:
int16_t *osoitin;
// Tämä lause tulostaa num-taulukon alkiot osoittimia käyttäen
for (osoitin = num; osoitin < num+3; osoitin++) {
printf("%d\n", *osoitin);
}
Tottakai myös operaattori
--
, osoittimien vähennyslasku jne muutkin toimivat!Funktion argumentteina¶
Kuten kaikki muitakin muuttujatyyppejä, myös osoittimia voidaan käyttää funktion argumentteina.
Tässä on oleellinen asia siis, että funktion argumenttina annetaan muuttujan osoite
&
-operaattorilla. Näin muuttujan osoite tallentuu funktion sisäiseen argumentti-muuttujaan. Esimerkki.
void vaihda(int8_t *a, int8_t *b); // prototyyppi
int main() {
int8_t main_a = 14;
int8_t main_b = 68;
printf("Ennen: main_a=%d ja main_b=%d\n", main_a, main_b);
vaihda(&main_a, &main_b);
printf("Jälkeen: main_a=%d ja main_b=%d\n", main_a, main_b);
}
void vaihda(int8_t *local_a, int8_t *local_b) {
int8_t temp = *local_a;
*local_a = *local_b;
*local_b = temp;
}
Nyt
vaihda
-funktiota kutsuttaessa muuttujien main_a
ja main_b
osoitteen kopio siis tallentuu osoitin-muuttujiin local_a
ja local_b
. Tätä vastaisi seuraavat sijoitukset.int8_t *local_a = &main_a;
int8_t *local_b = &main_b;
Esimerkin ohjelma tulostaa:
Ennen: main_a=14 ja main_b=68 Jälkeen: main_a=68 ja main_b=14
Okei.. miksi tässä tarvitaan osoittimia? Syy on siinä, että kuten funktioiden yhteydessä todettiin, niin C-kieli tekee funktion argumenteista kopion paikalliseen muuttujaan. Eli yllä kävisi ilman osoittimia niin, että vaihda-funktion kopioissa (sisäiset muuttujat a ja b) olevat arvot vaihtuisivat, koska funktio operoi argumentikseen saamilla muuttujilla. Mutta main-funktiossa olevien muuttujien (main_a ja main_b), arvot eivät muuttuisi koska niillä ei operoida. Kokeile ja muokkaa funktiota niin, ettei se käytä osoittimia. Osoittimilla operoimme suoraan muistissa olevan muuttujan arvon kanssa. Riittää, että tiedämme ja välitämme muuttujan osoitteen.
Nyt, jos meillä olisi funktion parametriksi tulossa vaikkapa iso taulukko
char kirja[2000]
, ei olisi varsinkaan sulautetuissa järjestelmissä mitään järkeä tehdä funktiolle omaa sisäistä kopiota 2000-merkkisestä taulukosta. Muistihan siinä loppuisi äkkiä kesken. Osoitin pelastaa meidät, kun voimme antaa parametriksi vain taulukon ensimmäisen alkion osoitteen ja sen koon. Muutenhan funktio ei tietäisi missä kohti taulukko loppuu muistissa..void tee_jotain(char *taulukko, int16_t taulukon_koko);
Ja joo, funktion parametriksi annetusta osoittimesta tehdään funktiossa oma sisäinen kopio. Mutta, se ei haittaa koska sen osoittama muistipaikka säilyy samana kopiossa. Ja osoitinmuuttuja vie muistia vain esimerkiksi 4 tavua, joka on huomattavasti vähemmän kuin useimpien taulukoiden koko.
Toinen etu osoittimista on että, nythän vaihda-funktiossa yllä palautimme funktiosta useamman kuin yhden arvon. Jos funktion argumentit ovat muistiosoitteita, voimme funktiossa tehdä niiden sisällölle mitä haluamme! Näin sinne muistiin voidaan kirjoittaa palautusarvot muuttujien muistipaikkoihin suoraan riippumatta siitä kuinka monta argumenttia on. Jälleen ohitimme itse muuttujat käsittelemällä niiden muistipaikkoja suoraan osoittimilla.
void vaihda(int8_t *a, int8_t *b) {
int8_t temp = *a;
*a = *b;
*b = temp;
}
Tässä vaihda-funktiossa return-lauseella, jos siinä sellainen olisi, saisimme palautettua vain joko argumentteina annetun a:n tai b:n arvon, mutta emme kumpaakin. C-kieli todellakin on laiteläheinen kieli. Jännää!
Lopuksi¶
Tässä materiaalissa oli kurssille riittävä johdatus osoittimiin C-kielessä. Niillä on vielä muitakin salaisuuksia.. kuten komentoriviargumentit, (moniulotteiset) osoitintaulukot ja funktio-osoittimet, joita emme käy kurssilla läpi.
Oppikirjoista löytyy toki kiinnostuneille lisätietoa.
Anna palautetta
Kommentteja materiaalista?