Osoittimet¶
Osaamistavoitteet: Tämän materiaalin läpikäytyäsi tiedät miten osoittimia käytetään C-kielessä ja mitä merkittävää hyötyä niiden käytöstä on muistinhallinnassa laiteläheisessä ohjelmoinnissa.
Otetaanpa alkuun vähän kertausta. Kun esittelemme ohjelmassamme muuttujan, vaikkapa
int8_t x = 42; tapahtuu seuraavaa:- Kääntäjä varaa muistista muistipaikan, jonka koko riippuu muuttujatyypistä. Tässä yksi tavu.
- Kääntäjä tallettaa varattuun muistipaikkaan muuttujan alustusarvon, eli tässä luvun 42.
- Kääntäjä pitää kirjaa siitä, missä muuttujan arvo säilytetään, ja tuottaa konekieliset käskyt sen käsittelemiseksi. Ohjelmasta ja arkkitehtuurista riippuen arvo voidaan säilyttää muistissa tai väliaikaisesti suorittimen rekisterissä. Kun muuttuja sijaitsee muistissa, sillä on muistiosoite, jota voidaan käsitellä osoittimen avulla. Palaamme yksityiskohtiin myöhemmin kurssin tietokonearkkitehtuuriosuudessa.
Osoitinmuuttuja¶
Osoittimet / osoitinmuuttujat (myös pointteri, engl. pointer) ovat tapa osoittaa / viitata suoraan ohjelmasta itsestään sen käytössä olevaan muistiin. Aikaisemmin osoitimme muistiin esittelemällä ohjelmassa muuttujia, joiden arvoja käsittelimme koodissa ikäänkuin "suoraan". Sen sijaan osoitinmuuttujalle voimme ohjelmassa asettaa muistipaikan, minne se osoittaa ja käsitellä siellä olevaa arvoa. Tällä tavoin pääsemme käsiksi mihin tahansa muistipaikkaan ohjelmalle varatussa muistissa.
Selvennetään asiaa kuvitteellisen esimerkin kautta, jossa olemme ohjelmassa esitelleet ja alustaneet kaksi muuttujaa
a ja osoitin_a.uint8_t a = 0x42;
uint8_t *osoitin_a = &a; // &-operaattori
Okei tässä nyt ei ole (vielä) mitään meille outoa, kaksi muuttujaa
a ja osoitin_a, joilla on omat muistipaikat muistissa (kuvitteelliset muistiosoitteet 0x8E ja 0x8F):| Muuttujatyyppi | Muuttujan nimi | Muistipaikan osoite | Arvo |
| uint8_t | a | 0x8E | 0x42 |
| uint8_t* | osoitin_a | 0x8F | 0x8E |
Nyt eroa syntyykin siitä, että miten näiden muuttujien arvot tulkitaan:
- Muuttujan
aarvo0x42ymmärretään ohjelmassa sellaisenaan. - Osoitinmuuttujan
osoitin_aarvo0x8Etulkitaan tähtioperaattorin*avulla muistiosoitteeksi.
Esimerkkiä tarkastellessa huomaamme, että osoitinmuuttujan
osoitin_a arvo onkin itseasiassa muuttujan a muistipaikan osoite! Tällöin sanotaan että se osoittaa / viittaa muuttujaan a. Osoittaminen tarkoittaa, sitä että osoitinmuuttujaa käytettäessä ikäänkuin hypätään sen arvon kertomaan muistipaikkaan. Itse muuttujaa a voimme edelleen käsitellä "tavallisen" muuttujan tavoin, mikään ei sen suhteen muutu.Esimerkki. Katsotaas miten käy seuraavassa:
#include <stdio.h>
#include <inttypes.h> // Johdetut muuttujatyypit täältä
int main() {
uint8_t a = 0x42;
uint8_t *osoitin_a = &a;
printf("Muuttujan a arvo %x\n",a);
printf("Muuttuja osoitin_a osoittaa arvoon %x\n",*osoitin_a);
// Muutetaan a:n arvoa sijoituksella
a = 0x56;
printf("Muuttuja osoitin_a osoittaa arvoon %x\n",*osoitin_a);
// Muutetaan a:n arvoa osoittimen kautta sijoituksella
*osoitin_a = 0x78;
printf("Muuttujan a arvo %x\n",a);
return 0;
}
Ja tämähän tulostaa:
Muuttujan a arvo 42 Muuttuja osoitin_a osoittaa arvoon 42 Muuttuja osoitin_a osoittaa arvoon 56 Muuttujan a arvo 78
Osoitinmuuttujat tarjoavat epäsuoran tavan osoittaa jollekin toiselle muuttujalle varattua muistia. Kuten tulemme näkemään, tästä on paljon hyötyä tehokkaan ja tiiviin koodin kirjoittamisessa, mikä on erityisen tärkeää sulautettujen järjestelmien ohjelmoinnissa resurssirajoitusten vuoksi.
Osoittimen täytyy sisältää suoritettavan operaation kannalta kelvollinen muistiosoite. Tavallisesti tämä osoite saadaan ohjelman toisesta muuttujasta tai oliosta. Sulautetuissa järjestelmissä osoittimia voidaan käyttää myös tiettyjen laitteisto-osoitteiden, kuten muistiinkuvattujen oheislaiterekisterien, käsittelyyn.
Käytämmekin tämän luentokappaleen loppuosan selvittämään mitä hyötyä tällaisesta kikkailusta on.
Osoitinoperaattorit¶
Ensin kuitenkin hieman C-kielen syntaksia. Kieli tarjoaa kaksi operaattoria osoitinmuuttujien kanssa toimimiseen. Operaattoreilla voidaan selvittää minkä tahansa muuttujan osoite, alustaa osoitinmuuttujia haluttuun osoitteeseen sekä noutaa osoitettujen muistipaikkojen arvot.
Operaattori &¶
Operaattoria
& käytetään kysymään miltä tahansa muuttujalta sen osoite muistissa. Operaattorin syntaksi on &muuttujan_nimi. Katsotaanpa koodiesimerkki luennoitsijan verovähennyskelpoisessa koti-PC:ssä:int8_t a = 12;
printf("a:n arvo on %d ja muistiosoite %p",a,&a);
..jonka tulostus kertoo, että
a:n arvo on 12 ja muistiosoite 000000000023FE47 (suorittimessa 64-bittinen arkkitehtuuri).Operaattori *¶
Operaattori *¶
Merkintä
* esiintyy osoittimien yhteydessä kahdessa tärkeässä käyttötavassa.1. Esittelyssä
* ilmaisee, että muuttuja on osoitin:int8_t *osoitin_a = &a;
Tässä
osoitin_a esitellään osoittimeksi, joka osoittaa int8_t-tyyppiseen arvoon.HOX: Jos osoitin esitellään mutta sitä ei alusteta, sen arvo on määrittelemätön. Se ei automaattisesti osoita kelvolliseen muistipaikkaan. Tällaisen osoittimen dereferointi voi siis johtaa määrittelemättömään toimintaan (eli C-kieli ei määrittele, mitä ohjelma tällöin tekee; ohjelma voi kaatua, tuottaa odottamattomia tuloksia tai näyttää toimivan oikein).
int main() {
int8_t *pointer_a; // Esitelty mutta ei alustettu
// pointer_a ei sisällä osoitetta, jota voisimme turvallisesti käyttää
return 0;
}
Jos meillä ei vielä osoittimen määrittelyn yhteydessä ole kelvollista osoitetta, osoitin kannattaa alustaa arvolla
NULL:int8_t *pointer_a = NULL;
NULL-osoitin ilmaisee yksiselitteisesti, ettei osoitin tällä hetkellä osoita kelvolliseen olioon. Sitä ei saa dereferoida.2. Lausekkeessa
* on dereferointioperaattori. Sen avulla käsitellään oliota, joka sijaitsee osoittimen sisältämässä muistiosoitteessa.Dereferointia voidaan käyttää sekä osoitetun olion muuttamiseen että sen arvon noutamiseen.
Esimerkiksi muuttujan arvoa voidaan muuttaa osoittimen kautta:
int8_t a = 47;
int8_t *osoitin_a = &a;
*osoitin_a = 23; // muutetaan osoittimen kautta a:n arvoa
Tässä
*osoitin_a viittaa muuttujaan a sen muistiosoitteen kautta. Sijoitusoperaattori = sijoittaa tämän jälkeen arvon 23 kyseiseen olioon.Nyt muuttujan
a arvo on 23.Sen sijaan uuden osoitteen sijoittaminen itse osoitinmuuttujaan muuttaa sitä, mihin osoitin osoittaa:
int8_t a = 47;
int8_t b = 23;
int8_t *osoitin_a = &a;
osoitin_a = &b;
Tämän jälkeen
osoitin_a osoittaa muuttujaan b muuttujan a sijaan.Dereferointia voidaan käyttää myös osoitetussa muistipaikassa olevan arvon noutamiseen:
uint16_t x = 0xBEEF;
uint16_t *osoitin_x = &x;
printf("x=%x\n", *osoitin_x);
Tämä tulostaa:
x=beef
Yhteenvetona:
osoitinsisältää muistiosoitteen.*osoitinkäsittelee kyseisessä osoitteessa olevaa oliota.&muuttujapalauttaa muuttujan muistiosoitteen.
Osoitinmuuttujan tyyppi¶
Yllä olevista esimerkeistä huomaamme, että osoitinmuuttujalle annetaan aina tietotyyppi sen esittelyn yhteydessä. Mutta miksi? Eikö osoitin ole vain muistiosoite? Tietotyyppi kertoo kääntäjälle minkä tyyppiseen muuttujaan tai arvoon osoitin osoittaa. Tämä määrittää muun muassa sen, kuinka paljon dataa käsitellään, kun osoitin dereferoidaan.
Esimerkiksi:
uint8_t byte = 0x78;
uint16_t word = 0x5678;
uint32_t longword = 0x12345678;
uint8_t *pointer_byte = &byte; // osoitin 8-bittiseen arvoon
uint16_t *pointer_word = &word; // osoitin 16-bittiseen arvoon
uint32_t *pointer_longword = &longword; // osoitin 32-bittiseen arvoon
printf("%x\n", *pointer_byte);
printf("%x\n", *pointer_word);
printf("%x\n", *pointer_longword);
Tämä tulostaa:
78 5678 12345678
Jokainen osoitin käsittelee osoitetussa muistiosoitteessa olevaa dataa osoittimen tietotyypin mukaisesti. Osoittimen tyyppi on siis tärkeä: se kertoo kääntäjälle, miten osoitettua muistia tulee tulkita, kun osoitin dereferoidaan. Tavallisesti osoittimen tulisi osoittaa yhteensopivaa tyyppiä olevaan dataan. Esimerkiksi
uint16_t *-osoittimen tulisi normaalisti osoittaa uint16_t-tyyppiseen olioon.Hox! Kun käännetään koodia, jossa osoitin alustetaan yhteensopimattoman tyyppisen muuttujan osoitteella, kääntäjä antaa yleensä esimerkiksi seuraavanlaisen varoituksen:
warning: initialization from incompatible pointer type
On kuitenkin yksi erityisen hyödyllinen tilanne, jossa haluamme tarkastella samaa oliota tavu kerrallaan. Useamman tavun kokoinen muuttuja tallennetaan muistiin yksittäisten tavujen muodostamana sarjana. C-kielessä tätä tavuesitystä voidaan tarkastella
unsigned char *-osoittimen avulla. Koska unsigned char vie yhden tavun, jokainen tämän osoittimen kautta käsiteltävä alkio vastaa yhtä alkuperäisen muuttujan tavua.Esimerkiksi:
uint32_t value = 0x12345678;
unsigned char *bytes = (unsigned char *)&value;
printf("%x\n", bytes[0]);
printf("%x\n", bytes[1]);
printf("%x\n", bytes[2]);
printf("%x\n", bytes[3]);
Tässä:
&valueantaa 32-bittisen muuttujan alkuosoitteen.- Osoite muunnetaan
unsigned char *-osoittimeksi. bytes[0]käsittelee muuttujanvalueensimmäistä tavua.bytes[1]käsittelee toista tavua ja niin edelleen.
Näiden tavujen järjestys riippuu suoritinarkkitehtuurin tavujärjestyksestä (engl. endianness). Little-endian-arkkitehtuurissa muuttuja:
0x12345678
tallennetaan muistiin pienimmästä muistiosoitteesta alkaen seuraavasti:
Osoite Arvo -------- ----- bytes[0] 0x78 bytes[1] 0x56 bytes[2] 0x34 bytes[3] 0x12
Big-endian-arkkitehtuurissa sama arvo tallennettaisiin seuraavasti:
Osoite Arvo -------- ----- bytes[0] 0x12 bytes[1] 0x34 bytes[2] 0x56 bytes[3] 0x78
Näin ollen
bytes[0] ei tarkoita sitä, että 32-bittinen arvo muunnettaisiin 8-bittiseksi kokonaisluvuksi. Sen sijaan tarkastelemme suoraan muuttujan muistiesityksen ensimmäistä tavua.Tämä on yksi syy siihen, miksi tavujärjestys on tärkeä laiteläheisessä ohjelmoinnissa, esimerkiksi laitteistorekisterien, tietoliikenneprotokollien, tiedostojen tai toisesta järjestelmästä vastaanotetun datan käsittelyssä.
Osoitinmuuttuja muistissa¶
Tietysti osoitinmuuttuja, kuten kaikki muuttujat, tarvitsee oman muistipaikkansa. Palataan yo. esimerkkikoodiin hieman muokattuna. Huomatkaa paikkamerkki
%p, jolla voidaan tulostaa muistiosoitteita. #include <stdio.h>
#include <inttypes.h>
int main() {
uint8_t a = 0x12;
uint8_t *osoitin_a = &a;
// uusi operaattori: sizeof
printf("muuttujan a muistiosoite: %p\n",&a);
printf("muuttujan koko tavuina: %z\n",sizeof(a));//%z is placeholder for size_t, type used for defining types of variables in C.
printf("osoitinmuuttujan muistiosoite: %p\n",&osoitin_a);
printf("osoitinmuuttujan koko tavuina: %z\n",sizeof(osoitin_a));//%z is placeholder for size_t, type used for defining types of variables in C.
return 0;
}
Joka tulostaa (muistiosoitteet nyt oikeita):
muuttujan a muistiosoite: 0x7ffd6232a93f muuttujan koko tavuina: 1 osoitinmuuttujan muistiosoite: 0x7ffd6232a940 osoitinmuuttujan koko tavuina: 8
Esimerkin tulostuksesta nähdään, että jokaisella osoitinmuuttujalla on oma osoitteensa muistissa.
sizeof-operaattorin avulla voimme myös selvittää itse osoitinmuuttujan koon. Tässä 64-bittisessä tietokoneessa osoitin vie 8 tavua. Oleellista on huomata, ettei osoittimen oma koko riipu sen osoittaman muuttujan koosta. Osoittimen koko riippuu arkkitehtuurista. Esimerkiksi 64-bittisessä arkkitehtuurissa osoitin on tyypillisesti 8 tavua ja 32-bittisessä arkkitehtuurissa 4 tavua.Osoittimien käyttäminen¶
Okei. hieno homma. Mutta mihin osoitinmuuttujaa sitten tarvitaan?
Osoittimet ja taulukkomuuttujat¶
Tarkastellaanpa osoitinmuuttujien ja taulukkojen läheistä suhdetta.
Koska osoittimien kanssa on vapaat kädet rellestää ohjelmalle varatussa muistissa, voimme tietysti alustaa osoitinmuuttujia osoittamaan myös taulukon alkioihin.
char merkkijono[] = "XYZ";
char *ptr_1 = &merkkijono[0]; // nyt osoittaa merkkiin X
char *ptr_2 = &merkkijono[1]; // nyt osoittaa merkkiin Y
char *ptr_3 = &merkkijono[2]; // nyt osoittaa merkkiin Z
printf("%c%c%c\n", *ptr_1, *ptr_3, *ptr_2);
Tulostaa:
XZY
Osoittimien alustamisessa taulukon kanssa on muutakin jännää.
int main() {
char merkkijono[] = "XYZ";
char *tapa_1 = &merkkijono[0]; // alustustapa 1
char *tapa_2 = merkkijono; // alustustapa 2 -> tulkitaan: decays
// Tulostetaan merkkijono
printf("%s\n", merkkijono);
printf("%s\n", tapa_1);
printf("%s\n", tapa_2);
return 0;
}
Koska kaikki kaksi alustustapaa osoittavat samaan muistiosoitteeseen, merkkijonon tulostus on sama.
XYZ XYZ XYZ
Voimme havaita, että C-kielessä, kun taulukkoa käytetään lausekkeessa, taulukon nimi tulkitaan (eng. decays) osoittimeksi sen ensimmäiseen alkioon. Tämä havainto tulee olemaan meille hyödyllinen pian...
Taulukot ja osoittimet eivät kuitenkaan ole identtisiä; taulukot ovat yhtenäinen muistilohko, kun taas osoitin sisältää jonkin muistipaikan osoitteen. On tärkeää huomata, että taulukon nimeä ei voi muuttaa osoittamaan toiseen paikkaan, toisin kuin osoittimet, joiden arvoa voi muuttaa.
Nyt tarkastellaan, mitä tapahtuu, kun taulukko välitetään funktiolle. Kun taulukko välitetään funktiolle, vain osoitin taulukon ensimmäiseen alkioon välitetään (taulukon nimi "tulkitaan" osoittimeksi), joten funktiolla ei ole tietoa taulukon koosta, ellet kerro sitä erikseen.
Tässä on esimerkki funktiosta, joka tulostaa kokonaislukutaulukon, ja meidän on välitettävä sekä taulukko että sen koko:
#include <stdio.h>
// Function prototype: Takes a pointer to the first array element and the array size
void print_array(int *array, int size);
int main() {
int numbers[] = {10, 20, 30, 40, 50}; // Määritellään kokonaislukutaulukko
int size = sizeof(numbers) / sizeof(numbers[0]); // Lasketaan taulukon koko
// Kutsutaan funktiota taulukolla ja sen koolla
print_array(numbers, size);
return 0;
}
// Funktio taulukon alkioiden tulostamiseksi
void print_array(int *array, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", array[i]);
}
printf("\n");
}
Hox: sizeof ei palauta taulukon alkioiden määrää, vaan taulukon kokonaismuistin koon tavuina. Jotta saisit oikean määrän alkioita, sinun tulee jakaa taulukon koko yhden alkion koolla, näin:
int size = sizeof(numbers) / sizeof(numbers[0]); // Laskee taulukon koon oikein tässä
Voit aina määritellä taulukon (esimerkiksi puskuri) koon vakioksi ja käyttää sitä argumenttina koon määrittelyssä.
Osoitin-aritmetiikkaa¶
Koska osoitinmuuttujat sisältävät muistiosoitteita, C-kieli sallii joitakin aritmeettisia operaatioita osoittimille. Osoitinta voidaan esimerkiksi kasvattaa tai pienentää sekä siirtää lisäämällä tai vähentämällä kokonaislukuarvo.
Esimerkiksi.
int main() {
char merkkijono[] = "ABCD";
char *ptr = merkkijono;
// tulostus merkki kerrallaan silmukassa..
for (ptr = merkkijono; *ptr != 0; ptr++) {
printf("%c",*ptr);
}
return 0;
}
Esimerkkikoodi tulostaa ruudulle.
ABCD
Nyt lienee hyvä paikka avata tätä esimerkkiä hieman:
- Alustuslauseessa
ptr = merkkijonoasetamme osoitinmuuttujanptrarvoksi merkkijonon alkuosoitteen (muista: taulukon tulkinta osoittimeksi). - Ehtolauseessa
*ptr != 0tarkistamme, ettei osoitin osoita merkkijonon viimeisenä alkiona olevaan lopetusnollaan (\0) (kts aiempi materiaali C-kielen muuttujista). Silmukka pyörii siis niin kauan, että*ptr:nosoittama arvo on nolla. - Sijoituslausekkeessa
ptr++kasvatamme osoitinta niin, että se siirtyy osoittamaan seuraavaan saman tyyppiseen alkioon.
Se, kuinka paljon muistiosoite muuttuu, riippuu siitä tietotyypistä, johon osoitin osoittaa.
Esimerkki. Tässä
++-operaattorilla päästään aina seuraavaan alkioon sijoituslauseessa ptr++. #include <stdio.h>
#include <inttypes.h>
int main() {
uint16_t taulukko[] = { 0x1234, 0x5678, 0x9ABC, 0x0000 };
uint16_t *ptr = taulukko;
for (ptr = taulukko; *ptr != 0; ptr++) {
printf("%lx ",*ptr);
}
return 0;
}
Joka tulostaa:
1234 5678 9abc
Kun kasvatat osoittimen arvoa, sen siirtymän määrä riippuu siitä, minkä kokoiseen tietotyyppiin osoitin viittaa. Esimerkiksi, kun kasvatat char * -osoitinta, se siirtyy 1 tavun verran (koska char on 1 tavu), kun taas uint16_t * -osoittimen kasvattaminen siirtää sitä 2 tavua (koska uint16_t on 2 tavua).
Tässä korostuu myös se, kuinka C todellakin on matalan tason, laiteläheinen ohjelmointikieli. Osoittimien avulla voimme käsitellä muistia suoraan ja siirtyä muistipaikkojen välillä, esimerkiksi taulukon alkioiden välillä. Kuten Ben-setä (Spider-Manin setä) muistuttaa meitä: "Suuren voiman mukana tulee suuri vastuu." Tätä voimaa täytyy käyttää huolellisesti. Osoittimen täytyy aina osoittaa kelvolliseen muistipaikkaan ennen kuin se dereferoidaan. Jos osoitinaritmetiikka siirtää osoittimen muuttujalle, taulukolle tai varatulle muistilohkolle kuuluvan muistialueen ulkopuolelle, dereferointi voi käsitellä muuta muistia, vioittaa dataa tai kaataa ohjelman.
Seurauksena tietysti C-ohjelmoijan on oltava varsin tarkkana, ettei osoittimilla pelatessa ohjelmassa rikkoudu mitään. Esimerkiksi saatamme vahingossa osoittaa muuttujan muistialueen ulkopuolelle, eli vahingossa muokata jonkin toisen muuttujan arvoa (ylivuoto), tai jopa koko sulautetun ohjelman muistialueen ulkopuolelle, josta seuraa esimerkiksi ohjelman kaatuminen.
Hox! Yllä olevassa esimerkissä käytimme arvoa
0x0000 merkkinä taulukon loppumisesta. Toinen yleinen tapa on selvittää taulukon alkioiden määrä sizeof-operaattorin avulla.uint16_t taulukko[] = { 0x1234, 0x5678, 0x9ABC };
int alkioiden_maara = sizeof(taulukko) / sizeof(taulukko[0]);
Tässä:
sizeof(taulukko)palauttaa koko taulukon koon tavuina.sizeof(taulukko[0])palauttaa yhden taulukon alkion koon tavuina.- Jakamalla nämä arvot keskenään saadaan taulukon alkioiden määrä.
Tässä esimerkissä
uint16_t vie 2 tavua, joten:sizeof(taulukko) = 6 tavua sizeof(taulukko[0]) = 2 tavua 6 / 2 = 3 alkiota
Alkioiden määrää voidaan tämän jälkeen käyttää silmukan rajana:
for (int i = 0; i < alkioiden_maara; i++) {
printf("%x ", taulukko[i]);
}
Funktion argumentteina¶
Kuten muitakin muuttujatyyppejä, myös osoittimia voidaan käyttää funktion parametreina. Tällöin osoittimeen tallennettu muistiosoite välitetään funktiolle, ja funktion parametri saa kopion tästä osoitteesta. Tästä on erittäin paljon hyötyä, kuten kohta näemme...
Lähdetään liikkeelle esimerkistä.. allaoleva funktio
vaihda ei toimi. Miksi? #include <stdio.h>
void vaihda(int8_t local_a, int8_t local_b); // prototyyppi
int main() {
int8_t a = 14;
int8_t b = 68;
printf("Ennen: a=%d ja b=%d\n", a, b);
vaihda(a, b);
printf("Jälkeen: a=%d ja b=%d\n", a, 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: a=14 ja b=68 Jälkeen: a=14 ja b=68
Selitys asiaan löytyy luentokappaleen Funktiot C-kielessä -materiaalista. Muistamme, että C-kielessä funktio loi argumenteista kopiot paikallisiin muuttujiin. Ylläolevassa koodissa siis vain vaihdetaankin vain kopioiden arvoja (muuttujat
local_a) ja local_b, eikä alkuperäisten muuttujien a ja b arvoja. No, ei hätää. Asia korjataan käyttämällä osoittimia funktion argumentteina. Nyt funktiossa osoitin
local_a osoittaakin argumenttina annettuun a:n muistipaikkaan.#include <stdio.h>
void vaihda(int8_t *a, int8_t *b); // prototyyppi osoittimilla
int main() {
int8_t a = 14;
int8_t b = 68;
printf("Ennen: a=%d ja b=%d\n", a, b);
vaihda(&a, &b); // Hox &-operaattorin käyttö
printf("Jälkeen: a=%d ja b=%d\n", a, b);
}
void vaihda(int8_t *local_a, int8_t *local_b) {
int8_t temp = *local_a; // a:n osoittama arvo talteen
*local_a = *local_b; // b:n osoittama arvo osoitin a:n arvoon
*local_b = temp; // a:n osoittama arvo b:n osoittamaan muistipaikkaan
}
Nyt 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!
Muistetaan esimerkki aiemmasta materiaalista, jossa välitimme
struct:n, joka sisälsi suuren taulukon, funktion argumenttina, ja funktio-parka yritti kuuliaisesti tehdä siitä kopion joka kerta.// Määritellään struct viestiä varten
typedef struct {
char destination_address[4]; // 4 tavun osoite
char message[2048]; // 2 kilotavun viesti
} Message;
...
// Määritellään viesti-struct ja täytetään se datalla
Message message_home = {"ABCD", "Tämä on viestin sisältö..."};
...
// Kutsutaan funktiota ja välitetään struct argumenttina
send_message(message_home);
...
Tämä ongelma voidaan kätevästi korjata antamalla funktiolle struct:in osoite argumenttina:
// Määritellään struct viestiä varten
typedef struct {
char destination_address[4]; // 4 tavun osoite
char message[2048]; // 2 kilotavun viesti
} Message;
// Funktion prototyyppi
void send_message(Message *message_home);
int main() {
// Määritellään viesti-struct ja täytetään se datalla
Message message_home = {"ABCD", "Tämä on viestin sisältö..."};
// Välitetään rakenteen muistiosoite.
// Funktiolle kopioidaan vain osoittimen arvo eli muistiosoite.
// Tämä simuloi viittauksena välittämistä: funktio voi käsitellä ja muuttaa alkuperäistä rakennetta osoittimen kautta.
send_message(&message_home);
return 0;
}
Hox! C-kielessä ei ole varsinaista viittauksena välittämistä (engl. pass-by-reference), vaan kaikki funktion argumentit välitetään arvona. Välittämällä funktiolle muuttujan osoitin voidaan kuitenkin saavuttaa samankaltainen toiminta kuin muiden ohjelmointikielten pass-by-reference-mekanismilla, koska funktio voi käsitellä ja muuttaa alkuperäistä muuttujaa sen muistiosoitteen kautta.
Hox! Toki funktio tekee kopion osoitinmuuttujan arvosta, mutta osoitin on paljon pienempi kuin itse rakenne (esimerkiksi 4 tavua 32-bittisessä arkkitehtuurissa).
Paluuarvot funktiosta¶
Palataan vielä hetkeksi aiemmin käsiteltyyn
swap-funktioon, sillä se havainnollistaa erästä osoittimien hyödyllistä ominaisuutta:void swap(int8_t *a, int8_t *b) {
int8_t temp = *a;
*a = *b;
*b = temp;
}
Funktio itsessään ei palauta mitään arvoa, kuten sen
void-palautustyyppi kertoo. Osoittimien avulla funktio voi kuitenkin muuttaa useampaa funktion ulkopuolella olevaa muuttujaa. Tässä esimerkissä swap muuttaa molempia alkuperäisiä muuttujia niiden muistiosoitteiden kautta. Näin funktio voi tuottaa useita tuloksia, vaikka C-kielen funktio voi palauttaa suoraan vain yhden arvon return-lauseella. Superkätevää!Hox! Voimme myös palauttaa funktiosta muistiosoitteen. Tällöin täytyy kuitenkin varmistaa, että osoitin viittaa edelleen kelvolliseen muistiin funktion suorituksen päätyttyä, esimerkiksi dynaamisesti varattuun muistiin tai globaaliin muuttujaan. Funktion paikallisen muuttujan osoitteen palauttaminen ei ole turvallista, koska kyseinen muistialue ei ole enää käytettävissä funktion suorituksen päätyttyä.
Merkkijonojen käsittely¶
Yksi oleellinen osoittimien hyöty ilmenee taul.. merkkijonojen käsittelyssä.
Standardikirjaston osana kirjasto 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 %zu merkkiä\n",nimi, strlen(nimi));
return 0;
}
Useimmat näistä kirjastofunktioista käsittelevät merkkijonoja osoittimien kautta. Mutta muista: osoitin sisältää vain muistiosoitteen; se ei sisällä tietoa taulukon koosta. Miten
<string.h>-kirjaston funktiot sitten tietävät, milloin merkkijonon loppu on saavutettu ja muistia ei enää pidä käsitellä? C-merkkijonoissa käytetään erityistä nollamerkkiä '\0' merkkijonon lopun merkitsemiseen. Nollamerkkiin päättyviä merkkijonoja käsittelevät funktiot jatkavat siis merkkien käsittelyä, kunnes ne kohtaavat tämän nollamerkin, eivätkä ne siksi tarvitse taulukon kokonaiskokoa parametrina. Joten tarkkana.Kätevyyttä laiteläheiseen¶
Loppuun vielä esimerkki
strtok-funktiosta, koska siitä on sulatetuissa järjestelmissä valtavasti hyötyä. Kuten aiemmin todettiin, niin tietorakenteet ja langattomat viestit kulkevat usein CSV-muodossa, jossa siis viestin eri kentät ovat erotettu toisistaan pilkulla.
Esimerkiksi
1234567890,temperature,27,C, jossa ensimmäinen kenttä olisi aikaleima (ts. milloin anturin arvo on mitattu), sitten anturin tyyppi (temperature), mkittausarvo (27) ja mittayksikkö (Celsius).Nyt tällaisen CSV-muotoisen merkkijonon purkaminen osiin hoituu helposti
strtok-funktiolla.#include <string.h>
#include <stdio.h>
int main () {
char str[] = "1234567890,temperature,27,C";
const char sep[] = ","; // Erotin pilkku
char *token; // paikkamerkki-osoitin
// Erota viestistä ensimmäinen osa
token = strtok(str, sep);
// Erota silmukassa loput osat
while( token != NULL ) {
printf("%s\n",token);
// Funktio uudelleen, jatketaan paikkamerkistä
token = strtok(NULL, sep);
}
return(0);
}
Katsotaas mitä esimerkki tulostaa!
1234567890 temperature 27 C
Huom: Huom!
strtok muokkaa alkuperäistä merkkijonoa samalla, kun se jakaa sen osiin. Se korvaa erotinmerkit merkeillä '\0' ja palauttaa osoittimia saman merkkitaulukon syntyneiden osien alkuun.Ensimmäinen kutsu:
token = strtok(str, sep);
aloittaa merkkijonon
str käsittelyn. Seuraavissa kutsuissa käytetään arvona NULL:token = strtok(NULL, sep);
jolloin
strtok jatkaa siitä kohdasta, johon se edellisellä kutsulla jäi.Tämän vuoksi
strtok voi aluksi tuntua hieman epäintuitiiviselta. Voit löytää tarkemman selityksen sen toiminnasta täältä.Esimerkissä täytyy huomata, että erotetut pätkät ovat vielä tyyppiä merkkijono. Jotta voimme käsitellä viestissä olevia numeerisia arvoja numeroina, täytyy meidän muuntaa merkkijonot kokonais- tai liukulukumuuttujatyyppeihin. Tähän standardikirjasto stdlib.h tarjoaa esimerkiksi funktiot
atoi kokonaisluvuille, atol pitkille kokonaisluvuille ja atof liukuluvuille. Nämä funktiot tarjoavat yksinkertaisen tavan muuntaa merkkijonoja numeerisiksi arvoiksi. Standardikirjasto tarjoaa myös funktioita, kuten strtol ja strtod. Ne tarjoavat enemmän mahdollisuuksia hallita muunnosta ja havaita virheellinen syöte tai muita muunnosvirheitä. Yksinkertaisissa esimerkeissä funktiot, kuten atol, ovat kuitenkin riittäviä.#include <stdio.h>
#include <stdlib.h>
int main () {
char str[] = "1234567890";
long arvo = atol(str);
printf("%ld\n",arvo);
return(0);
}
Tosin, jälleen kerran sulautetuissa kehitysympäristössä nämä standardikirjaston funktiot on saatettu korvata jollain toisilla funktioilla tai niiden toteutuksessa saattaa olla rajoituksia.
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?