Есть два объявления:

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.

Одна пара скобок полностью изменила структуру типа.


Здесь нужен не талант, а алгоритм

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

Разбор начинается с идентификатора — имени объявляемого объекта или функции.

Дальше всё объявление читается по одному правилу:

  1. На текущем уровне сначала движемся вправо до границы группировки и читаем все доступные суффиксы [] и ().

  2. Затем движемся влево и читаем слои указателя *.

  3. Если идентификатор заключён в круглые скобки, выходим из них на следующий уровень и снова повторяем: сначала вправо, затем влево.

  4. Когда декларатор закончился, последним читаем базовый тип.

Короткая запись правила:

вправо до границы текущей группировки → влево → выйти на следующий уровень → повторить

Последовательность разобранных частей будем записывать стрелками и называть путём чтения. Это учебный термин, а не термин стандарта Си.

Проверим этот алгоритм на нескольких объявлениях.


Указатель или массив указателей?

Простой случай:

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 — указатель на массив из 10 int.

Это, пожалуй, самая полезная пара объявлений для понимания всей системы:

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];

значит, первый шаг сделан.

А дальше нужна практика.