Osoittimet Ohjelmassa muuttujalla on nimi ja arvo. Kääntäjä ja linkkeri varaavat muistilohkon, jonne muuttujan arvo talletetaan. Muistilohkon koko riippuu muuttujan tyypistä, eli kuinka suuria arvoja muuttujan tulee kyetä tallettamaan. C-kielessä muuttujalle varattava tila on toteutuskohtainen. Esimerkiksi PC-koneissa int-tyypin muuttujalle varataan vähintään 2 tavua eli 16 bittiä (1 tavu eli byte on 8 bittiä), nykyisin int varaa 4 tavua (eli 32 bittiä). 378
Esimerkiksi määritellään kokonaislukumuuttuja nimeltä luku int luku; Sanan int perusteella kääntäjä varaa muistia 4 tavua ja muistin sisältö tulkitaan kokonaislukutyyppisenä. Samalla kääntäjä tallettaa symbolitauluun nimen luku ja lukumuuttujalle varatun muistipaikan osoitteen. Kun myöhemmin kirjoitetaan luku = 2; 379
Ohjelman suorituksen aikana oletetaan, että arvo 2 talletetaan luvulle varattuun muistipaikkaan. Muuttujaan luku voidaan liittää kaksi "arvoa". 1. Muuttujaan luku talletettu arvo 2 ja 2. Muistipaikan osoitteen "arvo" (muuttujan luku osoite voisi olla esim:bfff fda0), jonne arvo 2 on talletettu 380
Osoitinmuuttujat Usein tulee tilanteita, joissa tarvitaan muuttujia, jotka voivat sisältää muistipaikkojen osoitteita. Tällaista muistiosoitetta tallettavaa muuttujaa kutsutaan osoitinmuuttujaksi (osoitin, pointteri). C-kielessä osoitinmuuttuja määritellään lisäämällä muuttujan määrittelyssä tietotyypin ja nimen väliin tähti '*'. Muuttujan tyyppi ilmaisee minkä tyyppisen tiedon osoite voidaan tallettaa kyseisen osoitinmuuttujan arvoksi. 381
Esimerkiksi int * ptr; ptr on muuttujan nimi (aivan kuten luku on kokonaislukumuuttujan nimi ). Merkki '*' ilmoittaa kääntäjälle, että tarkoitus on luoda osoitinmuuttuja, jolle varataan tilaa sen verran kuin muistiosoitteen arvon tallettaminen vaatii. Sana int ilmoittaa, että muuttujaan ptr on tarkoitus tallettaa kokonaisluvun osoite, eli muuttuja voi osoittaa kokonaislukuun. 382
Kun kirjoitetaan int luku; luku- muuttujalle ei anneta vielä arvoa. Samoin muuttujalla ptr ei ole arvoa. Muuttujalle ptr voi antaa alkuarvoksi arvon NULL, joka ilmaisee, että muuttuja ptr ei osoita minnekkään. Joissakin kääntäjissä NULL on #define -lauseella määritelty 0:ksi, mutta kaikki kääntäjät eivät tee näin. 383
Kun muuttujaan ptr halutaan tallettaa kokonaislukumuuttujan luku osoite, käytetään &-operaattoria ptr = &luku; &-operaattori palauttaa muuttujan luku osoitteen ja tallettaa osoitteen muuttujan ptr sisällöksi, jolloin ptr "osoittaa muuttujaan luku ". 384
Kun osoitinmuuttujan avulla halutaan osoittaa jonkun muistipaikan sisältöön, käytetään osoitinoperaattoria '*'. *ptr = 7; Arvo 7 kopioidaan muuttujan ptr osoittamaan muistipaikkaan. Jos ptr osoittaa muuttujaan luku, niin silloin muuttuja luku saa arvon 7. Tähtioperaattorin avulla viitataan muuttujan (esim. ptr) osoittaman muistipaikan sisältöön, eikä viittausmuuttujan itsensä sisältöön (ns. epäsuora osoitus). 385
Esimerkkiohjelma, joka esittelee osoittimen käyttöä #include <stdio.h> int main ( void ) { int j = 1, k = 2; int *ptr; ptr = &k; printf("\nmuuttujan j arvo on %d ja osoite on %p\n", j, &j); printf("muuttujan k arvo on %d ja osoite on %p\n", k, &k); printf("muuttujan ptr arvo on %p ja osoite on %p\n", ptr, &ptr); printf("muuttujan ptr osoittaman luvun arvo on %d\n", *ptr); return ( 0); } 386
OSOITE 0x100001070 0x100001074 0x100001078 SISÄLTÖ 1 2 0x100001074 NIMI j k ptr 387
Kääntäjän täytyy tietää kuinka monta tavua täytyy tallettaa ptr:n osoittamamaan muistipaikkaan. Jos ptr on määritelty osoittamaan int-tyyppiä, kopioidaan 4 tavua. Vastaavasti double-tyypin kohdalla kopioidaan double tyypin koon määräämä tavumäärä. Tyypin koon saa selville sizeof -operaattorilla: sizeof(int) palauttaa 4 388
Osoitintyypin koon voi myös laskea esimerkiksi: sizeof(int *) palauttaa arvon 4 sizeof(double *) palauttaa arvon 4 sizeof(char *) palalauttaa myös arvon 4 389
Kun määritellään osoittimen osoittama tyyppi, voi kääntäjä tulkita ohjelmakoodia eri tavoin. Jos jossakin kohdassa keskusmuistiin on talletettu 10 kokonaislukua taulukkoon. 10 kokonaisluvun tallettamiseen tarvitaan tilaa: sizeof(int) * 10 eli 4 * 10 eli 40 tavua. 390
int * ptr = NULL; int lukuja[10]; lukuja[0] = 1; ptr = lukuja /* tai ptr = &lukuja[0] */ Jos taulukko lukuja alkaa osoitteesta 100 ja kokonaislukuosoitin ptr asetetaan osoittamaan luvuista ensimmäiseen, joka sijaitsee osoitteessa 100,niin mitä tapahtuu kun kirjoitetaan: ptr +1; 391
Kääntäjä "tietää", että muuttuja ptr on osoitin ja muuttuja osoittaa int -tyyppisen arvon osoitteeseen (osoite 100, jossa on tallessa jokin kokonaisluku). Kääntäjä lisää muuttujaan ptr arvon 4 (arvon 1 sijasta), jolloin osoitin osoittaa seuraavaan kokonaislukuun osoitteessa 104 eli osoitin siirtyy osoittamaan 4 tavua eteenpäin. 392
&lukuja[0] tai lukuja muistipaikan osoite 100 0000 0000 0 0000 0000 0000 0000 0000 0001 104 1 8 bitiä eli yksi tavu taulukon indeksit lukuja[0] ja lukuja[1] sisältö: kokonaisluku 1 binäärilukuna 393
Jos osoitin viittaisi tyyppiin double, lisättäisiin osoitinta arvolla 8 (koska double tyyppi varaa 8 tavua, 64 bittiä). Kääntäjä ei noudata samaa "aritmetiikkaa" kuin ihmiset, vaan tietotyypistä riippuen osoitinmuuttuja + 1 voi olla osoitinmuuttuja + 2 tai osoitinmuuttuja + 4, jne. Laskutoimitus ptr + 1 voidaan korvata ++ ptr ja ptr ++ operaatioilla, ptr ++:ssa osoitinta käytetään ensin ja vasta sitten kasvatetaan. Osoittimen kasvattaminen kasvattaa osoittimen arvoa sizeof(tyyppi) verran, jossa "tyyppi" on jokin C-kielen tietotyypeistä int, double, char jne. 394
Kymmenen kokonaisluvun lohko keskusmuistissa muodostaa taulukon, joten taulukoilla ja osoittimilla on seuraavanlainen yhteys int lukuja[] = {1, 2, 43, 9, 10, -1, 3, 99, -123, 0}; Taulukko lukuja sisältää 10 kokonaislukua, joihin viitataan tavallisesti indeksin avulla lukuja [0]... lukuja [9]. Taulukkoa voidaan käsitellä myös osoittimen avulla: int *ptr; ptr = &lukuja[0]; /* osoitin taulukon 1. alkioon */ 395
Seuraavassa koodissa tulostetaan taulukko sekä indeksien avulla että osoittimen avulla. #include <stdio.h> int main ( void ) { int lukuja[] = {1, 2, 43, 9, 10, -1, 3, 99, -123, 0}; int *ptr; int i; ptr = &lukuja[0]; /* ptr osoittaa taulukkoon lukuja */ printf("\n\n"); 396
for ( i = 0; i < 10; i++) { printf("lukuja[%2d] = %5d ", i, lukuja[i] ); /* <--A */ printf("ptr + %2d = %5d\n", i, *(ptr + i)); /* <--B */ } } return ( 0 ); 397
398
Joka paikassa jossa voidaan käyttää &muuttuja[0] voidaan sen sijasta käyttää muuttuja, joten koodi ptr = &lukuja[0]; voidaan kirjoittaa ptr = lukuja; Taulukon nimi on taulukon ensimmäisen alkion osoite! 399
Ei kuitenkaan voi kirjoittaa lukuja = ptr; ptr on muuttuja, mutta taulukko lukuja on ohjelman suoritusaikana vakio, eli taulukon ensimmäisen paikan osoitetta ei voi muuttaa sen jälkeen kun lukuja[] on määritelty. 400
Moniulotteiset taulukot ja osoittimet C-kielessä luodaan moniulotteinen taulukko kirjoittamalla taulukon määrittelyssä sama määrä sulkupareja kuin taulukolle halutaan ulottuvuuksia. Esimerkiksi kaksiulotteinen taulukko luodaan int lukuja [10][10]; Todellisuudessa taulukon alkioille varataan tilaa perättäisiin muistipaikkoihin. 401
Taulukon paikkaan lukuja [i][j] voidaan viitata osoittimen avulla usealla eri tavalla *(lukuja [i] + j ) (*(lukuja + i)[j] *((*(lukuja + i)) + j); *(&lukuja[0][0] + 10i + j) Sulkuja tarvitaan koska hakasulut [] on voimakkaampi operaattori kuin epäsuoruusoperaattori *. Kun moniulotteinen taulukko välitetään funktiolle, on funktion otsikossa kerrottava muiden dimensioiden paitsi ensimmäisen koot, jotta kääntäjä voi käsitellä muistia oikein. 402
Osoittimet ja merkkijonot C-kielessä merkkijonot ovat merkkitaulukoita, merkkijonon päättyminen ilmaistaan NUL-merkillä '\0'. char mjono[40]; /* yksi char vie tilaa yhden tavun 8 bit */ mjono[0] ='C'; mjono[1] = '-'; mjono[2] = 'k'; mjono[3] = 'i'; mjono[4] = 'e'; mjono[5] = 'l'; mjono[6] = 'i'; mjono[7] = '\0'; 403
Kääntäjä varaa merkkijonolle muistilohkon, kooltaan 40 tavua ja tallettaa lohkoon merkit C-kieli\0. 404
#include <stdio.h> char stra[80] ="Demonstraatiomerkkijono"; char strb[80]; int main ( void ) { char *pa; /* osoitin char-tyyppiin */ char *pb; /* osoitin char-tyyppiin */ puts (stra); /* tulosta stra-merkkijono */ pa = stra; /* pa osoittaa merkkijonoon stra */ puts ( pa ); /* näyttää minne pa osoittaa */ pb = strb; /* pb osoittaa merkkijonoon strb */ putchar('\n'); /* rivinvaihto */ while ( *pa!= '\0') { *pb++ = *pa++; } } *pb = '\0'; puts(strb); /* tulostetaan strb */ return ( 0 ); 405
Merkkijonot stra ja strb ovat globaaleja, jolloin ne strb alustetaan oletusarvoisesti '\0'-merkillä. Ohjelmassa funktiolla puts(stra) tulostetaan merkkijono kuvaruudulle, koska funktiolle välitetään stra[0]:n osoite. Sama asia voidaan tehdä myös funktiokutsulla puts( pa ), koska pa sisältää stra[0]:n osoitteen. while -toistorakenteessa toistetaan käskyä *pb++ = *pa++; niin kauan kuin pa:n osoittama merkki ei ole '\0'. 406
Käskyssä: *pb++ = *pa++; 1. Kopioidaan pa:n osoittama merkki pb:n osoittamaan paikkaan 2. jonka jälkeen pa:ta kasvatetaan osoittamaan seuraavaan merkkiin ja 3. pb:tä kasvatetaan osoittamaan seuraavan paikkaan Silmukassa ei kopioidaan '\0'-merkkiä ja se on lisättävä loppuun erikseen. 407
Seuraavassa esimerkkiohjelmassa toteutetaan oma merkkijonon kopiointifunktio omastrcpy, jolla korvataan C-kielen standardiversio. 408
char *omastrcpy ( char *kopio, char *kopioitava ) { char *p = kopio; while( *kopioitava!= '\0' ) { *p++ = *kopioitava++; } *p = '\0'; } return ( kopio ); 409
Funktio palauttaa viittauksen kopio-merkkijonoon, kuten alkuperäinenkin strcpy()-funktio. Ohjelmaa voidaan käyttää int main ( void ) {... omastrcpy ( strb, stra ); puts ( strb ); return ( 0 ); } 410
C-kielessä merkkijonovakio voidaan määritellä osoittimen avulla seuraavasti char *mjono = "Tämä on merkkijono"; 411
Mutta tämä aiheuttaa virhetilanteen: #include <stdio.h> int main( void ) { char * merkkijono; printf("\nanna merkkijono > "); scanf("%s", merkkijono); } printf("\nsyötit merkkijono \2%s\n\n", merkkijono); return 0; 412
413
Taulukkoon voidaan viitata usealla eri tavalla int taulu [10] = { 1, 2, 3, 4,5 }; int *pi; int i = 3; pi = &taulu[0]; taulu[i] = 11; /* tallettaa luvun 4 päälle luvun 11 */ *(pi + i) = 22; /* tallettaa luvun 11 päälle luvun 22 */ *(i + pi) = 33; /* tallettaa luvun 22 päälle luvun 33 */ i[taulu] = 44; /* tallettaa luvun 33 päälle luvun 44 */ 414