Quais são as barreiras para a compreensão dos indicadores e o que pode ser feito para superá-las?[fechado]

StackOverflow https://stackoverflow.com/questions/5727

  •  08-06-2019
  •  | 
  •  

Pergunta

Por que os ponteiros são um fator tão importante de confusão para muitos estudantes novos e até antigos de nível universitário em C ou C++?Existem ferramentas ou processos de pensamento que ajudaram você a entender como os ponteiros funcionam na variável, na função e além do nível?

Quais são algumas boas práticas que podem ser feitas para levar alguém ao nível de “Ah-hah, entendi”, sem deixá-lo atolado no conceito geral?Basicamente, faça exercícios semelhantes a cenários.

Foi útil?

Solução

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 o endereço de uma casa e o bloco de memória ao qual ele se refere como a casa real.Todos os tipos de operações podem assim ser facilmente explicados.

Adicionei algum código Delphi abaixo e alguns comentários quando apropriado.Escolhi Delphi porque minha outra linguagem de programação principal, C#, não apresenta coisas como vazamentos de memória da mesma maneira.

Se você deseja apenas aprender o conceito de alto nível de ponteiros, ignore as partes denominadas "Layout da memória" na explicação abaixo.O objetivo deles é dar exemplos de como a memória poderia ficar após as operações, mas são de natureza mais de baixo nível.No entanto, para explicar com precisão como os buffer overruns realmente funcionam, foi importante adicionar estes diagramas.

Isenção de responsabilidade:Para todos os efeitos, essa explicação e os layouts de memória de exemplo são muito simplificados.Há mais despesas gerais e muito mais detalhes que você precisa saber se precisa lidar com a memória em nível de baixo nível.No entanto, para a intenção de explicar a memória e os ponteiros, ela é precisa 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;

Ao inicializar o objeto house, o nome dado ao construtor é copiado no campo privado FName.Há uma razão pela qual ele é definido como uma matriz de tamanho fixo.

Na memória, haverá algum overhead associado à alocação de casa, vou ilustrar isso abaixo assim:

---[ttttNNNNNNNNNN]---
     ^   ^
     |   |
     |   +- the FName array
     |
     +- overhead

A área "tttt" é sobrecarga, normalmente haverá mais para vários tipos de tempos de execução e linguagens, como 8 ou 12 bytes.É imperativo que quaisquer valores armazenados nesta área nunca sejam alterados por nada além do alocador de memória ou das rotinas principais do sistema, ou você corre o risco de travar o programa.


Alocar memória

Peça a um empresário para construir sua casa e lhe dê o endereço da casa.Em contraste com o mundo real, a alocação de memória não pode ser informada onde alocar, mas encontrará um local adequado com espaço suficiente e reportará o endereço à memória alocada.

Ou seja, quem escolherá o local será o empreendedor.

THouse.Create('My house');

Disposição da memória:

---[ttttNNNNNNNNNN]---
    1234My house

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 este 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');
    ...

Disposição da memória:

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

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, e não a duas casas separadas.Qualquer tentativa de seguir o endereço de um papel e reorganizar os móveis daquela casa fará parecer que a outra casa foi modificado da mesma maneira, a menos que você detecte explicitamente que na verdade é apenas uma casa.

Observação Geralmente esse é o conceito que tenho mais dificuldade em explicar para as 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]---
    1234My house
    ^
    h2

Liberando a memória

Demolir a casa.Posteriormente, você poderá reutilizar o papel para um novo endereço, se desejar, ou apagá-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 obtenho seu endereço.Aí eu faço alguma coisa na casa (usa, o...código, deixado como exercício para o leitor) e depois o libero.Por último, limpo o endereço da minha variável.

Disposição da memória:

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after free
----------------------          | (note, memory might still
    xx34My house             <--+  contain some data)

Ponteiros pendurados

Você manda seu empresário destruir a casa, mas se esquece de apagar o endereço do papel.Quando mais tarde você olha para o pedaço de papel, você esquece que a casa não está mais lá, e vai visitá-la, sem resultados (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

Usando h depois da chamada para .Free poder trabalho, mas isso é pura sorte.O mais provável é que ele falhe, no local do cliente, no meio de uma operação crítica.

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h                        <--+
    v                           +- after free
----------------------          |
    xx34My house             <--+

Como você pode ver, H ainda aponta para os remanescentes 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.A casa ainda está em algum lugar e, quando mais tarde você 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 do h variável com o 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 àquela casa, e ela ficará de pé.Em outras palavras, a memória alocada permanecerá alocada até o aplicativo fechar, momento em que o sistema operacional irá desligá-lo.

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

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

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

                       h
                       v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

Uma maneira mais comum de obter esse método é esquecer de liberar algo, em vez de sobrescrevê-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 ​​onde o endereço da casa exista, mas a casa ainda está lá.

Disposição da memória:

    h                        <--+
    v                           +- before losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

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 acompanha quais áreas de memória foram usadas e não as reutiliza, a menos que você o liberte.


Liberando a memória, mas mantendo uma referência (agora inválida)

Demolir a casa, apagar um dos papéis mas você também tem outro papel com o endereço antigo, quando você for ao endereço não encontrará uma casa, mas poderá encontrar algo que lembre as ruínas de Um.

Talvez você até encontre uma casa, mas não é a casa cujo endereço lhe foi originalmente fornecido e, portanto, qualquer tentativa de usá-la como se pertencesse a você pode falhar terrivelmente.

Às vezes você pode até descobrir que um endereço vizinho tem uma casa bastante grande que ocupa três endereços (Rua Principal 1-3), e seu endereço vai para o meio da casa.Qualquer tentativa de tratar aquela 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 em h1, e enquanto h1 também foi limpo, h2 ainda tem o endereço antigo e desatualizado.O acesso à casa que não está mais de pé pode ou não funcionar.

Esta é uma variação do ponteiro pendente acima.Veja seu layout de memória.


Estouro de buffer

Você move mais coisas para dentro de casa do que pode caber, espalhando-as na casa ou no quintal dos vizinhos.Quando o dono daquela casa vizinha voltar mais tarde, ele encontrará todo tipo de coisas que considerará suas.

Esta é a razão pela qual escolhi um array de tamanho fixo.Para preparar o cenário, suponha que a segunda casa que alocemos, por algum motivo, será colocada antes da primeira em 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 da memória após a primeira alocação:

                        h1
                        v
-----------------------[ttttNNNNNNNNNN]
                        5678My house

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

    h2                  h1
    v                   v
---[ttttNNNNNNNNNN]----[ttttNNNNNNNNNN]
    1234My other house somewhereouse
                        ^---+--^
                            |
                            +- overwritten

A parte que mais frequentemente causa falha é quando você substitui partes importantes dos dados que você armazenou que realmente não devem ser alterados aleatoriamente.Por exemplo, pode não ser um problema que partes do nome da casa H1 fossem alteradas, em termos de travamento do programa, mas substituir a sobrecarga do objeto provavelmente falhará quando você tentar usar o objeto quebrado, assim Substituindo links armazenados em outros objetos no objeto.


Listas vinculadas

Quando você segue um endereço em um pedaço de papel, você 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 a nossa cabana.Podemos seguir a corrente 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:

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, observado com os quatro llll's no diagrama abaixo):

    h1                      h2
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home       +        5678Cabin      +
                   |        ^              |
                   +--------+              * (no 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 variedade de bytes, o primeiro byte tem o endereço 0, o próximo do endereço 1 e assim por diante.Isso é simplificado, mas é bom o suficiente.

Então este layout de memória:

    h1                 h2
    v                  v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

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

  • h1 = 4
  • h2 = 23

O que significa que nossa lista vinculada acima pode ser assim:

    h1 (=4)                 h2 (=28)
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home      0028      5678Cabin     0000
                   |        ^              |
                   +--------+              * (no link)

É típico armazenar um endereço que "não aponta 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 solicitar a linguagem de programação para fornecer seu número, mas a maioria das linguagens de programação e os tempos de execução tenta esconder 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 ou se importa com o que é realmente implementado, desde que funcione.

Outras dicas

Na minha primeira aula de Comp Sci, fizemos o seguinte exercício.É verdade que esta era uma sala de aula com cerca de 200 alunos...

O professor escreve no quadro: int john;

João se levanta

Professor escreve: int *sally = &john;

Sally se levanta e aponta para John

Professor: int *bill = sally;

Bill se levanta e aponta para John

Professor: int sam;

Sam se levanta

Professor: bill = &sam;

Bill agora aponta para Sam.

Acho que você entendeu.Acho que passamos cerca de uma hora fazendo isso, até revisarmos o básico da atribuição de ponteiros.

Uma analogia que achei útil para explicar ponteiros são os hiperlinks.A maioria das pessoas consegue entender que um link em uma página da web 'aponta' para outra página na Internet e, se você puder copiar e colar esse hiperlink, ambos apontarão para a mesma página da web original.Se você editar a página original e seguir qualquer um desses links (indicadores), você obterá a nova página atualizada.

A razão pela qual os ponteiros parecem confundir tantas pessoas é que, em sua maioria, eles vêm com pouca ou nenhuma experiência em arquitetura de computadores.Como muitos parecem não ter idéia de como os computadores (a máquina) são realmente implementados - trabalhar em C/C++ parece estranho.

Um exercício é pedir-lhes que implementem uma máquina virtual simples baseada em bytecode (em qualquer linguagem que escolherem, python funciona muito bem para isso) com um conjunto de instruções focado em operações de ponteiro (carregar, armazenar, endereçamento direto/indireto).Em seguida, peça-lhes que escrevam programas simples para esse conjunto de instruções.

Qualquer coisa que exija um pouco mais do que uma simples adição envolverá dicas e eles certamente entenderão.

Por que os ponteiros são um fator tão importante de confusão para muitos estudantes novos e até antigos de nível universitário na linguagem C/C++?

O conceito de espaço reservado para um valor – variáveis ​​– mapeia algo que aprendemos na escola – álgebra.Não existe um paralelo que você possa traçar sem entender como a memória é fisicamente disposta dentro de um computador, e ninguém pensa sobre esse tipo de coisa até que esteja lidando com coisas de baixo nível - no nível de comunicação C/C++/byte .

Existem ferramentas ou processos de pensamento que ajudaram você a entender como os ponteiros funcionam na variável, na função e além do nível?

Caixas de endereços.Lembro-me de quando estava aprendendo a programar BASIC em microcomputadores, havia uns livros lindos com jogos, e às vezes era preciso inserir 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 coisa pequena (um byte) cabia nessas caixas, e havia muitas delas - alguns computadores tinham até 65.535!Eles estavam próximos um do outro e todos tinham um endereço.

Quais são algumas boas práticas que podem ser feitas para levar alguém ao nível de “Ah-hah, entendi”, sem deixá-lo atolado no conceito geral?Basicamente, faça exercícios semelhantes a cenários.

Para uma furadeira?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 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);

Saída:

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 por meio de exemplos?

A razão pela qual tive dificuldade em entender os ponteiros, no início, é que muitas explicações incluem um monte de besteiras sobre passagem por referência.Tudo o que isso faz é confundir a questão.Ao usar um parâmetro de ponteiro, você está ainda passando por valor;mas o valor é um endereço em vez de, digamos, um int.

Alguém já criou um link para este tutorial, mas posso destacar o momento em que comecei a entender as dicas:

Um tutorial sobre ponteiros e matrizes em C:Capítulo 3 - Ponteiros e Strings

int puts(const char *s);

Por enquanto, ignore o const. O parâmetro passado para puts() é um ponteiro, esse é 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 um ponteiro de compreensão.

Mesmo que você seja um desenvolvedor VB .NET ou C# (como eu) e nunca use código inseguro, ainda vale a pena entender como funcionam os ponteiros, ou você não entenderá como funcionam as referências de objetos.Então você terá a noção comum, mas equivocada, de que passar uma referência de objeto para um método copia o objeto.

Achei o "Tutorial sobre ponteiros e matrizes em C" de Ted Jensen um excelente recurso para aprender sobre ponteiros.Está dividido em 10 lições, começando com uma explicação do que são (e para que servem) ponteiros e terminando com ponteiros de função. http://home.netcom.com/~tjensen/ptr/cpoint.htm

Continuando 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/

As complexidades dos ponteiros vão além do que podemos facilmente ensinar.Fazer com que os alunos apontem uns para os outros e usar pedaços de papel com endereços residenciais são ótimas ferramentas de aprendizagem.Eles fazem um ótimo trabalho ao apresentar os conceitos básicos.Na verdade, aprender os conceitos básicos é vital para usar ponteiros com sucesso.No entanto, no código de produção, é comum entrar em cenários muito mais complexos do que essas demonstrações simples podem encapsular.

Estive envolvido com sistemas onde 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ê tem vários níveis de indireção e começa a acabar 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).Adicione matrizes de ponteiros e ponteiros nó a nó (árvores, listas vinculadas) e a situação ficará ainda pior.Já vi alguns desenvolvedores realmente bons se perderem quando começaram a trabalhar nesses sistemas, até mesmo desenvolvedores que entendiam muito bem o básico.

Estruturas complexas de ponteiros também não indicam necessariamente uma codificação deficiente (embora possam).A composição é uma peça vital de uma boa programação orientada a objetos e, em linguagens com ponteiros brutos, inevitavelmente levará à indireção em múltiplas camadas.Além disso, os sistemas muitas vezes precisam usar bibliotecas de terceiros com estruturas que não combinam entre si em estilo ou técnica.Em situações como essa, a complexidade surgirá naturalmente (embora certamente devamos combatê-la tanto quanto possível).

Acho que a melhor coisa que as faculdades podem fazer para ajudar os alunos a aprender ponteiros é usar boas demonstrações, combinadas com projetos que exijam o uso de ponteiros.Um projeto difícil fará mais pela compreensão do ponteiro do que mil demonstrações.As demonstrações podem proporcionar uma compreensão superficial, mas para compreender profundamente os indicadores, você realmente precisa usá-los.

Pensei em adicionar uma analogia a esta lista que achei muito útil ao explicar dicas (no passado) como Tutor de Ciência da Computação;primeiro, vamos:


Prepare o cenário:

Considere um estacionamento com 3 vagas, essas vagas são numeradas:

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

De certa forma, são como localizações de memória, são sequenciais e contíguas.uma espécie de array.No momento não há carros neles, então é como um array vazio (parking_lot[3] = {0}).


Adicione os dados

Um estacionamento nunca fica vazio por muito tempo...se assim fosse, seria inútil e ninguém construiria nenhum.Então, digamos que com o passar do dia o estacionamento fique cheio 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 |

Esses carros são todos do mesmo tipo (carro), então uma maneira de pensar nisso é que nossos carros são algum tipo de dado (digamos, um int), mas eles têm valores diferentes (blue, red, green;isso pode ser uma cor enum)


Insira o ponteiro

Agora, se eu levar você para este estacionamento e pedir que 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 à minha pergunta.Olhando no seu dedo não me diz nada, mas se eu olhar onde está seu dedo apontando para (desreferenciando o ponteiro), posso encontrar o carro (os dados) que procurava.


Reatribuindo o ponteiro

Agora posso pedir que você encontre um carro vermelho e você pode redirecionar seu dedo para um carro novo.Agora o seu ponteiro (o mesmo de antes) está me mostrando novos dados (a vaga de estacionamento onde o carro vermelho pode ser encontrado) do mesmo tipo (o carro).

O ponteiro não mudou fisicamente, ainda está seu dedo, apenas os dados que ele estava me mostrando mudaram.(o endereço da "vaga de estacionamento")


Ponteiros duplos (ou um ponteiro para um ponteiro)

Isso também funciona com mais de um ponteiro.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.(isto é como int **finger_two = &finger)

Agora se eu quiser saber onde está o carro azul posso seguir a direção do primeiro dedo até o segundo dedo, até o carro (os dados).


O ponteiro pendurado

Agora, digamos que você esteja se sentindo como uma estátua e queira manter sua mão apontando para o carro vermelho indefinidamente.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 era mas não é mais.Digamos que um carro novo estacione ali...um carro laranja.Agora, se eu perguntar de novo, “onde está o carro vermelho”, você ainda estará apontando para lá, mas agora está errado.Isso não é um carro vermelho, é laranja.


Aritmética de ponteiro

Ok, então você ainda está apontando para a segunda vaga de estacionamento (agora ocupada pelo carro Orange)

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

Bom, agora tenho uma nova pergunta...Quero saber a cor do carro no próximo vaga de estacionamento.Você pode ver que está apontando para o ponto 2, então basta adicionar 1 e apontar para o próximo ponto.(finger+1), agora como eu queria saber quais dados estavam ali, você tem que verificar esse ponto (não apenas o dedo) para poder deferir o ponteiro (*(finger+1)) para ver se há um carro verde ali (os dados naquele local)

Não acho que os ponteiros como conceito sejam particularmente complicados - os modelos mentais da maioria dos alunos são mapeados para algo assim e alguns esboços rápidos podem ajudar.

A dificuldade, pelo menos aquela que experimentei no passado e com a qual vi outros lidarem, é que o gerenciamento de ponteiros em C/C++ pode ser desnecessariamente complicado.

Um exemplo de tutorial com um bom conjunto de diagramas ajuda muito na compreensão dos ponteiros.

Joel Spolsky apresenta alguns pontos positivos sobre a compreensão dos indicadores em seu Guia de Guerrilha para Entrevistas artigo:

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

Acho que a principal barreira para a compreensão dos indicadores são os maus professores.

Quase todo mundo aprende mentiras sobre dicas:Que eles são nada mais do que endereços de memória, ou que eles permitem que você aponte para locais arbitrários.

E claro que são difíceis de entender, perigosos e semimágicos.

Nada disso é verdade.Os ponteiros são, na verdade, conceitos bastante simples, contanto que você siga o que a linguagem C++ tem a dizer sobre eles e não os imbua de atributos que "geralmente" funcionam na prática, mas que 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 em esta postagem do blog - espero que ajude alguém.

(Observe, antes que alguém seja pedante comigo, sim, o padrão C++ diz que os ponteiros representar endereços de memória.Mas não diz que "ponteiros são endereços de memória e nada mais que endereços de memória e podem ser usados ​​ou considerados de forma intercambiável com endereços de memória".A distinção é importante)

O problema com ponteiros não é o conceito.É a execução e a linguagem envolvidas.Confusão adicional ocorre quando os professores assumem que é o CONCEITO de ponteiros que é difícil, e não o jargão, ou a bagunça complicada que C e C++ fazem do conceito.Portanto, muito esforço é gasto para explicar o conceito (como na resposta aceita para esta pergunta) e é praticamente desperdiçado em alguém como eu, porque já entendo tudo isso.Está apenas explicando a parte errada do problema.

Para se ter uma ideia de onde venho, sou alguém que entende de ponteiros perfeitamente e posso usá-los com competência em linguagem assembly.Porque na linguagem assembly eles não são chamados de ponteiros.Eles são chamados de endereços.Quando se trata de programação e uso de ponteiros em C, cometo muitos erros e fico muito confuso.Eu 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 isso 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 de um endereço para um buffer

(é aquele doIt(&mybuffer) ou doIt(mybuffer) ou doIt(*mybuffer)?)

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

(talvez seja isso doIt(&mybuffer).ou é doIt(&&mybuffer) ?ou mesmo doIt(&&&mybuffer))

e assim por diante, e a linguagem envolvida não deixa isso tão claro porque envolve as palavras "ponteiro" e "referência" que não têm tanto significado e clareza para mim quanto "x contém o endereço de y" e " esta função requer um endereço para y".A resposta também depende do que diabos é "mybuffer" para começar e do que DoIt pretende fazer com ele.A linguagem não suporta os níveis de aninhamento encontrados na prática.Como quando 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 saber onde modificar o conteúdo do ponteiro.Na maioria das vezes, só preciso adivinhar o que significa "ponteiro" e, na maioria das vezes, estou errado, independentemente de quanta experiência eu tenha em adivinhar.

"Pointer" está muito sobrecarregado.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, ela deseja o endereço que a variável ponteiro contém ou deseja o endereço da variável ponteiro?Estou confuso.

Eu acho que o que torna os ponteiros difíceis de aprender é que até os ponteiros você se sente confortável com a ideia de que "nesse local de memória há um conjunto de bits que representa um int, um duplo, um caractere, o que quer que seja".

Quando você vê um ponteiro pela primeira vez, você realmente não entende o que está naquele local de memória."O que você quer dizer com isso tem um endereço?"

Não concordo com a noção de que "ou você consegue 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 em funções).

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 da criação de uma variável define o tipo da variável.A declaração de ponteiro não funciona assim em C e C++.Em vez disso, eles dizem que a variável está apontando para o tipo à esquerda.Nesse caso: *meu ponteiro está apontando em um int.

Eu não entendi completamente os ponteiros até tentar usá-los em C# (com inseguro), eles funcionam exatamente da mesma maneira, mas com sintaxe lógica e consistente.O ponteiro é um tipo em si.Aqui meu ponteiro é um ponteiro para um int.

  int* mypointer;

Nem me fale sobre ponteiros de função ...

Eu poderia trabalhar com ponteiros quando só conhecia 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 uma compreensão completa foi a linguagem assembly.Se você fizer alguma depuração séria no nível de instrução com um programa em linguagem assembly que você escreveu, deverá ser capaz de entender muitas coisas.

Gosto da analogia do endereço residencial, mas sempre pensei que o endereço fosse da própria caixa de correio.Desta forma você pode visualizar o conceito de desreferenciar o ponteiro (abrir a caixa de correio).

Por exemplo, seguindo uma lista vinculada:1) Comece com seu artigo com o endereço 2) Vá para o endereço no artigo 3) Abra a caixa de correio para encontrar um novo pedaço de papel com o próximo endereço nele

Em uma lista encadeada linear, a última caixa de correio não contém nada (fim da lista).Em uma lista vinculada circular, a última caixa de correio contém o endereço da primeira caixa de correio.

Observe que a etapa 3 é onde ocorre a desreferência e onde você falhará ou dará errado quando o endereço for inválido.Supondo que você possa ir até a caixa de correio de um endereço inválido, imagine que há um buraco negro ou algo lá dentro que vira o mundo do avesso :)

Acho que a principal razão pela qual as pessoas têm problemas com isso é porque geralmente não é ensinado de uma forma interessante e envolvente.Eu gostaria de ver um palestrante reunir 10 voluntários da multidão e dar-lhes uma régua de 1 metro para cada um, fazê-los ficar em pé em uma determinada configuração e usar as réguas para apontarem uns para os outros.Em seguida, mostre a aritmética dos ponteiros movendo as pessoas (e para onde elas apontam suas réguas).Seria uma forma simples mas eficaz (e acima de tudo memorável) de mostrar os conceitos sem se prender muito à mecânica.

Depois de chegar a C e C++, parece ficar mais difícil para algumas pessoas.Não tenho certeza se isso ocorre porque eles estão finalmente colocando em prática uma teoria que não compreendem adequadamente ou porque a manipulação de ponteiros é inerentemente mais difícil nessas linguagens.Não consigo me lembrar muito bem da minha própria transição, mas sabia ponteiros em Pascal e depois mudei para C e fiquei totalmente perdido.

Não acho que os ponteiros em si sejam confusos.A maioria das pessoas consegue entender o conceito.Agora, em quantas dicas você consegue pensar ou com quantos níveis de indireção você se sente confortável.Não são necessários muitos para colocar as pessoas no limite.O fato de que eles podem ser alterados acidentalmente por bugs no seu programa também pode torná-los muito difíceis de depurar quando algo dá errado no seu código.

Acho que pode ser realmente um problema de sintaxe.A sintaxe C/C++ para ponteiros parece inconsistente e mais complexa do que precisa ser.

Ironicamente, o que realmente me ajudou a entender os ponteiros foi encontrar o conceito de um iterador no c++ Biblioteca de modelos padrão.É irônico porque só posso assumir que os iteradores foram concebidos como uma generalização do ponteiro.

Às vezes você simplesmente não consegue ver a floresta até aprender a ignorar as árvores.

A confusão vem das múltiplas camadas de abstração misturadas no conceito de "ponteiro".Os programadores não ficam confusos com referências comuns em Java/Python, mas os ponteiros são diferentes porque 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.

A maneira como eu gostava de explicar isso era em termos de arrays e índices - as pessoas podem não estar familiarizadas com ponteiros, mas geralmente sabem o que é um índice.

Então eu digo, imagine que a RAM é um array (e você tem 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 é, na verdade, apenas o índice (do primeiro byte) dessa variável na RAM.

Então, se você tiver 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, como RAM[RAM[index]].

Eu desenharia um array em uma lista de papel e apenas o usaria para mostrar coisas como muitos ponteiros apontando para a mesma memória, aritmética de ponteiro, ponteiro para ponteiro e assim por diante.

Número da caixa postal.

É uma informação que permite acessar outra coisa.

(E se você fizer contas com números de caixas postais, poderá ter um problema, porque a carta vai para a caixa errada.E se alguém se mudar para outro estado – sem endereço de encaminhamento – então você terá um ponteiro pendente.Por outro lado - se o correio encaminhar a correspondência, você terá um ponteiro para um ponteiro.)

Não é uma maneira ruim de entender isso, por meio de iteradores.mas continue olhando e você verá Alexandrescu começar a reclamar deles.

Muitos ex-desenvolvedores de C++ (que nunca entenderam que iteradores são um indicador moderno antes de descartar a linguagem) saltam para C# e ainda acreditam que têm iteradores decentes.

Hmm, o problema é que todos 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 onde todos são desenvolvedores.O que pode ser bom, mas eles disseram isso uma vez no livro roxo e disseram antes e antes de C:

Indireção.

Um conceito muito poderoso, mas nunca será assim se você fizer isso até o fim.Iteradores são úteis porque ajudam na abstração de algoritmos, outro exemplo.E o tempo de compilação é o lugar para um algoritmo, muito simples.Você conhece código + dados, ou nessa outra linguagem C#:

IEnumerable + LINQ + Massive Framework = 300 MB de penalidade de tempo de execução indireta de péssimos, arrastando aplicativos através de montes de instâncias de tipos de referência.

"Le Pointer é barato."

Algumas respostas acima afirmaram que "os ponteiros não são realmente difíceis", mas não foram abordados diretamente onde "o ponteiro é difícil!" vem de.Alguns anos atrás, eu dei aulas particulares para alunos do primeiro ano de ciências da computação (por apenas um ano, já que eu claramente era péssimo nisso) e ficou claro para mim que o ideia do ponteiro não é difícil.O que é difícil é entender por que e quando você iria querer um ponteiro.

Não acho que você possa separar essa questão - por que e quando usar um ponteiro - da explicação de questões mais amplas de engenharia de software.Por que toda variável deveria não ser uma variável global, e por que alguém deveria fatorar código semelhante em funções (que, veja só, use ponteiros para especializar seu comportamento em seu site de chamada).

Não vejo o que há de tão confuso nos ponteiros.Eles apontam para um local na memória, ou seja, 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, por isso é fácil chegar a algum local onde você não deveria estar.Como prova, observe as inúmeras falhas de segurança em aplicativos C/C++ devido ao buffer overflow (incrementando o ponteiro além do limite alocado).

Só para confundir um pouco mais as coisas, às vezes você tem que 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 o heap.Se o ponteiro mudar no meio da rotina, os resultados serão imprevisíveis, então primeiro você terá que travar a alça para garantir que nada vá a lugar nenhum.

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 fala sobre isso com um pouco mais de coerência do que eu.:-)

Todo iniciante em C/C++ tem o mesmo problema e esse problema ocorre não porque "os ponteiros são difíceis de aprender", mas "quem e como são explicados".Alguns alunos reúnem verbalmente ou visualmente e a melhor maneira de explicá-lo é usar exemplo de "treinar" (ternos para exemplo verbal e visual).

Onde "locomotiva" é um ponteiro que não pode segure qualquer coisa e "vagão" é o que a "locomotiva" tenta puxar (ou apontar).Depois, você pode classificar o próprio “vagão”, se ele pode conter animais, plantas ou pessoas (ou uma mistura deles).

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top