Почему звездочка перед именем переменной, а не после типа?

187

Почему большинство программистов на C называют переменные так:

int *myVariable;

а не так:

int* myVariable;

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

WBlasko
источник
1
Второй стиль кажется более интуитивным в целом, но первый - это способ избежать ошибок, связанных с типами, в коде. Если вы действительно привязаны к последнему стилю, вы всегда можете пойти с ним typedefs, но это добавит ненужную сложность, ИМХО.
Облако,

Ответы:

251

Они ТОЧНО эквивалентны. Однако в

int *myVariable, myVariable2;

Кажется очевидным, что myVariable имеет тип int * , а myVariable2 имеет тип int . В

int* myVariable, myVariable2;

может показаться очевидным, что оба имеют тип int * , но это неверно, так как myVariable2имеет тип int .

Поэтому первый стиль программирования более интуитивен.

luiscubal
источник
135
возможно, но я бы не стал смешивать и сопоставлять типы в одной декларации.
BobbyShaftoe
15
@BobbyShaftoe Согласен. Даже после прочтения каждого аргумента здесь я придерживаюсь int* someVarличных проектов. Это имеет больше смысла.
Алисса Гарольдсен
30
@Kupiakos Это имеет больше смысла, пока вы не изучите синтаксис объявления C, основанный на «объявлениях после использования». Объявления используют точно такой же синтаксис, что и переменные одного типа. Когда вы объявляете массив целых чисел, это не выглядит так: int[10] x. Это просто не синтаксис Си. Грамматика явно разбирается как:, int (*x)а не как (int *) x, поэтому размещение звездочки слева просто вводит в заблуждение и основано на неправильном понимании синтаксиса объявления Си.
Пикер
8
Исправление: поэтому вы никогда не должны объявлять более одной переменной в одной строке. В общем, вы не должны мотивировать определенный стиль кодирования, основанный на каком-то другом не связанном, плохом и опасном стиле кодирования.
Лундин
6
Вот почему я придерживаюсь одной переменной на объявление указателя. Там нет никакой путаницы, если вы делаете int* myVariable; int myVariable2;вместо этого.
Патрик Робертс
122

Если вы посмотрите на это по-другому, *myVariableэто тип int, который имеет некоторый смысл.

Биоцинк
источник
6
Это моё любимое объяснение, и оно работает хорошо, потому что оно объясняет причуды объявления C в целом - даже отвратительный и грубый синтаксис указателя на функцию.
Бенджамин Поллак
12
Это довольно аккуратно, так как вы можете себе представить, что нет никаких реальных типов указателей. Есть только переменные, которые при соответствующей ссылке или разыменовании дают вам один из примитивных типов.
биоцинк
1
На самом деле, * myVariable может иметь тип NULL. Что еще хуже, это может быть случайное место в памяти.
qonf
4
qonf: NULL не является типом. myVariableможет быть NULL, в этом случае *myVariableвызывает ошибку сегментации, но нет типа NULL.
Даниэль Ретлисбергер
28
Этот момент может вводить в заблуждение в таком контексте: int x = 5; int *pointer = &x;потому что он предполагает, что мы устанавливаем для int *pointerзначение, а не pointerсамо значение.
rafalcieslak
40

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

int* varA, varB; // This is misleading

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

int* varA;
int varB;

Трудно установить баланс между чистым кодом и кратким кодом - дюжина избыточных строк int a;тоже не годится. Тем не менее, я по умолчанию одно объявление на строку и беспокоюсь о комбинировании кода позже.

ojrac
источник
2
Что ж, вводящий в заблуждение первый пример - это ошибка дизайна. Если бы я мог, я бы полностью удалил этот способ объявления из C и сделал так, чтобы оба типа int *.
Адам Баджгер
1
«* более тесно связан с переменной, чем с типом». Это наивный аргумент. Посмотрим int *const a, b;. Где * "связать"? Тип ais int* const, так как вы можете сказать, что * принадлежит переменной, когда она является частью самого типа?
Лундин
1
Конкретный вопрос или охватывающий все случаи: выберите один. Я сделаю заметку, но это еще один хороший аргумент для моего последнего предложения: одна декларация на строку уменьшает возможность испортить это.
ojrac
36

Что-то еще никто не упомянул здесь, это то, что эта звездочка на самом деле является « оператором разыменования » в C.

*a = 10;

Линия выше не означает , что я хочу , чтобы назначить 10на aэто означает , что я хочу , чтобы назначить 10в любое место памяти aуказывает. И я никогда не видел, чтобы кто-нибудь писал

* a = 10;

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

x = a * b * c * d
  * e * f * g;

Это *eмогло бы ввести в заблуждение, не так ли?

Хорошо, теперь, что на самом деле означает следующая строка:

int *a;

Большинство людей скажут:

Это означает, что aэто указатель на intзначение.

Это технически правильно, большинству людей нравится видеть / читать его таким образом, и именно так его определяют современные стандарты C (обратите внимание, что язык C сам по себе предшествует всем стандартам ANSI и ISO). Но это не единственный способ взглянуть на это. Вы также можете прочитать эту строку следующим образом:

Значение разыменования aимеет тип int.

Таким образом, на самом деле звездочка в этом объявлении также может рассматриваться как оператор разыменования, что также объясняет его размещение. И этот aуказатель на самом деле не объявлен вообще, это подразумевается тем фактом, что единственное, что вы можете разыменовать, это указатель.

Стандарт C определяет только два значения для *оператора:

  • оператор косвенного обращения
  • оператор умножения

И косвенность - это просто одно значение, нет никакого дополнительного значения для объявления указателя, есть просто косвенность, то есть то, что делает операция разыменования, она выполняет косвенный доступ, так же как и внутри оператора, int *a;такого как косвенный доступ ( *означает косвенный доступ) и, следовательно, второе утверждение, приведенное выше, намного ближе к стандарту, чем первое.

Mecki
источник
6
Спасибо, что спасли меня от написания еще одного ответа здесь. Кстати, я обычно читаю что- int a, *b, (*c)();то вроде «объявляю следующие объекты как int: объект a, объект , на который указывает b, и объект, возвращенный из функции, на которую указывает c».
Антти Хаапала
1
Оператор *in int *a;не является оператором и не разыменовывается a(что даже не определено)
MM
1
@MM Пожалуйста, назовите страницу и номер строки любого стандарта ISO C, где этот стандарт говорит, что звездочка может быть чем-то иным, кроме умножения или косвенного обращения. Он показывает «объявление указателя» только в качестве примера, нигде не определяет третьего значения для звездочки (и не определяет никакого значения, которое не будет оператором). О, я нигде не утверждал, что что-то "определено", ты это выдумал. Или, как сказал Джонатан Леффлер, в стандарте C * всегда является «грамматикой», это не часть перечисленных спецификаторов объявлений (поэтому это не часть объявления, поэтому он должен быть оператором)
Mecki
1
@MM 6.7.6.1 показывает только декларацию , например, так же , как я уже сказал, это не дает никакого смысла , чтобы *(это дает лишь значение в суммарном выражении, а не в *пределах выражения!). Там написано, что "int a;" объявляет указатель, что он делает, никогда не заявлял иначе, но без значения *, читаемого как * значение разыменованного aявляется int , все еще полностью допустимо, поскольку это имеет тот же фактический смысл. Ничто, на самом деле ничто из написанного в 6.7.6.1 не противоречит этому утверждению.
Меки
2
Там нет "общего выражения". int *a;это декларация, а не выражение. aне разыменовывается int *a;. aдаже не существует в тот момент, *когда обрабатывается. Как вы думаете int *a = NULL;, это ошибка, потому что она разыменовывает нулевой указатель?
ММ
17

Я собираюсь выйти на конечности здесь и сказать , что есть прямой ответ на этот вопрос , как для объявления переменных и параметров и возвращаемых типов, которые в том , что звездочка должна идти рядом с названием: int *myVariable;. Чтобы понять почему, посмотрите, как вы объявляете другие типы символов в C:

int my_function(int arg); для функции;

float my_array[3] для массива.

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

int a_return_value = my_function(729);

float an_element = my_array[2];

и: int copy_of_value = *myVariable;.

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

Injektilo
источник
12

Это просто вопрос предпочтений.

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

Я предпочитаю объявлять указатели с соответствующими знаками рядом с именем типа, например

int* pMyPointer;
macbirdie
источник
3
Вопрос о С, где нет ссылок.
Антти Хаапала
Спасибо за указание на это, хотя вопрос был не об указателях или ссылках, а о форматировании кода, в основном.
Macbirdie
8

Один великий гуру однажды сказал: «Прочтите это по пути компилятора».

http://www.drdobbs.com/conversationsa-midsummer-nights-madness/184403835

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

Компилятор читает это как:

int (*a);

не как:

(int*) a;

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

int* a[10];

-- Редактировать --

Чтобы точно объяснить, что я имею в виду, когда я говорю, что он анализируется как int (*a), это означает, что он *более тесно связан с aчем int, во многом так, как это выражается в выражении 4 + 3 * 7 3более тесно, 7чем с ним 4из-за более высокого приоритета *.

С извинениями за ascii art, краткий обзор AST для разбора int *aвыглядит примерно так:

      Declaration
      /         \
     /           \
Declaration-      Init-
Secifiers       Declarator-
    |             List
    |               |
    |              ...
  "int"             |
                Declarator
                /       \
               /        ...
           Pointer        \
              |        Identifier
              |            |
             "*"           |
                          "a"

Как ясно показано, *связывается более тесно aс их общим предком Declarator, в то время как вам нужно пройти весь путь вверх по дереву, Declarationчтобы найти общего предка, который включает в себя int.

dgnuff
источник
Нет, компилятор определенно читает тип как (int*) a.
Лундин
@Lundin Это большое недоразумение, которое есть у большинства современных программистов на C ++. Разбор объявления переменной происходит примерно так. Шаг 1. Прочитайте первый токен, который становится «базовым типом» объявления. intв таком случае. Шаг 2. Прочитайте декларацию, включая любые украшения типа. *aв таком случае. Шаг 3 Прочитайте следующий символ. Если запятая, уничтожьте ее и вернитесь к шагу 2. Если точка с запятой остановится Если что-нибудь еще, сгенерируйте синтаксическую ошибку. ...
dgnuff
@Lundin ... Если парсер прочитает его так, как вы предлагаете, то мы сможем написать int* a, b;и получить пару указателей. Я хочу подчеркнуть, что *связывается с переменной и анализируется с ней, а не с типом, чтобы сформировать «базовый тип» объявления. Это также одна из причин того, что typedefs были введены для typedef int *iptr; iptr a, b;создания пары указателей. Используя typedef, вы можете привязать *к int.
dgnuff
1
... Конечно, компилятор "комбинирует" базовый тип из Declaration-Specifierс "украшениями" в Declaratorдля получения окончательного типа для каждой переменной. Однако это не «MOVE» декорации к спецификатор декларации в противном случае int a[10], b;будет производить совершенно нелепые результаты, он разбирает , int *a, b[10];как int *a , b[10] ;. Нет другого способа описать это, что имеет смысл.
dgnuff
1
Да, важная часть здесь на самом деле не синтаксис или порядок парсинга компилятора, а то, что тип int*aв конце читается компилятором как: « aимеет тип int*». Это было то, что я имею в виду с моим оригинальным комментарием.
Лундин
3

Потому что это имеет больше смысла, когда у вас есть такие объявления, как:

int *a, *b;
Грег Роджерс
источник
5
Это буквально пример умолять о вопросе. Нет, в этом нет больше смысла. "int * a, b" также может сделать оба указателя.
MichaelGG
1
@MichaelGG У тебя хвост, виляющий собакой. Конечно, K & R мог бы указать, что int* a, b;сделал bуказатель на int. Но они этого не сделали. И не без причины. Под вашей предложенной системой, какой тип bв следующей декларации int* a[10], b;:?
dgnuff
2

Для объявления нескольких указателей в одной строке я предпочитаю, чтобы int* a, * b;более интуитивно объявлять «a» как указатель на целое число и не смешивать стили, когда аналогично объявляется «b». Как кто-то сказал, я бы в любом случае не объявлял два разных типа в одном и том же утверждении.

heyitsluke
источник
2

Люди, которые предпочитают int* x;, пытаются втиснуть свой код в вымышленный мир, где тип находится слева, а идентификатор (имя) - справа.

Я говорю «вымышленный», потому что:

В C и C ++ в общем случае объявленный идентификатор окружен информацией о типе.

Это может звучать безумно, но вы знаете, что это правда. Вот некоторые примеры:

  • int main(int argc, char *argv[])означает « mainэто функция, которая принимает intмассив и указателей на charи возвращает int». Другими словами, большая часть информации о типе находится справа. Некоторые люди думают, что объявления функций не учитываются, потому что они как-то «особенные». Хорошо, давайте попробуем переменную.

  • void (*fn)(int)означает fn, что указатель на функцию, которая принимает intи ничего не возвращает.

  • int a[10]объявляет 'a' как массив из 10 intс.

  • pixel bitmap[height][width],

  • Понятно, что у меня есть несколько примеров, которые содержат много информации о типах, чтобы понять мою точку зрения. Есть много объявлений, где большинство - если не все - типа слева, например struct { int x; int y; } center.

Этот синтаксис декларации вырос из желания K & R иметь декларации, отражающие использование. Чтение простых объявлений интуитивно понятно, а чтение более сложных можно освоить, изучая правило справа налево и право (иногда называют спиральным правилом или просто правилом справа налево).

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

В C ++ синтаксис стал немного более сложным (с классами, ссылками, шаблонами, перечисляемыми классами), и, как реакция на эту сложность, вы увидите больше усилий по отделению типа от идентификатора во многих объявлениях. Другими словами, вы можете увидеть больше int* pобъявлений -style, если вы проверяете большую часть кода C ++.

В любом языке вы всегда можете иметь тип в левой части объявлений переменных : (1) никогда не объявлять несколько переменных в одном выражении и (2) использовать typedefs (или объявления псевдонимов, которые, как ни странно, ставят псевдоним идентификаторы слева от типов). Например:

typedef int array_of_10_ints[10];
array_of_10_ints a;
Адриан Маккарти
источник
Небольшой вопрос, почему void (* fn) (int) не может означать, что «fn - это функция, которая принимает int и возвращает (void *)?»
Сохам Донгаргаонкар,
1
@ a3y3: потому что в скобках хранится (*fn)указатель, fnа не возвращаемый тип.
Эдриан Маккарти
Понял. Я думаю, что это достаточно важно, чтобы быть добавленным в ваш ответ, я скоро предложу изменить.
Сохам Донгаргаонкар
1
Я думаю, что загромождает ответ, не добавляя большую ценность. Это просто синтаксис языка. Большинство людей, спрашивающих о том, почему такой синтаксис, вероятно, уже знают правила. Любой, кто смущен в этом вопросе, может увидеть разъяснения в этих комментариях.
Адриан Маккарти
1

Я уже ответил на аналогичный вопрос в CP, и, поскольку никто не упомянул об этом, здесь я также должен указать, что C - это язык свободного формата , какой бы стиль вы ни выбрали, он подходит, в то время как анализатор может различать каждый токен. Эта особенность С приводит к особому типу соревнований, называемых С обфускацией .

Frankie_C
источник
1

Многие аргументы в этой теме носят субъективный характер, а аргумент о «звезде, связанной с именем переменной» наивен. Вот несколько аргументов, которые не просто мнения:


Забытые указатели типа указателя

Формально «звезда» не принадлежит ни типу, ни имени переменной, она является частью своего грамматического элемента с именем указатель . Формальный синтаксис C (ISO 9899: 2018):

(6.7) декларация:
Декларация спецификаторы Init-описатель-лист опт ;

Где спецификатор объявления содержит тип (и хранилище), а список инициаторов объявления содержит указатель и имя переменной. Что мы увидим, если мы разберем синтаксис этого списка объявлений дальше:

(6.7.6) описатель:
указатель неавтоматического прямого описатель
...
(6.7.6) указатель:
* тип-спецификатор-список неавтоматического
* типа отборочные-лист неавтоматического указатель

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

То, что делает различные аргументы стиля о том, что «звезда принадлежит переменной» несовместимо, так это то, что они забыли об этих квалификаторах типа указателя. int* const x, int *const xИли int*const x?

Рассмотрим int *const a, b;, какие бывают типы aи b? Не так очевидно, что «звезда принадлежит переменной» больше. Скорее, можно было бы задуматься о том, где он constпринадлежит.

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

Список спецификаторов типов для указателя может вызвать проблемы у тех, кто использует int *aстиль. Те, кто использует указатели внутри typedef(что мы не должны делать, очень плохая практика!) И думают, что «звезда принадлежит имени переменной», как правило, пишут эту очень тонкую ошибку:

    /*** bad code, don't do this ***/
    typedef int *bad_idea_t; 
    ...
    void func (const bad_idea_t *foo);

Это компилируется чисто. Теперь вы можете подумать, что код сделан правильно. Не так! Этот код случайно является фальшивой константностью.

Тип на fooсамом деле int*const*- самый внешний указатель был сделан только для чтения, а не указывал на данные. Таким образом, внутри этой функции мы можем сделать, **foo = n;и она изменит значение переменной в вызывающей стороне.

Это происходит потому , что в выражении const bad_idea_t *foo, то *не принадлежит к имени переменной здесь! В псевдокоде это объявление параметра следует читать как, const (bad_idea_t *) fooа не как (const bad_idea_t) *foo. В этом случае звезда относится к скрытому типу указателя - тип является указателем, а указатель с константой записывается как *const.

Но тогда корень проблемы в приведенном выше примере - это практика скрывать указатели за, typedefа не *стиль.


Относительно объявления нескольких переменных в одной строке

Объявление нескольких переменных в одной строке широко признано плохой практикой 1) . CERT-C подытоживает это так:

DCL04-С. Не объявляйте более одной переменной на объявление

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

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

Так что спор о том, что программист запутался, int* a, bплохой. Корень проблемы - использование нескольких деклараторов, а не размещение *. Независимо от стиля, вы должны написать это:

    int* a; // or int *a
    int b;

Другим здравым, но субъективным аргументом было бы то, что данный int* aтип не aвызывает сомнений, int*и поэтому звезда относится к определителю типа.

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


1) CERT-C DCL04-C .

Лундин
источник
Скорее забавно, если вы прочитаете ту статью, которую я связал, есть короткий раздел на тему: « typedef int *bad_idea_t; void func(const bad_idea_t bar); Как великий пророк Дэн Сакс учит:« Если вы всегда располагаете constкак можно дальше вправо, не меняя смысловой смысл », это полностью прекращается. быть проблемой. Это также делает ваши constобъявления более последовательными для чтения. «Все справа от слова const - это то, что является const, все слева - это его тип». Это будет применяться ко всем константам в декларации. Попробуйте это сint const * * const x;
dgnuff
-1

Рассматривать

int *x = new int();

Это не переводит на

int *x;
*x = new int();

Это на самом деле переводится как

int *x;
x = new int();

Это делает int *xобозначения несколько непоследовательными.

Уркварт
источник
newявляется оператором C ++ Его вопрос помечен как «C», а не «C ++».
Sapphire_Brick