C不难: void ( *( *f[] ) () ) ()

202

我今天看到了一张图片,希望能得到解释。以下是该图片:

some c code

翻译:"C并不难:void (*(*f[])())()定义 f 为一个未指定大小的函数指针数组,它返回指向返回 void 的函数指针的指针。

我感到困惑,想知道这样的代码是否实用。我在谷歌上搜索了这张图片,并在Reddit上找到另一张图片,以下是该图片:

some interesting explanation

翻译:"因此,符号可以读作:f [] * () * () voidf 是一个指针数组,它不采用参数,并返回一个不采用参数且返回类型为 void 的指针。"

所以这种"螺旋阅读"是有效的吗?这是C编译器的解析方式吗?
如果有更简单的解释,那就太好了。
除此之外,这种代码有用吗?如果有,那在什么情况下使用?

关于"spriral rule",已经有一个问题提出,但我不仅仅想问如何应用它以及如何阅读该规则的表达式。我还质疑这种表达式和螺旋规则的有效性。关于这些问题,已经有一些很好的回答。


9
怎样才能更简单地解释呢?每个关键点只需几个词,就可以涵盖f的定义各个方面。 - Scott Hunter
32
C语言很难吗?实际上这声明了f是一个指向可以取任何参数的函数指针数组,如果它是void (*(*f[])(void))(void), 那么就是没有参数的函数了。 - txtechhelp
19
在实践中,不要编写这样晦涩难懂的代码。使用typedef来定义函数签名。 - Basile Starynkevitch
4
任何涉及函数指针的声明都可能很难。这并不意味着普通的 C 或 C++ 也是以同样的方式难处理的。其他编程语言采用不同的方法来解决这个问题,包括不使用函数指针,在某些情况下这可能是一个重要的缺陷。 - Kate Gregory
24
如果你眯起眼睛看,它看起来像LISP。 - user2023861
显示剩余15条评论
13个回答

120

有一个规则叫做"顺时针/螺旋法则",它可以帮助找到复杂声明的含义。

来自c-faq:

遵循以下三个简单步骤:

  1. 从未知元素开始,沿着螺旋/顺时针方向移动;当遇到以下元素时,请用相应的英语语句替换它们:

    [X][]
    => 数组X大小为...或数组大小未定义...

    (type1, type2)
    => 函数传递类型1和类型2并返回...

    *
    => 指向...的指针(s)

  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的指针”的英文声明。

请注意,这个问题也在此答案中得到了讨论,由James Kanze撰写(由评论中的haccks指出)。

5
我希望cdecl.org能够更好。 - Grady Player
11
“螺旋规则”并不存在...“int ***foo[][][]”定义了一个指向指针的数组,该数组又嵌套了三层数组。之所以称其为“螺旋”,只是因为这个声明语句恰好按照一种方式将内容分组,使它们交替出现。在每组括号中,先是右边的内容,接着是左边的。 - Random832
1
@Random832,有一个“螺旋规则”,它涵盖了你刚才提到的情况,即讨论如何处理括号/数组等。当然,这不是标准C规则,但对于解决复杂声明的助记符非常有用。在我看来,它非常有用,可以在遇到麻烦或cdecl.org无法解析声明时挽救你。当然,人们不应滥用这样的声明,但了解它们是如何解析的是很好的。 - vsoftco
5
如果只有在到达括号时才转向,那么它并不是“顺时针/螺旋方向移动”。 - Random832
2
哇,你应该提到螺旋规则不是普遍适用的 - haccks
显示剩余3条评论

118
“螺旋”规则实际上是以下优先级规则的自然结果:
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)()
然后你会意识到af可以是更复杂的表达式,而不仅仅是标识符;在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

在这种情况下,大多数人会说“使用typedef”,这当然是一种选择:

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

innerfunc *f[N];

但是...

你知道它是一个指针数组,但是如何在表达式中使用 f呢?您必须查看typedef并仔细推断出正确的语法。相比之下, "naked"版本非常难懂,但它确切地告诉您如何在表达式中使用 f(即(*(*f[i])())();,假设两个函数都不带参数)。


7
感谢您提供“信号”这个例子,说明这些东西确实出现在野外。 - Justsalt
那是一个很好的例子。 - Casey
我喜欢你的 f 声明树,解释了优先级...出于某种原因,当涉及到解释事物时,我总是对 ASCII 艺术感到兴奋 :) - txtechhelp
1
假设两个函数都不带参数:那么你必须在函数括号中使用 void,否则它可以接受任何参数。 - haccks
2
@haccks:关于声明,是的;我说的是函数调用。 - John Bode
@JohnBode; 哦!对不起,我的错。 - haccks

65

在 C 语言中,声明和使用是相互对应的——这就是标准规定的方式。下面是一个声明的例子:

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

这是一个关于表达式 (*(*f[i])())() 返回类型为 void 的断言。这意味着:

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

  • f[i]
    
  • 因为可以对它们进行解引用,所以f的元素必须是指针:

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

  • (*f[i])()
    
    这些函数的结果也必须是指针,因为你可以对它们进行解引用:
    *(*f[i])()
    
  • 这些指针必须同时也是指向不带参数的函数的指针,因为你可以调用它们:

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

“螺旋规则”只是一种记忆法,提供了一种不同的理解方式。


5
这是我从未见过的很棒的看法。+1 - tbodt
5
不错。这样看来,它确实很简单。实际上比像 vector< function<function<void()>()>* > f 这样的东西要容易得多,尤其是当你加上 std:: 时。(但好吧,这个例子确实有点牵强...即使是 f :: [IORef (IO (IO ()))] 看起来也很奇怪。) - leftaroundabout
谢谢你的回答!有了f[i]的写法,这对我来说肯定是有意义的,但我不明白为什么代码的原始部分(*f[])()是有效的。这不是在没有定义要选择哪个元素的情况下调用数组的函数吗? - Timo Denk
1
@TimoDenk:声明a[x]表示当i >= 0 && i < x时,表达式a[i]是有效的。而a[]则未指定大小,因此与*a相同:它表示表达式a[i](或等效地*(a + i))对于某些范围的i是有效的。 - Jon Purdy
4
这绝对是思考C语言类型最简单的方法,感谢这个方法。 - Alex Ozer
4
我喜欢这个!比愚蠢的螺旋线容易推理。(*f[])()是一种可以索引、解引用和调用的类型,因此它是一个函数指针的数组。 - Lynn

42

那么这个“螺旋阅读法”是有效的吗?

应用螺旋规则或使用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,展示了如何进行操作(点击图像查看更大的视图):

enter image description here


这里提到的规则来自K.N KING所著《C程序设计现代方法》一书。


1
这就像标准的方法一样,即“声明反映使用”。不过此时我想问另外一件事:你建议阅读K.N. King的书吗?我看到了很多好评。 - Motun
2
是的,我建议那本书。我从那本书开始学习编程。里面有很好的文本和问题。 - haccks
你能提供一个 cdecl 无法理解声明的例子吗?我以为 cdecl 使用与编译器相同的解析规则,就我所知它总是有效的。 - Fabio says Reinstate Monica
@FabioTurati; 一个函数不能返回数组或函数。char (x())[5] 应该导致语法错误,但是,cdecl将其解析为:声明 x 为返回 char 数组 5 的函数 - haccks

12

之所以这只是一个“螺旋”,是因为在这个声明中,每个括号级别内每一侧只有一个运算符。声称你要“螺旋”进行操作通常意味着在声明int ***foo [] [] []时你会在数组和指针之间交替,但实际上所有的数组级别都在任何指针级别之前。


在“螺旋式方法”中,您会尽可能向右走,然后尽可能向左走,依此类推。但是通常解释不正确... - Lynn

7

1
那个词并不表示在括号内嵌套或单行上的“within”。它描述了一种“蛇形”模式,其中LTR行后跟着RTL行。 - Potatoswatter

7

我怀疑这种结构在现实生活中没有任何用处。我甚至厌恶它们作为常规开发人员的面试问题(编译器编写者可能可以接受)。应该使用typedefs。


3
尽管如此,重要的是要知道如何解析它,即使只是为了知道如何解析typedef! - inetknght
1
@inetknght,使用typedef的方法是使它们足够简单,以便不需要解析。 - SergeyA
2
在面试中问这些类型的问题的人只是为了满足自己的虚荣心。 - Casey
1
@JohnBode,你最好将函数的返回值typedef化。 - SergeyA
1
@JohnBode,我认为这是个人选择的问题,不值得争论。我理解你的偏好,但我仍然保持我的看法。 - SergeyA
显示剩余4条评论

5

我恰巧是许多年前写下螺旋规则的原作者(当时我头发很多 :) ),很荣幸它被添加到了cfaq中。

我写螺旋规则是为了让我的学生和同事更轻松地“在脑中”阅读C语言声明,而不必使用像 cdecl.org 等软件工具。我的初衷从未是要声明螺旋规则是解析C表达式的标准方式。然而,多年来,我很高兴看到这个规则帮助了成千上万的C编程学生和实践者!

供参考:

许多网站包括我非常尊敬的 Linus Torvalds 多次“正确地”指出,有些情况下我的螺旋规则“失效”。最常见的情况是:

char *ar[10][10];

正如本帖其他人所指出的那样,规则可以更新为当您遇到数组时,只需像以下写法一样消耗所有索引:as if

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

现在,按照螺旋规则,我可以得到以下内容:
"ar是一个10x10的二维字符指针数组"
希望这个螺旋规则对学习C语言有所帮助!
附注:
我很喜欢那张"C语言不难"的图片 :)

5

关于其有用性,当使用shellcode时,您会经常看到这种结构:

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

虽然语法上没有那么复杂,但这种模式经常出现。

SO 问题中有更完整的示例。

因此,尽管原始图片的实用性值得怀疑(我建议任何生产代码都应该大幅简化),但确实存在一些常见的语法结构。


5

The declaration

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

只是一种晦涩的说法

Function f[]

使用

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

实际应用中,需要使用更具描述性的名称来代替ResultFunctionFunction。如果可能的话,还应将参数列表指定为void


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接