paulo1205
(usa Ubuntu)
Enviado em 14/01/2021 - 22:24h
princknoby escreveu:
Obrigado pela atenção Paulo!
Acho que consegui entender a questão de declarar n = 0
Fiz o programa declarando n =1 :
int n = 1;
Dados *pd = malloc(n * sizeof *pd) // Não entendi essa questão muito bem... *pd e Dados tem o mesmo tamanho?!
// Eu deveria usar sizeof *pd ou sizeof(Dados)?
Para manter o programa coerente, seria interessante indicar que a quantidade
n de elementos do
array dinâmico é zero, até o momento em que você decidir incluir o primeiro elemento. E a forma de evitar o comportamento dúbio de
malloc () invocado com tamanho zero é fazer tão somente o seguinte.
size_t n=0; // Atenção ao fato de que tamanhos em C são medidos com o tipo size_t, não com int.
Dados *pd=NULL; // É seguro, depois, chamar realloc(p, n+1), mesmo que p seja inicialmente nulo.
Para alocações iniciais, eu prefiro, sempre que possível, usar uma das seguintes formas (que são absolutamente sinônimas, e nas quais
p é um ponteiro para qualquer tipo diferente de
void e
N um valor inteiro positivo).
p=malloc(N * sizeof *p);
p=malloc(N * sizeof p[0]);
Qualquer dessas duas formas é vantajosa por reduzir a quantidade de coisas que você tem de escrever (a não ser que você comece a usar variáveis com nomes gigantescos e nomes de tipos bem curtinhos), e joga o trabalho de recuperar a informação sobre o tipo de dado apontado para o compilador, que já tem essa informação desde o momento em que o ponteiro foi declarado.
Se o tipo do ponteiro
p for
void * (ponteiro para tipo de dados não-determinado), a expressão
*p é inválida (e também não faria sentido fazer “
sizeof (void) ”). Nesse caso, o tamanho de cada elemento ao qual o ponteiro se refere teria de ser indicado de alguma outra forma; se tal forma for através da aplicação de
sizeof a alguma coisa, ou você usaria o nome do tipo (por exemplo: “
sizeof (double) ”) ou indicaria algum outro ponteiro para dado de tipo conhecido como referência (assim como eu fiz, na postagem anterior, ao indicar a forma correta de chamar
realloc ()).
Infelizmente não dá para evitar repetição de
p como destino do valor devolvido por
malloc () e com indicador do elemento cujo tamanho deve ser calculado dor
sizeof . No entanto, se essa repetição o incomodar, você pode criar macros de alocação e realocação (as macros são feias, mas o uso delas, mais tarde, pode vir a ser conveniente).
// Macros para alocação inicial: n pode ser constante ou variável, valor de p é alterado, e é o valor devolvido pela macro.
#define PTR_ALLOC(p, n) ( (p)=malloc((n) * sizeof *(p)) )
#define PTR_CALLOC(p, n) ( (p)=calloc((n), sizeof *(p)) )
// Função auxiliar para realocação genérica. Retorna verdadeiro se a alocação tiver sido bem sucedida e altera tanto o ponteiro quanto o tamanho.
// Se a realocação falhar, retorna falso e não altera nem o ponteiro nem o tamanho.
int _my_realloc(void **p, size_t *p_n, size_t new_n, size_t elem_size){
void *new_p=realloc(*p, new_n*elem_size);
if(new_p){
*p=new_p;
*p_n=new_n;
return 1;
}
return 0;
}
// Macro para realocação genérica. O argumento referente ao parâmetro n tem de ser uma variável do tipo size_t, que será alterada
// juntamente com o ponteiro p se a realocação tiver funcionado. Retorna um valor booleano indicando se a realocação foi bem sucedida.
#define PTR_REALLOC(p, n, new_n) _my_realloc((void **)&(p), &(n), new_n, sizeof *(p))
/* EXEMPLOS DE USO */
void func(...){
char *pc;
double *pd;
int *pi;
struct tm *ptm=NULL;
size_t ni, ntm=0;
if(PTR_ALLOC(pc, 6))
strcpy(pc, "Paulo");
if(!PTR_CALLOC(pd, 50)){
perror("Alocação de 50 doubles com valores iniciais zerados falhou");
exit(1);
}
ni=PTR_ALLOC(pi, 10)? 10: 0;
// Como ptm é incialmente NULL, a realocação tem o mesmo efeito de uma alocação inicial.
if(PTR_REALLOC(ptm, ntm, 25)){
if(!PTR_realloc(pi, ni, ntm)){
perror("Não foi possível realocar inteiros com a mesma quantidade de struct tms");
exit(2);
}
}
/* ... */
}
E na chamada da função para adicionar os dados, eu chamo a função assim:
add(&pd[n-1]);
Tem alguma forma mais bonitinha ao invés de usar [n-1]?
Olha, o
n-1 possivelmente vai aparecer mais cedo ou mais tarde de qualquer maneira: uma vez que o C começa a contar os índices de
arrays com
0 , os índices de um
array com
N elementos vão variar de
0 a
N-1 (por exemplo: num array com cinco elementos, os índices válidos serão
0 ,
1 ,
2 ,
3 e
4 ). Em outras palavras, o valor absoluto do último índice válido será sempre uma unidade menor do que o valor absoluto do número de total de elementos.
Eu cheguei a sugerir, na mensagem anterior, que você alterasse a ordem entre a realocação e a adição do elemento. Se o fizer, possivelmente ficará com algo parecido com o seguinte.
void *new_ptr=realloc(p, n+1);
if(!new_ptr){
perror("erro durante realocação");
exit(1);
}
p=new_ptr;
add(p+n++); // Sinônimo de “add(&p[n++]);”, e ambos têm o mesmo efeito de “add(p+n); ++n;”.
Eu pensei em passar todos os elementos da struct e usar uma variável de controle dentro da própria função, o problema foi que não consegui passar todos os elementos, fiquei confuso entre:
add(&pd); e add(pd);
O problema é que só com a informação do ponteiro, você não sabe quantos elementos estão alocados, nem quantos estão em uso. De alguma forma, você teria de passar à função o valor de
n .
Do jeito como você fez originalmente, o crescimento do
array é feito fora da função
add (), e você passa para ela o ponteiro para o elemento que acabou de ser acrescentado ao final. Apesar do nome, a função
add () na verdade não adiciona nada, mas apenas preenche os valores do que já foi adicionado ao
array fora da função, antes de ela ser chamada. Como você bem pode ver, o segundo parâmetro (
n ) acaba ficando sem uso dentro do corpo da função. (Por isso mesmo, no meu último exemplo, acima, eu omiti o segundo argumento: tendo visto como a função foi implementada, eu acabei ignorando mentalmente o segundo argumento.)
Você poderia passar a usar o segundo parâmetro, de modo a não precisar passar
p+n como primeiro argumento. Nesse caso, você teria de mexer também no interior da função, passando a usar
pd[n].nome e
pd[n].CPF em lugar de
pd->nome e
pd->CPF , respectivamente, como no exemplo abaixo (reduzido para mostrar apenas as partes que interessam).
// Primeira opção: add () recebe o índice do elemento que será alterado.
void add(Dados *pd, size_t n){
int l;
// É importante sempre testar o grau de sucesso da leitura!
if(l=0, scanf("%49[^\n]%*1[\n]%n", pd[n].nome, &l)!=1 || l==0){
perror("dados inválidos para o nome");
exit(1);
}
if(l=0, scanf("%29[^\n]%*1[\n]%n", pd[n].CPF, &l)!=1 || l==0){
perror("dados inválidos para o CPF");
exit(1);
}
}
// MODO DE USO
int main(void){
size_t n=0;
Dados *pd=NULL;
/* ... */
PTR_REALLOC(pd, n, n+1);
add(pd, n-1);
/* ... */
}
Alternativamente, você poderia reescrever
add () para receber p tamanho atual do
array , e internamente fazer o cálculo de
n-1 .
// Segunda opção: add() recebe o tamanho do array (assim como show()), e calcula o índice para o último elemento a partir do tamanho.
void add(Dados *pd, size_t n){
if(n==0){
perror("não é possível ler elementos num array vazio");
exit(1);
}
--n; // Seguro de fazer, uma vez que o valor do parâmetro é uma mera cópia do valor do argumento. Agora n é o índice do último elemento, não mais o tamanho.
/* ... igual ao código anterior... */
}
int main(void){
Dados *pd=NULL;
size_t n=0;
/* ... */
PTR_REALLOC(pd, n, n+1);
add(pd, n);
/* ... */
}
Um passo além seria mover a realocação para dentro da função
add (), fazendo com que o que ela faz tenha mais a ver com o seu nome. Nesse caso, a função teria de alterar tanto os valores de
pd quanto de
n , de modo que você teria de declarar a função com parâmetros que teriam de ser ponteiros para os valores a serem alterados (lembrando que C não tem passagem de argumentos por referência, mas apenas por cópia de valor, o que implica que você tem de criar explicitamente tais referências na hora de invocar a função).
// Opção 3: a função add () altera o array dinâmico e a informação do seu tamanho.
void add(Dados **pd, size_t *n){
void *new_ptr;
if(!(new_ptr=realloc(*pd, (1+*n) * sizeof **pd))){
perror("erro de realocação");
exit(1);
}
*pd=new_ptr;
int l;
if(l=0, scanf("%49[^\n]%*1[\n]%n", (*pd)[n].nome, &l)!=1 || l==0){
perror("dados inválidos para o nome");
exit(1);
}
if(l=0, scanf("%29[^\n]%*1[\n]%n", (*pd)[n].CPF, &l)!=1 || l==0){
perror("dados inválidos para o CPF");
exit(1);
}
++*n;
}
// MODO DE USO
int main(void){
Dados *pd=NULL;
size_t n=0;
/* ... */
// Agora você não faz realocação explícita aqui: ela é feita dentro de add().
// Por outro lado, note qe agora se tem “&pd” e “&n” em vez de “pd” e “n”.
add(&pd, &n);
/* ... */
}
Usando
add(pd); e recebendo na função:
void add (Dados *pd);
Acabou que não funcionou e eu consegui adicionar dados apenas a primeira posição da struct :/
Pelo que você falou acima, pareceu-me que você gostaria de dar um passo além, e não ter de ficar passando duas variáveis independentes para as funções. De fato, essa falta de acoplamento entre o
array e a representação do seu tamanho é inconveniente e é uma fonte comum de dificuldade de manutenção de código em programas grandes, e, por conseguinte, uma fonte em potencial de
bugs .
Para resolver esse desacoplamento, você poderia juntar o ponteiro para o
array dinâmico e o tamanho do
array dentro de uma estrutura, e usar varáveis com esse tipo de estrutura para representar seus dados.
typedef struct {
size_t n;
struct { char nome[50], CPF[30]; } *pd;
} da_Dados; // “da_” de dynamic array .
De posse desse tipo de dados, agora você teria de criar funções para lidar com esse tipo de dados. Como ele tem campos que incluem alocação dinâmica de recursos (no caso, memória), você teria de ter, no mínimo, uma função de inicialização e outra de liberação de recursos (em idioma de orientação a objetos, tais funções seriam, respectivamente, um construtor e um destrutor).
void init_da_Dados(da_Dados *pda){
pda->n=0;
pda->pd=NULL;
}
void free_da_Dados(da_Dados *pda){
free(pda->pd);
pda->pd=NULL;
pda->n=0;
}
void show_n_da_Dados(const da_Dados *pda, size_t n){
if(n>=pda->n){
perror("índice fora da faixa");
exit(1);
}
printf("Nome: %s\nCPF: %s\n", pda->pd[n].nome, pda->pd[n].CPF);
}
void show_da_Dados(const da_Dados *pda){
if(pda->n){
printf("Nome: %s\nCPF: %s\n", pda->pd[0].nome, pda->pd[0].CPF);
for(size_t n=1; n<pda->n; ++n)
printf("\nNome: %s\nCPF: %s\n", pda->pd[n].nome, pda->pd[n].CPF);
}
}
void add_read_da_Dados(da_Dados *pda){
void *new_pd=realloc(pda->pd, (pda->n+1)*sizeof *pda->pd);
if(!new_pd){
perror("falha de realocação de memória");
exit(1);
}
int l;
if(l=0, scanf("%49[^\n]%*1[\n]%n", pda->pd[pda->n].nome, &l)!=1 || l==0){
perror("dados inválidos para o nome");
exit(1);
}
if(l=0, scanf("%29[^\n]%*1[\n]%n", pda->pd[pda->n].CPF, &l)!=1 || l==0){
perror("dados inválidos para o CPF");
exit(1);
}
++pda->n;
}
// MODO DE USO
int main(void){
da_Dados dad; // Declaração: aloca espaço para o objeto na memória automática.
init_da_Dados(&dad); // Inicializa (constrói) o objeto que foi alocado.
/* ... */
add_read_da_Dados(&dad); // Insere um elemento.
/* ... */
show_da_Dados(&dad); // Exibe todos os elementos.
/* ... */
free_da_Dados(&dad); // Libera recursos (desconstrói) o objeto quando ele não for mais necessário.
}
A maneira acima resolve quase todos os problemas de acoplamento, ao preço de mover quase todas as funcionalidades que operam sobre o
array dinâmico para funções especializadas. Mas ainda existe um ponto do acoplamento que não foi resolvido: entre a criação do objeto (no momento de sua declaração) e sua inicialização (quando a função construtura é chamada), o objeto está num estado desconhecido (valores dos campos internos indeterminados). É até possível que o programador esqueça de chamar a função de inicialização completamente, o que pode trazer resultados imprevisíveis (pode funcionar hoje e não funcionar amanhã, ou pode funcionar num compilador, mas não com outro).
Além disso, note que todas as funções de manipulação do objeto, incluindo o construtor e o destrutor, empregam argumentos que são ponteiros, de modo que você é obrigado a colocar sempre um
& na frente do argumento. Alguém poderia até ficar tentado a criar uma segunda variável com a forma de ponteiro, só para não ter de ficar escrevendo toda hora “
&dad ”. Ainda além disso, a alocação automática pode ter restrições de memória severas em alguns ambientes, de modo que seria preferível alocação totalmente dinâmica do objeto.
Finalmente, existe ainda uma questão de encapsulamento. Já que todas as manipulações no objeto são feitas por funções dedicadas, pode ser vantajoso esconder o que vai dentro do objeto, a fim proteger segredos comerciais (não neste exemplo trivial) e também evitar que os usuários desses objetos fiquem tentados a manipular diretamente seus campos internos, sem usar as funções especializadas.
Desse modo, uma outra possível evolução serviria para atacar todas as questões acima de uma só vez.
Se o construtor for ligeiramente reescrito, ele pode providenciar a criação do objeto diretamente na memória para alocação dinâmica e inicializar seu conteúdo, devolvendo um ponteiro para a área devidamente inicializada, eliminando qualquer possibilidade de trabalhar com um objeto com campos em estado indeterminado.
// Novo construtor: em vez de ter tipo de retorno “void”, retorna um ponteiro para a área criada e inicializada.
da_Dados *init_da_Dados(void){
da_Dados *new_pad=malloc(sizeof *new_pad);
if(!new_pad){
perror("falha de alocação de memória");
exit(1);
}
new_pad->n=0;
new_pad->pd=NULL;
return new_pad;
}
// O destrutor também tem de ser ajustado, a fim de liberar o ponteiro para o próprio objeto.
void free_da_Dados(da_Dados *pda){
free(pda->pd);
// As duas linhas abaixo não são estritamente necessárias.
pda->pd=NULL;
pda->n=0;
free(pda);
}
As demais funções não precisam sofrer alteração. Mas o modo de uso se altera ligeiramente, pois agora você vai usar um ponteiro para se referir ao objeto.
// MODO DE USO.
int main(void){
da_Dados *pdad=init_da_Dados(); // Aloca e inicializa (constrói) um novo objeto, referido através de um ponteiro.
/* ... */
add_read_da_Dados(pdad); // Insere um elemento.
/* ... */
show_da_Dados(pdad); // Exibe todos os elementos.
/* ... */
free_da_Dados(pdad); // Libera recursos (desconstrói) o objeto quando ele não for mais necessário.
}
Além disso, como não há mais a manipulação de nada fora das funções especializadas que não seja através de ponteiros, você pode esconder a implementação tanto dessas funções quanto da própria definição do tipo de dados
da_Dados , isolando declarações e implementação.
Por exemplo, você poderia ter um arquivo de cabeçalho com o seguinte conteúdo.
// Arquivo «da_Dados.h».
#ifndef _DA_DADOS_H_
#define _DA_DADOS_H_
// Cria uma declaração de tipo baseado em estrutura, mas sem o definir.
// Como ele só vai aparecer no contexto de ponteiros, o compilador aceita ponteiros para esse “tipo incompleto”
// (o nome é esse mesmo!) como distinto dos demais tipos de ponteiros.
typedef struct _da_Dados da_Dados;
// Agora declara as funções que manipulam ponteiros para esse tipo.
da_Dados *init_da_Dados(void); // Construtor.
void free_da_Dados(da_Dados *pda); // Destrutor.
void show_n_da_Dados(const da_Dados *pda, size_t n); // Exibe um único elemento do array dinâmico.
void show_da_Dados(const da_Dados *pda); // Exibe todo o array dinâmico.
void add_read_da_Dados(da_Dados *pda); // Cria novo elemento e lê seu conteúdo.
// Outras possíveis funções.
size_t size_da_Dados(const da_Dados *pda); // Informa quantidade de elementos em uso.
void remove_last_da_Dados(da_dados *pda); // Apaga último elemento.
void remove_n_da_Dados(da_dados *pda, size_t n); // Apaga elemento em determinada posição do array dinâmico.
void add_str_da_Dados(da_dados *pda, const char *nome, const char *cpf); // Cria novo elemento com valores lidos externamente.
/* etc. */
#endif // #ifndef _DA_DADOS_H_
A efetiva definição do tipo de dados e a implementação das funções ficaria num arquivo .c separado. Esse arquivo não precisaria ser do conhecimento do usuário: em vez disso, ele poderia ser compilado e distribuído em forma de arquivo objeto (.o ou .OBJ) ou de biblioteca (.a, .so, .LIB ou .DLL), de modo que o usuário não precisaria conhecer em detalhes o que está dentro dele.
// Arquivo «da_dados.c», usado tão somente para criar uma biblioteca.
#include "da_dados.h"
// Definição do tipo de dados do qual “da_Dados” é um apelido.
struct _da_Dados {
size_t n;
struct { char nome[50], CPF[30]; } *pd;
};
// Definições das funções que foram declaradas em da_dados.h (não vou repetir aqui, pois são iguais ao que já foi escrito acima).
da_Dados *init_da_Dados(void){ /* ... */ }
void free_da_Dados(da_Dados *pda){ /* ... */ }
/* etc. etc. etc. */
No Linux, para gerar uma biblioteca estática (arquivo com sufixo “.a”), você compilaria o código acima para arquivo objeto (sufixo “.o”), e depois geraria incluiria no “
archive ”.
gcc -c da_dados.c # vai gerar da_dados.o.
ar rv libda_dados.a da_dados.o # coloca o objeto dentro do archive (biblioteca).
ranlib libda_dados.a # gera um índice das funções contidas na biblioteca e o coloca dentro do archive.
sudo install da_dados.h /usr/local/include
sudo install libda_dados.a /usr/local/lib
De posse os arquivo de cabeçalho e do arquivo contendo a biblioteca (por exemplo,
libda_dados.a , de acordo com o exemplo acima), você poderia construir um programa que usasse essa biblioteca. A função
main () ficaria nesse programa.
// Arquivo «usa_da_dados.c».
#include <stdio.h>
#include <stdlib.h>
// etc. etc. etc.
// Nosso cabeçalho.
#include <da_dados.h>
int main(void){
da_Dados *pdad=init_da_Dados();
/* ... */
add_read_da_Dados(pdad);
/* ... */
show_da_Dados(pdad);
/* ... */
free_da_Dados(pdad);
}
O executável seria formado pela compilação do código acima e pela adição da biblioteca gerada anteriormente, com um comando parecido com o seguinte (produz o executável chamado
usa_da_dados ).
gcc -I/usr/local/include usa_da_dados.c -L/usr/local/lib -lda_dados -o usa_da_dados
Está ficando difícil! Rsrs
Espero que a evolução indicada acima (faltando vários pedaços, mas era só para dar uma ideia) não o tenha deixado desanimado.
Até porque ainda há muito espaço para melhorias, tais como:
• Em lugar de imprimir mensagens de erro no terminal e abortar o programa, uma biblioteca que se preza deveria deixar o usuário final decidir a melhor fora de tratar eventuais erros. Para tanto, possivelmente seria melhor que algumas das funções tivessem tipos de retorno diferente de
void , a fim de indicar sucesso ou falha por meio de um valor booleano ou inteiro (exceto o construtor, que retorna um ponteiro, que poderia ser um ponteiro nulo em caso de erros) e ajustar o valor da variável global
errno (declarada em <errno.h>) ou criando um campo adicional dentro da
struct da_Dados para indicar erros no objeto, e que poderia ser consultado com uma função chamada
get_status_da_Dados () ou coisa parecida.
• Os nomes das funções estão feios, longos e inconsistentes. Um esquema melhor seria desejável.
• (DISCUTÍVEL) Em vez de ter o campo
pd como um ponteiro que requer uma segunda alocação, com o C99 ele poderia ser transformado num
flexible array member (cf.
https://en.wikipedia.org/wiki/Flexible_array_member ), potencialmente acelerando algumas operações ao economizar um nível de indireção. A implementação fica um pouco mais complexa, mas como já estamos com uma biblioteca especializada, essa complexidade adicional poderia ser tratada dentro da biblioteca, sem transparecer para o usuário.
Referente a usar um outro ponteiro para fazer a realocação, confesso que ainda estou tentando fazer rsrs
Mas isso é só copiar. A forma é exatamente aquela que eu mostrei (e repeti hoje em vários pedaços desta postagem).
... Então Jesus afirmou de novo: “(...) eu vim para que tenham vida, e a tenham plenamente.” (João 10:7-10)