С не так сложно: void (* (* f []) ()) ()


188

Я только сегодня увидел фотографию и думаю, что буду благодарен за объяснения. Итак, вот картинка:

некоторый код c

Я нашел это смущающим и задавался вопросом, практичны ли когда-либо такие коды. Я гуглил картинку и нашел другую картинку в этой записи Reddit, и вот эта картинка:

какое-то интересное объяснение

Так что это «чтение по спирали» является чем-то действительным? Это как C-компиляторы анализируют?
Было бы здорово, если бы есть более простые объяснения этого странного кода.
Кроме всего прочего, могут ли эти коды быть полезными? Если да, то где и когда?

Есть вопрос о «спиральном правиле», но я не просто спрашиваю о том, как оно применяется или как выражения читаются с этим правилом. Я подвергаю сомнению использование таких выражений и правильность спирального правила. Что касается этого, некоторые хорошие ответы уже опубликованы.


9
Как объяснение может быть проще? Он охватывает все аспекты определения fw / пары слов для каждого ключевого момента.
— Скотт Хантер

30
Может быть, это сложно? Это фактически объявляет fкак массив указателей на функции, которые могут принимать любой аргумент ... если бы это было так void (*(*f[])(void))(void);, тогда да, это были бы функции, которые не принимают аргументов ...
— txtechhelp

18
На практике не используйте такой неясный код. Используйте typedef для подписей
— Василий Старынкевич

4
любое объявление с указателями на функции может быть сложным. Это не значит, что нормальный C или C ++ сложен таким же образом. Другие языки решают эту проблему по-разному, включая отсутствие указателей на функции, что в некоторых случаях может быть существенным упущением
— Кейт Грегори

20
Если вы прищурите глаза, это будет похоже на LISP.
— user2023861 31.12.15

Ответы:


117

Существует правило, называемое «Правило по часовой стрелке / спираль», которое помогает найти значение сложного объявления.

Из c-faq :

Есть три простых шага:

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

    [X]or []
    => Array X size of ... или Array undefined size of ...

    (type1, type2)
    => функция, передающая type1 и type2, возвращающая ...

    *
    => указатель (и) на ...

  2. Продолжайте делать это по спирали / по часовой стрелке, пока все жетоны не будут закрыты.

  3. Всегда сначала разрешите что-нибудь в скобках!

Вы можете проверить ссылку выше для примеров.

Также обратите внимание, что в помощь вам также существует сайт под названием:

http://www.cdecl.org

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

void (*(*f[])())()

это выводит:

объявить f как массив указателей на функцию, возвращающую указатель на функцию, возвращающую void

РЕДАКТИРОВАТЬ:

Как указано в комментариях Random832 , спиральное правило не обращается к массиву массивов и приведет к неправильному результату (большей части) этих объявлений. Например, для int **x[1][2];спирального правила игнорируется тот факт, что []имеет более высокий приоритет над *.

Перед массивом массивов можно сначала добавить явные скобки перед применением правила спирали. Например: int **x[1][2];то же самое, что и int **(x[1][2]);(также допустимый C) из-за приоритета, и тогда спиральное правило правильно его читает, поскольку «x - это массив 1 из массива 2 указателя на указатель на int», который является правильным объявлением на английском языке.

Обратите внимание , что этот вопрос также рассматривается в этом ответе по Джеймс Kanze (указал haccks в комментариях).


5
Хотелось бы, чтобы cdecl.org был лучше
— Grady Player

8
Не существует "спирального правила" ... "int *** foo [] [] []" определяет массив массивов массивов указателей на указатели на указатели. «Спираль» происходит только от того факта, что эта декларация сгруппировала вещи в скобках таким образом, что они стали чередоваться. Это все справа, потом слева, внутри каждого набора скобок.
— Random832

1
@ Random832 Существует «спиральное правило», и оно охватывает только что упомянутый вами случай, то есть говорит о том, как обращаться с круглыми скобками / массивами и т. Д. Конечно, это не стандартное правило C, а хорошая мнемоника для выяснения, как действовать со сложными декларациями. ИМХО, это чрезвычайно полезно и спасает вас, когда у вас проблемы или когда cdecl.org не может разобрать объявление. Конечно, не следует злоупотреблять такими заявлениями, но хорошо знать, как они анализируются.
— vsoftco

5
@vsoftco Но это не «движение по спирали / по часовой стрелке», если вы только поворачиваетесь, когда достигаете скобок.
— Random832


105

Вид «спирального» правила выпадает из следующих правил приоритета:

T *a[]    -- a is an array of pointer to T
T (*a)[]  -- a is a pointer to an array of T
T *f()    -- f is a function returning a pointer to T
T (*f)()  -- f is a pointer to a function returning T

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

Так что если вы хотите указатель на массив или указатель на функцию, то вам нужно явно сгруппировать *с идентификатором, как в (*a)[]или (*f)().

Тогда вы понимаете это aи fможете быть более сложными выражениями, чем просто идентификаторы; in T (*a)[N], aможет быть простым идентификатором, или это может быть вызов функции вроде (*f())[N]( a-> f()), или это может быть массив типа (*p[M])[N]( a-> p[M]), или это может быть массив указателей на функции вроде (*(*p[M])())[N]( a-> (*p[M])()), и т.п.

Было бы неплохо, если бы оператор косвенности *был постфиксным, а не унарным, что могло бы несколько облегчить чтение деклараций слева направо ( void f[]*()*();безусловно, лучше, чем void (*(*f[])())()), но это не так.

Когда вы сталкиваетесь с таким волосатым объявлением, начните с поиска крайнего левого идентификатора и примените приведенные выше правила приоритета, рекурсивно применяя их к любым параметрам функции:

         f              -- f
         f[]            -- is an array
        *f[]            -- of pointers  ([] has higher precedence than *)
       (*f[])()         -- to functions
      *(*f[])()         -- returning pointers
     (*(*f[])())()      -- to functions
void (*(*f[])())();     -- returning void

signalФункция в стандартной библиотеке, вероятно , образец типа для этого вида безумия:

       signal                                       -- signal
       signal(                          )           -- is a function with parameters
       signal(    sig,                  )           --    sig
       signal(int sig,                  )           --    which is an int and
       signal(int sig,        func      )           --    func
       signal(int sig,       *func      )           --    which is a pointer
       signal(int sig,      (*func)(int))           --    to a function taking an int                                           
       signal(int sig, void (*func)(int))           --    returning void
      *signal(int sig, void (*func)(int))           -- returning a pointer
     (*signal(int sig, void (*func)(int)))(int)     -- to a function taking an int
void (*signal(int sig, void (*func)(int)))(int);    -- and returning void

В этот момент большинство людей говорят «используйте typedefs», что, безусловно, является опцией:

typedef void outerfunc(void);
typedef outerfunc *innerfunc(void);

innerfunc *f[N];

Но...

Как бы вы использовали f в выражении? Вы знаете, что это массив указателей, но как вы используете его для выполнения правильной функции? Вы должны просмотреть typedefs и найти правильный синтаксис. Напротив, «голая» версия довольно приятна, но она точно говорит вам, как использовать f в выражении (а именно (*(*f[i])())();, если предположить, что ни одна функция не принимает аргументы).


7
Спасибо за пример «сигнала», показывающий, что такого рода вещи действительно появляются в дикой природе.
— Justsalt

Это отличный пример.
— Кейси

Мне понравилось ваше fдерево замедления, объясняющее приоритет ... почему-то я всегда получаю
— удовольствие

1
при условии, что ни одна из функций не принимает аргументов : тогда вы должны использовать voidв скобках функции, в противном случае она может принимать любые аргументы.
— хак

1
@haccks: для декларации да; Я говорил о вызове функции.
— Джон Боде

57

В C декларация отражает использование - это то, как это определено в стандарте. Декларация:

void (*(*f[])())()

Утверждение, что выражение (*(*f[i])())()выдает результат типа void. Что значит:

  • f должен быть массивом, так как вы можете индексировать его:

    f[i]
  • Элементы fдолжны быть указателями, так как вы можете разыменовать их:

    *f[i]
  • Эти указатели должны быть указателями на функции без аргументов, так как вы можете вызывать их:

    (*f[i])()
  • Результаты этих функций также должны быть указателями, так как вы можете разыменовать их:

    *(*f[i])()
  • Эти указатели также должны быть указателями на функции без аргументов, так как вы можете вызывать их:

    (*(*f[i])())()
  • Эти указатели функций должны возвращаться void

«Правило спирали» - это просто мнемоника, которая дает другой способ понимания одного и того же.


3
Отличный способ смотреть на это, чего я никогда не видел прежде. +1
— августа

4
Ницца. С этой точки зрения это действительно просто . На самом деле довольно проще, чем что-то вроде vector< function<function<void()>()>* > f, особенно если добавить в std::с. (Но хорошо, пример это ухитрялся ... даже f :: [IORef (IO (IO ()))]выглядит странно.)
— leftaroundabout

1
@TimoDenk: Объявление a[x]указывает, что выражение a[i]является действительным, когда i >= 0 && i < x. Принимая во внимание, что a[]оставляет размер неуказанным, и, следовательно, идентичен *a: он указывает, что выражение a[i](или эквивалентно *(a + i)) является допустимым для некоторого диапазона i.
— Джон Пурди

4
На сегодняшний день это самый простой способ думать о типах Си, спасибо за это
— Алекс Озер

4
Мне это нравится! Намного легче рассуждать, чем глупые спирали. (*f[])()это тип, который вы можете индексировать, затем разыменовывать, затем вызывать, так что это массив указателей на функции.
— Линн

32

Так что это «чтение по спирали» является чем-то действительным?

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

Чтобы расшифровать сложные объявления, запомните эти два простых правила:

  • Всегда читайте объявления изнутри : начинайте с самой внутренней, если есть, круглой скобки. Найдите идентификатор, который объявлен, и начните расшифровывать объявление оттуда.

  • Когда есть выбор, всегда отдавайте предпочтение []и ()больше* : если *предшествует идентификатору и []следует за ним, идентификатор представляет собой массив, а не указатель. Аналогично, если *предшествует идентификатору и ()следует за ним, идентификатор представляет функцию, а не указатель. (Круглые скобки всегда можно использовать для переопределения обычного приоритета над []и ()над *.)

Это правило включает зигзаги от одной стороны идентификатора к другой.

Теперь расшифровываем простую декларацию

int *a[10];

Применяя правило:

int *a[10];      "a is"  
     ^  

int *a[10];      "a is an array"  
      ^^^^ 

int *a[10];      "a is an array of pointers"
    ^

int *a[10];      "a is an array of pointers to `int`".  
^^^      

Давайте расшифруем сложное объявление как

void ( *(*f[]) () ) ();  

применяя вышеуказанные правила:

void ( *(*f[]) () ) ();        "f is"  
          ^  

void ( *(*f[]) () ) ();        "f is an array"  
           ^^ 

void ( *(*f[]) () ) ();        "f is an array of pointers" 
         ^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function"   
               ^^     

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer"
       ^   

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function" 
                    ^^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function returning `void`"  
^^^^

Вот GIF, демонстрирующий, как вы идете (нажмите на картинку для увеличения):

введите описание изображения здесь


Упомянутые здесь правила взяты из книги « Программирование на современном подходе» К.Н. Кинга .


Это похоже на подход стандарта, то есть «декларация отражает использование». Я хотел бы спросить кое-что еще в этом пункте, хотя: Вы предлагаете книгу К. Н. Кинга? Я вижу много хороших отзывов о книге.
— Motun

1
Да. Я предлагаю эту книгу. Я начал программировать из этой книги. Хорошие тексты и проблемы там.
— хак

Можете ли вы привести пример, когда cdecl не понимает объявление? Я думал, что cdecl использует те же правила синтаксического анализа, что и компиляторы, и, насколько я могу судить, он всегда работает.
— Фабио говорит восстановить Монику

@FabioTurati; Функция не может возвращать массивы или функцию. char (x())[5]должно привести к синтаксической ошибке, но cdecl проанализирует ее как: объявить xкак функцию, возвращающую массив 5 ofchar .
— хак

12

Это всего лишь «спираль», потому что в этом объявлении только один оператор на каждой стороне в каждом уровне скобок. Утверждение о том, что вы действуете «по спирали», обычно предполагает, что вы должны чередовать массивы и указатели в объявлении, int ***foo[][][]когда в действительности все уровни массива предшествуют любому из уровней указателя.


Что ж, при «спиральном подходе» вы идете как можно дальше вправо, затем как можно дальше влево и т. Д. Но это часто объясняется ошибочно ...
— Линн

7

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


3
Тем не менее, важно знать, как его анализировать, даже если только знать, как анализировать typedef!
— inetknght

1
@inetknght, способ, которым вы делаете это с typedefs, состоит в том, чтобы сделать их достаточно простыми, чтобы не требовалось никакого анализа.
— СергейА

2
Люди, которые задают подобные вопросы во время интервью, делают это только для того, чтобы погладить свое эго.
— Кейси

1
@JohnBode, и вы сделаете себе одолжение, набрав возвращаемое значение функции.
— SergeyA

1
@JohnBode, я считаю, что вопрос личного выбора не стоит обсуждать. Я вижу ваши предпочтения, у меня все еще есть мои.
— СергейА

7

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

Ссылка: Ван дер Линден, 1994 - Страница 76


1
Это слово не указывает внутри, как во вложении паренов или в одной строке. Он описывает шаблон «змея» с линией LTR, за которой следует линия RTL.
— Potatoswatter

5

Что касается полезности этого, при работе с шеллкодом вы часто видите эту конструкцию:

int (*ret)() = (int(*)())code;
ret();

Хотя этот синтаксис не так сложен, он часто встречается.

Более полный пример в этом вопросе.

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


5

Декларация

void (*(*f[])())()

это просто неясный способ сказать

Function f[]

с участием

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

На практике, вместо ResultFunction и Function потребуется больше описательных имен . Если возможно, я бы также указал списки параметров как void.


4

Я нашел метод, описанный Брюсом Экелом, полезным и простым для понимания:

Определение указателя на функцию

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

void (*funcPtr)();

Когда вы смотрите на такое сложное определение, лучший способ атаковать его - начать с середины и найти выход. «Начать с середины» означает начинать с имени переменной, которая называется funcPtr. «Выходя из положения» означает поиск справа ближайшего предмета (ничего в данном случае; правая скобка не останавливает вас), затем взгляд влево (указатель, обозначенный звездочкой), затем взгляд вправо ( пустой список аргументов, указывающий на функцию, которая не принимает аргументов), затем смотрит влево (void, что указывает на то, что функция не имеет возвращаемого значения). Это движение вправо-влево-вправо работает с большинством объявлений.

Чтобы просмотреть, «начните с середины» («funcPtr - это ...»), перейдите направо (ничего нет - вас остановили правые скобки), перейдите влево и найдите «*» (« ... указатель на ... »), перейдите вправо и найдите пустой список аргументов (« ... функция, не принимающая аргументов ... »), перейдите влево и найдите пустоту (« funcPtr is указатель на функцию, которая не принимает аргументов и возвращает void »).

Вы можете задаться вопросом, почему * funcPtr требует скобок. Если вы их не используете, компилятор увидит:

void *funcPtr();

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

Сложные декларации и определения

Кроме того, как только вы поймете, как работает синтаксис объявлений C и C ++, вы можете создавать гораздо более сложные элементы. Например:

//: C03:ComplicatedDefinitions.cpp

/* 1. */     void * (*(*fp1)(int))[10];

/* 2. */     float (*(*fp2)(int,int,float))(int);

/* 3. */     typedef double (*(*(*fp3)())[10])();
             fp3 a;

/* 4. */     int (*(*f4())[10])();


int main() {} ///:~ 

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

Номер 2 говорит: «fp2 - это указатель на функцию, которая принимает три аргумента (int, int и float) и возвращает указатель на функцию, которая принимает целочисленный аргумент и возвращает float».

Если вы создаете много сложных определений, вы можете использовать typedef. Номер 3 показывает, как typedef сохраняет ввод сложного описания каждый раз. В нем говорится: «fp3 - это указатель на функцию, которая не принимает аргументов и возвращает указатель на массив из 10 указателей на функции, которые не принимают аргументов и возвращают double». Затем он говорит «а является одним из этих типов fp3». typedef обычно полезен для создания сложных описаний из простых.

Номер 4 - это объявление функции вместо определения переменной. В нем говорится: «f4 - это функция, которая возвращает указатель на массив из 10 указателей на функции, которые возвращают целые числа».

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

Взято из: Мышление в C ++ Том 1, второе издание, глава 3, раздел «Адреса функций» Брюса Эккеля.


4

Запомните эти правила для объявлений C,
и приоритет никогда не будет подвергаться сомнению:
начните с суффикса, продолжите с префикса
и прочитайте оба набора изнутри.
- я, середина 1980-х

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

Серьезно, это не трудно научиться делать с первого взгляда; Вы просто должны быть готовы потратить некоторое время на оттачивание навыка. Если вы собираетесь поддерживать или адаптировать C-код, написанный другими людьми, это определенно стоит потратить на это время. Это также забавный трюк для вечеринки, который сводит с ума других программистов, которые этого не изучали.

Для вашего собственного кода: как всегда, тот факт, что что-то может быть написано в виде одной строки, не означает, что так должно быть, если только это не очень распространенный шаблон, ставший стандартной идиомой (такой как цикл копирования строк) , Вы и те, кто следует за вами, будут намного счастливее, если вы будете создавать сложные типы из многоуровневых типов и пошаговых разыменований, а не полагаться на свою способность генерировать и анализировать их «одним махом». Производительность будет такой же хорошей, а удобочитаемость и удобство сопровождения кода будут значительно выше.

Знаешь, могло быть и хуже. Было юридическое заявление PL / I, которое начиналось с чего-то вроде:

if if if = then then then = else else else = if then ...

2
Заявление PL / I было IF IF = THEN THEN THEN = ELSE ELSE ELSE = ENDIF ENDIFи анализируется как if (IF == THEN) then (THEN = ELSE) else (ELSE = ENDIF).
— Коул Джонсон

Я думаю, что была версия, которая продвинулась на один шаг дальше, используя условное выражение IF / THEN / ELSE (эквивалент C? :), которое получило третий набор в миксе ... но это было несколько десятилетий и, возможно, зависит от конкретного диалекта языка. Дело в том, что любой язык имеет хотя бы одну патологическую форму.
— keshlam

4

Я был первым автором спирального правила, которое я написал очень много лет назад (когда у меня было много волос :), и было честью, когда оно было добавлено в cfaq.

Я написал спиральное правило, чтобы моим студентам и коллегам было легче читать декларации С "в их голове"; т.е. без использования программных инструментов, таких как cdecl.org и т. д. Я никогда не намеревался объявлять, что спиральное правило является каноническим способом синтаксического анализа выражений Си. Тем не менее, я рад видеть, что это правило помогло буквально тысячам студентов и практикующих программистов на C за эти годы!

Для записи,

На многих сайтах, в том числе Линусом Торвальдсом (кем-то, кого я безмерно уважаю), «правильно» неоднократно идентифицировалось, что бывают ситуации, когда мое спиральное правило «нарушается». Наиболее распространенным является:

char *ar[10][10];

Как отмечали другие в этом потоке, правило может быть обновлено, чтобы сказать, что когда вы сталкиваетесь с массивами, просто используйте все индексы, как если бы они были написаны так:

char *(ar[10][10]);

Теперь, следуя спиральному правилу, я получу:

«ar - это двумерный массив указателей на символ 10x10»

Я надеюсь, что спиральное правило будет полезным для изучения C!

PS:

Я люблю образ "С не сложно" :)


3
  • недействительным (*(*f[]) ()) ()

Разрешение void>>

  • (*(*f[]) ()) () = пусто

Resoiving ()>>

  • (* (*f[]) ()) = функция, возвращающая (void)

Разрешение *>>

  • (*f[]) () = указатель на (функция, возвращающая (void))

Разрешение ()>>

  • (* f[]) = функция, возвращающая (указатель на (функция, возвращающая (void)))

Разрешение *>>

  • f[] = указатель на (функция, возвращающая (указатель на (функция, возвращающая (void))))

Разрешение [ ]>>

  • f = массив (указатель на (функция, возвращающая (указатель на (функция, возвращающая (void)))))
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.