Эта головоломка состоит из трех частей.
Первая часть заключается в том, что пробелы в 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
Это подводит нас к третьей части головоломки - почему декларации структурированы именно так.
Предполагается, что структура объявления должна точно отражать структуру выражения в коде («объявление имитирует использование»). Например, предположим, что у нас есть массив указателей на intnamed ap, и мы хотим получить доступ к intзначению, на которое указывает i'th элемент. Мы получили бы доступ к этому значению следующим образом:
printf( "%d", *ap[i] );
Выражение *ap[i] имеет тип int; таким образом, объявление apзаписывается как
int *ap[N];
Декларатор *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;
и считают это плохой практикой по следующим причинам:
- Это не соответствует синтаксису;
- Это вносит путаницу (о чем свидетельствует этот вопрос, все дубликаты этого вопроса, вопросы о значении
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 ++ немного отличается, но концепции в основном те же.