O que as pessoas acham difícil sobre os ponteiros C? [fechadas]


173

A partir do número de perguntas postadas aqui, fica claro que as pessoas têm alguns problemas bastante fundamentais ao se orientar sobre indicadores e aritmética dos indicadores.

Estou curioso para saber o porquê. Eles nunca me causaram grandes problemas (embora eu tenha aprendido sobre eles no Neolítico). Para escrever melhores respostas para essas perguntas, gostaria de saber o que as pessoas acham difícil.

Então, se você está lutando com ponteiros, ou recentemente, mas de repente "entendeu", quais foram os aspectos dos ponteiros que lhe causaram problemas?


54
Snarky, exagerado, desnecessariamente polêmico, responde com um grão de verdade: eles foram aleijados - mentalmente mutilados, digo-lhe - ao avisar primeiro uma dessas línguas "expressivas" de alto nível. Eles deveriam começar programando em bare metal como Deus e Daniel Boone pretendiam!
dmckee --- ex-moderador gatinho

3
... e o Programador seria melhor, porque ele se tornará uma discussão, apesar dos seus melhores esforços.
dmckee --- gatinho ex-moderador

Isso é argumentativo e subjetivo, gera N resultados, o meu difícil é o seu fácil. Provavelmente funcionará com programadores, mas ainda não estamos migrando essas questões porque o site não está na versão beta.
Sam Saffron

2
@ Sam Saffron: Embora eu geralmente concorde que essa é mais uma pergunta do tipo programadores.SE, honestamente, não seria ruim se as pessoas estivessem dispostas a marcar "Eu acho que é fácil" e "Eu odeio ver ponteiros" como spam eles são.
Jackian 27/10/10

3
Alguém tem que trazer isso à tona: "É como um dedo apontando para a lua. Não se concentre no dedo ou você perderá toda a glória celestial" - Bruce Lee
mu é muito curto

Respostas:


86

Eu suspeito que as pessoas estão se aprofundando um pouco em suas respostas. Não é realmente necessário um entendimento de agendamento, operações reais da CPU ou gerenciamento de memória no nível de assembly.

Quando estava ensinando, descobri que os seguintes buracos no entendimento dos alunos eram a fonte mais comum de problemas:

  1. Armazenamento Heap vs Stack. É simplesmente impressionante quantas pessoas não entendem isso, mesmo em um sentido geral.
  2. Empilhar quadros. Apenas o conceito geral de uma seção dedicada da pilha para variáveis ​​locais, junto com o motivo de ser uma 'pilha' ... detalhes como esconder o local de retorno, detalhes do manipulador de exceções e registros anteriores podem ser deixados com segurança até que alguém tente construa um compilador.
  3. "Memória é memória é memória" A transmissão apenas altera as versões dos operadores ou a quantidade de espaço que o compilador oferece para um determinado pedaço de memória. Você sabe que está lidando com esse problema quando as pessoas falam sobre "o que realmente é a variável X (primitiva) ".

A maioria dos meus alunos conseguiu entender um desenho simplificado de um pedaço de memória, geralmente a seção de variáveis ​​locais da pilha no escopo atual. Geralmente, dar endereços fictícios explícitos aos vários locais ajudou.

Acho que, em resumo, estou dizendo que, se você deseja entender os ponteiros, precisa entender as variáveis ​​e o que elas realmente são nas arquiteturas modernas.


13
IMHO, entender pilha e pilha é tão desnecessário quanto detalhes de CPU de baixo nível. Pilha e pilha são detalhes de implementação. A especificação ISO C não possui uma única menção à palavra "pilha" e nem K&R.
sigjuice

4
@sigjuice: Suas objeções não atendem tanto à pergunta quanto à resposta. A) K&R C é um anacronismo B) ISO C não é o único idioma com ponteiros, meus pontos 1 e 2 foram desenvolvidos contra um idioma não baseado em C C) 95% das arquiteturas (não idiomas) por aí usam o heap / stack, é bastante comum que as exceções sejam explicadas em relação a ele. D) O ponto da pergunta era "por que as pessoas não entendem ponteiros", não "como explico a ISO C"?
jkerian 27/10/10

9
@ John Marchetti: Ainda mais ... dado que a pergunta era "Qual é o problema principal do problema das pessoas com ponteiros", não acho que as pessoas que fazem as perguntas relacionadas a ponteiros ficariam terrivelmente impressionadas com "Você não" realmente não preciso saber "como resposta. Obviamente eles discordam. :)
jkerian

3
@jkerian Pode estar desatualizado, mas as três ou quatro páginas da K&R que explicam os indicadores o fazem sem a necessidade de detalhes de implementação. O conhecimento dos detalhes da implementação é útil por várias razões, mas, no IMHO, não deve ser um pré-requisito para entender as principais construções de uma linguagem.
sigjuice

3
"Geralmente, dar endereços fictícios explícitos aos vários locais ajudou". -> +1
fredoverflow

146

Quando comecei a trabalhar com eles, o maior problema que tive foi a sintaxe.

int* ip;
int * ip;
int *ip;

são todos iguais.

mas:

int* ip1, ip2;  //second one isn't a pointer!
int *ip1, *ip2;

Por quê? porque a parte "ponteiro" da declaração pertence à variável e não ao tipo

E, em seguida, desreferenciar a coisa usa uma notação muito semelhante:

*ip = 4;  //sets the value of the thing pointed to by ip to '4'
x = ip;   //hey, that's not '4'!
x = *ip;  //ahh... there's that '4'

Exceto quando você realmente precisa obter um ponteiro ... então você usa um e comercial!

int *ip = &x;

Viva a consistência!

Então, aparentemente, para serem idiotas e provar como são inteligentes, muitos desenvolvedores de bibliotecas usam ponteiros para ponteiros para ponteiros, e se esperam uma variedade dessas coisas, por que não passar um ponteiro para isso também? .

void foo(****ipppArr);

Para chamar isso, preciso do endereço da matriz de ponteiros para ponteiros para ponteiros de ints:

foo(&(***ipppArr));

Em seis meses, quando tiver que manter esse código, passarei mais tempo tentando descobrir o que tudo isso significa do que reescrever do zero. (sim, provavelmente entendi errado a sintaxe - já faz um tempo desde que eu fiz alguma coisa em C. Eu meio que sinto falta disso, mas depois sou um massoquista)


21
O seu comentário sobre o primeiro, >> * ip = 4; // define o valor de ip como '4' << está errado. Deve ser >> // define o valor da coisa apontada pelo ip para '4'
aaaa bbbb 26/10/10

8
Empilhar muitos tipos um sobre o outro é uma má idéia, em qualquer idioma. Você pode encontrar escrevendo "foo (& (*** ipppArr)))"; estranho em C, mas escrevendo algo como "std :: map <std :: pair <int, int>, std :: pair <std :: vetor <int>, std :: tupla <int, duplo, std :: list <int> >>> "em C ++ também é muito complexo. Isso não significa que os ponteiros nos contêineres C ou STL no C ++ sejam complexos. Significa apenas que você precisa usar definições de tipo melhores para torná-lo compreensível para o leitor do seu código.
Patrick

20
Sinceramente, não posso acreditar que um mal-entendido da sintaxe seja a resposta mais votada. Essa é a parte mais fácil sobre ponteiros.
jason

4
Mesmo lendo essa resposta, fiquei tentado a pegar uma folha de papel e desenhar a imagem. Em C, eu estava sempre desenhando.
Michael Easter

19
@Jason Que medida objetiva de dificuldade existe além do que a maioria das pessoas acha difícil?
Rupert Madden-Abbott

52

O entendimento adequado dos ponteiros requer conhecimento sobre a arquitetura da máquina subjacente.

Atualmente, muitos programadores não sabem como suas máquinas funcionam, assim como a maioria das pessoas que sabe dirigir um carro não sabe nada sobre o motor.


18
@dmckee: Bem, estou errado? Quantos programadores Java poderiam lidar com um segfault?
Robert Harvey

5
Segfaults têm algo a ver com troca de marchas? - Um programador Java
Tom Anderson

6
@ Robert: foi concebido como um complemento genuíno. Este é um assunto difícil de discutir, sem ferir os sentimentos das pessoas. E temo que meu comentário tenha precipitado o mesmo conflito que pensei que você tivesse conseguido evitar. Mea cupla.
dmckee --- ex-moderador gatinho

30
Discordo; você não precisa entender a arquitetura subjacente para obter indicadores (eles são uma abstração, de qualquer maneira).
jason

11
@ Jason: Em C, um ponteiro é essencialmente um endereço de memória. Trabalhar com eles com segurança é impossível sem entender a arquitetura da máquina. Veja en.wikipedia.org/wiki/Pointer_(computing) e boredzo.org/pointers/#definition
Robert Harvey

42

Ao lidar com indicadores, as pessoas que ficam confusas estão amplamente em um dos dois campos. Eu estive (sou?) Em ambos.

A array[]multidão

Essa é a multidão que não sabe traduzir de notação de ponteiro para notação de matriz (ou nem sabe que eles estão relacionados). Aqui estão quatro maneiras de acessar elementos de uma matriz:

  1. notação de matriz (indexação) com o nome da matriz
  2. notação de matriz (indexação) com o nome do ponteiro
  3. notação de ponteiro (o *) com o nome do ponteiro
  4. notação de ponteiro (o *) com o nome da matriz

 

int vals[5] = {10, 20, 30, 40, 50};
int *ptr;
ptr = vals;

array       element            pointer
notation    number     vals    notation

vals[0]     0          10      *(ptr + 0)
ptr[0]                         *(vals + 0)

vals[1]     1          20      *(ptr + 1)
ptr[1]                         *(vals + 1)

vals[2]     2          30      *(ptr + 2)
ptr[2]                         *(vals + 2)

vals[3]     3          40      *(ptr + 3)
ptr[3]                         *(vals + 3)

vals[4]     4          50      *(ptr + 4)
ptr[4]                         *(vals + 4)

A idéia aqui é que acessar matrizes por meio de ponteiros parece bastante simples e direto, mas muitas coisas muito complicadas e inteligentes podem ser feitas dessa maneira. Alguns dos quais podem deixar programadores experientes em C / C ++ perplexos, muito menos novatos inexperientes.

O reference to a pointere pointer to a pointermultidão

Este é um ótimo artigo que explica a diferença e que citarei e roubarei algum código de :)

Como um pequeno exemplo, pode ser muito difícil ver exatamente o que o autor deseja fazer se você se deparar com algo assim:

//function prototype
void func(int*& rpInt); // I mean, seriously, int*& ??

int main()
{
  int nvar=2;
  int* pvar=&nvar;
  func(pvar);
  ....
  return 0;
}

Ou, em menor grau, algo como isto:

//function prototype
void func(int** ppInt);

int main()
{
  int nvar=2;
  int* pvar=&nvar;
  func(&pvar);
  ....
  return 0;
}

Então, no final do dia, o que realmente resolvemos com toda essa bobagem? Nada.

Agora vimos a sintaxe de ptr para ptr e ref-para-ptr. Existem vantagens de um sobre o outro? Eu tenho medo, não. O uso de um de ambos, para alguns programadores, são apenas preferências pessoais. Alguns que usam ref-to-ptr dizem que a sintaxe é "mais limpa", enquanto outros que usam ptr-para-ptr, dizem que a sintaxe de ptr-para-ptr torna mais claro para quem está lendo o que você está fazendo.

Essa complexidade e a aparente (aparente negritude) permutabilidade com referências, que geralmente é outra advertência de ponteiros e um erro dos recém-chegados, dificulta a compreensão dos ponteiros. Também é importante entender, para fins de conclusão, que indicadores de referências são ilegais em C e C ++ por motivos confusos que levam você a lvalue- rvaluesemântica.

Como uma resposta anterior comentou, muitas vezes você só tem esses programadores que pensam que estão sendo espertos ao usar ******awesome_var->lol_im_so_clever()e a maioria de nós provavelmente é culpada de escrever essas atrocidades às vezes, mas isso não é um bom código e certamente não é sustentável .

Bem, esta resposta acabou por ser mais longa do que eu esperava ...


5
Eu acho que você pode ter dado uma resposta C ++ para uma pergunta C aqui ... pelo menos a segunda parte.
detly

Ponteiros para ponteiros também se aplicam a C: p
David Titarenco

1
Heh? Só vejo ponteiros para ponteiros ao passar por matrizes - seu segundo exemplo não é realmente aplicável ao código C mais decente. Além disso, você está arrastando C para a bagunça do C ++ - referências em C não existem.
new123456

Você tem que lidar com ponteiros para ponteiros em muitos casos legítimos. Por exemplo, ao lidar com um ponteiro de função apontando para uma função que retorna um ponteiro. Outro exemplo: uma estrutura que pode conter um número variável de outras estruturas. E muitos mais ...
David Titarenco

2
"ponteiros para referências são ilegais em C" - mais como "ausente" :)
Kos

29

Culpo a qualidade dos materiais de referência e das pessoas que ensinam pessoalmente; a maioria dos conceitos em C (mas especialmente os indicadores) são simplesmente mal ensinados . Continuo ameaçando escrever meu próprio livro em C (intitulado A última coisa que o mundo precisa é outro livro sobre a linguagem de programação C ), mas não tenho tempo nem paciência para fazê-lo. Então eu saio aqui e jogo citações aleatórias do Standard nas pessoas.

Também existe o fato de que, quando o C foi projetado inicialmente, presumiu-se que você entendesse a arquitetura da máquina com um nível bastante detalhado, apenas porque não havia como evitá-la no seu trabalho cotidiano (a memória era tão estreita e os processadores eram tão lentos você tinha que entender como o que você escreveu afetou o desempenho).


3
Sim. 'int foo = 5; int * pfoo = & foo; Veja como isso é útil? OK, seguindo em frente ... 'Eu realmente não usei ponteiros até escrever minha própria biblioteca de listas com links duplos.
John Lopez

2
+1. Eu uso para orientar alunos do CS100 e muitos de seus problemas foram resolvidos apenas analisando os indicadores de maneira compreensível.
benzado 27/10

1
+1 para o contexto histórico. Tendo começado muito tempo depois desse período, isso nunca me ocorreu.
Lumi

26

Há um ótimo artigo que apóia a noção de que os indicadores são difíceis no site de Joel Spolsky - The Perils of JavaSchools .

[Isenção de responsabilidade - eu não sou um odiador de Java por si só .]


2
@ Jason - isso é verdade, mas não nega o argumento.
Steve Townsend

4
Spolsky não está dizendo que JavaSchools é a razão pela qual as pessoas acham os indicadores difíceis. Ele está dizendo que eles resultam em pessoas analfabetas com formação em Ciência da Computação.
benzado

1
@benzado - ponto de vista justo - meu breve post seria melhorado se fosse lido "um ótimo artigo apoiando a noção de que ponteiros são difíceis". O que o artigo implica é que "ter um diploma de CS de uma 'boa escola'" não é um bom indicador de sucesso como desenvolvedor, como costumava ser, enquanto "entende indicadores" (e recursão) ainda é.
Steve Townsend

1
@Steve Townsend: Acho que você está esquecendo o argumento do Sr. Spolsky.
jason

2
@Steve Townsend: Spolsky está argumentando que as escolas Java estão criando uma geração de programadores que não conhecem indicadores e recursão, e não que indicadores são difíceis por causa da prevalência de escolas Java. Como você declarou "há um ótimo artigo sobre por que isso é difícil" e, vinculado ao referido artigo, parece que você tem a última interpretação. Perdoe-me se eu estiver errado.
jason

24

A maioria das coisas é mais difícil de entender se você não está fundamentado no conhecimento que está "embaixo". Quando eu ensinei CS, ficou muito mais fácil quando comecei meus alunos a programar uma "máquina" muito simples, um computador decimal simulado com códigos de operação decimais cuja memória consistia em registros decimais e endereços decimais. Eles colocariam programas muito curtos para, por exemplo, adicionar uma série de números para obter um total. Depois, eles passavam para assistir ao que estava acontecendo. Eles poderiam manter pressionada a tecla "Enter" e vê-la correr "rápido".

Tenho certeza que quase todo mundo no SO se pergunta por que é útil ser tão básico. Esquecemos como era não saber programar. Brincar com um computador de brinquedo cria conceitos sem os quais você não pode programar, como as idéias de que a computação é um processo passo a passo, usando um pequeno número de primitivas básicas para criar programas e o conceito de memória variáveis ​​como locais onde os números são armazenados, nos quais o endereço ou o nome da variável é diferente do número que ela contém. Há uma distinção entre a hora em que você entra no programa e a hora em que "é executado". Eu comparo aprender a programar como atravessar uma série de "lombadas", como programas muito simples, depois loops e sub-rotinas, matrizes, E / S sequenciais, ponteiros e estrutura de dados.

Finalmente, ao chegar a C, os ponteiros são confusos, embora a K&R tenha feito um ótimo trabalho em explicá-los. A maneira como os aprendi em C foi saber como lê-los - da direita para a esquerda. Como quando vejo int *pna minha cabeça, digo " paponta para um int". C foi inventado como um passo à frente da linguagem assembly e é disso que eu gosto - é próximo desse "terreno". Ponteiros, como qualquer outra coisa, são mais difíceis de entender se você não tiver esse fundamento.


1
Uma boa maneira de aprender isso é programar microcontroladores de 8 bits. Eles são fáceis de entender. Pegue os controladores Atmel AVR; eles são até suportados pelo gcc.
Xenu

@Xenu: Eu concordo. Para mim, eram Intel 8008 e 8051 :-) #
6268 Mike Dunlavey 27/10/10

Para mim, era um computador personalizado de 8 bits (o "Talvez") no MIT na penumbra do tempo.
QuantumMechanic

Mike - você deve obter os seus alunos uma CARDÍACA :)
QuantumMechanic

1
@Quantum: CARDIAC- bom, não tinha ouvido falar disso. O "Talvez" - deixe-me adivinhar, foi quando Sussman (et al) teve pessoas lendo o livro Mead-Conway e fabricando seus próprios chips LSI? Isso foi um pouco depois do meu tempo lá.
precisa saber é o seguinte

17

Não recebi dicas até ler a descrição em K&R. Até aquele momento, os ponteiros não faziam sentido. Eu li um monte de coisas onde as pessoas diziam "Não aprenda dicas, elas são confusas e machucam sua cabeça e causam aneurismas", então eu me esquivei disso por um longo tempo e criei esse ar desnecessário de conceito difícil .

Caso contrário, principalmente o que eu pensava ser, por que diabos você desejaria uma variável que você precisa passar por aros para obter o valor e, se você quisesse atribuir coisas a ela, teria que fazer coisas estranhas para obter valores? neles. Todo o ponto de uma variável é algo para armazenar um valor, pensei, então por que alguém queria complicar estava além de mim. "Então, com um ponteiro, você precisa usar o *operador para obter seu valor ??? Que tipo de variável pateta é essa?" , Eu pensei. Inútil, sem trocadilhos.

A razão pela qual foi complicado foi porque eu não entendi que um ponteiro era um endereço para alguma coisa. Se você explicar que é um endereço, que é algo que contém um endereço para outra coisa e que você pode manipular esse endereço para fazer coisas úteis, acho que isso pode esclarecer a confusão.

Uma classe que exigia o uso de ponteiros para acessar / modificar portas em um PC, usando a aritmética de ponteiros para abordar diferentes localizações de memória, e olhando para o código C mais complicado que modificava seus argumentos me desapontou da ideia de que os ponteiros eram inúteis.


4
Se você possui recursos limitados para trabalhar (RAM, ROM, CPU), como em aplicativos incorporados, os ponteiros rapidamente fazem muito mais sentido.
Nick T

+1 no comentário de Nick - especialmente para passar estruturas.
new123456

12

Aqui está um exemplo de ponteiro / matriz que me deu uma pausa. Suponha que você tenha duas matrizes:

uint8_t source[16] = { /* some initialization values here */ };
uint8_t destination[16];

E seu objetivo é copiar o conteúdo do uint8_t do destino de origem usando memcpy (). Adivinhe quais das seguintes ações atingem esse objetivo:

memcpy(destination, source, sizeof(source));
memcpy(&destination, source, sizeof(source));
memcpy(&destination[0], source, sizeof(source));
memcpy(destination, &source, sizeof(source));
memcpy(&destination, &source, sizeof(source));
memcpy(&destination[0], &source, sizeof(source));
memcpy(destination, &source[0], sizeof(source));
memcpy(&destination, &source[0], sizeof(source));
memcpy(&destination[0], &source[0], sizeof(source));

A resposta (alerta de spoiler!) São TODOS eles. "destination", "& destination" e "& destination [0]" são todos do mesmo valor. "& destination" é um tipo diferente dos outros dois, mas ainda é o mesmo valor. O mesmo vale para as permutações de "fonte".

Como um aparte, eu pessoalmente prefiro a primeira versão.


Eu prefiro a primeira versão também (menos pontuação).
sigjuice 27/10/10

++ Eu também, mas você realmente precisa ter cuidado sizeof(source), porque se sourcefor um ponteiro, sizeofnão será o que você deseja. Às vezes (nem sempre) escrevo sizeof(source[0]) * number_of_elements_of_sourceapenas para ficar longe desse bug.
Mike Dunlavey

destination, & destination, & destination [0] não são os mesmos - mas cada um, através de um mecanismo diferente, será convertido no mesmo vazio * quando usado no memcpy. No entanto, quando usado como argumento de sizeof, você obterá dois resultados diferentes e três resultados diferentes serão possíveis.
precisa saber é o seguinte

Eu pensei que o endereço do operador era necessário?
MarcusJ

7

Eu deveria começar dizendo que C e C ++ foram as primeiras linguagens de programação que aprendi. Comecei com C, depois fiz C ++ na escola, muito, e depois voltei para C para me tornar fluente.

A primeira coisa que me confundiu sobre ponteiros ao aprender C foi a simples:

char ch;
char str[100];
scanf("%c %s", &ch, str);

Essa confusão estava principalmente enraizada em ter sido introduzida no uso de referência a uma variável para argumentos OUT antes que os ponteiros fossem corretamente introduzidos para mim. Lembro-me de que deixei de escrever os primeiros exemplos em C para Dummies, porque eram muito simples para nunca conseguir que o primeiro programa que escrevi funcionasse (provavelmente por causa disso).

O que era confuso sobre isso era o que &chrealmente significava e também porque strnão precisava.

Depois que me familiarizei com isso, lembro-me de estar confuso sobre a alocação dinâmica. Eu percebi em algum momento que ter ponteiros para dados não era extremamente útil sem a alocação dinâmica de algum tipo, então escrevi algo como:

char * x = NULL;
if (y) {
     char z[100];
     x = z;
}

para tentar alocar dinamicamente algum espaço. Não deu certo. Eu não tinha certeza de que iria funcionar, mas não sabia de que outra forma poderia funcionar.

Mais tarde eu aprendi sobre malloce new, mas eles realmente pareciam geradores de memória mágicos para mim. Eu não sabia nada sobre como eles poderiam funcionar.

Algum tempo depois, eu estava aprendendo a recursão novamente (eu já havia aprendido antes, mas agora estava na aula) e perguntei como funcionava sob o capô - onde estavam armazenadas as variáveis ​​separadas. Meu professor disse "na pilha" e muitas coisas ficaram claras para mim. Eu já tinha ouvido o termo antes e implementado pilhas de software antes. Eu já tinha ouvido outros se referirem à "pilha" muito antes, mas havia esquecido.

Nessa época, também percebi que o uso de matrizes multidimensionais em C pode ficar muito confuso. Eu sabia como eles funcionavam, mas eles eram tão fáceis de se envolver, que eu decidi tentar contorná-los sempre que pudesse. Eu acho que o problema aqui foi principalmente sintático (especialmente passando ou devolvendo-os de funções).

Desde que eu escrevi C ++ para a escola nos próximos dois anos, adquiri muita experiência usando ponteiros para estruturas de dados. Aqui eu tive um novo conjunto de problemas - misturando indicadores. Eu teria vários níveis de indicadores (coisas como node ***ptr;) me enganando. Desdiferenciaria um ponteiro o número errado de vezes e, eventualmente, recorria a descobrir quantas *eu precisava por tentativa e erro.

Em algum momento, aprendi como o heap de um programa funcionava (mais ou menos, mas bom o suficiente para não me manter acordado à noite). Lembro-me de ler que, se você olhar alguns bytes antes do ponteiro que mallocem um determinado sistema retornar, poderá ver quantos dados foram realmente alocados. Percebi que o código mallocpoderia pedir mais memória do sistema operacional e essa memória não fazia parte dos meus arquivos executáveis. Ter uma idéia decente de como mallocfunciona é realmente útil.

Logo depois disso, participei de uma aula de montagem, que não me ensinou tanto sobre ponteiros como a maioria dos programadores provavelmente pensa. Isso me fez pensar mais sobre em qual assembly meu código poderia ser traduzido. Eu sempre tentei escrever código eficiente, mas agora tinha uma idéia melhor de como fazê-lo.

Também participei de algumas aulas nas quais tive que escrever um cocô . Ao escrever lisp, eu não estava tão preocupado com a eficiência quanto em C. Eu tinha muito pouca idéia de como esse código poderia ser traduzido se compilado, mas eu sabia que parecia usar muitos símbolos nomeados locais (variáveis) criados coisas muito mais fáceis. Em algum momento, eu escrevi um pouco de código de rotação de árvore AVL em um pouco de lisp, que tive muita dificuldade em escrever em C ++ por causa de problemas com ponteiros. Percebi que minha aversão ao que eu pensava serem variáveis ​​locais em excesso havia prejudicado minha capacidade de escrever esse e vários outros programas em C ++.

Também participei de uma aula de compiladores. Enquanto nesta aula, avancei para o material avançado e aprendi sobre atribuição única estática (SSA) e variáveis ​​mortas, o que não é tão importante, exceto que me ensinou que qualquer compilador decente fará um trabalho decente ao lidar com variáveis ​​que são não mais usado. Eu já sabia que mais variáveis ​​(incluindo ponteiros) com tipos corretos e bons nomes me ajudariam a manter as coisas em mente, mas agora também sabia que evitá-las por razões de eficiência era ainda mais estúpido do que meus professores menos otimistas disseram mim.

Então, para mim, saber um pouco sobre o layout da memória de um programa ajudou muito. Pensar no que meu código significa, simbolicamente e no hardware, me ajuda. O uso de ponteiros locais com o tipo correto ajuda bastante. Costumo escrever um código que se parece com:

int foo(struct frog * f, int x, int y) {
    struct leg * g = f->left_leg;
    struct toe * t = g->big_toe;
    process(t);

de modo que, se eu estragar um tipo de ponteiro, é muito claro pelo erro do compilador qual é o problema. Se eu fiz:

int foo(struct frog * f, int x, int y) {
    process(f->left_leg->big_toe);

e com algum tipo de ponteiro errado, o erro do compilador seria muito mais difícil de descobrir. Eu ficaria tentado a recorrer a mudanças de tentativa e erro na minha frustração e provavelmente pioraria as coisas.


1
+1. Completo e perspicaz. Eu tinha esquecido o scanf, mas agora que você mencionou, lembro de ter a mesma confusão.
Joe White

6

Olhando para trás, havia quatro coisas que realmente me ajudaram a entender os ponteiros. Antes disso, eu podia usá-los, mas não os entendia completamente. Ou seja, eu sabia que, se seguisse os formulários, obteria os resultados desejados, mas não entendia completamente o "porquê" dos formulários. Sei que isso não é exatamente o que você pediu, mas acho que é um corolário útil.

  1. Escrevendo uma rotina que levou um ponteiro para um número inteiro e modificou o número inteiro. Isso me deu as formas necessárias para construir quaisquer modelos mentais de como os ponteiros funcionam.

  2. Alocação de memória dinâmica unidimensional. Descobrir a alocação de memória 1-D me fez entender o conceito do ponteiro.

  3. Alocação de memória dinâmica bidimensional. Descobrir a alocação de memória 2-D reforçou esse conceito, mas também me ensinou que o ponteiro em si requer armazenamento e deve ser levado em consideração.

  4. Diferenças entre variáveis ​​de pilha, variáveis ​​globais e memória de pilha. Descobrir essas diferenças me ensinou os tipos de memória aos quais os ponteiros apontam / se referem.

Cada um desses itens exigia imaginar o que estava acontecendo em um nível mais baixo - a construção de um modelo mental que satisfizesse todos os casos que eu pensasse em jogar nele. Demorou tempo e esforço, mas valeu a pena. Estou convencido de que, para entender os ponteiros, é preciso construir esse modelo mental sobre como eles funcionam e como são implementados.

Agora, de volta à sua pergunta original. Com base na lista anterior, houve vários itens que tive dificuldade em entender originalmente.

  1. Como e por que alguém usaria um ponteiro.
  2. Como eles são diferentes e ainda assim semelhantes a matrizes.
  3. Entendendo onde as informações do ponteiro são armazenadas.
  4. Entendendo o que e onde está o ponteiro está apontando.

Ei, você poderia me indicar um artigo / livro / seu desenho / doodle / qualquer coisa que eu pudesse aprender da mesma maneira que você descreveu na sua resposta? Acredito firmemente que este é o caminho a percorrer quando se aprende bem, basicamente qualquer coisa. Modelos mentais compreensão e boas profundas
Alexander Starbuck

1
@AlexStarbuck - não quero que isso pareça irreverente, mas o método científico é uma ótima ferramenta. Faça um desenho sobre o que você acha que pode acontecer em um cenário específico. Programe algo para testá-lo e analise o que você conseguiu. Combinou com o que você espera? Caso contrário, identifique onde difere? Repita conforme necessário, aumentando gradualmente a complexidade para testar sua compreensão e seus modelos mentais.
Sparky

6

Eu tive meu "momento ponteiro" trabalhando em alguns programas de telefonia em C. Eu tive que escrever um emulador de intercâmbio AXE10 usando um analisador de protocolo que só compreendesse o C. clássico. Tudo dependia de conhecer ponteiros. Tentei escrever meu código sem eles (ei, eu era "pré-apontador" me deu uma folga) e falhei completamente.

A chave para entendê-los, para mim, era o operador & (endereço). Depois que entendi que &isignificava o "endereço de i", entendi que *isignificava "o conteúdo do endereço apontado por i" veio um pouco mais tarde. Sempre que escrevia ou lia meu código, repetia sempre o que "&" significava e o que "*" significava, e acabei por usá-los intuitivamente.

Para minha vergonha, fui forçado a usar o VB e o Java, para que meu conhecimento sobre ponteiros não seja tão nítido quanto antes, mas estou feliz por ser "pós-ponteiro". Não me peça para usar uma biblioteca que exija que eu compreenda * * p.


Se &ié o endereço e *io conteúdo, o que é i?
Thomas Ahle

2
Estou sobrecarregando o uso de i. Para uma variável arbitrária i, & i significa "o endereço de" i, i, por si só, significa "o conteúdo de & i" e * i significa "tratar o conteúdo de & i como um endereço, vá para esse endereço e devolva o conteúdo".
Gary Rowe

5

A principal dificuldade com ponteiros, pelo menos para mim, é que não comecei com C. Comecei com Java. Toda a noção de ponteiros era realmente estranha até algumas aulas na faculdade, onde eu esperava conhecer C. Então, eu me ensinei o básico de C e como usar ponteiros em seu sentido muito básico. Mesmo assim, toda vez que me vejo lendo o código C, tenho que procurar na sintaxe do ponteiro.

Portanto, em minha experiência muito limitada (1 ano no mundo real + 4 na faculdade), os indicadores me confundem porque eu nunca tive que usá-lo realmente em outra coisa senão em sala de aula. E posso simpatizar com os alunos que agora começam o CS com JAVA em vez de C ou C ++. Como você disse, você aprendeu dicas na era 'Neolítica' e provavelmente a usa desde então. Para nós, pessoas novas, a noção de alocar memória e fazer aritmética de ponteiros é realmente estranha, porque todas essas linguagens abstraíram isso.

PS Depois de ler o ensaio de Spolsky, sua descrição de 'JavaSchools' não se parecia em nada com o que eu passei na faculdade em Cornell ('05 a 09). Tomei as estruturas e a programação funcional (sml), sistemas operacionais (C), algoritmos (caneta e papel) e várias outras classes que não eram ensinadas em java. No entanto, todas as classes de introdução e eletivas foram todas feitas em java porque é importante não reinventar a roda quando você está tentando fazer algo mais nivelado do que implementar uma hashtable com ponteiros.


4
Honestamente, considerando que você ainda tem dificuldades com indicadores, não tenho certeza de que sua experiência em Cornell contradiga substancialmente o artigo de Joel. Obviamente, o suficiente do seu cérebro está conectado a uma mentalidade de Java para mostrar seu ponto de vista.
jkerian

5
Wat? Referências em Java (ou C #, ou Python, ou provavelmente dezenas de outras linguagens) são apenas indicadores sem a aritmética. Ponteiros Compreender significa entender por que void foo(Clazz obj) { obj = new Clazz(); }é um não-op, enquanto void bar(Clazz obj) { obj.quux = new Quux(); }se transforma o argumento ...

1
Eu sei o que são referências em Java, mas estou apenas dizendo que se você me pediu para fazer reflexão em Java ou escrever algo significativo no CI, não é possível simplesmente entendê-lo. Exige muita pesquisa, como aprendê-la pela primeira vez, todas as vezes.
shoebox639

1
Como é que você passou por uma classe de sistemas operacionais em C sem se tornar fluente em C? Sem querer ofender, é só que eu lembro de ter que desenvolver um sistema operacional simples do zero. Eu devo ter usado ponteiros mil vezes ...
Gravity

5

Aqui está uma não resposta: Use cdecl (ou c ++ decl) para descobrir:

eisbaw@leno:~$ cdecl explain 'int (*(*foo)(const void *))[3]'
declare foo as pointer to function (pointer to const void) returning pointer to array 3 of int

4

Eles adicionam uma dimensão extra ao código sem uma alteração significativa na sintaxe. Pense sobre isso:

int a;
a = 5

Só há uma coisa a alteração: a. Você pode escrever a = 6e os resultados são óbvios para a maioria das pessoas. Mas agora considere:

int *a;
a = &some_int;

Há duas coisas arelevantes em momentos diferentes: o valor real de a, o ponteiro e o valor "por trás" do ponteiro. Você pode mudar a:

a = &some_other_int;

... e some_intainda está em algum lugar com o mesmo valor. Mas você também pode alterar o que ele aponta:

*a = 6;

Há uma lacuna conceitual entre eles a = 6, que tem apenas efeitos colaterais locais e *a = 6que pode afetar várias outras coisas em outros lugares. Meu argumento aqui não é que o conceito de indireção seja inerentemente complicado, mas porque você pode fazer tanto a coisa imediata e local aquanto a indireta com *a... isso pode ser o que confunde as pessoas.


4

Eu tinha programado em c ++ por 2 anos e depois convertido para Java (5 anos) e nunca mais olhei para trás. No entanto, quando recentemente tive que usar algumas coisas nativas, descobri (com espanto) que não havia esquecido nada sobre ponteiros e até as acho fáceis de usar. Este é um forte contraste com o que experimentei 7 anos atrás, quando tentei entender o conceito. Então, acho que entender e gostar é uma questão de maturidade de programação? :)

OU

Os ponteiros são como andar de bicicleta; depois que você descobre como trabalhar com eles, não há como esquecer.

Em suma, difícil de entender ou não, toda a idéia do ponteiro é MUITO educacional e acredito que deve ser entendida por todo programador, independentemente de ele programar em um idioma com ponteiros ou não.


3

Os ponteiros são difíceis por causa da indireção.


"Dizem que não há nenhum problema na ciência da computação que não possa ser resolvido com mais um nível de indireção" (embora não tenha idéia de quem o tenha dito primeiro)
The Archetypal Paul

É como mágica, onde a desorientação é o que confunde as pessoas (mas é totalmente incríveis)
Nick T

3

Ponteiros (junto com alguns outros aspectos do trabalho de baixo nível) exigem que o usuário tire a mágica.

A maioria dos programadores de alto nível gosta da mágica.


3

Ponteiros são uma maneira de lidar com a diferença entre um identificador para um objeto e um objeto em si. (ok, não necessariamente objetos, mas você sabe o que eu quero dizer, bem como a minha mente)

Em algum momento, você provavelmente terá que lidar com a diferença entre os dois. Na linguagem moderna e de alto nível, isso se torna a distinção entre copiar por valor e copiar por referência. De qualquer forma, é um conceito que muitas vezes é difícil para os programadores entenderem.

No entanto, como foi apontado, a sintaxe para lidar com esse problema em C é feia, inconsistente e confusa. Eventualmente, se você realmente tentar entender, um ponteiro fará sentido. Mas quando você começa a lidar com ponteiros para ponteiros, e assim por diante ad nauseum, fica realmente confuso para mim e para outras pessoas.

Outra coisa importante a lembrar sobre os ponteiros é que eles são perigosos. C é a linguagem de um programador mestre. Pressupõe que você saiba o que diabos está fazendo e, assim, lhe dá o poder de realmente estragar as coisas. Embora alguns tipos de programas ainda precisem ser escritos em C, a maioria dos programas não precisa, e se você tiver uma linguagem que forneça uma melhor abstração para a diferença entre um objeto e seu identificador, sugiro que você o use.

De fato, em muitos aplicativos C ++ modernos, geralmente ocorre que qualquer aritmética de ponteiro necessária seja encapsulada e abstraída. Não queremos que os desenvolvedores façam aritmética de ponteiros em todo o lugar. Queremos uma API centralizada e bem testada que faça a aritmética dos ponteiros no nível mais baixo. A alteração desse código deve ser feita com muito cuidado e testes extensivos.


3

Penso que uma das razões pelas quais os ponteiros C são difíceis é que eles conflitam vários conceitos que não são realmente equivalentes; No entanto, como todos eles são implementados usando ponteiros, as pessoas podem ter dificuldade em desembaraçar os conceitos.

Em C, os ponteiros estão acostumados, entre outras coisas:

  • Definir estruturas de dados recursivas

Em C, você definiria uma lista vinculada de números inteiros como este:

struct node {
  int value;
  struct node* next;
}

O ponteiro está lá apenas porque esta é a única maneira de definir uma estrutura de dados recursiva em C, quando o conceito realmente não tem nada a ver com detalhes de baixo nível como endereços de memória. Considere o seguinte equivalente em Haskell, que não requer o uso de ponteiros:

data List = List Int List | Null

Bem direto - uma lista está vazia ou é formada a partir de um valor e do restante da lista.

  • Iterar sobre cadeias e matrizes

Veja como você pode aplicar uma função fooa todos os caracteres de uma string em C:

char *c;
for (c = "hello, world!"; *c != '\0'; c++) { foo(c); }

Apesar de também usar um ponteiro como iterador, este exemplo tem muito pouco em comum com o anterior. Criar um iterador que você pode incrementar é um conceito diferente de definir uma estrutura de dados recursiva. Nenhum dos conceitos está especialmente ligado à idéia de um endereço de memória.

  • Atingir o polimorfismo

Aqui está uma assinatura de função real encontrada em glib :

typedef struct g_list GList;

void  g_list_foreach    (GList *list,
                 void (*func)(void *data, void *user_data),
                         void* user_data);

Uau! Isso é um bocado de void*'s. E é apenas para declarar uma função que itera sobre uma lista que pode conter qualquer tipo de coisa, aplicando uma função a cada membro. Compare com como mapé declarado em Haskell:

map::(a->b)->[a]->[b]

Isso é muito mais direto: mapé uma função que pega uma função que converte an aem a be a aplica a uma lista de a's' para produzir uma lista de b's. Assim como na função C g_list_foreach, mapnão precisa saber nada em sua própria definição sobre os tipos aos quais será aplicada.

Resumindo:

Eu acho que os ponteiros C seriam muito menos confusos se as pessoas aprendessem sobre estruturas de dados recursivas, iteradores, polimorfismo etc. como conceitos separados e depois aprendessem como os ponteiros podem ser usados ​​para implementar essas idéias em C , em vez de mascarar todas essas conceitos juntos em um único assunto de "ponteiros".


Uso indevido c != NULLno seu exemplo "Olá, mundo" ... você quer dizer *c != '\0'.
Olaf Seibert

2

Eu acho que requer uma base sólida, provavelmente do nível da máquina, com a introdução de alguns códigos de máquina, montagem e como representar itens e estrutura de dados na RAM. Demora um pouco de tempo, algum trabalho de casa ou prática de resolução de problemas e algum pensamento.

Mas se uma pessoa conhece linguagens de alto nível a princípio (o que não está errado - um carpinteiro usa um machado. Uma pessoa que precisa dividir átomo usa outra coisa. Precisamos de pessoas que são carpinteiros e temos pessoas que estudam átomos) e essa pessoa que conhece a linguagem de alto nível recebe uma introdução de 2 minutos aos ponteiros e, em seguida, é difícil esperar que ele entenda a aritmética de ponteiros, ponteiros para ponteiros, matriz de ponteiros para cadeias de tamanho variável e matriz de matriz de caracteres, etc. Uma base sólida de baixo nível pode ajudar muito.


2
Os ponteiros de groking não requerem um entendimento do código ou da montagem da máquina.
jason

Exigir, não. Mas as pessoas que entendem montagem provavelmente acham os indicadores muito, muito mais fáceis, pois já fizeram a maioria (se não todas) das conexões mentais necessárias.
precisa

2

O problema que sempre tive (principalmente autodidata) é o "quando" usar um ponteiro. Posso envolver minha sintaxe na construção de um ponteiro, mas preciso saber sob quais circunstâncias um ponteiro deve ser usado.

Eu sou o único com essa mentalidade? ;-)


Entendi. Minha resposta meio que lida com isso.
J. Polfer

2

Era uma vez ... Tínhamos microprocessadores de 8 bits e todos escreviam em montagem. A maioria dos processadores incluía algum tipo de endereçamento indireto usado para tabelas e kernels de salto. Quando as linguagens de nível superior surgiram, adicionamos uma fina camada de abstração e as denominamos ponteiros. Ao longo dos anos, nos afastamos cada vez mais do hardware. Isto não é necessariamente uma coisa ruim. Eles são chamados de idiomas de nível superior por um motivo. Quanto mais eu puder me concentrar no que quero fazer, em vez de nos detalhes de como isso é feito, melhor.


2

Parece que muitos estudantes têm um problema com o conceito de indireção, principalmente quando encontram o conceito de indireção pela primeira vez. Lembro-me de quando eu era um estudante que, dos mais de 100 alunos do meu curso, apenas um punhado de pessoas realmente entendeu as dicas.

O conceito de indireção não é algo que costumamos usar na vida real e, portanto, é um conceito difícil de entender inicialmente.


2

Recentemente, tive apenas o momento do clique do ponteiro e fiquei surpreso por achar isso confuso. Era mais que todo mundo falava tanto sobre isso, que eu assumi que alguma magia negra estava acontecendo.

O jeito que eu entendi foi isso. Imagine que todas as variáveis ​​definidas recebam espaço de memória em tempo de compilação (na pilha). Se você deseja um programa capaz de lidar com grandes arquivos de dados, como áudio ou imagens, não deseja uma quantidade fixa de memória para essas estruturas em potencial. Então, você espera até o tempo de execução para atribuir uma certa quantidade de memória para armazenar esses dados (na pilha).

Depois de ter seus dados na memória, você não deseja copiar esses dados em todo o barramento de memória toda vez que desejar executar uma operação nele. Digamos que você queira aplicar um filtro aos seus dados de imagem. Você tem um ponteiro que inicia na frente dos dados que você atribuiu à imagem e uma função percorre esses dados, alterando-os no lugar. Se você não soubesse o que estamos fazendo, provavelmente acabaria criando duplicatas de dados à medida que a executava na operação.

Pelo menos é assim que eu vejo no momento!


Como uma reflexão tardia, você pode definir uma quantidade fixa de memória para armazenar imagens / áudio / vídeo, digamos em um dispositivo com memória limitada, mas então você terá que lidar com algum tipo de solução de streaming dentro e fora da memória.
Chris Barry

1

Falando como um novato em C ++ aqui:

O sistema de ponteiros demorou um pouco para eu digerir não necessariamente por causa do conceito, mas por causa da sintaxe C ++ relativa ao Java. Algumas coisas que achei confusas são:

(1) Declaração variável:

A a(1);

vs.

A a = A(1);

vs.

A* a = new A(1); 

e aparentemente

A a(); 

é uma declaração de função e não uma declaração de variável. Em outros idiomas, há basicamente apenas uma maneira de declarar uma variável.

(2) O e comercial é usado de algumas maneiras diferentes. Se for

int* i = &a;

então o & a é um endereço de memória.

OTOH, se for

void f(int &a) {}

então o & a é um parâmetro passado por referência.

Embora isso possa parecer trivial, pode ser confuso para novos usuários - eu vim do Java e o Java é uma linguagem com um uso mais uniforme dos operadores

(3) Relação matriz-ponteiro

Uma coisa que é um pouco frustrante de entender é que um ponteiro

int* i

pode ser um ponteiro para um int

int *i = &n; // 

ou

pode ser uma matriz para um int

int* i = new int[5];

E apenas para deixar as coisas mais confusas, ponteiros e matriz não são intercambiáveis ​​em todos os casos e ponteiros não podem ser transmitidos como parâmetros da matriz.

Isso resume algumas das frustrações básicas que tive com o C / C ++ e seus indicadores, que a IMO é bastante composta pelo fato de o C / C ++ ter todas essas peculiaridades específicas da linguagem.


O C ++ 2011 melhorou bastante as coisas no que diz respeito às declarações de variáveis.
precisa saber é o seguinte

0

Eu, pessoalmente, não entendi o ponteiro, mesmo depois da minha pós-graduação e depois do meu primeiro emprego. A única coisa que eu sabia é que você precisa disso para lista vinculada, árvores binárias e para passar matrizes para funções. Esta foi a situação, mesmo no meu primeiro emprego. Somente quando comecei a dar entrevistas, entendi que o conceito de ponteiro é profundo e tem um tremendo uso e potencial. Então comecei a ler K & R e a escrever o próprio programa de teste. Todo o meu objetivo era orientado para o trabalho.
Nesse momento, descobri que os indicadores não são realmente ruins nem difíceis se forem ensinados de uma maneira boa. Infelizmente, quando eu aprendi C na graduação, o professor não estava ciente do ponteiro, e até as tarefas estavam usando menos ponteiros. No nível de pós-graduação, o uso do ponteiro é realmente apenas criar árvores binárias e lista vinculada. Esse pensamento de que você não precisa entender adequadamente os ponteiros para trabalhar com eles, acaba com a ideia de aprendê-los.


0

Ponteiros .. hah .. tudo sobre ponteiro na minha cabeça é que ele fornece um endereço de memória em que os valores reais sejam qual for sua referência .. então não há mágica nisso .. se você aprender alguma montagem, não terá muito problema em aprender como funciona o ponteiro .. vamos lá pessoal ... mesmo em Java tudo é uma referência ..


0

O principal problema que as pessoas não entendem por que precisam de indicadores. Porque eles não são claros sobre pilha e pilha. É bom começar a partir do assembler de 16 bits para x86 com modo de memória minúsculo. Ajudou muitas pessoas a ter idéia da pilha, pilha e "endereço". E byte :) Os programadores modernos às vezes não conseguem dizer quantos bytes você precisa para endereçar o espaço de 32 bits. Como eles podem ter uma idéia dos ponteiros?

O segundo momento é a notação: você declara o ponteiro como *, obtém o endereço como & e isso não é fácil de entender para algumas pessoas.

E a última coisa que vi foi um problema de armazenamento: eles entendem a pilha e a pilha, mas não conseguem entrar na ideia de "estática".

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.