CodeGym /Cursos /C++ SELF /Estados do stream: good/eof/fail/bad

Estados do stream: good/eof/fail/bad

C++ SELF
Nível 63, Lição 1
Disponível

1. Por que pensar no estado do stream ao ler um arquivo

Quando você começa a ler um arquivo, tudo parece simples: abriu um std::ifstream, fez um loop e processou os dados. Mas na prática a leitura tem sempre alguns finais possíveis: o arquivo pode ter terminado (e isso é normal), os dados podem ter o «formato errado» (e isso já é um erro de entrada), ou pode ter ocorrido um problema sério de I/O (por exemplo, leitura de mídia corrompida). Sem entender o estado do stream você vê apenas o sintoma: “o loop parou”.

Uma ideia importante: um stream não é só “uma mangueira por onde os bytes passam”. É um objeto que, após cada operação, lembra se tudo correu bem. E se você aprender a ler essas “anotações na margem”, vai conseguir escrever código que não finge que está tudo bem quando, na verdade, o arquivo está corrompido, o formato mudou, ou você está lendo algo diferente do esperado.

Simplificando, o objetivo desta aula é: aprender a distinguir três situações — “terminamos a leitura normalmente”, “não foi possível ler porque os dados não batem”, “não foi possível ler por causa de falha de I/O”.

Flags de estado do stream: goodbit/eofbit/failbit/badbit

Os streams têm um conjunto de flags de estado (bits) que são ajustadas após operações de leitura/escrita. Não é preciso decorá-las como feitiços — é melhor imaginar que o stream tem um pequeno painel de instrumentos. Enquanto tudo vai bem — uma luz verde está acesa. Quando chega ao fim do arquivo — a luz “EOF”. Quando o formato não bate — a luz “FAIL”. Quando acontece um problema sério — a luz “BAD”.

Aqui vai uma tabela em linguagem humana (sem tentar “encaixar todo o padrão”):

Estado Método O que isso significa
“tudo ok”
good()
Nenhuma flag “ruim” está definida
“chegou ao fim”
eof()
O stream alcançou o fim dos dados de entrada
“operação falhou”
fail()
Não foi possível realizar a entrada (freq.: formato não compatível)
“muito ruim”
bad()
Erro sério de I/O (não se pode confiar mais no stream)

Há uma sutileza que costuma confundir iniciantes: fail() é um conceito mais amplo que “formato incorreto”. Por exemplo, tentar ler um número além do fim do arquivo também leva a fail(). Ou seja, “EOF” e “FAIL” muitas vezes aparecem juntos — simplesmente porque o EOF se torna conhecido após uma tentativa de leitura malsucedida.

2. Verificação de leitura e loops corretos

if (in) e conversão do stream para bool

Para não verificar todas as luzes sempre, o C++ oferece um atalho conveniente: o stream pode ser testado como um valor booleano. A condição if (in) significa mais ou menos “o stream está num estado válido para leitura agora”, e if (!in) — “algo deu errado”.

É importante entender que a verificação booleana do stream está mais próxima de “não há estado de fail” do que de “tudo perfeito”. Por isso às vezes é útil perguntar explicitamente eof() ou bad() para entender com mais precisão o que aconteceu.

Mini-exemplo: lemos um número e imprimimos se deu certo.

#include <fstream>
#include <iostream>

int main() {
    std::ifstream in("numbers.txt");

    int x = 0;
    if (in >> x) {
        std::cout << "Read x = " << x << '\n';
    } else {
        std::cout << "Cannot read x\n";
    }
}

Repare no estilo: não fazemos in >> x; e depois “lembramos de checar”; em vez disso verificamos o resultado da operação imediatamente. Esse é um dos padrões principais de trabalho com streams: “faça — e cheque na hora”.

Se quiser ver as flags explicitamente (por exemplo, para depuração), pode imprimir as “luzes” no console:

#include <fstream>
#include <iostream>

int main() {
    std::ifstream in("numbers.txt");
    int x = 0;

    in >> x;

    std::cout << "good=" << in.good()
              << " eof=" << in.eof()
              << " fail=" << in.fail()
              << " bad=" << in.bad() << '\n';
}

Isso é útil como “lanterna diagnóstica”. Não é necessário imprimir sempre, mas quando algo para estranhamente — é uma maneira rápida de entender em qual estado está o stream.

Lendo diretamente na condição do loop

O erro mais comum na leitura é construir um loop baseado na “suposição” de que a leitura sempre funcionará, e depois tentar lidar com as consequências. O correto é o contrário: o loop deve repetir-se enquanto a leitura for bem-sucedida. Ou seja, a operação de leitura deve estar na condição do loop.

Se você lê números como tokens usando >>, isso fica assim:

#include <fstream>
#include <iostream>

int main() {
    std::ifstream in("numbers.txt");

    long long sum = 0;
    int x = 0;
    while (in >> x) {
        sum += x;
    }

    std::cout << "sum=" << sum << '\n'; // por exemplo: sum=60
}

Se você lê por linha, o equivalente é:

#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::ifstream in("lines.txt");

    std::string line;
    while (std::getline(in, line)) {
        std::cout << "line: " << line << '\n';
    }
}

Por que é tão importante “ler na condição”? Porque o stream lhe diz a verdade exatamente no momento da tentativa de leitura. Se você primeiro gira o loop e depois lê, pode facilmente acabar com uma iteração extra, processar lixo ou “repetir” um valor antigo.

Por que while (!in.eof()) é uma armadilha

Iniciantes costumam começar a amizade com eof() por um mal-entendido. A ideia é “enquanto não for EOF, leia”. Parece lógico. Mas eof() fica true somente depois de você tentar ler além do fim do arquivo.

Ou seja, o loop while (!in.eof()) quase garante uma iteração a mais: você entra no loop, faz uma leitura malsucedida e então… possivelmente processa dados inexistentes.

Exemplo clássico ruim:

#include <fstream>
#include <iostream>

int main() {
    std::ifstream in("numbers.txt");

    long long sum = 0;
    int x = 0;

    while (!in.eof()) {
        in >> x;
        sum += x; // x pode não ter sido atualizado se a leitura falhou
    }

    std::cout << "sum=" << sum << '\n';
}

Se o último in >> x não conseguiu ler um número (por exemplo, porque o arquivo acabou), a variável x normalmente permanece com o valor anterior. E você vai somar o “valor antigo” novamente. Resultado: um bug que aparece como “por que a soma às vezes está maior em um número?” — exatamente o caso em que o programador começa a culpar fantasmas no laptop.

O loop correto é apenas while (in >> x) ou while (std::getline(...)), como mostramos acima.

4. EOF não é erro: diferenciando fim de arquivo de falha

Quando o loop de leitura termina, resta a pergunta principal: “terminamos porque lemos tudo ou porque algo quebrou?”. É aqui que eof()/fail()/bad() devem ser usados conscientemente.

O padrão típico após leitura por tokens é: “se bad() — é problema de I/O; se fail() e não eof() — é erro de formato”.

#include <fstream>
#include <iostream>

int main() {
    std::ifstream in("numbers.txt");

    int x = 0;
    while (in >> x) {
        // processamento
    }

    if (in.bad()) {
        std::cerr << "I/O error while reading\n";
        return 1;
    }

    if (in.fail() && !in.eof()) {
        std::cerr << "Format error: expected a number\n";
        return 1;
    }

    // aqui chegamos se a leitura terminou normalmente (normalmente por EOF)
}

É importante captar a lógica: EOF costuma ser um término normal da leitura, mas “fail sem EOF” é sinal de que os dados não correspondem ao formato esperado. Por exemplo, você leu um int, mas no arquivo encontrou a palavra "cat".

Para std::getline a situação é mais simples: se std::getline retornou false, pode ser EOF ou problema de leitura. Muitas vezes basta checar if (!in.eof()), para saber se a parada não foi por causa “natural”.

5. O que fail() significa e como recuperar

fail() em exercícios costuma significar: “esperávamos uma coisa, recebemos outra”. Por exemplo, esperamos um número, mas vem uma string. Ou esperamos três campos, mas a linha está vazia. Não é “catástrofe de disco”, é “usuário (ou arquivo) escreveu algo errado”.

Vamos ver um cenário simples. Suponha que o arquivo tenha números, mas de repente aparece uma palavra:

10 20 cat 30

Código:

#include <fstream>
#include <iostream>

int main() {
    std::ifstream in("mixed.txt");

    int x = 0;
    while (in >> x) {
        std::cout << "x=" << x << '\n'; // x=10, x=20
    }

    if (in.fail() && !in.eof()) {
        std::cout << "Stopped: not a number\n"; // Stopped: not a number
    }
}

Ok, detectamos o problema. Dá para “continuar a leitura” após fail()? Às vezes sim, mas isso exige decidir: o que você vai pular para se sincronizar com o stream. Se você simplesmente chamar clear() enquanto o lixo de entrada permanece, pode entrar num loop infinito: tenta ler número → fail(), faz clear(), tenta de novo → fail(), … até o fim dos tempos.

Uma técnica comum de “reparo”: limpar o estado e descartar até o caractere '\n'. Isso funciona bem quando o formato é “uma entrada = uma linha”.

#include <fstream>
#include <iostream>
#include <limits>

int main() {
    std::ifstream in("mixed_lines.txt");

    int x = 0;
    while (true) {
        if (in >> x) {
            std::cout << "x=" << x << '\n';
            continue;
        }

        if (in.eof() || in.bad()) break;

        in.clear();
        in.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    }
}

Isso não é “mágica”, é disciplina: se a leitura falhou, você deve ou parar, ou pular o fragmento problemático para que a próxima tentativa de leitura tenha chance de sucesso.

6. O que bad() significa: quando é melhor parar

Se fail() é muitas vezes “os dados não batem”, então bad() indica que o problema é mais profundo: erro de I/O, corrupção de buffer, algo depois do qual continuar a leitura normalmente faz pouco sentido. Em sistemas reais isso pode ser raro, mas em código de ensino é útil manter a regra: bad() é motivo para parar e reportar o erro de forma clara.

Mentalmente, é útil entender bad() assim: fail() — “não consegui ler o número”, bad() — “não tenho confiança de que posso continuar operando como stream”.

Por isso, em carregadores bem feitos (e em utilitários normais) as checagens costumam ser nessa ordem: primeiro bad(), depois fail(), e só então “talvez seja só EOF”.

7. Esquema: como pensar na leitura de arquivo como algoritmo

Para não manter tudo na cabeça como uma sopa de if-s, é útil representar a leitura como um algoritmo com bifurcações. Isso ajuda especialmente quando você depura “por que parou”.

flowchart TD
    A[Início: stream aberto] --> B{Operação de leitura bem-sucedida?}
    B -->|sim| C[Processar dados]
    C --> B
    B -->|não| D{"bad()?"}
    D -->|sim| E[Erro sério de I/O -> parar]
    D -->|não| F{"fail() && !eof()?"}
    F -->|sim| G[Erro de formato -> reportar/decidir ação]
    F -->|não| H["Término normal da leitura (geralmente EOF)"]

Esse esquema não exige decorar “bits” — exige apenas hábito: após a parada do loop sempre pergunte “por quê?”.

8. Exemplo: TaskBook — carregando tarefas linha a linha

Para que o tema não fique “no vácuo”, vamos continuar com uma aplicação de ensino hipotética TaskBook — uma pequena lista de tarefas em console. Antes tínhamos comandos e trabalho em memória; agora queremos carregar tarefas de um arquivo. O formato será simples: uma linha — uma tarefa:

id;done;title

Por exemplo:

1;0;Buy milk
2;1;Read C++ book (optional)

Começamos pelo modelo:

#include <string>

struct Task {
    int id{};
    bool done{};
    std::string title;
};

Agora faremos um parser bem simples para uma linha (sem exageros). O importante não é “parsear tudo perfeitamente”, mas mostrar a ligação com o estado do stream: a linha é lida via std::getline, e erros de formato são detectados à parte.

#include <sstream>
#include <string>

bool parse_task_line(const std::string& line, Task& out) {
    std::stringstream ss(line);

    int id = 0;
    int done_int = 0;
    if (!(ss >> id)) return false;
    if (ss.get() != ';') return false;
    if (!(ss >> done_int)) return false;

    out.id = id;
    out.done = (done_int != 0);
    out.title = line.substr(line.find_last_of(';') + 1);
    return true;
}

Sim, o parser não é perfeito aqui (por exemplo, não checamos todos os cantos), mas isso é aceitável: o objetivo da aula não é parsing, e sim entender o término da leitura. O parser serve apenas como “carga” para a leitura por linha.

Agora a rotina de carregamento. E aqui aplicamos o principal: lemos no loop while (std::getline(...)), e após o loop distinguimos EOF de problema de leitura.

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

bool load_tasks(const std::string& filename,
                std::vector<Task>& tasks,
                std::string& error) {
    std::ifstream in(filename);
    if (!in) { error = "cannot open file"; return false; }

    std::string line;
    while (std::getline(in, line)) {
        Task t;
        if (!parse_task_line(line, t)) { error = "bad record"; return false; }
        tasks.push_back(t);
    }

    if (!in.eof()) { error = "read error"; return false; }
    return true;
}

Repare no sentido do if (!in.eof()) após o loop. O loop std::getline pode ter terminado porque foi EOF (normal), ou pode ter parado por causa de um erro de I/O (anormal). Essa verificação é a disciplina mínima para código “maduro”.

E um mini-main que mostra como isso pode aparecer no programa:

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<Task> tasks;
    std::string error;

    if (!load_tasks("tasks.txt", tasks, error)) {
        std::cerr << "Load failed: " << error << '\n';
        return 1;
    }

    std::cout << "Loaded tasks: " << tasks.size() << '\n'; // Loaded tasks: 2
}

Mesmo num exemplo tão pequeno fica claro: distinguir as causas da parada de leitura não é “estética”, é uma forma de não se enganar. Porque se std::getline parou por erro e você interpretou isso como EOF, vai acabar com “dados silenciosamente corrompidos”, o que quase sempre é pior que um erro explícito.

9. Erros típicos ao trabalhar com estados de stream

Erro #1: loop while (!eof()).
Parece lógico, mas cria uma iteração extra e provoca processamento de valores “antigos”, porque eof() só é ajustado após tentar ler além do fim do arquivo. É um daqueles bugs que geralmente funciona até que um dia começa a devolver uma soma com um elemento a mais, e você perde meia hora culpando a matemática.

Erro #2: “o loop acabou — logo está tudo bem”.
O iniciante frequentemente assume que o término de while (in >> x) significa final natural. Mas o loop para tanto por EOF quanto por “encontramos uma palavra onde era esperado número” ou por problema de I/O. Se após o loop você não diferencia bad() de fail() && !eof(), o programa aceitará silenciosamente lixo como normal.

Erro #3: após fail() chamam clear(), mas não removem a causa do fail.
Limpar a flag não basta. Se na entrada permanecer o mesmo caractere/token que causa o erro, a próxima leitura dará fail() de novo. Isso pode virar um loop infinito “ler → fail → clear → ler → fail…”. Depois de clear() quase sempre é preciso pular a parte problemática da entrada ou encerrar o processamento.

Erro #4: confundir “erro de dados” com “erro de I/O”.
fail() costuma indicar problema de formato (dados não batem com o esperado), enquanto bad() indica problema de I/O. São classes diferentes de erro. No primeiro caso geralmente você reporta “formato do arquivo incorreto”; no segundo — “erro ao ler o arquivo”. Se você mistura ambos em um único “algo deu errado”, dificulta a depuração para você e para o usuário.

Erro #5: não checar !eof() após o loop de std::getline.
Na leitura por linha é fácil esquecer que std::getline pode parar não só por EOF. Se você não checar !in.eof(), pode interpretar uma entrada truncada como término normal — especialmente ruim se o arquivo vier de uma unidade de rede ou de um ambiente instável.

1
Tarefa
C++ SELF, nível 63, lição 1
Bloqueado
Painel de flags
Painel de flags
1
Tarefa
C++ SELF, nível 63, lição 1
Bloqueado
Soma e motivo
Soma e motivo
1
Tarefa
C++ SELF, nível 63, lição 1
Bloqueado
Linhas para análise
Linhas para análise
1
Tarefa
C++ SELF, nível 63, lição 1
Bloqueado
Ignorando lixo
Ignorando lixo
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION