Notas sobre leitura e escrita em ficheiros

As operações que nos permitem guardar e carregar informação de ficheiros usam as classes ifstream e ofstream.  Uma variável de um destes tipos representa uma ligação entre um ficheiro e o nosso programa, por isso usa-se aqui o termo "canal" para referir instâncias destas classes.

Ao criar um canal é comum (e desejável) indicar imediatamente o nome do ficheiro a que esse canal estará ligado.  Por exemplo, para construir um canal de entrada (que apenas permite operações de extracção) pode-se usar as seguintes instruções:

ifstream canal_de_entrada("um nome de um ficheiro qualquer");

ou

string nome_do_ficheiro = "um nome de um ficheiro qualquer";
ifstream canal_de_entrada(nome_do_ficheiro.c_str());

ou

string nome_do_ficheiro;
cin >> nome_do_ficheiro;
ifstream canal_de_entrada(nome_do_ficheiro.c_str());

ou de modo semelhante, para criar um canal de saída (que apenas permite operações de inserção):

ofstream canal_de_saida("um nome de um ficheiro qualquer");

ou

string nome_do_ficheiro = "um nome de um ficheiro qualquer";
ofstream canal_de_saida(nome_do_ficheiro.c_str());

ou

string nome_do_ficheiro;
cin >> nome_do_ficheiro;
ofstream canal_de_saida(nome_do_ficheiro.c_str());

Ao construir um canal de saída, como foi feito no exemplo acima, se existir um ficheiro com o nome indicado, toda a informação que este contém é eliminada.

Depois de construído um canal de entrada ou saída pode-se verificar se este se encontra num estado de erro usando-o como se fosse uma variável booleana:

if(not canal_de_entrada)
    cerr << "O canal de entrada encontra-se num estado "
         <<
   "de erro não podendo ser feitas operações de leitura."
         <<  endl;

Um canal de entrada de dados pode passar a um estado de erro por várias razões.  As mais comuns são:
  1. O ficheiro que tentou abrir para entrada não existe ou está protegido.  Ou seja, o canal está construído mas não estabelece ligação com qualquer ficheiro.
  2. Falhou uma extracção por se ter tentado ultrapassado o fim do ficheiro.
  3. Falhou uma extracção por os dados que se encontram no ficheiro não serem compatíveis com o tipo de dados que se tentou extrair.
Também é possível que um canal de saída se encontre num estado de erro, embora isso seja menos frequente. Possíveis razões que podem causar esse estado são:
  1. O disco onde é suposto criar esse ficheiro está cheio.
  2. O utilizador não tem permissões para escrita do ficheiro.
Para efectuar uma extracção (ou inserção) pode-se usar um canal de entrada (ou saída) de dados a partir de ficheiros do mesmo modo que se usam os canais usuais cin e cout (que são do tipo istream e ostream respectivamente), por exemplo:

string uma_string_qualquer;
int um_inteiro_qualquer;

/* A extracção feita deste modo ignora espaços, tabuladores e fins-de-linha (espaços em branco).  A extracção de uma cadeia de caracteres termina quando encontra o primeiro espaço em branco depois de ter sido extraído pelo menos um outro caractere qualquer. */
canal_de_entrada >> uma_string_qualquer 
                 >>
   um_inteiro_qualquer;

// Inserção de um inteiro e uma cadeia de caracteres com um espaço entre os dois.
canal_de_saida << um_inteiro_qualquer << ' '
               << uma_string_qualquer << endl;

Num ficheiro, quando se guardam colecções de itens, é conveniente escrever  primeiro o número de itens guardados, de modo a facilitar a leitura, que nesse caso pode ser feita como no exemplo seguinte:

vector<Item> itens;

...

int numero_de_itens;
canal_de_entrada >> numero_de_itens;
for(int i = 0; i != numero_de_itens; ++i)
    itens.push_back(Item(canal_de_entrada));

Para que o código acima seja válido, a classe Item deve possuir um construtor que extrai a informação necessária para construir o item do canal de entrada passado como argumento.

Se se quiser alterar um Item previamente construído através da extracção dos seus dados de um canal, é possível substituir a última linha do exemplo anterior, por canal_de_entrada >> itens[i] ou ainda itens[i].carregaDe(canal_de_entrada).  De igual forma, se podem inserir um item num canal usando canal_de_saida << itens[i] e itens[i].guardaEm(canal_de_saida).  No entanto, é usual reservar os operadores << e >> para inserções e extracções de valores elementares, por exemplo, os dos tipos básicos ou de TAD.  Nesse caso, reservam-se as operações guardaEm() e carregaDe() para efectuar inserções e extracções para classes propriamente ditas, i.e., que representam objectos e não simples valores.

Novamente, os operadores e operações invocados devem estar definidos para a classe Item.  No primeiro caso os operadores istream& operator >> (istream&, Item&) e ostream& operator << (ostream&, Item const&) e no segundo, as operações void Item::carregaDe(istream&) e void Item::guardaEm(ostream&) const.

De seguida apresentam-se alguns exemplos de código típicos da extracção de dados de um canal ligado a um ficheiro.

Exemplo 1:

// Enquanto se conseguir ler...
while(canal_de_entrada >> uma_string_qualquer) {
    ...
}

Exemplo 2:

char c;
/*
O método operação get() extrai um caracter de cada vez, incluindo os espaços em branco (espaços, tabuladores e fins-de-linha).  Enquanto a operação get() for bem sucedida e o caracter lido for diferente de fim-de-linha... */
while(canal_de_entrada.get(c) and c != '\n')
    //
...adiciona esse caracter a uma cadeia de caracteres.
    uma_string_qualquer += c;
//
No final do ciclo a cadeia conterá todos os caracteres da linha (incluindo os espaços).

Exemplo 3:

// Se tiver falhado a leitura...
if(not canal_de_entrada) {
    //
...limpar o estado de erro do canal de entrada ...
    canal_de_entrada.clear();
    char c;

    //
...ler e ignorar todos os caracteres encontrados até ao fim da linha.
    while(canal_de_entrada.get(c) and c != '\n')
       ;
}

Tipicamente, o modo como se insere informação num canal ligado a um ficheiro é diferente do modo como se insere informação num canal ligado ao ecrã.  

Quando se necessita de inserir a informação de uma classe C++ para um canal de saída é possível definir o operador << cujo segundo argumento é uma referência constante para uma variável da classe, (por exemplo: ostream& operator << (ostream&, Item const&)) ou definir uma operação que, dado como argumento um canal aberto insere a informação da classe nesse canal, (por exemplo: void Item::guardaEm(ostream&) const).  Normalmente, como se referiu, a primeira versão usa-se em TAD e a segunda em classes propriamente ditas.

No caso das classes propriamente ditas, o formato a usar para apresentar  informação no ecrã  e o formato usado para escrever informação em ficheiros é, normalmente, diferente.  Esta razão justifica por vezes a coexistência de duas operações para inserção em canal.  A primeira, void Item::guardaEm(ostream&) destina-se a inserir a informação da classes num formato facilmente legível por um programa, nomeadamente através da operação complementar void Item::carregaDe(istream&), embora não forçosamente por humanos.  A segunda, void Item::mostraEm(ostream&) destina-se a inserir a informação da classe num canal que estará ligado, directa ou indirectamente, a um humano, pelo que deverá ser facilmente legível por humanos, embora não forçosamente por um programa.

No caso dos TAD normalmente os formatos de visualização e de armazenamento são equivalentes, pelo que nos basta o operador de inserção num canal << para os dois efeitos.

A extracção de canais pode realizar-se de uma das seguintes maneiras:

É usual usar instâncias de ifstream e ofstream como argumento de operações cujos parâmetros são, respectivamente, istream e ostream.  O tipo ifstream representa apenas canais de entrada de dados associados a ficheiros, mas é compatível com o tipo istream que representa qualquer canal de entrada de dados (ficheiro, teclado, etc.).  Usar istream nos parâmetros dos métodos torna-os mais flexíveis, pois podem lidar quer com istream quer com ifstream.  O mesmo se passa para canais de saída.

Em seguida apresenta-se dois exemplos de utilização de canais de entrada e saída para guardar e recuperar informação de um ficheiro.  Nestes exemplos são  apresentados modos alternativos de se implementar as operações que permitem  guardar e recuperar  informação de uma instância do TAD Ponto2D e da classe Aluno.  

...

class Ponto2D {
  public:

    Ponto2D(double const x = 0.0, double const y = 0.0);

    double x() const;
    double y() const;

  private:
    double x_;
    double y_;
};

...

istream& operator >> (istream& entrada, Ponto2D& ponto)
{
    double x, y;
    char parenteses_esquerdo, separador, parenteses_direito;


    //
Solução um pouco simplista...
    if(entrada >> parenteses_esquerdo
               >> x >> separador >> y
               >> parenteses_direito)
        if(parenteses_esquerdo == '(' and parenteses_direito == ')' and
           separador == ',')
            ponto = Ponto2D(x, y);
        else
            entrada.setstate(ios_base::failbit);
    }

    return entrada;

}

ostream& operator << (ostream& saida, Ponto2D const& ponto)

{
    return saida << '(' << ponto.x() << ',' << ponto.y() << ')';
}

int main()
{
    vector<Ponto2D> pontos;

    //
Leitura de pontos de um ficheiro para um vector:
    ifstream entrada("ficheiro_de_pontos.txt");
    int numero_de_pontos;
    entrada >> numero_de_pontos;

    for(int i = 0; i != numero_de_pontos; ++i) {
        Ponto2D ponto;

        entrada >> ponto;

        pontos.push_back(ponto);
    }

    if(not entrada) {
        cerr << "Leitura falhou!" << endl;
        return 1;
    }

    //
Mostrar lista de pontos no ecrã:
    for(vector<Ponto2D>::size_type i = 0; i != pontos.size(); ++i)
        cout << pontos[i] << endl;

    //
Escrita dos pontos guardados no vector num ficheiro:
    ofstream saida("outro_ficheiro_de_pontos.txt");
    saida << pontos.size() << endl;

    for(vector<Ponto2D>::size_type i = 0; i != pontos.size(); ++i)
        saida << alunos[i] << endl;

}

Exemplo de classe propriamente dita:

...

class Aluno {
  public:

    Aluno(std::string const& nome, std::string const& turma,
          int numero);

    Aluno(istream& entrada);

    ...

    string nome() const;
    string turma() const;
    int numero() const;

    void guardaEm(ostream& saida) const;
    void mostraEm(ostream& saida) const;  

    void carregaDe(istream&);
    void lêDe(istream&);

    static Aluno novoCarregadoDe(istream& entrada);

  private:
    std::string nome_;
    std::string turma_;
    int numero_;

    bool cumpreInvariante() const;
};

inline Aluno::Aluno(istream& entrada)
{
    assert(entrada);

    //
Pode-se melhorar este código garantindo que cada valor é lido de uma 
    // linha diferente (actualmente só lê nomes de uma palavra só...).  Como?
    if(not (entrada >> nome_ >> turma_ >> numero_))
        throw Excepção(); //
Uma excepção para representar erros de carregamento.

    assert(cumpreInvariante());
}

void Aluno::guardaEm(ostream& saida) const
{
    assert(saida);

    if(not( saida << nome() << endl 
                 << turma() << endl
                 << numero() << endl))
        throw Excepção(); //
Uma excepção para representar erros de armazenamento. 
}

void Aluno::mostraEm(ostream& saida) const
{
    saida << "Nome: "  <<  nome() << endl 
          << "Turma: " <<  turma() << endl
          << "Número: " << numero() << endl;
}

void Aluno::carregaDe(istream& entrada)
{
    assert(entrada);

    *this = Aluno(entrada);
}

void Aluno::lêDe(istream& entrada)
{
    //
Fica como exercício...
}

Aluno Aluno::novoCarregadoDe(istream& entrada)
{
    assert(entrada);

    return Aluno(entrada);
}

bool Aluno::cumpreInvariante() const
{
   
...
}

int main()
{
    vector<Aluno> alunos;

    //
Leitura de alunos de um ficheiro para um vector:
    ifstream entrada("ficheiro_de_alunos.txt");
    int numero_de_alunos;
    entrada >> numero_de_alunos;

    for(int i = 0; i != numero_de_alunos; ++i) {
        alunos.push_back(Aluno(entrada));

        //
ou (ainda que neste caso não seja muito boa ideia porque...
        // ...obriga à criação de uma variável aluno com lixo)...
        // Aluno novo_aluno("", "", 0);
        // novo_aluno.carregaDe(entrada);
        // alunos.push_back(novo_aluno);

        // ou ainda ...
        // alunos.push_back(Aluno::novoCarregadoDe(entrada));
    }

    // Mostrar lista de alunos no ecrã:
    for(vector<Aluno>::size_type i = 0; i != alunos.size(); ++i)
        alunos[i].mostraEm(cout);

    //
Escrita dos alunos guardados no vector num ficheiro:
    ofstream saida("outro_ficheiro_de_alunos.txt");
    saida << alunos.size() << endl;

    for(vector<Aluno>::size_type i = 0; i != alunos.size(); ++i)
        alunos[i].guardaEm(saida);

}