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


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

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 74

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 и т. д.).


теперь, принимая это во внимание, есть две основные проблемы с использованием только одного * независимо от количества уровней косвенности:

  1. указатель гораздо сложнее для компилятора разыменовать, потому что он должен ссылаться на последнее назначение для определения уровня косвенности и определения типа возвращаемого значения соответствующим образом.
  2. указатель труднее понять программисту, потому что легко потерять след того, сколько слоев косвенности есть.

в обоих случаях, единственный способ определить фактический тип значения будет отступать, вынуждая вас искать где-то еще, чтобы найти его.

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 непосредственно представляют уровень косвенности.