Standardi mallikirjasto



Samankaltaiset tiedostot
Osoitin ja viittaus C++:ssa

815338A Ohjelmointikielten periaatteet Harjoitus 5 Vastaukset

Demo 6 vastauksia. 1. tehtävä. #ifndef #define D6T1 H D6T1 H. #include <iostream> using std::ostream; using std::cout; using std::endl;

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

12 Mallit (Templates)

Ohjelmointi 1 Taulukot ja merkkijonot

Olio-ohjelmointi Syntaksikokoelma

C++11 lambdat: [](){} Matti Rintala

815338A Ohjelmointikielten periaatteet Harjoitus 3 vastaukset

Operaattoreiden uudelleenmäärittely

Osoittimet. Mikä on osoitin?

STL:n uudistukset. Seppo Koivisto TTY Ohjelmistotekniikka

Mallit standardi mallikirjasto parametroitu tyyppi

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

Kääreluokat (oppikirjan luku 9.4) (Wrapper-classes)

ITKP102 Ohjelmointi 1 (6 op)

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

Ohjelman virheet ja poikkeusten käsittely

13 Operaattoreiden ylimäärittelyjä

Virtuaalifunktiot ja polymorfismi

C++11 Syntaksi. Jari-Pekka Voutilainen Jari-Pekka Voutilainen: C++11 Syntaksi

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

Tietueet. Tietueiden määrittely

Ohjelmoinnin perusteet Y Python

Kääntäjän virheilmoituksia

Taulukot. Jukka Harju, Jukka Juslin

Ohjelmoinnin perusteet Y Python

Java-kielen perusteet

Luokan operaatiot. Osoittimet ja viittaukset luokan olioihin

812347A Olio-ohjelmointi, 2015 syksy 2. vsk. X Poikkeusten käsittelystä

ITKP102 Ohjelmointi 1 (6 op)

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

Lyhyt kertaus osoittimista

Chapel. TIE Ryhmä 91. Joonas Eloranta Lari Valtonen

7. Oliot ja viitteet 7.1

Periytyminen. Luokat ja olio-ohjelmointi

Olio-ohjelmointi 2. välikoe HYV5SN

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

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

ITKP102 Ohjelmointi 1 (6 op)

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

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

TIETORAKENTEET JA ALGORITMIT

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

Ohjelmoinnin peruskurssi Y1

Sisällys. 7. Oliot ja viitteet. Olion luominen. Olio Java-kielessä

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

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

3. Muuttujat ja operaatiot 3.1

Muuttujien roolit Kiintoarvo cin >> r;

INSIDE C++ Ohjelmoijan käsikirja. Ivor Horton WROX PRESS

ITKP102 Ohjelmointi 1 (6 op)

812347A Olio-ohjelmointi, 2015 syksy 2. vsk. V Geneerisyys

Ohjelmoinnin perusteet Y Python

A TIETORAKENTEET JA ALGORITMIT

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

Ohjelmoinnin perusteet Y Python

Ohjelmoinnin perusteet Y Python

Javan perusteita. Janne Käki

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

Java-kielen perusteet

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

Informaatioteknologian laitos Olio-ohjelmoinnin perusteet / Salo

Listarakenne (ArrayList-luokka)

815338A Ohjelmointikielten periaatteet Harjoitus 4 vastaukset

Ohjelmoinnin perusteet Y Python

Metodit. Metodien määrittely. Metodin parametrit ja paluuarvo. Metodien suorittaminen eli kutsuminen. Metodien kuormittaminen

Ohjelmoinnin jatkokurssi, kurssikoe

1. (a) Seuraava algoritmi tutkii, onko jokin luku taulukossa monta kertaa:

Sisällys. 18. Abstraktit tietotyypit. Johdanto. Johdanto

Omat tietotyypit. Mikä on olio?

Rakenteiset tietotyypit Moniulotteiset taulukot

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

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

7. Näytölle tulostaminen 7.1

18. Abstraktit tietotyypit 18.1

Pong-peli, vaihe Aliohjelman tekeminen. Muilla kielillä: English Suomi. Tämä on Pong-pelin tutoriaalin osa 3/7. Tämän vaiheen aikana

Olio-ohjelmointi Geneerisyys. 1. Johdanto

Algoritmit 2. Luento 7 Ti Timo Männikkö

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op Taulukot & Periytyminen

etunimi, sukunimi ja opiskelijanumero ja näillä

Osa. Listaus 2.1. HELLO.CPP esittelee C++ -ohjelman osat. 14: #include <iostream.h> 15: 16: int main() 17: {

Harjoitus Olkoon olemassa luokat Lintu ja Pelikaani seuraavasti:

Ohjelmoinnin perusteet Y Python

Olio-ohjelmointi Javalla

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

Tietorakenteet ja algoritmit

Harjoitustyö: virtuaalikone

1 C++:n standardikirjasto

Tietorakenteet ja algoritmit

T Olio-ohjelmointi Osa 3: Luokka, muodostin ja hajotin, this-osoitin Jukka Jauhiainen OAMK Tekniikan yksikkö 2010

Luokat. Luokat ja olio-ohjelmointi

Tehtävä 1. TL5302 Olio-ohjelmointi Koe Malliratkaisuja. Tässä sekä a)- että b)-kohdan toimiva ratkaisu:

Perusteet. Pasi Sarolahti Aalto University School of Electrical Engineering. C-ohjelmointi Kevät Pasi Sarolahti

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

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

JAVA-PERUSTEET. JAVA-OHJELMOINTI 3op A JAVAN PERUSTEET LYHYT KERTAUS JAVAN OMINAISUUKSISTA JAVAN OMINAISUUKSIA. Java vs. C++?

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

ITKP102 Ohjelmointi 1 (6 op), arvosteluraportti


Transkriptio:

Standardi mallikirjasto 20 Standardi mallikirjasto Kuten jo tiedätkin C++ sisältää laajan standardikirjaston, joka yksinkertaistaa monia ohjelmointitehtäviä. Tähän saakka näkemäsi lisäksi tämä kirjasto sisältää joukon malleja, jotka tarjoavat vielä suuremmat mahdollisuudet. Voit käyttää näitä malleja luodessasi standardimuotoisia säiliöitä, algoritmeja ja funktioita omille tietotyypeille, jos ne tarvitsevat tällaisia ominaisuuksia ohjelmissasi. Nämä mallit muodostavat yhdessä standardin mallikirjaston (STL). Tässä luvussa esittelemme STL:n perussäiliöt ja algoritmit, jotka toimivat näiden säiliöiden yhteydessä. Luvun aiheita ovat: Standardin mallikirjaston perusrakenne Miten luodaan ja käytetään peräkkäissäiliöitä vector<> ja list<> sekä assosiatiivisia säiliöitä map<> ja multimap<> vector<>- ja list<>-säiliöiden erikoiset talletustavat Miten iteraattorit yhdistävät säiliöt yksinkertaisiin C++:n standarditaulukoihin Miten STL käyttää erikoistumista suorituskyvyn parantamiseksi STL:n arkkitehtuurin perusteet Yksi STL:n tärkeä ominaisuus on se, että se antaa käyttöösi yleisiä työkaluja. Koska STL:n kaikki ominaisuudet ovat mallipohjaisia, ne toimivat lähes kaikkien mahdollisten tietotyyppien kanssa. STL:n avulla voit luoda luokan, joka määrittelee linkitetyn listan mille tahansa tietotyypillesi. Jos tarvitset linkitetyn listan Laatikko-olioille ja JalkapallonPelaaja-olioille, se ei ole mikään ongelma STL:lle. Kun sinun tarvitsee käsitellä olioitasi tavalliseen tapaan, STL voi auttaa tässäkin. Jos sinun tarvitsee lajitella Laatikko-oliosi nousevaan järjestykseen, STL voi muodostaa lajittelufunktion mille tahansa tietotyypille, jolle on määritelty vertailuoperaattori järjestyksen vertailua varten. Käyttääpä ohjelmasi millaisia olioita tahansa, on mahdollista, että STL voi auttaa niiden standardimuotoisessa järjestelyssä tai analysoinnissa. 823

C++ Ohjelmoijan käsikirja STL sisältää kolmenlaisia työkaluja - säiliöitä, iteraattoreita ja algoritmeja. Säiliöiden avulla voit järjestää mielivaltaisen tyyppisiä olioita - linkitetty lista on yksi esimerkki säiliöistä. STL:n säiliöt jakaantuvat kahteen luokkaan: Peräkkäissäiliöt (vector, deque ja list), jotka sisältävät tietyn tyyppisiä olioita peräkkäisessä muodossa. Assosiatiiviset säiliöt (map, multimap, set ja multiset) mahdollistavat tietyn tyyppisten olioiden tallettamisen ja hakemisen erityyppisten avainten avulla. Yksinkertainen esimerkki voisi olla Tyontekija-olio, jonka avaimena on henkilönumero. Tyontekija-olio voidaan hakea henkilönumeron perusteella. Iteraattorit ovat liima, joka pitää STL:n kasassa. Riippumatta käytettävästä säiliöstä, sen alkioita käsitellään lähes yksinomaan iteraattoreiden avulla ja iteraattoreita käytetään algoritmien soveltamisessa säiliön olioille. Iteraattori on fiksu osoitin. Luvussa 14 määrittelimme yksinkertaisen iteraattoriluokan - LtkPtr-luokan - AutoKuorma-säiliöllemme. Lisäksi STL sisältää iteraattoreita, joita voidaan käyttää olioiden siirtämiseen virtaan tai virrasta. Algoritmit tarjoavat laskenta- tai analysointimekanismeja iteraattoreiden avulla käytettäville oliojoukoille. Algoritmien esimerkkinä voidaan mainita säiliön olioiden haku ja lajittelu. Kaikille STL:n algoritmeille välitetään parametrinä iteraattori. Emme mitenkään voi käsitellä koko STL:ää tässä luvussa. Koko STL:n käsittely vaatisi oman kirjansa, joten käsittelemme tässä vain pienen osan mahdollisuuksistamme. Käsittelemme joitakin perustyökaluja ja katsomme, miten voimme niitä käyttää yksinkertaisten ongelmien ratkaisuun. Näin pääset alkuun ja saat itseluottamusta kokeilla itse joitakin muita STL:n ominaisuuksia. STL:n kaikkein tärkeimmät työkalut ovat säiliöt - tulet varmastikin käyttämään niitä kaikkein useimmin. Et voi kuitenkaan käyttää säiliöitä ilman iteraattoreita, joten sinun tulee ymmärtää myöskin ne hyvin. Algoritmit ovat STL:n kaikkein suurin työkalukokoelma. Suuri osa niistä ei ole kuitenkaan käyttökelpoisia suurelle osalle ohjelmia ja joidenkin käyttö on varsin erikoistunutta. Emme käsittele STL:n algoritmeja yksityiskohtaisesti tässä luvussa, mutta käytämme joitakin niistä. Seuraava taulukko antaa yleiskuvan tarjolla olevista vaihtoehdoista. Algoritmiperhe Perustuu Esimerkkejä 824 kuuluminen operator= copy, remove, replace sijoittaminen operator=swap random_shuffle, rotate, reverse, permute järjestys operator< sort, partition, merge, find, min, max etsintä ja valinta operator== unique, match, count, search, copy_if, remove_if, replace_if muuntaminen funktio-olio for_each, transform, generate, fill numeeriset funktio-olio inner_product, partial_sum, accumulate, adjacent_difference

Standardi mallikirjasto Nämä ovat vapaamuotoisia perheitä ja taulukossa on lueteltu vain pieni osa kaikista käytettävissä olevista algoritmeista. Algoritmit toimivat yleisesti ottaen säiliöihin talletettujen olioiden kanssa, mutta niiden ei tarvitse tietää mitään varsinaisesta käytetystä säiliöstä, koska olioita käsitellään aina iteraattoreiden avulla. Kaksi ensimmäistä ryhmää - kuuluminen ja sijoittaminen - hoitavat alkioiden siirtelyt. Tärkeimmät tässä käytettävät operaattorit ovat sijoitusoperaattori ja swap-operaattori (vaihtooperaattori). Järjestys-ryhmän algoritmit ovat kaikkein tärkeimmät. Lajittelu on itsessään äärettömän suuri tieteenala ja oikean tekniikan käyttö on suorituskyvyn kannalta kriittinen. STL:n tarjoamat lajittelualgoritmit ovat parhaimpien tunnettujen joukossa. Monet STL:n algoritmit ovat suhteellisen helppoja tehdä itsekin uudelleen, mutta lajittelualgoritmit ovat suuri poikkeus. Jos voit opetella vain yhden algoritmiperheen, opettele järjestys-algoritmit. Etsintä ja valinta -algoritmit ovat yksinkertaisia, mutta erittäin käteviä. Näitä algoritmeja käytetään tilanteissa, joissa alkiot ovat samat. Muuntaminen-ryhmän algoritmeja näkyy ohjelmissa harvemmin. Ellei tilanne ole hyvin yksinkertainen, on usein helpompaa kirjoittaa itse for-silmukka. STL:n toteutus näille algoritmeille sallii niiden optimoinnin tavalla, joka ei ole mahdollista omalle koodillesi. Harkitse näiden algoritmien käyttöä, kun hyvä suorituskyky on äärimmäisen tärkeää. Funktio-oliot, joihin muuntaminen- ja numeeriset -ryhmät perustuvat, ovat luokkaolioita, jotka uudelleenmäärittelevät funktion kutsu -operaattorin operator(), ja ovat erityisesti suunnitellut funktion välittämiseen parametrinä tehokkaammin kuin pelkän funktion osoittimen välittäminen tarjoaa. Edellä oleva taulukko jakaa algoritmit tärkeimmän toiminnon mukaan, mutta algoritmit voidaan jakaa ryhmiin myös muilla tavoilla. Usein algoritmit jaetaan muuttaviin ja ei-muuttaviin algoritmeihin. Näistä ensimmäiset ovat algoritmeja, jotka muuttavat säiliön sisältöä ja jälkimmäiset ovat algoritmeja, jotka eivät muuta säiliön sisältöä. STL:n otsikkotiedostot STL:n ominaisuudet esitellään seuraavissa otsikkotiedostoissa: Otsikkotiedosto <vector> <deque> <list> <map> Tarkoitus taulukkosäiliö, jossa on yksi pää taulukkosäiliö, jossa on kaksi päätä molempiin suuntiin linkitetty listasäiliö assosiatiivinen taulukkosäiliö Taulukko jatkuu seuraavalle sivulle 825

C++ Ohjelmoijan käsikirja Otsikkotiedosto <set> <queue> <stack> <iterator> <algorithm> <numeric> <functional> <bitset> <valarray> <complex> <utility> Tarkoitus järjestetty joukkosäiliö kaksipäinen jono (säiliömuunnin) pino (säiliömuunnin) iteraattorit ja säiliötuki yleiskäyttöiset algoritmit numeeriset algoritmit algoritmien funktio-olio -tuki bittisarjoja kuvaavat oliot numeeristen arvojen taulukot kompleksilukujen tuki funktio-olioiden tuki Jotkin STL:n osat on esitelty myös virtojen otsikkotiedostoissa - <iostream>, <istream>, <ostream>, <sstream>. Nämä tukevat STL:n algoritmien pohjalta syöttöä ja tulostusta. vector-säiliön käyttö Paras paikka aloittaa STL:n käsittely on vector-säiliö, joka on lähinnä C++:n tavallista taulukkoa. Itse asiassa vector-säiliötä voidaan käyttää lähes kaikissa paikoissa, joissa voidaan käyttää C++:n taulukkoakin. vector-säiliön käyttö on paljon helpompaa kuin C++:n taulukon - taulukon kohdalla sinun tulee itse huolehtia taulukon koosta, mutta vector-säiliön kohdalla kaikki tällainen työ hoidetaan automaattisesti. Kun olet tottunut käyttämään vector-säiliötä, saatat päättääkin, että et enää koskaan käytä tavallista taulukkoa. Yksi tehtävä kuitenkin vaatii yhä C++:n taulukon käyttöä: staattisten alkuarvojen esittely: string nimet[] = Alf, Bjarne, Zaphod ; Suurissa ohjelmissa tällaisten määrittelyjen määrä kannattaa pitää mahdollisimman pienenä. On usein parempi hakea kiinteät alkuarvot ohjelman resurssitiedoista suoritusaikana. 826 Kokeile itse - vector<> vs. C++:n taulukko Katsotaan pikaisesti int-taulukkoa ja int-tyyppistä vector-säiliötä. Huomaamme, että ne toimivat suurelta osin samaan tapaan. // Esimerkki 20.1 Taulukon ja vector-säiliön pikavertailu #include <iostream> #include <vector> using namespace std;

Standardi mallikirjasto int main() int a[10]; vector<int> v(10); // C++:n taulukon esittely // vastaava STL:n vector-säiliön esittely cout << "10-alkioisen taulukon koko: " << sizeof a << endl; cout << "10-alkioisen vector-säiliön koko: " << sizeof v << endl; for (int i = 0; i < 10; ++i) a[i] = v[i] = i; int a_summa = 0, v_summa = 0; for (int i = 0; i < 10; ++i) a_summa += a[i]; v_summa += v[i]; cout << "taulukon 10 alkion summa on: " << a_summa << endl; cout << "vector-säiliön 10 alkion summa on: " << v_summa << endl; return 0; Kun suoritat tämän ohjelman, se tulostaa: 10-alkioisen taulukon koko: 40 10-alkioisen vector-säiliön koko: 16 taulukon 10 alkion summa on: 45 vector-säiliön 10 alkion summa on: 45 Kuinka se toimii Kaikki STL:n oliot ovat osa standardikirjastoa, joten ne on määritelty nimiavaruudessa std. Tarvitsemiasi STL:n ominaisuuksia vastaavat otsikkotiedostot tulee tietysti sisällyttää lähdetekstiin ennen ominaisuuksien käyttöä. Jos haluat käyttää nimiä ilman std:n kirjoittamista aina nimen yhteyteen, voit ottaa std-nimiavaruuden käyttöön using-komennolla: #include <vector> using namespace std; Ainut ero taulukon ja vector-säiliön käytössä on niiden esittelyissä: int a[10]; vector<int> v(10); // C++:n taulukon esittely // vastaava STL:n vector-säiliön esittely C++:n taulukko on sisäänrakennettu tyyppi, joka esitellään []-syntaksilla. Toisaalta vectorsäiliö on mallina toteutettu standardikirjaston tyyppi. Malliparametri määrittelee vector-säiliön sisältämien olioiden tyypin. Tässä olemme esitelleet vector-säiliön, joka tallettaa int-tyyppisiä olioita. 827

C++ Ohjelmoijan käsikirja Tässä esimerkissä taulukko a ja vector-säiliö v ovat molemmat automaattisia muuttujia. Taulukon kohdalla tämä tarkoittaa sitä, että 10 kokonaisluvun tila varataan automaattisesta muistista. Minun tietokoneessani tyyppi int vie 4 tavua, joten taulukko a vie tilaa 40 tavua, kuten tulostuksesta huomaat. vector-säiliön olio v on myöskin automaattisessa näkyvyysalueessa, mutta (toisin kuin taulukko) se ei talleta sisältöään automaattiseen muistiin. Se varaa sisäisesti muistia alkioita varten vapaasta muistista. Aluksi se varaa talletettujen alkioiden verran muistia - mutta voit lisätä siihen niin monta alkiota kuin haluat. vector-säiliö huolehtii sisäisesti kaikista muistinvarauksen ongelmista (tietysti suhteessa vapaaseen muistiisi!). vector-olion ilmoitettu koko ei sisällä alkioita varten tarvittua muistia. Tästä syystä tällaista oliota kutsutaan usein kahvaksi. Se sisältää ainoastaan tarvittavan määrän muistia, jotta vectorsäiliö voi käsitellä alkioiden muistia. Kuten tulostuksesta huomaat, vector-olion sizeofoperaattorin palauttama koko on vain 16 tavua. v:n todellinen sisältö - muisti, joka tarvitaan sen tallettamien olioiden arvojen tallettamiseen - on varattu dynaamisesti, ja vie lisäksi 40 tavua vapaasta muistista. Taulukko vie siis selvästikin vähemmän muistia. vector-olion viemä lisätila on kuitenkin varsin pieni. Taulukon ongelma on siinä, että muisti on varattu ohjelmapinosta ja sen koko ja alkioiden lukumäärä on määrätty kiinteäksi jo käännösaikana. vector-olion viemän lisätilan vastapainona on kaksi korvaavaa hyötyä: ensiksikin, muistinhallinta on automaattinen ja toiseksi, voit lisätä alkioita vector-säiliöön aina tarvittaessa. 828 vector-säiliön perusoperaatiot vector-säiliön kanssa työskentely on huomattavasti joustavampaa kuin tavallisen taulukon kohdalla. Voit esimerkiksi lisätä alkioita vector-olion loppuun tai keskelle ja voit poistaa alkioita. Tarvittava muisti hallitaan aina automaattisesti. Saat aina selville vector-oliossa olevien alkioiden lukumäärän (kutsumalla sen size()-jäsenfunktiota) ja sen empty()-jäsenfunktio palauttaa arvon true, jos vector-oliossa ei ole lainkaan alkioita. Perusoperaatiot alkion lisäämiseksi tai poistamiseksi ovat: insert() Lisää yhden tai useamman alkion push_back() Lisää parametrinään saamansa oliot vector-olion perään erase() Poistaa yhden tai useamman alkion clear() Poistaa kaikki alkiot Jotta ymmärrät näiden alkioiden toiminnan, kokeillaan niitä esimerkin avulla. Meidän täytyy jo tässä esimerkissä käyttää iteraattoreita, mutta olet nähnyt iteraattoreita toiminnassa jo luvussa 14, joten tämän ei pitäisi olla kovinkaan pelottavaa. vector-olion begin()-jäsen palauttaa iteraattorin, joka osoittaa ensimmäiseen alkioon ja end()-jäsen palauttaa iteraattorin, joka osoittaa osoittimen, joka osoittaa yhden alkion verran viimeisen alkion ohi. Voit käyttää näitä iteraattoreita käydessäsi vector-olion alkioita läpi - kasvattamalla begin()-jäsenen palauttamaa iteraattoria, kunnes se on sama kuin end()-jäsenen palauttama iteraattori. Voit tietysti käyttää myös []-operaattoria; voit vapaasti päättää kumpaa tapaa käytät.

Standardi mallikirjasto Kokeile itse - Lyhyt johdatus vector-säiliöön! Tämä ohjelma käy läpi juuri käsittelemämme yksinkertaiset vector-säiliön operaatiot. Kokeilemme alkioiden lisäämistä ja poistamista vector-oliosta ja erilaisia tapoja tutkia vectorolion sisältöä. Huomaa, että sinun käännösympäristösi STL:n toteutus ei ehkä toimi tämän esimerkin kanssa täsmälleen kirjoitetulla tavalla, koska tällä hetkellä STL:n toteutukset vaihtelevat suurestikin. ANSI/ISO-standardin pitäisi muuttaa tämä tilanne, koska yhä enemmän ja enemmän C++:n toteutuksia päivitetään sen mukaiseksi. Jos esimerkki ei toimi, sinun tulee tarkistaa kirjaston dokumenteista, mikä ohjelman osa tulee muuttaa. Tässä esimerkissä on kirjoitettu funktio nayta_jarjestys(), joka havainnollistaa C++:n osoittimien ja vector-säiliön iteraattoreiden yhtäläisyyttä. tutki_vector()-funktio, joka tulostaa vector-säiliön sisällön ohjelman eri kohdissa, kutsuu nayta_jarjetys()-funktiota. // Esimerkki 20.2 vector<>-säiliön käsittely #include <iostream> #include <vector> #include <algorithm> // STL:n vector-säiliö // copy()-funktio using namespace std; // Näyttää alkiot järjestyksessä void nayta_jarjestys(const int* ensim, const int* viim) cout << " "; copy(ensim, viim, ostream_iterator<int>(cout, " ")); cout << "" << endl; // Näyttää vector-olion sisällön void tutki_vector(const vector<int>& v) cout << " vector-oliolla on " << v.size() << " alkiota: "; nayta_jarjestys(v.begin(), v.end()); int main() vector<int> v; cout << "uusi vektori luotu" << endl; tutki_vector(v); // Luodaan tyhjä vektori cout << "täytetään vektori taulukosta" << endl; int arvot[] = 1, 3, 7, 5; v.insert(v.end(), arvot+1, arvot+3); // Lisätään kaksi alkiota tutki_vector(v); cout << "lisätään arvo 5" << endl; v.push_back(5); tutki_vector(v); // Lisätään alkio loppuun 829

C++ Ohjelmoijan käsikirja cout << "poistetaan alkio siirtymässä 1" << endl; v.erase(&v[1]); // Poistetaan toinen alkio tutki_vector(v); cout << "lisätään alkio 4 siirtymään 1" << endl; v.insert (v.begin()+1, 4); // Lisätään alkio tutki_vector(v); cout << "poistetaan kaikki alkiot" << endl; v.clear(); tutki_vector(v); // Poistetaan kaikki alkiot return 0; Tulostus näyttää vector-olion sisällön jokaisen vector-operaation jälkeen: uusi vektori luotu vector-oliolla on 0 alkiota: täytetään vektori taulukosta vector-oliolla on 2 alkiota: 3 7 lisätään arvo 5 vector-oliolla on 3 alkiota: 3 7 5 poistetaan alkio siirtymässä 1 vector-oliolla on 2 alkiota: 3 5 lisätään alkio 4 siirtymään 1 vector-oliolla on 3 alkiota: 3 4 5 poistetaan kaikki alkiot vector-oliolla on 0 alkiota: Kuten huomaat, vector-säiliö on hyvä pitämään huolta sisällöstään. Tavallisen taulukon kohdalla kaikkien näiden operaatioiden toteuttaminen olisi ollut huomattavasti hankalampaa. Kuinka se toimii Koodi alkaa kahdella #include-komennolla, jotka sisällyttävät kaksi STL:n otsikkotiedostoa: #include <vector> // STL:n vector-säiliö #include <algorithm> // copy()-funktio 830 Otsikkotiedosto vector määrittelee vector-säiliön ja kaikki sen sisäänrakennetut operaatiot. STL mahdollistaa, että voit tehdä vector-oliolla paljon muutakin, kuin mikä kuuluu suoraan vectormallin ilmentymään. Otsikkotiedosto algorithm sisältää sitten loput tarvittavat herkut. Seuraavaksi määrittelemme nayta_jarjestys()-funktion, joka tulostaa alkiot järjestyksessä: void nayta_jarjestys(const int* ensim, const int* viim) cout << " "; copy(ensim, viim, ostream_iterator<int>(cout, " ")); cout << "" << endl;

Standardi mallikirjasto Tämä funktio käyttää STL:n copy()-algoritmia, joka tulostaa cout-virtaan kahden osoittimen osoittaman välin. Näet ohjelman tulostuksesta, että tämä myöskin toimii. Palaamme ostream_iterator<>-iteraattoriin myöhemmin, kun käsittelemme tulostusiteraattoreita tarkemmin. Funktio copy() on ainut funktio tässä esimerkissä, joka ei kuulu itse vector-malliin. Sen kaksi ensimmäistä parametriä määrittelevät lähteen välin. Kopiointi alkaa ensim-alkiosta ja jatkuu viim-alkioon saakka. viim-alkiota ei kuitenkaan kopioida. copy-funktio hyväksyy minkä tahansa iteraattorin kahden ensimmäisen parametrinsä arvoksi - tässä olemme käyttäneet tavallisia osoittimia. Kolmas parametri on kohdesijainti, joka määritellään myöskin iteraattorin avulla. Tässä esimerkissä käytämme tulostusiteraattoria, joka luodaan lausekkeella ostream_iterator<int>(cout, " "). STL sallii tällaisen joustavan määrittelyn. copy()-algoritmi sijoittaa jokaisen lähdealkion kohdeolioomme tietämättä tarkasti, minkälaista kohdetta se käsittelee - se vain sijoittaa kopioitavat oliot kolmantena parametrinä olevan iteraattorin mukaan ja olettaa, että iteraattori tietää, minne olio tulee sijoittaa. Tässä tapauksessa kohdeolio tulostaa siihen kopioidut arvot standardiin tulostusvirtaan cout. Voit välittää monia erilaisia iteraattoreita STL:n algoritmeille ja ne toimivat sen mukaisesti. Iteraattori voi olla kuten osoitin, joka lukee ja kirjoittaa arvoja muistiin, tai se voi olla monimutkaisempi olio, joka lukee tai kirjoittaa arvot virtaan. copy()- funktio toimii yhtä hyvin kopioitaessa olio vector-säiliöstä virtaan tai kopioitaessa tietynlainen olio säiliöstä toiseen. Funktio tutki_vector() kutsuu nayta_jarjetys()-funktiotamme: void tutki_vector(const vector<int>& v) cout << " vector-oliolla on " << v.size() << " alkiota: "; nayta_jarjestys(v.begin(), v.end());! Tämä funktio havainnollistaa yhtä vector-säiliön varsin hienoa ominaisuutta. Huomaa, että välitämme vector-olion vain yhtenä parametrinä. Meidän ei tarvitse välittää toista parametriä, joka kertoisi vector-säiliön sisältämien alkioiden lukumäärän - eikä kuinka monta alkiota vectorsäiliöön voi sijoittaa. Tulostamme vector-olion alkioiden lukumäärän kutsumalla sen size()- jäsentä. vector-olio tietää aina, montako alkiota siihen on talletettu. Huomaa, että saamme vector-olion ensimmäisen ja viimeisen alkion iteraattorin selville begin()- ja end()-jäsenillä ja välitämme ne nayta_jarjestys()-funktiolle. Nämä iteraattorit muunnetaan automaattisesti nayta_jarjestys()-funktion parametrien tyyppisiksi, eli int*-tyyppisiksi. Vector-iteraattorit ovat lähes identtiset tavallisiin C++:n osoittimiin. Itse asiassa, monet STL:n toteutukset määrittelevät vector<t>::iterator-tyypin tavalliseksi C++:n T*-tyyppiseksi osoittimeksi. Juuri tämä tekee vector-säiliöstä niin hyvän tavallisten taulukoiden korvaajan. Lähes kaikki, mitä olet tehnyt osoittimien kanssa voit tehdä myös vector-säiliön kanssa. 831

C++ Ohjelmoijan käsikirja Kuten olemme jo kertoneet, v.begin() osoittaa ensimmäiseen alkioon. Pääset käsiksi ensimmäisen alkion sisältöön kirjoittamalla lausekkeen *v.begin(), v[0] tai v.front(). Mitä näistä käytät, riippuu käyttötilanteesta. Myöhemmin tässä luvussa näemme tilanteita, joissa jokin näistä on luonnollisempi vaihtoehto kuin muut. Huomaa, että et voi käyttää osoittimen end() osoittamaa arvoa - se osoittaa aina yhden osoitteen säiliön viimeisen alkion yli. Osoitinta end() kutsutaan yleensä lopun yli -osoittimeksi. Eli *v.begin() on sallittu, mutta varsin samanlainen lauseke *v.end() on huono idea. Sen sijaan *(v.end()-1) on sallittu ja hyödyllinen tapa päästä käsiksi viimeiseen alkioon. Viimeiseen alkioon pääset käsiksi myös lausekkeilla *(v.begin()+v.size()-1), v[v.size()-1] tai yksinkertaisesti v.back(). Ennen kuin yrität käsitellä alkiota, sinun tulee varmistaa, että viimeinen alkio on olemassa. Nopea tapa tarkistaa tämä on kutsua jäsenfunktiota v.empty(), joka palauttaa arvon true, kun v ei sisällä ainuttakaan alkiota. Ohjelmamme käyttää vector-mallin ilmentymää vector<int>, joten esittelemme int-tyyppisiä arvoja tallettavan vector-säiliön seuraavasti: vector<int> v; // Luodaan tyhjä vektori Tässä esittelemme v:n mallin vector<int>-ilmentymänä kutsumalla oletusmuodostinfunktiota. Näemme tulostuksesta, että uusi säiliömme on aluksi tyhjä. Käytössäsi on tietysti myös muunlaisia muodostinfunktioita. Jos esimerkiksi haluaisit säiliön 50 double-tyyppiselle arvolle, voisit esitellä vector-säiliön data seuraavasti: vector<double> data(50); Uuteen säiliöömme voimme tallettaa arvoja monella tavalla. Yksi kaikkein tehokkaimmista on insert()-jäsenfunktio, joka kopioi kokonaisen sarjan arvoja: cout << "täytetään vektori taulukosta" << endl; int arvot[] = 1, 3, 7, 5; v.insert(v.end(), arvot+1, arvot+3); // Lisätään kaksi alkiota tutki_vector(v); 832 Tästä huomaa jälleen, että STL mielellään määrittelee sarjat kahden osoittimen väliin jäävällä välillä. Funktion insert() ensimmäinen parametri, v.end(), on osoitin sijaintikohtaan, johon haluamme alkiot sijoittaa. Huomaa, että käytämme tässä lopun yli -osoittavaa osoitinta - sen käyttö on sallittu, kun yhdistät arvoja. Tästä onkin helppo muistaa, mitä lopun yli -osoitin tarkoittaa: se on sijaintikohta, johon seuraava lisättävä arvo sijoitetaan. Eli voit käyttää end()-osoitinta uusien alkioiden lisäämiseen - mutta älä yritä käsitellä sen osoittamaa arvoa. insert()-funktion toinen ja kolmas parametri, eli lausekkeet arvot-1 ja arvot+3, määrittelevät yhdessä lisättävien alkioiden välin. Tässä lähteemme on taulukko arvot ja kopioimme tässä toisen ja kolmannen alkion - eli alkaen kohdasta arvot+1 ja päättyen alkiota arvot+3 edeltävään alkioon.

Standardi mallikirjasto! Välit määritellään tavallisesti tällä tavalla STL:ssä - väliä, joka sisältää ensimmäisen alkion, mutta ei viimeistä, kutsutaan puoliavoimeksi väliksi. Tällainen merkitään [alku, loppu), jossa sulkeet osoittavat, että väli on suljettu vasemmasta päästä, mutta avoin oikeasta päästä. Eli väli sisältää kohdan alku, mutta ei kohtaa loppu. Kun määrittelet [alku, loppu) välin, kuten esimerkiksi arvoilla arvot+1 ja arvot+3, varmista, että loppu on suurempi kuin alku - ja että molemmat osoittimet on liitetty samaan lähdeolioon. Nämä ovat varsin yleisiä - ja vaikeasti havaittavia - virheitä. Palataan esimerkin 20.2 käsittelyyn. Lisäämme vector-säiliöön alkion push_back()-jäsenellä: v.push_back(5); // Lisätään alkio loppuun Tämä lisää arvon 5 uudeksi alkioksi v-olion perään. Miksi tätä funktiota ei kutsuta yksinkertaisesti nimellä append()? Jos push saa sinut ajattelemaan pinoja, olet oikeilla jäljillä. Itse asiassa STL sisältää pinomuuntimen (stack adaptor), jolla voit muuntaa vector-säiliön pinoksi. Emme käsittele STL:n muuntimia tässä yhteydessä, saat lisätietoa STL:n dokumenteistasi. Monet STL:n nimet viittaavat siihen, että säiliöt voidaan muuntaa toimimaan toisella tavalla, kuten stack ja queue. Seuraavaksi testaamme erase()- ja insert()-funktioita: v.erase(&v[1]); // Poistetaan toinen alkio erase()- ja insert()-operaatioihin kuuluu alkioiden siirtelyä, jotta lisättäville alkioille saadaan tilaa tai poistettavan alkion aukko poistetaan. Sisäisesti vector-säiliö kopioi alkioita, jotta ne pysyvät oikeassa järjestyksessä. Tämä voi olla hyvin hidas operaatio, kuten näemme, kun käsittelemme vector-säiliön muistinhallintaa. Koska vector-säiliö on malli, se voi sisältää myös luokkaolioita, ja tällöin erase()-funktion tulee kutsua jokaisen poistettavan olion tuhoajafunktiota. Tämä lause poistaa toisen alkion vector-säiliöstä v. Se käyttää erase()-funktion yksinkertaisinta muotoa, jolle välitetään vain yksi iteraattori, joka määrittelee yhden poistettavan alkion. Osoitintyyppinen parametri muunnetaan automaattisesti iteraattoriksi. Sen sijaan, että olisimme käyttäneet begin()- ja end()-jäseniä iteraattoreiden muodostuksessa, olemme käyttäneet taulukkomuotoa. Tällä havainnollistamme sitä, että v teeskentelee olevansa taulukko. Lauseke &v[1] viittaa v:n toisen alkion osoitteeseen. Tämän lauseen tuloksena toinen alkio, jonka arvo on 7, poistetaan vector-säiliöstä v. Seuraavaksi lisäämme uuden alkion lauseella: v.insert (v.begin()+1, 4); // Lisätään alkio 833

C++ Ohjelmoijan käsikirja! Olemme jo kerran käyttäneet insert()-funktiota. Tällöin sillä oli kaksi iteraattoriparametriä. Tässä muodossa sillä on kaksi parametriä, joista ensimmäinen on iteraattori ja toinen kokonaisluku. Määrittelemme sijaintikohdan lausekkeella v.begin()+1 ja lisäämme uuden alkion - arvon 4 - tähän sijaintikohtaan. Tässä sijaintikohdassa nyt oleva alkio ja kaikki sitä seuraavat alkiot tulee ensin siirtää eteenpäin, jotta uusi arvo mahtuu väliin. Nytkin vector-olio varaa uuden tilan ja kopioi alkiot automaattisesti. Tämä automaattinen kopiointi on varsin miellyttävää. Ohjelmasi suoritus kuitenkin hidastuu merkittävästi, jos vector-olio on suuri ja lisäät tai poistat alkioita olion alkupäästä. STL sisältää toisen säiliön, listan list<>, joka tekee nämä kaikki samat asiat, mutta mitään kopiointia ei tarvitse tehdä. Näemme listan toiminnassa varsin pian. Lopuksi poistamme kaikki alkiot lauseella: v.clear(); // Poistetaan kaikki alkiot Voimme käyttää clear()-jäsentä, jos haluamme poistaa kaikki alkiot eksplisiittisesti. Tämä ei ole kuitenkaan tarpeen. Kun vector-olion v näkyvyysalue päättyy (lohkon lopussa), v ja sen sisältö poistetaan automaattisesti v:n tuhoajafunktion toimesta. v.clear() on itse asiassa lyhyempi muoto lausekkeesta v.erase(v.begin(), v.end()). vector-säiliön käyttö taulukko-operaatioissa Katsotaan seuraavaksi esimerkkiä, johon törmäsimme ensimmäisen kerran luvussa 8, kun käsittelimme funktioita. Seuraavassa on keskiarvo()-funktio esimerkistä 8.5, joka on kiinnostaja siitä syystä, että sille välitetään parametrinä taulukko[]: // Funktio, joka laskee keskiarvon double keskiarvo(double taulukko[], int lkm) double summa = 0.0; // Tähän lasketaan summa for(int i = 0 ; i < lkm ; i++) summa += taulukko[i]; // Summataan taulukon alkiot return summa / lkm; // Palautetaan keskiarvo 834 Ennen kuin STL oli olemassa, C- ja C++-ohjelmat kirjoitettiin tällä tavalla. Vaikka olemmekin käsitelleet runsaasti uusia asioita luvun 8 jälkeen, muistat varmasti vieläkin, että double taulukko[] ja double* taulukko tarkoittavat tässä yhteydessä aivan samaa - eli keskiarvo()-funktion ensimmäinen parametri on osoitin. Koska taulukko ei tiedä, montako alkiota siinä on, meidän tulee aina välittää myös toinen parametri, joka kertoo taulukon alkioiden lukumäärän (joskus meidän tarvitsee välittää myöskin taulukon kapasiteetti). Katsotaan uudelleen esimerkkimme 20.2 nayta_jarjestys()-funktiotamme. Tälle funktiolle välitetään ensimmäisenä parametrinä myöskin osoitin, mutta käytetään vain erilaista muotoa. Toinen parametri on myöskin osoitin:

Standardi mallikirjasto void nayta_jarjestys(const int* ensim, const int* viim) cout << " "; copy(ensim, viim, ostream_iterator<int>(cout, " ")); cout << "" << endl; Koska osoittimet vastaavat iteraattoreita, voimme ajatella parametrien olevan tässä iteraattoreita. Väli ensim - viim on jälleen puoliavoin väli - ensim otetaan mukaan, mutta viim ei oteta. Tämä on kuitenkin täysin eri kuin kahden indeksin käyttäminen, koska välin olioiden tyyppi on iteraattoreiden yhteydessä implisiittinen. Vaikka vector-säiliön ja välin käyttäminen on eri taulukkoon ja muuttujaan lkm verrattuna, saamme kuitenkin helposti selville käsiteltävien alkioiden lukumäärän: int lkm = viim - ensim; Eli miksi STL käyttää välejä, eikä taulukoita ja lukumääriä? Tähän on useita syitä. Ensinnäkin, jos taulukoita käytetään runsaasti, osoittimien käyttö on miellyttävämpää. Toiseksi, se on myös huomattavasti tehokkaampaa. Kun näet lauseen, jossa on moniulotteinen taulukko, kääntäjä joutuu tekemään runsaasti töitä lauseen kohdalla. Tutkitaan esimerkiksi seuraavaa koodia: // kun kirjoitat seuraavaa double ilmanpaine[200][200][200]; double paine_keskella = ilmanpaine[99][100][101]; Kääntäjä löytää alkion ilmanpaine[99][100][101] suorittamalla seuraavat lauseet: // kääntäjä tekee todellisuudessa seuraavaa double* apu = ilmanpaine + 99 * sizeof (ilmanpaine[0]) + 100 * sizeof (ilmanpaine[0][0]) + 101; double paine_keskella = *apu; Kuten huomaat, kääntäjä tekee kaiken tämän laskennan vain muuntaakseen kaikki takaisin osoitinlausekkeeksi. Hyvä kääntäjä tekee lisäksi paljon yksinkertaistaakseen tätä aina, kun se vain on mahdollista - mutta silti kaikki nämä laskennat ja lisäykset vaativat aikaa. Yksinkertainen totuus on, että osoittimet ovat yksinkertaisempia kuin taulukot ja lisäksi, kun muistamme, että iteraattori on olio, joka toimii osoittimen tapaan, niin tässä on syy siihen, miksi STL:ssä käytetään yleensä välejä, jotka on määritelty iteraattoriparin avulla. Keskiarvon laskeminen iteraattoreiden avulla Voimme määritellä mallin keskiarvo()-funktiolle, joka käyttää iteraattoreita taulukkojen sijaan. Mallin koodi olisi tässä tapauksessa seuraava: template <typename Iter> double keskiarvo(iter a, Iter loppu) double summa = 0.0; // Tähän lasketaan summa 835

C++ Ohjelmoijan käsikirja int lkm = 0 ; for( ; a!= loppu ; lkm++) summa += *a++; // Summataan taulukon alkiot // Palautetaan keskiarvo return summa / lkm; Huomaa, kuinka vähän suoritamme operaatioita parametrimuuttujalle a: a!= loppu tarkistaa viimeisen arvon *a haetaan nykyinen arvo a++ siirrytään seuraavaan arvoon Kaksi viimeistä operaatiota on yhdistetty yhdeksi lausekkeeksi *a++. Huomaat varmastikin jo nyt, että tällainen lauseke on yksi kaikkein useimmin C++:ssa käytetty lauseke. Olemme koodanneet mallin siten, että operaation * ja jälkiliitteellisen ++ täytyy toimia tyypille Iter ja näin on sekä osoittimien että iteraattoreiden kohdalla. Iteraattoreiden käyttö yksinkertaisti koodia varsin paljon. Kokeillaan sitä esimerkin avulla. Kokeile itse - Keskiarvon laskeminen iteraattoreiden avulla Seuraavassa on ohjelma, joka laskee keskiarvot mallimme avulla: // Esimerkki 20.3 Keskiarvon laskeminen iteraattoreiden avulla #include <iostream> #include <vector> using namespace std; template <typename Iter> double keskiarvo(iter a, Iter loppu) double summa = 0.0; int lkm = 0 ; for( ; a!= loppu ; lkm++) summa += *a++; // Tähän lasketaan summa // Summataan taulukon alkiot return summa / lkm; // Palautetaan keskiarvo int main() double lampotila[] = 10.5, 20.0, 8.5 ; cout << "taulukon keskiarvo = " << keskiarvo(lampotila, lampotila + (sizeof lampotila/sizeof lampotila[0])) << endl; vector<int> aurinkoinen; aurinkoinen.push_back(7); aurinkoinen.push_back(12); aurinkoinen.push_back(15); cout << aurinkoinen.size() << " kuukautta" << endl; cout << "aurinkoisten päivien lukumäärän keskiarvo: "; cout << keskiarvo(aurinkoinen.begin(), aurinkoinen.end()) << endl; return 0; 836

Standardi mallikirjasto Tämän ohjelman tulostus on seuraava: taulukon keskiarvo = 13 3 kuukautta aurinkoisten päivien lukumäärän keskiarvo: 11.3333 Kuinka se toimii Mallien hyvänä puolena on se, että saat niin monta versiota kuin tarvitset, mutta joudut määrittelemään vain yhden. Tämä esimerkki muodostaa kaksi ilmentymää mallifunktiosta keskiarvo. Ensimmäinen keskiarvo-mallin ilmentymä on tavalliseen C++:n taulukkoon sijoitetuille arvoille: double lampotila[] = 10.5, 20.0, 8.5 ; cout << "taulukon keskiarvo = " << keskiarvo(lampotila, lampotila + (sizeof lampotila/sizeof lampotila[0])) << endl; Mallin ilmentymän tyyppi määritellään automaattisesti funktion parametreistä. Toisen parametrin tulee olla osoitin mukaan haluamaamme viimeistä alkiota seuraavaan alkioon - laskemme tämän lisäämällä taulukon alkioiden lukumäärän osoittimeen lampotila. Toinen keskiarvo-mallin ilmentymä on vector-säiliöön talletetuille int-tyyppisille arvoille. Ensin esittelemme vector<int>-olion aurinkoinen: vector<int> aurinkoinen; Ilmentymä luodaan lauseessa: cout << keskiarvo(aurinkoinen.begin(), aurinkoinen.end()) << endl; aurinkoinen.begin() ja aurinkoinen.end() -funktioiden palauttamat paluuarvot ovat tyyppiä vector<int>::iterator - tätä tyyppiä käytetään mallin toisen version ilmentymän muodostamisessa. Eli mallifunktion keskiarvo() kahden ilmentymän allekirjoitukset näyttävät seuraavilta: double keskiarvo<double*>(double* a, double* loppu); double keskiarvo<vector<int>::iterator>(vector<int>::iterator a, vector<int>::iterator loppu); Kuten olemme maininneet, jotkin STL:n vector-säiliön toteutukset käyttävät osoittimia (kuten int*) iteraattoreina. Tällöin toisen ilmentymän allekirjoitus näyttää seuraavalta: double keskiarvo<int*>(int* a, int* loppu); Kaikki STL:n toteutukset eivät kuitenkaan näin tee eikä muutenkaan ole korrektia olettaa, että voimme käyttää lyhyempää muotoa. Jotta ymmärrät, miksi olemme kirjoittaneet ::iterator-osan tyyppiin, vilkaistaan, miltä vectormallin määrittely näyttäisi: template <typename Arvo> 837

C++ Ohjelmoijan käsikirja class vector public: typedef Arvo* iterator; // paljon lisää koodia... ; Kun ilmennämme vector-mallin ilmentymän vector<int>, mallista tulee jotain seuraavankaltaista: class vector<int> public: typedef int* iterator; ; Eli vector<int>::iterator on vain tapa viitata siihen tyyppiin, jota vector-mallin ilmentymässä on käytetty iteraattorin toteutuksessa. Nimi iterator kuvaa tätä tyyppiä. Kuten tulostuksesta huomaat, keskiarvon laskenta sujuu kuten odotimmekin. Funktiomme toimii niin vector-säiliön alkioiden kuin tavallisen taulukonkin yhteydessä. Mallitaulukko-vaihtoehto Olisimme voineet hyödyntää malleja myöskin taulukkomuotoisessa ratkaisussa. Kokeile itse - Keskiarvon laskeminen Mallitaulukon avulla Tässä on vaihtoehtoinen versio esimerkille 20.3: // Esimerkki 20.4 Keskiarvon laskeminen mallitaulukon avulla #include <iostream> #include <vector> using namespace std; template <typename Taulukko> double keskiarvo (Taulukko a, long lkm) // Taulukko voi olla osoitin tai iteraattori double summa = 0.0; for (long i = 0; i < lkm; ++i) summa += a[i]; return summa/lkm; // tapahtuu virhetilanne, jos lkm==0 int main() double lampotila[] = 10.5, 20.0, 8.5 ; // Toinen parametri on nyt lukumäärä cout << "taulukon keskiarvo = " << keskiarvo(lampotila, (sizeof lampotila/sizeof lampotila[0])) << endl; 838

Standardi mallikirjasto vector<int> aurinkoinen; aurinkoinen.push_back(7); aurinkoinen.push_back(12); aurinkoinen.push_back(15); cout << aurinkoinen.size() << " kuukautta" << endl; cout << "aurinkoisten päivien lukumäärän keskiarvo: "; cout << keskiarvo(aurinkoinen.begin(), aurinkoinen.end() - aurinkoinen.begin()) << endl; return 0; Tämän ohjelman tulostus on aivan sama kuin esimerkissä 20.3. Koodi näyttää jopa selkeämmältä! Huomaa, kuinka nyt tarvitsemme ainoastaan yhden operaation alkiolle a, nimittäin a.operator[](long). Tästä huomaamme heti, että indeksointioperaattorin parametri on long-tyyppinen - se kuvaa indeksin arvoa. Tässä versiossa meidän täytyy esitellä oma silmukkamuuttuja i - edellisessä versiossa se oli tarpeeton. Iteraattori istream_iterator Seuraava esimerkki havainnollistaa, kuinka voit käyttää mallialgoritmia epätavallistenkin iteraattoreiden kanssa. Olemme jo nähneet ostream_iterator-iteraattorin toiminnassa - tällä kertaa käytämme sen vastakohtaa, istream_iterator, joka hakee tiedon syöttövirrasta. Kokeile itse - Maaginen istream_iterator<> Esimerkeissä 20.3 ja 20.4 käytimme keskiarvo()-funktiotamme arvoille, jotka olivat olemassa taulukossa tai säiliössä. Tällä kertaa keskiarvo()-funktio lukee arvot suoraan syöttövirrasta. Käytämme tässä jälleen esimerkin 20.3 mallifunktiota: // Esimerkki 20.5 Lasketaan virrasta luettujen arvojen keskiarvo #include <iostream> #include <iterator> // istream_iterator<>-malli using namespace std; template <typename Iter> double keskiarvo(iter a, Iter loppu) double summa = 0.0; // Tähän lasketaan summa int lkm = 0 ; for( ; a!= loppu ; lkm++) summa += *a++; // Summataan taulukon alkiot return summa / lkm; // Palautetaan keskiarvo int main() cout << "Syötä numeroita, eroteltuina tyhjillä merkeillä - välilyönneillä, " << endl << "sarkaimella tai rivinvaihdoilla. Lopuksi paina merkkiyhdistelmää, " << endl << "joka vastaa tiedoston loppua (Ctrl-Z PC:ssä)" << endl; double ka = keskiarvo(istream_iterator<double>(cin), istream_iterator<double>()); cout << "Arvojen keskiarvo on " << ka << endl; return 0; 839

C++ Ohjelmoijan käsikirja! Kun suoritat tämän ohjelman, varmista, että käytät oikeaa merkkiyhdistelmää syöttövirran sulkemiseksi. PC-maailmassa tämä merkkiyhdistelmä on Ctrl-Z. Tämän jälkeen voi olla tarpeen painaa Enter. Eräs suosittu ohjelmointiympäristö tulee yhtiöltä, jonka tulee kuulla kaikki kahteen kertaan, joten jos ensimmäinen Ctrl-Z ei sulje virtaa, yritä painaa Ctrl-Z useampaan kertaan eri riveille. Tässä on ohjelman tulostus: Syötä numeroita, eroteltuina tyhjillä merkeillä - välilyönneillä, sarkaimella tai rivinvaihdoilla. Lopuksi paina merkkiyhdistelmää, joka vastaa tiedoston loppua (Ctrl-Z PC:ssä) 3.6 4.5 5.7 Arvojen keskiarvo on 4.6 Sinun järjestelmäsi saattaa tulostaa jotain hieman erilaista. Älä yritä suorittaa tätä ohjelmaa liian hartaasti, jos se tuottaa ongelmia. Myöhemmin meillä on toinen esimerkki, jossa vältetään konsolin käyttö. Palaamme istream_iterator-iteraattoriin vielä uudelleen. Kuinka se toimii Seuraava lause muodostaa mallimme ilmentymän: double ka = keskiarvo(istream_iterator<double>(cin), istream_iterator<double>()); Tässä välitämme keskiarvo()-funktiollemme varsin oudon näköisiä lausekkeita! Kirjoitetaan tämä toisella tavalla, jotta voimme tutkia sitä tarkemmin: istream_iterator<double> alku(cin); istream_iterator<double> loppu; double ka = keskiarvo(alku, loppu); Olio alku on mallin istream_iterator<double> ilmentymä. Virtaolio cin välitetään muodostinfunktiolle, jotta iteraattori lukee standardista syöttövirrasta. Olio loppu on myöskin mallin istream_iterator<double> ilmentymä. Luomme end-olion käyttämällä istream_iterator<double>-luokan oletusmuodostinfunktiota. Kun muuttuja end luodaan tällä tavalla, se toimii lopun yli -arvona, joka vastaa tiedoston loppua. Uuden versiomme alku ja loppu eivät todellakaan ole osoittimia - ne ovat luokkaolioita, joten miksi tämä toimii? Vastaus on se, että nämä oliot ovat iteraattoreita, mikä tarkoittaa, että ne voivat käyttäytyä osoittimien tapaan. Ne silti välittävät käyttöömme kolme operaattoria, joista olemme riippuvaisia keskiarvo()-funktiossamme: 840 bool Iter::operator!=(Iter end_value); double Iter::operator*(); Iter Iter::operator++(int); Vertailu Viittaus Lisäys

Standardi mallikirjasto Tällä kertaa olemme esittäneet nämä luokkafunktioina, koska ne todellakin ovat luokkafunktioita. Ne ovat luokan istream_iterator<double> uudelleenmääriteltyjä operaattorifunktioita. Palauta mieleen, että operator++(int) ei itse asiassa saa int-tyyppistä parametriä - tällä tavalla vain kerromme kääntäjälle että tarkoitamme lisäysoperaattorin jälkiliitteellistä muotoa. Voimme käyttää näitä operaatioita minkä tahansa istream_iterator<double>-tyyppisen olion kohdalla. Toiminnallisuutta sisältävät istream_iterator-iteraattorit Edellä olleet istream_iterator-oliot teeskentelevät olevansa osoittimia, kuten LtkPtr-luokkamme oliot esimerkissä 14.6. Nämä oliot pystyvät kuitenkin paljon muuhunkin - ne voivat sisältää monimutkaistakin toiminnallisuutta, kuten virtojen I/O-operaatioita. Tämä on erittäin keskeinen ajatus, sillä sen avulla STL on niin tehokas kirjasto. Kun yhdistämme mallifunktiot ja iteraattorit, voimme hankkia välimme mistä tahansa lähteestä. Tätä et voi tehdä vanhan taulukkomuotoisen esitystavan avulla. Kokeile itse - Lisää istream_iterator-taikaa Korostaaksemme iteraattoreiden yleistä luonnetta, tässä on vielä yksi esimerkki. Tällä kertaa kokeilemme puoliavointa väliä arvoilla, jotka ovat merkkijonossa. Jos sinulla oli vaikeuksia esimerkin 20.5 suorituksessa, kokeile mieluummin tätä. // Esimerkki 20.6 Lasketaan merkkijonovirran arvojen keskiarvo #include <iostream> #include <iterator> // istream_iterator<>-malli #include <sstream> // istringstream-iteraattori using namespace std; template <typename Iter> double keskiarvo(iter a, Iter loppu) double summa = 0.0; // Tähän lasketaan summa int lkm = 0 ; for( ; a!= loppu ; lkm++) summa += *a++; // Summataan taulukon alkiot return summa / lkm; // Palautetaan keskiarvo int main() char* kurssi_lukemat = "4.5 6.75 8.25"; istringstream kello (kurssi_lukemat); istream_iterator<double> alku(kello); istream_iterator<double> loppu; cout << "Lukemat: " << kurssi_lukemat << ". Päivän keskiarvo on "; cout << keskiarvo (alku, loppu) << endl; return 0; Esimerkin tulostus näyttää seuraavalta: Lukemat: 4.5 6.75 8.25. Päivän keskiarvo on 6.5 841

C++ Ohjelmoijan käsikirja Kuinka se toimii Jos katsot tarkkaan, tämä esimerkki on lähes identtinen esimerkkiin 20.5 verrattuna. Erona on se, että olemme korvanneet cin-virran omalla kello-virralla käyttämällä luokkaa istringstream: char* kurssi_lukemat = "4.5 6.75 8.25"; istringstream kello (kurssi_lukemat); Alustamme kello-syöttövirtamme siten, että se lukee arvot suoraan ohjelman merkkijonosta kurssi_lukemat. Tämän jälkeen välitämme virtaolion istream_iterator<doubel>muodostinfunktiolle: istream_iterator<double> alku(kello); Uuden kello-virtamme hienona piirteenä on se, että sitä voidaan käyttää useimmissa tapauksissa, joissa saattaisimme käyttää cin-virtaa - istream_iterator ei ole niin pikkutarkka. Se käyttää tyytyväisenä istingstream-oliotamme kello tai muuta merkkijonojen lähdettä virran cin sijaan. Kaikki muu säilyy aivan samana, mukaan lukien keskiarvo()-mallimme! Lisää iteraattoreista Esimerkeissä 20.3-20.6 olemme päässeet varsin pitkälle käyttämällä mallipohjaista versiotamme keskiarvo()-funktiosta. Olemme kuitenkin muodostaneet vain kolme erilaista ilmentymää: double keskiarvo<double*>(double*, double*); double keskiarvo<int*>(int*, int*); double keskiarvo<istream_iterator<double> > (istream_iterator<double>, istream_iterator<double>); Näistä näet selvästi, että kaksi ensimmäistä versiota käsittelevät yksinkertaisia osoittimia, kun taas kolmas käsittelee istream_iterator-oliota. Esimerkiksi esimerkissä 20.3 välitämme alkiot vector<int>-säiliöstä keskiarvo<int*>-versiolle. Esimerkeissä 20.5 ja 20.6 käytämme istream_iterator-oliota, jolloin voimme lukea numeeriset arvot konsolilta tai suoraan tiedostosta. Osoitinversiot käsittävät sekä C++:n perustaulukot että vector-säiliöt, kun taas istream_iterator-versio käsittää tiedostot ja merkkijonot. Emmekä ole vielä kuin hieman raapaisseet pintaa, mitä iteraattoreihin tulee. Iteraattoriolio näyttää yhä varsin mysteeriseltä, joten pysähdytään hetkeksi ja mennään hieman syvemmälle iteraattoriolioiden toimintaan. Huomaat varsin pian, että omien iteraattoriluokkien luominen ei olekaan niin vaikeaa. 842 Iteraattorit Meillä on jo hieman kokemusta hyvin yksinkertaisesta iteraattorista, kun luvussa 14 loimme LtkPtr-luokan; mutta voimme tehdä huomattavasti enemmänkin. Ymmärrämme paremmin STL:n iteraattoreiden toimintaa, jos luomme oman iteraattorimme aloittamalla ensin aivan tyhjästä. Aloitamme yksinkertaisimmasta mahdollisesta iteraattorista ja laajennamme sitä pikku hiljaa, kunnes se on sellainen kuin haluammekin sen olevan - STL:n iteraattoreiden veljeskunnan täysi jäsen.

Standardi mallikirjasto Kokeile itse - Oman iteraattorin luonti Aloitamme yksinkertaisesta iteraattorista, joka käy läpi kokonaislukuja. Seuraavassa on esimerkkimme koodi: // Esimerkki 20.7 Yksinkertainen kokonaislukuiteraattoriluokka #include <iostream> using namespace std; class Kokonaisluku public: Kokonaisluku (int par = 0) : X(par) bool operator!=(const Kokonaisluku& par) const //!= -vertailu if (X == par.x) // Debuggaustulostus cout << endl // Näytetään, että olemme täällä << "operator!= palauttaa arvon false" << endl; return X!= par.x; int operator*() const return X; Kokonaisluku& operator++() ++X; return *this; //Osoitusoperaattori // Etuliitteellinen lisäysoperaattori private: int X; ; int main() Kokonaisluku alku(3); Kokonaisluku loppu(7); cout << "Tämän päivän kokonaisluvut ovat: "; for( ; alku!= loppu ; ++alku) cout << *alku << " "; cout << endl; return 0; Kun esimerkki suoritetaan, se tulostaa: Tämän päivän kokonaisluvut ovat: 3 4 5 6 operator!= palauttaa arvon false Kuinka se toimii Ohjelma toimii aivan kuin olisimme esitelleet muuttujat alku ja loppu tavallisiksi kokonaisluvuiksi ja sijoittaneet niihin samat arvot. Kokonaisluku-luokkamme tulostaa lisäksi lisärivin, jos uudelleenmääritelty!=-operaattori palauttaa arvon false. Määrittelemme Kokonaisluku-luokan muodostinfunktion seuraavasti: 843

C++ Ohjelmoijan käsikirja Kokonaisluku (int par = 0) : X(par) Kokonaisluku-luokalla on jäsenmuuttuja X, joka tallettaa nykyisen arvon. Jos emme onnistu alustamaan Kokonaisluku-oliota esittelyn yhteydessä, jäsenmuuttujan X oletusarvo on 0. Luokalle määritellään!=-operaattorifunktio: bool operator!=(const Kokonaisluku& par) const //!= -vertailu if (X == par.x) // Debuggaustulostus cout << endl // Näytetään, että olemme täällä << "operator!= palauttaa arvon false" << endl; return X!= par.x; Tämän oikeanpuolisena parametrinä on Kokonaisluku-olio ja se palauttaa bool-tyyppisen paluuarvon. Ohjelman tulostuksesta huomaat, että heti kun alku on yhtä suuri kuin loppu,!=operaattori palauttaa arvon false ja silmukan suoritus päättyy. Osoitusoperaattori määritellään seuraavasti: int operator*() const return X; //Osoitusoperaattori Tämä palauttaa nykyisen Kokonaisluku-olion jäsenmuuttujan X. Tämä funktio, sekä myös operator!=()-funktio, määritellään const-tyyppisiksi, koska ne eivät muuta olion sisältöä. Etuliitteellinen lisäysoperaattori määritellään seuraavasti: Kokonaisluku& operator++() ++X; return *this; // Etuliitteellinen lisäysoperaattori Edellisten esimerkkien keskiarvo()-funktiossa käytimme lauseketta *a++ jälkiliitteellisen operator++(int)-funktion sijaan. Kokonaisluku-luokallamme on nyt yksinkertaiset funktiot. Tässä on itse asiassa kaikki mitä tarvitsemme, jotta saamme sen toimimaan osoittimen tapaan, jolloin voimme tehdä joitakin varsin mielikuvituksellisia asioita. main()-funktiossa meillä on lause: for( ; alku!= loppu ; ++alku) cout << *alku << " "; 844 Tällaisen lauseen pitäisi näyttää jo varsin tutulta. Käytimme kovin samanlaista lausetta keskiarvo()-algoritmissamme, mutta tällöin käytimme *a++-lauseketta. Itse asiassa tulostuslauseen *alku:n korvaaminen *alku++:lla ei toimi tässä, koska emme ole määritelleet operator++(int)- funktiota, joka tukee Kokonaisluku-luokan jälkiliitteellistä muotoa. Tästä syystä käytämme muotoa *alku tulostuslauseessa ja lauseketta ++alku silmukan kontrollilausekkeessa. Itse asiassa olisimme voineet kirjoittaa:

Standardi mallikirjasto for( ; alku!= loppu ; ) cout << *++alku << " "; Tässä silmukka tulostaisi arvot 4-7. Lausekkeen *++alku arvo on jäsenmuuttujan alku.x arvo lisäyksen jälkeen. Iteraattoreiden välitys algoritmille Eli mitä hyötyä Kokonaisluku-luokastamme on? Mitä sellaista Kokonaisluku voi tehdä, mihin vanha kunnon int ei pysty? Katsotaanpa. Kokeile itse - Kokonaisluku-olion välityskeskiarvo<>:lle // Esimerkki 20.8 Kokonaisluku-olion keskiarvojen laskenta #include <iostream> using namespace std; class Kokonaisluku public: Kokonaisluku (int par = 0) : X(par) bool operator!=(const Kokonaisluku& par) const return X!= par.x; //!= -vertailu int operator*() const return X; Kokonaisluku& operator++() ++X; return *this; //Osoitusoperaattori // Etuliitteellinen lisäysoperaattori const Kokonaisluku operator++(int) // Jälkiliitteellinen ++-operaattori Kokonaisluku temp(*this); // Talletetaan nykyinen arvo ++X; // Vaihdetaan uuteen arvoon return temp; // Palautetaan muuttumaton talletettu arvo private: int X; ; template <typename Iter> double keskiarvo(iter a, Iter loppu) double summa = 0.0; // Tähän lasketaan summa int lkm = 0 ; for( ; a!= loppu ; lkm++) summa += *a++; // Summataan taulukon alkiot 845

C++ Ohjelmoijan käsikirja return summa / lkm; // Palautetaan keskiarvo int main() Kokonaisluku ensim(1); Kokonaisluku viim(11); cout << "Kokonaislukujen keskiarvo väliltä " << *ensim << " - " << -1+*viim; cout << " on " << keskiarvo(ensim, viim) << endl; return 0; Ohjelman tulostus on seuraava: Kokonaislukujen keskiarvo väliltä 1-10 on 5.5 Kuinka se toimii Emme tehneet kovinkaan suuria muutoksia. Lisäsimme operator++()-operaattorin jälkiliitteellisen muodon: const Kokonaisluku operator++(int) // Jälkiliitteellinen ++-operaattori Kokonaisluku temp(*this); // Talletetaan nykyinen arvo ++X; // Vaihdetaan uuteen arvoon return temp; // Palautetaan muuttumaton talletettu arvo Tiedämme jo entuudestaan, että toteuttaaksemme jälkiliitteellisen lisäysoperaattorin, meidän täytyy luoda kopio nykyisestä oliosta - tätä meidän ei tarvitse tehdä etuliitteellisessä muodossa. Tämä luo tietysti lisätyötä, koska tällöin kutsutaan kopiomuodostinta. Tästä syystä suurin osa seuraavista esimerkeistä käyttää etuliitteellistä muotoa aina, kun se vain on mahdollista, koska se on tehokkaampaa. Yksinkertaisen Kokonaisluku-luokkamme kohdalla tällä ei ole juuri mitään merkitystä, mutta kun teemme mallialgoritmeja, emme voi olla varmoja, miten paljon suoritusaikaa olion kopioiminen vie, kun käytämme lauseketta Iter++. Käytämme Kokonaisluku-luokkaamme seuraavissa lauseissa: Kokonaisluku ensim(1); Kokonaisluku viim(11); cout << "Kokonaislukujen keskiarvo väliltä " << *ensim << " - " << -1+*viim; cout << " on " << keskiarvo(ensim, viim) << endl; Jälleen kerran, viim on lopun yli -arvo. Meidän tulee vähentää siitä 1, jotta saamme sallitun arvon. Lisäksi, jos ensim olisi pelkkä int iteraattorin sijaan, meidän ei tarvitsisi käyttää lauseketta *ensim, jotta saamme sen arvon selville. 846 Jos oikein pysähdyt ajattelemaan, olemme tehneet jotain hyvin erilaista kutsuessamme keskiarvo()-funktiota uudella Kokonaisluku-iteraattorillamme. Kaikissa aikaisemmissa esimerkeissä keskiarvoon lasketut arvot olivat jossain - taulukoissa, vector-säiliöissä tai virroissa. Tässä esimerkissä arvoja ei ole olemassa missään. Ne lasketaan Kokonaisluku-luokan sisällä, kun keskiarvo()-algoritmi kutsuu kolmikkoa operator!=(), operator*() ja operator++().