Размещение звездочки в объявлениях указателей
Недавно я решил, что мне просто нужно окончательно изучить C/C++, и есть одна вещь, которую я не совсем понимаю в отношении указателей или, точнее, их определения.
Как насчет этих примеров:
int* test;
int *test;
int * test;
int* test,test2;
int *test,test2;
int * test,test2;
Теперь, насколько я понимаю, все первые три случая делают одно и то же: Test - это не int, а указатель на один.
Второй набор примеров немного сложнее. В случае 4 и test, и test2 будут указателями на int, тогда как в случае 5 только test является указателем, тогда как test2 является "реальным" int. Что насчет случая 6? То же, что и в случае 5?
17 ответов
4, 5 и 6 - это одно и то же, только тест - это указатель. Если вы хотите два указателя, вы должны использовать:
int *test, *test2;
Или даже лучше (чтобы все было понятно):
int* test;
int* test2;
Пробелы вокруг звездочек не имеют значения. Все три означают одно и то же:
int* test;
int *test;
int * test;
"int *var1, var2
"это злой синтаксис, который просто предназначен для того, чтобы сбить людей с толку и его следует избегать. Он распространяется на:
int *var1;
int var2;
Многие рекомендации по кодированию рекомендуют объявлять только одну переменную в строке. Это позволяет избежать путаницы, которую вы имели до того, как задать этот вопрос. Большинство программистов на С ++, с которыми я работал, похоже, придерживаются этого.
Что-то немного в стороне, я знаю, но кое-что, что я нашел полезным, должно прочитать декларации назад.
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
Эта головоломка состоит из трех частей.
Первая часть заключается в том, что пробелы в 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
а деклараторы
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;
операнд
*
является
p
не
int
. Но поскольку оператор косвенного обращения является унарным, а пробелы не важны, компилятор не будет жаловаться, если мы напишем его таким образом. Однако это всегда интерпретируется как
int (*p);
.
Следовательно, если вы напишете
int* p, q;
операнд
*
является
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
Это подводит нас к третьему элементу головоломки - почему объявления так структурированы.
Предполагается, что структура объявления должна точно отражать структуру выражения в коде ("объявление имитирует использование"). Например, предположим, что у нас есть массив указателей на
int
названный
ap
, и мы хотим получить доступ к
int
значение, на которое указывает
i
й элемент. Мы получили бы доступ к этому значению следующим образом:
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])
).
В качестве другого примера предположим, что у нас есть указатель на массив
int
названный
pa
и мы хотим получить доступ к значению
i
й элемент. Мы бы написали это как
printf( "%d", (*pa)[i] );
Тип выражения
(*pa)[i]
является
int
, поэтому объявление записывается как
int (*pa)[N];
Опять же, применяются те же правила приоритета и ассоциативности. В этом случае мы не хотим разыменовать
i
й элемент
pa
, мы хотим получить доступ к
i
ый элемент чего
pa
указывает на, поэтому мы должны явно сгруппировать
*
оператор с
pa
.
В
*
,
[]
и
()
все операторы являются частью выражения в коде, поэтому все они являются частью декларатора в объявлении. Декларатор сообщает вам, как использовать объект в выражении. Если у вас есть декларация вроде
int *p;
, это означает, что выражение
*p
в вашем коде даст
int
значение. В более широком смысле это говорит вам, что выражение
p
дает значение типа "указатель на
int
", или же
int *
.
Итак, что насчет таких вещей, как актерский состав и
sizeof
выражения, где мы используем такие вещи, как
(int *)
или же
sizeof (int [10])
или тому подобное? Как мне прочитать что-то вроде
void foo( int *, int (*)[10] );
Нет декларатора, не так ли
*
и
[]
операторы, изменяющие тип напрямую?
Ну нет - есть еще декларатор, только с пустым идентификатором (известный как абстрактный декларатор). Если мы представим пустой идентификатор символом λ, то мы можем читать эти вещи как
(int *λ)
,
sizeof (int λ[10])
, и
void foo( int *λ, int (*λ)[10] );
и они ведут себя точно так же, как и любое другое объявление.
int *[10]
представляет собой массив из 10 указателей, а
int (*)[10]
представляет указатель на массив.
А теперь самоуверенная часть этого ответа. Мне не нравится соглашение C++ об объявлении простых указателей как
T* p;
и считают это плохой практикой по следующим причинам:
- Это не соответствует синтаксису;
- Это вносит путаницу (о чем свидетельствует этот вопрос, все дубликаты этого вопроса, вопросы о значении
T* p, q;
, все дубликаты этих вопросов и т. д.); - Это не внутренне непротиворечиво - объявление массива указателей как
T* a[N]
асимметричен с использованием (если вы не привыкли писать* a[i]
); - Его нельзя применять к типам указатель на массив или указатель на функцию (если только вы не создаете typedef только для того, чтобы применить
T* p
условно чисто, чего...нет); - Причина этого - "это подчеркивает указатель на объект" - надумана. Его нельзя применять к типам массивов или функций, и я думаю, что эти качества так же важно подчеркнуть.
В конце концов, это просто указывает на путаницу в размышлениях о том, как работают системы типов двух языков.
Есть веские причины декларировать товары отдельно; обход плохой практики (T* p, q;
) не один из них. Если вы правильно напишите свои деклараторы (T *p, q;
) у вас меньше шансов вызвать замешательство.
Я считаю это сродни тому, чтобы сознательно писать все свои простые
for
петли как
i = 0;
for( ; i < N; )
{
...
i++
}
Синтаксически верный, но сбивающий с толку, а цель, вероятно, будет неправильно истолкована. Тем не менее
T* p;
Соглашение укоренилось в сообществе C++, и я использую его в своем собственном коде на C++, потому что согласованность в кодовой базе - это хорошо, но меня зудит каждый раз, когда я это делаю.
- Я буду использовать терминологию C - терминология C++ немного отличается, но концепции в основном те же.
Используйте "Правило спирали по часовой стрелке", чтобы помочь разобрать объявления C/C++;
Есть три простых шага:
Начиная с неизвестного элемента, двигайтесь по спирали / по часовой стрелке; при обнаружении следующих элементов замените их соответствующими английскими выражениями:
[X]
или же[]
: Размер массива X... или размер неопределенного массива...
(type1, type2)
: функция, передающая type1 и type2, возвращающая...
*
: указатель (и) на...- Продолжайте делать это по спирали / по часовой стрелке, пока все жетоны не будут закрыты.
- Всегда сначала разрешите что-нибудь в скобках!
Кроме того, объявления должны быть в отдельных заявлениях, когда это возможно (что верно в подавляющем большинстве случаев).
Как уже упоминалось, 4, 5 и 6 одинаковы. Часто люди используют эти примеры, чтобы аргументировать, что *
принадлежит с переменной вместо типа. Хотя это вопрос стиля, есть некоторые споры о том, стоит ли думать и писать так:
int* x; // "x is a pointer to int"
или так:
int *x; // "*x is an int"
Я в первом лагере, но причина, по которой другие приводят аргументы в пользу второй формы, заключается в том, что она (в основном) решает эту конкретную проблему:
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
Лично я говорю, держите его в одной переменной на строку, тогда не имеет значения, какой стиль вы предпочитаете.
#include <type_traits>
std::add_pointer<int>::type test, test2;
В 4, 5 и 6, test
всегда указатель и test2
не указатель Пустое пространство (почти) никогда не бывает значительным в C++.
Смысл в C заключается в том, что вы объявляете переменные так, как вы их используете. Например
char *a[100];
Говорит, что *a[42]
будет char
, А также a[42]
указатель на символ И поэтому a
это массив указателей на символы
Это потому, что авторы оригинального компилятора хотели использовать один и тот же парсер для выражений и объявлений. (Не очень разумная причина для выбора дизайна языка)
Я бы сказал, что первоначальное соглашение состояло в том, чтобы поставить звезду на стороне имени указателя (правая сторона объявления
в языке программирования c Денниса М. Ричи звезды находятся на правой стороне декларации.
посмотрев исходный код linux по адресу https://github.com/torvalds/linux/blob/master/init/main.c мы увидим, что звезда также находится на правой стороне.
Вы можете следовать тем же правилам, но это не имеет большого значения, если вы поставите звезды на стороне типа. Помните, что последовательность важна, поэтому всегда, но звезда на одной стороне, независимо от того, какую сторону вы выбрали.
На мой взгляд, лучше ставить звездочку рядом с именем указателя, а не над типом. Сравните, например:
int *pointer1, *pointer2; // Fully consistent, two pointers
int* pointer1, pointer2; // Inconsistent, unexpected, and thus prone to errors
Почему второй случай несовместим? Потому что например int x,y;
объявляет две переменные одного типа, но тип упоминается только один раз в объявлении. Это создает прецедент и ожидаемое поведение. А также int* pointer1, pointer2;
несовместимо с этим.
Это скорее дополнение к ответу @John Bode, который представляет собой прекрасное произведение.
Как упомянул Боде, большая часть нынешней путаницы в C по поводу размещения унарного оператора в объявлении указателя имеет происхождение из C++.
Лучше всего это иллюстрируется следующим абзацем из книги Йенса Густедта « Современный C» (помните, что Г. является соредактором стандарта ISO C):
Обратите внимание, что символ * играет две разные роли в определении double_swap. В объявлении он создает новый тип (тип указателя), тогда как в выражении он разыменовываетC объект, на который ссылается указательC. Чтобы отличить эти два использования одного и того же символа, мы обычно сбрасываем * слева без пробелов между ними, если он изменяет тип (например, double*), и вправо, если он разыменовывает указатель (*p0).
Это извращение K&R , заявившего, что использование*
в объявлении указателя «предназначается как мнемоника», но становится легче понять, когда понимаешь, что М. Густедт имеет опыт работы с C++.
Я всегда предпочитал объявлять указатели следующим образом:
int* i;
Я прочитал это, чтобы сказать, что «имеет тип int-pointer». Вам может сойти с рук эта интерпретация, если вы объявляете только одну переменную для каждого объявления.
Однако неприятная правда состоит в том, что это прочтение неверно . Язык программирования C, 2-е изд. (стр. 94) объясняет противоположную парадигму, которая используется в стандартах C:
Объявление указателя,
int *ip;
задуман как мнемоника; он говорит, что выражение является . Синтаксис объявления переменной имитирует синтаксис выражений, в которых может появляться эта переменная. Это рассуждение применимо и к объявлениям функций. Например,
double *dp, atof(char *);
говорит, что в выражении
*dp
а такжеatof(s)
имеют значения типаdouble
, и что аргументatof
является указателем наchar
.
Итак, по рассуждениям языка C, когда вы объявляете
int* test, test2;
вы не объявляете две переменные типа
int*
, вы вводите два выражения, которые возвращают тип без привязки к выделению
int
в памяти.
Компилятор вполне счастлив принять следующее:
int *ip, i;
i = *ip;
потому что в парадигме C ожидается, что компилятор будет отслеживать только тип и . Ожидается, что программист
будет отслеживать значение*ip
а также
i
. В таком случае,
ip
является неинициализированным, поэтому программист несет ответственность за то, чтобы указать ему на что-то значимое, прежде чем разыменовывать его.
Вы можете думать о 4, 5 и 6 следующим образом: объявление типа должно быть сделано только один раз, но если вы хотите объявить указатель на этот тип (добавив звездочку), вы должны сделать это для каждой переменной.
При объявлении переменной-указателя я всегда добавляю пробел между переменной и звездочкой, даже если я объявляю более одного в строке. Отказ от этого заставляет меня путать его с выражением разыменования почти каждый раз.
Указатель является модификатором типа. Лучше читать их справа налево, чтобы лучше понять, как звездочка изменяет тип. 'int *' может читаться как "указатель на int". В нескольких объявлениях вы должны указать, что каждая переменная является указателем или она будет создана как стандартная переменная.
1,2 и 3) Тест относится к типу (int *). Пробелы не имеют значения.
4,5 и 6) Тест относится к типу (int *). Test2 имеет тип int. Опять пропуски несущественны.
Хорошее практическое правило, многие люди, кажется, понимают эти понятия следующим образом: В C++ много семантического значения получают путем левой привязки ключевых слов или идентификаторов.
Взять, к примеру:
int const bla;
Const относится к слову "int". То же самое относится и к звездочкам указателей, они применяются к ключевому слову слева от них. А фактическое имя переменной? Да, это заявлено тем, что от него осталось.
Случаи 1, 2 и 3 одинаковы, они объявляют указатели на переменные типа int. Случаи 3, 4 и 5 одинаковы, так как они объявляют один указатель на и одну переменную int соответственно. Если вы хотите объявить два указателя в одной строке (чего не следует делать), вам необходимо поставить звездочку перед каждым именем переменной:
int *test, *test2;
Нет определенного правильного способа, который говорит, куда идет звездочка. int* test
выглядит лучше, потому что нам легче представить, что добавление *
до конца типа означает "указатель" на этот тип. Тем не мение, int *test
имеет больше смысла, потому что вы можете работать с ним, как знак минус в математике:
-(-x) = x
аналогично
*(*test) = test
Это всегда помогало мне. К сожалению, результатом всего этого является то, что иногда я использую int* test
и иногда int *test
,