Указатели указателей против обычных указателей
Целью указателя является сохранение адреса определенной переменной. Тогда структура памяти следующего кода должна выглядеть так:
int a = 5;
int *b = &a;
...... адрес памяти...... значение
a... 0x000002................... 5
b... 0x000010................... 0x000002
Хорошо, отлично. Тогда предположим, что теперь я хочу сохранить адрес указателя * b. Затем мы обычно определяем двойной указатель, ** c, as
int a = 5;
int *b = &a;
int **c = &b;
Затем структура памяти выглядит так:
...... адрес памяти...... значение
a... 0x000002................... 5
b... 0x000010................... 0x000002
c... 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;
Ответы
Ответ 1
В
int a = 5;
int *b = &a;
int *c = &b;
Вы получаете предупреждение, потому что &b
имеет тип int **
, и вы пытаетесь инициализировать переменную типа int *
. Там нет неявных преобразований между этими двумя типами, что приводит к предупреждению.
Чтобы взять более длинный пример, который вы хотите работать, если мы попытаемся разыменовать f
, компилятор даст нам int
, а не указатель, который мы можем дополнительно разыменовать.
Также обратите внимание, что во многих системах int
и int*
не имеют одинакового размера (например, указатель может быть длиной 64 бита и длиной int
32 бита). Если вы разыскиваете f
и получаете int
, вы теряете половину значения, а затем вы не можете даже применить его к допустимому указателю.
Ответ 2
Если целью указателя является просто сохранение адреса памяти, я думаю не должно быть иерархии, если адрес, который мы собираемся сохранить ссылается на переменную, указатель, двойной указатель,... и т.д.
Во время выполнения да, указатель просто содержит адрес. Но во время компиляции существует также тип, связанный с каждой переменной. Как говорили другие, int*
и int**
- два разных, несовместимых типа.
Существует один тип void*
, который делает то, что вы хотите: он хранит только адрес, вы можете назначить ему любой адрес:
int a = 5;
int *b = &a;
void *c = &b;
Но если вы хотите разыменовать void*
, вам нужно указать "отсутствующую" информацию о типе самостоятельно:
int a2 = **((int**)c);
Ответ 3
Теперь мой вопрос: почему этот тип кода,
int a = 5;
int *b = &a;
int *c = &b;
создать предупреждение?
Вам нужно вернуться к основам.
- переменные имеют типы
- переменные сохраняют значения
- указатель - это значение
- указатель ссылается на переменную
- если
p
- значение указателя, тогда *p
является переменной
- if
v
- это переменная, а &v
- указатель
И теперь мы можем найти все ошибки в вашей публикации.
Тогда предположим, что теперь я хочу сохранить адрес указателя *b
Нет. *b
- это переменная типа int. Это не указатель. b
- это переменная, значение которой является указателем. *b
- это переменная, значение которой является целым числом.
**c
относится к адресу *b
.
NO NO NO. Точно нет. Вы должны понять это правильно, если собираетесь понять указатели.
*b
- переменная; это псевдоним для переменной a
. Адрес переменной a
- это значение переменной b
. **c
не относится к адресу a
. Скорее, это переменная, которая является псевдонимом для переменной a
. (И так же *b
.)
Правильный оператор: значение переменной c
является адресом b
. Или, что эквивалентно: значение c
является указателем, который ссылается на b
.
Как мы это знаем? Вернитесь к основам. Вы сказали, что c = &b
. Итак, каково значение c
? Указатель. К чему? b
.
Убедитесь, что вы полностью понимаете основные правила.
Теперь, когда вы, надеюсь, понимаете правильную взаимосвязь между переменными и указателями, вы должны уметь ответить на ваш вопрос о том, почему ваш код дает ошибку.
Ответ 4
Система типов C требует этого, если вы хотите получить правильное предупреждение и если вы хотите, чтобы код вообще компилировался. Только с одним уровнем глубины указателей вы не знаете, указывает ли указатель на указатель или на фактическое целое число.
Если вы разыскиваете тип int**
, вы знаете, что тип, который вы получаете, это int*
и аналогично, если вы разыскиваете int*
, тип int
. С вашим предложением тип будет неоднозначным.
Взяв из вашего примера, невозможно узнать, указывает ли c
на int
или int*
:
c = rand() % 2 == 0 ? &a : &b;
Какой тип c указывает? Компилятор этого не знает, поэтому эту следующую строку выполнить невозможно:
*c;
В C вся информация типа теряется после компиляции, так как каждый тип проверяется во время компиляции и больше не нужен. Ваше предложение будет фактически тратить память и время, так как каждый указатель должен иметь дополнительную информацию о времени выполнения, содержащуюся в указателях.
Ответ 5
Указатели - это абстракции адресов памяти с дополнительной семантикой типа и на языке типа 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
.
Ответ 6
Я думаю, что не должно быть иерархии, если адрес, который мы собираемся сохранить, ссылается на переменную, указатель, двойной указатель
Без "иерархии" было бы очень легко сгенерировать 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 (undefined).
Первый *pc
читает char
, как если бы он был указателем, то есть, вероятно, читал 4 или 8 байтов, хотя мы зарезервировали только 1 байт. Это UB.
Если программа не вышла из строя из-за вышеперечисленного UB, а только вернула некоторую мерную ценность, вторым шагом было бы разыменовать значение garbish. Еще раз UB.
Заключение
Система типов помогает нам обнаруживать ошибки, видя int *, int **, int *** и т.д. как разные типы.
Ответ 7
Если целью указателя является просто сохранение адреса памяти, я думаю, что не должно быть иерархии, если адрес, который мы собираемся сохранить, относится к переменной, указателю, двойному указателю и т.д., поэтому ниже тип кода должен Действительны.
Я думаю, что это ваше недоразумение. Цель самого указателя - сохранить адрес памяти, но обычно указатель имеет тип, чтобы мы знали, чего ожидать в том месте, на которое оно указывает.
В частности, в отличие от вас, другие люди действительно хотят иметь такую иерархию, чтобы знать, что делать с содержимым памяти, на которое указывает указатель.
Именно эта точка системы указателей C имеет прикрепленную к ней информацию типа.
Если вы делаете
int a = 5;
&a
подразумевает, что вы получаете int *
, так что если вы разыскиваете это снова int
.
Принеся это на следующие уровни,
int *b = &a;
int **c = &b;
&b
также является указателем. Но, не зная, что скрывается за ним, на что он указывает, это бесполезно. Важно знать, что разыменование указателя показывает тип исходного типа, так что *(&b)
является int *
, а **(&b)
является исходным значением int
, с которым мы работаем.
Если вы считаете, что в ваших обстоятельствах не должно быть иерархии типов, вы всегда можете работать с void *
, хотя прямое юзабилити довольно ограничено.
Ответ 8
Если целью указателя является просто сохранение адреса памяти, я думаю, что не должно быть иерархии, если адрес, который мы собираемся сохранить, относится к переменной, указателю, двойному указателю и т.д., поэтому ниже тип кода должен Действительны.
Хорошо, что это правда для машины (ведь примерно все - это число). Но на многих языках вводятся переменные, означает, что компилятор может затем убедиться, что вы используете их правильно (типы накладывают правильный контекст на переменные)
Верно, что указатель на указатель и указатель (возможно) используют один и тот же объем памяти для хранения их значения (остерегайтесь, это неверно для int и указателя на int, размер адреса не связан с размер дома).
Итак, если у вас есть адрес адреса, который вы должны использовать как есть, а не как простой адрес, потому что если вы будете обращаться к указателю на указатель как простой указатель, тогда вы сможете манипулировать адресом int, как если бы он это int, который не является (замените int без чего-либо еще, и вы должны увидеть опасность). Вы можете быть смущены, потому что все это цифры, но в повседневной жизни вы этого не делаете: я лично делаю большую разницу в собаках 1 и 1 доллара. dog и $- типы, вы знаете, что вы можете с ними делать.
Вы можете программировать в сборке и делать то, что хотите, но вы будете наблюдать, насколько это опасно, потому что вы можете делать почти то, что хотите, особенно странные вещи. Да, изменение значения адреса опасно, предположим, что у вас есть автономный автомобиль, который должен доставить что-то по адресу, указанному на расстоянии: 1200 памяти (адрес), и предположим, что в этих уличных домах разделены 100 футов (1221 - недействительный адрес), если вы можете манипулировать адресами, как вам нравится, как целое, вы могли бы попытаться выполнить доставку в 1223 и позволить пакету посередине тротуара.
Другим примером может быть: дом, адрес дома, номер записи в адресной книге этого адреса. Все эти три разные концепции, разные типы...
Ответ 9
Существуют разные типы. И для этого есть веская причина:
Имея...
int a = 5;
int *b = &a;
int **c = &b;
... выражение...
*b * 5
... действителен, а выражение...
*c * 5
не имеет смысла.
Большое дело не в том, как хранятся указатели или указатели на указатели, но на то, что они ссылаются.
Ответ 10
Язык C строго типизирован. Это означает, что для каждого адреса существует тип, который сообщает компилятору, как интерпретировать значение по этому адресу.
В вашем примере:
int a = 5;
int *b = &a;
Тип a
- int
, а тип b
- int *
(читается как "указатель на int
" ). Используя ваш пример, память будет содержать:
..... memory address ...... value ........ type
a ... 0x00000002 .......... 5 ............ int
b ... 0x00000010 .......... 0x00000002 ... int*
Тип фактически не хранится в памяти, это просто то, что компилятор знает, что когда вы читаете 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**
Поскольку "тип" не сохраняется вместе со значением, а указатель может указывать на любой адрес памяти, то, как компилятор знает тип значения по адресу, в основном, используя тип указателя и удаляя rightmost *
.
Кстати, это для общей 32-битной архитектуры. Для большинства 64-разрядных архитектур у вас будет:
..... memory address .............. value ................ type
a ... 0x0000000000000002 .......... 5 .................... int
b ... 0x0000000000000010 .......... 0x0000000000000002 ... int*
c ... 0x0000000000000020 .......... 0x0000000000000010 ... int**
Адреса теперь имеют 8 байтов, а int
- всего 4 байта. Поскольку компилятор знает тип каждой переменной, он легко справляется с этой разницей и читает 8 байтов для указателя и 4 байта для int
.
Ответ 11
Почему этот тип кода генерирует предупреждение?
int a = 5;
int *b = &a;
int *c = &b;
Оператор &
дает указатель на объект, то есть &a
имеет тип int *
, поэтому присваивание (посредством инициализации) его b
, которое также имеет тип int *
. &b
выводит указатель на объект b
, то есть &b
имеет указатель на тип int *
, i.e., int **
.
C говорит в ограничениях оператора присваивания (которые сохраняются для инициализации), что (C11, 6.5.16.1p1): "оба операнда являются указателями на квалифицированные или неквалифицированные версии совместимых типов". Но в определении C того, что является совместимым типом int **
и int *
, не являются совместимыми типами.
Таким образом, в инициализации int *c = &b;
имеется ограничение ограничения, что означает, что компилятор требует диагностики.
Одним из обоснований правила здесь является то, что Стандарт не гарантирует, что два разных типа указателей имеют одинаковый размер (кроме void *
и типов указателей символов), то есть sizeof (int *)
и sizeof (int **)
могут быть разными значениями.
Ответ 12
Это было бы потому, что любой указатель 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
. Тип возвращаемого значения можно рассматривать как обрезание самого левого "указателя на (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
.
Это очень помогает как примитивным компиляторам, так и программистам при анализе типа указателя: для компилятора оператор-адрес добавляет к типу *
, оператор разыменования удаляет *
из типа и любое несоответствие является ошибкой. Для программиста количество *
является прямым указанием того, сколько уровней косвенности вы имеете в виду (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 wrong, see main().
}
Это... очень опасная проблема, и это легко решить, если число *
напрямую представляет уровень косвенности.