OSOITTIMET

Osoittimet ovat C-kielen vahvimpia ominaisuuksia ja samalla vaikeimpia ja vaarallisimpia piirteitä. Osoittimia on todella helppo käyttää väärin ja aikaansaatujen virheiden jäljittäminen on vaikeaa. Osoitin on erikoistyyppinen muuttuja, osoitinmuuttuja. Se osoittaa muistin tiettyyn sijaintipaikkaan. Osoitin edustaa ensisijaisesti muuttujan sijaintia muistissa. Tyypillisesti osoitin osoittaa muistipaikkaan, johon arvo on talletettu tai aiotaan tallettaa.

Osoittimen käytön syyt:

• Osoittimet on menetelmän, jolla funktiot voivat muuttaa kutsuvia argumenttejaan. Osoittimia voidaan käyttää välittämään enemmän kuin yksi informaatio-osanen funktion ja funktiota kutsuvat koodirivin välillä.
• Osoittimia käytetään tukemaan dynaamista muistin allokointia.
• osoitintoiminnoilla saadaan lisää tehokkuutta taulukoiden käsittelyyn. Osoitin tarjoaa vaihtoehtoisen menetelmän käsitellä yksittäisiä taulukon alkioita.


OSOITTIMIEN MÄÄRITTELY

Osoitinta määriteltäessä täytyy käyttää erikoista osoitinmäärittelyä, jolla kerrotaan kääntäjälle, että ollaan määrittelemässä osoitinmuuttujaa. Koska eri tietotyypit vaativat eri määrän muistia, osoitinmäärittelyn täytyy sisältää sen tietotyypin määrittely, johon osoitin viittaa. Tämä toteutetaan määrittelemällä osoitin osoittamaan juuri tietyn tyyppiseen tietoon.

Osoitinmäärittelyn yleinen muoto:

tietotyyppi *nimi;

Tietotyyppi on mikä tahansa C:n tietotyyppi ja nimi kuvaa osoitinmuuttujan nimeä. Tietotyyppi määrittää sen muuttujan tyypin, johon osoitin voi osoittaa. Tähti (*) tietotyypin jäljessä kertoo, että tunniste on osoitin johonkin ja että osoitin osoittaa kyseisen tyyppiseen muuttujaan.

char *merkki; int *luku, *toinen_luku;

Huom! Mikäli haluat ohjelmasi kaatuvan, käytä alustamatonta osoitinta.

C-kielessä on sovittu, että osoittimelle, joka ei osoita minnekään, asetetaan vakioarvo NULL. Vaikka osoitin olisi NULL kääntäjä ei estä tällaisen osoittimen käyttöä ja todennäköinen lopputulos on ohjelman kaatuminen ajon aikana.


OSOITINOPERAATTORIT

Osoittimien yhteydessä toimii kaksi operaattoria, & ja * operaattorit. & -operaattori on osoiteoperaattori, joka määrittää osoitinmuuttujan osoitteen.

Yleinen muoto osoitinoperaattoreille:

osoite = &ch;

Koodi sijoittaa muuttujan ch muistiosoitteen osoitinmuuttujaan osoite. Muuttujan osoite edustaa tunnisteen ch muistipaikkaa, ei sen todellista arvoa. Muistipaikalla ei ole mitään tekemistä muuttujan arvon kanssa.

Esimerkissä (alla), määritellään kolme kokonaisluku-muuttujaa var1, var2 ja var3 sekä sijoitetaan niihin alkuarvot. Ohjelma näyttää kukin muuttujan osoitteen (&var1) käyttäen muotoilumäärettä %x eli etumerkittömänä heksalukuna.

/*Ohjelma näyttää muuttujien osoitteet heksalukuna */ #include <stdio.h> int main() { int muuttuja_1 = 1, muuttuja_2 = 2, muuttuja_3 = 3; printf(" Muuttujien osoitteet heksana \n\n"); printf(” muuttuja_1:n osoite: %x\n”, & muuttuja_1); printf(” muuttuja_2:n osoite: %x\n”, & muuttuja_2); printf(” muuttuja_3:n osoite: %x\n”, & muuttuja_3); printf(" muuttuja_3:n osoite: %p\n", & muuttuja_3); printf(" (Ylla, muuttuja_3:n osoite kahdella tavalla)"); getch(); return 0; }




Example pic

Kuvassa (yllä), tietokoneen muistipaikkoihin liittyy:

- muuttujien nimet (muuttuja_1, muuttuja_2 ja muuttuja_3),
- muuttujiin eli muistipaikkoihin talletettu tieto (numerot 1, 2 ja 3),
- muistipaikkojen osoitteet heksalukuina (22FF44, 22FF40 ja 22FF3C).


Toisen osoitinoperaattorin (*osoite) tehtävänä on palauttaa kyseisessä osoitteessa olevan muuttujan arvo. Koodilla asetetaan ch:n arvo muuttujaan nimeltä sisältö.

osoite = &ch; sisältö = *osoite;


Osoitin-käsitteen ymmärtämistä hankaloittaa * -operaattorin käyttö, jota käytetään sekä muuttujan määrittelyyn että viittaamaan arvoon, johon osoitin osoittaa. Lisäksi tähteä käytetään kertolaskun operaattorina mutta kääntäjä erottaa milloin kyseessä on osoitin tai laskutoimitus.

Esimerkkiohjelma (alla), joka tekee mitä ?

/* osoitintoimintoja */ #include <stdio.h> int main() { int LukuA=9, LukuB=0, *osoitinA, *osoitinB; osoitinA = &LukuA; LukuB = *osoitinA; osoitinB = &LukuB; printf("LukuA = %d &LukuA = %x", LukuA, &LukuA ); printf(" osoitinA = %x *osoitinA = %d\n", osoitinA, *osoitinA); printf("LukuB = %d &LukuB = %x", LukuB, &LukuB ); printf(" osoitinB = %x *osoitinB = %d\n", osoitinA, *osoitinA); getch(); return 0; }




Esimerkissä yllä:

Example pic
Yllä, int LukuA=9, LukuB=0, *osoitinA, *osoitinB; määritellään kaksi kokonaislukumuuttujaa (LukuA ja LukuB) ja niihin sijoitetaan luku sekä määritellään kaksi osoitinmuuttujaa (osoitinA ja osoitinB).


Example pic
Yllä, osoitinA = &lukuA; osoitinA -osoitinmuuttujaan sijoitetaan lukuA -muuttujan osoite.


Example pic
Yllä, lukuB = *osoitinA; lukuB -muuttujaan sijoitetaan osoitinA -osointinmuuttujassa oleva tieto joka on lukuA:n osoite.


Example pic
Yllä, osoitinB = &lukuB; osoitinB -osoitinmuuttujaan sijoitetaan lukuB:n osoite.



OSOITTIMIEN VÄLITYS FUNKTIOLLE

Osoittimet välitetään funktioon argumentteina. Tämä mahdollistaa sen, että funktio voi suoraan käyttää kutsuvan ohjelman osan tietoalkioita, muuttaa niitä ja palauttaa ne muutettuina kutsuvaan ohjelman osaan. Tavallisesti kun argumentti välitetään funktiolle, se välitetään arvona. Tällöin tietoalkion kopio viedään funktioon ja mitään funktion argumentteihin tehtyjä muutoksia ei palauteta kutsuvaan ohjelmaan. Kun argumentteina käytetään osoitteita voidaan sisältöä käsitellä ja osoitteeseen talletettuja arvoja voidaan muuttaa funktiossa. Nämä muutokset tapahtuvat muuttujien muistipaikoissa.

/* osoittimien välitys funktioon*/ #include <stdio.h> void eka_ali(int lukuA, int lukuB); void toka_ali(int *osoitin_lukuA, int *osoitin_lukuB); int main() { int luku1 = 0; int luku2 = 0; eka_ali(luku1, luku2); /* siirrytään eka_ali-aliohjelmaan vieden sinne luku1 ja luku2 muuttujissa oleva tieto, palataan takaisin eka_ali -aliohjel- masta ja näytetään luku1 ja luku2 muuttujien arvot */ printf("luku1 = %d ja luku2 = %d\n", luku1, luku2); toka_ali(&luku1, &luku2); /* siirrytään toka_ali-aliohjelmaan vieden sinne muuttujien luku1 ja luku2 osoitteet */ printf("luku1 = %d ja luku2 = %d\n", luku1, luku2); /* palattaessa toka-ali-ohjelmasta näytetään luku1 ja luku2 */ getch(); return 0; } void eka_ali(int lukuA, int lukuB) /* lukuA on 0, lukuB on 0 */ { lukuA = 1; lukuB = 1; printf("lukuA = %d ja lukuB = %d\n", lukuA, lukuB); } /*alla, *osoitin_lukuA ja *osoitin_lukuB saavat luku1 ja luku2 muuttujien osoitteet */ void toka_ali(int *osoitin_lukuA, int *osoitin_lukuB) { *osoitin_lukuA = 2; /* osoitin_lukuA osoittamaan paikkaan (luku1) sijoitetaan luku 2 */ *osoitin_lukuB = 2; /* osoitin_lukuA osoittamaan paikkaan (luku2) sijoitetaan luku 2 */ printf("*osoitin_lukuA = %d ja *osoitin_lukuB = %d\n", *osoitin_lukuA, *osoitin_lukuB); }



Välitettäessä argumentit arvoina, kaikki funktion argumentteihin tehdyt muutokset ovat paikallisia siinä funktiossa, jossa muutokset tapahtuivat.


OSOITTIMET MERKKIJONOIHIN

C-ohjelmissa käytetään merkkiosoittimia hyvin usein ja itse asiassa merkkiosoittimet onkin tarkoitettu merkkitaulukoiden laajennukseksi.

/* osoittimet ja merkkijonot */ #include <stdio.h> int main() { char *a; a = ”Hello, World !”; printf(”%s\n”, a); printf(”%c\n”, *a); getch(); return 0; }




Esimerkki (yllä). Ensimmäinen printf()-funktio esittää koko merkkijonon, kun taas toinen funktio tulostaa yksittäisen merkin H. Osoitinoperaattorin käyttäminen muuttujan kanssa osoittaa, että ohjelman on luettava muistista yksi merkkijonon osa.


OSOITTIMET JA TAULUKOT

Osoittimien ja taulukoiden välillä on läheinen yhteys. Taulukon nimi on todellisuudessa osoitin taulukon ensimmäiseen alkioon.

arr[99];

Määritellyn taulukon ensimmäisen alkion osoite voidaan ilmaista joko arr tai &arr[0]. Taulukon toisen alkion osoite voidaan kirjoittaa &arr[1] tai (arr + 1).

/* Taulukon alkioiden osoitteet */ #include <stdio.h> int main() { int arr[3] = {10, 11,12}; printf(”Ensimmaisen alkion osoite %d %d\n”, arr,&arr[0]); printf(”Toisen alkion osoite %d %d\n”, (arr + 1),&arr[1]); getch(); return 0; }




DYNAAMISEN MUISTIN ALLOKOINTI

Tähän mennessä tarkastellut tietorakenteet ovat olleet staattisia. Staattinen tietorakenne tarkoittaa, että kääntäjä allokoi muistia muuttujille niitä määriteltäessä. Muuttujat käyttävät muistitilaa ohjelman suorituksen yhteydessä. Staattinen muistin allokointi on yksinkertainen hallita, mutta se on joustamaton. Esimerkiksi taulukkoa luotaessa on C:lle kerrottava, kuinka suuri määrittelty taulukko on. Tämä informaatio vaaditaan, jotta oikea määrä muistia on käytettävissä ohjelmaa ajettaessa. Mikäli muistia allokoidaan liian vähän ohjelma kaatuu ja vastaavasti, jos alkioita on vähemmän kuin allokoitua muistia jää osa muistia käyttämättä. Staattisten tietorakenteiden vastakohtana on dynaamiset tietorakenteet. Näiden tietorakenteiden muisti allokoidaan ohjelmaa ajettaessa, joten dynaaminen tietorakenne kasvaa tarpeen mukaan. Mikäli muistia on allokoitu enemmän kuin tarvitaan dynaamisen tietorakenteen avulla voidaan muistia vapauttaa muuhun käyttöön.


Funktiot malloc() ja calloc()

Osoittimet voivat osoittaa mihin tahansa muistialueeseen. Tavallisest osoittimia käytetään viittaamaan muistilohkoihin, jotka on siirretty syrjään. C-kieli mahdollistaa osoituksen muistialueeseen, joka on siirretty syrjään. Järjestelmää voidaan kehoittaa laittamaan muistialue syrjään, jolloin sitä voidaan myöhemmin käsitellä osoittimien avulla. Tällöin määritetään kuinka monta tavua muistitilaa tarvitaan, kääntäjä määrää sen minne tieto tallennetaan. C-kääntäjän dynaamisen muistin allokointijärjestelmän muodostavat funktiot malloc() ja calloc(). Funktiot ovat osa standardia C-kielen funktiokirjastoa ja yleensä sisältyvät jokaiseen C-kääntäjään. Käytettäessä funktioita tulee ohjelmaan sisällyttää otsikkotiedosto alloc.h. Funktio malloc() allokoi muistilohkon. Funktio calloc() tekee saman mutta se ensin tyhjentää kunkin muistipaikan nollaksi. Funktiota calloc() käytetään funktion malloc() sijasta silloin, kun muistialue täytyy alustaa nollaksi ennen sen käyttämistä.

(HUOM ! DEV C++ kääntäjä ei sisällä alloc.h otsikkotiedostoa!)

Molemmat funktiot käyttävät yksittäistä kokonaislukuparametria määritellessään allokoitavan muistin määrää. Funktio paluttaa osoittimen, joka osoittaa allokoidun muistin ensimmäiseen tavuun. Jos tarpeeksi muistia ei ole käytettävissä, funktio palauttaa arvon NULL.

Kokonaislukumuuttuja voidaan luoda esim.

p = (int *)malloc (sizeof (int)); /*osoitin kokonaislukuun int*/

Tarkastellaan erikseen esimerkin osia:

• sizeof (int)

Tämä on parametri, joka välitetään funktiolle malloc(). Malloc() -funktio allokoi muistitilan, joka on välitetty funktiosta sizeof() ja paluttaa kyseisen muistin osoittimen. Funktiota sizeof() käytetään palauttamaan kokonaisluvulle tarvittavien tavujen määrä.

• (int *)

Ilmaisua käytetään funktion malloc() edellä. Tämä ilmaisu on nimeltään typecast. Se kertoo kääntäjälle, että tulkitaan funktion malloc paluuosoite osoittimena kokonaislukuun. Funktio malloc() ei osoita, minkä tyyppistä sen palauttama arvo on ja siksi kääntäjälle on kerrottava käytetyn tiedon tyyppi. Lopuksi sijoitetaan uuden kokonaisluvun osoite osoittimeen p. malloc() -komennon jälkeen voidaan käyttää muuttujaa *p kuten mitä tahansa muuta kokonaislukumuuttujaa. Siihen voidaan sijoittaa arvo tai palauttaa siihen arvo jostakin toisesta funktiosta.

*p = 1996; scanf(”%d”,p);

Esim.

pd = (double *) malloc (sizeof (double)); pc = (char *) malloc (sizeof (char));


Funktio free()

Funktioiden malloc() ja calloc() vastakohta on funktio free(), joka palauttaa aiemmin allokoidun muistin järjestelmälle. free()-funktio vapauttaa allokoidun muistilohkon käytettäväksi muihin tarkoituksiin.

Yleinen muoto free()-funktiosta

free(p);

Tunniste p on osoitin aiemmin allokoituun muistilohkoon. Kääntäjä pitää sisäistä listaa allokoidusta muistista, joten se vapauttaa muistia siitä sijaintipaikasta, johon funktion välittämä osoitin viittaa.

Esim. HUOM! DEV C++ kääntäjä ei sisällä alloc.h otsikkotiedostoa! Kokeile ohjelman alussa ilmoitusta stdio.h sekä stdlib.h otsikkotiedostoista.

#include<stdio.h> #include<stdlib.h> /* #include <alloc.h> TÄMÄ EI TOIMI Dev C++ kääntäjässä */ int main() { char *mem; mem = (char *) malloc(10); free (mem); return 0; }

Esim.

#include<stdio.h> #include<stdlib.h> /* #include <alloc.h> TÄMÄ EI TOIMI Dev C++ kääntäjässä */ int main() { int counter, number, temp; int *osoitin; printf(”Montako numeroa talletat ?\n”); scanf(”%d”,&number); osoitin = (int *) malloc(number * sizeof(int)); /*muistin allokointi*/ printf(”Anna numerot\n”); for (counter = 0; counter < number; counter++) { printf(”Anna numero %d: ”, counter + 1); scanf(”%d”, &temp); osoitin[counter] = temp; } printf(”\nNumerot ovat\n”); for (counter = 0; counter < number; counter++) printf(”Numero %d on %d\n”,counter+1,osoitin[counter]); free (osoitin); return 0; }




Ohjelma pyytää käyttäjää antamaan tallennettavien numeroiden lukumäärän. Tämän jälkeen ohjelma allokoi muistitilaa ja pyytää käyttäjältä kutakin numeroa. Lopuksi näytetään tulokset näytöllä ja vapautetaan muisti.
Silmukassa pyydetään kutakin numeroa. Numero talletetaan ensin muuttujaan luku. Allokoitua muistia voidaan käsitellä aivan kuin taulukkoa ja taulukon alkioon sijoitetaan tilapäiseen muuttujaan talletettu arvo. Ilmaisu arr osoittaa taulukon ensimmäiseen alkioon. Seuraavilla komennoilla vaihdetaan arvoa tilapäisen muuttujan ja dynaamisen muistipaikan välillä. Ohjelmassa ei tarvitsisi välttämättä käyttää free()-funktiota, koska ohjelman päättyessä muisti joka tapauksessa allokoidaan järjestelmälle.