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. Poikkeusten käsittelyyn palaamme myöhemmillä ohjelmointikursseilla.

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- tai ofstream-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 arvo true, 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äksi close-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.