Как правильно читать объявления в Си

http://www.unixwiz.net/techtips/reading-cdecl.html
  • Перевод
Даже совсем зеленые программисты на Си, не испытывают проблем с чтением таких объявлений:
int foo[5]; // foo массив из 5 элементов типа int
char *foo; // foo указатель на char
double foo(); // foo функция возвращающая значение типа double

Но как только объявления становятся немного сложнее, проблематично точно сказать что это. Например:
char *(*(**foo[][8])())[];


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

Основные и производные типы


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

Основные типы:
• char
• signed char
• unsigned char
• short
• unsigned short
• int
• unsigned int
• long
• unsigned long
• float
• double
• long double
• void
• struct tag
• union tag
• enum tag
• long long
• unsigned long long


Объявление может содержать только один основной тип, и он всегда находится слева выражения. Основные типы дополняются производными типами, в Си их три:

1) * — указатель на ...
Обозначается символом *, и важно понимать что указатель всегда на что-нибудь указывает.

2)[] — массив из…
Массив может быть безразмерный — [], а может быть и размерный [10]. Правда размерный массив или нет, это неважно при чтении объявлений (обычно все же пишется размер массива). Должно быть понятно что массив всегда «массив из чего-нибудь».

3) () — функция возвращающая ...
Обычно обозначается парой круглых скобок (), но также возможно что внутри их будут модели параметров.Список параметров (если он есть) не играет существенной роли при чтении объявлений, и мы его обычно игнорируем. Заметим, что круглые скобки используемые для обозначения функций, отличаются от скобок служащих для группировки: группирующие скобки окружают переменные, тогда как скобки для обозначения функция находятся справа.Функция не имеет смысла если она ничего не возврщает(когда мы объявляем функцию с возвращаемым типом значения void, то это просто выглядит как будто функция возвращает значения типа void)

Производные типы всегда что то модифицируют, будь то основной тип или производный, и что бы правильно читать объявления, всегда нужно вставлять предлог («на», «из», «возвращающая»). Используя при чтении «указатель» вместо «указатель на», вы точно прочитаете объявление неправильно.

Приоритет операторов.


Почти каждый программист Си знаком с таблицами приоритетов операторов, в которых говорится что (например) умножение и деление имеют более высокий приоритет (выполняются раньше) чем сложение и вычитание, и группирующие скобки используются для изменения этого приоритета.Это кажется нормальным для «обычных» выражений, но те же правила применимы и к объявлениям — они просто «типовые», а не вычислительные.
Операторы «массив из»[] и «функция возвращающая»() имеют более высокий приоритет чем «указатель на», что приводит к довольно простому правилу декодирования:
Всегда начинайте с имени переменной:

foo — это .....

И заканчивайте декодирование основным типом:

… типа int

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

Простой пример.


Давайте начнем с простого примера:
-> long **foo[7];

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

-> long **foo [7];

Начинаем с имени переменной и заканчиваем основным типом:
foo — это… типа long

Разбираем дальше:
-> long **foo[7];

В данной момент имя переменной окружает лексема значащая «массив из 7 » и лексема значащая «указатель на», и в соответствии с правилом двигаемся вправо и дописываем к нашему описанию «массив из 7 »:
foo — это массив из 7 … типа long

-> long **foo[7];

Вправо больше некуда двигаться, а ближайшая лексема это «указатель на». Добавим её:
foo — это массив из 7 указателей на… значение типа long

-> long **foo[7];

Ближайшая лексема так же «указатель на», добавим и её:
foo — это массив из 7 указателей на указатели на значение типа long

Ну вот и все.

Сложный пример



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

-> char *(*(**foo [][8])())[];
Все объявления стоит начинать читать с «имя переменной… основной тип»
foo — это… типа char;

->char *(*(**foo [][8])())[];
К имени примыкают «указатель на» и «массив из», идем вправо:
foo — это массив из… типа char;

char *(*(**foo[][8])())[];
Мы можем выбрать правую или левую примыкающую лексему, но правило гласит что, необходимо двигаться вправо насколько это возможно, пока к внутренней части группирующих скобок что нибудь примыкает, поэтому идем вправо.
foo — это массив из массив из… типа char;

->char *(*(** foo[][8])())[];
Мы дошли до группирующих скобок, и дальше двигаться направо не представляется возможным, поэтому двигаемся влево пока не дойдем до парной группирующей скобки, чтоб вычеркнуть все остальные лексемы.
foo — это массив из массив из указателей на… типа char;

->char *(*(* *foo[][8])())[];
Снова двигаемся влево и приписываем «указатель на».
foo — это массив из массив из указателей на указатели на… типа char;

->char *(* (** foo[][8])())[];
После того как мы дописали «указатель на» в предыдущем шаге, мы дошли до парной группирующей скобки, так что продолжим присоединять и к «группирующим скобкам». Сейчас к ним примыкает«функция возвращает» справа и «указатель на» слева. Двигаемся вправо.
foo — это массив из массив из указателей на указатели на функцию возвращающую… типа char;

->char *(* (** foo[][8])())[];
Мы снова уперлись в группирующие скобки, поэтому снова возвращаемся налево.
foo — это массив из массив из указателей на указатели на функцию возвращающую указатели на… типа char;

->char *(*(** foo[][8])())[];
Обойдя группирующие скобки, видим что сейчас к вычеркнутым лексемам примыкает «массив из» справа и «указатель на» слева, «массив из» находится справа, добавим.
foo — это массив из массив из указателей на указатели на функцию возвращающую указатели на массив из… типа char;

->char *(*(** foo[][8])())[];
Ну и добавляем последнюю лексему.
foo — это массив из массив из указателей на указатели на функцию возвращающую указатели на массив из указателей на тип char;

Мы правда не знаем как это применить, но описание типа корректно.

Абстрактные объявления



Стандарт Си позволяет использовать абстрактные объявления, когда тип должен быть объявлен, но не связан с именем переменной. Это используется при приведении типов, и как аргумент sizeof — иногда это выглядит ужасающе:

int (*(*)())();

Естественно возникает вопрос с чего же начать, так вот ответ будет звучать так «надо найти место, где будет стоять имя переменной и рассматривать как обычное объявление». Такое место будет только одно, и найти его на самом деле очень просто. Используя правила синтаксиса, которые мы знаем:

•справа от всех лексем «указатель на»
•слева от всех лексем «массив из»
•слева от всех лексем «функция возвращает»
•внутри всех группирующих скобок

А теперь посмотрим на пример. Мы видим что левый набор лексем «указатель на» устанавливает одну границу и правый набор лексем «функция возвращает» устанавливает другую границу.
int (*(* •)• ())();

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

int (*(*foo)())();
которое наши правила описывают как:
foo — это указатель на функцию возвращающую указатель на функцию возвращающую значение типа int

Семантические ограничения / Примечания


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

• Невозможно создать массив функций
Но зато можно использовать массив указателей на функцию

•Функция не может возвращать функцию
Но может возвращать указатель на функцию

•Функция не может вернуть массив
Опять таки функция может вернуть указатель на массив

•В массивах только левая лексема [] может быть пустой
Си поддерживает многомерные массивы (например foo[1][2][3][4]), представляющие собой очень простую структуру данных. Однако, когда массив имеет больше чем одно измерение, то только первые скобки могут быть пустыми. char foo[] и char foo[][5] имеют право на существование, а вот char foo[5][] уже запрещено
•Тип «void» ограниченный
Тип «void» -это псевдо-тип, и переменные такого типа могут быть только «указатель на» и «функция возвращающая». Запрещено (точнее невозможно) использовать «массив из void» и просто переменные типа «void».
void *foo; //разрешено
void foo(); //разрешено
void foo; //запрещено
void foo[]; //запрещено

Добавление типа соглашения вызова


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

extern int __cdecl main(int argc, char **argv);

extern BOOL __stdcall DrvQueryDriverInfo(DWORD dwMode, PVOID pBuffer,
DWORD cbBuf, PDWORD pcbNeeded);


Такое добавление очень часто встречается в разработке под win32, оно достаточно простое для понимания. Больше информации в статье Использование соглашения вызова win32 .

Где это становится каким-то более сложным, так это когда соглашение вызова должно быть включено в «указатель» (включая typedef), потому, что лексема не выглядит так, чтобы соответствовать нормальной схеме.Это часто используется когда речь идет о работе с LoadLibrary() и GetProcAddress() API для обращения к вызову функции из недавно загруженной библиотеки.
Это можно часто встретить с typedef:
typedef BOOL (__stdcall *PFNDRVQUERYDRIVERINFO)(
DWORD dwMode,
PVOID pBuffer,
DWORD cbBuf,
PDWORD pcbNeeded
);

...

/* get the function address from the DLL */
pfnDrvQueryDriverInfo = (PFNDRVRQUERYDRIVERINFO)
GetProcAddress(hDll, "DrvQueryDriverInfo")


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

BOOL (__stdcall *foo)(...);

Читается:
foo — это указатель на __stdcall функцию возвращающую BOOL.

p.s. О неточностях пишите, пожалуйста, в личку.
Поделиться публикацией

Комментарии 88

    +27
    Несколько надуманный «сложный пример» в начале статьи :) А так, да, с опытом уже не испытываешь проблем с прочтением. На смену вопросу «omg, что это?» приходит другой — «omg, зачем так всё усложнять-то?» :)
      +4
      Такой изврат может понадобится только как средство для запугивания новичков или как билет на экзамене по программированию. Если такое появится в реальном проекте, то стоит задуматься о наличии серьёзных косяков в процессе проектирования.
        +1
        Я бы даже добавил, что читать-то оно может и не проблема, но понять, то, что ты прочёл — это другая песня. Я б за такую конструкцию человеку из своей команды минимум по рукам надавал, а максимум перевёл такого умника в другой менее критический проект… Код должен быть как песня — всем понятен, лаконичен и каждый чтоб мог «подхватить» чтобы петь хором. 8-)
      • НЛО прилетело и опубликовало эту надпись здесь
          +1
          Да, там забавно вообще.
          Только вместо foo обычно zval :D
          еще и несколько наборов макросов для каждого уровня указателй на zval'ы )))
            0
            А зачем это там? Кто-то просто развлекся или подобное имеет под собой основание? Если это так плохо, неужели бы никто до сегодняшнего дня не запатчил бы?
            +2
            У меня вопрос на счет собственно редакторов кода. VS или еще какие не могут автоматом парсить «сложные типы»? Я сам AS3-кодер и в FDT(Eclipse) работаю. У нас из-за строгой типизации всего (по-хорошему) автокомплит часто очень выручает. Например, при использовании класса Vector.<Class_of_element> или более сложных конструкций:

            public var myList : Vector.<Vector.<Vector.<MyClass>>> = new Vector.<Vector.<Vector.<MyClass>>>();

            редактор(FDT) при дальнейшем использовании переменной в виде:

            myList[0][0][0].автокомплит_по_классу_MyClass;

            выдает все верно и как бы парится о том что ты не правильно присвоишь значение не приходится особо. собственно вопрос: В VS, например, так же дела обстоят? Я подумываю над C# изучением как бэкэнда…

            P.S.
            Vector это типизированный массив. Через двоеточие указывается тип переменной при дефайне, в С++, я так понимаю, он перед переменно пишется.
              0
              С++ и C# — разные языки, за последний ничего сказать не могу. На счёт C++ и VS — автокомплит в студии, имхо, весьма убог (в сравнении с FlashDevelop, например), да и сам C++ содержит много неприятных моментов.
                0
                Встроенный Intellisens в студии убогенький, но проблема решается VisualAssists
                0
                Автокомплит в студии прекрасно работает. В C# работает лучше, чем в C++. В 2010 студии — лучше, чем в 2008
              +1
              А кто-то ведь не бросил.
              +15
              А ещё блин есть typedef, который всё это безобразие заворачивает в более читаемый вид.
                +4
                Да, техника старая. На C-Faq известна, как «Clockwise/Spiral Rule». c-faq.com/decl/spiral.anderson.html
                  +1
                  Может я и не прав, но техники все таки разные.
                  0
                  Все проще.
                    +4
                    Прошу прощения.
                    Все проще, в си объявления сделаны подобными использованию, т.е.:
                    long **foo[7];
                    Должно использоваться как
                    **foo[n] и будет иметь тип long.
                    Т.е. указали индекс, два раза разыменовали, получили long.

                    Аналогично становится легко понять
                    void (*foo)(int (*bar)(int), float);
                    (*foo)(&bar, 10.f)
                    +6
                    OMG, используйте typedef
                      +4
                      foo это массив из 7 указателей на указателей на значение типа long

                      Несколько раз перечитывал эту строчку.

                      Как по мне, так было бы яснее:
                      foo это массив из 7 указателей на указатели на значение типа long


                      Или так, но получится с тавтологией:
                        0
                        упс, отправил случайно. Вот недописал:
                        foo это массив из 7 указателей на указатели, указывающие на значения типа long
                          +2
                          Исправил. Но на самом деле я специально оставил это в таком ввиде, чтоб была видна структура алгоритма разбора, что каждый раз дописывается какая либо константа и не надо думать что же следующее и куда его вписать.
                        +1
                        А вам часто приходится читать такие объявления?
                          0
                          Нет, но благодаря этой статье я лично на автомате понимаю объявления не очень сложные.
                          +2
                            +2
                            Повторюсь: техники все таки разные, и эта, на мой личный взгляд, проще.
                            +6
                            Скоро появится статья на хабре «Как правильно читать.»
                              +6
                              Это ниче, вот когда я на химика учился, мы органические соединения называли по их формуле и обратно…
                                +6
                                По-моему, с языком что-то не так, если к нему прилагаются десятистраничные мануалы по прочтению объявлений переменных :/
                                  0
                                  Скорее с программистами, которым доставляет садисткое наслаждение такие штуки писать.
                                    +2
                                    да, с C определенно что-то не так, срочно перестаю его использовать!
                                    +4
                                    Просто не стоит так писать. :/
                                      0
                                      Поздно… К сожалению, ТАК уже написано много всего…
                                        +1
                                        Рефакторить!
                                          +1
                                          Страшно ))
                                            0
                                            Для этого сначала нужно отрефакторить стандартную рефакторилку, а то она не всегда правильно рефакторит.
                                        +5
                                        На самом деле знания действительно полезны. Но все таки если возникает потребность в таких объявлениях, то значит, что-то не так в проектировании ))
                                          +4
                                          Как-же научится правилно писат?
                                          • НЛО прилетело и опубликовало эту надпись здесь
                                              +3
                                              А теперь передай это топикстартеру.
                                                +2
                                                Для этого наверно и существует личная почта. И в конце статьи напомнил об этом. Высмеивать чужие ошибки, как это чертовски клёво, не так ли?
                                                  –1
                                                  Нет, личная почта не для этого.
                                                  А прилюдно нужно указать на ошибки, чтобы ДРУГИЕ правильно писали. Неужели непонятно?
                                                    0
                                                    Вы часто высмеиваете друзей, когда у них не получается что то, что получается у вас?
                                                      +2
                                                      Указание на грамматические ошибки имеет целью либо улучшение качества статьи, либо самоутверждение в духе «автор — лох, гыгы».
                                                      Для качества статьи лучше писать в личку (будет меньше мусора в комментариях).
                                                      Для самоутверждения — устроить прилюдную порку.

                                                      P.S. А ссылка на «других» — это лишь попытка придать своим низким действиям возвышенный смысл. Неужели непонятно?
                                                        +1
                                                        Нет, не самоутверждение, а потому что задолбали безграмотные неучи. Смотришь пост, вроде хороший, а ебанутые ошибки всё портят.
                                                +1
                                                Я так понимаю, что остально вопросов не вызывает?
                                                0
                                                что-бы правелно писат, нужно учится учится и учится.
                                                +4
                                                ПробОвать, пробОвать, пробОвать! Откуда эта Ы взялась? Статья неплохая, но следите за русским.
                                                  0
                                                  спасибо
                                                    –1
                                                    и тире бы везде добавить: foo — это
                                                  +2
                                                  Сегодня вечером решил взяться за это безобразие, но опустились руки.
                                                  И вот статья как на заказ, прочитал, понял, спасибо =)
                                                    0
                                                    Да пожалуйста. Искал что то подобное на русском, но не нашел.
                                                      0
                                                      Держите тогда заодно и сайтик для проверки: cdecl.org Он помогает такие гадости расшифровывать.
                                                      +13
                                                      kvs@uv1 ~:> cdecl
                                                      Type `help' or `?' for help
                                                      cdecl> explain char *(*(**foo[][8])())[]
                                                      declare foo as array of array 8 of pointer to pointer to function returning pointer to array of pointer to char
                                                      cdecl>
                                                        +2
                                                        Не понимаю, зачем это надо читать? Важнее понимать что эта штука означает, и как она выглядит в памяти, а для этого лучше всего подходит старый добрый карандаш с листком бумаги. Берем и рисуем квадратики со стрелочками — скажете что примитивно, зато наглядно.
                                                        А вообще по рукам(и не только) надо бить за такой код.
                                                          +2
                                                          бггг, а регекспы читать?
                                                          ^(\w)|(\@)|(\.)|(\-)
                                                            0
                                                            У вас ошибка в регеспе
                                                            ^(\w|@|\.|-)
                                                              0
                                                              это не ошибка, просто данные в разные переменные отлавливал.
                                                                0
                                                                — и @ не требуют экранирования.
                                                                  0
                                                                  да, в этом вы правы
                                                            +39
                                                              0
                                                              Комментаторам-защитникам C и C++: ради бога, смиритесь с тем, что эти языки несовершенны. Объявления в них выглядят стрёмно, и все это знают. Совершенных языков нет, но тот же D в плане объявлений совершеннее:

                                                              long**[7] foo, bar, baz;

                                                              Вот так следует объявлять foo, bar и baz — массивы из семи указателей на указатели на long, а не заучивать дурацкие правила или прятать весь ужас за typedef. И нет ни дублирования, ни особого порядка чтения. Просто читаешь слева направо, и всё.
                                                                +2
                                                                «читаешь слева направо»
                                                                «long**[7]»
                                                                «массивы из»
                                                                где же тут слева направо?
                                                                  0
                                                                  long* — указатель
                                                                  long** — указатель на указатель
                                                                  long**[7] — массив из семи указателей на указатель

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

                                                                  long **foo[7];

                                                                  Здесь вообще нужно читать не слева или справа, а с середины.
                                                                    +3
                                                                    >приплюснутое

                                                                    простите, а при чем тут плюсы? мне кажется, вы немного путаете педали.
                                                                      +1
                                                                      И сишное тоже. Цепляетесь к словам моим вы, падаван, мне кажется.
                                                                  +3
                                                                  Пожалуйста смиритесь с тем, что примеры в статье надуманы и нужны будут людям, только начинающим учить С++(который конечно не идеален). Думаю ваш оффтоп тут не уместен.
                                                                  P.S. все же хотелось бы посмотреть объявление char *(*(**foo[][8])())[] на D :D
                                                                    +1
                                                                    Те, что в статье — надуманы, но подобные конструкции иногда бывают нужны. Пускай выбор архитектуры останется за человеком, а язык, в первую очередь, должен человеку помогать, а не мешать. Но помогать в меру, не поощряя корявый код. Если можно подобные вещи записать понятнее, пусть в языке будет принята более понятная запись.
                                                                      0
                                                                      Я ведь и не спорю, что С/С++ абсолютный, непогрешимый идеал. Недостатки есть, но тема не о том как здорово в С записываются объявления. Тогда бы ваш коммент был в тему. А выбор языка определяется далеко не только и не столько удобством записи объявлений.
                                                                      P.S. как все же будет выглядеть запись char *(*(**foo[][8])())[] на D?
                                                                        +1
                                                                        Какой же вы бессердечный человек, если хотите, чтобы я распарсил в голове такой кошмар! Доверюсь автору статьи и запишу с его слов:
                                                                        foo это массив из массив из указателей на указатели на функцию возвращающую указатели на массив из указателей на тип char;

                                                                        char*[]* delegate() **[8][] foo;

                                                                        Читаем слева направо:
                                                                        char
                                                                        указатель на char
                                                                        массив указателей на char
                                                                        указатель на массив указателей на char
                                                                        функция, возвращающая указатель на массив указателей на char
                                                                        указатель на функцию, возвращающую указатель на массив указателей на char
                                                                        указатель на указатель на функцию, возвращающую указатель на массив указателей на char
                                                                        массив из восьми указателей на указатель на функцию, возвращающую указатель на массив указателей на char
                                                                        массив из массивов из восьми указателей на указатель на функцию, возвращающую указатель на массив указателей на char
                                                                    0
                                                                    >смиритесь с тем, что эти языки несовершенны

                                                                    да нам вобщемто глубоко пофигу на проблемы очередных нубов, не прочитавший K&R и лезущих своими трясущимися ручками в божественную благодать сишных сорцов.
                                                                      –2
                                                                      Сколько пафоса, ей-богу. Хорошо ещё, что вы не назвали C лучшим языком за всю историю программирования, а то бы я запил от безмерной грусти за человечество.
                                                                    +1
                                                                    И тем, кто помнит шуточное якобы интервью со Страуструпом: эта шутка вполне может стать правдой с приходом нового стандарта на C++ (:
                                                                      0
                                                                      В книжке что «Язык программирования С» Функция была для правильного определения с каким объявлением имеешь дело.
                                                                      Кому интересно вот The C Programming Language
                                                                      глава называется «5.12 Complicated Declarations»
                                                                        0
                                                                        Первая мысль «Господи, кто ж такое пишет?»
                                                                        Но дочитав почти до конца успокоился, что даже авторы не используют такое в своих проектах и пример просто с++ скороговорка.
                                                                          0
                                                                          вот такое объявление функции встретить в boost

                                                                          template<typename T,std::size_t N>
                                                                          T (*addressof(T (&t)[N]))[N]
                                                                          {
                                                                          return reinterpret_cast<T(*)[N]>(&t);
                                                                          }
                                                                            0
                                                                            Ваш пример понятнее и логичнее. Но я понял к чему вы.
                                                                          +2
                                                                          Сколько можно разжевывать правило улитки: на хабре это, как минимум, 3-ья статья.
                                                                            0
                                                                            Как вы переводили :)
                                                                              0
                                                                              в каком смысле как?
                                                                              0
                                                                              Указатель на указателе сидит, и указателем погоняет…
                                                                                0
                                                                                намутил как то.хотел как проще, а в итоге лишь запутал меня. интуитивно ясно же все, не?
                                                                                0
                                                                                Простите не удержался
                                                                                  0
                                                                                  Синтаксический анализ на Си есть в классической книге «Язык программирования C» Брайна Кернигана и Денниса Ритчи в конце 5 главы, если кому интересно.

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

                                                                                  Самое читаемое