Указатель на уточнение указателя

Я следовал этому уроку о том, как работает указатель на указатель.

Позвольте мне процитировать соответствующий отрывок:


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

Теперь мы можем установить

    int **ipp = &ip1;

а также ipp указывает на ip1 что указывает на i, *ipp является ip1, а также **ipp является i, или 5. Мы можем проиллюстрировать ситуацию с помощью нашей знакомой нотации с прямоугольником и стрелкой, например:

Если тогда мы говорим

    *ipp = ip2;

мы изменили указатель, на который указывает ipp (то есть, ip1) содержать копию ip2так, чтобы оно (ip1) теперь указывает на j:


Мой вопрос: почему на втором рисунке ipp все еще указывая на ip1 но нет ip2?

16 ответов

Решение

Забудьте на секунду об указательной аналогии. На самом деле указатель содержит адрес памяти. & является оператором "address of" - т.е. возвращает адрес в памяти объекта. * Оператор дает вам объект, на который ссылается указатель, то есть, учитывая указатель, содержащий адрес, он возвращает объект по этому адресу памяти. Итак, когда вы делаете *ipp = ip2что ты делаешь *ipp получить объект по адресу в ipp который ip1 а затем назначить ip1 значение хранится в ip2, который является адресом j,

Просто
& -> Адрес
* -> Значение в

Потому что вы изменили значение, на которое указывает ipp не ценность ipp, Так, ipp еще указывает на ip1 (значение ipp), ip1значение теперь такое же, как ip2ценность, поэтому они оба указывают на j,

Это:

*ipp = ip2;

такой же как:

ip1 = ip2;

Как и большинство вопросов для начинающих в теге C, на этот вопрос можно вернуться, вернувшись к первым принципам:

  • Указатель является своего рода значением.
  • Переменная содержит значение.
  • & Оператор превращает переменную в указатель.
  • * Оператор превращает указатель в переменную.

(Технически я должен сказать "lvalue" вместо "variable", но я чувствую, что более понятно описать изменяемые места хранения как "variable".)

Итак, у нас есть переменные:

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

переменная ip1 содержит указатель & оператор поворачивается i в указатель, и это значение указателя присваивается ip1, Так ip1 содержит указатель на i,

переменная ip2 содержит указатель & оператор поворачивается j в указатель, и этот указатель назначен ip2, Так ip2 содержит указатель на j,

int **ipp = &ip1;

переменная ipp содержит указатель & оператор превращается в переменную ip1 в указатель, и это значение указателя присваивается ipp, Так ipp содержит указатель на ip1,

Подведем итоги истории:

  • i содержит 5
  • j содержит 6
  • ip1 содержит указатель на i"
  • ip2 содержит указатель на j"
  • ipp содержит указатель на ip1"

Теперь мы говорим

*ipp = ip2;

* Оператор превращает указатель обратно в переменную. Мы получаем значение ipp, который является "указатель на ip1 и превратить его в переменную. Какая переменная? ip1 конечно!

Поэтому это просто другой способ сказать

ip1 = ip2;

Таким образом, мы получаем значение ip2, Что это? указатель на jMsgstr "Мы присваиваем это значение указателя ip1, так ip1 сейчас указатель на j"

Мы изменили только одно: ценность ip1:

  • i содержит 5
  • j содержит 6
  • ip1 содержит указатель на j"
  • ip2 содержит указатель на j"
  • ipp содержит указатель на ip1"

Почему ipp по-прежнему указывают на ip1 и не ip2?

Переменная изменяется, когда вы присваиваете ей. Подсчитать назначения; не может быть больше изменений переменных, чем присваиваний! Вы начинаете с присвоения i, j, ip1, ip2 а также ipp, Затем вы назначаете *ippчто, как мы видели, означает то же самое, что и "назначить ip1". Так как вы не назначали ipp во второй раз, это не изменилось!

Если вы хотели изменить ipp тогда вам придется фактически назначить ipp:

ipp = &ip2;

например.

Надеюсь, что этот кусок кода может помочь.

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

это выводит:

Мое личное мнение таково, что картинки со стрелками, указывающими так или иначе, затрудняют понимание указателей. Это делает их похожими на некоторые абстрактные, таинственные сущности. Они не.

Как и все остальное в вашем компьютере, указатели являются числами. Название "указатель" - это просто причудливый способ сказать "переменная, содержащая адрес".

Поэтому позвольте мне все перемешать, объяснив, как на самом деле работает компьютер.

У нас есть intимеет название i и значение 5. Это сохраняется в памяти. Как и все, что хранится в памяти, ему нужен адрес, иначе мы не сможем его найти. Допустим i заканчивается по адресу 0x12345678 и его приятель j со значением 6 заканчивается сразу после него. Предполагая, что 32-битный ЦП, где int составляет 4 байта, а указатели - 4 байта, переменные сохраняются в физической памяти следующим образом:

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

Теперь мы хотим указать на эти переменные. Мы создаем один указатель на int, int* ip1, и один int* ip2, Как и все в компьютере, эти переменные-указатели также размещаются в памяти. Предположим, что они заканчиваются на следующих соседних адресах в памяти, сразу после j, Мы устанавливаем указатели для хранения адресов ранее выделенных переменных: ip1=&i; ("скопируйте адрес i в ip1") и ip2=&j, Что происходит между строк:

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)

Итак, мы получили всего лишь 4 байтовых блока памяти, содержащих числа. Там нет мистических или магических стрел нигде в поле зрения.

На самом деле, просто глядя на дамп памяти, мы не можем сказать, содержит ли адрес 0x12345680 int или же int*, Разница в том, как наша программа решает использовать содержимое, хранящееся по этому адресу. (Задача нашей программы - просто указать процессору, что делать с этими числами.)

Затем мы добавляем еще один уровень косвенности с int** ipp = &ip1;, Опять же, мы просто получаем кусок памяти:

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

Шаблон кажется знакомым. Еще один фрагмент из 4 байтов, содержащий число.

Теперь, если бы у нас был дамп памяти вышеупомянутого вымышленного небольшого ОЗУ, мы могли бы вручную проверить, куда указывают эти указатели. Мы посмотрим на то, что хранится по адресу ipp переменной и найдите содержимое 0x12345680. Который, конечно, адрес, где ip1 хранится. Мы можем перейти по этому адресу, проверить содержимое и найти адрес iи, наконец, мы можем перейти по этому адресу и найти номер 5.

Так что, если мы возьмем содержимое ipp, *ipp, мы получим адрес переменной указателя ip1, Письменно *ipp=ip2 мы копируем ip2 в ip1, это эквивалентно ip1=ip2, В любом случае мы бы получили

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

(Эти примеры были приведены для процессора с прямым порядком байтов)

Обратите внимание на назначения:

ipp = &ip1;

Результаты ipp указать на ip1,

Таким образом, для ipp указать на ip2мы должны измениться таким же образом,

ipp = &ip2;

что мы явно не делаем. Вместо этого мы меняем значение по адресу, указанному ipp,
Делая следующее

*ipp = ip2;

мы просто заменяем значение, хранящееся в ip1,

ipp = &ip1, средства *ipp = ip1 = &i,
Сейчас, *ipp = ip2 = &j,
Так, *ipp = ip2 по сути такой же, как ip1 = ip2,

Потому что, когда вы говорите

*ipp = ip2

Вы говорите, что объект ippуказать направление памяти, которое ip2 указывает.

Ты не говоришь ipp В точку ip2,

ipp = &ip1;

Никакое последующее назначение не изменило значение ipp, Вот почему это все еще указывает на ip1,

Что вы делаете с *ippс ip1, не меняет того факта, что ipp указывает на ip1,

Мой вопрос: почему на втором рисунке ipp все еще указывает на ip1, а не на ip2?

Вы разместили красивые картинки, я постараюсь сделать хорошее ascii art:

Как сказал @Robert-S-Barnes в своем ответе: забудьте про указатели и что указывает на что, но подумайте с точки зрения памяти. В основном, int* означает, что он содержит адрес переменной и int** содержит адрес переменной, содержащей адрес переменной. Затем вы можете использовать алгебру указателя для доступа к значениям или адресам: &foo средства address of foo, а также *foo средства value of the address contained in foo,

Итак, поскольку указатели связаны с памятью, лучший способ сделать это "осязаемым" - показать, что алгебра указателей делает с памятью.

Итак, вот память вашей программы (упрощенная для целей примера):

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

когда вы делаете свой начальный код:

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

вот как выглядит ваша память:

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

там вы можете увидеть ip1 а также ip2 получает адреса i а также j а также ipp до сих пор не существует. Не забывайте, что адреса - это просто целые числа, хранящиеся в специальном типе.

Затем вы объявляете и определяете ipp такие как:

int **ipp = &ip1;

так вот твоя память:

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

а затем вы меняете значение, указанное адресом, хранящимся в ipp, который является адресом, хранящимся в ip1:

*ipp = ip2;

память программы

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

NB: как int* это специальный тип, я предпочитаю всегда избегать объявления нескольких указателей на одной строке, так как я думаю, int *x; или же int *x, *y; запись может вводить в заблуждение. Я предпочитаю писать int* x; int* y;

НТН

Если вы добавите оператор разыменования * на указатель, вы перенаправляете с указателя на указатель на объект.

Примеры:

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`

Следовательно:

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

В самом начале вы установили,

ipp = &ip1;

Теперь разыщите это как,

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

Если вы хотите ipp указать на ip2, вы должны сказать ipp = &ip2;, Тем не менее, это оставило бы ip1 все еще указывая на i,

Рассмотрим каждую переменную, представленную следующим образом:

type  : (name, adress, value)

так что ваши переменные должны быть представлены так

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

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

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

Как значение ipp является &ip1 так что заучка

*ipp = ip2;

меняет значение по адресу &ip1 к стоимости ip2, что значит ip1 изменено:

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

Но ipp еще:

(ipp, &ipp, &ip1)

Так что ценность ipp еще &ip1 что означает, что это все еще указывает на ip1,

*ipp = ip2; предполагает:

приписывать ip2 к переменной, на которую указывает ipp, Так что это эквивалентно:

ip1 = ip2;

Если вы хотите адрес ip2 храниться в ippПросто сделайте:

ipp = &ip2;

Сейчас ipp указывает на ip2,

Потому что вы меняете указатель *ipp, Это значит

  1. ipp (изменяемое имя) ---- зайти внутрь.
  2. внутри ipp это адрес ip1,
  3. сейчас *ipp так что иди (адрес изнутри) ip1,

Теперь мы находимся в ip1,*ipp(т.е.ip1знак равно ip2.
ip2 содержать адрес j.так ip1 содержимое будет заменено содержимым ip2(т.е. адрес j), МЫ НЕ ИЗМЕНЯЕМСЯ ipp СОДЕРЖАНИЕ. ВОТ И ВСЕ.

ipp может содержать значение (т.е. указывать на) указатель на объект типа указателя. Когда вы делаете

ipp = &ip2;  

тогда ipp содержит адрес переменной (указатель) ip2, который (&ip2) типа указатель на указатель. Теперь стрела ipp во втором рис будет указывать на ip2,

Вики говорит:
* operator - оператор разыменования, работает с переменной указателя и возвращает l-значение (переменную), эквивалентное значению по адресу указателя. Это называется разыменованием указателя.

применение * оператор на ipp разыменовывать его до l-значения указателя на int тип. Разыменованное значение l *ipp имеет указатель типа на int, он может содержать адрес int введите данные. После утверждения

ipp = &ip1;

ipp держит адрес ip1 а также *ipp держит адрес (указывая на) i, Вы можете сказать, что *ipp псевдоним ip1, И то и другое **ipp а также *ip1 псевдоним для i,
При выполнении

 *ipp = ip2;  

*ipp а также ip2 обе точки в том же месте, но ipp все еще указывает на ip1,

Какие *ipp = ip2; на самом деле это то, что он копирует содержимое ip2 (адрес j) чтобы ip1 (как *ipp это псевдоним для ip1), фактически делает оба указателя ip1 а также ip2 указывая на тот же объект (j).
Итак, на втором рисунке стрелка ip1 а также ip2 указывает на j в то время как ipp все еще указывает на ip1 так как никаких изменений не делается для изменения значения ipp,

Другие вопросы по тегам