Указатели указателей против обычных указателей

Целью указателя является сохранение адреса определенной переменной. Тогда структура памяти следующего кода должна выглядеть так:

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().
}

Это... очень опасная проблема, и это легко решить, если число * напрямую представляет уровень косвенности.