Размещение звездочки в объявлениях указателей


95

Недавно я решил, что мне просто нужно наконец выучить C / C ++, и есть одна вещь, которую я действительно не понимаю в указателях или, точнее, в их определении.

Как насчет этих примеров:

  1. int* test;
  2. int *test;
  3. int * test;
  4. int* test,test2;
  5. int *test,test2;
  6. int * test,test2;

Теперь, насколько я понимаю, первые три случая делают то же самое: Test - это не int, а указатель на него.

Второй набор примеров немного сложнее. В случае 4 и test, и test2 будут указателями на int, тогда как в случае 5 только test является указателем, а test2 - «реальным» int. А как насчет случая 6? То же, что и в случае 5?


10
В C / C ++ пробелы не меняют значения.
Sulthan

19
7. int*test;?
Джин Квон,

3
+1, потому что я думал спросить только о 1–3. Прочитав этот вопрос, я узнал кое-что о 4–6, о чем я никогда не думал.
чрезвычайно superiorman

@Sulthan Это правда в 99% случаев, но не всегда. В голове у меня возник вопрос о типе шаблонного типа в требованиях к пространству шаблонного типа (до C ++ 11). В должны были быть написаны таким образом , не должны рассматриваться в качестве правого сдвига. Foo<Bar<char>>>>> >
AnorZaken

3
@AnorZaken Вы правы, это довольно старый комментарий. Существует несколько ситуаций, когда значение пробела изменится, например, ++оператор инкремента не может быть разделен пробелом, идентификаторы не могут быть разделены пробелом (и результат может быть допустимым для компилятора, но с неопределенным поведением во время выполнения). Точные ситуации очень сложно определить, учитывая синтаксический беспорядок, который представляет собой C / C ++.
Sulthan

Ответы:


128

4, 5 и 6 - это одно и то же, только test - это указатель. Если вам нужны два указателя, вы должны использовать:

int *test, *test2;

Или еще лучше (чтобы все было понятно):

int* test;
int* test2;

3
Значит, случай 4 на самом деле смертельная ловушка? Есть ли какая-либо спецификация или дополнительное чтение, объясняющее, почему int * test, test2 делает указателем только первую переменную?
Майкл Стам

8
@ Майкл Стум. Это C ++. Как вы думаете, есть ли логическое объяснение?
Джо Филлипс,

6
Прочтите K&R (язык программирования C). Это все очень четко объясняет.
Ферруччо,

8
Случаи 4, 5 и 6 - «смертельные ловушки». Это одна из причин, почему многие руководства по стилям C / C ++ предлагают только одно объявление для каждого оператора.
Майкл Берр,

14
Для компилятора C пробелы не важны (без учета препроцессора). Таким образом, независимо от того, сколько пробелов есть или нет между звездочкой и ее окружением, она имеет одно и то же значение.
ephemient

45

Пробелы вокруг звездочек значения не имеют. Все три означают одно и то же:

int* test;
int *test;
int * test;

" int *var1, var2" - это злой синтаксис, который предназначен только для того, чтобы запутать людей, и его следует избегать. Он расширяется до:

int *var1;
int var2;

16
Не забывай int*test.
Матин Улхак

2
пространство до или после звездочки - это просто вопрос эстетики. Однако стандарт кодирования Google поддерживаетint *test ( google-styleguide.googlecode.com/svn/trunk/… ). Просто будьте последовательны

@SebastianRaschka Руководство по стилю Google C ++ явно разрешает размещение любой звездочки. Возможно, он изменился с тех пор, как вы это прочитали.
Джаред Бек

33

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


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

int* test;   // test is a pointer to an int

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

int* const test; // test is a const pointer to an int

int const * test; // test is a pointer to a const int ... but many people write this as  
const int * test; // test is a pointer to an int that's const

Хотя «одна переменная на строку» кажется полезной, мы все еще не полностью решили ситуацию, когда звездочка находится левее или правее. Я совершенно уверен, что в исходном коде преобладает один вариант; немного похоже на то, что одни страны едут по правой стороне, а другие едут в неправильном направлении, например, Великобритания. ;-)
шеви

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

33

Используйте «Правило спирали по часовой стрелке» для облегчения анализа объявлений C / C ++;

Необходимо выполнить три простых шага:

  1. Начиная с неизвестного элемента, двигайтесь по спирали / по часовой стрелке; при обнаружении следующих элементов замените их соответствующими английскими утверждениями:

    [X] или [] : размер массива X ... или неопределенный размер массива ...

    (type1, type2): функция передает type1 и возвращает type2 ...

    *: указатель (ы) на ...

  2. Продолжайте делать это по спирали / по часовой стрелке, пока все жетоны не будут закрыты.
  3. Всегда сначала разрешайте все, что указано в скобках!

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


4
Это выглядит устрашающе и, к сожалению, довольно ужасно.
Джо Филлипс,

7
да, но это кажется неплохим объяснением некоторых из более сложных конструкций
Майкл Штум

@ d03boy: Нет никаких сомнений - объявления C / C ++ могут быть кошмаром.
Майкл Берр,

2
«Спираль» не имеет никакого смысла, тем более «по часовой стрелке». Я бы предпочел называть это «правилом вправо-влево», поскольку синтаксис не заставляет вас смотреть вправо-внизу-слева-вверху, только вправо-влево.
Руслан

Я усвоил это как правило «право-лево-право». Людям, работающим с C ++, часто нравится делать вид, будто вся информация о типе находится слева, что ведет к int* x;стилю, а не к традиционному int *x;стилю. Конечно, для компилятора интервал не имеет значения, но он влияет на людей. Отрицание фактического синтаксиса приводит к правилам стиля, которые могут раздражать и сбивать с толку читателей.
Адриан Маккарти

12

Как уже упоминалось, 4, 5 и 6 одинаковы. Часто люди используют эти примеры, чтобы аргументировать, что *переменная принадлежит переменной, а не типу. Хотя это вопрос стиля, есть некоторые споры о том, следует ли вам думать и писать так:

int* x; // "x is a pointer to int"

или так:

int *x; // "*x is an int"

FWIW Я нахожусь в первом лагере, но причина, по которой другие приводят аргумент в пользу второй формы, заключается в том, что она (в основном) решает эту конкретную проблему:

int* x,y; // "x is a pointer to int, y is an int"

что потенциально вводит в заблуждение; вместо этого вы бы написали либо

int *x,y; // it's a little clearer what is going on here

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

int *x, *y; // two pointers

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


6
это подделка, как вы называете int *MyFunc(void)? a *MyFunc- это функция, возвращающая int? нет. Очевидно, мы должны написать int* MyFunc(void), скажем, MyFuncфункцию, возвращающую int*. Так что для меня это ясно, правила синтаксического анализа грамматики C и C ++ просто неверны для объявления переменных. они должны были включать квалификацию указателя как часть общего типа для всей последовательности запятой.
v.oddou

1
Но *MyFunc() это файл int. Проблема с синтаксисом C заключается в смешивании синтаксиса префикса и постфикса - если бы использовался только постфикс, не было бы путаницы.
Антти Хаапала

1
Первый лагерь борется с синтаксисом языка, что приводит к путанице в конструкциях вроде int const* x;, которые я считаю такими же вводящими в заблуждение, как a * x+b * y.
Адриан Маккарти


5

В 4, 5 и 6 testвсегда есть указатель, а test2не указатель. Пробелы (почти) никогда не имеют значения в C ++.


3

Смысл C в том, что вы объявляете переменные так, как вы их используете. Например

char *a[100];

говорит, что *a[42]это будет char. И a[42]указатель на символ. Итак, aэто массив указателей на символы.

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


Тем не менее, запись char* a[100]; также выводит, что *a[42];это будет указатель на символы charи a[42];символы.
yyny 07

Что ж, мы все делаем одни и те же выводы, только порядок меняется.
Мишель Бийо,

Цитата: "говорит, что * a [42] будет char. А [42] указателем на char". Вы уверены, что не наоборот?
deLock

Если вы предпочитаете другой способ, скажем a[42], это charуказатель, а *a[42]это символ.
Мишель Бийо

3

На мой взгляд, ответ - ОБА, в зависимости от ситуации. Вообще, IMO, лучше ставить звездочку рядом с именем указателя, а не с типом. Сравните, например:

int *pointer1, *pointer2; // Fully consistent, two pointers
int* pointer1, pointer2;  // Inconsistent -- because only the first one is a pointer, the second one is an int variable
// The second case is unexpected, and thus prone to errors

Почему второй случай противоречив? Потому что, например, int x,y;объявляются две переменные одного и того же типа, но этот тип упоминается в объявлении только один раз. Это создает прецедент и ожидаемое поведение. И int* pointer1, pointer2;это несовместимо с этим, потому что он объявляется pointer1как указатель, но pointer2является целочисленной переменной. Очевидно, что он подвержен ошибкам, и, следовательно, его следует избегать (поставив звездочку рядом с именем указателя, а не с типом).

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

MyClass *volatile MyObjName

void test (const char *const p) // const value pointed to by a const pointer

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

void* ClassName::getItemPtr () {return &item;} // Clear at first sight


2

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

  • в языке программирования c Денниса М. Ритчи звезды находятся в правой части объявления.

  • посмотрев исходный код Linux на https://github.com/torvalds/linux/blob/master/init/main.c, мы увидим, что звездочка также находится справа.

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


Что ж - парсер, похоже, допускает любой вариант, но если Деннис и Линус говорят, что он должен быть на правильной стороне, это довольно убедительно. Но, тем не менее, нам не хватает какого-то обоснования, а также объяснения, почему это делается. Это немного похоже на ситуацию с табуляцией и пробелом - за исключением того, что она была решена, потому что люди, которые используют пробелы, а не табуляции, зарабатывают больше денег, согласно StackOverflow ... :-)
shevy

1

Указатель является модификатором типа. Лучше читать их справа налево, чтобы лучше понять, как звездочка изменяет тип. 'int *' можно читать как «указатель на int». В нескольких объявлениях вы должны указать, что каждая переменная является указателем, иначе она будет создана как стандартная переменная.

1,2 и 3) Тест имеет тип (int *). Пробелы не имеют значения.

4,5 и 6) Тест имеет тип (int *). Test2 имеет тип int. Опять же пробелы несущественны.


1

Эта головоломка состоит из трех частей.

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

На этапе предварительной обработки исходный текст разбивается на последовательность токенов - идентификаторы, знаки препинания, числовые литералы, строковые литералы и т. Д. Эта последовательность токенов позже анализируется на предмет синтаксиса и значения. Токенизатор является «жадным» и построит максимально длинный допустимый токен. Если вы напишете что-то вроде

inttest;

токенизатор видит только два токена - идентификатор, inttestза которым следует знак препинания ;. На intэтом этапе он не распознается как отдельное ключевое слово (это происходит позже в процессе). Итак, чтобы строка считалась объявлением целого числа с именем test, мы должны использовать пробелы для разделения токенов идентификаторов:

int test;

Этот *символ не является частью какого-либо идентификатора; это отдельный токен (пунктуатор) сам по себе. Итак, если вы напишете

int*test;

компилятор видит 4 отдельных лексем - int, *, test, и ;. Таким образом, пробелы не важны в объявлениях указателей, и все

int *test;
int* test;
int*test;
int     *     test;

интерпретируются одинаково.


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

unsigned long int a[10]={0}, *p=NULL, f(void);

в декларации спецификаторы unsigned long intи declarators являются a[10]={0}, *p=NULLи f(void). Декларатор вводит имя объявляемой вещи ( a, pи f) вместе с информацией о массиве, указателе и функции этой вещи. С декларатором также может быть связанный инициализатор.

Тип a- «10-элементный массив unsigned long int». Этот тип полностью определяется комбинацией спецификаторов объявления и декларатора, а начальное значение указывается с помощью инициализатора ={0}. Точно так же типом pявляется «указатель на unsigned long int», и снова этот тип определяется комбинацией спецификаторов объявления и декларатора и инициализируется значением NULL. И по той же причине тип f"возврат функции unsigned long int".

Это ключевой момент - нет спецификатора типа "указатель на" , так же как нет спецификатора типа "массив из", точно так же, как нет спецификатора типа "возвращающий функцию". Мы не можем объявить массив как

int[10] a;

потому что операнд []оператора aне int. Аналогично в декларации

int* p;

операнд *is p, not int. Но поскольку оператор косвенного обращения является унарным, а пробелы не важны, компилятор не будет жаловаться, если мы напишем его таким образом. Однако он всегда интерпретируется как int (*p);.

Следовательно, если вы напишете

int* p, q;

операнд *is p, поэтому он будет интерпретирован как

int (*p), q;

Таким образом, все

int *test1, test2;
int* test1, test2;
int * test1, test2;

сделайте то же самое - во всех трех случаях test1является операндом *и, следовательно, имеет тип "указатель на int", а test2имеет тип int.

Заявители могут быть очень сложными. У вас могут быть массивы указателей:

T *a[N];

у вас могут быть указатели на массивы:

T (*a)[N];

у вас могут быть функции, возвращающие указатели:

T *f(void);

у вас могут быть указатели на функции:

T (*f)(void);

у вас могут быть массивы указателей на функции:

T (*a[N])(void);

у вас могут быть функции, возвращающие указатели на массивы:

T (*f(void))[N];

у вас могут быть функции, возвращающие указатели на массивы указателей на функции, возвращающие указатели на T:

T *(*(*f(void))[N])(void); // yes, it's eye-stabby.  Welcome to C and C++.

и тогда у вас есть signal:

void (*signal(int, void (*)(int)))(int);

который читается как

       signal                             -- signal
       signal(                 )          -- is a function taking
       signal(                 )          --   unnamed parameter
       signal(int              )          --   is an int
       signal(int,             )          --   unnamed parameter
       signal(int,      (*)    )          --   is a pointer to
       signal(int,      (*)(  ))          --     a function taking
       signal(int,      (*)(  ))          --       unnamed parameter
       signal(int,      (*)(int))         --       is an int
       signal(int, void (*)(int))         --     returning void
     (*signal(int, void (*)(int)))        -- returning a pointer to
     (*signal(int, void (*)(int)))(   )   --   a function taking
     (*signal(int, void (*)(int)))(   )   --     unnamed parameter
     (*signal(int, void (*)(int)))(int)   --     is an int
void (*signal(int, void (*)(int)))(int);  --   returning void
    

и это лишь малая часть того, что возможно. Но обратите внимание, что массив, указатель и функция всегда являются частью декларатора, а не спецификатора типа.

Одна вещь, на которую следует обратить внимание - constможно изменить как тип указателя, так и тип указателя:

const int *p;  
int const *p;

Оба из вышеперечисленных объявляются pкак указатель на const intобъект. Вы можете написать новое значение, чтобы pоно указывало на другой объект:

const int x = 1;
const int y = 2;

const int *p = &x;
p = &y;

но вы не можете писать в объект, на который указывает:

*p = 3; // constraint violation, the pointed-to object is const

Однако,

int * const p;

объявляется pкак constуказатель на неконстантный int; вы можете написать то, на что pуказывает

int x = 1;
int y = 2;
int * const p = &x;

*p = 3;

но вы не pможете указать на другой объект:

p = &y; // constraint violation, p is const

Это подводит нас к третьей части головоломки - почему декларации структурированы именно так.

Предполагается, что структура объявления должна точно отражать структуру выражения в коде («объявление имитирует использование»). Например, предположим, что у нас есть массив указателей на intnamed ap, и мы хотим получить доступ к intзначению, на которое указывает i'th элемент. Мы получили бы доступ к этому значению следующим образом:

printf( "%d", *ap[i] );

Выражение *ap[i] имеет тип int; таким образом, объявление apзаписывается как

int *ap[N]; // ap is an array of pointer to int, fully specified by the combination
            // of the type specifier and declarator

Декларатор *ap[N]имеет ту же структуру, что и выражение *ap[i]. Операторы *и []ведут себя в объявлении так же, как и в выражении - []имеют более высокий приоритет, чем унарный *, поэтому операнд *равен ap[N](он анализируется как *(ap[N])).

В качестве другого примера предположим, что у нас есть указатель на массив intnamed, paи мы хотим получить доступ к значению i'th элемента. Мы бы написали это как

printf( "%d", (*pa)[i] );

Тип выражения (*pa)[i]- int, поэтому объявление записывается как

int (*pa)[N];

Опять же, применяются те же правила приоритета и ассоциативности. В этом случае мы не хотим разыменовывать i'th элемент pa, мы хотим получить доступ к i' th элементу, на который pa указывает , поэтому мы должны явно сгруппировать *оператор с pa.

Операторы *, []и ()являются частью выражения в коде, поэтому все они являются частью декларатора в объявлении. Декларатор сообщает вам, как использовать объект в выражении. Если у вас есть такое объявление int *p;, это говорит вам, что выражение *pв вашем коде даст intзначение. В расширении он сообщает вам, что выражение pдает значение типа «указатель на int» или int *.


Итак, что о таких вещах , как литые и sizeofвыражения, где мы используем такие вещи , как (int *)или sizeof (int [10])или тому подобные? Как мне прочитать что-то вроде

void foo( int *, int (*)[10] );

Там нет описателя, не является *и []операторами , изменяющих типа напрямую?

Что ж, нет - есть еще декларатор с пустым идентификатором (известный как абстрактный декларатор ). Если мы представим пустой идентификатор с символом X, то мы можем читать эти вещи , как (int *λ), sizeof (int λ[10])и

void foo( int *λ, int (*λ)[10] );

и они ведут себя точно так же, как и любое другое объявление. int *[10]представляет собой массив из 10 указателей, а int (*)[10]представляет собой указатель на массив.


А теперь самоуверенная часть этого ответа. Мне не нравится соглашение C ++ об объявлении простых указателей как

T* p;

и считают это плохой практикой по следующим причинам:

  1. Это не соответствует синтаксису;
  2. Это вносит путаницу (о чем свидетельствует этот вопрос, все дубликаты этого вопроса, вопросы о значении T* p, q;, все дубликаты этих вопросов и т. Д.);
  3. Это внутренне непротиворечиво - объявление массива указателей T* a[N]асимметричным с использованием (если вы не привыкли писать * a[i]);
  4. Его нельзя применять к типам «указатель на массив» или «указатель на функцию» (если только вы не создаете определение типа только для того, чтобы можно было T* pаккуратно применить соглашение, а это… нет );
  5. Причина этого - «это подчеркивает указатель на объект» - надумана. Его нельзя применять к типам массивов или функций, и я думаю, что эти качества так же важно подчеркнуть.

В конце концов, это просто указывает на путаницу в размышлениях о том, как работают системы типов двух языков.

Есть веские причины декларировать товары отдельно; работа над плохой практикой ( T* p, q;) не входит в их число. Если вы правильно напишете свои деклараторы ( T *p, q;), вы вряд ли запутаетесь.

Я считаю, что это сродни намеренному написанию всех ваших простых forциклов как

i = 0;
for( ; i < N; ) 
{ 
  ... 
  i++ 
}

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


  1. Я буду использовать терминологию C - терминология C ++ немного отличается, но концепции в основном те же.

-3

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

Например:

int const bla;

Константа применяется к слову "int". То же самое и со звездочками указателей, они применяются к ключевому слову слева от них. А собственно имя переменной? Ага, это декларируется тем, что от него осталось.


1
Это не отвечает на вопрос. Хуже того, если мы попытаемся вывести из него ответ, то это будет означать, что звездочка привязывается к типу слева от него, что, как все остальные сказали, является ложным. Он привязывается к единственному имени переменной справа.
underscore_d
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.