Osoittimet¶
Osaamistavoitteet: Tämän materiaalin läpikäytyäsi tiedät miten osoittimia käytetään osoittamaan muistia ja mitä merkittävää hyötyä siitä on.
C-kielessä on yksi muuttujatyyppi, mitä emme ole vielä käsitelleet eli osoitinmuuttuja. Nimensä mukaisesti osoitinmuuttujan arvo on muistipaikan osoite keskusmuistissa. Osoitteen avulla, kuten itse muuttujan nimelläkin, voidaan ohjelmassa hakea sitten varsinainen muistipaikan arvo. Osoittimien käyttäminen usein johtaa tiiviimpään ja tehokkaampaan koodiin. Tämä on tärkeää sulautettujen ohjelmoinnissa, joten niihin perehdymme seuraavaksi.
Osoitinmuuttujan ero "tavalliseen" muuttujaan:
- Muuttujan esittely varaa muistipaikan jollekin arvolle, jota tarvitsemme ohjelmassa. Muuttuja koodissa osoittaa (on viittaus) tähän muistipaikkaan, jossa oleva luku tulkitaan tulkitaan sen muuttujan arvoksi.
- Osoitinmuuttujan esittely varaa muistipaikan (toiselle) muistiosoitteelle. Osoitinmuuttujan muistipaikassa sijaitseva arvo on edelleen jokin luku, mutta ero on 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.
Tarkastellaan seuraava muistin sisältöä, jossa on jo muutamia muuttujia. Kuvasta nähdään, että muuttujan
a
osoite muistissa on 0x0030, jossa sijaitsee sen arvo (47). Muuttuja x
on osoitteissa 0x0031-0x0032, koska se 16-bittisenä muuttujana tarvitsee kaksi 8-bittistä muistipaikkaa. Taulukkomuuttuja msg
vastaavasti alkaa osoitteesta 0x0033. (Tähän muistiin palaamme alla esimerkeissä.)
Osoitinoperaattorit¶
C-kieli tarjoaa 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 = 12;
printf("a:n arvo on %d ja sen muistiosoite on %p",a,&a); // %p on tulostusmääre osoitinmuuttujalle
..jonka tulostus kertoo, että
a:n arvo on 12 ja sen muistiosoite on 000000000023FE47
(64-bittinen arkkitehtuuri).Ylläkuvatussa esimerkkimuistissa 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 alustaa osoitinmuuttujan viittaamalla muistiosoitteeseen &-operaattorilla. Jos osoitinmuuttuja jätetään alustamatta, sen arvoksi laitetaan yleensä vakio NULL
.int8_t *osoitin_a = &a;
int8_t *osoitin_b = NULL;
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
Vrt. Lause
printf("a=%x ja x=%x", osoitin_a, osoitin_x);
tulostaisi siis a=0x30 ja x=0x31
, koska ei ole *-operaattoria.3. Operaattoria * käytetään myös arvon sijoitukseen osoitettuun muistipaikkaan:
int8_t *osoitin_a = &a;
*osoitin_a = 44;
Tällöin osoitin_a:n osoittamaan muistipaikkaan (0x30) tallennetaan luku 44.
Vrt. Sijoitus ilman *-operaattoria, esimerkiksi
osoitin_a = 44
, sijoittaisi osoittimen muistipaikan arvoksi 44, jonka jälkeen osoitin osoittaisi *-operaattorilla muistipaikkaan nro 44. 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? Noh, osoitinmuuttujan koko on aina muistiosoitteen koko eikä osoitettavan muuttujan koko. . Esimerkin 16-bittisessä arkkitehtuurissa siis osoitinmuuttuja vie 2 tavua, 64-bittisessä PC:ssä 8 tavua, jne. Esimerkki. Osoittimen muistipaikan osoite, huomaa %p.
printf("osoitin_a:n muistiosoite on %p", &osoitin_a);
..joka tulostaa "osoitin_a:n muistiosoite on 000000000023FE30".
Sitten taas osoitinmuuttujan tyyppi kertoo osoitetussa muistiosoitteessa olevan muuttujan tyypin. Tästä tyypistä kääntäjä siis tietää millaisen muuttujan se hakee osoitteesta. Eli osoitinmuuttuja on syytä esitellä samalla muuttujatyypillä kuin sen osoittama muuttuja!
Asiaa valaiseva esimerkki. Ylläolevassa kuvassa on 16-bittinen muuttuja
x
.// 16-bittisen muuttujan x esittely
int16_t x = 0x1234;
// Nyt, jos osoitin_x olisi tyyppiä int8_t..
int8_t *osoitin_x = &x;
printf("x=%x", *osoitin_x);
Joka tulostaa: x=12 .. eli nyt noudetaan x:n muistiosoitteesta vain yksi tavu
ja toinen tavu (0x34) jäisi 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 msg-muuttujan muistialueelle,
siis taulukosta arvot 0x41 ja 0x42.
(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ä.)
Okei, otetaan vielä osoittimen toimintaa koodissa (toivottavasti) selventävä kuva.
Osoittimet ja taulukkomuuttujat¶
Tarkastellaan seuraavaksi osoitinmuuttujien ja taulukkojen läheistä suhdetta.
Nyt voimme tietysti esitellä taulukolle osoitinmuuttujan ja alustaa sen osoittamaan haluttuun alkioon:
char *osoitin_eka_msg = &msg[0]; // Osoittaa taulukon ensimmäiseen alkioon: arvo A
char *osoitin_toka_msg = &msg[1]; // Osoittaa taulukon toiseen alkioon: arvo B
Nyt,
osoitin_eka_msg:n arvo on 0x0033;
osoitin_toka_msg:n arvo on 0x0034; // +1 koska 8-bittinen luku
Sama toimii myös tietenkin taulukolle, jonka arvot ovat muita kuin tavuja:
int16_t *osoitin_eka_num = &num[0]; // Osoittaa taulukon ensimmäiseen alkioon: arvo 1
int16_t *osoitin_toka_num = &num[1]; // Osoittaa taulukon toiseen alkioon: arvo 2
Nyt,
osoitin_eka_num:n arvoksi tulee 0x0037;
osoitin_toka_num:n arvoksi tulee 0x0039; // +2 koska 16-bittinen luku
C-kielessä itseasiassa taulukon nimi on osoitin sen 1. alkioon , joka voidaan antaa osoitinmuuttujalle arvoksi! Eli seuraavat sijoitukset ovat laillisia
char *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
(Kuten huomataan, taulukon nimen käyttämisessä osoittimena on pientä erikoisuutta, jota emme kurssilla lähde purkamaan..)
Osoittimien käyttäminen¶
Okei.. hieno homma. Mutta mihin osoitinmuuttujaa sitten tarvitaan?
Osoitin-aritmetiikkaa¶
Osoittimilla voidaan liikkua taulukoissa (ja muistissa myös) hyvin kätevästi aritmeettisilla operaatioilla, koska ne ovat lukuja.
Kun
Kun
char *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ä.char *osoitin_msg = msg;
int16_t *osoitin_num = num;
// Koska osoitin on tyyppiä int8_t, sen arvo kasvaa 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, sen arvo kasvaa 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 taulukkomuuttujan 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. Yleensäkään sijoituslausekkeen vasemmalla puolen ei voi olla laskutoimituksia ilman suoritusjärjestyksen muuttamista.
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 argumenttia vastaavaan muuttujaan. Tarkastellaan asiaa esimerkin kautta.. allaoleva koodi ei toimi. Miksi?
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;
}
Joka tulostaa.
Ennen: main_a=14 ja main_b=68 Jälkeen: main_a=14 ja main_b=68
Muistamme funktio-kappaleesta, että C-kielessä funktio loi argumenteista kopiot paikallisiin muuttujiin. Joita yllä kuvaa etuliite
local_
. Nyt siis arvot vaihtuu näissä lokaaleissa muuttujissa eikä main-funktio muuttujissa main_a
ja main_b
!Asia korjataan käyttämällä osoittimia funktion argumentteina. Nyt osoitin
local_a
osoittaa main_a
:n muistipaikkaan ja siellä oleva arvo vaihtuu.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
Sääntö siis on, että osoittimilla operoimme suoraan muistissa olevan muuttujan arvon kanssa. Näin ollen tässä riittää, että 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. Keskusmuistihan 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, eli kaksi muuttujan arvoa jotka vaihtuivat keskenään. Jos siis funktion argumentit ovat muistiosoitteita, voimme funktiossa tehdä niiden sisällölle mitä haluamme! Näin muistiin voidaan kirjoittaa "palautusarvot" muuttujien muistipaikkoihin suoraan riippumatta siitä kuinka monta argumenttia on. 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ä 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ää!
Merkkijonojen käsittelyyn¶
Yksi oleellinen osoittimien hyöty ilmenee merkkijonojen käsittelyssä. Standardikirjaston osana string.h määrittelee joukon hyödyllisiä funktioita merkkijonojen kanssa pelaamiseen:
- Merkkijonon pituuden selvittäminen
- Merkkijonojen (merkkien) vertailu
- Merkin tai merkkijonon etsiminen toisesta merkkijonosta
- Merkkijonojen tai niiden osien kopiointi
- Merkkijonojen liittäminen toisiinsa
- Jopa merkkijonon purkaminen osiin!
.. ja kaikki nämä funktiot ottavat merkkijonoja sisäänsä niiden osoittimien kautta.
Otetaanpa esimerkki tästä viimeisestä, koska siitä on sulatetuissa järjestelmissä usein hyötyä mm. laitteen langattomassa viestinnässä saatujen viestien purkamisessa. Usein viesti on ASCII-muotoista ihan ihmisen luettavaa tekstiä tyyliin
1234567890,temperature,27,C
, josta haluamme erotella esimerkiksi lämpötilan numeroarvon. Tällöin puhutaan yleisesti Comma-separated value-formaatista. #include <string.h>
#include <stdio.h>
int main () {
char str[] = "Alpha,Bravo,Charlie,Delta"; // Tutkittava merkkijono
const char sep[] = ","; // Erotin
char *token; // Paikkamerkki
/* Erota ensimmäinen osa */
token = strtok(str, sep);
/* Erota loput osat */
while( token != NULL ) {
printf("%s\n",token);
/* Kutsutaan samaa funktiota uudelleen */
token = strtok(NULL, sep);
}
return(0);
}
Kokeile mitä esimerkki tulostaa!
Esimerkissä täytyy huomata, että erotetut pätkät ovat vielä merkkijono-muotoisia, joten esimerkiksi numeroarvo tarvitsee vielä muuntaa sopivaan kokonais- tai likulukumuuttujatyypiin, jota sitä voi käsiitellä koodissa numerona. Tähän standardikirjasto stdlib.h tarjoaa funktiot
atoi
, atol
, ja atof
. Tosin, sulautetuissa nämä on saatettu korvata jollain toisella funktiolla tai niiden toteutuksessa olla rajoituksia. Hox! Ilmainen vinkki harjoitustyöhön. Näitä merkkijonon käsittely- ja muunnosfunktiota kannattaa siis käyttää kun puretaan SensorTag:n harjoitustyö-serveriltä saamia viestejä.
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?