Cpp IO järjestelmän perusteet
Mureakuha
C++ tyylistä I/O:ta on jo käytetty kurssin alusta lähtien, mutta nyt on aika selvittää, mistä itse asiassa on kysymys. Kuten C++:n edeltäjä C, myös C++ sisältää joustavan ja tehokkaan I/O järjestelmän. C++ tukee edelleen kaikkia C:n I/O-järjestelmän toimintoja. C++:ssa on lisäksi täydellinen joukko oliopohjaisia I/O-rutiineita.
Suurin hyöty C++:n I/O-järjestelmästä tulee siitä, että sitä voidaan kuormittaa omiin luokkiin eli I/O-järjestelmän piiriin voidaan tuoda uusia tyyppejä.
Kuten C:ssä myös C++:ssa konsoli-I/O ja tiedosto-I/O eivät poikkea juurikaan toisistaan. Kyseessä on oikeastaan kaksi näkökulmaa samaan mekanismiin.
Sisällysluettelo |
C++:n I/O perusteita
Kuten C:n I/O-järjestelmä, myös C++:n I/O-järjestelmä toimii 'virtojen' (streams) avulla. Stream on looginen laite, joka joko tuottaa informaatiota tai käyttää informaatiota. Stream on kytketty fyysiseen I/O-laitteeseen C++:n I/O-järjestelmän kautta. Kaikki streamit käyttäytyvät samalla tavalla riippumatta siitä, mihin fyysiseen laitteeseen ne on kytketty. Tästä seuraa, että I/O-järjestelmä toimii periaatteessa minkä tahansa fyysisen laitteen kanssa. Samaa metodia voidaan käyttää tulostettiinpa näytölle, levylle tai printterille.
Kuten tietänet, kun C-ohjelma käynnistyy, avataan automaattisesti 3 etukäteen määriteltyä streamia: stdin, stdout ja stderr. Vastaavalla tavalla C++ ohjelman käynnistyksessä avataan automaattisesti 4 streamia:
| stream | selite | oletuslaite |
|---|---|---|
| cin | standardi syöttövirta | näppäimistö |
| cout | standardi tulostusvirta | näyttö |
| cerr | standardi virhetulostusvirta | näyttö |
| clog | cerr:n puskuroitu versio | näyttö |
Kuten C:ssäkin virrat voidaan ohjata tarvittaessa toisille laitteille, mikäli järjestelmä tukee I/O:n uudelleenohjausta.
C++:n I/O-tuki löytyy header-tiedostosta iostream. Tässä tiedostossa on määriteltynä luokkahierarkia, joka tukee I/O-operaatioita. Alimman tason I/O-luokka on nimeltään streambuf. Luokka sisältää perus-input ja -output operaatiot ja sitä käytetään lähinnä muiden luokkien kantaluokkana. Ellet aio tehdä edistyksellisimpiä I/O-ohjelmia, et tule tarvitsemaan streambuf luokkaa suoraan. Hierarkian seuraavana luokkana on ios. Tässä luokassa on toteutettuna stream-I/O:hon liittyvä formatointi, virheen tarkistus sekä statusinformaatio. ios on kantaluokkana seuraaville luokille: istream, ostream ja iostream. Näiden avulla tapahtuu syöttö, tulostus ja syöttö/tulostus.
ios-luokka sisältää useita jäsenfunktioita ja muuttujia, jotka ohjaavat stream:n perustoimintaa. Kun ohjelmaasi on sisällytetty iostream, ohjelmastasi on pääsy ios-luokkaan.
Formatoitu I/O
Tähän mennessä kaikki tulostus on tapahtunut standardi formaatilla. Kuitenkin tulostusta voidaan formatoida saman tapaisesti kuin C:n printf-funktiossa. Syötteiden muotoja voidaan myös muutella.
Jokaiseen C++:n streamiin liittyy joukko formatointilippuja, jotka määräävät tulostuksen muodon. Liput on koodattu long integer tyyppiseen muuttujaan. Seuraava ios luokassa määritelty muuttuja määrittelee nimivakiot lipuille:
// ios format flags enum { skipws = 0x0001, // skip whitespace on input left = 0x0002, // left-adjust output right = 0x0004, // right-adjust output internal = 0x0008, // padding after sign or base indicator dec = 0x0010, // decimal conversion oct = 0x0020, // octal conversion hex = 0x0040, // hexadecimal conversion showbase = 0x0080, // use base indicator on output showpoint = 0x0100, // force decimal point (floating output) uppercase = 0x0200, // upper-case hex output showpos = 0x0400, // add '+' to positive integers scientific= 0x0800, // use 1.2345E2 floating notation fixed = 0x1000, // use 123.45 floating notation unitbuf = 0x2000, // flush all streams after insertion stdio = 0x4000 // flush stdout, stderr after insertion };
Kun formatointi lippu asetetaan, ominaisuus kytkeytyy päälle. Kun lippu poistetaan, oletusformaatti astuu voimaan. Seuraavassa selitykset lipuille:
skipws Kun lippu on asetettu, syötteen alussa olevat white space merkit ohitetaan (tyhjä, tabulaatio, enter). Kun lippu poistetaan, white space merkkejä ei ohiteta. Käytännössä lippuun tarvitsee koskea vain, kun luetaan tietyn tyyppisiä levytiedostoja.
left, right, internal Kun left-lippu on asetettu, tulostus tasataan vasemmalle. Kun right-lippu on asetettu, tulostus tasataan oikealle. Kun internal-lippu on asetettu, numeerinen arvo tulostetaan siten, että se täyttää koko kentän (kentän leveydestä kohta) sekä etumerkin ja luvun väli täytetään tyhjillä merkeillä. Jos mikään lipuista ei ole asetettuna, tulostus on oletusarvoisesti tasattu oikealle.
dec, oct, hex Oletusarvoisesti kokonaislukujen tulostus tapahtuu desimaalisena. Se voidaan kuitenkin muuttaa oktaaliseksi tai heksadesimaaliseksi. Kun tulostus halutaan palauttaa desimaaliseksi, asetetaan dec-lippu.
showbase Kun lippu asetetaan, numeeristen arvojen kantaluku näytetään. Esimerkiksi heksadesimaalinen luku 1F tulostettaisiin: 0x1F.
showpoint Kun lippu asetetaan, desimaalipiste ja loppunollat näytetään kaikille liukuluvuille. Esimerkiksi liukuluku 5 olisi 5.000000.
uppercase Kun lippu asetetaan, heksadesimaaliesityksen 'x' ja eksponenttilukujen 'e' näytetään isoilla kirjaimilla.
showpos Kun lippu asetetaan, positiivisille desimaaliluvuille näytetään etumerkki (+).
scientific, fixed Kun scientific-lippu asetetaan liukuluvut näytetään tieteellisellä notaatiolla. Kun fixed-lippu asetetaan, luvut näytetään normaalisti. Oletusarvoisesti fixed on asetettu ja luvut näytetään 6 desimaalilla. Kun kumpikaan lipuista ei ole asetettu, kääntäjä valitsee sopivimman.
unitbuf Kun lippu asetetaan, C++ I/O-järjestelmän suorituskyky paranee joillain kääntäjillä.
stdio Kun lippu asetetaan, jokainen stream tyhjennetään (flush) jokaisen tulostuksen jälkeen eli käytännössä tulostus menee fyysiselle laitteelle asti.
Formaattilipun asetukseen käytetään setf()-funktiota. Funktio on ios-luokan jäsenfunktio ja sen yleisimmin käytetty muoto on:
long setf( long flags );
Funktio palauttaa lippujen vanhan arvon ja asettaa argumentissa määritellyt liput (muut liput jätetään koskematta). Esimerkiksi, jos halutaan asettaa showpos-lippu, asia voidaan hoitaa esimerkiksi seuraavasti:
stream.setf( ios::showpos ); // stream on se virta, jota muutetaan
setf() on ios-luokan jäsenfunktio, jolloin se vaikuttaa sellaisiin virtoihin, jotka on luotu ios-luokkaan. Jokainen setf:n kutsu on tehtävä tietyn stream:n suhteen, pelkkää setf-funktiota ei voi mitenkään kutsua.
Yhdellä setf:n kutsulla voidaan asettaa useita lippuja. Esimerkiksi asetetaan cout:n liput showbase ja hex:
cout.setf( ios::showbase | ios::hex );
Koska formaattiliput on määritelty ios-luokassa, lippuihin pääsee käsiksi vain seuraavasti: ios::lippu.
Setf:n komplementti on unsetf. Tällä ios-luokan jäsenfunktiolla nollataan yksi tai useampi lippu. Sen yleisimmin käytetty muoto on:
long unset( long flags );
Funktio palauttaa lippujen vanhan arvon ja poistaa asetuksen argumentissa määritellyiltä lipuilta (muut liput jätetään koskematta).
Kun halutaan ainoastaan tietää tämän hetken lipputilanne, muttei haluta tehdä muutoksia, käytetään ios:n jäsenfunktiota flags, joka palauttaa lippujen arvon:
long flags();
flags-funktiolla on toinenkin muoto, jolla voidaan asettaa liput annettuun arvoon:
long flags( long f );
Argumentin f bittikuvio kopioidaan muuttujaan, joka ylläpitää lippuinformaatiota eli kaikki liput muutetaan annettuihin arvoihin riippumatta vanhasta arvosta. Funktio palauttaa vanhan lipputilanteen.
setf-esimerkki:
#include <iostream> main() { // tulostus oletusarvoilla cout << 123.23 << " hello " << 100 << '\n'; cout << 10 << ' ' << -10 << '\n'; cout << 100.0 << "\n\n"; // muutetaan formaattia cout.setf( ios::hex | ios::scientific ); cout << 123.23 << " hello " << 100 << '\n'; cout.setf( ios::showpos ); cout << 10 << ' ' << -10 << '\n'; cout.setf( ios::showpoint || ios::fixed ); cout << 100.0 << '\n'; return 0; }
Tulostuu:
123.23 hello 100 10 -10 100 1.232300e+02 hello 64 a fffffff6 // showpos vaikuttaa vain desimaaliseen tulostukseen +1.000000e+02
unsetf-esimerkki:
#include <iostream> main() { // muutetaan formaattia cout.setf( ios::hex | ios::uppercase | ios::showbase ); cout << 88 << '\n'; cout.unsetf( ios::uppercase ); cout << 88 << '\n'; return 0; }
Tulostuu:
0X58 0x58
flags-esimerkki. Kiinnitä erityisesti huomiota showflags funktioon, jota voit käyttää hyväksesti jatkossa.
#include <iostream> void showflags(); main() { // oletusarvot showflags(); // muutetaan formaattia cout.setf( ios::oct | ios::fixed | ios::showbase ); showflags(); long f = 0x04A4; cout.flags( f ); showflags(); return 0; } void showflags() { long i, f; int j; char flgs[15][12] = { "skipws", "left", "right", "internal", "dec", "oct", "hex", "showbase", "showpoint", "uppercase", "showpos", "scientific", "fixed", "unitbuf", "stdio" }; f = cout.flags(); // haetaan asetus // tarkistetaan jokainen lippu for (i=1, j=0; i <= 0x4000; i = i << 1, j++) if ( i&f ) cout << flgs[j] << " is on\n"; else cout << flgs[j] << " is off\n"; cout << "\n"; }
Tulostuu:
skipws is on left is off right is off internal is off dec is off oct is off hex is off showbase is off showpoint is off uppercase is off showpos is off scientific is off fixed is off unitbuf is on stdio is off skipws is on left is off right is off internal is off dec is off oct is on hex is off showbase is on showpoint is off uppercase is off showpos is off scientific is off fixed is on unitbuf is on stdio is off skipws is off left is off right is on internal is off dec is off oct is on hex is off showbase is on showpoint is off uppercase is off showpos is on scientific is off fixed is off unitbuf is off stdio is off
width(), precision() ja fill()
ios:ssa on 3 jäsenfunktiota, joilla tulostusta formatoidaan: width(), precision() ja fill().
Oletusarvoisesti tulostettava arvo vie näytöltä tms. vain sen verran tilaa, kuin tarvitaan arvon esittämiseen. Minimikentän leveys voidaan määrittää käyttämällä width()-funktiota:
int width( int w );
Funktion argumenttina on kentän uusi leveys ja funktio palauttaa vanhan leveyden. Jotkut toteutukset vaativat, että kentän leveys on määriteltävä uudelleen ennen jokaista tulostuslausetta, muuten se palautuu takaisin oletusarvoon. Jos tulostus vie vähemmän tilaa kuin width-funktiossa on määritelty, kenttä täytetään täyttömerkillä, joka on oletusarvoisesti tyhjä merkki.
Desimaaliluvusta näytetään oletusarvoisesti 6 desimaalia. Desimaalien lukumäärää voidaan muuttaa precision()-funktiolla:
int precision( int p );
Uusi arvo viedään p:ssä ja vanha arvo palautetaan.
HUOMAA: Borland C++ 4.jotain muuttaa precision funktiolla kaikkien näytettävien numeroiden lukumäärää. Katso esimerkki perässä. Jotta tarkkuus muuttuu oikein on käytettävä lisäksi:
cout.setf( ios::fixed );
Kenttä täytetään täyttö merkillä, joka voidaan määrätä funktiolla:
char fill( char ch );
Kutsun jälkeen uusi täyttö merkki on ch, ja vanha merkki palautetaan.
#include <iostream> main() { cout << 123.234567 << '\n'; cout << "hello" << '\n'; cout.width( 10 ); cout << "hello" << '\n'; cout.fill('%'); cout.width( 10 ); cout << "hello" << '\n'; cout.setf( ios::left ); cout.width( 10 ); cout << "hello" << '\n'; cout.width( 10 ); cout.precision(3); cout << 123.234567 << '\n'; cout.width( 10 ); cout.precision(6); cout << 123.234567 << '\n'; cout.width( 10 ); cout.precision(9); cout << 123.234567 << '\n'; return 0; }
Tulostuu:
Borland: Kirjan mukaan: 123.235 123.234567 hello hello hello hello %%%%%hello %%%%%hello hello%%%%% hello%%%%% 123%%%%%%% 123.234%%% 123.235%%% 123.234567 123.234567 123.234567
Tehdään taulukkomuotoinen tulostus:
#include <iostream> #include <math.h> main() { double x; cout.precision(4); cout.setf( ios::right ); cout.width( 9 ); cout << "x"; cout.setf( ios::right ); cout.width( 9 ); cout << "sqrt(x)"; cout.setf( ios::right ); cout.width( 9 ); cout << "x^2"; cout << "\n\n"; for (x = 2.0; x <= 10.0; x++ ) { cout.width(9); cout << x; cout.width(9); cout << sqrt(x); cout.width(9); cout << x*x << '\n'; } return 0; }
Tulostuu:
x sqrt(x) x^2 2 1.414 4 3 1.732 9 4 2 16 5 2.236 25 6 2.449 36 7 2.646 49 8 2.828 64 9 3 81 10 3.162 100
I/O manipulaattorit
Informaatiota voidaan formatoida toisellakin tavalla kuin edellä esitetyllä. Voidaan käyttää erityisiä funktioita ns. I/O-manipulaattoreita, jotka ovat useimmissa tapauksissa helpompia käyttää kuin ios-formaattiliput ja ios-funktiot.
I/O-manipulaattorit ovat erityisiä I/O-formaatti funktioita, joita käytetään I/O-lausekkeen sisällä eikä erillisinä kuten ios:n jäsenfunktioita. Seuraavassa taulukossa on lueteltu standardimanipulaattorit. Kuten huomannet useat manipulaattorit suorittavat saman asian kuin ios-jäsenfunktiot.
| Manipulaattori | I/O | Tarkoitus |
|---|---|---|
| dec | O | numeerinen data desimaalisena |
| endl | O | rivinsiirtomerkki ja streamin tyhjennys |
| ends | O | nullin tulostus |
| flush | O | streamin tyhjennys |
| hex | O | numeriinen data hexadesimaalisena |
| oct | O | numeerinen data oktaalisena |
| resetiosflags( long f ) | I ja O | f:ssä ilmoitetut liput pois päältä |
| setbase( int base ) | O | kantaluku base:ksi |
| setfill( int ch ) | O | täyttömerkki ch:ksi |
| setiosflags( long f ) | I ja O | f:ssä ilmoitetut liput päälle |
| setprecision( int p ) | O | desimaalipisteen jälkeen tulostettavien numeroiden lukumäärä p:ksi |
| setw( int w ) | O | tulostuskentän leveys w:ksi |
| ws | I | ohitetaan alun whitespace-merkit |
HUOMAA: Borland C++ 4.jotain muuttaa setprecision funktiolla kaikkien näytettävien numeroiden lukumäärää. Katso esimerkki perässä.
#include <iostream> #include <iomanip> #include <math.h> main() { double x; cout << hex << 100 << endl; cout << oct << 10 << endl; cout << setfill('X') << setw(10) << 100 << " hi " << endl; cout << setfill(' '); cout.setf( ios::right ); cout << setprecision(3); cout << setw( 9 ) << "x"; cout << setw( 9 ) << "sqrt(x)"; cout << setw( 9 ) << "x^2" << endl << endl; for (x = 2.0; x <= 10.0; x++ ) { cout << setw(9) << x; cout << setw(9) << sqrt(x); cout << setw(9) << x*x << '\n'; } return 0; }
Tulostuu:
64 12 XXXXXXX144 hi x sqrt(x) x^2 2 1.41 4 3 1.73 9 4 2 16 5 2.24 25 6 2.45 36 7 2.65 49 8 2.83 64 9 3 81 10 3.16 100
Omat insertterit
Kuten aiemmin mainittu, I/O-operaattoreita voidaan kuormittaa omiin luokkiin. Nyt tarkastellaan, miten << tulostus-operaattorin kuormitus tapahtuu.
C++:ssa tulostusoperaatiota kutsutaan insertioksi ja tulostusoperaattoria << kutsutaan insertio-operaattoriksi. Kun <<-operaattoria kuormitetaan, luodaan insertterifunktio eli lyhyesti insertteri.
Kaikilla insertterifunktioilla on yleinen muoto:
ostream &operator<<( ostream &stream, class_name ob ) { // insertterin runko return stream; }
Ensimmäinen parametri on referenssi ostream-tyyppiseen olioon. Eli streamin on oltava tulostus-stream. (ostream on määritelty ios-luokassa). Toinen parametri on tulostettava olio. Insertteri-funktio palauttaa referenssin ostream-tyyppiseen olioon, jolloin << operaatiota voidaan käyttää monimutkaisemmissakin lausekkeissa kuten:
cout << ob1 << ob2 << ob3;
Itse insertteri-funktiossa voidaan suorittaa mikä tahansa toimenpide, kuitenkin hyvän ohjelmointitavan mukaisesti on syytä suorittaa tulostus ostream:iin.
Aluksi saattaa tuntua yllättävältä, että insertteri ei voi olla sen luokan jäsenfunktio, johon se on suunniteltu. Kun mikä tahansa operaattorifunktio on luokan jäsenfunktio, vasemmanpuoleinen operandi, joka välittyy funktioon epäsuorasti this-pointterin avulla, on olio, joka generoi operaattorifunktion kutsun. Vasemman operandin on siis oltava kyseisen luokan olio. Insertterin tapauksessa vasen operandi on stream ja oikean puoleinen operandi on tulostettava olio. Siis insertteri ei voi olla sen luokan jäsenfunktio, johon tulostettava olio kuuluu. Insertteri on kyseisen luokan friend-funktio. (Se voisi olla myös public-funktio, mutta silloin kadotettaisiin olio-ohjelmoinnin perusperiaatteita: kapselointi.)
#include <iostream> class coord { int x, y; public: coord() { x = 0; y = 0; } coord( int i, int j ) { x = i; y = j; } friend ostream &operator<<( ostream &stream, coord ob ); }; ostream &operator<<( ostream &stream, coord ob ) { stream << ob.x << ", " << ob.y << '\n'; return stream; } main() { coord a, b(1,1), c(10,23); cout << a << b << c; return 0; }
Tulostuu:
0, 0 1, 1 10, 23
Insertterit on syytä tehdä niin yleisiksi kuin mahdollista. Edellä tulostetaan coord-luokan olion x- ja y-koordinaatit streamiin, riippumatta siitä, mikä stream itseasiassa on. Usein aloitteleva ohjelmoija toteuttaa edellisen insertterin seuraavasti:
ostream &operator<<( ostream &stream, coord ob ) { // EI NÄIN cout << ob.x << ", " << ob.y << '\n'; return stream; }
Nyt insertteriä ei voi käyttää muille streameille kuin cout:lle.
Seuraavassa esimerkissä ei ole käytetty friendiä, jolloin coord-luokan x ja y on jouduttu tekemään public-tyyppisiksi, jotta insertteri funktio voisi niitä käsitellä. Älä kuitenkaan käytä tätä public tapaa ohjelmissasi yleisesti:
#include <iostream> class coord { public: int x, y; coord() { x = 0; y = 0; } coord( int i, int j ) { x = i; y = j; } }; ostream &operator<<( ostream &stream, coord ob ) { stream << ob.x << ", " << ob.y << '\n'; return stream; } main() { coord a, b(1,1), c(10,23); cout << a << b << c; return 0; }
Seuraavassa esimerkissä tulostetaan kolmioita.
#include <iostream> class triangle { int height, base; public: triangle( int x ) { height = x; base = x; } friend ostream &operator<<( ostream &stream, triangle ob ); }; ostream &operator<<( ostream &stream, triangle ob ) { int i, j, h, k; i = j = ob.base - 1; for ( h = ob.height - 1; h>0; h--) { for ( k = i; k>0; k-- ) stream << ' '; stream << '*'; if ( j!=i ) { for ( k = j - i - 1; k>0; k-- ) stream << ' '; stream << '*'; } i--; stream << '\n'; } for ( k = 0; k < ob.base; k++ ) stream << '*'; stream << '\n'; return stream; } main() { triangle a(5), b(7), c(12); cout << a << endl << b << endl << c; return 0; }
Omat extraktorit
Vastaavasti kuin << tulostusoperaattoria kuormitetaan, voidaan kuormittaa >> syöttöoperaattoria. C++:ssa syöttösoperaattoria >> kutsutaan extraktio-operaattoriksi ja sitä kuormittavaa funktiota kutsutaan extraktoriksi.
Extraktorin yleinen muoto:
istream &operator>>( istream &stream, class-name &ob ) { // extraktorin runko return stream; }
Extraktorit palauttavat referenssin istream-tyyppiseen olioon, eli syöttovirtaan. Ensimmäinen parametri on referenssi syöttövirtaan ja toinen parametri on referenssi olioon, joka saa syötteen. Kuten insertterit, extraktoritkaan eivät voi olla luokan jäsenfunktioita.
Ohjelmassa käytetään coord-luokan extraktoria.
#include <iostream> class coord { int x, y; public: coord() { x = 0; y = 0; } coord( int i, int j ) { x = i; y = j; } friend ostream &operator<<( ostream &stream, coord ob ); friend istream &operator>>( istream &stream, coord &ob ); }; ostream &operator<<( ostream &stream, coord ob ) { stream << ob.x << ", " << ob.y << '\n'; return stream; } istream &operator>>( istream &stream, coord &ob ) { cout << "\nEnter coordinates ( x y ):"; stream >> ob.x >> ob.y; return stream; } main() { coord a, b(1,1), c(10,23); cout << a << b << c; cin >> a; cout << a; return 0; }
Seuraavassa esimerkissä on "varasto-luokka", joka tallettaa tavaran nimen, lukumäärän varastossa ja yksikköhinnan.
#include <iostream> #include <string.h> class inventory { char item[40]; int onhand; double cost; public: inventory ( char *i, int o, double c ){ strcpy( item, i ); onhand = o; cost = c; } friend ostream &operator<<( ostream &stream, inventory ob ); friend istream &operator>>( istream &stream, inventory &ob ); }; ostream &operator<<( ostream &stream, inventory ob ) { stream << ob.item << ": " << ob.onhand; stream << " on hand at $" << ob.cost << '\n'; return stream; } istream &operator>>( istream &stream, inventory &ob ) { cout << "\nEnter item name: "; stream >> ob.item; cout << "Enter Number on hand: "; stream >> ob.onhand; cout << "Enter cost: "; cin >> ob.cost; return stream; } main() { inventory ob("hammer", 4, 12.55); cout << ob; cin >> ob; cout << ob; return 0; }
Kopio lisenssistä (englanniksi) löytyy täältä.
Alkuperäinen (c) Petteri Hämäläinen
