Как объяснить указатели C (объявление против унарных операторов) новичку?

142

Недавно я имел удовольствие объяснять указатели новичку в программировании на C и наткнулся на следующую трудность. Это может показаться не проблемой, если вы уже знаете, как использовать указатели, но постарайтесь внимательно рассмотреть следующий пример:

int foo = 1;
int *bar = &foo;
printf("%p\n", (void *)&foo);
printf("%i\n", *bar);

Для абсолютного новичка результат может быть неожиданным. В строке 2 он / она только что объявил * bar как & foo, но в строке 4 оказалось, что * bar на самом деле foo, а не & foo!

Путаница, можно сказать, проистекает из двусмысленности символа *: в строке 2 он используется для объявления указателя. В строке 4 он используется как унарный оператор, который выбирает значение, на которое указывает указатель. Две разные вещи, правда?

Однако новичку это «объяснение» совершенно не помогает. Он вводит новую концепцию, указывая на небольшое несоответствие. Это не может быть правильным способом учить это.

Итак, как это объяснили Керниган и Ричи?

Унарный оператор * - это оператор косвенного обращения или разыменования; при применении к указателю он обращается к объекту, на который указывает указатель. […]

Объявление указателя ip int *ipзадумано как мнемоника; он говорит, что выражение *ip- это int. Синтаксис объявления переменной имитирует синтаксис выражений, в которых может присутствовать переменная .

int *ipследует читать как " *ipвернет int"? Но почему тогда присвоение после объявления не следует этому шаблону? Что, если новичок захочет инициализировать переменную? int *ip = 1(читать: *ipвернет intи intесть 1) не будет работать должным образом. Концептуальная модель просто не кажется последовательной. Я что-то упустил?


Изменить: он попытался обобщить ответы здесь .

Армин
источник
15
Лучшее объяснение - рисовать предметы на бумаге и соединять их стрелками;)
Марун
17
Когда мне приходилось объяснять синтаксис указателей, я всегда настаивал на том, что *в объявлении есть токен, означающий «объявить указатель», в выражениях - это оператор разыменования, и что эти два элемента представляют разные вещи, которые имеют один и тот же символ. (то же, что и оператор умножения - тот же символ, другое значение). Это сбивает с толку, но все, что отличается от реального положения дел, будет еще хуже.
Маттео Италия
40
возможно, написав это так int* bar, станет более очевидным, что звезда на самом деле является частью типа, а не идентификатора. Конечно, при этом возникают разные проблемы с такими не интуитивными вещами, как int* a, b.
Никлас Б.
10
Я всегда думал, что объяснение K&R глупо и ненужно. В языке один и тот же символ используется для двух разных вещей, и нам просто нужно с этим разобраться. *может иметь два разных значения в зависимости от контекста. Точно так же, как одна и та же буква может произноситься по-разному, в зависимости от того, в каком слове она содержится, что затрудняет изучение многих языков. Если бы у каждой отдельной концепции / операции был свой собственный символ, нам понадобились бы гораздо большие клавиатуры, поэтому символы перерабатываются, когда это имеет смысл.
Art
8
Я много раз сталкивался с одной и той же проблемой, обучая других Си, и, по моему опыту, ее можно решить так, как предлагает большинство людей. Сначала объясните концепцию указателя без синтаксиса C. Затем обучите синтаксису и выделите звездочку как часть типа ( int* p), одновременно предостерегая учащегося от использования нескольких объявлений в одной строке, когда задействованы указатели. Когда студент полностью понял концепцию указателей, объясните ему, что int *pсинтаксис is эквивалентен, а затем объясните проблему с несколькими объявлениями.
Теодорос Хатцигианнакис

Ответы:

44

Чтобы ваш ученик мог понять значение *символа в разных контекстах, он должен сначала понять, что контексты действительно разные. Как только они поймут, что контексты различны (т. Е. Разница между левой частью задания и общим выражением), это не будет большим когнитивным скачком, чтобы понять, в чем заключаются различия.

Сначала объясните, что объявление переменной не может содержать операторов (продемонстрируйте это, показав, что размещение символа -или +в объявлении переменной просто вызывает ошибку). Затем покажите, что выражение (т. Е. В правой части присваивания) может содержать операторы. Убедитесь, что учащийся понимает, что выражение и объявление переменной - это два совершенно разных контекста.

Когда они поймут, что контексты различны, вы можете продолжить объяснение, что когда *символ находится в объявлении переменной перед идентификатором переменной, это означает «объявить эту переменную как указатель». Затем вы можете объяснить, что при использовании в выражении (в качестве унарного оператора) *символ является «оператором разыменования» и означает «значение по адресу», а не его предыдущее значение.

Чтобы по-настоящему убедить вашего ученика, объясните, что создатели C могли использовать любой символ для обозначения оператора разыменования (т.е. они могли бы использовать @вместо него), но по какой-либо причине они приняли решение об использовании *.

В общем, невозможно объяснить, что контексты разные. Если ученик не понимает, что контексты разные, он не может понять, почему *символ может означать разные вещи.

Фарап
источник
81

Причина, по которой стенография:

int *bar = &foo;

в вашем примере может сбивать с толку то, что его легко неверно истолковать как эквивалент:

int *bar;
*bar = &foo;    // error: use of uninitialized pointer bar!

когда это на самом деле означает:

int *bar;
bar = &foo;

Написанный таким образом, с разделением объявления и присваивания переменной, нет такой возможности для путаницы, и параллелизм использования ↔, описанный в вашей цитате K&R, работает отлично:

  • Первая строка объявляет переменную bar, так что *barэто int.

  • Вторая строка присваивает адрес fooдля bar, делая *bar(an int) псевдонимом для foo(также an int).

При знакомстве с синтаксисом указателя C новичкам может быть полезно изначально придерживаться этого стиля разделения объявлений указателя от назначений и вводить комбинированный сокращенный синтаксис (с соответствующими предупреждениями о его потенциальной путанице) только после того, как основные концепции использования указателя в C. были адекватно интернализованы.

Илмари Каронен
источник
4
У меня было бы искушение typedef. typedef int *p_int;означает, что переменная типа p_intимеет свойство, которое *p_intявляется int. Тогда у нас есть p_int bar = &foo;. Поощрять кого-либо создавать неинициализированные данные, а затем назначать их по умолчанию, кажется ... плохой идеей.
Якк - Адам Неврамонт
6
Это просто поврежденный мозг стиль деклараций C; это не относится к указателям. считают int a[2] = {47,11};, что не является инициализация (несуществующей) элемента a[2]eiher.
Marc van Leeuwen
5
@MarcvanLeeuwen Согласитесь с повреждением мозга. В идеале он *должен быть частью типа, а не привязан к переменной, и тогда вы сможете написать int* foo_ptr, bar_ptrдля объявления двух указателей. Но на самом деле он объявляет указатель и целое число.
Barmar
1
Речь идет не только о "сокращенных" объявлениях / присвоениях. Вся проблема снова возникает в тот момент, когда вы хотите использовать указатели в качестве аргументов функции.
armin
30

Коротко о декларациях

Приятно знать разницу между объявлением и инициализацией. Мы объявляем переменные как типы и инициализируем их значениями. Если мы делаем и то, и другое одновременно, мы часто называем это определением.

1. int a; a = 42;

int a;
a = 42;

Мы заявляем об intимени А . Затем мы инициализируем его, задавая ему значение 42.

2. int a = 42;

Мы заявляем и intназвали и дать ей значение 42. Это инициализируется . Определение.42

3. a = 43;

Когда мы используем переменные, мы говорим, что оперируем ими. a = 43это операция присваивания. Присваиваем переменной a число 43.

Говоря

int *bar;

мы объявляем bar указателем на int. Говоря

int *bar = &foo;

мы объявляем bar и инициализируем его адресом foo .

После того, как мы инициализировали bar, мы можем использовать тот же оператор, звездочку, для доступа и работы со значением foo . Без оператора мы получаем доступ и работаем с адресом, на который указывает указатель.

Кроме того, я позволил картинке говорить.

Какие

Упрощенное ВОСКРЕСЕНИЕ о том, что происходит. (А вот версия плеера, если хотите поставить на паузу и т. Д.)

          ВОСКРЕСЕНИЕ

Morpfh
источник
22

Второе утверждение int *bar = &foo;можно представить в виде наглядности в памяти как,

   bar           foo
  +-----+      +-----+
  |0x100| ---> |  1  |
  +-----+      +-----+ 
   0x200        0x100

Теперь barуказатель типа , intсодержащий адрес &из foo. Используя унарный оператор, *мы предпочитаем извлекать значение, содержащееся в 'foo', с помощью указателя bar.

РЕДАКТИРОВАТЬ : Мой подход к новичкам - объяснить memory addressпеременную, т.е.

Memory Address:Каждая переменная имеет связанный с ней адрес, предоставляемый ОС. В int a;, &aэто адрес переменной a.

Продолжайте объяснять основные типы переменных в Cвиде,

Types of variables: Переменные могут содержать значения соответствующих типов, но не адреса.

int a = 10; float b = 10.8; char ch = 'c'; `a, b, c` are variables. 

Introducing pointers: Как сказано выше, переменные, например

 int a = 10; // a contains value 10
 int b; 
 b = &a;      // ERROR

Назначение возможно, b = aно не возможно b = &a, поскольку переменная bможет содержать значение, но не адрес. Следовательно, нам нужны указатели .

Pointer or Pointer variables :Если переменная содержит адрес, она называется переменной-указателем. Используйте *в объявлении, чтобы сообщить, что это указатель.

• Pointer can hold address but not value
• Pointer contains the address of an existing variable.
• Pointer points to an existing variable
Сунил Боджанапалли
источник
3
Проблема в том, что при чтении int *ip«ip is a pointer (*) of type int» возникают проблемы при чтении чего-то вроде x = (int) *ip.
armin
2
@abw Это совсем другое, отсюда и круглые скобки. Я не думаю, что людям будет сложно понять разницу между декларациями и приведением типов.
bzeaman
@abw In x = (int) *ip;, получить значение путем разыменования указателя ipи привести значение к intлюбому типу ip.
Сунил Боджанапалли
1
@BennoZeeman Вы правы: кастинг и объявления - это разные вещи. Я попытался намекнуть на различную роль звездочки: 1-й "это не int, а указатель на int" 2-й "даст вам int, но не указатель на int".
armin
2
@abw: Именно поэтому обучение int* bar = &foo;делает нагрузки больше смысла. Да, я знаю, что это вызывает проблемы, когда вы объявляете несколько указателей в одном объявлении. Нет, я не думаю, что это вообще имеет значение.
Гонки за легкостью на орбите
17

Глядя на ответы и комментарии здесь, кажется, есть общее согласие с тем, что рассматриваемый синтаксис может сбивать с толку новичка. Большинство из них предлагают что-то в этом роде:

  • Прежде чем показывать какой-либо код, используйте диаграммы, эскизы или анимацию, чтобы проиллюстрировать, как работают указатели.
  • Представляя синтаксис, объясните две разные роли символа звездочки . Многие учебники отсутствуют или обходятся без этой части. Возникает путаница («Когда вы разбиваете объявление инициализированного указателя на объявление и последующее присвоение, вы должны не забыть удалить *» - часто задаваемые вопросы по comp.lang.c ) Я надеялся найти альтернативный подход, но я думаю, что это путь идти.

Вы можете написать int* barвместо, int *barчтобы подчеркнуть разницу. Это означает, что вы не будете следовать подходу K&R «использование имитации декларации», но подходу Stroustrup C ++ :

Мы не объявляем *barцелое число. Мы заявляем, barчто мы int*. Если мы хотим инициализировать вновь созданную переменную в той же строке, ясно, что мы имеем дело bar, а не *bar.int* bar = &foo;

Недостатки:

  • Вы должны предупредить своего ученика о проблеме с объявлением нескольких указателей ( int* foo, barvs int *foo, *bar).
  • Вы должны подготовить их к миру боли . Многие программисты хотят видеть звездочку рядом с именем переменной, и им потребуется много времени, чтобы оправдать свой стиль. И многие руководства по стилю явно применяют эту нотацию (стиль кодирования ядра Linux, Руководство по стилю NASA C и т. Д.).

Изменить: был предложен другой подход - использовать «имитацию» K&R, но без «сокращенного» синтаксиса (см. Здесь ). Как только вы пропустите объявление и присваивание в одной строке , все будет выглядеть намного более связным.

Однако рано или поздно ученику придется иметь дело с указателями как с аргументами функции. И указатели как возвращаемые типы. И указатели на функции. Вам нужно будет объяснить разницу между int *func();и int (*func)();. Думаю, рано или поздно все развалится. И, может быть, лучше раньше, чем позже.

Армин
источник
16

Есть причина, по которой стиль K&R отдает предпочтение int *pстилю Страуструпа int* p; оба действительны (и означают одно и то же) на каждом языке, но, как выразился Страуструп:

Выбор между "int * p;" и "int * p;" не о правильном и неправильном, а о стиле и акцентах. C подчеркнутые выражения; заявления часто считались не более чем необходимым злом. C ++, с другой стороны, уделяет большое внимание типам.

Теперь, поскольку вы пытаетесь обучить здесь Си, это наводит на мысль, что вам следует уделять больше внимания выражениям, чем типам, но некоторые люди могут легче понять одно ударение быстрее, чем другое, и это касается них, а не языка.

Поэтому некоторым людям будет легче начать с идеи, что an int*- это нечто иное, чем an, intи двигаться дальше.

Если кто - то быстро обращали внимание на дорогу смотреть на него , что использует , int* barчтобы иметь barкак вещь , которая не является INT, а указатель int, то они быстро поймут , что *barэто делают что - то для bar, а остальное приложится. После того, как вы сделали , что вы можете позже объяснить , почему C кодеры , как правило, предпочитают int *bar.

Или не. Если бы был один способ, которым все сначала поняли концепцию, у вас не было бы никаких проблем, и лучший способ объяснить это одному человеку не обязательно будет лучшим способом объяснить это другому.

Джон Ханна
источник
1
Мне нравится аргумент Страуструпа, но мне интересно, почему он выбрал символ & для обозначения ссылок - еще одна возможная ловушка.
armin
1
@abw Я думаю, он видел симметрию в том, что если мы можем, int* p = &aто можем int* r = *p. Я почти уверен, что он освещал это в книге «Дизайн и эволюция C ++» , но я давно не читал ее и по глупости отдал кому-то свой экземпляр.
Джон Ханна
3
Я думаю, вы имеете в виду int& r = *p. И держу пари, что заемщик все еще пытается переварить книгу.
armin
@abw, да, я именно это имел в виду. Увы, опечатки в комментариях не вызывают ошибок компиляции. Книгу на самом деле читают довольно оживленно.
Джон Ханна
4
Одна из причин, по которой я предпочитаю синтаксис Паскаля (в том виде, в котором он широко распространен), а не Си, заключается в том, что он Var A, B: ^Integer;дает понять, что тип «указатель на целое число» применяется как к, так Aи к B. Использование K&Rстиля int *a, *bтакже возможно; но заявление , как int* a,b;, впрочем, выглядит , как будто aи bоба объявленных как int*, но в действительности он объявляет aкак int*и bкак int.
supercat
9

tl; dr:

В: Как объяснить новичку указатели C (объявление против унарных операторов)?

A: не надо. Объясните указатели новичку и покажите им, как затем представить их концепции указателей в синтаксисе C.


Недавно я имел удовольствие объяснять указатели новичку в программировании на C и наткнулся на следующую трудность.

ИМО, синтаксис C не ужасен, но и не прекрасен: это не большая помеха, если вы уже понимаете указатели, и не помощь в их изучении.

Поэтому: начните с объяснения указателей и убедитесь, что они действительно их понимают:

  • Объясните их с помощью диаграмм в виде прямоугольников и стрелок. Вы можете сделать это без шестнадцатеричных адресов, если они не актуальны, просто покажите стрелки, указывающие либо на другое поле, либо на какой-либо символ нуля.

  • Объясните с помощью псевдокода: просто напишите адрес foo и значение, хранящееся в bar .

  • Затем, когда ваш новичок поймет, что такое указатели, почему и как их использовать; затем покажите отображение на синтаксис C.

Я подозреваю, что причина того, что текст K&R не предоставляет концептуальной модели, заключается в том, что они уже понимали указатели и, вероятно, предполагали, что это понимал и любой другой компетентный программист в то время. Мнемоника - это просто напоминание о преобразовании хорошо понятной концепции в синтаксис.

Бесполезный
источник
На самом деле; Сначала начните с теории, синтаксис придет позже (и это не важно). Обратите внимание, что теория использования памяти не зависит от языка. Эта модель из прямоугольников и стрелок поможет вам с задачами на любом языке программирования.
oɔɯǝɹ 02
См. Здесь несколько примеров (хотя Google тоже поможет) eskimo.com/~scs/cclass/notes/sx10a.html
oɔɯǝɹ 02
7

Эта проблема несколько сбивает с толку, когда вы начинаете изучать C.

Вот основные принципы, которые могут помочь вам начать работу:

  1. В C всего несколько основных типов:

    • char: целочисленное значение размером 1 байт.

    • short: целочисленное значение размером 2 байта.

    • long: целочисленное значение размером 4 байта.

    • long long: целочисленное значение размером 8 байт.

    • float: нецелое значение размером 4 байта.

    • double: нецелое значение размером 8 байт.

    Обратите внимание, что размер каждого типа обычно определяется компилятором, а не стандартом.

    Целочисленные типы short, longи long long, как правило , следуют int.

    Однако это не обязательно, и вы можете использовать их без int.

    В качестве альтернативы вы можете просто указать int, но это может быть по-разному интерпретировано разными компиляторами.

    Итак, чтобы резюмировать это:

    • shortто же самое, short intно не обязательно то же самое, что и int.

    • longто же самое, long intно не обязательно то же самое, что и int.

    • long longто же самое, long long intно не обязательно то же самое, что и int.

    • В данном компиляторе intэто либо short intили, long intлибо long long int.

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

    Например:

    int a;

    int* b = &a;

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

    Например: shortи short*.

    Есть два способа "взглянуть" на переменную b (что, вероятно, смущает большинство новичков) :

    • Вы можете рассматривать bкак переменную типа int*.

    • Вы можете рассматривать *bкак переменную типа int.

    Следовательно, одни люди будут заявлять int* b, тогда как другие заявляют int *b.

    Но в том-то и дело, что эти два объявления идентичны (пробелы не имеют смысла).

    Вы можете использовать либо bкак указатель на целочисленное значение, либо *bкак фактическое указанное целочисленное значение.

    Вы можете получить (прочитать) заостренное значение: int c = *b.

    И вы можете установить (запись) заостренное значение: *b = 5.

  3. Указатель может указывать на любой адрес памяти, а не только на адрес некоторой переменной, которую вы ранее объявили. Однако вы должны быть осторожны при использовании указателей, чтобы получить или установить значение, расположенное по указанному адресу памяти.

    Например:

    int* a = (int*)0x8000000;

    Здесь у нас есть переменная, aуказывающая на адрес памяти 0x8000000.

    Если этот адрес памяти не отображается в пространстве памяти вашей программы, то любая операция чтения или записи, использующая *a, скорее всего, приведет к сбою вашей программы из-за нарушения доступа к памяти.

    Вы можете безопасно изменять значение a, но вы должны быть очень осторожны при изменении значения *a.

  4. Тип void*является исключительным в том смысле, что он не имеет соответствующего «типа значения», который можно использовать (т. Е. Вы не можете объявить void a). Этот тип используется только как общий указатель на адрес памяти, без указания типа данных, которые находятся в этом адресе.

Барак Манос
источник
7

Возможно, пройдя через это еще немного, станет легче:

#include <stdio.h>

int main()
{
    int foo = 1;
    int *bar = &foo;
    printf("%i\n", foo);
    printf("%p\n", &foo);
    printf("%p\n", (void *)&foo);
    printf("%p\n", &bar);
    printf("%p\n", bar);
    printf("%i\n", *bar);
    return 0;
}

Попросите их сказать вам, что они ожидают от каждой строки, а затем пусть они запустят программу и посмотрят, что получится. Объясните их вопросы (голая версия наверняка подскажет некоторые, но о стиле, строгости и переносимости вы можете побеспокоиться позже). Затем, прежде чем их разум превратится в кашу от чрезмерных размышлений или они превратятся в зомби после обеда, напишите функцию, которая принимает значение, и ту же функцию, которая принимает указатель.

По моему опыту, можно преодолеть вопрос «почему это печатается именно так?» hump, а затем сразу же показывая, почему это полезно в параметрах функций, на практике (в качестве прелюдии к некоторым базовым материалам K&R, таким как синтаксический анализ строк / обработка массивов), что делает урок не просто осмысленным, но и закрепленным.

Следующий шаг - попросить их объяснить вам, как i[0]относится к &i. Если они могут это сделать, они не забудут об этом, и вы можете начать говорить о структурах, даже немного раньше времени, просто чтобы это стало понятным.

Вышеупомянутые рекомендации относительно прямоугольников и стрелок тоже хороши, но они также могут привести к полному обсуждению того, как работает память - это разговор, который должен произойти в какой-то момент, но может отвлечь от основной темы. : как интерпретировать обозначение указателя в C.

zxq9
источник
Это хорошее упражнение. Но проблема, которую я хотел поднять, - это специфическая синтаксическая проблема, которая может повлиять на ментальную модель, которую строят студенты. Рассмотрим это: int foo = 1;. Теперь это нормально: int *bar; *bar = foo;. Это не нормально:int *bar = foo;
armin
1
@abw Единственное, что имеет смысл, - это то, что студенты говорят сами себе. Это означает «увидеть один, сделать один, научить». Вы не можете защитить или предсказать, какой синтаксис или стиль они увидят в джунглях (даже в ваших старых репозиториях!), Поэтому вы должны показать достаточно перестановок, чтобы основные концепции понимались независимо от стиля - и затем начните объяснять им, почему были выбраны определенные стили. Как обучение английскому языку: основные выражения, идиомы, стили, определенные стили в определенном контексте. К сожалению, непросто. В любом случае удачи!
zxq9
6

Тип выражения *bar является int; таким образом, тип переменной (и выражения) barравен int *. Поскольку переменная имеет тип указателя, ее инициализатор также должен иметь тип указателя.

Существует несоответствие между инициализацией и назначением переменной-указателя; это просто то, что нужно усвоить на собственном горьком опыте.

Джон Боде
источник
3
Глядя на ответы здесь, я чувствую, что многие опытные программисты даже не видят проблему . Я предполагаю, что это побочный продукт «учиться жить с противоречиями».
armin
3
@abw: правила инициализации отличаются от правил присваивания; для скалярных арифметических типов различия незначительны, но имеют значение для типов указателей и агрегатов. Это то, что вам нужно будет объяснить вместе со всем остальным.
Джон Боде
5

Я бы предпочел читать это как первое, *относящееся к intболее чем bar.

int  foo = 1;           // foo is an integer (int) with the value 1
int* bar = &foo;        // bar is a pointer on an integer (int*). it points on foo. 
                        // bar value is foo address
                        // *bar value is foo value = 1

printf("%p\n", &foo);   // print the address of foo
printf("%p\n", bar);    // print the address of foo
printf("%i\n", foo);    // print foo value
printf("%i\n", *bar);   // print foo value
Grorel
источник
2
Затем вы должны объяснить, почему int* a, bне делает то, что думают.
Pharap
4
Верно, но я не думаю, что это int* a,bвообще следует использовать. Для лучшей наглядности, обновления и т. Д. В каждой строке должно быть только одно объявление переменной и никогда больше. Новичкам тоже есть что объяснять, даже если компилятор справится с этим.
Grorel
Но это мнение одного человека. Есть миллионы программистов, которые совершенно нормально объявляют более одной переменной в строке и делают это ежедневно в рамках своей работы. Вы не можете скрыть студентов от альтернативных способов ведения дел, лучше показать им все альтернативы и позволить им решать, в каком направлении они хотят что-то делать, потому что, если они когда-либо будут работать, от них ожидается, что они будут следовать определенному стилю, им может быть комфортно, а может и нет. Для программиста универсальность - очень хорошее качество.
Pharap
1
Я согласен с @grorel. Проще думать *как о типе и просто отговаривать int* a, b. Если вы не предпочитаете говорить, что *aэто тип, intа не aуказатель на int...
Кевин Уши
@grorel прав: int *a, b;не следует использовать. Объявление двух переменных с разными типами в одном операторе - довольно плохая практика и хороший кандидат для проблем с обслуживанием в будущем. Возможно, для тех из нас, кто работает во встраиваемой области, все по-другому, где an int*и an intчасто имеют разные размеры и иногда хранятся в совершенно разных местах памяти. Это один из многих аспектов языка C, который лучше всего преподавать как «это разрешено, но не делай этого».
Пирог злой собаки
5
int *bar = &foo;

Question 1: Что есть bar?

Ans: Это переменная-указатель (на тип int). Указатель должен указывать на какое-то допустимое место в памяти, а позже его следует разыменовать (* bar) с помощью унарного оператора *, чтобы прочитать значение, хранящееся в этом месте.

Question 2: Что есть &foo?

Ans: foo - это переменная типа., intкоторая хранится в некоторой допустимой области памяти, и это место мы получаем от оператора, &поэтому теперь у нас есть некоторая допустимая область памяти &foo.

Итак, оба собраны вместе, то есть то, что нужно указателю, было действительным местом в памяти, и оно получено, &fooпоэтому инициализация хорошая.

Теперь указатель barуказывает на допустимое место в памяти, и значение, хранящееся в нем, можно получить, разыменовав его, т.е.*bar

Гопи
источник
5

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

Ёнкил Квон
источник
4

Я думаю, что дьявол в космосе.

Я бы написал (не только для новичка, но и для себя): int * bar = & foo; вместо int * bar = & foo;

это должно показать, какова взаимосвязь между синтаксисом и семантикой

rpaulin56
источник
4

Уже отмечалось, что * имеет несколько ролей.

Есть еще одна простая идея, которая может помочь новичку понять суть:

Подумайте, что "=" также имеет несколько ролей.

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

Когда ты видишь:

int *bar = &foo;

Думаю, это почти эквивалентно:

int *bar(&foo);

Круглые скобки имеют приоритет над звездочкой, поэтому «& foo» гораздо легче интуитивно отнести к «bar», чем к «* bar».

морфизм
источник
4

Я видел этот вопрос несколько дней назад, а затем случайно прочитал объяснение объявления типа Go в блоге Go . Он начинается с описания объявлений типа C, которые кажутся полезным ресурсом для добавления в этот поток, хотя я думаю, что уже даны более полные ответы.

C использовал необычный и умный подход к синтаксису объявления. Вместо описания типов с помощью специального синтаксиса пишется выражение, включающее объявляемый элемент, и указывается, какой тип будет иметь это выражение. Таким образом

int x;

объявляет x как int: выражение 'x' будет иметь тип int. В общем, чтобы выяснить, как записать тип новой переменной, напишите выражение, включающее эту переменную, которая оценивается как базовый тип, затем поместите базовый тип слева, а выражение - справа.

Таким образом, декларации

int *p;
int a[3];

указать, что p является указателем на int, потому что '* p' имеет тип int, и что a является массивом целых чисел, потому что a [3] (игнорируя конкретное значение индекса, которое обозначается как размер массива) имеет тип внутр.

(Далее описывается, как распространить это понимание на указатели функций и т. Д.)

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

Энди Тернер
источник
3

Если проблема в синтаксисе, может быть полезно показать эквивалентный код с помощью шаблона / using.

template<typename T>
using ptr = T*;

Затем это можно использовать как

ptr<int> bar = &foo;

После этого сравните нормальный синтаксис / C с подходом только для C ++. Это также полезно для объяснения константных указателей.

MI3Guy
источник
2
Для новичков это будет намного сложнее.
Карстен
Я думал, что вы не покажете определение ptr. Просто используйте его для объявления указателей.
MI3Guy
3

Источник путаницы проистекает из того факта, что *символ может иметь разные значения в C, в зависимости от факта, в котором он используется. Чтобы объяснить указатель новичку, *следует пояснить значение символа в другом контексте.

В декларации

int *bar = &foo;  

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

*bar = &foo;

было бы неправильно, поскольку он присваивает адрес fooобъекту, на который barуказывает, а не самому barсебе.

хаки
источник
3

"возможно, если написать его как int * bar, станет более очевидным, что звезда на самом деле является частью типа, а не идентификатора". Так и делаю. И я говорю, что это что-то вроде Type, но только для одного имени указателя.

«Конечно, вы столкнетесь с разными проблемами, связанными с такими неинтуитивными вещами, как int * a, b».

Павел Бивойно
источник
2

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

Когда ты пишешь

int *bar = &foo;

компилятор группирует, как

{ int * } bar = &foo;

То есть: вот новая переменная, ее имя bar, тип - указатель на int и ее начальное значение &foo.

И вы должны добавить: в =выше , означает инициализацию не притворство, а в следующих выражениях *bar = 2;она является притворством

Редактировать за комментарий:

Осторожно: в случае многократного объявления это *относится только к следующей переменной:

int *bar = &foo, b = 2;

bar - это указатель на int, инициализированный адресом foo, b - это int, инициализированный значением 2, а в

int *bar=&foo, **p = &bar;

bar в неподвижном указателе на int, а p - указатель на указатель на int, инициализированный адресом или строкой.

Серж Бальеста
источник
2
На самом деле компилятор не группирует это так: int* a, b;объявляет a как указатель на int, а b как int. Этот *символ имеет два различных значения: в объявлении он указывает тип указателя, а в выражении - это унарный оператор разыменования.
tmlen
@tmlen: Я имел в виду, что при инициализации *in rattached к типу, так что указатель инициализируется, тогда как в процессе воздействия затрагивается указанное значение. Но, по крайней мере, ты дал мне хорошую шляпу :-)
Серж Баллеста
0

По сути, указатель не является индикатором массива. Новичку легко понять, что указатель выглядит как массив. большинство строковых примеров с использованием

"char * pstr" похоже, похоже

"char str [80]"

Но, что важно, указатель обрабатывается как просто целое число на нижнем уровне компилятора.

Посмотрим примеры:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv, char **env)
{
    char str[] = "This is Pointer examples!"; // if we assume str[] is located in 0x80001000 address

    char *pstr0 = str;   // or this will be using with
    // or
    char *pstr1 = &str[0];

    unsigned int straddr = (unsigned int)pstr0;

    printf("Pointer examples: pstr0 = %08x\n", pstr0);
    printf("Pointer examples: &str[0] = %08x\n", &str[0]);
    printf("Pointer examples: str = %08x\n", str);
    printf("Pointer examples: straddr = %08x\n", straddr);
    printf("Pointer examples: str[0] = %c\n", str[0]);

    return 0;
}

Результатам будет это 0x2a6b7ed0 - адрес str []

~/work/test_c_code$ ./testptr
Pointer examples: pstr0 = 2a6b7ed0
Pointer examples: &str[0] = 2a6b7ed0
Pointer examples: str = 2a6b7ed0
Pointer examples: straddr = 2a6b7ed0
Pointer examples: str[0] = T

Итак, в принципе, имейте в виду, что указатель - это своего рода целое число. представление Обращения.

cpplover - Slw Essencial
источник
-1

Я бы объяснил, что целые числа являются объектами, как и числа с плавающей запятой и т. Д. Указатель - это тип объекта, значение которого представляет адрес в памяти (поэтому указатель по умолчанию имеет значение NULL).

Когда вы впервые объявляете указатель, вы используете синтаксис имени указателя типа. Он читается как «целочисленный указатель с именем, которое может указывать на адрес любого целочисленного объекта». Мы используем этот синтаксис только во время удаления, аналогично тому, как мы объявляем int как int num1, но мы используем только num1, когда мы хотим использовать эту переменную, а не int num1.

int x = 5; // целочисленный объект со значением 5

int * ptr; // целое число со значением NULL по умолчанию

Чтобы указатель указывал на адрес объекта, мы используем символ «&», который можно прочитать как «адрес».

ptr = & x; // теперь значение - это адрес 'x'

Поскольку указатель - это только адрес объекта, чтобы получить фактическое значение, хранящееся по этому адресу, мы должны использовать символ «*», который при использовании перед указателем означает «значение по адресу, на который указывает».

std :: cout << * ptr; // распечатать значение по адресу

Вы можете кратко объяснить, что " " - это "оператор", который возвращает разные результаты с разными типами объектов. При использовании с указателем оператор ' ' больше не означает "умножить на".

Это помогает нарисовать диаграмму, показывающую, как переменная имеет имя и значение, а указатель имеет адрес (имя) и значение, и показывает, что значением указателя будет адрес типа int.

user2796283
источник
-1

Указатель - это просто переменная, используемая для хранения адресов.

Память в компьютере состоит из байтов (байт состоит из 8 бит), расположенных последовательно. Каждый байт имеет номер, связанный с ним так же, как индекс или индекс в массиве, который называется адресом байта. Адрес байта начинается с 0 на единицу меньше размера памяти. Например, скажем, в 64 МБ ОЗУ 64 * 2 ^ 20 = 67108864 байта. Следовательно, адрес этих байтов будет начинаться с 0 до 67108863.

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

Посмотрим, что произойдет, когда вы объявите переменную.

int mark;

Как мы знаем, int занимает 4 байта данных (при условии, что мы используем 32-разрядный компилятор), поэтому компилятор резервирует 4 последовательных байта из памяти для хранения целочисленного значения. Адрес первого байта из 4 выделенных байтов известен как адрес меток переменных. Предположим, что адрес четырех последовательных байтов - 5004, 5005, 5006 и 5007, тогда адрес переменных меток будет 5004. введите описание изображения здесь

Объявление переменных-указателей

Как уже было сказано, указатель - это переменная, в которой хранится адрес памяти. Как и любые другие переменные, вам нужно сначала объявить переменную-указатель, прежде чем вы сможете ее использовать. Вот как вы можете объявить переменную-указатель.

Синтаксис: data_type *pointer_name;

data_type - это тип указателя (также известный как базовый тип указателя). pointer_name - это имя переменной, которое может быть любым допустимым идентификатором C.

Возьмем несколько примеров:

int *ip;

float *fp;

int * ip означает, что ip - это переменная-указатель, способная указывать на переменные типа int. Другими словами, указатель на переменную ip может хранить адрес переменных только типа int. Точно так же указатель переменной fp может хранить только адрес переменной типа float. Тип переменной (также известный как базовый тип) ip - указатель на int, а тип fp - указатель на float. Переменная-указатель типа указатель на int может быть символически представлена ​​как (int *). Точно так же указатель переменной типа указатель на float может быть представлен как (float *)

После объявления переменной-указателя следующим шагом является присвоение ей действительного адреса памяти. Вы никогда не должны использовать переменную-указатель без присвоения ей действительного адреса памяти, потому что сразу после объявления она содержит значение мусора и может указывать на любое место в памяти. Использование неназначенного указателя может дать непредсказуемый результат. Это может даже привести к сбою программы.

int *ip, i = 10;
float *fp, f = 12.2;

ip = &i;
fp = &f;

Источник: thecguru - это, безусловно, самое простое, но подробное объяснение, которое я когда-либо находил.

Коди
источник