Como entender direito ponteiros? Ajuda em um exemplo falho

1. Como entender direito ponteiros? Ajuda em um exemplo falho

lucas
lucascp2004_mint

(usa Linux Mint)

Enviado em 15/04/2017 - 18:01h


struct lista{
int nota;
struct lista* prox;
};
typedef struct lista Lista;
Lista* inserir (Lista* l,int i);
Lista* inserir (Lista* l,int i){

Lista* novo_elemento;
Lista* temp;
novo_elemento=(Lista*) malloc(sizeof(Lista));
novo_elemento->nota=i;
novo_elemento->prox=NULL;

if (l==NULL){
printf("Chegou aq \n");
l=novo_elemento;
}else{
printf("Chegou aq tb \n");
temp=l;
while(temp->prox!=NULL){
temp=temp->prox;
}
temp->prox=novo_elemento;
}



}
float media (Lista* l);
float media (Lista* l){
int cont,soma;
Lista* copia;
soma=cont=0;


while (l!=NULL){

soma +=l->nota;
cont++;
l=l->prox;
}
return (float) soma/cont;

}


}

int main(int argc, char** argv) {
int nota;
Lista* l;

l=NULL;
printf("Digite a proxima nota. Caso queira encerrar digite 0: ");
scanf("%d",¬a);

while(nota!=0){
inserir(l,nota);
printf("\nDigite a proxima nota. Caso queira encerrar digite 0: ");
scanf("%d",¬a);

}
printf("A média é %.2f",media(l));

return (EXIT_SUCCESS);
}

Estou lendo o livro introdução a estrutura de dados em C e estou com dificuldade em entender completamente lista encadeada e funcionamento de ponteiros. Por exemplo, no código acima a função inserir não inseri o valor no final como tinha planejado. Até sei corrigir o código, mas não sei porque. não sei porque tenho que passar o endereço da variável l para o método inserir para que a lista seja alterada?
Pergunto por que se passo um vetor, não preciso passar o endereço do vetor pra alterar o vetor em um método. Qual é a diferença?

"Ninguém é tão sábio que não tenha nada a aprender ou
tão ignorante que não tenha nada pra ensinar."

"Ninguém é tão sábio que não tenha nada a aprender ou
tão ignorante que não tenha nada pra ensinar."


  


2. Re: Como entender direito ponteiros? Ajuda em um exemplo falho

Bruno Thomaz
SarusKant

(usa CentOS)

Enviado em 16/04/2017 - 12:37h

Ponteiro é a indicação da variável ou to vetor da mesma, sempre que for executado uma ação no ponteiro sera retornado a primeira posição dele.
Para adicionar material ao ponteiro voce deve realocar memória ou alocar de acordo com a quantidade que usará e contar os indices para cada posição exemplo.

Lista *ls = (Lista*) malloc (sizeof(Lista)); // ls terá somente uma posição no ponteiro somente o ls[0];
//para 2 posições
ls = (Lista*) realloc (ls, sizeof(Lista) * 2); // ls terá 2 posições para o ponteiro, ls[0], ls[1].

Você deve sempre alocar a memória que ira usar.

Boa sorte.
--
Bruno Thomaz


3. Re: Como entender direito ponteiros? Ajuda em um exemplo falho

Paulo
paulo1205

(usa Ubuntu)

Enviado em 17/04/2017 - 02:51h

lucascp2004_mint escreveu:

Estou lendo o livro introdução a estrutura de dados em C


Não conheço o livro em questão.

e estou com dificuldade em entender completamente lista encadeada e funcionamento de ponteiros.


Eu acho que ajuda pensar em ponteiros de acordo com a seguinte definição genérica: um ponteiro é um valor que indica uma posição da memória.

Pegando carona nessa definição genérica, pode-se também dizer que variáveis que sejam sempre usadas para armazenar endereços de memória podem ser chamadas de variáveis ponteiros.

No caso particular do C e do C++, sempre que encontra um valor de ponteiro (ou variável que o armazene), o compilador mantém também a informação sobre o tipo do dado que se espera estar contido no endereço apontado.

Por exemplo, no código acima a função inserir não inseri o valor no final como tinha planejado. Até sei corrigir o código, mas não sei porque. não sei porque tenho que passar o endereço da variável l para o método inserir para que a lista seja alterada?


Porque o C sempre envia para as funções meras cópias dos valores dos argumentos. Funções que porventura modifiquem os valores dos argumentos recebidos vão portanto modificar apenas as cópias, não os valores originais.

Quando o valor copiado é um ponteiro, a cópia do ponteiro vai referenciar exatamente o mesmo dado que o ponteiro original. Assim sendo, modificações sobre o conteúdo apontado (não sobre o ponteiro em si!) podem ser feitas dentro da função, com efeito que permanece depois que a função termina de executar.

Pergunto por que se passo um vetor, não preciso passar o endereço do vetor pra alterar o vetor em um método. Qual é a diferença?


Essa é outra particularidade do C e do C++. Quase sempre que o nome de um vetor aparece numa expressão, esse nome é interpretado como se fosse um ponteiro cujo valor aponta para o primeiro elemento do vetor (os três casos em que ele é entendido como vetor são durante sua própria declaração e quando se aplicam diretamente ao nome do vetor os operadores sizeof ou &). Então se o valor já é um ponteiro, ao ser copiado como argumento de função, ele continuará sendo um ponteiro, e permitirá acesso aos elementos do vetor original.


4. Re: Como entender direito ponteiros? Ajuda em um exemplo falho

Paulo
paulo1205

(usa Ubuntu)

Enviado em 17/04/2017 - 14:39h

Caro Bruno,

Acho que sua resposta não tem muito a ver com as perguntas trazidas originalmente, mas não vou entrar nesse assunto.

Quero apenas apontar um problema no breve trecho de código que você mostrou (muito comum, por sinal -- provavelmente você até foi ensinado a fazer desse modo errôneo). Na verdade, um problema mais sério e outros dois relacionados mais a estilo, mas que podem vir a se tornar problemas maiores numa eventual evolução do código.

Lista *ls = (Lista*) malloc (sizeof(Lista)); // ls terá somente uma posição no ponteiro somente o ls[0]; 


Crítica de estilo: em vez de fazer do modo como você fez, prefira o seguinte modo.

Lista *ls=malloc(sizeof *ls); 


Porquês:

1) É o que programadores experientes em C esperariam encontrar. É geralmente bom você trabalhar alinhado com os jargões e construções que a comunidade da linguagem utiliza.

2) Você faz com que o compilador decida o tamanho do dado apontado de acordo com o tipo do próprio ponteiro que vai receber o apontamento, não com um nome de tipo arbirtário. Não é o seu caso, pois a declaração e a definição da valor estão na mesma linha, mas é muito comum num caso genérico que declaração e atribuição fiquem em linhas distintas, e pode acontecer de você fazer a manutenção numa linha e esquecer de a fazer em outra (por exemplo, mudando tipo de dado associado ao ponteiro). Ao diminuir a quantidade de interdependências, você reduz o esforço de manutenção.

3) malloc() devolve um dado do tipo void *, que, em C (mas não em C++), é automaticamente conversível para qualquer outro tipo de ponteiro, de modo que a conversão explícita é redundante. Sendo redundante, aumenta desnecessariamente o esforço de manutenção, facilitando o aparecimento de bugs. Pior ainda: a conversão explícita de ponteiros pode ativamente induzir ao aparecimento de bugs, pois ela desabilita eventuais testes de compatibilidade que o compilador poderia realizar (que não se aplicam a void *, logo não ao resultado de malloc(), mas se aplicam a outros tipos de ponteiros e a combinações perigosas entre ponteiros e inteiros).

Uma razão para que se faça a conversão explícita é permitir o uso de compiladores anteriores à padronização do C (que ocorreu em 1989), uma vez que o suporte a void *, sua semântica e seu uso como parte da biblioteca padrão de funções só ficaram bem definidos após a publicação do padrão ANSI C, posteriormente ratificado como ISO C, em 1990(*). Como, porém, a instituição de void * caminha para completar sua terceira década, é muito improvável que alguém realmente precise trabalhar compiladores obsoletos hoje em dia, logo a conversão explícita não deveria ser feita.

Outra possível justificativa é permitir que o código seja compilado sem erros também em C++. Só que isso vai provocar narizes torcidos tanto na comunidade de programadores em C quanto na dos em C++. Os primeiros vão alegar as mesma coisas alegadas acima, e os mais puristas costumam ter ojeriza a C++, de modo que desprezam a “poluição” provocada pela compatibilidade com a ferramenta que eles abominam. No lado do C++, o ódio ao outro é bem menor, mas geralmente se repudia tanto a conversão de tipos ao estilo do C, por ser insegura (e realmente o é), quanto a gestão de memória baseada em funções da biblioteca. Geralmente se prefere em C++ o uso dos operadores new, para alocação de elementos simples, e new [], para alocação de arrays, e seus respectivos correspondentes de desalocação, delete e delete[], ou então o uso de um objeto de uma dos templates de classes de containers, como std::vector, std::list ou std::deque.

//para 2 posições 
ls = (Lista*) realloc (ls, sizeof(Lista) * 2); // ls terá 2 posições para o ponteiro, ls[0], ls[1].


Aqui o erro principal, que me levou a escrever esta postagem.

Lembre-se que o C sempre passa como parâmetros para as funções meras cópias dos valores daquilo que você colocar na lista de argumentos na hora em que invocar a função. Desse modo, quando você diz “realloc(ls, N)”, você está pegando uma cópia do valor de ls e uma cópia do valor de N, e entregando essas cópias para a função invocada.

A função realloc() foi projetada de modo a tentar fazer a realocação. Quer a realocação tenha sucesso, quer não, a função garante que os dados originais apontados pelo ponteiro para a área que tem de ser realocada serão preservados. Existem três possibilidades de conclusão da tentativa de realocação, a saber:

1) A função consegue alterar diretamente o tamanho da área previamente alocada, sem necessidade de movimentar dados. Nesse caso, o valor devolvido pela função será idêntico ao valor original do ponteiro recebido. Se o tamanho da área apontada tiver sido aumentado, a área de dados original terá o conteúdo preservado, mas o conteúdo da área que lhe foi acrescentada será indefinido (cabendo a você preenchê-lo com valores conhecidos antes de começar a usá-lo como fonte de informação).

2) A função não consegue alterar o tamanho da área já alocada, mas consegue arranjar outra região de memória com tamanho suficiente para acomodar o novo tamanho dos dados. Nesse caso, os dados apontados pelo valor original do ponteiro são copiados para a nova região de memória, do seguinte modo: se a nova área for menor que a antiga, os últimos elementos da área original não são copiados; e a nova área for maior, todos os elementos da antiga são copiados, e o espaço excedente fica com conteúdo indefinido. A região de memória anterior é liberada para uso futuro em outras operações de (re)alocação, e o valor retornado pela função é um ponteiro para a nova área;

3) A função não consegue nenhum espaço para acomodar a quantidade de memória solicitada. Nesse caso, todos os dados apontados pelo ponteiro original continuam íntegras e disponíveis para uso, como se a realocação não tivesse sequer sido tentada. Nesse caso, a função devolve um ponteiro nulo (NULL).

O erro da sua realocação é que ela não prevê a possibilidade (3). Ao atribuir o valor de retorno de realloc() à mesma variável cujo valor você usou como ponteiro a ser realocado, você perde a referência à área que terá sido preservada caso a realocação venha a falhar.

A forma correta de fazer seria mais ou menos a seguinte.

void *new_ptr;
new_ptr=realloc(ptr, new_desired_size * sizeof *ptr);
if(new_ptr){
ptr=new_ptr;
ptr_size=new_desired_size;
}
else{
// Trata o erro de alocação. Dependendo da aplicação,
// o melhor tratamento pode ser tão-somente continuar
// usando o ponteiro anterior.
}
// Usa ptr, ptr[n] e ptr_size.


---------
(*) Antes desse padrão, versões primordiais de malloc() devolviam um valor do tipo char *, que era o tipo de ponteiro mais genérico até então. Muitos dos compiladores da época não ligavam muito para isso, pois havia muito menos opções de diagnóstico de conversões entre ponteiros ou ponteiros e inteiros do que nos compiladores atuais. Havia, contudo, ferramentas especializadas, tais como o lint e o pcc, dedicadas a localizar bugs em potencial e possíveis problemas de portabilidade, e elas alarmavam quando se tentava uma conversão implícita entre tipos de ponteiros distintos. Isso, em parte, foi um dos motivos para que algo como void *, que é um “ponteiro para qualquer coisa” fosse criado.


5. Re: Como entender direito ponteiros? Ajuda em um exemplo falho

lucas
lucascp2004_mint

(usa Linux Mint)

Enviado em 18/04/2017 - 20:15h

Paulo, foram muito boas as suas explicações. Você é um ótimo professor. Entendi tudo o que você disse, mas ainda tenho uma dúvida. Fiz uma alteração no método inserir e o modifiquei assim:


Lista* inserir (Lista* l,int i){

Lista novo_elemento;
Lista* temp;

novo_elemento.nota=i;
novo_elemento.prox=NULL;

if (l==NULL){
printf("Chegou aq \n");
*l=novo_elemento;
}else{
printf("Chegou aq tb \n");

while(l->prox!=NULL){
l=l->prox;
}
*(l)->prox=&novo_elemento; //comando com erro
}
}

Na compilação do código há um erro no último comando. O erro é incompatible types when assigning to type ‘struct lista’ from type ‘Lista * {aka struct lista *}’. É possível eu corrigir o código só alterando o último comando? Caso sim, como?

"Ninguém é tão sábio que não tenha nada a aprender ou
tão ignorante que não tenha nada pra ensinar."


6. Re: Como entender direito ponteiros? Ajuda em um exemplo falho

Paulo
paulo1205

(usa Ubuntu)

Enviado em 19/04/2017 - 11:35h

Eu não intervim no código antes porque você disse que sabia como resolver, e que apenas queria entender os porquês. Tentei explicar esses porquês.

Contudo, vejo que você esta com dificuldade em entender o problema. Deixe-me tentar esclarecer.

Dentro de main, você declara a variável l com o tipo “ponteiro para Lista”, e define para ela o valor inicial NULL, para indicar que a lista está vazia. Até aí, tudo bem.

Quando você chama a função inserir(), na forma “inserir(l, nota)”, você envia uma cópia do valor de l e uma cópia do valor de nota. Como você apenas copia o valor de l, o que quer que aconteça dentro de inserir() garantidamente não vai afetar a lista referida por l dentro de main(), que vai continuar tendo NULL como valor associado.

O fato de uma variável ter um tipo ponteiro não a faz funcionalmente diferente de qualquer outra variável na hora de se chamar uma função: todos os argumentos passados à função continuam sendo apenas cópias dos valores das expressões em que a variável aparece. Se você quiser que uma função modifique o valor de uma variável que aparece na lista de argumentos, tem de passar para a função (uma cópia de) o endereço da variável a ser modificada, em vez do seu valor.

Assim sendo, você provavelmente deveria ter chamado inserir() da seguinte forma.

inserir(&l, nota);  // Copia para a função o endereço de ‘l’ e o valor de ‘nota’) 


Isso obviamente implicaria modificar também a declaração e a implementação da função, que não receberá mais um endereço (ponteiro) de nó da lista, mas sim o endereço (ponteiro) de um ponteiro para nó da lista.

void inserir(Lista **endereco_no_lista, int nota); 


Dentro dessa função de inserção, você provavelmente vai querer verificar/usar o valor do nó da lista ou, em outras palavras, o “valor do conteúdo do endereço” do nó da lista. Como você tem um argumento que é um endereço de nó de lista, você pode obter o conteúdo desse endereço com o operador *. Eis como:

Lista *no;
no=*endereço_no_lista;


-------------------

Note que a função media() não precisa passar por transformação semelhante. Por ser uma função apenas de consulta, ela não precisa modificar o valor de nenhuma variável, e pode trabalhar apenas com as cópias.

-------------------

Tente refazer seu programa usando as informações acima. Tenha sucesso ou não, poste o que você tiver conseguido fazer aqui, para nós analisarmos e podermos ajudar com eventuais melhorias.


7. Re: Como entender direito ponteiros? Ajuda em um exemplo falho

Paulo
paulo1205

(usa Ubuntu)

Enviado em 19/04/2017 - 17:28h

Uma dica extra.

Essa parte de ponteiros é, inicialmente, um pouco confusa, mesmo. Eu me confundia um bocado com ela quando estudei isso na faculdade, porque infelizmente os professores nem sempre têm uma didática muito boa.

Creio que, em grande parte, isso se deve à terminologia adotada. Acho que é importante adotar nomes de tipos e de variáveis que deixem claros os papéis de cada coisa no código.

Note que, na descrição teórica do que eu disse acima, eu procurei distinguir “lista” de “nó”. Isso poderia ser refletido na terminologia usada no próprio código.

Uma lista é formada por nós, e cada nó possui a informação sobre a continuação da lista, com os nós que o seguem.

Eu gosto de pensar que “lista” é sempre a lista inteira, desde o primeiro nó. A partir desse primeiro nó, eu posso percorrer os nós da lista, e cada um desses nós é uma parte da lista, seguida um um “rabicho” ou “cauda” da lista, que também pode ser considerada uma “sublista”. O próprio primeiro nó da lista pode ser entendido como um “nó com cauda”.

Então eis aqui uma forma parecida com a que eu costumo usar:

/*
Interface pública, que o usuário da lista tem de enxergar.

A interface pública geralmente fica num arquivo com sufixo “.h”.
*/

// Crio um alias de nome de tipo, mas não informo ainda os detalhes de implementação.
typedef struct lista LISTA;

// Criação da lista
LISTA *cria_lista(void);

// Inserções:
bool insere_inicio_lista(LISTA *lst, Tipo_do_valor_do_no val); // Inserção no início.
bool insere_fim_lista(LISTA *lst, Tipo_do_valor_do_no val); // Inserção no final.
bool insere_ordenado_lista(LISTA *lst, Tipo_do_valor_do_no val); // Inserção ordenada (precisa de função para comparar valores).

// Consulta (repare no uso do atributo “const”):
unsigned tamanho_lista(const LISTA *lst);
Tipo_do_valor_do_no valor_primeiro_lista(const LISTA *lst);
Tipo_do_valor_do_no valor_ultimo_lista(const LISTA *lst);
bool busca_valor_lista(const LISTA *lst, Tipo_do_valor_do_no val);
bool busca_ordenada_valor_lista(const LISTA *lst, Tipo_do_valor_do_no val);

// Funções para percorrer a lista iterativamente (não posso usar “const”,
// pois vai modificar campo interno que indica qual o valor atual).
void inicia_percurso_lista(LISTA *lst);
bool valor_proximo_lista(LISTA *lst, Tipo_do_valor_do_no *destino); // Pega valor atual e pula para próximo.

// Remoção de valores.
bool remove_inicio_lista(LISTA *lst);
bool remove_fim_lista(LISTA *lst);
bool remove_valor_lista(LISTA *lst, Tipo_do_valor_do_no val);

// Liberação da lista
void libera_lista(LISTA *lst);


/*
Implementação (de algumas) das funções da lista (algumas porque o
exercício não é meu, e não convém que eu mostre a você tintim-por-
tintim como as implementar).

Tipicamente reside um arquivo com sufixo “.c”.

A implementação normalmente não precisa ser conhecida do usuário
da lista, i.e. ele não precisa saber sobre os nós, sobre sublistas ou
caudas, nem sobre os campos internos da lista como um todo.

No seu caso, porém, a implementação é uma tarefa que você tem de
fazer, então você terá de implementar cada uma das funções acima,
além de definir o tipo “struct lista” que tinha ficado obscuro (graças ao
uso de ponteiros).
*/

// Estrutura representando um nó que tem uma cauda.
struct no_com_cauda {
Tipo_do_valor_do_no valor;
struct no_com_cauda * prox;
}

// Estrutura representando a lista.
struct lista {
// Guardo o último porque isso acelera operações de inserir no final.
// O atual é para as operações de percorrer a lista iterativamente.
struct no_com_cauda *primeiro, *ultimo, *atual;
unsigned n_nos;
};


// Função de criação da lista.
LISTA *cria_lista(void){
LISTA *lst=malloc(sizeof *lst);
if(lst){
lst->primeiro=lst->ultimo=lst->atual=NULL;
lst->n_nos=0;
}
return lst;
}

// Função de inserção no fim da lista.
bool insere_fim_lista(LISTA *lst, Tipo_do_valor_do_no val){
struct no_com_cauda *novo_no=malloc(sizeof *novo_no);
if(!novo_no)
return false; // Sai sinalizando falha.
novo_no->valor=val;
novo_no->prox=NULL;
if(!lst->n_nos)
lst->primeiro=novo_no;
else
lst->ultimo->prox=novo_no;
lst->ultimo=novo_no;
lst->n_nos++;
return true; // Sai sinalizando sucesso.
}

// Procura um determinado valor numa lista ordenada.
bool busca_ordenada_valor_lista(const LISTA *lst, Tipo_do_valor_do_no val){
const struct no_com_cauda *no=lst->primeiro;
while(no && no->valor<val)
no=no->prox;
return no && no->valor==val;
}

// Pega valor atual e pula para próximo. Retorna valor indicando se
// a operação foi bem-sucedida.
bool valor_proximo_lista(LISTA *lst, Tipo_do_valor_do_no *destino){
if(!lst->atual)
return false; // Fim da lista atingido.
// Se o ponteiro para o destino for nulo, não salva valor do no atual,
// mas ainda avança para o próximo nó.
if(destino)
*destino=lst->atual->valor;
lst->atual=lst->atual->prox;
return true;
}



8. Re: Como entender direito ponteiros? Ajuda em um exemplo falho

Paulo
paulo1205

(usa Ubuntu)

Enviado em 20/04/2017 - 00:31h

Corrigi um bug que havia na função de inserção no fim da lista na mensagem anterior.

Aproveitei também para acrescentar um exemplo de função de busca numa lista cujos elementos estejam ordenados, para mostrar o uso de ponteiros para dados constantes, tanto na passagem da lista como argumento quanto na hora de percorrer seus nós.


9. Re: Como entender direito ponteiros? Ajuda em um exemplo falho

Perfil removido
removido

(usa Nenhuma)

Enviado em 20/04/2017 - 00:42h

Não testei, mas ao invés de

struct lista{
int nota;
struct lista* prox;
}


não era para ser

struct lista{
int nota;
struct lista *prox;
}


?

----------------------------------------------------------------------------------------------------------------
Nem direita, nem esquerda. Quando se trata de corrupção o Brasil é ambidestro.
(anônimo)

Encryption works. Properly implemented strong crypto systems are one of the few things that you can rely on. Unfortunately, endpoint security is so terrifically weak that NSA can frequently find ways around it. — Edward Snowden



10. Re: Como entender direito ponteiros? Ajuda em um exemplo falho

Paulo
paulo1205

(usa Ubuntu)

Enviado em 20/04/2017 - 01:14h

listeiro_037 escreveu:

não era para ser

struct lista{
int nota;
struct lista *prox;
}


Para o compilador, tanto faz -- é mera questão de espaçamento entre tokens.

Eu prefiro a forma que você mostrou, porque o asterisco está mais ligado à variável do que às palavras que apresentam o tipo.

Para dar uma ideia do que estou querendo dizer, imagine a seguinte declaração.

int* a, b, **c, d[10]; 


Quais os tipos de cada uma das variáveis acima?

A forma da declaração pode dar a impressão de que a e b tem o mesmo tipo, e de que esse tipo seria “int*”. Pegando carona na mesma interpretação, o tipo de d seria array de ponteiros, e o de c seria o mais difícil de interpretar, mas poderia ser um ponteiro para ponteiro para ponteiro para inteiro.

Mas não é nada disso: apesar de estar junto à palavra reservada int, o asterisco se aplica apenas a a; o tipo de b é apenas int, c tem apenas dois níveis de ponteiros, e d é um array de inteiros.

Então, eu sempre advogo deixar os asteriscos dos ponteiros junto aos nomes das variáveis, não junto ao tipo dos dados.






Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts