Dificuldade Básica com Struct [RESOLVIDO]

1. Dificuldade Básica com Struct [RESOLVIDO]

Nick Us
Nick-us

(usa Slackware)

Enviado em 28/04/2020 - 21:21h

Estou tentando entender como gravar uma Struct, usando fwrite e Ler usando fread. O Problema é que tirei minhas conclusões sozinho e não sei se estão corretas!
Até o momento o meu exemplo funciona, ele grava o arquivo, lê o arquivo. Minha preocupação é estar errado e funcionar por apenas coincidência!

Dúvida: O Número 1 no fwrite eu coloquei porque entendi que estou gravando 1 Objeto/Item, ou seja 1 Struct Contacts. Assim como coloquei no fread para ler apenas 1 Struct. Quero saber se estou correto nisso, porque inicialmente eu havia colocado o número 6 me referindo a 6 registros, então analisando melhor achei que estou gravando a Struct Contacts Inteira e não seus registros. Isso porque a syntax de fwrite se refere a ítens, logo pensei 1 Struct Contacts = 1 Ítem apenas! Não importa quantos registros ela tenha, ela sempre será 1 ítem!
struct {
char Name[2];
int Phone;
} Read[6], Contacts[6] = {{"A", 1}, {"B", 2}, {"C", 3}, {"D", 4}, {"E", 5}, {"F", 6}};

fwrite(Contacts, sizeof(Contacts), 1, MyFile);
fread(Read, sizeof(Read), 1, MyFile);



  


2. MELHOR RESPOSTA

Paulo
paulo1205

(usa Ubuntu)

Enviado em 01/05/2020 - 03:39h

Nick-us escreveu:

Estou tentando entender como gravar uma Struct, usando fwrite e Ler usando fread. O Problema é que tirei minhas conclusões sozinho e não sei se estão corretas!
Até o momento o meu exemplo funciona, ele grava o arquivo, lê o arquivo. Minha preocupação é estar errado e funcionar por apenas coincidência!

Dúvida: O Número 1 no fwrite eu coloquei porque entendi que estou gravando 1 Objeto/Item, ou seja 1 Struct Contacts. Assim como coloquei no fread para ler apenas 1 Struct. Quero saber se estou correto nisso, porque inicialmente eu havia colocado o número 6 me referindo a 6 registros, então analisando melhor achei que estou gravando a Struct Contacts Inteira e não seus registros. Isso porque a syntax de fwrite se refere a ítens, logo pensei 1 Struct Contacts = 1 Ítem apenas! Não importa quantos registros ela tenha, ela sempre será 1 ítem!
struct {
char Name[2];
int Phone;
} Read[6], Contacts[6] = {{"A", 1}, {"B", 2}, {"C", 3}, {"D", 4}, {"E", 5}, {"F", 6}};

fwrite(Contacts, sizeof(Contacts), 1, MyFile);
fread(Read, sizeof(Read), 1, MyFile);


Você ter razão de estar preocupado de funcionar por coincidência, pois o jeito como você fez está semanticamente incorreto, embora não haja erro de sintaxe (pelo menos no trecho que você mostrou).

O motivo por que funcionou no seu programa é a coincidência entre valores absolutos de ponteiros para dados de tipos diferentes e algumas escolhas referentes tipos de parâmetros que impedem o diagnóstico por parte do compilador através de pura análise sintática.

Vou dividir a explicação em partes diferentes.


1) Diferenças entre arrays e structs

Uma coisa que chama atenção no texto em que você descreve o problema é que você parece considerar que Contacts e Read são, cada um, uma estrutura. Não são. Eles são, conforme suas declarações no programa, “arrays com seis elementos de um tipo struct anônimo” (anônimo porque você não atribuiu um rótulo, o que não é uma coisa muito boa, mas não vou entrar nessa questão agora). Essa distinção é importante porque se o que caracteriza a estrutura são os campos que a compõem, você não consegue acesso a esses campos sem, antes, especificar um elemento específico de cada um desses arrays.

Em outras palavras, você não pode dizer diretamente algo como “Contacts.Phone”, mas sim algo como “Contacts[0].Phone”. Ou, ainda, ao aparecer numa expressão, “Contacts” não traz uma estrutura à qual se pode aplicar a operação de obtenção do valor de um único campo. Numa expressão comum, “Contacts” produz apenas um ponteiro para o primeiro elemento de um array com seis elementos, cada um deles, sim, contendo uma estrutura.

Aí alguém pode ficar surpreso, e perguntar: “'Per'aí! Ponteiro? Mas você não acabou de dizer que Contacts foi declarado como array com seis elementos de um tipo struct anônimo?”

Já vou falar sobre isso. Antes, porém, vamos ver algo sobre o funcionamento de fread() e fwrite(). Vou me concentrar em fwrite(), mas fread() pode ser entendida como o análogo, com os dados fluindo no sentido inverso.


2) Funcionamento de fwrite()

Veja como é a declaração da função fwrite:
size_t fwrite(void *p_obj, size_t obj_size, size_t n_elements, FILE *fp); 


O último parâmetro da função é o mais simples de explicar, pois indica o arquivo onde os dados serão gravados.

O primeiro parâmetro é um ponteiro para void, que é a maneira do C de dizer que o argumento correspondente, na hora de chamar a função, pode ser um ponteiro para qualquer tipo de dados. Esse parâmetro é a única forma de garantir que você pode ter uma única função que seja capaz de gravar dados de quaisquer tipos. Se o tipo do primeiro argumento fosse qualquer outra coisa, seria necessário escolher uma das seguintes possibilidades: ter uma função diferente para cada tipo de dados que você quisesse gravar e que viesse a existir no programa (tanto tipos nativos da linguagem quanto tipos compostos e tipos definidos pelo usuário), ou ter uma função única, mas que obrigaria a ter uma conversão de tipos explícita antes de passar qualquer objeto para ser gravado (o que é enfadonho e sujeito a erro).

O segundo parâmetro decorre da escolha feita para o primeiro: como o ponteiro para o dado se ser escrito pode apontar qualquer tipo, a única forma que a função tem de saber quantos bytes o dado apontado realmente possui é informando explicitamente tal quantidade.

O terceiro parâmetro serve principalmente para o caso em que o objeto que será gravado seja um array, pois informa quantos elementos consecutivos devem ser gravados, começando da posição de memória indicada pelo primeiro parâmetro, e tendo cada elemento o tamanho indicado pelo segundo parâmetro. Se o primeiro parâmetro se referir a um único objeto, que não seja um array, o valor a ser usado como argumento deve ser igual a 1.

As formas canônicas de usar fwrite com diferentes tipos de dados são as seguintes.

  • Para um objeto simples (que não seja array, tal como um int, um double ou qualquer struct ou union):
rc=fwrite(&obj, sizeof obj. 1. file);	// endereço do objeto, tamanho do objeto, apenas 1 elemento (não é array), arquivo de saída 

  • Para um objeto simples referido por um ponteiro (assume que p_obj é um ponteiro para qualquer tipo que seja diferente de void e que aponte para um objeto válido):
rc=fwrite(p_obj, sizeof *p_obj, 1, file);	// ponteiro para objeto, tamanho do conteúdo apontado, apenas 1 elemento (não é array), arquivo de saída 

  • Para um array declarado como tal (não referenciado através de ponteiro):
rc=fwrite(array, sizeof array[0], n_elements, file);	// ponteiro para o primeiro elemento, tamanho de cada elemento (usamos o do primeiro, mas todos têm o mesmo tamanho), nº de elementos a gravar (pode ser <= nº de elementos declarados), arquivo de saída 

  • Para um array indiretamente referenciado por meio de ponteiro:
rc=fwrite(p_elements, sizeof p_elements[0], n_elements, file);	// ponteiro para o primeiro elemento, tamanho de cada elemento, nº de elementos a gravar, arquivo de saída 


Em todos os casos acima, o valor de retorno das chamadas à função são gravados numa variável que deve ser do tipo size_t, e representam a quantidade de elementos gravados com sucesso. Se a operação tiver sido bem sucedida, esse valor deve ser igual ao do terceiro argumento. Em caso de falha, o valor será 0, e no caso de sucesso parcial (válido para operações com arrays que tenham conseguido escrever apenas uma parte dos elementos especificados no terceiro argumento) pode ser qualquer valor menor do que o do terceiro argumento. Para falha ou sucesso parcial, a causa do erro pode ser investigada através dos valores da variável global errno e dos valores retornados por feof() (no caso de fread()) e ferror().

Note que o jeito como você fez não se encaixa em nenhum dos casos acima.

Para que você tivesse usado a forma canônica de gravar arrays, deveria ter feito do seguinte modo:
fwrite(Cadastro, sizeof Cadastro[0], sizeof Cadastro/sizeof Cadastro[0], MyFile) 

Por outro lado, se você quisesse tratar o array inteiro como único objeto, devera ter feito do seguinte modo:
fwrite(&Cadastro, sizeof Cadastro, 1, MyFile) 


Mas eu não recomendo gravar arrays como se fossem um único objeto por causa justamente da possibilidade que fwrite() oferece de sucesso parcial. Se você mandar gravar 5000 elementos de 1000 bytes cada um nunca disco que só tem espaço para gravar 4MB, provavelmente 4000 mil desses 5000 registros serão salvos com sucesso, e você pode dar um jeito de se virar com isso e recuperar os 1000 que faltaram. Por outro lado, se você mandar gravar 5000000 bytes como se fossem um único dado, fwrite() vai simplesmente falhar durante a operação de escrita, sem que você tenha qualquer meio confiável de saber em que ponto da operação o erro ocorreu.


3) Arrays, ponteiros, ponteiros para arrays e (in)felizes coincidências

Agora, sobre o array que vira ponteiro.

Essa é uma característica do C. Objetos declarados como arrays provocam, do ponto de sua declaração em diante, a operação de reservar uma porção fixa de memória suficiente para armazenar todos os elementos declarados como parte do array. Por ser fixa, não pode ser mudada ao longo de todo o tempo de vida do array, e o compilador não reserva espaço na memória, como faria com variáveis comuns, para guardar informações sobre o tamanho do array ou as posições de memória que ele ocupa. Em lugar disso, ele apenas lembra desses valores durante a compilação e os substitui diretamente no código quando necessário, como se fossem constantes do programa.

Se você tem uma declaração de array conforme abaixo
T arr[N]; 
(onde T é um tipo de dados qualquer e N é um valor inteiro positivo e constante), o compilador vai produzir os seguintes efeitos:

  • o array vai ocupar uma região de memória fixa suficiente para armazenar todos os elementos;

  • a expressão “sizeof arr” será um valor constante (lembrado pelo compilador e substituído diretamente no programa onde necessário, mas não gravado na memória) do tipo size_t e correspondente a N*sizeof(T);

  • a expressão “arr” será um valor constante (lembrado pelo compilador e substituído diretamente no programa onde necessário, mas não gravado na memória) do tipo T * (“ponteiro para T”) e correspondente ao endereço do primeiro elemento do array (logo no início da região de memória alocada); esse valor é idêntico ao da expressão “&arr[0]”;

  • o valor original de N pode ser calculado através da expressão “sizeof arr/sizeof arr[0]” (que também produzirá um valor constante, uma vez que tanto o numerador quanto o denominador são constantes);

  • a expressão “arr+N” (ou “arr+sizeof arr/sizeof arr[0]”) aponta para o fim do array (lembrando que, em C e C++, o fim do array é o primeiro endereço que vem depois do início do array e que não pertence mais a ele);

  • embora não seja possível tratar diretamente o array inteiro como um único objeto, é possível obter um ponteiro para um dado que pode ser entendido como o array inteiro através da expressão “&arr”, que também é um valor constante (lembrado pelo compilador e substituído diretamente no programa onde necessário), cujo tipo é T (*)[N] (“ponteiro para um array com N elementos do tipo T”).

  • a expressão “&arr+1” aponta para a primeira posição de memória após o final do array inteiro.

Se você reparar bem, se você pensar na memória como uma sequência de bytes, o valor absoluto do endereço onde reside o começo do primeiro elemento do array coincide com o endereço do começo do array inteiro, e o endereço do lugar após o final do último elemento do array coincide com o endereço do lugar após o final do array inteiro. Então, em termos de bytes, descartando o fato de que os elementos apontados são diferentes, arr produz o mesmo valor numérico que &arr, ambos correspondendo ao primeiro byte do array, assim como arr+N e &arr+1 correspondem ambos ao primeiro byte após o final do array. Ainda por essa linha, N*sizeof arr[0] indica a mesma quantidade de bytes que sizeof arr.

E esse é o motivo pelo qual o jeito como você fez aparentemente funcionou. Você passou para o primeiro parâmetro de fwrite() um argumento que ponteiro para o primeiro elemento do array e, para o segundo, um valor que corresponde ao tamanho do array inteiro. O valor do tamanho não corresponde ao tamanho do dado apontado, ou, vendo por outro ângulo, o tipo do ponteiro não possui todos os bytes que você que que ele possui. Contudo, a função não tem como verificar isso porque o C não tem como amarar semanticamente (nem sintaticamente) os valores dos argumentos correspondentes aos três primeiros parâmetros da função. Além disso, o fato de que o primeiro parâmetro é um “ponteiro para qualquer tipo de dado” também não ajuda nem um pouco a verificar a consistência da operação.

Para pegar esse tipo de erro, seria necessário ter no mínimo amarrações sintáticas que restringissem os tipos dos elementos. Imagine o seguinte código.

#include <stdbool.h>
#include <stdio.h>

struct my_record { // Estou dando um nome para a estrutura que você tinha deixado anônima
char Name[2];
int Phone;
};

#define ARRAY_SIZE 6

// Função para gravar vários registros, dispostos dentro de um array.
// Note que eu não preciso passar o tamanho de cada registro, porque o compilador tem como calcular isso.
size_t fwrite_records(const struct my_record *p_first_rec, size_t n_recs, FILE *out_file){
return fwrite(p_first_rec, sizeof p_first_rec[0], n_recs, out_file);
}

// Função que grava um array inteiro com ARRAY_SIZE elementos do tipo struct my_record.
// Note que o tipo de valor de retorno é bool, pois não vai existir sucesso parcial. Além disso, eu não tenho
// parâmetro para o tamanho total, pois o tamanho do array está amarrado.
bool fwrite_fixed_record_array(struct my_record (*const p_array)[ARRAY_SIZE], FILE *out_file){
return fwrite(p_array, sizeof *p_array, 1, out_file)==1;
}

// Função que recebe um array inteiro, mas pode ter sucesso parcial porque divide a gravação em elemento por elemento.
size_t fwrite_array_with_partial_success(struct my_record (*const p_array)[ARRAY_SIZE], FILE *out_file){
return fwrite(*p_array, sizeof (*p_array)[0], ARRAY_SIZE, out_file);
}

struct my_record Contacts[ARRAY_SIZE] = {{"A", 1}, {"B", 2}, {"C", 3}, {"D", 4}, {"E", 5}, {"F", 6}};
struct my_record Contacts8[ARRAY_SIZE+2] = {{"A", 1}, {"B", 2}, {"C", 3}, {"D", 4}, {"E", 5}, {"F", 6}, {"G", 7}, {"H", 8}};

int main(void){
FILE *fp=fopen("/tmp/output_file.dat", "wb");
if(!fp){
fputs("Não foi possível gerar arquivo de saída.\n", stderr);
return 1;
}
printf("fwrite_records(Contacts, ...): %zu\n", fwrite_records(Contacts, sizeof Contacts/sizeof Contacts[0], fp));
printf("fwrite_fixed_record_array(&Contacts, ...): %s\n", fwrite_fixed_record_array(&Contacts, fp)? "true": "false");
printf("fwrite_array_with_partial_success(&Contacts, ...): %zu\n", fwrite_array_with_partial_success(&Contacts, fp));
printf("fwrite_records(Contacts8, ...): %zu\n", fwrite_records(Contacts8, sizeof Contacts8/sizeof Contacts8[0], fp)); // OK: array tem tamanho diferente do padrão, mas o tipo dos elementos é o mesmo, então a versão que pega o ponteiro para o primeiro elemento e o nº de elementos funciona.

/*
As tentativas abaixo produzem erro (ou no mínimo alerta) de compilação. Estão aqui para ilustrar o mesmo
erro que você cometeu, mas de um modo que o compilador é capaz de identificar, em vez de deixar passar.
*/
printf("fwrite_records(&Contacts, ...): %zu\n", fwrite_records(&Contacts, sizeof Contacts/sizeof Contacts[0], fp)); // Erro de tipo, apesar da coincidência numérica do endereço.
printf("fwrite_fixed_record_array(Contacts, ...): %s\n", fwrite_fixed_record_array(Contacts, fp)? "true": "false"); // Erro de tipo, apesar da coincidência numérica do endereço.
printf("fwrite_array_with_partial_success(Contacts, ...): %zu\n", fwrite_array_with_partial_success(Contacts, fp)); // Erro de tipo, apesar da coincidência numérica do endereço.
printf("fwrite_fixed_record_array(&Contacts8, ...): %s\n", fwrite_fixed_record_array(&Contacts8, fp)? "true": "false"); // Erro de tipo: ponteiro para a “array inteiro” com tamanho diferente do “array inteiro” esperado, apesar do tipo dos elementos ser o mesmo.
printf("fwrite_array_with_partial_success(&Contacts8, ...): %zu\n", fwrite_array_with_partial_success(&Contacts8, fp)); // Erro de tipo: ponteiro para a “array inteiro” com tamanho diferente do “array inteiro” esperado, apesar do tipo dos elementos ser o mesmo.

fclose(fp);
}


Ao tentar compilar, veja o que acontece.
gcc x.c -Wall -Werror -O2 -std=c11 -pedantic-errors
x.c: In function ‘main’:
x.c:47:65: error: passing argument 1 of ‘fwrite_records’ from incompatible pointer type [-Wincompatible-pointer-types]
printf("fwrite_records(&Contacts, ...): %zu\n", fwrite_records(&Contacts, sizeof Contacts/sizeof Contacts[0], fp)); // Erro de tipo, apesar da coincidência numérica do endereço.
^
x.c:13:8: note: expected ‘const struct my_record *’ but argument is of type ‘struct my_record (*)[6]’
size_t fwrite_records(const struct my_record *p_first_rec, size_t n_recs, FILE *out_file){
^~~~~~~~~~~~~~
x.c:48:85: error: passing argument 1 of ‘fwrite_fixed_record_array’ from incompatible pointer type [-Wincompatible-pointer-types]
fixed_record_array(Contacts, ...): %s\n", fwrite_fixed_record_array(Contacts, fp)? "true": "false"); // Erro de tipo, apesar da coincidência numérica do endereço.
^~~~~~~~
x.c:20:6: note: expected ‘struct my_record (* const)[6]’ but argument is of type ‘struct my_record *’
bool fwrite_fixed_record_array(struct my_record (*const p_array)[ARRAY_SIZE], FILE *out_file){
^~~~~~~~~~~~~~~~~~~~~~~~~
x.c:49:102: error: passing argument 1 of ‘fwrite_array_with_partial_success’ from incompatible pointer type [-Wincompatible-pointer-types]
l_success(Contacts, ...): %zu\n", fwrite_array_with_partial_success(Contacts, fp)); // Erro de tipo, apesar da coincidência numérica do endereço.
^~~~~~~~
x.c:25:8: note: expected ‘struct my_record (* const)[6]’ but argument is of type ‘struct my_record *’
size_t fwrite_array_with_partial_success(struct my_record (*const p_array)[ARRAY_SIZE], FILE *out_file){
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
x.c:50:87: error: passing argument 1 of ‘fwrite_fixed_record_array’ from incompatible pointer type [-Wincompatible-pointer-types]
xed_record_array(&Contacts8, ...): %s\n", fwrite_fixed_record_array(&Contacts8, fp)? "true": "false"); // Erro de tipo: ponteiro para a “array inteiro” com tamanho diferente do “array inteiro” esperado, apesar do tipo dos elementos ser o mesmo.
^
x.c:20:6: note: expected ‘struct my_record (* const)[6]’ but argument is of type ‘struct my_record (*)[8]’
bool fwrite_fixed_record_array(struct my_record (*const p_array)[ARRAY_SIZE], FILE *out_file){
^~~~~~~~~~~~~~~~~~~~~~~~~
x.c:51:104: error: passing argument 1 of ‘fwrite_array_with_partial_success’ from incompatible pointer type [-Wincompatible-pointer-types]
success(&Contacts8, ...): %zu\n", fwrite_array_with_partial_success(&Contacts8, fp)); // Erro de tipo: ponteiro para a “array inteiro” com tamanho diferente do “array inteiro” esperado, apesar do tipo dos elementos ser o mesmo.
^
x.c:25:8: note: expected ‘struct my_record (* const)[6]’ but argument is of type ‘struct my_record (*)[8]’
size_t fwrite_array_with_partial_success(struct my_record (*const p_array)[ARRAY_SIZE], FILE *out_file){
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



... Então Jesus afirmou de novo: “(...) eu vim para que tenham vida, e a tenham plenamente.” (João 10:7-10)

3. Dificuldade Básica com Struct

Afonso T Freitas
atf

(usa openSUSE)

Enviado em 29/04/2020 - 09:30h

Parece estar tudo correto.

LinuxUser#142898


4. Re: Dificuldade Básica com Struct [RESOLVIDO]

Mauricio Ferrari
maurixnovatrento

(usa Slackware)

Enviado em 01/05/2020 - 07:36h

Nunca tentei usar o fwhite e fread. É sempre bom estar atento a esses detalhes.

Eu já usei fputs e fgets, mas não sei se funciona para esses casos.




5. Re: Dificuldade Básica com Struct [RESOLVIDO]

Nick Us
Nick-us

(usa Slackware)

Enviado em 02/05/2020 - 22:10h

paulo1205 escreveu: Você ter razão de estar preocupado de funcionar por coincidência, pois o jeito como você fez está semanticamente incorreto, embora não haja erro de sintaxe (pelo menos no trecho que você mostrou).

Puxa, levarei dias ou talvez meses pra entender tudo isso! De qualquer forma obrigado pela explicação!
Percebi com isso que terei que estudar muito mais a fundo a função fwrite e seu funcionamento!
Também levarei um tempo para criar exemplos com o material que vc postou para que eu possa entender na prática linha a linha!
Mas eu gosto disso!
Gosto de entender ao máximo o que estou fazendo, quando faço alguma coisa! Gosto de saber o que estou fazendo e porque estou fazendo!
Obrigado!







Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts