Указатель на разъяснение указателя
Я следовал этому учебнику о том, как работает указатель на указатель.
Позвольте мне привести соответствующий отрывок:
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
?
Ответы
Ответ 1
Забудьте о секунде о направленной аналогии. То, что действительно содержит указатель, - это адрес памяти. &
- это "адрес оператора", то есть он возвращает адрес в памяти объекта. Оператор *
дает вам объект, на который ссылается указатель, то есть с указателем, содержащим адрес, он возвращает объект по этому адресу памяти. Поэтому, когда вы выполняете *ipp = ip2
, то, что вы делаете, это *ipp
получить объект по адресу, содержащемуся в ipp
, который равен ip1
, а затем назначить ip1
значение, хранящееся в ip2
, которое является адрес j
.
Просто
&
→ Адрес
*
→ Значение в
Ответ 2
Поскольку вы изменили значение, на которое указывает ipp
, а не значение ipp
. Таким образом, ipp
по-прежнему указывает на ip1
(значение ipp
), значение ip1
теперь совпадает с значением ip2
, поэтому они оба указывают на j
.
Это:
*ipp = ip2;
совпадает с:
ip1 = ip2;
Ответ 3
надеюсь, что этот фрагмент кода может помочь.
#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);
}
выводится:
Ответ 4
Как и большинство начинающих вопросов в теге C, на этот вопрос можно ответить, вернувшись к первым принципам:
- Указатель - это своего рода значение.
- Переменная содержит значение.
- Оператор
&
превращает переменную в указатель.
- Оператор
*
превращает указатель в переменную.
(Технически я должен сказать "lvalue" вместо "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
. Что это? "указатель на j
". Мы присваиваем этому указателю значение 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;
например.
Ответ 5
Мое личное мнение состоит в том, что фотографии со стрелками, указывающими этот путь, или которые затрудняют понимание указателей. Это заставляет их казаться абстрактными, таинственными сущностями. Они не.
Как и все остальное на вашем компьютере, указатели - это числа. Имя "указатель" - просто причудливый способ сказать "переменная, содержащая адрес".
Поэтому позвольте мне разобраться, объясняя, как работает компьютер.
У нас есть 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;
( "копировать адрес я в 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*
. Разница заключается в том, как наша программа выбирает использование содержимого, хранящегося по этому адресу. (Задача нашей программы состоит в том, чтобы просто сказать CPU, что делать с этими числами.)
Затем добавим еще один уровень косвенности с 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
(Эти примеры были приведены для большого центрального процессора)
Ответ 6
Обратите внимание на присваивания:
ipp = &ip1;
результат ipp
, чтобы указать на ip1
.
поэтому для ipp
, чтобы указать на ip2
, мы должны измениться аналогичным образом,
ipp = &ip2;
которые мы явно не делаем. Вместо этого мы меняем значение по адресу, на которое указывает ipp
.
Сделав следующее
*ipp = ip2;
мы просто заменяем значение, хранящееся в ip1
.
ipp = &ip1
, означает *ipp = ip1 = &i
,
Теперь *ipp = ip2 = &j
.
Итак, *ipp = ip2
по существу совпадает с ip1 = ip2
.
Ответ 7
ipp = &ip1;
Никакое последующее назначение не изменило значение ipp
. Вот почему он все еще указывает на ip1
.
Что вы делаете с *ipp
, т.е. с ip1
, не меняет того факта, что ipp
указывает на ip1
.
Ответ 8
Потому что, когда вы говорите
*ipp = ip2
вы говорите, что объект, на который указывает ipp
', указывает направление памяти, на которое указывает ip2
.
Вы не говорите ipp
, чтобы указать ip2
.
Ответ 9
Если вы добавите оператор указателя *
в указатель, вы переадресовываете указатель на объект с указателем.
Примеры:
int i = 0;
int *p = &i; // <-- N.B. the pointer declaration also uses the `*`
// it 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.
Ответ 10
Мой вопрос: почему во втором изображении ipp все еще указывает на ip1, но не ip2?
вы разместили красивые картинки, я постараюсь сделать приятное искусство ascii:
Подобно @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]
N.B.: as int*
является специальным типом, я предпочитаю всегда избегать объявления нескольких указателей в одной строке, так как я думаю, что нотация int *x;
или int *x, *y;
может вводить в заблуждение. Я предпочитаю писать int* x; int* y;
НТН
Ответ 11
Если вы хотите, чтобы ipp
указывал на ip2
, вам нужно сказать ipp = &ip2;
. Однако это оставило бы ip1
все еще указывая на i
.
Ответ 12
Очень скоро вы установите
ipp = &ip1;
Теперь разыщите его так, как
*ipp = *&ip1 // Here *& becomes 1
*ipp = ip1 // Hence proved
Ответ 13
Объясните каждую переменную, представленную следующим образом:
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;
изменяет значение в addess &ip1
на значение ip2
, что означает ip1
:
(ip1, &ip1, &i) -> (ip1, &ip1, &j)
Но ipp
еще:
(ipp, &ipp, &ip1)
Итак, значение ipp
еще &ip1
, что означает, что оно все еще указывает на ip1
.
Ответ 14
Потому что вы меняете указатель *ipp
. Это означает
-
ipp
(varaiable name) ---- входите внутрь.
- внутри
ipp
- адрес ip1
.
- теперь
*ipp
, поэтому перейдите к (адрес внутри) ip1
.
Теперь мы находимся в ip1
.
*ipp
(т.е. ip1
) = ip
2.
ip2
содержать адрес j
.so ip1
содержимое будет заменено на содержимое ip2 (то есть адрес j),
МЫ НЕ ИЗМЕНИТЬ ipp
СОДЕРЖАНИЕ.
ЭТО.
Ответ 15
*ipp = ip2;
подразумевает:
Назначьте ip2
переменной, на которую указывает ipp
. Таким образом, это эквивалентно:
ip1 = ip2;
Если вы хотите сохранить адрес ip2
в ipp
, просто выполните:
ipp = &ip2;
Теперь ipp
указывает на ip2
.
Ответ 16
ipp
может содержать значение (например, указатель) указателя на объект типа указателя. Когда вы делаете
ipp = &ip2;
то ipp
содержит адрес переменной (указатель) ip2
, которая является (&ip2
) указателем типа на указатель. Теперь стрелка ipp
во втором pic укажет на ip2
.
Wiki говорит:
Оператор *
является оператором разыменования, который работает с переменной указателя и возвращает l-value (переменный) эквивалент на значение по адресу указателя. Это называется разыменование указателя.
Применяя оператор *
на 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
.