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


188

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

якийсь код C

Я вважав це заплутаним і цікавився, чи такі коди колись практичні. Я гуглив картинку і знайшов ще одну картинку у цій редакції reddit, і ось ця картинка:

кілька цікавих пояснень

Тож це "читання спірально" щось дійсне? Це як розбирати компілятори C?
Було б чудово, якщо є більш прості пояснення цього дивного коду.
Окрім усіх, чи можуть корисні такі коди? Якщо так, то де і коли?

Є питання про "правило спіралі", але я не просто запитую про те, як воно застосовується або як читаються вирази за допомогою цього правила. Я ставлю під сумнів використання таких виразів та дійсність правила спіралі. З приводу цього вже розміщено кілька приємних відповідей.


9
Як пояснення могло бути простішим? Він охоплює всі аспекти визначення fw / пару слів для кожного ключового моменту.
Скотт Хантер

30
Можливо, С важко? Це насправді оголошує fяк масив покажчиків на функції, які можуть приймати будь-який аргумент .. якби воно було void (*(*f[])(void))(void);, то так, це були б функції, які не беруть аргументів ...
txtechhelp

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

4
будь-яке оголошення, що включає вказівники функції, може бути важким. Це не означає, що нормально C або C ++ важко таким же чином. Інші мови вирішують це по-різному, включаючи відсутність функціональних покажчиків, що в деяких випадках може бути істотним упущенням
Кейт Грегорі

20
Якщо ви косаєте очима, це виглядає як LISP.
користувач2023861

Відповіді:


117

Існує правило, яке називається "Правило за годинниковою стрілкою / спіраль", яке допомагає знайти значення складної декларації.

Від c-faq :

Три наступні кроки:

  1. Починаючи з невідомого елемента, рухайтесь у напрямку спіраль / за годинниковою стрілкою; при згадці наступні елементи замінюють їх відповідними англійськими висловлюваннями:

    [X]або []
    => Масив X розміром ... або Не визначений розмір масиву ...

    (type1, type2)
    => функція передачі type1 і type2, що повертається ...

    *
    => покажчик (и) на ...

  2. Продовжуйте робити це в напрямку спіралі / за годинниковою стрілкою, поки всі жетони не будуть покриті.

  3. Завжди спочатку вирішуйте що-небудь в дужках!

Ви можете перевірити посилання вище для прикладів.

Також зауважте, що для того, щоб допомогти вам, існує також веб-сайт під назвою:

http://www.cdecl.org

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

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

він виводить:

оголосити f як масив покажчика на функцію повернення вказівника на функцію, що повертається недійсною

Редагувати:

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

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

Зверніть увагу , що це питання також розглядається в цьому відповіді по Джеймс Kanze (вказав haccks в коментарях).


5
Я б хотів, щоб cdecl.org був кращим
Граді Гравець

8
Не існує "правила спіралі" ... "int *** foo [] [] []" визначає масив масивів масивів покажчиків до покажчиків на покажчики. "Спіраль" виходить лише з того, що ця декларація трапилася для групування речей у дужках таким чином, що вони змусили їх чергуватися. Це все праворуч, потім ліворуч, у межах кожного набору дужок.
Випадково832

1
@ Random832 Існує "правило спіралі", і воно охоплює випадок, про який ви вже згадали, тобто говорить про те, як боротися з дужками / масивами тощо. зі складними деклараціями. IMHO, це надзвичайно корисно і заощаджує вас, коли трапляєте проблеми або коли cdecl.org не може проаналізувати декларацію. Звичайно, не слід зловживати такими деклараціями, але добре знати, як вони розбираються.
vsoftco

5
@vsoftco Але це не "рух по спіралі / за годинниковою стрілкою", якщо ви обертаєтеся лише тоді, коли доходите до дужок.
Випадково832

2
о, ви повинні згадати, що правило спіралі не є універсальним .
hack

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можуть бути складніші вирази, ніж просто ідентифікатори; в 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дерево уповільнення, пояснюючи пріоритет ... чомусь я завжди отримую удар з ASCII-мистецтва, особливо коли йдеться про пояснення речей :)
txtechhelp

1
припускаючи, що жодна функція не бере аргументів : тоді вам доведеться використовувати voidв дужках функцій, інакше вона може приймати будь-які аргументи.
hack

1
@haccks: для декларації, так; Я говорив про виклик функції.
Джон Боде

57

У З, використання дзеркал декларації - саме так це визначено в стандарті. Декларація:

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

Це твердження, що вираз (*(*f[i])())()створює результат типу void. Що означає:

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

    f[i]
  • Елементи fmust повинні бути покажчиками, оскільки ви можете знецінити їх:

    *f[i]
  • Ці вказівники повинні бути вказівниками на функції, які не мають аргументів, оскільки ви можете їх викликати:

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

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

    (*(*f[i])())()
  • Ці покажчики функцій повинні повернутися void

"Правило спіралі" - це просто мнемоніка, яка забезпечує інший спосіб розуміння одного і того ж.


3
Чудовий погляд на це, якого я ніколи не бачив. +1
tbodt

4
Приємно. Побачене таким чином, це дійсно просто . Насправді швидше, ніж щось подібне vector< function<function<void()>()>* > f, особливо якщо ви додасте в std::s. (Але добре, приклад це примудрявся ... навіть 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, що демонструє, як ви рухаєтесь (натисніть на зображення для збільшення зображення):

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


Правила, згадані тут, взяті з книги " Програмування сучасного підходу" КН КІНГ .


Це так само, як стандартний підхід, тобто "використання дзеркальних дзеркал". Я хотів би запитати ще щось у цьому пункті: Ви пропонуєте книгу К.Н. Кінга? Я бачу багато приємних відгуків про книгу.
Мотун

1
Так. Я пропоную цю книгу. Я почав програмувати з цієї книги. Хороші тексти та проблеми там.
hack

Чи можете ви навести приклад того, що cdecl не розуміє декларацію? Я думав, що cdecl використовує ті самі правила розбору, що і компілятори, і наскільки я можу сказати, це завжди працює.
Фабіо каже, що повернеться до Моніки

@FabioTurati; Функція не може повертати масиви чи функції. char (x())[5]повинно призвести до синтаксичної помилки, але, cdecl проаналізує його як: оголосити xфункцією, що повертає масив 5char .
hack

12

Це лише "спіраль", тому що в цій декларації буває лише один оператор з кожної сторони в межах кожного рівня дужок. Стверджуючи, що ви переходите "по спіралі", як правило, пропонується вам чергувати масиви та покажчики в декларації, int ***foo[][][]коли насправді всі рівні масиву переходять до будь-якого з рівнів вказівника.


Ну, при "спіральному підході" ви йдете якомога далі праворуч, потім вліво, наскільки можете, і т. Д. Але це часто пояснюється помилково ...
Лінн,

7

Сумніваюсь, подібні конструкції можуть мати будь-яку користь у реальному житті. Я навіть вважаю їх питаннями інтерв'ю для звичайних розробників (ймовірно, добре для авторів-компіляторів). замість цього слід використовувати typedefs.


3
Тим не менш, важливо знати, як розібрати його, навіть якщо тільки знати, як розібрати typedef!
inetknght

1
@inetknght, так, як ви це робите з typedefs, - це зробити їх досить простими, щоб не було необхідності аналізу.
СергійА

2
Люди, які задають такі питання під час інтерв'ю, роблять це лише для того, щоб погладити їхні Его.
Кейсі

1
@JohnBode, і ви зробите собі прихильність, набравши параметр повернення значення функції.
СергійА

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

7

Як випадковий дріб'язковий факт, вам може бути забавно знати, що в англійській мові є фактичне слово, яке описує, як читаються декларації C: Бустрофедонічно , тобто чергування справа наліво з ліворуч на право.

Довідка: Ван дер Лінден, 1994 - Сторінка 76


1
Це слово не вказує в якості в вкладеними шляхом або круглі дужки в одному рядку. Він описує візерунок "змія" з лінією LTR, за якою слідує лінія RTL.
Potatoswatter

5

Щодо корисності цього, працюючи з shellcode, ви багато бачите цю конструкцію:

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

Хоча це не так синтаксично складно, ця конкретна закономірність з'являється багато.

Більш повний приклад у цьому питанні.

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


5

Декларація

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

це лише незрозумілий спосіб висловлювання

Function f[]

з

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

На практиці потрібні більш описові назви замість ResultFunction та Function . Якщо можливо, я б також вказав списки параметрів як void.


4

Я знайшов метод, описаний Брюсом Еккелем, як корисний і простий у дотриманні:

Визначення покажчика функції

Щоб визначити покажчик на функцію, яка не має аргументів і не повертає значення, ви говорите:

void (*funcPtr)();

Коли ви дивитесь на таке складне визначення, як кращий, найкращий спосіб напасти на нього - почати з середини та виходити назовні. "Починати з середини" означає починати з назви змінної, яка є funcPtr. "Виправити вихід" означає шукати праворуч найближчий предмет (нічого в цьому випадку; права дужка не зупиняє вас), потім дивитися вліво (вказівник, позначений зірочкою), потім дивитися вправо ( порожній список аргументів, що вказує на функцію, яка не приймає аргументів), потім дивиться вліво (недійсна, що означає, що функція не має значення повернення). Цей рух вліво-вправо-вправо працює з більшістю декларацій.

Щоб ознайомитись, "починайте з середини" ("funcPtr - це ..."), перейдіть праворуч (нічого там - вас зупиняє права дужка), перейдіть ліворуч і знайдіть "*" (" ... вказівник на ... "), перейдіть праворуч і знайдіть порожній список аргументів (" ... функція, яка не бере аргументів ... "), перейдіть ліворуч і знайдіть порожнечу (" funcPtr is вказівник на функцію, яка не бере аргументів і повертає недійсність ”).

Вам може бути цікаво, чому * funcPtr вимагає дужок. Якщо ви їх не використовували, компілятор побачив би:

void *funcPtr();

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

Складні декларації та визначення

Крім того, коли ви зрозумієте, як працює синтаксис декларацій 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) і повертає вказівник на функцію, яка приймає цілий аргумент і повертає поплавок".

Якщо ви створюєте безліч складних визначень, можливо, ви хочете використовувати typedef. Число 3 показує, як typedef економить, вводячи складний опис кожного разу. У ньому сказано: "fp3 - це вказівник на функцію, яка не бере аргументів, і повертає вказівник на масив з 10 покажчиків на функції, які не беруть аргументів і повертають парні". Потім він говорить: "a - це один із цих типів 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? :), який отримав третій набір у суміш ... але минуло кілька десятиліть і, можливо, залежав від конкретного діалекту мови. Справа залишається, що будь-яка мова має хоча б одну патологічну форму.
кешлам

4

Я, мабуть, є оригінальним автором спірального правила, про яке я писав так багато років тому (коли у мене було багато волосся :) і був шанований, коли його додали до cfaq.

Я написав правило спіралі як спосіб полегшити моїм студентам та колегам читання декларацій С "у голові"; тобто без використання програмних засобів, таких як cdecl.org тощо. Ніколи не було наміру заявляти, що правило спіралі є канонічним способом розбору виразів C. Я, хоча, радий бачити, що це правило допомогло буквально тисячам студентів та практиків програмування на С!

Для запису,

На багатьох сайтах було "правильно" виявлено багато разів, зокрема Лінус Торвальдс (той, кого я дуже поважаю), що є ситуації, коли моє правило спіралі "ламається". Найпоширеніша істота:

char *ar[10][10];

Як вказували інші в цій темі, правило можна оновити, щоб сказати, що коли ви стикаєтесь з масивами, просто споживайте всі індекси так, ніби написані так:

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

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

"ar - двовимірний масив вказівників 10x10 на char"

Я сподіваюся, що спіральне правило продовжує свою корисність у навчанні С!

PS:

Мені подобається образ "С не важко" :)


3
  • порожнеча (*(*f[]) ()) ()

Розв’язання void>>

  • (*(*f[]) ()) () = недійсна

Відновлення ()>>

  • (* (*f[]) ()) = функція, що повертається (недійсна)

Розв’язання *>>

  • (*f[]) () = вказівник на (функція, що повертається (недійсна))

Розв’язання ()>>

  • (* f[]) = функція, що повертається (вказівник на (функція, що повертається (недійсна)))

Розв’язання *>>

  • f[] = вказівник на (функція, що повертається (вказівник на (функція, що повертається (недійсна))))

Розв’язання [ ]>>

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