Tiedostojen käsittely¶
Vertaillaan tiedostojen käsittelyä Pythonilla ja C++:lla. Seuraavassa on (kummallakin kielellä kirjoitettuna) yksinkertainen ohjelma, joka laskee tiedostossa olevien kokonaislukujen summan. Lukujen pitää olla tiedostossa jokaisen omalla rivillään. Tyhjiä rivejä ei saa olla.
def main():
tiedoston_nimi = input("Syötä tiedoston nimi: ")
try:
tiedosto_olio = open(tiedoston_nimi, "r")
summa = 0
for rivi in tiedosto_olio:
summa += int(rivi)
tiedosto_olio.close()
print("Lukujen summa: ", summa)
except IOError:
print("Virhe tiedoston avaamisessa.")
except ValueError:
print("Virhe tiedoston rivillä")
main()
#include <iostream>
#include <fstream> // Huomaa kirjasto
#include <string>
using namespace std;
int main() {
string tiedoston_nimi = "";
cout << "Syota tiedoston nimi: ";
getline(cin, tiedoston_nimi);
ifstream tiedosto_olio(tiedoston_nimi);
if ( not tiedosto_olio ) {
cout << "Virhe tiedoston avaamisessa." << endl;
} else {
int summa = 0;
string rivi;
while ( getline(tiedosto_olio, rivi) ) {
summa += stoi(rivi);
}
tiedosto_olio.close();
cout << "Lukujen summa: " << summa << endl;
}
}
Itse asiassa ohjelmat eivät toimi täysin samoin, koska C++-koodissa
tiedostosta luettu rivi muutetaan kokonaisluvuksi stoi
-funktiolla,
joka ei huomaa, jos rivin lopussa on jotain ylimääräistä.
Mikäli rivin alussa on jotain kokonaisluvuksi kelpaamatonta, stoi
aiheuttaa poikkeuksen, ja ohjelman suoritus päättyy. C++:n poikkeusten
käsittelyyn palaamme myöhemmin (seuraavalla ohjelmointikurssilla).
Jos C++:ssa halutaan lukea tiedostoja, toimitaan seuraavasti:
- Otetaan ohjelman alussa käyttöön
fstream
-kirjasto, joka sisältää tiedostonkäsittelyssä käytettäviä tietotyyppejä. - Riippuen siitä, halutaanko tiedostoa lukea vai kirjoittaa,
määritellään joko
ifstream
- taiofstream
-tyyppinen olio, jonka rakentajalle annetaan parametrina käsiteltävän tiedoston nimi. - Tiedoston avaamisen epäonnistuminen voidaan tunnistaa käyttämällä
tiedostomuuttujaa
if
-rakenteen ehtona: sille evaluoituu arvotrue
, jos tiedoston avaaminen onnistui,false
muussa tapauksessa. - Kuten Pythonissakin, helpoin tapa lukea tiedostoa on suorittaa lukeminen silmukassa rivi kerrallaan merkkijonomuuttujaan, jota sitten jatkokäsitellään merkkijono-operaatioilla.
- Kun tiedosto on luettu tai kirjoitettu loppuun, on hyvä tapa sulkea
se
close
-metodia kutsumalla. Esim. tiedostoja kirjoitettaessa tämä varmistaa, että kaikki kirjoitettavat tiedot tulevat talletetuiksi kovalevylle. Lisäksiclose
-metodi vapauttaa tiedoston ohjelman käytöstä.
Tiedostomuuttujasta, joka on tyypiltään ifstream
, voidaan lukea tietoa myös
lukuoperaattorin >>
avulla samalla tavalla kuin on totuttu lukemaan
tietovirtamuuttujasta cin
.
Tiedostoon kirjoittaminen (siis kovalevylle tallentaminen) tapahtuu
kohdistamalla ofstream
-tyyppiseen tiedostomuuttujaan
tulostusoperaattori <<
aivan kuten on totuttu tekemään cout
:in kanssa.
Yleisnimitys muuttujalle, jolla voidaan käsitellä (lukea ja kirjoittaa) tietokoneen oheislaitteita, on tietovirta (stream). C++:ssa kaikki oheislaitteiden käsittely on toteutettu tietovirtojen avulla. Lisäksi toteutusmekanismi on niin elegantti, että kaikkia syötevirtoja voidaan käsitellä toistensa kanssa identtisesti. Sama pätee myös tulostusvirroille.
Konkreettisesti tämä tarkoittaa sitä, että sekä cin
:iä että
kaikkia ifstream
-tyyppisiä tietovirtamuuttujia käsitellään
samoilla operaatioilla ja ne käyttäytyvät yhdenmukaisesti. Toisaalta
myös sekä cout
:ia että ofstream
-tyyppisiä virtoja käsitellään
samoin. Tämä on hyvä asia, koska ohjelmoijan ei tarvitse opetella kuin
yksi mekanismi, jota sitten voidaan soveltaa useaan
käyttötarkoitukseen.
Tulostus- ja syöteoperaatioita¶
Lyhyt lista hyödyllisiä operaatioita tietovirtojen käsittelyyn:
cout << tulostettava
tulostusvirta << tulostettava
Tulostusvirtaan saadaan tulostettua tai tallennettua tietoa tulostusoperaattorin
<<
avulla.cin >> tallennusmuuttuja
syötevirta >> tallennusmuuttuja
Syötevirrasta voidaan lukea tunnetun tyyppinen tietoalkio suoraan kyseistä tyyppiä olevaan muuttujaan lukuoperaattorin
>>
avulla.getline(syötevirta, rivi)
getline(syotevirta, rivi, erotinmerkki
)Luetaan syotevirrasta rivillinen tekstiä ja talletetaan se merkkijonomuuttujaan rivi. Jos kutsussa annetaan kolmas parametri -
char
-tyyppinen erotinmerkki, lukemista ja rivi-muuttujaan tallentamista jatketaan, kunnes tietovirrassa tulee vastaan ensimmäinen erotinmerkki.string tekstirivi = ""; // Yksi rivillinen näppäimistöltä getline(cin, tekstirivi); // Seuraavaan kaksoispisteeseen saakka // (saattaa lukea useita rivejä kerralla) getline(tiedosto_olio, tekstirivi, ':');
syötevirta.get(merkki)
Luetaan syötevirrasta yksi merkki (
char
) muuttujaan merkki:// Tiedoston voi lukea läpi merkki kerrallaan: char luettu_merkki; while ( tietosto_olio.get(luettu_merkki) ) { cout << "Merkki: " << luettu_merkki << endl; }
Tietovirtaoperaatioiden onnistumisen tarkastaminen¶
Tietovirtamuuttujaa voidaan C++:ssa käyttää if
-rakenteen ehtona.
Tällöin kääntäjä tulkitsee tietovirtaa bool
-tyyppisenä (suorittaa
implisiittisen tyyppimuunnoksen) siten, että arvona on true
, jos
tietovirta on kunnossa ja false
, jos ei. Esimerkiksi:
ifstream tiedosto_olio("tiedosto.txt");
if (tiedosto_olio) {
// Avaaminen onnistui, tehdään jotain tiedostolle
} else {
// Avaaminen epäonnistui, tietovirta on epäkunnossa
cout << "Virhe! ..." << endl;
}
Kaikkien tietovirtoihin kohdistuvien operaatioiden onnistumista voi
tarkastella C++:ssa kirjoittamalla operaation vaikkapa if
- tai
while
-rakenteen ehdoksi. Esimerkiksi:
while(getline(tiedosto_olio, rivi)){
// Toistorakenteeseen mennään, jos tietovirrasta saatiin luettua rivi
}
// Toistorakenne päättyy, kun rivejä ei enää saada luettua,
// eli kun tiedosto on luettu loppuun
Tarkemmin sanottuna tässä siis tapahtuu seuraavaa.
Aina, kun suoritat
ohjelmakoodissasi jonkin tietovirtaan kohdistuvan operaation, tulee
operaation paluuarvoksi se tietovirta, johon operaatio kohdistui. Jos
operaatio on esim. if
- tai while
-rakenteen ehtona, suorittaa
kääntäjä tietovirralle implisiittisen tyyppimuunnoksen
bool
-tyyppiseksi. Kuten edellä selitettiin, arvoksi tulee true
tai false
riippuen siitä, onko viimeisin kyseiseen tietovirtaan
kohdistunut operaatio on onnistunut.
Tämän opintojakson tehtävät saa yksinkertaisimmin ratkaistua siten,
että kirjoittaa tietovirtoihin kohdistuneet operaatiot aina e.m. rakenteiden
(if
, while
) ehdoksi, jolloin tietovirtaoperaation
onnistuminen tulee testattua.
Knoppitietoa: Tietovirroilla on myös metodi syötevirta.eof()
,
jolla voidaan tarkastaa, epäonnistuiko viimeisin lukuyritys
tietovirrasta sen vuoksi, että tiedosto on luettu loppuun.
// Ensin on yritettävä lukea virrasta jotakin
tiedosto_olio.get(merkki);
// Sen jälkeen voi yrittää tutkia,
// epäonnistuiko edeltävä lukuyritys siitä syystä,
// että luettavaa ei enää ollut jäljellä.
if ( tiedosto_olio.eof() ) {
// Tiedosto luettu loppuun: muuttujassa merkki
// on nyt epämääräinen ja käyttökelvoton arvo.
} else {
// Muuttujassa merkki on tiedostosta
// onnistuneesti luettu char-tyyppinen arvo.
}
Metodia eof
näkee kuitenkin usein käytettävän väärin. Esimerkiksi
seuraavantyyliset ratkaisut ovat lähes poikkeuksetta virheellisiä:
while ( not tiedosto_olio.eof() ) { // VIRHE!
getline(tiedosto_olio, rivi);
...
}
Virhe piilee siinä, että kun tiedoston viimeinen rivi luetaan,
lukuoperaatio onnistuu. Tämä tarkoittaa, että eof
palauttaa arvon
false
. (Jos et ymmärrä, tarkasta edeltä, mitä eof
tekeekään.)
Tämän vuoksi viimeisen rivin lukemisen jälkeen while
-lauseen ehdoksi
tulee true
, ja toistorakenne mennään suorittamaan vielä yhden
kerran, vaikka tiedostossa ei enää ole uutta riviä luettavaksi.
Metodia eof
voi käyttää esimerkiksi, jos haluat tarkastella, onko
tiedostoa käsittelevä toistorakenne päättynyt siksi, että tiedosto
luettiin loppuun, vai jonkin muun virhetilanteen vuoksi. Tämän
opintojakson puitteissa emme kuitenkaan harjoittele
tiedostonkäsittelyyn liittyviä erikoisempia virhetilanteita näin
perusteellisesti.