Como explicar ponteiros C (declaração vs. operadores unários) para um iniciante?

141

Tive o prazer recente de explicar indicadores para um iniciante em programação C e me deparei com a seguinte dificuldade. Pode não parecer um problema, se você já sabe usar ponteiros, mas tente observar o exemplo a seguir com uma mente clara:

int foo = 1;
int *bar = &foo;
printf("%p\n", (void *)&foo);
printf("%i\n", *bar);

Para o iniciante absoluto, a saída pode ser surpreendente. Na linha 2, ele / ela havia acabado de declarar * bar como & foo, mas na linha 4 acontece * bar é realmente foo em vez de & foo!

A confusão, você pode dizer, deriva da ambiguidade do símbolo *: na linha 2, é usado para declarar um ponteiro. Na linha 4, é usado como um operador unário que busca o valor em que o ponteiro aponta. Duas coisas diferentes, certo?

No entanto, essa "explicação" não ajuda em nada um iniciante. Introduz um novo conceito, apontando uma discrepância sutil. Este não pode ser o caminho certo para ensiná-lo.

Então, como Kernighan e Ritchie explicaram isso?

O operador unário * é o operador de indireção ou desreferenciação; quando aplicado a um ponteiro, ele acessa o objeto apontado pelo ponteiro. [...]

A declaração do ponteiro ip, int *ippretende ser um mnemônico; diz que a expressão *ipé um int. A sintaxe da declaração para uma variável imita a sintaxe das expressões nas quais a variável pode aparecer .

int *ipdeve ser lido como " *ipretornará um int"? Mas por que então a tarefa após a declaração não segue esse padrão? E se um iniciante quiser inicializar a variável? int *ip = 1(leia: *ipretornará um inte o inté 1) não funcionará conforme o esperado. O modelo conceitual simplesmente não parece coerente. Estou faltando alguma coisa aqui?


Edit: Tentou resumir as respostas aqui .

armin
fonte
15
A melhor explicação é desenhar coisas em um papel e conectá-las com setas;)
Maroun
16
Quando tive que explicar a sintaxe dos ponteiros, sempre insisti no fato de que *em uma declaração é um token que significa "declarar um ponteiro", nas expressões é o operador de desreferência e que esses dois representam coisas diferentes que têm o mesmo símbolo (igual ao operador de multiplicação - mesmo símbolo, significado diferente). É confuso, mas qualquer coisa diferente do estado real das coisas será ainda pior.
Matteo Italia
40
talvez escrevê-lo para int* bartornar mais óbvio que a estrela é realmente parte do tipo, não parte do identificador. Claro que isso leva a problemas diferentes com coisas não intuitivas, como int* a, b.
Niklas B.
9
Eu sempre pensei que a explicação da K&R é boba e desnecessária. A linguagem usa o mesmo símbolo para duas coisas diferentes e nós apenas temos que lidar com isso. *pode ter dois significados diferentes, dependendo do contexto. Assim como a mesma letra pode ser pronunciada de maneira diferente, dependendo da palavra em que ela fica difícil de aprender a falar muitas línguas. Se todo conceito / operação tivesse seu próprio símbolo, precisaríamos de teclados muito maiores, para que os símbolos sejam reciclados quando fizer sentido.
Art
8
Eu já tive o mesmo problema muitas vezes ao ensinar C para outras pessoas e, na minha experiência, ele pode ser resolvido da maneira que a maioria das pessoas aqui sugeriu. Primeiro, explique o conceito de um ponteiro sem a sintaxe C. Em seguida, ensine a sintaxe e enfatize o asterisco como parte do tipo ( int* p), enquanto avisa o aluno contra o uso de várias declarações na mesma linha quando houver ponteiros. Quando o aluno entender completamente o conceito de ponteiros, explique ao aluno que a int *psintaxe é equivalente e depois explique o problema com várias declarações.
Theodoros Chatzigiannakis

Respostas:

43

Para que seu aluno entenda o significado do *símbolo em diferentes contextos, ele deve primeiro entender que os contextos são realmente diferentes. Depois que eles entendem que os contextos são diferentes (ou seja, a diferença entre o lado esquerdo de uma tarefa e uma expressão geral), não é um salto cognitivo entender muito bem quais são as diferenças.

Em primeiro lugar, explique que a declaração de uma variável não pode conter operadores (demonstre isso mostrando que colocar um -ou +símbolo em uma declaração de variável simplesmente causa um erro). Depois, mostre que uma expressão (ou seja, no lado direito de uma atribuição) pode conter operadores. Certifique-se de que o aluno entenda que uma expressão e uma declaração variável são dois contextos completamente diferentes.

Quando eles entenderem que os contextos são diferentes, você pode explicar que quando o *símbolo está em uma declaração de variável na frente do identificador de variável, significa 'declarar essa variável como um ponteiro'. Em seguida, você pode explicar que, quando usado em uma expressão (como operador unário), o *símbolo é o 'operador de desreferência' e significa 'o valor no endereço de' em vez de seu significado anterior.

Para realmente convencer seu aluno, explique que os criadores de C poderiam ter usado qualquer símbolo para significar o operador de desreferência (isto é, eles poderiam ter usado @), mas por qualquer motivo, eles decidiram usar *.

Em suma, não há como explicar que os contextos são diferentes. Se o aluno não compreender os contextos são diferentes, eles não podem entender por que o *símbolo pode significar coisas diferentes.

Pharap
fonte
80

A razão pela qual a taquigrafia:

int *bar = &foo;

no seu exemplo, pode ser confuso: é fácil interpretá-lo erroneamente como sendo equivalente a:

int *bar;
*bar = &foo;    // error: use of uninitialized pointer bar!

quando realmente significa:

int *bar;
bar = &foo;

Escrito dessa maneira, com a variável declaração e atribuição separadas, não existe esse potencial de confusão, e o paralelismo de declaração de uso descrito em sua citação de K&R funciona perfeitamente:

  • A primeira linha declara uma variável bar, tal que *baré umaint .

  • A segunda linha atribui o endereço de foopara bar, tornando *bar(an int) um alias para foo(também um int).

Ao introduzir a sintaxe do ponteiro C para iniciantes, pode ser útil, inicialmente, seguir esse estilo de separar declarações de ponteiro de atribuições e introduzir apenas a sintaxe abreviada combinada (com avisos apropriados sobre seu potencial de confusão) quando os conceitos básicos de ponteiro forem usados. C foram adequadamente internalizados.

Ilmari Karonen
fonte
4
Eu ficaria tentado typedef. typedef int *p_int;significa que uma variável do tipo p_inttem a propriedade que *p_inté um int. Então nós temos p_int bar = &foo;. Incentivar qualquer pessoa a criar dados não inicializados e depois atribuí-los por uma questão de hábito padrão parece ... uma má idéia.
Yakk - Adam Nevraumont
6
Esse é apenas o estilo danificado pelo cérebro das declarações em C; não é específico para ponteiros. considere int a[2] = {47,11};, que não é uma inicialização do elemento (inexistente) a[2]ouiher.
Marc van Leeuwen
5
@MarcvanLeeuwen Concorde com o dano cerebral. Idealmente, *deve fazer parte do tipo, não vinculado à variável, e você poderá escrever int* foo_ptr, bar_ptrpara declarar dois ponteiros. Mas na verdade declara um ponteiro e um número inteiro.
Barmar
1
Não se trata apenas de declarações / atribuições "abreviadas". A questão toda surge novamente no momento em que você deseja usar ponteiros como argumentos de função.
Armin
30

Breves declarações

É bom saber a diferença entre declaração e inicialização. Declaramos variáveis ​​como tipos e as inicializamos com valores. Se fizermos as duas coisas ao mesmo tempo, geralmente chamamos de definição.

1. int a; a = 42;

int a;
a = 42;

Nós declaramos um intchamado um . Em seguida, inicializamos dando um valor a ele 42.

2. int a = 42;

Nós declaramos e intnomeado um e dar-lhe o valor de 42. Ele é inicializado com 42. Uma definição.

3. a = 43;

Quando usamos as variáveis, dizemos que operamos sobre elas. a = 43é uma operação de atribuição. Atribuímos o número 43 à variável a.

Dizendo

int *bar;

declaramos que a barra é um ponteiro para um int. Dizendo

int *bar = &foo;

declaramos bar e inicializá-lo com o endereço do foo .

Depois de inicializar a barra , podemos usar o mesmo operador, o asterisco, para acessar e operar com o valor de foo . Sem o operador, acessamos e operamos no endereço apontado pelo ponteiro.

Além disso, deixei a imagem falar.

o que

Uma ASCIIMAÇÃO simplificada sobre o que está acontecendo. (E aqui uma versão do player, se você quiser fazer uma pausa, etc.)

          ASCIIMAÇÃO

Morpfh
fonte
22

A 2ª afirmação int *bar = &foo;pode ser vista pictoricamente na memória como,

   bar           foo
  +-----+      +-----+
  |0x100| ---> |  1  |
  +-----+      +-----+ 
   0x200        0x100

Agora baré um ponteiro do tipo que intcontém o endereço &de foo. Usando o operador unário *, deferimos para recuperar o valor contido em 'foo' usando o ponteiro bar.

EDIT : Minha abordagem com iniciantes é explicar o memory addressde uma variável ou seja

Memory Address:Cada variável tem um endereço associado a ele fornecido pelo sistema operacional. In int a;, &aé o endereço da variável a.

Continue explicando os tipos básicos de variáveis ​​em Cas,

Types of variables: Variáveis ​​podem conter valores dos respectivos tipos, mas não endereços.

int a = 10; float b = 10.8; char ch = 'c'; `a, b, c` are variables. 

Introducing pointers: Como dito acima, variáveis, por exemplo

 int a = 10; // a contains value 10
 int b; 
 b = &a;      // ERROR

É possível atribuir, b = amas não b = &a, uma vez que a variável bpode conter valor, mas não o endereço, portanto, precisamos de Ponteiros .

Pointer or Pointer variables :Se uma variável contém um endereço, é conhecida como variável de ponteiro. Use *na declaração para informar que é um ponteiro.

 Pointer can hold address but not value
 Pointer contains the address of an existing variable.
 Pointer points to an existing variable
Sunil Bojanapally
fonte
3
O problema é que, ao ler int *ip"ip é um ponteiro (*) do tipo int", você fica com problemas ao ler algo parecido x = (int) *ip.
Armin
2
@abw Isso é algo completamente diferente, daí os parênteses. Eu não acho que as pessoas terão dificuldade em entender a diferença entre declarações e elenco.
bzeaman
@abw In x = (int) *ip;, obtenha o valor desreferenciando o ponteiro ipe converta o valor para intqualquer tipo ip.
Sunil Bojanapally
1
@BennoZeeman Você está certo: elenco e declarações são duas coisas diferentes. Tentei sugerir o papel diferente do asterisco: 1º "este não é um int, mas um ponteiro para int" 2º "isso fornecerá o int, mas não o ponteiro para int".
Armin
2
@abw: É por isso que o ensino int* bar = &foo;torna cargas mais sentido. Sim, eu sei que isso causa problemas quando você declara vários ponteiros em uma única declaração. Não, acho que isso não importa.
Lightness Races in Orbit
17

Observando as respostas e os comentários aqui, parece haver um acordo geral de que a sintaxe em questão pode ser confusa para um iniciante. A maioria deles propõe algo nesse sentido:

  • Antes de mostrar qualquer código, use diagramas, esboços ou animações para ilustrar como os ponteiros funcionam.
  • Ao apresentar a sintaxe, explique as duas funções diferentes do símbolo do asterisco . Muitos tutoriais estão faltando ou fugindo dessa parte. Ocorre confusão ("Quando você divide uma declaração inicial de ponteiro em uma declaração e em uma tarefa posterior, lembre-se de remover a *" - comp.lang.c FAQ ) Eu esperava encontrar uma abordagem alternativa, mas acho que isso é o caminho para seguir.

Você pode escrever em int* barvez de int *bardestacar a diferença. Isso significa que você não seguirá a abordagem K&R "declaração imita o uso", mas a abordagem Stroustrup C ++ :

Não declaramos *barser um número inteiro. Declaramos barser um int*. Se queremos inicializar uma variável recém-criada na mesma linha, é claro que estamos lidando com isso bar, não *bar.int* bar = &foo;

As desvantagens:

  • Você precisa avisar seu aluno sobre o problema da declaração de vários ponteiros ( int* foo, barvs int *foo, *bar).
  • Você tem que prepará-los para um mundo de mágoa . Muitos programadores desejam ver o asterisco adjacente ao nome da variável e farão um grande esforço para justificar seu estilo. E muitos guias de estilo aplicam essa notação explicitamente (estilo de codificação do kernel Linux, Guia de Estilo C da NASA, etc.).

Edit: Uma abordagem diferente que foi sugerida, é seguir o caminho "imitar" K&R, mas sem a sintaxe "abreviada" (veja aqui ). Assim que você deixar de fazer uma declaração e uma tarefa na mesma linha , tudo parecerá muito mais coerente.

No entanto, mais cedo ou mais tarde o aluno terá que lidar com ponteiros como argumentos de função. E ponteiros como tipos de retorno. E ponteiros para funções. Você terá que explicar a diferença entre int *func();e int (*func)();. Acho que mais cedo ou mais tarde as coisas vão desmoronar. E talvez mais cedo seja melhor que mais tarde.

armin
fonte
16

Há uma razão pela qual os estilos K&R int *pe Stroustrup int* p; ambos são válidos (e significam a mesma coisa) em cada idioma, mas como Stroustrup colocou:

A escolha entre "int * p;" e "int * p"; não é sobre certo e errado, mas sobre estilo e ênfase. C enfatizou expressões; as declarações eram frequentemente consideradas pouco mais que um mal necessário. C ++, por outro lado, tem uma forte ênfase nos tipos.

Agora, como você está tentando ensinar C aqui, isso sugere que você deve enfatizar expressões mais do que tipos, mas algumas pessoas podem entender mais rapidamente uma ênfase mais rapidamente que a outra, e isso é mais sobre elas do que com o idioma.

Portanto, algumas pessoas acharão mais fácil começar com a idéia de que um int*é uma coisa diferente do que um inte partir daí.

Se alguém entender rapidamente a maneira de vê-la, que costuma int* barter baralgo que não é um int, mas um indicador int, então verá rapidamente*bar está fazendo algo para bar, eo resto se seguirá. Depois disso, você poderá explicar mais tarde por que os codificadores C tendem a preferir int *bar.

Ou não. Se houvesse uma maneira de todos entenderem o conceito pela primeira vez, você não teria problemas em primeiro lugar, e a melhor maneira de explicá-lo a uma pessoa não será necessariamente a melhor maneira de explicá-lo a outra.

Jon Hanna
fonte
1
Gosto do argumento de Stroustrup, mas me pergunto por que ele escolheu o símbolo & para denotar referências - outra possível armadilha.
armin
1
@ ABW Eu acho que ele viu simetria se podemos fazer, int* p = &aentão podemos fazer int* r = *p. Tenho certeza de que ele o abordou no The Design and Evolution of C ++ , mas já faz muito tempo que eu li isso, e tolamente inclinei minha cópia para alguém.
Jon Hanna
3
Eu acho que você quer dizer int& r = *p. E aposto que o mutuário ainda está tentando digerir o livro.
Armin #
@ ABW, sim, foi exatamente isso que eu quis dizer. Infelizmente, erros de digitação nos comentários não geram erros de compilação. O livro é realmente uma leitura bastante rápida.
Jon Hanna
4
Uma das razões pelas quais eu prefiro a sintaxe de Pascal (como popularmente estendida) sobre os C's é que Var A, B: ^Integer;deixa claro que o tipo "ponteiro para inteiro" se aplica a ambos Ae B. Usar um K&Restilo int *a, *btambém é viável; mas uma declaração como int* a,b;, no entanto, parece ser ae bestá sendo declarada como int*, mas na realidade ela declara acomo um int*e bcomo um int.
Super dec
9

tl; dr:

P: Como explicar os ponteiros C (declaração vs. operadores unários) para um iniciante?

A: não. Explique os ponteiros para o iniciante e mostre a eles como representar seus conceitos de ponteiro na sintaxe C depois.


Tive o prazer recente de explicar indicadores para um iniciante em programação C e me deparei com a seguinte dificuldade.

A sintaxe C da IMO não é horrível, mas também não é maravilhosa: não é um grande obstáculo se você já entende os ponteiros, nem ajuda em aprendê-los.

Portanto: comece explicando os ponteiros e verifique se eles realmente os entendem:

  • Explique-os com diagramas de caixa e seta. Você pode fazer isso sem endereços hexadecimais, se eles não forem relevantes, basta mostrar as setas apontando para outra caixa ou para algum símbolo nulo.

  • Explique com pseudocódigo: basta escrever o endereço de foo e o valor armazenado na barra .

  • Então, quando seu iniciante entender o que são ponteiros, e por que e como usá-los; em seguida, mostre o mapeamento na sintaxe C.

Suspeito que a razão pela qual o texto K&R não forneça um modelo conceitual seja porque eles já entenderam os indicadores , e provavelmente assumiram que todos os outros programadores competentes da época também o fizeram. O mnemônico é apenas um lembrete do mapeamento do conceito bem compreendido para a sintaxe.

Sem utilidade
fonte
De fato; Comece com a teoria primeiro, a sintaxe vem depois (e não é importante). Observe que a teoria do uso da memória não depende da linguagem. Este modelo de caixa e setas o ajudará com tarefas em qualquer linguagem de programação.
oɔɯǝɹ
Veja aqui alguns exemplos (embora o Google irá ajudar também) eskimo.com/~scs/cclass/notes/sx10a.html
oɔɯǝɹ
7

Esse problema é um pouco confuso ao começar a aprender C.

Aqui estão os princípios básicos que podem ajudar você a começar:

  1. Existem apenas alguns tipos básicos em C:

    • char: um valor inteiro com o tamanho de 1 byte.

    • short: um valor inteiro com o tamanho de 2 bytes.

    • long: um valor inteiro com o tamanho de 4 bytes.

    • long long: um valor inteiro com o tamanho de 8 bytes.

    • float: um valor não inteiro com o tamanho de 4 bytes.

    • double: um valor não inteiro com o tamanho de 8 bytes.

    Observe que o tamanho de cada tipo geralmente é definido pelo compilador e não pelo padrão.

    Os tipos inteiros short, longe long longsão normalmente seguidas por int.

    Não é obrigatório, no entanto, e você pode usá-los sem o int.

    Como alternativa, você pode apenas declarar int, mas isso pode ser interpretado de maneira diferente por diferentes compiladores.

    Então, para resumir isso:

    • shorté o mesmo que, short intmas não necessariamente o mesmo que int.

    • longé o mesmo que, long intmas não necessariamente o mesmo que int.

    • long longé o mesmo que, long long intmas não necessariamente o mesmo que int.

    • Em um determinado compilador, inté ou short intou long intou long long int.

  2. Se você declarar uma variável de algum tipo, também poderá declarar outra variável apontando para ela.

    Por exemplo:

    int a;

    int* b = &a;

    Portanto, em essência, para cada tipo básico, também temos um tipo de ponteiro correspondente.

    Por exemplo: shorte short*.

    Existem duas maneiras de "olhar para" a variável b (é isso que provavelmente confunde a maioria dos iniciantes) :

    • Você pode considerar bcomo uma variável do tipo int*.

    • Você pode considerar *bcomo uma variável do tipo int.

    Por isso, algumas pessoas declarariam int* b, enquanto outras declarariam int *b.

    Mas o fato é que essas duas declarações são idênticas (os espaços não têm sentido).

    Você pode usar bcomo um ponteiro para um valor inteiro ou *bcomo o valor inteiro apontado real.

    Você pode obter (ler) o valor pontas: int c = *b.

    E você pode definir (escrever) o valor pontas: *b = 5.

  3. Um ponteiro pode apontar para qualquer endereço de memória e não apenas para o endereço de alguma variável que você declarou anteriormente. No entanto, você deve ter cuidado ao usar ponteiros para obter ou definir o valor localizado no endereço de memória apontado.

    Por exemplo:

    int* a = (int*)0x8000000;

    Aqui, temos variáveis aapontando para o endereço de memória 0x8000000.

    Se esse endereço de memória não estiver mapeado no espaço de memória do seu programa, *aé provável que qualquer operação de leitura ou gravação que esteja causando o travamento do programa, devido a uma violação de acesso à memória.

    Você pode alterar com segurança o valor de a, mas deve ter muito cuidado ao alterar o valor de *a.

  4. O tipo void*é excepcional no fato de que não possui um "tipo de valor" correspondente que pode ser usado (ou seja, você não pode declarar void a). Esse tipo é usado apenas como um ponteiro geral para um endereço de memória, sem especificar o tipo de dados que reside nesse endereço.

barak manos
fonte
7

Talvez percorrer um pouco mais o torne mais fácil:

#include <stdio.h>

int main()
{
    int foo = 1;
    int *bar = &foo;
    printf("%i\n", foo);
    printf("%p\n", &foo);
    printf("%p\n", (void *)&foo);
    printf("%p\n", &bar);
    printf("%p\n", bar);
    printf("%i\n", *bar);
    return 0;
}

Peça que eles digam o que eles esperam que a saída esteja em cada linha, depois faça com que eles executem o programa e vejam o que acontece. Explique as perguntas deles (a versão simplificada certamente levará alguns - mas você pode se preocupar com estilo, rigidez e portabilidade posteriormente). Então, antes que a mente deles pareça exagerada ou se tornem um zumbi depois do almoço, escreva uma função que tenha um valor e a mesma que use um ponteiro.

Na minha experiência, foi superando esse "por que isso é impresso dessa maneira?" hump e , imediatamente, mostrando por que isso é útil em parâmetros de função, brincando (como um prelúdio para algum material básico de K&R, como análise de strings / processamento de array) que faz com que a lição não faça apenas sentido, mas permaneça.

O próximo passo é fazer com que eles expliquem a você como i[0]se relaciona &i. Se eles puderem fazer isso, eles não esquecerão e você pode começar a falar sobre estruturas, mesmo um pouco antes do tempo, apenas para que afunde.

As recomendações acima sobre caixas e flechas também são boas, mas também podem terminar em uma discussão completa sobre como a memória funciona - que é uma palestra que deve acontecer em algum momento, mas pode desviar o foco do ponto imediatamente à mão. : como interpretar a notação do ponteiro em C.

zxq9
fonte
Este é um bom exercício. Mas a questão que eu queria abordar é uma questão sintática específica que pode ter um impacto no modelo mental que os alunos constroem. Considere o seguinte: int foo = 1;. Agora, este é OK: int *bar; *bar = foo;. Isso não está bom:int *bar = foo;
armin
1
@abw A única coisa que faz sentido é o que os alunos acabam dizendo a si mesmos. Isso significa "ver um, fazer um, ensinar um". Você não pode proteger ou prever qual sintaxe ou estilo eles verão na selva (até mesmo nos seus antigos repositórios!), Portanto, você precisa mostrar permutações suficientes para que os conceitos básicos sejam entendidos independentemente do estilo - e então comece a ensinar a eles por que certos estilos foram estabelecidos. Como ensinar inglês: expressão básica, expressões idiomáticas, estilos, estilos particulares em um determinado contexto. Não é fácil, infelizmente. De qualquer forma, boa sorte!
Zxq9
6

O tipo da expressão *bar é int; assim, o tipo da variável (e expressão) baré int *. Como a variável possui o tipo de ponteiro, seu inicializador também deve ter o tipo de ponteiro.

Há uma inconsistência entre a inicialização e a atribuição da variável do ponteiro; isso é algo que precisa ser aprendido da maneira mais difícil.

John Bode
fonte
3
Olhando para as respostas aqui, tenho a sensação de que muitos programadores experientes não conseguem mais ver o problema . Eu acho que é um subproduto de "aprender a viver com inconsistências".
Armin #
3
@abw: as regras para inicialização são diferentes das regras para atribuição; para tipos aritméticos escalares, as diferenças são insignificantes, mas são importantes para os tipos ponteiro e agregado. Isso é algo que você precisará explicar junto com todo o resto.
John Bode
5

Prefiro lê-lo como o primeiro se *aplica a intmais de bar.

int  foo = 1;           // foo is an integer (int) with the value 1
int* bar = &foo;        // bar is a pointer on an integer (int*). it points on foo. 
                        // bar value is foo address
                        // *bar value is foo value = 1

printf("%p\n", &foo);   // print the address of foo
printf("%p\n", bar);    // print the address of foo
printf("%i\n", foo);    // print foo value
printf("%i\n", *bar);   // print foo value
grorel
fonte
2
Então você tem que explicar por int* a, bque não faz o que eles pensam que faz.
Pharap
4
É verdade, mas não acho que int* a,bdeva ser usado. Para melhor visibilidade, atualização, etc ... deve haver apenas uma declaração de variável por linha e nunca mais. Também é algo para explicar aos iniciantes, mesmo que o compilador possa lidar com isso.
grorel
Essa é a opinião de um homem. Existem milhões de programadores por aí que estão completamente bem em declarar mais de uma variável por linha e fazem isso diariamente como parte de seus trabalhos. Você não pode ocultar os alunos de maneiras alternativas de fazer as coisas, é melhor mostrar a elas todas as alternativas e deixá-las decidir em que sentido elas querem fazer as coisas, porque, se alguma vez se tornarem empregadas, espera-se que elas sigam um certo estilo que eles podem ou não se sentir confortáveis. Para um programador, a versatilidade é uma característica muito boa de se ter.
Pharap
1
Eu concordo com @grorel. É mais fácil pensar *como parte do tipo e simplesmente desencorajar int* a, b. A menos que você prefere dizer que *aé do tipo intem vez de aum ponteiro para int...
Kevin Ushey
@Grelel está certo: int *a, b;não deve ser usado. Declarar duas variáveis com tipos diferentes na mesma instrução é uma prática muito ruim e uma forte candidata a problemas de manutenção na linha. Talvez seja diferente para aqueles de nós que trabalham no campo incorporado, onde um int*e um intgeralmente têm tamanhos diferentes e às vezes são armazenados em locais de memória completamente diferentes. É um dos muitos aspectos da linguagem C que seria melhor ensinado como 'é permitido, mas não faça'.
Mal Dog Pie
5
int *bar = &foo;

Question 1: O que é bar?

Ans: É uma variável de ponteiro (para digitar int). Um ponteiro deve apontar para algum local válido da memória e posteriormente deve ser desreferenciado (* bar) usando um operador unário *para ler o valor armazenado nesse local.

Question 2: O que é &foo?

Ans: foo é uma variável do tipo int.que é armazenada em algum local válido da memória e nesse local nós o obtemos do operador &; agora, o que temos é um local válido na memória &foo.

Então, ambos juntos, ou seja, o que o ponteiro precisava era de um local de memória válido e isso é obtido, &foopara que a inicialização seja boa.

Agora, o ponteiro barestá apontando para a localização válida da memória e o valor armazenado nela pode ser retirado da referência.*bar

Gopi
fonte
5

Você deve indicar um iniciante que * tenha significado diferente na declaração e na expressão. Como você sabe, * na expressão é um operador unário e * Na declaração não é um operador e apenas um tipo de sintaxe combinada com o tipo para permitir que o compilador saiba que é um tipo de ponteiro. é melhor dizer um iniciante, "* tem um significado diferente. Para entender o significado de *, você deve descobrir onde * é usado"

Yongkil Kwon
fonte
4

Eu acho que o diabo está no espaço.

Eu escreveria (não apenas para iniciantes, mas também para mim): int * bar = & foo; em vez de int * bar = & foo;

isso deve tornar evidente qual é a relação entre sintaxe e semântica

rpaulin56
fonte
4

Já foi observado que * possui várias funções.

Há outra idéia simples que pode ajudar um iniciante a entender as coisas:

Pense que "=" também tem várias funções.

Quando a atribuição é usada na mesma linha da declaração, pense nela como uma chamada de construtor, não como uma atribuição arbitrária.

Quando você vê:

int *bar = &foo;

Pense que é quase equivalente a:

int *bar(&foo);

Os parênteses têm precedência sobre o asterisco, portanto, "& foo" é muito mais facilmente atribuído intuitivamente a "bar" do que a "* bar".

morfizm
fonte
4

Vi essa pergunta há alguns dias e depois estava lendo a explicação da declaração de tipo de Go no Go Blog . Ele começa fornecendo uma conta das declarações do tipo C, que parece ser um recurso útil para adicionar a esse segmento, embora eu ache que já existem respostas mais completas.

C adotou uma abordagem incomum e inteligente da sintaxe da declaração. Em vez de descrever os tipos com sintaxe especial, escreve-se uma expressão envolvendo o item que está sendo declarado e declara que tipo essa expressão terá. portanto

int x;

declara x como int: a expressão 'x' terá o tipo int. Em geral, para descobrir como escrever o tipo de uma nova variável, escreva uma expressão envolvendo essa variável que seja avaliada como um tipo básico e coloque o tipo básico à esquerda e a expressão à direita.

Assim, as declarações

int *p;
int a[3];

declare que p é um ponteiro para int porque '* p' possui o tipo int e que a é uma matriz de entradas porque a [3] (ignorando o valor específico do índice, que é punido pelo tamanho da matriz) possui o tipo int.

(Ele continua descrevendo como estender esse entendimento para ponteiros de função, etc.)

Essa é uma maneira que eu nunca pensei sobre isso antes, mas parece uma maneira bastante direta de explicar a sobrecarga da sintaxe.

Andy Turner
fonte
3

Se o problema for a sintaxe, pode ser útil mostrar código equivalente com template / using.

template<typename T>
using ptr = T*;

Isso pode ser usado como

ptr<int> bar = &foo;

Depois disso, compare a sintaxe normal / C com essa abordagem somente em C ++. Isso também é útil para explicar ponteiros const.

MI3Guy
fonte
2
Para iniciantes, será muito mais confuso.
Karsten
Meu pensamento foi que você não mostraria a definição de ptr. Apenas use-o para declarações de ponteiro.
MI3Guy
3

A fonte da confusão surge do fato de que o *símbolo pode ter significados diferentes em C, dependendo do fato em que é usado. Para explicar o ponteiro para um iniciante, o significado de* símbolo em diferentes contextos deve ser explicado.

Na declaração

int *bar = &foo;  

o *símbolo não é o operador indireto . Em vez disso, ajuda a especificar o tipo de barinformação ao compilador que baré um ponteiro para umint . Por outro lado, quando aparece em uma declaração, o *símbolo (quando usado como operador unário ) executa indiretamente. Portanto, a declaração

*bar = &foo;

estaria errado, pois atribui o endereço de foopara o objeto que baraponta para, não para barsi mesmo.

haccks
fonte
3

"talvez escrevê-lo como int * bar torne mais óbvio que a estrela é realmente parte do tipo, não parte do identificador". Então eu faço. E eu digo que é algo como Type, mas apenas para um nome de ponteiro.

"É claro que isso gera problemas diferentes com coisas não intuitivas, como int * a, b."

Павел Бивойно
fonte
2

Aqui você tem que usar, entender e explicar a lógica do compilador, não a lógica humana (eu sei, você é um humano, mas aqui deve imitar o computador ...).

Quando você escreve

int *bar = &foo;

os grupos de compiladores que, como

{ int * } bar = &foo;

Ou seja: aqui está uma nova variável, seu nome é bar, seu tipo é ponteiro para int e seu valor inicial é &foo.

E você deve adicionar: as =denota acima uma inicialização não uma afetação, enquanto que no seguintes expressões *bar = 2;que é uma afetação

Editar por comentário:

Cuidado: no caso de declaração múltipla, isso *está relacionado apenas à seguinte variável:

int *bar = &foo, b = 2;

bar é um ponteiro para int inicializado pelo endereço de foo, b é um int inicializado para 2 e em

int *bar=&foo, **p = &bar;

bar no ponteiro imóvel para int ep é um ponteiro para um ponteiro para um int inicializado no endereço ou barra.

Serge Ballesta
fonte
2
Na verdade, o compilador não o agrupa assim: int* a, b;declara a como ponteiro para an int, mas b como an int. O *símbolo tem apenas dois significados distintos: em uma declaração, indica um tipo de ponteiro e, em uma expressão, é o operador de desreferência unária.
tmlen
@tmlen: O que eu quis dizer é que na inicialização, o *rattached ao tipo, para que o ponteiro seja inicializado, enquanto que em uma ação afetada o valor apontado é afetado. Mas pelo menos você me deu um chapéu agradável :-)
Serge Ballesta
0

Basicamente, o ponteiro não é uma indicação de matriz. Iniciante facilmente pensa que o ponteiro se parece com matriz. a maioria dos exemplos de strings usando o

"char * pstr" é semelhante a

"char str [80]"

Mas, coisas importantes, o ponteiro é tratado como apenas um número inteiro no nível mais baixo do compilador.

Vejamos exemplos:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv, char **env)
{
    char str[] = "This is Pointer examples!"; // if we assume str[] is located in 0x80001000 address

    char *pstr0 = str;   // or this will be using with
    // or
    char *pstr1 = &str[0];

    unsigned int straddr = (unsigned int)pstr0;

    printf("Pointer examples: pstr0 = %08x\n", pstr0);
    printf("Pointer examples: &str[0] = %08x\n", &str[0]);
    printf("Pointer examples: str = %08x\n", str);
    printf("Pointer examples: straddr = %08x\n", straddr);
    printf("Pointer examples: str[0] = %c\n", str[0]);

    return 0;
}

Os resultados serão 0x2a6b7ed0 como endereço de str []

~/work/test_c_code$ ./testptr
Pointer examples: pstr0 = 2a6b7ed0
Pointer examples: &str[0] = 2a6b7ed0
Pointer examples: str = 2a6b7ed0
Pointer examples: straddr = 2a6b7ed0
Pointer examples: str[0] = T

Portanto, basicamente, lembre-se de que ponteiro é algum tipo de número inteiro. apresentando o endereço.

cpplover - Slw Essencial
fonte
-1

Eu explicaria que ints são objetos, assim como floats, etc. Um ponteiro é um tipo de objeto cujo valor representa um endereço na memória (daí o motivo de um ponteiro usar como padrão NULL).

Quando você declara um ponteiro pela primeira vez, usa a sintaxe type-pointer-name. É lido como um "ponteiro inteiro chamado nome que pode apontar para o endereço de qualquer objeto inteiro". Nós usamos essa sintaxe apenas durante a decleração, semelhante à forma como declaramos um int como 'int num1', mas usamos apenas 'num1' quando queremos usar essa variável, não 'int num1'.

int x = 5; // um objeto inteiro com um valor de 5

int * ptr; // um número inteiro com um valor NULL por padrão

Para fazer um ponteiro apontar para o endereço de um objeto, usamos o símbolo '&' que pode ser lido como "o endereço de".

ptr = & x; // now value é o endereço de 'x'

Como o ponteiro é apenas o endereço do objeto, para obter o valor real mantido nesse endereço, devemos usar o símbolo '*' que, quando usado antes de um ponteiro, significa "o valor no endereço apontado por".

std :: cout << * ptr; // imprime o valor no endereço

Você pode explicar brevemente que ' ' é um 'operador' que retorna resultados diferentes com diferentes tipos de objetos. Quando usado com um ponteiro, o ' operador ' não significa mais "multiplicado por".

Ajuda a desenhar um diagrama mostrando como uma variável tem um nome e um valor e um ponteiro tem um endereço (o nome) e um valor e mostra que o valor do ponteiro será o endereço do int.

user2796283
fonte
-1

Um ponteiro é apenas uma variável usada para armazenar endereços.

A memória em um computador é composta de bytes (um byte consiste em 8 bits) organizados de maneira seqüencial. Cada byte tem um número associado a ele, exatamente como o índice ou o subscrito em uma matriz, que é chamado de endereço do byte. O endereço do byte começa de 0 a um menor que o tamanho da memória. Por exemplo, digamos em 64 MB de RAM, existem 64 * 2 ^ 20 = 67108864 bytes. Portanto, o endereço desses bytes começará de 0 a 67108863.

insira a descrição da imagem aqui

Vamos ver o que acontece quando você declara uma variável.

marcas int;

Como sabemos que um int ocupa 4 bytes de dados (assumindo que estamos usando um compilador de 32 bits), o compilador reserva 4 bytes consecutivos da memória para armazenar um valor inteiro. O endereço do primeiro byte dos 4 bytes alocados é conhecido como o endereço das marcas de variável. Digamos que o endereço de 4 bytes consecutivos seja 5004, 5005, 5006 e 5007, então o endereço das marcas de variável será 5004. insira a descrição da imagem aqui

Declarando Variáveis ​​de Ponteiro

Como já foi dito, um ponteiro é uma variável que armazena um endereço de memória. Assim como qualquer outra variável, você precisa primeiro declarar uma variável de ponteiro antes de poder usá-la. Aqui está como você pode declarar uma variável de ponteiro.

Sintaxe: data_type *pointer_name;

data_type é o tipo do ponteiro (também conhecido como o tipo base do ponteiro). pointer_name é o nome da variável, que pode ser qualquer identificador C válido.

Vamos dar alguns exemplos:

int *ip;

float *fp;

int * ip significa que ip é uma variável de ponteiro capaz de apontar para variáveis ​​do tipo int. Em outras palavras, uma variável de ponteiro ip pode armazenar apenas o endereço de variáveis ​​do tipo int. Da mesma forma, a variável de ponteiro fp pode armazenar apenas o endereço de uma variável do tipo float. O tipo de variável (também conhecido como tipo base) ip é um ponteiro para int e o tipo de fp é um ponteiro para flutuar. Uma variável de ponteiro do tipo ponteiro para int pode ser representada simbolicamente como (int *). Da mesma forma, uma variável de ponteiro do tipo ponteiro para flutuar pode ser representada como (float *)

Depois de declarar uma variável de ponteiro, o próximo passo é atribuir algum endereço de memória válido a ela. Você nunca deve usar uma variável de ponteiro sem atribuir um endereço de memória válido a ela, porque logo após a declaração ela contém valor de lixo e pode estar apontando para qualquer lugar da memória. O uso de um ponteiro não atribuído pode gerar um resultado imprevisível. Pode até causar uma falha no programa.

int *ip, i = 10;
float *fp, f = 12.2;

ip = &i;
fp = &f;

Fonte: thecguru é de longe a explicação mais simples e detalhada que já encontrei.

Cody
fonte