2.11.4 Syöttö ja tulostus tekstitiedostoille

Sen jälkeen kun tiedosto on avattu kirjoittamista varten, voidaan sinne tulostaa <<-operattorilla samalla tavoin kuin näytöllekin; cout:in tilalla vain käytetään ko. tiedostolle varattua tunnusta. Tarkastellaan seuraavaa ohjelmanpätkää:

ofstream tulos("tulos.txt");
if (!tulos) {
  cout << "Tiedoston avaaminen ei onnistunut!" << endl;
  return 1;
}
...
int kerroin = 3;
double luku = 12.4;
...
tulos << "Tässäpä mielenkiintoinen tulos:\n";
tulos << kerroin << " kertaa " << luku;
tulos << " on " << kerroin*luku << endl;
...
tulos.close();

Tämän suorittamisen jälkeen tietokoneen levyllä (oletushakemistossa) on tekstitiedosto tulos.txt, jonka sisältö on

Tässäpä mielenkiintoinen tulos:
3 kertaa 12.4 on 37.2

Toisaalta tiedostosta voidaan lukea >>-operaattorilla samoin kuin näppäimistöltä; jälleen vain vaihdetaan cin:in tilalle tiedoston tunnus. Olkoon levyllä tiedosto nimeltä syotto.txt, jonka sisältönä on joukko kokonaislukuja:

3 6 9 12 15 18

Silloin seuraava ohjelmanpätkä lukee näitä lukuja taulukkoon (edellyttäen, että MAX on korkeintaan 6):

ifstream syotto("syotto.txt");
if (!syotto) {
  cout << "Tiedoston avaaminen ei onnistunut!" << endl;
  return 1;
}
...
int kokluvut[MAX], i;
...
for (i=0; i<MAX; i++)
  syotto >> kokluvut[i];
...
syotto.close();

Tietenkään syöttötiedostossa ei saa tällöin olla mitään ylimääräisiä merkkejä, vaan ainoastaan sellaisia, jotka voidaan tulkita kokonaisluvuiksi; muuten lukeminen ei onnistu.

2.11.4.1 Tiedoston loppu

Entä jos edellä tiedostossa syotto.txt on vähemmän kuin MAX kokonaislukua? Silloinhan kohdataan tiedoston loppu ennen kuin taulukko on täynnä. Tällaisia tilanteita varten on olemassa jäsenfunktio eof, jolla voidaan tutkia, onko tiedoston loppumerkki (EOF, end of file) jo ohitettu. Funktion paluuarvona on nollasta eroava arvo (tosi), jos loppumerkki on ohitettu, muuten paluuarvo on nolla (epätosi). Siten edellä for-silmukka kannattaisi korvata seuraavanlaisella rakenteella:

i = 0;
syotto >> luku;
while (i < MAX && !syotto.eof()) { // huomaa negaatio
  kokluvut[i] = luku;
  i++;
  syotto >> luku;
} // muuttujassa i on nyt luettujen lukujen lukumäärä

Tässä siis silmukka päättyy joko siihen, että taulukko tulee täyteen, tai siihen, että luvut loppuvat.

Huomaa, että eof-funktion paluuarvo ei muutu todeksi vielä silloin, kun tiedoston viimeiset tiedot on luettu, vaan vasta sitten, kun on yritetty lukea jotain tiedoston lopun jälkeen! Tästä aiheutuu helposti virhetilanteita, jos ei ole tarkkana.

2.11.4.2 Merkkitiedon lukeminen

Merkkitiedon lukemisessa on aivan samat ongelmat kuin näppäimistöltäkin luettaessa: >>-operaattorilla ei voi lukea välilyöntejä, rivinvaihtoja tai tabulaattorimerkkejä. Esimerkiksi, olkoon levyllä tekstitiedosto, jonka sisältö on:

Tulokset:
    42  Hessu Hopo
    35  Aku Ankka
    23  Mikki Hiiri

Seuraavan ohjelmanpätkän tarkoituksena olisi lukea tämä tiedosto merkki kerrallaan ja tulostaa kaikki luetut merkit näytölle:

char merkki;
...
syotto >> merkki;
while (!syotto.eof()) {
  cout << merkki;
  syotto >> merkki;
}
cout << endl;

Tulostukseksi saadaan kuitenkin:

Tulokset:42HessuHopo35AkuAnkka23MikkiHiiri

Jotta tiedoston sisältö saataisiin luettua kokonaan, täytyy >>-operaattorin sijasta käyttää esimerkiksi jäsenfunktioita get, joka lukee yhden merkin, riippumatta siitä, mikä tuo merkki sattuu olemaan:

syotto.get(merkki);     // vrt. cin.get(merkki);
while (!syotto.eof()) {
  cout << merkki;
  syotto.get(merkki);
}
cout << endl;

Toinen tapa olisi lukea rivi kerrallaan merkkijonoon:

string rivi;
...
getline(syotto, rivi);  // vrt. getline(cin, rivi);
while (!syotto.eof()) {
  cout << rivi << endl;
  getline(syotto, rivi);
}

2.11.4.3 Esimerkkiohjelma

Seuraava ohjelma lukee tiedoston sisällön merkki kerrallaan ja kirjoittaa sen toiseen tiedostoon, kuitenkin siten, että välilyönnit, rivinvaihdot ja tabulaattorimerkit korvataan |-merkillä. Molempien tiedostojen nimet kysytään käyttäjältä. Huomaa, että tiedostoa avattaessa argumenttina annettava merkkijono (tiedoston nimi) täytyy olla C-kielen mukainen merkkijono. Nimien lukemisessa käytetään kuitenkin C++-kielen merkkijonoluokkaa, joten merkkijonot täytyy siten muuntaa jäsenfunktiolla c_str.

/* *********************************************************
MUUTAMER.CPP
  Lukee tekstitiedoston, muuttaa kaikki välilyönnit,
  rivinvaihdot ja tabulaattorit |-merkeiksi, ja kirjoittaa
  tuloksen toiseen tiedostoon.
********************************************************* */

#include <iostream.h>
#include <fstream.h>
#include <string>

using namespace std;

void muunnos(char &merkki)
{
  if (merkki == ' ' || merkki == '\t' || merkki == '\n')
    merkki = '|';
}

int main(void)
{
  string tdsto;
  char merkki;

  cout << "Anna syöttötiedoston nimi   > ";
  getline(cin, tdsto);

  ifstream syotto(tdsto.c_str()); // Huomaa muunnos C-merkkijonoksi!
  if (!syotto) {
    cout << "Tiedoston avaaminen ei onnistunut!" << endl;
    return 1;
  }

  cout << "Anna tulostustiedoston nimi > ";
  getline(cin, tdsto);

  ofstream tulos(tdsto.c_str()); // Huomaa muunnos C-merkkijonoksi!
  if (!tulos) {
    cout << "Tiedoston avaaminen ei onnistunut!" << endl;
    return 1;
  }

  syotto.get(merkki);
  while (!syotto.eof()) {
    muunnos(merkki);
    tulos << merkki;
    syotto.get(merkki);
  }

  syotto.close();
  tulos.close();

  return 0;
}

Esimerkkiajo:

Anna syöttötiedoston nimi   > vanhat.txt
Anna tulostustiedoston nimi > uudet.txt

Jos tiedoston vanhat.txt sisältö on

    42  Hessu Hopo
    35  Aku Ankka
    23  Mikki Hiiri

niin ohjelman suorituksen jälkeen tiedoston uudet.txt sisältö on

|42|Hessu|Hopo||35|Aku|Ankka||23|Mikki|Hiiri|

Kirjoita ohjelma, joka vertaa kahta tekstitiedostoa toisiinsa. Ohjelma kertoo, ovatko tiedostot identtisiä; jos ne eivät ole, kerrotaan sen rivin numero, jolla on ensimmäinen ero.