Например, давайте рассмотрим static
спецификатор класса хранения. Вот несколько примеров правильного и неправильного использования этого спецификатора класса хранения:
static int a; // valid
int static b; // valid
static int* c; // valid
int static* d; // valid
int* static e; // ill-formed
static int const* f; // valid
int static const* g; // valid
int const static* h; // valid
int const* static i; // ill-formed
typedef int* pointer;
static pointer j; // valid
pointer static k; // valid
(Объявления, помеченные как «действительные», были приняты Visual C ++ 2012, g ++ 4.7.2 и Clang ++ 3.1. Объявления, помеченные как «неправильно сформированные», были отклонены всеми этими компиляторами.)
Это кажется странным, потому что спецификатор класса хранения применяется к объявленной переменной. Это объявленная переменная, которая является static
, а не тип объявленной переменной. Почему e
а также i
плохо сформирован, но k
хорошо сформирован?
Какие правила определяют правильное размещение спецификаторов класса хранения? Пока я использовал static
в этом примере этот вопрос относится ко всем спецификаторам классов хранения. Предпочтительно, чтобы в полном ответе содержались ссылки на соответствующие разделы стандарта языка C ++ 11 и их пояснения.
Таким образом, в любом месте спецификатора декларации (см. Раздел 7.1 в ИСО / МЭК 14882-2012), т. Е. До *
, Отборочные после *
связаны с указателем указателя, а не с указателем типа, и static
не имеет смысла в контексте объявления указателя.
Рассмотрим следующие случаи:
Вы можете объявить обычный int и указатель на int в том же списке объявлений, например:
int a, *b;
это потому, что спецификатор типа int
тогда у вас есть два объявления с использованием этого спецификатора типа int
, a
и указатель-указатель *a
который объявляет указатель на int
, Теперь рассмотрим:
int a, static b; // error
int a, *static b; // error
int a, static *b; // error
что должно выглядеть неправильно (как они есть), и причина (как определено в разделах 7.1 и 8.1) состоит в том, что C и C ++ требуют, чтобы ваши описатели хранилища соответствовали вашему описателю типа, а не вашему декларатору.
Так что теперь должно быть ясно, что следующее также неверно, так как вышеупомянутые три также неверны:
int *static a; // error
Ваш последний пример,
typedef int* pointer;
static pointer j; // valid
pointer static k; // valid
оба действительны и оба эквивалентны, потому что pointer
Тип определяется как спецификатор типа, и вы можете поместить свой спецификатор типа и спецификатор хранилища в любом порядке. Обратите внимание, что они оба эквивалентны и будут эквивалентны высказыванию
static int *j;
static int *k;
или же
int static *j;
int static *k;
Согласно 7.1, [упрощенная] структура объявления C ++ имеет вид
decl-specifier-seq init-declarator-list;
Согласно 7.1 / 1, спецификаторы класса хранения принадлежат в начальной «общей» части decl-specifier-seq
,
За 8/1, init-declarator-list
это последовательность деклараторов.
За 8/4 г. *
Часть объявления указателя является частью отдельного объявления в этой последовательности. Это сразу означает, что все, что следует за *
является частью этого индивидуального декларатора. Вот почему некоторые из ваших мест размещения спецификаторов класса хранения недопустимы. Синтаксис объявления не позволяет включать спецификаторы класса хранения.
Обоснование довольно очевидно: так как спецификаторы класса хранения должны применяться к все деклараторы во всей декларации, они помещаются в «общую» часть декларации.
Я бы сказал, что более интересная (и несколько связанная) ситуация имеет место со спецификаторами, которые могут присутствовать в и то и другое decl-specifier-seq
и отдельные деклараторы, как const
спецификатор. Например, в следующем объявлении
int const *a, *b;
делает const
относится ко всем деклараторам или только к первому? Грамматика диктует прежнюю интерпретацию: const
применяется ко всем деклараторам, т. е. является частью decl-specifier-seq
,
Если вы нанимаете «Золотое правило» (что также не относится только к указателям) это следует естественно, интуитивно, и это позволяет избежать много ошибок а также подводные камни при объявлении переменных в C / C ++. «Золотое правило» не должно нарушаться (есть редкие исключения, такие как const
применяется к массиву typedefs, который распространяется const
на базовый тип и ссылки, которые пришли с C ++).
К&R, Приложение A, раздел 8.4, Значение деклараторов гласит:
Каждый декларатор считается утверждением о том, что когда в выражении появляется конструкция той же формы, что и декларатор, он возвращает объект указанного типа и класса хранения.
Чтобы объявить переменную в C / C ++, вы должны подумать о выражении, которое вы должны применить к нему, чтобы получить базовый тип.
1) Должно быть имя переменной
2) Затем следует выражение как допустимое * из оператора объявления, примененное к имени переменной
3) Затем следует оставшаяся информация и свойства объявления, такие как базовый тип и хранилище.
Хранение не является характеристикой, которую вы всегда можете придать выражению выражений, в отличие от константности, например. Это имеет смысл только при объявлении. Таким образом, хранилище должно прийти куда-то еще, но не в 2.
int * const *pp;
/*valid*/
int * static *pp;
/*invalid, this clearly shows how storage makes no sense for 2 and so breaks */
/*the golden rule. */
/*It's not a piece of information that goes well in the middle of a expression.*/
/*Neither it's a constraint the way const is, it just tells the storage of */
/*what's being declared. */
Я думаю К&R хотел, чтобы мы использовали инвертированные рассуждения при объявлении переменных, это часто не обычная привычка. При использовании он позволяет избежать большинства сложных ошибок и трудностей в декларации.
* valid не в строгом смысле, так как встречаются некоторые вариации, такие как x [], x [size, not indexing], constness и т. д. Итак, 2 — это выражение, которое карты хорошо (для использования декларации), «та же форма», та, которая отражает использование переменной, но не строго.
#include <iostream>
int (&f())[3] {
static int m[3] = {1, 2, 3};
return m;
}
int main() {
for(int i = 0; i < sizeof(f()) / sizeof(f()[0]); ++i)
std::cout << f()[i] << std::endl;
return 0;
}
В контексте деклараций, &
это не операция, чтобы получить адрес, она просто говорит, что ссылка.
f()
: f
это функция&
вернуть: его вернуть это ссылка[3]
: ссылка это к массив из 3 элементовint
массив [я]: элемент это интИтак, у вас есть функция, которая возвращает ссылку на массив из 3 целых чисел, и, поскольку у нас есть правильная информация времени компиляции о размере массива, мы можем проверить это с помощью sizeof
в любое время =)
Последний золотой совет — для всего, что может быть помещено перед типом, когда в нескольких объявлениях оно применяется ко всем переменным одновременно, и поэтому не может применяться индивидуально.
это const
нельзя поставить раньше int
:
int * const p;
Таким образом, следующее действительно:
int * const p1, * const p2;
Это можно:
int const *p; // or const int *p;
Таким образом, следующее недействительно:
int const *p1, const *p2;
Обмениваемый const
применяется ко всем:
int const *p1, *p2; // or const int *p1, *p2;
Из-за этого я всегда ставлю все, что не может ставить перед типом, ближе к переменной (int *a
, int &b
) и все, что Можно ставлю раньше, ставлю раньше (volatile int c
).
Есть много на эту тему в http://nosubstance.me/post/constant-bikeshedding/.