Одна пара скобок меняет всё: как читать сложные объявления в Си
Есть два объявления:
int *a[10];
int (*b)[10];
Они отличаются буквально одной парой скобок.
Но a и b имеют принципиально разные типы.
Если вы давно пишете на Си, скорее всего, сможете вспомнить или угадать ответ. Но попробуйте другой вопрос: можете ли вы получить его механически — не по памяти и не всматриваясь в скобки?
А если объявление выглядит так?
int (*f[10])(char *);
Или так?
void (*signal(int sig, void (*func)(int)))(int);
У сложных объявлений Си есть неприятное свойство: пока конструкции простые, кажется, что никакого специального метода не нужно. А когда появляется сразу несколько массивов, указателей, функций и скобок, привычное «ну я примерно вижу, что тут написано» перестаёт работать.
При этом синтаксис здесь не хаотичен. Его можно разбирать вполне механически.
В этой статье попробуем это сделать.
Для начала: что такое a?
Вернёмся к первой паре:
int *a[10];
int (*b)[10];
Не смотрите пока на int.
Начнём с имени объявляемого объекта.
В первом случае это:
a
На текущем уровне сначала идём вправо. Сразу справа находится суффикс:
[10]
Значит, a — массив из 10 элементов.
Продолжаем идти вправо, но других суффиксов на этом уровне нет. Поэтому меняем направление и идём влево. Там встречаем *.
Получаем:
a— массив из 10 указателей…
Декларатор закончился. Только теперь доходим до базового типа:
int
Итог:
a— массив из 10 указателей наint.
То есть:
int *a[10];
Теперь второй случай:
int (*b)[10];
Начинаем снова с имени:
b
Сначала пробуем идти вправо. Но сразу встречаем закрывающую скобку — границу текущей группировки. На этом уровне справа читать нечего, поэтому идём влево и встречаем *:
(*b)
Поэтому первым к b относится именно указатель.
Получаем:
b— указатель…
Дойдя до открывающей скобки, выходим на уровень выше и снова сначала идём вправо. Теперь видим:
[10]
Значит:
b— указатель на массив из 10 элементов…
После [10] справа больше ничего нет, а слева на этом уровне нет новых указателей. Декларатор закончился, и остаётся базовый тип int:
b— указатель на массив из 10 элементов типаint.
Одна пара скобок полностью изменила структуру типа.
Здесь нужен не талант, а алгоритм
На простых примерах разбор можно проделывать почти интуитивно. Но нам нужен способ, который продолжит работать, когда объявление станет длиннее.
Разбор начинается с идентификатора — имени объявляемого объекта или функции.
Дальше всё объявление читается по одному правилу:
На текущем уровне сначала движемся вправо до границы группировки и читаем все доступные суффиксы
[]и().Затем движемся влево и читаем слои указателя
*.Если идентификатор заключён в круглые скобки, выходим из них на следующий уровень и снова повторяем: сначала вправо, затем влево.
Когда декларатор закончился, последним читаем базовый тип.
Короткая запись правила:
вправо до границы текущей группировки → влево → выйти на следующий уровень → повторить
Последовательность разобранных частей будем записывать стрелками и называть путём чтения. Это учебный термин, а не термин стандарта Си.
Проверим этот алгоритм на нескольких объявлениях.
Указатель или массив указателей?
Простой случай:
int *p;
Начинаем с p.
Сначала идём вправо, но справа от p ничего нет. Значит, идём влево и встречаем *.
Значит:
p— указатель наint.
Теперь:
int a[10];
Начинаем с a.
Идём вправо и читаем суффикс [10]. Дальше справа ничего нет, слева тоже нет новых слоёв, поэтому последним читаем int.
Получаем:
a— массив из 10 элементов типаint.
А теперь объединим оба элемента:
int *a[10];
Начинаем с a: идём вправо и читаем [10]. Справа больше ничего нет — идём влево и читаем *. Последним читаем int.
Получаем:
a— массив из 10 указателей наint.
Но достаточно добавить скобки:
int (*p)[10];
Начинаем с p. Справа сразу находится закрывающая скобка, поэтому идём влево и читаем *. Дойдя до открывающей скобки, выходим на уровень выше, снова идём вправо и читаем [10]. Последним читаем int.
Порядок меняется:
p— указатель на массив из 10int.
Это, пожалуй, самая полезная пара объявлений для понимания всей системы:
int *a[10]; // массив указателей
int (*p)[10]; // указатель на массив
Если эта разница стала видна не как заученное правило, а как следствие структуры объявления, дальше становится значительно легче.
Добавим функцию
Рассмотрим:
int (*handler)(double);
Начинаем с handler.
Сначала идём вправо, но сразу упираемся в закрывающую скобку. Поэтому идём влево и читаем *:
handler— указатель…
Дойдя до открывающей скобки, выходим на уровень выше и снова идём вправо. Там находится суффикс функции:
(double)
указатель на функцию, принимающую
double…
После него справа больше ничего нет, а слева нет новых слоёв указателя. Декларатор закончился, и последним читаем базовый тип:
int
…и возвращающую
int.
Полное описание:
handler— указатель на функцию, которая принимаетdoubleи возвращаетint.
На этом месте обычно становится понятно, зачем вообще нужен механический способ разбора.
Когда мы видим:
int (*handler)(double);
глаз легко цепляется за отдельные знакомые элементы — int, *, круглые скобки, double — но сам по себе набор знакомых символов ещё не говорит нам, как они связаны друг с другом.
А теперь массив таких указателей
Сделаем ещё один шаг:
int (*handlers[10])(double);
Попробуйте разобрать его самостоятельно, прежде чем читать дальше.
Начинаем с:
handlers
Сначала идём вправо. С handlers связан суффикс:
[10]
Значит:
handlers— массив из 10 элементов…
Продолжаем вправо, но упираемся в закрывающую скобку. Поэтому идём влево и читаем *:
массив из 10 указателей…
Дойдя до открывающей скобки, выходим на уровень выше и снова идём вправо. Там находится:
(double)
массив из 10 указателей на функции, принимающие
double…
После суффикса функции справа больше ничего нет, а слева нет новых указателей. Последним читаем int:
handlers— массив из 10 указателей на функции, принимающиеdoubleи возвращающиеint.
То есть даже такое объявление:
int (*handlers[10])(double);
можно не «распознавать».
Его можно последовательно разобрать.
Проверим что-нибудь менее игрушечное
Например:
int (*f[10])(char *);
Не пытайтесь перевести строку целиком.
Начните с имени:
f
Идём вправо и читаем [10]. Дальше справа находится граница группировки, поэтому идём влево и читаем *. Дойдя до открывающей скобки, выходим на уровень выше, снова идём вправо и читаем (char *). Последним остаётся int.
Шаг за шагом получается:
f
→ массив из 10 ...
→ указателей на ...
→ функции ...
→ принимающие char * ...
→ возвращающие int
И получаем:
f— массив из 10 указателей на функции, принимающиеchar *и возвращающиеint.
В виде пути чтения тот же разбор записывается так:
f → массив[10] → указатель → функция(char *) → int
Почему скобки так много меняют
Сравним:
int *f(double);
и:
int (*f)(double);
В первом случае непосредственно с f связаны круглые скобки:
f(double)
Начинаем с f и идём вправо. Сразу читаем (double), значит, f — функция. Справа больше ничего нет, поэтому идём влево и читаем *.
А * относится уже к тому, что эта функция возвращает:
f— функция, принимающаяdoubleи возвращающая указатель наint.
Во втором случае:
(*f)
начинаем с f, пробуем идти вправо, но сразу упираемся в закрывающую скобку. Поэтому идём влево и читаем *. Дойдя до открывающей скобки, выходим на уровень выше, снова идём вправо и читаем (double). Скобки заставляют связать * с f раньше суффикса функции.
Получается:
f— указатель на функцию, принимающуюdoubleи возвращающуюint.
То есть:
int *f(double);
и:
int (*f)(double);
отличаются одной парой скобок, но описывают совершенно разные вещи.
Именно поэтому попытка читать сложные объявления просто слева направо быстро ломается.
Настоящее объявление из стандартной библиотеки
Есть классический пример:
void (*signal(int sig, void (*func)(int)))(int);
Он выглядит почти как издевательство над человеком.
Но принципиально нового синтаксиса здесь нет.
Начинаем с имени:
signal
Идём вправо. Первым доступен суффикс функции:
signal(...)
Значит:
signal— функция…
Посмотрим на параметры:
int sig
и:
void (*func)(int)
Второй параметр разбираем отдельно тем же способом. Начинаем с func. Справа сразу находится закрывающая скобка, поэтому идём влево и читаем *. Дойдя до открывающей скобки, выходим на уровень выше, идём вправо и читаем (int). Последним читаем void.
Значит, второй параметр сам является указателем на функцию:
func— указатель на функцию, принимающуюintи возвращающуюvoid.
Теперь возвращаемся к самой signal. Суффикс с параметрами уже прочитан. Продолжаем вправо, но упираемся в закрывающую скобку — границу группировки. Поэтому идём влево и читаем *: функция возвращает указатель.
Указатель на что?
Дойдя до открывающей скобки, выходим на уровень выше и снова идём вправо. Там находится суффикс функции:
(int)
После него справа больше ничего нет, а слева нет новых указателей. Последним читаем базовый тип:
void
Итого:
signal— функция, принимающаяintи указатель на функцию, принимающуюintи возвращающуюvoid; самаsignalвозвращает указатель на функцию, принимающуюintи возвращающуюvoid.
Большое объявление оказалось всего лишь последовательностью уже знакомых конструкций.
Полный путь чтения выглядит так:
signal
→ функция(int sig, указатель → функция(int) → void)
→ указатель
→ функция(int)
→ void
Небольшая проверка
Попробуйте без компилятора разобрать следующие объявления.
1
char *names[20];
2
char (*name)[20];
3
long (*convert)(const char *);
4
void (*callbacks[8])(int);
5
char (*next(int code))(long);
Для каждого попробуйте не просто сказать ответ, а записать последовательность:
имя → ... → ... → базовый тип
Понять один пример и приобрести навык — разные вещи
Когда алгоритм показан на готовом объявлении, всё обычно кажется довольно очевидным.
Проблема обнаруживается позже.
Вы открываете чужой заголовочный файл, встречаете что-нибудь вроде:
int (*(*factory)(void))(double);
и снова начинаете считать скобки.
Причина проста: понимание правила ещё не означает, что сформировался навык его применения.
Здесь очень похожая ситуация на арифметику: можно за минуту понять алгоритм деления столбиком, но от этого ещё не появляется способность быстро и без ошибок применять его к любым числам.
Со сложными объявлениями Си происходит то же самое.
Нужно несколько раз:
найти идентификатор;
определить следующий элемент структуры;
правильно обработать скобки;
пройти объявление целиком;
перевести полученную структуру в нормальное описание типа.
И постепенно синтаксис перестаёт выглядеть как набор специальных случаев.
А можно просто использовать typedef
Конечно.
Например, вместо:
void (*handler)(int);
можно написать:
typedef void (*Handler)(int);
Handler handler;
И часто именно так и стоит делать в собственном коде.
Но typedef не уничтожает сложные типы.
Он только даёт им имена.
А читать чужие заголовочные файлы, системные API, библиотечный код, сообщения компилятора и объявления внутри typedef всё равно приходится.
Поэтому вопрос не в том, стоит ли писать максимально сложные декларации самостоятельно.
Вопрос в том, можете ли вы спокойно разобрать такую декларацию, когда она уже встретилась в коде.
Вместо вывода
Сложные объявления в Си не обязательно запоминать как коллекцию отдельных трюков:
int *a[10];
int (*a)[10];
int (*f)(double);
int *f(double);
int (*f[10])(char *);
За ними стоит одна и та же система.
Если научиться последовательно восстанавливать структуру декларатора, даже страшные объявления постепенно превращаются в обычную механическую задачу.
Но здесь есть важная разница между «я понял объяснение» и «я действительно умею это делать».
Поэтому я сделал бесплатный практический курс «Сложные объявления в Си: читаем, строим и упрощаем».
Там вы будете читать и строить объявления с массивами, указателями и функциями, разбирать указатели на функции, таблицы обработчиков и сложные параметры, работать с типами без имени объекта и упрощать интерфейсы с помощью typedef.
Если вы после этой статьи уже можете уверенно объяснить разницу между:
int *a[10];
и:
int (*a)[10];
значит, первый шаг сделан.
А дальше нужна практика.