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

142

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

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


    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?

Блейк
источник
6
пожалуйста, добавьте тип ippпри определении, чтобы ваш вопрос был
готов
1
Потому что ippуказание на это ip1так *ipp = ip2же, какip1 = ip2
Grijesh Chauhan
1
Можем ли мы прекратить писать звездочки, которые находятся далеко от указанного типа? int **ippгораздо менее интуитивно, чем int** ipp, особенно когда **ippподразумевает совершенно другую вещь вне объявлений типов.
Darkhogg
4
@ Darkhogg int **ippкажется мне довольно интуитивным. Это означает, что я делаю . Что является правдой. **ippint
ithisa
2
@ user54609 значения и личную интуитивность отдельно, рассмотрим это: тип ippis int**, так что просто напишите int**вместо магического понимания « impразыменование является int» .
Manu343726

Ответы:

143

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

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

Роберт С. Барнс
источник
14
& и * никогда не было так просто
Рэй,
7
Я полагаю, что основной источник путаницы связан с неоднозначностью оператора *, который во время объявления переменной используется для указания того, что переменная, по сути, является указателем на определенный тип данных. Но, с другой стороны, он также используется в операторах для доступа к содержимому переменной, на которую указывает указатель (оператор разыменования).
Лукас А.
43

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

Это:

*ipp = ip2;

такой же как:

ip1 = ip2;
Skizz
источник
11
Может быть, стоит указать на разницу между int *ip1 = &iи *ipp = ip2;, т. Е. Если вы удалите intоператор from из первого оператора, тогда назначения выглядят очень похоже, но *в обоих случаях происходит нечто очень разное.
Crowman
22

Как и большинство вопросов для начинающих в теге 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;

например.

Эрик Липперт
источник
21

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

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

это выводит:

введите описание изображения здесь

michaeltang
источник
12

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

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

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

У нас есть 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

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

Лундин
источник
5
Хотя я понимаю вашу точку зрения, полезно думать об указателях как об абстрактных, загадочных сущностях. Любая конкретная реализация указателей - это просто числа, но набросанная вами стратегия реализации не является требованием реализации, это просто общая стратегия. Указатели не обязательно должны быть того же размера, что и int, указатели не обязательно должны быть адресами в плоской модели виртуальной памяти и т. Д .; это просто детали реализации.
Эрик Липперт
@EricLippert Я думаю, что можно сделать этот пример более абстрактным, не используя фактические адреса памяти или блоки данных. Если бы это была таблица, в которой указывалось что-то вроде location, value, variableместоположения 1,2,3,4,5и значения A,1,B,C,3, соответствующая идея указателей могла бы быть легко объяснена без использования стрелок, которые по своей сути сбивают с толку. В любой выбранной реализации значение существует в некотором месте, и это часть головоломки, которая становится запутанной при моделировании со стрелками.
MirroredFate
@EricLippert По моему опыту, большинство потенциальных программистов на C, которые испытывают проблемы с пониманием указателей, - это те, кому давали абстрактные, искусственные модели. Абстракция является не полезно, потому что вся цель языка C сегодня, является то , что она близка к аппаратному обеспечению. Если вы изучаете C, но не собираетесь писать код, близкий к оборудованию, вы тратите время впустую . Java и т. Д. - гораздо лучший выбор, если вы не хотите знать, как работают компьютеры, а просто занимаетесь программированием на высоком уровне.
Лундин
@EricLippert И да, могут существовать различные непонятные реализации указателей, где указатели не обязательно соответствуют адресам. Но рисование стрелок не поможет вам понять, как они работают. В какой-то момент вы должны оставить абстрактное мышление и перейти на аппаратный уровень, в противном случае вам не следует использовать C. Существует много гораздо более подходящих современных языков, предназначенных для чисто абстрактного программирования высокого уровня.
Лундин
@Lundin: я не большой поклонник диаграмм со стрелками; понятие стрелки как данных является хитрым. Я предпочитаю думать об этом абстрактно, но без стрелок. &Оператор переменной дает монету, представляющую эту переменную. *Оператор на этой монете дает вам обратно переменную. Стрелки не требуются!
Эрик Липперт
8

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

ipp = &ip1;

результаты, ippчтобы указать ip1.

поэтому, ippчтобы указать на ip2, мы должны изменить таким же образом,

ipp = &ip2;

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

*ipp = ip2;

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

ipp = &ip1значит *ipp = ip1 = &i,
сейчас *ipp = ip2 = &j.
Итак, *ipp = ip2по сути то же самое, что и ip1 = ip2.

Dipto
источник
5
ipp = &ip1;

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

То, что вы делаете с *ipp, то есть с ip1, не меняет того факта, на который ippуказывает ip1.

Даниэль Даранас
источник
5

Мой вопрос: почему на втором рисунке 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;

НТН

ZMO
источник
с , например, начальное значение ip2должно быть 3не 4.
Дипто
1
о, я просто изменил память, чтобы она соответствовала порядку объявления. Думаю, я это исправил?
zmo
5

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

*ipp = ip2

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

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

Диего Р. Алькантара
источник
4

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

Примеры:

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.
moooeeeep
источник
3

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

Andrejovich
источник
3

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

ipp = &ip1;

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

*ipp = *&ip1 // Here *& becomes 1  
*ipp = ip1   // Hence proved 
Сунил Боянапалли
источник
3

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

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так inctruction:

*ipp = ip2;

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

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

Но ippвсе же:

(ipp, &ipp, &ip1)

Таким образом, значение по- ippпрежнему &ip1означает, что это все еще указывает на ip1.

rullof
источник
1

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

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

Теперь мы находимся на ip1. *ipp(т.е. ip1) = ip2.
ip2содержит адрес j.so ip1содержимое будет заменено содержимым ip2 (то есть адресом j), МЫ НЕ ИЗМЕНИМ КОНТЕНТ ipp. ЭТО ОНО.

user3286725
источник
1

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

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

ip1 = ip2;

Если вы хотите, чтобы адрес ip2был сохранен ipp, просто выполните:

ipp = &ip2;

Теперь ippуказывает на ip2.

Rikayan Bandyopadhyay
источник
0

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

ipp = &ip2;  

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

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

Применение *оператора по 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указывает на jwhile ipp, все еще указывает на то, ip1что никакие модификации для изменения значения не выполняютсяipp .

haccks
источник