Указатели на указатели против обычных указателей
цель указателя-сохранить адрес определенной переменной. Тогда структура памяти следующего кода должна выглядеть так:
int a = 5;
int *b = &a;
...... адрес памяти ...... значение
один... 0x000002 ................... 5
б... 0x000010 ................... 0x000002
Ладно, хорошо. Тогда предположим, что теперь я хочу сохранить адрес указателя * b. тогда мы обычно определяем двойной указатель, * * c, как
int a = 5;
int *b = &a;
int **c = &b;
тогда структура памяти выглядит так:
...... адрес памяти ...... значение
один... 0x000002 ................... 5
б... 0x000010 ................... 0x000002
с. .. 0x000020 ................... 0x000010
Итак * * c ссылается на адрес *b.
теперь мой вопрос, почему такого кода
int a = 5;
int *b = &a;
int *c = &b;
генерировать предупреждение?
если цель указателя - просто сохранить адрес памяти, я думаю, что не должно быть иерархии, если адрес, который мы собираемся сохранить, относится к переменной, указателю, двойному указателю и т. д., поэтому ниже тип кода должен быть действительным.
int a = 5;
int *b = &a;
int *c = &b;
int *d = &c;
int *e = &d;
int *f = &e;
12 ответов:
на
int a = 5; int *b = &a; int *c = &b;
вы получаете предупреждение, потому что
&b
типаint **
, и вы пытаетесь инициализировать переменную типаint *
. Между этими двумя типами нет неявных преобразований, ведущих к предупреждению.чтобы взять более длинный пример, который вы хотите работать, если мы попытаемся разыменовать
f
компилятор даст намint
, а не указатель, который мы можем дополнительно разыменовать.Также обратите внимание, что на многих системах
int
иint*
не тот же размер (например, указатель может быть длиной 64 бита иint
32 бит). Если вы разыменоватьf
иint
, вы теряете половину значения, а затем вы даже не можете привести его к допустимому указателю.
если цель указателя-просто сохранить адрес памяти, я думаю не должно быть никакой иерархии, если адрес, который мы намерены сохранить ссылается на переменную, указатель, двойной указатель ... и т. д.
во время выполнения, да, указатель просто содержит адрес. Но во время компиляции есть также тип, связанный с каждой переменной. Как говорили другие,
int*
иint**
это два разных, несовместимых типа.есть один тип,
void*
, что делает то, что вы хотите: он хранит только адрес, вы можете назначить ему любой адрес:int a = 5; int *b = &a; void *c = &b;
но если вы хотите, чтобы разыменовать
void*
, вам нужно предоставить информацию о типе "отсутствует" самостоятельно:int a2 = **((int**)c);
теперь мой вопрос, почему этот тип кода,
int a = 5; int *b = &a; int *c = &b;
генерировать предупреждение?
вам нужно вернуться к основам.
- переменные типа
- переменные содержат значения
- указатель-это значение
- указатель ссылается на переменную
- если
p
- это значение указателя, то*p
переменная- если
v
является переменной, то&v
указательи теперь мы можем найти все ошибки в проводке.
тогда предположим, что теперь я хочу сохранить адрес указателя
*b
нет.
*b
- переменная типа int. Это не указатель.b
- это переменная, которой стоимостью - это указатель.*b
это переменная чье значение является целым числом.
**c
относится к адресу*b
.НЕТ НЕТ НЕТ. Абсолютно нет. Ты есть чтобы понять это правильно, если вы собираетесь понять указатели.
*b
- это переменная, это псевдоним переменнойa
. Адрес переменнойa
значение переменнойb
.**c
не относится к адресуa
. Скорее, это переменная что это псевдоним для переменнойa
. (И так же*b
.)правильное утверждение такое:стоимостью переменной
c
- это адрес наb
. Или, что эквивалентно: значениеc
- это указатель, который ссылается наb
.откуда мы это знаем? Вернемся к основам. Ты сказал, что
c = &b
. Так что же такое значениеc
? Указатель. К чему?b
.убедитесь, что вы полностью понять основные правила.
теперь, вы, надеюсь, поймете правильную взаимосвязь между переменными и указателями, вы должны быть в состоянии ответить на ваш вопрос о том, почему ваш код дает ошибку.
система типов C требует этого, если вы хотите получить правильное предупреждение и если вы хотите, чтобы код компилировался вообще. С помощью только одного уровня глубины указателей вы не знаете, указывает ли указатель на указатель или на фактическое целое число.
если вы разыменования типа
int**
вы знаете, что тип вы получаетеint*
и аналогично, если вы разыменованияint*
типint
. С вашим предложением тип будет неоднозначным.из твоего примера, невозможно знать, будет ли
c
точкиint
илиint*
:c = rand() % 2 == 0 ? &a : &b;
на какой тип указывает c? Компилятор этого не знает, поэтому следующую строку Выполнить невозможно:
*c;
в C вся информация о типе теряется после компиляции, так как каждый тип проверяется во время компиляции и больше не нужен. Ваше предложение фактически потеряет память и время, так как каждый указатель должен будет иметь дополнительную информацию о времени выполнения о содержащихся типах в указателях.
указатели абстракции адресов памяти с дополнительной семантикой типа, и в языке, таком как Тип C имеет значение.
во-первых, нет никакой гарантии, что
int *
иint **
имеют одинаковый размер или представление (на современных настольных архитектурах они делают, но вы не можете полагаться на то, что это универсально верно).во-вторых, тип имеет значение для арифметики указателя. Учитывая указатель
p
типаT *
, выражениеp + 1
дает адрес следующего объект типаT
. Итак, предположим следующие объявления:char *cp = 0x1000; short *sp = 0x1000; // assume 16-bit short int *ip = 0x1000; // assume 32-bit int long *lp = 0x1000; // assume 64-bit long
выражение
cp + 1
дает нам адрес следующейchar
объект, который был бы0x1001
. Выражениеsp + 1
дает нам адрес следующейshort
объект, который был бы0x1002
.ip + 1
дает0x1004
иlp + 1
дает0x1008
.таким образом, учитывая
int a = 5; int *b = &a; int **c = &b;
b + 1
дает нам адрес следующегоint
иc + 1
дает нам адрес следующей указатель доint
.указатель на указатель требуется, если вы хотите, чтобы функция записывала в параметр типа указателя. Возьмите следующий код:
void foo( T *p ) { *p = new_value(); // write new value to whatever p points to } void bar( void ) { T val; foo( &val ); // update contents of val }
это верно для любой тип
T
. Если мы заменимT
С типом указателяP *
, код становитсяvoid foo( P **p ) { *p = new_value(); // write new value to whatever p points to } void bar( void ) { P *val; foo( &val ); // update contents of val }
семантика точно такая же, это просто типы, которые отличаются; формальный параметр
p
всегда на один уровень больше косвенности, чем переменнаяval
.
Я думаю, что не должно быть никакой иерархии, если адрес, который мы собираемся сохранить, ссылается на переменную, указатель, двойной указатель
без "иерархии" было бы очень легко генерировать UB повсюду без каких - либо предупреждений-это было бы ужасно.
рассмотрим следующий пример:
char c = 'a'; char* pc = &c; char** ppc = &pc; printf("%c\n", **ppc); // compiles ok and is valid printf("%c\n", **pc); // error: invalid type argument of unary ‘*’
компилятор дает мне ошибку, и тем самым он помогает мне знать, что я сделал что-то неправильно, и я могу исправить ошибку.
но без "иерархия", например:
char c = 'a'; char* pc = &c; char* ppc = &pc; printf("%c\n", **ppc); // compiles ok and is valid printf("%c\n", **pc); // compiles ok but is invalid
компилятор не может дать никакой ошибки, так как нет"иерархии".
но когда строку:
printf("%c\n", **pc);
выполняет, это UB (неопределенное поведение).
первый
*pc
читаетchar
как если бы это был указатель, т. е., вероятно, читает 4 или 8 байт, хотя мы зарезервировали только 1 байт. Это UB.если программа не разбилась из-за UB выше, но просто вернула некоторое значение garbish, вторым шагом будет чтобы разыменовать значение garbish. В очередной раз УБ.
вывод
система типов помогает нам обнаруживать ошибки, видя int*, int**, int** * и т. д. Как разные типы.
если цель указателя - просто сохранить адрес памяти, я думаю, что не должно быть иерархии, если адрес, который мы собираемся сохранить, ссылается на переменную, указатель, двойной указатель,... так далее. поэтому ниже тип кода должен быть действительным.
я думаю, что вот ваше недоразумение: цель самого указателя-сохранить адрес памяти, но указатель обычно также имеет тип, чтобы мы знали, чего ожидать в месте, где он указывает к.
особенно, в отличие от вас, другие люди действительно хотят иметь такую иерархию, чтобы знать, что делать с содержимым памяти, на которое указывает указатель.
это самая точка системы указателей C, чтобы иметь информацию о типе, прикрепленную к нему.
если у вас
int a = 5;
&a
подразумевает, что вы получаетеint *
Так что если вы разыменовать этоint
снова.приведение этого к следующему уровни,
int *b = &a; int **c = &b;
&b
также является указателем. Но не зная, что за этим скрывается, ОТВ. то, на что он указывает, бесполезно. Важно знать, что разыменование указателя показывает тип исходного типа, так что*(&b)
этоint *
и**(&b)
оригиналint
значение, с которым мы работаем.если вы чувствуете, что в ваших обстоятельствах не должно быть никакой иерархии типов, вы всегда можете работать с
void *
, хотя прямое удобство использования весьма ограниченный.
Если цель указателя - просто сохранить адрес памяти, я думаю, что не должно быть иерархии, если адрес, который мы собираемся сохранить, ссылается на переменную, указатель, двойной указатель,... так далее. поэтому ниже тип кода должен быть действительным.
Ну это верно для машины (в конце концов, примерно все это число). Но во многих языках переменные типизированы, значит, компилятор может гарантировать, что вы используете их правильно (типы накладывают правильный контекст на переменные)
верно, что указатель на указатель и указатель (вероятно) используют один и тот же объем памяти для хранения их значения (будьте осторожны, это не верно для int и указателя на int, размер адреса не связан с размером дома).
поэтому, если у вас есть адрес адреса, который вы должны использовать как есть, а не как простой адрес, потому что если вы обращаетесь к указателю на указатель как к простому указателю, то вы сможете манипулировать адресом int, как если бы это был int, которого нет (замените int без чего-либо еще, и вы должны увидеть опасность). Вы можете быть смущены, потому что все это цифры, но в повседневной жизни вы не: я лично большой разницы в $1 и 1 собака. собака и $ - это типы, вы знаете, что с ними можно сделать.
вы можете запрограммировать в сборке и сделать то, что вы хотите, но вы увидите, насколько это опасно, потому что вы можете делать почти то, что вы хотите, особенно странные вещи. Да изменение значения адреса опасно, предположим, у вас есть автономный автомобиль, который должен доставить кое-что в обращении выражена в расстояние: 1200 память улиц (адрес) и предположим, что на этой улице дома разделяются 100 футов (1221-это не действительный адрес), если вы не в состоянии управлять адресов, как вам нравится, как целое число, вы могли бы попробовать поставить на 1223 и давай пакет в середине тротуара.
Другим примером может быть, дом, адрес дома, номер записи в адресной книге этот адрес. Все эти три понятия разные, разные типы...
есть разные типы. И для этого есть веская причина:
будет ...
int a = 5; int *b = &a; int **c = &b;
... выражение ...
*b * 5
... действует, в то время как выражение ...
*c * 5
нет смысла.
ничего страшного нет,как указатели и указатели на указатели хранятся, но к чему они ссылаются.
языка C ++ является строго типизированным. Это означает, что для каждого адреса есть тип, который сообщает компилятору, как интерпретировать значение по этому адресу.
в вашем примере:
int a = 5; int *b = &a;
тип
a
иint
, типаb
иint *
(читается как "указатель наint
"). Используя Ваш пример, память будет содержать:..... memory address ...... value ........ type a ... 0x00000002 .......... 5 ............ int b ... 0x00000010 .......... 0x00000002 ... int*
The тип на самом деле не хранятся в памяти, это просто компилятор знает это, когда вы читаете
a
вы можете найтиint
, а когда читаешьb
вы найдете адрес места, где вы можете найтиint
.второй пример:
int a = 5; int *b = &a; int **c = &b;
тип
c
иint **
прочитал как "указатель на указатель наint
". Это означает, что для компилятора:
c
указатель;- когда вы читаете
c
, вы получаете адрес другого указатель;- когда вы читаете этот другой указатель, вы получаете адрес
int
.то есть
c
указатель (int **
);*c
также является указателем (int *
);**c
этоint
.и память будет содержать:
..... memory address ...... value ........ type a ... 0x00000002 .......... 5 ............ int b ... 0x00000010 .......... 0x00000002 ... int* c ... 0x00000020 .......... 0x00000010 ... int**
так как " тип " не хранится вместе со значением, а указатель может указывать на любой адрес памяти, то способ, которым компилятор знает тип значения по адресу, в основном заключается в том, чтобы взять тип указателя и удалить самый правый
*
.
кстати, это для общей 32-битной архитектуры. Для большинства 64-разрядных архитектур, вы будете иметь:
..... memory address .............. value ................ type a ... 0x0000000000000002 .......... 5 .................... int b ... 0x0000000000000010 .......... 0x0000000000000002 ... int* c ... 0x0000000000000020 .......... 0x0000000000000010 ... int**
адреса теперь 8 байт каждый, в то время как
int
еще только 4 байта. Так как компилятор знает тип каждой переменной, она может легко общаться с этой разницей, и прочитала 8 байт для указателя и 4 байта дляint
.
почему этот код выдает предупреждение?
int a = 5; int *b = &a; int *c = &b;
The
&
оператор возвращает указатель на объект, то есть&a
типаint *
так назначая (через инициализацию) егоb
который также имеет типint *
действителен.&b
возвращает указатель на объектb
, что составляет&b
имеет тип указатель наint *
, Я .e.,int **
.C говорит в ограничениях оператора присваивания (которые держат для инициализация) что (C11, 6.5.16. 1p1):"оба операнда являются указателями на квалифицированных или неквалифицированных версий совместимых типов". Но в C определении того, что такое a совместимость тип
int **
иint *
не совместимость типы.так что есть нарушение ограничений в
int *c = &b;
инициализация, которая означает, что компилятор требует диагностики.одно из обоснований правила здесь нет стандарт гарантирует, что два разных типа указателей имеют одинаковый размер (за исключением
void *
и типы указателей символов), то естьsizeof (int *)
иsizeof (int **)
могут быть разные значения.
это было бы потому, что любой указатель
T*
- это на самом деле типаpointer to a T
(илиaddress of a T
), гдеT
- это указывает на тип. В этом случае*
можно читать какpointer to a(n)
иT
- это указывает на тип.int x; // Holds an integer. // Is type "int". // Not a pointer; T is nonexistent. int *px; // Holds the address of an integer. // Is type "pointer to an int". // T is: int int **pxx; // Holds the address of a pointer to an integer. // Is type "pointer to a pointer to an int". // T is: int*
это используется для разыменования целей, где оператор разыменования будет принимать
T*
, и возвращает значение, тип которого являетсяT
. Возвращаемый тип можно рассматривать как усечение крайнего левого " указателя на a (n)" и быть тем, что осталось свыше.*x; // Invalid: x isn't a pointer. // Even if a compiler allows it, this is a bad idea. *px; // Valid: px is "pointer to int". // Return type is: int // Truncates leftmost "pointer to" part, and returns an "int". *pxx; // Valid: pxx is "pointer to pointer to int". // Return type is: int* // Truncates leftmost "pointer to" part, and returns a "pointer to int".
обратите внимание, что для каждой из вышеперечисленных операций возвращаемый тип оператора разыменования совпадает с исходным
T*
декларацииT
тип.это значительно помогает как примитивным компиляторам, так и программистам в разборе типа указателя: для компилятора оператор address-of добавляет a
*
к типу оператор разыменования удаляет a*
из вида, и любое несоответствие является ошибкой. Для программиста, число*
s является прямым указание того, с каким количеством уровней косвенности вы имеете дело (int*
всегда указывает наint
,float**
всегда указывает наfloat*
, который, в свою очередь, всегда указывает наfloat
и т. д.).
теперь, принимая это во внимание, есть две основные проблемы с использованием только одного
*
независимо от количества уровней косвенности:
- указатель гораздо сложнее для компилятора разыменовать, потому что он должен ссылаться на последнее назначение для определения уровня косвенности и определения типа возвращаемого значения соответствующим образом.
- указатель труднее понять программисту, потому что легко потерять след того, сколько слоев косвенности есть.
в обоих случаях, единственный способ определить фактический тип значения будет отступать, вынуждая вас искать где-то еще, чтобы найти его.
void f(int* pi); int main() { int x; int *px = &x; int *ppx = &px; int *pppx = &ppx; f(pppx); } // Ten million lines later... void f(int* pi) { int i = *pi; // Well, we're boned. // To see what's wrong, see main(). }
этого... это очень опасно проблема, и тот, который легко решается, имея число
*
s непосредственно представляют уровень косвенности.