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

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

some c code

Мне показалось, что это сбивает с толку и задается вопросом, являются ли такие коды практичными. Я искал картинку и нашел другую картинку в this reddit, и вот эта картина:

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

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

Существует вопрос о "спиральном правиле", но я не просто спрашиваю о том, как он применяется, и как выражения читаются с этим правилом. Я также сомневаюсь в использовании таких выражений и верности правила. В связи с этим некоторые интересные ответы уже опубликованы.

Ответ 1

Существует правило, называемое "По часовой стрелке/спиральному правилу" , чтобы помочь найти значение сложной декларации.

Из c-faq:

Существует три простых шага:

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

    [X] или []
    = > Array X size of... или Array undefined размер...

    (type1, type2)
    = > передача функции type1 и type2...

    *
    = > указатель для...

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

  • Сначала сначала разрешайте что-либо в скобках!

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

Также обратите внимание, что для того, чтобы помочь вам, есть также веб-сайт:

http://www.cdecl.org

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

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

выводится:

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

EDIT:

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

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

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

Ответ 2

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

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 могут быть более сложными выражениями, чем просто идентификаторы; в T (*a)[N], a может быть простым идентификатором, или это может быть вызов функции типа (*f())[N] (af()), или он может быть массивом типа (*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

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

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

innerfunc *f[N];

Но...

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

Ответ 3

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

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

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

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

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

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

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

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

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

"Спиральное правило" - это всего лишь мнемоника, которая дает другой способ понять одно и то же.

Ответ 4

Итак, это "чтение спирально" - это что-то действительно?

Применение спирального правила или использование 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, демонстрирующий, как вы идете (нажмите на изображение для увеличения):

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


Описанные здесь правила взяты из книги C Программирование Современный подход KN KING.

Ответ 5

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

Ответ 6

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

Ответ 7

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

Ссылка: Van der Linden, 1994 - Страница 76

Ответ 8

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

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

Хотя это не так синтаксически сложно, этот конкретный шаблон очень много.

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

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

Ответ 9

Объявление

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

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

Function f[]

с

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

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

Ответ 10

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

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

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

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

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

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

Ответ 11

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

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

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

void (*funcPtr)();

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

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

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

void *funcPtr();

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

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

Как в стороне, как только вы выясните, как синтаксис объявления 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 - это указатель на функция, которая принимает целочисленный аргумент и возвращает указатель на массив из десяти указателей void."

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

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

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

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

Взято из: Думая на С++ Том 1, второе издание, глава 3, раздел "Адреса функций" Брюса Эккеля.

Ответ 12

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

Разрешение void

  • (*(*f[]) ())() = void

Реабилитация ()

  • (* (*f[]) ()) = функция return (void)

Разрешение *

  • (*f[])() = указатель на (функция return (void))

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

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

Разрешение *

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

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

  • f = массив (указатель на (возврат функции (указатель на (функция return (void)))))

Ответ 13

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

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

Для записи

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

char *ar[10][10];

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

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

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

"ar представляет собой двумерный массив указателей 10x10 для char"

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

P.S:.

Мне нравится образ "C is not hard":)