Pular para conteúdo

7. Lendo e escrevendo dados

Sobre os exercícios desta lista

Nem todo compilador online tem um sistema de arquivos de verdade por trás. Por isso, a Lista 07 testa a lógica de leitura/escrita usando std::istringstream/std::ostringstream (que simulam um arquivo em memória) em vez de abrir arquivos de disco. O código com std::ifstream/std::ofstream deste capítulo funciona igual no seu computador — é só uma troca da "fonte" dos dados.

Escrevendo em arquivo: std::ofstream

Em C, você usava FILE*, fopen, fprintf, fclose:

FILE* f = fopen("dados.txt", "w");
fprintf(f, "%s,%d\n", "Ana", 25);
fclose(f);

Em C++, std::ofstream ("output file stream") funciona com os mesmos operadores << que você já usa com std::cout — porque std::cout também é um "stream", só que a saída dele é o terminal em vez de um arquivo:

#include <fstream>

std::ofstream arquivo("dados.txt");
if (arquivo) {                       // true se abriu com sucesso
    arquivo << "Ana" << "," << 25 << "\n";
    arquivo << "Bruno" << "," << 30 << "\n";
}
// o arquivo fecha sozinho quando "arquivo" sai de escopo (mais sobre isso na Unidade 5 -- RAII)

Repare: não existe fclose explícito. O destrutor de std::ofstream fecha o arquivo automaticamente quando a variável deixa de existir — você vai entender exatamente por que isso funciona no capítulo 10 (RAII), mas já pode confiar nesse comportamento agora.

Lendo de arquivo: std::ifstream

#include <fstream>
#include <string>

std::ifstream arquivo("dados.txt");
if (arquivo) {
    std::string linha;
    while (std::getline(arquivo, linha)) {
        std::cout << linha << "\n";
    }
}

std::getline(stream, variavel) lê uma linha inteira (até o \n) e a coloca em variavel. O laço while (std::getline(...)) é o padrão idiomático para "leia linha por linha até acabar o arquivo" — a condição do while é verdadeira enquanto a leitura tiver sucesso, e falsa quando chega ao fim do arquivo.

Lendo dados formatados (CSV simples)

Um arquivo .csv é texto com valores separados por vírgula. A ferramenta certa para separar uma linha em campos é std::istringstream — um "stream" que lê de uma std::string em vez de um arquivo:

#include <sstream>
#include <string>

std::string linha = "Ana,25";
std::istringstream stream(linha);

std::string nome;
std::getline(stream, nome, ',');   // le ate a virgula (terceiro argumento = delimitador)

std::string idade_texto;
std::getline(stream, idade_texto); // le o resto da linha
int idade = std::stoi(idade_texto); // converte texto para int

std::stoi ("string to int") converte uma std::string numérica para int; existem equivalentes std::stod (para double), std::stol (para long), etc.

Juntando leitura de arquivo com parsing de CSV:

#include <fstream>
#include <sstream>
#include <vector>
#include <string>

struct Pessoa {
    std::string nome;
    int idade;
};

std::vector<Pessoa> le_pessoas(const std::string& caminho) {
    std::vector<Pessoa> pessoas;
    std::ifstream arquivo(caminho);
    std::string linha;

    while (std::getline(arquivo, linha)) {
        std::istringstream stream(linha);
        std::string nome, idade_texto;
        std::getline(stream, nome, ',');
        std::getline(stream, idade_texto);
        pessoas.push_back({nome, std::stoi(idade_texto)});
    }
    return pessoas;
}

Por que isso importa: separar a lógica do "de onde vêm os dados"

Repare que le_pessoas recebe um caminho de arquivo — isso significa que ela só pode ser testada abrindo um arquivo de verdade no disco, o que nem sempre é possível (compilador online, ambiente de teste automatizado, etc.).

A solução idiomática é separar em duas partes: uma função que faz o parsing de um texto já em memória (fácil de testar, porque não depende de disco), e outra, bem mais fina, que só abre o arquivo e repassa o conteúdo:

// Parte testavel: recebe o CONTEUDO como string, nao o caminho do arquivo.
std::vector<Pessoa> parseia_pessoas(const std::string& conteudo) {
    std::vector<Pessoa> pessoas;
    std::istringstream stream(conteudo);   // simula um arquivo em memoria
    std::string linha;

    while (std::getline(stream, linha)) {
        std::istringstream linha_stream(linha);
        std::string nome, idade_texto;
        std::getline(linha_stream, nome, ',');
        std::getline(linha_stream, idade_texto);
        pessoas.push_back({nome, std::stoi(idade_texto)});
    }
    return pessoas;
}

// Parte fina, nao testada automaticamente: so conecta o arquivo real a logica acima.
std::vector<Pessoa> le_pessoas_de_arquivo(const std::string& caminho) {
    std::ifstream arquivo(caminho);
    std::string conteudo((std::istreambuf_iterator<char>(arquivo)),
                           std::istreambuf_iterator<char>());
    return parseia_pessoas(conteudo);
}

Essa separação — lógica pura de um lado, "cano" de entrada/saída do outro — é exatamente o mesmo princípio que vai reaparecer com força total na Unidade 7, quando o "cano" for um sensor de hardware em vez de um arquivo. Vale a pena internalizar aqui.

Erros comuns

Esquecer de checar se o arquivo abriu — std::ifstream arquivo("nao_existe.txt") não lança erro nem trava o programa; arquivo simplesmente fica num estado de falha. Sempre confira com if (arquivo) antes de usar.

"terminate called after throwing an instance of ‘std::invalid_argument’" — std::stoi("abc") (texto que não é número) lança exceção. Se o dado pode vir corrompido, seria necessário tratar isso com try/catch (Unidade 5).

Misturar std::cin >> x com std::getline e ler uma linha vazia — o mesmo problema do capítulo 1: >> deixa o \n no buffer. Não é exclusivo de arquivo, mas aparece com frequência ao processar entrada linha por linha.

Linha com \r no final (arquivo salvo no Windows) atrapalhando o parsing — se o último campo de cada linha vier com um caractere estranho no fim, pode ser um \r sobrando (fim de linha do Windows). Fora do escopo deste capítulo, mas é um problema real que você pode encontrar ao processar arquivos de origens diferentes.

Resumo

  • std::ofstream/std::ifstream funcionam com <</>> e std::getline, igual std::cout/std::cin — porque todos são "streams".
  • O arquivo fecha sozinho quando a variável stream sai de escopo (RAII — capítulo 10).
  • std::istringstream lê de uma std::string como se fosse um arquivo — é a ferramenta para fazer parsing de uma linha (ex.: CSV) e também para testar lógica de parsing sem precisar de arquivo real.
  • std::stoi/std::stod convertem texto para número.
  • Separe a lógica de parsing (recebe conteúdo como string, testável) da leitura de arquivo em si (fina, não testada automaticamente) — o mesmo padrão vai reaparecer com hardware na Unidade 7.