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);

    // Yritetään avata tiedosto
    ifstream tiedosto_olio(tiedoston_nimi);
    if ( not tiedosto_olio ) {
        cout << "Virhe tiedoston avaamisessa." << endl;
        return 0;
    }

    // Tiedoston avaaminen onnistui
    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 ( not tiedosto_olio ) {
    // Avaaminen epäonnistui, tietovirta on epäkunnossa
    cout << "Virhe! ..." << endl;
    return ...;
}

// Avaaminen onnistui, tehdään jotain tiedostolle
...
tiedosto_olio.close();

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.