Указатель на уточнение указателя
Я следовал этому уроку о том, как работает указатель на указатель.
Позвольте мне процитировать соответствующий отрывок:
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
содержит 5j
содержит 6ip1
содержит указатель наi
"ip2
содержит указатель наj
"ipp
содержит указатель наip1
"
Теперь мы говорим
*ipp = ip2;
*
Оператор превращает указатель обратно в переменную. Мы получаем значение ipp
, который является "указатель на ip1
и превратить его в переменную. Какая переменная? ip1
конечно!
Поэтому это просто другой способ сказать
ip1 = ip2;
Таким образом, мы получаем значение ip2
, Что это? указатель на j
Msgstr "Мы присваиваем это значение указателя ip1
, так ip1
сейчас указатель на j
"
Мы изменили только одно: ценность ip1
:
i
содержит 5j
содержит 6ip1
содержит указатель на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
, Это значит
ipp
(изменяемое имя) ---- зайти внутрь.- внутри
ipp
это адресip1
, - сейчас
*ipp
так что иди (адрес изнутри)ip1
,
Теперь мы находимся в ip1
,*ipp
(т.е.ip1
знак равно ip
2.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
,