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
Nyt osoitinmuuttujat tarjoavat epäsuoran tavan osoittaa jollekin muulle muuttujalle varattua muistia. Kuten tulemme näkemään, tästä on valtavasti hyötyä tehokkaamman ja tiiviimmän koodiin kirjoittamisessa, joka on erityisen tärkeää sulautettujen ohjelmoinnissa resurssirajoitusten takia. (Lisäksi, koska osoitinmuuttujan arvo voi olla mikä tahansa luku, niin voimme osoittaa minne tahansa ohjelman käytössä olevaan muistin.)
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 *

Operaattorilla * on kolme käyttötarkoitusta.
1. Osoitinmuuttuja esitellään operaattorilla *, eli syntaksi *muuttujan_nimi.
int8_t *osoitin_a = &a; 
HOX: Jos ilmoitat osoittimen mutta et alusta sitä, osoittimen arvo on määrittelemätön, eli se sisältää roskadataa. Yritys dereferoida (päästä käsiksi arvoon, johon se osoittaa) johtaisi määrittelemättömään toimintaan.
int main() {
    int8_t *pointer_a;  // Ilmoitettu mutta ei alustettu
    printf("Alustamattoman osoittimen arvo: %p\n", pointer_a);  // Tämä voi tulostaa minkä tahansa satunnaisen osoitteen
    return 0;
}
Sen sijaan, jos emme halua alustaa osoitinta sen määrittelyn yhteydessä, meidän pitäisi alustaa se arvolla NULL.
int8_t *pointer_a = NULL;  // Nyt pointer_a on eksplisiittisesti alustettu arvolla NULL
2. Sijoitusoperaattori * jolla voidaan sijoittaa uusi arvo osoitettuun (toisen) muuttujan muistipaikkaan.
int8_t a = 47;
int8_t *osoitin_a = &a;
*osoitin_a = 23; // muutetaan osoittimen kautta a:n arvoa
Nyt siis alustetaan a arvoon 47 ja sitten osoittimella osoitin_a sijoitetaan siihen uusi arvo 23. Jännää..
Vrt. Sijoitus ilman *-operaattoria osoitin_a = 23 muuttaisi osoitinmuuttujen itsensä arvoa, niin että osoittaisikin muistipaikkaan 23 eikä enää (välttämättä) muuttujan a muistipaikkaan.
3. Osoitinmuuttujalla ja *-operaattorilla noudetaan osoitinmuuttujan osoittaman muistipaikan arvon.
uint16_t x = 0xBEEF;
uint16_t *osoitin_x = &x;	
printf("x=%x\n",*osoitin_x); 
Joka tulostaa noudetut osoitetut arvot.
x=beef

Osoitinmuuttujan tyyppi

Huomataan yltä, että aina osoitinmuuttujalle annetaan myös muuttujatyyppi kun se esitellään.. miksi? Eikös se ole muistiosoite?
Muuttujatyyppi tarvitaan siksi, että kääntäjä tietäisi minkä tyyppiseen arvoon osoitin viittaa siellä muistipaikassa. Katsotaanpa allaolevaa koodiesimerkkiä.
int main() {
   uint32_t a = 0x12345678;

   uint8_t *osoitin_byte = &a; // 8-bit osoitin
   uint16_t *osoitin_word = &a; // 16-bit osoitin
   uint32_t *osoitin_longword = &a; // 32-bit osoitin
	
   printf("%x\n",*osoitin_byte);
   printf("%lx\n",*osoitin_word);
   printf("%lx\n",*osoitin_longword);
	
   return 0;
}
Koodissa esitellään siis 32-bittinen kokonaislukumuuttuja ja sille 8-, 16- ja 32-bittiset osoittimet. Kun sitten tulostetaan (luennoitsijan pc:ssä) jokaisen osoittimen osoittama muuttujan arvo, niin saadaan:
78        // 8-bittinen osoitin palauttaa tavun
5678      // 16-bittinen osoitin palauttaa sanan
12345678  // 32-bittinen osoitin palauttaa pitkän sanan
Havaitaan että kääntäjä siis ymmärtää osoittimen sen muuttujatyypin mukaan ja noutaa sen mukaisen arvon muistista! Nyt siis osoitinmuuttuja on syytä esitellä samalla muuttujatyypillä kuin sen osoittama muuttuja.
Hox! Esimerkkikoodia kääntäessä eteen voi tulla varoitus vääräntyyppisen osoittimen esittelemisestä warning: initialization from incompatible pointer type.
Hox! Miksi tulostus ei ole 12 tai 1234?? Tässähän vaikuttaa suoritinarkkitehtuurin tavujärjestys.

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: %d\n",sizeof(a));
    
   printf("osoitinmuuttujan muistiosoite: %p\n",&osoitin_a);
   printf("osoitinmuuttujan koko tavuina: %d\n",sizeof(osoitin_a));

   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 ja meille uuden sizeof-operaattorin avulla saadaan osoitinmuuttujan muistipaikan koko (8 tavua -> 64-bittinen suoritinarkkitehtuuri). Oleellista tässä siis on hoksata että osoitinmuuttujan itsensä muistipaikan koko on tässä 4 tavua, riippumatta siitä että se osoittaa yhden tavun kokoista muuttujaa.

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>

// Funktion prototyyppi: Ottaa osoittimen taulukkoon ja sen koon
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 osoitinmuuttujien arvot ovat lukuarvoja, niitä voidaan tietysti käsitellä kaikilla C-kielen aritmeettisillä operaatioilla.
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:
Mikä on erinomaista, aritmeettiset operaatiot osoittimilla eivät välitä muuttujatyypistä.
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 };
   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ä myös korostuu se, kuinka C todellakin on matalan tason laiteläheinen kieli. Pääsemme käsiksi ohjelmamme muistiin vapaasti ja voimme sotkea muuttujien arvoja ja niiden muistipaikkoja.
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.

Funktion argumentteina

Kuten kaikkia muitakin muuttujatyyppejä, myös osoittimia voidaan käyttää funktion parametreinä. Oleellisesti tällöin argumenttina annetaan muistiosoite, joka tallentuu siihen funktion paikalliseen muuttujaan. Tästä on suuresti hyötyä kuten kohta nähdään..
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ö..."};
    
    // Kutsutaan funktiota ja välitetään struct argumenttina (viittauksena)
    send_message(&message_home);

    return 0;
}
Hox! Toki funktio tekee kopion osoitinmuuttujan arvosta, mutta onhan se osoitinmuuttujatyyppi (kooltaan 4 tai 8 tavua) paljon pienempi kuin itse taulukko.

Funktiosta palauttaminen

Palataan hetkeksi aiemmin esitettyyn vaihda-funktioon, nimittäin siitä hoksataan toinenkin jännä asia..
void vaihda(int8_t *a, int8_t *b) {
   int8_t temp = *a;
   *a = *b;
   *b = temp;
}
Nythän tässä funktiossa itseasiassa palautamme funktion suorituksen tuloksena kaksi arvoa, eli vaihtuneet muuttujien a ja b arvot. Tällä tavoin siis kierrettiin C-kielen rajoituksia käsittelemällä suoraan muistia muuttujien itsensä sijaan. Superkätevää!
Hox! Funktiosta voimme tietenkin myös palauttaa muistiosoitteen. Mutta varmista, että osoitin viittaa kelvolliseen muistiin (esimerkiksi dynaamisesti varattuun muistiin tai globaaliin muuttujaan). Osoittimen palauttaminen paikalliseen muuttujaan funktiossa on turvatonta, koska muisti muuttuu virheelliseksi, kun funktio päättyy.

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 %d merkkiä\n",nimi, strlen(nimi));

  return 0;
}
Nyt kaikki nämä kirjaston funktiot ottavat merkkijonoja sisäänsä osoittimina niiden muuttujanimen kautta. Eli ne olettavat annetun taulukon päättyvän aina nollaan ja siksi eivät tarvitse taulukon kokoa parametrina. Tämähän on tietysti vaarallista jos käsitellään merkkijonoja, jotka eivät pääty nollaan. 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: strtok ei toimi intuitiivisella tavalla. Voit löytää lisäselvitystä 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 mm. funktiot atoi kokonaisluvuille, atol pitkille kokonaisluvuille, ja atof liukuluvuille. Osan näistä funktioista toiminnallisuus on vanhempien C standardien mukaista, mutta voimme käyttää niitä silti vielä. Modernit järjestelmät käyttävät strtol ja strtod.
#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.
?