Чтение объявлений¹ типов Си.
Перевод статьи 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
Описание может иметь только один основной тип, и он всегда стоит с левого края выражения.
Основные типы дополняются "производными", и в Си есть три таких:
- * указатель на...
- [] массив из...
- () функция, возвращающая...
Функции не имеют смысла, пока они не возвращают что-либо (мы также вводим void (недействительный тип), взмахивая рукой и делая вид, что "возвращаем" недействительное).
Производный тип всегда изменяет то, что следует за ним, будь то основной тип или другой производный тип, и для правильного чтения описания мы всегда должны включать предлог ("на", "из", "возвращающий"). Сказав "указатель" вместо "указатель на", вы попросту поломаете описание.
Вполне вероятно, что выражения типа могут не иметь производных типов (то есть, "int i" описывает "i как int (целое)"), или наоборот иметь множество оных. Интерпретация производных типов, как правило, является камнем преткновения при чтении сложных описаний, однако эта проблема решается с помощью приоритета операций в следующем разделе.
Приоритет операций
Практический каждый программист на Си знаком с таблицей приоритетов операций, которая даёт правила, говорящие (к примеру), что умножение и деление имеет более высокий приоритет (выполняются перед), чем сложение или вычитание, а скобки могут быть использованы для изменения группирования. Это кажется естественным для "нормальных" выражений, но оказывается, что те же самые правила применимы и к описаниям — они являются больше выражениями типов, чем вычислений.Операторы типов "массив из" [] и "функция, возвращающая" () имеют более высокий приоритет, чем "указатель на" *, и это формирует к некоторым довольно простым правилам для расшифрования.
Всегда начинайте с имени переменной:
- foo это ...
и всегда заканчивайте основным типом:
- foo это ... int (целых)
Работа над "заполнением середины", как правило, является самой сложной, однако она может быть сведена к следующему правилу:
"иди направо пока можешь, иди налево когда должен"
Начиная свой путь от имени переменной, учитывая правила приоритета и принимая во внимания маркеры производных типов, нужно продвигаться вправо так далеко, насколько это возможно до врезания в группирующие скобки. Затем идите налево к соответствующей скобке.
Простой пример
Начнём с простого примера:
- long **foo[7];
Подойдем к этому систематически, концентрируясь только на одной или двух маленьких частях, для формирования описания на
- long **foo [7];
foo это ... длинное целое ²
long**foo[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 это указатель на функцию, возвращающую указатель на функцию, возвращающую целое
Семантические ограничения / замечания
Не все комбинации производных типов допустимы, и возможно создание описания, прекрасно следующего синтаксическим правилам, но не некорректному в Си (то есть, верному синтаксически, но неверному семантически). Здесь мы поговорим о них.
- Нельзя объявлять массивы функций
- Функции не могут возвращать функции
- Функции не могут возвращать массивы
- В многомерных массивах только крайний левый может быть безразмерным []
- Тип "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.
Добавлю от себя, что текст очень интересный и переводить было забавно. "Жесткий пример" нереален, но часто в Си передаются, к примеру, недействительный указатель в качестве аргумента некоторой функции. После разбора текста стал лучше понимать описания, надеюсь это поможет ещё кому-нибудь.
Традиционно, источники:
Статья
Указатель на статью
Бальзам на душу: готовое решение
Комментариев нет:
Отправить комментарий