paulo1205
(usa Ubuntu)
Enviado em 16/01/2022 - 22:07h
O SamL já disse o que deveria ser dito a respeito do que foi perguntado, mas eu gostaria de acrescentar mais algumas informações que têm a ver com estilo e com uso consciente de memória.
1. Repetir o tipo da variável que foi usado na declaração é um mau hábito, pois torna a manutenção do código mais difícil (e ainda por cima é feio!). Sugiro que em vez de algo como
fwrite(&Cadastro, sizeof(struct Registry), 1, MyFile); você use o seguinte.
fwrite(&Cadastro, sizeof Cadastro, 1, MyFile);
Ainda haverá alguma redundância, por ter de aparecer o nome da variável duas vezes, mas é um acoplamento muito mais direto e auto-documentado do que ter de lembrar o nome do tipo. Deixe o compilador fazer esse trabalho por você, e, de quebra, fique com um código mais limpo e mais manutenível.
2. Quando você usa
strings com uma limitação máxima de tamanho, pode, em alguns casos, suprimir o terminador nulo, sobretudo quando tal
string tiver exatamente o tamanho máximo previsto.
Num caso como o que você apresentou, no qual você trabalha com um registro com campos de tamanho fixo, eu suponho que você declarou, por exemplo, o campo
nome com 101 elementos para ter 100 caracteres úteis, com espaço extra de um
byte para o terminador. Suponho que os outros campos seguiram o mesmo padrão. Sendo assim, cada registro vai ter sempre pelo menos 3
bytes de espaço ocupado com algo que não representa nenhuma informação útil. Esses 3
bytes são pouco perto do tamanho total do registro (
250118 bytes ), mas poderiam representar um desperdício considerável se você tivesse muitos milhares de registros num banco de dados real.
Para trabalhar com
strings sem o terminador quando tais
strings atingem o tamanho máximo, você pode adaptar seu programa, ainda usando funções padronizadas. A tabela abaixo mostra alguns exemplos de transformações que podem ser feitas com essa finalidade.
//CAMPOS DO REGISTRO INCLUINDO TERMINADOR //CAMPOS DO REGISTRO SEM TERMINADOR
strlen(reg.nome) strnlen(reg.nome, sizeof reg.nome)
strcmp(s, reg.nome) (
strncmp(s, reg.nome, sizeof reg.nome) ||
(unsigned char)s[sizeof reg.nome]
)
strcmp(reg.nome, s) (
strncmp(reg.nome, s, sizeof reg.nome) ||
-(int)((unsigned char)s[sizeof reg.nome])
)
strncmp(reg.nome, s, n) (
n<sizeof reg.nome? strncmp(reg.nome, s, n):
strncmp(reg.nome, s, sizeof reg.nome) ||
-(int)((unsigned char)s[sizeof reg.nome])
)
// strcat() é sempre perigosa; melhor // strncat() não é muito melhor, pois também não leva o
// evitá-la em qualquer contexto. // tamanho do destino em consideração.
strcat(s, reg.nome) strncat(s, reg.nome, sizeof reg.nome)
// strcat() é sempre perigosa; melhor // Variáveis lr, ls e left preexistentes e declaradas com
// evitá-la em qualquer contexto. // tipo size_t.
strcat(reg.nome, s) (
lr=strnlen(reg.nome, sizeof reg.nome), ls=strlen(s),
left=sizeof reg.nome-lr,
memmove(reg.nome+lr, left<ls? left: ls), reg.nome
)
strlcat(reg.nome, s, sizeof reg.nome) // Variável lr preexistente e declarada com tipo size_t.
(
lr=strlcat(reg.nome, s, sizeof reg.nome-1),
lr>=sizeof nome-1?
(reg.nome[lr]=s[sizeof reg.nome-lr], ++lr): lr
)
printf("%s", reg.nome) printf("%.*s", (int)sizeof reg.nome, reg.nome);
fgets(reg.nome, sizeof reg.nome, stdin) // Variável ch preexistente e declarada com tipo int.
// Variável lr preexistente e declarada com tipo size_t.
(
fgets(reg.nome, sizeof reg.nome, stdin)==NULL?
NULL:
(
(lr=strlen(reg.nome))==sizeof reg.nome-1 &&
(ch=fgetc(stdin))!=EOF && reg.nome[lr++]=ch,
reg.nome
)
)
3. O uso típico de
fwrite () é com dados binários, não com dados majoritariamente em forma de texto, e sobretudo com texto que pode ter tamanhos muito variados. Algumas técnicas para esses casos incluem:
• Trabalhar com arquivos de texto puro, separando campos do registro por uma quebra de linha (ou outro caráter que não possa de modo nenhum fazer parte dos dados de cada campo). A vantagem é que é fácil de ler com ferramentas externas (mas essa mesma característica pode ser um problema de segurança). As maiores desvantagens são a dificuldade de localizar um registro específico (deslocar-se pelo arquivo geralmente implica voltar ao início e varrer todos os registros, até chegar ao ponto desejado) e substituir um registro que tenha sido alterado.
• Como caso particular, poder-se-ia usar uma biblioteca de I/O que passe os arquivos por um processo de compactação antes de escrever e descompactação ao ler. A
zlib , por exemplo, tem funções como
gzprintf () e
gzgets () para substituir
fprintf () e
fgets (), entre outras. Entretanto, isso agrava ainda mais a questão de dificuldade de modificar um arquivo preexistente.
• Trabalhar com registros de tamanho fixo para os campos com tamanho não muito grande, e transformar os campos com grande variação em algo que aponte para arquivos externos (um por registro ou por campo de registro, possivelmente com nomes na forma de UUID que são armazenados no arquivo principal, ou com ponteiros para um deslocamento e um tamanho dentro de um arquivão de
blob , que pode conter os dados de vários registros). Alivia um pouco o problema dos registros mantidos no arquivo principal, facilitando, por exemplo, alterações em registros preexistentes, mas ainda deixa os dados do
blob sujeitos às desvantagens apresentadas acima.
• Usar uma biblioteca específica para armazenamento de dados em formatos arbitrários, tais como a Berkeley DB, GDBM ou TokyoCabinet, com um cadastro principal para os dados com pouca variação de tamanho e um possível segundo cadastro para os dados com tamanho muito variável, com a ligação entre os dois cadastros feita através de campos armazenados em cada registro do cadastro principal.
• Usar um biblioteca de banco de dados pequeno, tal como SQLite (que pode ser uma forma de você se familiarizar com a forma de trabalhar que aparece na próxima alternativa).
• Comunicar-se com um SGBD externo ao seu programa, tal como MariaDB, PostgreSQL ou algum SGBD comercial.
... Então Jesus afirmou de novo: “(...) eu vim para que tenham vida, e a tenham plenamente.” (João 10:7-10)