Por que os indicadores são um fator de confusão tão importante para muitos estudantes de nível universitário, novos e até antigos, em C ou C ++? Existem ferramentas ou processos de pensamento que o ajudaram a entender como os ponteiros funcionam na variável, função e além do nível?
Quais são algumas das boas práticas que podem ser feitas para levar alguém ao nível de "Ah-hah, entendi", sem atolá-lo no conceito geral? Basicamente, faça drill como cenários.
Respostas:
Ponteiros é um conceito que para muitos pode ser confuso no início, principalmente quando se trata de copiar valores de ponteiros e ainda fazer referência ao mesmo bloco de memória.
Descobri que a melhor analogia é considerar o ponteiro como um pedaço de papel com um endereço residencial e o bloco de memória que ele faz referência como a casa real. Todos os tipos de operações podem ser facilmente explicados.
Adicionei algum código Delphi abaixo e alguns comentários, quando apropriado. Eu escolhi o Delphi porque minha outra linguagem de programação principal, C #, não exibe coisas como vazamentos de memória da mesma maneira.
Se você deseja apenas aprender o conceito de alto nível de ponteiros, deve ignorar as partes rotuladas como "Layout da memória" na explicação abaixo. Eles têm como objetivo dar exemplos de como a memória pode parecer após as operações, mas são de natureza mais baixa. No entanto, para explicar com precisão como as excedentes de buffer realmente funcionam, era importante que eu adicionasse esses diagramas.
Isenção de responsabilidade: para todos os efeitos, esta explicação e os layouts de memória de exemplo são bastante simplificados. Há mais sobrecarga e muito mais detalhes que você precisa saber se precisar lidar com a memória em um nível baixo. No entanto, para a intenção de explicar a memória e os ponteiros, é preciso o suficiente.
Vamos supor que a classe THouse usada abaixo seja assim:
Quando você inicializa o objeto de casa, o nome dado ao construtor é copiado no campo privado FName. Há uma razão para ele ser definido como uma matriz de tamanho fixo.
Na memória, haverá alguma sobrecarga associada à alocação da casa, ilustrarei isso abaixo desta forma:
A área "tttt" é aérea, normalmente haverá mais disso para vários tipos de tempos de execução e idiomas, como 8 ou 12 bytes. É imperativo que quaisquer valores armazenados nesta área nunca sejam alterados por algo que não seja o alocador de memória ou as rotinas do sistema principal, ou você corre o risco de travar o programa.
Alocar memória
Peça a um empresário que construa sua casa e forneça o endereço da casa. Ao contrário do mundo real, a alocação de memória não pode ser informada de onde alocar, mas encontrará um local adequado com espaço suficiente e reportará o endereço à memória alocada.
Em outras palavras, o empreendedor escolherá o local.
Layout da memória:
Mantenha uma variável com o endereço
Escreva o endereço da sua nova casa em um pedaço de papel. Este documento servirá como referência para sua casa. Sem esse pedaço de papel, você está perdido e não consegue encontrar a casa, a menos que já esteja nela.
Layout da memória:
Copiar valor do ponteiro
Basta escrever o endereço em um novo pedaço de papel. Agora você tem dois pedaços de papel que o levarão à mesma casa, não duas casas separadas. Qualquer tentativa de seguir o endereço de um jornal e reorganizar os móveis daquela casa fará parecer que a outra casa foi modificada da mesma maneira, a menos que você possa detectar explicitamente que na verdade é apenas uma casa.
Nota Geralmente, esse é o conceito que eu tenho mais problemas para explicar às pessoas: dois ponteiros não significam dois objetos ou blocos de memória.
Liberando a memória
Demolir a casa. Mais tarde, você poderá reutilizar o papel para um novo endereço, se desejar, ou limpá-lo para esquecer o endereço da casa que não existe mais.
Aqui, primeiro construo a casa e pego o endereço. Depois faço algo em casa (use-o, o código ... deixado como exercício para o leitor) e depois libero-o. Por fim, apago o endereço da minha variável.
Layout da memória:
Ponteiros pendurados
Você diz ao seu empresário para destruir a casa, mas esquece de apagar o endereço do seu pedaço de papel. Quando, mais tarde, você olha o pedaço de papel, esquece que a casa não está mais lá e vai visitá-lo, com resultados fracassados (veja também a parte sobre uma referência inválida abaixo).
O uso
h
após a chamada para.Free
pode funcionar, mas isso é pura sorte. Provavelmente, falhará, no local do cliente, no meio de uma operação crítica.Como você pode ver, h ainda aponta para os restos dos dados na memória, mas como eles podem não estar completos, usá-los como antes pode falhar.
Vazamento de memória
Você perde o pedaço de papel e não consegue encontrar a casa. Porém, a casa ainda está em algum lugar, e quando você mais tarde quiser construir uma nova casa, não poderá reutilizar esse local.
Aqui, substituímos o conteúdo da
h
variável pelo endereço de uma casa nova, mas a antiga ainda está de pé ... em algum lugar. Após esse código, não há como chegar a essa casa e ela ficará em pé. Em outras palavras, a memória alocada permanecerá alocada até o fechamento do aplicativo, momento em que o sistema operacional a derrubará.Layout de memória após a primeira alocação:
Layout de memória após a segunda alocação:
Uma maneira mais comum de obter esse método é esquecer de liberar algo, em vez de substituí-lo como acima. Em termos de Delphi, isso ocorrerá com o seguinte método:
Após a execução desse método, não há lugar em nossas variáveis que exista o endereço da casa, mas a casa ainda está lá fora.
Layout da memória:
Como você pode ver, os dados antigos são deixados intactos na memória e não serão reutilizados pelo alocador de memória. O alocador controla quais áreas da memória foram usadas e não as reutilizará, a menos que você a libere.
Liberando a memória, mas mantendo uma referência (agora inválida)
Demolir a casa, apagar um dos pedaços de papel, mas você também tem outro pedaço de papel com o endereço antigo; quando você vai para o endereço, não encontra uma casa, mas pode encontrar algo que se assemelha às ruínas de Um.
Talvez você até encontre uma casa, mas não é a casa para a qual foi originalmente fornecido o endereço e, portanto, qualquer tentativa de usá-la como se pertencesse a você pode falhar terrivelmente.
Às vezes, você pode até achar que um endereço vizinho possui uma casa bastante grande, que ocupa três endereços (Main Street 1-3), e seu endereço fica no meio da casa. Qualquer tentativa de tratar essa parte da grande casa de três endereços como uma única casa pequena também pode falhar terrivelmente.
Aqui a casa foi demolida, através da referência
h1
e, embora tenhah1
sido limpa também,h2
ainda tem o endereço antigo e desatualizado. O acesso à casa que não está mais em pé pode ou não funcionar.Esta é uma variação do ponteiro pendente acima. Veja seu layout de memória.
Saturação de buffer
Você move mais coisas para dentro da casa do que pode caber, derramando na casa ou no quintal dos vizinhos. Quando o dono daquela casa vizinha mais tarde voltar para casa, ele encontrará todo tipo de coisa que considerará sua.
Foi por esse motivo que escolhi uma matriz de tamanho fixo. Para preparar o cenário, suponha que a segunda casa que alocamos será, por algum motivo, colocada antes da primeira na memória. Em outras palavras, a segunda casa terá um endereço mais baixo que o primeiro. Além disso, eles são alocados um ao lado do outro.
Assim, este código:
Layout de memória após a primeira alocação:
Layout de memória após a segunda alocação:
A parte que mais frequentemente causa falha é quando você substitui partes importantes dos dados armazenados que realmente não devem ser alteradas aleatoriamente. Por exemplo, pode não ser um problema que partes do nome da h1-house tenham sido alteradas, em termos de travamento do programa, mas a substituição da sobrecarga do objeto provavelmente travará quando você tentar usar o objeto quebrado, como também substituindo links armazenados em outros objetos no objeto.
Listas vinculadas
Quando você segue um endereço em um pedaço de papel, chega a uma casa, e nessa casa há outro pedaço de papel com um novo endereço, para a próxima casa da cadeia, e assim por diante.
Aqui, criamos um link da nossa casa para nossa cabine. Podemos seguir a cadeia até que uma casa não tenha
NextHouse
referência, o que significa que é a última. Para visitar todas as nossas casas, poderíamos usar o seguinte código:Layout da memória (adicionado NextHouse como um link no objeto, anotado com os quatro LLLLs no diagrama abaixo):
Em termos básicos, o que é um endereço de memória?
Um endereço de memória é, em termos básicos, apenas um número. Se você pensa na memória como uma grande matriz de bytes, o primeiro byte tem o endereço 0, o próximo o endereço 1 e assim por diante. Isso é simplificado, mas bom o suficiente.
Portanto, este layout de memória:
Pode ter esses dois endereços (o mais à esquerda - é o endereço 0):
O que significa que nossa lista vinculada acima pode se parecer com a seguinte:
É comum armazenar um endereço que "aponte para lugar nenhum" como um endereço zero.
Em termos básicos, o que é um ponteiro?
Um ponteiro é apenas uma variável que contém um endereço de memória. Normalmente, você pode pedir à linguagem de programação que lhe forneça seu número, mas a maioria das linguagens e tempos de execução tenta ocultar o fato de que há um número abaixo, apenas porque o número em si não tem nenhum significado para você. É melhor pensar em um ponteiro como uma caixa preta, ou seja. você realmente não sabe nem se importa com como ele é realmente implementado, desde que funcione.
fonte
Na minha primeira aula de Comp Sci, fizemos o seguinte exercício. É verdade que era uma sala de palestras com cerca de 200 alunos ...
O professor escreve no quadro:
int john;
John se levanta
Professor escreve:
int *sally = &john;
Sally se levanta, aponta para john
Professor:
int *bill = sally;
Bill se levanta, aponta para John
Professor:
int sam;
Sam se levanta
Professor:
bill = &sam;
Bill agora aponta para Sam.
Eu acho que você entendeu a ideia. Acho que passamos cerca de uma hora fazendo isso, até examinarmos o básico da atribuição de ponteiros.
fonte
Uma analogia que eu achei útil para explicar os ponteiros são os hiperlinks. A maioria das pessoas pode entender que um link em uma página da Web 'aponta' para outra página na Internet e, se você pode copiar e colar esse hiperlink, ambas apontam para a mesma página da Web original. Se você editar a página original, siga um desses links (ponteiros) para obter a nova página atualizada.
fonte
int *a = b
não faz duas cópias*b
).A razão pela qual os indicadores parecem confundir tantas pessoas é que elas geralmente vêm com pouco ou nenhum conhecimento em arquitetura de computadores. Como muitos não parecem ter uma idéia de como os computadores (a máquina) são realmente implementados - trabalhar em C / C ++ parece estranho.
Uma dica é pedir que eles implementem uma máquina virtual simples baseada em bytecode (em qualquer idioma que eles escolherem, o python funciona muito bem para isso) com um conjunto de instruções focado nas operações do ponteiro (carregamento, armazenamento, endereçamento direto / indireto). Depois, peça que eles escrevam programas simples para esse conjunto de instruções.
Qualquer coisa que exija um pouco mais do que uma simples adição envolverá ponteiros e eles certamente a entenderão.
fonte
O conceito de um espaço reservado para um valor - variáveis - mapeia algo que aprendemos na escola - álgebra. Não existe um paralelo existente que você possa desenhar sem entender como a memória é fisicamente distribuída em um computador, e ninguém pensa sobre esse tipo de coisa até lidar com coisas de baixo nível - no nível de comunicação C / C ++ / byte .
Caixas de endereços. Lembro que quando eu estava aprendendo a programar o BASIC em microcomputadores, havia esses lindos livros com jogos e, às vezes, era preciso colocar valores em endereços específicos. Eles tinham uma foto de um monte de caixas, rotuladas incrementalmente com 0, 1, 2 ... e foi explicado que apenas uma pequena coisa (um byte) poderia caber nessas caixas, e havia muitas delas - alguns computadores tinha até 65535! Eles estavam próximos um do outro e todos tinham um endereço.
Para uma broca? Faça uma estrutura:
Mesmo exemplo que acima, exceto em C:
Resultado:
Talvez isso explique alguns dos princípios básicos através do exemplo?
fonte
A razão pela qual tive dificuldade em entender os ponteiros, a princípio, é que muitas explicações incluem muita porcaria sobre a passagem por referência. Tudo isso faz é confundir o problema. Quando você usa um parâmetro de ponteiro, ainda está passando por valor; mas o valor passa a ser um endereço e não, digamos, um int.
Alguém já se vinculou a este tutorial, mas posso destacar o momento em que comecei a entender os ponteiros:
Um tutorial sobre ponteiros e matrizes em C: Capítulo 3 - Ponteiros e seqüências de caracteres
No momento em que li essas palavras, as nuvens se separaram e um raio de sol me envolveu com a compreensão dos ponteiros.
Mesmo se você for um desenvolvedor de VB .NET ou C # (como eu) e nunca usar código inseguro, ainda vale a pena entender como os ponteiros funcionam, ou você não entenderá como as referências a objetos funcionam. Então você terá a noção comum, mas equivocada, de que a passagem de uma referência a um método copia o objeto.
fonte
Eu achei o "Tutorial sobre ponteiros e matrizes em C" de Ted Jensen um excelente recurso para aprender sobre ponteiros. Ele é dividido em 10 lições, começando com uma explicação do que são os ponteiros (e para que servem) e terminando com os ponteiros de função.http://home.netcom.com/~tjensen/ptr/cpoint.htm
A partir daí, o Guia de Programação de Rede de Beej ensina a API de soquetes Unix, a partir da qual você pode começar a fazer coisas realmente divertidas. http://beej.us/guide/bgnet/
fonte
As complexidades dos indicadores vão além do que podemos ensinar com facilidade. Fazer os alunos apontarem um para o outro e usar pedaços de papel com os endereços das casas é uma ótima ferramenta de aprendizado. Eles fazem um ótimo trabalho ao introduzir os conceitos básicos. De fato, aprender os conceitos básicos é vital para o sucesso do uso de ponteiros. No entanto, no código de produção, é comum entrar em cenários muito mais complexos do que essas demonstrações simples podem encapsular.
Eu estive envolvido com sistemas nos quais tínhamos estruturas apontando para outras estruturas apontando para outras estruturas. Algumas dessas estruturas também continham estruturas incorporadas (em vez de ponteiros para estruturas adicionais). É aqui que os ponteiros ficam realmente confusos. Se você possui vários níveis de indireção e começa a terminar com um código como este:
pode ficar confuso muito rapidamente (imagine muito mais linhas e potencialmente mais níveis). Lance matrizes de ponteiros e ponteiros nó a nó (árvores, listas vinculadas) e isso piora ainda mais. Vi alguns desenvolvedores realmente bons se perderem quando começaram a trabalhar em tais sistemas, até mesmo desenvolvedores que entenderam o básico muito bem.
Estruturas complexas de ponteiros também não indicam necessariamente uma codificação ruim (embora possam). A composição é uma peça vital da boa programação orientada a objetos e, em linguagens com ponteiros brutos, inevitavelmente levará a indiretas em várias camadas. Além disso, os sistemas geralmente precisam usar bibliotecas de terceiros com estruturas que não coincidem entre si em estilo ou técnica. Em situações como essa, a complexidade naturalmente surgirá (embora certamente devamos combatê-la o máximo possível).
Acho que a melhor coisa que as faculdades podem fazer para ajudar os alunos a aprender indicadores é usar boas demonstrações, combinadas com projetos que exigem o uso de indicadores. Um projeto difícil fará mais para compreender os ponteiros do que mil demonstrações. As demonstrações podem lhe proporcionar uma compreensão superficial, mas para entender profundamente os indicadores, você precisa realmente usá-los.
fonte
Pensei em acrescentar uma analogia a esta lista que achei muito útil ao explicar os ponteiros (na época) como um tutor de ciências da computação; primeiro, vamos:
Prepare o palco :
Considere um estacionamento com 3 vagas, essas vagas são numeradas:
De certa forma, isso é como locais de memória, eles são seqüenciais e contíguos ... mais ou menos como um array. No momento, não há carros neles, então é como uma matriz vazia (
parking_lot[3] = {0}
).Adicione os dados
Um estacionamento nunca fica vazio por muito tempo ... se o fizesse, seria inútil e ninguém construiria nenhum. Então, digamos que, à medida que o dia avança, o lote se enche de 3 carros, um carro azul, um carro vermelho e um carro verde:
Estes carros são todos do mesmo tipo (de carro) para que uma maneira de pensar disso é que nossos carros são algum tipo de dados (dizer um
int
) mas eles têm valores diferentes (blue
,red
,green
, que poderia ser uma corenum
)Digite o ponteiro
Agora, se eu te levar para esse estacionamento e pedir que você me encontre um carro azul, você estende um dedo e o usa para apontar para um carro azul no ponto 1. É como pegar um ponteiro e atribuí-lo a um endereço de memória (
int *finger = parking_lot
)Seu dedo (o ponteiro) não é a resposta para minha pergunta. Olhar para o seu dedo não me diz nada, mas se eu olhar onde você está, o dedo está apontando (sem referência ao ponteiro), posso encontrar o carro (os dados) que estava procurando.
Reatribuindo o ponteiro
Agora, peço que você encontre um carro vermelho e redirecione o dedo para um carro novo. Agora seu ponteiro (o mesmo de antes) está me mostrando novos dados (o local de estacionamento onde o carro vermelho pode ser encontrado) do mesmo tipo (o carro).
O ponteiro não mudou fisicamente, ainda é seu dedo, apenas os dados que estavam me mostrando mudaram. (o endereço do "local de estacionamento")
Ponteiros duplos (ou um ponteiro para um ponteiro)
Isso funciona com mais de um ponteiro também. Posso perguntar onde está o ponteiro, que está apontando para o carro vermelho, e você pode usar a outra mão e apontar com o dedo para o primeiro dedo. (isso é como
int **finger_two = &finger
)Agora, se eu quiser saber onde está o carro azul, posso seguir a direção do primeiro dedo para o segundo dedo, para o carro (os dados).
O ponteiro pendente
Agora, digamos que você esteja se sentindo muito parecido com uma estátua e queira segurar sua mão apontando indefinidamente para o carro vermelho. E se aquele carro vermelho for embora?
Seu ponteiro ainda está apontando para onde o carro vermelho estava, mas não está mais. Digamos que um carro novo chegue lá ... um carro da Orange. Agora, se eu perguntar novamente: "onde está o carro vermelho", você ainda está apontando para lá, mas agora está errado. Não é um carro vermelho, é laranja.
Aritmética do ponteiro
Ok, então você ainda está apontando para o segundo estacionamento (agora ocupado pelo carro Orange)
Bem, agora tenho uma nova pergunta ... quero saber a cor do carro na próxima vaga de estacionamento. Você pode ver que está apontando para o ponto 2, basta adicionar 1 e apontar para o próximo ponto. (
finger+1
), agora como eu queria saber quais eram os dados, você deve verificar esse ponto (não apenas o dedo) para deferir o ponteiro (*(finger+1)
) para ver se há um carro verde lá (os dados naquele local) )fonte
"without getting them bogged down in the overall concept"
como um entendimento de alto nível. E para o seu ponto:"I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"
- Você ficaria muito surpreso quantas pessoas não entendem ponteiros mesmo a este nívelEu não acho que os ponteiros como conceito sejam particularmente complicados - a maioria dos modelos mentais dos estudantes é mapeada para algo assim e alguns esboços rápidos podem ajudar.
A dificuldade, pelo menos a que experimentei no passado e vi outras pessoas lidando, é que o gerenciamento de ponteiros em C / C ++ pode ser desnecessariamente complicado.
fonte
Um exemplo de tutorial com um bom conjunto de diagramas ajuda bastante no entendimento dos ponteiros .
Joel Spolsky faz bons pontos sobre como entender os ponteiros em seu artigo Guerrilla Guide to Interviewing :
fonte
O problema com os ponteiros não é o conceito. É a execução e a linguagem envolvidas. Confusão adicional resulta quando os professores assumem que é difícil o CONCEITO de indicadores, e não o jargão ou a bagunça complicada que C e C ++ fazem do conceito. Tantos esforços são dedicados a explicar o conceito (como na resposta aceita para esta pergunta) e é praticamente desperdiçado em alguém como eu, porque eu já entendo tudo isso. Está apenas explicando a parte errada do problema.
Para ter uma idéia de onde eu venho, sou alguém que entende perfeitamente os ponteiros e posso usá-los com competência na linguagem assembler. Porque na linguagem assembler, eles não são referidos como ponteiros. Eles são chamados de endereços. Quando se trata de programar e usar ponteiros em C, eu cometo muitos erros e fico realmente confuso. Ainda não resolvi isso. Deixe-me lhe dar um exemplo.
Quando uma API diz:
o que ele quer?
poderia querer:
um número que representa um endereço para um buffer
(Para dizer isso, eu digo
doIt(mybuffer)
oudoIt(*myBuffer)
?)um número que representa o endereço para um endereço para um buffer
(é isso
doIt(&mybuffer)
oudoIt(mybuffer)
oudoIt(*mybuffer)
?)um número que representa o endereço para o endereço para o buffer
(talvez seja
doIt(&mybuffer)
. ou édoIt(&&mybuffer)
? ou atédoIt(&&&mybuffer)
)e assim por diante, e o idioma envolvido não deixa isso claro, porque envolve as palavras "ponteiro" e "referência" que não têm tanto significado e clareza para mim quanto "x mantém o endereço de y" e " esta função requer um endereço para y ". A resposta também depende apenas do que diabos "mybuffer" é para começar e do que ele pretende fazer. O idioma não suporta os níveis de aninhamento encontrados na prática. Como quando eu tenho que entregar um "ponteiro" para uma função que cria um novo buffer e modifica o ponteiro para apontar para o novo local do buffer. Ele realmente deseja o ponteiro ou um ponteiro para o ponteiro, para que ele saiba para onde modificar o conteúdo do ponteiro. Na maioria das vezes, tenho que adivinhar o que se entende por "
"Ponteiro" está sobrecarregado demais. Um ponteiro é um endereço para um valor? ou é uma variável que contém um endereço para um valor. Quando uma função deseja um ponteiro, deseja o endereço que a variável de ponteiro contém ou deseja o endereço para a variável de ponteiro? Estou confuso.
fonte
double *(*(*fn)(int))(char)
, então o resultado da avaliação*(*(*fn)(42))('x')
será adouble
. Você pode retirar as camadas de avaliação para entender quais devem ser os tipos intermediários.(*(*fn)(42))('x')
então?x
) em que, se você avaliar*x
, recebe uma duplicação.fn
é e mais em termos do que você pode fazer comfn
Eu acho que a principal barreira para entender os ponteiros são os maus professores.
Quase todo mundo é ensinado a mentir sobre ponteiros: que eles nada mais são do que endereços de memória ou que permitem que você aponte para locais arbitrários .
E é claro que eles são difíceis de entender, perigosos e semi-mágicos.
Nada disso é verdade. Os ponteiros são na verdade conceitos bastante simples, desde que você se atenha ao que a linguagem C ++ tem a dizer sobre eles e não os importe com atributos que "geralmente" acabam funcionando na prática, mas, no entanto, não são garantidos pela linguagem e, portanto, não fazem parte do conceito real de ponteiro.
Tentei escrever uma explicação sobre isso há alguns meses atrás neste blog - espero que ajude alguém.
(Observe, antes que alguém me pedante, sim, o padrão C ++ diz que ponteiros representam endereços de memória. Mas não diz que "ponteiros são endereços de memória e nada além de endereços de memória e podem ser usados ou pensados de forma intercambiável com memória endereços ". A distinção é importante)
fonte
Eu acho que o que torna os ponteiros difíceis de aprender é que até os ponteiros você se sentir confortável com a idéia de que "nesta localização da memória há um conjunto de bits que representam um int, um duplo, um caractere, o que for".
Quando você vê um ponteiro pela primeira vez, você realmente não entende o que está no local da memória. "Como assim, possui um endereço ?"
Não concordo com a noção de que "você os aceita ou não".
Eles se tornam mais fáceis de entender quando você começa a encontrar usos reais para eles (como não passar grandes estruturas para as funções).
fonte
A razão pela qual é tão difícil de entender não é porque é um conceito difícil, mas porque a sintaxe é inconsistente .
Você aprendeu primeiro que a parte mais à esquerda de uma criação de variável define o tipo da variável. A declaração do ponteiro não funciona assim em C e C ++. Em vez disso, eles dizem que a variável está apontando no tipo para a esquerda. Nesse caso:
*
mypointer está apontando em um int.Não compreendi completamente os ponteiros até tentar usá-los em C # (com segurança), eles funcionam exatamente da mesma maneira, mas com sintaxe lógica e consistente. O ponteiro é um tipo em si. Aqui mypointer é um ponteiro para um int.
Nem me inicie em indicadores de função ...
fonte
int *p;
tem um significado simples:*p
é um número inteiro.int *p, **pp
significa:*p
e**pp
são inteiros.*p
e não**pp
são números inteiros, porque você nunca inicializou ou ou aponta para algo. Entendo por que algumas pessoas preferem seguir a gramática desta, principalmente porque alguns casos extremos e casos complexos exigem que você faça isso (embora, ainda assim, você possa contornar isso trivialmente em todos os casos que conheço) ... mas não acho que esses casos sejam mais importantes do que o fato de que ensinar o alinhamento correto é enganoso para iniciantes. Sem mencionar que é feio! :)p
pp
*pp
Eu poderia trabalhar com ponteiros quando conhecia apenas C ++. Eu meio que sabia o que fazer em alguns casos e o que não fazer por tentativa / erro. Mas o que me deu um entendimento completo é a linguagem assembly. Se você fizer alguma depuração séria no nível de instruções com um programa de linguagem assembly que você escreveu, poderá entender muitas coisas.
fonte
Gosto da analogia do endereço residencial, mas sempre pensei no endereço da própria caixa de correio. Dessa forma, você pode visualizar o conceito de desreferenciar o ponteiro (abrir a caixa de correio).
Por exemplo, seguindo uma lista vinculada: 1) comece com o seu papel com o endereço 2) Vá para o endereço no papel 3) Abra a caixa de correio para encontrar um novo pedaço de papel com o próximo endereço.
Em uma lista vinculada linear, a última caixa de correio não possui nada (final da lista). Em uma lista vinculada circular, a última caixa de correio possui o endereço da primeira caixa de correio.
Observe que a etapa 3 é onde a desreferência ocorre e onde você trava ou falha quando o endereço é inválido. Supondo que você possa ir até a caixa de correio com um endereço inválido, imagine que haja um buraco negro ou algo que vire o mundo de dentro para fora :)
fonte
Penso que a principal razão pela qual as pessoas têm problemas é porque geralmente não é ensinado de maneira interessante e envolvente. Eu gostaria de ver um professor obter 10 voluntários da multidão e dar a eles uma régua de 1 metro cada, fazer com que eles fiquem em uma determinada configuração e use as réguas para apontar uma para a outra. Em seguida, mostre a aritmética dos ponteiros movendo as pessoas (e para onde elas apontam suas réguas). Seria uma maneira simples, mas eficaz (e acima de tudo memorável) de mostrar os conceitos sem ficar muito atolada na mecânica.
Quando você chega ao C e C ++, parece que fica mais difícil para algumas pessoas. Não tenho certeza se isso é porque eles estão finalmente colocando em prática a teoria de que não entendem adequadamente ou porque a manipulação de ponteiros é inerentemente mais difícil nessas línguas. Não me lembro bem da minha própria transição, mas conhecia dicas em Pascal e depois mudei para C e me perdi totalmente.
fonte
Não acho que os ponteiros sejam confusos. A maioria das pessoas pode entender o conceito. Agora, com quantos indicadores você consegue pensar ou com quantos níveis de indireção você se sente confortável? Não é preciso muito para colocar as pessoas além do limite. O fato de eles poderem ser alterados acidentalmente por bugs no seu programa também pode dificultar a depuração quando algo der errado no seu código.
fonte
Eu acho que pode realmente ser um problema de sintaxe. A sintaxe C / C ++ para ponteiros parece inconsistente e mais complexa do que precisa.
Ironicamente, o que realmente me ajudou a entender os ponteiros foi encontrar o conceito de um iterador na biblioteca de modelos padrão do c ++ . É irônico, porque só posso assumir que os iteradores foram concebidos como uma generalização do ponteiro.
Às vezes você simplesmente não pode ver a floresta até aprender a ignorar as árvores.
fonte
(*p)
teria sido(p->)
, e assim teríamosp->->x
em vez da ambígua*p->x
a->b
simplesmente significa(*a).b
.* p->x
significa* ((*a).b)
que*p -> x
significa meios(*(*p)) -> x
. A mistura de operadores de prefixo e postfix causa uma análise ambígua.1+2 * 3
deve ser 9.A confusão vem das múltiplas camadas de abstração misturadas no conceito "ponteiro". Os programadores não ficam confusos com as referências comuns em Java / Python, mas os indicadores são diferentes, pois expõem características da arquitetura de memória subjacente.
É um bom princípio separar claramente as camadas de abstração, e os ponteiros não fazem isso.
fonte
foo[i]
significa ir a um determinado ponto, avançar uma certa distância e ver o que está lá. O que complica as coisas é a camada de abstração extra muito mais complicada que foi adicionada pelo Padrão apenas para o benefício do compilador, mas modela as coisas de uma maneira que é inadequada para as necessidades do programador e as necessidades do compilador.O jeito que eu gostava de explicar era em termos de matrizes e índices - as pessoas podem não estar familiarizadas com os ponteiros, mas geralmente sabem o que é um índice.
Então, digamos que a RAM seja uma matriz (e você tenha apenas 10 bytes de RAM):
Então, um ponteiro para uma variável é realmente apenas o índice (o primeiro byte) dessa variável na RAM.
Portanto, se você tem um ponteiro / índice
unsigned char index = 2
, então o valor é obviamente o terceiro elemento, ou o número 4. Um ponteiro para um ponteiro é onde você pega esse número e o usa como um índice, por exemploRAM[RAM[index]]
.Eu desenharia uma matriz em uma lista de papéis e apenas a utilizava para mostrar coisas como muitos ponteiros apontando para a mesma memória, aritmética de ponteiro, ponteiro para ponteiro e assim por diante.
fonte
Número da caixa postal.
É uma informação que permite acessar outra coisa.
(E se você fizer aritmética nos números das caixas postais, poderá ter um problema, porque a letra está na caixa errada. E se alguém se mudar para outro estado - sem endereço de encaminhamento -, você terá um ponteiro pendente. por outro lado - se os correios encaminharem o correio, você terá um ponteiro para um ponteiro.)
fonte
Não é uma maneira ruim de entender, por meio de iteradores ... mas continue procurando, você verá Alexandrescu começar a reclamar deles.
Muitos desenvolvedores ex-C ++ (que nunca entenderam que os iteradores são um ponteiro moderno antes de despejar o idioma) saltam para C # e ainda acreditam que têm iteradores decentes.
Hmm, o problema é que tudo o que os iteradores estão em total desacordo com o que as plataformas de tempo de execução (Java / CLR) estão tentando alcançar: uso novo, simples e com todo mundo é dev. O que pode ser bom, mas eles disseram uma vez no livro roxo e disseram antes e antes de C:
Indirection.
Um conceito muito poderoso, mas nunca o faz se você o fizer. Os iteradores são úteis, pois ajudam na abstração de algoritmos, outro exemplo. E o tempo de compilação é o local para um algoritmo, muito simples. Você conhece código + dados ou nesse outro idioma C #:
IEnumerable + LINQ + Massive Framework = 300 MB de indução de penalidade no tempo de execução, arrastando aplicativos por montes de instâncias de tipos de referência.
"Le Pointer é barato."
fonte
Algumas respostas acima afirmaram que "os ponteiros não são realmente difíceis", mas ainda não abordaram diretamente onde "os ponteiros são difíceis!" vem de. Alguns anos atrás, eu ensinei os alunos do primeiro ano do ensino médio (por apenas um ano, desde que eu claramente o chupei) e ficou claro para mim que a idéia de ponteiro não é difícil. O difícil é entender por que e quando você deseja um ponteiro .
Não acho que você possa se divorciar dessa questão - por que e quando usar um ponteiro - de explicar problemas mais amplos de engenharia de software. Por que cada variável não deve ser uma variável global e por que se deve levar em consideração códigos semelhantes em funções (que, obtenha isso, use ponteiros para especializar seu comportamento no site de chamada).
fonte
Não vejo o que há de tão confuso nos ponteiros. Eles apontam para um local na memória, ou seja, ele armazena o endereço da memória. Em C / C ++, você pode especificar o tipo para o qual o ponteiro aponta. Por exemplo:
Diz que my_int_pointer contém o endereço para um local que contém um int.
O problema com os ponteiros é que eles apontam para um local na memória, facilitando a localização em algum local em que você não deveria estar. Como prova, observe as inúmeras falhas de segurança nos aplicativos C / C ++ causadas pelo estouro de buffer (aumentando o ponteiro além do limite alocado).
fonte
Só para confundir um pouco mais as coisas, às vezes você precisa trabalhar com alças em vez de ponteiros. Alças são ponteiros para ponteiros, para que o back-end possa mover coisas na memória para desfragmentar a pilha. Se o ponteiro mudar no meio da rotina, os resultados são imprevisíveis, então você deve primeiro travar a alça para garantir que nada vá a lugar algum.
http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 fala sobre isso um pouco mais coerentemente do que eu. :-)
fonte
Todo iniciante em C / C ++ tem o mesmo problema e esse problema ocorre não porque "os indicadores são difíceis de aprender", mas "quem e como isso é explicado". Alguns alunos o reúnem verbalmente, visualmente, e a melhor maneira de explicar isso é usar o exemplo "treinar" ( exemplos para exemplos verbais e visuais).
Onde "locomotiva" é um ponteiro que não pode segurar nada e "vagão" é o que a "locomotiva" tenta puxar (ou apontar para). Depois, você pode classificar o "vagão", ele pode conter animais, plantas ou pessoas (ou uma mistura deles).
fonte