Ponteiro para esclarecimento de ponteiro

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 ipp aponta para ip1 que aponta para i . *ipp é ip1 , e **ipp é i , ou 5. Podemos ilustrar a situação, com nossa familiar notação de checkbox e seta, assim:

insira a descrição da imagem aqui

Se então dissermos

  *ipp = ip2; 

nós mudamos o ponteiro apontado por ipp (isto é, ip1 ) para conter uma cópia de ip2 , de forma que ele ( ip1 ) agora aponta para j :

insira a descrição da imagem aqui


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

Esqueça por um segundo sobre a analogia apontada. O que um ponteiro realmente contém é um endereço de memory. O & é o operador “endereço de” – isto é, retorna o endereço na memory de um object. O operador * fornece o object ao qual um ponteiro se refere, ou seja, dado um ponteiro contendo um endereço, ele retorna o object naquele endereço de memory. Então, quando você faz *ipp = ip2 , o que você está fazendo é *ipp obter o object no endereço contido em ipp que é ip1 e então atribuir a ip1 o valor armazenado em ip2 , que é o endereço de j .

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

Porque você mudou o valor apontado por ipp não o valor de ipp . Então, ipp ainda aponta para ip1 (o valor de ipp ), o valor de ip1 agora é o mesmo que o valor de ip2 , então ambos apontam para j .

Este:

 *ipp = ip2; 

é o mesmo que:

 ip1 = ip2; 

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

 #include  #include  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); } 

ele produz:

insira a descrição da imagem aqui

Como a maioria das perguntas para iniciantes na tag C, essa questão 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 eu deveria dizer “lvalue” em vez de “variable”, mas eu sinto que é mais claro descrever os locais de armazenamento mutáveis ​​como “variables”).

Então nós temos variables:

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

Variável ip1 contém um ponteiro. O operador & transforma i em 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 & transforma j em um ponteiro e esse ponteiro é atribuído a ip2 . Então ip2 contém um ponteiro para j .

 int **ipp = &ip1; 

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

Vamos resumir a história até agora:

  • i contém 5
  • j contém 6
  • ip1 contém “ponteiro para i
  • ip2 contém “ponteiro para j
  • ipp conté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 é “pointer to ip1 e o transformamos em uma variável. Qual variável? ip1 claro!

Portanto, isso é simplesmente outra maneira de dizer

 ip1 = ip2; 

Então nós buscamos o valor de ip2 . O que é isso? “ponteiro para j “. Nós atribuímos esse valor de ponteiro para ip1 , então ip1 é agora “ponteiro para j

Nós só mudamos uma coisa: o valor de ip1 :

  • i contém 5
  • j contém 6
  • ip1 contém “ponteiro para j
  • ip2 contém “ponteiro para j
  • ipp contém “ponteiro para ip1

Por que o ipp ainda aponta para o ip1 e não para o ip2 ?

Uma variável muda quando você atribui a ela. Conte as atribuições; não pode haver mais mudanças nas variables ​​do que atribuições! Você começa atribuindo a i , j , ip1 , ip2 e ipp . Você então atribui a *ipp , que, como vimos, significa o mesmo que “assign to ip1 “. Como você não atribuiu a ipp pela segunda vez, isso não mudou!

Se você quisesse alterar o ipp , você teria que realmente atribuir ao ipp :

 ipp = &ip2; 

por exemplo.

Minha opinião pessoal é que fotos com setas apontando para esse lado ou que tornam os pointers mais difíceis de entender. Isso faz com que pareçam algumas entidades abstratas e misteriosas. Eles não são.

Como tudo mais no seu computador, os pointers são números . O nome “ponteiro” é apenas uma maneira elegante de dizer “uma variável contendo um endereço”.

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

Nós temos um int , ele tem o nome i e o valor 5. Isso é armazenado na memory. Como tudo armazenado na memory, precisa de um endereço ou não conseguiríamos encontrá-lo. Vamos dizer que i acabe no endereço 0x12345678 e seu amigo j com o valor 6 acaba logo depois dele. Supondo uma CPU de 32 bits onde int é de 4 bytes e pointers são de 4 bytes, então as variables ​​são armazenadas na memory física da seguinte forma:

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

Agora queremos apontar para essas variables. Criamos um ponteiro para int, int* ip1 e um int* ip2 . Como tudo no computador, essas variables ​​de ponteiro são alocadas em algum lugar na memory também. Vamos supor que eles acabam nos próximos endereços adjacentes na memory, imediatamente após j . Nós definimos os pointers para conter os endereços das variables ​​alocadas anteriormente: ip1=&i; (“copie o endereço de i em ip1”) e ip2=&j . O que acontece entre as linhas é:

 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 memory de 4 bytes contendo números. Não há setas místicas ou mágicas em qualquer lugar à vista.

Na verdade, apenas observando um despejo de memory, não podemos dizer se o endereço 0x12345680 contém um int ou int* . A diferença é como o nosso programa escolhe usar o conteúdo armazenado neste endereço. (A tarefa do nosso programa é apenas dizer à CPU o que fazer com esses números.)

Em seguida, adicionamos outro nível de indireção com int** ipp = &ip1; . Mais uma vez, acabamos de receber um pedaço de memory:

 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 dump de memory da pequena RAM fictícia acima, poderíamos verificar manualmente para onde esses pointers apontam. Nós espiamos o que está armazenado no endereço da variável ipp e encontramos o conteúdo 0x12345680. Qual é, claro, o endereço onde o ip1 está armazenado. Podemos ir para esse endereço, verificar o conteúdo lá e encontrar o endereço de i , e então finalmente podemos ir para esse endereço e encontrar o número 5.

Então, se pegarmos o conteúdo de ipp, *ipp , obteremos o endereço da variável de ponteiro ip1 . Escrevendo *ipp=ip2 ip2 em 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 

(Esses exemplos foram dados para uma CPU big endian)

Observe as atribuições:

 ipp = &ip1; 

resulta ipp para apontar para ip1 .

então para ipp apontar para 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 apontado por ipp .
Ao fazer o folowing

 *ipp = ip2; 

estamos apenas substituindo o valor armazenado em ip1 .

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

 ipp = &ip1; 

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

O que você faz com *ipp , ou seja, com ip1 , não altera o fato de que ipp aponta para ip1 .

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

você colocou fotos legais, eu vou tentar fazer arte ascii legal:

Como @ Robert-S-Barnes disse em sua resposta: esqueça os pointers , e o que aponta para o quê, mas pense em termos de memory. 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. Então você pode usar a álgebra do ponteiro para acessar os valores ou os endereços: &foo significa address of foo , e *foo significa value of the address contained in foo .

Então, como os pointers tratam da memory, a melhor maneira de tornar isso “tangível” é mostrar o que os álgebra dos pointers fazem com a memory.

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

 name: ij 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; 

Veja como é a sua memory:

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

lá você pode ver ip1 e ip2 obtém os endereços de i e j e ipp ainda não existe. Não se esqueça de que os endereços são simplesmente números inteiros armazenados com um tipo especial.

Então você declara e define ipp como:

 int **ipp = &ip1; 

então aqui está sua memory:

 name: ij 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 em ipp , que é o endereço armazenado em ip1 :

 *ipp = ip2; 

a memory do programa é

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

NB: como int* é um tipo especial, eu prefiro sempre evitar declarar múltiplos pointers na mesma linha, como eu acho que o int *x; ou int *x, *y; notação pode ser enganosa. Eu prefiro escrever int* x; int* y; int* x; int* y;

HTH

Porque quando você diz

 *ipp = ip2 

você está dizendo o ‘object apontado por ipp ‘ para apontar a direção da memory que o ip2 está apontando.

Você não está dizendo ipp para apontar o ip2 .

Se você adicionar o operador de remoção de referência * ao ponteiro, você redirectá do ponteiro para o object apontado.

Exemplos:

 int i = 0; int *p = &i; // < -- NB 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` 

Assim sendo:

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

Se você quiser que ipp aponte para ip2 , você terá que dizer ipp = &ip2; . No entanto, isso deixaria o ip1 ainda apontando para i .

Muito começo você definiu,

 ipp = &ip1; 

Agora, refira-o como,

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

Considere cada variável representada assim:

 type : (name, adress, value) 

então suas variables ​​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 é &ip1 então a inctrução:

 *ipp = ip2; 

altera o valor no addess &ip1 para o valor de ip2 , o que significa que o ip1 é alterado:

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

Mas ainda ipp :

 (ipp, &ipp, &ip1) 

Então o valor de ipp ainda &ip1 significa que ele ainda aponta para ip1 .

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

  1. ipp (nome variável) —- vá para dentro.
  2. inside ipp é o endereço do ip1 .
  3. agora *ipp então vá para (endereço de dentro) ip1 .

Agora estamos no ip1 . *ipp (ou seja, ip1 ) = ip 2.
ip2 contém o endereço do conteúdo de j .so ip1 será substituído pelo conteúdo do ip2 (ou seja, endereço do j), NÃO ALTERAMOS o ipp CONTENT. É ISSO AÍ.

*ipp = ip2; implica:

Atribuir ip2 para a variável apontada por ipp . Então isso é equivalente a:

 ip1 = ip2; 

Se você quiser que o endereço do ip2 seja armazenado no ipp , simplesmente faça:

 ipp = &ip2; 

Agora ipp aponta para ip2 .

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

 ipp = &ip2; 

então o ipp contém o endereço da variável (ponteiro) ip2 , que é ( &ip2 ) do tipo ponteiro para ponteiro . Agora a seta de ipp na segunda foto apontará para ip2 .

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

Aplicando * operador no ipp deerefrence para um valor l de ponteiro para int type. O valor de l não *ipp é do tipo ponteiro para int , ele pode conter o endereço de um dado do tipo int . Depois da declaração

 ipp = &ip1; 

ipp está mantendo o endereço de ip1 e *ipp está segurando o endereço de (apontando para) i . Você pode dizer que *ipp é um alias de ip1 . Ambos **ipp e *ip1 são alias para i .
Fazendo

  *ipp = ip2; 

*ipp e ip2 ambos apontam para o mesmo local, mas ipp ainda está apontando para ip1 .

Qual *ipp = ip2; Na verdade, ele copia o conteúdo de ip2 (o endereço de j ) para ip1 (como *ipp é um alias para ip1 ), fazendo com que os dois pointers ip1 e ip2 apontem para o mesmo object ( j ).
Portanto, na segunda figura, a seta de ip1 e ip2 está apontando para j enquanto ipp ainda está apontando para ip1 , já que nenhuma modificação é feita para alterar o valor de ipp .