Olio-ohjelmointi Geneerisyys Aiemmin käsiteltiin kolme monimuotoisuuden muotoa. Tässä osassa tutustutaan niistä neljänteen geneerisyyteen. Esitys perustuu pääosin teoksen [Bud] lukuun 18. Java-kielen geneerisyyden perusteisiin voi tutustua esimerkiksi teoksen [Ves] luvusta 9.8. C++-kielen geneerisen ohjelmoinnin tukimekanismeja käsitellään kirjan [Stro] luvussa 13, teoksen [Eck] luvussa 16 ja kirjan [Hie] luvussa 31. 1. Johdanto Ohjelman toimintojen toteutus on varsin usein olennaisilta osiltaan riippumaton siinä käytettävistä tietotyypeistä. Tämä ilmenee selvästi rakennettaessa erilaisia säiliöitä tai muita tietorakenteita. Esimerkiksi lineaarinen lista toimii täsmälleen samoin riippumatta siitä, ovatko sen alkiot kokonaislukuja, desimaalilukuja tai merkkijonoja. Geneerisen ohjelmoinnin avulla voidaan välttää kirjoittamasta eri toteutusta jokaiselle tietotyypille. Geneerisyys tarkoittaa monimuotoisuutta, jossa tietotyyppien nimet ovat monimuotoisia. Staattisesti tyypitetyissä kielissä tämä tarkoittaa käytännössä tietotyyppien parametrisoimista siten, että kutakin ohjelmassa esiintyvää konkreettista tyyppiä kohden kääntäjä tekee oman toteutuksen. Geneerisyys on näin ollen monimuotoisuuden käännösaikainen muoto, joka tukee erityisesti koodin uudelleenkäyttöä. Geneeristä ohjelmointia sovelletaan yleisimmin tyyppiriippumattomien säiliöiden ja algoritmien toteuttamiseen. Stroustrupin ([Stro], s. 40) mukaan geneerisen ohjelmoinnin paradigma voidaan kiteyttää lauseeseen: Decide which algorithms you want: parameterize them so that they work for a variety of suitable types and data structures. C++-kielen standardissa C++11 on tehty hieman laajennuksia kielen geneerisyyteen, mutta tässä käsitellään asiaa array-kokoelmaa lukuunottamatta aiemman standardin mukaisesti. 2. Geneeriset funktiot C++-kielessä geneerisyyttä tuetaan ns. template-mekanismin avulla. Tarkastellaan ensin, miten kielessä toteutetaan vapaa geneerinen funktio. Oletetaan, että halutaan tehdä funktio, joka palauttaa kahdesta parametristaan pienemmän. Tämä voidaan toteuttaa seuraavasti: template <typename Type> Type pienempi(type t, Type u) { if(t < u){ return t; return u; Edellä Type on eräänlainen parametri, mutta ei tavallinen funktion parametri, vaan tyyppiparametri, johon kääntäjä sovittaa funktiokutsussa käytettävien parametrien tyypin. Tällaista funktiota kutsutaan geneeriseksi funktioksi tai aliohjelmamalliksi ([Hie], luku 31). Kääntäjä tekee kaikkia erilaisia tyyppejä kohti oman version, mikäli tietotyyppi vain toteuttaa vertailuoperaattorin <. Funktiota voidaan siten kutsua ohjelmassa esimerkiksi seuraavasti: 1
int x=10, y=5; double d1 = 5.2, d2 = 8.2; string s1 = "alku"; string s2 = "loppu"; cout << pienempi(x,y) << endl; cout << pienempi<double>(d1,d2) << endl; cout << pienempi(s1,s2) << endl; Jos kääntäjä voi päätellä tietotyypin funktion kutsusta, ohjelmoijan ei tarvitse määritellä sitä välttämättä eksplisiittisesti. Edellä ainoastaan toisessa funktiokutsussa on parametrityyppi annettu. Näissä tapauksissa vertailuoperaattori on toteutettu, joten kääntäminen sujuu ongelmitta. Sen sijaan, jos ohjelmassa olisi määritelty luokka class Vertaamaton { ; joka ei toteuta vertailuoperaattoria, koodi pienempi(v1,v2); ei käänny mikäli muuttujat v1 ja v2 ovat ko. luokan olioita. Geneerisiä funktioita voidaan kyllä soveltaa myös käyttäjän määrittelemiin tyyppeihin, kunhan vain funktiossa tehdään ainoastaan tietotyypin sallimia operaatioita. Jos edelliseen luokkaan määriteltäisiin vertailuoperaattori seuraavasti class Vertaamaton { public: bool operator<(const Vertaamaton &v); ; bool Vertaamaton::operator<(const Vertaamaton &v){ // Palauta vertailun tulos niin funktiota voitaisiin soveltaa myös tähän luokkaan. Java-kielessä geneerisyys on liitetty kielen ominaisuuksiin versiosta 1.5 (eli JDK 5.0:sta) lähtien. Javassa ei vapaita funktioita ole, mutta edellistä esimerkkiä vastaava geneerinen metodi voitaisiin Javassa toteuttaa seuraavasti: public static <T extends Comparable> T pienempi (T x, T y){ if(x.compareto(y)<0) return x; return y; Huomaa, että Javassa on kulmasuluissa esiteltävän tyyppiparametrin jälkeen määriteltävä, että tyyppi perii luokan Comparable, jotta vertailun varsinaisesti suorittava metodi compareto() olisi käytettävissä. 2
3. Geneeriset luokat Varsinkin C++:ssa käytetään melko paljon geneerisiä funktioita, mutta vielä paljon yleisempää on parametrisoida luokka, jolloin luokkaa sanotaan geneeriseksi luokaksi tai luokkamalliksi ([Hie], luku 31). Erityisesti erilaisia tietorakenteita toteutettaessa on hyödyllistä tehdä luokista geneerisiä. Tutustaan C++:n geneerisiin luokkiin. Pino on tietorakenne, joka noudattaa LIFO-periaatetta ( Last In First Out ), ts. viimeksi pinoon tallennettu tietoalkio otetaan ensimmäiseksi pois. Oletetaan, että halutaan kirjoittaa pino, jonka eri toteutukset voivat säilöä minkä tahansa tyyppistä tietoa. Luokan rajapinta on // Lueteltu typpi: ei liity erityisesti geneerisyyteen enum PinoPoikkeus { PinoTyhja, PinoTaysi ; template<typename Type> class Pino { private: int koko; int paa; Type* pino; public: Pino(int k):koko(k),paa(0) { pino = new Type[koko]; ~Pino(){delete[] pino; void push(type); Type pop(); bool empty(); ; Tyyppiparametri annetaan luokan määrittelyn alussa ja sitä voi käyttää missä tahansa luokan sisällä. Kun luokasta tehdäänkin luokkamalli, sen metodeista tulee myös automaattisesti geneerisiä funktioita, minkä pitää myös näkyä toteutuksessa: // Malliluokan jäsenfunktioiden määrittelyt: template <typename Type> void Pino<Type>::push(Type elem){ if (paa == koko) throw PinoTaysi; // Ylivuoto pino[paa++] = elem; template <typename Type> Type Pino<Type>::pop(){ if (paa == 0) throw PinoTyhja; // Alivuoto return pino[--paa]; template <typename Type> bool Pino<Type>::empty(){ return (paa==0); 3
Huomaa, että tyyppiparametria voidaan käyttää luokassa kuten mitä tahansa tietotyyppiä: se voi olla luokan jäsenmuuttujien tyyppinä, voidaan määritellä ko. tyyppiä sisältäviä taulukoita, voidaan käyttää tyyppiä metodien parametrien ja paluuarvon tyyppinä. Ylläolevassa koodissa yli- ja alivuototilanteissa heitetään poikkeus; poikkeuksiin tutustutaan lyhyesti ihan kurssin lopussa. Jos luokkaa aiotaan käyttää ohjelmassa useammassa kooditiedostossa, on koko luokan koodi sijoitettava normaalista poiketen otsikkotiedostoon, koska kääntäjä tekee jokaiselle tyyppiparametrille oman toteutuksen luokasta. Luokkaa voitaisiin käyttää ohjelmassa esimerkiksi näin: // Pino jossa double-lukuja Pino<double> dpino(3); dpino.push(1.2); dpino.push(-1.3); cout << dpino.pop() << endl; cout << dpino.pop() << endl <<endl; // Pino jossa string-olioita Pino<std::string> spino(3); spino.push("eka"); spino.push("toka"); cout << spino.pop() << endl; cout << spino.pop() << endl << endl; Pino-oliota luotaessa on annettava tyyppi, jota käytetään parametrina. Muuten kääntäjä ei tiedä, minkälainen Pino on luotava. Alityyppijärjestelmä toimii luokkamalleille hieman eri tavalla kuin intuitiivisesti voisi olettaa. Vaikka yhden luokan tyyppiparametri on toisen luokan tyyppiparametrin alityyppi, ei luokkamallien välillä ole alityyppisuhdetta. Oletetaan että edellisessä pino-esimerkissä olisi määritelty luokat class Luokka { // Luokan koodia ; class Aliluokka : public Luokka { // Luokan koodia ; Nyt siis Aliluokka on Luokka-luokan alityyppi. Kuitenkaan Pino<Aliluokka> ei ole luokan Pino<Luokka> alityyppi, joten ohjelmassa ei ole sallittua tehdä sijoitusta Pino<Luokka> pino(2); Pino<Aliluokka> toinenpino(3); pino = toinenpino; Voidaan nimittäin osoittaa, että operaation salliminen johtaisi ongelmiin. Tässä ei kuitenkaan paneuduta asiaan syvemmin. Mainittakoon, että C++:aan liittyy olennaisena osana STL-kirjasto (Standard Template Library), joka on yleiskäyttöinen geneeristen algoritmien ja tietorakenteiden kokoelma. C++-ohjelmoijan on välttämätöntä tuntea ainakin jossain määrin kirjaston käyttöä. STL:n ominaisuuksia tarkastellaan hieman lisää tuonnempana. 4
Esitetään lopuksi vertailun vuoksi esimerkki geneerisen luokan toteuttamisesta Javalla. Aiempi geneerinen pinoluokka voitaisiin kirjoittaa Javalla seuraavasti: // Malliluokan määrittely: class Pino<T> { private int koko; private int paa; private T[] pino; public Pino(int k){ koko = k; paa = 0; pino = (T[])new Object[koko]; public void push(t elem){ if (paa == koko){ throw new PinoPoikkeus("Pino täysi"); pino[paa++] = elem; public T pop(){ if (paa == 0){ throw new PinoPoikkeus("Pino tyhjä"); return pino[--paa]; public boolean empty(){ return (paa==0); public boolean full(){ return (paa==koko); Javan kaikki luokat periytyvät viime kädessä Object-luokasta, jonka alityyppejä kaikki luokat ovat Javan periytymismallista johtuen. Näin ollen pinoon voi laittaa minkä tahansa luokan olioita, esimerkiksi Javan merkkijonoluokan String olioita seuraavasti: // Pino jossa string-olioita Pino<String> spino = new Pino<String>(3); spino.push("eka"); spino.push("toka"); System.out.println(sPino.pop()); System.out.println(sPino.pop()); 5
Perustietotyyppien muuttujat eivät sen sijaan ole Javassa olioita, joten perustietotyyppien tallentamisessa on käytettävä niiden kääreluokkia, esimerkiksi näin: // Pino jossa Double-olioita Pino<Double> dpino = new Pino<Double>(3); dpino.push(new Double(1.2)); dpino.push(new Double(-1.3)); System.out.println(dPino.pop().doubleValue()); System.out.println(dPino.pop().doubleValue()); Koodista havaitaan merkittävä ero Javan ja C++:n mallien välillä: C++ sallii ongelmitta parametrityyppisen taulukon luomisen, mutta Java antaa käännösvirheen, mikäli yrittää pinon konstruktorissa luoda taulukkoa seuraavasti: pino = new T [koko]; Javassa suositellaan tyyppiparametreina käytettävän yksikirjaimisia isoin kirjaimin kirjoitettuja nimiä. Kun ohjelmaa käännetään, kääntäjä korvaa kaikki muodolliset tyyppiparametrit todellisilla tyyppiparametreilla. Kääntäjä suorittaa ohjelmoijan puolesta tarvittavat tyyppimuunnokset ja -tarkastukset. Java onkin tässä suhteessa huomattavasti C++:aa tarkempi. Toisin kuin C++:ssa, Javassa voi asettaa rajoituksia tyyppiparametrille. Jos haluttaisiin esimerkiksi rajoittaa edellä käsitellyn pinoluokan alkiot numeerisiin tyyppeihin, muutettaisiin vain tyyppiparametrin esittely muotoon class Pino<T extends Number> Lisäksi muutetaan taulukon varaus muodostimessa: pino = (T[])new Number[koko]; Tällöin pinon tyyppiparametriksi ei voi antaa kuin Number-luokasta periytyviä luokkia. Nyt voitaisiin toteuttaa esimerkiksi pinon alkioiden summan laskeminen luokan metodiksi public double sum() { double d = 0.0; for(int i=0; i<paa; i++) { d += (pino[i]).doublevalue(); return d; mikä toimii, koska kaikki Number-luokasta periytyvät luokat toteuttavat metodin doublevalue(), joka palauttaa olion numeerisen arvon reaalilukuna. Javan tyyppiparametreissa voidaan lisäksi käyttää ns. jokereita (wild cards), jotka merkitään kysymysmerkillä. Jokeria tarvitaan erityisesti välitettäessä geneerisiä olioita parametriksi metodeille. Sanokaamme, että halutaan tehdä metodi, jota voi soveltaa mihin tahansa pinoon. Silloin metodin parametriksi tarvitaan parametrityypiltään tuntematon Pino. Tämä saadaan aikaan jokerilla: static void printandclear(pino<?> stack) { while(!stack.empty()) { System.out.println(stack.pop()); Tälle metodille voidaan antaa parametriksi mikä tahansa pino, jonka alkiot metodi tulostaa tyhjentäen samalla pinon. 6
4. Standard Template Library Kuten aiemmin mainittiin, C++:n toteutukseen sisältyy melko laaja joukko geneerisiä tietorakenteita, kokoelmia, ja niitä käsitteleviä algoritmeja. Tätä kokoelmaa kutsutaan nimellä Standard Template Library (STL). Tarkastellaan hieman tämän standardikirjaston perusominaisuuksia. Kirjaston pääkomponentteja ovat kokoelmat (tai tietosäiliöt), selaajat (eli iteraattorit) ja algoritmit. Selaajia käytetään kokoelmien läpikäymiseen ja algoritmeja kokoelmien käsittelemiseen, esimerkiksi lajitteluun, tiedon etsimiseen jne. Lisäksi STL sisältää funktio-olioita (funktioiden tapaan käyttäytyviä olioita) ja sovittimia (jotka vaihtavat komponenttien liittymiä) operoinnin helpottamiseksi. Kokoelmat Kokoelmat ovat siis standardikirjastoon integroituja geneerisiä tietorakenteita, joihin voidaan tallentaa millaisia tietoalkioita tahansa. Standardikirjasto sisältää luokkamalleja kokoelmia varten. Nämä kokoelmat voidaan karkeasti jakaa peräkkäisrakenteisiin kokoelmiin (deque, list, forward_list [c++11], vector, array [c++11]) ja avainrakenteisin kokoelmiin (set, unordered_set [c++11], multiset, unordered_multiset [c++11] map, unordered_map [c++11], multimap, unordered_multimap [c++11]). C++11-standardiin on lisätty kokoelmia, jotka on edellä merkitty luetteloon maininnalla [c++11]. Tässä tutustutaan esimerkinomaisesti vector-kokoelmaan, jonka avulla voidaan toteuttaa vaihtuvamittaisia taulukoita. Lisäksi perehdytään C++11-standardin array-kokoelmaan, joka kapseloi sisäänsä kiinteämittaisen taulukon. Vector on dynaamisesti kasvava taulukko, johon voidaan tallentaa minkä tahansa yhden tietotyypin tietoa, myös käyttäjän määrittelemien luokkien olioita. Perehdytään vector-luokan ominaisuuksiin tarkastelemalla seuraavaa ohjelmaa, joka tallentaa käyttäjän syöttämät reaaliluvut vector-kokoelmaan ja laskee niiden keskiarvon käymällä kokoelman läpi. Samalla luvut myös tulostetaan. 7
#include <iostream> #include <vector> using namespace std; int main(int argc, char** argv) { // Reaalilukuvektori vector<double> numbers; double input; cout << "Syötä reaalilukuja, nolla lopettaa" << endl; do { cin >> input; if( input!= 0 && cin.good()) { numbers.push_back(input); while(input!=0 && cin.good()); double sum = 0.0; if(!numbers.empty()) { cout << "Lukujen " << endl; for(int i=0; i < numbers.size(); i++){ cout << numbers[i] << " "; sum += numbers[i]; double average = sum/numbers.size(); cout << endl << "keskiarvo on " << average << endl; return 0; Rivi vector<double> numbers; määrittelee double-lukuja tallentavan vectorin. Tämän jälkeen ohjelma lukee silmukassa käyttäjältä lukuja nollasyötteeseen saakka. Luetut luvut lisätään vectoriin kutsumalla luokan metodia push_back: numbers.push_back(input); Metodin kutsu lisää uuden luvun vectorin viimeiseksi alkioksi samalla kasvattaen siitä tarvittaessa. Vectorin alkioihin voidaan viitata kuten taulukon alkioihin, mutta vectorin etu taulukkoon verrattuna on esimerkiksi, että vectorin sisältämien alkioiden lukumäärä saadaan selville kutsumalla luokan size-metodia: numbers.size(); 8
Lisäksi metodi numbers.empty() kertoo, onko vectorissa lainkaan alkioita. Mainittakoon vielä, että vectorista voidaan poistaa alkio metodin erase avulla. Tähän tarvitaan kuitenkin ns. selaajaa, johon tutustutaan seuraavaksi. Selaajat Selaajat eli iteraattorit ovat älykkäitä osoittimia eli osoitinolioita, joita STL:n -algoritmit käyttävät kokoelmien alkioiden käsittelyssä. Myös ohjelmoija voi niiden avulla lukea kokoelman alkioita, kirjoittaa alkioita kokoelmaan, kulkea kokoelmassa eteen- ja taaksepäin tai siirtyä suoraan tietyn alkion kohdalle. Selaajia voidaan käyttää indeksien asemesta niissäkin tapauksissa, joissa kokoelma sallii indeksien käytön. Seuraavaksi tutustutaan vector-kokoelman selaajia käyttävään esimerkkiohjelmaan. Edellisen esimerkkiohjelman osa if(!numbers.empty()) { cout << "Lukujen " << endl; for(int i=0; i < numbers.size(); i++){ cout << numbers[i] << " "; sum += numbers[i]; double average = sum/numbers.size(); cout << endl << "keskiarvo on " << average << endl; voitaisiin toteuttaa selaajan avulla seuraavasti: if(!numbers.empty()) { cout << "Lukujen " << endl; vector<double>::iterator iter; for(iter = numbers.begin(); iter!= numbers.end(); iter++){ cout << *iter << " "; sum += *iter; double average = sum/numbers.size(); cout << endl << "keskiarvo on " << average << endl; Nyt vector-kokoelmaa ei käydäkään läpi kuten taulukkoa, vaan ensin esitellään kokoelman selaaja vector<double>::iterator iter; Tässä määritellään double-tyyppisiä alkioita sisältävän vektorin selaaja. Koska ohjelmassa ei ole tarkoitus muuttaa vektorin alkioita, voitaisiin myös käyttää vakioselaajaa, jota ei voi käyttää alkioiden kirjoittamiseen. Näin ollen yllämainittu rivi voitaisiin myös kirjoittaa muotoon vector<double>::const_iterator iter; 9
Jokainen kokoelma määrittelee omat selaajansa, joiden avulla kokoelmia voi käyttää ohjelmassa samaan tyyliin. Seuraavassa for-silmukassa for(iter = numbers.begin(); iter!= numbers.end(); iter++){ cout << *iter << " "; sum += *iter; käydään läpi kaikki kokoelman alkiot; kokoelman jäsenfunktio begin palauttaa osoittimen kokoelman ensimmäiseen alkioon ja end palauttaa osoittimen, joka saadaan kun selaajaa siirretään eteenpäin kokoelman viimeisestä alkioista. Selaajaa siirretään eteenpäin operaattorilla ++. Huomaa, että selaaja on osoitin, joten kokoelman alkio saadaan viittaamalla selaajan osoittamaan muistiin käyttämällä operaattoria *. Kokoelmille määritellään algoritmeja alkioiden etsimiseen ja kokoelmien lajittelemiseen otsikkotiedostossa algorithm. Jos halutaan esimerkiksi poistaa kokoelmasta alkion 2.0 ensimmäinen esiintymä, se voidaan tehdä seuraavasti: vector<double>::iterator iter; iter = find(numbers.begin(), numbers.end(), 2.0); if(iter!= numbers.end()){ numbers.erase(iter); Ensin haetaan kokoelmasta mainittu alkio algoritmin find avulla. Parametriksi annetaan selaajilla väli jolta haetaan (yllä koko vector). Paluuarvona saadaan selaaja, joka osoittaa haluttuun arvoon. Mikäli haluttaisiin toteuttaa tulostaminen ja alkioiden summan lasku yleisesti funktioina, jotka käsittelevät minkä tahansa tyyppisiä vector-kokoelmia, voitaisiin laatia geneeriset funktiot: template <typename Type> void tulosta(vector<type> &vektori){ typename vector<type>::const_iterator iter; for(iter = vektori.begin(); iter!= vektori.end(); iter++){ cout << *iter << " "; template <typename Type> Type summa(vector<type> &vektori) { Type sum = 0; typename vector<type>::const_iterator iter; for(iter = vektori.begin(); iter!= vektori.end(); iter++){ sum += *iter; return sum; Nyt kokoelman alkiotyyppi on tyyppiparametrina, joka määräytyy kun ohjelmassa on funktion kutsu: tällöin kääntäjä kiinnittää parametrille Type jonkin tyyppiarvon. Yllä olevissa funktioissa kokoelman selaaja on määriteltävä käyttäen avainsanaa typename, jotta tunnistetaan sen olevan tyyppiparametrin avulla määritelty. Tällöin kokoelman tulostaminen ja summan laskeminen tapahtuisi seuraavasti: 10
cout << "Lukujen " << endl; tulosta(numbers); sum = summa(numbers); double average = sum/numbers.size(); cout << endl << "keskiarvo on " << average << endl; array C++11-standardissa on kokoelmien joukkoon lisätty array, joka kapseloi sisäänsä kiinteämittaisen taulukon. Kurssin alkuosasta muistetaan, että tavallinen taulukko ei tiedä omaa kokoansa, joten taulukon koko joudutaan välittämään funktiolle parametrina samalla kuin taulukkokin, mikäli halutaan välttyä taulukon rajojen ylitykseltä. Esimerkiksi funktiot taulukon tulostamiseksi ja lajittelemiseksi kirjoitettaisiin geneerisinä funktioina seuraavasti: // Tavallisen taulukon tulostus. // Huomaa, että // 1. taulukon koko on saatava parametrina // 2. Taulukko välitetään osoittimena ensimmäiseen alkioon // 3. Funktiossa ei voi muuttaa taulukon sisältöä template <typename Type> void tulostataulu(const Type *taulu, int koko) { for(int i = 0; i < koko; i++){ std::cout << "taulukko[" << i <<"] = " << taulu[i] << std::endl; // Tavallisen taulukon lajittelu. // Huomaa, että // 1. taulukon koko on saatava parametrina // 2. Taulukko välitetään osoittimena ensimmäiseen alkioon // 3. Funktiossa voidaan muuttaa taulukon sisältöä template <typename Type> void lajitteletaulu(type *taulu, int koko) { for(int i = 0; i < koko; i++){ for(int j = i+1; j < koko; j++){ if( taulu[i] > taulu[j] ){ Type temp = taulu[i]; taulu[i] = taulu[j]; taulu[j] = temp; Funktiot käsittelevät siis minkä tahansa tietotyypin taulukoita; taulukon koko joudutaan kuitenkin välittämään funktioille parametrina. Taulukon sijasta voidaan käyttää vectorkokoelmaa tämän tarpeen poistamiseksi, mutta tämä tapahtuu tehokkuuden kustannuksella, koska vector on vaihtuvamittainen kokoelma ja vaatii siksi mutkikkaampia operaatioita kuin kiinteämittainen taulukko. Tämän vuoksi C++-standardiin on lisätty array-kokoelma, joka yhdistää taulukon käsittelyn tehokkuuden kokoelmien ohjelmalliseen hallintaan, kokoelmaa voidaan esimerkiksi läpikäydä selaajien avulla. Seuraavassa esimerkissä on toteutettu edelliset taulukkojen käsittelyfunktiot array-kokoelman avulla: 11
// array-luokan otsikkotiedosto #include <array> // array-kokoelman tulostus. // Huomaa, että // 1. Taulukon koko voidaan kysyä kokoelmalta // 2. array voidaan välittää viitteenä funktiolle // 3. Funktiossa ei voi muuttaa kokoelman sisältöä template <typename Type, size_t Size> void tulostaarray(const std::array<type,size> &taulu) { for(int i = 0; i < taulu.size(); i++){ std::cout << "taulukko[" << i <<"] = " << taulu[i] << std::endl; // array-kokoelman lajittelu. // Huomaa, että // 1. Taulukon koko voidaan kysyä kokoelmalta // 2. array voidaan välittää viitteenä funktiolle // 3. Funktiossa voidaan muuttaa kokoelman sisältöä template <typename Type, size_t Size> void lajittelearray(std::array<type,size> &taulu) { int koko = taulu.size(); for(int i = 0; i < koko; i++){ for(int j = i+1; j < koko; j++){ if( taulu[i] > taulu[j] ){ Type temp = taulu[i]; taulu[i] = taulu[j]; taulu[j] = temp; Koska array on geneerinen luokka, joka ottaa tyyppiparametreina taulukkoon tallennettavien tietoalkioiden tyypin ja taulukon koon, pitää tyyppiparametrien näkyä myös funktion toteutuksessa. Tässä tapauksessa funktioista on tehty myös geneerisiä, joten ne voivat käsitellä mitä tahansa array-kokoelmia riippumatta niihin tallennettujen tietoalkioiden tyypistä. Funktioita voitaisiin käyttää esimerkiksi näin: int main(int argc, char** argv){ // Luodaan kokonaisulukuja sisältävä array std::array<int,10> intarray = {15, 28, 11, 56, 31, 8, 34, 17, 10, 19; std::cout << std::endl << "Array alussa:" << std::endl; tulostaarray(intarray); lajittelearray(intarray); std::cout << std::endl << "Array lajiteltuna:" << std::endl; tulostaarray(intarray); return 0; Käännettäessä C++11-standardin mukaista koodia kääntäjälle on yleensä ilmaistava tämä erikseen. Esimerkiksi g++-kääntäjälle on annettava käännösparametri std=c++11. 12
5. IOStream-kirjasto Aiemmin osassa Johdanto ohjelmointiin C++-kielellä tutustuttiin hieman perus IOtoimintoihin. Tässä perehdytään hieman tarkemmin C++:n IOStream-kirjastoon, jonka avulla käyttäjän kanssa kommunikoidaan. IOStream-kirjasto toteuttaa IO-toiminnot käyttäen tietovirroiksi tai tietovoiksi (yksikössä siis tietovuo ) kutsuttuja olioita. Vuopohjaisessa tiedonsiirrossa datan ajatellaan koostuvan virrasta samankokoisia yksiköitä, jotka voivat olla esimerkiksi tavuja tai merkkejä. Tällöin luku- ja tulostustoiminnot mielletään käsitteellisesti em. yksiköiden virtana ohjelmasta tai ohjelmaan. Tulostustoimintojen kantaluokka on luokkamalli basic_ostream; ohjelmassa käytetään useimmiten sen tavalliselle merkkityypille (char) kiinnitettyä toteutusta ostream. Esimerkiksi standarditulostusvirran cout tyyppi on ostream. Tulostusoperaattori << on kuormitettu luokassa ostream kaikille sisäisille tietotyypeille. Mikäli ohjelmoija haluaa tulostaa omia tyyppejä käyttäen operaattoria <<, on operaattori ylikuormitettava myös näille tyypeille. Tarkastellaan esimerkiksi koosteolioiden yhteydessä esiintynyttä Henkilo-luokkaa ja lisätään siihen ylikuormitettu tulostusoperaattori: // Tiedosto henkilo.h class Henkilo { private: std::string etunimi; std::string sukunimi; std::string sotu; public: Henkilo(std::string en,std::string sn,std::string stu); ~Henkilo(){; ; std::string getetunimi() const; std::string getsukunimi() const; std::string getsotu() const; // Ylikuormitettu tulostusoperaattori std::ostream& operator<<(std::ostream &os,const Henkilo &h); 13
// Tiedosto henkilo.cpp #include <iostream> #include <string> #include "henkilo.h" Henkilo::Henkilo(std::string en,std::string sn,std::string stu): etunimi(en),sukunimi(sn),sotu(stu){ std::string Henkilo::getEtunimi() const{ return etunimi; std::string Henkilo::getSukunimi() const{ return sukunimi; std::string Henkilo::getSotu() const{ return sotu; // Ylikuormitetun tulostusoperaattorin toteutus std::ostream& operator<<(std::ostream &os,const Henkilo &h){ os << h.getetunimi() <<" "<< h.getsukunimi() << std::endl; os << h.getsotu(); return os; Huomaa, että tulostusoperaattori ei ole luokan jäsenfunktio, joten jäsenmuuttujien arvot saadaan ainoastaan luokan saantimetodien kautta. Huomaa myös. että tulostusoperaattori palauttaa viitteen käytettävään tulostusvirtaan, joten tulostuksia voidaan ketjuttaa. Nyt luokan olioita voitaisiin tulostaa esimerkiksi seuraavasti: Henkilo h("aku", "Ankka","190920-3134"); Henkilo eco("umberto","eco","050132-2212"); cout << h << endl << eco << endl; Lukutoimintojen kantaluokka on basic_istream; luokka istream on sen tavalliselle merkkityypille (char) kiinnitetty toteutus, jota ohjelmassa yleensä käytetään. Lukuoperaattori >> on luokassa kuormitettu sisäisille tietotyypeille ja omille tietotyypeille voidaan lukuoperaattori ylikuormittaa tarvittaessa. Tällöin on kuitenkin huomioitava virhesyötteiden mahdollisuus. Edellä esiteltyyn Henkilo-luokkaan lukuoperaattorin toteutus on suoraviivainen, koska luokan konstruktorille syötetään kolme merkkijonoa. Ensin otsikkotiedostoon lisätään esittely // Ylikuormitettu lukuoperaattori std::istream& operator>>(std::istream &is, Henkilo &h); 14
Tämän jälkeen sen toteutus lisätään lähdekooditiedostoon: std::istream& operator>>(std::istream &is, Henkilo &h){ std::string enimi=""; std::string sunimi=""; std::string sotu=""; is >> enimi >> sunimi >> sotu; h = Henkilo(enimi, sunimi, sotu); return is; Nyt operaattoria voidaankin soveltaa ohjelmassa: Henkilo h("aku", "Ankka","190920-3134"); cout << "Anna henkilon tiedot" << endl; cin >> h; // Luetaan etunimi, sukunimi ja hetu cout << endl << h << endl; Tiedostot Standarditulostuksen ja lukemisen lisäksi ohjelmissa tarvitaan tiedostojen luku- ja kirjoitustoimintoja. Tiedostojen lukuvirtaluokka ifstream periytyy luokasta istream ja tulostusvirtaluokka ofstream luokasta ostream. Näin ollen luokille istream ja ostream määritellyt luku- ja kirjoitusoperaattorit operoivat myös tiedostoja, esimerkiksi Henkilo-luokan olio voitaisiin kirjoittaa tiedostoon lisäämällä ohjelmaan otsikkotiedosto <fstream>, jossa tiedostovirrat on määritelty, ja kirjoittamalla Henkilo h("aku", "Ankka","190920-3134"); ofstream tied_out("henk.txt"); tied_out << h << endl; Tällöin ohjelma kirjoittaa tiedostoon henk.txt rivit Aku Ankka 190920-3134 Henkilön tiedot voitaisiin puolestaan lukea ko. tiedostosta seuraavasti: Henkilo aku("","",""); ifstream tied("henk.txt"); tied >> aku; cout << aku << endl; Mikäli tiedostoon pitää kirjoittaa eri tavalla kuin muutoin tulostetaan, voidaan tulostusoperaattori luonnollisesti ylikuormittaa myös tiedoston tulostusvirralle. Sama koskee myös tiedostosta lukemista. Tiedosto on aina käytön jälkeen suljettava, minkä voi tehdä tiedostovirtaolioiden metodilla close. Tämä ei kuitenkaan aina ole tarpeen, sillä tiedostovirtaolion hajotin sulkee vielä avoimen tiedoston. 15
Muistipohjainen IO Luku- ja kirjoitusoperaatiot voidaan liittää myös string-luokan merkkijonoihin. Tämän tekee otsikkotiedostossa <sstream> määritelty stringstream-luokka, joka mainittiin jo osassa Johdanto ohjelmointiin C++-kielellä. Luokkaa käytetään useimmiten muuttamaan merkkijonoja jonkin tietotyypin muuttujien arvoiksi ja päinvastoin. Luokan luku- ja kirjoitustoiminnot kohdistuvat sisäiseen puskuriin, jonka sisältö string-oliona voidaan lukea milloin tahansa kutsumalla luokan jäsenfunktiota str. Esimerkiksi seuraava geneerinen funktio muuntaa minkä tahansa tietotyypin, jolle operaattori << on määritelty, muuttujan merkkijonoksi: template<class T> string converttostring(const T& t) { stringstream strm; strm << t; return strm.str(); Ohjelmissa joudutaan myös usein muuntamaan syötetty merkkijono toisen tyyppiseksi, esimerkiksi numeeriseksi tiedoksi. Se voidaan tehdä alla olevalla geneerisellä funktiolla: template <typename T> const T convertfromstring(const string& str) { stringstream strm(str); T tmp; strm >> tmp; return tmp; Jälkimmäisessä funktiossa stringstreamin puskuri alustetaan merkkijonolla, joka yritetään lukea annetun tietotyypin muuttujan arvoksi. Em. funktioita voitaisiin käyttää pääohjelmassa vaikkapa seuraavasti: 16
#include <iostream> #include <string> #include <sstream> using namespace std; int main() { string luku1 = "1.234"; string luku2 = "2.33"; cout << convertfromstring<double>(luku1) + convertfromstring<double>(luku2) << endl; int koko1 = 1234, koko2 = 5678; cout << converttostring<int>(koko1)+ converttostring<int>(koko2) << endl; string rivi,sana; cout << "Anna rivi niin erottelen siita sanat: " << endl; getline(cin, rivi); istringstream strm(rivi); int i=1; while(strm >> sana) { cout << "Sana " << i++ << " = " << sana << endl; return 0; Aluksi ohjelmassa muunnetaan kaksi merkkijonoa reaaliluvuiksi ja lasketaan ne yhteen. Tämän jälkeen muunnetaan kaksi kokonaislukua merkkijonoiksi, jolloin yhteenlasku liittää merkkijonot peräkkäin. Lopuksi ohjelmassa demonstroidaan stringstream-olion käyttämistä syötetyn rivin jäsentämiseksi sanoihin. Lähteet [Bud] Budd, Timothy A: An Introduction to Object-Oriented Programming, Addison-Wesley 2002 [Eck] Eckel, Bruce: Thinking in C++, 2 nd edition, Volume 1, 2000. Saatavissa osoitteesta: http://mindview.net/books/ticpp/thinkingincpp2e.html [Stro] Stroustrup, Bjarne: The C++ Programming Language, Addison-Wesley 1997 [Ves] Vesterholm, Kyppö: Java-ohjelmointi, Talentum 2006 17