Osoittimet¶
Osaamistavoitteet: Tämän materiaalin läpikäytyäsi tiedät miten osoittimia käytetään muistinkäsittelyssä ja mitä merkittävää hyötyä siitä on.
Osoittimet / osoitinmuuttujat ovat tapa osoittaa suoraan ohjelman käytössä olevaa muistia. Osoittimien käyttäminen johtaa tehokkaampaan ja tiiviimpään koodiin, joka on erityisen tärkeää sulautettujen ohjelmoinnissa. Nimensä mukaisesti osoitinmuuttujan arvo on muistipaikan osoite ja ideana on, että koodissa voimme käsitellä tätä muistiosoitetta kuten mitä tahansa muuttujan arvoa.
Osoitinmuuttujan ero "tavalliseen" muuttujaan:
- Muuttujan esittely varaa muistipaikan, jonne voimme tallentaa jonkin arvon. Koodissa sitten muuttujan nimi tulkitaan osoitteeksi tähän muistipaikkaan.
uint16_t a = 0x0042;
Muuttujan a arvo on 0x0042- Osoitinmuuttujan esittely varaa muistipaikan, jonne ihan samoin tallennamme arvon joka tulkitaan muistiosoitteeksi.
uint16_t *b = 0x1080;
Osoitinmuuttujan *b arvo on 0x1080, joka nyt koodissa tulkitaan muistiosoiteeksi 0x1080, josta voimme hakea osoitetun muuttujan arvon (tässä a:n arvo)
Esimerkki. Kuvassa osoitinmuuttuja *b osoittaa muuttujan a muistipaikkaan.
Voidaan sanoa, että "tavallinen" muuttuja osoittaa muistipaikkaan suoraan ja osoitinmuuttuja epäsuorasti.
Tarkastellaan seuraavaa muistin sisältöä, jossa on jo muuttujia. Kuvasta nähdään, että muuttujan
a
osoite muistissa on 0x0030, jossa sijaitsee sen arvo (47). 16-bittinen muuttuja x
on osoitteissa 0x0031-0x0032, koska se tarvitsee kaksi 8-bittistä muistipaikkaa. Taulukkomuuttuja (merkkijono) msg
vastaavasti sijaitsee osoitteissa 0x0033-0x0036. (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äolevassa kuvan esimerkkimuistissa operaatio
&a
palauttaisi osoitteen 0x0030, &x
osoitteen 0x0031 ja &msg
osoitteen 0x0033.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. Sijoitusoperaattori * , eli 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
osoitin_a = 44
sijoittaisi osoittimen osoittamaan muistipaikkaan 44. 3. 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 muistipaikkojen osoitteet a=0x30 ja x=0x31
, koska emme käyttäneet argumenteissa *-operaattoria.Esimerkki. Osoitinmuuttujan käyttö, huomaa %p.
int8_t a = 47;
int8_t *osoitin_a = &a; // sijoitetaan a:n osoite
printf("a:n muistiosoite on %p\n", &a);
printf("osoitin_a:n oma muistiosoite on %p\n", &osoitin_a);
printf("osoitin_a:n osoittama muistiosoite %p\n", osoitin_a);
printf("osoitin_a:n osoittamasta muistiosoitteesta haettu arvo %d\n", *osoitin_a);
// Tulostaa
a:n muistiosoite on 0x7ffdbf42ad8f
osoitin_a:n oma muistiosoite on 0x7ffdbf42ad80
osoitin_a:n osoittama muistiosoite 0x7ffdbf42ad8f // Eli a:n osoite
osoitin_a:n osoittamasta muistiosoitteesta haettu arvo 47
Osoitinmuuttujasta¶
Osoitinmuuttujan sielunelämän esittämiseksi palataan ym. muistin kuvaan.
Osoitinmuuttujan tyyppi¶
Esitelläänpä alla osoitinmuuttuja, jolla pääsemme käsiksi tähäm muistiin. Huomataan, että osoitinmuuttujan (
osoitin_a
) esittelyssä sille annetaan muuttujatyyppi (int8_t
). Miksi, kun sehän on muistiosoite?int8_t a = 47;
int8_t *osoitin_a = &a;
// Nyt *osoitin_a = 47
Nyt, osoitinmuuttujan tyyppi tarvitaan, koska se kertoo osoitetun muuttujan tyypin. 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. Ao. kuvassa on esitelty 16-bittinen muuttuja
x
.// 16-bittisen (kaksi tavua) muuttujan x esittely
int16_t x = 0x1234;
// Nyt, jos osoitin_x olisi tyyppiä int8_t eli yksi tavu
int8_t *osoitin_x = &x;
printf("x=%x", *osoitin_x);
// Tulostaa
x=12
// ..kääntäjä noutaa x:n muistiosoitteesta vain ensimmäisen tavun
// ja toinen tavu (0x34) jää 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);
// Tulostaa
x=12344142
// haettiin x:n kaksi tavua ja lisäksi "liikaa" msg-muuttujan
// kaksi ensimmäistä tavua..
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ä.
(C-kielessä on myös void-tyyppisiä osoittimia, mutta emme mene niihin kurssilla..)
Osoitinmuuttuja muistissa¶
Lisäksi huomataan, että osoitinmuuttuja tarvitsee oman muistipaikan. Nyt tämän muistipaikan koko ei liity osoitettavan muuttujan kokoon, vaan itse muistin kokoon.
Esimerkki. Muistin koko on 65536 muistipaikkaa, eli
2^16
(muistetaan aiemmasta 16-bittinen osoiteväylä). Tällöin tarvitsemme jokaisen muistipaikan yksilöimiseen / osoittamiseen 16-bittisen luvun. Tästä seuraa että (tässä muistissa) osoitinmuuttujan koko on 16-bittiä. Koko on siis vakio, riippumatta osoitettavan muuttujan tyypistä.
// Siis kaikissa näissä osoitinmuuttujien koko on 16 bittiä
// vaikka se osoittaa eri kokoisiin muuttujiin
uint8_t *ptr8 = &a;
uint16_t *ptr16 = &b;
uint32_t *ptr32 = %c;
Okei, otetaan vielä toinen osoittimen käyttöä (toivottavasti) selventävä kuva.
Osoittimien käyttäminen¶
Okei.. hieno homma. Mutta mihin osoitinmuuttujaa sitten tarvitaan?
Osoittimet ja taulukkomuuttujat¶
Tarkastellaan seuraavaksi osoitinmuuttujien ja taulukkojen läheistä suhdetta.
Koska osoitinmuuttujan arvo voi (periaatteessa olla mikä tahansa luku muistin koon rajoissa, niin 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
osoitin_eka_msg:n arvo on 0x0033;
osoitin_toka_msg:n arvo on 0x0034; // +1 tavu, 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
osoitin_eka_num:n arvoksi tulee 0x0037;
osoitin_toka_num:n arvoksi tulee 0x0039; // +2 tavua, koska 16-bittinen luku
C-kielessä itseasiassa taulukon nimi on osoitin sen 1. alkioon . Tästä syystä, kuten tulemme näkemään hetken päästä, funktiokutsuissa usein parametriksi annetaan taulukon nimi. Tällöin funktiomme näkee itseasiassa muistiosoitteen.
Samoin, nimi 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..)
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]; // arvo 0x0033
osoitin_msg+1 = &msg[1]; // arvo 0x0034
osoitin_msg+2 = &msg[2]; // arvo 0x0035
// Koska osoitin on tyyppiä int16_t, sen arvo kasvaa kahden tavun verran
// vaikka indeksi kasvaakin vain yhdellä
osoitin_num = &num[0]; // arvo 0x0037
osoitin_num+1 = &num[1]; // arvo 0x0039
osoitin_num+2 = &num[2];// arvo 0x003B
Eli tässä osoittimen arvo kasvaa aina taulukkomuuttujan alkion koon verran.
Nyt, nouto-operaatiot vastaavasti
*
-operaattorilla.*(osoitin_msg) = msg[0]; // arvo 'A'
*(osoitin_msg+1) = msg[1]; // arvo 'B'
*(osoitin_msg+2) = msg[2]; // arvo 'C'
*(osoitin_num) = num[0]; // arvo 1
*(osoitin_num+1) = num[1]; // arvo 2
*(osoitin_num+2) = num[2]; // arvo 3
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 local_a, int8_t local_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_
. Ongelmana tässä on, että kopioinnin takia arvot vaihtuu funktion sisäisissä muuttujissa, eikä argumentteina olevissa main-funktion muuttujissa main_a
ja main_b
. Asia korjataan käyttämällä osoittimia funktion argumentteina. Nyt osoitin
local_a
osoittaa samaan main_a
:n muistipaikkaan ja molemmilla muuttujilla operoidessa siinä muistipaikassa oleva arvo muuttuu!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);
// Argumentiksi muuttjien arvon sijasta niiden osoitteet!
// Huomaa &-operaattori
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; // talleta a:n muistipaikan arvo
*local_a = *local_b; // sijoita b:n muistipaikan arvo a:n muistipaikan arvoon
*local_b = temp; // sijoita a:n muistipaikan arvo b:n muistipaikkaan
}
Funktion vaihda-kutsua vastaisi seuraavat sijoitukset:
int8_t *local_a = &main_a;
int8_t *local_b = &main_b;
Nyt esimerkin ohjelma tulostaa:
Ennen: main_a=14 ja main_b=68 Jälkeen: main_a=68 ja main_b=14
Muistin säästöä¶
Samalla tavoin osoittimilla voi myös merkittävästi säästää muistia!
Alla välitämme funktiolle 2000-merkin taulukon, josta kääntäjä joutuu tekemän kopion. Tämähän nyt periaatteessa toimii työasemissa, mutta sulautetussa laitteessa isojen taulukkojen kopiointi funktiokutsussa on 1) hidasta ja 2) vie turhaan ison osan laitteen muistista.
// prototyyppi
void kasittele(char txt[2000]);
int main() {
char essee[2000];
kasittele(essee);
return 0;
}
void kasittele(char txt[2000]) {
// koodia ...
}
Tässä osoitin pelastaa meidät, kun annamme argumentiksi taulukon ensimmäisen alkion osoitteen ja taulukon pituuden.
void kasittele(char *txt, int16_t txt_len);
int main() {
char essee[2000];
kasittele(essee,2000);
return 0;
}
Ilman taulukon pituutta emme siis funktion sisällä tiedä, missä kohti taulukko loppuu muistissa..
Tässä toki funktio tekee kopion osoitinmuuttujan arvosta, mutta onhan se kooltaan paljon pienempi kuin itse taulukko. Vrt. osoitinmuuttujan koko (esimerkiksi) 4 tavua vs. taulukon koko 2000 tavua.
Usean arvon palauttaminen¶
Toinen hieno etu osoittimista on että, nythän vaihda()-funktiossa yllä palautimme funktiosta useamman kuin yhden arvon, eli a ja b jotka vaihtuivat keskenään. Eli kun funktion argumentit ovat muistiosoitteita, voimme funktiossa tehdä niiden sisällölle mitä haluamme! Juurikin, muistiin voidaan kirjoittaa "palautusarvot" muuttujien muistipaikkoihin suoraan. Tässä ikäänkuin ohitimme itse muuttujat käsittelemällä niiden muistipaikkoja suoraan.
void vaihda(int8_t *a, int8_t *b) {
int8_t temp = *a;
*a = *b;
*b = temp;
}
C-kieli todellakin on laiteläheinen kieli. Jännää!
Merkkijonojen käsittely¶
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
strlen
- Merkkijonojen (merkkien) vertailu
strncmp
- Merkin tai merkkijonon etsiminen toisesta merkkijonosta
strstr
- Merkkijonojen tai niiden osien kopiointi
strncpy
- Merkkijonojen liittäminen toisiinsa
strncat
- Jopa merkkijonon purkaminen osiin!
strtok
- .. ja paljon muuta
#include <stdio.h>
#include <string.h>
int main() {
char nimi[] = "Judge Dredd";
printf("Nimen %s pituus on %d\n",nimi, strlen(nimi));
return 0;
}
Nyt kaikki nämä funktiot ottavat merkkijonoja sisäänsä niiden muuttujanimen (osoitin taulukkoon) kautta.
ja vielä esimerkki strtok-funktiosta, koska siitä on sulatetuissa järjestelmissä usein hyötyä mm. laitteen langattomassa viestinnässä saatujen viestien purkamisessa. Usein viesti on ASCII-muotoista (jopa ihmisen luettavaa) merkkijonoa tyyliin
1234567890,temperature,27,C
. Tätä viestiformaattia kutsutaan yleisesti Comma-separated value-formaatiksi. Sulautetuissa järjestelmissä ohjelmamme voi esimerkiksi haluta erottaa lämpötilan numeerisen arvon, jotta sitä voidaan käsitellä ohjelmassa.#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-muodossa. Nyt jos erotettava osa halutaan tulkita lukuarvona, tarvitsee sitä kuvaava alimerkkijono 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. Mutta palataan tähän..Hox! Ilmainen vinkki harjoitustyöhön. Näitä merkkijonon käsittely- ja muunnosfunktiota kannattaa siis käyttää, kun lähetetään viestiä tai puretaan vastaanotettuja viestejä SensorTagin langattomalla radiolla.
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 osoittimista syvemmin kiinnostuneille lisätietoa.
Anna palautetta
Kommentteja materiaalista?