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 struct s
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)