Ficheiros e validação
Abertura, leitura, escrita, estado dos fluxos e separação entre linhas e registos.
As notas podem ficar num ficheiro de texto para serem lidas noutra execução. Usamos uma linha por registo, com um nome sem espaços e uma nota inteira de 0 a 20.
Admite espaços antes, entre e depois dos campos. A biblioteca extrai os valores. O programa confirma o formato completo e o intervalo.
Abrir, escrever e fechar
std::ifstream abre para leitura e std::ofstream para escrita. Por defeito, ofstream substitui o conteúdo de um ficheiro existente. Usa std::ios::app quando o contrato é acrescentar ao fim. O caminho relativo é resolvido a partir da pasta de trabalho do processo, não da pasta onde está o código.
Programa completo para compilar localmente:
#include <fstream>
#include <iostream>
#include <string>
int main() {
{
std::ofstream saida("notas.txt");
if (!saida) { std::cout << "Erro de abertura\n"; return 0; }
saida << "Ana 15\nRui 12\n";
saida.close();
if (!saida) { std::cout << "Erro de escrita\n"; return 0; }
}
std::ifstream entrada("notas.txt");
if (!entrada) { std::cout << "Erro de leitura\n"; return 0; }
std::string nome;
int nota = 0;
int total = 0;
while (entrada >> nome >> nota) {
std::cout << nome << ": " << nota << "\n";
total += nota;
}
if (entrada.bad()) { std::cout << "Erro de I/O\n"; return 0; }
std::cout << "Total: " << total << "\n";
}
Saída:
Ana: 15
Rui: 12
Total: 27
O programa cria notas.txt na pasta de trabalho e corre localmente, porque o executor do navegador não cria ficheiros; fecha a escrita antes de leres e verifica close(), pois o destrutor fecha mas não trata um erro final de escrita.
A leitura por tokens acima serve um ficheiro conhecido; para dados externos, ler por linhas distingue um registo incompleto do fim normal, validando cada linha em três passos: ler nome e nota, confirmar que não sobra texto e confirmar o intervalo.
Ler sem antecipar o fim
O ciclo while (entrada >> nome >> nota) só entra no corpo depois de ler os dois campos. eof() indica que uma operação anterior encontrou o fim; não prevê se a próxima leitura vai resultar.
Por isso while (!entrada.eof()) pode repetir o último registo ou usar dados incompletos.
Os fluxos conservam estado. fail() sinaliza falha de uma operação ou conversão. bad() sinaliza uma falha grave de I/O. eof() indica encontro do fim. Estes estados podem coexistir.
Depois de uma falha, novas leituras não prosseguem normalmente até limpar o estado com clear() e tratar a entrada problemática. Limpar o estado sem consumir ou corrigir a causa pode criar um ciclo sem progresso.
Separar linhas e registos
A leitura por tokens ignora espaços e fins de linha. Se o formato exige exatamente um registo por linha, lê primeiro a linha e interpreta-a num istringstream. Esta classe permite tratar uma string como um fluxo, sem abrir outro ficheiro.
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::string linha;
int numero = 0;
while (std::getline(std::cin, linha)) {
++numero;
std::istringstream registo(linha);
std::string nome;
std::string extra;
int nota = 0;
bool leu = static_cast<bool>(registo >> nome >> nota);
bool sobra = static_cast<bool>(registo >> extra);
bool intervalo = (nota >= 0 && nota <= 20);
if (!leu || sobra || !intervalo) {
std::cout << "Linha " << numero << " invalida\n";
continue;
}
std::cout << nome << ": " << nota << "\n";
}
}Executa o código para ver a saída e os gráficos.
Saída:
Ana: 15
Linha 2 invalida
Linha 3 invalida
Rui: 20
No exemplo, Ana 15 lê nome e 15, sem sobra e dentro do intervalo: válida; Eva 21 lê nome e 21, sem sobra, mas fora do intervalo: inválida; Leo 12 lixo lê 12 e ainda encontra lixo: inválida; Eva 12abc lê 12 e deixa abc por consumir: inválida. Uma linha vazia falha logo na primeira leitura.
| Entrada | leu | sobra | nota | Decisão |
|---|---|---|---|---|
Ana 15 | sim | não | 15 | válida |
Ana | não | não | 0 | inválida |
Eva 12abc | sim | sim | 12 | inválida |
| vazia | não | não | 0 | inválida |
Cada iteração cria um fluxo novo para a linha, por isso uma falha de conversão não contamina a seguinte. Se o formato permitisse nomes com espaços, teria de usar outra separação, por exemplo um delimitador, e definir como esse delimitador pode aparecer nos campos. Depois de uma falha de conversão no fluxo principal, limpa o estado e consome a entrada problemática antes de continuar:
// Excerto: recupera de uma leitura falhada.
#include <limits>
entrada.clear();
entrada.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
Binário não é memória bruta portátil
std::ios::binary evita transformações de texto próprias da plataforma. Não define por si só um formato de dados. Escrever os bytes de uma struct depende do padding, da ordem dos bytes e da representação dos tipos. Um formato portátil especifica tamanhos, ordem dos bytes e codificação, ou usa uma biblioteca que trate essas decisões.
Caso resolvido: fim depois de um nome
Com a linha Ana, sem nota, getline resulta porque leu uma linha, mas a extração do nome resulta e a da nota falha. Por isso leu é falso e o programa rejeita o registo sem usar o valor anterior de nota.
Num ficheiro vazio, getline falha antes do corpo do ciclo e não existe registo a validar. Os dois casos chegam ao fim dos dados com significados diferentes:
- registo incompleto: existe uma linha, mas falta um campo ou o formato é inválido;
- ausência de registo: não existe linha nenhuma para validar.
Só o primeiro caso produz uma mensagem de linha inválida.
Exercícios
O fim não prevê a leitura
Um ficheiro contém Ana 15 e termina com um fim de linha. Um
ciclo testa !entrada.eof() antes de tentar ler nome e nota.
Explica por que pode executar o corpo mais uma vez e como corrigir.
Pista
Lembra-te de que eof descreve uma tentativa anterior.
Solução
Depois do registo válido, ainda pode faltar a tentativa que deteta o fim.
O ciclo entra outra vez, a extração falha e o corpo pode usar valores
antigos. A condição deve ser
while (entrada >> nome >> nota), para só usar um
par lido com sucesso. Para um formato de uma linha por registo, lê e
valida cada linha.
Confere a tua resposta:
- Distingui encontro do fim de previsão da próxima leitura.
- Expliquei o risco de usar valores antigos.
- Usei a própria leitura como condição.
Escrever a validação em três passos
Escreve bool registo_valido(const std::string& linha) que
aceita só linhas com um nome sem espaços e um inteiro de 0 a 20, sem conteúdo extra.
São permitidos espaços iniciais e finais. Testa Ana 15,
Ana, Eva 12abc, a linha vazia, Rui 20
e Eva 21. Justifica cada rejeição e confirma que um erro numa linha
não impede a leitura da seguinte.
Pista
Trata separadamente campos em falta, conteúdo extra e nota fora do intervalo.
Solução
Ana 15 dá leu verdadeiro, sobra
falso e intervalo verdadeiro: válida. Ana falha em
leu. Eva 12abc tem sobra
verdadeira. A vazia falha em leu. A condição final é
leu && !sobra && intervalo.
Rui 20 é válido porque só sobram espaços; Eva 21
é inválido pelo intervalo. Uma implementação completa é:
#include <sstream>
#include <string>
bool registo_valido(const std::string& linha) {
std::istringstream entrada(linha);
std::string nome, extra;
int nota = 0;
if (!(entrada >> nome >> nota)) return false;
if (entrada >> extra) return false;
return 0 <= nota && nota <= 20;
}Cada chamada constrói outro fluxo, por isso uma falha não contamina a linha seguinte.
Confere a tua resposta:
- Separei ler, detetar sobra e confirmar intervalo.
- Rejeitei Ana sem nota, Eva 12abc e linha vazia.
- Aceitei espaços iniciais e finais, mas rejeitei conteúdo extra e notas fora do intervalo.