Esimerkki luokkahierarkiasta: C++-kielen IOstream-kirjasto Tässä materiaalissa tutustutaan tarkemmin C++:n luokkahierarkiaan. Koska kyseessä on oliopohjainen kieli, C++:n luokat on järjestetty hierarkisesti aikaisemmin tarkasteltuja periytymissääntöjä noudattaen. Hierarkiassa on yksi tai joissain tapauksissa useampia kantaluokkia, joista periytyy johdettuja luokkia. Virta eli stream on abstrakti käsite, joka tarkoittaa laitetta, johon ohjelmassa tapahtuvat syöttö- ja tulostusoperaatiot kohdistuvat. Virta voidaan ajatella loppumattomaksi merkkien virraksi, jota luetaan ja johon tulostetaan. Virrat ovat yleensä liitetty (assosioitu) johonkin fyysiseen laitteeseen kuten esimerkiksi tiedostoon, näppäimistöön, näyttöön tai tietoliikenneporttiin. Oletusarvoisesti merkkejä luetaan standardisyötöstä (stdin) joka on yleensä näppäimistö ja kirjoitetaan standarditulostuslaitteelle (stdout) joka on tietokoneen näyttö. Merkkien lisääminen virtaan tarkoittaa siis konkreettisesti merkkien lähettämistä jollekin fyysiselle laitteella ja lukeminen virrasta merkkien vastaanottamista laitteelta. Koko C++-kielen syöttö- ja tulostusjärjestelmä eli I/O-järjestelmä voidaan esittää oheisen luokkahierarkia mukaisesti. Kaaviossa luokat ovat valkealla pohjalla ja luokan metodit mustalla.
Kantaluokka on nimeltään ios_base ja se on määritelty otsikkotiedostossa <ios>. ios_base-luokan johdettu luokka on nimeltään ios. Tästä on edelleen johdettu luokat istream ja ostream. Luokkien määritykset sisältävät otsikkotiedostot (header files) eli kirjastot on kaaviossa ilmoitettu < >-merkkien sisällä. Yhdessä kirjastossa (esim <fstream>) voi siis olla määriteltynä monta luokkaa (kuvassa ifstream, fstream, ofstream ja filebuf). Syöttövirta <istream> Istream-oliot lukevat (read) ja tulkitsevat (interpret) syötteikseen saamiaan merkkejä (esimerkiksi näppäimistöltä tai tiedostosta). Luokalla on kahdentyyppisiä jäsenfunktioita: Muotoiltu syöttö (formatted input) o Nämä funktiot lukevat merkkejä käyttäen ylikuormattua >> operaattoria ja muuntavat ne määrätyn tyyppiseksi tiedoksi. o Esimerkkinä tämän tyyppisestä syötöstä on cin-metodi. Muotoilematon syöttö (unformatted input) o Nämä eivät tulkitse lukemiaan merkkejä tietyn tyyppiseksi tiedoksi.
o Lukevat syöttövirrasta merkkejä käyttäen metodeja get, getline, peek, read, readsome, manipuloivat get-osoitinta tai hakevat tietoa viimeisestä muotoilemattomasta syöttöoperaatiosta (gcount). Tulostusvirta <ostream> Ostream-oliot kirjoittavat (write) ja muotoilevat merkkejä määritellylle tulostuslaitteelle (esimerkiksi näyttö tai tiedosto). Myös nämä voidaan jakaa kahteen ryhmään: Muotoiltu tulostua (formatted output) o Jäsenfunktiot tulkitsevat ja muotoilevat tulostettavat merkit käyttäen ylikuormattua << operaattoria. o Esimerkkinä tämän tyyppisestä tulostuksesta on cout-metodi. Muotoilematon tulostua (unformatted output) o Jäsenfunktiot kirjoittavat dataa siinä muodossa kuin ne sen saavat, ilman tyyppimuunnoksia tai muotoiluja. o Metodeja put, write, seekp, tellp Muotoiltu syöttö- ja tulostus tarkoittavat käytännössä ASCII-muotoisen tiedon käsittelyä ja muotoilematon binäärisen tiedon käsittelyä (erityisesti tiedostojen tapauksessa). Syöttö- ja tulostusvirta <iostream> Luokkahierarkiakaaviosta nähdään, että paljon käyttämämme iostream-luokka on johdettu sekä luokasta istream (input stream) että ostream (output stream). Tällaista periytymistä sanotaan moniperinnäksi (multiple inheritance).
iostream-luokka perii sekä istream- että ostream-luokkien kaikki jäsenfunktiot. Siten se voi hoitaa samalla kertaa sekä syöttö- että tulostustoiminnot. Jokaisessa kirjoittamassamme C++-ohjelmassa on ollut alussa rivi #include<iostream>. Tällä esikääntäjän direktiivillä on siis otettu käyttöön fyysinen, kovalevylle tallennettu iostream-otsikkotiedosto, joka sisältää varsinaisen iostream-luokan toteutuksen eli implementoinnin. Puskuriluokka <streambuf> Palataan vielä luokkahierarkiakuvaan. Kuvasta nähdään, että kantaluokista ios tai ios_base ei ole nuolta luokkaan streambuf, vaikka se luokkahierarkiassa on samalla tasolla istream- ja ostream-luokkien kanssa. Luokka streambuf toimii kantaluokkana kaikille ns. puskuroitua tietoa käsitteleville luokille (stream buffers). Tällaista puskuroitua tietoa tuottavat esimerkiksi vaikkapa tietokoneeseen USB- tai sarjaportin kautta kytketyt mittauslaitteet. Myöskin tiedostojen luku ja kirjoitus käyttää hyväkseen puskuria. Puskuri on fyysisesti tietokoneen muistissa oleva muistialue, joka varataan dynaamisesti tiedon väliaikaistallennukseen ja jonka odotetaan olevan ajan tasalla ulkoisen tiedonlähteen tuottaman tiedon kanssa. Esimerkiksi mittauslaite tallentaa tietoa puskurin toiseen päähän samalla kun sitä luetaan tietokoneen työmuistiin toisesta päästä. Streambuf on ns. abstrakti kantaluokka. Abstraktista kantaluokasta ei voi luoda suoraan luokan instansseja eli olioita, vaan abstraktista luokasta on johdettava luokkia, joiden avulla varsinaiset oliot luodaan (tuntiesimerkkinä käytetty muoto-luokka oli itse asiassa abstrakti luokka).
Streambuf-luokalla on kaksi johdettua luokkaa: filebuf ja stringbuf eli tiedostopuskuri- ja merkkijonopuskuriluokat. Tiedostovirta <fstream> Fstream-kirjasto käsittele tiedostoon kirjoitusta ja lukua. Luokkahierarkiasta nähdään, että se sisältää luokat: ifstream o Johdettu istream-luokasta o Input File Stream, lukee tietoa tiedostosta ofstream o Johdettu ostream-luokasta o Output File Stream, kirjoittaa tietoa tiedostoon fstream o Johdettu iostream-luokasta o Sekä luku että kirjoitus toteutettuna samassa luokassa filebuf o Johdettu luokasta streambuf o Tiedostopuskuri o Fyysisen tiedoston yhdistäminen väliaikaismuistiin Merkkijonovirta <sstream> Luokassa sstream eli string stream tarjoaa käyttöliittymän merkkijonojen käsittelyyn virtoina. Tämä kirjasto mahdollistaa automaattiset ja turvalliset tietotyyppimuunnokset.
Tyyppimuunnoksista esimerkkinä olisi vaikkapa ohjelma, joka odottaa käyttäjältä syötteekseen liukulukua. Jos käyttäjä syöttääkin merkkijonon, ohjelma kaatuu ajonaikaiseen virheeseen tai jää ikuiseen silmukkaan. Turvallinen vaihtoehto on lukea kaikki tieto ensin sisään merkkijonoina ja vasta ohjelmassa yrittää muuttaa tieto halutun tyyppiseksi. Jos muunto ei onnistu, kysytään uudestaan niin kauan että onnistuu. C-kielessä vastaava toteutus tehtiin funktioilla atoi(), sprintf() ja sscanf(). Niiden toteutukseen liittyy pari pahaa ongelmaa: C:ssä ei ole string-luokkaa. Vaikka C mahdollistaa dynaamisen muistinvarauksen ja dynaamiset taulukot, käytännössä niiden käyttö on niin hankalaa että merkkijonot yleensä määritellään vakiomittaisiksi (tyyliin char mj[100];). Jos tyypinmuunnoksen tulos on pidempi kuin taulukon koko, tapahtuu ylivuoto. Tätä ylivuotobugia monet crackerit hyödyntävät pyrkiessään murtautumaan koneisiin verkon yli. Puutteellinen tyyppiturvallisuus (type safety). Stdio-kirjaston funktioissa ei ole tyypin tarkistusmekanismeja. Siten ohjelmat jotka käyttävät vääriä muotoilumääreitä (%d, %s, jne ) aiheuttavat ohjelman joutumisen epämääräiseen tilaan eli ylikirjoittavat muistia väärästä paikasta. Merkkijonovirtakirjasto sisältää seuraavat luokat: istringstream o Johdettu luokasta istream o Merkkijonojen käsittely syöttövirtoina ostringstream o Johdettu luokasta ostream o Merkkijonojen käsittely tulostusvirtoina Stringstream o Johdettu luokasta iostream
o Merkkijonojen käsittely syöttö- ja tulostusvirtoina Stringbuf o Johdettu luokasta streambuf o Puskuriluokka merkkijonojen tallennukseen tietokoneen muistiin. Esimerkkejä Katsotaan seuraavaksi muutama esimerkki IOstream-kirjaston luokkien käytöstä. Esimerkki 1: Merkkitiedon kirjoitus tiedostoon. Ohjelma lukee käyttäjän syöttämiä merkkejä yksitellen niin kauan kuin syötetään EOF eli CTRL-Z. Teksti tallentuu tiedostoon teksti.txt. #include <iostream> #include <fstream> using namespace std; int main() char c; ofstream tiedosto; int lkm=0; tiedosto.open("d:\\teksti.txt"); while ((c=cin.get())!=eof) tiedosto << c; lkm++; tiedosto.close();
cout << "Kirjoitettiin " << lkm << " merkkiä."<< endl; system("pause"); return 0; Esimerkki 2: Luetaan tekstitiedostoa merkki kerrallaan. #include <iostream> #include <fstream> using namespace std; int main() char c; ifstream tiedosto; int lkm=0; tiedosto.open("d:\\teksti.txt"); while ((c=tiedosto.get())!=eof) cout << c; lkm++; tiedosto.close(); cout << "Luettiin " << lkm << " merkkiä."<< endl; system("pause"); return 0;
Esimerkki 3: Ohjelma kysyy käyttäjältä tuotteen hinnan ja kappalemäärän merkkijonoina, muuttaa ne liukuluvuksi ja kokonaisluvuksi sekä tulostaa kokonaishinnan. #include <iostream> #include <sstream> using namespace std; int main () string mjono; float hinta=0; int maara=0; cout << "Hinta: "; getline (cin,mjono); stringstream(mjono) >> hinta; cout << "Kappalemäärä: "; getline (cin,mjono); stringstream(mjono) >> maara; cout << "Kokonaishinta: " << hinta*maara << endl; system("pause"); return 0; Esimerkki 4: Ohjelmassa määritellään stringstream-olio s ja sijoitetaan siihen kaksoistarkkuuden liukulukumuuttujan d arvo. <sstream>-kirjasto käyttää ylikuormattuja << ja >> operaattoreita. #include <sstream> using namespace std; int main()
double d=123.456; stringstream s; string str; s << d; // insert d into s s >> str; //insert string Esimerkki 5: Seuraavassa ohjelmassa määritellään stringstream-olio ja sijoitetaan double-tyyppisen muuttujan d arvo 123.456 siihen käyttäen << operaattoria. Luku voidaan muuttaa string-merkkijonoksi käyttämällä >> operaattoria. #include <iostream> #include <sstream> using namespace std; int main () double d=123.456; stringstream s; string val; s << d; // tallennetaan d s:ään s >> val; //muunnetaan s:n sisältämä arvo merkkijonoksi //val sisältää arvon 123.456 merkkijonona //tulostetaan normaalisti cout:lla: cout << d << " " << val << endl; system("pause"); return 0; Päinvastainen muunnosta stringistä doubleksi on yhtä yksinkertainen.