C并不那么难:void(*(* f [])())()


188

我今天刚看一张照片,并认为我会感谢您的解释。所以这是图片:

一些C代码

我感到困惑,并想知道这样的代码是否可行。我用图片搜索了一下,然后在 reddit条目中找到了另一张图片,是该图片:

一些有趣的解释

因此,这种“螺旋式阅读”是否有效?这是C编译器解析的方式吗?
如果对这个怪异的代码有更简单的解释,那就太好了。
除此以外,这类代码是否有用?如果是这样,何时何地?

关于“螺旋规则” 存在一个问题,但我不仅在询问它的应用方式或如何使用该规则读取表达式。我也质疑这种表达方式的使用以及螺旋规则的有效性。关于这些,已经发布了一些不错的答案。


9
解释如何更简单?它涵盖了fw /每个关键点的几个词定义的所有方面。
斯科特·亨特

29
也许C很难?实际上,它声明f为指向可以接受任何参数的函数的指针数组..如果是void (*(*f[])(void))(void);,则是,它将采取不接受参数的函数...
txtechhelp 2015年

18
在实践中,请勿编写此类晦涩的代码。使用typedef签名
Basile Starynkevitch 2015年

4
任何涉及函数指针的声明都很难。这并不意味着普通的C或C ++以同样的方式很难。其他语言以不同的方式解决了这一问题,包括没有函数指针,这在某些情况下可能是一个重大遗漏
Kate Gregory

20
如果您起眼睛,看起来就像LISP。
user2023861 2015年

Answers:


117

有一个规则称为“顺时针/螺旋规则”,以帮助查找复杂声明的含义。

c-faq

遵循三个简单步骤:

  1. 从未知元素开始,沿螺旋/顺时针方向移动;在遇到以下要素时,将其替换为相应的英文陈述:

    [X][]
    => 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];对于螺旋规则,忽略了[]优先于的事实*

在数组数组前面时,可以先应用显式括号,然后再应用螺旋规则。例如:由于优先级而与(也是有效的C)int **x[1][2];相同,int **(x[1][2]);然后螺旋规则正确地读取它,因为“ x是指向int的指针的数组2的数组1”是正确的英语声明。

请注意,这个问题也被包括在本答案詹姆斯·甘孜(中指出haccks在评论)。


5
我希望cdecl.org更好
Grady Player

8
没有“螺旋规则” ...“ int *** foo [] [] []”定义了一个指向指针数组的指针数组。“螺旋形”仅来自以下事实:该声明恰好将括号中的事物组合在一起,从而导致它们交替出现。每组括号中的所有内容均在右侧,然后在左侧。
2016年

1
@ Random832有一个“螺旋规则”,它涵盖了您刚才提到的情况,即讨论了如何处理括号/数组等。带有复杂的声明。恕我直言,它非常有用,当遇到麻烦或cdecl.org无法解析声明时,可以为您省钱。当然,不应滥用此类声明,但是知道如何解析它们是很好的。
vsoftco '16

5
@vsoftco但是,如果仅在到达括号时才转身,则不是“沿螺旋/顺时针方向移动”。
Random832 '16

2
哦,您应该提到螺旋规则不是普遍的
2016年

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在表达式中使用?您知道它是一个指针数组,但是如何使用它执行正确的功能?您必须仔细检查typedef并找出正确的语法。相比之下,“裸”版本看上去很麻烦,但是它可以告诉您确切如何在表达式中使用 f(即(*(*f[i])())();,假设两个函数都不带参数)。


7
感谢您提供“信号”的示例,表明此类事情确实出现了。
Justsalt 2015年

这是一个很好的例子。
凯西

我喜欢您的f减速树,解释了优先顺序...出于某种原因,我总是从ASCII艺术中
受益匪浅

1
假设两个函数都不带参数:那么您必须void在函数中使用括号,否则它可以带任何参数。
2013年

1
@haccks:声明,是的;我说的是函数调用。
John Bode

57

在C语言中,声明反映了用法-这就是标准中定义的方式。声明:

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

断言表达式(*(*f[i])())()产生type的结果void。意思是:

  • f 必须是一个数组,因为您可以对其进行索引:

    f[i]
  • 的元素f必须是指针,因为您可以取消引用它们:

    *f[i]
  • 这些指针必须是不带参数的函数的指针,因为您可以调用它们:

    (*f[i])()
  • 这些函数的结果也必须是指针,因为您可以取消引用它们:

    *(*f[i])()
  • 这些指针必须是不带参数的函数的指针,因为您可以调用它们:

    (*(*f[i])())()
  • 这些函数指针必须返回 void

“螺旋规则”只是一个助记符,它提供了一种不同的理解同一事物的方式。


3
从未见过的绝佳查看方式。+1
tbodt

4
真好 这样看来,确实很简单。实际上比之类的要容易得多vector< function<function<void()>()>* > f,特别是如果您添加std::s。(但是,这个例子人为的……甚至f :: [IORef (IO (IO ()))]看起来很奇怪。)
左右左转

1
@TimoDenk:声明在时a[x]表示该表达式a[i]有效i >= 0 && i < x。而未a[]指定大小,因此与相同*a:它表示表达式a[i](或等效地*(a + i))在的一定范围内有效i
乔恩·普迪

4
到目前为止,这是考虑C类型最简单的方法,这很感谢
Alex Ozer

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演示您的操作方法(单击图像可查看大图):

在此处输入图片说明


这里提到的规则摘自KN KING所著的C Programming A Modern Approach


就像标准的方法一样,即“声明反映用法”。我现在想问一下其他问题:您是否推荐KN King的书?我看到很多关于这本书的好评。
Motun,2016年

1
是的 我建议那本书。我从那本书开始编程。好文字和那里的问题。
2016年

您能否提供cdecl无法理解声明的示例?我认为cdecl使用与编译器相同的解析规则,据我所知它始终有效。
Fabio说恢复莫妮卡

@FabioTurati; 函数不能返回数组或函数。char (x())[5]应该会导致语法错误,但是cdecl将其解析为:声明x为函数,返回的数组5char

12

这只是一个“螺旋”,因为在此声明中,每个括号内的每一侧恰好只有一个运算符。声称您“螺旋式”进行通常会建议您在声明中在数组和指针之间交替,而int ***foo[][][]实际上所有数组级别都在任何指针级别之前。


好吧,在“螺旋式方法”中,您会尽可能地向右走,然后尽可能地向左走,等等。但这常常被错误地解释。
Lynn

7

我怀疑这样的构造在现实生活中会不会有用。我什至讨厌他们作为普通开发人员的面试问题(对于编译器编写者来说可能不错)。应该改用typedefs。


3
尽管如此,重要的是要知道如何解析它,即使只是知道如何解析typedef!
inetknght

1
@inetknght,使用typedef的方式是使它们足够简单,从而无需解析。
SergeyA 2015年

2
在面试中问这些类型问题的人只会抚摸自己的自我。
凯西2015年

1
@JohnBode,您可以通过类型定义函数的返回值来帮自己一个忙。
SergeyA 2015年

1
@JohnBode,我发现这是个人选择的问题,不值得争论。我看到了您的偏好,我仍然有我。
SergeyA 2015年


5

关于此功能的有用性,在使用shellcode时,您会看到很多这样的结构:

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

尽管语法上没有那么复杂,但是这种特殊模式却出现了很多。

更完整的示例 这样的问题。

因此,尽管原始图片在某种程度上的有用性值得怀疑(我建议应该大幅度简化任何生产代码),但确实有一些语法构造。


5

报关单

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

只是一种晦涩的说法

Function f[]

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

实际上,将需要更多描述性名称,而不是ResultFunctionFunction。如果可能的话,我还将参数列表指定为void


4

我发现布鲁斯·埃克尔(Bruce Eckel)描述的方法很有帮助,并且易于遵循:

定义函数指针

要定义指向没有参数也没有返回值的函数的指针,请说:

void (*funcPtr)();

当您查看这样的复杂定义时,最好的攻击方法是从中间开始并逐步解决。“从中间开始”是指从变量名funcPtr开始。“解决问题”是指向右看最近的项(在这种情况下,什么都没有;右括号会使您简短),然后向左看(星号表示的指针),然后向右看(空的参数列表,指示没有参数的函数),然后向左看(无效,表明该函数没有返回值)。此左右左右运动适用于大多数声明。

要进行审查,请“从中间开始”(“ funcPtr是一个...”),向右转到(什么都没有-您被右括号停住了),向左转到并找到“ *”(“ ...指向...的指针),向右查找空的参数列表(“ ...不带参数的函数...”),向左查找空白(“ funcPtr为指向不带任何参数并返回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个void指针的数组的指针的函数的指针。”

数字2表示“ fp2是指向带有三个参数(int,int和float)的函数的指针,并返回指向带有整数参数并返回浮点数的函数的指针。”

如果要创建许多复杂的定义,则可能要使用typedef。数字3显示了typedef如何节省每次键入复杂描述的时间。它说:“ fp3是指向不带任何参数的函数的指针,并返回一个指向由10个指针组成的数组的指针,该数组不带参数而返回双精度值。” 然后说:“ a是这些fp3类型之一。” 通常,typedef可用于从简单的描述构建复杂的描述。

4号是函数声明,而不是变量定义。它说:“ f4是一个返回指向10个指针的数组的函数,该数组指向返回整数的函数。”

您将很少需要这些复杂的声明和定义。但是,如果您要弄清楚它们,那么即使您在现实生活中遇到的那些稍微复杂的问题,也不会受到轻微的干扰。

摘自:布鲁斯·埃克尔(Bruce Eckel)的《 C ++卷第1卷,第二版,第三章》的“函数地址”部分。


4

请记住C声明的这些规则,
并且优先级永远不会被怀疑:
从后缀开始,以前缀开头,
然后从内而外读取这两个集合。
-我,1980年代中期

当然,除非括号中有修改。并请注意,声明这些语法的语法与使用该变量获取基类实例的语法完全相同。

认真地讲,这并不难学。您只需要愿意花一些时间来练习这项技能。如果您要维护或改编其他人编写的C代码,那绝对值得投资这段时间。这也是弄乱其他尚未学习的程序员的有趣聚会技巧。

对于您自己的代码:和往常一样,可以将某些东西编写为单行的事实并不意味着应该如此,除非它是已成为标准习语的极其常见的模式(例如字符串复制循环) 。您和跟随您的人将会非常如果您使用分层的typedef和逐步的取消引用来构建复杂的类型,而不是依靠您的能力来“一次暴涨”来生成和解析这些复杂的类型,那么您高兴。性能将一样好,代码的可读性和可维护性将大大提高。

您可能会更糟。有合法的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 '16

4

我恰好是多年前(当我有很多头发时:)写的螺旋法则的原始作者,当它被添加到cfaq中时感到很荣幸。

我写螺旋规则是为了让我的学生和同事更容易地“读懂” C声明。也就是说,不必使用cdecl.org等软件工具。我从来没有想过要声明螺旋规则是解析C表达式的规范方法。不过,我很高兴看到该规则多年来已经帮助了成千上万的C编程学生和实践者!

作为记录,

在许多站点上,包括莱纳斯·托瓦尔兹(Linus Torvalds)(我非常尊敬的人),已经多次“正确”地确定了在某些情况下我的螺旋规则“崩溃”了。最常见的是:

char *ar[10][10];

正如该线程中的其他人所指出的那样,可以更新规则以说,当您遇到数组时,只需消耗所有索引就好像写成这样:

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

现在,按照螺旋规则,我将得到:

“ ar是指向char的10x10二维指针数组”

我希望螺旋规则在学习C语言中继续有用!

PS:

我喜欢“ C并不难”的形象:)


3
  • 虚空 (*(*f[]) ()) ()

解决void>>

  • (*(*f[]) ()) ()=无效

接收()>>

  • (* (*f[]) ())=返回的函数(无效)

解决*>>

  • (*f[]) ()=指向(函数返回(void)的指针)

解决()>>

  • (* f[])=函数返回(指向(函数返回(void)的指针))

解决*>>

  • f[] =指向(函数返回(指向(函数返回(void))的指针))的指针)

解决[ ]>>

  • f =(指向(函数返回(指向(函数返回(void)))的指针)的数组
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.