Esclarecimento ponteiro para ponteiro


142

Eu estava seguindo este tutorial sobre como funciona um ponteiro para um ponteiro .

Deixe-me citar a passagem relevante:


    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;

Agora podemos definir

    int **ipp = &ip1;

e ipppontos para os ip1quais aponta i. *ippé ip1, e **ippé iou 5. Podemos ilustrar a situação, com nossa notação familiar de caixa e flecha, assim:

insira a descrição da imagem aqui

Se então dizemos

    *ipp = ip2;

alteramos o ponteiro apontado por ipp(isto é, ip1) para conter uma cópia de ip2, para que ( ip1) agora aponte para j:

insira a descrição da imagem aqui


Minha pergunta é: por que, na segunda foto, ippainda está apontando, ip1mas não ip2?


6
por favor, adicione o tipo de ippquando defini-lo, assim que sua pergunta é completo ;-)
Zmo

1
Porque ippapontar para ip1isso *ipp = ip2é o mesmo queip1 = ip2
Grijesh Chauhan

1
Podemos parar de escrever asteriks tão distantes do tipo pontudo? int **ippé muito menos intuitivo do que int** ipp, especialmente quando **ippsignifica algo completamente diferente fora das declarações de tipo.
Darkhogg

4
@Darkhogg int **ippparece bastante intuitivo para mim. Significa, estou fazendo **ippum int. Que é verdade.
Ithisa

2
@ user54609 significados e intuição pessoal à parte, considere o seguinte: O tipo de ippé int**, por isso basta escrever int**em vez de mágica " impdereference é um int" entendimentos .
precisa saber é o seguinte

Respostas:


143

Esqueça por um segundo a analogia do apontador. O que um ponteiro realmente contém é um endereço de memória. O &é o operador "endereço de" - isto é, retorna o endereço na memória de um objeto. O *operador fornece o objeto ao qual um ponteiro se refere, ou seja, se um ponteiro contém um endereço, ele retorna o objeto nesse endereço de memória. Portanto, quando você faz *ipp = ip2, o que você está fazendo é *ippobter o objeto no endereço em ippque está ip1e depois atribuir ao ip1valor armazenado em ip2, que é o endereço de j.

Simplesmente
& -> Endereço de
*-> Valor em


14
& E * nunca foi tão fácil
Ray

7
Acredito que a principal fonte de confusão se deva à ambiguidade do operador *, que durante a declaração da variável é usado para indicar que a variável, de fato, é um ponteiro para um determinado tipo de dados. Mas, por outro lado, também é usado em instruções para acessar o conteúdo da variável apontada por um ponteiro (operador de desreferenciação).
Lucas A.

43

Porque você alterou o valor apontado por ippnão o valor de ipp. Portanto, ippainda aponta para ip1(o valor de ipp), ip1o valor de agora é igual ao ip2valor de, portanto, ambos apontam para j.

Este:

*ipp = ip2;

é o mesmo que:

ip1 = ip2;

11
Pode valer a pena apontar a diferença entre int *ip1 = &ie *ipp = ip2;, ou seja, se você remover o intda primeira instrução, as atribuições parecerão muito semelhantes, mas isso *está fazendo algo muito diferente nos dois casos.
Crowman 06/02

22

Como a maioria das perguntas para iniciantes na tag C, essa pergunta pode ser respondida voltando aos primeiros princípios:

  • Um ponteiro é um tipo de valor.
  • Uma variável contém um valor.
  • O &operador transforma uma variável em um ponteiro.
  • O *operador transforma um ponteiro em uma variável.

(Tecnicamente, devo dizer "lvalue" em vez de "variável", mas acho que é mais claro descrever locais de armazenamento mutáveis ​​como "variáveis".)

Então, temos variáveis:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

Variável ip1 contém um ponteiro. O &operador se transforma iem um ponteiro e esse valor de ponteiro é atribuído a ip1. Então ip1 contém um ponteiro para i.

Variável ip2 contém um ponteiro. O &operador se transforma jem um ponteiro e esse ponteiro é atribuído a ip2. Então ip2 contém um ponteiro para j.

int **ipp = &ip1;

Variável ippcontém um ponteiro. O &operador transforma variável ip1em um ponteiro e esse valor de ponteiro é atribuído a ipp. Então ippcontém um ponteiro para ip1.

Vamos resumir a história até agora:

  • i contém 5
  • j contém 6
  • ip1contém "ponteiro para i"
  • ip2contém "ponteiro para j"
  • ippcontém "ponteiro para ip1"

Agora dizemos

*ipp = ip2;

O *operador transforma um ponteiro de volta em uma variável. Nós buscamos o valor de ipp, que é "ponteiro ip1e o transformamos em uma variável. Qual variável? É ip1claro!

Portanto, esta é simplesmente outra maneira de dizer

ip1 = ip2;

Então, buscamos o valor de ip2. O que é isso? "ponteiro para j". Atribuímos esse valor ao ponteiro ip1, ip1agora é "ponteiro para j"

Mudamos apenas uma coisa: o valor de ip1:

  • i contém 5
  • j contém 6
  • ip1contém "ponteiro para j"
  • ip2contém "ponteiro para j"
  • ippcontém "ponteiro para ip1"

Por que ippainda aponta para ip1e não ip2?

Uma variável muda quando você atribui a ela. Conte as tarefas; não pode haver mais alterações nas variáveis ​​do que atribuições! Você começa por atribuir a i, j, ip1, ip2e ipp. Você então atribui a *ipp, o que, como vimos, significa o mesmo que "atribuir a ip1". Como você não atribuiu ippuma segunda vez, isso não mudou!

Se você quiser mudar ipp, precisará atribuir a ipp:

ipp = &ip2;

por exemplo.


21

espero que este pedaço de código possa ajudar.

#include <iostream>
#include <stdio.h>
using namespace std;

int main()
{
    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;
    int** ipp = &ip1;
    printf("address of value i: %p\n", &i);
    printf("address of value j: %p\n", &j);
    printf("value ip1: %p\n", ip1);
    printf("value ip2: %p\n", ip2);
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
    *ipp = ip2;
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
}

produz:

insira a descrição da imagem aqui


12

Minha opinião pessoal é que as figuras com setas apontando dessa maneira ou dificultam a compreensão dos ponteiros. Faz com que pareçam algumas entidades abstratas e misteriosas. Eles não são.

Como tudo no seu computador, ponteiros são números . O nome "ponteiro" é apenas uma maneira elegante de dizer "uma variável que contém um endereço".

Portanto, deixe-me agitar as coisas explicando como um computador realmente funciona.

Temos um int, ele tem o nome ie o valor 5. Isso é armazenado na memória. Como tudo armazenado na memória, ele precisa de um endereço ou não poderíamos encontrá-lo. Digamos que itermina no endereço 0x12345678 e seu amigo jcom o valor 6 acaba logo após ele. Supondo que uma CPU de 32 bits em que int seja 4 bytes e ponteiros sejam 4 bytes, as variáveis ​​são armazenadas na memória física como esta:

Address     Data           Meaning
0x12345678  00 00 00 05    // The variable i
0x1234567C  00 00 00 06    // The variable j

Agora queremos apontar para essas variáveis. Criamos um ponteiro para int int* ip1, e um int* ip2. Como tudo no computador, essas variáveis ​​de ponteiro também são alocadas em algum lugar da memória. Vamos supor que eles acabem nos próximos endereços adjacentes na memória, imediatamente após j. Definimos os ponteiros para conter os endereços das variáveis ​​alocadas anteriormente: ip1=&i;("copie o endereço de i para ip1") e ip2=&j. O que acontece nas entrelinhas é:

Address     Data           Meaning
0x12345680  12 34 56 78    // The variable ip1(equal to address of i)
0x12345684  12 34 56 7C    // The variable ip2(equal to address of j)

Então, o que obtivemos foram apenas alguns pedaços de 4 bytes de memória contendo números. Não há flechas místicas ou mágicas em nenhum lugar à vista.

De fato, apenas olhando para um despejo de memória, não podemos dizer se o endereço 0x12345680 contém um intou int*. A diferença é como o nosso programa escolhe usar o conteúdo armazenado neste endereço. (A tarefa do nosso programa é realmente apenas dizer à CPU o que fazer com esses números.)

Então adicionamos mais um nível de indireção com int** ipp = &ip1;. Novamente, temos apenas um pedaço de memória:

Address     Data           Meaning
0x12345688  12 34 56 80    // The variable ipp

O padrão parece familiar. Ainda outro pedaço de 4 bytes contendo um número.

Agora, se tivéssemos um despejo de memória da pequena RAM fictícia acima, poderíamos verificar manualmente onde esses ponteiros apontam. Examinamos o que está armazenado no endereço da ippvariável e encontramos o conteúdo 0x12345680. Qual é, obviamente, o endereço onde ip1está armazenado. Podemos ir para esse endereço, verificar o conteúdo e encontrar o endereço de i; finalmente, podemos ir para esse endereço e encontrar o número 5.

Portanto, se pegarmos o conteúdo de ipp, *ippobteremos o endereço da variável ponteiro ip1. Ao escrever *ipp=ip2copiamos ip2 para ip1, é equivalente a ip1=ip2. Em ambos os casos, teríamos

Address     Data           Meaning
0x12345680  12 34 56 7C    // The variable ip1
0x12345684  12 34 56 7C    // The variable ip2

(Estes exemplos foram dados para uma grande CPU endian)


5
Embora eu entenda seu argumento, é importante pensar em indicadores como entidades abstratas e misteriosas. Qualquer implementação específica de ponteiros é apenas números, mas a estratégia de implementação que você esboça não é um requisito de uma implementação, é apenas uma estratégia comum. Os ponteiros não precisam ter o mesmo tamanho que um int, os ponteiros não precisam ter endereços em um modelo de memória virtual plana e assim por diante; estes são apenas detalhes de implementação.
Eric Lippert

@ EricLippert Acho que se pode tornar esse exemplo mais abstrato ao não usar endereços de memória ou blocos de dados reais. Se fosse uma tabela indicando algo como location, value, variableonde estava o local 1,2,3,4,5e o valor A,1,B,C,3, a ideia correspondente de ponteiros poderia ser explicada facilmente sem o uso de setas, que são inerentemente confusas. Qualquer que seja a implementação escolhida, existe um valor em algum local, e isso é parte do quebra-cabeça que fica ofuscado ao modelar com setas.
MirroredFate

@EricLippert Na minha experiência, a maioria dos possíveis programadores C que têm problemas para entender os ponteiros são aqueles que receberam modelos abstratos e artificiais. A abstração não é útil, porque todo o objetivo da linguagem C hoje é que ela esteja próxima do hardware. Se você está aprendendo C, mas não pretende escrever código próximo ao hardware, está perdendo tempo . Java etc é uma opção muito melhor se você não quiser saber como os computadores funcionam, mas apenas fazer programação de alto nível.
Lundin

@EricLippert E sim, várias implementações obscuras de ponteiros podem existir, onde os ponteiros não correspondem necessariamente a endereços. Mas desenhar setas não ajudará você a entender como elas funcionam. Em algum momento, você deve deixar o pensamento abstrato e descer ao nível do hardware; caso contrário, não deverá usar C. Existem muitas linguagens modernas mais adequadas para a programação de alto nível puramente abstrata.
Lundin

@Lundin: Eu também não sou muito fã de diagramas de flechas; a noção de uma flecha como dados é complicada. Prefiro pensar abstratamente, mas sem flechas. O &operador de uma variável fornece uma moeda que representa essa variável. O *operador nessa moeda devolve a variável. Não são necessárias flechas!
Eric Lippert

8

Observe as atribuições:

ipp = &ip1;

resultados ipppara apontar ip1.

Portanto, para ippapontar ip2, devemos mudar da mesma maneira,

ipp = &ip2;

o que claramente não estamos fazendo. Em vez disso, estamos alterando o valor no endereço indicado por ipp.
Fazendo o seguinte

*ipp = ip2;

estamos apenas substituindo o valor armazenado ip1.

ipp = &ip1, Meios *ipp = ip1 = &i,
agora, *ipp = ip2 = &j.
Então, *ipp = ip2é essencialmente o mesmo que ip1 = ip2.


5
ipp = &ip1;

Nenhuma atribuição posterior alterou o valor de ipp. É por isso que ainda aponta ip1.

O que você faz com *ipp, isto é, com ip1, não muda o fato de que ippaponta para ip1.


5

Minha pergunta é: Por que, na segunda foto, ipp ainda aponta para ip1, mas não para ip2?

você colocou boas fotos, vou tentar fazer uma bela arte ascii:

Como @ Robert-S-Barnes disse em sua resposta: esqueça os ponteiros e o que aponta para o que, mas pense em termos de memória. Basicamente, um int*significa que ele contém o endereço de uma variável e um int**contém o endereço de uma variável que contém o endereço de uma variável. Em seguida, você pode usar a álgebra do ponteiro para acessar os valores ou os endereços: &foomeios address of fooe *foomeios value of the address contained in foo.

Portanto, como os ponteiros tratam da memória, a melhor maneira de tornar isso "tangível" é mostrar o que a álgebra dos ponteiros faz com a memória.

Então, aqui está a memória do seu programa (simplificada para o propósito do exemplo):

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [   |   |   |   |   ]

quando você faz seu código inicial:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

aqui está como sua memória se parece:

name:    i   j ip1 ip2
addr:    0   1   2   3
mem : [  5|  6|  0|  1]

lá você pode ver ip1e ip2obter os endereços de ie je ippainda não existe. Não esqueça que endereços são simplesmente números inteiros armazenados com um tipo especial.

Então você declara e define ippcomo:

int **ipp = &ip1;

então aqui está sua memória:

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  0|  1|  2]

e então, você está alterando o valor apontado pelo endereço armazenado ipp, que é o endereço armazenado em ip1:

*ipp = ip2;

a memória do programa é

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  1|  1|  2]

NB: como int*é um tipo especial, prefiro sempre declarar vários ponteiros na mesma linha, pois acho que a notação int *x;ou int *x, *y;pode ser enganosa. Eu prefiro escreverint* x; int* y;

HTH


com o seu exemplo, o valor inicial de não ip2deve ser . 34
Dipto

1
ah, acabei de alterar a memória para que ela corresponda à ordem da declaração. Acho que consertei isso?
Zmo

5

Porque quando você diz

*ipp = ip2

você está dizendo o 'objeto apontado por ipp' para apontar a direção da memória que ip2está apontando.

Você não está dizendo ipppara apontar ip2.


4

Se você adicionar o operador de desreferência *ao ponteiro, será redirecionado do ponteiro para o objeto apontado.

Exemplos:

int i = 0;
int *p = &i; // <-- N.B. the pointer declaration also uses the `*`
             //     it's not the dereference operator in this context
*p;          // <-- this expression uses the pointed-to object, that is `i`
p;           // <-- this expression uses the pointer object itself, that is `p`

Portanto:

*ipp = ip2; // <-- you change the pointer `ipp` points to, not `ipp` itself
            //     therefore, `ipp` still points to `ip1` afterwards.

3

Se você gostaria ippde apontar ip2, você teria que dizer ipp = &ip2;. No entanto, isso deixaria ip1ainda apontando i.


3

Bem começando você definir,

ipp = &ip1;

Agora desreferencie-o como,

*ipp = *&ip1 // Here *& becomes 1  
*ipp = ip1   // Hence proved 

3

Considere cada variável representada assim:

type  : (name, adress, value)

então suas variáveis ​​devem ser representadas assim

int   : ( i ,  &i , 5 ); ( j ,  &j ,  6); ( k ,  &k , 5 )

int*  : (ip1, &ip1, &i); (ip1, &ip1, &j)

int** : (ipp, &ipp, &ip1)

Como o valor de ippé &ip1assim a introdução:

*ipp = ip2;

altera o valor no endereço &ip1para o valor de ip2, o que significa que ip1é alterado:

(ip1, &ip1, &i) -> (ip1, &ip1, &j)

Mas ippainda:

(ipp, &ipp, &ip1)

Portanto, o valor de ippstill, o &ip1que significa que ainda aponta para ip1.


1

Porque você está mudando o ponteiro de *ipp. Isso significa

  1. ipp (nome identificável) ---- entre.
  2. inside ippé o endereço de ip1.
  3. agora *ippentão vá para (endereço de dentro) ip1.

Agora estamos em ip1. *ipp(ie ip1) = ip2.
ip2contenha endereço de j., então o ip1conteúdo será substituído por contido de ip2 (ou seja, endereço de j), NÃO ESTAMOS ALTERANDO ippCONTEÚDO. É ISSO AÍ.


1

*ipp = ip2; implica:

Atribua ip2à variável apontada por ipp. Portanto, isso é equivalente a:

ip1 = ip2;

Se você deseja que o endereço ip2seja armazenado ipp, basta:

ipp = &ip2;

Agora ippaponta para ip2.


0

ipppode conter um valor de (ou seja, apontar para) um ponteiro para o objeto do tipo ponteiro . Quando você faz

ipp = &ip2;  

então o ippcontém o endereço da variável (ponteiro)ip2 , que é ( &ip2) do tipo ponteiro para ponteiro . Agora a seta da ippsegunda foto apontará para ip2.

O Wiki diz:
O *operador é um operador de desreferência que opera na variável ponteiro e retorna um valor l (variável) equivalente ao valor no endereço do ponteiro. Isso é chamado de desreferenciar o ponteiro.

Aplicando o *operador na ippreferência a um valor l do ponteiro paraint digitar. O valor l não referenciado *ippé do tipo ponteiro paraint , pode conter o endereço de um inttipo de dados. Após a declaração

ipp = &ip1;

ippestá mantendo o endereço de ip1e *ippestá mantendo o endereço de (apontando para) i. Você pode dizer que *ippé um apelido de ip1. Ambos **ippe *ip1são alias para i.
Fazendo

 *ipp = ip2;  

*ippe ip2ambos apontam para o mesmo local, mas ippainda estão apontando para ip1.

O *ipp = ip2;que realmente é é que ele copia o conteúdo de ip2(o endereço de j) para ip1(como *ippé um apelido para ip1), fazendo com que ambos os ponteiros ip1e ip2apontando para o mesmo objeto ( j).
Portanto, na segunda figura, a seta de ip1e ip2está apontando para jenquanto ippainda está apontando ip1, já que nenhuma modificação é feita para alterar o valor deipp .

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.