Ratkaistu: / tehtävää

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:

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:
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:

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ä:
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:
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ä:
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:
#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

Help
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.
?