Чому зірочка перед назвою змінної, а не після типу?


187

Чому більшість програмістів на C називають такі змінні:

int *myVariable;

а не так:

int* myVariable;

Обидва дійсні. Мені здається, що зірочка - це частина типу, а не частина назви змінної. Хтось може пояснити цю логіку?


1
Другий стиль загалом здається більш інтуїтивним, але перший - це спосіб уникнути помилок у коді, пов'язаних з типом. Якщо ви дійсно прив’язані до останнього стилю, ви завжди можете піти з цим typedefs, але це додасть зайвої складності, IMHO.
Хмара

Відповіді:


250

Вони САМО еквівалентні. Однак в

int *myVariable, myVariable2;

Здається очевидним, що myVariable має тип int * , тоді як myVariable2 має тип int . В

int* myVariable, myVariable2;

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

Тому перший стиль програмування більш інтуїтивний.


135
можливо, але я б не змішував і не відповідав типам в одній декларації.
BobbyShaftoe

15
@BobbyShaftoe Погодився. Навіть прочитавши тут кожен аргумент, я дотримуюся int* someVarособистих проектів. Це має більше сенсу.
Алісса Гарольдсен

30
@Kupiakos Це має більше сенсу, поки ви не дізнаєтесь синтаксис декларації C на основі "декларацій слід використовувати". Декларації використовують той самий синтаксис, який використовують однотипні змінні. Коли ви оголошуєте масив цілих чисел, це не виглядає так: int[10] x. Це просто не синтаксис С. Граматика явно аналізує як:, int (*x)а не як (int *) x, тому розміщення зірочки зліва просто вводить в оману і ґрунтується на нерозумінні синтаксису декларації C.
Пікер

8
Виправлення: отже, ви ніколи не повинні оголошувати більше однієї змінної в одному рядку. Взагалі, ви не повинні мотивувати певний стиль кодування, заснований на іншому непов'язаному, поганому та небезпечному стилі кодування.
Лундін

6
Ось чому я дотримуюся однієї змінної на кожне оголошення вказівника. Немає ніякої плутанини, якщо ви зробите це int* myVariable; int myVariable2;замість цього.
Патрік Робертс

122

Якщо ви подивитесь на це по-іншому, *myVariableмає тип int, який має певний сенс.


6
Це моє улюблене пояснення, і воно працює добре, тому що воно пояснює загальноприйняті декларації C - навіть огидний і симпатичний синтаксис вказівника.
Бенджамін Поллак

12
Це якось акуратно, адже ви можете уявити, що немає фактичних типів вказівників. Є лише змінні, які при належному посиланні або переналагодженні дають вам один із примітивних типів.
biozinc

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;не гарний. Тим не менш, я за замовчуванням до однієї декларації на рядок і переживаю за комбінування коду пізніше.


2
Ну, оманливий перший приклад - це в моїх очах помилка дизайну. Якби я міг, я б видалив цей спосіб декларації з C повністю і зробив це таким чином, що обидва мають тип int *.
Адам

1
"* * прив'язується до змінної, ніж до типу" Це наївний аргумент. Розглянемо int *const a, b;. Звідки * "пов'язується"? Тип aє 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;це непрямий доступ ( *означає непрямий доступ), і тому друге вище твердження набагато ближче до стандартного, ніж перше.


6
Дякуємо, що врятували мене від написання ще однієї відповіді. До речі, я зазвичай читаю int a, *b, (*c)();як щось на зразок "оголошувати такі об'єкти як int: об'єкт a, об'єкт , на який вказує b, і об'єкт, повернутий із функції, на яку вказує c".
Антті Хаапала

1
*В int *a;не є оператор, і вона не розіменування a(який навіть ще не визначений)
ММ

1
@MM Будь ласка, називайте сторінку та номер рядка будь-якого стандарту ISO C, де цей стандарт говорить про те, що зірочка може бути чимось іншим, ніж множення чи непрямість. Він показує лише "оголошення вказівника" на прикладі, він ніде не визначає третього значення для зірочки (і він не визначає значення, яке не було б оператором). О, я ніде не стверджував, що щось "визначено", ви це придумали. Або, як зазначає капелюх Джонатана Леффлера, у стандарті C * - це завжди "граматика", це не є частиною зазначених у деклараціях специфікаторів (тому вона не є частиною декларації, тому вона повинна бути оператором)
Меккі

1
@MM 6.7.6.1 показує тільки декларацію , наприклад, так само , як я вже сказав, це не дає ніякого сенсу , щоб *(це дає лише значення в сумарному вираженні, а не в *межах вираження!). У ньому сказано, що "int a;" оголошує вказівник, який він робить, ніколи не стверджував інакше, але без даного значення *, читаючи його як * дереференційоване значення aint все ще є абсолютно дійсним, оскільки це має те саме фактичне значення. Ніщо, насправді нічого, написане в 6.7.6.1, не суперечило б цьому твердженню.
Mecki

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 у разі покажчиків, тому посилання справді є дивним у цьому відношенні.


12

Це лише питання переваги.

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

Я вважаю за краще оголошувати покажчики з відповідним знаком поруч із назвою типу, наприклад

int* pMyPointer;

3
Питання стосується C, де немає посилань.
Антті Хаапала

Дякуємо, що вказали на це, хоча питання стосувалося не вказівників чи посилань, а в основному про форматування коду.
macbirdie

8

Чудовий гуру якось сказав: "Прочитайте, як слід, компілятор".

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

Зрозуміло, що це стосувалося розміщення const, але тут діє те саме правило.

Компілятор читає це як:

int (*a);

не як:

(int*) a;

Якщо ви звикли розміщувати зірку поруч зі змінною, це полегшить читання ваших декларацій. Це також уникає очей, таких як:

int* a[10];

- Редагувати -

Точно пояснити, що я маю на увазі, коли я кажу, що це розбирається як int (*a), це означає, що *зв'язується більш щільно, aніж це робиться int, значною мірою таким чином, який у виразі 4 + 3 * 7 3пов'язується більш щільно, 7ніж це відбувається 4через більш високий пріоритет *.

З вибаченнями за мистецтво ascii, конспект AST для розбору int *aвиглядає приблизно так:

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

Як добре видно, *зв’язується більш міцно, aоскільки їх спільним предком є Declarator, тоді як вам потрібно пройти всю дорогу вгору по дереву, Declarationщоб знайти спільного предка, до якого належить int.


Ні, компілятор, безумовно, читає тип як (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;який більш інтуїтивно оголошує "а" як вказівник на ціле число, а не змішує стилі, коли також оголошує "b". Як хтось сказав, я б ні в якому разі не оголошував два різні типи в одній заяві.


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 ints.

  • pixel bitmap[height][width].

  • Ясна річ, я набрав вишневих прикладів, які мають багато інформації про тип праворуч, щоб зробити свою думку. Існує безліч декларацій, де більшість - якщо не всі - типу ліворуч, як struct { int x; int y; } center.

Цей синтаксис декларацій виріс із бажання K&R мати декларації, що відображають використання. Читання простих декларацій є інтуїтивно зрозумілим, а читання складніших можна опанувати, вивчаючи правило справа-ліво-право (іноді називайте правило спіралі або просто правило право-ліво).

C досить простий, що багато програмістів на C сприймають цей стиль і пишуть прості декларації як int *p.

У C ++ синтаксис став трохи складнішим (з класами, посиланнями, шаблонами, класами enum), і, як реакція на цю складність, ви побачите більше зусиль для відокремлення типу від ідентифікатора у багатьох деклараціях. Іншими словами, ви можете побачити більше int* pстилів-декларацій, якщо переглянете велику частину коду С ++.

На будь-якій мові ви завжди можете мати тип ліворуч змінних декларацій шляхом (1) ніколи не оголошувати кілька змінних в одному і тому самому операторі та (2) використовувати typedefs (або декларації псевдоніму, які, як не дивно, ставлять псевдонім ідентифікатори зліва від типів). Наприклад:

typedef int array_of_10_ints[10];
array_of_10_ints a;

Невелике запитання, чому не можна анулювати (* fn) (int) означає "fn - це функція, яка приймає int і повертає (void *)?"
Soham Dongargaonkar

1
@ a3y3: Оскільки в дужках (*fn)зберігається вказівник, пов'язаний із fnтипом повернення, а не з ним.
Адріан Маккарті

Зрозумів. Я думаю, що це досить важливо, щоб бути доданим у вашу відповідь, я скоро запропоную редагувати.
Soham Dongargaonkar

1
Я думаю, що це захаращує відповідь, не додаючи великої цінності. Це просто синтаксис мови. Більшість людей запитують про те, чому саме такий синтаксис, мабуть, вже знають правила. Усі, хто плутається з цього приводу, можуть побачити уточнення в цих коментарях.
Адріан Маккарті

1

Я вже відповів на подібне запитання в CP, і, оскільки ніхто про це не згадував, також тут я маю зазначити, що C - це мова вільного формату , який би стиль ви не обрали, тоді як аналізатор може розрізняти кожен маркер. Ця особливість C призводить до дуже особливого типу змагань під назвою C obfuscation Competition .


1

Багато аргументів у цій темі є простими суб'єктивними, і аргумент про те, що "зірка прив'язується до імені змінної" є наївним. Ось кілька аргументів, які не є лише думками:


Кваліфікатори типу забутого вказівника

Формально "зірка" не належить ні до типу, ні до назви змінної, вона є частиною власного граматичного елемента з назвою покажчик . Формальний синтаксис С (ISO 9899: 2018) такий:

(6.7) декларація:
специфікатори декларації init-декларатор-список опт ;

Якщо специфікатори декларації містять тип (і сховище), а список init-декларатор містить вказівник та ім'я змінної. Що ми бачимо, якщо далі розібрати синтаксис цього списку деклараторів:

(6.7.6) декларатор:
вказівник opt direct-декларатор
...
(6.7.6) покажчик:
* type-kvalifier-list opt
* type-kvalifier-list opt pointer

Якщо декларатором є вся декларація, прямим декларатором є ідентифікатор (ім'я змінної), а вказівник - зірка, за яким подається необов'язковий список класифікаторів типу, що належить самому покажчику.

Те, що робить різні аргументи стилю про те, що "зірка належить до змінної", суперечить тому, що вони забули про ці класифікатори типу вказівника. 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);

Це складено чисто. Тепер ви можете подумати, що код зроблений правильно. Не так! Цей код є випадково підробленою правильністю const.

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

Це відбувається тому , що в вираженні const bad_idea_t *foo, то *не належить до імені змінної тут! У псевдокоді цей параметр декларації слід читати як, const (bad_idea_t *) fooа не як (const bad_idea_t) *foo. У цьому випадку зірка належить до типу прихованого вказівника - тип - це вказівник, а покажчик, який має кваліфікацію за констатом, записується як *const.

Але тоді корінь проблеми у наведеному вище прикладі - практика приховування покажчиків за стилем a, typedefа не *стилем.


Щодо оголошення декількох змінних в одному рядку

Декларування декількох змінних в одному рядку широко визнано поганою практикою 1) . CERT-C підсумовує це як:

DCL04-C. Не оголошуйте більше однієї змінної на декларацію

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

І не має значення, чи є змінні вказівниками чи ні. Оголошення кожної змінної в одному рядку робить код яснішим майже в кожному випадку.

Тож аргумент про те, що програміст заплутався 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.