Geneeriset luokat C++ - perusteet Java-osaajille luento 6/7: Template, tyyppi-informaatio, nimiavaruudet Geneerinen luokka tarkoittaa parametroitua luokkamallia, jonka avulla voidaan muodostaa useita, tietyllä tavalla toimivia "oikeita" luokkia. Luokkaa, joka on luotu geneerisen luokan mukaan, voidaan kutsua luokkamallin ilmentymäksi. (Samaan tapaan kuin oliota kutsutaan luokan ilmentymäksi) Geneerisestä luokasta ei voida suoraan luoda olioilmentymää. Geneerisestä luokasta luodaan ensin oikeita luokkia ja niistä sitten olioilmentymiä. Geneerisen luokan käyttö Geneeriset luokat Geneerisen luokat ovat hyödyllisiä haluttaessa yleiskäyttöisiä säiliöluokkia. Tyypillisiä säiliöluokkia ovat perustietorakenteita (listat, pinot, puut) kuvaavat luokat. Jos haluamme Pino-luokan, joka pitää sisällään Luokan X-olioita, teemme geneerisen Pino-luokan, johon kiinnitetään käännösaikaisesti parametriksi X-luokan olio. Miten geneeriset luokat ovat toteutettu Javassa? Java vs. C++ geneerisyys Java Javassa jokainen luokka periytyy Object-luokasta. Tällöin geneeristen luokkien teko perustuu periytymiseen. Jos luokan tyyppinä on siis Object, edustaa tämä olio Javan mitä tahansa oliota. Jos perustietotyyppejä halutaan säilöä "geneerisesti", pitää Javassa luoda näistä olioilmentymiä (Integer, Double ). C++ Myös C++:ssa geneerisyyden voi toteuttaa periyttämällä. Tällöin tosin pitää luoda Javan kaltainen Object-luokka ja periyttää kaikki käyttämänsä luokat tästä. C++:ssan tapa toteuttaa geneeriset luokat on template-luokat. (Malli, kaavain) Template (mallit) Mallit mahdollistavat tyyppiriippumattoman ohjelmointitavan (geneerinen ohjelmointi) Mallista generoidaan tyyppiin sidottuja ohjelmakoodiversioita käännösaikana C++:ssa voidaan geneerisiä 1. funktioita 2. luokkia 1
Funktiomallit Aina samanlaisena toistuva aliohjelmalogiikka voidaan kirjoittaa aliohjelmamalliksi (function template). Kääntäjä generoi aliohjelman kutsun yhtyedessä käytetyyn tyyppiin sopivan aliohjelman. Ilman aliohjelmamallija on kirjoitettava useita eri aliohjelmia, joissa logiikka on sama, mutta tiedon tyyppi vaihtelee. Ei siis käytetä funktion kuormittamista, vaan käytetään aliohjelmamalleja. Tavallinen aliohjelma int min(int p_eka, int p_toka) if(p_eka < p_toka) return p_eka; return p_toka; double max(double p_eka, double p_toka) if(p_eka < p_toka) return p_eka; return p_toka; cout << min(10, 3 ) << endl; cout << min(10.0, 12.7) << endl; Aliohjelmamalli T min(t p_eka, T p_toka) if(p_eka < p_toka) return p_eka; return p_toka; cout << min(10, 0) << endl; cout << min(12.2, 12.7) << endl; Aliohjelmamalli Aliohjelman esittely ja määrittely: template <geneerinen_tyyppi> aliohjelman toteutus Geneerisen tyypin nimenä käytetään tavallisesti yhtä suurta kirjainta (usein T). < > - merkkien välissä voi olla useita eri geneerisiä tyyppitunnuksia pilkuilla erotettuina. Kunkin tunnuksen eteen kirjoitetaan varattu sana class Kääntäjä korvaa geneerisen tyypin T kutsussa käytetyn tiedon tyypillä. Luokkamalli Luokkamalli (class template) sisältää tarvittavat tietojäsenet ja geneeristä tietotyyppiä käsittelevät aliohjelmamallit. Luokkamallin avulla voidaan uudelleenkäyttää kaikkia luokan aliohjelmia kirjoittamatta niitä uudelleen. Luokkamallin tyypillisiä käyttökohteita ovat mm. tietorakenneluokat. Luokkamallin määrittely ja aliohjelmien toteutus kirjoitetaan samaan tiedostoon. Aliohjelmia ei sijoiteta erilliseen cpp-tiedostoon, koska ne eivät ole sellaisenaan käännettäviä Aliohjelmat voidaan kääntää vasta, kun kääntäjä on korvannut geneerisen tyypin käytetyllä tyypillä tai luokalla. Luokkamallin määrittely Luokkamallin määrittely template <geneerinen_tyyppi> class Malliluokka ; Metodeiden esittelyn yhteydessä ei tarvitse käyttää merkintää Geneerisestä luokasta luodaan "normaali"-luokka kertomalla sille tyyppi (siis class T:n oikea tyyppi) Geneerinen luokka: Malliluokka Tavallinen luokka: Malliluokka<int> Luokan ilmentymä: Malliluokka<int> ilmentymä 2
Geneerinen luokka, C++ template <class t> class GeneerinenPino..; GeneerinenPino<Henkilo> GeneerinenPino<Henkilo> henkilopino Luokkamalli, esimerkki: pino Tehdään edellisessä kuvassa ollut GeneerinenPino-luokka Pinoon voidaan siis laittaa mitä tahansa olioita ja primitiivityyppejä. Toteutetaan Pino käyttäen dynaamista taulukkoa (listaratkaisu luonnollisesti parempi) GeneerinenPino<Auto> GeneerinenPino<Auto> autopino Pino Pino Pino (stack) sisältää alkioita, joita lisätään ja poistetaan pinon huipulta LIFO-periaatteen mukaisesti (last-in-first-out). Perusoperaatiot ovat pinoa (push), joka lisää uuden alkion pinon päälle ja pura (pop), joka poistaa alkion pinon päältä. Muita operaatioita ovat koko: Palauttaa alkioiden lukumäärän. onkotyhja: Tarkistetaan onko pino tyhjä. huippu: Palauttaa päällimmäisen alkion poistamatta sitä pinosta. tulosta: Tulostetaan pinon sisältö. Sarja pino-operaatioita ja niiden vaikutus pinoon. Pino on aluksi tyhjä. Operaatio onkotyhja() pinoa(1) pinoa(2) huippu() pinoa(3) koko() pura() pura() onkotyhja Tulos 2, 3 3, 2 false Pino () (1) (2,1) (2,1) (3,2,1) (3,2,1) (2,1) (1) (1) Pino: tavallinen toteutus class Pino private: int *lista; int ylin; int maksimi; Pino(int koko); void pinoa(int x); int pura(); ; Pino: tavallinen toteutus Pino::Pino(int koko) lista = new int[koko]; maksimi = koko -1; ylin = -1; void Pino::pinoa(int x) if(ylin < maksimi) lista[++ylin] = x; int Pino::pura() if(ylin >= 0) return lista[ylin--]; 3
Pino: tavallinen toteutus Pino pino(10); pino.pinoa(2); pino.pinoa(3); cout << pino.pura(); // 3 cout << pino.pura(); // 2 Mitä virheitä edellisessä toteutuksessa oli? Pino: template-toteutus class GeneerinenPino private: T *lista; // dynaamisesti varattava taulukko; int ylin; int maksimi; GeneerinenPino(int koko); ~GeneerinenPino(); void pinoa(t x); T pura(); ; Pino: template-toteutus GeneerinenPino<T>::GeneerinenPino(int koko) lista = new T[koko]; maksimi = koko -1; ylin = -1; GeneerinenPino<T>::~GeneerinenPino() delete lista; void GeneerinenPino<T>::pinoa(T x) if(ylin < maksimi) lista[++ylin] = x; T GeneerinenPino<T>::pura() if(ylin >= 0) return lista[ylin--]; Pino: template-toteutus GeneerinenPino<int> intpino(2); intpino.pinoa(2); intpino.pinoa(3); cout << intpino.pura(); cout << intpino.pura(); GeneerinenPino<string> stringpino(3); stringpino.pinoa("hellurei"); stringpino.pinoa("xxx"); stringpino.pinoa("yyy"); cout << stringpino.pura() << endl; cout << stringpino.pura() << endl; Harjoitus: funktiomalli a) Kirjoita funktiomalli samat(), joka palauttaa totuusarvon, jos kaksi sille välitettyä parametria (tyyppi mikä tahansa) ovat samat. Aliohjelma palauttaa arvon false, jos parametrit eivät ole samat. Testaa kokonaisluvuilla ja liukuluvuilla b) Tee luokka Henkilo, jolla on tietojäsenenä nimi ja henkilötunnus (string). Tee konstruktori ja get ja set metodit tietojäseniä varten. Luo main-funktiossa kaksi Henkilö oliota (Älä käytä dynaamista muistinvarausta). Välitä oliot funktiolla samat. Miten Henkilo-luokkaa pitää muuttaa, jotta funktio toimisi? Toteuta ratkaisusi siten, että Henkilo-oliot ovat samat, kun olioiden tietojäsenet (nimi ja henk. tunnus) ovat samat. Harjoitus: luokkamalli a) Kirjoita luokkamalli Pari, jonka jäseniksi voidaan tallettaa kaksi minkä tahansa tyyppistä tietoa/olioita. Määrittele luokkaan muodostin, joka saa nämä kaksi oliota parametrina. Tee luokalle tulosta-funktio, joka tulostaa tietojäsenet (cout). Testaa kokonaisluvuilla ja liukuluvuilla b) Testaa luokkamallia Henkilo-luokan olioilla. Kun tulostafunktiota kutsutaan, tulostetaan henkilöiden nimi ja henkilötunnus. (Kuormita operaattori <<) 4
Tyyppi-informaatio Tyyppi-informaatio ja tyyppimuunnokset RTTI (run-time type information)-mekanismi mahdollistaa olioiden tyypin ajonaikaisen tutkimisen. RTTI:n avulla on mahdollista tutkia olioiden luokkatyyppejä ja periytymishierarkiaa. Uudehko C++-mekanismi Vanhat kääntäjät eivät tue Visual C++ 6.0 ei oletusarvoisesti tue projektiasetuksista tuen saa päälle RTTI, typeid typeid-operaattorilla voidaan tutkia olion luokka. Operaattori palauttaa viittauksen Type_info olioon, joka edustaa olion tyyppinimeä. Operaattorin käyttö: typeid(lauseke) lauseke on osoitin tai viittaus olioon, muuttujanimi tai tyyppinimi Type_info luokkaa on ylikuormitettu operaattorit == ja!=, joilla voidaan vertailla lausekkeiden tyyppien yhdenmukaisuutta. before aliohjelmalla voidaan tarkistaa luokkajärjestystä typeid, yksinkertainen esimerkki #include <typeinfo> using namespace std; int x = 2; cout << typeid(x).name() << '\n'; // Tyypin nimi if(typeid(x) == typeid(4)) // Tyyppien vertailu cout << "Kummatkin kokonaislukuja\n"; typeid: monimutkainen esimerkki class Ylaluokka int x, y; virtual void f()cout << "\nyläluokka"; virtual void f1() ; class Alaluokka: Ylaluokka int x; void f()cout <<"\nalaalaluokka"; ; typeid: monimutkainen esimerkki Ylaluokka a; Alaluokka *c = new Alaluokka(); cout << "\na-olion luokan nimi: "; cout << typeid(a).name(); cout << "\nc-olion luokan nimi: "; cout << typeid(*c).name(); if(typeid(a) == typeid(*c)) cout <<"\na ja c ovat saman luokan olioita"; cout <<"\na ja c eivät ole saman luokan olioita"; if(typeid(ylaluokka).before(typeid(alaluokka))) cout << "\nalaluokka on Yläluokan yläpuolella"; if(typeid(*c).before(typeid(a))) cout << "\nc-olion luokka a-olion luokan yläpuolella\n"; 5
Tyyppimuunnoksista C-kielen tyyppimuunnokset sekoittuvat helposti muuhun ohjelmakoodiin. C++-kieleen on rakennetty tyyppimuunnos-operaattoreita static_cast const_cast dynamic_cast static_cast static_cast-operaattori muuntaa parametrina saadun muuttujan tai olion tyyppiä. Operaattorin toiminta vastaa C-kielen tyyppimuunnoksen toimintaa, mutta on näkyvämpi koodissa. int luku = 10; double tulos; tulos = static_cast<double>(luku) / 3; const_cast const_cast-operaattori poistaa parametrista const määreen. Muutoksen jälkeen muuttujaan voi siis tehdä muutoksia. int countchars(char *pstring, char character) // Teejotain return 1; const char *p_somestring = "Let's make things better"; int result = countchars(const_cast<char *> (p_somestring), 'e'); RTTI, dynamic_cast<t> T* osoitin2 = dynamic_cast<t*>(osoitin1); Turvallinen tyyppimuunnos luokkahierarkiassa Palauttaa NULL, jos muunnos ei onnistu Mahdollistaa osajoukon valinnan Esim. Valitaan henkilöistä pelkät työntekijät Esimerkki: dynamic_cast Tyontekija tyontekija("kalle","kojootti","kalastaja"); Henkilo *h = &tyontekija; Tyontekija *t; t = dynamic_cast<tyontekija*>(h); if(t) // jos kyseessä työntekijä cout << "Titteli: " << t->gettitteli() << endl; Nimiavaruudet 6
Nimiavaruudet Nimiavaruuden käyttö Yksitasoisen globaalin avaruuden ongelma Saman nimisiä globaaleja symboleja ei saa olla Yksitasoinen nimiavaruus ongelma erityisesti suurissa projekteissa, joissa ohjelmakoodi tulee useilta eri toimittajilta Perinteinen ratkaisu: Etuliitteen käyttö C++-kielen ratkaisu: Nimiavaruudet Javan ratkaisu: Paketti Tapa 1, nimiavaruus käyttöön using namespace std; void main() cout << Terve! << endl; Tapa 2, tunnuksen kautta viittaaminen void main() std::cout << Terve! << std::endl; Oman nimiavaruuden määrittely Nimiavaruus: esimerkki Määrittely jokaiseen otsikko/lähdekooditiedostoon // omat.h namespace oma class Luokka... ; void funktio(); Oman avaruuden käyttö #include omat.h void main() oma::luokka olio; oma::funktio(); #include <string> namespace a void tervehdi(std::string a) std::cout << a; namespace b std::string tervehdys = "moi"; a::tervehdi(b::tervehdys); 7