Quais são as barreiras para entender os indicadores e o que pode ser feito para superá-los? [fechadas]


449

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.


20
A tese dessa questão é que os indicadores são difíceis de entender. A questão não oferece evidências de que os indicadores sejam mais difíceis de entender do que qualquer outra coisa.
Bmargulies

14
Talvez esteja faltando alguma coisa (porque eu codigo nas linguagens do GCC), mas sempre pensei em ponteiros na memória como uma estrutura Key-> Value. Como é caro repassar grandes quantidades de dados em um programa, você cria a estrutura (valor) e repassa seu ponteiro / referência (chave) porque a chave é uma representação muito menor da estrutura maior. A parte difícil é quando você precisa comparar dois indicadores / referências (você está comparando as chaves ou os valores), o que exige mais trabalho para invadir os dados contidos na estrutura (valor).
Evan Plaice

2
@ Wolfpack'08 "Parece-me que uma memória no endereço sempre será um int." - Então deve parecer que nada tem um tipo, pois todos são apenas bits de memória. "Na verdade, o tipo do ponteiro é o tipo de var para o qual o ponteiro aponta" - Não, o tipo do ponteiro é o ponteiro para o tipo de var que o ponteiro aponta - o que é natural e deve ser óbvio.
Jim Balter

2
Eu sempre me perguntei o que é tão difícil de entender no fato de que variáveis ​​(e funções) são apenas blocos de memória e ponteiros são variáveis ​​que armazenam endereços de memória. Esse modelo de pensamento talvez muito prático pode não impressionar todos os fãs de conceitos abstratos, mas ajuda perfeitamente a entender como os ponteiros funcionam.
Christian Rau

8
Em poucas palavras, os alunos provavelmente não entendem porque não entendem corretamente, ou de maneira alguma, como a memória de um computador em geral, e especificamente o "modelo de memória" C, funciona. Este livro Programação desde o início fornece uma lição muito boa sobre esses tópicos.
Abbafei

Respostas:


745

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:

type
    THouse = class
    private
        FName : array[0..9] of Char;
    public
        constructor Create(name: PChar);
    end;

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:

--- [ttttNNNNNNNNNN] ---
     ^ ^
     | |
     | + - a matriz FName
     |
     + - sobrecarga

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.

THouse.Create('My house');

Layout da memória:

--- [ttttNNNNNNNNNN] ---
    Minha casa

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.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...

Layout da memória:

    h
    v
--- [ttttNNNNNNNNNN] ---
    Minha casa

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.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1
    v
--- [ttttNNNNNNNNNN] ---
    Minha casa
    ^
    h2

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.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    h := nil;

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:

    h <- +
    v + - antes de grátis
--- [ttttNNNNNNNNNN] --- |
    Minha casa <- +

    h (agora aponta para lugar nenhum) <- +
                                + - grátis
---------------------- | (note que a memória ainda pode
    xx34Minha casa <- + contém alguns dados)

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).

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    ... // forgot to clear h here
    h.OpenFrontDoor; // will most likely fail

O uso hapó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.

    h <- +
    v + - antes de grátis
--- [ttttNNNNNNNNNN] --- |
    Minha casa <- +

    h <- +
    v + - depois de grátis
---------------------- |
    Minha casa <- +

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.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    h := THouse.Create('My house'); // uh-oh, what happened to our first house?
    ...
    h.Free;
    h := nil;

Aqui, substituímos o conteúdo da hvariá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:

    h
    v
--- [ttttNNNNNNNNNN] ---
    Minha casa

Layout de memória após a segunda alocação:

                       h
                       v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNNN]
    1234 Minha casa 5678 Minha casa

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:

procedure OpenTheFrontDoorOfANewHouse;
var
    h: THouse;
begin
    h := THouse.Create('My house');
    h.OpenFrontDoor;
    // uh-oh, no .Free here, where does the address go?
end;

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:

    h <- +
    v + - antes de perder o ponteiro
--- [ttttNNNNNNNNNN] --- |
    Minha casa <- +

    h (agora aponta para lugar nenhum) <- +
                                + - depois de perder o ponteiro
--- [ttttNNNNNNNNNN] --- |
    Minha casa <- +

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.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1.Free;
    h1 := nil;
    h2.OpenFrontDoor; // uh-oh, what happened to our house?

Aqui a casa foi demolida, através da referência h1e, embora tenha h1sido limpa também, h2ainda 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:

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := THouse.Create('My other house somewhere');
                         ^-----------------------^
                          longer than 10 characters
                         0123456789 <-- 10 characters

Layout de memória após a primeira alocação:

                        h1
                        v
----------------------- [ttttNNNNNNNNNN]
                        Minha casa

Layout de memória após a segunda alocação:

    h2 h1
    vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNNN]
    Minha outra casa em algum lugar
                        ^ --- + - ^
                            |
                            + - substituído

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.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;

Aqui, criamos um link da nossa casa para nossa cabine. Podemos seguir a cadeia até que uma casa não tenha NextHousereferência, o que significa que é a última. Para visitar todas as nossas casas, poderíamos usar o seguinte código:

var
    h1, h2: THouse;
    h: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;
    ...
    h := h1;
    while h <> nil do
    begin
        h.LockAllDoors;
        h.CloseAllWindows;
        h := h.NextHouse;
    end;

Layout da memória (adicionado NextHouse como um link no objeto, anotado com os quatro LLLLs no diagrama abaixo):

    h1 h2
    vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNNLLLL]
    1234 Casa + 5678Cabine +
                   | ^ |
                   + -------- + * (sem link)

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:

    h1 h2
    vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNNN]
    1234 Minha casa 5678 Minha casa

Pode ter esses dois endereços (o mais à esquerda - é o endereço 0):

  • h1 = 4
  • h2 = 23

O que significa que nossa lista vinculada acima pode se parecer com a seguinte:

    h1 (= 4) h2 (= 28)
    vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNNLLLL]
    O que você quer tocar hoje?
                   | ^ |
                   + -------- + * (sem link)

É 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.


59
A saturação do buffer é hilária. "O vizinho chega em casa, abre o crânio dele escorregando no seu lixo e processa você no esquecimento."
GTD

12
Esta é uma boa explicação do conceito, com certeza. O conceito NÃO é o que eu acho confuso sobre indicadores, então todo esse ensaio foi um pouco desperdiçado.
Breton

10
Mas só para perguntar, o que você acha confuso sobre os ponteiros?
Lasse V. Karlsen

11
Revi este post várias vezes desde que você escreveu a resposta. Seu esclarecimento com o código é excelente, e agradeço que você o revise para adicionar / refinar mais pensamentos. Bravo Lasse!
David McGraw

3
Não há como uma única página de texto (não importa quanto tempo seja) resumir todas as nuances de gerenciamento de memória, referências, indicadores, etc. Como 465 pessoas votaram nisso, eu diria que serve como uma ferramenta suficientemente boa. página inicial da informação. Há mais para aprender? Claro, quando não é?
Lasse V. Karlsen

153

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.


5
Eu não acho que entendi errado. Minha intenção era alterar o valor da variável apontada de John para Sam. É um pouco mais difícil de representar com as pessoas, porque parece que você está alterando o valor dos dois indicadores.
Tryke 02/10/08

24
Mas a razão pela qual é confuso é que não é como se John se levantasse e depois Sam se sentasse, como poderíamos imaginar. É mais como se sam viesse e enfiasse a mão em john e clonasse a programação do sam no corpo de john, como hugo tecendo em matriz recarregada.
21909 Breton

59
Mais como Sam ocupa o assento de John, e John flutua pela sala até que ele esbarra em algo crítico e causa uma falha.
just_wes

2
Pessoalmente, acho este exemplo desnecessariamente complicado. Meu professor me disse para apontar para uma luz e disse: "sua mão é o ponteiro para o objeto de luz".
Celeritas

O problema com esse tipo de exemplo é que o ponteiro para X e X não é o mesmo. E isso não é retratado com as pessoas.
Isaac Nequittepas

124

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.


15
Eu realmente gosto disso. Não é difícil ver que escrever um hiperlink duas vezes não faz aparecer dois sites (assim como int *a = bnão faz duas cópias *b).
detly

4
Isso é realmente muito intuitivo e algo com o qual todos devem se relacionar. Embora existam muitos cenários em que essa analogia se desfaz. Ótimo para uma rápida introdução. +1
Brian Wigginton

Um link para uma página sendo aberta duas vezes geralmente cria duas instâncias quase totalmente independentes dessa página da web. Eu acho que um hiperlink poderia ser uma boa analogia para um construtor, talvez, mas não para um ponteiro.
Utkan Gezer

@ThoAppelsin Não necessariamente verdadeiro, se você está acessando uma página estática em html, por exemplo, está acessando um único arquivo no servidor.
dramzy

5
Você está pensando demais. Os hiperlinks apontam para arquivos no servidor, essa é a extensão da analogia.
dramzy

48

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.


2
Interessante. Mas não faço ideia de como começar a fazer isso. Algum recurso para compartilhar?
419 Karolis

1
Concordo. Por exemplo, aprendi a programar em montagem antes de C e, sabendo como os registros funcionam, aprender ponteiros era fácil. De fato, não houve muito aprendizado, tudo veio muito natural.
Milan Babuškov 01/06/2009

Pegue uma CPU básica, diga algo que corra cortadores de grama ou lava-louças e implemente-a. Ou um subconjunto muito, muito básico, de ARM ou MIPS. Ambos têm um ISA muito simples.
11119 Daniel Goldberg

1
Vale ressaltar que essa abordagem educacional foi defendida / praticada pelo próprio Donald Knuth. A Arte da Programação por Computador de Knuth descreve uma arquitetura hipotética simples e pede aos alunos que implementem soluções para praticar problemas em uma linguagem de montagem hipotética para essa arquitetura. Uma vez que se tornou praticamente viável, alguns alunos que lêem os livros de Knuth realmente implementam sua arquitetura como uma VM (ou usam uma implementação existente) e executam suas soluções. IMO, esta é uma ótima maneira de aprender, se você tiver tempo.
precisa saber é o seguinte

4
@ Lucas, eu não acho tão fácil entender as pessoas que simplesmente não conseguem entender os ponteiros (ou, para ser mais exato, a indireção em geral). Você está basicamente assumindo que pessoas que não entendem ponteiros em C poderiam começar a aprender montagem, entender a arquitetura subjacente do computador e retornar a C com uma compreensão de ponteiros. Isso pode ser verdade para muitos, mas, de acordo com alguns estudos, parece que algumas pessoas não conseguem entender indiretamente, mesmo em princípio (ainda acho isso muito difícil de acreditar, mas talvez tenha tido sorte com meus "alunos"). ").
Luaan 26/05

27

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, na linguagem C / C ++?

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 .

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?

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.

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.

Para uma broca? Faça uma estrutura:

struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;

Mesmo exemplo que acima, exceto em C:

// Same example as above, except in C:
struct {
    char a;
    char b;
    char c;
    char d;
} mystruct;

mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;

printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);

Resultado:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

Talvez isso explique alguns dos princípios básicos através do exemplo?


+1 para "sem entender como a memória é fisicamente colocada". Eu vim para C com base na linguagem assembly e o conceito de ponteiros era muito natural e fácil; e já vi pessoas com apenas um background de nível superior lutando para descobrir isso. Para piorar, a sintaxe é confusa (ponteiros de função!), Portanto, aprender o conceito e a sintaxe ao mesmo tempo é uma receita para problemas.
Brendan #

4
Eu sei que isso é um post antigo, mas seria ótimo se a saída do código fornecido fosse adicionada ao post.
21713 Josh

Sim, é semelhante à álgebra (embora a álgebra tenha um ponto extra de compreensibilidade em imutável suas "variáveis"). Mas cerca de metade das pessoas que conheço não tem conhecimento de álgebra na prática. Simplesmente não computa para eles. Eles conhecem todas essas "equações" e prescrições para chegar ao resultado, mas as aplicam de maneira aleatória e desajeitada. E eles não podem estendê- los para seu próprio objetivo - é apenas uma caixa preta imutável e incomodável para eles. Se você entende álgebra e é capaz de usá-la com eficiência, já está muito à frente do pacote - mesmo entre os programadores.
Luaan 26/05

24

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

int puts(const char *s);

Por enquanto, ignore o const.parâmetro O passado para puts()é um ponteiro, que é o valor de um ponteiro (já que todos os parâmetros em C são passados ​​por valor), e o valor de um ponteiro é o endereço para o qual ele aponta ou, simplesmente , um endereço. Assim, quando escrevemos puts(strA);como vimos, estamos passando o endereço de strA [0].

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.


Me deixa imaginando qual é o sentido de ter um ponteiro. Na maioria dos blocos de código que encontro, um ponteiro é visto apenas em sua declaração.
precisa

@ Wolfpack'08 ... o que ?? Qual código você está olhando?
Kyle Strand

@KyleStrand Deixe-me dar uma olhada.
Wolfpack'08

19

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/


1
Segundo o tutorial de Ted Jensen. Ele divide os indicadores em um nível de detalhe, que não é muito detalhado, nenhum livro que eu li faz. Extremamente útil! :)
Dave Gallagher

12

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:

widget->wazzle.fizzle = fazzle.foozle->wazzle;

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.


10

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:

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

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:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

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 cor enum)


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?

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

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)

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

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


Só não use a palavra "ponteiro duplo". Os ponteiros podem apontar para qualquer coisa, portanto, obviamente, você pode ter ponteiros apontando para outros ponteiros. Eles não são ponteiros duplos.
gnasher729

Eu acho que isso não leva ao ponto de os próprios "dedos", para continuar sua analogia, cada um "ocupar uma vaga de estacionamento". Não tenho certeza de que as pessoas tenham alguma dificuldade em entender os ponteiros no alto nível de abstração da sua analogia, é entender que os ponteiros são coisas mutáveis ​​que ocupam locais de memória e, como isso é útil, parece escapar das pessoas.
Emmet

1
@ Emmet - Eu não discordo que exista muito mais que se possa entrar em indicadores do WRT, mas li a pergunta: "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ível
Mike

Existe algum mérito em estender a analogia dedo do carro para uma pessoa (com um ou mais dedos - e uma anormalidade genética que pode permitir que cada um aponte em qualquer direção!) Sentou em um dos carros apontando para outro carro (ou inclinou-se para apontar para o terreno baldio ao lado do terreno como um "ponteiro não inicializado"; ou uma mão inteira espalhou-se apontando para uma fileira de espaços como um "conjunto de ponteiros de tamanho fixo [5]" ou enrolou na palma da mão "ponteiro nulo" que aponta para algum lugar onde se sabe que nunca há um carro) ... 8-)
SlySven

sua explicação foi apreciável e boa para iniciantes.
Yatendra Rathore

9

Eu 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.


9

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 :

Por alguma razão, a maioria das pessoas parece nascer sem a parte do cérebro que entende os ponteiros. Isso é uma coisa de aptidão, não de habilidade - requer uma forma complexa de pensamento duplamente indireto que algumas pessoas simplesmente não conseguem fazer.


8

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:

int doIt(char *buffer )
//*buffer is a pointer to the buffer

o que ele quer?

poderia querer:

um número que representa um endereço para um buffer

(Para dizer isso, eu digo doIt(mybuffer)ou doIt(*myBuffer)?)

um número que representa o endereço para um endereço para um buffer

(é isso doIt(&mybuffer)ou doIt(mybuffer)ou doIt(*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.


Eu já vi isso explicado assim: se você vê uma declaração de ponteiro como double *(*(*fn)(int))(char), então o resultado da avaliação *(*(*fn)(42))('x')será a double. Você pode retirar as camadas de avaliação para entender quais devem ser os tipos intermediários.
precisa saber é o seguinte

@BerndJendrissek Não tenho certeza se eu sigo. Qual é o resultado da avaliação (*(*fn)(42))('x') então?
Breton

você recebe uma coisa (vamos chamá-la x) em que, se você avaliar *x, recebe uma duplicação.
Bernd Jendrissek

@BerndJendrissek Isso deveria explicar algo sobre ponteiros? Eu não entendo. Onde você quer chegar? Tirei uma camada e não obtive novas informações sobre nenhum tipo intermediário. O que isso explica sobre o que uma função específica vai aceitar? O que isso tem a ver com alguma coisa?
Breton

Talvez a mensagem nesta explicação (e não seja minha, eu gostaria de encontrar onde a vi pela primeira vez) seja pensar menos em termos do que fn é e mais em termos do que você pode fazer comfn
Bernd Jendrissek

8

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)


Afinal, um ponteiro nulo não aponta para endereço zero na memória, mesmo que seu "valor" C seja zero. É um conceito totalmente separado e, se você lidar com isso errado, poderá acabar endereçando (e desreferenciando) algo que não esperava. Em alguns casos, isso pode até ser um endereço zero na memória (especialmente agora que o espaço de endereçamento é geralmente plano), mas em outros, pode ser omitido como comportamento indefinido por um compilador otimizador ou acessar outra parte da memória associada com "zero" para o tipo de ponteiro especificado. Hilaridade segue.
Luaan 29/06/16

Não necessariamente. Você precisa ser capaz de modelar o computador em sua cabeça para que os ponteiros façam sentido (e depure outros programas também). Nem todo mundo pode fazer isso.
Thorbjørn Ravn Andersen

5

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).


5

A razão pela qual é tão difícil de entender não é porque é um conceito difícil, mas porque a sintaxe é inconsistente .

   int *mypointer;

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.

  int* mypointer;

Nem me inicie em indicadores de função ...


2
Na verdade, os dois fragmentos são válidos C. É uma questão de muitos anos no estilo C que o primeiro é mais comum. O segundo é um pouco mais comum em C ++, por exemplo.
RBerteig

1
O segundo fragmento realmente não funciona bem com declarações mais complexas. E a sintaxe não é tão "inconsistente" quando você percebe que a parte direita de uma declaração de ponteiro mostra o que você deve fazer com o ponteiro para obter algo cujo tipo é o especificador de tipo atômico à esquerda.
precisa saber é o seguinte

2
int *p;tem um significado simples: *pé um número inteiro. int *p, **ppsignifica: *pe **ppsão inteiros.
Roteamento de milhas

@MilesRout: Mas esse é exatamente o problema. *pe 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! :)ppp*pp
Lightness Races in Orbit

@LightnessRacesinOrbit Ensinar o alinhamento à direita está longe de ser enganoso. É a única maneira correta de ensiná-lo. NÃO ensiná-lo é enganoso.
Miles Rout

5

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.


4

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 :)


Uma complicação desagradável com a analogia com o número da caixa de correio é que, embora a linguagem inventada por Dennis Ritchie defina comportamento em termos dos endereços de bytes e dos valores armazenados nesses bytes, a linguagem definida pelo Padrão C convida a "otimizar" implementações para usar um comportamento. modelo que é mais complicado, mas define vários aspectos do modelo de maneiras ambíguas, contraditórias e incompletas.
Supercat

3

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.


2

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.


2

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.


O problema está principalmente na sintaxe da declaração C. Mas o uso ponteiro teria certeza de ser mais fácil se (*p)teria sido (p->), e assim teríamos p->->xem vez da ambígua*p->x
MSalters

@MSalters Oh meu Deus, você está brincando, certo? Não há inconsistências lá. a->bsimplesmente significa (*a).b.
Rota das milhas

@ Miles: De fato, e por essa lógica * p->xsignifica * ((*a).b)que *p -> xsignifica meios (*(*p)) -> x. A mistura de operadores de prefixo e postfix causa uma análise ambígua.
MSalters

@MSalters não, porque o espaço em branco é irrelevante. Isso é como dizer que 1+2 * 3deve ser 9.
Miles Rout

2

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.


1
O interessante é que os ponteiros C não expõem nenhum charasteristic da arquitetura de memória subjacente. As únicas diferenças entre as referências Java e os ponteiros C são que você pode ter tipos complexos envolvendo ponteiros (por exemplo, int *** ou char * ( ) (void * )), existe aritmética de ponteiro para matrizes e ponteiros para estruturar membros, a presença do vazio * e dualidade de matriz / ponteiro. Fora isso, eles funcionam da mesma forma.
21139 jpalecek

Bom ponto. É a aritmética do ponteiro e a possibilidade de estouros de buffer - rompendo a abstração ao romper a área de memória atualmente relevante - o que faz isso.
Joshua Fox

@jpalecek: É muito fácil entender como os ponteiros funcionam em implementações que documentam seu comportamento em termos da arquitetura subjacente. Dizer 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.
Supercat

2

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):

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

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 exemplo RAM[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.


1

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.)


1

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."


4
O que isso tem a ver com alguma coisa?
29509 Neil Williams

... o que você está tentando dizer, além de "a ligação estática é a melhor coisa de sempre" e "eu não entendo como algo diferente do que eu aprendi anteriormente funciona"?
Luaan 29/06/16

Luaan, você não poderia saber o que se pode aprender desmontando o JIT em 2000, poderia? O fato de ele terminar em uma tabela de salto, a partir de uma tabela de ponteiros, como mostrado em 2000 online no ASM, portanto, não entender nada de diferente pode ter outro significado: ler com atenção é uma habilidade essencial, tente novamente.
rama-jka toti

1

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).


0

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:

int* my_int_pointer;

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).


0

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. :-)


-1: alças não são ponteiros para ponteiros; eles não são indicadores em nenhum sentido. Não os confunda.
Ian Goldby

"Eles não são indicadores em nenhum sentido" - hum, eu imploro para diferir.
SarekOfVulcan

Um ponteiro é um local de memória. Um identificador é qualquer identificador exclusivo. Pode ser um ponteiro, mas também pode ser um índice em uma matriz ou qualquer outra coisa. O link que você forneceu é apenas um caso especial em que o identificador é um ponteiro, mas não precisa ser. Veja também parashift.com/c++-faq-lite/references.html#faq-8.8
Ian Goldby

Esse link também não suporta sua afirmação de que eles não são ponteiros - "Por exemplo, os identificadores podem ser Fred **, onde os apontadores apontados para Fred * ..." Eu não acho o -1 foi justo.
SarekOfVulcan

0

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).

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.