Linguagem C - Gravação de arquivos e sua leitura

Pessoal veja o problema que estou enfrentando. O Hugo e o TerraSkill e o AnsiC me deram uma grande ajuda e consegui gravar um arquivo de dados para cálculo de índice de massa corpórea. Mas ainda falta dois detalhes:

Primeiro: Preciso gravar variáveis tipo int formatadas com 3 dígitos (qual a máscara do fprintf?). Preciso gravar variáveis tipo double com seis dígitos e três deles são decimais, além do ponto de divisão entre inteiro e decimal (tem alguma mascará do fprintf?)

Segundo: Preciso ler esses dados corretamente de acordo com a formatação acima (tem máscara para o fscanf?). Além disso, os dados numéricos precisam ser numéricos e não string, pois vou fazer contas com eles.

Vejam o que já estou fazendo (abaixo segue o programa e o arquivo gerado por ele).

Atenciosamente,
Ronaldo

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct registro {
	char nome[80];
	int idade;
	double peso;
	double altura;
};

main() {
	
	struct registro pessoa[10];

	printf("Dados para o arquivo: \n");
	int c;
	for(int i = 0; i < 4; i++) {
		while ( (c = getchar()) != '\n' && c != EOF ) { }
		printf("\n\n Nome: ");
		scanf("%79[^\n]", &pessoa[i].nome);
		while ( (c = getchar()) != '\n' && c != EOF ) { }
		printf("\n Idade: ");
		scanf("%d", &pessoa[i].idade);
		while ( (c = getchar()) != '\n' && c != EOF ) { }
		printf("\n Peso: ");
		scanf("%lf", &pessoa[i].peso);
		while ( (c = getchar()) != '\n' && c != EOF ) { }
		printf("\n Altura: ");
		scanf("%lf", &pessoa[i].altura);
	}
	
	FILE *ponteiro;
	ponteiro = fopen("imc.txt", "a");
	if(!ponteiro) {
		printf("\nErro ao abrir arquivo!\n");
		fclose(ponteiro);
		exit(0);
	}
	else
		printf("\nArquivo aberto com sucesso\n");
	for(int i = 0; i < 4; i++) {
		fprintf(ponteiro, "%-80s\n", pessoa[i].nome);
		fprintf(ponteiro, "%d", pessoa[i].idade);
		fprintf(ponteiro, "%lf", pessoa[i].peso);
		fprintf(ponteiro, "%lf",pessoa[i].altura);
	}
	fclose(ponteiro);
	return(0);
	
}

Veja o arquivo gerado pelo programa:

Ronaldo Rodrigues Godoi Filho do Neto do Pai                                    
4882.5000001.700000aaaaa bbbbb ccccc ddddd                                                         
4990.0000001.600000b                                                                               
212.0000000.800000L asdjlkfj alksdjf lkas djflkajsdlkfjalksd fjl                                  
3558.0000001.550000

Os dados estão certos, não tem problema os números terem seis casas decimais, mas note que não há como diferenciar cada campo. Eu tenho que saber o tamanho do campo quando gravo e quando leio.

Tem…, contudo apenas para saída. Já entrada de número acontece enquanto se segue os dígitos.

Por exemplo
Esse arquivo bem formato tem o seguinte aspecto:

NOME
IDADE PESO ALTURA
NOME
IDADE PESO ALTURA

Note que existem espaços para separar as informações.

Exemplo

#include"stdio.h"
struct registro {
     char nome[80];
     int idade;
     double peso, altura;
};
int
main (void) {
  struct registro pessoa = { "PESSOA TESTE", 10, 45.0,1.54 };
  printf ("%s\n%.3d %.3f %.3f\n", 
         pessoa.nome,
         pessoa.idade,pessoa.peso,pessoa.altura);
  return 0;
}
  • Para ler: a primeira ‘scanf’ pega “strings” com ‘scanset’ (" %[^\n]");
    segunda scanf pega idade peso e altura numa chamada (" %d%lf%lf")

Entendeu???

Isso aqui:

scanf("%79[^\n]", &pessoa[i].nome);

Essa linha dá um warning ao compilar, porque nome é um array de char (que “decai” pra um ponteiro ao ser passado para uma função, então na prática é como se fosse um ponteiro de char - ou seja, um char *). Mas ao usar & você está passando o endereço deste ponteiro (ou seja, um char **).

Talvez “funcione” por coincidência (o que é uma das características mais cruéis do C, pois isso mascara muitos erros), mas o correto é simplesmente passar o ponteiro diretamente, ou seja, sem o &:

scanf("%79[^\n]", pessoa[i].nome);

E em vez de usar valores fixos, você pode definir o tamanho máximo com define, deixando o programa mais flexível.

E por que criou um array com 10 posições (struct registro pessoa[10]) mas no for só vai até 4? Prefira criar somente com a quantidade desejada:

// tamanho máximo para o nome
#define MAX_SIZE_NOME 80

// pequeno truque para o scanf usar o tamanho acima
// ver como isso funciona em https://stackoverflow.com/a/25410835
#define S_(x) #x
#define S(x) S_(x)

struct registro {
    char nome[MAX_SIZE_NOME + 1]; // inclua 1 espaço para o terminador de string
    int idade;
    double peso;
    double altura;
};

int main() {
    int quantidadeRegistros = 4; // quantidade de registros no array
    struct registro pessoa[quantidadeRegistros]; // use a quantidade aqui
    printf("Dados para o arquivo: \n");
    int c;
    for (int i = 0; i < quantidadeRegistros; i++) { // use a quantidade aqui
        printf("Nome: ");
        scanf("%" S(MAX_SIZE_NOME) "[^\n]", pessoa[i].nome); // use o tamanho máximo aqui
        // etc (o resto é igual)
    }

Já o uso de %-80s para escrever, não sei se faz muito sentido, pois isso escreve o nome alinhado à esquerda, e completa com espaços à direita, até dar 80 caracteres. Ou seja, se o nome for “Fulano de Tal”, ele irá escrever uma linha com esse nome e mais 67 espaços em branco. Será que precisa disso?

Bom, a sugestão acima de usar espaços para separar os campos é uma boa. Deixando o nome em uma linha e o restante em outra (não sei porque fez assim, mas enfim):

for (int i = 0; i < quantidadeRegistros; i++) {
    fprintf(ponteiro, "%s\n%d %.3f %.3f\n", pessoa[i].nome, pessoa[i].idade, pessoa[i].peso, pessoa[i].altura);
}

E em vez de repetir o while que limpa o buffer, você pode criar uma função para isso. Ex:

void clearBuffer(FILE *fp) {
    int c;
    while ((c = fgetc(fp)) != EOF && c != '\n');
}

Aí no loop bastaria chamá-la, em vez de fazer o while toda hora:

clearBuffer(stdin);
1 curtida

Agora está dando tudo certo na gravação: o nome está isolado em uma linha e tem 80 caracteres, os outros campos estão separados por um espaço na linha abaixo do nome.

Meu problema agora é ler os dados. O programa não compila, quais os parâmetros devo usar para ler os dados veja o que usei:

       for(int i = 0; i < 4; i++) {
			fprintf(ponteiro, "%-80s\n", pessoa[i].nome);
			fprintf(ponteiro, " %d", pessoa[i].idade);
			fprintf(ponteiro, " %.3f", pessoa[i].peso);
			fprintf(ponteiro, " %.3f\n",pessoa[i].altura);
		}

Se vocês quiserem eu mando o programa inteiro, mas o trecho que está com erro é este, ele não compila. Como faço para ler os dados do arquivo?

Um abraço,
Ronaldo

Veja o arquivo que eu devo ler:

Ronaldo Rodrigues Godoi                                                         
 48 82.500 1.700
Stavros Godoi de Oliveira                                                       
 21 74.000 1.750
Patricia Godoi Alves                                                            
 40 59.580 1.580
Adriana Godoi de Oliveira                                                       
 41 60.000 1.580

Como eu disse antes, não sei se faz sentido imprimir desta maneira. Para que escrever o nome e depois vários espaços em branco, só pra completar os 80 caracteres? Eu acho desnecessário, pois na hora de ler você teria que remover esses espaços (a menos que eles sejam realmente necessários depois, mas enfim).

Aqui você está começando outra linha, então acho desnecessário este espaço antes (pra que começar a linha com espaço?) - eu removeria.

Outro detalhe é que você pode imprimir tudo de uma vez (como já mostrado acima):

for (int i = 0; i < quantidadeRegistros; i++) {
    fprintf(ponteiro, "%s\n%d %.3f %.3f\n",
            pessoa[i].nome, pessoa[i].idade, pessoa[i].peso, pessoa[i].altura);
}

Repare que removi o -80, assim ele só imprime o nome e pronto. Nada de espaços a mais pra completar os 80 caracteres, não vejo sentido em fazer isso.


Enfim, para ler o arquivo, basta ler 2 linhas de cada vez: primeiro você lê o nome, e depois lê a outra linha com os números.

Como já dito anteriormente (e eu sugiro que releia tudo com atenção), não é uma boa misturar scanf e fgets. Para ler linha a linha, prefira o segundo:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_SIZE_NOME 80

struct registro {
    char nome[MAX_SIZE_NOME + 1]; // inclua 1 espaço para o terminador de string
    int idade;
    double peso;
    double altura;
};

int main() {
    struct registro pessoas[10];
    FILE *arq = fopen("dados.txt", "r");
    if (! arq) {
        printf("Erro ao abrir arquivo\n");
        return 1;
    }

    char buffer[MAX_SIZE_NOME + 1];
    int registrosLidos = 0;
    while (1) {
        // lê a linha com o nome
        if (! fgets(buffer, MAX_SIZE_NOME, arq)) {
            // se não conseguiu ler mais, sai do while
            break;
        }
        // remove a quebra de linha do final
        buffer[strcspn(buffer, "\n")] = '\0';
        // copia para o nome
        strcpy(pessoas[registrosLidos].nome, buffer);

        // agora lê os demais dados
        if (! fgets(buffer, MAX_SIZE_NOME, arq)) {
            // se não conseguiu ler mais, sai do while
            break;
        }
        // consegui ler a linha toda, agora vou ler os números dela
        if (sscanf(buffer, "%d %lf %lf", &pessoas[registrosLidos].idade, &pessoas[registrosLidos].peso, &pessoas[registrosLidos].altura) != 3) {
            // se fscanf não retornou 3, é porque não conseguiu ler os 3 números
            break;
        }
        registrosLidos++;
    }

    // mostra o que foi lido
    printf("lidos:\n");
    for (int i = 0; i < registrosLidos; i++) {
        printf("Nome: %s\nidade: %d\nPeso: %.3f\nAltura: %.3f\n\n",
               pessoas[i].nome, pessoas[i].idade, pessoas[i].peso, pessoas[i].altura);
    }

    return 0;
}

Repare que para os números, primeiro eu leio a linha inteira com fgets - assim eu garanto que tudo vai ficar na string buffer. Depois eu uso sscanf (repare que tem um “s” a mais no início), que é similar a scanf, mas em vez de ler os dados do teclado, ele lê de uma string (no caso, eu passo a string buffer). Isso evita aqueles problemas que já citei antes, de misturar fgets com scanf e um nem sempre ler as quebras de linha, etc.

sscanf retorna a quantidade de itens que foram lidos, assim eu posso conferir se realmente os três números foram lidos.

Se qualquer linha der problema (ou se chegar no final do arquivo), eu interrompo o loop. E no fim eu mostro o que foi lido (foi só pra conferir se a leitura estava certa).


Se você quiser muito insistir nessa ideia de colocar o nome mais os espaços, eu sugiro que aumente o tamanho do buffer para 82 (pois são os 80 caracteres da linha, mais a quebra de linha, mais o terminador de string).

Por algum motivo o fgets está dando false e a leitura está com problema. O meu comando SEEK_SET está correto? Qual será o problema do programa? Vou te mandar o código, muitas das coisas eu fiz igual você me mandou, quase não tem diferença.

Obrigado,
Ronaldo

Programa:

/*

   Programa de manipulação de arquivo
   Ronaldo Rodrigues Godoi
   
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct registro {
	char nome[80+1];
	int idade;
	double peso;
	double altura;
};

main() {
	
	struct registro pessoas[4];
	
	printf("\n\nCalculo do IMC");
	char entrada;
	do {
		printf("\n\n Faz entrada de dados? (S/N)");
		scanf("%c", &entrada);
	} while(entrada != 'S' && entrada != 's' && entrada != 'N' && entrada != 'n');
	
	if(entrada == 's' || entrada == 'S') {
		printf("Dados para o arquivo: \n");
		int c;
		for(int i = 0; i < 4; i++) {
			while ( (c = getchar()) != '\n' && c != EOF ) { }
			printf("\n\n Nome: ");
			scanf("%80[^\n]", &pessoas[i].nome);
			while ( (c = getchar()) != '\n' && c != EOF ) { }
			printf("\n Idade: ");
			scanf("%d", &pessoas[i].idade);
			while ( (c = getchar()) != '\n' && c != EOF ) { }
			printf("\n Peso: ");
			scanf("%lf", &pessoas[i].peso);
			while ( (c = getchar()) != '\n' && c != EOF ) { }
			printf("\n Altura: ");
			scanf("%lf", &pessoas[i].altura);
		}
	}
	
	FILE *ponteiro;
	ponteiro = fopen("imc.txt", "a");
	if(!ponteiro) {
		printf("\nErro ao abrir arquivo!\n");
		fclose(ponteiro);
		exit(0);
	}
	else
		printf("\nArquivo aberto com sucesso\n");
		
	if(entrada == 's' || entrada == 'S') {
		for(int i = 0; i < 4; i++) {
			fprintf(ponteiro, "%s\n %d %.3f %.3f\n", pessoas[i].nome, pessoas[i].idade, pessoas[i].peso, pessoas[i].altura);
		}
	}
	
	int registrosLidos;
	char buffer[81];
	fseek(ponteiro, 0, SEEK_SET);
	while (1) {
		
		printf("fazendo a primeira leitura...0");
		
		if(! fgets(buffer, 80, ponteiro)) {
			printf("Não consegui ler o nome...2");
			break;
		}
		
		buffer[strcspn(buffer, "\n")] = '\0';
		strcpy(pessoas[registrosLidos].nome,buffer);
		
		printf("Fazendo a segunda leitura...1");
		
		if(! fgets(buffer, 80, ponteiro)) {
			printf("Não consegui ler os numeros...3");
			break;
		}
		
		if(sscanf(buffer, "%d %lf %lf", 	&pessoas[registrosLidos].idade,
									 	&pessoas[registrosLidos].peso,
									 	&pessoas[registrosLidos].altura) !=3) {
	 		break;
											 }
											 
		
		
		printf("\n\nNome: %-80s\n", pessoas[registrosLidos].nome);
		printf("\nIdade: %d", pessoas[registrosLidos].idade);
		printf("\nPeso: %.3f", pessoas[registrosLidos].peso);
		printf("\nAltura: %.3f", pessoas[registrosLidos].altura);
	}
	
	fclose(ponteiro);
	return(0);
	
}

Bom, vamos ver:

Eu já falei antes que para strings não deve ter o &. Na verdade strings são arrays de char, que ao serem passadas para uma função, decaem para um ponteiro (ou seja, ali o nome já é um ponteiro para char, e portanto não precisa do &).

Também falei pra criar uma função pra limpar o buffer em vez de repetir o while toda hora:

E o que vale para o fprintf (imprimir tudo de uma vez), também vale para o printf:

printf("\nNome: %-80s\nIdade: %d\nPeso: %.3f\n Altura: %.3f\n",
       pessoas[registrosLidos].nome, pessoas[registrosLidos].idade,
       pessoas[registrosLidos].peso, pessoas[registrosLidos].altura);

E insisto que o -80 é desnecessário. Você imprime vários espaços depois do nome, e depois disso muda a linha. Pra que?

Também falei que não precisa do espaço no começo da linha:

Ou seja, em vez de ter esse espaço:

fprintf(ponteiro, "%s\n %d %.3f %.3f\n"
                       ^
                       Pra que esse espaço?

Pode ser assim:

fprintf(ponteiro, "%s\n%d %.3f %.3f\n"
                       ^
                       Não tem mais o espaço

Assim não fica com espaço no começo da linha, que eu acho desnecessário.

Ah, em vez disso:

FILE *ponteiro;
ponteiro = fopen("imc.txt", "w");

Pode fazer assim:

FILE *ponteiro = fopen("imc.txt", "w");

Não tem vantagem nenhuma declarar a variável em uma linha e atribuir o valor na linha seguinte. Faça tudo de uma vez e pronto.


Bom, o problema é que você não inicializou a variável registrosLidos, então quando você tenta usar ela no loop, ela terá um valor indeterminado. Faça assim:

int registrosLidos = 0;

E o principal problema é que você abriu o arquivo para escrita, mas depois você tenta ler do mesmo. Em vez de fseek, você pode fechar o arquivo depois que terminou de escrever, e em seguida pode abrir de novo para leitura (e aí não precisa do fseek):

// escreve no arquivo
for(int i = 0; i < 4; i++) {
    fprintf(ponteiro, "%s\n%d %.3f %.3f\n", pessoas[i].nome, pessoas[i].idade, pessoas[i].peso, pessoas[i].altura);
}
// fecha
fclose(ponteiro);

// abre de novo, mas para leitura
ponteiro = fopen("imc.txt", "r");
int registrosLidos = 0;
// ***ATENÇÃO*** Não precisa mais do fseek
// etc...

Ou você pode abrir o arquivo em modo r+ (pode ler e escrever). Mas como você está escrevendo no modo a (ou seja, append - adiciona dados no final do arquivo), então primeiro você teria que ir para o final do arquivo e só depois começar a escrever:

// abre o arquivo no modo r+ (leitura e escrita, não apaga o conteúdo do arquivo)
FILE *ponteiro = fopen("imc.txt", "r+");
if(!ponteiro) {
    printf("\nErro ao abrir arquivo!\n");
    fclose(ponteiro);
    exit(0);
} else printf("\nArquivo aberto com sucesso\n");
// move para o final do arquivo
fseek(ponteiro, 0, SEEK_END);
// escreve os dados
for(int i = 0; i < 4; i++) {
    fprintf(ponteiro, "%s\n%d %.3f %.3f\n", pessoas[i].nome, pessoas[i].idade, pessoas[i].peso, pessoas[i].altura);
}

// volta para o início
fseek(ponteiro, 0, SEEK_SET);
// faz a leitura
int registrosLidos = 0;
// etc...

Mas eu pessoalmente prefiro não misturar leitura e escrita, nem de ficar indo e voltando com fseek. O risco de cometer algum erro e estragar o conteúdo do arquivo aumenta bastante. Eu prefiro a primeira abordagem: depois que escreveu, fecha, e abre de novo para leitura.

1 curtida

Tudo jóia pessoal?

O programa rodou graças as mudanças sugeridas mas deu um errinho quando eu não declarei FILE *ponteiro e já fiz a atribuição de valor, daí deixei só esta parte desse jeito.

Muito obrigado Hugo Kotsubo. Desculpe a teimosia anterior. Veja abaixo o resultado que eu queria no programa, deu certo:

Calculo do IMC

 Faz entrada de dados? (S/N)n

Arquivo aberto com sucesso


Nome: Ronaldo Rodrigues Godoi
Idade: 48
Peso: 82.567
Altura: 1.700

IMC: 28.570


Nome: Stavros Augusto Godoi de Oliveira
Idade: 21
Peso: 70.432
Altura: 1.750

IMC: 22.998


Nome: Patricia Godoi Alves
Idade: 40
Peso: 55.000
Altura: 1.550

IMC: 22.893


Nome: Adriana Godoi de Oliveira
Idade: 41
Peso: 58.000
Altura: 1.550

IMC: 24.142

Até a próxima. Se eu tiver mais alguma dúvida, vou abrir um novo tópico, porque este já está muito sobrecarregado.

Atenciosamente,
Ronaldo

P.S.: Valeu Hugo, valeu pessoal.

1 curtida