Osoittimet. Mikä on osoitin?



Samankaltaiset tiedostot
Osoitin ja viittaus C++:ssa

815338A Ohjelmointikielten periaatteet Harjoitus 5 Vastaukset

Java-kielen perusteet

Ohjelmassa muuttujalla on nimi ja arvo. Kääntäjä ja linkkeri varaavat muistilohkon, jonne muuttujan arvo talletetaan.

Koottu lause; { ja } -merkkien väliin kirjoitetut lauseet muodostavat lohkon, jonka sisällä lauseet suoritetaan peräkkäin.

Muuttujien roolit Kiintoarvo cin >> r;

815338A Ohjelmointikielten periaatteet Harjoitus 3 vastaukset

Java-kielen perusteet

C-kielessä taulukko on joukko peräkkäisiä muistipaikkoja, jotka kaikki pystyvät tallettamaan samaa tyyppiä olevaa tietoa.

Mallit standardi mallikirjasto parametroitu tyyppi

Ohjelmoinnin peruskurssi Y1

Ohjelmointi 1 Taulukot ja merkkijonot

11. oppitunti III. Viittaukset. Osa. Mikä on viittaus?

Tietotyypit ja operaattorit

Harjoitustyö: virtuaalikone

Ohjelmointitaito (ict1td002, 12 op) Kevät Java-ohjelmoinnin alkeita. Tietokoneohjelma. Raine Kauppinen

Luku 6. Dynaaminen ohjelmointi. 6.1 Funktion muisti

Ohjelmointi 2. Jussi Pohjolainen. TAMK» Tieto- ja viestintäteknologia , Jussi Pohjolainen TAMPEREEN AMMATTIKORKEAKOULU

Johdatus Ohjelmointiin

Sisällys. 11. Javan toistorakenteet. Laskurimuuttujat. Yleistä

Osa. Toimintojen toteuttaminen ohjelmissa vaatii usein haarautumisia ja silmukoita. Tässä luvussa tutustummekin seuraaviin asioihin:

ITKP102 Ohjelmointi 1 (6 op)

Luokat. Luokat ja olio-ohjelmointi

Merkkijono määritellään kuten muutkin taulukot, mutta tilaa on varattava yksi ylimääräinen paikka lopetusmerkille:

Ohjelmoinnin perusteet Y Python

Taulukot. Jukka Harju, Jukka Juslin

Sisältö. 2. Taulukot. Yleistä. Yleistä

Ehto- ja toistolauseet

Yleistä. Nyt käsitellään vain taulukko (array), joka on saman tyyppisten muuttujien eli alkioiden (element) kokoelma.

Operaattoreiden ylikuormitus. Operaattoreiden kuormitus. Operaattoreiden kuormitus. Operaattoreista. Kuormituksesta

Tietueet. Tietueiden määrittely

Esimerkkiprojekti. Mallivastauksen löydät Wroxin www-sivuilta. Kenttä Tyyppi Max.pituus Rajoitukset/Kommentit

Sisällys. 3. Muuttujat ja operaatiot. Muuttujat ja operaatiot. Muuttujat. Operaatiot. Imperatiivinen laskenta. Muuttujat. Esimerkkejä: Operaattorit.

Ohjelmointitaito (ict1td002, 12 op) Kevät Java-ohjelmoinnin alkeita. Tietokoneohjelma. Raine Kauppinen

Ohjelmoinnin perusteet Y Python

Sisältö. 22. Taulukot. Yleistä. Yleistä

3. Muuttujat ja operaatiot 3.1

5.6. C-kielen perusteet, osa 6/8, Taulukko , pva, kuvat jma

Alkuarvot ja tyyppimuunnokset (1/5) Alkuarvot ja tyyppimuunnokset (2/5) Alkuarvot ja tyyppimuunnokset (3/5)

Ohjelmoinnin perusteet Y Python

12 Mallit (Templates)

Ohjelmoinnin perusteet Y Python

Virtuaalifunktiot ja polymorfismi

Operaattoreiden uudelleenmäärittely

Ohjelmassa henkilön etunimi ja sukunimi luetaan kahteen muuttujaan seuraavasti:

Olion elinikä. Olion luominen. Olion tuhoutuminen. Olion tuhoutuminen. Kissa rontti = null; rontti = new Kissa();

2. Lisää Java-ohjelmoinnin alkeita. Muuttuja ja viittausmuuttuja (1/4) Muuttuja ja viittausmuuttuja (2/4)

Java-kielen perusteet

Taulukot ja merkkijonot

Ohjelmointi funktioiden avulla

ITKP102 Ohjelmointi 1 (6 op)

Kääntäjän virheilmoituksia

C-ohjelma. C-ohjelma. C-ohjelma. C-ohjelma. C-ohjelma. C-ohjelma. Operaatioiden suoritusjärjestys

Kirjoita oma versio funktioista strcpy ja strcat, jotka saavat parametrinaan kaksi merkkiosoitinta.

Valinnat ja päätökset

Ohjelmoinnin perusteet Y Python

13 Operaattoreiden ylimäärittelyjä

Luokassa määriteltävät jäsenet ovat pääasiassa tietojäseniä tai aliohjelmajäseniä. Luokan määrittelyyn liittyvät varatut sanat:

7. Näytölle tulostaminen 7.1

Ohjelmoinnin perusteet Y Python

C++ rautaisannos. Kolme tapaa sanoa, että tulostukseen käytetään standardikirjaston iostreamosassa määriteltyä, nimiavaruuden std oliota cout:

ITKP102 Ohjelmointi 1 (6 op)

Osoittimet ja taulukot

11. Javan toistorakenteet 11.1

815338A Ohjelmointikielten periaatteet Harjoitus 2 vastaukset

Java-kielen perusteita

Algoritmit 1. Demot Timo Männikkö

VIII. Osa. Liitteet. Liitteet Suoritusjärjestys Varatut sanat Binääri- ja heksamuoto

Taulukot. Taulukon määrittely ja käyttö. Taulukko metodin parametrina. Taulukon sisällön kopiointi toiseen taulukkoon. Taulukon lajittelu

Tietuetyypin määrittely toteutetaan C-kielessä struct-rakenteena seuraavalla tavalla:

Python-ohjelmointi Harjoitus 2

Kerta 2. Kerta 2 Kerta 3 Kerta 4 Kerta Toteuta Pythonilla seuraava ohjelma:

16. Ohjelmoinnin tekniikkaa 16.1

Ohjelmoinnin perusteet Y Python

Ohjelman virheet ja poikkeusten käsittely

815338A Ohjelmointikielten periaatteet Harjoitus 4 vastaukset

Perustietotyypit ja laskutoimitukset

Sisällys. 16. Ohjelmoinnin tekniikkaa. Aritmetiikkaa toisin merkiten. Aritmetiikkaa toisin merkiten

Javan perusteet. Ohjelman tehtävät: tietojen syöttö, lukeminen prosessointi, halutun informaation tulostaminen tulostus tiedon varastointi

Ohjelmointiharjoituksia Arduino-ympäristössä

16. Ohjelmoinnin tekniikkaa 16.1

Listarakenne (ArrayList-luokka)

T Olio-ohjelmointi Osa 5: Periytyminen ja polymorfismi Jukka Jauhiainen OAMK Tekniikan yksikkö 2010

Metropolia ammattikorkeakoulu TI00AA : Ohjelmointi Kotitehtävät 3

tään painetussa ja käsin kirjoitetussa materiaalissa usein pienillä kreikkalaisilla

AS C-ohjelmoinnin peruskurssi 2013: C-kieli käytännössä ja erot Pythoniin

815338A Ohjelmointikielten periaatteet Harjoitus 7 Vastaukset

Sisällys. 17. Ohjelmoinnin tekniikkaa. Aritmetiikkaa toisin merkiten. for-lause lyhemmin

LOAD R1, =2 Sijoitetaan rekisteriin R1 arvo 2. LOAD R1, 100

Tietojen syöttäminen ohjelmalle. Tietojen syöttäminen ohjelmalle Scanner-luokan avulla

Tutoriaaliläsnäoloista

Ohjausrakenteet. Valinta:

Table of Contents. T Olio-ohjelmointi C/C++ perusteita Jukka Jauhiainen OAMK Tekniikan yksikkö 2010, 2011

Sisällys. 3. Pseudokoodi. Johdanto. Johdanto. Johdanto ja esimerkki. Pseudokoodi lauseina. Kommentointi ja sisentäminen.

Omat tietotyypit. Mikä on olio?

Ohjelmoinnin perusteet Y Python

SQL-perusteet, SELECT-, INSERT-, CREATE-lauseet

ITKP102 Ohjelmointi 1 (6 op)

Tähtitieteen käytännön menetelmiä Kevät 2009 Luento 4: Ohjelmointi, skriptaus ja Python

Ohjelmoinnin peruskurssi Y1

Transkriptio:

Osoittimet 7 Osoittimet On aika siirtyä käsittelemään osoittimia, C++:lle elintärkeätä ominaisuutta. Osoittimet ovat tärkeitä, koska ne luovat perustan muistin dynaamiselle varaukselle ja käytölle. Ne tekevät muillakin tavoilla ohjelmistasi tehokkaita. Tässä luvussa käsittelemme: Mikä on osoitin ja miten sellainen esitellään Miten saadaan selville muuttujan osoite Miten osoittimet liittyvät taulukoihin Miten osoitinlaskenta toimii ja mihin sitä käytetään Mitä standardikirjaston funktioita on käytettävissä null-merkkiin päättyvien merkkijonojen käsittelyyn Miten muistia varataan uusille muuttujille ohjelman suoritusaikana Miten dynaamisesti varattu muisti vapautetaan Miten tietyn tyyppinen osoitin muunnetaan toisentyyppiseksi Mikä on osoitin? Jokaisella muuttujalla ja literaalilla on osoite muistissa - eli sijainti tietokoneen muistissa, johon tieto talletetaan. Vastaavasti funktioiden täytyy sijaita jossain kohtaa muistissa, jotta ne voidaan suorittaa; eli funktiolla on myös osoite. Nämä osoitteet riippuvat siitä, mihin ohjelmasi on muistissa ladattu käynnistäessäsi sen, joten osoitteet saattavat olla erit ohjelman eri suorituskerroilla. Osoitin on muuttuja, johon voidaan tallettaa muistiosoite. Osoittimeen talletettu osoite vastaa yleensä muuttujan sijaintikohtaa muistissa, mutta se voi olla myös funktion osoite, kuten näemme seuraavassa luvussa. 231

C++ Ohjelmoijan käsikirja Heksadesimaalinen muistiosoite 1000 1004 1008 100C 12345 long-tyyppinen muuttuja Sisältö on toisen muuttujan osoite 1000 301C 3020 3024 3028 Osoitinmuuttuja Kaaviosta huomaat, mistä osoitin on saanut nimensä: se osoittaa muistikohtaan, johon on talletettu jotain tietoa - muuttuja tai funktio. Ei kuitenkaan riitä, että osoitin tallettaa muistiosoitteen. Jotta kyseiseen muistipaikkaan talletettua tietoa voidaan käyttää hyväksi, tarvitaan myöskin tietoa mitä muistipaikassa on, ei pelkästään missä se on. Kuten tiedät, kokonaisluvulla on varsin erilainen esitysmuoto kuin liukuluvulla ja tavallisesti kokonaisluvun tallettamiseen tarvitaan vähemmän tavujakin. Eli, jotta osoittimen sisältämän osoitteen muistipaikan tietoa voidaan käyttää hyväksi, tarvitaan myös tiedon tyyppi. Tästä yksinkertaisesta logiikasta seuraa se, että osoitin ei ole pelkkä osoitin; se on osoitin tietyntyyppiseen tietoalkioon. Tämä tulee selvemmäksi, kun siirrymme yksityiskohtiin. Käsitellään ensiksi osoittimien luontia. Osoittimen esittely Osoittimen esittely on samanlainen kuin tavallisenkin muuttujan esittely. Erona on se, että osoittimen tyypin perässä on asteriski, mikä kertoo, että olemme esittelemässä muuttujaa, joka on osoitin sen tyyppiseen tietoon. Jos esimerkiksi haluat esitellä osoittimen pnumero, joka osoittaa long-tyyppiseen arvoon, voit käyttää seuraavaa lausetta: long* pnumero; 232 Tämä esittelee osoitinmuuttujan pnumero, joka voi tallettaa long-tyyppisen muuttujan osoitteen. Tämän muuttujan tyyppi on osoitin long-tyyppiseen tietoon, ja kun tyyppi kirjoitetaan yksinään (esimerkiksi eksplisiittisessä tyypinmuunnoksessa), se kirjoitetaan yleensä long*. Yllä oleva esittely on kirjoitettu siten, että asteriski kirjoitettiin tyypin perään, mutta se ei ole ainut tapa kirjoittaa sitä. Voit kirjoittaa esittelyn myös siten, että asteriski on muuttujan nimen edessä, kuten lauseessa: long *pnumero;

Osoittimet Tämä esittelee täsmälleen saman muuttujan kuin edelläkin. Kääntäjä hyväksyy molemmat muodot, mutta ensimmäinen on ehkä yleisempi, koska se ilmaisee paremmin tyypin osoitin long-tyyppiseen tietoon. Tässä on kuitenkin sekaannuksen vaara, jos sekoitat tavallisten muuttujien ja osoittimen esittelyjä samaan lauseeseen. Mieti, mitä seuraava lause tekee: long* pnumero, numero; Se määrittelee muuttujan pnumero, jonka tyyppi on osoitin long-tyyppiseen tietoon ja muuttujan numero, joka on tyyppiä long. Eli esitystapa, jossa asteriski yhdistetään tyypin nimeen, ei tuo tätä selkeästi esiin. Jos olisit määritellyt nämä kaksi muuttujaa käyttäen toista muotoa: long *pnumero, numero; Tämä on selkeämpi, koska asteriski on nyt selvästi liitetty muuttujaan pnumero. Paras ratkaisu tähän ongelmaan on kuitenkin se, että ei edes kirjoiteta niitä samaan lauseeseen. Paras tapa on esitellä kaikki muuttujat omilla riveillään, jolloin vältetään kaikki mahdollisesti epäselvyydet: long numero; long* pnumero; //long-tyyppisen muuttujan esittely // osoitin long-tyyppiseen tietoon -esittely Tästä on sekin lisähyöty, että voimme helposti lisätä kommentit muuttujien perään. Esimerkissämme käytimme tunnistetta pnumero osoitinmuuttujan nimenä. Se ei ole pakollista, mutta käytännön sopimuksena C++:ssa on käyttää osoittimien nimien edessä p-merkkiä (osoitin, pointer ). Tällä tavalla ohjelmasta näkyy selvästi, mitkä muuttujat ovat osoittimia, ja tämä helpottaa ohjelman lukemista. Osoittimet muuntyyppiseen tietoon kuin long esitellään aivan samaan tapaan. Voimme esimerkiksi esitellä muuttujat, jotka osoittavat double- ja string-tyyppiseen tietoon lauseilla: double* parvo; string* plause; Osoittimien käyttö //Osoitin double-tyyppiseen tietoon //Osoitin string-tyyppiseen tietoon Jotta voimme käyttää osoitinta, meidän tulee tallettaa siihen toisen, sopivan tyyppisen muuttujan osoite. Katsotaan seuraavaksi, miten saamme selville muuttujan osoitteen. Osoite-operaattori Osoite-operaattori & on unaarinen operaattori, joka kertoo muistiosoitteen, jossa kyseinen muuttuja sijaitsee. Voimme esitellä osoittimen pnumero ja muuttujan numero lauseilla: long numero = 12345; long* pnumero; Koska muuttujan numero ja osoittimen pnumero tyypit ovat yhteensopivat, voimme kirjoittaa seuraavan sijoituksen: pnumero = № //Sijoitetaan numeron osoite osoittimeen pnumero 233

C++ Ohjelmoijan käsikirja Tämä tarkoittaa, että osoittimeen pnumero sijoitetaan muuttujan numero osoite. Tämän operaation tulos selvitetään alla olevassa kaaviossa: Heksadesimaalinen muistiosoite 1000 1004 1008 100C 12345 long-tyyppinen muuttuja numero pnumero = &numero; Numeron osoite on talletettu osoittimeen 1000 301C 3020 3024 3028 pnumber on tyyppiä osoitin long- tyyppiseen tietoon Voit lukea &-operaattorilla minkä tyyppisen muuttujan osoitteen tahansa, mutta osoite tulee tallettaa sopivan tyyppiseen osoittimeen. Jos esimerkiksi haluat tallettaa double-tyyppisen muuttujan osoitteen, osoitin tulee olla esitelty double*-tyyppiseksi. Jos yrität tallettaa osoitteen väärän tyyppiseen osoittimeen, ohjelmasi ei käänny. Muuttujan osoitteen lukeminen ja sen tallettaminen osoittimeen on kaikki ihan ok, mutta kaikkein kiinnostavinta kuitenkin on, miten osoitinta voidaan käyttää; eli osoittimen osoittaman muistipaikan sisällön käsittely. Tämä tehdään osoitus-operaattorin avulla. Osoitus-operaattori Osoitus-operaattoria * käytetään osoittimen kanssa, jotta pääsemme käsiksi osoittimen osoittaman muistiosoitteen sisältöön. Katsotaan tätä käytännössä. Kokeile itse - Osoitus-operaattorin käyttö Tämä esimerkki havainnollistaa osoitus-operaattorin käyttöä. Sen avulla tulostetaan osoittimen osoittaman muuttujan sisältö: // Esimerkki 7.1 - Osoitus-operaattorin käyttö #include <iostream> using namespace std; int main() 234

Osoittimet long numero = 50L; long* pnumero; // Osoittimen esittely pnumero = &numero; // Talletetaan numeron osoite cout << endl << "Muuttujaan numero talletettu arvo on " << *pnumero << endl; return 0; Kun käännät ja suoritat ohjelman, se tulostaa: Muuttujaan numero talletettu arvo on 50 Kuinka se toimii Ensiksi esittelemme tavallisen long-tyyppisen muuttujan, joka alustetaan arvolla 50. long numero = 50L; Seuraavaksi esittelemme osoitinmuuttujan: long* pnumero; // Osoittimen esittely Koska osoittimen tyyppi on long*, se voi tallettaa long-tyyppisen muuttujan osoitteen. Talletamme muuttujan numero osoitteen osoittimeen pnumero lauseella: pnumero = &numero; // Talletetaan numeron osoite Osoite-operaattorilla & luemme muuttujan numero muistiosoitteen, joka talletetaan osoittimeen pnumero. Voimme nyt tulostaa muuttujaan numero talletetun arvon osoittimen pnumero avulla. Osoittimen yhteydessä käytetty osoitus-operaattori viittaa osoittimen osoittaman muistipaikan sisältöön. Koska pnumero sisältää muuttujan numero osoitteen, *pnumero viittaa muuttujan numero arvoon. Osoitus-operaattorin käyttö saattaa ainakin aluksi tuntua sekavalta, koska meillä on nyt useita eri käyttötarkoituksia samalle *-merkille. Se on kertolaskuoperaattori, osoitus-operaattori ja sitä käytetään myös osoittimen esittelyssä. Joka kerta kun käytät *-merkkiä, kääntäjä pystyy kuitenkin päättelemään sen merkityksen. Kun kerrot kaksi lukua keskenään - esimerkiksi hinta * maara - tätä lauseketta ei voida tulkita muuksi kuin kertolaskuksi. Seuraava esimerkkiohjelma (7.2) havainnollistaa, että kääntäjä todella pystyy tähän päättelyyn. Miksi käyttää osoittimia? Tässä kohtaa tulee yleensä mieleen, Miksi ylipäätään tulisi käyttää osoittimia? Näyttäähän siltä, että jo olemassa olevan muuttujan osoitteen lukeminen ja tallettaminen osoittimeen vain sen takia, että voit myöhemmin viitata muuttujaan osoittimen avulla, olisi tarpeetonta ja tulisit toimeen myös ilman osoitinta. Älä kiirehdi asioiden edelle - osoittimille on useita tärkeitä käyttötarkoituksia! 235

C++ Ohjelmoijan käsikirja Ensinnäkin, kuten kohta näet, voit käyttää osoitinmuotoa taulukon alkioiden käsittelyssä, joka yleensä toimii nopeammin kuin taulukkomuodon käyttö. Toiseksi, kun määrittelemme omia funktioitamme myöhemmin tässä kirjassa, huomaat, että osoittimia käytetään runsaasti, jotta funktion sisältä päästään käsiksi funktion ulkopuolella määriteltyihin suuriin tietojoukkoihin, kuten taulukoihin. Kolmanneksi, mikä onkin kaikkein tärkein syy, näet myöhemmin, kuinka osoittimien avulla voit varata muistia uusille muuttujille dynaamisesti - eli ohjelman suorituksen aikana. Tämä mahdollistaa ohjelmasi säätävän käyttämäänsä muistia syötteen mukaan. Voit luoda uusia muuttujia suorituksen aikana aina kun tarvitset sellaisen. Koska et etukäteen tiedä, kuinka monta muuttujaa luot dynaamisesti, ainut tapa tehdä se on käyttää osoittimia - joten opettele osoittimien käyttö tarkasti! Jotta saamme lisää tuntumaa osoittimiin, katsotaan toista varsin yksinkertaista esimerkkiä, joka havainnollistaa edelleen osoittimien käyttöä. Kokeile itse - Osoittimien käyttö Voimme kokeilla jo käsittelemiämme osoittimien operaatioita esimerkin avulla: // Esimerkki 7.2 - Osoittimien käyttö #include <iostream> using namespace std; int main() long* pnumero; long numero1 = 55L; long numero2 = 99L; // Osoittimen esittely // Joitakin muuttujia pnumero = &numero1; // Talleta osoite osoittimeen *pnumero += 11; // Kasvatetaan muuttujaa numero1 cout << endl << "numero1 = " << numero1 << " &numero1 = " << pnumero << endl; pnumero = &numero2; numero1 = *pnumero * 10; // Muutetaan osoitin osoittamaan numero2:ta // 10 kertaa numero2 cout << "numero1 = " << numero1 << " pnumero = " << pnumero << " *pnumero = " << *pnumero << endl; return 0; Minun tietokoneessa ohjelma tulostaa seuraavaa: 236 numero1 = 66 &numero1 = 0068FDF4 numero1 = 990 pnumero = 0068FDF0 *pnumero = 99

On todennäköistä, että osoitteiden arvot sinun tietokoneessasi ovat erit kuin edellä olleet. Osoittimet Kuinka se toimii Tällä esimerkillä ei ole lainkaan syötteitä; kaikki operaatiot suoritetaan ohjelmassa määritetyillä arvoilla. Kun muuttujan numero1 osoite on talletettu osoittimeen pnumero, muuttujan numero1 arvoa kasvatetaan epäsuorasti osoittimen avulla: *pnumero += 11; // Kasvatetaan muuttujaa numero1 Osoitus-operaattori määrittelee, että lisäämme luvun 11 osoittimen osoittamaan muuttujaan numero1. Tämä havainnollistaa, että voimme kirjoittaa osoittimen ja osoitus-operaattorin sijoitusoperaattorin vasemmalle puolelle. Jos jätämme *-merkin pois, ohjelma yrittää muuttaa osoittimeen talletettua osoitetta. (Käsittelemme osoitinaritmetiikkaa hieman myöhemmin.) Seuraava lause tulostaa muuttujan numero1 arvon ja numero1:n osoitteen, joka on talletettu osoittimeen pnumero: cout << endl << "numero1 = " << numero1 << " &numero1 = " << pnumero << endl; Jos lähetämme numeerisen osoittimen nimen sellaisenaan (eli tässä tapauksessa, pnumero) tulostusvirtaan, tulostuu osoite. Koska kyseessä on osoitin, tulostetaan arvo heksadesimaalisena. Muistiosoitteet esitetään yleensä heksadesimaalimuodossa - muissakin kielissä kuin C++:ssa. Koska muuttuja numero on tavallinen kokonaislukumuuttuja, sen arvo tulostetaan desimaalimuodossa. Ensimmäisen tulostusrivin jälkeen osoittimen pnumero sisällöksi muutetaan muuttujan numero2 osoite: pnumero = &numero2; // Muutetaan osoitin osoittamaan numero2:ta Nyt pnumero osoittaa muuttujaan numero2. Muuttujan numero1 osoite, joka aikaisemmin oli osoittimessa pnumero, kirjoitetaan yli. Voimme nyt muuttaa muuttujan numero1 arvoksi 10 kertaa numero2, johon viitataan osoittimen avulla: numero1 = *pnumero * 10; // 10 kertaa numero2 Sijoitusoperaattorin oikealla puolella olevassa lausekkeessa kerrotaan muuttujan numero2 arvoa epäsuorasti osoittimen avulla luvulla 10. Kääntäjä tietää, miten *-merkit tulkitaan tässä lausekkeessa. Seuraava tulostuslause tulostaa laskutoimitusten tulokset: cout << "numero1 = " << numero1 << " pnumero = " << pnumero << " *pnumero = " << *pnumero << endl; Tämä tulostaa muuttujan numero1 arvon, osoittimeen pnumero talletetun osoitteen ja osoittimen pnumero osoittaman muistipaikan sisällön. Jälleen osoittimen pnumero arvo tulostetaan heksadesimaalisena, koska se on osoite. Lauseke *pnumero tarkoittaa tavallista kokonaislukuarvoa - muuttujan numero2 arvoa - joten se tulostetaan desimaalisena. 237

C++ Ohjelmoijan käsikirja Osoittimien alustus Jos mahdollista, alustamattomien osoittimien käyttö on vielä vaarallisempaa kuin alustamattomien muuttujien käyttö. Jos osoittimessa on roskaa, saatat kirjoittaa satunnaiseen muistialueeseen. Tulos riippuu aivan siitä, kuinka epäonninen olit, joten osoittimien alustaminen on enemmän kuin suositeltavaa. Osoittimen alustaminen jo määritellyn muuttujan osoitteen mukaan on hyvin helppoa. Voit alustaa osoittimen pnumero muuttujan numero osoitteella yksinkertaisesti käyttämällä osoiteoperaattoria yhdessä muuttujan nimen kanssa osoittimen alkuarvona: int numero = 0; int* pnumero = &numero; //Alustetaan kokonaislukumuuttuja //Alustetaan osoitin Kun alustat osoittimen käyttämällä toista muuttujaa kuten edellä, muista, että muuttujan tulee olla esitelty ennen osoittimen esittelyä. Jos näin ei ole, kääntäjäsi valittaa asiasta. Jos et halua alustaa osoitinta minkään muuttujan osoitteella, voit alustaa osoittimen alkuarvoksi nollan: int* pnumero = 0; //Osoitin, joka ei osoita mihinkään Tämä esittely varmistaa, että pnumero ei sisällä mitään. Näin ollen, jos yrität käyttää sitä epäsuorassa viittauksessa ennen kuin siinä on arvo, ohjelmasi epäonnistuu tavalla, josta varmasti tiedät mitä tapahtui. Tällä tavalla alustettu osoitin on ns. null-osoitin. Tietysti voit aina testata onko osoitin null, ennen kuin käytät sitä: if(pnumero == 0) cout << endl << pnumero on null. ; Muista käyttää vertailussa yhtäsuuruusmerkkejä ==! Voit yhtä hyvin käyttää seuraavaa vastaavaa lausetta: if(pnumero) cout << endl << pnumero on null. ; Tietysti voit käyttää myös tätä muotoa: if(pnumero!= 0) //Osoitin ei ole null, tee jotain... Symboli NULL on määritelty standardikirjastossa arvoksi 0, ja sitä käytetäänkin usein alustettaessa osoitin nulliksi. NULL on kuitenkin olemassa vain yhteensopivuuden vuoksi C-kielen kanssa. C++-kielessä suositellaan käytettäväksi arvoa 0. 238

char-tyyppisten osoittimien alustus Osoittimet Muuttujalla, jonka tyyppi on osoitin char-tyyppiseen tietoon, on kiinnostava ominaisuus: se voidaan alustaa merkkijonoliteraalilla. Voimme esimerkiksi esitellä ja alustaa tällaisen osoittimen lauseella: char* psananlasku = Tavat tekevät miehen. ; Tämä näyttää varsin samanlaiselta kuin char-tyyppisen taulukon alustus, mutta älä anna ulkomuodon hämätä: se on varsin erilainen. Lause luo null-merkkiin päättyvän merkkijonoliteraalin (itse asiassa const char -tyyppisen taulukon) lainausmerkkien välissä olevista merkeistä ja tallettaa merkkijonoliteraalin ensimmäisen merkin osoitteen osoittimeen psananlasku. Tätä havainnollistetaan seuraavassa kaaviossa: char *psananlasku = "Tavat tekevät miehen. ; Osoite talletetaan psananlasku 2000 Osoite:2000 T a v a t t e k e v ä t m I e h e n. Merkkijonoliteraali talletetaan null-merkkiin päättyvänä merkkijonona Kaikki ei kuitenkaan ole aivan miltä näyttää. Merkkijonoliteraalin tyyppi on const, mutta osoittimen tyyppi ei ole. Lause ei luo merkkijonosta muokattavaa kopiota; se vain tallettaa ensimmäisen merkin osoitteen. Tämä tarkoittaa sitä, että jos kirjoitat koodia, joka yrittää muuttaa merkkijonoa, kuten seuraavan lauseen, joka yrittää muuttaa ensimmäisen merkin merkiksi X *psananlasku = X ; kääntäjä ei valita, koska se ei näe tässä mitään väärää. Mutta kun yrität suorittaa ohjelman, saat virheilmoituksen: merkkijonoliteraali on yhä vakio, jonka arvoa ei saa muuttaa. Saatat oikeutetusti ihmetellä, miksi kääntäjä salli sijoittaa const-tyyppisen arvon ei-consttyyppiin, koska tämähän aiheutti ongelman. Syynä on se, että merkkijonoliteraalit ovat tulleet vakioiksi vasta C++-standardin myötä ja olemassa on runsaasti ohjelmia, joissa käytetään väärää sijoitusta. Sen käyttöä ei kuitenkaan enää suositella ja ongelman oikea ratkaisutapa olisi esitellä osoitin seuraavasti: const char* psananlasku = Tavat tekevät miehen. ; Tämä esittelee, että psananlasku osoittaa const-tyyppiseen tietoon, eli se on yhteensopiva merkkijonoliteraalin tyypin kanssa. const-avainsanan käytöstä osoittimien yhteydessä on vielä paljon muutakin asiaa, joten palaamme tähän aiheeseen myöhemmin tässä luvussa. Nyt katsomme toisen esimerkin avulla, kuinka char*-tyyppisiä muuttujia käytetään. 239

C++ Ohjelmoijan käsikirja Kokeile itse - Tähtinäyttelijät osoittimien avulla Voimme kirjoittaa uuden version tähtinäyttelijät -ohjelmastamme (esimerkki 6.5), jossa käytetään osoittimia taulukon sijaan: // Esimerkki 7.3 - Osoittimien alustus merkkijonoilla #include <iostream> using namespace std; int main() // Tähtinäyttelijöihin viitataan osoittimien avulla const char* ptahti1 = "Mae West"; const char* ptahti2 = "Arnold Schwarzenegger"; const char* ptahti3 = "Lassie"; const char* ptahti4 = "Slim Pickens"; const char* ptahti5 = "Greta Garbo"; const char* ptahti6 = "Oliver Hardy"; const char* ptahti = "Tähtinäyttelijäsi on "; int valinta = 0; // Tähden valinta cout << endl << "Valitse tähtinäyttelijäsi!" << " Syötä numero väliltä 1-6: "; cin >> valinta; cout << endl; switch(valinta) case 1: cout << ptahti << ptahti1; break; case 2: cout << ptahti << ptahti2; break; case 3: cout << ptahti << ptahti3; break; case 4: cout << ptahti << ptahti4; break; case 5: cout << ptahti << ptahti5; break; case 6: cout << ptahti << ptahti6; break; default: cout << "Valitan, tähtinäyttelijääsi ei löydy."; 240 cout << endl; return 0;

Osoittimet Ohjelman esimerkkitulostus näyttää seuraavalta: Valitse tähtinäyttelijäsi! Syötä numero väliltä 1-6: 5 Tähtinäyttelijäsi on Greta Garbo Kuinka se toimii Alkuperäisen esimerkkimme taulukko on korvattu kuudella osoittimella, ptahti1 - ptahti6. Jokainen niistä alustetaan nimellä. Esittelemme myöskin osoittimen ptahti, joka alustetaan lauseella, jonka haluamme tulostaa normaalin tulostusrivin eteen. Koska kaikkia näitä osoittimia käytetään merkkijonoliteraaleihin osoittamiseen, määrittelemme ne const-tyyppisiksi. Koska meillä on erilliset osoittimet, oikean viestin valinta tapahtuu helpommin switch-lauseella kuin alkuperäisessä versiossa käytetyllä if-lauseella. Kielletyt arvot käsitellään kaikki switchlauseen default-osassa. Osoittimen osoittaman merkkijonon tulostaminen ei voisi olla helpompaa. Kuten huomaat, kirjoitamme yksinkertaisesti osoittimen nimen. Nyt olet saattanutkin jo huomata, että standarditulostusvirta cout kohtelee eri tyyppisiä osoittimien nimiä eri tavalla. Esimerkissä 7.2 lause cout << pnumero; tulostaisi osoittimen pnumero sisältämän osoitteen. Tässä esimerkissä lause: cout << ptahti1; tulostaisi kuitenkin merkkijonoliteraalin - ei osoitetta. Tämä ero johtuu siitä, että pnumero on osoitin numeeriseen tyyppiin, kun taas ptahti1 on osoitin char-tyyppiseen tietoon. Tulostusvirta cout käsittelee muuttujaa, joka on osoitin char-tyyppiseen tietoon, null-merkkiin päättyvänä merkkijonona ja tulostaa sen myös sellaisena. No mitkä ovat tämän version edut? Osoittimien käyttö poisti muistin tuhlauksen, jota tapahtui tämän ohjelman taulukkoratkaisussa, koska nyt jokainen merkkijono vie juuri sen verran tavuja kuin se tarvitsee. Ohjelma näyttää kuitenkin hieman ylipitkältä. Jos nyt ajattelet, että Täytyy olla parempikin tapa, olet aivan oikeassa - voimme käyttää osoitintaulukkoa. Kokeile itse - Osoitintaulukko char-tyyppisessä osoitintaulukossa jokainen alkio osoittaa erilliseen merkkijonoon ja jokaisen merkkijonon pituus voi olla eri. Voimme esitellä osoitintaulukon samaan tapaan kuin esittelemme minkä tahansa muunkin taulukon. Seuraavassa on uusi versio edellisestä esimerkistä. Siinä käytetään osoitintaulukkoa: // Esimerkki 7.4 - char-tyyppisen osoitintaulukon käyttö #include <iostream> using namespace std; int main() 241

C++ Ohjelmoijan käsikirja const char* ptahdet[] = "Mae West", // Alustetaan osoitintaulukko "Arnold Schwarzenegger", "Lassie", "Slim Pickens", "Greta Garbo", "Oliver Hardy" ; const char* ptahti = "Tähtinäyttelijäsi on "; int valinta = 0; const int tahtilkm = sizeof ptahdet / sizeof ptahdet[0]; // Taulukon koko cout << endl << "Valitse tähtinäyttelijäsi!" << " Syötä numero väliltä 1 - " << tahtilkm << ": "; cin >> valinta; cout << endl; if(valinta >= 1 && valinta <= tahtilkm) // Tarkistetaan syöte cout << ptahti << ptahdet[valinta - 1]; // Tulostetaan tähden nimi else cout << "Valitan, tähtinäyttelijääsi ei löydy."; // Kielletty syöte cout << endl; return 0; Kuinka se toimii Tämän paremmaksi tätä ohjelmaa ei juuri enää saa. Käytössämme on yksiulotteinen taulukko char-osoittimia esiteltynä siten, että kääntäjä päättelee alkuarvoluettelon perusteella, minkä kokoinen taulukon tulee olla: const char* ptahdet[] = "Mae West", // Alustetaan osoitintaulukko "Arnold Schwarzenegger", "Lassie", "Slim Pickens", "Greta Garbo", "Oliver Hardy" ; 242

Osoittimet Tämän lauseen jälkeen muistin käyttö näkyy alla olevasta kaaviosta: M a e W e s t \0 9 tavua Osoitintaulukko A r n o l d S c h w a r z e n e g g e r \0 22 tavua L a s s i e \0 7 tavua S l i m P i c k e n s \0 13 tavua Kukin osoitintaulukon alkio on saman kokoinen - yleensä 4 tavua - joten tämän taulukon koko on 24 tavua. G r e t a G a r b o \0 O l i v e r H a r d y \0 Muistin kokonaiskulutus on 99 tavua. 11 tavua 13 tavua Kuten huomaat, muistia tarvitaan jokaiselle null-merkkiin päättyvälle merkkijonolle sekä taulukon alkioille, jotka ovat osoittimia. Eli yhteensä 99 tavua. Verrattuna char-tyyppiseen taulukkoon, osoitintaulukko tarvitsee vähemmän muistia. Vanhassa staattisessa taulukossa jokaisen rivin pituus on vähintään pisimmän merkkijonon pituus; 6 riviä, jokainen 22 tavua, on yhteensä 132 tavua. Osoitintaulukon avulla säästimme 33 tavua. Säästö riippuu tietysti merkkijonojen lukumäärästä ja pituuksien erilaisuuksista. Joskus säästöä ei synny lainkaan, mutta yleisesti ottaen osoitintaulukko on tehokkaampi. Tilan säästö ei ole ainut hyöty, jonka saat osoittimien käytöstä. Monissa tapauksissa säästät myös suoritusajassa. Mieti esimerkiksi, mitä tapahtuu, jos haluat vaihtaa viidennessä alkiossa olevan merkkijonon Greta Garbo ensimmäisessä alkiossa olevaan merkkijonoon Mae West. Tämä on tyypillinen lajitteluoperaatio, jossa lajittelet merkkijonot aakkosjärjestykseen. Yllä olevassa osoitintoteutuksessa sinun täytyy ainoastaan vaihtaa osoittimet keskenään - merkkijonot voivat sijaita siellä missä ennenkin. Jos merkkijonot olisi talletettu char-tyyppiseen taulukkoon, tarvittaisiin suuri kopiointioperaatio. Koko merkkijono Greta Garbo tulisi kopioida väliaikaiseen muistipaikkaan, kun Mae West kopioidaan sen tilalle. Tämän jälkeen Greta Garbo tulisi kopioida uuteen paikkaan. Tämä vaatii huomattavasti enemmän suoritusaikaa. Tämä logiikka pätee yhtä hyvin string-tyyppisiin olioihinkin. Palataan takaisin esimerkkiimme. Talletamme perusviestin osoitteen toiseen osoittimeen: const char* ptahti = "Tähtinäyttelijäsi on "; Tämän jälkeen laskemme osoitintaulukon ptahdet alkioiden lukumäärän seuraavalla lauseella: const int tahtilkm = sizeof ptahdet / sizeof ptahdet[0]; // Taulukon koko Kun taulukon koko lasketaan tällä tavalla, ohjelman loppuosa käyttää automaattisesti oikeaa taulukon kokoa. Käytämme tätä taulukon kokoa pyytäessämme syötettä, joka talletetaan muuttujaan valinta: 243

C++ Ohjelmoijan käsikirja cout << endl << "Valitse tähtinäyttelijäsi!" << " Syötä numero väliltä 1 - " << tahtilkm << ": "; Luettuamme muuttujan valinta arvon samaan tapaan kuin edellisessä esimerkissämmekin, valitsemme tulostettavan merkkijonon yksinkertaisella if-lauseella. Tulostamme joko valitun merkkijonon taulukosta ptahdet tai sopivan viestin, että käyttäjä syötti kielletyn valinnan. iflauseessa käytetään muuttujaa tahtilkm valinnan ylärajana, eli nytkin ohjelma mukautuu automaattisesti valittavien tähtien lukumäärään. Jos haluat ohjelmaan enemmän valintoja, voit yksinkertaisesti lisätä niitä alkuarvoluetteloon. Merkkijonojen lajittelu osoittimien avulla Kuten edellisen esimerkin läpikäynnissä mainitsimme, voit lajitella merkkijonoja ilman, että itse merkkijonoja täytyy siirtää paikasta toiseen, jos käytät niihin osoittavia osoittimia. Voimme tehdä uuden version esimerkin 6.10 ohjelmasta, jossa otimme sanoja merkkijonosta. Tämä antaa meille arvokasta kokemusta string-tyyppisten olioiden käsittelystä ja stringtyyppisiin olioihin osoittavista osoitintaulukoista. Samalla saamme käytännön kokemusta lajittelusta osoittimien avulla. Kokeile itse - Merkkijonojen lajittelu osoittimien avulla Luemme näppäimistöltä merkkijonon ja lajittelemme sen sanat haluamaamme järjestykseen. Tässä on koodi: // Esimerkki 7.5 - Merkkijonojen lajittelu osoittimien avulla #include <iostream> #include <string> using namespace std; int main() string teksti; const string erottimet = ",.\"\n"; const int max_sanoja = 1000; string sanat[max_sanoja]; string* psanat[max_sanoja]; // Lajiteltava merkkijono // Sanojen erotinmerkit // Sanojen maksimimäärä // Sanojen taulukko // osoitintaulukko sanoihin // Luetaan merkkijono näppäimistöltä cout << endl << "Syötä merkkijono, # lopettaa:" << endl; getline(cin, teksti, '#'); 244 // Otetaan tekstin kaikki sanat erilleen int alku = teksti.find_first_not_of(erottimet);// Sanan alkuindeksi int loppu = 0; // Loppuerotinmerkin indeksi int sanalkm = 0; // Sanojen lukumäärä while(alku!= string::npos && sanalkm < max_sanoja) loppu = teksti.find_first_of(erottimet, alku + 1);

Osoittimet if(loppu == string::npos) loppu = teksti.length(); // Löytyikö erotinmerkki? // Ei löytynyt sanat[sanalkm] = teksti.substr(alku, loppu - alku); // Talletetaan sana psanat[sanalkm] = &sanat[sanalkm]; // Talletetaan osoitin sanalkm++; // kasvatetaan lukumäärää // Etsitään seuraavan sanan ensimmäinen merkki alku = teksti.find_first_not_of(erottimet, loppu + 1); // lajitellaan sanat nousevaan järjestykseen suoralla osoituksella int pienin = 0; // Pienimmän sanan indeksi for(int j = 0; j < sanalkm - 1; j++) pienin = j; // Asetetaan pienin // Tarkistetaan nyk sana kaikkiin muihin sen jäljessä oleviin for(int i = j + 1 ; i < sanalkm ; i++) if(*psanat[i] < *psanat[pienin]) // Nykyinen on pienin? pienin = i; if(pienin!= j) // Vaihdetaan osoittimet string* papu = psanat[j]; // Talletetaan nykyinen psanat[j] = psanat[pienin]; // Pienin nykyiseen psanat[pienin] = papu; // Palautetaan nykyinen // Tulostetaan sanat nousevassa järjestyksessä for(int i = 0 ; i < sanalkm ; i++) cout << endl << *psanat[i]; cout << endl; return 0; Ohjelman esimerkkitulostus näyttää seuraavalta: Syötä merkkijono, # lopettaa: Tässä maailmassa mikään ei ole niin varmaa kuin kuolema ja verot.# Tässä ei ja kuin kuolema maailmassa mikään niin ole varmaa verot 245

C++ Ohjelmoijan käsikirja Kuinka se toimii Merkkijono, jonka sanat lajitellaan, luetaan muuttujaan, joka esitellään seuraavasti: string teksti; // Lajiteltava merkkijono const string erottimet = ",.\"\n"; // Sanojen erotinmerkit Vakio erottimet sisältää kaikki merkit, jotka toimivat sanojen erotinmerkkeinä; eli merkit välilyönti, pilkku, piste, lainausmerkki ja rivinvaihto. Voit lisätä tähän listaan sarkainmerkin, jos haluat. Vaihtoehtoisesti voisit rakentaa erottimien merkkijonon etsimällä merkkijonosta teksti kaikki merkit, jotka eivät ole kirjaimia, numeroita tai heittomerkkejä. Talletamme merkkijonosta teksti irrottamamme sanat taulukkoon, johon mahtuu maksimissaan 1000 sanaa: const int max_sanoja = 1000; // Sanojen maksimimäärä string sanat[max_sanoja]; // Sanojen taulukko string* psanat[max_sanoja]; // osoitintaulukko sanoihin Taulukkoon sanat talletetaan sanat ja osoitintaulukkoon psanat talletetaan sanat-taulukon kunkin alkion osoitteet. Tämä kuulostaa hankalalta, mutta tarvitsemme osoittimet, jos haluamme välttää merkkijonojen toistuvat kopioinnit, kun lajittelemme ne. Meidän täytyy lisäksi varata tilaa max_sanoja string-tyyppiselle oliolle ja osoittimelle, vaikka emme niitä oikeastaan tarvitsekaan. Myöhemmin tässä luvussa käsittelemme parempaa tapaa tehdä tämä, käyttäen dynaamista muistinvarausta. Luemme tarvittaessa useamman rivin tekstiä merkkijonoon teksti samaan tapaan kuin aikaisemminkin; syöttö lopetetaan #-merkillä, joten voit syöttää niin monta riviä kuin haluat: cout << endl << "Syötä merkkijono, # lopettaa:" << endl; getline(cin, teksti, '#'); Yksittäisten sanojen irrottaminen merkkijonosta teksti ja tallettaminen taulukkoon sanat tehdään while-silmukassa: // Otetaan tekstin kaikki sanat erilleen int alku = teksti.find_first_not_of(erottimet); // Sanan alkuindeksi int loppu = 0; // Loppuerotinmerkin indeksi int sanalkm = 0; // Sanojen lukumäärä while(alku!= string::npos && sanalkm < max_sanoja) loppu = teksti.find_first_of(erottimet, alku + 1); if(loppu == string::npos) // Löytyikö erotinmerkki? loppu = teksti.length(); // Ei löytynyt sanat[sanalkm] = teksti.substr(alku, loppu - alku); // Talletetaan sana psanat[sanalkm] = &sanat[sanalkm]; // Talletetaan osoitin sanalkm++; // kasvatetaan lukumäärää 246 // Etsitään seuraavan sanan ensimmäinen merkki alku = teksti.find_first_not_of(erottimet, loppu + 1);

Osoittimet Tämä toimii samalla logiikalla kuin viimeksi käsittelimme tämäntapaista ongelmaa. Etsimme sanan ensimmäisen kirjaimen indeksin ja talletamme sen muuttujaan alku. Etsimme sanaa seuraavan ensimmäisen erotinmerkin ja talletamme sen indeksin muuttujaan loppu. Tämän jälkeen käytämme substr()-funktiota irrottamaan sanan string-tyyppisenä oliona, jonka sitten talletamme sanat-taulukon seuraavaan vapaaseen alkioon. Talletamme myös tämän alkion osoitteen taulukon psanat vastaavaan alkioon. Silmukan ehtolauseke varmistaa, että lopetamme sanojen etsinnän, kun olemme saavuttaneet merkkijonon teksti lopun tai kun taulukko sanat täyttyy. Valmistelemme lajittelua esittelemällä muuttujan pienin, joka pitää kirjaa pienimmän sanan indeksistä lajittelun aikana: int pienin = 0; // Pienimmän sanan indeksi Sanojen lajittelu suoritetaan sisäkkäisissä for-silmukoissa: for(int j = 0; j < sanalkm - 1; j++) pienin = j; // Asetetaan pienin // Tarkistetaan nyk sana kaikkiin muihin sen jäljessä oleviin for(int i = j + 1 ; i < sanalkm ; i++) if(*psanat[i] < *psanat[pienin]) // Nykyinen on pienin? pienin = i; if(pienin!= j) // Vaihdetaan osoittimet string* papu = psanat[j]; // Talletetaan nykyinen psanat[j] = psanat[pienin]; // Pienin nykyiseen psanat[pienin] = papu; // Palautetaan nykyinen Operaatio järjestää psanat-taulukon osoittimet uudelleen siten, että ne osoittavat taulukon sanat sanoihin nousevassa järjestyksessä. Tämä tehdään varsin yksinkertaisella tavalla. Ulompi silmukka käy läpi kaikki taulukon psanat alkiot ensimmäisestä viimeiseen. Sisemmässä silmukassa vertaamme kunkin osoittimen osoittamaa sanaa kaikkiin kyseistä osoitinta seuraavien osoittimien osoittamiin sanoihin, jotta löydämme pienimmän indeksin. Jos käsittelemämme sana ei ole pienin, vaihdamme osoittimet käyttämällä osoitinta papu väliaikaisena talletuspaikkana. Tämä prosessi suoritetaan kaikille taulukon psanat alkioille. Tämän jälkeen ensimmäisessä alkiossa on osoitin pienimpään sanaan. Toinen alkio osoittaa seuraavaksi pienempään sanaan ja niin edelleen kaikille taulukkoon talletetuille osoittimille. Huomaa, että tulostuksessa Tässä tulee ennen ei :tä, koska minun tietokoneessani, joka käyttää ASCII-koodeja, merkki T on pienempi kuin e. Lopuksi tulostamme sanat nousevassa järjestyksessä: for(int i = 0 ; i < sanalkm ; i++) cout << endl << *psanat[i]; Taulukon psanat alkiot osoittavat sanoihin nousevassa järjestyksessä. Tulostaaksemme ne järjestyksessä, tulostanne taulukon osoittimien osoittamat sisällöt järjestyksessä. 247

C++ Ohjelmoijan käsikirja Tulostuksen ulkoasun parantaminen Yhden sanan tulostaminen yhdelle riville toimii pienen sanamäärän yhteydessä, mutta jos sanoja on paljon, se on varsin epämiellyttävää. Olisi hyvä, jos kaikki sanat, jotka alkavat samalla kirjaimella, tulostettaisiin ryhmänä. Jos haluat hieman monimutkaisemman tulostustavan, voit korvata edellä olleen for-silmukan seuraavalla koodilla: // Tulostetaan max. 6 sanaa samalle riville ryhmiteltynä alkukirjaimen mukaan char ch = (*psanat[0])[0]; // Ensimmäisen sanan ensimmäinen kirjain int sanoja_rivilla = 0; // Sanojen lukumäärä rivillä for(int i = 0; i < sanalkm ; i++) if(ch!= (*psanat[i])[0]) // Uusi ensimmäinen kirjain? cout << endl; // Aloitetaan uusi rivi ch = (*psanat[i])[0]; // Talletetaan uusi ensimmäinen kirjain sanoja_rivilla = 0; // Palautetaan sanojen laskuri cout << *psanat[i] << " "; if(++sanoja_rivilla == 6) // Joka kuudes sana cout << endl; // Aloitetaan uusi rivi sanoja_rivilla = 0; Tämä tulostaa kaikki sanat nousevassa järjestyksessä, mutta nyt kaikki sanat, jotka alkavat samalla kirjaimella, ryhmitellään yhteen ryhmään. Jokainen uusi ryhmä alkaa uudelta riviltä ja rivillä voi olla maksimissaan kuusi sanaa. Ryhmän ensimmäinen kirjain talletetaan muuttujaan ch. Huomaa, kuinka viittaamme ensimmäisen string-tyyppisen olion ensimmäiseen kirjaimeen. Lausekkeen *psanat[0] arvo on taulukon psanat ensimmäisen alkion osoittama sana. Lauseke (*psanat[0])[0] antaa sitten tämän sanan ensimmäisen kirjaimen, koska sulkeiden ulkopuolella olevat toiset hakasulkeet ovat sanan kirjaimia varten. Hakasulkeet ovat suoritusjärjestyksessä *-operaattorin edellä, joten sulkeet ovat tässä välttämättömät. Muuttuja sanoja_rivilla pitää kirjaa rivillä olevien sanojen lukumäärästä; kun se saavuttaa arvon 6, tulostetaan rivinvaihto. Jos tulostettavan sanan ensimmäinen kirjain on eri kuin muuttujan ch sisältö, aloitamme uuden ryhmän, joten tulostamme rivinvaihdon ja muuttujan sanoja_rivilla arvoksi palautetaan 0. 248 Osoitinvakiot ja osoittimet vakioihin Kun käsittelimme char-tyyppisiä osoittimia aikaisemmin tässä luvussa käsitellessämme merkkijonoliteraaleja näimme, että osoittimet vakioihin on tekniikka, johon C++:n standardi pakotti. Tähtinäyttelijät-ohjelmassamme varmistimme, että kääntäjä huomaa kaikki yritykset muuttaa taulukon ptahdet alkioiden osoittamia merkkijonoja, koska esittelimme taulukon const-tyyppisenä: const char* ptahdet[] = "Mae West", "Arnold Schwarzenegger", "Lassie", "Slim Pickens",

Osoittimet "Greta Garbo", "Oliver Hardy" ; Tässä esittelyssä esittelemme osoitintaulukon osoittamat oliot vakioiksi. Kääntäjä estää kaikki yritykset muuttaa niitä, joten seuraavanlainen sijoituslause aiheuttaa virheen kääntäjässä ja estää ikävän ongelman suoritusaikana: *ptahdet[0] = X ; Voimme kuitenkin aivan hyvin kirjoittaa seuraavan lauseen, joka kopioi sijoitusoperaattorin oikealla puolella olevan alkion osoitteen vasemmalla puolella olevaan alkioon: ptahdet[0] = ptahdet[5]; Nyt molemmat osoittimet osoittavat samaan nimeen. Huomaa, että tämä ei muuttanut osoitintaulukon alkion osoittaman olion sisältöä - se muutti vain alkion ptahdet[0] sisältämää osoitetta, eli const-määrettä ei tässä rikottu. Meidän pitäisi kyllä estää myös tällainen muutos. Katso seuraavaa lausetta: const char* const ptahdet[] = "Mae West", "Arnold Schwarzenegger", "Lassie", "Slim Pickens", "Greta Garbo", "Oliver Hardy" ; Uusi const-määre esittelee osoitinvakion, joten nyt sekä osoittimet että niiden osoittamat merkkijonot ovat vakioita. Kumpaakaan niistä ei voida muuttaa. Yhteenvetona voimme muodostaa kolme erilaista tilannetta const-määreen käytöstä osoittimien ja niiden osoittamien asioiden yhteydessä: Osoitin vakioon. Tässä sitä mihin osoitetaan, ei voida muuttaa, mutta voimme asettaa osoittimen osoittamaan johonkin muuhun: const char* pmerkkijono = Tekstiä, jota ei voi muuttaa ; Tämä pätee tietysti myös muuntyyppisiin osoittimiin. Esimerkiksi: const int arvo = 20; const int* parvo = &arvo; arvo on vakio, eikä sitä voi muuttaa. parvo on osoitin vakioon, joten siihen voidaan sijoittaa muuttujan arvo osoite. Et voisi sijoittaa osoitetta osoittimeen, joka ei ole osoitin vakioon (koska tällöin voisit muuttaa vakiota osoittimen kautta), mutta voit sijoittaa ei-vakiomuuttujan osoitteen osoittimeen parvo. Jälkimmäisessä tapauksessa muuttujan muokkaus osoittimen kautta ei olisi sallittua. Yleisesti ottaen const-tason kasvattaminen tällä tavalla on aina mahdollista, mutta pienentäminen ei ole mahdollista. 249

C++ Ohjelmoijan käsikirja Osoitinvakio. Tässä osoittimeen sijoitettua osoitetta ei voida muuttaa, joten tällainen osoitin voi osoittaa vain siihen osoitteeseen, johon se alustettiin. Osoittimen osoittaman muistipaikan sisältö ei kuitenkaan ole vakio ja sitä voidaan muuttaa. Otetaan numeerinen esimerkki, joka selventää osoitinvakioita. Oletetaan, että esittelemme kokonaislukumuuttujan arvo ja osoitinvakion parvo seuraavasti: int arvo = 20; int* const parvo = &arvo; Tämä esittelee osoittimen parvo const-tyyppiseksi, joten se ei voi osoittaa muuhun kuin muuttujaan arvo. Kaikki yritykset muuttaa osoitin osoittamaan johonkin toiseen int-tyyppiseen muuttujaan saa aikaan kääntäjän virheilmoituksen. Muuttujan arvo sisältö ei ole vakio, joten voit muuttaa sisältöä aina halutessasi. Jos olisit esitellyt muuttujan arvo const-tyyppiseksi, et olisi voinut sijoittaa sen osoitetta osoittimeen parvo. Osoitin parvo voi osoittaa vain muuhun kuin vakiotyyppiseen int-muuttujaan. Osoitinvakio vakioon. Tässä sekä osoittimeen sijoitettu osoite, että osoitettu asia ovat vakioita, joten niitä ei voida kumpaakaan muuttaa. Tarkastellaan numeerista esimerkkiä. Esittelemme muuttujan arvo seuraavasti: const int arvo = 20; arvo on nyt vakio: et voi muuttaa sen arvoa. Voimme silti alustaa osoittimen sen osoitteella: const int* const parvo = &arvo; parvo on osoitinvakio vakioon. Et voi muuttaa sitä, mihin parvo osoittaa, etkä voi muuttaa osoitetun muistipaikan sisältöä. Luonnollisesti tämä toiminnallisuus ei ole vain char- ja int-tyyppien ominaisuus. Tämä pätee kaiken tyyppisille osoittimille. 250 Osoittimet ja taulukot Osoittimien ja taulukon nimien välillä on läheinen yhteys. Itse asiassa monissa tilanteissa voit käyttää taulukon nimeä aivan kuin se olisi osoitin. Jos palaamme takaisin lukuun 6 ja sovellamme nyt tullutta uutta asiaa, huomaamme, että taulukon nimi voi toimia osoittimen tapaan, kun sitä käytetään tulostuslauseessa. Jos yrität tulostaa taulukon sen pelkän nimen avulla (ja jos kyseessä ei ole char-tyyppinen taulukko), tulostuukin taulukon muistipaikan heksadesimaalinen osoite. Koska taulukon nimi toimii näin osoittimen tapaan, voit käyttää sitä osoittimen alustuksessa. Esimerkiksi: double arvot[10]; double* parvo = arvot; Tämä sijoittaa taulukon arvot osoitteen osoittimeen parvo. Haluan kuitenkin nyt muistuttaa, että taulukon nimi ja osoitin ovat varsin erilaisia olioita ja sinun tulee muistaa se. Kaikkein tärkein ero osoittimen ja taulukon nimen välillä on se, että voit muuttaa osoittimen osoitetta, mutta osoite, johon taulukon nimi viittaa, on kiinteä.

Osoitinlaskenta Osoittimet Voit suorittaa operaatioita osoittimella, jos haluat muuttaa sen sisältämää osoitetta. Käytössäsi on kuitenkin vain lisäys- ja vähennysoperaattorit, mutta voit myös vertailla osoittimia, jolloin lopputuloksena on totuusarvo. Lisäyksen kohdalla voit lisätä kokonaislukuarvon (tai lausekkeen, joka voidaan muuntaa kokonaisluvuksi) osoittimeen. Lisäyksen tuloksena on osoite. Voit myös vähentää kokonaisluvun osoittimesta ja tuloksena on jälleen osoite. Voit myös laskea kahden osoittimen erotuksen ja tällöin tuloksena on kokonaisluku, ei osoite. Mitkään muut operaattorit eivät ole sallittuja osoittimille. Osoitinlaskenta toimii erikoisella tavalla. Oletetaan, että lisäät arvon 1 osoittimeen: parvo++; Tämä kasvattaa osoitinta yhdellä. Se miten tarkalleen ottaen kasvatat osoittimen arvoa yhdellä, ei ole väliä - voit käyttää sijoitusoperaattoria tai +=-operaattoria, joten seuraavan lauseen tulos on sama kuin äskeisen lauseen: parvo += 1; Mielenkiintoinen asia on kuitenkin se, että osoittimeen sijoitettua osoitetta ei kasvateta yhdellä tavallisen laskennan tapaan. Osoitinlaskenta olettaa automaattisesti, että osoitin osoittaa taulukkoon ja että operaatio suoritetaan osoittimen sisältämälle osoitteelle. Kääntäjä tietää taulukon kunkin alkion koon ja luvun 1 lisääminen osoittimeen kasvattaa osoittimessa olevaa osoitetta alkion koon mukaan. Toisin sanoen luvun 1 lisääminen osoittimeen siirtää sen osoittamaan taulukon seuraavaan alkioon. Esimerkiksi, jos parvo on osoitin double-tyyppiseen tietoon, kuten viimeisimmässä esittelyssämme ja kääntäjä varaa 8 tavua double-tyyppiselle muuttujalle, osoittimen parvo sisältämää osoitetta kasvatetaan kahdeksalla. Tätä havainnollistetaan alla olevassa kaaviossa. double arvot[10]; Taulukko arvot - Heksadesimaaliset muistiosoitteet 1000 1008 1010 1018 1020 arvot[0] arvot[1] arvot[2] parvo parvo+1 parvo+2 (1000) (1008) (1010) arvot[3] arvot[4] ja niin edelleen Kunkin alkion pituudeksi oletetaan 8 tavua double *parvo = arvot; 1000 parvo 251

C++ Ohjelmoijan käsikirja Kuten kaaviosta näkyy, osoittimen parvo arvo alkaa taulukon ensimmäisen alkion osoitteesta. Luvun 1 lisääminen osoittimeen parvo kasvattaa sen sisältämää osoitetta kahdeksalla, joten tuloksena on taulukon seuraava alkio. Tästä seuraa se, että luvun 2 lisääminen osoittimeen siirtää osoitinta kaksi alkiota eteenpäin. Osoittimen parvo ei tietystikään tarvitse osoittaa taulukon alkuun. Voimme sijoittaa taulukon kolmannen alkion osoitteen osoittimeen lauseella: parvo = &arvot[2]; Tästä seuraa se, että lausekkeen parvo + 1 arvo on arvot[3]:n osoite, taulukon arvot neljäs alkio. Saamme osoittimen osoittamaan tähän alkioon suoraan kirjoittamalla: parvo += 1; Tämä lause kasvattaa osoittimessa parvo olevaa osoitetta taulukon arvot yhden alkion viemän muistimäärän verran. Yleisesti ottaen, lausekkeen parvo + n, jossa n voi olla mikä tahansa lauseke, jonka tulos on kokonaisluku, tulos on sama kuin osoittimen parvo sisältämään osoitteeseen lisätään n * sizeof(double), koska parvo on esitelty osoittimeksi double-tyyppiseen tietoon. Sama logiikka soveltuu myös kokonaisluvun vähentämiseen osoittimesta. Jos parvo sisältää osoitteen arvot[2], lausekkeen parvo - 2 tulos on taulukon ensimmäisen alkion osoite, arvot[0]. Toisin sanoen, osoittimen arvon lisäys ja vähennys tapahtuvat osoitettavan olion mukaan. longtyyppisen osoittimen arvon kasvattaminen yhdellä muuttaa sen sisällön seuraavaan longosoitteeseen, eli osoitetta kasvatetaan sizeof(long) tavua. Osoittimen arvon vähentäminen yhdellä vähentää osoitetta sizeof(long) tavua. Osoitinlaskennan tuloksena oleva osoite voi olla väliltä taulukon ensimmäisen alkion osoite ja taulukon viimeinen osoite + 1. Näiden rajojen ulkopuolella osoittimen toiminta on määrittelemätöntä. Luonnollisestikin voit käyttää osoitinlaskennalla muuttamaasi osoitinta epäsuorassa viittauksessa (muutenhan koko touhussa ei olisi mitään mieltä!). Oletetaan esimerkiksi, että parvo osoittaa yhä alkioon arvot[2], lause *(parvo + 1) = *(parvo + 2); on sama kuin arvot[3] = arvot[4]; Jos haluat käyttää osoitinta epäsuorassa viittauksessa osoitteen muuttamisen jälkeen, sulkeet ovat pakolliset, koska viittausoperaattori on suoritusjärjestyksessä ennen + ja - -operaattoreita. Jos kirjoitat lausekkeen muodossa *parvo + 1, etkä *(parvo + 1), lisätään yksi osoittimen parvo sisältämän muistipaikan sisältöön, mikä on sama kuin arvot[2] + 1. Lisäksi koska tulos on numeerinen arvo, joka ei ole osoite, sen käyttö sijoitusoperaattorin vasemmalla puolella saisi aikaan kääntäjän virheilmoituksen. 252

Osoittimet Muista, että lauseke parvo + 1 ei muuta parvo:n sisältämää osoitetta. Se on pelkästään lauseke, jonka arvo on saman tyyppinen kuin parvo. Tässä tapauksessa sen arvo on osoittimen parvo osoittamaa alkiota seuraavan alkion osoite. Luonnollisestikin, jos osoitin sisältää kielletyn osoitteen (esimerkiksi siihen liittyvän taulukon rajojen ulkopuolella olevan osoitteen), ja talletat arvon käyttämällä tätä osoitinta, yrität kirjoittaa kyseiseen muistipaikkaan. Tämä yleensä johtaa tuhoon; ohjelmasi suoritus päättyy tavalla tai toisella. Voi olla, ettei ole niin itsestään selvää, että ongelman aiheutti osoittimen väärä käyttö. Kahden osoittimen välisen eron laskeminen Voit vähentää osoittimen toisesta osoittimesta, mutta sillä on merkitystä vain, jos ne ovat samaa tyyppiä ja osoittavat samaan taulukkoon. Oletetaan, että meillä on yksiulotteinen long-tyyppinen taulukko numerot, joka on esitelty seuraavasti: long numerot[] = 10L, 20, 30, 40, 50, 60, 70, 80; Voimme esitellä ja alustaa kaksi osoitinmuuttujaa: long *pnumero1 = &numerot[6]; long *pnumero2 = &numerot[1]; //Osoittaa taulukon seitsemänteen alkioon //Osoittaa taulukon ensimmäiseen alkioon Nyt voimme laskea näiden kahden osoittimen välisen eron: int ero = pnum1 - pnum2; //Tulos on 5 Muuttujan ero arvoksi tulee 5, koska osoitteiden välinen ero lasketaan alkioissa, ei osoitteissa. Osoitinmuodon käyttö taulukon nimen yhteydessä Voimme käyttää taulukon nimeä osoittimien tapaan käsitellessämme taulukon alkioita. Meillä on yksiulotteinen taulukko esitelty seuraavasti: long data[5]; Voimme nyt osoitinmuodossa viitata alkioon data[3] lausekkeella *(data + 3). Tätä muotoa voidaan soveltaa myös yleisesti, eli viitatessamme alkioihin data[0], data[1], data[2],..., voimme kirjoittaa *data, *(data + 1), *(data + 2) ja niin edelleen. Taulukon nimi sellaisenaan viittaa taulukon alkuosoitteeseen, joten lausekkeen data + 2 tulos on osoite ensimmäisestä alkiosta kaksi eteenpäin. Voit käyttää osoitinmuotoa taulukon nimen yhteydessä aivan samaan tapaan kuin käytät indeksimuotoakin - lausekkeissa tai sijoitusoperaattorin vasemmalla puolella. Voit sijoittaa taulukon data arvoiksi parilliset kokonaisluvut seuraavalla lauseella: for(int i = 0 ; i < 5 ; i++) *(data + i) = 2 * (i + 1); *(data + i) viittaa taulukon peräkkäisiin alkioihin. *(data + 0) vastaa alkiota data[0], *(data + 1) vastaa alkiota data[1] ja niin edelleen. Silmukka sijoittaa taulukon alkioihin arvot 2, 4, 6, 8 ja 10. 253

C++ Ohjelmoijan käsikirja Jos nyt haluaisit laskea taulukon alkiot yhteen, voit kirjoittaa: long summa = 0; for(int i = 0 ; i < 5 ; i++) summa += *(data + i); Kokeillaan joitakin edellä mainittuja tekniikoita esimerkin avulla. Kokeile itse - Taulukon nimet osoittimina Koska olemme jo kokeilleet osoittimia merkkijonojen yhteydessä, kokeillaan nyt taulukon käsittelyä numeerisessa ohjelmassa, joka laskee alkulukuja (alkuluku on luku, joka on jaollinen vain itsellään ja luvulla 1). // Esimerkki 7.6 - Alkulukujen laskenta #include <iostream> #include <iomanip> using namespace std; int main() const int max = 100; long alkuluvut[max] = 2, 3, 5; int lkm = 3; long koe = 5; bool onalkuluku = true; do koe += 2; int i = 0; // Alkulukujen haluttu määrä // Ensimmäiset kolme alkulukua // Laskettujen alkulukujen lukumäärä // Alkulukukandidaatti // Alkuluku on löydetty // Seuraava tarkistettava arvo // Alkulukutaulukon indeksi // Yritetään jakaa kandidaatti kaikilla löydetyillä alkuluvuilla do onalkuluku = koe % *(alkuluvut + i) > 0; // False jos jaollinen while(++i < lkm && onalkuluku); if(onalkuluku) *(alkuluvut + lkm++) = koe; while(lkm < max); // Alkuluku löytyi... //...talletetaan se alkulukuihin // Tulostetaan viisi alkulukua riville for(int i = 0 ; i < max ; i++) if(i % 5 == 0) cout << endl; cout << setw(10) << *(alkuluvut + i); cout << endl; return 0; // Rivinvaihto 1. riville ja joka 5. jälkeen 254

Osoittimet Ohjelman tulostus on: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479 487 491 499 503 509 521 523 541 Kuinka se toimii Käytämme normaaleja #include-esikääntäjäkomentoja sisällyttämään otsikkotiedoston iostream syöttöä ja tulostusta varten sekä otsikkotiedoston iomanip tulostuksen muokkausfunktiota varten. Käytämme vakiota max määrittelemään haluamiemme alkulukujen lukumäärän: const int max = 100; // Alkulukujen haluttu määrä long alkuluvut[max] = 2, 3, 5; // Ensimmäiset kolme alkulukua int lkm = 3; // Laskettujen alkulukujen lukumäärä Alkuluvut tallettavassa taulukossa alkuluvut on kolme ensimmäistä alkulukua jo alustettu. Näin saamme laskennan mukavasti käyntiin. Muuttuja lkm pitää kirjaa löydettyjen alkulukujen lukumäärästä, joten sen arvoksi on alustettu 3. Alkulukujen testauksessa käytetään seuraavissa lauseissa esiteltyjä muuttujia: long koe = 5; bool onalkuluku = true; // Alkulukukandidaatti // Alkuluku on löydetty Muuttuja koe sisältää seuraavaksi testattavan alkulukukandidaatin, joten sen alkuarvoksi asetetaan arvo 5. Boolean-tyyppinen muuttuja onalkuluku on lippu, jota käytämme ilmaisemaan, että muuttujan koe arvo on alkuluku. Kaikki työ tehdään kahdessa silmukassa: ulompi do-while -silmukka valitsee seuraavaksi tarkistettavan kandidaatin ja lisää sen taulukkoon alkuluvut, jos se oli alkuluku. Sisempi silmukka tarkistaa onko kandidaatti alkuluku vai ei. Ulompaa silmukkaa suoritetaan niin kauan, että taulukko alkuluvut on täynnä. Ennen kuin sisempi silmukka suoritetaan, muuttujaan koe sijoitetaan seuraavaksi tarkistettava kandidaatti lauseella: koe += 2; // Seuraava tarkistettava arvo 255