среда, 20 апреля 2011 г.

Declarations


Чтение объявлений¹ типов Си.

 
Перевод статьи Reading C type declarations, увидел у Alenacpp.
Даже для новичков программирования на Си не представляется проблемой прочесть простые описания Си наподобие

int foo[5];        // foo это массив из пяти 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 • void
• struct tag • union tag • enum tag
• long long • unsigned long long • long double ANSI/ISO C only


Описание может иметь только один основной тип, и он всегда стоит с левого края выражения.
Основные типы дополняются "производными", и в Си есть три таких:
  • * указатель на...
Обозначается символом *, и очевидно, что указатель всегда на что-то указывает.
  • [] массив из...
"Массив из" может быть безразмерным -- [] -- или размерным -- [10] -- однако размеры в действительности не играют значительной роли в чтении описания. Мы просто включаем размер в описание. Очевидно, что массив должен быть "массивом из" чего-либо.
  • () функция, возвращающая...
Обычно указывается парой смежных скобок - () - хотя помимо этого можно найти прототип списка параметров внутри. Списки параметров (если они есть) в действительности не играют роли в чтении описаний, и мы просто их игнорируем. Заметим, что круглые скобки говорят "функция, возвращающая", в отличие от скобок, используемых в группировании: группирующие скобки окружают имя переменной, в то время как скобки "функции, возвращающей" всегда находятся справа.
Функции не имеют смысла, пока они не возвращают что-либо (мы также вводим void (недействительный тип), взмахивая рукой и делая вид, что "возвращаем" недействительное).

Производный тип всегда изменяет то, что следует за ним, будь то основной тип или другой производный тип, и для правильного чтения описания мы всегда должны включать предлог ("на", "из", "возвращающий"). Сказав "указатель" вместо "указатель на", вы попросту поломаете описание.
Вполне вероятно, что выражения типа могут не иметь производных типов (то есть, "int i" описывает "i как int (целое)"), или наоборот иметь множество оных. Интерпретация производных типов, как правило, является камнем преткновения при чтении сложных описаний, однако эта проблема решается с помощью приоритета операций в следующем разделе.

Приоритет операций

Практический каждый программист на Си знаком с таблицей приоритетов операций, которая даёт правила, говорящие (к примеру), что умножение и деление имеет более высокий приоритет (выполняются перед), чем сложение или вычитание, а скобки могут быть использованы для изменения группирования. Это кажется естественным для "нормальных" выражений, но оказывается, что те же самые правила применимы и к описаниям — они являются больше выражениями типов, чем вычислений.

Операторы типов "массив из" [] и "функция, возвращающая" () имеют более высокий приоритет, чем "указатель на" *, и это формирует к некоторым довольно простым правилам для расшифрования.

Всегда начинайте с имени переменной:

  • foo это ...

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

  • foo это ... int (целых)

Работа над "заполнением середины", как правило, является самой сложной, однако она может быть сведена к следующему правилу:


"иди направо пока можешь, иди налево когда должен"


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

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


Начнём с простого примера:

  • long **foo[7];

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


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

foo это ... длинное целое ²

  • long ** foo[7];
В данной точке с именем переменной соседствует два производных типа: "массив из 7" и "указатель на", а правило гласит, что нужно следовать направо пока мы можем, поэтому в данном случае мы останавливаемся на "массив из 7"

foo это массив из 7 ... длинных целых ³

  • long ** foo[7];
Теперь мы ушли так далеко вправо, как было возможно, поэтому внутрення часть соединяется только с "указателем на" - берем его.

foo это массив из 7 указателей на ... длинные целые

  • long * *foo[7];
Теперь внутренняя часть прикасается только к "указателю на", поэтому выбираем и его.
foo это массив из 7 указателей на указатели на длинные целые


Описание готово!

Жесткий пример


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

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

Все описания начинаются так: "имя переменной это .... основной тип"
foo это ... символ

  • char *(*(**foo[] [8])())[];
Внутренняя часть прикасается к "массиву из" и "указателю на" - идем вправо.
foo это массив из ... символов

  • char *(*(**foo[][8])())[];
Снова в описании противопоставлены левое и правое, но это не является правилом: правило говорит нам следовать направо так далеко, как только мы можем. Здесь мы снова видим, что внутренняя часть соседствует с "массивом из" и "указателем на". Снова идем направо.
foo это массив из массивов из 8 ... символов

  • char *(*(** foo[][8])())[];
Теперь мы достигли скобок, используемых для группирования, и это прерывает наше следование направо. Итак, нам надо идти обратно, собирая все части слева (однако только до соответствующей скобки). Выбираем "указатель на":
foo это массив из массивов из 8 указателей на ... символы

  • char *(*(* *foo[][8])())[];
Снова возвращаемся налево, выбирая следующий "указатель на":
foo это массив из массивов из 8 указателей на указатель на ... символ

  • char *(*(**foo[][8])())[];
Выбрав на предыдущем шаге "указатель на", мы миновали целое подвыражение в скобках, поэтому "забираем" также и скобки. Таким образом мы внутренняя часть прикасается к "функции, возвращающей" справа, и "указателем на" слева - идём направо:
foo это массив из массивов из 8 указателей на указатель на функцию, возвращающую ... символ

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

  • char * (*(**foo[][8])())[];
Вычеркивая группирующие скобки, мы обнаруживаем, что внутренняя часть соприкасается с "массивом из" справа, и "указателем на" слева. Следуем направо:
foo это массив из массивов из 8 указателей на указатель на функцию, возвращающую указатель на массив из ... символов

  • char * (*(**foo[][8])())[];
В конце концов, мы видим слева единственного соседа, "указатель на": забираем его для решения задачи.
foo это массив из массивов из 8 указателей на указатель на функцию, возвращающую указатель на массив из указателей на символы

Мы представить себе не можем, как эту переменную можно использовать, однако мы по крайней мере можем правильно описать её тип.

Абстрактные описания


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


  • int (*(*)())()

На очевидный вопрос "с чего начать?", ответ звучит так "найдем место, где будет имя переменной, а далее действовать как с обычным описанием". Существует только одно место, куда можно вставить имя переменной, и его определение на самом деле несложно. Используя синтаксические правила, скажем что это место:

* справа от всех маркеров производного типа "указатель на"
* слева от всех маркеров производного типа "массив из"
* слева от всех маркеров производного типа "функция, возвращающая"
* внутри всех группирующие скобок

Взглянув на пример, мы увидим, что правый "указатель на" устанавливает одну границу, а левая "функция, возвращающая" устанваливает другую:

  • int (*(* • ) • ())()

Красные индикаторы • указывают только два места, где возможно наличие имени переменной, однако единственным является левое, посколько оно следует правилу "внутри группирующих скобок". Это модифицирует наше описание следующим образом:

  • int (*(*foo)())()

которое наши "нормальные" правила опишут как:

foo это указатель на функцию, возвращающую указатель на функцию, возвращающую целое

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


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

  • Нельзя объявлять массивы функций
Вместо этого используется "массив указателей на функцию, возвращающую...".
  • Функции не могут возвращать функции
Вместо этого используется "функция, возвращающая указатель на функцию, возвращающую ...".
  • Функции не могут возвращать массивы
Вместо этого используется "функция, возвращающая указатель на массив из...".
  • В многомерных массивах только крайний левый может быть безразмерным []
 Си поддерживает многоразмерные массивы (например, char foo[1][2][3][4]), хотя на практике это часто предполагает плохое структурирование данных. Тем не менее, когда массив более чем одной размерности, только крайний левый может быть пустым. char foo[] и char foo[][5] корректны, но char foo[5][] нет.
  • Тип "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, и довольно просты для понимания. Более подробная информация может быть найдена в Unixwiz.net Tech Tip: Using Win32 calling conventions

Более сложным случаем является соглашение о вызове, которое должно быть включено в указатель (в том числе с помощью typedef), потому что тег не вписывается в нормальное положение вещей. Они часто используются (к примеру), когда мы имеем дело с вызовами LoadLibrary() и GetProcAddress() API для вызова функции из свежезагруженной DLL.

Часто мы можем увидеть подобное:

typedef BOOL (__stdcall *PFNDRVQUERYDRIVERINFO)(
DWORD dwMode,
PVOID pBuffer,
DWORD cbBuf,
PDWORD pcbNeeded
);

...

/* получить адрес функции из DLL */
pfnDrvQueryDriverInfo = (PFNDRVRQUERYDRIVERINFO)
GetProcAddress(hDll, "DrvQueryDriverInfo")


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

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

читается как:

foo это указатель
на __stdcall функцию,
возвращающую BOOL.

____________

¹ здесь и далее описание и объявление в данной статье подразумевают под собой одно и то же
² здесь и далее имена типов приводятся на русском для большей ясности описаний (пр. перев.)
³ предполагаю, что русскому человеку более удобным будет менять падеж и число основного типа при формировании описания. Также считаю, что число производного типа следует делать множественным только после фразы "массив из", дабы не фокусироваться на лишнем.

jightuse, 2011.

Добавлю от себя, что текст очень интересный и переводить было забавно. "Жесткий пример" нереален, но часто в Си передаются, к примеру, недействительный указатель в качестве аргумента некоторой функции. После разбора текста стал лучше понимать описания, надеюсь это поможет ещё кому-нибудь.


Традиционно, источники:
Статья
Указатель на статью
Бальзам на душу: готовое решение

Комментариев нет: