Эта головоломка состоит из трех частей.
Первая часть заключается в том, что пробелы в 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)
и тогда у вас есть signal
:
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)
и это лишь малая часть того, что возможно. Но обратите внимание, что массив, указатель и функция всегда являются частью декларатора, а не спецификатора типа.
Одна вещь, на которую следует обратить внимание - 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;
Однако,
int * const p;
объявляется p
как const
указатель на неконстантный int
; вы можете написать то, на что p
указывает
int x = 1;
int y = 2;
int * const p = &x;
*p = 3;
но вы не p
можете указать на другой объект:
p = &y
Это подводит нас к третьей части головоломки - почему декларации структурированы именно так.
Предполагается, что структура объявления должна точно отражать структуру выражения в коде («объявление имитирует использование»). Например, предположим, что у нас есть массив указателей на int
named ap
, и мы хотим получить доступ к int
значению, на которое указывает i
'th элемент. Мы получили бы доступ к этому значению следующим образом:
printf( "%d", *ap[i] );
Выражение *ap[i]
имеет тип int
; таким образом, объявление ap
записывается как
int *ap[N];
Декларатор *ap[N]
имеет ту же структуру, что и выражение *ap[i]
. Операторы *
и []
ведут себя в объявлении так же, как и в выражении - []
имеют более высокий приоритет, чем унарный *
, поэтому операнд *
равен ap[N]
(он анализируется как *(ap[N])
).
В качестве другого примера предположим, что у нас есть указатель на массив int
named, 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;
и считают это плохой практикой по следующим причинам:
- Это не соответствует синтаксису;
- Это вносит путаницу (о чем свидетельствует этот вопрос, все дубликаты этого вопроса, вопросы о значении
T* p, q;
, все дубликаты этих вопросов и т. Д.);
- Это внутренне непротиворечиво - объявление массива указателей
T* a[N]
асимметричным с использованием (если вы не привыкли писать * a[i]
);
- Его нельзя применять к типам «указатель на массив» или «указатель на функцию» (если только вы не создаете определение типа только для того, чтобы можно было
T* p
аккуратно применить соглашение, а это… нет );
- Причина этого - «это подчеркивает указатель на объект» - надумана. Его нельзя применять к типам массивов или функций, и я думаю, что эти качества так же важно подчеркнуть.
В конце концов, это просто указывает на путаницу в размышлениях о том, как работают системы типов двух языков.
Есть веские причины декларировать товары отдельно; работа над плохой практикой ( T* p, q;
) не входит в их число. Если вы правильно напишете свои деклараторы ( T *p, q;
), вы вряд ли запутаетесь.
Я считаю, что это сродни намеренному написанию всех ваших простых for
циклов как
i = 0;
for( ; i < N; )
{
...
i++
}
Синтаксически верный, но сбивающий с толку, а намерение может быть неправильно истолковано. Однако это T* p;
соглашение укоренилось в сообществе C ++, и я использую его в своем собственном коде C ++, потому что согласованность всей базы кода - это хорошо, но каждый раз, когда я это делаю, у меня начинает чесаться.
- Я буду использовать терминологию C - терминология C ++ немного отличается, но концепции в основном те же.